Commit Graph

17 Commits

Author SHA1 Message Date
Christopher Wiebe 8bf9eba2a7 M6.8: Hybrid trait pickers (Phase B)
Per theriapolis-rpg-clades.md "Building a Hybrid": hybrids now pick
two clade traits from the dominant parent + one from the other (2/1
split keyed off DominantParent), and one species trait + one species
detriment from each parent. All clade detriments still inherit fully
from both parents. Universal hybrid detriments unchanged.

CharacterDraft gains six new fields (sire/dam clade-trait arrays,
sire/dam species trait/detriment ids) and a CladeTraitLimit(lineage)
helper. Step 0/1 validators enforce the picks; Aside renders only the
chosen subset for hybrids.

Cascading clears: clade swap clears that lineage's bonus + clade
traits + (if species also invalidated) species pick; species swap
clears that lineage's species trait/detriment; dominant flip trims
overflow from the end (non-destructive when possible); hybrid-off
clears all six new fields.

Toggle buttons in both steps wire MouseEntered/Exited into
PopoverLayer so the player can read each trait's description on
hover (detriment buttons get the red-tinted "DETRIMENT" popover).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 20:12:56 -07:00
Christopher Wiebe e3f0296e6f M6.7: Parchment theme pass
Lights up the M5 codex design system across the wizard. Default
palette swaps from dark leather to aged-parchment cream with
sealing-wax red selection emphasis, matching the React prototype's
default theme variant. CodexTheme.Build() is applied at the wizard
root so every step + Aside + popover cascades through it.

Theme additions:
- Parchment palette in CodexPalette (Dark retained as alt)
- Type variations registered for Card, CodexPopover, Pill,
  PillDetriment, AbilityToken, AbilitySlot, SkillRow — without
  SetTypeVariation, panel-stylebox lookup falls through to Godot's
  default dark slate, which is what was happening to every bare
  PanelContainer before this pass.
- panel_hover stylebox on Card (gild border) wired via CodexCard's
  MouseEntered/Exited helper; panel_selected bumped to 3px seal-red
  border + soft shadow so selection reads at a glance.

Card selection refactor:
- Replaced the warm-cream Modulate hint on cards with stylebox swaps
  via the new CodexCard.SetSelected helper. The Modulate approach
  was a no-op on cream-on-cream parchment; the stylebox swap looks
  the same on either palette.
- Step intros + Aside section headers now use the existing Eyebrow /
  H2 / H3 / CardName / CardMeta / CardBody label variations.
- Confirm button on Step VIII uses the PrimaryButton variation.

Popover + chip behaviour:
- PopoverLayer is now MouseFilter=Ignore so clicks/scroll/hover all
  pass through. Adjacent chips fire reliably even when the previous
  popover overlaps them spatially.
- Dropped the 80ms grace timer; chip MouseExited closes immediately.
- TraitChip MouseFilter Stop → Pass so clicks bubble up to the
  parent card's GuiInput (selecting the card).

Misc:
- Wizard._Ready inserts a backing Panel so the parchment Bg fills
  the canvas — Wizard root is a plain Control, which paints nothing.
- CodexTheme font lookup tries Cormorant-Medium before -Regular and
  globalizes res://Fonts/ for runtime FontFile load (the previous
  fallback used ContentPaths which points at a sibling data tree).
- StepStats final-score Label rendered at font_size 22 to match the
  AbilityToken die.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 22:04:24 -07:00
Christopher Wiebe bb986d49f9 M6.6: StepReview signing + hybrid math revision
- New Step VIII (Review): name input and Confirm button that
  saves the finalized character to user://character.tres.
- Hybrid lineage rules simplified per project decision: drop
  the "no-stack on overlap, take +1 free elsewhere" rule from
  theriapolis-rpg-clades.md. Hybrids now pick one ability mod
  from each parent clade and they sum if they overlap.
  Removes HybridFreeAbility, the free-bonus picker row, and the
  overlap special case from AbilityCalc + WizardValidation.
- StepClade bonus rows now mutate in place (sync ButtonPressed)
  instead of tearing down on every Refresh — the old path freed
  the very button mid-Pressed-signal, leaving stale buttons next
  to the new ones.
- StepSkills drops the redundant "Calling: X · History: Y" meta
  line; both are already shown in the Aside summary.
