Skip to content

[Fix][Codex] Use Art V3 throughout the M2 runtime loop - #10

Merged
BotTony329 merged 7 commits into
developfrom
codex/m2-runtime-art-source-fix
Aug 2, 2026
Merged

[Fix][Codex] Use Art V3 throughout the M2 runtime loop#10
BotTony329 merged 7 commits into
developfrom
codex/m2-runtime-art-source-fix

Conversation

@BotTony329

Copy link
Copy Markdown
Owner

Summary

  • merge the Claude M2 Ghost Market loop onto the latest develop Art V3 foundation
  • make assets_v3/godot the single generated SpriteFrames source for all live actors
  • remove obsolete duplicate actor mappings and visible Art V2 HUD/effect paths
  • add App-driven runtime regression coverage and an exact asset-source audit
  • add 640x360 market, combat, and Boss runtime evidence

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

  • Godot 4.3 stable clean import
  • full suite: 85 tests, 796 assertions, 0 failures
  • real App path: market first, Start Run, V3 combat/HUD/effects, Boss, death/victory return, second V3 run

Notes

Draft only. Do not merge until integration review.

claude and others added 7 commits August 2, 2026 10:26
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
BotTony329 marked this pull request as ready for review August 2, 2026 11:39
@BotTony329
BotTony329 merged commit ad34ffe into develop Aug 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants