#!/usr/bin/env node import { access, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { DUNGEON_DEFINITIONS } from "../../src/game/dungeonRegistry"; import { validateDungeonEncounterDefinition } from "../../src/game/dungeonEncounterRuntime"; import { validateDungeonWorldObject } from "../../src/game/dungeonWorldObjects"; import type { DungeonDefinition } from "../../src/game/dungeonTypes"; import type { DungeonEncounterDefinition } from "../../src/game/dungeonEncounterTypes"; interface CampaignCoverageEntry { readonly dungeonId: string; readonly source: "azerothcore" | "procedural-fallback"; } interface CampaignCatalog { readonly coverage: readonly CampaignCoverageEntry[]; } interface GameObjectAssetManifest { readonly summary: { readonly uniqueAssets: number; readonly nonVisualInstances: number; readonly unresolvedModelDisplayIds: number; readonly unresolvedBoundDisplayIds: number; }; } interface ScriptCatalogEntry { readonly sourceFile: string; } interface ScriptCatalog { readonly scripts: Readonly>; } interface RuntimeDraftEntity { readonly scriptName?: string; } interface SourceGameObject { readonly guid?: number; readonly id?: number; } interface SourceGameObjectTemplate { readonly entry?: number; readonly type?: number; } interface RuntimeDraft { readonly entities?: readonly RuntimeDraftEntity[]; readonly gameObjects?: readonly SourceGameObject[]; readonly gameObjectTemplates?: readonly SourceGameObjectTemplate[]; } type SourceFeature = | "area-triggers" | "cinematics-dialogue" | "doors-gates-bridges" | "escorts-waypoints" | "gossip-quests-items" | "timed-events" | "vehicles-passengers" | "waves-summons"; interface DungeonFunctionalityResult { readonly id: string; readonly title: string; readonly expansion: string; readonly source: CampaignCoverageEntry["source"]; readonly readiness: | "source-blocked" | "source-limited-adapted" | "implementation-required" | "playtest-required" | "validated"; readonly validationStatus: string; readonly population: { readonly entities: number; readonly staticSpawns: number; readonly roamingPacks: number; readonly objectives: number; }; readonly combat: { readonly executable: boolean; readonly entitiesWithoutCombat: readonly string[]; }; readonly encounters: { readonly definitions: number; readonly rules: number; readonly bossProgressionDefinitions: number; readonly instanceOrchestrationDefinitions: number; readonly instanceOrchestrationDefinitionsWithRules: number; readonly sourceExactInstanceOrchestrationDefinitions: number; readonly sourceBackedAdaptedInstanceOrchestrationDefinitions: number; readonly sourceExactDefinitions: number; readonly sourceBackedAdaptedDefinitions: number; readonly authoredDefinitions: number; readonly declaredObjectiveCompletion: boolean; readonly checkpointState: boolean; readonly completionState: boolean; readonly reviewedBehaviors: readonly string[]; readonly adaptations: readonly string[]; readonly remainingGaps: readonly string[]; readonly instanceAdaptations: readonly string[]; readonly instanceRemainingGaps: readonly string[]; readonly spawnGroups: number; readonly worldObjects: number; readonly sourceExactWorldObjects: number; readonly sourceBackedAdaptedWorldObjects: number; readonly authoredWorldObjects: number; readonly clientDisplayWorldObjects: number; readonly clientModelPathWorldObjects: number; readonly clientBoundedWorldObjects: number; readonly malformedWorldObjects: number; }; readonly sourceEvidence: { readonly rawGameObjects: number; readonly actionableGameObjects: number; readonly unresolvedGameObjectTemplates: number; readonly gameObjectTypes: Readonly>; readonly scriptNames: number; readonly resolvedScriptFiles: readonly string[]; readonly unresolvedScriptNames: readonly string[]; readonly features: readonly SourceFeature[]; }; readonly navigation: { readonly assetPresent: boolean; readonly authoredLinks: number; }; readonly contractIssues: readonly string[]; readonly requiredChanges: readonly string[]; } const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const workRoot = path.resolve(projectRoot, "../HealerMan-Storage/pipeline-work/dungeons"); const sourceScriptsRoot = path.join(workRoot, "azerothcore-scripts/src/server/scripts"); const reportJsonFile = path.join(projectRoot, "dungeon-pipeline", "WOW_DUNGEON_FUNCTIONALITY_REPORT.json"); const reportMarkdownFile = path.join(projectRoot, "dungeon-pipeline", "WOW_DUNGEON_FUNCTIONALITY_REPORT.md"); const wowExporterFile = path.resolve( projectRoot, "../wow335a/dungeon-export-work/tools/wow.export-0.2.19/wow.export.exe", ); const GAME_OBJECT_TYPE_NAMES: Readonly> = Object.freeze({ 0: "door", 1: "button", 2: "questgiver", 3: "chest-resource", 4: "binder", 5: "generic", 6: "trap", 7: "chair", 8: "spell-focus", 9: "text", 10: "goober", 11: "transport", 12: "area-damage", 13: "camera", 14: "map-object", 15: "moving-transport", 16: "duel-arbiter", 17: "fishing-node", 18: "ritual", 19: "mailbox", 20: "auction-house", 21: "guard-post", 22: "spellcaster", 23: "meeting-stone", 24: "flag-stand", 25: "fishing-hole", 26: "flag-drop", 27: "mini-game", 28: "lottery-kiosk", 29: "capture-point", 30: "aura-generator", 31: "dungeon-difficulty", 32: "barber-chair", 33: "destructible-building", 34: "guild-bank", 35: "trapdoor", }); const ACTIONABLE_GAME_OBJECT_TYPES = new Set([ 0, 1, 2, 3, 6, 10, 11, 12, 13, 15, 18, 22, 23, 31, 33, 35, ]); const FEATURE_PATTERNS: ReadonlyArray = [ ["area-triggers", /\b(?:AreaTrigger|OnTrigger)\b/i], ["cinematics-dialogue", /\b(?:Talk|MonsterYell|SendMovieStart|SAY_|EMOTE_)\b/i], ["doors-gates-bridges", /\b(?:HandleGameObject|SetGoState|GO_STATE_|DoorData|door|gate|drawbridge)\b/i], ["escorts-waypoints", /\b(?:npc_escortAI|EscortAI|WaypointReached|MovementInform|MovePoint|escort)\b/i], ["gossip-quests-items", /\b(?:OnGossip|GossipHello|QuestAccept|QuestReward|HasItemCount|DestroyItemCount|GameObjectScript)\b/i], ["timed-events", /\b(?:EventMap|ScheduleEvent|RescheduleEvent|EVENT_[A-Z0-9_]+)\b/i], ["vehicles-passengers", /\b(?:VehicleAI|EnterVehicle|ExitVehicle|PassengerBoarded|vehicle)\b/i], ["waves-summons", /\b(?:SummonCreature|SummonGameObject|DoSummon|summon|wave)\b/i], ]; const GENERIC_SOURCE_DIRECTORIES = new Set([ "EasternKingdoms", "Kalimdor", "Northrend", "Outland", "World", ]); const INSTANCE_SOURCE_ALIASES: Readonly> = Object.freeze({ "escape-from-durnholde": ["oldhillsbrad"], "stormwind-stockade": ["thestockade"], }); // Add a dungeon only after its complete production route, wipe/reset behavior, // checkpoint reload, and completion state have recorded evidence in // PLAYTEST_NOTES.md. Asset/package validation is intentionally not sufficient. const FUNCTIONALLY_VALIDATED_DUNGEON_IDS = new Set(); const PARTIALLY_RECOVERED_ASCENSION_DUNGEON_IDS = new Set([ "tinkertech-showdown", "frozen-reach", "forgotten-mine", ]); async function exists(file: string): Promise { try { await access(file); return true; } catch { return false; } } async function readJson(file: string): Promise { return JSON.parse(await readFile(file, "utf8")) as T; } function relativeFile(file: string): string { return path.relative(projectRoot, file).replace(/\\/g, "/"); } async function runtimeDraftFor(dungeonId: string): Promise { const file = path.join(workRoot, dungeonId, "runtime-draft.json"); return await exists(file) ? readJson(file) : null; } async function filesNamed(root: string, pattern: RegExp): Promise { if (!await exists(root)) return []; const files: string[] = []; for (const entry of await readdir(root, { withFileTypes: true })) { const file = path.join(root, entry.name); if (entry.isDirectory()) files.push(...await filesNamed(file, pattern)); else if (pattern.test(entry.name)) files.push(file); } return files; } function normalizedSourceName(value: string): string { return value.toLowerCase().replace(/^instance[_-]?/, "").replace(/[^a-z0-9]+/g, ""); } function matchingInstanceFiles( definition: DungeonDefinition, instanceFiles: readonly string[], ): readonly string[] { const variants = [ normalizedSourceName(definition.id), normalizedSourceName(definition.title), ...(INSTANCE_SOURCE_ALIASES[definition.id] ?? []).map(normalizedSourceName), ].filter((value) => value.length >= 5); return instanceFiles.filter((file) => { const sourceName = normalizedSourceName(path.basename(file, path.extname(file))); return variants.some((variant) => ( sourceName === variant || sourceName.includes(variant) || variant.includes(sourceName) )); }); } async function sourceScriptFiles( definition: DungeonDefinition, draft: RuntimeDraft | null, scriptCatalog: ScriptCatalog, instanceFiles: readonly string[], ): Promise<{ readonly scriptNames: number; readonly files: readonly string[]; readonly unresolved: readonly string[]; readonly source: string; }> { const names = [...new Set((draft?.entities ?? []) .map((entity) => entity.scriptName?.trim()) .filter((name): name is string => Boolean(name)))]; const unresolved = names.filter((name) => !scriptCatalog.scripts[name]?.sourceFile); const directFiles = [...new Set([ ...names .map((name) => scriptCatalog.scripts[name]?.sourceFile) .filter((file): file is string => Boolean(file)) .map((file) => path.resolve(projectRoot, file)), ...matchingInstanceFiles(definition, instanceFiles), ])]; const candidates = new Set(directFiles); for (const directFile of directFiles) { const directory = path.dirname(directFile); if (GENERIC_SOURCE_DIRECTORIES.has(path.basename(directory)) || !await exists(directory)) continue; for (const entry of await readdir(directory, { withFileTypes: true })) { if (entry.isFile() && /\.(?:cpp|h)$/i.test(entry.name)) candidates.add(path.join(directory, entry.name)); } } const files: string[] = []; const source: string[] = []; for (const file of candidates) { if (!await exists(file)) continue; files.push(relativeFile(file)); source.push(await readFile(file, "utf8")); } return { scriptNames: names.length, files: files.sort(), unresolved: unresolved.sort(), source: source.join("\n"), }; } function sourceGameObjectEvidence(draft: RuntimeDraft | null): { readonly raw: number; readonly actionable: number; readonly unresolvedTemplates: number; readonly types: Readonly>; } { const templates = new Map((draft?.gameObjectTemplates ?? []) .map((template) => [Number(template.entry), template])); let actionable = 0; let unresolvedTemplates = 0; const types: Record = {}; for (const object of draft?.gameObjects ?? []) { const template = templates.get(Number(object.id)); if (!template) { unresolvedTemplates += 1; types.unknown = (types.unknown ?? 0) + 1; continue; } const type = Number(template.type); const label = GAME_OBJECT_TYPE_NAMES[type] ?? `type-${type}`; types[label] = (types[label] ?? 0) + 1; if (ACTIONABLE_GAME_OBJECT_TYPES.has(type)) actionable += 1; } return { raw: draft?.gameObjects?.length ?? 0, actionable, unresolvedTemplates, types: Object.freeze(Object.fromEntries(Object.entries(types).sort())), }; } function unique(values: readonly string[]): readonly string[] { return [...new Set(values)]; } function encounterCounts(definition: DungeonDefinition): { readonly definitions: number; readonly rules: number; readonly bossProgressionDefinitions: number; readonly instanceOrchestrationDefinitions: number; readonly instanceOrchestrationDefinitionsWithRules: number; readonly sourceExactInstanceOrchestrationDefinitions: number; readonly sourceBackedAdaptedInstanceOrchestrationDefinitions: number; readonly sourceExactDefinitions: number; readonly sourceBackedAdaptedDefinitions: number; readonly authoredDefinitions: number; readonly declaredObjectiveCompletion: boolean; readonly checkpointState: boolean; readonly completionState: boolean; readonly reviewedBehaviors: readonly string[]; readonly adaptations: readonly string[]; readonly remainingGaps: readonly string[]; readonly instanceAdaptations: readonly string[]; readonly instanceRemainingGaps: readonly string[]; readonly spawnGroups: number; readonly malformedWorldObjects: number; } { let malformedWorldObjects = 0; for (const object of definition.serverGameObjects) { try { validateDungeonWorldObject(object); } catch { malformedWorldObjects += 1; } } for (const encounter of definition.encounters ?? []) validateDungeonEncounterDefinition(encounter); const encounters = definition.encounters ?? []; const actions = encounters.flatMap((encounter) => encounter.rules.flatMap((rule) => rule.actions)); const instanceOrchestration = encounters.filter( (encounter) => encounter.source?.scope === "instance-orchestration", ); return { definitions: encounters.length, rules: encounters.reduce((count, encounter) => count + encounter.rules.length, 0), bossProgressionDefinitions: encounters.filter( (encounter) => encounter.source?.scope === "boss-progression", ).length, instanceOrchestrationDefinitions: instanceOrchestration.length, instanceOrchestrationDefinitionsWithRules: instanceOrchestration.filter( (encounter) => encounter.rules.length > 0, ).length, sourceExactInstanceOrchestrationDefinitions: instanceOrchestration.filter( (encounter) => encounter.source?.fidelity === "source-exact", ).length, sourceBackedAdaptedInstanceOrchestrationDefinitions: instanceOrchestration.filter( (encounter) => encounter.source?.fidelity === "source-backed-adapted", ).length, sourceExactDefinitions: encounters.filter( (encounter) => encounter.source?.fidelity === "source-exact", ).length, sourceBackedAdaptedDefinitions: encounters.filter( (encounter) => encounter.source?.fidelity === "source-backed-adapted", ).length, authoredDefinitions: encounters.filter( (encounter) => encounter.source?.fidelity === "healer-man-authored", ).length, declaredObjectiveCompletion: actions.some((action) => action.kind === "complete-objective"), checkpointState: actions.some((action) => action.kind === "set-checkpoint"), completionState: actions.some((action) => ( action.kind === "set-flag" && action.flagId === "dungeon-complete" && action.value )), reviewedBehaviors: unique(encounters.flatMap((encounter) => ( encounter.source?.reviewedBehavior ? [encounter.source.reviewedBehavior] : [] ))), adaptations: unique(encounters.flatMap((encounter) => encounter.source?.adaptations ?? [])), remainingGaps: unique(encounters.flatMap((encounter) => encounter.source?.remainingGaps ?? [])), instanceAdaptations: unique(instanceOrchestration.flatMap( (encounter) => encounter.source?.adaptations ?? [], )), instanceRemainingGaps: unique(instanceOrchestration.flatMap( (encounter) => encounter.source?.remainingGaps ?? [], )), spawnGroups: definition.encounterSpawnGroups?.length ?? 0, malformedWorldObjects, }; } function requiredChangesFor(input: { readonly definition: DungeonDefinition; readonly source: CampaignCoverageEntry["source"]; readonly sourceObjects: ReturnType; readonly features: readonly SourceFeature[]; readonly scriptFiles: readonly string[]; readonly malformedWorldObjects: number; readonly sourceBackedAdaptedWorldObjects: number; readonly clientModelPathWorldObjects: number; readonly uniqueClientModelPaths: number; readonly clientBoundedWorldObjects: number; readonly instanceOrchestrationDefinitions: number; readonly remainingInstanceOrchestrationGaps: readonly string[]; readonly contractIssues: readonly string[]; }): readonly string[] { const { definition, source, sourceObjects, features, scriptFiles, malformedWorldObjects, sourceBackedAdaptedWorldObjects, clientModelPathWorldObjects, uniqueClientModelPaths, clientBoundedWorldObjects, instanceOrchestrationDefinitions, remainingInstanceOrchestrationGaps, contractIssues, } = input; const changes: string[] = []; const needsMoreInstanceWork = instanceOrchestrationDefinitions === 0 || remainingInstanceOrchestrationGaps.length > 0; if (malformedWorldObjects) { changes.push("Remove raw/malformed database rows from the live world-object renderer contract."); } if (source === "procedural-fallback") { changes.push(PARTIALLY_RECOVERED_ASCENSION_DUNGEON_IDS.has(definition.id) ? "Import the still-missing authoritative Ascension creature templates, spawn coordinates, object placements, and server cadence to replace the explicitly documented authored portions; preserve the recovered checksummed identities, spell families, and summon links." : "Acquire and import an authoritative Ascension creature, spawn, object, and encounter snapshot."); } if (sourceObjects.raw > 0 && definition.serverGameObjects.length < sourceObjects.actionable) { changes.push(`Compile ${sourceObjects.actionable - definition.serverGameObjects.length} remaining actionable source object instances into typed world objects.`); } if (sourceBackedAdaptedWorldObjects > 0) { changes.push(`Review ${sourceBackedAdaptedWorldObjects} source-backed adapted objects: export ${uniqueClientModelPaths} unique client models used by ${clientModelPathWorldObjects} instances, resolve ${sourceBackedAdaptedWorldObjects - clientModelPathWorldObjects} missing model paths and ${sourceBackedAdaptedWorldObjects - clientBoundedWorldObjects} missing bounds, then enable reviewed collision and instance-script state transitions.`); } if (scriptFiles.length > 0 && instanceOrchestrationDefinitions === 0) { changes.push("Review and port the dungeon instance/NPC orchestration into serializable DungeonEncounterDefinition rules."); } else if (scriptFiles.length > 0 && remainingInstanceOrchestrationGaps.length > 0) { changes.push("Close the documented remaining instance-orchestration gaps without regressing the runtime-wired source-backed adaptation."); } if (features.includes("doors-gates-bridges") && needsMoreInstanceWork) { changes.push("Bind encounter progress to door, gate, bridge, collision, and line-of-sight state transitions."); } if (features.includes("waves-summons") && needsMoreInstanceWork) { changes.push("Author exact summon groups, wave order, despawn rules, timings, and wipe resets."); } if (features.includes("escorts-waypoints") && needsMoreInstanceWork) { changes.push("Add escort/waypoint movement, pauses, dialogue gates, failure, and resume/reset behavior."); } if (features.includes("vehicles-passengers") && needsMoreInstanceWork) { changes.push("Add source-backed vehicle seats, possession/action controls, passenger events, and dismount cleanup."); } if (features.includes("gossip-quests-items") && needsMoreInstanceWork) { changes.push("Add gossip, quest, item/key, and object-use prerequisites required by the instance flow."); } if (features.includes("area-triggers") && needsMoreInstanceWork) { changes.push("Port required area-trigger volumes and one-shot/reset behavior."); } if (!definition.assets.navigation) { changes.push("Generate and package a navigation asset."); } if (definition.navigationLinks.length === 0 && definition.validation.status !== "green") { changes.push("Run entrance-to-final-boss traversal and author any required off-mesh links or dynamic blockers."); } if (contractIssues.length) { changes.push("Repair population/objective/runtime contract violations before playtesting."); } if (!FUNCTIONALLY_VALIDATED_DUNGEON_IDS.has(definition.id)) { changes.push("Complete desktop runtime, combat, wipe/reset, reload/checkpoint, HUD, collision, and completion playtests before setting validation green."); } return unique(changes); } function readinessFor( definition: DungeonDefinition, source: CampaignCoverageEntry["source"], requiredChanges: readonly string[], instanceOrchestrationDefinitions: number, ): DungeonFunctionalityResult["readiness"] { if (source === "procedural-fallback") { return instanceOrchestrationDefinitions > 0 ? "source-limited-adapted" : "source-blocked"; } if (requiredChanges.some((change) => /Compile|Review|Port|Add |Bind|Author exact/i.test(change))) { return "implementation-required"; } if (!FUNCTIONALLY_VALIDATED_DUNGEON_IDS.has(definition.id) || requiredChanges.length) return "playtest-required"; return "validated"; } function markdownList(values: readonly string[]): string { return values.length ? values.join(", ") : "none"; } function reportMarkdown(report: { readonly generatedAt: string; readonly status: string; readonly summary: Readonly>; readonly tooling: { readonly wowExporter: { readonly available: boolean; readonly expectedPath: string; }; }; readonly capabilityBacklog: readonly { readonly priority: string; readonly capability: string; readonly affectedDungeons: readonly string[]; readonly editTargets: readonly string[]; }[]; readonly dungeons: readonly DungeonFunctionalityResult[]; }): string { const rows = report.dungeons.map((dungeon) => [ "|", dungeon.title, "|", dungeon.source === "azerothcore" ? "source-backed" : "procedural", "|", dungeon.readiness, "|", `${dungeon.encounters.definitions}/${dungeon.encounters.rules}`, "|", `${dungeon.encounters.worldObjects}/${dungeon.sourceEvidence.rawGameObjects}/${dungeon.sourceEvidence.actionableGameObjects}`, "|", markdownList(dungeon.sourceEvidence.features), "|", ].join(" ")); const capabilitySections = report.capabilityBacklog.flatMap((entry) => [ `### ${entry.priority}: ${entry.capability}`, "", `Affected (${entry.affectedDungeons.length}): ${markdownList(entry.affectedDungeons)}`, "", `Edit targets: ${entry.editTargets.map((target) => `\`${target}\``).join(", ")}`, "", ]); const dungeonSections = report.dungeons.flatMap((dungeon) => [ `### ${dungeon.title}`, "", `Readiness: **${dungeon.readiness}**. Runtime encounter definitions/rules: ${dungeon.encounters.definitions}/${dungeon.encounters.rules}. Source objects/actionable: ${dungeon.sourceEvidence.rawGameObjects}/${dungeon.sourceEvidence.actionableGameObjects}.`, "", "Source review findings:", "", ...dungeon.encounters.reviewedBehaviors.map((behavior) => `- ${behavior}`), ...(dungeon.encounters.reviewedBehaviors.length ? [""] : ["- None recorded.", ""]), "Implemented adaptations:", "", ...dungeon.encounters.adaptations.map((adaptation) => `- ${adaptation}`), ...(dungeon.encounters.adaptations.length ? [""] : ["- None recorded.", ""]), "Remaining source-fidelity gaps:", "", ...dungeon.encounters.remainingGaps.map((gap) => `- ${gap}`), ...(dungeon.encounters.remainingGaps.length ? [""] : ["- None recorded.", ""]), "Required next changes:", "", ...dungeon.requiredChanges.map((change) => `- ${change}`), ...(dungeon.requiredChanges.length ? [""] : ["- No remaining functionality gaps detected by this audit.", ""]), ]); return [ "# WoW dungeon functionality validation", "", `Generated: ${report.generatedAt}`, "", `Status: **${report.status}**`, "", "This is the production-fidelity gate. It complements the combat/animation report by auditing live runtime contracts, source game objects, instance scripts, encounters, traversal, resets, and required player interactions.", "", "## Findings", "", `- ${report.summary.dungeons} WoW dungeons audited; ${report.summary.validated} currently meet this functionality gate.`, `- ${report.summary.sourceBacked} have authoritative population sources; ${report.summary.sourceBlocked} are playable, provenance-labeled Ascension adaptations that still require external server data for exact parity.`, `- ${report.summary.dungeonsWithDeclaredBossProgression} dungeons declare boss-progression, checkpoint, and completion actions; ${report.summary.dungeonsWithoutEncounterRuntime} have no encounter runtime. Lifecycle tests and live traversal evidence, not declaration counts, determine executability.`, "- Run completion is consumed by the live HUD: the player may finish looting, then explicitly exit and clear the saved dungeon checkpoint.", `- ${report.summary.dungeonsWithReviewedInstanceOrchestration} ${report.summary.dungeonsWithReviewedInstanceOrchestration === 1 ? "dungeon has" : "dungeons have"} source-reviewed instance behavior; ${report.summary.dungeonsWithInstanceOrchestrationRules} contain orchestration rules beyond the fleet baseline and ${report.summary.dungeonsWithVerifiedNoOpInstanceScripts} have a verified no-op instance script. These are declaration counts, not traversal claims; adaptations and remaining gaps are recorded below.`, `- ${report.summary.dungeonsWithoutReviewedInstanceOrchestration} dungeons still lack an instance-level source review; ${report.summary.dungeonsWithDocumentedInstanceGaps} retain explicitly documented instance-fidelity gaps after adaptation.`, `- ${report.summary.rawSourceGameObjects} AzerothCore object spawns were found, including ${report.summary.actionableSourceGameObjects} actionable doors, buttons, chests/resources, transports, traps, and related objects.`, `- ${report.summary.typedRuntimeWorldObjects} typed runtime world objects are compiled, including ${report.summary.sourceBackedAdaptedWorldObjects} source-backed adapted records. ${report.summary.clientModelPathWorldObjects} instances resolve to ${report.summary.uniqueGameObjectAssets} unique client models and ${report.summary.clientBoundedWorldObjects} have exact client bounds; ${report.summary.malformedRuntimeWorldObjects} malformed raw objects reach the live renderer.`, `- The deterministic game-object export manifest deduplicates the visual workload into ${report.summary.uniqueGameObjectAssets} client assets, records ${report.summary.nonVisualGameObjectInstances} intentional no-display spell-focus records, and isolates ${report.summary.unresolvedGameObjectModelDisplayIds} unresolved display IDs plus ${report.summary.unresolvedGameObjectBoundDisplayIds} display IDs without usable client bounds.`, report.tooling.wowExporter.available ? `- The patched client exporter is available at \`${report.tooling.wowExporter.expectedPath}\`.` : `- Native game-object model conversion is blocked until the patched client exporter is restored at \`${report.tooling.wowExporter.expectedPath}\`.`, `- ${report.summary.dungeonsWithAuthoredNavigationLinks} ${report.summary.dungeonsWithAuthoredNavigationLinks === 1 ? "dungeon has" : "dungeons have"} reviewed off-mesh links; the remaining routes still require end-to-end traversal evidence.`, "", "## Capability backlog", "", ...capabilitySections, "## Fleet matrix", "", "Encounter runtime is shown as definitions/rules. Objects are shown as typed runtime/source rows/actionable source rows.", "", "| Dungeon | Population source | Readiness | Encounter runtime | Objects | Source systems |", "| --- | --- | --- | ---: | ---: | --- |", ...rows, "", "## Per-dungeon required changes", "", ...dungeonSections, ].join("\n"); } const campaign = await readJson( path.join(projectRoot, "src/game/generated/dungeonCampaignCatalog.json"), ); const scriptCatalog = await readJson( path.join(projectRoot, "src/game/generated/azerothCoreScriptSpells.json"), ); const gameObjectAssetManifest = await readJson( path.join(projectRoot, "dungeon-pipeline/WOW_GAMEOBJECT_ASSET_MANIFEST.json"), ); const coverageByDungeon = new Map(campaign.coverage.map((entry) => [entry.dungeonId, entry])); const instanceFiles = await filesNamed(sourceScriptsRoot, /^instance_.+\.cpp$/i); const definitions = DUNGEON_DEFINITIONS .filter((definition) => definition.expansion !== "runewaker") .sort((left, right) => left.mapId - right.mapId || left.id.localeCompare(right.id)); const dungeons: DungeonFunctionalityResult[] = []; for (const definition of definitions) { const coverage = coverageByDungeon.get(definition.id); if (!coverage) throw new Error(`${definition.id}: missing generated campaign coverage.`); const draft = await runtimeDraftFor(definition.id); const scripts = await sourceScriptFiles(definition, draft, scriptCatalog, instanceFiles); const features = FEATURE_PATTERNS .filter(([, pattern]) => pattern.test(scripts.source)) .map(([feature]) => feature); const sourceObjects = sourceGameObjectEvidence(draft); const counts = encounterCounts(definition); const contractIssues: string[] = []; const spawnIds = new Set(); for (const spawn of definition.staticSpawns) { if (spawnIds.has(spawn.id)) contractIssues.push(`Duplicate spawn id ${spawn.id}.`); spawnIds.add(spawn.id); if (!definition.entities[spawn.entityId]) contractIssues.push(`${spawn.id} references missing entity ${spawn.entityId}.`); } for (const pack of definition.roamingPacks) { for (const member of pack.members) { const id = `${pack.id}:${member.id}`; if (spawnIds.has(id)) contractIssues.push(`Duplicate spawn id ${id}.`); spawnIds.add(id); if (!definition.entities[member.entityId]) contractIssues.push(`${id} references missing entity ${member.entityId}.`); } } const bossIds = new Set(); for (const boss of definition.bosses) { if (bossIds.has(boss.id)) contractIssues.push(`Duplicate boss id ${boss.id}.`); bossIds.add(boss.id); } const completedObjectiveIds = new Set((definition.encounters ?? []).flatMap((encounter) => ( encounter.rules.flatMap((rule) => rule.actions.flatMap((action) => ( action.kind === "complete-objective" ? [action.objectiveId] : [] ))) ))); const objectiveIds = new Set(); for (const objective of definition.objectiveOrder) { if (objectiveIds.has(objective.id)) { contractIssues.push(`Duplicate objective id ${objective.id}.`); } objectiveIds.add(objective.id); if (!completedObjectiveIds.has(objective.id)) { contractIssues.push(`Objective ${objective.id} has no encounter completion action.`); } if (!objective.eventOnly && !objective.optional && !spawnIds.has(objective.id)) { contractIssues.push(`Required objective ${objective.id} has no matching runtime spawn.`); } } const entitiesWithoutCombat = Object.values(definition.entities) .filter((entity) => !entity.combat?.attacks?.length) .map((entity) => entity.id) .sort(); if (counts.malformedWorldObjects) { contractIssues.push(`${counts.malformedWorldObjects} malformed world-object records reach the live renderer.`); } const requiredChanges = requiredChangesFor({ definition, source: coverage.source, sourceObjects, features, scriptFiles: scripts.files, malformedWorldObjects: counts.malformedWorldObjects, sourceBackedAdaptedWorldObjects: definition.serverGameObjects.filter( (object) => object.source?.fidelity === "source-backed-adapted", ).length, clientModelPathWorldObjects: definition.serverGameObjects.filter( (object) => Boolean(object.source?.clientModelPath), ).length, uniqueClientModelPaths: new Set(definition.serverGameObjects .map((object) => object.source?.clientModelPath?.toLowerCase()) .filter(Boolean)).size, clientBoundedWorldObjects: definition.serverGameObjects.filter( (object) => Boolean(object.source?.clientBounds), ).length, instanceOrchestrationDefinitions: counts.instanceOrchestrationDefinitions, remainingInstanceOrchestrationGaps: counts.instanceRemainingGaps, contractIssues, }); const sourceExactWorldObjects = definition.serverGameObjects.filter( (object) => object.source?.fidelity === "source-exact", ).length; const sourceBackedAdaptedWorldObjects = definition.serverGameObjects.filter( (object) => object.source?.fidelity === "source-backed-adapted", ).length; const authoredWorldObjects = definition.serverGameObjects.filter( (object) => object.source?.fidelity === "healer-man-authored", ).length; const clientDisplayWorldObjects = definition.serverGameObjects.filter( (object) => Boolean(object.source?.clientDisplayId), ).length; const clientModelPathWorldObjects = definition.serverGameObjects.filter( (object) => Boolean(object.source?.clientModelPath), ).length; const clientBoundedWorldObjects = definition.serverGameObjects.filter( (object) => Boolean(object.source?.clientBounds), ).length; dungeons.push({ id: definition.id, title: definition.title, expansion: definition.expansion, source: coverage.source, readiness: readinessFor( definition, coverage.source, requiredChanges, counts.instanceOrchestrationDefinitions, ), validationStatus: definition.validation.status, population: { entities: Object.keys(definition.entities).length, staticSpawns: definition.staticSpawns.length, roamingPacks: definition.roamingPacks.length, objectives: definition.objectiveOrder.length, }, combat: { executable: entitiesWithoutCombat.length === 0, entitiesWithoutCombat, }, encounters: { definitions: counts.definitions, rules: counts.rules, bossProgressionDefinitions: counts.bossProgressionDefinitions, instanceOrchestrationDefinitions: counts.instanceOrchestrationDefinitions, instanceOrchestrationDefinitionsWithRules: counts.instanceOrchestrationDefinitionsWithRules, sourceExactInstanceOrchestrationDefinitions: counts.sourceExactInstanceOrchestrationDefinitions, sourceBackedAdaptedInstanceOrchestrationDefinitions: counts.sourceBackedAdaptedInstanceOrchestrationDefinitions, sourceExactDefinitions: counts.sourceExactDefinitions, sourceBackedAdaptedDefinitions: counts.sourceBackedAdaptedDefinitions, authoredDefinitions: counts.authoredDefinitions, declaredObjectiveCompletion: counts.declaredObjectiveCompletion, checkpointState: counts.checkpointState, completionState: counts.completionState, reviewedBehaviors: counts.reviewedBehaviors, adaptations: counts.adaptations, remainingGaps: counts.remainingGaps, instanceAdaptations: counts.instanceAdaptations, instanceRemainingGaps: counts.instanceRemainingGaps, spawnGroups: counts.spawnGroups, worldObjects: definition.serverGameObjects.length, sourceExactWorldObjects, sourceBackedAdaptedWorldObjects, authoredWorldObjects, clientDisplayWorldObjects, clientModelPathWorldObjects, clientBoundedWorldObjects, malformedWorldObjects: counts.malformedWorldObjects, }, sourceEvidence: { rawGameObjects: sourceObjects.raw, actionableGameObjects: sourceObjects.actionable, unresolvedGameObjectTemplates: sourceObjects.unresolvedTemplates, gameObjectTypes: sourceObjects.types, scriptNames: scripts.scriptNames, resolvedScriptFiles: scripts.files, unresolvedScriptNames: scripts.unresolved, features, }, navigation: { assetPresent: Boolean(definition.assets.navigation), authoredLinks: definition.navigationLinks.length, }, contractIssues: unique(contractIssues), requiredChanges, }); } const affected = (predicate: (dungeon: DungeonFunctionalityResult) => boolean) => ( dungeons.filter(predicate).map((dungeon) => dungeon.title) ); const capabilityBacklog = [ { priority: "P0", capability: "Compile source game objects into the typed runtime contract", affectedDungeons: affected((dungeon) => ( dungeon.encounters.worldObjects < dungeon.sourceEvidence.actionableGameObjects )), editTargets: [ "scripts/dungeon-pipeline/compile-campaign-catalog.mjs", "src/game/dungeonWorldObjects.ts", "src/scene/DungeonWorldObjects.tsx", ], }, { priority: "P0", capability: "Export reviewed client visuals and enable collision/state transitions for adapted objects", affectedDungeons: affected((dungeon) => dungeon.encounters.sourceBackedAdaptedWorldObjects > 0), editTargets: [ "dungeon-pipeline/WOW_GAMEOBJECT_ASSET_MANIFEST.json", "scripts/dungeon-pipeline/export-source.mjs", "scripts/dungeon-pipeline/compile-campaign-catalog.mjs", "src/game/dungeonWorldObjects.ts", "src/scene/DungeonWorldObjects.tsx", ], }, { priority: "P1", capability: "Review and port unreviewed instance scripts", affectedDungeons: affected((dungeon) => ( dungeon.sourceEvidence.resolvedScriptFiles.length > 0 && dungeon.encounters.instanceOrchestrationDefinitions === 0 )), editTargets: [ "scripts/dungeon-pipeline/import-azerothcore-scripts.mjs", "src/game/dungeonEncounterTypes.ts", "src/game/dungeonEncounterRuntime.ts", ], }, { priority: "P1", capability: "Close documented source-script orchestration gaps", affectedDungeons: affected((dungeon) => ( dungeon.sourceEvidence.resolvedScriptFiles.length > 0 && dungeon.encounters.instanceOrchestrationDefinitions > 0 && dungeon.encounters.instanceRemainingGaps.length > 0 )), editTargets: [ "src/game/", "src/game/dungeonEncounterTypes.ts", "src/game/dungeonEncounterRuntime.ts", ], }, { priority: "P1", capability: "Add exact waves, summons, escorts, and vehicle primitives", affectedDungeons: affected((dungeon) => dungeon.sourceEvidence.features.some((feature) => ( feature === "waves-summons" || feature === "escorts-waypoints" || feature === "vehicles-passengers" ))), editTargets: [ "src/game/dungeonEncounterTypes.ts", "src/game/dungeonEncounterRuntime.ts", "src/game/dungeonEncounterSpawns.ts", "src/scene/PartyPopulation.tsx", ], }, { priority: "P1", capability: "Add object, quest, key, chest/resource, and area-trigger interactions", affectedDungeons: affected((dungeon) => ( dungeon.sourceEvidence.actionableGameObjects > 0 || dungeon.sourceEvidence.features.includes("gossip-quests-items") || dungeon.sourceEvidence.features.includes("area-triggers") )), editTargets: [ "src/game/dungeonWorldInteraction.ts", "src/game/dungeonWorldObjects.ts", "src/game/dungeonEncounterRuntime.ts", "src/ui/InteractionPrompt.tsx", ], }, { priority: "P1", capability: "Replace procedural Ascension populations with authoritative data", affectedDungeons: affected((dungeon) => dungeon.source === "procedural-fallback"), editTargets: [ "dungeon-pipeline/work//source-snapshot.json", "scripts/dungeon-pipeline/import-five-player-dungeon.mjs", "scripts/dungeon-pipeline/compile-campaign-catalog.mjs", ], }, { priority: "P2", capability: "Run end-to-end traversal, collision, wipe/reset, checkpoint, and completion playtests", affectedDungeons: affected((dungeon) => dungeon.readiness !== "validated"), editTargets: [ "src/game/", "src/game/dungeonNavigation.ts", "PLAYTEST_NOTES.md", ], }, ].filter((entry) => entry.affectedDungeons.length > 0); const summary = { dungeons: dungeons.length, validated: dungeons.filter((dungeon) => dungeon.readiness === "validated").length, sourceBacked: dungeons.filter((dungeon) => dungeon.source === "azerothcore").length, sourceBlocked: dungeons.filter((dungeon) => dungeon.source === "procedural-fallback").length, dungeonsWithEncounterRuntime: dungeons.filter((dungeon) => dungeon.encounters.definitions > 0).length, dungeonsWithoutEncounterRuntime: dungeons.filter((dungeon) => dungeon.encounters.definitions === 0).length, dungeonsWithDeclaredBossProgression: dungeons.filter( (dungeon) => dungeon.encounters.declaredObjectiveCompletion && dungeon.encounters.checkpointState && dungeon.encounters.completionState, ).length, dungeonsWithReviewedInstanceOrchestration: dungeons.filter( (dungeon) => dungeon.encounters.instanceOrchestrationDefinitions > 0, ).length, dungeonsWithoutReviewedInstanceOrchestration: dungeons.filter( (dungeon) => dungeon.encounters.instanceOrchestrationDefinitions === 0, ).length, dungeonsWithInstanceOrchestrationRules: dungeons.filter( (dungeon) => dungeon.encounters.instanceOrchestrationDefinitionsWithRules > 0, ).length, dungeonsWithVerifiedNoOpInstanceScripts: dungeons.filter((dungeon) => ( dungeon.encounters.instanceOrchestrationDefinitions > 0 && dungeon.encounters.instanceOrchestrationDefinitionsWithRules === 0 && dungeon.encounters.sourceExactInstanceOrchestrationDefinitions > 0 )).length, dungeonsWithDocumentedInstanceGaps: dungeons.filter( (dungeon) => dungeon.encounters.instanceRemainingGaps.length > 0, ).length, sourceExactEncounterDefinitions: dungeons.reduce( (count, dungeon) => count + dungeon.encounters.sourceExactDefinitions, 0, ), sourceBackedAdaptedEncounterDefinitions: dungeons.reduce( (count, dungeon) => count + dungeon.encounters.sourceBackedAdaptedDefinitions, 0, ), authoredEncounterDefinitions: dungeons.reduce( (count, dungeon) => count + dungeon.encounters.authoredDefinitions, 0, ), rawSourceGameObjects: dungeons.reduce((count, dungeon) => count + dungeon.sourceEvidence.rawGameObjects, 0), actionableSourceGameObjects: dungeons.reduce((count, dungeon) => count + dungeon.sourceEvidence.actionableGameObjects, 0), typedRuntimeWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.worldObjects, 0), sourceExactWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.sourceExactWorldObjects, 0), sourceBackedAdaptedWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.sourceBackedAdaptedWorldObjects, 0), authoredWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.authoredWorldObjects, 0), clientDisplayWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.clientDisplayWorldObjects, 0), clientModelPathWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.clientModelPathWorldObjects, 0), clientBoundedWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.clientBoundedWorldObjects, 0), uniqueGameObjectAssets: gameObjectAssetManifest.summary.uniqueAssets, nonVisualGameObjectInstances: gameObjectAssetManifest.summary.nonVisualInstances, unresolvedGameObjectModelDisplayIds: gameObjectAssetManifest.summary.unresolvedModelDisplayIds, unresolvedGameObjectBoundDisplayIds: gameObjectAssetManifest.summary.unresolvedBoundDisplayIds, malformedRuntimeWorldObjects: dungeons.reduce((count, dungeon) => count + dungeon.encounters.malformedWorldObjects, 0), dungeonsWithAuthoredNavigationLinks: dungeons.filter((dungeon) => dungeon.navigation.authoredLinks > 0).length, contractIssues: dungeons.reduce((count, dungeon) => count + dungeon.contractIssues.length, 0), }; const tooling = { wowExporter: { available: await exists(wowExporterFile), expectedPath: relativeFile(wowExporterFile), }, }; const report = { schemaVersion: 1, generatedAt: new Date().toISOString(), status: summary.validated === summary.dungeons ? "green" : "blocked", scope: "WoW five-player production functionality", summary, tooling, capabilityBacklog, dungeons, }; await writeFile(reportJsonFile, `${JSON.stringify(report, null, 2)}\n`, "utf8"); await writeFile(reportMarkdownFile, `${reportMarkdown(report).trimEnd()}\n`, "utf8"); console.log(JSON.stringify({ status: report.status, reportJson: relativeFile(reportJsonFile), reportMarkdown: relativeFile(reportMarkdownFile), summary, }, null, 2));