Meet-spook tech

This commit is contained in:
Rob Kelly 2025-04-10 18:29:00 -06:00
parent b04f45b22b
commit 0c10a95cbf
22 changed files with 639 additions and 11 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
[gd_scene load_steps=4 format=3 uid="uid://8m6kh4f6pily"]
[ext_resource type="Script" uid="uid://1trqaximxxmx" path="res://src/world/meet_spook/meet_spook_mob.gd" id="1_545xt"]
[ext_resource type="Material" uid="uid://dutkfm4ek0ysh" path="res://assets/materials/gunk_bright.material" id="2_2vvgf"]
[sub_resource type="CapsuleMesh" id="CapsuleMesh_k8gi4"]
material = ExtResource("2_2vvgf")
[node name="DebugMob" type="Node3D"]
script = ExtResource("1_545xt")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("CapsuleMesh_k8gi4")
[node name="Label3D" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
billboard = 2
text = "SPOOKY!"

View File

@ -0,0 +1,98 @@
[gd_scene load_steps=15 format=3 uid="uid://1kreuo8tc4jv"]
[ext_resource type="PackedScene" uid="uid://bwe2jdmvinhqd" path="res://src/player/player.tscn" id="1_f3l4i"]
[ext_resource type="PackedScene" uid="uid://b6eg8t04rkh0c" path="res://src/props/wall_switch/wall_switch.tscn" id="2_eywuc"]
[ext_resource type="Script" uid="uid://cvx514gdjd5ev" path="res://src/world/meet_spook/meet_spook.gd" id="4_b6st5"]
[ext_resource type="PackedScene" uid="uid://8m6kh4f6pily" path="res://levels/meet_spook_test/debug_mob.tscn" id="5_cr4pm"]
[ext_resource type="Script" uid="uid://cisuvlqvqfqgo" path="res://src/world/meet_spook/meet_spook_spawner.gd" id="5_tak24"]
[ext_resource type="Script" uid="uid://c1gy2b2wcq127" path="res://levels/meet_spook_test/tween_debug.gd" id="6_cr4pm"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_eywuc"]
[sub_resource type="Sky" id="Sky_pka60"]
sky_material = SubResource("ProceduralSkyMaterial_eywuc")
[sub_resource type="Environment" id="Environment_nynr7"]
background_mode = 2
sky = SubResource("Sky_pka60")
[sub_resource type="PlaneMesh" id="PlaneMesh_b6st5"]
size = Vector2(50, 50)
[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_tak24"]
[sub_resource type="BoxMesh" id="BoxMesh_nynr7"]
size = Vector3(0.5, 2, 0.5)
[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_b6st5"]
data = PackedVector3Array(-0.25, 1, 0.25, 0.25, 1, 0.25, -0.25, -1, 0.25, 0.25, 1, 0.25, 0.25, -1, 0.25, -0.25, -1, 0.25, 0.25, 1, -0.25, -0.25, 1, -0.25, 0.25, -1, -0.25, -0.25, 1, -0.25, -0.25, -1, -0.25, 0.25, -1, -0.25, 0.25, 1, 0.25, 0.25, 1, -0.25, 0.25, -1, 0.25, 0.25, 1, -0.25, 0.25, -1, -0.25, 0.25, -1, 0.25, -0.25, 1, -0.25, -0.25, 1, 0.25, -0.25, -1, -0.25, -0.25, 1, 0.25, -0.25, -1, 0.25, -0.25, -1, -0.25, 0.25, 1, 0.25, -0.25, 1, 0.25, 0.25, 1, -0.25, -0.25, 1, 0.25, -0.25, 1, -0.25, 0.25, 1, -0.25, -0.25, -1, 0.25, 0.25, -1, 0.25, -0.25, -1, -0.25, 0.25, -1, 0.25, 0.25, -1, -0.25, -0.25, -1, -0.25)
[sub_resource type="SphereMesh" id="SphereMesh_tak24"]
[node name="MeetSpookTest" type="Node3D"]
[node name="Player" parent="." instance=ExtResource("1_f3l4i")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866025, -0.156955, -0.474726, 0.5, 0.271854, 0.82225, 0, -0.949453, 0.31391, 0, 0, 0)
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_nynr7")
[node name="WorldFloor" type="StaticBody3D" parent="." groups=["PlasticMaterial"]]
[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldFloor"]
mesh = SubResource("PlaneMesh_b6st5")
skeleton = NodePath("../..")
[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldFloor"]
shape = SubResource("WorldBoundaryShape3D_tak24")
[node name="TriggerPodium" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, -2)
[node name="MeshInstance3D" type="MeshInstance3D" parent="TriggerPodium"]
mesh = SubResource("BoxMesh_nynr7")
[node name="CollisionShape3D" type="CollisionShape3D" parent="TriggerPodium"]
shape = SubResource("ConcavePolygonShape3D_b6st5")
[node name="TriggerSwitch" parent="TriggerPodium" instance=ExtResource("2_eywuc")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0.35)
clean = true
[node name="Label3D" type="Label3D" parent="TriggerPodium"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0.3)
text = "GET SCARED"
[node name="MeetSpook" type="Marker3D" parent="." node_paths=PackedStringArray("spawners")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3, 1.5, 0)
script = ExtResource("4_b6st5")
mob_scene = ExtResource("5_cr4pm")
spawners = [NodePath("../MeetSpookSpawner"), NodePath("../MeetSpookSpawner2")]
metadata/_custom_type_script = "uid://cvx514gdjd5ev"
[node name="MeetSpookSpawner" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 6)
script = ExtResource("5_tak24")
metadata/_custom_type_script = "uid://cisuvlqvqfqgo"
[node name="Target" type="Marker3D" parent="MeetSpookSpawner"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 0, 0)
[node name="MeetSpookSpawner2" type="Marker3D" parent="."]
transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, 9, 1.5, 0)
script = ExtResource("5_tak24")
metadata/_custom_type_script = "uid://cisuvlqvqfqgo"
[node name="Target" type="Marker3D" parent="MeetSpookSpawner2"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 0, 0)
[node name="TweenDebug" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -4.5)
mesh = SubResource("SphereMesh_tak24")
script = ExtResource("6_cr4pm")
[connection signal="activated" from="TriggerPodium/TriggerSwitch" to="MeetSpook" method="prepare"]
[connection signal="activated" from="TriggerPodium/TriggerSwitch" to="TweenDebug" method="activate"]

View File

@ -0,0 +1,19 @@
extends MeshInstance3D
@export var movement := Vector3(3, 0, 0)
@export var duration := 2.0
@export var transition_type := Tween.TRANS_SINE
@export var ease_type := Tween.EASE_IN
@onready var start_position := position
func activate() -> void:
var end_position := start_position + movement
position = start_position
(
create_tween()
. tween_property(self, "position", end_position, duration)
. set_trans(transition_type)
. set_ease(ease_type)
)

View File

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

View File

@ -59,7 +59,10 @@ enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg", "res://addon
folder_colors={ folder_colors={
"res://assets/": "red", "res://assets/": "red",
"res://levels/": "blue", "res://levels/": "blue",
"res://src/": "green" "res://levels/ghost_ship/": "purple",
"res://src/": "green",
"res://src/ui/": "yellow",
"res://src/world/": "teal"
} }
[game] [game]
@ -77,6 +80,7 @@ config/input/hold_to_sneak=true
MetalMaterial="This surface is made of metal." 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"
[importer_defaults] [importer_defaults]

View File

@ -1,6 +1,8 @@
class_name GameManagerType extends Node class_name GameManagerType extends Node
## Autoloaded singleton encapsulating game state. ## Autoloaded singleton encapsulating game state.
# TODO a lot of this should really be a property of the world.
## Emitted just after `delta` is added to the player's grunk tank. ## Emitted just after `delta` is added to the player's grunk tank.
signal grunk_collected(delta: float) signal grunk_collected(delta: float)
@ -27,6 +29,10 @@ const MAX_ALERT := 6
## Grunk collection milestones ## Grunk collection milestones
@export var vault_milestones: Dictionary[int, Milestone] @export var vault_milestones: Dictionary[int, Milestone]
var debug_emit_alert_signal: int:
set(value):
alert_raised.emit(value)
## Amount of grunk the player is currently carrying. ## Amount of grunk the player is currently carrying.
var grunk_tank := 0.0 var grunk_tank := 0.0

17
src/util/scene_tools.gd Normal file
View File

@ -0,0 +1,17 @@
class_name SceneTools extends Object
## Tools for specialized operations in a scene
## Get the Node3D in the given group which is spatially closest to the target node.
##
## If there is no such node, e.g. if the group is empty, the result will be null.
static func closest_in_group(target: Node3D, group_name: String) -> Node3D:
var closest: Node3D
var min_dist_sq := INF
for n: Node in target.get_tree().get_nodes_in_group(group_name):
if n is Node3D:
var dist_sq := (n as Node3D).global_position.distance_squared_to(target.global_position)
if dist_sq < min_dist_sq:
min_dist_sq = dist_sq
closest = n
return closest

View File

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

View File

@ -0,0 +1,29 @@
class_name MeetSpook extends Marker3D
## Source for a "meet-spook" event.
##
## When the event conditions are met, the MeetSpook closest to the player prepares contextual
## resources for the event.
const GROUP := "MeetSpookSource"
@export var mob_scene: PackedScene
@export var spawners: Array[MeetSpookSpawner] = []
var event_prepared := false
func prepare() -> void:
if event_prepared:
return
for spawner: MeetSpookSpawner in spawners:
var instance: MeetSpookMob = mob_scene.instantiate()
instance.source = self
spawner.spawn(instance)
event_prepared = true
func resolve() -> void:
for spawner: MeetSpookSpawner in spawners:
spawner.resolve()
event_prepared = false

View File

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

View File

@ -0,0 +1,62 @@
class_name MeetSpookMob extends Node3D
## Mob spawned and controlled by a meet-spook event
enum State { WAITING, FREEZE, MOVING }
const RAY_MASK := 0b00000101
const BASE_SPEED := 6.0
@export var source: MeetSpook
@export var target: Node3D
@export var freeze_time: float
@export var lifespan: float
var state := State.WAITING
@onready var camera := get_viewport().get_camera_3d()
func activate() -> void:
print_debug(self, " spotted!")
state = State.FREEZE
get_tree().create_timer(freeze_time).timeout.connect(_unfreeze)
get_tree().create_timer(lifespan).timeout.connect(destroy)
source.resolve()
func is_active() -> bool:
return state != State.WAITING
func move_toward_target(delta: float) -> void:
# Should probably be overridden by implementations
global_position = global_position.move_toward(target.global_position, BASE_SPEED * delta)
func _unfreeze() -> void:
state = State.MOVING
func _physics_process(delta: float) -> void:
match state:
State.WAITING:
# Not yet seen by the player
if camera.is_position_in_frustum(global_position):
var query := PhysicsRayQueryParameters3D.create(
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:
# Just stand there for a sec
pass
State.MOVING:
move_toward_target(delta)
func destroy() -> void:
print_debug("Destroying ", self)
queue_free()

View File

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

View File

@ -0,0 +1,34 @@
class_name MeetSpookSpawner extends Marker3D
## Spawner which is part of a MeetSpook event
## Time to freeze the mob before moving after being spotted, in seconds.
@export var freeze_time := 0.4
## Time after which the mob will despawn after being spotted, in seconds.
@export var mob_lifespan := 1.0
var _instance: MeetSpookMob
func get_target() -> Marker3D:
for c: Node in get_children():
if c is Marker3D:
return c
return null
func spawn(instance: MeetSpookMob) -> void:
if is_instance_valid(_instance):
_instance.queue_free()
_instance = instance
_instance.target = get_target()
_instance.freeze_time = freeze_time
_instance.lifespan = mob_lifespan
add_child(_instance)
_instance.global_transform = global_transform
func resolve() -> void:
## Despawn if instance is not active
if is_instance_valid(_instance) and not _instance.is_active():
_instance.queue_free()

View File

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

View File

@ -0,0 +1,46 @@
class_name SpookManager extends Resource
## A strategy for handling horror elements through the level.
var debug_set_alert_level: int:
set = _on_alert_raised
func _init() -> void:
Game.manager.alert_raised.connect(_on_alert_raised)
Game.manager.alert_cleared.connect(_on_alert_cleared)
func _on_alert_raised(new_level: int) -> void:
match new_level:
0:
# LEVEL 0: UNAWARE
pass
1:
# LEVEL 1: PASSIVE
# Beast avoids the player.
# Set up meet-spook.
print_debug("Preparing meet-spook...")
# Get closest MeetSpook point to player.
if Player.instance:
var closest := SceneTools.closest_in_group(Player.instance, MeetSpook.GROUP)
(closest as MeetSpook).prepare()
2:
# LEVEL 2: AGGRESSIVE
# Beast pursues player on sight.
pass # TODO
3:
# LEVEL 3: PREDATORY
# Beast pursues player relentlessly.
pass # TODO
4:
# LEVEL 4: SWARMING
# Many beasts spawn, overwhelming the player.
pass # TODO
5:
# LEVEL 5: FUN
# Just kill that fool!
pass # TODO
func _on_alert_cleared() -> void:
pass # TODO

View File

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

View File

@ -1,6 +1,9 @@
class_name World extends Node class_name World extends Node
## Access and flow control for the game world. ## Access and flow control for the game world.
@export var spook_manager: SpookManager
@export_category("Game Scenes")
@export var initial_level: PackedScene @export var initial_level: PackedScene
@export var pause_scene: PackedScene @export var pause_scene: PackedScene

View File

@ -1,11 +1,17 @@
[gd_scene load_steps=4 format=3 uid="uid://884jqafhtrv0"] [gd_scene load_steps=6 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="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"]
script = ExtResource("2_bsf3i")
metadata/_custom_type_script = "uid://bsn026pxqwkbc"
[node name="World" type="Node"] [node name="World" type="Node"]
script = ExtResource("1_1k4gi") script = ExtResource("1_1k4gi")
spook_manager = SubResource("Resource_43c6p")
initial_level = ExtResource("2_jte2u") initial_level = ExtResource("2_jte2u")
pause_scene = ExtResource("2_6fy3g") pause_scene = ExtResource("2_6fy3g")

View File

@ -7,4 +7,24 @@ Summoned when the [[grunk alert]] maxes out. Hunts down the player and grunkifie
Concept: No head, tall spindly legs. Concept: No head, tall spindly legs.
Visually, I imagine it straining to pull itself out of the grunk. Visually, I imagine it straining to pull itself out of the grunk.
### Behavior
Behavior changes based on [[grunk alert]]
##### LEVEL 0 - Unaware
Inactive. No spooky until the player fucks up.
##### LEVEL 1 - Passive
The beast spawns somewhere the player can't see. It will avoid the player.
Can we orchestrate a spooky sighting? ([[meet spook]]?)
##### LEVEL 2 - Aggressive
Beast will pursue the player on sight.
##### LEVEL 3 - Predatory
Beast will pursue player automatically and relentlessly.
##### LEVEL 4 - Swarming
Beasts spawn at every spawn point, all pursuing the player.
##### LEVEL 5 - He's right behind me, isn't he?
Unavoidable death. Show cutscene with player grabbed by beast.
The player can reset the alert level to 2 by returning to the safe zone.
tags: #mechanics #lore tags: #mechanics #lore

View File

@ -0,0 +1 @@
After hitting [[grunk alert]] level 1, trigger a "meet spook" event. Mark all sightline boundaries (corners etc) for each area of the level, and spawn a passive [[grunk beast]] at each. When the player has line of sight to the beast, immediately scurry out of sight ( #maybe play a sting sfx?) and despawn.