Release v0.1.3 2026-07-11

This commit is contained in:
Warren H
2026-07-11 23:23:02 -04:00
parent 076f6cf97c
commit b48b3a4f8f
103 changed files with 6708 additions and 454 deletions
+81 -4
View File
@@ -4,6 +4,8 @@ import { distance, pointToSegmentDistance } from "./geometry";
import { barrierProtects, useGameStore } from "./store";
import { createClassInventory, HEALER_CLASSES } from "./healers";
import { dropVexaVenomPool, VEXA_VENOM } from "./bosses/vexa";
import { isInsideArena } from "./arena";
import { BOSS_DEFINITIONS } from "./bossCatalog";
describe("Disc Priest combat simulation", () => {
beforeEach(() => {
@@ -148,7 +150,7 @@ describe("Disc Priest combat simulation", () => {
expect(barrierProtects(barrier.center, barrier, 8)).toBe(false);
});
it("keeps ranged allies stable while Vale closes to melee range", () => {
it("keeps ranged allies stable while Vale holds behind the boss", () => {
const start = structuredClone(useGameStore.getState().partyPositions);
const bossPosition = useGameStore.getState().bossMotion.position;
useGameStore.getState().tick(1);
@@ -157,7 +159,8 @@ describe("Disc Priest combat simulation", () => {
expect(moved.brann).toEqual(start.brann);
expect(moved.nia).toEqual(start.nia);
expect(moved.orin).toEqual(start.orin);
expect(distance(moved.vale, bossPosition)).toBeLessThan(distance(start.vale, bossPosition));
expect(moved.brann[1]).toBeGreaterThan(bossPosition[1]);
expect(moved.vale[1]).toBeLessThan(bossPosition[1]);
});
it("freezes authoritative simulation while paused", () => {
@@ -255,7 +258,7 @@ describe("Disc Priest combat simulation", () => {
for (const memberId of exposedAtWarning) expect(hitIds).not.toContain(memberId);
});
it("marks a stack target after three charges and splits 300 pounce damage", () => {
it("marks a stack target after three charges and splits 200 pounce damage", () => {
useGameStore.setState((state) => ({
boss: { ...state.boss, nextMeleeAt: 999, nextNovaAt: 999, nextBrandAt: 999 },
playerPosition: [0, 0],
@@ -291,7 +294,7 @@ describe("Disc Priest combat simulation", () => {
const impacted = useGameStore.getState();
expect(impacted.bossMotion.mode).toBe("returning");
for (const member of impacted.party) {
expect(member.hp).toBeCloseTo(startingHp[member.id] - 42, 3);
expect(member.hp).toBeCloseTo(startingHp[member.id] - 28, 3);
}
expect(impacted.partyCombat.tankAura.expiresAt).toBeGreaterThan(impacted.time);
});
@@ -412,6 +415,80 @@ describe("PVE dual-boss encounter", () => {
});
});
describe("Roguelike rounds", () => {
beforeEach(() => {
useGameStore.getState().configureHealer(
"priest",
"Aelia",
createClassInventory("priest"),
["bulldrome", "vexa"],
"roguelike",
);
useGameStore.getState().startEncounter();
});
it("blocks progression for a buff, then starts round two with new bosses at 110% base HP", () => {
const roundOne = useGameStore.getState();
const previousBossIds = [roundOne.boss.id, roundOne.additionalBosses[0].boss.id];
useGameStore.setState((state) => ({
boss: { ...state.boss, hp: 0 },
additionalBosses: state.additionalBosses.map((entry) => ({ ...entry, boss: { ...entry.boss, hp: 0 } })),
}));
useGameStore.getState().tick(0.05);
expect(useGameStore.getState().phase).toBe("intermission");
const intermissionTime = useGameStore.getState().time;
useGameStore.getState().tick(10);
expect(useGameStore.getState().phase).toBe("intermission");
expect(useGameStore.getState().round).toBe(1);
expect(useGameStore.getState().time).toBe(intermissionTime);
expect(useGameStore.getState().chooseRunBuff("vital-bloom")).toBe(true);
const roundTwo = useGameStore.getState();
const nextBossIds = [roundTwo.boss.id, roundTwo.additionalBosses[0].boss.id];
expect(roundTwo.phase).toBe("combat");
expect(roundTwo.round).toBe(2);
expect(roundTwo.runBuffs).toEqual(["vital-bloom"]);
expect(nextBossIds.every((bossId) => !previousBossIds.includes(bossId))).toBe(true);
expect(roundTwo.boss.maxHp).toBe(Math.round(BOSS_DEFINITIONS[roundTwo.boss.id].maxHp * 1.1));
expect(roundTwo.additionalBosses[0].boss.maxHp).toBe(Math.round(BOSS_DEFINITIONS[roundTwo.additionalBosses[0].boss.id].maxHp * 1.1));
expect(roundTwo.party[0].maxHp).toBe(112);
});
it("rejects buff claims outside intermission", () => {
expect(useGameStore.getState().chooseRunBuff("deep-wells")).toBe(false);
expect(useGameStore.getState().round).toBe(1);
});
});
describe("shared arena boundary", () => {
beforeEach(() => {
useGameStore.getState().configureHealer("priest", "Aelia", createClassInventory("priest"));
useGameStore.getState().startEncounter();
});
it("constrains player, party, and boss positions to the same room", () => {
useGameStore.getState().setPlayerPosition([100, 100]);
useGameStore.setState((state) => ({
boss: { ...state.boss, nextMeleeAt: 999, nextNovaAt: 999, nextBrandAt: 999 },
bossMotion: { ...state.bossMotion, position: [100, -100], nextChargeAt: 999 },
partyPositions: {
...state.partyPositions,
brann: [50, 50],
nia: [-50, 50],
orin: [50, -50],
vale: [-50, -50],
},
}));
useGameStore.getState().tick(0.1);
const state = useGameStore.getState();
expect(isInsideArena(state.playerPosition)).toBe(true);
for (const position of Object.values(state.partyPositions)) expect(isInsideArena(position)).toBe(true);
expect(isInsideArena(state.bossMotion.position)).toBe(true);
});
});
describe("Cindermaw encounter", () => {
beforeEach(() => {
useGameStore.getState().configureHealer("priest", "Aelia", createClassInventory("priest"), "cindermaw");