grunk/src/world/grunk_beast/grunk_beast.gd

180 lines
3.8 KiB
GDScript

class_name GrunkBeast extends CharacterBody3D
## Grunk beast controller
#region Constants
signal state_changed(new_state: State)
enum State {
UNKNOWN,
LURKING,
STALKING,
PURSUIT,
}
const LURK_POINT_GROUP := "LurkPoint"
const STALKING_SOUND_LIMIT := 14.0
const IDLE_EPSILON := 0.001
#endregion
#region Exported Properties
@export var lurk_spontaneity := 0.02
@export var base_speed := 60.0
@export var pursuit_speed := 180.0
@export var initial_state := State.LURKING
#endregion
#region Member Variables
var gravity: Vector3 = (
ProjectSettings.get_setting("physics/3d/default_gravity")
* ProjectSettings.get_setting("physics/3d/default_gravity_vector")
)
var state: State:
set(value):
if state != value:
_on_state_change(value)
state = value
var pathfinding := true
@onready var nav_agent: NavigationAgent3D = %NavAgent
@onready var stalking_timeout: Timer = %StalkingTimeout
@onready var idle_timer: Timer = %IdleTimer
#endregion
#region Character Controller
func get_speed() -> float:
match state:
State.PURSUIT:
return pursuit_speed
return base_speed
func _physics_process(delta: float) -> void:
var motion := Vector3.ZERO
if pathfinding:
var path_pos := nav_agent.get_next_path_position()
var relative_pos := path_pos - global_position
motion = relative_pos.normalized() * get_speed() * delta
velocity.x = motion.x
velocity.z = motion.z
if not is_on_floor():
velocity += gravity * delta
if velocity.length_squared() > IDLE_EPSILON:
idle_timer.start()
move_and_slide()
#endregion
#region State Machine
func _ready() -> void:
state = initial_state
func _current_path_shorter_than(limit: float) -> bool:
var limit_sq := limit * limit
var length_sq := 0.0
var last_pos := global_position
for waypoint: Vector3 in nav_agent.get_current_navigation_path().slice(
nav_agent.get_current_navigation_path_index()
):
# Using distance squared to save a sqrt instruction
length_sq += last_pos.distance_squared_to(waypoint)
if length_sq > limit_sq:
return false
last_pos = waypoint
return true
func _set_new_lurk_point() -> void:
@warning_ignore("unsafe_cast")
var next_lurk_point := get_tree().get_nodes_in_group(LURK_POINT_GROUP).pick_random() as Node3D
if next_lurk_point:
nav_agent.target_position = next_lurk_point.global_position
else:
push_warning(self, " tried to lurk, but couldn't find a lurk point!")
## Lurking routine
func lurk() -> void:
if nav_agent.is_target_reached() or randf() < lurk_spontaneity:
_set_new_lurk_point()
## Stalking routine
func stalk() -> void:
pass
## Pursuit routine
func pursue() -> void:
if Player.instance:
nav_agent.target_position = Player.instance.global_position
func _on_state_change(new_state: State) -> void:
match new_state:
State.LURKING:
_set_new_lurk_point()
State.STALKING:
stalking_timeout.start()
state_changed.emit(new_state)
func update_behavior() -> void:
match state:
State.UNKNOWN:
state = State.LURKING
update_behavior()
State.LURKING:
lurk()
State.STALKING:
stalk()
State.PURSUIT:
pursue()
func on_sound_detected(source: Vector3) -> void:
var prev_target := nav_agent.target_position
nav_agent.target_position = source
# Check that the source isn't too far away, e.g. a sound from another room
if _current_path_shorter_than(STALKING_SOUND_LIMIT):
# First detection switches to stalking, second switches to pursuit
if state == State.STALKING:
state = State.PURSUIT
else:
state = State.STALKING
else:
nav_agent.target_position = prev_target
func on_player_entered_grabbing_range(_body: Node3D) -> void:
print_debug("GET GRABBED, IDIOT!")
func on_player_left_pursuit_range(_body: Node3D) -> void:
state = State.STALKING
func on_stalking_timeout_timeout() -> void:
state = State.LURKING
func on_stalking_idle_timer_timeout() -> void:
pass # Replace with function body.
#endregion