Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c245a199b6 | ||
|
|
eca6b5db9b | ||
|
|
b12fb84859 | ||
|
|
55cfd43d66 |
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
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
|
||||
|
||||
- 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`;
|
||||
- releasing right mouse stops camera motion;
|
||||
- no browser mouse-capture prompt appears.
|
||||
|
||||
|
||||
+2
-1
@@ -185,6 +185,7 @@ on the development PC. Stop if any test or build fails:
|
||||
|
||||
```powershell
|
||||
Set-Location F:\Projects\HealerMan
|
||||
npm run release:check
|
||||
|
||||
$VersionName = '0.1.3'
|
||||
$VersionCode = 1003
|
||||
@@ -206,8 +207,8 @@ npm run build
|
||||
npm test -- --run src/avatar
|
||||
npx vitest run src/game/inputManager.test.ts src/game/inputMath.test.ts
|
||||
|
||||
npm run release:check
|
||||
git status --short
|
||||
git diff --check
|
||||
git add -A
|
||||
git status --short
|
||||
git diff --cached --stat
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
|
||||
- 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
|
||||
avatar tests passed, and `npm run build` completed successfully with 954
|
||||
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
@@ -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
|
||||
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
|
||||
`healer-man-release-password.dpapi` file is present, blank signing-password
|
||||
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:
|
||||
|
||||
```powershell
|
||||
git diff --check
|
||||
npm run release:check
|
||||
git status --short
|
||||
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:
|
||||
|
||||
```powershell
|
||||
@@ -520,7 +538,7 @@ Set-Location F:\Projects\HealerMan
|
||||
npm run content:test
|
||||
npm run build
|
||||
git status --short
|
||||
git diff --check
|
||||
npm run release:check
|
||||
git add -A
|
||||
git status --short
|
||||
git diff --cached --stat
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/TempestKeepArcane",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/AuchindounDraenei",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "burning-crusade",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/Blackfathom",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [20, 30]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/BlackrockDepths",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/BlackRockSpire",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [55, 60]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/HellfireDemon",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [58, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/TempestKeepAtrium",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/DireMaul",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source-transform",
|
||||
"coordinateTransform": {"origin": [0, 0, 0], "yawDegrees": 180},
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [50, 60]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "burning-crusade",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/GnomeragonInstance",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [25, 40]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/HellfireRampart",
|
||||
"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]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/Sunwell5ManFix",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [70, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/AuchindounEthereal",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/Mauradon",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 55]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/TempestKeepFactory",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/OrgrimmarInstance",
|
||||
"environmentMode": "global-wmo",
|
||||
"globalWmoAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [13, 25]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/RazorfenDowns",
|
||||
"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]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/RazorfenKraulInstance",
|
||||
"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]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a",
|
||||
"mapDirectory": "World/Maps/MonasteryInstances",
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"adtSourceBasis": "z-y-negative-x",
|
||||
"difficultyVariants": [
|
||||
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [26, 45] }
|
||||
],
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/SchoolofNecromancy",
|
||||
"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]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/AuchindounDemon",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/AuchindounShadow",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [65, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
"expansion": "classic",
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"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]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
"extraction": {"rootWmo": "World/wmo/Dungeon/LD_ShadowFang/LD_ShadowFang.wmo", "doodadSets": ["Set_$DefaultGlobal"]},
|
||||
"navigation": {"cellSize": 0.2, "cellHeight": 0.1, "agentHeight": 1.8, "agentRadius": 0.36, "agentMaxClimb": 0.5, "agentMaxSlope": 50},
|
||||
"extraction": {"adtTiles": "all-present"},
|
||||
"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"},
|
||||
"exceptions": []
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/HellfireMilitary",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/CoilfangDraenei",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/CoilfangPumping",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/StormwindJail",
|
||||
"environmentMode": "global-wmo",
|
||||
"globalWmoAlignment": "source",
|
||||
"difficultyVariants": [
|
||||
{
|
||||
"id": "normal",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/Stratholme",
|
||||
"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]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/SunkenTemple",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [45, 55]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/Uldaman",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [
|
||||
{
|
||||
"id": "normal",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/CoilfangMarsh",
|
||||
"environmentMode": "global-wmo",
|
||||
"coordinateAlignment": "source",
|
||||
"difficultyVariants": [{"id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [60, 70]}],
|
||||
"clientSource": {"rootEnvironmentVariable": "WOW_CLIENT_ROOT", "relativeDefault": "../wow335a"},
|
||||
"serverSource": {"rootEnvironmentVariable": "WOW_SERVER_DATA", "adapter": "normalized-json", "readOnly": true},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"exceptions": [],
|
||||
"expansion": "wrath",
|
||||
"extraction": {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"clientBuild": "3.3.5a-Ascension",
|
||||
"mapDirectory": "World/Maps/TanarisInstance",
|
||||
"environmentMode": "detect",
|
||||
"coordinateAlignment": "source",
|
||||
"adtSourceBasis": "z-y-negative-x",
|
||||
"difficultyVariants": [
|
||||
{ "id": "normal", "difficulty": "normal", "spawnMask": 1, "levelRange": [40, 54] }
|
||||
],
|
||||
|
||||
Generated
+3
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "healer-man",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "healer-man",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.8",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "8.4.1",
|
||||
"@capacitor/core": "8.4.1",
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "8.4.1",
|
||||
"@dimforge/rapier3d-compat": "0.19.2",
|
||||
"@gltf-transform/cli": "^4.4.1",
|
||||
"@gltf-transform/core": "^4.4.2",
|
||||
"@recast-navigation/core": "0.43.1",
|
||||
|
||||
+7
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "healer-man",
|
||||
"private": true,
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
@@ -20,7 +20,8 @@
|
||||
"coa:sync:live": "node scripts/sync-ascension-coa.mjs --tooltips",
|
||||
"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: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:forensics": "node scripts/manastorm-import/forensics-cli.mjs",
|
||||
"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:audit": "node scripts/asset-pipeline/audit-ktx2.mjs --visual-only --require-all",
|
||||
"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: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: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: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",
|
||||
"preview": "vite preview --host 0.0.0.0",
|
||||
"pretest": "npm run loot:generate",
|
||||
@@ -118,6 +121,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "8.4.1",
|
||||
"@dimforge/rapier3d-compat": "0.19.2",
|
||||
"@gltf-transform/cli": "^4.4.1",
|
||||
"@gltf-transform/core": "^4.4.2",
|
||||
"@recast-navigation/core": "0.43.1",
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
{
|
||||
"id": "visual",
|
||||
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
|
||||
"checksum": "42d57961c31f54ecc7bc78829751110fc89294e6318336b8f260e132e478d13d",
|
||||
"byteLength": 10266900,
|
||||
"checksum": "7e4c7c75a898f5000fcb686dcfc6d3c1c4e675dc3dac5f7a8d48e53372b9f364",
|
||||
"byteLength": 7068884,
|
||||
"triangleCount": 248302,
|
||||
"compression": "meshopt+ktx2"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
||||
{
|
||||
"id": "visual",
|
||||
"url": "/assets/game/manastorm/230-blackrockdepths/visual.glb",
|
||||
"checksum": "42d57961c31f54ecc7bc78829751110fc89294e6318336b8f260e132e478d13d",
|
||||
"byteLength": 10266900,
|
||||
"checksum": "7e4c7c75a898f5000fcb686dcfc6d3c1c4e675dc3dac5f7a8d48e53372b9f364",
|
||||
"byteLength": 7068884,
|
||||
"triangleCount": 248302,
|
||||
"compression": "meshopt+ktx2"
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"versionName": "0.1.4",
|
||||
"versionCode": 1004
|
||||
"versionName": "0.1.8",
|
||||
"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));
|
||||
}
|
||||
|
||||
function horizontalDistanceSquared(left, right) {
|
||||
return (left[0] - right[0]) ** 2 + (left[2] - right[2]) ** 2;
|
||||
}
|
||||
|
||||
function normalizedName(value) {
|
||||
return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||
}
|
||||
@@ -201,6 +205,7 @@ const ADT_SOURCE_BASES = new Set([
|
||||
"z-y-negative-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) {
|
||||
if (environmentMode !== "adt-hybrid") {
|
||||
@@ -220,6 +225,43 @@ function sourceBasisFor(recipe, environmentMode) {
|
||||
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
|
||||
* packages have an additional horizontal basis change baked into their GLBs.
|
||||
@@ -238,7 +280,39 @@ function sourceToPackageBasis(position, environmentMode, sourceBasis) {
|
||||
: 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
|
||||
?? draft?.spawns?.[0]?.position
|
||||
?? playerAnchor.position;
|
||||
@@ -764,6 +838,8 @@ async function compile() {
|
||||
}
|
||||
const environmentMode = inferEnvironmentMode(recipe, assetPackage);
|
||||
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 trashAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "trash");
|
||||
const bossAnchor = assetPackage.anchors.find((anchor) => anchor.kind === "boss");
|
||||
@@ -774,7 +850,7 @@ async function compile() {
|
||||
trashAnchor.position[0] - playerAnchor.position[0],
|
||||
trashAnchor.position[2] - playerAnchor.position[2],
|
||||
);
|
||||
const entranceYaw = Number.isFinite(playerAnchor.yaw)
|
||||
let entranceYaw = Number.isFinite(playerAnchor.yaw)
|
||||
? playerAnchor.yaw
|
||||
: inferredEntranceYaw;
|
||||
|
||||
@@ -794,6 +870,13 @@ async function compile() {
|
||||
environmentMode,
|
||||
playerAnchor,
|
||||
sourceBasis,
|
||||
coordinateAlignment,
|
||||
sourceTransform,
|
||||
);
|
||||
const entrancePosition = mapPosition(
|
||||
draft?.entrance
|
||||
?? draft?.spawns?.[0]?.position
|
||||
?? playerAnchor.position,
|
||||
);
|
||||
const sourceEntities = (draft?.entities ?? [])
|
||||
.filter((entity) => (
|
||||
@@ -876,16 +959,49 @@ async function compile() {
|
||||
const entry = Number(spawn.entityId.replace("entry-", ""));
|
||||
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 bossObjectives = encounters.map((encounter, index) => {
|
||||
const candidate = bossCandidatesByEntry.get(Number(encounter.creatureId));
|
||||
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
|
||||
? [-151.27, -102.82, 252.26]
|
||||
: candidate
|
||||
? mapPosition(candidate.position)
|
||||
: offsetPosition(routePosition, index % 5, 2.4);
|
||||
: nativeRoutePosition ?? offsetPosition(routePosition, index % 5, 2.4);
|
||||
const id = encounter.eventOnly
|
||||
? "mutanus-spawn"
|
||||
: candidate?.id ?? `encounter-${encounter.encounterId}`;
|
||||
@@ -966,7 +1082,7 @@ async function compile() {
|
||||
});
|
||||
for (let index = 0; index < 15; index += 1) {
|
||||
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);
|
||||
staticSpawns.push({
|
||||
id: `generated-trash-${index + 1}`,
|
||||
@@ -979,9 +1095,10 @@ async function compile() {
|
||||
}
|
||||
|
||||
const allPoints = [
|
||||
playerAnchor.position,
|
||||
trashAnchor.position,
|
||||
bossAnchor.position,
|
||||
entrancePosition,
|
||||
...(coordinateAlignment === "player-anchor" || syntheticTrash
|
||||
? [trashAnchor.position, bossAnchor.position]
|
||||
: []),
|
||||
...staticSpawns.map((spawn) => spawn.position),
|
||||
...roamingPacks.flatMap((pack) => pack.waypoints),
|
||||
];
|
||||
@@ -1010,6 +1127,10 @@ async function compile() {
|
||||
modelCoverageWarning,
|
||||
...(syntheticTrash
|
||||
? ["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.`]),
|
||||
];
|
||||
|
||||
@@ -1034,12 +1155,12 @@ async function compile() {
|
||||
bounds,
|
||||
entrance: {
|
||||
name: `${recipe.title} Entrance`,
|
||||
footPosition: vector(playerAnchor.position),
|
||||
footPosition: vector(entrancePosition),
|
||||
forward: [Math.sin(entranceYaw), 0, Math.cos(entranceYaw)],
|
||||
yaw: entranceYaw,
|
||||
},
|
||||
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) => ({
|
||||
id: `area-${slugPart(boss.name)}`,
|
||||
name: `${boss.name}'s Encounter`,
|
||||
@@ -1056,6 +1177,7 @@ async function compile() {
|
||||
serverGameObjects: draft?.gameObjects ?? [],
|
||||
provenance: {
|
||||
clientBuild: recipe.clientBuild,
|
||||
coordinateAlignment,
|
||||
clientArchiveHashes: assetPackage.source.checksums ?? {},
|
||||
databaseAdapter: databaseKind,
|
||||
databaseRevision: draft?.spawns?.length
|
||||
@@ -1068,6 +1190,12 @@ async function compile() {
|
||||
? 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)"
|
||||
: 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"
|
||||
? "three(x,y,z)=(-draft.x,draft.y,draft.z)+package-anchor-translation"
|
||||
: "three(x,y,z)=draft(x,y,z)+package-anchor-translation",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user