feat: condition durations — 'Hexed (3)' ticks down each turn, drops at 0
Numbered conditions decrement when the combatant's turn starts and expire at zero; plain conditions still toggle manually. The custom condition input hints the syntax. Roadmap 1.8 — Phase 1 complete.
This commit is contained in:
@@ -157,10 +157,34 @@ export function InitiativeTracker() {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ponytail: conditions written as "Name (N)" auto-expire: the number
|
||||||
|
// decrements each time that combatant's turn starts, and the condition
|
||||||
|
// drops at zero. Plain conditions run until toggled off manually.
|
||||||
|
function tickDurations(id: string) {
|
||||||
|
setCombatants((prev) =>
|
||||||
|
prev.map((c) => {
|
||||||
|
if (c.id !== id) return c;
|
||||||
|
const next: string[] = [];
|
||||||
|
for (const cn of c.conditions) {
|
||||||
|
const m = cn.match(/^(.*?)\s*\((\d+)\)$/);
|
||||||
|
if (!m) {
|
||||||
|
next.push(cn);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const n = parseInt(m[2], 10);
|
||||||
|
if (n > 1) next.push(`${m[1]} (${n - 1})`);
|
||||||
|
// n === 1 → expired, dropped
|
||||||
|
}
|
||||||
|
return { ...c, conditions: next };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const nextTurn = useCallback(() => {
|
const nextTurn = useCallback(() => {
|
||||||
if (combatants.length === 0) return;
|
if (combatants.length === 0) return;
|
||||||
if (!activeId) {
|
if (!activeId) {
|
||||||
setActiveId(combatants[0].id);
|
setActiveId(combatants[0].id);
|
||||||
|
tickDurations(combatants[0].id);
|
||||||
bus.emit(Events.LogEntry, { text: `⚔ Combat starts — ${combatants[0].name}`, tag: "#combat" });
|
bus.emit(Events.LogEntry, { text: `⚔ Combat starts — ${combatants[0].name}`, tag: "#combat" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -168,9 +192,11 @@ export function InitiativeTracker() {
|
|||||||
if (idx === combatants.length - 1) {
|
if (idx === combatants.length - 1) {
|
||||||
setRound((r) => r + 1);
|
setRound((r) => r + 1);
|
||||||
setActiveId(combatants[0].id);
|
setActiveId(combatants[0].id);
|
||||||
|
tickDurations(combatants[0].id);
|
||||||
bus.emit(Events.LogEntry, { text: `⚔ Round ${round + 1} — ${combatants[0].name}'s turn`, tag: "#combat" });
|
bus.emit(Events.LogEntry, { text: `⚔ Round ${round + 1} — ${combatants[0].name}'s turn`, tag: "#combat" });
|
||||||
} else {
|
} else {
|
||||||
setActiveId(combatants[idx + 1].id);
|
setActiveId(combatants[idx + 1].id);
|
||||||
|
tickDurations(combatants[idx + 1].id);
|
||||||
bus.emit(Events.LogEntry, { text: `⚔ ${combatants[idx + 1].name}'s turn`, tag: "#combat" });
|
bus.emit(Events.LogEntry, { text: `⚔ ${combatants[idx + 1].name}'s turn`, tag: "#combat" });
|
||||||
}
|
}
|
||||||
setTimerLeft(TURN_SECONDS); // ponytail: reset the per-turn timer.
|
setTimerLeft(TURN_SECONDS); // ponytail: reset the per-turn timer.
|
||||||
@@ -513,7 +539,8 @@ export function InitiativeTracker() {
|
|||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<input
|
<input
|
||||||
className="flex-1 min-w-0 rounded bg-[var(--color-bg-deep)] border border-[var(--color-gold-bright)] px-1.5 py-0.5 text-[10px] text-[var(--color-text-primary)] focus:outline-none"
|
className="flex-1 min-w-0 rounded bg-[var(--color-bg-deep)] border border-[var(--color-gold-bright)] px-1.5 py-0.5 text-[10px] text-[var(--color-text-primary)] focus:outline-none"
|
||||||
placeholder="Custom condition…"
|
placeholder="Custom condition… e.g. Hexed (3)"
|
||||||
|
title="A number in (rounds) makes the condition auto-expire: it ticks down each turn and drops at zero."
|
||||||
value={condInput[c.id] ?? ""}
|
value={condInput[c.id] ?? ""}
|
||||||
onChange={(e) => setCondInput((p) => ({ ...p, [c.id]: e.target.value }))}
|
onChange={(e) => setCondInput((p) => ({ ...p, [c.id]: e.target.value }))}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user