Files
noname-shuying/tools/test_relics.mjs
T

292 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { grantRelic, canAcquireRelic, getRelic, heldRelics,
relicRewardIds, initializeRelicMaxHp } from "../mengsan/relics/definitions.js";
import { applyRoomRelics, restRecovery } from "../mengsan/relics/rooms.js";
import { createOpeningEffects, createRelicCombatSkills }
from "../mengsan/relics/combat.js";
import { createRelicBattle, createRelicSkills,
applyBattleEndRelics } from "../mengsan/relics/battle.js";
import { canAcquireReward, getRandomRewardChoices }
from "../mengsan/progression/reward.js";
import { createBattleSettlement }
from "../mengsan/battle/battle-settlement.js";
import config from "../mengsan/config.js";
const id = name => `mengsan_${name}_shuying`;
const newRun = () => ({ runId: "relic_run", actIndex: 0, revision: 1,
randomState: 123,
player: { character: "mengsan_liubei_shuying", hp: 20,
maxHp: 50, items: [], gold: 0, deck: [] },
statistics: { defeatedEnemies: 0, goldEarned: 0 },
map: { nodes: [{ id: "battle", completed: false }] },
});
const grant = (run, ...names) => {
for (const name of names) grantRelic(run, id(name));
};
const runtime = run => {
const draws = [], logs = [];
let running = true;
const battle = createRelicBattle(run, {
active: () => running,
draw: async amount => { draws.push(amount); },
log: (relic, effect) => logs.push([relic.name, effect]),
});
return { battle, draws, logs, stop: () => { running = false; } };
};
test("strawberry raises max HP once without healing, including early grants", () => {
const run = newRun();
grant(run, "strawberry", "strawberry");
assert.equal(run.player.maxHp, 57);
assert.equal(run.player.hp, 20);
run.player.maxHp = null;
run.player.items = [];
grant(run, "strawberry");
run.player.maxHp = 50;
initializeRelicMaxHp(run);
initializeRelicMaxHp(run);
assert.equal(run.player.maxHp, 57);
});
test("shop entrance and rest healing cap HP and survive reload without repeats", () => {
const run = newRun();
grant(run, "meal_ticket", "regal_pillow");
const shop = { id: "shop1", type: "shop" };
assert.equal(applyRoomRelics(run, shop, "enter").recovered, 15);
const restored = structuredClone(run);
assert.equal(applyRoomRelics(restored, shop, "enter"), null);
const rest = { id: "rest1", type: "rest" };
assert.equal(restRecovery(run), 23);
assert.equal(applyRoomRelics(run, rest, "upgrade"), null);
assert.equal(applyRoomRelics(run, rest, "heal").recovered, 15);
assert.equal(run.player.hp, 50);
assert.equal(applyRoomRelics(run, rest, "heal"), null);
assert.equal(restRecovery(newRun()), 8);
});
test("opening effects target enemies, apply buffs before damage, and lantern is temporary", async () => {
const run = newRun();
grant(run, "festive_poppers", "bag_of_marbles", "red_mask", "vajra", "lantern");
const hits = [];
const actor = camp => ({ storage: { mengsanCamp_shuying: camp },
isAlive: () => true, addSkill() {}, markSkill() {},
damage(amount, source, nature) {
hits.push({ amount, source, nature,
vulnerable: this.storage.mengsanVulnerable_shuying,
weak: this.storage.mengsanWeak_shuying });
return Promise.resolve();
} });
const owner = actor("player"), enemy = actor("enemy"), ally = actor("ally");
owner.storage.mengsanEnergy_shuying = 3;
owner.storage.mengsanMaxEnergy_shuying = 3;
let refreshes = 0;
const battle = createRelicBattle(run, { active: () => true,
draw: async () => {}, log: () => {},
effect: createOpeningEffects({ players: [owner, enemy, ally] },
owner, () => { refreshes++; }) });
await battle.start();
await battle.start();
assert.equal(hits.length, 1);
assert.equal(hits[0].amount, 9);
assert.equal(hits[0].vulnerable, 1);
assert.equal(hits[0].weak, 1);
assert.equal(hits[0].nature, "nocard");
assert.equal(owner.storage.mengsanStrength_shuying, 1);
assert.equal(ally.storage.mengsanWeak_shuying, undefined);
const first = {};
await battle.turnStart(first);
await battle.turnStart(first);
assert.equal(owner.storage.mengsanEnergy_shuying, 4);
assert.equal(owner.storage.mengsanMaxEnergy_shuying, 3);
owner.storage.mengsanEnergy_shuying = owner.storage.mengsanMaxEnergy_shuying;
await battle.turnStart({});
assert.equal(owner.storage.mengsanEnergy_shuying, 3);
assert.equal(refreshes, 1);
});
test("card attacks add owner strength before weakness and skip scripted damage", async () => {
const owner = { storage: { mengsanStrength_shuying: 1,
mengsanWeak_shuying: 1 } };
const skills = createRelicCombatSkills(() => ({ session: { active: true } }),
() => owner);
const attack = skills.mengsan_relic_attack_shuying;
const event = { source: owner, card: {}, num: 6 };
assert.equal(attack.filter(event), true);
await attack.content({}, event);
assert.equal(event.num, 5);
assert.equal(attack.filter({ ...event, card: null }), false);
assert.equal(attack.filter({ ...event, mengsanScriptedSkill_shuying: true }), false);
const enemy = { storage: { mengsanWeak_shuying: 1 },
removeSkill(skill) { assert.equal(skill, "mengsan_weak_shuying"); } };
const hit = { source: enemy, card: {}, num: 9 };
await attack.content({}, hit);
assert.equal(hit.num, 6);
await skills.mengsan_weak_shuying.content({}, {}, enemy);
assert.equal(enemy.storage.mengsanWeak_shuying, 0);
});
test("relics use existing items, reject duplicates and preserve belt saves", () => {
const run = newRun();
assert.equal(grantRelic(run, id("burning_blood")), true);
assert.equal(grantRelic(run, id("burning_blood")), false);
assert.equal(canAcquireRelic(run, id("burning_blood")), false);
assert.throws(() => grantRelic(run, "unknown"));
assert.equal(getRelic("unknown"), null);
run.player.items.push(id("hand_charm"));
run.player.handLimitBonus = 1;
assert.equal(grantRelic(run, id("hand_charm")), false);
assert.equal(run.player.handLimitBonus, 1);
const fresh = newRun();
assert.equal(grantRelic(fresh, id("hand_charm")), true);
assert.equal(fresh.player.handLimitBonus, 1);
const restored = JSON.parse(JSON.stringify(run));
assert.equal(heldRelics(restored).length, 2);
run.player.items.push(id("burning_blood"));
assert.equal(heldRelics(run).length, 2);
});
test("boss and chest relic rewards are available and owned ones disappear", () => {
const run = newRun();
for (const pool of ["shared.pool.boss.premium", "act1.pool.chest"]) {
const choices = getRandomRewardChoices(run, pool, 100);
for (const rewardId of relicRewardIds) {
assert.ok(choices.some(choice => choice.id === rewardId));
}
}
grant(run, "burning_blood");
const rewardId = relicRewardIds.find(value => value.endsWith(
id("burning_blood")));
assert.equal(canAcquireReward(run, config.rewards[rewardId]), false);
assert.equal(getRandomRewardChoices(run,
"shared.pool.boss.premium", 100).some(choice =>
choice.id === rewardId), false);
});
test("opening and own-turn draws stack at exact turns and reset per battle", async () => {
const run = newRun();
grant(run, "snake_ring", "long_snake_ring", "pendulum");
const { battle, draws } = runtime(run);
assert.equal(battle.openingDraw, 2);
await battle.start();
await battle.start();
assert.deepEqual(draws, [2]);
assert.equal(battle.openingHandBonus, 2);
for (let turn = 1; turn <= 6; turn++) {
const event = {};
await battle.turnStart(event);
await battle.turnStart(event);
}
assert.deepEqual(draws, [2, 2, 2, 2, 1, 1]);
const next = runtime(run);
await next.battle.start();
await next.battle.turnStart({});
assert.deepEqual(next.draws, [2, 2]);
});
test("puzzle ignores zero and healing, triggers once, and resets next battle", async () => {
const run = newRun();
grant(run, "millennium_puzzle");
const current = runtime(run);
await current.battle.start();
await current.battle.loseHp({ num: 0 });
await current.battle.loseHp({ num: 6 });
assert.deepEqual(current.draws, []);
const damage = { num: -1 };
await current.battle.loseHp(damage);
await current.battle.loseHp(damage);
await current.battle.loseHp({ num: -3 });
assert.deepEqual(current.draws, [3]);
assert.equal(current.battle.status(getRelic(id("millennium_puzzle"))),
"本场已触发");
const next = runtime(run);
await next.battle.start();
await next.battle.loseHp({ num: -2 });
assert.deepEqual(next.draws, [3]);
});
test("early HP loss waits for pile setup and preserves opening bonus draws", async () => {
const run = newRun();
grant(run, "snake_ring", "millennium_puzzle");
const current = runtime(run);
await current.battle.loseHp({ num: -1 });
assert.deepEqual(current.draws, []);
await current.battle.start();
assert.deepEqual(current.draws, [2, 3]);
assert.equal(current.battle.openingHandBonus, 5);
await current.battle.loseHp({ num: -1 });
assert.deepEqual(current.draws, [2, 3]);
await current.battle.turnStart({});
assert.equal(current.battle.openingHandBonus, 5);
});
test("engine adapter ignores other actors, armor-only loss and dead owner", async () => {
const run = newRun();
grant(run, "millennium_puzzle", "long_snake_ring");
const current = runtime(run);
const owner = { hp: 20 }, session = { active: true };
const adapter = createRelicSkills(() => ({
session, relics: current.battle }), () => owner).mengsan_relics_shuying;
await current.battle.start();
assert.equal(adapter.filter({ player: {}, name: "phase" }), false);
assert.equal(adapter.filter({ player: owner, num: 0 }), false);
assert.equal(adapter.filter({ player: owner, num: -1 }), true);
await adapter.content({ triggername: "phaseBegin" },
{ player: owner, name: "phase" });
await adapter.content({ triggername: "changeHp" },
{ player: owner, num: -1 });
assert.deepEqual(current.draws, [2, 3]);
owner.hp = 0;
assert.equal(adapter.filter({ player: owner, num: -1 }), false);
owner.hp = 20;
session.active = false;
assert.equal(adapter.filter({ player: owner, name: "phase" }), false);
current.stop();
await current.battle.turnStart({});
assert.deepEqual(current.draws, [2, 3]);
});
test("blood healing caps at max HP and never revives a failed run", () => {
const run = newRun();
grant(run, "burning_blood");
assert.equal(applyBattleEndRelics(run, "victory"), 6);
assert.equal(run.player.hp, 26);
run.player.hp = 49;
assert.equal(applyBattleEndRelics(run, "victory"), 1);
assert.equal(run.player.hp, 50);
run.player.hp = 0;
assert.equal(applyBattleEndRelics(run, "defeat"), 0);
assert.equal(run.player.hp, 0);
});
test("settlement prepare, reload and repeated commit heal only once", async () => {
const run = newRun();
grant(run, "burning_blood");
const storage = { run: structuredClone(run) };
const make = () => createBattleSettlement({
store: { read: () => storage,
update: async change => change(storage) },
config: { saveKey: "run", profileKey: "profile" },
getRandomRewardChoices: () => [], applyReward: () => {},
completeNode: result => {
result.map.nodes[0].completed = true;
return true;
},
enterNextAct: () => false,
});
const input = { run, node: run.map.nodes[0], hp: 20,
encounter: { skipRandomReward: true } };
const pending = await make().prepare(input);
assert.equal(pending.base.player.hp, 26);
assert.equal(pending.relicRecovery, 6);
assert.equal((await make().prepare(input)).base.player.hp, 26);
const restored = make();
const selected = await restored.choose(run.runId, pending.id, null);
assert.equal(selected.result.player.hp, 26);
await restored.choose(run.runId, pending.id, null);
await restored.commit(run.runId, pending.id);
assert.equal(storage.run.player.hp, 26);
await restored.commit(run.runId, pending.id);
assert.equal(storage.run.player.hp, 26);
});