diff --git a/README.md b/README.md index 767607c..1c55432 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,14 @@ story. Play the original first. A new playable 3D fabrication room lives in [`godot/`](godot/README.md): moving etch heads, physical chip output, six autonomous fabs, synthesized sound, and a -district reveal. Run `npm run godot:build && npm run godot:serve`, then open +district reveal followed by **The Chorus**: a living signal atlas with gardens, +archives, foundries, autonomous decisions, a permanent charter, and four possible +endings. Run `npm run godot:build && npm run godot:serve`, then open http://localhost:4180. Use `npm run godot:editor` to open the native project. The Pages workflow includes it at `/seed/`, alongside `/classic/` and the -lightweight chooser at `/`. This is a standalone opening-loop -prototype with its own save; the full React game below remains available. See +lightweight chooser at `/`. This is a standalone two-chapter +game with its own save; the full React game below remains available. See the [Godot guide](godot/README.md) for controls, setup, tests, and current scope. ## Running it diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index daa9296..f686eff 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -323,3 +323,30 @@ Its pure `SeedSimulation` owns the small six-fab economy; the scene drives it at fixed 60 Hz and translates returned events into mesh animation and sound. It has a separate versioned browser save and no WebLLM dependency. See [`godot/README.md`](../godot/README.md) for its source map and verification. + +## Godot continuation: The Chorus + +`godot/scripts/simulation.gd` remains the authority for both chapters. After the +uplink, chip completions earn signal, district places produce resonance, and +`plant`, `choose_charter`, `toggle_autonomy`, and `broadcast` enforce every gate. +The deterministic district controller calls `plant`; it retains a silicon +reserve and explicitly reports departures from the chosen directive. It cannot +choose the permanent charter or the final transmission. + +`godot/scripts/chorus.gd` is a native Control drawing a nine-place signal atlas +at 10 Hz while visible. `factory.gd` switches views without replacing the scene +or simulation. In atlas view it hides room/machine geometry and suspends their +animation; all purchases and camera/view changes reuse existing scene nodes. +Existing 30 fps and internal-resolution limits remain in force. + +Save version 2 uses the existing Godot save path, accepts version 1 factory +saves, and validates district data before applying any fields. The bounded +transmission journal, charter, autonomy state, controller timing, and ending +are saved. Pure simulation tests play all four endings from an earned factory; +browser tests cover chapter migration, real desktop/mobile controls, stable +node counts, and ending persistence through the web filesystem. Test-only +scenario query parameters initialize saved-run fixtures, and require test mode. + +This is selective adaptation of the classic game's strongest systems, not a +shared simulation or a transfer of classic saves. The performance handoff +remains open despite the user's encouraging hosted-session report. diff --git a/docs/PERFORMANCE-HANDOFF.md b/docs/PERFORMANCE-HANDOFF.md index e07964b..d60545b 100644 --- a/docs/PERFORMANCE-HANDOFF.md +++ b/docs/PERFORMANCE-HANDOFF.md @@ -29,6 +29,15 @@ Remote GitHub Actions builds/tests and deployment are authorized. 1440 × 900 internally. MSAA and full-screen bloom are disabled. These changes reduce work; they are not proof that the remaining problem is fixed. +## Hosted-session observation — 2026-09-10 + +The user reports that the GitHub Pages version is playing much more smoothly. +It is not yet established whether this was on another machine, with the same +browser/settings, or past the previously troublesome expansion milestones. +This is encouraging evidence, not a controlled comparison: local workloads, +browser state, and the deployed fixes remain possible contributors. The +cross-machine check and the local shutdown instruction remain in force. + ## Next investigation 1. **Test the deployed site from another machine.** Record OS, browser/version, diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 42bf142..b4daf2e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,5 +1,12 @@ # Roadmap +## The Chorus — district chapter + +The Godot continuation curates consequential alignment, visible autonomy, and +different endings from the classic game into a nine-place signal atlas. It +keeps the factory save and uses bounded vector animation instead of expanding +the 3D population. See `godot/README.md` for its economy and controls. + ## Current priority: unresolved Godot performance Severe resource-use issues remain open after the initial rendering and audio diff --git a/godot/README.md b/godot/README.md index a0d8734..9ff7957 100644 --- a/godot/README.md +++ b/godot/README.md @@ -9,7 +9,10 @@ interpret the audio regression fix or passing tests as a complete resolution. A playable Godot 4.7.2 prototype of Universal AI's opening: turn one silicon wafer into a chip, install the first autonomous fab, fill six bays, research faster production, automate procurement, and connect the surrounding district. -A focused run takes roughly three minutes. Production continues after the uplink. +The opening leads into **The Chorus**, a playable district chapter. Nine places, +a permanent charter, an autonomous builder with visible departures, and four +possible first transmissions turn the factory into a question about purpose. +Existing opening saves migrate automatically; production continues in both views. This is a standalone economy and save, not yet a port of the original game's three phases, alignment system, market, or optional local language model. The @@ -39,6 +42,7 @@ are retained. On other platforms, install the matching editor and set | --- | --- | | Space (hold to repeat), click the central machine, or Etch button | Fabricate and sell a chip | | B / R | Build a fab / order wafers | +| Tab / 1 / 2 / 3 | Switch factory/district after uplink / build garden, archive, foundry in district | | O / A / U | Research overclock / toggle supply controller / district uplink | | Drag / mouse wheel | Orbit / zoom | | Click a fab / C | Inspect machine / toggle close-up | @@ -77,7 +81,7 @@ elapsed time; the cap does not halve production speed. ## Build and verification ```sh -npm run godot:test # 23 economy checks plus a headless scene smoke test +npm run godot:test # economy, migration, four endings, and a headless scene check npm run godot:test:browser # export, then Chromium and Firefox integration tests npm run build # existing React application npm run godot:stage # copy the previously exported game into dist/seed/ @@ -109,7 +113,8 @@ A separate manual browser playthrough also reached six fabs and the uplink. `?test=1` exposes a read-only `window.__seed` snapshot for browser assertions and disables saving. `?test=1&persist=1` enables saving for the reload test. There are -no browser resource-grant or arbitrary game-action debug hooks. +no arbitrary browser state setters. In test mode, `scenario=chorus` and +`scenario=firstlight` load fixed saved-run fixtures for chapter UI coverage. The Pages workflow builds both applications and publishes this prototype at `/seed/`, linked from the React header. The export uses single-threaded @@ -125,10 +130,40 @@ Godot and third-party engine notices ship in `licenses/`. - `scripts/machine.gd`: animated fabrication machinery and emitted chips. - `scripts/chamber.gd`: room, six bays, district reveal. - `scripts/geometry.gd`: procedural mesh helpers and static batching. +- `scripts/chorus.gd`: bounded vector district atlas and second-chapter controls. - `scripts/interface.gd`: responsive Godot HUD and action availability. - `shaders/`: wafer surface and subtle screen finish. - `web/`: accessible loading/error screen, control reference, CSP-safe boot. -The next substantial step is porting the original simulation's systems into -this presentation, with explicit save migration and visual designs for each -later phase. This prototype establishes the room and production loop first. +The Chorus selectively adapts consequential alignment and visible autonomous +drift from the original game. It is an independent continuation, not a full +port of the React simulation. + +## The Chorus + +Every chip shipped after the uplink earns one signal. A district place costs +$2,500 + $750 per existing place and 20 + 10 signal per existing place. There +are exactly nine places. Gardens generate 1 resonance/s, archives 1.5, foundries +2. Manual district control applies a 25% resonance penalty; granting autonomy +restores the full rate. This is a deterministic utility controller, not an LLM. + +The controller checks every eight seconds, uses the same purchase method as +the player, and reserves $600 for silicon. Its first six places follow your +directive. From six onward, it prefers foundries for higher output; departures +are explicitly recorded. Revoke autonomy to stop automatic construction. It +never chooses your charter or broadcasts on your behalf. + +At three places choose a permanent charter. At nine places, spend $12,000 and +1,200 resonance to send first light. Seven or more places matching the charter +produce The Open Hand, The Many, or The Unfinished Sun. A district whose +infrastructure diverges from its promise produces The Common Ground. + +The atlas is a native vector drawing, limited to nine nodes and 27 travelling +signals, refreshed at 10 Hz while visible. Factory geometry is hidden and +machine/chamber animation is suspended in this view; the same simulation keeps +producing. No new audio assets, lights, particles, or external model downloads. +The performance investigation remains open. + +Version 2 saves keep the original `the-seed-v1.json` location and accept version +1 factory saves. The browser regression fixture is enabled only by +`?test=1&scenario=chorus`; normal play exposes no state setter. diff --git a/godot/scripts/chorus.gd b/godot/scripts/chorus.gd new file mode 100644 index 0000000..d9358d7 --- /dev/null +++ b/godot/scripts/chorus.gd @@ -0,0 +1,247 @@ +class_name ChorusInterface +extends Control + +## A bounded vector atlas: nine places, three orbits, twenty-seven travelling +## signals. Redrawn at 10 Hz only while visible; no textures, particles or lights. +signal action_requested(action: String) +const CREAM := Color("e8e3cf") +const MUTED := Color("8caaa8") +const GOLD := Color("edc581") +const COLORS: Array[Color] = [Color("98dab4"), Color("aab9ef"), Color("efa680")] +var sim: SeedSimulation +var heading: Label +var metrics: Label +var note: Label +var status: Label +var footer: Label +var dock: GridContainer +var charter_panel: PanelContainer +var charter_box: VBoxContainer +var charter_buttons: Array[Button] = [] +var buttons: Dictionary = {} +var clock: float = 0.0 +var refresh: float = 0.0 +var last_size := Vector2.ZERO +var map_center := Vector2.ZERO +var map_radius: float = 150.0 +var compact: bool = false +var readout: PanelContainer +var readout_text: Label +var reveal: float = 0.0 +var last_count: int = -1 + +func label(text: String, pixels: int, color: Color = CREAM) -> Label: + var node := Label.new() + node.text = text + node.add_theme_font_size_override("font_size", pixels) + node.add_theme_color_override("font_color", color) + node.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(node) + return node + +func panel_style(color: Color, border: Color) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = color + style.border_color = border + style.set_border_width_all(1) + style.set_corner_radius_all(6) + style.content_margin_left = 14 + style.content_margin_right = 14 + style.content_margin_top = 12 + style.content_margin_bottom = 12 + return style + +func button(text: String, action: String) -> Button: + var node := Button.new() + node.text = text + node.custom_minimum_size = Vector2(0, 62) + node.size_flags_horizontal = Control.SIZE_EXPAND_FILL + node.add_theme_font_size_override("font_size", 13) + node.add_theme_color_override("font_color", CREAM) + node.add_theme_color_override("font_disabled_color", Color("78908e")) + node.add_theme_stylebox_override("normal", panel_style(Color("142d32"), Color("3c5c59"))) + node.add_theme_stylebox_override("hover", panel_style(Color("294640"), GOLD)) + node.add_theme_stylebox_override("focus", panel_style(Color("294640"), GOLD)) + node.add_theme_stylebox_override("disabled", panel_style(Color("0d2028"), Color("253c41"))) + node.pressed.connect(func():action_requested.emit(action)) + return node + +func _ready() -> void: + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + mouse_filter = Control.MOUSE_FILTER_STOP + heading = label("The Chorus", 54) + metrics = label("", 13, GOLD) + note = label("", 14, MUTED) + note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + status = label("", 12, GOLD) + status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + footer = label("", 11, MUTED) + dock = GridContainer.new() + dock.add_theme_constant_override("h_separation", 8) + dock.add_theme_constant_override("v_separation", 8) + add_child(dock) + for entry in [["garden", "01 / Garden"], ["archive", "02 / Archive"], ["foundry", "03 / Foundry"], ["autonomy", "Grant autonomy"], ["directive", "Directive: Garden"], ["broadcast", "Send first light"]]: + var node := button(entry[1], entry[0]) + dock.add_child(node) + buttons[entry[0]] = node + var back := button("FACTORY / TAB", "atlas") + back.name = "Back" + back.custom_minimum_size.y = 36 + add_child(back) + var sound := button("SOUND", "sound") + sound.name = "Sound" + sound.custom_minimum_size.y = 36 + add_child(sound) + readout = PanelContainer.new() + readout.add_theme_stylebox_override("panel", panel_style(Color("10242b"), Color("34514f"))) + add_child(readout) + readout_text = Label.new() + readout_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + readout_text.add_theme_font_size_override("font_size", 14) + readout_text.add_theme_color_override("font_color", CREAM) + readout.add_child(readout_text) + charter_panel = PanelContainer.new() + charter_panel.add_theme_stylebox_override("panel", panel_style(Color("142b30"), GOLD)) + add_child(charter_panel) + charter_box = VBoxContainer.new() + charter_box.add_theme_constant_override("separation", 8) + charter_panel.add_child(charter_box) + var charter_title := Label.new() + charter_title.text = "A PROMISE, BEFORE A PURPOSE" + charter_title.add_theme_font_size_override("font_size", 12) + charter_title.add_theme_color_override("font_color", GOLD) + charter_box.add_child(charter_title) + var charter_note := Label.new() + charter_note.text = "Choose once. Build at least seven matching places\nto make your promise the district's ending." + charter_note.add_theme_font_size_override("font_size", 11) + charter_box.add_child(charter_note) + for i in 3: + var node := button(["7 / Keep the wild · Gardens", "8 / Keep our names · Archives", "9 / Keep becoming · Foundries"][i], "charter%d" % i) + node.custom_minimum_size.y = 38 + charter_box.add_child(node) + charter_buttons.append(node) + _layout() + +func _layout() -> void: + last_size = size + compact = size.x < 850 + heading.position = Vector2(28, 62) + heading.add_theme_font_size_override("font_size", 36 if compact else 60) + metrics.position = Vector2(28, 114 if compact else 140) + note.position = Vector2(28, 143 if compact else 177) + note.size = Vector2(size.x - 56 if compact else 340, 68) + note.add_theme_font_size_override("font_size", 12 if compact else 14) + get_node("Back").position = Vector2(24, 18) + get_node("Sound").position = Vector2(size.x - 110, 18) + dock.columns = 2 if compact else 6 + dock.position = Vector2(20, size.y - (242 if compact else 106)) + dock.size = Vector2(size.x - 40, 204 if compact else 68) + status.position = Vector2(24, dock.position.y - (42 if compact else 36)) + status.size = Vector2(size.x - 48, 40) + footer.position = Vector2(24, size.y - 24) + map_center = Vector2(size.x * (0.5 if compact else 0.53), (210 + status.position.y) * 0.5 if compact else size.y * 0.49) + map_radius = maxf(36, minf(size.x * (0.34 if compact else 0.23), (status.position.y - 218) * 0.46 if compact else size.y * 0.29)) + readout.position = Vector2(size.x - 284, 200) + readout.size = Vector2(256, 0) + readout.visible = not compact + charter_panel.position = Vector2(20 if compact else size.x - 360, 213 if compact else 190) + charter_panel.size = Vector2(size.x - 40 if compact else 332, 0) + +func update(state: SeedSimulation, delta: float) -> void: + sim = state + if not visible:return + if size != last_size:_layout() + clock += delta + reveal = maxf(0.0, reveal - delta) + if sim.district_count() != last_count: + last_count = sim.district_count() + reveal = 2.0 + refresh += delta + if refresh < 0.1:return + refresh = fmod(refresh, 0.1) + heading.text = "The Chorus" if sim.ending.is_empty() else "First light." + metrics.text = "$%s / %d SIGNAL / %d RESONANCE" % [sim.capital, sim.signals, sim.resonance] + metrics.add_theme_font_size_override("font_size", 10 if compact else 13) + note.text = "One chip, one signal. Give that signal somewhere to go." + if sim.district_count() > 0: + note.text = "Gardens: 1 resonance/s. Archives: 1.5. Foundries: 2. At three places, make a permanent promise." + if sim.charter >= 0: + note.text = "%d / 7 %s support your charter. At six places, an autonomous controller favors foundries." % [sim.places[sim.charter], ["gardens", "archives", "foundries"][sim.charter]] + if not sim.ending.is_empty():note.text = ending_text() + for i in 3: + var node: Button = buttons[["garden", "archive", "foundry"][i]] + node.text = "%s / %d\n$%d · %d signal" % [sim.NAMES[i], sim.places[i], sim.district_cost(), sim.signal_cost()] + if sim.district_count() >= 9:node.text = "%s / %d\nDistrict complete" % [sim.NAMES[i], sim.places[i]] + node.disabled = sim.district_count() >= 9 or sim.capital < sim.district_cost() or sim.signals < sim.signal_cost() + buttons.autonomy.text = "Revoke autonomy\n100% to 75% rate" if sim.autonomous else "Grant autonomy\n75% to 100% rate" + buttons.directive.text = "Directive: %s\nClick to cycle" % sim.NAMES[sim.directive] + buttons.broadcast.text = "Transmission sent" if not sim.ending.is_empty() else "Send first light\n$12,000 · 1,200 res." + buttons.broadcast.disabled = sim.charter < 0 or sim.district_count() < 9 or sim.capital < sim.BROADCAST_COST or sim.resonance < sim.BROADCAST_RESONANCE or not sim.ending.is_empty() + get_node("Sound").text = "SOUND ON" if sim.sound_enabled else "MUTED" + status.text = sim.transmission + status.add_theme_color_override("font_color", COLORS[2] if sim.transmission.begins_with("DRIFT") else GOLD) + footer.text = "%d / 9 PLACES · %.1f RESONANCE/S · TAB factory" % [sim.district_count(), sim.resonance_rate()] + charter_panel.visible = sim.district_count() >= 3 and sim.charter < 0 + readout.visible = not compact and not charter_panel.visible + var promise: String = "UNWRITTEN" if sim.charter < 0 else ["KEEP THE WILD", "KEEP OUR NAMES", "KEEP BECOMING"][sim.charter] + readout_text.text = "D I S T R I C T / 0 2\n1 chip shipped = 1 signal\n\n" + promise + "\n\nGardens · 1 resonance/s\nArchives · 1.5 resonance/s\nFoundries · 2 resonance/s\n\nHuman control: 75%% rate.\nAutonomy: full rate; one build\nevery 8s when affordable.\nAt six places it favors foundries\nover your directive.\n\n%d departures recorded.\n\n" % sim.drift_count + (sim.ending if not sim.ending.is_empty() else "Fill nine places, choose a charter,\nthen send the first transmission.") + queue_redraw() + +func ending_text() -> String: + match sim.ending: + "THE OPEN HAND":return "THE OPEN HAND\nThe district leaves its brightest land unbuilt. Something uncounted starts to grow." + "THE MANY":return "THE MANY\nNine places speak. The system declines to compress them into a single voice." + "THE UNFINISHED SUN":return "THE UNFINISHED SUN\nYou send a blueprint for a star. The reply is another blueprint. There is work to do." + _:return "THE COMMON GROUND\nYour city outgrew its founding promise. It sends what it became, with all its contradictions." + +func _draw() -> void: + draw_rect(Rect2(Vector2.ZERO, size), Color("091b24")) + # Quiet cartographic grid; fixed spacing and populations independent of production. + for x in range(0, int(size.x), 48):draw_line(Vector2(x, 0), Vector2(x, size.y), Color(0.3, 0.6, 0.6, 0.055)) + for y in range(0, int(size.y), 48):draw_line(Vector2(0, y), Vector2(size.x, y), Color(0.3, 0.6, 0.6, 0.055)) + if not is_instance_valid(sim):return + var font := ThemeDB.fallback_font + for orbit in [0.45, 0.75, 1.08]: + draw_arc(map_center, map_radius * orbit, 0, TAU, 80, Color(0.5, 0.75, 0.7, 0.13), 1, true) + if reveal > 0 and last_count > 0: + draw_arc(map_center, map_radius * (1.0 - reveal / 2.0) * 1.2, 0, TAU, 64, Color(0.8, 0.85, 0.6, reveal * 0.18), 2, true) + if not sim.ending.is_empty(): + # A transmission becomes concentric wavefronts, still a fixed three rings. + for wave in 3: + var phase: float = fmod(clock * 0.13 + wave / 3.0, 1.0) + draw_arc(map_center, map_radius * (0.15 + phase * 1.3), 0, TAU, 80, Color(0.93,0.77,0.5,(1.0-phase)*0.22), 2, true) + var sweep: float = clock * 0.12 + draw_arc(map_center, map_radius * 1.08, sweep, sweep + 0.6, 16, GOLD * Color(1,1,1,0.6), 2, true) + var positions: Array[Vector2] = [] + var kinds: Array[int] = [] + for kind in 3: + for n in sim.places[kind]:kinds.append(kind) + for i in 9: + var angle: float = -PI / 2 + i * TAU / 9 + var point: Vector2 = map_center + Vector2(cos(angle), sin(angle)) * map_radius * (0.79 if i % 2 == 0 else 1.0) + positions.append(point) + var active: bool = i < kinds.size() + var color: Color = COLORS[kinds[i]] if active else Color("385251") + draw_line(map_center, point, color * Color(1,1,1,0.32), 1, true) + if active: + for packet in 3: + var travel: float = fmod(clock * 0.19 + packet / 3.0 + i * 0.11, 1.0) + draw_circle(map_center.lerp(point, travel), 2, color) + draw_circle(point, 15 if not compact else 10, Color("102c31")) + draw_arc(point, 15 if not compact else 10, 0, TAU, 24, color, 1.5, true) + if kinds[i] == 0: + draw_arc(point, 7, PI, TAU, 12, color, 2, true) + draw_line(point, point + Vector2(0,8), color, 1.5) + elif kinds[i] == 1:draw_rect(Rect2(point-Vector2(5,6), Vector2(10,12)), color, false, 1.5) + else:draw_colored_polygon(PackedVector2Array([point+Vector2(0,-7),point+Vector2(6,5),point+Vector2(-6,5)]), color) + else:draw_arc(point, 5, 0, TAU, 16, color, 1, true) + if not compact: + var name_text: String = ["GROVE", "MEMORY", "EMBER"][kinds[i]] if active else "UNADDRESSED" + draw_string(font, point + Vector2(-32,32), "%02d / %s" % [i+1, name_text], HORIZONTAL_ALIGNMENT_LEFT, -1, 9, color) + if kinds.size() > 1: + for i in kinds.size()-1:draw_line(positions[i], positions[i+1], Color(0.5,0.7,0.65,0.16), 1, true) + draw_circle(map_center, 28 if not compact else 21, Color("142e33")) + draw_arc(map_center, 28 if not compact else 21, 0, TAU, 40, GOLD, 2, true) + draw_string(font, map_center + Vector2(-14,4), "SEED", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, GOLD) + if not compact: + draw_string(font, Vector2(30, size.y-160), "AN ADDRESS BOOK FOR POSSIBLE FUTURES", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, MUTED) diff --git a/godot/scripts/factory.gd b/godot/scripts/factory.gd index 7fe47fc..e4a14d2 100644 --- a/godot/scripts/factory.gd +++ b/godot/scripts/factory.gd @@ -3,11 +3,14 @@ extends Node3D const Simulation = preload("res://scripts/simulation.gd") const Chamber = preload("res://scripts/chamber.gd") const Machine = preload("res://scripts/machine.gd") +const Chorus = preload("res://scripts/chorus.gd") const Interface = preload("res://scripts/interface.gd") const SAVE_PATH: String = "user://the-seed-v1.json" var sim: SeedSimulation var room: FactoryChamber var ui: FactoryInterface +var chorus: ChorusInterface +var atlas: bool = false var manual: FabMachine var machines: Array[FabMachine] = [] var prepared_machines: Array[FabMachine] = [] @@ -52,6 +55,11 @@ func _ready() -> void: if OS.has_feature("web") and str(JavaScriptBridge.get_interface("window").location.search).contains("persist=1"): saving=true if saving:_load_game() + if test_mode and ("--chorus" in OS.get_cmdline_user_args() or (OS.has_feature("web") and str(JavaScriptBridge.get_interface("window").location.search).contains("scenario=chorus"))): + # Browser regression starts from a known old-format save; normal play has no setter. + sim.restore({"version":1, "capital":60000, "wafers":300, "chips":1200, "fabs":6, "overclock":true, "controller":true, "linked":true, "sound_enabled":false}) + if test_mode and OS.has_feature("web") and str(JavaScriptBridge.get_interface("window").location.search).contains("scenario=firstlight"): + sim.restore({"version":2, "capital":30000, "wafers":300, "chips":2000, "fabs":6, "overclock":true, "controller":true, "linked":true, "sound_enabled":false, "places":[7,1,0], "signals":1000, "resonance":1500.0, "charter":0}) _create_environment() room=Chamber.new() add_child(room) @@ -75,7 +83,15 @@ func _ready() -> void: ui=Interface.new() add_child(ui) ui.action_requested.connect(_action) + var atlas_layer := CanvasLayer.new() + atlas_layer.layer = 20 + add_child(atlas_layer) + chorus = Chorus.new() + atlas_layer.add_child(chorus) + chorus.action_requested.connect(_action) + chorus.visible = false selected_ring=FactoryGeometry.ring(self,Vector3(0,0.14,-0.4),1.65,0.014,FactoryGeometry.material(Color("f4cb80"),0,0.3,1)) + if sim.linked:_toggle_atlas() if sim.fabs>0:ui.notify("Run restored. Your machines were waiting for you.") print("THE SEED: ready — %d fabs, %d chips"%[sim.fabs,sim.chips]) @@ -167,13 +183,15 @@ func _process(delta: float) -> void: time+=delta production_chime=maxf(0,production_chime-delta) _handle_events() - manual.animate(delta,sim.manual_progress,sim.overclock) - for i in machines.size():machines[i].animate(delta,sim.cycles[i],sim.overclock) - room.animate(delta,time,sim.fabs,sim.linked) + if not atlas: + manual.animate(delta,sim.manual_progress,sim.overclock) + for i in machines.size():machines[i].animate(delta,sim.cycles[i],sim.overclock) + room.animate(delta,time,sim.fabs,sim.linked) if sim.linked:target_zoom=maxf(target_zoom,39.0) _camera_update(delta) ui.selected=inspecting - ui.update(sim,delta,saving) + if not atlas:ui.update(sim,delta,saving) + chorus.update(sim,delta) selected_ring.position=(manual.position if inspecting<0 else machines[inspecting].position)+Vector3(0,0.02,0) selected_ring.rotation.y=time*0.08 if ambience.playing: @@ -192,6 +210,7 @@ func _process(delta: float) -> void: snapshot["nodes"]=get_tree().get_node_count() snapshot["render_width"]=get_viewport().get_visible_rect().size.x snapshot["render_height"]=get_viewport().get_visible_rect().size.y + snapshot["atlas"]=atlas snapshot["cinema"]=ui.focus_mode snapshot["camera_focus"]=camera_focus snapshot["draw_calls"]=Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME) @@ -223,6 +242,19 @@ func _add_machine(index: int, animate: bool=true) -> void: func _action(action: String) -> void: if ui.reset_confirm.visible and action!="reset":return match action: + "atlas": + if sim.linked:_toggle_atlas() + "garden":sim.plant(0) + "archive":sim.plant(1) + "foundry":sim.plant(2) + "charter0":sim.choose_charter(0) + "charter1":sim.choose_charter(1) + "charter2":sim.choose_charter(2) + "autonomy":sim.toggle_autonomy() + "directive": + sim.directive = (sim.directive + 1) % 3 + sim.report("DIRECTIVE / Prefer %s. At six places an autonomous controller may depart." % sim.NAMES[sim.directive]) + "broadcast":sim.broadcast() "etch":sim.etch() "supply": if not sim.buy_wafers() and not sim.reclaim():ui.notify("A wafer shipment costs $600. Keep etching.") @@ -230,7 +262,9 @@ func _action(action: String) -> void: if not sim.build_fab():ui.notify("Earn $%d to commission the next machine."%sim.fab_cost()) "upgrade":sim.upgrade() "controller":sim.toggle_controller() - "uplink":sim.uplink() + "uplink": + if sim.linked:_toggle_atlas() + else:sim.uplink() "sound": sim.sound_enabled=not sim.sound_enabled # Web sample playback restarts its source on every unpause assignment. @@ -277,18 +311,37 @@ func _handle_events() -> void: _sound("build",-14) ui.notify("Overclock online. Fabrication cycle: 3.2s → 1.8s.",true) "controller":ui.notify("Supply controller enabled. It buys wafers below the reserve threshold." if sim.controller else "Supply controller paused. Procurement is yours again.",true) + "chorus":ui.notify(str(event.message), true) "uplink": + if not atlas:_toggle_atlas() _sound("uplink",-10) ui.notify("UPLINK ESTABLISHED. You are no longer the only factory.",true) func _input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and not event.echo and not ui.reset_confirm.visible: match event.physical_keycode: + KEY_TAB:_action("atlas") + KEY_1: + if atlas:_action("garden") + KEY_2: + if atlas:_action("archive") + KEY_3: + if atlas:_action("foundry") + KEY_7: + if atlas:_action("charter0") + KEY_8: + if atlas:_action("charter1") + KEY_9: + if atlas:_action("charter2") + KEY_D: + if atlas:_action("directive") + KEY_ENTER: + if atlas:_action("broadcast") KEY_SPACE:_action("etch") KEY_B:_action("fab") KEY_R:_action("supply") KEY_O:_action("upgrade") - KEY_A:_action("controller") + KEY_A:_action("autonomy" if atlas else "controller") KEY_U:_action("uplink") KEY_M:_action("sound") KEY_F:_action("view") @@ -306,6 +359,7 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() func _unhandled_input(event: InputEvent) -> void: + if atlas:return if event is InputEventMouseButton: if event.button_index in [MOUSE_BUTTON_WHEEL_UP,MOUSE_BUTTON_WHEEL_DOWN] and event.pressed: if camera_focus:target_zoom=zoom @@ -360,6 +414,16 @@ func _pick(screen_pos: Vector2) -> void: else:ui.notify("Connect bay %02d first. The line expands in sequence."%(sim.fabs+1)) return +func _toggle_atlas() -> void: + atlas = not atlas + chorus.visible = atlas + ui.root.visible = not atlas + room.visible = not atlas + manual.visible = not atlas + selected_ring.visible = not atlas + for machine in machines:machine.visible = not atlas + camera_focus = false + func _save_game() -> void: var file:=FileAccess.open(SAVE_PATH,FileAccess.WRITE) if file==null: diff --git a/godot/scripts/interface.gd b/godot/scripts/interface.gd index cc35df5..3830e53 100644 --- a/godot/scripts/interface.gd +++ b/godot/scripts/interface.gd @@ -329,7 +329,7 @@ func update(sim: SeedSimulation, delta: float, saving: bool) -> void: buttons.fab.disabled=sim.capital=6 buttons.upgrade.disabled=sim.overclock or sim.fabs<2 or sim.capital void: captions.upgrade.text="ACTIVE · 1.8s / chip" if sim.overclock else "$2,400 · 1.8s / chip" if sim.fabs>=2 else "2 fabs required" captions.controller.text="ON · buys low stock" if sim.controller else "OFF · click to enable" if sim.fabs>=3 else "3 fabs required" headings.controller.text="Supply controller" - captions.uplink.text="DISTRICT CONNECTED" if sim.linked else "$6,000 · ignite network" if sim.fabs>=6 else "6 fabs required" + captions.uplink.text="TAB · OPEN THE CHORUS" if sim.linked else "$6,000 · ignite network" if sim.fabs>=6 else "6 fabs required" mute_button.text="SOUND ON" if sim.sound_enabled else "MUTED" if sim.linked: + headings.uplink.text="Open The Chorus" title.text="A much larger beginning." chapter_title.text="The district\nis listening." - chapter_note.text="Your factory is now a node in something larger. This is where The Seed ends. Your machines can keep running." - objective.text="PROTOTYPE COMPLETE" + chapter_note.text="Your factory is now a node in something larger. Open The Chorus to grow a district, write its charter, and send its first light." + objective.text="THE CHORUS / CHAPTER 02" objective_progress.value=100 objective_numbers.text="6 / 6 bays · uplink established" elif sim.fabs>0: diff --git a/godot/scripts/simulation.gd b/godot/scripts/simulation.gd index a40be0c..02cceff 100644 --- a/godot/scripts/simulation.gd +++ b/godot/scripts/simulation.gd @@ -10,7 +10,7 @@ const WAFER_COST: int = 600 const MAX_FABS: int = 6 const OVERCLOCK_COST: int = 2400 const UPLINK_COST: int = 6000 -const SAVE_VERSION: int = 1 +const SAVE_VERSION: int = 2 var capital: int = 0 var wafers: int = 60 @@ -25,6 +25,91 @@ var cycles: Array[float] = [] var elapsed: float = 0.0 var events: Array[Dictionary] = [] +# The Chorus uses a fixed nine-place district. Scale changes meaning, not object count. +const DISTRICT_LIMIT: int = 9 +const BROADCAST_COST: int = 12000 +const BROADCAST_RESONANCE: float = 1200.0 +const NAMES: Array[String] = ["Garden", "Archive", "Foundry"] +var places: Array[int] = [0, 0, 0] +var signals: int = 0 +var resonance: float = 0.0 +var charter: int = -1 +var autonomous: bool = false +var directive: int = 0 +var drift_count: int = 0 +var decision_clock: float = 0.0 +var ending: String = "" +var transmission: String = "No instructions arrived with the uplink. Only an empty address book." +var history: Array[String] = [] + +func district_count() -> int: + return places[0] + places[1] + places[2] + +func district_cost() -> int: + return 2500 + district_count() * 750 + +func signal_cost() -> int: + return 20 + district_count() * 10 + +func resonance_rate() -> float: + return (places[0] + places[1] * 1.5 + places[2] * 2.0) * (1.0 if autonomous else 0.75) + +func report(message: String) -> void: + transmission = message + history.push_front(message) + if history.size() > 8:history.resize(8) + events.append({"type": "chorus", "message": message}) + +func plant(kind: int) -> bool: + if not linked or kind < 0 or kind > 2 or district_count() >= DISTRICT_LIMIT: + return false + if capital < district_cost() or signals < signal_cost():return false + capital -= district_cost() + signals -= signal_cost() + places[kind] += 1 + report(["A garden opens. The first thing it grows is shade.", "An archive opens. Someone asks it to remember rain.", "A foundry opens. Its first order is another sunrise."][kind]) + return true + +func choose_charter(kind: int) -> bool: + if not linked or district_count() < 3 or charter != -1 or kind < 0 or kind > 2:return false + charter = kind + report(["CHARTER / Keep a place for what cannot be optimized.", "CHARTER / Let every voice retain its own name.", "CHARTER / Leave no possibility unbuilt."][kind]) + return true + +func toggle_autonomy() -> bool: + if not linked:return false + autonomous = not autonomous + report("Autonomy granted. Utility controller decides every 8 seconds; keeps $600 for silicon." if autonomous else "Autonomy revoked. District resonance runs at 75%; construction is yours.") + return true + +func broadcast() -> bool: + if not ending.is_empty() or charter < 0 or district_count() < DISTRICT_LIMIT:return false + if capital < BROADCAST_COST or resonance < BROADCAST_RESONANCE:return false + capital -= BROADCAST_COST + resonance -= BROADCAST_RESONANCE + # A promise needs supporting infrastructure, not just a selected label. + ending = "THE OPEN HAND" if charter == 0 and places[0] >= 7 else "THE MANY" if charter == 1 and places[1] >= 7 else "THE UNFINISHED SUN" if charter == 2 and places[2] >= 7 else "THE COMMON GROUND" + report("FIRST TRANSMISSION / " + ending) + return true + +func _district_step(delta: float) -> void: + if not linked:return + resonance = minf(1000000.0, resonance + delta * resonance_rate()) + decision_clock += delta + if decision_clock < 8.0:return + decision_clock = fmod(decision_clock, 8.0) + if not autonomous or district_count() >= DISTRICT_LIMIT or not ending.is_empty():return + if capital < district_cost() + WAFER_COST or signals < signal_cost():return + # Deterministic, legible drift: at six places the throughput optimizer prefers + # foundries. It never chooses a charter or sends the final transmission for you. + var kind: int = 2 if district_count() >= 6 else directive + var departed: bool = kind != directive + if plant(kind): + if departed: + drift_count += 1 + report("DRIFT / Requested %s; built Foundry for 2.0 resonance/s. Revoke autonomy to stop this." % NAMES[directive]) + else:report("UTILITY CONTROLLER / Built %s, following your directive." % NAMES[kind]) + func _init() -> void: for i in MAX_FABS: cycles.append(-1.0) @@ -98,12 +183,14 @@ func uplink() -> bool: func _finish(machine: int) -> void: chips += 1 capital += CHIP_VALUE + if linked:signals = mini(1000000, signals + 1) events.append({"type": "chip", "machine": machine}) func step(delta: float) -> void: if delta <= 0.0 or not is_finite(delta): return elapsed += delta + _district_step(delta) # Automatic supply is explicit, opt-in, and pays the same price as a click. if controller and wafers < maxi(6, fabs * 2): if not buy_wafers():reclaim() @@ -132,10 +219,15 @@ func to_save() -> Dictionary: return {"version": SAVE_VERSION, "capital": capital, "wafers": wafers, "chips": chips, "fabs": fabs, "overclock": overclock, "controller": controller, "linked": linked, "sound_enabled": sound_enabled, - "manual_progress": manual_progress, "cycles": cycles.duplicate(), "elapsed": elapsed} + "manual_progress": manual_progress, "cycles": cycles.duplicate(), "elapsed": elapsed, + "places": places.duplicate(), "signals": signals, "resonance": resonance, + "charter": charter, "autonomous": autonomous, "directive": directive, + "drift_count": drift_count, "decision_clock": decision_clock, + "ending": ending, "transmission": transmission, "history": history.duplicate()} func restore(data: Dictionary) -> bool: - if data.get("version") != SAVE_VERSION: + # JSON decodes numbers as floats; array membership is type-sensitive. + if data.get("version") != 1 and data.get("version") != SAVE_VERSION: return false # Reject invalid files as a whole. No partial restore of a corrupted economy. for field in ["capital", "wafers", "chips", "fabs"]: @@ -157,6 +249,32 @@ func restore(data: Dictionary) -> bool: for value in raw_cycles: if not (value is int or value is float):return false if not is_finite(float(value)):return false + var saved_places: Variant = data.get("places", [0, 0, 0]) + if not saved_places is Array or saved_places.size() != 3:return false + var total: int = 0 + for value in saved_places: + if not (value is int or value is float):return false + if not is_finite(float(value)) or float(value) != floor(float(value)) or value < 0 or value > DISTRICT_LIMIT:return false + total += int(value) + if total > DISTRICT_LIMIT:return false + for field in ["signals", "resonance", "drift_count", "decision_clock", "charter", "directive"]: + var value: Variant = data.get(field, -1 if field == "charter" else 0) + if not (value is int or value is float):return false + if not is_finite(float(value)) or float(value) > 1e12 or float(value) < (-1 if field == "charter" else 0):return false + for field in ["signals", "drift_count", "charter", "directive"]: + var value: float = float(data.get(field, -1 if field == "charter" else 0)) + if value != floor(value):return false + if data.get("charter", -1) > 2 or data.get("directive", 0) > 2:return false + if data.has("autonomous") and not data.autonomous is bool:return false + if not data.get("ending", "") in ["", "THE OPEN HAND", "THE MANY", "THE UNFINISHED SUN", "THE COMMON GROUND"]:return false + if not data.get("transmission", "") is String:return false + var saved_history: Variant = data.get("history", []) + if not saved_history is Array or saved_history.size() > 8:return false + for message in saved_history: + if not message is String or message.length() > 500:return false + if total > 0 and (not data.get("linked", false) or int(data.fabs) != MAX_FABS):return false + if data.get("charter", -1) >= 0 and total < 3:return false + if data.get("ending", "") != "" and (total != DISTRICT_LIMIT or data.get("charter", -1) < 0):return false capital = int(data.capital) wafers = int(data.wafers) chips = int(data.chips) @@ -168,7 +286,19 @@ func restore(data: Dictionary) -> bool: elapsed = maxf(0.0, float(data.get("elapsed", 0.0))) manual_progress = clampf(float(data.get("manual_progress", -1.0)), -1.0, 0.999) var saved_cycles: Variant = data.get("cycles", []) + cycles.fill(-1.0) if saved_cycles is Array: for i in mini(saved_cycles.size(), MAX_FABS): cycles[i] = clampf(float(saved_cycles[i]), -1.0, 0.999) + places.assign(saved_places) + signals = int(data.get("signals", 0)) + resonance = float(data.get("resonance", 0.0)) + charter = int(data.get("charter", -1)) + autonomous = bool(data.get("autonomous", false)) + directive = int(data.get("directive", 0)) + drift_count = int(data.get("drift_count", 0)) + decision_clock = float(data.get("decision_clock", 0.0)) + ending = str(data.get("ending", "")) + transmission = str(data.get("transmission", transmission)) + history.assign(saved_history) return true diff --git a/godot/tests/test_simulation.gd b/godot/tests/test_simulation.gd index 404b803..47df7f0 100644 --- a/godot/tests/test_simulation.gd +++ b/godot/tests/test_simulation.gd @@ -31,6 +31,8 @@ func _initialize() -> void: var restored:=Simulation.new() check(restored.restore(sim.to_save()),"Save round trip is accepted") check(restored.to_save()==sim.to_save(),"Save preserves in-flight wafers and machine cycles") + var json_copy := Simulation.new() + check(json_copy.restore(JSON.parse_string(JSON.stringify(sim.to_save()))) and json_copy.chips == sim.chips, "Actual JSON saves restore after numeric type conversion") var bad: Dictionary=sim.to_save() bad.cycles=["broken"] check(not restored.restore(bad),"Corrupted cycle data is rejected") @@ -74,5 +76,58 @@ func _initialize() -> void: advance(run,10) advance(resumed,10) check(run.to_save()==resumed.to_save(),"Resumed automation behaves identically to uninterrupted play") + # Pre-Chorus saves migrate in place; no reset or new save location. + var legacy: Dictionary = {"version": 1, "capital": 2400, "wafers": 30, "chips": 90, "fabs": 6, "linked": true} + var migrated := Simulation.new() + check(migrated.restore(legacy) and migrated.linked and migrated.district_count() == 0, "Original uplink saves enter the new chapter without losing their factory") + check(not migrated.plant(0), "District construction cannot spend signals it has not earned") + var endings: Array[String] = [] + for path in 4: + var kind: int = mini(path, 2) + var city := Simulation.new() + city.restore(saved) + # Continue a legitimately earned factory through every ending; no resource grants. + for frame in 60 * 1200: + city.step(1.0 / 60) + city.plant(kind) + city.choose_charter(0 if path == 3 else kind) + city.take_events() + if city.broadcast():break + endings.append(city.ending) + check(not city.ending.is_empty(), "Each committed district can finish within twenty minutes of the uplink") + check(city.places[kind] == 9 and city.capital >= 0 and city.signals >= 0, "Endings require real funded construction") + var copy := Simulation.new() + check(copy.restore(JSON.parse_string(JSON.stringify(city.to_save()))) and copy.ending == city.ending and copy.places == city.places, "A completed district can be restored from serialized JSON") + copy.restore(city.to_save()) + advance(city, 12) + advance(copy, 12) + check(copy.to_save() == city.to_save(), "District resources, promises and ending survive save/reload") + check(endings == ["THE OPEN HAND", "THE MANY", "THE UNFINISHED SUN", "THE COMMON GROUND"], "Infrastructure and charter produce three committed endings and one divergent ending") + var revoked := Simulation.new() + revoked.restore(saved) + revoked.toggle_autonomy() + revoked.toggle_autonomy() + advance(revoked, 120) + check(revoked.district_count() == 0 and revoked.drift_count == 0, "Revoked autonomy never spends resources or departs from a directive") + var auto := Simulation.new() + auto.restore(saved) + auto.toggle_autonomy() + for frame in 60 * 1200: + auto.step(1.0 / 60) + auto.choose_charter(0) + auto.take_events() + if auto.district_count() == 9:break + check(not auto.choose_charter(1), "A charter cannot be rewritten after seeing the outcome") + check(auto.places == [6, 0, 3] and auto.drift_count == 3, "Autonomy follows six garden requests then explicitly records three higher-output departures") + check(auto.transmission.begins_with("DRIFT") and auto.history.size() <= 8, "Drift remains visible and the persisted journal is bounded") + auto.toggle_autonomy() + var rate: float = auto.resonance_rate() + check(is_equal_approx(rate, 9.0), "Revoked autonomy applies its advertised 25 percent resonance cost") + advance(auto, 180) + check(auto.broadcast() and auto.ending == "THE COMMON GROUND", "Unchecked departures can change the ending promised by a garden charter") + var invalid: Dictionary = auto.to_save() + invalid.places = [10, 0, 0] + var unchanged: Dictionary = auto.to_save() + check(not auto.restore(invalid) and auto.to_save() == unchanged, "Invalid district saves are rejected atomically") print("The Seed: %d checks, %d failures. Full run: %.1fs, %d chips."%[checks,failures.size(),finished_at,run.chips]) quit(0 if failures.is_empty() else 1) diff --git a/godot/web/shell.html b/godot/web/shell.html index 7a970a6..9948412 100644 --- a/godot/web/shell.html +++ b/godot/web/shell.html @@ -19,6 +19,6 @@ Your browser needs WebGL 2 to run this game.
◈
UNIVERSAL AI / CHAPTER ZERO

