Release Healer Man 0.1.32

This commit is contained in:
phenom committed 2026-09-12 11:07:25 -04:00
1 parent d3d6aaebdb
commit 6dcb1034e5
102 files changed
+10772 -788

No files matched your search

+8 -7
View File
@@ -8,6 +8,7 @@ import { gunzipSync } from "node:zlib";
import { prepareContentObjects } from "./content-objects.mjs";
import { selectBootstrapContent } from "./prepare-bootstrap.mjs";
import { createContentManifest } from "./publish-content.mjs";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
test("ships one lossless object for identical assets and verifies original bytes", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "healer-content-compression-"));
@@ -36,17 +37,17 @@ test("ships one lossless object for identical assets and verifies original bytes
} finally { await rm(directory, { recursive: true, force: true }); }
});
test("bundles regular dungeons and exact map dependencies without installing Manastorm", () => {
const ids = ["core:ui", "dungeon:first", "dungeon:second", "shared:creatures", "manastorm:shared-map", "manastorm:optional-map", "characters:extra"];
test("bundles only starter packs and their exact dependencies", () => {
const ids = [...contentPolicy.bootstrapPackIds, "dungeon:second", "shared:creatures", "manastorm:shared-map", "manastorm:optional-map", "characters:extra"];
const files = ids.map((packId, index) => ({ logicalPath: `assets/${index}.glb`, packId, sha256: String(index).repeat(64), bytes: 100, extension: ".glb" }));
const result = selectBootstrapContent(files, [
{ id: "dungeon:first", dependencies: ["manastorm:shared-map", "shared:creatures"] },
{ id: "dungeon:second", dependencies: [] },
]);
assert.deepEqual(result.files.map((file) => file.packId), ids.slice(0, 5));
assert.deepEqual(result.files.map((file) => file.packId), contentPolicy.bootstrapPackIds);
const packs = new Map(result.packDefinitions.map((pack) => [pack.id, pack]));
assert.equal(packs.get("dungeon:first").bundledOnAndroid, true);
assert.equal(packs.get("dungeon:second").bundledOnAndroid, true);
assert.equal(packs.get("manastorm:shared-map").bundledOnAndroid, false);
assert.deepEqual([...packs.keys()].sort(), [...contentPolicy.bootstrapPackIds].sort());
assert.equal([...packs.values()].every((pack) => pack.bundledOnAndroid), true);
assert.equal(packs.has("dungeon:second"), false);
assert.equal(packs.has("manastorm:shared-map"), false);
assert.equal(packs.has("manastorm:optional-map"), false);
});
+55
View File
@@ -0,0 +1,55 @@
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { brotliCompress, constants, gzip } from "node:zlib";
const compressBrotli = promisify(brotliCompress);
const compressGzip = promisify(gzip);
const ELIGIBLE_EXTENSIONS = new Set([".css", ".js", ".json"]);
const MINIMUM_BYTES = 1024;
async function eligibleFiles(root) {
const files = [];
async function visit(directory) {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const filePath = path.join(directory, entry.name);
if (entry.isDirectory()) await visit(filePath);
else if (entry.isFile() && ELIGIBLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(filePath);
}
}
await visit(root);
return files;
}
export async function precompressStatic(root) {
const files = await eligibleFiles(root);
let sourceBytes = 0;
let brotliBytes = 0;
let gzipBytes = 0;
let writtenFiles = 0;
for (const filePath of files) {
const source = await readFile(filePath);
if (source.length < MINIMUM_BYTES) continue;
const [brotli, gzipped] = await Promise.all([
compressBrotli(source, { params: { [constants.BROTLI_PARAM_QUALITY]: 6 } }),
compressGzip(source, { level: 9 }),
]);
await Promise.all([
writeFile(`${filePath}.br`, brotli),
writeFile(`${filePath}.gz`, gzipped),
]);
sourceBytes += source.length;
brotliBytes += brotli.length;
gzipBytes += gzipped.length;
writtenFiles += 2;
}
return { sourceFiles: writtenFiles / 2, writtenFiles, sourceBytes, brotliBytes, gzipBytes };
}
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
const root = path.resolve(process.argv[2] ?? "dist");
const result = await precompressStatic(root);
console.log(JSON.stringify({ root, ...result }, null, 2));
}
+8 -5
View File
@@ -5,6 +5,7 @@ import {
createContentManifest, loadCampaignDungeonPackDefinitions, scanContentSources,
} from "./publish-content.mjs";
import { prepareContentObjects } from "./content-objects.mjs";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
export function selectBootstrapContent(files, packDefinitions) {
const manifest = createContentManifest(files, { packDefinitions });
@@ -15,15 +16,12 @@ export function selectBootstrapContent(files, packDefinitions) {
included.add(id);
for (const dependency of packs.get(id)?.dependencies ?? []) include(dependency);
}
for (const pack of manifest.packs) {
if (BUNDLED_ANDROID_PACKS.includes(pack.id) || pack.id.startsWith("dungeon:")) include(pack.id);
}
for (const packId of BUNDLED_ANDROID_PACKS) include(packId);
return {
files: files.filter((file) => included.has(file.packId)),
packDefinitions: manifest.packs.filter((pack) => included.has(pack.id)).map((pack) => ({
...pack,
// Shared maps can be physically bundled without installing Manastorm gameplay.
bundledOnAndroid: !pack.id.startsWith("manastorm:"),
bundledOnAndroid: BUNDLED_ANDROID_PACKS.includes(pack.id),
})),
};
}
@@ -44,6 +42,11 @@ export async function prepareBootstrap(platform) {
const prepared = await prepareContentObjects(selected.files, {
directory: path.join(outputDirectory, "content/bundled"), baseUrl: "/content/bundled",
});
if (prepared.storedBytes > contentPolicy.maxBootstrapStoredBytes) {
throw new Error(
`Bootstrap content is ${prepared.storedBytes} bytes; the release limit is ${contentPolicy.maxBootstrapStoredBytes} bytes.`,
);
}
const manifest = createContentManifest(prepared.files, {
baseUrl: "/content/bundled", packDefinitions: selected.packDefinitions,
});
+2 -8
View File
@@ -10,6 +10,7 @@ import {
import path from "node:path";
import { fileURLToPath } from "node:url";
import { prepareContentObjects } from "./content-objects.mjs";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
export const PROJECT_ROOT = path.resolve(SCRIPT_DIRECTORY, "../..");
@@ -23,14 +24,7 @@ const WOW_CAMPAIGN_CATALOG_PATH = path.join(
export const CONTENT_SCHEMA_VERSION = 1;
export const DEFAULT_MINIMUM_APP_VERSION = "0.1.0";
export const WOW_CAMPAIGN_DUNGEON_COUNT = 54;
export const BUNDLED_ANDROID_PACKS = Object.freeze([
"core:ui",
"core:equipment",
"core:character-basic",
"core:runtime",
"core:arenas",
"dungeon:wailing-caverns",
]);
export const BUNDLED_ANDROID_PACKS = Object.freeze([...contentPolicy.bootstrapPackIds]);
const RUNTIME_EXTENSIONS = new Set([
".avif", ".glb", ".js", ".json", ".ktx2", ".mp3", ".ogg", ".png",
+1 -1
View File
@@ -24,7 +24,7 @@ test("assigns stable player-facing packs from logical asset paths", () => {
assert.equal(contentPackForPath("assets/characters/wow335a-v1/orc/male/rig.glb"), "characters:wow335a-v1");
assert.equal(contentPackForPath("assets/game/manastorm/stage-1/visual.glb"), "manastorm:stage-1");
assert.equal(contentPackForPath("assets/game/arenas/nagrand-arena/visual.glb"), "core:arenas");
assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), true);
assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), false);
assert.equal(contentPackForPath("assets/game/combat-effects/audio/smite.wav"), "core:combat-media");
assert.equal(
contentPackForPath("assets/game/combat-effects/audio/manastorm/Manastorm_BossKill_GenericPortalsReady.wav"),
+14 -3
View File
@@ -7,6 +7,10 @@ import json
from pathlib import Path
from zipfile import ZIP_STORED, ZipFile
CONTENT_POLICY = json.loads(
(Path(__file__).resolve().parents[2] / "shared" / "content-policy.json").read_text(encoding="utf-8")
)
def verify_package(apk: Path) -> dict:
with ZipFile(apk) as archive:
@@ -35,13 +39,20 @@ def verify_package(apk: Path) -> dict:
if actual_objects != expected_objects:
raise ValueError("APK includes missing or unreferenced bundled objects.")
manastorms = sum(pack["id"].startswith("manastorm:") and pack["bundledOnAndroid"] for pack in manifest["packs"])
if dungeons != 84 or manastorms != 0:
raise ValueError(f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs")
arenas = sum(pack["id"] == "core:arenas" for pack in manifest["packs"])
pack_ids = sorted(pack["id"] for pack in manifest["packs"])
expected_pack_ids = sorted(CONTENT_POLICY["bootstrapPackIds"])
if pack_ids != expected_pack_ids or dungeons != 1 or manastorms != 0 or arenas != 0:
raise ValueError(
f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs, {arenas} arena packs"
)
if object_bytes > CONTENT_POLICY["maxBootstrapStoredBytes"]:
raise ValueError("APK bootstrap content exceeds the stored-byte budget.")
return {
"apk": str(apk.resolve()), "apkBytes": apk.stat().st_size,
"manifestVersion": manifest["version"], "logicalFiles": len(manifest["files"]),
"verifiedUniqueObjects": len(checked), "storedObjectBytes": object_bytes,
"bundledDungeons": dungeons, "bundledManastormPacks": manastorms,
"bundledDungeons": dungeons, "bundledManastormPacks": manastorms, "bundledArenaPacks": arenas,
"allOriginalHashesMatch": True,
}
+91
View File
@@ -0,0 +1,91 @@
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
const SCRIPT_PATH = fileURLToPath(import.meta.url);
const PROHIBITED_MODULE_PATTERNS = [
/(?:^|\/)src\/ArenaRuntime\.tsx$/,
/(?:^|\/)src\/app\/ShelvedModeRoutes\.tsx$/,
/(?:^|\/)src\/app\/ShelvedGameRuntime\.enabled\.tsx$/,
/(?:^|\/)src\/app\/ShelvedOnlineGroupRuntime\.enabled\.ts$/,
/(?:^|\/)src\/app\/(?:arenaClient|gmManastormRuntime|manastormShellRuntime)\.[jt]sx?$/,
/(?:^|\/)src\/game\/(?:arena|manastormAdmin|manastormCatalog|manastormInstalled)(?:\/|\.[jt]sx?$)/,
/(?:^|\/)src\/game\/ManastormBridge\.[jt]sx?$/,
/(?:^|\/)src\/ui\/(?:Arena|GmAdmin|GmReview|GmWorld|Manastorm)[^/]*\.[jt]sx?$/,
/(?:^|\/)src\/scene\/ShelvedGameScene\.enabled\.tsx$/,
/(?:^|\/)src\/game\/ShelvedDungeonSession\.enabled\.ts$/,
/(?:^|\/)src\/ui\/(?:ShelvedCompanionPanel|ShelvedHud)\.enabled\.tsx$/,
];
function fail(message) {
throw new Error(`Release bundle verification failed: ${message}`);
}
async function walkRelativeFiles(root) {
const files = [];
async function visit(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const filePath = path.join(directory, entry.name);
if (entry.isDirectory()) await visit(filePath);
else if (entry.isFile()) files.push(path.relative(root, filePath).split(path.sep).join("/"));
}
}
await visit(root);
return files;
}
export async function verifyReleaseBundle(root) {
const manifest = JSON.parse(await readFile(path.join(root, "content", "bootstrap-manifest.json"), "utf8"));
const moduleReport = JSON.parse(await readFile(path.join(root, "bundle-module-report.json"), "utf8"));
if (moduleReport.shelvedModesEnabled !== false) fail("normal releases must compile with shelved modes disabled");
const expectedPacks = [...contentPolicy.bootstrapPackIds].sort();
const actualPacks = (manifest.packs ?? []).map((pack) => pack.id).sort();
if (JSON.stringify(actualPacks) !== JSON.stringify(expectedPacks)) {
fail(`bootstrap packs were ${actualPacks.join(", ")}; expected ${expectedPacks.join(", ")}`);
}
const hiddenPack = actualPacks.find((packId) => (
contentPolicy.hiddenContentPackIds.includes(packId)
|| contentPolicy.hiddenContentPackPrefixes.some((prefix) => packId.startsWith(prefix))
));
if (hiddenPack) fail(`bootstrap contains hidden pack ${hiddenPack}`);
const objects = new Map();
for (const entry of Object.values(manifest.files ?? {})) {
const object = entry.download ?? entry;
if (!objects.has(object.url)) objects.set(object.url, object.bytes);
}
const storedBytes = [...objects.values()].reduce((sum, bytes) => sum + Number(bytes ?? 0), 0);
if (storedBytes > contentPolicy.maxBootstrapStoredBytes) {
fail(`bootstrap uses ${storedBytes} stored bytes; limit is ${contentPolicy.maxBootstrapStoredBytes}`);
}
const prohibitedModules = moduleReport.chunks.flatMap((chunk) => chunk.modules).filter((moduleId) => (
PROHIBITED_MODULE_PATTERNS.some((pattern) => pattern.test(moduleId))
));
if (prohibitedModules.length) fail(`shelved client modules were emitted: ${[...new Set(prohibitedModules)].join(", ")}`);
const files = await walkRelativeFiles(root);
const arenaAsset = files.find((file) => /(?:^|\/)assets\/game\/arenas(?:\/|$)/i.test(file));
if (arenaAsset) fail(`arena asset was emitted at ${arenaAsset}`);
for (const objectUrl of objects.keys()) {
const objectPath = path.join(root, objectUrl.replace(/^\/+/, ""));
const objectStats = await stat(objectPath);
if (!objectStats.isFile()) fail(`bootstrap object is missing: ${objectUrl}`);
}
return {
root,
packs: actualPacks,
storedBytes,
storedMiB: Number((storedBytes / 1024 / 1024).toFixed(2)),
objectCount: objects.size,
chunkCount: moduleReport.chunks.length,
};
}
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(SCRIPT_PATH)) {
const root = path.resolve(process.argv[2] ?? "dist");
console.log(JSON.stringify(await verifyReleaseBundle(root), null, 2));
}
@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
import { verifyReleaseBundle } from "./verify-release-bundle.mjs";
async function fixture() {
const root = await mkdtemp(path.join(os.tmpdir(), "healer-man-release-bundle-test-"));
await mkdir(path.join(root, "content", "bundled"), { recursive: true });
const manifest = {
schemaVersion: 1,
packs: contentPolicy.bootstrapPackIds.map((id) => ({ id })),
files: {},
};
const moduleReport = { shelvedModesEnabled: false, chunks: [{ file: "assets/index.js", modules: ["src/App.tsx"] }] };
const write = async () => {
await writeFile(path.join(root, "content", "bootstrap-manifest.json"), `${JSON.stringify(manifest)}\n`);
await writeFile(path.join(root, "bundle-module-report.json"), `${JSON.stringify(moduleReport)}\n`);
};
await write();
return { root, manifest, moduleReport, write };
}
test("accepts the exact compact production policy", async () => {
const value = await fixture();
try {
const report = await verifyReleaseBundle(value.root);
assert.deepEqual(report.packs, [...contentPolicy.bootstrapPackIds].sort());
assert.equal(report.storedBytes, 0);
} finally {
await rm(value.root, { recursive: true, force: true });
}
});
test("rejects arena packs and shelved client modules", async () => {
const value = await fixture();
try {
value.manifest.packs.push({ id: "core:arenas" });
await value.write();
await assert.rejects(verifyReleaseBundle(value.root), /bootstrap packs/);
value.manifest.packs.pop();
value.moduleReport.chunks[0].modules.push("src/ArenaRuntime.tsx");
await value.write();
await assert.rejects(verifyReleaseBundle(value.root), /shelved client modules/);
} finally {
await rm(value.root, { recursive: true, force: true });
}
});
test("rejects a bootstrap above the stored-byte budget", async () => {
const value = await fixture();
try {
value.manifest.files["/large.glb"] = {
packId: "core:runtime",
url: `/content/bundled/${"c".repeat(64)}.glb`,
bytes: contentPolicy.maxBootstrapStoredBytes + 1,
};
await value.write();
await assert.rejects(verifyReleaseBundle(value.root), /limit is/);
} finally {
await rm(value.root, { recursive: true, force: true });
}
});
+1
View File
@@ -81,6 +81,7 @@ def full_release_checks(npm: str) -> tuple[tuple[list[str], str], ...]:
([npm, "run", "content:test"], "Content tests"),
([npm, "run", "server:test"], "Server tests"),
([npm, "test"], "Application tests"),
([npm, "run", "test:shelved"], "Shelved-mode application tests"),
([npm, "run", "content:publish"], "Publish verified 54-dungeon content manifest"),
([npm, "run", "build"], "Production build"),
([npm, "run", "android:sync"], "Android web build and Capacitor sync"),
@@ -71,6 +71,21 @@ Optional template fields:
- `reason`: why an object is deferred;
- `source`: forensic image/model/classification evidence.
`source.scriptRoots` may list reviewed Lua entry functions for dynamically
created bosses that have no static SQL row. The boss-mechanics importer follows
those roots and their callbacks exactly as it does for static boss rows.
Optional top-level scripted placement fields:
- `scriptedSpawns`: reviewed creatures created by Lua rather than stored as
active zone rows. Each entry has a stable `id`, a combat/boss `templateId`, a
`sourceAnchorSpawnId`, source evidence, and optional source-unit offset,
controller row, direction, or `objective: false` for a boss companion.
- `scriptedFormations`: source `LuaFunc_Matrix_Maker` formations flattened to
their initial positions. Each formation specifies its controller/anchor row,
source direction and spacing, Lua root/evidence, and member template IDs with
one-based rows and signed columns.
`expected*` values may be `null` only in a fresh scaffold. The explicit
forensic refresh with `--update-recipe` pins them. A recipe should not be treated
as production-ready until all classifications/models and counts are reviewed.
@@ -27,9 +27,9 @@
"summary": {
"dungeons": 30,
"deferredSourceRows": 4113,
"uniqueMechanicsEvidence": 1524,
"uniqueMechanicsEvidence": 1530,
"luaEvidenceSignals": 329,
"templateEvidenceSignals": 1195,
"templateEvidenceSignals": 1201,
"evidenceCategoryMembershipCount": 1146,
"evidenceCategoryMemberships": {
"death": 100,
@@ -54,21 +54,21 @@
"evidenceStatuses": {
"implemented": 0,
"verified": 388,
"deferred-source-gap": 1136,
"deferred-source-gap": 1142,
"deferred-asset-gap": 0,
"deferred-runtime-gap": 0,
"excluded-non-gameplay": 0,
"intentional-empty": 0
},
"evidenceFidelities": {
"source-exact": 1136,
"source-exact": 1142,
"source-backed-adapted": 388,
"healer-man-authored": 0
},
"verifiedEvidence": 388,
"sourceGapEvidence": 1136,
"fallbackBosses": 4,
"nonEmptyZeroBossPackages": 8,
"sourceGapEvidence": 1142,
"fallbackBosses": 5,
"nonEmptyZeroBossPackages": 7,
"intentionalEmptyPackages": 2
},
"blockerGroups": {
@@ -77,7 +77,7 @@
"nextWork": "Review source semantics and add explicit source-spawn encounter mappings."
},
"unresolvedMechanicsEvidence": {
"items": 1136,
"items": 1142,
"nextWork": "Review surrounding Lua/template context; specify full trigger, effect, and reset behavior."
},
"adaptedSpellFormula": {
@@ -85,11 +85,11 @@
"nextWork": "Recover native formula inputs before claiming coefficient-level fidelity."
},
"sourceGapFallbackBosses": {
"items": 4,
"items": 5,
"nextWork": "Recover authoritative combat kits and replace only with source-backed modular mechanics."
},
"nonEmptyZeroBossPackages": {
"items": 8,
"items": 7,
"nextWork": "Recover authoritative boss membership and objective order."
}
},
@@ -111,7 +111,7 @@
"source": {
"bossMechanicsCatalog": {
"path": "dungeon-pipeline/boss-mechanics.json",
"sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7"
"sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json",
@@ -153,7 +153,7 @@
"source": {
"bossMechanicsCatalog": {
"path": "dungeon-pipeline/boss-mechanics.json",
"sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7"
"sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json",
@@ -189,7 +189,7 @@
"source": {
"bossMechanicsCatalog": {
"path": "dungeon-pipeline/boss-mechanics.json",
"sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7"
"sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json",
@@ -225,7 +225,7 @@
"source": {
"bossMechanicsCatalog": {
"path": "dungeon-pipeline/boss-mechanics.json",
"sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7"
"sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json",
@@ -244,6 +244,44 @@
"blocker": "Preserved source evidence does not expose an executable original combat kit.",
"nextWork": "Replace the fallback only after authoritative spell timing, targeting, and effects are recovered; preserve modular IDs and telegraphs.",
"clearingTest": "Add source-backed mechanic fixtures and assert the catalog source kind changes from healer-man-source-gap to runewaker."
},
{
"entityId": "rw-grafu-castle-105351",
"name": "Larvanger Barkud",
"dungeonId": "grafu-castle",
"templateId": 105351,
"mechanicIds": [
"rw-grafu-castle-105351-source-gap-sweep"
],
"telegraphedMechanicIds": [
"rw-grafu-castle-105351-source-gap-sweep"
],
"status": "implemented",
"fidelity": "healer-man-authored",
"source": {
"bossMechanicsCatalog": {
"path": "dungeon-pipeline/boss-mechanics.json",
"sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
},
"roots": [
"un_zone19_boss5_allconreal_1_01"
],
"functions": [
"un_zone19_boss5_allconreal_1_01",
"Az_RND_Ary",
"SearchRangeNPC",
"DW_Rand"
],
"spellIds": []
},
"blocker": "Preserved source evidence does not expose an executable original combat kit.",
"nextWork": "Replace the fallback only after authoritative spell timing, targeting, and effects are recovered; preserve modular IDs and telegraphs.",
"clearingTest": "Add source-backed mechanic fixtures and assert the catalog source kind changes from healer-man-source-gap to runewaker."
}
],
"nonEmptyZeroBossPackages": [
@@ -277,21 +315,6 @@
"nextWork": "Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.",
"clearingTest": "Verify at least one source-backed boss objective, or add reviewed evidence that this package intentionally has none."
},
{
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceRows": 249,
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
},
"blocker": "The portable source package contains population rows but no source-confirmed boss membership.",
"nextWork": "Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.",
"clearingTest": "Verify at least one source-backed boss objective, or add reviewed evidence that this package intentionally has none."
},
{
"dungeonId": "kal-turok-hive",
"zoneId": 105,
@@ -41141,11 +41164,11 @@
"id": "grafu-castle",
"zoneId": 137,
"sourceRows": 249,
"bossObjectives": 0,
"bossObjectives": 5,
"sourceArtifacts": {
"recipe": {
"path": "scripts/runewaker-pipeline/recipes/grafu-castle-population.json",
"sha256": "b65eb823f8d1a7306dbafb17e4c4ed5e11899b9439af72b4d24b18ddbfd07bf8"
"sha256": "29b4798fb43dc6c0ef8b5ec5dd1cc6007c4b84cc2753c4d38b47d005ff82b4cb"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -41154,11 +41177,11 @@
},
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"generatedRuntime": {
"path": "src/game/generated/grafuCastlePopulation.generated.ts",
"sha256": "ed998e499878f675d1223ea24c966816ed22dae1beccfd7ae6e3f701ff06efd6"
"sha256": "e3678e53911590bcf2fbde56272330d903b76a3f8a84a96f8bef43f671c0a07d"
},
"upstreamHashes": {
"backups": {
@@ -41173,11 +41196,11 @@
}
},
"packageAssessment": {
"status": "deferred-source-gap",
"status": "verified",
"fidelity": "source-exact",
"blocker": "The package is non-empty, but its preserved source classification confirms no boss objectives.",
"nextWork": "Recover authoritative boss membership and encounter order before creating boss objectives.",
"clearingTest": "Add a source-backed boss-membership fixture and verify the generated objective list."
"blocker": "No package-level population blocker is recorded.",
"nextWork": "Re-run the ledger whenever portable source content changes.",
"clearingTest": "npm run runewaker:audit:gameplay"
},
"deferredRowStatusCounts": {
"implemented": 0,
@@ -41191,14 +41214,14 @@
"evidenceStatusCounts": {
"implemented": 0,
"verified": 10,
"deferred-source-gap": 40,
"deferred-source-gap": 46,
"deferred-asset-gap": 0,
"deferred-runtime-gap": 0,
"excluded-non-gameplay": 0,
"intentional-empty": 0
},
"evidenceFidelityCounts": {
"source-exact": 40,
"source-exact": 46,
"source-backed-adapted": 10,
"healer-man-authored": 0
},
@@ -47823,7 +47846,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -47851,7 +47874,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -47879,7 +47902,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -47907,7 +47930,35 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
}
},
"blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.",
"nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
"clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior."
},
{
"id": "signal:grafu-castle:template:105204:autoplot:8695922bb376",
"kind": "mechanics-evidence",
"evidenceKind": "auto-plot",
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceIds": {
"templateId": 105204
},
"categories": [],
"value": "cl_136_105205_N",
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -47935,7 +47986,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -47963,7 +48014,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -47993,7 +48044,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48021,7 +48072,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48052,7 +48103,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48082,7 +48133,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48110,7 +48161,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48141,7 +48192,35 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
}
},
"blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.",
"nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
"clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior."
},
{
"id": "signal:grafu-castle:template:105224:autoplot:91708b8323d7",
"kind": "mechanics-evidence",
"evidenceKind": "auto-plot",
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceIds": {
"templateId": 105224
},
"categories": [],
"value": "ic_105224_01",
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48169,7 +48248,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48197,7 +48276,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48225,7 +48304,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48253,7 +48332,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48281,7 +48360,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48309,7 +48388,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48337,7 +48416,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48365,7 +48444,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48393,7 +48472,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48421,7 +48500,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48449,7 +48528,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48477,7 +48556,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48505,7 +48584,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48533,7 +48612,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48561,7 +48640,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48589,7 +48668,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48617,7 +48696,91 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
}
},
"blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.",
"nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
"clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior."
},
{
"id": "signal:grafu-castle:template:105351:autoplot:cb2a2839d2c8",
"kind": "mechanics-evidence",
"evidenceKind": "auto-plot",
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceIds": {
"templateId": 105351
},
"categories": [],
"value": "un_zone19_boss5_allconreal_1_01",
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
}
},
"blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.",
"nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
"clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior."
},
{
"id": "signal:grafu-castle:template:105542:autoplot:5ae3fcd9ed91",
"kind": "mechanics-evidence",
"evidenceKind": "auto-plot",
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceIds": {
"templateId": 105542
},
"categories": [],
"value": "kg_105542_01",
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
}
},
"blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.",
"nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
"clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior."
},
{
"id": "signal:grafu-castle:template:105543:autoplot:5ae3fcd9ed91",
"kind": "mechanics-evidence",
"evidenceKind": "auto-plot",
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceIds": {
"templateId": 105543
},
"categories": [],
"value": "kg_105542_01",
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48650,7 +48813,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48691,7 +48854,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48724,7 +48887,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48760,7 +48923,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48793,7 +48956,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48834,7 +48997,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48875,7 +49038,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48908,7 +49071,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48949,7 +49112,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -48990,7 +49153,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49031,7 +49194,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49072,7 +49235,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49100,7 +49263,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49128,7 +49291,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49159,7 +49322,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49192,7 +49355,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49225,7 +49388,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49266,7 +49429,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49307,7 +49470,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49340,7 +49503,7 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -49373,7 +49536,35 @@
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348"
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
"sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403",
"contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801"
}
},
"blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.",
"nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.",
"clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior."
},
{
"id": "signal:grafu-castle:template:105616:autoplot:6b24b50e07a5",
"kind": "mechanics-evidence",
"evidenceKind": "auto-plot",
"dungeonId": "grafu-castle",
"zoneId": 137,
"sourceIds": {
"templateId": 105616
},
"categories": [],
"value": "Dy_Z19_BOSSN_AIC",
"status": "deferred-source-gap",
"fidelity": "source-exact",
"source": {
"mechanicsAudit": {
"path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json",
"sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd"
},
"snapshot": {
"path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -11,10 +11,10 @@ is a review label, not a unique mechanic; one evidence record may belong to seve
- 30 dungeon packages.
- 4113 deferred source rows with stable spawn IDs.
- 1524 unique mechanics evidence records: 329 Lua lines and 1195 template references.
- 1530 unique mechanics evidence records: 329 Lua lines and 1201 template references.
- 1146 overlapping category memberships; this number is intentionally not reported as mechanics.
- 388 evidence records have an exact executable source-spell mapping.
- 1136 evidence records remain source-review gaps.
- 1142 evidence records remain source-review gaps.
## Status definitions
@@ -37,7 +37,7 @@ is a review label, not a unique mechanic; one evidence record may belong to seve
| demon-stronghold-125 | 0 | 0 | 0 | 0 | 0 | 0 | intentional-empty |
| dungeon-of-dalanis | 229 | 162 | 26 | 5 | 21 | 0 | deferred-source-gap |
| forsaken-abbey | 251 | 101 | 77 | 0 | 77 | 8 | verified |
| grafu-castle | 249 | 193 | 50 | 10 | 40 | 0 | deferred-source-gap |
| grafu-castle | 249 | 193 | 56 | 10 | 46 | 5 | verified |
| hall-of-survivors | 363 | 201 | 84 | 34 | 50 | 9 | verified |
| hoto | 401 | 94 | 95 | 28 | 67 | 5 | verified |
| ice-dwarf-kingdom | 651 | 52 | 87 | 36 | 51 | 5 | verified |
@@ -69,6 +69,7 @@ is a review label, not a unique mechanic; one evidence record may belong to seve
| Flowing Chaos | `rw-fa-100561` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. |
| Cruel Eater | `rw-fa-100562` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. |
| Hollow Shell | `rw-fa-100563` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. |
| Larvanger Barkud | `rw-grafu-castle-105351` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. |
These attacks are intentionally playable and telegraphed HealerMan behavior. They are not
presented as original RuneWaker mechanics until authoritative source behavior is recovered.
@@ -77,7 +78,6 @@ presented as original RuneWaker mechanics until authoritative source behavior is
- `aeternal-circle`: 514 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.
- `dungeon-of-dalanis`: 229 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.
- `grafu-castle`: 249 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.
- `kal-turok-hive`: 4 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.
- `lair-demon-dragon`: 532 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.
- `tomb-seven-heroes`: 439 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.
@@ -108,10 +108,10 @@ presented as original RuneWaker mechanics until authoritative source behavior is
| Blocker group | Items | Clearing work |
| --- | ---: | --- |
| deferredSourceRows | 4113 | Review source semantics and add explicit source-spawn encounter mappings. |
| unresolvedMechanicsEvidence | 1136 | Review surrounding Lua/template context; specify full trigger, effect, and reset behavior. |
| unresolvedMechanicsEvidence | 1142 | Review surrounding Lua/template context; specify full trigger, effect, and reset behavior. |
| adaptedSpellFormula | 388 | Recover native formula inputs before claiming coefficient-level fidelity. |
| sourceGapFallbackBosses | 4 | Recover authoritative combat kits and replace only with source-backed modular mechanics. |
| nonEmptyZeroBossPackages | 8 | Recover authoritative boss membership and objective order. |
| sourceGapFallbackBosses | 5 | Recover authoritative combat kits and replace only with source-backed modular mechanics. |
| nonEmptyZeroBossPackages | 7 | Recover authoritative boss membership and objective order. |
The exhaustive per-item blocker, next-work instruction, clearing test, source IDs, source
files, and SHA-256 hashes are in `RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json`.
@@ -1,36 +1,36 @@
{
"schemaVersion": 1,
"validationStatus": "green",
"validationStatus": "blocked",
"fidelityStatus": "review-required",
"scope": "All configured offline RuneWaker dungeon packages",
"summary": {
"sourceRows": 8199,
"runtimeSpawns": 4086,
"runtimeEntities": 436,
"bossObjectives": 61,
"runtimeAttacks": 1068,
"genericDefaultAttacks": 436,
"runtimeSpawns": 4114,
"runtimeEntities": 446,
"bossObjectives": 66,
"runtimeAttacks": 1133,
"genericDefaultAttacks": 446,
"builderAuthoredAttacks": 0,
"recipeAuthoredAttacks": 0,
"catalogBossMechanics": 279,
"sourceBackedBossMechanics": 275,
"sourceGapBossMechanics": 4,
"telegraphedBossMechanics": 111,
"catalogBossMechanics": 334,
"sourceBackedBossMechanics": 329,
"sourceGapBossMechanics": 5,
"telegraphedBossMechanics": 143,
"healthTriggeredBossMechanics": 28,
"sourceBackedDamageFamilyAttacks": 483,
"sourceBackedDamageFamilyAttacks": 514,
"sourceSpellTemplates": 293,
"sourceSpellAssignments": 659,
"scriptedTemplates": 357,
"nativeBindings": 435,
"scriptedTemplates": 363,
"nativeBindings": 445,
"proceduralBindings": 1,
"proxyBindings": 0,
"objectiveRoutes": 61,
"blockedObjectiveRoutes": 0,
"sourceOrderedObjectiveRoutes": 0,
"objectiveRoutes": 66,
"blockedObjectiveRoutes": 3,
"sourceOrderedObjectiveRoutes": 5,
"derivedObjectiveRoutes": 61,
"unreachableDerivedObjectiveRoutes": 19,
"dungeons": 30,
"uniqueNativeModels": 321,
"uniqueNativeModels": 328,
"uniqueSourceSpellIds": 411,
"portableSpellCatalogRecords": 3199,
"portableDamageFamilyRecords": 1342,
@@ -79,9 +79,28 @@
],
"blockerGroups": {
"navigation": {
"blockedLegs": 0,
"affectedDungeons": 0,
"entries": [],
"blockedLegs": 3,
"affectedDungeons": 1,
"entries": [
{
"dungeonId": "grafu-castle",
"from": "entrance",
"to": "Cayus",
"objectiveId": "rw-grafu-castle-scripted-cayus"
},
{
"dungeonId": "grafu-castle",
"from": "Cayus",
"to": "Ardmond and Issac",
"objectiveId": "rw-grafu-castle-scripted-ardmond"
},
{
"dungeonId": "grafu-castle",
"from": "Ardmond and Issac",
"to": "Boro-Boro Brothers",
"objectiveId": "rw-grafu-castle-scripted-boro-boro-brothers"
}
],
"unreachableDerivedLegs": 19,
"derivedAffectedDungeons": 8,
"derivedEntries": [
@@ -223,10 +242,10 @@
"sourceMechanics": {
"status": "review-required",
"executableBossMechanicsGenerated": true,
"catalogBossMechanics": 279,
"sourceBackedBossMechanics": 275,
"sourceGapBossMechanics": 4,
"scriptedTemplates": 357,
"catalogBossMechanics": 334,
"sourceBackedBossMechanics": 329,
"sourceGapBossMechanics": 5,
"scriptedTemplates": 363,
"evidenceCounts": {
"death": 75,
"door": 22,
@@ -258,7 +277,7 @@
"validationStatus": "green",
"completionStatus": "review-required",
"deferredSourceRows": 4113,
"uniqueMechanicsEvidence": 1524
"uniqueMechanicsEvidence": 1530
},
"resolvedRuntimeObservations": [
{
@@ -272,11 +291,15 @@
"documentedExceptions": [
"/assets/creatures/sardo-castle/rune-device-red.glb: move exempt - Rune Device is a source-authored stationary encounter device; its native GLB retains stand, attack/activation, cast, wound, and death clips."
],
"issues": [],
"issues": [
"grafu-castle: packaged navigation cannot route entrance -> Cayus with the live 3m/6m endpoint limits.",
"grafu-castle: packaged navigation cannot route Cayus -> Ardmond and Issac with the live 3m/6m endpoint limits.",
"grafu-castle: packaged navigation cannot route Ardmond and Issac -> Boro-Boro Brothers with the live 3m/6m endpoint limits."
],
"reviewNotes": [
"demon-stronghold-125 has an authoritative empty Zone 125 snapshot, so it packages an environment but no combat population.",
"zurhidon-stronghold-124 has an authoritative empty Zone 124 snapshot, so it packages an environment but no combat population.",
"4 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.",
"5 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.",
"Source formula structure is preserved, but live damage uses a labeled HealerMan baseline adapter because the required native combat-stat inputs are not packaged.",
"61 objective legs use HealerMan's distance-from-entrance fallback because no source-authored boss order is packaged; 19 unreachable heuristic legs remain navigation/order review evidence."
],
@@ -774,38 +797,145 @@
"id": "grafu-castle",
"zoneId": 137,
"sourceRows": 249,
"runtimeSpawns": 56,
"runtimeEntities": 11,
"bossObjectives": 0,
"runtimeSpawns": 84,
"runtimeEntities": 21,
"bossObjectives": 5,
"mechanicsStatus": "review-required",
"objectiveOrderAuthoritative": false,
"objectiveOrderAuthoritative": true,
"objectiveOrderProvenance": {
"authoritative": false,
"kind": "healerMan-distance-derived",
"authoritative": true,
"kind": "source-authored",
"evidence": [
"scripts/runewaker-pipeline/lib/objective-order.mjs"
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
},
"objectiveRouteEvidence": [],
"runtimeAttacks": 21,
"genericDefaultAttacks": 11,
"objectiveRouteEvidence": [
{
"from": "entrance",
"to": "Cayus",
"objectiveId": "rw-grafu-castle-scripted-cayus",
"approachPosition": null,
"objectiveProjectionDistance": null,
"reachable": false,
"waypointCount": 0,
"orderAuthoritative": true,
"orderKind": "source-authored",
"orderEvidence": [
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
},
{
"from": "Cayus",
"to": "Ardmond and Issac",
"objectiveId": "rw-grafu-castle-scripted-ardmond",
"approachPosition": null,
"objectiveProjectionDistance": null,
"reachable": false,
"waypointCount": 0,
"orderAuthoritative": true,
"orderKind": "source-authored",
"orderEvidence": [
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
},
{
"from": "Ardmond and Issac",
"to": "Boro-Boro Brothers",
"objectiveId": "rw-grafu-castle-scripted-boro-boro-brothers",
"approachPosition": [
-176.317575,
-17.93685769127901,
-4.85641198856605
],
"objectiveProjectionDistance": 3.0893265615044496e-7,
"reachable": false,
"waypointCount": 0,
"orderAuthoritative": true,
"orderKind": "source-authored",
"orderEvidence": [
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
},
{
"from": "Boro-Boro Brothers",
"to": "Conjurer Chapeaunoir",
"objectiveId": "rw-grafu-castle-scripted-conjurer-chapeaunoir",
"approachPosition": [
-132.39614900162002,
-35.924047364518444,
-126.16280800000898
],
"objectiveProjectionDistance": 3.6452204204830595e-7,
"reachable": true,
"waypointCount": 113,
"orderAuthoritative": true,
"orderKind": "source-authored",
"orderEvidence": [
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
},
{
"from": "Conjurer Chapeaunoir",
"to": "Larvanger Barkud",
"objectiveId": "rw-grafu-castle-scripted-larvanger-barkud",
"approachPosition": [
10.319202,
-37.197608947753906,
-101.219726
],
"objectiveProjectionDistance": 5.2246093673602445e-8,
"reachable": true,
"waypointCount": 105,
"orderAuthoritative": true,
"orderKind": "source-authored",
"orderEvidence": [
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
}
],
"runtimeAttacks": 86,
"genericDefaultAttacks": 21,
"builderAuthoredAttacks": 0,
"recipeAuthoredAttacks": 0,
"catalogBossMechanics": 0,
"sourceBackedBossMechanics": 0,
"sourceGapBossMechanics": 0,
"telegraphedBossMechanics": 0,
"catalogBossMechanics": 55,
"sourceBackedBossMechanics": 54,
"sourceGapBossMechanics": 1,
"telegraphedBossMechanics": 32,
"healthTriggeredBossMechanics": 0,
"sourceBackedDamageFamilyAttacks": 10,
"nativeBindings": 11,
"sourceBackedDamageFamilyAttacks": 41,
"nativeBindings": 21,
"proceduralBindings": 0,
"proxyBindings": 0,
"sourceSpellTemplates": 6,
"sourceSpellAssignments": 17,
"scriptedTemplates": 15,
"objectiveRoutes": 0,
"blockedObjectiveRoutes": 0,
"sourceOrderedObjectiveRoutes": 0,
"scriptedTemplates": 21,
"objectiveRoutes": 5,
"blockedObjectiveRoutes": 3,
"sourceOrderedObjectiveRoutes": 5,
"derivedObjectiveRoutes": 0,
"unreachableDerivedObjectiveRoutes": 0
},
@@ -1,6 +1,6 @@
# RuneWaker gameplay content audit
Structural validation: **green**
Structural validation: **blocked**
Original-mechanics fidelity: **review-required**
This offline audit reads packaged recipes, snapshots, generated TypeScript, reports,
@@ -8,15 +8,15 @@ mechanics evidence, manifests, and creature GLBs. It never connects to SQL Serve
## Coverage
- 30 dungeons, 8199 source rows, 4086 static spawns, and 436 combat entities.
- 61 boss objectives and 1068 executable attacks.
- Source-authored objective navigation: 0/0 ordered entrance/boss legs are mesh-reachable.
- 30 dungeons, 8199 source rows, 4114 static spawns, and 446 combat entities.
- 66 boss objectives and 1133 executable attacks.
- Source-authored objective navigation: 2/5 ordered entrance/boss legs are mesh-reachable.
- HealerMan-derived objective diagnostics: 42/61 distance-ordered legs are mesh-reachable; unreachable derived legs are review evidence, not structural blockers.
- Animation entity bindings: 435 native, 1 static procedural, and 0 proxy.
- 293 templates preserve source spell evidence (659 assignments; 411 unique IDs); 357 preserve AutoPlot/Lua hook evidence.
- 279 modular boss mechanics are active: 275 source-backed and 4 explicitly flagged source-gap fallbacks.
- Boss signaling includes 111 spatial telegraphs and 28 visible health-triggered mechanics.
- The exhaustive encounter implementation ledger tracks 4113 deferred source rows and 1524 unique mechanics evidence records without inflating overlapping regex labels.
- Animation entity bindings: 445 native, 1 static procedural, and 0 proxy.
- 293 templates preserve source spell evidence (659 assignments; 411 unique IDs); 363 preserve AutoPlot/Lua hook evidence.
- 334 modular boss mechanics are active: 329 source-backed and 5 explicitly flagged source-gap fallbacks.
- Boss signaling includes 143 spatial telegraphs and 28 visible health-triggered mechanics.
- The exhaustive encounter implementation ledger tracks 4113 deferred source rows and 1530 unique mechanics evidence records without inflating overlapping regex labels.
## Authentic source data currently used
@@ -28,9 +28,9 @@ mechanics evidence, manifests, and creature GLBs. It never connects to SQL Serve
## HealerMan-generic or deferred
- 436 entities use generated Basic Attack/Corrosive Slam.
- 446 entities use generated Basic Attack/Corrosive Slam.
- 0 attacks are builder exceptions outside the modular boss catalog.
- 483 executable attacks carry a conservative source-backed direct-damage family mapping.
- 514 executable attacks carry a conservative source-backed direct-damage family mapping.
- Source-backed mapping preserves spell identity, damage family, school, range, cooldown,
source power fields, and the exact native formula shape/provenance.
- Damage execution uses a labeled HealerMan baseline adapter because native ATK/MATK,
@@ -53,7 +53,7 @@ Animation columns are native/procedural/proxy entity bindings.
| demon-stronghold-125 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0/0/0 | 0/0 derived | review-required |
| dungeon-of-dalanis | 229 | 67 | 17 | 0 | 22 | 9 | 13 | 17/0/0 | 0/0 derived | review-required |
| forsaken-abbey | 251 | 150 | 15 | 8 | 26 | 0 | 11 | 15/0/0 | 8/8 derived | review-required |
| grafu-castle | 249 | 56 | 11 | 0 | 21 | 6 | 15 | 11/0/0 | 0/0 derived | review-required |
| grafu-castle | 249 | 84 | 21 | 5 | 86 | 6 | 21 | 21/0/0 | 2/5 source | review-required |
| hall-of-survivors | 363 | 162 | 40 | 9 | 122 | 23 | 25 | 39/1/0 | 9/9 derived | review-required |
| hoto | 401 | 307 | 26 | 5 | 75 | 19 | 15 | 26/0/0 | 2/5 derived | review-required |
| ice-dwarf-kingdom | 651 | 599 | 32 | 5 | 80 | 25 | 11 | 32/0/0 | 3/5 derived | review-required |
@@ -91,8 +91,11 @@ Animation columns are native/procedural/proxy entity bindings.
## Remaining blocker groups
Navigation: 0 blocked source-authored ordered legs across 0 dungeons.
Navigation: 3 blocked source-authored ordered legs across 1 dungeons.
- grafu-castle: entrance -> Cayus
- grafu-castle: Cayus -> Ardmond and Issac
- grafu-castle: Ardmond and Issac -> Boro-Boro Brothers
Derived-order navigation review: 19 unreachable heuristic legs across 8 dungeons.
@@ -116,7 +119,7 @@ Derived-order navigation review: 19 unreachable heuristic legs across 8 dungeons
- ystra-labyrinth-boss1: Specter Guardian Creator -> Raichika the Destroyer (healerMan-distance-derived)
- ystra-labyrinth-boss1: Raichika the Destroyer -> Rune Guardian Leader (healerMan-distance-derived)
Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported boss combat behavior is compiled into the modular catalog.
Source-mechanics review: 363 templates preserve AutoPlot/Lua evidence; supported boss combat behavior is compiled into the modular catalog.
- death: 75
- door: 22
@@ -130,7 +133,9 @@ Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported
## Validation issues
- None.
- grafu-castle: packaged navigation cannot route entrance -> Cayus with the live 3m/6m endpoint limits.
- grafu-castle: packaged navigation cannot route Cayus -> Ardmond and Issac with the live 3m/6m endpoint limits.
- grafu-castle: packaged navigation cannot route Ardmond and Issac -> Boro-Boro Brothers with the live 3m/6m endpoint limits.
## Documented exceptions
@@ -144,6 +149,6 @@ Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported
- demon-stronghold-125 has an authoritative empty Zone 125 snapshot, so it packages an environment but no combat population.
- zurhidon-stronghold-124 has an authoritative empty Zone 124 snapshot, so it packages an environment but no combat population.
- 4 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.
- 5 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.
- Source formula structure is preserved, but live damage uses a labeled HealerMan baseline adapter because the required native combat-stat inputs are not packaged.
- 61 objective legs use HealerMan's distance-from-entrance fallback because no source-authored boss order is packaged; 19 unreachable heuristic legs remain navigation/order review evidence.
@@ -437,7 +437,8 @@ for (const recipeName of recipeNames) {
sourceTemplateIds.add(row.templateId);
const template = templateById.get(row.templateId);
if (!template) issue(issues, dungeonId, "source template " + row.templateId + " is unmapped.");
else if (template.classification !== row.classification) {
else if (template.classification !== row.classification
&& template.source?.classificationRule !== "reviewed Lua controller override") {
issue(issues, dungeonId, "classification drift for template " + row.templateId + ".");
}
}
@@ -651,8 +652,13 @@ for (const recipeName of recipeNames) {
}
if (!boss.name?.trim()) issue(issues, dungeonId, boss.id + " objective has no name.");
}
const runtimePrefix = recipe.runtimeIdPrefix ?? "rw-" + dungeonId;
const nonObjectiveBossIds = new Set((recipe.scriptedSpawns ?? [])
.filter((placement) => placement.objective === false)
.map((placement) => runtimePrefix + "-scripted-" + placement.id));
for (const spawn of spawns) {
if (entities[spawn.entityId]?.kind === "boss" && !bossIds.has(spawn.id)) {
if (entities[spawn.entityId]?.kind === "boss" && !bossIds.has(spawn.id)
&& !nonObjectiveBossIds.has(spawn.id)) {
issue(issues, dungeonId, spawn.id + " boss has no objective.");
}
}
@@ -702,8 +708,15 @@ for (const recipeName of recipeNames) {
!route.orderAuthoritative && !route.reachable
)).length;
const scriptedTemplateIds = new Set([
...(recipe.scriptedSpawns ?? []).map((placement) => placement.templateId),
...(recipe.scriptedFormations ?? []).flatMap((formation) => (
formation.members.map((member) => member.templateId)
)),
]);
const expectedEntities = recipe.templates.filter((template) => (
["combat", "boss"].includes(template.classification) && sourceTemplateIds.has(template.id)
["combat", "boss"].includes(template.classification)
&& (sourceTemplateIds.has(template.id) || scriptedTemplateIds.has(template.id))
)).length;
if (expectedEntities !== Object.keys(entities).length) {
issue(issues, dungeonId, "combat source templates and entity counts disagree.");
+102 -2
View File
@@ -431,6 +431,35 @@ function yawFromDirection(direction) {
return Number(((((direction % turn) + turn) % turn) * Math.PI * 2 / turn).toFixed(9));
}
function scriptedPlacements() {
const placements = [...(recipe.scriptedSpawns ?? [])];
for (const formation of recipe.scriptedFormations ?? []) {
const radians = formation.direction * Math.PI / 180;
for (const [index, member] of formation.members.entries()) {
const rowDistance = (member.row - 1) * formation.spacingSourceUnits;
const columnDistance = member.column * formation.spacingSourceUnits;
placements.push({
id: formation.id + "-" + (index + 1),
templateId: member.templateId,
sourceAnchorSpawnId: formation.sourceAnchorSpawnId,
controllerSpawnId: formation.controllerSpawnId ?? formation.sourceAnchorSpawnId,
sourceOffset: [
-rowDistance * Math.cos(radians) - columnDistance * Math.sin(radians),
0,
rowDistance * Math.sin(radians) - columnDistance * Math.cos(radians),
],
direction: formation.direction,
sourceScriptRoot: formation.sourceScriptRoot,
evidence: formation.evidence,
});
}
}
if (new Set(placements.map((placement) => placement.id)).size !== placements.length) {
throw new Error("Scripted placement ids must be unique after formation expansion.");
}
return placements;
}
function attacksFor(template, sourceSpells, spellCatalog, bossMechanics) {
const ooze = template.archetype === "ooze";
const basicAttack = {
@@ -564,7 +593,8 @@ function validateSnapshot(snapshot, templates) {
const template = templates.get(row.templateId);
if (!template) throw new Error("Unmapped RuneWaker template " + row.templateId + ".");
if (template.classification === "unclassified") throw new Error("Unreviewed template " + row.templateId + ".");
if (row.classification !== template.classification) {
if (row.classification !== template.classification
&& template.source?.classificationRule !== "reviewed Lua controller override") {
throw new Error("Classification drift for template " + row.templateId + ".");
}
const actor = recipe.actors.find((entry) => entry.id === template.actor);
@@ -636,6 +666,8 @@ async function generatePopulation() {
const records = [];
const rawBosses = [];
const countsByTemplate = {};
const rowsBySpawnId = new Map(snapshot.rows.map((row) => [row.spawnId, row]));
const scripted = scriptedPlacements();
try {
for (const row of snapshot.rows) {
const template = templates.get(row.templateId);
@@ -648,7 +680,8 @@ async function generatePopulation() {
sourceSpawnId: row.spawnId,
sourceTemplateId: row.templateId,
name: row.name,
classification: row.classification,
classification: template.classification,
sourceClassification: row.classification,
sourcePosition: row.sourcePosition,
sourceDirection: row.direction,
localPosition: local.map((value) => Number(value.toFixed(6))),
@@ -687,6 +720,72 @@ async function generatePopulation() {
});
}
}
for (const placementDefinition of scripted) {
const anchor = rowsBySpawnId.get(placementDefinition.sourceAnchorSpawnId);
if (!anchor) {
throw new Error(
"Scripted placement " + placementDefinition.id + " has no source anchor row "
+ placementDefinition.sourceAnchorSpawnId + ".",
);
}
if (placementDefinition.controllerSpawnId !== undefined
&& !rowsBySpawnId.has(placementDefinition.controllerSpawnId)) {
throw new Error("Scripted placement " + placementDefinition.id + " has no controller row " + placementDefinition.controllerSpawnId + ".");
}
const template = templates.get(placementDefinition.templateId);
const offset = placementDefinition.sourceOffset ?? [0, 0, 0];
const sourcePosition = anchor.sourcePosition.map((value, index) => value + offset[index]);
const direction = placementDefinition.direction ?? anchor.direction;
const local = sourceToLocal(sourcePosition, placement);
const projected = projector.project(local);
if (projected.distance > recipe.coordinateSystem.maximumProjectionDistance) {
throw new Error("Scripted placement " + placementDefinition.id + " is " + projected.distance + "m from navigation.");
}
const entityId = runtimePrefix + "-" + template.id;
if (!entities[entityId]) {
entities[entityId] = entityFor(
template,
actorForTemplate(template),
[...(sourceSpellsByTemplate.get(template.id)?.values() ?? [])],
spellCatalog,
bossMechanics,
);
}
const spawn = {
id: runtimePrefix + "-scripted-" + placementDefinition.id,
entityId,
position: projected.position,
yaw: yawFromDirection(direction),
spawnMask: template.spawnMask ?? 1,
};
staticSpawns.push(spawn);
records.push({
sourceSpawnId: anchor.spawnId,
sourceTemplateId: template.id,
scriptedPlacementId: placementDefinition.id,
sourceControllerSpawnId: placementDefinition.controllerSpawnId ?? null,
sourceScriptRoot: placementDefinition.sourceScriptRoot ?? null,
evidence: placementDefinition.evidence,
name: template.name,
classification: template.classification,
sourcePosition,
sourceDirection: direction,
localPosition: local.map((value) => Number(value.toFixed(6))),
projectedPosition: projected.position,
projectionDistance: projected.distance,
modelPath: template.source?.modelPath ?? null,
});
countsByTemplate[template.id] = (countsByTemplate[template.id] ?? 0) + 1;
if (template.classification === "boss" && placementDefinition.objective !== false) {
rawBosses.push({
id: spawn.id,
sourceSpawnId: anchor.spawnId,
templateId: template.id,
name: template.name,
position: spawn.position,
});
}
}
} finally {
projector.dispose();
}
@@ -720,6 +819,7 @@ async function generatePopulation() {
zoneId: recipe.zoneId,
sourceRowCount: snapshot.rows.length,
importedSpawnCount: staticSpawns.length,
scriptedSpawnCount: scripted.length,
excludedRowCount: excluded.length,
bossSpawnCount: bosses.length,
objectiveOrder: objectiveOrder.provenance,
@@ -12,9 +12,9 @@ test("encounter ledger exhaustively classifies portable RuneWaker evidence", asy
assert.equal(report.validationStatus, "green", report.issues.join("\n"));
assert.equal(report.summary.dungeons, 30);
assert.equal(report.summary.deferredSourceRows, 4_113);
assert.equal(report.summary.uniqueMechanicsEvidence, 1_524);
assert.equal(report.summary.uniqueMechanicsEvidence, 1_530);
assert.equal(report.summary.luaEvidenceSignals, 329);
assert.equal(report.summary.templateEvidenceSignals, 1_195);
assert.equal(report.summary.templateEvidenceSignals, 1_201);
const inventory = report.dungeons.flatMap((dungeon) => [
...dungeon.deferredSourceRows,
@@ -54,7 +54,13 @@ test("source gaps, fallbacks, zero-boss packages, and empty variants stay explic
const report = await buildEncounterImplementationReport();
assert.deepEqual(
report.specialCases.fallbackBosses.map((boss) => boss.entityId),
["rw-fa-100070", "rw-fa-100561", "rw-fa-100562", "rw-fa-100563"],
[
"rw-fa-100070",
"rw-fa-100561",
"rw-fa-100562",
"rw-fa-100563",
"rw-grafu-castle-105351",
],
);
assert.ok(report.specialCases.fallbackBosses.every((boss) => (
boss.status === "implemented" && boss.fidelity === "healer-man-authored"
@@ -68,7 +74,6 @@ test("source gaps, fallbacks, zero-boss packages, and empty variants stay explic
[
"aeternal-circle",
"dungeon-of-dalanis",
"grafu-castle",
"kal-turok-hive",
"lair-demon-dragon",
"tomb-seven-heroes",
@@ -44,6 +44,8 @@ const config = await readJson(configFile);
const dungeon = (config.dungeons ?? []).find((entry) => entry.id === dungeonConfigId);
if (!dungeon) throw new Error("Missing dungeon_config entry " + dungeonConfigId + ".");
const snapshot = await readJson(snapshotFile);
const templatesById = new Map(recipe.templates.map((template) => [template.id, template]));
const rowsBySpawnId = new Map((snapshot.rows ?? []).map((row) => [row.spawnId, row]));
const SIGNALS = [
{ kind: "summon", pattern: /summon|spawn.*npc|create.*npc|create.*monster|add.*npc/i },
@@ -115,10 +117,11 @@ for (const fileName of dungeon.lua_files ?? []) {
const templateMetadata = new Map();
for (const row of snapshot.rows ?? []) {
const template = templatesById.get(row.templateId);
const metadata = templateMetadata.get(row.templateId) ?? {
templateId: row.templateId,
name: row.name,
classification: row.classification,
name: template?.name ?? row.name,
classification: template?.classification ?? row.classification,
spawnCount: 0,
autoPlots: new Set(),
plotClassNames: new Set(),
@@ -139,6 +142,19 @@ for (const row of snapshot.rows ?? []) {
}
templateMetadata.set(row.templateId, metadata);
}
for (const template of recipe.templates) {
if (templateMetadata.has(template.id)) continue;
templateMetadata.set(template.id, {
templateId: template.id,
name: template.name,
classification: template.classification,
spawnCount: 0,
autoPlots: new Set(template.source?.scriptRoots ?? []),
plotClassNames: new Set(),
templateScripts: new Map(),
spells: new Map(),
});
}
const templateCandidates = [...templateMetadata.values()]
.map((entry) => ({
@@ -155,7 +171,7 @@ const templateCandidates = [...templateMetadata.values()]
.sort((left, right) => left.templateId - right.templateId);
const bossRows = (snapshot.rows ?? [])
.filter((row) => row.classification === "boss")
.filter((row) => templatesById.get(row.templateId)?.classification === "boss")
.map((row) => ({
spawnId: row.spawnId,
templateId: row.templateId,
@@ -164,6 +180,26 @@ const bossRows = (snapshot.rows ?? [])
autoPlot: clean(row.source?.autoPlot) || null,
plotClassName: clean(row.source?.plotClassName) || null,
}));
const scriptedBossSpawns = (recipe.scriptedSpawns ?? [])
.filter((spawn) => templatesById.get(spawn.templateId)?.classification === "boss")
.map((spawn) => {
const template = templatesById.get(spawn.templateId);
const anchor = rowsBySpawnId.get(spawn.sourceAnchorSpawnId);
const offset = spawn.sourceOffset ?? [0, 0, 0];
return {
scriptedPlacementId: spawn.id,
sourceAnchorSpawnId: spawn.sourceAnchorSpawnId,
controllerSpawnId: spawn.controllerSpawnId ?? null,
templateId: spawn.templateId,
name: template.name,
sourcePosition: anchor
? anchor.sourcePosition.map((value, index) => value + offset[index])
: null,
sourceScriptRoot: spawn.sourceScriptRoot ?? template.source?.scriptRoots?.[0] ?? null,
objective: spawn.objective !== false,
evidence: spawn.evidence,
};
});
const categories = Object.fromEntries(SIGNALS.map(({ kind }) => [
kind,
luaSignals.filter((signal) => signal.kinds.includes(kind)).length
@@ -186,8 +222,12 @@ const report = {
dungeonConfigId,
},
configuredBossTemplateIds: dungeon.boss_guids ?? [],
reviewedBossTemplateIds: recipe.templates
.filter((template) => template.classification === "boss")
.map((template) => template.id),
configuredBossPositions: dungeon.boss_positions ?? {},
bossRows,
scriptedBossSpawns,
luaSources,
luaSignals,
templateCandidates,
@@ -225,6 +265,7 @@ const generated = [
status: report.status,
categories: report.categories,
bossRows: report.bossRows,
scriptedBossSpawns: report.scriptedBossSpawns,
templateCandidates: report.templateCandidates,
reviewQueue: report.reviewQueue,
sourceReport: projectPath(reportFile),
@@ -2,6 +2,7 @@
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import {
optionValue,
pipelineDirectory,
projectPath,
projectRoot,
@@ -16,9 +17,12 @@ const spellCatalogFile = path.join(
"snapshots",
"runewaker-npc-spell-catalog.json",
);
const recipeNames = (await readdir(recipesDirectory))
.filter((name) => name.endsWith("-population.json"))
.sort();
const requestedRecipe = optionValue(process.argv.slice(2), "--recipe");
const recipeNames = requestedRecipe
? [path.basename(requestedRecipe)]
: (await readdir(recipesDirectory))
.filter((name) => name.endsWith("-population.json"))
.sort();
function slugify(value) {
return String(value ?? "")
@@ -407,6 +411,7 @@ const spellCatalog = await readJson(spellCatalogFile);
const spellsById = new Map(spellCatalog.spells.map((spell) => [Number(spell.spellId), spell]));
const catalog = await readJson(mechanicCatalogFile);
const runewakerBosses = {};
const replacedRuntimePrefixes = new Set();
let sourceBacked = 0;
let sourceGap = 0;
const firstRecipe = await readJson(path.join(recipesDirectory, recipeNames[0]));
@@ -423,6 +428,7 @@ const sharedFunctions = functionIndex(sharedLuaSources);
for (const recipeName of recipeNames) {
const recipe = await readJson(path.join(recipesDirectory, recipeName));
replacedRuntimePrefixes.add(recipe.runtimeIdPrefix ?? "rw-" + recipe.dungeonId);
const snapshot = await readJson(path.resolve(projectRoot, recipe.files.snapshot));
const rowsByTemplate = new Map();
for (const row of snapshot.rows ?? []) {
@@ -434,12 +440,15 @@ for (const recipeName of recipeNames) {
for (const template of recipe.templates.filter((entry) => entry.classification === "boss")) {
const rows = rowsByTemplate.get(Number(template.id)) ?? [];
if (!rows.length) continue;
const roots = [...new Set(rows.flatMap((row) => [
rootName(row.source?.autoPlot),
rootName(row.source?.plotClassName),
...Object.values(row.source?.templateScripts ?? {}).map(rootName),
]).filter(Boolean))];
const roots = [...new Set([
...(template.source?.scriptRoots ?? []).map(rootName),
...rows.flatMap((row) => [
rootName(row.source?.autoPlot),
rootName(row.source?.plotClassName),
...Object.values(row.source?.templateScripts ?? {}).map(rootName),
]),
].filter(Boolean))];
if (!rows.length && !roots.length) continue;
const reachable = reachableFunctions(sharedFunctions, roots);
const bodies = reachable.map((entry) => entry.body);
const scriptSpellIds = [...new Set(bodies.flatMap((body) => (
@@ -500,7 +509,11 @@ for (const recipeName of recipeNames) {
}
catalog.bosses = Object.fromEntries([
...Object.entries(catalog.bosses).filter(([key]) => !key.startsWith("rw-")),
...Object.entries(catalog.bosses).filter(([key]) => (
requestedRecipe
? ![...replacedRuntimePrefixes].some((prefix) => key.startsWith(prefix + "-"))
: !key.startsWith("rw-")
)),
...Object.entries(runewakerBosses).sort(([left], [right]) => left.localeCompare(right)),
]);
await writeFile(mechanicCatalogFile, JSON.stringify(catalog, null, 2) + "\n", "utf8");
@@ -34,11 +34,11 @@ const EXPECTED_FALLBACK_BOSSES = Object.freeze([
"rw-fa-100561",
"rw-fa-100562",
"rw-fa-100563",
"rw-grafu-castle-105351",
]);
const EXPECTED_NON_EMPTY_ZERO_BOSS_PACKAGES = Object.freeze([
"aeternal-circle",
"dungeon-of-dalanis",
"grafu-castle",
"kal-turok-hive",
"lair-demon-dragon",
"tomb-seven-heroes",
+69
View File
@@ -148,6 +148,7 @@ export function validatePopulationRecipe(recipe, file = "population recipe") {
actorIds.add(actor.id);
}
const templateIds = new Set();
const templatesById = new Map();
const classifications = new Set(["combat", "boss", "deferred-object", "unclassified"]);
for (const template of recipe.templates) {
if (!Number.isInteger(template.id)) throw new Error("Template ids must be integers.");
@@ -161,6 +162,74 @@ export function validatePopulationRecipe(recipe, file = "population recipe") {
&& template.proceduralFallback !== true) {
throw new Error("Combat template " + template.id + " has no mapped actor or explicit procedural fallback.");
}
if (template.source?.scriptRoots !== undefined
&& (!Array.isArray(template.source.scriptRoots)
|| template.source.scriptRoots.some((root) => typeof root !== "string" || !root.trim()))) {
throw new Error("Template " + template.id + " source.scriptRoots must contain non-empty strings.");
}
templatesById.set(template.id, template);
}
const scriptedIds = new Set();
const validateScriptedBase = (entry, label) => {
assertObject(entry, label);
assertString(entry.id, label + ".id");
if (scriptedIds.has(entry.id)) throw new Error("Duplicate scripted placement id " + entry.id + ".");
scriptedIds.add(entry.id);
if (!Number.isInteger(entry.sourceAnchorSpawnId) || entry.sourceAnchorSpawnId <= 0) {
throw new Error(label + ".sourceAnchorSpawnId must be a positive integer.");
}
if (entry.controllerSpawnId !== undefined
&& (!Number.isInteger(entry.controllerSpawnId) || entry.controllerSpawnId <= 0)) {
throw new Error(label + ".controllerSpawnId must be a positive integer.");
}
if (!Array.isArray(entry.evidence) || !entry.evidence.length
|| entry.evidence.some((value) => typeof value !== "string" || !value.trim())) {
throw new Error(label + ".evidence must contain non-empty source references.");
}
};
for (const [index, spawn] of (recipe.scriptedSpawns ?? []).entries()) {
const label = file + ".scriptedSpawns[" + index + "]";
validateScriptedBase(spawn, label);
const template = templatesById.get(spawn.templateId);
if (!template || !["combat", "boss"].includes(template.classification)) {
throw new Error(label + ".templateId must reference a combat or boss template.");
}
if (spawn.sourceOffset !== undefined
&& (!Array.isArray(spawn.sourceOffset) || spawn.sourceOffset.length !== 3
|| spawn.sourceOffset.some((value) => !Number.isFinite(value)))) {
throw new Error(label + ".sourceOffset must contain three finite source-unit values.");
}
if (spawn.direction !== undefined && !Number.isFinite(spawn.direction)) {
throw new Error(label + ".direction must be finite.");
}
if (spawn.objective !== undefined && typeof spawn.objective !== "boolean") {
throw new Error(label + ".objective must be boolean when provided.");
}
}
for (const [index, formation] of (recipe.scriptedFormations ?? []).entries()) {
const label = file + ".scriptedFormations[" + index + "]";
validateScriptedBase(formation, label);
assertString(formation.sourceScriptRoot, label + ".sourceScriptRoot");
if (!(Number(formation.spacingSourceUnits) > 0)) {
throw new Error(label + ".spacingSourceUnits must be positive.");
}
if (!Number.isFinite(formation.direction)) {
throw new Error(label + ".direction must be finite.");
}
if (!Array.isArray(formation.members) || !formation.members.length) {
throw new Error(label + ".members must be a non-empty array.");
}
for (const [memberIndex, member] of formation.members.entries()) {
const memberLabel = label + ".members[" + memberIndex + "]";
assertObject(member, memberLabel);
const template = templatesById.get(member.templateId);
if (!template || !["combat", "boss"].includes(template.classification)) {
throw new Error(memberLabel + ".templateId must reference a combat or boss template.");
}
if (!Number.isInteger(member.row) || member.row < 1 || !Number.isInteger(member.column)) {
throw new Error(memberLabel + " requires a positive integer row and integer column.");
}
}
}
return recipe;
}
@@ -4,13 +4,273 @@
"sourceDungeonConfigId": "grafu_castle",
"zoneId": 137,
"expectedActiveRows": 249,
"expectedBossSpawns": 0,
"expectedBossSpawns": 5,
"runtimeIdPrefix": "rw-grafu-castle",
"coordinateSystem": {
"metersPerSourceUnit": 0.1,
"directionUnitsPerTurn": 360,
"maximumProjectionDistance": 4
},
"bossObjectiveOrder": {
"kind": "source-authored",
"spawnIds": [
41927224,
41926939,
41926924,
41926979,
41927071
],
"evidence": [
"Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC",
"Resource/luascript/02324.lua:kg_105542_01",
"Resource/luascript/02231.lua:ic_105224_01",
"Resource/luascript/02330.lua:cl_136_105205_N",
"Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01"
]
},
"scriptedSpawns": [
{
"id": "cayus",
"templateId": 105616,
"sourceAnchorSpawnId": 41927224,
"controllerSpawnId": 41928351,
"sourceScriptRoot": "Dy_Z19_BOSSN_AIC",
"evidence": [
"Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)"
]
},
{
"id": "ardmond",
"templateId": 105542,
"sourceAnchorSpawnId": 41926939,
"controllerSpawnId": 41928345,
"sourceOffset": [
0,
0,
-20
],
"sourceScriptRoot": "kg_105542_01",
"evidence": [
"Resource/luascript/02324.lua:kg_105542_CreateBoss"
]
},
{
"id": "issac",
"templateId": 105543,
"sourceAnchorSpawnId": 41926939,
"controllerSpawnId": 41928345,
"sourceOffset": [
0,
0,
20
],
"objective": false,
"sourceScriptRoot": "kg_105542_01",
"evidence": [
"Resource/luascript/02324.lua:kg_105542_CreateBoss"
]
},
{
"id": "boro-boro-brothers",
"templateId": 105224,
"sourceAnchorSpawnId": 41926924,
"controllerSpawnId": 41927347,
"sourceScriptRoot": "ic_105224_01",
"evidence": [
"Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)"
]
},
{
"id": "conjurer-chapeaunoir",
"templateId": 105204,
"sourceAnchorSpawnId": 41926979,
"controllerSpawnId": 41927358,
"sourceScriptRoot": "cl_136_105205_N",
"evidence": [
"Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)"
]
},
{
"id": "larvanger-barkud",
"templateId": 105351,
"sourceAnchorSpawnId": 41927071,
"controllerSpawnId": 41927369,
"sourceScriptRoot": "un_zone19_boss5_allconreal_1_01",
"evidence": [
"Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)"
]
}
],
"scriptedFormations": [
{
"id": "castle-patrol-41927381",
"sourceAnchorSpawnId": 41927381,
"controllerSpawnId": 41927381,
"sourceScriptRoot": "cl_136_01_01_N",
"direction": 179.87330627441406,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105598, "row": 1, "column": 0 },
{ "templateId": 105229, "row": 2, "column": 1 },
{ "templateId": 105229, "row": 2, "column": -1 },
{ "templateId": 105230, "row": 3, "column": 0 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_01_01_N"
]
},
{
"id": "castle-patrol-41927382",
"sourceAnchorSpawnId": 41927382,
"controllerSpawnId": 41927382,
"sourceScriptRoot": "cl_136_01_01_N",
"direction": 269.88189697265625,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105598, "row": 1, "column": 0 },
{ "templateId": 105229, "row": 2, "column": 1 },
{ "templateId": 105229, "row": 2, "column": -1 },
{ "templateId": 105230, "row": 3, "column": 0 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_01_01_N"
]
},
{
"id": "castle-patrol-41927384",
"sourceAnchorSpawnId": 41927384,
"controllerSpawnId": 41927384,
"sourceScriptRoot": "cl_136_03_01_N",
"direction": 89.27565002441406,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105596, "row": 1, "column": 0 },
{ "templateId": 105597, "row": 2, "column": 1 },
{ "templateId": 105597, "row": 2, "column": -1 },
{ "templateId": 105601, "row": 3, "column": 0 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_03_01_N"
]
},
{
"id": "castle-patrol-41927385",
"sourceAnchorSpawnId": 41927385,
"controllerSpawnId": 41927385,
"sourceScriptRoot": "cl_136_03_02_N",
"direction": 180.14939880371094,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105599, "row": 1, "column": 0 },
{ "templateId": 105598, "row": 2, "column": 1 },
{ "templateId": 105598, "row": 2, "column": -1 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_03_02_N"
]
},
{
"id": "castle-patrol-41927386",
"sourceAnchorSpawnId": 41927386,
"controllerSpawnId": 41927386,
"sourceScriptRoot": "cl_136_03_04_N",
"direction": 0.1327040046453476,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105599, "row": 1, "column": 0 },
{ "templateId": 105598, "row": 2, "column": 1 },
{ "templateId": 105598, "row": 2, "column": -1 },
{ "templateId": 105601, "row": 3, "column": 0 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_03_04_N"
]
},
{
"id": "castle-patrol-41927387",
"sourceAnchorSpawnId": 41927387,
"controllerSpawnId": 41927387,
"sourceScriptRoot": "cl_136_03_042_N",
"direction": 86.2711410522461,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105599, "row": 1, "column": 0 },
{ "templateId": 105601, "row": 2, "column": 1 },
{ "templateId": 105601, "row": 2, "column": -1 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_03_042_N"
]
},
{
"id": "castle-patrol-41927391",
"sourceAnchorSpawnId": 41927391,
"controllerSpawnId": 41927391,
"sourceScriptRoot": "cl_136_04_052",
"direction": 89,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105237, "row": 1, "column": 0 },
{ "templateId": 105236, "row": 2, "column": 1 },
{ "templateId": 105236, "row": 2, "column": -1 },
{ "templateId": 105237, "row": 3, "column": 0 },
{ "templateId": 105236, "row": 3, "column": -1 },
{ "templateId": 105236, "row": 3, "column": 1 }
],
"evidence": [
"Resource/luascript/02214.lua:cl_136_04_052"
]
},
{
"id": "castle-patrol-41927392",
"sourceAnchorSpawnId": 41927392,
"controllerSpawnId": 41927392,
"sourceScriptRoot": "cl_136_01_01_N",
"direction": 269.99481201171875,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105598, "row": 1, "column": 0 },
{ "templateId": 105229, "row": 2, "column": 1 },
{ "templateId": 105229, "row": 2, "column": -1 },
{ "templateId": 105230, "row": 3, "column": 0 }
],
"evidence": [
"Resource/luascript/02334.lua:cl_136_01_01_N"
]
},
{
"id": "castle-patrol-41927395",
"sourceAnchorSpawnId": 41927395,
"controllerSpawnId": 41927395,
"sourceScriptRoot": "cl_136_03_042",
"direction": 181,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105233, "row": 1, "column": 0 },
{ "templateId": 105235, "row": 2, "column": 1 },
{ "templateId": 105235, "row": 2, "column": -1 }
],
"evidence": [
"Resource/luascript/02214.lua:cl_136_03_042"
]
},
{
"id": "castle-patrol-41927396",
"sourceAnchorSpawnId": 41927396,
"controllerSpawnId": 41927396,
"sourceScriptRoot": "cl_136_03_042",
"direction": 4.999985218048096,
"spacingSourceUnits": 20,
"members": [
{ "templateId": 105233, "row": 1, "column": 0 },
{ "templateId": 105235, "row": 2, "column": 1 },
{ "templateId": 105235, "row": 2, "column": -1 }
],
"evidence": [
"Resource/luascript/02214.lua:cl_136_03_042"
]
}
],
"files": {
"environmentRecipe": "scripts/runewaker-pipeline/recipes/grafu-castle.json",
"snapshot": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json",
@@ -43,6 +303,21 @@
"serverDefault": ".\\HEALERMAN_RW"
},
"actors": [
{
"id": "atmo",
"sourceModel": "model/character/creature/atmo/act_atmo.ros",
"runtimeRotationY": 0
},
{
"id": "barbo-brother",
"sourceModel": "model/character/creature/barbo_brother/act_barbo_brother.ros",
"runtimeRotationY": 0
},
{
"id": "black-hat",
"sourceModel": "model/character/creature/black_hat/act_black_hat.ros",
"runtimeRotationY": 0
},
{
"id": "black-hat-blue",
"sourceModel": "model/character/creature/black_hat/act_black_hat_blue.ros",
@@ -63,6 +338,11 @@
"sourceModel": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros",
"runtimeRotationY": 0
},
{
"id": "cursed-steel-guardian",
"sourceModel": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros",
"runtimeRotationY": 0
},
{
"id": "elder-myconid",
"sourceModel": "model/character/creature/elder_myconid/act_elder_myconid.ros",
@@ -83,6 +363,11 @@
"sourceModel": "model/character/pc/human/male/act_hm_male.ros",
"runtimeRotationY": 0
},
{
"id": "karth",
"sourceModel": "model/character/creature/karth/act_karth.ros",
"runtimeRotationY": 0
},
{
"id": "karth-01",
"sourceModel": "model/character/creature/karth/act_karth_01.ros",
@@ -92,6 +377,26 @@
"id": "milk-scarab-blue",
"sourceModel": "model/character/creature/milk_scarab/act_milk_scarab_blue.ros",
"runtimeRotationY": 0
},
{
"id": "mount-millenium-chimera-laby",
"sourceModel": "model/character/creature/mount_millenium_chimera/act_mount_millenium_chimera_laby.ros",
"runtimeRotationY": 0
},
{
"id": "enraged-big-bear-purple",
"sourceModel": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros",
"runtimeRotationY": 0
},
{
"id": "enraged-big-bear-cream",
"sourceModel": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros",
"runtimeRotationY": 0
},
{
"id": "rafael-bakderr",
"sourceModel": "model/character/creature/rafael_bakderr/act_rafael_bakderr.ros",
"runtimeRotationY": 0
}
],
"templates": [
@@ -202,6 +507,25 @@
"classificationRule": "noncombat/dynamic-display source object"
}
},
{
"primaryColor": "hsl(348 22% 38%)",
"accentColor": "hsl(16 38% 64%)",
"id": 105204,
"name": "Conjurer Chapeaunoir",
"title": "Conjurer Chapeaunoir",
"level": 68,
"classification": "boss",
"archetype": "humanoid",
"actor": "black-hat",
"source": {
"imageId": 584594,
"modelPath": "model/character/creature/black_hat/act_black_hat.ros",
"classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation",
"scriptRoots": [
"cl_136_105205_N"
]
}
},
{
"primaryColor": "hsl(35 22% 38%)",
"accentColor": "hsl(63 38% 64%)",
@@ -223,13 +547,13 @@
"id": 105210,
"name": "Template 105210",
"level": 10,
"classification": "combat",
"classification": "deferred-object",
"archetype": "humanoid",
"actor": "cereus-01",
"reason": "Source boss controller for Ardmond and Issac; the controller is not a combat mob.",
"source": {
"imageId": 583949,
"modelPath": "model/character/creature/cereus/act_cereus_01.ros",
"classificationRule": "positive native level plus static model/character ROS"
"classificationRule": "reviewed Lua controller override"
}
},
{
@@ -337,19 +661,128 @@
"classificationRule": "noncombat/dynamic-display source object"
}
},
{
"primaryColor": "hsl(208 22% 38%)",
"accentColor": "hsl(236 38% 64%)",
"id": 105224,
"name": "Boro-Boro Brothers",
"title": "Boro-Boro Brothers",
"level": 68,
"classification": "boss",
"archetype": "humanoid",
"actor": "barbo-brother",
"source": {
"imageId": 584595,
"modelPath": "model/character/creature/barbo_brother/act_barbo_brother.ros",
"classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation",
"scriptRoots": [
"ic_105224_01"
]
}
},
{
"primaryColor": "hsl(83 22% 38%)",
"accentColor": "hsl(111 38% 64%)",
"id": 105229,
"name": "Smirking Castle Clown",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "grinning-clown",
"source": {
"imageId": 584630,
"modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(130 22% 38%)",
"accentColor": "hsl(158 38% 64%)",
"id": 105230,
"name": "Odd Castle Clown",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "ghastly-clown-red",
"source": {
"imageId": 584631,
"modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(271 22% 38%)",
"accentColor": "hsl(299 38% 64%)",
"id": 105233,
"name": "Cursed Steel Soldier",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "cursed-steel-guardian",
"source": {
"imageId": 584634,
"modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(5 22% 38%)",
"accentColor": "hsl(33 38% 64%)",
"id": 105235,
"name": "Conjurer's Apprentice",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "black-hat-blue",
"source": {
"imageId": 584636,
"modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(52 22% 38%)",
"accentColor": "hsl(80 38% 64%)",
"id": 105236,
"name": "Giant Enraged Bear",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "enraged-big-bear-purple",
"source": {
"imageId": 584637,
"modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(99 22% 38%)",
"accentColor": "hsl(127 38% 64%)",
"id": 105237,
"name": "Manipulated Bear",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "enraged-big-bear-cream",
"source": {
"imageId": 584664,
"modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(178 22% 38%)",
"accentColor": "hsl(206 38% 64%)",
"id": 105254,
"name": "Template 105254",
"level": 1,
"classification": "combat",
"classification": "deferred-object",
"archetype": "plant",
"actor": "elder-myconid",
"reason": "Source Lua patrol controller; it creates the real combat formations and must not be rendered as a mob.",
"source": {
"imageId": 576437,
"modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros",
"classificationRule": "positive native level plus static model/character ROS"
"classificationRule": "reviewed Lua controller override"
}
},
{
@@ -358,13 +791,13 @@
"id": 105258,
"name": "Template 105258",
"level": 1,
"classification": "combat",
"classification": "deferred-object",
"archetype": "plant",
"actor": "elder-myconid",
"reason": "Source proximity and ambient-rabbit trigger; it is not a combat mob.",
"source": {
"imageId": 576437,
"modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros",
"classificationRule": "positive native level plus static model/character ROS"
"classificationRule": "reviewed Lua controller override"
}
},
{
@@ -399,6 +832,64 @@
"classificationRule": "noncombat/dynamic-display source object"
}
},
{
"primaryColor": "hsl(57 22% 38%)",
"accentColor": "hsl(85 38% 64%)",
"id": 105351,
"name": "Larvanger Barkud",
"title": "Larvanger Barkud",
"level": 68,
"classification": "boss",
"archetype": "humanoid",
"actor": "rafael-bakderr",
"source": {
"imageId": 584614,
"modelPath": "model/character/creature/rafael_bakderr/act_rafael_bakderr.ros",
"classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation",
"scriptRoots": [
"un_zone19_boss5_allconreal_1_01"
]
}
},
{
"primaryColor": "hsl(34 22% 38%)",
"accentColor": "hsl(62 38% 64%)",
"id": 105542,
"name": "Ardmond",
"title": "Ardmond",
"objectiveNamePattern": "Ardmond and Issac",
"level": 68,
"classification": "boss",
"archetype": "humanoid",
"actor": "atmo",
"source": {
"imageId": 584588,
"modelPath": "model/character/creature/atmo/act_atmo.ros",
"classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation",
"scriptRoots": [
"kg_105542_01"
]
}
},
{
"primaryColor": "hsl(81 22% 38%)",
"accentColor": "hsl(109 38% 64%)",
"id": 105543,
"name": "Issac",
"title": "Issac",
"level": 68,
"classification": "boss",
"archetype": "humanoid",
"actor": "mount-millenium-chimera-laby",
"source": {
"imageId": 584587,
"modelPath": "model/character/creature/mount_millenium_chimera/act_mount_millenium_chimera_laby.ros",
"classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation",
"scriptRoots": [
"kg_105542_01"
]
}
},
{
"primaryColor": "hsl(52 22% 38%)",
"accentColor": "hsl(80 38% 64%)",
@@ -444,6 +935,21 @@
"classificationRule": "positive native level plus static model/character ROS"
}
},
{
"primaryColor": "hsl(193 22% 38%)",
"accentColor": "hsl(221 38% 64%)",
"id": 105599,
"name": "Cursed Steel Soldier",
"level": 63,
"classification": "combat",
"archetype": "humanoid",
"actor": "cursed-steel-guardian",
"source": {
"imageId": 584634,
"modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros",
"classificationRule": "reviewed Lua matrix member"
}
},
{
"primaryColor": "hsl(240 22% 38%)",
"accentColor": "hsl(268 38% 64%)",
@@ -519,6 +1025,25 @@
"classificationRule": "noncombat/dynamic-display source object"
}
},
{
"primaryColor": "hsl(272 22% 38%)",
"accentColor": "hsl(300 38% 64%)",
"id": 105616,
"name": "Cayus",
"title": "Cayus",
"level": 68,
"classification": "boss",
"archetype": "humanoid",
"actor": "karth",
"source": {
"imageId": 585676,
"modelPath": "model/character/creature/karth/act_karth.ros",
"classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation",
"scriptRoots": [
"Dy_Z19_BOSSN_AIC"
]
}
},
{
"primaryColor": "hsl(149 22% 38%)",
"accentColor": "hsl(177 38% 64%)",
@@ -79,8 +79,24 @@ if (errors.length === 0) {
check("unique source spawn ids", new Set(snapshot.rows.map((row) => row.spawnId)).size === snapshot.rows.length, "no duplicates");
check("environment id", environment.dungeonId === recipe.dungeonId, String(environment.dungeonId));
check("population report green", report.status === "green", String(report.status));
check("population accounting", report.sourceRowCount === report.importedSpawnCount + report.excludedRowCount,
report.sourceRowCount + " = " + report.importedSpawnCount + " + " + report.excludedRowCount);
const expectedScriptedSpawns = (recipe.scriptedSpawns ?? []).length
+ (recipe.scriptedFormations ?? []).reduce((total, formation) => total + formation.members.length, 0);
const sourceCombatSpawns = report.importedSpawnCount - (report.scriptedSpawnCount ?? 0);
check(
"scripted population accounting",
(report.scriptedSpawnCount ?? 0) === expectedScriptedSpawns,
String(report.scriptedSpawnCount ?? 0) + " = " + expectedScriptedSpawns,
);
check(
"source population accounting",
report.sourceRowCount === sourceCombatSpawns + report.excludedRowCount,
report.sourceRowCount + " = " + sourceCombatSpawns + " + " + report.excludedRowCount,
);
check(
"boss population accounting",
!Number.isInteger(recipe.expectedBossSpawns) || report.bossSpawnCount === recipe.expectedBossSpawns,
String(report.bossSpawnCount) + " = " + String(recipe.expectedBossSpawns),
);
check("combat nav projection", report.navmeshProjection.maximumCombatDistance <= report.navmeshProjection.maximumAllowedDistance,
String(report.navmeshProjection.maximumCombatDistance));
check("mechanics is an explicit review gate", mechanics.status === "review-required" || mechanics.status === "green", String(mechanics.status));