Implement the 018-combatant-concentration feature that adds a per-combatant concentration toggle with Brain icon, purple border accent, and damage pulse animation in the encounter tracker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-06 14:34:28 +01:00
parent febe892e15
commit e59fd83292
19 changed files with 779 additions and 7 deletions

View File

@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { toggleConcentration } from "../toggle-concentration.js";
import type { Combatant, Encounter } from "../types.js";
import { combatantId, isDomainError } from "../types.js";
function makeCombatant(name: string, isConcentrating?: boolean): Combatant {
return isConcentrating
? { id: combatantId(name), name, isConcentrating }
: { id: combatantId(name), name };
}
function enc(combatants: Combatant[]): Encounter {
return { combatants, activeIndex: 0, roundNumber: 1 };
}
function success(encounter: Encounter, id: string) {
const result = toggleConcentration(encounter, combatantId(id));
if (isDomainError(result)) {
throw new Error(`Expected success, got error: ${result.message}`);
}
return result;
}
describe("toggleConcentration", () => {
it("toggles concentration on when falsy", () => {
const e = enc([makeCombatant("A")]);
const { encounter, events } = success(e, "A");
expect(encounter.combatants[0].isConcentrating).toBe(true);
expect(events).toEqual([
{ type: "ConcentrationStarted", combatantId: combatantId("A") },
]);
});
it("toggles concentration off when true", () => {
const e = enc([makeCombatant("A", true)]);
const { encounter, events } = success(e, "A");
expect(encounter.combatants[0].isConcentrating).toBeUndefined();
expect(events).toEqual([
{ type: "ConcentrationEnded", combatantId: combatantId("A") },
]);
});
it("returns error for nonexistent combatant", () => {
const e = enc([makeCombatant("A")]);
const result = toggleConcentration(e, combatantId("missing"));
expect(isDomainError(result)).toBe(true);
if (isDomainError(result)) {
expect(result.code).toBe("combatant-not-found");
}
});
it("does not mutate input encounter", () => {
const e = enc([makeCombatant("A")]);
const original = JSON.parse(JSON.stringify(e));
toggleConcentration(e, combatantId("A"));
expect(e).toEqual(original);
});
it("does not affect other combatants", () => {
const e = enc([makeCombatant("A"), makeCombatant("B", true)]);
const { encounter } = success(e, "A");
expect(encounter.combatants[0].isConcentrating).toBe(true);
expect(encounter.combatants[1].isConcentrating).toBe(true);
});
});