Files
healer-man/src/game/combatResultsIntegration.test.ts
T
2026-08-14 15:56:39 -04:00

180 lines
7.0 KiB
TypeScript

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<ItemStats>): 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);
});
});