Replace battle maps 59 and 60 with hand-painted assets

This commit is contained in:
2026-07-09 22:48:17 +09:00
parent 369be63676
commit 688a4d300d
11 changed files with 35 additions and 150 deletions

View File

@@ -2,7 +2,7 @@
## Scope
The first fifty-eight 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.
The first sixty 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.
## Art Direction
@@ -21,6 +21,7 @@ The first fifty-eight campaign battles now use the same hand-painted tactical ma
- Massive maps can use a 144px tile target through `MASSIVE_LARGE_MAP_TILE_SIZE` when 152px output approaches the WebP encoder edge.
- Colossal maps can use a 136px tile target through `COLOSSAL_LARGE_MAP_TILE_SIZE` when 144px output hits WebP encoding error 6.
- Gigantic maps can use a 128px tile target through `GIGANTIC_LARGE_MAP_TILE_SIZE` when 136px output would exceed the known WebP encoder comfort range.
- Immense maps can use a 120px tile target through `IMMENSE_LARGE_MAP_TILE_SIZE` when 128px output hits WebP encoding error 6.
- Re-evaluate the tile target before a generated map's longest edge approaches roughly 16,384px at 192px per tile.
- Use `--tile-size` only for controlled comparisons; commit the selected production size in the map config.
@@ -88,6 +89,8 @@ python scripts/generate-handpaint-map-sample.py --map fifty-fifth --webp-quality
python scripts/generate-handpaint-map-sample.py --map fifty-sixth --webp-quality 90
python scripts/generate-handpaint-map-sample.py --map fifty-seventh --webp-quality 90
python scripts/generate-handpaint-map-sample.py --map fifty-eighth --webp-quality 90
python scripts/generate-handpaint-map-sample.py --map fifty-ninth --webp-quality 90
python scripts/generate-handpaint-map-sample.py --map sixtieth --webp-quality 90
```
Comparison example:
@@ -156,6 +159,8 @@ python scripts/generate-handpaint-map-sample.py --map thirtieth --tile-size 192
- `src/assets/images/battle/fifty-sixth-battle-map.webp`
- `src/assets/images/battle/fifty-seventh-battle-map.webp`
- `src/assets/images/battle/fifty-eighth-battle-map.webp`
- `src/assets/images/battle/fifty-ninth-battle-map.webp`
- `src/assets/images/battle/sixtieth-battle-map.webp`
## QA Outputs
@@ -273,5 +278,9 @@ python scripts/generate-handpaint-map-sample.py --map thirtieth --tile-size 192
- `docs/fifty-seventh-battle-map-handpaint-preview.png`
- `docs/fifty-eighth-battle-map-handpaint-before-after.png`
- `docs/fifty-eighth-battle-map-handpaint-preview.png`
- `docs/fifty-ninth-battle-map-handpaint-before-after.png`
- `docs/fifty-ninth-battle-map-handpaint-preview.png`
- `docs/sixtieth-battle-map-handpaint-before-after.png`
- `docs/sixtieth-battle-map-handpaint-preview.png`
Browser screenshots are saved per verification run after build/deploy.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

@@ -23,6 +23,7 @@ EXTREME_LARGE_MAP_TILE_SIZE = 152
MASSIVE_LARGE_MAP_TILE_SIZE = 144
COLOSSAL_LARGE_MAP_TILE_SIZE = 136
GIGANTIC_LARGE_MAP_TILE_SIZE = 128
IMMENSE_LARGE_MAP_TILE_SIZE = 120
TILE = DEFAULT_TILE_SIZE
RNG = random.Random(240704)
@@ -582,6 +583,26 @@ MAP_CONFIGS = {
"seed": 241274,
"tile_size": GIGANTIC_LARGE_MAP_TILE_SIZE,
},
"fifty-ninth": {
"slug": "fifty-ninth-battle-map",
"terrain_export": "fiftyNinthBattleMap",
"units_export": "fiftyNinthBattleUnits",
"ally_positions_export": "fiftyNinthBattleAllyPositions",
"out": BATTLE_IMAGE_DIR / "fifty-ninth-battle-map.webp",
"before": BATTLE_IMAGE_DIR / "fifty-ninth-battle-map.svg",
"seed": 241284,
"tile_size": GIGANTIC_LARGE_MAP_TILE_SIZE,
},
"sixtieth": {
"slug": "sixtieth-battle-map",
"terrain_export": "sixtiethBattleMap",
"units_export": "sixtiethBattleUnits",
"ally_positions_export": "sixtiethBattleAllyPositions",
"out": BATTLE_IMAGE_DIR / "sixtieth-battle-map.webp",
"before": BATTLE_IMAGE_DIR / "sixtieth-battle-map.svg",
"seed": 241294,
"tile_size": IMMENSE_LARGE_MAP_TILE_SIZE,
},
}
@@ -1223,8 +1244,8 @@ def main() -> None:
config = dict(MAP_CONFIGS[args.map])
if args.tile_size is not None:
if args.tile_size < 128:
raise ValueError("--tile-size must be 128 or greater")
if args.tile_size < 120:
raise ValueError("--tile-size must be 120 or greater")
config["tile_size"] = args.tile_size
out_path = args.out or Path(config["out"])
before_path = args.before or Path(config["before"])

View File

@@ -1,79 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1888 1600" width="1888" height="1600">
<defs>
<linearGradient id="earth" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6d6246"/>
<stop offset=".44" stop-color="#9a8658"/>
<stop offset="1" stop-color="#463e2e"/>
</linearGradient>
<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#93815f"/>
<stop offset=".5" stop-color="#584c39"/>
<stop offset="1" stop-color="#272219"/>
</linearGradient>
<linearGradient id="coldRiver" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#26455a"/>
<stop offset=".48" stop-color="#4e8194"/>
<stop offset="1" stop-color="#172c3a"/>
</linearGradient>
<pattern id="dust" width="88" height="88" patternUnits="userSpaceOnUse">
<path d="M8 17c19-7 41-6 62 2M13 66c24-10 48-9 70 3M46 39c15 7 28 8 41 2" fill="none" stroke="#d3bd82" stroke-width="3" opacity=".28"/>
<circle cx="27" cy="42" r="2.5" fill="#2f271a" opacity=".2"/>
<circle cx="72" cy="23" r="2" fill="#e2cc91" opacity=".18"/>
<circle cx="58" cy="76" r="2.2" fill="#1d1a13" opacity=".22"/>
</pattern>
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="12" stdDeviation="9" flood-color="#080806" flood-opacity=".5"/>
</filter>
</defs>
<rect width="1888" height="1600" fill="url(#earth)"/>
<rect width="1888" height="1600" fill="url(#dust)" opacity=".82"/>
<path d="M0 1340 C190 1240 342 1286 520 1192 C700 1097 836 1114 1027 1002 C1225 886 1414 831 1888 770 L1888 1600 L0 1600 Z" fill="#324f2c" opacity=".78"/>
<path d="M0 1322 C206 1212 374 1248 536 1154 C714 1051 874 1074 1042 960 C1254 817 1453 775 1888 715" fill="none" stroke="#213b20" stroke-width="48" opacity=".42"/>
<path d="M0 245 C176 139 319 145 492 244 C661 341 796 315 962 212 C1135 105 1283 122 1447 232 C1604 337 1721 330 1888 226 L1888 0 L0 0 Z" fill="url(#wall)" filter="url(#shadow)" opacity=".92"/>
<path d="M0 603 C154 507 315 534 478 638 C654 750 814 705 973 584 C1138 459 1295 475 1456 589 C1600 691 1737 690 1888 596 L1888 832 C1707 900 1514 885 1357 778 C1205 674 1079 698 909 820 C720 956 551 924 375 812 C226 718 111 724 0 807 Z" fill="url(#wall)" opacity=".86"/>
<path d="M114 420 C248 364 372 394 506 477 M973 380 C1104 315 1238 324 1368 402 M1360 858 C1512 950 1685 938 1848 854" fill="none" stroke="#b1a374" stroke-width="12" opacity=".32"/>
<path d="M98 1600 C144 1410 225 1267 340 1150 C464 1024 540 868 576 692 C604 552 663 431 760 324 C842 232 928 135 997 0 L1118 0 C1053 139 965 251 885 355 C778 494 742 633 724 790 C702 1000 603 1178 465 1320 C367 1421 307 1512 282 1600 Z" fill="url(#coldRiver)" opacity=".88"/>
<path d="M190 1600 C234 1434 311 1299 424 1174 C550 1034 642 842 658 671 C672 515 729 382 826 263 C896 176 967 86 1030 0" fill="none" stroke="#a9d6d9" stroke-width="10" opacity=".34"/>
<path d="M298 1332 C517 1232 691 1132 823 1011 C1010 840 1114 678 1194 502 C1284 304 1424 176 1606 106" fill="none" stroke="#c2aa70" stroke-width="48" opacity=".76"/>
<path d="M284 1340 C512 1227 681 1128 813 1002 C996 828 1099 670 1183 488 C1270 297 1423 164 1602 88" fill="none" stroke="#53432c" stroke-width="13" opacity=".54"/>
<path d="M704 1426 C884 1306 1014 1199 1096 1070 C1194 916 1296 812 1442 735 C1610 647 1735 547 1888 435" fill="none" stroke="#c9ad71" stroke-width="44" opacity=".72"/>
<path d="M694 1435 C878 1295 1004 1188 1084 1059 C1182 902 1284 799 1434 722 C1605 634 1725 529 1888 414" fill="none" stroke="#4f412b" stroke-width="12" opacity=".52"/>
<path d="M1240 438 L1512 334 L1806 444 L1852 706 L1588 827 L1302 706 Z" fill="#8e7651" stroke="#3d3020" stroke-width="13" filter="url(#shadow)"/>
<path d="M1288 472 L1516 388 L1756 478 L1793 674 L1584 767 L1342 670 Z" fill="#5f513d" stroke="#2b2319" stroke-width="8"/>
<path d="M1390 535 L1515 489 L1648 539 L1668 646 L1554 700 L1421 644 Z" fill="#6f6047" stroke="#2c2419" stroke-width="6"/>
<g fill="#201a13" opacity=".45">
<rect x="1267" y="423" width="44" height="58" rx="4"/>
<rect x="1487" y="338" width="44" height="58" rx="4"/>
<rect x="1782" y="443" width="44" height="58" rx="4"/>
<rect x="1810" y="644" width="44" height="58" rx="4"/>
<rect x="1558" y="770" width="44" height="58" rx="4"/>
<rect x="1304" y="680" width="44" height="58" rx="4"/>
</g>
<path d="M111 1366 L285 1301 L470 1375 L505 1519 L318 1578 L139 1498 Z" fill="#a17947" stroke="#4d3925" stroke-width="10"/>
<path d="M735 1201 L920 1132 L1118 1218 L1163 1393 L961 1463 L767 1371 Z" fill="#a37c4d" stroke="#4d3925" stroke-width="10"/>
<path d="M1038 854 L1226 792 L1414 861 L1460 1024 L1265 1092 L1086 1011 Z" fill="#8d7149" stroke="#443525" stroke-width="10"/>
<g fill="#2f4b24" opacity=".8">
<circle cx="221" cy="965" r="55"/><circle cx="286" cy="922" r="43"/><circle cx="375" cy="984" r="52"/>
<circle cx="514" cy="905" r="58"/><circle cx="618" cy="955" r="45"/><circle cx="720" cy="886" r="50"/>
<circle cx="1513" cy="1119" r="58"/><circle cx="1604" cy="1170" r="53"/><circle cx="1735" cy="1096" r="63"/>
<circle cx="1641" cy="1342" r="56"/><circle cx="1770" cy="1411" r="50"/>
</g>
<g fill="none" stroke="#d5bd72" stroke-width="7" opacity=".46">
<path d="M117 1368 L318 1441 L502 1518"/>
<path d="M741 1209 L962 1296 L1156 1388"/>
<path d="M1045 862 L1268 944 L1455 1019"/>
<path d="M1321 704 L1556 609 L1802 707"/>
</g>
<g fill="#1d1912" opacity=".24">
<circle cx="318" cy="1441" r="10"/><circle cx="962" cy="1296" r="10"/><circle cx="1268" cy="944" r="10"/>
<circle cx="1447" cy="526" r="9"/><circle cx="1584" cy="612" r="10"/><circle cx="1728" cy="485" r="9"/>
<circle cx="835" cy="1084" r="9"/><circle cx="1188" cy="782" r="8"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 MiB

View File

@@ -1,66 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1632" role="img" aria-labelledby="title desc">
<title id="title">Wudu and Yinping mountain campaign map</title>
<desc id="desc">Original tactical battlefield map with twin northern valleys, mountain roads, river bends, Shu camps, Wei forts, and commandery towns.</desc>
<defs>
<linearGradient id="ground" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#56694b"/>
<stop offset="0.46" stop-color="#74835a"/>
<stop offset="1" stop-color="#a28a58"/>
</linearGradient>
<linearGradient id="river" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#2c6476"/>
<stop offset="1" stop-color="#5da3a9"/>
</linearGradient>
<filter id="grain" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="3" seed="60"/>
<feColorMatrix type="saturate" values="0.18"/>
<feBlend mode="multiply" in2="SourceGraphic"/>
</filter>
</defs>
<rect width="1920" height="1632" fill="url(#ground)"/>
<path d="M48 98 C230 210 190 360 380 500 C558 631 505 812 720 928 C928 1042 890 1218 1110 1342 C1328 1468 1494 1464 1872 1562" fill="none" stroke="url(#river)" stroke-width="88" stroke-linecap="round"/>
<path d="M974 8 C920 170 998 306 924 454 C842 620 946 776 874 934 C812 1070 850 1208 760 1450" fill="none" stroke="#356e7b" stroke-width="54" stroke-linecap="round" opacity="0.88"/>
<path d="M86 1410 C324 1358 468 1258 612 1142 C790 1000 1008 956 1188 806 C1388 640 1524 488 1840 358" fill="none" stroke="#c8a66b" stroke-width="42" stroke-linecap="round"/>
<path d="M264 1346 C442 1200 540 1032 760 950 C990 864 1084 690 1260 562 C1448 424 1612 414 1838 254" fill="none" stroke="#9f7d50" stroke-width="22" stroke-linecap="round"/>
<path d="M330 1466 C486 1302 650 1250 820 1122 C992 994 1186 1010 1350 862 C1514 714 1594 574 1850 530" fill="none" stroke="#d1b378" stroke-width="30" stroke-linecap="round"/>
<g fill="#394834" opacity="0.86">
<path d="M0 0 L430 0 L330 198 L450 380 L236 468 L0 392 Z"/>
<path d="M612 0 L936 0 L852 230 L690 336 L574 190 Z"/>
<path d="M1120 0 L1920 0 L1920 332 L1728 278 L1520 372 L1368 210 Z"/>
<path d="M1508 558 L1920 470 L1920 972 L1684 880 L1456 938 Z"/>
<path d="M0 548 L392 610 L236 848 L410 1078 L0 1162 Z"/>
</g>
<g fill="#253a2f" opacity="0.72">
<ellipse cx="280" cy="720" rx="210" ry="142"/>
<ellipse cx="542" cy="930" rx="196" ry="126"/>
<ellipse cx="1288" cy="1140" rx="280" ry="144"/>
<ellipse cx="1548" cy="1032" rx="206" ry="112"/>
<ellipse cx="1010" cy="642" rx="210" ry="116"/>
</g>
<g fill="#7c5d46" stroke="#3b2f28" stroke-width="8">
<rect x="222" y="1308" width="228" height="132" rx="6"/>
<rect x="574" y="1112" width="248" height="144" rx="6"/>
<rect x="930" y="920" width="260" height="150" rx="6"/>
<rect x="1278" y="674" width="286" height="164" rx="6"/>
<rect x="1580" y="356" width="250" height="148" rx="6"/>
</g>
<g fill="#5f3f33" stroke="#2b201b" stroke-width="10">
<path d="M1320 432 L1524 392 L1634 512 L1572 656 L1356 674 L1248 552 Z"/>
<path d="M890 654 L1088 616 L1208 728 L1148 870 L940 902 L822 774 Z"/>
<path d="M1492 174 L1708 148 L1838 250 L1772 386 L1556 414 L1434 300 Z"/>
</g>
<g fill="#d8c08d" stroke="#5e4733" stroke-width="7">
<rect x="148" y="1218" width="112" height="74" rx="5"/>
<rect x="468" y="1048" width="116" height="76" rx="5"/>
<rect x="704" y="900" width="122" height="78" rx="5"/>
<rect x="1162" y="778" width="126" height="82" rx="5"/>
<rect x="1370" y="520" width="122" height="78" rx="5"/>
</g>
<g fill="#ead9a5" font-family="serif" font-size="46" font-weight="700" stroke="#3a2a20" stroke-width="2" paint-order="stroke">
<text x="254" y="1388">촉 진영</text>
<text x="626" y="1196">음평로</text>
<text x="978" y="1010">무도로</text>
<text x="1326" y="774">곽회 방어선</text>
<text x="1608" y="454">위 본영</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 MiB

View File

@@ -8,8 +8,8 @@ import fiftyThirdBattleMapUrl from '../../assets/images/battle/fifty-third-battl
import fiftyFourthBattleMapUrl from '../../assets/images/battle/fifty-fourth-battle-map.webp';
import fiftyFifthBattleMapUrl from '../../assets/images/battle/fifty-fifth-battle-map.webp';
import fiftyEighthBattleMapUrl from '../../assets/images/battle/fifty-eighth-battle-map.webp';
import fiftyNinthBattleMapUrl from '../../assets/images/battle/fifty-ninth-battle-map.svg';
import sixtiethBattleMapUrl from '../../assets/images/battle/sixtieth-battle-map.svg';
import fiftyNinthBattleMapUrl from '../../assets/images/battle/fifty-ninth-battle-map.webp';
import sixtiethBattleMapUrl from '../../assets/images/battle/sixtieth-battle-map.webp';
import sixtyFirstBattleMapUrl from '../../assets/images/battle/sixty-first-battle-map.svg';
import sixtySecondBattleMapUrl from '../../assets/images/battle/sixty-second-battle-map.svg';
import sixtyThirdBattleMapUrl from '../../assets/images/battle/sixty-third-battle-map.svg';