From 4b174877beb35a7220a9d9383e8c2e72345f5dcb Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 31 Oct 2026 05:05:08 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E6=8F=90=E4=BA=A4(=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=87=A0=E4=BD=95=E5=8D=95=E7=82=B9=E5=8C=96=E6=89=B9=C2=B7?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=9D=A2+=E5=B7=A5=E5=85=B7=E9=9D=A2+archite?= =?UTF-8?q?cture=E8=A1=8C=C2=B7=E8=B7=A8=E4=BC=9A=E8=AF=9D=E9=81=97?= =?UTF-8?q?=E7=95=99):=20battle=5Flayout.tres=20=E6=96=B0=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E8=A1=8C=E4=BA=94=E5=AD=97=E6=AE=B5(stage=5Fh/info=5F?= =?UTF-8?q?panel=5Fw/ap=5Fbar=5Fh/log=5Fband=5Fh=5Fdig/log=5Fband=5Fh=5Fse?= =?UTF-8?q?ttle)=E7=BB=8F=20schema=20battle=5Flayout=5Ftable.gd+=E5=9F=9F?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=20config=5Fdomain=5Fpresentation.gd=20?= =?UTF-8?q?=E6=94=B6=E8=BF=9B=20ConfigDb.presentation(ConfigDB=20=E4=B8=89?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E2=86=92=E5=9B=9B=E5=AE=B9=E5=99=A8,manifest?= =?UTF-8?q?=20=E5=A2=9E=20battle=5Flayout=20=E9=94=AE,B=E5=8C=BA=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E8=A3=81=E5=86=B3=202026-10-30=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=A3=81=3D=E5=8F=96=E4=BB=A3=E7=A0=81=E6=A0=87=E5=87=86=20sta?= =?UTF-8?q?ge=5Fh);views=20=E4=BA=94=E4=BB=B6=E6=9E=84=E9=80=A0=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E5=A2=9E=20layout=20=E5=8F=82(BattleStageView/HeroInf?= =?UTF-8?q?oPanelView/BoardZoneView/ApBarView/BattleLogView=20=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E9=AB=98=E5=BA=A6=E6=94=B9=E8=A1=A8=E5=80=BC=E9=A9=B1?= =?UTF-8?q?=E5=8A=A8)+ui=5Fassembler=20=E8=A3=85=E9=85=8D=E4=BC=A0=20layou?= =?UTF-8?q?t+probes=20=E4=B8=A4=E4=BB=B6=E6=94=B9=E5=9D=80+main.gd=20?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E5=B8=A6=E6=B3=A8=E9=87=8A=E5=8E=BB=E6=95=B0?= =?UTF-8?q?=E5=80=BC;tools/layout=5Fconfig.py=20=E6=96=B0=E4=BB=B6+render?= =?UTF-8?q?=5Fsample=5Fsheet=20=E4=B8=A4=E7=89=88=E8=AF=BB=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E8=A1=A8=E7=8E=B0=E7=AE=97(=E6=A0=B7=E5=BC=A0?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E9=9D=A2);=E6=96=87=E6=A1=A3=E9=9D=A2:=20arc?= =?UTF-8?q?hitecture.md=20ConfigDB=20=E8=A1=8C=E6=8D=A2=E4=BB=A3(=E5=9B=9B?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E6=88=90=E5=91=98=207/7/5/1=E3=80=81?= =?UTF-8?q?=E7=B1=BB=E5=8F=98=E9=87=8F=2010)+HUD=E6=A1=86=E6=9E=B6.md=20?= =?UTF-8?q?=E4=B8=89=E5=8C=BA=E8=A1=A8/=E8=A3=85=E6=9D=BF=E4=B8=8D?= =?UTF-8?q?=E7=AD=89=E5=BC=8F/=E8=B0=83=E5=8F=82=E8=A1=A8=E5=8E=BB?= =?UTF-8?q?=E6=95=B0=E5=80=BC=E5=8C=96=E6=94=B9=E6=8C=87=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E8=A1=A8+=E6=95=B0=E6=8D=AE=E9=85=8D=E7=BD=AE.md+interaction-p?= =?UTF-8?q?atterns.md=20=E6=98=8E=E7=BB=86=E5=B8=A6=E5=8F=A5=E5=8E=BB?= =?UTF-8?q?=E6=95=B0=E5=80=BC(battle.md=20=E5=8C=BA=E8=A1=A8=E4=B8=8E?= =?UTF-8?q?=E5=BE=85=E5=8A=9E=E6=B8=85=E5=8D=95=E7=99=BB=E8=AE=B0=E9=9D=A2?= =?UTF-8?q?=E5=B7=B2=E9=9A=8F=201cdef6ba=20=E5=9C=A8=E5=BA=93);=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E6=BC=8F=E7=BD=91=20L1=20=E5=B0=B1=E5=9C=B0=E8=A1=A5?= =?UTF-8?q?=E6=94=B9=E4=B9=9D=E5=A4=84(=E6=9E=84=E9=80=A0=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E6=8D=A2=E5=9E=8B=E4=B8=8E=20iter45=20=E6=96=AD?= =?UTF-8?q?=E7=82=B9=E4=BB=B6=E6=94=B6=E7=BC=96=E7=9A=84=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=94=B9=E5=9D=80=E6=BC=8F=E7=BD=91,=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E9=9B=B6=E5=8F=98=E5=8C=96):=20theme=5Fprojection=5Ftest:386/p?= =?UTF-8?q?layer=5Fcopy=5Fsweep=5Ftest:44,98/adventure=5Ffeats=5Ftest:772/?= =?UTF-8?q?standee=5Ffx=5Fsweep=5Ftest:75/feat=5Fvouchers=5Ftest:292=20?= =?UTF-8?q?=E5=85=AD=E5=A4=84=E8=A1=A5=20layout=20=E5=AE=9E=E5=8F=82,adven?= =?UTF-8?q?ture=5Ffeats=5Ftest:791/level=5Fshop=5Feconomy=5Ftest:1480/feat?= =?UTF-8?q?=5Fvouchers=5Ftest:81=20=E4=B8=89=E5=A4=84=E6=96=AD=E7=82=B9?= =?UTF-8?q?=E6=88=90=E5=91=98=E7=9B=B4=E8=AF=BB=E6=94=B9=E7=BB=8F=20a.=5Fb?= =?UTF-8?q?p=5Fstore(iter45=E5=8D=A1=E2=91=A1=E5=85=AD=E6=88=90=E5=91=98?= =?UTF-8?q?=E6=90=AC=E7=A7=BB=E6=BC=8F=E7=BD=91,02=20=E9=97=B8=E5=90=88?= =?UTF-8?q?=E5=9B=9E=E5=90=8E=E9=A6=96=E5=85=A8=E9=87=8F=E6=9A=B4=E9=9C=B2?= =?UTF-8?q?);load=5Ftables=20=E8=B6=85=E5=87=BD=E6=95=B0=E9=95=BF=E9=94=AE?= =?UTF-8?q?=E6=B8=85=E5=81=BF(=E5=AE=B9=E5=99=A8=E8=A3=85=E9=85=8D?= =?UTF-8?q?=E5=9D=97=E6=8A=BD=20=5Fassign=5Floaded=20=E7=A7=81=E6=9C=89?= =?UTF-8?q?=E5=8D=8F=E5=87=BD=E6=95=B0=E2=80=94=E2=80=94=E8=A1=A8=E8=A1=8C?= =?UTF-8?q?=E6=8C=89=E5=9F=9F=E6=94=B6=E8=BF=9B=E5=9B=9B=E5=AE=B9=E5=99=A8?= =?UTF-8?q?+resolver=20=E6=B3=A8=E5=85=A5=E5=8D=95=E4=B8=80=E8=81=8C?= =?UTF-8?q?=E8=B4=A3,=E4=B8=8E=20=5Fcross=5Fvalidate=20=E5=A7=94=E6=89=98?= =?UTF-8?q?=E5=85=88=E4=BE=8B=E5=90=8C=E5=9E=8B,S13=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=BA=A2=E4=BF=AE=E8=87=B3=E5=9F=BA=E7=BA=BF=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E5=86=85);=E7=AA=84=E9=AA=8C:=20=E5=9B=9B=E4=BB=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=A5=97=E4=BB=B6=E5=8D=95=E8=B7=91=E5=85=A8=E7=BB=BF?= =?UTF-8?q?+gdstyle=20=E5=9F=BA=E7=BA=BF=E5=A4=96=E6=96=B0=E8=BF=9D?= =?UTF-8?q?=E8=A7=84=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../engineering/architecture/architecture.md | 2 +- design/gdd/systems/表现与支撑/HUD框架.md | 19 +++-- design/gdd/systems/表现与支撑/数据配置.md | 1 + design/gdd/ux/interaction-patterns.md | 2 +- game/core/config_db.gd | 20 +++-- game/core/config_domain_presentation.gd | 6 ++ game/core/config_domain_presentation.gd.uid | 1 + game/data/schemas/battle_layout_table.gd | 30 +++++++ game/data/schemas/battle_layout_table.gd.uid | 1 + game/data/tables/battle_layout.tres | 11 +++ game/presentation/main.gd | 2 +- game/presentation/ui_assembler.gd | 16 ++-- game/presentation/views/ap_bar_view.gd | 4 +- game/presentation/views/battle_log_view.gd | 16 ++-- game/presentation/views/battle_stage_view.gd | 10 +-- game/presentation/views/board_zone_view.gd | 8 +- .../views/hero_info_panel_view.gd | 4 +- game/probe/fx_probe.gd | 2 +- game/probe/ui_struct_probe.gd | 2 +- game/tests/unit/core/adventure_feats_test.gd | 4 +- game/tests/unit/core/feat_vouchers_test.gd | 4 +- .../unit/core/level_shop_economy_test.gd | 2 +- .../unit/presentation/pacing_queue_test.gd | 2 +- .../presentation/player_copy_sweep_test.gd | 4 +- .../presentation/standee_fx_sweep_test.gd | 2 +- .../presentation/theme_projection_test.gd | 2 +- tools/layout_config.py | 24 ++++++ tools/render_sample_sheet.py | 68 +++++++++------- tools/render_sample_sheet_v3.py | 80 +++++++++++-------- 29 files changed, 231 insertions(+), 118 deletions(-) create mode 100644 game/core/config_domain_presentation.gd create mode 100644 game/core/config_domain_presentation.gd.uid create mode 100644 game/data/schemas/battle_layout_table.gd create mode 100644 game/data/schemas/battle_layout_table.gd.uid create mode 100644 game/data/tables/battle_layout.tres create mode 100644 tools/layout_config.py diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index fd60bb59..63876289 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -85,7 +85,7 @@ | 模块 | 独占职责 | 对外暴露 | 消费依赖 | 引擎 API | |---|---|---|---|---| -| **ConfigDB**(autoload,D2) | 全部玩法数值表(`data/tables/*.tres` + `data/schemas/*.gd`);SCHEMA_VERSION;加载/校验/override/诊断状态机。**行族访问器归位各 schema 表类(表管自己的行查询),本类只留加载/校验/override/诊断门面;表持有成员按域收进三张域容器 Resource(`config_domain_combat/run/crossrun.gd`——已建件(迭代 41),类型化成员 7/7/5);三张 resolver 表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers)经 `load_tables` 装配段注入各容器,resolver 文件与键零搬移**——红线不变:本类仍是唯一加载与持有单点,"经 ConfigDB 单点读取"=经其持有的表对象 | 类型化域容器属性(每域一张容器);公开面 8(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload)、类变量 9 | 无(基础层顶点) | Resource/.tres 加载、autoload 生命周期——LOW | +| **ConfigDB**(autoload,D2) | 全部玩法数值表与表现层布局配置表(`data/tables/*.tres` + `data/schemas/*.gd`);SCHEMA_VERSION;加载/校验/override/诊断状态机。**行族访问器归位各 schema 表类(表管自己的行查询),本类只留加载/校验/override/诊断门面;表持有成员按域收进四张域容器 Resource(`config_domain_combat/run/crossrun/presentation.gd`——前三件迭代 41 已建件,第四件表现层布局域 2026-10-30 建件,类型化成员 7/7/5/1);三张 resolver 表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers)经 `load_tables` 装配段注入各容器,resolver 文件与键零搬移**——红线不变:本类仍是唯一加载与持有单点,"经 ConfigDB 单点读取"=经其持有的表对象 | 类型化域容器属性(每域一张容器);公开面 8(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload)、类变量 10 | 无(基础层顶点) | Resource/.tres 加载、autoload 生命周期——LOW | | **SlotStore**(`core/slot_store.gd`,D3 存档槽位域件,已建件——迭代 41 存量债批) | 存档三槽位文件域唯一所有者(自 save_manager 收编槽位族):槽位挂载与模板、激活与建删、槽位摘要窥视(peek 族);改名覆盖原语(_rename_overwrite)留 save_manager(IO 原语单点) | 槽位读写原语族+摘要投影(公开 10/类变量 4;SaveManager 与菜单消费方经 `save.slots.` 直连) | save_manager(注入路径与模板配置);FileAccess——LOW | | **MetaStateStore**(`core/meta_state_store.gd`,D3 存档内元进度数据件,已建件——迭代 41 存量债批) | 存档内元进度数据的读写唯一层(自 save_manager 收编 accessor 族:钥石/道具解锁/树节点与叉侧/锻造路线/同调/真视/计数器/摘要——数据读写原语,不含业务规则);save_manager 与 meta_progression 双 God Class 的共同病根=该数据域摊在两文件,本件即其归一 | 写口原语 13+只读投影 6(tree_state/forge_state/attune_state/career_state 四投影+keystone_balance/item_unlocked)=公开 19(add_keystone 私有 `_add_keystone`),零 _data 副本零缓存;MetaProgression 业务层与营地/锻造 UI 消费方经 `save.meta_state.` 直连(meta 的纯转发读方法已退役) | save_manager(序列化接线);FileAccess——LOW | | **RngService**(autoload,D2) | master_seed、各流 scope 计数器(封闭流词表现值以 `RngServiceCore.PURPOSE_TAGS` 为准;增流走 P1 评审) | `set_master_seed()`(唯一调用方 C2);`open_stream(stream)` 有状态门面;`derive_at(stream, scope)` 纯函数;只读计数器 | ConfigDB(无) | 无——纯逻辑 | diff --git a/design/gdd/systems/表现与支撑/HUD框架.md b/design/gdd/systems/表现与支撑/HUD框架.md index 66474843..48734014 100644 --- a/design/gdd/systems/表现与支撑/HUD框架.md +++ b/design/gdd/systems/表现与支撑/HUD框架.md @@ -55,13 +55,13 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻 三区各管一块: -| 区 | 位置与尺寸 | 管什么 | +| 区 | 位置与几何 | 管什么 | |---|---|---| -| B 战斗画面 | 全宽 × 270px | 主角站桩立绘、敌人站桩立绘 ×1–3(**全站桩形态,2026-09-20 用户拍板:无框立绘+底座、状态小图标挂血条下,英雄卡/敌人卡退役**)、轮次阶段指示、结算明细带 | -| C 角色信息 | 左下 240×530 | 生命 / 攻击 / 储备数据框、状态效果槽、小人立绘 | -| D 扫雷图 | 中下约 1036×530(原右下面板删除后横向加宽) | 行动点条与按钮组、棋盘本体、toast 槽 | +| B 战斗画面 | 全宽,高读布局表 `stage_h` | 主角站桩立绘、敌人站桩立绘 ×1–3(**全站桩形态,2026-09-20 用户拍板:无框立绘+底座、状态小图标挂血条下,英雄卡/敌人卡退役**)、轮次阶段指示、结算明细带 | +| C 角色信息 | 左下,宽读 `info_panel_w`,高 = 基准屏高 − B 区高 | 生命 / 攻击 / 储备数据框、状态效果槽、小人立绘 | +| D 扫雷图 | 中下,宽 = 基准屏宽 − C 区宽 − 列间距(原右下面板删除后横向加宽),高同 C 区 | 行动点条与按钮组、棋盘本体、toast 槽 | -分区尺寸与组件落位的像素级裁决归 [battle.md](../../ux/battle.md);三区结构与双舞台常驻是结构冻结项,修订走文档修订而非调参。节点链进度不在战斗屏呈现——它由关卡画面(选路覆盖层常驻形态)的节点态承载,见[关卡节点地图](../冒险循环/关卡节点地图.md)。 +分区几何的唯一事实源 = `game/data/tables/battle_layout.tres`(经 `ConfigDb.presentation.battle_layout` 读取);分区尺寸与组件落位的像素级裁决归 [battle.md](../../ux/battle.md),两份文档只写结构关系、不复写数值。三区结构与双舞台常驻是结构冻结项,修订走文档修订而非调参。节点链进度不在战斗屏呈现——它由关卡画面(选路覆盖层常驻形态)的节点态承载,见[关卡节点地图](../冒险循环/关卡节点地图.md)。 **结论**:棋盘格边长由 [battle.md](../../ux/battle.md)「棋盘格尺寸公式」按棋盘区实测面积与板型算出;本文只判"塞不塞得进",不复制算式与算例。**分辨率档位(2026-09-30 迭代 9 三裁决案甲修订)**:两档=1280×800 基准档(本表全部定值的口径)+1920×1080 档=**全局等比**(stretch canvas_items+aspect keep——16:10 设计在 16:9 窗口为等比 1.35×+左右黑边各 ~96px);本节与各屏定值一律按 1280 基准档书写,1920 档由引擎等比承载、零重排。 @@ -74,9 +74,9 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻 | 变量 | 含义 | 现行值 | |---|---|---| | N | 棋盘边长格数 | 读[棋盘生成](../排雷循环/棋盘生成.md)(现行四档;事实源 board_gen.tres) | -| 棋盘区宽 / 高 | 扫雷图区基准尺寸 | 由 D 区尺寸扣行动点条 44px 得出,本表不复制现算值 | +| 棋盘区宽 / 高 | 扫雷图区基准尺寸 | 由 D 区尺寸扣行动点条高(读 `ap_bar_h`)得出,本表不复制现算值 | -装板可行性由上面的不等式保证;格边长的现行算例与可读下限见 [battle.md](../../ux/battle.md)。棋盘数字直接显示排雷玩法给的真值——呈现层禁止自己算数字,这是"数据层从不说谎"在呈现侧的镜像。 +装板可行性由上面的不等式保证;格边长的可读下限见 [battle.md](../../ux/battle.md),算例由样张渲染脚本按布局表现算、不入文档。棋盘数字直接显示排雷玩法给的真值——呈现层禁止自己算数字,这是"数据层从不说谎"在呈现侧的镜像。 ### 提示与弹层:轻提示随起随消、重弹层排队进出 @@ -244,7 +244,7 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻 ## 调参表 -本系统的参数全部是呈现节奏与可读性常量,不是玩法数值——玩法数值一律归[数据配置](数据配置.md)的表,与本表无关。修订本文常量走文档修订流程。**本表常量无数据表承载,为文档面唯一事实源;入驻 .tres 时本列改指路。** +本系统的参数全部是呈现节奏与可读性常量,不是玩法数值——玩法数值一律归[数据配置](数据配置.md)的表,与本表无关。修订本文常量走文档修订流程。**本表常量无数据表承载,为文档面唯一事实源;入驻 .tres 时本列改指路。** 布局几何(B 区高 / C 区宽 / AP 条高 / 明细带两态高)已入驻 `game/data/tables/battle_layout.tres`,不在本表重复。 | 参数 | 域 | 现行值(文档自持,无表承载) | 说明 | |---|---|---|---| @@ -254,7 +254,6 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻 | clear_celebration_ms | 每次清图 | 3000–5000 | 上限约束:三五秒即收 | | font_min_board / font_min_detail / font_min_tooltip | 1280×800 基座 | 18 / 14 / 13 px | 可访问性基线 | | reduce_motion_scale | 每段动画 | 0.5 或跳终态 | 减动效开关系数 | -| detail_band_height | 结算明细带 | 24 / 84 px | 摘要 / 实时流两态高度 | 结构冻结(非调参项,改动走文档修订):三区布局与分区尺寸、双舞台常驻、单意图帧纪律、弹层层级纪律、图标键映射契约、程序化占位策略、订阅纪律。性能预算为引用值:同屏 draw calls < 200、进入战斗屏 500ms 内可交互。 @@ -262,7 +261,7 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻 以下判据逐条可测,全部通过才算闭环达标。 -- 三区无遮挡、无重叠、无内容裁切(棋盘全部格子与数字完整可见),分区尺寸与 battle.md 布局表一致;同屏 draw calls < 200;进入战斗屏 500ms 内首格可点。 +- 三区无遮挡、无重叠、无内容裁切(棋盘全部格子与数字完整可见),分区几何与布局表 `battle_layout.tres` 一致;同屏 draw calls < 200;进入战斗屏 500ms 内首格可点。 - 任意板型装板后网格实高不超出棋盘区:N × 格边长 + (N−1) × 2px ≤ 棋盘区可用高;数字字号与整数倍缩放判据见 battle.md。 - 已开 / 未开格态在灰度截图下可逐格分类;已引爆危险格"可通行可读"(已开底加巢穴纹、可作移动目标)。 - 类型微光:微光只在英雄周围 8 格内的未开格显示;出圈即灭、入圈即亮;已开格与封锁格恒无微光;微光呈现不随格是雷是安变化(不泄危险格与安全格)。(迭代 30 卡①实装后转正式验收) diff --git a/design/gdd/systems/表现与支撑/数据配置.md b/design/gdd/systems/表现与支撑/数据配置.md index b2106b48..f634f004 100644 --- a/design/gdd/systems/表现与支撑/数据配置.md +++ b/design/gdd/systems/表现与支撑/数据配置.md @@ -70,6 +70,7 @@ | 关卡表 | 逐关节点图与层名 | [冒险结构](../冒险循环/冒险结构.md) | | 冒险表 | 关卡大奖励与钥石入账(单行全局) | [冒险结构](../冒险循环/冒险结构.md) | | 元进度表 | 元进度字段(已建表 2026-09-21——现行五行族:属性锻造 / Boss 钥石 / 工坊阈值 / 换算率 / 同调刷新升级行族,顺带扩列) | [元进度](../跨局循环/元进度.md) | +| 战斗屏布局表(`battle_layout.tres`) | 分区几何与明细带两态高(单行;呈现层布局配置,非玩法数值——供呈现层与样张工具同源读取) | [战斗屏规格](../../ux/battle.md)(结构)/[HUD框架](HUD框架.md)(呈现常量) | 举证:本节为数据纪律约定,无计算。违规检测 = 静态扫描加人工复核,随各表回填定稿与里程碑检查执行。 diff --git a/design/gdd/ux/interaction-patterns.md b/design/gdd/ux/interaction-patterns.md index be842533..420d68b7 100644 --- a/design/gdd/ux/interaction-patterns.md +++ b/design/gdd/ux/interaction-patterns.md @@ -81,7 +81,7 @@ MVP 未使用标准控件(不入库):toggle / dropdown / input field / tab **Category**: Data Display **Used In**: 战斗画面区底部(结算期第一注视点) -**Description**: 轮末结算逐行流水实时呈现 + 可回看——「扫雷之因逐行兑现为战果」(P2 归因)的承载面。挖掘期收起为 1 行上轮摘要;结算期展开 84px 实时流;点击开完整回看 overlay。 +**Description**: 轮末结算逐行流水实时呈现 + 可回看——「扫雷之因逐行兑现为战果」(P2 归因)的承载面。挖掘期收起为 1 行上轮摘要;结算期展开为实时流(带高读 `log_band_h_settle`);点击开完整回看 overlay。 **Specification**: - 数据源:[战斗结算](../systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)步骤流(9 类行封闭协议(现行);公共字段:来源类别 / 来源格 / 轮次 / 显示名 / 数值 / 可空高光标记);**无来源行 = 断言失败** diff --git a/game/core/config_db.gd b/game/core/config_db.gd index dfde4ba6..4a6d4f8d 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -1,8 +1,9 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 数值表单点访问入口:全部 .tres 表的加载、校验诊断与门面(垂直切片验证:改表即改玩法,数值零硬编码)。 ## 玩法代码禁止绕过本单点自建 load();行查询归各表类(表管自己的行), -## 表持有按域收进三容器(combat 战斗编排域/run 冒险 runner 域/crossrun 跨局元进度域), -## 消费面经 db.<容器>.<表> 直连。「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88)。 +## 表持有按域收进四容器(combat 战斗编排域/run 冒险 runner 域/crossrun 跨局元进度域/ +## presentation 表现层布局域),消费面经 db.<容器>.<表> 直连。 +## 「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88)。 ## 冒烟脚本可显式实例化本类(headless --script 亦会加载 autoload——共享 .tres 上的 resolver 归最后装载的实例); ## 加载失败 is_ready=false 加 fatal 封印,调用方须查。 class_name ConfigDb @@ -28,11 +29,13 @@ const TABLE_MANIFEST := { "adventure": "adventure.tres", # AdventureTable(大奖励与钥石行值单点) "feats": "feats.tres", # FeatTable 规则改写型特性(定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动) "forge": "forge.tres", # ForgeTable——两根类型轴默认值+六路线行族;攻防所有权切分唯一数据面 + "battle_layout": "battle_layout.tres", # BattleLayoutTable——战斗屏分区几何与明细带高度单行 } var combat: ConfigDomainCombat # 战斗编排域七表(board_gen/variation/status_effects/enemy/skill/hero/settlement) var run: ConfigDomainRun # 冒险 runner 域七表(level/difficulty/level_gen_params/reward/adventure/action_economy/shop) var crossrun: ConfigDomainCrossrun # 跨局元进度域五表(meta/meta_tree/treasure_items/feats/forge) +var presentation: ConfigDomainPresentation # 表现层布局域一表(battle_layout) var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} @@ -45,6 +48,7 @@ func _init() -> void: combat = ConfigDomainCombat.new() run = ConfigDomainRun.new() crossrun = ConfigDomainCrossrun.new() + presentation = ConfigDomainPresentation.new() _resolver = ConfigDbResolver.new(push_diag, func() -> Variant: return combat.board_gen.rows, func() -> Variant: return combat.enemy.rows, func() -> Variant: return run.difficulty) # 协作类持门面引用(诊断/致命通道回呼) @@ -117,7 +121,14 @@ func load_tables(root: String, collect_only: bool) -> bool: if has_errors and not collect_only: # 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建 return false - # 容器装配(19 表成员按域收进三容器——2026-10-27 全清设计定稿) + # 容器装配(表行按域收进四容器+resolver 注入)归私有协函数——load_tables 只留加载/校验/致命门面 + _assign_loaded(loaded) + is_ready = not has_errors + return not has_errors + + +# 容器装配协函数:20 表成员按域收进四容器(2026-10-27 全清设计定稿,2026-10-31 增表现层布局域) +func _assign_loaded(loaded: Dictionary) -> void: combat.board_gen = loaded.get("board_gen") combat.variation = loaded.get("variation") combat.status_effects = loaded.get("status_effects") @@ -137,12 +148,11 @@ func load_tables(root: String, collect_only: bool) -> bool: crossrun.treasure_items = loaded.get("treasure_items") crossrun.feats = loaded.get("feats") crossrun.forge = loaded.get("forge") + presentation.battle_layout = loaded.get("battle_layout") # resolver 协作件注入三表(resolve 委托承载——resolver 文件与键零搬移) combat.board_gen._resolver = _resolver combat.enemy._resolver = _resolver run.difficulty._resolver = _resolver - is_ready = not has_errors - return not has_errors # 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体: diff --git a/game/core/config_domain_presentation.gd b/game/core/config_domain_presentation.gd new file mode 100644 index 00000000..b3cf13b3 --- /dev/null +++ b/game/core/config_domain_presentation.gd @@ -0,0 +1,6 @@ +## 表现层布局域容器(D2 域容器——config_db 表持有按域收敛):战斗屏布局一表类型化持有。 +## 零方法零行为——纯持有结构;「经 ConfigDB 单点读取」=经其持有的容器与表对象(架构 :88 定稿注记)。 +class_name ConfigDomainPresentation +extends Resource + +var battle_layout: BattleLayoutTable diff --git a/game/core/config_domain_presentation.gd.uid b/game/core/config_domain_presentation.gd.uid new file mode 100644 index 00000000..3e155efc --- /dev/null +++ b/game/core/config_domain_presentation.gd.uid @@ -0,0 +1 @@ +uid://bay1b1s3lrd1e diff --git a/game/data/schemas/battle_layout_table.gd b/game/data/schemas/battle_layout_table.gd new file mode 100644 index 00000000..2095fb2d --- /dev/null +++ b/game/data/schemas/battle_layout_table.gd @@ -0,0 +1,30 @@ +## 战斗屏布局表 schema:分区几何与明细带高度的单行类型化读取(呈现层布局配置,非玩法数值)。 +## 消费面 = BattleStageView / BattleLogView / HeroInfoPanelView / ApBarView(经 ConfigDb.presentation.battle_layout); +## 文档只写结构关系与表名,不复制数值。validate() 违例加载期致命(fail-fast)。 +class_name BattleLayoutTable +extends Resource + +@export var stage_h: int = 252 # B 区高(全宽战斗画面,含底部明细带) +@export var info_panel_w: int = 240 # C 区宽(左下角色信息列) +@export var ap_bar_h: int = 44 # D 区 AP 条高 +@export var log_band_h_dig: int = 24 # 明细带挖掘期收起高(单行上轮摘要) +@export var log_band_h_settle: int = 84 # 明细带结算期展开高(实时结算流) + + +func validate() -> Array: + var errs: Array[Dictionary] = [] + if stage_h < 120 or stage_h > 600: + errs.append({"category": "domain", "message": "stage_h 越域 [120,600]: %d" % stage_h}) + if info_panel_w < 120 or info_panel_w > 480: + errs.append({"category": "domain", "message": "info_panel_w 越域 [120,480]: %d" % info_panel_w}) + if ap_bar_h < 24 or ap_bar_h > 96: + errs.append({"category": "domain", "message": "ap_bar_h 越域 [24,96]: %d" % ap_bar_h}) + if log_band_h_dig < 16 or log_band_h_dig > 80: + errs.append({"category": "domain", "message": "log_band_h_dig 越域 [16,80]: %d" % log_band_h_dig}) + if log_band_h_settle < 24 or log_band_h_settle > 160: + errs.append({"category": "domain", "message": "log_band_h_settle 越域 [24,160]: %d" % log_band_h_settle}) + if log_band_h_settle < log_band_h_dig: + errs.append({"category": "cross_field", "message": "log_band_h_settle(%d) < log_band_h_dig(%d)——展开态不得矮于收起态" % [log_band_h_settle, log_band_h_dig]}) + if log_band_h_settle >= stage_h: + errs.append({"category": "cross_field", "message": "log_band_h_settle(%d) ≥ stage_h(%d)——明细带不得占满 B 区" % [log_band_h_settle, stage_h]}) + return errs diff --git a/game/data/schemas/battle_layout_table.gd.uid b/game/data/schemas/battle_layout_table.gd.uid new file mode 100644 index 00000000..5872dd30 --- /dev/null +++ b/game/data/schemas/battle_layout_table.gd.uid @@ -0,0 +1 @@ +uid://b3jh66yq4b7rp diff --git a/game/data/tables/battle_layout.tres b/game/data/tables/battle_layout.tres new file mode 100644 index 00000000..0a784bc0 --- /dev/null +++ b/game/data/tables/battle_layout.tres @@ -0,0 +1,11 @@ +[gd_resource type="Resource" script_class="BattleLayoutTable" load_steps=2 format=3] + +[ext_resource type="Script" path="res://data/schemas/battle_layout_table.gd" id="1"] + +[resource] +script = ExtResource("1") +stage_h = 252 +info_panel_w = 240 +ap_bar_h = 44 +log_band_h_dig = 24 +log_band_h_settle = 84 diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 9540e8c6..0619569c 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -634,7 +634,7 @@ func _refresh_presentations() -> void: func _sync_settlement_visuals() -> void: - # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px + # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开(高度读 battle_layout) var settling := _is_settling() if settling and not run_scope.was_settling: ui.battle.settlement_fx.play_round_sweep(ui.battle.board_zone.board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根 diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index e0d6909d..829b89e7 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -6,8 +6,8 @@ extends RefCounted # 战斗屏三区(B 战斗画面 / C·D 两列——节点链 -# 进度由关卡画面本体(选路覆盖层整图)承载,28px 纵向空间拨给 C/D 底行 520→548; -# 无顶栏进度条、无道具面板区)。 +# 进度由关卡画面本体(选路覆盖层整图)承载,纵向空间归 C/D 底行; +# 无顶栏进度条、无道具面板区)。分区几何读 ConfigDb.presentation.battle_layout。 # + 结算演出覆盖层(A–D 之上、模态之下——跨区弹道/光带载体)。 # 返回句柄字典:{stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx} # (返回句柄不含 gp_label/keystone_label——GP/钥石不在战斗屏呈现) @@ -17,20 +17,22 @@ static func build_battle_ui(root: Control, db: ConfigDb) -> Dictionary: root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_theme_constant_override("separation", 0) - # B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1) - var stage := BattleStageView.new(db) + var layout: BattleLayoutTable = db.presentation.battle_layout + + # B 区:战斗画面(全宽 × battle_layout.stage_h——常驻双舞台,布局决策 D1) + var stage := BattleStageView.new(db, layout) root.add_child(stage) - # C/D 两列(548px——含顶栏让出的 28px 纵向空间) + # C/D 两列(高度 = 基座高 − B 区高——底行承载) var bottom := HBoxContainer.new() bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL bottom.add_theme_constant_override("separation", 4) root.add_child(bottom) - var info_panel := HeroInfoPanelView.new() + var info_panel := HeroInfoPanelView.new(layout) bottom.add_child(info_panel) - var board_zone := BoardZoneView.new() + var board_zone := BoardZoneView.new(layout) bottom.add_child(board_zone) board_zone.ap_bar.set_refresh_cost(int(db.run.action_economy.rap_refresh_cost)) # RAP 读数表值驱动 diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index f80a4d6d..4045fbf0 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -31,8 +31,8 @@ var _arm_timer := ArmedTimer.new() # 弃图刷新确认窗(T3/D3 P4 乙— const REFRESH_CONFIRM_WINDOW_S := 4.0 # 弃图刷新确认窗(原 tween_interval 字面量升常量——语义零变化) -func _init() -> void: - custom_minimum_size = Vector2(0, 44) +func _init(layout: BattleLayoutTable) -> void: + custom_minimum_size = Vector2(0, layout.ap_bar_h) _arm_timer.expired.connect(_disarm) var sb := UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(8, 4, 8, 4)) add_theme_stylebox_override("panel", sb) diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index 30dd7e2c..10444258 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -4,12 +4,12 @@ class_name BattleLogView extends PanelContainer -const BAND_H_DIG := 24 -const BAND_H_SETTLE := 84 const AGG_TYPES := ["reward_ineffective"] const COL_GOLD := UiKit.HEX_GOLD var _db +var _band_h_dig := 0 +var _band_h_settle := 0 var _log: RichTextLabel var _summary: Label var _hint: Label @@ -18,9 +18,11 @@ var _round_rows: Array[String] = [] # 本结算轮行缓冲(上轮摘要 var _pending_agg := {} # 聚合计数行 pending(type+batch_key → {row, count}) -func _init(db_ref) -> void: +func _init(db_ref, layout: BattleLayoutTable) -> void: _db = db_ref - custom_minimum_size = Vector2(0, BAND_H_DIG) + _band_h_dig = layout.log_band_h_dig + _band_h_settle = layout.log_band_h_settle + custom_minimum_size = Vector2(0, _band_h_dig) size_flags_vertical = Control.SIZE_SHRINK_END var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 2) @@ -29,7 +31,7 @@ func _init(db_ref) -> void: vb.add_child(head) # 明细条灰字对深灰底实测 2.63:1 → 暗底双色墨制=暖白族 _summary = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM) - _summary.custom_minimum_size = Vector2(0, BAND_H_DIG - 4) + _summary.custom_minimum_size = Vector2(0, _band_h_dig - 4) _summary.clip_text = true _summary.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS head.add_child(_summary) @@ -39,7 +41,7 @@ func _init(db_ref) -> void: _log = RichTextLabel.new() _log.bbcode_enabled = true _log.scroll_following = true - _log.custom_minimum_size = Vector2(0, BAND_H_SETTLE - 26) + _log.custom_minimum_size = Vector2(0, _band_h_settle - 26) _log.clip_contents = true _log.scroll_active = true vb.add_child(_log) @@ -53,7 +55,7 @@ func set_mode(mode: String) -> void: var settle := mode == "settle" _log.visible = settle _summary.visible = not settle - custom_minimum_size = Vector2(0, BAND_H_SETTLE if settle else BAND_H_DIG) + custom_minimum_size = Vector2(0, _band_h_settle if settle else _band_h_dig) func mode() -> String: diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index 4682196d..251baffa 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -24,11 +24,11 @@ var _viewport: SubViewport var world: Node2D # StageWorld(preload 脚本实例——Node2D 世界根/字体挂点) -func _init(db_ref) -> void: - custom_minimum_size = Vector2(0, 252) +func _init(db_ref, layout: BattleLayoutTable) -> void: + custom_minimum_size = Vector2(0, layout.stage_h) _build_backdrop() _build_world_stage() - _build_text_half(db_ref) + _build_text_half(db_ref, layout) _build_companion_layer(db_ref) @@ -75,7 +75,7 @@ func _build_world_stage() -> void: ## Control 文本半区域(round_label / stage_center / log_view 原样保留——C1 永久 Control) -func _build_text_half(db_ref) -> void: # type-hint: ignore(db 鸭子型注入——单测以最小 stub 构造) +func _build_text_half(db_ref, layout: BattleLayoutTable) -> void: # type-hint: ignore(db 鸭子型注入——单测以最小 stub 构造) var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) vb.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -86,7 +86,7 @@ func _build_text_half(db_ref) -> void: # type-hint: ignore(db 鸭子型注入 add_child(vb) _build_narr_row(vb) _build_center_row(vb) - log_view = BattleLogView.new(db_ref) + log_view = BattleLogView.new(db_ref, layout) vb.add_child(log_view) diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index b6509338..52390e21 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -18,17 +18,17 @@ signal hover_forbidden(cell: Vector2i) signal chord_attempted(cell: Vector2i) -func _init() -> void: +func _init(layout: BattleLayoutTable) -> void: size_flags_horizontal = Control.SIZE_EXPAND_FILL var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) add_child(vb) - _build_views(vb) + _build_views(vb, layout) ## 视图三件域:AP 条+棋盘区(含动效与 toast 叠层) -func _build_views(vb: VBoxContainer) -> void: - ap_bar = ApBarView.new() +func _build_views(vb: VBoxContainer, layout: BattleLayoutTable) -> void: + ap_bar = ApBarView.new(layout) vb.add_child(ap_bar) var board_area := Control.new() board_area.size_flags_vertical = Control.SIZE_EXPAND_FILL diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 7dd765d7..af1c5387 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -21,8 +21,8 @@ var _board_variant_section: VBoxContainer # 地形变异常驻角标(空态 var _board_variant_list: VBoxContainer -func _init() -> void: - custom_minimum_size = Vector2(240, 0) +func _init(layout: BattleLayoutTable) -> void: + custom_minimum_size = Vector2(layout.info_panel_w, 0) clip_contents = true # 主面板穿石板戏服(纹理本色;数据框仍素面 §7.1;缺位回退 flat) var sb := UiKit.panel_skinned(Vector4(8, 6, 8, 6)) diff --git a/game/probe/fx_probe.gd b/game/probe/fx_probe.gd index cacd9a4f..b26f509b 100644 --- a/game/probe/fx_probe.gd +++ b/game/probe/fx_probe.gd @@ -136,7 +136,7 @@ func _ready() -> void: # ---- ⑥ 主角受击/死亡路由(battle_stage 主角走完整呈现链的断言) ---- var db2 = load("res://core/config_db.gd").new() if db2.load_tables("res://data/tables", false): - var stage2 = BattleStageView.new(db2) + var stage2 = BattleStageView.new(db2, db2.presentation.battle_layout) add_child(stage2) var flash_before: int = int(stage2.hero_standee.fx_probe().get("flash_count", 0)) stage2.on_step_row_fx({"type": "damage", "target": "hero", "killed": false}) diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 96bcac28..842de67b 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -530,7 +530,7 @@ func _career_nojump_contract(main: Node) -> void: problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav) -# ⑥standee 专项:B 区站桩内容矩形 ⊆ 252px 舞台矩形(内缩 8px)且不被裁切 +# ⑥standee 专项:B 区站桩内容矩形 ⊆ 舞台矩形(高度读布局表;内缩 8px)且不被裁切 # (名行基线离盒底 1px 同型高危写法返修回归闸; # 内容矩形=StandeeBody.content_rect(),最低绘制元素=底座下缘) func _standeefit_contract(main: Node) -> void: diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index 28e5093f..ddbb73b3 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -769,7 +769,7 @@ func _smoke_feats() -> void: and orch0.fx_host().fx_emit("mine_hit", {"damage": 5}).is_empty() and orch0.fx_host().fx_emit("battle_won", {"round": 1}).is_empty()) # ⑦c 特性持有呈现段(C 区「特性」节;呈现面在 06 在册) - var panel = load("res://presentation/views/hero_info_panel_view.gd").new() + var panel = load("res://presentation/views/hero_info_panel_view.gd").new(_db.presentation.battle_layout) panel.set_feat_holdings([]) Kit.check("特性段空态隐藏(视图空态纪律同特性段)", not panel._feat_section.visible) panel.set_feat_holdings(["疼痛记忆 ×1", "双管镐 ×1"]) @@ -788,7 +788,7 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: autoplay._drive_level(a.runner(), st, null, "normal", "min") if String(st["result"]) != "": return false - if a._pending_next_level: + if a._bp_store._pending_next_level: # 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化) if not a.stop()._pending.is_empty(): var bcands: Array = a.stop()._pending["candidates"] diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 641f12ba..c17b8881 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -78,7 +78,7 @@ func _init() -> void: # 账本保留(契约 1):主动件充能与每冒险种行入口零改动(单源化后=差额种行幂等——不吞装配) a._attunement = ["incense_stick"] a._loadout.set_attunement(["incense_stick"]) - a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支) + a._bp_store._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支——成员随断点件收编改址) a._loadout_on_level_inventory() # 种行(首关 bind 时点语义) var incense_left := 0 for r in a.runner().treasure_inventory.snapshot_inventory(): @@ -289,7 +289,7 @@ func _init() -> void: # A12 呈现两态文案与 _format_row 一致:两态均零「雷」字面 var LogViewScript := load("res://presentation/views/battle_log_view.gd") - var lv = LogViewScript.new(db) + var lv = LogViewScript.new(db, db.presentation.battle_layout) var broken_row := {"type": "seal_miss", "source_class": "seal_miss", "origin_cell": [0, 0], "round_index": 1, "display_name": "封印未中", "value": 0, "highlight": "perfect_broken"} var forgiven_row := {"type": "seal_miss", "source_class": "seal_miss", "origin_cell": [0, 0], diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 350f31bd..45069651 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1477,7 +1477,7 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: autoplay._drive_level(a.runner(), st, null, "normal", "min") if String(st["result"]) != "": return false - if a._pending_next_level: + if a._bp_store._pending_next_level: # 三选一停点确认(确定性选件后推进,与 run_adventure 自动策略同口径;TP0 令牌化) if not a.stop()._pending.is_empty(): var bcands: Array = a.stop()._pending["candidates"] diff --git a/game/tests/unit/presentation/pacing_queue_test.gd b/game/tests/unit/presentation/pacing_queue_test.gd index 7890d23f..2da70b5f 100644 --- a/game/tests/unit/presentation/pacing_queue_test.gd +++ b/game/tests/unit/presentation/pacing_queue_test.gd @@ -126,7 +126,7 @@ func _init() -> void: UiKit.motion_scale = 1.0 # ---- 断言④:排播两处取用按行 target_id 解析站桩(d12 排播半——假 stage 记参)---- var rp = RowPacerScript.new() - var fstage := FakeStage.new(null) + var fstage := FakeStage.new(null, root.get_node("/root/ConfigDB").presentation.battle_layout) var ffx := FakeFx.new() rp.stage = fstage rp.board_view = main.ui.battle.board_zone.board_view diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd index 04e389b9..40fa7d12 100644 --- a/game/tests/unit/presentation/player_copy_sweep_test.gd +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -41,7 +41,7 @@ func _init() -> void: # ① C 区两段头(u1/u2——直建 hero 面板,adventure_feats_test ⑦c 族同构) func _sweep_hero_panel() -> void: - var panel = load("res://presentation/views/hero_info_panel_view.gd").new() + var panel = load("res://presentation/views/hero_info_panel_view.gd").new(_db.presentation.battle_layout) var feat_head := _first_label(panel._feat_section) var curio_head := _first_label(panel._curio_section) Kit.check("C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕", @@ -95,7 +95,7 @@ func _sweep_source_names() -> void: row_view._hide_tip() Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕", String(_db.combat.settlement.core.source_label_map.get("mine_hit", "")) == "危险格") - var log_view = load("res://presentation/views/battle_log_view.gd").new(_db) + var log_view = load("res://presentation/views/battle_log_view.gd").new(_db, _db.presentation.battle_layout) var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit", "display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1})) Kit.check("战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕", diff --git a/game/tests/unit/presentation/standee_fx_sweep_test.gd b/game/tests/unit/presentation/standee_fx_sweep_test.gd index 8e23992a..9c372832 100644 --- a/game/tests/unit/presentation/standee_fx_sweep_test.gd +++ b/game/tests/unit/presentation/standee_fx_sweep_test.gd @@ -72,7 +72,7 @@ func _init() -> void: UiKit.motion_scale = 1.0 # ---- ⑤ 站桩按 id 解析(两口共用同名字敌载体)---- - var stage = StageScript.new(null) + var stage = StageScript.new(null, load("res://data/tables/battle_layout.tres")) root.add_child(stage) var s1 = StandeeBodyScript.new("enemy") var s2 = StandeeBodyScript.new("enemy") diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 4b02313f..7c8a8d42 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -383,7 +383,7 @@ func _smoke_type_glow() -> void: # 本段把现行行形态逐型喂进真实视图:拆行残留与行字段错位在此转红)---- func _smoke_battle_log_view() -> void: print("--- 战斗日志视图行格式化(行协议 9 类呈现层同步闸)---") - var view = load("res://presentation/views/battle_log_view.gd").new(_db) + var view = load("res://presentation/views/battle_log_view.gd").new(_db, _db.presentation.battle_layout) # 敌方行动伤害行(现行形态:baseline 在场、attach 已出协议——修复前此行即 Invalid access) view.append_row({"type": "damage", "display_name": "地精 撕咬", "value": 2, "target": "主角", "baseline": 3, "roll": 15, "ac": 12, "tier": 100, diff --git a/tools/layout_config.py b/tools/layout_config.py new file mode 100644 index 00000000..28436e0c --- /dev/null +++ b/tools/layout_config.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""战斗屏布局表读取(`game/data/tables/battle_layout.tres` 单点)。 + +样张渲染工具不另立布局常量:B 区高 / C 区宽 / AP 条高 / 明细带两态高一律读该表, +与游戏侧 `ConfigDb.presentation.battle_layout` 同源。 +""" +import os +import re + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TABLE_PATH = os.path.join(ROOT, "game", "data", "tables", "battle_layout.tres") +FIELDS = ("stage_h", "info_panel_w", "ap_bar_h", "log_band_h_dig", "log_band_h_settle") + + +def load(path=TABLE_PATH): + with open(path, encoding="utf-8") as f: + text = f.read() + vals = {} + for key in FIELDS: + m = re.search(r"^%s\s*=\s*(\d+)\s*$" % key, text, re.M) + if m is None: + raise ValueError("battle_layout.tres 缺字段或非整数值: %s" % key) + vals[key] = int(m.group(1)) + return vals diff --git a/tools/render_sample_sheet.py b/tools/render_sample_sheet.py index 922e5937..0d22c956 100644 --- a/tools/render_sample_sheet.py +++ b/tools/render_sample_sheet.py @@ -35,9 +35,21 @@ import re import hashlib from collections import deque +from layout_config import load as load_layout + ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAIN_ROOT = "D:/code/g-plan2" +CANVAS_W = 1280 +CANVAS_H = 800 +_LAYOUT = load_layout() +B_H = _LAYOUT["stage_h"] # B 区高(布局表单点——与游戏侧同源) +C_W = _LAYOUT["info_panel_w"] # C 区宽 +D_X = C_W + 4 # D 区左边 = C 区宽 + 列间距 +D_W = CANVAS_W - D_X # D 区宽 +BAND_DIG = _LAYOUT["log_band_h_dig"] # 明细带收起高 +AP_H = _LAYOUT["ap_bar_h"] # D 区 AP 条高 + def asset_root(*parts): """素材路径两级回退:worktree → 主检出(素材不入 git,fresh worktree 缺位)。""" @@ -638,10 +650,11 @@ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", def render_battle_screen(): """B/C/D 三区全内容 1280×800 mockup。""" - scr = Image.new("RGBA", (1280, 800), (24, 20, 17, 255)) + scr = Image.new("RGBA", (CANVAS_W, CANVAS_H), (24, 20, 17, 255)) d = ImageDraw.Draw(scr) - # ── B 区 252px 全宽 ── - d.rectangle([0, 0, 1280, 252], fill=UIKIT["PANEL_STAGE"], outline=UIKIT["PANEL_BORDER"]) + c_y = B_H + 4 + # ── B 区(高读布局表)全宽 ── + d.rectangle([0, 0, CANVAS_W, B_H], 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), @@ -650,33 +663,34 @@ def render_battle_screen(): draw_standee(scr, 973, 34, "enemy", "熊地精", "bugbear", order=2, armor="light", hp=0.8, intent="charge", statuses=("alert",)) draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff") - # 明细带(挖掘期收起 24px) - d.rectangle([0, 228, 1280, 252], fill=(90, 80, 66)) - d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2, + # 明细带(挖掘期收起——高读布局表) + d.rectangle([0, B_H - BAND_DIG, CANVAS_W, B_H], fill=(90, 80, 66)) + d.text((10, B_H - BAND_DIG // 2), "── 第 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)) + # ── C 区(宽读布局表)── + d.rectangle([0, c_y, C_W, CANVAS_H], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"]) + d.rectangle([8, c_y + 8, C_W - 8, c_y + 84], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线) + d.text((16, c_y + 20), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) + d.text((16, c_y + 42), "(+50% 增伤 ×1)", font=F_LBL2, fill=(140, 90, 30)) + d.text((16, c_y + 62), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) + d.text((8, c_y + 96), "魔法道具槽(预留)", 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.rectangle([8 + i * 40, c_y + 112, 8 + i * 40 + 34, c_y + 146], outline=(120, 106, 88), width=1) + d.text((8, c_y + 160), "本关强化列表(预留)", 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") + d.rectangle([D_X, c_y, CANVAS_W, CANVAS_H], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"]) + # 顶行:停手/图例/刷新+AP 点阵+RAP(坐标随 C 区宽平移) + dx = D_X + 8 + d.rectangle([dx, c_y + 6, dx + 52, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((dx + 26, c_y + 17), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([dx + 58, c_y + 6, dx + 102, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((dx + 80, c_y + 17), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([dx + 108, c_y + 6, dx + 176, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((dx + 142, c_y + 17), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm") for i in range(6): col = (232, 200, 120) if i < 4 else (120, 106, 88) - d.ellipse([440 + i * 18, 266, 452 + i * 18, 278], fill=col) - d.text((552, 273), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") + d.ellipse([dx + 188 + i * 18, c_y + 10, dx + 200 + i * 18, c_y + 22], fill=col) + d.text((dx + 300, c_y + 17), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") return scr @@ -938,8 +952,8 @@ def main(): # 三区 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 + bx = D_X + (D_W - board.size[0]) // 2 + by = B_H + 4 + AP_H + (CANVAS_H - B_H - 4 - AP_H - 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 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)", diff --git a/tools/render_sample_sheet_v3.py b/tools/render_sample_sheet_v3.py index ed7f2dcd..ae560eb0 100644 --- a/tools/render_sample_sheet_v3.py +++ b/tools/render_sample_sheet_v3.py @@ -40,7 +40,7 @@ v3.0 口径(方向稿=目检先行闸,冻结归机检): 迭代 43 扩展(2026-10-29 战斗一屏样张批):整屏样张两版出口(1280×800 基座 + 1920×1080 全局等比 1.35×+左右黑边,§8.2 1920 档策略)+ art-bible §1.4 整屏四项断言(S1–S4)+ 战斗表现五项断言(P1–P5) + S3b 图类型色板变体第二套 + S5 三组关键帧条 + S6 描边同屏对照与遮挡对照组 + V1 两版尺寸断言 -+ V10 改前/改后基线并排对拍;B 区高按代码现状 252 出样(art-bible §8.2 文 270 差异只登记不修)。 ++ V10 改前/改后基线并排对拍;分区几何读 game/data/tables/battle_layout.tres(与游戏侧同源)。 """ from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont import os @@ -50,6 +50,8 @@ import re import hashlib from collections import deque +from layout_config import load as load_layout + ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAIN_ROOT = "D:/code/g-plan2" @@ -845,14 +847,22 @@ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light", canvas.alpha_composite(gray, (ox, oy)) -B_H = 252 # B 区高=代码现状(battle_stage_view custom_minimum_size 252;§8.2 文 270 差异只登记不修) CANVAS_W = 1280 # 基座档(art-bible §8.1) +CANVAS_H = 800 +_LAYOUT = load_layout() +B_H = _LAYOUT["stage_h"] # B 区高(布局表单点——与游戏侧同源) +C_W = _LAYOUT["info_panel_w"] # C 区宽 +D_X = C_W + 4 # D 区左边 = C 区宽 + 列间距 +D_W = CANVAS_W - D_X # D 区宽 +BAND_DIG = _LAYOUT["log_band_h_dig"] # 明细带收起高 +BAND_SETTLE = _LAYOUT["log_band_h_settle"] +AP_H = _LAYOUT["ap_bar_h"] # D 区 AP 条高 -def draw_base(canvas_w=CANVAS_W, canvas_h=800, b_h=B_H): - """样张底板(T1):B/C/D 三区骨架——分区数值按 battle.md 指路、B 区按代码 252;格边长=CELL_D 45 +def draw_base(canvas_w=CANVAS_W, canvas_h=CANVAS_H, b_h=B_H): + """样张底板(T1):B/C/D 三区骨架——分区几何全部读布局表 battle_layout.tres;格边长=CELL_D 45 (D 区 10×10 板实比,§8.1)。返回 (img, zones)——zones 供 S/P 断言取样: - b=(0,0,w,b_h) 含明细带;band=明细带 24px;c=(0,b_h+4,240,h);d=(244,b_h+4,w,h)。""" + b=(0,0,w,b_h) 含明细带;band=明细带收起高;c=(0,b_h+4,C_W,h);d=(D_X,b_h+4,w,h)。""" img = Image.new("RGBA", (canvas_w, canvas_h), (24, 20, 17, 255)) d = ImageDraw.Draw(img) c_y = b_h + 4 @@ -865,25 +875,26 @@ def draw_base(canvas_w=CANVAS_W, canvas_h=800, b_h=B_H): a = int(26 * (1 - i / 60.0)) db.rectangle([bx0 - 46 + i // 3, 0, bx0 + 46 - i // 3, 40 + i * 3], fill=(255, 236, 200, a)) img.alpha_composite(beam, (0, 0)) - # 明细带(挖掘期收起 24px) - d.rectangle([0, b_h - 24, canvas_w, b_h], fill=(46, 39, 32)) + # 明细带(挖掘期收起——高读布局表) + d.rectangle([0, b_h - BAND_DIG, canvas_w, b_h], fill=(46, 39, 32)) # C 区 - d.rectangle([0, c_y, 240, canvas_h], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"]) + d.rectangle([0, c_y, C_W, canvas_h], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"]) # D 区 - d.rectangle([244, c_y, canvas_w, canvas_h], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"]) - # D 区顶行工具件(停手/图例/刷新+AP 点阵+RAP) - d.rectangle([252, c_y + 6, 304, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) - d.text((278, c_y + 17), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm") - d.rectangle([310, c_y + 6, 354, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) - d.text((332, c_y + 17), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm") - d.rectangle([360, c_y + 6, 428, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) - d.text((394, c_y + 17), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([D_X, c_y, canvas_w, canvas_h], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"]) + # D 区顶行工具件(停手/图例/刷新+AP 点阵+RAP——坐标随 C 区宽平移) + dx = D_X + 8 + d.rectangle([dx, c_y + 6, dx + 52, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((dx + 26, c_y + 17), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([dx + 58, c_y + 6, dx + 102, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((dx + 80, c_y + 17), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm") + d.rectangle([dx + 108, c_y + 6, dx + 176, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88)) + d.text((dx + 142, c_y + 17), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm") for i in range(6): col = (232, 200, 120) if i < 4 else (120, 106, 88) - d.ellipse([440 + i * 18, c_y + 10, 452 + i * 18, c_y + 22], fill=col) - d.text((552, c_y + 17), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") - zones = {"b": (0, 0, canvas_w, b_h), "band": (0, b_h - 24, canvas_w, b_h), - "c": (0, c_y, 240, canvas_h), "d": (244, c_y, canvas_w, canvas_h)} + d.ellipse([dx + 188 + i * 18, c_y + 10, dx + 200 + i * 18, c_y + 22], fill=col) + d.text((dx + 300, c_y + 17), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm") + zones = {"b": (0, 0, canvas_w, b_h), "band": (0, b_h - BAND_DIG, canvas_w, b_h), + "c": (0, c_y, C_W, canvas_h), "d": (D_X, c_y, canvas_w, canvas_h)} return img, zones @@ -901,24 +912,25 @@ def render_battle_screen(board_img=None): statuses=("alert",)) draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff") # 明细带摘要行 + 结算语境 hero 剪影 mini(P2 三语境之一;右端贴装不越缘) - d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2, + d.text((10, b_h - BAND_DIG // 2), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2, fill=(240, 226, 200), anchor="lm") mini = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST) - scr.alpha_composite(mini, (1236, b_h - 24)) - # ── C 区内容(次级文对齐 §7.2 ≥4.5:1——STONE_DEEP/GOLD_INK 在册墨) ── - d.rectangle([8, 264, 232, 340], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线) - d.text((16, 276), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) - d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(116, 80, 11)) - d.text((16, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) - d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(85, 77, 65)) + scr.alpha_composite(mini, (CANVAS_W - 44, b_h - BAND_DIG)) + # ── C 区内容(次级文对齐 §7.2 ≥4.5:1——STONE_DEEP/GOLD_INK 在册墨;y 随 B 区高平移) ── + cy = b_h + 4 + d.rectangle([8, cy + 8, C_W - 8, cy + 84], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线) + d.text((16, cy + 20), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) + d.text((16, cy + 42), "(+50% 增伤 ×1)", font=F_LBL2, fill=(116, 80, 11)) + d.text((16, cy + 62), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22)) + d.text((8, cy + 96), "魔法道具槽(预留)", font=F_LBL2, fill=(85, 77, 65)) for i in range(4): - d.rectangle([8 + i * 40, 368, 8 + i * 40 + 34, 402], outline=(120, 106, 88), width=1) - d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(85, 77, 65)) + d.rectangle([8 + i * 40, cy + 112, 8 + i * 40 + 34, cy + 146], outline=(120, 106, 88), width=1) + d.text((8, cy + 160), "本关强化列表(预留)", font=F_LBL2, fill=(85, 77, 65)) # ── D 区主样张板(10×10@45px=450×450+悬崖 8)嵌进工具行之下居中 ── board_origin = None if board_img is not None: - bx = 244 + (1036 - board_img.size[0]) // 2 - by = b_h + 4 + 34 + (800 - b_h - 4 - 34 - board_img.size[1]) // 2 + bx = D_X + (D_W - board_img.size[0]) // 2 + by = cy + AP_H + (CANVAS_H - cy - AP_H - board_img.size[1]) // 2 scr.paste(board_img.convert("RGB"), (bx, by), board_img) board_origin = (bx, by) return scr, zones, board_origin @@ -1644,7 +1656,7 @@ def main(): d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0;迭代 43 扩展:整屏两版+S1–S4/P1–P5 断言)——机检 %s(方向稿参考,冻结归目检后机检批)" % ("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE) canvas.paste(scr, (20, 52)) - d.text((20, 866), "三区 mockup(1280×800 实比,B 区按代码 252):B 区站桩(主角立绘 96 朝右/敌字形占位/顺位 FS_HINT/意图素牌/血条=危险红/名行=素底牌+护甲 PNG)", + d.text((20, 866), "三区 mockup(1280×800 实比,分区几何读布局表):B 区站桩(主角立绘 96 朝右/敌字形占位/顺位 FS_HINT/意图素牌/血条=危险红/名行=素底牌+护甲 PNG)", col=(225, 215, 195), font=F_LBL) d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光/悬停金框/封印印记/危险格巢穴占位)", col=(225, 215, 195), font=F_LBL) @@ -1732,7 +1744,7 @@ def main(): with open(OUT_REPORT, "w", encoding="utf-8") as f: f.write("# 样张 v3 机检报告(2026-09-30 批 A 断言定值 + 迭代 43 整屏扩展 S1–S4/P1–P5/V1/V10/A7/A5b)\n\n") f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;A3b 图底分离 Δ≥30;A4 尺寸基线(立绘 46×48/小人源 32×32 硬等)。" - "迭代 43 扩展:B 区按代码 252 出样;整屏四项与战斗表现五项断言落 CHECKS;两版样张出口与基线并排对拍。方向稿目检先行,机检冻结归目检通过后。\n\n") + "迭代 43 扩展:B 区高读布局表出样;整屏四项与战斗表现五项断言落 CHECKS;两版样张出口与基线并排对拍。方向稿目检先行,机检冻结归目检通过后。\n\n") f.write("## 断言明细\n\n") f.write("| 断言 | 判据 | 结果 | 明细 |\n|---|---|---|---|\n") for cid, name, okk, detail in CHECKS: