Release Healer Man 0.1.32
This commit is contained in:
1 parent
d3d6aaebdb
commit
6dcb1034e5
102 files changed
+10772
-788
No files matched your search
@@ -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);
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -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.");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user