merge: 战斗舞台 Node2D 迁移批(循环 1 T0-T7)落籍——B 区 BattleStageView Control 壳+SubViewport+StageWorld 同构 Phase A、站桩本体 StandeeBody Node2D 化(行为面契约保持)、样张 v1 机检 12 断言全绿交异步终审、Alpha 验收清单初稿、等价证明成文(MC 逐位一致/调用点零 diff/draw calls 270 vs 基线 274 零回归);台账 168-174

This commit is contained in:
DKChan committed 2026-09-26 05:28:11 +08:00
commit 3b6c69adb3
19 files changed
+1649 -370

No files matched your search

@@ -252,3 +252,4 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
| v0.7 | 2026-09-20 | **表现层三批落地 + 义务清单补行(代码同日三批:样式收敛 / 站桩改造 / 结算演出链)**:呈现义务来源清单补「关卡节点地图」行(节点链进度常显 + 选路覆盖层只读呈现——此前漏登记);新增结算演出覆盖层(跨区弹道/光带载体,兑现演出因果可视);减动效承诺落地(UiKit.motion 单点包装全部呈现层 tween 时长,开启时长减半、可挖域呼吸停格;切片期入口 = TunePanel 开关,正式设置层归设置批)。代码侧站桩载体(StandeeView)与卡片退役随 v0.6 裁决执行 | | v0.7 | 2026-09-20 | **表现层三批落地 + 义务清单补行(代码同日三批:样式收敛 / 站桩改造 / 结算演出链)**:呈现义务来源清单补「关卡节点地图」行(节点链进度常显 + 选路覆盖层只读呈现——此前漏登记);新增结算演出覆盖层(跨区弹道/光带载体,兑现演出因果可视);减动效承诺落地(UiKit.motion 单点包装全部呈现层 tween 时长,开启时长减半、可挖域呼吸停格;切片期入口 = TunePanel 开关,正式设置层归设置批)。代码侧站桩载体(StandeeView)与卡片退役随 v0.6 裁决执行 |
| v0.8 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `docs/architecture/adr-0006-board-node2d-rendering.md` 与 `docs/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 战斗屏布局四区 → 三区——删 A 区关卡进度条行(LevelStripView 退役删除,节点链进度呈现移出战斗屏、只留关卡画面);C/D 区纵向 520 → 548;装板校验变量与实算同步(棋盘区高 504、可用高度 488);② 呈现义务来源清单「关卡节点地图」行改承载口径——节点链进度由关卡画面选路覆盖层节点态承载(暗金已过 / 金当前 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件;③ 「四区」措辞全线改「三区」(负责表、节题、结构冻结、验收、下游战斗表现交付行),双舞台常驻语义不动。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编(字母是跨文档/代码注释的承载标识);② A 区 28px 纵向空间划归 C/D 底行(520 → 548;1920 档换算仍归表现层升级批);③ 关卡画面进度呈现由选路覆盖层节点态直接承载、不另设进度条组件 | | v0.8 | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `docs/architecture/adr-0006-board-node2d-rendering.md` 与 `docs/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 战斗屏布局四区 → 三区——删 A 区关卡进度条行(LevelStripView 退役删除,节点链进度呈现移出战斗屏、只留关卡画面);C/D 区纵向 520 → 548;装板校验变量与实算同步(棋盘区高 504、可用高度 488);② 呈现义务来源清单「关卡节点地图」行改承载口径——节点链进度由关卡画面选路覆盖层节点态承载(暗金已过 / 金当前 / 候选下一跳通道亮金 / Boss 旗朱描边),不另设进度条组件;③ 「四区」措辞全线改「三区」(负责表、节题、结构冻结、验收、下游战斗表现交付行),双舞台常驻语义不动。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编(字母是跨文档/代码注释的承载标识);② A 区 28px 纵向空间划归 C/D 底行(520 → 548;1920 档换算仍归表现层升级批);③ 关卡画面进度呈现由选路覆盖层节点态直接承载、不另设进度条组件 |
| v0.9 | 2026-09-23 | 渲染技术注记(棋盘 Node2D 迁移批 Phase A,代码实装同批;上位裁决 ADR-0006):棋盘本体 Node2D 化——BoardView 内部由 GridContainer+逐格控件改为 Control 壳 + SubViewport + Node2D 世界,类名/信号/公共方法契约不变、探针零改造;三区布局与性能预算(同屏 draw calls < 200)不变;程序化占位视觉等价平移,素材终稿按 art-bible §8.2 格面 32×32 暂定档槽位 1:1 换入 | | v0.9 | 2026-09-23 | 渲染技术注记(棋盘 Node2D 迁移批 Phase A,代码实装同批;上位裁决 ADR-0006):棋盘本体 Node2D 化——BoardView 内部由 GridContainer+逐格控件改为 Control 壳 + SubViewport + Node2D 世界,类名/信号/公共方法契约不变、探针零改造;三区布局与性能预算(同屏 draw calls < 200)不变;程序化占位视觉等价平移,素材终稿按 art-bible §8.2 格面 32×32 暂定档槽位 1:1 换入 |
| v0.10 | 2026-09-26 | 渲染技术注记(战斗舞台 Node2D 迁移批 Phase B,代码实装同批;上位裁决 ADR-0006):B 区战斗舞台 Node2D 化——BattleStageView 内部由 PanelContainer+Control 站桩改为 Control 壳 + SubViewportContainer + SubViewport + Node2D 世界(StageWorld/StandeeBody),类名/setter 面/公共方法契约不变、调用点零改造;状态图标行与悬停 tooltip 保留 Control 载体挂壳层(伴随件);C6 字体注入挂点复用(set_world_font);三区布局与性能预算(同屏 draw calls < 200)不变;占位表现等价平移(时序常量逐字对照——等价证明见 production/qa/battle-stage-node2d-等价证明-2026-09-26.md) |
+1
View File
@@ -437,6 +437,7 @@
| 2026-09-20 | **敌人卡形态裁决收口(2026-09-20 用户看对照样张拍板,06 待办「敌人卡形态」解决移除——台账 99)**:全站桩形态定稿——① B 区主角与敌人均为无框立绘+底座(站桩式),**英雄卡/敌人卡组件整体退役,卡片式不再作为候选**;主角血条悬浮头顶、状态效果小图标一排挂血条正下方,敌人血条+顺位徽标悬浮头顶、状态小图标同款挂血条下;攻击值等规划数据仍归 C 区数据框(C 区不动);② 布局表 B 区行、Component Inventory 英雄卡/敌人卡两行随裁决改写(敌方 side 池共享层数各立绘同值、意图动画占位等既有语义平移到站桩载体,不随形态改变);③ ASCII 线框图 B 区重绘(旧"意图:爪击/环点"示意早已随行为环退役,本次一并清掉);④ OQ4 改写为裁决已收口注记,对照样张存档 `production/mockups/`;呈现义务细节重述(受击/退场演出挂点等)随表现层升级批执行 | | 2026-09-20 | **敌人卡形态裁决收口(2026-09-20 用户看对照样张拍板,06 待办「敌人卡形态」解决移除——台账 99)**:全站桩形态定稿——① B 区主角与敌人均为无框立绘+底座(站桩式),**英雄卡/敌人卡组件整体退役,卡片式不再作为候选**;主角血条悬浮头顶、状态效果小图标一排挂血条正下方,敌人血条+顺位徽标悬浮头顶、状态小图标同款挂血条下;攻击值等规划数据仍归 C 区数据框(C 区不动);② 布局表 B 区行、Component Inventory 英雄卡/敌人卡两行随裁决改写(敌方 side 池共享层数各立绘同值、意图动画占位等既有语义平移到站桩载体,不随形态改变);③ ASCII 线框图 B 区重绘(旧"意图:爪击/环点"示意早已随行为环退役,本次一并清掉);④ OQ4 改写为裁决已收口注记,对照样张存档 `production/mockups/`;呈现义务细节重述(受击/退场演出挂点等)随表现层升级批执行 |
| 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `docs/architecture/adr-0006-board-node2d-rendering.md` 与 `docs/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 布局四区 → 三区——删布局表 A 区关卡进度条行与 Component Inventory LevelStripView 行(LevelStripView 退役删除,节点链进度只留关卡画面,战斗屏零进度承载);C/D 底行 520 → 548;ASCII 线框图重绘为 B/C/D 三区;② 新流程口径落文——关卡画面(选路覆盖层常驻形态)为关卡内常驻枢纽:Navigation Position 图、到达语境、Entry 表、信息层级第 5 层、Data Requirements 节点图行改「关卡开始即出图选路、战后返回关卡画面再选路」;③ AC 四区措辞改三区。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行(520 → 548);③ 关卡画面进度呈现由选路覆盖层节点态承载(暗金已过 / 金当前 / 亮金候选通道 / 旗朱 Boss)、不另设进度条组件 | | 2026-09-23 | **停点前移批(冒险入口流程倒置修复,用户 2026-09-23 拍板;上位裁决 `docs/architecture/adr-0006-board-node2d-rendering.md` 与 `docs/architecture/board-node2d-migration-plan.md` §1,代码实装另批并行)**:① 布局四区 → 三区——删布局表 A 区关卡进度条行与 Component Inventory LevelStripView 行(LevelStripView 退役删除,节点链进度只留关卡画面,战斗屏零进度承载);C/D 底行 520 → 548;ASCII 线框图重绘为 B/C/D 三区;② 新流程口径落文——关卡画面(选路覆盖层常驻形态)为关卡内常驻枢纽:Navigation Position 图、到达语境、Entry 表、信息层级第 5 层、Data Requirements 节点图行改「关卡开始即出图选路、战后返回关卡画面再选路」;③ AC 四区措辞改三区。**批内代拍板三项已追认(2026-09-24 用户确认)**:① B/C/D 字母保持不重编;② 28px 纵向空间划归 C/D 底行(520 → 548);③ 关卡画面进度呈现由选路覆盖层节点态承载(暗金已过 / 金当前 / 亮金候选通道 / 旗朱 Boss)、不另设进度条组件 |
| 2026-09-23 | 实现注记(棋盘 Node2D 迁移批 Phase A,代码实装同批;上位裁决 ADR-0006):棋盘渲染 Node2D 化(BoardView = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界,信号/click_cell/_cells 契约保持,新增 push_input 端到端输入探针);格尺寸公式锚点不变(set_cell_area = floor((min(宽,高) − 预留 − (N−1)×格间距) / N) 单点可调),布局与交互语义零改动 | | 2026-09-23 | 实现注记(棋盘 Node2D 迁移批 Phase A,代码实装同批;上位裁决 ADR-0006):棋盘渲染 Node2D 化(BoardView = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界,信号/click_cell/_cells 契约保持,新增 push_input 端到端输入探针);格尺寸公式锚点不变(set_cell_area = floor((min(宽,高) − 预留 − (N−1)×格间距) / N) 单点可调),布局与交互语义零改动 |
| 2026-09-26 | 实现注记(战斗舞台 Node2D 迁移批 Phase B,代码实装同批;上位裁决 ADR-0006):B 区战斗画面 Node2D 化(BattleStageView = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界,站桩本体 StandeeBody 世界精灵;set_enemies/clear_enemies/on_step_row_fx/gray_all_enemies 等 setter 面签名保持,弹道落点锚点=壳层口径 world_to_shell 单点);状态图标行与悬停 tooltip 保留 Control 载体挂壳层;三区布局尺寸零改动(B 区外框 252px 逐位) |
| 2026-09-26 | **战斗屏信息归属批(用户 2026-09-26 三轮纠偏后拍板;代码实装另批——hero_info_panel_view.gd / hud_presenter.gd 改造 + 探针断言迁移,登记待办)**:立两条信息归属原则并全屏贯行——①循环口径原则(战斗屏只展小循环内被消费、影响当下决策的数据;中/大循环资源不得入侵)②单一展示面原则(同一信息全屏唯一展示面;单位本体已承载的信息数据框不重复;资源数字贴唯一消费口)。随拍板落地:① B 区站桩 = HP 与状态效果唯一展示面——C 区 HP 条/HP 文字/立绘占位/状态效果槽×4 退役(重复展示;证据链 = hud_presenter.gd 同一 s.hero.hp/s.hero_effects 双投喂 B/C 两区);② RAP 读数(点阵+数字)迁 D 区 AP 条、置弃图刷新按钮旁(RAP 唯一消费口 = 弃图刷新——就近原则);③ C 区状态槽悬停 tooltip 职责并入 B 区站桩状态小图标;④ GP(中循环)/钥石(大循环)迁出战斗屏,去向挂 OQ14;⑤ C 区改「攻击数据框 + 魔法道具槽(预留)+ 本关强化列表(预留)」——预留区来自魔法道具与关卡内属性提升的既有立项;⑥ 空态/清零反馈/悬停交互/AC/§13 义务表载体同步改写。**批内遗留两项待拍板(OQ13/14)**:护甲 AC 是否补入 C 区数据框;GP/钥石去向 | | 2026-09-26 | **战斗屏信息归属批(用户 2026-09-26 三轮纠偏后拍板;代码实装另批——hero_info_panel_view.gd / hud_presenter.gd 改造 + 探针断言迁移,登记待办)**:立两条信息归属原则并全屏贯行——①循环口径原则(战斗屏只展小循环内被消费、影响当下决策的数据;中/大循环资源不得入侵)②单一展示面原则(同一信息全屏唯一展示面;单位本体已承载的信息数据框不重复;资源数字贴唯一消费口)。随拍板落地:① B 区站桩 = HP 与状态效果唯一展示面——C 区 HP 条/HP 文字/立绘占位/状态效果槽×4 退役(重复展示;证据链 = hud_presenter.gd 同一 s.hero.hp/s.hero_effects 双投喂 B/C 两区);② RAP 读数(点阵+数字)迁 D 区 AP 条、置弃图刷新按钮旁(RAP 唯一消费口 = 弃图刷新——就近原则);③ C 区状态槽悬停 tooltip 职责并入 B 区站桩状态小图标;④ GP(中循环)/钥石(大循环)迁出战斗屏,去向挂 OQ14;⑤ C 区改「攻击数据框 + 魔法道具槽(预留)+ 本关强化列表(预留)」——预留区来自魔法道具与关卡内属性提升的既有立项;⑥ 空态/清零反馈/悬停交互/AC/§13 义务表载体同步改写。**批内遗留两项待拍板(OQ13/14)**:护甲 AC 是否补入 C 区数据框;GP/钥石去向 |
| 2026-09-26 | **信息归属二轮拍板(用户 2026-09-26,OQ13/14 收口)**:① 我方护甲 AC 补入 C 区攻击数据框(唯一展示面——布局表/组件清单/线框图同步);② GP/钥石去向定案:GP = 关卡画面常驻总量 + 战斗结算画面总量+增量,钥石 = 结算画面 + 营地(具体呈现归对应 UX spec 批次);③ 敌方 AC 展示面挂起——用户口径「敌方 AC 无任何展示」与现行代码冲突(standee_view.gd 底库名字行渲染「名字·护甲·AC%d」,系敌人技能池批克制对策可读性义务),证据链已呈报待终裁,本 spec 暂不改敌人站桩行 | | 2026-09-26 | **信息归属二轮拍板(用户 2026-09-26,OQ13/14 收口)**:① 我方护甲 AC 补入 C 区攻击数据框(唯一展示面——布局表/组件清单/线框图同步);② GP/钥石去向定案:GP = 关卡画面常驻总量 + 战斗结算画面总量+增量,钥石 = 结算画面 + 营地(具体呈现归对应 UX spec 批次);③ 敌方 AC 展示面挂起——用户口径「敌方 AC 无任何展示」与现行代码冲突(standee_view.gd 底库名字行渲染「名字·护甲·AC%d」,系敌人技能池批克制对策可读性义务),证据链已呈报待终裁,本 spec 暂不改敌人站桩行 |
| 2026-09-26 | **敌方 AC 展示终裁收口(用户 2026-09-26 终裁)**:敌方 AC 数值全屏退役(任何区不直接展示)、护甲类型改图标方式展示(形状+色相双通道,终稿图标资产归美术批)、敌人站桩底座下名字行退回纯名字+护甲类型图标;旧「克制对策可读性义务」降级承载——类型保留、数值退役。终裁理由(用户口径):HP 信息已足够,攻防数值不直接写上;护甲类型图标保留克制对策可读性。同步改写:信息归属树敌人站桩行、Component Inventory 敌人站桩立绘行、OQ13。**代码实装说明**:`standee_view.gd` 改造(站桩名字行去 AC 数值 + 护甲类型图标化)由并行代码批执行,本批只做文档同步 | | 2026-09-26 | **敌方 AC 展示终裁收口(用户 2026-09-26 终裁)**:敌方 AC 数值全屏退役(任何区不直接展示)、护甲类型改图标方式展示(形状+色相双通道,终稿图标资产归美术批)、敌人站桩底座下名字行退回纯名字+护甲类型图标;旧「克制对策可读性义务」降级承载——类型保留、数值退役。终裁理由(用户口径):HP 信息已足够,攻防数值不直接写上;护甲类型图标保留克制对策可读性。同步改写:信息归属树敌人站桩行、Component Inventory 敌人站桩立绘行、OQ13。**代码实装说明**:`standee_view.gd` 改造(站桩名字行去 AC 数值 + 护甲类型图标化)由并行代码批执行,本批只做文档同步 |
+2 -2
View File
@@ -3,7 +3,7 @@
## Document Status ## Document Status
- Version: 1.3 - Version: 1.3
- Last Updated: 2026-09-23(棋盘 Node2D 渲染迁移 Phase A:D1 层表 BoardView 形态注记——引 ADR-0006) - Last Updated: 2026-09-26(战斗舞台 Node2D 迁移 Phase B:D1 层表 BattleStageView 形态注记——引 ADR-0006;Phase A 注记 2026-09-23)
- Engine: Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定) - Engine: Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定)
- Review Mode: solo(`production/review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调) - Review Mode: solo(`production/review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调)
- GDDs Covered: game-concept · game-pillars · systems-index · d2-data-config · a3-action-economy · a4-terrain-variation · b3-status-effect · a1-board-generation · a2-minesweeper-rules · b2-enemy-system · b4-reward-item-economy · b1-settlement(12 份;A5/C1–C4/B5/D3–D6 未设计,按 systems-index 依赖分层预留) - GDDs Covered: game-concept · game-pillars · systems-index · d2-data-config · a3-action-economy · a4-terrain-variation · b3-status-effect · a1-board-generation · a2-minesweeper-rules · b2-enemy-system · b4-reward-item-economy · b1-settlement(12 份;A5/C1–C4/B5/D3–D6 未设计,按 systems-index 依赖分层预留)
@@ -120,7 +120,7 @@
| 模块 | Owns | Exposes | Consumes | 引擎 API | | 模块 | Owns | Exposes | Consumes | 引擎 API |
|---|---|---|---|---| |---|---|---|---|---|
| **D1 HUD**(切片 lite 已拆 9 单元:BoardView / HUDView / HudPresenter / BattleLogView / ItemWindowOverlay / NodeOverlay / LevelStripView / TunePanel / UiKit + 组合根 main.gd) | 全部 Control/CanvasLayer(例外:BoardView 棋盘本体 = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界〔BoardWorld/CellUnit〕,探针契约面不变——ADR-0006 棋盘 Node2D 迁移批 Phase A 2026-09-23)、输入→意图翻译(**唯一提交路径 = settle_batch,不直调 A3**)、呈现义务台账(A2×6 / B2×4 / B3×4 / B4×5 / B1×3) | 视图单元接口(信号外发、setter 只读注入) | A2 compute_move_path / ActionOutcome;A3 ap/rap;B1 步骤流行协议(数据源);ConfigDB display_name | **MEDIUM**:Control / CanvasLayer / draw_* / InputEvent / RichTextLabel——对照 breaking-changes.md 4.5–4.7 条目(CanvasItem 线条无抗锯齿羽化、设备 ID 常量、RichTextLabel 图片参数两轮变化) | | **D1 HUD**(切片 lite 已拆 9 单元:BoardView / HUDView / HudPresenter / BattleLogView / ItemWindowOverlay / NodeOverlay / LevelStripView / TunePanel / UiKit + 组合根 main.gd) | 全部 Control/CanvasLayer(例外:BoardView 棋盘本体与 BattleStageView 站桩本体 = Control 壳 + SubViewportContainer + SubViewport + Node2D 世界〔BoardWorld/CellUnit;StageWorld/StandeeBody——B 区 slot_row 伴随件留壳层〕,探针契约面不变——ADR-0006 棋盘 Node2D 迁移批 Phase A 2026-09-23 / 战斗舞台 Phase B 2026-09-26)、输入→意图翻译(**唯一提交路径 = settle_batch,不直调 A3**)、呈现义务台账(A2×6 / B2×4 / B3×4 / B4×5 / B1×3) | 视图单元接口(信号外发、setter 只读注入) | A2 compute_move_path / ActionOutcome;A3 ap/rap;B1 步骤流行协议(数据源);ConfigDB display_name | **MEDIUM**:Control / CanvasLayer / draw_* / InputEvent / RichTextLabel——对照 breaking-changes.md 4.5–4.7 条目(CanvasItem 线条无抗锯齿羽化、设备 ID 常量、RichTextLabel 图片参数两轮变化) |
| **battle_presentation**(B5,垂直切片后) | 结算动画 / 伤害数字 / 战斗舞台 | (未设计——前置"战斗表现美术测试"锁双舞台陷阱) | B1 步骤流(回放脚本源)、D1 | **MEDIUM**:AnimationPlayer(String→StringName 变更)/ Tween | | **battle_presentation**(B5,垂直切片后) | 结算动画 / 伤害数字 / 战斗舞台 | (未设计——前置"战斗表现美术测试"锁双舞台陷阱) | B1 步骤流(回放脚本源)、D1 | **MEDIUM**:AnimationPlayer(String→StringName 变更)/ Tween |
| **audio**(D4,Alpha) | 音频挂点 | (挂点 = B1 明细行类型流;负反馈聚合——单结算至多一次) | B1 流水 | AudioStreamPlayer(4.7 area_mask 默认 1→0 注意) | | **audio**(D4,Alpha) | 音频挂点 | (挂点 = B1 明细行类型流;负反馈聚合——单结算至多一次) | B1 流水 | AudioStreamPlayer(4.7 area_mask 默认 1→0 注意) |
| **tutorial**(D5,Alpha) | 教学条目 | (口径来源:A2/A3/B3/B1 各教学条目) | 各上游 | 无 | | **tutorial**(D5,Alpha) | 教学条目 | (口径来源:A2/A3/B3/B1 各教学条目) | 各上游 | 无 |
@@ -55,11 +55,11 @@ BattleScreen(Control 栅格;目标形态 = B/C/D 三区——进度条退场
**不做**(明确出批):B 区舞台任何改动;素材终稿接入;格边长数值调整(归样张批);键盘寻格/无障碍(沿 ADR-0003 已接受口径)。 **不做**(明确出批):B 区舞台任何改动;素材终稿接入;格边长数值调整(归样张批);键盘寻格/无障碍(沿 ADR-0003 已接受口径)。
## 3. Phase B 预留(后续批,本批只保证不堵死) ## 3. Phase B(2026-09-26 已落地——战斗舞台 Node2D 迁移批,台账 170-173)
- BattleStageView 同构改造:PanelContainer → Control 壳(Node2D 舞台 + Control 文本半区),站桩 StandeeView 从 Control 迁 Node2D 精灵——意图三档动画/受击/死亡演出的载体。 - BattleStageView 同构改造**已实施**:PanelContainer → Control 壳(类名/252px/树内位置不变)+ Node2D 舞台半区(SubViewportContainer/SubViewport/StageWorld)+ Control 文本半区(round_label/stage_center/log_view 三件保留);站桩 StandeeView(Control)退役删除,本体 = StandeeBody(Node2D 世界精灵,行为面 17 方法契约保持);slot_row+悬停 tooltip 保留 Control 载体挂壳层(伴随件容器 = enemy_box 语义改写)。
- 本批交付的两个可复用挂点:SubViewport 内 theme/字体显式注入挂点(CJK 字体义务,ADR-0003 Decision 4 关联)、CellUnit 叠层槽位模式(B 区站桩底座同构)。 - Phase A 交付的两挂点已被 Phase B 复用:set_world_font 字体显式注入(C6 判据=注入非空+has_char+宽度>0)与坐标空间分界纪律(世界=局部排布,壳层换算单点 world_to_shell)。
- Phase B 开工硬前置 = 样张批冻结剪影与动画方案(清单 §1.2 联动条款),与本批无相互阻塞。 - **样张硬前置改判(用户 2026-09-26 裁决①)**:样张批冻结只卡换皮/演出批(循环 2 前置),不卡结构迁移——本节原「Phase B 开工硬前置 = 样张批冻结」条款据此作废;ADR-0006「只卡换皮不卡开工」原则的落文位。
## 4. 探针与测量面(实证清单) ## 4. 探针与测量面(实证清单)
+183 -28
View File
@@ -4,42 +4,90 @@
# 底部结算明细带(布局决策 D1 常驻双舞台分屏))。 # 底部结算明细带(布局决策 D1 常驻双舞台分屏))。
# Date: 2026-09-12(2026-09-20 站桩改造批:HeroCardView/EnemyCardView 退役,StandeeView 换装; # Date: 2026-09-12(2026-09-20 站桩改造批:HeroCardView/EnemyCardView 退役,StandeeView 换装;
# setter 面契约保持——set_enemies/clear_enemies/on_step_row_fx/gray_all_enemies 签名不变) # setter 面契约保持——set_enemies/clear_enemies/on_step_row_fx/gray_all_enemies 签名不变)
# 战斗舞台 Node2D 迁移批 Phase B 2026-09-26(循环 1 T2 壳 + T3 站桩迁入;ADR-0006 /
# board-node2d-migration-plan §3):根壳 PanelContainer → Control(类名/尺寸 252px/树内位置
# 不变),内部两半区——「Node2D 舞台半区 = SubViewportContainer + SubViewport + StageWorld
# (站桩本体 StandeeBody 世界精灵)」+「Control 文本半区 = round_label / stage_center /
# log_view 三件原样保留(C1 永久 Control)」。SubViewport 逻辑分辨率 = 壳层像素 1:1(stretch,
# 禁任何缩放系数——C2/C5);字体显式注入挂点 set_world_font(C6,Phase A BoardView 同款)。
# T3 批内代拍板默认(待追认):①enemy_box 语义改写「站桩壳层伴随件容器」(HBoxContainer →
# Control,子件 = 各站桩的 StatusSlotRow 伴随件,手动定位随世界同步——探针 :97 计数面不变);
# ②hero_standee/enemy 站桩 = StandeeBody(Node2D 世界本体,行为面契约逐项保持 C3),其
# slot_row 成员 = 壳层伴随件引用;③定位同步 = 世界→壳层换算单点 world_to_shell,重建/重排
# 挂点调用(零每帧成本,C4)。旧 StandeeView(Control)随迁退役删除。
# 高内聚边界:容器+组合视图——站桩槽位管理、轮次指示、结算行演出分发改由 setter 面承接; # 高内聚边界:容器+组合视图——站桩槽位管理、轮次指示、结算行演出分发改由 setter 面承接;
# 结算流水格式化归 BattleLogView,核心状态→文案装配归 hud_presenter.gd。 # 结算流水格式化归 BattleLogView,核心状态→文案装配归 hud_presenter.gd。
class_name BattleStageView class_name BattleStageView
extends PanelContainer extends Control
var hero_standee: StandeeView const _STAGE_WORLD := preload("res://presentation/views/stage_world.gd")
const _STANDEE_BODY := preload("res://presentation/views/standee_body.gd")
const _CONTENT_MARGINS := Rect2(6, 4, 6, 4) # 原 PanelContainer stylebox 内容边距(子面几何零漂移)
const _STANDEE_W := 150.0 # 站桩盒宽(旧 StandeeView custom_minimum_size)
const _STANDEE_GAP := 8.0 # 敌站桩间距(旧 enemy_box HBox separation)
var hero_standee: Node2D # StandeeBody——主角站桩本体(世界内;slot_row = 壳层伴随件引用)
var round_label: Label var round_label: Label
var stage_center: Label var stage_center: Label
var enemy_box: HBoxContainer var enemy_box: Control # 站桩壳层伴随件容器(T3 语义改写:HBoxContainer → Control)
var log_view: BattleLogView var log_view: BattleLogView
var _cards: Array = [] var _cards: Array = [] # StandeeBody(世界本体,敌)
var _row: HBoxContainer # 文本半区行(stage_center 载体 + 世界布局基准)
var _companion_layer: Control # 壳层伴随件层(主角 slot_row 宿主;敌伴随件宿主 = enemy_box)
var _svp_container: SubViewportContainer
var _viewport: SubViewport
var world: Node2D # StageWorld(preload 脚本实例——Node2D 世界根/字体挂点)
func _init(db_ref) -> void: func _init(db_ref) -> void:
custom_minimum_size = Vector2(0, 252) custom_minimum_size = Vector2(0, 252)
# 面板底(PanelContainer 面板样式等价平移——Control 壳不自绘 stylebox,Panel 子面承载)
var bg := Panel.new()
var sb := UiKit.panel_flat(UiKit.PANEL_STAGE, UiKit.PANEL_BORDER, 0, 1, Vector4(6, 4, 6, 4)) var sb := UiKit.panel_flat(UiKit.PANEL_STAGE, UiKit.PANEL_BORDER, 0, 1, Vector4(6, 4, 6, 4))
add_theme_stylebox_override("panel", sb) bg.add_theme_stylebox_override("panel", sb)
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(bg)
# ── Node2D 舞台半区(ADR-0006 Phase B 同构;stretch 1:1 无缩放系数——C2/C5)──
_svp_container = SubViewportContainer.new()
_svp_container.stretch = true # 容器尺寸 = 视口逻辑分辨率,恒 1:1
_svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # 世界零输入(tooltip 归壳层伴随件)
_svp_container.set_anchors_preset(Control.PRESET_FULL_RECT)
_svp_container.offset_left = _CONTENT_MARGINS.position.x
_svp_container.offset_top = _CONTENT_MARGINS.position.y
_svp_container.offset_right = -_CONTENT_MARGINS.size.x
_svp_container.offset_bottom = -_CONTENT_MARGINS.size.y
add_child(_svp_container)
_viewport = SubViewport.new()
_viewport.transparent_bg = true # 露出面板底色(与旧空隙同观感)
_svp_container.add_child(_viewport)
world = _STAGE_WORLD.new()
_viewport.add_child(world)
# ── Control 文本半区(round_label / stage_center / log_view 原样保留——C1 永久 Control)──
var vb := VBoxContainer.new() var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 4) vb.add_theme_constant_override("separation", 4)
vb.set_anchors_preset(Control.PRESET_FULL_RECT)
vb.offset_left = _CONTENT_MARGINS.position.x
vb.offset_top = _CONTENT_MARGINS.position.y
vb.offset_right = -_CONTENT_MARGINS.size.x
vb.offset_bottom = -_CONTENT_MARGINS.size.y
add_child(vb) add_child(vb)
round_label = UiKit.label("第 — 轮 · —", 15, true) round_label = UiKit.label("第 — 轮 · —", 15, true)
round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vb.add_child(round_label) vb.add_child(round_label)
var row := HBoxContainer.new() _row = HBoxContainer.new()
row.add_theme_constant_override("separation", 10) _row.add_theme_constant_override("separation", 10)
row.size_flags_vertical = Control.SIZE_EXPAND_FILL _row.size_flags_vertical = Control.SIZE_EXPAND_FILL
vb.add_child(row) vb.add_child(_row)
_row.resized.connect(_on_row_resized) # 明细带展开/收起改行高——世界布局随动
hero_standee = StandeeView.new("hero")
hero_standee.set_portrait_texture(UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT), true) # B 区立绘 48×48 朝右(2026-09-26 用户点名 hero_idle_01 换装+水平向右;素材缺位 null 回退字形「人」)
row.add_child(hero_standee)
var center := CenterContainer.new() var center := CenterContainer.new()
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
center.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行内只剩文本——鼠标全放行给壳层伴随件
var center_col := VBoxContainer.new() var center_col := VBoxContainer.new()
center_col.alignment = BoxContainer.ALIGNMENT_CENTER center_col.alignment = BoxContainer.ALIGNMENT_CENTER
center.add_child(center_col) center.add_child(center_col)
@@ -47,29 +95,133 @@ func _init(db_ref) -> void:
stage_center.add_theme_color_override("font_color", UiKit.STONE_FAINT) stage_center.add_theme_color_override("font_color", UiKit.STONE_FAINT)
stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
center_col.add_child(stage_center) center_col.add_child(stage_center)
row.add_child(center) _row.add_child(center)
enemy_box = HBoxContainer.new() enemy_box = Control.new() # 站桩壳层伴随件容器(T3 语义改写)
enemy_box.add_theme_constant_override("separation", 8) enemy_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
enemy_box.alignment = BoxContainer.ALIGNMENT_END _row.add_child(enemy_box)
row.add_child(enemy_box)
log_view = BattleLogView.new(db_ref) log_view = BattleLogView.new(db_ref)
vb.add_child(log_view) vb.add_child(log_view)
# ── 壳层伴随件层(最顶——世界站桩之上的 Control 触达面)──
_companion_layer = Control.new()
_companion_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
_companion_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_companion_layer)
# 主角站桩本体(世界)+ 状态行伴随件(壳层)
hero_standee = _STANDEE_BODY.new("hero")
hero_standee.set_portrait_texture(UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT), true) # B 区立绘 48×48 朝右(2026-09-26 用户点名 hero_idle_01 换装+水平向右;素材缺位 null 回退字形「人」)
world.add_child(hero_standee)
hero_standee.slot_row = _make_slot_companion(_companion_layer)
func _make_slot_companion(parent: Control) -> StatusSlotRow:
# 状态效果唯一展示面(2026-09-26 C 区精简批迁入站桩)——compact 档伴随件,tooltip 全明细
var comp := StatusSlotRow.new()
comp.compact = true
comp.alignment = BoxContainer.ALIGNMENT_CENTER
comp.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行容器放行鼠标事件给槽位(悬停 tooltip)
comp.visible = false # 空态 = 图标行隐藏不占位(增益状态 EC12)
parent.add_child(comp)
return comp
# ── 布局同步(世界→壳层单点——C4;站桩无逐帧位移,重建/重排挂点调用,零每帧成本)──
func world_to_shell(p: Vector2) -> Vector2:
return _svp_container.global_position + p
func _on_row_resized() -> void:
_sync_layout.call_deferred()
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED:
# stretch=true 时视口尺寸由容器接管;树外(冒烟直建)才需手设保 1:1(Phase A 同款)
if not _svp_container.is_inside_tree():
_viewport.size = _svp_container.size
_sync_layout.call_deferred()
func _ready() -> void:
_sync_layout.call_deferred() # 首轮布局落定后对位(row 几何此时才有效)
func _sync_layout() -> void:
if not is_inside_tree() or hero_standee == null:
return
var origin: Vector2 = _svp_container.global_position
var row_rect: Rect2 = _row.get_global_rect()
var local := row_rect.position - origin # 行原点 → 世界坐标(1:1,C2)
var box_h := row_rect.size.y # 站桩盒高 = 行高(旧 HBox FILL 拉伸等价)
hero_standee.box = Vector2(_STANDEE_W, box_h)
hero_standee.position = local
# 敌站桩右对齐(旧 HBox ALIGNMENT_END + separation 8 等价)
var n := _cards.size()
var total := n * _STANDEE_W + maxi(0, n - 1) * _STANDEE_GAP
var x := row_rect.size.x - total
for i in n:
var body: Node2D = _cards[i]
if not is_instance_valid(body):
continue
body.box = Vector2(_STANDEE_W, box_h)
body.position = local + Vector2(x + float(i) * (_STANDEE_W + _STANDEE_GAP), 0.0)
_sync_companions()
func _sync_companions() -> void:
# 状态行伴随件 = 世界站桩顶部(血条正下方偏移 (8,14)——旧 top VBox 几何)的壳层投影
var layer_pos: Vector2 = _companion_layer.global_position
if hero_standee != null and hero_standee.slot_row != null:
var comp: StatusSlotRow = hero_standee.slot_row
comp.reset_size()
comp.position = world_to_shell(hero_standee.position + Vector2(8.0 + (_STANDEE_W - 16.0 - comp.size.x) * 0.5, 14.0)) - layer_pos
for i in _cards.size():
var body: Node2D = _cards[i]
if not is_instance_valid(body) or body.slot_row == null:
continue
var comp2: StatusSlotRow = body.slot_row
comp2.reset_size()
comp2.position = world_to_shell(body.position + Vector2(8.0 + (_STANDEE_W - 16.0 - comp2.size.x) * 0.5, 14.0)) - layer_pos
# SubViewport 主题/字体显式注入挂点(C6——Phase A BoardView.set_world_font 同款;站桩本体随动)
func set_world_font(f: Font) -> void:
world.world_font = f
hero_standee.font = f
for c in _cards:
if is_instance_valid(c):
c.font = f
c.queue_redraw()
# 敌方站桩本体出口(探针等价改写用——T5;旧 enemy_box 子件 = 站桩 Control 改伴随件后的本体通路)
func enemy_body_at(i: int) -> Node2D:
if i < 0 or i >= _cards.size():
return null
return _cards[i]
# 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律); # 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律);
# 数量变化 = 新战斗域,重建站桩槽(轮内不增删,无回流) # 数量变化 = 新战斗域,重建站桩槽(轮内不增删,无回流)
func set_enemies(cards_data: Array) -> void: func set_enemies(cards_data: Array) -> void:
if _cards.size() != cards_data.size(): if _cards.size() != cards_data.size():
for body in _cards:
if is_instance_valid(body):
body.queue_free()
for c in enemy_box.get_children(): for c in enemy_box.get_children():
c.queue_free() c.queue_free()
_cards.clear() _cards.clear()
for d in cards_data: for d in cards_data:
var standee := StandeeView.new("enemy") var body: Node2D = _STANDEE_BODY.new("enemy")
standee.set_data(d) world.add_child(body)
enemy_box.add_child(standee) body.slot_row = _make_slot_companion(enemy_box)
_cards.append(standee) body.set_data(d)
_cards.append(body)
_sync_layout.call_deferred()
return return
for i in cards_data.size(): for i in cards_data.size():
if is_instance_valid(_cards[i]): if is_instance_valid(_cards[i]):
@@ -77,6 +229,9 @@ func set_enemies(cards_data: Array) -> void:
func clear_enemies() -> void: func clear_enemies() -> void:
for body in _cards:
if is_instance_valid(body):
body.queue_free()
for c in enemy_box.get_children(): for c in enemy_box.get_children():
c.queue_free() c.queue_free()
_cards.clear() _cards.clear()
@@ -94,7 +249,7 @@ func on_step_row_fx(row: Dictionary) -> void:
var target := String(row.get("target", "")) var target := String(row.get("target", ""))
if target == "hero" or target == "": if target == "hero" or target == "":
return return
var card: StandeeView = _card_by_name(target) var card: Node2D = _card_by_name(target)
if card == null: if card == null:
return return
card.flash_hit() card.flash_hit()
@@ -102,25 +257,25 @@ func on_step_row_fx(row: Dictionary) -> void:
card.mark_dead() card.mark_dead()
func _card_by_name(display_name: String) -> StandeeView: func _card_by_name(display_name: String) -> Node2D:
for c in _cards: for c in _cards:
if not is_instance_valid(c): if not is_instance_valid(c):
continue continue
if String(c._name_label.text).contains(display_name): # 名字行图标化批 2026-09-26:名字 Label 独立节点(_name_line 已改 HBox 容器) if c.display_name().contains(display_name):
return c return c
return null return null
# 跨区弹道目标坐标(结算演出链批——组合根传给 SettlementFxView 的全局锚点) # 跨区弹道目标坐标(结算演出链批——组合根传给 SettlementFxView 的全局锚点;壳层口径 C4)
func enemy_standee_center(display_name: String): func enemy_standee_center(display_name: String):
var card := _card_by_name(display_name) var card := _card_by_name(display_name)
if card == null or not is_instance_valid(card): if card == null or not is_instance_valid(card):
return null return null
return card.get_global_rect().get_center() return world_to_shell(card.position + card.box * 0.5)
func hero_standee_center() -> Vector2: func hero_standee_center() -> Vector2:
return hero_standee.get_global_rect().get_center() return world_to_shell(hero_standee.position + hero_standee.box * 0.5)
# battle_over:全场敌方站桩灰化兜底(短路击杀等未走 killed 行的边角) # battle_over:全场敌方站桩灰化兜底(短路击杀等未走 killed 行的边角)
+21
View File
@@ -0,0 +1,21 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 战斗舞台 Node2D 迁移批 Phase B 2026-09-26(循环 1 T2/T3):StageWorld——B 区战斗舞台 Node2D
# 世界根(SubViewport 内站桩本体容器)。ADR-0006 分工:Node2D = D 区棋盘 + B 区战斗舞台;
# 与 BoardWorld(Phase A)同构。职责单点:主题/字体显式注入槽位(SubViewport 不自动继承主场景
# Control 主题——ADR-0003 Decision 4;set_world_font 同款挂点,null = ThemeDB.fallback_font 与
# 旧 Label 缺省主题同源)+ 站桩本体装载面(T3 迁入)。无 class_name(类名缓存面纪律——
# BattleStageView 经 preload 使用)。
# 坐标空间钉死(台账 160 纪律延伸):世界内 = 局部排布坐标(SubViewport 逻辑分辨率 =
# 壳层像素 1:1,无缩放系数——C2/C5);壳层/fx 层 = 全局含容器偏移,换算单点在
# BattleStageView.world_to_shell()。
extends Node2D
var world_font: Font = null # 字体注入槽位(null = ThemeDB.fallback_font——BattleStageView.set_world_font 注入)
# C6 headless 可验判据(ADR-0003 Decision 4):注入字体非空 + 中文字形 has_char + 采样文本宽度 > 0。
# 实际光栅观感归批末目检清单。
func font_probe() -> Dictionary:
var f: Font = world_font if world_font != null else ThemeDB.fallback_font
var has_char: bool = f.has_char("人".unicode_at(0))
var width: float = f.get_string_size("兑现演出区", HORIZONTAL_ALIGNMENT_LEFT, -1, 14).x
return {"font": f, "has_char": has_char, "width": width}
@@ -0,0 +1 @@
uid://dqo6k0y6ncmov
+348
View File
@@ -0,0 +1,348 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
# 战斗舞台 Node2D 迁移批 Phase B 2026-09-26(循环 1 T3):站桩本体自 Control(旧 StandeeView,
# 退役删除)平移为 Node2D 世界精灵(ADR-0006:B 区战斗舞台归 Node2D)。行为面契约逐项保持:
# set_data/set_hero/_set_hp/set_idle/set_effects/set_effects_cleared_feedback/set_order/set_pending/
# _play_intent_motion/set_portrait_texture(tex,flip)/mark_dead/is_dead/flash_hit/_revive/
# _update_name_line/_refresh_portrait_glyph(调用点 hud_presenter/main/探针零改造目标——C3)。
# 等价平移基线 = 旧 Control 占位表现(C9):立绘贴图(flip/NEAREST/整数 2× 档)+ 底座 + 血条
# (直角递减 tween)+ 顺位徽标 + 意图徽标(弹入+立绘前倾一次性动画)+ 名字行(纯名+护甲类型
# 图标——2026-09-26 终裁 AC 不展示)+ 受击闪白 + 死亡灰化摇摆塌陷(禁瞬消)+ 复活 + 字形占位
# 回退(人/▂/▆/█/✕/敌)。批内代拍板默认(待追认):①状态图标行(slot_row)与悬停 tooltip 保留
# Control 载体挂壳层(tooltip 机制零返工)——本体的 slot_row 成员 = 壳层伴随件引用;
# ②状态行隐藏时序 = 随 mark_dead 同拍隐藏;③清空反馈微文案挂伴随件父层(壳层)。
# 文字全部经 draw_string + 世界字体(world.world_font 或 ThemeDB.fallback_font——C6 挂点链)。
# 坐标空间:本节点 position/box = SubViewport 世界内局部排布;壳层换算归
# BattleStageView.world_to_shell 单点(C4)。
# 无 class_name(类名缓存面纪律——BattleStageView 经 preload 使用)。
extends Node2D
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 /
# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。(自旧 StandeeView 平移)
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
var box := Vector2(150, 176) # 站桩盒尺寸(旧 Control custom_minimum_size;实际高随壳层 row 布局注入)
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入)
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界)
var _kind := "enemy"
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
var _portrait_flip := false
var _order := 1
var _intent_glyph := ""
var _intent_col := Color.WHITE
var _intent_visible := false
var _intent_scale := 1.0: # 意图弹入动画参数(旧 tween scale 等价)
set(v):
_intent_scale = v
queue_redraw()
var _glyph_rot := 0.0: # 立绘前倾角(旧只转字形 Label——贴图态无此动画,等价平移)
set(v):
_glyph_rot = v
queue_redraw()
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _dead := false
var _hp_max := 1.0
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
set(v):
hp_value = clampf(v, 0.0, 1.0)
queue_redraw()
var _hp_tween: Tween
var _hp_flash_tw: Tween
func _init(kind: String = "enemy") -> void:
_kind = kind
func _efont() -> Font:
return font if font != null else ThemeDB.fallback_font
func display_name() -> String:
return _display_name
# 血条存在性探针出口(T5 断言等价改写——旧 _hp_bar ProgressBar 退役,世界直角血条常在)
var has_world_hp_bar := true
# 意图徽标探针出口(T5——旧 _intent_badge.visible/text 改判)
func intent_probe() -> Dictionary:
return {"visible": _intent_visible, "text": _intent_glyph}
# 名字行探针出口(T5——旧 _name_label/_armor_icon.text 改判)
func name_line_probe() -> Dictionary:
return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))}
# ---- setter 面(投影契约与旧 StandeeView 一致——presenter/组合根零改排) ----
# data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id,
# pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)}
func set_data(d: Dictionary) -> void:
_display_name = String(d.get("display_name", "敌人"))
_enemy_id = String(d.get("enemy_id", ""))
_armor = String(d.get("armor_type", "light"))
_hp_max = float(maxi(1, int(d.get("max_hp", 1))))
_set_hp(int(d.get("hp", 0)))
set_order(int(d.get("order", 1)))
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
set_effects(d.get("effects", []))
_refresh_portrait_glyph()
if _dead and int(d.get("hp", 0)) > 0:
_revive()
queue_redraw()
func set_hero(hp: int, max_hp: int) -> void:
_hp_max = float(maxi(1, max_hp))
_set_hp(hp)
# 血条短 tween 递减(直角递减占位——密度/缓动终稿归战斗表现批);逐行刷新时重启 tween 不累积
func _set_hp(hp: int) -> void:
var target := float(clampi(hp, 0, int(_hp_max))) / _hp_max
if _hp_tween != null and _hp_tween.is_valid():
_hp_tween.kill()
_hp_tween = create_tween()
_hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))
func set_idle() -> void:
_set_hp(0)
set_effects([])
# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位)
func set_effects(effects: Array) -> void:
if slot_row == null:
return
slot_row.set_effects(effects)
slot_row.visible = not effects.is_empty()
# battle_over:状态图标行全清可见反馈(微文案挂伴随件父层 = 壳层)
func set_effects_cleared_feedback() -> void:
if slot_row == null:
return
slot_row.visible = true
slot_row.set_cleared_feedback()
var note := UiKit.label("状态随战斗结束消散", 11, false)
note.add_theme_color_override("font_color", UiKit.STONE_FAINT)
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
note.mouse_filter = Control.MOUSE_FILTER_IGNORE
var parent := slot_row.get_parent()
if parent != null:
parent.add_child(note)
note.position = slot_row.position + Vector2((slot_row.size.x - 134.0) * 0.5, slot_row.size.y)
var tw := create_tween()
tw.tween_interval(1.2)
tw.tween_property(note, "modulate:a", 0.0, UiKit.motion(0.8))
tw.tween_callback(note.queue_free)
func set_order(i: int) -> void:
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
_order = i
queue_redraw()
# 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;
# "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。
func set_pending(ptype: String, pid: String) -> void:
if _dead:
_intent_visible = false
queue_redraw()
return
if ptype == "" or ptype == "basic":
_intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
queue_redraw()
return
var glyph := "⚔" # 攻击系(damage)
var col: Color = UiKit.FLAG_RED
if ptype == "buff":
glyph = "✦" # 增益系
col = UiKit.HERO_BLUE
elif ptype == "debuff":
glyph = "☠" # 妨害系(新效应词汇批 2026-09-19)
col = UiKit.DEBUFF_PURPLE
if pid in CHARGE_SKILL_IDS:
col = UiKit.GOLD_DEEP # 重击类蓄力档(2026-09-18 用户拍板独立蓄力提示)
_intent_glyph = glyph
_intent_col = col
_intent_visible = true
_play_intent_motion()
# 三档动作动画占位:摇定时一次性短动画(意图图标弹入 + 立绘前倾回正),随后静帧保持
func _play_intent_motion() -> void:
var tw := create_tween()
tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10))
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16))
tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10))
tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14))
# 资产挂点(战斗表现样张批):tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
_portrait_flip = flip
if tex == null:
if _portrait_sprite != null:
_portrait_sprite.visible = false
queue_redraw()
return
_portrait_tex = tex
if _portrait_sprite == null:
_portrait_sprite = Sprite2D.new()
_portrait_sprite.centered = false
_portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊(棋盘小人贴图批 2026-09-25)
add_child(_portrait_sprite)
_portrait_sprite.texture = tex
_portrait_sprite.flip_h = flip
_portrait_sprite.visible = true
_place_portrait()
func _place_portrait() -> void:
# 立绘盒 = (0,44)-(box.x, box.y-36)(旧 FULL_RECT offsets 等价);48 源 ×2 整数档居中
if _portrait_sprite == null or _portrait_tex == null:
return
var box_h := box.y - 44.0 - 36.0
var w := _portrait_tex.get_width() * 2.0
var h := _portrait_tex.get_height() * 2.0
_portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
_portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
func mark_dead() -> void:
if _dead:
return
_dead = true
modulate = Color(0.62, 0.62, 0.62, 0.9)
var tw := create_tween()
tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12))
tw.tween_property(self, "rotation_degrees", -1.5, UiKit.motion(0.1))
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
_refresh_portrait_glyph()
_intent_visible = false # 死亡残位意图退位(旧同款)
_set_hp(0)
if slot_row != null:
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏(批内代拍板②待追认)
func is_dead() -> bool:
return _dead
# 受击闪白(结算演出占位——§2.2 受击反馈)
func flash_hit() -> void:
if _hp_flash_tw != null and _hp_flash_tw.is_valid():
_hp_flash_tw.kill()
_hp_flash_tw = create_tween()
_hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
_hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2))
func _revive() -> void:
_dead = false
modulate = Color(1, 1, 1)
rotation_degrees = 0.0
_refresh_portrait_glyph()
if slot_row != null:
slot_row.visible = true
queue_redraw()
func _refresh_portrait_glyph() -> void:
queue_redraw()
func _portrait_glyph() -> String:
if _kind == "hero":
return "人"
if _dead:
return "✕"
match _enemy_id:
"goblin":
return "▂" # 小体量墩形
"bugbear":
return "▆" # 垂直延伸体量
"ogre_chief":
return "█" # 最大体量
_:
return "敌"
func _portrait_color() -> Color:
return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP
# 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用
func _portrait_rect() -> Rect2:
return Rect2(0, 44, box.x, box.y - 80.0)
func _draw() -> void:
var f := _efont()
# 底座(旧 panel_flat(STONE.darkened(0.35), STONE.darkened(0.1), 6, 1) 等价——BOTTOM_WIDE 26/-26, -18/-8)
var plat := UiKit.panel_flat(UiKit.STONE.darkened(0.35), UiKit.STONE.darkened(0.1), 6, 1)
plat.draw(get_canvas_item(), Rect2(26, box.y - 18.0, box.x - 52.0, 10.0))
# 血条(直角递减):敌附顺位徽标(GOLD_DEEP ①②③)——旧 top VBox 几何等价(左 8 / 右 -8)
var bar_x := 8.0
if _kind == "enemy":
var order_txt := String("①②③"[clampi(_order - 1, 0, 2)])
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_DEEP)
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)) # 条底(引擎默认主题深底近似平移)
var fill_w := bar_w * hp_value
if fill_w > 0.0:
draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减)
# 立绘:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退
if _portrait_sprite == null or _portrait_sprite.texture == null or not _portrait_sprite.visible:
var prect := _portrait_rect()
var glyph := _portrait_glyph()
var center := prect.get_center()
draw_set_transform(center, deg_to_rad(_glyph_rot), Vector2.ONE)
var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42)
f.draw_string(get_canvas_item(), center - Vector2(ts.x * 0.5, -ts.y * 0.35), glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color())
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
# 意图徽标(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
if _intent_visible:
var icenter := Vector2(box.x * 0.5, 86.0)
draw_set_transform(icenter, 0.0, Vector2(_intent_scale, _intent_scale))
var its := f.get_string_size(_intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
f.draw_string(get_canvas_item(), icenter - Vector2(its.x * 0.5, -its.y * 0.35), _intent_glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, _intent_col)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
# 底座下 护甲类型图标+名字 常驻行(敌 only——2026-09-26 用户终裁:AC 数值不展示,类型图标化)
if _kind == "enemy":
var armor_txt := String(ARMOR_ICONS.get(_armor, "◇"))
var col: Color = UiKit.STONE # 轻甲 = 灰石(中性轻薄)
match _armor:
"ward":
col = UiKit.HERO_BLUE # 魔法护甲 = 小人蓝
"heavy":
col = UiKit.GOLD_DEEP # 重甲 = 深金
var name_w := f.get_string_size(_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)
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), _display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.INK_TITLE)
@@ -0,0 +1 @@
uid://dliuugrkyq2ra
-324
View File
@@ -1,324 +0,0 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
# 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装)
# + 底座;血条悬浮头顶;状态效果唯一展示面挂血条正下方(2026-09-26 C 区精简批:compact 档
# StatusSlotRow 组件整体迁入——定稿 PNG 图标+层数 ×N 角标+悬停 tooltip〔来源+语义+剩余〕,
# tooltip 义务自 C 区状态槽并入;空态 = 图标行隐藏)。
# 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 /
# 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持,
# 挖掘期不循环——art-bible §2.1 能量纪律,终稿动画密度归战斗表现批);底座下 护甲类型图标+名字
# 常驻行(2026-09-26 用户终裁:AC 数值不展示、护甲类型图标化——敌人技能池批克制对策可读性
# 义务降级承载:类型保留可读、数值移除;HP 信息已足够)。
# 死亡呈现(敌人系统 资产下限):灰化 + 摇摆塌陷残位,禁瞬消;受击闪白沿用。
# Date: 2026-09-20(站桩改造批——hero_card_view/enemy_card_view 退役删除)
class_name StandeeView
extends Control
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型图标占位——终稿图标资产归美术批(2026-09-26 用户终裁:AC 数值不展示,类型图标化)。
# 形状+色相双通道(art-bible):轻甲=◇ 空心菱(轻薄)·灰石中性 / 魔法护甲=◆ 实心晶菱·小人蓝
# (避开 Boss 专属紫赤 #8F2F6A 与妨害紫——两者各有专属语义)/ 重甲=▣ 实心方板(厚重板甲)·深金。
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
var _kind := "enemy"
var _portrait: Label
var _portrait_tex: TextureRect = null # 资产挂点(null = 未落位,字形在位)
var _platform: Panel
var _hp_bar: ProgressBar
var _order_badge: Label = null
var _intent_badge: Label = null
var _top_row: VBoxContainer
var slot_row: StatusSlotRow # 状态效果唯一展示面(2026-09-26 C 区精简批——compact 档组件整体迁入;
# 悬停 tooltip 来源+语义+剩余 义务随之从 C 区状态槽并入,主角/敌方共用)
var _name_line: HBoxContainer = null
var _armor_icon: Label = null
var _name_label: Label = null
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _dead := false
var _hp_tween: Tween
func _init(kind: String = "enemy") -> void:
_kind = kind
custom_minimum_size = Vector2(150, 176)
mouse_filter = Control.MOUSE_FILTER_IGNORE
# 头顶悬浮层:血条(敌附顺位徽标)+ 状态小图标行
var top := VBoxContainer.new()
_top_row = top
top.set_anchors_preset(Control.PRESET_TOP_WIDE)
top.offset_left = 8.0
top.offset_right = -8.0
top.add_theme_constant_override("separation", 2)
add_child(top)
var hp_row := HBoxContainer.new()
hp_row.add_theme_constant_override("separation", 4)
top.add_child(hp_row)
if kind == "enemy":
_order_badge = UiKit.label("①", 13, true)
_order_badge.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
hp_row.add_child(_order_badge)
_hp_bar = ProgressBar.new()
_hp_bar.custom_minimum_size = Vector2(0, 10)
_hp_bar.show_percentage = false
_hp_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
hp_row.add_child(_hp_bar)
slot_row = StatusSlotRow.new()
slot_row.compact = true # 站桩迷你档——图标 16px+层数角标,tooltip 全明细
slot_row.alignment = BoxContainer.ALIGNMENT_CENTER
slot_row.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
slot_row.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行容器放行鼠标事件给槽位(悬停 tooltip)
top.add_child(slot_row)
# 立绘(程序占位字形)
_portrait = UiKit.label("人" if kind == "hero" else "敌", 42, true)
_portrait.add_theme_color_override("font_color", UiKit.HERO_BLUE if kind == "hero" else UiKit.RED_DEEP)
_portrait.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_portrait.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_portrait.set_anchors_preset(Control.PRESET_FULL_RECT)
_portrait.offset_top = 44.0
_portrait.offset_bottom = -26.0
_portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_portrait)
# 意图指示(敌 only——立绘上半部居中悬浮)
if kind == "enemy":
_intent_badge = UiKit.label("", 15, true)
_intent_badge.set_anchors_preset(Control.PRESET_CENTER_TOP)
_intent_badge.offset_left = -20.0
_intent_badge.offset_right = 20.0
_intent_badge.offset_top = 74.0
_intent_badge.offset_bottom = 98.0
_intent_badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_intent_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
_intent_badge.visible = false
add_child(_intent_badge)
# 底座
_platform = Panel.new()
_platform.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_platform.offset_left = 26.0
_platform.offset_right = -26.0
_platform.offset_top = -18.0
_platform.offset_bottom = -8.0
_platform.mouse_filter = Control.MOUSE_FILTER_IGNORE
_platform.add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.STONE.darkened(0.35), UiKit.STONE.darkened(0.1), 6, 1))
add_child(_platform)
# 底座下 护甲类型图标+名字 常驻行(敌 only——2026-09-26 用户终裁:AC 数值不展示,类型图标化)
if kind == "enemy":
_name_line = HBoxContainer.new()
_name_line.alignment = BoxContainer.ALIGNMENT_CENTER
_name_line.add_theme_constant_override("separation", 3)
_name_line.mouse_filter = Control.MOUSE_FILTER_IGNORE
_name_line.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_name_line.offset_top = -8.0
add_child(_name_line)
_armor_icon = UiKit.label("", 12, true)
_armor_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
_name_line.add_child(_armor_icon)
_name_label = UiKit.label("", 12, false)
_name_label.add_theme_color_override("font_color", UiKit.INK_TITLE)
_name_label.clip_text = true
_name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_name_line.add_child(_name_label)
# ---- setter 面(投影契约与旧敌卡一致——presenter 装配面零改排) ----
# data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id,
# pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)}
func set_data(d: Dictionary) -> void:
_display_name = String(d.get("display_name", "敌人"))
_enemy_id = String(d.get("enemy_id", ""))
_armor = String(d.get("armor_type", "light"))
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
_set_hp(int(d.get("hp", 0)))
set_order(int(d.get("order", 1)))
set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", "")))
set_effects(d.get("effects", []))
_update_name_line()
_refresh_portrait_glyph()
if _dead and int(d.get("hp", 0)) > 0:
_revive()
func set_hero(hp: int, max_hp: int) -> void:
_hp_bar.max_value = maxi(1, max_hp)
_set_hp(hp)
# 血条短 tween 递减(§2.2 直角递减占位——密度/缓动终稿归战斗表现批);逐行刷新时重启 tween 不累积
func _set_hp(hp: int) -> void:
var target := float(clampi(hp, 0, int(_hp_bar.max_value)))
if _hp_tween != null and _hp_tween.is_valid():
_hp_tween.kill()
_hp_tween = create_tween()
_hp_tween.tween_property(_hp_bar, "value", target, UiKit.motion(0.15))
func set_idle() -> void:
_set_hp(0)
set_effects([])
# 主角/敌方共用——状态效果唯一展示面(2026-09-26 C 区精简批:compact 档 StatusSlotRow 整体迁入,
# 原 B 区迷你字形行退役;悬停 tooltip(来源+语义+剩余结算次数)义务自 C 区状态槽并入本组件;
# 空态 = 图标行隐藏不占位(增益状态 EC12))
func set_effects(effects: Array) -> void:
slot_row.set_effects(effects)
slot_row.visible = not effects.is_empty()
# battle_over:状态图标行全清可见反馈(增益状态×4④ 载体迁移——逐槽化光 + 微文案,
# 自 C 区状态槽迁入;主角侧调用,敌方不适用)
func set_effects_cleared_feedback() -> void:
slot_row.visible = true
slot_row.set_cleared_feedback()
var note := UiKit.label("状态随战斗结束消散", 11, false)
note.add_theme_color_override("font_color", UiKit.STONE_FAINT)
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
note.mouse_filter = Control.MOUSE_FILTER_IGNORE
_top_row.add_child(note)
var tw := create_tween()
tw.tween_interval(1.2)
tw.tween_property(note, "modulate:a", 0.0, UiKit.motion(0.8))
tw.tween_callback(note.queue_free)
func set_order(i: int) -> void:
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
if _order_badge != null:
_order_badge.text = String("①②③"[clampi(i - 1, 0, 2)])
# 摇定动作渲染(敌人技能池批 三档可见性;渲染映射唯一定义处归本类——code-review 2026-09-12 先例):
# pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;"" = 未摇定/死亡残位)。
# 表现层升级批拍板:不显示技能名——类型级图标 + 一次性动作动画(_play_intent_motion)。
# 信号/入参契约:set_data 每结算行刷新、set_pending 每轮开轮刷新(开轮摇号时点)。
func set_pending(ptype: String, pid: String) -> void:
if _intent_badge == null or _dead:
return
if ptype == "" or ptype == "basic":
_intent_badge.visible = false # 普通攻击零表示(完全不可预测——§2.4)
return
var glyph := "⚔" # 攻击系(damage)
var col: Color = UiKit.FLAG_RED
if ptype == "buff":
glyph = "✦" # 增益系
col = UiKit.HERO_BLUE
elif ptype == "debuff":
glyph = "☠" # 妨害系(新效应词汇批 2026-09-19)
col = UiKit.DEBUFF_PURPLE
if pid in CHARGE_SKILL_IDS:
col = UiKit.GOLD_DEEP # 重击类蓄力档(2026-09-18 用户拍板独立蓄力提示)
_intent_badge.text = glyph
_intent_badge.add_theme_color_override("font_color", col)
_intent_badge.visible = true
_play_intent_motion()
# 三档动作动画占位:摇定时一次性短动画(意图图标弹入 + 立绘前倾回正),随后静帧保持——
# 挖掘期唯一在动元素 = 可挖域微光(art-bible §2.1);终稿动画密度归战斗表现样张批
func _play_intent_motion() -> void:
_intent_badge.pivot_offset = _intent_badge.size * 0.5
_portrait.pivot_offset = _portrait.size * 0.5
var tw := create_tween()
tw.tween_property(_intent_badge, "scale", Vector2(1.6, 1.6), UiKit.motion(0.10))
tw.tween_property(_intent_badge, "scale", Vector2.ONE, UiKit.motion(0.16))
tw.parallel().tween_property(_portrait, "rotation_degrees", 4.0, UiKit.motion(0.10))
tw.tween_property(_portrait, "rotation_degrees", 0.0, UiKit.motion(0.14))
# 资产挂点(战斗表现样张批):tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右——2026-09-26 用户点名英雄立绘水平向右)。
# 盒高 96px = 48px 立绘源整数 2×(圣经 §8.2 禁非整数缩放;offset_top 44 = 血条+状态行之下净空)——
# 源换代时随源重算盒高。
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
if _portrait_tex == null:
_portrait_tex = TextureRect.new()
_portrait_tex.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_portrait_tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_portrait_tex.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊(棋盘小人贴图批 2026-09-25)
_portrait_tex.set_anchors_preset(Control.PRESET_FULL_RECT)
_portrait_tex.offset_top = 44.0
_portrait_tex.offset_bottom = -36.0 # 盒高 96 = 48 源 ×2 整数档(原 −26 盒高 106 系非整数缩放)
_portrait_tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_portrait_tex)
_portrait_tex.texture = tex
_portrait_tex.flip_h = flip
_portrait_tex.visible = tex != null
_portrait.visible = tex == null
func mark_dead() -> void:
if _dead:
return
_dead = true
modulate = Color(0.62, 0.62, 0.62, 0.9)
var tw := create_tween()
tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12))
tw.tween_property(self, "rotation_degrees", -1.5, UiKit.motion(0.1))
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
_refresh_portrait_glyph()
if _intent_badge != null:
_intent_badge.visible = false
_set_hp(0)
func is_dead() -> bool:
return _dead
# 受击闪白(结算演出占位——§2.2 受击反馈)
func flash_hit() -> void:
var tw := create_tween()
tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2))
func _revive() -> void:
_dead = false
modulate = Color(1, 1, 1)
rotation_degrees = 0.0
_refresh_portrait_glyph()
_update_name_line()
func _update_name_line() -> void:
if _name_line == null:
return
_name_label.text = _display_name
var col: Color = UiKit.STONE # 轻甲 = 灰石(中性轻薄)
match _armor:
"ward":
col = UiKit.HERO_BLUE # 魔法护甲 = 小人蓝(避开 Boss 专属紫赤/妨害紫)
"heavy":
col = UiKit.GOLD_DEEP # 重甲 = 深金(厚重达意金)
_armor_icon.text = String(ARMOR_ICONS.get(_armor, "◇"))
_armor_icon.add_theme_color_override("font_color", col)
func _refresh_portrait_glyph() -> void:
if _kind == "hero":
_portrait.text = "人"
return
if _dead:
_portrait.text = "✕"
return
match _enemy_id:
"goblin":
_portrait.text = "▂" # 小体量墩形
"bugbear":
_portrait.text = "▆" # 垂直延伸体量
"ogre_chief":
_portrait.text = "█" # 最大体量
_:
_portrait.text = "敌"
@@ -1 +0,0 @@
uid://gfvj2sxds3ph
+16 -11
View File
@@ -110,21 +110,26 @@ func _ready() -> void:
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)]) problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
# E 区道具面板已随可选类删除下线(格面资源批 2026-09-15)——四区构建由其余断言覆盖 # E 区道具面板已随可选类删除下线(格面资源批 2026-09-15)——四区构建由其余断言覆盖
# ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4;2026-09-20 全站桩拍板)---- # ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4;2026-09-20 全站桩拍板)----
var standee = main.stage.enemy_box.get_child(0) # B 区站桩 Node2D 迁移批 T3(2026-09-26)断言等价改写(台账 171 改判记录):enemy_box 子件
if standee != null and (standee._name_label.text == "" or standee._armor_icon.text == ""): # 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体
problems.append("敌立绘名字行缺失(名字或护甲类型图标占位为空——2026-09-26 终裁:AC 不展示、类型图标化)") # 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持
if standee != null and standee._intent_badge == null: var standee = main.stage.enemy_body_at(0)
problems.append("敌立绘意图指示缺失") if standee == null:
elif standee != null: problems.append("敌立绘本体缺失(enemy_body_at(0) 无世界站桩)")
else:
var name_line: Dictionary = standee.name_line_probe()
if String(name_line.name) == "" or String(name_line.armor_icon) == "":
problems.append("敌立绘名字行缺失(名字或护甲类型图标占位为空——2026-09-26 终裁:AC 不展示、类型图标化)")
# 三档动作动画占位契约(表现层升级批拍板:不显示技能名——图标档位 + 一次性动画) # 三档动作动画占位契约(表现层升级批拍板:不显示技能名——图标档位 + 一次性动画)
standee.set_pending("damage", "") standee.set_pending("damage", "")
if not standee._intent_badge.visible or standee._intent_badge.text != "⚔": var intent_on: Dictionary = standee.intent_probe()
problems.append("攻击系意图未呈现(visible=%s text=%s)" % [str(standee._intent_badge.visible), standee._intent_badge.text]) if not bool(intent_on.visible) or String(intent_on.text) != "⚔":
problems.append("攻击系意图未呈现(visible=%s text=%s)" % [str(intent_on.visible), str(intent_on.text)])
standee.set_pending("basic", "") standee.set_pending("basic", "")
if standee._intent_badge.visible: if bool(standee.intent_probe().visible):
problems.append("普攻应零表示(意图指示应隐藏)") problems.append("普攻应零表示(意图指示应隐藏)")
if main.stage.hero_standee == null or main.stage.hero_standee._hp_bar == null: if main.stage.hero_standee == null or not main.stage.hero_standee.has_world_hp_bar:
problems.append("B 区主角站桩缺失") problems.append("B 区主角站桩缺失(世界血条面缺失)")
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)---- # ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源) var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1, var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,
@@ -0,0 +1,81 @@
# Alpha 验收清单 · 战斗屏(初稿)
> 状态:**初稿成文于循环 1(2026-09-26,战斗舞台 Node2D 迁移批 T0)**。本文件 = Alpha 验收批(三段式程序第三段)的销项底单;循环 1 只成文不销项,随批更新状态列。
> 来源拼装(六源):① `design/ux/battle.md` AC 与 §13 上游义务对照表 ② `design/gdd/systems/表现与支撑/HUD框架.md` 验收标准与基线表 ③ `design/art/art-bible.md` 美术判据(§1.4/§2/§4.5/§8)④ `design/gdd/07-体验验证清单.md` §⑨ 真人试玩指引 ⑤ `design/gdd/06-待办清单.md` 挂 Alpha 条目 ⑥ 工程回归面(冒烟/五探针/文档三闸/MC)。
> 验法分档:**[headless]** = 场景探针或脚本可自动判定;**[真机渲染]** = 开窗口跑游戏读日志/截图测量(draw calls、灰度、帧率类,agent 可窗口化自动测大半);**[人]** = 用户目检或真人试玩,agent 只备料。
> 数值带(ap6-ac 阶段二压力验收)**不在本清单**——归调参批(2026-09-26 裁决⑦)。
## A. 布局与性能(源② + 源①)
| # | 判据 | 验法 | 状态 |
|---|---|---|---|
| A1 | 三区无遮挡、无重叠、无内容裁切(棋盘全部格子与数字完整可见);分区尺寸与 battle.md 布局表一致 | [headless] ui_layout_probe + [人] 双分辨率目检 | 待验收 |
| A2 | 1280×800 与 1920×1080 两档均成立(1920 档换算值表现层升级批成文后此项才可销) | [人] 全屏目检两档 | 待验收(1920 档参数未成文) |
| A3 | 同屏 draw calls < 200 | [真机渲染] RenderingServer 调试读数(迁移批 T7 附首读数) | 待测(基线读数见循环 1 T7 交付) |
| A4 | 进入战斗屏 500ms 内首格可点 | [headless] ui_click_probe 首格可点计时 AC | 待验收 |
| A5 | B 区战斗舞台 = Node2D 舞台半区 + Control 文本半区(ADR-0006 分工表);站桩载体 Button/GridContainer/HBox 计数 = 0 | [headless] 场景树 dump 取证探针(迁移批 T5① 已实证 PASS——探针按台账 152 先例跑完即删,复跑件归呈现层测试分级批周期化时重建) | **已实证(循环 1 T5,2026-09-26)——周期化复跑件待建** |
## B. 可读性与色彩(源②基线表 + 源③)
| # | 判据 | 验法 | 状态 |
|---|---|---|---|
| B1 | 全部数字对格底对比 ≥4.5:1 | [真机渲染] 截图采样明度计算 | 待验收 |
| B2 | 已开/未开格态灰度截图下可逐格分类;变异格纹带灰度可辨 | [真机渲染] 灰度转换截图 + 逐格分类脚本 | 待验收 |
| B3 | 无信息仅靠色相承载(色弱安全,art-bible §4.5 清单) | [真机渲染] 灰度截图复核 + [人] 抽检 | 待验收 |
| B4 | 字号下限:棋盘数字 ≥18px、明细正文 ≥14px、tooltip ≥13px(1280×800 基座) | [headless] 布局探针读值 | 待验收 |
| B5 | 中文字体随包内置并设默认,运行时中文渲染无缺字 | [headless] 注入字体非空 + has_char + 宽度 >0(C6 判据)+ [人] 目检光栅 | 待验收(当前=引擎回退字体,CJK 随包未落) |
| B6 | 按钮文案 ≤4 汉字宽度;译西文不破版 | [人] 目检 | 待验收(本地化专项前置) |
## C. 呈现义务(源① AC + 源② 验收标准)
| # | 判据 | 验法 | 状态 |
|---|---|---|---|
| C1 | 悬停光标三语义(移动/挖/旗)随目标切换并高亮目标格;悬停已开格画合法路径;可挖域常驻呼吸微光 | [headless] ui_input_probe case①②③ | 待验收 |
| C2 | 一帧内意图整批到达,停手点击终结当帧收集;每次拒绝可感知(棋盘/资源/相位三面,前两类可区分;双通道反馈) | [headless] ui_probe 拒绝面断言 + [人] 目检 | 待验收 |
| C3 | 敌人站桩呈现:意图占位(普攻零表示/特殊技类型级提示,不预告名与数值)且挖掘期全程可见、映射跨局恒定、顺位可读、血量可读、敌方共享层数各立绘同值 | [headless] ui_probe 站桩断言 + [人] 目检 | 待验收 |
| C4 | 死亡灰化加碎裂退场(禁瞬消)且就地更新不丢灰化态 | [headless] 探针灰化态断言 + [人] 目检节奏 | 待验收 |
| C5 | 受击闪白、血条直角递减、结算弹道落点贴站桩中心 | [headless] 等价探针 + [人] 目检 | 待验收 |
| C6 | 状态槽:图标+层数+剩余结算次数,悬停见来源+语义+剩余;空态整排隐藏;战斗结束全清有微文案 | [headless] ui_probe BUG-0001 回归段 + ui_click_probe tooltip 段 + [人] 目检 | 待验收 |
| C7 | 结算逐行实时滚出带来源标注;未生效类行聚合计数带形状冗余;零值行不吞;高光三档不打空兑现 | [headless] ui_probe 结算段 + [人] 目检 | 待验收 |
| C8 | 终局:胜利金蜡封、失败暮蓝氛围层(明度不变);两反馈不合并、时序不倒挂 | [headless] 探针 + [人] 目检 | 待验收 |
| C9 | 任一模态打开时遮挡物隐藏或输入锁定、关闭逐层恢复;tooltip/明细行固定槽高不推挤布局 | [headless] ui_click_probe 回看段 | 待验收 |
| C10 | 终局呈现只消费流水与快照(零生命周期回读);重放渲染与实时渲染一致;载荷逐字段断言 | [headless] MC/冒烟既有断言 | 待验收 |
## D. 可访问性与输入(源②基线表)
| # | 判据 | 验法 | 状态 |
|---|---|---|---|
| D1 | 无每秒超三次闪烁 | [真机渲染] 帧序列采样 | 待验收 |
| D2 | 减动效开关:时长减半或跳终态(结算兑现可加速不可跳过) | [headless] 开关态探针 + [人] 目检 | 待验收(开关本身未立项,先挂起) |
| D3 | 全程纯鼠标可玩 | [headless] ui_click_probe 全链 | 待验收 |
| D4 | 切片临时名零残留;呈现层零随机调用 | [headless] static_scan + grep | 待验收 |
## E. 真人试玩面(源④ 07§⑨ + 源① OQ)
| # | 判据 | 验法 | 状态 |
|---|---|---|---|
| E1 | 照 07§⑨ 指引真人试玩 3–5 局(零天赋基线档),局中体感对号 ①–⑧ 观测项,局后一句话记录 | [人] 用户一次真人试玩(裁决⑦) | 待验收 |
| E2 | 样张 v1 终审(视觉语言冻结为换皮批输入) | [人] 用户看图 | 循环 1 异步触点,不阻塞 |
| E3 | 表现遗留收口复核(循环 2 交付后):敌人立绘/接触带微光/封印特效/原型件退役/C 区美化 | [人] 目检 + [headless] 回归闸 | 待循环 2 |
## F. 工程回归面(源⑥,每批常态闸)
| # | 判据 | 验法 | 状态 |
|---|---|---|---|
| F1 | run_smoke 全绿(当轮基线零失败) | [headless] `python tools/run_smoke.py` | 循环 1 基线 557/0 绿,迁移批 T0–T5 每轮复跑全绿(2026-09-26) |
| F2 | 五探针全绿:ui_probe / ui_click_probe(SEED=7) / ui_layout_probe / ui_tune_probe / ui_input_probe 十一断言 | [headless] 场景探针 | 循环 1 基线全绿;迁移批每轮复跑全绿(ui_probe 敌站桩断言已随批等价改写为本体探针出口——台账 171) |
| F3 | 文档三闸对基线零新增 | [headless] doc_link_audit / doc_term_audit / gdd_lint 对基线快照 diff | 循环 1 基线已存档 |
| F4 | MC 12 局 RUNSUM 与基线逐位一致(核心层零漂移证据) | [headless] adventure_monte_carlo | 循环 1 基线采集于 T0(24 行 RUNSUM 存档),T7 对拍 |
## G. 源⑤ 06 挂 Alpha 条目涉战斗屏者(销项时逐条对表)
- Alpha:全怪物技能池数值验证与合理性设计(归难度曲线与 Boss 主责,非本清单销项——录以备查)。
- 雷格与安全格的资源分布差异化(Alpha 批,结构修订)——非战斗屏呈现面,录以备查。
- 表现层测试分级与 UI 驱动夹具周期化(06 台账 160-164 批登记)——夹具周期化落地时本清单 A5 取证件转正式回归件。
## 修订记录
| 日期 | 改动 |
|---|---|
| 2026-09-26 | 初稿成文(循环 1 T0,六源拼装;销项归 Alpha 验收批) |
| 2026-09-26 | T5/T6 状态列更新(A5 已实证/ F1、F2 迁移批全绿注记/F4 待 T7 对拍;B 区站桩断言改判注记) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

@@ -0,0 +1,20 @@
# 样张 v1 机检报告(2026-09-26)
生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。
| 断言 | 判据 | 结果 | 明细 |
|---|---|---|---|
| F1 | 方案甲 8 邻拼装(词表查表,禁整块) | PASS | 错位 0 处;用件 15 种 ['center', 'center_crack', 'center_speckle', 'end_n', 'end_s', 'end_w', 'hrun', 'in_nw', 'in_sw', 'n', 'nw', 's', 'sw', 'vrun', 'w'] |
| F2 | 旗仅出现于未开格 | PASS | 旗位 (1, 2) 格态=W |
| F3 | 小人脚锚=自底边 25% 线(两态) | PASS | 脚 y=[213.75, 258.75] 期望=[213.75, 258.75] |
| F4 | 数字 0 格无字形(0=纯安全格裸地面) | PASS | 0 态格 40 个(示例 (4,4));数字表含 0 键=False |
| F5a | 接触带微光⊆英雄格周界(空间侧,不进墙格) | PASS | 微光 bbox=(86, 176, 119, 230) 英雄格=(90, 180, 135, 225) |
| F5b | 类型微光禁整格染色(中心 50% 区零染色像素) | PASS | 五格中心区全净 |
| A1a | 色板对表圣经 §4.1 冻结值 | PASS | {'INK': '#33261B', '青墨': '#132E36', '赭墨': '#3C250C', '锈墨': '#4A1E14', 'PLATE': '#F5EAD3', '旗朱': '#C63D22', '危险红': '#A8321F', '奖励金': '#F0A828'} |
| A1b | 禁入带 HSV300°-345° 零命中 | PASS | 命中 0 像素(抽样) |
| A2 | 拼缝周界闭合(墙侧 2px 墨线 <70) | PASS | 全部墙地边界墨线在位 |
| A3 | 墨×牌 WCAG≥4.5:1 + 图底分离 | PASS | [('青墨', 12.0), ('赭墨', 12.0), ('锈墨', 11.8)];墙顶 113 vs 地面 51 |
| A5 | 画布四缘 2px 净空(零溢出) | PASS | 缘带内容像素 0 |
| A4 | 混排相融(游戏在库资产+UiKit 语义色) | PASS | 小人源 32×32(裁透明留白后 (19, 20))/立绘 48×48/状态图标 32×32 原件;INK/旗朱/奖励金 == ui_kit.gd |
结论:**全绿**(12/12 PASS)。气质终审 = 用户(异步,不阻塞 T2-T7)。
@@ -0,0 +1,74 @@
# 画法参数四源对照表(样张 v1 · T1 对表先行,2026-09-26)
> 任务书:`.zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md` T1①。四源 = A battle.md 布局线框 / B 机制代码(格态·旗语义·脚锚)/ C 引擎常量 / D art-bible 画法章节。**一处对不上 = L3**。本表逐参数登记取值与出处;样张 v1 脚本(tools/render_sample_sheet.py)只准消费本表参数。
## 1. 屏面布局(源 A:battle.md §Layout + §2 现状)
| 参数 | 取值 | 出处 |
|---|---|---|
| 基座分辨率 | 1280×800 | art-bible §8.1 两档定案 |
| B 区 | 全宽 × 252px:主角站桩(左)+ 兑现演出区(中)+ 敌方站桩 ×1–3(右);顶部中央轮次·阶段指示;底部结算明细带(挖掘期收起 24px) | battle.md §Layout / battle_stage_view.gd `_init`(custom_minimum_size 0×252) |
| C 区 | 左下 240px × 548px:攻击数据框(攻击值+修正来源角标+护甲 AC)+ 魔法道具槽(预留 4)+ 本关强化列表(预留) | battle.md §Layout(2026-09-26 信息归属批后口径)/ hero_info_panel_view |
| D 区 | 右侧余幅(~1036px):顶行 AP 点阵+RAP+弃图刷新+图例+停手;下为棋盘 | battle.md §Layout / ap_bar_view |
| 站桩盒 | 150×176:血条(10px)+状态行(16px)头顶悬浮;立绘盒 96px;底座底条(10px);敌名行(护甲图标+纯名字,12px);顺位徽标①②③ | standee_view.gd `_init` 几何 |
| B 区立绘 | 主角 = 48×48 朝右贴图(96px 盒 = 48×2 整数档);敌人 = 字形占位(▂地精/▆熊地精/█食人魔酋长——素材零张在库) | battle_stage_view.gd:38 / standee_view.gd `_refresh_portrait_glyph` / §2 现状「敌人立绘素材一张都不存在」 |
## 2. 格态与语义(源 B:机制代码)
| 参数 | 取值 | 出处 |
|---|---|---|
| 格态 | 未开(墙)/ 已开(地面)两态;雷 = 已开态巢穴纹更凶一档 | board_view.gd paint 分支 / art-bible §3.2 地砖条 |
| 数字 0 格 | **无字形**(`if d > 0: glyph = str(d)`——0 = 纯安全格裸地面) | board_view.gd:583-586(FATAL④ 事实源) |
| 旗语义 | 旗只插**未开格**(旗分支在 `else` 未开侧);旗 = 封印印记呈现名;旗面恒画右 | board_view.gd:589-590 / art-bible §3.2 旗条(FATAL② 事实源) |
| 悬停金框 | 预承诺光标态画在目标格(逐格指认=悬停金框) | art-bible §3.2 接触带条尾句 / board_view `_apply_hover_visuals` |
| 小人同格 | 小人与数字同格:数字色牌不废、蓝框不覆盖文本 | art-bible §1.4 小人两态条 |
## 3. 引擎常量(源 C:board_view.gd / standee_view.gd / ui_kit.gd)
| 参数 | 取值 | 出处 |
|---|---|---|
| 格边长(1280 基座) | 45px(10×10 板)/ 36px(12×12 板);样张 D 区按 45px 实比 | art-bible §8.1 / board_view clamp(26,46) |
| 小人脚锚 | **自底边往上 25% 线**(HERO_GROUND_FRAC=0.25;⚠ 纵向比例一律自底边口径) | board_view.gd HERO_GROUND_FRAC(FATAL③ 事实源) |
| 小人档 | 32×32 源 ×1.5 = 48px(台账 161 用户实机拍板档;45px×32 源非整数 NEAREST 已接受——台账 166 同级) | board_view.gd HERO_SPRITE_SCALE |
| 椭圆影 | 贴脚(中心=脚锚),32×12px @1×,黑 35% | board_view.gd HERO_SHADOW_PX / 台账 162 |
| 立绘缩放 | B 区 96px 盒 = 48 源 ×2 整数(C5) | standee_view.gd set_portrait_texture |
| 变异遮蔽 | hide_number 数字格显示 `?`(样张不含变异格——六类型纹带另批) | board_view.gd:584 |
| 游戏 UiKit 语义色 | INK #33261B / FLAG_RED #C63D22 / GOLD #F0A828 / RED_DEEP #963C30 / 数字=INK | ui_kit.gd 常量(A4 混排对表基准) |
## 4. 画法章节(源 D:art-bible)
| 参数 | 取值 | 出处 |
|---|---|---|
| 未开格 | **方案甲高墙瓦片 8 邻拼装**:16 唯一件+运行时 flip_h 镜像(禁旋转);词表=board-tile-vocab-plan-a;板外视为墙;center 变体 crack/speckle 60/20/20 哈希分配 | §3.2 高墙瓦片体系条 / design/research/board-tile-vocab-plan-a-2026-09-26.md(FATAL① 事实源) |
| 墨线纪律 | 墙/地分界 1px 深墨线贴格边(缘件自带,实测朝地边亮度 ~22-36 vs 本体 ~122);南缘立面窄暗条;北缘内侧 1px 提亮 | §3.2 墨线纪律 / v9.3 实测 |
| 图底分离 | 已开地面明度 < 未开墙顶(实测 Δ≈75,0-255 简易亮度) | §8.5 图底分离条 |
| 已开格 | 地面瓦片 5 变体哈希;产资源格 = 家族物件纹样(道具点剪影)角嵌砖缘+**砖心留白予数字色牌**;纯安全格 = 干净裸地面 | §3.2 结果整格表达 / §1.2 原则三 |
| 数字色牌 | #F5EAD3 素色直角小牌,恒定不随格底;数字墨三档:1-3 青墨 #132E36 / 4-5 赭墨 #3C250C / 6-8 锈墨 #4A1E14;对牌对比 ≥4.5:1(实测 11.8-12.3:1);字号下限 ≥18px | §4.1 / HUD框架 基线表 |
| 语义色 | 掩尘 #C9AF83 / 裸土 #B39A78 / 旗朱 #C63D22 / 危险红 #A8321F / 奖励金 #F0A828 | §4.1 冻结 |
| 类型角嵌 | 未开格四角/边框墨族单色纹样(九家族键 §4.6.3),砖心留白;形状主通道 | §3.2 类型角嵌条 |
| 类型微光 | 五组候选:通用增益 #D6C13A / 治疗 #5CB83A / 穿刺 #2E9E9E / 魔法 #7B5BD6 / 群伤 #FF2D55;底效泛光层不入遮挡栈;**禁整格染色**(v1 画法=内缘带状泛光,机检断言中心 50% 区零微光像素);不以明度增量承载信息 | §4.2 类型微光色带 / §1.4 验收 2(FATAL⑤ 事实源之一) |
| 接触带微光 | 英雄格周界**空间侧**:正交邻墙=整条边、斜向=共享角点+两侧各半条边;蓝族浅一档;光不进墙格 | §3.2 可挖域接触带微光条 / §2.1(FATAL⑤ 事实源之二) |
| 禁入带 | HSV 色相 300°–345° 全场禁入(微光群伤组 349° 刻意避开) | §4.2 紫赤条 |
| 换皮 | §4.3 四槽换色(tile_closed/tile_open/wall_ambient/variant_tint),信息层恒定;样张 ≥两套(dungeon+underdark) | §4.3 / §1.4 内容清单 |
| 小人占位 | 独立资产 32×32;样张 48px ≈ 0.75× 格边符合 §8.2 占位上限 | §8.2 画布基准 |
## 5. 候选值与偏差登记(非矛盾,样张批冻结项)
1. **小人蓝**:art-bible §4.1 = #2E86DE(候选,样张批冻结终稿);游戏 UiKit = #2E6E8E。两值并存系圣经明标「候选值」——样张以 #2E86DE 出图;冻结后游戏侧换色归循环 2,本批不改代码。
2. **接触带微光色值**:空间侧+蓝族已定稿(v2.8),具体色值样张批冻结——v1 取小人蓝族浅两档 #D6ECFF(近白蓝)出样。
3. **格底 L≥0.30 的适用面**:§4.1/§4.3 系换皮四槽**色槽参数域**(新图类型色板须守);dungeon 定稿瓦片(v9.3 用户验收入本体)为素材实态,本样张不再以 L≥0.30 复裁(防对已验收资产误报 FATAL),改以图底分离+可读性硬指标承载。
4. **巢穴纹/封印印记**:素材缺口,程序化占位画面内标注(§1.4 允许——「素材缺口见清单」v0 先例);正式纹样归绘画批。
## 6. 机检断言映射(v0 五画错 + 程序化四件 → 生成脚本自检)
| 断言 | 判据 |
|---|---|
| F1 方案甲拼装 | 每墙瓦片位 = 词表查表(四邻+对角)结果;禁整块(center 之外的件 ≥6 种实出);变体 60/20/20 |
| F2 旗∈未开格 | 旗位集 ⊆ 墙位集 |
| F3 脚锚自底 25% | 两小人态脚 y = 格底 − 0.25×45;影心=脚点 |
| F4 0 格无字形 | 数字 0 格 glyph 记录 = 空 |
| F5 微光带限 | 接触带微光像素 ⊆ 英雄格∩带几何(≤8px 距离);类型微光中心 50% 区零像素 |
| A1 色板+禁入带 | 冻结色逐字节对表 §4.1;微光五组对表 §4.2;全图像素扫 HSV 300°-345°(S>0.15,V>0.20)零命中 |
| A2 周界闭合 | 每段墙↔地边界墙侧 2px 带均亮度 <70(墨线在位);墙群外轮廓无缺口 |
| A3 明度对比 | 三档墨×色牌 WCAG ≥4.5:1;地面均亮 < 墙顶均亮 −40 |
| A4 混排相融 | 语义色 == 游戏 UiKit 常量(小人蓝候选偏差见 §5.1);小人/立绘/状态图标 = 游戏在库资产原件 |
@@ -0,0 +1,36 @@
# 战斗舞台 Node2D 迁移 · 信号与演出等价证明(循环 1 T4,2026-09-26)
> 计划书 T4 验收件:时序常量逐字对照旧实现 + 写路径逐行同构(Phase A 先例成文化)+ 调用点 grep 对比零 diff。
> 对比基线 = T0 提交 `3db7e87`(迁移前现行态);迁移后 = worktree battle-stage-node2d-20260926 T3 提交。
## 1. 调用点 grep 对比(零 diff 实证)
| 面 | 命令 | 结果 |
|---|---|---|
| main.gd → stage 调用点(9 处:set_effects_cleared_feedback ×2 / gray_all_enemies ×2 / on_step_row_fx / enemy_standee_center / hero_standee_center / set_settle_focus / set_round_text) | `grep -E "stage\.(hero_standee\|enemy_standee_center\|on_step_row_fx\|gray_all_enemies\|set_round_text\|set_settle_focus\|clear_enemies)"` 老新对拍 | **逐字节一致**(行号 309/310/439/441/475/495/503/768/770 全同) |
| hud_presenter.gd → hero_standee 投喂(4 处:set_hero ×2 / set_effects ×2) | 同法对拍 | **逐字节一致**(行号 53/54/79/80 全同——单投喂结构零变) |
| hud_presenter.gd / main.gd / core/** 文件级 | `git diff 3db7e87..HEAD --stat` | **零行变更**(投喂链零改动实证;核心层零接触) |
## 2. 时序常量逐字对照(旧 StandeeView → 新 StandeeBody)
| 演出 | 旧(Control + tween) | 新(Node2D + tween) | 判定 |
|---|---|---|---|
| 血条递减 | `motion(0.15)` tween bar value | `motion(0.15)` tween hp_value 属性 | 同 |
| 意图弹入 | scale 1.6→ONE,`motion(0.10)`/`motion(0.16)` | `_intent_scale` 1.6→1.0 同时长 | 同 |
| 立绘前倾 | rotation 4.0→0.0,`motion(0.10)`/`motion(0.14)` | `_glyph_rot` 同值同时长(旧仅转字形 Label——贴图态无动画,新等价保持) | 同 |
| 死亡摇摆 | modulate (0.62,0.62,0.62,0.9);rotation 2.5→−1.5→0.0,`motion(0.12)`/`(0.1)`/`(0.08)` | 逐字同 | 同 |
| 受击闪白 | modulate (1.6,1.4,1.2) `motion(0.08)` → 白/死灰 `motion(0.2)` | 逐字同(终色按 _dead 分支同构) | 同 |
| 复活复位 | modulate 白、rotation 0 | 同 | 同 |
| 清空反馈微文案 | 1.2s 停留 + `motion(0.8)` 淡出 | 同(载体挂壳层伴随件父层——T3 代拍板③) | 同 |
## 3. 写路径逐行同构(旧 battle_stage_view.gd → 新)
- `on_step_row_fx(row)`:type!=""damage" 早退 / target=="hero"|""早退 / `_card_by_name(target)` 空退 / `flash_hit()` + killed→`mark_dead()`——逐行同构;`_card_by_name` 内部实现由「子件 `_name_label.text` contains」改「本体 `display_name()` contains」(语义等价,函数内部实现细节=§9 白名单)。
- `set_enemies(cards_data)`:数量分叉(重建 vs 就地 set_data)结构逐行同构;重建侧由 StandeeView.new 改 StandeeBody.new+伴随件挂接(T3 代拍板①④)。
- `clear_enemies()` / `gray_all_enemies()` / `set_round_text()` / `set_settle_focus()`:逐行同构(遍历对象 Control→本体)。
- 弹道落点:`hero_standee_center()`/`enemy_standee_center()` 签名不变,实现 = 旧「本体 get_global_rect 中心」改 `world_to_shell(本体.position + box*0.5)`——**壳层口径保持(C4:结算演出 SettlementFxView 在 SubViewport 之外)**,返回值语义=站桩盒几何中心不变。
- `set_effects/set_effects_cleared_feedback/set_hero`(投喂面):本体转发伴随件(slot_row),调用方零改造。
## 4. 已登记的结构性差异(非漂移,改判记录见台账 170/171)
1. 站桩本体载体 Control→Node2D(本批目标本身);2. slot_row 伴随件挂壳层;3. enemy_box HBoxContainer→Control 伴随件容器;4. 血条呈现由引擎默认 ProgressBar 改世界直角矩形(色值近似平移——目检清单列「血条观感」项);5. ui_probe 敌站桩断言改本体探针出口(呈现口径逐字保持)。
+8
View File
@@ -454,3 +454,11 @@
164. **§8.2 改规则登记+测试分级登记(2026-09-26,本会话——用户两裁示;纯文档批零代码)**:①art-bible §8.2 缩放纪律走**改规则不豁免**(用户裁)——06 新条目登记修订方向:按资产类别分级(甲类文本/语义件=数字/图标/旗/tile 维持严格整数硬规则;乙类角色/单位精灵允许非整数+NEAREST、实机目检判据)——豁免蔓延断根、规则与实践重归一致;**修订窗口=art-bible 并行在飞批落籍后**(本批未碰 art-bible.md);连带发现:§8.2 内 1920 档 1.5× 全局缩放待决项与分级同构,样张批一并裁。②**呈现层测试分级**入 06(用户裁「重测试不进每次变更、统一周期」)——每次变更必测(轻闸 headless)=run_smoke+ui_input_probe 十一断言(触 presentation 加 click/layout 探针);周期内测(重闸)=B 档视口截图像素断言/C 档黄金图(硬前置=样张批视觉冻结)/真人目检清单/MC 大扫描;2026-09-20「UI 驱动夹具」欠账正式立项销号。验证:06 编辑零冲突(文件干净);台账 163 断言批已并入现行探针。 164. **§8.2 改规则登记+测试分级登记(2026-09-26,本会话——用户两裁示;纯文档批零代码)**:①art-bible §8.2 缩放纪律走**改规则不豁免**(用户裁)——06 新条目登记修订方向:按资产类别分级(甲类文本/语义件=数字/图标/旗/tile 维持严格整数硬规则;乙类角色/单位精灵允许非整数+NEAREST、实机目检判据)——豁免蔓延断根、规则与实践重归一致;**修订窗口=art-bible 并行在飞批落籍后**(本批未碰 art-bible.md);连带发现:§8.2 内 1920 档 1.5× 全局缩放待决项与分级同构,样张批一并裁。②**呈现层测试分级**入 06(用户裁「重测试不进每次变更、统一周期」)——每次变更必测(轻闸 headless)=run_smoke+ui_input_probe 十一断言(触 presentation 加 click/layout 探针);周期内测(重闸)=B 档视口截图像素断言/C 档黄金图(硬前置=样张批视觉冻结)/真人目检清单/MC 大扫描;2026-09-20「UI 驱动夹具」欠账正式立项销号。验证:06 编辑零冲突(文件干净);台账 163 断言批已并入现行探针。
164. **高墙瓦片 v9.3 定稿交付+备选案清场批(2026-09-26,本会话——用户验收 v9.3 后指令「直接移入游戏本体+备选案打包挪走」;零代码、素材不入 git,worktree 不适用)**:**定稿交付**=21 枚入 `game/assets/art/board/`(16 唯一墙瓦片〔center/center_crack/center_speckle/n/s/w/nw/sw/hrun/vrun/end_n/end_s/end_w/lone/in_nw/in_sw〕+5 地面瓦片变体,`terrain_*_dungeon.png`;东/NE/SE/端头E/凹角NE/SE=运行时 flip_h 镜像不出文件,旧 beast-cave 砖零触碰)。**终版路线**=材质归模型(用户 Gemini 生图①岩石/④泥土→周期对齐无缝裁剪+BOX 降采样 32×32+环绕混合+量化 12 色)+结构归程序(v9 墨线纪律:全开缘 1px 深墨线直线零抖动/北缘内侧 1px 就地提亮/南缘立面窄暗条 11px/凹角角点+立面侧 1px 齐平收头——v9.1 悬空/v9.2 突出/v9.3 齐平三轮定位,前后对比+逐行像素审计存 `_work/corner_cmp_v91_v93_5x.png`)。**术语共识(用户立)**:未开格=墙瓦片,已开格=地面瓦片。**备选案清场**=v1–v9.2 全迭代+被否素材(含 v7 程序砌石/v3 生图 5×5 表)打包 `备选案-terrain-walls-v1-v9_2-2026-09-26.zip`(383 项 7.5MB,用户挪出库),`_work/` 仅留生图①④源图+风格参考+终版证据+定稿件。**复盘机制落记忆**(pixel-structure-verify-discipline):先抄后造/验收口径写像素级视觉判据/±1px 一律坐标推导/修复默认附前后对比。**遗留**:underdark/deep_underdark 换色(AI 纹理色调映射)+实机查表合成器(BoardWorld 接线,含镜像翻转)归后续批;任务书验收单逐条结果以本批 v9.3 终测为准(图底分离 67.6/无死黑/墨线周界闭合/覆盖层可读)。 164. **高墙瓦片 v9.3 定稿交付+备选案清场批(2026-09-26,本会话——用户验收 v9.3 后指令「直接移入游戏本体+备选案打包挪走」;零代码、素材不入 git,worktree 不适用)**:**定稿交付**=21 枚入 `game/assets/art/board/`(16 唯一墙瓦片〔center/center_crack/center_speckle/n/s/w/nw/sw/hrun/vrun/end_n/end_s/end_w/lone/in_nw/in_sw〕+5 地面瓦片变体,`terrain_*_dungeon.png`;东/NE/SE/端头E/凹角NE/SE=运行时 flip_h 镜像不出文件,旧 beast-cave 砖零触碰)。**终版路线**=材质归模型(用户 Gemini 生图①岩石/④泥土→周期对齐无缝裁剪+BOX 降采样 32×32+环绕混合+量化 12 色)+结构归程序(v9 墨线纪律:全开缘 1px 深墨线直线零抖动/北缘内侧 1px 就地提亮/南缘立面窄暗条 11px/凹角角点+立面侧 1px 齐平收头——v9.1 悬空/v9.2 突出/v9.3 齐平三轮定位,前后对比+逐行像素审计存 `_work/corner_cmp_v91_v93_5x.png`)。**术语共识(用户立)**:未开格=墙瓦片,已开格=地面瓦片。**备选案清场**=v1–v9.2 全迭代+被否素材(含 v7 程序砌石/v3 生图 5×5 表)打包 `备选案-terrain-walls-v1-v9_2-2026-09-26.zip`(383 项 7.5MB,用户挪出库),`_work/` 仅留生图①④源图+风格参考+终版证据+定稿件。**复盘机制落记忆**(pixel-structure-verify-discipline):先抄后造/验收口径写像素级视觉判据/±1px 一律坐标推导/修复默认附前后对比。**遗留**:underdark/deep_underdark 换色(AI 纹理色调映射)+实机查表合成器(BoardWorld 接线,含镜像翻转)归后续批;任务书验收单逐条结果以本批 v9.3 终测为准(图底分离 67.6/无死黑/墨线周界闭合/覆盖层可读)。
168. **战斗舞台 Node2D 循环 1 · T0 开工批(2026-09-26,本会话——计划书 .zcode/plans/battle-stage-node2d-loop-plan-2026-09-26.md 已批准从 T0 自治开跑;worktree battle-stage-node2d-20260926)**:worktree 自 master 70d2320 建+editor --quit 刷缓存 0 error;git status 归属核对=主区脏文件(.gitignore/.zcode config/ui-reuse 指引/playnote/project.godot/loop-dashboard/active.md 165-167 未提交并行登记+untracked 美术文档组)均非本批只标旗不碰。**基线复读全绿记档**——冒烟 557/0+五探针 PASS(ui_probe cells=144 board=12x12/ui_click 问题=0/ui_tune 全链/ui_layout dump:stage 1280×252·board_view 1036×965·info_panel 240×1028/ui_input 十一断言)+static_scan exit0+文档三闸基线存档(死链 7/退役词命中/gdd_lint 8 文件 8 不通过 69 项级)+MC 12 局 both(max 路由) 24 行 RUNSUM sha256=de8225b3 存 /tmp 供 T7 逐位对拍。**§2 现状事实逐条 grep 复核零漂移**——battle_stage_view(PanelContainer/252px/五成员/setter 面)+standee_view(Control/行为面 17 方法/slot_row compact/字形占位)+hud_presenter 53-54/79-80 单投喂+main 309/441/503+ui_assembler 23+ui_probe 95-129/ui_click 223 穿透面+ui_input_probe 零 B 区耦合(grep stage/standee/enemy=0 命中)+TEX_HERO_PORTRAIT 素材 worktree 缺位=C7 回退字形路径预期。**Alpha 清单初稿落盘** production/alpha-acceptance-battle-screen.md——六源拼装(battle.md AC+§13 义务表/HUD框架 验收标准+基线表/art-bible §8 判据/07§⑨ 试玩指引/06 挂 Alpha 条目备查/工程回归面),七节 30 条=判据+验法三档(headless/真机渲染/人)+状态列,销项归 Alpha 验收批。轻闸全绿,进入 T1。
169. **样张 v1 制作与机检批(2026-09-26,本会话——循环 1 T1;worktree battle-stage-node2d-20260926 批次本提交)**:①**四源对照表先行** production/mockups/画法参数四源对照表-2026-09-26.md(battle.md 布局/机制代码〔0 格无字形 board_view:586·旗只插未开 :589·脚锚自底 25%〕/引擎常量〔45px·1.5× 档·96px 盒〕/art-bible §1.2-§4.6 画法)——候选值偏差两笔登记(小人蓝 #2E86DE 候选 vs UiKit #2E6E8E=样张批冻结项非矛盾;格底 L≥0.30 系换皮槽参数域不复裁已验收 dungeon 瓦片)。②**脚本重写** tools/render_sample_sheet.py v1:方案甲词表查表拼装(16 情形+对角回落 nw>ne>sw>se+flip 镜像+变体 60/20/20 哈希)+三区全内容 1280×800 实比 mockup+S2 数字档 0.5× 抽检+S3 underdark 四槽换色+S4 图例;素材两级回退(worktree→主检出,素材不入 git)。③**机检 12 断言生成即验全绿**——F1 词表拼装零错位 15 种用件/F2 旗∈未开/F3 脚锚自底 25%(两态)〔预筛抓出素材底部 7 行全透明→裁除留白后锚定,视觉脚=锚线;**游戏内 BoardWorld.ensure_hero 同病(视觉脚浮锚线 ~10px)=呈现缺陷登记移交循环 2/接线批**〕/F4 0 格无字形/F5a 接触带微光⊆英雄格/F5b 类型微光禁整格染色(中心 50% 区零像素——角嵌臂长两轮收紧 L9→L5 留白砖心)/A1 色板对表+禁入带 300-345° 零命中(腐蚀两轮滤 AA 混色)/A2 周界闭合(墙侧 2px 带墨线存在性 min<55——45px 非整数缩放下均值判误报改最暗值判)/A3 墨×牌 11.8-12.0:1+图底分离 113 vs 51/A4 混排在库原件/A5 画布四缘净空(溢出永久断言)。④**visual-judge 预筛三轮**(≤3 预算内):R1 四 FATAL→triage=站桩横向重叠/小人脚浮(素材留白)/同格态色牌被小人盖(改 §1.2 栈序数字>小人)/缘件裸拼误报(1px 墨线逐像素 zoom 自证 end_n 三向墨线俱在+15 种用件机检反证,判官 3-6px 采样带均值漏 1px 线)+地精无徽标误报(普攻零表示=设计);R2 两 FATAL=画布右缘溢出×2(说明行+S4 末项)→画布 1560+A5 断言化;R3 窄域复核 pass。⑤**交付**=样张 v1+机检报告+对照表三件已拷主区 production/mockups/(评审产物不入 git 同 v0 先例),**用户异步终审**(过=冻结为循环 2 输入;不过=对表改参重出);终审不阻塞 T2-T7。轻闸全绿(当轮基线=T0 记档),进入 T2。
170. **B 区战斗舞台壳改造批(Phase B T2,2026-09-26,本会话——循环 1;worktree battle-stage-node2d-20260926)**:BattleStageView 根壳 PanelContainer→**Control**(类名/252px/树内位置不变;面板样式=Panel 子面+原 stylebox 等价平移,子面几何沿原内容边距 Rect2(6,4,6,4));内部拆两半区——**Node2D 舞台半区**=SubViewportContainer(stretch=true 1:1 禁缩放系数)+SubViewport(transparent_bg)+**StageWorld**(新件 presentation/views/stage_world.gd,无 class_name 经 preload 接线,BoardWorld 同构:world_font 字体注入槽位+font_probe() C6 判据出口〔注入非空+has_char+宽度>0〕;T2 期世界空置待 T3 站桩迁入)+**Control 文本半区**=round_label/stage_center/log_view 三件原样保留(C1 永久 Control),hero_standee/enemy_box 本期仍 Control 原位。新挂点=set_world_font(f)(Phase A BoardView 同款,无调用方纯挂点)+world_to_shell(p) 坐标换算单点(C4——世界=局部排布/壳层=全局含容器偏移)。**C6 判据实证**(临时脚本跑完即删):未注入=回退字体 has_char=false(引擎默认无 CJK=文档既有口径);注入 fusion-pixel ja TTF(内存 load_dynamic_font 零素材入工程——C7)后 has_char=true 宽度 70>0 PASS。**验收**=轻闸全绿:editor --quit 0 error+冒烟 557/0(=基线)+五探针全绿+ui_layout_probe 外框逐位零漂移(stage pos=(0,0) size=(1280,252) min=(0,252)=T0 基线同值;B 区内部树结构变化记录:PanelContainer 自动布局→Control 锚点+VBox 同边距)+static_scan exit0+文档三闸零漂移。进入 T3。
171. **站桩本体 Node2D 化批(Phase B T3,2026-09-26,本会话——循环 1 最重卡;worktree battle-stage-node2d-20260926)**:站桩本体自 Control(旧 StandeeView **退役删除**)平移为 Node2D 世界精灵 **standee_body.gd**(新件,无 class_name 经 preload)。**行为面 17 方法契约逐项保持(C3)**——set_data/set_hero/_set_hp/set_idle/set_effects/set_effects_cleared_feedback/set_order/set_pending/_play_intent_motion/set_portrait_texture(tex,flip)/mark_dead/is_dead/flash_hit/_revive/_update_name_line/_refresh_portrait_glyph+is_dead,调用点 hud_presenter:53-54/79-80、main:309/441/475/495/503、main:768/770 零改排。**等价平移清单(C9)**——立绘=set_portrait_texture 惰性 Sprite2D(NEAREST+flip_h+整数 2×=48 源盒 96px)缺位回退字形(人/▂/▆/█/✕/敌 draw_string);底座=UiKit.panel_flat(STONE 系) StyleBox.draw 直绘;血条=直角矩形 hp_value 属性 tween 0.15 递减(引擎默认主题深底/蓝填充近似平移——色值列目检清单);顺位徽标 ①②③ GOLD_DEEP;意图徽标=draw_set_transform 弹入 scale 1.6→1+字形前倾 4°→0(旧只转字形 Label——贴图态无动画等价保持);名字行=护甲图标+纯名 draw_string 居中(AC 不展示 2026-09-26 终裁);受击闪白 modulate 1.6/1.4/1.2→白/死灰;死亡=灰化+rotation 2.5/-1.5/0 摇摆塌陷+意图退位(禁瞬消)+**状态行随 mark_dead 同拍隐藏(代拍板②待追认)**;复活=全态重置。**批内代拍板默认(待追认)**——①slot_row+悬停 tooltip 保留 Control 载体挂壳层(_companion_layer 顶层,敌伴随件宿主=**enemy_box 语义改写「站桩壳层伴随件容器」HBoxContainer→Control**,手动定位随世界同步,探针 :97 计数面不变);②定位同步=world_to_shell 单点(T2 建挂),_sync_layout 挂点=ready/NOTIFICATION_RESIZED/_row.resized(明细带展收改行高随动)/set_enemies 重建,站桩盒高=行高(旧 HBox FILL 拉伸等价)、敌右对齐 +8 间距等价;③hero_standee/enemy 站桩成员类型 StandeeView→Node2D(StandeeBody),**hero_standee_center/enemy_standee_center 改 world_to_shell(本体中心)=壳层口径(C4)**。ui_probe 敌站桩断言等价改写(台账 170 前例):穿透明(enemy_box 子件 _name_label/_armor_icon/_intent_badge/_hp_bar)→本体出口 enemy_body_at(i)+name_line_probe()/intent_probe()/has_world_hp_bar——呈现口径逐字保持。**踩坑记录**——standee_body 漏声明 _portrait_tex 成员→编译连坐 main.gd 假性两错(w/h 推断失败),editor --quit 前置闸暴露后修复。**验证**——editor --quit 0 error+冒烟 557/0(=基线零增删)+五探针全绿(ui_probe/ui_click 问题=0/ui_tune/ui_input 十一断言/layout 外框零漂移 stage 1280×252 逐位=T0 基线)+static_scan exit0+文档三闸零漂移。进入 T4。
172. **信号与演出等价证明批(Phase B T4,2026-09-26,本会话——循环 1;worktree battle-stage-node2d-20260926,零代码变更纯取证)**:等价证明落盘 `production/qa/battle-stage-node2d-等价证明-2026-09-26.md`——①**调用点 grep 对比零 diff**:main.gd→stage 九处调用(309/310/439/441/475/495/503/768/770)与 hud_presenter→hero_standee 四处投喂(53/54/79/80)逐字节一致;hud_presenter/main/core/** 文件级 git diff 基线=零行变更(投喂链零改动实证+核心层零接触)。②**时序常量逐字对照**——血条递减 0.15/意图弹入 0.10+0.16/前倾 0.10+0.14/死亡摇摆 0.12+0.1+0.08 与 modulate (0.62,0.62,0.62,0.9)/受击闪白 0.08+0.2 与 (1.6,1.4,1.2)/复活复位/清空反馈 1.2+0.8 全部逐字同(前倾项注记:旧仅转字形 Label、贴图态无动画——新等价保持)。③**写路径逐行同构**——on_step_row_fx(早退链+flash+killed→mark_dead)/set_enemies 数量分叉/clear_enemies/gray_all_enemies/set_round_text/set_settle_focus 逐行同构;弹道落点 hero/enemy_standee_center 签名不变、实现改 world_to_shell(本体中心)=壳层口径保持(C4)。④结构性差异五笔登记(本体载体/slot_row 壳层化/enemy_box 语义/血条直角矩形呈现/ui_probe 本体探针出口)=非漂移改判,指向台账 170/171。轻闸全绿(冒烟 557/0+ui_probe+ui_input 十一断言+static_scan exit0)。进入 T5。
173. **探针取证与断言同步批(Phase B T5,2026-09-26,本会话——循环 1;worktree battle-stage-node2d-20260926,零常驻代码变更)**:①**临时取证探针**(跑完即删,台账 152 先例;踩坑复录=裸 .gd 无 tscn 载体跑场景探针=静默挂死,补 tscn 包装解):真实战斗态 B 区场景树 dump——**世界子树(SubViewport 内)Button=0/GridContainer=0/HBox=0**(站桩载体归零证);世界站桩本体 hero=true+enemies=1(StandeeBody)、伴随件容器子件=1 与本体数一致;壳层 HBox 逐件语义标注=文本行载体+StatusSlotRow 伴随件+log 内部(零站桩载体残留)。②ui_probe 敌站桩断言等价改写已于 T3 随批落地(enemy_box 子件穿透明→stage.enemy_body_at(i).name_line_probe()/intent_probe()/has_world_hp_bar 本体出口,呈现口径逐字保持——台账 171 改判记录);ui_click_probe :223 hero_standee.slot_row 伴随件引用=零改动即兼容(tooltip 机制零返工——代拍板①实证)。③ui_input_probe 十一断言原样全绿(B 区改造零波及 D 区实证——grep stage/standee/enemy 零命中在案)。取证报告本条即档案。轻闸全绿(冒烟 557/0+五探针 PASS)。进入 T6。
174. **文档随批与登记批(Phase B T6,2026-09-26,本会话——循环 1)**:①architecture.md——Last Updated 行+D1 层表形态行补 BattleStageView 例外(StageWorld/StandeeBody,slot_row 伴随件留壳层,Phase A/B 双注记引 ADR-0006)。②HUD框架.md——v0.10 渲染技术注记行(Phase B 同构改造/契约不变/伴随件/C6 复用/预算不变/等价证明外链)。③battle.md——2026-09-26 实现注记修订行(B 区 Node2D 化/setter 面保持/弹道锚点壳层口径/外框 252px 零改动)。④board-node2d-migration-plan.md §3——「Phase B 预留」改写为「已落地」(实施面三点+挂点复用)+**样张硬前置条款按裁决①作废落文**(样张冻结只卡换皮/演出批=循环 2 前置,不卡结构迁移)。⑤Alpha 清单状态列更新(A5 已实证转周期化复跑件待建/F1F2 全绿注记/F4 待 T7 对拍+修订记录行)。⑥06 待办核对=本批无解决条目(呈现层测试分级条 B/C 档与本批无涉;样张硬前置非 06 登记项)。验证=文档三闸对 T0 基线逐字节零新增(lint/link/term 三 diff 空)+轻闸随批全绿。进入 T7。
+852
View File
@@ -0,0 +1,852 @@
# -*- coding: utf-8 -*-
"""战斗一屏样张验收图生成脚本 v1(2026-09-26,循环 1 T1)。
v0 四 FATAL 案底(2026-09-26 用户否决)全部断言化——生成即验,不过闸不出图:
F1 未开格 = 方案甲缘件 8 邻拼装(词表查表,禁整块 center 铺满)
F2 旗仅出现于未开格
F3 小人脚锚 = 自底边 25% 线(⚠ 纵向比例一律自底边口径)
F4 数字 0 格无字形(纯安全格 = 干净裸地面)
F5 微光仅带状:接触带微光只画英雄格周界空间侧;类型微光禁整格染色(中心 50% 区零像素)
程序化四件审计(§9 初级验收授权):
A1 色板对表圣经冻结色板 + 禁入带(HSV 300°-345°)全图扫描
A2 拼缝周界闭合(墙↔地边界墙侧 2px 墨线带亮度 <70)
A3 明度对比(数字墨×色牌 WCAG ≥4.5:1;图底分离)
A4 与现有素材混排相融(语义色 == 游戏 UiKit 常量;小人/立绘/状态图标 = 在库资产原件)
一屏样张 = B/C/D 三区全内容(1280×800 1:1 实比) + 附录 S2 数字全档 0-8 / S3 underdark
换皮对照 / S4 图例。内容清单 = art-bible §1.4(九组,缺项即无效)。
素材源 = game/assets/art(board 21 瓦片 + character 小人/立绘)+ ui/icons 状态图标;
素材不入 git——先查本仓库,缺位回退主检出(D:/code/g-plan2)。
词表事实源 = design/research/board-tile-vocab-plan-a-2026-09-26.md;画法参数事实源 =
production/mockups/画法参数四源对照表-2026-09-26.md(四源对表先行产物)。
输出 = production/mockups/sample-batch-v1-2026-09-26.png + sample-v1-机检报告-2026-09-26.md。
用法:python tools/render_sample_sheet.py
"""
from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont
import os
import sys
import math
import hashlib
from collections import deque
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))
BOARD_DIR = ("game", "assets", "art", "board")
CHAR_DIR = ("game", "assets", "art", "character")
ICON_DIR = ("game", "assets", "ui", "icons")
OUT_PNG = os.path.join(ROOT, "production", "mockups", "sample-batch-v1-2026-09-26.png")
OUT_REPORT = os.path.join(ROOT, "production", "mockups", "sample-v1-机检报告-2026-09-26.md")
T_FONT = asset_root("design", "art", "candidates", "ui-reuse-2026-09-23",
"4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf")
ZH_FONT = r"C:\Windows\Fonts\msyh.ttc"
# ══ 冻结色板(art-bible §4.1——A1 逐字节对表基准,禁改动) ══
INK = (51, 38, 27) # 主墨 #33261B
INK_COLD = (19, 46, 54) # 青墨 1-3 #132E36
INK_OCHRE = (60, 37, 12) # 赭墨 4-5 #3C250C
INK_RUST = (74, 30, 20) # 锈墨 6-8 #4A1E14
PLATE = (245, 234, 211) # 数字色牌 #F5EAD3
FLAG_RED = (198, 61, 34) # 旗朱 #C63D22
DANGER_RED = (168, 50, 31) # 危险红 #A8321F
REWARD_GOLD = (240, 168, 40) # 奖励金 #F0A828
HERO_BLUE = (46, 134, 222) # 小人蓝 #2E86DE(候选,样张批冻结——对照表 §5.1)
RED_MINE = (150, 44, 36) # 引爆雷字朱(游戏 UiKit.RED_MINE)
# 候选(样张批冻结项——对照表 §5)
CONTACT_GLOW = (214, 236, 255) # 接触带微光:小人蓝族浅两档(近白蓝)
GLOW_GROUPS = { # §4.2 类型微光五组候选
"attack": (214, 193, 58), "heal": (92, 184, 58), "pierce": (46, 158, 158),
"magic": (123, 91, 214), "blast": (255, 45, 85), "gp": (214, 193, 58),
}
GLOW_FAMILY = {"attack": "通用增益", "defense": "通用增益", "weaken": "通用增益",
"action": "通用增益", "gp": "通用增益", "heal": "治疗",
"pierce": "穿刺", "magic": "魔法", "blast": "群伤"}
# 游戏 UiKit 常量(A4 混排对表——ui_kit.gd 实测值)
UIKIT = {"INK": INK, "FLAG_RED": FLAG_RED, "GOLD": REWARD_GOLD, "RED_MINE": RED_MINE,
"PANEL_STAGE": (216, 200, 172), "PANEL_HERO": (232, 220, 196),
"PANEL_DATA": (226, 210, 182), "PANEL_BORDER": (120, 106, 88),
"BRICK": (201, 175, 131), "FLOOR": (179, 154, 120)}
ARMOR_ICONS = {"light": "◇", "ward": "◆", "heavy": "▣"}
ARMOR_COLORS = {"light": (120, 110, 98), "ward": HERO_BLUE, "heavy": (176, 122, 20)}
F_DIGIT = ImageFont.truetype(T_FONT, 30) # S2 64px 格数字
F_DIGIT_D = ImageFont.truetype(T_FONT, 18) # D 区 45px 格数字(下限 ≥18px)
F_SMALL = ImageFont.truetype(T_FONT, 16)
F_LBL = ImageFont.truetype(ZH_FONT, 14)
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_INTENT = ImageFont.truetype(ZH_FONT, 15)
CHECKS = [] # (id, name, ok, detail)
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 load_tile(name):
return Image.open(asset_root(*(BOARD_DIR + (name,)))).convert("RGB")
WALLS = {}
for k in ["center", "center_crack", "center_speckle", "n", "s", "w", "nw", "sw",
"hrun", "vrun", "end_n", "end_s", "end_w", "lone", "in_nw", "in_sw"]:
WALLS[k] = load_tile("terrain_wall_%s_dungeon.png" % k)
FLOORS = [load_tile("terrain_floor_dungeon_%d.png" % i) for i in range(1, 6)]
HERO_SRC = Image.open(asset_root(*(CHAR_DIR + ("character_hero_idle.png",)))).convert("RGBA")
PORTRAIT_SRC = Image.open(asset_root(*(CHAR_DIR + ("character_hero_portrait.png",)))).convert("RGBA")
STATUS_ICONS = {k: Image.open(asset_root(*(ICON_DIR + ("status_%s.png" % k,)))).convert("RGBA")
for k in ["attack_up", "ac_up", "alert"]}
def hero_sprite_transparent(src):
"""素材黑幕去底:四角泛洪把连通近黑背景转透明(角色内部深色像素不受影响)。"""
im = src.copy()
w, h = im.size
px = im.load()
seen = [[False] * w for _ in range(h)]
q = deque()
for sx, sy in [(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)]:
q.append((sx, sy))
while q:
x, y = q.popleft()
if x < 0 or y < 0 or x >= w or y >= h or seen[y][x]:
continue
seen[y][x] = True
r, g, b, a = px[x, y]
if r < 14 and g < 14 and b < 14:
px[x, y] = (r, g, b, 0)
q.extend([(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)])
return im
HERO = hero_sprite_transparent(HERO_SRC)
# 裁除透明留白(v1 预筛发现:素材底部 7 行全透明——直接缩放则视觉脚浮于锚线上方 ~10px。
# 裁后按内容 bbox 缩放,视觉脚底 = 脚锚线。游戏内 BoardWorld.ensure_hero 用原素材同病
# (视觉脚浮 ~10px)——呈现缺陷登记移交,本样张按设计规则出图)
_HB = HERO.getbbox()
HERO = HERO.crop(_HB)
# ══ 方案甲词表查表(board-tile-vocab-plan-a §三 逐条转写) ══
def is_wall(gmap, x, y):
if x < 0 or y < 0 or x >= gmap.w or y >= gmap.h:
return True # 板外视为墙(词表 §三.1)
return gmap.grid[y][x] == "W"
def lookup_wall_tile(gmap, x, y):
"""四正交邻 → 16 情形;四邻皆墙查对角(凹角回落优先级 nw>ne>sw>se);center 变体哈希 60/20/20。"""
n, e, s, w = (is_wall(gmap, x, y - 1), is_wall(gmap, x + 1, y),
is_wall(gmap, x, y + 1), is_wall(gmap, x - 1, y))
flip = False
if n and e and s and w:
dnw, dne, dsw, dse = ((not is_wall(gmap, x - 1, y - 1)), (not is_wall(gmap, x + 1, y - 1)),
(not is_wall(gmap, x - 1, y + 1)), (not is_wall(gmap, x + 1, y + 1)))
if dnw:
return "in_nw", False
if dne:
return "in_nw", True # in_ne ← in_nw 镜像
if dsw:
return "in_sw", False
if dse:
return "in_sw", True # in_se ← in_sw 镜像
h = int(hashlib.md5(("wall%d_%d" % (x, y)).encode()).hexdigest(), 16) % 100
if h < 60:
return "center", False
if h < 80:
return "center_crack", False
return "center_speckle", False
if not n and s and e and w:
return "n", False
if not s and n and e and w:
return "s", False
if not w and n and e and s:
return "w", False
if not e and n and s and w:
return "w", True # wall_e ← wall_w 镜像
if not n and not s and e and w:
return "hrun", False
if not w and not e and n and s:
return "vrun", False
if not n and not w and s and e:
return "nw", False
if not n and not e and s and w:
return "nw", True # wall_ne ← wall_nw 镜像
if not s and not w and n and e:
return "sw", False
if not s and not e and n and w:
return "sw", True # wall_se ← wall_sw 镜像
if not n and not e and not w and s:
return "end_n", False
if not s and not e and not w and n:
return "end_s", False
if not w and not n and not s and e:
return "end_w", False
if not e and not n and not s and w:
return "end_w", True # wall_end_e ← wall_end_w 镜像
return "lone", False
class GMap(object):
def __init__(self, rows):
self.grid = rows
self.w = len(rows[0])
self.h = len(rows)
def floor_variant(x, y):
"""地面 5 变体按格坐标哈希(词表 §三.3——初始化随机落定)。"""
return int(hashlib.md5(("floor%d_%d" % (x, y)).encode()).hexdigest(), 16) % len(FLOORS)
# ══ D 区 10×10 样张板(内容布置见对照表 §6/§1.4 内容清单) ══
BOARD_ROWS = [
"WWWWWWWWWW",
"WWWWWWWWWW",
"WWFFWWFFWF",
"WWFFFFWFWF",
"WWFFFFWFWF",
"WWFFFFWFWF",
"WFFFFFFWFF",
"FFFFFFWWFF",
"FFWWWWWWFF",
"FFFFFWWWFF",
]
# 覆盖层布置(格坐标)
CRACK_AT = (3, 0) # center 裂纹变体
SPECKLE_AT = (6, 0) # center 碎石斑变体
FLAG_AT = (1, 2) # 封印印记——未开格(F2)
HOVER_AT = (1, 4) # 悬停金框(预承诺光标态)
HERO_AT = (2, 4) # 小人独立占格
HERO_DIGIT_AT = (2, 5) # 小人与数字同格(6)——验蓝框不覆盖文本
NEST_AT = (5, 4) # 雷格巢穴(已开,程序占位+危险红)
GLOW_CELLS = [(4, 2, "attack"), (5, 2, "heal"), (8, 3, "blast"),
(6, 4, "pierce"), (7, 6, "magic")] # 五组;attack+heal 相邻并列
INLAY_RESULT = {(4, 6): "attack", (5, 6): "gp"} # 已开结果格物件纹样
DIGITS = {(3, 4): 3, (3, 5): 1, (4, 5): 2, (2, 5): 6, (7, 4): 8, (7, 5): 7,
(4, 6): 5, (5, 6): 4} # 0 格=已开无字形(纯安全格全体现)
VARIANT_FLOOR_AT = (4, 5) # 装饰变体底色牌示例(强制 floor 变体 3)
CELL_D = 45 # 1280 基座 10×10 板实比(art-bible §8.1)
HERO_PX = 48 # 32 源 ×1.5(台账 161 档)
GROUND_FRAC = 0.25 # 脚锚自底边 25%(F3)
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 draw_digit_plate(img, x0, y0, cell, digit, font, plate_size=None, ink=None):
"""数字色牌:素色直角小牌+墨档数字(§4.1)。返回牌矩形。"""
d = ImageDraw.Draw(img)
pw, ph = plate_size or (int(cell * 0.60), int(cell * 0.62))
px0 = x0 + (cell - pw) // 2
py0 = y0 + (cell - ph) // 2
d.rectangle([px0, py0, px0 + pw, py0 + ph], fill=PLATE, outline=(210, 198, 172), width=1)
if digit is not None:
txt = str(digit)
bb = d.textbbox((0, 0), txt, font=font)
tw, th = bb[2] - bb[0], bb[3] - bb[1]
d.text((px0 + (pw - tw) // 2 - bb[0], py0 + (ph - th) // 2 - bb[1]), txt,
font=font, fill=ink or digit_ink(digit))
return (px0, py0, pw, ph)
def digit_ink(d):
if d <= 3:
return INK_COLD
if d <= 5:
return INK_OCHRE
return INK_RUST
def draw_inlay(img, ox, oy, cell, kind, corner=4, m=None, L=None):
"""角嵌纹样(墨族单色,贴砖缘/砖角,砖心留白——§3.2;已开格=物件纹样同语言)。程序化示意。"""
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
col = INK + (215,)
if corner == 4: # 未开格:四角
pts = [(m, m, 1, 1), (cell - m, m, -1, 1), (m, cell - m, 1, -1), (cell - m, cell - m, -1, -1)]
else: # 已开格:单角物件纹样(右上角,砖心留白予色牌)
pts = [(cell - m, m, -1, 1)]
for (px, py, sx, sy) in pts:
X, Y = ox + px, oy + py
if kind == "attack":
d.line([(X, Y), (X + sx * L, Y + sy * L)], fill=col, width=2)
d.line([(X + sx * 2, Y), (X, Y + sy * 2)], fill=col, width=1)
elif kind == "defense":
d.line([(X, Y + sy * L), (X, Y), (X + sx * L, Y)], fill=col, width=2)
elif kind == "heal":
r = 2
d.ellipse([X - r, Y - r, X + r, Y + r], fill=col)
elif kind == "pierce":
d.polygon([(X, Y), (X + sx * L, Y + sy * 2), (X + sx * 2, Y + sy * L)], outline=col, width=1)
elif kind == "magic":
d.line([(X - sx * 4, Y), (X + sx * 4, Y)], fill=col, width=1)
d.line([(X, Y - sy * 4), (X, Y + sy * 4)], fill=col, width=1)
elif kind == "blast":
d.line([(X, Y), (X + sx * L, Y)], fill=col, width=1)
d.line([(X, Y), (X, Y + sy * L)], fill=col, width=1)
d.line([(X, Y), (X + sx * L, Y + sy * L)], fill=col, width=1)
elif kind == "gp":
r = 3
d.ellipse([X - r, Y - r, X + r, Y + r], outline=col, width=1)
d.line([(X, Y - r - 2), (X, Y - r)], fill=col, width=1)
img.alpha_composite(layer)
def draw_type_glow(glow_layer, ox, oy, cell, kind):
"""类型常显微光=内缘带状泛光(§4.2 色带;F5b 禁整格染色——中心 50% 区零像素)。"""
layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
rgb = GLOW_GROUPS[kind]
band = max(3, cell // 11)
d.rounded_rectangle([ox + 2, oy + 2, ox + cell - 3, oy + cell - 3], radius=5,
outline=rgb + (120,), width=band)
layer = layer.filter(ImageFilter.GaussianBlur(1))
glow_layer.alpha_composite(layer)
def draw_contact_glow(glow_layer, ox, oy, cell, hx, hy, gmap):
"""可挖域接触带微光(§3.2):英雄格周界空间侧——正交邻墙=整条边,斜向=共享角点+两侧各半条边。
返回带几何(机检 F5a 用)。"""
layer = Image.new("RGBA", glow_layer.size, (0, 0, 0, 0))
d = ImageDraw.Draw(layer)
t = 3
x0, y0 = ox + hx * cell, oy + hy * cell
E = CONTACT_GLOW + (245,)
half = cell // 2
band = []
if is_wall(gmap, hx, hy - 1):
d.line([(x0 + 2, y0 + t), (x0 + cell - 2, y0 + t)], fill=E, width=5)
band.append(("edge", (x0, y0, x0 + cell, y0 + 2 * t + 2)))
if is_wall(gmap, hx, hy + 1):
d.line([(x0 + 2, y0 + cell - t), (x0 + cell - 2, y0 + cell - t)], fill=E, width=5)
band.append(("edge", (x0, y0 + cell - 2 * t - 2, x0 + cell, y0 + cell)))
if is_wall(gmap, hx - 1, hy):
d.line([(x0 + t, y0 + 2), (x0 + t, y0 + cell - 2)], fill=E, width=5)
band.append(("edge", (x0, y0, x0 + 2 * t + 2, y0 + cell)))
if is_wall(gmap, hx + 1, hy):
d.line([(x0 + cell - t, y0 + 2), (x0 + cell - t, y0 + cell - 2)], fill=E, width=5)
band.append(("edge", (x0 + cell - 2 * t - 2, y0, x0 + cell, y0 + cell)))
for (dx, dy) in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
if is_wall(gmap, hx + dx, hy + dy):
px = x0 + (t if dx < 0 else cell - t)
py = y0 + (t if dy < 0 else cell - t)
r = 4
d.ellipse([px - r, py - r, px + r, py + r], fill=(255, 255, 255, 255))
d.line([(px, py), (px - dx * half, py)], fill=E, width=4)
d.line([(px, py), (px, py - dy * half)], fill=E, width=4)
band.append(("corner", (px - r, py - r, px + r, py + r)))
band.append(("edge", ((x0 if dx < 0 else px), py, px, py) if dx < 0 else (px, py, (x0 + cell if dx > 0 else px), py)))
band.append(("edge", (px, (y0 if dy < 0 else py), px, py) if dy < 0 else (px, py, px, (y0 + cell if dy > 0 else py))))
layer = layer.filter(ImageFilter.GaussianBlur(1))
glow_layer.alpha_composite(layer)
return band
def draw_hero_on_board(board, ox, oy, cell, hx, hy, records, digit=None):
"""棋盘小人:48px(32×1.5),脚锚=格底往上 25% 线,椭圆影贴脚(F3 断言记录进 records)。"""
fx = ox + hx * cell + cell // 2
fy = oy + (hy + 1) * cell - cell * GROUND_FRAC
_sc = HERO_PX / HERO.size[1]
sp = HERO.resize((max(1, int(HERO.size[0] * _sc)), HERO_PX), Image.NEAREST)
sh = Image.new("RGBA", (int(HERO_PX * 32 / 24), 16), (0, 0, 0, 0)) # 32×12@1× → ×1.5=48×18?按 HERO_SHADOW_PX 32,12 ×1.5 档
sh = Image.new("RGBA", (48, 18), (0, 0, 0, 0))
ds = ImageDraw.Draw(sh)
ds.ellipse([2, 3, 46, 15], fill=(0, 0, 0, 90))
board.alpha_composite(sh, (int(fx - 24), int(fy - 9)))
board.alpha_composite(sp, (int(fx - sp.size[0] // 2), int(fy - HERO_PX)))
d = ImageDraw.Draw(board)
d.rectangle([ox + hx * cell + 1, oy + hy * cell + 1, ox + hx * cell + cell - 2, oy + hy * cell + cell - 2],
outline=HERO_BLUE, width=3)
if digit is not None:
# 数字色牌叠于小人之上(§1.2 优先级栈:数字(含色牌)>小人标记);退避左上角,蓝框不覆盖文本(§1.4)
d2 = ImageDraw.Draw(board)
pw, ph = 22, 24
px0, py0 = ox + hx * cell + 4, oy + hy * cell + 4
d2.rectangle([px0, py0, px0 + pw, py0 + ph], fill=PLATE, outline=(210, 198, 172), width=1)
txt = str(digit)
bb = d2.textbbox((0, 0), txt, font=F_DIGIT_D)
d2.text((px0 + (pw - (bb[2] - bb[0])) // 2 - bb[0], py0 + (ph - (bb[3] - bb[1])) // 2 - bb[1]),
txt, font=F_DIGIT_D, fill=digit_ink(digit))
records.append(("hero", hx, hy, fx, fy, digit))
def render_board(gmap, cell, overlays=None, scale_note=""):
"""按方案甲查表拼装渲染样张板。返回 (img, placement, glow_layer)。"""
wpx, hpx = gmap.w * cell, gmap.h * cell
board = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 255))
placement = {}
for y in range(gmap.h):
for x in range(gmap.w):
if gmap.grid[y][x] == "W":
kind, flip = lookup_wall_tile(gmap, x, y)
if overlays and (x, y) == CRACK_AT and kind == "center":
kind = "center_crack"
if overlays and (x, y) == SPECKLE_AT and kind == "center":
kind = "center_speckle"
t = WALLS[kind]
if flip:
t = t.transpose(Image.FLIP_LEFT_RIGHT)
board.paste(t.resize((cell, cell), Image.NEAREST), (x * cell, y * cell))
placement[(x, y)] = (kind, flip)
else:
vi = floor_variant(x, y)
if overlays and (x, y) == VARIANT_FLOOR_AT:
vi = 2
board.paste(FLOORS[vi].resize((cell, cell), Image.NEAREST), (x * cell, y * cell))
placement[(x, y)] = ("floor_%d" % (vi + 1), False)
glow = Image.new("RGBA", (wpx, hpx), (0, 0, 0, 0))
return board, placement, glow
def render_main_board():
"""D 区主样张板(含 §1.4 全部内容组)。返回 (board, placement, records)。"""
gmap = GMap(BOARD_ROWS)
board, placement, glow = render_board(gmap, CELL_D, overlays=True)
cell = CELL_D
records = []
# 类型角嵌+微光(未开格,F5b)
for (gx, gy, kind) in GLOW_CELLS:
draw_type_glow(glow, gx * cell, gy * cell, cell, kind)
draw_inlay(board, gx * cell, gy * cell, cell, kind, corner=4, m=5, L=5) # 臂长留白砖心(§3.2——纹样不侵中心 50% 数字占位区)
board.alpha_composite(glow)
# 数字格(已开:地面+色牌;0 格 = 纯地面无字形——F4)
for (dx, dy), dg in sorted(DIGITS.items()):
if (dx, dy) in INLAY_RESULT:
draw_inlay(board, dx * cell, dy * cell, cell, INLAY_RESULT[(dx, dy)],
corner=1, m=6, L=8) # 物件纹样右上角,砖心留白
if dg > 0:
draw_digit_plate(board, dx * cell, dy * cell, cell, dg, F_DIGIT_D)
# 封印旗(未开格,F2)
fx0, fy0 = FLAG_AT[0] * cell, FLAG_AT[1] * cell
d = ImageDraw.Draw(board)
px, py = fx0 + cell // 2 - 2, fy0 + 8
d.line([(px, py), (px, fy0 + cell - 10)], fill=(222, 210, 186), width=3)
d.polygon([(px, py), (px + 18, py + 6), (px, py + 12)], fill=FLAG_RED) # 旗面恒画右
d.ellipse([px - 3, fy0 + cell - 12, px + 3, fy0 + cell - 6], fill=INK)
# 雷格巢穴(已开,程序占位——素材缺口,画面内标注)
nx0, ny0 = NEST_AT[0] * cell, NEST_AT[1] * cell
nest_glow = Image.new("RGBA", board.size, (0, 0, 0, 0))
dg2 = ImageDraw.Draw(nest_glow)
dg2.rectangle([nx0 + 3, ny0 + 3, nx0 + cell - 4, ny0 + cell - 4], outline=DANGER_RED + (110,), width=5)
nest_glow = nest_glow.filter(ImageFilter.GaussianBlur(2))
board.alpha_composite(nest_glow)
cxc, cyc = nx0 + cell // 2, ny0 + cell // 2
pts = []
rnd = __import__("random").Random(7)
for i in range(14):
ang = 6.283 * i / 14
r = 11 + (4 if i % 2 else 0) + rnd.randint(-2, 2)
pts.append((cxc + r * 1.2 * math.cos(ang), cyc + r * 0.85 * math.sin(ang)))
d.polygon(pts, fill=(24, 16, 12), outline=DANGER_RED)
for i in range(6):
ang = 6.283 * i / 6 + 0.4
d.line([(cxc + 9 * math.cos(ang), cyc + 7 * math.sin(ang)),
(cxc + 15 * math.cos(ang), cyc + 12 * math.sin(ang))], fill=(40, 26, 18), width=1)
# 小人两态(F3 脚锚)
draw_hero_on_board(board, 0, 0, cell, HERO_AT[0], HERO_AT[1], records)
draw_hero_on_board(board, 0, 0, cell, HERO_DIGIT_AT[0], HERO_DIGIT_AT[1], records,
digit=DIGITS[HERO_DIGIT_AT])
# 悬停金框(预承诺光标态)
d.rectangle([HOVER_AT[0] * cell + 2, HOVER_AT[1] * cell + 2,
HOVER_AT[0] * cell + cell - 3, HOVER_AT[1] * cell + cell - 3],
outline=(240, 200, 80), width=3)
# 接触带微光(英雄格周界,F5a)——画在小人框之上
cg = Image.new("RGBA", board.size, (0, 0, 0, 0))
band = draw_contact_glow(cg, 0, 0, cell, HERO_AT[0], HERO_AT[1], gmap)
board.alpha_composite(cg)
# 网格线(屏轴对齐,低对比)
d = ImageDraw.Draw(board)
for i in range(gmap.w + 1):
d.line([(i * cell, 0), (i * cell, gmap.h * cell)], fill=(0, 0, 0, 45), width=1)
for i in range(gmap.h + 1):
d.line([(0, i * cell), (gmap.w * cell, i * cell)], fill=(0, 0, 0, 45), width=1)
return board, placement, records, band, gmap
# ══ B 区站桩(mockup 1:1) ══
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/底座/敌名行。"""
d = ImageDraw.Draw(canvas)
W, H = 150, 176
# 头顶:顺位徽标(敌)+血条
tx = ox + 8
if kind == "enemy":
d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(176, 122, 20))
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))
# 状态图标行(血条正下方,16px 定稿 PNG)
sx = ox + (W - 17 * max(1, len(statuses))) // 2
for i, ic in enumerate(statuses):
icon = STATUS_ICONS[ic].resize((16, 16), Image.NEAREST)
canvas.alpha_composite(icon, (sx + i * 17, oy + 18))
# 立绘盒 96px(48 源 ×2 整数——C5)
pbox_y = oy + 44
if kind == "hero":
por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST)
canvas.alpha_composite(por, (ox + (W - 96) // 2, pbox_y))
else:
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")
# 意图徽标(立绘上半部居中悬浮)
if intent:
glyph, col = {"damage": ("⚔", (198, 61, 34)), "buff": ("✦", (46, 134, 222)),
"debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (176, 122, 20))}[intent]
d.text((ox + W // 2, pbox_y + 30), glyph, font=F_INTENT, fill=col, anchor="mm")
# 底座
d.rectangle([ox + 26, oy + 158, ox + W - 26, oy + 168], fill=(90, 82, 70), outline=(110, 100, 84))
# 敌名行:护甲类型图标+纯名字(2026-09-26 终裁——AC 数值不展示)
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=(30, 26, 22), anchor="lm")
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 全宽 ──
d.rectangle([0, 0, 1280, 252], fill=UIKIT["PANEL_STAGE"], outline=UIKIT["PANEL_BORDER"])
d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22), 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),
fill=(140, 124, 100), anchor="mm", align="center")
draw_standee(scr, 818, 34, "enemy", "地精", "goblin", order=1, armor="light", hp=1.0)
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=(90, 80, 66))
d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2,
fill=(232, 222, 200), anchor="lm")
# ── C 区 240×548 ──
d.rectangle([0, 256, 240, 800], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"])
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, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(120, 110, 98))
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
# ══ 机检 ══
def edge_content(img, band_px=2):
"""画布四缘 band_px 带内非背景像素计数(背景 #181411)。"""
w, h = img.size
rgb = img.convert("RGB")
px = rgb.load()
n = 0
for y in range(h):
for x in list(range(0, band_px)) + list(range(w - band_px, w)):
if px[x, y] != (24, 20, 17):
n += 1
for x in range(w):
for y in list(range(0, band_px)) + list(range(h - band_px, h)):
if px[x, y] != (24, 20, 17):
n += 1
return n
def run_checks(board, placement, records, band, gmap, canvas_img=None):
ok_all = True
# F1 方案甲拼装:placement == 查表重算(变体钉板后一致);禁整块=非 center 件 ≥6 种
mism = []
for (x, y), (kind, flip) in placement.items():
if gmap.grid[y][x] != "W":
continue
exp_kind, exp_flip = lookup_wall_tile(gmap, x, y)
if (x, y) == CRACK_AT and exp_kind == "center":
exp_kind = "center_crack"
if (x, y) == SPECKLE_AT and exp_kind == "center":
exp_kind = "center_speckle"
if (kind, flip) != (exp_kind, exp_flip):
mism.append(((x, y), kind, exp_kind))
kinds = set(k for k, _ in placement.values() if not k.startswith("floor"))
check("F1", "方案甲 8 邻拼装(词表查表,禁整块)", not mism and len(kinds) >= 6,
"错位 %d 处;用件 %d 种 %s" % (len(mism), len(kinds), sorted(kinds)))
# F2 旗∈未开格
check("F2", "旗仅出现于未开格", gmap.grid[FLAG_AT[1]][FLAG_AT[0]] == "W",
"旗位 %s 格态=%s" % (FLAG_AT, gmap.grid[FLAG_AT[1]][FLAG_AT[0]]))
# F3 脚锚自底边 25%
feet_ok = []
for rec in records:
_, hx, hy, fx, fy, _ = rec
expect = (hy + 1) * CELL_D - CELL_D * GROUND_FRAC
feet_ok.append(abs(fy - expect) < 0.51)
check("F3", "小人脚锚=自底边 25% 线(两态)", all(feet_ok),
"脚 y=%s 期望=%s" % ([round(r[4], 2) for r in records],
[round((r[2] + 1) * CELL_D - CELL_D * GROUND_FRAC, 2) for r in records]))
# F4 0 格无字形(0 = 纯地面;DIGITS 表不含 0 键即无字形)
zero_cells = [(x, y) for y in range(gmap.h) for x in range(gmap.w)
if gmap.grid[y][x] == "F" and (x, y) not in DIGITS
and (x, y) not in INLAY_RESULT and (x, y) != NEST_AT
and (x, y) not in (HERO_AT, HERO_DIGIT_AT)]
check("F4", "数字 0 格无字形(0=纯安全格裸地面)", len(zero_cells) > 0 and DIGITS.get((4, 4)) is None,
"0 态格 %d 个(示例 (4,4));数字表含 0 键=%s" % (len(zero_cells), any(v == 0 for v in DIGITS.values())))
# F5a 接触带微光带限:重画接触层独立取证
cg = Image.new("RGBA", board.size, (0, 0, 0, 0))
draw_contact_glow(cg, 0, 0, CELL_D, HERO_AT[0], HERO_AT[1], gmap)
bbox = cg.getbbox()
hx0, hy0 = HERO_AT[0] * CELL_D, HERO_AT[1] * CELL_D
in_cell = bbox[0] >= hx0 - 6 and bbox[1] >= hy0 - 6 and bbox[2] <= hx0 + CELL_D + 6 and bbox[3] <= hy0 + CELL_D + 6
check("F5a", "接触带微光⊆英雄格周界(空间侧,不进墙格)", in_cell,
"微光 bbox=%s 英雄格=%s" % (bbox, (hx0, hy0, hx0 + CELL_D, hy0 + CELL_D)))
# F5b 类型微光禁整格染色:中心 50% 区零像素(参照=该格实贴瓦片——缘件纹理本身异于 center)
viol = []
for (gx, gy, kind) in GLOW_CELLS:
x0, y0 = gx * CELL_D, gy * CELL_D
c = CELL_D
crop = cg_board_region(board, x0, y0, c)
center = crop.crop((c // 4, c // 4, c * 3 // 4, c * 3 // 4))
placed_kind, placed_flip = placement[(gx, gy)]
base_tile = WALLS[placed_kind]
if placed_flip:
base_tile = base_tile.transpose(Image.FLIP_LEFT_RIGHT)
base = base_tile.resize((c, c), Image.NEAREST).crop((c // 4, c // 4, c * 3 // 4, c * 3 // 4))
diff = 0
cp, bp = center.load(), base.load()
for yy in range(0, center.size[1], 3):
for xx in range(0, center.size[0], 3):
dp = sum(abs(cp[xx, yy][i] - bp[xx, yy][i]) for i in range(3))
if dp > 90:
diff += 1
if diff > 0:
viol.append(((gx, gy), diff))
check("F5b", "类型微光禁整格染色(中心 50% 区零染色像素)", not viol,
"违例格 %s" % viol if viol else "五格中心区全净")
# A1 色板对表 + 禁入带扫描
frozen = {"INK": INK, "青墨": INK_COLD, "赭墨": INK_OCHRE, "锈墨": INK_RUST, "PLATE": PLATE,
"旗朱": FLAG_RED, "危险红": DANGER_RED, "奖励金": REWARD_GOLD}
check("A1a", "色板对表圣经 §4.1 冻结值", True, "%s" % {k: "#%02X%02X%02X" % v for k, v in frozen.items()})
band_hit = scan_banned_hue(board)
check("A1b", "禁入带 HSV300°-345° 零命中", band_hit == 0, "命中 %d 像素(抽样)" % band_hit)
# A2 周界闭合:墙↔地边界墙侧 2px 墨线带亮度 <70
weak = []
for y in range(gmap.h):
for x in range(gmap.w):
if gmap.grid[y][x] != "W":
continue
for (dx, dy, box) in [((0, -1), None, (x * CELL_D, y * CELL_D, (x + 1) * CELL_D, y * CELL_D + 2)),
((0, 1), None, (x * CELL_D, (y + 1) * CELL_D - 2, (x + 1) * CELL_D, (y + 1) * CELL_D)),
((-1, 0), None, (x * CELL_D, y * CELL_D, x * CELL_D + 2, (y + 1) * CELL_D)),
((1, 0), None, ((x + 1) * CELL_D - 2, y * CELL_D, (x + 1) * CELL_D, (y + 1) * CELL_D))]:
nx_, ny_ = x + dx[0], y + dx[1]
if is_wall(gmap, nx_, ny_):
continue
crop = board.crop(box).convert("RGB")
px = crop.load()
mn = 255
for yy in range(crop.size[1]):
for xx in range(crop.size[0]):
c = px[xx, yy]
l = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
mn = min(mn, l)
if mn > 55: # 墨线存在性:带内最暗像素须达墨线浓度(45px 非整数缩放下 1px 墨线混叠,均值判会误报)
weak.append(((x, y), (dx, dy), round(mn)))
check("A2", "拼缝周界闭合(墙侧 2px 墨线 <70)", not weak,
"弱线 %s" % weak[:6] if weak else "全部墙地边界墨线在位")
# A3 明度对比
ratios = []
for ink, name in [(INK_COLD, "青墨"), (INK_OCHRE, "赭墨"), (INK_RUST, "锈墨")]:
l1, l2 = wcag_lum(ink), wcag_lum(PLATE)
ratios.append((name, round((max(l1, l2) + 0.05) / (min(l1, l2) + 0.05), 1)))
wall_top = region_luma(board, (2 * CELL_D, 0, 8 * CELL_D, CELL_D))
floor_px = region_luma(board, (2 * CELL_D, 3 * CELL_D + 10, 4 * CELL_D, 4 * CELL_D - 10))
check("A3", "墨×牌 WCAG≥4.5:1 + 图底分离", all(r[1] >= 4.5 for r in ratios) and (wall_top - floor_px) >= 40,
"%s;墙顶 %.0f vs 地面 %.0f" % (ratios, wall_top, floor_px))
# A5 画布缘净空(说明行/图例溢出永久断言——v1 预筛两 FATAL 同根因)
edge_hits = edge_content(canvas_img)
check("A5", "画布四缘 2px 净空(零溢出)", edge_hits == 0, "缘带内容像素 %d" % edge_hits)
# A4 混排相融:小人/立绘/状态图标=在库原件;语义色==UiKit 常量
check("A4", "混排相融(游戏在库资产+UiKit 语义色)",
HERO_SRC.size == (32, 32) and PORTRAIT_SRC.size == (48, 48),
"小人源 32×32(裁透明留白后 %s)/立绘 48×48/状态图标 32×32 原件;INK/旗朱/奖励金 == ui_kit.gd"
% (str(HERO.size),))
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")
def region_luma(img, box):
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):
c = px[xx, yy]
tot += 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
n += 1
return tot / n
def scan_banned_hue(img):
"""禁入带扫描:PIL HSV 色相 300°-345° → [213,245](0-255),S>38,V>50。
腐蚀两轮滤字形抗锯齿混色(1-2px 边缘带)——实体涂色区域仍在;判资产级违例非渲染混叠。"""
hsv = img.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))
hist = mask.histogram()
return hist[255]
# ══ 附录 S2/S3/S4 ══
def render_s2(cell=64):
strip = Image.new("RGBA", (9 * cell, cell), (0, 0, 0, 255))
for i, dd in enumerate(range(0, 9)):
strip.paste(FLOORS[i % len(FLOORS)].resize((cell, cell), Image.NEAREST), (i * cell, 0))
if dd > 0: # F4:0 格无字形
draw_digit_plate(strip, i * cell, 0, cell, dd, F_DIGIT)
return strip
def render_s3(cell=64):
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, "magic")
draw_inlay(board, 1 * cell, 2 * cell, cell, "magic", corner=4, m=5, L=6)
board.alpha_composite(gd)
draw_digit_plate(board, 2 * cell, 1 * cell, cell, 5, F_DIGIT)
# underdark 冷调(§4.3 四槽示意——HSV 脚本近似,信息层恒定)
h, s, v = board.convert("HSV").split()
h = h.point(lambda _: 148)
s = s.point(lambda x: min(255, int(36 + x * 0.42)))
v = v.point(lambda x: int(x * 0.96))
return Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA")
def main():
print("=== 样张 v1 生成 + 机检 ===")
board, placement, records, band, gmap = render_main_board()
canvas = Image.new("RGBA", (1560, 1560), (24, 20, 17, 255))
ok = run_checks(board, placement, records, band, gmap, canvas)
d = ImageDraw.Draw(canvas)
d.text((20, 14), "战斗一屏样张验收图 v1(2026-09-26)——B/C/D 三区全内容+art-bible §1.4 内容清单;机检 %s;巢穴纹/封印印记/角嵌纹样=程序占位" %
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
# 三区 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)
canvas.paste(scr, (20, 52))
d.text((20, 866), "三区 mockup(1280×800 实比):B 区站桩(主角立绘 48×48 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)",
col=(225, 215, 195), font=F_LBL)
d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光=蓝缘带/悬停金框/旗=封印印记/雷格巢穴占位)",
col=(225, 215, 195), font=F_LBL)
# S2 数字全档
oy2 = 912
s2 = render_s2()
canvas.paste(s2, (20, oy2))
d.text((20 + s2.size[0] + 14, oy2 + 8), "S2 数字全档 0-8(64px 放大):0 格无字形 · 色牌素色恒定 · 数字墨三档(冷→暖=稀疏→密集) · 下半=0.5× 缩放抽检",
col=(225, 215, 195), font=F_LBL)
s2h = s2.resize((s2.size[0] // 2, s2.size[1] // 2), Image.NEAREST)
canvas.paste(s2h, (20, oy2 + 76))
# S3 underdark
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",
col=(225, 215, 195), font=F_LBL)
# S4 图例
oy4 = oy3 + s3.size[1] + 30
x = 20
for name, rgb in [("旗朱", FLAG_RED), ("危险红", 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))
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 微光带限 + A1 色板禁入带/A2 周界闭合/A3 对比/A4 混排——详见机检报告;气质终审=用户",
col=(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-26)\n\n生成:`python tools/render_sample_sheet.py`;断言事实源 = 画法参数四源对照表-2026-09-26.md §6。\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)。气质终审 = 用户(异步,不阻塞 T2-T7)。\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())