Update Healer Man server

This commit is contained in:
phenom committed 2026-09-26 19:55:08 -04:00
1 parent 83eac6d476
commit 4957b69e86
21 files changed
+170 -40

No files matched your search

+19
View File
@@ -4014,3 +4014,22 @@ Vite's existing large-chunk advisory remains informational.
`playtest-artifacts/dungeon-race-exit/`. The offline practice playtest also
left after a modeled loss, confirmed the local race state cleared, and
started practice again. Browser page errors were absent in both passes.
## Bite-Sized spawn floor regression (2026-09-26)
- Fresh offline Dungeon Race practice entries in Wailing Caverns and Utgarde
Pinnacle previously settled about 0.5 m below the visible floor. During the
race start gate, Rapier physics was paused while PlayerRig accumulated
downward movement. Holding the player controller for that gate removed the
stored fall. Wailing keeps its original entrance; Utgarde now starts on the
connected Skadi wave corridor with three source first-wave guards.
- `scripts/playtest-bite-sized-spawns.cjs` entered both dungeons through the
menu, waited for content, intro, and race gate completion, then sampled five
seconds of play. Wailing was grounded at y=-31.016 from a -30.92 spawn;
Utgarde was grounded at y=104.812 from a 104.8 spawn. Both kept their entry
X/Z position, and there were no browser page errors. Before/after screenshots
and `result.json` are in `playtest-artifacts/bite-sized-spawns/`. The player
stands visibly on the floor in both after screenshots. Utgarde corridor samples
at z=370, 390, 410, 430, 445, and 460 also stood on collision in the live
client; the source guard points and boss position have collision and
navigation surfaces.
+2 -2
View File
@@ -12,13 +12,13 @@ Ratings:
- **B — promising:** a useful slice exists, but topology, gates, optional routes, or event handling need more design.
- **C — keep whole / no clean cut:** the full dungeon is already compact or event-led, or a cut would lose what makes the instance work.
The three first-pass candidates below are registered as derived Bite-Sized dungeons. After the first play report, the Wailing escort was replaced with Verdan’s room fight, the Deadmines entry was moved onto Mr. Smite’s source spawn, and Skadi’s hidden start interaction was replaced with a guard clear. Collision, companion navigation, wipes, completion, and results still need an in-game pass.
The three first-pass candidates below are registered as derived Bite-Sized dungeons. After the first play report, the Wailing escort was replaced with Verdan’s room fight, the Deadmines entry was moved onto Mr. Smite’s source spawn, and Skadi’s hidden start interaction was replaced with a guard clear. The Skadi entrance and guards now occupy the connected source wave corridor. Companion navigation, wipes, completion, and results still need an in-game pass.
| Prototype | Implemented slice | Open playtest question |
| --- | --- | --- |
| Wailing Caverns — Verdan’s Lair | Starts inside Verdan’s chamber at a navigation-surface point. Three source cave mobs spawn in the room; defeating all three activates Verdan. Naralex and Mutanus remain only in the full dungeon. | Confirm the room population, Verdan activation, party targeting, and completion in play. |
| The Deadmines — Take the Ship | Starts at Mr. Smite’s source floor position, clears the ship population, gates VanCleef behind Captain Greenskin, then ends in the cabin. | Confirm the new start is grounded and the ship route remains walkable. |
| Utgarde Pinnacle — Skadi’s Gauntlet | Starts at Eagle’s Eye. Defeating the three nearby imported guards activates Skadi at a source wave position on the same approach. The long waves and harpoons are omitted from this short run. | Confirm Skadi appears within the approach and the fight completes without an interaction. |
| Utgarde Pinnacle — Skadi’s Gauntlet | Starts on the grounded source wave corridor. Defeating three first-wave guards along that corridor activates Skadi at its far end. The long waves and harpoons are omitted from this short run. | Confirm Skadi appears after the guard clear and the fight completes without an interaction. |
Each derived ID resolves to its source dungeon’s content pack and map asset directory through `dungeonContentSourceId`; the original full-length dungeon remains unchanged. The new entries are labeled Bite-Sized and beta until those playtests are recorded.
+90
View File
@@ -0,0 +1,90 @@
const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const out = path.resolve('playtest-artifacts/bite-sized-spawns');
fs.mkdirSync(out, { recursive: true });
const locations = [
{ id: 'bite-sized-wailing-caverns', name: 'wailing' },
{ id: 'bite-sized-utgarde-pinnacle-skadi', name: 'utgarde' },
];
async function gameState(page) {
return page.evaluate(async () => {
const storeUrl = performance.getEntriesByType('resource').map((entry) => entry.name)
.find((url) => /\/src\/game\/store\.ts(?:\?|$)/.test(url));
if (!storeUrl) throw new Error('The active game store module was not found.');
const { useGameStore } = await import(storeUrl);
const state = useGameStore.getState();
return { assetStatus: state.assetStatus, dungeonIntroActive: state.dungeonIntroActive,
raceStartBlocked: state.raceStartBlocked,
playerPosition: state.playerPosition,
playerGrounded: state.playerGrounded, activeSpawn: state.activeSpawn };
});
}
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
const errors = [];
page.on('pageerror', (error) => errors.push(error.stack || error.message));
const results = [];
try {
await page.goto(process.env.RACE_TEST_URL || 'http://127.0.0.1:5173',
{ waitUntil: 'domcontentloaded', timeout: 120_000 });
await page.getByRole('button', { name: /Play offline/ }).click();
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Customize & name/ }).click();
await page.getByLabel('Character name').fill('SpawnTester');
await page.getByRole('button', { name: /Create character/ }).click();
await page.getByRole('button', { name: /Continue|Enter world/ }).first().click({ force: true });
await page.getByRole('button', { name: 'Continue', exact: true }).click({ force: true });
await page.locator('.main-menu-screen').waitFor();
for (const location of locations) {
await page.getByRole('button', { name: /Dungeon Race/ }).click();
await page.locator('.dungeon-race-screen__setup select').first().selectOption(location.id);
await page.getByRole('button', { name: 'Start practice' }).click();
await page.locator('.game-shell').waitFor({ timeout: 120_000 });
await page.locator('.loading-screen').waitFor({ state: 'hidden', timeout: 120_000 });
await page.locator('.dungeon-boss-intro').waitFor({ state: 'hidden', timeout: 120_000 });
await page.waitForFunction(async () => {
const storeUrl = performance.getEntriesByType('resource').map((entry) => entry.name)
.find((url) => /\/src\/game\/store\.ts(?:\?|$)/.test(url));
if (!storeUrl) return false;
const { useGameStore } = await import(storeUrl);
const state = useGameStore.getState();
return state.assetStatus === 'ready' && !state.dungeonIntroActive && !state.raceStartBlocked;
}, null, { timeout: 120_000 });
await page.waitForTimeout(5_000);
const state = await gameState(page);
await page.screenshot({ path: path.join(out, `${location.name}-after.png`) });
results.push({ location: location.name, state });
console.log(`${location.name} settled state: ${JSON.stringify(state)}`);
assert.equal(state.assetStatus, 'ready');
assert.equal(state.playerGrounded, true, `${location.name} player should be grounded`);
assert.ok(Math.abs(state.playerPosition[1] - state.activeSpawn.footPosition[1]) < 0.25,
`${location.name} player sank below the entry floor`);
assert.ok(Math.hypot(state.playerPosition[0] - state.activeSpawn.footPosition[0],
state.playerPosition[2] - state.activeSpawn.footPosition[2]) < 1,
`${location.name} player moved away from the entry point`);
await page.evaluate(async () => {
const storeUrl = performance.getEntriesByType('resource').map((entry) => entry.name)
.find((url) => /\/src\/game\/store\.ts(?:\?|$)/.test(url));
const { useGameStore } = await import(storeUrl);
useGameStore.getState().openOverlay('pause');
});
await page.getByRole('dialog', { name: 'Paused' }).getByRole('button', { name: 'Main menu' }).click();
await page.locator('.main-menu-screen').waitFor();
}
assert.deepEqual(errors, []);
const report = { results, errors, screenshots: out };
fs.writeFileSync(path.join(out, 'result.json'), `${JSON.stringify(report, null, 2)}\n`);
console.log(JSON.stringify(report, null, 2));
} catch (error) {
await page.screenshot({ path: path.join(out, 'failure.png') });
console.error((await page.locator('body').innerText()).slice(0, 3000));
throw error;
} finally { await browser.close(); }
})().catch((error) => { console.error(error); process.exitCode = 1; });
+2 -1
View File
@@ -538,7 +538,8 @@ function scoreAbility(
// summons are valid player buttons but are not autonomous combat actions.
if (categories.has("summon") && !damaging && !petSummon && !offensiveSummon && !maintenanceBuff) return null;
if (categories.has("buff") && !damaging && !defensive && !maintenanceBuff
&& !categories.has("resource") && !petSummon && !offensiveSummon) return null;
&& !categories.has("resource") && !petSummon && !offensiveSummon
&& !(offensiveMajorCooldown && profileIndex >= 0)) return null;
if (categories.has("movement") && !damaging && !context.moving) return null;
if (context.requiredControl && categories.has("control")) {
return { ability, score: 1_016_000, ruleId: "reactive-control", reason: "encounter requested control", targetId: context.hostileTarget?.id ?? null };
+2
View File
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import {
clearPartyRuntimeRegistry,
@@ -1039,6 +1040,7 @@ describe("combat store", () => {
expect(useCombatStore.getState().mobs[id].statuses).toContainEqual({
kind: "fear",
sourceAbilityId: "priest-psychic-scream",
sourceActorId: PLAYER_AGGRO_ID,
endsAt: 9000,
});
}
+6 -6
View File
@@ -1,6 +1,5 @@
import { afterAll, describe, expect, it } from "vitest";
import { mkdirSync, writeFileSync } from "node:fs";
import { BASE_CLASS_IDS, COA_CLASS_IDS, ROM_CLASS_IDS } from "../app/characterCatalog";
import { loadCanonicalBenchmarkBuilds } from "./benchmark/benchmarkBuilds";
import { runDungeonBossBenchmark } from "./benchmark/dungeonBossBenchmark";
import { BENCHMARK_POLICY_ID } from "./benchmark/benchmarkController";
@@ -14,16 +13,17 @@ const samples: unknown[] = [];
afterAll(() => {
if (process.env.BOSS_BALANCE_REPORT !== "1") return;
mkdirSync("artifacts/boss-difficulty", { recursive: true });
writeFileSync("artifacts/boss-difficulty/wow-rom-coa-balance.json", `${JSON.stringify({
writeFileSync("artifacts/boss-difficulty/ready-five-balance.json", `${JSON.stringify({
difficulty: 1, dungeon: dungeon.id, durationLimitSeconds: 60,
scope: "ready-five",
targets: { ordinary: 25, final: 45 }, policy: BENCHMARK_POLICY_ID, gear: "resolved starter heirlooms", samples,
}, null, 2)}\n`);
});
describe("default boss difficulty across WoW, RoM, and CoA classes", () => {
describe("default boss difficulty for the Ready five", () => {
for (const level of [8, 20, 40, 80]) {
it(`benchmarks every class-role build at level ${level}`, async () => {
const builds = await loadCanonicalBenchmarkBuilds(level, [...BASE_CLASS_IDS, ...ROM_CLASS_IDS, ...COA_CLASS_IDS]);
it(`benchmarks the Ready builds at level ${level}`, async () => {
const builds = await loadCanonicalBenchmarkBuilds(level);
const tooFast: string[] = [];
for (const build of builds) {
// Let the test worker deliver progress between CPU-heavy simulations.
@@ -43,7 +43,7 @@ describe("default boss difficulty across WoW, RoM, and CoA classes", () => {
for (const level of [8, 80]) {
it(`records healer outcomes with the final boss attacking at level ${level}`, async () => {
const builds = (await loadCanonicalBenchmarkBuilds(level, [...BASE_CLASS_IDS, ...ROM_CLASS_IDS, ...COA_CLASS_IDS]))
const builds = (await loadCanonicalBenchmarkBuilds(level))
.filter((build) => build.role === "healer");
for (const build of builds) {
const last = await runDungeonBossBenchmark(build, final.id, "class-balance-2", true);
+5 -3
View File
@@ -161,12 +161,14 @@ describe("dungeon boss durability and difficulty", () => {
expect(boss.seconds!, JSON.stringify({ level, boss, last })).toBeGreaterThanOrEqual(25);
expect(last.seconds!, JSON.stringify({ level, boss, last })).toBeGreaterThanOrEqual(45);
}
});
}, 60_000);
for (const level of [1, 8, 20, 40, 80]) {
it(`gives a RoM party time to play Ragefire boss mechanics at level ${level}`, () => {
it(`gives a RoM party time to play Ragefire boss mechanics at level ${level}`, async () => {
const ordinary = dungeon.bosses.find((boss) => boss.id !== final.id)!;
for (let index = 0; index < 12; index += 1) {
// Give Vitest's worker RPC a turn between CPU-heavy simulation seeds.
await new Promise<void>((resolve) => setTimeout(resolve, 0));
const seed = `rom-health-${index}`;
const boss = fight(ordinary.id, level, 1, seed);
const last = fight(final.id, level, 1, seed);
@@ -175,6 +177,6 @@ describe("dungeon boss durability and difficulty", () => {
expect(boss.seconds!).toBeGreaterThanOrEqual(25);
expect(last.seconds!).toBeGreaterThanOrEqual(45);
}
}, 30000);
}, 60_000);
}
});
+1 -1
View File
@@ -72,7 +72,7 @@ const WDT_PLACEMENT_DUNGEON_IDS = [
describe("generated dungeon campaign catalog", () => {
it("covers only reviewed five-player instances with loadable assets and combat content", () => {
expect(generated.definitions).toHaveLength(54);
expect(DUNGEON_DEFINITIONS).toHaveLength(86);
expect(DUNGEON_DEFINITIONS).toHaveLength(89);
expect(generated.definitions.map((dungeon) => dungeon.id))
.toEqual(campaign.dungeons.map((dungeon) => dungeon.slug));
for (const dungeon of generated.definitions) {
+5 -1
View File
@@ -23,7 +23,11 @@ export function scaledDungeonEnemyLevel(characterLevel: number, boss: boolean):
export function dungeonEnemyHealthBaseline(level: number, partySource: ContentCategoryId): number {
const safeLevel = clampPlayerLevel(level);
if (partySource === "wow") return standardWowDungeonCreatureHealth(safeLevel);
const romBaseline = 1_000 + safeLevel * 100 + safeLevel * safeLevel * 8;
// Companion damage stops tracking the quadratic health curve near the level cap.
// Ease the last ten levels so a slow but valid RoM party still finishes trash
// within the intended thirty-second combat window.
const highLevelTaper = 1 - Math.max(0, safeLevel - 70) * 0.006;
const romBaseline = Math.round((1_000 + safeLevel * 100 + safeLevel * safeLevel * 8) * highLevelTaper);
if (partySource === "rom") return romBaseline;
return Math.round(romBaseline * COA_PARTY_HEALTH_SCALE / ROM_PARTY_HEALTH_SCALE);
}
+1 -1
View File
@@ -18,7 +18,7 @@ describe("dungeon pipeline runtime foundation", () => {
});
it("registers every reviewed instanced dungeon with WC first", () => {
expect(DUNGEON_DEFINITIONS).toHaveLength(86);
expect(DUNGEON_DEFINITIONS).toHaveLength(89);
expect(DUNGEON_DEFINITIONS[0].id).toBe("wailing-caverns");
expect(DUNGEON_DEFINITIONS.some((definition) => definition.id === "deadmines")).toBe(true);
expect(DUNGEON_DEFINITIONS.some((definition) => definition.id === "forsaken-abbey")).toBe(true);
+14 -12
View File
@@ -19,7 +19,6 @@ import {
BITE_SIZED_WAILING_CAVERNS_DUNGEON_ID,
} from "../dungeonContentSource";
const DEADMINES_ROUTE_WIDTH = 34;
const SKADI_ROUTE_WIDTH = 32;
function normalizedName(value: string): string {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
@@ -331,14 +330,17 @@ export function createBiteSizedUtgardePinnacleDefinition(source: DungeonDefiniti
const skadi = source.objectiveOrder.find((objective) => normalizedName(objective.name) === "skaditheruthless");
const sourceSkadiGroup = source.encounterSpawnGroups?.find((group) => group.id === "utgarde-pinnacle:skadi-ground");
const firstWave = source.encounterSpawnGroups?.find((group) => group.id === "utgarde-pinnacle:skadi-first-wave");
const skadiStart = source.serverGameObjects.find((object) => object.id === "utgarde-pinnacle:skadi-start");
if (!skadi || !sourceSkadiGroup?.spawns[0] || !firstWave?.spawns[0] || !skadiStart) {
if (!skadi || !sourceSkadiGroup?.spawns[0] || !firstWave?.spawns[7]) {
throw new Error("Utgarde Pinnacle is missing the Skadi room content.");
}
// The source start object is across a collision gap from the Skadi corridor.
const startPosition: Vector3Tuple = [-511, 104.8, 370];
const bossPosition = firstWave.spawns[0].position;
const route: readonly Vector3Tuple[] = [skadiStart.position, bossPosition];
const staticSpawns = routeSpawns(source, route, SKADI_ROUTE_WIDTH);
if (staticSpawns.length === 0) throw new Error("Skadi's approach has no room mobs.");
const staticSpawns: readonly StaticMobSpawnDefinition[] = [
firstWave.spawns[7],
firstWave.spawns[6],
firstWave.spawns[5],
];
const skadiSpawn: StaticMobSpawnDefinition = {
...sourceSkadiGroup.spawns[0],
id: skadi.id,
@@ -358,10 +360,10 @@ export function createBiteSizedUtgardePinnacleDefinition(source: DungeonDefiniti
fidelity: "healer-man-authored",
scope: "boss-progression",
files: ["src/game/dungeons/biteSizedWoWDungeons.ts", "src/game/wowWrathEncounterBatchTwoCatalog.ts"],
reviewedBehavior: "Clear the three imported mobs near Eagle's Eye, then confront Skadi on the same walkable approach.",
reviewedBehavior: "Clear three source wave guards along the walkable Skadi corridor, then confront Skadi at its end.",
adaptations: [
"The start interaction, flying phase, harpoons, and long wave sequence are omitted from this short run.",
"Skadi uses the source boss combat data and objective ID at a floor-snapped source wave position near the entrance.",
"Skadi uses the source boss combat data and objective ID at a floor-snapped source wave position at the corridor's end.",
],
remainingGaps: ["The shortened encounter still needs an in-game combat and completion pass."],
},
@@ -393,9 +395,9 @@ export function createBiteSizedUtgardePinnacleDefinition(source: DungeonDefiniti
entrance: {
...source.entrance,
name: "Eagle’s Eye Gauntlet",
footPosition: skadiStart.position,
forward: [-0.167, 0, -0.986],
yaw: -2.974,
footPosition: startPosition,
forward: [0, 0, 1],
yaw: 0,
},
presentation: {
...source.presentation,
@@ -406,7 +408,7 @@ export function createBiteSizedUtgardePinnacleDefinition(source: DungeonDefiniti
unchartedAreaName: "Eagle’s Eye",
},
areas: [
routeArea("entrance", "Eagle’s Eye Gauntlet", skadiStart.position, 30),
routeArea("entrance", "Eagle’s Eye Gauntlet", startPosition, 30),
routeArea("skadi", "Skadi’s Arena", bossPosition, 34),
],
entities: entitiesFor(source, staticSpawns, [], [skadiGroup]),
+2
View File
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AGGRO_ID } from "./aggro";
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
import { useCombatStore } from "./combatStore";
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
@@ -139,6 +140,7 @@ describe("enemy casting and interrupts", () => {
expect(useCombatStore.getState().mobs.caster.statuses).toContainEqual({
kind: "stun",
sourceAbilityId: "rogue-gouge",
sourceActorId: PLAYER_AGGRO_ID,
endsAt: 5_100,
});
});
+1
View File
@@ -60,6 +60,7 @@ describe("server-backed enemy ability runtime", () => {
expect(useCombatStore.getState().mobs.kresh.statuses).toContainEqual({
kind: "fear",
sourceAbilityId: "priest-psychic-scream",
sourceActorId: PLAYER_AGGRO_ID,
endsAt: 9_000,
});
expect(useCombatStore.getState().mobs.vancleef.statuses).toEqual([]);
+1 -1
View File
@@ -108,7 +108,7 @@ describe("looped mob path sampling", () => {
});
it("keeps every packaged dungeon patrol on a usable loop", () => {
expect(DUNGEON_DEFINITIONS).toHaveLength(86);
expect(DUNGEON_DEFINITIONS).toHaveLength(89);
for (const dungeon of DUNGEON_DEFINITIONS) {
for (const pack of dungeon.roamingPacks) {
expect(hasUsableLoopedMobPath(pack.waypoints), `${dungeon.id}:${pack.id}`).toBe(true);
+5 -4
View File
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { DUNGEON_DEFINITIONS, dungeonCanEnter } from "./dungeonRegistry";
import { dungeonContentSourceId } from "./dungeonContentSource";
import manastormRegistry from "./generated/manastormAssetPackages.json";
import type { ManastormStageAssetPackage } from "./manastormStageAssets";
import {
@@ -70,7 +71,7 @@ function regularNavigationPath(
"assets",
"game",
"dungeons",
dungeonId,
dungeonContentSourceId(dungeonId),
navigation.fileName,
);
}
@@ -84,7 +85,7 @@ describe("party navigation fleet safety", () => {
const enabled = [];
const fallback = [];
expect(DUNGEON_DEFINITIONS).toHaveLength(86);
expect(DUNGEON_DEFINITIONS).toHaveLength(89);
for (const dungeon of DUNGEON_DEFINITIONS) {
expect(dungeonCanEnter(dungeon.id), dungeon.id).toBe(true);
const navigation = dungeon.assets.navigation;
@@ -119,8 +120,8 @@ describe("party navigation fleet safety", () => {
// Stockades has a reviewed navigation override without claiming that its
// broader encounter/reset validation is complete.
expect(enabled).toContain("stormwind-stockade");
expect(enabled).toHaveLength(32);
expect(fallback).toHaveLength(54);
expect(enabled).toHaveLength(33);
expect(fallback).toHaveLength(56);
});
it("covers every Manastorm package and keeps actual synchronous geometry under budget", () => {
+7 -2
View File
@@ -78,17 +78,22 @@ function measurePull(level: number, seed: string, maxHealth: number, isBoss = fa
describe("RoM dungeon enemy durability", () => {
for (const level of [1, 2, 4, 8, 9, 20, 40, 70, 80]) {
it(`Barren Caves trash survives a full RoM party's opening volley at level ${level}`, () => {
const results = [];
for (let seed = 0; seed < 12; seed += 1) {
const partySeed = `rom-health-${seed}`;
const before = measurePull(level, partySeed, 72 + level * 22);
const after = measurePull(level, partySeed, dungeonEnemyHealthBaseline(level, "rom"));
report.push({ level, seed: partySeed, before, after });
results.push({ seed: partySeed, before, after });
expect(after.openingHealth, JSON.stringify({ level, before, after })).toBeGreaterThan(0);
expect(after.seconds, JSON.stringify({ level, before, after })).not.toBeNull();
expect(after.seconds!, JSON.stringify({ level, before, after })).toBeGreaterThanOrEqual(3);
expect(after.seconds!, JSON.stringify({ level, before, after })).toBeLessThanOrEqual(30);
}
});
const slowest = results.reduce((longest, candidate) => (
candidate.after.seconds! > longest.after.seconds! ? candidate : longest
));
expect(slowest.after.seconds!, JSON.stringify({ level, ...slowest })).toBeLessThanOrEqual(30);
}, 60_000);
}
it("retains Barren Caves' authored boss health multiplier", () => {
+2 -2
View File
@@ -4,11 +4,11 @@ import { FORSAKEN_ABBEY_DEFINITION } from "./dungeons/forsakenAbbey";
import { WOW_DUNGEON_LIGHTING, withWowDungeonEnvironment, wowDungeonLightingProfile } from "./wowDungeonEnvironment";
import stairs from "./generated/wowDungeonStairs.generated.json";
import stockadesOverride from "../../dungeon-pipeline/runtime-overrides/stormwind-stockade.json";
import { isDerivedStockadesDungeonId } from "./dungeonContentSource";
import { isBiteSizedDungeonId } from "./dungeonContentSource";
describe("fleet-wide WoW dungeon environment policy", () => {
const wowDungeons = DUNGEON_DEFINITIONS.filter((dungeon) => (
dungeon.expansion !== "runewaker" && !isDerivedStockadesDungeonId(dungeon.id)
dungeon.expansion !== "runewaker" && !isBiteSizedDungeonId(dungeon.id)
));
it("gives every WoW dungeon readable ambient and hemisphere fill without changing fog draw distance or material roles", () => {
+1 -1
View File
@@ -121,7 +121,7 @@ function PlayerController({
verticalMotionRef,
animationEventRef,
}: PlayerControllerProps) {
const paused = useGameStore((state) => state.paused || state.dungeonIntroActive);
const paused = useGameStore((state) => state.paused || state.dungeonIntroActive || state.raceStartBlocked);
const mapOpen = useGameStore((state) => state.mapOpen);
const companionOpen = useGameStore((state) => state.companionOpen);
const resetRevision = useGameStore((state) => state.resetRevision);
+2 -2
View File
@@ -3,7 +3,7 @@ import path from "node:path";
import { MeshStandardMaterial } from "three";
import { describe, expect, it } from "vitest";
import { DUNGEON_DEFINITIONS } from "../game/dungeonRegistry";
import { isDerivedStockadesDungeonId } from "../game/dungeonContentSource";
import { isBiteSizedDungeonId } from "../game/dungeonContentSource";
import { prepareDungeonMaterial } from "./productionDungeonMaterials";
interface GltfMaterial {
@@ -28,7 +28,7 @@ function shippedMaterials(file: string): GltfMaterial[] {
}
const wowDungeons = DUNGEON_DEFINITIONS.filter((definition) => (
definition.expansion !== "runewaker" && !isDerivedStockadesDungeonId(definition.id)
definition.expansion !== "runewaker" && !isBiteSizedDungeonId(definition.id)
));
describe("shipped WoW dungeon material policies", () => {
+1 -1
View File
@@ -59,7 +59,7 @@ describe("gameplay panel loading integration", () => {
it("groups primary adventures separately and keeps testing utilities in the GM menu", () => {
expect(mainMenuSource).toContain('className="main-menu-card main-menu-card--adventures"');
expect(mainMenuSource).toContain('["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"]');
expect(mainMenuSource).toContain('["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized", "dungeon-race"]');
expect(mainMenuSource).toContain('controlId="mode-gm-menu"');
for (const controlId of [
"gm-menu-gm-tools",
+1
View File
@@ -119,6 +119,7 @@ export default defineConfig(({ mode }) => {
test: {
environment: "node",
include: ["src/**/*.test.ts"],
maxWorkers: 4,
},
};
});