Improve sixty-fourth battle map detail
This commit is contained in:
@@ -25,6 +25,7 @@ COLOSSAL_LARGE_MAP_TILE_SIZE = 136
|
||||
GIGANTIC_LARGE_MAP_TILE_SIZE = 128
|
||||
IMMENSE_LARGE_MAP_TILE_SIZE = 120
|
||||
VAST_LARGE_MAP_TILE_SIZE = 112
|
||||
DESKTOP_RUNTIME_MAP_TILE_SIZE = 64
|
||||
TILE = DEFAULT_TILE_SIZE
|
||||
RNG = random.Random(240704)
|
||||
|
||||
@@ -643,6 +644,8 @@ MAP_CONFIGS = {
|
||||
"before": BATTLE_IMAGE_DIR / "sixty-fourth-battle-map.svg",
|
||||
"seed": 241334,
|
||||
"tile_size": VAST_LARGE_MAP_TILE_SIZE,
|
||||
"output_tile_size": DESKTOP_RUNTIME_MAP_TILE_SIZE,
|
||||
"detail_profile": "weishui-camps",
|
||||
},
|
||||
"sixty-fifth": {
|
||||
"slug": "sixty-fifth-battle-map",
|
||||
@@ -1197,6 +1200,179 @@ def add_route_and_battlefield_details(canvas: Image.Image, terrain: list[list[st
|
||||
canvas.alpha_composite(details.filter(ImageFilter.GaussianBlur(0.15)))
|
||||
|
||||
|
||||
def terrain_at(terrain: list[list[str]], x: int, y: int) -> str | None:
|
||||
if 0 <= y < len(terrain) and 0 <= x < len(terrain[0]):
|
||||
return terrain[y][x]
|
||||
return None
|
||||
|
||||
|
||||
def draw_weishui_beacon(draw: ImageDraw.ImageDraw, x: int, y: int) -> None:
|
||||
cx, cy = center(x, y)
|
||||
base_y = cy + int(TILE * 0.24)
|
||||
tower_top = cy - int(TILE * 0.26)
|
||||
left = cx - int(TILE * 0.18)
|
||||
right = cx + int(TILE * 0.18)
|
||||
draw.line((left, base_y, cx - 6, tower_top), fill=(67, 45, 28, 150), width=max(4, TILE // 28))
|
||||
draw.line((right, base_y, cx + 6, tower_top), fill=(67, 45, 28, 150), width=max(4, TILE // 28))
|
||||
draw.line((left - 8, cy, right + 8, cy - 5), fill=(127, 86, 48, 142), width=max(4, TILE // 32))
|
||||
draw.rectangle((cx - 18, tower_top - 6, cx + 18, tower_top + 14), fill=(88, 55, 32, 172))
|
||||
flame = [
|
||||
(cx - 16, tower_top - 6),
|
||||
(cx - 4, tower_top - 40),
|
||||
(cx + 6, tower_top - 14),
|
||||
(cx + 18, tower_top - 46),
|
||||
(cx + 15, tower_top - 3),
|
||||
]
|
||||
draw.polygon([(px + 5, py + 7) for px, py in flame], fill=(64, 38, 23, 70))
|
||||
draw.polygon(flame, fill=(218, 101, 34, 176))
|
||||
draw.polygon([(cx - 6, tower_top - 5), (cx + 2, tower_top - 25), (cx + 8, tower_top - 5)], fill=(245, 181, 72, 154))
|
||||
for offset in (-24, 0, 22):
|
||||
draw.arc(
|
||||
(cx + offset - 20, tower_top - 76, cx + offset + 44, tower_top - 18),
|
||||
210,
|
||||
340,
|
||||
fill=(72, 68, 54, 50),
|
||||
width=max(2, TILE // 50),
|
||||
)
|
||||
|
||||
|
||||
def draw_weishui_supply_marker(draw: ImageDraw.ImageDraw, x: int, y: int) -> None:
|
||||
cx, cy = center(x, y)
|
||||
tent_w = int(TILE * 0.5)
|
||||
tent_h = int(TILE * 0.42)
|
||||
base_y = cy + int(TILE * 0.2)
|
||||
draw.polygon(
|
||||
[(cx - tent_w, base_y), (cx, cy - tent_h), (cx + tent_w, base_y)],
|
||||
fill=(112, 82, 43, 142),
|
||||
)
|
||||
draw.polygon(
|
||||
[(cx - int(tent_w * 0.72), base_y - 2), (cx, cy - int(tent_h * 0.72)), (cx + int(tent_w * 0.16), base_y - 2)],
|
||||
fill=(189, 147, 73, 132),
|
||||
)
|
||||
draw.line((cx - tent_w, base_y, cx + tent_w, base_y), fill=(58, 39, 25, 126), width=max(3, TILE // 34))
|
||||
for offset in (-int(TILE * 0.34), int(TILE * 0.38)):
|
||||
px = cx + offset
|
||||
py = cy + int(TILE * 0.33)
|
||||
draw.rectangle((px - int(TILE * 0.14), py - int(TILE * 0.1), px + int(TILE * 0.14), py + int(TILE * 0.1)), fill=(139, 90, 44, 132))
|
||||
draw.line((px - int(TILE * 0.16), py - int(TILE * 0.11), px + int(TILE * 0.16), py + int(TILE * 0.1)), fill=(216, 174, 91, 82), width=2)
|
||||
pole_x = cx + int(TILE * 0.48)
|
||||
pole_y = cy - int(TILE * 0.06)
|
||||
draw.line((pole_x, pole_y + int(TILE * 0.44), pole_x, pole_y - int(TILE * 0.38)), fill=(63, 42, 27, 150), width=max(3, TILE // 40))
|
||||
draw.polygon(
|
||||
[(pole_x + 3, pole_y - int(TILE * 0.38)), (pole_x + int(TILE * 0.38), pole_y - int(TILE * 0.26)), (pole_x + 3, pole_y - int(TILE * 0.12))],
|
||||
fill=(188, 59, 44, 136),
|
||||
)
|
||||
|
||||
|
||||
def add_weishui_camps_details(canvas: Image.Image, terrain: list[list[str]]) -> None:
|
||||
details = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(details)
|
||||
sharp = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
||||
sharp_draw = ImageDraw.Draw(sharp)
|
||||
|
||||
for y, row in enumerate(terrain):
|
||||
for x, kind in enumerate(row):
|
||||
left, top, right, bottom = tile_box(x, y)
|
||||
cx, cy = center(x, y)
|
||||
|
||||
if kind == "river":
|
||||
for _ in range(5):
|
||||
sx = RNG.randint(left + 12, max(left + 13, right - 70))
|
||||
sy = RNG.randint(top + 16, bottom - 16)
|
||||
span = RNG.randint(max(34, TILE // 3), max(42, TILE))
|
||||
draw.arc(
|
||||
(sx, sy - RNG.randint(8, 18), sx + span, sy + RNG.randint(10, 24)),
|
||||
190,
|
||||
352,
|
||||
fill=(*jitter(PALETTE["water_light"], 10), RNG.randint(38, 82)),
|
||||
width=RNG.choice([2, 2, 3]),
|
||||
)
|
||||
if terrain_at(terrain, x, y - 1) != "river":
|
||||
bank_y = top + RNG.randint(6, 18)
|
||||
for _ in range(4):
|
||||
px = RNG.randint(left + 8, right - 8)
|
||||
reed = RNG.choice([(87, 111, 60, 80), (151, 135, 78, 72), (42, 83, 51, 72)])
|
||||
draw.line((px, bank_y + RNG.randint(-2, 8), px + RNG.randint(-8, 9), bank_y - RNG.randint(8, 22)), fill=reed, width=2)
|
||||
if terrain_at(terrain, x, y + 1) != "river":
|
||||
bank_y = bottom - RNG.randint(7, 18)
|
||||
for _ in range(4):
|
||||
px = RNG.randint(left + 8, right - 8)
|
||||
reed = RNG.choice([(89, 112, 61, 82), (154, 134, 78, 70), (40, 82, 50, 70)])
|
||||
draw.line((px, bank_y + RNG.randint(-6, 2), px + RNG.randint(-9, 8), bank_y - RNG.randint(10, 24)), fill=reed, width=2)
|
||||
|
||||
if kind == "forest" and (x >= 92 or y >= 65 or RNG.random() < 0.22):
|
||||
for _ in range(3):
|
||||
bx = cx + RNG.randint(-54, 56)
|
||||
by = cy + RNG.randint(-48, 48)
|
||||
rx = RNG.randint(18, 42)
|
||||
ry = RNG.randint(12, 30)
|
||||
draw.polygon(organic_ellipse_points(bx, by, rx, ry, 14, 0.34), fill=(13, 42, 27, RNG.randint(44, 76)))
|
||||
for _ in range(5):
|
||||
sx = RNG.randint(left + 18, right - 18)
|
||||
sy = RNG.randint(top + 24, bottom - 22)
|
||||
draw.arc(
|
||||
(sx - 30, sy - 18, sx + 46, sy + 22),
|
||||
RNG.randint(190, 230),
|
||||
RNG.randint(300, 350),
|
||||
fill=(*jitter(PALETTE["forest_light"], 12), RNG.randint(46, 78)),
|
||||
width=RNG.choice([2, 3]),
|
||||
)
|
||||
if RNG.random() < 0.42:
|
||||
draw.line(
|
||||
(cx + RNG.randint(-24, 24), cy + RNG.randint(10, 42), cx + RNG.randint(-20, 28), cy + RNG.randint(38, 62)),
|
||||
fill=(66, 44, 26, 92),
|
||||
width=RNG.randint(4, 7),
|
||||
)
|
||||
|
||||
if kind == "road" and (y >= 70 or x >= 82) and RNG.random() < 0.55:
|
||||
for _ in range(2):
|
||||
py = cy + RNG.randint(-28, 28)
|
||||
draw.line(
|
||||
(left + RNG.randint(8, 28), py, right - RNG.randint(8, 28), py + RNG.randint(-8, 8)),
|
||||
fill=(103, 73, 42, RNG.randint(34, 58)),
|
||||
width=RNG.choice([2, 3]),
|
||||
)
|
||||
|
||||
if kind == "camp" and RNG.random() < 0.72:
|
||||
cart_x = cx + RNG.randint(-34, 30)
|
||||
cart_y = cy + RNG.randint(-14, 34)
|
||||
draw.rectangle((cart_x - 26, cart_y - 13, cart_x + 24, cart_y + 14), fill=(111, 73, 40, 118))
|
||||
draw.line((cart_x - 32, cart_y - 17, cart_x + 28, cart_y + 18), fill=(194, 156, 91, 72), width=3)
|
||||
for wx in (cart_x - 22, cart_x + 20):
|
||||
draw.ellipse((wx - 8, cart_y + 10, wx + 8, cart_y + 26), outline=(47, 32, 22, 118), width=3)
|
||||
for _ in range(3):
|
||||
px = RNG.randint(left + 24, right - 26)
|
||||
py = RNG.randint(top + 28, bottom - 24)
|
||||
draw.rectangle((px - 9, py - 7, px + 11, py + 8), fill=(146, 96, 48, RNG.randint(90, 132)))
|
||||
|
||||
if kind in {"camp", "fort"} and (x + y) % 9 == 0:
|
||||
pole_x = cx + RNG.randint(-44, 44)
|
||||
pole_y = cy + RNG.randint(-40, 30)
|
||||
sharp_draw.line((pole_x, pole_y + 36, pole_x, pole_y - 38), fill=(80, 52, 31, 160), width=max(3, TILE // 38))
|
||||
sharp_draw.polygon(
|
||||
[(pole_x + 4, pole_y - 37), (pole_x + 44, pole_y - 25), (pole_x + 4, pole_y - 13)],
|
||||
fill=(190, 150, 49, 158),
|
||||
)
|
||||
|
||||
for start, end, width in [
|
||||
((96, 34), (118, 34), 126),
|
||||
((99, 46), (124, 46), 118),
|
||||
((63, 74), (80, 78), 112),
|
||||
((83, 68), (101, 70), 104),
|
||||
]:
|
||||
paint_path_segment(draw, center(*start), center(*end), width, (204, 170, 103), 54, 15)
|
||||
paint_path_segment(draw, center(*start), center(*end), max(36, width // 2), (229, 196, 133), 34, 10)
|
||||
|
||||
for marker in [(59, 62), (72, 63), (101, 34), (115, 35), (70, 58)]:
|
||||
draw_weishui_supply_marker(sharp_draw, *marker)
|
||||
|
||||
for beacon in [(118, 72), (121, 54), (124, 33), (120, 45), (101, 61)]:
|
||||
draw_weishui_beacon(sharp_draw, *beacon)
|
||||
|
||||
canvas.alpha_composite(details.filter(ImageFilter.GaussianBlur(0.18)))
|
||||
canvas.alpha_composite(sharp.filter(ImageFilter.GaussianBlur(0.04)))
|
||||
|
||||
|
||||
def add_subtle_grid_and_grade(canvas: Image.Image, terrain: list[list[str]]) -> Image.Image:
|
||||
width, height = canvas.size
|
||||
overlay = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
|
||||
@@ -1237,9 +1413,21 @@ def build_map(config: dict[str, object]) -> Image.Image:
|
||||
paint_roads(canvas, terrain)
|
||||
paint_tiles(canvas, terrain)
|
||||
add_route_and_battlefield_details(canvas, terrain, units)
|
||||
if config.get("detail_profile") == "weishui-camps":
|
||||
add_weishui_camps_details(canvas, terrain)
|
||||
return add_subtle_grid_and_grade(canvas, terrain)
|
||||
|
||||
|
||||
def resize_for_runtime_if_needed(after: Image.Image, terrain: list[list[str]], config: dict[str, object]) -> Image.Image:
|
||||
output_tile_size = config.get("output_tile_size")
|
||||
if output_tile_size is None or int(output_tile_size) == TILE:
|
||||
return after
|
||||
|
||||
width = len(terrain[0]) * int(output_tile_size)
|
||||
height = len(terrain) * int(output_tile_size)
|
||||
return after.resize((width, height), Image.Resampling.LANCZOS)
|
||||
|
||||
|
||||
def make_flat_legacy_preview(terrain: list[list[str]], size: tuple[int, int]) -> Image.Image:
|
||||
colors = {
|
||||
"plain": (97, 116, 75),
|
||||
@@ -1301,6 +1489,7 @@ def main() -> None:
|
||||
parser.add_argument("--preview", type=Path)
|
||||
parser.add_argument("--tile-size", type=int)
|
||||
parser.add_argument("--webp-quality", type=int, default=90)
|
||||
parser.add_argument("--webp-method", type=int, default=6)
|
||||
args = parser.parse_args()
|
||||
|
||||
config = dict(MAP_CONFIGS[args.map])
|
||||
@@ -1315,28 +1504,30 @@ def main() -> None:
|
||||
|
||||
terrain = parse_battle_terrain(str(config["terrain_export"]))
|
||||
after = build_map(config)
|
||||
before = load_before_image(before_path, terrain, after.size)
|
||||
if before.size != after.size:
|
||||
before = before.resize(after.size, Image.Resampling.LANCZOS)
|
||||
runtime_after = resize_for_runtime_if_needed(after, terrain, config)
|
||||
before = load_before_image(before_path, terrain, runtime_after.size)
|
||||
if before.size != runtime_after.size:
|
||||
before = before.resize(runtime_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)
|
||||
runtime_after.save(out_path, "WEBP", quality=args.webp_quality, method=args.webp_method)
|
||||
else:
|
||||
output = after.quantize(colors=224, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
|
||||
output = runtime_after.quantize(colors=224, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
|
||||
output.save(out_path, optimize=True, compress_level=9)
|
||||
make_comparison(before, after, comparison_path)
|
||||
make_comparison(before, runtime_after, comparison_path)
|
||||
preview_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
after.resize((1400, int(after.height * 1400 / after.width)), Image.Resampling.LANCZOS).save(preview_path, quality=92)
|
||||
runtime_after.resize((1400, int(runtime_after.height * 1400 / runtime_after.width)), Image.Resampling.LANCZOS).save(preview_path, quality=92)
|
||||
|
||||
diff_size = (400, int(after.height * 400 / after.width))
|
||||
diff = ImageChops.difference(before.resize(diff_size), after.resize(diff_size)).convert("L")
|
||||
diff_size = (400, int(runtime_after.height * 400 / runtime_after.width))
|
||||
diff = ImageChops.difference(before.resize(diff_size), runtime_after.resize(diff_size)).convert("L")
|
||||
print(
|
||||
{
|
||||
"map": args.map,
|
||||
"tile_size": TILE,
|
||||
"output_tile_size": config.get("output_tile_size", TILE),
|
||||
"out": str(out_path),
|
||||
"size": after.size,
|
||||
"size": runtime_after.size,
|
||||
"comparison": str(comparison_path),
|
||||
"preview": str(preview_path),
|
||||
"avg_preview_delta": round(sum(diff.tobytes()) / (diff_size[0] * diff_size[1]), 2),
|
||||
|
||||
Reference in New Issue
Block a user