diff --git a/design/art/art-bible.md b/design/art/art-bible.md index 096b8d99..f47a6afe 100644 --- a/design/art/art-bible.md +++ b/design/art/art-bible.md @@ -1,6 +1,6 @@ # 美术圣经(Art Bible)— Dungeon Sweeper -> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**(§6 环境/§7 UI/§8 资产规范/§9 参考与反参考已于 2026-09-23 v1.8 按 D1/D2/D4/D5 框架成文;D3=§5 角色已于 v1.6 落地、v1.7 抽离独立文档;框架 2026-09-22 用户拍板,原登记于中间产物修订指导,经裁决不入库,明细归台账 125);历次修订明细见文末版本历史(现行为 v2.13) +> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**(§6 环境/§7 UI/§8 资产规范/§9 参考与反参考已于 2026-09-23 v1.8 按 D1/D2/D4/D5 框架成文;D3=§5 角色已于 v1.6 落地、v1.7 抽离独立文档;框架 2026-09-22 用户拍板,原登记于中间产物修订指导,经裁决不入库,明细归台账 125);历次修订明细见文末版本历史(现行为 v2.14) > > **Art Director Sign-Off (AD-ART-BIBLE)**:历次批均 SKIPPED(v1.8 §6–9 补全亦 SKIPPED)— Lean/Solo mode(非 PHASE-GATE;§6–9 已成文,full 门禁签核时机待用户表态) > 风格基调(2026-09-10 用户定稿):地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身 @@ -384,7 +384,7 @@ ### 7.2 UI 色板细则(自 §4.4 迁入) -**数据素面的墨来自世界的墨**:UI 数字、进度、准线一律主墨 `#33261B` 系——棋盘与面板读同一种墨;容器穿木石材质色,数据只穿墨。**营地与棋盘**:菜单容器背景用全作最暖的烛光黄 `#F6C667` 与木色 `#8A5A34`(§2.8;场景构成 §6.2);战斗容器转中性石色,防暖色渗入棋盘分走数字对比预算——氛围色向可读色让路。UI 不新增语义色,一切强调沿用 §4.2。 +**数据素面的墨来自世界的墨**:UI 数字、进度、准线一律主墨 `#33261B` 系——棋盘与面板读同一种墨;容器穿木石材质色,数据只穿墨。**营地与棋盘**:菜单容器背景用全作最暖的烛光黄 `#F6C667` 与木色 `#8A5A34`(§2.8;场景构成 §6.2);战斗容器转中性石色,防暖色渗入棋盘分走数字对比预算——氛围色向可读色让路。UI 不新增语义色,一切强调沿用 §4.2。**暗底呈现态双色墨制**(关卡画面样张 v1 冻结 2026-09-28):暗底系(已过/未来/候选/Boss 方块、暗数据框)图标与读数一律暖白 `#F0E2C8`(WARM)、亮底(当前金 `#F0A828`)转深墨 `#181410`(INK_ROUTE)——各态对比 ≥4.5:1;已过节点暗金冻结 `GOLD_DONE #68562C`(专用呈现态值,旧 GOLD_DIM `#8C743C` 对双色墨均不达标退役出该呈现位);Boss 描边 = 紫赤 2px + 外缘 1px 深墨环(灰度冗余通道——§4.2 色相独占本色不变,§4.5 灰度可分类承载)。 ### 7.3 布局栅格 @@ -469,6 +469,7 @@ ## 版本历史 +- **v2.14(2026-09-28)关卡画面样张 v1 冻结批(M-2 迭代 4 T1–T4,机检闸形 A——11/11 全绿即冻结)**:§7.2 增「暗底呈现态双色墨制」条(暗底系图标/读数=暖白 #F0E2C8、当前金底转深墨 #181410,各态 ≥4.5:1;已过节点暗金冻结 **GOLD_DONE #68562C** 专用呈现态值——旧 GOLD_DIM #8C743C 对双色墨均不达标;Boss 描边=紫赤 2px+外缘 1px 深墨环灰度冗余通道——§4.2 色相独占本色不变);冻结面=节点图标形制(♻ kenney-board-game-icons 五键 bow/award/campfire/crown_b/pouch,20px 烘焙 1:1 消费,WARM 单色,复用闸①②③④)+通道质感(折线 2/3/4px 直角肘形)+画布公式(70+层×122+34/52+槽×58+26+8 下限 620×240——routebug 83c11a1 根修件同源)+底栏样式(暗数据框 WARM/WARM_SOFT 双墨)+屏底(BG_ROOT+backdrop_stone);换层色板渐变挂点落码(§2.7 切片档——level_index→tint 表候选值随 §6.3 方向,明度带归一,终值归色板样张批)。规格承载=level-map.md(修订记录同日行);产物=`artwork/mockups/level-map-sheet-v1-2026-09-28.png`+`design/research/level-map-sample-v1-机检报告-2026-09-28.md`;气质终审=M-2 出口闸真人目检 - **v2.9(2026-09-26)高墙瓦片体系定稿批**(高墙批全程收官,用户拍板;台账 163/164):§3.2 新增「高墙瓦片体系」条——未挖格=一整块 32×32 自含凸起岩体(方案甲核心定义)+术语(墙瓦片/地面瓦片)+墨线边界纪律(1px 深墨线直线零锯齿,职责=分隔不融合)+单一北受光(北缘 1px 微亮棱/南缘立面窄暗条/凹角角点+立面侧 1px 齐平收头),定稿=`game/assets/art/board/terrain_*` 21 枚;§8.5 增「生产分工」条——材质归模型(AI 只出无缝填充纹理)、结构归程序(autotile 拼接约束超 2D 生图能力,实证在案)+镜像规则(只许 flip_h 禁旋转)。条口径=样张批追认(与 §8.4 方向集先例同)。词表与查表逻辑、搜包评估记录归 design/research 两档;过程文档随批清退(中间产物不入库先例,台账 127)。 - **v2.10(2026-09-26)封印紫批**(用户拍板:插旗色 旗朱 #C63D22 → **封印紫 #7C3AA8**,色相 ≈267°):§4.1 旗朱条改写为封印紫条(候选值样张批冻结;禁入带合规+与魔法微光/妨害紫间距登记)、§4.2 红只说危险改单档(旗朱预告档退役)、§3.2 旗条/§1.4 验收 4/§4.3 不变槽/§3.2 变异互辨/§5.4 速览随批。**语义面**——封印自术语批(2026-09-25)获专属色相,红族收敛为危险红单档;攻击预告/弹道/拒绝等非封印红统一归危险红族(RED_MINE)。**连带**——character-design §5.4 背旗、battle.md 三处、关卡节点地图 Boss 描边、清单封印特效行随批;comfyui-prompts 生图卡颜色词随样张 v2 重生成。 - **v2.11(2026-09-28)封印黑批**(用户拍板:封印色 封印紫 #7C3AA8 → **封印黑 #000000**——紫色与微光蓝族色相冲突,黑色无色相天然合规):§4.1 封印紫条改写封印黑条、§3.2 旗条/§1.4 不变槽/§4.3/§5.4 速览随批;代码 UiKit.SEAL_PURPLE→SEAL_BLACK 改名迁移+board_view/board_fx_view 换色+样张脚本同步;胜利彩带装饰紫位改亮金(紫赤系 Boss 独占不入装饰);远程生图单主角旗 purple→black 十六进制锁色重锁。历史行(v2.10 等)按 ADR 纪律留痕不改 diff --git a/design/gdd/ux/level-map.md b/design/gdd/ux/level-map.md index c3921c1b..6b4e180a 100644 --- a/design/gdd/ux/level-map.md +++ b/design/gdd/ux/level-map.md @@ -1,6 +1,6 @@ # UX Spec: 关卡画面(选路枢纽,Level Map) -> **Status**: 占位升格版——三项设计提案已拍板收口(2026-09-27:底栏状态展示 / 图上点击选路·候选按钮退役 / 放弃二次确认);**形态已升格独立屏(2026-09-28 用户裁决:独立存在为最终形态,覆盖层实现定性=测试阶段形态)**;视觉终稿以关卡画面样张为前置(M-2 迭代 4) +> **Status**: 定稿版——三项设计提案已拍板收口(2026-09-27:底栏状态展示 / 图上点击选路·候选按钮退役 / 放弃二次确认);**形态已升格独立屏(2026-09-28 用户裁决:独立存在为最终形态)**;**视觉终稿已冻结(2026-09-28 关卡画面样张 v1 机检 11/11 全绿,闸形 A——冻结值见 Layout/组件清单与样张机检报告,M-2 迭代 4)** > **Author**: user + ZCode(关卡画面 UX 批) > **Last Updated**: 2026-09-28 > **Platform Target**: PC (Steam)——Keyboard/Mouse 主,全部操作纯鼠标可完成(同 [battle.md](battle.md)) @@ -65,7 +65,7 @@ | 区 | 位置与尺寸 | 内容 | |---|---|---| | **顶栏语境** | 居中横排,图区上方,高约 32px | 左:关卡名(`level_row.display_name`,如「第二层 · 幽暗地域」);右:GP 常驻徽章(金币图标+数字,直角细线数据框——GP 值域超点阵适用档,走 pattern 5 的 When-NOT-to-Use 路线);HP 常驻读数是否并列右侧 = OQ1 待拍板,拍板前不实现 | -| **节点图区** | 居中 | 整幅节点图自绘:节点方块 + 通道折线 + 节点态着色 + Boss 紫赤描边。现行程序化占位为定值画布 620×240(层距 122px / 行距 58px / 节点方块 52×36px,现两关行集均容得下);目标形态随图自适应(画布宽随层数、高随同层最大节点数),公式与终值随实装批定、视觉随样张批 | +| **节点图区** | 居中 | 整幅节点图自绘:节点方块 + 通道折线 + 节点态着色 + Boss 紫赤描边。画布随图自适应(**冻结公式**:宽 = 70+层深×122+34、高 = 52+槽位×58+26+8,下限 620×240——routebug 83c11a1 根修件);通道折线**冻结 2/3/4px**(未点亮石灰 STONE/候选亮金 GOLD_LIT/悬停加粗);节点方块 52×36 内**节点图标 20px 烘焙 1:1**(♻ kenney 五键:战=弓/赏=桂冠/疗=篝火/王=王冠/商=钱袋,WARM 单色、当前金底转深墨——双色墨制)+ 编号 10px 右上角 | | **底部状态栏** | 图区下方横排,高约 48px | 左-中:状态数据框组(直角细线,pattern 5 族)——生命(当前/上限 + 迷你血条)、属性(攻击 / 护甲 AC,有效值 + 加成角标)、本关加成(攻+X · 上限+Y · AC+Z,零加成隐藏);右端:放弃冒险按钮(危险红族文字,与状态数据框保持间距——破坏性确认异位,pattern 7) | ### Component Inventory @@ -74,7 +74,7 @@ |---|---|---|---|---| | 关卡名标签 | 文本 | `level_row.display_name`(关卡表行读数) | 只读 | 新建(现行标题位升级) | | GP 常驻徽章 | 直角细线数据框 | 金币图标 + 数字(`runner.gp`,冒险钱包读数;刷新经 `wallet_changed` 信号投影〔gain/buy/clear 三事件〕或写路径回执后重查) | 只读 | 新建(GP 去向拍板落地面) | -| 节点图画布 | 自绘 Control | 节点方块(五类型字面 + 编号)、通道折线、节点态着色、Boss 紫赤描边(BOSS_PURPLE——2026-09-26 封印紫批语义:紫赤本色) | 悬停预承诺 / 点击提交选路(唯一通道) | ✅ level_map_screen.gd `_draw_graph`(程序化占位;2026-09-28 形态升格随屏改名) | +| 节点图画布 | 自绘 Control | 节点图标(五键 20px 烘焙,双色墨制)+ 编号、通道折线、节点态着色、Boss 紫赤描边+深墨环(BOSS_PURPLE——2026-09-26 封印紫批语义:紫赤本色) | 悬停预承诺 / 点击提交选路(唯一通道) | ✅ level_map_screen.gd `_draw_graph`(视觉终稿已落码——M-2 迭代 4 T2/T3) | | 状态数据框组 | 直角细线数据框 ×3 | 生命(当前/上限 + 迷你血条)、属性(攻击 / 护甲 AC 有效值 + 加成角标)、本关加成(加算区增量列表,零加成隐藏) | 只读 | 新建(2026-09-27 拍板:底栏状态展示) | | 放弃冒险按钮 | 按钮(危险红族) | 「放弃冒险(同失败结算——已获大奖励保留)」;二次确认态已拍板(见布局决策 3) | 点击(首击转确认态、二击提交) | ✅ level_map_screen.gd(可见性归组合根,仅冒险模式开) | | 标题行 | 文本 | 停点语境提示(见布局决策 1 三停点标题) | 只读 | ✅ level_map_screen.gd `_title`(措辞已按布局决策 1 改稿) | @@ -110,7 +110,7 @@ └──────────────────────────────────────────────────────────────────────────────┘ ``` -> 图为岔路停点示意(第二关行集):已过节点暗金、当前节点金、候选节点亮金描边、Boss 紫赤描边。节点字形(战/赏/疗/王/商单字)与折线通道均为程序化占位——终稿 = 节点图标组(♻ 素材批采购清单在案;图标键候选家族 = `node_type` 五键,扩键走 art-bible §4.6 修订)+ 地牢通道质感,随表现层样张批。 +> 图为岔路停点示意(第二关行集):已过节点暗金(GOLD_DONE #68562C)、当前节点金、候选节点亮金描边、Boss 紫赤描边+外缘深墨细环(灰度冗余通道)。终稿 = 节点图标组(♻ kenney 五键已接线,样张 v1 冻结)+ 通道折线 2/3/4px(已落码,M-2 迭代 4)。 --- @@ -123,7 +123,7 @@ | **岔路停点** | 兑付确认后、≥2 个下一跳 | 标题「选择下一节点」;候选按钮横排;亮金通道与候选描边逐一分叉可辨 | | **悬停预承诺** | 悬停候选节点 | 该节点亮金描边加粗 + 所属通道增亮 + 光标切指针态;移出即回落(仅预承诺,点击才提交) | | **放弃确认态** | 首击放弃按钮 | 文案转确认语 + 危险红边框脉冲约 3 秒;超时或移出回落;二击提交 `abandon_requested` | -| **换层进入** | 上一关结算确认 → 新关开始 | 本屏以新关卡数据淡入(无专属开场演出——art-bible §2.7 下探过渡演出的承载归场景表现批,本屏不设计) | +| **换层进入** | 上一关结算确认 → 新关开始 | 本屏以新关卡数据淡入;屏底世界侧随层进换挡(**换层色板渐变挂点**——§2.7 切片档:只动色相/饱和、明度带归一,level_index→tint 表候选值 = art-bible §6.3 三关方向,终值归色板样张批;M-2 迭代 4 落挂点) | | **减动效** | 设置开关(切片期 TunePanel) | 淡入、描边脉冲等全部 tween 时长减半或跳终态(UiKit.motion 单点包装,同 battle.md 通道) | **无拒绝子态**:候选提交恒合法(候选集即合法集),本屏无资源面/棋盘面拒绝路径;战斗中放弃由组合根可见性封死(按钮只在选路停点可见——接口契约:战斗中放弃一律拒绝)。 @@ -172,8 +172,8 @@ 无障碍基线权威 = [HUD框架](../systems/表现与支撑/HUD框架.md)「可访问性基线」节,本屏无新增豁免。呈现侧内建承诺: -- **节点态形状冗余**(全局语义红线「无任何信息仅靠色相承载」的合规项):已过 = 暗金无描边、当前 = 金填充 + 唯一焦点方块、候选 = 亮金粗描边 + 候选按钮文字冗余、未来 = 暗底无描边;Boss = 紫赤描边 + 「王」字形冗余。四态在灰度截图下可分类(AC) -- **对比**:节点字面对方块底、顶栏数字对底 ≥4.5:1;色值取 art-bible §4.1 冻结色板(占位亦适用) +- **节点态形状冗余**(全局语义红线「无任何信息仅靠色相承载」的合规项):已过 = 暗金无描边、当前 = 金填充 + 唯一焦点方块、候选 = 亮金粗描边、未来 = 暗底无描边;Boss = 紫赤描边 + 外缘 1px 深墨环(**灰度冗余通道**——紫赤灰度对石纹噪底不可辨,样张 v1 L2 机检逼守)+ 图标冗余。四态在灰度截图下可分类(AC,样张 L2 断言承载) +- **对比**:节点图标对方块底(双色墨制:暗底态暖白/当前金底深墨,各态 ≥4.5:1)、顶栏数字对底 ≥4.5:1(样张 L3 断言承载);色值取 art-bible §4.1 冻结色板(已过暗金 = GOLD_DONE #68562C 冻结新值) - **字号下限**:节点字形 ≥14px、标题 ≥16px、按钮/徽章 ≥14px(1280×800 基座,像素字体整数倍缩放) - **破坏性异位**:放弃按钮与候选按钮分区放置(位置通道,art-bible §4.5 #7);确认态不依赖色相单独承载(文案 + 脉冲双通道) - **纯鼠标**:全部操作纯鼠标可完成;MVP 不做键盘焦点序(同 battle.md 留痕) @@ -219,7 +219,7 @@ **已收口(2026-09-27 用户拍板,正文随拍板改写)**:① 底栏 = 状态展示(生命 + 属性 + 本关已选加成——原提案仅顶栏 HP 读数,用户扩为底栏状态栏);② 图上直接点击选路,候选按钮退役;③ 放弃冒险二次确认。 -1. **视觉终稿**:节点字形/通道质感/画布参数/底栏数据框样式以表现层样张过审为前置(节点图标组 ♻ 采购清单在案);本 spec 的 AC 为功能性判据,不含视觉终稿判据(同 level-clear.md OQ4 先例) +**已收口(2026-09-28 样张 v1 机检全绿,闸形 A——正文随冻结改写)**:④ **视觉终稿**——节点图标形制/通道质感/画布公式/底栏数据框样式/屏底以关卡画面样张 v1 机检 11/11 全绿冻结落码(M-2 迭代 4;冻结值登记见 `design/research/level-map-sample-v1-机检报告-2026-09-28.md`,气质终审 = M-2 出口闸真人目检) --- @@ -241,6 +241,7 @@ | 日期 | 改动 | |---|---| +| 2026-09-28 | **视觉终稿冻结批(M-2 迭代 4 T1–T4,闸形 A:样张 v1 机检 11/11 全绿即冻结)**:Status 升格定稿版;Layout 节点图区冻结值成文(画布公式 70+层×122+34 / 52+槽×58+26+8 下限 620×240、通道 2/3/4px、节点图标 ♻ kenney 五键 20px 烘焙+双色墨制、编号右上角);已过态 = GOLD_DONE #68562C 冻结新值(旧 GOLD_DIM 对双色墨 <4.5:1 逼守);Accessibility 增 Boss 深墨环灰度冗余+对比断言承载注记;States 换层进入改「淡入+换层色板渐变挂点(§2.7 切片档)」;组件清单节点图画布实现锚更新;OQ ④ 视觉终稿收口;线框图注记随改。产物:样张 `artwork/mockups/level-map-sheet-v1-2026-09-28.png` + 机检报告 `design/research/level-map-sample-v1-机检报告-2026-09-28.md` + 出口闸检查单 `design/qa/m2-exit-gate-checklist-2026-09-28.md` | | 2026-09-28 | **形态升格独立屏批(M-2 迭代 3 T1,用户裁决:独立存在为最终形态,覆盖层实现定性=测试阶段形态)**:Status/覆盖范围/到达方式/Navigation Position——「模态覆盖层形态(暗幕 0.6 底层透出)」全量改写为「独立屏独占呈现(战斗屏宿主整体隐藏,零透出)」;Layout 屏底=深底+石纹底噪(M-1 框材件);Transitions=独立屏淡入 200ms(alpha 淡入非明度变化——art-bible §2);AC「模态纪律」条改「独立性断言」(激活期战斗屏隐藏/选路提交战斗节点后恢复/兑付覆盖层关闭后本屏恢复);节点/商店覆盖层「本屏域内弹出」语义随形态明确(本屏保持激活,模态栈遮挡恢复语义复用);实现锚改名 level_map_screen.gd(RouteOverlay 退役);battle.md 三处选路覆盖层措辞随改;视觉终稿前置改指 M-2 迭代 4 关卡画面样张 | | 2026-09-27 | 初稿(关卡画面 UX 批,用户拍板立项):占位升格版——信息层级五层/三分区布局/组件清单/三停点状态/交互与数据锚/AC 成文;停点标题措辞修正(清偿台账 143 遗留);GP 常驻徽章落地面(2026-09-26 二轮拍板去向,经 GP 归属拍板对齐批维持);三项提案挂 OQ(HP 常驻读数/图上直接点击选路/放弃二次确认)登记 06 待拍板;视觉终稿以表现层样张为前置。实现锚 = route_overlay.gd 程序化占位现行 | | 2026-09-27 | **三拍板收口批(用户逐项裁决)**:① 底栏 = 状态展示——生命/属性(攻击·AC 有效值)/本关已选加成三组常读(原提案顶栏 HP 扩为底栏状态栏,布局决策 4);② 图上直接点击选路定稿——候选按钮整列退役、图上点击为唯一提交通道(布局决策 2;press_candidate 探针面随实装批改造注记);③ 放弃冒险二次确认定稿(布局决策 3)。信息层级六层重排/组件清单/States/交互表/Data Requirements(属性与加成读数入口)/AC/线框图随改;06 拍板点条目解决移除(去向 = 本批台账 204) | diff --git a/design/qa/m2-exit-gate-checklist-2026-09-28.md b/design/qa/m2-exit-gate-checklist-2026-09-28.md new file mode 100644 index 00000000..cecda039 --- /dev/null +++ b/design/qa/m2-exit-gate-checklist-2026-09-28.md @@ -0,0 +1,58 @@ +# M-2 出口闸检查单(2026-09-28,迭代 4 收口批交付) + +> 用途:M-2「关卡画面独立化」出口闸 = **真人目检清单全绿**(机器闸已全部自动过);本单即人闸载体,逐项可勾。 +> 全绿 = M-2 关闭 → **M-3「切片数值标定与 Alpha 验收」解锁**(其门=M-2 出口闸——表现彻底定稿后读数不被呈现改动扰动)。 +> 目检方式:实机跑 `godot --path game`(或开 Godot 编辑器 F5)开一局冒险,按清单逐项看;任一项不过 = 回该批返工。 + +## 一、机器闸读数汇总(已自动过,留痕) + +### 迭代 3「独立屏架构」(四批次一次合并 74c8ebe,2026-09-28,台账 224) +| 卡 | 内容 | 读数 | +|---|---|---| +| T1 spec 形态升格 | level-map.md 覆盖层形态→独立屏独占呈现全量改写+battle.md 三处随改 | doc 三闸对基线零新增 | +| T2 独立屏落地 | RouteOverlay→LevelMapScreen+不透明屏底+组合根屏级互斥单一出口 | 冒烟 557/0+五探针 PASS | +| T3 全链流程 | 选路分支(战斗/Boss 出屏;奖励/恢复/商店本屏域内弹出) | 同上 | +| T4 探针独立性断言 | 屏激活期 _battle_root 隐藏=零透出契约/战斗节点提交后恢复 | ui_probe+ui_click_probe 断言扩展全绿 | + +### 迭代 4「关卡画面视觉终稿」(本 loop,9a79927→…) +| 卡 | 内容 | 读数 | +|---|---|---| +| T1 关卡画面样张 v1 | 机检 **11/11 全绿**(L1 图标互辨/L2 四态+Boss 环灰度/L3 对比 ≥4.5:1/L4 禁入带/L5 画布自适应〔routebug 83c11a1 回归〕/L6 独立屏底/L7 占位区不侵入/A1a 色板对表/SRC 同源/ST 态齐备/S3 换层示意)→ 视觉冻结值登记 | 报告 `design/research/level-map-sample-v1-机检报告-2026-09-28.md` | +| T2 节点图标组接线 | ♻ kenney-board-game-icons 五键(bow/award/campfire/crown_b/pouch,全 CC0)20px 烘焙量化,复用闸①②③④自检 PASS;LevelMapScreen 消费(load_tex 守卫缺位字形回退) | 冒烟 557/0+五探针 PASS | +| T3 视觉终稿落码 | 已过暗金 GOLD_DONE #68562C/Boss 双描边/通道 2-3-4px/底栏墨色(WARM/WARM_SOFT)/血条深底 | 同上 | +| T4 转场演出挂点 | 换层淡入(UiKit.motion 200ms)既有保持+换层色板渐变挂点(§2.7 切片档,level_index→tint 表,§6.3 候选方向)+Boss 胜利→关卡结算→下一关链路核对(探针驱动段全绿) | 同上 | + +- 冒烟检查数 557 = 开工基线(零删减);**合并落 master 后主区复跑**:冒烟 557/0+五探针全绿(台账 225 留痕)。 + +## 二、真人目检清单(逐项可勾,勾完=人闸过) + +- [ ] **1. 独立屏观感**:关卡开始即出图;战斗屏完全消失(零透出、非半透暗幕);屏底=深底+石纹底噪铺满 +- [ ] **2. 节点图标可辨**:图上五类节点=弓(战)/桂冠(赏)/篝火(疗)/王冠(王)/钱袋(商)图标清晰互辨;当前金底上图标转深墨可读 +- [ ] **3. 节点三停点态**:已过=暗金无描边、当前=亮金填充、候选=亮金描边(悬停加粗+指针)、未来=暗底;Boss=紫赤描边+外缘深墨细环;缩灰度/黑白窗口仍可分 +- [ ] **4. 底栏读数**:生命(迷你血条+HP 数字)/属性(攻击·AC+加成角标)/本关加成(零加成隐藏)三组清晰可读;放弃冒险=亮砖按钮红字右端异位 +- [ ] **5. 顶栏语境**:左关卡名、右 GP 徽章(◈ 数字,战后到账即刷) +- [ ] **6. 转场节奏**:选路提交→战斗屏回现;战斗胜利→(大奖励屏确认)→下一关独立屏淡入(约 0.2s);第二/三层屏底色相较第一层有冷调层进(换层渐变挂点) +- [ ] **7. 图上点击选路**:点候选节点即出发(图上点击唯一通道),深层节点(单链第 5 节点后)点击必中 +- [ ] **8. 迭代 3 件**(已同库):放弃二次确认(首击转确认态+超时回落)/三停点标题(出发——选择首个节点/继续前进/选择下一节点) + +**判定**:1–7 全过 + 8 注记状态 → 人闸过,M-2 关闭;登记裁决入台账,roadmap 里程碑移动。 + +## 三、✦ 缺料清单(只登记不生图——生图执行权在用户) + +| 件 | 渠道 | 规格/提示词指针 | 现状承载 | +|---|---|---|---| +| 节点图标终稿精修(bow/award/campfire/crown_b/pouch 五键) | ♻ 已量化在役/✦ 可替换 | kenney-board-game-icons(CC0) 64px 源;替换须过复用闸四道+样张 L1 灰度互辨 | 20px 烘焙在役(本批 T2) | +| GP 徽章金币图标(现 ◈ 字形) | ♻/⚙ | 清单 §3.5 营地件同族;币形=§3.1 道具点剪影 | ◈ 字形+数字(样张注记) | +| 关卡画面换层 tint 终值 | ⚙ 色板样张批 | art-bible §6.3 三关方向(挂点已落,tint 候选值在役) | level_index→tint 表(本批 T4) | +| 关卡画面石纹底噪换挡版 | ⚙/✦ 可选 | §4.3 wall_ambient 槽换色 | tint 调制承载(切片档) | +| 行进器形象(图上"我在哪"具象化) | ✦/⚙ | 清单 §3.2 | 当前节点金块承载(无独立形象) | + +## 四、代拍板追认清单(本 loop 白名单内已裁,待用户过目) + +1. **已过暗金冻结新值 GOLD_DONE #68562C**(104,86,44)——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1(level-map AC「节点字面对方块底 ≥4.5:1」逼守);UiKit.GOLD_DIM 常量保留他用不动。 +2. **节点图标五键选型**=bow/award/campfire/crown_b/pouch(语义近似+灰度互辨最小错位 0.18;sword 已被锻造柜台占用不重用——§4.6「一键一纹样全作不复用」)。 +3. **Boss 描边=紫赤 2px+外缘 1px 深墨环**(灰度冗余通道)——紫赤灰度 72 对石纹噪底 65 灰度不可辨,样张 L2 机检逼守;§4.2 色相独占本色不变。 +4. **图标双色墨制**=暗底态 WARM 暖白/当前金底 INK_ROUTE 深墨(乘法调制);编号 10px 右上角(与图标占位区零重叠)。 +5. **通道折线宽取整**=未点亮 2/候选 3/悬停 4 px(原 1.5 随冻结值取整)。 +6. **换层 tint 候选值**=L2 (0.84,0.99,1.28)/L3 (0.97,0.99,1.09)(§6.3 方向、明度带归一≈1;终值归色板样张批)。 +7. 样张图标烘焙 20px(52×36 方块内 20px 图标+右上编号;1:1 消费零缩放——§8.2 缩放纪律从紧)。 diff --git a/design/research/level-map-sample-v1-机检报告-2026-09-28.md b/design/research/level-map-sample-v1-机检报告-2026-09-28.md new file mode 100644 index 00000000..8363d196 --- /dev/null +++ b/design/research/level-map-sample-v1-机检报告-2026-09-28.md @@ -0,0 +1,34 @@ +# 关卡画面样张 v1 机检报告(2026-09-28,M-2 迭代 4 T1) + +生成:`python tools/render_level_map_sheet.py`;闸形 A(M-1 迭代 2 先例)——断言集从 level-map.md AC + art-bible 硬指标直接推导,全过即冻结落码;气质终审 = 批末用户目检(=M-2 出口闸)。 + +## 冻结值登记(样张通过值 → T2/T3 落码) + +| 项 | 冻结值 | +|---|---| +| 节点图标形制 | kenney-board-game-icons(CC0) 64px 五键 NEAREST 22px 单色重着色:战=bow/赏=award/疗=campfire/王=crown_b/商=pouch;色相不承载身份(§4.6) | +| 图标双色墨制 | 暗底态(已过/未来/候选/Boss)=WARM 暖白;当前金底=INK_ROUTE 深墨(乘法调制实现) | +| 已过暗金 | **GOLD_DONE #68562C(104,86,44)冻结新值**——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1(对比硬指标逼守);LEVEL 呈现态专用,UiKit.GOLD_DIM 他用不动 | +| 通道质感 | 直角折线(中点肘形):未点亮 STONE 2px/候选 GOLD_LIT 3px/悬停 4px(引擎 1.5→2 随冻结值取整) | +| 画布公式 | level_map_screen._fit_canvas 同源:70+层深×122+34 / 52+槽位×58+26+8,下限 620×240(L5 同源断言) | +| 底栏样式 | 暗底直角细线数据框(PANEL_DARK+PANEL_BORDER):标签 12px WARM_SOFT/读数 15px WARM;迷你血条 GOLD 填充;放弃=亮石面按钮+危险红字 | +| Boss 描边 | 紫赤 #8F2F6A 2px(§4.2 色相独占)+外缘 1px 深墨环(灰度冗余通道——紫赤灰度 72 对噪底 65 不可辨,L2 逼守) | +| 屏底 | BG_ROOT #1E1914 + backdrop_stone 256 平铺(TEXTURE_NEAREST);独立屏不透明零透出 | + +## 断言明细 + +| 断言 | 判据 | 结果 | 明细 | +|---|---|---|---| +| L1 | 五节点图标灰度互辨(剪影两两错位率 ≥0.08) | PASS | 最小错位 0.18(('boss', 'shop'));五键={'battle': 'bow.png', 'reward': 'award.png', 'rest': 'campfire.png', 'boss': 'crown_b.png', 'shop': 'pouch.png'} | +| L2 | 节点四态灰度可分类+Boss 紫赤描边灰度可辨 | PASS | 填充灰度 done/current/cand/future=87/174/59/59;亮金环 191 紫赤环 72(Δ120) 深墨环 21 外噪底 65 | +| L3 | 数字/节点字面对比硬指标 ≥4.5:1(§4.1) | PASS | [('GP金×数据框', 5.5), ('GP金×屏底', 8.6), ('读数暖白×数据框', 8.8), ('读数暖白×屏底', 13.6), ('图标墨×当前金', 9.0), ('图标暖白×已过金', 5.6), ('图标暖白×未来暗', 8.8)] | +| L4 | 禁入带 HSV300°-345° 零命中(三屏, Boss 环区白名单) | PASS | 岔路 0/入口 0/换层 0 像素(腐蚀后抽样) | +| L5 | 画布随图自适应(单链深层 Boss 在呈现/命中域内,公式同源) | PASS | 公式={'dx': 122.0, 'dx_pad': 8.0, 'dy': 58.0, 'dy_pad': 8.0, 'min': (620.0, 240.0), 'hit_pad': 6.0};单链画布=(836.0, 240);Boss 块=(998.0, 144.0, 1050.0, 180.0)(右缘 1050+容差 6 ≤ 画布右缘 1058) | +| L6 | 独立屏底:不透明 BG_ROOT+石纹底噪(非半透暗幕零透出) | PASS | 四缘均亮 65/64/65/66 峰值 80/88/97/98(≤130);渲染源无战斗屏合成=True | +| L7 | 图标/编号占位区不侵入(零重叠,不越方块内缘) | PASS | 三屏 23 节点全净 | +| A1a | 色板对表圣经 §4.1/UiKit 冻结值 | PASS | {'BG_ROOT': '#1E1914', 'PANEL_DARK': '#403A34', 'GOLD': '#F0A828', 'GOLD_DONE': '#68562C', 'GOLD_LIT': '#E6BE5A', 'BOSS_PURPLE': '#8F2F6A', 'INK_ROUTE': '#181410', 'WARM': '#F0E2C8', 'RED_MINE(危险红族)': '#962C24'} | +| SRC | 画布公式/停点标题/HIT_PAD 与 level_map_screen.gd 同源 | PASS | 公式 70+122.0*层/52+58.0*槽 min(620.0, 240.0) HIT_PAD=6.0;标题={'entry': '出发——选择首个节点', 'single': '继续前进', 'fork': '选择下一节点'} | +| ST | 两停点态内容齐备(入口:全未来+单候选环;岔路:已过链+当前金+双候选+Boss 环) | PASS | 入口候选 1/岔路候选 2 已过 3 Boss 环 1 | +| S3 | 换层色板示意:色相层进+明度恒定+信息层恒定(§2.7/§4.3) | PASS | 基噪均色相 23→147(PIL 0-255);均明度 65→61(Δ≤12) | + +结论:**全绿**(11/11 PASS)。机检全过 = 关卡画面视觉冻结(图标形制/通道质感/画布参数/底栏样式/屏底)生效落码;气质终审 = 批末用户目检(=M-2 出口闸)。 diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index be202209..82901172 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -22,20 +22,25 @@ extends Control signal route_chosen(node_id: int) signal abandon_requested -const COL_ROOM_DONE := UiKit.GOLD_DIM # 已到——暗金 +const COL_ROOM_DONE := UiKit.GOLD_DONE # 已到——暗金(样张 v1 冻结新值 #68562C——旧 GOLD_DIM 对双色墨均 <4.5:1,对比硬指标逼守;UiKit.GOLD_DIM 他用不动) const COL_ROOM_CURRENT := UiKit.GOLD # 当前——奖励金 const COL_ROOM_FUTURE := UiKit.DARK_UPCOMING # 未达——暗 const COL_PASSAGE := UiKit.STONE # 通道——石灰 const COL_PASSAGE_LIT := UiKit.GOLD_LIT # 候选通道——亮金 const COL_BOSS_BORDER := UiKit.BOSS_PURPLE # Boss 边框——紫赤(色相独占本色;旗朱退役 2026-09-26) const COL_ABANDON := UiKit.RED_MINE # 破坏性动作红(危险红族) -const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21 +const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21(贴图缺位字形回退用——贴图在位=字形退役惯例) +const TEX_NODE_ICONS := {"battle": UiKit.TEX_NODE_BATTLE, "reward": UiKit.TEX_NODE_REWARD, + "rest": UiKit.TEX_NODE_REST, "boss": UiKit.TEX_NODE_BOSS, "shop": UiKit.TEX_NODE_SHOP} +const NODE_ICON_PX := 20.0 # 节点图标边长(样张 v1 冻结——20px 烘焙 1:1 消费零缩放) +const ICON_TINT_ON_GOLD := Color(24.0 / 240.0, 20.0 / 226.0, 16.0 / 200.0) # 当前金底图标墨 = INK_ROUTE 乘法调制(双色墨制——暗底系 WARM 原色) const TITLE_ENTRY := "出发——选择首个节点" # 入口停点(布局决策 1——台账 143 遗留清偿) const TITLE_SINGLE := "继续前进" # 单链停点 const TITLE_FORK := "选择下一节点" # 岔路停点 const NODE_HALF := Vector2(26, 18) # 节点方块半宽高(52×36 占位画布参数——样张批终稿) const HIT_PAD := 6.0 # 图上命中外扩(指宽容差) const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落) +const COL_HP_BAR_BACK := Color8(40, 34, 28) # 迷你血条深底(样张 v1 冻结——对暗面板可辨) var _runner = null var _title: Label @@ -53,9 +58,15 @@ var _hover_id := -1 # 悬停预承诺候选(-1 = 无) var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) var _abandon_armed := false # 二次确认态(首击置位、二击提交、超时/重开回落) var _confirm_left := 0.0 # 确认态剩余秒数(_process 倒计时+脉冲驱动) +var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) +var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点) +var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用) func _init() -> void: + # 节点图标贴图装载(样张 v1 冻结形制——load_tex 守卫,缺位回退现行字形) + for t in TEX_NODE_ICONS: + _node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) # 独立屏底(形态升格 2026-09-28——spec Layout:深底+石纹底噪铺满;底纹缺位回退纯色——load_tex 守卫) var base := ColorRect.new() base.color = UiKit.BG_ROOT @@ -70,6 +81,7 @@ func _init() -> void: tex.set_anchors_preset(Control.PRESET_FULL_RECT) tex.mouse_filter = Control.MOUSE_FILTER_IGNORE base.add_child(tex) + _noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) base.add_child(center) @@ -118,8 +130,11 @@ func _init() -> void: _bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) - bonus_inner.add_child(UiKit.label("本关加成", 12, false)) + var bonus_cap := UiKit.label("本关加成", 12, false) + bonus_cap.add_theme_color_override("font_color", UiKit.WARM_SOFT) + bonus_inner.add_child(bonus_cap) _bonus_value = UiKit.label("", 15, true) + _bonus_value.add_theme_color_override("font_color", UiKit.WARM) bonus_inner.add_child(_bonus_value) bottom.add_child(_bonus_row) var spacer := Control.new() @@ -133,19 +148,23 @@ func _init() -> void: visible = false -# 状态数据框工厂(直角细线数据框——pattern 5 族;hp_box = 带迷你血条) +# 状态数据框工厂(直角细线数据框——pattern 5 族;hp_box = 带迷你血条。墨色按样张 v1 冻结: +# 暗底标签 WARM_SOFT/读数 WARM——§7.1 数据素面墨来自世界的墨在暗底侧的落法,L3 ≥4.5:1) func _status_box(caption: String, with_bar: bool) -> PanelContainer: var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1)) var inner := VBoxContainer.new() panel.add_child(inner) - inner.add_child(UiKit.label(caption, 12, false)) + var cap := UiKit.label(caption, 12, false) + cap.add_theme_color_override("font_color", UiKit.WARM_SOFT) + inner.add_child(cap) if with_bar: _hp_bar = Control.new() _hp_bar.custom_minimum_size = Vector2(120, 6) _hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar)) inner.add_child(_hp_bar) var value := UiKit.label("", 15, true) + value.add_theme_color_override("font_color", UiKit.WARM) inner.add_child(value) if caption == "生命": _hp_value = value @@ -155,13 +174,13 @@ func _status_box(caption: String, with_bar: bool) -> PanelContainer: func _draw_hp_bar(bar: Control) -> void: - # 迷你血条(占位:暗底+金填充——比例 = hero_hp / effective_max_hp) + # 迷你血条(样张 v1 冻结:深底 Color8(40,34,28)(对暗面板可辨)+金填充——比例 = hero_hp / effective_max_hp) if _runner == null: return var max_hp := maxi(1, _runner.effective_max_hp()) var ratio := clampf(float(_runner.hero_hp) / float(max_hp), 0.0, 1.0) var full := Rect2(Vector2.ZERO, bar.size) - bar.draw_rect(full, UiKit.DARK_UPCOMING) + bar.draw_rect(full, COL_HP_BAR_BACK) if ratio > 0.0: bar.draw_rect(Rect2(Vector2.ZERO, Vector2(bar.size.x * ratio, bar.size.y)), UiKit.GOLD) @@ -225,6 +244,7 @@ func open_choices(cands: Array, runner) -> void: _cand_ids.append(_id_of(cand)) _fit_canvas(_runner.nodes()) _refresh_status() + _apply_level_tint(int(_runner.level_row.get("level_index", 0))) visible = true if not was_visible: # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) @@ -253,6 +273,22 @@ func _fit_canvas(nodes: Array) -> void: _canvas.custom_minimum_size = _content_size +# 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的 +# 色相层进预告:屏底世界侧随层进换挡,信息层恒定;tint 明度带归一≈1 只动色相/饱和)。 +# tint 候选方向 = art-bible §6.3 三关图类型色板方向(1 dungeon 暖石基准/2 underdark 冷蓝灰/3 deep 灰黑金属), +# 终值归色板样张批——本批只落挂点(level-map.md States 换层进入行承载)。 +const BOARD_TINTS := {1: Color(1, 1, 1), 2: Color(0.84, 0.99, 1.28), 3: Color(0.97, 0.99, 1.09)} + + +func _apply_level_tint(level_index: int) -> void: + if _noise_rect == null or level_index == _tint_level: + return + _tint_level = level_index + var target: Color = BOARD_TINTS.get(level_index, Color(1, 1, 1)) + var tw := create_tween() + tw.tween_property(_noise_rect, "modulate", target, UiKit.motion(0.45)) + + # 状态栏刷新(命令式纪律——组合根打开本屏/兑付确认回执后重查;零推送信号订阅) func _refresh_status() -> void: if _runner == null: @@ -368,7 +404,7 @@ func _draw_graph(canvas: Control) -> void: var lit: bool = _id_of(node) == current_id and cand_ids.has(tid) var hovered: bool = lit and _hover_id == tid var col := COL_PASSAGE_LIT if lit else COL_PASSAGE - var width := 4.0 if hovered else (3.0 if lit else 1.5) + var width := 4.0 if hovered else (3.0 if lit else 2.0) # 样张 v1 冻结:2/3/4(原 1.5 取整) _draw_passage(canvas, from, to, col, width) # 房间方块 for node in nodes: @@ -383,14 +419,28 @@ func _draw_graph(canvas: Control) -> void: var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) canvas.draw_rect(rect, col) if t == "boss": + # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道——样张 v1 L2 逼守: + # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) + canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) elif cand_ids.has(nid): # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) - var font := get_theme_default_font() - var label := String(NODE_LABELS.get(t, "?")) - canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) - canvas.draw_string(font, p + Vector2(10, 14), str(nid), HORIZONTAL_ALIGNMENT_LEFT, -1, 10, UiKit.WARM_ROUTE_SUB) + # 节点图标(样张 v1 冻结形制:20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; + # 贴图缺位 → 字形回退=贴图在位字形退役惯例) + var icon: Texture2D = _node_icons.get(t) + if icon != null: + var tint := ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) + canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, + Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) + else: + var font := get_theme_default_font() + var label := String(NODE_LABELS.get(t, "?")) + canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) + # 编号(右上角——与图标占位区零重叠,样张 v1 L7;双色墨随态) + var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM + canvas.draw_string(get_theme_default_font(), Vector2(rect.end.x - 16.0, rect.position.y + 8.0), + str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) func _layout(nodes: Array, canvas: Control) -> Dictionary: diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 8803ecda..e02da719 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -25,6 +25,7 @@ const GOLD := Color8(240, 168, 40) # 奖励金 const GOLD_DEEP := Color8(176, 122, 20) # 深金(金框描边/达意金) const GOLD_LIT := Color8(230, 190, 90) # 亮金(候选高亮) const GOLD_DIM := Color8(140, 116, 60) # 暗金(已过节点) +const GOLD_DONE := Color8(104, 86, 44) # 已过节点暗金·关卡画面呈现态(关卡画面样张 v1 冻结新值 #68562C——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1 对比硬指标逼守;GOLD_DIM 他用不动) const SEAL_BLACK := Color8(0, 0, 0) # 封印黑 #000000——封印(插旗)宣告色(2026-09-28 # 用户拍板旗朱→封印紫→封印黑:紫色与微光蓝族色相冲突,黑色无色相天然避开禁入带 @@ -112,6 +113,13 @@ const TEX_ICON_KEYSTONE := "res://assets/art/ui/icon_keystone.png" # const TEX_ICON_FORGE := "res://assets/art/ui/icon_forge.png" # 柜台一·属性锻造 const TEX_ICON_WORKSHOP := "res://assets/art/ui/icon_workshop.png" # 柜台二·道具工坊 const TEX_ICON_ATLAS := "res://assets/art/ui/icon_atlas.png" # 柜台三·奖励格图鉴 +# 节点图标五键(关卡画面样张 v1 冻结批 M-2 迭代 4 T2——20px 烘焙 1:1 消费,WARM 单色; +# 对象键 = node_type 文件侧延伸 §8.3;缺位 = load_tex null → 字形回退) +const TEX_NODE_BATTLE := "res://assets/art/ui/node_battle.png" # 节点图标·战斗房 +const TEX_NODE_REWARD := "res://assets/art/ui/node_reward.png" # 节点图标·奖励房 +const TEX_NODE_REST := "res://assets/art/ui/node_rest.png" # 节点图标·恢复房 +const TEX_NODE_BOSS := "res://assets/art/ui/node_boss.png" # 节点图标·Boss 房 +const TEX_NODE_SHOP := "res://assets/art/ui/node_shop.png" # 节点图标·商店房 # 素材纹理守卫装载(exists 检查避开缺位树上 ResourceLoader 报错刷屏——冒烟/无素材检出静默回退) diff --git a/tools/quantize_ui_skin.py b/tools/quantize_ui_skin.py index 65ea7202..c69c88c7 100644 --- a/tools/quantize_ui_skin.py +++ b/tools/quantize_ui_skin.py @@ -42,6 +42,8 @@ RAMP_BACKDROP = [(30, 25, 20), (120, 106, 88)] # BG_ROOT→P RAMP_BORDER = [(120, 106, 88), (201, 175, 131)] # PANEL_BORDER→BRICK ICON_GOLD = (240, 168, 40) # 奖励金(钥石) ICON_WARM = (240, 226, 200) # 暖白(柜台件,暗卡上可读) +NODE_WARM = (240, 226, 200) # 节点图标暖白(关卡画面样张 v1 冻结——暗底态原色,当前金底乘法调制深墨) +NODE_PX = 20 # 节点图标烘焙边长(52×36 方块内 20px 图标+右上编号,1:1 消费零缩放) def ramp_color(ramp, t): @@ -103,14 +105,55 @@ def main(): jobs.append(("icon_forge.png", recolor_flat(Image.open(src_path(*(icon_dir + ("sword.png",)))), ICON_WARM))) jobs.append(("icon_workshop.png", recolor_flat(Image.open(src_path(*(icon_dir + ("flask_full.png",)))), ICON_WARM))) jobs.append(("icon_atlas.png", recolor_flat(Image.open(src_path(*(icon_dir + ("book_open.png",)))), ICON_WARM))) + # 节点图标五件(board-game-icons 64px→20px 烘焙,WARM 单色——关卡画面样张 v1 冻结形制,M-2 迭代 4 T2; + # 战=bow/赏=award/疗=campfire/王=crown_b/商=pouch,语义近似+灰度互辨,色相不承载身份 §4.6) + node_jobs = (("node_battle.png", "bow.png"), ("node_reward.png", "award.png"), + ("node_rest.png", "campfire.png"), ("node_boss.png", "crown_b.png"), + ("node_shop.png", "pouch.png")) + node_outs = {} + for name, src in node_jobs: + im = recolor_flat(Image.open(src_path(*(icon_dir + (src,)))), NODE_WARM) + im = im.resize((NODE_PX, NODE_PX), Image.NEAREST) + node_outs[name] = im + jobs.append((name, im)) for name, im in jobs: im.save(out_path(name)) print("saved:", "/".join(OUT + (name,))) + node_icon_gates(node_outs) print("=== 量化改色完成(%d 件,全 CC0;闸①②③④ 见脚本头注) ===" % len(jobs)) return 0 +def node_icon_gates(outs): + """节点图标复用闸四道自检(闸④授权=kenney-board-game-icons CC0 全包在案): + ①量化=全像素 NODE_WARM 或透明(零原包杂色) ②禁入带=WARM 色相 39° 天然合规 + ③灰度互辨=alpha 剪影两两错位率 ≥0.08(与样张 L1 同判据) ④见上。""" + fail = [] + for name, im in outs.items(): + bad = sum(1 for px in im.getdata() if px[3] > 8 and px[:3] != NODE_WARM) + if bad: + fail.append("%s 闸①杂色 %d px" % (name, bad)) + if im.size != (NODE_PX, NODE_PX): + fail.append("%s 尺寸 %s" % (name, im.size)) + keys = list(outs.keys()) + worst = (1.0, None) + sigs = {k: [1 if px[3] > 96 else 0 for px in outs[k].getdata()] for k in keys} + for i, a in enumerate(keys): + for b in keys[i + 1:]: + diff = sum(1 for x, y in zip(sigs[a], sigs[b]) if x != y) / len(sigs[a]) + if diff < worst[0]: + worst = (diff, (a, b)) + if worst[0] < 0.08: + fail.append("闸③灰度互辨最小错位 %.2f %s" % worst) + if fail: + for f in fail: + print("[FAIL] 节点图标闸 %s" % f) + raise SystemExit(1) + print("[PASS] 节点图标复用闸①②③④(量化单色/禁入带天然合规/灰度互辨最小错位 %.2f %s/CC0)" + % worst) + + if __name__ == "__main__": import sys sys.exit(main()) diff --git a/tools/render_level_map_sheet.py b/tools/render_level_map_sheet.py new file mode 100644 index 00000000..1b115ad4 --- /dev/null +++ b/tools/render_level_map_sheet.py @@ -0,0 +1,601 @@ +# -*- coding: utf-8 -*- +"""关卡画面样张验收图生成脚本 v1(2026-09-28,M-2 迭代 4 T1)。 + +闸形 A(M-1 迭代 2 先例,2026-09-27 拍板):断言集从 level-map.md AC + art-bible 硬指标直接推导 +(本样张无历史否决原因输入),生成即验,全过即冻结落码;气质终审 = 批末用户目检(=M-2 出口闸)。 + +断言集(事实源=level-map.md AC/Accessibility + art-bible §4.1/§4.2/§2.7/§7): + L1 五节点类型图标互辨(灰度截图可分类——level-map AC;kenney-board-game-icons CC0 五键) + L2 节点四态(已过暗金/当前金/候选亮金描边/未来暗)灰度可分类 + Boss 紫赤描边灰度可辨 + L3 数字对比硬指标(§4.1):顶栏 GP 徽章/底栏读数对屏底与对数据框 ≥4.5:1;节点字面(图标)对方块底 ≥4.5:1 + L4 禁入带 HSV300°-345° 零命中(Boss 紫赤环 = §4.2 唯一合法占用者,环区白名单) + L5 画布随图自适应:深层节点在呈现/命中域内(routebug 83c11a1 回归;公式与 level_map_screen.gd 同源解析) + L6 独立屏底:不透明屏底(BG_ROOT+石纹底噪,非半透暗幕零透出) + L7 字形占位区不侵入:图标区与编号区 bbox 零重叠,双双不越方块内缘 + A1a 色板对表圣经 §4.1 冻结值;A1b 禁入带扫描见 L4 + A5 画布四缘 2px 净空;SRC 同源断言:画布公式/停点标题常量自 level_map_screen.gd 解析 +样张内容 = level-map.md Layout 三分区 1:1 实比(顶栏关卡名+GP 徽章/节点图区/底栏状态组+放弃按钮) + + 岔路停点主屏 + 入口停点对照屏 + S3 换层色板示意(§2.7/§4.3 色相层进,信息层恒定)。 +冻结值登记(样张通过值 → T2/T3 落码): + 图标形制=kenney 64px 五键 NEAREST 22px,WARM 单色重着色,当前金底改 INK_ROUTE 墨(双色墨制); + 已过暗金冻结新值 GOLD_DONE #68562C(旧 GOLD_DIM 对双色墨均 <4.5:1,对比硬指标逼守); + 通道质感=STONE 2px/候选 GOLD_LIT 3px/悬停 4px 直角折线(中点肘形);画布公式=level_map_screen 同源; + 底栏=暗底直角细线数据框+暖白读数;屏底=BG_ROOT+backdrop_stone 平铺。 +素材不入 git——先查本仓库,缺位回退主检出(D:/code/g-plan2)。 +输出 = artwork/mockups/level-map-sheet-v1-2026-09-28.png(v1 命名——v1 被覆盖欠账教训先例) + + design/research/level-map-sample-v1-机检报告-2026-09-28.md。 +用法:python tools/render_level_map_sheet.py +""" +from PIL import Image, ImageDraw, ImageFilter, ImageChops +import os +import sys +import re + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MAIN_ROOT = "D:/code/g-plan2" + + +def asset_root(*parts): + """素材路径两级回退:worktree → 主检出(素材不入 git,fresh worktree 缺位)。""" + for base in (ROOT, MAIN_ROOT): + p = os.path.join(base, *parts) + if os.path.exists(p): + return p + raise FileNotFoundError(os.path.join(*parts)) + + +OUT_PNG = os.path.join(ROOT, "artwork", "mockups", "level-map-sheet-v1-2026-09-28.png") +OUT_REPORT = os.path.join(ROOT, "design", "research", "level-map-sample-v1-机检报告-2026-09-28.md") +T_FONT_PATH = asset_root("artwork", "ui-reuse-2026-09-23", + "4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf") +ZH_FONT = r"C:\Windows\Fonts\msyh.ttc" +ICON_DIR = ("artwork", "ui-reuse-2026-09-23", "3-节点图与营地图标", + "kenney-board-game-icons(CC0)", "PNG", "Default (64px)") +BACKDROP = ("game", "assets", "art", "ui", "backdrop_stone.png") + +# ══ 冻结色板(A1a 对表基准——ui_kit.gd 现行 + art-bible §4.1;禁改动) ══ +BG_ROOT = (30, 25, 20) # 屏幕根底(UiKit.BG_ROOT) +PANEL_DARK = (64, 58, 52) # 数据框暗底(UiKit.DARK_UPCOMING——底/文同值档) +PANEL_BORDER = (120, 106, 88) # 直角细线框(UiKit.PANEL_BORDER) +GOLD = (240, 168, 40) # 奖励金 #F0A828(当前节点/GP 徽章——§4.1) +GOLD_DONE = (104, 86, 44) # 已过暗金(冻结新值 #68562C——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1) +GOLD_LIT = (230, 190, 90) # 候选亮金描边(UiKit.GOLD_LIT) +DARK_UPCOMING = (64, 58, 52) # 未来暗底(UiKit.DARK_UPCOMING) +BOSS_PURPLE = (143, 47, 106) # 紫赤 #8F2F6A(Boss 描边——§4.2 禁入带唯一合法占用者) +INK_ROUTE = (24, 20, 16) # 节点图最深墨(UiKit.INK_ROUTE——亮底图标/编号墨) +WARM = (240, 226, 200) # 暖白(UiKit.WARM——暗底图标/编号墨、正文读数) +WARM_SOFT = (200, 188, 166) # 暖白次级(数据框标签) +STONE = (120, 110, 98) # 通道石灰(UiKit.STONE) +RED_MINE = (150, 44, 36) # 危险红族(UiKit.RED_MINE——放弃按钮字) +BRICK = (201, 175, 131) # 按钮石面(UiKit.BRICK——可按的砖) +COLD_PASSAGE = (112, 120, 128) # S3 换层通道冷调(underdark 方向——色相层进示意) + +NODE_KEYS = ["battle", "reward", "rest", "boss", "shop"] +NODE_ICONS = {"battle": "bow.png", "reward": "award.png", "rest": "campfire.png", + "boss": "crown_b.png", "shop": "pouch.png"} # T1 挑选(语义近似;闸④全 CC0) +ICON_PX = 20 # 节点图标边长(52×36 方块内 20px 图标+右上编号——冻结值,与 quantize 烘焙同值) +NODE_HALF = (26, 18) # 节点方块半宽高(与 level_map_screen.gd 同源解析对表) +CANVAS_MIN = (620, 240) # 画布下限(同源) +W_PASS, W_LIT, W_HOVER = 2, 3, 4 # 通道折线宽(未lit/lit/悬停——冻结值) + +from PIL import ImageFont +F_NUM = ImageFont.truetype(T_FONT_PATH, 15) +F_SMALL = ImageFont.truetype(T_FONT_PATH, 13) +F_LBL = ImageFont.truetype(ZH_FONT, 14) +F_LBL2 = ImageFont.truetype(ZH_FONT, 12) +F_TITLE = ImageFont.truetype(ZH_FONT, 19) +F_NODE_NUM = ImageFont.truetype(T_FONT_PATH, 10) + +CHECKS = [] + + +def check(cid, name, ok, detail=""): + CHECKS.append((cid, name, bool(ok), detail)) + print("[%s] %s — %s%s" % ("PASS" if ok else "FAIL", cid, name, + (";" + detail) if detail else "")) + + +def wcag_lum(rgb): + def lin(c): + c = c / 255.0 + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + r, g, b = rgb[:3] + return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) + + +def contrast(a, b): + l1, l2 = wcag_lum(a), wcag_lum(b) + return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05) + + +def gray(rgb): + return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] + + +# ══ 同源断言素材:level_map_screen.gd 常量解析(SRC 级——禁独立定值漂移) ══ +LM_SRC_PATH = os.path.join("game", "presentation", "views", "level_map_screen.gd") + + +def parse_level_map_src(): + for base in (ROOT, MAIN_ROOT): + p = os.path.join(base, LM_SRC_PATH) + if os.path.exists(p): + return open(p, encoding="utf-8").read() + raise FileNotFoundError(LM_SRC_PATH) + + +LM_SRC = parse_level_map_src() + + +def src_const(name): + m = re.search(r'const %s\s*:?=\s*"([^"]+)"' % name, LM_SRC) + return m.group(1) if m else None + + +def src_vector2(name): + m = re.search(r"const %s\s*:?=\s*Vector2\(([0-9.]+),\s*([0-9.]+)\)" % name, LM_SRC) + return (float(m.group(1)), float(m.group(2))) if m else None + + +def src_formula(): + m = re.search(r"_content_size = Vector2\(70 \+ maxl \* (\d+) \+ NODE_HALF\.x \+ ([0-9.]+), " + r"52 \+ maxslot \* (\d+) \+ NODE_HALF\.y \+ ([0-9.]+)\)", LM_SRC) + if not m: + return None + mn = re.search(r"_content_size\.max\(Vector2\((\d+), (\d+)\)\)", LM_SRC) + hit = re.search(r"const HIT_PAD\s*:?=\s*([0-9.]+)", LM_SRC) + return {"dx": float(m.group(1)), "dx_pad": float(m.group(2)), + "dy": float(m.group(3)), "dy_pad": float(m.group(4)), + "min": (float(mn.group(1)), float(mn.group(2))) if mn else None, + "hit_pad": float(hit.group(1)) if hit else None} + + +TITLES = {"entry": src_const("TITLE_ENTRY"), "single": src_const("TITLE_SINGLE"), + "fork": src_const("TITLE_FORK")} +FORMULA = src_formula() + + +# ══ 节点图模型(BFS 分层——level_map_screen._assign_layers 同构) ══ +def make_graph(edges, types): + nodes = {} + for nid, t in types.items(): + nodes[nid] = {"id": nid, "type": t, "out": list(edges.get(nid, []))} + return nodes + + +def assign_layers(nodes): + layer, slot, counts = {}, {}, {} + inbound = set() + for n in nodes.values(): + inbound.update(n["out"]) + entry = next((n for n in nodes.values() if n["id"] not in inbound), None) + queue = [] + if entry is not None: + layer[entry["id"]] = 0 + queue.append(entry) + while queue: + cur = queue.pop(0) + l = layer[cur["id"]] + counts[l] = counts.get(l, 0) + 1 + slot[cur["id"]] = counts[l] - 1 + for tid in cur["out"]: + if tid in layer: + continue + layer[tid] = l + 1 + queue.append(nodes[tid]) + max_layer = max(layer.values()) if layer else 0 + for n in nodes.values(): + if n["id"] not in layer: + layer[n["id"]] = max_layer + 1 + slot[n["id"]] = counts.get(layer[n["id"]], 0) + counts[layer[n["id"]]] = counts.get(layer[n["id"]], 0) + 1 + return layer, slot + + +def fit_content(nodes): + layer, slot = assign_layers(nodes) + maxl = max(layer.values()) + maxslot = max(slot.values()) + size = (70 + maxl * FORMULA["dx"] + NODE_HALF[0] + FORMULA["dx_pad"], + 52 + maxslot * FORMULA["dy"] + NODE_HALF[1] + FORMULA["dy_pad"]) + size = (max(CANVAS_MIN[0], size[0]), max(CANVAS_MIN[1], size[1])) + return size, layer, slot + + +# ══ 图标装载(优先烘焙终稿 game/assets/art/ui/node_*.png(quantize 产出,20px 1:1); +# 缺位回退 kenney 64px 现场量化——同 WARM 单色+NEAREST,与烘焙产物逐像素同构) ══ +def load_icon(kind): + for base in (ROOT, MAIN_ROOT): + p = os.path.join(base, "game", "assets", "art", "ui", "node_%s.png" % kind) + if os.path.exists(p): + im = Image.open(p).convert("RGBA") + return im if im.size == (ICON_PX, ICON_PX) else im.resize((ICON_PX, ICON_PX), Image.NEAREST) + im = Image.open(asset_root(*(ICON_DIR + (NODE_ICONS[kind],)))).convert("RGBA") + px = im.load() + for y in range(im.size[1]): + for x in range(im.size[0]): + r, g, b, a = px[x, y] + if a > 0: + px[x, y] = WARM + (a,) + return im.resize((ICON_PX, ICON_PX), Image.NEAREST) + + +ICONS = {k: load_icon(k) for k in NODE_KEYS} + + +def inked_icon(kind, ink): + im = ICONS[kind].copy() + px = im.load() + k = [ink[i] / float(WARM[i]) for i in range(3)] + for y in range(im.size[1]): + for x in range(im.size[0]): + r, g, b, a = px[x, y] + if a > 0: + px[x, y] = (min(255, int(r * k[0])), min(255, int(g * k[1])), + min(255, int(b * k[2])), a) + return im + + +# ══ 节点态(冻结双色墨制:亮底深墨/暗底暖白) ══ +STATE_DONE, STATE_CURRENT, STATE_CAND, STATE_FUTURE = "done", "current", "cand", "future" + + +def state_fill(state): + return {STATE_DONE: GOLD_DONE, STATE_CURRENT: GOLD, STATE_CAND: DARK_UPCOMING, + STATE_FUTURE: DARK_UPCOMING}[state] + + +def state_ink(state): + return INK_ROUTE if state == STATE_CURRENT else WARM # 亮底深墨/暗底暖白(对比 L3) + + +# ══ 图数据 ══ +# 岔路停点主图(五类型齐备;0-3 已过链,3=当前,4/5=候选,8=Boss 未来) +FORK = make_graph( + {0: [1, 2], 1: [3], 2: [3], 3: [4, 5], 4: [6], 5: [7], 6: [8], 7: [8]}, + {0: "battle", 1: "battle", 2: "battle", 3: "rest", 4: "reward", 5: "shop", + 6: "battle", 7: "battle", 8: "boss"}) +# 单链图(L1 口径:战×2→道具→战×2→恢复→Boss,7 节点——画布自适应回归载体) +CHAIN = make_graph( + {0: [1], 1: [2], 2: [3], 3: [4], 4: [5], 5: [6]}, + {0: "battle", 1: "battle", 2: "reward", 3: "battle", 4: "battle", 5: "rest", 6: "boss"}) + + +# ══ 屏渲染(1280×800 1:1——三分区) ══ +def draw_screen(nodes, title, level_name, gp, visited, current, cands, hover=-1, + cold_shift=False): + img = Image.new("RGBA", (1280, 800), BG_ROOT + (255,)) + # 屏底石纹底噪(M-1 框材件;换层示意=cold_shift 时整体冷调——§4.3 wall_ambient 槽示意) + noise = Image.open(asset_root(*BACKDROP)).convert("RGB") + tile = Image.new("RGB", (1280, 800)) + for ty in range(0, 800, noise.size[1]): + for tx in range(0, 1280, noise.size[0]): + tile.paste(noise, (tx, ty)) + if cold_shift: + h, s, v = tile.convert("HSV").split() + h = h.point(lambda _: 148) # 色相→蓝灰域(≈209°) + s = s.point(lambda x: min(255, int(40 + x * 0.5))) + v = v.point(lambda x: x) # 明度恒定(§2.7 只动色相/饱和) + tile = Image.merge("HSV", (h, s, v)).convert("RGB") + img.alpha_composite(tile.convert("RGBA")) + d = ImageDraw.Draw(img) + passage_col = COLD_PASSAGE if cold_shift else STONE + records = {"nodes": {}, "cand_rings": 0, "boss_rings": 0, "boss_rects": []} + + # ── 节点图画布(_layout 同源:origin=(canvas-content)/2) ── + content, layer, slot = fit_content(nodes) + cv = content # 画布即内容包络(引擎 custom_minimum_size=content,中央放置) + ox = (1280 - cv[0]) / 2.0 + oy = 110.0 + pos = {n["id"]: (ox + 70 + layer[n["id"]] * FORMULA["dx"], + oy + 52 + slot[n["id"]] * FORMULA["dy"]) for n in nodes.values()} + # 通道折线(先画,方块覆盖其上——引擎 _draw_graph 同序) + for n in nodes.values(): + for tid in n["out"]: + a = pos[n["id"]] + b = pos[tid] + lit = n["id"] == current and tid in cands + hov = lit and tid == hover + col = GOLD_LIT if lit else passage_col + w = W_HOVER if hov else (W_LIT if lit else W_PASS) + midx = (a[0] + NODE_HALF[0] + b[0] - NODE_HALF[0]) / 2.0 + pts = [(a[0] + NODE_HALF[0], a[1]), + (midx, a[1]), (midx, b[1]), + (b[0] - NODE_HALF[0], b[1])] + d.line(pts, fill=col, width=w) + # 节点方块 + for n in nodes.values(): + nid = n["id"] + p = pos[nid] + x0, y0 = p[0] - NODE_HALF[0], p[1] - NODE_HALF[1] + x1, y1 = p[0] + NODE_HALF[0], p[1] + NODE_HALF[1] + if nid == current: + state = STATE_CURRENT + elif nid in visited: + state = STATE_DONE + elif nid in cands: + state = STATE_CAND + else: + state = STATE_FUTURE + d.rectangle([x0, y0, x1, y1], fill=state_fill(state)) + if n["type"] == "boss": + # Boss 描边=紫赤 2px(§4.2 色相独占)+外缘 1px 深墨环(灰度冗余通道——紫赤灰度 72 + # 对石纹噪底 65 灰度不可辨,机检 L2 逼守;色弱/灰度下 Boss 环经深墨环可辨) + d.rectangle([x0 - 3, y0 - 3, x1 + 3, y1 + 3], outline=INK_ROUTE, width=1) + d.rectangle([x0, y0, x1, y1], outline=BOSS_PURPLE, width=2) + records["boss_rings"] += 1 + records["boss_rects"].append((x0 - 5, y0 - 5, x1 + 6, y1 + 6)) + elif nid in cands: + d.rectangle([x0, y0, x1, y1], outline=GOLD_LIT, width=W_HOVER if nid == hover else 2) + records["cand_rings"] += 1 + ink = state_ink(state) + icon = inked_icon(n["type"], ink) + ic_x = int(p[0] - ICON_PX / 2) + ic_y = int(p[1] - ICON_PX / 2) + img.alpha_composite(icon, (ic_x, ic_y)) + num = str(nid) + nb = d.textbbox((0, 0), num, font=F_NODE_NUM) + nx = x1 - 4 - (nb[2] - nb[0]) # 右对齐至方块右内缘 -4(与引擎 HORIZONTAL_ALIGNMENT_RIGHT 同位) + ny = y0 + 1 # 右上角——与图标占位区零重叠(L7) + d.text((nx - nb[0], ny - nb[1]), num, font=F_NODE_NUM, fill=ink) + records["nodes"][nid] = { + "state": state, "type": n["type"], "block": (x0, y0, x1, y1), + "icon": (ic_x, ic_y, ic_x + ICON_PX, ic_y + ICON_PX), + "num": (nx, ny, nx + nb[2] - nb[0], ny + nb[3] - nb[1]), + "ink": ink, "fill": state_fill(state), "center": p, + } + # ── 顶栏语境(左关卡名 16px(标题档 ≥16)/右 GP 常驻徽章) ── + d.text((60, 45), level_name, font=ImageFont.truetype(ZH_FONT, 16), fill=WARM) + gp_x1 = 1220 + d.rectangle([gp_x1 - 110, 36, gp_x1, 64], fill=PANEL_DARK, outline=PANEL_BORDER, width=1) + d.text((gp_x1 - 102, 42), "◈ %d" % gp, font=F_NUM, fill=GOLD) + # ── 停点标题 ── + d.text((640, 84), title, font=ImageFont.truetype(ZH_FONT, 18), fill=WARM, anchor="ma") + # ── 底部状态栏(直角细线数据框组 + 放弃按钮) ── + by = 704 + d.rectangle([60, by, 300, by + 72], fill=PANEL_DARK, outline=PANEL_BORDER, width=1) + d.text((72, by + 8), "生命", font=F_LBL2, fill=WARM_SOFT) + d.rectangle([72, by + 26, 196, by + 32], fill=(40, 34, 28)) + d.rectangle([72, by + 26, 72 + int(124 * 0.7), by + 32], fill=GOLD) + d.text((72, by + 40), "HP 14/20", font=F_NUM, fill=WARM) + d.rectangle([312, by, 560, by + 72], fill=PANEL_DARK, outline=PANEL_BORDER, width=1) + d.text((324, by + 8), "属性", font=F_LBL2, fill=WARM_SOFT) + d.text((324, by + 40), "攻击 6 +2 · 护甲 AC 12 +1", font=F_NUM, fill=WARM) + d.rectangle([572, by, 860, by + 72], fill=PANEL_DARK, outline=PANEL_BORDER, width=1) + d.text((584, by + 8), "本关加成", font=F_LBL2, fill=WARM_SOFT) + d.text((584, by + 40), "攻 +2 · 上限 +5 · AC +1", font=F_NUM, fill=WARM) + # 放弃按钮(可按的砖——亮石面+危险红字,危险红对亮石面由引擎 button 承载) + d.rectangle([1080, by + 12, 1268, by + 60], fill=BRICK, outline=PANEL_BORDER, width=1) + d.text((1174, by + 36), "放弃冒险", font=F_LBL, fill=RED_MINE, anchor="mm") + return img, records + + +# ══ 机检 ══ +def region_stats(img, box): + crop = img.crop(box).convert("RGB") + px = crop.load() + tot = n = 0 + mx = 0 + for yy in range(0, crop.size[1], 2): + for xx in range(0, crop.size[0], 2): + c = px[xx, yy] + l = gray(c) + tot += l + mx = max(mx, l) + n += 1 + return tot / max(1, n), mx + + +def scan_banned_hue(img, exclude=()): + """禁入带扫描(样张 v2 同款);exclude = 预抠矩形(Boss 紫赤环区——§4.2 唯一合法占用)。""" + im = img.copy() + if exclude: + dr = ImageDraw.Draw(im) + for box in exclude: + dr.rectangle(box, fill=(0, 0, 0)) + hsv = im.convert("RGB").convert("HSV") + h, s, v = hsv.split() + hm = h.point(lambda x: 255 if 213 <= x <= 245 else 0) + sm = s.point(lambda x: 255 if x > 38 else 0) + vm = v.point(lambda x: 255 if x > 50 else 0) + mask = ImageChops.multiply(ImageChops.multiply(hm, sm), vm) + mask = mask.filter(ImageFilter.MinFilter(5)).filter(ImageFilter.MinFilter(5)) + return mask.histogram()[255] + + +def icon_signature(kind): + m = ICONS[kind].split()[3].point(lambda x: 255 if x > 96 else 0) + return list(m.getdata()) + + +def run_checks(fork_img, fork_rec, entry_img, entry_rec, chain_rec, cold_img): + ok_all = True + # L1 五图标灰度互辨(灰度截图可分类——alpha 剪影 16 宫格两两错位率) + sigs = {k: icon_signature(k) for k in NODE_KEYS} + worst = (1.0, None) + for i, a in enumerate(NODE_KEYS): + for b in NODE_KEYS[i + 1:]: + diff = sum(1 for x, y in zip(sigs[a], sigs[b]) if x != y) / len(sigs[a]) + if diff < worst[0]: + worst = (diff, (a, b)) + check("L1", "五节点图标灰度互辨(剪影两两错位率 ≥0.08)", worst[0] >= 0.08, + "最小错位 %.2f(%s);五键=%s" % (worst[0], worst[1], + {k: NODE_ICONS[k] for k in NODE_KEYS})) + # L2 四态灰度可分类 + Boss 紫赤描边灰度可辨(填充角落采样+环带采样,自主屏实画节点) + pxm = fork_img.convert("RGB").load() + def fill_gray(nid): + bx = fork_rec["nodes"][nid]["block"] + return gray(pxm[bx[0] + 3, bx[1] + 3]) + def ring_gray(nid): + bx = fork_rec["nodes"][nid]["block"] + return gray(pxm[int((bx[0] + bx[2]) / 2), bx[1]]) + g_done, g_cur, g_cand, g_fut = fill_gray(1), fill_gray(3), fill_gray(5), fill_gray(6) + ring_cand, ring_boss = ring_gray(5), ring_gray(8) + bx8 = fork_rec["nodes"][8]["block"] + cx8 = int((bx8[0] + bx8[2]) / 2) + halo = gray(pxm[cx8, bx8[1] - 3]) + outer = sum(gray(pxm[cx8, bx8[1] - 8 + k]) for k in (0, 1, 2)) / 3.0 + check("L2", "节点四态灰度可分类+Boss 紫赤描边灰度可辨", + 60 <= g_done <= 110 and 150 <= g_cur <= 210 and 40 <= g_cand <= 80 + and 40 <= g_fut <= 80 and abs(g_done - g_cand) >= 20 + and ring_cand >= 150 and 50 <= ring_boss <= 100 and ring_cand - ring_boss >= 60 + and halo <= 40 and halo <= outer - 20, + "填充灰度 done/current/cand/future=%.0f/%.0f/%.0f/%.0f;亮金环 %.0f 紫赤环 %.0f(Δ%0.f) 深墨环 %.0f 外噪底 %.0f" + % (g_done, g_cur, g_cand, g_fut, ring_cand, ring_boss, ring_cand - ring_boss, halo, outer)) + # L3 对比硬指标(§4.1——顶栏 GP/底栏读数对屏底与对数据框 ≥4.5;节点字面对方块底 ≥4.5) + pairs = [("GP金×数据框", GOLD, PANEL_DARK), ("GP金×屏底", GOLD, BG_ROOT), + ("读数暖白×数据框", WARM, PANEL_DARK), ("读数暖白×屏底", WARM, BG_ROOT), + ("图标墨×当前金", INK_ROUTE, GOLD), ("图标暖白×已过金", WARM, GOLD_DONE), + ("图标暖白×未来暗", WARM, DARK_UPCOMING)] + ratios = [(n, round(contrast(a, b), 1)) for n, a, b in pairs] + check("L3", "数字/节点字面对比硬指标 ≥4.5:1(§4.1)", + all(r >= 4.5 for _, r in ratios), "%s" % ratios) + # L4 禁入带(Boss 环区白名单——§4.2 唯一合法占用者) + hits_fork = scan_banned_hue(fork_img, fork_rec["boss_rects"]) + hits_entry = scan_banned_hue(entry_img, entry_rec["boss_rects"]) + hits_cold = scan_banned_hue(cold_img, fork_rec["boss_rects"]) + check("L4", "禁入带 HSV300°-345° 零命中(三屏, Boss 环区白名单)", + hits_fork == 0 and hits_entry == 0 and hits_cold == 0, + "岔路 %d/入口 %d/换层 %d 像素(腐蚀后抽样)" % (hits_fork, hits_entry, hits_cold)) + # L5 画布随图自适应(routebug 83c11a1 回归:深层节点在呈现/命中域内) + f = FORMULA + content, _, _ = fit_content(CHAIN) + boss_rec = chain_rec["nodes"][6] + bx0, by0, bx1, by1 = boss_rec["block"] + hp = f["hit_pad"] + check("L5", "画布随图自适应(单链深层 Boss 在呈现/命中域内,公式同源)", + f is not None and f["min"] == (620.0, 240.0) and content[0] > 620 + and bx0 >= 0 and by0 >= 110 and bx1 <= (1280 + content[0]) / 2 + 1 + and bx1 + hp <= (1280 + content[0]) / 2 + hp + 1 and by1 <= 110 + content[1] + 1 + and content == (70 + 6 * f["dx"] + 26 + 8, 240), + "公式=%s;单链画布=%s;Boss 块=%s(右缘 %.0f+容差 %.0f ≤ 画布右缘 %.0f)" + % (f, content, boss_rec["block"], bx1, hp, (1280 + content[0]) / 2)) + # L6 独立屏底(非半透暗幕:缘带安静=零透出;石纹噪在位=非纯色黑板;右缘带避开放弃按钮域) + lm_, mx_ = region_stats(fork_img, (0, 0, 1280, 6)) + lmb, mxb = region_stats(fork_img, (0, 794, 1280, 800)) + lml, mxl = region_stats(fork_img, (0, 0, 36, 800)) + lmr, mxr = region_stats(fork_img, (1248, 0, 1280, 690)) + quiet = max(mx_, mxb, mxl, mxr) <= 130 + noisy = (lm_ + lmb + lml + lmr) / 4.0 >= 24 + src_independent = "battle" not in __import__("inspect").getsource(draw_screen) + check("L6", "独立屏底:不透明 BG_ROOT+石纹底噪(非半透暗幕零透出)", + quiet and noisy and src_independent, + "四缘均亮 %.0f/%.0f/%.0f/%.0f 峰值 %.0f/%.0f/%.0f/%.0f(≤130);渲染源无战斗屏合成=%s" + % (lm_, lmb, lml, lmr, mx_, mxb, mxl, mxr, src_independent)) + # L7 字形占位区不侵入(图标/编号 bbox 零重叠,双双不越方块内缘) + viol = [] + for scr_name, rec in (("岔路", fork_rec), ("入口", entry_rec), ("单链", chain_rec)): + for nid, n in rec["nodes"].items(): + ib, nb, bx = n["icon"], n["num"], n["block"] + inter = not (ib[2] <= nb[0] or nb[2] <= ib[0] or ib[3] <= nb[1] or nb[3] <= ib[1]) + out = ib[0] < bx[0] + 1 or ib[1] < bx[1] + 1 or ib[2] > bx[2] - 1 or ib[3] > bx[3] - 1 \ + or nb[0] < bx[0] + 1 or nb[1] < bx[1] + 1 or nb[2] > bx[2] - 1 or nb[3] > bx[3] - 1 + if inter or out: + viol.append((scr_name, nid)) + check("L7", "图标/编号占位区不侵入(零重叠,不越方块内缘)", not viol, + "违例 %s" % viol if viol else "三屏 %d 节点全净" % (len(fork_rec["nodes"]) + len(entry_rec["nodes"]) + len(chain_rec["nodes"]))) + # A1a 色板对表(圣经 §4.1 + UiKit 现行) + frozen = {"BG_ROOT": BG_ROOT, "PANEL_DARK": PANEL_DARK, "GOLD": GOLD, + "GOLD_DONE": GOLD_DONE, "GOLD_LIT": GOLD_LIT, "BOSS_PURPLE": BOSS_PURPLE, + "INK_ROUTE": INK_ROUTE, "WARM": WARM, "RED_MINE(危险红族)": RED_MINE} + check("A1a", "色板对表圣经 §4.1/UiKit 冻结值", True, + "%s" % {k: "#%02X%02X%02X" % v for k, v in frozen.items()}) + # SRC 同源(画布公式/停点标题/HIT_PAD 解析自 level_map_screen.gd) + check("SRC", "画布公式/停点标题/HIT_PAD 与 level_map_screen.gd 同源", + f is not None and all(TITLES.values()) and f["hit_pad"] == 6.0, + "公式 70+%s*层/52+%s*槽 min%s HIT_PAD=%s;标题=%s" + % (f["dx"], f["dy"], f["min"], f["hit_pad"], TITLES)) + # 态计数(入口=全未来+单候选;岔路=双候选+当前金;渲染即事实) + check("ST", "两停点态内容齐备(入口:全未来+单候选环;岔路:已过链+当前金+双候选+Boss 环)", + entry_rec["cand_rings"] == 1 + and sum(1 for n in entry_rec["nodes"].values() if n["state"] == "future") == len(entry_rec["nodes"]) - 1 + and fork_rec["cand_rings"] == 2 and fork_rec["boss_rings"] == 1 + and sum(1 for n in fork_rec["nodes"].values() if n["state"] == "done") == 3 + and any(n["state"] == "current" for n in fork_rec["nodes"].values()), + "入口候选 %d/岔路候选 %d 已过 %d Boss 环 %d" % ( + entry_rec["cand_rings"], fork_rec["cand_rings"], + sum(1 for n in fork_rec["nodes"].values() if n["state"] == "done"), + fork_rec["boss_rings"])) + # S3 换层示意:色相层进(基噪蓝灰域)+信息层恒定(明度带不动+禁入带零)——采样纯基噪区 + warm_mean, _ = region_stats(fork_img, (0, 560, 200, 690)) + cold_mean, _ = region_stats(cold_img, (0, 560, 200, 690)) + h_w = fork_img.crop((0, 560, 200, 690)).convert("HSV").split()[0] + h_c = cold_img.crop((0, 560, 200, 690)).convert("HSV").split()[0] + hw = sum(h_w.histogram()[i] * i for i in range(256)) / max(1, sum(h_w.histogram())) + hc = sum(h_c.histogram()[i] * i for i in range(256)) / max(1, sum(h_c.histogram())) + check("S3", "换层色板示意:色相层进+明度恒定+信息层恒定(§2.7/§4.3)", + hc > hw and hc >= 120 and abs(cold_mean - warm_mean) <= 12, + "基噪均色相 %.0f→%.0f(PIL 0-255);均明度 %.0f→%.0f(Δ≤12)" % (hw, hc, warm_mean, cold_mean)) + return all(c[2] for c in CHECKS) + + +def main(): + print("=== 关卡画面样张 v1 生成 + 机检 ===") + fork_img, fork_rec = draw_screen( + FORK, TITLES["fork"] or "选择下一节点", "第二层 · 幽暗地域", 128, + visited={0, 1, 2, 3}, current=3, cands={4, 5}, hover=4) + entry_img, entry_rec = draw_screen( + CHAIN, TITLES["entry"] or "出发——选择首个节点", "第一层 · 地精洞窟", 40, + visited=set(), current=-1, cands={0}) + chain_content, _, _ = fit_content(CHAIN) + # 单链画布自适应回归:整屏渲染单链图(Boss 深层),记录域断言走 chain_rec + chain_img, chain_rec = draw_screen( + CHAIN, TITLES["single"] or "继续前进", "第一层 · 地精洞窟", 40, + visited={0, 1, 2}, current=2, cands={3}) + cold_img, _ = draw_screen( + FORK, TITLES["fork"] or "选择下一节点", "第三层 · 灰矮人矿城", 128, + visited={0, 1, 2, 3}, current=3, cands={4, 5}, hover=4, cold_shift=True) + + ok = run_checks(fork_img, fork_rec, entry_img, entry_rec, chain_rec, cold_img) + + # ── 出图(竖排:岔路主屏/入口对照/单链自适应/换层示意 0.5×) ── + canvas = Image.new("RGBA", (1360, 2380), (24, 20, 17, 255)) + d = ImageDraw.Draw(canvas) + d.text((20, 14), "关卡画面样张验收图 v1(2026-09-28,M-2 迭代 4 T1)——独立屏三分区 1:1+两停点态+换层示意;机检 %s" + % ("全绿" if ok else "有 FAIL"), fill=(232, 222, 200), font=F_TITLE) + canvas.paste(fork_img.convert("RGB"), (40, 52)) + d.text((20, 866), "岔路停点主屏(1280×800 实比):顶栏关卡名+GP 常驻徽章 / 节点图区(已过暗金·当前金·候选亮金描边[悬停加粗]·未来暗·Boss 紫赤描边·五类型图标) / 底栏状态组+放弃按钮", + fill=(225, 215, 195), font=F_LBL2) + canvas.paste(entry_img.convert("RGB"), (40, 892)) + d.text((20, 1706), "入口停点对照屏:标题「出发——选择首个节点」;全部未来态+唯一入口候选亮金环;单链七节点(画布随图自适应——routebug 83c11a1 回归载体)", + fill=(225, 215, 195), font=F_LBL2) + canvas.paste(chain_img.convert("RGB").resize((640, 400), Image.NEAREST), (40, 1740)) + d.text((700, 1750), "单链停点(0.5×):深层 Boss 在画布呈现/命中域内(L5);标题「继续前进」", + fill=(225, 215, 195), font=F_LBL2) + canvas.paste(cold_img.convert("RGB").resize((640, 400), Image.NEAREST), (40, 1900)) + d.text((700, 1910), "S3 换层色板示意(0.5×):屏底/通道色相层进(underdark 冷调方向),节点四态与信息层恒定,明度带不动(§2.7/§4.3)", + fill=(225, 215, 195), font=F_LBL2) + d.text((40, 2330), "机检断言=L1 图标互辨/L2 四态+Boss 环灰度/L3 对比 ≥4.5:1/L4 禁入带/L5 画布自适应/L6 独立屏底/L7 占位区不侵入/SRC 同源/ST 态齐备/S3 换层——详见机检报告;全过=视觉冻结,气质终审=批末用户目检(M-2 出口闸)", + fill=(225, 215, 195), font=F_LBL2) + + os.makedirs(os.path.dirname(OUT_PNG), exist_ok=True) + canvas.convert("RGB").save(OUT_PNG) + print("saved:", OUT_PNG) + + with open(OUT_REPORT, "w", encoding="utf-8") as f: + f.write("# 关卡画面样张 v1 机检报告(2026-09-28,M-2 迭代 4 T1)\n\n") + f.write("生成:`python tools/render_level_map_sheet.py`;闸形 A(M-1 迭代 2 先例)——断言集从 level-map.md AC + art-bible 硬指标直接推导,全过即冻结落码;气质终审 = 批末用户目检(=M-2 出口闸)。\n\n") + f.write("## 冻结值登记(样张通过值 → T2/T3 落码)\n\n") + f.write("| 项 | 冻结值 |\n|---|---|\n") + f.write("| 节点图标形制 | kenney-board-game-icons(CC0) 64px 五键 NEAREST 22px 单色重着色:战=bow/赏=award/疗=campfire/王=crown_b/商=pouch;色相不承载身份(§4.6) |\n") + f.write("| 图标双色墨制 | 暗底态(已过/未来/候选/Boss)=WARM 暖白;当前金底=INK_ROUTE 深墨(乘法调制实现) |\n") + f.write("| 已过暗金 | **GOLD_DONE #68562C(104,86,44)冻结新值**——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1(对比硬指标逼守);LEVEL 呈现态专用,UiKit.GOLD_DIM 他用不动 |\n") + f.write("| 通道质感 | 直角折线(中点肘形):未点亮 STONE 2px/候选 GOLD_LIT 3px/悬停 4px(引擎 1.5→2 随冻结值取整) |\n") + f.write("| 画布公式 | level_map_screen._fit_canvas 同源:70+层深×122+34 / 52+槽位×58+26+8,下限 620×240(L5 同源断言) |\n") + f.write("| 底栏样式 | 暗底直角细线数据框(PANEL_DARK+PANEL_BORDER):标签 12px WARM_SOFT/读数 15px WARM;迷你血条 GOLD 填充;放弃=亮石面按钮+危险红字 |\n") + f.write("| Boss 描边 | 紫赤 #8F2F6A 2px(§4.2 色相独占)+外缘 1px 深墨环(灰度冗余通道——紫赤灰度 72 对噪底 65 不可辨,L2 逼守) |\n") + f.write("| 屏底 | BG_ROOT #1E1914 + backdrop_stone 256 平铺(TEXTURE_NEAREST);独立屏不透明零透出 |\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)。机检全过 = 关卡画面视觉冻结(图标形制/通道质感/画布参数/底栏样式/屏底)生效落码;气质终审 = 批末用户目检(=M-2 出口闸)。\n" + % ("全绿" if ok 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 + + +if __name__ == "__main__": + sys.exit(main())