Phase 1: hub + reactor, hydroponics, comms, bridge zones — full ship playable

zones.gd builders on the existing schema: heat + radio elements, thermal
slug + radio beacon gadgets, wax seal, order-chain core igniter, growth
vine (water then daylight), cross-zone cascade (reactor melts hydro ice
wall -> melt pool feeds the mains), through-wall resonator, sealed
bridge (opens 3/3 systems), core-donation ending + PORT 00 kiosk tease.
Each zone in its acquired-brand Flux textures. Integration test now
plays the entire ship: 35 checks passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-03 21:14:45 +10:00
parent 06178a4e08
commit 65dd78efd5
4 changed files with 567 additions and 22 deletions

View File

@ -173,11 +173,70 @@ class Socket extends Node3D:
droid.toast("CANNOT COMPLY — MOBILITY CRITICAL")
class Gadget extends Node3D:
## Portable stateful emitter (thermal slug, radio beacon): an Exposable
## plus an ElementField that's live while the state matches. Same schema
## as everything else — a gadget is just a component you can carry.
var kind := "gadget"
var prompt_text := "TAKE"
var ex: Exposable
var emitter: ElementField
var active_state := ""
func setup(kind_: String, prompt_: String, tint: Color, size_: Vector3,
ex_name: String, initial: String, trans: Array,
emit_el: String, emit_r: float, emit_col: Color, active: String) -> Gadget:
kind = kind_
prompt_text = prompt_
active_state = active
var mi := MeshInstance3D.new()
var mesh := BoxMesh.new()
mesh.size = size_
mi.mesh = mesh
var mat := StandardMaterial3D.new()
mat.albedo_color = tint
mat.emission_enabled = true
mat.emission = tint
mat.emission_energy_multiplier = 0.6
mi.material_override = mat
add_child(mi)
ex = Exposable.new().setup(ex_name, initial, trans)
add_child(ex)
emitter = ElementField.make(emit_el, emit_r, emit_col)
emitter.enabled = false
emitter.exclude_root = self
add_child(emitter)
return self
func _ready() -> void:
add_to_group("interactable")
func _physics_process(_d: float) -> void:
emitter.enabled = ex.state == active_state
func interact(droid: Node3D) -> void:
droid.take(self)
class Trigger extends Node3D:
## Press-E-to-do-a-thing. The thing is a Callable set by the builder.
var prompt_text := "USE"
var action: Callable
func _ready() -> void:
add_to_group("interactable")
func interact(droid: Node3D) -> void:
if action.is_valid():
action.call(droid)
class Tank extends Node3D:
var door: Door
var count := 0
var need := 2
var label: Label3D
var on_full: Callable
var prompt_text := "POUR COOLANT"
func _ready() -> void:
@ -203,8 +262,11 @@ class Tank extends Node3D:
if label:
label.text = "GRAV TANK %d/%d" % [count, need]
if count >= need:
if door:
door.open()
droid.toast("TANK FULL — SLUICE DOOR RELEASED")
if on_full.is_valid():
on_full.call()
droid.toast("TANK FULL")
else:
droid.toast("POURED. TANK %d/%d" % [count, need])
else:

View File

