summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 19:03:22 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 19:03:22 +0800
commit588560deda6b701a62653dff19079dc0a21fa944 (patch)
tree3d734aaf9ad546ec52a42af0a2c2635d2d26d670 /scripts
parent5c67e85688ff1e6150b03501adae1a5d5e67bbdc (diff)
downloadliving-village-588560deda6b701a62653dff19079dc0a21fa944.tar.gz
p43: HUD 星露谷化切片 — 右上日历钟卡/体力条形图/底部10格hotbar(1-0选中+高亮+输入隔离)
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p43-hud.py130
-rw-r--r--scripts/make-p43-hud-closeup.py49
2 files changed, 179 insertions, 0 deletions
diff --git a/scripts/analyze-p43-hud.py b/scripts/analyze-p43-hud.py
new file mode 100644
index 0000000..ad0a859
--- /dev/null
+++ b/scripts/analyze-p43-hud.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+"""P43 HUD analysis: prove the new calendar card + energy bar + hotbar are real.
+
+Reads the `LV_P43_SHOT=1` frames and the shot log (which prints the layout rects
+computed by HudLayout in the running game), then writes
+docs/evidence/p43-analysis.txt with the measured numbers:
+
+ 1. calendar card / hotbar / slot0 rectangles exactly as the game laid them out,
+ all inside the 1280x720 viewport and non-overlapping;
+ 2. per-slot hotbar brightness (10 distinct cells) and the selected-slot delta;
+ 3. calendar-card and hotbar opacity is semi-transparent (never an opaque block).
+
+Usage:
+ python3 scripts/analyze-p43-hud.py <record-dir> <shot-log> <out-txt>
+
+Exit 0 when checks pass, 1 when they fail, 2 on usage/read error.
+"""
+
+import re
+import sys
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+WIDTH = 1280
+HEIGHT = 720
+SHOT_RE = re.compile(
+ r"p43-shot=(\S+) tick=(\d+) calendar=([\d,]+) hotbar=([\d,]+) slot0=([\d,]+) "
+ r"selected=(\d+) energy=([\d.]+) day=(\d+) season=(\S+) period=(\S+) clock=(\S+)"
+)
+
+
+def rect(text):
+ x, y, w, h = (int(v) for v in text.split(","))
+ return (x, y, w, h)
+
+
+def inside(name, r):
+ x, y, w, h = r
+ return x >= 0 and y >= 0 and x + w <= WIDTH and y + h <= HEIGHT
+
+
+def main(argv):
+ if len(argv) != 4:
+ sys.stderr.write(__doc__)
+ return 2
+ record_dir = Path(argv[1])
+ log_text = Path(argv[2]).read_text(encoding="utf-8")
+ shots = []
+ for line in log_text.splitlines():
+ m = SHOT_RE.search(line)
+ if m:
+ shots.append(
+ {
+ "name": m.group(1),
+ "tick": int(m.group(2)),
+ "calendar": rect(m.group(3)),
+ "hotbar": rect(m.group(4)),
+ "slot0": rect(m.group(5)),
+ "selected": int(m.group(6)),
+ "energy": float(m.group(7)),
+ "day": int(m.group(8)),
+ "season": m.group(9),
+ "period": m.group(10),
+ "clock": m.group(11),
+ }
+ )
+ if not shots:
+ sys.stderr.write("no p43-shot lines in log\n")
+ return 1
+
+ lines = ["P43 HUD analysis (LV_P43_SHOT=1)", ""]
+ failures = []
+ for shot in shots:
+ img = np.asarray(Image.open(record_dir / f"{shot['name']}.png").convert("RGBA"), dtype=np.float64)
+ lines.append(f"[{shot['name']}] tick={shot['tick']} day={shot['day']} season={shot['season']} "
+ f"period={shot['period']} clock={shot['clock']} energy={shot['energy']:.0f} selected={shot['selected']}")
+ cx, cy, cw, ch = shot["calendar"]
+ bx, by, bw, bh = shot["hotbar"]
+ sx, sy, sw, sh = shot["slot0"]
+ lines.append(f" calendar_rect = ({cx},{cy},{cw}x{ch}) hotbar_rect = ({bx},{by},{bw}x{bh}) slot0_rect = ({sx},{sy},{sw}x{sh})")
+ for name in ("calendar", "hotbar", "slot0"):
+ if not inside(name, shot[name]):
+ failures.append(f"{shot['name']} {name} out of viewport {shot[name]}")
+ # calendar must not overlap the top strip (y >= 39) and must sit left of hotbar band.
+ if cy < 39:
+ failures.append(f"{shot['name']} calendar overlaps top bar")
+
+ # 1) per-slot brightness across the 10 slots (distinct cells, selected delta).
+ slot_means = []
+ for index in range(10):
+ x = bx + 8 + index * (sw + 6)
+ y = by + 8
+ cell = img[y:y + sh, x:x + sw, :3].mean()
+ slot_means.append(cell)
+ sel = shot["selected"]
+ others = [v for i, v in enumerate(slot_means) if i != sel]
+ lines.append(" hotbar_slot_mean_luma = " + ", ".join(f"{v:.1f}" for v in slot_means))
+ lines.append(f" selected_slot={sel} selected_luma={slot_means[sel]:.1f} others_mean={np.mean(others):.1f} "
+ f"delta={slot_means[sel] - np.mean(others):+.1f}")
+
+ # 2) opacity: the calendar card and hotbar backdrops must be semi-transparent.
+ for name, r in (("calendar", shot["calendar"]), ("hotbar", shot["hotbar"])):
+ x, y, w, h = r
+ alpha = img[y:y + h, x:x + w, 3]
+ region = img[y:y + h, x:x + w, :3]
+ opaque_white = int(((alpha >= 250) & (region.min(axis=2) >= 250)).sum())
+ lines.append(f" {name}: alpha_min={int(alpha.min())} alpha_max={int(alpha.max())} "
+ f"mean_luma={region.mean():.1f} opaque_white_px={opaque_white}")
+ if opaque_white > 0:
+ failures.append(f"{shot['name']} {name} contains opaque white pixels")
+ if region.mean() < 8.0:
+ failures.append(f"{shot['name']} {name} too dark (possible big black block)")
+ lines.append("")
+
+ lines.append("verdict = " + ("PASS" if not failures else "FAIL"))
+ out_txt = Path(argv[3])
+ out_txt.parent.mkdir(parents=True, exist_ok=True)
+ out_txt.write_text("\n".join(lines) + "\n", encoding="utf-8")
+ sys.stdout.write("\n".join(lines) + "\n")
+ if failures:
+ for f in failures:
+ sys.stderr.write(f"FAIL {f}\n")
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))
diff --git a/scripts/make-p43-hud-closeup.py b/scripts/make-p43-hud-closeup.py
new file mode 100644
index 0000000..adcabca
--- /dev/null
+++ b/scripts/make-p43-hud-closeup.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+"""P43 evidence: NEAREST 2x close-up sheet of the two new HUD cards.
+
+Crops the top-right calendar/clock card and the bottom 10-slot hotbar from the
+`LV_P43_SHOT=1` day frame and stacks them at exactly 2x with NEAREST so pixel
+edges stay sharp. The sheet is a pure function of the input PNG (fixed crop
+boxes, fixed gap), no synthetic pixels.
+
+Usage:
+ python3 scripts/make-p43-hud-closeup.py <record-dir> <out-dir>
+"""
+
+import sys
+from pathlib import Path
+
+from PIL import Image
+
+SOURCE = "p43-hud-day.png"
+OUT_NAME = "p43-hud-closeup-2x.png"
+# (left, top, right, bottom) in the 1280x720 frame; rects from the p43-shot log.
+CALENDAR_BOX = (1124, 44, 1272, 102)
+HOTBAR_BOX = (398, 655, 882, 719)
+GAP = 12
+
+
+def main(argv):
+ if len(argv) != 3:
+ sys.stderr.write(__doc__)
+ return 2
+ record_dir = Path(argv[1])
+ out_dir = Path(argv[2])
+ out_dir.mkdir(parents=True, exist_ok=True)
+ source = Image.open(record_dir / SOURCE).convert("RGBA")
+ crops = [source.crop(CALENDAR_BOX), source.crop(HOTBAR_BOX)]
+ doubled = [c.resize((c.width * 2, c.height * 2), Image.NEAREST) for c in crops]
+ width = max(img.width for img in doubled)
+ height = sum(img.height for img in doubled) + GAP * (len(doubled) - 1)
+ sheet = Image.new("RGBA", (width, height), (0, 0, 0, 0))
+ y = 0
+ for img in doubled:
+ sheet.paste(img, (0, y))
+ y += img.height + GAP
+ sheet.save(out_dir / OUT_NAME)
+ print(f"ok {OUT_NAME} {sheet.width}x{sheet.height} from {SOURCE} boxes={[CALENDAR_BOX, HOTBAR_BOX]}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv))