grunk/src/world/grunk_beast/grunk_beast.gd

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()