Update Healer Man server

This commit is contained in:
phenom committed 2026-09-20 19:27:37 -04:00
1 parent 97ee0c13f9
commit f864b5aea7
20 files changed
+468 -211

No files matched your search

+61 -1
View File
@@ -1,5 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { BITE_SIZED_STOCKADES_DUNGEON_ID } from "../game/dungeonContentSource";
import {
BITE_SIZED_STOCKADES_DUNGEON_ID,
RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
} from "../game/dungeonContentSource";
import type { DungeonRunProgress } from "../game/dungeonRunTypes";
import { canStartAnotherBiteSizedDungeon, startAnotherBiteSizedDungeon } from "./biteSizedDungeonRuntime";
import { useOnlineGroupStore } from "./onlineGroupStore";
@@ -52,6 +55,36 @@ describe("bite-sized replay", () => {
difficultyId: "normal",
bossDifficulty: 1,
freshRun: true,
runId: expect.any(String),
});
});
it("replays randomized mode with a new seed and prepares that seed's creature assets", async () => {
const enterDungeon = vi.fn(() => true);
const saveCheckpoint = vi.fn();
const prepareRandomizedContent = vi.fn(async () => undefined);
useShellStore.setState({
session: { kind: "offline", ownerId: "local-owner", displayName: "Tester" },
activeDungeonRole: "healer",
enterDungeon,
saveActiveDungeonRunCheckpoint: saveCheckpoint,
});
const randomizedProgress = { ...progress, dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID };
await expect(startAnotherBiteSizedDungeon(randomizedProgress, {
random: () => 0,
createRunId: () => "next-randomized-seed",
contentIsUsable: () => true,
prepareRandomizedContent,
})).resolves.toBe(true);
expect(prepareRandomizedContent).toHaveBeenCalledWith("next-randomized-seed");
expect(saveCheckpoint).toHaveBeenCalledWith(null);
expect(enterDungeon).toHaveBeenCalledWith(RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID, "healer", {
difficultyId: "normal",
bossDifficulty: 1,
freshRun: true,
runId: "next-randomized-seed",
});
});
@@ -91,4 +124,31 @@ describe("bite-sized replay", () => {
useShellStore.setState({ session: { kind: "account", ownerId: "follower", displayName: "Follower", accessToken: "test-token" } });
expect(canStartAnotherBiteSizedDungeon()).toBe(false);
});
it("publishes another randomized activity instead of crossing an online group into regular Bite-Sized", async () => {
const startActivity = vi.fn(async () => true);
useShellStore.setState({
session: { kind: "account", ownerId: "leader", displayName: "Leader", accessToken: "test-token" },
saveActiveDungeonRunCheckpoint: vi.fn(),
});
useOnlineGroupStore.setState({
startActivity,
snapshot: { invitations: [], group: { id: "group", leaderAccountId: "leader", members: [], activity: null,
activityRevision: 2, createdAt: 1, updatedAt: 2 } },
});
await expect(startAnotherBiteSizedDungeon(
{ ...progress, dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID },
{ random: () => 0, createRunId: () => "unused-local-seed", contentIsUsable: () => true,
prepareRandomizedContent: vi.fn() },
)).resolves.toBe(true);
expect(startActivity).toHaveBeenCalledWith(expect.objectContaining({ ownerId: "leader" }), {
type: "dungeon",
contentId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
difficultyId: "normal",
bossDifficulty: 1,
partySize: 5,
});
});
});
+40 -9
View File
@@ -1,9 +1,11 @@
import { dungeonContentIsUsable } from "../content/contentManager";
import { randomDungeonByCategory } from "../game/dungeonCatalog";
import { randomDungeonByCategory, randomRandomizedBiteSizedDungeon } from "../game/dungeonCatalog";
import { isRandomizedBiteSizedDungeonId } from "../game/dungeonContentSource";
import { dungeonDefinitionById } from "../game/dungeonRegistry";
import type { DungeonRunProgress } from "../game/dungeonRunTypes";
import { useOnlineGroupStore } from "./onlineGroupStore";
import { useOnlineSessionStore } from "./onlineSessionStore";
import { ensureRandomizedBiteSizedRunContent } from "./randomizedBiteSizedContent";
import { useShellStore } from "./shellStore";
export function canStartAnotherBiteSizedDungeon(): boolean {
@@ -14,8 +16,25 @@ export function canStartAnotherBiteSizedDungeon(): boolean {
return !online || online.authority;
}
interface BiteSizedReplayDependencies {
readonly random: () => number;
readonly createRunId: () => string;
readonly contentIsUsable: (dungeonId: string) => boolean;
readonly prepareRandomizedContent: (seed: string) => Promise<void>;
}
const DEFAULT_REPLAY_DEPENDENCIES: BiteSizedReplayDependencies = {
random: Math.random,
createRunId: () => globalThis.crypto.randomUUID(),
contentIsUsable: dungeonContentIsUsable,
prepareRandomizedContent: ensureRandomizedBiteSizedRunContent,
};
/** Starts a fresh random run locally, or publishes one shared activity for the whole online party. */
export async function startAnotherBiteSizedDungeon(progress: DungeonRunProgress): Promise<boolean> {
export async function startAnotherBiteSizedDungeon(
progress: DungeonRunProgress,
dependencies: BiteSizedReplayDependencies = DEFAULT_REPLAY_DEPENDENCIES,
): Promise<boolean> {
const shell = useShellStore.getState();
const groupState = useOnlineGroupStore.getState();
const group = groupState.snapshot.group;
@@ -25,14 +44,16 @@ export async function startAnotherBiteSizedDungeon(progress: DungeonRunProgress)
return false;
}
const next = randomDungeonByCategory(
"bite-sized",
progress.dungeonId,
Math.random,
(dungeon) => dungeonContentIsUsable(dungeon.id),
);
const randomized = isRandomizedBiteSizedDungeonId(progress.dungeonId);
const next = randomized
? randomRandomizedBiteSizedDungeon(progress.dungeonId, dependencies.random,
(dungeon) => dependencies.contentIsUsable(dungeon.id))
: randomDungeonByCategory("bite-sized", progress.dungeonId, dependencies.random,
(dungeon) => dependencies.contentIsUsable(dungeon.id));
if (!next) {
shell.setNotice("No installed Bite-Sized dungeon is ready to enter.");
shell.setNotice(randomized
? "No Randomized Bite-Sized location is ready to enter."
: "No installed Bite-Sized dungeon is ready to enter.");
return false;
}
const definition = dungeonDefinitionById(next.id);
@@ -65,11 +86,21 @@ export async function startAnotherBiteSizedDungeon(progress: DungeonRunProgress)
shell.setNotice("Reconnect to the party before starting the next shared dungeon.");
return false;
}
const runId = dependencies.createRunId();
if (randomized) {
try {
await dependencies.prepareRandomizedContent(runId);
} catch (caught) {
shell.setNotice(caught instanceof Error ? caught.message : "The randomized creature download failed.");
return false;
}
}
shell.saveActiveDungeonRunCheckpoint(null);
const entered = shell.enterDungeon(next.id, shell.activeDungeonRole, {
difficultyId,
bossDifficulty: progress.bossDifficulty,
freshRun: true,
runId,
});
return entered;
}