Approve Shu spearman handpainted sprite v3

This commit is contained in:
2026-07-01 11:06:33 +09:00
parent 199bc47192
commit 513c6ec226
24 changed files with 183 additions and 52 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 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: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -0,0 +1,24 @@
# Shu Spearman Sample v3
- source: `handpaint-batch2-shu-spearman-source-v3.png`
- base status: ok
- action status: ok
- base partial alpha: 0, opaque pixels: 1829476
- action partial alpha: 0, opaque pixels: 4242447
- base frame border hits: 0
- action frame border hits: 0
- base empty frames: 0
- action empty frames: 0
- base bbox width range: 146..263
- base bbox height range: 274..290
- base bottom range: 303..303
- action bbox width range: 146..276
- action bbox height range: 280..295
- action bottom range: 303..303
- contact: `handpaint-batch2-shu-spearman-v3-contact.png`
- animation: `handpaint-batch2-shu-spearman-v3-animation.gif`
- before/after: `handpaint-batch2-shu-spearman-v3-before-after.png`
- scale compare: `handpaint-batch2-shu-spearman-v3-scale-compare.png`
- battle composite: `handpaint-batch2-shu-spearman-v3-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: 50 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: 7
- Approved baseline units: 8
- Pending approval units: 0
- Queued rework units: 84
- Queued rework units: 83
## Checked Rules

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

View File

@@ -4,9 +4,9 @@ This file is generated by `scripts/write-unit-sprite-next-steps.py`.
## Current Gate
- Approved baseline units: 7
- Approved baseline units: 8
- Pending approval units: 0
- Queued rework units: 84
- Queued rework units: 83
- 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,11 +14,10 @@ This file is generated by `scripts/write-unit-sprite-next-steps.py`.
## Next Unit Queue
1. `unit-shu-spearman` - queued-rework, base `border-hits=32`, actions `border-hits=82`, spec `unit-sprite-batch2-shu-spearman-spec.md`
2. `unit-shu-archer` - queued-rework, base `border-hits=64`, actions `border-hits=140`, spec `unit-sprite-batch2-shu-archer-spec.md`
3. `unit-shu-cavalry` - queued-rework, base `border-hits=64`, actions `border-hits=144`, spec `unit-sprite-batch2-shu-cavalry-spec.md`
4. `unit-shu-strategist` - queued-rework, base `border-hits=32`, actions `border-hits=98`, spec `unit-sprite-batch2-shu-strategist-spec.md`
5. `unit-shu-officer` - queued-rework, base `border-hits=32`, actions `border-hits=82`, spec `unit-sprite-batch2-shu-officer-spec.md`
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`
## Commands Before Approval
@@ -45,6 +44,6 @@ pnpm build
## First Task After Current Approval
- Start with `unit-shu-spearman`.
- Use spec: `unit-sprite-batch2-shu-spearman-spec.md`.
- Start with `unit-shu-archer`.
- Use spec: `unit-sprite-batch2-shu-archer-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: 73 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -11,8 +11,8 @@
"batch7-unique-officer": 19
},
"stage_counts": {
"approved-baseline": 7,
"queued-rework": 84
"approved-baseline": 8,
"queued-rework": 83
},
"rows": [
{
@@ -2639,7 +2639,7 @@
},
{
"batch": "batch2-shu",
"review_stage": "queued-rework",
"review_stage": "approved-baseline",
"stem": "unit-shu-spearman",
"base": {
"key": "unit-shu-spearman",
@@ -2649,23 +2649,23 @@
1252
],
"partial_alpha": 0,
"opaque_pixels": 2000768,
"border_hits": 32,
"min_width": 194,
"max_width": 254,
"min_height": 279,
"max_height": 279,
"min_bottom": 281,
"max_bottom": 287,
"opaque_pixels": 1829476,
"border_hits": 0,
"min_width": 146,
"max_width": 263,
"min_height": 274,
"max_height": 290,
"min_bottom": 303,
"max_bottom": 303,
"empty_frames": 0,
"width_swing": 60,
"height_swing": 0,
"bottom_swing": 6,
"battle_min_width_px": 31.0,
"battle_max_width_px": 40.6,
"battle_min_height_px": 44.6,
"battle_max_height_px": 44.6,
"status": "border-hits=32"
"width_swing": 117,
"height_swing": 16,
"bottom_swing": 0,
"battle_min_width_px": 23.3,
"battle_max_width_px": 42.0,
"battle_min_height_px": 43.8,
"battle_max_height_px": 46.3,
"status": "ok"
},
"actions": {
"key": "unit-shu-spearman-actions",
@@ -2675,23 +2675,23 @@
1252
],
"partial_alpha": 0,
"opaque_pixels": 4471927,
"border_hits": 82,
"min_width": 194,
"max_width": 263,
"min_height": 275,
"max_height": 279,
"min_bottom": 274,
"max_bottom": 289,
"opaque_pixels": 4242447,
"border_hits": 0,
"min_width": 146,
"max_width": 276,
"min_height": 280,
"max_height": 295,
"min_bottom": 303,
"max_bottom": 303,
"empty_frames": 0,
"width_swing": 69,
"height_swing": 4,
"bottom_swing": 15,
"battle_min_width_px": 31.0,
"battle_max_width_px": 42.0,
"battle_min_height_px": 43.9,
"battle_max_height_px": 44.6,
"status": "border-hits=82"
"width_swing": 130,
"height_swing": 15,
"bottom_swing": 0,
"battle_min_width_px": 23.3,
"battle_max_width_px": 44.1,
"battle_min_height_px": 44.7,
"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: 7
- queued-rework: 84
- approved-baseline: 8
- queued-rework: 83
## Batch Plan
@@ -98,7 +98,7 @@ Visual review boards are generated by `scripts/render-unit-sprite-review-boards.
| batch2-shu | queued-rework | `unit-shu-officer` | border-hits=32 | border-hits=82 | 200..254 x 279..279 | 200..263 x 275..279 | 31.9..40.6 x 44.6..44.6 | w54 h0 b6 |
| batch2-shu | queued-rework | `unit-shu-officer-council` | border-hits=32 | border-hits=82 | 200..254 x 279..279 | 200..263 x 275..279 | 31.9..40.6 x 44.6..44.6 | w54 h0 b6 |
| batch2-shu | queued-rework | `unit-shu-officer-vanguard` | border-hits=64 | border-hits=144 | 208..258 x 282..288 | 208..267 x 275..290 | 33.2..41.2 x 45.0..46.0 | w50 h6 b6 |
| batch2-shu | queued-rework | `unit-shu-spearman` | border-hits=32 | border-hits=82 | 194..254 x 279..279 | 194..263 x 275..279 | 31.0..40.6 x 44.6..44.6 | w60 h0 b6 |
| batch2-shu | approved-baseline | `unit-shu-spearman` | ok | ok | 146..263 x 274..290 | 146..276 x 280..295 | 23.3..42.0 x 43.8..46.3 | w117 h16 b0 |
| batch2-shu | queued-rework | `unit-shu-spearman-guard` | border-hits=64 | border-hits=144 | 202..258 x 282..288 | 202..267 x 275..290 | 32.3..41.2 x 45.0..46.0 | w56 h6 b6 |
| batch2-shu | queued-rework | `unit-shu-spearman-veteran` | border-hits=64 | border-hits=144 | 202..258 x 282..288 | 202..267 x 275..290 | 32.3..41.2 x 45.0..46.0 | w56 h6 b6 |
| batch2-shu | queued-rework | `unit-shu-strategist` | border-hits=32 | border-hits=98 | 243..246 x 280..281 | 220..255 x 275..281 | 38.8..39.3 x 44.7..44.9 | w3 h1 b6 |

