Files
FrankandClaude Sonnet 5 9052218927 Characters: build the page around Darragh Oakwanderer's sheet
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>
2026-09-22 08:55:28 +02:00

57 lines
2.5 KiB
Twig

{% extends 'base.html.twig' %}
{% block title %}{{ character.name }} · Characters · dmtools{% endblock %}
{% import 'characters/_sheet.html.twig' as cs %}
{% block body %}
<div class="d-flex justify-content-between align-items-start mb-3 gap-3 flex-wrap">
<div>
<a href="{{ path('app_characters') }}" class="text-decoration-none small">&larr; All characters</a>
<h1 class="h3 mb-0">{{ character.name }}</h1>
<span class="text-body-secondary fst-italic">{{ character.metaLine ?: '—' }}</span>
{% if character.playerName %}
<div class="text-body-secondary small">Played by {{ character.playerName }}</div>
{% endif %}
</div>
<div class="d-flex gap-2">
<a href="{{ path('app_characters_edit', {id: character.id}) }}" class="btn btn-outline-secondary btn-sm">Edit sheet</a>
<form method="post" action="{{ path('app_characters_delete', {id: character.id}) }}"
onsubmit="return confirm('Delete “{{ character.name|e('js') }}” from the character list?')">
<input type="hidden" name="_token" value="{{ csrf_token('delete-character-' ~ character.id) }}">
<button class="btn btn-outline-danger btn-sm">Delete</button>
</form>
</div>
</div>
<div class="row g-4">
<div class="col-lg-7">
<div class="char-portrait char-portrait--hero">
<img src="{{ asset(character.portraitPath) }}" alt="{{ character.name }}" onerror="this.parentElement.remove()">
</div>
<div class="card shadow-sm">
<div class="card-body">
{{ cs.block(character) }}
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Game notes</span>
{% if character.updatedAt %}
<span class="text-body-secondary small fw-normal">saved {{ character.updatedAt|date('Y-m-d H:i') }}</span>
{% endif %}
</div>
<div class="card-body">
{{ form_start(form) }}
{{ form_row(form.gameNotes) }}
<button class="btn btn-primary mt-2">Save notes</button>
{{ form_end(form) }}
</div>
</div>
</div>
</div>
{% endblock %}