This commit is contained in:
Henry 2026-05-10 19:28:42 +01:00
commit f6d66e0101
14 changed files with 383 additions and 78 deletions

View file

@ -1,44 +1,43 @@
extends Node3D
var survivors: int = 5
var survivor_rate: float = .75
var speed_modifer: int = 0
var spawn_readiness: bool = false
@export var batch_duration: float = 16.0
@export var lateral_variance: float = 1.5
var _active_walkers: Array[Node] = []
var _batch_generation: int = 0
@onready var survivor = preload("res://scenes/survivor.tscn")
@onready var world = preload("res://scenes/world.tscn")
@onready var survivor_spawn = get_node("/root/Game/World/SuvivorSpawn")
@onready var start_pos = survivor_spawn.global_position
@onready var start_pos: Vector3 = survivor_spawn.global_position
func _ready() -> void:
start()
func _enter_tree() -> void:
EventBus.floor_started.connect(_on_floor_started, CONNECT_DEFERRED)
func start() -> void:
spawn_readiness = true
spawn_survivor()
func _on_floor_started(count: int) -> void:
_batch_generation += 1
_clear_active_walkers()
if count <= 0:
return
_run_batch(count, _batch_generation)
#func _physics_process(delta):
#if spawn_readiness == true:
#spawn_survivor()
#spawn_readiness = false
#
#else: pass
func _clear_active_walkers() -> void:
for w in _active_walkers:
if is_instance_valid(w):
w.queue_free()
_active_walkers.clear()
func spawn_survivor():
func _run_batch(count: int, generation: int) -> void:
var gap: float = batch_duration / float(count)
for i in range(count):
if generation != _batch_generation:
return
_spawn_one()
var jitter: float = randf_range(-gap * 0.25, gap * 0.25)
await get_tree().create_timer(max(0.05, gap + jitter)).timeout
if spawn_readiness == false: pass
else:
var survivor_delay: float = randf_range(1, 2.5)
var start_variance = Vector3((randf_range(-1, 2)),0,0)
for x in range(survivors):
func _spawn_one() -> void:
var s = survivor.instantiate()
var offset := Vector3(randf_range(-lateral_variance, lateral_variance), 0, 0)
get_tree().root.add_child.call_deferred(s)
#get_node("/root/Game/World").add_child(s)
s.position = start_pos + start_variance
await get_tree().create_timer(survivor_rate * survivor_delay).timeout
spawn_readiness = true
s.position = start_pos + offset
_active_walkers.append(s)

View file

@ -4,6 +4,7 @@ extends PanelContainer
var current_floor := 10
var saved_count := 0
var doors_closing_flag := false
var _onboarded := false
# Survivors per floor: more at top, fewer as you descend
var base_survivors := 2
@ -19,14 +20,6 @@ var points_per_person := 100 # base + bonus per person above threshold
var people_in_elevator := 0
const CHASE_SHORT_1 := preload("res://audio/Shortchase1.wav")
const CHASE_SHORT_2 := preload("res://audio/Shortchase2.wav")
const CHASE_MID := preload("res://audio/Midchase1.wav")
const CHASE_LONG := preload("res://audio/Chase1.wav")
const CHASE_TRACKS := [CHASE_SHORT_1, CHASE_SHORT_2, CHASE_MID, CHASE_LONG]
var _last_chase: AudioStream = null
var _chase_tween: Tween = null
func _ready():
var button = $PanelMargin/PanelColumn/CloseButton
button.text = "BLOCK"
@ -36,12 +29,14 @@ func _ready():
screen.pulse_blocked.connect(_on_pulse_blocked)
screen.doors_closing.connect(_on_doors_closing)
_start_floor()
EventBus.game_started.connect(_start_floor)
func _unhandled_input(event):
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_SPACE:
_on_block_pressed()
elif event.keycode == KEY_R:
get_tree().reload_current_scene()
# --- Floor lifecycle ---
@ -56,11 +51,22 @@ func _start_floor():
EventBus.people_changed.emit(people_in_elevator, threshold)
EventBus.floor_changed.emit(current_floor)
EventBus.floor_started.emit(survivors_remaining)
var screen = $PanelMargin/PanelColumn/Screen
screen.start()
if not _onboarded:
_onboarded = true
screen.show_onboarding("BLOCK\nIN GREEN ZONE")
get_tree().create_timer(3.0).timeout.connect(
func():
screen.end_onboarding()
screen.launch_pulse(),
CONNECT_ONE_SHOT
)
else:
screen.launch_pulse() # first survivor arrives
_start_chase()
# --- Input ---
@ -72,7 +78,6 @@ func _on_block_pressed():
# --- Pulse callbacks ---
func _on_pulse_blocked():
_stop_chase()
# This survivor made it in
survivors_remaining -= 1
people_in_elevator += 1
@ -91,8 +96,7 @@ func _on_pulse_blocked():
get_tree().create_timer(gap).timeout.connect(
func():
if not doors_closing_flag:
screen.launch_pulse()
_start_chase(),
screen.launch_pulse(),
CONNECT_ONE_SHOT
)
@ -101,8 +105,6 @@ func _on_doors_closing():
return
doors_closing_flag = true
_stop_chase()
var screen = $PanelMargin/PanelColumn/Screen
# Lose: not enough people
@ -137,22 +139,3 @@ func _on_doors_closing():
var tween = create_tween()
tween.tween_interval(2.0)
tween.tween_callback(_start_floor)
func _start_chase():
if _chase_tween and _chase_tween.is_valid():
_chase_tween.kill()
var candidates: Array = CHASE_TRACKS.filter(func(s): return s != _last_chase)
var stream: AudioStream = candidates[randi() % candidates.size()]
_last_chase = stream
$SfxChase.stream = stream
$SfxChase.volume_db = 0.0
$SfxChase.play()
func _stop_chase():
if _chase_tween and _chase_tween.is_valid():
_chase_tween.kill()
_chase_tween = create_tween()
_chase_tween.tween_property($SfxChase, "volume_db", -80.0, 0.3)
_chase_tween.tween_callback($SfxChase.stop)