View File

@@ -17,12 +17,12 @@
"unit-rebel-archer",
"unit-rebel-cavalry",
"unit-shu-infantry",
"unit-shu-spearman",
"unit-zhang-fei"
],
"pendingApproval": [],
"currentApprovalCandidate": {},
"nextAfterApproval": [
"unit-shu-spearman",
"unit-shu-archer",
"unit-shu-cavalry",
"unit-shu-strategist",

View File

@@ -0,0 +1,85 @@
# Shu Spearman v3 Approval Packet
This packet records the approved `unit-shu-spearman` Batch 2 Shu quality baseline.
## Decision Target
- Unit: `unit-shu-spearman`
- Batch: Batch 2 Shu generic units
- Status: approved by user
- 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-archer`
- 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-spearman-source-v3.png`
- Contact sheet: `handpaint-batch2-shu-spearman-v3-contact.png`
- Animation GIF: `handpaint-batch2-shu-spearman-v3-animation.gif`
- v2/v3 comparison: `handpaint-batch2-shu-spearman-v3-before-after.png`
- 50px/72px scale comparison: `handpaint-batch2-shu-spearman-v3-scale-compare.png`
- Battle-scale map composite: `handpaint-batch2-shu-spearman-v3-battle-composite.png`
- Local browser title check: `handpaint-batch2-shu-spearman-v3-browser-check.png`
- Local browser first battle check: `handpaint-batch2-shu-spearman-v3-battle-browser-check.png`
- Local browser debug sprite preview: `handpaint-batch2-shu-spearman-v3-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-spearman.png`
- Action sheet: `src/assets/images/units/unit-shu-spearman-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: `146..263 x 274..290`
- Action bbox range: `146..276 x 280..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-spearman-source-v3.png --stem unit-shu-spearman --work-dir tmp\handpaint-shu-spearman-v3 --output-prefix handpaint-batch2-shu-spearman-v3 --fit-width-scale 1.12 --min-frame-height 274`
- `pnpm build`
- Local browser app load passed at `http://127.0.0.1:4173/heros_web/?debug&debugSpritePreview=unit-shu-spearman&v=shu-spearman-v3-final`
- Local browser first battle load passed at the same URL.
- Local browser debug sprite preview passed at the same URL.
- Local browser console errors: `0`
- Technical alpha/border/bbox inspection passed
- Build and browser verification passed locally.
- Release gate passed after approval.
## Quality Note
This v3 candidate replaced v1 and v2 and is approved as the current Shu spearman baseline. v1 had strong spear identity but made the body feel slightly small at 50px; v2 improved stability but still had narrow front-facing silhouettes. v3 widens the front body block, keeps the spear class identity obvious, and better matches the approved Shu infantry v8 scale while preserving the 313px frame format.
Approved because:
- It reads clearly as a spear unit at the 50px battle size.
- It feels close enough to the approved Liu Bei/Guan Yu/Zhang Fei/Rebel/Shu infantry v8 direction.
- The spear line, green Shu armor, helmet, and foot-soldier silhouette read before the background.
- The unit does not look ghostly, transparent, low-resolution, too thin, or like a placeholder.
- The user accepted this as the quality bar for continuing Batch 2 spearmen and the next Shu generic unit.
After approval completed:
- `unit-shu-spearman` was moved from `pendingApproval` to `approvedBaseline` in `unit-sprite-rework-manifest.json`.
- Audit, review boards, release gate, and build were rerun.
- Next unit: `unit-shu-archer`.
Reject or request revision if:
- The body still feels too small next to approved Shu infantry v8.
- The spear tip or shaft feels cropped, detached, or too thin at 50px.
- The style is not close enough to the approved first batch and Shu infantry v8 direction.
- The 50px view is not readable enough for live battle play.

View File

@@ -23,6 +23,8 @@ REPORT_TITLE = "Shu Infantry Quality Restart Sample v3"
SAMPLE_LABEL = "shu sample"
COMPARE_STEMS = ["unit-liu-bei", "unit-guan-yu", "unit-zhang-fei", "unit-rebel", STEM]
COMPARE_LABELS = ["liu-bei", "guan-yu", "zhang-fei", "rebel", "shu-infantry"]
FIT_WIDTH_SCALE = 1.0
MIN_FRAME_HEIGHT = 0
FRAME = 313
DIRECTIONS = ("south", "east", "north", "west")
BASE_FRAMES_PER_DIRECTION = 16
@@ -186,7 +188,18 @@ def solidify_alpha(image: Image.Image) -> Image.Image:
return Image.fromarray(arr, "RGBA")
def grow_to_minimum_height(subject: Image.Image, max_width: int, max_height: int) -> Image.Image:
if MIN_FRAME_HEIGHT <= 0 or subject.height >= MIN_FRAME_HEIGHT:
return subject
grow = min(max_width / subject.width, max_height / subject.height, MIN_FRAME_HEIGHT / subject.height)
if grow <= 1:
return subject
size = (max(1, round(subject.width * grow)), max(1, round(subject.height * grow)))
return trim_to_alpha(solidify_alpha(subject.resize(size, 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)
if subject.width <= 1 or subject.height <= 1:
return Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
@@ -194,9 +207,11 @@ def fit_frame(source: Image.Image, max_width: int, max_height: int, bottom: int
size = (max(1, round(subject.width * scale)), max(1, round(subject.height * scale)))
subject = solidify_alpha(subject.resize(size, Image.Resampling.LANCZOS))
subject = trim_to_alpha(subject)
subject = grow_to_minimum_height(subject, max_width, max_height)
if not allow_detached_effects:
subject = keep_largest_component(subject)
subject = trim_to_alpha(subject)
subject = grow_to_minimum_height(subject, max_width, max_height)
frame = Image.new("RGBA", (FRAME, FRAME), (0, 0, 0, 0))
x = (FRAME - subject.width) // 2 + x_offset
y = bottom - subject.height
@@ -517,6 +532,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--sample-label", default=SAMPLE_LABEL)
parser.add_argument("--compare-stems", default=",".join(COMPARE_STEMS))
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)
return parser.parse_args()
@@ -524,6 +541,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
SOURCE = project_path(args.source)
WORK_DIR = project_path(args.work_dir)
@@ -537,6 +555,8 @@ def configure(args: argparse.Namespace) -> None:
SAMPLE_LABEL = args.sample_label
COMPARE_STEMS = split_csv(args.compare_stems)
COMPARE_LABELS = split_csv(args.compare_labels)
FIT_WIDTH_SCALE = args.fit_width_scale
MIN_FRAME_HEIGHT = args.min_frame_height
if len(COMPARE_STEMS) != len(COMPARE_LABELS):
raise ValueError("--compare-stems and --compare-labels must have the same number of entries")

View File

@@ -72,12 +72,12 @@ def write_text_block(draw: ImageDraw.ImageDraw, x: int, y: int, lines: list[str]
draw.text((x, y + index * 21), line, fill=fill, font=small)
def render_board() -> Path:
def render_board() -> Path | None:
manifest = load_json(MANIFEST_PATH)
audit = load_json(AUDIT_PATH)
pending = manifest.get("pendingApproval", [])
if not pending:
raise ValueError("no pendingApproval unit to render")
return None
stem = pending[0]
candidates = manifest.get("currentApprovalCandidate", {})
@@ -151,6 +151,9 @@ def render_board() -> Path:
def main() -> None:
path = render_board()
if path is None:
print("Skipped current approval board: no pendingApproval unit to render")
return
print(f"Wrote {path}")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 KiB

After

Width:  |  Height:  |  Size: 884 KiB