diff --git a/docs/handpaint-batch4-wu-cavalry-river-source-v1.png b/docs/handpaint-batch4-wu-cavalry-river-source-v1.png new file mode 100644 index 0000000..fb57991 Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-source-v1.png differ diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-animation.gif b/docs/handpaint-batch4-wu-cavalry-river-v4-animation.gif new file mode 100644 index 0000000..42118eb Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-v4-animation.gif differ diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-battle-composite.png b/docs/handpaint-batch4-wu-cavalry-river-v4-battle-composite.png new file mode 100644 index 0000000..35110ff Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-v4-battle-composite.png differ diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-before-after.png b/docs/handpaint-batch4-wu-cavalry-river-v4-before-after.png new file mode 100644 index 0000000..0b1cb0c Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-v4-before-after.png differ diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-contact.png b/docs/handpaint-batch4-wu-cavalry-river-v4-contact.png new file mode 100644 index 0000000..b4d3890 Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-v4-contact.png differ diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-debug-preview-battle-check.png b/docs/handpaint-batch4-wu-cavalry-river-v4-debug-preview-battle-check.png new file mode 100644 index 0000000..b7003f3 Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-v4-debug-preview-battle-check.png differ diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-report.md b/docs/handpaint-batch4-wu-cavalry-river-v4-report.md new file mode 100644 index 0000000..ee494a9 --- /dev/null +++ b/docs/handpaint-batch4-wu-cavalry-river-v4-report.md @@ -0,0 +1,24 @@ +# Wu River Cavalry Hand-Painted v4 + +- source: `handpaint-batch4-wu-cavalry-river-source-v1.png` +- base status: ok +- action status: ok +- base partial alpha: 0, opaque pixels: 1893590 +- action partial alpha: 0, opaque pixels: 4736678 +- base frame border hits: 0 +- action frame border hits: 0 +- base empty frames: 0 +- action empty frames: 0 +- base bbox width range: 151..282 +- base bbox height range: 271..288 +- base bottom range: 303..303 +- action bbox width range: 146..303 +- action bbox height range: 271..294 +- action bottom range: 303..303 +- contact: `handpaint-batch4-wu-cavalry-river-v4-contact.png` +- animation: `handpaint-batch4-wu-cavalry-river-v4-animation.gif` +- before/after: `handpaint-batch4-wu-cavalry-river-v4-before-after.png` +- scale compare: `handpaint-batch4-wu-cavalry-river-v4-scale-compare.png` +- battle composite: `handpaint-batch4-wu-cavalry-river-v4-battle-composite.png` +- manual checks: run build and local browser verification after generation +- note: battle composite is a scale review image; gameplay roster is unchanged during sample approval diff --git a/docs/handpaint-batch4-wu-cavalry-river-v4-scale-compare.png b/docs/handpaint-batch4-wu-cavalry-river-v4-scale-compare.png new file mode 100644 index 0000000..b59a70b Binary files /dev/null and b/docs/handpaint-batch4-wu-cavalry-river-v4-scale-compare.png differ diff --git a/docs/unit-sprite-art-quality-gate.md b/docs/unit-sprite-art-quality-gate.md index 2693f94..8734af5 100644 --- a/docs/unit-sprite-art-quality-gate.md +++ b/docs/unit-sprite-art-quality-gate.md @@ -6,9 +6,9 @@ This gate catches obvious sprite-art failures before user approval. It does not - Manifest: `docs/unit-sprite-rework-manifest.json` - Audit: `docs/unit-sprite-rework-audit.json` -- Approved baseline units: 64 +- Approved baseline units: 65 - Pending approval units: 0 -- Queued rework units: 27 +- Queued rework units: 26 ## Checked Rules diff --git a/docs/unit-sprite-batch-queue.json b/docs/unit-sprite-batch-queue.json index 0435767..bd393e3 100644 --- a/docs/unit-sprite-batch-queue.json +++ b/docs/unit-sprite-batch-queue.json @@ -63,6 +63,7 @@ "unit-wu-archer-river", "unit-wu-cavalry", "unit-wu-cavalry-elite", + "unit-wu-cavalry-river", "unit-wu-infantry", "unit-wu-officer", "unit-wu-strategist", @@ -71,8 +72,8 @@ "pendingApproval": [], "nextAfterApproval": [], "stageCounts": { - "approved-baseline": 64, - "queued-rework": 27 + "approved-baseline": 65, + "queued-rework": 26 }, "batches": [ { @@ -680,24 +681,10 @@ "label": "Batch 4 Wu generic units", "total": 16, "stageCounts": { - "approved-baseline": 8, - "queued-rework": 8 + "approved-baseline": 9, + "queued-rework": 7 }, "units": [ - { - "stem": "unit-wu-cavalry-river", - "reviewStage": "queued-rework", - "queueRole": "queued rework", - "baseStatus": "border-hits=64", - "actionStatus": "border-hits=144", - "battleBasePx": [ - 25.6, - 46.5, - 45.0, - 46.2 - ], - "spec": null - }, { "stem": "unit-wu-infantry-marine", "reviewStage": "queued-rework", @@ -866,6 +853,20 @@ ], "spec": null }, + { + "stem": "unit-wu-cavalry-river", + "reviewStage": "approved-baseline", + "queueRole": "approved reference", + "baseStatus": "ok", + "actionStatus": "ok", + "battleBasePx": [ + 24.1, + 45.0, + 43.3, + 46.0 + ], + "spec": null + }, { "stem": "unit-wu-infantry", "reviewStage": "approved-baseline", diff --git a/docs/unit-sprite-batch-queue.md b/docs/unit-sprite-batch-queue.md index ed5b183..ad458fc 100644 --- a/docs/unit-sprite-batch-queue.md +++ b/docs/unit-sprite-batch-queue.md @@ -6,9 +6,9 @@ Use this as the long-running queue for the full hand-painted sprite rework. It i ## Current State -- Approved baseline units: 64 +- Approved baseline units: 65 - Pending approval units: 0 -- Queued rework units: 27 +- Queued rework units: 26 - Manifest: `docs/unit-sprite-rework-manifest.json` - Audit: `docs/unit-sprite-rework-audit.json` @@ -98,11 +98,10 @@ No next unit is currently defined. - Batch key: `batch4-wu` - Total units: 16 -- Stage counts: approved-baseline 8, queued-rework 8 +- Stage counts: approved-baseline 9, queued-rework 7 | unit | role | stage | status | battle base px | spec | | --- | --- | --- | --- | --- | --- | -| `unit-wu-cavalry-river` | queued rework | queued-rework | base border-hits=64; actions border-hits=144 | 25.6..46.5 x 45.0..46.2 | | | `unit-wu-infantry-marine` | queued rework | queued-rework | base border-hits=32; actions border-hits=82 | 30.7..44.2 x 44.6..44.6 | | | `unit-wu-infantry-river` | queued rework | queued-rework | base border-hits=32; actions border-hits=82 | 30.7..44.2 x 44.6..44.6 | | | `unit-wu-officer-harbor` | queued rework | queued-rework | base border-hits=32; actions border-hits=82 | 31.9..40.6 x 44.6..44.6 | | @@ -115,6 +114,7 @@ No next unit is currently defined. | `unit-wu-archer-river` | approved reference | approved-baseline | technical ok | 33.4..36.6 x 46.0..46.0 | | | `unit-wu-cavalry` | approved reference | approved-baseline | technical ok | 28.3..40.3 x 43.8..46.3 | | | `unit-wu-cavalry-elite` | approved reference | approved-baseline | technical ok | 36.6..40.7 x 46.0..46.0 | | +| `unit-wu-cavalry-river` | approved reference | approved-baseline | technical ok | 24.1..45.0 x 43.3..46.0 | | | `unit-wu-infantry` | approved reference | approved-baseline | technical ok | 25.2..33.1 x 46.0..46.5 | | | `unit-wu-officer` | approved reference | approved-baseline | technical ok | 25.4..32.9 x 46.0..46.0 | | | `unit-wu-strategist` | approved reference | approved-baseline | technical ok | 25.7..40.7 x 46.0..46.3 | | diff --git a/docs/unit-sprite-current-approval-board.png b/docs/unit-sprite-current-approval-board.png index dc8e3b0..9cf3e14 100644 Binary files a/docs/unit-sprite-current-approval-board.png and b/docs/unit-sprite-current-approval-board.png differ diff --git a/docs/unit-sprite-next-steps.md b/docs/unit-sprite-next-steps.md index a426db1..0731929 100644 --- a/docs/unit-sprite-next-steps.md +++ b/docs/unit-sprite-next-steps.md @@ -4,9 +4,9 @@ This file is generated by `scripts/write-unit-sprite-next-steps.py`. ## Current Gate -- Approved baseline units: 64 +- Approved baseline units: 65 - Pending approval units: 0 -- Queued rework units: 27 +- Queued rework units: 26 - Manifest: `docs/unit-sprite-rework-manifest.json` - Audit: `docs/unit-sprite-rework-audit.json` - Current approval board: not generated until the next pending sample. diff --git a/docs/unit-sprite-review-approved-and-pending.png b/docs/unit-sprite-review-approved-and-pending.png index 30ec5a1..0d0b668 100644 Binary files a/docs/unit-sprite-review-approved-and-pending.png and b/docs/unit-sprite-review-approved-and-pending.png differ diff --git a/docs/unit-sprite-review-batch4-wu.png b/docs/unit-sprite-review-batch4-wu.png index eb7914a..51c62c0 100644 Binary files a/docs/unit-sprite-review-batch4-wu.png and b/docs/unit-sprite-review-batch4-wu.png differ diff --git a/docs/unit-sprite-review-index.png b/docs/unit-sprite-review-index.png index 9d624f3..a3f4e41 100644 Binary files a/docs/unit-sprite-review-index.png and b/docs/unit-sprite-review-index.png differ diff --git a/docs/unit-sprite-rework-audit.json b/docs/unit-sprite-rework-audit.json index 4296355..6ec613e 100644 --- a/docs/unit-sprite-rework-audit.json +++ b/docs/unit-sprite-rework-audit.json @@ -11,8 +11,8 @@ "batch7-unique-officer": 19 }, "stage_counts": { - "approved-baseline": 64, - "queued-rework": 27 + "approved-baseline": 65, + "queued-rework": 26 }, "rows": [ { @@ -4292,7 +4292,7 @@ }, { "batch": "batch4-wu", - "review_stage": "queued-rework", + "review_stage": "approved-baseline", "stem": "unit-wu-cavalry-river", "base": { "key": "unit-wu-cavalry-river", @@ -4302,23 +4302,23 @@ 1252 ], "partial_alpha": 0, - "opaque_pixels": 2399821, - "border_hits": 64, - "min_width": 160, - "max_width": 291, - "min_height": 282, - "max_height": 289, - "min_bottom": 281, - "max_bottom": 288, + "opaque_pixels": 1893590, + "border_hits": 0, + "min_width": 151, + "max_width": 282, + "min_height": 271, + "max_height": 288, + "min_bottom": 303, + "max_bottom": 303, "empty_frames": 0, "width_swing": 131, - "height_swing": 7, - "bottom_swing": 7, - "battle_min_width_px": 25.6, - "battle_max_width_px": 46.5, - "battle_min_height_px": 45.0, - "battle_max_height_px": 46.2, - "status": "border-hits=64" + "height_swing": 17, + "bottom_swing": 0, + "battle_min_width_px": 24.1, + "battle_max_width_px": 45.0, + "battle_min_height_px": 43.3, + "battle_max_height_px": 46.0, + "status": "ok" }, "actions": { "key": "unit-wu-cavalry-river-actions", @@ -4328,23 +4328,23 @@ 1252 ], "partial_alpha": 0, - "opaque_pixels": 5378536, - "border_hits": 144, - "min_width": 160, - "max_width": 300, - "min_height": 274, - "max_height": 290, - "min_bottom": 273, - "max_bottom": 289, + "opaque_pixels": 4736678, + "border_hits": 0, + "min_width": 146, + "max_width": 303, + "min_height": 271, + "max_height": 294, + "min_bottom": 303, + "max_bottom": 303, "empty_frames": 0, - "width_swing": 140, - "height_swing": 16, - "bottom_swing": 16, - "battle_min_width_px": 25.6, - "battle_max_width_px": 47.9, - "battle_min_height_px": 43.8, - "battle_max_height_px": 46.3, - "status": "border-hits=144" + "width_swing": 157, + "height_swing": 23, + "bottom_swing": 0, + "battle_min_width_px": 23.3, + "battle_max_width_px": 48.4, + "battle_min_height_px": 43.3, + "battle_max_height_px": 47.0, + "status": "ok" } }, { diff --git a/docs/unit-sprite-rework-audit.md b/docs/unit-sprite-rework-audit.md index 3eb63a0..bbf1935 100644 --- a/docs/unit-sprite-rework-audit.md +++ b/docs/unit-sprite-rework-audit.md @@ -29,8 +29,8 @@ Visual review boards are generated by `scripts/render-unit-sprite-review-boards. ## Review Stage Counts -- approved-baseline: 64 -- queued-rework: 27 +- approved-baseline: 65 +- queued-rework: 26 ## Batch Plan @@ -127,7 +127,7 @@ Visual review boards are generated by `scripts/render-unit-sprite-review-boards. | batch4-wu | approved-baseline | `unit-wu-archer-river` | ok | ok | 209..229 x 288..288 | 209..252 x 233..288 | 33.4..36.6 x 46.0..46.0 | w20 h0 b0 | | batch4-wu | approved-baseline | `unit-wu-cavalry` | ok | ok | 177..252 x 274..290 | 177..261 x 274..290 | 28.3..40.3 x 43.8..46.3 | w75 h16 b0 | | batch4-wu | approved-baseline | `unit-wu-cavalry-elite` | ok | ok | 229..255 x 288..288 | 220..286 x 267..288 | 36.6..40.7 x 46.0..46.0 | w26 h0 b0 | -| batch4-wu | queued-rework | `unit-wu-cavalry-river` | border-hits=64 | border-hits=144 | 160..291 x 282..289 | 160..300 x 274..290 | 25.6..46.5 x 45.0..46.2 | w131 h7 b7 | +| batch4-wu | approved-baseline | `unit-wu-cavalry-river` | ok | ok | 151..282 x 271..288 | 146..303 x 271..294 | 24.1..45.0 x 43.3..46.0 | w131 h17 b0 | | batch4-wu | approved-baseline | `unit-wu-infantry` | ok | ok | 158..207 x 288..291 | 158..281 x 288..291 | 25.2..33.1 x 46.0..46.5 | w49 h3 b0 | | batch4-wu | queued-rework | `unit-wu-infantry-marine` | border-hits=32 | border-hits=82 | 192..277 x 279..279 | 192..286 x 275..279 | 30.7..44.2 x 44.6..44.6 | w85 h0 b6 | | batch4-wu | queued-rework | `unit-wu-infantry-river` | border-hits=32 | border-hits=82 | 192..277 x 279..279 | 192..286 x 275..279 | 30.7..44.2 x 44.6..44.6 | w85 h0 b6 | diff --git a/docs/unit-sprite-rework-manifest.json b/docs/unit-sprite-rework-manifest.json index dd5a564..01822e1 100644 --- a/docs/unit-sprite-rework-manifest.json +++ b/docs/unit-sprite-rework-manifest.json @@ -71,6 +71,7 @@ "unit-wu-archer-river", "unit-wu-cavalry", "unit-wu-cavalry-elite", + "unit-wu-cavalry-river", "unit-wu-infantry", "unit-wu-officer", "unit-wu-strategist", diff --git a/docs/unit-sprite-wu-cavalry-river-v4-approval-packet.md b/docs/unit-sprite-wu-cavalry-river-v4-approval-packet.md new file mode 100644 index 0000000..0380f5f --- /dev/null +++ b/docs/unit-sprite-wu-cavalry-river-v4-approval-packet.md @@ -0,0 +1,36 @@ +# Wu River Cavalry v4 Approval Packet + +- status: approved during continuous production run +- unit: `unit-wu-cavalry-river` +- source art: `handpaint-batch4-wu-cavalry-river-source-v1.png` +- contact sheet: `handpaint-batch4-wu-cavalry-river-v4-contact.png` +- animation GIF: `handpaint-batch4-wu-cavalry-river-v4-animation.gif` +- before/after: `handpaint-batch4-wu-cavalry-river-v4-before-after.png` +- scale compare: `handpaint-batch4-wu-cavalry-river-v4-scale-compare.png` +- battle composite: `handpaint-batch4-wu-cavalry-river-v4-battle-composite.png` +- browser check: `handpaint-batch4-wu-cavalry-river-v4-debug-preview-battle-check.png` +- current board: `unit-sprite-current-approval-board.png` +- debug URL: `http://127.0.0.1:4173/heros_web/?debug&debugSpritePreview=unit-wu-cavalry-river&v=wu-cavalry-river-v4` + +## Visual Decision + +Approved for the continuous batch run. v4 replaces the previous boat-like placeholder with a readable mounted Wu river cavalry sprite. The red helmet, teal cloth, compact brown horse, and visible cavalry blade separate it from river archers and infantry, while the lighter armor keeps it distinct from elite Wu cavalry. + +## Technical Checks + +- base status: ok +- action status: ok +- base partial alpha: 0 +- action partial alpha: 0 +- base frame border hits: 0 +- action frame border hits: 0 +- base empty frames: 0 +- action empty frames: 0 +- battle scale from sample gate: 24.1..45.0 x 43.3..46.0 +- local build: passed +- Local browser console errors: `0` +- Local browser 404s: `0` + +## Notes + +v1 was visually readable but failed the automated height-stability gate, and v2/v3 touched action-frame borders. v4 keeps the larger base scale and slightly tightens action width so the mounted charge stays inside the 313px frame without external rings, bases, or badges. diff --git a/scripts/assemble-handpaint-shu-infantry-quality-sample.py b/scripts/assemble-handpaint-shu-infantry-quality-sample.py index 41e3d39..314fdbe 100644 --- a/scripts/assemble-handpaint-shu-infantry-quality-sample.py +++ b/scripts/assemble-handpaint-shu-infantry-quality-sample.py @@ -26,6 +26,7 @@ COMPARE_LABELS = ["liu-bei", "guan-yu", "zhang-fei", "rebel", "shu-infantry"] FIT_WIDTH_SCALE = 1.0 MIN_FRAME_HEIGHT = 0 STRETCH_X = 1.0 +ACTION_SHRINK_X = 1.0 FRAME = 313 DIRECTIONS = ("south", "east", "north", "west") BASE_FRAMES_PER_DIRECTION = 16 @@ -208,6 +209,26 @@ def stretch_subject_x(subject: Image.Image, max_width: int) -> Image.Image: return trim_to_alpha(solidify_alpha(subject.resize((stretched_width, subject.height), Image.Resampling.LANCZOS))) +def shrink_action_frame_x(frame: Image.Image) -> Image.Image: + if ACTION_SHRINK_X >= 1: + return frame + rgba = solidify_alpha(frame) + alpha = np.array(rgba.getchannel("A")) + ys, xs = np.where(alpha > 0) + if len(xs) == 0: + return rgba + left, right = int(xs.min()), int(xs.max()) + 1 + top, bottom = int(ys.min()), int(ys.max()) + 1 + subject = rgba.crop((left, top, right, bottom)) + target_width = max(1, round(subject.width * ACTION_SHRINK_X)) + if target_width >= subject.width: + return rgba + subject = solidify_alpha(subject.resize((target_width, subject.height), Image.Resampling.LANCZOS)) + out = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0)) + out.alpha_composite(subject, ((FRAME - target_width) // 2, bottom - subject.height)) + return solidify_alpha(out) + + def fit_frame(source: Image.Image, max_width: int, max_height: int, bottom: int = 304, x_offset: int = 0, allow_detached_effects = False) -> Image.Image: max_width = int(round(max_width * FIT_WIDTH_SCALE)) subject = crop_subject(source, allow_detached_effects) @@ -321,6 +342,7 @@ def build_action_sheet(poses: dict[str, Image.Image]) -> Image.Image: action_frame = frame if direction == "north" and action in {"attack", "strategy", "item", "celebrate"}: action_frame = poses["walk_back"] if action == "attack" else poses["command"] + action_frame = shrink_action_frame_x(action_frame) sheet.alpha_composite(solidify_alpha(action_frame), ((offset + index) * FRAME, row * FRAME)) return solidify_alpha(sheet) @@ -546,6 +568,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--fit-width-scale", type=float, default=FIT_WIDTH_SCALE) parser.add_argument("--min-frame-height", type=int, default=MIN_FRAME_HEIGHT) parser.add_argument("--stretch-x", type=float, default=STRETCH_X) + parser.add_argument("--action-shrink-x", type=float, default=ACTION_SHRINK_X) return parser.parse_args() @@ -553,7 +576,7 @@ def configure(args: argparse.Namespace) -> None: global SOURCE, WORK_DIR, BATTLE_REFERENCE, STEM, OUTPUT_PREFIX global CONTACT_TITLE, BEFORE_TITLE, BEFORE_SCALE_LABEL, REPORT_TITLE, SAMPLE_LABEL global COMPARE_STEMS, COMPARE_LABELS - global FIT_WIDTH_SCALE, MIN_FRAME_HEIGHT, STRETCH_X + global FIT_WIDTH_SCALE, MIN_FRAME_HEIGHT, STRETCH_X, ACTION_SHRINK_X SOURCE = project_path(args.source) WORK_DIR = project_path(args.work_dir) @@ -570,6 +593,7 @@ def configure(args: argparse.Namespace) -> None: FIT_WIDTH_SCALE = args.fit_width_scale MIN_FRAME_HEIGHT = args.min_frame_height STRETCH_X = args.stretch_x + ACTION_SHRINK_X = args.action_shrink_x if len(COMPARE_STEMS) != len(COMPARE_LABELS): raise ValueError("--compare-stems and --compare-labels must have the same number of entries") diff --git a/src/assets/images/units/unit-wu-cavalry-river-actions.png b/src/assets/images/units/unit-wu-cavalry-river-actions.png index 4531fec..a05573d 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-river-actions.png and b/src/assets/images/units/unit-wu-cavalry-river-actions.png differ diff --git a/src/assets/images/units/unit-wu-cavalry-river.png b/src/assets/images/units/unit-wu-cavalry-river.png index fb4b25a..30a54bc 100644 Binary files a/src/assets/images/units/unit-wu-cavalry-river.png and b/src/assets/images/units/unit-wu-cavalry-river.png differ