52
scenes/end_screen.gd Normal file
View file

@ -0,0 +1,52 @@
extends CanvasLayer
var _score := 0
var _saved := 0
func _ready():
visible = false
EventBus.game_won.connect(_on_game_won)
EventBus.game_lost.connect(_on_game_lost)
EventBus.score_changed.connect(func(s): _score = s)
EventBus.saved_changed.connect(func(s): _saved = s)
$Center/Card/Margin/Column/RestartButton.pressed.connect(_on_restart_pressed)
func _on_game_won():
$Center/Card/Margin/Column/Headline.text = "YOU ESCAPED"
_show()
func _on_game_lost():
$Center/Card/Margin/Column/Headline.text = "TOO FEW"
_apply_loss_palette()
_show()
func _apply_loss_palette():
var border := Color(1, 0.3, 0.3, 1)
var subtle := Color(0.95, 0.7, 0.7, 1)
var card_style := ($Center/Card.get_theme_stylebox("panel") as StyleBoxFlat).duplicate() as StyleBoxFlat
if card_style:
card_style.border_color = border
card_style.bg_color = Color(0.18, 0.05, 0.05, 1)
$Center/Card.add_theme_stylebox_override("panel", card_style)
var button = $Center/Card/Margin/Column/RestartButton
var button_style := (button.get_theme_stylebox("normal") as StyleBoxFlat).duplicate() as StyleBoxFlat
if button_style:
button_style.border_color = border
button_style.bg_color = Color(0.3, 0.1, 0.1, 1)
for state in ["normal", "hover", "pressed", "focus"]:
button.add_theme_stylebox_override(state, button_style)
button.add_theme_color_override("font_color", border)
$Center/Card/Margin/Column/Headline.add_theme_color_override("font_color", border)
$Center/Card/Margin/Column/ScoreLabel.add_theme_color_override("font_color", subtle)
$Center/Card/Margin/Column/SavedLabel.add_theme_color_override("font_color", subtle)
func _show():
$Center/Card/Margin/Column/ScoreLabel.text = "Score: %d" % _score
$Center/Card/Margin/Column/SavedLabel.text = "Survivors saved: %d" % _saved
visible = true
func _on_restart_pressed():
get_tree().reload_current_scene()

1
scenes/end_screen.gd.uid Normal file
View file

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

86
scenes/end_screen.tscn Normal file
View file

