feat: roll out V2 art across all battle maps

This commit is contained in:
2026-07-15 21:19:30 +09:00
parent b5aad69e39
commit 2530694bca
86 changed files with 3146 additions and 101 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
node_modules/
dist/
/tmp/
__pycache__/
.dev-server.log
.dev-server.err
*.local

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

File diff suppressed because it is too large Load Diff

View 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.

View File

@@ -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

View File

@@ -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",

View File

@@ -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.',

View 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();
}

File diff suppressed because it is too large Load Diff

View 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();

View File

@@ -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) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1004 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1009 KiB

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 KiB

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 MiB

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 MiB

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 MiB

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 MiB

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 MiB

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 824 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 649 KiB

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1022 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 991 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 MiB

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 MiB

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 MiB

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 MiB

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB