The wizard now produces a real runtime Character instead of just
persisting the draft Resource. New Theriapolis.Godot/UI/CharacterAssembler
bridges CharacterDraft → CharacterBuilder, picking the purebred Build
or hybrid TryBuildHybrid path, threading clade/species/class/background
lookups, ability scores, skill picks, dominant-parent, subclass id,
and the items table for the starting kit. The captured
PlayerCharacterState writes to user://character.json (resumability
before the M7 save format lands) and the live Character is held in
CharacterAssembler.LastBuilt for future PlayScreen pickup.
StepReview.OnConfirmPressed surfaces build errors on the status label
instead of crashing, logs HP/hybrid/skill totals on success, and
keeps emitting the existing CharacterConfirmed signal.
Hybrid lineage bonuses now match the wizard's preview math.
CharacterBuilder gains HybridSireChosenAbility / HybridDamChosenAbility;
TryBuildHybrid replaces the old "apply both clades' full mods + both
species mods" blend with "apply each parent's chosen mod only" —
species mods don't apply for hybrids per project decision. Picks
stack additively when both parents land on the same ability
(canidae +1 CON × ursidae +2 CON → +3 CON). Empty pick = no bonus
(defensive fallback for headless builds). HybridCharacterTests'
BlendsAbilityMods rewritten for the new rule, plus two new tests
for stacking and empty-pick fallback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Skill list grows from 18 → 30 so every ability has 5 skills tied to it.
New skills appended at SkillId byte indices 18–29 to preserve
save-game compat with pre-M6.18 characters.
STR (+4): Brawl, Build-Read, Force, Haulage
DEX (+2): Driving, Marksmanship
CON (+5): Endurance, Fortitude, Hardiness, Lung-Craft, Pain Tolerance
CHA (+1): Scent-Speak
Endurance covers applied effort over time; Hardiness covers external
condition tolerance (temperature, smoke, altitude); Fortitude is
ingestion-only (poison, spoiled food, ritual draughts). Pain Tolerance
is function-while-wounded. Lung-Craft is breath/voice discipline.
Class skill_options updated across all 8 callings so every new skill
has 3+ class homes (Muzzle-Speaker remains the universal pick of
all 30). Backgrounds left untouched — their skill grants are
doc-canon and would benefit from a separate balancing pass.
CharacterBuilder.SkillToJsonName extended with explicit cases for the
new skills so compound enum names like BuildRead serialize as
"build_read" rather than the fallback "buildread" — caught by
CharacterBuilderTests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Species variants populated against the M6.13 schema:
- Lion-Folk sex axis: Mane Guard (male) / Huntress Reflexes (female,
+5 ft speed + advantage on initiative).
- Elk-Folk sex axis: Antler Combat with 10 ft reach when full rack
(male, retains seasonal Antler Drag) / Kick (female, prone on crit).
Base traits restored to doc canon: Herd Coordination (Help → +3) +
Endurance Runner (40 ft + advantage CON vs forced march); base speed
bumped 30 → 40; new base detriment Herd Instinct.
Ram-Folk replaced with separate Sheep-Folk + Goat-Folk species rather
than a lineage-axis variant on a single Ram entry. Bovidae now has 4
species. The lineage-axis toggle UI in StepSpecies BuildCard rolled
back; the schema stays for sex-axis (Lion/Elk) which auto-resolves.
ContentLoadTests + HybridCharacterTests updated; Size.cs comment too.
Calling lore: ClassDef gains Description; classes.json populated for
all 8 callings with the doc's italic blockquote + paragraph profile.
StepClass surfaces the description on the card.
Card layout uniformity: StepClass / StepSubclass / StepBackground all
switched to single-column ExpandFill grids (matching StepClade /
StepSpecies). Each card now spans the wizard's content width so the
description and feature chips have room to breathe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switch Step 0 (Clade) and Step 1 (Species) from a 3-column card grid
to a 1-column layout, with each card carrying a codex-voice
description paragraph between the meta line and the trait chips.
Rationale: establish the world's tone before mechanics — the player
reads who Canidae or Wolf-Folk *are* before evaluating ability mods
and trait pills. Trade is more vertical scrolling, but the card
content was already going wider than three columns comfortably
allowed once the parchment theme bumped padding.
Schema: CladeDef and SpeciesDef gain a Description field (string,
empty default). Populated for all 7 clades and 19 species, sourced
from the doc's italicized blockquote + a one-sentence summary of
the prose paragraph that follows. Empty descriptions fall through
silently — a species without a description still renders, just
without the paragraph.
UI: MakeGrid in both steps becomes Columns = 1 with ExpandFill;
BuildCard sets card.SizeFlagsHorizontal = ExpandFill (replaces the
fixed CustomMinimumSize 200) and prepends the autowrap description
label after the meta line. Hybrid mode stacks sire and dam single-
column grids vertically — same logic as before, just one card wide
each.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds character Sex (male/female) as a top-level CharacterDraft field
required for every character, and species variants as a layer on top
of the base species record. Two variant axes:
- "sex": auto-resolves from CharacterDraft.Sex for purebreds; for
hybrids, pinned by parent role (sire = male, dam = female)
by definition. No picker needed beyond Step 0.
- "lineage": explicit per-species pick (Ram-Folk's sheep/goat). Has
its own picker on Step 1 (purebred path under the grid;
hybrid path embedded into the per-parent pick column).
Schema (Theriapolis.Core/Data):
- SpeciesDef gains VariantAxis (string) and Variants (array of
SpeciesVariantDef { Id, Name, Traits, Detriments }).
- JSON content not yet populated — that's M6.14.
CharacterDraft adds:
- Sex (required by Step 0 validation)
- SpeciesVariant / SireSpeciesVariant / DamSpeciesVariant
- ResolveVariantId(species, role) that returns the active variant
id for a given context — used by Aside to layer variant traits
onto the base species traits.
Step 0 (StepClade): sex picker row above the hybrid toggle. Two
toggle buttons radio-style.
Step 1 (StepSpecies): purebred path renders a lineage picker below
the grid when the picked species has VariantAxis == "lineage";
hybrid path embeds a lineage picker at the top of each parent's
pick column. Hover popovers summarise each variant's contents.
Validation: Sex is required at Step 0. Lineage variant required at
Step 1 for any picked species (purebred or per-hybrid-parent) with
VariantAxis == "lineage".
Aside: AddVariantContent layers the resolved variant's extra
traits/detriments onto the base species rendering, for both purebred
and hybrid paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EnsureLoadedAround skipped Get() for any active chunk already in
_inflight. That worked for the MonoGame TacticalRenderer, which calls
Get() during its own draw loop and incidentally drains pre-warm tasks.
But subscribers to OnChunkLoaded (e.g. the Godot port) saw no event
when a previously-pre-warmed chunk transitioned into the active set on
a later frame — the chunk stayed in _inflight forever, presenting as
permanently-uncached gaps in the rendered world.
Fix: drop the !_inflight.ContainsKey(cc) guard. Get() already handles
all three paths (cache hit, inflight drain, fresh generate), so passing
every active chunk through Get() guarantees OnChunkLoaded fires once
per chunk regardless of how it was scheduled.
Same flavour of bug as M1's MoistureGen FastNoiseLite race —
cross-process / event-driven consumers exercise paths the in-process
pull-based test fixtures never hit. 708/708 tests still pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FastNoiseLite lazily populates its internal _perm[512] table on the first
GetNoise call via EnsurePerm(). When called concurrently from a Parallel.For
loop, threads race on this initialization and may read a partially-populated
table, producing different moisture/temperature values per row across runs.
Empirical: a 10-run worldgen-hash sweep on seed 12345 produced 4+ distinct
moisture hashes and 3+ distinct temperature hashes. All other channels
(elevation, biomes, settlements, polylines) remained stable; biomes only
because their bucket thresholds happened to absorb the upstream float noise.
The fix is the same one ElevationGenStage:125-130 and BorderDistortionGenStage:
102-104 already apply: call GetNoise once on the main thread before the
Parallel.For so _perm is fully initialized when worker threads start reading.
MoistureGenStage and TemperatureGenStage were missing this; now they have it.
WorldgenDeterminismTests didn't catch this because xUnit's WorldCache fixture
runs both pipeline variants in the same process, where consecutive runs hit
the same JIT/thread-pool state and produce the same corrupted output. The
Godot port surfaced it by invoking Core from a fresh process with different
threading.
Verified: post-fix 10-run sweep produces stable hashes on all six channels
(0xA8F99BB9795D8CF8 moisture, 0xAA05F3FB1523F6C3 temperature, seed 12345).
708/708 tests still pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>