Release Healer Man 0.1.58

This commit is contained in:
phenom committed 2026-09-20 16:46:17 -04:00
1 parent 14e105642c
commit 246b4832b0
38 files changed
+952 -93

No files matched your search

@@ -0,0 +1,127 @@
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.",
],
},
};
}