import { beforeEach, describe, expect, it } from "vitest"; import { EMPTY_ITEM_STATS, type ItemStats } from "./itemStats"; import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry"; import { useCombatStore } from "./combatStore"; import type { EnemyAttackDefinition } from "./enemyAttacks"; function stableUnit(seed: string): number { let hash = 2166136261; for (let index = 0; index < seed.length; index += 1) { hash ^= seed.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0) / 0x1_0000_0000; } function timeForRoll(seed: (now: number) => string, minimum: number, maximum: number): number { for (let now = 1_000; now < 200_000; now += 1) { const roll = stableUnit(`${seed(now)}:outcome`); if (roll >= minimum && roll < maximum) return now; } throw new Error(`No deterministic roll in [${minimum}, ${maximum})`); } function registerTarget( id = "target", options: { level?: number; armor?: number; fireResistance?: number; fireImmune?: boolean } = {}, ): void { useCombatStore.getState().registerMob(id, { name: id, level: options.level, maxHealth: 10_000, xpReward: 0, resistances: { fire: options.fireResistance ?? 0 }, schoolImmunities: options.fireImmune ? ["fire"] : [], }); if (options.armor !== undefined) { useCombatStore.setState((state) => ({ mobs: { ...state.mobs, [id]: { ...state.mobs[id], armor: options.armor! } }, })); } setMobPosition(id, [2, 0, 0]); useCombatStore.getState().selectMob(id); } function setGear(patch: Partial): void { useCombatStore.setState({ gearStats: { ...EMPTY_ITEM_STATS, ...patch } }); } const ENEMY_MELEE: EnemyAttackDefinition = { id: "test-swing", name: "Test Swing", delivery: "melee", target: "primary", school: "physical", animation: "attack", castTimeMs: 0, range: 5, cooldownMs: 10_000, initialCooldownMs: [0, 0], repeatCooldownMs: [10_000, 10_000], damageMultiplier: 1, }; describe("combat store discrete combat-result integration", () => { beforeEach(() => { clearMobRuntimeRegistry(); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20 }); useCombatStore.getState().setPlayerPosition([0, 0, 0]); }); it("produces real spell misses and critical strikes", () => { registerTarget("high-level", { level: 255 }); expect(useCombatStore.getState().castAbility("mage-fire-blast", undefined, 1_000).ok).toBe(true); expect(useCombatStore.getState().lastCombatResult).toMatchObject({ outcome: "miss", finalDamage: 0, missed: true, }); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20 }); setGear({ criticalStrikeRating: 1_000_000, hitRating: 1_000_000 }); registerTarget("crit-target", { level: 20 }); expect(useCombatStore.getState().castAbility("mage-fire-blast", undefined, 2_000).ok).toBe(true); const critical = useCombatStore.getState().lastCombatResult!; expect(critical.outcome).toBe("crit"); expect(critical.damageBeforeMitigation).toBe(critical.rawDamage * 2); }); it.each([ ["dodge", 0.05, 0.10], ["parry", 0.10, 0.15], ] as const)("resolves player melee attacks as %s from the NPC attack table", (outcome, low, high) => { useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 20 }); registerTarget(outcome, { level: 20 }); const now = timeForRoll( (candidate) => `__player__:rogue-sinister-strike:${outcome}:${candidate}:direct`, low, high, ); expect(useCombatStore.getState().castAbility("rogue-sinister-strike", undefined, now).ok).toBe(true); expect(useCombatStore.getState().lastCombatResult).toMatchObject({ outcome, finalDamage: 0 }); }); it("uses mob armor and armor penetration without expected-value offense multipliers", () => { useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 20 }); registerTarget("armored", { level: 20, armor: 2_000 }); const now = timeForRoll( (candidate) => `__player__:rogue-sinister-strike:armored:${candidate}:direct`, 0.25, 0.95, ); useCombatStore.getState().castAbility("rogue-sinister-strike", undefined, now); const withoutPenetration = useCombatStore.getState().lastCombatResult!; expect(withoutPenetration.armorMitigatedAmount).toBeGreaterThan(0); useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 20 }); setGear({ armorPenetrationRating: 1_000_000, criticalStrikeRating: 1_000_000, hitRating: 1_000_000, expertiseRating: 1_000_000, }); registerTarget("armored", { level: 20, armor: 2_000 }); useCombatStore.getState().castAbility("rogue-sinister-strike", undefined, now); const withRatings = useCombatStore.getState().lastCombatResult!; expect(withRatings.rawDamage).toBe(withoutPenetration.rawDamage); expect(withRatings.effectiveArmor).toBe(0); expect(withRatings.finalDamage).toBeGreaterThan(withoutPenetration.finalDamage); }); it("resolves partial resistance and school immunity through the live store", () => { setGear({ hitRating: 1_000_000 }); registerTarget("resistant", { level: 20, fireResistance: 500 }); useCombatStore.getState().castAbility("mage-fire-blast", undefined, 3_000); expect(useCombatStore.getState().lastCombatResult).toMatchObject({ landed: true, partiallyResisted: true, }); expect(useCombatStore.getState().lastCombatResult!.resistedAmount).toBeGreaterThan(0); useCombatStore.getState().initializeCharacter({ classId: "mage", level: 20 }); registerTarget("immune", { level: 20, fireImmune: true }); useCombatStore.getState().castAbility("mage-fire-blast", undefined, 4_000); expect(useCombatStore.getState().lastCombatResult).toMatchObject({ outcome: "resist", fullyResisted: true, finalDamage: 0, }); }); it.each([ ["dodge", { dodgeRating: 1_000_000 }], ["parry", { parryRating: 1_000_000 }], ["block", { blockRating: 1_000_000, blockValue: 25 }], ] as const)("resolves incoming %s once without party-style double mitigation", (outcome, gear) => { useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 80 }); setGear(gear); useCombatStore.getState().registerMob(`enemy-${outcome}`, { name: `enemy-${outcome}`, level: 80, maxHealth: 1_000, xpReward: 0, attacks: [ENEMY_MELEE], }); setMobPosition(`enemy-${outcome}`, [2, 0, 0]); const now = timeForRoll( (candidate) => `enemy-${outcome}:test-swing:__player__:0:${candidate}`, 0.25, 0.75, ); useCombatStore.getState().damageMob(`enemy-${outcome}`, 1, now - 1); const healthBefore = useCombatStore.getState().health; const applied = useCombatStore.getState().advanceMobCombat(now); const result = useCombatStore.getState().lastCombatResult!; expect(result.outcome).toBe(outcome); expect(healthBefore - useCombatStore.getState().health).toBe(applied); expect(applied).toBe(result.finalDamage); }); });