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>
761 lines
26 KiB
GDScript
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 ""
|