- Aside hybrid section adds dual-species traits and the
  universal-hybrid detriment pills.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 20:51:55 -07:00
Christopher Wiebe 0e5d4b7425 M6.5: StepSkills + ability-bonus breakdown + Aside redesign
The skills step lands as the last data-driven character creation step
(only Sign — name + confirm — remains for M6.6). Brought a few cross-
cutting refactors with it.

Scenes/Steps/StepSkills.cs:
  Direct port of StepSkills in src/steps.jsx — all 18 skills laid out
  in 6 ability-grouped panels (STR/DEX/CON/INT/WIS/CHA), 2-column
  grid. Background-granted skills appear pre-checked and locked;
  user picks `class.SkillsChoose` more from `class.SkillOptions`.
  Hover the skill name → popover with the codex flavor description
  (limited to the title only — hovering checkboxes / source tags
  doesn't trigger the popover, avoids interference with adjacent
  rows' click targets). Fixed-width [✓] / [ ] / [—] checkbox slot
  so toggling doesn't shift the row layout.

UI/SkillsCatalog.cs (new):
  Static skill table — JSON id, display label, governing ability,
  and the codex SKILL_DESC text ported verbatim from src/data.jsx.
  Mirrors Theriapolis.Core.Rules.Stats.SkillId; descriptions live
  here because backgrounds.json and classes.json don't carry them.

UI/AbilityCalc.cs (new):
  Final-score math — base assignment + clade and species mods, with
  per-source breakdown for the bonus popover ("+1 from Canidae · +2
  from Wolf"). Hybrid mode tags each clade source with its lineage
  ("(sire)" / "(dam)"). Used by both StepStats and the Aside so the
  two views can never disagree on what a +N badge means.

UI/BackgroundAvailability.cs (new):
  Extracted from StepBackground — shared rules table for hybrid-only
  and clade-restricted backgrounds. Now also consulted by StepClade
  when the player changes lineage: the currently-selected background
  is auto-cleared if the new lineage no longer satisfies its rule
  (e.g., Pack-Raised clears when switching from Canidae to Felidae,
  Passer clears when toggling Hybrid off). Implemented via
  Resource.Duplicate + Patch on the duplicate to evaluate the
  hypothetical post-patch state without committing prematurely.

StepStats.cs:
  Per-row layout extended: ability label | slot | bonus chip | final
  | d20 mod. Bonus chip is a TraitChip with the per-source breakdown
  in its hover description. Auto-assign now sorts empty abilities by
  AbilityCalc.TotalBonus DESCENDING (with class.PrimaryAbility as
  tiebreaker) — biggest pool value lands on the ability already
  receiving the biggest lineage bonus, maximising final scores.

Aside.cs (significant redesign):
  - Name centered at top.
  - Lineage details: 2-column grid, full-width.
    - Purebred:  Clade | Species, then Calling | Background, then
                 Subclass | (empty).
    - Hybrid:    SIRE ★ | DAM (centered + underlined column heads),
                 Clade | Clade, Species | Species, then the same
                 calling/background/subclass rows.
  - Attributes: STR/DEX/CON/INT/WIS/CHA each with bonus chip (omitted
    when +0), final score, d20 modifier. Self-contained min-width
    table so it can't widen the panel beyond its alloc.
  - Pills: traits, detriments, level-1 features, background feature,
    skill chips (BG-locked + user-chosen). All hoverable for descriptions.
  - Whole panel wraps in a ScrollContainer so an over-tall summary
    scrolls in place instead of pushing the wizard layout off-screen.
  - Width nudged 320 → 360px. Smaller font on label tags, autowrap
    on value labels so long names ("Hybrid Underground") wrap rather
    than push the panel wider.

Card grids: changed all five card-grid steps (Clade, Species, Class,
Subclass, Background) from SizeFlagsHorizontal.ExpandFill →
ShrinkCenter. Cards stay at their CustomMinimumSize 200 wide and
the grid horizontally centers in PageMain. The right-side gap
between content and Aside is now uniform regardless of how many
cards or whether the last row is partial — fixes the "Clade tab
feels too padded, Background tab too tight" perception.

Closes M6.5. Per guide §12, what's left in M6: M6.6 (StepReview —
name + summary + Confirm handoff per guide §11) and M6.7 (parchment
Theme pass).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 18:10:05 -07:00
Christopher Wiebe ce87eb11ad M6.4: Card-grid steps + hybrid origin + clade-restricted backgrounds
Per GODOT_PORTING_GUIDE.md §12, the four "easy" card-grid steps land
together (Species / Calling / Subclass / History), plus three real
features that emerged during testing: cross-step validation gating,
hybrid origin, and clade-restricted background availability.

New step files (Scenes/Steps/):
  StepSpecies.cs    — cards filtered by clade; for hybrids shows two
                      stacked grids (Sire / Dam).
  StepClass.cs      — all classes; class change clears chosen skills
                      and the previously-selected subclass.
  StepSubclass.cs   — subclasses filtered by ClassDef.SubclassIds.
  StepBackground.cs — backgrounds filtered by hybrid + clade rules
                      (see below).

UI/WizardValidation.cs (new):
  Static per-step validators against CharacterDraft. Replaces the
  per-instance Validate() route on the wizard side — Wizard now
  computes the lock state for every step in the flow, not just the
  current one. Mirrors app.jsx's firstIncomplete rule exactly.

  Bug it fixes: previously the wizard checked only the current step's
  validity, so picking a clade let you skip directly to Abilities
  without picking species/calling/etc.

UI/CharacterDraft.cs:
  Phase 6.5 hybrid fields — IsHybrid, SireCladeId, SireSpeciesId,
  DamCladeId, DamSpeciesId, DominantParent. EffectiveCladeId /
  EffectiveSpeciesId resolve to the dominant parent's lineage when
  hybrid; downstream steps don't need to care which path. Helpers
  HasClade(id) and HasAnyCladeOfKind(kind) feed the background
  availability rules.

StepClade.cs:
  Hybrid toggle splits the picker into Sire + Dam grids with a
  Dominant Lineage radio. Validation refuses same-clade Sire+Dam.
  Switched to build-once + mutate-in-place: cards are created once
  during Build(), Refresh just updates Modulate per selection state.
  Tearing down + rebuilding inside the click callback caused
  duplicates because Free() defers when the freed node is mid-signal.

StepBackground.cs:
  Availability rules table — predicates per restricted background id.
  Hybrid-only: passer, hybrid_underground, former_chattel.
  Clade-restricted: warren_runner (Leporidae), pack_raised (Canidae),
  herd_city_born (any prey clade).
  Hybrids match if either parent satisfies the rule.

Other steps (Species/Class/Subclass/Background):
  Refresh dispatched via Callable.From(Refresh).CallDeferred() so the
  rebuild runs after the click handler completes — same Free()-during-
  signal bug as StepClade hit, fixed via deferral instead of mutate-
  in-place because the card lists are dynamic (clade- / class- /
  hybrid-flag-dependent).

Wizard.cs:
  - RebuildStepperStates uses WizardValidation.FirstIncomplete to lock
    every step past the first unsatisfied one.
  - OnStepperClicked checks every step in [0..target-1].
  - UpdateChrome's banner uses WizardValidation for the active step.
  - Scroll preservation moved here (snapshot before step.Refresh
    fires, restore in _Process); StepStats's local copy removed.

Wizard.tscn:
  Scroll node marked unique_name_in_owner so Wizard can grab it.
  PopoverLayer's TraitChip is reused throughout the new step cards.

Aside.cs:
  Hybrid-aware summary — shows "Sire (dominant)" / "Dam" lineage rows
  when IsHybrid; otherwise the existing Clade / Species rows.

Verified end-to-end:
  - Walk Clade → Species → Calling → Subclass → History → Abilities
  - Stepper locks every step past first unsatisfied
  - Hybrid toggle works both directions, dominant changes lineage
  - Hybrid-only and clade-restricted backgrounds appear / disappear
    based on lineage
  - Scroll position preserved across selections
  - Drag-drop still works on Abilities

Closes M6.4. Per guide §12, next is M6.5 — StepSkills (class-driven
choice list with TraitChip per skill).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 22:24:33 -07:00
Christopher Wiebe ba3ebe7ff3 M6.3: Trait popovers — shared PopoverLayer + TraitChip triggers
Per GODOT_PORTING_GUIDE.md §6 (and §12 build order — popovers before
the easy card-grid steps because traits/skills/bonuses surface them
everywhere). One reusable popover panel; lightweight chip triggers.

Scenes/Widgets/PopoverLayer.cs:
  CanvasLayer added once as a child of Wizard.tscn. Owns one
  PanelContainer + close Timer; static Instance for chip-side access.
  ShowFor(trigger, ...) populates and positions the popover at the
  trigger's global rect with viewport clamp + flip-above logic
  (mirrors src/trait-hint.jsx). 80 ms grace period when moving from
  trigger to popover so the popover stays open across the gap.
  Detriment popovers get a red Modulate as a placeholder for the
  seal-coloured StyleBox the theming pass will install.

Scenes/Widgets/TraitChip.cs:
  Lightweight PanelContainer + Label trigger. On MouseEntered asks
  PopoverLayer.Instance to show; on MouseExited schedules close.
  Pill styling deferred to theming (default Godot panel for now;
  TraitChip / TraitChipDetriment styleboxes will land alongside
  the parchment Theme pass).

Wizard.tscn:
  PopoverLayer added as a top-level CanvasLayer child so popovers
  float above every step's content regardless of where the trigger
  is in the tree.

Steps/StepClade.cs:
  Replaces the placeholder "{n} traits, {m} detriments" line with an
  HFlowContainer of TraitChip per trait + per detriment. Hover any
  chip → popover shows name + description (+ DETRIMENT tag for the
  detriment chips).

  Also: cards switched from Button to PanelContainer for content-
  driven height. Button isn't a Container, so its intrinsic min
  size didn't aggregate from the inner vbox — at higher trait
  counts the chips overflowed into the cards below. PanelContainer
  is a Container, so the card grows with its content. GuiInput
  handles the click-to-select; selected state shown via Modulate
  tint until the proper StyleBox swap lands in theming.

Closes M6.3. Per guide §12, next is M6.4 — easy card-grid steps
(Species / Calling / Subclass / History) variations on the StepClade
pattern, then StepSkills, then StepReview.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 20:57:02 -07:00
Christopher Wiebe 4d3db17a89 M6.2: Step V Abilities — drag-drop assignment + roll/auto-assign
Per GODOT_PORTING_GUIDE.md §7, the highest-risk piece in the wizard.
Three reusable widgets + the orchestrating step.

Scenes/Widgets/AbilityToken.cs:
  Draggable Control with Value + Origin metadata. _GetDragData returns
  a Dictionary payload {kind, value, from, ability, idx} per guide
  §7.1. MouseFilter = Pass so clicks propagate to the parent slot
  for the click-to-return affordance (later removed; see commit body).
  Drag preview is a dimmed duplicate.

Scenes/Widgets/AbilitySlot.cs:
  PanelContainer drop target per guide §7.2. Accepts any
  ability_value payload via _CanDropData / _DropData and emits
  Dropped(payload). Each slot owns one ability id (STR/DEX/CON/INT/
  WIS/CHA).

Scenes/Widgets/AbilityPool.cs:
  HBoxContainer drop target per guide §7.3. Accepts only slot→pool
  drops (returning an assigned value to the pool); pool→pool drops
  are no-ops.

Scenes/Steps/StepStats.cs:
  Direct port of StepStats in steps.jsx per guide §7.4. Standard
  array (default) and roll-4d6-drop-lowest method tabs; Reroll
  button visible in roll mode; Auto Assign sorts the remaining pool
  descending and places the largest values into empty slots ordered
  by class.PrimaryAbility. Three drag-drop cases (pool→slot,
  slot→slot swap, slot→pool) all delegate to a single Patch call,
  then the entire token tree rebuilds from the new draft state on
  the Changed signal — handlers don't reparent anything manually.

Issues hit during development and resolved before commit:
  - Initial click-to-return on slot pre-empted drag-from-slot every
    time (the GuiInput fired on mouse-down, before Godot detected
    the drag). Removed click-to-return — drag is the canonical
    interaction; that matches the React prototype anyway.
  - Token MouseFilter = Stop blocked clicks from reaching the slot
    layer; switched to Pass which still works as a drag source.
  - Refresh() teardown + rebuild reset the parent ScrollContainer's
    scroll to 0 every drop. CallDeferred / SetDeferred / CreateTimer
    all raced because layout settles over multiple frames; the fix
    that worked was capturing scroll position pre-rebuild and
    restoring in _Process the next frame.

Wizard.cs:
  StepTypes[5] = typeof(StepStats); the Abilities step is now
  reachable. (StepTypes[1..4, 6..7] still null — coming in M6.3+.)

Verified: all three drag scenarios + click handling + auto-assign
+ method switch + reroll work; scroll position holds across drops.

Closes M6.2. Next per guide §12: M6.3 — popover system (TraitChip +
shared PopoverLayer) before adding more easy card-grid steps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 20:36:19 -07:00
Christopher Wiebe ee5439285c M6.1: Character creation wizard foundation (.tscn + Resource-based draft)
Pivots from M5's code-built UI to the editor-authorable .tscn pattern
recommended in GODOT_PORTING_GUIDE.md, after a session of fighting
Godot idioms with code-only layout. Default theme only; the parchment
Theme lands last per the guide's §12 build order so layout bugs surface
as layout bugs, not theming bugs.

GODOT_PORTING_GUIDE.md:
  Authored by Claude Design as the canonical port reference. Maps the
  React prototype's structure onto Godot 4.6 with concrete code sketches
  and a build-order recommendation. Drove the M6 architecture.

Fonts/:
  Cormorant Garamond (Medium + MediumItalic) and Crimson Pro (Regular +
  Italic + SemiBold) under OFL — the React prototype's serif-display
  and serif-body families. Not yet wired through CodexTheme.Build()
  because theming is deferred; CodexTheme.LoadFontFromFonts already
  picks them up automatically when the Theme pass lands.

Scenes/Wizard.tscn + Wizard.cs:
  Wizard shell per guide §4: codex-header (title + folio counter) +
  Stepper + Page (StepHost + Aside) + NavBar (Back / validation / Next).
  All node lookups via unique-name (%) syntax; layout authored as a
  scene file you can open in the editor. Step lifecycle drives the
  Aside via signal binding. Stepper logic mirrors app.jsx — locked
  iff some EARLIER step is unsatisfied; "type not yet implemented"
  doesn't lock.

Scenes/Aside.tscn + Aside.cs:
  Right-rail summary per guide §10. Single Refresh() rebuild on
  CharacterDraft.Changed; cheap enough not to bother with partial
  updates. Width 320 (was 380 before the layout overflow fix).

Scenes/Steps/IStep.cs + StepClade.cs:
  Per-step Bind(draft) + Validate() contract. StepClade renders the
  3-column clade card grid; click commits via CharacterDraft.Patch
  which triggers the Resource.Changed signal that Aside and Wizard
  both subscribe to.

UI/CharacterDraft.cs:
  Resource (not Node) per guide §2.1. Mirrors app.jsx's `state` shape
  exactly. Patch(dictionary) emits the inherited Resource.Changed
  signal — listeners use `draft.Changed += handler` regardless of
  which field changed. CodexContent provides lazy-loaded immutable
  content tables (Clades, Species, Classes, Subclasses, Backgrounds).

Main.{cs,tscn}: Node → Control
  When Main was a Node, Control children couldn't anchor to a real
  parent rect — they sat at (0,0) at intrinsic min size. With wide
  step content (3-column 200-px-card grid), the Wizard's min size
  pushed the navbar beyond the viewport's right edge, hiding the Next
  button on smaller windowed viewports. Making Main a full-rect-
  anchored Control gives child scenes a proper rect to lay out in.

UI/Widgets/CodexStepper.cs:
  Anchored the inner vbox to fill the button rect. Without this, the
  vbox sat at the button's top-left at intrinsic size and labels
  rendered in the corner — visible as the active-step label being
  off-center from the highlight bar.

Verified at 1152x720 windowed and (separately) at fullscreen:
  - 3-column card grid fits inside Wrap margins + Aside without
    horizontal overflow
  - Stepper labels centered under their highlight bars
  - Next button visible after clade selection; future steps switch
    to "coming soon" placeholder when clicked
  - Aside summary fills in CLADE block on selection

Closes M6.1.  Next per guide §12 build order: M6.2 — StepStats with
drag-drop (highest-risk piece, de-risk before easy steps).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:35:03 -07:00
Christopher Wiebe 953bb985ad M5: Codex design system (dark theme) + kitchen-sink
Programmatic Theme builder + reusable popover and stepper widgets,
ported from CharacterCreator.zip's :root design tokens. Kitchen-sink
scene exercises every primitive for visual eyeballing.

CodexPalette.cs:
  Color tokens lifted verbatim from the React prototype's `:root`
  block (--bg, --ink, --gild, --seal, etc.). Variable names mirror
  the CSS so the audit trail stays readable. Spacing locked at the
  prototype's normal density (--gap=24, --pad=28, --radius=2).

  Scope cut: only the Dark theme ships. The React prototype designed
  Parchment, Dark, and Blood as switchable variations — user direction
  during M5 is that only Dark (leather + candlelight) is wanted for
  this game. Parchment/Blood code dropped, plan doc updated to match
  (§1 goal #5, §4.5 UI map, §5 M5 scope, §10 resolved decisions #4).
  No runtime theme switcher.

CodexTheme.Build():
  Programmatically constructs a Godot Theme from CodexPalette.Dark
  plus CodexSpacing/CodexType tokens. Configures Panel, Card,
  CodexPopover styleboxes; Label variations for H1..H4, CodexTitle,
  Eyebrow, Meta, ValidationOk/Error, CardName/Body/Meta, StepperNum/
  Name; Button + PrimaryButton + GhostButton variants; LineEdit,
  CheckBox, scrollbar styling.

  Fonts: looks for CormorantGaramond / CrimsonPro / JetBrainsMono
  TTFs in res://Fonts/ (or Content/Fonts/) and graceful-falls-back to
  Godot defaults if missing. M5 ships with no fonts in repo; user can
  drop them in later for typography parity with the React prototype.

CodexPopover.cs:
  Hoverable text trigger + floating PanelContainer, mirrors
  src/trait-hint.jsx. Viewport-clamps horizontally and vertically;
  flips above the trigger if there's no room below; 80 ms grace
  period when moving cursor from trigger to popover. Detriment
  variant uses the seal-coloured stylebox. Future TraitName /
  SkillChip / BonusPill widgets layer className differences on top.

CodexStepper.cs:
  Roman-numeral horizontal stepper with Pending / Active / Complete /
  Locked states. Active step gets a 2-px gild underline, Complete
  shows a ✓ in seal-red, Locked shows ✕ + 0.45 modulate. Emits
  StepClicked(int) for non-locked rows. M5 is decorative — M6 wires
  the signal to the character-creation state machine.

KitchenSink.cs + Main.cs --codex-test:
  Verification scene rendering every primitive (header, stepper,
  buttons, inputs, cards, trait popovers). Clicks log to console.
  Fonts default to Godot's Noto Sans until res://Fonts/ is populated.

Closes M5 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M6 (title + character creation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:29:22 -07:00
Christopher Wiebe 42d66c00c3 M4: Tactical render + unified seamless-zoom WorldView
Implements the seamless-zoom contract from CLAUDE.md: one Camera2D
covers both world-map and tactical scales; layers fade in/out at zoom
thresholds; polyline widths and the player marker counter-scale with
zoom so on-screen sizing stays consistent across the full range.

Layers (bottom-up in WorldView):
  Biome sprite     — 256x256 ImageTexture scaled by WORLD_TILE_PIXELS;
                     always visible (acts as backdrop past the tactical
                     streaming radius).
  TacticalChunks   — TacticalChunkNode children added on chunk-loaded
                     event; visible only when zoom ≥ 4.
  Polylines/Bridge — Line2D children; always visible. Width recomputed
                     each frame as baseScreenPx / camera.Zoom so the
                     on-screen stroke is constant (4 px highway, 3 px
                     post road, 2 px dirt road, 4.5/3/2 for major-river/
                     river/stream, 4/2 for rail tie/line, 6 for bridge).
  Settlements      — SettlementDot children; hidden when zoom ≥ 2 (you
                     are visually "inside" them at tactical scale).
  PlayerMarker     — Always visible; Scale = 1/zoom keeps it at
                     PLAYER_MARKER_SCREEN_PX on-screen across all zooms.

TacticalAtlas:
  Loads PNGs from Content/Gfx/tactical/{surface,deco}/ via ContentLoader
  with name_0.png/name_1.png/... variant probing (silent miss). Falls
  back to procedurally-generated solid placeholders matching MonoGame's
  TacticalAtlas colour table so missing art doesn't break rendering.

TacticalChunkNode:
  One Node2D per cached chunk, positioned at (OriginX, OriginY) in
  world-pixel space. _Draw iterates the 64x64 tile grid once and Godot
  caches the rasterised CanvasItem; subsequent frames blit instead of
  re-issuing 4096 DrawTextureRect calls.

ChunkStreamer integration:
  WorldView listens to OnChunkLoaded / OnChunkEvicting and adds /
  removes TacticalChunkNode children. Streaming radius is computed
  dynamically from the viewport size and camera zoom plus a 2-tile
  buffer, so chunk loads always cover the visible viewport with margin.
  Chunks only stream when zoom ≥ 4 (tactical is visible).

Main.cs:
  --world-map [seed]            → WorldView, fit-to-viewport zoom
  --tactical  [seed] [tx] [ty]  → WorldView, zoom 32 at given tile
  Both flags converge on the same scene; mouse wheel transitions
  seamlessly between modes.

ContentLoader silent miss:
  Removed the "Missing texture" PrintErr — atlas variant probing
  legitimately tries name_3.png that doesn't exist, and the noise
  drowned the console. Genuine asset failures still surface via
  AssetTest's count summary.

Deleted (replaced by WorldView):
  Theriapolis.Godot/Rendering/WorldMapView.cs
  Theriapolis.Godot/Rendering/TacticalView.cs (created earlier in M4,
  never committed — superseded before commit).

Closes M4 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M5 (codex design system).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:08:14 -07:00
Christopher Wiebe f57ea0b70c Fix ChunkStreamer.EnsureLoadedAround leaving pre-warmed chunks stuck
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>
2026-05-01 20:07:06 -07:00
Christopher Wiebe a23cf8bd97 M3: Asset pipeline — ContentPaths + ContentLoader + asset-test
Formalises Content/ access from the Godot host. Content lives at the
repo root (sibling of Theriapolis.Godot/), not duplicated under res://,
so the MonoGame branch and headless Tools keep reading from the same
single source of truth.

ContentPaths.cs:
  Static ContentRoot/DataDir/GfxDir resolved once via res:// walk-up
  and cached. Replaces two inline ResolveDataDir copies in SmokeTest
  and WorldMapView.

ContentLoader.cs:
  LoadGfx(relativePath) -> ImageTexture, cached by relative path.
  Bypasses the res:// import pipeline because Content/ lives outside
  the project — fine for static pixel-art assets at native size, and
  the project default texture filter is already Nearest. Cache is
  per-process, never evicted (full atlas <1 MB).

AssetTest.cs + Main.cs --asset-test flag:
  Smoke-tests the pipeline. Walks Content/Data and Content/Gfx,
  reports counts, attempts to load every PNG, prints per-subdir
  breakdown. Quits with non-zero on any failure.

Verified post-refactor (--asset-test):
  53 JSON files in Data, 50 PNG files in Gfx (8 tactical/deco +
  42 tactical/surface), 50/50 loaded, 0 failures.

Verified no regressions:
  --smoke-test (M1) still produces canonical FNV hashes.
  --world-map 12345 (M2) still produces 3 rivers / 91 roads /
  226 settlements / 0 rails / 0 bridges.

Scope note: plan mentioned "tile/NPC/CodexUI atlases — three
separate themes". Only tactical/ exists in Content/Gfx today; NPC
and CodexUI atlases never landed during MonoGame development. M3
ships what's actually present. ContentLoader.LoadGfx works for any
future sub-directories without changes.

Closes M3 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M4 (tactical render).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 19:13:51 -07:00
Christopher Wiebe 59784048cd M2: World map render in Godot
Renders the full worldgen output as a Godot scene at visual parity with
worldgen-dump's PNG output: biome tiles, rivers/roads/rails as Line2D
polylines, settlements as filled circles. Pan + zoom via Camera2D.

WorldMapView.cs:
  - Loads Content/Data via res:// walk-up, runs WorldGenerator.RunAll
  - Tile palette built from BiomeDef.ParsedColor() — same source as the
    PNG dump, so colours are identical
  - Tiles rendered as a 256x256 Image scaled by WORLD_TILE_PIXELS to
    cover world-pixel space (matches polyline coord system)
  - Polyline draw order mirrors LineFeatureRenderer.cs: roads (smaller
    first) -> rivers -> rail tie underlay -> rail line. Bridges as
    short Line2Ds; settlements as SettlementDot (Node2D + _Draw circle)
  - Line widths in world-pixel space, tuned for visibility at world-map
    zoom; M4 will add zoom-aware width scaling for tactical view
  - Camera fits the whole world (95% of viewport) on first frame

PanZoomCamera.cs:
  - Mouse-wheel zoom centered on cursor (cursor world-point stays fixed)
  - Middle/right click + drag to pan
  - MinZoom/MaxZoom configurable per-instance

Main.cs:
  - --world-map [seed] flag launches the view (default seed 12345)
  - Arg parser now reads both GetCmdlineArgs and GetCmdlineUserArgs so
    callers don't need to remember the "--" separator
  - --smoke-test path and M0 hello-world fallback unchanged

Visual diff against world_seed12345.png (generated by
worldgen-dump --seed 12345) confirmed manually: same biome palette, same
rivers/roads topology, same settlement placement and tier colours.
3 rivers, 91 roads, 226 settlements (138 PoIs), 0 rails (ENABLE_RAIL=false),
0 bridges (this seed has no road/river crossings). All match the PNG.

Settlement dot sizes iterated twice from user feedback — final values
in tile units, scaled to world-pixel space, so they shrink at world-map
zoom and grow toward tactical zoom (the right "scale with the map"
behaviour).

Closes M2 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M3 (asset pipeline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 19:04:02 -07:00
Christopher Wiebe 57fe6bf173 M1: Headless parity verified between Tools and Godot
Proves Theriapolis.Core works untouched under Godot's csproj — the worldgen
pipeline produces byte-identical output whether invoked from the Tools CLI
or from inside the Godot process. This is the determinism contract surviving
the port.

Architecture test:
  CoreNoDependencyTests now forbids Godot.* and GodotSharp in addition to
  Microsoft.Xna and MonoGame. Both bans stay in force for the duration of
  the port so neither engine can leak into Core.

Determinism oracle:
  New worldgen-hash Tools command runs the full pipeline and prints FNV-1a
  hashes for every channel (elevation, moisture, temperature, biomes,
  settlements, polylines) plus per-stage hashes. Pairs with the Godot
  smoke-test for cross-process verification.

Godot-side smoke test:
  SmokeTest.cs runs WorldGenerator.RunAll inside the Godot process; Main.cs
  fires it on --smoke-test <seed>. Resolves Content/Data via res:// walk-up.
  M0 hello-world behaviour preserved when launched without the flag.

Verification (seed 12345):
  - dotnet run -- worldgen-hash and Godot --headless --smoke-test agree on
    all 6 channels and all 14 per-stage hashes (diff produces zero output)
  - 10-run sweeps stable on both sides post-determinism-fix
  - dotnet test: 708/708 pass

Closes M1 of theriapolis-rpg-implementation-plan-godot-port.md.
Next: M2 (world map render).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 21:38:15 -07:00
Christopher Wiebe b3da447673 Fix MoistureGen/TemperatureGen non-determinism (FastNoiseLite race)
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>
2026-04-30 21:37:55 -07:00
Christopher Wiebe 59e86af7a2 M0: Scaffold Theriapolis.Godot project (hello-world)
First milestone of the Godot port. Establishes the new project structure
and verifies the toolchain is wired up end-to-end.

- Godot.NET.Sdk/4.6.2 targeting net8.0; references Theriapolis.Core
- project.godot configured for borderless fullscreen at native resolution
  (per port plan §10 resolved decisions); F11 toggles to windowed
- Main.tscn + Main.cs hello-world; nearest-neighbor texture filtering
- icon.svg placeholder (T in gild on dark)
- Added to Theriapolis.sln

Verification:
- dotnet build Theriapolis.Godot.csproj: 0 errors, 0 warnings
- dotnet build Theriapolis.sln: 0 errors (6 pre-existing warnings unrelated)
- dotnet test: 708/708 pass in 26s (unchanged from master)
- Godot 4.6.2 opens project; fullscreen + F11 toggle confirmed visually

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:52:35 -07:00
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
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>
2026-04-30 20:40:51 -07:00