forked from phenom/healer-man
627 lines
22 KiB
TypeScript
627 lines
22 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
MANASTORM_ENCOUNTERS,
|
|
actorEnteredManastormPortal,
|
|
drawManastormEncounter,
|
|
drawManastormStage,
|
|
isManastormSpecialLevel,
|
|
manastormActiveRuntimeIds,
|
|
manastormCacheChance,
|
|
manastormCheckpointForLevel,
|
|
manastormModeForCharacterLevel,
|
|
manastormExperienceReward,
|
|
manastormAffixes,
|
|
manastormScaling,
|
|
setManastormCatalog,
|
|
shuffleManastormEncounterIds,
|
|
type ManastormCatalogLike,
|
|
type ManastormEncounterDefinition,
|
|
} from "./manastorm";
|
|
import { useManastormStore } from "./manastormStore";
|
|
|
|
function stage(
|
|
id: string,
|
|
options: Partial<ManastormEncounterDefinition> = {},
|
|
): ManastormEncounterDefinition {
|
|
const base = MANASTORM_ENCOUNTERS[0];
|
|
return {
|
|
...base,
|
|
id,
|
|
bossRuntimeId: `${id}-boss`,
|
|
bossEntityId: `${id}-entity`,
|
|
bossName: id,
|
|
trashRuntimeIds: [`${id}-trash-a`, `${id}-trash-b`],
|
|
...options,
|
|
};
|
|
}
|
|
|
|
describe("Manastorm rules", () => {
|
|
beforeEach(() => {
|
|
setManastormCatalog(null);
|
|
useManastormStore.getState().resetRun();
|
|
});
|
|
|
|
it("uses level one as baseline and caps the bonus item chance", () => {
|
|
expect(manastormScaling(1)).toEqual({
|
|
statMultiplier: 1,
|
|
bonusLootChance: 0,
|
|
experienceMultiplier: 1,
|
|
});
|
|
expect(manastormScaling(2)).toEqual({
|
|
statMultiplier: 1.03,
|
|
bonusLootChance: 0.03,
|
|
experienceMultiplier: 1.03,
|
|
});
|
|
expect(manastormScaling(34)).toEqual({
|
|
statMultiplier: 1.99,
|
|
bonusLootChance: 0.99,
|
|
experienceMultiplier: 1.99,
|
|
});
|
|
expect(manastormScaling(35)).toEqual({
|
|
statMultiplier: 2.02,
|
|
bonusLootChance: 1,
|
|
experienceMultiplier: 2.02,
|
|
});
|
|
expect(manastormScaling(900).bonusLootChance).toBe(1);
|
|
});
|
|
|
|
it("applies imported experience, party, and map reward multipliers", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
modeId: 2,
|
|
modifiers: [{
|
|
id: "xp-fixture",
|
|
modeId: 2,
|
|
level: 5,
|
|
tuning: [0, 0, 0, 0, 0, 0, 0, 0, 1.5, 1.75, 2],
|
|
}],
|
|
groupModifiers: [{
|
|
playerCount: 3,
|
|
modeId: 2,
|
|
multipliers: [1.25, 1.4],
|
|
}],
|
|
};
|
|
expect(manastormExperienceReward(100, 5, 3, 1.5, catalog, 2)).toBe(420);
|
|
});
|
|
|
|
it("keeps durably unlocked affixes eligible when replaying a lower stage", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "durable-affix-fixture",
|
|
stages: [stage("durable-affix-stage")],
|
|
affixes: [
|
|
{
|
|
id: "early-affix",
|
|
name: "Early",
|
|
description: "Unlocked early.",
|
|
unlockLevel: 2,
|
|
},
|
|
{
|
|
id: "later-affix",
|
|
name: "Later",
|
|
description: "Unlocked later.",
|
|
unlockLevel: 5,
|
|
},
|
|
],
|
|
};
|
|
setManastormCatalog(catalog);
|
|
useManastormStore.getState().resetRun();
|
|
|
|
useManastormStore.getState().startRun({
|
|
startingLevel: 1,
|
|
unlockedThroughLevel: 5,
|
|
random: () => 0,
|
|
});
|
|
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
level: 1,
|
|
unlockedThroughLevel: 5,
|
|
activeAffixes: [{ id: "early-affix" }],
|
|
});
|
|
});
|
|
|
|
it("draws every legacy boss once and prevents a repeat across bag boundaries", () => {
|
|
let bag: readonly string[] = [];
|
|
let previous: string | null = null;
|
|
const drawn: string[] = [];
|
|
for (let index = 0; index < MANASTORM_ENCOUNTERS.length; index += 1) {
|
|
const draw = drawManastormEncounter(bag, previous, () => 0.37);
|
|
drawn.push(draw.encounter.id);
|
|
previous = draw.encounter.id;
|
|
bag = draw.remainingBag;
|
|
}
|
|
expect(new Set(drawn).size).toBe(MANASTORM_ENCOUNTERS.length);
|
|
|
|
const nextBag = shuffleManastormEncounterIds(() => 0.37, previous);
|
|
expect(nextBag[0]).not.toBe(previous);
|
|
});
|
|
|
|
it("selects unlocked stages deterministically, honors party gates, and reserves milestone stages", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "selection-fixture",
|
|
stages: [
|
|
stage("alpha", { unlockLevel: 1, minimumPartySize: 1 }),
|
|
stage("beta", { unlockLevel: 6, minimumPartySize: 3 }),
|
|
stage("five", { unlockLevel: 5, stageKind: "milestone", milestoneLevel: 5 }),
|
|
],
|
|
};
|
|
const first = drawManastormStage(1, 1, [], null, () => 0.4, catalog);
|
|
expect(first.encounter.id).toBe("alpha");
|
|
const milestone = drawManastormStage(5, 1, [], first.encounter.id, () => 0.4, catalog);
|
|
expect(milestone.encounter.id).toBe("five");
|
|
const smallGroup = drawManastormStage(6, 2, [], milestone.encounter.id, () => 0.4, catalog);
|
|
expect(smallGroup.encounter.id).toBe("alpha");
|
|
const fullGroup = drawManastormStage(6, 3, [], "alpha", () => 0.4, catalog);
|
|
expect(["beta", "five"]).toContain(fullGroup.encounter.id);
|
|
expect(fullGroup.encounter.id).not.toBe("alpha");
|
|
expect(drawManastormStage(6, 3, [], "alpha", () => 0.4, catalog).encounter.id)
|
|
.toBe(fullGroup.encounter.id);
|
|
});
|
|
|
|
it("does not restore fallback content when a published Ready pool is empty", () => {
|
|
const emptyPublishedCatalog: ManastormCatalogLike = { stages: [], fallbackPolicy: "none" };
|
|
expect(() => drawManastormStage(1, 1, [], null, () => 0, emptyPublishedCatalog))
|
|
.toThrow("no unlocked encounter");
|
|
});
|
|
|
|
it("uses the standard pool at milestone levels when forced milestones are disabled", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
stages: [
|
|
stage("standard", { unlockLevel: 1 }),
|
|
stage("milestone", { unlockLevel: 5, stageKind: "milestone", milestoneLevel: 5 }),
|
|
],
|
|
runtimeOptions: { milestoneEncountersEnabled: false },
|
|
};
|
|
expect(drawManastormStage(5, 1, [], null, () => 0, catalog).encounter.id).toBe("standard");
|
|
});
|
|
|
|
it("disables both explicit and modifier-derived affixes", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
affixes: [{ id: "explicit", name: "Explicit", description: "Fixture", unlockLevel: 1 }],
|
|
modifiers: [{ id: "fallback", modeId: 0, level: 1, rawFlag: 7, tuning: [] }],
|
|
runtimeOptions: { affixesEnabled: false },
|
|
};
|
|
expect(manastormAffixes(1, catalog, 0)).toEqual([]);
|
|
});
|
|
|
|
it("keeps leveling and end-game encounter pools separate by character state", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "split-mode-fixture",
|
|
modeId: 0,
|
|
modeIds: [0, 2],
|
|
stages: [
|
|
stage("leveling", { modeIds: [0] }),
|
|
stage("endgame", { modeIds: [2] }),
|
|
],
|
|
};
|
|
expect(manastormModeForCharacterLevel(59, catalog)).toBe(0);
|
|
expect(manastormModeForCharacterLevel(60, catalog)).toBe(2);
|
|
expect(drawManastormStage(99, 1, [], null, () => 0, catalog, 0).encounter.id)
|
|
.toBe("leveling");
|
|
expect(drawManastormStage(101, 1, [], null, () => 0, catalog, 2).encounter.id)
|
|
.toBe("endgame");
|
|
});
|
|
|
|
it("keeps the selected mode invariant when a run crosses level 100", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "immutable-mode-fixture",
|
|
modeId: 0,
|
|
modeIds: [0, 2],
|
|
maximumLevel: 200,
|
|
stages: [
|
|
stage("leveling", {
|
|
modeIds: [0],
|
|
stageKind: "milestone",
|
|
milestoneLevel: 100,
|
|
}),
|
|
stage("endgame", { modeIds: [2] }),
|
|
],
|
|
};
|
|
setManastormCatalog(catalog);
|
|
useManastormStore.getState().resetRun();
|
|
useManastormStore.getState().startRun({
|
|
startingLevel: 100,
|
|
modeId: 0,
|
|
random: () => 0,
|
|
});
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
useManastormStore.getState().beginTransition();
|
|
useManastormStore.getState().prepareNextEncounter(() => 0);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
level: 101,
|
|
modeId: 0,
|
|
currentEncounter: { id: "leveling", modeIds: [0] },
|
|
});
|
|
});
|
|
|
|
it("draws later every-fifth levels from the unlocked special pool", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "repeating-milestone-fixture",
|
|
stages: [
|
|
stage("standard", { unlockLevel: 1 }),
|
|
stage("five", { unlockLevel: 5, stageKind: "milestone", milestoneLevel: 5 }),
|
|
stage("ten", { unlockLevel: 10, stageKind: "milestone", milestoneLevel: 10 }),
|
|
],
|
|
};
|
|
|
|
const first = drawManastormStage(20, 1, [], null, () => 0, catalog);
|
|
const second = drawManastormStage(25, 1, [], first.encounter.id, () => 0, catalog);
|
|
|
|
expect(["five", "ten"]).toContain(first.encounter.id);
|
|
expect(["five", "ten"]).toContain(second.encounter.id);
|
|
expect(second.encounter.id).not.toBe(first.encounter.id);
|
|
});
|
|
|
|
it("keeps previously introduced content in a fresh run's random pool", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "durable-unlock-fixture",
|
|
stages: [
|
|
stage("initial", { unlockLevel: 1 }),
|
|
stage("later-standard", { unlockLevel: 6 }),
|
|
stage("introduced-milestone", {
|
|
unlockLevel: 5,
|
|
stageKind: "milestone",
|
|
milestoneLevel: 5,
|
|
}),
|
|
],
|
|
};
|
|
expect(drawManastormStage(
|
|
1,
|
|
1,
|
|
["later-standard"],
|
|
null,
|
|
() => 0,
|
|
catalog,
|
|
0,
|
|
7,
|
|
).encounter.id).toBe("later-standard");
|
|
expect(drawManastormStage(
|
|
1,
|
|
1,
|
|
["introduced-milestone"],
|
|
null,
|
|
() => 0,
|
|
catalog,
|
|
0,
|
|
7,
|
|
).encounter.id).toBe("introduced-milestone");
|
|
});
|
|
|
|
it("guards enter, trash, boss, loot, portal, and next as distinct phases", () => {
|
|
const first = useManastormStore.getState().startRun(() => 0.2);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
status: "entering",
|
|
phase: "enter",
|
|
level: 1,
|
|
completed: 0,
|
|
currentEncounter: first,
|
|
});
|
|
|
|
expect(useManastormStore.getState().enterStage()).toBe(true);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
status: "clearing-trash",
|
|
phase: "trash",
|
|
});
|
|
useManastormStore.getState().observeTrash(false);
|
|
expect(useManastormStore.getState().status).toBe("clearing-trash");
|
|
useManastormStore.getState().observeTrash(true);
|
|
expect(useManastormStore.getState()).toMatchObject({ status: "fighting", phase: "boss" });
|
|
|
|
useManastormStore.getState().observeBoss(true, false);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
status: "awaiting-loot",
|
|
phase: "loot",
|
|
pendingReward: { stageId: first.id, level: 1 },
|
|
});
|
|
expect(useManastormStore.getState().beginTransition()).toBe(false);
|
|
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
status: "portal-ready",
|
|
phase: "portal",
|
|
completed: 1,
|
|
pendingReward: null,
|
|
lastReward: { stageId: first.id },
|
|
});
|
|
expect(useManastormStore.getState().currency.bonzoBolts).toBeGreaterThan(0);
|
|
expect(useManastormStore.getState().useResurrectionCharge()).toBe(true);
|
|
expect(useManastormStore.getState().resurrectionCharges).toBe(4);
|
|
expect(useManastormStore.getState().beginTransition()).toBe(true);
|
|
expect(useManastormStore.getState().beginTransition()).toBe(false);
|
|
|
|
const next = useManastormStore.getState().prepareNextEncounter(() => 0.6);
|
|
expect(next?.id).not.toBe(first.id);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
status: "transitioning",
|
|
phase: "next",
|
|
level: 2,
|
|
completed: 1,
|
|
previousEncounterId: first.id,
|
|
resurrectionCharges: 5,
|
|
});
|
|
useManastormStore.getState().finishTransition();
|
|
expect(useManastormStore.getState()).toMatchObject({ status: "entering", phase: "enter" });
|
|
});
|
|
|
|
it("tracks every-fifth milestones, repeating checkpoints, party sizes, and five shared charges", () => {
|
|
expect([5, 10, 15, 20, 25].every(isManastormSpecialLevel)).toBe(true);
|
|
expect(isManastormSpecialLevel(21)).toBe(false);
|
|
expect([1, 5, 6, 10, 11, 15, 16, 21].map(manastormCheckpointForLevel))
|
|
.toEqual([1, 1, 6, 6, 11, 11, 16, 21]);
|
|
|
|
useManastormStore.getState().startRun({ partySize: -4, startingLevel: 20, random: () => 0.1 });
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
partySize: 1,
|
|
level: 20,
|
|
checkpointLevel: 16,
|
|
milestone: true,
|
|
resurrectionCharges: 5,
|
|
});
|
|
for (let charge = 4; charge >= 0; charge -= 1) {
|
|
expect(useManastormStore.getState().useResurrectionCharge()).toBe(true);
|
|
expect(useManastormStore.getState().resurrectionCharges).toBe(charge);
|
|
}
|
|
expect(useManastormStore.getState().useResurrectionCharge()).toBe(false);
|
|
});
|
|
|
|
it("commits the clear and checkpoint atomically with boss loot", () => {
|
|
useManastormStore.getState().startRun({
|
|
partySize: 2,
|
|
startingLevel: 5,
|
|
random: () => 0,
|
|
});
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
const rewarded = useManastormStore.getState();
|
|
expect(rewarded).toMatchObject({
|
|
status: "portal-ready",
|
|
level: 5,
|
|
completed: 5,
|
|
checkpointLevel: 6,
|
|
});
|
|
const currency = rewarded.currency;
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
expect(useManastormStore.getState().currency).toEqual(currency);
|
|
useManastormStore.getState().beginTransition();
|
|
useManastormStore.getState().prepareNextEncounter(() => 0);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
level: 6,
|
|
completed: 5,
|
|
checkpointLevel: 6,
|
|
});
|
|
});
|
|
|
|
it("restarts a failed milestone attempt from its checkpoint with restored charges", () => {
|
|
useManastormStore.getState().startRun({ partySize: 3, startingLevel: 10, random: () => 0.1 });
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
level: 10,
|
|
milestone: true,
|
|
checkpointLevel: 6,
|
|
});
|
|
expect(useManastormStore.getState().useResurrectionCharge()).toBe(true);
|
|
expect(useManastormStore.getState().failRun()).toBe(true);
|
|
|
|
const encounter = useManastormStore.getState().restartFromCheckpoint(() => 0.2);
|
|
|
|
expect(encounter).not.toBeNull();
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
status: "entering",
|
|
phase: "enter",
|
|
level: 6,
|
|
completed: 5,
|
|
checkpointLevel: 6,
|
|
resurrectionCharges: 5,
|
|
});
|
|
});
|
|
|
|
it("guarantees a bonus cache when the bad-luck meter reaches five clears", () => {
|
|
useManastormStore.getState().startRun({ cacheMeter: 4, random: () => 0.2 });
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
expect(useManastormStore.getState()).toMatchObject({ cacheMeter: 0 });
|
|
expect(useManastormStore.getState().rewardCache).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ cacheId: "manastorm-bonus-cache" }),
|
|
]));
|
|
});
|
|
|
|
it("stacks end-game cache chance, increases it with level, and guarantees the fifth roll", () => {
|
|
expect(manastormCacheChance(1, 0)).toBeCloseTo(0.12);
|
|
expect(manastormCacheChance(101, 0)).toBeGreaterThan(manastormCacheChance(1, 0));
|
|
expect(manastormCacheChance(1, 3)).toBeGreaterThan(manastormCacheChance(1, 0));
|
|
expect(manastormCacheChance(1, 4)).toBe(1);
|
|
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "chance-cache-fixture",
|
|
modeId: 2,
|
|
stages: [stage("chance-cache-stage", { rewardId: "chance-cache-reward" })],
|
|
rewards: [{
|
|
id: "chance-cache-reward",
|
|
modeIds: [2],
|
|
currencyId: "bonzoBolts",
|
|
currencyBase: 1,
|
|
cacheId: "endgame-manastorm-cache",
|
|
cachePolicy: "accumulating-chance",
|
|
}],
|
|
};
|
|
setManastormCatalog(catalog);
|
|
useManastormStore.getState().resetRun();
|
|
useManastormStore.getState().startRun({
|
|
modeId: 2,
|
|
cacheMeter: 0,
|
|
random: () => 0.2,
|
|
});
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true, () => 0.99);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
cacheMeter: 1,
|
|
rewardCache: [],
|
|
});
|
|
|
|
useManastormStore.getState().resetRun();
|
|
useManastormStore.getState().startRun({
|
|
modeId: 2,
|
|
cacheMeter: 4,
|
|
random: () => 0.2,
|
|
});
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true, () => 0.99);
|
|
expect(useManastormStore.getState()).toMatchObject({ cacheMeter: 0 });
|
|
expect(useManastormStore.getState().rewardCache).toEqual([
|
|
expect.objectContaining({ cacheId: "manastorm-bonus-cache" }),
|
|
]);
|
|
});
|
|
|
|
it("resets bad-luck protection on a normal cache without double-awarding a bonus cache", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "normal-cache-fixture",
|
|
stages: [stage("normal-cache-stage", { rewardId: "normal-cache-reward" })],
|
|
rewards: [{
|
|
id: "normal-cache-reward",
|
|
currencyId: "bonzoBolts",
|
|
currencyBase: 1,
|
|
cacheId: "normal-cache",
|
|
}],
|
|
};
|
|
setManastormCatalog(catalog);
|
|
useManastormStore.getState().resetRun();
|
|
useManastormStore.getState().startRun({ cacheMeter: 4, random: () => 0.2 });
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
|
|
expect(useManastormStore.getState().cacheMeter).toBe(0);
|
|
expect(useManastormStore.getState().rewardCache).toEqual([
|
|
expect.objectContaining({ cacheId: "normal-cache" }),
|
|
]);
|
|
});
|
|
|
|
it("keeps replayed cache rewards as distinct unopened instances", () => {
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "replayed-cache-fixture",
|
|
stages: [stage("replayed-cache-stage", { rewardId: "replayed-cache-reward" })],
|
|
rewards: [{
|
|
id: "replayed-cache-reward",
|
|
currencyId: "bonzoBolts",
|
|
currencyBase: 1,
|
|
cacheId: "replayed-cache",
|
|
}],
|
|
};
|
|
const originalCache = {
|
|
id: "replayed-cache-stage:1:replayed-cache-reward",
|
|
cacheId: "replayed-cache",
|
|
rewardId: "replayed-cache-reward",
|
|
stageId: "replayed-cache-stage",
|
|
level: 1,
|
|
};
|
|
setManastormCatalog(catalog);
|
|
useManastormStore.getState().resetRun();
|
|
useManastormStore.getState().startRun({
|
|
rewardCaches: [originalCache],
|
|
random: () => 0.2,
|
|
});
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
|
|
const caches = useManastormStore.getState().rewardCache;
|
|
expect(caches).toHaveLength(2);
|
|
expect(new Set(caches.map(({ id }) => id))).toHaveLength(2);
|
|
expect(useManastormStore.getState().openRewardCache(caches[0].id)).toBe(true);
|
|
expect(useManastormStore.getState().rewardCache).toEqual([caches[1]]);
|
|
});
|
|
|
|
it("derives tuning, group modifiers, affixes, rewards, cache, currency, messages, and loadouts from a catalog", () => {
|
|
const tuning = [-45, 0, -53, -20, 0, 10, 0, 5, 1.75, 1.1, 1.2] as const;
|
|
const catalog: ManastormCatalogLike = {
|
|
id: "runtime-fixture",
|
|
modeId: 2,
|
|
maximumLevel: 100,
|
|
stages: [stage("fixture-stage", { rewardId: "fixture-reward" })],
|
|
modifiers: [{ id: "fixture-modifier", modeId: 2, level: 1, tuning, rawFlag: 7 }],
|
|
groupModifiers: [{ playerCount: 2, modeId: 2, multipliers: [1.2, 1.5] }],
|
|
affixes: [{ id: "fixture-affix", name: "Fixture", description: "Catalog affix.", unlockLevel: 1 }],
|
|
rewards: [{
|
|
id: "fixture-reward",
|
|
currencyId: "fixture-currency",
|
|
currencyBase: 2,
|
|
cacheId: "fixture-cache",
|
|
}],
|
|
loadouts: [{ id: "fixture-loadout", unlockLevel: 1 }],
|
|
defaultLoadoutId: "fixture-loadout",
|
|
messages: [{
|
|
modeId: 2,
|
|
progressionValue: 1,
|
|
groupId: 2,
|
|
title: "Fixture title",
|
|
description: "Fixture message.",
|
|
}],
|
|
};
|
|
setManastormCatalog(catalog);
|
|
useManastormStore.getState().resetRun();
|
|
const encounter = useManastormStore.getState().startRun({ partySize: 2, random: () => 0 });
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
catalogId: "runtime-fixture",
|
|
modeId: 2,
|
|
partySize: 2,
|
|
groupModifier: { enemyStatMultiplier: 1.2, rewardMultiplier: 1.5 },
|
|
scaling: {
|
|
statMultiplier: 1.75,
|
|
bonusLootChance: 0,
|
|
experienceMultiplier: 1.2,
|
|
enemyStatMultiplier: 2.1,
|
|
rewardMultiplier: 1.5,
|
|
},
|
|
activeAffixes: [{ id: "fixture-affix" }],
|
|
availableLoadoutIds: ["fixture-loadout"],
|
|
loadoutId: "fixture-loadout",
|
|
message: "Fixture title: Fixture message.",
|
|
});
|
|
|
|
useManastormStore.getState().enterStage();
|
|
useManastormStore.getState().observeTrash(true);
|
|
useManastormStore.getState().observeBoss(true, true);
|
|
expect(useManastormStore.getState()).toMatchObject({
|
|
currentEncounter: encounter,
|
|
currency: { "fixture-currency": 3 },
|
|
rewardCache: [{ cacheId: "fixture-cache", stageId: "fixture-stage" }],
|
|
});
|
|
const cacheId = useManastormStore.getState().rewardCache[0].id;
|
|
expect(useManastormStore.getState().openRewardCache(cacheId)).toBe(true);
|
|
expect(useManastormStore.getState().rewardCache).toEqual([]);
|
|
});
|
|
|
|
it("exposes phase-specific runtime ids for renderer filtering", () => {
|
|
const encounter = stage("runtime-filter");
|
|
expect(manastormActiveRuntimeIds(encounter, "trash")).toEqual([
|
|
"runtime-filter-trash-a",
|
|
"runtime-filter-trash-b",
|
|
]);
|
|
expect(manastormActiveRuntimeIds(encounter, "boss")).toEqual([
|
|
"runtime-filter-trash-a",
|
|
"runtime-filter-trash-b",
|
|
"runtime-filter-boss",
|
|
]);
|
|
expect(manastormActiveRuntimeIds(encounter, "next")).toEqual([]);
|
|
});
|
|
|
|
it("detects either a player or companion crossing the snowglobe", () => {
|
|
const portal = [10, 4, 10] as const;
|
|
expect(actorEnteredManastormPortal(portal, [[30, 4, 10], [11, 4, 10]])).toBe(true);
|
|
expect(actorEnteredManastormPortal(portal, [[30, 4, 10], [13, 4, 10]])).toBe(false);
|
|
});
|
|
|
|
it("ships finite, unique fallback encounter and spawn identities for all nine bosses", () => {
|
|
expect(MANASTORM_ENCOUNTERS).toHaveLength(9);
|
|
expect(new Set(MANASTORM_ENCOUNTERS.map((encounter) => encounter.bossRuntimeId)).size).toBe(9);
|
|
for (const encounter of MANASTORM_ENCOUNTERS) {
|
|
expect(encounter.playerSpawn.footPosition.every(Number.isFinite)).toBe(true);
|
|
expect(encounter.portalPosition.every(Number.isFinite)).toBe(true);
|
|
expect(encounter.bossEntityId).toBe(encounter.id);
|
|
}
|
|
});
|
|
});
|