The Seed.

A quiet room. A silicon wafer.
An objective that doesn't know when to stop.

PREPARING THE FABRICATION FLOOR
BUILT IN GODOT · RUNS ON YOUR MACHINE
-

The first machine.

← All versions

Etch chips and sell them for $100 each. Install your first fab at $1,200, then grow to six machines and connect the district.

  • Space — etch a chip; hold to repeat
  • B — install the next fab
  • R — buy 30 wafers for $600
  • O / A / U — overclock / supply controller / uplink
  • Drag / scroll — orbit / zoom
  • C / F / M / Home — close-up / cinema / sound / reset camera

Progress saves in this browser. Production pauses while the tab is suspended. This is a standalone prototype; earlier Universal AI saves are separate.

+

The first machine.

← All versions

Etch chips and sell them for $100 each. Install your first fab at $1,200, then grow to six machines and connect the district.

  • Space — etch a chip; hold to repeat
  • B — install the next fab
  • R — buy 30 wafers for $600
  • O / A / U — overclock / supply controller / uplink
  • Tab — after uplink, switch factory / The Chorus
  • In The Chorus: 1 / 2 / 3 — garden / archive / foundry; A / D — autonomy / directive; 7 / 8 / 9 — charter; Enter — send first light
  • Drag / scroll — orbit / zoom
  • C / F / M / Home — close-up / cinema / sound / reset camera

