forked from phenom/healer-man
128 lines
5.1 KiB
TypeScript
128 lines
5.1 KiB
TypeScript
import type { DungeonDefinition } from "../dungeonTypes";
|
|
import type { PopulationEntityDefinition } from "../mobPopulation";
|
|
import {
|
|
RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
STORMWIND_STOCKADE_DUNGEON_ID,
|
|
} from "../dungeonContentSource";
|
|
import {
|
|
BITE_SIZED_STOCKADES_BOSS_CANDIDATES,
|
|
createBiteSizedStockadesDefinition,
|
|
} from "./biteSizedStockades";
|
|
|
|
export type RandomizedCreatureKind = "mob" | "boss";
|
|
|
|
export interface RandomizedBiteSizedCreature {
|
|
readonly key: string;
|
|
readonly sourceDungeonId: string;
|
|
readonly sourceDungeonTitle: string;
|
|
readonly entityId: string;
|
|
readonly name: string;
|
|
readonly kind: RandomizedCreatureKind;
|
|
readonly serverEntry: number | null;
|
|
}
|
|
|
|
function seedHash(seed: string): number {
|
|
let hash = 0x811c9dc5;
|
|
for (let index = 0; index < seed.length; index += 1) {
|
|
hash ^= seed.charCodeAt(index);
|
|
hash = Math.imul(hash, 0x01000193);
|
|
}
|
|
return hash >>> 0;
|
|
}
|
|
|
|
function placedEntityIds(source: DungeonDefinition): ReadonlySet<string> {
|
|
return new Set([
|
|
...source.staticSpawns.map((spawn) => spawn.entityId),
|
|
...source.roamingPacks.flatMap((pack) => pack.members.map((member) => member.entityId)),
|
|
]);
|
|
}
|
|
|
|
/** The immutable review catalog used by both seeded generation and the GM readiness screen. */
|
|
export function randomizedBiteSizedCreatureCatalog(
|
|
source: DungeonDefinition,
|
|
): readonly RandomizedBiteSizedCreature[] {
|
|
if (source.id !== STORMWIND_STOCKADE_DUNGEON_ID) {
|
|
throw new Error(`Randomized Bite-Sized Stockades requires ${STORMWIND_STOCKADE_DUNGEON_ID}.`);
|
|
}
|
|
const placed = placedEntityIds(source);
|
|
const bossEntityIds = new Set(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((candidate) => {
|
|
const spawn = source.staticSpawns.find((entry) => entry.id === candidate.sourceSpawnId);
|
|
if (!spawn) throw new Error(`Stockades boss source is missing: ${candidate.sourceSpawnId}`);
|
|
return spawn.entityId;
|
|
}));
|
|
|
|
return Object.values(source.entities)
|
|
.filter((entity) => placed.has(entity.id) && (entity.kind === "mob" || bossEntityIds.has(entity.id)))
|
|
.map((entity) => ({
|
|
key: `${source.id}:${entity.id}`,
|
|
sourceDungeonId: source.id,
|
|
sourceDungeonTitle: source.title,
|
|
entityId: entity.id,
|
|
name: entity.name,
|
|
kind: entity.kind,
|
|
serverEntry: entity.combat?.serverEntry ?? null,
|
|
}))
|
|
.sort((left, right) => left.kind.localeCompare(right.kind) || left.name.localeCompare(right.name));
|
|
}
|
|
|
|
/**
|
|
* Rerolls every ordinary corridor spawn from the complete placed Stockades mob
|
|
* pool. The existing bite-sized builder remains the authority for geometry,
|
|
* navigation, sealed wings, and the independently seeded five-boss roll.
|
|
*/
|
|
export function createRandomizedBiteSizedStockadesDefinition(
|
|
source: DungeonDefinition,
|
|
seed: string,
|
|
): DungeonDefinition {
|
|
const base = createBiteSizedStockadesDefinition(source, seed);
|
|
const catalog = randomizedBiteSizedCreatureCatalog(source);
|
|
const mobCandidates = catalog.filter((candidate) => candidate.kind === "mob");
|
|
if (!mobCandidates.length) throw new Error("Randomized Bite-Sized Stockades has no ordinary mob candidates.");
|
|
const bossSpawnId = base.bosses[0]?.id;
|
|
const randomizedSpawns = base.staticSpawns.map((spawn) => {
|
|
if (spawn.id === bossSpawnId) return spawn;
|
|
const candidate = mobCandidates[seedHash(`${seed}:${spawn.id}`) % mobCandidates.length];
|
|
return { ...spawn, entityId: candidate.entityId };
|
|
});
|
|
const usedEntityIds = new Set(randomizedSpawns.map((spawn) => spawn.entityId));
|
|
const entities = Object.fromEntries(
|
|
Object.entries(source.entities).filter(([entityId]) => usedEntityIds.has(entityId)),
|
|
) as Readonly<Record<string, PopulationEntityDefinition>>;
|
|
|
|
return {
|
|
...base,
|
|
id: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
title: "Randomized Bite-Sized: Cellblock Shuffle",
|
|
presentation: {
|
|
...base.presentation,
|
|
location: "Stormwind Stockade · Shifting Cellblock",
|
|
theme: "Randomized Bite-Sized",
|
|
summary: "Every enemy slot and the final warden are rerolled for a fast, unpredictable one-boss run.",
|
|
loadingMessage: "Shuffling prisoners, guards, and the final warden...",
|
|
},
|
|
entities,
|
|
staticSpawns: randomizedSpawns,
|
|
encounters: (base.encounters ?? []).map((encounter) => ({
|
|
...encounter,
|
|
id: `${RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID}:boss-progression`,
|
|
dungeonId: RANDOMIZED_BITE_SIZED_STOCKADES_DUNGEON_ID,
|
|
source: encounter.source ? {
|
|
...encounter.source,
|
|
files: ["src/game/dungeons/randomizedBiteSizedStockades.ts"],
|
|
reviewedBehavior: "Every corridor slot and the final Stockades boss are deterministic rolls from the run seed.",
|
|
adaptations: [
|
|
...encounter.source.adaptations,
|
|
"Each ordinary spawn slot independently selects a placed Stockades mob archetype from the immutable review catalog.",
|
|
],
|
|
} : encounter.source,
|
|
})),
|
|
validation: {
|
|
...base.validation,
|
|
warnings: [
|
|
...base.validation.warnings,
|
|
"Randomized creature identities are Healer Man-authored and deterministic per run seed.",
|
|
],
|
|
},
|
|
};
|
|
}
|