[Fix][Codex] Use Art V3 throughout the M2 runtime loop - #10
Merged
Conversation
What one run amounted to: outcome, duration, kills, sacrifices, integrity, imbalance, soul ash earned. A plain value with no behaviour and no node references, so it survives the run scene being torn down and a test can build one by hand. soul_ash_earned is the one field the run does not fill: the run reports what happened, the meta layer prices it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
Five persisted values and nothing else: soul ash, runs, deaths, victories, and whether the Tempered Blade is owned. Mirrors RunState's shape on purpose — private fields, reads through getters, writes through named commands — so the Ghost Market observes and asks rather than assigning, exactly as the HUD does with RunState. MetaConfig holds the meta tunables in data/meta_config.tres, deliberately apart from BalanceConfig: in-run balance belongs to combat design and must not shift because someone tuned the shop. Its soul_ash_for() is pure, so the reward rule is testable without a save file or a scene. Serialisation lives on MetaState because the field list and its JSON shape are one piece of knowledge. Unknown or missing keys fall back to a fresh profile's value rather than throwing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
user://meta_save.json with load, save, reset and a version field. The path is injected rather than a constant, which is the whole reason this is an object: tests point it at a scratch file instead of the player's real save. A missing, unreadable or malformed save yields a fresh profile with a warning. Losing progress is bad; refusing to launch is worse. A save written by a newer version is also refused rather than half-read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
Closes the loop: Ghost Market -> run -> run end -> Ghost Market.
app.tscn becomes the main scene and is the composition root outside a run. It
owns the meta profile and the save file, and is the only place the two halves
meet -- Main knows nothing about the market, GhostMarket knows nothing about
combat.
Run-end pipeline, identical for death and victory because both already converge
on RunCoordinator._finish_run:
run_finished(result) -> MetaState.record_run -> MetaSave.save_state
-> result screen dismissed -> Ghost Market
Banking happens on run end rather than on dismissal, so quitting at the result
screen still keeps what was earned. Duplicate end requests are ignored twice
over: the coordinator's phase guard, and App._run_recorded.
Changes to existing code, kept as small as the goal allows:
- RunCoordinator now tracks kills and elapsed time and builds the RunResult.
run_finished carries that result instead of a bare outcome (ADR-012). Combat,
enemy AI, the sacrifice system and balance are untouched.
- RunState gains add_flat_attack, the single channel through which meta
progression reaches a run. Offensive, not structural, so a permanent upgrade
cannot make the body read as more or less whole.
- ResultScreen now only reports and emits "dismissed". Who listens decides what
happens next, which is how the market loop and standalone main.tscn both work
without a mode flag.
Exactly one child scene exists at a time. Hiding the idle one is not an option:
both present their UI on CanvasLayers, and a CanvasLayer does not inherit
visibility from a parent Node2D -- a "hidden" market kept drawing over the run.
Market scenery reuses existing floor, brick, brazier and ghost-fire art and is
decoration only. Nothing reads it, so replacing the placeholder with real market
art touches no script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
Twelve cases covering exactly the M2 brief, driving the real app.tscn against a scratch save file. Market loads first, start run works, death returns to the market, victory returns to the market, a run is rewarded exactly once, soul ash persists to disk and reloads, the upgrade persists and reaches the next run's starting attack, and a second run resets HP, stamina, integrity, imbalance and sacrifice history while keeping soul ash and the upgrade. Plus three the brief did not ask for but the code needed: the reward rule priced straight from config, a malformed save yielding a fresh profile instead of a crash, and restart clearing leftover actors -- the market path rebuilds the whole run scene, so _clear_actors is only really exercised by the in-scene restart. Two harness additions: TestCase.wait_for for bounded polling, since the result screen is gated on a death animation, and an awaited frame in after_each so a torn-down app is really gone before the next case boots one. Whole suite: 78 tests, 484 assertions, 0 failures on Godot 4.3 headless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
- docs/GHOST_MARKET_LOOP.md (new): the loop, the run-end pipeline, the data shapes, the save format, how the one upgrade reaches a run, what a new run resets, and what M2 deliberately does not do. - ARCHITECTURE.md: app.tscn as the composition root outside a run, the new state-ownership rows, and the meta gap in the known-gaps list. - AI_HANDOFF.md: branch, M2 summary, updated test figures, and two new limitations -- meta is one upgrade, and the engine version has drifted (project.godot and CI say 4.3, the committed assets_v2 .import files carry Godot 4.4+ keys, and the M1.5 handoff reports local work on 4.7.1). - DECISIONS.md: ADR-012 (run_finished carries a RunResult) and ADR-013 (one live child scene rather than hiding the idle one). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jo4LHdYmCJBrqzgB5j8KGa
BotTony329
marked this pull request as ready for review
August 2, 2026 11:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Root cause
The M2 and Art V3 lines diverged from the same older commit. M2 correctly loaded scenes/main.tscn, but its committed actor resources still resolved to Art V2. This was branch-history divergence, not a Godot cache override.
Validation
Notes
Draft only. Do not merge until integration review.