@ -0,0 +1,86 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://scenes/end_screen.gd" id="1"]
[sub_resource type="StyleBoxFlat" id="CardStyle"]
bg_color = Color(0.05, 0.18, 0.05, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.2, 1, 0.2, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="ButtonStyle"]
bg_color = Color(0.1, 0.3, 0.1, 1)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.2, 1, 0.2, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
content_margin_left = 24
content_margin_right = 24
content_margin_top = 12
content_margin_bottom = 12
[node name="EndScreen" type="CanvasLayer"]
layer = 10
script = ExtResource("1")
[node name="Dim" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
color = Color(0, 0, 0, 0.85)
[node name="Center" type="CenterContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 1
[node name="Card" type="PanelContainer" parent="Center"]
theme_override_styles/panel = SubResource("CardStyle")
[node name="Margin" type="MarginContainer" parent="Center/Card"]
theme_override_constants/margin_left = 56
theme_override_constants/margin_right = 56
theme_override_constants/margin_top = 40
theme_override_constants/margin_bottom = 40
[node name="Column" type="VBoxContainer" parent="Center/Card/Margin"]
theme_override_constants/separation = 24
[node name="Headline" type="Label" parent="Center/Card/Margin/Column"]
text = "YOU ESCAPED"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.2, 1, 0.2, 1)
theme_override_font_sizes/font_size = 56
[node name="ScoreLabel" type="Label" parent="Center/Card/Margin/Column"]
text = "Score: 0"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.7, 0.95, 0.7, 1)
theme_override_font_sizes/font_size = 28
[node name="SavedLabel" type="Label" parent="Center/Card/Margin/Column"]
text = "Survivors saved: 0"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.7, 0.95, 0.7, 1)
theme_override_font_sizes/font_size = 28
[node name="RestartButton" type="Button" parent="Center/Card/Margin/Column"]
text = "RESTART"
theme_override_colors/font_color = Color(0.2, 1, 0.2, 1)
theme_override_colors/font_hover_color = Color(0.5, 1, 0.5, 1)
theme_override_font_sizes/font_size = 24
theme_override_styles/normal = SubResource("ButtonStyle")
theme_override_styles/hover = SubResource("ButtonStyle")
theme_override_styles/pressed = SubResource("ButtonStyle")
theme_override_styles/focus = SubResource("ButtonStyle")

View file

@ -1,6 +1,4 @@
extends Node3D
func _ready() -> void:
$ComponentSpawn.spawn_survivor()
pass

View file

@ -8,6 +8,8 @@
[ext_resource type="AudioStream" uid="uid://bavo8f76jr7i6" path="res://audio/ElevatorOpen1.wav" id="5_u5sy4"]
[ext_resource type="AudioStream" uid="uid://dgqb1rovgwuan" path="res://audio/ElevatorClose1.wav" id="6_gee14"]
[ext_resource type="PackedScene" path="res://scenes/component_spawn.tscn" id="9_0tnpc"]
[ext_resource type="PackedScene" path="res://scenes/end_screen.tscn" id="10_endsc"]
[ext_resource type="PackedScene" path="res://scenes/title_screen.tscn" id="11_title"]
[node name="Game" type="Node3D" unique_id=1456297160]
script = ExtResource("1_lbhrr")
@ -35,3 +37,7 @@ stream = ExtResource("6_gee14")
[node name="SfxChase" type="AudioStreamPlayer3D" parent="CanvasPanel/ElevatorPanel" unique_id=812445001]
[node name="ComponentSpawn" parent="." unique_id=649225939 instance=ExtResource("9_0tnpc")]
[node name="EndScreen" parent="." instance=ExtResource("10_endsc")]
[node name="TitleScreen" parent="." instance=ExtResource("11_title")]

View file

