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>
57 lines
2.5 KiB
Twig
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">← 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 %}
|