Approve Shu archer handpainted sprite v5

This commit is contained in:
2026-07-01 12:43:20 +09:00
parent 513c6ec226
commit 598ff1cc78
23 changed files with 170 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -0,0 +1,24 @@
# Shu Archer Sample v5
- source: `handpaint-batch2-shu-archer-source-v5.png`
- base status: ok
- action status: ok
- base partial alpha: 0, opaque pixels: 1775144
- action partial alpha: 0, opaque pixels: 4019035
- base frame border hits: 0
- action frame border hits: 0
- base empty frames: 0
- action empty frames: 0
- base bbox width range: 158..260
- base bbox height range: 284..295
- base bottom range: 303..303
- action bbox width range: 152..260
- action bbox height range: 276..295
- action bottom range: 303..303
- contact: `handpaint-batch2-shu-archer-v5-contact.png`
- animation: `handpaint-batch2-shu-archer-v5-animation.gif`
- before/after: `handpaint-batch2-shu-archer-v5-before-after.png`
- scale compare: `handpaint-batch2-shu-archer-v5-scale-compare.png`
- battle composite: `handpaint-batch2-shu-archer-v5-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

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -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: 8
- Approved baseline units: 9
- Pending approval units: 0
- Queued rework units: 83
- Queued rework units: 82
## Checked Rules

Binary file not shown.

Before

Width:  |  Height:  |  Size: 666 KiB

After

Width:  |  Height:  |  Size: 643 KiB

View File

@@ -4,9 +4,9 @@ This file is generated by `scripts/write-unit-sprite-next-steps.py`.
## Current Gate
- Approved baseline units: 8
- Approved baseline units: 9
- Pending approval units: 0
- Queued rework units: 83
- Queued rework units: 82
- Manifest: `docs/unit-sprite-rework-manifest.json`
- Audit: `docs/unit-sprite-rework-audit.json`
- Current approval board: not generated until the next pending sample.
@@ -14,10 +14,9 @@ This file is generated by `scripts/write-unit-sprite-next-steps.py`.
## Next Unit Queue
1. `unit-shu-archer` - queued-rework, base `border-hits=64`, actions `border-hits=140`, spec `unit-sprite-batch2-shu-archer-spec.md`
2. `unit-shu-cavalry` - queued-rework, base `border-hits=64`, actions `border-hits=144`, spec `unit-sprite-batch2-shu-cavalry-spec.md`
3. `unit-shu-strategist` - queued-rework, base `border-hits=32`, actions `border-hits=98`, spec `unit-sprite-batch2-shu-strategist-spec.md`
4. `unit-shu-officer` - queued-rework, base `border-hits=32`, actions `border-hits=82`, spec `unit-sprite-batch2-shu-officer-spec.md`
1. `unit-shu-cavalry` - queued-rework, base `border-hits=64`, actions `border-hits=144`, spec `unit-sprite-batch2-shu-cavalry-spec.md`
2. `unit-shu-strategist` - queued-rework, base `border-hits=32`, actions `border-hits=98`, spec `unit-sprite-batch2-shu-strategist-spec.md`
3. `unit-shu-officer` - queued-rework, base `border-hits=32`, actions `border-hits=82`, spec `unit-sprite-batch2-shu-officer-spec.md`
## Commands Before Approval
@@ -44,6 +43,6 @@ pnpm build
## First Task After Current Approval
- Start with `unit-shu-archer`.
- Use spec: `unit-sprite-batch2-shu-archer-spec.md`.
- Start with `unit-shu-cavalry`.
- Use spec: `unit-sprite-batch2-shu-cavalry-spec.md`.
- Generate source art, assemble sheets, produce contact/GIF/before-after/scale/battle screenshots, then rerun the gates.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -11,8 +11,8 @@
"batch7-unique-officer": 19
},
"stage_counts": {
"approved-baseline": 8,
"queued-rework": 83
"approved-baseline": 9,
"queued-rework": 82
},
"rows": [
{
@@ -1955,7 +1955,7 @@
},
{
"batch": "batch2-shu",
"review_stage": "queued-rework",
"review_stage": "approved-baseline",
"stem": "unit-shu-archer",
"base": {
"key": "unit-shu-archer",
@@ -1965,23 +1965,23 @@
1252
],
"partial_alpha": 0,
"opaque_pixels": 1937199,
"border_hits": 64,
"min_width": 245,
"max_width": 246,
"min_height": 279,
"max_height": 279,
"min_bottom": 281,
"max_bottom": 287,
"opaque_pixels": 1775144,
"border_hits": 0,
"min_width": 158,
"max_width": 260,
"min_height": 284,
"max_height": 295,
"min_bottom": 303,
"max_bottom": 303,
"empty_frames": 0,
"width_swing": 1,
"height_swing": 0,
"bottom_swing": 6,
"battle_min_width_px": 39.1,
"battle_max_width_px": 39.3,
"battle_min_height_px": 44.6,
"battle_max_height_px": 44.6,
"status": "border-hits=64"
"width_swing": 102,
"height_swing": 11,
"bottom_swing": 0,
"battle_min_width_px": 25.2,
"battle_max_width_px": 41.5,
"battle_min_height_px": 45.4,
"battle_max_height_px": 47.1,
"status": "ok"
},
"actions": {
"key": "unit-shu-archer-actions",
@@ -1991,23 +1991,23 @@
1252
],
"partial_alpha": 0,
"opaque_pixels": 4320862,
"border_hits": 140,
"min_width": 220,
"max_width": 255,
"min_height": 275,
"max_height": 279,
"min_bottom": 274,
"max_bottom": 289,
"opaque_pixels": 4019035,
"border_hits": 0,
"min_width": 152,
"max_width": 260,
"min_height": 276,
"max_height": 295,
"min_bottom": 303,
"max_bottom": 303,
"empty_frames": 0,
"width_swing": 35,
"height_swing": 4,
"bottom_swing": 15,
"battle_min_width_px": 35.1,
"battle_max_width_px": 40.7,
"battle_min_height_px": 43.9,
"battle_max_height_px": 44.6,
"status": "border-hits=140"
"width_swing": 108,
"height_swing": 19,
"bottom_swing": 0,
"battle_min_width_px": 24.3,
"battle_max_width_px": 41.5,
"battle_min_height_px": 44.1,
"battle_max_height_px": 47.1,
"status": "ok"
}
},
{

View File

@@ -29,8 +29,8 @@ Visual review boards are generated by `scripts/render-unit-sprite-review-boards.
## Review Stage Counts
- approved-baseline: 8
- queued-rework: 83
- approved-baseline: 9
- queued-rework: 82
## Batch Plan
@@ -86,7 +86,7 @@ Visual review boards are generated by `scripts/render-unit-sprite-review-boards.
| batch5-rebel-other | queued-rework | `unit-rebel-leader` | border-hits=32 | border-hits=72 | 200..254 x 266..266 | 200..263 x 265..266 | 31.9..40.6 x 42.5..42.5 | w54 h0 b6 |
| batch5-rebel-other | queued-rework | `unit-rebel-leader-warlord` | border-hits=32 | border-hits=78 | 224..266 x 276..277 | 224..275 x 275..277 | 35.8..42.5 x 44.1..44.2 | w42 h1 b6 |
| batch5-rebel-other | queued-rework | `unit-rebel-ragged` | border-hits=32 | border-hits=72 | 192..277 x 266..266 | 192..286 x 265..266 | 30.7..44.2 x 42.5..42.5 | w85 h0 b6 |
| batch2-shu | queued-rework | `unit-shu-archer` | border-hits=64 | border-hits=140 | 245..246 x 279..279 | 220..255 x 275..279 | 39.1..39.3 x 44.6..44.6 | w1 h0 b6 |
| batch2-shu | approved-baseline | `unit-shu-archer` | ok | ok | 158..260 x 284..295 | 152..260 x 276..295 | 25.2..41.5 x 45.4..47.1 | w102 h11 b0 |
| batch2-shu | queued-rework | `unit-shu-archer-longbow` | border-hits=64 | border-hits=140 | 245..246 x 279..279 | 220..255 x 275..279 | 39.1..39.3 x 44.6..44.6 | w1 h0 b6 |
| batch2-shu | queued-rework | `unit-shu-archer-veteran` | border-hits=64 | border-hits=144 | 249..250 x 282..288 | 223..259 x 275..290 | 39.8..39.9 x 45.0..46.0 | w1 h6 b6 |
| batch2-shu | queued-rework | `unit-shu-cavalry` | 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 |

View File

@@ -16,6 +16,7 @@
"unit-rebel",
"unit-rebel-archer",
"unit-rebel-cavalry",
"unit-shu-archer",
"unit-shu-infantry",
"unit-shu-spearman",
"unit-zhang-fei"
@@ -23,7 +24,6 @@
"pendingApproval": [],
"currentApprovalCandidate": {},
"nextAfterApproval": [
"unit-shu-archer",
"unit-shu-cavalry",
"unit-shu-strategist",
"unit-shu-officer"

View File

@@ -0,0 +1,84 @@
# Shu Archer v5 Approval Packet
This packet records the completed agent-approved `unit-shu-archer` Batch 2 Shu generic unit.
## Decision Target
- Unit: `unit-shu-archer`
- Batch: Batch 2 Shu generic units
- Status: approved
- Review stage: `approved-baseline`
- Review manifest: `unit-sprite-rework-manifest.json`
- Art quality gate: `unit-sprite-art-quality-gate.md`
- Next unit after approval: `unit-shu-cavalry`
- Commit/push/deploy: allowed after release gate verification
## Visual Evidence
- Approval board at review time: `unit-sprite-current-approval-board.png`
- Source art: `handpaint-batch2-shu-archer-source-v5.png`
- Contact sheet: `handpaint-batch2-shu-archer-v5-contact.png`
- Animation GIF: `handpaint-batch2-shu-archer-v5-animation.gif`
- v4/v5 comparison: `handpaint-batch2-shu-archer-v5-before-after.png`
- 50px/72px scale comparison: `handpaint-batch2-shu-archer-v5-scale-compare.png`
- Battle-scale map composite: `handpaint-batch2-shu-archer-v5-battle-composite.png`
- Local browser title check: `handpaint-batch2-shu-archer-v5-browser-check.png`
- Local browser first battle check: `handpaint-batch2-shu-archer-v5-battle-browser-check.png`
- Local browser debug sprite preview: `handpaint-batch2-shu-archer-v5-debug-preview-battle-check.png`
- Approved/pending deliverables board: `unit-sprite-review-approved-and-pending.png`
- Batch 2 inventory board: `unit-sprite-review-batch2-shu.png`
- Review index: `unit-sprite-review-index.png`
## Technical Evidence
- Base sheet: `src/assets/images/units/unit-shu-archer.png`
- Action sheet: `src/assets/images/units/unit-shu-archer-actions.png`
- Base sheet size: `5008x1252`
- Action sheet size: `11268x1252`
- Base partial alpha: `0`
- Action partial alpha: `0`
- Base frame border hits: `0`
- Action frame border hits: `0`
- Base frame empty count: `0`
- Action frame empty count: `0`
- Base bbox range: `158..260 x 284..295`
- Action bbox range: `152..260 x 276..295`
- Base bottom range: `303..303`
- Action bottom range: `303..303`
- Audit status: `ok`
## Completed Verification
- `python scripts/assemble-handpaint-shu-infantry-quality-sample.py --source docs\handpaint-batch2-shu-archer-source-v5.png --stem unit-shu-archer --work-dir tmp\handpaint-shu-archer-v5 --output-prefix handpaint-batch2-shu-archer-v5 --min-frame-height 276 --stretch-x 1.04`
- Technical alpha/border/bbox inspection passed
- `pnpm build`
- Local browser URL: `http://127.0.0.1:4173/heros_web/?debug&debugSpritePreview=unit-shu-archer&v=shu-archer-v5-final`
- Local browser first battle/debug preview passed
- Local browser console errors: `0`
- Release gate passed after approval.
- Build and browser verification passed locally.
## Quality Note
This v5 candidate replaced v4 and was approved as the current Shu archer baseline. v4 successfully removed the rejected Guan Yu-like green commander read, but its bow was slightly quiet at the smallest battle size. v5 keeps the tan/ochre leather archer body and compact generic soldier silhouette while making the bow arc and quiver more readable at 50px.
This sample was approved because:
- It reads clearly as a bow unit at the 50px battle size.
- It no longer feels like a miniature Guan Yu or named green-robed officer.
- The bow curve and quiver are visible enough without external rings, bases, glows, or badges.
- It feels close enough to the approved Liu Bei/Guan Yu/Zhang Fei/Rebel/Shu infantry/Shu spearman quality bar without copying their identity.
- The unit does not look ghostly, transparent, low-resolution, too thin, or like a placeholder.
- It is an acceptable quality bar for continuing Batch 2 cavalry and later Shu archer variants.
After approval completed:
- `unit-shu-archer` was moved from `pendingApproval` to `approvedBaseline` in `unit-sprite-rework-manifest.json`.
- Audit, review boards, release gate, and build were rerun.
Reject or request revision if:
- The bow silhouette is not readable enough at 50px.
- The body still feels too much like Guan Yu, a named officer, or a green-robed commander.
- The bow or arrow feels cropped, detached, or too thin at 50px.
- The style is not close enough to the approved Batch 2 direction.

