Replace eighth battle map with hand-painted asset
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
The first seven 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 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.
|
||||||
|
|
||||||
## Art Direction
|
## Art Direction
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ python scripts/generate-handpaint-map-sample.py --map fourth --webp-quality 90
|
|||||||
python scripts/generate-handpaint-map-sample.py --map fifth --webp-quality 90
|
python scripts/generate-handpaint-map-sample.py --map fifth --webp-quality 90
|
||||||
python scripts/generate-handpaint-map-sample.py --map sixth --webp-quality 90
|
python scripts/generate-handpaint-map-sample.py --map sixth --webp-quality 90
|
||||||
python scripts/generate-handpaint-map-sample.py --map seventh --webp-quality 90
|
python scripts/generate-handpaint-map-sample.py --map seventh --webp-quality 90
|
||||||
|
python scripts/generate-handpaint-map-sample.py --map eighth --webp-quality 90
|
||||||
```
|
```
|
||||||
|
|
||||||
## Current Assets
|
## Current Assets
|
||||||
@@ -35,6 +36,7 @@ python scripts/generate-handpaint-map-sample.py --map seventh --webp-quality 90
|
|||||||
- `src/assets/images/battle/fifth-battle-map.webp`
|
- `src/assets/images/battle/fifth-battle-map.webp`
|
||||||
- `src/assets/images/battle/sixth-battle-map.webp`
|
- `src/assets/images/battle/sixth-battle-map.webp`
|
||||||
- `src/assets/images/battle/seventh-battle-map.webp`
|
- `src/assets/images/battle/seventh-battle-map.webp`
|
||||||
|
- `src/assets/images/battle/eighth-battle-map.webp`
|
||||||
|
|
||||||
## QA Outputs
|
## QA Outputs
|
||||||
|
|
||||||
@@ -50,5 +52,7 @@ python scripts/generate-handpaint-map-sample.py --map seventh --webp-quality 90
|
|||||||
- `docs/sixth-battle-map-handpaint-preview.png`
|
- `docs/sixth-battle-map-handpaint-preview.png`
|
||||||
- `docs/seventh-battle-map-handpaint-before-after.png`
|
- `docs/seventh-battle-map-handpaint-before-after.png`
|
||||||
- `docs/seventh-battle-map-handpaint-preview.png`
|
- `docs/seventh-battle-map-handpaint-preview.png`
|
||||||
|
- `docs/eighth-battle-map-handpaint-before-after.png`
|
||||||
|
- `docs/eighth-battle-map-handpaint-preview.png`
|
||||||
|
|
||||||
Browser screenshots are saved per verification run after build/deploy.
|
Browser screenshots are saved per verification run after build/deploy.
|
||||||
|
|||||||
BIN
docs/eighth-battle-map-handpaint-before-after.png
Normal file
BIN
docs/eighth-battle-map-handpaint-before-after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/eighth-battle-map-handpaint-preview.png
Normal file
BIN
docs/eighth-battle-map-handpaint-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -82,6 +82,15 @@ MAP_CONFIGS = {
|
|||||||
"before": BATTLE_IMAGE_DIR / "seventh-battle-map.svg",
|
"before": BATTLE_IMAGE_DIR / "seventh-battle-map.svg",
|
||||||
"seed": 240764,
|
"seed": 240764,
|
||||||
},
|
},
|
||||||
|
"eighth": {
|
||||||
|
"slug": "eighth-battle-map",
|
||||||
|
"terrain_export": "eighthBattleMap",
|
||||||
|
"units_export": "eighthBattleUnits",
|
||||||
|
"ally_positions_export": "eighthBattleAllyPositions",
|
||||||
|
"out": BATTLE_IMAGE_DIR / "eighth-battle-map.webp",
|
||||||
|
"before": BATTLE_IMAGE_DIR / "eighth-battle-map.svg",
|
||||||
|
"seed": 240774,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -132,12 +141,77 @@ def extract_balanced(text: str, start: int, open_char: str, close_char: str) ->
|
|||||||
raise RuntimeError(f"Could not find balanced block starting at {start}")
|
raise RuntimeError(f"Could not find balanced block starting at {start}")
|
||||||
|
|
||||||
|
|
||||||
|
def ts_condition_to_python(condition: str) -> str:
|
||||||
|
expression = condition.replace("\n", " ")
|
||||||
|
expression = expression.replace("!==", "!=")
|
||||||
|
expression = expression.replace("===", "==")
|
||||||
|
expression = expression.replace("&&", " and ")
|
||||||
|
expression = expression.replace("||", " or ")
|
||||||
|
expression = expression.replace("Math.floor", "math.floor")
|
||||||
|
return expression
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_terrain_factory(text: str, factory_name: str) -> list[list[str]]:
|
||||||
|
start = text.index(f"function {factory_name}")
|
||||||
|
brace_start = text.index("{", start)
|
||||||
|
block = extract_balanced(text, brace_start, "{", "}")
|
||||||
|
size_match = re.search(
|
||||||
|
r"Array\.from\(\{\s*length:\s*(\d+)\s*\}.*?Array\.from\(\{\s*length:\s*(\d+)\s*\}",
|
||||||
|
block,
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
if not size_match:
|
||||||
|
raise RuntimeError(f"Could not read terrain dimensions from {factory_name}")
|
||||||
|
|
||||||
|
height = int(size_match.group(1))
|
||||||
|
width = int(size_match.group(2))
|
||||||
|
rules: list[tuple[str, str]] = []
|
||||||
|
cursor = 0
|
||||||
|
while True:
|
||||||
|
match = re.search(r"\bif\s*\(", block[cursor:])
|
||||||
|
if not match:
|
||||||
|
break
|
||||||
|
paren_start = cursor + match.end() - 1
|
||||||
|
condition_block = extract_balanced(block, paren_start, "(", ")")
|
||||||
|
brace_start = block.index("{", paren_start + len(condition_block))
|
||||||
|
body = extract_balanced(block, brace_start, "{", "}")
|
||||||
|
terrain_match = re.search(r"return\s+'([^']+)';", body)
|
||||||
|
if terrain_match:
|
||||||
|
rules.append((ts_condition_to_python(condition_block[1:-1]), terrain_match.group(1)))
|
||||||
|
cursor = brace_start + len(body)
|
||||||
|
|
||||||
|
returns = re.findall(r"return\s+'([^']+)';", block)
|
||||||
|
if not returns:
|
||||||
|
raise RuntimeError(f"Could not read terrain fallback from {factory_name}")
|
||||||
|
fallback = returns[-1]
|
||||||
|
|
||||||
|
terrain: list[list[str]] = []
|
||||||
|
for y in range(height):
|
||||||
|
row: list[str] = []
|
||||||
|
for x in range(width):
|
||||||
|
tile = fallback
|
||||||
|
for expression, terrain_type in rules:
|
||||||
|
if eval(expression, {"__builtins__": {}}, {"x": x, "y": y, "math": math}):
|
||||||
|
tile = terrain_type
|
||||||
|
break
|
||||||
|
row.append(tile)
|
||||||
|
terrain.append(row)
|
||||||
|
return terrain
|
||||||
|
|
||||||
|
|
||||||
def parse_battle_terrain(export_name: str) -> list[list[str]]:
|
def parse_battle_terrain(export_name: str) -> list[list[str]]:
|
||||||
text = SCENARIO_FILE.read_text(encoding="utf-8")
|
text = SCENARIO_FILE.read_text(encoding="utf-8")
|
||||||
start = text.index(f"export const {export_name}")
|
start = text.index(f"export const {export_name}")
|
||||||
terrain_key = text.index("terrain:", start)
|
brace_start = text.index("{", text.index("=", start))
|
||||||
bracket_start = text.index("[", terrain_key)
|
map_block = extract_balanced(text, brace_start, "{", "}")
|
||||||
return ast.literal_eval(extract_balanced(text, bracket_start, "[", "]"))
|
terrain_key = map_block.index("terrain:")
|
||||||
|
terrain_source = map_block[terrain_key + len("terrain:") :].lstrip()
|
||||||
|
if terrain_source.startswith("["):
|
||||||
|
return ast.literal_eval(extract_balanced(terrain_source, 0, "[", "]"))
|
||||||
|
factory_match = re.match(r"(\w+)\(\)", terrain_source)
|
||||||
|
if factory_match:
|
||||||
|
return evaluate_terrain_factory(text, factory_match.group(1))
|
||||||
|
raise RuntimeError(f"Unsupported terrain source for {export_name}")
|
||||||
|
|
||||||
|
|
||||||
def parse_export_array_source(export_name: str) -> str:
|
def parse_export_array_source(export_name: str) -> str:
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2600 2200" width="2600" height="2200">
|
|
||||||
<defs>
|
|
||||||
<filter id="grain">
|
|
||||||
<feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="4" seed="821" />
|
|
||||||
<feColorMatrix type="saturate" values="0.62" />
|
|
||||||
<feBlend mode="multiply" in2="SourceGraphic" />
|
|
||||||
</filter>
|
|
||||||
<pattern id="grass" width="96" height="96" patternUnits="userSpaceOnUse">
|
|
||||||
<rect width="96" height="96" fill="#61764b" />
|
|
||||||
<path d="M8 28h28M48 58h30M24 76l20-14M66 18l16 12" stroke="#b7c077" stroke-width="3" opacity="0.24" />
|
|
||||||
<path d="M12 62l14-24M40 34l13-18M70 80l9-25" stroke="#2f4b2d" stroke-width="2" opacity="0.36" />
|
|
||||||
<circle cx="20" cy="44" r="3" fill="#7f9258" opacity="0.42" />
|
|
||||||
<circle cx="68" cy="39" r="4" fill="#3f5f35" opacity="0.32" />
|
|
||||||
</pattern>
|
|
||||||
<pattern id="earth" width="104" height="104" patternUnits="userSpaceOnUse">
|
|
||||||
<rect width="104" height="104" fill="#9b7749" />
|
|
||||||
<path d="M0 26c38-18 70 12 104-8M0 70c42-18 72 14 104-4" stroke="#c9a069" stroke-width="7" opacity="0.25" />
|
|
||||||
<path d="M20 86l18-24M62 42l24-16M78 82l14-9" stroke="#57422c" stroke-width="3" opacity="0.34" />
|
|
||||||
</pattern>
|
|
||||||
<pattern id="forest" width="126" height="110" patternUnits="userSpaceOnUse">
|
|
||||||
<rect width="126" height="110" fill="#31482e" />
|
|
||||||
<circle cx="26" cy="42" r="30" fill="#203823" />
|
|
||||||
<circle cx="62" cy="32" r="35" fill="#587448" />
|
|
||||||
<circle cx="96" cy="50" r="31" fill="#294328" />
|
|
||||||
<circle cx="52" cy="82" r="27" fill="#69844f" />
|
|
||||||
<path d="M14 62c34 18 74 16 104-8M36 26c22 18 54 16 72-6" stroke="#142615" stroke-width="5" opacity="0.36" />
|
|
||||||
</pattern>
|
|
||||||
<pattern id="hill" width="132" height="98" patternUnits="userSpaceOnUse">
|
|
||||||
<rect width="132" height="98" fill="#77704b" />
|
|
||||||
<path d="M-10 84c48-62 92-82 134-64c36 15 56 44 74 80" fill="#55543a" opacity="0.72" />
|
|
||||||
<path d="M18 76c36-40 72-50 108-32" fill="none" stroke="#aa9d66" stroke-width="10" opacity="0.3" />
|
|
||||||
<path d="M40 36l30 20M82 22l22 22" stroke="#393828" stroke-width="4" opacity="0.34" />
|
|
||||||
</pattern>
|
|
||||||
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#61a4bd" />
|
|
||||||
<stop offset="0.48" stop-color="#2c6687" />
|
|
||||||
<stop offset="1" stop-color="#17384f" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="wall" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#907a5f" />
|
|
||||||
<stop offset="1" stop-color="#493728" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="roof" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#d79a41" />
|
|
||||||
<stop offset="1" stop-color="#7a4d1d" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<rect width="2600" height="2200" fill="url(#grass)" />
|
|
||||||
<g filter="url(#grain)">
|
|
||||||
<path d="M-80 2020C210 1740 386 1542 604 1322C836 1086 1056 1018 1260 1014C1508 1008 1740 888 2040 708C2268 570 2440 484 2680 438" fill="none" stroke="#a77e4d" stroke-width="272" stroke-linecap="round" opacity="0.96" />
|
|
||||||
<path d="M-80 2020C210 1740 386 1542 604 1322C836 1086 1056 1018 1260 1014C1508 1008 1740 888 2040 708C2268 570 2440 484 2680 438" fill="none" stroke="url(#earth)" stroke-width="178" stroke-linecap="round" opacity="0.96" />
|
|
||||||
<path d="M300 2220C350 1830 304 1600 430 1382C560 1158 860 1110 1160 1028C1446 950 1788 764 2180 458" fill="none" stroke="#a77e4d" stroke-width="190" stroke-linecap="round" opacity="0.9" />
|
|
||||||
<path d="M300 2220C350 1830 304 1600 430 1382C560 1158 860 1110 1160 1028C1446 950 1788 764 2180 458" fill="none" stroke="url(#earth)" stroke-width="114" stroke-linecap="round" opacity="0.86" />
|
|
||||||
|
|
||||||
<path d="M1460 -80c-88 250-58 420 72 552c158 160 194 350 106 568c-76 190-52 354 72 492c128 142 170 326 88 594" fill="none" stroke="#173c52" stroke-width="280" stroke-linecap="round" opacity="0.96" />
|
|
||||||
<path d="M1460 -80c-88 250-58 420 72 552c158 160 194 350 106 568c-76 190-52 354 72 492c128 142 170 326 88 594" fill="none" stroke="url(#river)" stroke-width="184" stroke-linecap="round" opacity="0.98" />
|
|
||||||
<path d="M1508 146c36 132 48 250-16 384M1638 782c88 108 102 238 34 386M1808 1560c58 108 68 216 24 332" fill="none" stroke="#c0eef5" stroke-width="14" opacity="0.34" />
|
|
||||||
|
|
||||||
<path d="M-40 40c260-76 518-42 760 106c40 146-54 282-276 408c-238-22-402-104-492-250Z" fill="url(#forest)" opacity="0.84" />
|
|
||||||
<path d="M520 320c236-98 474-70 710 78c14 152-88 268-306 346c-246-32-382-170-404-424Z" fill="url(#forest)" opacity="0.82" />
|
|
||||||
<path d="M18 762c314-118 610-64 872 156c-70 226-330 350-774 366C-16 1118-42 950 18 762Z" fill="url(#forest)" opacity="0.84" />
|
|
||||||
<path d="M1800 1710c260-96 520-66 760 88v360c-298 76-572 18-820-176c-50-112-30-202 60-272Z" fill="url(#forest)" opacity="0.82" />
|
|
||||||
<path d="M910 1540c236-84 432-58 590 72c-42 220-190 366-448 438c-148-76-206-246-142-510Z" fill="url(#hill)" opacity="0.84" />
|
|
||||||
<path d="M2240 760c150-38 286-12 410 82v464c-170 64-322 36-454-82c-58-160-42-314 44-464Z" fill="url(#hill)" opacity="0.86" />
|
|
||||||
|
|
||||||
<g transform="translate(2020 230)">
|
|
||||||
<rect x="-46" y="-26" width="520" height="318" rx="20" fill="#241d16" opacity="0.55" />
|
|
||||||
<rect x="0" y="0" width="450" height="260" rx="16" fill="url(#wall)" />
|
|
||||||
<rect x="42" y="50" width="366" height="170" fill="#765f43" stroke="#241d15" stroke-width="12" />
|
|
||||||
<path d="M42 50h366M42 220h366M42 50v170M408 50v170" stroke="#35291e" stroke-width="10" />
|
|
||||||
<rect x="82" y="104" width="74" height="84" fill="#98704a" />
|
|
||||||
<rect x="198" y="78" width="88" height="110" fill="#9e7650" />
|
|
||||||
<rect x="330" y="104" width="58" height="84" fill="#8c6846" />
|
|
||||||
<path d="M62 104l58-48l62 48M184 78l60-52l70 52M318 104l42-38l54 38" fill="url(#roof)" stroke="#302316" stroke-width="9" />
|
|
||||||
<path d="M220 188v-60h46v60" fill="#1f1712" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(760 1220)">
|
|
||||||
<rect x="0" y="0" width="230" height="130" rx="10" fill="#6d563b" stroke="#2a2119" stroke-width="10" />
|
|
||||||
<path d="M-16 12L116-74l132 86Z" fill="url(#roof)" stroke="#342313" stroke-width="10" />
|
|
||||||
<path d="M42 130v-58h46v58M140 130v-70h46v70" fill="#2a2119" opacity="0.8" />
|
|
||||||
</g>
|
|
||||||
<g transform="translate(1710 770)">
|
|
||||||
<rect x="0" y="0" width="220" height="126" rx="10" fill="#6d563b" stroke="#2a2119" stroke-width="10" />
|
|
||||||
<path d="M-14 14L110-62l128 76Z" fill="url(#roof)" stroke="#342313" stroke-width="10" />
|
|
||||||
<path d="M78 126v-56h52v56" fill="#2a2119" opacity="0.8" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<g transform="translate(60 1630)">
|
|
||||||
<rect x="0" y="0" width="315" height="230" rx="16" fill="#4f5c53" stroke="#222820" stroke-width="14" />
|
|
||||||
<path d="M20 56h270M20 116h270M20 176h270M74 20v190M154 20v190M234 20v190" stroke="#263124" stroke-width="9" opacity="0.68" />
|
|
||||||
<rect x="72" y="-34" width="168" height="76" fill="url(#roof)" stroke="#2c2115" stroke-width="10" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/images/battle/eighth-battle-map.webp
Normal file
BIN
src/assets/images/battle/eighth-battle-map.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1009 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.svg';
|
import eighthBattleMapUrl from '../../assets/images/battle/eighth-battle-map.webp';
|
||||||
import eighteenthBattleMapUrl from '../../assets/images/battle/eighteenth-battle-map.svg';
|
import eighteenthBattleMapUrl from '../../assets/images/battle/eighteenth-battle-map.svg';
|
||||||
import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg';
|
import eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg';
|
||||||
import fiftiethBattleMapUrl from '../../assets/images/battle/fiftieth-battle-map.svg';
|
import fiftiethBattleMapUrl from '../../assets/images/battle/fiftieth-battle-map.svg';
|
||||||
|
|||||||
Reference in New Issue
Block a user