Add combat HP tracker
- 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>
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
{ route: 'app_characters', label: 'Characters' },
|
||||
{ route: 'app_npcs', label: "NPC's" },
|
||||
{ route: 'app_enemies', label: 'Enemies' },
|
||||
{ route: 'app_tracker', label: 'Tracker' },
|
||||
{ route: 'app_art', label: 'Art' },
|
||||
{ route: 'app_storyline', label: 'Storyline' },
|
||||
] %}
|
||||
|
||||
@@ -3,9 +3,17 @@
|
||||
{% block title %}Enemies · dmtools{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 gap-2 flex-wrap">
|
||||
<h1 class="h3 mb-0">Enemies</h1>
|
||||
<a href="{{ path('app_enemies_new') }}" class="btn btn-primary btn-sm">Add enemy</a>
|
||||
<div class="d-flex gap-2"
|
||||
data-controller="tracker-picker"
|
||||
data-tracker-picker-url-value="{{ path('app_tracker_add') }}"
|
||||
data-tracker-picker-token-value="{{ csrf_token('track-add') }}">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-action="tracker-picker#submit">
|
||||
Add to tracker
|
||||
</button>
|
||||
<a href="{{ path('app_enemies_new') }}" class="btn btn-primary btn-sm">Add enemy</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
@@ -22,6 +30,7 @@
|
||||
<th data-data="challengeRating">CR</th>
|
||||
<th data-data="armorClass">AC</th>
|
||||
<th data-data="gameNotes" data-format="truncate" data-orderable="false">Game notes</th>
|
||||
<th data-data="id" data-format="pick" data-orderable="false">Track</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Tracker · dmtools{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 gap-2 flex-wrap">
|
||||
<h1 class="h3 mb-0">Combat tracker
|
||||
{% if combatants %}<span class="text-body-secondary fs-6">· {{ combatants|length }}</span>{% endif %}
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ path('app_enemies') }}" class="btn btn-outline-secondary btn-sm">Add from Enemies</a>
|
||||
{% if combatants %}
|
||||
<form method="post" action="{{ path('app_tracker_clear') }}"
|
||||
onsubmit="return confirm('Clear the whole tracker?')">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('tracker-op') }}">
|
||||
<button class="btn btn-outline-danger btn-sm">Clear all</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not combatants %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center text-body-secondary py-5">
|
||||
No combatants yet. Go to <strong>Enemies</strong>, tick the ones you want (set a count for
|
||||
multiples), and choose <strong>Add to tracker</strong>.
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for c in combatants %}
|
||||
{% set pct = c.hpPercent %}
|
||||
{% set bar = c.isDown ? 'bg-secondary' : (pct <= 25 ? 'bg-danger' : (pct <= 50 ? 'bg-warning' : 'bg-success')) %}
|
||||
<div class="card shadow-sm {{ c.isDown ? 'opacity-75' : '' }}">
|
||||
<div class="card-body py-2">
|
||||
<div class="row align-items-center g-2">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="fw-semibold {{ c.isDown ? 'text-decoration-line-through' : '' }}">
|
||||
{{ c.label }}
|
||||
{% if c.isDown %}<span class="badge text-bg-secondary ms-1">Down</span>{% endif %}
|
||||
</div>
|
||||
{% if c.enemy %}
|
||||
<a href="{{ path('app_enemies_show', {id: c.enemy.id}) }}"
|
||||
class="small text-decoration-none text-body-secondary">stat block</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="font-monospace" style="min-width: 5rem;">
|
||||
<span class="fs-5 fw-semibold">{{ c.currentHp }}</span> / {{ c.maxHp }}
|
||||
</span>
|
||||
<div class="progress flex-grow-1" role="progressbar" style="height: 10px;">
|
||||
<div class="progress-bar {{ bar }}" style="width: {{ pct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="d-flex align-items-center gap-1 justify-content-md-end flex-wrap">
|
||||
<form method="post" action="{{ path('app_tracker_hp', {id: c.id}) }}"
|
||||
class="d-flex align-items-center gap-1">
|
||||
<input type="number" name="amount" min="0" inputmode="numeric"
|
||||
class="form-control form-control-sm" style="width: 4.5rem;"
|
||||
placeholder="HP" aria-label="Amount">
|
||||
<button name="mode" value="damage" class="btn btn-sm btn-outline-danger">Damage</button>
|
||||
<button name="mode" value="heal" class="btn btn-sm btn-outline-success">Heal</button>
|
||||
</form>
|
||||
<form method="post" action="{{ path('app_tracker_remove', {id: c.id}) }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('tracker-op') }}">
|
||||
<button class="btn btn-sm btn-outline-secondary" title="Remove" aria-label="Remove">×</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user