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>
123 lines
3.4 KiB
TypeScript
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(),
|
|
},
|
|
};
|
|
}
|