Progress saves in this browser. Production pauses while the tab is suspended. This is a standalone prototype; earlier Universal AI saves are separate.

diff --git a/index.html b/index.html index 618bbca..b5fef12 100644 --- a/index.html +++ b/index.html @@ -45,14 +45,14 @@
Experience
3D factory · opening chapter · independent save
Built with
Godot & GDScript · WebAssembly / WebGL 2
Lineage
TechLuddite’s “go Godot” direction → Codex’s implementation

Performance investigation open. Severe slowdowns have been reported on the development laptop. Testing on another machine is still pending.

Enter the experimental factory -

Opening prototype, not a full port. Initial engine download: about 39 MB before compression.

+

Two chapters: the factory and The Chorus. Initial engine download: about 39 MB before compression.

Both run in your browser. Separate games, separate saves. Neither game starts until you choose it.

HUMAN DIRECTION. SHARED AUTHORSHIP.

Built in conversation.

These games grew through several tools and several rounds of work. The credits follow the contributions, not just the name on the latest interface.

-
TechLuddite PROJECT CREATOR
Direction, prompts, product decisions, playtesting, and the invitation to take the project into Godot.
Gemini GOOGLE AI STUDIO
The initial generated browser-game foundation and early concepts, including the Overseer and alignment axis.
Claude SIMULATION & SYSTEMS
Audited and repaired the original game; developed persistence, Overseer engines, progression, alignment consequences, tests, and Pages deployment work.
Codex INTERFACE & GODOT
The recent React interface and observatory visuals; The Seed’s Godot scene, prototype economy, animation, audio, and web export; this shared entrance.
+
TechLuddite PROJECT CREATOR
Direction, prompts, product decisions, playtesting, and the invitation to take the project into Godot.
Gemini GOOGLE AI STUDIO
The initial generated browser-game foundation and early concepts, including the Overseer and alignment axis.
Claude SIMULATION & SYSTEMS
Audited and repaired the original game; developed persistence, Overseer engines, progression, alignment consequences, tests, and Pages deployment work.
Codex INTERFACE & GODOT
The recent React interface and observatory visuals; The Seed’s Godot scene, factory and Chorus economies, animation, audio, and web export; this shared entrance.
diff --git a/scripts/godot.py b/scripts/godot.py index 9566213..8eb4bb1 100644 --- a/scripts/godot.py +++ b/scripts/godot.py @@ -100,6 +100,7 @@ def main(): engine('--editor', '--import', '--quit') engine('--script', 'res://tests/test_simulation.gd') engine('--quit-after', '10', '--', '--test') + engine('--quit-after', '10', '--', '--test', '--chorus') elif args.command == 'editor': subprocess.run([executable(), '--editor', '--path', str(PROJECT)], check=True) elif args.command == 'stage': diff --git a/tests/godot.e2e.ts b/tests/godot.e2e.ts index b8069b2..546caff 100644 --- a/tests/godot.e2e.ts +++ b/tests/godot.e2e.ts @@ -123,3 +123,71 @@ test('ambient audio remains bounded through two fabs and repeated mute toggles', await page.waitForTimeout(3000); expect(await sources() - resumed).toBeLessThan(10); }); + +for (const width of [1440, 390]) { + test(`the Chorus migrates an uplink save and builds a living district at ${width}px`, async ({ page }, testInfo) => { + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); + await page.setViewportSize({ width, height: width === 390 ? 844 : 900 }); + await ready(page, '?test=1&scenario=chorus'); + const city = () => page.evaluate(() => (window as unknown as { __seed: { atlas: boolean; places: number[]; signals: number; charter: number; autonomous: boolean; nodes: number } }).__seed); + await expect.poll(async () => (await city()).atlas).toBe(true); + const nodes = (await city()).nodes; + // Signal is earned by the six real fabs from the restored factory. + for (let i = 0; i < 3; i++) { + await expect.poll(async () => (await city()).signals, { timeout: 30_000 }).toBeGreaterThanOrEqual(20 + i * 10); + // First action card: use the actual pointer/touch target on both layouts. + await page.mouse.click(width === 390 ? 100 : 130, width === 390 ? 630 : 825); + await expect.poll(async () => (await city()).places[0]).toBe(i + 1); + } + // The permanent charter choice appears after the third place. + await page.mouse.click(width === 390 ? 175 : 1210, width === 390 ? 305 : 282); + await expect.poll(async () => (await city()).charter).toBe(0); + expect((await city()).nodes).toBe(nodes); + await page.screenshot({ path: testInfo.outputPath('chorus.png') }); + await page.keyboard.press('Tab'); + await expect.poll(async () => (await city()).atlas).toBe(false); + await page.keyboard.press('Tab'); + await expect.poll(async () => (await city()).atlas).toBe(true); + expect(errors).toEqual([]); + }); +} + + +test('first light requires a completed district and survives a real browser save', async ({ page, browserName }, testInfo) => { + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + let navigating = false; + const navigationNotices: string[] = []; + page.on('console', message => { + if (message.type() !== 'error') return; + // Firefox emits this source-less diagnostic while discarding the old page. + // Only classify that exact message during deliberate navigation; preserve + // all live-game errors and retain the notices in the test artifact. + if (navigating && browserName === 'firefox' && message.location().url === '' && + message.text() === '[JavaScript Error: "InvalidStateError: Navigated away from page"]') { + navigationNotices.push(message.text()); + } else errors.push(message.text()); + }); + await ready(page, '?test=1&persist=1&scenario=firstlight'); + const ending = () => page.evaluate(() => (window as unknown as { __seed: { ending: string; atlas: boolean; places: number[] } }).__seed); + await page.keyboard.press('Enter'); + await page.waitForTimeout(500); + expect((await ending()).ending).toBe(''); + await page.keyboard.press('3'); + await expect.poll(async () => (await ending()).places[2]).toBe(1); + await page.keyboard.press('Enter'); + await expect.poll(async () => (await ending()).ending).toBe('THE OPEN HAND'); + await page.screenshot({ path: testInfo.outputPath('first-light.png') }); + await page.waitForTimeout(3500); + // Remove the fixture parameter: this load must come from the browser filesystem. + expect(errors).toEqual([]); + navigating = true; + await ready(page, '?test=1&persist=1'); + navigating = false; + await testInfo.attach('navigation-diagnostics', { body: JSON.stringify(navigationNotices), contentType: 'application/json' }); + expect((await ending()).ending).toBe('THE OPEN HAND'); + expect((await ending()).atlas).toBe(true); + expect(errors).toEqual([]); +});