Halfling (Mark of Hospitality) paladin, Oath of the Ancients, level 2,
played by Gerdien. Read from photos of her printed sheet (Aoife_1-4),
which are committed alongside the other source sheets. Named "Aoife
Fermwyr" so the existing AoifeFermwyr.png portrait is picked up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The party reaches Brackenford two hours after Eamon met Aldren: the Thorn
beekeepers, the Vale family and Edric's death, the council hiding Eamon,
the collection cart, and the first word of blood.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Darragh Oakwanderer never made it past the local database - the
Characters migration only creates the empty table, and I'd added him
with a one-off script that was never committed. This closes that gap
the same way app:enemy:seed does: a small catalog of ready-made sheets,
picked interactively or by name, idempotent (skips what's already
there), with --purge to remove them.
Verified against the local DB: purge, add, and a repeat add (correctly
reports 0 added / 1 already present) all produce exactly the row I'd
checked by hand earlier.
Co-Authored-By: Claude Sonnet 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>
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>
Worker::runAll() parses start/stop/restart/... off the real \$argv on
Linux - which here is Symfony console's own arguments (bin/console
app:puzzle-relay), not a command it recognizes, so it printed its
usage text and exited on every boot, and Docker kept restarting it
forever. Windows skips that argv parsing entirely, which is why this
never showed up locally. Worker::\$command = 'start' is Workerman's
own mechanism for exactly this "used as a library" case - it appends
to what getArgv() returns without touching the real \$argv.
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>
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>
The command now prompts with a numbered multi-select list instead of
seeding all monsters unconditionally. Names can also be passed as
arguments (case-insensitive) and --all keeps the old bulk behaviour for
non-interactive runs. Adds the homebrew Hearthburrow (Tiny Fey, CR 1/2)
to the catalogue, with its Cottage Actions in the legendary-actions
field and the Temper track in the description.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CSRF-protected POST to /enemies/{id}/delete with a confirm. Any
combatants referencing the enemy keep their snapshotted stats (the FK
sets enemy_id null), so deleting a monster type won't disturb a fight.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Combatant snapshots armor_class from the enemy when added (like maxHp),
shown as an "AC n" pill next to the name for quick hit checks. Migration
backfills existing combatants from their enemy.
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>
- docker/php/php.ini: upload_max_filesize 8M / post_max_size 10M
- docker/nginx/default.conf: client_max_body_size 10M
- ArtworkType: Image maxSize 8M with a clear message + help text
(local C:\php\php.ini was bumped from 2M the same way — not in the repo)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Artwork entity (name, stored filename, original name, mime, size, date)
- /art gallery grid; /art/upload (name + image file, PNG/JPEG/GIF/WebP,
20M cap); /art/{id}/file streams the image behind auth; /art/{id}/delete
(CSRF) removes row + file
- files stored under var/uploads/art/ (git-ignored, on the php_var volume
in Docker), served through a controller so they stay login-gated
- "Art" nav item
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Enemy entity: full 5e stat block (size/type/subtype/alignment, AC, HP,
speed, six ability scores, CR/XP/prof, saves/skills/senses/languages,
damage & condition modifiers, traits/actions/bonus/reactions/legendary,
description, source) plus a DM game-notes field and updated_at.
- CreatureSize / CreatureType enums.
- /enemies: server-side DataTable (name, size, type, CR, AC, HP, notes),
"Add enemy" button.
- /enemies/{id}: stat block rendered read-only + editable game notes.
- /enemies/new and /enemies/{id}/edit: the full stat-block form (no notes).
- "Enemies" nav item between NPC's and Storyline.
- Table left empty on purpose.
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>
- /npcs/{id} (app_npc_show): portrait, all fields shown read-only, and
the only editable control is the game-notes textarea. Father/Mother
link through to their own pages.
- /npcs/{id}/edit (app_npc_edit): the full structured form, now WITHOUT
game notes; saving returns to the detail page.
- NpcType drops gameNotes; new NpcGameNotesType holds just that field.
- The DataTable row link (/npcs/{id}) now lands on the detail page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Npc::getPortraitPath() maps the name to image/npc-pictures/<NameNoSpaces>.png;
the edit page renders it beside the heading (with family · occupation), and
drops the <img> via onerror when a portrait is missing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Parents now live in the father/mother fields, so "son/daughter/child of
X [and Y]" is redundant noise in Relationships / Notes.
- app:npc:strip-parent-text removes that clause and tidies leftover
punctuation ("Son of X, husband of Y" -> "Husband of Y"; standalone
"Son of X and Y" -> cleared). Keeps "grandson of", "father of",
"daughter-in-law of". 92 rows cleaned.
- data/npcs.csv regenerated from the DB with the cleaned text plus new
Father / Mother name columns
- app:npc:import resolves those optional columns in a second pass, so a
fresh install reproduces the parent links without app:npc:link-parents
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Npc gains self-referencing father/mother (nullable, ON DELETE SET NULL);
editable as dropdowns on the per-NPC form
- app:npc:link-parents parses "son/daughter/child of X [and Y]" from the
Relationships / Notes column, resolves the names within the same family,
and works out which parent is father vs mother from husband/wife/widow(er)
and son/daughter/sister/brother cues (plus a small name-gender table for
the few the text can't disambiguate). --dry-run / --force. 92/180 linked,
none ambiguous.
- migrations now pin ENGINE=InnoDB - the local WAMP MariaDB defaults to
MyISAM, which silently drops foreign keys
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>
Navbar (collapsible) with active-state highlighting driven by a nav list
in base.html.twig. Three routed, auth-gated controllers each rendering a
shared placeholder partial - data models still to be designed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>