Phase 3: combat depth

- Condition durations + auto-expiry: timed conditions tick down on the affected
  combatant's turn and drop at 0 (engine tickConditions; UI rounds field + (Nr) chip)
- Initiative: Roll-all (d20 + per-combatant bonus, re-sort anchored) + group/quantity add
- Combat log: per-round event feed (turns, round changes, damage/heal, expiries,
  removals) + in-memory Undo of the last change
- Encounter difficulty budget: 5e XP thresholds + PF2e level budget (pure lib +
  tests); live difficulty/XP-award readout from monster combatants vs the party
- Combatants carry initBonus + cr/level; compendium add-to-combat passes them

18 new unit tests (durations, applyInitiatives, budget), new combat-depth e2e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 01:23:13 +02:00
parent 1c87b0e3aa
commit 3e5bdc06e2
13 changed files with 472 additions and 35 deletions
+14
View File
@@ -13,14 +13,26 @@ export const combatantSchema = z.object({
monsterRef: z.string().optional(),
initiative: int,
/** modifier added when (re)rolling initiative */
initBonus: int.default(0),
ac: int,
hp: hpSchema,
conditions: z.array(conditionSchema).default([]),
notes: z.string().max(2000).default(''),
/** 5e challenge rating (for difficulty budget), if known */
cr: z.number().finite().nonnegative().optional(),
/** PF2e creature level (for difficulty budget), if known */
level: int.optional(),
});
export type Combatant = z.infer<typeof combatantSchema>;
export const logEntrySchema = z.object({
round: int.nonnegative(),
text: z.string(),
});
export type LogEntry = z.infer<typeof logEntrySchema>;
export const encounterStatusSchema = z.enum(['planning', 'active', 'ended']);
export const encounterSchema = z.object({
@@ -32,6 +44,8 @@ export const encounterSchema = z.object({
/** index into combatants of whose turn it is */
turnIndex: int.nonnegative().default(0),
combatants: z.array(combatantSchema).default([]),
/** newest-last event feed for the fight */
log: z.array(logEntrySchema).default([]),
createdAt: z.string(),
updatedAt: z.string(),
});