@ -13,12 +13,23 @@ var pulse_gap_min := 0.3
var block_zone_shrink := 3.0
var block_zone_min := 12.0
const TRAIL_GHOSTS := 8
const TRAIL_SPACING := 3.0
const TRAIL_ALPHAS := [0.5, 0.42, 0.34, 0.27, 0.2, 0.14, 0.09, 0.05]
const GLOW_PADDING := Vector2(6, 6)
const GLOW_COLOR := Color(1, 0.3, 0.3, 0.35)
# Visual
var base_color: Color
# State
var active := false
var pulses_blocked := 0
var _ready_pulse_tween: Tween = null
var _trail: Array[ColorRect] = []
var _glow: ColorRect
var _face_scale_tween: Tween
var _face_shake_tween: Tween
signal pulse_blocked
signal doors_closing
@ -41,6 +52,23 @@ func _ready():
face.size = Vector2(size.x, 30)
face.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
face.text = ">:)"
face.pivot_offset = face.size / 2
_glow = ColorRect.new()
_glow.color = GLOW_COLOR
_glow.size = Vector2($SweepLine.size.x + GLOW_PADDING.x * 2, $SweepLine.size.y + GLOW_PADDING.y * 2)
_glow.visible = false
add_child(_glow)
move_child(_glow, 0)
for i in TRAIL_GHOSTS:
var ghost := ColorRect.new()
ghost.color = Color($SweepLine.color.r, $SweepLine.color.g, $SweepLine.color.b, TRAIL_ALPHAS[i])
ghost.size = $SweepLine.size
ghost.visible = false
add_child(ghost)
move_child(ghost, 0)
_trail.append(ghost)
# Duplicate the StyleBoxFlat so flash tweens don't bleed into other panels
var style = get_theme_stylebox("panel") as StyleBoxFlat
@ -62,7 +90,8 @@ func start():
func stop():
active = false
pulse_active = false
$SweepLine.visible = false
_hide_pulse_visuals()
_stop_ready_pulse()
func shrink_block_zone():
var bz = $TargetZone
@ -80,7 +109,11 @@ func launch_pulse():
pulse_active = true
$SweepLine.visible = true
$SweepLine.position.x = 0
_glow.visible = true
for ghost in _trail:
ghost.visible = true
$AIFace.text = ">:)"
_start_ready_pulse()
func get_pulse_gap() -> float:
return pulse_gap
@ -91,12 +124,18 @@ func _process(delta):
pulse_x += pulse_speed * delta
$SweepLine.position.x = pulse_x
_glow.position = Vector2(pulse_x - GLOW_PADDING.x, $SweepLine.position.y - GLOW_PADDING.y)
for i in _trail.size():
var gx = pulse_x - (i + 1) * TRAIL_SPACING
_trail[i].visible = gx >= 0
_trail[i].position = Vector2(gx, $SweepLine.position.y)
# Pulse reached the far side unblocked: AI wins, doors close
if pulse_x >= size.x:
pulse_active = false
$SweepLine.visible = false
_hide_pulse_visuals()
$AIFace.text = ":D"
_punch_face(1.5, 0.0)
flash(Color.RED)
active = false
doors_closing.emit()
@ -116,16 +155,18 @@ func attempt_block() -> bool:
pulses_blocked += 1
pulse_speed += pulse_speed_increase
pulse_gap = max(pulse_gap - pulse_gap_decrease, pulse_gap_min)
$SweepLine.visible = false
_hide_pulse_visuals()
$AIFace.text = ">:("
_punch_face(1.4, 4.0)
flash(Color.GREEN)
pulse_blocked.emit()
return true
else:
# Mistimed: same result as letting it through
pulse_active = false
$SweepLine.visible = false
_hide_pulse_visuals()
$AIFace.text = ":D"
_punch_face(1.5, 0.0)
flash(Color.RED)
active = false
doors_closing.emit()
@ -163,3 +204,50 @@ func show_loss(message: String):
$TargetZone.visible = false
$AIFace.text = message
flash(Color.RED)
func show_onboarding(msg: String):
$AIFace.add_theme_font_size_override("font_size", 18)
$AIFace.size = Vector2(size.x, 60)
$AIFace.text = msg
func end_onboarding():
$AIFace.add_theme_font_size_override("font_size", 32)
$AIFace.size = Vector2(size.x, 30)
func _start_ready_pulse():
if _ready_pulse_tween:
_ready_pulse_tween.kill()
$TargetZone.modulate.a = 1.0
_ready_pulse_tween = create_tween()
_ready_pulse_tween.set_loops()
_ready_pulse_tween.tween_property($TargetZone, "modulate:a", 0.6, 0.5)
_ready_pulse_tween.tween_property($TargetZone, "modulate:a", 1.0, 0.5)
func _stop_ready_pulse():
if _ready_pulse_tween:
_ready_pulse_tween.kill()
_ready_pulse_tween = null
$TargetZone.modulate.a = 1.0
func _hide_pulse_visuals():
$SweepLine.visible = false
_glow.visible = false
for ghost in _trail:
ghost.visible = false
func _punch_face(scale_amount: float, shake_amount: float):
if _face_scale_tween:
_face_scale_tween.kill()
if _face_shake_tween:
_face_shake_tween.kill()
var face := $AIFace
face.position = Vector2(0, 0)
face.scale = Vector2(scale_amount, scale_amount)
_face_scale_tween = create_tween()
_face_scale_tween.tween_property(face, "scale", Vector2.ONE, 0.25).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
if shake_amount > 0:
_face_shake_tween = create_tween()
_face_shake_tween.tween_property(face, "position:x", -shake_amount, 0.04)
_face_shake_tween.tween_property(face, "position:x", shake_amount, 0.04)
_face_shake_tween.tween_property(face, "position:x", -shake_amount * 0.5, 0.04)
_face_shake_tween.tween_property(face, "position:x", 0.0, 0.04)

