220 lines
7.8 KiB
Python
220 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DOCS_DIR = ROOT / "docs"
|
|
MANIFEST_PATH = DOCS_DIR / "unit-sprite-rework-manifest.json"
|
|
AUDIT_PATH = DOCS_DIR / "unit-sprite-rework-audit.json"
|
|
MARKDOWN_PATH = DOCS_DIR / "unit-sprite-batch-queue.md"
|
|
JSON_PATH = DOCS_DIR / "unit-sprite-batch-queue.json"
|
|
|
|
|
|
BATCH_ORDER = [
|
|
"batch1-approved-core",
|
|
"batch1-approved-rebel",
|
|
"batch2-shu",
|
|
"batch3-wei",
|
|
"batch4-wu",
|
|
"batch5-rebel-other",
|
|
"batch6-nanman-special",
|
|
"batch7-unique-officer",
|
|
]
|
|
|
|
BATCH_LABELS = {
|
|
"batch1-approved-core": "Batch 1 approved core heroes",
|
|
"batch1-approved-rebel": "Batch 1 approved rebel references",
|
|
"batch2-shu": "Batch 2 Shu generic units",
|
|
"batch3-wei": "Batch 3 Wei generic units",
|
|
"batch4-wu": "Batch 4 Wu generic units",
|
|
"batch5-rebel-other": "Batch 5 remaining rebel/bandit units",
|
|
"batch6-nanman-special": "Batch 6 Nanman/special units",
|
|
"batch7-unique-officer": "Batch 7 unique officers",
|
|
}
|
|
|
|
SPEC_BY_STEM = {
|
|
"unit-shu-spearman": "unit-sprite-batch2-shu-spearman-spec.md",
|
|
"unit-shu-archer": "unit-sprite-batch2-shu-archer-spec.md",
|
|
"unit-shu-cavalry": "unit-sprite-batch2-shu-cavalry-spec.md",
|
|
"unit-shu-strategist": "unit-sprite-batch2-shu-strategist-spec.md",
|
|
"unit-shu-officer": "unit-sprite-batch2-shu-officer-spec.md",
|
|
}
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"missing required file: {path.relative_to(ROOT)}")
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def row_sort_key(row: dict[str, Any], manifest: dict[str, Any]) -> tuple[int, int, str]:
|
|
stem = row["stem"]
|
|
pending = manifest.get("pendingApproval", [])
|
|
next_after = manifest.get("nextAfterApproval", [])
|
|
if stem in pending:
|
|
return (0, pending.index(stem), stem)
|
|
if stem in next_after:
|
|
return (1, next_after.index(stem), stem)
|
|
if row.get("review_stage") == "approved-baseline":
|
|
return (3, 0, stem)
|
|
return (2, 0, stem)
|
|
|
|
|
|
def summarize_status(row: dict[str, Any]) -> str:
|
|
base = row["base"]["status"]
|
|
actions = row["actions"]["status"]
|
|
if base == "ok" and actions == "ok":
|
|
return "technical ok"
|
|
return f"base {base}; actions {actions}"
|
|
|
|
|
|
def queue_data(manifest: dict[str, Any], audit: dict[str, Any]) -> dict[str, Any]:
|
|
rows = audit.get("rows", [])
|
|
batches: list[dict[str, Any]] = []
|
|
pending = set(manifest.get("pendingApproval", []))
|
|
approved = set(manifest.get("approvedBaseline", []))
|
|
next_after = set(manifest.get("nextAfterApproval", []))
|
|
|
|
for batch in BATCH_ORDER:
|
|
batch_rows = [row for row in rows if row.get("batch") == batch]
|
|
batch_rows = sorted(batch_rows, key=lambda row: row_sort_key(row, manifest))
|
|
stage_counts = Counter(row.get("review_stage", "queued-rework") for row in batch_rows)
|
|
units: list[dict[str, Any]] = []
|
|
for row in batch_rows:
|
|
stem = row["stem"]
|
|
queue_role = "approved reference"
|
|
if stem in pending:
|
|
queue_role = "current approval sample"
|
|
elif stem in next_after:
|
|
queue_role = "next primary queue"
|
|
elif stem not in approved:
|
|
queue_role = "queued rework"
|
|
units.append(
|
|
{
|
|
"stem": stem,
|
|
"reviewStage": row.get("review_stage"),
|
|
"queueRole": queue_role,
|
|
"baseStatus": row["base"]["status"],
|
|
"actionStatus": row["actions"]["status"],
|
|
"battleBasePx": [
|
|
row["base"].get("battle_min_width_px"),
|
|
row["base"].get("battle_max_width_px"),
|
|
row["base"].get("battle_min_height_px"),
|
|
row["base"].get("battle_max_height_px"),
|
|
],
|
|
"spec": SPEC_BY_STEM.get(stem),
|
|
}
|
|
)
|
|
batches.append(
|
|
{
|
|
"key": batch,
|
|
"label": BATCH_LABELS.get(batch, batch),
|
|
"total": len(batch_rows),
|
|
"stageCounts": dict(sorted(stage_counts.items())),
|
|
"units": units,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"schema": 1,
|
|
"manifest": MANIFEST_PATH.relative_to(ROOT).as_posix(),
|
|
"audit": AUDIT_PATH.relative_to(ROOT).as_posix(),
|
|
"approvedBaseline": manifest.get("approvedBaseline", []),
|
|
"pendingApproval": manifest.get("pendingApproval", []),
|
|
"nextAfterApproval": manifest.get("nextAfterApproval", []),
|
|
"stageCounts": audit.get("stage_counts", {}),
|
|
"batches": batches,
|
|
}
|
|
|
|
|
|
def write_markdown(data: dict[str, Any]) -> None:
|
|
lines = [
|
|
"# Unit Sprite Batch Queue",
|
|
"",
|
|
"This file is generated by `scripts/write-unit-sprite-batch-queue.py`.",
|
|
"",
|
|
"Use this as the long-running queue for the full hand-painted sprite rework. It is inventory and sequencing, not approval by itself.",
|
|
"",
|
|
"## Current State",
|
|
"",
|
|
f"- Approved baseline units: {len(data['approvedBaseline'])}",
|
|
f"- Pending approval units: {len(data['pendingApproval'])}",
|
|
f"- Queued rework units: {data.get('stageCounts', {}).get('queued-rework', 0)}",
|
|
f"- Manifest: `{data['manifest']}`",
|
|
f"- Audit: `{data['audit']}`",
|
|
"",
|
|
]
|
|
|
|
pending = data["pendingApproval"]
|
|
if pending:
|
|
lines.extend(
|
|
[
|
|
"## Approval Blocker",
|
|
"",
|
|
"Do not start the next unit, commit, push, or deploy while these units are pending:",
|
|
"",
|
|
]
|
|
)
|
|
for stem in pending:
|
|
lines.append(f"- `{stem}`")
|
|
lines.append("")
|
|
|
|
lines.extend(["## Next After Approval", ""])
|
|
next_after = data["nextAfterApproval"]
|
|
if next_after:
|
|
for index, stem in enumerate(next_after, start=1):
|
|
spec = SPEC_BY_STEM.get(stem)
|
|
spec_note = f" - spec `{spec}`" if spec else ""
|
|
lines.append(f"{index}. `{stem}`{spec_note}")
|
|
else:
|
|
lines.append("No next unit is currently defined.")
|
|
lines.append("")
|
|
|
|
lines.extend(["## Batch Queue", ""])
|
|
for batch in data["batches"]:
|
|
lines.extend(
|
|
[
|
|
f"### {batch['label']}",
|
|
"",
|
|
f"- Batch key: `{batch['key']}`",
|
|
f"- Total units: {batch['total']}",
|
|
f"- Stage counts: {', '.join(f'{key} {value}' for key, value in batch['stageCounts'].items()) or 'none'}",
|
|
"",
|
|
"| unit | role | stage | status | battle base px | spec |",
|
|
"| --- | --- | --- | --- | --- | --- |",
|
|
]
|
|
)
|
|
for unit in batch["units"]:
|
|
battle = unit["battleBasePx"]
|
|
battle_text = f"{battle[0]}..{battle[1]} x {battle[2]}..{battle[3]}"
|
|
spec = f"`{unit['spec']}`" if unit["spec"] else ""
|
|
status = (
|
|
"technical ok"
|
|
if unit["baseStatus"] == "ok" and unit["actionStatus"] == "ok"
|
|
else f"base {unit['baseStatus']}; actions {unit['actionStatus']}"
|
|
)
|
|
lines.append(
|
|
f"| `{unit['stem']}` | {unit['queueRole']} | {unit['reviewStage']} | {status} | {battle_text} | {spec} |"
|
|
)
|
|
lines.append("")
|
|
|
|
MARKDOWN_PATH.write_text("\n".join(lines), encoding="utf-8")
|
|
|
|
|
|
def main() -> None:
|
|
manifest = load_json(MANIFEST_PATH)
|
|
audit = load_json(AUDIT_PATH)
|
|
data = queue_data(manifest, audit)
|
|
JSON_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
write_markdown(data)
|
|
print(f"Wrote {JSON_PATH}")
|
|
print(f"Wrote {MARKDOWN_PATH}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|