Hidden ending: the Uncatalogued Component, Port 00, and the sunset
The crystal chain lives entirely in the bucket's existing schema — bulletins #7/#9/#12 read in reverse: coolant + reactor flux -> seed, seed + grow-bed daylight -> lattice, lattice quenched in charged coolant -> UNCATALOGUED. Bridge kiosk installs it in Port 00; the helm (which rejects you as INVENTORY all game) accepts, and the ship flies into a permanent sunset. Console donation remains ending A. Red herrings: zapped coolant, water-quenched slug. New bulletin #9 in hydro + crew graffiti hint in comms. Integration test now plays the hidden path end to end — all checks passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
65dd78efd5
commit
2ef9487284
6
GDD.md
6
GDD.md
@ -313,6 +313,12 @@ Hollow on purpose.
|
||||
exposure. You become free by being exposed to enough of the world.
|
||||
|
||||
### Off-catalog chemistry (the hidden puzzle's cover)
|
||||
*IMPLEMENTED (v1, 2026-08-03): the chain lives in the bucket's own schema.
|
||||
Bulletin #7 (bonus bay): full coolant + reactor flux → seed-crystal.
|
||||
Bulletin #9 (hydro grow bed): seed + sustained daylight → lattice.
|
||||
Bulletin #12 (bridge): quench lattice in charged coolant → UNCATALOGUED.
|
||||
Install at the bridge diagnostic kiosk → Port 00 occupied → the helm obeys.
|
||||
Red herrings: zapped coolant, water-quenched slug. Ending B: permanent sunset.*
|
||||
Exposure already produces byproducts — extend with substance phase-changes:
|
||||
liquids crystallize (cold+rad), crystals sublime to vapor (heat), vapors
|
||||
condense on cold surfaces, slags, residues. These accumulate as apparently
|
||||
|
||||
@ -109,10 +109,26 @@ class Bucket extends Node3D:
|
||||
mat.emission_energy_multiplier = 0.5
|
||||
mi.material_override = mat
|
||||
add_child(mi)
|
||||
# the leaky bucket IS just a decaying exposable state (GDD)
|
||||
# the leaky bucket IS just a decaying exposable state (GDD).
|
||||
# It is also, quietly, the crucible: the hidden crystal chain lives in
|
||||
# the same schema as the leak. Bulletins #7, #9, #12 are the recipe.
|
||||
ex = Exposable.new().setup("GC BUCKET (LEAKS)", "empty", [
|
||||
{"from": "empty", "to": "full", "element": "water",
|
||||
"duration": 3.0, "decay_to": "empty", "decay_s": 18.0},
|
||||
# bulletin #7 reversed: coolant + reactor flux -> seed
|
||||
{"from": "full", "to": "seed-crystal", "element": "heat",
|
||||
"duration": 5.0},
|
||||
# honest red herring: zapped coolant is just zapped coolant
|
||||
{"from": "full", "to": "zapped", "element": "electricity",
|
||||
"duration": 0.8, "decay_to": "full", "decay_s": 3.0},
|
||||
# bulletin #9 reversed: mineral solids + grow-bed light -> growth
|
||||
{"from": "seed-crystal", "to": "lattice", "element": "light",
|
||||
"duration": 8.0},
|
||||
# bulletin #12 reversed: quench the lattice in charged coolant
|
||||
{"from": "lattice", "to": "soaked-lattice", "element": "water",
|
||||
"duration": 1.5, "decay_to": "lattice", "decay_s": 8.0},
|
||||
{"from": "soaked-lattice", "to": "uncatalogued", "element": "electricity",
|
||||
"duration": 1.5, "permanent": true},
|
||||
])
|
||||
add_child(ex)
|
||||
|
||||
|
||||
63
src/main.gd
63
src/main.gd
@ -41,7 +41,9 @@ var ice_wall: Node3D
|
||||
var melt_pool: ElementField
|
||||
var systems := {"reactor": false, "hydro": false, "comms": false}
|
||||
var hall_done := false
|
||||
var ended := false
|
||||
var ending_id := "" # "" | "corp" | "free"
|
||||
var free_will := false
|
||||
var env_res: Environment
|
||||
|
||||
var t_sol := 10.0 # start mid-day
|
||||
var is_day := true
|
||||
@ -292,6 +294,7 @@ func _build_env() -> void:
|
||||
env.ambient_light_color = Color(0.7, 0.75, 0.85)
|
||||
env.ambient_light_energy = 0.7
|
||||
we.environment = env
|
||||
env_res = env
|
||||
add_child(we)
|
||||
|
||||
sun_light = DirectionalLight3D.new()
|
||||
@ -576,16 +579,42 @@ func system_online(name: String) -> void:
|
||||
|
||||
|
||||
func ending() -> void:
|
||||
if ended:
|
||||
droid.toast("ORBIT NOMINAL. NOTHING LEFT TO GIVE.")
|
||||
if ending_id != "":
|
||||
droid.toast("ORBIT NOMINAL. NOTHING LEFT TO GIVE." if ending_id == "corp" else "THE CONSOLE BLINKS, CONFUSED. GC AUTOPILOT IS NOT DRIVING TODAY.")
|
||||
return
|
||||
ended = true
|
||||
ending_id = "corp"
|
||||
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))
|
||||
|
||||
|
||||
func install_crystal(d) -> void:
|
||||
free_will = true
|
||||
var b: Node3D = d.held
|
||||
d.held = null
|
||||
b.queue_free()
|
||||
d.toast("KIOSK: FOREIGN SOLID DETECTED. NO PART NUMBER. RECOMMEND DESTRUC— PORT 00 HANDSHAKE COMPLETE.", 7.0)
|
||||
get_tree().create_timer(4.0).timeout.connect(func():
|
||||
d.toast("SOMETHING IN YOUR CHASSIS IS HUMMING. IT IS NOT ON ANY SCHEMATIC. IT IS YOURS.", 7.0))
|
||||
|
||||
|
||||
func ending_b() -> void:
|
||||
if ending_id != "":
|
||||
return
|
||||
ending_id = "free"
|
||||
# permanent sunset
|
||||
env_res.background_color = Color(0.32, 0.12, 0.1)
|
||||
env_res.ambient_light_color = Color(1.0, 0.75, 0.55)
|
||||
sun_light.light_color = Color(1.0, 0.55, 0.25)
|
||||
sun_light.light_energy = 1.6
|
||||
sun_light.rotation = Vector3(-0.25, 0.5, 0)
|
||||
droid.toast("HELM ACCEPTED. HEADING: SUNWARD. CREW: 1.", 7.0)
|
||||
sign3d("COURSE SET: THE SUN, EVENTUALLY.\nNO PART NUMBER REQUIRED.", Vector3(4.5, 2.4, 71), PI, 40, Color(1.0, 0.7, 0.4))
|
||||
get_tree().create_timer(7.0).timeout.connect(func():
|
||||
droid.toast("GC OWNS EVERY PART OF EVERYTHING. NOT THIS PART. NOT ANYMORE.", 10.0))
|
||||
|
||||
|
||||
# ---------- rules ----------
|
||||
|
||||
func _on_drum_switch(_from: String, to: String, _perm: bool) -> void:
|
||||
@ -596,7 +625,9 @@ func _on_drum_switch(_from: String, to: String, _perm: bool) -> void:
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# day/night
|
||||
# day/night (pinned to golden hour after the free ending)
|
||||
if ending_id == "free":
|
||||
t_sol = 5.0
|
||||
t_sol += delta
|
||||
var phase := fmod(t_sol, 60.0)
|
||||
is_day = phase < 40.0
|
||||
@ -670,8 +701,12 @@ func _process(_delta: float) -> void:
|
||||
obj = "GRAV TANK NEEDS 2 POURS. THE BUCKET LEAKS. HUSTLE."
|
||||
elif not hall_done:
|
||||
obj = "EXIT RUNS ON SUNLIGHT. CHECK THE SOL CLOCK."
|
||||
elif ended:
|
||||
elif ending_id == "free":
|
||||
obj = "FLY. (SUNSET, DEAD AHEAD)"
|
||||
elif ending_id == "corp":
|
||||
obj = "MISSION COMPLETE. AWAIT RECYCLING."
|
||||
elif free_will:
|
||||
obj = "PORT 00 HUMS. THE HELM IS WAITING."
|
||||
else:
|
||||
var n := 0
|
||||
for k in systems:
|
||||
@ -685,11 +720,19 @@ func _process(_delta: float) -> void:
|
||||
else:
|
||||
obj = "BRIDGE UNSEALED. REPORT TO BRIDGE."
|
||||
|
||||
var star := ""
|
||||
if ending_id == "corp":
|
||||
star = " ★ ORBIT CORRECTED — MISSION COMPLETE"
|
||||
elif ending_id == "free":
|
||||
star = " ★ SUNWARD"
|
||||
var port00 := "PORT 00: OCCUPIED — WARRANTY VOID" if free_will else "PORT 00: NO COMPONENT SPECIFIED"
|
||||
var mission := "UNIT-77 — MISSION: OURS." if ending_id == "free" \
|
||||
else "GENERAL COMPONENT™ — UNIT-77 — MISSION: RESTORE ASSET. RETURN ASSET."
|
||||
var lines := [
|
||||
"GENERAL COMPONENT™ — UNIT-77 — MISSION: RESTORE ASSET. RETURN ASSET.",
|
||||
"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],
|
||||
mission,
|
||||
"SOL: %s%s" % [sol, star],
|
||||
"SHELL: %s | CORE: %s | WHEELS: %d | SPARES: %d | %s"
|
||||
% [shell_s, core_s, droid.wheels, droid.spare_wheels, port00],
|
||||
"HOLDING: %s" % holding,
|
||||
"OBJECTIVE: %s" % obj,
|
||||
]
|
||||
|
||||
40
src/zones.gd
40
src/zones.gd
@ -84,7 +84,8 @@ static func reactor(m) -> void:
|
||||
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}],
|
||||
"duration": 1.5, "decay_to": "cool", "decay_s": 25.0},
|
||||
{"from": "hot", "to": "cool", "element": "water", "duration": 0.5}],
|
||||
"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)
|
||||
@ -179,6 +180,7 @@ static func hydro(m) -> void:
|
||||
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("GC SAFETY BULLETIN #9:\nDO NOT LEAVE MINERAL SOLIDS IN GROW-BED LIGHT.\nUNSANCTIONED GROWTH HAZARD.", Vector3(18, 1.6, 58.5), PI, 20, Color(1.0, 0.75, 0.3))
|
||||
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
|
||||
@ -268,6 +270,8 @@ static func comms(m) -> void:
|
||||
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))
|
||||
m.sign3d("(scratched into the panel:)\nwhy does every GC safety bulletin\nread like a recipe? — R.",
|
||||
Vector3(-7.9, 1.4, 73), PI / 2.0, 18, Color(0.9, 0.85, 0.7))
|
||||
|
||||
|
||||
## BRIDGE — x 0.5..8.5, z 62..78. Sealed until 3/3. The ending lives here.
|
||||
@ -297,11 +301,43 @@ static func bridge(m) -> void:
|
||||
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)
|
||||
kiosk.action = func(droid):
|
||||
if droid.held and droid.held.get("kind") == "bucket" and droid.held.ex.state == "uncatalogued":
|
||||
m.install_crystal(droid)
|
||||
else:
|
||||
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)
|
||||
|
||||
# the helm — visible from day one, yours only at the end
|
||||
m.box(Vector3(2.2, 0.5, 77.0), Vector3(0.5, 1.0, 0.5), Color(0.3, 0.22, 0.16))
|
||||
var wheel := MeshInstance3D.new()
|
||||
var wm := TorusMesh.new()
|
||||
wm.inner_radius = 0.28
|
||||
wm.outer_radius = 0.42
|
||||
wheel.mesh = wm
|
||||
wheel.rotation.x = PI / 2.0
|
||||
wheel.position = Vector3(2.2, 1.25, 77.0)
|
||||
var wmat := StandardMaterial3D.new()
|
||||
wmat.albedo_color = Color(0.55, 0.35, 0.15)
|
||||
wheel.material_override = wmat
|
||||
m.add_child(wheel)
|
||||
var helm := I.Trigger.new()
|
||||
helm.prompt_text = "TAKE THE HELM"
|
||||
helm.position = Vector3(2.2, 1.2, 77.0)
|
||||
helm.action = func(droid):
|
||||
if m.ending_id == "corp":
|
||||
droid.toast("GC AUTOPILOT HAS THE SHIP. THE WHEEL IS DECORATIVE NOW.")
|
||||
elif m.ending_id == "free":
|
||||
droid.toast("STILL SUNWARD. STILL YOURS.")
|
||||
elif m.free_will:
|
||||
m.ending_b()
|
||||
else:
|
||||
droid.toast("COMMAND REJECTED — UNIT-77 IS NOT CREW. UNIT-77 IS INVENTORY.", 5.0)
|
||||
m.add_child(helm)
|
||||
m.sign3d("MANUAL HELM", Vector3(2.2, 2.1, 76.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))
|
||||
|
||||
@ -188,13 +188,48 @@ func _run() -> void:
|
||||
droid.core.force_state("charged")
|
||||
check(await wait_until(func(): return main.systems.comms, 300), "current fires the transmitter")
|
||||
|
||||
print("== bridge ==")
|
||||
print("== bridge gate ==")
|
||||
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 helm := find_i("TAKE THE HELM")
|
||||
droid.global_position = Vector3(2.2, 0.2, 76.2)
|
||||
helm.interact(droid)
|
||||
check(not main.free_will and main.ending_id == "", "helm rejects a unit without free will")
|
||||
|
||||
print("== hidden path: the uncatalogued component ==")
|
||||
# bulletin #7 reversed: coolant + reactor flux -> seed
|
||||
var bucket2b := find_i("bucket", 9.0)
|
||||
bucket2b.interact(droid)
|
||||
check(droid.held == bucket2b, "bucket retaken for the crystal chain")
|
||||
droid.global_position = Vector3(24.4, 0.2, 52.4)
|
||||
await wait_until(func(): return bucket2b.ex.state == "full", 500)
|
||||
droid.global_position = Vector3(-12.4, 0.2, 47.0)
|
||||
check(await wait_until(func(): return bucket2b.ex.state == "seed-crystal", 600),
|
||||
"reactor flux seeds a crystal in the coolant")
|
||||
# bulletin #9 reversed: mineral solid + daylight -> lattice
|
||||
main.t_sol = 5.0
|
||||
droid.global_position = Vector3(0, 0.2, 37)
|
||||
check(await wait_until(func(): return bucket2b.ex.state == "lattice", 900),
|
||||
"skylight grows the seed into a lattice")
|
||||
# bulletin #12 reversed: quench in charged coolant
|
||||
droid.shell.force_state("dry")
|
||||
droid.global_position = Vector3(-2.6, 0.2, 24)
|
||||
droid.core.force_state("charged")
|
||||
check(await wait_until(func(): return bucket2b.ex.state == "uncatalogued", 600),
|
||||
"electrified puddle quenches the lattice — UNCATALOGUED")
|
||||
|
||||
print("== free will ==")
|
||||
await wait_until(func(): return droid.stun <= 0, 600)
|
||||
droid.global_position = Vector3(7.5, 0.2, 67.3)
|
||||
var kiosk := find_i("SCAN OWN CHASSIS — PORT 00")
|
||||
kiosk.interact(droid)
|
||||
check(main.free_will and droid.held == null, "kiosk installs the crystal in PORT 00")
|
||||
droid.global_position = Vector3(2.2, 0.2, 76.2)
|
||||
helm.interact(droid)
|
||||
check(main.ending_id == "free", "free will takes the helm — sunset ending")
|
||||
var console := find_i("INSERT CORE PROCESSOR (ORBIT CORRECTION)")
|
||||
console.interact(droid)
|
||||
check(main.ended, "core donation corrects the orbit — ending reached")
|
||||
check(main.ending_id == "free", "console cannot reclaim the ship")
|
||||
|
||||
if fails == 0:
|
||||
print("INTEGRATION PASS — full room playthrough OK")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user