diff --git a/docs/visual-asset-quality-audit.md b/docs/visual-asset-quality-audit.md index d4f975b..c49b3be 100644 --- a/docs/visual-asset-quality-audit.md +++ b/docs/visual-asset-quality-audit.md @@ -7,7 +7,7 @@ Generated by `scripts/audit-visual-asset-quality.mjs`. - Target desktop viewport: 1280x720 - Battle map assets: 66 - Flagged battle map review rows: 43 -- Flagged battle map rows with repaint reports: 2 +- Flagged battle map rows with repaint reports: 3 - Battle map source pixel density range: 64.0-224.0 px/tile - Story source backgrounds: 136 - Story page background references: 466 @@ -23,7 +23,7 @@ Generated by `scripts/audit-visual-asset-quality.mjs`. ## Recommended Next Batch -- Battle map candidate: `sixty-fifth-battle-weishui-northbank` / `battle-map-sixty-fifth` +- Battle map candidate: `sixty-third-battle-lucheng-pursuit` / `battle-map-sixty-third` - Battle reason: low tile pixel density - Story background candidate: `src/assets/images/story/25-yizhou-luo-road.png` - Story reason: high-repeat story background @@ -33,7 +33,6 @@ Generated by `scripts/audit-visual-asset-quality.mjs`. | score | battle id | map key | grid | image | px/tile | bytes/px | concerns | | ---: | --- | --- | ---: | ---: | ---: | ---: | --- | -| 6 | `sixty-fifth-battle-weishui-northbank` | `battle-map-sixty-fifth` | 130x112 | 14560x12544 | 112.0 | 0.1059 | low tile pixel density | | 6 | `sixty-third-battle-lucheng-pursuit` | `battle-map-sixty-third` | 126x108 | 14112x12096 | 112.0 | 0.1200 | low tile pixel density | | 6 | `sixtieth-battle-wudu-yinping` | `battle-map-sixtieth` | 120x102 | 14400x12240 | 120.0 | 0.1116 | low tile pixel density | | 6 | `sixty-first-battle-hanzhong-rain-defense` | `battle-map-sixty-first` | 122x104 | 14640x12480 | 120.0 | 0.1162 | low tile pixel density | @@ -45,6 +44,7 @@ Generated by `scripts/audit-visual-asset-quality.mjs`. | 4 | `fifty-fifth-battle-northern-qishan-road` | `battle-map-fifty-fifth` | 110x92 | 14080x11776 | 128.0 | 0.1146 | reduced tile pixel density | | 4 | `fifty-third-battle-meng-huo-sixth-capture` | `battle-map-fifty-third` | 106x88 | 14416x11968 | 136.0 | 0.1155 | reduced tile pixel density | | 4 | `fifty-fourth-battle-meng-huo-final-capture` | `battle-map-fifty-fourth` | 108x90 | 14688x12240 | 136.0 | 0.1168 | reduced tile pixel density | +| 4 | `fifth-battle-sishui-vanguard` | `battle-map-fifth` | 24x20 | 5376x4480 | 224.0 | 0.0270 | very low WebP byte density | ## Top Story Background Review Queue @@ -103,7 +103,7 @@ Generated by `scripts/audit-visual-asset-quality.mjs`. | `sixteenth-battle-liu-biao-refuge` | `battle-map-sixteenth` | 34x26 | 7616x5824 | 224.0 | 1.49 | low WebP byte density | | `sixth-battle-jieqiao-relief` | `battle-map-sixth` | 24x20 | 5376x4480 | 224.0 | 0.64 | very low WebP byte density | | `sixtieth-battle-wudu-yinping` | `battle-map-sixtieth` | 120x102 | 14400x12240 | 120.0 | 18.76 | low tile pixel density | -| `sixty-fifth-battle-weishui-northbank` | `battle-map-sixty-fifth` | 130x112 | 14560x12544 | 112.0 | 18.44 | low tile pixel density | +| `sixty-fifth-battle-weishui-northbank` | `battle-map-sixty-fifth` | 130x112 | 8320x7168 | 64.0 | 8.00 | low tile pixel density
repaint report exists | | `sixty-first-battle-hanzhong-rain-defense` | `battle-map-sixty-first` | 122x104 | 14640x12480 | 120.0 | 20.24 | low tile pixel density | | `sixty-fourth-battle-weishui-camps` | `battle-map-sixty-fourth` | 128x110 | 8192x7040 | 64.0 | 7.51 | low tile pixel density
repaint report exists | | `sixty-second-battle-qishan-renewed-offensive` | `battle-map-sixty-second` | 124x106 | 14880x12720 | 120.0 | 22.05 | low tile pixel density | diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-before-after.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-before-after.png new file mode 100644 index 0000000..06d43ff Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-before-after.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-delay-line.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-delay-line.png new file mode 100644 index 0000000..8b9bf8a Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-delay-line.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-deploy.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-deploy.png new file mode 100644 index 0000000..9e07aac Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-deploy.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-detail-crops.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-detail-crops.png new file mode 100644 index 0000000..1627144 Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-detail-crops.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-north-gate.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-north-gate.png new file mode 100644 index 0000000..ef27217 Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-north-gate.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-report.md b/docs/visual-asset-sixty-fifth-map-repaint-v1-report.md new file mode 100644 index 0000000..4a5ce81 --- /dev/null +++ b/docs/visual-asset-sixty-fifth-map-repaint-v1-report.md @@ -0,0 +1,41 @@ +# Sixty-Fifth Battle Map Repaint v1 + +Date: 2026-07-10 + +Target: `sixty-fifth-battle-weishui-northbank` / `battle-map-sixty-fifth` + +## Changes + +- Added the `weishui-northbank` detail profile to `scripts/generate-handpaint-map-sample.py`. +- Regenerated `src/assets/images/battle/sixty-fifth-battle-map.webp` from the scenario terrain with desktop runtime output at 64 px/tile. +- Added northbank battle details: stronger river flow strokes, reeded banks, ferry planks, pursuit road ruts, forward command tents, supply crates, delay-line barricades, beacon towers, and camp flags. +- Kept the original 130x112 terrain grid and aspect ratio. + +## Asset Result + +- Previous image: 14,560 x 12,544, 19,334,520 bytes. +- New image: 8,320 x 7,168, 8,388,418 bytes. +- Source render density remains 112 px/tile before downsampling; runtime asset density is 64 px/tile for desktop browser performance. + +## Evidence + +- `docs/visual-asset-sixty-fifth-map-repaint-v1-before-after.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-source-preview.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-detail-crops.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-deploy.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-start.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-start-clear.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-river-ferry.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-village-beacon.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-delay-line.png` +- `docs/visual-asset-sixty-fifth-map-repaint-v1-north-gate.png` + +## Validation + +- `python -m py_compile scripts\generate-handpaint-map-sample.py` +- `pnpm run verify:static-data` +- `pnpm run build` +- `pnpm run audit:visual-assets` +- `git diff --check` + +Browser QA reached the 65th battle deployment screen and active battle views around the start area, river ferry, northbank village beacon line, and Sima Yi delay line. The in-app browser log buffer still contained an older 64th-map Phaser image-processing error timestamped before this pass; the 65th battle map rendered successfully in all captured views. diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-river-ferry.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-river-ferry.png new file mode 100644 index 0000000..9d372c2 Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-river-ferry.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-source-preview.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-source-preview.png new file mode 100644 index 0000000..2f262e5 Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-source-preview.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-start-clear.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-start-clear.png new file mode 100644 index 0000000..b761653 Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-start-clear.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-start.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-start.png new file mode 100644 index 0000000..b4c319d Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-start.png differ diff --git a/docs/visual-asset-sixty-fifth-map-repaint-v1-village-beacon.png b/docs/visual-asset-sixty-fifth-map-repaint-v1-village-beacon.png new file mode 100644 index 0000000..e1bd78e Binary files /dev/null and b/docs/visual-asset-sixty-fifth-map-repaint-v1-village-beacon.png differ diff --git a/scripts/generate-handpaint-map-sample.py b/scripts/generate-handpaint-map-sample.py index 3ba16b6..c7265cc 100644 --- a/scripts/generate-handpaint-map-sample.py +++ b/scripts/generate-handpaint-map-sample.py @@ -658,6 +658,8 @@ MAP_CONFIGS = { "before": BATTLE_IMAGE_DIR / "sixty-fifth-battle-map.svg", "seed": 241344, "tile_size": VAST_LARGE_MAP_TILE_SIZE, + "output_tile_size": DESKTOP_RUNTIME_MAP_TILE_SIZE, + "detail_profile": "weishui-northbank", }, "sixty-sixth": { "slug": "sixty-sixth-battle-map", @@ -1377,6 +1379,205 @@ def add_weishui_camps_details(canvas: Image.Image, terrain: list[list[str]]) -> canvas.alpha_composite(sharp.filter(ImageFilter.GaussianBlur(0.04))) +def draw_northbank_ferry(draw: ImageDraw.ImageDraw, x: int, y: int) -> None: + cx, cy = center(x, y) + plank_w = int(TILE * 0.58) + plank_h = int(TILE * 0.12) + for row, offset_y in enumerate((-int(TILE * 0.18), 0, int(TILE * 0.18))): + left = cx - plank_w + row * 5 + top = cy + offset_y - plank_h + right = cx + plank_w + row * 5 + bottom = cy + offset_y + plank_h + draw.rounded_rectangle( + (left, top, right, bottom), + radius=max(3, TILE // 36), + fill=(116, 78, 43, 132), + ) + draw.line((left + 8, top + 4, right - 10, bottom - 4), fill=(218, 176, 99, 78), width=2) + for offset in (-int(TILE * 0.45), int(TILE * 0.46)): + draw.line( + (cx + offset, cy - int(TILE * 0.38), cx + offset + int(TILE * 0.08), cy + int(TILE * 0.4)), + fill=(65, 45, 32, 138), + width=max(3, TILE // 34), + ) + draw.arc( + (cx - int(TILE * 0.7), cy - int(TILE * 0.34), cx + int(TILE * 0.7), cy + int(TILE * 0.38)), + 195, + 345, + fill=(189, 212, 205, 64), + width=max(3, TILE // 34), + ) + + +def draw_northbank_barricade(draw: ImageDraw.ImageDraw, x: int, y: int) -> None: + cx, cy = center(x, y) + for offset in (-int(TILE * 0.36), -int(TILE * 0.12), int(TILE * 0.12), int(TILE * 0.36)): + draw.line( + (cx + offset - int(TILE * 0.16), cy + int(TILE * 0.22), cx + offset + int(TILE * 0.14), cy - int(TILE * 0.24)), + fill=(66, 43, 27, 146), + width=max(4, TILE // 30), + ) + draw.line( + (cx + offset - int(TILE * 0.14), cy - int(TILE * 0.2), cx + offset + int(TILE * 0.16), cy + int(TILE * 0.2)), + fill=(91, 61, 36, 130), + width=max(3, TILE // 36), + ) + draw.line( + (cx - int(TILE * 0.58), cy + int(TILE * 0.12), cx + int(TILE * 0.58), cy + int(TILE * 0.08)), + fill=(39, 31, 23, 88), + width=max(3, TILE // 32), + ) + + +def draw_northbank_command_post(draw: ImageDraw.ImageDraw, x: int, y: int) -> None: + cx, cy = center(x, y) + base_y = cy + int(TILE * 0.24) + tent_w = int(TILE * 0.58) + tent_h = int(TILE * 0.42) + draw.polygon( + [(cx - tent_w, base_y), (cx - int(TILE * 0.04), cy - tent_h), (cx + tent_w, base_y)], + fill=(116, 79, 42, 152), + ) + draw.polygon( + [(cx - int(tent_w * 0.62), base_y - 3), (cx - int(TILE * 0.04), cy - int(tent_h * 0.7)), (cx + int(tent_w * 0.2), base_y - 3)], + fill=(204, 158, 78, 132), + ) + draw.line((cx - tent_w, base_y, cx + tent_w, base_y), fill=(56, 38, 25, 138), width=max(3, TILE // 34)) + for crate_x in (cx - int(TILE * 0.44), cx + int(TILE * 0.42)): + crate_y = cy + int(TILE * 0.38) + draw.rectangle( + (crate_x - int(TILE * 0.14), crate_y - int(TILE * 0.1), crate_x + int(TILE * 0.14), crate_y + int(TILE * 0.1)), + fill=(132, 85, 42, 132), + ) + draw.line( + (crate_x - int(TILE * 0.14), crate_y - int(TILE * 0.09), crate_x + int(TILE * 0.14), crate_y + int(TILE * 0.09)), + fill=(217, 172, 91, 78), + width=2, + ) + pole_x = cx + int(TILE * 0.52) + pole_y = cy - int(TILE * 0.04) + draw.line((pole_x, pole_y + int(TILE * 0.42), pole_x, pole_y - int(TILE * 0.4)), fill=(65, 43, 27, 154), width=max(3, TILE // 40)) + draw.polygon( + [(pole_x + 4, pole_y - int(TILE * 0.39)), (pole_x + int(TILE * 0.4), pole_y - int(TILE * 0.27)), (pole_x + 4, pole_y - int(TILE * 0.13))], + fill=(176, 55, 42, 144), + ) + + +def add_weishui_northbank_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 - 72)) + sy = RNG.randint(top + 14, bottom - 14) + span = RNG.randint(max(38, TILE // 3), max(48, TILE)) + draw.arc( + (sx, sy - RNG.randint(8, 20), sx + span, sy + RNG.randint(10, 24)), + 190, + 352, + fill=(*jitter(PALETTE["water_light"], 12), RNG.randint(42, 86)), + width=RNG.choice([2, 2, 3]), + ) + if terrain_at(terrain, x, y - 1) != "river" or terrain_at(terrain, x, y + 1) != "river": + bank_y = top + RNG.randint(8, TILE - 8) + for _ in range(5): + px = RNG.randint(left + 8, right - 8) + reed = RNG.choice([(88, 111, 58, 82), (157, 134, 75, 72), (42, 82, 51, 72)]) + draw.line((px, bank_y + RNG.randint(-5, 6), px + RNG.randint(-9, 8), bank_y - RNG.randint(10, 26)), fill=reed, width=2) + + if kind == "road" and (y >= 47 or x >= 76) and RNG.random() < 0.64: + for _ in range(2): + py = cy + RNG.randint(-30, 30) + draw.line( + (left + RNG.randint(10, 26), py, right - RNG.randint(10, 26), py + RNG.randint(-7, 7)), + fill=(93, 65, 39, RNG.randint(42, 68)), + width=RNG.choice([2, 3]), + ) + if RNG.random() < 0.3: + for step in range(3): + px = left + 22 + step * int(TILE * 0.24) + RNG.randint(-6, 6) + py = cy + RNG.randint(-18, 22) + draw.ellipse((px - 4, py - 2, px + 5, py + 3), fill=(57, 43, 31, 54)) + + if kind == "forest" and (y >= 60 or x >= 88): + for _ in range(3): + bx = cx + RNG.randint(-54, 56) + by = cy + RNG.randint(-46, 46) + draw.polygon( + organic_ellipse_points(bx, by, RNG.randint(18, 42), RNG.randint(12, 30), 14, 0.34), + fill=(13, 43, 28, RNG.randint(42, 74)), + ) + if RNG.random() < 0.4: + draw.line( + (cx + RNG.randint(-22, 22), cy + RNG.randint(8, 38), cx + RNG.randint(-20, 24), cy + RNG.randint(34, 58)), + fill=(59, 39, 24, 88), + width=RNG.randint(4, 7), + ) + + if kind in {"hill", "cliff"} and RNG.random() < 0.55: + for _ in range(2): + px = RNG.randint(left + 12, right - 12) + py = RNG.randint(top + 12, bottom - 12) + draw.line( + (px - RNG.randint(12, 38), py + RNG.randint(-5, 8), px + RNG.randint(22, 54), py + RNG.randint(-9, 9)), + fill=(72, 62, 46, RNG.randint(30, 56)), + width=RNG.choice([2, 2, 3]), + ) + + if kind in {"camp", "fort"} and RNG.random() < 0.52: + for _ in range(2): + px = RNG.randint(left + 24, right - 26) + py = RNG.randint(top + 26, bottom - 24) + draw.rectangle((px - 10, py - 7, px + 12, py + 8), fill=(140, 91, 45, RNG.randint(86, 126))) + if (x + y) % 8 == 0: + pole_x = cx + RNG.randint(-38, 38) + pole_y = cy + RNG.randint(-34, 24) + sharp_draw.line((pole_x, pole_y + 36, pole_x, pole_y - 38), fill=(80, 52, 31, 156), width=max(3, TILE // 38)) + sharp_draw.polygon( + [(pole_x + 4, pole_y - 37), (pole_x + 42, pole_y - 25), (pole_x + 4, pole_y - 13)], + fill=(190, 150, 49, 150), + ) + + if kind == "village" and RNG.random() < 0.48: + lx = cx + RNG.randint(-36, 36) + ly = cy + RNG.randint(-24, 28) + draw.ellipse((lx - 9, ly - 9, lx + 9, ly + 9), fill=(229, 168, 76, 84)) + draw.arc((lx - 18, ly - 42, lx + 28, ly + 10), 205, 330, fill=(84, 74, 55, 42), width=2) + + for start, end, width in [ + ((20, 95), (43, 92), 116), + ((43, 92), (63, 84), 112), + ((63, 84), (90, 72), 106), + ((90, 72), (112, 66), 98), + ((104, 58), (122, 47), 92), + ]: + paint_path_segment(draw, center(*start), center(*end), width, (198, 162, 94), 48, 14) + paint_path_segment(draw, center(*start), center(*end), max(34, width // 2), (230, 194, 126), 28, 9) + + for ferry in [(28, 39), (58, 39), (88, 40), (105, 48)]: + draw_northbank_ferry(sharp_draw, *ferry) + + for post in [(31, 96), (63, 83), (82, 55), (99, 69), (113, 61)]: + draw_northbank_command_post(sharp_draw, *post) + + for barrier in [(91, 74), (103, 63), (113, 52), (121, 47), (124, 38), (126, 28)]: + draw_northbank_barricade(sharp_draw, *barrier) + + for beacon in [(97, 72), (112, 66), (121, 55), (124, 42), (126, 31)]: + draw_weishui_beacon(sharp_draw, *beacon) + + canvas.alpha_composite(details.filter(ImageFilter.GaussianBlur(0.16))) + canvas.alpha_composite(sharp.filter(ImageFilter.GaussianBlur(0.035))) + + def draw_wuzhang_ledger_tent(draw: ImageDraw.ImageDraw, x: int, y: int) -> None: cx, cy = center(x, y) tent_w = int(TILE * 0.62) @@ -1589,6 +1790,8 @@ def build_map(config: dict[str, object]) -> Image.Image: add_route_and_battlefield_details(canvas, terrain, units) if config.get("detail_profile") == "weishui-camps": add_weishui_camps_details(canvas, terrain) + elif config.get("detail_profile") == "weishui-northbank": + add_weishui_northbank_details(canvas, terrain) elif config.get("detail_profile") == "wuzhang-final": add_wuzhang_final_details(canvas, terrain) return add_subtle_grid_and_grade(canvas, terrain) diff --git a/src/assets/images/battle/sixty-fifth-battle-map.webp b/src/assets/images/battle/sixty-fifth-battle-map.webp index b5117f8..0d617f7 100644 Binary files a/src/assets/images/battle/sixty-fifth-battle-map.webp and b/src/assets/images/battle/sixty-fifth-battle-map.webp differ