2025-04-16 17:20:33 -06:00
|
|
|
class_name GrunkBeast extends CharacterBody3D
|
|
|
|
## Grunk beast controller
|
|
|
|
|
2025-04-17 10:02:01 -06:00
|
|
|
#region Constants
|
2025-04-18 09:53:21 -06:00
|
|
|
const STALKING_SOUND_LIMIT := 25.0
|
2025-04-17 10:02:01 -06:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Exported Properties
|
2025-09-02 18:41:27 -06:00
|
|
|
@export var play_spawn_animation := true
|
2025-08-30 18:42:35 -06:00
|
|
|
@export var base_speed := 80.0
|
|
|
|
@export var pursuit_speed := 360.0
|
2025-04-17 18:47:15 -06:00
|
|
|
|
|
|
|
@export var debug_destroy: bool:
|
|
|
|
set(value):
|
|
|
|
queue_free()
|
2025-04-17 10:02:01 -06:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Member Variables
|
2025-04-16 17:20:33 -06:00
|
|
|
var gravity: Vector3 = (
|
|
|
|
ProjectSettings.get_setting("physics/3d/default_gravity")
|
|
|
|
* ProjectSettings.get_setting("physics/3d/default_gravity_vector")
|
|
|
|
)
|
|
|
|
|
2025-04-17 10:02:01 -06:00
|
|
|
var pathfinding := true
|
|
|
|
|
2025-04-18 09:53:21 -06:00
|
|
|
var traversing_link := false
|
|
|
|
|
2025-08-30 18:42:35 -06:00
|
|
|
@onready var model: BeastModel = %Shambler
|
2025-04-18 09:53:21 -06:00
|
|
|
|
2025-04-16 17:20:33 -06:00
|
|
|
@onready var nav_agent: NavigationAgent3D = %NavAgent
|
2025-04-17 13:41:06 -06:00
|
|
|
@onready var nav_probe: NavigationAgent3D = %NavProbe
|
|
|
|
@onready var stalking_timer: Timer = %StalkingTimer
|
2025-04-16 17:20:33 -06:00
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
@onready var blackboard: Blackboard = %Blackboard
|
2025-09-02 19:12:40 -06:00
|
|
|
@onready var behavior: BeehaveTree = %GrunkBeastBehavior
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
#endregion
|
2025-04-16 17:20:33 -06:00
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
#region Character Controller
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-16 17:20:33 -06:00
|
|
|
|
2025-09-02 18:41:27 -06:00
|
|
|
func _ready() -> void:
|
|
|
|
if play_spawn_animation:
|
|
|
|
model.play_spawn_animation()
|
2025-09-02 19:12:40 -06:00
|
|
|
behavior.disable()
|
|
|
|
model.spawn_animation_finished.connect(behavior.enable)
|
2025-09-02 18:41:27 -06:00
|
|
|
|
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
func is_pursuing() -> bool:
|
|
|
|
return blackboard.has_value("pursuit_target")
|
2025-04-16 17:20:33 -06:00
|
|
|
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
func is_stalking() -> bool:
|
|
|
|
return false # TODO
|
2025-04-17 10:02:01 -06:00
|
|
|
|
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
func get_speed() -> float:
|
|
|
|
if is_pursuing():
|
|
|
|
return pursuit_speed
|
|
|
|
return base_speed
|
2025-04-17 10:02:01 -06:00
|
|
|
|
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
func path_shorter_than(target: Vector3, limit: float) -> bool:
|
2025-04-17 16:26:41 -06:00
|
|
|
var length := 0.0
|
2025-04-17 10:02:01 -06:00
|
|
|
var last_pos := global_position
|
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
nav_probe.target_position = target
|
|
|
|
|
2025-04-17 16:26:41 -06:00
|
|
|
# Fail early if the target is unreachable.
|
|
|
|
# NOTE: this call also forces a navigation path refresh! Do not remove!
|
|
|
|
if not nav_probe.is_target_reachable():
|
|
|
|
return false
|
|
|
|
|
|
|
|
var path := nav_probe.get_current_navigation_path().slice(
|
2025-04-17 13:41:06 -06:00
|
|
|
nav_probe.get_current_navigation_path_index()
|
2025-04-17 16:26:41 -06:00
|
|
|
)
|
|
|
|
if not path:
|
|
|
|
# Shouldn't be possible, but if it is it would cause problems if we didn't fail here
|
|
|
|
print_debug("Target is reachable but has no path (tell the developer!)")
|
|
|
|
return false
|
|
|
|
|
|
|
|
# Integrate along path
|
|
|
|
for waypoint: Vector3 in path:
|
|
|
|
length += last_pos.distance_to(waypoint)
|
|
|
|
if length > limit:
|
2025-04-17 10:02:01 -06:00
|
|
|
return false
|
|
|
|
last_pos = waypoint
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
2025-04-20 02:15:46 -06:00
|
|
|
## Clear this beast's pursuit and stalking targets
|
|
|
|
func clear_aggro() -> void:
|
|
|
|
blackboard.erase_value("pursuit_target")
|
|
|
|
blackboard.erase_value("stalking_target")
|
|
|
|
|
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
func _physics_process(delta: float) -> void:
|
|
|
|
var motion := Vector3.ZERO
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-17 17:06:07 -06:00
|
|
|
if pathfinding and not nav_agent.is_navigation_finished():
|
2025-04-17 13:41:06 -06:00
|
|
|
var path_pos := nav_agent.get_next_path_position()
|
|
|
|
var relative_pos := path_pos - global_position
|
|
|
|
motion = relative_pos.normalized() * get_speed() * delta
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
velocity.x = motion.x
|
|
|
|
velocity.z = motion.z
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-17 13:41:06 -06:00
|
|
|
if not is_on_floor():
|
|
|
|
velocity += gravity * delta
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-18 09:53:21 -06:00
|
|
|
if motion:
|
|
|
|
model.set_target_rotation(atan2(motion.x, motion.z))
|
|
|
|
|
2025-08-30 18:42:35 -06:00
|
|
|
model.set_move_speed(velocity.length())
|
2025-04-17 13:41:06 -06:00
|
|
|
move_and_slide()
|
2025-04-17 10:02:01 -06:00
|
|
|
|
|
|
|
|
|
|
|
func on_sound_detected(source: Vector3) -> void:
|
|
|
|
# Check that the source isn't too far away, e.g. a sound from another room
|
2025-09-02 19:12:40 -06:00
|
|
|
if behavior.enabled and path_shorter_than(source, STALKING_SOUND_LIMIT):
|
2025-04-17 13:41:06 -06:00
|
|
|
blackboard.set_value("stalking_target", source)
|
|
|
|
stalking_timer.start()
|
2025-04-17 10:02:01 -06:00
|
|
|
|
2025-04-18 09:53:21 -06:00
|
|
|
|
2025-04-17 10:02:01 -06:00
|
|
|
#endregion
|
2025-04-18 09:53:21 -06:00
|
|
|
|
|
|
|
|
|
|
|
func _on_link_reached(_details: Dictionary) -> void:
|
|
|
|
traversing_link = true
|
|
|
|
|
|
|
|
|
|
|
|
func _on_waypoint_reached(_details: Dictionary) -> void:
|
|
|
|
traversing_link = false
|