forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { GLTFLoader, MeshoptDecoder } from "three-stdlib";
|
||||
import { DUNGEON_REGISTRY } from "../../src/game/dungeonRegistry";
|
||||
import { findPartyNavigationPath, type PartyNavigationPosition } from "../../src/game/partyNavigation";
|
||||
import { preparePartyNavigationGraph } from "../../src/game/partyNavigationAssetCache";
|
||||
|
||||
// Observational plan only. The companion playtest uses ordinary UI/WASD and
|
||||
// reports physical blocking; this planner never relocates a runtime player.
|
||||
const output = path.resolve(process.argv.find(value => value.startsWith("--output="))?.slice(9)
|
||||
?? "artifacts/playtest/rom-deep-review/walking-plans.json");
|
||||
const maximumDistance = 45;
|
||||
const plans: Record<string, unknown> = {};
|
||||
const reports: unknown[] = [];
|
||||
const loader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder);
|
||||
|
||||
function boundedWaypoints(route: readonly PartyNavigationPosition[]) {
|
||||
const points: number[][] = [];
|
||||
let distance = 0;
|
||||
let sinceCapture = 0;
|
||||
for (let index = 1; index < route.length; index += 1) {
|
||||
const a = route[index - 1], b = route[index];
|
||||
const length = Math.hypot(b[0] - a[0], b[2] - a[2]);
|
||||
const retainedLength = Math.min(length, maximumDistance - distance);
|
||||
const segments = Math.max(1, Math.ceil(retainedLength / 4));
|
||||
for (let step = 1; step <= segments; step++) {
|
||||
const ratio = length ? retainedLength / length * step / segments : 1;
|
||||
const point = a.map((value, axis) => value + (b[axis] - value) * ratio);
|
||||
sinceCapture += retainedLength / segments;
|
||||
// Keep portal crossings even on short legs so walking follows actual links.
|
||||
if (sinceCapture > 0.5 || step === segments) points.push(point);
|
||||
sinceCapture = 0;
|
||||
}
|
||||
distance += retainedLength;
|
||||
if (distance >= maximumDistance) break;
|
||||
}
|
||||
return { points, plannedDistance: distance };
|
||||
}
|
||||
|
||||
for (const definition of Object.values(DUNGEON_REGISTRY).filter(row => row.expansion === "runewaker" && !row.id.startsWith("bite-sized-"))) {
|
||||
const asset = definition.assets.navigation;
|
||||
if (!asset) continue;
|
||||
const file = `src/assets/game/dungeons/${definition.id}/${asset.fileName}`;
|
||||
try {
|
||||
const bytes = await readFile(file);
|
||||
const checksum = createHash("sha256").update(bytes).digest("hex");
|
||||
const gltf = await loader.parseAsync(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), "");
|
||||
const graph = preparePartyNavigationGraph(file, gltf.scene, definition.navigationLinks ?? [],
|
||||
definition.traversalRamps ?? [], 250_000, false, gltf.userData.runewakerNavigationTopology,
|
||||
asset.detourTopology, asset.navigationComponentPolicy, checksum);
|
||||
const entry = definition.entrance.footPosition;
|
||||
const targets = definition.bosses.map(boss => ({ name: boss.name, position: boss.position }));
|
||||
const bossRoutes = targets.map(target => ({ target, route: findPartyNavigationPath(graph, entry, target.position, {
|
||||
maxStartSnapDistance: 3, maxEndSnapDistance: 8,
|
||||
}) }));
|
||||
let selected = bossRoutes.find(row => row.route && row.route.length > 1);
|
||||
if (!selected) {
|
||||
const forward = definition.entrance.forward;
|
||||
// Try a retained surface ahead of the entry when no boss is connected.
|
||||
for (const distance of [45, 25, 12, 6]) {
|
||||
const position = entry.map((value, axis) => value + forward[axis] * distance) as unknown as PartyNavigationPosition;
|
||||
const route = findPartyNavigationPath(graph, entry, position, { maxStartSnapDistance: 3, maxEndSnapDistance: 8 });
|
||||
if (route && route.length > 1) {
|
||||
selected = { target: { name: `forward surface ${distance}m`, position }, route };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bounded = selected?.route ? boundedWaypoints(selected.route) : {
|
||||
points: [3, 6].map(distance => entry.map((value, axis) => value + definition.entrance.forward[axis] * distance)),
|
||||
plannedDistance: 6,
|
||||
source: "ordinary short forward collision probe; navigation did not certify a connected target",
|
||||
};
|
||||
plans[definition.id] = bounded;
|
||||
reports.push({ id: definition.id, checksum, target: selected?.target.name ?? null,
|
||||
bossRoutes: bossRoutes.map(row => ({ name: row.target.name, connected: row.route !== null })), ...bounded });
|
||||
console.log(`${definition.id}: ${bounded.points.length} waypoints, ${bounded.plannedDistance.toFixed(1)}m, ${selected?.target.name ?? "no connected target"}`);
|
||||
} catch (error) {
|
||||
reports.push({ id: definition.id, error: String(error) });
|
||||
plans[definition.id] = { points: [] };
|
||||
console.log(`${definition.id}: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
await mkdir(path.dirname(output), { recursive: true });
|
||||
await writeFile(output, `${JSON.stringify(plans, null, 2)}\n`);
|
||||
await writeFile(output.replace(/\.json$/, "-proof.json"), `${JSON.stringify({ maximumDistance, reports }, null, 2)}\n`);
|
||||
Reference in new issue
Block a user