View file

@ -1,5 +1,12 @@
extends CharacterBody3D
const CHASE_SHORT_1 := preload("res://audio/Shortchase1.wav")
const CHASE_SHORT_2 := preload("res://audio/Shortchase2.wav")
const CHASE_MID := preload("res://audio/Midchase1.wav")
const CHASE_LONG := preload("res://audio/Chase1.wav")
const CHASE_TRACKS := [CHASE_SHORT_1, CHASE_SHORT_2, CHASE_MID, CHASE_LONG]
static var _last_chase: AudioStream = null
var speed: int = 3
var clumsiness: int = 0
@ -10,6 +17,17 @@ var clumsiness: int = 0
#transform = xform
#velocity = (-xform.basis.z * speed).rotated(Vector3.UP, randf_range(-PI/4, PI/4))
func _ready():
var candidates: Array = CHASE_TRACKS.filter(func(s): return s != _last_chase)
var stream: AudioStream = candidates[randi() % candidates.size()]
_last_chase = stream
$ScreamPlayer.stream = stream
var delay := randf_range(0.0, 0.5)
get_tree().create_timer(delay).timeout.connect(
func(): $ScreamPlayer.play(),
CONNECT_ONE_SHOT
)
func _physics_process(delta):
velocity.z -= speed * delta
move_and_slide()

View file

@ -55,4 +55,6 @@ frame_progress = 0.99938977
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.080844045, -0.14428711)
shape = SubResource("BoxShape3D_7yc2n")
[node name="ScreamPlayer" type="AudioStreamPlayer3D" parent="."]
[connection signal="area_entered" from="Area3D" to="." method="_on_area_3d_area_entered"]

14
scenes/title_screen.gd Normal file
View file

@ -0,0 +1,14 @@
extends CanvasLayer
func _ready():
get_tree().paused = true
func _unhandled_input(event):
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_SPACE or event.keycode == KEY_ENTER:
_start()
func _start():
get_tree().paused = false
EventBus.game_started.emit()
queue_free()

View file

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

55
scenes/title_screen.tscn Normal file
View file

@ -0,0 +1,55 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scenes/title_screen.gd" id="1"]
[sub_resource type="StyleBoxFlat" id="CardStyle"]
bg_color = Color(0.05, 0.18, 0.05, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.2, 1, 0.2, 1)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[node name="TitleScreen" type="CanvasLayer"]
layer = 20
process_mode = 3
script = ExtResource("1")
[node name="Dim" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
color = Color(0, 0, 0, 0.85)
[node name="Center" type="CenterContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 1
[node name="Card" type="PanelContainer" parent="Center"]
theme_override_styles/panel = SubResource("CardStyle")
[node name="Margin" type="MarginContainer" parent="Center/Card"]
theme_override_constants/margin_left = 64
theme_override_constants/margin_right = 64
theme_override_constants/margin_top = 48
theme_override_constants/margin_bottom = 48
[node name="Column" type="VBoxContainer" parent="Center/Card/Margin"]
theme_override_constants/separation = 24
[node name="Title" type="Label" parent="Center/Card/Margin/Column"]
text = "ELEVATOR"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.2, 1, 0.2, 1)
theme_override_font_sizes/font_size = 72
[node name="Prompt" type="Label" parent="Center/Card/Margin/Column"]
text = "PRESS SPACE TO START"
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.5, 1, 0.5, 1)
theme_override_font_sizes/font_size = 28

View file

@ -6,7 +6,9 @@ signal floor_changed(floor_num: int)
signal saved_changed(count: int)
signal score_changed(new_score: int)
signal people_changed(count: int, threshold: int)
signal game_started
signal game_won
signal game_lost
signal floor_started(survivor_count: int)
@warning_ignore_restore("unused_signal") # put any future signals you add between the two ignore annotations