merge: U2人闸裁断登记——三推翻(a完美L1裁甲新杠杆/b裁C1两刀/c裁AC v1硬梯度)+两追认(d精英解读A/e断言钉模板0);15骨架目检全不过立返工mini-loop(计划书.zcode/plans待批);06四条目重开+完美L1新杠杆条目;裁断记录入design/qa

This commit is contained in:
DKChan committed 2026-09-29 02:47:12 +08:00
commit 700f2ff6cd
20 files changed
+285 -4

No files matched your search

+12 -4
View File
@@ -75,7 +75,7 @@
**归属**:怪物/敌人系统设定批投值(排在验收读数重测后、与曲线调参同批),[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md)联动复算
**状态**:已投值(2026-09-28 M-3 迭代 5 T4——Boss-only 终版:杂鱼全 0·Boss 台阶 8/9/12;v1/v2 梯度证普通 L2 与 AC 不可兼得,17.1% 越下带挂账列 U2 追认三选;生存链复算+L2 池复验+三版对照读数见 design/qa/m3-iter5-t4-ac-2026-09-28.md;本条收口移除)
**状态**:**重开——U2 推翻重投**(2026-09-29 用户裁 v1 硬梯度:重击手 8/重装手 9、Boss 台阶 8/10/12;2026-09-28 落表的 Boss-only 终版〔杂鱼全 0·Boss 8/9/12〕退役,17.1% 挂账随之作废)。⚠ v1 旧读数(普通 L2 14.3%)测于 C3 锁值之上,用户新裁组合=**C1 两刀+v1 AC 无测量**——重投+小样 20 局+全量确认归迭代 5 返工批(`.zcode/plans/m3-iter5-rework-mini-loop-2026-09-29.md` T-A)。精英半边口径(U2 追认 A,代拟兼容读法):重击手/重装手 8/9 系逐行手设值、不新设精英编码位,P2 真精英行另启用。T4 三版对照与生存链复算记录见 design/qa/m3-iter5-t4-ac-2026-09-28.md,U2 裁断全录见 design/qa/m3-iter5-u2-gate-2026-09-29.md
#### 技能池情境条件触发器
@@ -211,7 +211,7 @@
**归属**:难度曲线与Boss调参批
**状态**:已部分收口(2026-09-28 M-3 迭代 5 换算率锚复验履行——6/9/1 campaign 段驱动正常无断链;重标归出口判读,见 design/qa/m3-iter5-lever-scan-2026-09-28.md §三④)
**状态**:已部分收口(2026-09-28 M-3 迭代 5 换算率锚复验履行——6/9/1 campaign 段驱动正常无断链;U2 后 L2 难度再变〔C1+v1〕,重标判读改道归迭代 5 返工批 T-A 读数到账后,见 design/qa/m3-iter5-u2-gate-2026-09-29.md)
#### 关卡内结构随机生成(哈迪斯 / 哥布林弹珠式布局变化)
@@ -219,7 +219,7 @@
**归属**:[关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(设计 owner;行集内容责任方仍为[冒险结构](systems/冒险循环/冒险结构.md),行进与图合法性校验归[关卡节点地图](systems/冒险循环/关卡节点地图.md))
**状态**:已实施(2026-09-28 M-3 迭代 5 T5 台阶一落地——15 张骨架+node_map 扩签+首次冒险固定脚本,文档 v0.5;台阶二/三仍后置;本条收口移除,去向=迭代 5 台账与文档 v0.5 版本历史)
**状态**:**重开——U2 目检推翻现池**(2026-09-29 用户逐张目检 15 张:L1 五张全不过——新规则**奖励/休整不得出现在前三个节点**〔现池五张全违反〕;L2/L3 十张全不过——分支过窄,新规则 **BFS 同深度层节点数上限放宽到 5**〔现池层宽最大 3〕,新模板应用足宽度;边合法性/必经位/可达性面无问题)。池重生成+模板 0 处置〔随新规则重生成 vs 回滚豁免,待批〕+8 处钉固断言期望随动+预览图重渲染二轮目检归迭代 5 返工批(`.zcode/plans/m3-iter5-rework-mini-loop-2026-09-29.md` T-C)。目检产物与判定 design/qa/t5-skeleton-preview-20260929/,裁断全录 design/qa/m3-iter5-u2-gate-2026-09-29.md
### 难度曲线与Boss
@@ -237,7 +237,15 @@
**归属**:难度曲线与Boss调参批,扫描执行与[棋盘生成](systems/排雷循环/棋盘生成.md)工具交付联动
**状态**:已收口移除(2026-09-28 M-3 迭代 5:基线重测+杠杆网格扫描执行完毕——C3 组合锁值收带〔普通 L2 31.2%/完美 L2 56.6% 入带〕;完美 L1 89% 全网格不可达按用户裁丙挂账;L2 Boss 承伤残余 46.0% 已回落带内。去向=design/qa/m3-iter5-baseline 与 m3-iter5-lever-scan 两报告+迭代 5 台账;下游挂账〔完美 L1 已裁丙、普通 L2 17.1% AC 挂账〕归迭代 5 出口人闸追认清单)
**状态**:**重开——U2 两裁推翻锁值**(2026-09-29:b 项推翻 C3 四件组合裁 **C1 两刀**〔岩浆 effect_value 2→1+逐层变异基数 2→0;变异占比维持 5%、卓尔血维持 130〕——已落表的占比 4→5、卓尔 100→130 两行回退;a 项推翻丙裁 **甲=新杠杆授权**〔完美 L1 89% 挂账废止,见新条目「完美 L1 新杠杆授权」〕)。C1+v1 AC 组合全量确认与 r_boss 复算(回退后 ≈0.33 仍低于域)、L3 密度重算归返工批 T-A。原扫描记录见 design/qa/m3-iter5-lever-scan-2026-09-28.md,U2 裁断全录见 design/qa/m3-iter5-u2-gate-2026-09-29.md
#### 完美 L1 新杠杆授权(U2 裁甲,2026-09-29 立项)
**现状**:完美档 L1 条件通关率 85–89%,越带 [35,65] 上缘 ~20 点;杠杆网格扫描证明全注册杠杆集不可达(L1 定向杠杆全弱:荆棘 +1 最多 −7pp、食人魔酋长加血对完美无感〔L1 Boss 胜率 98%〕、群战上探被校验域封死;全部打开下限 81%)——强势来源=10×10 板形×完美画像挖掘效率×奖励经济学,不在已注册旋钮量程内。**2026-09-28 用户曾裁丙挂账(上界仪不判),2026-09-29 U2 推翻丙改裁甲=新杠杆授权**。两子路候选(未选):① 敌方攻击基线上探(acceptance 文档 f① 候选、未入杠杆注册表——注意会连坐普通档 L1,其落带余量仅 ~15 点);② L1 专属加难内容(全新杠杆,须走杠杆注册)。子路选择+设计提案+扫描复测待批归返工批 T-B。
**归属**:迭代 5 返工批 T-B(`.zcode/plans/m3-iter5-rework-mini-loop-2026-09-29.md`),[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md)
**状态**:待办(子路二选一待批——设计与提案交用户裁)
#### L2 Boss 承伤微越带与完美档压带迭代(现役口径)
+41
View File
@@ -0,0 +1,41 @@
# M-3 迭代 5 出口人闸(U2)裁断记录
- **日期**:2026-09-29(裁断会话——只做裁断与登记,零代码零数值表改动)
- **裁断人**:用户(本会话逐项给裁决)
- **事实源**:design/qa/m3-iter5-baseline-2026-09-28.md / m3-iter5-lever-scan-2026-09-28.md / m3-iter5-t4-ac-2026-09-28.md + 台账 230-244
- **裁断形态**:**迭代 5 出口不闭**——五项中三项推翻,15 张骨架目检不过;按用户指令立返工 mini-loop(`.zcode/plans/m3-iter5-rework-mini-loop-2026-09-29.md`),不擅自改数值
## 一、五项裁决
| 项 | 旧态(代拍板) | 用户裁决 | 后果 |
|---|---|---|---|
| a) 完美 L1(89% 越上带) | 裁丙挂账(09-28) | **推翻丙 → 裁甲:新杠杆授权** | 89% 挂账废止;甲的两子路(敌方攻击基线上探 / L1 专属加难内容)未选,设计提案+杠杆注册待批——返工批任务卡 T-B |
| b) L2 收带组合 | C3 四件锁值已落表 | **推翻 C3 → 裁 C1 两刀**(岩浆 effect_value 2→1 + 逐层变异基数 2→0) | C3 追加两行回退:变异占比 4→5、卓尔血 100→130;C1 单独读数(扫描 N=100)普通 L2 22.0/完美 L2 43.8——返工批任务卡 T-A 落表+全量确认 |
| c) 敌方 AC 形态 | v3 Boss-only 已落表 | **推翻 v3 → 裁 v1 硬梯度**(重击手 8/重装手 9,Boss 台阶 8/10/12) | v3 的杂鱼全 0 与 Boss 8/9/12 退役;17.1% 挂账作废。⚠ T4 的 v1 读数(普通 L2 14.3%)测于 C3 锁值之上——**C1+v1 组合无测量**,预期更低(估 ~12-14%),真数随 T-A 全量确认 |
| d) 精英怪解读 | 解读 A(现行无精英行) | **追认 A**(不设精英/Boss 编码位,精英 P2 内容承载) | 与 c=v1 的兼容口径(代拟,如误读请纠正):v1 的重击手 8/重装手 9 系**逐行手设值**,不新设精英分类位;P2 真精英行到位时按 8~12 带另行启用 |
| e) 冒烟断言钉模板 0 | 8 处钉固+17 新断言 | **追认**(按已实施维持) | 无动作;若 T-C 重排模板 0,钉固断言期望值随动(归 T-C) |
**裁决一致性解读**:a 甲 + b C1 + c v1 同向=用户要整体加难、知情接受带缺口(v1 的 14.3% 越下带读数在案仍选之);d A + c v1 按「无精英编码位+逐行手设值」口径相容。
## 二、15 张节点图骨架目检判定(第三件)
- **L1 全部五张不过**——新规则:**奖励/休整不得出现在前三个节点**(现池五张全部违反,奖励/休整最早出现在第 2–3 位)。
- **L2/L3 全部十张不过**——**分支过窄**;新规则:**按 BFS 深度分层后,同一层节点数上限放宽到 5**(现池层宽最大 3),新模板应把宽度用起来。
- 其余面(边合法性/商店→Boss 必经/可达性/编号语义)目检无问题。
- **模板 0 处置待批**(返工批待批点甲):随新规则重生成(8 处钉固断言期望+首次冒险图随动)vs 保留现行图作回滚豁免——荐前者(迁移期已过,规则应全池一致)。
- 预览产物:design/qa/t5-skeleton-preview-20260929/(15 张+总览拼图+渲染脚本,数据源=level.tres 落表实态)。
## 三、挂账状态重算
| 挂账 | 旧态 | 新态 |
|---|---|---|
| 完美 L1 89% | 丙挂账 | **废止**——裁甲,新杠杆立项(06 新条目) |
| 普通 L2 17.1%(v3 AC) | 挂账待 U2 | **作废**——由 C1+v1 组合重测读数替代定案(预期缺口更大,到账后追认) |
| r_boss 锚漂移 | 卓尔 100 →≈0.26 | 回退卓尔 130 后 ≈0.33,仍低于注册域 [0.5,1.1] 下限——**挂账维持** |
| L3 侧效应(变异密度) | C3 使 9%→4% | C1 口径重算(plb0 仍在、占比维持 5%,密度介于两者间)——归 T-A 复算 |
| 刷新经济阈值数字 / 护栏 B 三选一 / 真人 k 补跑 | 挂账 | **维持不变** |
| 换算率 6/9/1 重标 | 归出口判读 | 出口判读改道——**归返工批**(L2 难度再变,爬升速度随 T-A 读数重判) |
## 四、执行归属
返工 mini-loop 计划书:`.zcode/plans/m3-iter5-rework-mini-loop-2026-09-29.md`(任务卡 T-A 数值面重投 / T-B 完美 L1 新杠杆 / T-C 节点图池重生成;待批点甲乙丙;待用户批准后开工)。
@@ -0,0 +1,31 @@
# T5 骨架 15 张目检清单(U2 人闸第三件,2026-09-29)
数据源=game/data/tables/level.tres;用户逐张判定已回填(2026-09-29)。
## 判定结果:15/15 不过(两类结构性规则缺口,非渲染/可读性问题)
- [x] L1-模板0-地精洞窟主线洞道现行.png(地精洞窟·主线洞道(现行))——**不过**:奖励在第 3 位(前三位须纯战斗)
- [x] L1-模板1-地精洞窟休整前移.png(地精洞窟·休整前移)——**不过**:休整在第 3 位
- [x] L1-模板2-地精洞窟先赏后战.png(地精洞窟·先赏后战)——**不过**:奖励在第 2 位
- [x] L1-模板3-地精洞窟连战直入.png(地精洞窟·连战直入)——**不过**:奖励在第 3 位
- [x] L1-模板4-地精洞窟缓入长坡.png(地精洞窟·缓入长坡)——**不过**:休整在第 2 位
- [x] L2-模板0-幽暗地域四岔环廊现行.png(幽暗地域·四岔环廊(现行))——**不过**:分支过窄(BFS 层宽最大 3,新规则上限 5 且应用足)
- [x] L2-模板1-幽暗地域奖励窄门.png(幽暗地域·奖励窄门)——**不过**:同上
- [x] L2-模板2-幽暗地域直入深层.png(幽暗地域·直入深层)——**不过**:同上
- [x] L2-模板3-幽暗地域休整远路.png(幽暗地域·休整远路)——**不过**:同上
- [x] L2-模板4-幽暗地域双线归束.png(幽暗地域·双线归束)——**不过**:同上
- [x] L3-模板0-灰矮人矿城主竖井现行.png(灰矮人矿城·主竖井(现行))——**不过**:同 L2(L3 与 L2 共用布线族)
- [x] L3-模板1-灰矮人矿城奖励窄门.png(灰矮人矿城·奖励窄门)——**不过**:同上
- [x] L3-模板2-灰矮人矿城直入深层.png(灰矮人矿城·直入深层)——**不过**:同上
- [x] L3-模板3-灰矮人矿城休整远路.png(灰矮人矿城·休整远路)——**不过**:同上
- [x] L3-模板4-灰矮人矿城双线归束.png(灰矮人矿城·双线归束)——**不过**:同上
## 用户原话(判定依据)
1. 「第一关的奖励和修整不能出现在前三个节点中」——L1 新规则:节点 1–3 必须全为战斗。
2. 「第二跟第三关卡分支太少了;我希望在『某一层』(按图分层,从起点 BFS 深度相同即同层)可以出现最多五个节点」——L2/L3 新规则:BFS 同深度层节点数上限 5,新模板应比现池(层宽 ≤3)明显加宽。
3. 「其他暂时没有什么问题」。
## 去向
池重生成归迭代 5 返工批 T-C(`.zcode/plans/m3-iter5-rework-mini-loop-2026-09-29.md`);重生成后重渲染本目录同形预览图交二轮目检。模板 0 处置(随新规则重生成 vs 回滚豁免)=返工批待批点甲。
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -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()