Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d060296ade | ||
|
|
c245a199b6 | ||
|
|
eca6b5db9b | ||
|
|
b12fb84859 | ||
|
|
55cfd43d66 |
No files matched your search
@@ -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.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Balance benchmarks
|
||||
|
||||
The balance benchmark suite provides repeatable evidence for tuning World of Warcraft, CoA, and Runes of Magic classes. It runs fixed builds through versioned damage, tanking, healing, movement, stacking, and resource-endurance scenarios. The goal is to reveal balance changes and outliers, not to name a universally best class.
|
||||
|
||||
## Default cohort
|
||||
|
||||
The default comparison cohort is level 60, matching CoA's authored cap while keeping the three content sources in one cohort. It uses the `coefficient-empty` preset: no equipment, so the first cohort isolates base kits, scaling coefficients, talents, and resources. Gear-normalized cohorts should be added separately rather than mixed with this one.
|
||||
|
||||
Every comparison must use the same gear preset, scenario version, baseline policy ID, duration scale, and seed set. A changed level, gear preset, scenario, policy, or app version creates a new cohort and should not be compared directly with an older one.
|
||||
|
||||
Run the suite with:
|
||||
|
||||
```sh
|
||||
npm run balance:benchmark
|
||||
```
|
||||
|
||||
Use `npm run balance:smoke` for a short end-to-end check. The full command accepts comma-separated `--class`, `--role`, `--scenario`, and `--seeds` filters, plus `--level`, `--duration-scale`, and `--output`. For example:
|
||||
|
||||
```sh
|
||||
npm run balance:benchmark -- --class priest,rom-priest --role healer --seeds 1,2,3
|
||||
```
|
||||
|
||||
The command is the intended stable entry point for the benchmark runner. Its output directory contains:
|
||||
|
||||
- `matrix.json`: raw run reports, build and scenario definitions, seed metadata, validation results, ability details, resources, and event counts.
|
||||
- `matrix.csv`: one row per scenario, build, and metric, with observed/null counts plus median, p10, and p90 values.
|
||||
- `summary.md`: a concise role-separated comparison for review and pull-request evidence.
|
||||
|
||||
Keep the JSON artifact when investigating a result. The CSV and Markdown are derived views and can be regenerated from it.
|
||||
|
||||
## Reading results
|
||||
|
||||
The reported center is the median across validated seeded runs. The p10–p90 interval shows run-to-run spread; a wide interval calls for more seeds or investigation before tuning. Invalid runs are counted but excluded from distributions. An early death or other scenario stop can be a valid outcome and should not be discarded merely because the full duration was not reached.
|
||||
|
||||
Interpret each role as a vector of outcomes, not a single ranking:
|
||||
|
||||
- Damage builds emphasize DPS and priority-target damage, with deaths and mechanics failures alongside throughput.
|
||||
- Tanks emphasize damage taken per second, mitigation, avoidance, threat control, minimum health, survival, and their damage contribution.
|
||||
- Healers emphasize healing per second, consumed absorbs, overheal, party health, deaths, and time to resource failure.
|
||||
- Movement and stack scenarios add movement interrupts and stack compliance to the relevant role vector.
|
||||
|
||||
No composite score is produced. Compare a build only with other builds in the same scenario version and cohort, and inspect the underlying metrics before making a balance change. A null time to resource failure means the resource was not exhausted during that run; it is not zero. Similarly, a null time to first death means no death occurred.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The baseline policy is a deterministic, maintainable reference policy, not an optimal player rotation. Skilled play may outperform it, and policy changes can move results without a class-data change.
|
||||
- The initial `coefficient-empty` cohort does not model production gear breakpoints or itemization. Add explicit equal-budget gear presets for that work.
|
||||
- Runes of Magic coverage is primary-class-only initially. Secondary-class and dual-class interactions require their own explicitly versioned cohort when supported.
|
||||
- Results cover mechanics represented by the simulator and scenarios; they do not replace live encounter playtesting, usability review, or PvP testing.
|
||||
- Small seed sets can miss rare random outcomes. Treat p10 and p90 as descriptive bounds for the sampled runs, not guarantees.
|
||||
- Total metrics depend on scenario duration. Compare rates or fixed-duration totals only within the same scenario definition.
|
||||
- Gear-stat budgets must be equivalent, but equivalent budgets do not guarantee identical interactions with every class kit.
|
||||
+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,102 @@ 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.
|
||||
|
||||
# 2026-08-28 — configurable render caps and idle-loop efficiency
|
||||
|
||||
- Opened the saved Human Priest roster after a fresh client reload. The full
|
||||
body and starter staff rendered correctly in the single character-preview
|
||||
canvas, which reported the persisted 30 FPS preview cap and manual frame loop.
|
||||
- Entered Wailing Caverns as healer and exercised the gameplay setting live.
|
||||
The performance probe settled at about 30.5 FPS for the 30 FPS option and 60
|
||||
FPS for the 60 FPS option; combat and fixed-step physics continued normally.
|
||||
- Pausing switched the dungeon canvas to demand rendering. Its performance
|
||||
counters remained byte-for-byte unchanged over 1.4 seconds, confirming that
|
||||
the local paused scene did not keep rendering in the background.
|
||||
- Returned gameplay rendering to the default 60 FPS and left the character
|
||||
preview at 30 FPS. Evidence reviewed during the browser run included the
|
||||
roster-preview and live dungeon screenshots. The final fresh session emitted
|
||||
no errors; the existing third-party initialization deprecation warning
|
||||
remained non-blocking.
|
||||
- Verification: the focused efficiency tests passed all 83 cases, all 28 avatar
|
||||
tests passed, the complete Vitest suite passed all 809 cases across 148 files,
|
||||
and the production build completed with 1,043 modules transformed. KTX2 and
|
||||
environment-performance audits remained green; the existing large-chunk
|
||||
advisory remains informational.
|
||||
|
||||
# 2026-08-30 — authored weapon grips and ready stances
|
||||
|
||||
- Reloaded the offline client and switched a Human female preview directly
|
||||
between Hunter, Warrior, and Priest. The bow, sword/shield, and staff changed
|
||||
immediately to their native HoldBow, Ready1H, and Ready2HL poses; no stale
|
||||
class pose or backward procedural axis remained.
|
||||
- Verified RuneWaker Human Warrior and Scout previews against their explicit
|
||||
hand/shield locator frames. The one-hand, shield, and bow silhouettes used the
|
||||
RuneWaker socket-axis adapter and remained proportionate to the character.
|
||||
Also checked the Ascension Human Ranger fallback with its bow visible in a
|
||||
supported ready pose.
|
||||
- The saved Human Priest roster preview showed the complete body and staff with
|
||||
both hands aligned to the shaft. In Wailing Caverns, the player and party
|
||||
equipment remained attached through live combat animation without a second
|
||||
local weapon swing or load-time placement jump.
|
||||
- Evidence: `playtest-artifacts/2026-08-30-weapon-grip/weapon-grip-roster.png`,
|
||||
`human-hunter-holdbow.png`, `human-warrior-ready1h.png`,
|
||||
`human-priest-ready2hl.png`, `runewaker-warrior-sockets.png`,
|
||||
`runewaker-scout-bow.png`, `ascension-ranger-bow.png`, and
|
||||
`weapon-grip-dungeon.png`.
|
||||
- Verification: all 41 avatar tests passed, the 37-test focused weapon/stance
|
||||
suite passed, and the production build completed with 1,047 modules
|
||||
transformed. Asset audits remained green with zero KTX2 fallbacks and zero
|
||||
environment-performance regressions; the existing large-chunk advisory
|
||||
remains informational.
|
||||
|
||||
# 2026-08-30 — bounded combat VFX bursts and silent audio default
|
||||
|
||||
- Created a fresh offline Human Priest and confirmed the in-game Options panel
|
||||
opened with **Master volume** at 0%. Gameplay and preview frame-rate controls,
|
||||
VFX quality, and per-actor VFX/audio controls remained available.
|
||||
- Entered Wailing Caverns as healer, sent the party into combat, and captured a
|
||||
live four-effect spell burst. High quality reported four active effect systems
|
||||
and zero combat point lights; additive particles, impacts, melee arcs, the HUD,
|
||||
and combat telegraphs remained visible.
|
||||
- The captured burst reported 60 FPS, a 16.7 ms average frame, a 16.8 ms slowest
|
||||
frame, 258 draw calls, and 353,526 triangles at 1.5 pixel ratio. A three-second
|
||||
combat sample reached five concurrent effects, stayed between 54.1 and 60 FPS,
|
||||
and returned to a 16.7 ms frame average.
|
||||
- A cold Vite dynamic import failed once while first opening character creation;
|
||||
reloading after compilation resolved it. The final dungeon run had no further
|
||||
errors; the existing third-party initialization deprecation warning remained.
|
||||
- Verification: the complete Vitest suite passed all 839 cases across 153 files,
|
||||
the production build completed with 1,047 modules transformed, KTX2 and
|
||||
environment-performance audits remained green, and the release-candidate
|
||||
check passed. 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.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "healer-man",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.9",
|
||||
"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",
|
||||
|
||||
+9
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "healer-man",
|
||||
"private": true,
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.9",
|
||||
"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,17 @@
|
||||
"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",
|
||||
"balance:benchmark": "vite-node scripts/combat-benchmark/run.ts",
|
||||
"balance:smoke": "vite-node scripts/combat-benchmark/run.ts --class mage --role damage --scenario damage-single-burst --seeds 1 --duration-scale 0.2",
|
||||
"prebuild": "npm run loot:generate && npm run assets:ktx2:audit && npm run assets:performance:audit",
|
||||
"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 +123,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.9",
|
||||
"versionCode": 1009
|
||||
}
|
||||
@@ -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,174 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { CLASSES, type ClassId } from "../../src/app/characterCatalog";
|
||||
import {
|
||||
CANONICAL_BENCHMARK_LEVEL,
|
||||
loadCanonicalBenchmarkBuilds,
|
||||
} from "../../src/game/benchmark/benchmarkBuilds";
|
||||
import { BENCHMARK_POLICY_ID } from "../../src/game/benchmark/benchmarkController";
|
||||
import {
|
||||
renderBenchmarkMatrixMarkdown,
|
||||
serializeBenchmarkMatrixCsv,
|
||||
} from "../../src/game/benchmark/benchmarkReport";
|
||||
import { runCombatBenchmark } from "../../src/game/benchmark/benchmarkRunner";
|
||||
import { BENCHMARK_SCENARIOS } from "../../src/game/benchmark/benchmarkScenarios";
|
||||
import type { BenchmarkMatrixReport } from "../../src/game/benchmark/benchmarkTypes";
|
||||
import type { PartyRole } from "../../src/game/partyRoles";
|
||||
|
||||
interface CliOptions {
|
||||
readonly level: number;
|
||||
readonly classIds: readonly ClassId[] | null;
|
||||
readonly roles: readonly PartyRole[] | null;
|
||||
readonly scenarioIds: readonly string[] | null;
|
||||
readonly seeds: readonly number[];
|
||||
readonly durationScale: number;
|
||||
readonly output: string | null;
|
||||
}
|
||||
|
||||
const projectRoot = resolve(import.meta.dirname, "../..");
|
||||
|
||||
function argumentValue(arguments_: readonly string[], name: string): string | null {
|
||||
const index = arguments_.indexOf(name);
|
||||
if (index < 0) return null;
|
||||
const value = arguments_[index + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function list(value: string | null): readonly string[] | null {
|
||||
if (value === null) return null;
|
||||
const values = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
||||
if (!values.length) throw new Error("A comma-separated filter cannot be empty.");
|
||||
return values;
|
||||
}
|
||||
|
||||
function finiteNumber(value: string | null, fallback: number, name: string): number {
|
||||
if (value === null) return fallback;
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) throw new Error(`${name} must be a finite number.`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseOptions(arguments_: readonly string[]): CliOptions {
|
||||
const knownClasses = new Set(CLASSES.map((definition) => definition.id));
|
||||
const classIds = list(argumentValue(arguments_, "--class"));
|
||||
if (classIds?.some((id) => !knownClasses.has(id as ClassId))) {
|
||||
throw new Error(`Unknown class filter: ${classIds.find((id) => !knownClasses.has(id as ClassId))}.`);
|
||||
}
|
||||
const roles = list(argumentValue(arguments_, "--role"));
|
||||
if (roles?.some((role) => !["tank", "healer", "damage"].includes(role))) {
|
||||
throw new Error(`Unknown role filter: ${roles.find((role) => !["tank", "healer", "damage"].includes(role))}.`);
|
||||
}
|
||||
const scenarioIds = list(argumentValue(arguments_, "--scenario"));
|
||||
const knownScenarios = new Set(BENCHMARK_SCENARIOS.map((scenario) => scenario.id));
|
||||
if (scenarioIds?.some((id) => !knownScenarios.has(id))) {
|
||||
throw new Error(`Unknown scenario filter: ${scenarioIds.find((id) => !knownScenarios.has(id))}.`);
|
||||
}
|
||||
const seeds = (list(argumentValue(arguments_, "--seeds")) ?? ["1", "2", "3"])
|
||||
.map((seed) => Number(seed));
|
||||
if (!seeds.length || seeds.some((seed) => !Number.isSafeInteger(seed))) {
|
||||
throw new Error("--seeds must contain comma-separated safe integers.");
|
||||
}
|
||||
const durationScale = finiteNumber(argumentValue(arguments_, "--duration-scale"), 1, "--duration-scale");
|
||||
if (durationScale <= 0) throw new Error("--duration-scale must be greater than zero.");
|
||||
return {
|
||||
level: Math.trunc(finiteNumber(argumentValue(arguments_, "--level"), CANONICAL_BENCHMARK_LEVEL, "--level")),
|
||||
classIds: classIds as readonly ClassId[] | null,
|
||||
roles: roles as readonly PartyRole[] | null,
|
||||
scenarioIds,
|
||||
seeds,
|
||||
durationScale,
|
||||
output: argumentValue(arguments_, "--output"),
|
||||
};
|
||||
}
|
||||
|
||||
function help(): string {
|
||||
return [
|
||||
"Healer Man balance benchmark",
|
||||
"",
|
||||
"Usage: npm run balance:benchmark -- [options]",
|
||||
"",
|
||||
"Options:",
|
||||
" --class mage,templar Limit primary classes.",
|
||||
" --role damage,healer Limit roles.",
|
||||
" --scenario id,id Limit scenario IDs.",
|
||||
" --level 60 Comparison level (default 60).",
|
||||
" --seeds 1,2,3 Deterministic seeds.",
|
||||
" --duration-scale 1 Scale scenario duration (smoke tests only below 1).",
|
||||
" --output path Output directory under the current path or absolute.",
|
||||
" --help Show this help.",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function timestampDirectory(iso: string): string {
|
||||
return iso.replaceAll(":", "-").replace(/\.\d{3}Z$/u, "Z");
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const arguments_ = process.argv.slice(2);
|
||||
if (arguments_.includes("--help")) {
|
||||
process.stdout.write(help());
|
||||
return;
|
||||
}
|
||||
const options = parseOptions(arguments_);
|
||||
const generatedAt = new Date().toISOString();
|
||||
const packageJson = JSON.parse(readFileSync(resolve(projectRoot, "package.json"), "utf8")) as { version: string };
|
||||
const builds = (await loadCanonicalBenchmarkBuilds(options.level, options.classIds ?? undefined))
|
||||
.filter((build) => !options.roles || options.roles.includes(build.role));
|
||||
const scenarios = BENCHMARK_SCENARIOS.filter((scenario) => (
|
||||
!options.scenarioIds || options.scenarioIds.includes(scenario.id)
|
||||
));
|
||||
const pairs = builds.flatMap((build) => scenarios
|
||||
.filter((scenario) => scenario.roles.includes(build.role))
|
||||
.map((scenario) => ({ build, scenario })));
|
||||
if (!pairs.length) throw new Error("The selected filters produced no compatible build/scenario pairs.");
|
||||
|
||||
const totalRuns = pairs.length * options.seeds.length;
|
||||
const runs = [];
|
||||
let completed = 0;
|
||||
for (const pair of pairs) {
|
||||
for (const seed of options.seeds) {
|
||||
completed += 1;
|
||||
process.stdout.write(
|
||||
`[${completed}/${totalRuns}] ${pair.build.classId}:${pair.build.role} / ${pair.scenario.id} / seed ${seed}\n`,
|
||||
);
|
||||
const run = await runCombatBenchmark(pair.build, pair.scenario, {
|
||||
seed,
|
||||
durationScale: options.durationScale,
|
||||
});
|
||||
runs.push(run);
|
||||
if (!run.validation.valid) {
|
||||
process.stderr.write(` invalid: ${run.validation.reasons.join("; ")}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const report: BenchmarkMatrixReport = {
|
||||
schemaVersion: 1,
|
||||
generatedAt,
|
||||
appVersion: packageJson.version,
|
||||
cohort: {
|
||||
level: options.level,
|
||||
gearPresetId: builds[0]?.gearPresetId ?? "coefficient-empty",
|
||||
seeds: options.seeds,
|
||||
durationScale: options.durationScale,
|
||||
policyId: BENCHMARK_POLICY_ID,
|
||||
},
|
||||
scenarios,
|
||||
builds,
|
||||
runs,
|
||||
};
|
||||
const defaultOutput = resolve(projectRoot, "artifacts/combat-benchmarks", timestampDirectory(generatedAt));
|
||||
const output = options.output ? resolve(process.cwd(), options.output) : defaultOutput;
|
||||
mkdirSync(output, { recursive: true });
|
||||
writeFileSync(resolve(output, "matrix.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
writeFileSync(resolve(output, "matrix.csv"), serializeBenchmarkMatrixCsv(report), "utf8");
|
||||
writeFileSync(resolve(output, "summary.md"), renderBenchmarkMatrixMarkdown(report), "utf8");
|
||||
process.stdout.write(`Wrote ${runs.length} run(s) to ${output}\n`);
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -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));
|
||||
Loaded 100 of 359 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user