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 ""