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:
Frank
2026-09-06 19:16:12 +02:00
co-authored by Claude Sonnet 5
parent e417ff9655
commit 9854e6c034
9 changed files with 461 additions and 2 deletions
@@ -0,0 +1,41 @@
import { Controller } from '@hotwired/stimulus';
/*
* Collects the ticked rows (+ their quantity) from a DataTable and POSTs them
* to the combat tracker.
*
* <div data-controller="tracker-picker"
* data-tracker-picker-url-value="/tracker/add"
* data-tracker-picker-token-value="...">
* <button data-action="tracker-picker#submit">Add to tracker</button>
* </div>
*/
export default class extends Controller {
static values = { url: String, token: String };
submit() {
const picks = [...document.querySelectorAll('.track-check:checked')].map((cb) => {
const qtyEl = cb.closest('.track-pick')?.querySelector('.track-qty');
const qty = Math.max(1, Math.min(50, parseInt(qtyEl?.value ?? '1', 10) || 1));
return { id: cb.value, qty };
});
if (picks.length === 0) {
window.alert('Tick at least one enemy first.');
return;
}
const form = document.createElement('form');
form.method = 'post';
form.action = this.urlValue;
form.insertAdjacentHTML('beforeend',
`<input type="hidden" name="_token" value="${this.tokenValue}">`);
picks.forEach((p, i) => {
form.insertAdjacentHTML('beforeend',
`<input type="hidden" name="picks[${i}][id]" value="${p.id}">` +
`<input type="hidden" name="picks[${i}][qty]" value="${p.qty}">`);
});
document.body.appendChild(form);
form.requestSubmit();
}
}