Files
dmtools/templates/enemy/_statblock.html.twig
T
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

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 %}