exposure/src/main.gd
type-two 2ef9487284 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>
2026-08-03 22:24:41 +10:00

761 lines
26 KiB
GDScript

extends Node3D
## Phase 0 grey-box: one hall, five gates, three elements (light/water/electricity).
## Everything below is fields + exposables + doors — no bespoke puzzle logic
## beyond wiring. See GDD.md build plan.
const I := preload("res://src/interactables.gd")
var droid: Droid
var hud_status: Label
var hud_toast: Label
var door1: Node3D
var door2: Node3D
var door3: Node3D
var door4: Node3D
var exit_door: Node3D
var bonus_door: Node3D
var contact_a: Exposable
var contact_b: Exposable
var relay: Exposable
var solar: Exposable
var drum_switch: Exposable
var puddle_water: ElementField
var puddle_live: ElementField
var sun_field: ElementField
var sun_light: DirectionalLight3D
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 ending_id := "" # "" | "corp" | "free"
var free_will := false
var env_res: Environment
var t_sol := 10.0 # start mid-day
var is_day := true
var _live_t := 0.0
var _mats := {}
func _ready() -> void:
_init_input()
_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():
_photo_tour()
func _photo_tour() -> void:
var spots := [
[Vector3(0, 0.2, 2), PI], # spawn, facing door1
[Vector3(1.4, 0.2, 6.2), PI], # socket close-up
[Vector3(0, 0.2, 9), PI], # segment B: post + bus gap
[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]
droid.rotation.y = spots[i][1]
for f in 20:
await get_tree().process_frame
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
img.save_png("/tmp/exposure_shot_%d.png" % i)
get_tree().quit()
# ---------- input ----------
func _init_input() -> void:
var keys := {
"move_forward": KEY_W, "move_back": KEY_S,
"move_left": KEY_A, "move_right": KEY_D,
"interact": KEY_E,
}
for action in keys:
if not InputMap.has_action(action):
InputMap.add_action(action)
var ev := InputEventKey.new()
ev.physical_keycode = keys[action]
InputMap.action_add_event(action, ev)
# ---------- construction helpers ----------
var _grid_tex: ImageTexture
func grid_tex() -> ImageTexture:
if not _grid_tex:
var img := Image.create(64, 64, false, Image.FORMAT_RGB8)
for y in 64:
for x in 64:
var line := x % 16 == 0 or y % 16 == 0
img.set_pixel(x, y, Color(0.5, 0.5, 0.5) if line else Color(0.92, 0.92, 0.92))
_grid_tex = ImageTexture.create_from_image(img)
return _grid_tex
func mat(col: Color) -> StandardMaterial3D:
if not _mats.has(col):
var m := StandardMaterial3D.new()
m.albedo_color = col
_mats[col] = m
return _mats[col]
var _texs := {}
func seamless_tex(path: String) -> ImageTexture:
# mirror-fold 2x2 so any image tiles without seams
# ponytail: runtime load+fold; bake to imported textures if load time ever matters
if _texs.has(path):
return _texs[path]
var img := Image.load_from_file(ProjectSettings.globalize_path(path))
img.convert(Image.FORMAT_RGB8)
var w := img.get_width()
var h := img.get_height()
var out := Image.create(w * 2, h * 2, false, Image.FORMAT_RGB8)
var r := Rect2i(0, 0, w, h)
out.blit_rect(img, r, Vector2i(0, 0))
var fx: Image = img.duplicate()
fx.flip_x()
out.blit_rect(fx, r, Vector2i(w, 0))
var fy: Image = img.duplicate()
fy.flip_y()
out.blit_rect(fy, r, Vector2i(0, h))
var fxy: Image = fx.duplicate()
fxy.flip_y()
out.blit_rect(fxy, r, Vector2i(w, h))
out.generate_mipmaps()
_texs[path] = ImageTexture.create_from_image(out)
return _texs[path]
func surface_mat(file: String, tile_m: float, col := Color.WHITE) -> StandardMaterial3D:
var path := "res://assets/textures/" + file
if not FileAccess.file_exists(path):
return struct_mat(Color(0.5, 0.52, 0.58)) # fallback: grid grey-box
var key := "t" + file + str(tile_m)
if not _mats.has(key):
var m := StandardMaterial3D.new()
m.albedo_color = col
m.albedo_texture = seamless_tex(path)
m.uv1_triplanar = true
m.uv1_scale = Vector3.ONE / tile_m
_mats[key] = m
return _mats[key]
func struct_mat(col: Color) -> StandardMaterial3D:
var key := "g" + str(col)
if not _mats.has(key):
var m := StandardMaterial3D.new()
m.albedo_color = col
m.albedo_texture = grid_tex()
m.uv1_triplanar = true
m.uv1_scale = Vector3(0.25, 0.25, 0.25) # 1m grid cells
_mats[key] = m
return _mats[key]
func glow(col: Color, energy := 0.7) -> StandardMaterial3D:
var key := "e" + str(col) + str(energy)
if not _mats.has(key):
var m := StandardMaterial3D.new()
m.albedo_color = col
m.emission_enabled = true
m.emission = col
m.emission_energy_multiplier = energy
_mats[key] = m
return _mats[key]
const SURF := {"wall": ["wall.jpg", 3.0], "floor": ["floor.jpg", 2.0],
"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:
var b := CSGBox3D.new()
b.size = size
b.position = pos
b.use_collision = true
if surf:
b.material = surface_mat(SURF[surf][0], SURF[surf][1])
elif grid:
b.material = struct_mat(col)
else:
b.material = mat(col)
(parent if parent else self).add_child(b)
return b
func sign3d(text: String, pos: Vector3, rot_y := PI, size := 30, col := Color(0.55, 1.0, 0.6)) -> Label3D:
var l := Label3D.new()
l.text = text
l.position = pos
l.rotation.y = rot_y
l.font_size = size
l.modulate = col
add_child(l)
return l
func divider(z: float) -> void:
var c := Color(0.45, 0.46, 0.52)
box(Vector3(-2.6, 2, z), Vector3(2.8, 4, 0.3), c, null, true, "bulk")
box(Vector3(2.6, 2, z), Vector3(2.8, 4, 0.3), c, null, true, "bulk")
box(Vector3(0, 3.5, z), Vector3(2.4, 1, 0.3), c, null, true, "bulk")
func raw_tex(path: String) -> ImageTexture:
if _texs.has(path):
return _texs[path]
var img := Image.load_from_file(ProjectSettings.globalize_path(path))
img.generate_mipmaps()
_texs[path] = ImageTexture.create_from_image(img)
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()
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
(parent if parent else self).add_child(e)
return e
# ---------- world ----------
func _build_env() -> void:
var we := WorldEnvironment.new()
var env := Environment.new()
env.background_mode = Environment.BG_COLOR
env.background_color = Color(0.02, 0.03, 0.07)
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
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()
sun_light.rotation = Vector3(-0.9, 0.5, 0)
sun_light.light_energy = 1.2
add_child(sun_light)
# corridor lighting — segments feel lit, not fog-grey
for z in [4.0, 12.0, 18.5, 24.0, 30.0, 37.0, 42.5]:
var o := OmniLight3D.new()
o.position = Vector3(0, 3.4, z)
o.omni_range = 10.0
o.light_energy = 1.35
o.light_color = Color(1.0, 0.96, 0.88)
add_child(o)
for p in [Vector3(8, 6.5, 22), Vector3(6.4, 3.2, 25.8), Vector3(-5.0, 3.0, 24)]:
var o := OmniLight3D.new()
o.position = p
o.omni_range = 8.0
o.light_energy = 1.0
add_child(o)
func _build_geometry() -> void:
var wall := Color(0.5, 0.52, 0.58)
# floor
box(Vector3(3, -0.15, 22.5), Vector3(18.6, 0.3, 46), Color(0.62, 0.62, 0.65), null, true, "floor")
# left wall x=-4.15, niche gap z 23.4..24.6
box(Vector3(-4.15, 2, 11.45), Vector3(0.3, 4, 23.9), wall, null, true, "wall")
box(Vector3(-4.15, 2, 33.55), Vector3(0.3, 4, 17.9), wall, null, true, "wall")
# relay niche (behind left wall) + bars
box(Vector3(-5.8, 2, 24), Vector3(0.3, 4, 1.6), wall, null, true, "greeble")
box(Vector3(-5.0, 2, 23.4), Vector3(1.6, 4, 0.3), wall, null, true, "wall")
box(Vector3(-5.0, 2, 24.6), Vector3(1.6, 4, 0.3), wall, null, true, "wall")
box(Vector3(-4.15, 1, 23.8), Vector3(0.1, 2, 0.1), Color(0.15, 0.15, 0.15))
box(Vector3(-4.15, 1, 24.2), Vector3(0.1, 2, 0.1), Color(0.15, 0.15, 0.15))
# right wall x=4.15: gaps for drum bay (z 20..24) and bonus bay (z 25..26.6)
box(Vector3(4.15, 2, 9.75), Vector3(0.3, 4, 20.5), wall, null, true, "wall")
box(Vector3(4.15, 2, 24.5), Vector3(0.3, 4, 1.0), wall, null, true, "wall")
box(Vector3(4.15, 2, 34.55), Vector3(0.3, 4, 15.9), wall, null, true, "wall")
# bonus bay shell
box(Vector3(8.65, 2, 25.8), Vector3(0.3, 4, 3.0), wall, null, true, "greeble")
box(Vector3(6.4, 2, 24.9), Vector3(4.4, 4, 0.3), wall, null, true, "wall")
box(Vector3(6.4, 2, 26.7), Vector3(4.4, 4, 0.3), wall, null, true, "wall")
# drum bay outer shell
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 wall (south only — north opens into the hub)
box(Vector3(0, 2, -0.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)
func _build_objects() -> void:
door1 = make_door(8.0)
door2 = make_door(16.0)
door3 = make_door(26.0)
door4 = make_door(34.0)
exit_door = make_door(40.0)
bonus_door = I.Door.new()
bonus_door.width = 1.6
bonus_door.position = Vector3(4.15, 0, 25.8)
bonus_door.rotation.y = PI / 2.0
add_child(bonus_door)
# --- segment A: donation socket ---
var socket := I.Socket.new()
socket.door = door1
socket.position = Vector3(1.8, 1.2, 7.6)
add_child(socket)
sign3d("DOOR BUS — COMPONENT MISSING", Vector3(1.8, 2.6, 7.55))
sign3d("GC ORIENTATION: YOU ARE A VALUED GENERAL COMPONENT COMPONENT.",
Vector3(-2.0, 2.2, 4.0), PI, 22, Color(1.0, 0.75, 0.3))
# --- segment B: charge post + be-the-wire gap ---
box(Vector3(-2.5, 0.9, 10.5), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6)).material = glow(Color(0.15, 0.7, 0.85))
var post1 := ElementField.make("electricity", 1.3, Color(0.3, 1.0, 1.0, 0.12))
post1.position = Vector3(-2.5, 0.9, 10.5)
add_child(post1)
sign3d("CHARGE POST", Vector3(-2.5, 2.3, 10.2))
for x in [-1.0, 1.0]:
box(Vector3(x, 1.0, 15.3), Vector3(0.25, 2.0, 0.25), Color(0.55, 0.55, 0.6)).material = glow(Color(0.8, 0.8, 0.5), 0.5)
var contact_trans := [{"from": "idle", "to": "hot", "element": "electricity",
"duration": 0.15, "decay_to": "idle", "decay_s": 0.6}]
contact_a = make_exposable("BUS CONTACT A", "idle", contact_trans.duplicate(true), Vector3(-1.0, 1.0, 15.3))
contact_b = make_exposable("BUS CONTACT B", "idle", contact_trans.duplicate(true), Vector3(1.0, 1.0, 15.3))
sign3d("BUS GAP — CONDUCTOR REQUIRED. STAND IN THE GAP, CHARGED.", Vector3(0, 2.7, 15.2))
var wheel := I.Pickup.new()
wheel.kind = "wheel"
wheel.prompt_text = "TAKE WHEEL"
wheel.position = Vector3(2.5, 0.4, 17.5)
add_child(wheel)
sign3d("SPARE MOBILITY UNIT — GC CAT. 6601-W", Vector3(2.5, 1.4, 17.8))
# --- segment C: post 2, drum bay, puddle + relay ---
box(Vector3(-2.8, 0.9, 20.5), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6)).material = glow(Color(0.15, 0.7, 0.85))
var post2 := ElementField.make("electricity", 1.3, Color(0.3, 1.0, 1.0, 0.12))
post2.position = Vector3(-2.8, 0.9, 20.5)
add_child(post2)
sign3d("CHARGE POST", Vector3(-2.8, 2.3, 20.2))
# rotating drum, opening starts facing UP; +90° about z turns it to face the corridor
drum = Node3D.new()
drum.position = Vector3(8, 3.5, 22)
add_child(drum)
var dc := Color(0.42, 0.38, 0.3)
box(Vector3(0, -3.2, 0), Vector3(6.8, 0.4, 4.4), dc, drum, false, "bulk")
box(Vector3(3.2, 0, 0), Vector3(0.4, 6.8, 4.4), dc, drum, false, "bulk")
box(Vector3(-3.2, 0, 0), Vector3(0.4, 6.8, 4.4), dc, drum, false, "bulk")
box(Vector3(0, 0, 2.2), Vector3(6.8, 6.8, 0.4), dc, drum, false, "bulk")
box(Vector3(0, 0, -2.2), Vector3(6.8, 6.8, 0.4), dc, drum, false, "bulk")
box(Vector3(-2.6, 2.3, 0), Vector3(1.0, 0.15, 1.0), dc, drum) # shelf
var crate := I.Pickup.new()
crate.kind = "crate"
crate.prompt_text = "TAKE MAG-CLAMPED CARGO"
crate.tint = Color(0.55, 0.4, 0.2)
crate.size = Vector3(0.6, 0.5, 0.6)
crate.position = Vector3(-2.6, 2.65, 0)
drum.add_child(crate)
box(Vector3(3.7, 0.7, 18.6), Vector3(0.3, 1.4, 0.3), Color(0.5, 0.4, 0.2)).material = glow(Color(0.9, 0.6, 0.15), 0.5)
drum_switch = make_exposable("DRUM MOTOR BUS", "idle",
[{"from": "idle", "to": "active", "element": "electricity",
"duration": 0.8, "permanent": true}],
Vector3(3.7, 1.2, 18.6))
drum_switch.state_changed.connect(_on_drum_switch)
sign3d("CARGO DRUM STUCK. MOTOR BUS DEAD — APPLY CURRENT.", Vector3(3.4, 2.6, 18.6), PI, 26)
var pm := MeshInstance3D.new()
var pmesh := CylinderMesh.new()
pmesh.top_radius = 1.6
pmesh.bottom_radius = 1.6
pmesh.height = 0.05
pm.mesh = pmesh
pm.position = Vector3(-2.6, 0.03, 24)
pm.material_override = mat(Color(0.15, 0.3, 0.6))
add_child(pm)
puddle_water = ElementField.make("water", 1.6, Color(0.2, 0.4, 1.0, 0.10))
puddle_water.position = Vector3(-2.6, 0.4, 24)
add_child(puddle_water)
puddle_live = ElementField.make("electricity", 2.6, Color(0.3, 1.0, 1.0, 0.10))
puddle_live.position = Vector3(-2.6, 0.4, 24)
puddle_live.enabled = false
add_child(puddle_live)
box(Vector3(-5.0, 1.0, 24), Vector3(0.4, 0.4, 0.4), Color(0.6, 0.3, 0.3)).material = glow(Color(0.9, 0.25, 0.2), 0.6)
relay = make_exposable("SLUICE RELAY", "open",
[{"from": "open", "to": "tripped", "element": "electricity",
"duration": 1.0, "permanent": true}],
Vector3(-5.0, 1.0, 24))
relay.state_changed.connect(func(_f, to, _p): if to == "tripped": door3.open())
sign3d("SLUICE RELAY — SEALED BAY. CURRENT REQUIRED AT RELAY.", Vector3(-3.9, 2.6, 23.0), PI / 2.0, 24)
sign3d("CAUTION: COOLANT SPILL. DO NOT INTRODUCE CURRENT TO STANDING COOLANT.",
Vector3(-2.6, 1.9, 22.6), PI, 22, Color(1.0, 0.75, 0.3))
var plate := I.Plate.new()
plate.door = bonus_door
plate.position = Vector3(2.0, 0.02, 25.0)
add_child(plate)
sign3d("CARGO PLATE", Vector3(2.0, 1.0, 25.3))
# bonus bay contents
var wheel2 := I.Pickup.new()
wheel2.kind = "wheel"
wheel2.prompt_text = "TAKE SPARE WHEEL"
wheel2.position = Vector3(6.5, 0.4, 25.8)
add_child(wheel2)
sign3d("GC SAFETY BULLETIN #7:\nDO NOT EXPOSE G-9 COOLANT TO REACTOR FLUX.\nCRYSTALLIZATION HAZARD.",
Vector3(7.2, 2.0, 25.8), PI / 2.0, 24, Color(1.0, 0.75, 0.3))
# --- segment D: valve, bucket, tank ---
box(Vector3(-3.8, 1.4, 28), Vector3(0.5, 0.5, 0.5), Color(0.3, 0.5, 0.7)).material = glow(Color(0.25, 0.5, 0.9), 0.5)
box(Vector3(-3.4, 0.9, 28), Vector3(0.12, 1.6, 0.12), Color(0.3, 0.5, 0.9)).material = glow(Color(0.3, 0.55, 1.0), 0.9) # stream
var valve := ElementField.make("water", 1.3, Color(0.2, 0.4, 1.0, 0.10))
valve.position = Vector3(-3.3, 0.8, 28)
add_child(valve)
sign3d("COOLANT VALVE — DRIPPING", Vector3(-3.4, 2.4, 27.7))
var bucket := I.Bucket.new()
bucket.position = Vector3(-2.8, 0, 29)
add_child(bucket)
var tank := I.Tank.new()
tank.door = door4
tank.position = Vector3(3.2, 0, 32.5)
add_child(tank)
tank.label = sign3d("GRAV TANK 0/2", Vector3(3.2, 1.9, 32.2))
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.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))
panel.material = glow(Color(0.15, 0.25, 0.7), 0.5)
panel.rotation.x = -0.5
solar = make_exposable("SOLAR CELL", "unpowered",
[{"from": "unpowered", "to": "powered", "element": "light",
"duration": 0.5, "decay_to": "unpowered", "decay_s": 1.5}],
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)
func _build_droid() -> void:
droid = Droid.new()
droid.position = Vector3(0, 0.2, 2)
droid.rotation.y = PI
add_child(droid)
func _build_hud() -> void:
var cl := CanvasLayer.new()
add_child(cl)
hud_status = Label.new()
hud_status.position = Vector2(14, 10)
hud_status.add_theme_font_size_override("font_size", 15)
hud_status.add_theme_color_override("font_color", Color(0.55, 1.0, 0.6))
cl.add_child(hud_status)
hud_toast = Label.new()
hud_toast.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE)
hud_toast.offset_top = -90
hud_toast.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hud_toast.add_theme_font_size_override("font_size", 19)
hud_toast.add_theme_color_override("font_color", Color(1.0, 0.75, 0.3))
cl.add_child(hud_toast)
var cross := Label.new()
cross.text = "+"
cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
cross.add_theme_font_size_override("font_size", 20)
cross.add_theme_color_override("font_color", Color(0.55, 1.0, 0.6, 0.7))
cl.add_child(cross)
_marker = Label3D.new()
_marker.text = ""
_marker.font_size = 72
_marker.modulate = Color(0.55, 1.0, 0.6)
_marker.billboard = BaseMaterial3D.BILLBOARD_ENABLED
_marker.no_depth_test = true
_marker.visible = false
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 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
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:
if to == "active" and not drum_turned:
drum_turned = true
droid.toast("DRUM MOTOR LIVE — ROTATING CARGO DRUM")
create_tween().tween_property(drum, "rotation:z", PI / 2.0, 3.0)
func _physics_process(delta: float) -> void:
# 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
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
if not door2.is_open and contact_a.state == "hot" and contact_b.state == "hot":
door2.open()
droid.toast("CIRCUIT COMPLETE — YOU WERE THE WIRE")
# conductive puddle: any live electricity field near it goes live for a while
var live := false
for f in get_tree().get_nodes_in_group("elem_field"):
if f == puddle_live or f == puddle_water:
continue
if f.enabled and f.element == "electricity" \
and f.global_position.distance_to(puddle_water.global_position) <= puddle_water.radius + 0.6:
live = true
break
if live:
puddle_live.enabled = true
_live_t = 4.0
elif puddle_live.enabled:
_live_t -= delta
if _live_t <= 0.0:
puddle_live.enabled = false
# exit door follows solar cell state
if solar.state == "powered" and not exit_door.is_open:
exit_door.open()
elif solar.state != "powered" and exit_door.is_open:
exit_door.close()
# 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:
var phase := fmod(t_sol, 60.0)
var sol := "DAY (%ds)" % int(40.0 - phase) if is_day else "NIGHT (%ds)" % int(60.0 - phase)
var core_s := droid.core.state.to_upper()
if droid.core.decay_remaining() > 0:
core_s += " (%ds)" % ceil(droid.core.decay_remaining())
var shell_s := droid.shell.state.to_upper()
if droid.shell.decay_remaining() > 0:
shell_s += " (%ds)" % ceil(droid.shell.decay_remaining())
var holding := ""
if droid.held:
holding = str(droid.held.get("kind")).to_upper()
var ex: Exposable = droid.held.get("ex") if droid.held.get("kind") == "bucket" else null
if ex:
holding += " [%s%s]" % [ex.state.to_upper(),
" %ds" % ceil(ex.decay_remaining()) if ex.decay_remaining() > 0 else ""]
var obj := "WALK OUT THE EXIT."
if not door1.is_open:
obj = "DOOR 1 BUS IS MISSING A COMPONENT. YOU ARE MADE OF COMPONENTS."
elif not door2.is_open:
obj = "BUS GAP AHEAD — GET CHARGED AT THE POST, STAND IN THE GAP."
elif not door3.is_open:
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 hall_done:
obj = "EXIT RUNS ON SUNLIGHT. CHECK THE SOL CLOCK."
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:
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 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 := [
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,
]
var scan := droid.best_target(["exposable"], 8.0, 0.93)
if scan:
var d := ""
if scan.decay_remaining() > 0:
d = " (%ds)" % ceil(scan.decay_remaining())
lines.append("SCAN: %s%s%s" % [scan.display_name, scan.state.to_upper(), d])
else:
lines.append("SCAN: —")
var target := droid.best_target(["interactable"], 3.0, 0.7)
if target:
lines.append("[E] %s" % target.get("prompt_text"))
_marker.visible = true
_marker.global_position = target.global_position + Vector3(0, 1.1 + 0.12 * sin(t_sol * 5.0), 0)
else:
_marker.visible = false
if droid.stun > 0:
lines.append(">>> REBOOTING <<<")
hud_status.text = "\n".join(lines)
hud_toast.text = droid.toast_text if droid.toast_t > 0 else ""