feat: roll out V2 art across all battle maps
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
/tmp/
|
||||
__pycache__/
|
||||
.dev-server.log
|
||||
.dev-server.err
|
||||
*.local
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Hand-Painted Tactical Map Pipeline
|
||||
|
||||
> Production V2 note (2026-07-15): battle 1 keeps the approved direct repaint, while battles 2-66 use the deterministic atlas compositor documented in `docs/visual-asset-all-battle-maps-v2-report.md`. The older per-map V1 commands and resolution table below remain only as historical reference.
|
||||
|
||||
## Scope
|
||||
|
||||
All sixty-six campaign battles now use the same hand-painted tactical map pipeline. The renderer still reads the original scenario tile coordinates and battle logic; only the map texture asset changes from flat/vector map art to low-contrast raster terrain.
|
||||
All sixty-six campaign battles now use the same hand-painted tactical map art direction. Battle 1 is a direct repaint and battles 2-66 use the V2 atlas compositor. The renderer still reads the original scenario tile coordinates and battle logic; only the map texture asset changes from the earlier flat raster treatment to low-contrast painted terrain.
|
||||
|
||||
## V2 Pilot
|
||||
## V2 Rollout
|
||||
|
||||
The first battle now uses `src/assets/images/battle/first-battle-map-v2.webp` as the approved V2 vertical slice. It is an original 96 px/tile repaint with reduced runtime tint and grid weight. The procedural generator still targets the retired `first-battle-map.webp` filename for controlled V1 comparisons; do not promote that output over the V2 asset without a new runtime visual review.
|
||||
The first battle uses `src/assets/images/battle/first-battle-map-v2.webp` as the approved direct-repaint vertical slice. Battles 2-66 are generated from nine original V2 material sources with runtime-corrected scenario terrain. Run `pnpm run generate:battle-maps-v2`; the compositor preserves production filenames, emits a manifest and contact sheet, and caps every raster edge at 8,192 px for desktop WebGL reliability.
|
||||
|
||||
## Art Direction
|
||||
|
||||
|
||||
BIN
docs/battle-map-v2-contact-sheet.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
1127
docs/battle-map-v2-manifest.json
Normal file
75
docs/visual-asset-all-battle-maps-v2-report.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# All Battle Maps V2 Rollout
|
||||
|
||||
Date: 2026-07-15
|
||||
|
||||
## Scope
|
||||
|
||||
- Battle 1 retains the approved direct repaint at `src/assets/images/battle/first-battle-map-v2.webp`.
|
||||
- Battles 2-66 use the same original V2 terrain language through a deterministic atlas compositor.
|
||||
- Scenario coordinates, terrain arrays, units, goals, movement rules, and map keys are unchanged.
|
||||
- Runtime-corrected terrain is exported from the loaded `battleScenarios` data, so secure terrain-center overrides are included before rendering.
|
||||
|
||||
## Original V2 Sources
|
||||
|
||||
The reusable source set lives in `src/assets/images/battle/terrain-v2/`:
|
||||
|
||||
- `plain-v2-source.png`
|
||||
- `road-v2-source.png`
|
||||
- `hill-v2-source.png`
|
||||
- `forest-v2-source.png`
|
||||
- `river-v2-source.png`
|
||||
- `cliff-v2-source.png`
|
||||
- `village-v2-source.png`
|
||||
- `fort-v2-source.png`
|
||||
- `camp-v2-source.png`
|
||||
|
||||
The sources were created with the built-in image generation workflow. The prompt set requested original orthographic late-Han battlefield materials with muted olive, ochre, stone, timber, and blue-green water; hand-painted tactical readability; restrained lighting; and no grid, UI, units, text, logos, watermarks, modern objects, or reproduction of copyrighted game assets.
|
||||
|
||||
## Production Pipeline
|
||||
|
||||
1. `scripts/export-battle-map-v2-data.mjs` loads the production scenario and map-asset modules through Vite SSR.
|
||||
2. It exports the 66 runtime map grids, asset paths, dimensions, map keys, and unit positions to `tmp/battle-map-v2-runtime-data.json`.
|
||||
3. `scripts/generate-battle-maps-v2.py` composites battles 2-66 with seeded texture quilting, organic visual masks, sparse terrain features, meandering wide-map rivers, non-grid road paths, component-spaced structures, scenario-era grading, and event atmosphere.
|
||||
4. The generator writes `docs/battle-map-v2-manifest.json` and `docs/battle-map-v2-contact-sheet.jpg` after a full run.
|
||||
|
||||
The output is deterministic for a fixed source set and seed. The adaptive tile policy uses 96, 88, 80, 72, 64, or 60 source pixels per logical tile and guarantees that neither image edge exceeds 8,192 px.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```powershell
|
||||
pnpm run generate:battle-maps-v2
|
||||
pnpm run verify:battle-map-assets
|
||||
pnpm run audit:visual-assets
|
||||
pnpm run qa:battle-maps:webgl
|
||||
```
|
||||
|
||||
For a targeted preview:
|
||||
|
||||
```powershell
|
||||
node scripts/export-battle-map-v2-data.mjs
|
||||
python scripts/generate-battle-maps-v2.py --maps 2,66 --quality 84 --skip-artifacts
|
||||
```
|
||||
|
||||
## Visual Quality Controls
|
||||
|
||||
- Overlapping feathered source crops replace hard rectangular tiling.
|
||||
- Forest and structure feature masks remove visible source-sheet boxes.
|
||||
- Large road components become a small number of natural routes instead of a tactical-looking lattice.
|
||||
- Thick late-game river blocks become meandering water channels inside a subtly graded logical floodplain.
|
||||
- Structure placement is capped and spatially separated by connected component.
|
||||
- Noise, feature selection, theme grading, and placements use stable seeded hashes.
|
||||
- The first battle remains untouched, providing the approved quality and palette reference for the rollout.
|
||||
|
||||
## Verification
|
||||
|
||||
Local release-candidate verification completed on 2026-07-15:
|
||||
|
||||
- `verify:battle-map-assets`: 66 scenario map keys and 66 battle-map assets matched, including exact image axes and the 8,192 px edge ceiling.
|
||||
- `audit:visual-assets`: all 66 maps passed the V2 density and maximum-edge audit.
|
||||
- `verify:static-data`: all 20 static-data gates passed for the 66-battle campaign.
|
||||
- `build`: TypeScript and the production Vite build passed (546 modules transformed).
|
||||
- `qa:battle-maps:webgl`: 12 representative battles passed in the real Phaser WebGL renderer, with 12 victories, zero failures, and 71/71 objectives achieved. The set includes battles 40, 54, 59, and 61 for flood, river, and late-game large-map coverage.
|
||||
- `verify:performance`: all 8 budgets passed; first battle became playable 5,717 ms after the story transition in the measured local run.
|
||||
- `verify:release`: the release-candidate browser flow passed, including new-game auto-save, reload/continue, defeat retry, victory-to-camp return, FHD input mapping, and WebGL battle smoke coverage.
|
||||
|
||||
The tracked release pipeline re-runs the full local verification and all-battle QA before NAS publication, then checks the public release manifest.
|
||||
@@ -6,9 +6,9 @@ Generated by `scripts/audit-visual-asset-quality.mjs`.
|
||||
|
||||
- Target desktop viewport: 1920x1080
|
||||
- Battle map assets: 66
|
||||
- Flagged battle map review rows: 43
|
||||
- Flagged battle map rows with repaint reports: 4
|
||||
- Battle map source pixel density range: 64.0-224.0 px/tile
|
||||
- Flagged battle map review rows: 10
|
||||
- Flagged battle map rows with repaint reports: 3
|
||||
- Battle map source pixel density range: 60.0-96.0 px/tile
|
||||
- Story source backgrounds: 136
|
||||
- Story page background references: 466
|
||||
- Flagged story background review rows: 120
|
||||
@@ -16,15 +16,15 @@ Generated by `scripts/audit-visual-asset-quality.mjs`.
|
||||
## Main Findings
|
||||
|
||||
- Existing verification already gates missing files and minimum dimensions; this audit ranks visual-review risk instead of failing the build.
|
||||
- Battle maps are scored by source pixels per scenario tile, source/image aspect match, and WebP byte density.
|
||||
- Battle maps are scored by the V2 adaptive 60 px/tile floor, the 8,192 px WebGL edge budget, source/image aspect match, and WebP byte density.
|
||||
- Battle maps with repaint reports stay in the inventory, but are skipped for the next-batch recommendation.
|
||||
- Story backgrounds are scored by standard 1672x941 dimensions, repeat usage, and PNG byte density.
|
||||
- The audit cannot judge composition or painterly quality by itself; top rows are candidates for desktop browser screenshot review.
|
||||
|
||||
## Recommended Next Batch
|
||||
|
||||
- Battle map candidate: `sixty-third-battle-lucheng-pursuit` / `battle-map-sixty-third`
|
||||
- Battle reason: low tile pixel density
|
||||
- Battle map candidate: `sixty-first-battle-hanzhong-rain-defense` / `battle-map-sixty-first`
|
||||
- Battle reason: watch tile pixel density
|
||||
- Story background candidate: `src/assets/images/story/25-yizhou-luo-road.png`
|
||||
- Story reason: high-repeat story background
|
||||
- Suggested workflow: capture the top battle candidate in a 1920x1080 battle scene, inspect terrain readability under units/grid, then regenerate or repaint only that asset if the screenshot confirms the issue.
|
||||
@@ -33,18 +33,13 @@ Generated by `scripts/audit-visual-asset-quality.mjs`.
|
||||
|
||||
| score | battle id | map key | grid | image | px/tile | bytes/px | concerns |
|
||||
| ---: | --- | --- | ---: | ---: | ---: | ---: | --- |
|
||||
| 6 | `sixty-third-battle-lucheng-pursuit` | `battle-map-sixty-third` | 126x108 | 14112x12096 | 112.0 | 0.1200 | low tile pixel density |
|
||||
| 6 | `sixtieth-battle-wudu-yinping` | `battle-map-sixtieth` | 120x102 | 14400x12240 | 120.0 | 0.1116 | low tile pixel density |
|
||||
| 6 | `sixty-first-battle-hanzhong-rain-defense` | `battle-map-sixty-first` | 122x104 | 14640x12480 | 120.0 | 0.1162 | low tile pixel density |
|
||||
| 6 | `sixty-second-battle-qishan-renewed-offensive` | `battle-map-sixty-second` | 124x106 | 14880x12720 | 120.0 | 0.1222 | low tile pixel density |
|
||||
| 4 | `fifty-sixth-battle-tianshui-advance` | `battle-map-fifty-sixth` | 112x94 | 14336x12032 | 128.0 | 0.0939 | reduced tile pixel density |
|
||||
| 4 | `fifty-seventh-battle-jieting-crisis` | `battle-map-fifty-seventh` | 114x96 | 14592x12288 | 128.0 | 0.0987 | reduced tile pixel density |
|
||||
| 4 | `fifty-eighth-battle-qishan-retreat` | `battle-map-fifty-eighth` | 116x98 | 14848x12544 | 128.0 | 0.1003 | reduced tile pixel density |
|
||||
| 4 | `fifty-ninth-battle-chencang-siege` | `battle-map-fifty-ninth` | 118x100 | 15104x12800 | 128.0 | 0.1049 | reduced tile pixel density |
|
||||
| 4 | `fifty-fifth-battle-northern-qishan-road` | `battle-map-fifty-fifth` | 110x92 | 14080x11776 | 128.0 | 0.1146 | reduced tile pixel density |
|
||||
| 4 | `fifty-third-battle-meng-huo-sixth-capture` | `battle-map-fifty-third` | 106x88 | 14416x11968 | 136.0 | 0.1155 | reduced tile pixel density |
|
||||
| 4 | `fifty-fourth-battle-meng-huo-final-capture` | `battle-map-fifty-fourth` | 108x90 | 14688x12240 | 136.0 | 0.1168 | reduced tile pixel density |
|
||||
| 4 | `fifth-battle-sishui-vanguard` | `battle-map-fifth` | 24x20 | 5376x4480 | 224.0 | 0.0270 | very low WebP byte density |
|
||||
| 2 | `sixty-first-battle-hanzhong-rain-defense` | `battle-map-sixty-first` | 122x104 | 7808x6656 | 64.0 | 0.1027 | watch tile pixel density |
|
||||
| 2 | `fifty-eighth-battle-qishan-retreat` | `battle-map-fifty-eighth` | 116x98 | 7424x6272 | 64.0 | 0.1342 | watch tile pixel density |
|
||||
| 2 | `fifty-seventh-battle-jieting-crisis` | `battle-map-fifty-seventh` | 114x96 | 7296x6144 | 64.0 | 0.1360 | watch tile pixel density |
|
||||
| 2 | `sixty-second-battle-qishan-renewed-offensive` | `battle-map-sixty-second` | 124x106 | 7936x6784 | 64.0 | 0.1381 | watch tile pixel density |
|
||||
| 2 | `sixtieth-battle-wudu-yinping` | `battle-map-sixtieth` | 120x102 | 7680x6528 | 64.0 | 0.1407 | watch tile pixel density |
|
||||
| 2 | `fifty-ninth-battle-chencang-siege` | `battle-map-fifty-ninth` | 118x100 | 7552x6400 | 64.0 | 0.1414 | watch tile pixel density |
|
||||
| 2 | `sixty-third-battle-lucheng-pursuit` | `battle-map-sixty-third` | 126x108 | 8064x6912 | 64.0 | 0.1448 | watch tile pixel density |
|
||||
|
||||
## Top Story Background Review Queue
|
||||
|
||||
@@ -67,72 +62,72 @@ Generated by `scripts/audit-visual-asset-quality.mjs`.
|
||||
|
||||
| battle id | map key | grid | image | px/tile | MiB | concerns |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | --- |
|
||||
| `eighteenth-battle-bowang-ambush` | `battle-map-eighteenth` | 36x26 | 8064x5824 | 224.0 | 1.99 | low WebP byte density |
|
||||
| `eighth-battle-xiaopei-supply-road` | `battle-map-eighth` | 26x22 | 5824x4928 | 224.0 | 0.99 | low WebP byte density |
|
||||
| `eleventh-battle-xudu-refuge-road` | `battle-map-eleventh` | 28x22 | 6272x4928 | 224.0 | 1.11 | low WebP byte density |
|
||||
| `fifteenth-battle-yuan-refuge-road` | `battle-map-fifteenth` | 34x24 | 7616x5376 | 224.0 | 1.33 | very low WebP byte density |
|
||||
| `fifth-battle-sishui-vanguard` | `battle-map-fifth` | 24x20 | 5376x4480 | 224.0 | 0.62 | very low WebP byte density |
|
||||
| `fiftieth-battle-meng-huo-third-capture` | `battle-map-fiftieth` | 100x82 | 15200x12464 | 152.0 | 19.13 | watch tile pixel density |
|
||||
| `fifty-eighth-battle-qishan-retreat` | `battle-map-fifty-eighth` | 116x98 | 14848x12544 | 128.0 | 17.82 | reduced tile pixel density |
|
||||
| `fifty-fifth-battle-northern-qishan-road` | `battle-map-fifty-fifth` | 110x92 | 14080x11776 | 128.0 | 18.11 | reduced tile pixel density |
|
||||
| `fifty-first-battle-meng-huo-fourth-capture` | `battle-map-fifty-first` | 102x84 | 14688x12096 | 144.0 | 18.73 | watch tile pixel density |
|
||||
| `fifty-fourth-battle-meng-huo-final-capture` | `battle-map-fifty-fourth` | 108x90 | 14688x12240 | 136.0 | 20.03 | reduced tile pixel density |
|
||||
| `fifty-ninth-battle-chencang-siege` | `battle-map-fifty-ninth` | 118x100 | 15104x12800 | 128.0 | 19.33 | reduced tile pixel density |
|
||||
| `fifty-second-battle-meng-huo-fifth-capture` | `battle-map-fifty-second` | 104x86 | 14976x12384 | 144.0 | 18.84 | watch tile pixel density |
|
||||
| `fifty-seventh-battle-jieting-crisis` | `battle-map-fifty-seventh` | 114x96 | 14592x12288 | 128.0 | 16.89 | reduced tile pixel density |
|
||||
| `fifty-sixth-battle-tianshui-advance` | `battle-map-fifty-sixth` | 112x94 | 14336x12032 | 128.0 | 15.45 | reduced tile pixel density |
|
||||
| `fifty-third-battle-meng-huo-sixth-capture` | `battle-map-fifty-third` | 106x88 | 14416x11968 | 136.0 | 19.00 | reduced tile pixel density |
|
||||
| `first-battle-zhuo-commandery` | `battle-map-first` | 20x18 | 1920x1728 | 96.0 | 1.02 | low tile pixel density<br>repaint report exists |
|
||||
| `fortieth-battle-han-river-flood` | `battle-map-fortieth` | 80x62 | 15360x11904 | 192.0 | 11.76 | none |
|
||||
| `forty-eighth-battle-meng-huo-main-force` | `battle-map-forty-eighth` | 96x78 | 15360x12480 | 160.0 | 16.44 | none |
|
||||
| `forty-fifth-battle-yiling-vanguard` | `battle-map-forty-fifth` | 90x72 | 14400x11520 | 160.0 | 13.98 | none |
|
||||
| `forty-first-battle-fan-castle-siege` | `battle-map-forty-first` | 82x64 | 15744x12288 | 192.0 | 12.58 | none |
|
||||
| `forty-fourth-battle-maicheng-isolation` | `battle-map-forty-fourth` | 88x70 | 15488x12320 | 176.0 | 15.70 | none |
|
||||
| `forty-ninth-battle-meng-huo-second-capture` | `battle-map-forty-ninth` | 98x80 | 14896x12160 | 152.0 | 17.61 | watch tile pixel density |
|
||||
| `forty-second-battle-jing-rear-crisis` | `battle-map-forty-second` | 84x66 | 14784x11616 | 176.0 | 12.54 | none |
|
||||
| `forty-seventh-battle-nanzhong-stabilization` | `battle-map-forty-seventh` | 94x76 | 15040x12160 | 160.0 | 16.22 | none |
|
||||
| `forty-sixth-battle-yiling-fire` | `battle-map-forty-sixth` | 92x74 | 14720x11840 | 160.0 | 14.87 | none |
|
||||
| `forty-third-battle-gongan-collapse` | `battle-map-forty-third` | 86x68 | 15136x11968 | 176.0 | 13.34 | none |
|
||||
| `fourteenth-battle-cao-break` | `battle-map-fourteenth` | 32x24 | 7168x5376 | 224.0 | 1.31 | low WebP byte density |
|
||||
| `fourth-battle-guangzong-camp` | `battle-map-fourth` | 24x20 | 5376x4480 | 224.0 | 0.63 | very low WebP byte density |
|
||||
| `nineteenth-battle-changban-refuge` | `battle-map-nineteenth` | 38x28 | 8512x6272 | 224.0 | 2.08 | low WebP byte density |
|
||||
| `ninth-battle-xuzhou-gate-night-raid` | `battle-map-ninth` | 26x22 | 5824x4928 | 224.0 | 1.00 | low WebP byte density |
|
||||
| `second-battle-yellow-turban-pursuit` | `battle-map-second` | 24x20 | 5376x4480 | 224.0 | 0.65 | very low WebP byte density |
|
||||
| `seventeenth-battle-wolong-visit-road` | `battle-map-seventeenth` | 34x26 | 7616x5824 | 224.0 | 1.81 | low WebP byte density |
|
||||
| `seventh-battle-xuzhou-rescue` | `battle-map-seventh` | 24x20 | 5376x4480 | 224.0 | 0.65 | very low WebP byte density |
|
||||
| `sixteenth-battle-liu-biao-refuge` | `battle-map-sixteenth` | 34x26 | 7616x5824 | 224.0 | 1.49 | low WebP byte density |
|
||||
| `sixth-battle-jieqiao-relief` | `battle-map-sixth` | 24x20 | 5376x4480 | 224.0 | 0.64 | very low WebP byte density |
|
||||
| `sixtieth-battle-wudu-yinping` | `battle-map-sixtieth` | 120x102 | 14400x12240 | 120.0 | 18.76 | low tile pixel density |
|
||||
| `sixty-fifth-battle-weishui-northbank` | `battle-map-sixty-fifth` | 130x112 | 8320x7168 | 64.0 | 8.00 | low tile pixel density<br>repaint report exists |
|
||||
| `sixty-first-battle-hanzhong-rain-defense` | `battle-map-sixty-first` | 122x104 | 14640x12480 | 120.0 | 20.24 | low tile pixel density |
|
||||
| `sixty-fourth-battle-weishui-camps` | `battle-map-sixty-fourth` | 128x110 | 8192x7040 | 64.0 | 7.51 | low tile pixel density<br>repaint report exists |
|
||||
| `sixty-second-battle-qishan-renewed-offensive` | `battle-map-sixty-second` | 124x106 | 14880x12720 | 120.0 | 22.05 | low tile pixel density |
|
||||
| `sixty-sixth-battle-wuzhang-final` | `battle-map-sixty-sixth` | 132x114 | 8448x7296 | 64.0 | 7.91 | low tile pixel density<br>repaint report exists |
|
||||
| `sixty-third-battle-lucheng-pursuit` | `battle-map-sixty-third` | 126x108 | 14112x12096 | 112.0 | 19.53 | low tile pixel density |
|
||||
| `tenth-battle-xuzhou-breakout` | `battle-map-tenth` | 28x22 | 6272x4928 | 224.0 | 1.16 | low WebP byte density |
|
||||
| `third-battle-guangzong-road` | `battle-map-third` | 24x20 | 5376x4480 | 224.0 | 0.65 | very low WebP byte density |
|
||||
| `thirteenth-battle-xiapi-final` | `battle-map-thirteenth` | 32x24 | 7168x5376 | 224.0 | 1.32 | low WebP byte density |
|
||||
| `thirtieth-battle-luofeng-ambush` | `battle-map-thirtieth` | 60x42 | 11520x8064 | 192.0 | 5.18 | none |
|
||||
| `thirty-eighth-battle-jing-defense` | `battle-map-thirty-eighth` | 76x58 | 14592x11136 | 192.0 | 10.88 | none |
|
||||
| `thirty-fifth-battle-yangping-scout` | `battle-map-thirty-fifth` | 70x52 | 13440x9984 | 192.0 | 8.67 | none |
|
||||
| `thirty-first-battle-luo-main-gate` | `battle-map-thirty-first` | 62x44 | 11904x8448 | 192.0 | 6.17 | none |
|
||||
| `thirty-fourth-battle-jiameng-pass` | `battle-map-thirty-fourth` | 68x50 | 13056x9600 | 192.0 | 7.99 | none |
|
||||
| `thirty-ninth-battle-fan-castle-vanguard` | `battle-map-thirty-ninth` | 78x60 | 14976x11520 | 192.0 | 11.19 | none |
|
||||
| `thirty-second-battle-mianzhu-gate` | `battle-map-thirty-second` | 64x46 | 12288x8832 | 192.0 | 6.75 | none |
|
||||
| `thirty-seventh-battle-hanzhong-decisive` | `battle-map-thirty-seventh` | 74x56 | 14208x10752 | 192.0 | 9.92 | none |
|
||||
| `thirty-sixth-battle-dingjun-vanguard` | `battle-map-thirty-sixth` | 72x54 | 13824x10368 | 192.0 | 9.26 | none |
|
||||
| `thirty-third-battle-chengdu-surrender` | `battle-map-thirty-third` | 66x48 | 12672x9216 | 192.0 | 7.44 | none |
|
||||
| `twelfth-battle-xiapi-outer-siege` | `battle-map-twelfth` | 30x24 | 6720x5376 | 224.0 | 1.25 | low WebP byte density |
|
||||
| `twentieth-battle-jiangdong-envoy` | `battle-map-twentieth` | 40x28 | 8960x6272 | 224.0 | 2.05 | low WebP byte density |
|
||||
| `twenty-eighth-battle-fu-pass-entry` | `battle-map-twenty-eighth` | 56x38 | 10752x7296 | 192.0 | 4.51 | none |
|
||||
| `twenty-fifth-battle-wuling-mountain-road` | `battle-map-twenty-fifth` | 50x34 | 11200x7616 | 224.0 | 3.69 | none |
|
||||
| `twenty-first-battle-red-cliffs-vanguard` | `battle-map-twenty-first` | 42x30 | 9408x6720 | 224.0 | 2.19 | low WebP byte density |
|
||||
| `twenty-fourth-battle-guiyang-persuasion` | `battle-map-twenty-fourth` | 48x32 | 10752x7168 | 224.0 | 2.81 | low WebP byte density |
|
||||
| `twenty-ninth-battle-luo-outer-wall` | `battle-map-twenty-ninth` | 58x40 | 11136x7680 | 192.0 | 4.74 | none |
|
||||
| `twenty-second-battle-red-cliffs-fire` | `battle-map-twenty-second` | 44x30 | 9856x6720 | 224.0 | 2.47 | low WebP byte density |
|
||||
| `twenty-seventh-battle-yizhou-relief-road` | `battle-map-twenty-seventh` | 54x36 | 10368x6912 | 192.0 | 3.89 | none |
|
||||
| `twenty-sixth-battle-changsha-veteran` | `battle-map-twenty-sixth` | 52x34 | 9984x6528 | 192.0 | 2.73 | low WebP byte density |
|
||||
| `twenty-third-battle-jingzhou-south-entry` | `battle-map-twenty-third` | 46x32 | 10304x7168 | 224.0 | 2.66 | low WebP byte density |
|
||||
| `eighteenth-battle-bowang-ambush` | `battle-map-eighteenth` | 36x26 | 3456x2496 | 96.0 | 0.98 | none |
|
||||
| `eighth-battle-xiaopei-supply-road` | `battle-map-eighth` | 26x22 | 2496x2112 | 96.0 | 0.59 | none |
|
||||
| `eleventh-battle-xudu-refuge-road` | `battle-map-eleventh` | 28x22 | 2688x2112 | 96.0 | 0.70 | none |
|
||||
| `fifteenth-battle-yuan-refuge-road` | `battle-map-fifteenth` | 34x24 | 3264x2304 | 96.0 | 0.81 | none |
|
||||
| `fifth-battle-sishui-vanguard` | `battle-map-fifth` | 24x20 | 2304x1920 | 96.0 | 0.50 | none |
|
||||
| `fiftieth-battle-meng-huo-third-capture` | `battle-map-fiftieth` | 100x82 | 8000x6560 | 80.0 | 6.51 | none |
|
||||
| `fifty-eighth-battle-qishan-retreat` | `battle-map-fifty-eighth` | 116x98 | 7424x6272 | 64.0 | 5.96 | watch tile pixel density |
|
||||
| `fifty-fifth-battle-northern-qishan-road` | `battle-map-fifty-fifth` | 110x92 | 7920x6624 | 72.0 | 7.08 | none |
|
||||
| `fifty-first-battle-meng-huo-fourth-capture` | `battle-map-fifty-first` | 102x84 | 7344x6048 | 72.0 | 5.40 | none |
|
||||
| `fifty-fourth-battle-meng-huo-final-capture` | `battle-map-fifty-fourth` | 108x90 | 7776x6480 | 72.0 | 6.03 | none |
|
||||
| `fifty-ninth-battle-chencang-siege` | `battle-map-fifty-ninth` | 118x100 | 7552x6400 | 64.0 | 6.52 | watch tile pixel density |
|
||||
| `fifty-second-battle-meng-huo-fifth-capture` | `battle-map-fifty-second` | 104x86 | 7488x6192 | 72.0 | 5.68 | none |
|
||||
| `fifty-seventh-battle-jieting-crisis` | `battle-map-fifty-seventh` | 114x96 | 7296x6144 | 64.0 | 5.81 | watch tile pixel density |
|
||||
| `fifty-sixth-battle-tianshui-advance` | `battle-map-fifty-sixth` | 112x94 | 8064x6768 | 72.0 | 7.59 | none |
|
||||
| `fifty-third-battle-meng-huo-sixth-capture` | `battle-map-fifty-third` | 106x88 | 7632x6336 | 72.0 | 6.03 | none |
|
||||
| `first-battle-zhuo-commandery` | `battle-map-first` | 20x18 | 1920x1728 | 96.0 | 1.02 | repaint report exists |
|
||||
| `fortieth-battle-han-river-flood` | `battle-map-fortieth` | 80x62 | 7680x5952 | 96.0 | 4.41 | none |
|
||||
| `forty-eighth-battle-meng-huo-main-force` | `battle-map-forty-eighth` | 96x78 | 7680x6240 | 80.0 | 6.47 | none |
|
||||
| `forty-fifth-battle-yiling-vanguard` | `battle-map-forty-fifth` | 90x72 | 7920x6336 | 88.0 | 6.12 | none |
|
||||
| `forty-first-battle-fan-castle-siege` | `battle-map-forty-first` | 82x64 | 7216x5632 | 88.0 | 4.62 | none |
|
||||
| `forty-fourth-battle-maicheng-isolation` | `battle-map-forty-fourth` | 88x70 | 7744x6160 | 88.0 | 5.50 | none |
|
||||
| `forty-ninth-battle-meng-huo-second-capture` | `battle-map-forty-ninth` | 98x80 | 7840x6400 | 80.0 | 6.23 | none |
|
||||
| `forty-second-battle-jing-rear-crisis` | `battle-map-forty-second` | 84x66 | 7392x5808 | 88.0 | 4.82 | none |
|
||||
| `forty-seventh-battle-nanzhong-stabilization` | `battle-map-forty-seventh` | 94x76 | 7520x6080 | 80.0 | 6.08 | none |
|
||||
| `forty-sixth-battle-yiling-fire` | `battle-map-forty-sixth` | 92x74 | 7360x5920 | 80.0 | 5.32 | none |
|
||||
| `forty-third-battle-gongan-collapse` | `battle-map-forty-third` | 86x68 | 7568x5984 | 88.0 | 5.07 | none |
|
||||
| `fourteenth-battle-cao-break` | `battle-map-fourteenth` | 32x24 | 3072x2304 | 96.0 | 0.80 | none |
|
||||
| `fourth-battle-guangzong-camp` | `battle-map-fourth` | 24x20 | 2304x1920 | 96.0 | 0.52 | none |
|
||||
| `nineteenth-battle-changban-refuge` | `battle-map-nineteenth` | 38x28 | 3648x2688 | 96.0 | 1.16 | none |
|
||||
| `ninth-battle-xuzhou-gate-night-raid` | `battle-map-ninth` | 26x22 | 2496x2112 | 96.0 | 0.32 | none |
|
||||
| `second-battle-yellow-turban-pursuit` | `battle-map-second` | 24x20 | 2304x1920 | 96.0 | 0.49 | none |
|
||||
| `seventeenth-battle-wolong-visit-road` | `battle-map-seventeenth` | 34x26 | 3264x2496 | 96.0 | 0.98 | none |
|
||||
| `seventh-battle-xuzhou-rescue` | `battle-map-seventh` | 24x20 | 2304x1920 | 96.0 | 0.47 | none |
|
||||
| `sixteenth-battle-liu-biao-refuge` | `battle-map-sixteenth` | 34x26 | 3264x2496 | 96.0 | 0.97 | none |
|
||||
| `sixth-battle-jieqiao-relief` | `battle-map-sixth` | 24x20 | 2304x1920 | 96.0 | 0.48 | none |
|
||||
| `sixtieth-battle-wudu-yinping` | `battle-map-sixtieth` | 120x102 | 7680x6528 | 64.0 | 6.73 | watch tile pixel density |
|
||||
| `sixty-fifth-battle-weishui-northbank` | `battle-map-sixty-fifth` | 130x112 | 7800x6720 | 60.0 | 6.81 | reduced tile pixel density<br>repaint report exists |
|
||||
| `sixty-first-battle-hanzhong-rain-defense` | `battle-map-sixty-first` | 122x104 | 7808x6656 | 64.0 | 5.09 | watch tile pixel density |
|
||||
| `sixty-fourth-battle-weishui-camps` | `battle-map-sixty-fourth` | 128x110 | 8192x7040 | 64.0 | 6.96 | watch tile pixel density<br>repaint report exists |
|
||||
| `sixty-second-battle-qishan-renewed-offensive` | `battle-map-sixty-second` | 124x106 | 7936x6784 | 64.0 | 7.09 | watch tile pixel density |
|
||||
| `sixty-sixth-battle-wuzhang-final` | `battle-map-sixty-sixth` | 132x114 | 7920x6840 | 60.0 | 6.85 | reduced tile pixel density<br>repaint report exists |
|
||||
| `sixty-third-battle-lucheng-pursuit` | `battle-map-sixty-third` | 126x108 | 8064x6912 | 64.0 | 7.70 | watch tile pixel density |
|
||||
| `tenth-battle-xuzhou-breakout` | `battle-map-tenth` | 28x22 | 2688x2112 | 96.0 | 0.64 | none |
|
||||
| `third-battle-guangzong-road` | `battle-map-third` | 24x20 | 2304x1920 | 96.0 | 0.49 | none |
|
||||
| `thirteenth-battle-xiapi-final` | `battle-map-thirteenth` | 32x24 | 3072x2304 | 96.0 | 0.80 | none |
|
||||
| `thirtieth-battle-luofeng-ambush` | `battle-map-thirtieth` | 60x42 | 5760x4032 | 96.0 | 3.09 | none |
|
||||
| `thirty-eighth-battle-jing-defense` | `battle-map-thirty-eighth` | 76x58 | 7296x5568 | 96.0 | 4.75 | none |
|
||||
| `thirty-fifth-battle-yangping-scout` | `battle-map-thirty-fifth` | 70x52 | 6720x4992 | 96.0 | 4.22 | none |
|
||||
| `thirty-first-battle-luo-main-gate` | `battle-map-thirty-first` | 62x44 | 5952x4224 | 96.0 | 3.38 | none |
|
||||
| `thirty-fourth-battle-jiameng-pass` | `battle-map-thirty-fourth` | 68x50 | 6528x4800 | 96.0 | 3.90 | none |
|
||||
| `thirty-ninth-battle-fan-castle-vanguard` | `battle-map-thirty-ninth` | 78x60 | 7488x5760 | 96.0 | 5.02 | none |
|
||||
| `thirty-second-battle-mianzhu-gate` | `battle-map-thirty-second` | 64x46 | 6144x4416 | 96.0 | 3.28 | none |
|
||||
| `thirty-seventh-battle-hanzhong-decisive` | `battle-map-thirty-seventh` | 74x56 | 7104x5376 | 96.0 | 4.78 | none |
|
||||
| `thirty-sixth-battle-dingjun-vanguard` | `battle-map-thirty-sixth` | 72x54 | 6912x5184 | 96.0 | 4.82 | none |
|
||||
| `thirty-third-battle-chengdu-surrender` | `battle-map-thirty-third` | 66x48 | 6336x4608 | 96.0 | 3.65 | none |
|
||||
| `twelfth-battle-xiapi-outer-siege` | `battle-map-twelfth` | 30x24 | 2880x2304 | 96.0 | 0.73 | none |
|
||||
| `twentieth-battle-jiangdong-envoy` | `battle-map-twentieth` | 40x28 | 3840x2688 | 96.0 | 1.12 | none |
|
||||
| `twenty-eighth-battle-fu-pass-entry` | `battle-map-twenty-eighth` | 56x38 | 5376x3648 | 96.0 | 2.66 | none |
|
||||
| `twenty-fifth-battle-wuling-mountain-road` | `battle-map-twenty-fifth` | 50x34 | 4800x3264 | 96.0 | 1.90 | none |
|
||||
| `twenty-first-battle-red-cliffs-vanguard` | `battle-map-twenty-first` | 42x30 | 4032x2880 | 96.0 | 1.27 | none |
|
||||
| `twenty-fourth-battle-guiyang-persuasion` | `battle-map-twenty-fourth` | 48x32 | 4608x3072 | 96.0 | 1.61 | none |
|
||||
| `twenty-ninth-battle-luo-outer-wall` | `battle-map-twenty-ninth` | 58x40 | 5568x3840 | 96.0 | 2.98 | none |
|
||||
| `twenty-second-battle-red-cliffs-fire` | `battle-map-twenty-second` | 44x30 | 4224x2880 | 96.0 | 1.30 | none |
|
||||
| `twenty-seventh-battle-yizhou-relief-road` | `battle-map-twenty-seventh` | 54x36 | 5184x3456 | 96.0 | 2.41 | none |
|
||||
| `twenty-sixth-battle-changsha-veteran` | `battle-map-twenty-sixth` | 52x34 | 4992x3264 | 96.0 | 2.14 | none |
|
||||
| `twenty-third-battle-jingzhou-south-entry` | `battle-map-twenty-third` | 46x32 | 4416x3072 | 96.0 | 1.53 | none |
|
||||
|
||||
## Story Background Inventory
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"generate:audio": "node scripts/generate-bgm.mjs",
|
||||
"generate:camp-audio": "node scripts/generate-camp-audio.mjs",
|
||||
"generate:sfx": "node scripts/generate-sfx.mjs",
|
||||
"export:battle-map-v2-data": "node scripts/export-battle-map-v2-data.mjs",
|
||||
"generate:battle-maps-v2": "node scripts/export-battle-map-v2-data.mjs && python scripts/generate-battle-maps-v2.py",
|
||||
"audit:equipment-icons": "node scripts/audit-equipment-icon-quality.mjs",
|
||||
"audit:visual-assets": "node scripts/audit-visual-asset-quality.mjs",
|
||||
"preview": "vite preview --host 0.0.0.0",
|
||||
@@ -42,6 +44,7 @@
|
||||
"verify:local-release": "pnpm run verify:static-data && tsc --noEmit && pnpm run build && pnpm run verify:release && pnpm run verify:performance",
|
||||
"verify:public-deploy": "node scripts/verify-public-deploy.mjs",
|
||||
"qa:representative": "node scripts/qa-representative-battles.mjs",
|
||||
"qa:battle-maps:webgl": "node scripts/qa-representative-battles.mjs --set=campaign --battles=1,9,18,22,33,40,46,54,59,61,64,66 --renderer=webgl --report=dist/qa-battle-maps-webgl.json",
|
||||
"qa:smoke": "node scripts/qa-representative-battles.mjs --set=smoke",
|
||||
"qa:early": "node scripts/qa-representative-battles.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66",
|
||||
"qa:early:repeat": "node scripts/qa-repeat.mjs --set=campaign --battles=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66",
|
||||
|
||||
@@ -75,13 +75,16 @@ function battleConcerns({ image, pxPerTile, aspectDelta }) {
|
||||
concerns.push({ label: 'missing readable image metadata', weight: 8 });
|
||||
return concerns;
|
||||
}
|
||||
if (pxPerTile < 128) {
|
||||
if (pxPerTile < 60) {
|
||||
concerns.push({ label: 'low tile pixel density', weight: 6 });
|
||||
} else if (pxPerTile < 144) {
|
||||
} else if (pxPerTile < 64) {
|
||||
concerns.push({ label: 'reduced tile pixel density', weight: 4 });
|
||||
} else if (pxPerTile < 160) {
|
||||
} else if (pxPerTile < 72) {
|
||||
concerns.push({ label: 'watch tile pixel density', weight: 2 });
|
||||
}
|
||||
if (Math.max(image.width, image.height) > 8192) {
|
||||
concerns.push({ label: 'exceeds WebGL texture edge budget', weight: 8 });
|
||||
}
|
||||
if (image.bytes / (image.width * image.height) < 0.035) {
|
||||
concerns.push({ label: 'very low WebP byte density', weight: 4 });
|
||||
} else if (image.bytes / (image.width * image.height) < 0.045) {
|
||||
@@ -259,7 +262,7 @@ function renderReport({ battleRows, storyRows, storyUsage }) {
|
||||
'## Main Findings',
|
||||
'',
|
||||
'- Existing verification already gates missing files and minimum dimensions; this audit ranks visual-review risk instead of failing the build.',
|
||||
'- Battle maps are scored by source pixels per scenario tile, source/image aspect match, and WebP byte density.',
|
||||
'- Battle maps are scored by the V2 adaptive 60 px/tile floor, the 8,192 px WebGL edge budget, source/image aspect match, and WebP byte density.',
|
||||
'- Battle maps with repaint reports stay in the inventory, but are skipped for the next-batch recommendation.',
|
||||
'- Story backgrounds are scored by standard 1672x941 dimensions, repeat usage, and PNG byte density.',
|
||||
'- The audit cannot judge composition or painterly quality by itself; top rows are candidates for desktop browser screenshot review.',
|
||||
|
||||
49
scripts/export-battle-map-v2-data.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, sep } from 'node:path';
|
||||
import { createServer } from 'vite';
|
||||
|
||||
const outputPath = process.argv[2] ?? join('tmp', 'battle-map-v2-runtime-data.json');
|
||||
const battleAssetMarker = '/src/assets/images/battle/';
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
server: { middlewareMode: true },
|
||||
appType: 'custom'
|
||||
});
|
||||
|
||||
try {
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
const { battleMapAssets } = await server.ssrLoadModule('/src/game/data/battleMapAssets.ts');
|
||||
|
||||
const battles = Object.values(battleScenarios).map((scenario, index) => {
|
||||
const assetUrl = battleMapAssets[scenario.mapTextureKey];
|
||||
const markerIndex = typeof assetUrl === 'string' ? assetUrl.indexOf(battleAssetMarker) : -1;
|
||||
if (markerIndex < 0) {
|
||||
throw new Error(`${scenario.id}: cannot resolve battle map asset URL ${JSON.stringify(assetUrl)}`);
|
||||
}
|
||||
|
||||
const fileName = assetUrl.slice(markerIndex + battleAssetMarker.length);
|
||||
return {
|
||||
number: index + 1,
|
||||
id: scenario.id,
|
||||
title: scenario.title,
|
||||
mapTextureKey: scenario.mapTextureKey,
|
||||
assetPath: join('src', 'assets', 'images', 'battle', fileName).split(sep).join('/'),
|
||||
width: scenario.map.width,
|
||||
height: scenario.map.height,
|
||||
terrain: scenario.map.terrain,
|
||||
units: scenario.units.map((unit) => ({
|
||||
id: unit.id,
|
||||
faction: unit.faction,
|
||||
x: unit.x,
|
||||
y: unit.y
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
mkdirSync(dirname(outputPath), { recursive: true });
|
||||
writeFileSync(outputPath, `${JSON.stringify({ generatedAt: new Date().toISOString(), battles }, null, 2)}\n`, 'utf8');
|
||||
console.log(`Wrote ${outputPath} with ${battles.length} runtime battle maps.`);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
1752
scripts/generate-battle-maps-v2.py
Normal file
@@ -6,7 +6,8 @@ import { chromium } from 'playwright';
|
||||
|
||||
const cliOptions = parseCliOptions(process.argv.slice(2));
|
||||
const defaultQaPort = process.env.QA_PORT ?? '41737';
|
||||
const targetUrl = withCanvasRenderer(process.env.QA_URL ?? `http://127.0.0.1:${defaultQaPort}/`);
|
||||
const qaRenderer = cliOptions.renderer ?? process.env.QA_RENDERER ?? 'canvas';
|
||||
const targetUrl = withRenderer(process.env.QA_URL ?? `http://127.0.0.1:${defaultQaPort}/`, qaRenderer);
|
||||
const headless = process.env.QA_HEADLESS !== '0';
|
||||
const maxRounds = Number(process.env.QA_MAX_ROUNDS ?? 80);
|
||||
const cumulativeMode = process.env.QA_CUMULATIVE === '1';
|
||||
@@ -17,9 +18,12 @@ const campaignSnapshotPath = process.env.QA_CAMPAIGN_SNAPSHOT_PATH;
|
||||
const resumeCampaignSnapshot = process.env.QA_RESUME_CAMPAIGN_SNAPSHOT === '1';
|
||||
const baselineViewport = { width: 1920, height: 1080 };
|
||||
|
||||
function withCanvasRenderer(url) {
|
||||
function withRenderer(url, renderer) {
|
||||
if (!['canvas', 'webgl'].includes(renderer)) {
|
||||
throw new Error(`Unknown QA renderer "${renderer}". Use canvas or webgl.`);
|
||||
}
|
||||
const parsed = new URL(url);
|
||||
parsed.searchParams.set('renderer', 'canvas');
|
||||
parsed.searchParams.set('renderer', renderer);
|
||||
return parsed.toString();
|
||||
}
|
||||
const supplyLabels = {
|
||||
@@ -1268,6 +1272,7 @@ try {
|
||||
async function setupBattle(page, battle) {
|
||||
await gotoTargetPage(page);
|
||||
await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined && window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 });
|
||||
await assertRequestedRenderer(page);
|
||||
await page.evaluate((selected) => {
|
||||
const now = new Date().toISOString();
|
||||
const state = {
|
||||
@@ -1317,6 +1322,7 @@ async function setupCumulativeBattle(page, battle, firstBattle) {
|
||||
: undefined;
|
||||
await gotoTargetPage(page);
|
||||
await page.waitForFunction(() => window.__HEROS_GAME__ !== undefined && window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 });
|
||||
await assertRequestedRenderer(page);
|
||||
const preparation = await page.evaluate(
|
||||
({ firstBattle, initialSnapshot, labels, protectedUnitIds, selected, slotKey, storageKey }) => {
|
||||
const now = new Date().toISOString();
|
||||
@@ -1467,6 +1473,17 @@ async function gotoTargetPage(page) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function assertRequestedRenderer(page) {
|
||||
const probe = await page.evaluate(() => ({
|
||||
type: window.__HEROS_GAME__?.renderer?.type ?? null,
|
||||
name: window.__HEROS_GAME__?.renderer?.constructor?.name ?? null
|
||||
}));
|
||||
const expectedType = qaRenderer === 'webgl' ? 2 : 1;
|
||||
if (probe.type !== expectedType) {
|
||||
throw new Error(`Expected ${qaRenderer} renderer type ${expectedType}, received ${JSON.stringify(probe)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartLocalServer() {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
|
||||
@@ -6,6 +6,7 @@ const battleMapDir = join('src', 'assets', 'images', 'battle');
|
||||
const battleMapAssetsSourcePath = join('src', 'game', 'data', 'battleMapAssets.ts');
|
||||
const minimumRasterMapWidth = 1024;
|
||||
const minimumRasterMapHeight = 768;
|
||||
const maximumRasterMapEdge = 8192;
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
@@ -21,7 +22,7 @@ try {
|
||||
|
||||
validateRuntimeMatchesSource(errors, battleMapAssets, staticBattleMapAssets);
|
||||
validateScenarioMapKeys(errors, battleScenarios, battleMapAssets);
|
||||
validateMapAssetFiles(errors, battleMapAssets, staticBattleMapAssets);
|
||||
validateMapAssetFiles(errors, battleMapAssets, staticBattleMapAssets, battleScenarios);
|
||||
validateBattleDirectoryCoverage(errors, staticBattleMapAssets);
|
||||
|
||||
if (errors.length) {
|
||||
@@ -80,11 +81,24 @@ function validateRuntimeMatchesSource(errors, battleMapAssets, staticBattleMapAs
|
||||
});
|
||||
}
|
||||
|
||||
function validateMapAssetFiles(errors, battleMapAssets, staticBattleMapAssets) {
|
||||
function validateMapAssetFiles(errors, battleMapAssets, staticBattleMapAssets, battleScenarios) {
|
||||
const scenariosByMapKey = new Map(
|
||||
Object.values(battleScenarios).map((scenario) => [scenario.mapTextureKey, scenario])
|
||||
);
|
||||
Object.entries(staticBattleMapAssets)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.forEach(([mapKey, path]) => {
|
||||
validateMapFile(errors, mapKey, path);
|
||||
const dimensions = validateMapFile(errors, mapKey, path);
|
||||
const scenario = scenariosByMapKey.get(mapKey);
|
||||
if (dimensions && scenario) {
|
||||
const widthPerTile = dimensions.width / scenario.map.width;
|
||||
const heightPerTile = dimensions.height / scenario.map.height;
|
||||
if (Math.abs(widthPerTile - heightPerTile) > 0.01) {
|
||||
errors.push(
|
||||
`${mapKey}: "${path}" is ${dimensions.width}x${dimensions.height} for a ${scenario.map.width}x${scenario.map.height} grid; expected equal source pixels per tile on both axes`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const url = battleMapAssets[mapKey];
|
||||
assertNonEmptyString(errors, url, `${mapKey} runtime asset URL`);
|
||||
@@ -178,7 +192,7 @@ function validateMapFile(errors, context, path) {
|
||||
if (!/viewBox=["'][^"']+["']/.test(text) && !/width=["'][^"']+["']/.test(text)) {
|
||||
errors.push(`${context}: "${path}" should declare SVG dimensions or a viewBox`);
|
||||
}
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (path.toLowerCase().endsWith('.webp')) {
|
||||
@@ -187,23 +201,29 @@ function validateMapFile(errors, context, path) {
|
||||
bytes.subarray(8, 12).toString('ascii') !== 'WEBP'
|
||||
) {
|
||||
errors.push(`${context}: "${path}" does not have a WebP RIFF header`);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dimensions = readWebpDimensions(bytes);
|
||||
if (!dimensions) {
|
||||
errors.push(`${context}: "${path}" does not expose readable WebP dimensions`);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
if (dimensions.width < minimumRasterMapWidth || dimensions.height < minimumRasterMapHeight) {
|
||||
errors.push(
|
||||
`${context}: "${path}" is ${dimensions.width}x${dimensions.height}, expected at least ${minimumRasterMapWidth}x${minimumRasterMapHeight}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
if (dimensions.width > maximumRasterMapEdge || dimensions.height > maximumRasterMapEdge) {
|
||||
errors.push(
|
||||
`${context}: "${path}" is ${dimensions.width}x${dimensions.height}, exceeding the ${maximumRasterMapEdge}px WebGL texture budget`
|
||||
);
|
||||
}
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
errors.push(`${context}: "${path}" uses an unsupported map asset extension`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readWebpDimensions(bytes) {
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1004 KiB |
|
Before Width: | Height: | Size: 1009 KiB After Width: | Height: | Size: 607 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 718 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 827 KiB |
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 19 MiB After Width: | Height: | Size: 6.5 MiB |
|
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 6.0 MiB |
|
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 19 MiB After Width: | Height: | Size: 5.4 MiB |
|
Before Width: | Height: | Size: 20 MiB After Width: | Height: | Size: 6.0 MiB |
|
Before Width: | Height: | Size: 19 MiB After Width: | Height: | Size: 6.5 MiB |
|
Before Width: | Height: | Size: 19 MiB After Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 17 MiB After Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 7.6 MiB |
|
Before Width: | Height: | Size: 19 MiB After Width: | Height: | Size: 6.0 MiB |
|
Before Width: | Height: | Size: 12 MiB After Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 6.5 MiB |
|
Before Width: | Height: | Size: 14 MiB After Width: | Height: | Size: 6.1 MiB |
|
Before Width: | Height: | Size: 13 MiB After Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 18 MiB After Width: | Height: | Size: 6.2 MiB |
|
Before Width: | Height: | Size: 12 MiB After Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 16 MiB After Width: | Height: | Size: 6.1 MiB |
|
Before Width: | Height: | Size: 15 MiB After Width: | Height: | Size: 5.3 MiB |
|
Before Width: | Height: | Size: 13 MiB After Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 824 KiB |
|
Before Width: | Height: | Size: 649 KiB After Width: | Height: | Size: 533 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1022 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1000 KiB |
|
Before Width: | Height: | Size: 661 KiB After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 991 KiB |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 495 KiB |
|
Before Width: | Height: | Size: 19 MiB After Width: | Height: | Size: 6.7 MiB |
|
Before Width: | Height: | Size: 8.0 MiB After Width: | Height: | Size: 6.8 MiB |
|
Before Width: | Height: | Size: 20 MiB After Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 7.5 MiB After Width: | Height: | Size: 7.0 MiB |
|
Before Width: | Height: | Size: 22 MiB After Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 7.9 MiB After Width: | Height: | Size: 6.8 MiB |
|
Before Width: | Height: | Size: 20 MiB After Width: | Height: | Size: 7.7 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 658 KiB |
BIN
src/assets/images/battle/terrain-v2/camp-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
src/assets/images/battle/terrain-v2/cliff-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
BIN
src/assets/images/battle/terrain-v2/forest-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
src/assets/images/battle/terrain-v2/fort-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
src/assets/images/battle/terrain-v2/hill-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
src/assets/images/battle/terrain-v2/plain-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
src/assets/images/battle/terrain-v2/river-v2-source.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
src/assets/images/battle/terrain-v2/road-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
src/assets/images/battle/terrain-v2/village-v2-source.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 506 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 817 KiB |
|
Before Width: | Height: | Size: 5.2 MiB After Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 8.7 MiB After Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 8.0 MiB After Width: | Height: | Size: 3.9 MiB |
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 6.7 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 9.9 MiB After Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 9.3 MiB After Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 7.4 MiB After Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 752 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 4.5 MiB After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 3.9 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 1.5 MiB |