Grunkbeast spawning & alert response logic

This commit is contained in:
Rob Kelly 2025-04-17 18:47:15 -06:00
parent efc62c16c1
commit fb34e3b659
11 changed files with 151 additions and 22 deletions

File diff suppressed because one or more lines are too long

View File

@ -85,6 +85,8 @@ MetalMaterial="This surface is made of metal."
PlasticMaterial="This surface is made of plastic." PlasticMaterial="This surface is made of plastic."
MeetSpookSource="meet-spook event sources" MeetSpookSource="meet-spook event sources"
LurkPoint="Point which a lurking beast may wander to." LurkPoint="Point which a lurking beast may wander to."
BeastSpawnPoint="Spawn point for a grunkbeast"
GrunkBeast="GrunkBeast instances."
[importer_defaults] [importer_defaults]

View File

@ -1,6 +1,8 @@
class_name SceneTools extends Object class_name SceneTools extends Object
## Tools for specialized operations in a scene ## Tools for specialized operations in a scene
const RAY_MASK := 0b00100101
## Get the Node3D in the given group which is spatially closest to the target node. ## Get the Node3D in the given group which is spatially closest to the target node.
## ##
@ -15,3 +17,39 @@ static func closest_in_group(target: Node3D, group_name: String) -> Node3D:
min_dist_sq = dist_sq min_dist_sq = dist_sq
closest = n closest = n
return closest return closest
## Does the player have an unobstructed line-of-sight to the given point?
static func player_can_see(target: Vector3) -> bool:
if not is_instance_valid(Player.instance):
return false
var camera := Player.instance.get_viewport().get_camera_3d()
if not camera.is_position_in_frustum(target):
return false
var query := PhysicsRayQueryParameters3D.create(camera.global_position, target, RAY_MASK)
# TODO may need to add an "exceptions" parameter for the source's body
var raycast := Player.instance.get_world_3d().direct_space_state.intersect_ray(query)
# If raycast is empty, view is unoccluded and player has line-of-sight!
return not raycast
## Get a random Node3D from the given group which is not currently visible to the player
##
## Returns null is there is no such node, either because the group is empty
## or the player can see every node in the group.
static func pick_unseen_from_group(group_name: String) -> Node3D:
if not is_instance_valid(Player.instance):
return null
var nodes: Array[Node] = Player.instance.get_tree().get_nodes_in_group(group_name)
while nodes:
var candidate: Node = nodes.pick_random()
nodes.erase(candidate)
var target := candidate as Node3D
if target and not SceneTools.player_can_see(target.global_position):
return target
return null

View File

@ -0,0 +1,16 @@
@tool
class_name SetPlayerPriorityTarget extends ActionLeaf
## Set the player as a pursuit target if the grunk alert level is above a priority threshold.
## Blackboard key to set.
@export var blackboard_key := "pursuit_target"
## Set the player as target only if the grunk alert level is greater than or equal to this.
@export var alert_threshold := 3
func tick(_actor: Node, blackboard: Blackboard) -> int:
if Game.manager.alert_level >= alert_threshold and is_instance_valid(Player.instance):
blackboard.set_value(blackboard_key, Player.instance)
return SUCCESS
return FAILURE

View File

@ -0,0 +1 @@
uid://c5cikaa68tsto

View File

