Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { abilityAtLevel, abilityById } from "./abilityCatalog";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
import { HOTO_ENTITIES } from "./generated/hotoPopulation.generated";
|
||||
import type { EnemyAttackDefinition } from "./enemyAttacks";
|
||||
|
||||
const LEVEL = 80;
|
||||
const SOURCE_ABILITY_ID = "rom-mage-primary-53";
|
||||
const SOURCE_SKILL_ID = 491_168;
|
||||
const SOURCE_DAMAGE_COMPONENT_ID = 620_205;
|
||||
const SOURCE_DOT_COMPONENT_ID = 501_550;
|
||||
const NATIVE_NPC_ID = 102_041;
|
||||
|
||||
function setupMage() {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock();
|
||||
usePartyStore.setState({ members: [] });
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-mage",
|
||||
secondaryClassId: "rom-warlock",
|
||||
raceId: "rom-human",
|
||||
role: "damage",
|
||||
level: LEVEL,
|
||||
statRuleset: "rom",
|
||||
});
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
}
|
||||
|
||||
function registerTarget(id: string) {
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: "Native hit-gate target",
|
||||
runewakerNpcId: NATIVE_NPC_ID,
|
||||
level: 85,
|
||||
maxHealth: 2_000_000,
|
||||
xpReward: 0,
|
||||
attacks: [],
|
||||
});
|
||||
setMobPosition(id, [2, 0, 0]);
|
||||
}
|
||||
|
||||
function resolveImportedEnemyGate(attempt: number) {
|
||||
setupMage();
|
||||
const mobId = `native-shared-hit-enemy-${attempt}`;
|
||||
const sourceEntity = HOTO_ENTITIES["rw-hoto-102041"];
|
||||
const sourceAttacks: readonly EnemyAttackDefinition[] = sourceEntity.combat?.attacks ?? [];
|
||||
const sourceAttack = sourceAttacks.find((attack) => attack.spellId === 493_231);
|
||||
if (!sourceAttack) throw new Error("Missing imported Big Ripper source attack.");
|
||||
useCombatStore.getState().registerMob(mobId, {
|
||||
name: "Imported Teeth of the Reef",
|
||||
runewakerNpcId: NATIVE_NPC_ID,
|
||||
level: sourceEntity.combat?.level ?? 56,
|
||||
maxHealth: 1_000_000,
|
||||
xpReward: 0,
|
||||
attacks: [{ ...sourceAttack, initialCooldownMs: [0, 0] }],
|
||||
});
|
||||
setMobPosition(mobId, [2, 0, 0]);
|
||||
useCombatStore.getState().selectMob(mobId);
|
||||
const now = 1_000 + attempt * 173;
|
||||
useCombatStore.getState().damageMob(mobId, 1, now,
|
||||
{ actorId: PLAYER_AGGRO_ID, role: "damage" });
|
||||
useCombatStore.setState((state) => ({
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[mobId]: {
|
||||
...state.mobs[mobId]!,
|
||||
engaged: true,
|
||||
targetActorId: PLAYER_AGGRO_ID,
|
||||
abilityReadyAt: { [sourceAttack.id]: now },
|
||||
},
|
||||
},
|
||||
}));
|
||||
useCombatStore.getState().advanceMobCombat(now, [0, 0, 0]);
|
||||
const state = useCombatStore.getState();
|
||||
const result = state.lastCombatResult;
|
||||
const dot = state.enemyPeriodicEffects.find((entry) => (
|
||||
entry.sourceMob.id === mobId && entry.attack.spellId === sourceAttack.spellId
|
||||
));
|
||||
return { result, dot, sourceAttack };
|
||||
}
|
||||
|
||||
describe("native Runewaker shared hit gates", () => {
|
||||
it("uses one target hit gate for a direct packet and its DotMagic in one cast", () => {
|
||||
const catalog = abilityById(SOURCE_ABILITY_ID);
|
||||
if (!catalog) throw new Error("Missing active-five Electric Bolt source ability.");
|
||||
const ability = abilityAtLevel(catalog, LEVEL);
|
||||
expect(ability.effects).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "damage",
|
||||
romNativeSource: { skillId: SOURCE_SKILL_ID, componentId: SOURCE_DAMAGE_COMPONENT_ID, kind: "damage" },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dot",
|
||||
romNativeSource: { skillId: SOURCE_SKILL_ID, componentId: SOURCE_DOT_COMPONENT_ID, kind: "dot" },
|
||||
}),
|
||||
]));
|
||||
|
||||
const gateResults: Array<{ directGatePassed: boolean; dotApplied: boolean }> = [];
|
||||
for (let attempt = 0; attempt < 32; attempt += 1) {
|
||||
setupMage();
|
||||
const targetId = `native-shared-hit-target-${attempt}`;
|
||||
registerTarget(targetId);
|
||||
const now = 1_000 + attempt * 137;
|
||||
const cast = useCombatStore.getState().castAbility(ability.id, {
|
||||
targetActorId: targetId,
|
||||
origin: [0, 0, 0],
|
||||
now,
|
||||
});
|
||||
expect(cast.ok, JSON.stringify(cast)).toBe(true);
|
||||
const activeCast = useCombatStore.getState().activeCast;
|
||||
if (activeCast) useCombatStore.getState().castAbility(ability.id, {
|
||||
targetActorId: targetId,
|
||||
origin: [0, 0, 0],
|
||||
now: activeCast.completesAt,
|
||||
completingCast: activeCast,
|
||||
});
|
||||
const state = useCombatStore.getState();
|
||||
const direct = state.lastCombatResult;
|
||||
expect(direct).toBeDefined();
|
||||
const dot = state.effects.find((effect) => effect.kind === "dot"
|
||||
&& effect.targetId === targetId && effect.sourceAbilityId === ability.id);
|
||||
if (dot) expect(dot.nativePeriodicDamageSnapshot).toMatchObject({
|
||||
sourceSkillId: SOURCE_SKILL_ID,
|
||||
componentId: SOURCE_DOT_COMPONENT_ID,
|
||||
});
|
||||
gateResults.push({ directGatePassed: !direct!.missed && !direct!.dodged, dotApplied: Boolean(dot) });
|
||||
}
|
||||
|
||||
// This source has a sub-100% native magical hit chance against the higher-level target,
|
||||
// so both passed and failed gates occur in the fixed seed sweep.
|
||||
expect(gateResults.some((result) => result.directGatePassed)).toBe(true);
|
||||
expect(gateResults.some((result) => !result.directGatePassed)).toBe(true);
|
||||
expect(gateResults.every((result) => result.directGatePassed === result.dotApplied)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses one target hit gate for an imported NPC direct packet and its DotMagic", () => {
|
||||
const gateResults: Array<{ directGatePassed: boolean; dotApplied: boolean }> = [];
|
||||
for (let attempt = 0; attempt < 32; attempt += 1) {
|
||||
const { result, dot, sourceAttack } = resolveImportedEnemyGate(attempt);
|
||||
expect(sourceAttack).toMatchObject({ spellId: 493_231, runewakerFormula: { componentId: 503_925 } });
|
||||
expect(result).toBeDefined();
|
||||
expect(dot?.nativeSnapshot).toMatchObject({
|
||||
sourceSkillId: 493_231,
|
||||
componentId: 503_485,
|
||||
});
|
||||
gateResults.push({
|
||||
directGatePassed: !result!.missed && !result!.dodged,
|
||||
dotApplied: Boolean(dot),
|
||||
});
|
||||
}
|
||||
|
||||
// MagicCollect uses the deterministic 100% hit function for this spell. A
|
||||
// generic legacy miss reroll on its unsupported direct CalcType would break
|
||||
// parity with the native DotMagic application gate.
|
||||
expect(gateResults.every((result) => result.directGatePassed)).toBe(true);
|
||||
expect(gateResults.every((result) => result.dotApplied)).toBe(true);
|
||||
expect(gateResults.every((result) => result.directGatePassed === result.dotApplied)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user