forked from phenom/healer-man
Release Healer Man 0.1.58
This commit is contained in:
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.",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user