Each chapter page gets a collapsible "Chapter notes" card at the top — a
free-text log the DM fills during play (the story the party tells after
the Crooked Goose, names given to NPCs, threads for next time). Saved
per chapter slug with a "saved <timestamp>" marker, same shape as the
Enemy/NPC game-notes field.
New: ChapterNote entity (slug-keyed, unique) + repository findOrCreate(),
ChapterNoteType form, chapter_note table migration (ENGINE = InnoDB
pinned). The storyline chapter route now also accepts POST to save.
Chapters stay hand-written Twig; only the mutable notes live in the DB.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chapters now carry an eyebrow ("Overview", "Prologue", ...) and a short
tag for the list/sidebar badge, instead of a bare running number, so
non-numbered entries read correctly.
New chapter: "Prologue - The Long Road to Brackenford", the session
outline formatted as a runnable document -- an at-a-glance metadata band
(levels, duration, locations, hook), the in-world courier notice as a
posted handout, numbered scenes, GM callouts for the "ask the players"
prompts, label/body rows for the skill-check clues and travel-day
prompts, a three-stage encounter block, dramatic beat lines, and a
before/after panel for the legend the party's own retelling becomes.
Cross-links the Hearthburrow to Enemies and the closing line to the
overview's "Five and the Five".
Tests: render every chapter partial headless (no HTTP/DB) so a broken
variable or path() fails loudly, check each registered chapter has its
template file, and assert the /storyline routes sit behind the login gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Storyline section is now a set of hand-written chapters. /storyline
lists them; /storyline/{slug} renders one inside a shell with a sticky
chapter nav and prev/next pager. Chapters are declared in
StorylineController::CHAPTERS, each pointing at a Twig partial under
templates/storyline/chapters/.
First chapter is the Brackenford Campaign Overview: the campaign-bible
text laid out as a long-form reading page -- numbered section headers,
a "by the numbers" / "the cast" quick-reference strip, pull-quotes for
in-world lines, GM callouts for the ticking clocks and the red herring,
and the escalation ladder. Storyline styles added to app.css.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the leftover scaffold DataTable with a grid of tiles linking to
Characters, NPC's, Enemies, Tracker, Art and Storyline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- extract the 5e stat-block markup into enemy/_statblock.html.twig
(sb.block(enemy) macro); the enemy detail page now uses it too
- each tracker combatant gets a "stat block" toggle that expands the
block in place (Bootstrap collapse), so you don't have to leave the
encounter to check AC / attacks
- open blocks are remembered in localStorage and re-opened after each
damage/heal reload, and the caret rotates when expanded
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Encounter entity; Combatant now belongs to an encounter (ON DELETE
CASCADE). Migration adds encounter_id, rehoming any existing combatants.
- /tracker lists encounters (name, count, last updated) with a "New
encounter" form; /tracker/{id} is the HP tracker for one encounter.
- Encounters can be renamed, have their combatants cleared, or be deleted.
- The Enemies list gains an encounter picker next to "Add to tracker"
(existing encounter or "— New encounter —", which prompts for a name);
an encounter's "Add enemies" link pre-selects it via ?encounter=.
- Routes for combatant HP/remove moved under /tracker/combatant/ to keep
the numeric {id} for encounters.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- one running tracker (Combatant rows); no encounters to manage
- Enemies list gets a "Track" column (checkbox + count) and an
"Add to tracker" button; picking N of an enemy makes "Goblin 1..N",
numbering continues if some are already on the tracker
- /tracker: per-combatant current/max HP with an HP bar, Damage / Heal
by amount, remove one, "Clear all". HP is clamped to [0, max];
0 HP shows a "Down" badge. Stats are snapshotted from the Enemy on add.
- "Tracker" nav item; tracker_picker Stimulus controller; app.js gains a
data-format="pick" cell renderer
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- gallery thumbnails now object-fit: contain (whole image, letterboxed),
and a card links to the new detail page instead of the raw file
- /art/{id} detail page: large uncropped image, "Open full size",
"Delete" (moved here from the gallery), and "Copy image"
- clipboard Stimulus controller copies the image to the OS clipboard
(converts non-PNG via canvas) for pasting into WhatsApp etc.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- /npcs/family/{family} (app_npc_family) renders a parent->child tree for
everyone sharing that family name; co-parents who are both in the family
are paired into one couple node, elders first
- FamilyTreeBuilder does the flat-list -> tree assembly
- the NPC detail page's family name (subtitle + Details row) now links there
- .family-tree CSS: nested list with connector lines, small portrait avatars
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 41 cut from the "Wren Thorn … Mira Rook" sheet (space-free names)
- removed 17 stale files that matched no NPC: the 16 hallucinated names
from the first sheet (CalenCooper, *Mill, *Reed) now superseded by the
real families, plus HiyHearth
assets/image/npc-pictures/ = 180 files, one per NPC.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 50 portraits cut from the "Sten Voss … Ralf Brewster" sheet
- 51 from the "Mara Brewster … Poppy Thorn" sheet (incl. one stray
"HiyHearth" the generator invented between Eli and May Hearth)
- existing 55 files renamed to remove spaces (Garrick Voss.png ->
GarrickVoss.png)
assets/image/npc-pictures/ now holds 156 files.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut from the contact sheet, one PNG per character named by the label
under each portrait. 39 match an existing NPC row; 16 (the extra Cooper
kids, the "Mill" family, the "Reed" block) have no matching NPC in the
database yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The back button restored a Turbo snapshot that still held DataTables'
injected search / length controls; re-init on turbo:load then stacked
another set each time. Tear every instance down on turbo:before-cache
so the cached snapshot is a plain table again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Npc entity (village spreadsheet columns) + game_notes text field for
the DM's live play notes, plus updated_at touched on every change
- Race / VillageStatus / BloodCartInvolvement / MysteryImportance as
string-backed PHP enums, rendered as dropdowns on the edit form
- /npcs: server-side DataTables (paging, global search and column sort
all run in the backend via NpcRepository::findForDataTable); name cell
links to the edit page, game notes shown truncated
- /npcs/{id}: Bootstrap-themed edit form (all fields + a large game
notes textarea); Symfony forms now use the bootstrap_5 theme
- app.js: generic server-side DataTable wiring driven by data-* on the
<table>/<th> (data-datatable-src, data-row-url, data-format)
- app:npc:import console command - upserts from data/npcs.csv by
(family, name), never overwrites game_notes; --purge / --dry-run
- data/npcs.csv: 180 villagers exported from the working spreadsheet
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>