diff --git a/levels/pathing_test/pathing_test.tscn b/levels/pathing_test/pathing_test.tscn index 20a4d88..46a060d 100644 --- a/levels/pathing_test/pathing_test.tscn +++ b/levels/pathing_test/pathing_test.tscn @@ -550,5 +550,11 @@ source_gunk_material = ExtResource("14_olej6") [node name="CollisionShape3D" type="CollisionShape3D" parent="NavigationRegion3D/Corridor1/Hallway4/MessHallSign/GunkBody"] shape = SubResource("ConcavePolygonShape3D_spe8j") +[node name="LurkPoint" type="Marker3D" parent="NavigationRegion3D/Corridor1" groups=["LurkPoint"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 4.5) + +[node name="LurkPoint2" type="Marker3D" parent="NavigationRegion3D/Corridor1" groups=["LurkPoint"]] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -27, 1.5, -19.5) + [connection signal="activated" from="NavigationRegion3D/Corridor1/Hallway1/SwitchA2" to="NavigationRegion3D/Corridor1/Hallway1/SwitchA2" method="disable"] [connection signal="activated" from="NavigationRegion3D/Corridor1/Hallway4/SwitchB1" to="NavigationRegion3D/Corridor1/Hallway4/SwitchB1" method="disable"] diff --git a/project.godot b/project.godot index ca87287..b33b0d7 100644 --- a/project.godot +++ b/project.godot @@ -81,6 +81,7 @@ config/input/hold_to_sneak=true MetalMaterial="This surface is made of metal." PlasticMaterial="This surface is made of plastic." MeetSpookSource="meet-spook event sources" +LurkPoint="Point which a lurking beast may wander to." [importer_defaults] diff --git a/src/world/grunk_beast/grunk_beast.gd b/src/world/grunk_beast/grunk_beast.gd index 9c9652f..363e6ca 100644 --- a/src/world/grunk_beast/grunk_beast.gd +++ b/src/world/grunk_beast/grunk_beast.gd @@ -1,30 +1,179 @@ class_name GrunkBeast extends CharacterBody3D ## Grunk beast controller -@export var base_speed := 1.0 +#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 path_pos := nav_agent.get_next_path_position() + 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 - var relative_pos := path_pos - global_position - var motion := relative_pos.normalized() * base_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() -func track_player() -> void: +#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 diff --git a/src/world/grunk_beast/grunk_beast.tscn b/src/world/grunk_beast/grunk_beast.tscn index 787759d..c09831f 100644 --- a/src/world/grunk_beast/grunk_beast.tscn +++ b/src/world/grunk_beast/grunk_beast.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=12 format=3 uid="uid://ehf5sg3ahvbf"] +[gd_scene load_steps=16 format=3 uid="uid://ehf5sg3ahvbf"] [ext_resource type="Script" uid="uid://gwwmqwixqqr5" path="res://src/world/grunk_beast/grunk_beast.gd" id="2_qqnhb"] [ext_resource type="Shader" uid="uid://ckxc0ngd37rtk" path="res://src/shaders/gunk.gdshader" id="4_0gxpq"] +[ext_resource type="Script" uid="uid://cfsiyhhrcua6o" path="res://src/world/game_sound/game_sound_listener.gd" id="5_3gbao"] [ext_resource type="Texture2D" uid="uid://cm1jrvx7ftx4c" path="res://assets/black.png" id="5_xuag8"] [ext_resource type="FastNoiseLite" uid="uid://cnlvdtx68giv6" path="res://assets/materials/gunk_noise.tres" id="6_mbqcc"] @@ -63,9 +64,17 @@ rings = 1 [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_faau1"] radius = 0.45 +[sub_resource type="SphereShape3D" id="SphereShape3D_wffas"] +radius = 7.0 + +[sub_resource type="SphereShape3D" id="SphereShape3D_3gbao"] +radius = 1.4 + +[sub_resource type="SphereShape3D" id="SphereShape3D_d4ex2"] +radius = 20.0 + [node name="GrunkBeast" type="CharacterBody3D"] script = ExtResource("2_qqnhb") -base_speed = 60.0 [node name="MeshInstance3D" type="MeshInstance3D" parent="."] mesh = SubResource("CapsuleMesh_d4ex2") @@ -82,7 +91,44 @@ shape = SubResource("CapsuleShape3D_faau1") [node name="NavAgent" type="NavigationAgent3D" parent="."] unique_name_in_owner = true -[node name="TrackingTimer" type="Timer" parent="."] +[node name="IdleTimer" type="Timer" parent="."] +unique_name_in_owner = true +wait_time = 1.2 +one_shot = true + +[node name="StalkingTimeout" type="Timer" parent="."] +unique_name_in_owner = true +wait_time = 25.0 +one_shot = true + +[node name="BehaviorUpdateTimer" type="Timer" parent="."] autostart = true -[connection signal="timeout" from="TrackingTimer" to="." method="track_player"] +[node name="GameSoundListener" type="StaticBody3D" parent="."] +collision_layer = 16 +collision_mask = 0 +script = ExtResource("5_3gbao") +metadata/_custom_type_script = "uid://cfsiyhhrcua6o" + +[node name="CollisionShape3D" type="CollisionShape3D" parent="GameSoundListener"] +shape = SubResource("SphereShape3D_wffas") + +[node name="GrabbingRange" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 8 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="GrabbingRange"] +shape = SubResource("SphereShape3D_3gbao") + +[node name="PursuitLimit" type="Area3D" parent="."] +collision_layer = 0 +collision_mask = 8 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="PursuitLimit"] +shape = SubResource("SphereShape3D_d4ex2") + +[connection signal="timeout" from="StalkingTimeout" to="." method="on_stalking_timeout_timeout"] +[connection signal="timeout" from="BehaviorUpdateTimer" to="." method="update_behavior"] +[connection signal="sound_detected" from="GameSoundListener" to="." method="on_sound_detected"] +[connection signal="body_entered" from="GrabbingRange" to="." method="on_player_entered_grabbing_range"] +[connection signal="body_exited" from="PursuitLimit" to="." method="on_player_left_pursuit_range"] diff --git a/src/world/procedural_grunk_beast/ik_target.gd b/src/world/procedural_grunk_beast/ik_target.gd index b0d784d..1a71a63 100644 --- a/src/world/procedural_grunk_beast/ik_target.gd +++ b/src/world/procedural_grunk_beast/ik_target.gd @@ -10,7 +10,7 @@ const CRITICAL_ANGLE := 0.1 var stepping := false var _resting := false -@onready var parent := owner as GrunkBeast +@onready var parent := owner as ProceduralGrunkBeast func step() -> void: diff --git a/src/world/procedural_grunk_beast/target_container.gd b/src/world/procedural_grunk_beast/target_container.gd index b1467cf..72117ad 100644 --- a/src/world/procedural_grunk_beast/target_container.gd +++ b/src/world/procedural_grunk_beast/target_container.gd @@ -1,6 +1,6 @@ extends Node3D -@onready var parent := owner as GrunkBeast +@onready var parent := owner as ProceduralGrunkBeast @onready var prev_position := parent.global_position