Expand handpainted tactical maps

This commit is contained in:
2026-07-04 11:10:22 +09:00
parent d13e69e4d3
commit fe7d1167a7
17 changed files with 203 additions and 225 deletions

View File

@@ -0,0 +1,38 @@
# Hand-Painted Tactical Map Pipeline
## Scope
The first three 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
- Units must read before the terrain at 100% desktop browser scale.
- Terrain uses muted greens, ochre roads, dark blue-green water, and compact structure markers.
- Roads, forests, rivers, camps, villages, cliffs, and forts should be recognizable by silhouette and texture, not by high saturation.
- The grid remains subtle and embedded in the paint grade, so it helps targeting without becoming the main visual layer.
## Generator
`scripts/generate-handpaint-map-sample.py` reads terrain and unit positions from `src/game/data/scenario.ts`, then outputs a WebP map and documentation previews.
Example:
```powershell
python scripts/generate-handpaint-map-sample.py --map second --webp-quality 90
python scripts/generate-handpaint-map-sample.py --map third --webp-quality 90
```
## Current Assets
- `src/assets/images/battle/first-battle-map.webp`
- `src/assets/images/battle/second-battle-map.webp`
- `src/assets/images/battle/third-battle-map.webp`
## QA Outputs
- `docs/second-battle-map-handpaint-before-after.png`
- `docs/second-battle-map-handpaint-preview.png`
- `docs/third-battle-map-handpaint-before-after.png`
- `docs/third-battle-map-handpaint-preview.png`
Browser screenshots are saved per verification run after build/deploy.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -12,12 +12,42 @@ from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter, ImageOp
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SCENARIO_FILE = ROOT / "src" / "game" / "data" / "scenario.ts" SCENARIO_FILE = ROOT / "src" / "game" / "data" / "scenario.ts"
FIRST_BATTLE_MAP = ROOT / "src" / "assets" / "images" / "battle" / "first-battle-map.webp" BATTLE_IMAGE_DIR = ROOT / "src" / "assets" / "images" / "battle"
DOCS_DIR = ROOT / "docs" DOCS_DIR = ROOT / "docs"
TILE = 224 TILE = 224
RNG = random.Random(240704) RNG = random.Random(240704)
MAP_CONFIGS = {
"first": {
"slug": "first-battle-map",
"terrain_export": "firstBattleMap",
"units_export": "firstBattleUnits",
"ally_positions_export": None,
"out": BATTLE_IMAGE_DIR / "first-battle-map.webp",
"before": BATTLE_IMAGE_DIR / "first-battle-map.webp",
"seed": 240704,
},
"second": {
"slug": "second-battle-map",
"terrain_export": "secondBattleMap",
"units_export": "secondBattleUnits",
"ally_positions_export": "secondBattleAllyPositions",
"out": BATTLE_IMAGE_DIR / "second-battle-map.webp",
"before": BATTLE_IMAGE_DIR / "second-battle-map.svg",
"seed": 240714,
},
"third": {
"slug": "third-battle-map",
"terrain_export": "thirdBattleMap",
"units_export": "thirdBattleUnits",
"ally_positions_export": "thirdBattleAllyPositions",
"out": BATTLE_IMAGE_DIR / "third-battle-map.webp",
"before": BATTLE_IMAGE_DIR / "third-battle-map.svg",
"seed": 240724,
},
}
PALETTE = { PALETTE = {
"plain": (88, 104, 62), "plain": (88, 104, 62),
@@ -53,33 +83,76 @@ def jitter(color: tuple[int, int, int], amount: int) -> tuple[int, int, int]:
return tuple(clamp(channel + RNG.randint(-amount, amount)) for channel in color) return tuple(clamp(channel + RNG.randint(-amount, amount)) for channel in color)
def parse_first_battle_terrain() -> list[list[str]]: def extract_balanced(text: str, start: int, open_char: str, close_char: str) -> str:
text = SCENARIO_FILE.read_text(encoding="utf-8")
start = text.index("export const firstBattleMap")
terrain_key = text.index("terrain:", start)
bracket_start = text.index("[", terrain_key)
depth = 0 depth = 0
for index in range(bracket_start, len(text)): for index in range(start, len(text)):
char = text[index] char = text[index]
if char == "[": if char == open_char:
depth += 1 depth += 1
elif char == "]": elif char == close_char:
depth -= 1 depth -= 1
if depth == 0: if depth == 0:
return ast.literal_eval(text[bracket_start : index + 1]) return text[start : index + 1]
raise RuntimeError("Could not parse first battle terrain") raise RuntimeError(f"Could not find balanced block starting at {start}")
def parse_first_battle_units() -> list[dict[str, object]]: 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")
block_start = text.index("export const firstBattleUnits") start = text.index(f"export const {export_name}")
block_end = text.index("const secondBattleAllyPositions", block_start) terrain_key = text.index("terrain:", start)
block = text[block_start:block_end] bracket_start = text.index("[", terrain_key)
return ast.literal_eval(extract_balanced(text, bracket_start, "[", "]"))
def parse_export_array_source(export_name: str) -> str:
text = SCENARIO_FILE.read_text(encoding="utf-8")
start = text.index(f"export const {export_name}")
bracket_start = text.index("[", text.index("=", start))
return extract_balanced(text, bracket_start, "[", "]")
def parse_ally_positions(export_name: str | None) -> list[dict[str, object]]:
if export_name is None:
return []
text = SCENARIO_FILE.read_text(encoding="utf-8")
marker = f"const {export_name}"
if marker not in text:
return []
start = text.index(marker)
brace_start = text.index("{", text.index("=", start))
block = extract_balanced(text, brace_start, "{", "}")
display_names = {
"liu-bei": "Liu Bei",
"guan-yu": "Guan Yu",
"zhang-fei": "Zhang Fei",
}
units: list[dict[str, object]] = [] units: list[dict[str, object]] = []
for match in re.finditer(r"id: '([^']+)'.*?name: '([^']+)'.*?faction: '([^']+)'.*?x: (\d+),\s*y: (\d+)", block, re.S): for match in re.finditer(r"'([^']+)':\s*\{\s*x:\s*(\d+),\s*y:\s*(\d+)\s*\}", block):
unit_id = match.group(1)
units.append( units.append(
{ {
"id": match.group(1), "id": unit_id,
"name": display_names.get(unit_id, unit_id),
"faction": "ally",
"x": int(match.group(2)),
"y": int(match.group(3)),
}
)
return units
def parse_battle_units(units_export: str, ally_positions_export: str | None) -> list[dict[str, object]]:
block = parse_export_array_source(units_export)
units = parse_ally_positions(ally_positions_export)
seen = {str(unit["id"]) for unit in units}
for match in re.finditer(r"id: '([^']+)'.*?name: '([^']+)'.*?faction: '([^']+)'.*?x: (\d+),\s*y: (\d+)", block, re.S):
unit_id = match.group(1)
if unit_id in seen:
continue
seen.add(unit_id)
units.append(
{
"id": unit_id,
"name": match.group(2), "name": match.group(2),
"faction": match.group(3), "faction": match.group(3),
"x": int(match.group(4)), "x": int(match.group(4)),
@@ -464,9 +537,12 @@ def add_subtle_grid_and_grade(canvas: Image.Image, terrain: list[list[str]]) ->
return graded return graded
def build_map() -> Image.Image: def build_map(config: dict[str, object]) -> Image.Image:
terrain = parse_first_battle_terrain() global RNG
units = parse_first_battle_units() RNG = random.Random(int(config["seed"]))
terrain = parse_battle_terrain(str(config["terrain_export"]))
ally_positions_export = config["ally_positions_export"]
units = parse_battle_units(str(config["units_export"]), None if ally_positions_export is None else str(ally_positions_export))
width = len(terrain[0]) * TILE width = len(terrain[0]) * TILE
height = len(terrain) * TILE height = len(terrain) * TILE
canvas = add_base_texture(width, height) canvas = add_base_texture(width, height)
@@ -477,6 +553,42 @@ def build_map() -> Image.Image:
return add_subtle_grid_and_grade(canvas, terrain) return add_subtle_grid_and_grade(canvas, terrain)
def make_flat_legacy_preview(terrain: list[list[str]], size: tuple[int, int]) -> Image.Image:
colors = {
"plain": (97, 116, 75),
"road": (161, 124, 72),
"forest": (50, 78, 42),
"hill": (119, 105, 72),
"cliff": (76, 68, 57),
"river": (46, 93, 113),
"village": (134, 92, 48),
"camp": (171, 137, 74),
"fort": (94, 72, 48),
}
width = len(terrain[0]) * 96
height = len(terrain) * 96
image = Image.new("RGB", (width, height), colors["plain"])
draw = ImageDraw.Draw(image)
for y, row in enumerate(terrain):
for x, kind in enumerate(row):
left, top = x * 96, y * 96
draw.rectangle((left, top, left + 96, top + 96), fill=colors.get(kind, colors["plain"]))
if kind == "road":
draw.line((left, top + 52, left + 96, top + 44), fill=(199, 159, 93), width=9)
elif kind == "forest":
for ox, oy in ((28, 30), (54, 38), (40, 60)):
draw.ellipse((left + ox - 16, top + oy - 16, left + ox + 16, top + oy + 16), fill=(73, 111, 58))
elif kind == "river":
draw.arc((left + 8, top + 20, left + 90, top + 78), 190, 350, fill=(111, 165, 176), width=4)
return image.resize(size, Image.Resampling.LANCZOS)
def load_before_image(path: Path, terrain: list[list[str]], target_size: tuple[int, int]) -> Image.Image:
if path.exists() and path.suffix.lower() != ".svg":
return Image.open(path).convert("RGB")
return make_flat_legacy_preview(terrain, target_size)
def make_comparison(before: Image.Image, after: Image.Image, out_path: Path) -> None: def make_comparison(before: Image.Image, after: Image.Image, out_path: Path) -> None:
preview_width = 1400 preview_width = 1400
before_small = before.resize((preview_width, int(before.height * preview_width / before.width)), Image.Resampling.LANCZOS) before_small = before.resize((preview_width, int(before.height * preview_width / before.width)), Image.Resampling.LANCZOS)
@@ -488,43 +600,53 @@ def make_comparison(before: Image.Image, after: Image.Image, out_path: Path) ->
combined.paste(after_small, (padding * 2 + before_small.width, padding + label_height)) combined.paste(after_small, (padding * 2 + before_small.width, padding + label_height))
draw = ImageDraw.Draw(combined) draw = ImageDraw.Draw(combined)
draw.text((padding, padding + 10), "Before", fill=(232, 219, 178)) draw.text((padding, padding + 10), "Before", fill=(232, 219, 178))
draw.text((padding * 2 + before_small.width, padding + 10), "After: hand-painted tactical sample", fill=(232, 219, 178)) draw.text((padding * 2 + before_small.width, padding + 10), "After: hand-painted tactical map", fill=(232, 219, 178))
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)
combined.save(out_path, quality=92) combined.save(out_path, quality=92)
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--out", type=Path, default=FIRST_BATTLE_MAP) parser.add_argument("--map", choices=sorted(MAP_CONFIGS), default="first")
parser.add_argument("--before", type=Path, default=FIRST_BATTLE_MAP) parser.add_argument("--out", type=Path)
parser.add_argument("--comparison", type=Path, default=DOCS_DIR / "first-battle-map-handpaint-before-after.png") parser.add_argument("--before", type=Path)
parser.add_argument("--preview", type=Path, default=DOCS_DIR / "first-battle-map-handpaint-preview.png") parser.add_argument("--comparison", type=Path)
parser.add_argument("--preview", type=Path)
parser.add_argument("--webp-quality", type=int, default=90) parser.add_argument("--webp-quality", type=int, default=90)
args = parser.parse_args() args = parser.parse_args()
before = Image.open(args.before).convert("RGB") config = MAP_CONFIGS[args.map]
after = build_map() out_path = args.out or Path(config["out"])
if before.size != after.size: before_path = args.before or Path(config["before"])
raise RuntimeError(f"Map size changed from {before.size} to {after.size}") comparison_path = args.comparison or (DOCS_DIR / f"{config['slug']}-handpaint-before-after.png")
preview_path = args.preview or (DOCS_DIR / f"{config['slug']}-handpaint-preview.png")
args.out.parent.mkdir(parents=True, exist_ok=True) terrain = parse_battle_terrain(str(config["terrain_export"]))
if args.out.suffix.lower() == ".webp": after = build_map(config)
after.save(args.out, "WEBP", quality=args.webp_quality, method=6) before = load_before_image(before_path, terrain, after.size)
if before.size != after.size:
before = before.resize(after.size, Image.Resampling.LANCZOS)
out_path.parent.mkdir(parents=True, exist_ok=True)
if out_path.suffix.lower() == ".webp":
after.save(out_path, "WEBP", quality=args.webp_quality, method=6)
else: else:
output = after.quantize(colors=224, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE) output = after.quantize(colors=224, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
output.save(args.out, optimize=True, compress_level=9) output.save(out_path, optimize=True, compress_level=9)
make_comparison(before, after, args.comparison) make_comparison(before, after, comparison_path)
args.preview.parent.mkdir(parents=True, exist_ok=True) preview_path.parent.mkdir(parents=True, exist_ok=True)
after.resize((1400, int(after.height * 1400 / after.width)), Image.Resampling.LANCZOS).save(args.preview, quality=92) after.resize((1400, int(after.height * 1400 / after.width)), Image.Resampling.LANCZOS).save(preview_path, quality=92)
diff = ImageChops.difference(before.resize((400, 360)), after.resize((400, 360))).convert("L") diff_size = (400, int(after.height * 400 / after.width))
diff = ImageChops.difference(before.resize(diff_size), after.resize(diff_size)).convert("L")
print( print(
{ {
"out": str(args.out), "map": args.map,
"out": str(out_path),
"size": after.size, "size": after.size,
"comparison": str(args.comparison), "comparison": str(comparison_path),
"preview": str(args.preview), "preview": str(preview_path),
"avg_preview_delta": round(sum(diff.tobytes()) / (400 * 360), 2), "avg_preview_delta": round(sum(diff.tobytes()) / (diff_size[0] * diff_size[1]), 2),
} }
) )

View File

@@ -1,101 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2400 2000" width="2400" height="2000">
<defs>
<filter id="paintedNoise">
<feTurbulence type="fractalNoise" baseFrequency="0.018" numOctaves="4" seed="23" />
<feColorMatrix type="saturate" values="0.42" />
<feBlend mode="multiply" in2="SourceGraphic" />
</filter>
<pattern id="grassGrain" width="48" height="48" patternUnits="userSpaceOnUse">
<rect width="48" height="48" fill="#65784e" />
<path d="M8 31l8-14M29 42l5-17M39 18l5-9M17 9l10 4" stroke="#9cab67" stroke-width="3" opacity="0.24" />
<path d="M4 14h9M22 26h13M32 5h8" stroke="#344f32" stroke-width="2" opacity="0.3" />
</pattern>
<pattern id="earthGrain" width="54" height="54" patternUnits="userSpaceOnUse">
<rect width="54" height="54" fill="#98754b" />
<path d="M5 12c17-9 30 8 44-2M1 37c18-11 35 12 50-1" stroke="#c49a5d" stroke-width="4" opacity="0.26" />
<path d="M13 48l6-11M32 21l10-8M42 44l7-3" stroke="#5d4a33" stroke-width="3" opacity="0.32" />
</pattern>
<pattern id="forestLeaf" width="90" height="76" patternUnits="userSpaceOnUse">
<rect width="90" height="76" fill="#334b2d" />
<circle cx="20" cy="24" r="19" fill="#263b25" />
<circle cx="44" cy="21" r="23" fill="#49683a" />
<circle cx="67" cy="31" r="20" fill="#2c4329" />
<circle cx="37" cy="51" r="19" fill="#5d7b44" />
<path d="M28 19c12 12 31 11 45-4M9 42c18 10 44 9 61-6" stroke="#152619" stroke-width="5" opacity="0.35" />
</pattern>
<linearGradient id="riverGradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#386f8c" />
<stop offset="0.55" stop-color="#24526d" />
<stop offset="1" stop-color="#173a52" />
</linearGradient>
<linearGradient id="cliffGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#665d48" />
<stop offset="1" stop-color="#302c24" />
</linearGradient>
</defs>
<rect width="2400" height="2000" fill="url(#grassGrain)" />
<g filter="url(#paintedNoise)">
<path d="M-90 1180C250 1060 440 1190 710 1090C930 1010 1030 810 1270 760C1530 707 1710 850 1960 760C2150 692 2268 520 2490 536V2090H-90Z" fill="#4d693c" opacity="0.72" />
<path d="M-60 470C270 420 470 510 720 455C1000 394 1160 210 1460 255C1765 301 1950 440 2450 300V-70H-60Z" fill="#778056" opacity="0.65" />
<path d="M-120 1560C170 1430 360 1370 640 1220C860 1102 1050 915 1225 790C1440 636 1652 594 1905 466C2105 365 2266 245 2520 150" fill="none" stroke="#a17a47" stroke-width="220" stroke-linecap="round" stroke-linejoin="round" opacity="0.98" />
<path d="M-120 1560C170 1430 360 1370 640 1220C860 1102 1050 915 1225 790C1440 636 1652 594 1905 466C2105 365 2266 245 2520 150" fill="none" stroke="url(#earthGrain)" stroke-width="154" stroke-linecap="round" stroke-linejoin="round" opacity="0.95" />
<path d="M300 2050C390 1770 610 1530 860 1320C1110 1110 1165 960 1200 750C1240 515 1350 285 1540-90" fill="none" stroke="#9a7447" stroke-width="178" stroke-linecap="round" opacity="0.95" />
<path d="M300 2050C390 1770 610 1530 860 1320C1110 1110 1165 960 1200 750C1240 515 1350 285 1540-90" fill="none" stroke="url(#earthGrain)" stroke-width="126" stroke-linecap="round" opacity="0.9" />
<path d="M1420-80C1330 210 1295 435 1370 630C1450 840 1640 965 1688 1180C1745 1435 1600 1670 1490 2100" fill="none" stroke="#143347" stroke-width="250" stroke-linecap="round" opacity="0.95" />
<path d="M1420-80C1330 210 1295 435 1370 630C1450 840 1640 965 1688 1180C1745 1435 1600 1670 1490 2100" fill="none" stroke="url(#riverGradient)" stroke-width="180" stroke-linecap="round" opacity="0.98" />
<path d="M1195 930C1280 870 1410 860 1515 905C1575 930 1635 920 1705 875" fill="none" stroke="#80613e" stroke-width="118" stroke-linecap="round" stroke-linejoin="round" opacity="0.96" />
<path d="M1195 930C1280 870 1410 860 1515 905C1575 930 1635 920 1705 875" fill="none" stroke="url(#earthGrain)" stroke-width="78" stroke-linecap="round" stroke-linejoin="round" opacity="0.98" />
<path d="M1240 908C1348 888 1452 890 1600 906" fill="none" stroke="#d4ad6a" stroke-width="8" stroke-linecap="round" opacity="0.38" />
<path d="M1355 80c70 66 62 157 22 240M1450 735c115 94 170 194 166 318M1630 1365c35 154 6 288-80 405" fill="none" stroke="#8ac5d5" stroke-width="14" opacity="0.38" />
<path d="M80 60h525c80 80 115 168 104 266c-206 45-421 26-676-38Z" fill="url(#forestLeaf)" opacity="0.95" />
<path d="M760 170h325c118 85 160 186 124 302c-210 55-360 15-518-92Z" fill="url(#forestLeaf)" opacity="0.82" />
<path d="M90 670c280-110 520-65 695 93c-55 185-258 300-618 314c-132-116-165-254-77-407Z" fill="url(#forestLeaf)" opacity="0.9" />
<path d="M1770 90c190-82 380-68 586 36c42 160-18 305-155 430c-235 16-404-86-520-252Z" fill="url(#forestLeaf)" opacity="0.9" />
<path d="M1810 1010c240-90 420-72 600 22v420c-235 68-450 30-635-110c-32-126-24-236 35-332Z" fill="url(#forestLeaf)" opacity="0.88" />
<path d="M620 1560c290-120 545-84 735 70c-30 220-220 334-570 350c-145-94-200-226-165-420Z" fill="url(#forestLeaf)" opacity="0.82" />
<path d="M0 1630c212-116 370-125 545-74c20 164-46 303-198 418H0Z" fill="url(#cliffGradient)" opacity="0.9" />
<path d="M1920 1510c175-35 350 13 520 142v348h-520c-110-138-110-304 0-490Z" fill="url(#cliffGradient)" opacity="0.86" />
<path d="M1620 520c170-86 330-79 480 18c-5 130-83 234-236 312c-168-35-260-143-244-330Z" fill="url(#cliffGradient)" opacity="0.7" />
<g transform="translate(840 1120)">
<rect x="0" y="0" width="220" height="160" rx="12" fill="#5d4630" />
<rect x="26" y="42" width="58" height="78" fill="#8d6740" />
<rect x="112" y="30" width="70" height="92" fill="#8d6740" />
<path d="M14 42l43-42l43 42M100 30l50-38l54 38" fill="#c58b32" stroke="#34251b" stroke-width="9" />
<rect x="130" y="70" width="22" height="52" fill="#2d2219" />
</g>
<g transform="translate(1780 645)">
<rect x="0" y="0" width="260" height="160" rx="14" fill="#67503a" />
<rect x="32" y="46" width="70" height="80" fill="#98704a" />
<rect x="132" y="30" width="82" height="96" fill="#98704a" />
<path d="M14 46l54-42l54 42M118 30l58-42l66 42" fill="#c8923e" stroke="#3a2a1d" stroke-width="9" />
<path d="M0 144h260" stroke="#2e231a" stroke-width="10" />
</g>
<g transform="translate(300 1360)">
<rect x="0" y="0" width="240" height="170" rx="12" fill="#534030" />
<rect x="34" y="44" width="72" height="92" fill="#8a6848" />
<rect x="132" y="50" width="62" height="84" fill="#8a6848" />
<path d="M18 44l53-40l54 40M122 50l42-34l48 34" fill="#be8431" stroke="#302215" stroke-width="9" />
</g>
<g transform="translate(1940 300)">
<rect x="0" y="0" width="220" height="220" fill="#4a4034" />
<rect x="28" y="28" width="164" height="164" fill="#6e604c" />
<rect x="74" y="74" width="72" height="118" fill="#32251c" />
<path d="M-14 0h248M-14 220h248M0-14v248M220-14v248" stroke="#221913" stroke-width="24" />
<path d="M28 28h164v164H28Z" fill="none" stroke="#b49354" stroke-width="10" opacity="0.7" />
</g>
<g opacity="0.38" stroke="#21180f" stroke-width="4">
<path d="M130 1660l115 46l-80 86M2040 1590l140 60l-95 116M1680 640l130 65l-82 92" fill="none" />
<path d="M456 1210c82-40 140-34 210 10M1060 910c110-72 214-80 334-22M2100 730c92-24 176-8 244 46" fill="none" opacity="0.45" />
</g>
</g>
<rect width="2400" height="2000" fill="#080a0c" opacity="0.08" />
</svg>

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

View File

@@ -1,81 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2400 2000" width="2400" height="2000">
<defs>
<filter id="paintTexture">
<feTurbulence type="fractalNoise" baseFrequency="0.016" numOctaves="4" seed="41" />
<feColorMatrix type="saturate" values="0.48" />
<feBlend mode="multiply" in2="SourceGraphic" />
</filter>
<pattern id="field" width="64" height="64" patternUnits="userSpaceOnUse">
<rect width="64" height="64" fill="#64754c" />
<path d="M8 19h18M34 42h21M42 13l12 8M16 53l15-11" stroke="#a5af68" stroke-width="3" opacity="0.26" />
<path d="M4 41l12-16M28 12l7-10M50 58l8-18" stroke="#344b31" stroke-width="2" opacity="0.28" />
</pattern>
<pattern id="earth" width="72" height="72" patternUnits="userSpaceOnUse">
<rect width="72" height="72" fill="#9a7449" />
<path d="M4 16c22-11 42 8 62-3M0 49c24-13 48 11 68-2" stroke="#c99c5d" stroke-width="5" opacity="0.25" />
<path d="M17 62l9-14M42 29l15-10M58 58l7-5" stroke="#5d4932" stroke-width="3" opacity="0.33" />
</pattern>
<pattern id="leaf" width="96" height="82" patternUnits="userSpaceOnUse">
<rect width="96" height="82" fill="#31472c" />
<circle cx="22" cy="27" r="22" fill="#263b24" />
<circle cx="48" cy="23" r="25" fill="#4b6839" />
<circle cx="72" cy="34" r="22" fill="#2b4128" />
<circle cx="41" cy="57" r="21" fill="#5b7a43" />
<path d="M14 45c20 12 50 11 72-5M29 22c15 13 36 12 51-4" stroke="#142516" stroke-width="5" opacity="0.36" />
</pattern>
<linearGradient id="river" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3b7894" />
<stop offset="0.55" stop-color="#24536e" />
<stop offset="1" stop-color="#183b52" />
</linearGradient>
<linearGradient id="cliff" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#71664d" />
<stop offset="1" stop-color="#2f2b24" />
</linearGradient>
</defs>
<rect width="2400" height="2000" fill="url(#field)" />
<g filter="url(#paintTexture)">
<path d="M-80 1560C230 1410 455 1372 710 1210C928 1072 1044 886 1212 736C1412 556 1690 505 1890 340C2050 208 2175 70 2480-18" fill="none" stroke="#a17a47" stroke-width="210" stroke-linecap="round" stroke-linejoin="round" opacity="0.98" />
<path d="M-80 1560C230 1410 455 1372 710 1210C928 1072 1044 886 1212 736C1412 556 1690 505 1890 340C2050 208 2175 70 2480-18" fill="none" stroke="url(#earth)" stroke-width="148" stroke-linecap="round" stroke-linejoin="round" opacity="0.94" />
<path d="M180 2060C360 1750 555 1502 828 1290C1052 1117 1170 940 1192 720C1212 520 1130 310 1225-90" fill="none" stroke="#9c7448" stroke-width="175" stroke-linecap="round" opacity="0.94" />
<path d="M180 2060C360 1750 555 1502 828 1290C1052 1117 1170 940 1192 720C1212 520 1130 310 1225-90" fill="none" stroke="url(#earth)" stroke-width="124" stroke-linecap="round" opacity="0.9" />
<path d="M1010-90C1020 180 990 410 1068 610C1160 848 1370 1000 1455 1215C1560 1484 1500 1710 1400 2100" fill="none" stroke="#15394f" stroke-width="250" stroke-linecap="round" opacity="0.94" />
<path d="M1010-90C1020 180 990 410 1068 610C1160 848 1370 1000 1455 1215C1560 1484 1500 1710 1400 2100" fill="none" stroke="url(#river)" stroke-width="176" stroke-linecap="round" opacity="0.98" />
<path d="M970 120c80 82 78 184 28 288M1182 760c120 82 184 190 190 325M1450 1405c42 156 10 292-83 420" fill="none" stroke="#91d0dc" stroke-width="14" opacity="0.34" />
<path d="M-40 0h520c100 76 142 175 126 294C410 350 207 328-42 276Z" fill="url(#leaf)" opacity="0.93" />
<path d="M650 110h325c132 80 180 188 140 315C922 480 744 430 602 316Z" fill="url(#leaf)" opacity="0.86" />
<path d="M80 610c275-110 506-72 690 88c-44 190-244 318-594 340C34 928 0 772 80 610Z" fill="url(#leaf)" opacity="0.9" />
<path d="M1515 45c240-82 468-42 708 100c34 164-38 310-200 434c-255 20-420-78-548-258Z" fill="url(#leaf)" opacity="0.88" />
<path d="M1715 1010c240-78 445-54 640 70v410c-250 54-468 6-650-150c-28-128-24-232 10-330Z" fill="url(#leaf)" opacity="0.86" />
<path d="M560 1580c286-126 555-96 760 72c-40 210-236 332-590 350c-142-94-200-236-170-422Z" fill="url(#leaf)" opacity="0.82" />
<path d="M1650 405c190-95 370-70 542 60c-18 154-120 278-302 370c-182-44-270-180-240-430Z" fill="url(#cliff)" opacity="0.7" />
<path d="M0 1645c230-120 400-128 574-62c18 166-56 305-224 417H0Z" fill="url(#cliff)" opacity="0.88" />
<path d="M1900 1490c182-36 360 18 520 150v360h-540c-108-146-102-315 20-510Z" fill="url(#cliff)" opacity="0.84" />
<g transform="translate(1500 540)">
<rect x="0" y="0" width="240" height="160" rx="14" fill="#5f4934" />
<rect x="30" y="46" width="68" height="82" fill="#98704a" />
<rect x="128" y="32" width="74" height="96" fill="#98704a" />
<path d="M14 46l52-42l54 42M116 32l52-40l64 40" fill="#c8923e" stroke="#332317" stroke-width="9" />
<path d="M0 144h240" stroke="#2d2218" stroke-width="10" />
</g>
<g transform="translate(1760 150)">
<rect x="0" y="0" width="310" height="210" rx="16" fill="#514538" />
<rect x="28" y="54" width="250" height="126" fill="#725c42" />
<path d="M18 54l70-56l70 56M148 54l70-56l70 56" fill="#8f6d3e" stroke="#2b2118" stroke-width="10" />
<path d="M64 178v-56h38v56M198 178v-70h46v70" fill="#211915" />
<path d="M-4 188h318" stroke="#1e1814" stroke-width="12" />
</g>
<g transform="translate(820 1180)">
<rect x="0" y="0" width="230" height="170" rx="13" fill="#584432" />
<rect x="32" y="48" width="70" height="88" fill="#8a6848" />
<rect x="130" y="52" width="64" height="84" fill="#8a6848" />
<path d="M16 48l52-42l56 42M120 52l42-34l50 34" fill="#be8431" stroke="#302215" stroke-width="9" />
</g>
</g>
<rect width="2400" height="2000" fill="#0b0d12" opacity="0.08" />
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 KiB

View File

@@ -35,7 +35,7 @@ import fourteenthBattleMapUrl from '../../assets/images/battle/fourteenth-battle
import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg'; import fourthBattleMapUrl from '../../assets/images/battle/fourth-battle-map.svg';
import nineteenthBattleMapUrl from '../../assets/images/battle/nineteenth-battle-map.svg'; import nineteenthBattleMapUrl from '../../assets/images/battle/nineteenth-battle-map.svg';
import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg'; import ninthBattleMapUrl from '../../assets/images/battle/ninth-battle-map.svg';
import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.svg'; import secondBattleMapUrl from '../../assets/images/battle/second-battle-map.webp';
import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg'; import seventhBattleMapUrl from '../../assets/images/battle/seventh-battle-map.svg';
import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg'; import seventeenthBattleMapUrl from '../../assets/images/battle/seventeenth-battle-map.svg';
import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg'; import sixteenthBattleMapUrl from '../../assets/images/battle/sixteenth-battle-map.svg';
@@ -52,7 +52,7 @@ import thirtySecondBattleMapUrl from '../../assets/images/battle/thirty-second-b
import thirtySeventhBattleMapUrl from '../../assets/images/battle/thirty-seventh-battle-map.svg'; import thirtySeventhBattleMapUrl from '../../assets/images/battle/thirty-seventh-battle-map.svg';
import thirtySixthBattleMapUrl from '../../assets/images/battle/thirty-sixth-battle-map.svg'; import thirtySixthBattleMapUrl from '../../assets/images/battle/thirty-sixth-battle-map.svg';
import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg'; import thirtyThirdBattleMapUrl from '../../assets/images/battle/thirty-third-battle-map.svg';
import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.svg'; import thirdBattleMapUrl from '../../assets/images/battle/third-battle-map.webp';
import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg'; import twentiethBattleMapUrl from '../../assets/images/battle/twentieth-battle-map.svg';
import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg'; import twentyEighthBattleMapUrl from '../../assets/images/battle/twenty-eighth-battle-map.svg';
import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg'; import twentyFifthBattleMapUrl from '../../assets/images/battle/twenty-fifth-battle-map.svg';