Release Healer Man 0.1.21
This commit is contained in:
1 parent
699fc858c9
commit
f6f2375145
210 files changed
+976039
-6247
No files matched your search
@@ -0,0 +1,406 @@
|
||||
import type {
|
||||
DungeonDefinition,
|
||||
DungeonEncounterSpawnGroupDefinition,
|
||||
} from "./dungeonTypes";
|
||||
import type { DungeonEncounterDefinition } from "./dungeonEncounterTypes";
|
||||
import type { DungeonWorldObjectDefinition } from "./dungeonWorldObjects";
|
||||
import type { PopulationEntityDefinition } from "./mobPopulation";
|
||||
|
||||
export type RunewakerEncounterContentFidelity = "source-exact" | "source-backed-adapted";
|
||||
|
||||
export interface RunewakerEncounterSourceFile {
|
||||
readonly path: string;
|
||||
readonly sha256: string;
|
||||
}
|
||||
|
||||
export interface RunewakerEncounterEvidenceReview {
|
||||
readonly id: string;
|
||||
readonly fidelity: RunewakerEncounterContentFidelity;
|
||||
readonly sourceRows: readonly number[];
|
||||
readonly evidenceIds: readonly string[];
|
||||
readonly sourceFiles: readonly RunewakerEncounterSourceFile[];
|
||||
readonly reviewedBehavior: string;
|
||||
readonly adaptations: readonly string[];
|
||||
readonly remainingGaps: readonly string[];
|
||||
}
|
||||
|
||||
interface RunewakerEncounterEntityAlias {
|
||||
readonly id: string;
|
||||
readonly sourceEntityId: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface RunewakerEncounterContentPackage {
|
||||
readonly dungeonId: string;
|
||||
readonly encounters: readonly DungeonEncounterDefinition[];
|
||||
readonly spawnGroups: readonly DungeonEncounterSpawnGroupDefinition[];
|
||||
readonly worldObjects: readonly DungeonWorldObjectDefinition[];
|
||||
readonly entityAliases: readonly RunewakerEncounterEntityAlias[];
|
||||
readonly reviews: readonly RunewakerEncounterEvidenceReview[];
|
||||
}
|
||||
|
||||
const FORSAKEN_PATROL_GROUP_IDS = Object.freeze([
|
||||
"forsaken-abbey:follower-a",
|
||||
"forsaken-abbey:follower-b",
|
||||
"forsaken-abbey:follower-c",
|
||||
] as const);
|
||||
|
||||
const FORSAKEN_PATROL_ENCOUNTER = Object.freeze({
|
||||
id: "forsaken-abbey:follower-patrols",
|
||||
dungeonId: "forsaken-abbey",
|
||||
initialPhase: "initializing",
|
||||
resetPolicy: { bossWipePhase: "initializing", checkpointPhase: "initializing" },
|
||||
rules: [
|
||||
{
|
||||
id: "create-source-formations",
|
||||
trigger: { kind: "tick" },
|
||||
guards: [{ kind: "phase", phases: ["initializing"] }],
|
||||
actions: [
|
||||
{ id: "enter-formation-hold", kind: "set-phase", phase: "formation-hold" },
|
||||
...FORSAKEN_PATROL_GROUP_IDS.map((spawnGroupId, index) => ({
|
||||
id: `activate-follower-${String.fromCharCode(97 + index)}`,
|
||||
kind: "activate-spawn-group" as const,
|
||||
spawnGroupId,
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "release-source-formations",
|
||||
trigger: { kind: "elapsed", afterMs: 2_000 },
|
||||
guards: [{ kind: "phase", phases: ["formation-hold"] }],
|
||||
actions: [{ id: "enter-patrol-phase", kind: "set-phase", phase: "patrols-active" }],
|
||||
},
|
||||
],
|
||||
} as const satisfies DungeonEncounterDefinition);
|
||||
|
||||
const FORSAKEN_PATROL_SPAWN_GROUPS = Object.freeze([
|
||||
{
|
||||
id: FORSAKEN_PATROL_GROUP_IDS[0],
|
||||
spawns: [
|
||||
{
|
||||
id: "forsaken-abbey:follower-a:ghoul",
|
||||
entityId: "rw-fa-100653",
|
||||
position: [119.523032, 4.05115, 5.524107],
|
||||
yaw: 4.865415815,
|
||||
spawnMask: 1,
|
||||
},
|
||||
{
|
||||
id: "forsaken-abbey:follower-a:spider",
|
||||
entityId: "rw-fa-100393",
|
||||
position: [121.104334, 4.05115, 5.767995],
|
||||
yaw: 4.865415815,
|
||||
spawnMask: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: FORSAKEN_PATROL_GROUP_IDS[1],
|
||||
spawns: [
|
||||
{
|
||||
id: "forsaken-abbey:follower-b:ghoul",
|
||||
entityId: "rw-fa-100653",
|
||||
position: [76.438701, -6.138638, 17.204093],
|
||||
yaw: 4.828148071,
|
||||
spawnMask: 1,
|
||||
},
|
||||
{
|
||||
id: "forsaken-abbey:follower-b:spider",
|
||||
entityId: "rw-fa-100393",
|
||||
position: [78.027993, -6.138638, 17.388895],
|
||||
yaw: 4.828148071,
|
||||
spawnMask: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: FORSAKEN_PATROL_GROUP_IDS[2],
|
||||
spawns: [
|
||||
{
|
||||
id: "forsaken-abbey:follower-c:ghoul",
|
||||
entityId: "rw-fa-100653",
|
||||
position: [129.755498, -15.077516, -74.285004],
|
||||
yaw: 1.132059978,
|
||||
spawnMask: 1,
|
||||
},
|
||||
{
|
||||
id: "forsaken-abbey:follower-c:spider",
|
||||
entityId: "rw-fa-100393",
|
||||
position: [128.307036, -15.077516, -73.60533],
|
||||
yaw: 1.132059978,
|
||||
spawnMask: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly DungeonEncounterSpawnGroupDefinition[]);
|
||||
|
||||
const TREASURE_TROVE_DRAWBRIDGE_ID = "treasure-trove:drawbridge";
|
||||
const TREASURE_TROVE_SWITCH_ID = "treasure-trove:drawbridge-switch";
|
||||
|
||||
const TREASURE_TROVE_WORLD_OBJECTS = Object.freeze([
|
||||
{
|
||||
id: TREASURE_TROVE_DRAWBRIDGE_ID,
|
||||
name: "Drawbridge",
|
||||
kind: "bridge",
|
||||
position: [588.054827, -4.084724, -6.244854],
|
||||
yaw: 3.024162567,
|
||||
halfExtents: [3.5, 0.35, 5.5],
|
||||
initialState: "raised",
|
||||
states: [
|
||||
{ id: "raised", label: "Raised", visible: true, blocksMovement: true, blocksSight: false },
|
||||
{ id: "lowered", label: "Lowered", visible: true, blocksMovement: false, blocksSight: false },
|
||||
],
|
||||
visual: { color: "#6b5540", emissive: "#1b1209" },
|
||||
source: {
|
||||
fidelity: "source-backed-adapted",
|
||||
files: [
|
||||
"scripts/runewaker-pipeline/snapshots/treasure-trove-zone-108.json",
|
||||
"src/assets/game/dungeons/treasure-trove/population-import-report.json",
|
||||
"preserved RuneWaker Resource/luascript/01159.lua (sha256 23314a25d9654ec34a7d25de588974453f415c235804cc64e7240bf4e03423b6)",
|
||||
],
|
||||
templateId: 112098,
|
||||
spawnId: 5952428,
|
||||
note: "Source placement and state transition are preserved; the collision box and procedural visual are HealerMan adapters.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: TREASURE_TROVE_SWITCH_ID,
|
||||
name: "Drawbridge Switch",
|
||||
kind: "switch",
|
||||
position: [585.282218, -4.084724, -6.571043],
|
||||
yaw: 1.470568359,
|
||||
halfExtents: [0.55, 0.8, 0.55],
|
||||
initialState: "locked",
|
||||
states: [
|
||||
{ id: "locked", label: "Locked by Boulder Sidaar", visible: true, blocksMovement: false, interactionEnabled: false },
|
||||
{ id: "ready", label: "Ready", visible: true, blocksMovement: false, interactionEnabled: true },
|
||||
{ id: "activated", label: "Activated", visible: true, blocksMovement: false, interactionEnabled: false },
|
||||
],
|
||||
interaction: {
|
||||
prompt: "Lower the drawbridge",
|
||||
range: 2.5,
|
||||
signalId: "treasure-trove:drawbridge-activated",
|
||||
allowedStates: ["ready"],
|
||||
oneShot: true,
|
||||
instructions: "Defeat Boulder Sidaar, then interact to lower the nearby drawbridge.",
|
||||
},
|
||||
visual: { color: "#a76e2a", emissive: "#3a2108" },
|
||||
source: {
|
||||
fidelity: "source-backed-adapted",
|
||||
files: [
|
||||
"scripts/runewaker-pipeline/snapshots/treasure-trove-zone-108.json",
|
||||
"src/assets/game/dungeons/treasure-trove/mechanics-audit.json",
|
||||
"preserved RuneWaker Resource/luascript/01159.lua (sha256 23314a25d9654ec34a7d25de588974453f415c235804cc64e7240bf4e03423b6)",
|
||||
],
|
||||
templateId: 112100,
|
||||
spawnId: 5952438,
|
||||
note: "Source placement and boss-gated interaction are preserved; interaction range and procedural visual are HealerMan adapters.",
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly DungeonWorldObjectDefinition[]);
|
||||
|
||||
const TREASURE_TROVE_DRAWBRIDGE_ENCOUNTER = Object.freeze({
|
||||
id: "treasure-trove:drawbridge-sequence",
|
||||
dungeonId: "treasure-trove",
|
||||
initialPhase: "boss-active",
|
||||
initialState: {
|
||||
worldObjectStates: {
|
||||
[TREASURE_TROVE_DRAWBRIDGE_ID]: "raised",
|
||||
[TREASURE_TROVE_SWITCH_ID]: "locked",
|
||||
},
|
||||
},
|
||||
resetPolicy: { bossWipePhase: "boss-active", checkpointPhase: "boss-active" },
|
||||
rules: [
|
||||
{
|
||||
id: "boulder-sidaar-defeated",
|
||||
trigger: { kind: "entity-death", entityId: "rw-treasure-trove-spawn-5952494" },
|
||||
guards: [{ kind: "world-object-state", objectId: TREASURE_TROVE_SWITCH_ID, states: ["locked"] }],
|
||||
actions: [
|
||||
{ id: "arm-drawbridge-switch", kind: "set-phase", phase: "switch-ready", persistence: "run" },
|
||||
{
|
||||
id: "unlock-drawbridge-switch",
|
||||
kind: "set-world-object-state",
|
||||
objectId: TREASURE_TROVE_SWITCH_ID,
|
||||
state: "ready",
|
||||
persistence: "run",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "activate-drawbridge-switch",
|
||||
trigger: { kind: "interact", objectId: TREASURE_TROVE_SWITCH_ID },
|
||||
guards: [{ kind: "world-object-state", objectId: TREASURE_TROVE_SWITCH_ID, states: ["ready"] }],
|
||||
actions: [
|
||||
{ id: "complete-drawbridge-sequence", kind: "set-phase", phase: "complete", persistence: "run" },
|
||||
{
|
||||
id: "mark-drawbridge-switch-activated",
|
||||
kind: "set-world-object-state",
|
||||
objectId: TREASURE_TROVE_SWITCH_ID,
|
||||
state: "activated",
|
||||
persistence: "run",
|
||||
},
|
||||
{
|
||||
id: "lower-treasure-trove-drawbridge",
|
||||
kind: "set-world-object-state",
|
||||
objectId: TREASURE_TROVE_DRAWBRIDGE_ID,
|
||||
state: "lowered",
|
||||
persistence: "run",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "restore-ready-phase-after-reset",
|
||||
trigger: { kind: "tick" },
|
||||
guards: [
|
||||
{ kind: "phase", phases: ["boss-active"] },
|
||||
{ kind: "world-object-state", objectId: TREASURE_TROVE_SWITCH_ID, states: ["ready"] },
|
||||
],
|
||||
actions: [{ id: "restore-switch-ready-phase", kind: "set-phase", phase: "switch-ready" }],
|
||||
},
|
||||
{
|
||||
id: "restore-complete-phase-after-reset",
|
||||
trigger: { kind: "tick" },
|
||||
guards: [
|
||||
{ kind: "phase", phases: ["boss-active", "switch-ready"] },
|
||||
{ kind: "world-object-state", objectId: TREASURE_TROVE_DRAWBRIDGE_ID, states: ["lowered"] },
|
||||
],
|
||||
actions: [{ id: "restore-drawbridge-complete-phase", kind: "set-phase", phase: "complete" }],
|
||||
},
|
||||
],
|
||||
} as const satisfies DungeonEncounterDefinition);
|
||||
|
||||
const FORSAKEN_REVIEW = Object.freeze({
|
||||
id: "forsaken-abbey:follower-formations-review",
|
||||
fidelity: "source-backed-adapted",
|
||||
sourceRows: [12230, 12236, 12238],
|
||||
evidenceIds: [
|
||||
"signal:forsaken-abbey:template:110426:autoplot:f355bf45c2e5",
|
||||
"signal:forsaken-abbey:template:110426:autoplot:191cb880e4a9",
|
||||
"signal:forsaken-abbey:template:110426:autoplot:8cb0866b5187",
|
||||
],
|
||||
sourceFiles: [
|
||||
{ path: "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json", sha256: "3d19cf799b376b2cb2e0eccc4b509da22821e55f0a0581ce6406643759d37874" },
|
||||
{ path: "src/assets/game/dungeons/forsaken-abbey/mechanics-audit.json", sha256: "e6069c60e63de77b4f32731d8888a4025c1d5b916bfa699b206c18d16738beca" },
|
||||
{ path: "preserved RuneWaker Resource/luascript/00571.lua", sha256: "110efc48ad7a915ca76d269ec577a56c0e9e7b2edced0f146720a446aba33cf7" },
|
||||
{ path: "preserved RuneWaker Resource/luascript/00727.lua", sha256: "10e2e3e315e54dd0d7340fb314d88602a9692985ad7a67e79099a83bef0e387a" },
|
||||
],
|
||||
reviewedBehavior: "Each of three source initialization triggers creates a two-member Ghoul/Carrion Spider formation, holds it for two seconds, then hands it to an endless flag patrol.",
|
||||
adaptations: [
|
||||
"Source sleep units are converted from tenths of a second to a 2,000 ms encounter phase.",
|
||||
"Formation offsets are transformed into packaged dungeon coordinates and use projected trigger heights.",
|
||||
"The source-only level-scaled Ghoul 100653 reuses the packaged canonical Ghoul 100072 visual and combat definition.",
|
||||
],
|
||||
remainingGaps: [
|
||||
"The flag-path patrol primitive is not represented by encounter spawn groups, so the formations remain at their source start positions after the hold phase.",
|
||||
"Original reset/respawn cadence is not exposed by the preserved scripts and remains dungeon-reset scoped.",
|
||||
],
|
||||
} as const satisfies RunewakerEncounterEvidenceReview);
|
||||
|
||||
const TREASURE_TROVE_REVIEW = Object.freeze({
|
||||
id: "treasure-trove:drawbridge-review",
|
||||
fidelity: "source-backed-adapted",
|
||||
sourceRows: [5952428, 5952438],
|
||||
evidenceIds: [
|
||||
"signal:treasure-trove:template:101533:autoplot:3a096d804fa4",
|
||||
"signal:treasure-trove:template:112100:autoplot:9520ea9af0f5",
|
||||
],
|
||||
sourceFiles: [
|
||||
{ path: "scripts/runewaker-pipeline/snapshots/treasure-trove-zone-108.json", sha256: "aca7a3e57cbe704a6624f722c8060dc8e7be5d2014240781bc4668b162225a1e" },
|
||||
{ path: "src/assets/game/dungeons/treasure-trove/mechanics-audit.json", sha256: "a496185046e28657d76bd1d64353dcae070bee45c3e65a265f927a94f0c9fd86" },
|
||||
{ path: "preserved RuneWaker Resource/luascript/01159.lua", sha256: "23314a25d9654ec34a7d25de588974453f415c235804cc64e7240bf4e03423b6" },
|
||||
],
|
||||
reviewedBehavior: "Boulder Sidaar's death arms source switch 112100; touching that switch activates both it and nearby drawbridge 112098 exactly once.",
|
||||
adaptations: [
|
||||
"Source placements use the package's navmesh-projected local coordinates.",
|
||||
"The source motions are represented as explicit object states with procedural collision and visuals.",
|
||||
"The switch prompt is hidden until the boss death instead of showing the source failure message before it is armed.",
|
||||
],
|
||||
remainingGaps: [
|
||||
"Original ACT motion clips and object meshes are not packaged, so raised/lowered visuals use the generic world-object proxy.",
|
||||
"Black Mary's key-gated door remains deferred because the encounter guard model does not expose inventory item 203550.",
|
||||
],
|
||||
} as const satisfies RunewakerEncounterEvidenceReview);
|
||||
|
||||
export const RUNEWAKER_ENCOUNTER_CONTENT_CATALOG = Object.freeze([
|
||||
Object.freeze({
|
||||
dungeonId: "forsaken-abbey",
|
||||
encounters: [FORSAKEN_PATROL_ENCOUNTER],
|
||||
spawnGroups: FORSAKEN_PATROL_SPAWN_GROUPS,
|
||||
worldObjects: [],
|
||||
entityAliases: [{ id: "rw-fa-100653", sourceEntityId: "rw-fa-100072", name: "Ghoul (source summon 100653)" }],
|
||||
reviews: [FORSAKEN_REVIEW],
|
||||
}),
|
||||
Object.freeze({
|
||||
dungeonId: "treasure-trove",
|
||||
encounters: [TREASURE_TROVE_DRAWBRIDGE_ENCOUNTER],
|
||||
spawnGroups: [],
|
||||
worldObjects: TREASURE_TROVE_WORLD_OBJECTS,
|
||||
entityAliases: [],
|
||||
reviews: [TREASURE_TROVE_REVIEW],
|
||||
}),
|
||||
] as const satisfies readonly RunewakerEncounterContentPackage[]);
|
||||
|
||||
export const RUNEWAKER_ENCOUNTER_CONTENT_LEDGER = Object.freeze({
|
||||
sourceReportDeferredRows: 4_113,
|
||||
mappedDeferredRows: RUNEWAKER_ENCOUNTER_CONTENT_CATALOG.reduce(
|
||||
(count, entry) => count + entry.reviews.reduce((subtotal, review) => subtotal + review.sourceRows.length, 0),
|
||||
0,
|
||||
),
|
||||
remainingDeferredRows: 4_108,
|
||||
sourceReportUnresolvedEvidence: 1_136,
|
||||
reviewedEvidenceRecords: RUNEWAKER_ENCOUNTER_CONTENT_CATALOG.reduce(
|
||||
(count, entry) => count + entry.reviews.reduce((subtotal, review) => subtotal + review.evidenceIds.length, 0),
|
||||
0,
|
||||
),
|
||||
remainingUnresolvedEvidence: 1_131,
|
||||
});
|
||||
|
||||
const CONTENT_BY_DUNGEON: ReadonlyMap<string, RunewakerEncounterContentPackage> = new Map(
|
||||
RUNEWAKER_ENCOUNTER_CONTENT_CATALOG.map((entry) => [entry.dungeonId, entry] as const),
|
||||
);
|
||||
|
||||
function mergeUniqueById<T extends { readonly id: string }>(
|
||||
label: string,
|
||||
existing: readonly T[],
|
||||
additions: readonly T[],
|
||||
): readonly T[] {
|
||||
const merged = new Map(existing.map((entry) => [entry.id, entry] as const));
|
||||
for (const addition of additions) {
|
||||
if (merged.has(addition.id)) throw new Error(`Duplicate ${label} ${addition.id}`);
|
||||
merged.set(addition.id, addition);
|
||||
}
|
||||
return Object.freeze([...merged.values()]);
|
||||
}
|
||||
|
||||
function aliasedEntities(
|
||||
definition: DungeonDefinition,
|
||||
aliases: readonly RunewakerEncounterEntityAlias[],
|
||||
): Readonly<Record<string, PopulationEntityDefinition>> {
|
||||
const result: Record<string, PopulationEntityDefinition> = { ...definition.entities };
|
||||
for (const alias of aliases) {
|
||||
if (result[alias.id]) throw new Error(`Duplicate encounter entity ${alias.id}`);
|
||||
const source = definition.entities[alias.sourceEntityId];
|
||||
if (!source) throw new Error(`Encounter entity alias ${alias.id} references missing ${alias.sourceEntityId}`);
|
||||
result[alias.id] = Object.freeze({ ...source, id: alias.id, name: alias.name });
|
||||
}
|
||||
return Object.freeze(result);
|
||||
}
|
||||
|
||||
/** Adds only reviewed, source-linked encounter content; generated package files remain immutable. */
|
||||
export function withRunewakerEncounterContent(definition: DungeonDefinition): DungeonDefinition {
|
||||
const content = CONTENT_BY_DUNGEON.get(definition.id);
|
||||
if (!content) return definition;
|
||||
if (definition.expansion !== "runewaker") {
|
||||
throw new Error(`RuneWaker encounter content cannot attach to ${definition.expansion} dungeon ${definition.id}`);
|
||||
}
|
||||
return Object.freeze({
|
||||
...definition,
|
||||
entities: aliasedEntities(definition, content.entityAliases),
|
||||
encounters: mergeUniqueById("encounter", definition.encounters ?? [], content.encounters),
|
||||
encounterSpawnGroups: mergeUniqueById(
|
||||
"encounter spawn group",
|
||||
definition.encounterSpawnGroups ?? [],
|
||||
content.spawnGroups,
|
||||
),
|
||||
serverGameObjects: mergeUniqueById("world object", definition.serverGameObjects, content.worldObjects),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user