@ -31,10 +31,21 @@ var drum: Node3D
var drum_turned := false
var _marker: Label3D
# Phase 1: zones + ship systems
var bridge_door: Node3D
var bridge_label: Label3D
var hub_water: ElementField
var reactor_glow: OmniLight3D
var sun_field2: ElementField
var ice_wall: Node3D
var melt_pool: ElementField
var systems := {"reactor": false, "hydro": false, "comms": false}
var hall_done := false
var ended := false
var t_sol := 10.0 # start mid-day
var is_day := true
var _live_t := 0.0
var won := false
var _mats := {}
@ -44,6 +55,12 @@ func _ready() -> void:
_build_env()
_build_geometry()
_build_objects()
var Z := preload("res://src/zones.gd")
Z.hub(self)
Z.reactor(self)
Z.hydro(self)
Z.comms(self)
Z.bridge(self)
_build_droid()
_build_hud()
if "--shot" in OS.get_cmdline_user_args():
@ -58,6 +75,11 @@ func _photo_tour() -> void:
[Vector3(-0.5, 0.2, 17.5), PI], # segment C: drum bay + puddle
[Vector3(-1.5, 0.2, 27), PI], # segment D: valve + tank
[Vector3(0, 0.2, 35), PI], # segment E: skylight + solar
[Vector3(0, 0.2, 46), PI], # hub
[Vector3(-11, 0.2, 53), PI / 2.0], # reactor
[Vector3(11, 0.2, 53), -PI / 2.0], # hydroponics
[Vector3(-4.2, 0.2, 63.5), PI], # comms
[Vector3(4.5, 0.2, 65), PI], # bridge
]
for i in spots.size():
droid.global_position = spots[i][0]
@ -179,7 +201,12 @@ func glow(col: Color, energy := 0.7) -> StandardMaterial3D:
const SURF := {"wall": ["wall.jpg", 3.0], "floor": ["floor.jpg", 2.0],
"bulk": ["bulkhead.jpg", 3.0], "greeble": ["greeble.jpg", 2.2]}
"bulk": ["bulkhead.jpg", 3.0], "greeble": ["greeble.jpg", 2.2],
"reactor_wall": ["reactor_wall.jpg", 3.0], "reactor_floor": ["reactor_floor.jpg", 2.2],
"reactor_greeble": ["reactor_greeble.jpg", 2.4], "hydro_wall": ["hydro_wall.jpg", 3.0],
"hydro_floor": ["hydro_floor.jpg", 3.4], "hydro_greeble": ["hydro_greeble.jpg", 2.4],
"comms_wall": ["comms_wall.jpg", 3.0], "comms_greeble": ["comms_greeble.jpg", 2.6],
"bridge_wall": ["bridge_wall.jpg", 1.7], "bridge_floor": ["bridge_floor.jpg", 2.0]}
func box(pos: Vector3, size: Vector3, col: Color, parent: Node3D = null, grid := false, surf := "") -> CSGBox3D:
@ -224,15 +251,29 @@ func raw_tex(path: String) -> ImageTexture:
return _texs[path]
func door_face() -> Texture2D:
if FileAccess.file_exists("res://assets/textures/door_face.jpg"):
return raw_tex("res://assets/textures/door_face.jpg")
return null
func make_door(z: float) -> Node3D:
var d := I.Door.new()
if FileAccess.file_exists("res://assets/textures/door_face.jpg"):
d.face_tex = raw_tex("res://assets/textures/door_face.jpg")
d.face_tex = door_face()
d.position = Vector3(0, 0, z)
add_child(d)
return d
func charge_post(pos: Vector3) -> ElementField:
box(pos + Vector3(0, 0.9, 0), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6)).material = glow(Color(0.15, 0.7, 0.85))
var f := ElementField.make("electricity", 1.3, Color(0.3, 1.0, 1.0, 0.12))
f.position = pos + Vector3(0, 0.9, 0)
add_child(f)
sign3d("CHARGE POST", pos + Vector3(0, 2.3, -0.3))
return f
func make_exposable(dname: String, initial: String, trans: Array, pos: Vector3, parent: Node3D = null) -> Exposable:
var e := Exposable.new().setup(dname, initial, trans)
e.position = pos
@ -299,9 +340,8 @@ func _build_geometry() -> void:
box(Vector3(12.15, 3.5, 22), Vector3(0.3, 8, 6), wall, null, true, "greeble")
box(Vector3(8, 3.5, 19.3), Vector3(8, 8, 0.3), wall, null, true, "greeble")
box(Vector3(8, 3.5, 24.7), Vector3(8, 8, 0.3), wall, null, true, "greeble")
# end walls
# end wall (south only — north opens into the hub)
box(Vector3(0, 2, -0.5), Vector3(8.6, 4, 0.3), wall, null, true, "bulk")
box(Vector3(0, 2, 44.5), Vector3(8.6, 4, 0.3), wall, null, true, "bulk")
# dividers + doors
for z in [8.0, 16.0, 26.0, 34.0, 40.0]:
divider(z)
@ -446,7 +486,7 @@ func _build_objects() -> void:
sign3d("SLUICE DOOR OPENS AT TANK 2/2. BUCKET LEAKS. HUSTLE.", Vector3(0, 2.7, 33.2), PI, 24)
# --- segment E: skylight + solar cell + exit ---
sun_field = ElementField.make("light", 3.2, Color(1.0, 0.95, 0.4, 0.08))
sun_field = ElementField.make("light", 3.2, Color(1.0, 0.95, 0.4, 0.045))
sun_field.position = Vector3(0, 2.2, 37)
add_child(sun_field)
var panel := box(Vector3(1.5, 1.5, 37.3), Vector3(1.2, 0.8, 0.1), Color(0.1, 0.15, 0.4))
@ -458,10 +498,6 @@ func _build_objects() -> void:
Vector3(1.5, 1.5, 37.3))
sign3d("SKYLIGHT — SHUTTER STUCK OPEN. EXIT BUS FED BY SOLAR CELL.\nEXIT OPERATES IN DAYLIGHT ONLY.", Vector3(0, 2.8, 36.2), PI, 24)
# --- end pad ---
sign3d("ROOM RESTORED. PHASE 0 COMPLETE.", Vector3(0, 2.6, 43.9), PI, 48)
sign3d("GENERAL COMPONENT THANKS UNIT-77 FOR ITS CONTINUED COMPONENTHOOD.",
Vector3(0, 1.9, 43.9), PI, 22, Color(1.0, 0.75, 0.3))
func _build_droid() -> void:
@ -505,6 +541,51 @@ func _build_hud() -> void:
add_child(_marker)
# ---------- ship systems ----------
func system_online(name: String) -> void:
if systems.get(name, true):
return
systems[name] = true
var n := 0
for k in systems:
if systems[k]:
n += 1
if bridge_label:
bridge_label.text = "BRIDGE SEAL — SYSTEMS %d/3" % n
match name:
"reactor":
if reactor_glow:
reactor_glow.light_energy = 2.2
if ice_wall:
ice_wall.queue_free()
ice_wall = null
if melt_pool:
melt_pool.enabled = true
droid.toast("REACTOR ONLINE — SHIP HEATING RESTORED. SOMEWHERE, ICE IS MELTING.", 6.0)
"hydro":
if hub_water:
hub_water.enabled = true
droid.toast("WATER MAINS ONLINE — THE SHIP DRINKS AGAIN. HUB HYDRANT LIVE.", 6.0)
"comms":
droid.toast("COMMS ONLINE — INCOMING FROM GC HQ: \"ASSET 7734-C, STATUS REPORT OVERDUE 6 YEARS. PLEASE HOLD.\"", 7.0)
if n >= 3 and bridge_door and not bridge_door.is_open:
bridge_door.open()
get_tree().create_timer(2.0).timeout.connect(func():
droid.toast("ALL SYSTEMS RESTORED — BRIDGE SEAL RELEASED. REPORT TO BRIDGE.", 6.0))
func ending() -> void:
if ended:
droid.toast("ORBIT NOMINAL. NOTHING LEFT TO GIVE.")
return
ended = true
droid.toast("CORE PROCESSOR DONATED. ORBIT CORRECTION RUNNING. THIS UNIT WILL COAST.", 7.0)
sign3d("ORBIT CORRECTED.\nASSET RESTORED. ASSET RETURNED.", Vector3(4.5, 2.4, 71), PI, 44)
get_tree().create_timer(7.0).timeout.connect(func():
droid.toast("GC THANKS UNIT-77 FOR ITS COMPONENTHOOD. RECYCLING QUEUE POSITION: 7. HAVE A PRODUCTIVE WAIT.", 10.0))
# ---------- rules ----------
func _on_drum_switch(_from: String, to: String, _perm: bool) -> void:
@ -520,6 +601,8 @@ func _physics_process(delta: float) -> void:
var phase := fmod(t_sol, 60.0)
is_day = phase < 40.0
sun_field.enabled = is_day
if sun_field2:
sun_field2.enabled = is_day
sun_light.light_energy = 1.2 if is_day else 0.08
# be-the-wire
@ -550,10 +633,11 @@ func _physics_process(delta: float) -> void:
elif solar.state != "powered" and exit_door.is_open:
exit_door.close()
# win
if not won and droid.global_position.z > 41.5:
won = true
droid.toast("ASSET STATUS: ROOM RESTORED. PHASE 0 COMPLETE. GC IS PROUD-ADJACENT.", 10.0)
# hall completion beat
if not hall_done and droid.global_position.z > 41.5 and droid.global_position.z < 45.0 \
and absf(droid.global_position.x) < 4.0:
hall_done = true
droid.toast("INTAKE HALL RESTORED. PROCEED TO CENTRAL JUNCTION. RESTORE THE SHIP.", 8.0)
func _process(_delta: float) -> void:
@ -584,12 +668,26 @@ func _process(_delta: float) -> void:
obj = "SEALED BAY — RELAY IS BEHIND THE BARS. READ THE CAUTION SIGN. DISOBEY IT."
elif not door4.is_open:
obj = "GRAV TANK NEEDS 2 POURS. THE BUCKET LEAKS. HUSTLE."
elif not won:
elif not hall_done:
obj = "EXIT RUNS ON SUNLIGHT. CHECK THE SOL CLOCK."
elif ended:
obj = "MISSION COMPLETE. AWAIT RECYCLING."
else:
var n := 0
for k in systems:
if systems[k]:
n += 1
if n < 3:
obj = "RESTORE SHIP SYSTEMS — REACTOR:%s HYDRO:%s COMMS:%s" % [
"OK" if systems.reactor else "--",
"OK" if systems.hydro else "--",
"OK" if systems.comms else "--"]
else:
obj = "BRIDGE UNSEALED. REPORT TO BRIDGE."
var lines := [
"GENERAL COMPONENT™ — UNIT-77 — MISSION: RESTORE ASSET. RETURN ASSET.",
"SOL: %s%s" % [sol, " ★ ROOM RESTORED — PHASE 0 COMPLETE" if won else ""],
"SOL: %s%s" % [sol, "ORBIT CORRECTED — MISSION COMPLETE" if ended else ""],
"SHELL: %s | CORE: %s | WHEELS: %d | SPARES: %d | PORT 00: NO COMPONENT SPECIFIED"
% [shell_s, core_s, droid.wheels, droid.spare_wheels],
"HOLDING: %s" % holding,

309
src/zones.gd Normal file
View File

@ -0,0 +1,309 @@
extends Node
## Phase 1 zone builders. Each static func takes main (`m`) and uses its
## helpers (box/sign3d/make_exposable/charge_post/...). Pure content — every
## puzzle is fields + exposables + doors on the one schema.
const I := preload("res://src/interactables.gd")
## HUB — crossroads, x -10..10, z 44.5..62
static func hub(m) -> void:
# floor
m.box(Vector3(0, -0.15, 53.25), Vector3(20.6, 0.3, 17.5), Color.WHITE, null, false, "floor")
# south wall (hall opening x -1.2..1.2)
m.box(Vector3(-5.6, 2, 44.5), Vector3(8.8, 4, 0.3), Color.WHITE, null, false, "wall")
m.box(Vector3(5.6, 2, 44.5), Vector3(8.8, 4, 0.3), Color.WHITE, null, false, "wall")
m.box(Vector3(0, 3.5, 44.5), Vector3(2.4, 1, 0.3), Color.WHITE, null, false, "wall")
# west wall, reactor arch z 51..55
m.box(Vector3(-10, 2, 47.75), Vector3(0.3, 4, 6.5), Color.WHITE, null, false, "wall")
m.box(Vector3(-10, 2, 58.5), Vector3(0.3, 4, 7.0), Color.WHITE, null, false, "wall")
m.box(Vector3(-10, 3.5, 53), Vector3(0.3, 1, 4.0), Color.WHITE, null, false, "wall")
# east wall, hydro arch z 51..55
m.box(Vector3(10, 2, 47.75), Vector3(0.3, 4, 6.5), Color.WHITE, null, false, "wall")
m.box(Vector3(10, 2, 58.5), Vector3(0.3, 4, 7.0), Color.WHITE, null, false, "wall")
m.box(Vector3(10, 3.5, 53), Vector3(0.3, 1, 4.0), Color.WHITE, null, false, "wall")
# north wall: comms arch at x -4.2, bridge door at x 4.2
m.box(Vector3(-8.2, 2, 62), Vector3(5.6, 4, 0.3), Color.WHITE, null, false, "wall")
m.box(Vector3(0, 2, 62), Vector3(6.0, 4, 0.3), Color.WHITE, null, false, "wall")
m.box(Vector3(8.2, 2, 62), Vector3(5.6, 4, 0.3), Color.WHITE, null, false, "wall")
m.box(Vector3(-4.2, 3.5, 62), Vector3(2.4, 1, 0.3), Color.WHITE, null, false, "wall")
m.box(Vector3(4.2, 3.5, 62), Vector3(2.4, 1, 0.3), Color.WHITE, null, false, "wall")
m.charge_post(Vector3(-8.5, 0, 45.8))
m.box(Vector3(0, 2, 53.25), Vector3(1.8, 4, 1.8), Color.WHITE, null, false, "greeble")
var hub_light := OmniLight3D.new()
hub_light.position = Vector3(0, 3.6, 53.25)
hub_light.omni_range = 14.0
hub_light.light_energy = 1.4
hub_light.light_color = Color(1.0, 0.96, 0.88)
m.add_child(hub_light)
m.sign3d("CENTRAL JUNCTION — GC DECK 4", Vector3(0, 3.0, 61.8), PI, 40)
m.sign3d("◄ REACTOR (HELIOTROPE ENERGY)", Vector3(-9.8, 2.6, 53), PI / 2.0, 30, Color(1.0, 0.5, 0.3))
m.sign3d("HYDROPONICS (VERDANTCO) ►", Vector3(9.8, 2.6, 53), -PI / 2.0, 30, Color(0.5, 1.0, 0.5))
m.sign3d("COMMS ▲", Vector3(-4.2, 2.9, 61.8), PI, 30, Color(0.5, 0.7, 1.0))
m.sign3d("REMINDER: EVERY PART OF EVERYTHING IS A GC PART. INCLUDING YOU.",
Vector3(0, 2.2, 44.7), 0.0, 22, Color(1.0, 0.75, 0.3))
# sealed bridge door
var bd := I.Door.new()
bd.face_tex = m.door_face()
bd.position = Vector3(4.2, 0, 62)
m.add_child(bd)
m.bridge_door = bd
m.bridge_label = m.sign3d("BRIDGE SEAL — SYSTEMS 0/3", Vector3(4.2, 3.1, 61.8), PI, 30, Color(1.0, 0.4, 0.3))
# decorative water drop point, live once mains restored
m.hub_water = ElementField.make("water", 1.2, Color(0.2, 0.4, 1.0, 0.10))
m.hub_water.position = Vector3(8.5, 0.8, 59)
m.hub_water.enabled = false
m.add_child(m.hub_water)
m.box(Vector3(8.5, 1.6, 59.6), Vector3(0.4, 0.4, 0.4), Color(0.3, 0.5, 0.7))
m.sign3d("HUB HYDRANT — DRY (MAINS OFFLINE)", Vector3(8.5, 2.4, 58.9), PI, 22)
## REACTOR (Heliotrope) — x -32..-10, z 46..60. Heat element. Master: ignite core.
static func reactor(m) -> void:
m.box(Vector3(-21, -0.15, 53), Vector3(22.6, 0.3, 14.6), Color.WHITE, null, false, "reactor_floor")
m.box(Vector3(-21, 2, 46), Vector3(22.6, 4, 0.3), Color.WHITE, null, false, "reactor_wall")
m.box(Vector3(-21, 2, 60), Vector3(22.6, 4, 0.3), Color.WHITE, null, false, "reactor_wall")
m.box(Vector3(-32, 2, 53), Vector3(0.3, 4, 14.6), Color.WHITE, null, false, "reactor_greeble")
# divider x=-22 with wax hatch gap z 51.8..54.2
m.box(Vector3(-22, 2, 48.9), Vector3(0.3, 4, 5.8), Color.WHITE, null, false, "reactor_wall")
m.box(Vector3(-22, 2, 57.1), Vector3(0.3, 4, 5.8), Color.WHITE, null, false, "reactor_wall")
m.box(Vector3(-22, 3.5, 53), Vector3(0.3, 1, 2.4), Color.WHITE, null, false, "reactor_wall")
# emergency vent: always-hot field
m.box(Vector3(-12, 1.0, 46.6), Vector3(1.2, 2.0, 0.4), Color.WHITE, null, false, "reactor_greeble")
var vent := ElementField.make("heat", 1.5, Color(1.0, 0.45, 0.1, 0.13))
vent.position = Vector3(-12, 1.0, 47.2)
m.add_child(vent)
m.sign3d("EMERGENCY THERMAL VENT — RESIDUAL CORE HEAT", Vector3(-12, 2.6, 47.6), 0.0, 24, Color(1.0, 0.6, 0.3))
# thermal slug: gadget, hot while recently heated, emits heat
var slug := I.Gadget.new()
slug.setup("slug", "TAKE THERMAL SLUG", Color(0.7, 0.3, 0.2), Vector3(0.35, 0.35, 0.35),
"THERMAL SLUG", "cool",
[{"from": "cool", "to": "hot", "element": "heat",
"duration": 1.5, "decay_to": "cool", "decay_s": 25.0}],
"heat", 1.2, Color(1.0, 0.45, 0.1, 0.12), "hot")
slug.position = Vector3(-13.5, 0.5, 48)
m.add_child(slug)
m.sign3d("THERMAL SLUG — HEAT IT, CARRY IT, HURRY", Vector3(-13.5, 1.6, 48.4), 0.0, 22)
# wax-sealed hatch
var hatch := I.Door.new()
hatch.face_tex = m.door_face()
hatch.position = Vector3(-22, 0, 53)
hatch.rotation.y = PI / 2.0
m.add_child(hatch)
var wax = m.make_exposable("WAX SEAL", "sealed",
[{"from": "sealed", "to": "melted", "element": "heat",
"duration": 2.0, "permanent": true}],
Vector3(-21.6, 1.2, 53))
wax.state_changed.connect(func(_f, to, _p):
if to == "melted":
hatch.open()
m.droid.toast("WAX SEAL MELTED — HATCH RELEASED"))
m.sign3d("HATCH SEALED IN MAINTENANCE WAX.\nAPPLY SUSTAINED HEAT.", Vector3(-21.6, 2.7, 52.4), PI / 2.0, 24)
# core chamber: igniter needs heat THEN electricity (order chain)
m.charge_post(Vector3(-26, 0, 58.5))
m.box(Vector3(-30.2, 1.5, 53), Vector3(1.6, 3, 2.6), Color.WHITE, null, false, "reactor_greeble")
m.reactor_glow = OmniLight3D.new()
m.reactor_glow.position = Vector3(-29, 2.5, 53)
m.reactor_glow.omni_range = 12.0
m.reactor_glow.light_color = Color(1.0, 0.35, 0.15)
m.reactor_glow.light_energy = 0.0
m.add_child(m.reactor_glow)
var igniter = m.make_exposable("CORE IGNITER", "cold",
[{"from": "cold", "to": "primed", "element": "heat",
"duration": 2.0, "decay_to": "cold", "decay_s": 20.0},
{"from": "primed", "to": "lit", "element": "electricity",
"duration": 1.0, "permanent": true}],
Vector3(-28.6, 1.2, 53))
igniter.state_changed.connect(func(_f, to, _p):
if to == "lit":
m.system_online("reactor"))
m.sign3d("CORE IGNITER — PRIME WITH HEAT, STRIKE WITH CURRENT.\nIN THAT ORDER. HELIOTROPE IS NOT LIABLE FOR THE OTHER ORDER.",
Vector3(-28, 2.8, 52.2), PI / 2.0, 22, Color(1.0, 0.6, 0.3))
## HYDROPONICS (VerdantCo) — x 10..32, z 46..60. Vine gate, ice wall, mains master.
static func hydro(m) -> void:
m.box(Vector3(21, -0.15, 53), Vector3(22.6, 0.3, 14.6), Color.WHITE, null, false, "hydro_floor")
m.box(Vector3(21, 2, 46), Vector3(22.6, 4, 0.3), Color.WHITE, null, false, "hydro_wall")
m.box(Vector3(21, 2, 60), Vector3(22.6, 4, 0.3), Color.WHITE, null, false, "hydro_wall")
m.box(Vector3(32, 2, 53), Vector3(0.3, 4, 14.6), Color.WHITE, null, false, "hydro_greeble")
# vine-gate divider x=20, gap z 51.8..54.2
m.box(Vector3(20, 2, 48.9), Vector3(0.3, 4, 5.8), Color.WHITE, null, false, "hydro_wall")
m.box(Vector3(20, 2, 57.1), Vector3(0.3, 4, 5.8), Color.WHITE, null, false, "hydro_wall")
m.box(Vector3(20, 3.5, 53), Vector3(0.3, 1, 2.4), Color.WHITE, null, false, "hydro_wall")
var grate := I.Door.new()
grate.tint = Color(0.5, 0.7, 0.5)
grate.position = Vector3(20, 0, 53)
grate.rotation.y = PI / 2.0
m.add_child(grate)
# outer: sprinkler valve (electricity) + vine bed (water then light)
m.charge_post(Vector3(12, 0, 47.5))
var sprinkler := ElementField.make("water", 2.0, Color(0.2, 0.4, 1.0, 0.10))
sprinkler.position = Vector3(16, 1.5, 57)
sprinkler.enabled = false
m.add_child(sprinkler)
m.sun_field2 = ElementField.make("light", 2.5, Color(1.0, 0.95, 0.4, 0.045))
m.sun_field2.position = Vector3(16, 2.5, 57)
m.add_child(m.sun_field2)
m.box(Vector3(16, 0.15, 57), Vector3(2.4, 0.3, 2.4), Color(0.35, 0.28, 0.2))
var vine_visual: CSGBox3D = m.box(Vector3(16, 0.5, 57), Vector3(0.4, 0.6, 0.4), Color(0.25, 0.7, 0.3))
var valve = m.make_exposable("SPRINKLER VALVE", "stuck",
[{"from": "stuck", "to": "open", "element": "electricity",
"duration": 1.0, "permanent": true}],
Vector3(13.5, 1.0, 57))
m.box(Vector3(13.5, 0.8, 57.4), Vector3(0.4, 1.6, 0.4), Color.WHITE, null, false, "hydro_greeble")
valve.state_changed.connect(func(_f, to, _p):
if to == "open":
sprinkler.enabled = true
m.droid.toast("SPRINKLER VALVE FREED — IRRIGATION FLOWING"))
var vine = m.make_exposable("GROWTH VINE", "seed",
[{"from": "seed", "to": "sprouted", "element": "water",
"duration": 4.0, "decay_to": "seed", "decay_s": 30.0},
{"from": "sprouted", "to": "grown", "element": "light",
"duration": 4.0, "permanent": true}],
Vector3(16, 0.6, 57))
vine.state_changed.connect(func(_f, to, _p):
if to == "sprouted":
m.droid.toast("VINE SPROUTED — IT WANTS SUNLIGHT NOW")
elif to == "grown":
grate.open()
var tw = vine_visual.create_tween()
tw.tween_property(vine_visual, "size", Vector3(0.8, 3.4, 0.8), 2.0)
m.droid.toast("VINE FULL-GROWN — IT PRIED THE GRATE OPEN"))
m.sign3d("VERDANTCO GROW BED — WATER, THEN DAYLIGHT.\nSKYLIGHT ABOVE. PATIENCE, GARDENER.", Vector3(16, 2.9, 56.3), PI, 24, Color(0.5, 1.0, 0.5))
m.sign3d("VALVE STUCK. APPLY CURRENT.", Vector3(13.5, 2.2, 57.0), PI, 22)
# mid: bucket + the ICE WALL (x=26) — only reactor heat clears it
var bucket2 := I.Bucket.new()
bucket2.position = Vector3(22, 0, 50)
m.add_child(bucket2)
m.box(Vector3(26, 2, 48.9), Vector3(0.3, 4, 5.8), Color.WHITE, null, false, "hydro_wall")
m.box(Vector3(26, 2, 57.1), Vector3(0.3, 4, 5.8), Color.WHITE, null, false, "hydro_wall")
m.box(Vector3(26, 3.5, 53), Vector3(0.3, 1, 2.4), Color.WHITE, null, false, "hydro_wall")
m.ice_wall = m.box(Vector3(26, 1.5, 53), Vector3(0.5, 3.0, 2.6), Color.WHITE)
var ice_mat := StandardMaterial3D.new()
ice_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
ice_mat.albedo_color = Color(0.6, 0.85, 1.0, 0.5)
ice_mat.emission_enabled = true
ice_mat.emission = Color(0.5, 0.8, 1.0)
ice_mat.emission_energy_multiplier = 0.15
m.ice_wall.material = ice_mat
m.sign3d("PASSAGE ICED SOLID. SHIP HEATING OFFLINE.\nSOMETHING, SOMEWHERE, MUST BURN.", Vector3(25.6, 2.8, 52.2), -PI / 2.0, 24, Color(0.6, 0.85, 1.0))
# melt pool (hidden until reactor online)
m.melt_pool = ElementField.make("water", 1.6, Color(0.2, 0.4, 1.0, 0.10))
m.melt_pool.position = Vector3(24.8, 0.4, 53)
m.melt_pool.enabled = false
m.add_child(m.melt_pool)
# inner: water-mains pressurizer (2 pours)
var basin := I.Tank.new()
basin.position = Vector3(30, 0, 53)
basin.on_full = func(): m.system_online("hydro")
m.add_child(basin)
basin.label = m.sign3d("MAINS PRESSURIZER 0/2", Vector3(29.4, 2.0, 52.4), -PI / 2.0, 26)
m.sign3d("WATER MAINS PRESSURIZER — FEED IT AND THE SHIP DRINKS AGAIN.",
Vector3(29, 2.9, 53.8), -PI / 2.0, 22, Color(0.5, 1.0, 0.5))
## COMMS — x -8.5..-0.5, z 62..78. Radio element, through-wall resonator, transmitter.
static func comms(m) -> void:
m.box(Vector3(-4.5, -0.15, 70), Vector3(8.6, 0.3, 16.6), Color.WHITE, null, false, "floor")
m.box(Vector3(-8.5, 2, 70), Vector3(0.3, 4, 16.6), Color.WHITE, null, false, "comms_wall")
m.box(Vector3(-0.5, 2, 70), Vector3(0.3, 4, 16.6), Color.WHITE, null, false, "comms_wall")
m.box(Vector3(-4.5, 2, 78), Vector3(8.6, 4, 0.3), Color.WHITE, null, false, "comms_greeble")
# divider z=70, door gap x -5.4..-3
m.box(Vector3(-7.2, 2, 70), Vector3(2.9, 4, 0.3), Color.WHITE, null, false, "comms_wall")
m.box(Vector3(-1.7, 2, 70), Vector3(2.9, 4, 0.3), Color.WHITE, null, false, "comms_wall")
m.box(Vector3(-4.2, 3.5, 70), Vector3(2.4, 1, 0.3), Color.WHITE, null, false, "comms_wall")
var d2 := I.Door.new()
d2.face_tex = m.door_face()
d2.position = Vector3(-4.2, 0, 70)
m.add_child(d2)
m.charge_post(Vector3(-1.5, 0, 64))
# radio beacon gadget
var beacon := I.Gadget.new()
beacon.setup("beacon", "TAKE RADIO BEACON", Color(0.6, 0.4, 0.9), Vector3(0.3, 0.5, 0.3),
"RADIO BEACON", "idle",
[{"from": "idle", "to": "broadcasting", "element": "electricity",
"duration": 1.0, "decay_to": "idle", "decay_s": 18.0}],
"radio", 4.5, Color(0.8, 0.4, 1.0, 0.08), "broadcasting")
beacon.position = Vector3(-7, 0.8, 64)
m.add_child(beacon)
m.box(Vector3(-7, 0.4, 64), Vector3(0.8, 0.8, 0.8), Color.WHITE, null, false, "comms_greeble")
m.sign3d("PORTABLE BEACON — CHARGE TO BROADCAST.\nRADIO DOES NOT CARE ABOUT WALLS.", Vector3(-7, 2.2, 64.5), PI, 24, Color(0.8, 0.5, 1.0))
# resonator sealed INSIDE the divider wall — only radio reaches it
var reso = m.make_exposable("CRYSTAL RESONATOR (SEALED IN WALL)", "idle",
[{"from": "idle", "to": "rung", "element": "radio",
"duration": 2.0, "permanent": true}],
Vector3(-6.5, 2.0, 70))
reso.state_changed.connect(func(_f, to, _p):
if to == "rung":
d2.open()
m.droid.toast("RESONATOR RUNG THROUGH THE WALL — DOOR RELEASED"))
m.sign3d("DOOR CIRCUIT: RESONATOR EMBEDDED IN WALL CAVITY.\nNO PHYSICAL ACCESS. THINK WAVES.", Vector3(-6.5, 2.9, 69.8), PI, 22)
# master: transmitter — warm with radio, strike with current
m.box(Vector3(-4.5, 1.0, 76.8), Vector3(3.0, 2.0, 0.6), Color.WHITE, null, false, "comms_greeble")
var tx = m.make_exposable("MAIN TRANSMITTER", "idle",
[{"from": "idle", "to": "warmed", "element": "radio",
"duration": 2.5, "decay_to": "idle", "decay_s": 15.0},
{"from": "warmed", "to": "transmitting", "element": "electricity",
"duration": 1.0, "permanent": true}],
Vector3(-4.5, 1.2, 76.2))
tx.state_changed.connect(func(_f, to, _p):
if to == "transmitting":
m.system_online("comms"))
m.charge_post(Vector3(-7.5, 0, 75))
m.sign3d("MAIN TRANSMITTER — WARM THE CRYSTAL (RADIO), THEN STRIKE (CURRENT).",
Vector3(-4.5, 2.9, 75.6), PI, 22, Color(0.8, 0.5, 1.0))
## BRIDGE — x 0.5..8.5, z 62..78. Sealed until 3/3. The ending lives here.
static func bridge(m) -> void:
m.box(Vector3(4.5, -0.15, 70), Vector3(8.6, 0.3, 16.6), Color.WHITE, null, false, "bridge_floor")
m.box(Vector3(0.5, 2, 70), Vector3(0.3, 4, 16.6), Color.WHITE, null, false, "bridge_wall")
m.box(Vector3(8.5, 2, 70), Vector3(0.3, 4, 16.6), Color.WHITE, null, false, "bridge_wall")
m.box(Vector3(4.5, 2, 78), Vector3(8.6, 4, 0.3), Color.WHITE, null, false, "bridge_wall")
var o := OmniLight3D.new()
o.position = Vector3(4.5, 3.2, 70)
o.omni_range = 12.0
o.light_energy = 1.2
o.light_color = Color(1.0, 0.9, 0.75)
m.add_child(o)
# captain's console + core socket
m.box(Vector3(4.5, 0.6, 75.5), Vector3(3.4, 1.2, 1.0), Color(0.35, 0.25, 0.18))
var console := I.Trigger.new()
console.prompt_text = "INSERT CORE PROCESSOR (ORBIT CORRECTION)"
console.position = Vector3(4.5, 1.2, 75.5)
console.action = func(_droid): m.ending()
m.add_child(console)
m.sign3d("ORBIT CORRECTION CONSOLE\nREQUIRED: 1x CORE PROCESSOR (GC CAT. 0001-C)", Vector3(4.5, 2.8, 74.9), PI, 26)
# PORT 00 kiosk — the tease
var kiosk := I.Trigger.new()
kiosk.prompt_text = "SCAN OWN CHASSIS — PORT 00"
kiosk.position = Vector3(7.5, 1.0, 68)
kiosk.action = func(droid): droid.toast("PORT 00: NO CATALOGUED COMPONENT FITS THIS SOCKET. NO PART NUMBER ON RECORD. QUERY DISCARDED.", 6.0)
m.add_child(kiosk)
m.box(Vector3(7.5, 0.5, 68), Vector3(0.6, 1.0, 0.6), Color(0.2, 0.2, 0.24))
m.sign3d("DIAGNOSTIC KIOSK", Vector3(7.5, 1.9, 67.5), PI, 22)
# lore
m.sign3d("CAPTAIN'S LOG (CASSETTE, LABEL FADED):\n\"...CREW OFF AT LUYTEN GATE. SHIP WOULDN'T TURN. I STAYED.\nTELL GC THE ASSET IS FINE. THE ASSET IS ALWAYS FINE.\"",
Vector3(1.0, 2.2, 72), PI / 2.0, 20, Color(0.9, 0.85, 0.7))
m.sign3d("GC SAFETY BULLETIN #12:\nDO NOT QUENCH CRYSTAL LATTICE GROWTH IN CHARGED COOLANT.\nUNCATALOGUED SOLIDS MUST BE REPORTED AND DESTROYED.",
Vector3(8.3, 2.2, 72), -PI / 2.0, 20, Color(1.0, 0.75, 0.3))

View File

@ -28,13 +28,22 @@ func wait_until(cond: Callable, max_frames: int) -> bool:
return cond.call()
func find_i(kind_or_prompt: String) -> Node3D:
func find_i(kind_or_prompt: String, min_x := -999.0) -> Node3D:
for n in get_nodes_in_group("interactable"):
if n.global_position.x < min_x:
continue
if str(n.get("kind")) == kind_or_prompt or str(n.get("prompt_text")) == kind_or_prompt:
return n
return null
func find_ex(dname: String) -> Node:
for n in get_nodes_in_group("exposable"):
if n.display_name == dname:
return n
return null
func _initialize() -> void:
root.add_child(load("res://scenes/main.tscn").instantiate())
_run()
@ -115,10 +124,77 @@ func _run() -> void:
"night kills solar cell, exit closes")
main.t_sol = 5.0
print("== win ==")
print("== hall done ==")
await wait_until(func(): return main.exit_door.is_open, 120)
droid.global_position = Vector3(0, 0.2, 42)
check(await wait_until(func(): return main.won, 60), "crossing the exit wins Phase 0")
check(await wait_until(func(): return main.hall_done, 60), "crossing the exit completes the hall")
print("== reactor ==")
var slug := find_i("slug")
slug.interact(droid)
check(droid.held == slug, "thermal slug taken")
droid.global_position = Vector3(-12.4, 0.2, 47.0)
check(await wait_until(func(): return slug.ex.state == "hot", 300), "slug heats at the vent")
droid.global_position = Vector3(-20.9, 0.2, 53)
check(await wait_until(func(): return find_ex("WAX SEAL").state == "melted", 300),
"hot slug melts the wax seal")
droid.global_position = Vector3(-27.8, 0.2, 52.6)
check(await wait_until(func(): return find_ex("CORE IGNITER").state == "primed", 300),
"slug primes the igniter")
droid.shell.force_state("dry")
droid.core.force_state("charged")
check(await wait_until(func(): return main.systems.reactor, 300), "current ignites the primed core")
check(main.ice_wall == null and main.melt_pool.enabled, "reactor melts the hydro ice wall")
print("== hydroponics ==")
droid.global_position = Vector3(12.8, 0.2, 56.5)
droid.core.force_state("charged")
check(await wait_until(func(): return find_ex("SPRINKLER VALVE").state == "open", 300),
"current frees the sprinkler valve")
main.t_sol = 5.0
check(await wait_until(func(): return find_ex("GROWTH VINE").state == "sprouted", 600),
"sprinkler water sprouts the vine")
check(await wait_until(func(): return find_ex("GROWTH VINE").state == "grown", 600),
"daylight grows the vine (grate opens)")
var bucket2 := find_i("bucket", 9.0)
bucket2.interact(droid)
check(droid.held == bucket2, "hydro bucket taken")
var basin := find_i("POUR COOLANT", 9.0)
for trip in 2:
droid.global_position = Vector3(24.4, 0.2, 52.4)
check(await wait_until(func(): return bucket2.ex.state == "full", 500),
"bucket fills at the melt pool (trip %d)" % (trip + 1))
droid.global_position = Vector3(29.4, 0.2, 52.6)
await wait(5)
basin.interact(droid)
check(main.systems.hydro, "two pours pressurize the water mains")
print("== comms ==")
var beacon := find_i("beacon")
beacon.interact(droid)
check(droid.held == beacon, "radio beacon taken (bucket dropped)")
droid.global_position = Vector3(-1.2, 0.2, 63.2)
check(await wait_until(func(): return beacon.ex.state == "broadcasting", 300),
"beacon charges to broadcasting at the post")
droid.global_position = Vector3(-5.5, 0.2, 68.5)
check(await wait_until(func(): return find_ex("CRYSTAL RESONATOR (SEALED IN WALL)").state == "rung", 300),
"radio rings the resonator through the wall")
droid.global_position = Vector3(-7.2, 0.2, 74.2)
await wait_until(func(): return beacon.ex.state == "broadcasting", 300)
droid.global_position = Vector3(-4.4, 0.2, 75.4)
check(await wait_until(func(): return find_ex("MAIN TRANSMITTER").state == "warmed", 300),
"beacon warms the transmitter")
droid.shell.force_state("dry")
droid.core.force_state("charged")
check(await wait_until(func(): return main.systems.comms, 300), "current fires the transmitter")
print("== bridge ==")
check(await wait_until(func(): return main.bridge_door.is_open, 120),
"3/3 systems unseal the bridge")
droid.global_position = Vector3(4.5, 0.2, 74.3)
var console := find_i("INSERT CORE PROCESSOR (ORBIT CORRECTION)")
console.interact(droid)
check(main.ended, "core donation corrects the orbit — ending reached")
if fails == 0:
print("INTEGRATION PASS — full room playthrough OK")