Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6f2375145 | ||
|
|
699fc858c9 | ||
|
|
d9bebb4e81 | ||
|
|
eaddad4134 | ||
|
|
fc8a7a722a | ||
|
|
b57d2b2839 | ||
|
|
f49e54f35f | ||
|
|
7b41b102fa |
No files matched your search
@@ -20,5 +20,6 @@ releases/
|
||||
/content/
|
||||
dist-android/
|
||||
.android-public/
|
||||
.web-public/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
+66
-33
@@ -1,10 +1,12 @@
|
||||
# Local-first content updates and Android releases
|
||||
# Local-first content updates for browser and Android
|
||||
|
||||
Healer Man separates the installed game client from large game content. The
|
||||
Android APK contains the React/Three.js runtime, native Android bridge, menus,
|
||||
local profile and save support, UI assets, and the Wailing Caverns starter
|
||||
pack. Other GLBs, textures, audio, character packs, dungeons, and Manastorm
|
||||
stages are published by TrueNAS as optional content.
|
||||
Healer Man separates the installed game client from large game content. Both
|
||||
the production browser build and Android APK contain a compact required set:
|
||||
the React/Three.js runtime, menus, local profile and save support, UI assets,
|
||||
starter equipment, Wailing Caverns starter content, and the basic Human
|
||||
character model with its default male and female appearances. Android also
|
||||
contains its native bridge. Other GLBs, textures, audio, character/skin packs,
|
||||
dungeons, and Manastorm stages are published by TrueNAS as optional content.
|
||||
|
||||
Single-player is local-first. Starting the game, loading a character, entering
|
||||
installed content, simulating combat, and saving progress never require the
|
||||
@@ -27,16 +29,12 @@ UI behavior, shaders or loaders, native Android code, permissions, or the
|
||||
content-manifest schema. A manifest can set `minimumAppVersion` so an older APK
|
||||
will not activate incompatible content.
|
||||
|
||||
The browser build remains a full build served by TrueNAS. It receives client
|
||||
code when the app checkout is pulled, rebuilt, and restarted. The Android build
|
||||
is deliberately separate and compact so the APK does not inherit the complete
|
||||
multi-gigabyte browser asset catalog.
|
||||
|
||||
Measured on August 14, 2026, the production browser `dist` is 3.88 GiB across
|
||||
18,895 files; GLBs account for 3.75 GiB. The compact Android `dist-android` is
|
||||
148.0 MiB across 5,561 files, including 47.8 MiB of starter GLBs. The final
|
||||
signed APK will be smaller because Android packages compress eligible files;
|
||||
record its exact size after the first release build.
|
||||
Production builds deliberately stage only required content. `npm run build`
|
||||
prepares `.web-public` and emits the compact browser `dist`; Android prepares
|
||||
`.android-public` and emits `dist-android`. The development server continues to
|
||||
use the source `public` tree so asset-pipeline work remains convenient. The
|
||||
complete source catalog and published immutable objects stay on TrueNAS, not in
|
||||
every player's initial browser or Android installation.
|
||||
|
||||
## Content manifest
|
||||
|
||||
@@ -56,8 +54,9 @@ The runtime continues to request stable paths such as:
|
||||
```
|
||||
|
||||
The content resolver maps that path to a verified file in Android app-private
|
||||
storage. Filenames are therefore not used as version numbers and runtime code
|
||||
does not need to know the hash.
|
||||
storage or, in production browsers, to the app-managed content cache through a
|
||||
same-origin service worker. Filenames are therefore not used as version numbers
|
||||
and runtime code does not need to know the hash.
|
||||
|
||||
## Safe publication and activation
|
||||
|
||||
@@ -68,17 +67,34 @@ Publication order is intentionally atomic:
|
||||
3. Write the versioned manifest.
|
||||
4. Replace `manifest.json` only after all referenced objects exist.
|
||||
|
||||
On a device, the client fetches the small manifest and compares it with its
|
||||
installed manifest. Only missing or hash-mismatched objects are downloaded.
|
||||
Android streams each object to a `.part` file, supports HTTP range resume,
|
||||
verifies its size and SHA-256, then renames it into the object cache. The active
|
||||
logical-path map changes only after the complete pack verifies. An interrupted
|
||||
or corrupt update leaves the previous pack playable.
|
||||
The client fetches the small manifest and compares it with its bundled and
|
||||
installed file identities. Only missing or hash-mismatched objects are
|
||||
downloaded. Browsers download into a temporary Cache Storage staging area,
|
||||
verify every object's byte count and SHA-256, and promote the pack to the active
|
||||
cache only after the complete pack succeeds. Android streams each object to a
|
||||
`.part` file, supports HTTP range resume, verifies its size and SHA-256, then
|
||||
renames it into the object cache. An interrupted or corrupt update leaves the
|
||||
previous pack playable on either platform.
|
||||
|
||||
The starter pack is bundled into the APK. Downloaded packs live under Android's
|
||||
app-private files directory, outside the packaged web bundle, and survive an
|
||||
APK update. Players may remove optional packs through the content manager; save
|
||||
data is stored separately and is never removed with content.
|
||||
The Downloads screen uses the same lifecycle in browser and Android. A missing
|
||||
pack offers **Download**, an installed pack with changed files offers
|
||||
**Update**, and an installed optional pack offers **Remove**. Required packs are
|
||||
marked as included and cannot be removed. Browser packs live in the app-managed
|
||||
cache; Android packs live in app-private storage and survive APK updates. Save
|
||||
data and roster identities are stored separately and are never removed with
|
||||
content.
|
||||
|
||||
Character packs are presentation-only. When a selected race's pack is absent,
|
||||
removed, disabled, or fails to load, the shared character renderer uses the
|
||||
matching-gender basic Human model and default appearance. The saved race, class,
|
||||
equipment, progression, and appearance choices remain intact and become visible
|
||||
again after the pack is installed.
|
||||
|
||||
Generated spell effects and synthesized combat cues are the default. The
|
||||
`core:combat-media` pack containing packaged original spell visuals and sounds
|
||||
is never part of required bootstrap content. Original mode becomes available
|
||||
only after an explicit download and falls back to generated effects if the pack
|
||||
is later removed.
|
||||
|
||||
## Offline behavior
|
||||
|
||||
@@ -87,6 +103,8 @@ data is stored separately and is never removed with content.
|
||||
- The last verified content map is read synchronously from local storage.
|
||||
- A failed update check is silent outside the content manager.
|
||||
- A failed or interrupted download never replaces active content.
|
||||
- Missing optional character packs fall back to the basic Human model.
|
||||
- Generated spell effects remain available without original spell media.
|
||||
- Local saves are authoritative during play and cloud synchronization retries
|
||||
when connectivity returns.
|
||||
- Content that has never been downloaded is clearly marked as unavailable
|
||||
@@ -104,9 +122,10 @@ Keeping the runtime in the APK is what makes offline startup reliable.
|
||||
/mnt/usbssds/apps/healer-man/backups Database backups
|
||||
```
|
||||
|
||||
The Node server exposes the content volume at `/content`. Manifests use
|
||||
`no-cache`; immutable hashed objects use long-lived cache headers and support
|
||||
byte ranges for resumed downloads.
|
||||
The Node server exposes the compact production browser client at `/` and the
|
||||
complete downloadable-content volume at `/content`. Manifests use `no-cache`;
|
||||
immutable hashed objects use long-lived cache headers and support byte ranges
|
||||
for resumed downloads.
|
||||
|
||||
After pulling source on TrueNAS, publish content before restarting the app:
|
||||
|
||||
@@ -121,15 +140,29 @@ discover a manifest whose files have not been published.
|
||||
## Pack policy
|
||||
|
||||
Packs are player-facing groups while files remain individually addressable.
|
||||
This permits a fast initial pack install and small later patches. Current pack
|
||||
IDs are derived predictably:
|
||||
This permits a compact initial install and small later patches. Required pack
|
||||
IDs include:
|
||||
|
||||
```text
|
||||
core:runtime
|
||||
core:ui
|
||||
core:equipment
|
||||
core:character-basic
|
||||
dungeon:wailing-caverns
|
||||
```
|
||||
|
||||
`core:character-basic` owns the two basic Human rigs, the character manifest,
|
||||
and only the default texture selections needed by the fallback. The remaining
|
||||
human customizations and every non-Human character model/skin are optional.
|
||||
|
||||
Downloadable pack IDs include:
|
||||
|
||||
```text
|
||||
dungeon:forsaken-abbey
|
||||
characters:wow335a-v1
|
||||
characters:ascension-v1
|
||||
characters:runewaker-v1
|
||||
core:combat-media
|
||||
manastorm:<stage-id>
|
||||
shared:creatures
|
||||
```
|
||||
|
||||
+24
-18
@@ -24,8 +24,9 @@ container, rebuilding `dist`, or pulling source must not replace the database.
|
||||
TrueNAS Gitea stores the source repository and APK release attachments; it is
|
||||
not the game database.
|
||||
|
||||
Large runtime assets are published separately under `/app/content`. Android
|
||||
clients download verified content objects from `/content` and retain them in
|
||||
Large runtime assets are published separately under `/app/content`. Production
|
||||
browser and Android clients download verified content objects from `/content`;
|
||||
browsers retain them in an app-managed cache and Android retains them in
|
||||
app-private storage for offline play. See `CONTENT_UPDATES.md` for the manifest,
|
||||
atomic activation, compatibility, and rollback contract.
|
||||
|
||||
@@ -55,9 +56,10 @@ Container/host port:
|
||||
```
|
||||
|
||||
The Gitea bare repository is not runnable. Clone it with Git; never copy it
|
||||
with `cp`. The checkout contains several gigabytes of required GLB assets, so
|
||||
the initial clone and first build will take substantially longer than later
|
||||
fast-forward updates.
|
||||
with `cp`. The checkout contains several gigabytes of content-source GLB assets,
|
||||
so the initial clone and first content publication will take substantially
|
||||
longer than later fast-forward updates. Production browser and Android bundles
|
||||
stage only their compact required-content subset.
|
||||
|
||||
Do not turn the GLBs into Git LFS pointers without also changing this deployment
|
||||
procedure. Gitea keeps LFS objects outside the bare repository, so a bare-path
|
||||
@@ -80,7 +82,8 @@ git branch --show-current
|
||||
|
||||
If the repository slug differs, change both the remote URL here and every
|
||||
`healer-man.git` bare-repository path below. Do not commit `.env` files,
|
||||
keystores, API tokens, `content`, `dist`, `dist-android`, or `.android-public`.
|
||||
keystores, API tokens, `content`, `dist`, `dist-android`, `.web-public`, or
|
||||
`.android-public`.
|
||||
|
||||
On TrueNAS, confirm the bare repository path. If the first command fails, use
|
||||
the search command:
|
||||
@@ -154,8 +157,9 @@ account/cloud-save data; `/app/content` owns downloadable manifests and
|
||||
immutable content objects. Never mount a volume at `/app/data`, because that
|
||||
would hide the checkout's required `/app/data/loot/schema.sql`. The
|
||||
startup command installs locked dependencies, initializes the schema, publishes
|
||||
new or changed content objects, builds the full browser bundle, and starts the
|
||||
combined static/API/content server. Existing content objects are reused.
|
||||
new or changed content objects, builds the compact required-content browser
|
||||
bundle, and starts the combined static/API/content server. Existing content
|
||||
objects are reused.
|
||||
|
||||
After startup, test on the LAN:
|
||||
|
||||
@@ -220,8 +224,8 @@ git rev-parse HEAD
|
||||
|
||||
Before `git commit`, read the staged file list printed by `git status --short`.
|
||||
Do not commit a `.env` file, password, API token, keystore, APK, database,
|
||||
`content/`, `dist/`, `dist-android/`, or `.android-public/` output. The final
|
||||
command prints the exact deployed commit ID.
|
||||
`content/`, `dist/`, `dist-android/`, `.web-public/`, or `.android-public/`
|
||||
output. The final command prints the exact deployed commit ID.
|
||||
|
||||
Next, open the TrueNAS Apps UI and **stop** `healer-man`. Open a TrueNAS shell
|
||||
and run these exact commands:
|
||||
@@ -247,11 +251,12 @@ sudo git -C /mnt/usbssds/apps/healer-man/app rev-parse HEAD
|
||||
The TrueNAS commit ID must match the one printed on the development PC. Start
|
||||
`healer-man` in the TrueNAS Apps UI afterward. Container startup
|
||||
publishes immutable content objects and replaces `manifest.json` only after all
|
||||
objects exist, then rebuilds the browser client and starts the server. Android
|
||||
devices discover the small new manifest and download only missing or changed
|
||||
files. Asset-only updates do not require a new APK. A failed fast-forward pull
|
||||
deliberately leaves the running checkout unchanged; resolve diverged history on
|
||||
the development machine rather than forcing the TrueNAS checkout.
|
||||
objects exist, then rebuilds the browser client and starts the server. Browser
|
||||
and Android clients discover the small new manifest and download only missing
|
||||
or changed files for packs the player installed. Asset-only updates do not
|
||||
require a new APK. A failed fast-forward pull deliberately leaves the running
|
||||
checkout unchanged; resolve diverged history on the development machine rather
|
||||
than forcing the TrueNAS checkout.
|
||||
|
||||
After the logs say the server is listening on port `4173`, run on TrueNAS:
|
||||
|
||||
@@ -286,9 +291,10 @@ copies; it does not replace dataset-level protection.
|
||||
## APK releases and Obtainium
|
||||
|
||||
Keep APK binaries out of the Git branch. The Android build uses `dist-android`,
|
||||
which contains the game runtime, UI, equipment, and Wailing Caverns starter
|
||||
content rather than the full multi-gigabyte browser catalog. Downloaded packs
|
||||
survive APK updates.
|
||||
which contains the game runtime, UI, starter equipment, basic Human character,
|
||||
and Wailing Caverns starter content. Optional character/skin packs, original
|
||||
spell media, dungeons, and other large content remain outside the APK.
|
||||
Downloaded packs survive APK updates.
|
||||
|
||||
Install JDK 21 and Android SDK Platform 36 on the development PC and set
|
||||
`JAVA_HOME` plus `ANDROID_SDK_ROOT`. The build wrapper also recognizes the
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Encounter authoring and fidelity
|
||||
|
||||
Healer Man keeps boss abilities, dungeon orchestration, presentation, and source
|
||||
evidence separate. Boss abilities are modular `EnemyAttackDefinition` records;
|
||||
dungeon-wide doors, waves, checkpoints, and transitions are modular
|
||||
`DungeonEncounterDefinition` records. Neither runtime stores Three.js or React
|
||||
objects, so the same definitions can later be selected or randomized without
|
||||
rewriting a boss component.
|
||||
|
||||
## Adding or editing a boss mechanic
|
||||
|
||||
1. Add or edit the attack in the boss entity's `combat.attacks` list. Imported
|
||||
RuneWaker lists are generated under `src/game/generated`; authored WoW
|
||||
exceptions live with their dungeon content.
|
||||
2. Give the mechanic a stable `id`, timing, condition, target, effect, and source
|
||||
record. Health mechanics use `self-health-percent`; timed mechanics use
|
||||
`initialCooldownMs` and `repeatCooldownMs`.
|
||||
3. Author a `telegraph` when the source supplies geometry. Harmful area attacks
|
||||
without explicit geometry receive a conservative circle/cone/line inference,
|
||||
but inferred geometry is not described as source-exact.
|
||||
4. Add a guide override only when the generated explanation needs encounter
|
||||
context that cannot be derived from the mechanic. The Boss Guide otherwise
|
||||
reads the runtime definition directly, including its trigger, timer, effect,
|
||||
telegraph, counterplay, fidelity, and test procedure.
|
||||
5. Run the focused mechanic, telegraph-geometry, campaign, timeline, and guide
|
||||
tests, then perform the browser playtest described in `PLAYTEST_NOTES.md`.
|
||||
|
||||
Telegraph geometry is shared by presentation and hit resolution. A circle,
|
||||
frontal cone, or line shown on the floor is therefore the area the resolver
|
||||
tests when the cast completes. `warningMs` is also enforced as a minimum cast
|
||||
time, so a telegraph cannot appear after its damage resolves.
|
||||
|
||||
## Adding a dungeon encounter rule
|
||||
|
||||
Attach a `DungeonEncounterDefinition` to `DungeonDefinition.encounters`. Rules
|
||||
combine a trigger, optional guards, and reusable actions. Available primitives
|
||||
cover tick/elapsed time, signals, interactions, entity engagement/death/health,
|
||||
all-members-dead checks, phases, flags, counters, timers, spawn groups, dynamic
|
||||
world-object state, objective completion, checkpoints, party teleports, loot,
|
||||
and signal chaining.
|
||||
|
||||
Definitions and snapshots are JSON-safe and stable-ID based. Actions are
|
||||
idempotent by default. Loot actions must use `persistence: "run"`; this prevents
|
||||
a wipe or checkpoint reload from duplicating a reward. Declare encounter-owned
|
||||
mob waves in `DungeonDefinition.encounterSpawnGroups`. Declare doors, switches,
|
||||
chests, keys, bridges, NPC prompts, and quest objects in
|
||||
`DungeonDefinition.serverGameObjects`; their collision, sight blocking, visual,
|
||||
prompt, and allowed interaction states follow the current encounter snapshot.
|
||||
|
||||
Online groups use party-leader authority. A follower interaction is sent as a
|
||||
request, then revalidated by the leader against the follower's synchronized
|
||||
position, range, line of sight, object state, and object ID before any rule is
|
||||
dispatched. The leader publishes serializable encounter snapshots to followers.
|
||||
|
||||
## Save behavior
|
||||
|
||||
Character profiles store a versioned safe dungeon checkpoint. The checkpoint
|
||||
projection retains only actions marked `checkpoint` or `run`; active timers,
|
||||
temporary waves, and a half-finished pull are reset. Entering the same dungeon
|
||||
hydrates that safe projection and its spawn after definitions are registered.
|
||||
Loading a different dungeon starts at its normal entrance.
|
||||
|
||||
## Source fidelity and remaining review
|
||||
|
||||
The deterministic ledgers are:
|
||||
|
||||
- `scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md`
|
||||
- `scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.md`
|
||||
- `scripts/runewaker-pipeline/RUNEWAKER_ENVIRONMENT_ASSEMBLY_REPORT.md`
|
||||
- `scripts/runewaker-pipeline/RUNEWAKER_NAVIGATION_GAP_REPORT.md`
|
||||
|
||||
Current encounter evidence covers 30 RuneWaker packages, 4,113 deferred source
|
||||
rows, and 1,524 unique direct evidence records. Of those evidence records, 388
|
||||
have exact executable spell mappings and 1,136 still need source review. Four
|
||||
bosses have clearly labeled, playable Healer Man fallback kits because the
|
||||
preserved data does not expose their original executable behavior. Eight
|
||||
non-empty packages do not contain source-confirmed boss membership; two
|
||||
authoritative variants are intentionally empty.
|
||||
|
||||
The upgraded WDB environment builder can assemble all referenced ROS resources
|
||||
with full placement translation, quaternion, and nonuniform scale, source-hash
|
||||
caching, shared meshes, collision chunking, and a separate dynamic-placement
|
||||
manifest. The integration pass regenerated Raksha Temple and The Origin through
|
||||
that full-WDB path; remaining shipping packages retain their last validated
|
||||
build until they are individually regenerated. The current audit finds 107
|
||||
filename-only dynamic candidates; they remain static and review-required
|
||||
because filenames alone do not prove door/switch behavior.
|
||||
|
||||
Treasure Trove's 908-placement full-WDB staging export is reproducible and its
|
||||
487,380-triangle visual/collision assembly, meshopt pass, and KTX2 compression
|
||||
all succeed. Its 0.3-metre, 512-cell full-resolution Recast navigation bake did
|
||||
not complete: the watershed run was stopped after nearly two CPU-hours when it
|
||||
showed the smaller-tile retry signature, and the explicit monotone run was
|
||||
stopped after more than four CPU-hours on its first pass. The fail-closed
|
||||
pipeline therefore did not publish that staged package. The last validated
|
||||
shipping package remains active. The recipe retains the monotone tiled policy
|
||||
for a future offline bake or a reviewed spatial-partitioning optimization;
|
||||
lowering navigation resolution was not silently substituted.
|
||||
|
||||
The route report contains 19 unreachable legs across eight dungeons, all from a
|
||||
distance-from-entrance objective-order heuristic. Those legs are evidence that
|
||||
objective order needs review, not proof that an off-mesh link should exist.
|
||||
Links are added only when source geometry or a reviewed traversal justifies
|
||||
them.
|
||||
|
||||
Run `npm run runewaker:audit:gameplay`, `npm run
|
||||
runewaker:audit:environment-assembly`, and `npm run runewaker:audit:navigation`
|
||||
after importing new source data. A report can be structurally green while its
|
||||
content status remains `review-required`; this is intentional and prevents an
|
||||
incomplete source inference from being presented as an original mechanic.
|
||||
@@ -1572,3 +1572,159 @@ Vite's existing large-chunk advisory remains informational.
|
||||
106 environment assets passed their performance audit with no regressions,
|
||||
the KTX2 audit found no fallback textures, and the release-candidate check
|
||||
passed. The existing large-chunk advisory remains informational.
|
||||
|
||||
# 2026-09-04 — optional content offload and basic Human fallback
|
||||
|
||||
- Built and served the production browser output from a clean localhost origin.
|
||||
A fresh Undead female Priest preview rendered with the real bundled Human
|
||||
female rig and default textures while retaining the Undead/Priest identity in
|
||||
the roster. No optional character pack had been installed.
|
||||
- Entered Wailing Caverns as healer from a separate clean browser origin. The
|
||||
Undead player, Undead tank, and Goblin party member all rendered through the
|
||||
matching basic Human fallbacks while their saved race, role, class, equipment,
|
||||
and party labels remained intact. The dungeon stayed playable and emitted no
|
||||
new error; only the existing third-party initialization deprecation warning
|
||||
appeared.
|
||||
- Exercised the browser Downloads lifecycle with a small local two-revision
|
||||
content fixture. Missing character and original-media packs showed Download;
|
||||
a verified install showed Remove; the changed revision showed Update and
|
||||
Remove together; and applying Update returned the pack to offline-ready state.
|
||||
- Reloaded after service-worker activation. The application and lazy JavaScript
|
||||
chunks loaded normally, while the content worker continued to reserve managed
|
||||
character, creature, equipment, game, and UI paths for bundled or verified
|
||||
cached content. The initial broad `/assets/` interception found during this
|
||||
playtest was corrected before the final run.
|
||||
- In-dungeon Options showed Generated as the active default, kept Original
|
||||
disabled until its optional spell-effects-and-sounds pack is downloaded, and
|
||||
exposed a Download action for that pack.
|
||||
- Evidence:
|
||||
`playtest-artifacts/2026-09-04-optional-content/roster-undead-priest-basic-human-fallback.png`,
|
||||
`dungeon-optional-models-basic-human-fallback.png`,
|
||||
`browser-downloads-optional-packs.png`, and
|
||||
`browser-downloads-update-and-remove.png`.
|
||||
- Verification: all 43 avatar tests passed; the complete Vitest suite passed all
|
||||
896 cases across 164 files; production browser and Android builds completed;
|
||||
both binary inventories contained only the two Human rigs, starter equipment,
|
||||
and Wailing Caverns content; and neither output contained optional character
|
||||
packs, original combat media, or WAV files. KTX2 and environment-performance
|
||||
audits remained green, and the release-candidate check passed. The existing
|
||||
large-chunk advisory remains informational.
|
||||
|
||||
# 2026-09-05 — Power Word: Shield live character anchoring
|
||||
|
||||
- Installed the optional Original Spell Effects & Sounds pack through the
|
||||
production Downloads screen, enabled it, and selected Original spell visuals.
|
||||
- Entered Wailing Caverns as a Human Priest and cast Power Word: Shield on both
|
||||
the party tank and the player while exercising party movement and combat. The
|
||||
persistent original aura stayed at the rendered character root instead of
|
||||
following the throttled gameplay-position snapshot, with no inherited camera
|
||||
or character rotation.
|
||||
- Reviewed live dungeon screenshots before and during the movement test. The
|
||||
browser emitted no new errors; only the existing third-party initialization
|
||||
deprecation warning remained.
|
||||
- Verification: all 899 Vitest cases passed across 165 files, including three
|
||||
new visual-anchor registry cases. The production build completed with 920
|
||||
modules transformed, and asset audits remained green. The existing
|
||||
large-chunk advisory remains informational.
|
||||
|
||||
# 2026-09-05 — modular RuneWaker boss mechanics
|
||||
|
||||
- Built a temporary local production content feed for Forsaken Abbey and the
|
||||
first two Ystra Labyrinth boss rooms, then installed each pack through the
|
||||
browser Downloads workflow with checksum verification.
|
||||
- Loaded the source-backed Specter Guardian Creator encounter. Its mechanic
|
||||
panel appeared below the minimap and buff/debuff strips with Guard Bomb,
|
||||
Transport Magic, and Meteor Shower countdowns. A live Meteor Shower cast
|
||||
showed its one-second cast warning over the boss.
|
||||
- The first pass exposed that four countdown rows could crowd the boss's
|
||||
pending 80%, 60%, 40%, and 20% phase triggers out of the panel. The timeline
|
||||
now limits only countdown rows and always retains every pending health
|
||||
threshold. The corrected production build displayed all eight rows without
|
||||
covering the central playfield.
|
||||
- Loaded Rune Guardian Leader in Ystra Labyrinth Boss Room 2. Its source-backed
|
||||
Manastorms Duo Lv5 mechanic displayed a large purple circular floor warning
|
||||
while the matching panel row counted down at 0.6 seconds. The area indicator,
|
||||
cast timing, party damage, and effect/debuff presentation all advanced in the
|
||||
live encounter.
|
||||
- Browser logs contained no uncaught application errors. The existing
|
||||
third-party initialization deprecation warning remained; WebGL context-lost
|
||||
messages corresponded to intentional production rebuild reloads and opening
|
||||
replacement test tabs.
|
||||
- Verification: the full suite passed all 913 cases across 169 files, and the
|
||||
corrected production build completed with 923 modules transformed. KTX2 and
|
||||
environment-performance audits remained green, and the release-candidate
|
||||
check passed.
|
||||
|
||||
# 2026-09-05 — specialization-aware heirlooms and endgame loot
|
||||
|
||||
- Activated the legacy level-10 Priest `Rolecheck`. The mandatory Class Hall
|
||||
picker appeared before the main menu, offered Discipline, Holy, and Shadow,
|
||||
and presented Cancel, Keep talents, and Reset talents after choosing Holy.
|
||||
Keeping talents returned to the main menu with the specialization saved.
|
||||
- Opened Wailing Caverns from Dungeon Finder. Role selection was restricted to
|
||||
Healer, matching the saved Holy specialization; no Tank or DPS override was
|
||||
available.
|
||||
- Reviewed the roster preview and a live Wailing Caverns combat scene. The
|
||||
player body, staff, and all four AI party bodies/equipment silhouettes were
|
||||
visible and correctly anchored without obscuring the HUD or playfield.
|
||||
- Opened Inventory & Equipment in the dungeon. The player had zero earned
|
||||
items but every applicable slot resolved to a level-10 Priest healer
|
||||
heirloom and reported average item level 10. Morvane, the Blood Death Knight
|
||||
companion, showed a complete tank heirloom loadout with the intended
|
||||
two-handed weapon layout and empty conflicting slots.
|
||||
- Browser logs contained no uncaught application errors. Only the existing
|
||||
third-party initialization deprecation warning appeared.
|
||||
- Automated coverage validates every classic and CoA specialization, Feral
|
||||
Bear/Cat separation, RoM-derived profiles, player and party scaling caps,
|
||||
same-level catalog dominance at levels 1/40/79, real item-level-85 upgrade
|
||||
availability for every gear archetype, two-hand replacement scoring,
|
||||
fallback reappearance, percentage-preserving stat refreshes, specialization
|
||||
persistence and talent reset behavior, and the item-level-255 save limit.
|
||||
- Verification: the complete Vitest suite passed all 928 cases across 171
|
||||
files, including all 43 avatar cases. All 15 server persistence/API tests
|
||||
passed. The production build completed with 925 modules transformed, KTX2
|
||||
and environment-performance audits remained green, and the release-candidate
|
||||
check passed. The existing large-chunk advisory remains informational.
|
||||
|
||||
# 2026-09-07 — modular encounter completion and boss guide
|
||||
|
||||
- Entered Wailing Caverns through the live offline roster and verified the 3D
|
||||
dungeon, party HUD, minimap, and buff/debuff strips remained readable. The
|
||||
encounter timeline occupies the same top-right HUD column beneath those
|
||||
elements, keeping timed casts and pending health thresholds out of the
|
||||
central playfield.
|
||||
- Opened the new Boss Guide from the in-dungeon pause menu. The three-column
|
||||
field manual listed both WoW and RuneWaker dungeons, their bosses, source
|
||||
provenance, triggers, timings, target/effect descriptions, warning geometry,
|
||||
counterplay, and a concrete test procedure for every listed mechanic.
|
||||
- Selected Archaedas in Uldaman and confirmed all four authored mechanics:
|
||||
Awaken Earthen Custodian every 10 seconds, the two-second 18-metre Ground
|
||||
Tremor warning circle, six Earthen Guardians at 70% health, and two Vault
|
||||
Warders at 40% health. Bosses with no sourced special attack are explicitly
|
||||
labelled with zero special mechanics instead of receiving invented behavior.
|
||||
- The encounter authoring layer now drives serializable timers, health gates,
|
||||
guards, actions, summon groups, chained signals, stateful world objects,
|
||||
teleports, rewards, checkpoints, and online leader-authoritative interaction
|
||||
validation. Harmful area mechanics use the same geometry for both the world
|
||||
warning and hit resolution.
|
||||
- Browser review found no visible layout or interaction regressions. The only
|
||||
previously observed browser message remains Rapier's third-party
|
||||
initialization deprecation warning.
|
||||
- Source limitations and the exact remaining RuneWaker evidence, navigation,
|
||||
formula, loot, animation, and asset-fidelity gaps are recorded in the linked
|
||||
deterministic audit reports and authoring documentation rather than silently
|
||||
approximated.
|
||||
- Full-WDB environment packages for Raksha Temple and The Origin completed and
|
||||
passed their GLB gates. Treasure Trove's 908-placement visual/collision stage
|
||||
also completed, but its full-resolution 36-tile Recast bake did not terminate
|
||||
after nearly two CPU-hours on the watershed path and more than four CPU-hours
|
||||
on the monotone path. The fail-closed pipeline preserved the prior validated
|
||||
shipping package; this remaining offline bake is documented in
|
||||
`ENCOUNTER_AUTHORING.md` instead of lowering navigation resolution.
|
||||
- Verification passed: the complete Vitest suite passed all 981 cases across
|
||||
185 files; online authority passed all 4 server cases; RuneWaker gameplay,
|
||||
encounter, environment, navigation, formula/loot, and animation audits passed;
|
||||
and the production build completed with 955 modules transformed. The KTX2
|
||||
audit covered 12,347 compressed textures with zero fallbacks, and the
|
||||
environment-performance audit found no regressions. The existing large-chunk
|
||||
advisory and Rapier initialization deprecation warning remain informational.
|
||||
@@ -41,6 +41,32 @@ npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
Boss-mechanic and dungeon-event extension points, telegraph rules, checkpoint
|
||||
behavior, test workflow, and the explicit source-gap ledger are documented in
|
||||
[`ENCOUNTER_AUTHORING.md`](ENCOUNTER_AUTHORING.md).
|
||||
|
||||
## Content downloads
|
||||
|
||||
Production browser and Android builds ship a compact required set: the game
|
||||
runtime, interface, starter equipment, Wailing Caverns starter content, and a
|
||||
basic male/female Human character model with its default appearance. Large
|
||||
character and skin libraries, additional dungeons, Manastorm stages, and the
|
||||
original spell-effects and sound pack are optional downloads.
|
||||
|
||||
Open **Downloads** from the main menu to download, update, or remove optional
|
||||
packs on either platform. Browser downloads live in an app-managed verified
|
||||
cache; Android downloads live in app-private storage. Removing a pack never
|
||||
removes a roster or save. If a character's model/skin pack is unavailable, the
|
||||
shared character renderer displays the matching-gender basic Human while
|
||||
preserving that character's identity and progress for a later reinstall.
|
||||
|
||||
Generated spell effects and synthesized combat cues are the default and need
|
||||
no original-media download. Players who want the packaged original visuals and
|
||||
sounds can download that pack explicitly, select **Original** effects, and
|
||||
remove the pack again to reclaim its storage. See
|
||||
[`CONTENT_UPDATES.md`](CONTENT_UPDATES.md) for the content lifecycle and release
|
||||
contract.
|
||||
|
||||
## Conquest of Azeroth classes
|
||||
|
||||
All 21 CoA classes use a checked-in live Vol'Jin catalog generated from
|
||||
@@ -81,12 +107,23 @@ validates the relational data and ships both a browser-safe JSON catalog and
|
||||
for a future loot-browser screen.
|
||||
|
||||
Every Wailing Caverns boss rolls one item when it dies. The item instance
|
||||
captures the player's level at the killing blow, remains on the corpse until
|
||||
looted, and is then saved in that character's inventory. The SQL loot source
|
||||
contains the original 3.3.5a attributes, ratings, armor, block, weapon damage,
|
||||
weapon speed, regeneration, and spell-power values for each item. A scaled
|
||||
drop preserves that original stat distribution instead of replacing it with a
|
||||
generic item-power budget.
|
||||
captures its item level at the killing blow, remains on the corpse until
|
||||
looted, and is then saved in that character's inventory. Drops use character
|
||||
level through 79; level-80 drops begin the first endgame tier at item level 85.
|
||||
The SQL loot source contains the original 3.3.5a attributes, ratings, armor,
|
||||
block, weapon damage, weapon speed, regeneration, and spell-power values for
|
||||
each item. A scaled drop preserves that original stat distribution instead of
|
||||
replacing it with a generic item-power budget.
|
||||
|
||||
Every WoW and Conquest of Azeroth character has a saved specialization that
|
||||
locks its activity role and selects a complete specialization-aware heirloom
|
||||
loadout. Runes of Magic derives the same profile from its active primary class
|
||||
and selected role. Virtual heirlooms fill every applicable unoccupied slot,
|
||||
rescale through item level 79 for players and 80 for AI party members, never
|
||||
enter inventory, and automatically return when earned equipment is removed.
|
||||
During leveling their score is normalized above compatible same-level catalog
|
||||
drops. At level 80 the item-level-85 boss tier contains real upgrades, which
|
||||
the equipment and loot interfaces highlight without equipping automatically.
|
||||
|
||||
The equipment runtime uses the Wrath primary attributes and itemizable combat
|
||||
stats. Strength, Agility, Stamina, Intellect, Spirit, Attack Power, Spell
|
||||
@@ -181,12 +218,13 @@ npm run android:apk
|
||||
The debug APK is written under `android/app/build/outputs/apk/debug/`. To build
|
||||
and install directly onto a connected device, use `npm run android:install`.
|
||||
|
||||
Android uses a separate compact `dist-android` build. It bundles the client,
|
||||
interface, equipment, and Wailing Caverns starter content while optional
|
||||
dungeons, character packs, and Manastorm stages are downloaded into app-private
|
||||
storage. Installed content and local saves remain playable without a server
|
||||
connection and survive APK updates. See `CONTENT_UPDATES.md` for the update
|
||||
contract and `DEPLOYMENT.md` for TrueNAS publishing and signed Gitea releases.
|
||||
Android uses a compact `dist-android` build with the same required-content
|
||||
boundary as the production browser build. Optional dungeons, character/skin
|
||||
packs, Manastorm stages, and original spell media are downloaded into
|
||||
app-private storage. Installed content and local saves remain playable without
|
||||
a server connection and survive APK updates. See `CONTENT_UPDATES.md` for the
|
||||
update contract and `DEPLOYMENT.md` for TrueNAS publishing and signed Gitea
|
||||
releases.
|
||||
|
||||
## Runtime details
|
||||
|
||||
@@ -206,6 +244,8 @@ contract and `DEPLOYMENT.md` for TrueNAS publishing and signed Gitea releases.
|
||||
compressed in GPU memory instead of expanding to uncompressed RGBA textures.
|
||||
- Missing production GLBs activate a small cave proxy; malformed assets activate
|
||||
the recoverable scene-error screen.
|
||||
- Missing optional character or skin packs render the built-in basic Human
|
||||
model instead of hiding the player, party member, or roster preview.
|
||||
- The controller action HUD displays only the active eight-slot layer. Its
|
||||
face-button diamond, shoulders, and D-pad keep the physical controller layout
|
||||
visible without showing all 24 bindings over the playfield.
|
||||
|
||||
+8
-4
@@ -71,9 +71,12 @@ file, use the shorter procedure at the end. Those updates can be published by
|
||||
the server and downloaded by an already-installed APK without an Obtainium
|
||||
release.
|
||||
|
||||
Single-player remains offline-capable. A fresh APK includes the runtime and
|
||||
starter content. Additional content that a player has downloaded is retained
|
||||
in Android app-private storage and can be used offline after download.
|
||||
Single-player remains offline-capable. A fresh APK includes the runtime,
|
||||
interface, starter equipment, Wailing Caverns, and the basic Human character
|
||||
fallback. Additional content that a player has downloaded is retained in
|
||||
Android app-private storage and can be used offline after download. Generated
|
||||
spell effects are available without downloading the optional original-media
|
||||
pack.
|
||||
|
||||
## Fixed locations
|
||||
|
||||
@@ -287,7 +290,8 @@ git diff --cached --check
|
||||
```
|
||||
|
||||
Do not continue if the staged list contains a keystore, APK, `.env` file,
|
||||
database, `content/`, `dist/`, `dist-android/`, or `.android-public/` output.
|
||||
database, `content/`, `dist/`, `dist-android/`, `.web-public/`, or
|
||||
`.android-public/` output.
|
||||
Unstage any accidental file with `git restore --staged -- <path>` without
|
||||
deleting the local file.
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# RuneWaker encounter content catalog
|
||||
|
||||
This is the reviewed runtime layer above the exhaustive, machine-generated RuneWaker
|
||||
encounter audit. It intentionally contains only source behavior that can be mapped without
|
||||
guessing. The executable catalog is `src/game/runewakerEncounterCatalog.ts`.
|
||||
|
||||
## Bounded reviewed coverage
|
||||
|
||||
| Dungeon | Deferred source rows mapped | Runtime behavior | Fidelity |
|
||||
| --- | --- | --- | --- |
|
||||
| Forsaken Abbey | `12230`, `12236`, `12238` | Three two-member Ghoul/Carrion Spider formations are created on encounter initialization. After the source `sleep(20)` (2 seconds because RuneWaker sleep units are tenths of a second), the encounter enters its patrol phase. | source-backed adapted |
|
||||
| Treasure Trove | `5952428`, `5952438` | Killing Boulder Sidaar (`5952494`) arms switch `112100`; interacting with it activates the switch and lowers drawbridge `112098` once. | source-backed adapted |
|
||||
|
||||
The Forsaken mapping is backed by preserved Lua `00571.lua` and matrix helper `00727.lua`.
|
||||
The Treasure Trove mapping is backed by preserved Lua `01159.lua`. Snapshot, mechanics-audit,
|
||||
and source hashes are attached to each catalog review record.
|
||||
|
||||
“Source-backed adapted” is deliberate. Source IDs, trigger order, spawn membership, timing,
|
||||
and placement transforms are retained. HealerMan supplies serializable phases, procedural
|
||||
world-object geometry, projected heights, interaction range, and the packaged canonical Ghoul
|
||||
visual/combat definition for the source-only level-scaled Ghoul template `100653`.
|
||||
|
||||
## Effective ledger after this review
|
||||
|
||||
The generated audit baseline inventories 4,113 deferred source rows and 1,136 unresolved
|
||||
mechanics-evidence records. This catalog maps five unique deferred rows and clears five stable
|
||||
evidence IDs, leaving:
|
||||
|
||||
| Ledger item | Baseline | Reviewed here | Remaining |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Deferred source rows | 4,113 | 5 | 4,108 |
|
||||
| Unresolved mechanics-evidence records | 1,136 | 5 | 1,131 |
|
||||
|
||||
Per affected package, Forsaken Abbey moves from 101 to 98 deferred rows and 77 to 74
|
||||
unresolved evidence records. Treasure Trove moves from 188 to 186 deferred rows and 47 to 45
|
||||
unresolved evidence records. All other package counts remain unchanged.
|
||||
|
||||
The exhaustive generated JSON remains the immutable audit baseline until its generator learns
|
||||
to consume the runtime catalog. The effective counts above are exported as
|
||||
`RUNEWAKER_ENCOUNTER_CONTENT_LEDGER` and are regression tested.
|
||||
|
||||
## Explicit remaining gaps
|
||||
|
||||
- Forsaken formations remain at their source start positions after the two-second hold. The
|
||||
source uses endless flag-path patrols; the encounter runtime has no reviewed path-following
|
||||
action yet.
|
||||
- Forsaken source template `100653` is a level-scaled Ghoul variant created only by Lua and has
|
||||
no packaged actor row. The runtime alias reuses canonical Ghoul `100072`; this is not claimed
|
||||
as an exact visual or formula mapping.
|
||||
- The Forsaken keeper gate (`7883`) remains deferred. Its four Necromage deaths source-summon
|
||||
Ghoul Drake `100073`, but that summon has neither a packaged entity nor a reviewed combat
|
||||
definition. Opening the gate without that fight would be false fidelity.
|
||||
- Black Mary's Hold Door (`5952433`) remains deferred because its source interaction consumes
|
||||
inventory item `203550`; encounter guards currently expose no inventory predicate.
|
||||
- Treasure Trove uses procedural bridge/switch proxies because source object meshes and ACT
|
||||
motions are not packaged.
|
||||
- The remaining 4,108 rows require the same source-by-source review. Bulk promotion from names,
|
||||
model paths, or regex categories is not evidence of executable behavior.
|
||||
|
||||
## Validation
|
||||
|
||||
`src/game/runewakerEncounterCatalog.test.ts` validates each encounter definition and requires
|
||||
every referenced runtime spawn, entity, encounter spawn group, world object state, and objective
|
||||
to exist. It also simulates both reviewed state machines and locks the ledger arithmetic.
|
||||
@@ -73,6 +73,23 @@ attaches native animated client models by display ID. It writes:
|
||||
- `src/game/generated/dungeonCampaignCatalog.compressed.ts`, the bundled catalog
|
||||
- `src/game/generated/dungeonAvailability.json`, the generated availability list
|
||||
|
||||
Boss specials are compiled as independent mechanic records. AzerothCore
|
||||
SmartAI timers and health events are imported automatically; C++ encounter
|
||||
spells are imported from the pinned script checkout. Add or override a boss in
|
||||
`dungeon-pipeline/boss-mechanics.json` when an encounter needs hand-authored
|
||||
targeting, summon behavior, timing, or telegraph geometry. Use `mode: "replace"`
|
||||
to make that list authoritative or `mode: "append"` to layer entries ahead of
|
||||
the imported mechanics, then run `npm run dungeon:catalog`. The generated
|
||||
`combat.mechanicIds` list is the stable loadout boundary intended for later
|
||||
encounter randomization.
|
||||
|
||||
The same catalog also stores Runewaker kits under
|
||||
`rw-<dungeon>-<entity-id>` keys. Run `npm run runewaker:bosses:refresh` to
|
||||
rebuild those records from preserved RuneWaker spell/Lua evidence, regenerate
|
||||
the population packages, and run `npm run runewaker:audit:gameplay`. A record
|
||||
whose preserved source has no recoverable combat ability is explicitly marked
|
||||
`source.kind: "healer-man-source-gap"` rather than being claimed as authentic.
|
||||
|
||||
Stock AzerothCore population is used for the standard dungeons. The three
|
||||
Ascension-specific instances—Tinkertech Showdown, Frozen Reach, and Forgotten
|
||||
Mine—keep their client encounter identities and use an explicit procedural
|
||||
@@ -229,4 +246,4 @@ Kresh is intentionally recorded as a source-faithful melee-only boss: entry
|
||||
3653 has no spell or SmartAI row in the pinned AzerothCore snapshot. Trial of
|
||||
the Champion is likewise documented as a boss-arena population rather than a
|
||||
trash route. These exceptions are asserted by exact dungeon/entity IDs; any
|
||||
new basic-only boss or mob-less dungeon fails the batch.
|
||||
new basic-only boss or mob-less dungeon fails the batch.
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"status": "green",
|
||||
"scope": "HealerMan WoW five-player campaign",
|
||||
"scope": "HealerMan WoW five-player combat and animation coverage",
|
||||
"summary": {
|
||||
"dungeons": 54,
|
||||
"mobs": 1031,
|
||||
"bosses": 253,
|
||||
"attacks": 2953,
|
||||
"specialAttacks": 1669,
|
||||
"attacks": 3765,
|
||||
"specialAttacks": 2481,
|
||||
"nativeModels": 1266,
|
||||
"proceduralModels": 18,
|
||||
"uniqueNativeModels": 1166,
|
||||
"nativeStateFallbacks": 16
|
||||
"nativeStateFallbacks": 18
|
||||
},
|
||||
"documentedExceptions": [
|
||||
{
|
||||
@@ -47,6 +47,14 @@
|
||||
],
|
||||
"renderer": "OriginalCreatureModel combat fallback"
|
||||
},
|
||||
{
|
||||
"url": "/assets/game/dungeons/dire-maul/creatures/display-14430-animated.glb",
|
||||
"state": "move",
|
||||
"users": [
|
||||
"dire-maul:entry-14386"
|
||||
],
|
||||
"renderer": "AnimatedMobBody locomotion fallback"
|
||||
},
|
||||
{
|
||||
"url": "/assets/game/dungeons/dire-maul/creatures/display-14430-animated.glb",
|
||||
"state": "attack",
|
||||
@@ -56,7 +64,15 @@
|
||||
"renderer": "OriginalCreatureModel combat fallback"
|
||||
},
|
||||
{
|
||||
"url": "/assets/game/creatures/shared/2fbb8d2226a7fdf3e9a7af13568f6bb46cb1b27afc008732832b9fd42c2e510e.glb",
|
||||
"url": "/assets/game/dungeons/dire-maul/creatures/display-14430-animated.glb",
|
||||
"state": "wound",
|
||||
"users": [
|
||||
"dire-maul:entry-14386"
|
||||
],
|
||||
"renderer": "OriginalCreatureModel combat fallback"
|
||||
},
|
||||
{
|
||||
"url": "/assets/game/creatures/shared/52c8b3aea63f21f8e98ad84eaf601f2918d1df17a05525d41f5b3e17da53c8ff.glb",
|
||||
"state": "wound",
|
||||
"users": [
|
||||
"steamvault:entry-21694",
|
||||
@@ -170,8 +186,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 24,
|
||||
"bosses": 7,
|
||||
"attacks": 58,
|
||||
"specialAttacks": 27,
|
||||
"attacks": 65,
|
||||
"specialAttacks": 34,
|
||||
"nativeModels": 31,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -182,8 +198,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 6,
|
||||
"bosses": 5,
|
||||
"attacks": 32,
|
||||
"specialAttacks": 21,
|
||||
"attacks": 42,
|
||||
"specialAttacks": 31,
|
||||
"nativeModels": 11,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -194,8 +210,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 15,
|
||||
"bosses": 7,
|
||||
"attacks": 48,
|
||||
"specialAttacks": 26,
|
||||
"attacks": 55,
|
||||
"specialAttacks": 33,
|
||||
"nativeModels": 22,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -218,8 +234,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 31,
|
||||
"bosses": 7,
|
||||
"attacks": 78,
|
||||
"specialAttacks": 40,
|
||||
"attacks": 87,
|
||||
"specialAttacks": 49,
|
||||
"nativeModels": 38,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -230,8 +246,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 21,
|
||||
"bosses": 6,
|
||||
"attacks": 58,
|
||||
"specialAttacks": 31,
|
||||
"attacks": 65,
|
||||
"specialAttacks": 38,
|
||||
"nativeModels": 27,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -242,8 +258,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 31,
|
||||
"bosses": 7,
|
||||
"attacks": 85,
|
||||
"specialAttacks": 47,
|
||||
"attacks": 104,
|
||||
"specialAttacks": 66,
|
||||
"nativeModels": 38,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -254,8 +270,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 27,
|
||||
"bosses": 4,
|
||||
"attacks": 68,
|
||||
"specialAttacks": 37,
|
||||
"attacks": 82,
|
||||
"specialAttacks": 51,
|
||||
"nativeModels": 31,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -266,8 +282,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 29,
|
||||
"bosses": 4,
|
||||
"attacks": 86,
|
||||
"specialAttacks": 53,
|
||||
"attacks": 93,
|
||||
"specialAttacks": 60,
|
||||
"nativeModels": 33,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -278,8 +294,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 19,
|
||||
"bosses": 6,
|
||||
"attacks": 55,
|
||||
"specialAttacks": 30,
|
||||
"attacks": 63,
|
||||
"specialAttacks": 38,
|
||||
"nativeModels": 25,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -290,8 +306,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 32,
|
||||
"bosses": 5,
|
||||
"attacks": 89,
|
||||
"specialAttacks": 52,
|
||||
"attacks": 107,
|
||||
"specialAttacks": 70,
|
||||
"nativeModels": 37,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -302,8 +318,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 16,
|
||||
"bosses": 5,
|
||||
"attacks": 53,
|
||||
"specialAttacks": 32,
|
||||
"attacks": 69,
|
||||
"specialAttacks": 48,
|
||||
"nativeModels": 21,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -314,8 +330,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 63,
|
||||
"bosses": 15,
|
||||
"attacks": 210,
|
||||
"specialAttacks": 132,
|
||||
"attacks": 273,
|
||||
"specialAttacks": 195,
|
||||
"nativeModels": 78,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -326,8 +342,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 48,
|
||||
"bosses": 13,
|
||||
"attacks": 145,
|
||||
"specialAttacks": 84,
|
||||
"attacks": 182,
|
||||
"specialAttacks": 121,
|
||||
"nativeModels": 61,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -338,8 +354,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 30,
|
||||
"bosses": 8,
|
||||
"attacks": 115,
|
||||
"specialAttacks": 77,
|
||||
"attacks": 150,
|
||||
"specialAttacks": 112,
|
||||
"nativeModels": 38,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -350,8 +366,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 41,
|
||||
"bosses": 9,
|
||||
"attacks": 131,
|
||||
"specialAttacks": 81,
|
||||
"attacks": 176,
|
||||
"specialAttacks": 126,
|
||||
"nativeModels": 50,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -362,8 +378,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 25,
|
||||
"bosses": 8,
|
||||
"attacks": 83,
|
||||
"specialAttacks": 50,
|
||||
"attacks": 95,
|
||||
"specialAttacks": 62,
|
||||
"nativeModels": 33,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -374,8 +390,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 7,
|
||||
"bosses": 4,
|
||||
"attacks": 21,
|
||||
"specialAttacks": 10,
|
||||
"attacks": 23,
|
||||
"specialAttacks": 12,
|
||||
"nativeModels": 11,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -386,8 +402,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 63,
|
||||
"bosses": 12,
|
||||
"attacks": 184,
|
||||
"specialAttacks": 109,
|
||||
"attacks": 242,
|
||||
"specialAttacks": 167,
|
||||
"nativeModels": 75,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -398,8 +414,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 29,
|
||||
"bosses": 3,
|
||||
"attacks": 74,
|
||||
"specialAttacks": 42,
|
||||
"attacks": 92,
|
||||
"specialAttacks": 60,
|
||||
"nativeModels": 32,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -410,8 +426,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 14,
|
||||
"bosses": 3,
|
||||
"attacks": 44,
|
||||
"specialAttacks": 27,
|
||||
"attacks": 53,
|
||||
"specialAttacks": 36,
|
||||
"nativeModels": 17,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -422,8 +438,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 10,
|
||||
"bosses": 2,
|
||||
"attacks": 36,
|
||||
"specialAttacks": 24,
|
||||
"attacks": 41,
|
||||
"specialAttacks": 29,
|
||||
"nativeModels": 12,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -434,8 +450,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 14,
|
||||
"bosses": 3,
|
||||
"attacks": 50,
|
||||
"specialAttacks": 33,
|
||||
"attacks": 63,
|
||||
"specialAttacks": 46,
|
||||
"nativeModels": 17,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -446,8 +462,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 18,
|
||||
"bosses": 4,
|
||||
"attacks": 59,
|
||||
"specialAttacks": 37,
|
||||
"attacks": 80,
|
||||
"specialAttacks": 58,
|
||||
"nativeModels": 22,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -458,8 +474,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 18,
|
||||
"bosses": 3,
|
||||
"attacks": 53,
|
||||
"specialAttacks": 32,
|
||||
"attacks": 60,
|
||||
"specialAttacks": 39,
|
||||
"nativeModels": 21,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -470,8 +486,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 25,
|
||||
"bosses": 4,
|
||||
"attacks": 73,
|
||||
"specialAttacks": 44,
|
||||
"attacks": 105,
|
||||
"specialAttacks": 76,
|
||||
"nativeModels": 29,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -482,8 +498,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 24,
|
||||
"bosses": 5,
|
||||
"attacks": 67,
|
||||
"specialAttacks": 38,
|
||||
"attacks": 85,
|
||||
"specialAttacks": 56,
|
||||
"nativeModels": 29,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -494,8 +510,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 14,
|
||||
"bosses": 3,
|
||||
"attacks": 45,
|
||||
"specialAttacks": 28,
|
||||
"attacks": 69,
|
||||
"specialAttacks": 52,
|
||||
"nativeModels": 17,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -506,8 +522,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 18,
|
||||
"bosses": 4,
|
||||
"attacks": 46,
|
||||
"specialAttacks": 24,
|
||||
"attacks": 67,
|
||||
"specialAttacks": 45,
|
||||
"nativeModels": 22,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -518,8 +534,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 14,
|
||||
"bosses": 2,
|
||||
"attacks": 39,
|
||||
"specialAttacks": 23,
|
||||
"attacks": 50,
|
||||
"specialAttacks": 34,
|
||||
"nativeModels": 16,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -530,8 +546,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 11,
|
||||
"bosses": 3,
|
||||
"attacks": 33,
|
||||
"specialAttacks": 19,
|
||||
"attacks": 46,
|
||||
"specialAttacks": 32,
|
||||
"nativeModels": 14,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -542,8 +558,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 12,
|
||||
"bosses": 2,
|
||||
"attacks": 30,
|
||||
"specialAttacks": 16,
|
||||
"attacks": 44,
|
||||
"specialAttacks": 30,
|
||||
"nativeModels": 14,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -554,8 +570,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 15,
|
||||
"bosses": 3,
|
||||
"attacks": 33,
|
||||
"specialAttacks": 15,
|
||||
"attacks": 57,
|
||||
"specialAttacks": 39,
|
||||
"nativeModels": 18,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -566,8 +582,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 4,
|
||||
"bosses": 3,
|
||||
"attacks": 20,
|
||||
"specialAttacks": 13,
|
||||
"attacks": 25,
|
||||
"specialAttacks": 18,
|
||||
"nativeModels": 7,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -578,8 +594,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 26,
|
||||
"bosses": 3,
|
||||
"attacks": 45,
|
||||
"specialAttacks": 16,
|
||||
"attacks": 53,
|
||||
"specialAttacks": 24,
|
||||
"nativeModels": 29,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -590,8 +606,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 19,
|
||||
"bosses": 3,
|
||||
"attacks": 35,
|
||||
"specialAttacks": 13,
|
||||
"attacks": 44,
|
||||
"specialAttacks": 22,
|
||||
"nativeModels": 22,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -602,8 +618,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 13,
|
||||
"bosses": 4,
|
||||
"attacks": 32,
|
||||
"specialAttacks": 15,
|
||||
"attacks": 44,
|
||||
"specialAttacks": 27,
|
||||
"nativeModels": 17,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -614,8 +630,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 21,
|
||||
"bosses": 5,
|
||||
"attacks": 50,
|
||||
"specialAttacks": 24,
|
||||
"attacks": 61,
|
||||
"specialAttacks": 35,
|
||||
"nativeModels": 26,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -626,8 +642,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 8,
|
||||
"bosses": 4,
|
||||
"attacks": 27,
|
||||
"specialAttacks": 15,
|
||||
"attacks": 33,
|
||||
"specialAttacks": 21,
|
||||
"nativeModels": 12,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -638,8 +654,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 16,
|
||||
"bosses": 4,
|
||||
"attacks": 41,
|
||||
"specialAttacks": 21,
|
||||
"attacks": 44,
|
||||
"specialAttacks": 24,
|
||||
"nativeModels": 20,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -650,8 +666,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 13,
|
||||
"bosses": 4,
|
||||
"attacks": 32,
|
||||
"specialAttacks": 15,
|
||||
"attacks": 42,
|
||||
"specialAttacks": 25,
|
||||
"nativeModels": 16,
|
||||
"proceduralModels": 1,
|
||||
"status": "green"
|
||||
@@ -662,8 +678,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 18,
|
||||
"bosses": 4,
|
||||
"attacks": 38,
|
||||
"specialAttacks": 16,
|
||||
"attacks": 45,
|
||||
"specialAttacks": 23,
|
||||
"nativeModels": 22,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -674,8 +690,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 10,
|
||||
"bosses": 3,
|
||||
"attacks": 25,
|
||||
"specialAttacks": 12,
|
||||
"attacks": 30,
|
||||
"specialAttacks": 17,
|
||||
"nativeModels": 13,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -686,8 +702,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 16,
|
||||
"bosses": 4,
|
||||
"attacks": 35,
|
||||
"specialAttacks": 15,
|
||||
"attacks": 49,
|
||||
"specialAttacks": 29,
|
||||
"nativeModels": 20,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -698,8 +714,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 15,
|
||||
"bosses": 5,
|
||||
"attacks": 43,
|
||||
"specialAttacks": 23,
|
||||
"attacks": 53,
|
||||
"specialAttacks": 33,
|
||||
"nativeModels": 20,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -710,8 +726,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 2,
|
||||
"bosses": 3,
|
||||
"attacks": 19,
|
||||
"specialAttacks": 14,
|
||||
"attacks": 24,
|
||||
"specialAttacks": 19,
|
||||
"nativeModels": 5,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -722,8 +738,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 19,
|
||||
"bosses": 5,
|
||||
"attacks": 43,
|
||||
"specialAttacks": 19,
|
||||
"attacks": 60,
|
||||
"specialAttacks": 36,
|
||||
"nativeModels": 24,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -734,8 +750,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 11,
|
||||
"bosses": 2,
|
||||
"attacks": 21,
|
||||
"specialAttacks": 8,
|
||||
"attacks": 23,
|
||||
"specialAttacks": 10,
|
||||
"nativeModels": 13,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -746,8 +762,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 0,
|
||||
"bosses": 5,
|
||||
"attacks": 22,
|
||||
"specialAttacks": 17,
|
||||
"attacks": 88,
|
||||
"specialAttacks": 83,
|
||||
"nativeModels": 5,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -758,8 +774,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 27,
|
||||
"bosses": 3,
|
||||
"attacks": 43,
|
||||
"specialAttacks": 13,
|
||||
"attacks": 49,
|
||||
"specialAttacks": 19,
|
||||
"nativeModels": 30,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
@@ -770,8 +786,8 @@
|
||||
"source": "azerothcore",
|
||||
"mobs": 8,
|
||||
"bosses": 2,
|
||||
"attacks": 26,
|
||||
"specialAttacks": 16,
|
||||
"attacks": 38,
|
||||
"specialAttacks": 28,
|
||||
"nativeModels": 10,
|
||||
"proceduralModels": 0,
|
||||
"status": "green"
|
||||
|
||||
@@ -1,64 +1,66 @@
|
||||
# WoW dungeon content report
|
||||
# WoW dungeon combat and animation report
|
||||
|
||||
Status: **green**
|
||||
|
||||
Coverage: 54 dungeons, 1031 mobs, 253 bosses, 2953 executable attacks, and 1669 special attacks/mechanics.
|
||||
Scope: executable combat populations and creature animation bindings. This report does not certify instance scripts, doors, interactables, escorts, waves, vehicles, or navigation.
|
||||
|
||||
Animation paths: 1266 native model bindings and 18 procedural bindings across 1166 unique native GLBs; 16 missing native states use the shared procedural fallback.
|
||||
Coverage: 54 dungeons, 1031 mobs, 253 bosses, 3765 executable attacks, and 2481 special attacks/mechanics.
|
||||
|
||||
Animation paths: 1266 native model bindings and 18 procedural bindings across 1166 unique native GLBs; 18 missing native states use the shared procedural fallback.
|
||||
|
||||
| Dungeon | Mobs | Bosses | Attacks | Specials | Native | Procedural | Status |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| Shadowfang Keep | 24 | 7 | 58 | 27 | 31 | 0 | green |
|
||||
| Stormwind Stockade | 6 | 5 | 32 | 21 | 11 | 0 | green |
|
||||
| The Deadmines | 15 | 7 | 48 | 26 | 22 | 0 | green |
|
||||
| Shadowfang Keep | 24 | 7 | 65 | 34 | 31 | 0 | green |
|
||||
| Stormwind Stockade | 6 | 5 | 42 | 31 | 11 | 0 | green |
|
||||
| The Deadmines | 15 | 7 | 55 | 33 | 22 | 0 | green |
|
||||
| Wailing Caverns | 12 | 8 | 51 | 31 | 15 | 5 | green |
|
||||
| Razorfen Kraul | 31 | 7 | 78 | 40 | 38 | 0 | green |
|
||||
| Blackfathom Deeps | 21 | 6 | 58 | 31 | 27 | 0 | green |
|
||||
| Uldaman | 31 | 7 | 85 | 47 | 38 | 0 | green |
|
||||
| Gnomeregan | 27 | 4 | 68 | 37 | 31 | 0 | green |
|
||||
| Sunken Temple | 29 | 4 | 86 | 53 | 33 | 0 | green |
|
||||
| Razorfen Downs | 19 | 6 | 55 | 30 | 25 | 0 | green |
|
||||
| Scarlet Monastery | 32 | 5 | 89 | 52 | 37 | 0 | green |
|
||||
| Zul'Farrak | 16 | 5 | 53 | 32 | 21 | 0 | green |
|
||||
| Blackrock Spire | 63 | 15 | 210 | 132 | 78 | 0 | green |
|
||||
| Blackrock Depths | 48 | 13 | 145 | 84 | 61 | 0 | green |
|
||||
| Scholomance | 30 | 8 | 115 | 77 | 38 | 0 | green |
|
||||
| Stratholme | 41 | 9 | 131 | 81 | 50 | 0 | green |
|
||||
| Maraudon | 25 | 8 | 83 | 50 | 33 | 0 | green |
|
||||
| Ragefire Chasm | 7 | 4 | 21 | 10 | 11 | 0 | green |
|
||||
| Dire Maul | 63 | 12 | 184 | 109 | 75 | 0 | green |
|
||||
| Hellfire Citadel: The Shattered Halls | 29 | 3 | 74 | 42 | 32 | 0 | green |
|
||||
| Hellfire Citadel: The Blood Furnace | 14 | 3 | 44 | 27 | 17 | 0 | green |
|
||||
| Hellfire Citadel: Ramparts | 10 | 2 | 36 | 24 | 12 | 0 | green |
|
||||
| Coilfang: The Steamvault | 14 | 3 | 50 | 33 | 17 | 0 | green |
|
||||
| Coilfang: The Underbog | 18 | 4 | 59 | 37 | 22 | 0 | green |
|
||||
| Coilfang: The Slave Pens | 18 | 3 | 53 | 32 | 21 | 0 | green |
|
||||
| Tempest Keep: The Arcatraz | 25 | 4 | 73 | 44 | 29 | 0 | green |
|
||||
| Tempest Keep: The Botanica | 24 | 5 | 67 | 38 | 29 | 0 | green |
|
||||
| Tempest Keep: The Mechanar | 14 | 3 | 45 | 28 | 17 | 0 | green |
|
||||
| Auchindoun: Shadow Labyrinth | 18 | 4 | 46 | 24 | 22 | 0 | green |
|
||||
| Auchindoun: Sethekk Halls | 14 | 2 | 39 | 23 | 16 | 0 | green |
|
||||
| Auchindoun: Mana-Tombs | 11 | 3 | 33 | 19 | 14 | 0 | green |
|
||||
| Auchindoun: Auchenai Crypts | 12 | 2 | 30 | 16 | 14 | 0 | green |
|
||||
| Magister's Terrace | 15 | 3 | 33 | 15 | 18 | 0 | green |
|
||||
| The Black Morass | 4 | 3 | 20 | 13 | 7 | 0 | green |
|
||||
| The Escape From Durnholde | 26 | 3 | 45 | 16 | 29 | 0 | green |
|
||||
| Utgarde Keep | 19 | 3 | 35 | 13 | 22 | 0 | green |
|
||||
| Utgarde Pinnacle | 13 | 4 | 32 | 15 | 17 | 0 | green |
|
||||
| The Nexus | 21 | 5 | 50 | 24 | 26 | 0 | green |
|
||||
| The Oculus | 8 | 4 | 27 | 15 | 12 | 0 | green |
|
||||
| The Culling of Stratholme | 16 | 4 | 41 | 21 | 20 | 0 | green |
|
||||
| Halls of Stone | 13 | 4 | 32 | 15 | 16 | 1 | green |
|
||||
| Drak'Tharon Keep | 18 | 4 | 38 | 16 | 22 | 0 | green |
|
||||
| Azjol-Nerub | 10 | 3 | 25 | 12 | 13 | 0 | green |
|
||||
| Halls of Lightning | 16 | 4 | 35 | 15 | 20 | 0 | green |
|
||||
| Gundrak | 15 | 5 | 43 | 23 | 20 | 0 | green |
|
||||
| Violet Hold | 2 | 3 | 19 | 14 | 5 | 0 | green |
|
||||
| Ahn'kahet: The Old Kingdom | 19 | 5 | 43 | 19 | 24 | 0 | green |
|
||||
| The Forge of Souls | 11 | 2 | 21 | 8 | 13 | 0 | green |
|
||||
| Trial of the Champion | 0 | 5 | 22 | 17 | 5 | 0 | green |
|
||||
| Pit of Saron | 27 | 3 | 43 | 13 | 30 | 0 | green |
|
||||
| Halls of Reflection | 8 | 2 | 26 | 16 | 10 | 0 | green |
|
||||
| Razorfen Kraul | 31 | 7 | 87 | 49 | 38 | 0 | green |
|
||||
| Blackfathom Deeps | 21 | 6 | 65 | 38 | 27 | 0 | green |
|
||||
| Uldaman | 31 | 7 | 104 | 66 | 38 | 0 | green |
|
||||
| Gnomeregan | 27 | 4 | 82 | 51 | 31 | 0 | green |
|
||||
| Sunken Temple | 29 | 4 | 93 | 60 | 33 | 0 | green |
|
||||
| Razorfen Downs | 19 | 6 | 63 | 38 | 25 | 0 | green |
|
||||
| Scarlet Monastery | 32 | 5 | 107 | 70 | 37 | 0 | green |
|
||||
| Zul'Farrak | 16 | 5 | 69 | 48 | 21 | 0 | green |
|
||||
| Blackrock Spire | 63 | 15 | 273 | 195 | 78 | 0 | green |
|
||||
| Blackrock Depths | 48 | 13 | 182 | 121 | 61 | 0 | green |
|
||||
| Scholomance | 30 | 8 | 150 | 112 | 38 | 0 | green |
|
||||
| Stratholme | 41 | 9 | 176 | 126 | 50 | 0 | green |
|
||||
| Maraudon | 25 | 8 | 95 | 62 | 33 | 0 | green |
|
||||
| Ragefire Chasm | 7 | 4 | 23 | 12 | 11 | 0 | green |
|
||||
| Dire Maul | 63 | 12 | 242 | 167 | 75 | 0 | green |
|
||||
| Hellfire Citadel: The Shattered Halls | 29 | 3 | 92 | 60 | 32 | 0 | green |
|
||||
| Hellfire Citadel: The Blood Furnace | 14 | 3 | 53 | 36 | 17 | 0 | green |
|
||||
| Hellfire Citadel: Ramparts | 10 | 2 | 41 | 29 | 12 | 0 | green |
|
||||
| Coilfang: The Steamvault | 14 | 3 | 63 | 46 | 17 | 0 | green |
|
||||
| Coilfang: The Underbog | 18 | 4 | 80 | 58 | 22 | 0 | green |
|
||||
| Coilfang: The Slave Pens | 18 | 3 | 60 | 39 | 21 | 0 | green |
|
||||
| Tempest Keep: The Arcatraz | 25 | 4 | 105 | 76 | 29 | 0 | green |
|
||||
| Tempest Keep: The Botanica | 24 | 5 | 85 | 56 | 29 | 0 | green |
|
||||
| Tempest Keep: The Mechanar | 14 | 3 | 69 | 52 | 17 | 0 | green |
|
||||
| Auchindoun: Shadow Labyrinth | 18 | 4 | 67 | 45 | 22 | 0 | green |
|
||||
| Auchindoun: Sethekk Halls | 14 | 2 | 50 | 34 | 16 | 0 | green |
|
||||
| Auchindoun: Mana-Tombs | 11 | 3 | 46 | 32 | 14 | 0 | green |
|
||||
| Auchindoun: Auchenai Crypts | 12 | 2 | 44 | 30 | 14 | 0 | green |
|
||||
| Magister's Terrace | 15 | 3 | 57 | 39 | 18 | 0 | green |
|
||||
| The Black Morass | 4 | 3 | 25 | 18 | 7 | 0 | green |
|
||||
| The Escape From Durnholde | 26 | 3 | 53 | 24 | 29 | 0 | green |
|
||||
| Utgarde Keep | 19 | 3 | 44 | 22 | 22 | 0 | green |
|
||||
| Utgarde Pinnacle | 13 | 4 | 44 | 27 | 17 | 0 | green |
|
||||
| The Nexus | 21 | 5 | 61 | 35 | 26 | 0 | green |
|
||||
| The Oculus | 8 | 4 | 33 | 21 | 12 | 0 | green |
|
||||
| The Culling of Stratholme | 16 | 4 | 44 | 24 | 20 | 0 | green |
|
||||
| Halls of Stone | 13 | 4 | 42 | 25 | 16 | 1 | green |
|
||||
| Drak'Tharon Keep | 18 | 4 | 45 | 23 | 22 | 0 | green |
|
||||
| Azjol-Nerub | 10 | 3 | 30 | 17 | 13 | 0 | green |
|
||||
| Halls of Lightning | 16 | 4 | 49 | 29 | 20 | 0 | green |
|
||||
| Gundrak | 15 | 5 | 53 | 33 | 20 | 0 | green |
|
||||
| Violet Hold | 2 | 3 | 24 | 19 | 5 | 0 | green |
|
||||
| Ahn'kahet: The Old Kingdom | 19 | 5 | 60 | 36 | 24 | 0 | green |
|
||||
| The Forge of Souls | 11 | 2 | 23 | 10 | 13 | 0 | green |
|
||||
| Trial of the Champion | 0 | 5 | 88 | 83 | 5 | 0 | green |
|
||||
| Pit of Saron | 27 | 3 | 49 | 19 | 30 | 0 | green |
|
||||
| Halls of Reflection | 8 | 2 | 38 | 28 | 10 | 0 | green |
|
||||
| Tinkertech Showdown | 3 | 1 | 8 | 4 | 0 | 4 | green |
|
||||
| Frozen Reach | 3 | 1 | 8 | 4 | 0 | 4 | green |
|
||||
| Forgotten Mine | 3 | 1 | 8 | 4 | 0 | 4 | green |
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "blades-edge-arena",
|
||||
"title": "Blade's Edge Arena",
|
||||
"mapId": 562,
|
||||
"lfgDungeonIds": [],
|
||||
"expansion": "the-burning-crusade",
|
||||
"clientBuild": "3.3.5a",
|
||||
"mapDirectory": "World/Maps/bladesedgearena",
|
||||
"environmentMode": "global-wmo",
|
||||
"difficultyVariants": [{"id": "rated", "difficulty": "normal", "spawnMask": 1, "levelRange": [80, 80]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
"extraction": {"rootWmo": "World/wmo/PvP/Buildings/AncientOrcArena/ancorc_PVPstadium.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||
"review": {"entrance": "required", "bossOrder": "not-applicable", "optionalEncounters": "not-applicable", "presentation": "required", "offMeshLinks": "required"},
|
||||
"exceptions": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "dalaran-sewers",
|
||||
"title": "Dalaran Sewers",
|
||||
"mapId": 617,
|
||||
"lfgDungeonIds": [],
|
||||
"expansion": "wrath-of-the-lich-king",
|
||||
"clientBuild": "3.3.5a",
|
||||
"mapDirectory": "World/Maps/DalaranArena",
|
||||
"environmentMode": "global-wmo",
|
||||
"difficultyVariants": [{"id": "rated", "difficulty": "normal", "spawnMask": 1, "levelRange": [80, 80]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
"extraction": {"rootWmo": "World/wmo/PvP/Buildings/Dalaran/dalaran_sewer_arena.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||
"review": {"entrance": "required", "bossOrder": "not-applicable", "optionalEncounters": "not-applicable", "presentation": "required", "offMeshLinks": "required"},
|
||||
"exceptions": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "nagrand-arena",
|
||||
"title": "Nagrand Arena",
|
||||
"mapId": 559,
|
||||
"lfgDungeonIds": [],
|
||||
"expansion": "the-burning-crusade",
|
||||
"clientBuild": "3.3.5a",
|
||||
"mapDirectory": "World/Maps/PVPZone01",
|
||||
"environmentMode": "global-wmo",
|
||||
"difficultyVariants": [{"id": "rated", "difficulty": "normal", "spawnMask": 1, "levelRange": [80, 80]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
"extraction": {"rootWmo": "World/wmo/Dungeon/OL_OgreHuts/PVP_Ogre_Arena01.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||
"review": {"entrance": "required", "bossOrder": "not-applicable", "optionalEncounters": "not-applicable", "presentation": "required", "offMeshLinks": "required"},
|
||||
"exceptions": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "ring-of-valor",
|
||||
"title": "Ring of Valor",
|
||||
"mapId": 618,
|
||||
"lfgDungeonIds": [],
|
||||
"expansion": "wrath-of-the-lich-king",
|
||||
"clientBuild": "3.3.5a",
|
||||
"mapDirectory": "World/Maps/OrgrimmarArena",
|
||||
"environmentMode": "global-wmo",
|
||||
"difficultyVariants": [{"id": "rated", "difficulty": "normal", "spawnMask": 1, "levelRange": [80, 80]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
"extraction": {"rootWmo": "World/wmo/PvP/Buildings/Orgrimmar/OrgrimmarArena.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||
"review": {"entrance": "required", "bossOrder": "not-applicable", "optionalEncounters": "not-applicable", "presentation": "required", "offMeshLinks": "required"},
|
||||
"exceptions": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "ruins-of-lordaeron",
|
||||
"title": "Ruins of Lordaeron",
|
||||
"mapId": 572,
|
||||
"lfgDungeonIds": [],
|
||||
"expansion": "the-burning-crusade",
|
||||
"clientBuild": "3.3.5a",
|
||||
"mapDirectory": "World/Maps/PVPLordaeron",
|
||||
"environmentMode": "global-wmo",
|
||||
"difficultyVariants": [{"id": "rated", "difficulty": "normal", "spawnMask": 1, "levelRange": [80, 80]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
"extraction": {"rootWmo": "World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||
"review": {"entrance": "required", "bossOrder": "not-applicable", "optionalEncounters": "not-applicable", "presentation": "required", "offMeshLinks": "required"},
|
||||
"exceptions": []
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "healer-man",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "healer-man",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.21",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "8.4.1",
|
||||
"@capacitor/core": "8.4.1",
|
||||
|
||||
+11
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "healer-man",
|
||||
"private": true,
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.21",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
@@ -10,6 +10,7 @@
|
||||
"db:backup": "node scripts/backup-game-database.mjs",
|
||||
"server:test": "node --test server/*.test.mjs",
|
||||
"content:publish": "node scripts/content/publish-content.mjs",
|
||||
"web:prepare": "node scripts/content/prepare-web-bootstrap.mjs",
|
||||
"android:prepare": "node scripts/content/prepare-android-bootstrap.mjs",
|
||||
"content:test": "node --test scripts/content/*.test.mjs",
|
||||
"effects:generate": "node scripts/combat-effects/generate-manifest.mjs --package-audio && node scripts/combat-effects/package-priest-original.mjs",
|
||||
@@ -62,6 +63,8 @@
|
||||
"dungeon:navmesh": "node scripts/dungeon-pipeline/cli.mjs navmesh",
|
||||
"dungeon:navmesh:conversion": "node scripts/dungeon-pipeline/cli.mjs navmesh-conversion",
|
||||
"dungeon:validate": "node scripts/dungeon-pipeline/cli.mjs validate",
|
||||
"arena:assets:build": "node scripts/arena-pipeline/build-wmo-arenas.mjs && node scripts/arena-pipeline/build-custom-arenas.mjs && node scripts/arena-pipeline/prepare-warnorken.mjs",
|
||||
"arena:assets:validate": "node scripts/arena-pipeline/validate-arenas.mjs",
|
||||
"dungeon:enable": "node scripts/dungeon-pipeline/cli.mjs enable",
|
||||
"runewaker:forsaken": "node \"%INIT_CWD%\\scripts\\runewaker-pipeline\\build-forsaken-abbey.mjs\"",
|
||||
"runewaker:forsaken:actors": "node scripts/runewaker-pipeline/build-forsaken-population.mjs --actors-only",
|
||||
@@ -73,6 +76,9 @@
|
||||
"runewaker:register:all": "node scripts/runewaker-pipeline/cli.mjs register-all",
|
||||
"runewaker:refresh:all": "node scripts/runewaker-pipeline/cli.mjs refresh-all",
|
||||
"runewaker:spells:refresh": "node scripts/runewaker-pipeline/export-spell-catalog.mjs",
|
||||
"runewaker:loot:refresh": "node scripts/runewaker-pipeline/export-loot-catalog.mjs",
|
||||
"runewaker:audit:formula-loot": "node --test scripts/runewaker-pipeline/runewaker-formula-export.test.mjs scripts/runewaker-pipeline/runewaker-loot-import.test.mjs && vitest run src/game/runewakerCombatFormula.test.ts src/game/runewakerLootCatalog.test.ts",
|
||||
"runewaker:bosses:refresh": "npm run runewaker:spells:refresh && node scripts/runewaker-pipeline/import-boss-mechanics.mjs",
|
||||
"runewaker:players:refresh": "node scripts/runewaker-pipeline/export-player-catalog.mjs && npm run runewaker:players:avatars",
|
||||
"runewaker:players:avatars": "node scripts/runewaker-pipeline/build-population.mjs --recipe scripts/runewaker-pipeline/recipes/runewaker-player-avatars.json --actors-only && node scripts/runewaker-pipeline/finalize-player-avatar-manifest.mjs",
|
||||
"runewaker:paperdolls:refresh": "node scripts/runewaker-pipeline/refresh-paperdoll-appearances.mjs",
|
||||
@@ -82,7 +88,9 @@
|
||||
"runewaker:audit:pose-batch": "node scripts/runewaker-pipeline/audit-animated-poses.mjs",
|
||||
"runewaker:audit:pose-master": "node scripts/runewaker-pipeline/generate-master-actor-pose-audit.mjs",
|
||||
"runewaker:audit:animations": "npm run runewaker:audit:animation-readiness && vitest run src/game/runewakerDungeonAnimation.test.ts src/game/mobAnimation.test.ts src/game/runewakerDungeonRegistry.test.ts",
|
||||
"runewaker:audit:gameplay": "node scripts/runewaker-pipeline/audit-gameplay-content.mjs",
|
||||
"runewaker:audit:gameplay": "node scripts/runewaker-pipeline/audit-gameplay-content.mjs && node --test scripts/runewaker-pipeline/encounter-implementation-report.test.mjs",
|
||||
"runewaker:audit:encounters": "node scripts/runewaker-pipeline/audit-encounter-implementation.mjs && node --test scripts/runewaker-pipeline/encounter-implementation-report.test.mjs",
|
||||
"runewaker:audit:environment-assembly": "node scripts/runewaker-pipeline/audit-environment-assembly.mjs && node --test scripts/runewaker-pipeline/environment-assembly.test.mjs",
|
||||
"runewaker:audit:navigation": "node scripts/runewaker-pipeline/analyze-navigation-gaps.mjs && vitest run src/game/runewakerObjectiveRouteAudit.test.ts",
|
||||
"assets:wailing:instance": "gltf-transform instance ../wow335a/dungeon-export-work/WailingCaverns/converted/wailing-caverns.visual.glb src/assets/game/dungeons/wailing-caverns/wailing-caverns-visual.glb --min 3",
|
||||
"assets:wailing:creatures:animated": "node scripts/prepareAnimatedCreatures.mjs",
|
||||
@@ -96,7 +104,7 @@
|
||||
"assets:performance:audit": "node scripts/asset-pipeline/audit-environment-performance.mjs",
|
||||
"balance:benchmark": "vite-node scripts/combat-benchmark/run.ts",
|
||||
"balance:smoke": "vite-node scripts/combat-benchmark/run.ts --class mage --role damage --scenario damage-single-burst --seeds 1 --duration-scale 0.2",
|
||||
"prebuild": "npm run loot:generate && npm run assets:ktx2:audit && npm run assets:performance:audit",
|
||||
"prebuild": "npm run loot:generate && npm run web:prepare && npm run arena:assets:validate && npm run assets:ktx2:audit && npm run assets:performance:audit",
|
||||
"build": "node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build",
|
||||
"build:android:web": "npm run prebuild && npm run android:prepare && node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode android",
|
||||
"android:sync": "npm run build:android:web && cap sync android",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 258876,
|
||||
"gridHeight": 564,
|
||||
"gridWidth": 459,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "ashamanes-fall",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
16.435361862182617,
|
||||
-2.676706314086914,
|
||||
4.506431579589844
|
||||
],
|
||||
[
|
||||
65.03536224365234,
|
||||
26.523292541503906,
|
||||
43.1064338684082
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 353,
|
||||
"sourceVertices": 1059,
|
||||
"triangles": 353,
|
||||
"vertices": 1059
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-6.364639759063721,
|
||||
-19.976707458496094,
|
||||
-59.093570709228516
|
||||
],
|
||||
[
|
||||
85.27696990966797,
|
||||
36.782005310058594,
|
||||
53.62468338012695
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 1,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 2783,
|
||||
"vertexCount": 8349
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/ashamanes-fall/navigation-collision.glb": "cdc39b903da6a6f9f21bd9e5e0b2ca61e76b09e13575b02349ca92d68815e5a8"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/ashamanes-fall/navigation-collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/ashamanes-fall/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"adtFiles": [
|
||||
"valsharaharena_20_24.adt",
|
||||
"valsharaharena_20_25.adt",
|
||||
"valsharaharena_20_26.adt",
|
||||
"valsharaharena_21_24.adt",
|
||||
"valsharaharena_21_25.adt",
|
||||
"valsharaharena_21_26.adt",
|
||||
"valsharaharena_22_24.adt",
|
||||
"valsharaharena_22_25.adt",
|
||||
"valsharaharena_22_26.adt"
|
||||
],
|
||||
"gameplayBounds": {
|
||||
"max": [
|
||||
58,
|
||||
16,
|
||||
36
|
||||
],
|
||||
"min": [
|
||||
14,
|
||||
-4,
|
||||
2
|
||||
]
|
||||
},
|
||||
"kind": "adt-placed-wmo-repair",
|
||||
"legacyCollision": "public/assets/game/manastorm/1785-valsharaharena/collision.glb",
|
||||
"placedWmos": [
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/VALSHARAH/7VS_CAVEMICRO01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RUINPIECE06.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_BRIDGE01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_BRIDGE01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_TOWER01_NIGHTMARE.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_BRIDGE01_NIGHTMARE.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RUINPIECE06.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_BRIDGESHORT.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/DUNGEON/PVP/CUSTOM_VALSHARAH_ARENA.WMO"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "ashamanes-fall",
|
||||
"title": "Ashamane's Fall",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"mapDirectory": "patch-WB2/world/maps/valsharaharena",
|
||||
"adtFiles": [
|
||||
"valsharaharena_20_24.adt",
|
||||
"valsharaharena_20_25.adt",
|
||||
"valsharaharena_20_26.adt",
|
||||
"valsharaharena_21_24.adt",
|
||||
"valsharaharena_21_25.adt",
|
||||
"valsharaharena_21_26.adt",
|
||||
"valsharaharena_22_24.adt",
|
||||
"valsharaharena_22_25.adt",
|
||||
"valsharaharena_22_26.adt"
|
||||
],
|
||||
"placementCount": 16,
|
||||
"placedWmos": [
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RETAINERCURVED02.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/VALSHARAH/7VS_CAVEMICRO01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RUINPIECE06.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_BRIDGE01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_BRIDGE01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_TOWER01_NIGHTMARE.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_DRUID_BRIDGE01_NIGHTMARE.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_RUINPIECE06.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/NIGHTELF/7NE_NIGHTELF_BRIDGESHORT.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/DUNGEON/PVP/CUSTOM_VALSHARAH_ARENA.WMO"
|
||||
]
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
6.0437164306640625,
|
||||
-37.49217987060547,
|
||||
30.740463256835938
|
||||
],
|
||||
"alignmentOffset": [
|
||||
34.174934,
|
||||
-32.347685,
|
||||
20.31486
|
||||
],
|
||||
"alignment": "primary WMO matched to the existing HealerMan stage coordinate frame"
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-289.133544921875,
|
||||
-32.347686767578125,
|
||||
-200.8459930419922
|
||||
],
|
||||
"max": [
|
||||
357.4833984375,
|
||||
316.2064514160156,
|
||||
241.47572326660156
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 18,
|
||||
"triangles": 82815,
|
||||
"collisionTriangles": 82815,
|
||||
"collisionChunks": 1
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 209,
|
||||
"vertices": 627,
|
||||
"sourceComponents": 20,
|
||||
"spawnDistances": [
|
||||
0.5941920148506428,
|
||||
0.0212655626862784
|
||||
],
|
||||
"rawTriangles": 353,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"file": "visual.glb",
|
||||
"bytes": 2611788,
|
||||
"sha256": "25e8a0d63d420559a6bc965dc2b930039d951de8cdc189d785a00edc3610b112"
|
||||
},
|
||||
{
|
||||
"file": "collision.glb",
|
||||
"bytes": 1805536,
|
||||
"sha256": "48f45c434687f07969a618e1251e05e8672061d16a1f3a88338dbc667e33e78e"
|
||||
},
|
||||
{
|
||||
"file": "navigation.glb",
|
||||
"bytes": 10936,
|
||||
"sha256": "ac69ef1e845f632576598a8b2a5d0bbfa8086b21ed5d7fd17af06d5a565a5248"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 2074478,
|
||||
"gridHeight": 994,
|
||||
"gridWidth": 2087,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "black-rook-hold-arena",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
-92.08255767822266,
|
||||
-16.14961814880371,
|
||||
-279.4311218261719
|
||||
],
|
||||
[
|
||||
321.9174499511719,
|
||||
37.25038528442383,
|
||||
-82.83113098144531
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 7170,
|
||||
"sourceVertices": 21510,
|
||||
"triangles": 7170,
|
||||
"vertices": 21510
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-93.08255767822266,
|
||||
-39.549617767333984,
|
||||
-280.0311279296875
|
||||
],
|
||||
[
|
||||
324.1624450683594,
|
||||
69.1881103515625,
|
||||
-81.26593780517578
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 1,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 49821,
|
||||
"vertexCount": 149463
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/black-rook-hold-arena/navigation-collision.glb": "285bdce7d5b5085eef7d25f82a5b6a5b380605a310c539e8e94984182c835749"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/black-rook-hold-arena/navigation-collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/black-rook-hold-arena/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"adtFiles": [
|
||||
"blackrookholdarena_28_28.adt",
|
||||
"blackrookholdarena_28_29.adt",
|
||||
"blackrookholdarena_28_30.adt",
|
||||
"blackrookholdarena_29_28.adt",
|
||||
"blackrookholdarena_29_29.adt",
|
||||
"blackrookholdarena_29_30.adt",
|
||||
"blackrookholdarena_30_28.adt",
|
||||
"blackrookholdarena_30_29.adt",
|
||||
"blackrookholdarena_30_30.adt"
|
||||
],
|
||||
"gameplayBounds": {
|
||||
"max": [
|
||||
312,
|
||||
30,
|
||||
-110
|
||||
],
|
||||
"min": [
|
||||
-82,
|
||||
-10,
|
||||
-242
|
||||
]
|
||||
},
|
||||
"kind": "adt-placed-wmo-repair",
|
||||
"legacyCollision": "public/assets/game/manastorm/1784-blackrookholdarena/collision.glb",
|
||||
"placedWmos": [
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/VALSHARAH/7VS_BLACKROOKHOLD_MICRO01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/PVP/BUILDINGS/BLACKROOKHOLD/7VS_BLACKROOKHOLD_ARENA.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/PVP/BUILDINGS/BLACKROOKHOLD/BLACKROOKHOLD_ARENA_PLATFORMTEST.WMO"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "black-rook-hold-arena",
|
||||
"title": "Black Rook Hold Arena",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"mapDirectory": "patch-WB1/world/maps/blackrookholdarena",
|
||||
"adtFiles": [
|
||||
"blackrookholdarena_28_28.adt",
|
||||
"blackrookholdarena_28_29.adt",
|
||||
"blackrookholdarena_28_30.adt",
|
||||
"blackrookholdarena_29_28.adt",
|
||||
"blackrookholdarena_29_29.adt",
|
||||
"blackrookholdarena_29_30.adt",
|
||||
"blackrookholdarena_30_28.adt",
|
||||
"blackrookholdarena_30_29.adt",
|
||||
"blackrookholdarena_30_30.adt"
|
||||
],
|
||||
"placementCount": 3,
|
||||
"placedWmos": [
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/BROKENISLES/VALSHARAH/7VS_BLACKROOKHOLD_MICRO01.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/PVP/BUILDINGS/BLACKROOKHOLD/7VS_BLACKROOKHOLD_ARENA.WMO",
|
||||
"../LadiksMPQEditor/patch-WC1/WORLD/WMO/PVP/BUILDINGS/BLACKROOKHOLD/BLACKROOKHOLD_ARENA_PLATFORMTEST.WMO"
|
||||
]
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
-117.73179244995117,
|
||||
-104.28124237060547,
|
||||
60.68927764892578
|
||||
],
|
||||
"alignmentOffset": [
|
||||
117.542488,
|
||||
-102.29949,
|
||||
-224.463489
|
||||
],
|
||||
"alignment": "primary WMO matched to the existing HealerMan stage coordinate frame"
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-110.3111343383789,
|
||||
-102.29949188232422,
|
||||
-519.4838256835938
|
||||
],
|
||||
"max": [
|
||||
345.3961181640625,
|
||||
410.11322021484375,
|
||||
70.55683898925781
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 20,
|
||||
"triangles": 298817,
|
||||
"collisionTriangles": 298817,
|
||||
"collisionChunks": 4
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 4948,
|
||||
"vertices": 14844,
|
||||
"sourceComponents": 805,
|
||||
"spawnDistances": [
|
||||
0.009180110009213026,
|
||||
0.01568599673120942
|
||||
],
|
||||
"rawTriangles": 7170,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"file": "visual.glb",
|
||||
"bytes": 9908648,
|
||||
"sha256": "622c0832eb5a3bca85837de418b02be6dc5dad54db3b05e69e740d93c870e319"
|
||||
},
|
||||
{
|
||||
"file": "collision.glb",
|
||||
"bytes": 2059780,
|
||||
"sha256": "27a698c59e08744bd7bf6cfe9585f643a28aea45850da85020e3b4bac45ce92f"
|
||||
},
|
||||
{
|
||||
"file": "collision-001.glb",
|
||||
"bytes": 2443032,
|
||||
"sha256": "914702646d8a6a657d677bd9f69b1b0aea13f4033952c143ff85d8932127a82f"
|
||||
},
|
||||
{
|
||||
"file": "collision-002.glb",
|
||||
"bytes": 2377504,
|
||||
"sha256": "33622cbbc5e82cebee74049a53cd9fbb4e1ca8196322697f751916a3dd9d30ec"
|
||||
},
|
||||
{
|
||||
"file": "collision-003.glb",
|
||||
"bytes": 916280,
|
||||
"sha256": "2f405e27efcf4e7b4d41d2286293dcbe700d854edf4d5aa276bf5b0523e9748a"
|
||||
},
|
||||
{
|
||||
"file": "navigation.glb",
|
||||
"bytes": 238420,
|
||||
"sha256": "e463a0220842a4793b2150e95f4fd704f575def288813c781792b021afe8d3f0"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 652881,
|
||||
"gridHeight": 849,
|
||||
"gridWidth": 769,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "blades-edge-arena",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
-75.85150146484375,
|
||||
6.099999904632568,
|
||||
-80.292236328125
|
||||
],
|
||||
[
|
||||
76.14849853515625,
|
||||
82.0999984741211,
|
||||
80.3077621459961
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 3932,
|
||||
"sourceVertices": 11796,
|
||||
"triangles": 3932,
|
||||
"vertices": 11796
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-76.85150146484375,
|
||||
0,
|
||||
-84.8922348022461
|
||||
],
|
||||
[
|
||||
76.85150146484375,
|
||||
82.12013244628906,
|
||||
84.8922348022461
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 1,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 7327,
|
||||
"vertexCount": 7086
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/blades-edge-arena/collision.glb": "96bb151cc43fcac887cee1e573878221e14d4690e023bc4cf7b5b40c2788333a"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/blades-edge-arena/collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/blades-edge-arena/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"files": [
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/AncientOrcArena/ancorc_PVPstadium.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/AncientOrcArena/ancorc_PVPstadium_000.wmo"
|
||||
],
|
||||
"kind": "direct-wmo-parser"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "blades-edge-arena",
|
||||
"title": "Blade's Edge Arena",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"root": "../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/AncientOrcArena/ancorc_PVPstadium.wmo",
|
||||
"groupCount": 1
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
-0.32387542724609375,
|
||||
-3.3863677978515625,
|
||||
-0.1770477294921875
|
||||
]
|
||||
},
|
||||
"sourceBounds": {
|
||||
"min": [
|
||||
-77.17537689208984,
|
||||
-3.3863677978515625,
|
||||
-85.06928253173828
|
||||
],
|
||||
"max": [
|
||||
76.52762603759766,
|
||||
78.7337646484375,
|
||||
84.7151870727539
|
||||
]
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-76.85150146484375,
|
||||
0,
|
||||
-84.8922348022461
|
||||
],
|
||||
"max": [
|
||||
76.85150146484375,
|
||||
82.12013244628906,
|
||||
84.8922348022461
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 1,
|
||||
"vertices": 7086,
|
||||
"triangles": 7327,
|
||||
"collisionTriangles": 7327,
|
||||
"triangleFlags": [
|
||||
{
|
||||
"group": "group-000",
|
||||
"counts": {
|
||||
"8": 680,
|
||||
"36": 616,
|
||||
"38": 137,
|
||||
"72": 2729,
|
||||
"100": 2922,
|
||||
"102": 243
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 465,
|
||||
"vertices": 1395,
|
||||
"sourceComponents": 134,
|
||||
"spawnDistances": [
|
||||
0.0026893801684426334,
|
||||
0.004853620635976984
|
||||
],
|
||||
"rawTriangles": 3932,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"role": "visual",
|
||||
"file": "visual.glb",
|
||||
"bytes": 259116,
|
||||
"sha256": "5dc8c6662bccf04892b059c819de91b1134cce6011a1f707c5ee9645491e5279"
|
||||
},
|
||||
{
|
||||
"role": "collision",
|
||||
"file": "collision.glb",
|
||||
"bytes": 173784,
|
||||
"sha256": "96bb151cc43fcac887cee1e573878221e14d4690e023bc4cf7b5b40c2788333a"
|
||||
},
|
||||
{
|
||||
"role": "navigation",
|
||||
"file": "navigation.glb",
|
||||
"bytes": 23228,
|
||||
"sha256": "d99822c69df3f6363c9fab1c6c67c70c3afa4e94ed0e66fa992adf8b6724cf2b"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 529759,
|
||||
"gridHeight": 713,
|
||||
"gridWidth": 743,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "dalaran-sewers",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
-73.60513305664062,
|
||||
0.30000001192092896,
|
||||
-69.85077667236328
|
||||
],
|
||||
[
|
||||
73.5948715209961,
|
||||
79,
|
||||
70.14923095703125
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 4054,
|
||||
"sourceVertices": 12162,
|
||||
"triangles": 4054,
|
||||
"vertices": 12162
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-74.20513153076172,
|
||||
0,
|
||||
-71.25077819824219
|
||||
],
|
||||
[
|
||||
74.20513153076172,
|
||||
92.40137481689453,
|
||||
71.25077819824219
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 2,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 15469,
|
||||
"vertexCount": 13094
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/dalaran-sewers/collision.glb": "88c889838b76365ee0541dcce567151419b3310a6dc1252934ffb997f31215bd"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/dalaran-sewers/collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/dalaran-sewers/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"files": [
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Dalaran/dalaran_sewer_arena.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Dalaran/dalaran_sewer_arena_000.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Dalaran/dalaran_sewer_arena_001.wmo"
|
||||
],
|
||||
"kind": "direct-wmo-parser"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "dalaran-sewers",
|
||||
"title": "Dalaran Sewers",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"root": "../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Dalaran/dalaran_sewer_arena.wmo",
|
||||
"groupCount": 2
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
-9.411468505859375,
|
||||
-16.876672744750977,
|
||||
10.746021270751953
|
||||
]
|
||||
},
|
||||
"sourceBounds": {
|
||||
"min": [
|
||||
-83.6166000366211,
|
||||
-16.876672744750977,
|
||||
-60.5047607421875
|
||||
],
|
||||
"max": [
|
||||
64.79366302490234,
|
||||
75.52470397949219,
|
||||
81.9968032836914
|
||||
]
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-74.20513153076172,
|
||||
0,
|
||||
-71.25077819824219
|
||||
],
|
||||
"max": [
|
||||
74.20513153076172,
|
||||
92.40137481689453,
|
||||
71.25077819824219
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 2,
|
||||
"vertices": 13094,
|
||||
"triangles": 15469,
|
||||
"collisionTriangles": 15469,
|
||||
"triangleFlags": [
|
||||
{
|
||||
"group": "group-000",
|
||||
"counts": {
|
||||
"8": 85,
|
||||
"32": 545,
|
||||
"36": 573,
|
||||
"72": 40,
|
||||
"96": 2835,
|
||||
"100": 98
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "group-001",
|
||||
"counts": {
|
||||
"8": 165,
|
||||
"10": 480,
|
||||
"32": 766,
|
||||
"36": 2362,
|
||||
"72": 136,
|
||||
"74": 416,
|
||||
"96": 2053,
|
||||
"100": 4915
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 458,
|
||||
"vertices": 1374,
|
||||
"sourceComponents": 104,
|
||||
"spawnDistances": [
|
||||
0.004929398584727517,
|
||||
0.0049300317713031425
|
||||
],
|
||||
"rawTriangles": 4054,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"role": "visual",
|
||||
"file": "visual.glb",
|
||||
"bytes": 501848,
|
||||
"sha256": "91b37f5df3c5c0d2ecad9e1096177abfe8eb36acc384df36a949ce246a0155ba"
|
||||
},
|
||||
{
|
||||
"role": "collision",
|
||||
"file": "collision.glb",
|
||||
"bytes": 344140,
|
||||
"sha256": "88c889838b76365ee0541dcce567151419b3310a6dc1252934ffb997f31215bd"
|
||||
},
|
||||
{
|
||||
"role": "navigation",
|
||||
"file": "navigation.glb",
|
||||
"bytes": 22896,
|
||||
"sha256": "2e1db39fe16bd3d4a747710e1da774d841bc9cd9ada446c666a1d95a4e1aafb2"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 605259,
|
||||
"gridHeight": 783,
|
||||
"gridWidth": 773,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "nagrand-arena",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
-75.41183471679688,
|
||||
1.2000000476837158,
|
||||
-74.84174346923828
|
||||
],
|
||||
[
|
||||
75.9881591796875,
|
||||
72.80000305175781,
|
||||
65.15826416015625
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 3536,
|
||||
"sourceVertices": 10608,
|
||||
"triangles": 3536,
|
||||
"vertices": 10608
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-77.21183776855469,
|
||||
0,
|
||||
-78.24174499511719
|
||||
],
|
||||
[
|
||||
77.21183776855469,
|
||||
82.11756134033203,
|
||||
78.24174499511719
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 1,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 7727,
|
||||
"vertexCount": 8197
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/nagrand-arena/collision.glb": "c5ec36344bc335aad5c63607a93d6cacb535d4055c2626fed7da250855f5fa74"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/nagrand-arena/collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/nagrand-arena/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"files": [
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/Dungeon/OL_OgreHuts/PVP_Ogre_Arena01.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/Dungeon/OL_OgreHuts/PVP_Ogre_Arena01_000.wmo"
|
||||
],
|
||||
"kind": "direct-wmo-parser"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "nagrand-arena",
|
||||
"title": "Nagrand Arena",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"root": "../LadiksMPQEditor/common-2.mpq/World/wmo/Dungeon/OL_OgreHuts/PVP_Ogre_Arena01.wmo",
|
||||
"groupCount": 1
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
49.19449806213379,
|
||||
-10.582855224609375,
|
||||
30.535707473754883
|
||||
]
|
||||
},
|
||||
"sourceBounds": {
|
||||
"min": [
|
||||
-28.017337799072266,
|
||||
-10.582855224609375,
|
||||
-47.70603561401367
|
||||
],
|
||||
"max": [
|
||||
126.40633392333984,
|
||||
71.53470611572266,
|
||||
108.77745056152344
|
||||
]
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-77.21183776855469,
|
||||
0,
|
||||
-78.24174499511719
|
||||
],
|
||||
"max": [
|
||||
77.21183776855469,
|
||||
82.11756134033203,
|
||||
78.24174499511719
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 1,
|
||||
"vertices": 8197,
|
||||
"triangles": 7727,
|
||||
"collisionTriangles": 7727,
|
||||
"triangleFlags": [
|
||||
{
|
||||
"group": "group-000",
|
||||
"counts": {
|
||||
"8": 783,
|
||||
"32": 35,
|
||||
"36": 831,
|
||||
"38": 2,
|
||||
"72": 2230,
|
||||
"96": 502,
|
||||
"100": 3291,
|
||||
"102": 53
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 1450,
|
||||
"vertices": 4350,
|
||||
"sourceComponents": 155,
|
||||
"spawnDistances": [
|
||||
0.0025342739142059146,
|
||||
0.0025336963502397706
|
||||
],
|
||||
"rawTriangles": 3536,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"role": "visual",
|
||||
"file": "visual.glb",
|
||||
"bytes": 290580,
|
||||
"sha256": "6730970440c31df26e4fe90a6eb3913e6402f0027a334d966fb2b57c2e7b3e7c"
|
||||
},
|
||||
{
|
||||
"role": "collision",
|
||||
"file": "collision.glb",
|
||||
"bytes": 191916,
|
||||
"sha256": "c5ec36344bc335aad5c63607a93d6cacb535d4055c2626fed7da250855f5fa74"
|
||||
},
|
||||
{
|
||||
"role": "navigation",
|
||||
"file": "navigation.glb",
|
||||
"bytes": 70512,
|
||||
"sha256": "cd9027c8d3cf175a8879efad1eb51584f3fbce1bc2eb9355959676471e06798a"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 2185272,
|
||||
"gridHeight": 1208,
|
||||
"gridWidth": 1809,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "ring-of-valor",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
-180.21835327148438,
|
||||
0.10000000149011612,
|
||||
-117.37078094482422
|
||||
],
|
||||
[
|
||||
180.181640625,
|
||||
100.80000305175781,
|
||||
120.02922058105469
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 4405,
|
||||
"sourceVertices": 13215,
|
||||
"triangles": 4405,
|
||||
"vertices": 13215
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-180.818359375,
|
||||
0,
|
||||
-120.77078247070312
|
||||
],
|
||||
[
|
||||
180.818359375,
|
||||
116.26216125488281,
|
||||
120.77078247070312
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 1,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 9713,
|
||||
"vertexCount": 12167
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/ring-of-valor/collision.glb": "25162e36ed6fea03d6de939cf4d742bc142fbebb2db1c9fbc6f2fcc9e15cffdf"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/ring-of-valor/collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/ring-of-valor/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"files": [
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Orgrimmar/OrgrimmarArena.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Orgrimmar/OrgrimmarArena_000.wmo"
|
||||
],
|
||||
"kind": "direct-wmo-parser"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "ring-of-valor",
|
||||
"title": "Ring of Valor",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"root": "../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Orgrimmar/OrgrimmarArena.wmo",
|
||||
"groupCount": 1
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
-0.44493865966796875,
|
||||
-32.01484298706055,
|
||||
0
|
||||
]
|
||||
},
|
||||
"sourceBounds": {
|
||||
"min": [
|
||||
-181.26329040527344,
|
||||
-32.01484298706055,
|
||||
-120.77078247070312
|
||||
],
|
||||
"max": [
|
||||
180.3734130859375,
|
||||
84.24732208251953,
|
||||
120.77078247070312
|
||||
]
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-180.818359375,
|
||||
0,
|
||||
-120.77078247070312
|
||||
],
|
||||
"max": [
|
||||
180.818359375,
|
||||
116.26216125488281,
|
||||
120.77078247070312
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 1,
|
||||
"vertices": 12167,
|
||||
"triangles": 9713,
|
||||
"collisionTriangles": 9713,
|
||||
"triangleFlags": [
|
||||
{
|
||||
"group": "group-000",
|
||||
"counts": {
|
||||
"8": 18,
|
||||
"32": 1063,
|
||||
"36": 590,
|
||||
"38": 586,
|
||||
"72": 126,
|
||||
"96": 1307,
|
||||
"100": 5028,
|
||||
"102": 995
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 702,
|
||||
"vertices": 2106,
|
||||
"sourceComponents": 61,
|
||||
"spawnDistances": [
|
||||
1.1157695927091633,
|
||||
1.258558140579426
|
||||
],
|
||||
"rawTriangles": 4405,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"role": "visual",
|
||||
"file": "visual.glb",
|
||||
"bytes": 409692,
|
||||
"sha256": "32d3dc93c8f5026d29f3e9bf2efb299145e8e191e358b201a62dc987e0fa3c46"
|
||||
},
|
||||
{
|
||||
"role": "collision",
|
||||
"file": "collision.glb",
|
||||
"bytes": 263388,
|
||||
"sha256": "25162e36ed6fea03d6de939cf4d742bc142fbebb2db1c9fbc6f2fcc9e15cffdf"
|
||||
},
|
||||
{
|
||||
"role": "navigation",
|
||||
"file": "navigation.glb",
|
||||
"bytes": 34608,
|
||||
"sha256": "db986ef21767aecf08edb435f11674a094996213685f83cd7d2172886949374d"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"build": {
|
||||
"attempts": [
|
||||
{
|
||||
"failures": [],
|
||||
"tileCount": 1,
|
||||
"tileSize": null,
|
||||
"valid": true
|
||||
}
|
||||
],
|
||||
"gridCells": 1226343,
|
||||
"gridHeight": 1213,
|
||||
"gridWidth": 1011,
|
||||
"ignoredEmptyTilePackingFailures": 0,
|
||||
"intermediateRetention": null,
|
||||
"mode": "solo",
|
||||
"partition": "watershed",
|
||||
"populatedTileCount": 1,
|
||||
"tileColumns": 1,
|
||||
"tileCount": 1,
|
||||
"tileRows": 1,
|
||||
"tileSize": null
|
||||
},
|
||||
"dungeonId": "ruins-of-lordaeron",
|
||||
"generator": "@recast-navigation/three@0.43.1",
|
||||
"geometry": {
|
||||
"bounds": [
|
||||
[
|
||||
-100.48125457763672,
|
||||
3.5,
|
||||
-119.45735168457031
|
||||
],
|
||||
[
|
||||
96.71875,
|
||||
77.70000457763672,
|
||||
120.54264831542969
|
||||
]
|
||||
],
|
||||
"boundsTolerance": 0.4,
|
||||
"rejectedOutOfBoundsTriangles": 0,
|
||||
"sourceTriangles": 7958,
|
||||
"sourceVertices": 23874,
|
||||
"triangles": 7958,
|
||||
"vertices": 23874
|
||||
},
|
||||
"input": {
|
||||
"bounds": [
|
||||
[
|
||||
-101.08125305175781,
|
||||
0,
|
||||
-121.25735473632812
|
||||
],
|
||||
[
|
||||
101.08125305175781,
|
||||
77.79415893554688,
|
||||
121.25735473632812
|
||||
]
|
||||
],
|
||||
"degenerateTriangleCount": 0,
|
||||
"invalidIndexCount": 0,
|
||||
"meshCount": 5,
|
||||
"nonFiniteCoordinateCount": 0,
|
||||
"triangleCount": 23642,
|
||||
"vertexCount": 26192
|
||||
},
|
||||
"inputChunkChecksums": {
|
||||
"public/assets/game/arenas/ruins-of-lordaeron/collision.glb": "a844092b66cbfcc8ae430879de3ce7a1e6441c2cfc69328dc2cb259e4cb6c645"
|
||||
},
|
||||
"inputChunks": [
|
||||
"public/assets/game/arenas/ruins-of-lordaeron/collision.glb"
|
||||
],
|
||||
"output": "public/assets/game/arenas/ruins-of-lordaeron/navigation-raw.glb",
|
||||
"schemaVersion": 1,
|
||||
"settings": {
|
||||
"agentHeight": 1.8,
|
||||
"agentMaxClimb": 0.5,
|
||||
"agentMaxSlope": 50,
|
||||
"agentRadius": 0.36,
|
||||
"cellHeight": 0.1,
|
||||
"cellSize": 0.2
|
||||
},
|
||||
"source": {
|
||||
"files": [
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena_000.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena_001.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena_002.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena_003.wmo",
|
||||
"../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena_004.wmo"
|
||||
],
|
||||
"kind": "direct-wmo-parser"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "ruins-of-lordaeron",
|
||||
"title": "Ruins of Lordaeron",
|
||||
"status": "generated-review-required",
|
||||
"source": {
|
||||
"root": "../LadiksMPQEditor/common-2.mpq/World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena.wmo",
|
||||
"groupCount": 5
|
||||
},
|
||||
"coordinateSystem": {
|
||||
"conversion": "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
"rebasedOrigin": [
|
||||
4.71783447265625,
|
||||
-31.21632957458496,
|
||||
15.956310272216797
|
||||
]
|
||||
},
|
||||
"sourceBounds": {
|
||||
"min": [
|
||||
-96.36341857910156,
|
||||
-31.21632957458496,
|
||||
-105.3010482788086
|
||||
],
|
||||
"max": [
|
||||
105.79908752441406,
|
||||
46.57782745361328,
|
||||
137.2136688232422
|
||||
]
|
||||
},
|
||||
"bounds": {
|
||||
"min": [
|
||||
-101.08125305175781,
|
||||
0,
|
||||
-121.25735473632812
|
||||
],
|
||||
"max": [
|
||||
101.08125305175781,
|
||||
77.79415893554688,
|
||||
121.25735473632812
|
||||
]
|
||||
},
|
||||
"geometry": {
|
||||
"groups": 5,
|
||||
"vertices": 26192,
|
||||
"triangles": 23642,
|
||||
"collisionTriangles": 23642,
|
||||
"triangleFlags": [
|
||||
{
|
||||
"group": "group-000",
|
||||
"counts": {
|
||||
"8": 1135,
|
||||
"36": 3730,
|
||||
"72": 730,
|
||||
"100": 10270
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "group-001",
|
||||
"counts": {
|
||||
"8": 1,
|
||||
"9": 2,
|
||||
"32": 106,
|
||||
"33": 22,
|
||||
"36": 21,
|
||||
"37": 14,
|
||||
"72": 275,
|
||||
"73": 47,
|
||||
"96": 225,
|
||||
"97": 236,
|
||||
"100": 2550,
|
||||
"101": 117
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "group-002",
|
||||
"counts": {
|
||||
"32": 21,
|
||||
"96": 132
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "group-003",
|
||||
"counts": {
|
||||
"8": 13,
|
||||
"9": 2,
|
||||
"32": 185,
|
||||
"33": 22,
|
||||
"36": 53,
|
||||
"37": 14,
|
||||
"72": 252,
|
||||
"73": 56,
|
||||
"96": 276,
|
||||
"97": 229,
|
||||
"100": 2579,
|
||||
"101": 174
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "group-004",
|
||||
"counts": {
|
||||
"36": 21,
|
||||
"100": 132
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"triangles": 2085,
|
||||
"vertices": 6255,
|
||||
"sourceComponents": 157,
|
||||
"spawnDistances": [
|
||||
0.00292914523132256,
|
||||
0.004205306248988376
|
||||
],
|
||||
"rawTriangles": 7958,
|
||||
"report": "recast-report.json"
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"role": "visual",
|
||||
"file": "visual.glb",
|
||||
"bytes": 916788,
|
||||
"sha256": "b00bcd87bd72bbabcf3e7e7edab99e8f7ddbe820b849a40df90510096e9f2f4e"
|
||||
},
|
||||
{
|
||||
"role": "collision",
|
||||
"file": "collision.glb",
|
||||
"bytes": 601048,
|
||||
"sha256": "a844092b66cbfcc8ae430879de3ce7a1e6441c2cfc69328dc2cb259e4cb6c645"
|
||||
},
|
||||
{
|
||||
"role": "navigation",
|
||||
"file": "navigation.glb",
|
||||
"bytes": 100996,
|
||||
"sha256": "be7aa6c4822f0baecafe4eb148a5952d3ce8001c7617f351e7ab257d61e7cb59"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "warnorken-arena",
|
||||
"title": "Warnorken Arena",
|
||||
"status": "validated",
|
||||
"source": {
|
||||
"kind": "validated-runewaker-package",
|
||||
"navigationSource": "navigation-source.glb"
|
||||
},
|
||||
"navigation": {
|
||||
"sourceComponents": 96,
|
||||
"triangles": 365,
|
||||
"vertices": 1095,
|
||||
"spawnDistances": [
|
||||
0.00402499431590021,
|
||||
0.004012153291366225
|
||||
]
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"file": "visual.glb",
|
||||
"bytes": 3752136,
|
||||
"triangles": 55200,
|
||||
"sha256": "c452a843c2e4123d4fe0d20da41558f5b5a1906548b3d091dc691ffc42ed19d9"
|
||||
},
|
||||
{
|
||||
"file": "collision.glb",
|
||||
"bytes": 3005760,
|
||||
"triangles": 60751,
|
||||
"sha256": "0b1ce7555da0d2900465e3539e1bf17ac244d23a0f5eb57a2106f29570757c5c"
|
||||
},
|
||||
{
|
||||
"file": "navigation.glb",
|
||||
"bytes": 18436,
|
||||
"triangles": 365,
|
||||
"sha256": "c6085ffc96b40b1f437b8bb7e111604c135a856b02fd9b6ccae20243183cf417"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,68 @@
|
||||
const CONTENT_CACHE_NAME = "healer-man-content-v1";
|
||||
const MANAGED_ASSET_PREFIXES = [
|
||||
"/assets/characters/",
|
||||
"/assets/creatures/",
|
||||
"/assets/equipment/",
|
||||
"/assets/game/",
|
||||
"/assets/ui/",
|
||||
];
|
||||
const BUNDLED_ASSET_PREFIXES = [
|
||||
"/assets/ui/",
|
||||
"/assets/equipment/",
|
||||
"/assets/characters/wow335a-v1/manifest.json",
|
||||
"/assets/characters/wow335a-v1/human/",
|
||||
"/assets/creatures/wailing-caverns/",
|
||||
"/assets/game/dungeons/wailing-caverns/",
|
||||
];
|
||||
const BUNDLED_ASSET_PATHS = new Set([
|
||||
"/assets/characters/wow335a-v1/textures/character/human/faciallowerhair00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/facialupperhair00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/female/humanfemalefacelower00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/female/humanfemalefaceupper00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/female/humanfemalenakedpelvisskin00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/female/humanfemalenakedtorsoskin00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/female/humanfemaleskin00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/hair00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/male/humanmalefacelower00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/male/humanmalefaceupper00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/male/humanmalenakedpelvisskin00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/scalplowerhair00_00.webp",
|
||||
"/assets/characters/wow335a-v1/textures/character/human/scalpupperhair00_00.webp",
|
||||
]);
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil((async () => {
|
||||
const names = await caches.keys();
|
||||
await Promise.all(names
|
||||
.filter((name) => name.startsWith(`${CONTENT_CACHE_NAME}-staging-`))
|
||||
.map((name) => caches.delete(name)));
|
||||
await self.clients.claim();
|
||||
})());
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
if (request.method !== "GET") return;
|
||||
const url = new URL(request.url);
|
||||
if (url.origin !== self.location.origin
|
||||
|| !MANAGED_ASSET_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) return;
|
||||
|
||||
event.respondWith((async () => {
|
||||
const cache = await caches.open(CONTENT_CACHE_NAME);
|
||||
const cached = await cache.match(request, { ignoreSearch: true });
|
||||
if (cached) return cached;
|
||||
if (BUNDLED_ASSET_PATHS.has(url.pathname)
|
||||
|| BUNDLED_ASSET_PREFIXES.some((prefix) => url.pathname.startsWith(prefix))) {
|
||||
return fetch(request);
|
||||
}
|
||||
return new Response("Optional content is not installed.", {
|
||||
status: 404,
|
||||
headers: { "content-type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
})());
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"versionName": "0.1.13",
|
||||
"versionCode": 1013
|
||||
"versionName": "0.1.21",
|
||||
"versionCode": 1021
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { access, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { bakeNavigationFromCollisionAssets } from "../dungeon-pipeline/recast-bake.mjs";
|
||||
import { extractNavigationComponent } from "./extract-navigation-component.mjs";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
import { Vector3 } from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import {
|
||||
geometryBounds,
|
||||
parseRootGroupCount,
|
||||
parseWmoGroup,
|
||||
rebaseWmoGroups,
|
||||
wmoChunks,
|
||||
writeWmoGeometry,
|
||||
} from "./build-wmo-arenas.mjs";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const extractedRoot = path.resolve(projectRoot, "../LadiksMPQEditor");
|
||||
const outputRoot = path.join(projectRoot, "public/assets/game/arenas");
|
||||
const sourceLayers = ["patch-WC1", "patch-WB2", "patch-WB1", "patch-2", "patch", "lichking", "common-2.mpq"];
|
||||
|
||||
const DEFINITIONS = Object.freeze([
|
||||
{
|
||||
slug: "black-rook-hold-arena",
|
||||
title: "Black Rook Hold Arena",
|
||||
mapDirectory: "patch-WB1/world/maps/blackrookholdarena",
|
||||
primaryPattern: /7vs_blackrookhold_arena\.wmo$/i,
|
||||
alignmentOffset: [117.542488, -102.29949, -224.463489],
|
||||
legacyCollision: "public/assets/game/manastorm/1784-blackrookholdarena/collision.glb",
|
||||
gameplayBounds: { min: [-82, -10, -242], max: [312, 30, -110] },
|
||||
legacyNavigation: "public/assets/game/manastorm/1784-blackrookholdarena/navigation.glb",
|
||||
teamSpawns: [[-66.098564, 8.416428, -201.852448], [123.158585, 9.916428, -219.909592]],
|
||||
},
|
||||
{
|
||||
slug: "ashamanes-fall",
|
||||
title: "Ashamane's Fall",
|
||||
mapDirectory: "patch-WB2/world/maps/valsharaharena",
|
||||
primaryPattern: /custom_valsharah_arena\.wmo$/i,
|
||||
alignmentOffset: [34.174934, -32.347685, 20.31486],
|
||||
legacyCollision: "public/assets/game/manastorm/1785-valsharaharena/collision.glb",
|
||||
gameplayBounds: { min: [14, -4, 2], max: [58, 16, 36] },
|
||||
teamSpawns: [[18.223099, -0.480263, 32.034554], [55.023094, -0.480263, 7.123436]],
|
||||
},
|
||||
]);
|
||||
|
||||
function strings(buffer) {
|
||||
return buffer.toString("utf8").split("\0").filter(Boolean);
|
||||
}
|
||||
|
||||
export function parseAdtPlacements(buffer) {
|
||||
const top = wmoChunks(buffer);
|
||||
const mwmo = top.get("MWMO");
|
||||
const mwid = top.get("MWID");
|
||||
const modf = top.get("MODF");
|
||||
if (!mwmo || !mwid || !modf) return [];
|
||||
const names = new Map();
|
||||
for (let offset = 0; offset < mwmo.length;) {
|
||||
const end = mwmo.indexOf(0, offset);
|
||||
if (end < 0) break;
|
||||
if (end > offset) names.set(offset, mwmo.toString("utf8", offset, end));
|
||||
offset = end + 1;
|
||||
}
|
||||
const offsets = [];
|
||||
for (let offset = 0; offset + 4 <= mwid.length; offset += 4) offsets.push(mwid.readUInt32LE(offset));
|
||||
const placements = [];
|
||||
for (let offset = 0; offset + 64 <= modf.length; offset += 64) {
|
||||
const nameIndex = modf.readUInt32LE(offset);
|
||||
placements.push({
|
||||
path: names.get(offsets[nameIndex]) ?? strings(mwmo)[nameIndex] ?? null,
|
||||
uniqueId: modf.readUInt32LE(offset + 4),
|
||||
position: [modf.readFloatLE(offset + 8), modf.readFloatLE(offset + 12), modf.readFloatLE(offset + 16)],
|
||||
rotation: [modf.readFloatLE(offset + 20), modf.readFloatLE(offset + 24), modf.readFloatLE(offset + 28)],
|
||||
scale: modf.readUInt16LE(offset + 62) / 1024 || 1,
|
||||
});
|
||||
}
|
||||
return placements.filter((placement) => placement.path);
|
||||
}
|
||||
|
||||
async function exists(file) {
|
||||
try { await access(file); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
async function resolveWmo(relativePath) {
|
||||
const normalized = relativePath.replace(/\\/g, "/");
|
||||
for (const layer of sourceLayers) {
|
||||
const file = path.join(extractedRoot, layer, ...normalized.split("/"));
|
||||
if (await exists(file)) return file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rotate([x, y, z], [xDegrees, yDegrees, zDegrees]) {
|
||||
const radians = [xDegrees, yDegrees, zDegrees].map((degrees) => degrees * Math.PI / 180);
|
||||
let [nextX, nextY, nextZ] = [x, y, z];
|
||||
let cosine = Math.cos(radians[0]); let sine = Math.sin(radians[0]);
|
||||
[nextY, nextZ] = [nextY * cosine - nextZ * sine, nextY * sine + nextZ * cosine];
|
||||
cosine = Math.cos(radians[1]); sine = Math.sin(radians[1]);
|
||||
[nextX, nextZ] = [nextX * cosine + nextZ * sine, -nextX * sine + nextZ * cosine];
|
||||
cosine = Math.cos(radians[2]); sine = Math.sin(radians[2]);
|
||||
[nextX, nextY] = [nextX * cosine - nextY * sine, nextX * sine + nextY * cosine];
|
||||
return [nextX, nextY, nextZ];
|
||||
}
|
||||
|
||||
function placementBase(position) {
|
||||
return [17066.6666666667 - position[2], 17066.6666666667 - position[0], position[1]];
|
||||
}
|
||||
|
||||
function placeGroup(group, placement, primaryBase) {
|
||||
const base = placementBase(placement.position);
|
||||
const positions = new Float32Array(group.positions.length);
|
||||
const normals = group.normals ? new Float32Array(group.normals.length) : null;
|
||||
for (let offset = 0; offset < group.positions.length; offset += 3) {
|
||||
// Undo the direct parser's Three-axis conversion before applying the MODF transform.
|
||||
const local = [-group.positions[offset], group.positions[offset + 2], group.positions[offset + 1]];
|
||||
const transformed = rotate(local.map((value) => value * placement.scale), placement.rotation);
|
||||
positions[offset] = -(base[0] + transformed[0] - primaryBase[0]);
|
||||
positions[offset + 1] = base[2] + transformed[2] - primaryBase[2];
|
||||
positions[offset + 2] = base[1] + transformed[1] - primaryBase[1];
|
||||
if (normals) {
|
||||
const localNormal = [-group.normals[offset], group.normals[offset + 2], group.normals[offset + 1]];
|
||||
const transformedNormal = rotate(localNormal, placement.rotation);
|
||||
normals[offset] = -transformedNormal[0];
|
||||
normals[offset + 1] = transformedNormal[2];
|
||||
normals[offset + 2] = transformedNormal[1];
|
||||
}
|
||||
}
|
||||
return { ...group, positions, normals };
|
||||
}
|
||||
|
||||
async function loadPlacementGroups(placement, placementIndex, primaryBase) {
|
||||
const rootFile = await resolveWmo(placement.path);
|
||||
if (!rootFile) throw new Error(`Missing placed WMO: ${placement.path}`);
|
||||
const root = await readFile(rootFile);
|
||||
const groupCount = parseRootGroupCount(root);
|
||||
const extension = path.extname(rootFile);
|
||||
const base = rootFile.slice(0, -extension.length);
|
||||
const groups = [];
|
||||
for (let groupIndex = 0; groupIndex < groupCount; groupIndex += 1) {
|
||||
const groupFile = `${base}_${String(groupIndex).padStart(3, "0")}${extension}`;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseWmoGroup(await readFile(groupFile), `placement-${placementIndex}-group-${groupIndex}`);
|
||||
} catch (error) {
|
||||
throw new Error(`${placement.path} group ${groupIndex}: ${error.message}`, { cause: error });
|
||||
}
|
||||
groups.push(placeGroup(parsed, placement, primaryBase));
|
||||
}
|
||||
return { rootFile, groups };
|
||||
}
|
||||
|
||||
function splitCollisionGroups(groups, triangleLimit = 90000) {
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
let currentTriangles = 0;
|
||||
for (const group of groups) {
|
||||
let indexOffset = 0;
|
||||
while (indexOffset < group.collisionIndices.length) {
|
||||
const availableTriangles = triangleLimit - currentTriangles;
|
||||
if (availableTriangles <= 0) {
|
||||
chunks.push(current);
|
||||
current = [];
|
||||
currentTriangles = 0;
|
||||
continue;
|
||||
}
|
||||
const takeIndices = Math.min(group.collisionIndices.length - indexOffset, availableTriangles * 3);
|
||||
current.push({ ...group, collisionIndices: group.collisionIndices.slice(indexOffset, indexOffset + takeIndices) });
|
||||
currentTriangles += takeIndices / 3;
|
||||
indexOffset += takeIndices;
|
||||
}
|
||||
}
|
||||
if (current.length) chunks.push(current);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async function hash(file) {
|
||||
return createHash("sha256").update(await readFile(file)).digest("hex");
|
||||
}
|
||||
|
||||
function triangleInsideGameplayBounds(points, bounds) {
|
||||
const centroid = [0, 1, 2].map((axis) => (points[0][axis] + points[1][axis] + points[2][axis]) / 3);
|
||||
return centroid[0] >= bounds.min[0] - 6 && centroid[0] <= bounds.max[0] + 6
|
||||
&& centroid[1] >= bounds.min[1] - 6 && centroid[1] <= bounds.max[1] + 16
|
||||
&& centroid[2] >= bounds.min[2] - 6 && centroid[2] <= bounds.max[2] + 6;
|
||||
}
|
||||
|
||||
async function croppedNavigationGroup(legacyCollisionFile, groups, bounds) {
|
||||
await MeshoptDecoder.ready;
|
||||
const bytes = await readFile(legacyCollisionFile);
|
||||
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const scene = await new Promise((resolve, reject) => new GLTFLoader()
|
||||
.setMeshoptDecoder(MeshoptDecoder)
|
||||
.parse(buffer, "", (gltf) => resolve(gltf.scene), reject));
|
||||
scene.updateMatrixWorld(true);
|
||||
const output = [];
|
||||
const a = new Vector3();
|
||||
const b = new Vector3();
|
||||
const c = new Vector3();
|
||||
scene.traverse((object) => {
|
||||
if (!object.isMesh || !object.geometry?.getAttribute("position")) return;
|
||||
const positions = object.geometry.getAttribute("position");
|
||||
const indices = object.geometry.getIndex();
|
||||
const count = indices?.count ?? positions.count;
|
||||
const vertexIndex = (element) => indices ? indices.getX(element) : element;
|
||||
for (let element = 0; element < count; element += 3) {
|
||||
const points = [a, b, c].map((point, corner) => point.fromBufferAttribute(positions, vertexIndex(element + corner)).applyMatrix4(object.matrixWorld).toArray());
|
||||
if (triangleInsideGameplayBounds(points, bounds)) output.push(...points.flat());
|
||||
}
|
||||
});
|
||||
for (const group of groups) {
|
||||
for (let offset = 0; offset < group.collisionIndices.length; offset += 3) {
|
||||
const points = [0, 1, 2].map((corner) => {
|
||||
const index = group.collisionIndices[offset + corner] * 3;
|
||||
return [group.positions[index], group.positions[index + 1], group.positions[index + 2]];
|
||||
});
|
||||
if (triangleInsideGameplayBounds(points, bounds)) output.push(...points.flat());
|
||||
}
|
||||
}
|
||||
const positions = new Float32Array(output);
|
||||
const indices = Uint32Array.from({ length: positions.length / 3 }, (_, index) => index);
|
||||
return { name: "rated-gameplay-collision", positions, normals: null, indices, collisionIndices: indices, flagCounts: {} };
|
||||
}
|
||||
|
||||
async function build(definition) {
|
||||
const mapRoot = path.join(extractedRoot, ...definition.mapDirectory.split("/"));
|
||||
const adtFiles = (await readdir(mapRoot)).filter((name) => name.toLowerCase().endsWith(".adt")).sort();
|
||||
const placementsById = new Map();
|
||||
for (const adt of adtFiles) {
|
||||
for (const placement of parseAdtPlacements(await readFile(path.join(mapRoot, adt)))) placementsById.set(placement.uniqueId, placement);
|
||||
}
|
||||
const placements = [...placementsById.values()];
|
||||
const primary = placements.find((placement) => definition.primaryPattern.test(placement.path));
|
||||
if (!primary) throw new Error(`${definition.slug}: primary arena WMO placement was not found.`);
|
||||
const primaryBase = placementBase(primary.position);
|
||||
const groups = [];
|
||||
const sources = [];
|
||||
for (const [index, placement] of placements.entries()) {
|
||||
const loaded = await loadPlacementGroups(placement, index, primaryBase);
|
||||
groups.push(...loaded.groups);
|
||||
sources.push(path.relative(projectRoot, loaded.rootFile).replace(/\\/g, "/"));
|
||||
}
|
||||
const transform = rebaseWmoGroups(groups);
|
||||
for (const group of groups) {
|
||||
for (let offset = 0; offset < group.positions.length; offset += 3) {
|
||||
group.positions[offset] += definition.alignmentOffset[0];
|
||||
group.positions[offset + 1] += definition.alignmentOffset[1];
|
||||
group.positions[offset + 2] += definition.alignmentOffset[2];
|
||||
}
|
||||
}
|
||||
const alignedBounds = geometryBounds(groups);
|
||||
const outputDirectory = path.join(outputRoot, definition.slug);
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
const visualFile = path.join(outputDirectory, "visual.glb");
|
||||
await writeWmoGeometry(visualFile, groups, true);
|
||||
const collisionChunks = splitCollisionGroups(groups);
|
||||
const collisionFiles = [];
|
||||
for (const [index, chunk] of collisionChunks.entries()) {
|
||||
const file = path.join(outputDirectory, index === 0 ? "collision.glb" : `collision-${String(index).padStart(3, "0")}.glb`);
|
||||
await writeWmoGeometry(file, chunk, false);
|
||||
collisionFiles.push(file);
|
||||
}
|
||||
const navigationFile = path.join(outputDirectory, "navigation.glb");
|
||||
const rawNavigationFile = path.join(outputDirectory, "navigation-raw.glb");
|
||||
const legacyCollisionFile = path.join(projectRoot, ...definition.legacyCollision.split("/"));
|
||||
const navigationCollisionFile = path.join(outputDirectory, "navigation-collision.glb");
|
||||
const navigationGroup = await croppedNavigationGroup(legacyCollisionFile, groups, definition.gameplayBounds);
|
||||
await writeWmoGeometry(navigationCollisionFile, [navigationGroup], false);
|
||||
const navigation = await bakeNavigationFromCollisionAssets({
|
||||
slug: definition.slug,
|
||||
collisionAssets: [{ id: "rated-gameplay-collision", role: "collision", path: path.relative(projectRoot, navigationCollisionFile).replace(/\\/g, "/") }],
|
||||
settings: { cellSize: 0.2, cellHeight: 0.1, agentHeight: 1.8, agentRadius: 0.36, agentMaxClimb: 0.5, agentMaxSlope: 50 },
|
||||
output: rawNavigationFile,
|
||||
reportFile: path.join(outputDirectory, "recast-report.json"),
|
||||
source: { kind: "adt-placed-wmo-repair", adtFiles, placedWmos: sources, legacyCollision: definition.legacyCollision, gameplayBounds: definition.gameplayBounds },
|
||||
});
|
||||
const selectionSource = definition.legacyNavigation
|
||||
? path.join(projectRoot, ...definition.legacyNavigation.split("/"))
|
||||
: rawNavigationFile;
|
||||
const connectedNavigation = await extractNavigationComponent({
|
||||
source: selectionSource,
|
||||
output: navigationFile,
|
||||
teamSpawns: definition.teamSpawns,
|
||||
});
|
||||
await Promise.all([unlink(rawNavigationFile), unlink(navigationCollisionFile)]);
|
||||
const files = [visualFile, ...collisionFiles, navigationFile];
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
id: definition.slug,
|
||||
title: definition.title,
|
||||
status: "generated-review-required",
|
||||
source: { mapDirectory: definition.mapDirectory, adtFiles, placementCount: placements.length, placedWmos: sources },
|
||||
coordinateSystem: {
|
||||
conversion: "three(x,y,z)=(-wow.x,wow.z,wow.y)",
|
||||
rebasedOrigin: transform.origin,
|
||||
alignmentOffset: definition.alignmentOffset,
|
||||
alignment: "primary WMO matched to the existing HealerMan stage coordinate frame",
|
||||
},
|
||||
bounds: alignedBounds,
|
||||
geometry: {
|
||||
groups: groups.length,
|
||||
triangles: groups.reduce((sum, group) => sum + group.indices.length / 3, 0),
|
||||
collisionTriangles: groups.reduce((sum, group) => sum + group.collisionIndices.length / 3, 0),
|
||||
collisionChunks: collisionFiles.length,
|
||||
},
|
||||
navigation: {
|
||||
triangles: connectedNavigation.triangles,
|
||||
vertices: connectedNavigation.vertices,
|
||||
sourceComponents: connectedNavigation.sourceComponents,
|
||||
spawnDistances: connectedNavigation.spawnDistances,
|
||||
rawTriangles: navigation.geometry.triangles,
|
||||
report: "recast-report.json",
|
||||
},
|
||||
assets: await Promise.all(files.map(async (file) => ({ file: path.basename(file), bytes: (await readFile(file)).length, sha256: await hash(file) }))),
|
||||
};
|
||||
await writeFile(path.join(outputDirectory, "stage-pack.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(`${definition.slug}: ${placements.length} placed WMOs, ${report.geometry.triangles} triangles, ${report.navigation.triangles} nav triangles`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
|
||||
const requested = new Set(process.argv.slice(2));
|
||||
for (const definition of DEFINITIONS.filter((entry) => !requested.size || requested.has(entry.slug))) await build(definition);
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { Document, NodeIO } from "@gltf-transform/core";
|
||||
import { bakeNavigationFromCollisionAssets } from "../dungeon-pipeline/recast-bake.mjs";
|
||||
import { extractNavigationComponent } from "./extract-navigation-component.mjs";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const sourceRoot = path.resolve(projectRoot, "../LadiksMPQEditor/common-2.mpq");
|
||||
const outputRoot = path.join(projectRoot, "public/assets/game/arenas");
|
||||
|
||||
const ARENAS = Object.freeze([
|
||||
{ slug: "nagrand-arena", title: "Nagrand Arena", root: "World/wmo/Dungeon/OL_OgreHuts/PVP_Ogre_Arena01.wmo", teamSpawns: [[-43.41, 13.4, -42.04], [42.03, 14.5, -44.44]] },
|
||||
{ slug: "blades-edge-arena", title: "Blade's Edge Arena", root: "World/wmo/PvP/Buildings/AncientOrcArena/ancorc_PVPstadium.wmo", teamSpawns: [[29.2, 50.7, -51.49], [31.13, 54.4, 49.51]] },
|
||||
{ slug: "ruins-of-lordaeron", title: "Ruins of Lordaeron", root: "World/wmo/PvP/Buildings/Lordaeron/PVP_Lordaeron_Arena.wmo", teamSpawns: [[-8.68, 30.1, -73.96], [-5.08, 27.6, 57.28]] },
|
||||
{ slug: "dalaran-sewers", title: "Dalaran Sewers", root: "World/wmo/PvP/Buildings/Dalaran/dalaran_sewer_arena.wmo", teamSpawns: [[-12.41, 20.1, -63.05], [21.79, 20.2, 25.15]] },
|
||||
{ slug: "ring-of-valor", title: "Ring of Valor", root: "World/wmo/PvP/Buildings/Orgrimmar/OrgrimmarArena.wmo", teamSpawns: [[0, 27, 25], [0, 27, -25]] },
|
||||
]);
|
||||
|
||||
function tag(buffer, offset) {
|
||||
return buffer.toString("ascii", offset, offset + 4).split("").reverse().join("");
|
||||
}
|
||||
|
||||
export function wmoChunks(buffer, start = 0, end = buffer.length) {
|
||||
const result = new Map();
|
||||
let offset = start;
|
||||
while (offset + 8 <= end) {
|
||||
const id = tag(buffer, offset);
|
||||
const size = buffer.readUInt32LE(offset + 4);
|
||||
const payloadStart = offset + 8;
|
||||
let payloadEnd = payloadStart + size;
|
||||
// Some Legion-era custom group files report the original uncompressed MOGP
|
||||
// container length even though trailing optional chunks were stripped.
|
||||
// MOGP is the terminal top-level container, so the physical EOF is safe.
|
||||
if (id === "MOGP" && payloadEnd > end) payloadEnd = end;
|
||||
if (payloadEnd > end) throw new Error(`Invalid ${id} chunk at ${offset}: ${size} bytes exceeds the WMO boundary.`);
|
||||
result.set(id, buffer.subarray(payloadStart, payloadEnd));
|
||||
offset = payloadEnd;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseRootGroupCount(buffer) {
|
||||
const root = wmoChunks(buffer);
|
||||
const header = root.get("MOHD");
|
||||
if (!header || header.length < 8) throw new Error("WMO root is missing MOHD.");
|
||||
return header.readUInt32LE(4);
|
||||
}
|
||||
|
||||
export function parseWmoGroup(buffer, name) {
|
||||
const top = wmoChunks(buffer);
|
||||
const group = top.get("MOGP");
|
||||
if (!group || group.length <= 68) throw new Error(`${name}: missing MOGP geometry.`);
|
||||
const nested = wmoChunks(group, 68, group.length);
|
||||
const vertexBytes = nested.get("MOVT");
|
||||
const normalBytes = nested.get("MONR");
|
||||
const indexBytes = nested.get("MOVI");
|
||||
const materialBytes = nested.get("MOPY");
|
||||
if (!vertexBytes || !indexBytes) throw new Error(`${name}: missing MOVT or MOVI.`);
|
||||
const positions = new Float32Array(vertexBytes.length / 4);
|
||||
for (let offset = 0; offset < vertexBytes.length; offset += 4) positions[offset / 4] = vertexBytes.readFloatLE(offset);
|
||||
const sourceNormals = normalBytes ? new Float32Array(normalBytes.length / 4) : null;
|
||||
if (normalBytes) for (let offset = 0; offset < normalBytes.length; offset += 4) sourceNormals[offset / 4] = normalBytes.readFloatLE(offset);
|
||||
const indices = new Uint32Array(indexBytes.length / 2);
|
||||
for (let offset = 0; offset < indexBytes.length; offset += 2) indices[offset / 2] = indexBytes.readUInt16LE(offset);
|
||||
const transformed = new Float32Array(positions.length);
|
||||
const normals = sourceNormals?.length === positions.length ? new Float32Array(positions.length) : null;
|
||||
for (let index = 0; index < positions.length; index += 3) {
|
||||
transformed[index] = -positions[index];
|
||||
transformed[index + 1] = positions[index + 2];
|
||||
transformed[index + 2] = positions[index + 1];
|
||||
if (normals) {
|
||||
normals[index] = -sourceNormals[index];
|
||||
normals[index + 1] = sourceNormals[index + 2];
|
||||
normals[index + 2] = sourceNormals[index + 1];
|
||||
}
|
||||
}
|
||||
const flagCounts = {};
|
||||
const collisionIndexValues = [];
|
||||
if (materialBytes) {
|
||||
for (let offset = 0; offset < materialBytes.length; offset += 2) {
|
||||
const flag = materialBytes[offset];
|
||||
flagCounts[flag] = (flagCounts[flag] ?? 0) + 1;
|
||||
if (flag & 0x04) {
|
||||
const triangleOffset = (offset / 2) * 3;
|
||||
collisionIndexValues.push(indices[triangleOffset], indices[triangleOffset + 1], indices[triangleOffset + 2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name, positions: transformed, normals, indices, flagCounts,
|
||||
// Retain every structural WMO triangle for authoritative collision. MOPY
|
||||
// bit meanings vary by client generation and several arena floors use flag
|
||||
// combinations that would otherwise be omitted; Recast filters slopes.
|
||||
collisionIndices: indices,
|
||||
flaggedCollisionIndices: materialBytes ? new Uint32Array(collisionIndexValues) : indices,
|
||||
};
|
||||
}
|
||||
|
||||
export function geometryBounds(groups) {
|
||||
const min = [Infinity, Infinity, Infinity];
|
||||
const max = [-Infinity, -Infinity, -Infinity];
|
||||
for (const group of groups) {
|
||||
for (let offset = 0; offset < group.positions.length; offset += 3) {
|
||||
for (let axis = 0; axis < 3; axis += 1) {
|
||||
min[axis] = Math.min(min[axis], group.positions[offset + axis]);
|
||||
max[axis] = Math.max(max[axis], group.positions[offset + axis]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
export function rebaseWmoGroups(groups) {
|
||||
const sourceBounds = geometryBounds(groups);
|
||||
const center = [
|
||||
(sourceBounds.min[0] + sourceBounds.max[0]) / 2,
|
||||
sourceBounds.min[1],
|
||||
(sourceBounds.min[2] + sourceBounds.max[2]) / 2,
|
||||
];
|
||||
for (const group of groups) {
|
||||
for (let offset = 0; offset < group.positions.length; offset += 3) {
|
||||
group.positions[offset] -= center[0];
|
||||
group.positions[offset + 1] -= center[1];
|
||||
group.positions[offset + 2] -= center[2];
|
||||
}
|
||||
}
|
||||
return { sourceBounds, origin: center, bounds: geometryBounds(groups) };
|
||||
}
|
||||
|
||||
export async function writeWmoGeometry(file, groups, visual) {
|
||||
const document = new Document();
|
||||
const buffer = document.createBuffer("arena-buffer");
|
||||
const scene = document.createScene("arena");
|
||||
const colors = [
|
||||
[0.37, 0.32, 0.22, 1], [0.29, 0.34, 0.3, 1], [0.42, 0.4, 0.34, 1],
|
||||
[0.31, 0.27, 0.24, 1], [0.34, 0.37, 0.42, 1],
|
||||
];
|
||||
for (const [index, group] of groups.entries()) {
|
||||
const groupIndices = visual ? group.indices : group.collisionIndices;
|
||||
if (!groupIndices.length) continue;
|
||||
const position = document.createAccessor(`${group.name}-position`, buffer).setType("VEC3").setArray(group.positions);
|
||||
const indices = document.createAccessor(`${group.name}-indices`, buffer).setType("SCALAR").setArray(groupIndices);
|
||||
const primitive = document.createPrimitive().setAttribute("POSITION", position).setIndices(indices);
|
||||
if (visual && group.normals) {
|
||||
primitive.setAttribute("NORMAL", document.createAccessor(`${group.name}-normal`, buffer).setType("VEC3").setArray(group.normals));
|
||||
}
|
||||
if (visual) {
|
||||
primitive.setMaterial(document.createMaterial(`${group.name}-stone`)
|
||||
.setBaseColorFactor(colors[index % colors.length])
|
||||
.setRoughnessFactor(0.84)
|
||||
.setMetallicFactor(0.02)
|
||||
.setDoubleSided(true));
|
||||
}
|
||||
const mesh = document.createMesh(group.name).addPrimitive(primitive);
|
||||
scene.addChild(document.createNode(group.name).setMesh(mesh));
|
||||
}
|
||||
await new NodeIO().write(file, document);
|
||||
}
|
||||
|
||||
async function sha256(file) {
|
||||
const buffer = await readFile(file);
|
||||
return createHash("sha256").update(buffer).digest("hex");
|
||||
}
|
||||
|
||||
async function buildArena(definition) {
|
||||
const rootFile = path.join(sourceRoot, ...definition.root.split("/"));
|
||||
const root = await readFile(rootFile);
|
||||
const groupCount = parseRootGroupCount(root);
|
||||
const extension = path.extname(rootFile);
|
||||
const base = rootFile.slice(0, -extension.length);
|
||||
const groups = [];
|
||||
const sourceFiles = [rootFile];
|
||||
for (let index = 0; index < groupCount; index += 1) {
|
||||
const groupFile = `${base}_${String(index).padStart(3, "0")}${extension}`;
|
||||
const data = await readFile(groupFile);
|
||||
sourceFiles.push(groupFile);
|
||||
groups.push(parseWmoGroup(data, `group-${String(index).padStart(3, "0")}`));
|
||||
}
|
||||
const transform = rebaseWmoGroups(groups);
|
||||
const outputDirectory = path.join(outputRoot, definition.slug);
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
const visualFile = path.join(outputDirectory, "visual.glb");
|
||||
const collisionFile = path.join(outputDirectory, "collision.glb");
|
||||
const navigationFile = path.join(outputDirectory, "navigation.glb");
|
||||
const rawNavigationFile = path.join(outputDirectory, "navigation-raw.glb");
|
||||
const recastReport = path.join(outputDirectory, "recast-report.json");
|
||||
await writeWmoGeometry(visualFile, groups, true);
|
||||
await writeWmoGeometry(collisionFile, groups, false);
|
||||
const triangles = groups.reduce((sum, group) => sum + group.indices.length / 3, 0);
|
||||
const collisionTriangles = groups.reduce((sum, group) => sum + group.collisionIndices.length / 3, 0);
|
||||
if (collisionTriangles > 100000) throw new Error(`${definition.slug}: ${collisionTriangles} collision triangles exceed the rated limit.`);
|
||||
const relativeCollision = path.relative(projectRoot, collisionFile).replace(/\\/g, "/");
|
||||
const navigation = await bakeNavigationFromCollisionAssets({
|
||||
slug: definition.slug,
|
||||
collisionAssets: [{ id: "collision", role: "collision", path: relativeCollision }],
|
||||
settings: { cellSize: 0.2, cellHeight: 0.1, agentHeight: 1.8, agentRadius: 0.36, agentMaxClimb: 0.5, agentMaxSlope: 50 },
|
||||
output: rawNavigationFile,
|
||||
reportFile: recastReport,
|
||||
source: { kind: "direct-wmo-parser", files: sourceFiles.map((file) => path.relative(projectRoot, file).replace(/\\/g, "/")) },
|
||||
});
|
||||
const connectedNavigation = await extractNavigationComponent({
|
||||
source: rawNavigationFile,
|
||||
output: navigationFile,
|
||||
teamSpawns: definition.teamSpawns,
|
||||
});
|
||||
await unlink(rawNavigationFile);
|
||||
const assets = await Promise.all([
|
||||
["visual", visualFile], ["collision", collisionFile], ["navigation", navigationFile],
|
||||
].map(async ([role, file]) => ({ role, file: path.basename(file), bytes: (await readFile(file)).length, sha256: await sha256(file) })));
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
id: definition.slug,
|
||||
title: definition.title,
|
||||
status: "generated-review-required",
|
||||
source: { root: path.relative(projectRoot, rootFile).replace(/\\/g, "/"), groupCount },
|
||||
coordinateSystem: { conversion: "three(x,y,z)=(-wow.x,wow.z,wow.y)", rebasedOrigin: transform.origin },
|
||||
sourceBounds: transform.sourceBounds,
|
||||
bounds: transform.bounds,
|
||||
geometry: {
|
||||
groups: groups.length,
|
||||
vertices: groups.reduce((sum, group) => sum + group.positions.length / 3, 0),
|
||||
triangles,
|
||||
collisionTriangles,
|
||||
triangleFlags: groups.map((group) => ({ group: group.name, counts: group.flagCounts })),
|
||||
},
|
||||
navigation: {
|
||||
triangles: connectedNavigation.triangles,
|
||||
vertices: connectedNavigation.vertices,
|
||||
sourceComponents: connectedNavigation.sourceComponents,
|
||||
spawnDistances: connectedNavigation.spawnDistances,
|
||||
rawTriangles: navigation.geometry.triangles,
|
||||
report: "recast-report.json",
|
||||
},
|
||||
assets,
|
||||
};
|
||||
await writeFile(path.join(outputDirectory, "stage-pack.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
return report;
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
|
||||
const requested = new Set(process.argv.slice(2));
|
||||
const selected = requested.size ? ARENAS.filter((arena) => requested.has(arena.slug)) : ARENAS;
|
||||
if (!selected.length) throw new Error(`No matching arena. Choose: ${ARENAS.map((arena) => arena.slug).join(", ")}`);
|
||||
for (const arena of selected) {
|
||||
const report = await buildArena(arena);
|
||||
console.log(`${arena.slug}: ${report.geometry.triangles} triangles, ${report.navigation.triangles} navigation triangles`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
import { Vector3 } from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { writeWmoGeometry } from "./build-wmo-arenas.mjs";
|
||||
|
||||
globalThis.self = globalThis;
|
||||
globalThis.ProgressEvent ??= class ProgressEvent {
|
||||
constructor(type, values = {}) { this.type = type; Object.assign(this, values); }
|
||||
};
|
||||
|
||||
class UnionFind {
|
||||
constructor(size) { this.parent = Array.from({ length: size }, (_, index) => index); }
|
||||
find(value) {
|
||||
if (this.parent[value] !== value) this.parent[value] = this.find(this.parent[value]);
|
||||
return this.parent[value];
|
||||
}
|
||||
union(left, right) {
|
||||
const a = this.find(left);
|
||||
const b = this.find(right);
|
||||
if (a !== b) this.parent[b] = a;
|
||||
}
|
||||
}
|
||||
|
||||
function key(point) {
|
||||
return point.map((value) => Math.round(value * 1000)).join(":");
|
||||
}
|
||||
|
||||
async function loadTriangles(file) {
|
||||
await MeshoptDecoder.ready;
|
||||
const bytes = await readFile(file);
|
||||
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const scene = await new Promise((resolve, reject) => new GLTFLoader()
|
||||
.setMeshoptDecoder(MeshoptDecoder)
|
||||
.parse(buffer, "", (gltf) => resolve(gltf.scene), reject));
|
||||
scene.updateMatrixWorld(true);
|
||||
const triangles = [];
|
||||
const points = [new Vector3(), new Vector3(), new Vector3()];
|
||||
scene.traverse((object) => {
|
||||
if (!object.isMesh || !object.geometry?.getAttribute("position")) return;
|
||||
const positions = object.geometry.getAttribute("position");
|
||||
const indices = object.geometry.getIndex();
|
||||
const count = indices?.count ?? positions.count;
|
||||
const vertexIndex = (element) => indices ? indices.getX(element) : element;
|
||||
for (let element = 0; element < count; element += 3) {
|
||||
triangles.push(points.map((point, corner) => [
|
||||
...point.fromBufferAttribute(positions, vertexIndex(element + corner)).applyMatrix4(object.matrixWorld).toArray(),
|
||||
]));
|
||||
}
|
||||
});
|
||||
return triangles;
|
||||
}
|
||||
|
||||
function distanceSquared(point, target) {
|
||||
return (point[0] - target[0]) ** 2 + (point[1] - target[1]) ** 2 + (point[2] - target[2]) ** 2;
|
||||
}
|
||||
|
||||
export async function extractNavigationComponent({ source, output, teamSpawns }) {
|
||||
const triangles = await loadTriangles(source);
|
||||
const union = new UnionFind(triangles.length);
|
||||
const ownerByVertex = new Map();
|
||||
for (const [triangleIndex, triangle] of triangles.entries()) {
|
||||
for (const point of triangle) {
|
||||
const positionKey = key(point);
|
||||
const owner = ownerByVertex.get(positionKey);
|
||||
if (owner == null) ownerByVertex.set(positionKey, triangleIndex);
|
||||
else union.union(owner, triangleIndex);
|
||||
}
|
||||
}
|
||||
const components = new Map();
|
||||
for (let index = 0; index < triangles.length; index += 1) {
|
||||
const root = union.find(index);
|
||||
const component = components.get(root) ?? [];
|
||||
component.push(triangles[index]);
|
||||
components.set(root, component);
|
||||
}
|
||||
const scored = [...components.values()].map((component) => {
|
||||
const vertices = component.flat();
|
||||
const spawnDistances = teamSpawns.map((spawn) => Math.sqrt(Math.min(...vertices.map((point) => distanceSquared(point, spawn)))));
|
||||
return { component, spawnDistances, score: Math.max(...spawnDistances) };
|
||||
}).sort((left, right) => left.score - right.score || right.component.length - left.component.length);
|
||||
const selected = scored[0];
|
||||
if (!selected?.component.length) throw new Error(`${source}: no navigation component could be selected.`);
|
||||
const values = selected.component.flat(2);
|
||||
const positions = new Float32Array(values);
|
||||
const indices = Uint32Array.from({ length: positions.length / 3 }, (_, index) => index);
|
||||
await writeWmoGeometry(output, [{
|
||||
name: "rated-connected-navigation",
|
||||
positions,
|
||||
normals: null,
|
||||
indices,
|
||||
collisionIndices: indices,
|
||||
flagCounts: {},
|
||||
}], false);
|
||||
return {
|
||||
sourceComponents: components.size,
|
||||
triangles: selected.component.length,
|
||||
vertices: positions.length / 3,
|
||||
spawnDistances: selected.spawnDistances,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const catalog = JSON.parse(await readFile(path.join(projectRoot, "shared/arena-stages.json"), "utf8"));
|
||||
const io = new NodeIO();
|
||||
const requested = new Set(process.argv.slice(2).filter((value) => !value.startsWith("--")));
|
||||
const detail = process.argv.includes("--all");
|
||||
|
||||
class UnionFind {
|
||||
constructor(size) { this.parent = Array.from({ length: size }, (_, index) => index); }
|
||||
find(value) { if (this.parent[value] !== value) this.parent[value] = this.find(this.parent[value]); return this.parent[value]; }
|
||||
union(left, right) { const a = this.find(left); const b = this.find(right); if (a !== b) this.parent[b] = a; }
|
||||
}
|
||||
|
||||
function round(values) { return values.map((value) => Math.round(value * 100) / 100); }
|
||||
|
||||
for (const stage of catalog.stages) {
|
||||
if (requested.size && !requested.has(stage.id)) continue;
|
||||
const base = path.join(projectRoot, "public", ...stage.assetBaseUrl.split("/").filter(Boolean));
|
||||
let document;
|
||||
try { document = await io.read(path.join(base, "navigation.glb")); } catch { continue; }
|
||||
const vertices = [];
|
||||
const triangles = [];
|
||||
const byPosition = new Map();
|
||||
for (const mesh of document.getRoot().listMeshes()) {
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
const positions = primitive.getAttribute("POSITION")?.getArray();
|
||||
if (!positions) continue;
|
||||
const indices = primitive.getIndices()?.getArray() ?? Uint32Array.from({ length: positions.length / 3 }, (_, index) => index);
|
||||
for (let offset = 0; offset < indices.length; offset += 3) {
|
||||
const triangle = [];
|
||||
for (let corner = 0; corner < 3; corner += 1) {
|
||||
const index = indices[offset + corner];
|
||||
const vertex = [positions[index * 3], positions[index * 3 + 1], positions[index * 3 + 2]];
|
||||
const key = vertex.map((value) => Math.round(value * 1000)).join(":");
|
||||
if (!byPosition.has(key)) {
|
||||
byPosition.set(key, vertices.length);
|
||||
vertices.push(vertex);
|
||||
}
|
||||
triangle.push(byPosition.get(key));
|
||||
}
|
||||
triangles.push(triangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
const union = new UnionFind(vertices.length);
|
||||
for (const triangle of triangles) {
|
||||
union.union(triangle[0], triangle[1]);
|
||||
union.union(triangle[0], triangle[2]);
|
||||
}
|
||||
const grouped = new Map();
|
||||
for (let index = 0; index < vertices.length; index += 1) {
|
||||
const root = union.find(index);
|
||||
const component = grouped.get(root) ?? [];
|
||||
component.push(vertices[index]);
|
||||
grouped.set(root, component);
|
||||
}
|
||||
const components = [...grouped.values()];
|
||||
components.sort((left, right) => right.length - left.length);
|
||||
const main = components[0] ?? [];
|
||||
const min = [0, 1, 2].map((axis) => Math.min(...main.map((vertex) => vertex[axis])));
|
||||
const max = [0, 1, 2].map((axis) => Math.max(...main.map((vertex) => vertex[axis])));
|
||||
const center = min.map((value, axis) => (value + max[axis]) / 2);
|
||||
const axis = max[0] - min[0] >= max[2] - min[2] ? 0 : 2;
|
||||
const sorted = [...main].sort((left, right) => left[axis] - right[axis]);
|
||||
const inset = Math.max(1, Math.floor(sorted.length * 0.08));
|
||||
const teamSpawns = [sorted[inset] ?? sorted[0], sorted[sorted.length - inset - 1] ?? sorted.at(-1)];
|
||||
console.log(JSON.stringify({
|
||||
id: stage.id,
|
||||
componentCount: components.length,
|
||||
mainVertices: main.length,
|
||||
nextVertices: components[1]?.length ?? 0,
|
||||
mainBounds: { min: round(min), max: round(max) },
|
||||
center: round(center),
|
||||
suggestedTeamSpawns: teamSpawns.map(round),
|
||||
topComponents: components.slice(0, detail ? components.length : 8).map((component) => ({
|
||||
vertices: component.length,
|
||||
min: round([0, 1, 2].map((componentAxis) => Math.min(...component.map((vertex) => vertex[componentAxis])))),
|
||||
max: round([0, 1, 2].map((componentAxis) => Math.max(...component.map((vertex) => vertex[componentAxis])))),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
import { extractNavigationComponent } from "./extract-navigation-component.mjs";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const directory = path.join(projectRoot, "public/assets/game/arenas/warnorken-arena");
|
||||
const source = path.join(directory, "navigation-source.glb");
|
||||
const output = path.join(directory, "navigation.glb");
|
||||
const teamSpawns = [[-42.7, -21.75, 14.12], [41.3, -21.65, -6.58]];
|
||||
const connected = await extractNavigationComponent({ source, output, teamSpawns });
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({ "meshopt.decoder": MeshoptDecoder });
|
||||
const assets = [];
|
||||
for (const name of ["visual", "collision", "navigation"]) {
|
||||
const file = path.join(directory, `${name}.glb`);
|
||||
const bytes = await readFile(file);
|
||||
const document = await io.read(file);
|
||||
const triangles = document.getRoot().listMeshes().reduce((sum, mesh) => sum + mesh.listPrimitives().reduce((primitiveSum, primitive) => primitiveSum + (primitive.getIndices()?.getCount() ?? primitive.getAttribute("POSITION")?.getCount() ?? 0) / 3, 0), 0);
|
||||
assets.push({ file: `${name}.glb`, bytes: (await stat(file)).size, triangles: Math.round(triangles), sha256: createHash("sha256").update(bytes).digest("hex") });
|
||||
}
|
||||
await writeFile(path.join(directory, "stage-pack.json"), `${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
id: "warnorken-arena",
|
||||
title: "Warnorken Arena",
|
||||
status: "validated",
|
||||
source: { kind: "validated-runewaker-package", navigationSource: "navigation-source.glb" },
|
||||
navigation: connected,
|
||||
assets,
|
||||
}, null, 2)}\n`);
|
||||
console.log(`warnorken-arena: ${connected.triangles} connected navigation triangles`);
|
||||
@@ -0,0 +1,118 @@
|
||||
import { access, readFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const catalog = JSON.parse(await readFile(path.join(projectRoot, "shared/arena-stages.json"), "utf8"));
|
||||
const failures = [];
|
||||
const reports = [];
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({ "meshopt.decoder": MeshoptDecoder });
|
||||
|
||||
class UnionFind {
|
||||
constructor(size) { this.parent = Array.from({ length: size }, (_, index) => index); }
|
||||
find(value) {
|
||||
if (this.parent[value] !== value) this.parent[value] = this.find(this.parent[value]);
|
||||
return this.parent[value];
|
||||
}
|
||||
union(left, right) {
|
||||
const a = this.find(left);
|
||||
const b = this.find(right);
|
||||
if (a !== b) this.parent[b] = a;
|
||||
}
|
||||
}
|
||||
|
||||
function inspectConnectedNavigation(document, spawns) {
|
||||
const vertices = [];
|
||||
const triangles = [];
|
||||
const byPosition = new Map();
|
||||
for (const mesh of document.getRoot().listMeshes()) {
|
||||
for (const primitive of mesh.listPrimitives()) {
|
||||
const positions = primitive.getAttribute("POSITION")?.getArray();
|
||||
if (!positions) continue;
|
||||
const indices = primitive.getIndices()?.getArray() ?? Uint32Array.from({ length: positions.length / 3 }, (_, index) => index);
|
||||
for (let offset = 0; offset < indices.length; offset += 3) {
|
||||
const triangle = [];
|
||||
for (let corner = 0; corner < 3; corner += 1) {
|
||||
const index = indices[offset + corner];
|
||||
const point = [positions[index * 3], positions[index * 3 + 1], positions[index * 3 + 2]];
|
||||
const key = point.map((value) => Math.round(value * 1000)).join(":");
|
||||
if (!byPosition.has(key)) {
|
||||
byPosition.set(key, vertices.length);
|
||||
vertices.push(point);
|
||||
}
|
||||
triangle.push(byPosition.get(key));
|
||||
}
|
||||
triangles.push(triangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
const union = new UnionFind(vertices.length);
|
||||
for (const triangle of triangles) {
|
||||
union.union(triangle[0], triangle[1]);
|
||||
union.union(triangle[0], triangle[2]);
|
||||
}
|
||||
const components = new Set(vertices.map((_, index) => union.find(index)));
|
||||
const spawnDistances = spawns.map((spawn) => Math.sqrt(Math.min(...vertices.map((point) => (
|
||||
(point[0] - spawn[0]) ** 2 + (point[1] - spawn[1]) ** 2 + (point[2] - spawn[2]) ** 2
|
||||
)))));
|
||||
return { components: components.size, vertices: vertices.length, triangles: triangles.length, spawnDistances };
|
||||
}
|
||||
|
||||
function validateVec3(value, label, stage) {
|
||||
if (!Array.isArray(value) || value.length !== 3 || value.some((entry) => !Number.isFinite(entry))) failures.push(`${stage.id}: invalid ${label}.`);
|
||||
}
|
||||
|
||||
for (const stage of catalog.stages) {
|
||||
validateVec3(stage.bounds?.min, "minimum bounds", stage);
|
||||
validateVec3(stage.bounds?.max, "maximum bounds", stage);
|
||||
for (const [index, spawn] of (stage.teamSpawns ?? []).entries()) validateVec3(spawn, `team spawn ${index}`, stage);
|
||||
if (stage.teamSpawns?.length !== 2) failures.push(`${stage.id}: two team spawns are required.`);
|
||||
if (stage.shadowSight?.length !== 2) failures.push(`${stage.id}: two Shadow Sight locations are required.`);
|
||||
for (const [index, spawn] of (stage.teamSpawns ?? []).entries()) {
|
||||
if (spawn.some((value, axis) => value < stage.bounds.min[axis] || value > stage.bounds.max[axis])) failures.push(`${stage.id}: team spawn ${index} is outside the sealed gameplay bounds.`);
|
||||
}
|
||||
for (const object of stage.lineOfSight ?? []) {
|
||||
validateVec3(object.position, `${object.id} LOS position`, stage);
|
||||
validateVec3(object.size, `${object.id} LOS size`, stage);
|
||||
if (object.position[0] < stage.bounds.min[0] || object.position[0] > stage.bounds.max[0]
|
||||
|| object.position[2] < stage.bounds.min[2] || object.position[2] > stage.bounds.max[2]) failures.push(`${stage.id}: ${object.id} LOS geometry is outside the gameplay bounds.`);
|
||||
}
|
||||
const dynamicIds = new Set((stage.dynamicObjects ?? []).map((object) => object.id));
|
||||
for (const object of stage.lineOfSight ?? []) {
|
||||
if (object.dynamicObjectId && !dynamicIds.has(object.dynamicObjectId)) failures.push(`${stage.id}: ${object.id} references a missing dynamic object.`);
|
||||
}
|
||||
const base = path.join(projectRoot, "public", ...stage.assetBaseUrl.split("/").filter(Boolean));
|
||||
const assets = {};
|
||||
for (const name of ["visual", "collision", "navigation"]) {
|
||||
const file = path.join(base, `${name}.glb`);
|
||||
try {
|
||||
await access(file);
|
||||
const info = await stat(file);
|
||||
const document = await io.read(file);
|
||||
const triangles = document.getRoot().listMeshes().reduce((sum, mesh) => sum + mesh.listPrimitives().reduce((primitiveSum, primitive) => primitiveSum + (primitive.getIndices()?.getCount() ?? primitive.getAttribute("POSITION")?.getCount() ?? 0) / 3, 0), 0);
|
||||
assets[name] = { bytes: info.size, triangles: Math.round(triangles) };
|
||||
if (name === "navigation") {
|
||||
const navigation = inspectConnectedNavigation(document, stage.teamSpawns);
|
||||
assets[name].components = navigation.components;
|
||||
assets[name].spawnDistances = navigation.spawnDistances.map((distance) => Math.round(distance * 1000) / 1000);
|
||||
if (navigation.components !== 1) failures.push(`${stage.id}: rated navigation must contain one connected gameplay component.`);
|
||||
for (const [index, distance] of navigation.spawnDistances.entries()) {
|
||||
if (distance > 3) failures.push(`${stage.id}: team spawn ${index} is ${distance.toFixed(2)}m from the navigation surface.`);
|
||||
}
|
||||
}
|
||||
if (!triangles) failures.push(`${stage.id}: ${name}.glb has no triangles.`);
|
||||
} catch {
|
||||
if (stage.validation.status === "validated") failures.push(`${stage.id}: required ${name}.glb is missing or unreadable.`);
|
||||
}
|
||||
}
|
||||
if (assets.visual?.bytes > 25 * 1024 * 1024) failures.push(`${stage.id}: visual asset exceeds 25 MiB.`);
|
||||
if (stage.validation.status === "validated" && ["traversal", "collision", "spawnSafety", "lineOfSight", "performance"].some((key) => stage.validation[key] !== true)) {
|
||||
failures.push(`${stage.id}: validated stages must carry every validation flag.`);
|
||||
}
|
||||
reports.push({ id: stage.id, validation: stage.validation, assets });
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ schemaVersion: 1, arenaCount: reports.length, failures, stages: reports }, null, 2));
|
||||
if (failures.length) process.exitCode = 1;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
BASIC_HUMAN_TEXTURE_PATHS,
|
||||
PROJECT_ROOT,
|
||||
createContentManifest,
|
||||
scanContentSources,
|
||||
@@ -12,12 +13,29 @@ if (path.dirname(outputDirectory) !== PROJECT_ROOT || path.basename(outputDirect
|
||||
}
|
||||
|
||||
const sources = [
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "basis"), logicalPrefix: "basis" },
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "assets", "ui"), logicalPrefix: "assets/ui" },
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "assets", "equipment"), logicalPrefix: "assets/equipment" },
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "characters", "wow335a-v1", "manifest.json"),
|
||||
logicalPrefix: "assets/characters/wow335a-v1/manifest.json",
|
||||
},
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "characters", "wow335a-v1", "human"),
|
||||
logicalPrefix: "assets/characters/wow335a-v1/human",
|
||||
},
|
||||
...BASIC_HUMAN_TEXTURE_PATHS.map((logicalPath) => ({
|
||||
directory: path.join(PROJECT_ROOT, "public", ...logicalPath.split("/")),
|
||||
logicalPrefix: logicalPath,
|
||||
})),
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "creatures", "wailing-caverns"),
|
||||
logicalPrefix: "assets/creatures/wailing-caverns",
|
||||
},
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "game", "arenas"),
|
||||
logicalPrefix: "assets/game/arenas",
|
||||
},
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "src", "assets", "game", "dungeons", "wailing-caverns"),
|
||||
logicalPrefix: "assets/game/dungeons/wailing-caverns",
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
BASIC_HUMAN_TEXTURE_PATHS,
|
||||
PROJECT_ROOT,
|
||||
createContentManifest,
|
||||
scanContentSources,
|
||||
} from "./publish-content.mjs";
|
||||
|
||||
const outputDirectory = path.join(PROJECT_ROOT, ".web-public");
|
||||
if (path.dirname(outputDirectory) !== PROJECT_ROOT || path.basename(outputDirectory) !== ".web-public") {
|
||||
throw new Error(`Refusing to replace unexpected browser staging directory: ${outputDirectory}`);
|
||||
}
|
||||
|
||||
const sources = [
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "basis"), logicalPrefix: "basis" },
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "data"), logicalPrefix: "data" },
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "assets", "ui"), logicalPrefix: "assets/ui" },
|
||||
{ directory: path.join(PROJECT_ROOT, "public", "assets", "equipment"), logicalPrefix: "assets/equipment" },
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "characters", "wow335a-v1", "manifest.json"),
|
||||
logicalPrefix: "assets/characters/wow335a-v1/manifest.json",
|
||||
},
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "characters", "wow335a-v1", "human"),
|
||||
logicalPrefix: "assets/characters/wow335a-v1/human",
|
||||
},
|
||||
...BASIC_HUMAN_TEXTURE_PATHS.map((logicalPath) => ({
|
||||
directory: path.join(PROJECT_ROOT, "public", ...logicalPath.split("/")),
|
||||
logicalPrefix: logicalPath,
|
||||
})),
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "creatures", "wailing-caverns"),
|
||||
logicalPrefix: "assets/creatures/wailing-caverns",
|
||||
},
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets", "game", "arenas"),
|
||||
logicalPrefix: "assets/game/arenas",
|
||||
},
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "src", "assets", "game", "dungeons", "wailing-caverns"),
|
||||
logicalPrefix: "assets/game/dungeons/wailing-caverns",
|
||||
},
|
||||
];
|
||||
|
||||
await rm(outputDirectory, { recursive: true, force: true });
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
|
||||
const files = await scanContentSources(sources);
|
||||
for (const file of files) {
|
||||
const destination = path.join(outputDirectory, ...file.logicalPath.split("/"));
|
||||
await mkdir(path.dirname(destination), { recursive: true });
|
||||
await copyFile(file.sourcePath, destination);
|
||||
}
|
||||
|
||||
await copyFile(
|
||||
path.join(PROJECT_ROOT, "public", "content-cache-sw.js"),
|
||||
path.join(outputDirectory, "content-cache-sw.js"),
|
||||
);
|
||||
|
||||
const generated = createContentManifest(files, { baseUrl: "/" });
|
||||
const bootstrapManifest = {
|
||||
...generated,
|
||||
files: Object.fromEntries(Object.entries(generated.files).map(([logicalPath, entry]) => [
|
||||
logicalPath,
|
||||
{ ...entry, url: logicalPath },
|
||||
])),
|
||||
};
|
||||
const manifestPath = path.join(outputDirectory, "content", "bootstrap-manifest.json");
|
||||
await mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
await writeFile(manifestPath, `${JSON.stringify(bootstrapManifest, null, 2)}\n`, "utf8");
|
||||
|
||||
const bytes = files.reduce((total, file) => total + file.bytes, 0);
|
||||
console.log(`Prepared browser bootstrap content: ${files.length} files (${bytes} bytes)`);
|
||||
console.log(`Bootstrap version: ${bootstrapManifest.version}`);
|
||||
@@ -18,14 +18,35 @@ export const DEFAULT_MINIMUM_APP_VERSION = "0.1.0";
|
||||
export const BUNDLED_ANDROID_PACKS = Object.freeze([
|
||||
"core:ui",
|
||||
"core:equipment",
|
||||
"core:character-basic",
|
||||
"core:runtime",
|
||||
"core:arenas",
|
||||
"dungeon:wailing-caverns",
|
||||
]);
|
||||
|
||||
const RUNTIME_EXTENSIONS = new Set([
|
||||
".avif", ".glb", ".json", ".ktx2", ".mp3", ".ogg", ".png",
|
||||
".svg", ".wav", ".webm", ".webp",
|
||||
".avif", ".glb", ".js", ".json", ".ktx2", ".mp3", ".ogg", ".png",
|
||||
".svg", ".wasm", ".wav", ".webm", ".webp",
|
||||
]);
|
||||
|
||||
export const BASIC_HUMAN_TEXTURE_PATHS = Object.freeze([
|
||||
"assets/characters/wow335a-v1/textures/character/human/faciallowerhair00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/facialupperhair00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/female/humanfemalefacelower00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/female/humanfemalefaceupper00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/female/humanfemalenakedpelvisskin00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/female/humanfemalenakedtorsoskin00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/female/humanfemaleskin00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/hair00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/male/humanmalefacelower00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/male/humanmalefaceupper00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/male/humanmalenakedpelvisskin00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/scalplowerhair00_00.webp",
|
||||
"assets/characters/wow335a-v1/textures/character/human/scalpupperhair00_00.webp",
|
||||
]);
|
||||
const BASIC_HUMAN_TEXTURE_PATH_SET = new Set(BASIC_HUMAN_TEXTURE_PATHS);
|
||||
|
||||
export const DEFAULT_CONTENT_SOURCES = Object.freeze([
|
||||
{
|
||||
directory: path.join(PROJECT_ROOT, "public", "assets"),
|
||||
@@ -49,18 +70,34 @@ function packLabel(packId) {
|
||||
const [category, id = category] = packId.split(":", 2);
|
||||
const name = id.split("-").map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join(" ");
|
||||
if (category === "dungeon") return name;
|
||||
if (category === "characters") return `${name} Characters`;
|
||||
if (packId === "characters:wow335a-v1") return "Extra Wrath Character Models & Skins";
|
||||
if (packId === "characters:ascension-v1") return "Ascension Character Models & Skins";
|
||||
if (packId === "characters:runewaker-v1") return "RuneWaker Character Models & Skins";
|
||||
if (category === "characters") return `${name} Character Models & Skins`;
|
||||
if (category === "manastorm") return `${name} Manastorm`;
|
||||
if (packId === "shared:creatures") return "Shared Creatures";
|
||||
if (packId === "core:ui") return "Interface";
|
||||
if (packId === "core:equipment") return "Starter Equipment";
|
||||
if (packId === "core:character-basic") return "Basic Human Character Model";
|
||||
if (packId === "core:runtime") return "Game Runtime";
|
||||
if (packId === "core:arenas") return "Rated Arenas";
|
||||
if (packId === "core:combat-media") return "Original Spell Effects & Sounds";
|
||||
return name;
|
||||
}
|
||||
|
||||
export function contentPackForPath(logicalPath) {
|
||||
const segments = normalizedRelativePath(logicalPath).split("/");
|
||||
const normalizedPath = normalizedRelativePath(logicalPath);
|
||||
const segments = normalizedPath.split("/");
|
||||
if (segments[0] === "basis" || segments[0] === "data") return "core:runtime";
|
||||
if (segments[0] !== "assets") return "core:misc";
|
||||
if (segments[1] === "characters" && segments[2]) return `characters:${segments[2]}`;
|
||||
if (segments[1] === "characters" && segments[2]) {
|
||||
const basicWrathHuman = segments[2] === "wow335a-v1" && (
|
||||
segments[3] === "manifest.json"
|
||||
|| segments[3] === "human"
|
||||
|| BASIC_HUMAN_TEXTURE_PATH_SET.has(normalizedPath)
|
||||
);
|
||||
return basicWrathHuman ? "core:character-basic" : `characters:${segments[2]}`;
|
||||
}
|
||||
if (segments[1] === "creatures" && segments[2]) {
|
||||
return segments[2] === "shared" ? "shared:creatures" : `dungeon:${segments[2]}`;
|
||||
}
|
||||
@@ -70,15 +107,21 @@ export function contentPackForPath(logicalPath) {
|
||||
if (segments[1] === "game" && segments[2] === "dungeons" && segments[3]) {
|
||||
return `dungeon:${segments[3]}`;
|
||||
}
|
||||
if (segments[1] === "game" && segments[2] === "arenas") return "core:arenas";
|
||||
if (segments[1] === "game" && segments[2] === "manastorm" && segments[3]) {
|
||||
return `manastorm:${segments[3]}`;
|
||||
}
|
||||
if (segments[1] === "game" && segments[2] === "combat-effects") return "core:combat-media";
|
||||
if (segments[1] === "ui") return "core:ui";
|
||||
if (segments[1] === "equipment") return "core:equipment";
|
||||
return "core:misc";
|
||||
}
|
||||
|
||||
async function walkFiles(directory) {
|
||||
const rootStats = await stat(directory);
|
||||
if (rootStats.isFile()) {
|
||||
return RUNTIME_EXTENSIONS.has(path.extname(directory).toLowerCase()) ? [directory] : [];
|
||||
}
|
||||
const files = [];
|
||||
async function visit(current) {
|
||||
const entries = await readdir(current, { withFileTypes: true });
|
||||
@@ -111,7 +154,9 @@ export async function scanContentSources(sources = DEFAULT_CONTENT_SOURCES) {
|
||||
const files = await walkFiles(sourceDirectory);
|
||||
for (const sourcePath of files) {
|
||||
const relative = normalizedRelativePath(path.relative(sourceDirectory, sourcePath));
|
||||
const logicalPath = normalizedRelativePath(`${source.logicalPrefix}/${relative}`);
|
||||
const logicalPath = normalizedRelativePath(relative
|
||||
? `${source.logicalPrefix}/${relative}`
|
||||
: source.logicalPrefix);
|
||||
const fileStats = await stat(sourcePath);
|
||||
const sha256 = await sha256File(sourcePath);
|
||||
const prior = discovered.get(logicalPath);
|
||||
@@ -170,11 +215,17 @@ export function createContentManifest(files, options = {}) {
|
||||
|
||||
if (packs.has("shared:creatures")) {
|
||||
for (const pack of packs.values()) {
|
||||
if (pack.id.startsWith("dungeon:") || pack.id.startsWith("manastorm:")) {
|
||||
if ((pack.id.startsWith("dungeon:") && pack.id !== "dungeon:wailing-caverns")
|
||||
|| pack.id.startsWith("manastorm:")) {
|
||||
pack.dependencies = ["shared:creatures"];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (packs.has("core:character-basic")) {
|
||||
for (const pack of packs.values()) {
|
||||
if (pack.id.startsWith("characters:")) pack.dependencies = ["core:character-basic"];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: CONTENT_SCHEMA_VERSION,
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import {
|
||||
BUNDLED_ANDROID_PACKS,
|
||||
contentPackForPath,
|
||||
createContentManifest,
|
||||
publishContent,
|
||||
@@ -13,8 +14,21 @@ import {
|
||||
test("assigns stable player-facing packs from logical asset paths", () => {
|
||||
assert.equal(contentPackForPath("assets/game/dungeons/wailing-caverns/visual.glb"), "dungeon:wailing-caverns");
|
||||
assert.equal(contentPackForPath("assets/creatures/wailing-caverns/mutanus.glb"), "dungeon:wailing-caverns");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/manifest.json"), "characters:wow335a-v1");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/manifest.json"), "core:character-basic");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/human/male/rig.glb"), "core:character-basic");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_00.webp"), "core:character-basic");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_01.webp"), "characters:wow335a-v1");
|
||||
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(contentPackForPath("assets/game/combat-effects/audio/smite.wav"), "core:combat-media");
|
||||
assert.equal(
|
||||
contentPackForPath("assets/game/combat-effects/audio/manastorm/Manastorm_BossKill_GenericPortalsReady.wav"),
|
||||
"core:combat-media",
|
||||
);
|
||||
assert.equal(BUNDLED_ANDROID_PACKS.includes("core:combat-media"), false);
|
||||
assert.equal(contentPackForPath("basis/basis_transcoder.wasm"), "core:runtime");
|
||||
});
|
||||
|
||||
test("publishes objects before a deterministic latest manifest", async () => {
|
||||
|
||||
@@ -116,10 +116,12 @@ function reportMarkdown(report) {
|
||||
dungeon.proceduralModels, "|", dungeon.status, "|",
|
||||
].join(" "));
|
||||
return [
|
||||
"# WoW dungeon content report",
|
||||
"# WoW dungeon combat and animation report",
|
||||
"",
|
||||
"Status: **" + report.status + "**",
|
||||
"",
|
||||
"Scope: executable combat populations and creature animation bindings. This report does not certify instance scripts, doors, interactables, escorts, waves, vehicles, or navigation.",
|
||||
"",
|
||||
"Coverage: " + report.summary.dungeons + " dungeons, " + report.summary.mobs
|
||||
+ " mobs, " + report.summary.bosses + " bosses, " + report.summary.attacks
|
||||
+ " executable attacks, and " + report.summary.specialAttacks + " special attacks/mechanics.",
|
||||
@@ -177,6 +179,10 @@ for (const dungeon of definitions) {
|
||||
let specialAttacks = 0;
|
||||
let nativeModels = 0;
|
||||
let proceduralModels = 0;
|
||||
const optionalBossEntityIds = new Set((dungeon.objectiveOrder ?? [])
|
||||
.filter((objective) => objective.optional)
|
||||
.map((objective) => dungeon.staticSpawns?.find((spawn) => spawn.id === objective.id)?.entityId)
|
||||
.filter(Boolean));
|
||||
|
||||
if (!entities.length) dungeonIssues.push("has no combat entities.");
|
||||
if (!bosses.length) dungeonIssues.push("has no bosses.");
|
||||
@@ -203,7 +209,7 @@ for (const dungeon of definitions) {
|
||||
attacks += entityAttacks.length;
|
||||
const specials = entityAttacks.filter((attack) => attack.id !== "basic-attack");
|
||||
specialAttacks += specials.length;
|
||||
if (entity.kind === "boss" && !specials.length) {
|
||||
if (entity.kind === "boss" && !specials.length && !optionalBossEntityIds.has(entityId)) {
|
||||
const exceptionKey = dungeon.id + ":" + entityId;
|
||||
if (!basicOnlyBossExceptions.has(exceptionKey)) {
|
||||
dungeonIssues.push(entityId + " has no boss mechanic beyond its basic attack.");
|
||||
@@ -297,7 +303,7 @@ const summary = dungeons.reduce((result, dungeon) => ({
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
status: issues.length ? "blocked" : "green",
|
||||
scope: "HealerMan WoW five-player campaign",
|
||||
scope: "HealerMan WoW five-player combat and animation coverage",
|
||||
summary,
|
||||
documentedExceptions,
|
||||
animationFallbacks,
|
||||
@@ -317,4 +323,4 @@ console.log(JSON.stringify({
|
||||
documentedExceptions: report.documentedExceptions,
|
||||
issues: report.issues,
|
||||
}, null, 2));
|
||||
if (issues.length) process.exitCode = 1;
|
||||
if (issues.length) process.exitCode = 1;
|
||||
@@ -47,6 +47,14 @@ const azerothCoreReferenceFile = path.join(
|
||||
"generated",
|
||||
"azerothCoreReference.json",
|
||||
);
|
||||
const azerothCoreWorldAiFile = path.join(
|
||||
projectRoot,
|
||||
"src",
|
||||
"game",
|
||||
"generated",
|
||||
"azerothCoreWorldAi.json",
|
||||
);
|
||||
const bossMechanicsFile = path.join(projectRoot, "dungeon-pipeline", "boss-mechanics.json");
|
||||
const encounterFile = path.join(projectRoot, "src", "game", "generated", "manastormRuntimeCatalog.json");
|
||||
const sourceCatalogFile = path.join(projectRoot, "src", "game", "generated", "manastormCatalog.json");
|
||||
const epochCatalogFile = path.join(
|
||||
@@ -533,8 +541,206 @@ function fallbackSpecial(id, boss) {
|
||||
};
|
||||
}
|
||||
|
||||
function combatFor(entity, boss, spells, scriptCatalog = undefined) {
|
||||
function validateBossMechanicsCatalog(catalog) {
|
||||
if (catalog.schemaVersion !== 1) {
|
||||
throw new Error(`Unsupported boss mechanic schema ${catalog.schemaVersion}.`);
|
||||
}
|
||||
const deliveries = new Set(["melee", "projectile", "area"]);
|
||||
const targets = new Set(["primary", "random-party", "nearby-party", "self", "lowest-health-friendly"]);
|
||||
const schools = new Set(["physical", "arcane", "fire", "frost", "nature", "shadow", "holy"]);
|
||||
const animations = new Set(["attack", "cast"]);
|
||||
for (const [entry, boss] of Object.entries(catalog.bosses ?? {})) {
|
||||
if (!/^\d+$/.test(entry) && !/^rw-[a-z0-9-]+-\d+$/.test(entry)) {
|
||||
throw new Error(`Boss mechanic key ${entry} is not a creature entry or RuneWaker entity id.`);
|
||||
}
|
||||
if (!["append", "replace"].includes(boss.mode)) {
|
||||
throw new Error(`${entry}: mechanic mode must be append or replace.`);
|
||||
}
|
||||
if (!Array.isArray(boss.mechanics)) throw new Error(`${entry}: mechanics must be an array.`);
|
||||
const ids = new Set();
|
||||
for (const mechanic of boss.mechanics) {
|
||||
if (!mechanic.id || ids.has(mechanic.id)) {
|
||||
throw new Error(`${entry}: mechanic ids must be non-empty and unique (${mechanic.id}).`);
|
||||
}
|
||||
ids.add(mechanic.id);
|
||||
if (!mechanic.name || !deliveries.has(mechanic.delivery) || !targets.has(mechanic.target)
|
||||
|| !schools.has(mechanic.school) || !animations.has(mechanic.animation)
|
||||
|| !Number.isFinite(mechanic.cooldownMs) || mechanic.cooldownMs <= 0
|
||||
|| !Number.isFinite(mechanic.range) || mechanic.range < 0) {
|
||||
throw new Error(`${entry}:${mechanic.id}: mechanic fields are incomplete or invalid.`);
|
||||
}
|
||||
if (mechanic.telegraph && !["circle", "cone", "line"].includes(mechanic.telegraph.shape)) {
|
||||
throw new Error(`${entry}:${mechanic.id}: unsupported telegraph shape.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function smartAiEventProperties(row) {
|
||||
if (row.event_type === 0) {
|
||||
const initial = [Number(row.event_param1) || 0, Number(row.event_param2) || Number(row.event_param1) || 0];
|
||||
const repeat = [Number(row.event_param3) || 1_000, Number(row.event_param4) || Number(row.event_param3) || 1_000];
|
||||
return {
|
||||
initialCooldownMs: initial,
|
||||
repeatCooldownMs: repeat,
|
||||
cooldownMs: Math.max(1_000, Math.round((repeat[0] + repeat[1]) / 2)),
|
||||
};
|
||||
}
|
||||
if (row.event_type === 2) {
|
||||
return {
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [1_000, 1_000],
|
||||
maximumCasts: 1,
|
||||
cooldownMs: 1_000,
|
||||
condition: {
|
||||
kind: "self-health-percent",
|
||||
minimum: Math.max(0, Number(row.event_param1) || 0),
|
||||
maximum: Math.min(100, Number(row.event_param2) || 100),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (row.event_type === 4) {
|
||||
return {
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [1_000, 1_000],
|
||||
maximumCasts: 1,
|
||||
cooldownMs: 1_000,
|
||||
};
|
||||
}
|
||||
if (row.event_type === 14) {
|
||||
return {
|
||||
initialCooldownMs: [0, 0],
|
||||
repeatCooldownMs: [1_000, 1_000],
|
||||
cooldownMs: 1_000,
|
||||
condition: {
|
||||
kind: "friendly-missing-health",
|
||||
amount: Math.max(1, Number(row.event_param1) || 1),
|
||||
range: Math.max(1, Number(row.event_param2) || 30),
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function smartAiMechanicsFor(entity, boss, spells, worldAi) {
|
||||
if (!boss) return [];
|
||||
const entry = Number(entity.entry);
|
||||
const rows = worldAi?.smartScriptsByEntry?.[String(entry)] ?? [];
|
||||
const rowsById = new Map(rows.map((row) => [Number(row.id), row]));
|
||||
const mechanics = [];
|
||||
for (const trigger of rows.filter((row) => Number(row.event_type) !== 61)) {
|
||||
const event = smartAiEventProperties(trigger);
|
||||
if (!event) continue;
|
||||
const chain = [];
|
||||
let row = trigger;
|
||||
const visited = new Set();
|
||||
while (row && !visited.has(Number(row.id))) {
|
||||
visited.add(Number(row.id));
|
||||
chain.push(row);
|
||||
row = Number(row.link) ? rowsById.get(Number(row.link)) : undefined;
|
||||
}
|
||||
for (const action of chain) {
|
||||
const actionType = Number(action.action_type);
|
||||
const source = {
|
||||
entry,
|
||||
sourceType: Number(action.source_type) || 0,
|
||||
rowId: Number(action.id) || 0,
|
||||
eventType: Number(trigger.event_type) || 0,
|
||||
actionType,
|
||||
targetType: Number(action.target_type) || 0,
|
||||
comment: String(action.comment ?? trigger.comment ?? ""),
|
||||
};
|
||||
if (actionType === 11) {
|
||||
const spellId = Number(action.action_param1);
|
||||
const spell = spells.get(spellId);
|
||||
if (!spell) continue;
|
||||
const hash = stableHash(`${entry}:${spellId}:${action.id}`);
|
||||
const attack = spellAttack(spell, hash, true);
|
||||
mechanics.push({
|
||||
...attack,
|
||||
...event,
|
||||
id: `smart-${entry}-${action.id}-${slugPart(spell.name)}`,
|
||||
target: [5, 6].includes(Number(action.target_type))
|
||||
? "random-party"
|
||||
: attack.target,
|
||||
source,
|
||||
});
|
||||
} else if (actionType === 12) {
|
||||
const creatureEntry = Number(action.action_param1);
|
||||
if (!creatureEntry) continue;
|
||||
mechanics.push({
|
||||
id: `smart-${entry}-${action.id}-summon-${creatureEntry}`,
|
||||
name: String(action.comment ?? "Summon Reinforcements")
|
||||
.replace(/^.*? - (?:In Combat|Health Between [^-]+) - /, ""),
|
||||
delivery: "area",
|
||||
target: "self",
|
||||
school: "physical",
|
||||
animation: "cast",
|
||||
castTimeMs: 1_200,
|
||||
interruptible: false,
|
||||
range: 0,
|
||||
damageMultiplier: 0,
|
||||
effect: { kind: "summon", creatureEntry, count: 1 },
|
||||
...event,
|
||||
source,
|
||||
});
|
||||
} else if (actionType === 39) {
|
||||
const radius = Math.max(1, Number(action.action_param1) || 30);
|
||||
mechanics.push({
|
||||
id: `smart-${entry}-${action.id}-call-for-help`,
|
||||
name: "Call for Help",
|
||||
delivery: "area",
|
||||
target: "self",
|
||||
school: "physical",
|
||||
animation: "cast",
|
||||
castTimeMs: 800,
|
||||
interruptible: false,
|
||||
range: 0,
|
||||
damageMultiplier: 0,
|
||||
effect: { kind: "call-for-help", radius },
|
||||
...event,
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return mechanics;
|
||||
}
|
||||
|
||||
function initializationSpawnChance(entry, worldAi) {
|
||||
const despawnRows = (worldAi?.smartScriptsByEntry?.[String(entry)] ?? []).filter((row) => (
|
||||
Number(row.event_type) === 37
|
||||
&& Number(row.action_type) === 41
|
||||
&& Number(row.target_type) === 1
|
||||
));
|
||||
if (!despawnRows.length) return 1;
|
||||
return rounded(despawnRows.reduce((chance, row) => (
|
||||
chance * (1 - Math.max(0, Math.min(100, Number(row.event_chance) || 0)) / 100)
|
||||
), 1));
|
||||
}
|
||||
|
||||
function uniqueMechanics(mechanics) {
|
||||
const seenIds = new Set();
|
||||
const seenSpellIds = new Set();
|
||||
return mechanics.filter((mechanic) => {
|
||||
if (seenIds.has(mechanic.id)) return false;
|
||||
if (mechanic.spellId && seenSpellIds.has(mechanic.spellId)) return false;
|
||||
seenIds.add(mechanic.id);
|
||||
if (mechanic.spellId) seenSpellIds.add(mechanic.spellId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function combatFor(
|
||||
entity,
|
||||
boss,
|
||||
spells,
|
||||
scriptCatalog = undefined,
|
||||
worldAi = undefined,
|
||||
bossMechanics = undefined,
|
||||
) {
|
||||
const hash = stableHash(`${entity.id}:${entity.name}`);
|
||||
const entry = Number(entity.entry);
|
||||
const basicCooldown = Math.max(1_250, Math.min(3_200, Number(entity.baseAttackTimeMs) || 1_900));
|
||||
const scriptDetails = (scriptCatalog?.scripts?.[entity.scriptName]?.spells ?? [])
|
||||
.filter((spell) => spell.combat);
|
||||
@@ -565,20 +771,27 @@ function combatFor(entity, boss, spells, scriptCatalog = undefined) {
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(0, boss ? 4 : 2)
|
||||
.map((spell) => spellAttack(spell, hash, boss));
|
||||
if (!spellAttacks.length && boss) spellAttacks.push(fallbackSpecial(entity.id, true));
|
||||
const importedMechanics = smartAiMechanicsFor(entity, boss, spells, worldAi);
|
||||
const authored = boss ? bossMechanics?.bosses?.[String(entry)] : undefined;
|
||||
let mechanics = authored?.mode === "replace"
|
||||
? authored.mechanics ?? []
|
||||
: [...(authored?.mechanics ?? []), ...importedMechanics, ...spellAttacks];
|
||||
mechanics = uniqueMechanics(mechanics);
|
||||
if (!mechanics.length && boss && !authored) mechanics.push(fallbackSpecial(entity.id, true));
|
||||
const levels = (entity.level ?? []).map(Number).filter(Number.isFinite);
|
||||
const level = levels.length
|
||||
? Math.max(1, Math.round(levels.reduce((sum, value) => sum + value, 0) / levels.length))
|
||||
: undefined;
|
||||
return {
|
||||
...(entry ? { serverEntry: entry } : {}),
|
||||
...(level ? { level } : {}),
|
||||
healthMultiplier: boss ? 1.15 + Math.min(1.5, Number(entity.rank || 0) * 0.18) : 1,
|
||||
damageMultiplier: boss ? 1.12 : 1,
|
||||
moveSpeed: Math.max(2.4, Math.min(5.2, 3.2 * (Number(entity.runSpeed) || 1))),
|
||||
leashRange: boss ? 70 : 52,
|
||||
attacks: [basicAttack(basicCooldown), ...spellAttacks],
|
||||
...(boss ? { mechanicIds: mechanics.map((mechanic) => mechanic.id) } : {}),
|
||||
attacks: [basicAttack(basicCooldown), ...mechanics],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -596,6 +809,7 @@ function wailingServerCombatFor(entity, boss, reference) {
|
||||
damageMultiplier: 1,
|
||||
moveSpeed: Math.max(2.4, Math.min(5.2, 3.2 * (Number(entity.runSpeed) || 1))),
|
||||
leashRange: boss ? 55 : 42,
|
||||
...(boss ? { mechanicIds: scripted.map((attack) => attack.id) } : {}),
|
||||
attacks: [{
|
||||
...basicAttack(basicCooldown),
|
||||
initialCooldownMs: [450, 450],
|
||||
@@ -664,7 +878,7 @@ function offsetPosition(position, index, radius = 3.2) {
|
||||
];
|
||||
}
|
||||
|
||||
function syntheticEntity(id, name, boss, level) {
|
||||
function syntheticEntity(id, name, boss, level, bossMechanics = undefined) {
|
||||
const entity = {
|
||||
id,
|
||||
entry: Number(id.match(/\d+/)?.[0] ?? stableHash(id)),
|
||||
@@ -681,7 +895,7 @@ function syntheticEntity(id, name, boss, level) {
|
||||
name,
|
||||
...(boss ? { title: "Dungeon Boss", hasLoot: true } : { hasLoot: false }),
|
||||
combat: {
|
||||
...combatFor(entity, boss, new Map()),
|
||||
...combatFor(entity, boss, new Map(), undefined, undefined, bossMechanics),
|
||||
...(!boss ? { attacks: [basicAttack(), fallbackSpecial(id, false)] } : {}),
|
||||
},
|
||||
visual: visualFor(id, name, boss, entity.rank),
|
||||
@@ -764,6 +978,9 @@ async function compile() {
|
||||
const scriptSpellCatalog = await exists(scriptSpellCatalogFile)
|
||||
? await readJson(scriptSpellCatalogFile)
|
||||
: { scripts: {} };
|
||||
const azerothCoreWorldAi = await readJson(azerothCoreWorldAiFile);
|
||||
const bossMechanics = await readJson(bossMechanicsFile);
|
||||
validateBossMechanicsCatalog(bossMechanics);
|
||||
const azerothCoreReference = await readJson(azerothCoreReferenceFile);
|
||||
if (azerothCoreReference.wailingCaverns.unsupportedRequiredBossActions.length) {
|
||||
throw new Error("Wailing Caverns has unsupported required-boss SmartAI actions.");
|
||||
@@ -798,6 +1015,14 @@ async function compile() {
|
||||
for (const detail of scriptSpellCatalog.scripts?.[entity.scriptName]?.spells ?? []) {
|
||||
if (detail.combat) wantedSpellIds.add(Number(detail.spellId));
|
||||
}
|
||||
for (const row of azerothCoreWorldAi.smartScriptsByEntry?.[String(entity.entry)] ?? []) {
|
||||
if (Number(row.action_type) === 11) wantedSpellIds.add(Number(row.action_param1));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const boss of Object.values(bossMechanics.bosses ?? {})) {
|
||||
for (const mechanic of boss.mechanics ?? []) {
|
||||
if (mechanic.spellId) wantedSpellIds.add(Number(mechanic.spellId));
|
||||
}
|
||||
}
|
||||
const spellDbc = path.resolve(process.env.ASCENSION_SPELL_DBC ?? defaultSpellDbc);
|
||||
@@ -897,7 +1122,14 @@ async function compile() {
|
||||
...(boss ? { title: "Dungeon Boss", hasLoot: true } : {}),
|
||||
combat: recipe.slug === "wailing-caverns"
|
||||
? wailingServerCombatFor(source, boss, azerothCoreReference)
|
||||
: combatFor(source, boss, spells, scriptSpellCatalog),
|
||||
: combatFor(
|
||||
source,
|
||||
boss,
|
||||
spells,
|
||||
scriptSpellCatalog,
|
||||
azerothCoreWorldAi,
|
||||
bossMechanics,
|
||||
),
|
||||
visual: visualFor(
|
||||
source.id,
|
||||
source.name,
|
||||
@@ -920,7 +1152,14 @@ async function compile() {
|
||||
hasLoot: true,
|
||||
combat: recipe.slug === "wailing-caverns"
|
||||
? wailingServerCombatFor(source, true, azerothCoreReference)
|
||||
: combatFor(source, true, spells, scriptSpellCatalog),
|
||||
: combatFor(
|
||||
source,
|
||||
true,
|
||||
spells,
|
||||
scriptSpellCatalog,
|
||||
azerothCoreWorldAi,
|
||||
bossMechanics,
|
||||
),
|
||||
visual: visualFor(
|
||||
entityId,
|
||||
encounter.name,
|
||||
@@ -949,6 +1188,7 @@ async function compile() {
|
||||
encounter.name,
|
||||
true,
|
||||
recipe.difficultyVariants[0]?.levelRange?.[1] ?? 60,
|
||||
bossMechanics,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1005,6 +1245,7 @@ async function compile() {
|
||||
const id = encounter.eventOnly
|
||||
? "mutanus-spawn"
|
||||
: candidate?.id ?? `encounter-${encounter.encounterId}`;
|
||||
const spawnChance = initializationSpawnChance(encounter.creatureId, azerothCoreWorldAi);
|
||||
usedBossSpawnIds.add(id);
|
||||
return {
|
||||
id,
|
||||
@@ -1012,6 +1253,7 @@ async function compile() {
|
||||
name: encounter.name,
|
||||
position,
|
||||
yaw: candidate?.yaw ?? 0,
|
||||
...(spawnChance < 1 ? { optional: true } : {}),
|
||||
...(encounter.eventOnly ? { eventOnly: true } : {}),
|
||||
};
|
||||
});
|
||||
@@ -1020,6 +1262,7 @@ async function compile() {
|
||||
const roamingPacks = [];
|
||||
for (const spawn of sourceSpawns) {
|
||||
const entry = Number(spawn.entityId.replace("entry-", ""));
|
||||
const spawnChance = initializationSpawnChance(entry, azerothCoreWorldAi);
|
||||
const isBoss = bossEntries.has(entry);
|
||||
const transformedPosition = mapPosition(spawn.position);
|
||||
if (!isBoss && spawn.waypoints?.length >= 2) {
|
||||
@@ -1041,6 +1284,7 @@ async function compile() {
|
||||
pathId: spawn.pathId,
|
||||
formationSource: "database-solo",
|
||||
spawnMask: spawn.spawnMask,
|
||||
...(spawnChance < 1 ? { spawnChance } : {}),
|
||||
waypoints,
|
||||
members: [{
|
||||
id: "member",
|
||||
@@ -1058,6 +1302,7 @@ async function compile() {
|
||||
position: transformedPosition,
|
||||
yaw: spawn.yaw,
|
||||
spawnMask: spawn.spawnMask,
|
||||
...(spawnChance < 1 ? { spawnChance } : {}),
|
||||
});
|
||||
}
|
||||
for (const boss of bossObjectives) {
|
||||
@@ -1109,10 +1354,11 @@ async function compile() {
|
||||
...roamingPacks.flatMap((pack) => pack.waypoints),
|
||||
];
|
||||
const bounds = finiteBounds(allPoints);
|
||||
const bosses = bossObjectives.map(({ id, name, position, eventOnly }) => ({
|
||||
const bosses = bossObjectives.map(({ id, name, position, optional, eventOnly }) => ({
|
||||
id,
|
||||
name,
|
||||
position,
|
||||
...(optional ? { optional: true } : {}),
|
||||
...(eventOnly ? { eventOnly: true } : {}),
|
||||
}));
|
||||
const databaseKind = recipe.clientBuild === "3.3.5a-Epoch" || draft?.spawns?.length
|
||||
|
||||
@@ -19,14 +19,15 @@ const outputFile = path.join(
|
||||
"generated",
|
||||
"azerothCoreScriptSpells.json",
|
||||
);
|
||||
const epochSourceFile = path.join(
|
||||
const campaignSourceFile = path.join(
|
||||
projectRoot,
|
||||
"dungeon-pipeline",
|
||||
"epoch-five-player-instances.json",
|
||||
"dungeon-campaign.json",
|
||||
);
|
||||
const repository = "https://github.com/azerothcore/azerothcore-wotlk";
|
||||
const sourceRoots = [
|
||||
"src/server/scripts/Kalimdor/CavernsOfTime",
|
||||
"src/server/scripts/EasternKingdoms",
|
||||
"src/server/scripts/Kalimdor",
|
||||
"src/server/scripts/Northrend",
|
||||
"src/server/scripts/Outland",
|
||||
"src/server/scripts/World",
|
||||
@@ -45,7 +46,10 @@ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
||||
|
||||
function runGit(argumentsList, cwd = projectRoot) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git", argumentsList, {
|
||||
const safeArguments = cwd === sourceRoot
|
||||
? ["-c", `safe.directory=${sourceRoot.replace(/\\/g, "/")}`, ...argumentsList]
|
||||
: argumentsList;
|
||||
const child = spawn("git", safeArguments, {
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
@@ -61,7 +65,10 @@ function runGit(argumentsList, cwd = projectRoot) {
|
||||
|
||||
function readGit(argumentsList, cwd = projectRoot) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git", argumentsList, {
|
||||
const safeArguments = cwd === sourceRoot
|
||||
? ["-c", `safe.directory=${sourceRoot.replace(/\\/g, "/")}`, ...argumentsList]
|
||||
: argumentsList;
|
||||
const child = spawn("git", safeArguments, {
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
@@ -203,8 +210,8 @@ if (checkedOutCommit !== DEFAULT_AZEROTHCORE_COMMIT) {
|
||||
`AzerothCore script checkout is ${checkedOutCommit}; rerun with --fetch to restore ${DEFAULT_AZEROTHCORE_COMMIT}.`,
|
||||
);
|
||||
}
|
||||
const epochSource = await readJson(epochSourceFile);
|
||||
const wantedSlugs = new Set(epochSource.dungeons.map((dungeon) => dungeon.slug));
|
||||
const campaignSource = await readJson(campaignSourceFile);
|
||||
const wantedSlugs = new Set(campaignSource.dungeons.map((dungeon) => dungeon.slug));
|
||||
const scriptNames = new Set();
|
||||
for (const slug of wantedSlugs) {
|
||||
const draftFile = path.join(projectRoot, "..", "HealerMan-Storage", "pipeline-work", "dungeons", slug, "runtime-draft.json");
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { threeToSoloNavMesh, threeToTiledNavMesh, NavMeshHelper } from "@recast-navigation/three";
|
||||
import { Vector3 } from "three";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { MeshoptDecoder } from "meshoptimizer";
|
||||
import {
|
||||
exists,
|
||||
fixturesRoot,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
import {
|
||||
nextTiledNavMeshSize,
|
||||
planNavigationBuild,
|
||||
shouldRetryFailedTiledBuildWithMonotone,
|
||||
shouldUseMonotonePartition,
|
||||
validateTiledBuild,
|
||||
} from "./recast-policy.mjs";
|
||||
@@ -39,9 +41,10 @@ globalThis.ProgressEvent ??= class ProgressEvent {
|
||||
globalThis.createImageBitmap ??= async () => ({ width: 1, height: 1, close() {} });
|
||||
|
||||
async function loadScene(file) {
|
||||
await MeshoptDecoder.ready;
|
||||
const bytes = await readFile(file);
|
||||
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
return new Promise((resolve, reject) => new GLTFLoader().parse(buffer, "", (gltf) => resolve(gltf.scene), reject));
|
||||
return new Promise((resolve, reject) => new GLTFLoader().setMeshoptDecoder(MeshoptDecoder).parse(buffer, "", (gltf) => resolve(gltf.scene), reject));
|
||||
}
|
||||
|
||||
function meshesIn(scene) {
|
||||
@@ -273,9 +276,11 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
const meshes = scenes.flatMap(meshesIn);
|
||||
if (!meshes.length) throw new Error(`${slug}: collision chunks contain no triangle meshes.`);
|
||||
const input = inspectNavigationInput(meshes);
|
||||
let build = planNavigationBuild(input.bounds, settings.cellSize, {
|
||||
tileSize: settings.tileSize,
|
||||
const navigationPlan = (tileSize) => planNavigationBuild(input.bounds, settings.cellSize, {
|
||||
tileSize,
|
||||
...(settings.forceTiled === true ? { maxSoloAxis: 0, maxSoloCells: 0 } : {}),
|
||||
});
|
||||
let build = navigationPlan(settings.tileSize);
|
||||
const generatorConfig = {
|
||||
cs: settings.cellSize,
|
||||
ch: settings.cellHeight,
|
||||
@@ -323,7 +328,20 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
retainTileIntermediates,
|
||||
);
|
||||
}
|
||||
if (!generated.success) throw new Error(`${slug}: Recast generation failed: ${generated.error}`);
|
||||
if (!generated.success) {
|
||||
buildAttempts.push({
|
||||
partition: tiledPartition,
|
||||
tileSize: build.tileSize,
|
||||
tileCount: build.tileCount,
|
||||
valid: false,
|
||||
failures: [{ x: null, y: null, message: generated.error }],
|
||||
});
|
||||
if (shouldRetryFailedTiledBuildWithMonotone(generated.error, tiledPartition)) {
|
||||
tiledPartition = "monotone";
|
||||
continue;
|
||||
}
|
||||
throw new Error(`${slug}: Recast generation failed: ${generated.error}`);
|
||||
}
|
||||
tiledValidation = validateTiledBuild(generated.intermediates, {
|
||||
errorCategory: Recast.RC_LOG_ERROR,
|
||||
warningCategory: Recast.RC_LOG_WARNING,
|
||||
@@ -349,9 +367,7 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
releaseTiledIntermediates(generated.intermediates);
|
||||
generated.navMesh.destroy();
|
||||
generated = null;
|
||||
build = planNavigationBuild(input.bounds, settings.cellSize, {
|
||||
tileSize: nextTileSize,
|
||||
});
|
||||
build = navigationPlan(nextTileSize);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
@@ -362,9 +378,7 @@ export async function bakeNavigationFromCollisionAssets({
|
||||
generated.navMesh.destroy();
|
||||
generated = null;
|
||||
tiledPartition = "monotone";
|
||||
build = planNavigationBuild(input.bounds, settings.cellSize, {
|
||||
tileSize: settings.tileSize,
|
||||
});
|
||||
build = navigationPlan(settings.tileSize);
|
||||
continue;
|
||||
}
|
||||
const sample = tiledValidation.failures
|
||||
|
||||
@@ -146,3 +146,8 @@ export function shouldUseMonotonePartition(validation) {
|
||||
(failure) => /^rcBuildRegions: \d+ overlapping regions\.$/.test(failure.message),
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldRetryFailedTiledBuildWithMonotone(error, partition) {
|
||||
return partition === "watershed"
|
||||
&& /failed to triangulate contours/i.test(String(error ?? ""));
|
||||
}
|
||||
@@ -25,16 +25,31 @@ async function main() {
|
||||
new Uint8Array(fs.readFileSync(file.path)),
|
||||
{ uri: file.file, maxIssues: 1_000 },
|
||||
);
|
||||
const validatorMessages = report.issues.messages;
|
||||
const hasBasisu = validatorMessages.some((issue) => (
|
||||
issue.code === "UNSUPPORTED_EXTENSION" && issue.message.includes("'KHR_texture_basisu'")
|
||||
));
|
||||
const actionable = validatorMessages.filter((issue) => {
|
||||
if (issue.code === "UNSUPPORTED_EXTENSION"
|
||||
&& /'(?:EXT_mesh_gpu_instancing|KHR_texture_basisu)'/.test(issue.message)) return false;
|
||||
if (issue.code === "VALUE_NOT_IN_LIST"
|
||||
&& hasBasisu
|
||||
&& /^\/images\/\d+\/mimeType$/.test(issue.pointer ?? "")
|
||||
&& issue.message.includes("'image/ktx2'")) return false;
|
||||
return true;
|
||||
});
|
||||
rows.push({
|
||||
file: file.file,
|
||||
errors: report.issues.numErrors,
|
||||
warnings: report.issues.numWarnings,
|
||||
errors: actionable.filter((issue) => issue.severity === 0).length,
|
||||
warnings: actionable.filter((issue) => issue.severity === 1).length,
|
||||
ignoredKnownToolingIssues: validatorMessages.length - actionable.length,
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
files: rows.length,
|
||||
errors: rows.reduce((sum, row) => sum + row.errors, 0),
|
||||
warnings: rows.reduce((sum, row) => sum + row.warnings, 0),
|
||||
ignoredKnownToolingIssues: rows.reduce((sum, row) => sum + row.ignoredKnownToolingIssues, 0),
|
||||
nonGreen: rows.filter((row) => row.errors || row.warnings),
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
@@ -47,15 +47,23 @@ reviewed mechanics passes.
|
||||
|
||||
## Coordinate conversion
|
||||
|
||||
RuneWaker server positions are world coordinates. The environment ROS is
|
||||
placed by one WDB descriptor. Spawns must be moved into that ROS's local frame
|
||||
before the RuneWaker-to-Three coordinate conversion:
|
||||
RuneWaker server positions are world coordinates. The reviewed primary WDB
|
||||
descriptor remains the coordinate anchor, while every static and reviewed
|
||||
dynamic WDB resource is assembled relative to that anchor. Spawns use:
|
||||
|
||||
```text
|
||||
sourceLocal = inverse(primaryWdbPlacement) * serverPosition
|
||||
threeLocal = (sourceLocal.x, sourceLocal.y, -sourceLocal.z) * metersPerSourceUnit
|
||||
```
|
||||
|
||||
Every resource placement uses the full matrix equivalent, including translation,
|
||||
quaternion rotation, and nonuniform scale:
|
||||
|
||||
```text
|
||||
threeMatrix = S * inverse(primaryTRS) * placementTRS * inverse(S)
|
||||
S = scale(metersPerSourceUnit, metersPerSourceUnit, -metersPerSourceUnit)
|
||||
```
|
||||
|
||||
The current calibrated scale is `0.1` meter per RuneWaker source unit. The
|
||||
authoritative test-dungeon table uses 360 degrees per turn:
|
||||
|
||||
@@ -283,12 +291,17 @@ Review:
|
||||
- collision chunks for decorative blockers;
|
||||
- Recast report and entrance projection;
|
||||
- WDB anchor in `import-metadata.json`;
|
||||
- complete placement accounting in `wdb-placement-manifest.json`;
|
||||
- source-evidenced dynamic placements are absent from static collision and have separately packaged runtime assets;
|
||||
- dynamic filename candidates remain static and review-required until a reviewed recipe rule exists;
|
||||
- Khronos validation output.
|
||||
|
||||
Secondary WDB doodads/effects remain audited. Repeated primary room placements
|
||||
and documented source-coordinate support/fallback geometry are automated; a
|
||||
complete arbitrary multi-ROS WDB scene still requires explicit placement,
|
||||
material, collision, and performance review.
|
||||
Every valid WDB ROS resource is exported once into
|
||||
`runewaker-export-work/resource-cache/<resource-key>`, then reused for all of its
|
||||
placements and across dungeon builds. Missing ROS files, non-ROS descriptors,
|
||||
export failures, and non-mesh particle/light/sound/script components are recorded
|
||||
instead of receiving substitute geometry. The preview and editable Blender file
|
||||
remain primary-ROS diagnostics; review the assembled GLB for final coverage.
|
||||
|
||||
### 6. Review and build creature actors
|
||||
|
||||
@@ -401,6 +414,13 @@ Each mechanic must specify trigger, targets, timing, reset behavior, persistence
|
||||
offline behavior, and multiplayer synchronization boundary before being marked
|
||||
implemented.
|
||||
|
||||
Run `npm run runewaker:audit:encounters` after changing source snapshots,
|
||||
mechanics evidence, generated populations, or the shared boss-mechanic catalog.
|
||||
Its JSON report is the exhaustive implementation ledger. A source-spawn row or
|
||||
evidence record may be promoted from
|
||||
`deferred-source-gap` only when an exact executable mapping and its clearing test
|
||||
are both present; a regex category or suggestive object name is not sufficient.
|
||||
|
||||
### 9. Register the dungeon
|
||||
|
||||
Use the generated environment metadata and population constants to create the
|
||||
@@ -443,6 +463,8 @@ errors, WebGL errors, and mobile/desktop performance.
|
||||
| `public/assets/creatures/<slug>/*` | Browser-shipping actor GLBs/manifest. |
|
||||
| `src/game/generated/*Population.generated.ts` | Runtime entities/spawns/objectives. |
|
||||
| `src/game/generated/*Mechanics.generated.ts` | Non-executable mechanics evidence summary. |
|
||||
| `scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.*` | Deterministic per-row/per-signal fidelity and blocker ledger. |
|
||||
| `scripts/runewaker-pipeline/RUNEWAKER_ENVIRONMENT_ASSEMBLY_REPORT.*` | Non-mutating WDB resource/placement coverage and unsupported-type audit. |
|
||||
|
||||
Generated runtime data must be reproducible from recipes plus portable snapshots
|
||||
without SQL or network access.
|
||||
@@ -462,13 +484,13 @@ without SQL or network access.
|
||||
|
||||
## Current deliberate limits
|
||||
|
||||
The generic scripts automate source discovery, primary environment conversion,
|
||||
The generic scripts automate source discovery, source-hash-validated multi-resource WDB environment conversion,
|
||||
portable population recovery, direct GR2 actor conversion, exact paperdoll
|
||||
assembly, source-safe direct-damage attacks, spawn generation, repeated-room
|
||||
assembly, source-anchor navigation, mechanics evidence extraction, runtime
|
||||
assembly, full placement transforms, static navigation, reviewed dynamic-object packaging, mechanics evidence extraction, runtime
|
||||
registration, and validation. They do not pretend that arbitrary Lua/AutoPlot
|
||||
code can be safely translated without review. Disconnected navmesh islands,
|
||||
doors, phases, summons, patrols, timers, interactions, loot semantics, arbitrary
|
||||
multi-ROS placement assembly, source-exact damage formulas, and multiplayer
|
||||
source behavior for unreviewed dynamic candidates, source-exact damage formulas, and multiplayer
|
||||
encounter synchronization remain explicit per-instance engineering work until
|
||||
implemented and playtested.
|
||||
@@ -3,11 +3,12 @@
|
||||
This pipeline converts preserved RuneWaker `.ros` dungeon assets into the same
|
||||
browser-facing GLB roles used by HealerMan's WoW dungeon pipeline:
|
||||
|
||||
1. the preserved Rune engine reads the WDB entity table and exports the primary ROS as an OBJ/MTL bridge;
|
||||
2. Microsoft `texconv` decodes the legacy DDS textures to PNG;
|
||||
3. Blender converts the Y-up source to textured visual and chunked collision GLBs;
|
||||
4. glTF Transform optimizes the visual asset with meshopt;
|
||||
5. HealerMan's Recast stage bakes navigation and the Khronos validator gates every GLB.
|
||||
1. the preserved Rune engine reads every WDB descriptor and exports each unique ROS once into a normalized-path cache validated by the source SHA-256;
|
||||
2. Microsoft `texconv` decodes each cached resource's legacy DDS textures to PNG;
|
||||
3. Blender converts the Y-up resources to reusable visual and collision GLBs;
|
||||
4. the assembler applies every placement's full translation/quaternion/scale transform relative to the reviewed primary WDB anchor;
|
||||
5. glTF Transform instances and optimizes the assembled visual with meshopt;
|
||||
6. HealerMan's Recast stage bakes static navigation and the Khronos validator gates every GLB.
|
||||
|
||||
Run the Forsaken Abbey pilot from the HealerMan root:
|
||||
|
||||
@@ -24,10 +25,29 @@ Optional environment variables:
|
||||
|
||||
The shipping environment is written to
|
||||
`src/assets/game/dungeons/forsaken-abbey` and registered as a playable
|
||||
dungeon. The build reads all 184 WDB entity descriptors,
|
||||
uses the primary ROS placement as the server-to-local anchor, reads the entrance
|
||||
from `Resource/luascript/03242.lua`, and requires that entrance to project within
|
||||
2 meters of the generated navmesh.
|
||||
dungeon. The build reads all 184 WDB entity descriptors, uses the primary ROS
|
||||
placement only as the server-to-local coordinate anchor, reads the entrance from
|
||||
`Resource/luascript/03242.lua`, and requires that entrance to project within 2
|
||||
meters of the generated navmesh.
|
||||
|
||||
All valid ROS placement resources are assembled by default. Use `--primary-only`
|
||||
only for a deliberate compatibility/debug build. The build writes
|
||||
`wdb-placement-manifest.json` with stable placement IDs, source TRS, the complete
|
||||
primary-local Three.js matrix, resource-cache keys, bounds, flags, asset status,
|
||||
and reviewed dynamic-object metadata. Static placements enter visual, collision,
|
||||
and navigation assets. Explicit dynamic placements ship separately so runtime
|
||||
state can move, hide, or replace them without baking their collision into the
|
||||
static world.
|
||||
|
||||
Audit the checked-in WDB coverage without regenerating GLBs:
|
||||
|
||||
```powershell
|
||||
npm run runewaker:audit:environment-assembly
|
||||
```
|
||||
|
||||
See
|
||||
[RUNEWAKER_ENVIRONMENT_ASSEMBLY_REPORT.md](./RUNEWAKER_ENVIRONMENT_ASSEMBLY_REPORT.md)
|
||||
for unsupported types and dynamic candidates that still need source review.
|
||||
|
||||
Population and creature assets are separate, offline-capable stages:
|
||||
|
||||
@@ -36,6 +56,15 @@ npm run runewaker:forsaken:actors
|
||||
npm run runewaker:forsaken:population
|
||||
```
|
||||
|
||||
Source-shaped combat coefficients and boss loot are refreshed independently:
|
||||
|
||||
```powershell
|
||||
npm run runewaker:spells:refresh
|
||||
npm run runewaker:loot:refresh
|
||||
```
|
||||
|
||||
See [RUNEWAKER_FORMULA_AND_LOOT.md](./RUNEWAKER_FORMULA_AND_LOOT.md) for the exact formula mapping boundary and [RUNEWAKER_LOOT_IMPORT_REPORT.md](./RUNEWAKER_LOOT_IMPORT_REPORT.md) for every boss's loot status.
|
||||
|
||||
Only an explicit forensic refresh connects to the isolated development SQL
|
||||
instance:
|
||||
|
||||
@@ -105,8 +134,9 @@ NPC spell evidence is exported from the preserved read-only RuneWaker client
|
||||
catalogs into a checked-in portable snapshot:
|
||||
|
||||
~~~powershell
|
||||
npm run runewaker:spells:refresh
|
||||
npm run runewaker:bosses:refresh
|
||||
npm run runewaker:build:all -- --phase population --force
|
||||
npm run runewaker:forsaken:population
|
||||
npm run runewaker:audit:gameplay
|
||||
~~~
|
||||
|
||||
@@ -120,14 +150,34 @@ Spell conversion is deliberately fail-closed. Only a MagicObject component
|
||||
positively classified as direct HP damage becomes an executable HealerMan
|
||||
attack. Buffs, healing, summons, plot hooks, and unknown effects remain
|
||||
evidence-only. The portable mapping preserves source spell identity, name,
|
||||
damage family, school, range, and cooldown. The current 0.85 damage coefficient
|
||||
is HealerMan-authored and is not represented as the original RuneWaker formula.
|
||||
damage family, school, range, cooldown, source power fields, and source-code
|
||||
formula provenance. Live damage uses a labeled HealerMan baseline adapter because
|
||||
the required native ATK/MATK, DEF/MDEF, weapon, resistance, critical, level-rate,
|
||||
and global-rate inputs are not packaged; it is not represented as source-exact.
|
||||
|
||||
`runewaker:bosses:refresh` first rebuilds that portable spell snapshot, then
|
||||
statically follows each packaged boss's preserved AutoPlot/template callbacks
|
||||
and writes modular `rw-<dungeon>-<entity-id>` kits to
|
||||
`dungeon-pipeline/boss-mechanics.json`. Each special is an independent record
|
||||
with stable ID, trigger, target, effects, and explicit circle, cone, or line
|
||||
telegraph geometry. Both population builders copy those IDs into
|
||||
`combat.mechanicIds`, so the same runtime loadout boundary is shared by
|
||||
Runewaker and WoW encounters and can later support reviewed randomization.
|
||||
|
||||
Source-backed records identify the RuneWaker Lua and spell evidence they came
|
||||
from. If the preserved scripts expose only encounter-progression hooks, the
|
||||
importer creates a conservative playable mechanic marked
|
||||
`source.kind: "healer-man-source-gap"`; it never labels that fallback as an
|
||||
original RuneWaker ability. Edit or append a boss kit in the shared catalog,
|
||||
then rerun the population build and gameplay audit to update an encounter.
|
||||
|
||||
The gameplay audit also:
|
||||
|
||||
- verifies every executable attack.spellId against both its source template
|
||||
assignment and the portable spell catalog;
|
||||
- rejects evidence-only spell IDs if they enter runtime combat data;
|
||||
- requires every boss objective to resolve a modular catalog kit and validates
|
||||
its source label, telegraph, timed trigger, and health trigger;
|
||||
- reuses the live party-navigation graph to inspect boss approach points
|
||||
projected to the retained surface within 6m;
|
||||
- validates native animation families, with Rune Device's missing locomotion
|
||||
@@ -146,6 +196,21 @@ accepts that reviewed override only as `bossObjectiveOrder: { kind:
|
||||
"source-authored", spawnIds: [...], evidence: [...] }`; it rejects missing
|
||||
evidence, duplicates, or an order that does not account for every boss spawn.
|
||||
|
||||
The same gameplay audit also regenerates the deterministic
|
||||
[RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md](./RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md)
|
||||
and its exhaustive JSON companion. The ledger records all deferred source spawn
|
||||
rows and direct mechanics evidence with stable IDs, source files and hashes,
|
||||
fidelity, implementation status, blocker, next work, and a clearing test.
|
||||
Category totals are overlapping review labels, never a claimed count of unique
|
||||
mechanics. Unproven behavior remains `deferred-source-gap` instead of being
|
||||
silently inferred from a name or regex match.
|
||||
|
||||
Run only that ledger and its pipeline tests with:
|
||||
|
||||
~~~powershell
|
||||
npm run runewaker:audit:encounters
|
||||
~~~
|
||||
|
||||
Run the component-level navigation review separately:
|
||||
|
||||
~~~powershell
|
||||
|
||||
@@ -13,7 +13,7 @@ Required top-level fields:
|
||||
- `title`: display/source title.
|
||||
- `source`: preserved WDB, primary ROS, config, and entrance evidence.
|
||||
- `coordinateSystem`: source axes, units, and direction units.
|
||||
- `navigation`: Recast settings.
|
||||
- `navigation`: Recast settings. `partition` may select `watershed` or `monotone`; `forceTiled` is an explicit source-geometry workaround when a compact solo heightfield cannot triangulate its contours.
|
||||
- `review`: human review states.
|
||||
- `exceptions`: known limitations retained in metadata.
|
||||
|
||||
@@ -27,6 +27,25 @@ Required top-level fields:
|
||||
- `entryLua` / `entryConstantPrefix`: authoritative entry constants; or
|
||||
- `entryCoordinates`: explicit position/direction with `reviewRequired`.
|
||||
|
||||
Optional `source.wdbAssembly` fields:
|
||||
|
||||
- `mode`: `all-ros` by default, or `primary-only` for a deliberate compatibility build.
|
||||
- `dynamicPlacements`: reviewed dynamic object selectors. Each entry requires a
|
||||
stable `id`, `kind`, `evidence`, and either an exact WDB `guid` or normalized
|
||||
`resource`; optional `initialState` and `collision` describe the runtime contract.
|
||||
|
||||
Valid ROS placements not selected as dynamic are static. Filename hints are
|
||||
reported for review but never infer stateful behavior. Non-ROS descriptors,
|
||||
invalid transforms, missing resources, and exporter failures remain explicit
|
||||
unsupported records in the placement manifest.
|
||||
|
||||
The full placement transform is preserved as:
|
||||
|
||||
```text
|
||||
threeMatrix = S * inverse(primaryTRS) * placementTRS * inverse(S)
|
||||
S = scale(metersPerSourceUnit, metersPerSourceUnit, -metersPerSourceUnit)
|
||||
```
|
||||
|
||||
## Population recipe
|
||||
|
||||
Required fields:
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,121 @@
|
||||
# RuneWaker encounter implementation and fidelity ledger
|
||||
|
||||
Structural validation: **green**
|
||||
Content completion: **review-required**
|
||||
|
||||
This report is deterministic and offline. Its JSON companion inventories every deferred
|
||||
source spawn row and every direct mechanics evidence record. A regex category membership
|
||||
is a review label, not a unique mechanic; one evidence record may belong to several categories.
|
||||
|
||||
## Coverage
|
||||
|
||||
- 30 dungeon packages.
|
||||
- 4113 deferred source rows with stable spawn IDs.
|
||||
- 1524 unique mechanics evidence records: 329 Lua lines and 1195 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.
|
||||
|
||||
## Status definitions
|
||||
|
||||
- `verified`: an exact source identifier is present in executable runtime data and checked by the audit.
|
||||
- `implemented`: executable behavior exists, but source fidelity is incomplete or explicitly authored.
|
||||
- `deferred-source-gap`: source context does not prove the full behavior; the generator never guesses it.
|
||||
- `deferred-asset-gap`: behavior is understood but a required asset is unavailable.
|
||||
- `deferred-runtime-gap`: behavior is understood but the runtime primitive is unavailable.
|
||||
- `excluded-non-gameplay`: a reviewed source item is confirmed to have no gameplay behavior.
|
||||
- `intentional-empty`: the authoritative packaged snapshot contains no population rows.
|
||||
|
||||
## Per-dungeon inventory
|
||||
|
||||
| Dungeon | Source rows | Deferred rows | Evidence | Verified | Source gaps | Bosses | Package status |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| aeternal-circle | 514 | 391 | 47 | 13 | 34 | 0 | deferred-source-gap |
|
||||
| barren-caves | 63 | 15 | 14 | 7 | 7 | 1 | verified |
|
||||
| cave-water-dragon | 144 | 47 | 15 | 2 | 13 | 2 | verified |
|
||||
| demon-stronghold | 439 | 256 | 38 | 12 | 26 | 1 | verified |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| kal-turok-hive | 4 | 3 | 10 | 4 | 6 | 0 | deferred-source-gap |
|
||||
| kalins-shrine | 289 | 124 | 294 | 17 | 277 | 5 | verified |
|
||||
| kawaks-tomb | 293 | 176 | 46 | 7 | 39 | 1 | verified |
|
||||
| kulech-bones-nest | 287 | 133 | 59 | 18 | 41 | 1 | verified |
|
||||
| lair-demon-dragon | 532 | 429 | 28 | 9 | 19 | 0 | deferred-source-gap |
|
||||
| mystic-altar | 398 | 147 | 68 | 26 | 42 | 3 | verified |
|
||||
| paspers-shrine | 255 | 31 | 22 | 6 | 16 | 5 | verified |
|
||||
| raksha-temple | 487 | 364 | 85 | 21 | 64 | 1 | verified |
|
||||
| sand-vortex | 2 | 1 | 2 | 0 | 2 | 1 | verified |
|
||||
| sardo-castle | 247 | 176 | 53 | 12 | 41 | 3 | verified |
|
||||
| the-origin | 510 | 322 | 65 | 29 | 36 | 2 | verified |
|
||||
| tomb-seven-heroes | 439 | 199 | 101 | 32 | 69 | 0 | deferred-source-gap |
|
||||
| treasure-trove | 685 | 188 | 81 | 34 | 47 | 3 | verified |
|
||||
| warnorken-arena | 40 | 39 | 2 | 0 | 2 | 0 | deferred-source-gap |
|
||||
| ystra-labyrinth-boss1 | 17 | 14 | 12 | 2 | 10 | 3 | verified |
|
||||
| ystra-labyrinth-boss2 | 3 | 2 | 3 | 1 | 2 | 1 | verified |
|
||||
| ystra-labyrinth-boss3 | 7 | 6 | 2 | 0 | 2 | 1 | verified |
|
||||
| zurhidon-stronghold | 400 | 247 | 58 | 23 | 35 | 0 | deferred-source-gap |
|
||||
| zurhidon-stronghold-124 | 0 | 0 | 0 | 0 | 0 | 0 | intentional-empty |
|
||||
|
||||
## Four explicit fallback bosses
|
||||
|
||||
| Boss | Runtime ID | Status | Fidelity | Blocker |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Necromage | `rw-fa-100070` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. |
|
||||
| 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. |
|
||||
|
||||
These attacks are intentionally playable and telegraphed HealerMan behavior. They are not
|
||||
presented as original RuneWaker mechanics until authoritative source behavior is recovered.
|
||||
|
||||
## Non-empty packages without confirmed boss objectives
|
||||
|
||||
- `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.
|
||||
- `warnorken-arena`: 40 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.
|
||||
- `zurhidon-stronghold`: 400 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.
|
||||
|
||||
## Intentional empty variants
|
||||
|
||||
- `demon-stronghold-125` (Zone 125): The authoritative packaged snapshot contains no active population rows.
|
||||
- `zurhidon-stronghold-124` (Zone 124): The authoritative packaged snapshot contains no active population rows.
|
||||
|
||||
## Evidence category memberships
|
||||
|
||||
| Category | Evidence records carrying the label |
|
||||
| --- | ---: |
|
||||
| death | 100 |
|
||||
| door | 23 |
|
||||
| interaction | 166 |
|
||||
| loot | 77 |
|
||||
| movement | 6 |
|
||||
| phase | 58 |
|
||||
| spell | 679 |
|
||||
| summon | 11 |
|
||||
| timer | 26 |
|
||||
|
||||
## Remaining work ledger
|
||||
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
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`.
|
||||
|
||||
## Validation issues
|
||||
|
||||
- None.
|
||||
@@ -0,0 +1,476 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"generatedBy": "scripts/runewaker-pipeline/audit-environment-assembly.mjs",
|
||||
"deterministic": true,
|
||||
"validationStatus": "green",
|
||||
"reviewStatus": "review-required",
|
||||
"summary": {
|
||||
"dungeons": 30,
|
||||
"sourceDescriptors": 15976,
|
||||
"uniqueDungeonResources": 2799,
|
||||
"staticPlacements": 15976,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 107,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0
|
||||
},
|
||||
"unsupportedResourceTypes": [
|
||||
"WDB placement resources other than ROS are recorded but not converted.",
|
||||
"Particle, light, sound, camera, and script-only components absent from ROS mesh descriptors remain unsupported.",
|
||||
"Animated/stateful behavior remains unsupported until an exact reviewed dynamic-placement rule supplies its kind, initial state, collision policy, and evidence.",
|
||||
"A ROS that the preserved exporter cannot decode is skipped, recorded with its resource path and failure, and never replaced with guessed geometry."
|
||||
],
|
||||
"issues": [],
|
||||
"dungeons": [
|
||||
{
|
||||
"id": "aeternal-circle",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/aeternal-circle.json",
|
||||
"wdbReport": "src/assets/game/dungeons/aeternal-circle/wdb-report.json",
|
||||
"sourceDescriptors": 575,
|
||||
"uniqueResources": 182,
|
||||
"staticPlacements": 575,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 54,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "barren-caves",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/barren-caves.json",
|
||||
"wdbReport": "src/assets/game/dungeons/barren-caves/wdb-report.json",
|
||||
"sourceDescriptors": 1,
|
||||
"uniqueResources": 1,
|
||||
"staticPlacements": 1,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "cave-water-dragon",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/cave-water-dragon.json",
|
||||
"wdbReport": "src/assets/game/dungeons/cave-water-dragon/wdb-report.json",
|
||||
"sourceDescriptors": 121,
|
||||
"uniqueResources": 16,
|
||||
"staticPlacements": 121,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "demon-stronghold",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/demon-stronghold.json",
|
||||
"wdbReport": "src/assets/game/dungeons/demon-stronghold/wdb-report.json",
|
||||
"sourceDescriptors": 90,
|
||||
"uniqueResources": 4,
|
||||
"staticPlacements": 90,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "demon-stronghold-125",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/demon-stronghold-125.json",
|
||||
"wdbReport": "src/assets/game/dungeons/demon-stronghold-125/wdb-report.json",
|
||||
"sourceDescriptors": 90,
|
||||
"uniqueResources": 4,
|
||||
"staticPlacements": 90,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "dungeon-of-dalanis",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/dungeon-of-dalanis.json",
|
||||
"wdbReport": "src/assets/game/dungeons/dungeon-of-dalanis/wdb-report.json",
|
||||
"sourceDescriptors": 652,
|
||||
"uniqueResources": 124,
|
||||
"staticPlacements": 652,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 5,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "forsaken-abbey",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/forsaken-abbey.json",
|
||||
"wdbReport": "src/assets/game/dungeons/forsaken-abbey/wdb-report.json",
|
||||
"sourceDescriptors": 184,
|
||||
"uniqueResources": 26,
|
||||
"staticPlacements": 184,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 1,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "grafu-castle",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/grafu-castle.json",
|
||||
"wdbReport": "src/assets/game/dungeons/grafu-castle/wdb-report.json",
|
||||
"sourceDescriptors": 899,
|
||||
"uniqueResources": 286,
|
||||
"staticPlacements": 899,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 17,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "hall-of-survivors",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/hall-of-survivors.json",
|
||||
"wdbReport": "src/assets/game/dungeons/hall-of-survivors/wdb-report.json",
|
||||
"sourceDescriptors": 296,
|
||||
"uniqueResources": 32,
|
||||
"staticPlacements": 296,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "hoto",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/hoto.json",
|
||||
"wdbReport": "src/assets/game/dungeons/hoto/wdb-report.json",
|
||||
"sourceDescriptors": 1627,
|
||||
"uniqueResources": 119,
|
||||
"staticPlacements": 1627,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "ice-dwarf-kingdom",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/ice-dwarf-kingdom.json",
|
||||
"wdbReport": "src/assets/game/dungeons/ice-dwarf-kingdom/wdb-report.json",
|
||||
"sourceDescriptors": 1054,
|
||||
"uniqueResources": 209,
|
||||
"staticPlacements": 1054,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "kal-turok-hive",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/kal-turok-hive.json",
|
||||
"wdbReport": "src/assets/game/dungeons/kal-turok-hive/wdb-report.json",
|
||||
"sourceDescriptors": 2,
|
||||
"uniqueResources": 2,
|
||||
"staticPlacements": 2,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 1,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "kalins-shrine",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/kalins-shrine.json",
|
||||
"wdbReport": "src/assets/game/dungeons/kalins-shrine/wdb-report.json",
|
||||
"sourceDescriptors": 943,
|
||||
"uniqueResources": 133,
|
||||
"staticPlacements": 943,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "kawaks-tomb",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/kawaks-tomb.json",
|
||||
"wdbReport": "src/assets/game/dungeons/kawaks-tomb/wdb-report.json",
|
||||
"sourceDescriptors": 573,
|
||||
"uniqueResources": 60,
|
||||
"staticPlacements": 573,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "kulech-bones-nest",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/kulech-bones-nest.json",
|
||||
"wdbReport": "src/assets/game/dungeons/kulech-bones-nest/wdb-report.json",
|
||||
"sourceDescriptors": 199,
|
||||
"uniqueResources": 52,
|
||||
"staticPlacements": 199,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "lair-demon-dragon",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/lair-demon-dragon.json",
|
||||
"wdbReport": "src/assets/game/dungeons/lair-demon-dragon/wdb-report.json",
|
||||
"sourceDescriptors": 530,
|
||||
"uniqueResources": 96,
|
||||
"staticPlacements": 530,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "mystic-altar",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/mystic-altar.json",
|
||||
"wdbReport": "src/assets/game/dungeons/mystic-altar/wdb-report.json",
|
||||
"sourceDescriptors": 2096,
|
||||
"uniqueResources": 48,
|
||||
"staticPlacements": 2096,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "paspers-shrine",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/paspers-shrine.json",
|
||||
"wdbReport": "src/assets/game/dungeons/paspers-shrine/wdb-report.json",
|
||||
"sourceDescriptors": 564,
|
||||
"uniqueResources": 143,
|
||||
"staticPlacements": 564,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "raksha-temple",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/raksha-temple.json",
|
||||
"wdbReport": "src/assets/game/dungeons/raksha-temple/wdb-report.json",
|
||||
"sourceDescriptors": 1028,
|
||||
"uniqueResources": 228,
|
||||
"staticPlacements": 1028,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 11,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "sand-vortex",
|
||||
"mode": "primary-only-procedural-fallback",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/sand-vortex.json",
|
||||
"wdbReport": "src/assets/game/dungeons/sand-vortex/wdb-report.json",
|
||||
"sourceDescriptors": 20,
|
||||
"uniqueResources": 14,
|
||||
"staticPlacements": 20,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "sardo-castle",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/sardo-castle.json",
|
||||
"wdbReport": "src/assets/game/dungeons/sardo-castle/wdb-report.json",
|
||||
"sourceDescriptors": 436,
|
||||
"uniqueResources": 132,
|
||||
"staticPlacements": 436,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "the-origin",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/the-origin.json",
|
||||
"wdbReport": "src/assets/game/dungeons/the-origin/wdb-report.json",
|
||||
"sourceDescriptors": 693,
|
||||
"uniqueResources": 220,
|
||||
"staticPlacements": 693,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 17,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "tomb-seven-heroes",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/tomb-seven-heroes.json",
|
||||
"wdbReport": "src/assets/game/dungeons/tomb-seven-heroes/wdb-report.json",
|
||||
"sourceDescriptors": 450,
|
||||
"uniqueResources": 56,
|
||||
"staticPlacements": 450,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "treasure-trove",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/treasure-trove.json",
|
||||
"wdbReport": "src/assets/game/dungeons/treasure-trove/wdb-report.json",
|
||||
"sourceDescriptors": 908,
|
||||
"uniqueResources": 193,
|
||||
"staticPlacements": 908,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 1,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "warnorken-arena",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/warnorken-arena.json",
|
||||
"wdbReport": "src/assets/game/dungeons/warnorken-arena/wdb-report.json",
|
||||
"sourceDescriptors": 256,
|
||||
"uniqueResources": 14,
|
||||
"staticPlacements": 256,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "ystra-labyrinth-boss1",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss1.json",
|
||||
"wdbReport": "src/assets/game/dungeons/ystra-labyrinth-boss1/wdb-report.json",
|
||||
"sourceDescriptors": 3,
|
||||
"uniqueResources": 1,
|
||||
"staticPlacements": 3,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "ystra-labyrinth-boss2",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss2.json",
|
||||
"wdbReport": "src/assets/game/dungeons/ystra-labyrinth-boss2/wdb-report.json",
|
||||
"sourceDescriptors": 3,
|
||||
"uniqueResources": 1,
|
||||
"staticPlacements": 3,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "ystra-labyrinth-boss3",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss3.json",
|
||||
"wdbReport": "src/assets/game/dungeons/ystra-labyrinth-boss3/wdb-report.json",
|
||||
"sourceDescriptors": 3,
|
||||
"uniqueResources": 1,
|
||||
"staticPlacements": 3,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "zurhidon-stronghold",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/zurhidon-stronghold.json",
|
||||
"wdbReport": "src/assets/game/dungeons/zurhidon-stronghold/wdb-report.json",
|
||||
"sourceDescriptors": 840,
|
||||
"uniqueResources": 201,
|
||||
"staticPlacements": 840,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
},
|
||||
{
|
||||
"id": "zurhidon-stronghold-124",
|
||||
"mode": "all-ros",
|
||||
"recipe": "scripts/runewaker-pipeline/recipes/zurhidon-stronghold-124.json",
|
||||
"wdbReport": "src/assets/game/dungeons/zurhidon-stronghold-124/wdb-report.json",
|
||||
"sourceDescriptors": 840,
|
||||
"uniqueResources": 201,
|
||||
"staticPlacements": 840,
|
||||
"dynamicPlacements": 0,
|
||||
"dynamicReviewCandidates": 0,
|
||||
"unsupportedPlacements": 0,
|
||||
"missingSourceResources": 0,
|
||||
"missingResources": [],
|
||||
"unsupported": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# RuneWaker WDB environment assembly audit
|
||||
|
||||
Structural validation: **green**
|
||||
Source-behavior review: **review-required**
|
||||
|
||||
This deterministic audit reads the checked-in WDB reports and environment recipes. It does
|
||||
not convert or replace shipping GLBs. The builder now supports every valid ROS placement
|
||||
resource with normalized-path model caching validated by source hashes and the complete translation/quaternion/scale
|
||||
transform relative to the reviewed primary placement.
|
||||
|
||||
## Coverage
|
||||
|
||||
- 30 dungeon WDB packages and 15976 source descriptors.
|
||||
- 2799 per-dungeon unique ROS resources (resources reused across dungeons share cache keys).
|
||||
- 15976 static placements and 0 explicitly reviewed dynamic placements.
|
||||
- 107 filename-based dynamic candidates remain static and review-required; names never prove behavior.
|
||||
- 0 unsupported placement records and 0 missing source ROS resources.
|
||||
|
||||
| Dungeon | Mode | Descriptors | Resources | Static | Dynamic | Dynamic review | Unsupported | Missing ROS |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| aeternal-circle | all-ros | 575 | 182 | 575 | 0 | 54 | 0 | 0 |
|
||||
| barren-caves | all-ros | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
|
||||
| cave-water-dragon | all-ros | 121 | 16 | 121 | 0 | 0 | 0 | 0 |
|
||||
| demon-stronghold | all-ros | 90 | 4 | 90 | 0 | 0 | 0 | 0 |
|
||||
| demon-stronghold-125 | all-ros | 90 | 4 | 90 | 0 | 0 | 0 | 0 |
|
||||
| dungeon-of-dalanis | all-ros | 652 | 124 | 652 | 0 | 5 | 0 | 0 |
|
||||
| forsaken-abbey | all-ros | 184 | 26 | 184 | 0 | 1 | 0 | 0 |
|
||||
| grafu-castle | all-ros | 899 | 286 | 899 | 0 | 17 | 0 | 0 |
|
||||
| hall-of-survivors | all-ros | 296 | 32 | 296 | 0 | 0 | 0 | 0 |
|
||||
| hoto | all-ros | 1627 | 119 | 1627 | 0 | 0 | 0 | 0 |
|
||||
| ice-dwarf-kingdom | all-ros | 1054 | 209 | 1054 | 0 | 0 | 0 | 0 |
|
||||
| kal-turok-hive | all-ros | 2 | 2 | 2 | 0 | 1 | 0 | 0 |
|
||||
| kalins-shrine | all-ros | 943 | 133 | 943 | 0 | 0 | 0 | 0 |
|
||||
| kawaks-tomb | all-ros | 573 | 60 | 573 | 0 | 0 | 0 | 0 |
|
||||
| kulech-bones-nest | all-ros | 199 | 52 | 199 | 0 | 0 | 0 | 0 |
|
||||
| lair-demon-dragon | all-ros | 530 | 96 | 530 | 0 | 0 | 0 | 0 |
|
||||
| mystic-altar | all-ros | 2096 | 48 | 2096 | 0 | 0 | 0 | 0 |
|
||||
| paspers-shrine | all-ros | 564 | 143 | 564 | 0 | 0 | 0 | 0 |
|
||||
| raksha-temple | all-ros | 1028 | 228 | 1028 | 0 | 11 | 0 | 0 |
|
||||
| sand-vortex | primary-only-procedural-fallback | 20 | 14 | 20 | 0 | 0 | 0 | 0 |
|
||||
| sardo-castle | all-ros | 436 | 132 | 436 | 0 | 0 | 0 | 0 |
|
||||
| the-origin | all-ros | 693 | 220 | 693 | 0 | 17 | 0 | 0 |
|
||||
| tomb-seven-heroes | all-ros | 450 | 56 | 450 | 0 | 0 | 0 | 0 |
|
||||
| treasure-trove | all-ros | 908 | 193 | 908 | 0 | 1 | 0 | 0 |
|
||||
| warnorken-arena | all-ros | 256 | 14 | 256 | 0 | 0 | 0 | 0 |
|
||||
| ystra-labyrinth-boss1 | all-ros | 3 | 1 | 3 | 0 | 0 | 0 | 0 |
|
||||
| ystra-labyrinth-boss2 | all-ros | 3 | 1 | 3 | 0 | 0 | 0 | 0 |
|
||||
| ystra-labyrinth-boss3 | all-ros | 3 | 1 | 3 | 0 | 0 | 0 | 0 |
|
||||
| zurhidon-stronghold | all-ros | 840 | 201 | 840 | 0 | 0 | 0 | 0 |
|
||||
| zurhidon-stronghold-124 | all-ros | 840 | 201 | 840 | 0 | 0 | 0 | 0 |
|
||||
|
||||
## Runtime-facing output
|
||||
|
||||
A full build writes `wdb-placement-manifest.json` beside the environment package. Each
|
||||
placement has a stable WDB GUID ID, normalized resource/cache key, source TRS, primary-local
|
||||
Three.js matrix, bounds, flags, asset status, and optional reviewed dynamic definition.
|
||||
Static placements are assembled into the optimized visual and navigation collision. Reviewed
|
||||
dynamic placements are omitted from static collision and packaged as separately addressable GLBs.
|
||||
|
||||
## Unsupported or review-required behavior
|
||||
|
||||
- WDB placement resources other than ROS are recorded but not converted.
|
||||
- Particle, light, sound, camera, and script-only components absent from ROS mesh descriptors remain unsupported.
|
||||
- Animated/stateful behavior remains unsupported until an exact reviewed dynamic-placement rule supplies its kind, initial state, collision policy, and evidence.
|
||||
- A ROS that the preserved exporter cannot decode is skipped, recorded with its resource path and failure, and never replaced with guessed geometry.
|
||||
- The primary-only top-down preview and editable `.blend` do not include the final merged WDB
|
||||
assembly; the generated GLB and placement manifest are authoritative.
|
||||
- Dynamic-candidate filenames are not enough to remove geometry from the static package. Add a
|
||||
reviewed `source.wdbAssembly.dynamicPlacements` rule with evidence first.
|
||||
|
||||
## Validation issues
|
||||
|
||||
- None.
|
||||
@@ -0,0 +1,45 @@
|
||||
# RuneWaker combat formula and loot fidelity
|
||||
|
||||
## Combat damage
|
||||
|
||||
Imported RuneWaker damage mechanics now carry a `runewakerFormula` descriptor from `magicobject.db` into the generated enemy attack. The descriptor keeps the source spell/component ids, spell level, damage power, fixed value, damage-over-time base, calculation family, and source-code provenance.
|
||||
|
||||
The preserved implementation is in `SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp`:
|
||||
|
||||
- `CalPhyDamage` begins at line 3366.
|
||||
- `CalMagicDamage` begins at line 3651.
|
||||
- Both use the source ATK/DEF or MATK/MDEF ratio `max(0.2, 1 + ((attack + 100) - (defense + 200)) / max(attack + 100, defense + 200))`.
|
||||
- Both also depend on native weapon or magic damage, level-difference tables, resistance processing, critical power, fixed-attribute lookup, and global NPC/PvP/AoE/target rates.
|
||||
|
||||
HealerMan does not have equivalent RuneWaker ATK, MATK, DEF, MDEF, `FixType`, native weapon/magic damage, level-rate tables, or global rate arrays. The adapter therefore does not label its live result as exact RuneWaker damage:
|
||||
|
||||
- Negative `Atk.DmgPower` values with magnitude at most 4 are treated as source ratios against HealerMan's level-scaled enemy damage budget; the native 0.2 minimum is retained.
|
||||
- Point-valued source powers use the explicit `runtimeBaselineMultiplier` balancing adapter stored with the formula.
|
||||
- HealerMan's existing hit table, armor, resistance, critical, aura, and absorb pipeline runs afterward and remains identified as HealerMan/WotLK-shaped mitigation.
|
||||
- Unclassified formula records return zero from the adapter rather than executing guessed damage.
|
||||
|
||||
`resolveRunewakerNativeResolvedPower` exposes the source-shaped tail for later use. It requires every native-only input as an argument and supplies no silent one-valued defaults.
|
||||
|
||||
## Loot
|
||||
|
||||
`export-loot-catalog.mjs` reads each source-classified boss's 15 independent `NPCObjectDB.DropItem` slots. It preserves the original rate denominator of 100,000 and recursively expands `TreasureObjectDB`'s weighted, mutually exclusive outcomes and fixed `DropCount` quantities.
|
||||
|
||||
Runtime rules are intentionally bounded:
|
||||
|
||||
- Recognized leaves come only from `itemobject.db`, `armorobject.db`, `weaponobject.db`, `cardobject.db`, `runeobject.db`, `recipeobject.db`, and `keyitemobject.db`.
|
||||
- Treasure recursion stops at 12 levels and cycles fail closed.
|
||||
- Unknown object ids fail closed.
|
||||
- Treasure tables with a Lua predicate fail closed because executing arbitrary preserved server Lua is outside the browser runtime.
|
||||
- `NeedDLv` requirements are preserved and evaluated from the caller-provided source level delta.
|
||||
- Source names are retained, but source stats, quality, and equipment slots are not inferred. Portable drops enter inventory as common, non-equipable `misc` items with zero stats.
|
||||
|
||||
The detailed status for every boss is generated in `RUNEWAKER_LOOT_IMPORT_REPORT.md`. The full probability graph is available in `public/data/runewaker-loot-catalog.json` and the runtime snapshot.
|
||||
|
||||
## Refresh and verification
|
||||
|
||||
```powershell
|
||||
npm run runewaker:spells:refresh
|
||||
npm run runewaker:loot:refresh
|
||||
node --test scripts/runewaker-pipeline/runewaker-loot-import.test.mjs
|
||||
vitest run src/game/runewakerCombatFormula.test.ts src/game/runewakerLootCatalog.test.ts
|
||||
```
|
||||
@@ -8,11 +8,16 @@
|
||||
"runtimeSpawns": 4086,
|
||||
"runtimeEntities": 436,
|
||||
"bossObjectives": 61,
|
||||
"runtimeAttacks": 824,
|
||||
"runtimeAttacks": 1068,
|
||||
"genericDefaultAttacks": 436,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 388,
|
||||
"catalogBossMechanics": 279,
|
||||
"sourceBackedBossMechanics": 275,
|
||||
"sourceGapBossMechanics": 4,
|
||||
"telegraphedBossMechanics": 111,
|
||||
"healthTriggeredBossMechanics": 28,
|
||||
"sourceBackedDamageFamilyAttacks": 483,
|
||||
"sourceSpellTemplates": 293,
|
||||
"sourceSpellAssignments": 659,
|
||||
"scriptedTemplates": 357,
|
||||
@@ -27,9 +32,9 @@
|
||||
"dungeons": 30,
|
||||
"uniqueNativeModels": 321,
|
||||
"uniqueSourceSpellIds": 411,
|
||||
"portableSpellCatalogRecords": 411,
|
||||
"portableDamageFamilyRecords": 227,
|
||||
"portableEvidenceOnlyRecords": 184
|
||||
"portableSpellCatalogRecords": 3199,
|
||||
"portableDamageFamilyRecords": 1342,
|
||||
"portableEvidenceOnlyRecords": 1857
|
||||
},
|
||||
"mechanicsCategories": {
|
||||
"death": 75,
|
||||
@@ -53,23 +58,24 @@
|
||||
"boss spawn membership"
|
||||
],
|
||||
"genericRuntimeSystems": [
|
||||
"unmapped attacks",
|
||||
"source-gap attacks",
|
||||
"health/damage scaling",
|
||||
"movement/leash",
|
||||
"clustering/aggro",
|
||||
"combat/death state",
|
||||
"loot policy",
|
||||
"loot item stats/slots",
|
||||
"objective ordering",
|
||||
"procedural animation"
|
||||
],
|
||||
"deferredMechanics": [
|
||||
"doors/gates",
|
||||
"summons/waves",
|
||||
"phases",
|
||||
"complex doors/gates",
|
||||
"unmapped multi-actor waves",
|
||||
"unmapped phases",
|
||||
"patrols",
|
||||
"interactions",
|
||||
"encounter resets",
|
||||
"source loot"
|
||||
"blocked source-loot predicates",
|
||||
"source item stat/slot mapping"
|
||||
],
|
||||
"blockerGroups": {
|
||||
"navigation": {
|
||||
@@ -216,7 +222,10 @@
|
||||
},
|
||||
"sourceMechanics": {
|
||||
"status": "review-required",
|
||||
"executableMechanicsGenerated": false,
|
||||
"executableBossMechanicsGenerated": true,
|
||||
"catalogBossMechanics": 279,
|
||||
"sourceBackedBossMechanics": 275,
|
||||
"sourceGapBossMechanics": 4,
|
||||
"scriptedTemplates": 357,
|
||||
"evidenceCounts": {
|
||||
"death": 75,
|
||||
@@ -237,9 +246,19 @@
|
||||
"safety": {
|
||||
"runtimeSqlDependency": false,
|
||||
"unknownEffectsExecute": false,
|
||||
"coefficientsAreSourceExact": false
|
||||
"coefficientsAreSourceExact": false,
|
||||
"unavailableNativeInputsDefaulted": false,
|
||||
"pointValuedDamageUsesLabeledScaledAdapter": true
|
||||
},
|
||||
"mappingPolicy": "Only positively classified direct-HP damage families execute; unknown/buff/heal/summon/script effects remain evidence-only."
|
||||
"mappingPolicy": "Only positively classified direct-HP damage families execute; named source auras, controls, summons, and health gates use conservative modular effects; unknown semantics remain evidence-only."
|
||||
},
|
||||
"encounterImplementationLedger": {
|
||||
"json": "scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json",
|
||||
"markdown": "scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md",
|
||||
"validationStatus": "green",
|
||||
"completionStatus": "review-required",
|
||||
"deferredSourceRows": 4113,
|
||||
"uniqueMechanicsEvidence": 1524
|
||||
},
|
||||
"resolvedRuntimeObservations": [
|
||||
{
|
||||
@@ -257,9 +276,8 @@
|
||||
"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.",
|
||||
"436 executable attacks do not carry an audited source-spell mapping.",
|
||||
"Boss objectives are spawn-authentic, but boss attacks and state machines remain generic.",
|
||||
"Source-backed spell coefficients are HealerMan-generic; the original RuneWaker formula/stat scaling has not been reconstructed.",
|
||||
"4 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."
|
||||
],
|
||||
"dungeons": [
|
||||
@@ -284,6 +302,11 @@
|
||||
"genericDefaultAttacks": 16,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 13,
|
||||
"nativeBindings": 16,
|
||||
"proceduralBindings": 0,
|
||||
@@ -333,10 +356,15 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 14,
|
||||
"runtimeAttacks": 15,
|
||||
"genericDefaultAttacks": 7,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 3,
|
||||
"sourceBackedBossMechanics": 3,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 3,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 7,
|
||||
"nativeBindings": 7,
|
||||
"proceduralBindings": 0,
|
||||
@@ -400,11 +428,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 9,
|
||||
"runtimeAttacks": 20,
|
||||
"genericDefaultAttacks": 7,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 2,
|
||||
"catalogBossMechanics": 11,
|
||||
"sourceBackedBossMechanics": 11,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 6,
|
||||
"healthTriggeredBossMechanics": 1,
|
||||
"sourceBackedDamageFamilyAttacks": 9,
|
||||
"nativeBindings": 7,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -453,11 +486,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 28,
|
||||
"runtimeAttacks": 32,
|
||||
"genericDefaultAttacks": 16,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 12,
|
||||
"catalogBossMechanics": 4,
|
||||
"sourceBackedBossMechanics": 4,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 1,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 14,
|
||||
"nativeBindings": 16,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -491,6 +529,11 @@
|
||||
"genericDefaultAttacks": 0,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 0,
|
||||
"nativeBindings": 0,
|
||||
"proceduralBindings": 0,
|
||||
@@ -525,6 +568,11 @@
|
||||
"genericDefaultAttacks": 17,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 5,
|
||||
"nativeBindings": 17,
|
||||
"proceduralBindings": 0,
|
||||
@@ -700,11 +748,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 15,
|
||||
"runtimeAttacks": 26,
|
||||
"genericDefaultAttacks": 15,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 0,
|
||||
"catalogBossMechanics": 11,
|
||||
"sourceBackedBossMechanics": 7,
|
||||
"sourceGapBossMechanics": 4,
|
||||
"telegraphedBossMechanics": 6,
|
||||
"healthTriggeredBossMechanics": 4,
|
||||
"sourceBackedDamageFamilyAttacks": 2,
|
||||
"nativeBindings": 15,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -738,6 +791,11 @@
|
||||
"genericDefaultAttacks": 11,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 10,
|
||||
"nativeBindings": 11,
|
||||
"proceduralBindings": 0,
|
||||
@@ -931,11 +989,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 74,
|
||||
"runtimeAttacks": 122,
|
||||
"genericDefaultAttacks": 40,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 34,
|
||||
"catalogBossMechanics": 49,
|
||||
"sourceBackedBossMechanics": 49,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 23,
|
||||
"healthTriggeredBossMechanics": 2,
|
||||
"sourceBackedDamageFamilyAttacks": 60,
|
||||
"nativeBindings": 39,
|
||||
"proceduralBindings": 1,
|
||||
"proxyBindings": 0,
|
||||
@@ -1052,11 +1115,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 54,
|
||||
"runtimeAttacks": 75,
|
||||
"genericDefaultAttacks": 26,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 28,
|
||||
"catalogBossMechanics": 25,
|
||||
"sourceBackedBossMechanics": 25,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 4,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 34,
|
||||
"nativeBindings": 26,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1173,11 +1241,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 68,
|
||||
"runtimeAttacks": 80,
|
||||
"genericDefaultAttacks": 32,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 36,
|
||||
"catalogBossMechanics": 16,
|
||||
"sourceBackedBossMechanics": 16,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 8,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 39,
|
||||
"nativeBindings": 32,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1211,6 +1284,11 @@
|
||||
"genericDefaultAttacks": 1,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 4,
|
||||
"nativeBindings": 1,
|
||||
"proceduralBindings": 0,
|
||||
@@ -1316,11 +1394,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 31,
|
||||
"runtimeAttacks": 48,
|
||||
"genericDefaultAttacks": 14,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 17,
|
||||
"catalogBossMechanics": 23,
|
||||
"sourceBackedBossMechanics": 23,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 8,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 20,
|
||||
"nativeBindings": 14,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1369,11 +1452,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 27,
|
||||
"runtimeAttacks": 33,
|
||||
"genericDefaultAttacks": 20,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 7,
|
||||
"catalogBossMechanics": 6,
|
||||
"sourceBackedBossMechanics": 6,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 4,
|
||||
"healthTriggeredBossMechanics": 1,
|
||||
"sourceBackedDamageFamilyAttacks": 11,
|
||||
"nativeBindings": 20,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1422,11 +1510,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 34,
|
||||
"runtimeAttacks": 40,
|
||||
"genericDefaultAttacks": 16,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 18,
|
||||
"catalogBossMechanics": 6,
|
||||
"sourceBackedBossMechanics": 6,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 2,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 22,
|
||||
"nativeBindings": 16,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1460,6 +1553,11 @@
|
||||
"genericDefaultAttacks": 14,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 9,
|
||||
"nativeBindings": 14,
|
||||
"proceduralBindings": 0,
|
||||
@@ -1545,10 +1643,15 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 38,
|
||||
"runtimeAttacks": 50,
|
||||
"genericDefaultAttacks": 12,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 23,
|
||||
"sourceBackedBossMechanics": 23,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 11,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 26,
|
||||
"nativeBindings": 12,
|
||||
"proceduralBindings": 0,
|
||||
@@ -1670,11 +1773,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 18,
|
||||
"runtimeAttacks": 52,
|
||||
"genericDefaultAttacks": 12,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 6,
|
||||
"catalogBossMechanics": 34,
|
||||
"sourceBackedBossMechanics": 34,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 9,
|
||||
"healthTriggeredBossMechanics": 16,
|
||||
"sourceBackedDamageFamilyAttacks": 13,
|
||||
"nativeBindings": 12,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1719,11 +1827,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 61,
|
||||
"runtimeAttacks": 65,
|
||||
"genericDefaultAttacks": 40,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 21,
|
||||
"catalogBossMechanics": 4,
|
||||
"sourceBackedBossMechanics": 4,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 3,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 25,
|
||||
"nativeBindings": 40,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1772,11 +1885,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 1,
|
||||
"runtimeAttacks": 5,
|
||||
"genericDefaultAttacks": 1,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 0,
|
||||
"catalogBossMechanics": 4,
|
||||
"sourceBackedBossMechanics": 4,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 3,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 2,
|
||||
"nativeBindings": 1,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1861,11 +1979,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 28,
|
||||
"runtimeAttacks": 31,
|
||||
"genericDefaultAttacks": 16,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 12,
|
||||
"catalogBossMechanics": 3,
|
||||
"sourceBackedBossMechanics": 3,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 2,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 14,
|
||||
"nativeBindings": 16,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1924,11 +2047,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 55,
|
||||
"runtimeAttacks": 64,
|
||||
"genericDefaultAttacks": 26,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 29,
|
||||
"catalogBossMechanics": 9,
|
||||
"sourceBackedBossMechanics": 9,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 1,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 34,
|
||||
"nativeBindings": 26,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -1962,6 +2090,11 @@
|
||||
"genericDefaultAttacks": 25,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 32,
|
||||
"nativeBindings": 25,
|
||||
"proceduralBindings": 0,
|
||||
@@ -2035,11 +2168,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 55,
|
||||
"runtimeAttacks": 63,
|
||||
"genericDefaultAttacks": 21,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 34,
|
||||
"catalogBossMechanics": 12,
|
||||
"sourceBackedBossMechanics": 12,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 3,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 36,
|
||||
"nativeBindings": 21,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -2073,6 +2211,11 @@
|
||||
"genericDefaultAttacks": 1,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 0,
|
||||
"nativeBindings": 1,
|
||||
"proceduralBindings": 0,
|
||||
@@ -2150,11 +2293,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 5,
|
||||
"runtimeAttacks": 25,
|
||||
"genericDefaultAttacks": 3,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 2,
|
||||
"catalogBossMechanics": 22,
|
||||
"sourceBackedBossMechanics": 22,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 8,
|
||||
"healthTriggeredBossMechanics": 4,
|
||||
"sourceBackedDamageFamilyAttacks": 11,
|
||||
"nativeBindings": 3,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -2203,11 +2351,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 2,
|
||||
"runtimeAttacks": 8,
|
||||
"genericDefaultAttacks": 1,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 1,
|
||||
"catalogBossMechanics": 7,
|
||||
"sourceBackedBossMechanics": 7,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 4,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 5,
|
||||
"nativeBindings": 1,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -2256,11 +2409,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimeAttacks": 1,
|
||||
"runtimeAttacks": 8,
|
||||
"genericDefaultAttacks": 1,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 0,
|
||||
"catalogBossMechanics": 7,
|
||||
"sourceBackedBossMechanics": 7,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 2,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 3,
|
||||
"nativeBindings": 1,
|
||||
"proceduralBindings": 0,
|
||||
"proxyBindings": 0,
|
||||
@@ -2294,6 +2452,11 @@
|
||||
"genericDefaultAttacks": 25,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 23,
|
||||
"nativeBindings": 25,
|
||||
"proceduralBindings": 0,
|
||||
@@ -2328,6 +2491,11 @@
|
||||
"genericDefaultAttacks": 0,
|
||||
"builderAuthoredAttacks": 0,
|
||||
"recipeAuthoredAttacks": 0,
|
||||
"catalogBossMechanics": 0,
|
||||
"sourceBackedBossMechanics": 0,
|
||||
"sourceGapBossMechanics": 0,
|
||||
"telegraphedBossMechanics": 0,
|
||||
"healthTriggeredBossMechanics": 0,
|
||||
"sourceBackedDamageFamilyAttacks": 0,
|
||||
"nativeBindings": 0,
|
||||
"proceduralBindings": 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RuneWaker gameplay content audit
|
||||
|
||||
Structural validation: **green**
|
||||
Structural validation: **green**
|
||||
Original-mechanics fidelity: **review-required**
|
||||
|
||||
This offline audit reads packaged recipes, snapshots, generated TypeScript, reports,
|
||||
@@ -9,11 +9,14 @@ 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 824 executable attacks.
|
||||
- 61 boss objectives and 1068 executable attacks.
|
||||
- Source-authored objective navigation: 0/0 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.
|
||||
|
||||
## Authentic source data currently used
|
||||
|
||||
@@ -21,17 +24,19 @@ mechanics evidence, manifests, and creature GLBs. It never connects to SQL Serve
|
||||
directions, model paths, source hashes, and boss spawn membership.
|
||||
- Navmesh-projected placements derived from each packaged environment anchor.
|
||||
- Original static meshes; original GR2 clips only where the manifest and GLB prove they exist.
|
||||
- Spell IDs, Lua hooks, AutoPlot names, and Lua signals are retained as evidence, not guessed.
|
||||
- Spell IDs, Lua hooks, AutoPlot names, and Lua signals feed the modular boss-mechanic catalog.
|
||||
|
||||
## HealerMan-generic or deferred
|
||||
|
||||
- 436 entities use generated Basic Attack/Corrosive Slam.
|
||||
- 0 attacks are builder exceptions without source mapping.
|
||||
- 388 executable attacks carry a conservative source-backed direct-damage family mapping.
|
||||
- Source-backed mapping preserves spell identity, damage family, school, range, and cooldown;
|
||||
HealerMan's 0.85 damage coefficient is intentionally generic, not claimed as source-exact.
|
||||
- Scaling, movement/leash, clustering, aggro, death state, loot, and objective ordering are generic.
|
||||
- Doors, phases, summons, patrols, resets, interactions, and source loot remain review-required.
|
||||
- 0 attacks are builder exceptions outside the modular boss catalog.
|
||||
- 483 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,
|
||||
DEF/MDEF, weapon power, resist, critical, level-delta, and global rate inputs are unavailable.
|
||||
- Scaling, movement/leash, clustering, aggro, death state, loot item stats/slots, and objective ordering are generic.
|
||||
- Unmapped complex doors, multi-actor phases, patrols, resets, and interactions remain review-required.
|
||||
- Procedural animation states are HealerMan-authored; native GLBs must provide idle, move,
|
||||
attack, cast, wound, and death families.
|
||||
|
||||
@@ -42,33 +47,33 @@ Animation columns are native/procedural/proxy entity bindings.
|
||||
| Dungeon | Source | Spawns | Entities | Bosses | Attacks | Spell templates | Scripted templates | N/P/X | Routes | Mechanics |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| aeternal-circle | 514 | 123 | 16 | 0 | 29 | 11 | 20 | 16/0/0 | 0/0 derived | review-required |
|
||||
| barren-caves | 63 | 48 | 7 | 1 | 14 | 7 | 3 | 7/0/0 | 1/1 derived | review-required |
|
||||
| cave-water-dragon | 144 | 97 | 7 | 2 | 9 | 4 | 5 | 7/0/0 | 1/2 derived | review-required |
|
||||
| demon-stronghold | 439 | 183 | 16 | 1 | 28 | 7 | 14 | 16/0/0 | 1/1 derived | review-required |
|
||||
| barren-caves | 63 | 48 | 7 | 1 | 15 | 7 | 3 | 7/0/0 | 1/1 derived | review-required |
|
||||
| cave-water-dragon | 144 | 97 | 7 | 2 | 20 | 4 | 5 | 7/0/0 | 1/2 derived | review-required |
|
||||
| demon-stronghold | 439 | 183 | 16 | 1 | 32 | 7 | 14 | 16/0/0 | 1/1 derived | review-required |
|
||||
| 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 | 15 | 0 | 11 | 15/0/0 | 8/8 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 |
|
||||
| hall-of-survivors | 363 | 162 | 40 | 9 | 74 | 23 | 25 | 39/1/0 | 9/9 derived | review-required |
|
||||
| hoto | 401 | 307 | 26 | 5 | 54 | 19 | 15 | 26/0/0 | 2/5 derived | review-required |
|
||||
| ice-dwarf-kingdom | 651 | 599 | 32 | 5 | 68 | 25 | 11 | 32/0/0 | 3/5 derived | 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 |
|
||||
| kal-turok-hive | 4 | 1 | 1 | 0 | 5 | 1 | 3 | 1/0/0 | 0/0 derived | review-required |
|
||||
| kalins-shrine | 289 | 165 | 14 | 5 | 31 | 13 | 17 | 14/0/0 | 0/5 derived | review-required |
|
||||
| kawaks-tomb | 293 | 117 | 20 | 1 | 27 | 13 | 24 | 20/0/0 | 1/1 derived | review-required |
|
||||
| kulech-bones-nest | 287 | 154 | 16 | 1 | 34 | 14 | 20 | 16/0/0 | 1/1 derived | review-required |
|
||||
| kalins-shrine | 289 | 165 | 14 | 5 | 48 | 13 | 17 | 14/0/0 | 0/5 derived | review-required |
|
||||
| kawaks-tomb | 293 | 117 | 20 | 1 | 33 | 13 | 24 | 20/0/0 | 1/1 derived | review-required |
|
||||
| kulech-bones-nest | 287 | 154 | 16 | 1 | 40 | 14 | 20 | 16/0/0 | 1/1 derived | review-required |
|
||||
| lair-demon-dragon | 532 | 103 | 14 | 0 | 23 | 9 | 10 | 14/0/0 | 0/0 derived | review-required |
|
||||
| mystic-altar | 398 | 251 | 12 | 3 | 38 | 11 | 15 | 12/0/0 | 3/3 derived | review-required |
|
||||
| paspers-shrine | 255 | 224 | 12 | 5 | 18 | 7 | 6 | 12/0/0 | 5/5 derived | review-required |
|
||||
| raksha-temple | 487 | 123 | 40 | 1 | 61 | 29 | 29 | 40/0/0 | 0/1 derived | review-required |
|
||||
| sand-vortex | 2 | 1 | 1 | 1 | 1 | 1 | 1 | 1/0/0 | 1/1 derived | review-required |
|
||||
| sardo-castle | 247 | 71 | 16 | 3 | 28 | 7 | 24 | 16/0/0 | 3/3 derived | review-required |
|
||||
| the-origin | 510 | 188 | 26 | 2 | 55 | 21 | 10 | 26/0/0 | 0/2 derived | review-required |
|
||||
| mystic-altar | 398 | 251 | 12 | 3 | 50 | 11 | 15 | 12/0/0 | 3/3 derived | review-required |
|
||||
| paspers-shrine | 255 | 224 | 12 | 5 | 52 | 7 | 6 | 12/0/0 | 5/5 derived | review-required |
|
||||
| raksha-temple | 487 | 123 | 40 | 1 | 65 | 29 | 29 | 40/0/0 | 0/1 derived | review-required |
|
||||
| sand-vortex | 2 | 1 | 1 | 1 | 5 | 1 | 1 | 1/0/0 | 1/1 derived | review-required |
|
||||
| sardo-castle | 247 | 71 | 16 | 3 | 31 | 7 | 24 | 16/0/0 | 3/3 derived | review-required |
|
||||
| the-origin | 510 | 188 | 26 | 2 | 64 | 21 | 10 | 26/0/0 | 0/2 derived | review-required |
|
||||
| tomb-seven-heroes | 439 | 240 | 25 | 0 | 57 | 18 | 24 | 25/0/0 | 0/0 derived | review-required |
|
||||
| treasure-trove | 685 | 497 | 21 | 3 | 55 | 20 | 17 | 21/0/0 | 0/3 derived | review-required |
|
||||
| treasure-trove | 685 | 497 | 21 | 3 | 63 | 20 | 17 | 21/0/0 | 0/3 derived | review-required |
|
||||
| warnorken-arena | 40 | 1 | 1 | 0 | 1 | 0 | 2 | 1/0/0 | 0/0 derived | review-required |
|
||||
| ystra-labyrinth-boss1 | 17 | 3 | 3 | 3 | 5 | 2 | 4 | 3/0/0 | 1/3 derived | review-required |
|
||||
| ystra-labyrinth-boss2 | 3 | 1 | 1 | 1 | 2 | 1 | 1 | 1/0/0 | 1/1 derived | review-required |
|
||||
| ystra-labyrinth-boss3 | 7 | 1 | 1 | 1 | 1 | 0 | 1 | 1/0/0 | 1/1 derived | review-required |
|
||||
| ystra-labyrinth-boss1 | 17 | 3 | 3 | 3 | 25 | 2 | 4 | 3/0/0 | 1/3 derived | review-required |
|
||||
| ystra-labyrinth-boss2 | 3 | 1 | 1 | 1 | 8 | 1 | 1 | 1/0/0 | 1/1 derived | review-required |
|
||||
| ystra-labyrinth-boss3 | 7 | 1 | 1 | 1 | 8 | 0 | 1 | 1/0/0 | 1/1 derived | review-required |
|
||||
| zurhidon-stronghold | 400 | 153 | 25 | 0 | 48 | 15 | 17 | 25/0/0 | 0/0 derived | review-required |
|
||||
| zurhidon-stronghold-124 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0/0/0 | 0/0 derived | review-required |
|
||||
|
||||
@@ -111,7 +116,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)
|
||||
|
||||
Unimplemented source mechanics: 357 templates preserve AutoPlot/Lua evidence, with executable generation deliberately disabled.
|
||||
Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported boss combat behavior is compiled into the modular catalog.
|
||||
|
||||
- death: 75
|
||||
- door: 22
|
||||
@@ -139,7 +144,6 @@ Unimplemented source mechanics: 357 templates preserve AutoPlot/Lua evidence, wi
|
||||
|
||||
- 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.
|
||||
- 436 executable attacks do not carry an audited source-spell mapping.
|
||||
- Boss objectives are spawn-authentic, but boss attacks and state machines remain generic.
|
||||
- Source-backed spell coefficients are HealerMan-generic; the original RuneWaker formula/stat scaling has not been reconstructed.
|
||||
- 4 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.
|
||||
@@ -0,0 +1,763 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"status": "portable-with-explicit-fallbacks",
|
||||
"generatedFrom": {
|
||||
"sourceFiles": [
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/npcobject.db",
|
||||
"sha256": "b627b6eb269c30576a6558000a31abe2db472689ef9a60cc1c1e20207abd69f1"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/treasureobject.db",
|
||||
"sha256": "6e7433190f4b9f49e3cea32e346128a57c62a5993dd3b98b0de1cd11152895a0"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/itemobject.db",
|
||||
"sha256": "00e082b4c18d561d29c893a6400bfe62e3daf90ae539728cb05d1c2893a69880"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/armorobject.db",
|
||||
"sha256": "a2f0017c4772165d562ebb08ca0ac51625cdc4453e3670a1501df7d741d2bad3"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/weaponobject.db",
|
||||
"sha256": "2bc28eebbe413bd6118f027e3fec1a3953cef371476937d0b65622c542971a67"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/cardobject.db",
|
||||
"sha256": "4d154fe733e5359ca9e40c0aefd458ace0cf109b1b01e20da0f6e0683966a59f"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/runeobject.db",
|
||||
"sha256": "8ea064e1ae8220a69e3c72bbd5b64dbd149738a2c86f1a5d51201a7f3a799f85"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/recipeobject.db",
|
||||
"sha256": "0f7d6ec38f8c52c403065d3f6f5b996117344c194ba42dd22f4d52061a6984e5"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/keyitemobject.db",
|
||||
"sha256": "ea06ed6dd9e4111cfd308b3d26d50c9e39a26ad358584db8dee6812e9a91358a"
|
||||
},
|
||||
{
|
||||
"file": "../rom-pvt-server-backups/Runewaker/Resource/data/string_enus.db",
|
||||
"sha256": "4fce021ba9ed110a213e08d3228b2de57cec66ddbbc8cdcf5753204874d9ab75"
|
||||
}
|
||||
],
|
||||
"npcDropAlgorithm": {
|
||||
"file": "SourceCodeVS2019/Server/NetGameServer/MainProc/Global.cpp",
|
||||
"firstRelevantLine": 4707,
|
||||
"rateDenominator": 100000
|
||||
},
|
||||
"treasureAlgorithm": {
|
||||
"file": "SourceCodeVS2019/Libraries/Union/RoleData/RD_ItemProc.cxx",
|
||||
"firstRelevantLine": 3033,
|
||||
"rateDenominator": 100000
|
||||
},
|
||||
"layout": "RuneWaker v0.20 fixed records; read-only export"
|
||||
},
|
||||
"runtimePolicy": {
|
||||
"rollsAreIndependent": true,
|
||||
"treasureOutcomesAreMutuallyExclusive": true,
|
||||
"unknownObjectsExecute": false,
|
||||
"luaPredicatesExecute": false,
|
||||
"sourceItemStatsInvented": false,
|
||||
"importedItemsUseGenericNonEquipableInventoryRepresentation": true
|
||||
},
|
||||
"leafCatalogs": [
|
||||
{
|
||||
"name": "itemobject.db",
|
||||
"count": 13431,
|
||||
"objectType": 2,
|
||||
"recordSize": 1776
|
||||
},
|
||||
{
|
||||
"name": "armorobject.db",
|
||||
"count": 15050,
|
||||
"objectType": 2,
|
||||
"recordSize": 1776
|
||||
},
|
||||
{
|
||||
"name": "weaponobject.db",
|
||||
"count": 4843,
|
||||
"objectType": 2,
|
||||
"recordSize": 1776
|
||||
},
|
||||
{
|
||||
"name": "cardobject.db",
|
||||
"count": 1765,
|
||||
"objectType": 2,
|
||||
"recordSize": 1776
|
||||
},
|
||||
{
|
||||
"name": "runeobject.db",
|
||||
"count": 1781,
|
||||
"objectType": 2,
|
||||
"recordSize": 1776
|
||||
},
|
||||
{
|
||||
"name": "recipeobject.db",
|
||||
"count": 3443,
|
||||
"objectType": 10,
|
||||
"recordSize": 416
|
||||
},
|
||||
{
|
||||
"name": "keyitemobject.db",
|
||||
"count": 10001,
|
||||
"objectType": 9,
|
||||
"recordSize": 196
|
||||
}
|
||||
],
|
||||
"bossCount": 56,
|
||||
"bossWithSourceRollsCount": 47,
|
||||
"portableBossCount": 34,
|
||||
"partialBossCount": 13,
|
||||
"noSourceDropsBossCount": 9,
|
||||
"blockedBossCount": 0,
|
||||
"executableOutcomeCount": 8741,
|
||||
"blockedOutcomeCount": 27,
|
||||
"bosses": [
|
||||
{
|
||||
"bossKey": "rw-barren-caves-101462",
|
||||
"dungeonId": "barren-caves",
|
||||
"sourceNpcId": 101462,
|
||||
"bossName": "Anglo",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 76,
|
||||
"blockedOutcomeCount": 3,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-cave-water-dragon-102623",
|
||||
"dungeonId": "cave-water-dragon",
|
||||
"sourceNpcId": 102623,
|
||||
"bossName": "Lytfir",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-cave-water-dragon-104161",
|
||||
"dungeonId": "cave-water-dragon",
|
||||
"sourceNpcId": 104161,
|
||||
"bossName": "Template 104161",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-demon-stronghold-102701",
|
||||
"dungeonId": "demon-stronghold",
|
||||
"sourceNpcId": 102701,
|
||||
"bossName": "Naos",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 228,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-fa-100070",
|
||||
"dungeonId": "forsaken-abbey",
|
||||
"sourceNpcId": 100070,
|
||||
"bossName": "Necromage",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 12,
|
||||
"executableOutcomeCount": 117,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-fa-100074",
|
||||
"dungeonId": "forsaken-abbey",
|
||||
"sourceNpcId": 100074,
|
||||
"bossName": "Demon Witch Ancalon",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 130,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-fa-100561",
|
||||
"dungeonId": "forsaken-abbey",
|
||||
"sourceNpcId": 100561,
|
||||
"bossName": "Flowing Chaos",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 127,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-fa-100562",
|
||||
"dungeonId": "forsaken-abbey",
|
||||
"sourceNpcId": 100562,
|
||||
"bossName": "Cruel Eater",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 12,
|
||||
"executableOutcomeCount": 131,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-fa-100563",
|
||||
"dungeonId": "forsaken-abbey",
|
||||
"sourceNpcId": 100563,
|
||||
"bossName": "Hollow Shell",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 129,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102395",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102395,
|
||||
"bossName": "Guldamor",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 228,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102421",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102421,
|
||||
"bossName": "Andriol",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 227,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102425",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102425,
|
||||
"bossName": "Zygro",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 229,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102430",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102430,
|
||||
"bossName": "Andaphelmor",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 14,
|
||||
"executableOutcomeCount": 232,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table requires unavailable Lua predicate star_WorldBattle_CheckBonus."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102452",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102452,
|
||||
"bossName": "Mantarick",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102475",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102475,
|
||||
"bossName": "Glamo",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 226,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102476",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102476,
|
||||
"bossName": "Divine Punishment Pursuer",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102560",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102560,
|
||||
"bossName": "Prison Pursuer",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hall-of-survivors-102643",
|
||||
"dungeonId": "hall-of-survivors",
|
||||
"sourceNpcId": 102643,
|
||||
"bossName": "Horror Seeker",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hoto-102021",
|
||||
"dungeonId": "hoto",
|
||||
"sourceNpcId": 102021,
|
||||
"bossName": "Jiasha",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 219,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hoto-102040",
|
||||
"dungeonId": "hoto",
|
||||
"sourceNpcId": 102040,
|
||||
"bossName": "Medusa",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 222,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table requires unavailable Lua predicate star_WorldBattle_CheckBonus."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hoto-102041",
|
||||
"dungeonId": "hoto",
|
||||
"sourceNpcId": 102041,
|
||||
"bossName": "Teeth of the Reef",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 217,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hoto-102046",
|
||||
"dungeonId": "hoto",
|
||||
"sourceNpcId": 102046,
|
||||
"bossName": "Bloodthirsty Claw",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 218,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-hoto-102063",
|
||||
"dungeonId": "hoto",
|
||||
"sourceNpcId": 102063,
|
||||
"bossName": "Geba",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 220,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ice-dwarf-kingdom-101268",
|
||||
"dungeonId": "ice-dwarf-kingdom",
|
||||
"sourceNpcId": 101268,
|
||||
"bossName": "Pangkor",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 9,
|
||||
"executableOutcomeCount": 210,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ice-dwarf-kingdom-101584",
|
||||
"dungeonId": "ice-dwarf-kingdom",
|
||||
"sourceNpcId": 101584,
|
||||
"bossName": "Thynos",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 211,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ice-dwarf-kingdom-101881",
|
||||
"dungeonId": "ice-dwarf-kingdom",
|
||||
"sourceNpcId": 101881,
|
||||
"bossName": "Icebound Guardian",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 188,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ice-dwarf-kingdom-101883",
|
||||
"dungeonId": "ice-dwarf-kingdom",
|
||||
"sourceNpcId": 101883,
|
||||
"bossName": "Crasset Hairyfoot",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 181,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ice-dwarf-kingdom-101884",
|
||||
"dungeonId": "ice-dwarf-kingdom",
|
||||
"sourceNpcId": 101884,
|
||||
"bossName": "Mole King",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 8,
|
||||
"executableOutcomeCount": 208,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kalins-shrine-101500",
|
||||
"dungeonId": "kalins-shrine",
|
||||
"sourceNpcId": 101500,
|
||||
"bossName": "Goddess of Art's Disciple",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 318,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kalins-shrine-101501",
|
||||
"dungeonId": "kalins-shrine",
|
||||
"sourceNpcId": 101501,
|
||||
"bossName": "Yusalien",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 14,
|
||||
"executableOutcomeCount": 326,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kalins-shrine-101502",
|
||||
"dungeonId": "kalins-shrine",
|
||||
"sourceNpcId": 101502,
|
||||
"bossName": "Locatha",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 325,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kalins-shrine-101503",
|
||||
"dungeonId": "kalins-shrine",
|
||||
"sourceNpcId": 101503,
|
||||
"bossName": "Ensia",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 325,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kalins-shrine-101507",
|
||||
"dungeonId": "kalins-shrine",
|
||||
"sourceNpcId": 101507,
|
||||
"bossName": "Iron Rune Warrior",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 323,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kawaks-tomb-104228",
|
||||
"dungeonId": "kawaks-tomb",
|
||||
"sourceNpcId": 104228,
|
||||
"bossName": "Linobia",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-kulech-bones-nest-107306",
|
||||
"dungeonId": "kulech-bones-nest",
|
||||
"sourceNpcId": 107306,
|
||||
"bossName": "Template 107306",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-mystic-altar-100138",
|
||||
"dungeonId": "mystic-altar",
|
||||
"sourceNpcId": 100138,
|
||||
"bossName": "Cursed lron Golem",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 128,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-mystic-altar-100154",
|
||||
"dungeonId": "mystic-altar",
|
||||
"sourceNpcId": 100154,
|
||||
"bossName": "Razeela",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 142,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-mystic-altar-100611",
|
||||
"dungeonId": "mystic-altar",
|
||||
"sourceNpcId": 100611,
|
||||
"bossName": "Dorlos",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 124,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-paspers-shrine-100794",
|
||||
"dungeonId": "paspers-shrine",
|
||||
"sourceNpcId": 100794,
|
||||
"bossName": "Blackhorn Hafiz",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 128,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-paspers-shrine-101018",
|
||||
"dungeonId": "paspers-shrine",
|
||||
"sourceNpcId": 101018,
|
||||
"bossName": "Blackhorn Afalen",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 109,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-paspers-shrine-101019",
|
||||
"dungeonId": "paspers-shrine",
|
||||
"sourceNpcId": 101019,
|
||||
"bossName": "Blackhorn Bareth",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 103,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-paspers-shrine-101020",
|
||||
"dungeonId": "paspers-shrine",
|
||||
"sourceNpcId": 101020,
|
||||
"bossName": "Blackhorn Nisorn",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 105,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-paspers-shrine-101021",
|
||||
"dungeonId": "paspers-shrine",
|
||||
"sourceNpcId": 101021,
|
||||
"bossName": "Blackhorn Dorglas",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 112,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-raksha-temple-103946",
|
||||
"dungeonId": "raksha-temple",
|
||||
"sourceNpcId": 103946,
|
||||
"bossName": "Shadoj",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 7,
|
||||
"executableOutcomeCount": 71,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-sand-vortex-100645",
|
||||
"dungeonId": "sand-vortex",
|
||||
"sourceNpcId": 100645,
|
||||
"bossName": "Sandlord Sesphiroth",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 9,
|
||||
"executableOutcomeCount": 101,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-sardo-castle-105701",
|
||||
"dungeonId": "sardo-castle",
|
||||
"sourceNpcId": 105701,
|
||||
"bossName": "Undead Guard",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 7,
|
||||
"executableOutcomeCount": 91,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-the-origin-102307",
|
||||
"dungeonId": "the-origin",
|
||||
"sourceNpcId": 102307,
|
||||
"bossName": "Holy Tree Falynum",
|
||||
"status": "no-source-drops",
|
||||
"sourceRollCount": 0,
|
||||
"executableOutcomeCount": 0,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-the-origin-102349",
|
||||
"dungeonId": "the-origin",
|
||||
"sourceNpcId": 102349,
|
||||
"bossName": "Life Leecher",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 12,
|
||||
"executableOutcomeCount": 279,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-treasure-trove-101533",
|
||||
"dungeonId": "treasure-trove",
|
||||
"sourceNpcId": 101533,
|
||||
"bossName": "Boulder Sidaar",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 313,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-treasure-trove-101534",
|
||||
"dungeonId": "treasure-trove",
|
||||
"sourceNpcId": 101534,
|
||||
"bossName": "Lookout Luke",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 11,
|
||||
"executableOutcomeCount": 315,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-treasure-trove-101535",
|
||||
"dungeonId": "treasure-trove",
|
||||
"sourceNpcId": 101535,
|
||||
"bossName": "Snow Blake",
|
||||
"status": "partial",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 322,
|
||||
"blockedOutcomeCount": 2,
|
||||
"blockedReasons": [
|
||||
"Treasure table has no positive, resolvable outcomes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ystra-labyrinth-boss1-101432",
|
||||
"dungeonId": "ystra-labyrinth-boss1",
|
||||
"sourceNpcId": 101432,
|
||||
"bossName": "Rune Guardian Leader",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 12,
|
||||
"executableOutcomeCount": 92,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ystra-labyrinth-boss1-101433",
|
||||
"dungeonId": "ystra-labyrinth-boss1",
|
||||
"sourceNpcId": 101433,
|
||||
"bossName": "Raichika the Destroyer",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 13,
|
||||
"executableOutcomeCount": 125,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ystra-labyrinth-boss1-102195",
|
||||
"dungeonId": "ystra-labyrinth-boss1",
|
||||
"sourceNpcId": 102195,
|
||||
"bossName": "Specter Guardian Creator",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 8,
|
||||
"executableOutcomeCount": 84,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ystra-labyrinth-boss2-102196",
|
||||
"dungeonId": "ystra-labyrinth-boss2",
|
||||
"sourceNpcId": 102196,
|
||||
"bossName": "Rune Guardian Leader",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 99,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
},
|
||||
{
|
||||
"bossKey": "rw-ystra-labyrinth-boss3-102197",
|
||||
"dungeonId": "ystra-labyrinth-boss3",
|
||||
"sourceNpcId": 102197,
|
||||
"bossName": "Raichika the Destroyer",
|
||||
"status": "portable",
|
||||
"sourceRollCount": 10,
|
||||
"executableOutcomeCount": 107,
|
||||
"blockedOutcomeCount": 0,
|
||||
"blockedReasons": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# RuneWaker loot import report
|
||||
|
||||
Generated from the preserved fixed-record databases. Source rates use the original 1-in-100,000 denominator.
|
||||
|
||||
- Boss templates audited: 56
|
||||
- Bosses with source rolls: 47
|
||||
- Fully portable bosses: 34
|
||||
- Partially portable bosses: 13
|
||||
- Bosses with no authored drop slots: 9
|
||||
- Executable item outcomes: 8741
|
||||
- Blocked outcomes: 27
|
||||
|
||||
Unknown object relationships and Lua-gated treasure paths fail closed. Imported source items are collected as named, non-equipable generic items because the RuneWaker item-stat and equipment-slot conversion is not yet proven.
|
||||
|
||||
| Dungeon | Boss | Status | Rolls | Executable | Blocked | Blocker |
|
||||
| --- | --- | --- | ---: | ---: | ---: | --- |
|
||||
| barren-caves | Anglo | partial | 13 | 76 | 3 | Treasure table has no positive, resolvable outcomes. |
|
||||
| cave-water-dragon | Lytfir | no-source-drops | 0 | 0 | 0 | — |
|
||||
| cave-water-dragon | Template 104161 | no-source-drops | 0 | 0 | 0 | — |
|
||||
| demon-stronghold | Naos | portable | 10 | 228 | 0 | — |
|
||||
| forsaken-abbey | Necromage | portable | 12 | 117 | 0 | — |
|
||||
| forsaken-abbey | Demon Witch Ancalon | partial | 13 | 130 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| forsaken-abbey | Flowing Chaos | portable | 11 | 127 | 0 | — |
|
||||
| forsaken-abbey | Cruel Eater | partial | 12 | 131 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| forsaken-abbey | Hollow Shell | portable | 11 | 129 | 0 | — |
|
||||
| hall-of-survivors | Guldamor | portable | 10 | 228 | 0 | — |
|
||||
| hall-of-survivors | Andriol | portable | 10 | 227 | 0 | — |
|
||||
| hall-of-survivors | Zygro | portable | 10 | 229 | 0 | — |
|
||||
| hall-of-survivors | Andaphelmor | partial | 14 | 232 | 2 | Treasure table requires unavailable Lua predicate star_WorldBattle_CheckBonus. |
|
||||
| hall-of-survivors | Mantarick | no-source-drops | 0 | 0 | 0 | — |
|
||||
| hall-of-survivors | Glamo | portable | 10 | 226 | 0 | — |
|
||||
| hall-of-survivors | Divine Punishment Pursuer | no-source-drops | 0 | 0 | 0 | — |
|
||||
| hall-of-survivors | Prison Pursuer | no-source-drops | 0 | 0 | 0 | — |
|
||||
| hall-of-survivors | Horror Seeker | no-source-drops | 0 | 0 | 0 | — |
|
||||
| hoto | Jiasha | portable | 10 | 219 | 0 | — |
|
||||
| hoto | Medusa | partial | 13 | 222 | 2 | Treasure table requires unavailable Lua predicate star_WorldBattle_CheckBonus. |
|
||||
| hoto | Teeth of the Reef | portable | 10 | 217 | 0 | — |
|
||||
| hoto | Bloodthirsty Claw | portable | 10 | 218 | 0 | — |
|
||||
| hoto | Geba | portable | 10 | 220 | 0 | — |
|
||||
| ice-dwarf-kingdom | Pangkor | portable | 9 | 210 | 0 | — |
|
||||
| ice-dwarf-kingdom | Thynos | portable | 10 | 211 | 0 | — |
|
||||
| ice-dwarf-kingdom | Icebound Guardian | portable | 10 | 188 | 0 | — |
|
||||
| ice-dwarf-kingdom | Crasset Hairyfoot | portable | 10 | 181 | 0 | — |
|
||||
| ice-dwarf-kingdom | Mole King | portable | 8 | 208 | 0 | — |
|
||||
| kalins-shrine | Goddess of Art's Disciple | partial | 13 | 318 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| kalins-shrine | Yusalien | partial | 14 | 326 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| kalins-shrine | Locatha | partial | 11 | 325 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| kalins-shrine | Ensia | partial | 11 | 325 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| kalins-shrine | Iron Rune Warrior | partial | 11 | 323 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| kawaks-tomb | Linobia | no-source-drops | 0 | 0 | 0 | — |
|
||||
| kulech-bones-nest | Template 107306 | no-source-drops | 0 | 0 | 0 | — |
|
||||
| mystic-altar | Cursed lron Golem | portable | 10 | 128 | 0 | — |
|
||||
| mystic-altar | Razeela | portable | 10 | 142 | 0 | — |
|
||||
| mystic-altar | Dorlos | portable | 11 | 124 | 0 | — |
|
||||
| paspers-shrine | Blackhorn Hafiz | portable | 11 | 128 | 0 | — |
|
||||
| paspers-shrine | Blackhorn Afalen | portable | 10 | 109 | 0 | — |
|
||||
| paspers-shrine | Blackhorn Bareth | portable | 10 | 103 | 0 | — |
|
||||
| paspers-shrine | Blackhorn Nisorn | portable | 10 | 105 | 0 | — |
|
||||
| paspers-shrine | Blackhorn Dorglas | portable | 13 | 112 | 0 | — |
|
||||
| raksha-temple | Shadoj | portable | 7 | 71 | 0 | — |
|
||||
| sand-vortex | Sandlord Sesphiroth | portable | 9 | 101 | 0 | — |
|
||||
| sardo-castle | Undead Guard | portable | 7 | 91 | 0 | — |
|
||||
| the-origin | Holy Tree Falynum | no-source-drops | 0 | 0 | 0 | — |
|
||||
| the-origin | Life Leecher | portable | 12 | 279 | 0 | — |
|
||||
| treasure-trove | Boulder Sidaar | partial | 10 | 313 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| treasure-trove | Lookout Luke | partial | 11 | 315 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| treasure-trove | Snow Blake | partial | 13 | 322 | 2 | Treasure table has no positive, resolvable outcomes. |
|
||||
| ystra-labyrinth-boss1 | Rune Guardian Leader | portable | 12 | 92 | 0 | — |
|
||||
| ystra-labyrinth-boss1 | Raichika the Destroyer | portable | 13 | 125 | 0 | — |
|
||||
| ystra-labyrinth-boss1 | Specter Guardian Creator | portable | 8 | 84 | 0 | — |
|
||||
| ystra-labyrinth-boss2 | Rune Guardian Leader | portable | 10 | 99 | 0 | — |
|
||||
| ystra-labyrinth-boss3 | Raichika the Destroyer | portable | 10 | 107 | 0 | — |
|
||||
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,9 @@ against every raw Recast surface component and against the retained runtime surf
|
||||
The current boss sequence is HealerMan's distance-from-entrance fallback, not a claimed
|
||||
source-authored order. It does not create links or infer doors/teleports.
|
||||
|
||||
Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
Unreachable currently ordered legs analyzed: **19** across **8** dungeons.
|
||||
Source-authored fixed orders implemented by the audited packages: **0**.
|
||||
Authored off-mesh links added by this review: **0**.
|
||||
|
||||
| Dungeon | Ordered leg | Geometry classification | From raw comp/dist | To raw comp/dist | Retained distances |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
@@ -20,9 +22,9 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
| kalins-shrine | Yusalien -> Locatha | separate-recast-surface-components | 2/0m | 12/0m | 245.55m -> 122.731m |
|
||||
| kalins-shrine | Locatha -> Goddess of Art's Disciple | separate-recast-surface-components | 12/0m | 3/0m | 122.731m -> 12.741m |
|
||||
| kalins-shrine | Goddess of Art's Disciple -> Ensia | separate-recast-surface-components | 3/0m | 0/0m | 12.741m -> 0m |
|
||||
| raksha-temple | entrance -> Shadoj | missing-primary-environment-geometry-source-support-pad | 41/0m | 165/0m | 0.203m -> 200.079m |
|
||||
| the-origin | entrance -> Life Leecher | missing-primary-environment-geometry-source-support-pad | 106/0m | 302/0m | 466.405m -> 234.143m |
|
||||
| the-origin | Life Leecher -> Holy Tree Falynum | missing-primary-environment-geometry-source-support-pad | 302/0m | 305/0m | 234.143m -> 361.263m |
|
||||
| raksha-temple | entrance -> Shadoj | objective-or-entrance-outside-all-navigation | 6069/0m | 2982/7.159m | 578.214m -> 699.329m |
|
||||
| the-origin | entrance -> Life Leecher | separate-recast-surface-components | 4276/0m | 2868/1.099m | 456.333m -> 258.631m |
|
||||
| the-origin | Life Leecher -> Holy Tree Falynum | separate-recast-surface-components | 2868/1.099m | 4857/5.632m | 258.631m -> 559.406m |
|
||||
| treasure-trove | entrance -> Lookout Luke | missing-primary-environment-geometry-source-support-pad | 487/0m | 854/0m | 0.184m -> 519.5m |
|
||||
| treasure-trove | Lookout Luke -> Snow Blake | missing-primary-environment-geometry-source-support-pad | 854/0m | 490/0m | 519.5m -> 525.287m |
|
||||
| treasure-trove | Snow Blake -> Boulder Sidaar | missing-primary-environment-geometry-source-support-pad | 490/0m | 489/0m | 525.287m -> 565.311m |
|
||||
@@ -35,8 +37,8 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
- hoto: 260 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 5777 welded nodes discarded by largest-surface retention.
|
||||
- ice-dwarf-kingdom: 599 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 6237 welded nodes discarded by largest-surface retention.
|
||||
- kalins-shrine: 93 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 4858 welded nodes discarded by largest-surface retention.
|
||||
- raksha-temple: 167 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 1262 welded nodes discarded by largest-surface retention.
|
||||
- the-origin: 306 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 1678 welded nodes discarded by largest-surface retention.
|
||||
- raksha-temple: 13137 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 180838 welded nodes discarded by largest-surface retention.
|
||||
- the-origin: 6736 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 97103 welded nodes discarded by largest-surface retention.
|
||||
- treasure-trove: 1025 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 16502 welded nodes discarded by largest-surface retention.
|
||||
- ystra-labyrinth-boss1: 36 raw vertex-connected surfaces; 1 shared-edge corridors inside the retained surface; 901 welded nodes discarded by largest-surface retention.
|
||||
|
||||
@@ -46,7 +48,12 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
|
||||
- Disposition: gated-wave-and-door-review.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: partial-gate-evidence-insufficient-for-complete-order.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: Template 104161 runs the ten-add water-elemental controller, creates a blocking door, and removes it only after the counter reaches ten. Lytfir is on a lower isolated surface, so a permanent link would bypass the preserved gate/drop sequence.
|
||||
- Remaining requirement: encounter-gate-state.
|
||||
- Remaining requirement: source-objective-review.
|
||||
- Remaining requirement: dynamic-navigation-transition.
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01562.lua:1
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01562.lua:21
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01562.lua:42
|
||||
@@ -55,7 +62,11 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
|
||||
- Disposition: source-order-required-before-linking.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: source-objective-review-required.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: The distance fallback crosses the same 6.65m vertical surface break three times. Preserved local configuration supplies boss anchors but no authoritative encounter order, so these heuristic legs cannot justify a link.
|
||||
- Remaining requirement: source-objective-review.
|
||||
- Remaining requirement: navigation-geometry-review.
|
||||
- Evidence: scripts/runewaker-pipeline/build-population.mjs:634
|
||||
- Evidence: scripts/runewaker-pipeline/recipes/hoto-population.json
|
||||
|
||||
@@ -63,7 +74,12 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
|
||||
- Disposition: door-gated-room-review.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: partial-gate-evidence-insufficient-for-complete-order.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: Crasset's isolated surface is separated by a 1.64m/0.60m gap, but his preserved death hook creates an orb that finds and hides door 112321. A permanent bidirectional link would erase that gate state.
|
||||
- Remaining requirement: encounter-gate-state.
|
||||
- Remaining requirement: source-objective-review.
|
||||
- Remaining requirement: dynamic-navigation-transition.
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01356.lua:16
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01356.lua:36
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01356.lua:46
|
||||
@@ -73,30 +89,48 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
|
||||
- Disposition: teleport-room-mechanic.
|
||||
- Safe automatic link: no.
|
||||
- Finding: The preserved server scripts register six Kalin locations as separate keeper rooms and advance with a group teleport. The vertical component gaps are therefore encounter state, not wall seams.
|
||||
- Objective order: source-randomized-not-linear.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: The preserved server scripts register six Kalin locations as separate keeper rooms, select the next uncleared room randomly, and advance with a group teleport. The vertical component gaps are encounter state, not wall seams, and a fixed objective order would contradict the source.
|
||||
- Remaining requirement: nonlinear-objectives.
|
||||
- Remaining requirement: encounter-teleport-state.
|
||||
- Remaining requirement: dynamic-navigation-transition.
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:713
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:1298
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:1363
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:1354
|
||||
|
||||
### raksha-temple
|
||||
|
||||
- Disposition: missing-modular-environment-assembly.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: not-applicable-single-objective.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: The environment recipe explicitly adds isolated source-anchor support pads because the single primary ROS does not cover the modular WDB combat anchors. Secondary environment pieces must be assembled before route authoring.
|
||||
- Remaining requirement: modular-environment-assembly.
|
||||
- Evidence: scripts/runewaker-pipeline/recipes/raksha-temple.json:16
|
||||
|
||||
### the-origin
|
||||
|
||||
- Disposition: missing-modular-environment-assembly.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: source-objective-review-required.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: Both objectives and the entrance sit on isolated source-anchor support pads. The WDB contains many separately placed cathedral, slope, bridge, and connector ROS pieces that the primary-only environment package does not yet assemble.
|
||||
- Remaining requirement: modular-environment-assembly.
|
||||
- Remaining requirement: source-objective-review.
|
||||
- Evidence: scripts/runewaker-pipeline/recipes/the-origin.json:16
|
||||
|
||||
### treasure-trove
|
||||
|
||||
- Disposition: missing-modular-environment-plus-bridge-door-mechanics.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: partial-gate-evidence-insufficient-for-complete-order.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: Boss anchors are isolated support pads, while preserved Lua requires a boss-enabled drawbridge switch and a key-gated hold door. The modular environment and stateful bridge/door must precede navigation links.
|
||||
- Remaining requirement: modular-environment-assembly.
|
||||
- Remaining requirement: encounter-gate-state.
|
||||
- Remaining requirement: source-objective-review.
|
||||
- Remaining requirement: dynamic-navigation-transition.
|
||||
- Evidence: scripts/runewaker-pipeline/recipes/treasure-trove.json:16
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01159.lua:1
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/01159.lua:38
|
||||
@@ -106,9 +140,29 @@ Unreachable heuristic legs analyzed: **19** across **8** dungeons.
|
||||
|
||||
- Disposition: separate-authored-room-placements.
|
||||
- Safe automatic link: no.
|
||||
- Objective order: independent-room-order-unresolved.
|
||||
- Current package order: healerMan-distance-derived (non-authoritative).
|
||||
- Finding: The package intentionally clones three distinct WDB room placements hundreds of meters apart; the preserved travel catalog also treats the boss-room destinations separately. Connecting them through open space would invent geometry.
|
||||
- Remaining requirement: encounter-teleport-state.
|
||||
- Remaining requirement: source-objective-review.
|
||||
- Remaining requirement: dynamic-navigation-transition.
|
||||
- Evidence: scripts/runewaker-pipeline/recipes/ystra-labyrinth-boss1.json:16
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:1423
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:1425
|
||||
- Evidence: ../rom-pvt-server-backups/Runewaker/Resource/luascript/03246.lua:1426
|
||||
|
||||
## Resolution boundary
|
||||
|
||||
No off-mesh link was emitted. Every failed leg is either controlled by encounter state,
|
||||
depends on environment geometry that is not yet in the runtime package, represents
|
||||
separate authored rooms, or still lacks a complete source-authored boss sequence.
|
||||
A static link would let companions bypass a door, bridge, wave, or teleport mechanic.
|
||||
The structured requirement counts below are deterministic and identify the remaining work.
|
||||
|
||||
- dynamic-navigation-transition: 5 dungeon(s).
|
||||
- encounter-gate-state: 3 dungeon(s).
|
||||
- encounter-teleport-state: 2 dungeon(s).
|
||||
- modular-environment-assembly: 3 dungeon(s).
|
||||
- navigation-geometry-review: 1 dungeon(s).
|
||||
- nonlinear-objectives: 1 dungeon(s).
|
||||
- source-objective-review: 6 dungeon(s).
|
||||
Loaded 100 of 402 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user