Update Healer Man server
This commit is contained in:
1 parent
a868401ebc
commit
48a678dcc5
295 files changed
+7644
-467
No files matched your search
@@ -0,0 +1,181 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const publicRoot = path.join(projectRoot, "public");
|
||||
const archiveRoot = path.join(publicRoot, "assets/game/combat-effects/original/runewaker");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(publicRoot, "assets/game/combat-effects/manifest.json"), "utf8"));
|
||||
const archive = JSON.parse(fs.readFileSync(path.join(archiveRoot, "manifest.json"), "utf8"));
|
||||
const catalog = JSON.parse(fs.readFileSync(path.join(projectRoot, "src/game/romPlayerCatalog.generated.json"), "utf8"));
|
||||
const write = process.argv.includes("--write");
|
||||
if (process.argv.some((argument, index) => index > 1 && argument !== "--write")) {
|
||||
throw new Error("Usage: node scripts/combat-effects/plan-rom-media.mjs [--write]");
|
||||
}
|
||||
|
||||
const cohorts = [
|
||||
["rom-priest", null], ["rom-priest", "rom-knight"],
|
||||
["rom-knight", null], ["rom-knight", "rom-scout"],
|
||||
["rom-scout", null], ["rom-scout", "rom-rogue"],
|
||||
["rom-rogue", null], ["rom-rogue", "rom-warrior"],
|
||||
["rom-mage", null], ["rom-mage", "rom-warlock"],
|
||||
];
|
||||
const spellIds = new Set(catalog.skills.filter((skill) => !skill.passive && cohorts.some(
|
||||
([primary, secondary]) => skill.classId === primary && (skill.secondaryClassId ?? null) === secondary,
|
||||
)).flatMap((skill) => [skill.sourceSkillId, ...(skill.ranks ?? []).map((rank) => rank.sourceSkillId)]));
|
||||
const entries = manifest.entries.filter((entry) => entry.source === "runewaker" && spellIds.has(entry.spellId));
|
||||
const entriesBySpellId = new Map(entries.map((entry) => [entry.spellId, entry]));
|
||||
const usages = new Map();
|
||||
for (const entry of entries) for (const [phase, assets] of Object.entries(entry.phases ?? {})) {
|
||||
for (const asset of assets) {
|
||||
if (!asset.model) continue;
|
||||
const key = asset.model.toLowerCase();
|
||||
const list = usages.get(key) ?? [];
|
||||
list.push({ spellId: entry.spellId, phase });
|
||||
usages.set(key, list);
|
||||
}
|
||||
}
|
||||
|
||||
function graphFor(result) {
|
||||
if (!result.graphUrl) return null;
|
||||
const graphPath = path.resolve(publicRoot, result.graphUrl.replace(/^\/+/, ""));
|
||||
const graphsRoot = path.join(archiveRoot, "graphs") + path.sep;
|
||||
if (!graphPath.toLowerCase().startsWith(graphsRoot.toLowerCase())) {
|
||||
throw new Error(`RuneWaker graph path escapes archive: ${result.graphUrl}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(graphPath, "utf8"));
|
||||
}
|
||||
|
||||
function eventSoundPath(value) {
|
||||
const normalized = String(value ?? "").replaceAll("\\", "/").replace(/^\/+/, "").toLowerCase();
|
||||
return /^sound\/fx\/[\w./ -]+\.(?:mp3|wav|ogg)$/.test(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
const results = archive.results.map((result) => {
|
||||
const model = result.model.toLowerCase();
|
||||
const graph = graphFor(result);
|
||||
const references = [...new Map((usages.get(model) ?? []).map((usage) =>
|
||||
[`${usage.spellId}:${usage.phase}`, usage])).values()];
|
||||
const blockers = [...new Set(result.unsupported ?? [])].sort();
|
||||
const errors = [...new Set(result.errors ?? [])];
|
||||
const state = result.rendererReady ? "ready"
|
||||
: result.partialRendererReady ? "partial-original"
|
||||
: result.status === "decoded" && errors.length === 0 ? "clean-unverified"
|
||||
: result.status === "failed" ? "failed" : "blocked";
|
||||
const directAudioEvents = [...new Map((graph?.motions ?? []).flatMap((motion) =>
|
||||
(motion.controllers ?? []).filter((controller) => (
|
||||
controller.sourceType === "CRuController_EventTrigger"
|
||||
&& controller.eventID === 43 && eventSoundPath(controller.eventString)
|
||||
)).map((controller) => {
|
||||
const sourcePath = eventSoundPath(controller.eventString);
|
||||
return [`${motion.name}:${sourcePath}:${controller.eventParameter0}:${controller.eventParameter1}`,
|
||||
{ motion: motion.name, sourcePath, loop: controller.eventParameter0 === 1,
|
||||
parameter1: controller.eventParameter1 }];
|
||||
}))).values()];
|
||||
const missingEventCues = directAudioEvents.flatMap((event) => {
|
||||
if (event.loop || event.parameter1 !== 0) return [];
|
||||
const missing = references.filter((reference) => {
|
||||
const entry = entriesBySpellId.get(reference.spellId);
|
||||
return !entry?.sounds?.some((sound) => sound.phase === reference.phase
|
||||
&& sound.sourcePath?.toLowerCase() === event.sourcePath && sound.url);
|
||||
});
|
||||
return missing.length ? [{ ...event, references: missing }] : [];
|
||||
});
|
||||
return {
|
||||
model: result.model,
|
||||
state,
|
||||
graphUrl: result.graphUrl,
|
||||
spriteControllers: result.spriteControllers,
|
||||
textures: result.textures,
|
||||
blockers,
|
||||
errors,
|
||||
references,
|
||||
directAudioEvents,
|
||||
missingEventCues,
|
||||
};
|
||||
});
|
||||
|
||||
const stateCounts = Object.fromEntries([
|
||||
"ready", "partial-original", "clean-unverified", "blocked", "failed",
|
||||
].map((state) => [state, results.filter((result) => result.state === state).length]));
|
||||
const blockerFamilies = [...new Set(results.flatMap((result) => result.blockers))].map((controller) => {
|
||||
const models = results.filter((result) => result.blockers.includes(controller));
|
||||
return { controller, models: models.length,
|
||||
spellIds: new Set(models.flatMap((model) => model.references.map((reference) => reference.spellId))).size };
|
||||
}).sort((left, right) => right.models - left.models || left.controller.localeCompare(right.controller));
|
||||
const signatureGroups = [...new Set(results.filter((result) => result.state === "blocked")
|
||||
.map((result) => result.blockers.join(" + ") || "decode errors"))].map((signature) => {
|
||||
const models = results.filter((result) => result.state === "blocked"
|
||||
&& (result.blockers.join(" + ") || "decode errors") === signature);
|
||||
return { signature, models: models.length,
|
||||
spellIds: new Set(models.flatMap((model) => model.references.map((reference) => reference.spellId))).size,
|
||||
example: models[0]?.model };
|
||||
}).sort((left, right) => right.models - left.models || left.signature.localeCompare(right.signature));
|
||||
const summary = {
|
||||
models: results.length,
|
||||
...stateCounts,
|
||||
cohortSpellEntries: entries.length,
|
||||
entriesWithPackagedAudio: entries.filter((entry) => entry.sounds?.some((sound) => sound.url)).length,
|
||||
eventCueCandidates: results.reduce((sum, result) => sum + result.missingEventCues.length, 0),
|
||||
};
|
||||
const report = { schemaVersion: 1, scope: archive.scope, summary, blockerFamilies, signatureGroups, results };
|
||||
|
||||
const clean = results.filter((result) => result.state === "clean-unverified");
|
||||
const eventOnly = results.filter((result) => result.state === "blocked"
|
||||
&& result.spriteControllers > 0 && result.blockers.length === 1
|
||||
&& result.blockers[0] === "CRuController_EventTrigger");
|
||||
const countRefs = (result) => result.references.length;
|
||||
const priority = (left, right) => countRefs(right) - countRefs(left) || left.model.localeCompare(right.model);
|
||||
const row = (result) => `| ${result.model} | ${result.references.length} | ${result.spriteControllers} | ${result.blockers.join(", ") || "—"} |`;
|
||||
const lines = [
|
||||
"# RuneWaker media batch plan",
|
||||
"",
|
||||
"Generated by `npm run effects:rom:plan` from the current primary-five spell manifest and decoded ROS archive. It groups remaining controller and timing work. The manifest generator already attaches source direct audio events at supported phase boundaries.",
|
||||
"",
|
||||
`The archive has ${summary.models} unique models: ${summary.ready} ready, ${summary["partial-original"]} partial original, ${summary["clean-unverified"]} fully decoded but unverified, ${summary.blocked} blocked, and ${summary.failed} failed. ${summary.entriesWithPackagedAudio} of ${summary.cohortSpellEntries} source spell entries have at least one packaged audio cue.`,
|
||||
"",
|
||||
"## Shared controller blockers",
|
||||
"",
|
||||
"Counts overlap when a model has several controller families. A family should be ported and verified once against representative models before its other users are considered for promotion.",
|
||||
"",
|
||||
"| Controller family | Models | Source spell IDs using those models |",
|
||||
"| --- | ---: | ---: |",
|
||||
...blockerFamilies.map((family) => `| ${family.controller} | ${family.models} | ${family.spellIds} |`),
|
||||
"",
|
||||
"## Fully decoded models awaiting visual review",
|
||||
"",
|
||||
"| Source model | Spell-phase references | Sprite layers | Blockers |",
|
||||
"| --- | ---: | ---: | --- |",
|
||||
...clean.sort(priority).map(row),
|
||||
"",
|
||||
"## Event-only models still blocked",
|
||||
"",
|
||||
"These models have sprite data but include an event callback outside the validated visual-only set. Effect-spawning and projectile-flight callbacks require browser behavior before their visuals can be promoted.",
|
||||
"",
|
||||
"| Source model | Spell-phase references | Sprite layers | Blockers |",
|
||||
"| --- | ---: | ---: | --- |",
|
||||
...eventOnly.sort(priority).slice(0, 20).map(row),
|
||||
"",
|
||||
`There are ${eventOnly.length} event-only models. The archive also exposes ${summary.eventCueCandidates} model/phase audio-event candidates missing from the current packaged manifest. They need delayed timing or a later lifecycle phase; an ROS motion name alone is insufficient. Full per-model evidence is in \`docs/ROM_MEDIA_BATCH_PLAN.json\`.`,
|
||||
"",
|
||||
"## Largest blocker combinations",
|
||||
"",
|
||||
"| Controller signature | Models | Source spell IDs | Example model |",
|
||||
"| --- | ---: | ---: | --- |",
|
||||
...signatureGroups.slice(0, 15).map((group) =>
|
||||
`| ${group.signature.replaceAll("|", "\\|")} | ${group.models} | ${group.spellIds} | ${group.example} |`),
|
||||
"",
|
||||
"## Batch workflow",
|
||||
"",
|
||||
"1. Run `npm run effects:rom:batch` to refresh source links, package audio and ROS graphs, attach verified phases, and rebuild this plan.",
|
||||
"2. Review the remaining event timing candidates and largest shared blocker families above.",
|
||||
"3. Port one controller family in the exporter/WebGL runtime and add a parser check that every promoted graph is renderable.",
|
||||
"4. Regenerate the manifest and media audit, replay representative graphs in the RuneWaker preview, then spot-check distinct motion and attachment patterns in live combat.",
|
||||
];
|
||||
|
||||
if (write) {
|
||||
const docs = path.join(projectRoot, "docs");
|
||||
fs.writeFileSync(path.join(docs, "ROM_MEDIA_BATCH_PLAN.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
fs.writeFileSync(path.join(docs, "ROM_MEDIA_BATCH_PLAN.md"), `${lines.join("\n").trimEnd()}\n`);
|
||||
}
|
||||
console.log(JSON.stringify(summary));
|
||||
Reference in new issue
Block a user