4 Commits
Author SHA1 Message Date
phenom c245a199b6 Release Healer Man 0.1.8 2026-08-27 13:29:58 -04:00
phenom eca6b5db9b Release Healer Man 0.1.7 2026-08-22 15:53:43 -04:00
phenom b12fb84859 Release Healer Man 0.1.6 2026-08-20 14:05:50 -04:00
phenom 55cfd43d66 Release Healer Man 0.1.5 2026-08-18 16:22:19 -04:00
303 changed files with 24019 additions and 1079 deletions
+10
View File
@@ -0,0 +1,10 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{bat,cmd}]
end_of_line = crlf
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
+6
View File
@@ -0,0 +1,6 @@
{
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true
}
+7 -1
View File
@@ -2,6 +2,13 @@
These rules apply to every change under this project root. These rules apply to every change under this project root.
## Text-file hygiene
- Every text file must end with exactly one newline, with no extra blank line
at EOF and no trailing whitespace.
- Before handing off changes, run `npm run release:check`. This check includes
untracked files; `git diff --check` alone does not.
## Workspace and reference boundaries ## Workspace and reference boundaries
- The active project root is `D:\Projects\HealerMan`. All implementation, - The active project root is `D:\Projects\HealerMan`. All implementation,
@@ -44,4 +51,3 @@ These rules apply to every change under this project root.
- `document.pointerLockElement` remains `null`; - `document.pointerLockElement` remains `null`;
- releasing right mouse stops camera motion; - releasing right mouse stops camera motion;
- no browser mouse-capture prompt appears. - no browser mouse-capture prompt appears.
+2 -1
View File
@@ -185,6 +185,7 @@ on the development PC. Stop if any test or build fails:
```powershell ```powershell
Set-Location F:\Projects\HealerMan Set-Location F:\Projects\HealerMan
npm run release:check
$VersionName = '0.1.3' $VersionName = '0.1.3'
$VersionCode = 1003 $VersionCode = 1003
@@ -206,8 +207,8 @@ npm run build
npm test -- --run src/avatar npm test -- --run src/avatar
npx vitest run src/game/inputManager.test.ts src/game/inputMath.test.ts npx vitest run src/game/inputManager.test.ts src/game/inputMath.test.ts
npm run release:check
git status --short git status --short
git diff --check
git add -A git add -A
git status --short git status --short
git diff --cached --stat git diff --cached --stat
+65
View File
@@ -4,6 +4,41 @@ This file is the durable record of runtime behavior, regressions, and the
evidence required before calling a visual bug fixed. Update it whenever scene, evidence required before calling a visual bug fixed. Update it whenever scene,
camera, input, physics, mob-rendering, or asset-loading code changes. camera, input, physics, mob-rendering, or asset-loading code changes.
## 2026-08-18 GM Manastorm administration - COMPLETE
- Server-authorized `Phenom` access, revisioned SQLite configuration, safe
all-Needs-tested defaults, public/offline caching, optimistic conflicts, and
denial/revision logging were exercised through the production HTTP surface.
- The GM hub exposed all 406 deduplicated boss records, readiness and runnable
state, pool filters, six global switches, dungeon entry, placement editing,
and GM tests. A fresh revision correctly reported zero Ready bosses.
- Live Wailing Caverns authoring captured and nudged the shared group anchor,
displayed derived party, boss, portal, and exact linked-mob data, and saved a
three-mob / two-kill Chaotic Link stage. The desktop Save control was moved
above the Tactical control after the playtest found their hit targets
overlapping; a normal semantic click then published the next revision.
- Promoting an imported encounter directly to Ready initially exposed missing
stage materialization at the server boundary. The readiness transition now
persists its complete inherited stage. Cross-mode duplicates also merge
their mode memberships, so a boss imported for both leveling and end-game is
represented once and remains eligible in both modes.
- Live pool acceptance promoted Lady Anacondra and Burning Felguard, entered a
level-1 public Manastorm, then marked the active Lady Anacondra record Not
ready from a parallel GM client. The existing run retained Lady Anacondra and
its two-stack Chaotic Link snapshot; after reload, the next run selected only
Burning Felguard. This verified new-run publication and active-run
immutability together.
- Desktop and Thor-preview passes rendered the DOM editor over the dungeon,
retained camera/game separation while the panel was open, and exposed the GM
pause-menu entry. Console inspection found no errors and only the existing
third-party initialization deprecation warning.
- Verification passed: server **7/7**, full Vitest **121 files / 699 tests**,
TypeScript, production build, and the KTX2 audit of 11,839 compressed textures
with zero fallbacks. The build retained the existing informational
large-chunk advisory.
- Evidence: `playtest-artifacts/2026-08-18-gm-manastorm-admin/desktop-admin-hub.png`,
`desktop-world-editor-final.png`, and `thor-world-editor.png`.
## 2026-08-17 authentic combat VFX and audio - COMPLETE ## 2026-08-17 authentic combat VFX and audio - COMPLETE
- Player, party, and enemy combat actions now publish transient presentation - Player, party, and enemy combat actions now publish transient presentation
@@ -1386,3 +1421,33 @@ Vite's existing large-chunk advisory remains informational.
- Verification: 38 focused party/pathing/jump/animation tests passed, all 28 - Verification: 38 focused party/pathing/jump/animation tests passed, all 28
avatar tests passed, and `npm run build` completed successfully with 954 avatar tests passed, and `npm run build` completed successfully with 954
modules transformed. The existing large-chunk advisory remains informational. modules transformed. The existing large-chunk advisory remains informational.
# 2026-08-20 — shared online dungeon session with AI fill
- Ran two signed-in production clients against the local account server on
separate browser origins. **SharedLeader / Bulwarka** invited
**SharedMember / Mendara**; the accepted group roster showed both selected
characters and their Tank and Healer roles as ready.
- The leader launched Wailing Caverns with **Fill open slots with AI** enabled.
Both clients automatically entered the same activity, each rendered the
other player through the normal equipped `CharacterModel` path, and each
showed a live, targetable remote party frame. Three AI companions filled the
remaining slots, producing the expected five-member party on both clients.
- The reciprocal scene captures showed Bulwarka and Mendara at the same shared
dungeon position with matching environment, objective, AI roster, mob state,
and remote role labels. The leader client continued encounter simulation;
the member client received the synchronized mob and AI state without running
a second competing world simulation.
- From the member client, selected Bulwarka's online party frame and completed
a Lesser Heal cast. The member's mana/resource change propagated to the
leader client and both clients retained the same live remote health state.
Enemy combat and AI resource/health changes also continued to propagate while
the two clients remained connected.
- Evidence reviewed during the browser run: invitation/group roster DOM,
coordinated-launch DOM from both clients, reciprocal full-window dungeon
captures, and before/cast-complete remote-healing DOM snapshots. No missing
character-body or equipment fallback appeared in either dungeon capture.
- Verification: server/API tests passed all 11 cases, the complete Vitest suite
passed all 755 cases across 132 files (including the 28 avatar cases), and the
TypeScript production build completed successfully with 983 modules
transformed. The existing large-chunk advisory remains informational.
+20 -2
View File
@@ -18,6 +18,21 @@ approval before it commits, pushes `main`, builds the signed APK, and publishes
the Gitea release. Signing passwords and the Gitea token are passed to the the Gitea release. Signing passwords and the Gitea token are passed to the
child process in memory rather than on its command line. child process in memory rather than on its command line.
Before changing versions or running the longer test/build sequence, the manager
runs an isolated release-candidate check. It validates tracked and untracked
files without changing the real Git index, so extra blank lines at EOF and
other whitespace errors fail immediately. Run the same fast check at any time:
```powershell
npm run release:check
```
Editors should honor the repository's `.editorconfig`. The checked-in VS Code
settings also trim extra final newlines whenever a file is saved.
Git may print a one-time CRLF-to-LF conversion warning when an existing Windows
working copy is first staged under `.gitattributes`; that warning is not a
release failure, and subsequent repository text stays consistently LF.
The local release key is `F:\secure\healer-man-release.jks`. When the companion The local release key is `F:\secure\healer-man-release.jks`. When the companion
`healer-man-release-password.dpapi` file is present, blank signing-password `healer-man-release-password.dpapi` file is present, blank signing-password
fields are filled from that Windows user-encrypted credential in memory. The fields are filled from that Windows user-encrypted credential in memory. The
@@ -254,11 +269,14 @@ npm run dev
First check for whitespace errors and review the final file list: First check for whitespace errors and review the final file list:
```powershell ```powershell
git diff --check npm run release:check
git status --short git status --short
git diff --stat git diff --stat
``` ```
Use `npm run release:check` before the longer release validation. Unlike
`git diff --check`, it includes new, untracked files.
Stage the intended release: Stage the intended release:
```powershell ```powershell
@@ -520,7 +538,7 @@ Set-Location F:\Projects\HealerMan
npm run content:test npm run content:test
npm run build npm run build
git status --short git status --short
git diff --check npm run release:check
git add -A git add -A
git status --short git status --short
git diff --cached --stat git diff --cached --stat
File diff suppressed because it is too large Load Diff
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/TempestKeepArcane", "mapDirectory": "World/Maps/TempestKeepArcane",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/AuchindounDraenei", "mapDirectory": "World/Maps/AuchindounDraenei",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "burning-crusade", "expansion": "burning-crusade",
"extraction": { "extraction": {
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/Blackfathom", "mapDirectory": "World/Maps/Blackfathom",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [20, 30]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [20, 30]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/BlackrockDepths", "mapDirectory": "World/Maps/BlackrockDepths",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/BlackRockSpire", "mapDirectory": "World/Maps/BlackRockSpire",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/HellfireDemon", "mapDirectory": "World/Maps/HellfireDemon",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/TempestKeepAtrium", "mapDirectory": "World/Maps/TempestKeepAtrium",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
+2
View File
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/DireMaul", "mapDirectory": "World/Maps/DireMaul",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source-transform",
"coordinateTransform": {"origin": [0, 0, 0], "yawDegrees": 180},
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "burning-crusade", "expansion": "burning-crusade",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/GnomeragonInstance", "mapDirectory": "World/Maps/GnomeragonInstance",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/HellfireRampart", "mapDirectory": "World/Maps/HellfireRampart",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source-transform",
"coordinateTransform": {"origin": [1262.4427, -60.8902, 1622.5397], "yawDegrees": 262.143},
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/Sunwell5ManFix", "mapDirectory": "World/Maps/Sunwell5ManFix",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [70, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [70, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/AuchindounEthereal", "mapDirectory": "World/Maps/AuchindounEthereal",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/Mauradon", "mapDirectory": "World/Maps/Mauradon",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 55]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 55]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/TempestKeepFactory", "mapDirectory": "World/Maps/TempestKeepFactory",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/OrgrimmarInstance", "mapDirectory": "World/Maps/OrgrimmarInstance",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"globalWmoAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [13, 25]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [13, 25]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/RazorfenDowns", "mapDirectory": "World/Maps/RazorfenDowns",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source-transform",
"coordinateTransform": {"origin": [-2402.2809, 62, 1208.3219], "yawDegrees": 0},
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [35, 50]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [35, 50]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+2 -1
View File
@@ -8,7 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/RazorfenKraulInstance", "mapDirectory": "World/Maps/RazorfenKraulInstance",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"globalWmoSourceBasis": "flip-x", "coordinateAlignment": "source-transform",
"coordinateTransform": {"origin": [-1952.1803, 87.684, 1587.8923], "yawDegrees": -133},
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a", "clientBuild": "3.3.5a",
"mapDirectory": "World/Maps/MonasteryInstances", "mapDirectory": "World/Maps/MonasteryInstances",
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"adtSourceBasis": "z-y-negative-x",
"difficultyVariants": [ "difficultyVariants": [
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [26, 45] } { "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [26, 45] }
], ],
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/SchoolofNecromancy", "mapDirectory": "World/Maps/SchoolofNecromancy",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source-transform",
"coordinateTransform": {"origin": [-141.694, 148.0683, 105.6784], "yawDegrees": 1},
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/AuchindounDemon", "mapDirectory": "World/Maps/AuchindounDemon",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/AuchindounShadow", "mapDirectory": "World/Maps/AuchindounShadow",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -7,12 +7,14 @@
"expansion": "classic", "expansion": "classic",
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/Shadowfang", "mapDirectory": "World/Maps/Shadowfang",
"environmentMode": "global-wmo", "environmentMode": "detect",
"coordinateAlignment": "source",
"adtSourceBasis": "z-y-negative-x",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [18, 30]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [18, 30]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
"extraction": {"rootWmo": "World/wmo/Dungeon/LD_ShadowFang/LD_ShadowFang.wmo", "doodadSets": ["Set_$DefaultGlobal"]}, "extraction": {"adtTiles": "all-present"},
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50}, "navigation": {"cellSize": 0.2, "cellHeight": 0.1, "tileSize": 256, "retainTileIntermediates": false, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
"review": {"entrance": "required", "bossOrder": "required", "optionalEncounters": "required", "presentation": "required", "offMeshLinks": "required"}, "review": {"entrance": "required", "bossOrder": "required", "optionalEncounters": "required", "presentation": "required", "offMeshLinks": "required"},
"exceptions": [] "exceptions": []
} }
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/HellfireMilitary", "mapDirectory": "World/Maps/HellfireMilitary",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/CoilfangDraenei", "mapDirectory": "World/Maps/CoilfangDraenei",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/CoilfangPumping", "mapDirectory": "World/Maps/CoilfangPumping",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/StormwindJail", "mapDirectory": "World/Maps/StormwindJail",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"globalWmoAlignment": "source",
"difficultyVariants": [ "difficultyVariants": [
{ {
"id": "normal", "id": "normal",
+2
View File
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/Stratholme", "mapDirectory": "World/Maps/Stratholme",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source-transform",
"coordinateTransform": {"origin": [-3449.6608, 144.3848, -3379.6966], "yawDegrees": 0},
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/SunkenTemple", "mapDirectory": "World/Maps/SunkenTemple",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [45, 55]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [45, 55]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
+1
View File
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
+1
View File
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/Uldaman", "mapDirectory": "World/Maps/Uldaman",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [ "difficultyVariants": [
{ {
"id": "normal", "id": "normal",
+1
View File
@@ -8,6 +8,7 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/CoilfangMarsh", "mapDirectory": "World/Maps/CoilfangMarsh",
"environmentMode": "global-wmo", "environmentMode": "global-wmo",
"coordinateAlignment": "source",
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}], "difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"}, "clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true}, "serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
@@ -27,6 +27,7 @@
} }
], ],
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"exceptions": [], "exceptions": [],
"expansion": "wrath", "expansion": "wrath",
"extraction": { "extraction": {
+2
View File
@@ -8,6 +8,8 @@
"clientBuild": "3.3.5a-Ascension", "clientBuild": "3.3.5a-Ascension",
"mapDirectory": "World/Maps/TanarisInstance", "mapDirectory": "World/Maps/TanarisInstance",
"environmentMode": "detect", "environmentMode": "detect",
"coordinateAlignment": "source",
"adtSourceBasis": "z-y-negative-x",
"difficultyVariants": [ "difficultyVariants": [
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 54] } { "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 54] }
], ],
+3 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "healer-man", "name": "healer-man",
"version": "0.1.4", "version": "0.1.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "healer-man", "name": "healer-man",
"version": "0.1.4", "version": "0.1.8",
"dependencies": { "dependencies": {
"@capacitor/android": "8.4.1", "@capacitor/android": "8.4.1",
"@capacitor/core": "8.4.1", "@capacitor/core": "8.4.1",
@@ -21,6 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@capacitor/cli": "8.4.1", "@capacitor/cli": "8.4.1",
"@dimforge/rapier3d-compat": "0.19.2",
"@gltf-transform/cli": "^4.4.1", "@gltf-transform/cli": "^4.4.1",
"@gltf-transform/core": "^4.4.2", "@gltf-transform/core": "^4.4.2",
"@recast-navigation/core": "0.43.1", "@recast-navigation/core": "0.43.1",
+7 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "healer-man", "name": "healer-man",
"private": true, "private": true,
"version": "0.1.4", "version": "0.1.8",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
@@ -20,7 +20,8 @@
"coa:sync:live": "node scripts/sync-ascension-coa.mjs --tooltips", "coa:sync:live": "node scripts/sync-ascension-coa.mjs --tooltips",
"coa:icons": "node scripts/export-spell-icons.mjs", "coa:icons": "node scripts/export-spell-icons.mjs",
"ascension:talents": "node scripts/generate-wow335-talents.mjs ../wow335a/Interface/AddOns/PlayerBotManager/PBM/PBM_TalentData.lua ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc src/game/wow335Talents.generated.ts ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Talent.dbc", "ascension:talents": "node scripts/generate-wow335-talents.mjs ../wow335a/Interface/AddOns/PlayerBotManager/PBM/PBM_TalentData.lua ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc src/game/wow335Talents.generated.ts ../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Talent.dbc",
"ascension:sync": "npm run coa:sync && npm run wow335:abilities && npm run ascension:talents && npm run coa:icons", "catalogs:shard": "vite-node scripts/generate-catalog-shards.ts",
"ascension:sync": "npm run coa:sync && npm run wow335:abilities && npm run ascension:talents && npm run coa:icons && npm run catalogs:shard",
"manastorm:import": "node scripts/manastorm-import/cli.mjs", "manastorm:import": "node scripts/manastorm-import/cli.mjs",
"manastorm:forensics": "node scripts/manastorm-import/forensics-cli.mjs", "manastorm:forensics": "node scripts/manastorm-import/forensics-cli.mjs",
"manastorm:sync": "npm run manastorm:import && npm run manastorm:forensics && npm run coa:icons", "manastorm:sync": "npm run manastorm:import && npm run manastorm:forensics && npm run coa:icons",
@@ -91,13 +92,15 @@
"assets:ktx2:creatures": "node scripts/asset-pipeline/compress-creature-ktx2.mjs", "assets:ktx2:creatures": "node scripts/asset-pipeline/compress-creature-ktx2.mjs",
"assets:ktx2:audit": "node scripts/asset-pipeline/audit-ktx2.mjs --visual-only --require-all", "assets:ktx2:audit": "node scripts/asset-pipeline/audit-ktx2.mjs --visual-only --require-all",
"assets:ktx2:refresh-metadata": "node scripts/asset-pipeline/refresh-ktx2-metadata.mjs", "assets:ktx2:refresh-metadata": "node scripts/asset-pipeline/refresh-ktx2-metadata.mjs",
"prebuild": "npm run loot:generate && npm run assets:ktx2:audit", "assets:performance:audit": "node scripts/asset-pipeline/audit-environment-performance.mjs",
"prebuild": "npm run loot:generate && 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": "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", "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", "android:sync": "npm run build:android:web && cap sync android",
"android:apk": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleDebug", "android:apk": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleDebug",
"android:apk:release": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease", "android:apk:release": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease",
"android:install": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 installDebug", "android:install": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 installDebug",
"release:check": "python scripts/release_manager.py --check-release",
"release:gui": "python scripts/release_manager.py", "release:gui": "python scripts/release_manager.py",
"preview": "vite preview --host 0.0.0.0", "preview": "vite preview --host 0.0.0.0",
"pretest": "npm run loot:generate", "pretest": "npm run loot:generate",
@@ -118,6 +121,7 @@
}, },
"devDependencies": { "devDependencies": {
"@capacitor/cli": "8.4.1", "@capacitor/cli": "8.4.1",
"@dimforge/rapier3d-compat": "0.19.2",
"@gltf-transform/cli": "^4.4.1", "@gltf-transform/cli": "^4.4.1",
"@gltf-transform/core": "^4.4.2", "@gltf-transform/core": "^4.4.2",
"@recast-navigation/core": "0.43.1", "@recast-navigation/core": "0.43.1",
@@ -13,8 +13,8 @@
{ {
"id": "visual", "id": "visual",
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb", "url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
"checksum": "42d57961c31f54ecc7bc78829751110fc89294e6318336b8f260e132e478d13d", "checksum": "7e4c7c75a898f5000fcb686dcfc6d3c1c4e675dc3dac5f7a8d48e53372b9f364",
"byteLength": 10266900, "byteLength": 7068884,
"triangleCount": 248302, "triangleCount": 248302,
"compression": "meshopt+ktx2" "compression": "meshopt+ktx2"
} }
File diff suppressed because it is too large Load Diff
@@ -7,8 +7,8 @@
{ {
"id": "visual", "id": "visual",
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb", "url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
"checksum": "42d57961c31f54ecc7bc78829751110fc89294e6318336b8f260e132e478d13d", "checksum": "7e4c7c75a898f5000fcb686dcfc6d3c1c4e675dc3dac5f7a8d48e53372b9f364",
"byteLength": 10266900, "byteLength": 7068884,
"triangleCount": 248302, "triangleCount": 248302,
"compression": "meshopt+ktx2" "compression": "meshopt+ktx2"
} }
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"versionName": "0.1.4", "versionName": "0.1.8",
"versionCode": 1004 "versionCode": 1008
} }
@@ -0,0 +1,121 @@
#!/usr/bin/env node
import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { analyzeGlbFile } from "./glb-performance.mjs";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const baselineFile = path.join(
projectRoot,
"scripts/asset-pipeline/environment-performance-baseline.json",
);
const defaultRoots = [
path.join(projectRoot, "public/assets/game/manastorm"),
path.join(projectRoot, "src/assets/game/dungeons"),
];
const defaultThresholds = {
maxEstimatedDrawCalls: 1_500,
maxUnbatchedDrawCallSavings: 250,
};
async function exists(file) {
try {
await access(file);
return true;
} catch {
return false;
}
}
async function collectVisualGlbs(root, files = []) {
if (!await exists(root)) return files;
const entry = await stat(root);
if (entry.isFile()) {
if (/(?:^|[-_])visual\.glb$/i.test(path.basename(root))) files.push(root);
return files;
}
for (const child of await readdir(root, { withFileTypes: true })) {
const target = path.join(root, child.name);
if (child.isDirectory()) await collectVisualGlbs(target, files);
else if (/(?:^|[-_])visual\.glb$/i.test(child.name)) files.push(target);
}
return files;
}
function projectPath(file) {
return path.relative(projectRoot, file).split(path.sep).join("/");
}
function exceeds(report, limits) {
return report.estimatedDrawCalls > limits.maxEstimatedDrawCalls
|| report.unbatchedDrawCallSavings > limits.maxUnbatchedDrawCallSavings;
}
const args = process.argv.slice(2);
const writeBaseline = args.includes("--write-baseline");
const roots = args.filter((argument) => argument !== "--write-baseline")
.map((argument) => path.resolve(projectRoot, argument));
const files = [...new Set((await Promise.all(
(roots.length ? roots : defaultRoots).map((root) => collectVisualGlbs(root)),
)).flat())].sort();
const reports = await Promise.all(files.map(async (file) => ({
file: projectPath(file),
byteLength: (await stat(file)).size,
...(await analyzeGlbFile(file)),
})));
if (writeBaseline) {
const baseline = {
schemaVersion: 1,
thresholds: defaultThresholds,
exceptions: Object.fromEntries(
reports
.filter((report) => exceeds(report, defaultThresholds))
.map((report) => [report.file, {
maxEstimatedDrawCalls: Math.max(defaultThresholds.maxEstimatedDrawCalls, report.estimatedDrawCalls),
maxUnbatchedDrawCallSavings: Math.max(
defaultThresholds.maxUnbatchedDrawCallSavings,
report.unbatchedDrawCallSavings,
),
}]),
),
};
await mkdir(path.dirname(baselineFile), { recursive: true });
await writeFile(baselineFile, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
console.log(`Wrote ${Object.keys(baseline.exceptions).length} performance exceptions to ${projectPath(baselineFile)}.`);
process.exit(0);
}
if (!await exists(baselineFile)) {
throw new Error(`Missing ${projectPath(baselineFile)}. Run this script with --write-baseline.`);
}
const baseline = JSON.parse(await readFile(baselineFile, "utf8"));
const thresholds = { ...defaultThresholds, ...baseline.thresholds };
const regressions = reports.filter((report) => {
const limits = { ...thresholds, ...baseline.exceptions?.[report.file] };
return exceeds(report, limits);
});
const debt = reports.filter((report) => exceeds(report, thresholds));
const top = [...reports]
.sort((left, right) => right.estimatedDrawCalls - left.estimatedDrawCalls)
.slice(0, 12)
.map((report) => ({
asset: report.file,
calls: report.estimatedDrawCalls,
avoidable: report.unbatchedDrawCallSavings,
gpuBatches: report.gpuInstanceBatches,
nodes: report.nodes,
}));
console.table(top);
console.log(JSON.stringify({
status: regressions.length ? "blocked" : "green",
assets: reports.length,
knownBudgetExceptions: debt.length,
regressions: regressions.map((report) => ({
file: report.file,
estimatedDrawCalls: report.estimatedDrawCalls,
unbatchedDrawCallSavings: report.unbatchedDrawCallSavings,
})),
}, null, 2));
if (regressions.length) process.exitCode = 1;
@@ -0,0 +1,249 @@
{
"schemaVersion": 1,
"thresholds": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 250
},
"exceptions": {
"public/assets/game/manastorm/109-sunkentemple/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 320
},
"public/assets/game/manastorm/129-razorfendowns/visual.glb": {
"maxEstimatedDrawCalls": 1507,
"maxUnbatchedDrawCallSavings": 1406
},
"public/assets/game/manastorm/1771-custom-marsh/visual.glb": {
"maxEstimatedDrawCalls": 2493,
"maxUnbatchedDrawCallSavings": 2114
},
"public/assets/game/manastorm/1774-custom-tropical/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 910
},
"public/assets/game/manastorm/1775-custom-tuskarr/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 500
},
"public/assets/game/manastorm/1776-custom-vrykul1/visual.glb": {
"maxEstimatedDrawCalls": 2223,
"maxUnbatchedDrawCallSavings": 1818
},
"public/assets/game/manastorm/1781-forgottenmine/visual.glb": {
"maxEstimatedDrawCalls": 5756,
"maxUnbatchedDrawCallSavings": 3818
},
"public/assets/game/manastorm/1785-valsharaharena/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 392
},
"public/assets/game/manastorm/1787-baradinholdarena/visual.glb": {
"maxEstimatedDrawCalls": 10462,
"maxUnbatchedDrawCallSavings": 9362
},
"public/assets/game/manastorm/1794-twistingnether/visual.glb": {
"maxEstimatedDrawCalls": 3077,
"maxUnbatchedDrawCallSavings": 2557
},
"public/assets/game/manastorm/209-tanarisinstance/visual.glb": {
"maxEstimatedDrawCalls": 4579,
"maxUnbatchedDrawCallSavings": 3547
},
"public/assets/game/manastorm/229-blackrockspire/visual.glb": {
"maxEstimatedDrawCalls": 3972,
"maxUnbatchedDrawCallSavings": 3598
},
"public/assets/game/manastorm/269-cavernsoftime/visual.glb": {
"maxEstimatedDrawCalls": 5446,
"maxUnbatchedDrawCallSavings": 5289
},
"public/assets/game/manastorm/289-schoolofnecromancy/visual.glb": {
"maxEstimatedDrawCalls": 5140,
"maxUnbatchedDrawCallSavings": 4790
},
"public/assets/game/manastorm/309-zul-gurub/visual.glb": {
"maxEstimatedDrawCalls": 17377,
"maxUnbatchedDrawCallSavings": 15605
},
"public/assets/game/manastorm/329-stratholme/visual.glb": {
"maxEstimatedDrawCalls": 2954,
"maxUnbatchedDrawCallSavings": 2725
},
"public/assets/game/manastorm/34-stormwindjail/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 487
},
"public/assets/game/manastorm/349-mauradon/visual.glb": {
"maxEstimatedDrawCalls": 5624,
"maxUnbatchedDrawCallSavings": 5352
},
"public/assets/game/manastorm/36-deadminesinstance/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 309
},
"public/assets/game/manastorm/389-orgrimmarinstance/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 881
},
"public/assets/game/manastorm/429-diremaul/visual.glb": {
"maxEstimatedDrawCalls": 5021,
"maxUnbatchedDrawCallSavings": 4636
},
"public/assets/game/manastorm/469-blackwinglair/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 452
},
"public/assets/game/manastorm/47-razorfenkraulinstance/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 897
},
"public/assets/game/manastorm/48-blackfathom/visual.glb": {
"maxEstimatedDrawCalls": 2164,
"maxUnbatchedDrawCallSavings": 1950
},
"public/assets/game/manastorm/509-ahnqiraj/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 600
},
"public/assets/game/manastorm/531-ahnqirajtemple/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 953
},
"public/assets/game/manastorm/532-karazahn/visual.glb": {
"maxEstimatedDrawCalls": 8540,
"maxUnbatchedDrawCallSavings": 7892
},
"public/assets/game/manastorm/533-stratholme-raid/visual.glb": {
"maxEstimatedDrawCalls": 4165,
"maxUnbatchedDrawCallSavings": 4014
},
"public/assets/game/manastorm/540-hellfiremilitary/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 1150
},
"public/assets/game/manastorm/542-hellfiredemon/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 444
},
"public/assets/game/manastorm/543-hellfirerampart/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 296
},
"public/assets/game/manastorm/545-coilfangpumping/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 674
},
"public/assets/game/manastorm/546-coilfangmarsh/visual.glb": {
"maxEstimatedDrawCalls": 3104,
"maxUnbatchedDrawCallSavings": 3033
},
"public/assets/game/manastorm/547-coilfangdraenei/visual.glb": {
"maxEstimatedDrawCalls": 1535,
"maxUnbatchedDrawCallSavings": 1455
},
"public/assets/game/manastorm/548-coilfangraid/visual.glb": {
"maxEstimatedDrawCalls": 1916,
"maxUnbatchedDrawCallSavings": 1683
},
"public/assets/game/manastorm/550-tempestkeepraid/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 420
},
"public/assets/game/manastorm/552-tempestkeeparcane/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 999
},
"public/assets/game/manastorm/553-tempestkeepatrium/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 1115
},
"public/assets/game/manastorm/554-tempestkeepfactory/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 872
},
"public/assets/game/manastorm/555-auchindounshadow/visual.glb": {
"maxEstimatedDrawCalls": 2624,
"maxUnbatchedDrawCallSavings": 2516
},
"public/assets/game/manastorm/556-auchindoundemon/visual.glb": {
"maxEstimatedDrawCalls": 2095,
"maxUnbatchedDrawCallSavings": 1987
},
"public/assets/game/manastorm/557-auchindounethereal/visual.glb": {
"maxEstimatedDrawCalls": 3164,
"maxUnbatchedDrawCallSavings": 3026
},
"public/assets/game/manastorm/558-auchindoundraenei/visual.glb": {
"maxEstimatedDrawCalls": 2613,
"maxUnbatchedDrawCallSavings": 2523
},
"public/assets/game/manastorm/560-hillsbradpast/visual.glb": {
"maxEstimatedDrawCalls": 28259,
"maxUnbatchedDrawCallSavings": 24733
},
"public/assets/game/manastorm/568-zulaman/visual.glb": {
"maxEstimatedDrawCalls": 4307,
"maxUnbatchedDrawCallSavings": 3850
},
"public/assets/game/manastorm/574-valgarde70/visual.glb": {
"maxEstimatedDrawCalls": 8271,
"maxUnbatchedDrawCallSavings": 7072
},
"public/assets/game/manastorm/575-utgardepinnacle/visual.glb": {
"maxEstimatedDrawCalls": 16277,
"maxUnbatchedDrawCallSavings": 14018
},
"public/assets/game/manastorm/576-nexus70/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 396
},
"public/assets/game/manastorm/578-nexus80/visual.glb": {
"maxEstimatedDrawCalls": 2283,
"maxUnbatchedDrawCallSavings": 1896
},
"public/assets/game/manastorm/585-sunwell5manfix/visual.glb": {
"maxEstimatedDrawCalls": 1810,
"maxUnbatchedDrawCallSavings": 1618
},
"public/assets/game/manastorm/595-stratholmecot/visual.glb": {
"maxEstimatedDrawCalls": 4353,
"maxUnbatchedDrawCallSavings": 3386
},
"public/assets/game/manastorm/600-draktheronkeep/visual.glb": {
"maxEstimatedDrawCalls": 3518,
"maxUnbatchedDrawCallSavings": 2876
},
"public/assets/game/manastorm/602-ulduar80/visual.glb": {
"maxEstimatedDrawCalls": 1973,
"maxUnbatchedDrawCallSavings": 1295
},
"public/assets/game/manastorm/619-azjol-lowercity/visual.glb": {
"maxEstimatedDrawCalls": 1596,
"maxUnbatchedDrawCallSavings": 1243
},
"public/assets/game/manastorm/658-quarryoftears/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 618
},
"public/assets/game/manastorm/668-hallsofreflection/visual.glb": {
"maxEstimatedDrawCalls": 5464,
"maxUnbatchedDrawCallSavings": 4606
},
"public/assets/game/manastorm/70-uldaman/visual.glb": {
"maxEstimatedDrawCalls": 3268,
"maxUnbatchedDrawCallSavings": 3115
},
"public/assets/game/manastorm/821-tinketechshowdown/visual.glb": {
"maxEstimatedDrawCalls": 2585,
"maxUnbatchedDrawCallSavings": 2379
},
"public/assets/game/manastorm/90-gnomeragoninstance/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 1051
},
"public/assets/game/manastorm/908-alvaencounter/visual.glb": {
"maxEstimatedDrawCalls": 1500,
"maxUnbatchedDrawCallSavings": 1096
}
}
}
@@ -0,0 +1,98 @@
import { open } from "node:fs/promises";
const GLB_MAGIC = 0x46546c67;
const JSON_CHUNK_TYPE = 0x4e4f534a;
const GPU_INSTANCING_EXTENSION = "EXT_mesh_gpu_instancing";
function primitiveCount(json, meshIndex) {
return json.meshes?.[meshIndex]?.primitives?.length ?? 0;
}
function gpuInstanceCount(json, node) {
const attributes = node.extensions?.[GPU_INSTANCING_EXTENSION]?.attributes;
if (!attributes || typeof attributes !== "object") return 0;
return Math.max(
0,
...Object.values(attributes).map((accessorIndex) => (
json.accessors?.[accessorIndex]?.count ?? 0
)),
);
}
export function analyzeGlbJson(json) {
const repeatedMeshes = new Map();
let estimatedDrawCalls = 0;
let expandedPrimitiveInstances = 0;
let meshNodes = 0;
let gpuInstanceBatches = 0;
let gpuInstances = 0;
for (const node of json.nodes ?? []) {
if (!Number.isInteger(node.mesh)) continue;
meshNodes += 1;
const primitives = primitiveCount(json, node.mesh);
const instances = gpuInstanceCount(json, node);
estimatedDrawCalls += primitives;
expandedPrimitiveInstances += primitives * Math.max(1, instances);
if (instances > 0) {
gpuInstanceBatches += 1;
gpuInstances += instances;
continue;
}
repeatedMeshes.set(node.mesh, (repeatedMeshes.get(node.mesh) ?? 0) + 1);
}
let eligibleRepeatedMeshes = 0;
let eligibleRepeatedNodes = 0;
let unbatchedDrawCallSavings = 0;
for (const [meshIndex, count] of repeatedMeshes) {
if (count < 3) continue;
eligibleRepeatedMeshes += 1;
eligibleRepeatedNodes += count;
unbatchedDrawCallSavings += (count - 1) * primitiveCount(json, meshIndex);
}
const materials = json.materials ?? [];
return {
nodes: json.nodes?.length ?? 0,
meshNodes,
meshes: json.meshes?.length ?? 0,
materials: materials.length,
textures: json.textures?.length ?? 0,
doubleSidedMaterials: materials.filter((material) => material.doubleSided === true).length,
blendedMaterials: materials.filter((material) => material.alphaMode === "BLEND").length,
estimatedDrawCalls,
expandedPrimitiveInstances,
gpuInstanceBatches,
gpuInstances,
eligibleRepeatedMeshes,
eligibleRepeatedNodes,
unbatchedDrawCallSavings,
usesGpuInstancing: (json.extensionsUsed ?? []).includes(GPU_INSTANCING_EXTENSION),
};
}
export async function readGlbJson(file) {
const handle = await open(file, "r");
try {
const header = Buffer.alloc(20);
const { bytesRead } = await handle.read(header, 0, header.length, 0);
if (bytesRead !== header.length
|| header.readUInt32LE(0) !== GLB_MAGIC
|| header.readUInt32LE(4) !== 2
|| header.readUInt32LE(16) !== JSON_CHUNK_TYPE) {
throw new Error(`${file} is not a supported GLB 2.0 file.`);
}
const jsonLength = header.readUInt32LE(12);
const jsonBuffer = Buffer.alloc(jsonLength);
const jsonRead = await handle.read(jsonBuffer, 0, jsonLength, 20);
if (jsonRead.bytesRead !== jsonLength) throw new Error(`${file} has a truncated JSON chunk.`);
return JSON.parse(jsonBuffer.toString("utf8"));
} finally {
await handle.close();
}
}
export async function analyzeGlbFile(file) {
return analyzeGlbJson(await readGlbJson(file));
}
@@ -0,0 +1,54 @@
import assert from "node:assert/strict";
import test from "node:test";
import { analyzeGlbJson } from "./glb-performance.mjs";
test("counts repeated mesh placements as avoidable draw calls", () => {
const report = analyzeGlbJson({
meshes: [
{ primitives: [{}, {}] },
{ primitives: [{}] },
],
nodes: [
{ mesh: 0 },
{ mesh: 0 },
{ mesh: 0 },
{ mesh: 1 },
],
materials: [
{ doubleSided: true, alphaMode: "BLEND" },
{},
],
textures: [{}, {}],
});
assert.equal(report.estimatedDrawCalls, 7);
assert.equal(report.expandedPrimitiveInstances, 7);
assert.equal(report.eligibleRepeatedMeshes, 1);
assert.equal(report.eligibleRepeatedNodes, 3);
assert.equal(report.unbatchedDrawCallSavings, 4);
assert.equal(report.doubleSidedMaterials, 1);
assert.equal(report.blendedMaterials, 1);
});
test("counts one draw-call set for an EXT_mesh_gpu_instancing batch", () => {
const report = analyzeGlbJson({
extensionsUsed: ["EXT_mesh_gpu_instancing"],
accessors: [{ count: 12 }],
meshes: [{ primitives: [{}, {}, {}] }],
nodes: [{
mesh: 0,
extensions: {
EXT_mesh_gpu_instancing: {
attributes: { TRANSLATION: 0 },
},
},
}],
});
assert.equal(report.estimatedDrawCalls, 3);
assert.equal(report.expandedPrimitiveInstances, 36);
assert.equal(report.gpuInstanceBatches, 1);
assert.equal(report.gpuInstances, 12);
assert.equal(report.unbatchedDrawCallSavings, 0);
assert.equal(report.usesGpuInstancing, true);
});
@@ -0,0 +1,122 @@
#!/usr/bin/env node
import path from "node:path";
import { fileURLToPath } from "node:url";
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { BufferAttribute, BufferGeometry, Vector3 } from "three";
import { MeshBVH } from "three-mesh-bvh";
import { MeshoptDecoder } from "meshoptimizer";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const campaign = await import("../../src/game/generated/dungeonCampaignCatalog.json", {
with: { type: "json" },
}).then((module) => module.default);
const useCollision = process.argv.includes("--collision");
const requestedSlugs = new Set(process.argv.slice(2).filter((argument) => argument !== "--collision"));
const definitions = campaign.definitions.filter((definition) => (
!requestedSlugs.size || requestedSlugs.has(definition.id)
));
if (!definitions.length) {
throw new Error("No matching dungeon definitions. Pass one or more dungeon slugs.");
}
await MeshoptDecoder.ready;
const io = new NodeIO()
.registerExtensions(ALL_EXTENSIONS)
.registerDependencies({ "meshopt.decoder": MeshoptDecoder });
const point = new Vector3();
function normalizerFor(accessor, array) {
if (!accessor.getNormalized()) return (value) => value;
if (array instanceof Int16Array) return (value) => Math.max(value / 32_767, -1);
if (array instanceof Uint16Array) return (value) => value / 65_535;
if (array instanceof Int8Array) return (value) => Math.max(value / 127, -1);
if (array instanceof Uint8Array) return (value) => value / 255;
return (value) => value;
}
async function geometryTrees(file) {
const document = await io.read(file);
const trees = [];
for (const node of document.getRoot().listNodes()) {
const mesh = node.getMesh();
if (!mesh) continue;
const matrix = node.getWorldMatrix();
for (const primitive of mesh.listPrimitives()) {
const accessor = primitive.getAttribute("POSITION");
const source = accessor?.getArray();
if (!accessor || !source) continue;
const normalize = normalizerFor(accessor, source);
const positions = new Float32Array(source.length);
for (let offset = 0; offset < source.length; offset += 3) {
const x = normalize(source[offset]);
const y = normalize(source[offset + 1]);
const z = normalize(source[offset + 2]);
positions[offset] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
positions[offset + 1] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
positions[offset + 2] = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
}
const geometry = new BufferGeometry();
geometry.setAttribute("position", new BufferAttribute(positions, 3));
const indices = primitive.getIndices()?.getArray();
if (indices) geometry.setIndex(new BufferAttribute(indices, 1));
trees.push(new MeshBVH(geometry));
}
}
return trees;
}
function nearestDistance(trees, position) {
point.fromArray(position);
let distance = Infinity;
for (const tree of trees) {
const result = tree.closestPointToPoint(point);
distance = Math.min(distance, result?.distance ?? Infinity);
}
return distance;
}
function percentile(sorted, proportion) {
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * proportion))] ?? 0;
}
const results = [];
for (const definition of definitions) {
const geometryUrls = useCollision
? definition.assets.collision.map((chunk) => chunk.url)
: [definition.assets.navigation.url];
const trees = (await Promise.all(geometryUrls.map((url) => (
geometryTrees(path.join(projectRoot, "public", url.replace(/^\//, "")))
)))).flat();
const samples = [
{ id: "entrance", position: definition.entrance.footPosition },
...definition.staticSpawns.map((spawn) => ({ id: spawn.id, position: spawn.position })),
...definition.roamingPacks.flatMap((pack) => (
pack.waypoints.map((position, index) => ({ id: `${pack.id}:${index}`, position }))
)),
];
const distances = samples
.map((sample) => ({ ...sample, distance: nearestDistance(trees, sample.position) }))
.sort((left, right) => left.distance - right.distance);
const values = distances.map((sample) => sample.distance);
results.push({
dungeonId: definition.id,
geometry: useCollision ? "collision" : "navigation",
coordinateAlignment: definition.provenance.coordinateAlignment,
coordinateTransform: definition.provenance.coordinateTransform,
samples: values.length,
withinOneMeter: values.filter((distance) => distance <= 1).length,
medianDistance: percentile(values, 0.5),
p90Distance: percentile(values, 0.9),
maximumDistance: values.at(-1) ?? 0,
furthest: distances.slice(-5).reverse().map(({ id, position, distance }) => ({
id,
position,
distance,
})),
});
}
console.log(JSON.stringify(results, null, 2));
@@ -175,6 +175,10 @@ function interpolate(left, right, alpha) {
return left.map((value, axis) => rounded(value + (right[axis] - value) * alpha)); return left.map((value, axis) => rounded(value + (right[axis] - value) * alpha));
} }
function horizontalDistanceSquared(left, right) {
return (left[0] - right[0]) ** 2 + (left[2] - right[2]) ** 2;
}
function normalizedName(value) { function normalizedName(value) {
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, ""); return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
} }
@@ -201,6 +205,7 @@ const ADT_SOURCE_BASES = new Set([
"z-y-negative-x", "z-y-negative-x",
]); ]);
const GLOBAL_WMO_SOURCE_BASES = new Set(["draft", "flip-x"]); const GLOBAL_WMO_SOURCE_BASES = new Set(["draft", "flip-x"]);
const COORDINATE_ALIGNMENTS = new Set(["player-anchor", "source", "source-transform"]);
function sourceBasisFor(recipe, environmentMode) { function sourceBasisFor(recipe, environmentMode) {
if (environmentMode !== "adt-hybrid") { if (environmentMode !== "adt-hybrid") {
@@ -220,6 +225,43 @@ function sourceBasisFor(recipe, environmentMode) {
return basis; return basis;
} }
function coordinateAlignmentFor(recipe, environmentMode) {
const legacyGlobalWmoAlignment = environmentMode === "global-wmo"
? recipe.globalWmoAlignment
: undefined;
if (
recipe.coordinateAlignment
&& legacyGlobalWmoAlignment
&& recipe.coordinateAlignment !== legacyGlobalWmoAlignment
) {
throw new Error(`${recipe.slug}: coordinate alignment overrides disagree.`);
}
const alignment = recipe.coordinateAlignment ?? legacyGlobalWmoAlignment ?? "player-anchor";
if (!COORDINATE_ALIGNMENTS.has(alignment)) {
throw new Error(`${recipe.slug}: unsupported coordinate alignment ${alignment}.`);
}
return alignment;
}
function sourceTransformFor(recipe, alignment) {
const transform = recipe.coordinateTransform;
if (alignment !== "source-transform") {
if (transform) {
throw new Error(`${recipe.slug}: coordinateTransform requires source-transform alignment.`);
}
return undefined;
}
if (!transform || !Array.isArray(transform.origin) || transform.origin.length !== 3) {
throw new Error(`${recipe.slug}: source-transform alignment requires a three-value coordinateTransform.origin.`);
}
const origin = transform.origin.map(Number);
const yawDegrees = Number(transform.yawDegrees ?? 0);
if (!origin.every(Number.isFinite) || !Number.isFinite(yawDegrees)) {
throw new Error(`${recipe.slug}: coordinateTransform values must be finite numbers.`);
}
return { origin, yawDegrees };
}
/** /**
* Runtime drafts retain the original pipeline convention. wow.export ADT * Runtime drafts retain the original pipeline convention. wow.export ADT
* packages have an additional horizontal basis change baked into their GLBs. * packages have an additional horizontal basis change baked into their GLBs.
@@ -238,7 +280,39 @@ function sourceToPackageBasis(position, environmentMode, sourceBasis) {
: point; : point;
} }
function coordinateMapper(draft, environmentMode, playerAnchor, sourceBasis) { function rotateAroundY(position, yawDegrees) {
const radians = yawDegrees * Math.PI / 180;
const cosine = Math.cos(radians);
const sine = Math.sin(radians);
return vector([
position[0] * cosine - position[2] * sine,
position[1],
position[0] * sine + position[2] * cosine,
]);
}
function coordinateMapper(
draft,
environmentMode,
playerAnchor,
sourceBasis,
alignment,
sourceTransform,
) {
if (alignment === "source") {
return (position) => sourceToPackageBasis(position, environmentMode, sourceBasis);
}
if (alignment === "source-transform") {
const basisOrigin = sourceToPackageBasis(
sourceTransform.origin,
environmentMode,
sourceBasis,
);
return (position) => rotateAroundY(
subtract(sourceToPackageBasis(position, environmentMode, sourceBasis), basisOrigin),
sourceTransform.yawDegrees,
);
}
const sourceEntrance = draft?.entrance const sourceEntrance = draft?.entrance
?? draft?.spawns?.[0]?.position ?? draft?.spawns?.[0]?.position
?? playerAnchor.position; ?? playerAnchor.position;
@@ -764,6 +838,8 @@ async function compile() {
} }
const environmentMode = inferEnvironmentMode(recipe, assetPackage); const environmentMode = inferEnvironmentMode(recipe, assetPackage);
const sourceBasis = sourceBasisFor(recipe, environmentMode); const sourceBasis = sourceBasisFor(recipe, environmentMode);
const coordinateAlignment = coordinateAlignmentFor(recipe, environmentMode);
const sourceTransform = sourceTransformFor(recipe, coordinateAlignment);
const playerAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "player"); const playerAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "player");
const trashAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "trash"); const trashAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "trash");
const bossAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "boss"); const bossAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "boss");
@@ -774,7 +850,7 @@ async function compile() {
trashAnchor.position[0] - playerAnchor.position[0], trashAnchor.position[0] - playerAnchor.position[0],
trashAnchor.position[2] - playerAnchor.position[2], trashAnchor.position[2] - playerAnchor.position[2],
); );
const entranceYaw = Number.isFinite(playerAnchor.yaw) let entranceYaw = Number.isFinite(playerAnchor.yaw)
? playerAnchor.yaw ? playerAnchor.yaw
: inferredEntranceYaw; : inferredEntranceYaw;
@@ -794,6 +870,13 @@ async function compile() {
environmentMode, environmentMode,
playerAnchor, playerAnchor,
sourceBasis, sourceBasis,
coordinateAlignment,
sourceTransform,
);
const entrancePosition = mapPosition(
draft?.entrance
?? draft?.spawns?.[0]?.position
?? playerAnchor.position,
); );
const sourceEntities = (draft?.entities ?? []) const sourceEntities = (draft?.entities ?? [])
.filter((entity) => ( .filter((entity) => (
@@ -876,16 +959,49 @@ async function compile() {
const entry = Number(spawn.entityId.replace("entry-", "")); const entry = Number(spawn.entityId.replace("entry-", ""));
if (!bossCandidatesByEntry.has(entry)) bossCandidatesByEntry.set(entry, spawn); if (!bossCandidatesByEntry.has(entry)) bossCandidatesByEntry.set(entry, spawn);
} }
const mappedNativeRoutePositions = coordinateAlignment !== "player-anchor"
? (draft?.spawns ?? [])
.map((spawn) => mapPosition(spawn.position))
.filter((position) => position.every(Number.isFinite))
.sort((left, right) => (
horizontalDistanceSquared(left, entrancePosition)
- horizontalDistanceSquared(right, entrancePosition)
))
: [];
const entranceDirectionTarget = mappedNativeRoutePositions.find((position) => (
Math.abs(position[1] - entrancePosition[1]) <= 3
&& horizontalDistanceSquared(position, entrancePosition) >= 16
));
if (entranceDirectionTarget) {
entranceYaw = Math.atan2(
entranceDirectionTarget[0] - entrancePosition[0],
entranceDirectionTarget[2] - entrancePosition[2],
);
}
const entranceLevelRoutePositions = mappedNativeRoutePositions.filter(
(position) => Math.abs(position[1] - entrancePosition[1]) <= 2.5,
);
const nativeRoutePositions = (
bossCandidatesByEntry.size === 0
&& entranceLevelRoutePositions.length >= encounters.length
)
? entranceLevelRoutePositions
: mappedNativeRoutePositions;
const usedBossSpawnIds = new Set(); const usedBossSpawnIds = new Set();
const bossObjectives = encounters.map((encounter, index) => { const bossObjectives = encounters.map((encounter, index) => {
const candidate = bossCandidatesByEntry.get(Number(encounter.creatureId)); const candidate = bossCandidatesByEntry.get(Number(encounter.creatureId));
const alpha = encounters.length <= 1 ? 1 : index / (encounters.length - 1); const alpha = encounters.length <= 1 ? 1 : index / (encounters.length - 1);
const routePosition = interpolate(trashAnchor.position, bossAnchor.position, 0.18 + alpha * 0.82); const routeAlpha = 0.18 + alpha * 0.82;
const nativeRoutePosition = nativeRoutePositions.length
? nativeRoutePositions[Math.round(routeAlpha * (nativeRoutePositions.length - 1))]
: undefined;
const routePosition = nativeRoutePosition
?? interpolate(trashAnchor.position, bossAnchor.position, routeAlpha);
const position = encounter.eventOnly const position = encounter.eventOnly
? [-151.27, -102.82, 252.26] ? [-151.27, -102.82, 252.26]
: candidate : candidate
? mapPosition(candidate.position) ? mapPosition(candidate.position)
: offsetPosition(routePosition, index % 5, 2.4); : nativeRoutePosition ?? offsetPosition(routePosition, index % 5, 2.4);
const id = encounter.eventOnly const id = encounter.eventOnly
? "mutanus-spawn" ? "mutanus-spawn"
: candidate?.id ?? `encounter-${encounter.encounterId}`; : candidate?.id ?? `encounter-${encounter.encounterId}`;
@@ -966,7 +1082,7 @@ async function compile() {
}); });
for (let index = 0; index < 15; index += 1) { for (let index = 0; index < 15; index += 1) {
const segment = index < 6 const segment = index < 6
? interpolate(playerAnchor.position, trashAnchor.position, 0.25 + index * 0.1) ? interpolate(entrancePosition, trashAnchor.position, 0.25 + index * 0.1)
: interpolate(trashAnchor.position, bossAnchor.position, 0.08 + (index - 6) * 0.085); : interpolate(trashAnchor.position, bossAnchor.position, 0.08 + (index - 6) * 0.085);
staticSpawns.push({ staticSpawns.push({
id: `generated-trash-${index + 1}`, id: `generated-trash-${index + 1}`,
@@ -979,9 +1095,10 @@ async function compile() {
} }
const allPoints = [ const allPoints = [
playerAnchor.position, entrancePosition,
trashAnchor.position, ...(coordinateAlignment === "player-anchor" || syntheticTrash
bossAnchor.position, ? [trashAnchor.position, bossAnchor.position]
: []),
...staticSpawns.map((spawn) => spawn.position), ...staticSpawns.map((spawn) => spawn.position),
...roamingPacks.flatMap((pack) => pack.waypoints), ...roamingPacks.flatMap((pack) => pack.waypoints),
]; ];
@@ -1010,6 +1127,10 @@ async function compile() {
modelCoverageWarning, modelCoverageWarning,
...(syntheticTrash ...(syntheticTrash
? ["Trash identities and placements are procedural; boss identities come from the installed Ascension encounter catalog."] ? ["Trash identities and placements are procedural; boss identities come from the installed Ascension encounter catalog."]
: coordinateAlignment === "source"
? [`Server positions use the packaged ${environmentMode} environment's native world coordinates.`]
: coordinateAlignment === "source-transform"
? [`Server positions use the packaged ${environmentMode} environment's authoritative WDT placement transform.`]
: [`Server positions were aligned to the packaged ${environmentMode} environment at its reviewed player anchor.`]), : [`Server positions were aligned to the packaged ${environmentMode} environment at its reviewed player anchor.`]),
]; ];
@@ -1034,12 +1155,12 @@ async function compile() {
bounds, bounds,
entrance: { entrance: {
name: `${recipe.title} Entrance`, name: `${recipe.title} Entrance`,
footPosition: vector(playerAnchor.position), footPosition: vector(entrancePosition),
forward: [Math.sin(entranceYaw), 0, Math.cos(entranceYaw)], forward: [Math.sin(entranceYaw), 0, Math.cos(entranceYaw)],
yaw: entranceYaw, yaw: entranceYaw,
}, },
areas: [ areas: [
{ id: "entrance", name: `${recipe.title} Entrance`, center: vector(playerAnchor.position), radius: 35 }, { id: "entrance", name: `${recipe.title} Entrance`, center: vector(entrancePosition), radius: 35 },
...bosses.map((boss) => ({ ...bosses.map((boss) => ({
id: `area-${slugPart(boss.name)}`, id: `area-${slugPart(boss.name)}`,
name: `${boss.name}'s Encounter`, name: `${boss.name}'s Encounter`,
@@ -1056,6 +1177,7 @@ async function compile() {
serverGameObjects: draft?.gameObjects ?? [], serverGameObjects: draft?.gameObjects ?? [],
provenance: { provenance: {
clientBuild: recipe.clientBuild, clientBuild: recipe.clientBuild,
coordinateAlignment,
clientArchiveHashes: assetPackage.source.checksums ?? {}, clientArchiveHashes: assetPackage.source.checksums ?? {},
databaseAdapter: databaseKind, databaseAdapter: databaseKind,
databaseRevision: draft?.spawns?.length databaseRevision: draft?.spawns?.length
@@ -1068,6 +1190,12 @@ async function compile() {
? sourceBasis === "z-y-negative-x" ? sourceBasis === "z-y-negative-x"
? "three(x,y,z)=(wow.y,wow.z,wow.x)" ? "three(x,y,z)=(wow.y,wow.z,wow.x)"
: "three(x,y,z)=(-wow.y,wow.z,wow.x)" : "three(x,y,z)=(-wow.y,wow.z,wow.x)"
: coordinateAlignment === "source-transform"
? `three(x,y,z)=rotate-y(${sourceBasis === "flip-x" ? "(-draft.x,draft.y,draft.z)" : "draft(x,y,z)"}-[${sourceTransform.origin.join(",")}],${sourceTransform.yawDegrees}deg)`
: coordinateAlignment === "source"
? sourceBasis === "flip-x"
? "three(x,y,z)=(-draft.x,draft.y,draft.z)"
: "three(x,y,z)=draft(x,y,z)"
: sourceBasis === "flip-x" : sourceBasis === "flip-x"
? "three(x,y,z)=(-draft.x,draft.y,draft.z)+package-anchor-translation" ? "three(x,y,z)=(-draft.x,draft.y,draft.z)+package-anchor-translation"
: "three(x,y,z)=draft(x,y,z)+package-anchor-translation", : "three(x,y,z)=draft(x,y,z)+package-anchor-translation",
+194
View File
@@ -0,0 +1,194 @@
import fs from "node:fs";
import path from "node:path";
const projectRoot = path.resolve(import.meta.dirname, "..");
const clientRoot = path.resolve(projectRoot, "..", "LadiksMPQEditor", "client-current");
const spellPath = path.join(clientRoot, "area-52", "patch-D", "DBFilesClient", "Spell.dbc");
const supportRoot = path.join(clientRoot, "patch-S", "DBFilesClient");
const runtimePath = path.join(projectRoot, "src", "game", "generated", "coaAbilityRuntimeCatalog.json");
const outputPath = path.join(projectRoot, "src", "game", "generated", "coaTriggeredSpellCatalog.json");
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function dbcNumberTable(filePath, valueField, floating = false) {
const buffer = fs.readFileSync(filePath);
assert(buffer.toString("ascii", 0, 4) === "WDBC", `${path.basename(filePath)} is not a WDBC file`);
const recordCount = buffer.readUInt32LE(4);
const recordSize = buffer.readUInt32LE(12);
return new Map(Array.from({ length: recordCount }, (_, index) => {
const base = 20 + index * recordSize;
return [
buffer.readUInt32LE(base),
floating ? buffer.readFloatLE(base + valueField * 4) : buffer.readInt32LE(base + valueField * 4),
];
}));
}
function dbcRangeTable(filePath) {
const buffer = fs.readFileSync(filePath);
assert(buffer.toString("ascii", 0, 4) === "WDBC", `${path.basename(filePath)} is not a WDBC file`);
const recordCount = buffer.readUInt32LE(4);
const recordSize = buffer.readUInt32LE(12);
return new Map(Array.from({ length: recordCount }, (_, index) => {
const base = 20 + index * recordSize;
return [buffer.readUInt32LE(base), {
min: Math.max(0, buffer.readFloatLE(base + 4), buffer.readFloatLE(base + 8)),
max: Math.max(0, buffer.readFloatLE(base + 12), buffer.readFloatLE(base + 16)),
}];
}));
}
function referencedSpellIds(runtime) {
const ids = new Set();
const inspectEffects = (effects) => {
for (const effect of effects ?? []) {
if (effect.kind === "trigger-spell" && effect.spellId > 0) ids.add(effect.spellId);
if (effect.kind !== "apply-aura") continue;
for (const proc of effect.aura?.procs ?? []) {
const spellId = proc.action?.kind === "custom" ? proc.action.data?.spellId : 0;
if (typeof spellId === "number" && spellId > 0) ids.add(spellId);
}
}
};
for (const abilities of Object.values(runtime.abilitiesByClass)) {
for (const ability of abilities) {
inspectEffects(ability.effects);
for (const rank of ability.ranks ?? []) inspectEffects(rank.effects);
}
}
return ids;
}
function cleanText(value) {
return value.replace(/\r/g, "").replace(/\s+/g, " ").trim();
}
function spellParser(buffer, castTimes, durations, radii, ranges) {
assert(buffer.toString("ascii", 0, 4) === "WDBC", "Spell.dbc is not a WDBC file");
const recordCount = buffer.readUInt32LE(4);
const fieldCount = buffer.readUInt32LE(8);
const recordSize = buffer.readUInt32LE(12);
const stringSize = buffer.readUInt32LE(16);
assert(fieldCount >= 234 && recordSize >= fieldCount * 4, "Unexpected Spell.dbc schema");
const stringsOffset = 20 + recordCount * recordSize;
const offsets = new Map(Array.from({ length: recordCount }, (_, index) => {
const base = 20 + index * recordSize;
return [buffer.readUInt32LE(base), base];
}));
const readString = (offset) => {
if (!offset || offset >= stringSize) return "";
const start = stringsOffset + offset;
const end = buffer.indexOf(0, start);
return buffer.toString("utf8", start, end < 0 ? buffer.length : end);
};
return (id) => {
const base = offsets.get(id);
if (base === undefined) return null;
const unsigned = (field) => buffer.readUInt32LE(base + field * 4);
const signed = (field) => buffer.readInt32LE(base + field * 4);
const float = (field) => buffer.readFloatLE(base + field * 4);
const localized = (firstField) => {
for (let locale = 0; locale < 16; locale += 1) {
const value = readString(unsigned(firstField + locale));
if (value) return cleanText(value);
}
return "";
};
const effects = Array.from({ length: 3 }, (_, effectIndex) => ({
index: effectIndex,
effectType: unsigned(71 + effectIndex),
auraType: unsigned(95 + effectIndex),
basePoints: signed(80 + effectIndex) + 1,
dieSides: signed(74 + effectIndex),
pointsPerLevel: float(77 + effectIndex),
mechanic: unsigned(83 + effectIndex),
implicitTargetA: unsigned(86 + effectIndex),
implicitTargetB: unsigned(89 + effectIndex),
radiusIndex: unsigned(92 + effectIndex),
radius: radii.get(unsigned(92 + effectIndex)) ?? 0,
periodMs: unsigned(98 + effectIndex),
valueMultiplier: float(101 + effectIndex),
chainTargets: unsigned(104 + effectIndex),
itemType: unsigned(107 + effectIndex),
miscValue: signed(110 + effectIndex),
miscValueB: signed(113 + effectIndex),
triggerSpellId: unsigned(116 + effectIndex),
pointsPerCombo: float(119 + effectIndex),
coefficient: float(229 + effectIndex),
})).filter((effect) => effect.effectType || effect.auraType || effect.basePoints !== 1 || effect.triggerSpellId);
return {
id,
name: localized(136),
rankText: localized(153),
description: localized(170) || localized(187),
categoryId: unsigned(1),
dispelType: unsigned(2),
mechanic: unsigned(3),
attributes: Array.from({ length: 8 }, (_, index) => unsigned(4 + index)),
castTimeIndex: unsigned(28),
castTimeMs: Math.max(0, castTimes.get(unsigned(28)) ?? 0),
recoveryTimeMs: unsigned(29),
categoryRecoveryTimeMs: unsigned(30),
interruptFlags: unsigned(31),
auraInterruptFlags: unsigned(32),
channelInterruptFlags: unsigned(33),
procFlags: unsigned(34),
procChance: unsigned(35),
procCharges: unsigned(36),
maxLevel: unsigned(37),
baseLevel: unsigned(38),
spellLevel: unsigned(39),
durationIndex: unsigned(40),
durationMs: durations.get(unsigned(40)) ?? 0,
powerType: signed(41),
powerCost: unsigned(42),
powerCostPerLevel: unsigned(43),
powerCostPerSecond: unsigned(44),
powerCostPerSecondPerLevel: unsigned(45),
rangeIndex: unsigned(46),
rangeMin: ranges.get(unsigned(46))?.min ?? 0,
rangeMax: ranges.get(unsigned(46))?.max ?? 0,
projectileSpeed: float(47),
stackAmount: unsigned(49),
equippedItemClass: signed(67),
equippedItemSubclassMask: signed(68),
equippedItemInventoryMask: signed(69),
iconId: unsigned(133),
powerCostPercentage: unsigned(204),
startRecoveryCategory: unsigned(205),
startRecoveryTimeMs: unsigned(206),
maximumTargets: unsigned(212),
damageClass: unsigned(213),
preventionType: unsigned(214),
schoolMask: unsigned(225),
runeCostId: unsigned(226),
powerDisplayId: unsigned(228),
effects,
};
};
}
assert(fs.existsSync(spellPath), `CoA Spell.dbc was not found at ${spellPath}`);
const runtime = JSON.parse(fs.readFileSync(runtimePath, "utf8"));
const castTimes = dbcNumberTable(path.join(supportRoot, "SpellCastTimes.dbc"), 1);
const durations = dbcNumberTable(path.join(supportRoot, "SpellDuration.dbc"), 3);
const radii = dbcNumberTable(path.join(supportRoot, "SpellRadius.dbc"), 3, true);
const ranges = dbcRangeTable(path.join(supportRoot, "SpellRange.dbc"));
const parseSpell = spellParser(fs.readFileSync(spellPath), castTimes, durations, radii, ranges);
const pending = [...referencedSpellIds(runtime)];
const spells = new Map();
while (pending.length) {
const id = pending.shift();
if (spells.has(id)) continue;
const spell = parseSpell(id);
if (!spell) continue;
spells.set(id, spell);
for (const effect of spell.effects) {
if (effect.triggerSpellId > 0 && !spells.has(effect.triggerSpellId)) pending.push(effect.triggerSpellId);
}
}
fs.writeFileSync(outputPath, `${JSON.stringify({ schemaVersion: 1, spells: [...spells.values()] })}\n`, "utf8");
console.log(`Wrote ${outputPath} with ${spells.size} triggered spell records.`);

Some files were not shown because too many files have changed in this diff Show More