diff --git a/docs/battle-map-terrain-atlas-pipeline.md b/docs/battle-map-terrain-atlas-pipeline.md index 69aedd1..f64cb27 100644 --- a/docs/battle-map-terrain-atlas-pipeline.md +++ b/docs/battle-map-terrain-atlas-pipeline.md @@ -2,7 +2,7 @@ ## 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 @@ -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 sixth --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 @@ -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/sixth-battle-map.webp` - `src/assets/images/battle/seventh-battle-map.webp` +- `src/assets/images/battle/eighth-battle-map.webp` ## 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/seventh-battle-map-handpaint-before-after.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. diff --git a/docs/eighth-battle-map-handpaint-before-after.png b/docs/eighth-battle-map-handpaint-before-after.png new file mode 100644 index 0000000..c93224f Binary files /dev/null and b/docs/eighth-battle-map-handpaint-before-after.png differ diff --git a/docs/eighth-battle-map-handpaint-preview.png b/docs/eighth-battle-map-handpaint-preview.png new file mode 100644 index 0000000..ec43ded Binary files /dev/null and b/docs/eighth-battle-map-handpaint-preview.png differ diff --git a/scripts/generate-handpaint-map-sample.py b/scripts/generate-handpaint-map-sample.py index 7a35e77..01469b1 100644 --- a/scripts/generate-handpaint-map-sample.py +++ b/scripts/generate-handpaint-map-sample.py @@ -82,6 +82,15 @@ MAP_CONFIGS = { "before": BATTLE_IMAGE_DIR / "seventh-battle-map.svg", "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}") +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]]: text = SCENARIO_FILE.read_text(encoding="utf-8") start = text.index(f"export const {export_name}") - terrain_key = text.index("terrain:", start) - bracket_start = text.index("[", terrain_key) - return ast.literal_eval(extract_balanced(text, bracket_start, "[", "]")) + brace_start = text.index("{", text.index("=", start)) + map_block = extract_balanced(text, brace_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: diff --git a/src/assets/images/battle/eighth-battle-map.svg b/src/assets/images/battle/eighth-battle-map.svg deleted file mode 100644 index 8bed22d..0000000 --- a/src/assets/images/battle/eighth-battle-map.svg +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/images/battle/eighth-battle-map.webp b/src/assets/images/battle/eighth-battle-map.webp new file mode 100644 index 0000000..9716648 Binary files /dev/null and b/src/assets/images/battle/eighth-battle-map.webp differ diff --git a/src/game/data/battleMapAssets.ts b/src/game/data/battleMapAssets.ts index a838605..970ecd3 100644 --- a/src/game/data/battleMapAssets.ts +++ b/src/game/data/battleMapAssets.ts @@ -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 eleventhBattleMapUrl from '../../assets/images/battle/eleventh-battle-map.svg'; import fiftiethBattleMapUrl from '../../assets/images/battle/fiftieth-battle-map.svg';