diff --git a/design/qa/t5-skeleton-preview-20260929/0-目检清单.md b/design/qa/t5-skeleton-preview-20260929/0-目检清单.md new file mode 100644 index 00000000..62ade7b9 --- /dev/null +++ b/design/qa/t5-skeleton-preview-20260929/0-目检清单.md @@ -0,0 +1,19 @@ +# T5 骨架 15 张目检清单(U2 人闸第三件,2026-09-29) + +数据源=game/data/tables/level.tres;逐张给「过/不过+原因」,回填记台账。 + +- [ ] L1-模板0-地精洞窟主线洞道现行.png(地精洞窟·主线洞道(现行)) +- [ ] L1-模板1-地精洞窟休整前移.png(地精洞窟·休整前移) +- [ ] L1-模板2-地精洞窟先赏后战.png(地精洞窟·先赏后战) +- [ ] L1-模板3-地精洞窟连战直入.png(地精洞窟·连战直入) +- [ ] L1-模板4-地精洞窟缓入长坡.png(地精洞窟·缓入长坡) +- [ ] L2-模板0-幽暗地域四岔环廊现行.png(幽暗地域·四岔环廊(现行)) +- [ ] L2-模板1-幽暗地域奖励窄门.png(幽暗地域·奖励窄门) +- [ ] L2-模板2-幽暗地域直入深层.png(幽暗地域·直入深层) +- [ ] L2-模板3-幽暗地域休整远路.png(幽暗地域·休整远路) +- [ ] L2-模板4-幽暗地域双线归束.png(幽暗地域·双线归束) +- [ ] L3-模板0-灰矮人矿城主竖井现行.png(灰矮人矿城·主竖井(现行)) +- [ ] L3-模板1-灰矮人矿城奖励窄门.png(灰矮人矿城·奖励窄门) +- [ ] L3-模板2-灰矮人矿城直入深层.png(灰矮人矿城·直入深层) +- [ ] L3-模板3-灰矮人矿城休整远路.png(灰矮人矿城·休整远路) +- [ ] L3-模板4-灰矮人矿城双线归束.png(灰矮人矿城·双线归束) diff --git a/design/qa/t5-skeleton-preview-20260929/00-总览拼图.png b/design/qa/t5-skeleton-preview-20260929/00-总览拼图.png new file mode 100644 index 00000000..12ab4cc3 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/00-总览拼图.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L1-模板0-地精洞窟主线洞道现行.png b/design/qa/t5-skeleton-preview-20260929/L1-模板0-地精洞窟主线洞道现行.png new file mode 100644 index 00000000..9c8f21d3 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L1-模板0-地精洞窟主线洞道现行.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L1-模板1-地精洞窟休整前移.png b/design/qa/t5-skeleton-preview-20260929/L1-模板1-地精洞窟休整前移.png new file mode 100644 index 00000000..859a6cb9 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L1-模板1-地精洞窟休整前移.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L1-模板2-地精洞窟先赏后战.png b/design/qa/t5-skeleton-preview-20260929/L1-模板2-地精洞窟先赏后战.png new file mode 100644 index 00000000..45464ccd Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L1-模板2-地精洞窟先赏后战.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L1-模板3-地精洞窟连战直入.png b/design/qa/t5-skeleton-preview-20260929/L1-模板3-地精洞窟连战直入.png new file mode 100644 index 00000000..90d8874d Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L1-模板3-地精洞窟连战直入.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L1-模板4-地精洞窟缓入长坡.png b/design/qa/t5-skeleton-preview-20260929/L1-模板4-地精洞窟缓入长坡.png new file mode 100644 index 00000000..71f99aa6 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L1-模板4-地精洞窟缓入长坡.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L2-模板0-幽暗地域四岔环廊现行.png b/design/qa/t5-skeleton-preview-20260929/L2-模板0-幽暗地域四岔环廊现行.png new file mode 100644 index 00000000..c7ae52d3 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L2-模板0-幽暗地域四岔环廊现行.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L2-模板1-幽暗地域奖励窄门.png b/design/qa/t5-skeleton-preview-20260929/L2-模板1-幽暗地域奖励窄门.png new file mode 100644 index 00000000..d4e3f7b8 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L2-模板1-幽暗地域奖励窄门.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L2-模板2-幽暗地域直入深层.png b/design/qa/t5-skeleton-preview-20260929/L2-模板2-幽暗地域直入深层.png new file mode 100644 index 00000000..dc1320c8 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L2-模板2-幽暗地域直入深层.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L2-模板3-幽暗地域休整远路.png b/design/qa/t5-skeleton-preview-20260929/L2-模板3-幽暗地域休整远路.png new file mode 100644 index 00000000..82e90795 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L2-模板3-幽暗地域休整远路.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L2-模板4-幽暗地域双线归束.png b/design/qa/t5-skeleton-preview-20260929/L2-模板4-幽暗地域双线归束.png new file mode 100644 index 00000000..546b3832 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L2-模板4-幽暗地域双线归束.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L3-模板0-灰矮人矿城主竖井现行.png b/design/qa/t5-skeleton-preview-20260929/L3-模板0-灰矮人矿城主竖井现行.png new file mode 100644 index 00000000..2e2e59ea Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L3-模板0-灰矮人矿城主竖井现行.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L3-模板1-灰矮人矿城奖励窄门.png b/design/qa/t5-skeleton-preview-20260929/L3-模板1-灰矮人矿城奖励窄门.png new file mode 100644 index 00000000..44dd97d1 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L3-模板1-灰矮人矿城奖励窄门.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L3-模板2-灰矮人矿城直入深层.png b/design/qa/t5-skeleton-preview-20260929/L3-模板2-灰矮人矿城直入深层.png new file mode 100644 index 00000000..15cee3fe Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L3-模板2-灰矮人矿城直入深层.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L3-模板3-灰矮人矿城休整远路.png b/design/qa/t5-skeleton-preview-20260929/L3-模板3-灰矮人矿城休整远路.png new file mode 100644 index 00000000..28f93e21 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L3-模板3-灰矮人矿城休整远路.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/L3-模板4-灰矮人矿城双线归束.png b/design/qa/t5-skeleton-preview-20260929/L3-模板4-灰矮人矿城双线归束.png new file mode 100644 index 00000000..a8895881 Binary files /dev/null and b/design/qa/t5-skeleton-preview-20260929/L3-模板4-灰矮人矿城双线归束.png differ diff --git a/design/qa/t5-skeleton-preview-20260929/render_preview.py b/design/qa/t5-skeleton-preview-20260929/render_preview.py new file mode 100644 index 00000000..1bb6017d --- /dev/null +++ b/design/qa/t5-skeleton-preview-20260929/render_preview.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""T5 节点图骨架 15 张一次性目检预览(M-3 迭代 5 U2 人闸第三件,2026-09-29)。 + +数据源 = game/data/tables/level.tres(游戏内实际落表,非生成器重算)。 +产物 = 本目录 15 张单图 + 00-总览拼图.png + 0-目检清单.md。一次性工具,随产物留档可删。 +用法:python design/qa/t5-skeleton-preview-20260929/render_preview.py(仓库根执行) +""" +import json +import math +import os +import re +from PIL import Image, ImageDraw, ImageFont + +ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..")) +TRES = os.path.join(ROOT, "game", "data", "tables", "level.tres") +FONT = os.path.join(ROOT, "game", "assets", "fonts", "ui_font_fusion_pixel_zh_hans.ttf") +OUT = os.path.dirname(os.path.abspath(__file__)) + +BG = (30, 27, 34) +INK = (240, 234, 220) +DIM = (150, 142, 132) +EDGE = (168, 158, 142) +TYPE_STYLE = { + "battle": {"label": "战斗", "fill": (62, 86, 113), "border": (120, 148, 180)}, + "reward": {"label": "奖励", "fill": (176, 127, 44), "border": (222, 178, 96)}, + "rest": {"label": "休整", "fill": (74, 124, 89), "border": (128, 186, 148)}, + "shop": {"label": "商店", "fill": (58, 110, 140), "border": (118, 172, 200)}, + "boss": {"label": "Boss", "fill": (122, 44, 72), "border": (226, 120, 150)}, +} + + +def parse_rows(path): + text = open(path, encoding="utf-8").read() + body = text.split("rows = [", 1)[1].rsplit("\n]", 1)[0] + rows = [] + depth, start = 0, None + for i, ch in enumerate(body): + if ch == "{": + if depth == 0: + start = i + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0 and start is not None: + rows.append(json.loads(body[start:i + 1])) + start = None + return rows + + +def f(size): + return ImageFont.truetype(FONT, size) + + +def arrow(d, p0, p1, color, w=3): + d.line([p0, p1], fill=color, width=w) + ang = math.atan2(p1[1] - p0[1], p1[0] - p0[0]) + L, W = 14, 5 + a1 = (p1[0] - L * math.cos(ang) + W * math.sin(ang), p1[1] - L * math.sin(ang) - W * math.cos(ang)) + a2 = (p1[0] - L * math.cos(ang) - W * math.sin(ang), p1[1] - L * math.sin(ang) + W * math.cos(ang)) + d.polygon([p1, a1, a2], fill=color) + + +def polyline_arrow(d, pts, color, w=3): + d.line(pts, fill=color, width=w) + ang = math.atan2(pts[-1][1] - pts[-2][1], pts[-1][0] - pts[-2][0]) + L, W = 14, 5 + a1 = (pts[-1][0] - L * math.cos(ang) + W * math.sin(ang), pts[-1][1] - L * math.sin(ang) - W * math.cos(ang)) + a2 = (pts[-1][0] - L * math.cos(ang) - W * math.sin(ang), pts[-1][1] - L * math.sin(ang) + W * math.cos(ang)) + d.polygon([pts[-1], a1, a2], fill=color) + + +def node_box(d, cx, cy, ntype, depth, big=False): + st = TYPE_STYLE[ntype] + w, h = (132, 72) if big else (112, 60) + x0, y0, x1, y1 = cx - w // 2, cy - h // 2, cx + w // 2, cy + h // 2 + d.rounded_rectangle([x0, y0, x1, y1], radius=10, fill=st["fill"], + outline=st["border"], width=4 if big else 2) + label = st["label"] + fnt = f(24 if big else 21) + bb = d.textbbox((0, 0), label, font=fnt) + d.text((cx - (bb[2] - bb[0]) // 2, cy - (bb[3] - bb[1]) // 2 - 4), label, font=fnt, fill=INK) + if depth is not None: + tag = "#%d" % depth + fnt2 = f(15) + bb2 = d.textbbox((0, 0), tag, font=fnt2) + tw, th = bb2[2] - bb2[0], bb2[3] - bb2[1] + bx, by = x0 + 2, y0 - 12 + d.rounded_rectangle([bx, by, bx + tw + 12, by + th + 8], radius=7, fill=BG, + outline=st["border"], width=1) + d.text((bx + 6, by + 4), tag, font=fnt2, fill=INK) + return (x0, y0, x1, y1) + + +def render_chain(name, level_name, nodes, path): + W, H = 1280, 320 + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + d.text((24, 18), "%s — %s" % (level_name, name), font=f(30), fill=INK) + d.text((24, 64), "单链:自左向右依次推进|#N = 遭遇表行(战斗深度序)|入口 = 最左", font=f(17), fill=DIM) + n = len(nodes) + xs = [140 + i * ((W - 280) // (n - 1)) for i in range(n)] + cy = 190 + boxes = [node_box(d, xs[i], cy, nd["type"], nd.get("node_depth"), + big=nd["type"] == "boss") for i, nd in enumerate(nodes)] + for i in range(n - 1): + arrow(d, (boxes[i][2] + 2, cy), (boxes[i + 1][0] - 3, cy), EDGE) + d.text((xs[0] - 62, cy + 46), "▶ 入口", font=f(17), fill=DIM) + img.save(path) + + +def render_graph(name, level_name, nodes, path): + ids = [nd["node_depth"] for nd in nodes] + inbound = {} + for nd in nodes: + for t in nd.get("next", []): + inbound.setdefault(t, []).append(nd["node_depth"]) + layer = {} + + def lay(i): + if i in layer: + return layer[i] + layer[i] = 0 if i not in inbound else max(lay(p) for p in inbound[i]) + 1 + return layer[i] + + for i in ids: + lay(i) + maxl = max(layer.values()) + W, H = 1280, 480 + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + d.text((24, 18), "%s — %s" % (level_name, name), font=f(30), fill=INK) + d.text((24, 64), "网状:箭头=可走的下一节点|#N = 遭遇表行(战斗深度)|入口 = 最左|商店→Boss 必经", + font=f(17), fill=DIM) + cols = {} + for i in sorted(ids, key=lambda x: (layer[x], x)): + cols.setdefault(layer[i], []).append(i) + xs = {l: 150 + l * ((W - 300) / max(maxl, 1)) for l in cols} + pos = {} + for l, members in cols.items(): + k = len(members) + for j, i in enumerate(members): + y = 280 + (j - (k - 1) / 2.0) * (120 if k > 1 else 0) + pos[i] = (int(xs[l]), int(y)) + for nd in nodes: + i = nd["node_depth"] + for t in nd.get("next", []): + (x0, y0), (x1, y1) = pos[i], pos[t] + if x1 - x0 >= 120: + arrow(d, (x0 + 60, y0), (x1 - 62, y1), EDGE, w=3) + else: + bow = 40 if y1 >= y0 else -40 + pts = [(x0 + (x1 - x0) * u / 20.0, + y0 + (y1 - y0) * u / 20.0 + bow * math.sin(math.pi * u / 20.0)) + for u in range(21)] + polyline_arrow(d, pts, EDGE) + entry = [i for i in ids if i not in inbound][0] + for nd in nodes: + i = nd["node_depth"] + node_box(d, pos[i][0], pos[i][1], nd["type"], i, big=nd["type"] == "boss") + if i == entry: + d.text((pos[i][0] - 32, pos[i][1] + 42), "▶ 入口", font=f(17), fill=DIM) + img.save(path) + + +def main(): + rows = parse_rows(TRES) + assert len(rows) == 3, "rows=%d" % len(rows) + files = [] + for row in rows: + li = row["level_index"] + for ti, tpl in enumerate(row["templates"]): + nodes = tpl["nodes"] + fn = "L%d-模板%d-%s.png" % (li, ti, re.sub(r"[\\/:*?\"<>|()·]", "", tpl["display_name"])) + path = os.path.join(OUT, fn) + is_graph = any("next" in n for n in nodes) + if is_graph: + render_graph(tpl["display_name"], row["display_name"], nodes, path) + else: + render_chain(tpl["display_name"], row["display_name"], nodes, path) + files.append((li, ti, tpl["display_name"], fn)) + assert len(files) == 15, len(files) + CW, CH = 640, 190 + sheet = Image.new("RGB", (CW, 40 + CH * 3), BG) + sd = ImageDraw.Draw(sheet) + sd.text((16, 8), "T5 骨架池总览(15 张 = 3 关 × 5;逐张单图见同目录)", font=f(22), fill=INK) + for li, ti, name, fn in files: + im = Image.open(os.path.join(OUT, fn)).resize((CW - 8, CH - 8)) + sheet.paste(im, (4, 36 + (li - 1) * CH)) + sheet.save(os.path.join(OUT, "00-总览拼图.png")) + body = "\n".join("- [ ] %s(%s)" % (fn, name) for li, ti, name, fn in files) + open(os.path.join(OUT, "0-目检清单.md"), "w", encoding="utf-8").write( + "# T5 骨架 15 张目检清单(U2 人闸第三件,2026-09-29)\n\n" + "数据源=game/data/tables/level.tres;逐张给「过/不过+原因」,回填记台账。\n\n" + body + "\n") + print("rendered %d files + sheet" % len(files)) + for li, ti, name, fn in files: + print(" L%d-模板%d %s -> %s" % (li, ti, name, fn)) + + +if __name__ == "__main__": + main()