Files
initiative/apps/web/src/__tests__/adapters/in-memory-adapters.ts
Lukas c3707cf0b6 Add PF2e attack effects, ability frequency, and perception details
Show inline on-hit effects on attack lines (e.g., "plus Grab"), frequency
limits on abilities (e.g., "(1/day)"), and perception details text alongside
senses. Strip redundant frequency lines from Foundry descriptions.

Also add resilient PF2e source fetching: batched requests with retry,
graceful handling of ad-blocker-blocked creature files (partial success
with toast warning and re-fetch prompt for missing creatures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:37:03 +02:00

123 lines
3.4 KiB
TypeScript

import {
type AnyCreature,
type CreatureId,
EMPTY_UNDO_REDO_STATE,
type Encounter,
type PlayerCharacter,
type UndoRedoState,
} from "@initiative/domain";
import type { Adapters } from "../../contexts/adapter-context.js";
export function createTestAdapters(options?: {
encounter?: Encounter | null;
undoRedoState?: UndoRedoState;
playerCharacters?: PlayerCharacter[];
creatures?: Map<CreatureId, AnyCreature>;
sources?: Map<
string,
{ displayName: string; creatures: AnyCreature[]; cachedAt: number }
>;
}): Adapters {
let storedEncounter = options?.encounter ?? null;
let storedUndoRedo = options?.undoRedoState ?? EMPTY_UNDO_REDO_STATE;
let storedPCs = options?.playerCharacters ?? [];
const sourceStore =
options?.sources ??
new Map<
string,
{ displayName: string; creatures: AnyCreature[]; cachedAt: number }
>();
// Pre-populate sourceStore from creatures map if provided
if (options?.creatures && !options?.sources) {
// No-op: creatures are accessed directly from the map
}
const creatureMap = options?.creatures ?? new Map<CreatureId, AnyCreature>();
return {
encounterPersistence: {
load: () => storedEncounter,
save: (e) => {
storedEncounter = e;
},
},
undoRedoPersistence: {
load: () => storedUndoRedo,
save: (state) => {
storedUndoRedo = state;
},
},
playerCharacterPersistence: {
load: () => [...storedPCs],
save: (pcs) => {
storedPCs = pcs;
},
},
bestiaryCache: {
cacheSource(system, sourceCode, displayName, creatures) {
const key = `${system}:${sourceCode}`;
sourceStore.set(key, {
displayName,
creatures,
cachedAt: Date.now(),
});
for (const c of creatures) {
creatureMap.set(c.id, c);
}
return Promise.resolve();
},
isSourceCached(system, sourceCode) {
return Promise.resolve(sourceStore.has(`${system}:${sourceCode}`));
},
getCachedSources(system) {
return Promise.resolve(
[...sourceStore.entries()]
.filter(([key]) => !system || key.startsWith(`${system}:`))
.map(([key, info]) => ({
sourceCode: key.includes(":")
? key.slice(key.indexOf(":") + 1)
: key,
displayName: info.displayName,
creatureCount: info.creatures.length,
cachedAt: info.cachedAt,
})),
);
},
clearSource(system, sourceCode) {
sourceStore.delete(`${system}:${sourceCode}`);
return Promise.resolve();
},
clearAll() {
sourceStore.clear();
return Promise.resolve();
},
loadAllCachedCreatures() {
return Promise.resolve(new Map(creatureMap));
},
},
bestiaryIndex: {
loadIndex: () => ({ sources: {}, creatures: [] }),
getAllSourceCodes: () => [],
getDefaultFetchUrl: (sourceCode, baseUrl) => {
const filename = `bestiary-${sourceCode.toLowerCase()}.json`;
if (baseUrl !== undefined) {
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
return `${normalized}${filename}`;
}
return `https://example.com/${filename}`;
},
getSourceDisplayName: (sourceCode) => sourceCode,
},
pf2eBestiaryIndex: {
loadIndex: () => ({ sources: {}, creatures: [] }),
getAllSourceCodes: () => [],
getDefaultFetchUrl: (sourceCode) =>
`https://example.com/creatures-${sourceCode.toLowerCase()}.json`,
getSourceDisplayName: (sourceCode) => sourceCode,
getCreaturePathsForSource: () => [],
getCreatureNamesByPaths: () => new Map(),
},
};
}