Display strings derived from Callings/Races/Skills at call time. Retune a hit
die and the card follows, because the card never knew the number. Prose stays in
content; numbers stay in code; nothing is written twice.
Deviation from the plan, approved by the human: the plan's test asserted the
rendered line contains Callings.armor(id) — the RAW table value — while the plan's
own ARMOR_WORD maps "none" to "no armour". "none" is not a substring of "no armour",
so the Hedge-Mage failed. The card's copy is what the plan's docstring example shows,
so the copy stands and the test was fixed: armor_word(id) is now public and the test
asserts against the derived word. The guard is unchanged — hardcode an armour string
and retune the table, and it still goes red.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three callers (main_window_shell.gd, narrate_harness.gd, npc_harness.gd)
still built LogPlayer with the OLD 3-arg shape (name, class_id,
luck_descriptor) after LogPlayer.new() was migrated to the 4-arg
(name, race_id, calling_id, luck_descriptor) shape. This was invisible
to both grep and the test suite: every ctor param carries a default, so
GDScript compiles a 3-arg call against a 4-arg signature without error —
it just silently mis-binds positionally. "sellsword" landed in the
race_id slot (rejected — not a race), the luck descriptor landed in the
calling_id slot (rejected — not a calling), and luck_descriptor itself
fell back to its default "". Every field in the emitted dict came out
empty, which 422s against the canon-log schema's minLength/enum
constraints. Grepping for class_id can't see this, because there is no
class_id token left anywhere — the bug is purely positional.
Also fixes prompts.py's _article(""), which returned "an" because
Python's "" in "aeiou" is True, producing a doubled-space/wrong-article
digest line when race_id is empty. _describe_player now builds its
descriptor from whichever of race/calling are present and omits the
clause entirely when both are absent.
Adds a schema-parity guard for origin.schema.json's inlined
allowed_callings enum, which duplicated the calling roster with no
runtime check tying it to Callings.IDS. Renames leftover "class"
vocabulary in test names/comments to "calling".
Regression coverage:
- client/tests/unit/test_entities.gd: ctor populates both race_id and
calling_id; a calling passed into the race_id slot (the exact shape
of the three broken call sites) yields an all-empty row.
- api/tests/test_prompts.py: empty-race and empty-race-and-calling
digest rendering, asserting no doubled space and no dangling article.
- client/tests/unit/test_schema_parity.gd: origin.schema.json's
allowed_callings enum matches Callings.IDS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
The seed placeholder was a flat 1240 gold — 12.4 million copper under the real
model, which is 1,240 plot points in the command bar. It reseeds to 347c, a
party carrying silver and no gold: '◈ 3s 47c'.
This is the only call site of Currency.format() — the display edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
The scripts were tracked but their .gd.uid siblings weren't. Godot 4.4+ .uid
files pin each script's uid://, so leaving them out lets each clone regenerate
different uids and break uid:// references in scenes/resources. Matches the 159
.uid files already tracked in the repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuHRPE7VfppUJEaoGBEUqZ
The hex-removal correction set _glow.color = Palette.BLOOD (opaque), which
with the 0.5-0.8 modulate pulse read as a near-solid red slab. Restore the
intended low alpha via Color(Palette.BLOOD, 0.22) — RGB still from Palette,
alpha a numeric param (same idiom as the ember ramp). Whole-branch review
catch. Title tests 5/5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the three shell scenes (SystemDock, NarrationBook, MainWindowShell)
from build-everything-in-_ready() to authored .tscn node trees with the
theme + type-variations set in the editor, so the layout previews and can
be arranged in the Godot editor. Scripts keep only on-load work: @onready
node refs, DM-loop wiring, the dock slide, and binding seed ShellState into
the fixed authored turn-order tokens + consumable slots.
Fixes the dock offset + over-long buttons (now fixed-width DockButtons in a
right-anchored menu). Headless probe: root 1920x1080, world/book split
1180/740, dock at the world's right edge — no collapse. Suite 138/138.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DarkPanel is a PanelContainer-base variation (only a `panel`
stylebox); a Button looks up normal/hover/pressed, which don't exist
on it, so SystemDock's menu buttons rendered as default gray Godot
buttons instead of mock-faithful dark nav buttons.
Add a dedicated Button-base DockButton variation (Palette-only
colours, dock-dark normal/hover/pressed styleboxes), point
system_dock.gd's menu buttons at it, and regenerate the committed
game_theme.tres artifact from the builder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
By-hand cold-start test showed 3 phrases before the response; 1.5x the
cadence so each line stays on screen a touch longer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>