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>
68 lines
2.9 KiB
Twig
68 lines
2.9 KiB
Twig
{#
|
|
# Shared 5e stat-block rendering. Used on the enemy detail page and, folded
|
|
# up, inside each tracker combatant; its prose helpers (mod, _line, _section)
|
|
# are also borrowed by the character sheet (characters/_sheet.html.twig).
|
|
# {% import 'enemy/_statblock.html.twig' as sb %}
|
|
# {{ sb.block(enemy) }}
|
|
#}
|
|
|
|
{% macro mod(n) %}{{ n >= 0 ? '+' ~ n : n }}{% endmacro %}
|
|
|
|
{% macro _stat(label, score, m) %}
|
|
<div class="col text-center">
|
|
<div class="text-uppercase small text-body-secondary">{{ label }}</div>
|
|
<div class="fw-semibold">{{ score }} ({{ m }})</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro _line(label, value) %}
|
|
{% if value %}
|
|
<div><span class="fw-semibold">{{ label }}.</span> {{ value }}</div>
|
|
{% endif %}
|
|
{% endmacro %}
|
|
|
|
{% macro _section(title, body) %}
|
|
{% if body %}
|
|
<h3 class="h6 border-bottom pb-1 mt-3">{{ title }}</h3>
|
|
<div>{{ body|nl2br }}</div>
|
|
{% endif %}
|
|
{% endmacro %}
|
|
|
|
{% macro block(e) %}
|
|
{% import _self as sb %}
|
|
{{ sb._line('Armor Class', e.armorClass ~ (e.armorClassNote ? ' (' ~ e.armorClassNote ~ ')' : '')) }}
|
|
{{ sb._line('Hit Points', e.hitPoints ~ (e.hitDice ? ' (' ~ e.hitDice ~ ')' : '')) }}
|
|
{{ sb._line('Speed', e.speed) }}
|
|
|
|
<div class="row row-cols-6 my-3 g-1">
|
|
{{ sb._stat('Str', e.strength, sb.mod(e.abilityModifier(e.strength))) }}
|
|
{{ sb._stat('Dex', e.dexterity, sb.mod(e.abilityModifier(e.dexterity))) }}
|
|
{{ sb._stat('Con', e.constitution, sb.mod(e.abilityModifier(e.constitution))) }}
|
|
{{ sb._stat('Int', e.intelligence, sb.mod(e.abilityModifier(e.intelligence))) }}
|
|
{{ sb._stat('Wis', e.wisdom, sb.mod(e.abilityModifier(e.wisdom))) }}
|
|
{{ sb._stat('Cha', e.charisma, sb.mod(e.abilityModifier(e.charisma))) }}
|
|
</div>
|
|
|
|
{{ sb._line('Saving Throws', e.savingThrows) }}
|
|
{{ sb._line('Skills', e.skills) }}
|
|
{{ sb._line('Damage Vulnerabilities', e.damageVulnerabilities) }}
|
|
{{ sb._line('Damage Resistances', e.damageResistances) }}
|
|
{{ sb._line('Damage Immunities', e.damageImmunities) }}
|
|
{{ sb._line('Condition Immunities', e.conditionImmunities) }}
|
|
{{ sb._line('Senses', e.senses) }}
|
|
{{ sb._line('Languages', e.languages ?? '—') }}
|
|
{{ sb._line('Challenge', e.challengeRating ? e.challengeRating ~ (e.experiencePoints ? ' (' ~ e.experiencePoints ~ ' XP)' : '') : null) }}
|
|
{{ sb._line('Proficiency Bonus', e.proficiencyBonus ? sb.mod(e.proficiencyBonus) : null) }}
|
|
|
|
{{ sb._section('Traits', e.traits) }}
|
|
{{ sb._section('Actions', e.actions) }}
|
|
{{ sb._section('Bonus Actions', e.bonusActions) }}
|
|
{{ sb._section('Reactions', e.reactions) }}
|
|
{{ sb._section('Legendary Actions', e.legendaryActions) }}
|
|
{{ sb._section('Description', e.description) }}
|
|
|
|
{% if e.source %}
|
|
<p class="text-body-secondary small mt-3 mb-0">Source: {{ e.source }}</p>
|
|
{% endif %}
|
|
{% endmacro %}
|