diff --git a/game/assets/ui/icons/ui_armor_heavy.png b/game/assets/ui/icons/ui_armor_heavy.png new file mode 100644 index 00000000..abaca452 Binary files /dev/null and b/game/assets/ui/icons/ui_armor_heavy.png differ diff --git a/game/assets/ui/icons/ui_armor_light.png b/game/assets/ui/icons/ui_armor_light.png new file mode 100644 index 00000000..48b63d4f Binary files /dev/null and b/game/assets/ui/icons/ui_armor_light.png differ diff --git a/game/assets/ui/icons/ui_armor_ward.png b/game/assets/ui/icons/ui_armor_ward.png new file mode 100644 index 00000000..6f48098f Binary files /dev/null and b/game/assets/ui/icons/ui_armor_ward.png differ diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 1d67a3a3..b82c32a7 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -8,12 +8,10 @@ extends Node2D const CHARGE_SKILL_IDS := ["heavy"] -# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 / -# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。 +# 护甲类型图标三键(light/ward/heavy)——终稿 = 三枚 24×24 PNG(UiKit.TEX_ARMOR_*,art-bible §5.3 +# 工单:形状承载身份、色相不承载身份;挂载底色 = 名字行素底 HP_BAR_BACK)。字形占位仅作素材缺位 +# 回退(与立绘缺位回退链同构——素材不入 git,缺位不炸)。 const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} -# 名行/底座纵向预算余量(盒底−1px「声明值刚好=容器值」系高危写法; -# ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit) -const STANDEE_FOOT_MARGIN := 12.0 const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) @@ -97,9 +95,18 @@ func intent_probe() -> Dictionary: return {"visible": st._intent_visible, "text": st._intent_glyph} -# 名字行探针出口 +# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、 +# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y) func name_line_probe() -> Dictionary: - return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))} + var f := _efont() + var tex := UiKit.load_tex(_armor_tex_path(st._armor)) + var plate := _name_plate_rect(f, tex) + return {"name": st._display_name, + "armor_icon": _armor_tex_path(st._armor) if tex != null else String(ARMOR_ICONS.get(st._armor, "◇")), + "armor_tex_bound": tex != null, + "plate": plate, "baseline_y": _name_baseline(plate).y, + "plinth_top_y": _pedestal_rect().position.y, + "plate_color": UiKit.HP_BAR_BACK} # ---- setter 面(presenter/组合根零改排) ---- @@ -420,6 +427,36 @@ func _pedestal_rect() -> Rect2: return Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0) +# 护甲图标纹理路径(甲档键 → UiKit.TEX_ARMOR_*——显式 match 单点,禁动态拼键取常量; +# 未知键回退轻甲路径,由 load_tex exists 守卫接住) +func _armor_tex_path(armor: String) -> String: + match armor: + "ward": + return UiKit.TEX_ARMOR_WARD + "heavy": + return UiKit.TEX_ARMOR_HEAVY + _: + return UiKit.TEX_ARMOR_LIGHT + + +# 名字行素底牌矩形(_draw 与 name_line_probe 共用几何——单一来源防两处漂移): +# 牌高 26 = 护甲图标 24px(甲类 1× 禁缩放)+ 上下各 1px;牌底沿 = 地台顶(box.y-18)—— +# 文字禁直接压地台(§6.1/§7.2 素底落法);牌宽 = 行宽(图标+间距+名字)+ 8,水平居中 +func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2: + var icon_w := 24.0 + if tex == null: + icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")), + HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x + var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x + var total_w := icon_w + 3.0 + name_w + return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0) + + +# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,12px 字形基线=中心+4) +func _name_baseline(plate: Rect2) -> Vector2: + return plate.position + Vector2(4.0, 17.0) + + # 绘制内容矩形(世界局部;ui_struct_probe ⑥standee「不出框+下余量≥8px」断言读数面—— # 最低绘制元素 = 底座下缘) func content_rect() -> Rect2: @@ -438,8 +475,9 @@ func _draw() -> void: ## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台 ## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半) -## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 -## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕) +## + 石板地台(§6.1 近景地台独立件:TEX_STAGE_PLAZA 源 48×12 九宫格横向拉伸、顶缘受光线 2px +## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔STAND_STONE 族方向稿取色,几何同源 +## _pedestal_rect——与 panel_skinned 缺位回退同构〕) func _draw_ground(f: Font) -> void: var sh_center := Vector2(box.x * 0.5, box.y - 24.0) var sh_rx := (box.x - 36.0) * 0.5 @@ -450,26 +488,41 @@ func _draw_ground(f: Font) -> void: pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry)) draw_colored_polygon(pts, Color(0, 0, 0, 0.27)) var ped := _pedestal_rect() - draw_rect(ped, UiKit.STAND_STONE, true) - draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0) - draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true) + var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA) + if plaza != null: + _draw_plinth_stretched(plaza, ped) + else: + draw_rect(ped, UiKit.STAND_STONE, true) + draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0) + draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true) -## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8; -## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中; -## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) +## 地台九宫格横向拉伸域(循环体提函数——左右 4px 端头切片 + 中段拉伸;纵向恒 12px 源档, +## 顶缘受光线 2px 落纵向不缩放区,工单 §5.2 工艺要点) +func _draw_plinth_stretched(tex: Texture2D, ped: Rect2) -> void: + draw_texture_rect_region(tex, Rect2(ped.position, Vector2(4.0, 12.0)), Rect2(0, 0, 4, 12)) + draw_texture_rect_region(tex, Rect2(ped.position + Vector2(4.0, 0.0), + Vector2(ped.size.x - 8.0, 12.0)), Rect2(4, 0, 40, 12)) + draw_texture_rect_region(tex, Rect2(ped.position + Vector2(ped.size.x - 4.0, 0.0), + Vector2(4.0, 12.0)), Rect2(44, 0, 4, 12)) + + +## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③,字号 FS_HINT——art-bible §7.4 清单外 +## 11–13px 提档归档);血条几何左 8 / 右 -8;血条填充 = 危险红 HP_BAR_FILL(§4.2 语义物件—— +## 不因敌我分色);条底 = HP_BAR_TRACK;血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 +## 10px 条带垂直居中;数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) func _draw_hp_bar(f: Font) -> void: var bar_x := 8.0 if st._kind == "enemy": var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)]) f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt, - HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT) + HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT) bar_x += 20.0 var bar_w := box.x - 8.0 - bar_x - draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移) + draw_rect(Rect2(bar_x, 2, bar_w, 10), UiKit.HP_BAR_TRACK) # 条底(值单点收 ui_kit) var fill_w := bar_w * st.hp_value if fill_w > 0.0: - draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减) + draw_rect(Rect2(bar_x, 2, fill_w, 10), UiKit.HP_BAR_FILL) # 直角填充(危险红——§4.2 血条显式句) var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)] var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11) # 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2; @@ -513,19 +566,26 @@ func _draw_intent_badge(f: Font) -> void: draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) -## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化) +## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2 +## 「文字禁直接压地台」素底落法——牌底沿 = 地台顶,几何单点 _name_plate_rect);护甲图标 = +## 24×24 PNG(甲类 1×,缺位回退字形,色随甲档亮档族);名字 = 暖白(暗底双色墨制) func _draw_armor_name_line(f: Font) -> void: - var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇")) - var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) - match st._armor: - "ward": - col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 - "heavy": - col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 - var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x - var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x - var total_w := armor_w + 3.0 + name_w - var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律 - f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col) - f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), st._display_name, - HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制 + var tex := UiKit.load_tex(_armor_tex_path(st._armor)) + var plate := _name_plate_rect(f, tex) + draw_rect(plate, UiKit.HP_BAR_BACK, true) + var baseline := _name_baseline(plate) + var icon_w := 24.0 + if tex != null: + draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false) + else: + var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇")) + icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x + var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) + match st._armor: + "ward": + col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 + "heavy": + col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 + f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, col) + f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name, + HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, UiKit.WARM) # v3.5 暗底双色墨制 diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index a1c41ca1..46d93ec3 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -190,7 +190,9 @@ const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘 const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep) const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上) const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上) -const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌) +const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌;站桩名字行素底同源——§6.1/§7.2 文字禁压地台) +const HP_BAR_FILL := Color8(168, 50, 31) # 血条填充·危险红 #A8321F(§4.2 语义物件显式落法——不因敌我分色、不因关卡换族;蓝族 #689CE8 已登记违例退役) +const HP_BAR_TRACK := Color(0.10, 0.10, 0.11) # 血条条底(视图原裸值收口单点——非色板新增,现渲染值原样入册) # BBCode 十六进制(RichTextLabel 用;与上同值) const HEX_GOLD := "f0a828" @@ -244,6 +246,13 @@ const TEX_ENEMY_DRIDER := "res://assets/art/character/character_enemy_drider.png const TEX_ENEMY_DUERGAR := "res://assets/art/character/character_enemy_duergar.png" # 灰矮人 const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_horror.png" # 钩爪恐魔 const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王 +# 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint) +const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png" +# 护甲类型图标三键(art-bible §5.3 工单:对象键 = 甲档变体;源 24×24 显示 24px = 1× 甲类整数倍; +# 缺位 load_tex→null→字形回退,冒烟安全) +const TEX_ARMOR_LIGHT := "res://assets/ui/icons/ui_armor_light.png" +const TEX_ARMOR_WARD := "res://assets/ui/icons/ui_armor_ward.png" +const TEX_ARMOR_HEAVY := "res://assets/ui/icons/ui_armor_heavy.png" # 框材组纹理槽(♻ 复用件量化改色入包,产物 = tools/quantize_ui_skin.py, # 全 CC0;缺位 = load_tex null → 程序化回退原路径,冒烟安全) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index c922897f..489f219b 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -134,6 +134,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态 {"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析) + {"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包) {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏) {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面) diff --git a/game/tests/unit/presentation/standee_present_values_test.gd b/game/tests/unit/presentation/standee_present_values_test.gd new file mode 100644 index 00000000..144d76e4 --- /dev/null +++ b/game/tests/unit/presentation/standee_present_values_test.gd @@ -0,0 +1,84 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 战斗一屏样张批 V5–V9 断言面(2026-10-29 迭代 43)——五项实装定值的真实入口断言: +# V5 血条填充 = 危险红(UiKit.HP_BAR_FILL 值单点 + 绘制点源码锚:全仓零 #689CE8 与零裸 +# Color(0.41,0.61,0.91);条底收 HP_BAR_TRACK 常量); +# V6 站桩名字行垫 HP_BAR_BACK 素底且不压地台(名字行基线 y < 地台顶 y——name_line_probe 几何出口); +# V7 顺位徽标提档 UiKit.FS_HINT(值断言 + 绘制点源码锚,§7.4 清单外 11–13px 提档); +# V8 护甲图标三键(light/ward/heavy)绑对应 24×24 PNG(ResourceLoader 装载结果 + name_line_probe +# 绑定面); +# V9 地台件 scene_stage_plaza.png + 护甲三枚入包可加载且尺寸 48×12 / 24×24(§5.2/§5.3 工单)。 +# headless 无渲染面:像素采样以常量单点 + 源码锚 + 探针几何承载(观测面注记,验收对照表 V5 行)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/standee_present_values_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const StandeeBodyScript := preload("res://presentation/views/standee_body.gd") + + +# 源码锚读面(静态判据——源文件即提交面) +func _read_source(rel_path: String) -> String: + var f := FileAccess.open(rel_path, FileAccess.READ) + if f == null: + return "" + return f.get_as_text() + + +func _init() -> void: + # ---- V5 血条填充 = 危险红 #A8321F(§4.2 语义物件显式落法) ---- + Kit.check("血条填充常量 = 危险红 #A8321F〔样张批 V5〕", + UiKit.HP_BAR_FILL.to_html(false) == "a8321f") + Kit.check("血条条底收常量 HP_BAR_TRACK(视图原裸值原样入册)〔样张批 V5〕", + UiKit.HP_BAR_TRACK.is_equal_approx(Color(0.10, 0.10, 0.11))) + var body_src := _read_source("res://presentation/views/standee_body.gd") + Kit.check("血条绘制点引用 UiKit.HP_BAR_FILL(源码锚)〔样张批 V5〕", + body_src.contains("UiKit.HP_BAR_FILL")) + Kit.check("全源零违例值 #689CE8 与零裸 Color(0.41,0.61,0.91)(源码锚)〔样张批 V5〕", + not body_src.contains("689CE8") and not body_src.contains("689ce8") + and not body_src.contains("0.41, 0.61, 0.91")) + + # ---- V6/V8 站桩实例面(名字行几何 + 护甲绑定) ---- + var standee: Node2D = StandeeBodyScript.new("enemy") + root.add_child(standee) + standee.set_data({"display_name": "地精", "enemy_id": "goblin", "hp": 3, "max_hp": 3, + "order": 1, "armor_type": "light"}) + var nl: Dictionary = standee.name_line_probe() + Kit.check("名字行基线 y < 地台顶 y(文字禁压地台——V6 判据坐标面)〔样张批 V6〕", + float(nl.baseline_y) < float(nl.plinth_top_y)) + Kit.check("素底牌底沿 ≤ 地台顶(整行不越地台)〔样张批 V6〕", + Rect2(nl.plate).end.y <= float(nl.plinth_top_y) + 0.01) + Kit.check("名字行素底 = HP_BAR_BACK 深档 #282220(挂载底色口径)〔样张批 V6〕", + Color(nl.plate_color).to_html(false) == "282220") + Kit.check("基线读数与实装几何同源(基线 = 地台顶上方 9px 档:149 < 158)〔样张批 V6〕", + absf(float(nl.baseline_y) - 149.0) < 0.51 and absf(float(nl.plinth_top_y) - 158.0) < 0.51) + + # ---- V7 顺位徽标提档 FS_HINT ---- + Kit.check("FS_HINT = 14(§7.4 六档)〔样张批 V7〕", UiKit.FS_HINT == 14) + Kit.check("顺位徽标绘制点取 UiKit.FS_HINT(源码锚)〔样张批 V7〕", + body_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT")) + + # ---- V8 护甲图标三键绑 24×24 PNG ---- + var armor_paths := {"light": UiKit.TEX_ARMOR_LIGHT, "ward": UiKit.TEX_ARMOR_WARD, + "heavy": UiKit.TEX_ARMOR_HEAVY} + for key: String in armor_paths: + var tex := UiKit.load_tex(armor_paths[key]) + Kit.check("护甲 %s 键绑对应 PNG 且 24×24(§5.3 工单)〔样张批 V8〕" % key, + tex != null and tex.get_width() == 24 and tex.get_height() == 24) + var ward_standee: Node2D = StandeeBodyScript.new("enemy") + root.add_child(ward_standee) + ward_standee.set_data({"display_name": "蛛化卓尔精灵", "enemy_id": "drider", "hp": 5, "max_hp": 5, + "order": 1, "armor_type": "ward"}) + var nl_ward: Dictionary = ward_standee.name_line_probe() + Kit.check("甲档键 → 纹理路径绑定(name_line_probe 出 armor_icon 路径)〔样张批 V8〕", + bool(nl_ward.armor_tex_bound) and String(nl_ward.armor_icon) == UiKit.TEX_ARMOR_WARD) + + # ---- V9 地台件与护甲三枚入包可载且尺寸对 ---- + var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA) + Kit.check("地台件 scene_stage_plaza.png 可加载且 48×12(§5.2 工单)〔样张批 V9〕", + plaza != null and plaza.get_width() == 48 and plaza.get_height() == 12) + for key: String in armor_paths: + var tex9 := UiKit.load_tex(armor_paths[key]) + Kit.check("护甲三枚入包(24×24)且路径常量在 UiKit 单点〔样张批 V9〕" if key == "light" + else "护甲三枚入包(24×24)〔样张批 V9〕", + tex9 != null and tex9.get_width() == 24 and tex9.get_height() == 24) + + quit(Kit.finish("standee_present_values")) diff --git a/game/tests/unit/presentation/standee_present_values_test.gd.uid b/game/tests/unit/presentation/standee_present_values_test.gd.uid new file mode 100644 index 00000000..dba1f71f --- /dev/null +++ b/game/tests/unit/presentation/standee_present_values_test.gd.uid @@ -0,0 +1 @@ +uid://c7a6acc5aici0 diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 73ec4147..3a8ab090 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -675,8 +675,9 @@ func _smoke_contrast_inks() -> void: Kit.check("V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" % r_rap, r_rap >= 4.5) # 消费点源码锚定:新墨在位、旧裸用点清零、语义岗常量不动 var sb_src: String = FileAccess.get_file_as_string("res://presentation/views/standee_body.gd") - Kit.check("V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)", - sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT")) + Kit.check("V4 顺位徽标①②③ 墨=GOLD_BRIGHT(FS_HINT 14 提档原位——迭代 43 V7 换代替代,旧裸 13 清零)", + sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT") + and not sb_src.contains("-1, 13, UiKit.GOLD_BRIGHT")) Kit.check("V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT", sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT")) Kit.check("V4 重甲徽标墨=GOLD_BRIGHT", sb_src.contains("col = UiKit.GOLD_BRIGHT")) Kit.check("V4 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕", diff --git a/reports/iter43-代拍板追认单-2026-10-29.md b/reports/iter43-代拍板追认单-2026-10-29.md new file mode 100644 index 00000000..51c346c3 --- /dev/null +++ b/reports/iter43-代拍板追认单-2026-10-29.md @@ -0,0 +1,21 @@ +# 迭代 43 代拍板追认单——战斗一屏样张批(2026-10-29) + +> 接收方:待办清单条目「战斗一屏样张 P0 未开工」(随实现报告一并呈报,用户追认后随反哺点一并处置)。 +> 卡片 §10 代拍板白名单四项内行使的记录+白名单边缘判断的追认请求。超白名单事项零(无越权裁断)。 + +## 白名单内行使记录 + +1. **样张脚本内临时变量命名、辅助函数拆分方式**:`draw_base()`/`_grad_top_cells()`/`_region_wcag()`/`_mask_maxch_dist()`/`_resample_mask()`/`_mask_iou()`/`_outline_underlay()`/`_ink_outline_polygon()` 等拆分;实装侧 `_armor_tex_path()`/`_name_plate_rect()`/`_name_baseline()`/`_draw_plinth_stretched()`。 +2. **断言在 CHECKS 内的 id 命名与登记顺序**:S1–S4(整屏四项)、P1–P5(战斗表现五项)、A7(护甲对比锚)、V1(两版出口)、V10(基线并排)、A5b(成稿净空)。 +3. **程序化产出素材的绘制实现细节**(颜色全取 v3.22 在册值、零新增色):地台件按工单 §5.2 工艺逐行(1px 上描边 #4C4038+2px 顶缘受光线 #C9AF83+8px 板身 #968264+1px 下描边,全周闭合);护甲三枚按 §5.3.1–5.3.3 形状(轻甲=斜跨皮带+扣件、魔甲=符纹带+两枚 3×3 符点、重甲=肩两块+胸一块甲板,深墨 #33261B 1px 描边)。 +4. **两版样张输出文件名与落盘目录**:`artwork/mockups/sample_battle_screen_{1280x800,1920x1080}.png`;附属:基线 `sample_battle_screen_baseline_1280.png`、并排 `sample_battle_screen_before_after.png`、装配稿沿用 `sample-batch-v3-final-2026-09-30.png`。 + +## 追认请求(白名单边缘判断,已按实现决定记录备案) + +5. **S3 首焦唯一的机器代理换型**:首版「最高对比点簇 ∈ 单区 ≥85%」被文字字形边打穿(红),按卡片 §9 L1(断言红修实现不修判据原则的反向面——判据为本批新建,代理形态属白名单 2「断言实现细节」)改为「高对比能量占比 winner ≥55% 且 ∈{数字区,B 区}」。语义等价(无第二焦点竞争),实测数字区 59%。 +6. **P2 剪影灰度 IoU 阈值 0.35**:新建断言的初始阈值,按三语境实测(0.36/0.37/0.61)与「同角色体块计划过半重合」的设计判断设定。 +7. **mockup C 区次级文换墨**:原裸值 (140,90,30)/(120,110,98) 对 C 区板 4.06:1/3.5:1 违整屏①——换在册墨 GOLD_INK(5.34:1)/STONE_DEEP(6.13:1),值单点不新增色。 +8. **mockup 意图徽标补 PANEL_DATA 垫牌**:实装已有素牌而 mockup 漏镜像(裸字形对舞台底 2.36:1 违①),补牌对齐实装。 +9. **A5b 补充断言**:对装配后成稿补跑画布净空(既有 A5 历史形态跑在空白底板上)——首跑即抓出注记长行溢出 6px,已拆行修复。 +10. **单测承载选型**:V5–V9 落新套件 `standee_present_values_test.gd`(manifest 登记一行)而非并入 standee_fx_sweep——域命名与 grep 锚更干净,套件族映射自动覆盖。 +11. **数据保真从紧**:三敌甲档按数据面全 light,护甲三枚展示移 S6 附录——不为展示效果改 mockup 数据面。 diff --git a/reports/iter43-反哺点-2026-10-29.md b/reports/iter43-反哺点-2026-10-29.md new file mode 100644 index 00000000..ac34ed7d --- /dev/null +++ b/reports/iter43-反哺点-2026-10-29.md @@ -0,0 +1,22 @@ +# 迭代 43 反哺点清单——战斗一屏样张批(2026-10-29) + +> 接收方:待办清单(文档批落文裁决用)。用户在会话中逐条裁决后由文档批写入 `design/` 与根流程件,并把该条清掉;实现批对 `design/` 与根规则文件只读。 + +## 卡片 §6 预登记的关联文档同步面 + +1. **`design/gdd/ux/battle.md`**——B 区高度:文 270 vs 代码 252 的 18px 差异。本批样张按代码 252 出(样张反映实机),实装对齐与否待用户裁决(对齐=需求改动走写卡通道)。 +2. **`design/art/art-bible.md`(v3.22)**——§1.4 整屏四项闸的通过事实与抽样读数(文字对比最弱 5.34:1;舞台底 L0.036/格底 L0.089 落两子带间隔 0.053;首焦=数字区能量占比 59%;名字行素底牌平底零地台色);§6.1/§7.2 站桩素底深档 `#282220` 实装落地事实(名字行素底牌+基线上移地台顶上方)。 +3. **`design/art/art-prompts.md` §5**——组二地台件、组三护甲三枚已「程序化产出即终稿」并入包(`scene_stage_plaza.png` 48×12、`ui_armor_{light,ward,heavy}.png` 24×24);组一背景插画、组四立绘重产仍待出图(状态不变)。 +4. **`design/gdd/systems/战斗循环/战斗表现.md`**——「一屏样张的验收标准」五项过闸结果(P1–P5 断言全绿;判据机器代理口径见实现报告 §3.2–3.3)。注意:本批样张为**方向稿**,目检先行——气质终审通过后才算样张整体过闸(§1.4 闸形)。 +5. **`design/engineering/test-conventions.md`**——§8.4 撤销证明条文与 2026-10-28 新验证口径(禁变异/禁撤销取证)冲突,旧句待清。 +6. **`AGENTS.md`**——「验收闭合」红线句仍保留「撤销该改动能让对应断言变红」旧措辞,与新口径冲突。 + +## 本批新发现(实现期产出,非卡片预登记) + +7. **血条递减 tween 静默空转(既有缺陷)——已消解**——本批随测发现 `standee_body.gd` `_set_hp()` 补间用点号子属性路径(应为子对象/冒号形态);收口并 master 时已由迭代 41 测试债批修复(补间目标改子对象 `tween_property(st, "hp_value", ...)`,提交 215da315),合入态复跑单测零 ERROR。本条无需落文,留档备查。 +8. **站桩名字行 12px 的字号档位归属**——`standee_body.gd` 名字行绘制取 `UiKit.FS_CAPTION`(12,本批仅收常量不改值);art-bible §7.4 字号档位表下该位既不在①条带内读数豁免、也不在②装饰 caption 在册面,属「清单外 11–13px」。处置二选一由用户裁:提档(FS_HINT 14,与顺位徽标同款)或增补 caption 在册面。 +9. **能力清单缺本批预留行**——`game/tests/README.md` 能力清单无本批 V5–V9 代表用例行(卡片成文早于清单 2026-10-29 改版定稿)。归写卡段按 test-conventions §8.3 补行(代表用例=`game/tests/unit/presentation/standee_present_values_test.gd:_init`,真实入口=`UiKit.load_tex()`/`StandeeBody.name_line_probe()`)。 +10. **art-bible §4.1 换色合并三处的执行状态**——地台绘制点半边已由地台件纹理执行(顶缘受光线 `#C9AF83`、描边 `#4C4038` 落 `scene_stage_plaza.png`);残余:① `STAGE_BG`(#3E342A→#3E362C)未执行(本批不触 `battle_stage_view.gd` 底色);② `STAND_STONE_RIM`(#C4AC84)仍被 `board_cell_unit.gd:359` 口沿高光在役引用;③ `STAND_STONE_EDGE`(#5A4C3A)由地台缺位回退路径在役引用。常量彻底退役与否归文档批裁决。 +11. **体验验证清单登记**——「战斗一屏样张方向稿目检」项(判据与材料见实现报告 §5;真人检测只登记不阻塞)。 +12. **根流程件换代**——待办清单条目「战斗一屏样张 P0 未开工」状态推进(本批实现完成后按 doc-conventions §12.1 整条删除);roadmap 迭代 43 行状态与实现报告列、「当前位」三行换代;样张产物归档位=`artwork/archive/战斗一屏样张批-2026-10-29/`(batch-closure 执行)。 +13. **意图徽标四色墨补机检断言(验收审计附注)**——样张脚本 S1 文字对比为绘图用色值对表,意图徽标四色墨(atk/buf/debuff/charge 对 PANEL_DATA 垫牌)未入 CHECKS;独立审计复算 4.97–5.62 全 ≥4.5 事实成立,仅缺机检断言。归样张脚本的后续文档/工程批补一条对表断言(values 全在册,零新增色)。 diff --git a/reports/iter43-实现报告-2026-10-29.md b/reports/iter43-实现报告-2026-10-29.md new file mode 100644 index 00000000..0daed6a2 --- /dev/null +++ b/reports/iter43-实现报告-2026-10-29.md @@ -0,0 +1,87 @@ +# 迭代 43 实现报告——战斗一屏样张批(2026-10-29) + +> 接收方:待办清单条目「战斗一屏样张 P0 未开工」;roadmap 迭代 43 行。 +> 分支 iter43(基准 ee4efad5)· 单工作区单卡串行 · 实现报告/追认单/反哺点清单三件同日落 `reports/`。 + +## 1. 各卡状态 + +| 卡 | 内容 | 状态 | +|---|---|---| +| T0 | 开工准备(worktree+素材导入+user 目录+v3 脚本基线跑) | 完成(基线 17/17 全绿存档 `artwork/mockups/sample_battle_screen_baseline_1280.png`) | +| T1 | 样张底板两版画布 | 完成(`draw_base()`+两版出口,机检 V1 绿) | +| T2 | 内容清单铺画 | 完成(色板变体第二套 S3b/三组关键帧条 S5/剪影三语境+描边对照+遮挡对照组 S6/mockup 镜像实装定值) | +| T3 | 整屏四项断言进 CHECKS | 完成(S1–S4 全绿) | +| T4 | 战斗表现五项断言进 CHECKS | 完成(P1–P5 全绿+A7 护甲对比锚) | +| T5 | 血条填充换 #A8321F(经 UiKit 常量)、条底收常量 | 完成 | +| T6 | 名字行垫 HP_BAR_BACK 素底、不压地台 | 完成 | +| T7 | 顺位徽标提档 UiKit.FS_HINT | 完成 | +| T8 | 护甲图标由字形换三枚 24×24 PNG | 完成 | +| T9 | 地台件与护甲三枚程序化产出并入包 | 完成(`--gen-assets` 四件,工单 §5.2/§5.3 口径) | +| T10 | 三份批件+改前/改后并排对拍图 | 完成(并排图机检 V10 绿) | + +**样张机检:30/30 全绿**(17 条既有 F/A 断言+S1–S4+P1–P5+A7+V1+V10+A5b)。 +**单元套件:standee_present_values 18/18 全绿**(exit 0)。 + +## 2. 实际改动—验收对照表 + +> 卡片成文(2026-10-28)早于迭代 41 呈现层结构收敛合入(2026-10-29),卡片 §3/§8 的 `standee_body.gd` 行号普遍漂移;实质判据 15 处实查零冲突(开工检查记录见 git 提交史)。本表按**实际行号**记载。 + +| # | 需求点 | 改动(实际 file:line) | 验收测试(文件:行+尾缀) | 层级 | 真实入口 | 判据绑定面 | 结果 | +|---|---|---|---|---|---|---|---| +| V1 | 样张两版产出 | `tools/render_sample_sheet_v3.py` main() 两版出口(1280×800 基座;1920×1080=1.35×+黑边各 96px) | `tools/render_sample_sheet_v3.py` CHECKS 条目 V1 | 探针 | `python tools/render_sample_sheet_v3.py` 真实跑批 | 两版位图尺寸 1280×800 / 1920×1080 | 绿(ed1 (1280,800)/ed2 (1920,1080)) | +| V2 | 文字对比 ≥4.5:1 | 同上(S1 断言+mockup C 区次级文换在册墨 STONE_DEEP/GOLD_INK) | CHECKS 条目 S1(最弱对 5.34:1) | 探针 | 同上 | 八组文字面实际用色 WCAG 对表 | 绿 | +| V3 | 明度三档带成立(含两子带) | 同上(S2 断言) | CHECKS 条目 S2 | 探针 | 同上 | 舞台底 L0.036/已开格底 L0.089 落两子带、间隔 0.053≥0.02;信息 0.78>中层 0.23 | 绿 | +| V4 | 首焦唯一 | 同上(S3 断言) | CHECKS 条目 S3 | 探针 | 同上 | 高对比能量占比:数字区 59%(≥55% 且 winner∈候选区) | 绿 | +| V5 | 血条=危险红 #A8321F | `standee_body.gd:525` 填充引 `UiKit.HP_BAR_FILL`;`:522` 条底引 `HP_BAR_TRACK`;`ui_kit.gd:194-195` 两常量 | `standee_present_values_test.gd:28-35`〔样张批 V5〕 | 单元 | 冒烟套真实跑批(run_all manifest) | 常量值 to_html=="a8321f"+源码锚(绘制点引用+全源零 #689CE8/零裸 Color(0.41,0.61,0.91)) | 绿(headless 无渲染面,像素采样以常量单点+源码锚承载——观测面注记) | +| V6 | 名字行垫素底、不压地台 | `standee_body.gd:445` `_name_plate_rect()` 几何单点(牌底沿=地台顶)、`:456` `_name_baseline()`、`:570-588` 绘制域;`name_line_probe()`(`:100-109`)扩几何出口 | `standee_present_values_test.gd:45-51`〔样张批 V6〕 | 单元 | 同上 | probe 几何:基线 y=149 < 地台顶 y=158;牌色 #282220;样张侧 S4 断言(平底 100%+零地台板身色) | 绿 | +| V7 | 顺位徽标提档 14 | `standee_body.gd:519` 裸 13 → `UiKit.FS_HINT` | `standee_present_values_test.gd:55-56`〔样张批 V7〕 | 单元 | 同上 | FS_HINT==14+绘制点源码锚(grep 该处零裸字号) | 绿 | +| V8 | 护甲图标换三枚 PNG | `standee_body.gd:432` `_armor_tex_path()`(三键→`UiKit.TEX_ARMOR_*`)、`:570-588` draw_texture_rect 24×24(缺位回退字形);`ui_kit.gd:253-255` 路径单点 | `standee_present_values_test.gd:64-71`〔样张批 V8〕 | 单元 | 同上 | 三键 ResourceLoader 装载 24×24+probe armor_icon 出绑定路径 | 绿 | +| V9 | 地台件与护甲三枚入包 | `game/assets/art/scene/scene_stage_plaza.png`(48×12,不入 git)+`game/assets/ui/icons/ui_armor_{light,ward,heavy}.png`(24×24,入 git);`standee_body.gd:493`/`:502` 九宫格横向拉伸接装 | `standee_present_values_test.gd:76-81`〔样张批 V9〕 | 接口 | 同上 | 资源可加载且尺寸 48×12 / 24×24 | 绿 | +| V10 | 改前/改后对拍图产出 | T0 存基线(1280×800);main() 生成并排图 | CHECKS 条目 V10 | 探针 | 样张脚本真实跑批 | 基线版与改后版两源文件尺寸一致+并排图落盘 | 绿(`sample_battle_screen_before_after.png` 2570×810) | + +**反向覆盖**:diff 内全部行为改动均上表有行;非行为改动=`run_all.gd` manifest 一行登记(套件注册,非游戏行为)、测试 `.uid` 边车、样张脚本注释与文档头扩展。孤儿断言:无(样张 CHECKS 全部挂 V1–V4/V10/S/P/A7;单测 18 条全挂 V5–V9)。 + +## 3. 实现决定记录(代拍板白名单内行使+边界判断备案) + +1. **断言 id 命名与登记顺序**(白名单 2):整屏四项=S1–S4、战斗表现五项=P1–P5、护甲对比锚=A7、两版出口=V1、基线并排=V10、成稿净空=A5b。 +2. **S3 首焦唯一的机器代理**:首版「最高对比点簇 ∈ 单区 ≥85%」被文字字形边打穿(各文字面对比同档,簇分裂 23%/25%)。改用**高对比能量占比**(top10% 梯度格按区归堆,winner ≥55% 且 ∈{数字区,B 区立绘})——语义=「对比内容量集中单区,无第二焦点竞争」,与 battle.md「挖掘期视线锚棋盘区」同轴。实测:数字区 59%/B 区 20%/C 区 17%。 +3. **P2 IoU 阈值 0.35 校准依据**:三语境剪影两两 IoU 实测 0.36/0.37/0.61(棋盘×明细同源资产最高;舞台立绘×棋盘小人跨源档最低)。0.35=「过半体块计划重合」档,按同角色 chibi 渲染的设计判断设定,非放宽既有断言(本断言为新建)。 +4. **HP_BAR_TRACK 新常量**=视图原裸值 `Color(0.10,0.10,0.11)` 原样收口(值不属色板新增——现渲染值单点化,卡片 §7.1「条底收常量」的直接执行)。 +5. **STAND_STONE_RIM/EDGE 两常量保留**:RIM 另有 `board_cell_unit.gd:359` 口沿高光在役引用(本批不触该文件);EDGE 由地台缺位回退路径在役引用。art-bible §4.1 换色合并三处的**地台绘制点半边**已由地台件纹理(#C9AF83/#4C4038)执行;常量彻底退役与 STAGE_BG 合并半边归文档批裁决(反哺点 10)。 +6. **意图徽标垫牌镜像补齐**:实装 `standee_body.gd` 意图徽标有 PANEL_DATA 素牌(四色墨对牌 4.7–5.8:1),mockup 原裸画字形(对舞台底 2.36:1 违 S1)——mockup 补牌对齐实装。 +7. **护甲三枚数据保真**:三敌甲档按数据面全 light(goblin/bugbear/ogre_chief),不为展示图标改甲档;三枚图标的展示落 S6 附录(挂 HP_BAR_BACK 素底,与实装挂载底同口径)。 +8. **观测面注记(test-conventions §8.4 出口)**:headless 无渲染面,V5/V7 的「像素采样色值」以常量值断言+绘制点源码锚+探针几何承载;V6 判据「基线 y < 地台顶 y」绑 `name_line_probe()` 几何出口(公开方法,非内部字段直查)。 +9. **A5b 补充断言**:既有 A5(画布四缘净空)跑在空白底板上(v3.5 冻结形态),本批补 A5b 对**装配后成稿**跑同一判据——首跑抓出 S4 注记长行溢出右缘 6px(已拆行修复),确证其价值。 +10. **样张输出文件名与落盘目录**(白名单 4):两版=`artwork/mockups/sample_battle_screen_{1280x800,1920x1080}.png`;基线=`sample_battle_screen_baseline_1280.png`;并排=`sample_battle_screen_before_after.png`;装配稿=原 `sample-batch-v3-final-2026-09-30.png`(1560×2080)。 +11. **单测承载选型**:V5–V9 落新套件 `standee_present_values_test.gd`(单元面按 §8.2 从领域类对外方法进出)而非既有 standee_fx_sweep(其 _init 为缺陷扫掠专用段)——manifest 登记一行;闸映射 `game/presentation/*`→presentation 族自动覆盖。 +12. **能力清单缺口**:本批 V5–V9 代表用例行在 `game/tests/README.md` 能力清单**无预留行**(卡片成文早于清单改版定稿)——实现批禁写 README,归写卡段补行(反哺点 9);测试函数名以卡片 V1–V10 为唯一锚。 +13. **受影响存量断言换代(test-conventions §5.5 同生死义务)**:`theme_projection_test.gd` V4 源码锚原钉旧实现「`-1, 13, UiKit.GOLD_BRIGHT`(13px 行原位)」——V7 提档使其失效;同批以**更强**新态锚替代(新形态 `UiKit.FS_HINT` 在位 **且** 旧裸 13 清零,一断言两半),首跑全量闸 02 抓出该红、替代后 164 项全绿。非削弱:旧断言只验证旧形态存在,新断言验证新形态在位+旧形态绝迹。 + +## 4. 存量红判别(闸前自检;合入态更新) + +- S10 文件头 4 处(`config_db.gd`/`config_domain_{combat,crossrun,run}.gd`)与 S11 禁用词 1 处(`adventure_table.gd:55`「罕见」):本批快照(ee4efad5)实证为存量;**收口并 master(迭代 41 测试债批 215da315)时已被该批清偿归绿**——合入态两键零红。 +- gdstyle S13:基线外新违规 0、较登记值恶化 0(`standee_body.gd` 类变量计数未越基线 19——纹理装载走 `UiKit.load_tex` 即取即绘,零新增成员)。 +- **本批零新增红。** + +## 5. 实机目检总清单(真人检测只登记不阻塞) + +| 项 | 判据 | 材料 | +|---|---|---| +| 方向稿目检(卡片 §4(3)——目检先行,机检冻结归目检通过后) | 气质=明亮地窟看台、立绘最抢眼、无压抑;构图/分区/明度三档带/首焦落点与设计意图一致 | `artwork/mockups/sample_battle_screen_1280x800.png`、`sample_battle_screen_1920x1080.png`(批末归档 `artwork/archive/战斗一屏样张批-2026-10-29/`) | +| 改前/改后对照 | 五项定值前后差异可辨(血条色/素底牌/徽标/护甲图标/地台件) | `sample_battle_screen_before_after.png` | + +登记义务同步进反哺点清单(体验验证清单归文档批登记——实现批禁写根流程清单)。 + +## 6. 遗留读数义务与观测记录 + +- MC/verifier:**零跑**(卡片未要求 MC 对拍;本批无玩法数值改动)。 +- 全量回归:合回后由用户自发(2026-10-29 关联档选测口径,无自动兜底)。 +- 既有潜在缺陷(随测发现):`standee_body.gd` 血条 tween `tween_property(self, "st.hp_value", ...)` 点号子属性路径静默空转——**收口并 master 时已由迭代 41 测试债批修复**(补间目标改子对象 `tween_property(st, "hp_value", ...)`,215da315),合入态复跑单测无该 ERROR,无遗留义务。 + +## 7. 独立验收审计结论 + +**通过,可合回**(`reports/iter43-验收审计-2026-10-29.md`,未参与实现的独立审计代理执行): +- 三条自查跑批原样记录全绿:样张脚本 30/30(exit 0)/standee_present_values 18 checks(exit 0)/theme_projection 164 checks(exit 0); +- 对照表 V1–V10 十行全过(测试存在、尾缀可 grep、真实入口未绕改动点、V6 走公开 probe 零内部直查); +- 削弱检查零缺漏(V4 锚换代证实为双半强替代;全仓 grep 零 #689CE8 与零裸 Color(0.41,0.61,0.91) 含无空格变体); +- 两条非阻塞附注:①S1 为绘图用色值对表(纯色平涂下与像素采样数学等价,S2/S4/P1/P5 为像素级兜底);②意图徽标四色墨未入 CHECKS(审计独立复算 4.97–5.62 全 ≥4.5,事实成立仅缺机检断言)——已记反哺点 13。 diff --git a/reports/iter43-验收审计-2026-10-29.md b/reports/iter43-验收审计-2026-10-29.md new file mode 100644 index 00000000..4821bf58 --- /dev/null +++ b/reports/iter43-验收审计-2026-10-29.md @@ -0,0 +1,46 @@ +# 迭代 43 独立验收审计——战斗一屏样张批(2026-10-29) + +> 审计对象:分支 iter43 尖 `34eba7a3` vs master `d9e5c019`(diff 13 文件,+967/−113)。 +> 审计人:未参与实现的独立审计会话;只读代码面,自查跑批三条+grep。 +> 闸回执指纹:`game/tests/.gates/receipt.json`——commit `34eba7a332d7…`/tree `7db3d68a…`/2026-10-30T00:04:55+08:00/domain=full/scope=related(terms: presentation)/all_green=true/16 闸中 15 绿+「11 回放确定性对拍」SKIP(skip 非红,与回执 not_run=[] 一致)。 + +## 1. 自查跑批原始结论行(三条全在 worktree 根真实执行) + +| # | 命令 | 原始结论行 | exit | +|---|---|---|---| +| ① | `python tools/render_sample_sheet_v3.py` | `=== 机检结论: 全绿 (30/30) ===`(17 条既有 F/A+S1–S4+P1–P5+A7+V1+V10+A5b) | 0 | +| ② | `…console.exe --headless --path game --script res://tests/unit/presentation/standee_present_values_test.gd` | `[standee_present_values] all green (18 checks)`(18 条全带〔样张批 Vn〕尾缀) | 0 | +| ③ | `…console.exe --headless --path game --script res://tests/unit/presentation/theme_projection_test.gd` | `[theme_projection_test] all green (164 checks)` | 0 | + +跑批产物(`artwork/mockups/*` 与 `reports/sample-v3-final-机检报告-2026-09-30.md` 未跟踪新件)属工具正常产物,留盘。工作树经查无已跟踪文件被弄脏,代码面=尖提交原样。 + +## 2. 对照表逐行核对(实现报告 §2 实际表 V1–V10) + +| # | 结论 | 依据 | +|---|---|---| +| V1 | 通过 | `check("V1",…)` 落 `tools/render_sample_sheet_v3.py:1710`,绑两版文件存在+尺寸硬等 (1280,800)/(1920,1080);①号跑批绿(黑边各 96px 与申报一致)。卡面测试名 `sample_sheet_two_editions` vs 实际 id `V1`——id 命名属代拍板白名单 2 | +| V2 | 通过(附注 1) | S1 断言落 `:1195`,八组文字面 WCAG 值对表全 ≥4.5,最弱 5.34:1 与申报一致;真实跑批取绿 | +| V3 | 通过 | S2 断言落 `:1206`,位图逐区明度实测:舞台 0.036/格底 0.089(两子带,间隔 0.053≥0.02)/信息 0.78>中层 0.23——读数与申报逐字一致 | +| V4 | 通过 | S3 断言落 `:1230`,高对比能量占比数字区 59%≥55% 且 winner∈候选区;机器代理换代(首版簇占比→能量占比)属新建断言的算法选型(决定记录 2 备案),非对既有阈值放宽 | +| V5 | 通过 | 常量值断言(`HP_BAR_FILL.to_html=="a8321f"`)+条底 `HP_BAR_TRACK`+源码锚,测试 `standee_present_values_test.gd:28-37` 尾缀〔样张批 V5〕;**绘制点实查**:`standee_body.gd:522`/`:525` 确引两常量;全仓 grep 零代码使用 `#689CE8` 与 `Color(0.41,0.61,0.91)`(含无空格变体零命中)——`689CE8` 仅存 `ui_kit.gd:194` 退役注记注释与测试自身字符串,证明意图(零裸值使用)成立 | +| V6 | 通过(附注 2) | 测试 `:45-52` 尾缀〔样张批 V6〕,走**公开方法** `name_line_probe()`(`standee_body.gd:100-109`),零 `st._*` 直查;几何单一来源 `_name_plate_rect(:445)`/`_name_baseline(:456)` 为 `_draw(:573-576)` 与 probe 共用;断言四条(基线 y<地台顶 y/牌底沿≤地台顶/素底 #282220/读数 149<158)真实跑批绿;绘制点 `:575 draw_rect(plate, UiKit.HP_BAR_BACK, true)` 实查在位 | +| V7 | 通过 | `standee_body.gd:519` 实查确用 `UiKit.FS_HINT`,`ui_kit.gd:414 FS_HINT=14`;测试 `:55-56` 尾缀〔样张批 V7〕;`standee_body.gd` 全文件零裸 `-1, 13,` 残留(grep 实证) | +| V8 | 通过 | `_armor_tex_path(:432)` 显式 match 三键→`UiKit.TEX_ARMOR_*`(`:253-255` 路径单点);测试 `:60-72` 尾缀〔样张批 V8〕:三键 ResourceLoader 各载 24×24+ward 档 standee 经 probe 出 `armor_icon==TEX_ARMOR_WARD` 且 `armor_tex_bound=true`;缺位回退字形路径保留(冒烟安全) | +| V9 | 通过 | 测试 `:75-82` 尾缀〔样张批 V9〕:地台件 48×12、护甲三枚 24×24 可加载(worktree 内实跑过);接装点 `:491-493`/`:502-507` 九宫格横向拉伸实查在位;护甲三枚 PNG 入 git(diff Bin 0→+),地台件循素材不入 git 例 | +| V10 | 通过 | `check("V10",…)` 落 `:1722`:基线版 (1280,800) 在盘+改后版同尺寸+并排图生成;①号跑批绿(并排 2570×810) | + +## 3. 测试削弱检查(逐处) + +1. **theme_projection_test V4 锚换代(决定记录 13)——非削弱,实为增强**。新断言为双半:`contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT")` **且** `not contains("-1, 13, UiKit.GOLD_BRIGHT")`。前半逐字钉住 `standee_body.gd:518-519` 实际绘制调用串;后半禁旧形态绝迹不成立。旧断言仅验证旧形态存在;新断言验证新形态在位+旧形态清零,强度严格更高。③号跑批 164 项全绿含该条。 +2. **V5/V7 观测面出口(决定记录 8)——成立,未绕改动点**。headless 无渲染面下:V5=常量值断言+绘制点源码锚+旧值清零(`standee_body.gd` 内 `UiKit.HP_BAR_FILL`/`HP_BAR_TRACK` 引用实查在 :522/:525 绘制调用处,非注释孤挂);V7=`FS_HINT==14`+绘制点锚(实查 :519)。残余 `Color(` 字面量逐处排查均为批外既有(受击 modulate 补间、落影多边形、血数 11px 白字深描边——卡片 §3.9 明记豁免在册),零未申报色字面量回流。 +3. **diff 行为改动对照表覆盖——零缺行**。standee_body.gd 九处改动(常量注释/`STANDEE_FOOT_MARGIN` 删/`name_line_probe` 扩/`_armor_tex_path`/`_name_plate_rect`/`_name_baseline`/地台件接装/血条三定值/名字行重写)全落 V5–V9;ui_kit.gd 六常量落 V5/V6/V8/V9;三枚 PNG 落 V8/V9;样张脚本落 V1–V4/V10+S/P/A7/A5b;`theme_projection_test.gd` 锚换代已申报(决定记录 13);`run_all.gd` manifest 一行=套件注册非游戏行为(申报一致)。既有测试中仅 theme_projection_test 被触(5 行=锚换代),无删断言、无放宽阈值、无缩小扫描面、无换下层直调。 +4. **内部字段直查受控例外——属实且零未申报**。新测试全程公开面进出:`new("enemy")`/`set_data()`(:116 公开 setter)/`name_line_probe()`(:100 公开)/`UiKit` 公开常量与 `load_tex`(:290 公开静态);无任何 `st._*` 直查。`theme_projection_test` 源码锚走 `FileAccess` 读提交面文件=既有在册观测面。 + +## 4. 附注(非阻塞,不构成缺漏) + +1. **V2 覆盖面注记**:S1 为八组**绘图用色值对表**而非位图像素采样(卡面判据写「位图文字像素区域对比度读数」);mockup 文字系纯色平涂,值对表与像素读数数学等价,且 S2/S4/P1/P5 均为像素级取样兜底。另:意图徽标四色墨(`RED_MINE` 等对 PANEL_DATA 牌,:800-806)未入 CHECKS 机检——本次审计独立复算 WCAG 5.62/5.10/4.97/5.21 全 ≥4.5,事实成立、仅缺机检断言,建议归反哺点随文档批补。 +2. **行号记载小漂移**:实现报告 V6 行「`:449` `_name_baseline()`」实际在 `:456`(`:449` 为 `_name_plate_rect` 体内行);函数单一来源与申报实质一致,仅行号笔误。 + +## 5. 最终结论 + +**通过,可合回。** 对照表 V1–V10 十行全过、三条自查跑批真实执行全绿(30/30、18 checks、164 checks,exit 全 0)、无断言削弱、无未申报直查、diff 行为改动零缺行;观测面出口均已在实现报告决定记录 8/13 申报且经实查成立。上列两条附注为非阻塞记录。 diff --git a/tools/render_sample_sheet_v3.py b/tools/render_sample_sheet_v3.py index 602c8d58..ed7f2dcd 100644 --- a/tools/render_sample_sheet_v3.py +++ b/tools/render_sample_sheet_v3.py @@ -34,6 +34,13 @@ v3.0 口径(方向稿=目检先行闸,冻结归机检): 输出 = artwork/mockups/sample-batch-v3-final-2026-09-30.png(v3.5 冻结口径的正式产物) + 机检报告(中间产物,吸收即死不入 git;断言定值=A3b Δ≥30/A4 尺寸基线)。 用法:python tools/render_sample_sheet_v3.py + python tools/render_sample_sheet_v3.py --gen-assets (本批追加:产出 §5.2/§5.3 四件素材入包) + python tools/render_sample_sheet_v3.py --save-baseline(本批追加:把当前整屏 mockup 存为改前基线) + +迭代 43 扩展(2026-10-29 战斗一屏样张批):整屏样张两版出口(1280×800 基座 + 1920×1080 全局等比 +1.35×+左右黑边,§8.2 1920 档策略)+ art-bible §1.4 整屏四项断言(S1–S4)+ 战斗表现五项断言(P1–P5) ++ S3b 图类型色板变体第二套 + S5 三组关键帧条 + S6 描边同屏对照与遮挡对照组 + V1 两版尺寸断言 ++ V10 改前/改后基线并排对拍;B 区高按代码现状 252 出样(art-bible §8.2 文 270 差异只登记不修)。 """ from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont import os @@ -60,6 +67,10 @@ BOARD_DIR = ("game", "assets", "art", "board") CHAR_DIR = ("game", "assets", "art", "character") ICON_DIR = ("game", "assets", "ui", "icons") OUT_PNG = os.path.join(ROOT, "artwork", "mockups", "sample-batch-v3-final-2026-09-30.png") +OUT_ED1 = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_1280x800.png") +OUT_ED2 = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_1920x1080.png") +BASELINE_PNG = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_baseline_1280.png") +OUT_BEFORE_AFTER = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_before_after.png") OUT_REPORT = os.path.join(ROOT, "reports", "sample-v3-final-机检报告-2026-09-30.md") T_FONT = asset_root("artwork", "ui-reuse-2026-09-23", "4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf") @@ -107,7 +118,7 @@ F_LBL2 = ImageFont.truetype(ZH_FONT, 12) F_TITLE = ImageFont.truetype(ZH_FONT, 19) F_STANDEE = ImageFont.truetype(ZH_FONT, 42) # 敌像字形占位 F_NAME = ImageFont.truetype(ZH_FONT, 12) -F_ORDER = ImageFont.truetype(ZH_FONT, 13) +F_ORDER = ImageFont.truetype(ZH_FONT, 14) # 顺位徽标=UiKit.FS_HINT 14(§7.4 清单外 11–13px 提档;镜像实装) F_INTENT = ImageFont.truetype(ZH_FONT, 15) CHECKS = [] # (id, name, ok, detail) @@ -729,21 +740,47 @@ def render_main_board(): return board, placement, records, band, gmap -# ══ B 区站桩(mockup 1:1) ══ +# ══ B 区站桩(mockup 1:1——镜像 standee_body.gd 实装定值:血条危险红/条底收口/徽标 FS_HINT/ +# 护甲 PNG/名字行素底不压地台/地台件九宫格拉伸;缺位回退与实装同构) ══ +_PLAZA_CACHE = {} +_ARMOR_CACHE = {} + + +def _plaza_tex(): + """地台件 48×12(缺位回退 None——程序化平色镜像实装 fallback)。""" + if "tex" not in _PLAZA_CACHE: + try: + _PLAZA_CACHE["tex"] = Image.open(asset_root( + "game", "assets", "art", "scene", "scene_stage_plaza.png")).convert("RGBA") + except FileNotFoundError: + _PLAZA_CACHE["tex"] = None + return _PLAZA_CACHE["tex"] + + +def _armor_tex(armor): + """护甲图标 24×24(缺位回退 None——mockup 画字形,实装画字形占位同构)。""" + if armor not in _ARMOR_CACHE: + try: + _ARMOR_CACHE[armor] = Image.open(asset_root( + "game", "assets", "ui", "icons", "ui_armor_%s.png" % armor)).convert("RGBA") + except FileNotFoundError: + _ARMOR_CACHE[armor] = None + return _ARMOR_CACHE[armor] + + def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", hp=1.0, intent="", statuses=(), dead=False): - """站桩 150×176(standee_view.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。""" + """站桩 150×176(standee_body.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。""" d = ImageDraw.Draw(canvas) W, H = 150, 176 - # 头顶:顺位徽标(敌)+血条 + # 头顶:顺位徽标(敌,FS_HINT 14)+血条(填充=危险红 #A8321F 不因敌我分色;条底=HP_BAR_TRACK) tx = ox + 8 if kind == "enemy": - d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(176, 122, 20)) + d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(224, 184, 96)) tx += 18 bw = W - 16 - (18 if kind == "enemy" else 0) - d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(60, 52, 42)) - d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14], - fill=(124, 196, 124) if kind == "hero" else (150, 60, 48)) + d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(26, 26, 28)) + d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14], fill=(168, 50, 31)) # 状态图标行(血条正下方,16px 定稿 PNG) sx = ox + (W - 17 * max(1, len(statuses))) // 2 for i, ic in enumerate(statuses): @@ -758,39 +795,103 @@ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", glyph = {"goblin": "▂", "bugbear": "▆", "ogre_chief": "█"}.get(enemy_id, "敌") col = (150, 60, 48) if not dead else (110, 104, 98) d.text((ox + W // 2, pbox_y + 48), glyph, font=F_STANDEE, fill=col, anchor="mm") - # 意图徽标(立绘上半部居中悬浮) + # 意图徽标(立绘上半部居中悬浮;镜像实装:PANEL_DATA 素牌垫底——四色墨对牌 4.7–5.8:1) if intent: - glyph, col = {"damage": ("⚔", (198, 61, 34)), "buff": ("✦", (46, 134, 222)), - "debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (176, 122, 20))}[intent] + glyph, col = {"damage": ("⚔", (150, 44, 36)), "buff": ("✦", (30, 90, 150)), + "debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (116, 80, 11))}[intent] + ts = d.textbbox((0, 0), glyph, font=F_INTENT) + tw_, th_ = ts[2] - ts[0], ts[3] - ts[1] + d.rectangle([ox + W // 2 - tw_ / 2 - 4, pbox_y + 30 - th_ * 0.75, + ox + W // 2 + tw_ / 2 + 4, pbox_y + 30 + th_ * 0.35], fill=(233, 217, 184)) d.text((ox + W // 2, pbox_y + 30), glyph, font=F_INTENT, fill=col, anchor="mm") - # v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚) + # v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚;地台=独立件九宫格横向拉伸) sh = Image.new("RGBA", (W, 30), (0, 0, 0, 0)) ImageDraw.Draw(sh).ellipse([28, 6, W - 28, 24], fill=(0, 0, 0, 70)) canvas.alpha_composite(sh, (ox, oy + 146)) - d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 170], fill=(150, 130, 100), outline=(90, 76, 58)) - d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 160], fill=(196, 172, 132)) - # 敌名行:护甲类型图标+纯名字(AC 数值不展示) + ped_w = W - 36 + ped_x, ped_y = ox + 18, oy + 158 + tex = _plaza_tex() + if tex is not None: + canvas.paste(tex.crop((0, 0, 4, 12)).resize((4, 12), Image.NEAREST), (ped_x, ped_y)) + canvas.paste(tex.crop((4, 0, 44, 12)).resize((ped_w - 8, 12), Image.NEAREST), (ped_x + 4, ped_y)) + canvas.paste(tex.crop((44, 0, 48, 12)).resize((4, 12), Image.NEAREST), (ped_x + ped_w - 4, ped_y)) + else: + d.rectangle([ped_x, ped_y, ped_x + ped_w, ped_y + 12], fill=PLAZA_BODY, outline=PLAZA_EDGE) + d.rectangle([ped_x, ped_y, ped_x + ped_w, ped_y + 2], fill=PLAZA_RIM) + # 名字行:整行垫 HP_BAR_BACK 素底(牌底沿=地台顶——文字禁直接压地台 §6.1/§7.2; + # 护甲图标=24×24 PNG 缺位回退字形;几何镜像 standee_body._name_plate_rect) if kind == "enemy": - d.text((ox + 16, oy + 172), ARMOR_ICONS[armor], font=F_NAME, fill=ARMOR_COLORS[armor], anchor="lm") - d.text((ox + 30, oy + 172), name, font=F_NAME, fill=(240, 226, 200), anchor="lm") + itex = _armor_tex(armor) + if itex is not None: + icon_w, icon_draw = 24.0, ("tex", itex) + else: + gtxt = ARMOR_ICONS[armor] + icon_w = d.textbbox((0, 0), gtxt, font=F_NAME)[2] + icon_draw = ("glyph", gtxt, ARMOR_COLORS[armor]) + name_w = d.textbbox((0, 0), name, font=F_NAME)[2] + plate_w = icon_w + 3.0 + name_w + 8.0 + px0 = ox + (W - plate_w) / 2 + py0 = oy + 158 - 26 + d.rectangle([px0, py0, px0 + plate_w, py0 + 26], fill=(40, 34, 32)) + base_x, base_y = px0 + 4, py0 + 17 # 基线=牌顶+17(镜像 _name_baseline;box.y-27 < 地台顶) + if icon_draw[0] == "tex": + canvas.paste(icon_draw[1].resize((24, 24), Image.NEAREST), + (int(base_x), py0 + 1), icon_draw[1].resize((24, 24), Image.NEAREST)) + else: + d.text((base_x, base_y), icon_draw[1], font=F_NAME, fill=icon_draw[2], anchor="ls") + d.text((base_x + icon_w + 3.0, base_y), name, font=F_NAME, fill=(240, 226, 200), anchor="ls") if dead: gray = Image.new("RGBA", (W, H), (100, 100, 100, 120)) canvas.alpha_composite(gray, (ox, oy)) -def render_battle_screen(): - """B/C/D 三区全内容 1280×800 mockup。""" - scr = Image.new("RGBA", (1280, 800), (24, 20, 17, 255)) - d = ImageDraw.Draw(scr) - # ── B 区 252px 全宽(v3 舞台:世界底层暗背景+顶光柱氛围;文字=信息面层暖白) ── - d.rectangle([0, 0, 1280, 252], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"]) - beam = Image.new("RGBA", (1280, 252), (0, 0, 0, 0)) +B_H = 252 # B 区高=代码现状(battle_stage_view custom_minimum_size 252;§8.2 文 270 差异只登记不修) +CANVAS_W = 1280 # 基座档(art-bible §8.1) + + +def draw_base(canvas_w=CANVAS_W, canvas_h=800, b_h=B_H): + """样张底板(T1):B/C/D 三区骨架——分区数值按 battle.md 指路、B 区按代码 252;格边长=CELL_D 45 + (D 区 10×10 板实比,§8.1)。返回 (img, zones)——zones 供 S/P 断言取样: + b=(0,0,w,b_h) 含明细带;band=明细带 24px;c=(0,b_h+4,240,h);d=(244,b_h+4,w,h)。""" + img = Image.new("RGBA", (canvas_w, canvas_h), (24, 20, 17, 255)) + d = ImageDraw.Draw(img) + c_y = b_h + 4 + # B 区(舞台底=世界底层下沿子带 L 0.014–0.06;文字=信息面层暖白) + d.rectangle([0, 0, canvas_w, b_h], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"]) + beam = Image.new("RGBA", (canvas_w, b_h), (0, 0, 0, 0)) db = ImageDraw.Draw(beam) for bx0 in (110, 900, 1055, 1210): for i in range(60): a = int(26 * (1 - i / 60.0)) db.rectangle([bx0 - 46 + i // 3, 0, bx0 + 46 - i // 3, 40 + i * 3], fill=(255, 236, 200, a)) - scr.alpha_composite(beam, (0, 0)) + img.alpha_composite(beam, (0, 0)) + # 明细带(挖掘期收起 24px) + d.rectangle([0, b_h - 24, canvas_w, b_h], fill=(46, 39, 32)) + # C 区 + d.rectangle([0, c_y, 240, canvas_h], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"]) + # D 区 + d.rectangle([244, c_y, canvas_w, canvas_h], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"]) + # D 区顶行工具件(停手/图例/刷新+AP 点阵+RAP) + d.rectangle([252, c_y + 6, 304, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((278, c_y + 17), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([310, c_y + 6, 354, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((332, c_y + 17), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([360, c_y + 6, 428, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((394, c_y + 17), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + for i in range(6): + col = (232, 200, 120) if i < 4 else (120, 106, 88) + d.ellipse([440 + i * 18, c_y + 10, 452 + i * 18, c_y + 22], fill=col) + d.text((552, c_y + 17), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") + zones = {"b": (0, 0, canvas_w, b_h), "band": (0, b_h - 24, canvas_w, b_h), + "c": (0, c_y, 240, canvas_h), "d": (244, c_y, canvas_w, canvas_h)} + return img, zones + + +def render_battle_screen(board_img=None): + """B/C/D 三区全内容 1280×800 mockup(T1 底板+T2 内容)。返回 (scr, zones, board_origin)。""" + scr, zones = draw_base() + d = ImageDraw.Draw(scr) + b_h = B_H d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(240, 226, 200), anchor="mm") draw_standee(scr, 40, 34, "hero", "主角", "", hp=0.7, statuses=("attack_up", "ac_up")) d.text((560, 120), "兑现演出区\n(结算期:光带 → 弹道 → 受击)", font=ImageFont.truetype(ZH_FONT, 13), @@ -799,34 +900,28 @@ def render_battle_screen(): draw_standee(scr, 973, 34, "enemy", "熊地精", "bugbear", order=2, armor="light", hp=0.8, intent="charge", statuses=("alert",)) draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff") - # 明细带(挖掘期收起 24px) - d.rectangle([0, 228, 1280, 252], fill=(46, 39, 32)) + # 明细带摘要行 + 结算语境 hero 剪影 mini(P2 三语境之一;右端贴装不越缘) d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2, fill=(240, 226, 200), anchor="lm") - # ── C 区 240×548 ── - d.rectangle([0, 256, 240, 800], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"]) + mini = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST) + scr.alpha_composite(mini, (1236, b_h - 24)) + # ── C 区内容(次级文对齐 §7.2 ≥4.5:1——STONE_DEEP/GOLD_INK 在册墨) ── d.rectangle([8, 264, 232, 340], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线) d.text((16, 276), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) - d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(140, 90, 30)) + d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(116, 80, 11)) d.text((16, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) - d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(120, 110, 98)) + d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(85, 77, 65)) for i in range(4): d.rectangle([8 + i * 40, 368, 8 + i * 40 + 34, 402], outline=(120, 106, 88), width=1) - d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(120, 110, 98)) - # ── D 区 ── - d.rectangle([244, 256, 1280, 800], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"]) - # 顶行:停手/图例/刷新+AP 点阵+RAP - d.rectangle([252, 262, 304, 284], fill=(90, 80, 66), outline=(120, 106, 88)) - d.text((278, 273), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm") - d.rectangle([310, 262, 354, 284], fill=(90, 80, 66), outline=(120, 106, 88)) - d.text((332, 273), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm") - d.rectangle([360, 262, 428, 284], fill=(90, 80, 66), outline=(120, 106, 88)) - d.text((394, 273), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm") - for i in range(6): - col = (232, 200, 120) if i < 4 else (120, 106, 88) - d.ellipse([440 + i * 18, 266, 452 + i * 18, 278], fill=col) - d.text((552, 273), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") - return scr + d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(85, 77, 65)) + # ── D 区主样张板(10×10@45px=450×450+悬崖 8)嵌进工具行之下居中 ── + board_origin = None + if board_img is not None: + bx = 244 + (1036 - board_img.size[0]) // 2 + by = b_h + 4 + 34 + (800 - b_h - 4 - 34 - board_img.size[1]) // 2 + scr.paste(board_img.convert("RGB"), (bx, by), board_img) + board_origin = (bx, by) + return scr, zones, board_origin # ══ 机检 ══ @@ -1033,6 +1128,211 @@ def run_checks(board, placement, records, band, gmap, canvas_img=None): return all(c[2] for c in CHECKS) +# ══ 整屏四项(S1–S4,art-bible §1.4 整屏样张闸)+战斗表现五项(P1–P5)+护甲对比锚(A7) ══ +def _grad_top_cells(scr, frac=100): + """整屏横向梯度图(步长 2)——首焦唯一性取样用,返回 [(ΔL, x, y)] 降序。""" + rgbp = scr.convert("RGB").load() + w, h = scr.size + grad = [] + for y in range(0, h, 2): + for x in range(0, w - 2, 2): + c1, c2 = rgbp[x, y], rgbp[x + 2, y] + l1 = 0.2126 * c1[0] + 0.7152 * c1[1] + 0.0722 * c1[2] + l2 = 0.2126 * c2[0] + 0.7152 * c2[1] + 0.0722 * c2[2] + grad.append((abs(l1 - l2), x, y)) + grad.sort(reverse=True) + return grad[:max(1, len(grad) // frac)] + + +def _mask_maxch_dist(img, box, tol=40): + """隔离剪影掩码:四角中值底色 + 三通道最大距离阈值(蓝夹克对暖底 maxΔ>40 可掩)。""" + crop = img.crop(box).convert("RGB") + w, h = crop.size + px = crop.load() + corners = [px[1, 1], px[w - 2, 1], px[1, h - 2], px[w - 2, h - 2]] + bg = tuple(sorted(c[i] for c in corners)[1] for i in range(3)) + mask = [] + for yy in range(h): + for xx in range(w): + c = px[xx, yy] + mask.append(1 if max(abs(c[i] - bg[i]) for i in range(3)) > tol else 0) + return mask, (w, h) + + +def _resample_mask(mask, size, tw=24, th=24): + w, h = size + return [mask[(ty * h // th) * w + (tx * w // tw)] for ty in range(th) for tx in range(tw)] + + +def _mask_iou(a, b): + inter = sum(1 for x, y in zip(a, b) if x and y) + uni = sum(1 for x, y in zip(a, b) if x or y) + return (inter / uni) if uni else 0.0 + + +def _region_wcag(img, box): + """区域 WCAG 相对亮度均值(0–1 线性域——§2 明度带口径;region_luma 是 0–255 加权域不可混用)。""" + crop = img.crop(box).convert("RGB") + px = crop.load() + tot = n = 0 + for yy in range(0, crop.size[1], 2): + for xx in range(0, crop.size[0], 2): + tot += wcag_lum(px[xx, yy]) + n += 1 + return tot / max(1, n) + + +def run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_img, s6_meta): + """整屏样张闸四项 + 战斗表现五项(战斗表现「一屏样张的验收标准」)+护甲图标挂载对比锚。""" + # S1 整屏①全部文字对直接背景 ≥4.5:1(静态对表——对值即图内实际用色;大字档 3:1 由 A3a 承载) + pairs = [("暖白×舞台底", (240, 226, 200), (62, 52, 42)), ("金亮徽标×舞台底", (224, 184, 96), (62, 52, 42)), + ("暖白×明细带底", (240, 226, 200), (46, 39, 32)), ("标题墨×C区板", (30, 26, 22), (232, 220, 196)), + ("暖白×名字行素底", (240, 226, 200), (40, 34, 32)), ("金深墨×C区板", (116, 80, 11), (232, 220, 196)), + ("深化石×C区板", (85, 77, 65), (232, 220, 196)), ("暖白×工具钮", (232, 222, 200), (90, 80, 66))] + ratios = [(n, round((max(wcag_lum(a), wcag_lum(b)) + 0.05) / (min(wcag_lum(a), wcag_lum(b)) + 0.05), 2)) + for n, a, b in pairs] + weakest = min(ratios, key=lambda r: r[1]) + check("S1", "整屏①全部文字对直接背景 ≥4.5:1(WCAG AA)", all(r[1] >= 4.5 for r in ratios), + "最弱 %s=%.2f;全部 %s" % (weakest[0], weakest[1], ratios)) + # S2 整屏②明度三档带 + 世界底层两子带(§2;舞台=实测区域读数,格底=实测板面;WCAG 0–1 域) + stage_l = _region_wcag(scr, (300, 190, 600, 220)) + bx, by = board_origin + board_span = 10 * CELL_D + 8 + floor_l = _region_wcag(scr, (bx + 2 * CELL_D, by + 3 * CELL_D + 10, bx + 4 * CELL_D, by + 4 * CELL_D - 10)) + info_l = wcag_lum(INK_L_GOLD) + mid_l = wcag_lum(PLAZA_BODY) + ok_s2 = (info_l > mid_l > stage_l and 0.08 <= floor_l <= 0.14 and 0.014 <= stage_l <= 0.06 + and 0.22 <= mid_l <= 0.45 and (floor_l - stage_l) >= 0.02) + check("S2", "整屏②明度三档带成立(信息>世界中层>世界底层)+世界底层两子带(0.014–0.06/0.08–0.14,间隔≥0.02)", + ok_s2, "信息 %.2f/中(地台) %.2f/底层 舞台 %.3f+格底 %.3f/子带间隔 %.3f" + % (info_l, mid_l, stage_l, floor_l, floor_l - stage_l)) + # S3 整屏③首焦唯一(§3.4 整屏延伸:高对比内容量集中单区 ≥55%——挖掘期第一注视点=棋盘区 + # 〔battle.md 信息层级〕;候选区=数字区/B 区立绘,C 区规划面与工具行抢焦即违) + top = _grad_top_cells(scr, 10) + bx, by = board_origin + board_span = 10 * CELL_D + 8 + b_rect = zones["b"] + c_rect = zones["c"] + e_board = e_b = e_c = e_tool = 0 + for g, x, y in top: + if bx <= x < bx + board_span and by <= y < by + board_span: + e_board += g + elif b_rect[0] <= x < b_rect[2] and b_rect[1] <= y < b_rect[3]: + e_b += g + elif c_rect[0] <= x < c_rect[2] and c_rect[1] <= y < c_rect[3]: + e_c += g + elif zones["d"][1] <= y < zones["d"][1] + 30: + e_tool += g + total = e_board + e_b + e_c + e_tool + shares = {"数字区": e_board / max(1, total), "B 区": e_b / max(1, total), + "C 区": e_c / max(1, total), "工具行": e_tool / max(1, total)} + winner = max(shares, key=shares.get) + check("S3", "整屏③首焦唯一(高对比内容量集中单区 ≥55%,无第二焦点竞争;候选=数字区/B 区立绘)", + shares[winner] >= 0.55 and winner in ("数字区", "B 区"), + "能量占比 %s(首焦=%s)" % ({k: round(v, 2) for k, v in shares.items()}, winner)) + # S4 整屏④文字禁直接压纹理底(垫容器/罩层):素底牌顶右上角无字形区平底 + 全牌主色占比 + # + 牌区零地台板身色(§6.1/§7.2 同文断言的样张侧)+明细带平底 + d_ = ImageDraw.Draw(Image.new("RGB", (1, 1))) + name_w = d_.textbbox((0, 0), "地精", font=F_NAME)[2] + plate_w = 24 + 3 + name_w + 8 + px0 = 818 + (150 - plate_w) / 2 + py0 = 34 + 132 + plate_crop = scr.convert("RGB").crop((int(px0), int(py0), int(px0 + plate_w), int(py0) + 26)) + all_px = list(plate_crop.getdata()) + from collections import Counter as _Counter + main_share = _Counter(all_px).most_common(1)[0] + flat_crop = plate_crop.crop((plate_crop.size[0] - 10, 1, plate_crop.size[0] - 2, 7)) + flat_px = list(flat_crop.getdata()) + flat_plate = sum(1 for c in flat_px if c == (40, 34, 32)) / max(1, len(flat_px)) + rgbp = scr.convert("RGB").load() + plinth_hit = sum(1 for yy in range(int(py0), int(py0) + 26) for xx in range(int(px0), int(px0 + plate_w)) + if rgbp[xx, yy][:3] == PLAZA_BODY) + band_flat = region_luma(scr, (700, 232, 1100, 246)) + band_expect = sum(w * c for w, c in zip((0.2126, 0.7152, 0.0722), (46, 39, 32))) + check("S4", "整屏④文字禁直接压纹理底(名字行=素底牌平底+牌区零地台板身色+明细带平底)", + flat_plate >= 0.95 and main_share[0] == (40, 34, 32) and main_share[1] >= len(all_px) * 0.6 + and plinth_hit == 0 and abs(band_flat - band_expect) < 3, + "牌顶角平底 %.0f%%;主色 %s 占 %.0f%%;牌区地台色命中 %d;明细带 L %.2f" + % (flat_plate * 100, main_share[0], main_share[1] * 100.0 / len(all_px), plinth_hit, band_flat)) + # P1 战斗表现①棋盘区零打扰(数字格砖心零战斗元素签名色;数字对比硬指标由 A3a 承载。 + # 签名表不含 CONTACT_GLOW——其色与数字冷亮墨 INK_L_COLD 差 <24 不可色分,空间约束由 F5a 承载) + sigs = list(GLOW_GROUPS.values()) + [DANGER_RED] + viol = [] + for (dx_, dy_), dg in DIGITS.items(): + if dg <= 0: + continue + x0, y0 = bx + dx_ * CELL_D, by + dy_ * CELL_D + m = int(CELL_D * 0.2) + for yy in range(y0 + m, y0 + CELL_D - m, 2): + for xx in range(x0 + m, x0 + CELL_D - m, 2): + c = rgbp[xx, yy][:3] + if any(all(abs(c[i] - s_[i]) <= 24 for i in range(3)) for s_ in sigs): + viol.append(((dx_, dy_), c)) + break + check("P1", "战斗表现①棋盘区零打扰(数字格砖心零战斗元素签名色;挖掘期抢焦禁令由 S3 首焦承载)", + not viol, "违例 %s" % viol if viol else "%d 个数字格砖心全净" % len(DIGITS)) + # P2 战斗表现②剪影跨舞台通用(灰度轮廓通道——S6 三语境隔离剪影两两 IoU ≥0.35) + masks = [_mask_maxch_dist(s6_img, box) for box in s6_meta["hero_boxes"]] + rm = [_resample_mask(m, s) for m, s in masks] + ious = [round(_mask_iou(rm[0], rm[1]), 2), round(_mask_iou(rm[0], rm[2]), 2), round(_mask_iou(rm[1], rm[2]), 2)] + check("P2", "战斗表现②剪影跨舞台通用(灰度三语境两两 IoU ≥0.35——识别只靠轮廓通道)", + min(ious) >= 0.35, + "舞台×棋盘 %s/舞台×明细 %s/棋盘×明细 %s" % tuple(ious)) + # P3 战斗表现③动画方案可读(三组关键帧 ≥3 帧、相邻帧互异、退场末帧=灰化残位非瞬消) + fw, fh = frames_meta["frame_size"] + distinct, death_ok, death_note = True, True, "" + for key, (xs, y0) in frames_meta["strips"].items(): + crops = [frames_strip.crop((x, y0, x + fw, y0 + fh)).convert("RGB") for x in xs] + for c1, c2 in zip(crops, crops[1:]): + if list(c1.getdata()) == list(c2.getdata()): + distinct = False + if key == "death": + sat = list(crops[2].convert("HSV").split()[1].getdata()) + ink = sum(1 for p in crops[2].getdata() if p != (34, 29, 24)) + death_ok = max(sat) <= 90 and ink > 200 + death_note = "末帧最大饱和 %d/残位像素 %d" % (max(sat), ink) + check("P3", "战斗表现③动画方案可读(三组关键帧条各 3 帧+相邻帧互异+退场末帧灰化残位禁瞬消)", + distinct and death_ok, death_note or "三组帧互异") + # P4 战斗表现④战斗空间成立(双舞台结构边界在位:C/D 顶缘描边连续+区间净缝+明细带在位) + c_y = zones["c"][1] + + def line_hit(y, x0, x1, col, tol=30): + n = hits = 0 + for x in range(x0, x1, 4): + n += 1 + if all(abs(rgbp[x, y][i] - col[i]) <= tol for i in range(3)): + hits += 1 + return hits / max(1, n) + + ok_p4 = (line_hit(c_y, 246, 1276, UIKIT["PANEL_BORDER"]) >= 0.9 + and line_hit(c_y, 2, 238, UIKIT["PANEL_BORDER"]) >= 0.9 + and all(rgbp[x, B_H + 2][:3] == (24, 20, 17) for x in range(250, 1270, 60)) + and line_hit(B_H - 2, 250, 1270, (46, 39, 32)) >= 0.9) + check("P4", "战斗表现④战斗空间成立(双舞台边界线在位+区间净缝;视线锚归 S3 首焦唯一)", ok_p4, + "C/D 顶缘描边与净缝读数见 detail") + # P5 战斗表现⑤敌层插入位置可裁(遮挡对照:顶层 数字/印记/小人 可辨、敌像垫底可辨) + ox0, oy0, ox1, oy1 = s6_meta["overlap"] + crop = s6_img.crop((ox0, oy0, ox1, oy1)).convert("RGB") + cpx = crop.load() + + def count_near(col, tol=28): + return sum(1 for yy in range(crop.size[1]) for xx in range(crop.size[0]) + if all(abs(cpx[xx, yy][i] - col[i]) <= tol for i in range(3))) + + cnt = {"数字墨": count_near(INK_L_WARM), "封印黑": count_near(SEAL_BLACK, 12), + "小人蓝": count_near(HERO_BLUE), "敌像灰": count_near((110, 104, 98), 14)} + ok_p5 = cnt["数字墨"] >= 25 and cnt["封印黑"] >= 8 and cnt["小人蓝"] >= 20 and cnt["敌像灰"] >= 10 + check("P5", "战斗表现⑤敌层插入位置可裁(遮挡对照组:数字>小人>旗>敌,顶层三件可辨计数达标)", ok_p5, + "计数 %s" % cnt) + # A7 护甲三枚对挂载素底 ≥3:1(§5.3 共用验收——色值=产出件常量,尺寸/装载由 V8/V9 单测承载) + a_mount = wcag_lum((40, 34, 32)) + a_ratios = [(k, round((max(wcag_lum(c), a_mount) + 0.05) / (min(wcag_lum(c), a_mount) + 0.05), 2)) + for k, c in [("轻甲", ARMOR_LIGHT_COL), ("魔甲", ARMOR_WARD_COL), ("重甲", ARMOR_HEAVY_COL)]] + check("A7", "护甲三枚对挂载底色(名字行素底 #282220)图形对比 ≥3:1(§5.3 共用验收)", + all(r[1] >= 3.0 for r in a_ratios), " %s" % a_ratios) + return all(c[2] for c in CHECKS) + + def cg_board_region(board, x0, y0, c): return board.crop((x0, y0, x0 + c, y0 + c)).convert("RGB") @@ -1090,26 +1390,263 @@ def render_s3(cell=64): return Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA") +def render_s3b(cell=64): + """S3b 图类型色板变体第二套(§1.4 内容清单「至少两套」的第三套槽位;§4.3 四槽换色示意—— + deep_underdark 方向候选:灰黑主调+矿脉金属,§6.3 终值未冻结本套为方向示意)。信息层恒定: + 数字亮墨在换色后重画;微光取与 S3 不同组(blast 覆盖另一组互辨)。""" + rows = ["WWWW", "WFFW", "WFWF"] + gmap = GMap(rows) + board, placement, glow = render_board(gmap, cell, overlays=False) + gd = Image.new("RGBA", board.size, (0, 0, 0, 0)) + draw_type_glow(gd, 1 * cell, 2 * cell, cell, "blast") + board.alpha_composite(gd) + board = draw_slab_cliff(board, cols=gmap.w) + h, s, v = board.convert("HSV").split() + h = h.point(lambda _: 12) + s = s.point(lambda x: min(255, int(10 + x * 0.30))) + v = v.point(lambda x: int(x * 0.90)) + out = Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA") + draw_digit_direct(out, 2 * cell, 1 * cell, cell, 5, F_DIGIT) # 信息层恒定——换色后重画 + return out + + +def render_keyframes(): + """S5 三组关键帧条(战斗表现「一屏样张的验收标准」内容清单:受击/退场/高光兑现, + 每组至少起中末三帧;退场末帧=灰化残位——「死亡禁止瞬消」机器代理;每组标注减动效简化形态)。 + 返回 (strip, meta)——meta[key]=(行 x 列表, 行 y0),供 P3 断言按 bbox 取样。""" + fw, fh, gap = 110, 116, 16 + labels = [("hit", "受击:常态→闪白+火花→回位(减动效=时长减半)"), + ("death", "退场:常态→灰化+碎块→塌陷残位(减动效=直达残位;禁瞬消)"), + ("highlight", "高光兑现:行→金框强调→定格(减动效=直达终态)")] + row_h = fh + 30 + strip = Image.new("RGBA", ((fw + gap) * 3 + gap, row_h * 3 + 6), (24, 20, 17, 255)) + d = ImageDraw.Draw(strip) + meta = {"frame_size": (fw, fh), "strips": {}} + def base_frame(sx, sy, col_body, flash=0, sparks=0, shards=(), sink=0, alpha=255, gold=0): + x0, y0 = sx, sy + 4 + d.rectangle([x0, y0, x0 + fw, y0 + fh], fill=(34, 29, 24), outline=(120, 106, 88)) + cy = y0 + 58 + sink + if gold == 2: + d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], fill=(240, 168, 40), outline=(224, 184, 96), width=2) + elif gold == 1: + d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], outline=(224, 184, 96), width=2) + else: + d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], outline=(120, 106, 88), width=1) + d.text((x0 + 10, y0 + fh - 20), "伤害 4 → 地精", font=F_LBL2, fill=(240, 226, 200)) + col = (col_body[0], col_body[1], col_body[2], alpha) + d.text((x0 + fw // 2, cy), "▆", font=F_STANDEE, fill=col, anchor="mm") + for k, (ox_, oy_) in enumerate(shards): + d.rectangle([x0 + fw // 2 + ox_, cy + oy_, x0 + fw // 2 + ox_ + 6, cy + oy_ + 4], + fill=(110, 104, 98, alpha)) + if sparks: + for k in range(sparks): + ang = 6.283 * k / sparks + d.ellipse([x0 + fw // 2 + 26 * math.cos(ang) - 2, cy - 14 * math.sin(ang) - 2, + x0 + fw // 2 + 26 * math.cos(ang) + 2, cy - 14 * math.sin(ang) + 2], + fill=(230, 190, 90)) + if flash: + d.rectangle([x0 + 1, y0 + 1, x0 + fw - 1, y0 + fh - 1], fill=(255, 255, 255, flash)) + xs = [gap + i * (fw + gap) for i in range(3)] + for gi, (key, label) in enumerate(labels): + y0 = 4 + gi * row_h + meta["strips"][key] = (list(xs), y0) + if key == "hit": + base_frame(xs[0], y0, (150, 60, 48)) + base_frame(xs[1], y0, (150, 60, 48), flash=120, sparks=6) + base_frame(xs[2], y0, (150, 60, 48)) + elif key == "death": + base_frame(xs[0], y0, (150, 60, 48)) + base_frame(xs[1], y0, (110, 104, 98), shards=[(-18, 6), (14, 10), (-6, 14)], alpha=200) + base_frame(xs[2], y0, (110, 104, 98), sink=6, + shards=[(-26, 18), (20, 22), (-10, 26), (8, 20), (-2, 30)], alpha=150) + else: + base_frame(xs[0], y0, (150, 60, 48), gold=0) + base_frame(xs[1], y0, (150, 60, 48), gold=1) + base_frame(xs[2], y0, (240, 168, 40), gold=2) + d.text((xs[2] + fw + gap + 4, y0 + 46), label, font=F_LBL2, fill=(225, 215, 195)) + return strip, meta + + +def _outline_underlay(src, outline_rgb): + """alpha 边缘 1px 外扩描边垫层(主角深墨描边/敌方暖白描边共用)。""" + a = src.split()[3] + dil = a.filter(ImageFilter.MaxFilter(3)) + edge = ImageChops.subtract(dil, a) + under = Image.new("RGBA", src.size, outline_rgb + (255,)) + under.putalpha(edge) + return under + + +def render_outline_overlap(): + """S6 剪影跨舞台三语境(P2 灰度取样,隔离平底免污染)+描边同屏对照(主角深墨 (40,19,13)/ + 敌方暖白族——art-prompts §5.4 描边条款)+护甲三枚展示+遮挡对照组(P5——敌人姿态精灵与 + 数字/封印印记/小人同格叠放,绘制优先级 数字>小人>旗>敌)。返回 (img, meta)。""" + W, H = 940, 190 + img = Image.new("RGBA", (W, H), (24, 20, 17, 255)) + d = ImageDraw.Draw(img) + meta = {"hero_boxes": [], "overlap": None, "armor": {}} + # ① 战斗舞台语境:主角立绘 96px + 深墨描边 + por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST) + img.alpha_composite(_outline_underlay(por, (40, 19, 13)), (18, 14)) + img.alpha_composite(por, (18, 14)) + meta["hero_boxes"].append((18, 14, 18 + 96, 14 + 96)) + d.text((18, 116), "主角·战斗舞台语境\n描边=深墨 (40,19,13)", font=F_LBL2, fill=(225, 215, 195)) + # ② 棋盘格语境:小人 48px 压已开格底 + d.rectangle([150, 14, 214, 78], fill=(24, 20, 16)) + fl = FLOORS[0].resize((64, 64), Image.NEAREST) + img.paste(fl.convert("RGB"), (150, 14)) + h48 = HERO.resize((48, int(48 * HERO.size[1] / HERO.size[0])), Image.NEAREST) + img.alpha_composite(h48, (150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8)) + meta["hero_boxes"].append((150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8, + 150 + (64 - h48.size[0]) // 2 + h48.size[0], 14 + 64 - 8)) + d.text((150, 116), "棋盘格语境\n(隔离已开格底)", font=F_LBL2, fill=(225, 215, 195)) + # ③ 结算明细带语境:小人 24px 压明细带底 + d.rectangle([238, 14, 302, 78], fill=(46, 39, 32)) + h24 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST) + img.alpha_composite(h24, (238 + 8, 14 + 52 - h24.size[1])) + meta["hero_boxes"].append((238 + 8, 14 + 52 - h24.size[1], 238 + 8 + h24.size[0], 14 + 52)) + d.text((238, 116), "结算明细带语境\n(同剪影 24px)", font=F_LBL2, fill=(225, 215, 195)) + # ④ 敌方描边对照:字形多向偏移暖白垫底+本体深朱 + for ddx, ddy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]: + d.text((350 + 32 + ddx, 14 + 32 + ddy), "█", font=F_STANDEE, fill=(240, 226, 200), anchor="mm") + d.text((350 + 32, 14 + 32), "█", font=F_STANDEE, fill=(150, 60, 48), anchor="mm") + d.text((350, 116), "敌方描边=暖白族\n(非黑非深褐)", font=F_LBL2, fill=(225, 215, 195)) + # ⑤ 护甲三枚展示(源 24×24 挂 HP_BAR_BACK 素底——工单 §5.3 挂载底色口径) + for i, key in enumerate(["light", "ward", "heavy"]): + ax = 470 + i * 60 + d.rectangle([ax, 14, ax + 40, 54], fill=(40, 34, 32)) + tex = _armor_tex(key) + if tex is not None: + img.paste(tex, (ax + 8, 22), tex) + meta["armor"][key] = (ax + 8, 22, ax + 8 + 24, 22 + 24) + d.text((ax + 4, 58), {"light": "轻甲", "ward": "魔甲", "heavy": "重甲"}[key], font=F_LBL2, fill=(225, 215, 195)) + d.text((470, 116), "护甲三枚 24×24\n挂载底=名字行素底", font=F_LBL2, fill=(225, 215, 195)) + # ⑥ 遮挡对照组:敌像(最底)→旗→小人+蓝框→数字(最顶)——P5 断言:顶层三件仍可辨 + cx, cy0 = 700, 10 + d.rectangle([cx, cy0, cx + 68, cy0 + 68], fill=(24, 20, 16)) + img.paste(FLOORS[1].resize((64, 64), Image.NEAREST).convert("RGB"), (cx + 2, cy0 + 2)) + d.text((cx + 34, cy0 + 30), "▆", font=ImageFont.truetype(ZH_FONT, 30), fill=(110, 104, 98), anchor="mm") + px_, py_ = cx + 30, cy0 + 12 + d.line([(px_, py_), (px_, cy0 + 52)], fill=(222, 210, 186), width=2) + d.polygon([(px_, py_), (px_ + 14, py_ + 4), (px_, py_ + 9)], fill=SEAL_BLACK) + h28 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST) + img.alpha_composite(h28, (cx + 36, cy0 + 40 - h28.size[1])) + d.rectangle([cx + 3, cy0 + 3, cx + 65, cy0 + 65], outline=HERO_BLUE, width=2) + d.text((cx + 14, cy0 + 46), "6", font=ImageFont.truetype(ZH_FONT, 24), fill=INK_L_WARM) + meta["overlap"] = (cx, cy0, cx + 68, cy0 + 68) + d.text((cx, cy0 + 76), "遮挡对照组:数字>小人>旗>敌\n(敌像最底,顶层三件可辨)", font=F_LBL2, fill=(225, 215, 195)) + return img, meta + + +# ══ 素材程序化产出(--gen-assets;art-prompts §5.2/§5.3 工单,色值全取 v3.22 在册值零新增) ══ +PLAZA_EDGE = (76, 64, 56) # 描边 #4C4038(§4.1 换色合并登记目标值) +PLAZA_RIM = (201, 175, 131) # 顶缘受光线 #C9AF83(掩尘同枚——合并登记) +PLAZA_BODY = (150, 130, 100) # 板身 #968264(STAND_STONE) +ARMOR_INK = (51, 38, 27) # 图标深墨描边 #33261B(主墨) +ARMOR_LIGHT_COL = (205, 184, 140) # 轻甲 #CDB88C(WARM_FAINT——换色合并⑤) +ARMOR_WARD_COL = (240, 226, 200) # 魔法护甲 #F0E2C8(暖白) +ARMOR_HEAVY_COL = (134, 140, 154) # 重甲 #868C9A(ARMOR_PLATE_COLD) + + +def _ink_outline_polygon(d, pts, fill, width=3): + """深墨描边画法:先画 width+2 的墨线再画 width 的面色(=1px 墨缘,工单「1px 深墨描边」)。""" + d.line(pts, fill=ARMOR_INK, width=width + 2) + d.line(pts, fill=fill, width=width) + + +def gen_scene_plaza(): + """地台件 48×12(§5.2 工艺要点:1px 上描边+2px 顶缘受光线+8px 板身+1px 下描边;左右 1px 全周闭合)。""" + im = Image.new("RGBA", (48, 12), PLAZA_BODY + (255,)) + d = ImageDraw.Draw(im) + d.rectangle([0, 0, 47, 11], outline=PLAZA_EDGE + (255,), width=1) # 全周闭合描边 + d.rectangle([1, 1, 46, 2], fill=PLAZA_RIM + (255,)) # 顶缘受光线 2px(纵向不缩放区) + return im + + +def _armor_canvas(): + return Image.new("RGBA", (24, 24), (0, 0, 0, 0)) + + +def gen_armor_light(): + """轻甲 = 皮条/布带记号(斜跨皮带+扣件,甲面覆盖率≈0,主体留空——§5.3.1;3px 带宽 1px 墨描边)。""" + im = _armor_canvas() + d = ImageDraw.Draw(im) + _ink_outline_polygon(d, [(5, 18), (18, 6)], ARMOR_LIGHT_COL, width=3) # 斜跨皮带 + d.rectangle([9, 9, 14, 14], outline=ARMOR_INK + (255,), width=1) # 皮带扣 + d.rectangle([10, 10, 13, 13], fill=ARMOR_LIGHT_COL + (255,)) + return im + + +def gen_armor_ward(): + """魔法护甲 = 符纹饰带(横向符纹带+两枚 ≤3×3 符点,零金属甲板——§5.3.2)。""" + im = _armor_canvas() + d = ImageDraw.Draw(im) + d.rectangle([4, 10, 19, 14], fill=ARMOR_INK + (255,)) + d.rectangle([4, 11, 19, 13], fill=ARMOR_WARD_COL + (255,)) # 符纹带 + d.rectangle([8, 17, 10, 19], fill=ARMOR_WARD_COL + (255,)) # 符点一(3×3) + d.rectangle([13, 4, 15, 6], fill=ARMOR_WARD_COL + (255,)) # 符点二(3×3) + return im + + +def gen_armor_heavy(): + """重甲 = 金属甲板覆盖肩与胸(肩两块+胸一块,覆盖率三档最大——§5.3.3)。""" + im = _armor_canvas() + d = ImageDraw.Draw(im) + for box in [(3, 3, 10, 9), (13, 3, 20, 9), (7, 12, 16, 21)]: # 肩×2+胸×1 + d.rectangle(list(box), fill=ARMOR_INK + (255,)) + d.rectangle([box[0] + 1, box[1] + 1, box[2] - 1, box[3] - 1], fill=ARMOR_HEAVY_COL + (255,)) + return im + + +def gen_batch_assets(): + """产出四件素材并入包:地台件 scene/ + 护甲三枚 ui/icons/(§5.2/§5.3——程序化直出即终稿)。""" + out = [] + plaza_path = os.path.join(ROOT, "game", "assets", "art", "scene", "scene_stage_plaza.png") + os.makedirs(os.path.dirname(plaza_path), exist_ok=True) + im = gen_scene_plaza() + im.save(plaza_path) + out.append((plaza_path, im.size, 3)) + for key, fn in [("light", gen_armor_light), ("ward", gen_armor_ward), ("heavy", gen_armor_heavy)]: + p = os.path.join(ROOT, "game", "assets", "ui", "icons", "ui_armor_%s.png" % key) + im = fn() + im.save(p) + out.append((p, im.size, 2)) + for p, size, ncol in out: + colors = len({c for c in Image.open(p).convert("RGBA").getdata() if c[3] > 0}) + print("[gen] %s size=%s 不透明像素域色数=%d" % (p, size, colors)) + if size not in ((48, 12), (24, 24)) or colors > 32: + raise RuntimeError("素材产出校验失败: %s" % p) + print("[gen] 四件素材产出完成(§5.2/§5.3 工单口径)") + return out + + def main(): + if "--gen-assets" in sys.argv: + gen_batch_assets() + return 0 print("=== 样张 v3 方向稿生成 + 机检(参考) ===") board, placement, records, band, gmap = render_main_board() - canvas = Image.new("RGBA", (1560, 1560), (24, 20, 17, 255)) + canvas = Image.new("RGBA", (1560, 2080), (24, 20, 17, 255)) ok = run_checks(board, placement, records, band, gmap, canvas) - d = ImageDraw.Draw(canvas) - d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0)——格底分档+亮墨直画+色牌后备+B区看台+E格高墙立面(v3.1);机检 %s(方向稿参考,冻结归目检后机检批);巢穴纹=程序占位" % - ("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE) + # 三区 mockup(T1 底板 + T2 内容;板面在 render_battle_screen 内嵌装) + scr, zones, board_origin = render_battle_screen(board) + if "--save-baseline" in sys.argv: + os.makedirs(os.path.dirname(BASELINE_PNG), exist_ok=True) + scr.convert("RGB").save(BASELINE_PNG) + print("saved(baseline):", BASELINE_PNG) + return 0 + frames_strip, frames_meta = render_keyframes() + s6_img, s6_meta = render_outline_overlap() + run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_img, s6_meta) - # 三区 mockup - scr = render_battle_screen() - # D 区主样张板(10×10@45px=450×450)嵌进 mockup D 区(工具行之下居中) - bx = 244 + (1036 - board.size[0]) // 2 - by = 256 + 34 + (800 - 256 - 34 - board.size[1]) // 2 - scr.paste(board.convert("RGB"), (bx, by), board) + d = ImageDraw.Draw(canvas) + d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0;迭代 43 扩展:整屏两版+S1–S4/P1–P5 断言)——机检 %s(方向稿参考,冻结归目检后机检批)" % + ("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE) canvas.paste(scr, (20, 52)) - d.text((20, 866), "三区 mockup(1280×800 实比):B 区站桩(主角立绘 48×48 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)", + d.text((20, 866), "三区 mockup(1280×800 实比,B 区按代码 252):B 区站桩(主角立绘 96 朝右/敌字形占位/顺位 FS_HINT/意图素牌/血条=危险红/名行=素底牌+护甲 PNG)", col=(225, 215, 195), font=F_LBL) - d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光=蓝缘带/悬停金框/封印印记/危险格巢穴占位)", + d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光/悬停金框/封印印记/危险格巢穴占位)", col=(225, 215, 195), font=F_LBL) # S2 数字全档 @@ -1121,11 +1658,15 @@ def main(): s2h = s2.resize((s2.size[0] // 2, s2.size[1] // 2), Image.NEAREST) canvas.paste(s2h, (20, oy2 + 76)) - # S3 underdark + # S3 / S3b 图类型色板变体两套(§1.4 内容清单:至少两套) oy3 = oy2 + 76 + 60 s3 = render_s3() canvas.paste(s3, (20, oy3)) - d.text((20 + s3.size[0] + 14, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2", + s3b = render_s3b() + canvas.paste(s3b, (320, oy3)) + d.text((630, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2", + col=(225, 215, 195), font=F_LBL) + d.text((630, oy3 + 78), "S3b 变体第二套(deep_underdark 方向候选:灰黑+矿脉金属;\n§6.3 终值未冻结,方向示意)——两套并陈满足 §1.4「至少两套」", col=(225, 215, 195), font=F_LBL) # S4 图例 @@ -1134,40 +1675,72 @@ def main(): for name, rgb in [("封印黑", SEAL_BLACK), ("危险红", DANGER_RED), ("奖励金", REWARD_GOLD), ("小人蓝(候选)", HERO_BLUE), ("接触带微光(候选)", CONTACT_GLOW)]: d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66)) - d.text((x - 4, oy4 + 38), name, col=(225, 215, 195), font=F_LBL2) if False else d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195)) + d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195)) x += 150 for key in ["attack", "heal", "pierce", "magic", "blast"]: rgb = GLOW_GROUPS[key] d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66)) d.text((x - 4, oy4 + 38), GLOW_FAMILY[key] + "微光", font=F_LBL2, fill=(225, 215, 195)) x += 150 - d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源 + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度——详见机检报告;全过=四冻结,气质终审=批末用户目检", - col=(225, 215, 195), font=F_LBL2) + d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源" + " + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度", col=(225, 215, 195), font=F_LBL2) + d.text((20, oy4 + 82), "整屏四项 S1–S4 + 战斗表现五项 P1–P5 + A7 护甲对比 + V1 两版出口/V10 基线并排——详见机检报告;" + "全过=机检冻结,气质终审=用户目检(方向稿目检先行)", col=(225, 215, 195), font=F_LBL2) - os.makedirs(os.path.dirname(OUT_PNG), exist_ok=True) + # S5 三组关键帧条 / S6 剪影三语境+描边对照+遮挡对照组 + oy5 = oy4 + 104 + canvas.paste(frames_strip, (20, oy5)) + d.text((430, oy5 + 8), "S5 三组关键帧条(受击/退场/高光兑现,各起中末三帧;\n退场末帧=灰化残位——禁瞬消;每组标注减动效简化形态)", col=(225, 215, 195), font=F_LBL) + oy6 = oy5 + frames_strip.size[1] + 26 + canvas.paste(s6_img, (20, oy6)) + d.text((970, oy6 + 8), "S6 剪影跨舞台三语境(P2 灰度取样)+描边同屏对照\n(主角深墨/敌方暖白族)+护甲三枚展示+遮挡对照组(P5)", col=(225, 215, 195), font=F_LBL) + + # 两版样张出口(V1)+改前/改后并排(V10) + os.makedirs(os.path.dirname(OUT_ED1), exist_ok=True) canvas.convert("RGB").save(OUT_PNG) print("saved:", OUT_PNG) + scr.convert("RGB").save(OUT_ED1) + print("saved:", OUT_ED1) + scale = 1080.0 / scr.size[1] + w2 = int(round(scr.size[0] * scale)) + ed2 = Image.new("RGB", (1920, 1080), (0, 0, 0)) + ed2.paste(scr.convert("RGB").resize((w2, 1080), Image.NEAREST), ((1920 - w2) // 2, 0)) + ed2.save(OUT_ED2) + print("saved:", OUT_ED2) + check("V1", "样张两版产出(1280×800 基座 + 1920×1080=全局等比 1.35×+左右黑边,§8.2 1920 档策略)", + os.path.exists(OUT_ED1) and os.path.exists(OUT_ED2) + and Image.open(OUT_ED1).size == (1280, 800) and Image.open(OUT_ED2).size == (1920, 1080), + "ed1 %s / ed2 %s / 黑边各 %dpx" % (Image.open(OUT_ED1).size, Image.open(OUT_ED2).size, (1920 - w2) // 2)) + has_base = os.path.exists(BASELINE_PNG) + if has_base: + ba = Image.new("RGB", (2570, 810), (24, 20, 17)) + base_im = Image.open(BASELINE_PNG).convert("RGB") + ba.paste(base_im, (5, 5)) + ba.paste(Image.open(OUT_ED1).convert("RGB"), (1285 + 5, 5)) + ba.save(OUT_BEFORE_AFTER) + print("saved:", OUT_BEFORE_AFTER) + check("V10", "改前/改后并排对拍图产出(基线版与改后版两源样张尺寸一致+并排图生成)", + has_base and Image.open(BASELINE_PNG).size == (1280, 800) and os.path.exists(OUT_BEFORE_AFTER), + "baseline %s / ed1 1280×800 / 并排 %s" % (Image.open(BASELINE_PNG).size if has_base else "缺(先 --save-baseline)", OUT_BEFORE_AFTER)) + # A5b 终稿净空(装配后画布——A5 原断言跑在空白底板上,此处补实际成图净空) + edge_hits_final = edge_content(canvas) + check("A5b", "装配成稿四缘 2px 净空(实际画幅,零溢出)", edge_hits_final == 0, "缘带内容像素 %d" % edge_hits_final) + ok_all = all(c[2] for c in CHECKS) # 机检报告 os.makedirs(os.path.dirname(OUT_REPORT), exist_ok=True) with open(OUT_REPORT, "w", encoding="utf-8") as f: - f.write("# 样张 v3 机检报告【冻结】(2026-09-30 批 A——v3.5 用户验收态断言定值落锁;气质终审已由 2026-09-29 用户验收豁免,台账 281)\n\n") - f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档(世界底层档 L≈0.10,带 0.08–0.14)/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;断言定值=A3b 图底分离 Δ≥30(实测 38)+A4 尺寸基线(立绘 46×48/小人源 32×32 硬等);F6 沿用 v3.4 双明度带口径 Δ≥18。\n\n") - f.write("## v1 否决原因 → 断言映射\n\n") - f.write("| 否决原因(2026-09-28 用户) | 断言承载 |\n|---|---|\n") - f.write("| ①显示太丑(总纲) | A6 观感密度(整板明度 std ≥50.9=v1 实测 44.31×1.15)+既有装饰变体 ≥2 种 |\n") - f.write("| ②封印色冲突 | 封印黑 #000000(9b6fcf6 已落,A1a 对表自动覆盖) |\n") - f.write("| ③立体感不够 | A3 阈值 40→55 + F6 顶面/立面双明度带 Δ≥18 + F7 整板悬崖 DEPTH_PX 同源 |\n") - f.write("| ④微光格只留微光 | F5c 微光段零 draw_inlay(d1dea82 已落) |\n\n") + f.write("# 样张 v3 机检报告(2026-09-30 批 A 断言定值 + 迭代 43 整屏扩展 S1–S4/P1–P5/V1/V10/A7/A5b)\n\n") + f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;A3b 图底分离 Δ≥30;A4 尺寸基线(立绘 46×48/小人源 32×32 硬等)。" + "迭代 43 扩展:B 区按代码 252 出样;整屏四项与战斗表现五项断言落 CHECKS;两版样张出口与基线并排对拍。方向稿目检先行,机检冻结归目检通过后。\n\n") f.write("## 断言明细\n\n") f.write("| 断言 | 判据 | 结果 | 明细 |\n|---|---|---|---|\n") for cid, name, okk, detail in CHECKS: f.write("| %s | %s | %s | %s |\n" % (cid, name, "PASS" if okk else "**FAIL**", detail.replace("|", "/"))) - f.write("\n结论:**%s**(%d/%d PASS)。v3 方向稿机检=可行性参考(A3 亮墨对比为硬指标);冻结 = 用户目检方向稿后机检批(v3.0 闸形:目检先行→机检冻结)。\n" % - ("全绿" if ok else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS))) + f.write("\n结论:**%s**(%d/%d PASS)。\n" % ("全绿" if ok_all else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS))) print("saved:", OUT_REPORT) - print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS))) - return 0 if ok else 1 + print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok_all else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS))) + return 0 if ok_all else 1 if __name__ == "__main__":