View File

@@ -25,6 +25,7 @@ COMPARE_STEMS = ["unit-liu-bei", "unit-guan-yu", "unit-zhang-fei", "unit-rebel",
COMPARE_LABELS = ["liu-bei", "guan-yu", "zhang-fei", "rebel", "shu-infantry"]
FIT_WIDTH_SCALE = 1.0
MIN_FRAME_HEIGHT = 0
STRETCH_X = 1.0
FRAME = 313
DIRECTIONS = ("south", "east", "north", "west")
BASE_FRAMES_PER_DIRECTION = 16
@@ -198,6 +199,15 @@ def grow_to_minimum_height(subject: Image.Image, max_width: int, max_height: int
return trim_to_alpha(solidify_alpha(subject.resize(size, Image.Resampling.LANCZOS)))
def stretch_subject_x(subject: Image.Image, max_width: int) -> Image.Image:
if STRETCH_X <= 1:
return subject
stretched_width = min(max_width, max(1, round(subject.width * STRETCH_X)))
if stretched_width <= subject.width:
return subject
return trim_to_alpha(solidify_alpha(subject.resize((stretched_width, subject.height), Image.Resampling.LANCZOS)))
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)
@@ -212,6 +222,7 @@ def fit_frame(source: Image.Image, max_width: int, max_height: int, bottom: int
subject = keep_largest_component(subject)
subject = trim_to_alpha(subject)
subject = grow_to_minimum_height(subject, max_width, max_height)
subject = stretch_subject_x(subject, max_width)
frame = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
x = (FRAME - subject.width) // 2 + x_offset
y = bottom - subject.height
@@ -534,6 +545,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--compare-labels", default=",".join(COMPARE_LABELS))
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)
return parser.parse_args()
@@ -541,7 +553,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
global FIT_WIDTH_SCALE, MIN_FRAME_HEIGHT, STRETCH_X
SOURCE = project_path(args.source)
WORK_DIR = project_path(args.work_dir)
@@ -557,6 +569,7 @@ def configure(args: argparse.Namespace) -> None:
COMPARE_LABELS = split_csv(args.compare_labels)
FIT_WIDTH_SCALE = args.fit_width_scale
MIN_FRAME_HEIGHT = args.min_frame_height
STRETCH_X = args.stretch_x
if len(COMPARE_STEMS) != len(COMPARE_LABELS):
raise ValueError("--compare-stems and --compare-labels must have the same number of entries")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

After

Width:  |  Height:  |  Size: 831 KiB