generated from krampus/template-godot4
243 lines
6.9 KiB
GDScript
243 lines
6.9 KiB
GDScript
class_name GrunkBeast extends CharacterBody3D
|
|
## Grunk beast controller
|
|
|
|
#region Constants
|
|
const GROUP := "GrunkBeast"
|
|
const ANGER_KEY := "anger_level"
|
|
const POI_KEY := "point_of_interest"
|
|
#endregion
|
|
|
|
#region Exported Properties
|
|
@export var spawn_on_load := false
|
|
@export var speed_curve: Curve
|
|
@export var anger_level: float:
|
|
set = _set_anger_level,
|
|
get = _get_anger_level
|
|
@export var point_of_interest: Vector3:
|
|
set = _set_poi,
|
|
get = _get_poi
|
|
#@export var base_speed := 80.0
|
|
#@export var pursuit_speed := 360.0
|
|
|
|
@export var debug_destroy: bool:
|
|
set(value):
|
|
queue_free()
|
|
#endregion
|
|
|
|
#region Member Variables
|
|
var gravity: Vector3 = (
|
|
ProjectSettings.get_setting("physics/3d/default_gravity")
|
|
* ProjectSettings.get_setting("physics/3d/default_gravity_vector")
|
|
)
|
|
|
|
var pathfinding := true
|
|
|
|
var traversing_link := false
|
|
|
|
@onready var model: BeastModel = %Shambler
|
|
|
|
@onready var nav_agent: NavigationAgent3D = %NavAgent
|
|
@onready var nav_probe: NavigationAgent3D = %NavProbe
|
|
|
|
@onready var spawn_zone_finder: Area3D = %SpawnZoneFinder
|
|
|
|
@onready var sound_detection_cooldown: Timer = %SoundDetectionCooldown
|
|
@onready var touch_cooldown: Timer = %TouchCooldown
|
|
|
|
@onready var blackboard: Blackboard = %Blackboard
|
|
@onready var behavior: BeehaveTree = %GrunkBeastBehavior
|
|
@onready var root_block: AwaitSignal = %RootBlock
|
|
|
|
@onready var debug_canvas_layer: CanvasLayer = %DebugCanvasLayer
|
|
@onready var beast_behavior_label: Label = %BeastBehaviorLabel
|
|
@onready var beast_anger_meter: ProgressBar = %BeastAngerMeter
|
|
#endregion
|
|
|
|
#region Static Variables
|
|
static var anger_min: float = ProjectSettings.get_setting("game/gameplay/beast/anger_min")
|
|
static var anger_max: float = ProjectSettings.get_setting("game/gameplay/beast/anger_max")
|
|
static var anger_decay_rate: float = ProjectSettings.get_setting(
|
|
"game/gameplay/beast/anger_decay_rate"
|
|
)
|
|
static var anger_noise: float = ProjectSettings.get_setting("game/gameplay/beast/anger_noise")
|
|
static var anger_noise_near: float = ProjectSettings.get_setting(
|
|
"game/gameplay/beast/anger_noise_near"
|
|
)
|
|
static var anger_alarm: float = ProjectSettings.get_setting("game/gameplay/beast/anger_alarm")
|
|
static var anger_alarm_extra: float = ProjectSettings.get_setting(
|
|
"game/gameplay/beast/anger_alarm_extra"
|
|
)
|
|
static var anger_touch: float = ProjectSettings.get_setting("game/gameplay/beast/anger_touch")
|
|
static var anger_attack: float = ProjectSettings.get_setting("game/gameplay/beast/anger_attack")
|
|
static var anger_sticker: float = ProjectSettings.get_setting("game/gameplay/beast/anger_sticker")
|
|
static var provocation_range: float = ProjectSettings.get_setting(
|
|
"game/gameplay/beast/provocation_range"
|
|
)
|
|
static var anger_extra_alert_level: float = ProjectSettings.get_setting(
|
|
"game/gameplay/beast/anger_extra_alert_level"
|
|
)
|
|
#endregion
|
|
|
|
|
|
#region Member Access
|
|
func _get_anger_level() -> float:
|
|
return blackboard.get_value(ANGER_KEY, 0.0)
|
|
|
|
|
|
func _set_anger_level(value: float) -> void:
|
|
blackboard.set_value(ANGER_KEY, clampf(value, GrunkBeast.anger_min, GrunkBeast.anger_max))
|
|
|
|
|
|
func _get_poi() -> Vector3:
|
|
return blackboard.get_value(POI_KEY, Vector3.ZERO)
|
|
|
|
|
|
func _set_poi(value: Vector3) -> void:
|
|
blackboard.set_value(POI_KEY, value)
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region Character Controller
|
|
func _ready() -> void:
|
|
nav_agent.velocity_computed.connect(_on_velocity_computed)
|
|
if World.instance and World.instance.manager:
|
|
World.instance.manager.alarm_triggered.connect(_on_alarm_triggered)
|
|
if spawn_on_load:
|
|
start_spawn()
|
|
|
|
|
|
func start_spawn() -> void:
|
|
model.play_spawn_animation()
|
|
root_block.await_signal(model.spawn_animation_finished)
|
|
# Set point of interest to spawn point
|
|
point_of_interest = global_position
|
|
|
|
|
|
func get_spawn_zones() -> Array[BeastSpawnZone]:
|
|
var zones: Array[BeastSpawnZone] = []
|
|
zones.assign(spawn_zone_finder.get_overlapping_areas())
|
|
return zones
|
|
|
|
|
|
func get_speed() -> float:
|
|
return speed_curve.sample(anger_level)
|
|
|
|
|
|
func path_shorter_than(target: Vector3, limit: float) -> bool:
|
|
var length := 0.0
|
|
var last_pos := global_position
|
|
|
|
nav_probe.target_position = target
|
|
|
|
# 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(
|
|
nav_probe.get_current_navigation_path_index()
|
|
)
|
|
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:
|
|
return false
|
|
last_pos = waypoint
|
|
|
|
return true
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
# Update debug info
|
|
if debug_canvas_layer.visible:
|
|
beast_behavior_label.text = str(blackboard.get_value("current_mode"))
|
|
beast_anger_meter.value = anger_level
|
|
|
|
# Don't use nav agent before map has synchronized
|
|
if NavigationServer3D.map_get_iteration_id(nav_agent.get_navigation_map()) == 0:
|
|
return
|
|
|
|
var motion := Vector3.ZERO
|
|
|
|
if pathfinding and (traversing_link or not nav_agent.is_navigation_finished()):
|
|
motion = global_position.direction_to(nav_agent.get_next_path_position()) * get_speed()
|
|
|
|
motion.y = velocity.y
|
|
|
|
if not is_on_floor():
|
|
motion += gravity
|
|
|
|
if motion.x or motion.z:
|
|
model.set_target_rotation(atan2(motion.x, motion.z))
|
|
|
|
if nav_agent.avoidance_enabled:
|
|
nav_agent.set_velocity(motion)
|
|
else:
|
|
_on_velocity_computed(motion)
|
|
|
|
|
|
func _on_velocity_computed(safe_velocity: Vector3) -> void:
|
|
model.set_move_speed(safe_velocity.length())
|
|
velocity = safe_velocity
|
|
move_and_slide()
|
|
|
|
|
|
func on_sound_detected(source: Vector3) -> void:
|
|
if root_block.is_blocked():
|
|
return
|
|
|
|
point_of_interest = source
|
|
if sound_detection_cooldown.is_stopped():
|
|
print_debug("Beast heard something from ", source)
|
|
anger_level += GrunkBeast.anger_noise
|
|
if source.distance_to(self.global_position) <= GrunkBeast.provocation_range:
|
|
print_debug("... And it was close, too!")
|
|
anger_level += GrunkBeast.anger_noise_near
|
|
sound_detection_cooldown.start()
|
|
# TODO animation?
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
func _on_link_reached(_details: Dictionary) -> void:
|
|
traversing_link = true
|
|
|
|
|
|
func _on_waypoint_reached(_details: Dictionary) -> void:
|
|
traversing_link = false
|
|
|
|
|
|
func _anger_decay() -> void:
|
|
if not root_block.is_blocked():
|
|
anger_level -= GrunkBeast.anger_decay_rate
|
|
|
|
|
|
func _on_alarm_triggered(source: GunkAlarm) -> void:
|
|
if root_block.is_blocked():
|
|
return
|
|
|
|
print_debug("The beast was angered by the alarm!")
|
|
point_of_interest = source.global_position
|
|
anger_level += GrunkBeast.anger_alarm
|
|
if World.instance.manager.alert_level >= GrunkBeast.anger_extra_alert_level:
|
|
print_debug("The beast got extra-angry!")
|
|
anger_level += GrunkBeast.anger_alarm_extra
|
|
|
|
|
|
func _on_touch(_body: Node3D) -> void:
|
|
if root_block.is_blocked():
|
|
return
|
|
|
|
if touch_cooldown.is_stopped():
|
|
print_debug("Touched the beast!")
|
|
anger_level += GrunkBeast.anger_touch
|
|
touch_cooldown.start()
|