@ -8,6 +8,10 @@ const STALKING_SOUND_LIMIT := 20.0
#region Exported Properties #region Exported Properties
@export var base_speed := 60.0 @export var base_speed := 60.0
@export var pursuit_speed := 180.0 @export var pursuit_speed := 180.0
@export var debug_destroy: bool:
set(value):
queue_free()
#endregion #endregion
#region Member Variables #region Member Variables

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=41 format=3 uid="uid://ehf5sg3ahvbf"] [gd_scene load_steps=42 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="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="Shader" uid="uid://ckxc0ngd37rtk" path="res://src/shaders/gunk.gdshader" id="4_0gxpq"]
@ -19,6 +19,7 @@
[ext_resource type="Script" uid="uid://dcojdhvj8qcw0" path="res://addons/beehave/nodes/composites/sequence_reactive.gd" id="12_xde72"] [ext_resource type="Script" uid="uid://dcojdhvj8qcw0" path="res://addons/beehave/nodes/composites/sequence_reactive.gd" id="12_xde72"]
[ext_resource type="Script" uid="uid://b34l3v4sr8rmq" path="res://src/world/grunk_beast/behaviors/actions/set_target_from_area.gd" id="13_x8l6r"] [ext_resource type="Script" uid="uid://b34l3v4sr8rmq" path="res://src/world/grunk_beast/behaviors/actions/set_target_from_area.gd" id="13_x8l6r"]
[ext_resource type="Script" uid="uid://om57w2acvgb7" path="res://src/world/grunk_beast/behaviors/actions/travel_to_destination.gd" id="14_4y64f"] [ext_resource type="Script" uid="uid://om57w2acvgb7" path="res://src/world/grunk_beast/behaviors/actions/travel_to_destination.gd" id="14_4y64f"]
[ext_resource type="Script" uid="uid://c5cikaa68tsto" path="res://src/world/grunk_beast/behaviors/actions/set_player_priority_target.gd" id="14_csisu"]
[ext_resource type="Script" uid="uid://bkdwuqv4tudka" path="res://src/world/grunk_beast/behaviors/actions/pursue_target.gd" id="14_x8l6r"] [ext_resource type="Script" uid="uid://bkdwuqv4tudka" path="res://src/world/grunk_beast/behaviors/actions/pursue_target.gd" id="14_x8l6r"]
[ext_resource type="Script" uid="uid://demv7xh27ouvr" path="res://src/world/grunk_beast/behaviors/actions/blackboard_erase_safe.gd" id="15_4b27i"] [ext_resource type="Script" uid="uid://demv7xh27ouvr" path="res://src/world/grunk_beast/behaviors/actions/blackboard_erase_safe.gd" id="15_4b27i"]
[ext_resource type="Script" uid="uid://dwfdg523bk776" path="res://addons/beehave/nodes/decorators/failer.gd" id="15_oons1"] [ext_resource type="Script" uid="uid://dwfdg523bk776" path="res://addons/beehave/nodes/decorators/failer.gd" id="15_oons1"]
@ -102,7 +103,7 @@ radius = 3.0
[sub_resource type="SphereShape3D" id="SphereShape3D_lak6w"] [sub_resource type="SphereShape3D" id="SphereShape3D_lak6w"]
radius = 10.0 radius = 10.0
[node name="GrunkBeast" type="CharacterBody3D"] [node name="GrunkBeast" type="CharacterBody3D" groups=["GrunkBeast"]]
collision_layer = 36 collision_layer = 36
script = ExtResource("2_qqnhb") script = ExtResource("2_qqnhb")
@ -124,6 +125,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
[node name="NavAgent" type="NavigationAgent3D" parent="Navigation"] [node name="NavAgent" type="NavigationAgent3D" parent="Navigation"]
unique_name_in_owner = true unique_name_in_owner = true
path_postprocessing = 1 path_postprocessing = 1
avoidance_enabled = true
debug_enabled = true debug_enabled = true
[node name="NavProbe" type="NavigationAgent3D" parent="Navigation"] [node name="NavProbe" type="NavigationAgent3D" parent="Navigation"]
@ -213,6 +215,10 @@ metadata/_custom_type_script = "uid://dcojdhvj8qcw0"
script = ExtResource("12_dkcdj") script = ExtResource("12_dkcdj")
metadata/_custom_type_script = "uid://8hn4kne15ac5" metadata/_custom_type_script = "uid://8hn4kne15ac5"
[node name="SetPlayerPriorityTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence/TargetSelector"]
script = ExtResource("14_csisu")
metadata/_custom_type_script = "uid://c5cikaa68tsto"
[node name="TargetInPursuitRange" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence/TargetSelector" node_paths=PackedStringArray("area")] [node name="TargetInPursuitRange" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence/TargetSelector" node_paths=PackedStringArray("area")]
script = ExtResource("9_xuag8") script = ExtResource("9_xuag8")
blackboard_key = "pursuit_target" blackboard_key = "pursuit_target"

View File

@ -3,7 +3,6 @@ class_name MeetSpookMob extends Node3D
enum State { WAITING, FREEZE, MOVING } enum State { WAITING, FREEZE, MOVING }
const RAY_MASK := 0b00100101
const BASE_SPEED := 4.0 const BASE_SPEED := 4.0
@export var source: MeetSpook @export var source: MeetSpook
@ -41,15 +40,8 @@ func _physics_process(delta: float) -> void:
match state: match state:
State.WAITING: State.WAITING:
# Not yet seen by the player # Not yet seen by the player
if camera.is_position_in_frustum(global_position): if SceneTools.player_can_see(global_position):
var query := PhysicsRayQueryParameters3D.create( activate()
camera.global_position, global_position, RAY_MASK
)
# TODO exceptions should include this mob's collision body
var raycast := get_world_3d().direct_space_state.intersect_ray(query)
if not raycast:
# Player has line-of-sight!
activate()
State.FREEZE: State.FREEZE:
# Just stand there for a sec # Just stand there for a sec
pass pass

View File

@ -1,6 +1,10 @@
class_name SpookManager extends Resource class_name SpookManager extends Resource
## A strategy for handling horror elements through the level. ## A strategy for handling horror elements through the level.
const SPAWN_GROUP := "BeastSpawnPoint"
@export var grunkbeast_scene: PackedScene
var debug_set_alert_level: int: var debug_set_alert_level: int:
set = _on_alert_raised set = _on_alert_raised
@ -10,6 +14,35 @@ func _init() -> void:
Game.manager.alert_cleared.connect(_on_alert_cleared) Game.manager.alert_cleared.connect(_on_alert_cleared)
func _spawn_beast_at_point(spawn_point: Node3D) -> void:
print_debug("Spawning active beast at ", spawn_point.global_position)
var beast: GrunkBeast = grunkbeast_scene.instantiate()
spawn_point.add_child(beast)
## Spawn a beast somewhere the player isn't looking.
func spawn_beast() -> void:
var spawn_point := SceneTools.pick_unseen_from_group(SPAWN_GROUP)
if not spawn_point:
print_debug("Couldn't find a hidden spawn point... Picking one at random.")
var nodes := Game.manager.get_tree().get_nodes_in_group(SPAWN_GROUP)
if not nodes:
print_debug("Oh that's why. There aren't any spawn points. Complain to a developer.")
return
spawn_point = nodes.pick_random() as Node3D
_spawn_beast_at_point(spawn_point)
## Spawn beasts at _every_ spawn point the player can't see.
func spawn_many_beasts() -> void:
var nodes := Game.manager.get_tree().get_nodes_in_group(SPAWN_GROUP)
for node: Node in nodes:
var target := node as Node3D
if is_instance_valid(target) and not SceneTools.player_can_see(target.global_position):
_spawn_beast_at_point(target)
func _on_alert_raised(new_level: int) -> void: func _on_alert_raised(new_level: int) -> void:
match new_level: match new_level:
0: 0:
@ -27,15 +60,16 @@ func _on_alert_raised(new_level: int) -> void:
2: 2:
# LEVEL 2: AGGRESSIVE # LEVEL 2: AGGRESSIVE
# Beast pursues player on sight. # Beast pursues player on sight.
pass # TODO spawn_beast()
3: 3:
# LEVEL 3: PREDATORY # LEVEL 3: PREDATORY
# Beast pursues player relentlessly. # Beast pursues player relentlessly.
pass # TODO # The logic for this behavior change is handled directly in the GrunkBeast AI.
pass
4: 4:
# LEVEL 4: SWARMING # LEVEL 4: SWARMING
# Many beasts spawn, overwhelming the player. # Many beasts spawn, overwhelming the player.
pass # TODO spawn_many_beasts()
5: 5:
# LEVEL 5: FUN # LEVEL 5: FUN
# Just kill that fool! # Just kill that fool!

View File

@ -1,12 +1,14 @@
[gd_scene load_steps=6 format=3 uid="uid://884jqafhtrv0"] [gd_scene load_steps=7 format=3 uid="uid://884jqafhtrv0"]
[ext_resource type="Script" uid="uid://cgqmhtemibxc5" path="res://src/world/world.gd" id="1_1k4gi"] [ext_resource type="Script" uid="uid://cgqmhtemibxc5" path="res://src/world/world.gd" id="1_1k4gi"]
[ext_resource type="PackedScene" uid="uid://byvjsvavbg5xe" path="res://src/ui/menus/pause_menu/pause_menu.tscn" id="2_6fy3g"] [ext_resource type="PackedScene" uid="uid://byvjsvavbg5xe" path="res://src/ui/menus/pause_menu/pause_menu.tscn" id="2_6fy3g"]
[ext_resource type="PackedScene" uid="uid://ehf5sg3ahvbf" path="res://src/world/grunk_beast/grunk_beast.tscn" id="2_43c6p"]
[ext_resource type="Script" uid="uid://bsn026pxqwkbc" path="res://src/world/spook_manager/spook_manager.gd" id="2_bsf3i"] [ext_resource type="Script" uid="uid://bsn026pxqwkbc" path="res://src/world/spook_manager/spook_manager.gd" id="2_bsf3i"]
[ext_resource type="PackedScene" uid="uid://bov4ok76woyc" path="res://levels/ghost_ship/ghost_ship.tscn" id="2_jte2u"] [ext_resource type="PackedScene" uid="uid://bov4ok76woyc" path="res://levels/ghost_ship/ghost_ship.tscn" id="2_jte2u"]
[sub_resource type="Resource" id="Resource_43c6p"] [sub_resource type="Resource" id="Resource_43c6p"]
script = ExtResource("2_bsf3i") script = ExtResource("2_bsf3i")
grunkbeast_scene = ExtResource("2_43c6p")
metadata/_custom_type_script = "uid://bsn026pxqwkbc" metadata/_custom_type_script = "uid://bsn026pxqwkbc"
[node name="World" type="Node"] [node name="World" type="Node"]

View File

@ -28,7 +28,7 @@
- [x] Model - [x] Model
- [x] Animation - [x] Animation
- [ ] Controller - [ ] Controller
- [ ] Nav/logic? - [x] Nav/logic?
- [ ] [[level.canvas|level]] - [ ] [[level.canvas|level]]
- [x] level planning - [x] level planning
- [ ] [[player ship]] - [ ] [[player ship]]