|
|
@@ -91,6 +91,124 @@ def _math_report_md(slot_config):
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
|
|
|
+def _image_alpha_stats(path):
|
|
|
+ try:
|
|
|
+ from PIL import Image
|
|
|
+ with Image.open(path) as img:
|
|
|
+ rgba = img.convert("RGBA")
|
|
|
+ alpha = rgba.getchannel("A")
|
|
|
+ bbox = alpha.getbbox()
|
|
|
+ if not bbox:
|
|
|
+ return {"ok": False, "reason": "empty_alpha", "size": rgba.size}
|
|
|
+ pix = list(alpha.getdata())
|
|
|
+ transparent = sum(1 for v in pix if v == 0)
|
|
|
+ opaque = sum(1 for v in pix if v > 16)
|
|
|
+ area = rgba.width * rgba.height
|
|
|
+ return {
|
|
|
+ "ok": True,
|
|
|
+ "size": rgba.size,
|
|
|
+ "bbox": bbox,
|
|
|
+ "transparentRatio": round(transparent / max(1, area), 4),
|
|
|
+ "opaqueRatio": round(opaque / max(1, area), 4),
|
|
|
+ "bboxRatio": round(((bbox[2] - bbox[0]) * (bbox[3] - bbox[1])) / max(1, area), 4),
|
|
|
+ }
|
|
|
+ except Exception as e:
|
|
|
+ return {"ok": False, "reason": str(e), "size": [0, 0]}
|
|
|
+
|
|
|
+
|
|
|
+def _qa_add(report, level, code, message, target="", suggestion=""):
|
|
|
+ report["items"].append({
|
|
|
+ "level": level,
|
|
|
+ "code": code,
|
|
|
+ "target": target,
|
|
|
+ "message": message,
|
|
|
+ "suggestion": suggestion,
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+def _qa_report(lib, base, slot_src):
|
|
|
+ slot_config = lib.get("slot_config") or {}
|
|
|
+ report = {
|
|
|
+ "game": lib.get("game"),
|
|
|
+ "summary": {"error": 0, "warning": 0, "info": 0},
|
|
|
+ "items": [],
|
|
|
+ }
|
|
|
+ chars = {c.get("id"): c for c in lib.get("characters", [])}
|
|
|
+ ui_art = {a.get("id"): a for a in lib.get("ui_art", [])}
|
|
|
+ required_art = ["bg_main", "logo", "reel_frame", "btn_spin", "hud_pill"]
|
|
|
+ for aid in required_art:
|
|
|
+ path = os.path.join(base, "ui_art", f"{aid}.png")
|
|
|
+ if aid not in ui_art or not os.path.isfile(path):
|
|
|
+ _qa_add(report, "error", "missing_ui_art", f"关键 UI 资源缺失:{aid}", aid, "在 UI 美术任务卡片里补生成或重新生成。")
|
|
|
+
|
|
|
+ logo_path = os.path.join(base, "ui_art", "logo.png")
|
|
|
+ if os.path.isfile(logo_path):
|
|
|
+ st = _image_alpha_stats(logo_path)
|
|
|
+ if not st.get("ok"):
|
|
|
+ _qa_add(report, "error", "bad_logo_alpha", "Logo 图片没有有效 Alpha 内容", "logo", "重新生成 logo。")
|
|
|
+ elif st["opaqueRatio"] < 0.03:
|
|
|
+ _qa_add(report, "warning", "logo_too_empty", "Logo 有效像素很少,可能是空框或文字缺失。", "logo", "重新生成 logo,并要求真实可读标题文字。")
|
|
|
+
|
|
|
+ frame_path = os.path.join(base, "ui_art", "reel_frame.png")
|
|
|
+ if os.path.isfile(frame_path):
|
|
|
+ st = _image_alpha_stats(frame_path)
|
|
|
+ if st.get("ok") and st["transparentRatio"] < 0.25:
|
|
|
+ _qa_add(report, "warning", "reel_frame_not_hollow", "卷轴框透明区域偏少,可能不是空心框。", "reel_frame", "重新生成空心透明中心的 reel_frame。")
|
|
|
+
|
|
|
+ boss = slot_config.get("boss") or {}
|
|
|
+ if boss.get("enabled"):
|
|
|
+ boss_id = boss.get("id") or "boss_demon_lord"
|
|
|
+ row = chars.get(boss_id)
|
|
|
+ if not row:
|
|
|
+ _qa_add(report, "error", "missing_boss", f"关主资源缺失:{boss_id}", boss_id, "在角色库补生成关主。")
|
|
|
+ else:
|
|
|
+ preview = row.get("preview")
|
|
|
+ if not preview or not os.path.isfile(os.path.join(base, preview)):
|
|
|
+ _qa_add(report, "warning", "missing_boss_preview", "关主缺少完整预览图,页面会误把 atlas 当角色。", boss_id, "重新生成关主,或让系统补完整 preview。")
|
|
|
+ if row.get("type") != "spine_parts":
|
|
|
+ _qa_add(report, "warning", "boss_not_parts", "关主不是 spine_parts,爆炸拆件动作会受限。", boss_id, "重新生成关主拆件。")
|
|
|
+ theme = (slot_config.get("theme") or {}).get("key", "")
|
|
|
+ title = boss.get("title", "")
|
|
|
+ if theme in ("jelly", "pirate_jelly") and ("恶魔" in title or "魔王" in title):
|
|
|
+ _qa_add(report, "warning", "boss_theme_mismatch", "关主名称/设定仍偏暗黑,可能不贴合当前糖果主题。", boss_id, "点击重新生成,使用主题化 boss prompt。")
|
|
|
+
|
|
|
+ layout = slot_config.get("layout") or {}
|
|
|
+ reel = layout.get("reel") or {}
|
|
|
+ rows = int(reel.get("rows") or 0)
|
|
|
+ cols = int(reel.get("cols") or 0)
|
|
|
+ if rows and cols and rows <= 3:
|
|
|
+ _qa_add(report, "info", "compact_reel_layout", "低行数盘面已使用紧凑布局,避免中间空行过大。", "SlotGame.ts")
|
|
|
+ if "fillColor = new Color(255, 255, 255, 0)" not in slot_src:
|
|
|
+ _qa_add(report, "warning", "symbol_card_background", "符号格子仍可能有白色底板。", "SlotGame.ts", "检查 buildGrid,格子底板应为透明或极弱描边。")
|
|
|
+ if "BOSS_CONFIG.logoSide" not in slot_src:
|
|
|
+ _qa_add(report, "warning", "boss_logo_position", "关主没有按 logo 旁边定位,可能挤进盘面。", "SlotGame.ts", "检查 buildBoss 的位置。")
|
|
|
+
|
|
|
+ for item in report["items"]:
|
|
|
+ report["summary"][item["level"]] += 1
|
|
|
+ return report
|
|
|
+
|
|
|
+
|
|
|
+def _qa_report_md(report):
|
|
|
+ lines = [
|
|
|
+ "# Asset QA Report",
|
|
|
+ "",
|
|
|
+ f"- Game: `{report.get('game')}`",
|
|
|
+ f"- Errors: `{report['summary']['error']}`",
|
|
|
+ f"- Warnings: `{report['summary']['warning']}`",
|
|
|
+ f"- Info: `{report['summary']['info']}`",
|
|
|
+ "",
|
|
|
+ "## Findings",
|
|
|
+ ]
|
|
|
+ if not report["items"]:
|
|
|
+ lines.append("- No QA findings.")
|
|
|
+ for item in report["items"]:
|
|
|
+ lines.append(
|
|
|
+ f"- **{item['level'].upper()}** `{item['code']}` `{item.get('target','')}`: "
|
|
|
+ f"{item['message']}" + (f" 建议:{item['suggestion']}" if item.get("suggestion") else "")
|
|
|
+ )
|
|
|
+ return "\n".join(lines) + "\n"
|
|
|
+
|
|
|
+
|
|
|
# ---------------------------------------------------------------- 粒子贴图
|
|
|
def _write_particle_png(path):
|
|
|
"""生成一张柔光圆点透明 PNG(粒子配置引用的 particle.png)。"""
|
|
|
@@ -699,6 +817,18 @@ def export(game, out_root, log=print):
|
|
|
.replace("__SYMBOL_FIT__", json.dumps(symbol_fit, ensure_ascii=False))
|
|
|
.replace("__SYMBOL_FIT_DEFAULT__", json.dumps(symbol_fit_default, ensure_ascii=False)))
|
|
|
open(os.path.join(scripts, "SlotGame.ts"), "w", encoding="utf-8").write(slot)
|
|
|
+ qa = _qa_report(lib, base, slot_src)
|
|
|
+ open(os.path.join(pack, "QAReport.json"), "w", encoding="utf-8").write(
|
|
|
+ json.dumps(qa, ensure_ascii=False, indent=2)
|
|
|
+ )
|
|
|
+ open(os.path.join(pack, "QAReport.md"), "w", encoding="utf-8").write(_qa_report_md(qa))
|
|
|
+ if qa["summary"]["error"] or qa["summary"]["warning"]:
|
|
|
+ log(f"🧪 QA:{qa['summary']['error']} 个错误,{qa['summary']['warning']} 个警告;详见 QAReport.md")
|
|
|
+ for item in qa["items"][:6]:
|
|
|
+ if item["level"] in ("error", "warning"):
|
|
|
+ log(f"🧪 [{item['level']}] {item['target']} · {item['message']}")
|
|
|
+ else:
|
|
|
+ log("🧪 QA:通过,未发现错误或警告")
|
|
|
src_tween = os.path.join(base, "ui", "TweenPresets.ts")
|
|
|
if os.path.isfile(src_tween):
|
|
|
shutil.copy2(src_tween, os.path.join(scripts, "TweenPresets.ts"))
|