The party sheets in assets/characters/ changed: Darrah_Oakwanderer.pdf is
gone, replaced by Darragh Fermwyr (same halfling ranger, renamed and
updated), Owayn Fermwyr (stout halfling fighter, BJ) and Dunnchadh
(ghostwise halfling druid, Circle of the Blighted, Rick).
- app:character:seed now carries those three sheets, read from the PDFs'
form fields. Darragh's portrait is renamed to match his new name.
- New skill expertise field (double proficiency bonus), since Darragh's
Deft Explorer gives him +7 Stealth and the sheet had nowhere to show
it. Expertise implies proficiency; the sheet marks it with two dots,
the edit form gets a checkbox list next to skill proficiencies.
- Migration adds skill_expertise, backfilling existing rows with [] so
the NOT NULL JSON column works on tables that already have characters.
Tests: expertise math on the entity, and a catalog test that every seed
sheet hydrates into a Character that passes validation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the Characters placeholder with a real 5e character sheet
feature, following the same read-only-detail/edit-split/game-notes
pattern as NPCs and Enemies:
- Character entity: identity, combat vitals, six ability scores, saving
throw and skill proficiencies (computed modifiers, not stored), a
small attacks table, and the sheet's freeform boxes (features,
equipment, personality, appearance, backstory, allies, treasure).
- Detail page shows the portrait uncropped, then the sheet - genuine
<table>s for ability scores, skills and attacks, since that's the one
part of a character sheet that actually is a table; everything else
reuses the prose helpers from the enemy stat block. Game notes are
the only editable field here, same as Enemy/NPC.
- Edit page covers every field via CharacterType; the attack rows are
six plain inputs handled by hand (not a Symfony collection field),
since it's a short fixed-shape table, not a dynamic list.
- Portraits follow the NPC convention: assets/image/characters/<NameNo
Spaces>.png, computed path, not a DB column.
- Imported the example sheet from assets/characters/ (Darragh
Oakwanderer, halfling ranger) directly into the database.
Tests: entity math (ability/save/skill modifiers, passive perception,
portrait path), template rendering for show/edit/index (catches broken
Twig/macro/path()/asset() calls), and the login-gate redirect. 21/21
passing; schema validate clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Halfling daughter of Bramble and Poppy Thorn, younger sister of Wren.
Portrait cropped of its frame and downsized to 450px wide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each chapter in the right-hand menu gets a caret that unfolds its
numbered sections (e.g. the prologue's 28 scenes); clicking one jumps
straight to it. The chapter you're reading starts unfolded, other
chapters' sections link across (/storyline/overview#section-…), and the
section at the top of the pane is highlighted as you scroll.
- ChapterOutline service parses each rendered chapter for its
<h2 class="storyline-section"> headings, adds anchor ids and returns the
outline; the controller renders all chapters once to feed the sidebar.
- storyline-toc Stimulus controller: fold/unfold, in-pane jump, scroll-spy
(works in the split view and in the small-screen window-scroll fallback).
- Tests for the outline parser.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each gallery tile gets a small drag handle (top-left, ✱). Grab it and
drop it anywhere among the others - the tile moves live as you drag
over neighbours, then the new order is saved automatically on drop.
- Artwork gets a `position` column; the gallery lists by position (was
upload date). New uploads land after everything else.
- New POST /art/reorder endpoint takes an ordered list of ids (one
shared CSRF token, "reorder-art") and rewrites positions 1..n.
- Migration backfills position from the existing upload-date order via
ROW_NUMBER(), then drops the bootstrap default so it matches the
Doctrine mapping.
- New Stimulus controller (art-reorder): native HTML5 drag/drop, no
library. dragover live-reorders the DOM by comparing cursor x to the
hovered tile's midpoint; dragend posts the resulting id order and
shows a small "Saving order… / Order saved" status pill.
Verified: schema validate clean: a throwaway script exercised
persist+reload of a reordered set against the real DB, and a live
browser check dispatched real DragEvents through the actual Stimulus
controller and confirmed the exact POST payload it sent. phpunit 9/9.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Players open a no-login link and see live cursors and dragged gear
pieces synced in real time, backed by a small Workerman WebSocket
relay (src/Command/PuzzleRelayCommand.php) run alongside symfony
serve - no Docker/Mercure hub needed. The board renders the wall
artwork with a spare-gears tray, and a "Reset puzzle" button (visible
only to logged-in users, verified server-side via a signed token) lets
the DM wipe the shared state for everyone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The chapter page now fills the viewport as two horizontal panes: the
chapter scrolls in the top ~2/3, the notes sit in a fixed dock in the
bottom ~1/3, always visible while you read. Drag the divider to change
the ratio, click the "Chapter notes" header to collapse the dock to a
bar; both the height and the collapsed state persist in localStorage.
- base.html.twig: add a {% block body_class %} hook on <body>.
- chapter.html.twig sets body class "storyline-chapter-page"; the split
layout CSS is scoped to it and falls back to normal stacked flow under
768px wide or 560px tall.
- app.js publishes the navbar height as --app-nav-h so the split can size
itself to viewport-minus-navbar even when the navbar wraps.
- new chapter_notes Stimulus controller: drag-resize + collapse + persist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>