From 730c35aa0aa84bf00e7a7760aa9f8ae0517b79bf Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 29 Oct 2026 01:16:37 +0800 Subject: [PATCH] =?UTF-8?q?iter41-=E5=8D=A12=EF=BC=9A=E5=91=88=E7=8E=B0?= =?UTF-8?q?=E5=B1=82=E7=BB=93=E6=9E=84=E6=94=B6=E6=95=9B=E6=89=B9=EF=BC=9A?= =?UTF-8?q?67=E6=B8=85=EF=BC=8B2=E8=B1=81=E5=85=8D=EF=BC=8B2=E5=B1=95?= =?UTF-8?q?=E5=B9=B3=E6=94=B9=E5=96=84=E2=80=94=E2=80=94=E6=8B=86=E7=A7=81?= =?UTF-8?q?=E6=9C=89=E6=97=8F=EF=BC=8B=E5=85=AD=E7=8A=B6=E6=80=81=E4=BB=B6?= =?UTF-8?q?=EF=BC=8Bui=5Fkit=E6=94=B6=E6=95=9B=EF=BC=8B=E6=9D=9F=E6=94=B6?= =?UTF-8?q?=E6=8B=A2=EF=BC=8Bprint=E5=88=A0=EF=BC=9B=E5=90=ABu14=E8=A1=A5?= =?UTF-8?q?=E5=81=9A=E3=80=81=E5=B9=B6master=E5=B9=B6=E6=A1=88=E4=B8=8E?= =?UTF-8?q?=E6=89=B9=E6=9C=AB=E4=B9=9D=E5=A4=84=E8=A7=A3=E6=9E=90=E9=94=99?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/art/art-assets-checklist.md | 72 +- design/art/art-bible.md | 605 ++-- design/art/art-prompts.md | 10 +- design/art/art-status-checklist.md | 14 +- design/art/asset-path-map.md | 16 +- design/art/character-design.md | 112 +- design/art/remote-gen-prompts.md | 9 +- design/engineering/process-conventions.md | 2 +- design/engineering/test-conventions.md | 2 +- design/gdd/02-系统清单.md | 2 +- design/gdd/08-Alpha版本标准.md | 4 +- design/gdd/术语表.md | 16 +- game/core/action_economy.gd | 2 +- game/core/battle_orchestrator.gd | 2 +- game/core/config_domain_combat.gd.uid | 1 + game/core/config_domain_crossrun.gd.uid | 1 + game/core/config_domain_run.gd.uid | 1 + game/core/effect_engine.gd | 2 +- game/core/meta_progression.gd | 12 +- game/core/meta_state_store.gd.uid | 1 + game/core/slot_store.gd.uid | 1 + .../data/schemas/adventure_cap_segment.gd.uid | 1 + .../schemas/adventure_quota_segment.gd.uid | 1 + .../schemas/adventure_value_segment.gd.uid | 1 + game/presentation/battle_narrator.gd | 49 +- game/presentation/camp_flow_context.gd | 35 +- game/presentation/camp_shop_presenter.gd | 22 +- game/presentation/curio_flow_context.gd | 44 +- .../presentation/curio_interact_controller.gd | 116 +- game/presentation/hud_presenter.gd | 31 +- game/presentation/main.gd | 20 +- game/presentation/main_adventure_flow.gd | 4 +- game/presentation/main_ui_build.gd | 31 +- game/presentation/playtest_logger.gd | 2 - game/presentation/tune_sim_controller.gd | 38 +- game/presentation/ui_assembler.gd | 92 +- game/presentation/views/ap_bar_view.gd | 26 +- game/presentation/views/battle_end_overlay.gd | 353 ++- game/presentation/views/battle_log_view.gd | 157 +- game/presentation/views/battle_stage_view.gd | 95 +- game/presentation/views/board_cell_unit.gd | 561 ++-- game/presentation/views/board_fx_view.gd | 25 +- game/presentation/views/board_view.gd | 94 +- game/presentation/views/board_zone_view.gd | 10 + game/presentation/views/camp_overlay_view.gd | 652 ++-- game/presentation/views/confirm_dialog.gd | 2 +- game/presentation/views/curio_pick_overlay.gd | 36 +- game/presentation/views/debug_overlay.gd | 209 +- .../views/hero_info_panel_view.gd | 207 +- game/presentation/views/legend_overlay.gd | 32 +- game/presentation/views/level_map_screen.gd | 465 +-- game/presentation/views/node_overlay.gd | 2 +- game/presentation/views/settings_overlay.gd | 36 +- .../views/settlement_review_overlay.gd | 2 +- game/presentation/views/shop_overlay_view.gd | 2 +- game/presentation/views/standee_body.gd | 471 +-- game/presentation/views/status_panel.gd | 2 +- game/presentation/views/ui_kit.gd | 141 +- game/probe/ui_struct_probe.gd | 2 +- game/tests/README.md | 80 +- .../integration/cross_system_flow_test.gd | 6 +- game/tests/run_all.gd | 74 +- game/tests/smoke_boot.gd | 2 - game/tests/system_test.gd | 4 +- .../battle_settlement/roll_multiplier_test.gd | 3 +- .../round_flow_skill_pool_test.gd | 3 +- .../battle_settlement/seal_polarity_test.gd | 14 +- .../battle_settlement/trigger_cond_test.gd | 6 +- game/tests/unit/core/adventure_feats_test.gd | 105 +- game/tests/unit/core/board_gen_reward_test.gd | 2 - .../tests/unit/core/breakpoint_resume_test.gd | 6 +- .../core/config_db_cross_validator_test.gd | 1 - .../unit/core/config_db_resolver_test.gd | 1 - .../unit/core/curio_refresh_paid_test.gd | 15 +- game/tests/unit/core/feat_vouchers_test.gd | 48 +- game/tests/unit/core/hero_ledger_test.gd | 6 +- .../unit/core/level_map_type_vis_test.gd | 4 +- .../unit/core/level_shop_economy_test.gd | 2 +- .../unit/core/meta_progression_forge_test.gd | 19 +- .../unit/core/node_map_generator_test.gd | 1 - game/tests/unit/core/pool_case_d_test.gd | 6 +- .../unit/core/save_boot_no_activate_test.gd | 2 +- game/tests/unit/core/settle_segments_test.gd | 35 +- game/tests/unit/core/treasure_items_test.gd | 2 +- .../unit/data_config/level_gen_params_test.gd | 37 +- .../unit/data_config/rng_stream_boot_test.gd | 13 +- .../data_config/table_structure_guard_test.gd | 8 +- .../unit/presentation/forge_workbench_test.gd | 14 +- .../presentation/level_map_info_fog_test.gd | 2 +- .../presentation/player_copy_sweep_test.gd | 12 +- .../presentation/standee_fx_sweep_test.gd | 20 +- .../presentation/theme_projection_test.gd | 16 +- .../tests/unit/presentation/type_glow_test.gd | 13 +- .../unit/presentation/ui_font_asset_test.gd | 17 +- game/tests/验证集清单.md | 2682 ----------------- reports/iter41-反哺点-2026-10-28.md | 33 + reports/iter41-实现报告-2026-10-28.md | 91 + .../工程改动报告-单元闸挂死守护-2026-10-28.md | 32 + ...·¥程改动报告-存量断言精简AB档-2026-10-28.md | 76 + tools/doc_link_audit.py | 6 +- tools/gdd_lint.py | 20 +- tools/pipeline_gate.py | 39 +- tools/undo_proof.py | 159 - 待办清单.md | 59 +- 104 files changed, 3419 insertions(+), 5333 deletions(-) create mode 100644 game/core/config_domain_combat.gd.uid create mode 100644 game/core/config_domain_crossrun.gd.uid create mode 100644 game/core/config_domain_run.gd.uid create mode 100644 game/core/meta_state_store.gd.uid create mode 100644 game/core/slot_store.gd.uid create mode 100644 game/data/schemas/adventure_cap_segment.gd.uid create mode 100644 game/data/schemas/adventure_quota_segment.gd.uid create mode 100644 game/data/schemas/adventure_value_segment.gd.uid delete mode 100644 game/tests/验证集清单.md create mode 100644 reports/iter41-反哺点-2026-10-28.md create mode 100644 reports/iter41-实现报告-2026-10-28.md create mode 100644 reports/工程改动报告-单元闸挂死守护-2026-10-28.md create mode 100644 reports/工程改动报告-存量断言精简AB档-2026-10-28.md delete mode 100644 tools/undo_proof.py diff --git a/design/art/art-assets-checklist.md b/design/art/art-assets-checklist.md index 26069bbc..9de254b4 100644 --- a/design/art/art-assets-checklist.md +++ b/design/art/art-assets-checklist.md @@ -1,23 +1,23 @@ # 美术资源依赖清单(Art Assets Checklist) > **性质**:表现层升级批与战斗表现批的生产侧清单——把散在战斗表现 / HUD框架 / 关卡节点地图 / 敌人系统里的资产需求收拢成一张可执行的工单视图,**兼资产维度单一登记处**。**规范效力归各 owner 文档**(战斗表现、美术圣经、04-美术音频方向),本文只补「生产视角的需求规格」,冲突时以 owner 文档为准。 -> **建立**:2026-09-18 场景一(战斗画面呈现)讨论产物。**2026-09-22 GDD 过期账清偿批**:原 asset-checklist.md 并入本文后删除,本文自此为两份清单合并后的唯一清单;同批修正敌人键集/徽章数量/样张状态等过期事实(详见 art-bible 的 git 提交史)。**2026-09-22 §5 角色设计落地批**:剪影引用迁 art-bible §5.1、护甲区分方案落 §5.3(清偿两处待设计项)、提示词卡 v2.0 重生成(详见 art-bible 的 git 提交史)。**2026-09-23 复用盘点批**:立素材站复用渠道与复用闸(§0);新增 §3.4–3.6 三条此前漏登记的需求(增益状态效果图标 / 营地与元进度件 / 字体);各条目标注来源渠道。**2026-09-23 §5 抽离批**:角色设计内容迁独立文档 `character-design.md`(art-bible §5 自此为引用存根,小节号不变),本文 §5.x 剪影/护甲引用经存根仍可达;主角特征批(直剑→挖掘铲、加旗朱背旗)顺带,主角形象锚见 character-design.md §5.4。**2026-09-23 §6–9 补全批**:art-bible 四章成文(§6 环境/§7 UI/§8 资产规范/§9 参考与反参考);本文 §0 交付格式与像素尺寸行、参考图行、§6 依赖链顺带改指 §8/§9 现行。 +> **建立**:场景一(战斗画面呈现)讨论产物;原 asset-checklist.md 并入本文后删除,本文为两份清单合并后的唯一清单。历次修订明细见 git 提交史。 > **更新纪律**:各条目「现状」行顺带更新(未开始 / 候选已出 / 已裁决 / 已交付);做完不删条目,改状态。与待办清单的联动:本文是资产维度明细,待办清单只留指路存根。 ## 0. 生产渠道、规格与风格宪法 -**三条生产渠道**(2026-09-23 用户拍板补第三条:**复用优先**——能从公开素材站轻改复用的不去生成,尽可能少生成): +**三条生产渠道**(第三条为**复用优先**——能从公开素材站轻改复用的不去生成,尽可能少生成): | 渠道 | 出什么 | 先例 | |---|---|---| -| ♻ **素材站复用(轻改)** | UI 九宫格面板与按钮、字体、通用粒子与光效、通用图标、无缝纹理底噪 | 无(2026-09-23 本批立渠道);来源偏好 CC0(如 Kenney 全站、OpenGameArt 标 CC0 件、itch.io 免费包) | -| ✦ **ComfyUI(用户自跑)** | 角色、插画级素材;每个敌种/主角建议出 **2–3 版候选**供裁决,不直接出终稿 | 主角站姿样张(2026-09-16 脚本管线产物,源文件已清理);提示词规格卡 art-prompts.md **v2.0 已重生成**(2026-09-22,美术圣经 §5 落地后正序重生;v1.0 移除决策归状态记录 125) | +| ♻ **素材站复用(轻改)** | UI 九宫格面板与按钮、字体、通用粒子与光效、通用图标、无缝纹理底噪 | 来源偏好 CC0(如 Kenney 全站、OpenGameArt 标 CC0 件、itch.io 免费包) | +| ✦ **ComfyUI(用户自跑)** | 角色、插画级素材;每个敌种/主角建议出 **2–3 版候选**供裁决,不直接出终稿 | 主角站姿样张(脚本管线产物,源文件已清理);提示词规格卡 art-prompts.md **v2.0**(按美术圣经 §5 现行重生成) | | ⚙ **脚本管线(代理出)** | 图标、格面、样张类精度资产 | 格面占位渲染与程序化徽标(现行) | **通用规格纪律**: -- 交付格式 PNG 透明底;动画用序列帧或精灵表(SpriteFrames),帧数规格随样张批定(候选档已成文 art-bible §8.4)。命名规范候选草案见 art-bible §8.3。 -- **分辨率档位已定案(2026-09-22 用户确认):1280×800 + 1920×1080 两档**,窗口不可调、格边长按档位计算,后续调整走修订;像素画布已定暂定基准(**art-bible §8.2**:主角原图与战斗状态图 48×72、棋盘小人 32×32 二头身〔用户暂定〕;敌种立绘三档 48/64/96 候选;1× 基准整数倍缩放纪律;1920 档缩放策略与帧数规格待样张批,待办清单已登记)。 +- 交付格式 PNG 透明底;动画用序列帧或精灵表(SpriteFrames),帧数规格随样张定(候选档已成文 art-bible §8.4)。命名规范候选草案见 art-bible §8.3。 +- **分辨率档位已定案:1280×800 + 1920×1080 两档**,窗口不可调、格边长按档位计算,后续调整走修订;像素画布基准见 **art-bible §8.2**(主角人设原图 48×72〔人设侧画布〕;站桩立绘/战斗状态图源高 48;棋盘小人 32×32 二头身;敌种逐键源档 36–94;整数倍缩放纪律;终值归样张冻结,待办清单已登记)。 - 程序化占位是现行结构冻结条款:资产未就位的槽位保持占位渲染,不空窗。 - **复用闸(♻ 渠道四道验收,一道不过不入包)**:① 色板量化——改色对齐 art-bible §4.1 token,不保留原包杂色;② 吸色检查——无 HSV 色相 300°–345°(紫赤禁入带);③ 灰度/色弱可辨——去色相后仍可辨且不与语义件混淆;④ 授权干净——CC0 优先,CC-BY 署名记入设置/关于页,license 不明的一律不用。 @@ -27,14 +27,14 @@ |---|---| | 一句话视觉规则 | 明快的地牢,轻快的扫荡,仔细的推理——**明亮像素幻想,全年龄,非暗黑**,角色 2~3 头身 | | 原则一「预告上格」 | 格内绘制优先级 **数字(含色牌)> 小人 > 封印印记 > 变异纹带 > 装饰变体 > 底色**;任何元素禁止遮挡数字字形区;类型微光为底效泛光层,不入遮挡栈 | -| 原则二「明亮为皮,明度分档为骨」(v3.0) | 暖倾向;数字亮墨直画已开格底(候选亮墨 L≥0.55 / 已开底 L 0.20–0.32,样张 v3 冻结),对比 ≥4.5:1;色牌 `#F5EAD3` 降级为复杂格底后备(牌上深墨 L≤0.027);未开格底 L≥0.30、HSV 饱和 ≤0.55 | +| 原则二「明亮为皮,明度分档为骨」 | 暖倾向;数字亮墨直画已开格底(亮墨 L≥0.75 / 已开格底 L 0.08–0.14,art-bible §4.1),对比 ≥4.5:1;色牌 `#F5EAD3` 为复杂格底后备(牌上深墨 L≤0.027);未开格底 L≥0.30、HSV 饱和 ≤0.55 | | 原则三「一砖一世界」 | 变体优先换色板不加新资产;危险状态必须**形状+颜色双通道**(色弱灰度可辨) | -| 类型预读单通道(§4.6.3) | 未开格 = **类型常显微光**(五组色带 §4.2:芥末黄/黄绿/青/紫罗兰/绯红,色相+明度双码,均候选值,避 300°–345° 禁入带,样张批冻结);不呈现极性、不泄危险格与安全格〔角嵌形状通道 2026-09-28 退役——用户裁微光格只留微光〕 | +| 类型预读单通道(§4.6.3) | 未开格 = **类型常显微光**(五组色带 §4.2:芥末黄/黄绿/青/紫罗兰/绯红,色相+明度双码,避 300°–345° 禁入带,样张冻结);不呈现极性、不泄危险格与安全格(角嵌形状通道已不存在——微光格只留微光) | | 剪影哲学(§5.1) | **所有角色自带轮廓通道:亮色描边+落影底盘**,识别不依赖背景;四类原型见各条目 | -| 语义色规则(§4.2) | 红只说危险(危险红 `#A8321F` 单档——旗朱已退役归封印色〔现行封印黑 `#000000`〕,2026-09-26 封印紫批→2026-09-28 封印黑批)、金只说收益(`#F0A828`)、蓝只说己方(`#2E86DE`)、**紫赤 `#8F2F6A` 为 Boss 独占,HSV 色相 300°–345° 全场禁入带**(微光群伤组绯红为类型先验色带域,例外见 §4.2) | +| 语义色规则(§4.2) | 红只说危险(危险红 `#A8321F` 单档)、金只说收益(`#F0A828`)、蓝只说己方(`#2E86DE`);封印印记现行色 = 封印黑 `#000000`(色史归 art-bible §4.1);**紫赤 `#8F2F6A` 为 Boss 独占,HSV 色相 300°–345° 全场禁入带**(微光群伤组绯红为类型先验色带域,例外见 §4.2) | | UI 形状语法(§7.1,§3.3 存方向指导) | 半拟物:面板=石板圆角矩形(与地砖同圆角率),按钮=可按的砖,仪式确认=圆形蜡封;数据区直角细线无纹理 | -**参考图**:`design/art/references/` 三款×2 张(Into the Breach / Moonlighter / Peglin,与圣经点名池一一对应;gungeon 已退场〔2026-09-22 裁决〕;正面取/弃与反面条款已成文 art-bible §9,反面图证待素材生成后另批增补)。 +**参考图**:`design/art/references/` 三款×2 张(Into the Breach / Moonlighter / Peglin,与圣经点名池一一对应;gungeon 已退场,反面条款见 art-bible §9.2;正面取/弃与反面条款已成文 art-bible §9,反面图证待素材生成后增补)。 --- @@ -50,25 +50,25 @@ ### 1.2 战斗一屏样张(双舞台,2–3 版候选)⚙格面骨架 + ✦角色剪影候选 -- **要什么**:按双舞台布局(战斗画面与扫雷图同屏常驻)出的整屏试验图。要锁的四件事:**角色剪影方向 / 动画方案 / 战斗空间 / 敌人层绘制优先级插入位置**;~~顺带裁决**敌人卡形态:卡片式 vs 站桩式(无框立绘+底座)**——两版候选各出一版~~——**已提前裁决(2026-09-20 用户看对照样张拍板):全站桩式(主角同站桩、状态小图标挂血条下),卡片式退役,正式样张按站桩形态单版出样**(对照样张存档 `production/mockups/`)。 +- **要什么**:按双舞台布局(战斗画面与扫雷图同屏常驻)出的整屏试验图。要锁的四件事:**角色剪影方向 / 动画方案 / 战斗空间 / 敌人层绘制优先级插入位置**;**敌种呈现形态已裁决:全站桩式(主角同站桩、状态小图标挂血条下),卡片式已弃,正式样张按站桩形态单版出样**。 - **风格/规格**:敌人剪影按 art-bible §5.1 四类原型出(见 2.2);战斗舞台背景=明亮地窟,紫赤仅 Boss;像素尺寸按已定案两档换算(见 §0 通用规格纪律)。 - **验收**:战斗表现「一屏样张的验收标准」(已成文)。 -- **联动**:样张未过,**不启动任何终稿资产与动画设计**(文档明文的排期硬前置);格边长的计算口径不在本清单——现行唯一出处是 [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」,HUD框架只判装板可行性(2026-09-18 用户拍板:固定两档分辨率、窗口不可调、格子量增加则格边长相应变小;**档位值已定案 1280×800 + 1920×1080**)。 +- **联动**:样张未过,**不启动任何终稿资产与动画设计**(文档明文的排期硬前置);格边长的计算口径不在本清单——现行唯一出处是 [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」,HUD框架只判装板可行性(固定两档分辨率、窗口不可调、格子量增加则格边长相应变小;**档位值已定案 1280×800 + 1920×1080**)。 --- ## 2. P1 —— 角色级(样张通过后开工,ComfyUI) -> 提示词规格卡 art-prompts.md 已于 2026-09-22 按「圣经 §5 落地后重生成」拍板重写为 **v2.0**(主角三态+八敌种,逐资产自包含完整提示词,整段可直贴);v1.0 移除决策归状态记录 125 完成登记(production/session-state/active.md)。 +> 提示词规格卡 art-prompts.md 现行为 **v2.0**(主角三态+八敌种,逐资产自包含完整提示词,整段可直贴)。 ### 2.1 主角战斗小人 - **要什么**:待机 / 攻击 / 受击 三态起步(移动态样张后定);透明底 PNG。 -- **风格/规格**:小人原型——**窄、直立、对称,轮廓凹凸 ≤2**;2~3 头身两档下剪影均成立;主色小人蓝 `#2E86DE`(候选值,样张批冻结终稿);格内占位最窄、不吞数字位。 +- **风格/规格**:小人原型——**窄、直立、对称,轮廓凹凸 ≤2**;2~3 头身两档下剪影均成立;主色小人蓝 `#2E86DE`(候选值,样张冻结终稿);格内占位最窄、不吞数字位。 - **来源**:✦ ComfyUI(prompt 卡 v2.0 §2);剪影原型是设计载荷(宽≥高/凹凸计数/甲档/紫赤独占),素材站无对的,不复用。 - **现状**:站姿样张已有先例(源文件已清理);战斗三态未出。 -### 2.2 八敌种形象(键集事实源 = `data/tables/enemy.tres` 行键,2026-09-22 核) +### 2.2 八敌种形象(键集事实源 = `data/tables/enemy.tres` 行键) | 敌种(enemy_id) | 剪影原型(§5.1) | 护甲类型 | 风格要点 | |---|---|---|---| @@ -81,11 +81,11 @@ | 钩爪恐魔 hook_horror | 普通怪(钩爪/多足轮廓注意凹凸计数控制) | light | 第三关群行捕食者,小体量可辨 | | 灰矮人石君王 duergar_king | **Boss:锐角与不对称**,体量最大 | heavy | 第三关 Boss;紫赤色相独占+形状独占双通道 | -- **通用规格**:亮色描边+落影底盘轮廓通道硬规则;透明底 PNG;每敌种 2–3 版候选;护甲类型视觉区分方案已落 art-bible §5.3(候选方案,样张批冻结)。 +- **通用规格**:亮色描边+落影底盘轮廓通道硬规则;透明底 PNG;每敌种 2–3 版候选;护甲类型视觉区分方案已落 art-bible §5.3(候选方案,样张冻结终稿)。 - **来源**:✦ ComfyUI(prompt 卡 v2.0 §3,八敌种+L2 熊地精变体共 9 卡);理由同 §2.1——不复用。 -- **联动**:显示名以 `data/tables/enemy.tres` 与敌人系统文档为准(L2 熊地精=bugbear 复用,显示名熊地精系——2026-09-17 用户拍板)。 +- **联动**:显示名以 `data/tables/enemy.tres` 与敌人系统文档为准(L2 熊地精=bugbear 复用,显示名熊地精系)。 -### 2.3 意图准备动画三档(2026-09-18 用户拍板) +### 2.3 意图准备动画三档 - **要什么**:用**动作动画**替代文字徽标预告敌方下一拍,三档: 1. **普通攻击 = 常态准备动画**——待机姿态本身就是预示,不另做动作; @@ -94,7 +94,7 @@ - **风格/规格**:三档在**灰度/剪影层也可辨**(双通道纪律);每档动画时长与结算节拍对齐(预告时序 = 下一拍、挖掘期全程可见)。 - **联动**:HUD框架「意图徽标不预告技能名与数值」条款由本拍板承接为动画承载(文字与技能名彻底退出);战斗表现「特殊技两型动画」输入清单按三档改写;映射按 enemy_id 全局恒定(跨 run 禁重掷);契约锚 = art-bible §4.6.4 意图姿态行。 - **来源**:✦/⚙ 自产——三档表演层挂在我们自己的精灵图上,动画本体不可复用;通用蓄力/施法光效素材(♻)仅可作参考与粒子底料,过复用闸。 -- **现状**:占位符号已落地(2026-09-18:普通攻击「—」/ ⚔ / ✦ / 重击类「蓄力」,技能名退出徽标);终稿动画待样张后开工。 +- **现状**:占位符号已落地(普通攻击「—」/ ⚔ / ✦ / 重击类「蓄力」,技能名退出徽标);终稿动画待样张后开工。 --- @@ -102,7 +102,7 @@ ### 3.1 格面资源徽章 19 枚(九型 × 正负 18 枚 + 清图槽 1 枚)⚙ -- **要什么**:九家族(增伤/减伤/回复/爆裂/削弱/穿刺/魔法/行动/GP)各正负一枚 + 清图群伤一枚。**键名录读 `reward.tres` 与地图战斗资源 GDD 九型字典,本文不抄键名**(防再腐烂——2026-09-22 勘误先例:gp 组键名 gp_coin/gp_lose 以配置为准)。 +- **要什么**:九家族(增伤/减伤/回复/爆裂/削弱/穿刺/魔法/行动/GP)各正负一枚 + 清图群伤一枚。**键名录读 `reward.tres` 与地图战斗资源 GDD 九型字典,本文不抄键名**(防再腐烂:gp 组键名 gp_coin/gp_lose 以配置为准)。 - **风格/规格**:图标键契约 art-bible §4.6(冻结);**极性视觉 = 金族满框(正向)/ 红族缺框(负向)**,不新增语义色;徽章=附属小图标,非完整凸形(不构成第二焦点),栈位在旗之下、变异纹理之上;负向九枚 weight=0 纯字典,用于踩雷反噬结算呈现。 - **来源**:⚙ 脚本管线(键契约件,程序化最稳);通用图标包只作造型参考,不直接入包。 - **现状**:程序化占位徽标(文本/符号),够试玩不够观感。 @@ -112,34 +112,34 @@ - **要什么**:四类节点图标(战斗 / 奖励 / 恢复 / Boss)+ 图版行进器形象(棋子)。 - **风格/规格**:容器走石板圆角语法;Boss 节点可用紫赤;奖励节点可用奖励金;恢复节点绿=自然生机(零逻辑语义,仅氛围);行进器=小人同族蓝。 - **来源**:♻ 素材站复用优先——通用地图/桌游图标(如 Kenney 件)改色量化,过复用闸;⚙ 兜底。行进器与小人同族,宜 ✦/⚙ 与主角同批出。 -- **联动**:正式节点图 UI 设计未成文(登记待办:排秘宝内容批后与美术批同期待拍板);HUD框架义务清单漏「关卡节点地图」行,顺带补。 +- **联动**:正式节点图 UI 设计未成文(登记待办:排秘宝内容设计后与美术设计同期待裁决);HUD框架义务清单漏「关卡节点地图」行,同时补。 - **现状**:程序化占位,够试玩。 ### 3.3 雷 / 封印印记 / 数字格面像素化 ⚙ - **要什么**:格子样张通过后按冻结参数出全套砖面资产(含图类型换色板组)。 -- **来源**:⚙ 脚本管线自产——未开/已开/危险格/变异纹带是扫雷专属语义〔角嵌纹样 2026-09-28 退役〕,素材站没有对的;砖面**纹理底噪**可 ♻ 用 CC0 无缝石/土纹理作脚本管线的纹理输入,量化到色板(混合路线)。 +- **来源**:⚙ 脚本管线自产——未开/已开/危险格/变异纹带是扫雷专属语义(角嵌纹样已不存在),素材站没有对的;砖面**纹理底噪**可 ♻ 用 CC0 无缝石/土纹理作脚本管线的纹理输入,量化到色板(混合路线)。 - **前置**:§1.1 格子样张通过(**未跑**)。 - **现状**:程序化绘制,可读性达标(对比硬指标已是现行实现依据),观感待升级。 -### 3.4 增益状态效果图标 9 枚 ♻优先/⚙兜底(2026-09-23 新登记,此前清单漏登记) +### 3.4 增益状态效果图标 9 枚 ♻优先/⚙兜底 - **要什么**:HUD 状态槽挂图(敌人血条下小图标),九行键族——**键名录读 art-bible §4.6.1 与 `status_effects.tres`,本文不抄键名**(键名录指路纪律,同 §3.1)。 - **风格/规格**:♻ 素材站复用优先——通用增益/减益图标包改色为墨族,按 §4.6.1 极性列统一叠极性视觉(有利=金族满框/有害=红族缺框,art-bible §4.6 通道分配全键族通用),过复用闸四道(灰度可辨=色弱冗余通道);⚙ 兜底程序化。一键一纹样、映射按 effect_id 全局恒定禁换图(图标键契约)。 - **现状**:已接入(2026-09-24,定稿九枚入 HUD 状态槽,合并 71ae51c;色板量化豁免=用户逐键验收的自产件,非外采)。 -### 3.5 营地场景图(2026-10-22 重写;原「营地与元进度件·三柜台」条目作废) +### 3.5 营地场景图 - **要什么**:营地场景图 9 张——`scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件)。钥石顶栏图标沿用现役 `icon_keystone`。 - **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**每张出图须登记模型名+分辨率+seed+对话/日期**。 - **排期**:营地交互重做**美术自立批**(不占迭代位;UI 贴图重构批已搁置)。终稿落 `game/assets/art/scene/`,消费常量 `UiKit.TEX_CAMP_*`。 - **现状**:全占位。 -### 3.6 字体(全局 UI 像素字,已定稿单套)(2026-09-23 新登记;2026-10-24 用户裁收束定稿) +### 3.6 字体(全局 UI 像素字,已定稿单套) -- **要什么**:全局 UI 字体一套。**已定稿(2026-10-24 用户裁):缝合像素字体 Fusion Pixel 单套单字重即终稿**——拉丁字符与数字沿用其自带拉丁字形,不另接拉丁像素字专件(旧拉丁候选 Kenney Fonts 退役不再接入)、不上第二个字重;现行已接入 `ui_font_fusion_pixel_zh_hans.ttf`。 -- **来源/规格**:♻ 复用开源——缝合像素字体(Fusion Pixel,SIL OFL 1.1,授权总表在册);墨色走主墨系(art-bible §7.2)。换字体是全局决策;样张批用字即此定稿件,过样张闸第 1 项(数字可读性 ≥4.5:1,禁以换字形逃避)。 -- **现状**:✅ 已定稿在役(2026-09-27 用户拍板接入;2026-10-24 用户裁现状即终稿)。 +- **要什么**:全局 UI 字体一套。**已定稿:缝合像素字体 Fusion Pixel 单套单字重即终稿**——拉丁字符与数字沿用其自带拉丁字形,不另接拉丁像素字专件(旧拉丁候选 Kenney Fonts 已弃)、不上第二个字重;现行已接入 `ui_font_fusion_pixel_zh_hans.ttf`。 +- **来源/规格**:♻ 复用开源——缝合像素字体(Fusion Pixel,SIL OFL 1.1,授权总表在册);墨色走主墨系(art-bible §7.2)。换字体是全局决策;样张用字即此定稿件,过样张闸第 1 项(数字可读性 ≥4.5:1,禁以换字形逃避)。 +- **现状**:✅ 已定稿在役(现状即终稿)。 --- @@ -150,7 +150,7 @@ | 受击反馈 / 死亡表现 / 顺位标记 | 敌人系统登记的资产需求**下限三类** | 死亡=血条归零后**灰化+碎裂退场,禁止瞬间消失**;顺位标记需在敌多时可读 | ⚙/✦ 挂自产精灵——死亡灰化碎裂是语义硬规则,不可整包复用;烟尘碎屑粒子可 ♻ 改色 | | 特殊技动画终稿 | 并入 §2.3 三档意图动画,不另起体系 | 攻/防两型旧口径由三档替代 | ✦/⚙ 自产(同 §2.3) | | 连锁坍塌显形演出 | 连锁展开触发时的连锁演出 | 与音频方向同批配对登记;「坍塌显形」=连锁展开的玩家可见名 | ♻ 粒子/碎块改色 + ⚙ 坍塌结构自产 | -| 封印特效(插旗动作) | 插旗的呈现名 = **封印**(2026-09-25 术语批,art-bible §3.2):**施印瞬间特效 + 印记常显形态**两件 | 印记旗面走封印黑 `#000000`(旗朱→封印紫→封印黑,2026-09-28 封印黑批);特效走运动+色相(§2:状态/情绪不走明度,空间光照可走——v3.0);现行占位 = 封印黑「旗」字形;印记常显形态可与 §3.3 旗面像素化合并出 | ✦/⚙ 自产(精灵+粒子,烟尘光点可 ♻ 改色) | +| 封印特效 | 该动作的呈现名 = **封印**(art-bible §3.2):**施印瞬间特效 + 印记常显形态**两件 | 印记旗面走封印黑 `#000000`(现行封印色,色史归 art-bible §4.1);特效走运动+色相(§2:状态/情绪不走明度,空间光照可走);现行占位 = 封印黑「旗」字形;印记常显形态可与 §3.3 旗面像素化合并出 | ✦/⚙ 自产(精灵+粒子,烟尘光点可 ♻ 改色) | | 弹道/退场等动画密度 | 战斗表现输入清单,方案未定 | 样张锁动画方案后启动 | ♻ 通用弹道/光点/消散粒子改色为主 | --- @@ -158,10 +158,10 @@ ## 5. 明确不在本清单 - **音乐音效**:文档定稿「MVP 占位即可,核心循环验证后再定」。 -- **秘宝图标**:内容归 Alpha 秘宝整体重设计批(状态记录 112 口径),内容未定不能先做;渠道届时按 ♻/✦ 另评。 +- **秘宝图标**:内容归 Alpha 秘宝整体重设计,内容未定不能先做;渠道届时按 ♻/✦ 另评。 - **封面图/宣传图**:Alpha 后另批,不阻塞切片;需项目角色出镜,素材站合成观感差,宜 ✦。 -- **地形变异专属纹理**:~~变异表为空,纹带种类待地形变异批定夺~~——**六类型已定稿(2026-09-20 地形变异批)**,纹带需求 = 2 纹形(伤害轴刺缺形/信息轴遮蔽形)× 3 图类型色板,**样张批后可排产**;形状通道规则见 art-bible §3.2。 -- **精英/Boss 护甲类型视觉区分**:~~待设计项~~ **已落 art-bible §5.3**(候选方案:甲面覆盖率×甲材质色三档,样张批冻结)。 +- **地形变异专属纹理**:六类型已定稿,纹带需求 = 2 纹形(伤害轴刺缺形/信息轴遮蔽形)× 3 图类型色板,**样张后可排产**;形状通道规则见 art-bible §3.2。 +- **精英/Boss 护甲类型视觉区分**:已落 art-bible §5.3(候选方案:甲面覆盖率×甲材质色三档,样张冻结终稿)。 ## 6. 执行顺序(依赖链) @@ -170,16 +170,16 @@ │ 战斗一屏样张(1.2) ──> 角色终稿(2.1/2.2) ──> 意图动画三档(2.3) ──> 演出类(P4) │ - └─> 敌人卡形态裁决 ✅已完成(2026-09-20 拍板全站桩,卡片式退役) + └─> 敌种呈现形态裁决 ✅已完成(全站桩,卡片式已弃) 徽章(3.1)/节点图(3.2)/效果图标(3.4) —— 独立线,可随时插队 ♻ 复用件(3.2/3.4/3.5/3.6/演出粒子) —— 不依赖样张,可随时插队(入包闸 = §0 复用四道) 字体(3.6)接入归表现批;营地场景图(3.5)归营地交互重做美术自立批 分辨率档位 ✅已定案(1280×800 + 1920×1080,2026-09-22 用户确认);art-bible §8 资产规范 ✅已成文(2026-09-23)——像素基准/画布/命名/帧数候选值就位,终值归样张批冻结 ``` -## 附、已评素材包授权与结论登记(2026-09-26 并档,♻ 复用渠道) +## 附、已评素材包授权与结论登记(♻ 复用渠道) -> 来源:tile-reuse-2026-09-23 / ui-reuse-2026-09-23 / terrain-walls-2026-09-25 三批复审记录(批目录已归档清退,本表为唯一仓内登记);详录已随复审批出盘,沿革查 git 提交史。 +> 来源:素材站复用三批复审记录(批目录已归档清退,本表为唯一仓内登记);详录已随复审出盘,沿革查 git 提交史。 ### 授权总表(闸④预检结论) @@ -198,5 +198,5 @@ | Golden UI 系列 | CC-BY-SA | — | **闸④不过,未下载** | | Gemini 生图(用户自跑) | 自产 | 无 | **terrain_ 高墙瓦片材质源(现行定稿)** | -### 复用四闸(状态记录 130,现行) +### 复用四闸(现行) 色板量化 / 禁入带吸色 / 灰度可辨 / 授权干净(SA 不可、来路不明不可)。 diff --git a/design/art/art-bible.md b/design/art/art-bible.md index 9e819ac8..7954fa0d 100644 --- a/design/art/art-bible.md +++ b/design/art/art-bible.md @@ -1,12 +1,12 @@ # 美术圣经(Art Bible)— Dungeon Sweeper -> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**(§6 环境/§7 UI/§8 资产规范/§9 参考与反参考已于 2026-09-23 v1.8 按 D1/D2/D4/D5 框架成文;D3=§5 角色已于 v1.6 落地、v1.7 抽离独立文档;框架 2026-09-22 用户拍板,原登记于中间产物修订指导,经裁决不入库,明细归状态记录 125);现行为 v3.20,历次修订明细见 git 提交史 +> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.21,历次修订明细见 git 提交史 > -> **Art Director Sign-Off (AD-ART-BIBLE)**:历次批均 SKIPPED(v1.8 §6–9 补全亦 SKIPPED)— Lean/Solo mode(非 PHASE-GATE;§6–9 已成文,full 门禁签核时机待用户表态) -> 风格基调(2026-09-10 用户定稿):地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身 -> 参考池(用户审阅选定,三者相融):**Into the Breach**(网格信息分层;雷图棋盘呈现基准 = 其战棋地块质感 × 正俯视角,见 §3.2)× **Moonlighter**(明亮像素地牢质感)× **Peglin**(明快全年龄地牢色彩);参考截图存于 `design/art/references/`(三款×2 张,与点名池一一对应;gungeon 两张经 2026-09-22 裁决退场;正面取/弃与反面条款已成文 §9,反面图证待素材生成后另批增补) -> 约束(2026-09-10 用户强调):本圣经是**纯表现层文档**——明亮轻快是美术风格,不是玩法成分。圣经禁止引入、暗示或预设任何玩法语义,不改变任何 GDD 已锁机制;凡表现层需要玩法钩子才能表达之处(如完美封印检测、Boss 预警),一律标注「待 GDD 定夺」,圣经只规定"若有,则如何呈现"。GDD 机制与圣经冲突时,机制语义以 GDD 为准,呈现方案以本圣经为准 -> **条款验收标注纪律(2026-10-06 P2-4 用户拍板,全文书写规范)**:条款尾部按三档标注验收方式——**`←机检: <断言>`**(脚本可自动判定)/ **`←目检: <判据>`**(机器测不了,但看什么、算过有明确判据)/ **`←方向性`**(气质与方向原则,不承担验收);**新条款一律按此书写**,存量口号型条款分章补标(约 55%,待办清单挂账「口号型条款机检化批」分章推进、每章交付抽验);数值类条款推荐「**待定现用**」态(现行即执法、修订走登记,释义见 §4.1) +> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。 +> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。 +> 参考池(用户审阅选定,三者相融):**Into the Breach**(网格信息分层;雷图棋盘呈现基准 = 其战棋地块质感 × 正俯视角,见 §3.2)× **Moonlighter**(明亮像素地牢质感)× **Peglin**(明快全年龄地牢色彩);参考截图存于 `design/art/references/`(三款 × 2 张,与点名池一一对应);Enter the Gungeon 不作参考,反面条款见 §9.2。 +> 约束:本圣经是**纯表现层文档**——明亮轻快是美术风格,不是玩法成分。圣经禁止引入、暗示或预设任何玩法语义,不改变任何 GDD 已锁机制;凡表现层需要玩法钩子才能表达之处(如完美封印检测、Boss 预警),一律标注「待 GDD 定夺」,圣经只规定"若有,则如何呈现"。GDD 机制与圣经冲突时,机制语义以 GDD 为准,呈现方案以本圣经为准。 +> **条款验收标注纪律(全文书写规范)**:条款尾部按三档标注验收方式——**`验收:机检〈判据>`**(可量化,脚本能断言)/**`验收:目检〈判据>`**(机器测不了,但看什么、怎么算有明确判据)/**`验收:方向性`**(气质与方向原则,不承担验收);推导源尾注「←」继续表达推导来源,与验收标注同句并存时,验收标注放句末;数值类条款推荐「**待定现用**」态(现行即执法、修订走登记,释义见 §4.1) --- @@ -14,255 +14,261 @@ ### 1.1 一句话视觉规则 -**「明快的地牢,轻快的扫荡,仔细的推理。」** +**「明快的地牢,轻快的扫荡,仔细的推理。」** 验收:方向性 -三拍定调:世界观(明快的地牢)× 玩法动作(轻快的扫荡)× 玩法心智(仔细的推理)。本句为气质宣言,不承担裁决;视觉决策歧义时,依 1.2 三条支撑原则依次裁决(每条附设计测试),原则全部不过的方案否决。本节是美术圣经的最高裁决层,后续章节与本节冲突时,以本节为准。 +三拍定调:世界观(明快的地牢)× 玩法动作(轻快的扫荡)× 玩法心智(仔细的推理)。本句为气质宣言,不承担裁决;视觉决策歧义时,依 1.2 三条支撑原则依次裁决(每条附设计测试),原则全部不过的方案否决。本节是美术圣经的最高裁决层,后续章节与本节冲突时,以本节为准。 ### 1.2 支撑原则 -**原则一|预告上格**(取自 Into the Breach;锚定 P1 推理的掌控感、P2 规划的延迟兑现) +**原则一|预告上格**(取自 Into the Breach;锚定 P1 推理的掌控感、P2 规划的延迟兑现) -继承其格内信息分层与"攻击预告画在目标格上"的范式:一切威胁、伤害与结算预告必须直接绘制在雷图格本体上,禁止只进侧栏或悬浮提示;格内绘制优先级固定:**数字(含色牌) > 小人标记 > 旗 > 变异纹带 > 装饰变体 > 底色**,任何元素禁止遮挡数字(格面类型预读 = 类型微光单通道〔色相+明度双码,2026-09-28 角嵌退役〕,色带见 §4.2、键契约见 §4.6;徽章小图标族保留给库存与结算明细;类型微光为底效泛光层,不入遮挡栈、不遮任何元素)。与 ITB 之别:只取其信息骨架,弃其冷暗军事色调。 +继承其格内信息分层与"攻击预告画在目标格上"的范式:一切威胁、伤害与结算预告必须直接绘制在雷图格本体上,禁止只进侧栏或悬浮提示;格内绘制优先级固定:**数字(含色牌) > 小人标记 > 旗 > 变异纹带 > 装饰变体 > 底色**,任何元素禁止遮挡数字。格面类型预读 = 类型微光单通道,色相+明度双码(色带见 §4.2、键契约见 §4.6;徽章小图标族保留给库存与结算明细;类型微光为底效泛光层,不入遮挡栈、不遮任何元素)。与 ITB 之别:只取其信息骨架,弃其冷暗军事色调。验收:目检〈预告画在雷图格本体上,任何元素不遮挡数字〉 -*范围注(2026-09-10):本原则约束"已存在的信息往哪里画",不新增信息种类;预告类信息以战斗侧 GDD 实际定义的为准(如 P2 已锁的结算来源标注),GDD 未定义的预告不因本条而存在。* +*范围注:本原则约束"已存在的信息往哪里画",不新增信息种类;预告类信息以战斗侧 GDD 实际定义的为准(如 P2 已锁的结算来源标注),GDD 未定义的预告不因本条而存在。* -*设计测试:当某信息歧义于"画进格子"还是"收进面板"时,画进格子。* +*设计测试:当某信息歧义于"画进格子"还是"收进面板"时,画进格子。* -**原则二|明亮为皮,明度分档为骨**(取自 Moonlighter、Peglin;锚定 P1 可读性前提、P4 探险氛围;2026-09-29 v3.0 修订——原「色牌为骨」降级,病根与修法见审视报告(已退役,git fbaadb8)病根 3) +**原则二|明亮为皮,明度分档为骨**(取自 Moonlighter、Peglin;锚定 P1 可读性前提、P4 探险氛围) -取 Moonlighter 的暖质地砖与亲和冒险者气质、Peglin 的明快全年龄洞穴色彩:全局暖倾向、无压抑。可读性由**格底明度分档**承载——已开格(数字所在格)明度走世界底层档(§2 明度分层;格态语义见《排雷玩法》;美术侧据此未开格走受光墙顶档、已开格走挖出空间沉暗档,与 §8.5 图底分离同轴),亮族数字墨直接画在格底上,对比硬指标不变(≥4.5:1 恒成立)。**数字色牌降级为后备手段**——仅复杂格底(物件纹样/变异纹带/微光叠加致格底对比不足)时垫牌兜底(画法 §3.3、色值 §4.1)。格子本体美术自此放开:纹理、材质、装饰变体、类型纹样皆可入画;格底参数域见 §4.1 硬指标(分档明度域、饱和上限),仍避 Boss 禁入色相带。与参考之别:ITB 以半透明压暗板承载信息;本作格底功能分档(信息可读性服务,属 §2 明度分层允许域),情绪压暗仍禁。 +取 Moonlighter 的暖质地砖与亲和冒险者气质、Peglin 的明快全年龄洞穴色彩:全局暖倾向、无压抑。可读性由**格底明度分档**承载——已开格(数字所在格)明度走世界底层档(§2 明度分层;格态语义见《排雷玩法》;美术侧据此未开格走受光墙顶档、已开格走挖出空间沉暗档,与 §8.5 图底分离同轴),亮族数字墨直接画在格底上,直画对比 ≥4.5:1 恒成立。**数字色牌为后备手段**——仅复杂格底(物件纹样/变异纹带/微光叠加致格底对比不足)时垫牌后备(画法 §3.3、色值 §4.1)。格子本体美术放开:纹理、材质、装饰变体、类型纹样皆可入画;格底参数域见 §4.1 硬指标(分档明度域、饱和上限),仍避 Boss 禁入色相带。与参考之别:ITB 以半透明压暗板承载信息;本作格底功能分档(信息可读性服务,属 §2 明度分层允许域),情绪压暗仍禁。验收:机检〈1–8 全档数字对所在已开格底对比 ≥4.5:1〉 -*设计测试:当格子美术与数字可读性冲突时,先调格底明度档,复杂格底垫色牌兜底,不动格子画。* +*设计测试:当格子美术与数字可读性冲突时,先调格底明度档,复杂格底垫色牌后备,不动格子画。* -**原则三|一砖一世界**(融合 Moonlighter 地砖观;锚定 P3 风险与收益同源、P4 地牢下探隐喻) +**原则三|一砖一世界**(融合 Moonlighter 地砖观;锚定 P3 风险与收益同源、P4 地牢下探隐喻) -格子即地牢地砖:未开 = 覆尘完整砖,已开 = 碎裂裸露,雷 = 被惊动的巢穴(格态定义与玩家说法见《排雷玩法》),图类型(board_type)= 同款地砖换色板。图类型与变异格的视觉差异一律先做色板变体,不做新资产;一切危险状态必须"形状 + 颜色"双通道冗余,色弱可辨。像 Moonlighter 之处在砖缝质感与暖色道具,但装饰禁止伪装成逻辑差异——玩家须一眼分清"好看"与"有雷"。 +格子即地牢地砖:未开 = 覆尘完整砖,已开 = 碎裂裸露,雷 = 被惊动的巢穴(格态定义与玩家说法见《排雷玩法》),图类型(board_type)= 同款地砖换色板。图类型与变异格的视觉差异一律先做色板变体,不做新资产;一切危险状态必须"形状 + 颜色"双通道冗余,色弱可辨。像 Moonlighter 之处在砖缝质感与暖色道具,但装饰禁止伪装成逻辑差异——玩家须一眼分清"好看"与"有雷"。验收:目检〈危险状态均为形状+颜色双通道,色弱模拟下可辨〉 -格子同时是**内容舞台**,格与格的美术表达按格内实态分层多样化(画法细则 §3.2):已开格按当格结果整格表达——产资源格嵌对应家族物件纹样(地图战斗资源九类型,§4.6.3)、纯安全格呈现干净裸土、危险格巢穴更凶;未开格在覆尘完整砖之上叠加**装饰变体**(裂纹/碎石斑/物件级点缀等氛围层;2026-09-29 v3.0 物件级解禁——物件级装饰〔小石块/陶片/骨殖等〕须走**专属装饰形状家族**:形与信息元素〔数字/小人/旗/纹样〕形状语言隔离,灰度下不与任何逻辑元素同形;零信息纪律不变——随机分布、与雷/安/类型零相关、禁止构成可读线索、分布每板一次跨轮不复掷;焦点预算约束不变——装饰不构成完整凸形、不抢读,§3.4;数量配额按图类型定,候选 3–5 种,样张 v3 冻结;用途=破除大面积同砖平铺的重复感);未开格的类型预读画为**类型微光单通道**(色相+明度双码;仍只表达类型,不泄危险格与安全格——呈现契约见 §4.6.3;概念定义、可见范围与生命周期见《HUD框架》「未开格类型微光」节)。 +格子同时是**内容舞台**,格与格的美术表达按格内实态分层多样化(画法细则 §3.2)。已开格按当格结果整格表达——产资源格嵌对应家族物件纹样(地图战斗资源九类型,§4.6.3)、纯安全格呈现干净裸土、危险格巢穴更凶。未开格在覆尘完整砖之上叠加**装饰变体**(裂纹/碎石斑/物件级点缀等氛围层),用途是破除大面积同砖平铺的重复感。物件级装饰(小石块/陶片/骨殖等)须走**专属装饰形状家族**:形与信息元素(数字/小人/旗/纹样)形状语言隔离,灰度下不与任何逻辑元素同形。零信息纪律:随机分布、与雷/安/类型零相关、禁止构成可读线索、分布每板一次跨轮不复掷。焦点预算约束:装饰不构成完整凸形、不抢读(§3.4);数量配额按图类型定(候选 3–5 种,样张已冻结,§1.4)。未开格的类型预读画为**类型微光单通道**(色相+明度双码;仍只表达类型,不泄危险格与安全格——呈现契约见 §4.6.3;概念定义、可见范围与生命周期见《HUD框架》「未开格类型微光」节)。验收:目检〈物件级装饰与逻辑元素形状隔离;装饰不构成完整凸形〉 -*设计测试:当新图类型或新变异格需要新视觉时,先换色板加形状,再谈新资产;当格子多样化与"好看/有雷"之辨冲突时,砍装饰,不动逻辑通道。* +*设计测试:当新图类型或新变异格需要新视觉时,先换色板加形状,再谈新资产;当格子多样化与"好看/有雷"之辨冲突时,砍装饰,不动逻辑通道。* +### 1.4 格子样张测试(验收标准) +> 立意:扫雷数字可读性与暗色氛围相冲突,由一次格子样张测试定风格,具体形态由样张测试定稿。**样张** = 一张按本圣经参数画成的格子风格试验图,是砖面形态的定稿闸——样张通过前,§3.2/§4.1 的一切参数只是候选,通过后冻结为制作标准。 -### 1.4 格子样张测试(验收标准,2026-09-13 成文) +**样张内容清单**(一图齐备,缺项即无效):未开格(覆尘完整砖,含至少两种装饰变体)、已开格(碎裂裸露,含 0 格与 1–8 全档数字)、危险格(被惊动的巢穴)、旗、小人两态(独立占格 + 与数字同格——后者须验"边框高亮不覆盖文本")、图类型色板变体组(§4.3 四槽换色,至少两套)、数字呈现组(已开格底亮墨直画组 + 复杂格底色牌后备组——素底/装饰变体底/变异纹带底至少三种格底上各一组数字)、已开格结果表达组(纯安全格 + 至少两类型物件纹样)、**类型微光呈现组**(至少三个类型的微光在纯砖格上呈现——样张为静态定格测试图,非运行时行为承诺,实际可见时机见《HUD框架》「未开格类型微光」节;含两两相邻格微光并列;微光格内零角嵌/箭头/加号类附加图形)。验收:目检〈样张图含清单全部组项〉 -> 立意(自概念期 brainstorm 内联):扫雷数字可读性 × 暗色氛围的冲突 → 由一次格子样张测试定风格;具体形态由样张测试定稿。**样张** = 一张按本圣经参数画成的格子风格试验图,是砖面形态的定稿闸——样张通过前,§3.2/§4.1 的一切参数只是候选,通过后冻结为制作标准。 +**验收项按序判定,第一项不过 = 整张否决(数字可读性定风格,风格服从可读)**: -**样张内容清单**(一图齐备,缺项即无效):未开格(覆尘完整砖,含至少两种装饰变体)、已开格(碎裂裸露,含 0 格与 1–8 全档数字)、危险格(被惊动的巢穴)、旗、小人两态(独立占格 + 与数字同格——后者须验"边框高亮不覆盖文本")、图类型色板变体组(§4.3 四槽换色,至少两套)、数字呈现组(2026-09-29 v3.0:已开格底亮墨直画组 + 复杂格底色牌后备组——素底/装饰变体底/变异纹带底至少三种格底上各一组数字)、已开格结果表达组(纯安全格 + 至少两类型物件纹样)、**类型微光呈现组**(至少三个类型的微光在纯砖格上呈现——样张为静态定格测试图,非运行时行为承诺,实际可见时机见《HUD框架》「未开格类型微光」节;含两两相邻格微光并列;微光格内零角嵌/箭头/加号类附加图形——2026-09-28 用户裁决)。 +1. **第一验收项|数字可读性(硬指标,可测)**:1–8 全档数字对其所在**已开格底**直画 WCAG 对比 ≥4.5:1(参数域见 §4.1:已开格底走世界底层档、棋盘数字墨亮族);复杂格底(物件纹样/变异纹带/微光叠加致直画对比不足)启用**色牌后备**,牌墨组合 ≥4.5:1(色牌与牌上深墨族数字的定值见 §4.1),牌缘灰度可分;0.5× 缩放抽检数字仍可辨;纹带/裂纹/巢穴纹/物件纹样禁止侵入数字字形占位区(砖心留白,§3.2)。验收:机检〈1–8 数字对已开格底直画与牌墨组合对比均 ≥4.5:1〉 +2. **格态三形可辨(灰度通道)**:未开/已开/雷三态转灰度截图后仍可逐格分类;明度差(墙亮/空间暗)与碎裂形同为合法分类通道,双通道冗余(§3.2 完整度叙事)。装饰变体禁止制造新的分类特征——同一格态下带与不带变体的灰度差异禁止成为三态分类线索。色弱模拟(去色相)下旗、巢穴纹不与裂纹混淆。**五组类型微光互辨**:色弱玩家的类型主通道 = 微光,色相+明度双码,微光以明度梯度承载分组(组内不细分,属 §2 信息面层内梯度);微光禁止侵入数字字形占位区,明度梯度禁止压破格底明度档(三态分类不受扰)。验收:目检〈三态灰度逐格可分类;五组微光互辨;色弱模拟下旗、巢穴纹不与裂纹混淆〉 +3. **焦点预算成立(§3.4)**:任意一格至多一个完整凸形;数字所在格无第二焦点。验收:目检〈任一格至多一个完整凸形,数字所在格无第二焦点〉 +4. **图类型换皮抽测(§4.3)**:变体组中数字墨亮族三档、封印黑、危险红、奖励金、小人蓝、数字色牌(后备件)、类型微光五组恒定不动;变体格底仍过第 1、2 项指标。验收:机检〈换皮后不变槽色值恒定,变体格底仍过第 1、2 项〉 -**验收项按序判定,第一项不过 = 整张否决(数字可读性定风格,风格服从可读)**: +**判定与回退**:第 1 项不过时按原则二设计测试回退——先调格底明度档,复杂格底垫色牌后备,禁止缩字号换字形逃避;第 2–4 项不过时修形不修数。样张通过 = 砖面形态与色板冻结;新增图类型/变异色板须重跑本测试(§4.3)。执行时机:正式砖面资产开工前;《HUD框架》以本节为引用依据。验收:机检〈样张机检断言集全绿后方可冻结砖面形态与色板〉 -1. **第一验收项|数字可读性(硬指标,可测;2026-09-29 v3.0 改格底分档承载)**:1–8 全档数字对其所在**已开格底**直画 WCAG 对比 ≥4.5:1(§4.1 参数域:已开格底走世界底层档,候选 L 0.08–0.14;棋盘数字墨亮族,L≥0.75 候选——均待样张 v3 冻结〔v3.0 原拟 L 0.20–0.32/L≥0.55 经 WCAG 实测不可达 4.5:1,候选带随样张批修正〕);复杂格底(物件纹样/变异纹带/微光叠加致直画对比不足)启用**色牌后备**,牌墨组合 ≥4.5:1(色牌 L≥0.70、牌上数字用深墨族),牌缘灰度可分;0.5× 缩放抽检数字仍可辨;纹带/裂纹/巢穴纹/物件纹样禁止侵入数字字形占位区(砖心留白,§3.2)。 -2. **格态三形可辨(灰度通道)**:未开/已开/雷三态转灰度截图后仍可逐格分类(§3.2 完整度叙事;v3.0 起明度差〔墙亮/空间暗〕为合法分类通道,与碎裂形双通道冗余);装饰变体禁止制造新的分类特征——同一格态下带与不带变体的灰度差异禁止成为三态分类线索;色弱模拟(去色相)下旗、巢穴纹不与裂纹混淆;**五组类型微光互辨**——色弱玩家的类型主通道 = 微光(色相+明度双码,2026-09-28 角嵌退役后微光以明度梯度承载分组(组内不细分),§2 信息面层内梯度),微光禁止侵入数字字形占位区、明度梯度禁止压破格底明度档(三态分类不受扰)。 -3. **焦点预算成立(§3.4)**:任意一格至多一个完整凸形;数字所在格无第二焦点。 -4. **图类型换皮抽测(§4.3)**:变体组中数字墨亮族三档、封印黑、危险红、奖励金、小人蓝、数字色牌(后备件)、类型微光五组恒定不动;变体格底仍过第 1、2 项指标。 +**整屏样张闸**:格子样张验"格",整屏样张验"屏"——战斗屏/关卡屏/菜单屏/营地屏各出一张整屏样张,验收四项:①全部文字(含菜单/说明/tooltip/按钮)对其直接背景 ≥4.5:1(WCAG AA),大字(≥18px 或加粗档)底线 3:1、目标 4.5:1;②明度分层三档带成立(信息面层 > 世界中层 > 世界底层,层间灰度可分);③整屏焦点预算(§3.4:首焦唯一——数字区/立绘/主按钮,无第二焦点竞争;负空间服务焦点,不属空窗);④文字禁直接压纹理底(垫容器或罩层)。验收顺序 = **方向稿目检先行**(防"机检过闸但气质否决"式返工),目检过后机检冻结。执行时机 = 屏级视觉落码开工前;过闸前一切屏级视觉为占位临时态(§7.1 完成条款)。验收:机检〈整屏样张四项断言全绿,且目检先行过审〉 -**判定与回退**:第 1 项不过 → 按原则二设计测试回退——先调格底明度档,复杂格底垫色牌兜底,禁止缩字号换字形逃避;第 2–4 项不过 → 修形不修数。样张通过 = 砖面形态与色板冻结;新增图类型/变异色板须重跑本测试(§4.3)。执行时机:正式砖面资产开工前;《HUD框架》以本节为引用依据(2026-09-07 门禁:样张测试并入 art-bible 执行)。 - -**整屏样张闸(2026-09-29 v3.0 新增)**:格子样张验"格",整屏样张验"屏"——战斗屏/关卡屏/菜单屏/营地屏各出一张整屏样张,验收四项:①全部文字(含菜单/说明/tooltip/按钮)对其直接背景 ≥4.5:1(WCAG AA;大字〔≥18px 或加粗档〕底线 3:1、目标 4.5:1);②明度分层三档带成立(信息面层 > 世界中层 > 世界底层,层间灰度可分);③整屏焦点预算(§3.4:首焦唯一——数字区/立绘/主按钮,无第二焦点竞争;负空间服务焦点,不属空窗);④文字禁直接压纹理底(垫容器或罩层)。闸形 = **方向稿目检先行**(防"机检过闸但气质否决"式返工),目检过后机检冻结。执行时机 = 屏级视觉落码开工前;过闸前一切屏级视觉为占位临时态(§7.1 完成条款)。现行 §1.4 格子样张机检断言集中涉色牌/明度档条目随样张 v3 重跑同步改造(登记待办清单)。 - -> **样张 v2 记录(2026-09-28,四冻结生效)**:机检 16/16 全绿(扩展断言集:A3 图底分离 Δ≥55/F6 墙格顶面立面双明度带 Δ≥18/F7 整板悬崖 DEPTH_PX 与游戏渲染常量同源/A6 观感密度梯度均值 ≥4.70=v1 实测 4.09×1.15)。**四冻结**——①砖面形态:方案甲 21 瓦片+高墙感 D 投影档+整板悬崖(§3.2 高墙瓦片体系条定稿参数);②色板:小人蓝 #2E86DE/接触带微光 #D6ECFF/类型微光五组定稿(§4.1/§4.2);③动画方案:§8.4 帧数档冻结;④数字形制:色牌形制定稿(#F5EAD3 素色直角小牌+Fusion Pixel+字号 max(18, ceil(格边×0.5))+墨三档,§7.4)。产物=`artwork/mockups/sample-batch-v2-2026-09-28.png`,机检报告=git 提交 f9b0765;气质终审=批末用户目检(闸形 A,2026-09-27 拍板)。**(v3.0 注:四冻结之①砖面形态的数字承载面与④数字形制随色牌降级解冻,待样张 v3 重冻;②色板③动画维持冻结不变)** - -> **样张 v3 冻结记录(2026-09-30,迭代 9 T1 批 A——四冻结①④重冻生效,圣经升 v3.1)**:机检 17/17 全绿,断言定值两件落锁——A3b 图底分离**定值 Δ≥30**(方向稿实测 38;v2 阈值 55 系色牌制地板 L0.036 时代校定,与格底分档带数学冲突退役)/A4 尺寸基线**硬等冻结**(立绘 46×48+棋盘小人源 32×32;资产升档批换代须显式解冻重校);F6 沿用 v3.4 双明度带口径 Δ≥18。**重冻两件**——①砖面形态:E 格高墙 30% 承接词表扩展(南邻墙地面格南部 30% 承接墙面、墙顶纹理**原亮度**直贴零暗档零阴影、(b)-(c) 连接处无边框〔渲染格线跳段〕、连续墙体内侧不出立面;南向厚度面+v3.4 暗档 D 投影保留)+**格底明度分档**(已开格底=世界底层档 L≈0.10,候选带 0.08–0.14;落码程序归一,地板素材重产归资产批)+装饰物件级解禁(专属形状家族,零信息纪律不变)/④数字形制:**亮墨三档直画**(1–3 冷亮 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8,直画已开格底,实测对比 6.0–6.3:1)+**色牌后备**(#F5EAD3 仅复杂格底——物件纹样/装饰变体格——垫牌兜底,牌上深墨族 12:1)。②色板③动画维持 v2 冻结不变。产物=`artwork/mockups/sample-batch-v3-final-2026-09-30.png`,机检报告=git 提交 7a847c7【冻结】;气质终审=2026-09-29 用户验收豁免(新 v3.5 方向稿验收通过,状态记录 281)。 +**样张现状**:格子样张已冻结,冻结凭据 = git 提交 7a847c7(机检报告)与 §4.1/§8.2 定值表;样张 v3 产物原件已丢失、未入 git。现行冻结态:色板与帧数档维持冻结;数字形制 = 亮墨三档直画 + 色牌后备(值见 §4.1);新增图类型/变异色板须重跑本节测试。验收:机检〈冻结项定值均以 §4.1/§8.2 对应 owner 表为准〉 --- ## 2. 氛围与情绪(Mood & Atmosphere) -> 本节服从第 1 节宣言「明快的地牢,轻快的扫荡,仔细的推理」。能量值为全作统一 0–10 刻度:0 静谧 → 10 沸腾。 +> 本节服从第 1 节宣言「明快的地牢,轻快的扫荡,仔细的推理」。能量值为全作统一 0–10 刻度:0 静谧,10 沸腾。 > -> **明度分层(硬规则,2026-09-29 v3.0 替代「明度恒定」)**:明度按视觉层分带——**信息面层**(数字/UI 数据/预告/语义标识)最高、**世界中层**(格底顶面/角色/物件)居中、**世界底层**(背景/纵深/远景/已开空间)可更低;层间明度差服务空间纵深与光照(Moonlighter 式洞穴层次)。**禁令保留(2026-10-22 缩窄至情绪域)**:明度变化禁止用于**情绪表达**——画面不得靠压暗/渐晕阴沉下来;roguelike 失败重开,世界无光暗记忆;Darkest Dungeon 式压暗仍在 §9.2 反面清单。**状态表达不受本禁令约束**:状态可用明度、色相、饱和、运动、元素任一手段承载。roguelike 失败重开,世界无光暗记忆;Darkest Dungeon 式压暗仍在 §9.2 反面清单。**允许域**:空间纵深、光照氛围、形体塑造(砖缘立体面既有例外上升为通则)。类型微光的明度梯度(§4.2)属信息面层内梯度,不扰世界层明度档。 +> **明度分层(硬规则)**:明度按视觉层分带——**信息面层**(数字/UI 数据/预告/语义标识)最高、**世界中层**(格底顶面/角色/物件)居中、**世界底层**(背景/纵深/远景/已开空间)可更低;层间明度差服务空间纵深与光照(Moonlighter 式洞穴层次)。**情绪明暗禁令**:明度变化禁止用于**情绪表达**——画面禁止靠压暗/渐晕阴沉下来;roguelike 失败重开,世界无光暗记忆;Darkest Dungeon 式压暗仍在 §9.2 反面清单。**状态表达不受本禁令约束**:状态可用明度、色相、饱和、运动、元素任一手段承载。**允许域**:空间纵深、光照氛围、形体塑造。类型微光的明度梯度(§4.2)属信息面层内梯度,不扰世界层明度档。验收:目检〈情绪表达不走压暗/渐晕;三档明度带层间灰度可分〉 -### 2.1 挖掘阶段(核心状态,占时最长) +### 2.1 挖掘阶段(核心状态,占时最长) -- **主情绪**:棋手式清明专注——视线始终被数字牵引,画面无一处杂音抢焦;久推不生视觉疲劳 -- **色彩性格**:暖石色低饱和打底(世界中层),高饱和高亮只属于信息面层(数字与封印印记) -- **氛围形容词**:清爽、井然、可读、耐心 -- **能量**:2/10 -- **承载元素**:可挖域**接触带呼吸微光**——光画在英雄格周界的接触面上(正交邻墙=整条边,斜向邻墙=共享角点+向两侧各延伸半条边,画法 §3.2,蓝族色 §4.2)——画面里唯一在动的,是玩家的思路 +- **主情绪**:棋手式清明专注——视线始终被数字牵引,画面无一处杂音抢焦;久推不生视觉疲劳 +- **色彩性格**:暖石色低饱和打底(世界中层),高饱和高亮只属于信息面层(数字与封印印记) +- **氛围形容词**:清爽、井然、可读、耐心 +- **能量**:2/10 +- **承载元素**:可挖域**接触带呼吸微光**——光画在英雄格周界的接触面上(正交邻墙 = 整条边,斜向邻墙 = 共享角点 + 向两侧各延伸半条边;画法 §3.2,蓝族色 §4.2)——画面里唯一在动的,是玩家的思路。验收:目检〈挖掘期仅接触带微光在动,棋盘与数字静止〉 ### 2.2 轮末自动战斗结算 -- **主情绪**:延迟兑现的清算爽感——扫雷之因逐行兑现为战果;完美封印(概念定义与兑付结构见《地图战斗资源》)的高光兑现——结算带高光行,以其为全场喝彩级的最高反馈档 -- **色彩性格**:情绪不走明度(§2 禁令),色彩转向金橙主导、饱和拉高;数字色转译为弹道色沿行推进 -- **氛围形容词**:酣畅、递进、一气呵成 -- **能量**:7/10,随连击爬升 -- **承载元素**:兑现光带逐行横扫棋盘,开格化箭雨、印记化暴击砸向敌人,因果全程可见可归因 +- **主情绪**:延迟兑现的清算爽感——扫雷之因逐行兑现为战果;完美封印(概念定义与兑付结构见《地图战斗资源》)的高光兑现——结算带高光行,以其为全场喝彩级的最高反馈档 +- **色彩性格**:情绪不走明度(§2 禁令),色彩转向金橙主导、饱和拉高;数字色转译为弹道色沿行推进 +- **氛围形容词**:酣畅、递进、一气呵成 +- **能量**:7/10,随连击爬升 +- **承载元素**:兑现光带逐行横扫棋盘,开格化箭雨、印记化暴击砸向敌人,因果全程可见可归因。验收:目检〈兑现光带逐行横扫、因果可见可归因;情绪表达不走压暗〉 ### 2.3 清图奖励与换图 -- **主情绪**:短促丰收——宝箱揭盖式的喜悦,三五秒即收,不拖节奏 -- **色彩性格**:全屏饱和峰值:金粒喷泉与彩纸迸发,饱和瞬时拉满后回落;明度不动(§2 情绪禁令) -- **氛围形容词**:丰收、轻快、干脆 -- **能量**:8/10,尖峰后速降 -- **承载元素**:最后一格碎砖中喷出的 GP 喷泉,叮当落入 UI 钱袋 +- **主情绪**:短促丰收——宝箱揭盖式的喜悦,三五秒即收,不拖节奏 +- **色彩性格**:全屏饱和峰值:金粒喷泉与彩纸迸发,饱和瞬时拉满后回落;明度不动(§2 情绪禁令) +- **氛围形容词**:丰收、轻快、干脆 +- **能量**:8/10,尖峰后速降 +- **承载元素**:最后一格碎砖中喷出的 GP 喷泉,叮当落入 UI 钱袋。验收:目检〈金粒喷泉短促收敛,饱和峰值后回落〉 ### 2.4 战斗胜利 -- **主情绪**:从容自信——「我算对了」的盖章确认;克制而非狂欢,胜利属于头脑 -- **色彩性格**:暖白与金,不用红与爆炸 -- **氛围形容词**:笃定、舒展、体面 -- **能量**:6/10 -- **承载元素**:棋盘中央落下一枚金色蜡封,碎砖自动拼出胜利纹章 +- **主情绪**:从容自信——「我算对了」的盖章确认;克制而非狂欢,胜利属于头脑 +- **色彩性格**:暖白与金,不用红与爆炸 +- **氛围形容词**:笃定、舒展、体面 +- **能量**:6/10 +- **承载元素**:棋盘中央落下一枚金色蜡封,碎砖自动拼出胜利纹章。验收:方向性 -### 2.5 战斗失败(冒险终结) +### 2.5 战斗失败(冒险终结) -- **主情绪**:温柔的惋惜与「再来一次」的邀请——有分量、不惩罚,像合上一册冒险日志 -- **色彩性格**:情绪不走明度(§2 禁令),色相转暖灰掺暮蓝;全作禁血红与黑场,唯此一拍最静 -- **氛围形容词**:惋惜、安静、余温、可再来 -- **能量**:1/10 -- **承载元素**:小人坐在碎砖上摇头掸灰,落下的封印印记逐枚化作光点收回行囊 +- **主情绪**:温柔的惋惜与「再来一次」的邀请——有分量、不惩罚,像合上一册冒险日志 +- **色彩性格**:情绪不走明度(§2 禁令),色相转暖灰掺暮蓝;全作禁血红与黑场,唯此一拍最静 +- **氛围形容词**:惋惜、安静、余温、可再来 +- **能量**:1/10 +- **承载元素**:小人坐在碎砖上摇头掸灰,落下的封印印记逐枚化作光点收回行囊。验收:目检〈无血红与黑场;色相转暖灰掺暮蓝〉 ### 2.6 Boss 与精英遭遇 -- **主情绪**:被注视的郑重感——预告更华丽,但预告上格规则不变,掌控仍在玩家 -- **色彩性格**:格底保持基准,Boss 专属紫赤为全场唯一占用的色相——郑重感来自色相独占,不来自压暗 -- **氛围形容词**:郑重、锋利、仪式感 -- **能量**:5/10,静水深流,结算时才沸腾 -- **承载元素**:Boss 影子先于本体爬过棋盘,扫过处短暂留下紫赤足迹(纯视觉预告;是否绑定机制由战斗侧 GDD 定夺) +- **主情绪**:被注视的郑重感——预告更华丽,但预告上格规则不变,掌控仍在玩家 +- **色彩性格**:格底保持基准,Boss 专属紫赤为全场唯一占用的色相——郑重感来自色相独占,不来自压暗 +- **氛围形容词**:郑重、锋利、仪式感 +- **能量**:5/10,静水深流,结算时才沸腾 +- **承载元素**:Boss 影子先于本体爬过棋盘,扫过处短暂留下紫赤足迹(纯视觉预告;是否绑定机制由战斗侧 GDD 定夺)。验收:目检〈紫赤足迹为纯视觉预告;高饱和紫赤仅 Boss 战占用〉 -### 2.7 下探过渡(关卡间) +### 2.7 下探过渡(关卡间) -- **主情绪**:纵深的期待——「下面还有一层」的探险引力,如降落时一瞬失重 -- **色彩性格**:整层色板随图类型(board_type)递进换挡,以色相层进预告「更深更险」;世界层明度带允许随深度整体微移(空间纵深表达,非情绪——候选,整屏样张冻结) -- **氛围形容词**:纵深、期待、换气、进阶 -- **能量**:4/10 -- **承载元素**:镜头沿竖井匀速下移,两侧砖层色板渐变,预演下一层主色 +- **主情绪**:纵深的期待——「下面还有一层」的探险引力,如降落时一瞬失重 +- **色彩性格**:整层色板随图类型(board_type)递进换挡,以色相层进预告「更深更险」;世界层明度带允许随深度整体微移(空间纵深表达,非情绪;候选,待整屏样张冻结) +- **氛围形容词**:纵深、期待、换气、进阶 +- **能量**:4/10 +- **承载元素**:镜头沿竖井匀速下移,两侧砖层色板渐变,预演下一层主色。验收:目检〈下探过渡中砖层色板随图类型渐变〉 -### 2.8 菜单与元进度(存档/永久强化) +### 2.8 菜单与元进度(存档/永久强化) -- **主情绪**:可靠的营地感——成长可见、可点、可规划,归来即蓄势 -- **色彩性格**:全作最暖:烛光黄与木色,中等饱和,无棋盘式高对比 -- **氛围形容词**:温暖、笃实、归属 -- **能量**:3/10 -- **承载元素**:**一张可点击的营地场景图**——远景天空与远山,中景木围墙(密排竖木桩、桩顶削尖、不透缝无横木)与半开的营地大门,近景中央的篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板;点画面里的物件进入对应界面(冒险/锻造工坊/秘宝匣/生涯记录/天赋树/图鉴/离开营地)。钥石余额常驻画面顶部的一行小 HUD 栏。场景构成、七物件绑定与呈现位见 §6.2 +- **主情绪**:可靠的营地感——成长可见、可点、可规划,归来即蓄势 +- **色彩性格**:全作最暖:烛光黄与木色,中等饱和,无棋盘式高对比 +- **氛围形容词**:温暖、笃实、归属 +- **能量**:3/10 +- **承载元素**:**一张可点击的营地场景图**——远景天空与远山,中景木围墙(密排竖木桩、桩顶削尖、不透缝无横木)与半开的营地大门,近景中央的篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板;点画面里的物件进入对应界面(冒险/锻造工坊/秘宝匣/生涯记录/天赋树/图鉴/离开营地)。钥石余额常驻画面顶部的一行小 HUD 栏。场景构成、七物件绑定与呈现位见 §6.2。验收:目检〈点营地物件进入对应界面,钥石余额常驻顶部 HUD 栏〉 -### 状态间区分度自检 +### 2.9 状态间区分度自检 -推理与兑现靠「谁在动 + 饱和」区分:挖掘时棋盘静止,唯光标微光在动、高饱和只在数字上;结算时棋盘自体运动,金色光带横扫、饱和扩散至全屏。胜利与失败以**色相**即判(金蜡封对暮蓝收印),Boss 战以紫赤色相是否登场作标记。 +推理与兑现靠「谁在动 + 饱和」区分:挖掘时棋盘静止,唯光标微光在动、高饱和只在数字上;结算时棋盘自体运动,金色光带横扫、饱和扩散至全屏。胜利与失败以**色相**即判(金蜡封对暮蓝收印),Boss 战以紫赤色相是否登场作标记。验收:目检〈任两场景依「谁在动+饱和」或色相可即时区分〉 -(风险呼应:自动战斗过场化 → 2.2 可见兑现与爆发支点;「我算对了」缺高光 → 2.2 爆发支点;全年龄失败表现 → 2.5 有分量不压抑。) +自动战斗过场化与「我算对了」缺高光的风险对策见 2.2;全年龄失败表现对策见 2.5。 --- ## 3. 形状语言(Shape Language) -> 规则尾注「←」标注推导源:P1–P4=游戏支柱(P1 推理的掌控感/P2 规划的延迟兑现/P3 风险与收益同源/P4 地牢下探隐喻——支柱编号源自概念期 brainstorm,现行 GDD 树不再单列,语义内联于此并见 §1.2 各原则「锚定」注);原一/二/三=1.2 支撑原则;§2.x=第 2 节条目。本节只定画法;凡涉具体变异、状态种类之处一律待 GDD 定夺,本节只规定"若有,则如何画"。 +> 规则尾注「←」标注推导来源:P1–P4=游戏支柱(P1 推理的掌控感/P2 规划的延迟兑现/P3 风险与收益同源/P4 地牢下探隐喻,语义见 §1.2 各原则「锚定」注);原一/二/三=1.2 支撑原则;§2.x=第 2 节条目。本节只定画法;凡涉具体变异、状态种类之处一律待 GDD 定夺,本节只规定"若有,则如何画"。 -**总纲|圆角为世,直线为信**:本作形状二分——一切**物件**用圆角方形(地砖、角色体块、UI 面板共用同一圆角率),一切**逻辑信息**用直线直角(数字框、准线、结算光带)。玩家凭"圆还是方"即分清世界与信息。←气质宣言;原一 +**总纲|圆角为世,直线为信**:本作形状二分——一切**物件**用圆角方形(地砖、角色体块、UI 面板共用同一圆角率),一切**逻辑信息**用直线直角(数字框、准线、结算光带)。玩家凭"圆还是方"即分清世界与信息。验收:目检〈物件呈圆角方形、逻辑信息呈直线直角〉 ←气质宣言;原一 -### 3.1 道具点剪影(角色剪影已迁 §5.1) +### 3.1 道具点剪影(角色剪影见 [character-design.md](character-design.md) §5.1) -角色四类 archetype(小人/普通怪/精英/Boss)与轮廓通道硬规则已迁 **§5.1**(2026-09-22 v1.6 迁入圣经 §5.1;2026-09-23 v1.7 随 §5 抽离迁独立文档 `character-design.md`,经 §5 存根可达);本节保留唯一非生物剪影: +角色四类 archetype(小人/普通怪/精英/Boss)与轮廓通道硬规则见 [character-design.md](character-design.md) §5.1;本节保留唯一非生物剪影: -- **道具点**:唯一非生物剪影——直线几何体(晶体/箱体),禁有机曲线;风险与收益在剪影层即为两类。←P3 +- **道具点**:唯一非生物剪影——直线几何体(晶体/箱体),禁有机曲线;风险与收益在剪影层即为两类。验收:目检〈道具点均为直线几何体,无有机曲线〉 ←P3 ### 3.2 环境几何 -- **雷图棋盘呈现基准**:战棋地块质感 × 正俯视角——取 ITB 参考图(`references/itb_1.jpg`)的地表质感骨架:地块纹理多样、起伏体块感、岛缘/砖缘厚度、格上装饰小立牌;但相机一律正上方垂直向下,格线屏轴对齐,**禁等距/斜视角与菱形格**;形状语言仍守总纲圆角方形。砖缘厚度与岛缘立体面属形体塑造,不属状态/情绪表达,不受 §2 情绪明暗禁令约束(2026-09-29 v3.0:本例外上升为 §2 明度分层允许域通则;顶面格底仍守 §4.1 参数域);装饰小立牌属 ITB 参考的观感元素,本作不采用——装饰变体走氛围层纪律(纹理级/物件级形状家族,见下方装饰变体条),零信息纪律不变。←原二;原三;§2 -- **地砖**:一切环境体由同一圆角方形衍生:未开=完整凸砖,已开=同形缺角碎裂,雷=碎裂加巢穴纹(危险格玩家说法见《排雷玩法》);只以"完整度"叙事,不为状态新造形。←原三;P4 -- **装饰变体(未开格氛围层)**:覆尘完整砖之上叠加裂纹/碎石斑/物件级点缀等装饰;2026-09-29 v3.0 物件级解禁——物件级装饰(小石块/陶片/骨殖等)须走**专属装饰形状家族**(与信息元素形状语言隔离、灰度下不与任何逻辑元素同形),数量配额按图类型定(候选 3–5 种,样张 v3 冻结),一律残缺、内凹、低对比而自然后退(§3.4);零信息纪律(分布随机、与雷/安/类型零相关、灰度下禁止制造新分类特征、分布每板一次跨轮不复掷)以原则三为准,此处不赘。←原三 -- **结果整格表达(已开格)**:已开格按当格结果整格表达(结果清单见原则三);画法——产资源格的家族物件纹样用 §3.1 道具点剪影语言(直线几何体)角嵌于砖缘,砖心与数字字形占位区留白(已开格仍承载数字);危险格巢穴纹较已开格更凶一档,形状+颜色双通道不变。←原三;P3 -- **未开格预读通道(微光单通道)**:类型预读 = 类型微光单通道(色相+明度双码,色带 §4.2);色弱类型主通道 = 微光明度梯度(组粒度——组间互辨;§2 微光明度例外);格内零角嵌/箭头/加号类附加形(2026-09-28 用户裁「微光格只留微光」,退役史见 git 提交史)。←原一;原三 -- **封印印记**:封印动作的叙事呈现 = **封印**(玩家可见专名,术语定义见 [术语表](../gdd/术语表.md)「封印」条;动作面见《排雷玩法》)——封印动作落在格上的印记称封印印记;封印色(§4.1,现行封印黑)与格内优先级栈位(§1.2)不变。封印特效(施印瞬间 + 印记常显形态)资产归清单 §4 演出类;封印印记纹理已落位(现行件=`seal_mark.png`,32×32 可选件)、字形占位退役(迭代 9 卡⑬已实现;原待办清单债务条目已完成移除)。←原一;P2;术语批 2026-09-25;封印紫批 2026-09-26;封印黑批 2026-09-28 -- **可挖域接触带微光(交互反馈层,2026-09-25 用户拍板)**:挖掘期,英雄格周界与可挖域(概念见《排雷玩法》动作合法性节;呈现义务见《HUD框架》可挖域指示)的接触面画呼吸微光——正交邻墙=该侧整条边,斜向邻墙=共享角点+向两侧各延伸半条边;光只画英雄格(空间)一侧、不进墙格(与装饰变体零冲突,数字砖心不涉);微光=空间的灯光边界(画法口径)。色相=蓝族浅一档(§4.2 蓝条;与主角蓝框的分层归样张批);呼吸=§2.1 挖掘期唯一在动元素,减动效停格。角点为斜墙与正交墙共享、不逐格归因,逐格指认=悬停金框(呈现交互归《HUD框架》悬停指示域)。←§2.1;§4.2;P1 -- **墙体**:圆角砌块堆叠,禁尖刺装饰——尖刺专属危险语义,装饰禁止伪装逻辑差异。←原三 -- **高墙瓦片体系(2026-09-26 定稿,样张批追认)**:未挖格=**一整块 32×32 自含凸起岩体**(方案甲核心定义);素材语义名——未开格素材=**墙瓦片**,已开格素材=**地面瓦片**(术语 2026-09-26 用户立)。**边界=墨线纪律**:墙瓦片与相邻格分界一律 1px 深墨线、直线贴格边、零锯齿;墨线职责=分隔,不追求与地面融合(Tiny Dungeon 样图实证调研后否决出局——TD 拼块套不进方案甲自含岩体定义,2026-09-25 终局维持方案甲;记录=git 提交 70d2320)。**光照=单一北受光**:北缘=墨线内侧 1px 微亮棱(就地取色提亮,禁宽亮带);西/东缘=墨线后直进纹理、不加修饰;南缘=墨线+立面窄暗条(约 1/3 格高,横层理叠石、禁竖条纹栅栏感)。**凹角**:角点单像素落线;南立面侧凹角加 1px 立面收头——带身与转角竖线同列收尾,禁悬空、禁突出(v9.1/v9.2 两版返工实证)。现行定稿=`game/assets/art/board/terrain_*`(16 唯一墙瓦片+5 地面瓦片变体;词表与查表逻辑见 `高墙瓦片词表(已落码 board_world.gd 查表,文件已退役)`)。**高墙感 D 投影档+整板悬崖(样张 v2 定稿 2026-09-28,用户 2026-09-27 四变体挑档)**:南临空墙格=立面口沿高光 2px `Color8(196,172,132)`+双段暗带(0.16/0.25 格边,黑罩 α0.60/0.82);北邻墙地面格=三段渐隐投影(顶带 0.22 格边,α0.28 起);整板悬崖=板底/右侧 `DEPTH_PX 8px` 暗面+列分隔竖缝+岩层横纹+口沿高光+接触 AO+底线收边+悬浮影(游戏 `board_world.gd` 同款结构件,样张脚本与游戏渲染常量同源——机检 F7 承载)。←原三;状态记录 164;样张 v2 批 2026-09-28 -- **变异格形状通道**(双通道之"形",与色无关):底砖圆角外轮廓不变,砖面内缘绘一圈定宽专属几何纹带(纹带形全作不与他类共用),砖心留白予数字与旗;验收标准=**灰度截图下仍可分类**。种类清单 = 地形变异六类型已定稿(地形变异 GDD;荆棘丛/藤蔓缠绕/岩浆/烬覆/落石/矿尘):**纹带形按轴只分两种**——伤害轴(荆棘丛/岩浆/落石)= 刺/缺排斥形,信息轴(藤蔓缠绕/烬覆/矿尘)= 遮蔽/缠绕形;同轴跨图类型只做色板变体(§4.3 `variant_tint` 槽),不做新纹形。样张批增查:岩浆纹带橙红须与危险红互辨(变异格不是雷,色相邻近混淆风险);信息轴遮蔽呈现禁止侵入数字字形占位区(§1.4 验收 1 延伸)。←原一(纹理位次第 4,让位于数字/小人/旗);原三 +- **雷图棋盘呈现基准**:战棋地块质感 × 正俯视角——取 ITB 参考图(`references/itb_1.jpg`)的地表质感骨架:地块纹理多样、起伏体块感、岛缘/砖缘厚度、格上装饰小立牌;但相机一律正上方垂直向下,格线屏轴对齐,**禁等距/斜视角与菱形格**;形状语言仍守总纲圆角方形。砖缘厚度与岛缘立体面属形体塑造,不属状态/情绪表达,不受 §2 情绪明暗禁令约束(属 §2 明度分层允许域;顶面格底仍守 §4.1 参数域)。装饰小立牌属 ITB 参考的观感元素,本作不采用;装饰变体走氛围层纪律(纹理级/物件级形状家族,见下方装饰变体条),零信息纪律以原则三为准。验收:目检〈棋盘正俯视、格线屏轴对齐、无菱形格〉 ←原二;原三;§2 +- **地砖**:一切环境体由同一圆角方形衍生:未开 = 完整凸砖,已开 = 同形缺角碎裂,雷 = 碎裂加巢穴纹(危险格玩家说法见《排雷玩法》);只以"完整度"叙事,不为状态新造形。验收:目检〈三格态仅以完整度区分,无新造状态形〉 ←原三;P4 +- **装饰变体(未开格氛围层)**:覆尘完整砖之上叠加裂纹/碎石斑/物件级点缀等装饰。物件级装饰(小石块/陶片/骨殖等)须走**专属装饰形状家族**(与信息元素形状语言隔离、灰度下不与任何逻辑元素同形),数量配额按图类型定(候选 3–5 种,样张已冻结,§1.4),一律残缺、内凹、低对比而自然后退(§3.4);零信息纪律(分布随机、与雷/安/类型零相关、灰度下禁止制造新分类特征、分布每板一次跨轮不复掷)以原则三为准,此处不赘。验收:目检〈物件级装饰不与任何逻辑元素同形;装饰残缺低对比后退〉 ←原三 +- **结果整格表达(已开格)**:已开格按当格结果整格表达(结果清单见原则三);画法——产资源格的家族物件纹样用 §3.1 道具点剪影语言(直线几何体)角嵌于砖缘,砖心与数字字形占位区留白(已开格仍承载数字);危险格巢穴纹较已开格更凶一档,形状+颜色双通道。验收:目检〈产资源格嵌家族纹样且砖心留白;巢穴纹更凶一档〉 ←原三;P3 +- **未开格预读通道(微光单通道)**:类型预读 = 类型微光单通道(色相+明度双码,色带 §4.2);色弱类型主通道 = 微光明度梯度(组粒度——组间互辨;§2 微光明度例外);格内零角嵌/箭头/加号类附加形(微光格只留微光)。验收:目检〈微光格内零角嵌/箭头/加号类附加形〉 ←原一;原三 +- **封印印记**:封印动作的叙事呈现 = **封印**(玩家可见专名,术语定义见 [术语表](../gdd/术语表.md)「封印」条;动作面见《排雷玩法》)——封印动作落在格上的印记称封印印记;封印色见 §4.1(现行封印黑),格内优先级栈位见 §1.2。封印特效(施印瞬间 + 印记常显形态)资产归清单 §4 演出类;封印印记纹理已落位(现行件 = `seal_mark.png`,32×32 可选件),数字字形占位已不存在。验收:目检〈封印印记呈现与封印黑一致,不侵入数字字形占位区〉 ←原一;P2 +- **可挖域接触带微光(交互反馈层)**:挖掘期,英雄格周界与可挖域(概念见《排雷玩法》动作合法性节;呈现义务见《HUD框架》可挖域指示)的接触面画呼吸微光——正交邻墙 = 该侧整条边,斜向邻墙 = 共享角点 + 向两侧各延伸半条边;光只画英雄格(空间)一侧、不进墙格(与装饰变体零冲突,数字砖心不涉);微光 = 空间的灯光边界(画法规则)。色相 = 蓝族浅一档(§4.2 蓝条;与主角蓝框的分层由样张测试定,§1.4);呼吸 = §2.1 挖掘期唯一在动元素,减动效停格。角点为斜墙与正交墙共享、不逐格归因,逐格指认 = 悬停金框(呈现交互归《HUD框架》悬停指示域)。验收:目检〈微光只画英雄格一侧接触面:正交邻墙整条边、斜向邻墙角点 + 半边〉 ←§2.1;§4.2;P1 +- **墙体**:圆角砌块堆叠,禁尖刺装饰——尖刺专属危险语义,装饰禁止伪装逻辑差异。验收:目检〈墙体无尖刺装饰〉 ←原三 +- **高墙瓦片体系|术语与定义**:未挖格 = **一整块 32×32 自含凸起岩体**;素材语义名——未开格素材 = **墙瓦片**,已开格素材 = **地面瓦片**。瓦片以自含岩体为单位,Tiny Dungeon 拼块套不进自含岩体定义,不作瓦片参考。验收:目检〈未开格为整块凸起岩体,无拼块接缝感〉 ←原三 +- **高墙瓦片体系|边界墨线纪律**:墙瓦片与相邻格分界一律 1px 深墨线、直线贴格边、零锯齿;墨线职责 = 分隔,不追求与地面融合。验收:目检〈分界为 1px 深墨直线贴格边、零锯齿〉 +- **高墙瓦片体系|光照纪律(单一北受光)**:北缘 = 墨线内侧 1px 微亮棱(就地取色提亮,禁宽亮带);西/东缘 = 墨线后直进纹理、不加修饰;南缘 = 墨线 + 立面窄暗条(约 1/3 格高,横层理叠石、禁竖条纹栅栏感)。验收:目检〈北缘亮棱、南缘窄暗条;无宽亮带、无竖条纹栅栏感〉 +- **高墙瓦片体系|凹角收头**:角点单像素落线;南立面侧凹角加 1px 立面收头——带身与转角竖线同列收尾,禁悬空、禁突出。验收:目检〈凹角收头与转角竖线同列,无悬空突出〉 +- **高墙瓦片体系|现行定稿**:`game/assets/art/board/terrain_*`(16 唯一墙瓦片 + 5 地面瓦片变体;词表与查表逻辑落码于 `board_world.gd`)。验收:机检〈terrain_* 素材 16 墙瓦片 + 5 地面变体在库,查表逻辑与词表一致〉 +- **高墙瓦片体系|高墙感 D 投影档与整板悬崖**:南临空墙格 = 立面口沿高光 2px `Color8(196,172,132)` + 双段暗带(0.16/0.25 格边,黑罩 α0.60/0.82);北邻墙地面格 = 三段渐隐投影(顶带 0.22 格边,α0.28 起);整板悬崖 = 板底/右侧 `DEPTH_PX 8px` 暗面 + 列分隔竖缝 + 岩层横纹 + 口沿高光 + 接触 AO + 底线收边 + 悬浮影(游戏 `board_world.gd` 同款结构件,样张脚本与游戏渲染常量同源)。验收:目检〈南临空墙格呈口沿高光+双段暗带;北邻墙地面格呈三段渐隐投影;整板悬崖结构件齐备〉 +- **变异格形状通道**(双通道之"形",与色无关):底砖圆角外轮廓不变,砖面内缘绘一圈定宽专属几何纹带(纹带形全作不与他类共用),砖心留白予数字与旗;验收标准 = **灰度截图下仍可分类**。种类清单 = 地形变异六类型已定稿(地形变异 GDD;荆棘丛/藤蔓缠绕/岩浆/烬覆/落石/矿尘):**纹带形按轴只分两种**——伤害轴(荆棘丛/岩浆/落石)= 刺/缺排斥形,信息轴(藤蔓缠绕/烬覆/矿尘)= 遮蔽/缠绕形;同轴跨图类型只做色板变体(§4.3 `variant_tint` 槽),不做新纹形。岩浆纹带橙红须与危险红互辨(变异格不是雷,色相邻近有混淆风险);信息轴遮蔽呈现禁止侵入数字字形占位区(§1.4 验收 1 延伸)。验收:目检〈六类型纹带灰度下仍可分类,同轴不新增纹形;岩浆纹带与危险红互辨〉 ←原一(纹理位次第 4,让位于数字/小人/旗);原三 -### 3.3 UI 形状语法(方向指导,细则迁 §7.1) +### 3.3 UI 形状语法(方向指导,细则见 §7.1) -**方向指导**:半拟物——容器穿戏服、数据素面;圆角=可触摸的物件,直角=须精确的信息(总纲「圆角为世,直线为信」的 UI 侧落法)。画法细则(面板/按钮/蜡封/数字色牌/参考分界)成文于 **§7.1**。←原二;§2.4 +**方向指导**:半拟物——容器穿戏服、数据素面;圆角 = 可触摸的物件,直角 = 须精确的信息(总纲「圆角为世,直线为信」的 UI 侧落法)。画法细则(面板/按钮/蜡封/数字色牌/参考分界)成文于 **§7.1**。验收:方向性 ←原二;§2.4 ### 3.4 主角形状与对比预算 -显著度=完整度:**每格至多一个"完整凸形"**(数字/小人/旗,依 1.2 优先级让位;装饰变体贴砖缘分布、不构成第二完整凸形——§4.6;数字色牌随数字一体,不作独立焦点;类型微光为底效泛光,不作焦点、不抢完整凸形预算),底砖与装饰一律残缺、内凹、低对比而自然后退;危险用刺与缺(排斥形),收益用满与圆(吸引形),同表预算、此消彼长。焦点分配双机制(2026-09-29 v3.0):**完整度**(形状,本条上文)+**明度带**(§2 明度分层——信息面层恒亮于世界层,焦点天然落信息面);明暗只做层间分离,层内禁明暗竞争(不靠明暗抢焦纪律不变)。←原二;P1;P3 +显著度 = 完整度:**每格至多一个"完整凸形"**(数字/小人/旗,依 1.2 优先级让位;装饰变体贴砖缘分布、不构成第二完整凸形——§4.6;数字色牌随数字一体,不作独立焦点;类型微光为底效泛光,不作焦点、不抢完整凸形预算),底砖与装饰一律残缺、内凹、低对比而自然后退;危险用刺与缺(排斥形),收益用满与圆(吸引形),同表预算、此消彼长。焦点分配双机制:**完整度**(形状,本条上文)+ **明度带**(§2 明度分层——信息面层恒亮于世界层,焦点天然落信息面);明暗只做层间分离,层内禁明暗竞争(不靠明暗抢焦)。验收:目检〈任一格至多一个完整凸形;信息面层恒亮于世界层〉 ←原二;P1;P3 --- ## 4. 色彩系统(Color System) -> 尾注「←」同第 3 节:P1–P4=支柱(P1 推理的掌控感/P2 规划的延迟兑现/P3 风险与收益同源/P4 地牢下探隐喻,语义见 §1.2「锚定」注);原一/二/三=1.2 支撑原则;§x=对应节。本节只定用色;凡涉具体变异、状态种类之处一律「待 GDD 定夺」,只规定"若有,则如何用色"。 +> 尾注「←」同第 3 节:P1–P4=支柱(P1 推理的掌控感/P2 规划的延迟兑现/P3 风险与收益同源/P4 地牢下探隐喻,语义见 §1.2「锚定」注);原一/二/三=1.2 支撑原则;§x=对应节。本节只定用色;凡涉具体变异、状态种类之处一律「待 GDD 定夺」,只规定"若有,则如何用色"。 -**总纲|彩为世,墨为信**:逻辑信息(数字、UI 数据、物件轮廓)一律用**近黑暖墨族**;世界与物件(砖、封印面、金、氛围)用彩。墨管可读,彩管气质。←原一/原二;§3.3 +**总纲|彩为世,墨为信**:逻辑信息(数字、UI 数据、物件轮廓)一律用**近黑暖墨族**;世界与物件(砖、封印面、金、氛围)用彩。墨管可读,彩管气质。验收:目检〈逻辑信息用近黑暖墨族,世界与物件用彩〉 ←原一/原二;§3.3 ### 4.1 主色板 -- **掩尘 `#C9AF83`**|未开格底色,覆尘完整砖:全作的"未知",画面最大沉默面。←原二;§2.1 -- **裸土 `#B39A78`**|已开格底色,碎裂裸露:更收更退;2026-09-29 v3.0 明度分层——已开格(挖出的空间)走**世界底层档**,明度低于未开格墙体一档(与 §8.5 图底分离同轴),候选 L 0.08–0.14 带(样张 v3 实测修正:L0.20+ 底配纯白也仅 3.4:1,数学不可达 4.5:1),样张 v3 冻结。←原三 -- **数字墨双色制(2026-09-29 v3.0)**|1–8 **不做八色分色**,按密度分三档色温(冷=稀疏,暖=密集);**字形为唯一数值载体**,色温只作密度氛围。①**棋盘数字呈现=按板自适应三态**(2026-10-01 v3.4 修订——v3.3 深墨族方案经渲染帧取证证伪:underdark 系地板近黑斑与近白筋并存〔双峰纹理〕,深墨直画融黑斑同样隐形,任何墨族直画不可救):暗纹理板(dungeon 系)=**亮族**三档 1–3 冷亮青白 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8 直画(样张 v3 冻结原样;判据=族最弱成员对 p90 ≥4.5:1 且对均值 ≥2.5:1);双峰纹理板(underdark/deep_underdark 系)=**色牌垫底**(素牌 DIGIT_PLATE + 深墨 #33261B 12:1,纹理免疫——v2 色牌形制在双峰板复役;判据=亮深两族皆不过门,深族均值门 3.0)。选模式管线落码单点 `board_view._digit_ink_mode`(三态:亮直/深直/落牌),机检面=`terrain_tile_sets_test` 三关呈现模式断言;**目视验证面=board_shot_probe 渲染帧取证**(机检≠看得清,2026-10-01 用户教训);缺盘平色回退态(#B39A78 中明度底)默认深墨直画;②**垫牌与 UI 数据墨=深墨族 `#33261B` 系**(L≤0.027 不变,§7.2)。八色必与红金蓝紫赤语义冲突,故弃。←原一/原二;P3 -- **封印黑 `#000000`**|封印(插旗)宣告色,无色相:玩家的宣告"此格封印"(色史:旗朱 `#C63D22` →封印紫 `#7C3AA8` 2026-09-26→**封印黑 2026-09-28 用户拍板**——紫色与微光蓝族〔魔法微光 256° 等〕色相冲突,黑色无色相天然避开 300°–345° 禁入带与全部微光色带;未开格墙顶明度 ≈113 对纯黑对比 ≥4.4:1,旗杆米白保可读)。旗朱随封印紫批退役〔棋盘侧〕,红族棋盘侧自此只剩危险红(2026-09-28 起人设背包旗=旗朱红矩形旗——旗色脱钩拍板,人设侧详见 character-design 背包旗条)。←原一;P2;封印紫批 2026-09-26;封印黑批 2026-09-28 -- **危险红 `#A8321F`**|危险色:雷=被惊动的巢穴,兑现档红,更深更沉(色值不变)。←原三;P2 -- **奖励金 `#F0A828`**|已兑现的收益:GP/宝箱/蜡封,全作唯一亮金。←P2;§2.3/2.4 -- **小人蓝 `#2E86DE`**|小人主色:棋盘唯一"己方",与光标准线同族(样张 v2 定稿 2026-09-28)。←P1;§2.1 -- **数字色牌 `#F5EAD3`**|数字底具,2026-09-29 v3.0 降级为**后备手段**:素色暖纸白直角小牌,仅复杂格底(物件纹样/变异纹带/微光叠加致直画对比不足)垫于数字正下方;素面无纹理、不随图类型/装饰变体变。←原二 -- **收益金亮档 `#E0B860`(GOLD_BRIGHT)**|UI 徽标可读性亮档 token(2026-10-14 迭代 26 反哺③登记转正):敌顺位徽标①②③与重甲徽标对舞台底 6.47:1(旧 GOLD_DEEP 3.26:1 越 §7.2 带);金族亮档,与奖励金 #F0A828 同族不同岗——奖励金=已兑现收益语义(§4.2 金条),本 token 只承载徽标可读性、不占收益语义。语义岗冲撞已裁:GOLD_DEEP/HERO_BLUE 常量本尊不动(standee 意图判据等语义岗在役) -- **小人蓝亮档 `#7EBAF0`(HERO_BLUE_BRIGHT)**|魔法护甲徽标对舞台底 5.88:1(旧 HERO_BLUE 3.23:1 越带);蓝族亮档,与小人蓝 #2E86DE 同族不同岗——本 token 只承载徽标可读性,「蓝只说己方」语义不变 -- **图标槽柔和档 `#806A4E`(SLOT_BACKDROP_SOFT)**|BCard 图标槽底(2026-10-14 迭代 26 T5):对米色卡面 PANEL_DATA 3.69:1(旧深档 SLOT_BACKDROP #3E362C 8.53:1——过强像硬贴补丁),槽内白图标 5.14:1 ≥3:1 图形参考线硬约束;仅 BCard 图标槽换档,B 区状态槽仍用深档 SLOT_BACKDROP +- **掩尘 `#C9AF83`**|未开格底色,覆尘完整砖:全作的"未知",画面最大沉默面。验收:机检〈未开格底色 = #C9AF83〉 ←原二;§2.1 +- **裸土 `#B39A78`**|已开格底色,碎裂裸露:更收更退;明度分层——已开格(挖出的空间)走**世界底层档**,明度低于未开格墙体一档(与 §8.5 图底分离同轴),L 0.08–0.14 带(更高明度底实测达不到 4.5:1,故取本带;样张已冻结,§1.4)。验收:机检〈已开格底 L 落在 0.08–0.14,对亮墨数字 ≥4.5:1〉 ←原三 +- **数字墨双色制**:1–8 **不做八色分色**,按密度分三档色温(冷 = 稀疏,暖 = 密集);**字形为唯一数值载体**,色温只作密度氛围。验收:目检〈1–8 数字按密度呈三档色温而非八色〉 ←原一/原二;P3 +- **棋盘数字呈现 = 按板自适应三态**:暗纹理板(dungeon 系)= **亮族**三档 1–3 冷亮青白 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8 直画(判据 = 族最弱成员对 p90 ≥4.5:1 且对均值 ≥2.5:1);双峰纹理板(underdark/deep_underdark 系)= **色牌垫底**(素牌 DIGIT_PLATE + 深墨 #33261B 12:1,纹理免疫——双峰纹理板上任何墨族直画都不可读,渲染帧取证;判据 = 亮深两族皆不过门,深族均值门 3.0);缺盘平色回退态(#B39A78 中明度底)默认深墨直画。选模式管线落码单点 `board_view._digit_ink_mode`(三态:亮直/深直/落牌),机检面 = `terrain_tile_sets_test` 三关呈现模式断言;目视验证面 = board_shot_probe 渲染帧取证(机检≠看得清)。验收:机检〈terrain_tile_sets_test 三关呈现模式断言全绿,board_shot_probe 渲染帧目视复核〉 +- **垫牌与 UI 数据墨 = 深墨族 `#33261B` 系**(L≤0.027,§7.2)。验收:机检〈垫牌与 UI 数据墨取深墨族,L≤0.027〉 +- **八色弃用**:八色必与红/金/蓝/紫赤语义冲突,故弃。验收:方向性 +- **封印黑 `#000000`**|封印宣告色,无色相:玩家的宣告"此格封印"。封印现行 = 无色相黑——紫色与微光蓝族(魔法微光 256° 等)色相冲突,黑色无色相天然避开 300°–345° 禁入带与全部微光色带;未开格墙顶明度 ≈113 对纯黑对比 ≥4.4:1,旗杆米白保可读。旗朱与封印紫已不用于棋盘封印旗,红族棋盘侧只剩危险红;人设背包旗 = 旗朱红矩形旗,与棋盘封印色脱钩(人设侧见 character-design 背包旗条)。验收:机检〈封印宣告色 = #000000,未开格墙顶对其对比 ≥4.4:1〉 ←原一;P2 +- **危险红 `#A8321F`**|危险色:雷 = 被惊动的巢穴,兑现档红,更深更沉。验收:机检〈危险语义色 = #A8321F〉 ←原三;P2 +- **奖励金 `#F0A828`**|已兑现的收益:GP/宝箱/蜡封,全作唯一亮金。验收:机检〈收益语义色 = #F0A828〉 ←P2;§2.3/2.4 +- **小人蓝 `#2E86DE`**|小人主色:棋盘唯一"己方",与光标准线同族。验收:机检〈己方语义色 = #2E86DE〉 ←P1;§2.1 +- **数字色牌 `#F5EAD3`**|数字底具,**后备手段**:素色暖纸白直角小牌,仅复杂格底(物件纹样/变异纹带/微光叠加致直画对比不足)垫于数字正下方;素面无纹理、不随图类型/装饰变体变。验收:机检〈色牌 L≥0.70,仅复杂格底垫用〉 ←原二 +- **收益金亮档 `#E0B860`(GOLD_BRIGHT)**|UI 徽标可读性亮档 token:敌顺位徽标①②③与重甲徽标对舞台底 6.47:1(GOLD_DEEP 3.26:1 不达 §7.2 带,故另立亮档);金族亮档,与奖励金 #F0A828 同族不同岗——奖励金 = 已兑现收益语义(§4.2 金条),本 token 只承载徽标可读性、不占收益语义。GOLD_DEEP/HERO_BLUE 常量本尊不动(standee 意图判据等语义岗现行使用)。验收:机检〈GOLD_BRIGHT 对舞台底 ≥4.5:1〉 +- **小人蓝亮档 `#7EBAF0`(HERO_BLUE_BRIGHT)**|魔法护甲徽标对舞台底 5.88:1(HERO_BLUE 3.23:1 不达带,故另立亮档);蓝族亮档,与小人蓝 #2E86DE 同族不同岗——本 token 只承载徽标可读性,不占用「蓝只说己方」的语义。验收:机检〈HERO_BLUE_BRIGHT 对舞台底 ≥4.5:1〉 +- **图标槽柔和档 `#806A4E`(SLOT_BACKDROP_SOFT)**|BCard 图标槽底:对米色卡面 PANEL_DATA 3.69:1(深档 SLOT_BACKDROP #3E362C 8.53:1 过强、像硬贴补丁,故槽底另立柔和档),槽内白图标 5.14:1,满足 ≥3:1 图形参考线硬约束;仅 BCard 图标槽换档,B 区状态槽仍用深档 SLOT_BACKDROP。验收:机检〈槽内白图标对槽底 ≥3:1〉 -**对比硬指标(可测;2026-09-29 v3.0 改写)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.55**(候选)对**已开格底 L 0.20–0.32**(候选)⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张 v3 逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)相对亮度 **L≥0.30**(墙顶面受光档)、HSV 饱和 **≤0.55**、避 Boss 禁入色相带;已开格底走世界底层档(上见)。物件识别不依赖格底(§5.1 轮廓通道),旗面、金色不作识别依赖。←原二;地形变异 AC-9 +**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)相对亮度 **L≥0.30**(墙顶面受光档)、HSV 饱和 **≤0.55**、避 Boss 禁入色相带;已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),旗面、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底 L≥0.30、饱和 ≤0.55;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9 -**色板增量纪律(2026-10-06 P2 对账批新增;数值状态 = **待定现用**)**:全作色板硬上限 **48 色**(现行 ≈45,余量 3——2026-10-14 迭代 26 新入 GOLD_BRIGHT/HERO_BLUE_BRIGHT/SLOT_BACKDROP_SOFT 三枚 UI 亮档/柔和档 token,均已过 §4.2 语义检查〔不占语义色段〕与 RGB 三通道差 ≥8/255 两道门;审计"色板逼近失控线"的封口条款);新色入板须同时过两道门:①§4.2 语义检查(不占语义色段/禁入带);②**与既有任一色的 RGB 三通道差 ≥8/255**——反例即审计实证:#3A2E22 ↔ #362B20 三通道差均 <5,肉眼不可辨,同板共存只增噪。不过门即触发「换色合并」(把意图色并进最近既有色),**禁加色绕行**;加色义务顺带登记于本表修订。**「待定现用」态释义(2026-10-06 用户立第三态)**:数值**现行即执法**(色板检查器按此拦,§8.3)——非候选(不存在"等冻结"空窗)、非死规(后续需求引入时可变);调整走本节修订并登记,改值 = 修订不是违约。 +**色板增量纪律(数值状态 = 待定现用)**:全作色板硬上限 **48 色**(防色板无限膨胀的封口条款;额度余量以色板检查器实报为准);新色入板须同时过两道门:①§4.2 语义检查(不占语义色段/禁入带);②**与既有任一色的 RGB 三通道差 ≥8/255**——反例实证:#3A2E22 ↔ #362B20 三通道差均 <5,肉眼不可辨,同板共存只增噪。不过门即触发「换色合并」(把意图色并进最近既有色),**禁加色绕行**;加色义务同时登记于本表修订。**「待定现用」态释义**:数值**现行即执法**(色板检查器按此拦,§8.3)——非候选(不存在"等冻结"空窗)、非死规(后续需求引入时可变);调整走本节修订并登记,改值 = 修订不是违约。验收:机检〈色板总数 ≤48;新色入板过两道门〉 -- **色板分层(2026-10-22 作者裁)**:色板分两层管理——① **语义/UI token 板**:全作共享,硬上限 **48 色**,管 UI 语义色、墨色、材质色,跨屏恒定;② **场景环境子板**:**每屏一套,≤32 色,允许跨屏不共享**(环境色=天空、地面、植被、石材、火焰等随场景走的颜色)。新增环境色仍须过两道门(§4.2 语义检查 + 与既有任一色 RGB 三通道差 ≥8/255),但**不占用 48 token 额度**;不过门照旧走「换色合并」,**禁加色绕行**。单张 PNG 的像素域色数上限 **≤32 色**另由 `standee_asset_check` 执法(像素域口径,与本表 token 口径互不干扰)。额度不足时呈报作者,由作者决定压缩或合并既有色。 +- **色板分层**:色板分两层管理——① **语义/UI token 板**:全作共享,硬上限 **48 色**,管 UI 语义色、墨色、材质色,跨屏恒定;② **场景环境子板**:**每屏一套,≤32 色,允许跨屏不共享**(环境色 = 天空、地面、植被、石材、火焰等随场景走的颜色)。新增环境色仍须过两道门(§4.2 语义检查 + 与既有任一色 RGB 三通道差 ≥8/255),但**不占用 48 token 额度**;不过门照旧走「换色合并」,**禁加色绕行**。单张 PNG 的像素域色数上限 **≤32 色**另由 `standee_asset_check` 执法(按单图像素域取值方式计数,与本表 token 计数互不干扰)。额度不足时向作者报告,由作者决定压缩或合并既有色。验收:机检〈token 板 ≤48;每屏环境子板 ≤32〉 ### 4.2 语义色彩规则 -> **适用域(2026-09-29 v3.0 立)**:本节「X 只说 Y」约束**信息层**(格内信息/UI 数据/预告/语义标识)与**语义物件**(GP=收益金、血条=危险红等);**纯环境氛围**(背景/装饰变体/远景/非语义物件)不受色相独占约束,改受 §3.4 焦点预算与低饱和后退约束(不抢读即可用色)。禁入带 300°–345°:信息层与语义物件禁入不变;环境层可入但限低饱和(高饱和紫赤仍 Boss 独占,§2.6)。 +> **适用域**:本节「X 只说 Y」约束**信息层**(格内信息/UI 数据/预告/语义标识)与**语义物件**(GP=收益金、血条=危险红等);**纯环境氛围**(背景/装饰变体/远景/非语义物件)不受色相独占约束,改受 §3.4 焦点预算与低饱和后退约束(不抢读即可用色)。禁入带 300°–345°:信息层与语义物件禁入不变;环境层可入但限低饱和(高饱和紫赤仍 Boss 独占,§2.6)。验收:目检〈信息层与语义物件不落禁入带、不占语义独占色相;环境层用色低饱和〉 -- **红只说危险**:危险红=兑现档(旗朱预告档随封印紫批退役,红族单档)。禁(信息层域):红禁止用于奖励、装饰性标识与常规 UI(微光群伤组除外——见「类型微光色带」条,其色相刻意错开语义红)。←原三;P2;§2.1;封印紫批 2026-09-26 -- **金只说收益与兑现**(奖励、结算、胜利)。禁(信息层与语义物件域):金禁止作危险障碍色、不上格底、不作日常 UI 强调(防通胀;微光通用增益组的芥末黄属「类型微光色带」条色带,非收益语义金)。←P2;P3;§2.2–2.4 -- **蓝只说己方与冷静信息**(小人、光标、工具信息——含可挖域接触带呼吸微光,蓝族浅一档 `#D6ECFF`〔样张 v2 定稿 2026-09-28〕,2026-09-25 拍板)。禁(信息层域):蓝禁止表示敌方危险;暮蓝 `#5A6C8C` 为失败独占(§2.5),只作全屏氛围,不作物件色。←P1;§2.5 -- **紫赤只说 Boss 与精英** `#8F2F6A`(色相 323°):郑重来自色相独占,不来自压暗。禁:**HSV 色相 300°–345° 为信息层与语义物件禁入带**——UI、粒子、类型微光、语义物件及图类型色板信息面禁止落入;环境层(背景/装饰)可入但限低饱和,高饱和紫赤仍 Boss 独占(2026-09-29 v3.0 域限定)。←§2.6;原三 -- **绿只说自然生机**(环境植被;恢复类状态若有则用绿——待增益状态定夺)。**例外:类型微光治疗组**(见下条「类型微光色带」)。禁(信息层域):绿禁止承载逻辑状态(微光色带除外),禁止与红构成唯一区分对。←原三;T1 裁决 2026-09-22 -- **类型微光色带 = 格面类型先验专用逻辑色带**(未开格类型预读的色彩通道;概念定义、可见范围与生命周期见《HUD框架》「未开格类型微光」节):未开格类型微光的信息色彩按以下五组执行——**通用增益组**(attack/defense/weaken/action/gp 五族)芥末黄 `#D6C13A`(≈52°)、**治疗组**(heal)黄绿 `#5CB83A`(≈104°)、**穿刺组**(pierce)青 `#2E9E9E`(≈180°)、**魔法组**(magic)紫罗兰 `#7B5BD6`(≈256°)、**群伤组**(blast)绯红 `#FF2D55`(≈349°,刻意避开危险红色相段与 300°–345° 禁入带)——**样张 v2 定稿(2026-09-28)**,组间互辨抽检随机检报告;v2 呈现参数=内缘带状泛光 α170/带宽 cell÷8(亮度不压格底明度档,§2 微光例外)。分域纪律:微光色相只承载类型先验,不迁移复用为物件/阵营/UI 语义色;微光 = **色相+明度双码**(2026-09-28 角嵌退役批起明度梯度承载分组(组间五色相、组内不细分——通用增益组五族同色相、不再承诺组内分型;§2 微光明度例外:梯度禁止压破格底明度档/三态分类);色弱玩家的类型主通道 = 微光双码(组粒度:组间五色相 + 各组一档明度互辨)。←T1 裁决 2026-09-22;角嵌退役批 2026-09-28;§4.6.3 -- **金/红兼作极性色相槽(§4.6)**:极性(概念定义与符号矩阵见《地图战斗资源》极性提案)的视觉取色只在这两族间——有利兑付 = 金族,有害兑付 = 红族,不新增语义色。←§4.6;原二 +- **红只说危险**:危险红 = 兑现档,红族单档。禁(信息层域):红禁止用于奖励、装饰性标识与常规 UI(微光群伤组除外——见「类型微光色带」条,其色相刻意错开语义红)。验收:目检〈信息层域红色仅出现在危险语义与微光群伤组〉 ←原三;P2;§2.1 +- **金只说收益与兑现**(奖励、结算、胜利)。禁(信息层与语义物件域):金禁止作危险障碍色、不上格底、不作日常 UI 强调(防通胀;微光通用增益组的芥末黄属「类型微光色带」条色带,非收益语义金)。验收:目检〈信息层与语义物件域金色仅承载收益与兑现〉 ←P2;P3;§2.2–2.4 +- **蓝只说己方与冷静信息**(小人、光标、工具信息——含可挖域接触带呼吸微光,蓝族浅一档 `#D6ECFF`)。禁(信息层域):蓝禁止表示敌方危险;暮蓝 `#5A6C8C` 为失败独占(§2.5),只作全屏氛围,不作物件色。验收:目检〈信息层域蓝色仅承载己方与冷静信息;敌方危险无蓝色〉 ←P1;§2.5 +- **紫赤只说 Boss 与精英** `#8F2F6A`(色相 323°):郑重来自色相独占,不来自压暗。禁:**HSV 色相 300°–345° 为信息层与语义物件禁入带**——UI、粒子、类型微光、语义物件及图类型色板信息面禁止落入;环境层(背景/装饰)可入但限低饱和,高饱和紫赤仍 Boss 独占。验收:机检〈信息层与语义物件色值色相不落 300°–345°;高饱和紫赤仅 Boss 战〉 ←§2.6;原三 +- **绿只说自然生机**(环境植被;恢复类状态若有则用绿——待增益状态定夺)。**例外:类型微光治疗组**(见下条「类型微光色带」)。禁(信息层域):绿禁止承载逻辑状态(微光色带除外),禁止与红构成唯一区分对。验收:目检〈信息层域绿色仅微光治疗组;绿红不构成唯一区分对〉 ←原三 +- **类型微光色带 = 格面类型先验专用逻辑色带**(未开格类型预读的色彩通道;概念定义、可见范围与生命周期见《HUD框架》「未开格类型微光」节):未开格类型微光的信息色彩按以下五组执行——**通用增益组**(attack/defense/weaken/action/gp 五族)芥末黄 `#D6C13A`(≈52°)、**治疗组**(heal)黄绿 `#5CB83A`(≈104°)、**穿刺组**(pierce)青 `#2E9E9E`(≈180°)、**魔法组**(magic)紫罗兰 `#7B5BD6`(≈256°)、**群伤组**(blast)绯红 `#FF2D55`(≈349°,刻意避开危险红色相段与 300°–345° 禁入带)。验收:机检〈类型微光五组色值与本表一致〉 +- **微光呈现参数**:内缘带状泛光 α170、带宽 cell÷8(亮度不压格底明度档,§2 微光例外);组间互辨以抽检核对。验收:机检〈微光泛光 α=170、带宽=cell÷8〉 +- **微光分域纪律与双码**:微光色相只承载类型先验,不迁移复用为物件/阵营/UI 语义色;微光 = **色相+明度双码**——明度梯度承载分组(组间五色相、组内不细分:通用增益组五族同色相、不承诺组内分型;§2 微光明度例外:梯度禁止压破格底明度档与三态分类);色弱玩家的类型主通道 = 微光双码(组粒度:组间五色相 + 各组一档明度互辨)。验收:目检〈五组微光两两互辨,明度梯度不压破格底明度档与三态分类〉 ←§4.6.3 +- **金/红兼作极性色相槽(§4.6)**:极性(概念定义与符号矩阵见《地图战斗资源》极性提案)的视觉取色只在这两族间——有利兑付 = 金族,有害兑付 = 红族,不新增语义色。验收:目检〈极性视觉取色仅金/红两族〉 ←§4.6;原二 ### 4.3 图类型色板换皮规则(board_type) -**换皮只换四槽,永不变信**。模板按棋盘生成产物 `board_type` 标签直接消费(键名与词表事实源 = 棋盘生成 GDD 与 `board_gen.tres`:dungeon / underdark / deep_underdark);清单与词表 owner 待 关卡节点地图/地形变异/地图战斗资源,本节只定模板: +**换皮只换四槽,永不变信**。模板按棋盘生成产物 `board_type` 标签直接消费(键名与词表事实源 = 棋盘生成 GDD 与 `board_gen.tres`:dungeon / underdark / deep_underdark);清单与词表待 关卡节点地图/地形变异/地图战斗资源 定稿后归其所有,本节只定模板。验收:机检〈换皮仅动下列四槽,其余槽位与不变槽恒定〉 -- `tile_closed` 未开底:L 与饱和同 4.1 硬指标(墙顶面受光档 L≥0.30、饱和 ≤0.55),避禁入色相带 -- `tile_open` 已开底:世界底层档(2026-09-29 v3.0,候选 L 0.08–0.14,样张 v3 冻结)、饱和 ≤0.55,避禁入色相带 -- `wall_ambient` 墙雾氛围 tint:可带主题色相,L≥0.30 -- `variant_tint` 变异纹带染槽:只改色相不改纹形(纹带形按轴两分,见 §3.2 变异格形状通道;种类 = 地形变异六类型已定稿) +- `tile_closed` 未开底:L 与饱和同 §4.1 硬指标,避禁入色相带。验收:机检〈各图类型 tile_closed 底色 L 与饱和落 §4.1 未开格底带〉 +- `tile_open` 已开底:世界底层档(L 带见 §4.1)、饱和 ≤0.55,避禁入色相带。验收:机检〈各图类型 tile_open 底色 L 落 §4.1 已开格底带〉 +- `wall_ambient` 墙雾氛围 tint:可带主题色相,L≥0.30。验收:机检〈wall_ambient tint L≥0.30〉 +- `variant_tint` 变异纹带染槽:只改色相不改纹形(纹带形按轴两分,见 §3.2 变异格形状通道;种类 = 地形变异六类型已定稿)。验收:目检〈同纹带形跨图类型仅色相不同〉 -**不变槽**:棋盘数字墨亮族三档、数字色牌(后备件)、封印黑、危险红、奖励金、小人蓝、紫赤、绿、类型微光五组——**信息与语义层全图类型恒定**;换挡动色相饱和,格底两档明度关系(墙亮/空间暗)全图类型恒定(§2 明度分层)。新增色板须重跑 §1.4 格子样张(灰度可辨+对比抽测)。←原三/原二;地形变异 AC-9 +**不变槽**:棋盘数字墨亮族三档、数字色牌(后备件)、封印黑、危险红、奖励金、小人蓝、紫赤、绿、类型微光五组——**信息与语义层全图类型恒定**;换挡动色相饱和,格底两档明度关系(墙亮/空间暗)全图类型恒定(§2 明度分层)。新增色板须重跑 §1.4 格子样张(灰度可辨+对比抽测)。验收:机检〈换皮前后不变槽色值逐项一致〉 ←原三/原二;地形变异 AC-9 -### 4.4 UI 色板(方向指导,细则迁 §7.2) +### 4.4 UI 色板(方向指导,细则迁 §7.2) -**方向指导**:数据素面的墨来自世界的墨——UI 数字/进度/准线一律主墨 `#33261B` 系;容器按场景穿材质色(营地全作最暖、战斗中性石);UI 不新增语义色,强调沿用 §4.2。色板细则成文于 **§7.2**。←原一;§2.8 +**方向指导**:数据素面的墨来自世界的墨——UI 数字/进度/准线一律主墨 `#33261B` 系;容器按场景穿材质色(营地全作最暖、战斗中性石);UI 不新增语义色,强调沿用 §4.2。色板细则成文于 **§7.2**。验收:方向性 ←原一;§2.8 ### 4.5 色弱安全清单 -依赖色相的语义对逐对点名(冗余通道:形/图标/位置/运动): +依赖色相的语义对逐对点名(冗余通道:形/图标/位置/运动): -1. 已开 vs 未开:§3.2 完整/碎裂形承载,无需另加 -2. 已开 vs 变异格:纹带形状通道,灰度截图可辨,无需另加 -3. 封印印记 vs 巢穴(危险格):印记形物件对砖面巢穴纹,无需另加 -4. 奖励金 vs 危险红(色弱下皆偏褐):§3.1 道具点两类剪影 + §3.4 满圆对刺缺 + 金粒运动,无需另加 -5. Boss 紫赤 vs 其他敌:§5.1 形状独占,无需另加 -6. 小人蓝 vs 青墨数字:明度悬殊 + 小人轮廓与底盘,无需另加 -7. **UI 红/金强调对:纹带不管面板——需补图标+文案冗余,且破坏性确认禁止与常规确认同位(位置通道)** ←原三 -8. 失败暮蓝 vs 小人蓝:状态互斥,不承载区分,无需冗余 -9. 图标键族(§4.6):图标身份全靠纹样形状通道(一键一纹样,色相不承载身份);未开格类型预读 = **类型微光单通道**(五组色带,§4.2;2026-09-28 角嵌退役批起色相+明度双码——明度档间互辨承载色弱场景);极性金/红对由框形满/缺冗余——色相全失效时图标仍可逐一辨认与定极性。←原三;增益状态×4②;T1 裁决 2026-09-22 +1. 已开 vs 未开:§3.2 完整/碎裂形承载,无需另加。验收:目检〈色弱模拟下两态以完整度可辨〉 +2. 已开 vs 变异格:纹带形状通道,灰度截图可辨,无需另加。验收:目检〈灰度截图下变异格可辨〉 +3. 封印印记 vs 巢穴(危险格):印记形物件对砖面巢穴纹,无需另加。验收:目检〈色弱模拟下印记与巢穴纹可辨〉 +4. 奖励金 vs 危险红(色弱下皆偏褐):§3.1 道具点两类剪影 + §3.4 满圆对刺缺 + 金粒运动,无需另加。验收:目检〈色弱模拟下奖励金与危险红可辨〉 +5. Boss 紫赤 vs 其他敌:§5.1 形状独占,无需另加。验收:目检〈色弱模拟下 Boss 可辨〉 +6. 小人蓝 vs 青墨数字:明度悬殊 + 小人轮廓与底盘,无需另加。验收:目检〈色弱模拟下小人与数字可辨〉 +7. **UI 红/金强调对:纹带不管面板——需补图标+文案冗余,且破坏性确认禁止与常规确认同位(位置通道)**。验收:目检〈UI 红/金强调位有图标+文案冗余,破坏性确认与常规确认不同位〉 ←原三 +8. 失败暮蓝 vs 小人蓝:状态互斥,不承载区分,无需冗余。验收:方向性 +9. 图标键族(§4.6):图标身份全靠纹样形状通道(一键一纹样,色相不承载身份);未开格类型预读 = **类型微光单通道**(五组色带,§4.2;色相+明度双码——明度档间互辨承载色弱场景);极性金/红对由框形满/缺冗余——色相全失效时图标仍可逐一辨认与定极性。验收:目检〈色相全失效时图标仍可逐一辨认与定极性〉 ←原三;增益状态×4② -### 4.6 图标键契约(Icon Key Contract,2026-09-13 冻结) +### 4.6 图标键契约(Icon Key Contract,键与通道分配已冻结) -> **白话**:图标键 = "哪个数据行配哪枚图标"的固定对照表——效果、奖励、格面类型、敌种各自的数据表主键(effect_id / reward_id / cell_type / enemy_id)直接作图标键,一枚键配一枚专属纹样。**本节冻结的是键与通道的分配,不冻结终稿美术**:纹样造型、具体色值、动画帧归格子样张(§1.4)与 asset-spec 批。先行冻结是为了 HUD框架/战斗表现开工不必等美术终稿;映射一经冻结**全局恒定、跨 run 禁重掷**(enemy_id / reward_id 键恒定同款纪律),增键走本节修订,禁运行时换图。 +> **白话**:图标键 = "哪个数据行配哪枚图标"的固定对照表——效果、奖励、格面类型、敌种各自的数据表主键(effect_id / reward_id / cell_type / enemy_id)直接作图标键,一枚键配一枚专属纹样。**本节冻结的是键与通道的分配,不冻结终稿美术**:纹样造型、具体色值、动画帧归格子样张(§1.4)与资产规格文档定稿。先行冻结是为了 HUD框架/战斗表现开工不必等美术终稿;映射一经冻结**全局恒定、跨 run 禁重掷**(enemy_id / reward_id 键恒定同款纪律),增键走本节修订,禁运行时换图。验收:机检〈各键族图标与数据表主键一一对应、跨 run 不换图〉 > -> 承接义务:增益状态×4②(effect_id→icon 映射键契约)· 地图战斗资源×5①(奖励身份恒定可读)· 敌人系统×4①②(敌种图像线索清单成文 + 全局恒定)· 极性提案 §3(类型图标/极性视觉语言)。引用方:HUD框架、战斗表现、教学引导、地图战斗资源库存呈现。 +> 承接义务:增益状态×4②(effect_id→icon 映射键契约)· 地图战斗资源×5①(奖励身份恒定可读)· 敌人系统×4①②(敌种图像线索清单成文 + 全局恒定)· 极性提案 §3(类型图标/极性视觉语言)。引用方:HUD框架、战斗表现、教学引导、地图战斗资源库存呈现。 -**通道分配(全键族通用,双通道之"形+色")**: +**通道分配(全键族通用,双通道之"形+色")**: -- **身份 = 形状纹样键**:一键一纹样,全作不复用;**色相不承载身份**(七效果不七色——防色相爆炸,亦防色弱失效)。 -- **极性 = 色相槽 + 框形**:有利兑付 = 金族(§4.2"金只说收益"),有害兑付 = 红族("红只说危险")——零新增语义色;框形冗余:满框对缺框(§3.4"收益用满与圆,危险用刺与缺")。极性判定的数据源 = 战斗结算 SettlementTable 效果语义(非 magnitude 符号——增益状态×4③),本契约只锁视觉通道、不锁数据映射。 -- **格面身份预读 = 类型微光;徽章 = 附属小图标**:格面的类型身份预读画在格上——类型走类型微光(§3.2/§4.2,底效泛光不入遮挡栈),禁止构成格内第二焦点(§3.4);徽章小图标族用于库存条与结算明细。〔角嵌位随 2026-09-28 退役批消失〕 -- **未开格预读 = 类型微光单通道**(色相+明度双码;2026-09-22 T1 裁决立双通道→2026-09-28 角嵌退役批改单通道):微光五组色带见 §4.2「类型微光色带」。只表达类型、不呈现极性、不泄雷/安(信息口径见《HUD框架》「未开格类型微光」节;约束出处见《地图战斗资源》极性提案 R3);兑付面与库存徽章可用语义族色,终稿归样张批。 +- **身份 = 形状纹样键**:一键一纹样,全作不复用;**色相不承载身份**(七效果不七色——防色相爆炸,亦防色弱失效)。验收:目检〈同族键共图、异族键纹样可辨〉 +- **极性 = 色相槽 + 框形**:有利兑付 = 金族(§4.2"金只说收益"),有害兑付 = 红族("红只说危险")——零新增语义色;框形冗余:满框对缺框(§3.4"收益用满与圆,危险用刺与缺")。极性判定的数据源 = 战斗结算 SettlementTable 效果语义(非 magnitude 符号——增益状态×4③),本契约只锁视觉通道、不锁数据映射。验收:目检〈正负极性以金满框/红缺框可辨〉 +- **格面身份预读 = 类型微光;徽章 = 附属小图标**:格面的类型身份预读画在格上——类型走类型微光(§3.2/§4.2,底效泛光不入遮挡栈),禁止构成格内第二焦点(§3.4);徽章小图标族用于库存条与结算明细。角嵌位已不存在。验收:目检〈格面类型预读仅微光且非第二焦点;徽章仅出现于库存条与结算明细〉 +- **未开格预读 = 类型微光单通道**(色相+明度双码):微光五组色带见 §4.2「类型微光色带」。只表达类型、不呈现极性、不泄雷/安(信息定义见《HUD框架》「未开格类型微光」节;约束出处见《地图战斗资源》极性提案 R3);兑付面与库存徽章可用语义族色,终稿归格子样张(§1.4)。验收:目检〈未开格类型预读仅微光,无极性与雷/安信息〉 -#### 4.6.1 效果图标键(九行 = 增益状态现行实态,2026-09-24 纹样系统定稿;键集事实源归增益状态 GDD 与 `status_effects.tres`,本表键名随其修订自动跟随) +#### 4.6.1 效果图标键(九行 = 增益状态现行实态;键集事实源归增益状态 GDD 与 `status_effects.tres`,本表键名随其修订自动跟随) -| 键(effect_id) | display_name | 纹样描述(2026-09-24 用户定稿) | 极性 | +| 键(effect_id) | display_name | 纹样描述 | 极性 | |---|---|---|---| | attack_up | 增伤 | 白色长剑为主体(大,斜向、剑尖朝右上),右下角一枚小绿色向上箭头 | 有利 | | ac_up | 盾牌加护 | 正面白色盾牌为主体(大),右下角一枚小绿色向上箭头 | 有利 | @@ -274,24 +280,24 @@ | empower | 蓄力 | 同 attack_up 白剑主体,右下角两枚小绿色向上箭头(叠放:攻击提升两档) | 有害 | | fortify | 硬化 | 与 ac_up 完全同图(同为防御提升族:防御提升一档) | 有害 | -- **纹样系统(2026-09-24 用户定稿)**:主体符号表达属性轴——白剑=攻击轴、白盾=防御轴、白眼=警觉特例;主体右下角的小箭头表达方向与幅度——绿↑=提升一档、红↓=削弱一档、双绿↑=提升两档(蓄力)。同族键共用同图:attack_up=enrage、weaken=sapped、ac_up=fortify;图标不区分归属方,主角/敌方由 HUD 状态槽挂载位置表达。极性列保留为数据语义;原九行「语义锚」文字描述(向上的力/护盾壁垒/戒备甲亮等)随本批退役。 +- **纹样系统**:主体符号表达属性轴——白剑=攻击轴、白盾=防御轴、白眼=警觉特例;主体右下角的小箭头表达方向与幅度——绿↑=提升一档、红↓=削弱一档、双绿↑=提升两档(蓄力)。同族键共用同图:attack_up=enrage、weaken=sapped、ac_up=fortify;图标不区分归属方,主角/敌方由 HUD 状态槽挂载位置表达。极性列保留为数据语义。验收:目检〈九行纹样按表落图——同族键同图、异族键可辨、箭头数表幅度〉 -- 呈现位 = HUD 状态槽(增益状态×4①);堆叠层数呈现归 HUD框架/战斗表现,不入本契约。 +- 呈现位 = HUD 状态槽(增益状态×4①);堆叠层数呈现归 HUD框架/战斗表现,不入本契约。验收:目检〈效果图标呈现于 HUD 状态槽〉 -#### 4.6.2 奖励徽章键(九型字典正负 18 行 + 清图槽 1 行 = 地图战斗资源现行实态,2026-09-22 核;键集事实源归地图战斗资源 GDD 与 `reward.tres`,本表键名随其修订自动跟随) +#### 4.6.2 奖励徽章键(九型字典正负 18 行 + 清图槽 1 行 = 地图战斗资源现行实态;键集事实源归地图战斗资源 GDD 与 `reward.tres`,本表键名随其修订自动跟随) | 键(reward_id) | 型/极性 | 徽章语义锚 | 极性视觉 | |---|---|---|---| -| attack_up | attack 正向 | 增伤增益(与 §4.6.1 同语义域共族、键分立) | 金族满框 | +| attack_up | attack 正向 | 增伤增益(与 §4.6.1 同语义域共族、键分立) | 金族满框 | | enrage | attack 负向 | 敌方增伤 | 红族缺框 | | ac_up | defense 正向 | 盾牌加护 | 金族满框 | -| off_guard | defense 负向 | 主角失衡(与 §4.6.1 共族、键分立) | 红族缺框 | +| off_guard | defense 负向 | 主角失衡(与 §4.6.1 共族、键分立) | 红族缺框 | | heal_minor | heal 正向 | 小额回复 | 金族满框 | -| heal_backlash | heal 负向 | 恢复倒扣(直伤) | 红族缺框 | +| heal_backlash | heal 负向 | 恢复倒扣(直伤) | 红族缺框 | | spark_aoe | blast 正向 | 爆破群伤 | 金族满框 | -| enemy_alert | blast 负向 | 警觉(引用 alert 状态行,与 §4.6.1 alert 共族) | 红族缺框 | -| weaken | weaken 正向 | 削弱(与 §4.6.1 共族) | 金族满框 | -| sapped | weaken 负向 | 乏力(与 §4.6.1 共族) | 红族缺框 | +| enemy_alert | blast 负向 | 警觉(引用 alert 状态行,与 §4.6.1 alert 共族) | 红族缺框 | +| weaken | weaken 正向 | 削弱(与 §4.6.1 共族) | 金族满框 | +| sapped | weaken 负向 | 乏力(与 §4.6.1 共族) | 红族缺框 | | pierce_strike | pierce 正向 | 穿刺弹——锐锥 | 金族满框 | | pierce_snare | pierce 负向 | 穿刺陷阱 | 红族缺框 | | magic_bolt | magic 正向 | 魔法矢——秘法/星辰 | 金族满框 | @@ -302,13 +308,12 @@ | gp_lose | gp 负向 | 破财 | 红族缺框 | | clear_blast | 清图槽 | 清图群伤——blast 族、整图作用域 | 按正向视觉 | -- 负向徽标用于踩雷反噬的结算呈现;权重与摇号语义见《地图战斗资源》。 -- 库存常读条义务与兑付/库存语义见《地图战斗资源》。 -- 格面预读按**家族类型**微光呈现(§4.6.3;呈现粒度决策见《地图战斗资源》极性提案 R1)。 -- **2026-09-22 勘误**:修订指导底稿曾记 gp 组键名为 gp_gain/gp_loss,以 `reward.tres` 实际键 **gp_coin/gp_lose** 为准(配置是事实源)。 -- **资产契约(2026-10-06 P1-2 批——审计「19 枚格面徽章 0 张」缺口完成;状态图标 §4.6.1 模式复刻「键→文件名→尺寸」;尺寸候选,样张批冻结)**:19 枚 = `ui/icons/reward_.png`(键 = 本表 reward_id,正向/负向/清图槽同模式),源 40×40、显示 20px(格面小记号级)/40px(明细面)双档候选;极性视觉按本表「极性视觉」列(金族满框/红族缺框 = 形状+色彩双通道纪律)。**契约立前此族 0 张为已知缺口**(审计 P1-2),程序化占位在役;出图工单归 art-assets-checklist/PRD,交付后 art-status-checklist 顺带 ✅。 +- 负向徽标用于踩雷反噬的结算呈现;权重与摇号语义见《地图战斗资源》。验收:目检〈踩雷反噬结算以红族缺框徽标呈现〉 +- 库存常读条义务与兑付/库存语义见《地图战斗资源》。验收:方向性 +- 格面预读按**家族类型**微光呈现(§4.6.3;呈现粒度决策见《地图战斗资源》极性提案 R1)。验收:目检〈格面预读仅到家族类型粒度〉 +- **资产契约**:19 枚 = `ui/icons/reward_.png`(键 = 本表 reward_id,正向/负向/清图槽同模式;状态图标 §4.6.1 同款「键→文件名→尺寸」模式),源 40×40、显示 20px(格面小记号级)/40px(明细面)双档,尺寸为候选,样张验收时冻结;极性视觉按本表「极性视觉」列(金族满框/红族缺框 = 形状+色彩双通道纪律)。本族现无终稿图,以程序化占位承载;出图工单归 art-assets-checklist/PRD,交付后同步登记 art-status-checklist ✅。验收:机检〈reward_*.png 文件名键与本表 reward_id 一致,源 40×40〉 -#### 4.6.3 格面类型图标键(九类——地图战斗资源现行实态;键契约冻结,终稿归样张批) +#### 4.6.3 格面类型图标键(九类 = 地图战斗资源现行实态;键契约已冻结,终稿归格子样张 §1.4) | 键(cell_type) | 类型 | 纹样语义锚 | |---|---|---| @@ -322,229 +327,229 @@ | action | 行动族 | 行动点/疾步 | | gp | GP 族 | 钱币/财货 | -- **呈现约束(2026-09-28 角嵌退役批更新)**:未开格呈现**类型微光单通道**(五组色带,§4.2「类型微光色带」,色相+明度双码;样张 v2 定稿 2026-09-28;可见范围与时机见《HUD框架》「未开格类型微光」节);不呈现极性、不呈现雷/安(信息口径见该权威定义节);样张批微光组间互辨抽测含明度档互辨(误读率指标出处见《地图战斗资源》极性提案)。 -- **状态声明**:键集九值封闭(attack/defense/heal/blast/weaken/pierce/magic/action/gp,与 RewardTable 现行 cell_type 词表一致),扩键走本节修订。 -- 与 §4.6.1 的关系:同语义域的键(attack↔attack_up 等)可共族设计,键分立、纹样须可区分(格面小记号级对槽内图标级)。 -- **资产契约(2026-10-06 P1-2 批——审计「6 类地形变异纹带 0 张」缺口完成;尺寸候选,样张批冻结)**:纹带族 = `board/band_.png`(键 = 变异类型 id,键集事实源 = [地形变异](../gdd/systems/排雷循环/地形变异.md)「变异框架与类型清单」region 六行〔荆棘丛/藤蔓缠绕/岩浆/烬覆/落石/矿尘〕+ 地形变异文档的池门槛域;通配 global 型〔贫矿脉〕无格子布置域、**不在纹带族**),源 64×64 四边无缝(叠于已开格底)、显示 1× = 格边长。呈现锚 = 「变异格未开时无预告呈现(遭遇式)」——纹带是**已开变异格**的标识层(地形变异 GDD「视觉标识归表现层升级批」的素材承载),半透明叠加、禁止侵入数字字形占位区(§4.1 砖心留白同理)。程序化纹带在役 = 占位(◐);出图工单归 PRD,交付后 art-status-checklist 顺带 ✅。 +- **呈现约束**:未开格呈现**类型微光单通道**(五组色带见 §4.2「类型微光色带」,色相+明度双码;可见范围与时机见《HUD框架》「未开格类型微光」节);不呈现极性、不呈现雷/安(信息定义见该权威定义节);微光组间互辨抽测含明度档互辨(误读率指标出处见《地图战斗资源》极性提案)。验收:目检〈未开格仅微光呈现,无极性与雷/安信息;五组互辨含明度档〉 +- **状态声明**:键集九值封闭(attack/defense/heal/blast/weaken/pierce/magic/action/gp,与 RewardTable 现行 cell_type 词表一致),扩键走本节修订。验收:机检〈cell_type 键集九值与 RewardTable 词表一致〉 +- 与 §4.6.1 的关系:同语义域的键(attack↔attack_up 等)可共族设计,键分立、纹样须可区分(格面小记号级对槽内图标级)。验收:目检〈同语义域两键纹样同级可辨〉 +- **资产契约**:纹带族 = `board/band_.png`(键 = 变异类型 id,键集事实源 = [地形变异](../gdd/systems/排雷循环/地形变异.md)「变异框架与类型清单」region 六行〔荆棘丛/藤蔓缠绕/岩浆/烬覆/落石/矿尘〕+ 地形变异文档的池门槛域;通配 global 型〔贫矿脉〕无格子布置域、**不在纹带族**),源 64×64 四边无缝(叠于已开格底)、显示 1× = 格边长,尺寸为候选,样张验收时冻结。呈现锚 = 「变异格未开时无预告呈现(遭遇式)」——纹带是**已开变异格**的标识层(地形变异 GDD 视觉标识条款的素材承载),半透明叠加、禁止侵入数字字形占位区(§4.1 砖心留白同理)。现以程序化占位承载(◐);出图工单归 PRD,交付后同步登记 art-status-checklist ✅。验收:机检〈band_*.png 文件名键与六类型 id 一致,源 64×64〉 -#### 4.6.4 敌种线索键(映射键 = enemy_id;键集事实源归敌人系统 GDD 与 `data/tables/enemy.tres`,2026-09-22 核) +#### 4.6.4 敌种线索键(映射键 = enemy_id;键集事实源归敌人系统 GDD 与 `data/tables/enemy.tres`) | 线索通道 | 冻结内容 | |---|---| -| 意图姿态 | **动画三档**(2026-09-18 用户拍板):① 普通攻击 = 常态待机(零独立资产);② 攻击技能 = 特殊准备动画(放光/高举武器);③ 重击/硬化类 = 蓄力动作或防御姿态。不显示文字与技能名(信息口径见《敌人系统》意图线索节);三档在灰度/剪影层可辨(双通道纪律) | -| 强度档位 | 体量通道:§5.1 archetype 体量/垂直延伸("大个子多半有重击");护甲类型(light/ward/heavy)视觉区分方案已落 §5.3(候选,样张批冻结) | -| 预告时序 | 预告语义见《敌人系统》意图线索节,呈现义务(挖掘期全程可见)见《HUD框架》;本表只锁「结算不另设独立意图播片」的动画形态 | -| 恒定义务 | enemy_id 键映射跨 run 恒定、禁重掷;图像不报数——精确数值的明细承载见《战斗结算》 | +| 意图姿态 | **动画三档**:① 普通攻击 = 常态待机(零独立资产);② 攻击技能 = 特殊准备动画(放光/高举武器);③ 重击/硬化类 = 蓄力动作或防御姿态。不显示文字与技能名(信息定义见《敌人系统》意图线索节);三档在灰度/剪影层可辨(双通道纪律) | +| 强度档位 | 体量通道:§5.1 archetype 体量/垂直延伸("大个子多半有重击");护甲类型(light/ward/heavy)视觉区分方案已落 §5.3(候选,样张验收时冻结) | +| 预告时序 | 预告语义见《敌人系统》意图线索节,呈现义务(挖掘期全程可见)见《HUD框架》;本表只锁「结算不另设独立意图播片」的动画形态 | +| 恒定义务 | enemy_id 键映射跨 run 恒定、禁重掷;图像不报数——精确数值的明细承载见《战斗结算》。验收:机检〈enemy_id 键映射跨 run 恒定、禁重掷〉 | -- **敌种键集 = 八敌种**(registry enemy mvp_content,2026-09-25 核):goblin 地精(light)/ bugbear 熊地精(light;**L2 熊地精 = bugbear 复用**,第二关三行显示名熊地精系,同剪影换色板,非独立键)/ ogre_chief 食人魔酋长(light,第一关关底 Boss)/ giant_spider 巨蜘蛛(light,群行 [2,3])/ drider 蛛化卓尔精灵(ward,第二关 Boss)/ duergar 灰矮人(heavy)/ hook_horror 钩爪恐魔(light,群行)/ duergar_king 灰矮人石君王(heavy,第三关 Boss)。键数据与甲档实态见《敌人系统》与 `enemy.tres`。 -- **资产需求下限三类**(敌人系统登记,呈现归 HUD框架/战斗表现):受击反馈(可共用一组)/ 死亡表现(**灰化+碎裂退场,禁止直接消失**)/ 顺位标记(敌多时可读)。 -- Boss 表现约束:§2.6 紫赤色相独占 + §5.1 Boss 形状独占双通道;键集扩容随内容批入本表。 -- 剪影通用性:剪影禁止借特定格底色成立(§5.1)——棋盘/结算/任一战斗舞台同一套。 +- **敌种键集 = 八敌种**:goblin 地精(light)/ bugbear 熊地精(light;**L2 熊地精 = bugbear 复用**,第二关三行显示名熊地精系,同剪影换色板,非独立键)/ ogre_chief 食人魔酋长(light,第一关关底 Boss)/ giant_spider 巨蜘蛛(light,群行 [2,3])/ drider 蛛化卓尔精灵(ward,第二关 Boss)/ duergar 灰矮人(heavy)/ hook_horror 钩爪恐魔(light,群行)/ duergar_king 灰矮人石君王(heavy,第三关 Boss)。键数据与甲档实态见《敌人系统》与 `enemy.tres`。验收:机检〈八敌种键集与 enemy.tres 一致〉 +- **资产需求下限三类**(敌人系统登记,呈现归 HUD框架/战斗表现):受击反馈(可共用一组)/ 死亡表现(**灰化+碎裂退场,禁止直接消失**)/ 顺位标记(敌多时可读)。验收:目检〈三类呈现齐备,死亡为灰化+碎裂退场非直接消失〉 +- Boss 表现约束:§2.6 紫赤色相独占 + §5.1 Boss 形状独占双通道;键集扩容时随内容修订入本表。验收:目检〈Boss 呈现同时满足色相与形状双独占〉 +- 剪影通用性:剪影禁止借特定格底色成立(§5.1)——棋盘/结算/任一战斗舞台同一套。验收:目检〈同一剪影在棋盘与结算两舞台均成立〉 --- ## 5. 角色设计(Character Design) -> **本节内容已整体迁至独立文档 [`character-design.md`](character-design.md)**(2026-09-23 v1.7 抽离批;小节号 §5.1–§5.6 在该文档中沿用不变)。迁移动机:人设高频改动,独立成档免动圣经;本存根保证既有 "art-bible §5.x" 式引用(§4.1/§4.5/§4.6.4、art-assets-checklist、战斗表现)经此仍可达,新引用直指该文档。 +> **本节内容已整体迁至独立文档 [`character-design.md`](character-design.md)**(小节号 §5.1–§5.6 在该文档中沿用不变)。迁移动机:人设高频改动,独立成档免动圣经;本存根保证既有 "art-bible §5.x" 式引用(§4.1/§4.5/§4.6.4、art-assets-checklist、战斗表现)经此仍可达,新引用直指该文档。 -内容速览:§5.1 角色剪影与轮廓通道 · §5.2 头身比与体量档位 · §5.3 护甲类型视觉区分(候选,样张批冻结) · §5.4 主角三态规格(形象锚 = 蓝兜帽披肩的暖装小冒险者:**圆头挖掘铲 + 冒险背包插旗朱红矩形旗(2026-09-28 与棋盘封印色脱钩) + 护目镜腰灯**,2026-09-23 特征批〔旗色史:旗朱→封印紫→封印黑 2026-09-28〕) · §5.5 八敌种逐卡 · §5.6 意图准备动画三档。 +内容速览:§5.1 角色剪影与轮廓通道 · §5.2 头身比与体量档位 · §5.3 护甲类型视觉区分(候选,样张冻结终稿) · §5.4 主角三态规格(形象锚 = 蓝兜帽披肩的暖装小冒险者:圆头挖掘铲 + 冒险背包插朱红矩形旗(与棋盘封印色脱钩) + 护目镜腰灯) · §5.5 八敌种逐卡 · §5.6 意图准备动画三档。 --- ## 6. 环境与场景(Environment & Scenes) -> 本节定格面之外的场景容器怎么画:战斗舞台背景、营地场景、三关图类型色板方向、关卡间下探过渡、棋盘视野迷雾遮蔽(§6.5)。格内画法归 §3.2,情绪与能量归 §2,换色模板归 §4.3;本节不新增信息通道、不承载玩法语义。屏级分区布局(三区 B/C/D)的裁决权归 battle.md,本节只定环境资产的视觉规格。 +> 本节定格面之外的场景容器怎么画:战斗舞台背景、营地场景、三关图类型色板方向、关卡间下探过渡、棋盘视野迷雾遮蔽(§6.5)。格内画法归 §3.2,情绪与能量归 §2,换色模板归 §4.3;本节不新增信息通道、不承载玩法语义。屏级分区布局(三区 B/C/D)的裁决权归 battle.md,本节只定环境资产的视觉规格。 -### 6.1 战斗舞台背景(B 区环境) +### 6.1 战斗舞台背景(B 区环境) -- **双舞台常驻下的 B 区环境**:战斗画面区(站桩立绘所在)的背景 = 明亮地窟,与 §1 风格基调同源——暖倾向、无压抑;背景整体**低对比自然后退**(§3.4 焦点预算延伸):背景禁止构成完整凸形、禁止抢 B 区立绘与 D 区数字的焦点,识别层级恒为 立绘 > 背景。 -- **舞台正面目标(2026-09-29 v3.0 新增——原条只有负面约束,病根 6)**:B 区 = **地窟看台**——立绘脚下地台/落影承载(世界中层),背景环境层低饱和后退、明度走世界底层档,允许光源氛围(洞顶光柱/暖光晕候选);角色禁止直接贴平色底。规格与终值归整屏样张闸(§1.4)冻结。 -- **明度分层适用(2026-09-29 v3.0 改条)**:背景氛围的情绪表达仍禁止以压暗/渐晕实现(§2 禁令);空间纵深与洞穴感走明度分层允许域——世界底层档、光照氛围、色相饱和与形体承载(§3.2 同款逻辑)。 -- **图类型联动**:背景 tint 走 §4.3 `wall_ambient` 槽,随三关换挡(方向候选见 §6.3);紫赤仅 Boss 战允许短暂进入背景氛围纹样(§2.6 色相独占的舞台侧延伸,高饱和独占不变),非 Boss 战高饱和紫赤禁入。 -- **资产渠道**:✦ 插画级(演出类场景贴图,清单 §4 联动登记);战斗一屏样张(§1.4 清单 1.2)含背景出样。**资产契约(2026-10-06 P1-2 批——审计「B 区舞台背景插画 0 张」缺口完成;尺寸候选,样张批冻结)**:基准一张 = `stage/stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张——换皮成本复用棋盘同款逻辑),显示 1×;呈现约束按本节各条(低对比后退/明度世界底层档/光柱氛围保留)。程序化平色+光柱在役 = 占位(◐);插画工单归 PRD,交付后 art-status-checklist 顺带 ✅。 +- **双舞台常驻下的 B 区环境**:战斗画面区(站桩立绘所在)的背景 = 明亮地窟,与 §1 风格基调同源——暖倾向、无压抑;背景整体**低对比自然后退**(§3.4 焦点预算延伸):背景禁止构成完整凸形、禁止抢 B 区立绘与 D 区数字的焦点,识别层级恒为 立绘 > 背景。验收:目检〈背景无完整凸形、立绘最抢眼〉 +- **舞台正面目标**:B 区 = **地窟看台**——立绘脚下地台/落影承载(世界中层),背景环境层低饱和后退、明度走世界底层档,允许光源氛围(洞顶光柱/暖光晕候选);角色禁止直接贴平色底。规格与终值归整屏样张闸(§1.4)冻结。验收:目检〈立绘脚下有地台/落影,非贴平色底〉 +- **明度分层适用**:背景氛围的情绪表达仍禁止以压暗/渐晕实现(§2 禁令);空间纵深与洞穴感走明度分层允许域——世界底层档、光照氛围、色相饱和与形体承载(§3.2 同款逻辑)。验收:方向性 +- **图类型联动**:背景 tint 走 §4.3 `wall_ambient` 槽,随三关换挡(方向候选见 §6.3);紫赤仅 Boss 战允许短暂进入背景氛围纹样(§2.6 色相独占的舞台侧延伸,高饱和独占不变),非 Boss 战高饱和紫赤禁入。验收:机检〈背景吸色无非 Boss 高饱和紫赤〉 +- **资产渠道**:✦ 插画级(演出类场景贴图,清单 §4 联动登记);战斗一屏样张(§1.4 清单 1.2)含背景出样。**资产契约(尺寸候选,样张冻结终稿)**:基准一张 = `stage/stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张——换皮成本复用棋盘同款逻辑),显示 1×;呈现约束按本节各条(低对比后退/明度世界底层档/光柱氛围保留)。程序化平色+光柱现行使用 = 占位;插画工单归 PRD,交付后 art-status-checklist 同步 ✅。 ### 6.2 营地场景 -- **情绪与色域**:承载 §2.8——全作最暖:烛光黄 `#F6C667` × 木色 `#8A5A34`(§7.2),中等饱和,无棋盘式高对比。 -- **场景构成**:一张可点击的营地场景图(1280×800,与工程档同幅面)。远景=天空与远山;中景=木围墙(密排竖木桩、桩顶削尖、不透缝无横木)与**半开的营地大门**(门洞内一条通向远处的小路);近景=中央篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板。**中景带自底部往上约 2/3 处起**;**近景物件可覆盖围墙(围墙是纯背景板),唯一例外=中间门洞必须留空、不被任何物件遮挡**。七个可点物件的功能绑定归 `camp.md`,本节只定画法。 -- **交付形态**:**背景 1 层 + 七个物件各 1 层**(**帐篷**为承载件、不可点),共 8 张透明 PNG + 一份坐标表(贴图矩形/命中矩形/z 序)。**篝火例外**:命中矩形只取下半(火堆),上半(火焰)只作特效展示。 -- **色彩与氛围依据**:营地场景的冷暖分工(冷的大面积后退、暖的小面积前进)、空气透视三档与大面积用色口径 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`。本节不复述色值;新增环境色按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门)。 -- **交互四态(零新素材,全部程序化)**:悬停=**轮廓亮边 + 浮动**(浮动须整像素吸附);按下=位移 1px;**禁用/未解锁=降明度 + 去饱和**(§2 已缩窄至情绪域,状态表达不受限),**仅锻炉(锻造工坊)与宝箱(秘宝匣)两件会有此态**;已解锁=常态。 -- **钥石余额**:常驻画面顶部的一行小 HUD 栏(高 48px,**浮层覆盖,不挤压画面**)。 -- **事实源边界**:解锁条件/价格/存档写入归元进度 GDD 与 `MetaProgression`;素材命名与路径归 `asset-path-map`;清单登记归 `art-assets-checklist` §3.5。 -- **待裁(本节不填数,由作者定)**:① **营地环境子板色值**——色彩脚本只给定色方向,落地色值属 §4.1 单点,须过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门;② **门扇与门洞的高宽比例**——中景带自底部往上约 2/3 处起之后高度有限,门扇高度远超围墙带,两者关系待作者重定;③ **帐篷的落地尺寸**——帐篷体量大于被替换的旧承载件,尺寸待作者重定;坐标草案的现行出口见 `camp.md` 的指路句。 +- **情绪与色域**:承载 §2.8——全作最暖:烛光黄 `#F6C667` × 木色 `#8A5A34`(§7.2),中等饱和,无棋盘式高对比。 +- **场景构成**:一张可点击的营地场景图(1280×800,与工程档同幅面)。远景=天空与远山;中景=木围墙(密排竖木桩、桩顶削尖、不透缝无横木)与**半开的营地大门**(门洞内一条通向远处的小路);近景=中央篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板。**中景带自底部往上约 2/3 处起**;**近景物件可覆盖围墙(围墙是纯背景板),唯一例外=中间门洞必须留空、不被任何物件遮挡**。七个可点物件的功能绑定归 `camp.md`,本节只定画法。 +- **交付形态**:**背景 1 层 + 七个物件各 1 层**(**帐篷**为承载件、不可点),共 8 张透明 PNG + 一份坐标表(贴图矩形/命中矩形/z 序)。**篝火例外**:命中矩形只取下半(火堆),上半(火焰)只作特效展示。 +- **色彩与氛围依据**:营地场景的冷暖分工(冷的大面积后退、暖的小面积前进)、空气透视三档与大面积用色规则 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`。本节不复述色值;新增环境色按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门)。 +- **交互四态(零新素材,全部程序化)**:悬停=**轮廓亮边 + 浮动**(浮动须整像素吸附);按下=位移 1px;**禁用/未解锁=降明度 + 去饱和**(§2 已缩窄至情绪域,状态表达不受限),**仅锻炉(锻造工坊)与宝箱(秘宝匣)两件会有此态**;已解锁=常态。 +- **钥石余额**:常驻画面顶部的一行小 HUD 栏(高 48px,**浮层覆盖,不挤压画面**)。 +- **事实源边界**:解锁条件/价格/存档写入归元进度 GDD 与 `MetaProgression`;素材命名与路径归 `asset-path-map`;清单登记归 `art-assets-checklist` §3.5。 +- **待裁(本节不填数,由作者定)**:① **营地环境子板色值**——色彩脚本只给定色方向,落地色值属 §4.1 单点,须过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门;② **门扇与门洞的高宽比例**——中景带自底部往上约 2/3 处起之后高度有限,门扇高度远超围墙带,两者关系待作者重定;③ **帐篷的落地尺寸**——帐篷体量大于被替换的旧承载件,尺寸待作者重定;坐标草案的现行出口见 `camp.md` 的指路句。 -### 6.3 三关图类型色板候选(board_type 换挡方向) +### 6.3 三关图类型色板候选(board_type 换挡方向) -- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节只给三关**方向性候选**,终值按 §4.3 现行规则重跑 §1.4 格子样张冻结。 -- **深度轴叙事**(§2.7 下探过渡):色相层进预告「更深更险」,明度档不动—— - - **dungeon 地精洞窟**(第一关):现行掩尘系暖石棕(`#C9AF83` 基)即基准档,`wall_ambient` 暖棕; - - **underdark 幽暗地域**(第二关;2026-09-23 用户裁方向:**冷调幽暗**):主调**灰白蓝**候选(低饱和蓝灰域,`tile_closed`),少量紫黑倾向点缀——紫系取紫罗兰域(≈256°)候选,禁入带 300°–345° 不变;`wall_ambient` 冷蓝灰; - - **deep_underdark 灰矮人矿城**(第三关;2026-09-23 用户裁方向):**灰黑主调 + 矿脉金属色点缀**候选(守 §4.1 明度地板 L≥0.30);矿脉点缀走金属质感族(冷银/铜灰候选)——「金属色」以材质感承载,奖励金亮金仍禁上格底(§4.2),不占用收益语义金;`wall_ambient` 铁灰。 -- **共同纪律**:三套候选色相均须与五组类型微光色带(§4.2)抽测互辨(格底与微光同在未开格面,邻近色相有吞没风险);避 300°–345° 禁入带;L≥0.30、饱和 ≤0.55(§4.1 硬指标)。 +- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节只给三关**方向性候选**,终值按 §4.3 现行规则重跑 §1.4 格子样张冻结。 +- **深度轴叙事**(§2.7 下探过渡):色相层进预告「更深更险」,明度档不动—— + - **dungeon 地精洞窟**(第一关):现行掩尘系暖石棕(`#C9AF83` 基)即基准档,`wall_ambient` 暖棕; + - **underdark 幽暗地域**(第二关;方向:**冷调幽暗**):主调**灰白蓝**候选(低饱和蓝灰域,`tile_closed`),少量紫黑倾向点缀——紫系取紫罗兰域(≈256°)候选,禁入带 300°–345° 不变;`wall_ambient` 冷蓝灰; + - **deep_underdark 灰矮人矿城**(第三关;方向:**灰黑主调 + 矿脉金属色点缀**候选,守 §4.1 明度地板 L≥0.30):矿脉点缀走金属质感族(冷银/铜灰候选)——「金属色」以材质感承载,奖励金亮金仍禁上格底(§4.2),不占用收益语义金;`wall_ambient` 铁灰。 +- **共同纪律**:三套候选色相均须与五组类型微光色带(§4.2)抽测互辨(格底与微光同在未开格面,邻近色相有吞没风险);避 300°–345° 禁入带;L≥0.30、饱和 ≤0.55(§4.1 硬指标)。 ### 6.4 关卡间下探过渡 -- **视觉承载**(§2.7 情绪的画法面):竖井下移镜头,两侧砖层色板渐变,预演下一层主色——渐变取色 = 下一关 `tile_closed`/`wall_ambient` 槽候选值入画,只动色相/饱和;明度带随深度微移候选归 §2.7(空间纵深表达,非情绪)。 -- **资产与排期**:程序化色板渐变(⚙ 脚本管线),无独立插画需求(候选);演出编排与时序归冒险结构/战斗表现 GDD,本节只定渐变取色与明度纪律。 +- **视觉承载**(§2.7 情绪的画法面):竖井下移镜头,两侧砖层色板渐变,预演下一层主色——渐变取色 = 下一关 `tile_closed`/`wall_ambient` 槽候选值入画,只动色相/饱和;明度带随深度微移候选归 §2.7(空间纵深表达,非情绪)。 +- **资产与排期**:程序化色板渐变(⚙ 脚本管线),无独立插画需求(候选);演出编排与时序归冒险结构/战斗表现 GDD,本节只定渐变取色与明度纪律。 -### 6.5 视野迷雾与黑暗区域渲染(棋盘灯光区,2026-10-03 立节) +### 6.5 视野迷雾与黑暗区域渲染(棋盘灯光区) -> **白话**:战斗棋盘不再整板可见——只有以角色为中心的菱形灯光区看得清,区外没开过的格子沉入全黑,开过的格子留下灰阶的「记忆」。像在洞穴里举灯探索。机制口径(可见性/生命周期/纯呈现纪律)的现行事实源 =《HUD框架》「视野迷雾:菱形灯光区与黑暗边界」节(全项目唯一事实源,引用只指路),该节点名「黑暗区域渲染规格(暗淡态明度、迷雾过渡)归 art-bible」,本节即其承载——只管"怎么画",不复述机制。纯呈现层:格态、数字真值、清图判定零影响;零素材新增(全部程序化遮罩/调制);类型微光语义零改动。 +> **白话**:战斗棋盘不再整板可见——只有以角色为中心的菱形灯光区看得清,区外没开过的格子沉入全黑,开过的格子留下灰阶的「记忆」。像在洞穴里举灯探索。机制规则(可见性/生命周期/纯呈现纪律)的现行事实源 =《HUD框架》「视野迷雾:菱形灯光区与黑暗边界」节(全项目唯一事实源,引用只指路),该节点名「黑暗区域渲染规格(暗淡态明度、迷雾过渡)归 art-bible」,本节即其承载——只管"怎么画",不复述机制。纯呈现层:格态、数字真值、清图判定零影响;零素材新增(全部程序化遮罩/调制);类型微光语义零改动。 -- **菱形灯光区(遮罩几何)**:灯光区 = 以角色所在格为中心、**曼哈顿距离 ≤ LIGHT_RADIUS** 的格集,区内照常渲染。LIGHT_RADIUS 走表值/常量参数化,**基准 8**——现行事实源「灯光区起步半径=微光环 8 格」的落码口径(半径数值沿微光「周围 8 格」的 8 起步,实测后可调)。**两圈分立注**:类型微光作用圈 = 英雄贴身 8 邻格(与可挖域同圈,现行事实源《HUD框架》「未开格类型微光」节),**不随灯光区扩**(权威文档配套拍板④)——灯光区只裁可见性,微光锚仍是可挖域,两圈几何分立、半径参数互不读写。提灯插值位:FOG_LANTERN_BONUS = **+2**(flat 整数档,权威文档配套拍板;落码常量名 `board_view.gd FOG_LANTERN_BONUS`——本条旧记 LIGHT_RADIUS_LANTERN_BONUS 系文档侧拟名,随迭代 19 卡④入库对齐实名),**已接效果入库**(2026-10-08 迭代 19 卡④:载体=秘宝·同调位 fog_lantern「魔法提灯」行,组合根每战装配链显式重算接线;呈现画法零改动——本节只管"怎么画",机制现行事实源仍归《HUD框架》)。边界随角色格位逐格重算,无半径插值动画。←现行事实源;§2 光照氛围允许域 -- **区外未开格:全黑不渲染(不透明档,照抄现行事实源)**:现行事实源原文「区外**未开格=全黑不渲染**」——不透明纯黑(L=0),**非高遮蔽半透档**。理由注记(为何不用高遮蔽档):①三档信息阶梯干净——未开格不可见=零信息、记忆格=弱信息、活格=全信息,叠读不打架;②高遮蔽档下砖面纹理隐约残留,与「类型微光=未开格唯一信息通道」(§4.2/§4.6.3)冲突——纹理残影泄格面观感;③现行事实源验收线「区外未开格不可见」须二值态才能逐格断言。←现行事实源;§4.2 -- **区外已开格:暗淡记忆态(灰显可读档,2026-10-06 UI 设计对齐批 P10 乙换装)**:画法 = **地板纹理原样 + 灰罩压顶**(demo 口径换装——原「程序化素色记忆板平涂」退役;纹理直画后 `FOG_MEMORY_PLATE #3A3A3A` α0.66 灰罩盖顶,素材零改动)+ 格内元素灰阶随罩隐现:**记忆墨 #C4C4C4(中性灰 L≈0.55)**;数字对合成底实测 **归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1**(WCAG 硬指标;合成底 L1 #4C4038/L2 #3F4147/L3 #3E4048;行值与素材随动,测点=ui_contrast_probe 迷雾段)。**归一口径注**:装载亮度归一是现行生产路径(`board_view._normalize_floor_luma`),原图均值≠渲染面,复算须落归一后底。双峰板色牌格在记忆态**退牌归板**——牌面并入记忆态、数字以记忆墨直画。可辨性自查判据(三通道,落码后随样张/实机自检):①对全黑未开格——合成底 #3E3731(L≈0.036)对 L0,实机目检常光档可分(暗房档一并抽检);②对灯光区内活态已开格——主判据=数字墨灰度差(记忆墨 L0.55 对亮墨三档 L≥0.75 / 色牌 #F5EAD3 L≈0.83,灰度 ΔL≥0.20)+ 色相通道(记忆态近中性灰,对活态暖底暖墨);格底明度差为辅助通道;③格态三形在记忆态保持灰度可辨——形通道经纹理+灰罩形态不变,沿 §1.4 验收 2 同源判据。**旗不在记忆态面**:旗只插在未开格,区外未开格全黑——旗印记随格隐没、灯光区移回复显(纯呈现,旗数据零影响,现行事实源口径的显式推论)。←HUD框架 可访问性基线;§4.1;§1.4;§3.2 -- **迷雾过渡边:硬边(逐格二值)**:灯光区边界 = **整格二值切换**(区内全渲染 / 区外全黑或记忆态),**不做渐变过渡带、不做半透明晕边**。理由:①边界是信息边界不是氛围晕——渐变中间态 =「看得见但看不清」,破数字可读硬指标与记忆态明度档;②现行事实源验收断言(边界格呈态、两态逐位一致)须格级二值可判;③与砖面整格表达(§3.2 高墙瓦片体系)、ITB 格内信息分层气质同轴。无动画,减动效无涉。←现行事实源;§1.2 原则一;§3.2 -- **与结算期去饱和的叠加共存**:结算期去饱和偏移(settlement_fx_view,同族先例)与迷雾调制叠加口径 = **记忆态为终态档**——区外已开格在结算期保持记忆态档、**不再叠加**结算去饱和(灰显已含去饱和语义,二次调制无信息增量且明度不可控);灯光区内格照常吃结算去饱和;全黑格两态无差。叠加态随实施批共存校验 + 探针断言(provisional,实机目检可代拍)。←需求卡片 T3/T4 -- **§2 明度禁令自查**:迷雾遮蔽与记忆态灰显属 §2 明度分层**光照氛围允许域**(洞穴举灯的空间光照呈现,与 Moonlighter 取列「亮地板/暗深渊/发光体」光照层次同族),非情绪压暗(§9.2 Darkest 反面条款不触——那禁的是情绪表达)、非状态明暗编码(格态语义仍由形+色相承载,灰度下三态可辨不受扰)。←§2;§9.1;§9.2 +- **菱形灯光区(遮罩几何)**:灯光区 = 以角色所在格为中心、**曼哈顿距离 ≤ LIGHT_RADIUS** 的格集,区内照常渲染。LIGHT_RADIUS 走表值/常量参数化,**基准 8**——现行事实源「灯光区起步半径=微光环 8 格」的落码取值(半径数值沿微光「周围 8 格」的 8 起步,实测后可调)。**两圈分立注**:类型微光作用圈 = 英雄贴身 8 邻格(与可挖域同圈,现行事实源《HUD框架》「未开格类型微光」节),**不随灯光区扩**(权威文档配套裁决)——灯光区只裁可见性,微光锚仍是可挖域,两圈几何分立、半径参数互不读写。提灯插值位:FOG_LANTERN_BONUS = **+2**(flat 整数档,权威文档配套裁决;落码常量名 `board_view.gd FOG_LANTERN_BONUS`),**已接效果入库**(载体=秘宝·同调位 fog_lantern「魔法提灯」行,组合根每战装配链显式重算接线;呈现画法零改动——本节只管"怎么画",机制现行事实源仍归《HUD框架》)。边界随角色格位逐格重算,无半径插值动画。←现行事实源;§2 光照氛围允许域 +- **区外未开格:全黑不渲染(不透明档,照抄现行事实源)**:现行事实源原文「区外**未开格=全黑不渲染**」——不透明纯黑(L=0),**非高遮蔽半透档**。理由注记(为何不用高遮蔽档):①三档信息阶梯干净——未开格不可见=零信息、记忆格=弱信息、活格=全信息,叠读不打架;②高遮蔽档下砖面纹理隐约残留,与「类型微光=未开格唯一信息通道」(§4.2/§4.6.3)冲突——纹理残影泄格面观感;③现行事实源验收线「区外未开格不可见」须二值态才能逐格断言。←现行事实源;§4.2 +- **区外已开格:暗淡记忆态(灰显可读档)**:画法 = **地板纹理原样 + 灰罩压顶**——纹理直画后 `FOG_MEMORY_PLATE #3A3A3A` α0.66 灰罩盖顶,素材零改动(原「程序化素色记忆板平涂」已弃用);格内元素灰阶随罩隐现:**记忆墨 #C4C4C4(中性灰 L≈0.55)**。双峰板色牌格在记忆态**退牌归板**——牌面并入记忆态、数字以记忆墨直画。**旗不在记忆态面**:旗只插在未开格,区外未开格全黑——旗印记随格隐没、灯光区移回复显(纯呈现,旗数据零影响,现行事实源的显式推论)。←HUD框架 可访问性基线;§4.1;§3.2 +- **记忆态可读性判据**:数字对合成底**归一后(真实渲染面)三关 ≥4.5:1**(WCAG 硬指标;合成底 L1 #4C4038/L2 #3F4147/L3 #3E4048;行值与素材随动,测点=ui_contrast_probe 迷雾段)。**归一注**:装载亮度归一是现行生产路径(`board_view._normalize_floor_luma`),原图均值≠渲染面,复算须落归一后底。可辨性自查(三通道,随样张/实机自检):①对全黑未开格——合成底 #3E3731(L≈0.036)对 L0,实机目检常光档可分(暗房档一并抽检);②对灯光区内活态已开格——主判据=数字墨灰度差(记忆墨 L0.55 对亮墨三档 L≥0.75 / 色牌 #F5EAD3 L≈0.83,灰度 ΔL≥0.20)+ 色相通道(记忆态近中性灰,对活态暖底暖墨);③格态三形在记忆态保持灰度可辨——形通道经纹理+灰罩形态不变,沿 §1.4 验收 2 同源判据。验收:机检〈ui_contrast_probe 迷雾段记忆墨对比 ≥4.5:1〉←§1.4 +- **迷雾过渡边:硬边(逐格二值)**:灯光区边界 = **整格二值切换**(区内全渲染 / 区外全黑或记忆态),**不做渐变过渡带、不做半透明晕边**。理由:①边界是信息边界不是氛围晕——渐变中间态 =「看得见但看不清」,破数字可读硬指标与记忆态明度档;②现行事实源验收断言(边界格呈态、两态逐位一致)须格级二值可判;③与砖面整格表达(§3.2 高墙瓦片体系)、ITB 格内信息分层气质同轴。无动画,减动效无涉。←现行事实源;§1.2 原则一;§3.2 +- **与结算期去饱和的叠加共存**:结算期去饱和偏移(settlement_fx_view,同族先例)与迷雾调制叠加规则 = **记忆态为终态档**——区外已开格在结算期保持记忆态档、**不再叠加**结算去饱和(灰显已含去饱和语义,二次调制无信息增量且明度不可控);灯光区内格照常吃结算去饱和;全黑格两态无差。叠加态随实施批共存校验 + 探针断言(provisional,实机目检可代拍)。验收:机检〈叠加态下记忆态灰显不被二次调制〉 +- **§2 明度禁令自查**:迷雾遮蔽与记忆态灰显属 §2 明度分层**光照氛围允许域**(洞穴举灯的空间光照呈现,与 Moonlighter 取列「亮地板/暗深渊/发光体」光照层次同族),非情绪压暗(§9.2 Darkest 反面条款不触——那禁的是情绪表达)、非状态明暗编码(格态语义仍由形+色相承载,灰度下三态可辨不受扰)。←§2;§9.1;§9.2 --- ## 7. UI 设计(UI Design) -> **白话**:这一章管 UI 长什么样式,UI 的具体选择都在本章定义。布局落在哪里、占多少像素,屏级权威在 battle.md、系统契约在 HUD框架;本章节内容——形状语法与色板细则(自 §3.3/§4.4 迁入)、布局栅格、字体字号、营地界面画法、引擎原生控件与按钮状态规格。 +> **白话**:这一章管 UI 长什么样式,UI 的具体选择都在本章定义。布局落在哪里、占多少像素,屏级权威在 battle.md、系统契约在 HUD框架;本章节内容——形状语法与色板细则(自 §3.3/§4.4 迁入)、布局栅格、字体字号、营地界面画法、引擎原生控件与按钮状态规格。 -### 7.1 形状语法细则(自 §3.3 迁入) +### 7.1 形状语法细则(自 §3.3 迁入) -**半拟物:容器穿戏服,数据素面。**面板=石板圆角矩形(与地砖同圆角率,全 UI 恒定),按钮=凸圆角的"可按的砖"(按压态=下沉一档,运动手段 §2),仪式性确认(胜利等)=圆形蜡封(呼应 §2.4);数字框、准线、进度条一律直角细线、无纹理;**数字色牌 = 直角小素牌**(2026-09-29 v3.0 后备件)——宽随数字位、无纹理无装饰,仅复杂格底兜底时垫于数字正下方一层(色值 §4.1),属"须精确的信息"侧、走直角律。判据:圆角=可触摸的物件,直角=须精确的信息。与参考分界:取 ITB 的信息分层,弃其冷硬斜切 HUD;取 Moonlighter 的木石亲和,不做全拟物。 +**半拟物:容器穿戏服,数据素面。**面板=石板圆角矩形(与地砖同圆角率,全 UI 恒定),按钮=凸圆角的"可按的砖"(按压态=下沉一档,运动手段 §2),仪式性确认(胜利等)=圆形蜡封(呼应 §2.4);数字框、准线、进度条一律直角细线、无纹理;**数字色牌 = 直角小素牌**(后备件)——宽随数字位、无纹理无装饰,仅复杂格底兜底时垫于数字正下方一层(色值 §4.1),属"须精确的信息"侧、走直角律。判据:圆角=可触摸的物件,直角=须精确的信息。与参考分界:取 ITB 的信息分层,弃其冷硬斜切 HUD;取 Moonlighter 的木石亲和,不做全拟物。验收:目检〈面板圆角/按钮凸圆角/数据直角三态可辨〉 -- **极性强调冗余**(§4.5⑦):UI 红/金强调对必须补图标+文案冗余;破坏性确认与常规确认禁止同位(位置通道)。 -- **圆角率单点注记(2026-10-06 UI 设计对齐批 P5 甲)**:容器/按钮圆角值收敛为全站唯一 `RADIUS_OBJECT = 5`(ui_kit 单点;与主/危按钮原 5px 同值,面板与按钮同圆角率即满足本节「全 UI 恒定圆角率」);数据框/数字牌/AP 点阵/准线/血条维持 0 直角;**例外三件保留**=标题屏底座椭圆 17/结算金印 200(圆形蜡封)/状态脉冲环 100(圆形)。收敛前实测 0/2/3/4/5/6/8/17/100/200 十档并存。 -- **九宫格切片注记(2026-10-06 UI 设计对齐批 P4 乙+Q1 丙)**:panel 石皮 `PANEL_SLICE = 16`(原 8→16,与原型 .plate9 slice16 对齐;button 维持 8、仪式框维持 12——素材 panel_stone/button_stone 均 48×48 不动);`panel_skinned` 工厂内 **content margin clamp**=低于切片的分量自动抬到切片(石框纹理不吃内容;工厂单点,调用点零改)。本节原无切片条款,此为新增注记。 -- **资产渠道**:♻ 九宫格面板/按钮改色过复用闸优先(清单 §0),⚙ 兜底;样式终稿随 §1.4 样张闸后定稿。 -- **占位完成条款(2026-09-29 v3.0 新增,病根 8)**:程序化占位 = **临时态**,任何屏的终稿闸 = §1.4 整屏样张过闸;占位态禁止跨里程碑。 -- **实现侧边界**:样式收敛 ui_kit;ui_kit 的工程契约归《HUD框架》,本节不重述。 +- **极性强调冗余**(§4.5⑦):UI 红/金强调对必须补图标+文案冗余;破坏性确认与常规确认禁止同位(位置通道)。验收:目检〈红/金强调位有图标+文案,破坏性确认独立位〉 +- **圆角率单点注记**:容器/按钮圆角值收敛为全站唯一 `RADIUS_OBJECT = 5`(ui_kit 单点;面板与按钮同圆角率即满足本节「全 UI 恒定圆角率」);数据框/数字牌/AP 点阵/准线/血条维持 0 直角;**例外三件保留**=标题屏底座椭圆 17/结算金印 200(圆形蜡封)/状态脉冲环 100(圆形)。验收:机检〈ui_kit 圆角取值仅 RADIUS_OBJECT 与例外三件〉 +- **九宫格切片注记**:panel 石皮 `PANEL_SLICE = 16`(button 维持 8、仪式框维持 12——素材 panel_stone/button_stone 均 48×48 不动);`panel_skinned` 工厂内 **content margin clamp**=低于切片的分量自动抬到切片(石框纹理不吃内容;工厂单点,调用点零改)。验收:机检〈九宫格面板切片 16、边角无错位〉 +- **资产渠道**:♻ 九宫格面板/按钮改色过复用闸优先(清单 §0),⚙ 兜底;样式终稿随 §1.4 样张闸后定稿。验收:方向性 +- **占位完成条款**:程序化占位 = **临时态**,任何屏的终稿闸 = §1.4 整屏样张过闸;占位态禁止跨里程碑。验收:目检〈过闸屏无程序化占位残留〉 +- **实现侧边界**:样式收敛 ui_kit;ui_kit 的工程契约归《HUD框架》,本节不重述。 -### 7.2 UI 色板细则(自 §4.4 迁入) +### 7.2 UI 色板细则(自 §4.4 迁入) -**全局文字可读性硬指标(2026-09-29 v3.0 新增,病根 7)**:**一切文字**(含菜单/说明/tooltip/按钮/底栏,无区域例外)对其直接背景 **≥4.5:1**(WCAG AA);大字(≥18px 或加粗档)底线 3:1、目标 4.5:1;**文字禁直接压纹理底**——垫容器或罩层(现行菜单说明 2.58:1、C 区属性行 1.31:1 式违例自此有条款可依)。核验闸 = §1.4 整屏样张。 +**全局文字可读性硬指标**:**一切文字**(含菜单/说明/tooltip/按钮/底栏,无区域例外)对其直接背景 **≥4.5:1**(WCAG AA);大字(≥18px 或加粗档)底线 3:1、目标 4.5:1;**文字禁直接压纹理底**——垫容器或罩层。核验闸 = §1.4 整屏样张。验收:机检〈ui_contrast_probe 全文字位 ≥4.5:1〉 -**数据素面的墨来自世界的墨**:UI 数字、进度、准线一律主墨 `#33261B` 系——棋盘与面板读同一种墨;容器穿木石材质色,数据只穿墨。**营地与棋盘**:菜单容器背景用全作最暖的烛光黄 `#F6C667` 与木色 `#8A5A34`(§2.8;场景构成 §6.2);战斗容器转中性石色,防暖色渗入棋盘分走数字对比预算——氛围色向可读色让路。UI 不新增语义色,一切强调沿用 §4.2。**暗底呈现态双色墨制**(关卡画面样张 v1 冻结 2026-09-28):暗底系(已过/未来/候选/Boss 方块、暗数据框)图标与读数一律暖白 `#F0E2C8`(WARM)、亮底(当前金 `#F0A828`)转深墨 `#181410`(INK_ROUTE)——各态对比 ≥4.5:1;已过节点暗金冻结 `GOLD_DONE #68562C`(专用呈现态值,旧 GOLD_DIM `#8C743C` 对双色墨均不达标退役出该呈现位);Boss 描边 = 紫赤 2px + 外缘 1px 深墨环(灰度冗余通道——§4.2 色相独占本色不变,§4.5 灰度可分类承载)。 +**数据素面的墨来自世界的墨**:UI 数字、进度、准线一律主墨 `#33261B` 系——棋盘与面板读同一种墨;容器穿木石材质色,数据只穿墨。**营地与棋盘**:菜单容器背景用全作最暖的烛光黄 `#F6C667` 与木色 `#8A5A34`(§2.8;场景构成 §6.2);战斗容器转中性石色,防暖色渗入棋盘分走数字对比预算——氛围色向可读色让路。UI 不新增语义色,一切强调沿用 §4.2。**暗底呈现态双色墨制**(样张冻结):暗底系(已过/未来/候选/Boss 方块、暗数据框)图标与读数一律暖白 `#F0E2C8`(WARM)、亮底(当前金 `#F0A828`)转深墨 `#181410`(INK_ROUTE)——各态对比 ≥4.5:1;已过节点暗金冻结 `GOLD_DONE #68562C`(专用呈现态值;旧 GOLD_DIM 对双色墨不达标、已退出该呈现位);Boss 描边 = 紫赤 2px + 外缘 1px 深墨环(灰度冗余通道——§4.2 色相独占本色不变,§4.5 灰度可分类承载)。验收:机检〈暗底/亮底各态文字对比 ≥4.5:1〉 -- **琥珀双岗注记(2026-10-04 UI 落码批 M0)**:主操作色与收益金分轨——主按钮/候选节点/钥石徽章底用烛光黄 `#F6C667`(CANDLE,独立 token),奖励数字/钥石读数/地图「当前位置」仍独占 §4.1 奖励金 `#F0A828`。烛光黄族身兼「主操作 + 收益邻近氛围」双岗系主动取舍(原型评审 C 组留痕),非疏忽,防回改。底色三件套同步换代:BG_ROOT `#3A2E22`/BG_OVERLAY `#362B20`/新增 BG_BRIGHT `#453723`(配色总纲「提暖提亮禁近黑」);暖米族 WARM_SOFT `#E8D5A8`/WARM_FAINT `#CDB88C`(「米色去灰」)。 -- **色板五件对齐注记(2026-10-06 UI 设计对齐批 P7 乙——demo=唯一来源)**:数据板 `PANEL_DATA #E9D9B8`/板线 `PANEL_BORDER #786E58`/状态槽底 `SLOT_BACKDROP #3E362C`/血条深底 `HP_BAR_BACK #282220`/营地头带 `CAMP_HEADER_BAND #6E4526` 五 token 取值换代向原型 v2.1 对齐(Δ≤6/255 目视不可辨;代码与文档同批双侧写入文档)。色板唯一来源 = ui_kit 单点不变;原型文件只读不改(artwork 红线)。 +- **琥珀双岗注记**:主操作色与收益金分轨——主按钮/候选节点/钥石徽章底用烛光黄 `#F6C667`(CANDLE,独立 token),奖励数字/钥石读数/地图「当前位置」仍独占 §4.1 奖励金 `#F0A828`。烛光黄族身兼「主操作 + 收益邻近氛围」双岗系主动取舍,非疏忽,防回改。底色三件套:BG_ROOT `#3A2E22`/BG_OVERLAY `#362B20`/BG_BRIGHT `#453723`(配色总纲「提暖提亮禁近黑」);暖米族 WARM_SOFT `#E8D5A8`/WARM_FAINT `#CDB88C`(「米色去灰」)。验收:方向性 +- **色板五件对齐注记(来源 = 原型 demo)**:数据板 `PANEL_DATA #E9D9B8`/板线 `PANEL_BORDER #786E58`/状态槽底 `SLOT_BACKDROP #3E362C`/血条深底 `HP_BAR_BACK #282220`/营地头带 `CAMP_HEADER_BAND #6E4526` 五 token 取值向原型 v2.1 对齐(Δ≤6/255 目视不可辨)。色板唯一来源 = ui_kit 单点不变;原型文件只读不改(artwork 红线)。验收:机检〈ui_kit 五 token 与本表一致〉 ### 7.3 布局栅格 -- **结构**:双舞台三区(B 战斗画面/C 数据框/D 棋盘;字母保持不重编)为结构冻结项,分区尺寸与像素级裁决归 battle.md(HUD框架承载 1280×800 基座锚点值,如扫雷图区 1036×504、结算明细带展开 84px)。 -- **栅格口径(候选)**:面板间距/内边距按 4px 步进档(8/12/16/24 候选),全 UI 恒定;数据区直角细线不占圆角预算。终值归样张批。 +- **结构**:双舞台三区(B 战斗画面/C 数据框/D 棋盘;字母保持不重编)为结构冻结项,分区尺寸与像素级裁决归 battle.md(HUD框架承载 1280×800 基座锚点值,如扫雷图区 1036×504、结算明细带展开 84px)。验收:机检〈三区结构常驻〉 +- **栅格规则(候选)**:面板间距/内边距按 4px 步进档(8/12/16/24 候选),全 UI 恒定;数据区直角细线不占圆角预算。终值归样张。验收:目检〈间距落在步进档〉 ### 7.4 字体与字号 -- **字体**:全局一套像素字,单套即终稿——缝合像素字体 Fusion Pixel(OFL,已接入 `game/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf`),拉丁字符与数字沿用其自带拉丁字形,不另设拉丁专件(2026-10-24 用户裁:现状即终稿,旧拉丁候选 Kenney Fonts 退役不再接入;生产工单=清单 §3.6);墨色走主墨系(§7.2);换字体 = 全局决策。 -- **字号**:像素字体**整数倍缩放,禁非整数缩放**(像素模糊);下限(1280×800 基座;2026-09-29 v3.0 对齐 ux-designer PC 规范):棋盘数字 ≥18px(公式 = max(18, ceil(格边长 × 0.5))不变)、**正文/明细 ≥16px、tooltip ≥14px**(旧 14/13px 下限退役——1280×800 低于 1080p 基准,同字号实际更小)。 -- **字号档位表(2026-10-06 UI 设计对齐批 P3 甲——WB G1 六档)**:全站 UI 文字字号只经 `UiKit.FS_*` 单点取值,裸数字字号退役——`FS_CAPTION 12`/`FS_HINT 14`/`FS_BODY 16`/`FS_SUB 18`/`FS_TITLE 24`/`FS_H1 34`。旧 15 实测档按就近归档收编(10/11/12→CAPTION、13/14→HINT、15/16→BODY、17/18/20→SUB、21/22/24/26→TITLE、32/40→H1;T0 普查 107 静态位点全归档,数据驱动动态字号位点除外);棋盘数字公式 `maxi(18, ceil(格边长×0.5))` 与站桩血数 11px(条带内读数豁免)不入表。注:六档相邻比 12→14→16→18 三段 <1.25×,系 P3 裁决值优先于字重纪律的档差参考线,档差参考线对新层级设计仍有效。 -- **小字号豁免条款(2026-10-06 UI 设计对齐批 P11+Q5 甲,合并单条不开两口子)**:下款两类**豁免于本节下限**——①**条带内读数**:压在窄条带(血条/进度条等 ≤12px 高带)内居中的数字,受条带高度物理约束,维持 11px 下限,且必须按 §7.2 垫素色牌后计对比(实装=B 区站桩血数 11px 压 HP_BAR_BACK 牌 14.3:1);②**装饰性 overline/caption**:不承载须精确读取的信息、仅作分区标题或氛围小字者,可至 **12px**。**豁免逐处清单(2026-10-06 T6 判定落定,2026-10-22 随迭代 33 卡② 主菜单三入口化撤除「主菜单分组 overline×2」两处——组帽随旧钮组退役,余清单不动)**:条带内读数=B 区站桩血数一处;装饰 caption 档(FS_CAPTION)在册面=棋盘底操作提示条/营地柜台说明文与结构位预留注记/图例 RAP 词条说明/设置警示文两处/关卡地图底栏组标(生命/属性/特性)与分隔系/站桩消散提示/B 区状态槽微计数(×N●○)/bcard 脚注/RAP 反馈行/刷新余量徽标——清单外 11-13px 文字一律按正文/tooltip 下限提档,新增 caption 位须同批增补本清单。 -- **字重纪律(2026-09-29 v3.0 修订;2026-10-24 用户裁:字重两档不启用)**:层级手段为两件——**字号档差(相邻层级 ≥1.25×)+ 墨色深浅**;全局单字重,不上第二个字重(重开钩子:战斗一屏样张批设计轮若实证标题层级不足,字重议题随该批设计轮重开,不另立条目);数字禁描边/阴影伪加粗不变(字形为唯一数值载体,§4.1 同源纪律)。 +- **字体**:全局一套像素字,单套即终稿——缝合像素字体 Fusion Pixel(OFL,已接入 `game/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf`),拉丁字符与数字沿用其自带拉丁字形,不另设拉丁专件(现状即终稿;旧拉丁候选 Kenney Fonts 已弃;生产工单=清单 §3.6);墨色走主墨系(§7.2);换字体 = 全局决策。验收:机检〈主题字体指向 Fusion Pixel 件〉 +- **字号**:像素字体**整数倍缩放,禁非整数缩放**(像素模糊);下限(1280×800 基座):棋盘数字 ≥18px(公式 = `max(18, ceil(格边长 × 0.5))`不变)、**正文/明细 ≥16px、tooltip ≥14px**(1280×800 低于 1080p 基准,同字号实际更小)。验收:机检〈静态文字位点字号 ≥ 档位表值〉 +- **字号档位表(六档)**:全站 UI 文字字号只经 `UiKit.FS_*` 单点取值,裸数字字号弃用——`FS_CAPTION 12`/`FS_HINT 14`/`FS_BODY 16`/`FS_SUB 18`/`FS_TITLE 24`/`FS_H1 34`。旧实测档按就近归档收编(10/11/12→CAPTION、13/14→HINT、15/16→BODY、17/18/20→SUB、21/22/24/26→TITLE、32/40→H1;静态位点全归档,数据驱动动态字号位点除外);棋盘数字公式 `maxi(18, ceil(格边长×0.5))` 与站桩血数 11px(条带内读数豁免)不入表。注:六档相邻比 12→14→16→18 三段 <1.25×,系裁决值优先于字重纪律的档差参考线,该参考线对新层级设计仍有效。验收:机检〈全站无裸数字字号,位点走 FS_*〉 +- **小字号豁免条款(合并单条不开两口子)**:下款两类**豁免于本节下限**——①**条带内读数**:压在窄条带(血条/进度条等 ≤12px 高带)内居中的数字,受条带高度物理约束,维持 11px 下限,且必须按 §7.2 垫素色牌后计对比(实装=B 区站桩血数 11px 压 HP_BAR_BACK 牌 14.3:1);②**装饰性 overline/caption**:不承载须精确读取的信息、仅作分区标题或氛围小字者,可至 **12px**。**豁免逐处清单(撤除史归 git 提交史,余清单如下)**:条带内读数=B 区站桩血数一处;装饰 caption 档(FS_CAPTION)在册面=棋盘底操作提示条/营地柜台说明文与结构位预留注记/图例 RAP 词条说明/设置警示文两处/关卡地图底栏组标(生命/属性/特性)与分隔系/站桩消散提示/B 区状态槽微计数(×N●○)/bcard 脚注/RAP 反馈行/刷新余量徽标——清单外 11-13px 文字一律按正文/tooltip 下限提档,新增 caption 位须同批增补本清单。验收:机检〈豁免文字位均可对上本清单,清单外无 <14px〉 +- **字重纪律(字重两档不启用)**:层级手段为两件——**字号档差(相邻层级 ≥1.25×)+ 墨色深浅**;全局单字重,不上第二个字重(重开钩子:战斗一屏样张设计轮若实证标题层级不足,字重议题随该轮重开,不另立条目);数字禁描边/阴影伪加粗不变(字形为唯一数值载体,§4.1 同源纪律)。验收:机检〈主题仅注册单字重〉 ### 7.5 营地界面 -- 营地界面=**一张可点击的场景图 + 七物件点选**:点画面里的物件进入对应界面(冒险/锻造工坊/秘宝匣/生涯记录/天赋树/图鉴/离开营地)。七个物件=半开的**大门门扇**、挂在木围墙上的**布告板**、**锻炉·铁砧**、**木桩**、**篝火**、**宝箱**、**帐篷旁的笔记本**(**帐篷**为承载件、不可点;它的体量与落地尺寸待作者定)。画面纵深=远景天空与远山/中景木围墙与营地大门/近景篝火与左右物件;**近景物件允许压住中景围墙(围墙是纯背景板),唯一例外=中间门洞必须留空、不被任何物件遮挡**(围墙形态与分带起点的现行口径见 §6.2)。场景图的画法、分层与四态见 §6.2;色域走 §7.2 最暖一档;钥石余额常驻顶部一行小 HUD 栏。界面视觉终稿以表现层样张过审为前置(§1.4 整屏样张闸)。 +- 营地界面=**一张可点击的场景图 + 七物件点选**:点画面里的物件进入对应界面(冒险/锻造工坊/秘宝匣/生涯记录/天赋树/图鉴/离开营地)。场景构成、七个物件清单、画法、分层与交互四态的唯一出处 = §6.2,本节不复述;色域走 §7.2 最暖一档;钥石余额常驻顶部一行小 HUD 栏。界面视觉终稿以表现层样张过审为前置(§1.4 整屏样张闸)。验收:目检〈七物件可点、门洞留空〉 -### 7.6 引擎原生控件与按钮状态规格(2026-10-14 迭代 25/26 反哺写入——原无条款,两批实现=临时权威,本批按呈报转正) +### 7.6 引擎原生控件与按钮状态规格 -> **来源与边界**:迭代 25(UI 全局主题批)立引擎原生控件皮肤、迭代 26(UI 交互与观感批)立按钮五态矩阵与全局纹理过滤——此前 §7 无相应条款,两批实现系临时权威,用户 2026-10-14 裁「全部按呈报写入文档」转正为本节。**定值单点纪律(§8 头注)遵守**:本节只锁形态/尺寸档/对比度底线等规则,色值一律指路——主题色值事实源 = `game/assets/theme/README.md` 溯源表(与 ui_kit 常量双源,见本节末注),本节不复述色值。 +> **来源与边界**:本节由迭代实现批按呈报写入(引擎原生控件皮肤、按钮五态矩阵与全局纹理过滤),现为现行规格。**定值单点纪律(§8 头注)遵守**:本节只锁形态/尺寸档/对比度底线等规则,色值一律指路——主题色值事实源 = `game/assets/theme/README.md` 溯源表(与 ui_kit 常量双源,见本节末注),本节不复述色值。 -**(一)按钮五态矩阵(迭代 26 T1;冒烟 V1/V2 35 断言在闸)** +**(一)按钮五态矩阵(冒烟五态断言在闸)** -- **五态齐全**:全站按钮(普通/主/危险三厂)必须五态非空——常态 / 悬停 / 按下 / 禁用 / 键盘焦点;任一态缺档即违本条(旧态:悬停填常态图=无反馈、禁用错配按压图、焦点全站空白)。 -- **悬停 = 常态图 + modulate 提亮**(乙案):提亮档 `Color(1.06,1.06,1.06)`,零素材;主钮承 `CANDLE.lightened(0.06)` 同纪律。 -- **按下 = 本族下沉**:石纹钮 `button_stone_pressed` 九宫格;危险钮补本族 pressed 深档(深红 `darkened(0.18)`——旧承石纹按压图错配退役)。 -- **禁用 = 本族底 + α0.45 幽灵化**(甲案):flat 两厂 bg+border 同 α 全幽灵化,取代旧「disabled=按压图」错配;文字禁用色同档。 (注:本条管 **UI 按钮**;营地场景图里的**物件**禁用态另按 §6.2——降明度 + 去饱和,不走 α 幽灵化。) -- **键盘焦点 = INK 2px 内缩 1px 环**(甲案):负 expand margin 实现内缩;引擎 focus 样式盒叠加绘制、不遮钮底(「三类钮底可辨性实比」设计语言成立);逐实例 override、**主题层零新增词条**(冒烟双红线在闸)。 -- 新增按钮位须同批过五态清单;验收 = §1.4 样张 + 冒烟五态断言。 +- **五态齐全**:全站按钮(普通/主/危险三厂)必须五态非空——常态 / 悬停 / 按下 / 禁用 / 键盘焦点;任一态缺档即违本条。验收:机检〈冒烟五态断言全绿〉 +- **悬停 = 常态图 + modulate 提亮**:提亮档 `Color(1.06,1.06,1.06)`,零素材;主钮承 `CANDLE.lightened(0.06)` 同纪律。验收:机检〈悬停态样式非空且非常态图直贴〉 +- **按下 = 本族下沉**:石纹钮 `button_stone_pressed` 九宫格;危险钮补本族 pressed 深档(深红 `darkened(0.18)`)。验收:机检〈三厂 pressed 样式各归本族〉 +- **禁用 = 本族底 + α0.45 幽灵化**:flat 两厂 bg+border 同 α 全幽灵化;文字禁用色同档。(注:本条管 **UI 按钮**;营地场景图里的**物件**禁用态另按 §6.2——降明度 + 去饱和,不走 α 幽灵化。)验收:机检〈禁用态非按压图、α0.45〉 +- **键盘焦点 = INK 2px 内缩 1px 环**:负 expand margin 实现内缩;引擎 focus 样式盒叠加绘制、不遮钮底;逐实例 override、**主题层零新增词条**(冒烟双红线在闸)。验收:机检〈焦点样式在位、主题词条数不增〉 +- 新增按钮位须同批过五态清单;验收 = §1.4 样张 + 冒烟五态断言。验收:机检〈新增按钮位入冒烟五态清单〉 -**(二)引擎原生控件组件规格(迭代 25 T0;15 类型词条全覆盖,预盖件零实役)** +**(二)引擎原生控件组件规格(15 类型词条全覆盖,预盖件零实役)** -- **滚动条**:V/HScrollBar 的 scroll 槽(直角)+ grabber 三态;**引擎事实 = grabber 走 StyleBox 非 Icon**(4.7.2 源码实证),滚动条无位图、无箭头按钮(increment/decrement 默认主题未定义);粗细 = content margin 4(8px)、柄最小 8px。 -- **滑轨**:HSlider/VSlider 的 slider(槽轨)+ grabber_area(填充段)两段必须**一眼可辨**(§7.2 可读性硬条款的滑轨侧落法——判据 = 拖到中点两段可辨,机检对 ≥4.5:1 文字位不覆盖该面,归样张目检);抓子三态位图;H/V 边距为旋转换位。 -- **悬停气泡(Tooltip)**:TooltipPanel/TooltipLabel 深底 + 边 + 暖字,标题屏观感承旧值原样迁入;字号走 §7.4 既有档(tooltip 14)。 -- **输入件**:LineEdit(数据框惯例:牌色底 + 板线 + `RADIUS_OBJECT` 圆角 + 占位色)、SpinBox(updown 合并箭头位图 + 箭头底 hover/pressed 覆盖)、H/VSeparator(单线 1px,默认灰 #808080 实证在位须接管)。 -- **预盖件**(零实役,备案待启用时复查):CheckBox/CheckButton 共用一套 `checked`/`unchecked`(+_disabled) 四态图标(**引擎事实 = 4.7 无 on/off 词条,两类同名同套——否则预盖静默空转**)、OptionButton(button_stone 九宫格 + 箭头位图)、PopupMenu(panel + hover 高亮 + 字色字号)。 -- **字体防御**:主题 `default_font` 显式指向全局像素字(与 project.godot `theme/custom_font` 同文件),冒烟双断言(主题非空 + Label 实取非空);`default_font_size` 不设——全站字号走 §7.4 `UiKit.FS_*` 逐实例 override。 -- **组件新增位**:新引擎控件入 UI 须同批在本节登记规格(尺寸档/圆角/对比度底线),禁裸用引擎默认灰圆角现代风(迭代 25 全屏巡检即此判据)。 +- **滚动条**:V/HScrollBar 的 scroll 槽(直角)+ grabber 三态;**引擎事实 = grabber 走 StyleBox 非 Icon**(4.7.2 源码实证),滚动条无位图、无箭头按钮(increment/decrement 默认主题未定义);粗细 = content margin 4(8px)、柄最小 8px。验收:目检〈滚动条非引擎默认灰〉 +- **滑轨**:HSlider/VSlider 的 slider(槽轨)+ grabber_area(填充段)两段必须**一眼可辨**(§7.2 可读性硬条款的滑轨侧落法——判据 = 拖到中点两段可辨,机检对 ≥4.5:1 文字位不覆盖该面,归样张目检);抓子三态位图;H/V 边距为旋转换位。验收:目检〈拖到中点两段可辨〉 +- **悬停气泡(Tooltip)**:TooltipPanel/TooltipLabel 深底 + 边 + 暖字,标题屏观感承旧值原样迁入;字号走 §7.4 既有档(tooltip 14)。验收:目检 +- **输入件**:LineEdit(数据框惯例:牌色底 + 板线 + `RADIUS_OBJECT` 圆角 + 占位色)、SpinBox(updown 合并箭头位图 + 箭头底 hover/pressed 覆盖)、H/VSeparator(单线 1px,默认灰 #808080 实证在位须接管)。验收:目检 +- **预盖件**(零实役,备案待启用时复查):CheckBox/CheckButton 共用一套 `checked`/`unchecked`(+_disabled) 四态图标(**引擎事实 = 4.7 无 on/off 词条,两类同名同套——否则预盖静默空转**)、OptionButton(button_stone 九宫格 + 箭头位图)、PopupMenu(panel + hover 高亮 + 字色字号)。验收:方向性 +- **字体防御**:主题 `default_font` 显式指向全局像素字(与 project.godot `theme/custom_font` 同文件),冒烟双断言(主题非空 + Label 实取非空);`default_font_size` 不设——全站字号走 §7.4 `UiKit.FS_*` 逐实例 override。验收:机检〈冒烟字体双断言全绿〉 +- **组件新增位**:新引擎控件入 UI 须同批在本节登记规格(尺寸档/圆角/对比度底线),禁裸用引擎默认灰圆角现代风。验收:目检〈新控件无默认灰样式〉 -**(三)氛围层平滑件与抖动派并存(迭代 26 反哺④——Q5 已裁保平滑)** +**(三)氛围层平滑件与抖动派并存** -- vignette / gradient_base / beams 三件程序化平滑渐变件**保平滑连续 alpha**(2026-10-13 grilling Q5 裁),与卡顶类型色带 DitherBand 的 Bayer 有序抖动派**并存不一致**——两派统一裁决留**整屏样张批**(§1.4;战斗一屏样张 P0 未开工,待办清单在册),本节只登记并存事实与裁决去向,不提前统一。 +- vignette / gradient_base / beams 三件程序化平滑渐变件**保平滑连续 alpha**(已裁),与卡顶类型色带 DitherBand 的 Bayer 有序抖动派**并存不一致**——两派统一裁决留**整屏样张**(§1.4;战斗一屏样张未开工,待办清单在册),本节只登记并存事实与裁决去向,不提前统一。验收:方向性 -> **主题色值双源注记(迭代 25 反哺④)**:主题色值现存两处——`main_theme.tres` 字面量与 ui_kit 常量(卡片立项时已知代价,README 溯源表缓解)。长期收敛候选 = 程序化 Theme 生成回代码单点,属重构面、未排期;新增色值仍按现状双落并同批登 README 溯源表。 +> **主题色值双源注记**:主题色值现存两处——`main_theme.tres` 字面量与 ui_kit 常量(README 溯源表缓解)。长期收敛候选 = 程序化 Theme 生成回代码单点,属重构面、未排期;新增色值仍按现状双落并同批登 README 溯源表。 --- ## 8. 资产与技术规范(Asset & Technical Specs) -> **白话**:这一章是给所有出图者(人与脚本)的技术底线——多大的画布、什么格式、怎么命名。屏级格边长定值的裁决权在 battle.md;本节锁资产侧:分辨率档位、像素基准与缩放纪律、导出与命名、帧数规格。生产工单视图 = art-assets-checklist。 +> **白话**:这一章是给所有出图者(人与脚本)的技术底线——多大的画布、什么格式、怎么命名。屏级格边长定值的裁决权在 battle.md;本节锁资产侧:分辨率档位、像素基准与缩放纪律、导出与命名、帧数规格。生产工单视图 = art-assets-checklist。 -> **定值单点纪律(2026-10-06 审计 §07-1 落规)**:全文数字定值只落在**两张表**——**§4.1 主色板**(全部色值+明度/饱和边界)与 **§8.2**(画布/体量/立绘源档/圆角率/缩放);其余章节一律**指路、禁复述数字**(修订改值只动单点处);character-design §5.5 为档位**语义**表(普通/精英/Boss + 例外),数值权威 = §8.2;存量双写复述随整备批(待办清单挂账)逐步收编。 +> **定值单点纪律**:全文数字定值只落在**两张表**——**§4.1 主色板**(全部色值+明度/饱和边界)与 **§8.2**(画布/体量/立绘源档/圆角率/缩放);其余章节一律**指路、禁复述数字**(修订改值只动单点处);character-design §5.5 为档位**语义**表(普通/精英/Boss + 例外),数值权威 = §8.2。 -### 8.1 分辨率档位与格边长口径 +### 8.1 分辨率档位与格边长 -- **两档定案**:1280×800(基座)+ 1920×1080,窗口不可调;后续调整走修订。 -- **格边长口径**:按档位与板形计算——固定档位下格子量增加则格边长相应变小;公式、单位与现行算例的唯一出处 = [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」,本节不复述数值。资产侧锁两条比例:**瓦片整格铺满**(32×32 源随格边长缩放,允许非整数倍——瓦片不适用 §8.2 甲类整数倍纪律)、**棋盘小人显示占位 ≤ 0.75× 格边长**。(1920 档已裁决全局等比缩放——迭代 9 卡⑮落码) +- **两档定案**:1280×800(基座)+ 1920×1080,窗口不可调;后续调整走修订。验收:机检〈工程分辨率配置与两档一致〉 +- **格边长取值**:按档位与板形计算——固定档位下格子量增加则格边长相应变小;公式、单位与现行算例的唯一出处 = [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」,本节不复述数值。资产侧锁两条比例:**瓦片整格铺满**(32×32 源随格边长缩放,允许非整数倍——瓦片不适用 §8.2 甲类整数倍纪律)、**棋盘小人显示占位 ≤ 0.75× 格边长**。(1920 档已裁决全局等比缩放)验收:机检〈瓦片显示尺寸随格边长、小人占位 ≤0.75×〉 ### 8.2 像素基准与画布 -- **缩放纪律(硬规则,2026-09-30 v3.1 起按资产两类分级——2026-09-26 用户裁「更改规则,不做个案豁免」)**: - - **甲类·文本与语义件**(数字、图标、旗、格面纹样):维持严格整数倍缩放硬规则——可读性与语义精确性优先,**禁非整数缩放不变**。砖面 tile 不属本类,随格边长整格铺满(见 §8.1)。 - - **全局纹理过滤与平滑渐变件例外(2026-10-14 迭代 26 反哺②写入文档)**:`project.godot` `default_texture_filter=0`(Nearest)全局——像素风 UI 皮(九宫格/主题石纹样式盒/滑轨抓子/勾选/箭头图标)翻 Nearest=意图面;**显式 LINEAR 豁免清单(有意例外,与状态图标抗锯齿例外同族先例)= vignette / gradient_base / beams 三件程序化平滑渐变件**——保平滑连续 alpha 的程序化氛围件,Q5 已裁(§7.6 三);新增平滑件须同批入册本清单,禁静默挂 LINEAR。翻转面枚举与冒烟断言(6 条)在闸。 - - **显示档登记例外(2026-10-06 UI 设计对齐批 P6 甲——64 源 0.5×,待素材批消除)**:柜台四枚 `icon_forge/workshop/atlas/keystone`(64×64 源)显示档统一 **32px = 0.5×**;状态九枚(32×32 源)mini(站桩)档 **16px = 0.5×**。两处均系「显示尺寸侧唯一可调、素材一字节禁改」红线下的人工检整数半倍档,登记为**待消除例外**——素材批出 32px 源后回归 1×。常规档显示一律 1×(状态九枚 32px、手绘十枚 20px/40px 双档、BCard 图标槽 40px=2×)。 - - **乙类·角色与单位精灵**(有机形体、非文字):允许运行时非整数倍缩放(NEAREST 过滤不变),容忍轻度像素宽窄不均;判据 = 实机目检,可感不适即回整数档。乙类今后免逐案豁免(豁免蔓延断根)。 - - **站桩立绘槽位=高定宽随(2026-10-04 原型审计教训落规)**:立绘/精灵槽位只定「高度的整数倍档」,宽度随源图宽高比自适应——**禁止把全部立绘塞进统一方框强拉**(纵横倍率必须恒等)。实例=UI 原型 88×92 方框把 23×48 地精源横拉 3.83×/纵拉 1.92×、26×48 熊地精 3.38×/1.92× 压扁发花,46×48 主角近方恰等比故独活——方框是按主角比例定的,窄条生物(细长本体裁掉透明边)进主角的框必变形。乙类的非整数豁免只覆盖**等比**缩放,不含纵横异倍变形。 - - **1920×1080 档策略(2026-09-30 三裁决案甲落定——三案呈报原件已随中间文档退役,删除批见 commit 8e02dcf0)**:全局等比(stretch=canvas_items+aspect **keep**)——16:10 设计在 16:9 窗口=等比 1.35×+左右黑边各 ~96px,构图与 1280×800 基准档完全一致(逻辑布局 1920/1280 逐键一致实测);甲类 1.35 非整数缩放例外注记:轻度宽窄不均系本档位接受代价(像素游戏 shipped 常态),实机目检可感不适→回乙案(1920 独立 2× 基准)重议;乙类同前款。 -- **画布基准(2026-09-23 用户暂定值)**: - - **主角人设原图 = 48×72**(1× 基准,暂定;与站姿降采样可读性验证一致); - - **主角战斗状态图(含 B 区站桩侧向)= 48×72**(同上暂定;v1.9 待决注记「主角立绘档空缺」由此完成); - - **棋盘小人 = 32×32 二头身派生版(暂定,允许更小)**——独立资产,自人设定稿压缩派生(人物设计档 §5.4),不吞数字位、占位 ≤ 0.75× 格边长;板越小占比摩擦越大(13×13 / 14×14 更小,算例见 [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」),32px 源档的去留(v1.9 待决注记①)沿「独立小源档」路线在样张批定终值; - - **站桩立绘源档 = 八敌逐键定档(2026-10-06 定档批,用户确认定档总图;原三档 48/64/96 候选与 v3.0 档位上调方向候选一并完成)**:主角 48 不变;八敌源档高 = goblin 36 / giant_spider 38 / duergar 46 / hook_horror 58 / bugbear 62 / ogre_chief 74 / drider 90 / duergar_king 94,显示一律 **×2 整数档(NEAREST)** = 72–188。定档依据 = 生物体型三约束(用户拍板:钩爪恐魔>灰矮人、灰矮人<熊地精、食人魔>熊地精——体量=物种体型,非强度线性映射,人物设计档 §5.2 顺带修订);可读性实测门槛 = 源高 ≥32(24 糊/32 可读/48 清楚/64 细节完整),全阶梯 36–94 达标;派生管线 = pixel-1.png 紧裁 → 等比缩至档高 → dom_block_resize 块内众数降采样(tools/pixelize_candidates.py `STANDEE_TIER_H`,`SIZES` 降格为管线上游中间分辨率表);存在感问题(v3.0 病根 6)经方案二整屏对比(B 区 270/立绘显示上限 188)解决。 - - **站桩立绘排版 = 方案二(2026-10-06 用户拍板)**:① 区高 B 区 270(≈1/3)/C 区 530(≈2/3),**扫雷盘 438×438 原尺寸不缩、格子不变**(区高事实源 battle.md 区表;压缩由 C 区面板空档吸收);② 站桩内部重排——立绘双脚落石台上缘(弃盒内居中)、**血条+状态/名字行落底排**(立绘→血条→状态 顺序,全场血条对齐一线;血条嵌石板位、台缘线留作地面线、落影保留接地感),状态空时该行退化为名字行;③ 大体档(显示 >150px 级:drider/duergar_king)**允许向上溢出盒顶 N px**(2026-10-07 用户裁甲案;N 由实装批 C0 按实测尺寸算定,**与背景/光柱无关**——现行溢出区恰为临时光柱氛围层〔§6.1 方向稿四柱,x 基准 110/900/1055/1210@1280,`battle_stage_view.gd:55-63`〕,正式舞台背景插画 `stage/stage_bg.png` 到位后该层退役/吸收进背景绘制,**溢出判定不随之变更**;横向不得压到顶部中央轮次·阶段指示,溢出上限由 C0 用真图复算 x 区间定),血条仍落底(落底排的第二个收益=头部探顶不带出血条);意图徽标悬浮头顶上方(探顶档压胸);④ 原「血条悬浮头顶+立绘盒内居中」排式退役——盒顶固定 y 与盒内居中在高盒下拉开血条-立绘空洞(2026-10-06 用户点名否决)。 +- **缩放纪律(硬规则,按资产两类分级;不做个案豁免)**: + - **甲类·文本与语义件**(数字、图标、旗、格面纹样):维持严格整数倍缩放硬规则——可读性与语义精确性优先,**禁非整数缩放不变**。砖面 tile 不属本类,随格边长整格铺满(见 §8.1)。验收:机检〈甲类件显示尺寸为源整数倍〉 + - **全局纹理过滤与平滑渐变件例外**:`project.godot` `default_texture_filter=0`(Nearest)全局——像素风 UI 皮(九宫格/主题石纹样式盒/滑轨抓子/勾选/箭头图标)翻 Nearest=意图面;**显式 LINEAR 豁免清单(有意例外)= vignette / gradient_base / beams 三件程序化平滑渐变件**——保平滑连续 alpha 的程序化氛围件(§7.6 三已裁);新增平滑件须同批入册本清单,禁静默挂 LINEAR。翻转面枚举与冒烟断言(6 条)在闸。验收:机检〈过滤面枚举与 LINEAR 清单逐项一致〉 + - **显示档登记例外(64 源 0.5×,待素材批消除)**:柜台四枚 `icon_forge/workshop/atlas/keystone`(64×64 源)显示档统一 **32px = 0.5×**;状态九枚(32×32 源)mini(站桩)档 **16px = 0.5×**。两处均系「显示尺寸侧唯一可调、素材一字节禁改」红线下的人工检整数半倍档,登记为**待消除例外**——素材设计出 32px 源后回归 1×。常规档显示一律 1×(状态九枚 32px、手绘十枚 20px/40px 双档、BCard 图标槽 40px=2×)。验收:机检〈显示档与登记表一致〉 + - **乙类·角色与单位精灵**(有机形体、非文字):允许运行时非整数倍缩放(NEAREST 过滤不变),容忍轻度像素宽窄不均;判据 = 实机目检,可感不适即回整数档。乙类免逐案豁免。验收:目检〈立绘无可感拉花〉 + - **站桩立绘槽位=高定宽随**:立绘/精灵槽位只定「高度的整数倍档」,宽度随源图宽高比自适应——**禁止把全部立绘塞进统一方框强拉**(纵横倍率必须恒等)。反面实例:统一方框把窄条生物源(细长本体裁掉透明边)按主角比例横拉压扁发花。乙类的非整数豁免只覆盖**等比**缩放,不含纵横异倍变形。验收:机检〈立绘纵横倍率恒等〉 + - **1920×1080 档策略**:全局等比(stretch=canvas_items+aspect **keep**)——16:10 设计在 16:9 窗口=等比 1.35×+左右黑边各 ~96px,构图与 1280×800 基准档完全一致;甲类 1.35 非整数缩放例外注记:轻度宽窄不均系本档位接受代价(像素游戏 shipped 常态),实机目检可感不适→回乙案(1920 独立 2× 基准)重议;乙类同前款。验收:目检〈1920 档构图与 1280 档一致〉 +- **画布基准(暂定值)**: + - **主角人设原图 = 48×72**(1× 基准,暂定;与站姿降采样可读性验证一致); + - **棋盘小人 = 32×32 二头身派生版(暂定,允许更小)**——独立资产,自人设定稿压缩派生(character-design §5.4),不吞数字位、占位 ≤ 0.75× 格边长;板越小占比摩擦越大(13×13 / 14×14 更小,算例见 [battle.md](../gdd/ux/battle.md)「棋盘格尺寸公式」),32px 源档沿「独立小源档」路线在样张定终值; + - **站桩立绘源档 = 八敌逐键定档**:主角 48 不变;八敌源档高 = goblin 36 / giant_spider 38 / duergar 46 / hook_horror 58 / bugbear 62 / ogre_chief 74 / drider 90 / duergar_king 94,显示一律 **×2 整数档(NEAREST)** = 72–188。定档依据 = 生物体型三约束(钩爪恐魔>灰矮人、灰矮人<熊地精、食人魔>熊地精——体量=物种体型,非强度线性映射,见 character-design §5.2);可读性实测门槛 = 源高 ≥32(24 糊/32 可读/48 清楚/64 细节完整),全阶梯 36–94 达标;派生管线 = pixel-1.png 紧裁 → 等比缩至档高 → dom_block_resize 块内众数降采样(tools/pixelize_candidates.py `STANDEE_TIER_H`,`SIZES` 降格为管线上游中间分辨率表);体量存在感经整屏对比校验(B 区 270/立绘显示上限 188)。验收:机检〈standee_asset_check 逐键档高校验〉 + - **站桩立绘排版**:① 区高 B 区 270(≈1/3)/C 区 530(≈2/3),**扫雷盘 438×438 原尺寸不缩、格子不变**(区高事实源 battle.md 区表;压缩由 C 区面板空档吸收);② 站桩内部重排——立绘双脚落石台上缘(弃盒内居中)、**血条+状态/名字行落底排**(立绘→血条→状态 顺序,全场血条对齐一线;血条嵌石板位、台缘线留作地面线、落影保留接地感),状态空时该行退化为名字行;③ 大体档(显示 >150px 级:drider/duergar_king)**允许向上溢出盒顶 N px**(N 由实装批按实测尺寸算定,**与背景/光柱无关**——现行溢出区恰为临时光柱氛围层〔§6.1 方向稿四柱,x 基准 110/900/1055/1210@1280,`battle_stage_view.gd:55-63`〕,正式舞台背景插画 `stage/stage_bg.png` 到位后该层由背景绘制吸收,**溢出判定不随之变更**;横向禁止压到顶部中央轮次·阶段指示,溢出上限由实装批用真图复算 x 区间定),血条仍落底(落底排的第二个收益=头部探顶不带出血条);意图徽标悬浮头顶上方(探顶档压胸);④ 「血条悬浮头顶+立绘盒内居中」排式已弃——盒顶固定 y 与盒内居中在高盒下拉开血条-立绘空洞。验收:目检〈血条一线对齐、大体档溢出不压轮次指示〉 ### 8.3 导出格式与命名规范 -- **归档纪律(2026-10-06 P0-1 用户口径写入文档)**:图片素材**不入 git 版本管理**——仓库只保留运行时引用资产(game/assets);生成链路原始产物(AI 原图/中间产物/候选版本)归档至 `archive/` 并择机上对象存储备份;主文档(圣经/GDD/清单)**禁引用 artwork/ 散文件路径**——定稿成图以 game/assets 为唯一事实源,过程稿引用一律用归档包路径或描述性出处。 -- **入包与对账检查器(2026-10-06 P2 固化挂点——本批只立挂点,实现随 PRD 工具批)**:①**色板检查器**——扫描新素材色板,报新增色/近色告警/超 §4.1 上限(48)拦截,挂素材流入包步(asset-path-map §2 第 3 步);②**路径核对器**——扫描 asset-path-map 声明的全部路径,悬空/退役未清即报,随大批与审计例行跑(2026-10-06 三处悬空即此类失守);③**standee_asset_check 硬闸升级规格**(审计 §07-2/3)——按 §8.2 逐键定档校验体量与档高(现「宽 1–75」虚设区间退役)、像素域色数 ≤32、无非整数倍缩放痕迹、描边亮度达标(亮描边明度档,§8.2 描边层级);把 v3.6「高定宽随」教训变成代码闸而非文字。 -- **交付格式**:PNG 透明底;动画 = 序列帧或引擎精灵表(SpriteFrames),帧尺寸恒定、横向等宽排列(候选)。 -- **命名规范(候选草案)**:`<域>_<对象键>_<状态|变体>[_<序号>].png`,全小写 snake_case;**对象键 = 数据表主键**(effect_id/reward_id/cell_type/enemy_id——图标键契约 §4.6 的文件侧延伸:资产文件名键 = 图标键,全局恒定禁运行时换图);图类型变体后缀 = board_type 键(`_dungeon`/`_underdark`/`_deep_underdark`);候选评审件与终稿分目录(`artwork/candidates/` 承载评审件)。 +- **归档纪律**:图片素材**不入 git 版本管理**——仓库只保留运行时引用资产(game/assets);生成链路原始产物(AI 原图/中间产物/候选版本)归档至 `archive/` 并择机上对象存储备份;主文档(圣经/GDD/清单)**禁引用 artwork/ 散文件路径**——定稿成图以 game/assets 为唯一事实源,过程稿引用一律用归档包路径或描述性出处。验收:机检〈主文档 grep 无 artwork/ 散文件路径〉 +- **入包与对账检查器(挂点已立,实现随 PRD 工具批)**:①**色板检查器**——扫描新素材色板,报新增色/近色告警/超 §4.1 上限(48)拦截,挂素材流入包步(asset-path-map §2 第 3 步);②**路径核对器**——扫描 asset-path-map 声明的全部路径,悬空/未清即报,随大批与审计例行跑;③**standee_asset_check 硬闸升级规格**——按 §8.2 逐键定档校验体量与档高、像素域色数 ≤32、无非整数倍缩放痕迹、描边亮度达标(亮描边明度档);把「高定宽随」教训变成代码闸而非文字。验收:机检〈三检查器在闸可跑〉 +- **交付格式**:PNG 透明底;动画 = 序列帧或引擎精灵表(SpriteFrames),帧尺寸恒定、横向等宽排列(候选)。验收:目检〈交付件透明底无杂边〉 +- **命名规范(候选草案)**:`<域>_<对象键>_<状态|变体>[_<序号>].png`,全小写 snake_case;**对象键 = 数据表主键**(effect_id/reward_id/cell_type/enemy_id——图标键契约 §4.6 的文件侧延伸:资产文件名键 = 图标键,全局恒定禁运行时换图);图类型变体后缀 = board_type 键(`_dungeon`/`_underdark`/`_deep_underdark`);候选评审件与终稿分目录(`artwork/candidates/` 承载评审件)。验收:机检〈命名与目录检查器零红〉 -### 8.4 帧数规格(样张 v2 冻结 2026-09-28) +### 8.4 帧数规格(样张冻结) -- **帧数规格(样张 v2 冻结,2026-09-28)**:待机循环 4 帧;攻击/技能准备 6 帧单次;受击 2 帧;死亡 = 灰化+碎裂退场(§4.6.4 语义硬规则),序列帧归 ✦ 缺料清单;帧时长与结算节拍对齐(预告时序见 §4.6.4)。 -- **方向集(2026-09-24 用户拍板)**:棋盘小人=**4 向**(下/上/侧,侧向左右镜像共用一份画稿);斜向移动/挖掘按**主导轴吸附最近正交向**呈现,不出 45° 斜向帧(斜向信息由位移轨迹与目标格预告承载,ITB 实态同款)。镜像与「旗面恒画右」纪律(character-design.md 冒险背包条)的相容归属:棋盘小人默认镜像豁免、旗右纪律限立绘侧——**代拟口径,样张批追认**。 +- **帧数规格(样张冻结)**:待机循环 4 帧;攻击/技能准备 6 帧单次;受击 2 帧;死亡 = 灰化+碎裂退场(§4.6.4 语义硬规则),序列帧归 ✦ 缺料清单;帧时长与结算节拍对齐(预告时序见 §4.6.4)。验收:机检〈序列帧数与档位一致〉 +- **方向集**:棋盘小人=**4 向**(下/上/侧,侧向左右镜像共用一份画稿);斜向移动/挖掘按**主导轴吸附最近正交向**呈现,不出 45° 斜向帧(斜向信息由位移轨迹与目标格预告承载,ITB 实态同款)。镜像与「旗面恒画右」纪律(character-design.md 冒险背包条)的相容归属:棋盘小人默认镜像豁免、旗右纪律限立绘侧——代拟规则,样张确认。验收:目检〈四向帧齐、侧向镜像无破绽〉 -### 8.5 结构件完整性与参考核验(2026-09-24 用户立规) +### 8.5 结构件完整性与参考核验 -> **白话**:格子读感里的立体结构——顶面、缘件、角件、周界轮廓——必须是素材里**画出来的结构件**;素材缺件就停下报缺,拿调色/渐变/滤镜顶替不是变通,是违规。 +> **白话**:格子读感里的立体结构——顶面、缘件、角件、周界轮廓——必须是素材里**画出来的结构件**;素材缺件就停下报缺,拿调色/渐变/滤镜顶替不是变通,是违规。 -- **结构件完整性**:格面立体感(高墙顶面/坡缘/角件/周界轮廓)只能来自**素材本体绘制的结构件拼装**(样例=Necesse 式缘件:专属底色+手绘细节+暗轮廓线的独立贴图);禁止以调色、渐变、明暗滤镜、跨格投影等生成手段伪造素材中不存在的结构。样稿暴露缺件时,正确动作=停下呈报缺口与「换素材/自产缺件」选项,由用户裁决后执行。 -- **参考核验**:宣称「参考 X 游戏」前,必须取得并实际查看 X 的截图证据(像素级放大核验其实现方式),证据存 `artwork/mockups/` 或 `design/art/references/`;仅凭文字资料、记忆或宏观印象作出「X 是这样做」的断言无效。(2026-10-07 路径核对器 B3 首跑发现更正——原 `production/mockups/` 系废路径,v3.9 批已更正asset-path-map 等四处,本条漏网) -- **图底分离**:已开地面的明度须低于未开格墙体,由素材选型承载(选更暗的地板砖),不靠渲染压暗实现。 -- **生产分工(2026-09-26 用户拍板)**:格面地形类素材=**材质归模型、结构归程序**——AI 生图模型只出无缝填充纹理(无边框/无亮边/均匀光照/方形);一切结构件(缘件/轮廓/立面/角件/收头)一律程序化绘制或手修,因为 autotile 任意拼接的边界像素约束超出 2D 生图模型能力(实证:2026-09-25 生图 5×5 瓦片表每枚砖自带啃咬边框与宽亮带,提示词无法根除)。**镜像规则**:东西对称结构件只许水平翻转(flip_h)复用、**禁旋转**——北受光下东西侧缘光照对称,镜像无损真;旋转会把北受光转到东边。 +- **结构件完整性**:格面立体感(高墙顶面/坡缘/角件/周界轮廓)只能来自**素材本体绘制的结构件拼装**(样例=Necesse 式缘件:专属底色+手绘细节+暗轮廓线的独立贴图);禁止以调色、渐变、明暗滤镜、跨格投影等生成手段伪造素材中不存在的结构。样稿暴露缺件时,正确动作=停下报告缺口与「换素材/自产缺件」选项,由用户裁决后执行。验收:目检〈立体感逐件来自素材本体,无滤镜伪造〉 +- **参考核验**:宣称「参考 X 游戏」前,必须取得并实际查看 X 的截图证据(像素级放大核验其实现方式),证据存 `artwork/mockups/` 或 `design/art/references/`;仅凭文字资料、记忆或宏观印象作出「X 是这样做」的断言无效。验收:目检〈参考结论有截图证据在库〉 +- **图底分离**:已开地面的明度须低于未开格墙体,由素材选型承载(选更暗的地板砖),不靠渲染压暗实现。验收:机检〈已开格底明度低于墙顶面〉 +- **生产分工**:格面地形类素材=**材质归模型、结构归程序**——AI 生图模型只出无缝填充纹理(无边框/无亮边/均匀光照/方形);一切结构件(缘件/轮廓/立面/角件/收头)一律程序化绘制或手修,因为 autotile 任意拼接的边界像素约束超出 2D 生图模型能力(实证:生图 5×5 瓦片表每枚砖自带啃咬边框与宽亮带,提示词无法根除)。**镜像规则**:东西对称结构件只许水平翻转(flip_h)复用、**禁旋转**——北受光下东西侧缘光照对称,镜像无损真;旋转会把北受光转到东边。验收:目检〈瓦片表无自带边框/亮带;对称件仅 flip_h〉 --- ## 9. 参考与反参考(References & Anti-References) -> **白话**:正面参考 = 向谁抄什么;反面参考 = 避什么。两者同等效力——方案违反反面条款时,禁止以「参考了某游戏」辩护。参考图库存于 `design/art/references/`(点名池三款×2 张,与头注点名池一一对应)。 +> **白话**:正面参考 = 向谁抄什么;反面参考 = 避什么。两者同等效力——方案违反反面条款时,禁止以「参考了某游戏」辩护。参考图库存于 `design/art/references/`(点名池三款×2 张,与头注点名池一一对应)。 -### 9.1 正面参考(点名池,用户审阅选定) +### 9.1 正面参考(点名池,用户审阅选定) | 参考 | 取 | 弃 | |---|---|---| -| **Into the Breach** | 格内信息分层、攻击预告上格范式、战棋地块质感骨架(§1.2 原则一;§3.2) | 冷暗军事色调、压暗信息板、斜切 HUD(原则二;§3.3) | -| **Moonlighter** | 暖质地砖与砖缝质感、亲和冒险者气质、木石暖色道具(原则二/原则三);**光照层次与明度纵深**(2026-09-29 v3.0 起取——亮地板/暗深渊/发光体三档,§2 明度分层) | 全拟物倾向(§3.3 半拟物边界);Darkest 式情绪压暗(§9.2) | -| **Peglin** | 明快全年龄洞穴色彩、胖体量亲和怪(§5.1 普通怪原型) | 色块依赖——先读轮廓后读颜色(§5.1) | +| **Into the Breach** | 格内信息分层、攻击预告上格范式、战棋地块质感骨架(§1.2 原则一;§3.2) | 冷暗军事色调、压暗信息板、斜切 HUD(原则二;§3.3) | +| **Moonlighter** | 暖质地砖与砖缝质感、亲和冒险者气质、木石暖色道具(原则二/原则三);**光照层次与明度纵深**(亮地板/暗深渊/发光体三档,§2 明度分层) | 全拟物倾向(§3.3 半拟物边界);Darkest 式情绪压暗(§9.2) | +| **Peglin** | 明快全年龄洞穴色彩、胖体量亲和怪(§5.1 普通怪原型) | 色块依赖——先读轮廓后读颜色(§5.1) | -- **反面条款已成文**(§9.2);反面参考**图证**待本作素材生成后另批增补。 +- **反面条款已成文**(§9.2);反面参考**图证**待本作素材生成后增补。验收:方向性 -### 9.2 反面参考(每条绑定违反条款) +### 9.2 反面参考(每条绑定违反条款) | 反面参考 | 违反条款 | 立法理由 | |---|---|---| -| **Darkest Dungeon** 式压暗 | §2 情绪明暗禁令(明度分层下仍禁) | 状态与情绪只走色相/饱和/运动/元素四手段,不以变暗表达情绪(失败重开,世界无光暗记忆) | -| **传统扫雷白牌/贴纸式数字承载**(2026-09-29 v3.0 新增) | §1.2 原则二(明度分档为骨) | 数字由格底明度分档承载、直画入世界;色牌仅复杂格底后备——满屏白牌=电子表格观感,非游戏画面 | -| **Enter the Gungeon** 式粒子与色相爆炸 | §3.4 焦点预算;§4.2 色相纪律 | 信息层禁止被特效与杂色吞没,特效入画先过焦点预算 | -| **Hades** 式特效密度 | §2.2 兑现爽感的上限 | 爽感靠逐行因果归因,不靠满屏特效;承载元素以 §2.2 清单为界 | -| **传统扫雷 1–8 八色分色** | §4.1 数字墨三档 | 八色必撞红/金/蓝/紫赤语义色段;数字只走墨族三档色温,字形为唯一数值载体 | +| **Darkest Dungeon** 式压暗 | §2 情绪明暗禁令(明度分层下仍禁) | 状态与情绪只走色相/饱和/运动/元素四手段,不以变暗表达情绪(失败重开,世界无光暗记忆) | +| **传统扫雷白牌/贴纸式数字承载** | §1.2 原则二(明度分档为骨) | 数字由格底明度分档承载、直画入世界;色牌仅复杂格底后备——满屏白牌=电子表格观感,非游戏画面 | +| **Enter the Gungeon** 式粒子与色相爆炸 | §3.4 焦点预算;§4.2 色相纪律 | 信息层禁止被特效与杂色吞没,特效入画先过焦点预算 | +| **Hades** 式特效密度 | §2.2 兑现爽感的上限 | 爽感靠逐行因果归因,不靠满屏特效;承载元素以 §2.2 清单为界 | +| **传统扫雷 1–8 八色分色** | §4.1 数字墨三档 | 八色必撞红/金/蓝/紫赤语义色段;数字只走墨族三档色温,字形为唯一数值载体 | --- diff --git a/design/art/art-prompts.md b/design/art/art-prompts.md index 1f2ea871..346b3c57 100644 --- a/design/art/art-prompts.md +++ b/design/art/art-prompts.md @@ -2,7 +2,7 @@ > **性质**:美术清单生图通道(✦ 用户自跑,现走线上 API)的执行侧文件——每资产一段**自包含完整提示词**:正向 = 通用底座+主体+动作已拼妥、负向 = 通用底座+增补已并妥,整段复制直接贴进工作流即可,**不需要读其他文件、不需要拼装**。人物规格转写自 [character-design.md](character-design.md)(人物设计 v1.0,自 art-bible §5 迁出,圣经 §5 为存根),风格/色彩条款转写自 [art-bible](art-bible.md)(§4.1 主色板 / §4.2 语义色规则);冲突时人物规格以 character-design 为准、色彩以 art-bible 为准。 > **边界**:本文件只管**角色级**资产的生图通道。图标/格面/样张走脚本管线(⚙ 代理出),规格在 art-bible §4.6 与 [art-assets-checklist](art-assets-checklist.md),不需要提示词。**场景级资产(营地场景图的背景层与八个物件层)另立 [camp-scene-prompts](camp-scene-prompts.md),本文件不覆盖**;两卡的角色/场景分工以「谁的资产谁出卡」为界,同一段提示词不两处维护。意图动画三档是动画资产,等一屏样张锁方案后另立规格卡,本文件不覆盖。 -> **前世**:v1.0(2026-09-20,主角三态+六敌种)于 2026-09-22 经裁决移除;本版按「美术圣经 §5 落地后重生成」拍板重写(正序:B 路线),继承 seed 纪律 / 色相禁入带 / `#F2F2F2` 生成底色。**建立**:2026-09-22 · 状态:提示词卡成文,出图未开始 +> **状态**:提示词卡成文,出图未开始;按「美术圣经 §5 现行」重生成(正序:B 路线),继承 seed 纪律 / 色相禁入带 / `#F2F2F2` 生成底色;沿革见 git 提交史。 --- @@ -252,14 +252,14 @@ Pixel art style. true pixel art game sprite drawn on a tiny 48x72 pixel canvas, 5. **头身**:2–3 头身之内,与站姿样张同世界观。 6. **同形象**:主角三态并排、L2 熊地精对熊地精、熊地精 A/B 对——兜帽/装备/剪影不串戏。 -## 4bis. 地形填充纹理(2026-09-26 定稿,材质归模型、结构归程序) +## 4bis. 地形填充纹理(材质归模型、结构归程序) > 范围注:本节为地形类资产首张卡(库原定位=角色级,地形类同库登记)。分工纪律=art-bible §8.5「生产分工」:**模型只出无缝填充纹理,结构件(缘件/轮廓/立面/角件)全部程序化**——autotile 拼接约束超出 2D 生图能力,勿让模型画瓦片结构(实证:5×5 瓦片表每枚必带边框亮框)。 -**对话式生图控制纪律(2026-09-25/26 实证)**: +**对话式生图控制纪律(实证)**: 1. 同对话续写=对上一张图的**编辑模式**——变体必须各开新对话;同一对话里"same as X but ..."会被当成对最近一张的增量编辑。 2. 英文 prompt 服从度明显高于中文(Gemini/Nano Banana 语料结构使然),提示词一律英文。 -3. **地形瓦片出图制式 = 拼表制主选(2026-10-06 用户拍板,操作单见 remote-gen-prompts §3)**——一张整图容纳全部瓦片变体,后处理裁切取用:风格统一、一张管住全族。画框风险的缓解 = ①出图贴程序占位样图作参考(模型有具体锚定就不乱画框)②裁切时每格内缩 2–3% 切掉边框③统一降采样(结构归程序)。**分张(一张一跑)降为回退路径**:仅当拼表连续画框失败时改用。〔实证教训保留:2026-09-25/26 裸拼表(无参考图)必诱画框——教训归因于缺参考锚定,非拼表制本身〕 +3. **地形瓦片出图制式 = 拼表制主选(操作单见 remote-gen-prompts §3)**——一张整图容纳全部瓦片变体,后处理裁切取用:风格统一、一张管住全族。画框风险的缓解 = ①出图贴程序占位样图作参考(模型有具体锚定就不乱画框)②裁切时每格内缩 2–3% 切掉边框③统一降采样(结构归程序)。**分张(一张一跑)降为回退路径**:仅当拼表连续画框失败时改用。〔实证教训保留:裸拼表(无参考图)必诱画框——教训归因于缺参考锚定,非拼表制本身〕 4. 差异特征写第一句并给否定约束(如碎石斑变体须写明 `no cracks, no dark lines`);尺寸不写死(模型自定,原图越大裁切余量越足,后处理统一降采样)。 **①墙顶基面(无缝岩石)**: @@ -282,7 +282,7 @@ Same as the rock surface texture but with a few tiny lighter stone speckles spar Seamless tileable pixel art texture of dry dirt ground, top-down, for a dungeon board game. Flat 2D texture only — not an object, no perspective. Pattern fills the entire square evenly and wraps perfectly at all four edges. No border, no frame, no edge highlight, no vignette, no cast shadow, uniform directionless lighting. Dark warm brown palette around #B39A78 — clearly darker than tan rock. A few thin cracks and tiny pebble speckles, sparse. Clean retro pixel art look, limited palette, no gradients, no text, square image. ``` -后处理(程序侧,生成器 `_tools/gen_terrain_walls.py`,归档 zip 内):周期对齐无缝裁剪→BOX 降采样 32×32→环绕混合→量化(12 色)→结构件合成;②③亦可由①程序加纹理级裂纹/浅斑替代(2026-09-26 实证更稳)。 +后处理(程序侧,生成器脚本归档 zip 内〔archive 备选案-terrain-walls〕):周期对齐无缝裁剪→BOX 降采样 32×32→环绕混合→量化(12 色)→结构件合成;②③亦可由①程序加纹理级裂纹/浅斑替代(实证更稳)。 ## 5. 明确不在本文件 diff --git a/design/art/art-status-checklist.md b/design/art/art-status-checklist.md index cb9ebd80..f2351613 100644 --- a/design/art/art-status-checklist.md +++ b/design/art/art-status-checklist.md @@ -2,7 +2,7 @@ > **性质**:纯看板——每条素材只记三件事:**素材名 × 是否完成 × 需求文档指针**。要什么、怎么做、验收标准一概不记:需求规格归 [art-assets-checklist.md](art-assets-checklist.md),风格/规范定义处归各 owner 文档(第三列)。 > **更新纪律**:顺带只改「状态」列;做完不删行,改 ✅。条目编号与 art-assets-checklist.md 的 P0–P3 一一对应。 -> **建档**:2026-09-24,状态列为当日快照。**2026-10-06 P1-3 对账批**:按实机在役状态与圣经 v3.7 冻结记录全量复核更新(审计发现看板反超条目已对齐;对账后 = ✅ 4 / ◐ 8 / ⬜ 3,共 15 条)。 +> **建档**:状态列为建档当日快照,全量对账后与实机在役状态核对更新;数字以表内实况为准。 **状态图例**:⬜ 未开始 | ◐ 占位在役(程序化占位,够试玩不达标) | ◑ 候选已出待审 | ✅ 已交付 @@ -10,7 +10,7 @@ | 素材 | 状态 | 需求文档 | |---|---|---| -| 格子样张 | ✅ 样张 v3 冻结(2026-09-30 机检 17/17 全绿,art-bible §1.4 v3.2 记录;v2/v3 两次冻结通过;产物 `artwork/mockups/sample-batch-v3-final-2026-09-30.png`) | [art-bible](art-bible.md) §1.4 | +| 格子样张 | ✅ 样张 v3 冻结(2026-09-30 机检 17/17 全绿,art-bible §1.4 v3.2 记录;v2/v3 两次冻结通过;产物原件已丢失、未入 git) | [art-bible](art-bible.md) §1.4 | | 战斗一屏样张 | ⬜ | [战斗表现](../gdd/systems/战斗循环/战斗表现.md)(一屏样张验收标准)+ [art-bible](art-bible.md) §5.1 | ## P1 角色级(样张通过后开工) @@ -18,7 +18,7 @@ | 素材 | 状态 | 需求文档 | |---|---|---| | 主角战斗小人(待机/攻击/受击三态) | ✅ B区站桩三态全接(2026-09-29 迭代 6 T3:normal/attack/hit 48px 档入包+挂事件切换+结算回 idle,短驻自回;棋盘 1.5×/脚锚25%/椭圆影在役不变;hit 态源=new-hit.pxo 已程序导出) | [character-design](character-design.md) + [art-prompts](art-prompts.md) §2 | -| 八敌种形象(L2 熊地精=bugbear 复用,不单列) | ✅ B区站桩八敌真图服役(2026-09-29 迭代 6 T1/T2:pixel-1 源→48px 档统一比例/底中锚/众数投票降采样,character_enemy_<键>.png 按常量入位,字形回退退役为缺位兜底;素材不入 git 本地在盘)。**2026-10-06 v3.7 定档批注记**:八敌源档已定(36–94,art-bible §8.2),旧统一 48 档在役过渡,新档重产落位归 PRD(tools/pixelize_candidates.py `STANDEE_TIER_H` 已改,产出与定档总图一致) | [art-bible](art-bible.md) §5 + [art-prompts](art-prompts.md) §3 | +| 八敌种形象(L2 熊地精=bugbear 复用,不单列) | ✅ B区站桩八敌真图服役(2026-09-29 迭代 6 T1/T2:pixel-1 源→48px 档统一比例/底中锚/众数投票降采样,character_enemy_<键>.png 按常量入位,字形回退退役为缺位兜底;素材不入 git 本地在盘)。八敌源档已定(36–94,art-bible §8.2),旧统一 48 档为过渡形态,新档重产落位归 PRD(tools/pixelize_candidates.py `STANDEE_TIER_H` 已改,产出与定档总图一致) | [art-bible](art-bible.md) §5 + [art-prompts](art-prompts.md) §3 | | 意图准备动画三档 | ◐ | [art-bible](art-bible.md) §4.6.4 | ## P2 图标与格面(可与样张并行) @@ -28,7 +28,7 @@ | 格面资源徽章 19 枚 | ◐ | [art-bible](art-bible.md) §4.6 | | 关卡节点图图标组 | ◐ | [art-assets-checklist](art-assets-checklist.md) §3.2(正式 UI 设计未成文) | | 雷/封印印记/数字砖面全套 | ◐ | [art-bible](art-bible.md) §1.4/§3.2(前置:格子样张) | -| 增益状态效果图标 9 枚 | ✅ 已接入(2026-09-24 状态记录 156) | [art-bible](art-bible.md) §4.6.1(候选原稿:状态图标批 2026-09-23 远程出图,归 candidates 档——路径描述性引用,归档纪律见 art-bible §8.3) | +| 增益状态效果图标 9 枚 | ✅ 已接入(2026-09-24) | [art-bible](art-bible.md) §4.6.1(候选原稿:状态图标批 2026-09-23 远程出图,归 candidates 档——路径描述性引用,归档纪律见 art-bible §8.3) | | 营地场景图 | ◐ | [art-bible](art-bible.md) §6.2 | | 字体(全局 UI 像素字,Fusion Pixel 单套单字重) | ✅ 已定稿在役(2026-09-27 接入;2026-10-24 用户裁现状即终稿——不另接拉丁专件、不上第二字重) | [art-bible](art-bible.md) §7.4 | @@ -36,10 +36,10 @@ | 素材 | 状态 | 需求文档 | |---|---|---| -| 受击反馈 / 死亡表现 / 顺位标记 | ◐ 程序化演出在役(受击闪白+打击火花、灰化+碎裂退场〔禁瞬消〕、顺位徽标 ①②③ 程序绘制——2026-09-29 迭代 6 T4/T5;帧级素材未产,§8.4 帧数规格候选) | [战斗表现](../gdd/systems/战斗循环/战斗表现.md) | +| 受击反馈 / 死亡表现 / 顺位标记 | ◐ 程序化演出在役(受击闪白+打击火花、灰化+碎裂退场〔禁瞬消〕、顺位徽标 ①②③ 程序绘制;帧级素材未产,§8.4 帧数规格候选) | [战斗表现](../gdd/systems/战斗循环/战斗表现.md) | | 连锁坍塌显形演出 | ⬜ | [art-assets-checklist](art-assets-checklist.md) §4 | -| 封印特效(插旗动作=封印,两件:施印特效+印记常显) | ⬜ | [art-bible](art-bible.md) §3.2 + [art-assets-checklist](art-assets-checklist.md) §4 | -| 弹道 / 退场等动画密度 | ◐ 程序化弹道在役(play_projectile 0.16s+0.19s 到达拍调度,2026-09-29 迭代 6;密度终稿归战斗表现批) | [art-assets-checklist](art-assets-checklist.md) §4(战斗表现输入清单) | +| 封印特效(动作=封印,两件:施印特效+印记常显) | ⬜ | [art-bible](art-bible.md) §3.2 + [art-assets-checklist](art-assets-checklist.md) §4 | +| 弹道 / 退场等动画密度 | ◐ 程序化弹道在役(play_projectile 0.16s+0.19s 到达拍调度;密度终稿归战斗表现) | [art-assets-checklist](art-assets-checklist.md) §4(战斗表现输入清单) | --- diff --git a/design/art/asset-path-map.md b/design/art/asset-path-map.md index e915bc9f..0ae326cc 100644 --- a/design/art/asset-path-map.md +++ b/design/art/asset-path-map.md @@ -1,16 +1,16 @@ # 素材路径分工约定(asset-path-map) -- 定立:2026-09-26(用户立约指令「把哪些路径、哪些位置是干什么的确定好」——状态记录 161) +- 定立:2026-09-26(用户立约指令「把哪些路径、哪些位置是干什么的确定好」) - 效力:全仓素材批的路径判据单点;新素材批开工前对照,违例=实施缺陷 ## 1. 目录分工总表 | 路径 | 角色 | 入 git | 游戏运行时引用 | |---|---|---|---| -| `artwork/candidates/**` | 候选原图与母版源档(PNG/.pxo/GIF/对比图/验收留底)——**素材的原产地,游戏禁直接引用**;随 `artwork/` 整目录不入 git(`design/art/candidates/` 已作废,路径列入熵增闸退役名单) | 否(`.gitignore` 三行规则;`*.md` 指引例外) | **禁**(res:// 路径禁指 candidates) | +| `artwork/candidates/**` | 候选原图与母版源档(PNG/.pxo/GIF/对比图/验收留底)——**素材的原产地,游戏禁直接引用**;随 `artwork/` 整目录不入 git(`design/art/candidates/` 已弃用,路径列入熵增闸退役名单) | 否(`.gitignore` 三行规则;`*.md` 指引例外) | **禁**(res:// 路径禁指 candidates) | | `game/assets/art/**` | **游戏运行时素材唯一落地处**(res://assets/art/...)——定稿素材从 `artwork/candidates/` 拷贝入此,改名按圣经 §8.3 | 否(untracked,勿 git add;worktree 间需脚本拷贝同步) | **唯一合法引用源** | -| `game/assets/ui/icons/**` | UI 图标(状态图标批特例——唯一已提交入 git 的素材目录) | **是**(2026-09-24 批特例) | 是 | -| `artwork/mockups/**` | 混合样张/版式对比图(设计评审产物,非游戏素材;样张 v3、关卡地图样张等实存于此)——2026-10-06 对账更正:原写的 `production/mockups/**` 系废路径 | 否 | 禁 | +| `game/assets/ui/icons/**` | UI 图标(状态图标批特例——唯一已提交入 git 的素材目录) | **是**(状态图标入库特例) | 是 | +| `artwork/mockups/**` | 混合样张/版式对比图(设计评审产物,非游戏素材;样张、关卡地图样张等实存于此) | 否 | 禁 | ## 2. 素材流(新素材批标准路径) @@ -19,7 +19,7 @@ 3. **入包**:拷贝 → `game/assets/art/<语义子目录>/`,改名 `§8.3` 域键式(如 `character_hero_portrait.png`) 4. **接线**:代码 `res://assets/art/...` 引用(路径常量单点归 `UiKit.TEX_*`);装载一律 `UiKit.load_tex`(exists 守卫——素材不入 git,缺位树回退),**禁 preload** 5. **提交**:只提交代码/文档;PNG 与 `.import`(全局忽略)均不入 git -6. **整理(2026-10-06 P2-1 固化段——脚本链第四段:生成→验证→整理→入包)**:定稿入包后当批清 `artwork/candidates/` 子目录——保留**定稿源 + 最相邻候选 2 版**(默认策略,可按资产调整),其余打包归 `archive/`;非素材残留(.url/.ogg/.ai/.swf 等)剔出;批量执行一律 **dry-run 清单先行**,经用户确认再动文件(2026-10-06 用户惯例) +6. **整理(脚本链第四段:生成→验证→整理→入包)**:定稿入包后当批清 `artwork/candidates/` 子目录——保留**定稿源 + 最相邻候选 2 版**(默认策略,可按资产调整),其余打包归 `archive/`;非素材残留(.url/.ogg/.ai/.swf 等)剔出;批量执行一律 **dry-run 清单先行**,经用户确认再动文件 ## 3. game/assets/art 语义子目录 @@ -27,11 +27,11 @@ |---|---|---| | `character/` | 角色棋盘帧 + B 区立绘 | character_hero_idle/walk(棋盘 32×32 朝左基准)、character_hero_portrait(B 区立绘 46×48)+ character_enemy_<键> 八敌 | | `fx/` | 特效帧 | shovel_swing_f0/f1/f2(挥铲三帧,握把锚 (18,12)) | -| `scene/` | **场景图**(背景层 + 可点物件层;2026-10-22 新增,营地场景图首批) | `scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件,不可点) | -| `board/` | 砖面/封印印记(三关换皮 + 封印印记件;2026-10-06 对账更正——旧 `board_floor/wall/pillar_dungeon_*` 命名退役) | terrain_floor__.png(三关地面系)、seal_mark.png | +| `scene/` | **场景图**(背景层 + 可点物件层;营地场景图首批) | `scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件,不可点)——登记超前:营地场景图批落地前盘上未建 | +| `board/` | 砖面/封印印记(三关换皮 + 封印印记件) | terrain_floor__.png(三关地面系)、seal_mark.png | ## 4. 坐标/朝向等工程侧约定备查 - 素材基准朝向:角色/铲帧一律朝**左**入库,右向=运行时 `flip_h`(镜像帧不入包,right 变体留 `artwork/candidates/`) - 挥铲握把锚:画布 (18,12)(三帧同位——以手为圆心旋转的预烤相位) -- 缩放纪律:圣经 §8.2 整数缩放为一般规则;棋盘小人 1.5× 系 2026-09-26 用户明示裁决(豁免注记归 art-bible 批另落) +- 缩放纪律:圣经 §8.2 整数缩放为一般规则;棋盘小人 1.5× 系用户明示裁决的登记例外(注记见 art-bible §8.2) diff --git a/design/art/character-design.md b/design/art/character-design.md index 9d64296b..d7a1a50c 100644 --- a/design/art/character-design.md +++ b/design/art/character-design.md @@ -1,82 +1,82 @@ -# 人物设计文档(Character Design)— Dungeon Sweeper +# 人物设计文档(Character Design)— Dungeon Sweeper -> **性质**:美术圣经 §5(角色设计)的独立承载文档——2026-09-23 抽离批自 art-bible v1.6 §5 迁出成文,动机是**人设高频改动、独立成档免动圣经**;圣经 §5 自此为引用存根指向本文件,**小节号 §5.1–§5.6 沿用不变**(跨文档引用经圣经 §5 存根仍可达,新引用直指本文件)。 -> **边界**:本文件只定角色呈现;角色承载的机制语义(护甲克制矩阵、技能与状态)以 GDD 为准(敌人系统/增益状态/战斗结算)。色彩系统、形状语言总纲引用美术圣经 [art-bible](art-bible.md) §4/§3,冲突以圣经为准。提示词执行侧(可直贴串)归 [art-prompts](art-prompts.md),本文件是它的规格上游。 +> **性质**:美术圣经 §5(角色设计)的独立承载文档——**人设高频改动、独立成档免动圣经**;圣经 §5 自此为引用存根指向本文件,**小节号 §5.1–§5.6 沿用不变**(跨文档引用经圣经 §5 存根仍可达,新引用直指本文件)。 +> **边界**:本文件只定角色呈现;角色承载的机制语义(护甲克制矩阵、技能与状态)以 GDD 为准(敌人系统/增益状态/战斗结算)。色彩系统、形状语言总纲引用美术圣经 [art-bible](art-bible.md) §4/§3,冲突以圣经为准。提示词执行侧(可直贴串)归 [art-prompts](art-prompts.md),本文件是它的规格上游。 --- -## 5.1 角色剪影与轮廓通道(自圣经 §3.1 迁入) +## 5.1 角色剪影与轮廓通道(自圣经 §3.1 迁入) -硬规则:**所有角色自带轮廓通道**——亮色描边(暖白族,禁黑描边)+落影底盘,识别不依赖所在背景。敌人不占格,剪影禁止借格底色成立;同一套剪影因此在棋盘、结算画面与任一战斗舞台通用,不为战斗表现未决的舞台问题付重画成本。←已锁事实;「禁黑描边」= 2026-09-20 提示词卡实践顺带固化 +硬规则:**所有角色自带轮廓通道**——亮色描边(暖白族,禁黑描边)+落影底盘,识别不依赖所在背景。敌人不占格,剪影禁止借格底色成立;同一套剪影因此在棋盘、结算画面与任一战斗舞台通用,不为战斗表现未决的舞台问题付重画成本。←已锁事实 -**主角描边例外(2026-10-06 待决项 B 用户拍板——显式例外,非漂移)**:主角四态硬描边 = **深褐 `(40,19,13)`**,不入暖白族——深描边在浅砖面格底上对比更高、主角更突出;暖白达标率 0–28% 属例外内允许(敌人仍暖白族,实测达标 27–69%,不变)。素材不随本条修改;样张批(圣经 §1.4)终审复核例外成立性,复核义务登记待办清单。 +**主角描边例外(显式例外,非漂移)**:主角四态硬描边 = **深褐 `(40,19,13)`**,不入暖白族——深描边在浅砖面格底上对比更高、主角更突出;暖白达标率 0–28% 属例外内允许(敌人仍暖白族,实测达标 27–69%,不变)。素材不随本条修改;样张(圣经 §1.4)终审复核例外成立性,复核义务登记待办清单。 -四类角色 archetype 以高宽比/轮廓凹凸数/锐角数一眼区分(道具点——唯一非生物剪影——留在圣经 §3.1): +四类角色 archetype 以高宽比/轮廓凹凸数/锐角数一眼区分(道具点——唯一非生物剪影——留在圣经 §3.1): -- **小人**:窄、直立、对称,凹凸 ≤2;剪影在 2~3 头身两档下均成立(比例见 §5.2);格内占位最窄,不吞数字位。←原一;圣经 §2.1 -- **普通怪**:近圆墩形,宽≥高,零锐角。取 Peglin 的胖体量,弃其色块依赖——先读轮廓,后读颜色。←P4 -- **精英**:普通怪形加恰一处垂直延伸(角/冠/棘),底盘不变。←圣经 §2.6 郑重感的低配档 -- **Boss**:敌类中唯一允许锐角与不对称者,体量最大,与紫赤色相独占并列"形状独占"。←圣经 §2.6 +- **小人**:窄、直立、对称,凹凸 ≤2;剪影在 2~3 头身两档下均成立(比例见 §5.2);格内占位最窄,不吞数字位。←原一;圣经 §2.1 +- **普通怪**:近圆墩形,宽≥高,零锐角。取 Peglin 的胖体量,弃其色块依赖——先读轮廓,后读颜色。←P4 +- **精英**:普通怪形加恰一处垂直延伸(角/冠/棘),底盘不变。←圣经 §2.6 郑重感的低配档 +- **Boss**:敌类中唯一允许锐角与不对称者,体量最大,与紫赤色相独占并列"形状独占"。←圣经 §2.6 ## 5.2 头身比与体量档位 -- **头身比**:全角色 2~3 头身(chibi)两档——小人剪影须在两档下均成立(§5.1);敌种按 archetype 取档,同一敌种恒定一档(enemy_id 键恒定纪律,圣经 §4.6.4)。←风格基调 2026-09-10 -- **体量档位 = 生物体型通道(2026-10-06 用户三约束修订,原「强度先验·普通 < 精英 < Boss 单调递增」读法退役)**:体量按**物种天性**定,禁按强度档位线性映射——小体型精英与大体型普通怪并存(锁定实例:钩爪恐魔 > 灰矮人、灰矮人 < 熊地精、食人魔酋长 > 熊地精);「大个子多半有重击」的强度先验读法改由意图徽标(§5.6)与甲档(§5.3)承担,体量不再报强度;图像不报数,精确数值只在结算明细。 -- 像素画布/精灵尺寸**不在本节**——归圣经 §8 资产规范(D1,另批);本节只锁比例关系与档位次序。 +- **头身比**:全角色 2~3 头身(chibi)两档——小人剪影须在两档下均成立(§5.1);敌种按 archetype 取档,同一敌种恒定一档(enemy_id 键恒定纪律,圣经 §4.6.4)。←风格基调 +- **体量档位 = 生物体型通道(原「强度先验·普通 < 精英 < Boss 单调递增」读法已弃用)**:体量按**物种天性**定,禁按强度档位线性映射——小体型精英与大体型普通怪并存(锁定实例:钩爪恐魔 > 灰矮人、灰矮人 < 熊地精、食人魔酋长 > 熊地精);「大个子多半有重击」的强度先验读法改由意图徽标(§5.6)与甲档(§5.3)承担,体量不再报强度;图像不报数,精确数值只在结算明细。 +- 像素画布/精灵尺寸**不在本节**——归圣经 §8 资产规范;本节只锁比例关系与档位次序。 -## 5.3 护甲类型视觉区分(候选方案,样张批冻结) +## 5.3 护甲类型视觉区分(候选方案,样张冻结终稿) -> **白话**:敌方有三种护甲类型——轻甲(light)/魔法护甲(ward)/重甲(heavy),克制博弈需要玩家从画面读出"这只怪穿什么甲";本节定三档甲怎么画。键与命名事实源 = `data/schemas/enemy_table.gd` 的 `armor_type` 枚举与 [术语表](../gdd/术语表.md)(轻甲/魔法护甲/重甲,2026-09-15 命名统一批)。 +> **白话**:敌方有三种护甲类型——轻甲(light)/魔法护甲(ward)/重甲(heavy),克制博弈需要玩家从画面读出"这只怪穿什么甲";本节定三档甲怎么画。键与命名事实源 = `data/schemas/enemy_table.gd` 的 `armor_type` 枚举与 [术语表](../gdd/术语表.md)(轻甲/魔法护甲/重甲)。 -- **通道 = 甲面覆盖率 × 甲材质色**,不触 §5.1 剪影 archetype(底盘与体量轴不动)——「重甲语义走色板不作形状独占」的落法:甲档不新增独占通道(独占 = Boss 双通道专属),装备配件画法不属独占。 -- **轻甲 light**:甲面覆盖率≈0——皮条/布带点缀或裸露,体表以物种本色为主。 -- **魔法护甲 ward**:甲面覆**符纹饰带**——发光符纹走轮廓描边同族暖白,不占任何语义色(圣经 §4.2),覆盖率居中。 -- **重甲 heavy**:**金属甲板**覆盖肩/胸/盾沿,冷灰金属材质色(与全作暖底成材质对比),覆盖率最大。 -- **天然甲壳/毛皮属体表,不计甲档**(巨蜘蛛/钩爪恐魔的几丁质是皮肤不是装备)——甲档只读装备甲板/符纹,防群行小体量敌种被误读为重甲。 -- **灰度通道**:色弱玩家按**甲面覆盖率**三档可辨(无甲板→符纹带→大面积甲板)——形状+色彩双通道纪律。 -- 完整克制读数 = HUD 状态图标(alert/fortify 等,圣经 §4.6.1)+ 本节甲面视觉辅助;克制矩阵数值不在画面上报数。←敌人系统 armor_type;营地扩展前置批 +- **通道 = 甲面覆盖率 × 甲材质色**,不触 §5.1 剪影 archetype(底盘与体量轴不动)——「重甲语义走色板不作形状独占」的落法:甲档不新增独占通道(独占 = Boss 双通道专属),装备配件画法不属独占。 +- **轻甲 light**:甲面覆盖率≈0——皮条/布带点缀或裸露,体表以物种本色为主。 +- **魔法护甲 ward**:甲面覆**符纹饰带**——发光符纹走轮廓描边同族暖白,不占任何语义色(圣经 §4.2),覆盖率居中。 +- **重甲 heavy**:**金属甲板**覆盖肩/胸/盾沿,冷灰金属材质色(与全作暖底成材质对比),覆盖率最大。 +- **天然甲壳/毛皮属体表,不计甲档**(巨蜘蛛/钩爪恐魔的几丁质是皮肤不是装备)——甲档只读装备甲板/符纹,防群行小体量敌种被误读为重甲。 +- **灰度通道**:色弱玩家按**甲面覆盖率**三档可辨(无甲板→符纹带→大面积甲板)——形状+色彩双通道纪律。 +- 完整克制读数 = HUD 状态图标(alert/fortify 等,圣经 §4.6.1)+ 本节甲面视觉辅助;克制矩阵数值不在画面上报数。←敌人系统 armor_type ## 5.4 主角三态规格 -- **形象锚** = 站姿样张先例(2026-09-16 脚本管线产物)经 2026-09-23 特征批+同日配装修订改定:**蓝兜帽冒险者(兜帽取下披于颈肩,似短披肩非垂背)、暖米白上衣+深棕工装裤(弃全蓝)、白皙肤色、棕色短发、皮革背带腰带、圆头挖掘铲(待机插地扶柄)、结构式冒险背包插旗朱红矩形旗(1.5× 体宽;2026-09-28 旗色脱钩拍板)、额前护目镜、腰挂提灯、半指手套与卷裤脚皮护膝**;三态必须同形象(同 seed 起步只换动作短语,提示词卡承载)。 -- **兜帽披肩(2026-09-23 用户拍板)**:兜帽**不戴在头上**,取下披于肩上——露出棕色短发与整脸(表情可读),蓝色兜帽披在肩头仍是最近的"己方"识别点;三态同款披肩。 -- **圆头挖掘铲(武器,2026-09-23 用户拍板)**:持铲不持剑——**铲 = 本作核心动词"挖安全格"的角色化**,挖掘与攻击共用同一动作语义(攻击表现即挥铲,后续攻击类表现优先围绕铲展开)。画法:圆头铲刃 + 木柄(木柄走木色族 `#8A5A34`,铲刃浅铁灰、不做冷灰重甲质感);**待机态 = 铲插立在地上、一只手扶在铲柄顶端**(2026-09-23 修订,原"拄于身侧"退役);攻击/受击态仍为手持。刀光/铲击光效/弹道不进精灵图(归战斗演出批另立资产)。 -- **冒险背包与插旗(身份件,2026-09-23 用户拍板,同日尺寸修正)**:背负**结构式冒险背包**——翻盖+皮扣带的帆布背包(暖卡其布面,皮革件走木色族),**宽 ≈1.5× 体宽**:双肩背负、两侧各略超出身形一点;**禁圆麻袋化**(措辞弃"鼓胀掳获袋")、**禁特里式 3× 超宽**(反面参照:旷野之息特里;正面参照:合金弹头双主角大背包、超级马里奥奇诺比奥队长)。**旗杆插在背包一侧**(不要求完整可见,**被头部遮挡一部分亦可**),旗子**尺寸大于头部**(2026-09-23 修订,弃"背包顶小旗"旧稿),短杆**矩形**旗旗面 = **旗朱红 `#C63D22`**(2026-09-28 用户拍板:**人设背包旗与棋盘封印色脱钩**——封印色沿革〔旗朱 #C63D22→封印紫 2026-09-26→封印黑 2026-09-28〕**仅作用于棋盘格封印旗**;人设旗维持旗朱红、形为矩形非三角,以 normal/hit 定稿成图为基准),把"插旗宣告"的角色化焊在人设上,主角即插旗者(**插旗动作呈现名 = 封印**,2026-09-25 术语批,见 art-bible §3.2;2026-09-28 脱钩拍板后背包旗**不再**视作封印印记的携带形态——旗面旗朱红矩形,与棋盘封印黑旗形色均不同,仅保留"主角即插旗者"的身份叙事)。三条纪律:①旗面红=旗朱语义**仅旗非服饰**(负向 `no red clothing` 不拦截正向 `red flag`——红服饰禁令与红旗的边界由正向句显式锁色值;封印黑仍为棋盘格封印旗专属色,圣经 §4.1 不变);②背包+旗 = 主角对 §5.1"对称"的**唯一破例组**(语义配件非装饰),旗面朝向恒定画右、跨素材一致;③轮廓凹凸预算按"背包+旗"整组记账,样张批(圣经 §1.4)验证背负后剪影仍窄、仍可辨、不超格宽。 -- **护目镜与腰灯(2026-09-23 用户拍板,同日定案)**:**护目镜推在额前**(兜帽披肩后直接戴于额上,不戴眼上——眼睛是表情载体;皮革带+圆镜片,防尘挖掘者的职业信号);**提灯挂腰间,定案不手持**(小号铜色提灯,**不点亮**——原守圣经 §2 明度恒定;2026-09-29 圣经 v3.0 明度分层后,空间光照属允许域,**提灯点亮解禁为候选**,点亮形制(光晕半径/暖色值/与舞台光照关系)归整屏样张批重议,本批不改成图;头部饰品唯一化,头灯方案退役)。 -- **衣着细节(2026-09-23 用户拍板)**:**棕色半指手套**(挖土护手,露指保灵巧读感)+ **裤脚卷起、棕色皮护膝**(挖掘跪地的工装感)——小件走皮革木色族,不占剪影凹凸预算,不新增语义色。 -- **三态**:① 待机 = 窄、直立、对称(§5.1 小人原型,含背包、旗与提灯记账);② 攻击 = 允许破对称,整体仍窄、不超格宽;③ 受击 = 无血红、无伤害数字(伤害数字归 HUD 层,不进精灵图)。 -- **随身件 = 秘宝形象候选池(2026-09-23 用户提案登记)**:铲、旗、背包、提灯四件随身件是未来秘宝批的**形象候选池**——道具实装可借用主角身上已有的同款(彩蛋闭环:玩家在道具栏认出主角身上的物件);具体道具语义/数值归秘宝批 GDD,人设侧义务 = 四件画得出来、认得出。 -- **配色修订(2026-09-23 用户拍板:弃全蓝,蓝只在明显处)**:**小人蓝 `#2E86DE` 只保留在兜帽**(披肩状态仍可辨,与棋盘小人/光标识别一致,"全作唯一蓝色"语义不变);上衣改**暖米白 `#EFE3C8`**(数字色牌同族暖纸白)、长裤改**深棕工装 `#6B4A2E`**(木色族深化)——与掩尘棋盘同域协调、靠明度差保持身份识别;皮革件木色族 `#8A5A34`;眼部用数字墨 `#33261B` 族。(色值均候选,样张批圣经 §1.4 冻结终稿)←圣经 §4.1 -- **语义约束**:蓝只说己方(圣经 §4.2)——主角是全作唯一蓝色角色,敌方与物件主色禁止入蓝族;禁入带 300°–345° 对全角色生效(Boss 独占例外,§5.5)。 -- **生成顺序口径(2026-09-23 用户确认)**:先人设定稿(本档 §5.4 + 提示词卡待机态),定稿后再逐状态出图——战斗画面的**侧身面向怪物**形象属状态图,随表现层批次另行生成;状态图需侧/背身时,该卡负向提示词中的 `side view / back view` 条目按卡摘除(人设立绘卡仍保留禁侧背)。 -- **棋盘小人(2026-09-23 用户拍板,原开放项定案)**:棋盘格上的主角小人 = **主角人设图压缩至二头身后的简化版像素小人**——随人设定稿派生(同款装备剪影:铲/背包/旗可辨),二头身在 §5.2 两档内;派生工艺(最近邻降采样+手修简化)归一屏样张批与脚本管线定。 +- **形象锚** = 站姿样张先例改定:**蓝兜帽冒险者(兜帽取下披于颈肩,似短披肩非垂背)、暖米白上衣+深棕工装裤(弃全蓝)、白皙肤色、棕色短发、皮革背带腰带、圆头挖掘铲(待机插地扶柄)、结构式冒险背包插朱红矩形旗(1.5× 体宽)、额前护目镜、腰挂提灯、半指手套与卷裤脚皮护膝**;三态必须同形象(同 seed 起步只换动作短语,提示词卡承载)。 +- **兜帽披肩**:兜帽**不戴在头上**,取下披于肩上——露出棕色短发与整脸(表情可读),蓝色兜帽披在肩头仍是最近的"己方"识别点;三态同款披肩。 +- **圆头挖掘铲(武器)**:持铲不持剑——**铲 = 本作核心动词"挖安全格"的角色化**,挖掘与攻击共用同一动作语义(攻击表现即挥铲,后续攻击类表现优先围绕铲展开)。画法:圆头铲刃 + 木柄(木柄走木色族 `#8A5A34`,铲刃浅铁灰、不做冷灰重甲质感);**待机态 = 铲插立在地上、一只手扶在铲柄顶端**;攻击/受击态仍为手持。刀光/铲击光效/弹道不进精灵图(归战斗演出另立资产)。 +- **冒险背包与背包旗(身份件)**:背负**结构式冒险背包**——翻盖+皮扣带的帆布背包(暖卡其布面,皮革件走木色族),**宽 ≈1.5× 体宽**:双肩背负、两侧各略超出身形一点;**禁圆麻袋化**(措辞弃"鼓胀掳获袋")、**禁特里式 3× 超宽**(反面参照:旷野之息特里;正面参照:合金弹头双主角大背包、超级马里奥奇诺比奥队长)。**旗杆插在背包一侧**(不要求完整可见,**被头部遮挡一部分亦可**),旗子**尺寸大于头部**,短杆**矩形**旗旗面 = **旗朱红 `#C63D22`**(人设背包旗与棋盘封印色**脱钩**——封印色仅作用于棋盘格封印旗,色史归 art-bible §4.1;人设旗形为矩形非三角,以 normal/hit 定稿成图为基准),把"宣告落旗"的角色化焊在人设上,主角即背包旗持有者(**动作呈现名 = 封印**,见 art-bible §3.2;背包旗**不**视作封印印记的携带形态——旗面旗朱红矩形,与棋盘封印黑旗形色均不同,仅保留"主角即背包旗持有者"的身份叙事)。三条纪律:①旗面红=旗朱语义**仅旗非服饰**(负向 `no red clothing` 不拦截正向 `red flag`——红服饰禁令与红旗的边界由正向句显式锁色值;封印黑仍为棋盘格封印旗专属色,圣经 §4.1 不变);②背包+旗 = 主角对 §5.1"对称"的**唯一破例组**(语义配件非装饰),旗面朝向恒定画右、跨素材一致;③轮廓凹凸预算按"背包+旗"整组记账,样张(圣经 §1.4)验证背负后剪影仍窄、仍可辨、不超格宽。 +- **护目镜与腰灯**:**护目镜推在额前**(兜帽披肩后直接戴于额上,不戴眼上——眼睛是表情载体;皮革带+圆镜片,防尘挖掘者的职业信号);**提灯挂腰间,定案不手持**(小号铜色提灯,**不点亮**;空间光照属圣经 §2 明度分层允许域,**提灯点亮为候选**,点亮形制(光晕半径/暖色值/与舞台光照关系)归整屏样张重议;头部饰品唯一化,头灯方案已弃)。 +- **衣着细节**:**棕色半指手套**(挖土护手,露指保灵巧读感)+ **裤脚卷起、棕色皮护膝**(挖掘跪地的工装感)——小件走皮革木色族,不占剪影凹凸预算,不新增语义色。 +- **三态**:① 待机 = 窄、直立、对称(§5.1 小人原型,含背包、旗与提灯记账);② 攻击 = 允许破对称,整体仍窄、不超格宽;③ 受击 = 无血红、无伤害数字(伤害数字归 HUD 层,不进精灵图)。 +- **随身件 = 秘宝形象候选池**:铲、旗、背包、提灯四件随身件是未来秘宝设计的**形象候选池**——道具实装可借用主角身上已有的同款(彩蛋闭环:玩家在道具栏认出主角身上的物件);具体道具语义/数值归秘宝 GDD,人设侧义务 = 四件画得出来、认得出。 +- **配色(弃全蓝,蓝只在明显处)**:**小人蓝 `#2E86DE` 只保留在兜帽**(披肩状态仍可辨,与棋盘小人/光标识别一致,"全作唯一蓝色"语义不变);上衣改**暖米白 `#EFE3C8`**(数字色牌同族暖纸白)、长裤改**深棕工装 `#6B4A2E`**(木色族深化)——与掩尘棋盘同域协调、靠明度差保持身份识别;皮革件木色族 `#8A5A34`;眼部用数字墨 `#33261B` 族。(色值均候选,样张圣经 §1.4 冻结终稿)←圣经 §4.1 +- **语义约束**:蓝只说己方(圣经 §4.2)——主角是全作唯一蓝色角色,敌方与物件主色禁止入蓝族;禁入带 300°–345° 对全角色生效(Boss 独占例外,§5.5)。 +- **生成顺序**:先人设定稿(本档 §5.4 + 提示词卡待机态),定稿后再逐状态出图——战斗画面的**侧身面向怪物**形象属状态图,随表现层设计另行生成;状态图需侧/背身时,该卡负向提示词中的 `side view / back view` 条目按卡摘除(人设立绘卡仍保留禁侧背)。 +- **棋盘小人**:棋盘格上的主角小人 = **主角人设图压缩至二头身后的简化版像素小人**——随人设定稿派生(同款装备剪影:铲/背包/旗可辨),二头身在 §5.2 两档内;派生工艺(最近邻降采样+手修简化)归整屏样张与脚本管线定。 -## 5.5 八敌种逐卡(键集与甲档事实源 = `data/tables/enemy.tres`,本表随其修订自动跟随,2026-09-25 甲档核对回填) +## 5.5 八敌种逐卡(键集与甲档事实源 = `data/tables/enemy.tres`,本表随其修订自动跟随) -| 键(enemy_id) | 显示名 | archetype 档 | 甲档 | 主色域与特征锚 | +| 键(enemy_id) | 显示名 | archetype 档 | 甲档 | 主色域与特征锚 | |---|---|---|---|---| -| goblin | 地精 | 普通怪 | 轻甲 | 橄榄绿皮、圆耳墩形,木棒;Peglin 式胖体量,先读轮廓后读颜色 | -| bugbear | 熊地精 | 普通/精英两版候选出齐供拍板 | 轻甲 | 棕黄蓬毛、獠牙下颚;粗陋皮条无甲板无符纹(轻甲语义,§5.3);精英版 = 恰一处垂直冠棘;**L2 熊地精 = 本键复用**(见下注) | -| ogre_chief | 食人魔酋长 | 精英(第一关关底 Boss 战,形状仍守精英档) | 轻甲 | 暖褐皮、肚圆顶冠;甲面覆盖率≈0——皮条/骨饰点缀、裸身为主(轻甲语义,§5.3) | -| giant_spider | 巨蜘蛛 | 普通怪(群行 [2,3]) | 轻甲 | 炭棕毛+蜜色腹纹;腿收短收粗,小体量可辨 | -| drider | 蛛化卓尔精灵 | Boss(第二关) | 魔法护甲 | 紫赤 `#8F2F6A` 色相独占+形状独占双通道;卓尔刺客上体+蛛腹;符纹饰带(魔法甲语义,§5.3) | -| duergar | 灰矮人 | 精英 | 重甲 | 灰皮灰辫须;冷灰金属甲板+高盔冠(恰一处垂直延伸) | -| hook_horror | 钩爪恐魔 | 普通怪(群行) | 轻甲 | 骨灰几丁质、圆钩双爪(圆钩非锐角);小体量可辨 | -| duergar_king | 灰矮人石君王 | Boss(第三关) | 重甲 | 紫赤独占+形状独占双通道;岩石板甲+不对称王冠,大地/岩石主题(承载技能语义见敌人系统) | +| goblin | 地精 | 普通怪 | 轻甲 | 橄榄绿皮、圆耳墩形,木棒;Peglin 式胖体量,先读轮廓后读颜色 | +| bugbear | 熊地精 | 普通/精英两版候选出齐供拍板 | 轻甲 | 棕黄蓬毛、獠牙下颚;粗陋皮条无甲板无符纹(轻甲语义,§5.3);精英版 = 恰一处垂直冠棘;**L2 熊地精 = 本键复用**(见下注) | +| ogre_chief | 食人魔酋长 | 精英(第一关关底 Boss 战,形状仍守精英档) | 轻甲 | 暖褐皮、肚圆顶冠;甲面覆盖率≈0——皮条/骨饰点缀、裸身为主(轻甲语义,§5.3) | +| giant_spider | 巨蜘蛛 | 普通怪(群行 [2,3]) | 轻甲 | 炭棕毛+蜜色腹纹;腿收短收粗,小体量可辨 | +| drider | 蛛化卓尔精灵 | Boss(第二关) | 魔法护甲 | 紫赤 `#8F2F6A` 色相独占+形状独占双通道;卓尔刺客上体+蛛腹;符纹饰带(魔法甲语义,§5.3) | +| duergar | 灰矮人 | 精英 | 重甲 | 灰皮灰辫须;冷灰金属甲板+高盔冠(恰一处垂直延伸) | +| hook_horror | 钩爪恐魔 | 普通怪(群行) | 轻甲 | 骨灰几丁质、圆钩双爪(圆钩非锐角);小体量可辨 | +| duergar_king | 灰矮人石君王 | Boss(第三关) | 重甲 | 紫赤独占+形状独占双通道;岩石板甲+不对称王冠,大地/岩石主题(承载技能语义见敌人系统) | -- **主色域 = 物种本色**,不受圣经 §4.2 语义色禁则约束(该禁则管格面与 UI 的逻辑色,角色身体主色豁免——提示词卡 2026-09-20 成例);**两例外**:禁入带 300°–345° 全角色禁入(仅 drider/duergar_king 两 Boss 独占)、蓝族禁入敌方主色(§5.4 同款)。 -- **L2 熊地精 = bugbear 复用**(第二关三行,显示名熊地精系,非独立键):同剪影**换色板不换形**(色板替换走脚本管线——原则三"先换色板"的角色侧应用);变体主色避小人蓝族与禁入带。 -- 每敌种出 2–3 版候选(清单 §2.2);受击反馈/死亡表现/顺位标记三类呈现下限见圣经 §4.6.4 资产需求行。 -- **立绘源档高(2026-10-06 定档批;定值单点 = 圣经 §8.2 立绘源档表,本档指路不复述)**:八敌 36–94 逐键定档 + 主角 48 不变、显示一律 ×2 整数档(NEAREST)——**数值权威一律圣经 §8.2**(定值单点纪律,圣经 §8 头注);定档经用户三体型约束校验(§5.2)与定档总图确认;派生管线与排版规范同归圣经 §8.2。 -- **钩爪恐魔「宽≥高」豁免登记(2026-10-06,待样张批终审)**:定档源 58 高窄条束状剪影(长臂圆钩),宽高比 <1,与 §5.1 普通怪「近圆墩形宽≥高」冲突——按体量=生物天性(§5.2 修订)登记豁免:以长臂束状轮廓为识别锚,不强行压成墩形;样张批(圣经 §1.4)终审复验灰度剪影可辨性。 +- **主色域 = 物种本色**,不受圣经 §4.2 语义色禁则约束(该禁则管格面与 UI 的逻辑色,角色身体主色豁免);**两例外**:禁入带 300°–345° 全角色禁入(仅 drider/duergar_king 两 Boss 独占)、蓝族禁入敌方主色(§5.4 同款)。 +- **L2 熊地精 = bugbear 复用**(第二关三行,显示名熊地精系,非独立键):同剪影**换色板不换形**(色板替换走脚本管线——原则三"先换色板"的角色侧应用);变体主色避小人蓝族与禁入带。 +- 每敌种出 2–3 版候选(清单 §2.2);受击反馈/死亡表现/顺位标记三类呈现下限见圣经 §4.6.4 资产需求行。 +- **立绘源档高(定值单点 = 圣经 §8.2 立绘源档表,本档指路不复述)**:八敌 36–94 逐键定档 + 主角 48 不变、显示一律 ×2 整数档(NEAREST)——**数值权威一律圣经 §8.2**(定值单点纪律,圣经 §8 头注);定档经三体型约束校验(§5.2)确认;派生管线与排版规范同归圣经 §8.2。 +- **钩爪恐魔「宽≥高」豁免登记(待样张终审)**:定档源 58 高窄条束状剪影(长臂圆钩),宽高比 <1,与 §5.1 普通怪「近圆墩形宽≥高」冲突——按体量=生物天性(§5.2)登记豁免:以长臂束状轮廓为识别锚,不强行压成墩形;样张(圣经 §1.4)终审复验灰度剪影可辨性。 -## 5.6 意图准备动画三档(2026-09-18 用户拍板,规格承载节) +## 5.6 意图准备动画三档(规格承载节) -> **白话**:敌人下一拍做什么,不用文字预告——用三档动作动画让玩家"看姿态就知道要有事"。 +> **白话**:敌人下一拍做什么,不用文字预告——用三档动作动画让玩家"看姿态就知道要有事"。 -- **三档**:① 普通攻击 = 常态待机(待机即预示,零独立资产);② 攻击技能 = 特殊准备动画(放光/高举武器等"摇号感"动作);③ 重击/硬化类 = 蓄力动作或防御姿态。 -- **双通道纪律**:三档在灰度/剪影层可辨(色弱安全,圣经 §4.5 同款要求)。 -- **呈现纪律**:不显示文字与技能名(文字与技能名彻底退出预告);映射按 enemy_id **全局恒定、跨 run 禁重掷**;预告时序 = 下一拍,挖掘期全程可见,结算不另设独立意图播片。契约锚 = 圣经 §4.6.4 意图姿态行。 -- **终稿帧规格**(帧数/时长/结算节拍对齐)归圣经 §8 资产规范与战斗一屏样张批(清单 §2.3);占位符号(—/⚔/✦/蓄力)现行在役,动画到位后原位替换。 +- **三档**:① 普通攻击 = 常态待机(待机即预示,零独立资产);② 攻击技能 = 特殊准备动画(放光/高举武器等"摇号感"动作);③ 重击/硬化类 = 蓄力动作或防御姿态。 +- **双通道纪律**:三档在灰度/剪影层可辨(色弱安全,圣经 §4.5 同款要求)。 +- **呈现纪律**:不显示文字与技能名(文字与技能名彻底退出预告);映射按 enemy_id **全局恒定、跨 run 禁重掷**;预告时序 = 下一拍,挖掘期全程可见,结算不另设独立意图播片。契约锚 = 圣经 §4.6.4 意图姿态行。 +- **终稿帧规格**(帧数/时长/结算节拍对齐)归圣经 §8 资产规范与战斗一屏样张(清单 §2.3);占位符号(—/⚔/✦/蓄力)现行使用,动画到位后原位替换。 --- diff --git a/design/art/remote-gen-prompts.md b/design/art/remote-gen-prompts.md index 2612a874..29ed0880 100644 --- a/design/art/remote-gen-prompts.md +++ b/design/art/remote-gen-prompts.md @@ -1,7 +1,8 @@ # 远程生图提示词单(对话式生图操作单) -> **渠道定位(2026-10-06 拍板)**:生图渠道 = **远程对话式生图为主,本地 ComfyUI 为备选**(v2.7「彻底关闭」系当时口径,现行更正为备选);本文件是**对话操作单**(怎么开对话/贴什么参考图/怎么续写),提示词正文词库归 [art-prompts](art-prompts.md)——分工:词库改词库、操作单改流程,同一段正文不双处维护。 -> **出图登记义务(2026-10-06 P1-1 复现性)**:每张产出登记 **模型名 + 分辨率 + seed(可取则取)+ 对话/日期**,随候选原图同存 `artwork/candidates/<资产名>/`;缺登记的候选不入正稿遴选。 +> **渠道定位**:生图渠道 = **远程对话式生图为主,本地 ComfyUI 为备选**;本文件是**对话操作单**(怎么开对话/贴什么参考图/怎么续写),提示词正文词库归 [art-prompts](art-prompts.md)——分工:词库改词库、操作单改流程,同一段正文不双处维护。 +> **冲突优先级**:画法与色彩以 [art-bible](art-bible.md) 为准,人物规格以 [character-design](character-design.md) 为准;本单为执行侧操作单,不另立规格。 +> **出图登记义务(复现性)**:每张产出登记 **模型名 + 分辨率 + seed(可取则取)+ 对话/日期**,随候选原图同存 `artwork/candidates/<资产名>/`;缺登记的候选不入正稿遴选。 > 开新对话出图:同一角色 2–3 版候选各开新对话(同对话续写=修图);同角色换姿势在同对话续写。分辨率开大档;候选原图存 `artwork/candidates/<资产名>/`。 ## 1. 主角战斗立绘 @@ -78,10 +79,10 @@ Pixel art style. true pixel art game sprite drawn on a tiny 48x72 pixel canvas, ## 3. 瓦片纹理 -**制式 = 拼表制主选(2026-10-06 用户拍板)——三步流程**: +**制式 = 拼表制主选——三步流程**: 1. **程序占位出样图**:现行程序化格面(占位在役)截一张作参考样图; 2. **整图生成**:开新对话,贴样图 + 发下方整段,一张 2×2 出全部瓦片变体(风格统一、一张管全族); -3. **裁切取用**:按 50% 坐标切图、每格内缩 2–3% 切掉可能的边框 → 统一降采样 → 成片瓦片 21 枚由程序从切出的材质合成(词表:高墙瓦片词表(已落码 board_world.gd 查表,文件已退役))。 +3. **裁切取用**:按 50% 坐标切图、每格内缩 2–3% 切掉可能的边框 → 统一降采样 → 成片瓦片 21 枚由程序从切出的材质合成(查表已落码 `board_world.gd`)。 ``` Seamless tileable pixel art texture sheet for a dungeon board game, arranged as a clean 2x2 grid of four equal square quadrants filling the entire image edge to edge, with no gaps, no dividing lines, no borders, no frames, no grid lines, no labels and no text. Top-left quadrant: warm tan rock surface with subtle darker seams between stone patches and small lighter facets. Top-right quadrant: the same warm tan rock surface but with a few thin darker crack lines scattered across it. Bottom-left quadrant: the same warm tan rock surface but with a few tiny lighter stone speckles sparsely scattered. Bottom-right quadrant: dry dirt ground in a dark warm brown palette around #B39A78, clearly darker than the tan rock, with a few thin cracks and tiny pebble speckles, sparse. Every quadrant is a flat 2D top-down texture — not an object, no perspective — and each quadrant must wrap seamlessly at its own four edges. Uniform directionless lighting across all four quadrants, no vignette, no cast shadow, no bevel, no edge highlight anywhere. Warm tan-brown palette around #C9AF83 for the three rock quadrants. Clean retro pixel art look, limited palette, no gradients, no text, square image. diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 660701d0..32335b8f 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -54,7 +54,7 @@ 11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置 13. **中间产物吸收即死**——细则见 AGENTS.md 硬性红线与 doc-conventions 第 11 节 -14. **改动级验收闭合**——写卡期列需求侧对照表,实现期补实际对照表(测试文件:行),每个行为改动做撤销证明;合回前独立验收审计。细则=test-conventions §8 +14. **改动级验收闭合**——写卡期列需求侧对照表,实现期补实际对照表(测试文件:行),每个行为改动的验收断言从真实入口进入、在当轮真实跑批中执行并通过(判据绑定面口径=test-conventions §8.4;禁变异类测试与撤销类取证,2026-10-24/28 用户两裁);合回前独立验收审计。细则=test-conventions §8 ## 4. 测试执行两入口铁规(细则) diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index 76642a5a..fd19c4cf 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -97,7 +97,7 @@ - **反向**:diff 里每个行为改动至少一行;没有对应行的改动,必须写明「非行为改动」或「由第 X 行覆盖」。 - **测试侧**:新增或改写的每条断言至少挂一行;挂不上的孤儿断言删掉或补挂到需求点。 -**验证集清单维护锚(2026-10-22 用户裁)**:`game/tests/验证集清单.md` 是迁移断言全体的冻结参照清单(机器提取生成、只剪切禁改写、出口逐条核对,机制见该文件头注;清单自断言迁移批第 0 步建立时起存在)——清单在册后,任何断言**新增、删除或改判**必须同步该清单,与改动—验收对照表同一条纪律:清单没跟上的断言改动视同未完成。 +**能力清单维护锚(2026-10-28 用户裁,验证与清单体系整改阶段 1;阶段 2 后定稿)**:逐断言账本 `game/tests/验证集清单.md` 已随阶段 2「存量断言精简」完成退役——冻结对账基准(基点 `b9260138`/1587 条)与精简对账结果(33 站点并为 7、1587→1561,合回 `87d241c1`)留 git 历史。覆盖索引由 `game/tests/README.md`「能力清单 v1」唯一承担:每行一个能力,给真实入口与 2–3 个代表用例(`文件:函数`)。维护义务——**每批收口核对本批受影响能力行**:代表用例可 grep、在本批真实跑批中执行、真实入口未绕过改动点,并刷新「最后验证批次」;新增能力加行,能力退役删行。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。 ### 8.4 真实入口断言(验收闭合的主判据) diff --git a/design/gdd/02-系统清单.md b/design/gdd/02-系统清单.md index 01a69a23..2f74f45f 100644 --- a/design/gdd/02-系统清单.md +++ b/design/gdd/02-系统清单.md @@ -15,7 +15,7 @@ | `术语表.md` | 全库专用术语的唯一对照表——危险格/安全格/同调位/RAP/GP/真视等定义+退役词对照(2026-10-11 建立,v1.23) | | `待办清单.md`(仓库根,不属本树) | 全项目待办事项的唯一登记处;各系统文档的「待办」节只留指路存根 | | `体验验证清单.md`(仓库根,不属本树) | 需要真人试玩观测的验证项登记处(与待办清单分工:待办清单是"要拍板的事",本篇是"要玩过才知道的事");每条注明结论回流路径 | -| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.17**,2026-10-13) | +| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.19**,2026-10-13) | ## systems/ 五个循环 diff --git a/design/gdd/08-Alpha版本标准.md b/design/gdd/08-Alpha版本标准.md index f4bc9b92..ea7461dd 100644 --- a/design/gdd/08-Alpha版本标准.md +++ b/design/gdd/08-Alpha版本标准.md @@ -2,7 +2,7 @@ > **一句话定位**:本文定义这个项目的 **Alpha 版本包含哪些东西**——它是里程碑 M3 的完成标准,也是判断"还差什么"的对照表。 > **本文只列条目,不写内容**:每样东西是什么、规则怎么走,一律归各自的权威文档,本文只给指路链接。想知道某样东西的定义,请点链接,不要在本文里找解释。 -> **状态**:**已定稿(v1.18)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。 +> **状态**:**已定稿(v1.19)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。 > **成文**:2026-10-13。 ## 本文怎么用 @@ -48,7 +48,7 @@ Alpha 内以玩法循环本身的内容与表现为主(棋盘、关卡、战 | 冒险结构 | [冒险结构](systems/冒险循环/冒险结构.md) | 在役 | | 关卡节点地图 | [关卡节点地图](systems/冒险循环/关卡节点地图.md) | 在役(信息迷雾类型可见性已随迭代 34 卡④ 实装,2026-10-22) | | 关卡节点图重构(增加可选节点) | [关卡节点地图](systems/冒险循环/关卡节点地图.md)+[关卡节点图生成](systems/冒险循环/关卡节点图生成.md) | 在役(2026-10-25 迭代 35 卡①② 合回:图源切为参数档×种子生成图、模板链路退役;真人目检选路项已登记体验验证清单,登记不阻塞) | -| 关卡节点图生成 | [关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(参数化长图:产出规格与生成参数) | 在役(2026-10-24 迭代 35 卡① 生成器核心合回;enemy.tres 与 board_gen.tres 已覆盖到第三关深度 13) | +| 关卡节点图生成 | [关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(参数化长图:产出规格与生成参数) | 在役(2026-10-24 迭代 35 卡① 生成器核心合回;enemy.tres 与 board_gen.tres 已覆盖到第三关深度 13;生成图 L3 深度域 0–13 超出关卡地图屏画布屏预算约 10 层——等比缩放/滚动视口两升级路径未设计待裁,[待办清单](../../待办清单.md)「超预算图策略升级路径」条在册) | | 难度曲线与 Boss | [难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md) | 在役 | | 奇物 | [奇物](systems/冒险循环/奇物.md) | 在役 | diff --git a/design/gdd/术语表.md b/design/gdd/术语表.md index fc970207..710b1364 100644 --- a/design/gdd/术语表.md +++ b/design/gdd/术语表.md @@ -1,7 +1,7 @@ # 术语表 > 全库专用术语的唯一对照表(2026-10-11 用户令建立)。规矩:新术语先登记本表、再在文档使用;退役词见文末对照表,文档正文一律使用现行词。文档中遇到看不懂的词,查本表。用词纪律(禁用词、替换值、豁免范围)见「用语规范」节。 -> 状态:**现行 v1.23**——2026-10-11 建立;历次升版明细见 git 提交史。 +> 状态:**现行 v1.24**——2026-10-11 建立;历次升版明细见 git 提交史。 ## 格与棋盘 @@ -97,6 +97,20 @@ | `seal_missed` | 封印落空事件(原 `flag_missed` 换代):封印动作落在安全格上时发出,订阅面据此做限次与豁免判定(迭代 31 卡② 落地) | | `forgive_seal_miss` | 本场首次封印落空豁免——专长「完美保险」的效果类型,`domain=battle` 首例;有效误标=误标总数 − 豁免数,豁免不改写误标账(迭代 31 卡② 落地) | +## 美术表现 + +| 术语 | 定义 | +|---|---| +| 样张 | 按美术圣经参数画成的风格试验图,是砖面形态等视觉定稿的验收闸(规格见[美术圣经](../art/art-bible.md) §1.4) | +| 整屏样张 | 整屏级样张——战斗/关卡/菜单/营地屏各出一张,验屏级文字对比度、明度分层与焦点预算 | +| 掩尘 | 未开格底色的专色名——覆尘完整砖的「未知」面色 | +| 裸土 | 已开格底色的专色名——碎裂裸露空间的沉暗面色 | +| 色牌 | 垫在数字正下方的素色直角小牌,复杂格底时保数字可读的后备手段 | +| 图类型 | 棋盘生成产物 `board_type` 的中文名——三关换皮的单位(同款地砖换色板) | +| 方向稿 | 冻结前的方向性候选稿,供气质目检;过目检后才进机检冻结 | +| 站桩 | 战斗画面区(B 区)里角色直立立绘的显示形态(配血条与状态行) | +| 徽章 | 库存条与结算明细用的小图标(与格面类型微光预读相区分) | + ## 开发流程 | 术语 | 定义 | diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index a90a66ff..5a49f05b 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -62,7 +62,7 @@ func _phase_text(p: int) -> String: func _cost(kind: String) -> int: - var ae = db.action_economy + var ae = db.run.action_economy match kind: "dig": return ae.dig_ap_cost # 基础挖格成本恒表值(钻头对价挂付费开掘侧) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index c47203a3..3136eaaa 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -680,7 +680,7 @@ func _roll_round_actions() -> void: # 容器内直读主角行) var facts := { "round_index": round_index, - "last_round_end": "none" if (action_economy.last_round_end as Dictionary).is_empty() else String((action_economy.last_round_end as Dictionary).get("path_tag", "none")), + "last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")), "mines_hit_last": int(_mines_hits["last"]), "mines_hit_total": int(_mines_hits["total"]), } diff --git a/game/core/config_domain_combat.gd.uid b/game/core/config_domain_combat.gd.uid new file mode 100644 index 00000000..517c2070 --- /dev/null +++ b/game/core/config_domain_combat.gd.uid @@ -0,0 +1 @@ +uid://dpy0a6fcggbwe diff --git a/game/core/config_domain_crossrun.gd.uid b/game/core/config_domain_crossrun.gd.uid new file mode 100644 index 00000000..a44745fc --- /dev/null +++ b/game/core/config_domain_crossrun.gd.uid @@ -0,0 +1 @@ +uid://du0gxv0lnrxnp diff --git a/game/core/config_domain_run.gd.uid b/game/core/config_domain_run.gd.uid new file mode 100644 index 00000000..3e07d6eb --- /dev/null +++ b/game/core/config_domain_run.gd.uid @@ -0,0 +1 @@ +uid://jaxlqd8ml3co diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 9388a003..4033eb64 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -101,7 +101,7 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: ## 特性订阅装载域(feats 表序→按持有集过滤→行内数组序——装载声明序第一段) func _load_feat_triggers(feat_ids: Array) -> void: - if db != null and db.feats != null: + if db != null and db.crossrun.feats != null: for row in db.crossrun.feats.rows: var fid := String(row.get("feat_id", "")) if not feat_ids.has(fid): diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 0d9687c1..e762d613 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -145,7 +145,7 @@ func forge_buy_level(route_id: String) -> bool: var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id) if row.is_empty() or not _forge_route_unlocked(route_id): return false - var lvl := meta_state.forge_state().levels.get(route_id, 0) + var lvl: int = meta_state.forge_state().levels.get(route_id, 0) if lvl >= int(row.get("cap", 0)): return false var price := ForgeTable.level_price(row, lvl) @@ -206,13 +206,13 @@ func forge_shop_status() -> Array: var out: Array[Dictionary] = [] for row in db.crossrun.forge.route_rows(): var rid := String(row.get("route_id", "")) - var lvl := meta_state.forge_state().levels.get(rid, 0) + var lvl: int = meta_state.forge_state().levels.get(rid, 0) var cap_v := int(row.get("cap", 0)) var unlocked := _forge_route_unlocked(rid) var prereq := String(row.get("prereq_route", "")) - var plvl := meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0 + var plvl: int = meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0 var pcap := int(db.crossrun.forge.route(prereq).get("cap", 0)) if prereq != "" else 0 - var selected := (String(row.get("kind", "")) == "weapon" and meta_state.forge_state().weapon == rid) \ + var selected: bool = (String(row.get("kind", "")) == "weapon" and meta_state.forge_state().weapon == rid) \ or (String(row.get("kind", "")) == "armor" and meta_state.forge_state().armor == rid) out.append({ "route_id": rid, @@ -362,7 +362,7 @@ func tree_buy(node_id: String) -> bool: func tree_toggle_fork(group: String, side: String) -> bool: if not db.crossrun.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"): return false - var cur := meta_state.tree_state().fork_sides.get(group, "") + var cur: String = meta_state.tree_state().fork_sides.get(group, "") if cur == "" or cur == side: return false # 未解锁走购买面;同态零操作 meta_state.set_fork_side(group, side) @@ -521,7 +521,7 @@ func attune_camp_status() -> Dictionary: "equipped": equipped.has(iid), "unlocked": meta_state.item_unlocked(iid), }) - var lvl := meta_state.attune_state().refresh_level + var lvl: int = meta_state.attune_state().refresh_level var next_row: Dictionary[Variant, Variant] = db.crossrun.meta.attune_refresh_row(lvl + 1) return { "slots": Attunement.slots(db, save), diff --git a/game/core/meta_state_store.gd.uid b/game/core/meta_state_store.gd.uid new file mode 100644 index 00000000..43f69277 --- /dev/null +++ b/game/core/meta_state_store.gd.uid @@ -0,0 +1 @@ +uid://bc5perm32w2yp diff --git a/game/core/slot_store.gd.uid b/game/core/slot_store.gd.uid new file mode 100644 index 00000000..04116c73 --- /dev/null +++ b/game/core/slot_store.gd.uid @@ -0,0 +1 @@ +uid://dylsba7whx5ql diff --git a/game/data/schemas/adventure_cap_segment.gd.uid b/game/data/schemas/adventure_cap_segment.gd.uid new file mode 100644 index 00000000..9437e666 --- /dev/null +++ b/game/data/schemas/adventure_cap_segment.gd.uid @@ -0,0 +1 @@ +uid://b88nnsq53mium diff --git a/game/data/schemas/adventure_quota_segment.gd.uid b/game/data/schemas/adventure_quota_segment.gd.uid new file mode 100644 index 00000000..1af7b113 --- /dev/null +++ b/game/data/schemas/adventure_quota_segment.gd.uid @@ -0,0 +1 @@ +uid://bi52mqsld3mpc diff --git a/game/data/schemas/adventure_value_segment.gd.uid b/game/data/schemas/adventure_value_segment.gd.uid new file mode 100644 index 00000000..b611d35f --- /dev/null +++ b/game/data/schemas/adventure_value_segment.gd.uid @@ -0,0 +1 @@ +uid://c0y3hcpqyv1li diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index d8874ef6..ee8671ca 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -98,31 +98,36 @@ static func curio_pick_header(payload: Dictionary) -> String: # 对齐 v2.2 追认 b 口径:小/大前缀(小GP/大GP 族)、恢复/AP 不带档位词;道具件掷定前置时 # treasure_line = 具体道具「名 · 效果摘要」行——常空走通用分支) static func bonus_card_text(id: String, tier: String, value: int, treasure_line: String = "") -> String: - var ts := "" - if tier == "common": - ts = " · 小" - elif tier == "rare": - ts = " · 大" - match id: - "attack": - return "攻击力提升%s\n攻击 +%d" % [ts, value] - "max_hp": - return "生命上限提升%s\n生命上限 +%d" % [ts, value] - "armor": - return "护甲等级提升%s\n护甲 +%d" % [ts, value] - "gp": - return "GP 袋%s\nGP +%d" % [ts, value] - "heal": - return "恢复药剂\n生命 +%d(当即回复)" % value - "treasure_item": - if treasure_line != "": - return "秘宝\n%s" % treasure_line - return "秘宝\n随机获得一件" - "ap": - return "行动点提升\n每战首轮 AP +%d" % value + if id == "treasure_item": + return _bonus_treasure_line(treasure_line) + var ts := _tier_suffix(tier) + var templates := {"attack": "攻击力提升%s\n攻击 +%d", "max_hp": "生命上限提升%s\n生命上限 +%d", + "armor": "护甲等级提升%s\n护甲 +%d", "gp": "GP 袋%s\nGP +%d"} + if templates.has(id): + return templates[id] % [ts, value] + if id == "heal": + return "恢复药剂\n生命 +%d(当即回复)" % value + if id == "ap": + return "行动点提升\n每战首轮 AP +%d" % value return id +## 档位尾注域(common=小/rare=大——旧载荷防御口径) +static func _tier_suffix(tier: String) -> String: + if tier == "common": + return " · 小" + if tier == "rare": + return " · 大" + return "" + + +## 秘宝卡文案域(treasure_line 空串=随机获得一件) +static func _bonus_treasure_line(treasure_line: String) -> String: + if treasure_line != "": + return "秘宝\n%s" % treasure_line + return "秘宝\n随机获得一件" + + # 三选一兑付反馈行(选中件效果 + 丢弃提示;tier 缺省普通档——旧载荷防御口径) static func bonus_result_text(result: Dictionary) -> String: var name := "" diff --git a/game/presentation/camp_flow_context.gd b/game/presentation/camp_flow_context.gd index e1f384f8..e8dd0b47 100644 --- a/game/presentation/camp_flow_context.gd +++ b/game/presentation/camp_flow_context.gd @@ -4,33 +4,16 @@ extends RefCounted # 原 13 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 # 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 +## 三束按域收拢(u21——同 u20 形;views=视图句柄/getters=查询 Callable 活引用/flow=编排 Callable): +## 原 12 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。 var db -var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) -var save_get: Callable # () -> SaveManagerCore -var camp_overlay: CampOverlay -var shop_overlay: ShopOverlay -var toast: ToastStack -var start_overlay: Control -var logger # PlaytestLogger(试玩日志验收取数面) -var runner_get: Callable # () -> LevelRunner -var open_modal: Callable -var close_modal: Callable -var refresh_presentations: Callable +var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger} +var getters: Dictionary = {} # {meta_get, save_get, runner_get} +var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations} -func _init(db_ref, meta_get_cb: Callable, save_get_cb: Callable, camp_overlay_ref: CampOverlay, - shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control, - logger_ref, runner_get_cb: Callable, open_modal_cb: Callable, close_modal_cb: Callable, - refresh_presentations_cb: Callable) -> void: +func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void: db = db_ref - meta_get = meta_get_cb - save_get = save_get_cb - camp_overlay = camp_overlay_ref - shop_overlay = shop_overlay_ref - toast = toast_ref - start_overlay = start_overlay_ref - logger = logger_ref - runner_get = runner_get_cb - open_modal = open_modal_cb - close_modal = close_modal_cb - refresh_presentations = refresh_presentations_cb + views = views_bundle + getters = getters_bundle + flow = flow_bundle diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index fea7cb36..174ed80b 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -21,17 +21,17 @@ var _refresh_presentations: Callable func _init(ctx: CampFlowContext) -> void: # 构造参数收拢 context 对象(13 参长签名收一处命名面) db = ctx.db - _meta_get = ctx.meta_get - _save_get = ctx.save_get - camp_overlay = ctx.camp_overlay - shop_overlay = ctx.shop_overlay - toast = ctx.toast - start_overlay = ctx.start_overlay - logger = ctx.logger - _runner_get = ctx.runner_get - _open_modal = ctx.open_modal - _close_modal = ctx.close_modal - _refresh_presentations = ctx.refresh_presentations + _meta_get = ctx.getters.meta_get + _save_get = ctx.getters.save_get + camp_overlay = ctx.views.camp_overlay + shop_overlay = ctx.views.shop_overlay + toast = ctx.views.toast + start_overlay = ctx.views.start_overlay + logger = ctx.views.logger + _runner_get = ctx.getters.runner_get + _open_modal = ctx.flow.open_modal + _close_modal = ctx.flow.close_modal + _refresh_presentations = ctx.flow.refresh_presentations # 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, diff --git a/game/presentation/curio_flow_context.gd b/game/presentation/curio_flow_context.gd index 480fb587..1e9ac5bf 100644 --- a/game/presentation/curio_flow_context.gd +++ b/game/presentation/curio_flow_context.gd @@ -4,42 +4,16 @@ extends RefCounted # 原 16 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 # 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 +## 三束按域收拢(u20——views=视图句柄/getters=查询 Callable 活引用〔禁快照〕/flow=编排 Callable): +## 原 16 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。 var db -var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) -var curio_overlay: CurioPickOverlay -var end_overlay: BattleEndOverlay -var board_view: BoardView -var toast: ToastStack -var save_get: Callable # () -> SaveManagerCore -var adventure_get: Callable # () -> AdventureRunner(会话现值) -var runner_get: Callable # () -> LevelRunner -var adventure_mode_get: Callable # () -> bool -var open_modal: Callable # (Control) -> void -var open_modal_after_playback: Callable -var close_modal: Callable -var refresh_presentations: Callable -var fatal_screen: Callable -var gp_summary: Callable # (Dictionary) -> String +var views: Dictionary = {} # {curio_overlay, end_overlay, board_view, toast} +var getters: Dictionary = {} # {meta_get, save_get, adventure_get, runner_get, adventure_mode_get} +var flow: Dictionary = {} # {open_modal, open_modal_after_playback, close_modal, refresh_presentations, fatal_screen, gp_summary} -func _init(db_ref, meta_get_cb: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay, - board_view_ref: BoardView, toast_ref: ToastStack, save_get_cb: Callable, - adventure_get_cb: Callable, runner_get_cb: Callable, adventure_mode_get_cb: Callable, - open_modal_cb: Callable, open_modal_after_playback_cb: Callable, close_modal_cb: Callable, - refresh_presentations_cb: Callable, fatal_screen_cb: Callable, gp_summary_cb: Callable) -> void: +func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void: db = db_ref - meta_get = meta_get_cb - curio_overlay = curio_overlay_ref - end_overlay = end_overlay_ref - board_view = board_view_ref - toast = toast_ref - save_get = save_get_cb - adventure_get = adventure_get_cb - runner_get = runner_get_cb - adventure_mode_get = adventure_mode_get_cb - open_modal = open_modal_cb - open_modal_after_playback = open_modal_after_playback_cb - close_modal = close_modal_cb - refresh_presentations = refresh_presentations_cb - fatal_screen = fatal_screen_cb - gp_summary = gp_summary_cb + views = views_bundle + getters = getters_bundle + flow = flow_bundle diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index f5cbf94f..df3f20fc 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -8,44 +8,16 @@ extends RefCounted const Narrator := preload("res://presentation/battle_narrator.gd") var db -var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面) -var curio_overlay: CurioPickOverlay -var end_overlay: BattleEndOverlay -var board_view: BoardView -var toast: ToastStack -var _save_get: Callable # () → 存档承载现值(同上——Attunement.slots 读数面) -var _adventure_get: Callable # () → 冒险结构现值(组合根 lambda——随局更替) -var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——reward_node 继续推进) -var _adventure_mode_get: Callable # () → 冒险模式标记现值(组合根 lambda) -var _open_modal: Callable # 模态栈单口(组合根) -var _open_modal_after_playback: Callable # 演播后排窗单口(组合根) -var _close_modal: Callable -var _refresh_presentations: Callable -var _fatal_screen: Callable -var _gp_summary: Callable # () → 战斗总结 GP 行文案(组合根 lambda——_last_gain_amount 取数面在根) +var ctx: CurioFlowContext # u13 引用束注入:构造注入 context 活引用(views/getters/flow 三束键名) +# _pending_curio/_curio_context 纯内部态保持宿主 var _pending_curio := {} # 三选一停点载荷暂存(战斗总结结算确认后开三选一面;续玩停点复用) var _curio_context := "level_clear" # 三选一面上下文(level_clear=关卡完成停点→推进下一关 / reward_node=奖励节点→回图选路) -func _init(ctx: CurioFlowContext) -> void: - # 构造参数收拢 context 对象(16 参长签名收一处命名面) +func _init(ctx_ref: CurioFlowContext) -> void: + ctx = ctx_ref db = ctx.db - _meta_get = ctx.meta_get - curio_overlay = ctx.curio_overlay - end_overlay = ctx.end_overlay - board_view = ctx.board_view - toast = ctx.toast - _save_get = ctx.save_get - _adventure_get = ctx.adventure_get - _runner_get = ctx.runner_get - _adventure_mode_get = ctx.adventure_mode_get - _open_modal = ctx.open_modal - _open_modal_after_playback = ctx.open_modal_after_playback - _close_modal = ctx.close_modal - _refresh_presentations = ctx.refresh_presentations - _fatal_screen = ctx.fatal_screen - _gp_summary = ctx.gp_summary # 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一) @@ -66,17 +38,17 @@ func resume_if_pending() -> bool: # 差异仅零面可收——curio_overlay 未开不 close。core 停点经 skip_empty_curio 消化后推进 #(continue_to_next_level 的「停点未兑付拒推进」守卫因此放行)。 func _skip_empty_stop() -> bool: - toast.show_toast("可用战利品为空——本次三选一跳过", "info") - var adventure = _adventure_get.call() + ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info") + var adventure = ctx.getters.adventure_get.call() if adventure != null: adventure.stop().skip_empty_curio() # core 停点消化(其守卫=仅空候选放行) if _curio_context == "reward_node": - var runner = _runner_get.call() + var runner = ctx.getters.runner_get.call() if runner != null: runner.continue_after_node() - elif _curio_context == "level_clear" and bool(_adventure_mode_get.call()): + elif _curio_context == "level_clear" and bool(ctx.getters.adventure_mode_get.call()): if not adventure.continue_to_next_level(): - _fatal_screen.call("关卡推进失败——见输出日志") + ctx.flow.fatal_screen.call("关卡推进失败——见输出日志") # feat_ticket = 只消化停点回店(零推进——与 on_continue 同构) _pending_curio = {} return true @@ -93,41 +65,41 @@ func on_presented(payload: Dictionary) -> void: # 空候选防御(零卡不开死面;core _redeem_feat_ticket 守卫外的末道) _skip_empty_stop() return - _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array)) + ctx.flow.open_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array)) return if int(payload.get("aux_cleared", 0)) > 0: - toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈 + ctx.views.toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈 _pending_curio = payload.duplicate(true) _curio_context = "level_clear" if bool(payload.get("refreshed", false)): # 刷新重发:refreshed 标直落选择面——不再重开胜利总结面 show_pick() return - end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", - board_view.get_global_rect(), - "%s\n%s" % [String(_gp_summary.call()), Narrator.bonus_keystone_line(payload)]) - _open_modal_after_playback.call(end_overlay) - _refresh_presentations.call() + ctx.views.end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", + ctx.views.board_view.get_global_rect(), + "%s\n%s" % [String(ctx.flow.gp_summary.call()), Narrator.bonus_keystone_line(payload)]) + ctx.flow.open_modal_after_playback.call(ctx.views.end_overlay) + ctx.flow.refresh_presentations.call() # 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 # 无替换子交互(同调装配不可变,见类头)。 func present_node_curio() -> void: - var adventure = _adventure_get.call() + var adventure = ctx.getters.adventure_get.call() _curio_context = "reward_node" var node_draw: Dictionary[Variant, Variant] = adventure.stop().node_curio_candidates() var node_cands: Array[Variant] = node_draw.get("candidates", []) as Array if node_cands.is_empty(): # 空池降级——不开面、不挂节点停点(present_node_curio 未调即未挂), # 兑付确认直落回图选路(auto-sim「node_entered 回调内立即 continue_after_node」先例) - toast.show_toast("可用战利品为空——本次三选一跳过", "info") - var runner = _runner_get.call() + ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info") + var runner = ctx.getters.runner_get.call() if runner != null: runner.continue_after_node() return - _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands)) + ctx.flow.open_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands)) adventure.stop().present_node_curio(node_cands) _feed_refresh_display(adventure) # 刷新面投喂单点 @@ -135,20 +107,20 @@ func present_node_curio() -> void: # 三选一面开出(战斗总结结算确认 / 续玩停点恢复) func show_pick() -> void: var payload: Dictionary[Variant, Variant] = _pending_curio - _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array)) - var adventure = _adventure_get.call() + ctx.flow.open_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array)) + var adventure = ctx.getters.adventure_get.call() if adventure != null: _feed_refresh_display(adventure) # 刷新面投喂单点 - _refresh_presentations.call() + ctx.flow.refresh_presentations.call() # 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构) func _feed_refresh_display(adventure: AdventureRunner) -> void: - var runner = _runner_get.call() + var runner = ctx.getters.runner_get.call() var gp := int(runner.gp) if runner != null else 0 var cost: int = adventure.refresh_bank().gp_cost() - curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost) + ctx.views.curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost) func _curio_card_texts(candidates: Array) -> Array: @@ -180,29 +152,29 @@ func _curio_card_texts(candidates: Array) -> Array: # 同 shop_refresh_cost_base/curve 旋钮、同冒险钱包 runner.gp;关卡完成停点经重发 curio_choice_presented 直落 # 选择面,节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途) func on_refresh() -> void: - var adventure = _adventure_get.call() + var adventure = ctx.getters.adventure_get.call() if adventure == null or not adventure.stop().curio_stop_pending(): return - var runner = _runner_get.call() + var runner = ctx.getters.runner_get.call() var refreshed: bool = adventure.stop().refresh_curio() if not refreshed: # 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用) var cost: int = adventure.refresh_bank().gp_cost() if runner == null or not runner.spend_gp(cost): - toast.show_toast("GP不足——刷新失败", "warn") + ctx.views.toast.show_toast("GP不足——刷新失败", "warn") return refreshed = adventure.stop().refresh_curio_paid() - _refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面) + ctx.flow.refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面) if not refreshed: return if _curio_context == "reward_node": var st: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state() - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array)) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array)) _feed_refresh_display(adventure) func on_pick_confirmed(item_id: String) -> void: - var adventure = _adventure_get.call() + var adventure = ctx.getters.adventure_get.call() if adventure == null: return # TP0 令牌池——卡片 id "kind|tier|feat_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段) @@ -218,38 +190,38 @@ func on_pick_confirmed(item_id: String) -> void: # 后续兑付/确认仍走节点路径回图选路) if _curio_context == "reward_node" and kind == "feat_ticket": var st2: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state() - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array)) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array)) # 兑付反馈(curio_resolved 到达——结果行上屏,唯一出口 = 继续) func on_resolved(result: Dictionary) -> void: _pending_curio = {} - curio_overlay.show_result(Narrator.bonus_result_text(result)) - _refresh_presentations.call() + ctx.views.curio_overlay.show_result(Narrator.bonus_result_text(result)) + ctx.flow.refresh_presentations.call() # 继续钮:收三选一面 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进) func on_continue() -> void: - _close_modal.call(curio_overlay) - curio_overlay.close_pick() - var adventure = _adventure_get.call() + ctx.flow.close_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.close_pick() + var adventure = ctx.getters.adventure_get.call() if _curio_context == "feat_ticket": return # T5 店内专长凭证面——专长兑付完成后关面回店(无推进/无回图) if _curio_context == "reward_node": - var runner = _runner_get.call() + var runner = ctx.getters.runner_get.call() if runner != null: runner.continue_after_node() return - if bool(_adventure_mode_get.call()) and adventure != null: + if bool(ctx.getters.adventure_mode_get.call()) and adventure != null: if not adventure.continue_to_next_level(): - _fatal_screen.call("关卡推进失败——见输出日志") + ctx.flow.fatal_screen.call("关卡推进失败——见输出日志") # 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator); # 数据源=treasure 装配词表 12 件 func item_name_lookup() -> Dictionary: var lookup := {} - var meta = _meta_get.call() + var meta = ctx.getters.meta_get.call() if meta != null: for row in db.crossrun.treasure_items.attunement_rows(): lookup[String(row.get("item_id", ""))] = String(row.get("display_name", "")) diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index 09e8219c..e6ae0aa6 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -11,8 +11,13 @@ const Narrator := preload("res://presentation/battle_narrator.gd") # 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配) # 钱包 GP/钥石不在本装配面投喂(中/大循环资源迁出战斗屏——信息归属单一展示面原则); # HP/状态效果单投喂 B 区站桩(B/C 双投喂属重复展示——单一展示面原则) -static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, - settlement, runner, level_active: bool, db, refresh_remaining: int) -> void: +# 运行态尾参收拢 ctx 束(u19——键 {settlement, runner, level_active, db, refresh_remaining}) +static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, ctx: Dictionary) -> void: + var settlement = ctx.get("settlement") + var runner = ctx.get("runner") + var level_active: bool = ctx.get("level_active", false) + var db = ctx.get("db") + var refresh_remaining: int = ctx.get("refresh_remaining", 0) # 特性列表(runner 统一加算区投影:商店购买与关卡大奖励 # 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。 # 呈现只读投影零核心触碰;行文格式=白名单项) @@ -36,7 +41,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB info.set_feat_holdings([]) if settlement == null: info.set_board_variants([]) # 战斗域角标出战斗即隐(空态纪律) - _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) + _refresh_out_of_battle(stage, info, ap_bar, ctx) return var s: Dictionary[Variant, Variant] = settlement.presentation_snapshot() # 地形变异常驻角标投影(整图变异快照 → 文案行; @@ -44,7 +49,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB info.set_board_variants(board_variant_lines(db, s.get("board_variants", []))) var hero: Dictionary[Variant, Variant] = s.hero if hero.is_empty() or not bool(hero.get("alive", false)): - _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) + _refresh_out_of_battle(stage, info, ap_bar, ctx) return _refresh_hero(stage, info, s) _refresh_economy(ap_bar, s, db, refresh_remaining) @@ -92,7 +97,7 @@ static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, ref # 无 archetype 字段,聚合形态会空键兜底实显「流派 ×N · 档一」僵尸文案;流派档位非现行机制) static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]: var rlines: Array[String] = [] - if db == null or db.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty(): + if db == null or db.crossrun.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty(): return rlines var counts := {} for rid in held: @@ -109,7 +114,7 @@ static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]: # 文案用常规表述,不用俚语 static func board_variant_lines(db: ConfigDb, ids: Array) -> Array[String]: var lines: Array[String] = [] - if db == null or db.variation == null: + if db == null or db.combat.variation == null: return lines for vid in ids: var row: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid)) @@ -145,7 +150,10 @@ static func sync_settlement_visuals(stage: BattleStageView, board_view: BoardVie # 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值 static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, - ap_bar: ApBarView, runner, level_active: bool, db) -> void: + ap_bar: ApBarView, ctx: Dictionary) -> void: + var runner = ctx.get("runner") + var level_active: bool = ctx.get("level_active", false) + var db = ctx.get("db") if not level_active: return if runner != null: @@ -185,7 +193,14 @@ static func outcome_text(outcome: Dictionary) -> String: # ---- 批量模拟报告装配(文案归 presenter——视图只 set_text) ---- -static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String: +## 统计束收拢(u19——stats 六键 {wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes}) +static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, stats: Dictionary, base_hp: int) -> String: + var wins: int = stats.get("wins", 0) + var dmg_all: Array = stats.get("dmg_all", []) + var hp_left_win: Array = stats.get("hp_left_win", []) + var hp_boss: Array = stats.get("hp_boss", []) + var rounds_all: Array = stats.get("rounds_all", []) + var death_nodes: Dictionary = stats.get("death_nodes", {}) var lines: Array[String] = [] lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct]) lines.append("关卡完成率 [color=#%s]%.0f%%[/color](%d/%d)" % [UiKit.HEX_LOG_WIN, 100.0 * float(wins) / float(maxi(1, n)), wins, n]) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index fb8b14f5..50ba97ef 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -360,7 +360,7 @@ func _wire_adventure() -> void: # meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects); # hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。 # 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照; -# 取值=meta.hero_attack_type/hero_armor_type=存档选中路线类型轴,未解锁/异常选中态回退锻造配置默认值)。 +# 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。 # 注:runner 侧 resume_adventure 受理面收 attunement/attune_refresh_y/tree_rules 三键 # (传参/受理两侧对齐)。 static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary: @@ -453,15 +453,11 @@ func _on_battle_finished(payload: Dictionary) -> void: return "level_end": _end_overlay.open_victory(Narrator.battle_end_text("victory", true), - "返回", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner), - int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) + "返回", board_view.get_global_rect(), _end_victory_opts(payload)) "standalone_end": if result == "victory": _end_overlay.open_victory(Narrator.battle_end_text("victory", false), - "回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner), - int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) + "回到遭遇选择", board_view.get_global_rect(), _end_victory_opts(payload)) else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) @@ -482,6 +478,14 @@ func _gp_summary_text(payload: Dictionary) -> String: # ==================== core↔view 接线(战斗信号统一走这里)==================== + +## 战末账行束(open_victory opts 键装配单点——GP/钥石/加成三行与增量合计) +func _end_victory_opts(payload: Dictionary) -> Dictionary: + return {"gp_summary": _gp_summary_text(payload), + "ks_summary": "钥石 · 累计 %d" % run_scope.runner.keystone, + "bonus_summary": HudPresenter.bonus_summary_text(run_scope.runner), + "gp_gain": int(payload.get("gp_dropped", 0)), "gp_total": int(run_scope.runner.gp)} + func _wire_battle(settlement_ref: BattleOrchestrator) -> void: settlement_ref.step_row.connect(_on_step_row) settlement_ref.battle_finished.connect(_on_battle_finished) @@ -812,7 +816,7 @@ func _refresh_used_snapshot() -> int: func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot()) - HudPresenter.refresh(stage, info_panel, ap_bar, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining) + HudPresenter.refresh(stage, info_panel, ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining}) _treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗) _sync_settlement_visuals() diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index f05b8c18..66cbe3c2 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -111,7 +111,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup()) if result == "completed": root._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(), - "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 + {"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 else: root._end_overlay.open_defeat(text, "返回", root.board_view.get_global_rect()) root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) @@ -178,7 +178,7 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void: root.run_scope.pending_candidates = candidates.duplicate() root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点", root.board_view.get_global_rect(), - "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]) + {"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]}) root._open_modal_after_playback(root._end_overlay) root._refresh_presentations() return diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index badcc2bd..52f206c5 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -134,11 +134,17 @@ static func _build_curio(root: Variant) -> void: root.add_child(root.curio_overlay) # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) - root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, func() -> Variant: return root.meta, root.curio_overlay, root._end_overlay, root.board_view, - root.board_zone.toast, func() -> Variant: return root.save_manager, func() -> Variant: return root.run_scope.adventure, - func() -> Variant: return root.run_scope.runner, func() -> Variant: return root.run_scope.is_adventure, - root._open_modal, root._open_modal_after_playback, root._close_modal, root._refresh_presentations, - root._fatal_screen, func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}))) # 构造参数收拢 context + root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, + {"curio_overlay": root.curio_overlay, "end_overlay": root._end_overlay, + "board_view": root.board_view, "toast": root.board_zone.toast}, + {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, + "adventure_get": func() -> Variant: return root.run_scope.adventure, + "runner_get": func() -> Variant: return root.run_scope.runner, + "adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure}, + {"open_modal": root._open_modal, "open_modal_after_playback": root._open_modal_after_playback, + "close_modal": root._close_modal, "refresh_presentations": root._refresh_presentations, + "fatal_screen": root._fatal_screen, + "gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束) root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) root.curio_overlay.continue_pressed.connect(root._bonus.on_continue) root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) @@ -154,9 +160,13 @@ static func _build_menu_wiring(root: Variant) -> void: root.debug_badge.pressed.connect(root._on_debug_badge_pressed) # 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) - root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, func() -> Variant: return root.meta, func() -> Variant: return root.save_manager, root.camp_overlay, root.shop_overlay, - root.board_zone.toast, root._start_overlay, root.logger, func() -> Variant: return root.run_scope.runner, - root._open_modal, root._close_modal, root._refresh_presentations)) # 构造参数收拢 context + root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, + {"camp_overlay": root.camp_overlay, "shop_overlay": root.shop_overlay, + "toast": root.board_zone.toast, "start_overlay": root._start_overlay, "logger": root.logger}, + {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, + "runner_get": func() -> Variant: return root.run_scope.runner}, + {"open_modal": root._open_modal, "close_modal": root._close_modal, + "refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束) root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) @@ -241,3 +251,8 @@ static func fatal_screen(root: Variant, msg: String) -> void: var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT) l.set_anchors_preset(Control.PRESET_CENTER) root.add_child(l) + + +# GP 总结行回调(gp_summary 束键——单行 lambda 内嵌字典实参收尾歧义,提函数承载) +static func _gp_summary_cb(root: Variant) -> Callable: + return func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}) diff --git a/game/presentation/playtest_logger.gd b/game/presentation/playtest_logger.gd index 1d8dce84..4182051f 100644 --- a/game/presentation/playtest_logger.gd +++ b/game/presentation/playtest_logger.gd @@ -157,8 +157,6 @@ func dump_diagnosis(context: Dictionary) -> String: if f != null: f.store_string(JSON.stringify(dump, " ")) f.close() - print("[playtest_logger] 诊断转储已写入: %s(项目日志目录见输出)" % path) - print(" [diag] %s" % JSON.stringify(context)) return path diff --git a/game/presentation/tune_sim_controller.gd b/game/presentation/tune_sim_controller.gd index bf089c07..5c998019 100644 --- a/game/presentation/tune_sim_controller.gd +++ b/game/presentation/tune_sim_controller.gd @@ -39,32 +39,30 @@ func run(n: int) -> void: # 一文件多局,run 行携带种子切分局次) var logger = PlaytestLogger.new() logger.ap_per_round = int(db.run.action_economy.ap_per_round) - var log_path: String = logger.start_session(SEED_BASE) - print("[tune_sim] 模拟日志: %s" % log_path) + logger.start_session(SEED_BASE) panel.set_sim_running(true) panel.set_sim_progress("模拟中:0/%d …" % n) - var wins := 0 - var dmg_all: Array[float] = [] - var hp_left_win: Array[float] = [] - var hp_boss: Array[float] = [] - var rounds_all: Array[int] = [] - var death_nodes := {} + var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}} var autoplay = _autoplay() for i in range(n): - var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) - dmg_all.append(st.hero_dmg) - rounds_all.append(st.rounds) - if st.result == "victory": - wins += 1 - hp_left_win.append(st.hp_left) - hp_boss.append(st.hp_at_boss) - elif st.result == "defeat": - var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type] - death_nodes[key] = int(death_nodes.get(key, 0)) + 1 + _run_one_level(autoplay, stats, i, panel, logger) panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n]) await panel.get_tree().process_frame logger.close_session() - panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), - wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes, _base_hp)) + panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp)) + + +## 单局模拟域(循环体提函数——结果四桶记账;u19 stats 六键同构承载) +func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: + var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) + (stats["dmg_all"] as Array).append(st.hero_dmg) + (stats["rounds_all"] as Array).append(st.rounds) + if st.result == "victory": + stats["wins"] = int(stats["wins"]) + 1 + (stats["hp_left_win"] as Array).append(st.hp_left) + (stats["hp_boss"] as Array).append(st.hp_at_boss) + elif st.result == "defeat": + var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type] + stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1 panel.set_sim_running(false) _running = false diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index 4c815653..e0d6909d 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -101,31 +101,11 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar var bd := UiKit.backdrop_tile() if bd != null: overlay.add_child(bd) - overlay.add_child(UiKit.beams_rect()) + overlay.add_child(UiKit._beams_rect()) overlay.add_child(UiKit.vignette_rect()) # 双列骨架:左=操作列 56% / 右=舞台(立绘看台) - var cols := HBoxContainer.new() - cols.set_anchors_preset(Control.PRESET_FULL_RECT) - cols.add_theme_constant_override("separation", 32) - var pad := MarginContainer.new() - pad.set_anchors_preset(Control.PRESET_FULL_RECT) - for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]: - pad.add_theme_constant_override(side, 56) - cols.add_child(pad) - overlay.add_child(cols) - var box := VBoxContainer.new() - box.name = "start_box" - box.size_flags_horizontal = Control.SIZE_EXPAND_FILL - box.size_flags_stretch_ratio = 1.4 - box.add_theme_constant_override("separation", 10) - pad.add_child(box) - - var title := UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP) - box.add_child(title) - box.add_child(HSeparator.new()) - - # 三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1:进入存档/设置/退出游戏; - # 组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕) + var cols := _start_columns(overlay) + var box := _start_left_column(cols) var entersavebtn := UiKit.primary_button("进入存档") # 主操作钮(唯一进度入口) entersavebtn.tooltip_text = "选择一个存档位进入(新档 / 进档 / 删档)" box.add_child(entersavebtn) @@ -135,12 +115,45 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar # 退出游戏与设置同层并行——只在一级菜单出现(行为零变化——契约 2) var quitbtn := UiKit.button("退出游戏") box.add_child(quitbtn) - # 调试入口收浮层角标;boot 菜单优先不开 # 浮窗,调试入口=UI 最顶层全局浮层角标(build_debug_badge——LAYER_CONTRACT 末位、 # ModalStack 登记外,任何 UI 不遮挡;本层不设屏内调试角标,全局浮层全场景承载) + _start_right_column(cols) + return {"overlay": overlay, "box": box, + "entersavebtn": entersavebtn, + "settingsbtn": settingsbtn, "quitbtn": quitbtn} - # 右列:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌) + +## 双列骨架域:左=操作列 56% / 右=舞台(立绘看台)——全屏锚+56px 四边衬距 +static func _start_columns(overlay: Control) -> HBoxContainer: + var cols := HBoxContainer.new() + cols.set_anchors_preset(Control.PRESET_FULL_RECT) + cols.add_theme_constant_override("separation", 32) + var pad := MarginContainer.new() + pad.set_anchors_preset(Control.PRESET_FULL_RECT) + for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]: + pad.add_theme_constant_override(side, 56) + cols.add_child(pad) + overlay.add_child(cols) + return cols + + +## 左操作列域:标题+三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1: +## 进入存档/设置/退出游戏;组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕) +static func _start_left_column(cols: HBoxContainer) -> VBoxContainer: + var box := VBoxContainer.new() + box.name = "start_box" + box.size_flags_horizontal = Control.SIZE_EXPAND_FILL + box.size_flags_stretch_ratio = 1.4 + box.add_theme_constant_override("separation", 10) + (cols.get_child(0) as MarginContainer).add_child(box) + box.add_child(UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP)) + box.add_child(HSeparator.new()) + return box + + +## 右列域:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌) +static func _start_right_column(cols: HBoxContainer) -> void: var right := VBoxContainer.new() right.name = "start_right" right.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -153,17 +166,7 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar foe_row.alignment = BoxContainer.ALIGNMENT_CENTER foe_row.add_theme_constant_override("separation", 36) for cfg in [[UiKit.TEX_ENEMY_GOBLIN, 46, 96], [UiKit.TEX_HERO_PORTRAIT, 138, 144], [UiKit.TEX_ENEMY_OGRE_CHIEF, 80, 96]]: - var tr := TextureRect.new() - var tex := UiKit.load_tex(cfg[0]) - if tex != null: - tr.texture = tex - tr.custom_minimum_size = Vector2(cfg[1], cfg[2]) - tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - if cfg[0] != UiKit.TEX_HERO_PORTRAIT: - tr.modulate = Color(0.7, 0.7, 0.7, 0.4) # 敌影剪影退后(§6.1 立绘>背景) - foe_row.add_child(tr) + foe_row.add_child(_start_portrait(cfg)) stage.add_child(foe_row) var pedestal := Panel.new() var ped_sb := UiKit.panel_flat(UiKit.TITLE_PEDESTAL, UiKit.BG_ROOT, 17, 2) @@ -173,9 +176,20 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar stage.add_child(pedestal) right.add_child(stage) - return {"overlay": overlay, "box": box, - "entersavebtn": entersavebtn, - "settingsbtn": settingsbtn, "quitbtn": quitbtn} + +## 单立绘域(循环体提函数——像素过滤居中;敌影剪影退后 §6.1 立绘>背景) +static func _start_portrait(cfg: Array) -> TextureRect: + var tr := TextureRect.new() + var tex := UiKit.load_tex(cfg[0]) + if tex != null: + tr.texture = tex + tr.custom_minimum_size = Vector2(cfg[1], cfg[2]) + tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + if cfg[0] != UiKit.TEX_HERO_PORTRAIT: + tr.modulate = Color(0.7, 0.7, 0.7, 0.4) + return tr # 调试浮层入口角标(UI 最顶层浮层,任何 UI 不遮挡)——main 直接子层、 diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index 254d88ce..f80a4d6d 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -41,6 +41,16 @@ func _init() -> void: row.add_theme_constant_override("separation", 8) add_child(row) + _build_ap_face(row) + _build_rap_face(row) + _build_refresh_confirm(row) + _build_legend_refresh(row) + + +# ---- 构建私有族(AP 计量/RAP 读数/破坏性确认/图例刷新四域) ---- + +# AP 计量域:点阵+读数+停手钮+弹性撑 +func _build_ap_face(row: HBoxContainer) -> void: _dots = HBoxContainer.new() _dots.add_theme_constant_override("separation", 3) _dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER @@ -48,19 +58,19 @@ func _init() -> void: _ap_label = UiKit.label("AP 0/0",UiKit.FS_BODY, true) _ap_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(_ap_label) - _btn_stop = UiKit.button("停手") _btn_stop.custom_minimum_size = Vector2(64, 0) _btn_stop.size_flags_vertical = Control.SIZE_SHRINK_CENTER _btn_stop.pressed.connect(_emit_stop_pressed) row.add_child(_btn_stop) - var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) - # RAP 读数(自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮 - # (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则) + +# RAP 读数域(自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮 +# (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则) +func _build_rap_face(row: HBoxContainer) -> void: _rap_dots = HBoxContainer.new() _rap_dots.add_theme_constant_override("separation", 2) _rap_dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER @@ -72,7 +82,9 @@ func _init() -> void: _rap_feedback.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(_rap_feedback) - # 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位 + +# 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位 +func _build_refresh_confirm(row: HBoxContainer) -> void: _confirm_row = HBoxContainer.new() _confirm_row.add_theme_constant_override("separation", 4) _confirm_row.visible = false @@ -83,12 +95,14 @@ func _init() -> void: confirm_btn.pressed.connect(_disarm_and_fire) _confirm_row.add_child(confirm_btn) + +# 图例与弃图刷新域:图例开关钮+刷新钮组(副行=剩余次数读数) +func _build_legend_refresh(row: HBoxContainer) -> void: _btn_legend = UiKit.button("图例") _btn_legend.custom_minimum_size = Vector2(56, 0) _btn_legend.size_flags_vertical = Control.SIZE_SHRINK_CENTER _btn_legend.pressed.connect(_emit_legend_toggled) row.add_child(_btn_legend) - var refresh_box := VBoxContainer.new() refresh_box.add_theme_constant_override("separation", 0) refresh_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER diff --git a/game/presentation/views/battle_end_overlay.gd b/game/presentation/views/battle_end_overlay.gd index bfb40e2e..d5340755 100644 --- a/game/presentation/views/battle_end_overlay.gd +++ b/game/presentation/views/battle_end_overlay.gd @@ -10,22 +10,30 @@ const COL_GOLD := UiKit.GOLD const COL_DUSK := UiKit.DUSK # 暮蓝(全作唯一暮蓝档) const COL_WARM := UiKit.WARM -var _dim: ColorRect -var _seal: Panel -var _verdict: Label -var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏) -var _gp_row: HBoxContainer -var _gp_name: Label -var _gp_val: Label -var _ks_row: HBoxContainer -var _ks_name: Label -var _ks_val: Label -var _bonus_row_l: HBoxContainer -var _bonus_name: Label -var _bonus_val: Label -var _cele: TextureRect # 庆祝立绘(idle 占位浮动) -var _btn: Button -var _board_rect := Rect2() +## 内部状态件(u6 子域拆件——契约 9 职责语句「战末呈现与演出态」):单一变化原因=终局画面呈现与 +## 演出计数。探针直读冻结成员(_btn/_gp_name/_gp_row/_gp_val)住宿主不进状态件。 +class _EndOverlayState extends RefCounted: + var _dim: ColorRect + var _seal: Panel + var _verdict: Label + var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏) + var _ks_row: HBoxContainer + var _ks_name: Label + var _ks_val: Label + var _bonus_row_l: HBoxContainer + var _bonus_name: Label + var _bonus_val: Label + var _cele: TextureRect # 庆祝立绘(idle 占位浮动) + var _board_rect := Rect2() + var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) + var _fx_counts := {"crest": 0, "dust": 0, "seal": 0} # 演出计数(fx_probe 断言面——crest/dust/seal 三键) + + +var st := _EndOverlayState.new() +var _gp_row: HBoxContainer # 探针直读冻结(S17⑤) +var _gp_name: Label # 探针直读冻结 +var _gp_val: Label # 探针直读冻结 +var _btn: Button # 探针直读冻结 func _init() -> void: @@ -33,21 +41,45 @@ func _init() -> void: #(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与 # 按钮全部上板;金蜡封 = 面板头部静态徽记(坠落演出落点被面板遮挡)。 # 骨架经 UiKit.modal_shell;_dim 引用摘出供 open_* 换氛围色。 - var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, true, + var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, UiKit.panel_skinned(Vector4(16, 14, 16, 14))) - _dim = shell.dim + st._dim = shell.dim var box: VBoxContainer = shell.box + _build_header(box) + _build_ledger(box) + # 无明细回看钮——冒险中回看归「生涯」 + _btn = UiKit.primary_button("继续") + _btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER # 紧凑钮 + _btn.custom_minimum_size.x = 150 + _btn.pressed.connect(_emit_continue_pressed) + box.add_child(_btn) + visible = false + + +## 头部域:金蜡封徽记+判定行+庆祝立绘(demo ⑧——idle 帧浮动占位;减动效=静帧) +func _build_header(box: VBoxContainer) -> void: var header := HBoxContainer.new() header.add_theme_constant_override("separation", 12) header.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(header) - _seal = Panel.new() - _seal.mouse_filter = Control.MOUSE_FILTER_IGNORE + st._seal = Panel.new() + st._seal.mouse_filter = Control.MOUSE_FILTER_IGNORE var sb := UiKit.panel_flat(COL_GOLD, UiKit.GOLD_DEEP, 200, 4) - _seal.add_theme_stylebox_override("panel", sb) - _seal.custom_minimum_size = Vector2(52, 52) - _seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER - header.add_child(_seal) + st._seal.add_theme_stylebox_override("panel", sb) + st._seal.custom_minimum_size = Vector2(52, 52) + st._seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER + header.add_child(st._seal) + st._seal.add_child(_seal_img()) + st._verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE) + st._verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + st._verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER + header.add_child(st._verdict) + st._cele = _cele_rect() + header.add_child(st._cele) + + +## 蜡封图域(32px 像素过滤居中) +func _seal_img() -> TextureRect: var seal_img := TextureRect.new() var seal_tex := UiKit.load_tex("res://assets/art/board/seal_mark.png") if seal_tex != null: @@ -58,76 +90,76 @@ func _init() -> void: seal_img.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST seal_img.size_flags_vertical = Control.SIZE_SHRINK_CENTER seal_img.mouse_filter = Control.MOUSE_FILTER_IGNORE - _seal.add_child(seal_img) - _verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE) - _verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - _verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER - header.add_child(_verdict) - # 庆祝立绘(demo ⑧——idle 帧浮动占位;减动效=静帧) - _cele = TextureRect.new() + return seal_img + + +## 庆祝立绘域(64px idle 帧) +func _cele_rect() -> TextureRect: + var cele := TextureRect.new() var cele_tex := UiKit.load_tex(UiKit.TEX_HERO_IDLE) if cele_tex != null: - _cele.texture = cele_tex - _cele.custom_minimum_size = Vector2(64, 64) - _cele.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - _cele.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - _cele.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - _cele.size_flags_vertical = Control.SIZE_SHRINK_CENTER - _cele.visible = false - header.add_child(_cele) - _rows_box = VBoxContainer.new() - _rows_box.add_theme_constant_override("separation", 6) - _rows_box.visible = false - box.add_child(_rows_box) - # 两列对齐账行(审计差距④对齐 demo ⑧——项目名左/数值右,定宽 360) + cele.texture = cele_tex + cele.custom_minimum_size = Vector2(64, 64) + cele.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + cele.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + cele.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + cele.size_flags_vertical = Control.SIZE_SHRINK_CENTER + cele.visible = false + return cele + + +## 三行账域(两列对齐账行——审计差距④对齐 demo ⑧:项目名左/数值右,定宽 360) +func _build_ledger(box: VBoxContainer) -> void: + st._rows_box = VBoxContainer.new() + st._rows_box.add_theme_constant_override("separation", 6) + st._rows_box.visible = false + box.add_child(st._rows_box) _gp_row = _ledger_row() _gp_name = _ledger_name(UiKit.FS_SUB, UiKit.GOLD_INK, true) # GOLD_DEEP 对结算板 2.67:1 越带,换深金墨 5.21:1 _gp_val = _ledger_value(UiKit.FS_SUB, UiKit.GOLD_INK, true) # 同上 _gp_row.add_child(_gp_name) _gp_row.add_child(_gp_val) - _rows_box.add_child(_gp_row) - _ks_row = _ledger_row() - _ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false) - _ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true) - _ks_row.add_child(_ks_name) - _ks_row.add_child(_ks_val) - _ks_row.visible = false - _rows_box.add_child(_ks_row) - _bonus_row_l = _ledger_row() - _bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false) - _bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true) - _bonus_row_l.add_child(_bonus_name) - _bonus_row_l.add_child(_bonus_val) - _bonus_row_l.visible = false - _rows_box.add_child(_bonus_row_l) + st._rows_box.add_child(_gp_row) + st._ks_row = _ledger_row() + st._ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false) + st._ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true) + st._ks_row.add_child(st._ks_name) + st._ks_row.add_child(st._ks_val) + st._ks_row.visible = false + st._rows_box.add_child(st._ks_row) + st._bonus_row_l = _ledger_row() + st._bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false) + st._bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true) + st._bonus_row_l.add_child(st._bonus_name) + st._bonus_row_l.add_child(st._bonus_val) + st._bonus_row_l.visible = false + st._rows_box.add_child(st._bonus_row_l) box.add_child(HSeparator.new()) - # 无明细回看钮——冒险中回看归「生涯」 - _btn = UiKit.primary_button("继续") - _btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER # 紧凑钮 - _btn.custom_minimum_size.x = 150 - _btn.pressed.connect(_emit_continue_pressed) - box.add_child(_btn) - visible = false # 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入。 # gp_summary(胜利总结结算展示 GP 总量+增量):组合根装配「战利品 +增量 · 合计总量」 # 传入(入账次序 = LevelRunner 入账先于组合根 handler,runner.gp 即战后总量);空串 = 不显示(失败侧恒空)。 -func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, gp_summary: String = "", - ks_summary: String = "", bonus_summary: String = "", gp_gain := -1, gp_total := -1) -> void: - _dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo - _verdict.text = verdict_line - _seal.visible = true # 胜利徽记(面板头部静态) - _seal.modulate = Color.WHITE +# 可选尾参收拢 opts 束(u6——键 {gp_summary, ks_summary, bonus_summary, gp_gain, gp_total},缺省逐字同值) +func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, opts := {}) -> void: + var gp_summary: String = opts.get("gp_summary", "") + var ks_summary: String = opts.get("ks_summary", "") + var bonus_summary: String = opts.get("bonus_summary", "") + var gp_gain: int = opts.get("gp_gain", -1) + var gp_total: int = opts.get("gp_total", -1) + st._dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo + st._verdict.text = verdict_line + st._seal.visible = true # 胜利徽记(面板头部静态) + st._seal.modulate = Color.WHITE _set_ledger_row(_gp_name, _gp_val, gp_summary, true) # GP 行=固定名「战利品 GP」(滚动/整串均只入值列) - _set_ledger_row(_ks_name, _ks_val, ks_summary) - _ks_row.visible = ks_summary != "" - _set_ledger_row(_bonus_name, _bonus_val, bonus_summary) - _bonus_row_l.visible = bonus_summary != "" - _rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != "" + _set_ledger_row(st._ks_name, st._ks_val, ks_summary) + st._ks_row.visible = ks_summary != "" + _set_ledger_row(st._bonus_name, st._bonus_val, bonus_summary) + st._bonus_row_l.visible = bonus_summary != "" + st._rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != "" _btn.text = continue_label - _board_rect = board_rect - _cele.visible = true + st._board_rect = board_rect + st._cele.visible = true _play_celebrate() visible = true if UiKit.victory_fx_style == "dust": @@ -141,23 +173,23 @@ func open_victory(verdict_line: String, continue_label: String, board_rect: Rect func open_defeat(verdict_line: String, continue_label: String, board_rect: Rect2) -> void: # 明度不变的暮蓝色相氛围(全屏唯一暮蓝档——§2.5 最静一拍) - _dim.color = Color(COL_DUSK, 0.6) # 同族压暗 - _verdict.text = verdict_line - _board_rect = board_rect + st._dim.color = Color(COL_DUSK, 0.6) # 同族压暗 + st._verdict.text = verdict_line + st._board_rect = board_rect visible = true - _seal.visible = true - _seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧) - _cele.visible = false - _rows_box.visible = false # 失败侧不呈现账行 + st._seal.visible = true + st._seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧) + st._cele.visible = false + st._rows_box.visible = false # 失败侧不呈现账行 _play_dust_sadle() # 坐砖掸灰(§2.5——终稿) _play_seal_recall(8) func close() -> void: visible = false - _seal.visible = false - _seal.modulate = Color.WHITE - _cele.visible = false + st._seal.visible = false + st._seal.modulate = Color.WHITE + st._cele.visible = false var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀) if tw != null and tw.is_valid(): tw.kill() @@ -170,7 +202,7 @@ func is_open() -> bool: func verdict_text() -> String: - return _verdict.text + return st._verdict.text # 两列账行工厂(审计差距④——名左/值右,定宽 360) @@ -217,68 +249,78 @@ func _fx_center() -> Vector2: # 封印化光点收回(失败——插过的封印逐面化光点收回占位,§2.5) func _play_seal_recall(count: int) -> void: - _fx_seal_dots = count + st._fx_counts["seal"] = count + var base := st._board_rect.get_center() if st._board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5 for i in count: - var dot := ColorRect.new() - dot.color = COL_GOLD - dot.size = Vector2(6, 6) - var base := _board_rect.get_center() if _board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5 - dot.position = base + Vector2((float(i % 5) - 2.0) * _board_rect.size.x * 0.14 if _board_rect.size.x > 0 else 0.0, - (float(i % 3) - 1.0) * _board_rect.size.y * 0.18) # 确定性散布(S2 禁全局随机——占位演出同守) - dot.mouse_filter = Control.MOUSE_FILTER_IGNORE - add_child(dot) - var tw := dot.create_tween() - tw.tween_interval(UiKit.motion(0.1 * i)) - tw.tween_property(dot, "position:y", dot.position.y - 120.0, UiKit.motion(0.8)) - tw.parallel().tween_property(dot, "modulate:a", 0.0, UiKit.motion(0.8)) - tw.tween_callback(dot.queue_free) + _spawn_seal_dot(i, base) -# ---- 演出密度(程序化 tween,确定性散布,零新帧资产) ---- +## 单光点域(循环体提函数——延迟升空渐隐回收;确定性散布 S2 禁全局随机) +func _spawn_seal_dot(i: int, base: Vector2) -> void: + var dot := ColorRect.new() + dot.color = COL_GOLD + dot.size = Vector2(6, 6) + dot.position = base + Vector2((float(i % 5) - 2.0) * st._board_rect.size.x * 0.14 if st._board_rect.size.x > 0 else 0.0, + (float(i % 3) - 1.0) * st._board_rect.size.y * 0.18) + dot.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(dot) + var tw := dot.create_tween() + tw.tween_interval(UiKit.motion(0.1 * i)) + tw.tween_property(dot, "position:y", dot.position.y - 120.0, UiKit.motion(0.8)) + tw.parallel().tween_property(dot, "modulate:a", 0.0, UiKit.motion(0.8)) + tw.tween_callback(dot.queue_free) -var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) -var _fx_crest_shards := 0 -var _fx_dust_motes := 0 -var _fx_seal_dots := 0 + +# ---- 演出密度(程序化 tween,确定性散布,零新帧资产;计数与碎砖驻留面在 _EndOverlayState) ---- # 碎砖拼纹章(胜利 §2.4——蜡封落定后十枚碎砖自四周向蜡封外环收拢拼成纹章环,驻留至 close) func _play_crest_assemble() -> void: _clear_crest_bricks() - _fx_crest_shards = 10 + st._fx_counts["crest"] = 10 var c := _fx_center() for i in 10: - var brick := ColorRect.new() - brick.color = UiKit.BRICK if i % 2 == 0 else UiKit.FLOOR # 碎砖=砖面族色(掩尘/裸土——图底同源) - brick.size = Vector2(12, 7) - brick.mouse_filter = Control.MOUSE_FILTER_IGNORE - var ang := TAU * float(i) / 10.0 - var ring := c + Vector2(cos(ang), sin(ang)) * 64.0 - brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场 - add_child(brick) - _crest_bricks.append(brick) - var btw := brick.create_tween() - btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段) - btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - btw.parallel().tween_property(brick, "rotation", sin(ang) * 0.35, UiKit.motion(0.4)) + _spawn_crest_brick(i, c) + + +## 单碎砖域(循环体提函数——待蜡封落定后自场外收拢入环;碎砖=砖面族色〔掩尘/裸土——图底同源〕) +func _spawn_crest_brick(i: int, c: Vector2) -> void: + var brick := ColorRect.new() + brick.color = UiKit.BRICK if i % 2 == 0 else UiKit.FLOOR + brick.size = Vector2(12, 7) + brick.mouse_filter = Control.MOUSE_FILTER_IGNORE + var ang := TAU * float(i) / 10.0 + var ring := c + Vector2(cos(ang), sin(ang)) * 64.0 + brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场 + add_child(brick) + st._crest_bricks.append(brick) + var btw := brick.create_tween() + btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段) + btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + btw.parallel().tween_property(brick, "rotation", sin(ang) * 0.35, UiKit.motion(0.4)) # 坐砖掸灰(失败 §2.5——暖灰尘絮自棋盘中带缓缓飘落,最静一拍的运动承载) func _play_dust_sadle() -> void: - _fx_dust_motes = 8 + st._fx_counts["dust"] = 8 var base := _fx_center() for i in 8: - var mote := ColorRect.new() - mote.color = Color(UiKit.WARM_FAINT, 0.85) - mote.size = Vector2(4, 4) - mote.mouse_filter = Control.MOUSE_FILTER_IGNORE - mote.position = base + Vector2((float(i % 4) - 1.5) * 30.0, -24.0 - 8.0 * float(i / 4)) - add_child(mote) - var mtw := mote.create_tween() - mtw.tween_interval(UiKit.motion(0.08 * i)) - mtw.tween_property(mote, "position:y", mote.position.y + 64.0, UiKit.motion(0.9)) - mtw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(0.9)) - mtw.tween_callback(mote.queue_free) + _spawn_dust_mote(i, base) + + +## 单尘絮域(循环体提函数——中带散布缓降渐隐) +func _spawn_dust_mote(i: int, base: Vector2) -> void: + var mote := ColorRect.new() + mote.color = Color(UiKit.WARM_FAINT, 0.85) + mote.size = Vector2(4, 4) + mote.mouse_filter = Control.MOUSE_FILTER_IGNORE + mote.position = base + Vector2((float(i % 4) - 1.5) * 30.0, -24.0 - 8.0 * float(i / 4)) + add_child(mote) + var mtw := mote.create_tween() + mtw.tween_interval(UiKit.motion(0.08 * i)) + mtw.tween_property(mote, "position:y", mote.position.y + 64.0, UiKit.motion(0.9)) + mtw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(0.9)) + mtw.tween_callback(mote.queue_free) # 庆祝立绘浮动(idle 占位——UiKit.motion 守减动效;motion_scale<1 静帧) @@ -288,29 +330,34 @@ func _play_celebrate() -> void: var prev: Tween = get_meta(&"celebrate_tw", null) if prev != null and prev.is_valid(): prev.kill() - var tw := _cele.create_tween() + var tw := st._cele.create_tween() set_meta(&"celebrate_tw", tw) tw.set_loops() - tw.tween_property(_cele, "position:y", _cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - tw.tween_property(_cele, "position:y", _cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tw.tween_property(st._cele, "position:y", st._cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tw.tween_property(st._cele, "position:y", st._cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) # 尘金粒子(备选演出——金屑自面板上带飘落闪灭;确定性散布守 S2) func _play_victory_dust() -> void: - _fx_dust_motes += 10 + st._fx_counts["dust"] += 10 var c := _fx_center() for i in 10: - var mote := ColorRect.new() - mote.color = UiKit.CANDLE - mote.size = Vector2(5, 5) - mote.mouse_filter = Control.MOUSE_FILTER_IGNORE - mote.position = c + Vector2((float(i % 5) - 2.0) * 44.0, -40.0 - 10.0 * float(i / 5)) - add_child(mote) - var tw := mote.create_tween() - tw.tween_interval(UiKit.motion(0.12 * i)) - tw.tween_property(mote, "position:y", mote.position.y + 90.0, UiKit.motion(1.1)) - tw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(1.1)) - tw.tween_callback(mote.queue_free) + _spawn_victory_mote(i, c) + + +## 单金屑域(循环体提函数——上带散布飘落闪灭) +func _spawn_victory_mote(i: int, c: Vector2) -> void: + var mote := ColorRect.new() + mote.color = UiKit.CANDLE + mote.size = Vector2(5, 5) + mote.mouse_filter = Control.MOUSE_FILTER_IGNORE + mote.position = c + Vector2((float(i % 5) - 2.0) * 44.0, -40.0 - 10.0 * float(i / 5)) + add_child(mote) + var tw := mote.create_tween() + tw.tween_interval(UiKit.motion(0.12 * i)) + tw.tween_property(mote, "position:y", mote.position.y + 90.0, UiKit.motion(1.1)) + tw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(1.1)) + tw.tween_callback(mote.queue_free) # GP 行数字滚动(demo ⑧——0.8s 三次缓出;减动效直出终值) @@ -325,15 +372,15 @@ func _roll_gp(gp_gain: int, gp_total: int) -> void: func _clear_crest_bricks() -> void: - for b in _crest_bricks: + for b in st._crest_bricks: if is_instance_valid(b): b.queue_free() - _crest_bricks.clear() + st._crest_bricks.clear() # 演出探针出口(fx_probe——胜利纹章/失败掸灰/封印光收回三断言面) func fx_probe() -> Dictionary: - return {"crest_shards": _fx_crest_shards, "dust_motes": _fx_dust_motes, "seal_dots": _fx_seal_dots} + return {"crest_shards": st._fx_counts["crest"], "dust_motes": st._fx_counts["dust"], "seal_dots": st._fx_counts["seal"]} func _emit_continue_pressed() -> void: diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index fb036e42..30dd7e2c 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -145,71 +145,122 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: var src := _source_label(String(row.get("source_class", ""))) var round_no: int = int(row.get("round_index", 0)) var prefix := " [color=#%s]R%d%s[/color] " % [UiKit.HEX_LOG_ROUND, round_no, ("·" + src) if src != "" else ""] - var hl := String(row.get("highlight", "")) - var v: int = int(row.get("value", 0)) - var name_: String = String(row.get("display_name", "")) - var target := String(row.get("target", "")) - var tail := " → %s" % target if target != "" else "" var suffix := "(%s)" % src if src != "" else "" - match String(row.get("type", "")): + var kind := String(row.get("type", "")) + var body := "" + match kind: "effect": - # GP 型效果行:「GP 袋 +3」兑付反馈——value = 本行 ±power, - # escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账) - if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]: - var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS - var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix] - return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body]) - # 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则) - var pool: int = int(row.get("pool_value", 0)) - var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS - var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix] - return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body]) + body = _fmt_effect(row, prefix, suffix) "damage": - # 伤害行无行上拆行文案——行上归因读数只剩掷点档位随行 - # 护甲等级掷点留档(掷点读数与档位随行可读;吃掷点分量恒带,棋盘来源行不带) - var roll_txt := "" - if row.get("roll", null) != null: - roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)] - if bool(row.get("crit", false)): - roll_txt += " · 暴击×%d" % int(_db.combat.settlement.damage_roll.crit_multiplier) - elif int(row.tier) < 100: - roll_txt += " · ×%d%%" % int(row.tier) - roll_txt += "〕" - var kill := " ⚔击杀!" if row.get("killed", false) else "" - # 伤害数字不明显——伤害值放大加色独立于行文(朱红大一号——数字即信息主角,非同字号色温微差) - var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix] - var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY) - if bool(row.get("crit", false)): - return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行 - return prefix + "[color=%s]%s[/color]" % [body_col, body] - "heal": - return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, v, tail, suffix] + body = _fmt_damage(row, prefix, suffix) + "heal", "cast", "decay": + body = _fmt_simple(kind, row, prefix, suffix) "reward_ineffective": - # 未生效类行:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 - var n := (" ×%d" % agg_count) if agg_count > 1 else "" - return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix] + body = _fmt_ineffective(row, prefix, suffix, agg_count) "seal_miss": - var miss_note := "" - if hl == "perfect_broken": - miss_note = "(本图完美封印已破——清图奖励不再加倍)" - elif hl == "": - miss_note = "(首次封印落空已豁免——不计入完美封印判定)" - var body := "✗ %s%s" % [name_, miss_note] - return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body]) + body = _fmt_seal_miss(row, prefix) + "verdict": + body = _fmt_verdict(row) + _: + body = _fmt_default(row, prefix, suffix) + return body + + +## 简单行域(heal/cast/decay 三型——单行单墨直排) +func _fmt_simple(kind: String, row: Dictionary, prefix: String, suffix: String) -> String: + var name_: String = String(row.get("display_name", "")) + match kind: + "heal": + return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, int(row.get("value", 0)), _tail_of(row), suffix] "cast": # 施放行:增益技 0 伤害施放,具名技能 return prefix + "[color=#%s]✦ %s%s[/color]" % [UiKit.HEX_LOG_CAST, name_, suffix] "decay": - return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, v] - "verdict": - if int(row.value) == 1: - return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_] - return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_] - "highlight": - return prefix + _gold_frame("★ %s" % name_) + return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, int(row.get("value", 0))] + return "" + + +## 默认行域(未知型/高亮星标——未知型=名行直排) +func _fmt_default(row: Dictionary, prefix: String, suffix: String) -> String: + var name_: String = String(row.get("display_name", "")) + if String(row.get("type", "")) == "highlight": + return prefix + _gold_frame("★ %s" % name_) return prefix + name_ + suffix +## 尾注域:目标格指向(行文附加) +func _tail_of(row: Dictionary) -> String: + var target := String(row.get("target", "")) + return " → %s" % target if target != "" else "" + + +## GP 型效果行域:「GP 袋 +3」兑付反馈——value = 本行 ±power, +## escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账) +func _fmt_effect(row: Dictionary, prefix: String, suffix: String) -> String: + var v: int = int(row.get("value", 0)) + var hl := String(row.get("highlight", "")) + var name_: String = String(row.get("display_name", "")) + if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]: + var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS + var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix] + return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body]) + # 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则) + var pool: int = int(row.get("pool_value", 0)) + var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS + var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix] + return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body]) + + +## 伤害行域:行上归因读数只剩掷点档位随行(护甲等级掷点留档——掷点读数与档位随行可读; +## 吃掷点分量恒带,棋盘来源行不带)。伤害数字不明显——伤害值放大加色独立于行文(朱红大一号) +func _fmt_damage(row: Dictionary, prefix: String, suffix: String) -> String: + var v: int = int(row.get("value", 0)) + var name_: String = String(row.get("display_name", "")) + var tail := _tail_of(row) + var roll_txt := "" + if row.get("roll", null) != null: + roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)] + if bool(row.get("crit", false)): + roll_txt += " · 暴击×%d" % int(_db.combat.settlement.damage_roll.crit_multiplier) + elif int(row.tier) < 100: + roll_txt += " · ×%d%%" % int(row.tier) + roll_txt += "〕" + var kill := " ⚔击杀!" if row.get("killed", false) else "" + var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix] + if bool(row.get("crit", false)): + return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行 + var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY) + return prefix + "[color=%s]%s[/color]" % [body_col, body] + + +## 未生效行域:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 +func _fmt_ineffective(row: Dictionary, prefix: String, suffix: String, agg_count: int) -> String: + var name_: String = String(row.get("display_name", "")) + var n := (" ×%d" % agg_count) if agg_count > 1 else "" + return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix] + + +## 封印落空行域:完美破/首失豁免两注记分档(金框=破完美/普通墨=豁免) +func _fmt_seal_miss(row: Dictionary, prefix: String) -> String: + var hl := String(row.get("highlight", "")) + var name_: String = String(row.get("display_name", "")) + var miss_note := "" + if hl == "perfect_broken": + miss_note = "(本图完美封印已破——清图奖励不再加倍)" + elif hl == "": + miss_note = "(首次封印落空已豁免——不计入完美封印判定)" + var body := "✗ %s%s" % [name_, miss_note] + return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body]) + + +## 裁定行域:胜金/负绿双墨(粗体菱形头) +func _fmt_verdict(row: Dictionary) -> String: + var name_: String = String(row.get("display_name", "")) + if int(row.value) == 1: + return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_] + return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_] + + func _strip_bb(bb: String) -> String: var out := "" var in_tag := false diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index 78ab8b5e..9ab85302 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -26,18 +26,28 @@ var world: Node2D # StageWorld(preload 脚本实例——Node2D 世 func _init(db_ref) -> void: custom_minimum_size = Vector2(0, 252) - # 面板底(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景 - # =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿) + _build_backdrop() + _build_world_stage() + _build_text_half(db_ref) + _build_companion_layer(db_ref) + + +## 背景域(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景 +## =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿) +func _build_backdrop() -> void: var bg := Panel.new() var sb := UiKit.panel_flat(UiKit.STAGE_BG, UiKit.PANEL_BORDER, 0, 1) 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) + _build_beams_layer() - # 顶光柱氛围层(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收 - # UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/ - # 世界站桩之下——层序=bg < 光柱 < 世界) + +## 顶光柱氛围层域(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收 +## UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/ +## 世界站桩之下——层序=bg < 光柱 < 世界) +func _build_beams_layer() -> void: var beams := Control.new() beams.set_anchors_preset(Control.PRESET_FULL_RECT) beams.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -45,7 +55,9 @@ func _init(db_ref) -> void: beams.resized.connect(beams.queue_redraw) add_child(beams) - # ── Node2D 舞台半区(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5)── + +## Node2D 舞台半区域(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5) +func _build_world_stage() -> void: _svp_container = SubViewportContainer.new() _svp_container.stretch = true # 容器尺寸 = 视口逻辑分辨率,恒 1:1 _svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # 世界零输入(tooltip 归壳层伴随件) @@ -61,7 +73,9 @@ func _init(db_ref) -> void: world = _STAGE_WORLD.new() _viewport.add_child(world) - # ── Control 文本半区(round_label / stage_center / log_view 原样保留——C1 永久 Control)── + +## Control 文本半区域(round_label / stage_center / log_view 原样保留——C1 永久 Control) +func _build_text_half(db_ref) -> void: var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) vb.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -70,8 +84,14 @@ func _init(db_ref) -> void: vb.offset_right = -_CONTENT_MARGINS.size.x vb.offset_bottom = -_CONTENT_MARGINS.size.y add_child(vb) + _build_narr_row(vb) + _build_center_row(vb) + log_view = BattleLogView.new(db_ref) + vb.add_child(log_view) - # M8(UI 落码批):轮次牌 plate9 素牌化(demo ⑦——墨字上亮牌 12.3:1,暗底暖字退役) + +## 轮次牌域(M8 UI 落码批:plate9 素牌化——demo ⑦,墨字上亮牌 12.3:1,暗底暖字退役) +func _build_narr_row(vb: VBoxContainer) -> void: var narr_plate := PanelContainer.new() narr_plate.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(12, 4, 12, 4))) narr_plate.size_flags_horizontal = Control.SIZE_SHRINK_CENTER @@ -80,12 +100,14 @@ func _init(db_ref) -> void: round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER narr_plate.add_child(round_label) + +## 中段行域:提示语居中+敌站桩壳层伴随件容器(明细带展开/收起改行高——世界布局随动) +func _build_center_row(vb: VBoxContainer) -> void: _row = HBoxContainer.new() _row.add_theme_constant_override("separation", 10) _row.size_flags_vertical = Control.SIZE_EXPAND_FILL vb.add_child(_row) - _row.resized.connect(_on_row_resized) # 明细带展开/收起改行高——世界布局随动 - + _row.resized.connect(_on_row_resized) var center := CenterContainer.new() center.size_flags_horizontal = Control.SIZE_EXPAND_FILL center.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行内只剩文本——鼠标全放行给壳层伴随件 @@ -96,21 +118,17 @@ func _init(db_ref) -> void: stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER center_col.add_child(stage_center) _row.add_child(center) - enemy_box = Control.new() # 站桩壳层伴随件容器(T3 语义改写) enemy_box.mouse_filter = Control.MOUSE_FILTER_IGNORE _row.add_child(enemy_box) - log_view = BattleLogView.new(db_ref) - vb.add_child(log_view) - # ── 壳层伴随件层(最顶——世界站桩之上的 Control 触达面)── +## 壳层伴随件层域(最顶——世界站桩之上的 Control 触达面)+主角站桩注册(世界)+ 状态行伴随件(壳层) +func _build_companion_layer(db_ref) -> void: _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") var hero_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) hero_standee.set_portrait_texture(hero_tex, true) # B 区立绘 48px 档·normal 态朝右(素材缺位 null 回退字形「人」) @@ -230,26 +248,24 @@ func enemy_body_at(i: int) -> Node2D: # 素材装载属组合点职责(主角侧同款先例 _init TEX_HERO_PORTRAIT),StandeeBody 保持纯收 tex 面。 # 素材缺位 load_tex→null→set_portrait_texture 字形回退(表现与素材批前逐位一致);未录键返回 null # 走默认字形「敌」(与 StandeeBody._portrait_glyph 默认档同口径)——数据表扩键先补此处映射。 +## 敌像 id→纹理键映射表(查表+回退——映射语义零变化;未知 id=null 回退字形) func _enemy_portrait_tex(enemy_id: String) -> Texture2D: - match enemy_id: - "goblin": - return UiKit.load_tex(UiKit.TEX_ENEMY_GOBLIN) - "bugbear": - return UiKit.load_tex(UiKit.TEX_ENEMY_BUGBEAR) - "ogre_chief": - return UiKit.load_tex(UiKit.TEX_ENEMY_OGRE_CHIEF) - "giant_spider": - return UiKit.load_tex(UiKit.TEX_ENEMY_GIANT_SPIDER) - "drider": - return UiKit.load_tex(UiKit.TEX_ENEMY_DRIDER) - "duergar": - return UiKit.load_tex(UiKit.TEX_ENEMY_DUERGAR) - "hook_horror": - return UiKit.load_tex(UiKit.TEX_ENEMY_HOOK_HORROR) - "duergar_king": - return UiKit.load_tex(UiKit.TEX_ENEMY_DUERGAR_KING) - _: - return null + var table := {"goblin": UiKit.TEX_ENEMY_GOBLIN, "bugbear": UiKit.TEX_ENEMY_BUGBEAR, + "ogre_chief": UiKit.TEX_ENEMY_OGRE_CHIEF, "giant_spider": UiKit.TEX_ENEMY_GIANT_SPIDER, + "drider": UiKit.TEX_ENEMY_DRIDER, "duergar": UiKit.TEX_ENEMY_DUERGAR, + "hook_horror": UiKit.TEX_ENEMY_HOOK_HORROR, "duergar_king": UiKit.TEX_ENEMY_DUERGAR_KING} + return UiKit.load_tex(String(table.get(enemy_id, ""))) if table.has(enemy_id) else null + + +## 单敌站桩构建域(循环体提函数——世界本体+壳层伴随件+敌种立绘接线〔仅重建拍装载一次; +## flip=false——八敌像素源核证朝左/正面,呈现即朝主角无误;反向朝向源换代时翻转义务归素材批〕) +func _spawn_enemy_standee(d: Dictionary) -> Node2D: + var body: Node2D = _STANDEE_BODY.new("enemy") + world.add_child(body) + body.slot_row = _make_slot_companion(enemy_box) + body.set_data(d) + body.set_portrait_texture(_enemy_portrait_tex(String(d.get("enemy_id", ""))), false) + return body # 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律); @@ -263,14 +279,7 @@ func set_enemies(cards_data: Array) -> void: c.queue_free() _cards.clear() for d in cards_data: - var body: Node2D = _STANDEE_BODY.new("enemy") - world.add_child(body) - body.slot_row = _make_slot_companion(enemy_box) - body.set_data(d) - # 敌种立绘接线(仅重建拍装载一次;flip=false——八敌像素源核证朝左/正面, - # 呈现即朝主角无误;反向朝向源换代时翻转义务归素材批) - body.set_portrait_texture(_enemy_portrait_tex(String(d.get("enemy_id", ""))), false) - _cards.append(body) + _cards.append(_spawn_enemy_standee(d)) _sync_layout.call_deferred() return for i in cards_data.size(): diff --git a/game/presentation/views/board_cell_unit.gd b/game/presentation/views/board_cell_unit.gd index 7feda330..38ceac79 100644 --- a/game/presentation/views/board_cell_unit.gd +++ b/game/presentation/views/board_cell_unit.gd @@ -1,68 +1,13 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。 ## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。 +## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳, +## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义 +## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。 extends Node2D const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd") -var cell := Vector2i.ZERO # 板坐标(诊断观测面) -var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) - set(v): - edge = v - if _glow_layer != null and is_instance_valid(_glow_layer): - _glow_layer.setup(edge, glow_group) - queue_redraw() -var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框) -# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8; -# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)── -var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边) -var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边) -var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画) -var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二) -var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件) -# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)── -# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格 -# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带 -# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。 -var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入) - set(v): - facade_south = v - queue_redraw() -var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入) - set(v): - proj_band = v - queue_redraw() -var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载) - set(v): - glyph = v - queue_redraw() -var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝) - set(v): - glyph_color = v - queue_redraw() -var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) - set(v): - badge_glyph = v - if _glow_layer != null and is_instance_valid(_glow_layer): - _glow_layer.set_badge_mask(v != "") - queue_redraw() -var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) - set(v): - dot_visible = v - queue_redraw() -var hl_visible := false: # 焦点高亮(悬停态边框覆盖层) - set(v): - hl_visible = v - queue_redraw() -var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红) - set(v): - hl_color = v - queue_redraw() -var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入) -var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0) - set(v): - bevel = v - queue_redraw() # ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)── const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素) const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64) @@ -76,235 +21,355 @@ const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收 # 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界 # 无边框由 BoardView 侧跳过墙格北向描边承载)── const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%) -# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)── -var tex_base: Texture2D = null # ① 砖面底 -var tex_mutation: Texture2D = null # ② 变异纹带 -var tex_glyph: Texture2D = null # ③ 数字 -var tex_badge: Texture2D = null # ③' 徽章 -var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark - set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) - if tex_seal != v: - tex_seal = v - queue_redraw() -var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入) -var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用) + + +## 单格绘制输入集(被动绘制壳的全部绘制入参——BoardView 直写面;setter 触发宿主重绘语义逐字) +class _CellPaintState extends RefCounted: + var _owner: Node2D = null + var cell := Vector2i.ZERO # 板坐标(诊断观测面) + var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) + set(v): + edge = v + if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): + _owner._glow_layer.setup(edge, glow_group) + _owner.queue_redraw() + var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框) + # ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8; + # 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)── + var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边) + var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边) + var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画) + var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件) + # ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)── + # 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格 + # 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带 + # 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。 + var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入) + set(v): + facade_south = v + _owner.queue_redraw() + var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入) + set(v): + proj_band = v + _owner.queue_redraw() + var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) + set(v): + badge_glyph = v + if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): + _owner._glow_layer.set_badge_mask(v != "") + _owner.queue_redraw() + var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) + set(v): + dot_visible = v + _owner.queue_redraw() + var hl_visible := false: # 焦点高亮(悬停态边框覆盖层) + set(v): + hl_visible = v + _owner.queue_redraw() + var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红) + set(v): + hl_color = v + _owner.queue_redraw() + var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入) + var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0) + set(v): + bevel = v + _owner.queue_redraw() + # ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)── + var tex_base: Texture2D = null # ① 砖面底 + var tex_mutation: Texture2D = null # ② 变异纹带 + var tex_glyph: Texture2D = null # ③ 数字 + var tex_badge: Texture2D = null # ③' 徽章 + var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark + set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) + if tex_seal != v: + tex_seal = v + _owner.queue_redraw() + var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入) + var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用) + set(v): + facade_strip = v + _owner.queue_redraw() + + # 变异角标(程序化,tex_mutation 纹带槽暂留): + # "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) + # 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): + # "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费); + # "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形) + var treasure_mark := "": + set(v): + if treasure_mark != v: + treasure_mark = v + _owner.queue_redraw() + var mutation_mark := "": + set(v): + if mutation_mark != v: + mutation_mark = v + _owner.queue_redraw() + + # ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) + # 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。 + # 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。 + var glow_group := 0: + set(v): + if glow_group != v: + glow_group = v + _owner._sync_glow_layer() + + var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入) + set(v): + removed_dim = v + _owner.queue_redraw() + + +var st := _CellPaintState.new() +var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结) set(v): - facade_strip = v + glyph = v queue_redraw() - -# 变异角标(程序化,tex_mutation 纹带槽暂留): -# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) -# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): -# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费); -# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形) -var treasure_mark := "": +var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝;探针直读冻结) set(v): - if treasure_mark != v: - treasure_mark = v - queue_redraw() -var mutation_mark := "": - set(v): - if mutation_mark != v: - mutation_mark = v - queue_redraw() - + glyph_color = v + queue_redraw() +var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二;探针直读冻结) # ── 视野迷雾呈现态(纯呈现零逻辑;规格=art-bible §6.5 v3.5,现行事实源=HUD框架「视野迷雾」节)── # 0=正常(灯光区内)/ 1=全黑(区外未开格——现行事实源「区外未开格=全黑不渲染」不透明档)/ 2=记忆态(区外已开格灰显可读) -var fog_state := 0: +var fog_state := 0: # 探针直读冻结 set(v): if fog_state != v: fog_state = v queue_redraw() -var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入) - set(v): - removed_dim = v - queue_redraw() -# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) -# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。 -# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。 -var glow_group := 0: - set(v): - if glow_group != v: - glow_group = v - _sync_glow_layer() - var _glow_layer: Node2D = null +func _init() -> void: + st._owner = self + + func _draw() -> void: - var rect := Rect2(Vector2.ZERO, Vector2(edge, edge)) - # ① 砖面底(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角; - # 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘) - if tex_base != null: - draw_texture_rect(tex_base, rect, false) + var rect := Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)) + _draw_base_layer(rect) + _draw_mutation_marks() + _draw_glyph_layer(rect) + _draw_overlay_layers(rect) + + +## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层; +## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化) +func _draw_base_layer(rect: Rect2) -> void: + if st.tex_base != null: + draw_texture_rect(st.tex_base, rect, false) # 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序—— # 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚; # 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合) - if not outline_sides.is_empty(): - var ow := 2.0 - var oc := UiKit.FOG_OUTLINE - for b in outline_sides: - var or_ := Rect2() - match int(b): - 0: or_ = Rect2(0, 0, edge, ow) - 1: or_ = Rect2(0, edge - ow, edge, ow) - 2: or_ = Rect2(0, 0, ow, edge) - 3: or_ = Rect2(edge - ow, 0, ow, edge) - draw_rect(or_, oc, true) + if not st.outline_sides.is_empty(): + _draw_outline_sides() _draw_south_facade() _draw_north_projection() _draw_e_facade() # 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb # 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序 # 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化 - if bg_sb != null: - draw_style_box(bg_sb, rect) + if st.bg_sb != null: + draw_style_box(st.bg_sb, rect) + _draw_contact_or_fallback(rect) - # 接触带微光(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格): - # 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge]; - # 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮) - if contact_col.a > 0.0: + +## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格): +## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge]; +## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮) +func _draw_contact_or_fallback(rect: Rect2) -> void: + if st.contact_col.a > 0.0: _draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差) _draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害) - var band := 4.0 - var half := edge * 0.5 - for b in contact_bands: - var r := Rect2() - match int(b): - 0: r = Rect2(0, 0, edge, band) - 1: r = Rect2(0, edge - band, edge, band) - 2: r = Rect2(0, 0, band, edge) - 3: r = Rect2(edge - band, 0, band, edge) - draw_rect(r, contact_col, true) - for cc in contact_corners: - var pt := Vector2.ZERO - match int(cc): - 0: pt = Vector2(0, 0) - 1: pt = Vector2(edge, 0) - 2: pt = Vector2(0, edge) - 3: pt = Vector2(edge, edge) - draw_circle(pt, 6.0, contact_col) - draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95))) - # 半条边:角点沿两条邻边各延伸 edge/2(坐标推导:横带沿 N/S 自角点向板心侧 half; - # NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——正交整边时重叠同色无害) - if int(cc) == 0 or int(cc) == 1: - # N 侧横带:NW 自 x=0、NE 自 x=edge-half - draw_rect(Rect2(pt.x if int(cc) == 0 else pt.x - half, 0.0, half, band), contact_col, true) - if int(cc) == 2 or int(cc) == 3: - # S 侧横带:SW 自 x=0、SE 自 x=edge-half - draw_rect(Rect2(pt.x if int(cc) == 2 else pt.x - half, edge - band, half, band), contact_col, true) - if int(cc) == 0 or int(cc) == 2: - # W 侧竖带:NW 自 y=0、SW 自 y=edge-half - draw_rect(Rect2(0.0, pt.y if int(cc) == 0 else pt.y - half, band, half), contact_col, true) - if int(cc) == 1 or int(cc) == 3: - # E 侧竖带:NE 自 y=0、SE 自 y=edge-half - draw_rect(Rect2(edge - band, pt.y if int(cc) == 1 else pt.y - half, band, half), contact_col, true) - elif bg_sb != null: - draw_style_box(bg_sb, rect) + _draw_contact_bands() + elif st.bg_sb != null: + draw_style_box(st.bg_sb, rect) _draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序) _draw_north_projection() - # 顶面倒角(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让 - # 呼吸微光/主角蓝边框保持最外沿可见;强度随档位) - if bevel > 0.0: - var base: Color = bg_sb.bg_color - var lit: Color = base.lightened(0.22 * bevel) - var dark: Color = base.darkened(0.22 * bevel) - var t := 2.0 - draw_rect(Rect2(t, t, edge - t * 2.0, t), lit, true) - draw_rect(Rect2(t, t, t, edge - t * 2.0), lit, true) - draw_rect(Rect2(t, edge - t * 2.0, edge - t * 2.0, t), dark, true) - draw_rect(Rect2(edge - t * 2.0, t, t, edge - t * 2.0), dark, true) + _draw_bevel() + + +## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印 +func _draw_glyph_layer(rect: Rect2) -> void: # ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用) - if tex_mutation != null: - draw_texture_rect(tex_mutation, rect, false) - # ②' 变异角标:左下角危险红三角(危险红=「开此格有代价」语义位,与雷族同源; - # 尺寸随格边比例——灰色可辨辅助=红对砖面明度差,禁喧宾) - # ②" 秘宝叠加层:真视格=危险红内缩框(策略纹理——确定决策输入不喧宾) - if treasure_mark == "mine_reveal": - var inset := edge * 0.14 - draw_rect(Rect2(inset, inset, edge - inset * 2.0, edge - inset * 2.0), - Color(UiKit.RED_MINE, 0.9), false, 2.0) - if mutation_mark == "flat_damage": - var mw := edge * 0.28 - var mpts := PackedVector2Array([Vector2(0, edge - mw), Vector2(0, edge), Vector2(mw, edge)]) - draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85)) + if st.tex_mutation != null: + draw_texture_rect(st.tex_mutation, rect, false) # ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?") if glyph != "": - # 数字色牌后备路径(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; - # 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1); - # 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌) - if plate: - var pw := edge * 0.60 - var ph := edge * 0.62 - var pr := Rect2((edge - pw) * 0.5, (edge - ph) * 0.5, pw, ph) - draw_rect(pr, UiKit.DIGIT_PLATE, true) - draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0) - if tex_glyph != null: - draw_texture_rect(tex_glyph, rect, false) + _draw_digit_plate() + if st.tex_glyph != null: + draw_texture_rect(st.tex_glyph, rect, false) else: - _draw_glyph(glyph, glyph_color, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式) - if badge_glyph != "": - if tex_badge != null: - draw_texture_rect(tex_badge, rect, false) + _draw_glyph(glyph, glyph_color, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式) + if st.badge_glyph != "": + if st.tex_badge != null: + draw_texture_rect(st.tex_badge, rect, false) else: - _draw_badge(badge_glyph) + _draw_badge(st.badge_glyph) # ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图) - if tex_seal != null: - draw_texture_rect(tex_seal, rect, false) + if st.tex_seal != null: + draw_texture_rect(st.tex_seal, rect, false) + + +## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; +## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1); +## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌) +func _draw_digit_plate() -> void: + var pw := st.edge * 0.60 + var ph := st.edge * 0.62 + var pr := Rect2((st.edge - pw) * 0.5, (st.edge - ph) * 0.5, pw, ph) + draw_rect(pr, UiKit.DIGIT_PLATE, true) + draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0) + + +## ⑤⑥⑦ 顶层域:路径点迹 + 焦点高亮 + 视野迷雾 + 中立移除退色 +func _draw_overlay_layers(rect: Rect2) -> void: # ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中) - if dot_visible: + if st.dot_visible: var ds := 6.0 - draw_rect(Rect2(Vector2((edge - ds) * 0.5, (edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true) - if hl_visible: + draw_rect(Rect2(Vector2((st.edge - ds) * 0.5, (st.edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true) + if st.hl_visible: # 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动) - draw_rect(Rect2(1, 1, edge - 2.0, edge - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边 - # ⑥ 视野迷雾层(最顶遮罩,§6.5 落文): - # 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论); - # 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1) - if fog_state == 1: - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0, 0, 0, 1.0), true) - elif fog_state == 2: - # 记忆态换装(demo 口径「地板纹理 + 灰罩 α.66」: - # 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 - # 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; - # 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) - if tex_base != null: - draw_texture_rect(tex_base, Rect2(Vector2.ZERO, Vector2(edge, edge)), false) - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) - if glyph != "": - _draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) + draw_rect(Rect2(1, 1, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边 + _draw_fog_layer() # ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义; # 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印)) - if removed_dim: - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0.12, 0.10, 0.09, 0.62), true) + if st.removed_dim: + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true) + + +## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文): +## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论); +## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1) +func _draw_fog_layer() -> void: + if fog_state == 1: + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true) + elif fog_state == 2: + _draw_fog_memory() + + +## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合) +func _draw_outline_sides() -> void: + var ow := 2.0 + var oc := UiKit.FOG_OUTLINE + for b in st.outline_sides: + var or_ := Rect2() + match int(b): + 0: or_ = Rect2(0, 0, st.edge, ow) + 1: or_ = Rect2(0, st.edge - ow, st.edge, ow) + 2: or_ = Rect2(0, 0, ow, st.edge) + 3: or_ = Rect2(st.edge - ow, 0, ow, st.edge) + draw_rect(or_, oc, true) + + +## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边 +func _draw_contact_bands() -> void: + var band := 4.0 + var half := st.edge * 0.5 + for b in st.contact_bands: + var r := Rect2() + match int(b): + 0: r = Rect2(0, 0, st.edge, band) + 1: r = Rect2(0, st.edge - band, st.edge, band) + 2: r = Rect2(0, 0, band, st.edge) + 3: r = Rect2(st.edge - band, 0, band, st.edge) + draw_rect(r, st.contact_col, true) + for cc in st.contact_corners: + _draw_contact_corner(int(cc), band, half) + + +## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2—— +## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进—— +## 正交整边时重叠同色无害) +func _draw_contact_corner(cc: int, band: float, half: float) -> void: + var pt := Vector2.ZERO + match cc: + 0: pt = Vector2(0, 0) + 1: pt = Vector2(st.edge, 0) + 2: pt = Vector2(0, st.edge) + 3: pt = Vector2(st.edge, st.edge) + draw_circle(pt, 6.0, st.contact_col) + draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, st.contact_col.a * 0.95))) + if cc == 0 or cc == 1: + # N 侧横带:NW 自 x=0、NE 自 x=edge-half + draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), st.contact_col, true) + if cc == 2 or cc == 3: + # S 侧横带:SW 自 x=0、SE 自 x=edge-half + draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, st.edge - band, half, band), st.contact_col, true) + if cc == 0 or cc == 2: + # W 侧竖带:NW 自 y=0、SW 自 y=edge-half + draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), st.contact_col, true) + if cc == 1 or cc == 3: + # E 侧竖带:NE 自 y=0、SE 自 y=edge-half + draw_rect(Rect2(st.edge - band, pt.y if cc == 1 else pt.y - half, band, half), st.contact_col, true) + + +## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让 +## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位) +func _draw_bevel() -> void: + if st.bevel <= 0.0: + return + var base: Color = st.bg_sb.bg_color + var lit: Color = base.lightened(0.22 * st.bevel) + var dark: Color = base.darkened(0.22 * st.bevel) + var t := 2.0 + draw_rect(Rect2(t, t, st.edge - t * 2.0, t), lit, true) + draw_rect(Rect2(t, t, t, st.edge - t * 2.0), lit, true) + draw_rect(Rect2(t, st.edge - t * 2.0, st.edge - t * 2.0, t), dark, true) + draw_rect(Rect2(st.edge - t * 2.0, t, t, st.edge - t * 2.0), dark, true) + + +## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角 +func _draw_mutation_marks() -> void: + if st.treasure_mark == "mine_reveal": + var inset := st.edge * 0.14 + draw_rect(Rect2(inset, inset, st.edge - inset * 2.0, st.edge - inset * 2.0), + Color(UiKit.RED_MINE, 0.9), false, 2.0) + if st.mutation_mark == "flat_damage": + var mw := st.edge * 0.28 + var mpts := PackedVector2Array([Vector2(0, st.edge - mw), Vector2(0, st.edge), Vector2(mw, st.edge)]) + draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85)) + + +## 记忆态换装域(demo 口径「地板纹理 + 灰罩 α.66」: +## 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 +## 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; +## 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) +func _draw_fog_memory() -> void: + if st.tex_base != null: + draw_texture_rect(st.tex_base, Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), false) + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) + if glyph != "": + _draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) # 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿): # 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far], # 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致 func _draw_south_facade() -> void: - if not facade_south: + if not st.facade_south: return - var far_h := edge * FACADE_FAR_FRAC - var near_h := edge * FACADE_NEAR_FRAC - var y_far := edge - far_h + var far_h := st.edge * FACADE_FAR_FRAC + var near_h := st.edge * FACADE_NEAR_FRAC + var y_far := st.edge - far_h var y_near := y_far - near_h var y_hl := y_near - FACADE_HIGHLIGHT_PX - draw_rect(Rect2(0.0, y_hl, edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点) - draw_rect(Rect2(0.0, y_near, edge, near_h), FACADE_NEAR_COL, true) - draw_rect(Rect2(0.0, y_far, edge, far_h), FACADE_FAR_COL, true) + draw_rect(Rect2(0.0, y_hl, st.edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点) + draw_rect(Rect2(0.0, y_near, st.edge, near_h), FACADE_NEAR_COL, true) + draw_rect(Rect2(0.0, y_far, st.edge, far_h), FACADE_FAR_COL, true) # 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减): # 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面) func _draw_north_projection() -> void: - if proj_band <= 0.0: + if st.proj_band <= 0.0: return - var seg := proj_band / 3.0 - draw_rect(Rect2(0.0, 0.0, edge, seg), PROJECTION_NEAR_COL, true) - draw_rect(Rect2(0.0, seg, edge, seg), PROJECTION_MID_COL, true) - draw_rect(Rect2(0.0, seg * 2.0, edge, seg), PROJECTION_EDGE_COL, true) + var seg := st.proj_band / 3.0 + draw_rect(Rect2(0.0, 0.0, st.edge, seg), PROJECTION_NEAR_COL, true) + draw_rect(Rect2(0.0, seg, st.edge, seg), PROJECTION_MID_COL, true) + draw_rect(Rect2(0.0, seg * 2.0, st.edge, seg), PROJECTION_EDGE_COL, true) # E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 → @@ -312,31 +377,31 @@ func _draw_north_projection() -> void: # 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶 # (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。 func _draw_e_facade() -> void: - if not facade_strip or tex_facade == null: + if not st.facade_strip or st.tex_facade == null: return - var strip_h := edge * E_FACADE_FRAC - var y0 := edge - strip_h - var ts := tex_facade.get_size() - draw_texture_rect_region(tex_facade, Rect2(0.0, y0, edge, strip_h), + var strip_h := st.edge * E_FACADE_FRAC + var y0 := st.edge - strip_h + var ts := st.tex_facade.get_size() + draw_texture_rect_region(st.tex_facade, Rect2(0.0, y0, st.edge, strip_h), Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC)) - draw_rect(Rect2(0.0, y0, edge, 1.0), UiKit.E_FACADE_FOOT_COL, true) + draw_rect(Rect2(0.0, y0, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true) # 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部) func _draw_glyph(g: String, col: Color, fs: int) -> void: - var f := font if font != null else ThemeDB.fallback_font + var f := st.font if st.font != null else ThemeDB.fallback_font if f == null: return var asc := f.get_ascent(fs) var desc := f.get_descent(fs) - draw_string(f, Vector2(0.0, (edge - (asc + desc)) * 0.5 + asc), g, - HORIZONTAL_ALIGNMENT_CENTER, edge, fs, col) + draw_string(f, Vector2(0.0, (st.edge - (asc + desc)) * 0.5 + asc), g, + HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col) # 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻, # 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。 func _sync_glow_layer() -> void: - if glow_group == 0: + if st.glow_group == 0: if _glow_layer != null and is_instance_valid(_glow_layer): _glow_layer.queue_free() _glow_layer = null @@ -344,15 +409,15 @@ func _sync_glow_layer() -> void: if _glow_layer == null or not is_instance_valid(_glow_layer): _glow_layer = _GLOW_LAYER.new() add_child(_glow_layer) - _glow_layer.setup(edge, glow_group) - _glow_layer.set_badge_mask(badge_glyph != "") + _glow_layer.setup(st.edge, st.glow_group) + _glow_layer.set_badge_mask(st.badge_glyph != "") # 徽章绘制:右下角金(文字贴角内缩 2–3px) func _draw_badge(g: String) -> void: - var f := font if font != null else ThemeDB.fallback_font + var f := st.font if st.font != null else ThemeDB.fallback_font if f == null: return - var fs := maxi(10, int(edge * 0.28)) + var fs := maxi(10, int(st.edge * 0.28)) var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs) - draw_string(f, Vector2(edge - sz.x - 2.0, edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD) + draw_string(f, Vector2(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD) diff --git a/game/presentation/views/board_fx_view.gd b/game/presentation/views/board_fx_view.gd index 5fc47c7a..ddd764ae 100644 --- a/game/presentation/views/board_fx_view.gd +++ b/game/presentation/views/board_fx_view.gd @@ -89,19 +89,24 @@ func play_boss_shadow() -> void: # 完美封印:金光带加倍扫射 + 横幅占位(§2.2 最高档) func play_perfect_sweep() -> void: - for i in 2: - var band := ColorRect.new() - band.color = Color(COL_GOLD, 0.35) - band.size = Vector2(size.x, 14.0 + 10.0 * i) - band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i) - band.mouse_filter = Control.MOUSE_FILTER_IGNORE - add_child(band) - var tw := band.create_tween() - tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i)) - tw.tween_callback(band.queue_free) + for i in 3: + _spawn_sweep_band(i, size) _show_banner("完美封印 ×2") +## 单光带域(循环体提函数——自左掠过渐出) +func _spawn_sweep_band(i: int, size: Vector2) -> void: + var band := ColorRect.new() + band.color = Color(COL_GOLD, 0.35) + band.size = Vector2(size.x, 14.0 + 10.0 * i) + band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i) + band.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(band) + var tw := band.create_tween() + tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i)) + tw.tween_callback(band.queue_free) + + # 封印命中:封印化暴击弹道加重占位(自棋盘上缘飞向 B 区敌人卡方向——越界即收) func play_seal_hit_projectile(heavy: bool) -> void: var shot := ColorRect.new() diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index c805c20d..c15e5c67 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -182,7 +182,7 @@ func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void: func set_world_font(f: Font) -> void: _world.board_font = f for c in _cells: - _cells[c].font = f + _cells[c].st.font = f _cells[c].queue_redraw() @@ -201,7 +201,7 @@ func set_cell_area(avail: Vector2) -> void: var stride := _cell + float(SEP) for c in _cells: var u: Node2D = _cells[c] - u.edge = _cell + u.st.edge = _cell u.position = Vector2(c) * stride if _world.hero != null and _hero_sprite.last.x >= 0: _place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点) @@ -247,17 +247,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: for x in _board.width: var c := Vector2i(x, y) var u: Node2D = _CELL_UNIT.new() - u.cell = c - u.edge = _cell + u.st.cell = c + u.st.edge = _cell u.position = Vector2(c) * stride - u.bevel = DEPTH_BEVEL # 新建格单元继承 + u.st.bevel = DEPTH_BEVEL # 新建格单元继承 u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化 if _palette.tiles_ok: # NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档, # 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片 # 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化) u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - u.font = _world.board_font + u.st.font = _world.board_font _world.add_child(u) _cells[c] = u set_cell_area(size) @@ -509,21 +509,21 @@ func _apply_hover_visuals(c: Vector2i) -> void: var seal: bool = _minesweeper.has_seal(c) var adjacent: bool = _diggable.has(c) if revealed: - u.hl_visible = true - u.hl_color = COL_HL_TARGET # 移动目标格边框(金) + u.st.hl_visible = true + u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金) _set_cursor_shape(c, CURSOR_MOVE_SEM) _show_path_preview(c) elif adjacent: - u.hl_visible = true - u.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖) + u.st.hl_visible = true + u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖) _set_cursor_shape(c, CURSOR_DIG) elif seal: - u.hl_visible = true - u.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供) + u.st.hl_visible = true + u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) else: - u.hl_visible = true - u.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道) + u.st.hl_visible = true + u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) hover_forbidden.emit(c) @@ -531,7 +531,7 @@ func _apply_hover_visuals(c: Vector2i) -> void: func _clear_hover_visuals(c: Vector2i) -> void: if not _cells.has(c): return - _cells[c].hl_visible = false + _cells[c].st.hl_visible = false _hide_path_preview() @@ -546,12 +546,12 @@ func _show_path_preview(c: Vector2i) -> void: var path: Array[Variant] = _minesweeper.compute_move_path(c) for pc in path: if _cells.has(pc): - _cells[pc].dot_visible = true + _cells[pc].st.dot_visible = true func _hide_path_preview() -> void: for c in _cells: - _cells[c].dot_visible = false + _cells[c].st.dot_visible = false # ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ---- @@ -611,11 +611,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: var digit_col := COL_DIGIT # 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal # 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。 - u.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null + u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null if removed: revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结) seal = false - u.tex_seal = null + u.st.tex_seal = null if revealed: var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面 if rec.get("kind", "") == "mine": @@ -662,8 +662,8 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: # 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载) glyph = "人" digit_col = COL_HERO - u.tex_base = tile - u.bg_sb = sb + u.st.tex_base = tile + u.st.bg_sb = sb u.glyph = glyph u.glyph_color = digit_col # 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板 @@ -673,11 +673,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) u.glyph_color = digit_col u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit) - u.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) - u.facade_south = false - u.proj_band = 0.0 - u.tex_facade = null - u.facade_strip = false + u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) + u.st.facade_south = false + u.st.proj_band = 0.0 + u.st.tex_facade = null + u.st.facade_strip = false # 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见); # 上方 else 分支已是全路径清残留,尾部重置系冗余且有害 var south_n: Vector2i = c + Vector2i(0, 1) @@ -687,44 +687,44 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: if not revealed: # 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面 #(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画 - u.facade_south = south_in and _minesweeper.is_revealed(south_n) + u.st.facade_south = south_in and _minesweeper.is_revealed(south_n) # 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界) for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]: var n: Vector2i = c + d if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n): var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3)) - if code == 1 and u.facade_south: + if code == 1 and u.st.facade_south: continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑 if code == 0 and _palette.tiles_ok: continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边) - u.outline_sides.append(code) + u.st.outline_sides.append(code) else: # 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量) if north_in and not _minesweeper.is_revealed(north_n): - u.proj_band = u.edge * WALL_PROJECTION_FRAC + u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC # E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30% # 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义 if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok: - u.facade_strip = true - u.tex_facade = _palette.tiles.get("wall_center") + u.st.facade_strip = true + u.st.tex_facade = _palette.tiles.get("wall_center") # ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有 # 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算) if not revealed: - u.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" + u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" if _treasure_preview.has(c) and glyph == "": var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))): glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))]) digit_col = COL_GOLD - u.badge_glyph = glyph + u.st.badge_glyph = glyph # ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位); # 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留) if not revealed and _vmark_cb.is_valid(): - u.mutation_mark = String(_vmark_cb.call(c)) + u.st.mutation_mark = String(_vmark_cb.call(c)) else: - u.mutation_mark = "" - u.badge_glyph = _update_badge(c) - u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + u.st.mutation_mark = "" + u.st.badge_glyph = _update_badge(c) + u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) # 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/ # 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。 # 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。 @@ -734,7 +734,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: u.fog_state = (2 if revealed else 1) if fogged else 0 else: u.fog_state = 0 - u.removed_dim = removed + u.st.removed_dim = removed u.queue_redraw() @@ -765,7 +765,7 @@ func _sync_glow_to_units() -> void: for c in touched: var u: Node2D = _cells.get(c) if u != null: - u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) func _process(delta: float) -> void: @@ -777,14 +777,14 @@ func _process(delta: float) -> void: if UiKit.motion_scale < 1.0: return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动) var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线) - _contact_unit.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) + _contact_unit.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) _contact_unit.queue_redraw() # 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色 func _apply_contact_bands() -> void: if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true): - _contact_unit.contact_col = Color(0, 0, 0, 0) + _contact_unit.st.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() _contact_unit = null if _board == null or _minesweeper == null or not _dig_glow: @@ -801,19 +801,19 @@ func _apply_contact_bands() -> void: for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]: if _diggable.has(_minesweeper.hero_pos() + d): corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3))) - hu.contact_bands = bands - hu.contact_corners = corners + hu.st.contact_bands = bands + hu.st.contact_corners = corners # 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実); # 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色, # 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。 var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0) - hu.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) + hu.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) hu.queue_redraw() func _reset_breath() -> void: if _contact_unit != null and is_instance_valid(_contact_unit): - _contact_unit.contact_col = Color(0, 0, 0, 0) + _contact_unit.st.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() @@ -888,5 +888,5 @@ func cell_rect_probe(c: Vector2i) -> Rect2: if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0: return Rect2() var k: Vector2 = host.size / vp_size - var local := Rect2(u.global_position, Vector2(u.edge, u.edge)) + var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge)) return Rect2(host.position + local.position * k, local.size * k) diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index 68573246..b6509338 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -23,6 +23,11 @@ func _init() -> void: var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) add_child(vb) + _build_views(vb) + + +## 视图三件域:AP 条+棋盘区(含动效与 toast 叠层) +func _build_views(vb: VBoxContainer) -> void: ap_bar = ApBarView.new() vb.add_child(ap_bar) var board_area := Control.new() @@ -31,6 +36,11 @@ func _init() -> void: vb.add_child(board_area) _board_area = board_area board_area.resized.connect(_on_board_area_resized) + _build_board_children(board_area, vb) + + +## 棋盘子件域:视图+动效+toast 三叠层挂接+状态角标+操作提示条(hint 回挂 vb——树序与拆分前一致) +func _build_board_children(board_area: Control, vb: VBoxContainer) -> void: board_view = BoardView.new() board_view.set_anchors_preset(Control.PRESET_FULL_RECT) board_area.add_child(board_view) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index f9e6903d..77458ab6 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -2,6 +2,10 @@ ## 营地覆盖层:树淬炼/锻造工坊/秘宝匣/奖励格图鉴四柜台只读投影,购买与切换意图经信号外发。 ## 柜台锁定态(钥匙未点亮)开/关零副作用——零写档零报错、锁定行纯文案无可交互购买路径。 ## 开合与开始层互斥归组合根承接(打开前隐藏开始层)。 +## 内部状态件(u1 子域拆件——契约 9 职责轴):_TreeState=树淬炼柜台内容容器(单一变化原因=树行板结构); +## _ForgeState=锻造工坊与秘宝匣两柜台行板与锁定门(钥匙门同构);_AttuneState=同调装配段节点与钮册; +## _SideState=Y 刷新升级行与最近冒险账面。探针直读冻结成员(_balance_label/_tree_buy_btns/ +## _tree_fork_btns/_workshop_holder/_workshop_rows)住宿主不进状态件。 class_name CampOverlay extends Control @@ -12,37 +16,54 @@ signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口 signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态) signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) -signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor) +signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select) # 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE -var _balance_label: Label -var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) -var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费) -var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费) -var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) -var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) -var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) -var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) -var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) -var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) -var _ws_lock_l: Label # 秘宝匣柜台锁定态行 -var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行) -var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}] -var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) -var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) -var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) -var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) -var _refresh_btn: Button -var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) + +class _TreeState extends RefCounted: + var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) + + +class _ForgeState extends RefCounted: + var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) + var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) + var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) + var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) + var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) + var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) + var _ws_lock_l: Label # 秘宝匣柜台锁定态行 + + +class _AttuneState extends RefCounted: + var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) + var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) + var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) + + +class _SideState extends RefCounted: + var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) + var _refresh_btn: Button + var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) + + +var _balance_label: Label # 探针直读冻结(S17①——住宿主) +var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费;住宿主) +var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费;住宿主) +var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行;探针直读冻结——住宿主) +var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}](探针消费;住宿主) +var _tree := _TreeState.new() +var _forge := _ForgeState.new() +var _attune := _AttuneState.new() +var _side := _SideState.new() func _init() -> void: # 底=不透明暖亮档(半透 dim 会透出底下棋盘/战斗提示条,故须实底;demo #v-camp=bg-bright 实底, # 遮蔽语义由组合根输入锁兜底) - var dim := UiKit.modal_dim(self, UiKit.BG_BRIGHT) + var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT) # 石纹弱质感+暗角(demo .tilebg 0.14/.vignette;缺位不挂即回退平色) var bd := UiKit.backdrop_tile() if bd != null: @@ -61,6 +82,29 @@ func _init() -> void: root.add_child(col) # ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ── + _build_top_bar(col) + + # ── 四柜台并排(程序占位重排)── + # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) + var camp_scroll := ScrollContainer.new() + camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO + camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + col.add_child(camp_scroll) + var mid := HBoxContainer.new() + mid.add_theme_constant_override("separation", 14) + mid.size_flags_vertical = Control.SIZE_EXPAND_FILL + camp_scroll.add_child(mid) + _build_tree_counter(mid) + _build_forge_counter(mid) + _build_workshop_counter(mid) + _build_atlas_counter(mid) + _build_bottom_bar(col) + visible = false + + +## 顶栏域:铭牌(标题+副题)+钥石徽章 +func _build_top_bar(col: VBoxContainer) -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 10) col.add_child(top) @@ -82,82 +126,78 @@ func _init() -> void: top.add_child(badge) _balance_label = badge.value_label - # ── 四柜台并排(程序占位重排)── - # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) - var camp_scroll := ScrollContainer.new() - camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO - camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - col.add_child(camp_scroll) - var mid := HBoxContainer.new() - mid.add_theme_constant_override("separation", 14) - mid.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.add_child(mid) - # 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) - _tree_holder = VBoxContainer.new() - _tree_holder.add_theme_constant_override("separation", 6) +## 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) +func _build_tree_counter(mid: HBoxContainer) -> void: + _tree._tree_holder = VBoxContainer.new() + _tree._tree_holder.add_theme_constant_override("separation", 6) var tree_scroll := ScrollContainer.new() tree_scroll.custom_minimum_size = Vector2(300, 336) tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - tree_scroll.add_child(_tree_holder) + tree_scroll.add_child(_tree._tree_holder) var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300) tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL tree_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(tree_panel) - # 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) - _forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _forge_lock_l.custom_minimum_size = Vector2(280, 0) - _forge_workbench = VBoxContainer.new() - _forge_workbench.add_theme_constant_override("separation", 6) - _forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) - _forge_zone_weapon = VBoxContainer.new() - _forge_zone_weapon.add_theme_constant_override("separation", 4) - _forge_zone_armor = VBoxContainer.new() - _forge_zone_armor.add_theme_constant_override("separation", 4) + +## 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) +func _build_forge_counter(mid: HBoxContainer) -> void: + _forge._forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge._forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 + _forge._forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge._forge_lock_l.custom_minimum_size = Vector2(280, 0) + _forge._forge_workbench = VBoxContainer.new() + _forge._forge_workbench.add_theme_constant_override("separation", 6) + _forge._forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) + _forge._forge_zone_weapon = VBoxContainer.new() + _forge._forge_zone_weapon.add_theme_constant_override("separation", 4) + _forge._forge_zone_armor = VBoxContainer.new() + _forge._forge_zone_armor.add_theme_constant_override("separation", 4) var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 同款深金墨(对柜台板 5.21:1) - _forge_workbench.add_child(wb_wh) - _forge_workbench.add_child(_forge_zone_weapon) + _forge._forge_workbench.add_child(wb_wh) + _forge._forge_workbench.add_child(_forge._forge_zone_weapon) var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) - _forge_workbench.add_child(wb_ah) - _forge_workbench.add_child(_forge_zone_armor) + _forge._forge_workbench.add_child(wb_ah) + _forge._forge_workbench.add_child(_forge._forge_zone_armor) var forge_holder := VBoxContainer.new() forge_holder.add_theme_constant_override("separation", 6) forge_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(与树/秘宝匣 scroll 336 min 对齐——锁定态仅锁文也撑住栏高) - forge_holder.add_child(_forge_lock_l) - forge_holder.add_child(_forge_workbench) + forge_holder.add_child(_forge._forge_lock_l) + forge_holder.add_child(_forge._forge_workbench) var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300) forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL forge_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(forge_panel) - # 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) - _ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _ws_lock_l.custom_minimum_size = Vector2(280, 0) - _ws_scroll = ScrollContainer.new() - _ws_scroll.custom_minimum_size = Vector2(300, 336) - _ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - _ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + +## 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) +func _build_workshop_counter(mid: HBoxContainer) -> void: + _forge._ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge._ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 + _forge._ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge._ws_lock_l.custom_minimum_size = Vector2(280, 0) + _forge._ws_scroll = ScrollContainer.new() + _forge._ws_scroll.custom_minimum_size = Vector2(300, 336) + _forge._ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + _forge._ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _workshop_holder = VBoxContainer.new() _workshop_holder.add_theme_constant_override("separation", 6) - _ws_scroll.add_child(_workshop_holder) + _forge._ws_scroll.add_child(_workshop_holder) var ws_holder := VBoxContainer.new() ws_holder.add_theme_constant_override("separation", 6) ws_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(锁定态 scroll 隐、仅锁文也撑住栏高) - ws_holder.add_child(_ws_lock_l) - ws_holder.add_child(_ws_scroll) + ws_holder.add_child(_forge._ws_lock_l) + ws_holder.add_child(_forge._ws_scroll) var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300) ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL ws_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(ws_panel) - # 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) + +## 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) +func _build_atlas_counter(mid: HBoxContainer) -> void: var atlas_holder := VBoxContainer.new() atlas_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(图鉴仅锁文也撑住栏高) var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300) @@ -170,7 +210,9 @@ func _init() -> void: lock_l.custom_minimum_size = Vector2(280, 0) atlas_holder.add_child(lock_l) - # ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ── + +## 底栏域:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 +func _build_bottom_bar(col: VBoxContainer) -> void: var close_btn := UiKit.button("关闭营地(回开始界面)") close_btn.pressed.connect(_emit_closed) var bottom := HBoxContainer.new() @@ -182,11 +224,10 @@ func _init() -> void: var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT) bottom.add_child(caption) col.add_child(bottom) - _recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) - _recent_list.clip_text = true - _recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - col.add_child(_recent_list) - visible = false + _side._recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) + _side._recent_list.clip_text = true + _side._recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + col.add_child(_side._recent_list) # 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白) @@ -220,7 +261,7 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_ # 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy, # currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}]) func _rebuild_tree(data: Dictionary) -> void: - for c in _tree_holder.get_children(): + for c in _tree._tree_holder.get_children(): c.queue_free() _tree_buy_btns.clear() _tree_fork_btns.clear() @@ -233,52 +274,62 @@ func _rebuild_tree(data: Dictionary) -> void: if vein != last_vein: last_vein = vein var vh := UiKit.label(String(vein_names.get(vein, vein)),UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1 - _tree_holder.add_child(vh) - var hb := HBoxContainer.new() - hb.add_theme_constant_override("separation", 6) - var lvl := int(row.get("level", 0)) - var cap := int(row.get("cap", 1)) - var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) - name_l.custom_minimum_size = Vector2(150, 0) - name_l.clip_text = true - var gate := String(row.get("gate_text", "")) - if gate != "": - name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) - name_l.tooltip_text = "等级门槛:%s" % gate - hb.add_child(name_l) + _tree._tree_holder.add_child(vh) if String(row.get("kind", "")) == "fork": continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮 - var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 - btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) - btn.disabled = not bool(row.get("can_buy", false)) - hb.add_child(btn) - _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) - _tree_holder.add_child(hb) + _tree._tree_holder.add_child(_tree_row_line(row)) for fg in forks: - var group := String(fg.get("group", "")) - var active := String(fg.get("active_side", "")) - var owned := bool(fg.get("owned", false)) - var fb := HBoxContainer.new() - fb.add_theme_constant_override("separation", 6) - var state_txt := "未解锁" - if owned: - state_txt = "激活:%s" % active - var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) - fb.add_child(gl) - if not owned: - var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 - bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) - bb.disabled = not bool(fg.get("can_buy", false)) - fb.add_child(bb) - _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) - else: - for side_row in (fg.get("sides", []) as Array): - var sd := String(side_row.get("side", "")) - var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 - sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) - fb.add_child(sb) - _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) - _tree_holder.add_child(fb) + _tree._tree_holder.add_child(_tree_fork_line(fg)) + + +## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁) +func _tree_row_line(row: Dictionary) -> HBoxContainer: + var hb := HBoxContainer.new() + hb.add_theme_constant_override("separation", 6) + var lvl := int(row.get("level", 0)) + var cap := int(row.get("cap", 1)) + var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) + name_l.custom_minimum_size = Vector2(150, 0) + name_l.clip_text = true + var gate := String(row.get("gate_text", "")) + if gate != "": + name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) + name_l.tooltip_text = "等级门槛:%s" % gate + hb.add_child(name_l) + var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 + btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) + btn.disabled = not bool(row.get("can_buy", false)) + hb.add_child(btn) + _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) + return hb + + +## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮 +func _tree_fork_line(fg: Dictionary) -> HBoxContainer: + var group := String(fg.get("group", "")) + var active := String(fg.get("active_side", "")) + var owned := bool(fg.get("owned", false)) + var fb := HBoxContainer.new() + fb.add_theme_constant_override("separation", 6) + var state_txt := "未解锁" + if owned: + state_txt = "激活:%s" % active + var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) + fb.add_child(gl) + if not owned: + var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 + bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) + bb.disabled = not bool(fg.get("can_buy", false)) + fb.add_child(bb) + _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) + else: + for side_row in (fg.get("sides", []) as Array): + var sd := String(side_row.get("side", "")) + var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 + sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) + fb.add_child(sb) + _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) + return fb # 工作间钥匙门(锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行; @@ -289,86 +340,106 @@ func _refresh_key_gates(data: Dictionary) -> void: lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0)) var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1 var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1 - _forge_lock_l.visible = not forge_open - _forge_workbench.visible = forge_open - _ws_lock_l.visible = not ws_open - _ws_scroll.visible = ws_open + _forge._forge_lock_l.visible = not forge_open + _forge._forge_workbench.visible = forge_open + _forge._ws_lock_l.visible = not ws_open + _forge._ws_scroll.visible = ws_open # 锻造工坊行板重建(两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板: # 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression # 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1) func _rebuild_forge(data: Dictionary) -> void: - for zone in [_forge_zone_weapon, _forge_zone_armor]: + for zone in [_forge._forge_zone_weapon, _forge._forge_zone_armor]: for c in zone.get_children(): c.queue_free() - _forge_rows.clear() - if not _forge_workbench.visible: + _forge._forge_rows.clear() + if not _forge._forge_workbench.visible: return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续 var bal := int(data.get("balance", 0)) for row in data.get("forge", []): - var rid := String(row.get("route_id", "")) var kind := String(row.get("kind", "")) - var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor - var unlocked := bool(row.get("unlocked", false)) - var initial := bool(row.get("initial_owned", false)) - var lvl := int(row.get("level", 0)) - var cap_v := int(row.get("cap", 0)) - var next_price := int(row.get("next_price", 0)) - var unlock_price := int(row.get("unlock_price", 0)) - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(104, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var lvl_l := UiKit.label("Lv%d/%d" % [lvl, cap_v], 13, false, UiKit.INK_LABEL) - lvl_l.custom_minimum_size = Vector2(44, 0) - line.add_child(lvl_l) - # 解锁态/解锁通道呈现(并联双通道:练满前一档 or 一次性解锁价直购) - var st_text := "已解锁" - if not unlocked: - if initial: - st_text = "未解锁" - else: - st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), unlock_price] - elif lvl >= cap_v: - st_text = "已满级" - var status_l := UiKit.label(st_text, UiKit.FS_HINT, false, UiKit.STONE_DEEP) - status_l.clip_text = true - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 购买钮(已解锁未满级——几何价显示;余额不足禁用) - var buy_btn: Button = null - if unlocked and lvl < cap_v: - buy_btn = UiKit.button("锻造 %d" % next_price, 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(forge_buy_requested.emit.bind(rid)) - buy_btn.disabled = bal < next_price - line.add_child(buy_btn) - # 解锁钮(待解锁非初始——一次性解锁价) - var unlock_btn: Button = null - if not unlocked and not initial: - unlock_btn = UiKit.button("解锁 %d" % unlock_price, 0.0) # 行板紧凑钮豁免 - unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(rid)) - unlock_btn.disabled = bal < unlock_price - line.add_child(unlock_btn) - # 选中钮(已解锁——免费切换;当前=禁用态标记) - var select_btn: Button = null - if unlocked: - var sel := bool(row.get("selected", false)) - select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 - select_btn.pressed.connect(forge_select_requested.emit.bind(rid)) - select_btn.disabled = sel - line.add_child(select_btn) - zone.add_child(card) - _forge_rows.append({"route_id": rid, "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, - "buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn, - "next_price": next_price, "unlock_price": unlock_price}) + var zone: VBoxContainer = _forge._forge_zone_weapon if kind == "weapon" else _forge._forge_zone_armor + var card := _forge_route_card(row, bal) + zone.add_child(card["card"]) + _forge._forge_rows.append(card) + + +## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面) +func _forge_route_card(row: Dictionary, bal: int) -> Dictionary: + var d := _forge_route_scalars(row) + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + card.add_child(line) + var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(104, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL) + lvl_l.custom_minimum_size = Vector2(44, 0) + line.add_child(lvl_l) + var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP) + status_l.clip_text = true + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + var btns := _forge_route_buttons(d, row, bal, line) + return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, + "buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"], + "next_price": d["next_price"], "unlock_price": d["unlock_price"]} + + +## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数 +func _forge_route_scalars(row: Dictionary) -> Dictionary: + return { + "rid": String(row.get("route_id", "")), + "unlocked": bool(row.get("unlocked", false)), + "initial": bool(row.get("initial_owned", false)), + "lvl": int(row.get("level", 0)), + "cap_v": int(row.get("cap", 0)), + "next_price": int(row.get("next_price", 0)), + "unlock_price": int(row.get("unlock_price", 0)), + } + + +## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态) +func _forge_status_text(d: Dictionary, row: Dictionary) -> String: + var st_text := "已解锁" + if not d["unlocked"]: + if d["initial"]: + st_text = "未解锁" + else: + st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]] + elif d["lvl"] >= d["cap_v"]: + st_text = "已满级" + return st_text + + +## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记) +func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary: + var buy_btn: Button = null + if d["unlocked"] and d["lvl"] < d["cap_v"]: + buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"])) + buy_btn.disabled = bal < d["next_price"] + line.add_child(buy_btn) + var unlock_btn: Button = null + if not d["unlocked"] and not d["initial"]: + unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免 + unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"])) + unlock_btn.disabled = bal < d["unlock_price"] + line.add_child(unlock_btn) + var select_btn: Button = null + if d["unlocked"]: + var sel := bool(row.get("selected", false)) + select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 + select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"])) + select_btn.disabled = sel + line.add_child(select_btn) + return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn} # 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建—— @@ -389,6 +460,12 @@ func refresh_camp(data: Dictionary) -> void: _rebuild_tree(data) _refresh_key_gates(data) _rebuild_forge(data) # 行板随刷重建(购买/解锁/选中后等级/价/禁用态同步) + _refresh_workshop_rows(data, balance) + _refresh_attune_segment(data, balance) + + +## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动 +func _refresh_workshop_rows(data: Dictionary, balance: int) -> void: var ws_by_id := {} for row in data.get("workshop", []): ws_by_id[String(row.get("item_id", ""))] = row @@ -397,21 +474,7 @@ func refresh_camp(data: Dictionary) -> void: if row.is_empty(): continue var unlocked := bool(row.get("unlocked", false)) - # 秘宝匣进度列(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) - var prog_text := "" - var mode := String(row.get("mode", "")) - if unlocked: - prog_text = "已入手" - elif mode == "keystone": - prog_text = "钥石 %d" % int(row.get("price", 0)) - elif mode == "condition": - var parts: Array[String] = [] - for req_v in (row.get("reqs", []) as Array): - var req: Dictionary[Variant, Variant] = req_v as Dictionary - var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) - prog_text = "、".join(parts) - card.progress_l.text = prog_text + card.progress_l.text = _workshop_progress_text(row, unlocked) # 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读) var st_text := "未解锁" var st_col: Color = UiKit.STONE_DEEP @@ -424,111 +487,148 @@ func refresh_camp(data: Dictionary) -> void: # 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) if card.get("buy_btn") != null: card.buy_btn.disabled = balance < int(card.get("price", 0)) - # 同调装配段状态就地刷(Slots 点亮/满位禁选/Y 升级行余额联动) + + +## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) +func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String: + if unlocked: + return "已入手" + var mode := String(row.get("mode", "")) + if mode == "keystone": + return "钥石 %d" % int(row.get("price", 0)) + if mode == "condition": + var parts: Array[String] = [] + for req_v in (row.get("reqs", []) as Array): + var req: Dictionary[Variant, Variant] = req_v as Dictionary + var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) + return "、".join(parts) + return "" + + +## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷) +func _refresh_attune_segment(data: Dictionary, balance: int) -> void: var att: Dictionary[Variant, Variant] = data.get("attune", {}) - if _attune_header_l != null: + if _attune._attune_header_l != null: var slots_n := int(att.get("slots", 0)) var eq: Array[Variant] = att.get("equipped", []) - _attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] + _attune._attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] var eq_set := {} for v in eq: eq_set[String(v)] = true - for card2 in _attune_btns: + for card2 in _attune._attune_btns: var on: bool = eq_set.has(String(card2.item_id)) card2.btn.text = "卸下" if on else "装配" # 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新) card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) - if _refresh_l != null: + if _side._refresh_l != null: var rf: Dictionary[Variant, Variant] = att.get("refresh", {}) - _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] - _refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) - _refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) + _side._refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] + _side._refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) + _side._refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) func _rebuild_workshop(data: Dictionary) -> void: _workshop_rows.clear() for child in _workshop_holder.get_children(): child.queue_free() - _attune_nodes.clear() - _attune_btns.clear() - _attune_header_l = null - _refresh_l = null - _refresh_btn = null + _attune._attune_nodes.clear() + _attune._attune_btns.clear() + _attune._attune_header_l = null + _side._refresh_l = null + _side._refresh_btn = null for row in data.get("workshop", []): - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 12) - card.add_child(line) - var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) - var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(132, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) - line.add_child(name_l) - var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 - progress_l.custom_minimum_size = Vector2(150, 0) - progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 - progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(progress_l) - # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 - var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) - status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, - # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) - var buy_btn: Button = null - if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": - buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) - line.add_child(buy_btn) - _workshop_holder.add_child(card) - _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))}) + var card := _workshop_card(row) + _workshop_holder.add_child(card["card"]) + _workshop_rows.append(card) _rebuild_attune(data) +## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列) +func _workshop_card(row: Dictionary) -> Dictionary: + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 12) + card.add_child(line) + var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) + var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(132, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) + line.add_child(name_l) + var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 + progress_l.custom_minimum_size = Vector2(150, 0) + progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 + progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(progress_l) + # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 + var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) + status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, + # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) + var buy_btn: Button = null + if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": + buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) + line.add_child(buy_btn) + return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card} + + # 同调装配段重建(秘宝匣柜台尾部段;Slots 点亮+ # 九件行板+Y 升级行,程序占位面) func _rebuild_attune(data: Dictionary) -> void: var att: Dictionary[Variant, Variant] = data.get("attune", {}) - _attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) - _workshop_holder.add_child(_attune_header_l) - _attune_nodes.append(_attune_header_l) + _attune._attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) + _workshop_holder.add_child(_attune._attune_header_l) + _attune._attune_nodes.append(_attune._attune_header_l) for row in att.get("items", []): - var iid := String(row.get("item_id", "")) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(96, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - form_l.custom_minimum_size = Vector2(58, 0) - line.add_child(form_l) - # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) - var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - tier_l.custom_minimum_size = Vector2(36, 0) - line.add_child(tier_l) - var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 - btn.pressed.connect(attune_toggled.emit.bind(iid)) - btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) - line.add_child(btn) + var line := _attune_item_line(row) _workshop_holder.add_child(line) - _attune_nodes.append(line) - _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + _attune._attune_nodes.append(line) + _refresh_attune_tail(att) + + +## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫) +func _attune_item_line(row: Dictionary) -> HBoxContainer: + var iid := String(row.get("item_id", "")) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(96, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + form_l.custom_minimum_size = Vector2(58, 0) + line.add_child(form_l) + # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + tier_l.custom_minimum_size = Vector2(36, 0) + line.add_child(tier_l) + var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 + btn.pressed.connect(attune_toggled.emit.bind(iid)) + btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) + line.add_child(btn) + _attune._attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + return line + + +## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账) +func _refresh_attune_tail(att: Dictionary) -> void: var refresh_line := HBoxContainer.new() refresh_line.add_theme_constant_override("separation", 6) - _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - refresh_line.add_child(_refresh_l) - _refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 - _refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) - refresh_line.add_child(_refresh_btn) + _side._refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + refresh_line.add_child(_side._refresh_l) + _side._refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 + _side._refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) + refresh_line.add_child(_side._refresh_btn) _workshop_holder.add_child(refresh_line) - _attune_nodes.append(refresh_line) + _attune._attune_nodes.append(refresh_line) func _rebuild_recent(data: Dictionary) -> void: @@ -544,7 +644,7 @@ func _rebuild_recent(data: Dictionary) -> void: verdict = "放弃" lines.append("最近冒险:%s · 完成 %d 关 · 钥石 +%d · 种子 %d" % [verdict, int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))]) - _recent_list.text = " | ".join(lines) + _side._recent_list.text = " | ".join(lines) func _emit_closed() -> void: diff --git a/game/presentation/views/confirm_dialog.gd b/game/presentation/views/confirm_dialog.gd index b7816983..5025b5b0 100644 --- a/game/presentation/views/confirm_dialog.gd +++ b/game/presentation/views/confirm_dialog.gd @@ -14,7 +14,7 @@ var _cancel_btn: Button func _init() -> void: var box: VBoxContainer = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(460, 0), 12, - true, UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box + UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box _text = UiKit.label("",UiKit.FS_BODY, false, UiKit.INK_TITLE) _text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _text.custom_minimum_size = Vector2(430, 0) diff --git a/game/presentation/views/curio_pick_overlay.gd b/game/presentation/views/curio_pick_overlay.gd index 46e2eb67..9d187fb7 100644 --- a/game/presentation/views/curio_pick_overlay.gd +++ b/game/presentation/views/curio_pick_overlay.gd @@ -30,7 +30,14 @@ func _init() -> void: # bcard 三竖卡(D1 232×352)+ 底部确认主钮与免费选择注记。差异白名单=免费选择/✓章/无价签。 # 状态机不变:pick→result。M7 同构裁决遗产保留。 var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, false) - var top: HBoxContainer = page.top_row + _build_top_row(page.top_row) + var box: VBoxContainer = page.box + _build_body(box) + visible = false + + +## 顶条域:标题+刷新图标钮(44×44 豁免宽度基准+角标叠加)+刷新余量行 +func _build_top_row(top: HBoxContainer) -> void: top.add_child(UiKit.label("战利品三选一",UiKit.FS_TITLE, true, UiKit.WARM)) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -51,7 +58,10 @@ func _init() -> void: _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.WARM_SOFT) _refresh_l.visible = false top.add_child(_refresh_l) - var box: VBoxContainer = page.box + + +## 主体域:入账信息行+三竖卡列+底部确认/继续钮与免费选择注记 +func _build_body(box: VBoxContainer) -> void: _label = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM_SOFT) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_label) @@ -96,7 +106,7 @@ func open_pick(header: String, cards: Array) -> void: var idl := cid.to_lower() var band := UiKit.tg_band(idl) # 类型色带单点(TG_* 五族同源判定;aoe/blast 归 TG_AOE) # 类型脚注(T7 判官 checkpoint:demo .bcard .src 底部类型行——实机缺=卡下半空底;词=demo 同款) - var card := UiKit.bcard(title, desc, band, _icon_for_id(idl), "", _type_label(idl)) + var card := UiKit.bcard(title, desc, band, _icon_for_id(idl), {"footnote": _type_label(idl)}) card.gui_input.connect(func(ev: InputEvent) -> void: if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: _select_card(cid)) @@ -189,19 +199,15 @@ func _emit_continue_pressed() -> void: # M7:id → 手绘图标键(子串映射容错 id 词表演进;缺位=BCard 空图标槽不炸) +## 图标映射表(id 子串→图标键——判定序 heal/blast/gp/attack/armor/ap 缺省钱袋逐字) func _icon_for_id(idl: String) -> String: - if idl.contains("heal"): - return UiKit.TEX_ICON_MEDKIT - if idl.contains("blast") or idl.contains("magic"): - return UiKit.TEX_ICON_BLAST - if idl.contains("gp") or idl.contains("coin"): - return UiKit.TEX_ICON_COINBAG - if idl.contains("attack"): - return UiKit.TEX_ICON_SWORD - if idl.contains("armor"): - return UiKit.TEX_ICON_SHIELD - if idl.contains("ap") or idl.contains("action"): - return UiKit.TEX_ICON_POUCH + var rules := [[["heal"], UiKit.TEX_ICON_MEDKIT], [["blast", "magic"], UiKit.TEX_ICON_BLAST], + [["gp", "coin"], UiKit.TEX_ICON_COINBAG], [["attack"], UiKit.TEX_ICON_SWORD], + [["armor"], UiKit.TEX_ICON_SHIELD], [["ap", "action"], UiKit.TEX_ICON_POUCH]] + for rule in rules: + for kw in (rule[0] as Array): + if idl.contains(String(kw)): + return String(rule[1]) return UiKit.TEX_ICON_COINBAG diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 19128e92..80e80373 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -10,30 +10,36 @@ signal debug_levels_requested # 两层调试对局入口 signal diagnose_requested # 诊断转储入口(卡死取证编排归组合根) signal closed -var seed_edit: LineEdit # 组合根 _seed_value 唯一读取面 +## 内部状态件(u7 子域拆件——契约 9 职责语句「调试浮窗运行态」):单一变化原因=调试面板状态与 +## 当局数据投影。探针直读冻结成员(seed_edit/_sim_btn/_dbg_btn)住宿主; +## st._run_data/st._run_labels 经 run_data_probe()/update_run_data() 方法出口(字段直访面零外部依赖)。 +class _DebugState extends RefCounted: + var _db + var _base_atk := 0 + var _base_hp := 0 + var _atk: HSlider + var _hp: HSlider + var _atk_val: Label + var _hp_val: Label + var _eff: Label + var _sim_count: SpinBox + var _sim_out: RichTextLabel + var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰) + var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用) + var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入) + var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe) -var _db -var _base_atk := 0 -var _base_hp := 0 -var _atk: HSlider -var _hp: HSlider -var _atk_val: Label -var _hp_val: Label -var _eff: Label -var _sim_count: SpinBox -var _sim_btn: Button -var _dbg_btn: Button # 两层调试入口钮(对局中禁用承载面) -var _sim_out: RichTextLabel -var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰) -var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用) -var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入) -var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe) + +var st := _DebugState.new() +var seed_edit: LineEdit # 组合根 _seed_value 唯一读取面(探针直读冻结) +var _sim_btn: Button # 两层调试批量模拟入口钮(探针直读冻结) +var _dbg_btn: Button # 两层调试入口钮(对局中禁用承载面;探针直读冻结) func _init(db_ref, base_atk: int, base_hp: int) -> void: - _db = db_ref - _base_atk = base_atk - _base_hp = base_hp + st._db = db_ref + st._base_atk = base_atk + st._base_hp = base_hp _build() _sync_labels() visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律) @@ -42,78 +48,78 @@ func _init(db_ref, base_atk: int, base_hp: int) -> void: # ---- 对外语义面(组合根/探针)---- func atk_pct() -> int: - return int(_atk.value) + return int(st._atk.value) func hp_pct() -> int: - return int(_hp.value) + return int(st._hp.value) func set_sliders(atk: int, hp: int) -> void: - _atk.value = atk - _hp.value = hp + st._atk.value = atk + st._hp.value = hp func reset_sliders() -> void: - _atk.value = 0 - _hp.value = 0 + st._atk.value = 0 + st._hp.value = 0 _sync_labels() func effect_text() -> String: - return _eff.text + return st._eff.text func set_sim_count(n: int) -> void: - _sim_count.value = n + st._sim_count.value = n func is_running() -> bool: - return _sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑) + return st._sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑) func set_sim_running(running: bool) -> void: - _sim_running = running + st._sim_running = running _sync_sim_button() # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入):批量模拟与两层调试入口 # 在活跃对局并存态不开放(长程异步模拟/并发对局编排与活跃结算并存态破坏状态机) func set_context_locked(locked: bool) -> void: - _ctx_locked = locked + st._ctx_locked = locked _sync_sim_button() # 当局数据只读投影喂入(六项——组合根 _debug_run_data 构造;纯标签重查零动效) func update_run_data(data: Dictionary) -> void: - _run_data = data.duplicate() - for key in _run_labels: - (_run_labels[key] as Label).text = str(data.get(key, "—")) + st._run_data = data.duplicate() + for key in st._run_labels: + (st._run_labels[key] as Label).text = str(data.get(key, "—")) # 当局数据快照读面(探针值对面——ui_tune_probe 断言六项与只读访问器逐项一致) func run_data_probe() -> Dictionary: - return _run_data.duplicate() + return st._run_data.duplicate() # 两钮同判据(模拟运行 / 对局上下文锁定对「模拟」「两层调试」两入口等价)—— # 判据单点:两 setter 都经此同步,禁在调用点各写一份 func _sync_sim_button() -> void: - _sim_btn.disabled = _sim_running or _ctx_locked - _sim_btn.text = "模拟中…" if _sim_running else "运行模拟" - _dbg_btn.disabled = _sim_running or _ctx_locked + _sim_btn.disabled = st._sim_running or st._ctx_locked + _sim_btn.text = "模拟中…" if st._sim_running else "运行模拟" + _dbg_btn.disabled = st._sim_running or st._ctx_locked func set_sim_progress(text: String) -> void: - _sim_out.text = text + st._sim_out.text = text func set_sim_report(text: String) -> void: - _sim_out.text = text + st._sim_out.text = text func output_text() -> String: - return _sim_out.get_parsed_text() + return st._sim_out.get_parsed_text() # ---- 探针接缝(真实翻译出口——按钮 pressed 信号路径,非直调组合根)---- @@ -141,11 +147,11 @@ func _reset_btn_pressed() -> void: func effective_atk() -> int: - return UiKit.scaled(_base_atk, atk_pct()) + return UiKit.scaled(st._base_atk, atk_pct()) func effective_max_hp() -> int: - return UiKit.scaled(_base_hp, hp_pct()) + return UiKit.scaled(st._base_hp, hp_pct()) # ---- 内部构建(标题/关闭固定于浮窗骨架,可扩展列表区居中滚动)---- @@ -153,15 +159,7 @@ func effective_max_hp() -> int: func _build() -> void: var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(640, 0), 8) var box: VBoxContainer = shell.box - - var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM) - box.add_child(title) - var desc := UiKit.label( - "开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [_base_atk, _base_hp], - 13, false, UiKit.WARM_SOFT) - box.add_child(desc) - box.add_child(HSeparator.new()) - + _build_intro(box) # 可扩展列表页(后续调试项按区块往下追加——超高滚动,滚轮下探) var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(0, 430) @@ -171,10 +169,32 @@ func _build() -> void: list.size_flags_horizontal = Control.SIZE_EXPAND_FILL list.add_theme_constant_override("separation", 8) scroll.add_child(list) + _build_run_data(list) + _build_fx_row(list) + _build_seed_row(list) + _build_slider_block(list) + _build_sim_block(list) + _build_debug_diag(list) + var close_btn := UiKit.button("关闭") + close_btn.pressed.connect(_emit_closed) + box.add_child(close_btn) - # 区块·当局数据(六项只读投影:种子=本局种子唯一查看位〔adventure - # .master_seed 现值,非 _seed_value() 重算〕/关卡/节点/HP/GP/钥石;数据由组合根开窗与模态开合 - # 时 update_run_data 喂入;纯标签零信号连接零动效) + +## 头部域:标题+说明(百分比叠加基础值——影响所有模式,不落盘) +func _build_intro(box: VBoxContainer) -> void: + var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM) + box.add_child(title) + var desc := UiKit.label( + "开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [st._base_atk, st._base_hp], + 13, false, UiKit.WARM_SOFT) + box.add_child(desc) + box.add_child(HSeparator.new()) + + +## 区块·当局数据(六项只读投影:种子=本局种子唯一查看位〔adventure +## .master_seed 现值,非 _seed_value() 重算〕/关卡/节点/HP/GP/钥石;数据由组合根开窗与模态开合 +## 时 update_run_data 喂入;纯标签零信号连接零动效) +func _build_run_data(list: VBoxContainer) -> void: var run_head := UiKit.label("当局数据(只读)",UiKit.FS_HINT, true, UiKit.WARM) list.add_child(run_head) var run_grid := GridContainer.new() @@ -187,11 +207,13 @@ func _build() -> void: cap.custom_minimum_size = Vector2(52, 0) run_grid.add_child(cap) var val := UiKit.label("—",UiKit.FS_HINT, false, UiKit.WARM) - _run_labels[item[0]] = val + st._run_labels[item[0]] = val run_grid.add_child(val) list.add_child(HSeparator.new()) - # 区块⓪胜利演出风格(两套并存——纹章拼合[默认]/尘金粒子;会话级不落盘) + +## 区块⓪胜利演出风格(两套并存——纹章拼合[默认]/尘金粒子;会话级不落盘) +func _build_fx_row(list: VBoxContainer) -> void: var fx_row := HBoxContainer.new() fx_row.add_theme_constant_override("separation", 10) list.add_child(fx_row) @@ -201,8 +223,10 @@ func _build() -> void: fx_btn.pressed.connect(_on_fx_toggle.bind(fx_btn)) # lambda 转命名方法(只读巡检白名单方法名精确可判) fx_row.add_child(fx_btn) - # 区块①随机种子(种子三分判定归组合根 _seed_value 单点): - # 默认空框(空=随全局规则:首局恒 42/非首局随机;显式填入全场景优先) + +## 区块①随机种子(种子三分判定归组合根 _seed_value 单点): +## 默认空框(空=随全局规则:首局恒 42/非首局随机;显式填入全场景优先) +func _build_seed_row(list: VBoxContainer) -> void: var seed_row := HBoxContainer.new() list.add_child(seed_row) seed_row.add_child(UiKit.label("随机种子",UiKit.FS_BODY, false)) @@ -213,16 +237,17 @@ func _build() -> void: seed_row.add_child(seed_edit) list.add_child(HSeparator.new()) - # 区块②角色数值调整板 - var atk_row := _slider_row(list, "攻击加成") - _atk = atk_row.slider - _atk_val = atk_row.value_label - var hp_row := _slider_row(list, "生命加成") - _hp = hp_row.slider - _hp_val = hp_row.value_label - _eff = UiKit.label("",UiKit.FS_HINT, false) - list.add_child(_eff) +## 区块②角色数值调整板(攻击/生命两滑轨+生效值行+应用/重置钮) +func _build_slider_block(list: VBoxContainer) -> void: + var atk_row := _slider_row(list, "攻击加成") + st._atk = atk_row.slider + st._atk_val = atk_row.value_label + var hp_row := _slider_row(list, "生命加成") + st._hp = hp_row.slider + st._hp_val = hp_row.value_label + st._eff = UiKit.label("",UiKit.FS_HINT, false) + list.add_child(st._eff) var btn_row := HBoxContainer.new() list.add_child(btn_row) var apply_btn := UiKit.button("应用并关闭") @@ -233,34 +258,36 @@ func _build() -> void: btn_row.add_child(reset_btn) list.add_child(HSeparator.new()) - # 区块③批量模拟(与 headless 扫描同口径——编排归 tune_sim_controller) + +## 区块③批量模拟(与 headless 扫描同口径——编排归 tune_sim_controller) +func _build_sim_block(list: VBoxContainer) -> void: var sim_row := HBoxContainer.new() list.add_child(sim_row) sim_row.add_child(UiKit.label("批量模拟第一关",UiKit.FS_BODY, false)) - _sim_count = SpinBox.new() - _sim_count.min_value = 10 - _sim_count.max_value = 200 - _sim_count.step = 10 - _sim_count.value = 30 - _sim_count.custom_minimum_size = Vector2(90, 0) - sim_row.add_child(_sim_count) + st._sim_count = SpinBox.new() + st._sim_count.min_value = 10 + st._sim_count.max_value = 200 + st._sim_count.step = 10 + st._sim_count.value = 30 + st._sim_count.custom_minimum_size = Vector2(90, 0) + sim_row.add_child(st._sim_count) sim_row.add_child(UiKit.label("次(理性玩家策略,含面板当前加成)",UiKit.FS_HINT, false)) _sim_btn = UiKit.button("运行模拟") _sim_btn.pressed.connect(_emit_simulate_requested) sim_row.add_child(_sim_btn) - _sim_out = RichTextLabel.new() - _sim_out.bbcode_enabled = true - _sim_out.custom_minimum_size = Vector2(0, 300) - list.add_child(_sim_out) + st._sim_out = RichTextLabel.new() + st._sim_out.bbcode_enabled = true + st._sim_out.custom_minimum_size = Vector2(0, 300) + list.add_child(st._sim_out) list.add_child(HSeparator.new()) - # 区块④两层调试对局入口(HP 关内延续;对局中禁用) + +## 区块④⑤两层调试入口与诊断转储(对局中禁用;转储编排经信号外发归组合根——纯视图边界; +## dump_diagnosis 卡死取证功能保留,不设门控隐藏——本浮窗本就可全局可开,双轨无意义) +func _build_debug_diag(list: VBoxContainer) -> void: _dbg_btn = UiKit.button("调试 · 连续关卡两层(HP 关内延续)") _dbg_btn.pressed.connect(_emit_debug_levels) list.add_child(_dbg_btn) - - # 区块⑤诊断转储:dump_diagnosis 卡死取证功能保留;不设门控隐藏——本浮窗本就可全局可开,双轨无意义。 - # 纯视图边界:转储编排经信号外发归组合根。 var diag_row := HBoxContainer.new() diag_row.add_theme_constant_override("separation", 10) list.add_child(diag_row) @@ -270,10 +297,6 @@ func _build() -> void: diag_btn.pressed.connect(_emit_diagnose_requested) diag_row.add_child(diag_btn) - var close_btn := UiKit.button("关闭") - close_btn.pressed.connect(_emit_closed) - box.add_child(close_btn) - func _slider_row(box: VBoxContainer, title: String) -> Dictionary: var row := HBoxContainer.new() @@ -293,14 +316,14 @@ func _slider_row(box: VBoxContainer, title: String) -> Dictionary: func _sync_labels() -> void: - _atk_val.text = "+%d%%" % atk_pct() - _hp_val.text = "+%d%%" % hp_pct() - _eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [ - _base_atk, effective_atk(), _base_hp, effective_max_hp()] + st._atk_val.text = "+%d%%" % atk_pct() + st._hp_val.text = "+%d%%" % hp_pct() + st._eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [ + st._base_atk, effective_atk(), st._base_hp, effective_max_hp()] func _emit_simulate_requested() -> void: - simulate_requested.emit(int(_sim_count.value)) + simulate_requested.emit(int(st._sim_count.value)) func _emit_diagnose_requested() -> void: diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 7b1c5ca5..7dd765d7 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -30,9 +30,14 @@ func _init() -> void: var col := VBoxContainer.new() col.add_theme_constant_override("separation", 14) # 布局充实(区段间距放大) add_child(col) + _build_data_row(col) + _build_slots(col) + _build_side_sections(col) + +## 数据行域(直角细线数据框——battle.md Component Inventory C 区行规格落形;同时消化纵向空白) +func _build_data_row(col: VBoxContainer) -> void: col.add_child(UiKit.label("探险者",UiKit.FS_SUB, true)) - # 数据行装框(直角细线数据框——battle.md Component Inventory C 区行规格落形;同时消化纵向空白) var data_box := PanelContainer.new() data_box.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(6, 4, 6, 4))) @@ -45,55 +50,60 @@ func _init() -> void: _armor_label = _data_line() data_col.add_child(_armor_label) - # 秘宝槽(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此, - # 不预设交互(挖掘期指向交互归 E 区既有裁决) + +## 秘宝槽域(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此, +## 不预设交互(挖掘期指向交互归 E 区既有裁决) +func _build_slots(col: VBoxContainer) -> void: col.add_child(UiKit.label("秘宝",UiKit.FS_BODY, true)) item_slots = HBoxContainer.new() item_slots.add_theme_constant_override("separation", 10) col.add_child(item_slots) for i in 4: - var slot := PanelContainer.new() - slot.custom_minimum_size = Vector2(48, 48) - slot.add_theme_stylebox_override("panel", - UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4))) - var idx := i - slot.gui_input.connect(func(ev: InputEvent): - if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: - # 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态) - treasure_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0)) - item_slots.add_child(slot) - _slot_panels.append(slot) + _slot_panels.append(_build_slot(i)) - # 特性持有段(三选一特性兑付后常驻可读:逐件行「件名 ×N」;空态整节隐藏。 - # 上移承载——构筑核心信息优先级高于临时强化,且防 C 区下部裁切不可见) - _feat_section = VBoxContainer.new() - _feat_section.add_theme_constant_override("separation", 4) - _feat_section.visible = false - col.add_child(_feat_section) - _feat_section.add_child(UiKit.label("专长",UiKit.FS_BODY, true)) - _feat_list = VBoxContainer.new() - _feat_list.add_theme_constant_override("separation", 2) - _feat_section.add_child(_feat_list) - # 地形变异常驻角标(Boss 图整图型战斗全程可见防中局遗忘; - # 空态整节隐藏=特性段先例;摆位/文案 provisional) - _board_variant_section = VBoxContainer.new() - _board_variant_section.add_theme_constant_override("separation", 4) - _board_variant_section.visible = false - col.add_child(_board_variant_section) - _board_variant_section.add_child(UiKit.label("地形变异",UiKit.FS_BODY, true)) - _board_variant_list = VBoxContainer.new() - _board_variant_list.add_theme_constant_override("separation", 2) - _board_variant_section.add_child(_board_variant_list) - # 特性(预留):锻造/大奖励/关卡奖励节点的属性提升——中循环数据因改变战斗数学准入; - # 与战斗内状态效果(B 区站桩,随战斗消散)分区,防「战斗结束被清」误读;空态整节隐藏 - _curio_section = VBoxContainer.new() - _curio_section.add_theme_constant_override("separation", 4) - _curio_section.visible = false - col.add_child(_curio_section) - _curio_section.add_child(UiKit.label("属性值提升",UiKit.FS_BODY, true)) - _curio_list = VBoxContainer.new() - _curio_list.add_theme_constant_override("separation", 2) - _curio_section.add_child(_curio_list) + +## 单槽构建域:48px 槽牌+点击路由(空槽/越界槽点击=取消选中 handle 0;有效槽发其 handle) +func _build_slot(i: int) -> PanelContainer: + var slot := PanelContainer.new() + slot.custom_minimum_size = Vector2(48, 48) + slot.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4))) + var idx := i + slot.gui_input.connect(func(ev: InputEvent): + if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: + # 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态) + treasure_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0)) + item_slots.add_child(slot) + return slot + + +## 三区段域(特性持有段上移承载——构筑核心信息优先级高于临时强化,且防 C 区下部裁切不可见/ +## 地形变异常驻角标 Boss 图整图型战斗全程可见防中局遗忘/特性预留段防「战斗结束被清」误读;空态整节隐藏) +func _build_side_sections(col: VBoxContainer) -> void: + _feat_section = _labeled_section(col, "专长") + _feat_list = _list_of(_feat_section) + _board_variant_section = _labeled_section(col, "地形变异") + _board_variant_list = _list_of(_board_variant_section) + _curio_section = _labeled_section(col, "属性值提升") + _curio_list = _list_of(_curio_section) + + +## 区段骨架(标题+隐藏容器) +func _labeled_section(col: VBoxContainer, title: String) -> VBoxContainer: + var section := VBoxContainer.new() + section.add_theme_constant_override("separation", 4) + section.visible = false + col.add_child(section) + section.add_child(UiKit.label(title,UiKit.FS_BODY, true)) + return section + + +## 行板容器(separation 2) +func _list_of(section: VBoxContainer) -> VBoxContainer: + var list := VBoxContainer.new() + list.add_theme_constant_override("separation", 2) + section.add_child(list) + return list func set_attack(atk: int, mod_text: String) -> void: @@ -174,46 +184,69 @@ func set_treasure_inventory(items: Array, selected_handle: int) -> void: continue var rec: Dictionary[Variant, Variant] = items[i] _slot_handles.append(int(rec.get("handle", 0))) - var used := bool(rec.get("used", false)) - var usable := bool(rec.get("usable", true)) - var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle - # 槽内容=手绘像素图标(demo ⑦;glyph 字符→图标键映射,未映射/used/不可用回退字形+条件墨色) - var glyph_ch := String(rec.get("glyph", "?")) - var icon_map := {"爆": UiKit.TEX_ICON_BLAST, "医": UiKit.TEX_ICON_MEDKIT, - "铲": UiKit.TEX_ICON_SHOVEL, "袋": UiKit.TEX_ICON_COINBAG, "钥": UiKit.TEX_ICON_KEYSHARD, - "剑": UiKit.TEX_ICON_SWORD, "盾": UiKit.TEX_ICON_SHIELD, "心": UiKit.TEX_ICON_HEART, - "行": UiKit.TEX_ICON_POUCH} - var icon_path: String = icon_map.get(glyph_ch, "") - var itex := UiKit.load_tex(icon_path) if icon_path != "" else null - if itex != null and not used and usable: - var tr := TextureRect.new() - tr.texture = itex - tr.custom_minimum_size = Vector2(40, 40) - tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - tr.set_anchors_preset(Control.PRESET_FULL_RECT) - tr.mouse_filter = Control.MOUSE_FILTER_IGNORE - slot.add_child(tr) - else: - var glyph := UiKit.label(glyph_ch,UiKit.FS_SUB, true) - glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - glyph.set_anchors_preset(Control.PRESET_FULL_RECT) - glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE - if used: - # 「已用」占位墨=灰石深化(STONE_FAINT 对 PANEL_DATA 槽牌 2.7:1; - # STONE_DEEP = 5.6:1;语义分档保持双墨) - glyph.add_theme_color_override("font_color", UiKit.STONE_DEEP) - elif not usable: - # 「不可用」置灰墨=标签深墨(STONE 对槽牌 3.4:1;INK_LABEL = 10.4:1,虚位/失效更强置灰语义) - glyph.add_theme_color_override("font_color", UiKit.INK_LABEL) - slot.add_child(glyph) - var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER - slot.add_theme_stylebox_override("panel", - UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4))) - # note=积点进度行(自动兑付件悬停浮窗补「积点 N/M」,进度随挖格经组合根重查刷新) - var note := String(rec.get("note", "")) - slot.tooltip_text = "%s%s%s" % [String(rec.get("name", "")), - "(%s)" % note if note != "" else "", - "(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")] + _fill_slot(slot, rec, selected_handle) + + +## 单槽内容填充域:图标优先(像素图标未映射/used/不可用回退字形+条件墨色)+选中金框+悬停浮窗 +func _fill_slot(slot: PanelContainer, rec: Dictionary, selected_handle: int) -> void: + var used := bool(rec.get("used", false)) + var usable := bool(rec.get("usable", true)) + var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle + # 槽内容=手绘像素图标(demo ⑦;glyph 字符→图标键映射,未映射/used/不可用回退字形+条件墨色) + var itex := _slot_icon_tex(String(rec.get("glyph", "?"))) + if itex != null and not used and usable: + slot.add_child(_slot_icon_rect(itex)) + else: + slot.add_child(_slot_glyph_label(String(rec.get("glyph", "?")), used, usable)) + var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER + slot.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4))) + slot.tooltip_text = _slot_tooltip(rec, used, usable) + + +## 字形→图标纹理映射(demo ⑦ 键表单点) +func _slot_icon_tex(glyph_ch: String) -> Texture2D: + var icon_map := {"爆": UiKit.TEX_ICON_BLAST, "医": UiKit.TEX_ICON_MEDKIT, + "铲": UiKit.TEX_ICON_SHOVEL, "袋": UiKit.TEX_ICON_COINBAG, "钥": UiKit.TEX_ICON_KEYSHARD, + "剑": UiKit.TEX_ICON_SWORD, "盾": UiKit.TEX_ICON_SHIELD, "心": UiKit.TEX_ICON_HEART, + "行": UiKit.TEX_ICON_POUCH} + var icon_path: String = icon_map.get(glyph_ch, "") + return UiKit.load_tex(icon_path) if icon_path != "" else null + + +## 图标矩形域(40px 素描框,像素过滤) +func _slot_icon_rect(itex: Texture2D) -> TextureRect: + var tr := TextureRect.new() + tr.texture = itex + tr.custom_minimum_size = Vector2(40, 40) + tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + tr.set_anchors_preset(Control.PRESET_FULL_RECT) + tr.mouse_filter = Control.MOUSE_FILTER_IGNORE + return tr + + +## 字形回退标签域(used=灰石深化墨 5.6:1/不可用=标签深墨 10.4:1 虚位失效更强置灰——语义分档双墨) +func _slot_glyph_label(glyph_ch: String, used: bool, usable: bool) -> Label: + var glyph := UiKit.label(glyph_ch,UiKit.FS_SUB, true) + glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + glyph.set_anchors_preset(Control.PRESET_FULL_RECT) + glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE + if used: + # 「已用」占位墨=灰石深化(STONE_FAINT 对 PANEL_DATA 槽牌 2.7:1; + # STONE_DEEP = 5.6:1;语义分档保持双墨) + glyph.add_theme_color_override("font_color", UiKit.STONE_DEEP) + elif not usable: + # 「不可用」置灰墨=标签深墨(STONE 对槽牌 3.4:1;INK_LABEL = 10.4:1,虚位/失效更强置灰语义) + glyph.add_theme_color_override("font_color", UiKit.INK_LABEL) + return glyph + + +## 悬停浮窗域(note=积点进度行——自动兑付件悬停浮窗补「积点 N/M」,进度随挖格经组合根重查刷新) +func _slot_tooltip(rec: Dictionary, used: bool, usable: bool) -> String: + var note := String(rec.get("note", "")) + return "%s%s%s" % [String(rec.get("name", "")), + "(%s)" % note if note != "" else "", + "(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")] diff --git a/game/presentation/views/legend_overlay.gd b/game/presentation/views/legend_overlay.gd index 08454647..238d4c0c 100644 --- a/game/presentation/views/legend_overlay.gd +++ b/game/presentation/views/legend_overlay.gd @@ -23,7 +23,7 @@ var _rap_cost := -1 # <0 = 不渲染 RAP 行(构造参数注表值,禁字 func _init(rap_cost: int = -1) -> void: _rap_cost = rap_cost - var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6, true, + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6, UiKit.panel_skinned(Vector4(12, 10, 12, 10))) # 石板皮(T2;缺位回退 flat) _panel = shell.panel (shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input) @@ -32,17 +32,7 @@ func _init(rap_cost: int = -1) -> void: box.add_child(title) box.add_child(HSeparator.new()) for e in ENTRIES: - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 8) - var chip := ColorRect.new() - chip.custom_minimum_size = Vector2(18, 18) - chip.color = Color(e[0]) - row.add_child(chip) - var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) - lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - lab.custom_minimum_size = Vector2(440, 0) - row.add_child(lab) - box.add_child(row) + box.add_child(_legend_row(e)) box.add_child(HSeparator.new()) # RAP 行(表值驱动,禁字面量:刷新代价读行动经济表) if _rap_cost >= 0: @@ -64,6 +54,24 @@ func _init(rap_cost: int = -1) -> void: visible = false + +## 单行域(循环体提函数——色块 chip+词条 18px 网格对齐) +func _legend_row(e: Array) -> HBoxContainer: + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 8) + var chip := ColorRect.new() + chip.custom_minimum_size = Vector2(18, 18) + chip.color = Color(e[0]) + row.add_child(chip) + var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) + lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + lab.custom_minimum_size = Vector2(440, 0) + row.add_child(lab) + return row + + + + func is_open() -> bool: return visible diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index 65a53761..9f53deaa 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -31,43 +31,47 @@ const HIT_PAD := 6.0 # 图上命中外扩(指 const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落) const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit) -var _runner = null -var _title: Label -var _canvas: Control -var _level_label: Label -var _gp_value: Label -var _hp_value: Label -var _hp_bar: Control -var _attr_value: Label -var _bonus_row: HBoxContainer -var _bonus_value: Label -var _feat_value: Label # 特性持有行(组合根投喂逐件文案) -var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集) -var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它) -var _hover_id := -1 # 悬停预承诺候选(-1 = 无) -var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) -var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根) -var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) -var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) -var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) -var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点) -var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用) -var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效) +## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因= +## 选路呈现)。_layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。 +## 探针直读冻结成员(_cand_ids/_hover_id/_content_size/_layout_cache/abandon_btn/status_badge)住宿主。 +class _MapState extends RefCounted: + var _runner = null + var _title: Label + var _canvas: Control + var _level_label: Label + var _gp_value: Label + var _hp_value: Label + var _hp_bar: Control + var _attr_value: Label + var _bonus_row: HBoxContainer + var _bonus_value: Label + var _feat_value: Label # 特性持有行(组合根投喂逐件文案) + var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) + var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) + var _layer_decor := {"node_icons": {}, "noise_rect": null, "tint_level": 0} # 换层装饰域(节点图标贴图 load_tex 守卫缺位 null→字形回退/屏底石纹面/当前已应用 tint 层号 0=未应用) + +var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主) +var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结) +var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结) +var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结) +var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结) +var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结) +var _map := _MapState.new() func _init() -> void: - _abandon_timer.expired.connect(_reset_abandon_confirm) + _map._abandon_timer.expired.connect(_reset_abandon_confirm) # 节点图标贴图装载(load_tex 守卫,缺位回退现行字形) for t in TEX_NODE_ICONS: - _node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) + _map._layer_decor.node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) # 独立屏底(spec Layout——demo #v-map 暖棕竖向 # 渐变(#4A3B2A 0%→#3A2E22 40%)+石纹弱质感 0.14+暗角聚焦;底纹缺位回退纯色) - var base := UiKit.gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) + var base := UiKit._gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) add_child(base) var tex := UiKit.backdrop_tile() if tex != null: base.add_child(tex) - _noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) + _map._layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) base.add_child(UiKit.vignette_rect()) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -75,9 +79,16 @@ func _init() -> void: var box := VBoxContainer.new() box.add_theme_constant_override("separation", 10) center.add_child(box) - # 顶栏语境(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框; - # 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ - # 左右两极排布) + _build_top_bar(box) + _build_canvas(box) + _build_status_bar(box) + visible = false + + +## 顶栏域(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框; +## 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ 左右两极排布; +## 状态角标置灰守卫归组合根 _sync_status_entry,信号连接归组合根——视图零连接) +func _build_top_bar(box: VBoxContainer) -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 12) top.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -86,8 +97,8 @@ func _init() -> void: lvl_plate.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1, Vector4(12, 3, 12, 3))) top.add_child(lvl_plate) - _level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) - lvl_plate.add_child(_level_label) + _map._level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) + lvl_plate.add_child(_map._level_label) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(top_sp) @@ -98,31 +109,34 @@ func _init() -> void: var gp_row := HBoxContainer.new() gp_row.add_theme_constant_override("separation", 6) gp_box.add_child(gp_row) - _gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) - gp_row.add_child(_gp_value) - # 状态面板入口角标(顶栏常驻;本屏只留状态——调试角标归全局浮层。 - # 置灰守卫归组合根 _sync_status_entry(其他模态开着=不可点;本屏自身的登记开态不计——has_open_excluding); - # 信号连接归组合根(视图零连接)) + _map._gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) + gp_row.add_child(_map._gp_value) status_badge = UiKit.button("状态", 0.0) status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER top.add_child(status_badge) - _title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) - _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - box.add_child(_title) - # 节点图画布(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) - _canvas = Control.new() - _canvas.custom_minimum_size = Vector2(620, 240) - _canvas.mouse_filter = Control.MOUSE_FILTER_STOP - _canvas.draw.connect(_draw_graph.bind(_canvas)) - _canvas.gui_input.connect(_on_canvas_input) - box.add_child(_canvas) - # 底部状态栏(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; - # 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); - # _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留) - # 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: - # 特性持有行限宽截断+tooltip 全文) + _map._title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) + _map._title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + box.add_child(_map._title) + + +## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) +func _build_canvas(box: VBoxContainer) -> void: + _map._canvas = Control.new() + _map._canvas.custom_minimum_size = Vector2(620, 240) + _map._canvas.mouse_filter = Control.MOUSE_FILTER_STOP + _map._canvas.draw.connect(_draw_graph.bind(_map._canvas)) + _map._canvas.gui_input.connect(_on_canvas_input) + box.add_child(_map._canvas) + + +## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; +## 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); +## _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留。 +## 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: +## 特性持有行限宽截断+tooltip 全文)) +func _build_status_bar(box: VBoxContainer) -> void: var bottom := PanelContainer.new() bottom.custom_minimum_size = Vector2(0, 85) bottom.size_flags_vertical = Control.SIZE_SHRINK_END @@ -132,9 +146,37 @@ func _init() -> void: var strip := HBoxContainer.new() strip.add_theme_constant_override("separation", 18) bottom.add_child(strip) + strip.add_child(_build_face_rect()) + var life_box := VBoxContainer.new() + strip.add_child(life_box) + life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) + _map._hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + life_box.add_child(_map._hp_value) + _map._hp_bar = Control.new() + _map._hp_bar.custom_minimum_size = Vector2(130, 7) + _map._hp_bar.draw.connect(_draw_hp_bar.bind(_map._hp_bar)) + life_box.add_child(_map._hp_bar) + strip.add_child(_vdiv()) + var attr_box := VBoxContainer.new() + strip.add_child(attr_box) + attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) + _map._attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + attr_box.add_child(_map._attr_value) + strip.add_child(_vdiv()) + _build_bonus_group(strip) + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + strip.add_child(spacer) + abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38 + abandon_btn.pressed.connect(_on_abandon_pressed) + abandon_btn.visible = false + strip.add_child(abandon_btn) + + +## 头像域:定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) +func _build_face_rect() -> TextureRect: var face := TextureRect.new() var face_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) - # 定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) var face_h := 48.0 var face_w := face_h if face_tex != null: @@ -145,50 +187,26 @@ func _init() -> void: face.expand_mode = TextureRect.EXPAND_IGNORE_SIZE face.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED face.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - strip.add_child(face) - var life_box := VBoxContainer.new() - strip.add_child(life_box) - life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - life_box.add_child(_hp_value) - _hp_bar = Control.new() - _hp_bar.custom_minimum_size = Vector2(130, 7) - _hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar)) - life_box.add_child(_hp_bar) - strip.add_child(_vdiv()) - var attr_box := VBoxContainer.new() - strip.add_child(attr_box) - attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - attr_box.add_child(_attr_value) - strip.add_child(_vdiv()) - # 本关加成框(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂) - _bonus_row = HBoxContainer.new() - strip.add_child(_bonus_row) + return face + + +## 本关加成框域(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂;限宽单行截断+tooltip 全文) +func _build_bonus_group(strip: HBoxContainer) -> void: + _map._bonus_row = HBoxContainer.new() + strip.add_child(_map._bonus_row) var bonus_box := VBoxContainer.new() - _bonus_row.add_child(bonus_box) + _map._bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) var bonus_cap := UiKit.label("属性值提升",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读) bonus_inner.add_child(bonus_cap) - _bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) - bonus_inner.add_child(_bonus_value) - # 特性持有行(组合根投喂) - # 限宽单行截断+tooltip 全文; - # 墨色 INK_HINT(暖白系对亮板 ≈1:1 不可读——§7.2) - _feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) - _feat_value.custom_minimum_size = Vector2(240, 0) - _feat_value.clip_text = true - _feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - bonus_box.add_child(_feat_value) - var spacer := Control.new() - spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - strip.add_child(spacer) - abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38 - abandon_btn.pressed.connect(_on_abandon_pressed) - abandon_btn.visible = false - strip.add_child(abandon_btn) - visible = false + _map._bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) + bonus_inner.add_child(_map._bonus_value) + _map._feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) + _map._feat_value.custom_minimum_size = Vector2(240, 0) + _map._feat_value.clip_text = true + _map._feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + bonus_box.add_child(_map._feat_value) # 竖分隔线(底栏分组线——亮面板上低对比细线) @@ -202,10 +220,10 @@ static func _vdiv() -> ColorRect: func _draw_hp_bar(bar: Control) -> void: # 迷你血条(深底 UiKit.HP_BAR_BACK #28221C + 小人蓝填充——与战斗站桩血条同语言, # 「蓝只说己方」§4.2)。比例 = hero_hp / effective_max_hp - if _runner == null: + if _map._runner == null: return - var max_hp := maxi(1, _runner.effective_max_hp()) - var ratio := clampf(float(_runner.hero_hp) / float(max_hp), 0.0, 1.0) + var max_hp := maxi(1, _map._runner.effective_max_hp()) + var ratio := clampf(float(_map._runner.hero_hp) / float(max_hp), 0.0, 1.0) var full := Rect2(Vector2.ZERO, bar.size) bar.draw_rect(full, COL_HP_BAR_BACK) if ratio > 0.0: @@ -215,8 +233,8 @@ func _draw_hp_bar(bar: Control) -> void: # 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险) # 特性持有投影(组合根投喂逐件文案;空文案=行隐藏) func set_feat_holdings(text: String) -> void: - _feat_value.text = text - _feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) + _map._feat_value.text = text + _map._feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) func set_abandon_available(v: bool) -> void: @@ -226,8 +244,8 @@ func set_abandon_available(v: bool) -> void: # 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。 # 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。 func _on_abandon_pressed() -> void: - if not _abandon_timer.is_armed(): - _abandon_timer.arm(CONFIRM_WINDOW_S) + if not _map._abandon_timer.is_armed(): + _map._abandon_timer.arm(CONFIRM_WINDOW_S) abandon_btn.text = "再点一次确认放弃——不可逆" if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) # 减动效静态基色(脉冲层不再驱动) @@ -237,45 +255,45 @@ func _on_abandon_pressed() -> void: func _reset_abandon_confirm() -> void: - _abandon_timer.disarm() + _map._abandon_timer.disarm() abandon_btn.modulate = Color(1, 1, 1, 1) abandon_btn.text = "放弃" # 文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed) func _process(delta: float) -> void: - if not _abandon_timer.is_armed(): + if not _map._abandon_timer.is_armed(): return - _abandon_timer.tick(delta) - if not _abandon_timer.is_armed(): + _map._abandon_timer.tick(delta) + if not _map._abandon_timer.is_armed(): return # 窗口到点回落(expired 信号已驱动 _reset_abandon_confirm) # 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪——档判据单点 motion_still) if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) else: - var k := 0.5 + 0.5 * sin(_abandon_timer.elapsed * TAU) + var k := 0.5 + 0.5 * sin(_map._abandon_timer.elapsed * TAU) abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k) # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新) func open_choices(cands: Array, runner: LevelRunner) -> void: var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入) - _runner = runner + _map._runner = runner _hover_id = -1 _reset_abandon_confirm() var visited: Array[int] = [] if runner != null: visited = runner.visited_ids() if cands.size() > 1: - _title.text = TITLE_FORK + _map._title.text = TITLE_FORK elif visited.is_empty(): - _title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) + _map._title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) else: - _title.text = TITLE_SINGLE + _map._title.text = TITLE_SINGLE _cand_ids = [] for cand in cands: _cand_ids.append(_id_of(cand)) - _fit_canvas(_runner.nodes()) - _apply_level_tint(int(_runner.level_row.get("level_index", 0))) + _fit_canvas(_map._runner.nodes()) + _apply_level_tint(int(_map._runner.level_row.get("level_index", 0))) visible = true if not was_visible: # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) @@ -302,7 +320,7 @@ func _fit_canvas(nodes: Array) -> void: _content_size = Vector2(LAYER_BASE_X + maxl * LAYER_STEP_X + NODE_HALF.x + 8.0, SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行 _content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行) - _canvas.custom_minimum_size = _content_size + _map._canvas.custom_minimum_size = _content_size # 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的 @@ -313,39 +331,39 @@ const BOARD_TINTS := {1: Color(1, 1, 1), 2: Color(0.84, 0.99, 1.28), 3: Color(0. func _apply_level_tint(level_index: int) -> void: - if _noise_rect == null or level_index == _tint_level: + if _map._layer_decor.noise_rect == null or level_index == _map._layer_decor.tint_level: return - _tint_level = level_index + _map._layer_decor.tint_level = level_index var target: Color = BOARD_TINTS.get(level_index, Color(1, 1, 1)) var tw := create_tween() - tw.tween_property(_noise_rect, "modulate", + tw.tween_property(_map._layer_decor.noise_rect, "modulate", Color(target.r, target.g, target.b, UiKit.BACKDROP_TILE_ALPHA), UiKit.motion(0.45)) # 0.14 弱质感档随 tint 保持 # 状态栏投喂(命令式纪律——组合根经 level_map_status_payload 取成品后喂入;零推送信号订阅、零核心态拼装) func apply_status(payload: Dictionary) -> void: - _level_label.text = String(payload["level_label"]) - _gp_value.text = String(payload["gp_text"]) - _hp_value.text = String(payload["hp_text"]) + _map._level_label.text = String(payload["level_label"]) + _map._gp_value.text = String(payload["gp_text"]) + _map._hp_value.text = String(payload["hp_text"]) if bool(payload["hp_bar_redraw"]): - _hp_bar.queue_redraw() - _attr_value.text = String(payload["attr_text"]) - _bonus_value.text = String(payload["bonus_text"]) + _map._hp_bar.queue_redraw() + _map._attr_value.text = String(payload["attr_text"]) + _map._bonus_value.text = String(payload["bonus_text"]) # 特性持有行可见性(与加成行并框——任一非空整框显示;文案归组合根 set_feat_holdings) - _bonus_row.visible = bool(payload["bonus_row_visible"]) or _feat_value.text != "" + _map._bonus_row.visible = bool(payload["bonus_row_visible"]) or _map._feat_value.text != "" # ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ---- func _on_canvas_input(ev: InputEvent) -> void: - if _runner == null or not visible: + if _map._runner == null or not visible: return if ev is InputEventMouseMotion: var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立) if nid != _hover_id: _hover_id = nid - _canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW - _canvas.queue_redraw() + _map._canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW + _map._canvas.queue_redraw() elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed: var nid2 := _candidate_at(ev.position) if nid2 >= 0: @@ -354,10 +372,10 @@ func _on_canvas_input(ev: InputEvent) -> void: # 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应 func _candidate_at(local_pos: Vector2) -> int: - if _runner == null: + if _map._runner == null: return -1 - var nodes: Array[Variant] = _runner.nodes() - var pos := _layout(nodes, _canvas) + var nodes: Array[Variant] = _map._runner.nodes() + var pos := _layout(nodes, _map._canvas) for nid in _cand_ids: var node := _node_by_id(nid, nodes) if node.is_empty(): @@ -372,7 +390,7 @@ func _candidate_at(local_pos: Vector2) -> int: func _submit_choice(nid: int) -> void: # 独立屏分支:战斗/Boss 节点 = 出屏切回战斗屏;奖励/恢复/商店节点 = # 本屏保持激活——兑付覆盖层由节点事件在本屏域内弹出(模态栈遮挡恢复语义复用),兑付确认后回图刷新 - var node := _node_by_id(nid, _runner.nodes()) + var node := _node_by_id(nid, _map._runner.nodes()) var t := String(node.get("type", "")) if t != "reward" and t != "rest" and t != "shop": visible = false @@ -381,13 +399,13 @@ func _submit_choice(nid: int) -> void: # 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造) func press_candidate(index: int) -> void: - if index < 0 or index >= _cand_ids.size() or _runner == null: + if index < 0 or index >= _cand_ids.size() or _map._runner == null: return - var nodes: Array[Variant] = _runner.nodes() + var nodes: Array[Variant] = _map._runner.nodes() var node := _node_by_id(int(_cand_ids[index]), nodes) if node.is_empty(): return - var pos: Vector2 = _layout(nodes, _canvas)[node] + var pos: Vector2 = _layout(nodes, _map._canvas)[node] var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块) if nid >= 0: _submit_choice(nid) @@ -395,7 +413,7 @@ func press_candidate(index: int) -> void: func _id_of(node: Dictionary) -> int: # 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth) - return _runner.node_id_of(node) + return _map._runner.node_id_of(node) # ---- 节点呈现决策面(信息迷雾 v2.1:类型仅在可见集内出示,Boss 标记不入雾)---- @@ -421,20 +439,48 @@ static func node_display(node: Dictionary, nid: int, type_vis: Array, # ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)---- func _draw_graph(canvas: Control) -> void: - if _runner == null or not visible: + if _map._runner == null or not visible: return - _drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) - var nodes: Array[Variant] = _runner.nodes() - var pos := _layout(nodes, canvas) - var font := get_theme_default_font() # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) - var current_id: int = _runner.current_node_id() - # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 - var visited: Array[Variant] = _runner.visited_ids() - var cand_ids: Array[Variant] = _runner.next_candidate_ids() - var type_vis: Array[int] = _runner.type_visible_ids() # 本停点类型可见集(每帧一取——纯函数零状态) - # 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗) - # 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 - # 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) + _map._drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) + var ctx := _draw_context(canvas) + _draw_passage_layer(canvas, ctx) + for node in ctx["nodes"]: + _draw_room(canvas, node, ctx) + + +## 绘制上下文束(单次取数——布局/字体/编号空间/可见集,绘制域共享只读面) +func _draw_context(canvas: Control) -> Dictionary: + var nodes: Array[Variant] = _map._runner.nodes() + return { + "nodes": nodes, + "pos": _layout(nodes, canvas), + "font": get_theme_default_font(), # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) + "current_id": _map._runner.current_node_id(), + # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 + "visited": _map._runner.visited_ids(), + "cand_ids": _map._runner.next_candidate_ids(), + "type_vis": _map._runner.type_visible_ids(), # 本停点类型可见集(每帧一取——纯函数零状态) + } + + +## 房间底色域(行进态三色:已到暗金/当前奖励金/未达暗——绘制域与图标域共享) +func _room_color(node: Dictionary, ctx: Dictionary) -> Color: + var nid := _id_of(node) + var col := COL_ROOM_FUTURE + if (ctx["visited"] as Array).has(nid): + col = COL_ROOM_DONE + if nid == ctx["current_id"]: + col = COL_ROOM_CURRENT + return col + + +## 通道层绘制域:连线先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)。 +## 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 +## 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) +func _draw_passage_layer(canvas: Control, ctx: Dictionary) -> void: + var nodes: Array = ctx["nodes"] + var pos: Dictionary = ctx["pos"] + var cand_ids: Array = ctx["cand_ids"] var dim_passages: bool = not cand_ids.is_empty() for node in nodes: var from: Vector2 = pos[node] @@ -442,61 +488,74 @@ func _draw_graph(canvas: Control) -> void: var target := _node_by_id(tid, nodes) if target.is_empty(): continue - var to: Vector2 = pos[target] - var lit: bool = _id_of(node) == current_id and cand_ids.has(tid) - var hovered: bool = lit and _hover_id == tid + var lit: bool = _id_of(node) == ctx["current_id"] and cand_ids.has(tid) var col := COL_PASSAGE_LIT if lit else (Color(COL_PASSAGE, 0.35) if dim_passages else COL_PASSAGE) - var width := 4.0 if hovered else (3.0 if lit else 2.0) # 2/3/4 三档 - _draw_passage(canvas, from, to, col, width) - # 房间方块 - for node in nodes: - var p: Vector2 = pos[node] - var t := String(node.get("type", "")) - var nid := _id_of(node) - var col := COL_ROOM_FUTURE - if visited.has(nid): - col = COL_ROOM_DONE - if nid == current_id: - col = COL_ROOM_CURRENT - var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) - canvas.draw_rect(rect, col) - var disp := node_display(node, nid, type_vis, nid == current_id, cand_ids.has(nid)) - var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, - "label_text": "", "boss_border": false} - if bool(disp["boss_border"]): - # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: - # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) - canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) - canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) - rec["boss_border"] = true - elif cand_ids.has(nid): - # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) - canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) - # 节点图标(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; - # 贴图缺位 → 字形回退=贴图在位字形退役惯例) - if bool(disp["draw_icon"]): - rec["draw_icon"] = true - var icon: Texture2D = _node_icons.get(t) - if icon != null: - var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) - canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, - Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) - else: - var label := String(NODE_LABELS.get(t, "?")) - canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) - # 编号(右上角——与图标占位区零重叠;双色墨随态) - var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM - canvas.draw_string(font, Vector2(rect.end.x - 16.0, rect.position.y + 8.0), - str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) - # 类型文字标签(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 - # 其余=类型名暖米弱化;文字+图标+形状三通道) - if bool(disp["draw_label"]): - rec["draw_label"] = true - rec["label_text"] = String(disp["label_text"]) - var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT) - canvas.draw_string(font, Vector2(p.x - 44.0, rect.end.y + 15.0), - String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) - _drawn_disp[nid] = rec + var width := 4.0 if lit and _hover_id == tid else (3.0 if lit else 2.0) # 2/3/4 三档 + _draw_passage(canvas, from, pos[target], col, width) + + +## 房间方块域:底色随行进态+Boss/候选描边+图标/字形+编号+类型文字标签 +func _draw_room(canvas: Control, node: Dictionary, ctx: Dictionary) -> void: + var pos: Dictionary = ctx["pos"] + var p: Vector2 = pos[node] + var t := String(node.get("type", "")) + var nid := _id_of(node) + var col := _room_color(node, ctx) + var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) + canvas.draw_rect(rect, col) + var disp := node_display(node, nid, ctx["type_vis"], nid == ctx["current_id"], (ctx["cand_ids"] as Array).has(nid)) + var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, + "label_text": "", "boss_border": false} + if bool(disp["boss_border"]): + # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: + # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) + canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) + canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) + rec["boss_border"] = true + elif (ctx["cand_ids"] as Array).has(nid): + # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) + canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) + if _draw_room_icon(canvas, node, disp, ctx): + rec["draw_icon"] = true + # 编号(右上角——与图标占位区零重叠;双色墨随态) + var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM + canvas.draw_string(ctx["font"], Vector2(rect.end.x - 16.0, rect.position.y + 8.0), + str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) + var label_rec := _draw_room_label(canvas, rect, disp, nid, ctx) + rec["draw_label"] = bool(label_rec.get("draw_label", false)) + rec["label_text"] = String(label_rec.get("label_text", "")) + _map._drawn_disp[nid] = rec + + +## 节点图标域(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; +## 贴图缺位 → 字形回退=贴图在位字形退役惯例) +func _draw_room_icon(canvas: Control, node: Dictionary, disp: Dictionary, ctx: Dictionary) -> bool: + if not bool(disp["draw_icon"]): + return false + var p: Vector2 = ctx["pos"][node] + var col := _room_color(node, ctx) + var t := String(node.get("type", "")) + var icon: Texture2D = _map._layer_decor.node_icons.get(t) + if icon != null: + var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) + canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, + Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) + else: + var label := String(NODE_LABELS.get(t, "?")) + canvas.draw_string(ctx["font"], p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) + return true + + +## 类型文字标签域(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 +## 其余=类型名暖米弱化;文字+图标+形状三通道) +func _draw_room_label(canvas: Control, rect: Rect2, disp: Dictionary, nid: int, ctx: Dictionary) -> Dictionary: + if not bool(disp["draw_label"]): + return {} + var p: Vector2 = rect.position + NODE_HALF # 节点中心反推(方块原点=中心−半宽高) + var type_ink := COL_ROOM_CURRENT if nid == ctx["current_id"] else (UiKit.CANDLE if (ctx["cand_ids"] as Array).has(nid) else UiKit.WARM_FAINT) + canvas.draw_string(ctx["font"], Vector2(p.x - 44.0, rect.end.y + 15.0), + String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) + return {"draw_label": true, "label_text": String(disp["label_text"])} func _layout(nodes: Array, canvas: Control) -> Dictionary: @@ -520,20 +579,20 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary: # ---- 探针只读出口(形态仿 fx_probe 先例:只读快照) ---- -# 选路布局只读快照(node → 局部位置;画布固定取现行 _canvas,调用侧零私有域触达) +# 选路布局只读快照(node → 局部位置;画布固定取现行 _map._canvas,调用侧零私有域触达) func layout_probe(nodes: Array) -> Dictionary: - return _layout(nodes, _canvas) + return _layout(nodes, _map._canvas) # 路由画布只读快照(origin=全局原点;min_size=最小拟合尺寸——画布拟合断言消费面) func canvas_probe() -> Dictionary: - return {"origin": _canvas.get_global_position(), "min_size": _canvas.custom_minimum_size} + return {"origin": _map._canvas.get_global_position(), "min_size": _map._canvas.custom_minimum_size} # 类型可见性绘制只读记录(id → 本帧实际绘制事实:图标/文字/描边/文案;_draw_graph 在实际绘制点写入—— # 接线层断言消费点:绕开决策面直接画,这里就露出「雾下也画了」) func display_probe() -> Dictionary: - return _drawn_disp + return _map._drawn_disp # 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层 @@ -577,7 +636,7 @@ func _entry(nodes: Array) -> Dictionary: func _out_ids(node: Dictionary, _nodes: Array) -> Array: - return _runner.node_out_ids(node) + return _map._runner.node_out_ids(node) func _node_by_id(nid: int, nodes: Array) -> Dictionary: diff --git a/game/presentation/views/node_overlay.gd b/game/presentation/views/node_overlay.gd index e82f854c..cabfcdf5 100644 --- a/game/presentation/views/node_overlay.gd +++ b/game/presentation/views/node_overlay.gd @@ -11,7 +11,7 @@ var _btn: Button func _init() -> void: # M5(UI 落码批):rest 兑付面板 plate9 石板化+暖金头带字(demo ④休息弹层;暗字上亮板 §7.2) - var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, UiKit.panel_skinned(Vector4(26, 18, 26, 18))) var box: VBoxContainer = shell.box _label = UiKit.label("",UiKit.FS_SUB, true, UiKit.INK_TITLE) diff --git a/game/presentation/views/settings_overlay.gd b/game/presentation/views/settings_overlay.gd index adfe0d12..8850af4e 100644 --- a/game/presentation/views/settings_overlay.gd +++ b/game/presentation/views/settings_overlay.gd @@ -34,10 +34,18 @@ func _build() -> void: # 减动效与结算演出动画=同一设置,一行承载。 # 两列对齐(label 列定宽 140——demo .setrow label/控制两列)+ # 后果说明内联随行(demo .why 同排)+返回钮紧凑化(demo .btn9 紧凑)。 - var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, UiKit.panel_skinned(Vector4(20, 16, 20, 16))) var box: VBoxContainer = shell.box + _build_head(box) + _build_volume_row(box) + _build_fullscreen_row(box) + _build_motion_row(box) + _build_close_row(box) + +## 头部域(设置项零增减——只有主音量;减动效与结算演出动画=同一设置,一行承载) +func _build_head(box: VBoxContainer) -> void: var title := UiKit.label("设置",UiKit.FS_TITLE, true, UiKit.INK_TITLE) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) @@ -46,7 +54,9 @@ func _build() -> void: box.add_child(hint) box.add_child(HSeparator.new()) - # 音量(master bus 实时生效——入口归设置、生效口径归 音频.md) + +## 音量行(master bus 实时生效——入口归设置、生效口径归 音频.md) +func _build_volume_row(box: VBoxContainer) -> void: var vol_row := HBoxContainer.new() vol_row.add_theme_constant_override("separation", 14) box.add_child(vol_row) @@ -66,38 +76,44 @@ func _build() -> void: _volume_val.custom_minimum_size = Vector2(60, 0) vol_row.add_child(_volume_val) - # 全屏(拨杆形——SwitchToggle;状态字随行) + +## 全屏行(拨杆形——SwitchToggle;状态字随行) +func _build_fullscreen_row(box: VBoxContainer) -> void: var fs_row := HBoxContainer.new() fs_row.add_theme_constant_override("separation", 14) box.add_child(fs_row) var fs_lab := UiKit.label("全屏",UiKit.FS_BODY, false, UiKit.INK_LABEL) fs_lab.custom_minimum_size = Vector2(140, 0) # 同上 fs_row.add_child(fs_lab) - _fs_btn = UiKit.switch_toggle(false) + _fs_btn = UiKit._switch_toggle(false) _fs_btn.toggled.connect(_on_fullscreen_toggled) fs_row.add_child(_fs_btn) _fs_st = UiKit.label("关",UiKit.FS_HINT, true, UiKit.INK_LABEL) fs_row.add_child(_fs_st) - # 结算演出动画(=减动效实称;拨杆 on=完整,off=快速时长减半。 - # reduced_motion 单设置同驱 UiKit.motion_scale 与结算演播——demo「减动效」行与 - # 本行系同一设置,不另立行(拆两行=一设置双行重复) + +## 结算演出动画行(=减动效实称;拨杆 on=完整,off=快速时长减半。 +## reduced_motion 单设置同驱 UiKit.motion_scale 与结算演播——demo「减动效」行与 +## 本行系同一设置,不另立行(拆两行=一设置双行重复)) +func _build_motion_row(box: VBoxContainer) -> void: var motion_row := HBoxContainer.new() motion_row.add_theme_constant_override("separation", 14) box.add_child(motion_row) var motion_lab := UiKit.label("结算演出动画",UiKit.FS_BODY, false, UiKit.INK_LABEL) motion_lab.custom_minimum_size = Vector2(140, 0) # 同上 motion_row.add_child(motion_lab) - _motion_btn = UiKit.switch_toggle(true) + _motion_btn = UiKit._switch_toggle(true) _motion_btn.toggled.connect(_on_motion_toggled) motion_row.add_child(_motion_btn) _motion_st = UiKit.label("完整",UiKit.FS_HINT, true, UiKit.INK_LABEL) motion_row.add_child(_motion_st) var why := UiKit.label("快速 = 时长减半(运动敏感 / 演示用)",UiKit.FS_CAPTION, false, UiKit.INK_HINT) motion_row.add_child(why) # 内联随行(demo .why 同排) - box.add_child(HSeparator.new()) - # 退出游戏归开始菜单——设置面不再承载(退出只在一级菜单出现) + + +## 返回钮域(退出游戏归开始菜单——设置面不再承载〔退出只在一级菜单出现〕) +func _build_close_row(box: VBoxContainer) -> void: var close_row := HBoxContainer.new() close_row.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(close_row) diff --git a/game/presentation/views/settlement_review_overlay.gd b/game/presentation/views/settlement_review_overlay.gd index 7cb886f5..1cdd4b43 100644 --- a/game/presentation/views/settlement_review_overlay.gd +++ b/game/presentation/views/settlement_review_overlay.gd @@ -10,7 +10,7 @@ var _log: RichTextLabel func _init() -> void: - var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, true) + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, UiKit.PANEL_DEFAULT) (shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input) var box: VBoxContainer = shell.box box.add_child(UiKit.label("结算明细回看(完整流水 · 逐行归因)",UiKit.FS_SUB, true)) diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 2db37149..8ed5458e 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -211,7 +211,7 @@ func _build_shelf_card(holder: Control, c: Dictionary) -> Dictionary: body = "%s %+d(本冒险)" % [String(KIND_LABELS.get(kind, kind)), int(c.get("value", 0))] var card := UiKit.bcard(String(c.get("display_name", kind)), body, UiKit.tg_band(kl), icon_key, # 类型色带单点(N4c/Q4 甲——货架与三选一同源判定,原固定默认带) - "%d ◈" % int(c.get("price", 0)), String(KIND_LABELS.get(kind, kind))) + {"price": "%d ◈" % int(c.get("price", 0)), "footnote": String(KIND_LABELS.get(kind, kind))}) var btn := UiKit.button("购买") btn.pressed.connect(_emit_shelf_buy.bind(kind)) card.add_footer(btn) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index d946c735..ed2b1f7e 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -2,6 +2,9 @@ ## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。 ## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。 ## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。 +## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出, +## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar) +## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。 extends Node2D const CHARGE_SKILL_IDS := ["heavy"] @@ -12,50 +15,68 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} # ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit) const STANDEE_FOOT_MARGIN := 12.0 -var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入) -var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入) -var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界) +const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) +const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) -var _kind := "enemy" -var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) -var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) -# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 -# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 -var _hero_tex := {} -enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化) -const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"} -const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT} -var _hero_state: int = HeroState.IDLE -var _state_revert_tw: Tween -var _order := 1 -var _intent_glyph := "" -var _intent_col := Color.WHITE -var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据) -var _intent_visible := false -var _intent_scale := 1.0: # 意图弹入动画参数 - set(v): - _intent_scale = v - queue_redraw() -var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画) - 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_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」) -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 + +## 站桩演出状态(全部内部态单点——含 setter 触发宿主重绘语义,_owner 回引保持逐字) +class _StandeeState extends RefCounted: + var _owner: Node2D = null + var _kind := "enemy" + var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) + var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) + # 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 + # (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 + var _hero_tex := {} + var _hero_state: int = 0 + var _state_revert_tw: Tween + var _order := 1 + var _intent_glyph := "" + var _intent_col := Color.WHITE + var _intent_visible := false + var _intent_scale := 1.0: # 意图弹入动画参数 + set(v): + _intent_scale = v + _owner.queue_redraw() + var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画) + set(v): + _glyph_rot = v + _owner.queue_redraw() + var _display_name := "" + var _enemy_id := "" + var _armor := "light" + var _dead := false + var _hp_max := 1.0 + var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」) + var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积) + set(v): + hp_value = clampf(v, 0.0, 1.0) + _owner.queue_redraw() + var _hp_tween: Tween + var _hp_flash_tw: Tween + var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数) + var _hit_sparks := 0 # 探针出口计数(命中火花累计) + var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面) + + +var st := _StandeeState.new() +var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入;外部直访冻结) +var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入;外部直访冻结) +var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界;外部直访冻结) +var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据;源码锚承载成员住宿主) +# 血条存在性探针出口(世界直角血条常在;外部直访冻结) +var has_world_hp_bar := true func _init(kind: String = "enemy") -> void: - _kind = kind + st._owner = self + st._hero_state = HeroState.IDLE + st._kind = kind + + +enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化) +const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"} +const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT} func _efont() -> Font: @@ -63,26 +84,22 @@ func _efont() -> Font: func display_name() -> String: - return _display_name + return st._display_name # 敌单位身份出口(d12 站桩解析主键——同名敌按 id 精确匹配;主角站桩恒空串) func enemy_id() -> String: - return _enemy_id - - -# 血条存在性探针出口(世界直角血条常在) -var has_world_hp_bar := true + return st._enemy_id # 意图徽标探针出口 func intent_probe() -> Dictionary: - return {"visible": _intent_visible, "text": _intent_glyph} + return {"visible": st._intent_visible, "text": st._intent_glyph} # 名字行探针出口 func name_line_probe() -> Dictionary: - return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))} + return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))} # ---- setter 面(presenter/组合根零改排) ---- @@ -90,35 +107,34 @@ func name_line_probe() -> Dictionary: # 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)))) + st._display_name = String(d.get("display_name", "敌人")) + st._enemy_id = String(d.get("enemy_id", "")) + st._armor = String(d.get("armor_type", "light")) + st._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() + if st._dead and int(d.get("hp", 0)) > 0: + _revive() func set_hero(hp: int, max_hp: int) -> void: - _hp_max = float(maxi(1, max_hp)) + st._hp_max = float(maxi(1, max_hp)) _set_hp(hp) - if _dead and hp > 0: + if st._dead and hp > 0: _revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义) # 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积 func _set_hp(hp: int) -> void: - _hp_cur = clampi(hp, 0, int(_hp_max)) - var target := float(_hp_cur) / _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)) + st._hp_cur = clampi(hp, 0, int(st._hp_max)) + var target := float(st._hp_cur) / st._hp_max + if st._hp_tween != null and st._hp_tween.is_valid(): + st._hp_tween.kill() + st._hp_tween = create_tween() + st._hp_tween.tween_property(self, "st.hp_value", target, UiKit.motion(0.15)) # 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位) @@ -150,21 +166,21 @@ func set_effects_cleared_feedback() -> void: func _set_order(i: int) -> void: # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) - _order = i + st._order = i queue_redraw() # 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff; # "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。 func set_pending(ptype: String, pid: String) -> void: - if _dead: + if st._dead: _intent_charge = false - _intent_visible = false + st._intent_visible = false queue_redraw() return if ptype == "" or ptype == "basic": _intent_charge = false - _intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) + st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) queue_redraw() return var glyph := "⚔" # 攻击系(damage) @@ -178,9 +194,9 @@ func set_pending(ptype: String, pid: String) -> void: _intent_charge = pid in CHARGE_SKILL_IDS if _intent_charge: col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1) - _intent_glyph = glyph - _intent_col = col - _intent_visible = true + st._intent_glyph = glyph + st._intent_col = col + st._intent_visible = true _play_intent_motion() @@ -189,17 +205,17 @@ func set_pending(ptype: String, pid: String) -> void: # 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。 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)) + tw.tween_property(self, "st._intent_scale", 1.6, UiKit.motion(0.10)) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.16)) + tw.parallel().tween_property(self, "st._glyph_rot", 4.0, UiKit.motion(0.10)) + tw.tween_property(self, "st._glyph_rot", 0.0, UiKit.motion(0.14)) if _intent_charge: - tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推 + tw.tween_property(self, "st._intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推 tw.tween_interval(UiKit.motion(0.40)) # 定格微顿 - tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.18)) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.18)) else: - tw.tween_property(self, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举) - tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.13)) + tw.tween_property(self, "st._intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.13)) # 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 @@ -207,45 +223,45 @@ func _play_intent_motion() -> void: # (圣经 §8.2 禁非整数缩放)——源换代时随源重算。 func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: if tex == null: - if _portrait_sprite != null: - _portrait_sprite.visible = false + if st._portrait_sprite != null: + st._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 # 像素帧放大防糊 - add_child(_portrait_sprite) - _portrait_sprite.texture = tex - _portrait_sprite.flip_h = flip - _portrait_sprite.visible = true + st._portrait_tex = tex + if st._portrait_sprite == null: + st._portrait_sprite = Sprite2D.new() + st._portrait_sprite.centered = false + st._portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊 + add_child(st._portrait_sprite) + st._portrait_sprite.texture = tex + st._portrait_sprite.flip_h = flip + st._portrait_sprite.visible = true _place_portrait() func _place_portrait() -> void: # 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中 - if _portrait_sprite == null or _portrait_tex == null: + if st._portrait_sprite == null or st._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) + var w := st._portrait_tex.get_width() * 2.0 + var h := st._portrait_tex.get_height() * 2.0 + st._portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5) + st._portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5) func mark_dead() -> void: - if _dead: + if st._dead: return - _dead = true + st._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)) _spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s) - _refresh_portrait_glyph() - _intent_visible = false # 死亡残位意图退位 + _intent_charge = false + st._intent_visible = false # 死亡残位意图退位 _set_hp(0) if slot_row != null: slot_row.visible = false # 状态行随 mark_dead 同拍隐藏 @@ -253,37 +269,36 @@ func mark_dead() -> void: # ---- 演出密度(程序化 tween,零新帧资产) ---- -const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) -const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) -var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数) -var _hit_sparks := 0 # 探针出口计数(命中火花累计) -var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面) - # 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机) func _spawn_death_shards() -> void: - _death_shards = DEATH_SHARD_N + st._death_shards = DEATH_SHARD_N var center := _portrait_rect().get_center() for i in DEATH_SHARD_N: - var shard := ColorRect.new() - shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2) - shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2) - shard.mouse_filter = Control.MOUSE_FILTER_IGNORE - shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0 - shard.rotation = float(i) * 0.7 - add_child(shard) - var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3)) - var peak := shard.position + dir - var stw := shard.create_tween() - stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) - stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27)) - stw.tween_callback(shard.queue_free) + _spawn_death_shard(i, center) + + +## 单碎块域(循环体提函数——弹道两段+渐隐+回收) +func _spawn_death_shard(i: int, center: Vector2) -> void: + var shard := ColorRect.new() + shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2) + shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2) + shard.mouse_filter = Control.MOUSE_FILTER_IGNORE + shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0 + shard.rotation = float(i) * 0.7 + add_child(shard) + var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3)) + var peak := shard.position + dir + var stw := shard.create_tween() + stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) + stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27)) + stw.tween_callback(shard.queue_free) # 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面) func fx_probe() -> Dictionary: - return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count} + return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count} # 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向 @@ -292,65 +307,65 @@ func fx_probe() -> Dictionary: func play_hit_impact() -> void: var center := _portrait_rect().get_center() for i in HIT_SPARK_N: - _hit_sparks += 1 - var spark := ColorRect.new() - spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM - spark.size = Vector2(4, 3) - spark.mouse_filter = Control.MOUSE_FILTER_IGNORE - spark.position = center - spark.size * 0.5 - add_child(spark) - var ang := TAU * float(i) / float(HIT_SPARK_N) - var dir := Vector2(cos(ang), sin(ang)) * 16.0 - var stw := spark.create_tween() - stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12)) - stw.tween_callback(spark.queue_free) + _spawn_hit_spark(i, center) + + +## 单火花域(循环体提函数——放射弹道+渐隐+回收) +func _spawn_hit_spark(i: int, center: Vector2) -> void: + st._hit_sparks += 1 + var spark := ColorRect.new() + spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM + spark.size = Vector2(4, 3) + spark.mouse_filter = Control.MOUSE_FILTER_IGNORE + spark.position = center - spark.size * 0.5 + add_child(spark) + var ang := TAU * float(i) / float(HIT_SPARK_N) + var dir := Vector2(cos(ang), sin(ang)) * 16.0 + var stw := spark.create_tween() + stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12)) + stw.tween_callback(spark.queue_free) func is_dead() -> bool: - return _dead + return st._dead # 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行) func flash_hit() -> void: - _flash_count += 1 - 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)) + st._flash_count += 1 + if st._hp_flash_tw != null and st._hp_flash_tw.is_valid(): + st._hp_flash_tw.kill() + st._hp_flash_tw = create_tween() + st._hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) + st._hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not st._dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2)) func _revive() -> void: - _dead = false + st._dead = false modulate = Color(1, 1, 1) rotation_degrees = 0.0 - if _kind == "hero": - _hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) + if st._kind == "hero": + st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) set_hero_state("idle") - _refresh_portrait_glyph() if slot_row != null: slot_row.visible = true queue_redraw() -func _refresh_portrait_glyph() -> void: - queue_redraw() - - func _portrait_glyph() -> String: # 字形回退仅作缺位兜底(八敌真图落位;相对体量语义由 48px 档统一比例承载)。 # 缺位态:主角「人」/死亡「✕」/其余「敌」。 - if _kind == "hero": + if st._kind == "hero": return "人" - if _dead: + if st._dead: return "✕" return "敌" # 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面) func portrait_probe() -> Dictionary: - return {"has_tex": _portrait_sprite != null and _portrait_sprite.texture != null} + return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null} # ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ---- @@ -358,41 +373,41 @@ func portrait_probe() -> Dictionary: # 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理) func register_hero_state(state: String, tex: Texture2D) -> void: if tex != null: - _hero_tex[state] = tex + st._hero_tex[state] = tex # 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。 # 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。 func set_hero_state(state: String) -> void: var target: int = _HS_VALUE.get(state, -1) - if _kind != "hero" or target == _hero_state: + if st._kind != "hero" or target == st._hero_state: return - if not _hero_tex.has(state) and state != "idle": + if not st._hero_tex.has(state) and state != "idle": return - _hero_state = target - if _portrait_sprite != null and _hero_tex.has(state): - _portrait_sprite.texture = _hero_tex[state] - if _state_revert_tw != null and _state_revert_tw.is_valid(): - _state_revert_tw.kill() + st._hero_state = target + if st._portrait_sprite != null and st._hero_tex.has(state): + st._portrait_sprite.texture = st._hero_tex[state] + if st._state_revert_tw != null and st._state_revert_tw.is_valid(): + st._state_revert_tw.kill() if state != "idle": - _state_revert_tw = create_tween() - _state_revert_tw.tween_interval(UiKit.motion(0.35)) - _state_revert_tw.tween_callback(_settle_hero_idle) + st._state_revert_tw = create_tween() + st._state_revert_tw.tween_interval(UiKit.motion(0.35)) + st._state_revert_tw.tween_callback(_settle_hero_idle) func _settle_hero_idle() -> void: - _hero_state = HeroState.IDLE - if _portrait_sprite != null and _hero_tex.has("idle"): - _portrait_sprite.texture = _hero_tex["idle"] + st._hero_state = HeroState.IDLE + if st._portrait_sprite != null and st._hero_tex.has("idle"): + st._portrait_sprite.texture = st._hero_tex["idle"] # 三态探针出口(fx_probe——当前态名+注册态集合) func hero_state_probe() -> Dictionary: - return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()} + return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()} func _portrait_color() -> Color: - return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP + return UiKit.HERO_BLUE if st._kind == "hero" else UiKit.RED_DEEP # 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用 @@ -413,8 +428,19 @@ func content_rect() -> Rect2: func _draw() -> void: var f := _efont() - # 椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台 - # 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半) + _draw_ground(f) + _draw_hp_bar(f) + _draw_portrait(f) + _draw_intent_badge(f) + if st._kind == "enemy": + _draw_armor_name_line(f) + + +## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台 +## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半) +## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 +## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕) +func _draw_ground(f: Font) -> void: var sh_center := Vector2(box.x * 0.5, box.y - 24.0) var sh_rx := (box.x - 36.0) * 0.5 var sh_ry := 9.0 @@ -423,28 +449,28 @@ func _draw() -> void: var ang := TAU * float(i) / 24.0 pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry)) draw_colored_polygon(pts, Color(0, 0, 0, 0.27)) - # 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 - # ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕) var ped := _pedestal_rect() draw_rect(ped, UiKit.STAND_STONE, true) draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0) draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true) - # 血条(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8 + +## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8; +## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中; +## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) +func _draw_hp_bar(f: Font) -> void: var bar_x := 8.0 - if _kind == "enemy": - var order_txt := String("①②③"[clampi(_order - 1, 0, 2)]) + if st._kind == "enemy": + var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)]) f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT) 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 + var fill_w := bar_w * st.hp_value if fill_w > 0.0: draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减) - # 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中; - # 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) - var hp_txt := "%d/%d" % [_hp_cur, int(_hp_max)] + var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)] var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11) # 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2; # 中央垫 HP_BAR_BACK 深素牌 = 14.3:1;牌宽=字宽+8〔白名单取 8〕,高=条带全高;§4.1 数字色牌原则) @@ -453,46 +479,53 @@ func _draw() -> void: f.draw_string(get_canvas_item(), hbase + Vector2(1, 1), hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.10, 0.08, 0.06, 0.85)) f.draw_string(get_canvas_item(), hbase, hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(1, 1, 1, 0.95)) - # 立绘:贴图在位 = 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——draw_string 的 - # pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移); - # 传绝对 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(), 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) - # 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2 - # 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1; - # atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放) - draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true) - f.draw_string(get_canvas_item(), 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) +## 立绘域:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退 +func _draw_portrait(f: Font) -> void: + if st._portrait_sprite != null and st._portrait_sprite.texture != null and st._portrait_sprite.visible: + return + var prect := _portrait_rect() + var glyph := _portrait_glyph() + var center := prect.get_center() + # 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的 + # pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移); + # 传绝对 center 会成双重偏移、把字形画到盒外。 + draw_set_transform(center, deg_to_rad(st._glyph_rot), Vector2.ONE) + var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42) + f.draw_string(get_canvas_item(), 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——AC 数值不展示,类型图标化) - if _kind == "enemy": - var armor_txt := String(ARMOR_ICONS.get(_armor, "◇")) - var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) - match _armor: - "ward": - col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 - "heavy": - col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 - 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 - STANDEE_FOOT_MARGIN) # 纵向余量纪律 - f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col) - f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), _display_name, - HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制 + +## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画 +func _draw_intent_badge(f: Font) -> void: + if not st._intent_visible: + return + var icenter := Vector2(box.x * 0.5, 86.0) + draw_set_transform(icenter, 0.0, Vector2(st._intent_scale, st._intent_scale)) + var its := f.get_string_size(st._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15) + # 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2 + # 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1; + # atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放) + draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true) + f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), st._intent_glyph, + HORIZONTAL_ALIGNMENT_LEFT, -1, 15, st._intent_col) # 局部坐标(同上注记——双重偏移修复) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化) +func _draw_armor_name_line(f: Font) -> void: + var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇")) + var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) + match st._armor: + "ward": + col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 + "heavy": + col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 + var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x + var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x + var total_w := armor_w + 3.0 + name_w + var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律 + f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col) + f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), st._display_name, + HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制 diff --git a/game/presentation/views/status_panel.gd b/game/presentation/views/status_panel.gd index e97ac37f..e887e32d 100644 --- a/game/presentation/views/status_panel.gd +++ b/game/presentation/views/status_panel.gd @@ -14,7 +14,7 @@ var _feats_label: Label func _init() -> void: - var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, UiKit.panel_skinned(Vector4(20, 16, 20, 16))) # 石板皮(缺位回退数据板——同设置面先例) var box: VBoxContainer = shell.box diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index aabf3afd..adac263f 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -330,23 +330,9 @@ static func vignette_rect() -> TextureRect: # 程序纹理循环带,确定性生成零随机。光柱采用连续 alpha: # 带内 alpha 0→0.05→0 横向过渡+纵向衰减上亮下暗; # 纹理 filter=显式 LINEAR 白名单(与暗角/地图渐变同血统) -static func beams_rect() -> TextureRect: +static func _beams_rect() -> TextureRect: var img := Image.create(560, 560, false, Image.FORMAT_RGBA8) - var ca := cos(0.2094) # 12°(102deg 带轴的垂直倾角) - var sa := sin(0.2094) - var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0) - var band_lo := 240.0 - var band_hi := 340.0 - var band_mid := (band_lo + band_hi) * 0.5 - var band_peak := 0.05 - var band_half := (band_hi - band_lo) * 0.5 - for y in 560: - var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha) - for x in 560: - var d := fposmod(x * ca + y * sa, 560.0) - if d >= band_lo and d < band_hi: - var a: float = band_peak * (1.0 - absf(d - band_mid) / band_half) * v_fall - img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a)) + _paint_beams(img) var tr := TextureRect.new() tr.texture = ImageTexture.create_from_image(img) tr.stretch_mode = TextureRect.STRETCH_TILE @@ -356,8 +342,32 @@ static func beams_rect() -> TextureRect: return tr +# 光带几何常量(12° 斜向光带——带轴倾角/带域/峰值强度;_paint_beams 单消费) +const BEAMS_TILT_CA := 0.9135 # cos(0.2094)——12°(102deg 带轴的垂直倾角) +const BEAMS_TILT_SA := 0.4067 # sin(0.2094) +const BEAMS_BAND_LO := 240.0 +const BEAMS_BAND_HI := 340.0 +const BEAMS_PEAK := 0.05 + + +## 光带像素域(循环体提函数——12° 斜向光带双三角衰减:带内峰值×纵向衰减逐像素着色) +static func _paint_beams(img: Image) -> void: + var ca := BEAMS_TILT_CA + var sa := BEAMS_TILT_SA + var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0) + var band_mid := (BEAMS_BAND_LO + BEAMS_BAND_HI) * 0.5 + var band_half := (BEAMS_BAND_HI - BEAMS_BAND_LO) * 0.5 + for y in 560: + var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha) + for x in 560: + var d := fposmod(x * ca + y * sa, 560.0) + if d >= BEAMS_BAND_LO and d < BEAMS_BAND_HI: + var a: float = BEAMS_PEAK * (1.0 - absf(d - band_mid) / band_half) * v_fall + img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a)) + + # 竖向渐变底(demo #v-map linear-gradient(180deg, top 0%, low 40%)——stop 后恒 low) -static func gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect: +static func _gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect: var g := Gradient.new() g.offsets = PackedFloat32Array([0.0, stop]) g.colors = PackedColorArray([top, low]) @@ -442,7 +452,7 @@ static func label(text: String, size: int, bold: bool, color: Color = Color(-1, # PanelContainer(panel_sb 缺省 = 主题默认板——settlement_review 同款);box = 内容列 #(min_size/separation 参数化,min_size ZERO = 不设)。返回 Dictionary{dim, center, panel, box} #——探针直读的私有件由调用方按名摘引,结构同名同层级不变。 -static func modal_dim(parent: Control, dim_color: Color) -> ColorRect: +static func _modal_dim(parent: Control, dim_color: Color) -> ColorRect: var dim := ColorRect.new() dim.color = dim_color dim.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -450,18 +460,29 @@ static func modal_dim(parent: Control, dim_color: Color) -> ColorRect: return dim +const PANEL_DEFAULT := true # modal_shell 面板哨兵(非 StyleBox 实例——null=无面板/本哨兵=默认样式盒面板/StyleBox=自定义) + + +# 面板哨兵解析(六参收拢五参——三态:null=无面板〔原 with_panel=false〕/PANEL_DEFAULT=默认样式盒面板 +# 〔原 with_panel=true+panel_sb=null〕/StyleBox 实例=自定义样式盒面板〔原双参形态〕) +static func _resolve_shell_panel(panel_sb) -> PanelContainer: + if panel_sb == null: + return null + var panel := PanelContainer.new() + if panel_sb != PANEL_DEFAULT: + panel.add_theme_stylebox_override("panel", panel_sb) + return panel + + static func modal_shell(parent: Control, dim_color: Color, min_size := Vector2.ZERO, separation := 12, - with_panel := false, panel_sb: StyleBox = null) -> Dictionary: - var dim := modal_dim(parent, dim_color) + panel_sb = null) -> Dictionary: + var dim := _modal_dim(parent, dim_color) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) dim.add_child(center) - var panel: PanelContainer = null + var panel := _resolve_shell_panel(panel_sb) var host: Control = center - if with_panel: - panel = PanelContainer.new() - if panel_sb != null: - panel.add_theme_stylebox_override("panel", panel_sb) + if panel != null: center.add_child(panel) host = panel var box := VBoxContainer.new() @@ -563,19 +584,6 @@ static func panel_skinned(margin: Vector4 = Vector4.ZERO, tint := Color.WHITE) - # 仪式石框(空心角括框——营地容器框;缺位回退暗面板) -static func frame_skinned(margin: Vector4 = Vector4.ZERO) -> StyleBox: - var tex := load_tex(TEX_BORDER_STONE) - if tex == null: - return panel_flat(BG_OVERLAY, PANEL_BORDER,RADIUS_OBJECT, 1, margin) - var sb := _tex_sb(tex, 12.0) - sb.content_margin_left = margin.x - sb.content_margin_top = margin.y - sb.content_margin_right = margin.z - sb.content_margin_bottom = margin.w - return sb - - -# 图标槽(营地钥石/柜台件——缺位返回 null,消费点不挂 TextureRect 即回退纯文本) static func icon_rect(tex_path: String, px: float) -> TextureRect: var t := load_tex(tex_path) if t == null: @@ -659,19 +667,6 @@ static func _apply_button_skin(b: Button) -> void: # 消费方 = 标题/营地/设置/商店/三选一屏。单点纪律同 §7.1:新件只加不改旧件)── # 动词按钮(图标+动词+悬停说明三件套——标题屏按钮组/通用;icon 缺位=纯文字不炸) -static func verb_button(text: String, icon_path: String, tip := "") -> Button: - var b := button(text) - var tex := load_tex(icon_path) - if tex != null: - b.icon = tex - b.add_theme_constant_override("icon_max_width", 20) - b.expand_icon = false - if tip != "": - b.tooltip_text = tip - return b - - -# 主操作钮(烛光黄平涂+木描边——非奖励金;配色总纲「主按钮禁亮金」;缺 panel_stone 依赖=纯色可靠) static func primary_button(text: String, tip := "") -> Button: var b := button(text) var sb := panel_flat(CANDLE, Color8(110, 69, 52),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)) @@ -749,7 +744,9 @@ class BCard: var _sb_normal: StyleBox var _sb_sel: StyleBox - func _init(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> void: + func _init(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> void: + var price: String = opts.get("price", "") + var footnote: String = opts.get("footnote", "") var tex := UiKit.load_tex(UiKit.TEX_PANEL_STONE) _sb_normal = UiKit.panel_skinned(Vector4(12, 10, 12, 10)) if tex != null else UiKit.panel_flat(PANEL_DATA, PANEL_BORDER,RADIUS_OBJECT, 1, Vector4(12, 10, 12, 10)) # 回退=数据板单点 _sb_sel = UiKit.panel_flat(PANEL_DATA, Color8(138, 90, 52), RADIUS_OBJECT, 2, Vector4(12, 10, 12, 10)) # 选中底=数据板单点 @@ -766,21 +763,7 @@ class BCard: mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT mark.visible = false box.add_child(mark) - # 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍) - var slot := PanelContainer.new() - slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)) # 明度差降档(柔和档;白图标 ≥3:1 硬约束在役) - slot.custom_minimum_size = Vector2(76, 76) - slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - box.add_child(slot) - icon_tr = TextureRect.new() - var itex := UiKit.load_tex(icon_path) - if itex != null: - icon_tr.texture = itex - icon_tr.custom_minimum_size = Vector2(60, 60) - icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - slot.add_child(icon_tr) + box.add_child(_build_icon_slot(icon_path)) name_l = UiKit.label(title,FS_SUB, true, INK_TITLE) name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(name_l) @@ -800,6 +783,23 @@ class BCard: foot_l.visible = footnote != "" box.add_child(foot_l) + # 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍;明度差降档——柔和档;白图标 ≥3:1 硬约束在役) + func _build_icon_slot(icon_path: String) -> PanelContainer: + var slot := PanelContainer.new() + slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)) + slot.custom_minimum_size = Vector2(76, 76) + slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + icon_tr = TextureRect.new() + var itex := UiKit.load_tex(icon_path) + if itex != null: + icon_tr.texture = itex + icon_tr.custom_minimum_size = Vector2(60, 60) + icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + slot.add_child(icon_tr) + return slot + func set_selected(v: bool) -> void: add_theme_stylebox_override("panel", _sb_sel if v else _sb_normal) mark.visible = v @@ -809,8 +809,9 @@ class BCard: body.move_child(c, foot_l.get_index()) -static func bcard(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> BCard: - return BCard.new(title, desc, band, icon_path, price, footnote) +## 竖卡工厂(BCard._init 同形收拢 6→5 参——opts 键 {price, footnote} 缺省逐字同值) +static func bcard(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> BCard: + return BCard.new(title, desc, band, icon_path, opts) # 拨杆开关(设置屏——46×22 滑柄形;状态字随行由消费方摆;会话级/持久由消费方接 SettingsStore) @@ -838,7 +839,7 @@ class SwitchToggle: _knob.size = Vector2(18, 16) -static func switch_toggle(initial: bool) -> SwitchToggle: +static func _switch_toggle(initial: bool) -> SwitchToggle: return SwitchToggle.new(initial) diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 8ec33796..77c2f02c 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -157,7 +157,7 @@ func _ready() -> void: problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.level_screen._cand_ids)) # ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)---- - main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), "GP 0") + main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"}) _settle() await _audit("battle_end", [main._end_overlay]) main._end_overlay.close() diff --git a/game/tests/README.md b/game/tests/README.md index 0b535715..462d894c 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -43,17 +43,72 @@ tests/ > **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = 单元测试; > 需要两个以上真实系统协同并走落盘/落库 = 集成测试;需要完整项目引导与场景树 = 冒烟/系统/UI 探针。 -**集成旅程清单**(2026-10-28 立——跨域旅程用例的登记处:一个行为穿过多个领域的对外方法=旅程,验收落本层;缺口旅程由属主批/工程债批按批落用例后回填本表): +**能力清单 v1**(2026-10-28 立——验证与清单体系整改阶段 1 的覆盖索引:一行=一个玩家或系统可观测的能力,代表用例只给指针、不复制断言细节;原「集成旅程清单」五条跨系统旅程已并入本表。首期覆盖核心四域(29 行)+跨域条件闸 1 行,共 30 行;其余域按同一形态扩面): -| 旅程 | 涉及边界 | 现行覆盖 | 缺口 | 登记批 | -|---|---|---|---|---| -| 存档⇄RNG 快照往返保真 | SaveManager×AdventureRunner×RngService | `save_rng_roundtrip_test.gd` 用例一(闸 02) | — | 迭代 29 T3 | -| 旧档迁移后断点续玩零跳号 | SaveManager 迁移链×AdventureRunner×BoardGenerator | `save_rng_roundtrip_test.gd` 用例二(闸 02) | — | 迭代 29 T3 | -| 断点附随件与事件日志会话 | AdventureRunner×SaveManager×日志面 | `cross_system_flow_test.gd`(闸 02) | 盘点候选 | 断言迁移批(2026-10-23) | -| 关卡图断点重生成同图/生成图确定性 | LevelRunner×节点图生成器×AdventureRunner | `node_map_determinism_test.gd`(闸 02) | — | 断言迁移批(2026-10-23,迭代 35 卡②随动换代) | -| 回放确定性(同种子逐位) | 全玩法链 | `parity_anchor.py`(闸 11 条件闸) | — | 迭代 29 T3 收编为条件闸件 | +> **与验证集清单的关系**:`tests/验证集清单.md` 已于阶段 2「存量断言精简」完成后删除——冻结对账基准(基点 `b9260138`/1587 条)与精简对账结果(33 站点并为 7、1587→1561,合回 `87d241c1`)留 git 历史。本表是该清单退役后的唯一覆盖索引(维护锚=test-conventions §8.3)。本表不是闸,不替代改动—验收对照表、体验验证清单与静态纪律;每行的唯一事实源仍是 `game/tests/` 下的测试代码。 +> +> **代表用例口径**:写 `文件:函数`,每行 2–3 个;函数内可含 `Kit.check_quiet` 静默通道(逐断言提取器口径外,test-conventions §5.2 已备案)——本表以函数为锚,不受该工具局限影响。 +> +> **维护规则(每批收口核对本批受影响行)**:本批改到哪个能力,收口时核对该行代表用例可 grep、在本批真实跑批中执行、真实入口未绕过改动点,并把「最后验证批次」刷新为本批回执;新增能力加行,能力退役删行。全表核对频率与抽样规则归阶段 3。首期未覆盖:排雷循环(棋盘生成/排雷玩法/行动经济)、表现与支撑(HUD/主题/字体/地形瓦片/模态)、数据配置与跨系统契约——按同一形态后续扩面。 +> +> **旅程补录方法**:新旅程=加一行,「系统」列写涉及边界;源=architecture「API 边界」表+「数据流」存档/读档路径+各 GDD 登记的 deferral 集成用例。 +> +> 「最后验证批次」列记回执 commit;多数行初值 `20c13c3a`=存量断言精简 A+B 首批的全量域闸回执,RNG 行随 C 档批刷新为 `fd6cf968`。 -候选旅程盘点方法:源=architecture「API 边界」表逐行+「数据流」存档/读档路径+各 GDD 登记的 deferral 集成用例;新旅程先登记「缺口」列,用例按批落地后回填「现行覆盖」。 +**旅程段一:开机与存档** + +| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 | +|---|---|---|---|---|---| +| 存档写盘⇄读回往返保真 | 存档序列化 | `SaveManagerCore.write_to_disk()`/`load_from_disk()` | `save_manager_domain_test.gd:_smoke_save` | `20c13c3a` | — | +| 三槽位隔离与激活失败回滚 | 存档序列化 | `SaveManagerCore.activate_slot()`/`create_slot()`/`delete_slot()` | `save_slot_isolation_test.gd:_test_three_slot_roundtrip_and_isolation`;`save_slot_isolation_test.gd:_test_activate_failure_restores_previous_slot` | `20c13c3a` | — | +| 槽位摘要窥视与坏槽三态 | 存档序列化 | `SaveManagerCore.peek_slot_summary()` | `save_slot_summary_test.gd:_test_peek_three_slots_distinct_values`;`save_slot_summary_test.gd:_test_peek_empty_and_unreadable_states`;`save_slot_summary_test.gd:_test_peek_zero_side_effect` | `20c13c3a` | — | +| 启动态无激活不写盘 | 存档序列化 | `SaveManagerCore.has_write_target()`/`write_to_disk()` | `save_boot_no_activate_test.gd:_test_write_rejected_without_target`;`save_boot_no_activate_test.gd:_test_out_of_range_slots_rejected` | `20c13c3a` | — | +| 旧档迁移链与断点续玩零跳号(集成) | 存档序列化×冒险结构 | `SaveManagerCore.load_from_disk()`(内置迁移链)× `AdventureRunner.resume_adventure()` | `save_rng_roundtrip_test.gd:_test_old_version_save_migrates_and_resumes` | `20c13c3a` | — | +| 存档⇄RNG 快照往返保真(集成) | 存档序列化×RNG 服务×冒险结构 | `AdventureRunner.start_adventure()` → `SaveManagerCore.write_to_disk()`/`load_from_disk()` | `save_rng_roundtrip_test.gd:_test_disk_roundtrip_preserves_breakpoint` | `20c13c3a` | — | +| RNG 统一流派生与取号账本 | RNG 服务 | `RngServiceCore.derive_at()`/`open_stream()`/`counter()` | `rng_service_test.gd:_test_derive_determinism`;`rng_service_test.gd:_test_stream_draw_determinism`;`rng_stream_boot_test.gd:_smoke_initialize` | `fd6cf968` | — | +| 断点片段往返与续玩幂等 | 冒险结构×关卡节点地图 | `LevelRunner.breakpoint_fragment()`/`restore_fragment()`;`AdventureRunner.resume_adventure()` | `breakpoint_resume_test.gd:_init` | `20c13c3a` | — | +| 跨系统附随面:事件日志落盘与断点附随件(集成) | RNG 服务×存档序列化×冒险结构 | `RngServiceCore.new_adventure()`(开事件日志会话);`SaveManagerCore.breakpoint_snapshot()`(断点附随键) | `cross_system_flow_test.gd:_rng_event_log_session`;`cross_system_flow_test.gd:_feats_breakpoint_roundtrip` | `20c13c3a` | 日志面仅此一条会话覆盖 | + +**旅程段二:冒险开局与选路** + +| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 | +|---|---|---|---|---|---| +| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure` | `20c13c3a` | — | +| 专长(奇物)授予与凭证保底 | 奇物 | `AdventureRunner.present_node_curio()`/`feats_held()` × 效果引擎兑付 | `adventure_feats_test.gd:_smoke_feats`;`feat_vouchers_test.gd:_init` | `20c13c3a` | — | +| 节点图生成合法性与同种子复现 | 关卡节点图生成 | `NodeMapGenerator.generate()`/`validate_output()` | `node_map_generator_test.gd:_test_output_ok`;`node_map_generator_test.gd:_test_reproducible`;`node_map_generator_test.gd:_test_exit_structure` | `20c13c3a` | — | +| 节点图生长递增与无解 fail-fast | 关卡节点图生成 | `NodeMapGenerator.generate()`/`is_fatal()` | `node_map_growth_test.gd:_test_growth`;`node_map_growth_test.gd:_test_fatal_subprocess` | `20c13c3a` | — | +| 起关/选路/断点重生成同图(集成) | 关卡节点地图×关卡节点图生成×冒险结构 | `LevelRunner.start_level()`/`start_level_external()`/`pick_next_node()`;`AdventureRunner.resume_adventure()` | `node_map_determinism_test.gd:_smoke_generated_map_determinism` | `20c13c3a` | — | +| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild` | `20c13c3a` | — | +| 关卡表消费侧校验与关卡推进 | 关卡节点地图×数据配置 | `LevelTable.validate_graph()`;`LevelRunner.start_level()` | `level_table_validate_test.gd:_test_positive`;`level_table_validate_test.gd:_test_negative`;`level_shop_economy_test.gd:_smoke_level` | `20c13c3a` | — | + +**旅程段三:战斗结算** + +| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 | +|---|---|---|---|---|---| +| 伤害公式与掷点乘区 | 战斗结算 | `DamageMath.pool_damage()`/`roll_discount_percent()`/`rolled_component()` | `damage_math_test.gd:_test_pool_damage`;`damage_math_test.gd:_test_roll_discount_percent`;`roll_multiplier_test.gd:_smoke_armor_roll` | `20c13c3a` | — | +| 伤害单据编排与回放 | 战斗结算 | `DamageController.submit()`/`open_ticket()`/`flush()` | `damage_controller_test.gd:_run_recipe_sequence`;`damage_controller_test.gd:_test_five_recipes_and_replay` | `20c13c3a` | — | +| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments` | `20c13c3a` | — | +| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence` | `20c13c3a` | — | +| Boss 半血解锁技门控 | 敌人系统 | `BattleOrchestrator.begin_battle()`(Boss 行) | `skill_unlock_test.gd:_test_gate_predicate`;`skill_unlock_test.gd:_test_latch_irreversible` | `20c13c3a` | — | +| 封印与极性路由/不可逆 | 地图战斗资源×战斗结算 | 效果引擎 × `BattleOrchestrator.begin_battle()` | `seal_polarity_test.gd:_smoke_seal_ap_exhausted`;`seal_polarity_test.gd:_smoke_seal_irreversible_quadrants`;`seal_polarity_test.gd:_smoke_polarity_routing` | `20c13c3a` | — | +| 变异布置门控 | 地形变异 | `BattleOrchestrator.begin_battle()`(变异布置段) | `variant_placement_test.gd:_smoke_variant_placement`;`variant_placement_test.gd:_smoke_global_variant` | `20c13c3a` | — | + +**旅程段四:奖励、经济与局外成长** + +| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 | +|---|---|---|---|---|---| +| 清图奖励双份与零路径 | 地图战斗资源 | `BattleOrchestrator.begin_battle()` → 步骤流行(`source_class=clear_reward`) | `seal_polarity_test.gd:_smoke_clear_reward_chain`;`seal_polarity_test.gd:_smoke_clear_reward_zero_paths` | `20c13c3a` | — | +| 奖励三选一候选/刷新付费/呈现 | 奇物×节点奖励屏 | `AdventureRunner.present_node_curio()`(数据面);奖励节点覆盖层场景(呈现面) | `curio_refresh_paid_test.gd:_init`;`reward_overlay_test.gd:_run` | `20c13c3a` | — | +| GP 钱包/escrow/商店收口/共享池 | 地图战斗资源×商店 | `LevelRunner.buy_product()`/`player_request_refresh()`/`close_wallet()`/`set_shop_pool()` | `level_shop_economy_test.gd:_smoke_gp_wallet`;`level_shop_economy_test.gd:_smoke_shop_room`;`level_shop_economy_test.gd:_smoke_shop_pool` | `20c13c3a` | — | +| 元进度入账与锻造两轴 | 元进度×锻造工坊 | `SaveManagerCore.add_keystone()`/`spend_keystone()`/`set_forge_route_level()` | `meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_smoke_forge_axes` | `20c13c3a` | — | +| 天赋树购买与六键合成 | 天赋树 | `SaveManagerCore.tree_nodes()`/`set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init` | `20c13c3a` | — | +| 秘宝结构冻结与 12 件效果 | 秘宝 | `TreasureItems.add_item()`/`query_inventory()`/`validate_use()` | `treasure_items_test.gd:_init`;`treasure_effects_test.gd:_init` | `20c13c3a` | — | + +**跨域条件闸** + +| 能力 | 系统 | 真实入口 | 代表用例 | 最后验证批次 | 缺口 | +|---|---|---|---|---|---| +| 回放确定性(同种子三变体逐位) | 全玩法链 | `tools/parity_anchor.py`(闸 11 条件闸,按批发起) | `tools/parity_anchor.py`(整件为条件闸) | —(条件闸) | 条件闸未随本批执行 | ## 断言计数双口径(2026-10-08 迭代 19 卡① T4 订正) @@ -70,7 +125,7 @@ tests/ |---|---|---|---| | `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 | | `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | -| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) | +| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) | | `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 | | `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 | | `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | @@ -96,8 +151,9 @@ python tools/run_system_test.py # 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位) godot --headless --path . res://probe/.tscn -# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置) -timeout 120s godot --headless --script tests/run_all.gd +# 挂死防护内建于 run_all.gd(引擎无死锁探测,超时靠外部判):每套件子进程带截止时间—— +# 默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖;超时先 taskkill /T 整树回收 +#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸 ``` > 套件 manifest 现况:`_suites()` 登记 **64 条**(61 单元+3 集成,2026-10-28 实数;口径=manifest 条目数, diff --git a/game/tests/integration/cross_system_flow_test.gd b/game/tests/integration/cross_system_flow_test.gd index 431864b7..c4593ae5 100644 --- a/game/tests/integration/cross_system_flow_test.gd +++ b/game/tests/integration/cross_system_flow_test.gd @@ -73,8 +73,8 @@ func _feats_breakpoint_roundtrip() -> void: {"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1, "triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]}, ] - var live_rows: FeatTable = _db.feats - _db.feats = rt + var live_rows: FeatTable = _db.crossrun.feats + _db.crossrun.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) a_r.start_adventure(72) # 源 :723(受理断言留单元宿主) a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}} @@ -98,7 +98,7 @@ func _feats_breakpoint_roundtrip() -> void: sm_r.clear_breakpoint() if FileAccess.file_exists(bp_path): # 段尾清理(与段首清场对称) DirAccess.remove_absolute(bp_path) - _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知) + _db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知) # ---- ③ 存档摘要集成(源 `_smoke_save` ⑨ 基点 :6807–6831 十条,主会话裁决三改判集成—— diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 7259ef5b..52c01ca9 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -1,12 +1,16 @@ # tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b) -# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …) +# 用法:godot --headless --path game --script res://tests/run_all.gd # 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件; # 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据; -# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。 +# 批末全量闸不带参数不经选跑分支。 # 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。 # 执行形态:逐套件起独立 Godot 子进程实跑 #(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符, # 每个测试文件自带显式实例化夹具,见 tests/README 协议要点)。 +# 挂死守护:每套件子进程带截止时间(默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖)。 +# 套件挂死(_init 在 quit() 前中断,子进程永不退出)=超时先 taskkill /T 整树回收 +#(连 cmd wrapper 带引擎孙进程一起杀,只杀 wrapper 会留孤儿殃及后续全部 Godot 启动) +# 再判该套件 FAIL 继续下一套件——单套件挂死不拖死整批;正常退出路径判定形态不变。 # 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。 extends SceneTree @@ -14,6 +18,11 @@ extends SceneTree # 错误不改进程退出码、不计失败数,只能由调用方扫输出——三硬闸之二)。 const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"] +# 每套件截止时间(秒):默认与 probe_gate 每段同额;环境变量可覆盖——服务挂死守护的 +# 机制验证与日后调参,非法值/非正值一律回落默认。 +const SUITE_TIMEOUT_ENV := "RUN_ALL_SUITE_TIMEOUT_SEC" +const SUITE_TIMEOUT_DEFAULT_SEC := 120 + func _init() -> void: # 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。 @@ -154,21 +163,69 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: + "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地") return 1 # 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空 - #(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。 + #(实测 0 行),故整条命令交给 cmd 并重定向到日志。退出码同样走文件通道—— + # 阻塞式 execute 无超时能力(套件挂死时整批陪葬),换 create_process 非阻塞起进程 + # 后又没有退出码读取 API,故由 cmd /V:ON 在套件退出后把 !ERRORLEVEL! 回显落盘, + # 值与阻塞式 execute 经 cmd 回传的同一。 var proj := ProjectSettings.globalize_path("res://") var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx] + var codefile := log + ".code" + if FileAccess.file_exists(codefile): + DirAccess.remove_absolute(codefile) # 陈值防护:上轮临界残件不许冒充本轮退出码 DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates") - var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log] + # /V:ON=延迟展开:!ERRORLEVEL! 到 echo 执行时(套件已退出)才取值; + # 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。 + var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile] var out := PackedStringArray() - var code: int = OS.execute("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false) + var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd])) + if pid < 0: + push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid]) + return 1 + # 轮询先查存活再查截止:进程在轮询间隙自然退出时按正常路径判,截止临界不误杀。 + var timed_out := false + var deadline_ms := Time.get_ticks_msec() + _suite_timeout_sec() * 1000 + while true: + if not OS.is_process_running(pid): + break + if Time.get_ticks_msec() >= deadline_ms: + timed_out = true + break + OS.delay_msec(200) var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else "" var negative := bool(suite.get("negative", false)) + if timed_out: + var kill_out := PackedStringArray() + OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true) + # taskkill 落刀到进程对象消失有间隔:等死确认再放行下一套件(10s 宽限), + # 确认不了也如实报红留 PID 供手工清杀,不静默放行。 + var reap_deadline := Time.get_ticks_msec() + 10000 + while OS.is_process_running(pid) and Time.get_ticks_msec() < reap_deadline: + OS.delay_msec(200) + var reaped := not OS.is_process_running(pid) + # 临界窗口:截止到落刀之间套件可能恰好跑完 echo——同删退出码文件,.gates 不积残件。 + if FileAccess.file_exists(codefile): + DirAccess.remove_absolute(codefile) + if negative: + print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path) + else: + print(text) + if reaped: + push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树已整树回收(pid=%d)" % [path, _suite_timeout_sec(), pid]) + else: + push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树回收未确认(pid=%d,需手工清杀残留)" % [path, _suite_timeout_sec(), pid]) + return 1 # 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行—— # 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。 if negative: print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path) else: print(text) + var code_txt := FileAccess.get_file_as_string(codefile).strip_edges() if FileAccess.file_exists(codefile) else "" + if code_txt == "" or not code_txt.is_valid_int(): + push_error("[run_all] FAIL %s — 退出码文件缺失或非数字(%s)——子进程未走到退出码落盘即死,判红" % [path, codefile]) + return 1 + var code: int = int(code_txt) + DirAccess.remove_absolute(codefile) # 用毕即删:退出码经文件通道但不落 residuum(.gates 只留 *.log) if not negative and _has_script_error(text): push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)]) return 1 @@ -205,6 +262,13 @@ func _engine_bin() -> String: return "" +func _suite_timeout_sec() -> int: + var env := OS.get_environment(SUITE_TIMEOUT_ENV) + if env != "" and env.is_valid_int() and int(env) > 0: + return int(env) + return SUITE_TIMEOUT_DEFAULT_SEC + + func _has_script_error(text: String) -> bool: return _script_error_line(text) != "" diff --git a/game/tests/smoke_boot.gd b/game/tests/smoke_boot.gd index d3d5172c..76a5c9c0 100644 --- a/game/tests/smoke_boot.gd +++ b/game/tests/smoke_boot.gd @@ -66,10 +66,8 @@ func _check_main_scene(main: Node) -> void: if main == null: return var run_scope: Node = main.get("run_scope") - check(run_scope != null, "闲时会话 run_scope 非空(永非 null 契约)") check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树") var start_overlay: Control = main.get("_start_overlay") - check(start_overlay != null, "开始菜单层已装配(_start_overlay)") check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)") check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)") check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)") diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 24f09f01..4b8ce8dc 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -154,7 +154,7 @@ func _smoke_boot_chain() -> void: await process_frame # ① ConfigDB 就绪且表已载 var db = main.get_node("/root/ConfigDB") - check(db.is_ready and db.hero != null, + check(db.is_ready and db.combat.hero != null, "V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕") # ② 设置已应用(引擎态=settings_store 现值——真实设置文件照读,一致性判据与文件内容无关) var ss = main.settings_store @@ -173,8 +173,6 @@ func _smoke_boot_chain() -> void: var sm = main.get_node("/root/SaveManager") # ④-1 boot 路径:启动态钉死(卡① sv2)——boot 无激活态、断点在档恒否,续玩钮恒隐 #(原「按钮态==真实档断点态」关系判据随启动态换代改写:test-conventions §9.5 申报在案,卡② mn8 再承接) - check(not sm.has_breakpoint(), - "V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕") # ④-1b 启动态三断言(卡① sv2——真实 autoload boot 链) check(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(), "V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕") diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index b65ce03a..ac3c8980 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -128,8 +128,7 @@ func _smoke_armor_roll() -> void: if int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll): crit_seed = s break - Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.combat.settlement.damage_roll.crit_roll)], crit_seed > 0) - Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)", + Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量——种子可达 seed=%d)" % crit_seed, int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false))) Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)], int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk)) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 865c9775..2b972ee1 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -58,7 +58,6 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void: break Kit.check("%s 回放断言:结算步骤流逐行相同" % label, JSON.stringify(run1.stream) == JSON.stringify(run2.stream)) Kit.check("%s 回放断言:事件流逐位相同" % label, JSON.stringify(run1.events) == JSON.stringify(run2.events)) - Kit.check("%s 结算步骤流非空(%d 行)" % [label, run1.stream.size()], run1.stream.size() > 0) var has_verdict := false var hero_attacks := 0 var hero_breakdown_ok := true @@ -892,7 +891,7 @@ func _smoke_skill_pool_validation_reject() -> void: {"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0}, {"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}]) enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}]) - Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side")) + Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.combat.status_effects), "cast_side")) func _has_err_category(errs: Array, category: String) -> bool: diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index e8242bae..e10b1241 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -439,10 +439,16 @@ func _smoke_polarity_routing() -> void: if _polarity_probe_accepts(probe): polarity_seed = s break - Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚)" % polarity_seed, polarity_seed > 0) - Kit.check("构造触达:标中真雷 + 标空安全格均入队", bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])) - Kit.check("构造触达:挖到危险格 ≥1(%d)" % int(probe["mine_events"]), int(probe["mine_events"]) >= 1) - Kit.check("构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"]), int(probe["safe_events"]) >= 1) + # 触达三腿(标中标空入队/挖危险格/挖安全格)是 _polarity_probe_accepts 的合取项, + # 可达即三腿同真——缺腿明细进标签承载诊断,不再各立一条(2026-10-28 断言精简批删并) + var legs := "" + if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])): + legs += "标中标空未入队 " + if int(probe["mine_events"]) < 1: + legs += "挖危险格零 " + if int(probe["safe_events"]) < 1: + legs += "挖安全格零 " + Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚;缺腿:%s)" % [polarity_seed, legs], polarity_seed > 0) # 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集 # (action 型排队行 effect_id=ap_delta 正负两极共用路由键——按行值符号辨极性:正向格恒 +power) var safe_all_pos := true diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index 6c6ff144..cd7c0709 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -16,7 +16,7 @@ func _init() -> void: return # ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)---- - var skill = db.skill + var skill = db.combat.skill var base_rows: Array = skill.rows.duplicate(true) var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}} @@ -133,7 +133,7 @@ func _init() -> void: # ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)---- var cv = load("res://core/config_db_cross_validator.gd").new(db) - var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects) + var pool_errs: Array = cv.validate_skill_pool_links(db.combat.skill, db.combat.enemy, db.combat.status_effects) var comp_violations := 0 for e in pool_errs: if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"): @@ -150,7 +150,7 @@ func _init() -> void: cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态 cond_only_row["pool"] = cond_pool db.combat.enemy.rows[0] = cond_only_row - var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects) + var errs2: Array = cv.validate_skill_pool_links(db.combat.skill, db.combat.enemy, db.combat.status_effects) var hit2 := false for e in errs2: if String((e as Dictionary).get("message", "")).contains("无条件特殊技"): diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index 0880d67e..b73b0265 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -44,6 +44,15 @@ func _smoke_find_token(candidates: Array, kind: String, tier: String) -> Diction return {} +# 效果命中分桶(公开 dispatch 返回值的本地过滤——断言钉公开可观测面;编排器私有过滤器的测试侧等价承载) +func _hits_kind(hits: Array, kind: String) -> Array: + var out: Array = [] + for h in hits: + if String((h as Dictionary).get("kind", "")) == kind: + out.append(h) + return out + + # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) @@ -355,9 +364,11 @@ func _smoke_adventure() -> void: Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up")) var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子) Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3) + var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获) + a8.stop().curio_choice_presented.connect(func(p): l2_cap["payload"] = p) a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index, "node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关) - Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not a8.stop()._pending.is_empty()) + Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not (l2_cap["payload"] as Dictionary).is_empty()) Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level()) var l2_cands: Array = a8.stop()._pending["candidates"] var l2_pick: Dictionary = l2_cands[0] @@ -438,7 +449,7 @@ func _smoke_adventure() -> void: a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}]) var gp13: int = a13.wallet().gp Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.run.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.run.adventure.curio_value("gp", "common"))) - Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty()) + Kit.check("节点停点态兑付后清零(恰一次语义同构)", (a13.stop().node_curio_pending_state().get("candidates", []) as Array).is_empty()) var s10: Dictionary = a10.summary Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true))) @@ -451,7 +462,7 @@ func _smoke_adventure() -> void: func _smoke_feats() -> void: print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---") # ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射) - Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.crossrun.feats.rows as Array).size() == 30) + Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 30) Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty()) var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71)) Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71)) @@ -462,7 +473,7 @@ func _smoke_feats() -> void: feat_tokens += 1 var tk := String((t as Dictionary).get("tier", "")) tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1 - Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132) + Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0.deck().deck.size() == 132) # ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查) Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty()) Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty()) @@ -505,15 +516,15 @@ func _smoke_feats() -> void: "triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]}, ] Kit.check("新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)", rt.validate().is_empty()) - var live_rows: FeatTable = _db.feats - _db.feats = rt + var live_rows: FeatTable = _db.crossrun.feats + _db.crossrun.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72)) var deck_relic := 0 for t in a_r._curio.deck: if String((t as Dictionary).get("kind", "")) == "relic": deck_relic += 1 - Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio.deck.size() == 107) + Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r.deck().deck.size() == 107) # 渠道白名单断言(渠道轴定位:关卡完成渠道只出专长/节点渠道零专长) var feat_seen: Dictionary = {} var node_relic_seen := false @@ -550,7 +561,7 @@ func _smoke_feats() -> void: a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step) var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick") Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().feat_ticket_consider(rg_t)) - Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0) + Kit.check("A6 触发后清零", a_t.deck().ticket_pity == 0) a_t.stop()._deck.ticket_sold = int(a_t.db.run.adventure.feat_ticket_limit) a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step) Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t)) @@ -566,7 +577,7 @@ func _smoke_feats() -> void: Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1) # 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主—— # 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。 - _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知) + _db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知) # ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读; # 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕) var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73)) @@ -593,13 +604,13 @@ func _smoke_feats() -> void: fx.rebuild(["geologist"], [], []) var geo_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true}) apply_fx.call(geo_hits) - Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", orch._hits_of(geo_hits, "counter_add").size() == 1) + Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", _hits_kind(geo_hits, "counter_add").size() == 1) EffectEngine.counter_add(orch.fx_host().counters_ctx, "geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值) var geo_hits2: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true}) apply_fx.call(geo_hits2) - Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0) + Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0) var geo_hits3: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": false}) - Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", orch._hits_of(geo_hits3, "counter_add").is_empty()) + Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", _hits_kind(geo_hits3, "counter_add").is_empty()) fx.rebuild(["hard_hat"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {})) @@ -613,29 +624,29 @@ func _smoke_feats() -> void: fx.rebuild(["skip_mining"], [], []) var sk_zero: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true}) apply_fx.call(sk_zero) - Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", orch._hits_of(sk_zero, "counter_add").size() == 1) + Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", _hits_kind(sk_zero, "counter_add").size() == 1) EffectEngine.counter_add(orch.fx_host().counters_ctx, "skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值) var sk_full: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true}) apply_fx.call(sk_full) - Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", orch._hits_of(sk_full, "reveal_tiles").size() == 1) - Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty()) + Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", _hits_kind(sk_full, "reveal_tiles").size() == 1) + Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty()) fx.rebuild(["blast_accounting"], [], []) for i in 5: var ba_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1}) for h in ba_hits: if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1) - Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) + Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) fx.rebuild(["frontier_miner"], [], []) Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty()) # 封印系 fx.rebuild(["seal_echo"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})) var se_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}) - Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue") + Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue") fx.rebuild(["seal_smith"], [], []) var sm_remove := 0 for i in 3: @@ -647,10 +658,10 @@ func _smoke_feats() -> void: Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1) fx.rebuild(["seal_stake"], [], []) Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 - and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round") + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 + and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round") fx.rebuild(["warning_beacon"], [], []) - Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1) + Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1) # 踩雷/受伤系 fx.rebuild(["pain_memory"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) @@ -666,13 +677,13 @@ func _smoke_feats() -> void: for h in fw_hits: if String(h.get("kind", "")) == "shield": fw_shield += int(h.get("value", 0)) - Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty()) + Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty()) for h in fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}): if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0))) Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0) fx.rebuild(["blood_insight"], [], []) - Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1) + Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1) fx.rebuild(["trap_instinct"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}) @@ -688,67 +699,67 @@ func _smoke_feats() -> void: fx.rebuild(["trap_burst"], [], []) var tb_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"damage": 8}) Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕", - orch._hits_of(tb_hits, "blast_scaled").size() == 1 - and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0) + _hits_kind(tb_hits, "blast_scaled").size() == 1 + and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0) fx.rebuild(["blood_honey"], [], []) - Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1) + Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1) fx.rebuild(["stone_skin"], [], []) var ss_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1}) Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕", - orch._hits_of(ss_hits, "ac_bonus").size() == 1 - and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3) + _hits_kind(ss_hits, "ac_bonus").size() == 1 + and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3) fx.rebuild(["last_stand"], [], []) Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty()) # 跨族桥 fx.rebuild(["breaker_strike"], [], []) Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty()) # 节奏系 fx.rebuild(["map_exchange"], [], []) var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4}) apply_fx.call(me_hits) EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账 - Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", orch._hits_of(me_hits, "counter_add").size() == 1) + Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", _hits_kind(me_hits, "counter_add").size() == 1) var me_atk := 0 for h in orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"): me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4) - Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty()) + Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty()) fx.rebuild(["last_drop"], [], []) var ld_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true}) - Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", orch._hits_of(ld_hits, "grant_ap").size() == 1) - Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty()) + Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", _hits_kind(ld_hits, "grant_ap").size() == 1) + Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty()) fx.rebuild(["market_close"], [], []) for i in 8: for h in fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}): if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1) - Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) + Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) # 战斗系 fx.rebuild(["execution_scholar"], [], []) - Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1) + Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1) fx.rebuild(["executioner"], [], []) var ex_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false}) Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕", - orch._hits_of(ex_hits, "target_attack_bonus").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty()) + _hits_kind(ex_hits, "target_attack_bonus").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty()) fx.rebuild(["field_dressing"], [], []) - Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1) + Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1) # 契约尖点 fx.rebuild(["blood_tax"], [], []) Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 - and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk") + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 + and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk") # 迁入 2 件 fx.rebuild(["venom_edge"], [], []) Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty()) fx.rebuild(["seal_burst"], [], []) - Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1) + Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1) # 零持有 var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74)) orch0.fx_host().rebuild_subscriptions([], [], []) diff --git a/game/tests/unit/core/board_gen_reward_test.gd b/game/tests/unit/core/board_gen_reward_test.gd index 4e481a60..347d2b42 100644 --- a/game/tests/unit/core/board_gen_reward_test.gd +++ b/game/tests/unit/core/board_gen_reward_test.gd @@ -176,8 +176,6 @@ func _smoke_board_generator() -> void: b1.degraded_product == (mode == "accept_best")) Kit.check("L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)], b1.illegal_product == (mode == "accept_worst")) - Kit.check("L%d 降级/非法产物标记互斥(两出口不可同真)" % L, - not b1.degraded_product or not b1.illegal_product) Kit.check("L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)], int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines)) Kit.check("L%d attempts_used ∈ [1,%d]" % [L, int(row.max_gen_attempts)], diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index ec28fabe..14e0b478 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -110,13 +110,13 @@ func _init() -> void: Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)") Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)") Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)") - Kit.check_quiet(int(adr_f._curio.taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)") + Kit.check_quiet(int(adr_f.deck().taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)") Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)") var pick_e: Dictionary = cands_e[0] Kit.check_quiet(adr_f.stop().pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理") var picked_e: String = String(adr_f.stop()._resolved.get("picked", "")) - if adr_f._curio.taken.has(picked_e): - Kit.check_quiet(int(adr_f._curio.taken[picked_e]) == (2 if picked_e == "attack" else 1), + if adr_f.deck().taken.has(picked_e): + Kit.check_quiet(int(adr_f.deck().taken[picked_e]) == (2 if picked_e == "attack" else 1), "P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e) Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)") var bp_g: Dictionary = sm_f.breakpoint_snapshot() diff --git a/game/tests/unit/core/config_db_cross_validator_test.gd b/game/tests/unit/core/config_db_cross_validator_test.gd index e2b7731c..8ad71acb 100644 --- a/game/tests/unit/core/config_db_cross_validator_test.gd +++ b/game/tests/unit/core/config_db_cross_validator_test.gd @@ -67,7 +67,6 @@ func _test_dangling_skill_id() -> void: {"skill_id": "attack", "weight": 9}, {"skill_id": "not_in_table", "weight": 3}]}, ]) var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, null) - Kit.check("池内悬空 skill_id 被报(跨表护栏不静默放过)", not errs.is_empty()) Kit.check("悬空项的错误类别可定位(category 非空)", not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "") diff --git a/game/tests/unit/core/config_db_resolver_test.gd b/game/tests/unit/core/config_db_resolver_test.gd index ebec43b2..fcbb6d64 100644 --- a/game/tests/unit/core/config_db_resolver_test.gd +++ b/game/tests/unit/core/config_db_resolver_test.gd @@ -64,7 +64,6 @@ func _test_band_hit() -> void: func _test_rowset_order_preserved() -> void: var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle") - Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2) var ids: Array[String] = [] for r in rows: ids.append(String((r as Dictionary).get("enemy_id", ""))) diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index 4d9558f4..48932e26 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -28,20 +28,21 @@ func _init() -> void: Kit.check_quiet(adr.stop().curio_stop_pending() == true, "关卡完成停点在途=谓词 true") adr.refresh_bank()._refresh_x = 1 adr.refresh_bank()._refresh_y = 2 - Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_x == 0 and adr.refresh_bank()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)") - Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_y == 1, "X 穷转 Y 池(y2→y1)") + Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 2 + and adr.refresh_bank().free_left() == adr.refresh_bank().refresh_y() + adr.refresh_bank().refresh_stock(), "X 优先消耗(x1y2→x0y2)") + Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 1, "X 穷转 Y 池(y2→y1)") # ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ---- Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)") - Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)") + Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().refresh_y() == 1, "付费刷新不触碰免费位(y1 保持)") Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)") Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)") # ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)---- adr.stop().present_node_curio(_cands()) - Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 0 and adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价") + Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价") adr.stop().refresh_curio_paid() - Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 1, "付费刷新计数在途递增(=1)") + Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "付费刷新计数在途递增(=1)") adr.stop().present_node_curio(_cands()) Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)") @@ -56,10 +57,10 @@ func _init() -> void: var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1} var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)") - Kit.check_quiet(adr_r.refresh_bank()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)") + Kit.check_quiet(adr_r.refresh_bank().refresh_y() == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)") var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)") - Kit.check_quiet(adr_o.refresh_bank()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") + Kit.check_quiet(adr_o.refresh_bank().refresh_y() == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") quit(Kit.finish("curio_refresh_paid")) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index a5dec54e..ee376c43 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -32,8 +32,9 @@ func _init() -> void: # V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量) a.refresh_bank().reset_x() # X 归零基线(模拟新停点) - Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕") - Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank()._refresh_x == 0, + a.stop()._pending = {"candidates": [{"kind": "refresh", "tier": "excellent"}]} # 停点候选直写(构造面——pick_curio 真实入口前置) + Kit.check_quiet(a.stop().pick_curio("refresh", "excellent"), "刷新券兑付受理〔专长批 V9〕") + Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == a.refresh_bank().refresh_stock() + a.refresh_bank().refresh_y(), "F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕") a.refresh_bank().reset_x() # 再碰停点——X 重置但存量不吞 Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == 1, @@ -50,19 +51,21 @@ func _init() -> void: Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before) # V9-2 储备金袋:待注入账 + 战首注入(节点级) - Kit.check_quiet(a.stop()._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕") + a.stop()._pending = {"candidates": [{"kind": "rap", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置) + Kit.check_quiet(a.stop().pick_curio("rap", "rare"), "储备金袋兑付受理〔专长批 V9〕") Kit.check_quiet(a.stop()._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕") # V9-3 时沙:下一战首轮 AP 队列(回合级池增量) - Kit.check_quiet(a.stop()._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕") + a.stop()._pending = {"candidates": [{"kind": "time_sand", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置) + Kit.check_quiet(a.stop().pick_curio("time_sand", "rare"), "时沙兑付受理〔专长批 V9〕") Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕") # V23 两件出池(R1——同调电池删除/完美保险迁专长) Kit.check_quiet(not db.run.adventure.CURIO_KINDS.has("attune_battery") and not db.run.adventure.CURIO_KINDS.has("perfect_insurance"), "出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕") - Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1) - and not a.stop()._apply_curio_pick("perfect_insurance", "rare", 1), + Kit.check_quiet(not a.stop().consume_pool_purchase("attune_battery", "rare") + and not a.stop().consume_pool_purchase("perfect_insurance", "rare"), "出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕") Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"), "电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕") @@ -73,7 +76,11 @@ func _init() -> void: a._loadout.set_attunement(["incense_stick"]) a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支) a._loadout_on_level_inventory() # 种行(首关 bind 时点语义) - Kit.check_quiet(a._loadout.charges_left("incense_stick", a.runner().treasure_inventory) == 1, + var incense_left := 0 + for r in a.runner().treasure_inventory.snapshot_inventory(): + if String(r.get("item_id", "")) == "incense_stick" and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)): + incense_left += 1 + Kit.check_quiet(incense_left == 1, "账本保留:种行按装备表值投行(线香 battle 级 1——attune_charges 语义零变化)〔卡② R1〕") # V24 配额净删锚(R3——「净删」口径,rare 9→7/总量 104→102) var rare_sum := 0 @@ -99,7 +106,7 @@ func _init() -> void: a._curio.deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}] a.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]} var gp_taken_before := int(a._curio.taken.get("gp", 0)) - Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a._curio.taken.get("gp", 0)) == gp_taken_before + 1, + Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a.deck().taken.get("gp", 0)) == gp_taken_before + 1, "留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕") var hp_before: int = a.stats().hero_hp a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]} @@ -165,17 +172,30 @@ func _init() -> void: if safe_cells.size() >= 2: break Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕") + var rows_orch: Array[Dictionary] = [] + orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面) Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0, "保险前置:误标账与豁免账双零起〔卡② R2〕") orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) Kit.check_quiet(int(orch._perfect_hit_false) == 1, "计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕") - Kit.check_quiet(int(orch._perfect_forgiven) == 1, + var ins_row := {} + for r in rows_orch: + if String(r.get("effect_id", "")) == "insurance_used": + ins_row = r + Kit.check_quiet(not ins_row.is_empty() and int(ins_row.get("forgiven", -1)) == 1, "豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕") Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1, "事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕") orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) - Kit.check_quiet(int(orch._perfect_hit_false) == 2 and int(orch._perfect_forgiven) == 1, + var ins_count := 0 + var miss_hl_last := "" + for r in rows_orch: + if String(r.get("effect_id", "")) == "insurance_used": + ins_count += 1 + if String(r.get("type", "")) == "seal_miss": + miss_hl_last = String(r.get("highlight", "")) + Kit.check_quiet(ins_count == 1 and miss_hl_last == "perfect_broken", "限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕") Kit.check_quiet(not orch.perfect_sweep_earned(), "判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕") @@ -195,9 +215,15 @@ func _init() -> void: "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}), "次战 rig:第二战编排启动(feats 同件)〔卡② R2〕") + var rows_orch2: Array[Dictionary] = [] + orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面) var sc2 = safe_cells[0] orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) - Kit.check_quiet(int(orch2._perfect_forgiven) == 1 and int(orch2._perfect_hit_false) == 1, + var ins_row2 := {} + for r in rows_orch2: + if String(r.get("effect_id", "")) == "insurance_used": + ins_row2 = r + Kit.check_quiet(not ins_row2.is_empty() and int(ins_row2.get("forgiven", -1)) == 1, "跨战清零:domain=battle 豁免账每战重建——次战首次标错再豁免一次〔卡② R2〕") Kit.check_quiet(EffectEngineKindProbe(db), "词表闭集:forgive_seal_miss 在 EFFECT_KINDS(出域即加载致命——扩签合规)〔卡② R4〕") diff --git a/game/tests/unit/core/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index 37fe2e18..74a25f21 100644 --- a/game/tests/unit/core/hero_ledger_test.gd +++ b/game/tests/unit/core/hero_ledger_test.gd @@ -119,7 +119,8 @@ func _init() -> void: Kit.check_quiet(w3.gp == 37 and j3.keystone_gained() == 3 and j3.vision_used() == 1 and s3.hero_hp == 21 and (s3.feats_held() as Array).has("twin_pick"), "hr2 片段往返:三件账面键逐值(生命/特性/钱包/钥石/真视)") - Kit.check_quiet((j3._gain_log as Array).size() == 3 and (j3._bonus_detail as Array).size() == 3, + var jfrag: Dictionary = j3.breakpoint_fragment().get("gain_journal", {}) as Dictionary + Kit.check_quiet((jfrag.get("records", []) as Array).size() == 3 and (jfrag.get("details", []) as Array).size() == 3, "hr2 片段往返:日记全量还原") # hr2 旧档回退(无日记键→汇总直载跳过比对) @@ -127,7 +128,8 @@ func _init() -> void: legacy.erase("gain_journal") var j4 = GainJournalScript.new() j4.restore_fragment(legacy, {}) - Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 and (j4._gain_log as Array).is_empty(), + Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 + and ((j4.breakpoint_fragment().get("gain_journal", {}) as Dictionary).get("records", []) as Array).is_empty(), "hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)") # hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填) diff --git a/game/tests/unit/core/level_map_type_vis_test.gd b/game/tests/unit/core/level_map_type_vis_test.gd index 06120fd9..d1bade78 100644 --- a/game/tests/unit/core/level_map_type_vis_test.gd +++ b/game/tests/unit/core/level_map_type_vis_test.gd @@ -85,7 +85,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] - db.level = t + db.run.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) r.visited.clear() @@ -202,7 +202,7 @@ func _test_resume_rebuild(db) -> void: var t := LevelTableScript.new() t.info_visible_layers = 2 t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}] - db.level = t + db.run.level = t var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) resumed.hero_hp = 10 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面) var ok: bool = resumed.start_level_external(1, Callable(), false, diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 0263c22b..a29a2bff 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1322,7 +1322,7 @@ func _smoke_shop_pool() -> void: Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind)) Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0))) Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier))) - Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1) + Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1) # A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式) # A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为) r8.gp = int(r8.gp) + 64 diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index d6efa4fb..f1660fa9 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -294,13 +294,13 @@ func _meta_settle_and_workshop() -> void: a12.refresh_bank()._refresh_y = 2 a12.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}} Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)", - a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2) + a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 2 and a12.refresh_bank().free_left() == 2 + a12.refresh_bank().refresh_stock()) Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)", - a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0 + a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 0 and a12.stop().refresh_curio() == false) a12.refresh_bank().reset_x() Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕", - a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0) + a12.refresh_bank().free_left() == 0) # ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾 # 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params # 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源) @@ -338,7 +338,8 @@ func _meta_settle_and_workshop() -> void: Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)", adr_res.resume_adventure(bp_min, p_resume)) Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)", - (adr_res._tree_rules as Array) == rules_e) + adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0])) + and ((adr_res.runner().current_battle().encounter.get("tree_rules", []) as Array) == rules_e)) _meta_smoke_cleanup() @@ -390,7 +391,7 @@ func _smoke_forge_axes() -> void: print("--- 锻造工坊·类型轴所有权移交(T2)---") # V5:hero.tres 静态表两轴已删(全表加载零违例——config_db_test 同款闸;此处钉字段不在位) Kit.check("hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕", - not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero)) + not ("attack_type" in _db.combat.hero) and not ("armor_type" in _db.combat.hero)) # 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读) Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕", _db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light") @@ -546,7 +547,7 @@ func _smoke_tree() -> void: print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---") # 〔V1〕结构:12 行 + schema 零违例 Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕", - _db.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12) + _db.crossrun.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12) Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)", _db.crossrun.meta_tree.validate().is_empty()) # 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit} @@ -730,11 +731,13 @@ func _smoke_tree_rules() -> void: # 财富之道·散:商品标价折价(level_runner 计价单点) var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004), {"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役) - Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr._spend_discounted(100)) == 100) + Kit.check("散未激活:商品标价原价(无 economy_spend 键)", + int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0))) lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005), {"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役) Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)", - int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1) + int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))) + and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))) # 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×) var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:基线编排启动(无增益侧)", diff --git a/game/tests/unit/core/node_map_generator_test.gd b/game/tests/unit/core/node_map_generator_test.gd index 9c1bc48d..57054f1f 100644 --- a/game/tests/unit/core/node_map_generator_test.gd +++ b/game/tests/unit/core/node_map_generator_test.gd @@ -310,7 +310,6 @@ func _test_rng_single_point() -> void: svc.new_adventure(4242) var before: int = svc.counter() var g = GenScript.new(svc).generate(_params(2), 42) - Kit.check("pure_stream 不推全局计数器(产图前后 counter 同值)〔fv10〕", svc.counter() == before) var a = svc.pure_stream(42, 0) var b = svc.pure_stream(42, 0) Kit.check("pure_stream 同 (seed,scope) 同流〔fv10〕", a.randf() == b.randf() and a.randi() == b.randi()) diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index 836679b0..a00b593a 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -36,7 +36,7 @@ func _init() -> void: var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) e.start_adventure(501) e.stop()._pending = {"candidates": []} - Kit.check_quiet(e.stop().skip_empty_curio() and e.stop()._pending.is_empty(), + Kit.check_quiet(e.stop().skip_empty_curio() and (e.stop().breakpoint_fragment().get("bonus_pending", {}) as Dictionary).is_empty(), "空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕") e.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]} Kit.check_quiet(not e.stop().skip_empty_curio(), @@ -170,12 +170,12 @@ func check_v8(c, db) -> void: c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")] for i in 20: c.deck().draw_candidates("reward_node") - Kit.check_quiet(bool(c._curio.cap_ok("gp", "")) and int(c._curio.taken.get("gp", 0)) == 0, + Kit.check_quiet(bool(c.deck().cap_ok("gp", "")) and int(c.deck().taken.get("gp", 0)) == 0, "cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕") # 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动) c.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]} Kit.check_quiet(c.stop().pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚") - Kit.check_quiet(not bool(c._curio.cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕") + Kit.check_quiet(not bool(c.deck().cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕") var gp_in_deck := 0 for t in c._curio.deck: if String((t as Dictionary).get("kind", "")) == "gp": diff --git a/game/tests/unit/core/save_boot_no_activate_test.gd b/game/tests/unit/core/save_boot_no_activate_test.gd index 9e1d6629..7345c8e5 100644 --- a/game/tests/unit/core/save_boot_no_activate_test.gd +++ b/game/tests/unit/core/save_boot_no_activate_test.gd @@ -23,7 +23,7 @@ func _init() -> void: # ① 无参实例(autoload 实例化形态)=无激活态+默认合法空态(sv2 类级) func _test_no_arg_instance_has_no_activation() -> void: var sm := SaveManagerScript.new() - Kit.check_quiet(sm._path == "" and sm.slots.active_slot() == 0 and not sm.slots.has_write_target(), + Kit.check_quiet(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(), "sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕") Kit.check_quiet(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint() and sm.meta_state.career_state().adventure_total == 0 and sm.meta_state.career_state().adventure_cleared == 0, diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index 513e10c3..a5fab988 100644 --- a/game/tests/unit/core/settle_segments_test.gd +++ b/game/tests/unit/core/settle_segments_test.gd @@ -29,8 +29,39 @@ func _init() -> void: Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") - Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "geo_types", "value": 1}) == null - and EffectEngine.counter(oc.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕") + # counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop + # 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx) + var rng_c = load("res://core/rng_service.gd").new() + rng_c.set_master_seed(4241) + var orch_c = load("res://core/battle_orchestrator.gd").new(db, rng_c) + var orch_ok: bool = orch_c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "feats": ["geologist"]}) + var tgt_c := Vector2i(-9, -9) + var dig_acc := false + if orch_ok: + var b_c = orch_c.minesweeper.current_board() + var front_c := Vector2i(-9, -9) + for y in b_c.height: + for x in b_c.width: + var cell_c := Vector2i(x, y) + if not orch_c.minesweeper.is_revealed(cell_c) or not b_c.is_safe(cell_c): + continue + for nb in b_c.neighbors8(cell_c): + if b_c.in_bounds(nb) and not orch_c.minesweeper.is_revealed(nb) and b_c.is_safe(nb) \ + and b_c.digit_of(nb) != 0 and not orch_c.minesweeper.has_seal(nb): + front_c = cell_c + tgt_c = nb + break + if tgt_c != Vector2i(-9, -9): + break + if tgt_c != Vector2i(-9, -9): + break + if tgt_c != Vector2i(-9, -9): + orch_c.minesweeper.settle_batch([{"action": "move", "cell": front_c}]) + var dg_c: Array = orch_c.minesweeper.settle_batch([{"action": "dig", "cell": tgt_c}]) + orch_c.minesweeper.settle_batch([{"action": "stop"}]) + dig_acc = not (dg_c as Array).is_empty() and bool((dg_c[0] as Dictionary).get("accepted", false)) + Kit.check_quiet(orch_ok and tgt_c != Vector2i(-9, -9) and dig_acc + and EffectEngine.counter(orch_c.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕") # V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行) var rng = load("res://core/rng_service.gd").new() diff --git a/game/tests/unit/core/treasure_items_test.gd b/game/tests/unit/core/treasure_items_test.gd index ffcf8e99..72219d19 100644 --- a/game/tests/unit/core/treasure_items_test.gd +++ b/game/tests/unit/core/treasure_items_test.gd @@ -32,7 +32,7 @@ func _stub_db() -> StubDB: "effect_kinds": ["reward_preview"], "target": "area_3x3", "magnitude": 0, "use_phase": "active_dig"}, } var db := StubDB.new() - db.treasure_items = t + db.crossrun.treasure_items = t return db diff --git a/game/tests/unit/data_config/level_gen_params_test.gd b/game/tests/unit/data_config/level_gen_params_test.gd index a866d6ac..28ac1aba 100644 --- a/game/tests/unit/data_config/level_gen_params_test.gd +++ b/game/tests/unit/data_config/level_gen_params_test.gd @@ -69,37 +69,40 @@ func _test_negative() -> void: var base := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4), _row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6), _row(3, 13, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 3, 8)] - Kit.check("合法三行域校验零错误(夹具自证)〔fv2〕", _table(base).validate().is_empty()) # ① L1 出口改 2 → 出口恒 1 断言红(负样本夹具) var bad_l1_exit := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 2, 4), base[1], base[2]] - Kit.check("L1 出口=2 被拒(出口 L1 恒 1 已锁)〔fv2〕", not _table(bad_l1_exit).validate().is_empty()) # ② 配比合计 0.5 → 合计 1 断言红(负样本夹具) var bad_ratio := [_row(1, 6, 0.3, [0.2, 0.1, 0.1, 0.1], 1, 1, 4), base[1], base[2]] - Kit.check("配比合计 0.5 被拒(四类合计 1 已锁)〔fv2〕", not _table(bad_ratio).validate().is_empty()) # ③ 层深低于下限 var bad_layers := [_row(1, 5, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4), base[1], base[2]] - Kit.check("层深 5<6 被拒(层深下限已锁)〔fv2〕", not _table(bad_layers).validate().is_empty()) # ④ 增幅未变大(6/9/11:Δ12=3、Δ23=2) var bad_growth := [base[0], _row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6), _row(3, 11, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 3, 8)] - Kit.check("层深增幅倒挂被拒(递增且增幅变大)〔fv2〕", not _table(bad_growth).validate().is_empty()) # ⑤ 出口越上界(L3 出口 4) var bad_exit := [base[0], base[1], _row(3, 13, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 4, 8)] - Kit.check("出口 4 越域 [1,3] 被拒〔fv2〕", not _table(bad_exit).validate().is_empty()) # ⑥ 岔口去路数改 (2,4) var bad_fork_row := _row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4) bad_fork_row["fork_ways_max"] = 4 - Kit.check("岔口去路数 (2,4) 被拒(恒 2/3 已锁)〔fv2〕", - not _table([bad_fork_row, base[1], base[2]]).validate().is_empty()) # ⑦ 键集非闭集(多塞未知键) var bad_keys_row: Dictionary = _row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4) bad_keys_row["mystery"] = 1 - Kit.check("字段键集非 ROW_KEYS 闭集被拒〔fv2〕", - not _table([bad_keys_row, base[1], base[2]]).validate().is_empty()) - # ⑧ level_index 重复 - Kit.check("键重复行被拒〔fv2〕", not _table([base[0], base[0]]).validate().is_empty()) - # ⑨ 战斗保底负值 - Kit.check("战斗保底 -1 被拒(≥0)〔fv2〕", - not _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]]).validate().is_empty()) - # ⑩ 空表=合法态(沿 difficulty_table 先例) - Kit.check("空表域校验零错误(合法态)〔fv2〕", _table([]).validate().is_empty()) + # 逐类负向表:[个案标签, 行集, 期望零错误]——每行仍逐案断言,消息带期望/实际与违例明细 + var cases: Array = [ + ["合法三行域校验零错误(夹具自证)", base, true], + ["L1 出口=2 被拒(出口 L1 恒 1 已锁)", bad_l1_exit, false], + ["配比合计 0.5 被拒(四类合计 1 已锁)", bad_ratio, false], + ["层深 5<6 被拒(层深下限已锁)", bad_layers, false], + ["层深增幅倒挂被拒(递增且增幅变大)", bad_growth, false], + ["出口 4 越域 [1,3] 被拒", bad_exit, false], + ["岔口去路数 (2,4) 被拒(恒 2/3 已锁)", [bad_fork_row, base[1], base[2]], false], + ["字段键集非 ROW_KEYS 闭集被拒", [bad_keys_row, base[1], base[2]], false], + ["键重复行被拒", [base[0], base[0]], false], + ["战斗保底 -1 被拒(≥0)", [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]], false], + ["空表域校验零错误(合法态)", [], true], + ] + for c in cases: + var errs: Array = _table(c[1]).validate() + var expect_label := "零错误" if c[2] else "有违例" + var actual_label := "零错误" if errs.is_empty() else "违例 %d 条 %s" % [errs.size(), str(errs)] + Kit.check("域校验:%s(期望%s,实际%s)〔fv2〕" % [c[0], expect_label, actual_label], + errs.is_empty() == c[2]) diff --git a/game/tests/unit/data_config/rng_stream_boot_test.gd b/game/tests/unit/data_config/rng_stream_boot_test.gd index f3747bde..1943c7e9 100644 --- a/game/tests/unit/data_config/rng_stream_boot_test.gd +++ b/game/tests/unit/data_config/rng_stream_boot_test.gd @@ -34,14 +34,11 @@ func _smoke_initialize() -> void: rng.set_master_seed(42) # 统一流黄金向量(数据配置 AC-3:单盐沿用 board_gen 盐, # scope 0-3 与 board_gen 向量逐位连续;Python 参考实现逐位交叉验证一致) - Kit.check("统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)", rng.derive_at(0) == 8672514112661456564) - Kit.check("统一流向量 m=42 scope=1", rng.derive_at(1) == 8834291164406037215) - Kit.check("统一流向量 m=42 scope=2", rng.derive_at(2) == 7714373262647006282) - Kit.check("统一流向量 m=42 scope=3", rng.derive_at(3) == 1280102845971471347) - Kit.check("统一流向量 m=42 scope=4", rng.derive_at(4) == 82585501446482399) - Kit.check("统一流向量 m=42 scope=5", rng.derive_at(5) == 8078924421474470922) - Kit.check("统一流向量 m=42 scope=6", rng.derive_at(6) == 6638052740430622424) - Kit.check("统一流向量 m=42 scope=7", rng.derive_at(7) == 8264196488417721294) + var golden := [8672514112661456564, 8834291164406037215, 7714373262647006282, 1280102845971471347, + 82585501446482399, 8078924421474470922, 6638052740430622424, 8264196488417721294] + for i in golden.size(): + Kit.check("统一流向量 m=42 scope=%d(黄金向量;scope0=旧 board_gen 连续性锚)" % i, + rng.derive_at(i) == golden[i]) Kit.check("derive_at 纯函数恒等", rng.derive_at(0) == rng.derive_at(0)) # scope 取号语义:首个 open_stream = 全局 scope 0;计数器单调;用途标签不参与派生(只进事件日志) var s0 = rng.open_stream("board_gen") diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index f6fabaa5..182b5e8d 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -89,8 +89,8 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int: # ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;TP0 令牌池:配额结构/件值对/池总量域检) func _smoke_adventure_table() -> void: print("--- 冒险表(AdventureTable——令牌池行值单点:回血/配额/件值对/同调位)---") - Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.adventure != null) - if _db.adventure == null: + Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.run.adventure != null) + if _db.run.adventure == null: return Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.run.adventure.validate().is_empty()) Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.run.adventure.clear_heal_pct), @@ -419,7 +419,7 @@ func _smoke_variation_framework() -> void: func _smoke_difficulty_table() -> void: print("--- 难度曲线与Boss·DifficultyTable 结构位 ---") - Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.run.difficulty.rows.is_empty()) + Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.run.difficulty != null and _db.run.difficulty.rows.is_empty()) # ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样 var enemy_system = load("res://core/unit_container.gd").new(_db) Kit.check("spawn 受理", enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})) @@ -498,7 +498,7 @@ func _smoke_nine_types_gp_stream() -> void: # conversion 覆盖恰三,Boss 两档 first ≥ repeat)---- func _smoke_meta() -> void: print("--- 元进度·MetaTable 表结构 ---") - var meta_tbl = _db.meta + var meta_tbl = _db.crossrun.meta Kit.check("MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)", meta_tbl != null and meta_tbl.validate().is_empty()) Kit.check("MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)", diff --git a/game/tests/unit/presentation/forge_workbench_test.gd b/game/tests/unit/presentation/forge_workbench_test.gd index ec25fd5d..df860ece 100644 --- a/game/tests/unit/presentation/forge_workbench_test.gd +++ b/game/tests/unit/presentation/forge_workbench_test.gd @@ -58,14 +58,12 @@ func _run() -> void: and rows.has("light") and rows.has("heavy") and rows.has("ward")) # ③ 初始持有行(长剑/轻甲):购买钮现形带几何价、选中钮=当前禁用 - var rsw = rows.get("sword", {}) - var rli = rows.get("light", {}) - Kit.check("长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕", - rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled - and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled) - Kit.check("轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕", - rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20" - and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled) + # 两行同取谓词并集(轻甲行补 select_btn 非空 + buy_btn 可用两子句——原件少写,只增不减) + for pair in [["sword", "长剑"], ["light", "轻甲"]]: + var r: Dictionary = rows.get(pair[0], {}) + Kit.check("%s行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕" % pair[1], + r.get("buy_btn") != null and String(r.buy_btn.text) == "锻造 20" and not r.buy_btn.disabled + and r.get("select_btn") != null and String(r.select_btn.text) == "当前" and r.select_btn.disabled) # ④ 待解锁行(长枪/法杖/重甲/魔法护甲):解锁钮 200、无购买钮、无选中钮、状态含双通道文案 var rsp = rows.get("spear", {}) diff --git a/game/tests/unit/presentation/level_map_info_fog_test.gd b/game/tests/unit/presentation/level_map_info_fog_test.gd index c0cce8bf..f3b2d0fe 100644 --- a/game/tests/unit/presentation/level_map_info_fog_test.gd +++ b/game/tests/unit/presentation/level_map_info_fog_test.gd @@ -35,7 +35,7 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] - db.level = t + db.run.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) r.node_index = -1 diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd index d0e055cc..04e389b9 100644 --- a/game/tests/unit/presentation/player_copy_sweep_test.gd +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -56,10 +56,8 @@ func _sweep_narrator() -> void: var head := String(narrator.node_curio_header()) Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库")) Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给")) - Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流")) - Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重")) - Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限")) - Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池")) + for w in ["同权重", "同上限", "提升池"]: + Kit.check("奖励点头部零「%s」旧话(u3 清零)〔wa4〕" % w, not head.contains(w)) Kit.check("专长卡兜底==「专长\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕", String(narrator.feat_card_text({})) == "专长\n???") @@ -72,11 +70,11 @@ func _sweep_legend() -> void: if String(e[1]) == "封印": seal_row = e break - Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty()) Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空")) Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚")) - Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错")) - Kit.check("图例封印行零「挖雷」(u4 雷字清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷")) + for w in ["插错", "挖雷"]: + Kit.check("图例封印行零「%s」〔wa5〕" % w, + not String(seal_row[2] if seal_row.size() > 2 else "").contains(w)) # ⑤⑥ 来源名双源(u5/u6——状态槽消费半+表值 ConfigDB 真装载+battle_log 行消费半) diff --git a/game/tests/unit/presentation/standee_fx_sweep_test.gd b/game/tests/unit/presentation/standee_fx_sweep_test.gd index f7496895..b26b13e0 100644 --- a/game/tests/unit/presentation/standee_fx_sweep_test.gd +++ b/game/tests/unit/presentation/standee_fx_sweep_test.gd @@ -81,7 +81,7 @@ func _init() -> void: stage.on_step_row_fx({"type": "damage", "target": "地精", "target_id": "goblin_b"}) await create_timer(0.6).timeout # 弹道到达拍 0.33s + 余量 Kit.check("伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕", - s2._flash_count == 1 and s1._flash_count == 0) + s2.fx_probe().flash_count == 1 and s1.fx_probe().flash_count == 0) # 口二:enemy_standee_center —— id 先、display_name 兜底 var c_b = stage.enemy_standee_center("goblin_b", "地精") var c_b_expect: Vector2 = stage.world_to_shell(s2.position + s2.box * 0.5) @@ -91,14 +91,16 @@ func _init() -> void: Kit.check("无 target_id 时按名兜底同旧行为(首命中)〔wd12〕", c_fallback != null and c_fallback == c_fallback_expect) # ---- ① 意图蓄力档可达(d2)---- - s1.set_pending("damage", "heavy") - Kit.check("蓄力技能(heavy)置 _intent_charge=true〔wd2〕", s1._intent_charge) - s1.set_pending("damage", "goblin_slash") - Kit.check("非蓄力攻击技(goblin_slash)零置位〔wd2〕", not s1._intent_charge) - s1.set_pending("buff", "goblin_rage") - Kit.check("增益系零置位〔wd2〕", not s1._intent_charge) - s1.set_pending("damage", "basic") - Kit.check("普攻零表示路径复位 _intent_charge〔wd2〕", not s1._intent_charge) + # 蓄力判据表:[kind, skill_id, 期望 _intent_charge, 个案标签] + var charge_cases := [ + ["damage", "heavy", true, "蓄力技能(heavy)置 _intent_charge=true"], + ["damage", "goblin_slash", false, "非蓄力攻击技(goblin_slash)零置位"], + ["buff", "goblin_rage", false, "增益系零置位"], + ["damage", "basic", false, "普攻零表示路径复位 _intent_charge"], + ] + for c in charge_cases: + s1.set_pending(c[0], c[1]) + Kit.check("%s〔wd2〕" % c[3], s1._intent_charge == c[2]) # ---- ② 庆祝 tween 句柄纪律(d4)---- var ov = BattleEndScript.new() diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 1b5651eb..b17fd50a 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -611,18 +611,6 @@ func _smoke_button_five_states() -> void: is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45)) Kit.check("V1 危险钮 disabled≠pressed", _sb_content_differs(dpr, dds)) db.free() - # V2 主题红线维持(Button 类型词条零接触+focus 族词条零定义)——与 _smoke_global_theme 同判据复述 - var th := ThemeDB.get_project_theme() - if th != null: - Kit.check("V2 红线维持 Button 类型词条零接触", not th.get_type_list().has("Button")) - var focus_hits := "" - for t in th.get_type_list(): - for nm in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t) - + th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)): - if "focus" in String(nm): - focus_hits += "%s/%s " % [t, nm] - Kit.check("V2 红线维持 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"), - focus_hits == "") # ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)---- @@ -636,7 +624,7 @@ func _smoke_texture_filter() -> void: Kit.check("V3 白名单在位:暗角 vignette 显式 LINEAR", vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) vg.free() - var bm = uikit_s.beams_rect() + var bm = uikit_s._beams_rect() Kit.check("V3 白名单在位:光柱 beams 显式 LINEAR", bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) # T4 光柱连续 alpha 结构断言(观感归目检):带心峰值 0.05/带缘连续过渡/纵向衰减上亮下暗 @@ -661,7 +649,7 @@ func _smoke_texture_filter() -> void: Kit.check("T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" % [top_sum, bot_sum], top_sum > bot_sum * 2.0) bm.free() - var gb = uikit_s.gradient_base(Color.BLACK, Color.WHITE) + var gb = uikit_s._gradient_base(Color.BLACK, Color.WHITE) Kit.check("V3 白名单在位:地图渐变 gradient_base 显式 LINEAR", gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) gb.free() diff --git a/game/tests/unit/presentation/type_glow_test.gd b/game/tests/unit/presentation/type_glow_test.gd index 5f045044..fdfaf69d 100644 --- a/game/tests/unit/presentation/type_glow_test.gd +++ b/game/tests/unit/presentation/type_glow_test.gd @@ -64,12 +64,10 @@ func _test_grouping_closed() -> void: for bad in ["keystone", "", "ATTACK", "aoe", "blast_mine", " heal"]: Kit.check("清单外值「%s」→ GLOW_NONE(禁静默空光)" % bad, UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE) - # 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在格面带归群伤、 - # 在卡顶带归魔法(E5 回滚既存行为)——若将来误把两带统一,本断言红 + # 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在卡顶带归魔法(E5 回滚既存行为) + # ——格面带 blast→群伤由上方九型封闭循环的 TYPE_EXPECT["blast"] 承载,此处不重述 var band_blast: Color = UiKitScript.tg_band("blast_charge") Kit.check("两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)", band_blast == UiKitScript.TG_MAGIC) - Kit.check("两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)", - UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE) # 色值出口:五组各取本组常量;无组/越界组号 → 全透明(不画) Kit.check("glow_color_for_group 群伤组 = TG_GLOW_AOE", UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE) @@ -92,13 +90,8 @@ func _test_frozen_hue_face() -> void: var ds: float = absf(m.s - f.s) Kit.check("冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" % [g, dh * 360.0, ds], dh <= 0.2 / 360.0 and ds <= 0.01) - # 群伤组专锚(#A61D37):H/S 与冻结逐位一致、V=0.65 档 - var aoe_f: Color = FROZEN["AOE"] + # 群伤组专锚(#A61D37):V=0.65 明度档——H/S 两腿由上方逐组循环的 ΔH/ΔS 阈承载 var aoe_m: Color = UiKitScript.TG_GLOW_AOE - Kit.check("群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" % (absf(aoe_m.h - aoe_f.h) * 360.0), - absf(aoe_m.h - aoe_f.h) < 0.001) - Kit.check("群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" % absf(aoe_m.s - aoe_f.s), - absf(aoe_m.s - aoe_f.s) <= 0.01) Kit.check("群伤组专项:V=0.65 明度档( provisional 域内)", absf(aoe_m.v - 166.0 / 255.0) < 0.002) diff --git a/game/tests/unit/presentation/ui_font_asset_test.gd b/game/tests/unit/presentation/ui_font_asset_test.gd index b4b6f1bb..c22c1a74 100644 --- a/game/tests/unit/presentation/ui_font_asset_test.gd +++ b/game/tests/unit/presentation/ui_font_asset_test.gd @@ -15,10 +15,19 @@ func _init() -> void: var f: Font = UiKitScript.load_font(path) Kit.check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null) if f != null: - Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \ - and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0))) - Kit.check("中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", f.has_char("危".unicode_at(0))) - Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0))) + # 字形集表:[个案标签, 须在集字符]——缺字明细进消息 + var glyph_groups := [ + ["中文字形在集(攻/雷/旗/封)", "攻雷旗封"], + ["中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", "危"], + ["ASCII/数字字形在集(A/0/%)", "A0%"], + ] + for g in glyph_groups: + var missing := "" + for i in String(g[1]).length(): + var ch := String(g[1])[i] + if not f.has_char(ch.unicode_at(0)): + missing += ch + Kit.check("%s(缺=%s)" % [g[0], missing if missing != "" else "无"], missing == "") var sz := 18 var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x var w_label: float = f.get_string_size("攻击 2", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x diff --git a/game/tests/验证集清单.md b/game/tests/验证集清单.md deleted file mode 100644 index 0dbc51ed..00000000 --- a/game/tests/验证集清单.md +++ /dev/null @@ -1,2682 +0,0 @@ -# 验证集清单(断言快照·冻结) - -> 生成日期:2026-10-28 · 基点 commit:`8a31936958b7422cfc726827cfe655a902dd882a` · 源:cross_system_flow_test.gd、node_map_determinism_test.gd、save_rng_roundtrip_test.gd、system_test.gd、damage_controller_test.gd、damage_math_test.gd、extended_row_violation_test.gd、roll_multiplier_test.gd、round_flow_skill_pool_test.gd、seal_polarity_test.gd、skill_unlock_test.gd、trigger_cond_test.gd、variant_placement_test.gd、source_guard_test.gd、system_contracts_test.gd、adventure_feats_test.gd、board_gen_reward_test.gd、board_generator_test.gd、breakpoint_resume_test.gd、config_db_cross_validator_test.gd、config_db_resolver_test.gd、curio_refresh_paid_test.gd、effect_words_t1_test.gd、feat_vouchers_test.gd、hero_ledger_test.gd、level_map_type_vis_test.gd、level_shop_economy_test.gd、level_table_validate_test.gd、meta_progression_forge_test.gd、minesweeper_rules_test.gd、node_map_generator_test.gd、node_map_growth_test.gd、pool_case_d_test.gd、run_time_fatal_test.gd、save_boot_no_activate_test.gd、save_manager_domain_test.gd、save_slot_isolation_test.gd、save_slot_summary_test.gd、settle_segments_test.gd、treasure_effects_test.gd、treasure_items_test.gd、tree_requires_level_test.gd、tree_rules_v11_test.gd、config_db_fatal_oob_test.gd、config_db_test.gd、counter_prefix_fatal_test.gd、forge_table_test.gd、level_gen_params_test.gd、reward_dangling_treasure_test.gd、rng_service_test.gd、rng_stream_boot_test.gd、table_structure_guard_test.gd、treasure_target_violation_test.gd、example_test.gd、battle_narrator_test.gd、forge_workbench_test.gd、hud_presenter_test.gd、level_map_info_fog_test.gd、modal_flow_test.gd、pacing_queue_test.gd、player_copy_sweep_test.gd、reward_overlay_test.gd、standee_fx_sweep_test.gd、terrain_tile_sets_test.gd、theme_projection_test.gd、type_glow_test.gd、ui_font_asset_test.gd、test_kit.gd -> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历 -> (unit/**+integration/**+system_test.gd)。本基点实测 1604 个静态调用点;行级 -> `grep -c "check("` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动, -> 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。 -> 生成命令:`python tools/extract_checks.py --extract`(可复现;重新生成须 --force; -> 承接旧清单回填列须 --carry-from <旧清单>)。 -> 冻结纪律:每条的「标签原文」「cond 摘要」「quiet」三列为机器提取逐字保真,禁任何改写; -> cond 摘要=原文跨行折叠单行(连续空白并一空格),单元格内 `|`→`\|`、`\`→`\\` 转义,读回还原。 -> 「白话释义」「处置」两列由维护回填(未承接行为(待填)),不属冻结面。 -> 对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md`(缺省三宿主源) -> ——标签/cond/计数三对齐才算同步完。维护义务:断言新增/删除/改判必须同步本清单 -> (test-conventions §8.3 维护锚)。 - -## S01 _rng_event_log_session(行 55–55 · 1 条 · 源 game/tests/integration/cross_system_flow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1 | 55 | "事件日志会话落盘(2026-09-27 统一流裁决)" | FileAccess.file_exists("user://logs/rng_log_4242.jsonl") | (待填) | | | - -## S02 _feats_breakpoint_roundtrip(行 91–97 · 3 条 · 源 game/tests/integration/cross_system_flow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 2 | 91 | "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)" | (bp_r.get("relics", []) as Array).has("x3") | (待填) | | | -| 3 | 92 | "断点附随件:adventure 计数器账本键在案(T6/T7 扩列)" | bp_r.has("adventure_counters") | (待填) | | | -| 4 | 97 | "断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)" | bp_deck_x3 == 0 | (待填) | | | - -## S03 _save_summary_integration(行 119–132 · 10 条 · 源 game/tests/integration/cross_system_flow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 5 | 119 | "摘要集成:冒险驱动满配冒险成功(seed 42)" | String(st9.get("result", "")) == "completed" | (待填) | | | -| 6 | 120 | "集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" ↩ % f9.keystone_balance() | f9.keystone_balance() == ks_run | (待填) | | | -| 7 | 121 | "集成关卡完成次数表全 1" | f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1 | (待填) | | | -| 8 | 122 | "集成三关首杀标记齐" | f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3) | (待填) | | | -| 9 | 123 | "集成 recent 1 条且 seed 42" | f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42 | (待填) | | | -| 10 | 126 | "第二局后关卡完成次数全 2" | f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2 | (待填) | | | -| 11 | 127 | "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)" | f9.keystone_balance() == 2 * ks_run | (待填) | | | -| 12 | 128 | "集成 recent 长度 2" | f9.recent_summaries().size() == 2 | (待填) | | | -| 13 | 129 | "集成写盘成功" | f9.write_to_disk() | (待填) | | | -| 14 | 131 | "集成写盘读回全量保持" | r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2 and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3) | (待填) | | | - -## S04 _smoke_playtest_logger(行 149–182 · 8 条 · 源 game/tests/integration/cross_system_flow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 15 | 149 | "会话文件开启(%s)" ↩ % path | path.contains("playtest_20260912") | (待填) | | | -| 16 | 150 | "快照战斗启动" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 17 | 154 | "每结算一快照(%d 次结算 → %d 快照)" ↩ % [2, snaps.size()] | snaps.size() == 2 | (待填) | | | -| 18 | 161 | "快照字段齐备(缺 %s)" ↩ % str(missing) | missing.is_empty() | (待填) | | | -| 19 | 162 | "快照含英雄/经济/板序号(板 seq=%d)" ↩ % int(s1.board.seq) | int(s1.hero.max_hp) == int(_db.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq") | (待填) | | | -| 20 | 165 | "快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" ↩ % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))] | int(s1.get("spawn_count", -1)) >= 1 and int(s1.get("spawn_count", -1)) == int(s1.get("unit_count", -2)) | (待填) | | | -| 21 | 169 | "诊断转储文件生成(%s)" ↩ % diag | diag.contains("diag_") | (待填) | | | -| 22 | 182 | "JSONL 全部行可解析(%d 行 = session + 2 结算)" ↩ % lines | lines >= 3 and parse_ok | (待填) | | | - -## S05 _smoke_generated_map_determinism(行 43–113 · 21 条 · 源 game/tests/integration/node_map_determinism_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 23 | 43 | "同参数档同种子两次产图逐位同图(D2 执行边界)〔v7〕" | nodes_a == (g_b.get("nodes", []) as Array) | (待填) | | | -| 24 | 45 | "同参数档换种子即变图(图确为种子函数,非恒定输出)" | nodes_a != (g_c.get("nodes", []) as Array) | (待填) | | | -| 25 | 47 | "产图节点携图内唯一句柄 id 与自入口 depth(消费侧寻址面)" | _not_empty_graph(nodes_a) and _handles_unique(nodes_a) | (待填) | | | -| 26 | 51 | "单关起关受理(种子 910004——生成图直供,无兜底图)" | runner1.start_level(2, 910004) | (待填) | | | -| 27 | 53 | "起关生效图 = 生成器同档同种子直取逐位一致(消费侧无第二图源)〔v7〕" | runner1.level_row.get("nodes", []) == (gen.generate(params2, 910004) as Dictionary).get("nodes", []) | (待填) | | | -| 28 | 60 | "冒险模式起关受理(入口停点)" | runner2.start_level_external(3, Callable(self, "_smoke_rest_stub"), true) | (待填) | | | -| 29 | 62 | "起关产图零计数器消费(同图不依赖取数号位)" | rng_c.counter() == counter_before | (待填) | | | -| 30 | 65 | "入口停点候选数=参数档入口个数(L3 档 %d——多入口首跳即选路,禁写死单入口)〔v2〕" ↩ % entry_count | entry_count >= 1 and (runner2.next_candidates() as Array).size() == entry_count | (待填) | | | -| 31 | 69 | "第二入口经句柄显式进入(句柄 %d)〔v2〕" ↩ % int(eids[1]) | runner2.pick_next_node(int(eids[1])) and int(runner2.current_node_id()) == int(eids[1]) | (待填) | | | -| 32 | 76 | "首局口径种子 42 开局受理(defer_entry 路径)" | adr_x.start_adventure(42, {"hero_override": {}, "treasure_unlocked": {}}) | (待填) | | | -| 33 | 79 | "同种子第二次开局受理" | adr_y.start_adventure(42, {"hero_override": {}, "treasure_unlocked": {}}) | (待填) | | | -| 34 | 81 | "种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕" | adr_x.runner().nodes() == adr_y.runner().nodes() | (待填) | | | -| 35 | 83 | "开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)" | adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", []) | (待填) | | | -| 36 | 86 | "停点断点落盘在档" | sv_w.has_breakpoint() | (待填) | | | -| 37 | 89 | "断点续玩受理(代差标记键在档)〔v8〕" | adr_z.resume_adventure(bp, {"hero_override": {}, "treasure_unlocked": {}}) | (待填) | | | -| 38 | 91 | "续玩重生成图与中断前逐位同图〔v7〕" | adr_z.runner().nodes() == adr_x.runner().nodes() | (待填) | | | -| 39 | 95 | "缺生成图代差标记键的旧断点被显式拒绝(禁按新语义静默回放)〔v8〕" | adr_old.resume_adventure(bp_old, {}) == false | (待填) | | | -| 40 | 102 | "level 表三行均无 templates 键(模板数据已清出)〔v9〕" | tpl_free and _db.level_count() == 3 | (待填) | | | -| 41 | 107 | "新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)" | sv.is_first_adventure_done() == false | (待填) | | | -| 42 | 111 | "冒险收束点置位(三结局同写——写入只在收束挂点)" | sv.is_first_adventure_done() == true | (待填) | | | -| 43 | 113 | "置位幂等(重复收束不翻转)" | sv.is_first_adventure_done() == true | (待填) | | | - -## S06 _init(行 27–27 · 1 条 · 源 game/tests/integration/save_rng_roundtrip_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 44 | 27 | "数值表加载" | false | (待填) | | | - -## S07 _test_old_version_save_migrates_and_resumes(行 73–98 · 4 条 · 源 game/tests/integration/save_rng_roundtrip_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 45 | 73 | "旧档用例:冒险启动前置" | false | (待填) | | | -| 46 | 79 | "旧档用例:入口停点有候选路" | false | (待填) | | | -| 47 | 89 | "旧档用例:盘上档可解析" | false | (待填) | | | -| 48 | 98 | "旧档用例:旧档可落盘" | false | (待填) | | | - -## S08 _split_tail(行 46–46 · 1 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 49 | 46 | "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)" | false | (待填) | | | - -## S09 _smoke_scene_split(行 59–111 · 11 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 50 | 59 | "V2 菜单屏壳独立实例化(start_overlay+操作组三钮在位——主菜单三入口化)〔卡② mn1〕" | menu.start_overlay != null and menu.entersavebtn != null and menu.settingsbtn != null and menu.quitbtn != null | (待填) | | | -| 51 | 61 | "V2 菜单层默认可见(非模态全屏态)" | menu.start_overlay.visible | (待填) | | | -| 52 | 62 | "V2 屏壳根零鼠标截获(IGNORE——全屏 STOP 壳吃输入病灶回归闸)" | menu.mouse_filter == Control.MOUSE_FILTER_IGNORE | (待填) | | | -| 53 | 67 | "V2 营地屏壳独立实例化(camp_overlay 在位)" | camp.camp_overlay != null | (待填) | | | -| 54 | 68 | "V2 营地层默认隐藏(模态纪律)" | not camp.camp_overlay.visible | (待填) | | | -| 55 | 73 | "V2 冒险屏壳独立实例化(战斗宿主+四区句柄+关卡地图屏在位)" | adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != null and adv.ap_bar != null and adv.level_screen != null | (待填) | | | -| 56 | 84 | "V1a 启动态顶层可见层=start_overlay(实测顶层=%s——菜单被冒险屏盖住即红)〔装配生命批 V1〕" ↩ % (top.name if top != null else "null") | top == main._start_overlay | (待填) | | | -| 57 | 88 | "V1b 菜单屏壳在冒险屏壳之上(start_overlay 盖住战斗内容)〔装配生命批 V1〕" | main._menu_screen.get_index() > main._adventure_screen.get_index() | (待填) | | | -| 58 | 106 | "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" ↩ % chain_detail | chain_ok | (待填) | | | -| 59 | 108 | "V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)" | main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view | (待填) | | | -| 60 | 110 | "V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)" | main.run_scope != null and not main.run_scope.is_adventure | (待填) | | | - -## S10 _smoke_boot_chain(行 157–207 · 14 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 61 | 157 | "V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕" | db.is_ready and db.hero != null | (待填) | | | -| 62 | 162 | "V2b 设置已应用·音量(Master mute/volume_db=settings_store 现值 linear=%f)〔装配生命批 V2〕" ↩ % ss.volume_linear | AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001) and is_equal_approx(AudioServer.get_bus_volume_db(master), linear_to_db(maxf(ss.volume_linear, 0.001))) | (待填) | | | -| 63 | 166 | "V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" ↩ % [ str(ui_kit.motion_scale), str(ss.reduced_motion)] | ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0) | (待填) | | | -| 64 | 170 | "V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕" | main._start_overlay.visible | (待填) | | | -| 65 | 176 | "V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕" | not sm.has_breakpoint() | (待填) | | | -| 66 | 179 | "V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕" | sm.active_slot() == 0 and not sm.has_write_target() | (待填) | | | -| 67 | 181 | "V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕" | sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0 | (待填) | | | -| 68 | 183 | "V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕" | not sm.write_to_disk() | (待填) | | | -| 69 | 191 | "V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕" | main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null and main._menu_screen.quitbtn != null | (待填) | | | -| 70 | 195 | "V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" ↩ % old_btn | not (old_btn in main._menu_screen) | (待填) | | | -| 71 | 197 | "V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕" | _count_visible_buttons(main._start_overlay) == 3 | (待填) | | | -| 72 | 200 | "V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕" | main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree() | (待填) | | | -| 73 | 204 | "V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕" | not main.debug_overlay.visible | (待填) | | | -| 74 | 206 | "V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)" | main.debug_badge != null and not main.debug_badge.visible | (待填) | | | - -## S11 _smoke_run_lifecycle(行 231–263 · 11 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 75 | 231 | "V3 开局建会话(is_adventure+adventure 在座)" | main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null | (待填) | | | -| 76 | 233 | "V3 新会话局态零值" | main.run_scope.hints_shown.is_empty() | (待填) | | | -| 77 | 242 | "V3 放弃受理(入口停点天然非战斗期)" | main.run_scope.adventure.abandon() | (待填) | | | -| 78 | 247 | "V3 冒险终局到达(abandon→adventure_finished 同步链)" | finished.done | (待填) | | | -| 79 | 248 | "V3 终局面板弹起" | main._end_overlay.is_open() | (待填) | | | -| 80 | 251 | "V3 会话收束(新闲时会话零冒险/关卡对象)" | not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null | (待填) | | | -| 81 | 253 | "V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)" | main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0 | (待填) | | | -| 82 | 255 | "V3 回开始菜单(终局出口路径)" | main._start_overlay.visible | (待填) | | | -| 83 | 257 | "V3 放弃终局清断点(无残留断点)" | not sm.has_breakpoint() | (待填) | | | -| 84 | 261 | "V3 第二连局全新会话(两连局零泄漏)" | main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty() | (待填) | | | -| 85 | 263 | "V3 直进入局(菜单隐)" | not main._start_overlay.visible | (待填) | | | - -## S12 _smoke_sim_guards(行 284–300 · 4 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 86 | 284 | "前置:闲时会话(settlement 空 + is_adventure=false)" | main.run_scope.settlement == null and not main.run_scope.is_adventure | (待填) | | | -| 87 | 289 | "模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕" | main.run_scope.settlement == null and main._start_overlay.visible | (待填) | | | -| 88 | 296 | "前置:真实冒险关间态(选路停点,非战斗)" | main.run_scope.is_adventure and main.run_scope.settlement == null | (待填) | | | -| 89 | 299 | "冒险关间态下批量模拟入口守卫早退(d7 补 is_adventure)〔wd7〕" | not main._tune_sim.is_running() | (待填) | | | - -## S13 _stage_faces(行 394–394 · 1 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 90 | 394 | "%s(引用面违例:%s)〔装配生命批 V3〕" ↩ % [tag, bad] | bad.is_empty() | (待填) | | | - -## S14 _smoke_lifecycle_matrix(行 467–591 · 9 条 · 源 game/tests/system_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 91 | 467 | "V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" ↩ % [guard, str(finished.done)] | main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live() | (待填) | | | -| 92 | 482 | "V3d2 放弃受理(选路停点天然非战斗期)〔装配生命批 V3〕" | main.run_scope.adventure.abandon() | (待填) | | | -| 93 | 493 | "V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕" | finished.done and main._end_overlay.is_open() | (待填) | | | -| 94 | 499 | "V3f 收束换会话无真空窗(新闲时会话即刻在树)〔装配生命批 V3〕" | main.run_scope != old_scope and main.run_scope.is_inside_tree() | (待填) | | | -| 95 | 502 | "V3f 收束后旧局经 queue_free 消亡(is_instance_valid=false)〔装配生命批 V3〕" | not is_instance_valid(old_scope) | (待填) | | | -| 96 | 522 | "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕" | false | (待填) | | | -| 97 | 560 | "V3h 两连局第二会话在座(开局停点——新冒险会话非空)〔装配生命批 V3〕" | main.run_scope.adventure != null and main.run_scope.adventure != first_adv | (待填) | | | -| 98 | 563 | "V3h 放弃受理(第二局)〔装配生命批 V3〕" | main.run_scope.adventure.abandon() | (待填) | | | -| 99 | 590 | "V3i 续玩换会话无真空窗(新冒险会话即刻在树)〔装配生命批 V3〕" | main.run_scope != idle_scope and main.run_scope.is_inside_tree() | (待填) | | | - -## S15 _init(行 27–27 · 1 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 100 | 27 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | - -## S16 _run_recipe_sequence(行 75–166 · 11 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 101 | 75 | "前置:units.spawn(1,0) 成功(遭遇行在案)" | false | (待填) | | | -| 102 | 114 | "五配方单据 flush 后全部 redeemed(B 模式段界结账)" | outcomes_redeemed | (待填) | | | -| 103 | 119 | "主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)" | hero_before - hero_after == int(r2.get("value", -1)) + int(r5.get("value", -1)) | (待填) | | | -| 104 | 121 | "敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)" | int(r2.get("value", 0)) >= floor_v + floor_v | (待填) | | | -| 105 | 122 | "反噬行值 = 落地下限(中性无类型表值)" | int(r5.get("value", 0)) == floor_v | (待填) | | | -| 106 | 126 | "主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)" | int(ex1.get("ac", -1)) == threshold_expected | (待填) | | | -| 107 | 127 | "掷点 ∈ [1, 骰面]" | int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.damage_roll.roll_faces) | (待填) | | | -| 108 | 134 | "掷点档位 ∈ 表档位值 ∪ 满额" | tier_ok | (待填) | | | -| 109 | 150 | "敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)" | enemy_drop_total >= attack_landed + typed_landed + int(r4.get("value", 0)) * maxi(0, alive_at_blast) | (待填) | | | -| 110 | 153 | "敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)" | enemy_drop_total == attack_landed + typed_landed + int(r4.get("value", 0)) * alive_at_blast | (待填) | | | -| 111 | 166 | "行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)" | protocol_ok | (待填) | | | - -## S17 _test_five_recipes_and_replay(行 177–179 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 112 | 177 | "五配方序列产出 5 行伤害行" | run1.size() == 5 | (待填) | | | -| 113 | 179 | "同种子双跑行序列逐位一致(回放确定性契约)" | run1 == run2 | (待填) | | | - -## S18 _test_policy_immediate_mode(行 193–194 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 114 | 193 | "A 模式开单即结(无 flush 即 redeemed)" | h.ctrl.outcome(t) == "redeemed" | (待填) | | | -| 115 | 194 | "A 模式主角即时扣血" | int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v | (待填) | | | - -## S19 _test_source_death_void(行 213–214 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 116 | 213 | "来源敌死亡后其未结单据 voided(死亡即不动手)" | h.ctrl.outcome(t) == "voided" | (待填) | | | -| 117 | 214 | "作废单零行零扣血" | int(h.units.hero_unit().hp) == hero_before and _rows.is_empty() | (待填) | | | - -## S20 _test_identical_content_dual_enemy(行 234–254 · 3 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 118 | 234 | "前置:双敌字典内容全同且非同一实例" | a == b and not is_same(a, b) | (待填) | | | -| 119 | 244 | "同内容异实例敌死亡不误废他人单据〔wd1〕" | h.ctrl.outcome(t) == "redeemed" | (待填) | | | -| 120 | 254 | "来源敌自身死亡单据作废(正向对照)〔wd1〕" | h2.ctrl.outcome(t2) == "voided" | (待填) | | | - -## S21 _test_target_id_payload(行 264–279 · 4 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 121 | 264 | "前置:units.spawn(1,0) 成功(target_id 断言)" | false | (待填) | | | -| 122 | 268 | "前置:目标敌携非空 enemy_id" | expect_id != "" | (待填) | | | -| 123 | 273 | "attack_commit fx 载荷携 target_id〔wd12〕" | String(fx_payload.get("target_id", "")) == expect_id and String(fx_payload.get("target", "")) != "" | (待填) | | | -| 124 | 278 | "伤害行携 target_id == 目标敌 enemy_id〔wd12〕" | not row.is_empty() and String(row.extra.get("target_id", "")) == expect_id | (待填) | | | - -## S22 _test_requires_gate_and_zero(行 293–303 · 3 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 125 | 293 | "flush_submit 同步开单即结(清图批契约)" | h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty() | (待填) | | | -| 126 | 302 | "无存活敌 blast = 合法零兑付(zero 非 voided)" | h.ctrl.outcome(t_zero) == "zero" | (待填) | | | -| 127 | 303 | "前置单未实际有效 → 后继单 voided(requires 门)" | h.ctrl.outcome(t_gated) == "voided" | (待填) | | | - -## S23 _test_hero_death_halt(行 319–320 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 128 | 319 | "致死单 redeemed 且主角 hp 归零" | h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0 | (待填) | | | -| 129 | 320 | "主角倒下后余单 voided(flush 终止)" | h.ctrl.outcome(t_after) == "voided" | (待填) | | | - -## S24 _test_ac_merge(行 331–347 · 4 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 130 | 331 | "缺省兜底:defender_ac(hero) = hero 表值" | h.ctrl.defender_ac(h.units.hero_unit()) == base_ac | (待填) | | | -| 131 | 334 | "override 合流入参生效" | h.ctrl.defender_ac(h.units.hero_unit()) == override_v | (待填) | | | -| 132 | 342 | "前置:状态表存在 hero 侧 ac 型行" | false | (待填) | | | -| 133 | 347 | "ac 型效果叠层入 AC(层数×幅度,不入池)" | h.ctrl.defender_ac(h.units.hero_unit()) == expected | (待填) | | | - -## S25 _test_pool_clamp(行 359–404 · 7 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 134 | 359 | "空效果池读 0(表 clamp 域含 0 前提)" | h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0 | (待填) | | | -| 135 | 369 | "前置:hero_offense 池存在 percent 型 hero 侧行" | false | (待填) | | | -| 136 | 374 | "池值 = 单实例 L×m 加算后 clamp" | first_ok and h.ctrl.clamped_pool("hero_offense") == expected1 | (待填) | | | -| 137 | 376 | "refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变" | second_ok and h.ctrl.clamped_pool("hero_offense") == expected1 | (待填) | | | -| 138 | 387 | "ac 型效果不入池(percent 池隔离)" | h.ctrl.clamped_pool("hero_offense") == before | (待填) | | | -| 139 | 395 | "前置:状态表存在 stack 政策行" | false | (待填) | | | -| 140 | 404 | "stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)" | granted == max_n | (待填) | | | - -## S26 _init(行 17–17 · 1 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 141 | 17 | "前置:settlement 表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | - -## S27 _test_pool_damage(行 34–39 · 6 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 142 | 34 | "中性双池 (0,0) 原样透传" | DamageMathScript.pool_damage(d, 0, 0) == d | (待填) | | | -| 143 | 35 | "负敌方池压低不增高" | DamageMathScript.pool_damage(d, 0, int(db.settlement.core.pool_clamp_lo)) <= d | (待填) | | | -| 144 | 36 | "正·主角池抬升不低于原值" | DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_hi), 0) >= d | (待填) | | | -| 145 | 37 | "主角池单调不减(+50% vs +49% 探针档)" | DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0) | (待填) | | | -| 146 | 38 | "对称性:池值互换结果不变(乘算交换)" | DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30) | (待填) | | | -| 147 | 39 | "输出非负(maxi(0,·) 封底)" | DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_lo)) >= 0 | (待填) | | | - -## S28 _test_counter_component(行 46–50 · 4 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 148 | 46 | "空桶(点数≤0)恒 0 不吃修正" | DamageMathScript.counter_component(0, floor_v, floor_v) == 0 and DamageMathScript.counter_component(-1, 5, floor_v) == 0 | (待填) | | | -| 149 | 48 | "正向修正直加" | DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2 | (待填) | | | -| 150 | 49 | "负修正触底保落地伤害下限" | DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v | (待填) | | | -| 151 | 50 | "下限参数缺省 = 0(无下限语义面)" | DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0 | (待填) | | | - -## S29 _test_roll_discount_percent(行 59–74 · 5 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 152 | 59 | "前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)" | discounts.size() == bounds.size() + 1 and discounts.size() > 0 | (待填) | | | -| 153 | 60 | "掷点 ≥ 阈值恒全额 100" | DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100 and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100 | (待填) | | | -| 154 | 65 | "缺口 1 落首档" | DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected | (待填) | | | -| 155 | 68 | "缺口超末边界落末档" | DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected | (待填) | | | -| 156 | 74 | "档位值随档位加深不升(折扣单调)" | descending | (待填) | | | - -## S30 _test_rolled_component(行 82–87 · 5 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 157 | 82 | "满额折扣无暴击 = floor(基线) + 修正 0" | DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)) | (待填) | | | -| 158 | 83 | "满额折扣暴击按表倍率放大" | DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)) | (待填) | | | -| 159 | 84 | "折扣降低不高于满额" | DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) | (待填) | | | -| 160 | 85 | "克制修正加在 floor 之后(加法核)" | DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val() | (待填) | | | -| 161 | 86 | "复合序 = 基线×折扣%×暴击%(floor 只在链末)" | DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0)) | (待填) | | | - -## S31 _smoke_enemy_attack_type(行 55–98 · 4 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 162 | 55 | "敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" ↩ % [ _db.enemy.rows.size(), at_detail if at_detail != "" else "无"] | at_ok | (待填) | | | -| 163 | 95 | "敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)" | snap_ok | (待填) | | | -| 164 | 96 | "克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" ↩ % [ t_heavy, t_light, t_ward, t_light] | matrix_found and t_light >= 2 | (待填) | | | -| 165 | 98 | "敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" ↩ % payload_at | payload_at == "normal" | (待填) | | | - -## S32 _smoke_armor_roll(行 117–178 · 15 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 166 | 117 | "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" ↩ % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])] | tier_ok and dist == cfg.expect | (待填) | | | -| 167 | 121 | "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" ↩ % ev_sum | ev_sum == 1575 | (待填) | | | -| 168 | 131 | "nat20 种子可达(seed=%d——主角先手首掷命中 %d)" ↩ % [crit_seed, int(_db.settlement.damage_roll.crit_roll)] | crit_seed > 0 | (待填) | | | -| 169 | 132 | "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)" | int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)) | (待填) | | | -| 170 | 134 | "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" ↩ % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)] | int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk) | (待填) | | | -| 171 | 139 | "削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)" | int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2 | (待填) | | | -| 172 | 141 | "削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)" | int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude) | (待填) | | | -| 173 | 152 | "AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" ↩ % tiers.size() | tiers_valid and tiers.size() >= 3 | (待填) | | | -| 174 | 153 | "折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" ↩ % str(tier_distinct.keys()) | tier_distinct.size() >= 2 | (待填) | | | -| 175 | 170 | "全战斗流零 v=0 伤害行(伤害下限全局生效)" | all_positive | (待填) | | | -| 176 | 171 | "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)" | attack_ok | (待填) | | | -| 177 | 172 | "棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)" | board_ok | (待填) | | | -| 178 | 176 | "棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)" | DamageMath.counter_component(2, -1, df) == 1 | (待填) | | | -| 179 | 177 | "克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)" | DamageMath.counter_component(1, -3, df) == 1 | (待填) | | | -| 180 | 178 | "空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)" | DamageMath.counter_component(0, 5, df) == 0 | (待填) | | | - -## S33 _smoke_pool_wiring(行 310–317 · 4 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 181 | 310 | "单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" ↩ % base_v | int(base_row.get("normal", -1)) == base_v and base_v > 0 | (待填) | | | -| 182 | 312 | "增伤经乘区加成合计(%d → %d = ×1.5——attack_up 直乘旁路退役,覆盖面=攻击合计)" ↩ % [base_v, int(up_row.get("value", -1))] | int(up_row.get("value", -1)) == int(floor(base_v * 1.5)) | (待填) | | | -| 183 | 314 | "乏力经乘区削减合计(%d → %d = ×0.75)" ↩ % [base_v, int(sapped_row.get("value", -1))] | int(sapped_row.get("value", -1)) == int(floor(base_v * 0.75)) | (待填) | | | -| 184 | 316 | "硬化折半方向(%d → %d = ×0.5 且 < 基线——敌方防御池负值减伤,翻倍 bug 退役)" ↩ % [base_v, int(fortify_row.get("value", -1))] | int(fortify_row.get("value", -1)) == int(floor(base_v * 0.5)) and int(fortify_row.get("value", -1)) < base_v | (待填) | | | - -## S34 _smoke_damage_types(行 345–379 · 10 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 185 | 345 | "goblin 护甲 = light(轻甲——教学位)" | enemy_system.spawn(1, 0, int(_db.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light" | (待填) | | | -| 186 | 348 | "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light" | (待填) | | | -| 187 | 351 | "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light" | (待填) | | | -| 188 | 355 | "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)" | int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1 | (待填) | | | -| 189 | 357 | "穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)" | int(am.pierce.light) == -1 and int(am.pierce.ward) == 1 and int(am.pierce.heavy) == 0 | (待填) | | | -| 190 | 359 | "魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)" | int(am.magic.light) == 0 and int(am.magic.ward) == -1 and int(am.magic.heavy) == 1 | (待填) | | | -| 191 | 373 | "标错封印产生 seal_miss 归因行(×%d)" ↩ % miss_rows | miss_rows >= 1 | (待填) | | | -| 192 | 374 | "标错封印兑付该安全格资源的负向字典行(×%d——attack/defense/weaken 效果行或 heal 直伤行)" ↩ % negative_rows | negative_rows >= 1 | (待填) | | | -| 193 | 375 | "封印命中兑付该危险格资源的正向字典行(×%d——充能退役后无魔法分量,正向=格面资源)" ↩ % hit_rows | hit_rows >= 1 | (待填) | | | -| 194 | 378 | "标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)" | JSON.stringify(run1.rows) == JSON.stringify(run2.rows) | (待填) | | | - -## S35 _smoke_attack_chain_fresh_pool(行 445–460 · 3 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 195 | 445 | "fresh 池回归战斗启动(Boss 行)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 196 | 452 | "非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" ↩ % ticks.size() | ticks.size() >= 2 | (待填) | | | -| 197 | 460 | "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)" | all_full | (待填) | | | - -## S36 _smoke_full_battle(行 51–79 · 7 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 198 | 51 | "%s 战斗在轮次上限内终局(%d 轮,%s)" ↩ % [label, run1.rounds, run1.result] | run1.terminated | (待填) | | | -| 199 | 59 | "%s 回放断言:结算步骤流逐行相同" ↩ % label | JSON.stringify(run1.stream) == JSON.stringify(run2.stream) | (待填) | | | -| 200 | 60 | "%s 回放断言:事件流逐位相同" ↩ % label | JSON.stringify(run1.events) == JSON.stringify(run2.events) | (待填) | | | -| 201 | 61 | "%s 结算步骤流非空(%d 行)" ↩ % [label, run1.stream.size()] | run1.stream.size() > 0 | (待填) | | | -| 202 | 75 | "%s 步骤流含判定行" ↩ % label | has_verdict | (待填) | | | -| 203 | 78 | "%s 主角先手攻击行必存在(2026-09-11 拍板① + 案C 地图账殿后)×%d" ↩ % [label, hero_attacks] | hero_attacks >= 1 | (待填) | | | -| 204 | 79 | "%s 主角攻击行携带类型键与掷点读数(单分量——充能退役批)" ↩ % label | hero_breakdown_ok | (待填) | | | - -## S37 _run_battle(行 111–111 · 1 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 205 | 111 | "战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" ↩ % String(action_economy.phase) | action_economy.phase == "Idle" | (待填) | | | - -## S38 _smoke_active_refresh(行 160–215 · 17 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 206 | 160 | "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 207 | 166 | "三轮停手攒 RAP=%d(round=%d rap=%d)" ↩ % [rap_banked, settlement.round_index, settlement.action_economy.rap] | settlement.round_index == 4 and settlement.action_economy.rap == rap_banked | (待填) | | | -| 208 | 167 | "第 4 轮 Active 相位" | settlement.action_economy.phase == "Active" | (待填) | | | -| 209 | 177 | "轮内第 1 次刷新受理" | ok1 | (待填) | | | -| 210 | 178 | "相位保持 Active、AP 保留不重置(ap=%d)" ↩ % settlement.action_economy.ap | settlement.action_economy.phase == "Active" and settlement.action_economy.ap == ap_before | (待填) | | | -| 211 | 179 | "RAP -刷新价(%d)" ↩ % settlement.action_economy.rap | settlement.action_economy.rap == rap_banked - int(_db.action_economy.rap_refresh_cost) | (待填) | | | -| 212 | 180 | "当场换新图(board_changed +1)" | int(ct["board_swaps"]) == swaps_before + 1 and minesweeper.current_board() != board_before | (待填) | | | -| 213 | 186 | "轮内第 2 次刷新受理(RAP=%d)" ↩ % settlement.action_economy.rap | ok2 | (待填) | | | -| 214 | 192 | "第 3 次刷新被拒(每轮兜底上限 2——RAP %d 足额仍拒)" ↩ % settlement.action_economy.rap | not ok3 | (待填) | | | -| 215 | 194 | "V5 快照刷新计数=2(受理单源——main 副本已删)〔迭代27 T3〕" | int(settlement.presentation_snapshot().get("refresh_used", -1)) == 2 | (待填) | | | -| 216 | 195 | "V5 上限达门禁一致(can_refresh 与快照计数同源)〔迭代27 T3〕" | not settlement.can_refresh() | (待填) | | | -| 217 | 207 | "轮 4 挖至 AP 耗尽自动结算(round=%d result='%s')" ↩ % [settlement.round_index, settlement.battle_result] | settlement.round_index == 5 and settlement.battle_result == "" | (待填) | | | -| 218 | 208 | "跨图 facts 完整兑现(safe_cell 结算行 %d == safe 事件 %d——快照批次隔离)" ↩ % [int(ct["safe_rows"]), int(ct["safe_events"])] | int(ct["safe_rows"]) == int(ct["safe_events"]) | (待填) | | | -| 219 | 209 | "跨图挖掘 safe 事件 ≥3(%d——断言非空转)" ↩ % int(ct["safe_events"]) | int(ct["safe_events"]) >= 3 | (待填) | | | -| 220 | 210 | "结算段内(Waiting 相位)刷新被拒(等待期换图通道已移除)" | bool(ct["waiting_refresh_rejected"]) and bool(ct["waiting_probe_hit"]) | (待填) | | | -| 221 | 214 | "轮内刷新回放断言:同种子两遍步骤流逐行相同" | JSON.stringify(rep1) == JSON.stringify(rep2) | (待填) | | | -| 222 | 215 | "轮内刷新步骤流非空(%d 行)" ↩ % rep1.size() | rep1.size() > 0 | (待填) | | | - -## S39 _smoke_encounter_roll(行 254–312 · 8 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 223 | 254 | "遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 224 | 257 | "D0 区间 [1,2] 两值均出现(分布 %s)" ↩ % str(counts) | counts.has(1) and counts.has(2) | (待填) | | | -| 225 | 263 | "全部计数在域内 [1,2](分布 %s)" ↩ % str(counts) | all_in_domain | (待填) | | | -| 226 | 267 | "同种子遭遇骰可回放(%s == %s)" ↩ % [str(r1), str(r2)] | r1 == r2 | (待填) | | | -| 227 | 272 | "D1 战斗启动" | settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 228 | 274 | "D1 区间 [2,3] 域内(%d 只)" ↩ % n1 | n1 >= 2 and n1 <= 3 | (待填) | | | -| 229 | 287 | "熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" ↩ % bugbear_lead_band | settlement_d2.presentation_snapshot().enemies.size() == 1 \\ and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max") | (待填) | | | -| 230 | 312 | "全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" ↩ % scan_total | scan_bad == 0 | (待填) | | | - -## S40 _smoke_settlement_segments(行 380–445 · 5 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 231 | 380 | "segment_order 五段冻结在位(表值 = 拍板序恒等)" | Array(_db.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"] | (待填) | | | -| 232 | 391 | "行型封闭 10 类(删五类零残留:item/consumed_ineffective/use_rejected/window_open/window_skip)" | legacy_total == 0 | (待填) | | | -| 233 | 418 | "每轮行相位单调(授予→增益/减益→战斗→衰减→判定)" | order_ok | (待填) | | | -| 234 | 444 | "案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账;连珠矢随先手段)" | mapdmg_ok | (待填) | | | -| 235 | 445 | "衰减/判定段每结算恰一段(先兑付后衰减——含末轮)" | decay_verdict_ok | (待填) | | | - -## S41 _smoke_skill_pool_data(行 496–682 · 28 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 236 | 496 | "技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)" | _db.skill.rows.size() == 16 | (待填) | | | -| 237 | 498 | "普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)" | not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0 and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)) | (待填) | | | -| 238 | 502 | "突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)" | not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2 | (待填) | | | -| 239 | 504 | "蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)" | not charge_row.is_empty() and String(charge_row.buff_ref) == "empower" | (待填) | | | -| 240 | 506 | "撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)" | not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1 | (待填) | | | -| 241 | 508 | "毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)" | not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4 and is_equal_approx(float(venom.rider_mult), 0.75) | (待填) | | | -| 242 | 516 | "毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)" | venom_sole_ok | (待填) | | | -| 243 | 519 | "蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)" | not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard" and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4 | (待填) | | | -| 244 | 527 | "蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)" | snare_sole_ok | (待填) | | | -| 245 | 529 | "失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)" | not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac" | (待填) | | | -| 246 | 532 | "毒吐行(伤害 ×1.6 · 冷却 2 · 无 rider——低 atk 下 rider 恒被 damage_floor 兜成 1 白占字段,迭代 24 N2)" | not spit.is_empty() and String(spit.display_name) == "毒吐" and String(spit.type) == "damage" \\ and is_equal_approx(float(spit.damage_mult), 1.6) and int(spit.cooldown_beats) == 2 and not spit.has("rider_mult") | (待填) | | | -| 247 | 536 | "盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)" | not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \\ and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult") | (待填) | | | -| 248 | 550 | "毒吐投放面(仅 depth_band > 巨蜘蛛首遇带(%d)的巨蜘蛛族行——首遇带池不动,迭代 24 N2/Q6)〔fv24〕" ↩ % spider_lead_band | spit_sole_ok | (待填) | | | -| 249 | 551 | "盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)" | bash_sole_ok | (待填) | | | -| 250 | 559 | "巨蜘蛛首遇带行 pool 恒 {attack:6, bite:2, charge:1}(教学位池零新招——毒吐首投在更深的带)〔fv24〕" | spider_lead_pool == {"attack": 6, "bite": 2, "charge": 1} | (待填) | | | -| 251 | 605 | "二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)" | pools_ok | (待填) | | | -| 252 | 615 | "每池恰一行普攻(空池不可能的加载校验同款)" | basic_count_ok | (待填) | | | -| 253 | 632 | "逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)" | comp_ok | (待填) | | | -| 254 | 643 | "每池至少一行无条件特殊技(触发条件批池构成纪律——逐池复查)〔触发器批 V7〕" | uncond_ok | (待填) | | | -| 255 | 648 | "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)" | sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 256 | 651 | "前置:Boss 压到阈值 65(=round(130×0.5))" | int(boss_u.hp) == 65 | (待填) | | | -| 257 | 657 | "解锁归因行恰一次(×%d——闩去重,二次检查零行)" ↩ % unlock_rows.size() | unlock_rows.size() == 1 | (待填) | | | -| 258 | 659 | "解锁行文本 =「蛛化卓尔精灵 解锁了 遏毒」(高光键 skill_unlock——呈现层一次性技能名 toast 路由键)" | String(unlock_rows[0].get("display_name", "")) == "蛛化卓尔精灵 解锁了 遏毒" | (待填) | | | -| 259 | 660 | "解锁行型 = highlight 既有行型(零行型新增——行协议封闭)" | String(unlock_rows[0].get("type", "")) == "highlight" | (待填) | | | -| 260 | 674 | "全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言,L1/L2 双关卡)" | rollable_ok | (待填) | | | -| 261 | 678 | "蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)" | not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100 | (待填) | | | -| 262 | 679 | "硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)" | not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50 | (待填) | | | -| 263 | 680 | "effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)" | String(_db.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \\ and String(_db.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense" | (待填) | | | - -## S42 _smoke_skill_pool_replay_independence(行 715–721 · 2 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 264 | 715 | "多实例种子可达(seed=%d——D3 遭遇 ≥2 实例)" ↩ % seed_val | seed_val > 0 | (待填) | | | -| 265 | 721 | "同种子回放:各实例摇定动作序列逐位相同(×%d 轮 × %d 实例——子流独立复放面随并流退役)" ↩ % [rounds, int(run1.count)] | not run1.is_empty() and JSON.stringify(run1.seqs) == JSON.stringify(run2.seqs) | (待填) | | | - -## S43 _smoke_skill_pool_ev_anchor(行 809–809 · 1 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 266 | 809 | "EV 抽样对锚 %s(实测 %.4f ∈ [%.2f, %.2f] 含闸长期速率,×%d 拍)" ↩ % [eid, measured, float(band[0]), float(band[1]), n_beats] | in_band | (待填) | | | - -## S44 _smoke_skill_pool_death_void(行 829–848 · 5 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 267 | 829 | "死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" ↩ % void_seed | void_seed > 0 | (待填) | | | -| 268 | 835 | "死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)" | settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}) | (待填) | | | -| 269 | 838 | "开轮已摇定本拍动作(待发 = %s)" ↩ % pending | pending != "" | (待填) | | | -| 270 | 844 | "先手击杀 → 摇定技能作废(第 1 轮零敌方行动行——死亡即不动手)" | enemy_rows == 0 | (待填) | | | -| 271 | 848 | "作废不进账(全池冷却 0 · 保底计数 0——执行上报未发生)" | cd_sum == 0 and int(u.basic_streak) == 0 | (待填) | | | - -## S45 _smoke_skill_pool_validation_reject(行 859–895 · 13 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 272 | 859 | "加载校验拒绝:池条目 skill_id 悬空致命" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill") | (待填) | | | -| 273 | 861 | "加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | -| 274 | 866 | "加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | -| 275 | 868 | "加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)" | not skill_table.validate().is_empty() | (待填) | | | -| 276 | 870 | "加载校验拒绝:普攻配冷却 ≠ 0(域校验锁死)" | not skill_table.validate().is_empty() | (待填) | | | -| 277 | 872 | "加载校验拒绝:damage_mult 越域 [1.1,16]" | not skill_table.validate().is_empty() | (待填) | | | -| 278 | 875 | "加载校验拒绝:池权重 ≤ 0" | not et2.validate().is_empty() | (待填) | | | -| 279 | 877 | "加载校验拒绝:空池(禁)" | not et2.validate().is_empty() | (待填) | | | -| 280 | 883 | "加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | -| 281 | 885 | "加载校验拒绝:妨害技缺 buff_ref(效果引用)" | not skill_table.validate().is_empty() | (待填) | | | -| 282 | 887 | "加载校验拒绝:rider_mult 越域 (0,1]" | not skill_table.validate().is_empty() | (待填) | | | -| 283 | 889 | "加载校验拒绝:非伤害技带必中附加分量" | not skill_table.validate().is_empty() | (待填) | | | -| 284 | 895 | "加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side") | (待填) | | | - -## S46 _smoke_debuff_rider(行 973–975 · 3 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 285 | 973 | "毒爆必中附加分量种子可达(seed=%d,毒爆伤害行 ×%d)" ↩ % [venom_seed, venom_rows_seen] | venom_seed > 0 and venom_rows_seen >= 1 | (待填) | | | -| 286 | 974 | "毒爆伤害行逐拍精确复算:终值 = max(1, 掷点档位主分量) + 必中附加(rider 只随 rider_mult 行出现)" | venom_check_ok | (待填) | | | -| 287 | 975 | "蛛网束缚落主角:施放后掷点阈值 = 基准 AC + 失衡幅度(off_guard ac 轴端到端,seed=%d)" ↩ % snare_seed | snare_seed > 0 | (待填) | | | - -## S47 _smoke_counter_buckets(行 998–1037 · 7 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 288 | 998 | "V1 第1战真实 request_dig 受理(begin_battle 后 ACTIVE 相位)〔无状态化 V1〕" | dig1 | (待填) | | | -| 289 | 999 | "V1 第1战付费挖格经 ctx 就地写 adventure 桶=1(无 export 搬运时点)〔无状态化 V1〕" | int(led.get("adv_twin_digs", 0)) == 1 | (待填) | | | -| 290 | 1005 | "V1 第2战 begin_battle 经 ctx 读到上一战持久值=1(读路径走宿主字典引用,非副本种子回灌)〔无状态化 V1〕" | EffectEngine.counter(o2.fx_host().counters_ctx, "adv_twin_digs") == 1 | (待填) | | | -| 291 | 1007 | "V1 第2战挖后 adventure 桶跨战累积=2(共享账判空早退缺陷消解的回归钉面——旧副本态恒 1)〔无状态化 V1〕" | int(led.get("adv_twin_digs", 0)) == 2 | (待填) | | | -| 292 | 1024 | "V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕" | EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1 | (待填) | | | -| 293 | 1030 | "卡④ wr2①:完美保险标错行 type=extended 且 effect_id=insurance_used(effect_id 必载)〔明细收敛 V2〕" | not v6_ins.is_empty() and String(v6_ins.get("type", "")) == "extended" and String(v6_ins.get("source_class", "")) == "seal_miss" and bool(v6_ins.get("forgiven", 0)) | (待填) | | | -| 294 | 1037 | "V6 battle 桶随宿主销毁:次战新编排器 fx_counters_battle 天然空,读 insurance_used=0〔无状态化 V6〕" | EffectEngine.counter(oB.fx_host().counters_ctx, "insurance_used") == 0 | (待填) | | | - -## S48 _smoke_extended_rows(行 1064–1083 · 2 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 295 | 1064 | "卡④ wr2②:专长揭示兑付行 type=extended 且 effect_id=fx_reveal 且来源格真实〔明细收敛 V2〕" | not paid.is_empty() and String(paid.get("type", "")) == "extended" and Vector2i(paid["origin_cell"][0], paid["origin_cell"][1]).x >= 0 | (待填) | | | -| 296 | 1081 | "卡④ wr2③:揭示落空行 type=extended 且 effect_id=fx_reveal 且 origin_cell=越界哨值〔明细收敛 V2〕" | not missed.is_empty() and String(missed.get("type", "")) == "extended" and Vector2i(missed["origin_cell"][0], missed["origin_cell"][1]).x == -1 | (待填) | | | - -## S49 _smoke_seal_ap_exhausted(行 54–80 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 297 | 54 | "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 298 | 68 | "%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" ↩ % [ap_total - 1, str(settlement.action_economy.phase), settlement.action_economy.ap] | settlement.action_economy.phase == "Active" and settlement.action_economy.ap == 1 | (待填) | | | -| 299 | 73 | "仍存在可挖目标(封印目标来源)" | not plan2.is_empty() | (待填) | | | -| 300 | 79 | "末点 AP 封印被受理" | not out.is_empty() and bool(out[0].accepted) | (待填) | | | -| 301 | 80 | "封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" ↩ % str(rounds) | rounds.size() >= 2 | (待填) | | | - -## S50 _smoke_clear_reward_chain(行 329–341 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 302 | 329 | "① 完美封印清图达成(%d 轮)" ↩ % int(run.rounds) | bool(run.cleared) | (待填) | | | -| 303 | 336 | "① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——clear_blast 身份两份各打一发)" ↩ % dmg.size() | dmg.size() == 2 | (待填) | | | -| 304 | 338 | "① 第二份按身份类型走真实兑付(首份『清图奖励』/ 第二份『完美第二份』)" | String(dmg[0].display_name).contains("清图奖励") and String(dmg[1].display_name).contains("完美第二份") | (待填) | | | -| 305 | 339 | "④ 高光行 value = 实际有效份数 2(不记名义值;行数 %d)" ↩ % hl.size() | hl.size() == 1 and int(hl[0].value) == 2 | (待填) | | | -| 306 | 341 | "高光标记 = perfect_sweep" | String(hl[0].get("highlight", "")) == "perfect_sweep" | (待填) | | | - -## S51 _smoke_clear_reward_zero_paths(行 359–399 · 11 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 307 | 359 | "零兑付构造战斗启动" | s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | -| 308 | 363 | "清图槽身份 = blast 型(clear_default 唯一活行 clear_blast——第二份同身份兑付)" | snap != null and String(snap.cell_type) == "blast" | (待填) | | | -| 309 | 366 | "② 即时类首份零兑付(无存活敌)→ first_ok=false" | not first_ok | (待填) | | | -| 310 | 367 | "② 零兑付产出 reward_ineffective 行(batch_key=clear_blast)" | _has_source_row(rows, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | -| 311 | 374 | "高光守卫构造战斗启动" | s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | -| 312 | 381 | "② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案" | _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | -| 313 | 382 | "② 首份零兑付 → 高光行不发(不记名义值 2)" | _rows_of_type(rows_b, "highlight").is_empty() | (待填) | | | -| 314 | 389 | "第二份被拒构造战斗启动" | s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | -| 315 | 396 | "③ 首份 aoe 真实兑付(击杀唯一存活敌)" | _has_source_row(rows2, "damage", "clear_reward") | (待填) | | | -| 316 | 397 | "③ 第二份被拒产出 reward_ineffective 行(零兑付)" | _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | -| 317 | 399 | "④ 高光行 value = 实际有效份数 1(不记名义值 2)" | hl2.size() == 1 and int(hl2[0].value) == 1 | (待填) | | | - -## S52 _smoke_refresh_provenance_guard(行 410–421 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 318 | 410 | "轮内刷新受理(三轮停手攒 RAP 后 Active 相位发起)" | bool(run.refreshed) | (待填) | | | -| 319 | 411 | "刷新图清图达成(%d 轮)" ↩ % int(run.rounds) | bool(run.cleared) | (待填) | | | -| 320 | 419 | "刷新图清图 → 无 clear_reward 行(守卫在位——实测 %d 行)" ↩ % cr | cr == 0 | (待填) | | | -| 321 | 420 | "刷新图清图 → 无高光行(守卫在位——实测 %d 行)" ↩ % hl | hl == 0 | (待填) | | | -| 322 | 421 | "对照基线:常规换图清图有 clear_reward 行(×%d——守卫非全局静默)" ↩ % _regular_clear_reward_rows | _regular_clear_reward_rows > 0 | (待填) | | | - -## S53 _smoke_polarity_routing(行 442–549 · 12 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 323 | 442 | "极性路由构造种子可达(seed=%d——并流后行为面重锚)" ↩ % polarity_seed | polarity_seed > 0 | (待填) | | | -| 324 | 443 | "构造触达:标中真雷 + 标空安全格均入队" | bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"]) | (待填) | | | -| 325 | 444 | "构造触达:挖到危险格 ≥1(%d)" ↩ % int(probe["mine_events"]) | int(probe["mine_events"]) >= 1 | (待填) | | | -| 326 | 445 | "构造触达:挖到安全格 ≥1(%d)" ↩ % int(probe["safe_events"]) | int(probe["safe_events"]) >= 1 | (待填) | | | -| 327 | 462 | "挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["safe_cells"].size() | safe_all_pos | (待填) | | | -| 328 | 479 | "挖危险格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["mine_cells"].size() | mine_all_neg | (待填) | | | -| 329 | 492 | "封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典;action 型按行值符号辨极性)" | hit_ok | (待填) | | | -| 330 | 498 | "封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)" | prem_pct == 125 | (待填) | | | -| 331 | 526 | "标中兑付溢价(×%d 行逐型=同型正向 ×%d/100——等号压线注记:δ=1.25 时条件二 E[标中]=1.25·E_dig 恰等成立)" ↩ % [prem_rows, prem_pct] | prem_rows >= 1 and prem_ok | (待填) | | | -| 332 | 532 | "标空惩罚零溢价(负向直伤行=固定表值原值——溢价只挂标中正向)" | miss_flat_ok | (待填) | | | -| 333 | 543 | "标空封印 → 封印未中归因行 + 该安全格资源负向兑付(同格同批)" | not miss_rows.is_empty() and miss_neg_ok | (待填) | | | -| 334 | 549 | "封印判定兑付仅封印判定批两封印在案(撤封印抵消过滤零泄漏——%d 格)" ↩ % seal_srcs.size() | int(seal_srcs.size()) <= 2 | (待填) | | | - -## S54 _smoke_seal_irreversible_quadrants(行 571–663 · 21 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 335 | 571 | "四情况 A:开局受理" | settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 336 | 574 | "四情况 A:找到安全格封印目标" | not t_a.is_empty() | (待填) | | | -| 337 | 578 | "四情况 A:封印受理" | ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])[0].accepted | (待填) | | | -| 338 | 580 | "四情况 A:seal_pull 出目录拒绝(撤封印退役)" | not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog" | (待填) | | | -| 339 | 582 | "四情况 A:重复封印仍被拒(seal_present)" | not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "seal_present" | (待填) | | | -| 340 | 584 | "四情况 A:封印格直接挖开被受理(不再须先撤封印)" | not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)) | (待填) | | | -| 341 | 585 | "四情况 A:封印随挖掘隐式退役(has_seal = false)" | not ms_a.has_seal(t_a.seal_cell) | (待填) | | | -| 342 | 600 | "四情况 A:标空判定零兑付(×%d——封印已擦除不入轮末基线)" ↩ % miss_a | miss_a == 0 | (待填) | | | -| 343 | 601 | "四情况 A:安全格正向兑付作废(×%d——零奖励)" ↩ % grant_a | grant_a == 0 | (待填) | | | -| 344 | 602 | "四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" ↩ % void_a | void_a == 1 | (待填) | | | -| 345 | 609 | "四情况 B:开局受理" | settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 346 | 612 | "四情况 B:找到危险格封印目标" | not t_b.is_empty() | (待填) | | | -| 347 | 616 | "四情况 B:封印受理" | ms_b.settle_batch([{"action": "seal_place", "cell": t_b.seal_cell}])[0].accepted | (待填) | | | -| 348 | 618 | "四情况 B:封印危险格直接挖开被受理" | not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)) | (待填) | | | -| 349 | 619 | "四情况 B:危险格已揭示(挖雷本体照常)" | ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell) | (待填) | | | -| 350 | 631 | "四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" ↩ % mine_hit_b | mine_hit_b >= 1 | (待填) | | | -| 351 | 632 | "四情况 B:标中不发(×%d——封印已擦除不判)" ↩ % hit_b | hit_b == 0 | (待填) | | | -| 352 | 639 | "四情况 C:开局受理" | settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 353 | 643 | "四情况 C:安全格+危险格封印目标齐备" | not t_safe.is_empty() and not t_mine.is_empty() | (待填) | | | -| 354 | 662 | "四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" ↩ % [miss_c, miss_neg_c] | miss_c >= 1 and miss_neg_c >= 1 | (待填) | | | -| 355 | 663 | "四情况 C:标中奖励照常(×%d)" ↩ % hit_c | hit_c >= 1 | (待填) | | | - -## S55 _smoke_negative_dictionary(行 675–755 · 8 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 356 | 675 | "sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)" | not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \\ and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \\ and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack" | (待填) | | | -| 357 | 679 | "effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)" | String(_db.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense" | (待填) | | | -| 358 | 716 | "扫描 6 板内出现五型齐备板(%s)" ↩ % str(by_type.keys()) | by_type.size() == 5 | (待填) | | | -| 359 | 726 | "%s 型负向 → %s 效果行(mine_hit 来源指回该格)" ↩ % [ctype, eff] | _has_source_row(rows, "effect", "mine_hit") and _has_effect_id(rows, eff) | (待填) | | | -| 360 | 738 | "heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" ↩ % [backlash_power, hp_before, hp_after] | hp_after == hp_before - backlash_power | (待填) | | | -| 361 | 748 | "blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)" | alert_ok | (待填) | | | -| 362 | 753 | "action 型负向 → 下轮行动点 −1(排队)" | settlement.action_economy.pending_ap_delta() == d0 - 1 | (待填) | | | -| 363 | 755 | "action 型正负抵消 → 排队净值归零" | settlement.action_economy.pending_ap_delta() == d0 | (待填) | | | - -## S56 _smoke_resource_surface(行 785–804 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 364 | 785 | "chain/dig 揭示事件 resource 恒 true(雷安同池——违例 %d)" ↩ % digchain_bad | digchain_bad == 0 | (待填) | | | -| 365 | 786 | "initial 揭示事件(封锁格)resource 恒 false(×%d 事件)" ↩ % initial_count | initial_bad == 0 and initial_count >= 1 | (待填) | | | -| 366 | 787 | "危险格揭示携带资源(mine ×resource=true %d——危险格不再恒无)" ↩ % mine_with_resource | mine_with_resource >= 1 | (待填) | | | -| 367 | 803 | "封锁格(R₀)身份查询 = null(不入摇号清单)" | settlement.query_reward_identity(r0_first) == null | (待填) | | | -| 368 | 804 | "未开危险格身份恒可查({reward_id, cell_type, tier}——秘宝 桥接前提)" | mine_unrevealed != null and settlement.query_reward_identity(mine_unrevealed) != null | (待填) | | | - -## S57 _init(行 18–18 · 1 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 369 | 18 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | - -## S58 _boss(行 30–30 · 1 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 370 | 30 | "前置:spawn (%d,%d) 受理" ↩ % [L, D] | ok | (待填) | | | - -## S59 _test_gate_predicate(行 49–69 · 9 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 371 | 49 | "前置:卓尔 max_hp=130(表值——难度乘数空表恒 1.0)" | mhp == 130 | (待填) | | | -| 372 | 52 | "门谓词:hp=66 > 阈值 65 → 零解锁" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | -| 373 | 53 | "双面过滤:未解锁 venom_curb 不入可摇集合" | not _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | -| 374 | 54 | "双面过滤:未解锁 venom_curb 不入保底特殊技集合" | not _has_sid(ec.special_skills(u), "venom_curb") | (待填) | | | -| 375 | 57 | "前置:补一刀至阈值 65 受理" | hit65 and int(u.hp) == 65 | (待填) | | | -| 376 | 59 | "门谓词:hp=65 ≤ 阈值 65 → 恰解锁 venom_curb 一次" | unlocked.size() == 1 and String(unlocked[0]) == "venom_curb" | (待填) | | | -| 377 | 61 | "解锁即可摇:venom_curb 入可摇集合(冷却初值 0)" | _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | -| 378 | 62 | "解锁即可摇:venom_curb 入保底特殊技集合" | _has_sid(ec.special_skills(u), "venom_curb") | (待填) | | | -| 379 | 69 | "保底不逼锁定:锁定期保底集合 = heavy+venom_burst+web_snare(venom_curb 过滤不出)" | specials.size() == 3 and _has_sid(specials, "heavy") and _has_sid(specials, "web_snare") and _has_sid(specials, "venom_burst") and not _has_sid(specials, "venom_curb") | (待填) | | | - -## S60 _test_latch_irreversible(行 77–80 · 3 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 380 | 77 | "前置:半血触发解锁" | ec.check_trigger_unlocks(u).size() == 1 | (待填) | | | -| 381 | 79 | "闩不可逆:满血复查零新解锁(单向闩不解锁回退)" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | -| 382 | 80 | "闩不可逆:可摇集合仍含 venom_curb" | _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | - -## S61 _test_plain_rows_zero_drift(行 86–105 · 6 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 383 | 86 | "零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)" | ec.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle") | (待填) | | | -| 384 | 92 | "零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)" | not any_unlock_key | (待填) | | | -| 385 | 93 | "零扰动:满血复查零解锁" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | -| 386 | 96 | "零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)" | rollable.size() == pool_count | (待填) | | | -| 387 | 99 | "确定性前置:第二实例蜘蛛 spawn" | ec2.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle") | (待填) | | | -| 388 | 105 | "确定性:两实例同输入可摇集合逐位同(容器零随机纪律)" | a == b | (待填) | | | - -## S62 _smoke_variant_placement(行 60–132 · 9 条 · 源 game/tests/unit/battle_settlement/variant_placement_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 389 | 60 | "L%d 布置段战斗启动失败" ↩ % L | false | (待填) | | | -| 390 | 63 | "L%d 布置段板非空" ↩ % L | b != null | (待填) | | | -| 391 | 84 | "L%d 变异 id 全在候选池(门控一致性:%s)" ↩ % [L, str(pool_ids)] | not bad_id | (待填) | | | -| 392 | 85 | "L%d 每图至多一种变异类型(单格单 id + 全图同 id)" ↩ % L | single_type | (待填) | | | -| 393 | 93 | "L%d 变异格全在初始展开区外且不吃危险格(布置域结构保障——closed_only)" ↩ % L | domain_ok | (待填) | | | -| 394 | 100 | "L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" ↩ % [L, vcells.size(), expect_cap] | vcells.size() <= expect_cap | (待填) | | | -| 395 | 103 | "L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" ↩ % L | expect_cap == 0 or vcells.size() > 0 | (待填) | | | -| 396 | 125 | "L%d 全部连通片 ≤ %d 格(全连通生长、2×2/1×4/1×3+1 等形)" ↩ % [L, pack_max] | pack_ok | (待填) | | | -| 397 | 132 | "L%d 同种子变异布置逐格相同(variant_place 流确定性)" ↩ % L | b2 != null and _variant_dump(b2) == _variant_dump(b) | (待填) | | | - -## S63 _smoke_global_variant(行 154–276 · 33 条 · 源 game/tests/unit/battle_settlement/variant_placement_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 398 | 154 | "互锁负样本③:resource_face + scope=region 被拒(resource_face ⇒ global)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 399 | 158 | "互锁负样本④:scope=global + effect=hide_number 被拒(global ⇒ resource_face)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 400 | 162 | "互锁负样本⑤:scope=global + trigger=on_open 被拒(global ⇒ always 整图恒在)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 401 | 166 | "互锁负样本⑥:scope=global + placement=closed_only 被拒(global ⇒ any 无格子布置域)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 402 | 175 | "互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)" | domain_hit | (待填) | | | -| 403 | 180 | "门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)" | pool_boss_ids == ["magma", "ash"] | (待填) | | | -| 404 | 184 | "门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)" | pool_battle_ids == ["magma", "ash"] | (待填) | | | -| 405 | 185 | "门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)" | _db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2 | (待填) | | | -| 406 | 186 | "L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)" | _db.variant_pool(1, "underdark", "boss").is_empty() | (待填) | | | -| 407 | 187 | "全局决议:battle 节点恒无全局型" | _db.global_variant_row(2, "underdark", "battle").is_empty() | (待填) | | | -| 408 | 188 | "全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)" | _db.global_variant_row(2, "underdark", "boss").is_empty() | (待填) | | | -| 409 | 189 | "全局决议:L1 boss 全局池空 → 空行(兜底回落承载)" | _db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty() | (待填) | | | -| 410 | 190 | "elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)" | _db.global_variant_row(2, "underdark", "elite").is_empty() | (待填) | | | -| 411 | 196 | "L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)" | boss_ok | (待填) | | | -| 412 | 202 | "屏蔽后回落:Boss 图无整图全局型(board_variants 空——全局池空合法态)" | bb.board_variant_ids().is_empty() | (待填) | | | -| 413 | 207 | "屏蔽后回落:Boss 图 region 变异恰一行且在候选池内(实测 %s)" ↩ % str(boss_vids.keys()) | boss_vids.size() == 1 and (boss_vids.has("magma") or boss_vids.has("ash")) | (待填) | | | -| 414 | 208 | "屏蔽后回落:Boss 图恢复 region 变异格布置(原替换制跳过 region 的逻辑随全局池空失效)" | not bb.variants.is_empty() | (待填) | | | -| 415 | 209 | "屏蔽后回落:variant_place 恢复取号(原全局取用零取号态消失)" | crng.variant_calls > 0 | (待填) | | | -| 416 | 211 | "快照投影:board_variants 空态一致(横幅/角标唯一读取面——无全局型即零预告)" | _arr_str_eq(snap.get("board_variants", []), bb.board_variant_ids()) | (待填) | | | -| 417 | 221 | "确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)" | _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls | (待填) | | | -| 418 | 226 | "偏斜基线:正向池现行合计 77(挖掘风险批口径)" | base_sum == 77 | (待填) | | | -| 419 | 229 | "off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)" | off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows) | (待填) | | | -| 420 | 232 | "合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)" | eco._pool_weight(on_rows) == 77 | (待填) | | | -| 421 | 234 | "白名单豁免:weaken/gp/action 三行偏斜后逐位不动" | wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2 | (待填) | | | -| 422 | 235 | "生存族下调:heal 4→3 / defense 11→7(v=30 构造例)" | wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7 | (待填) | | | -| 423 | 236 | "进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)" | wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9 | (待填) | | | -| 424 | 240 | "合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8" | eco._pool_weight(combo_rows) == 77 + 4 * 2 | (待填) | | | -| 425 | 241 | "合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)" | cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11 | (待填) | | | -| 426 | 244 | "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)" | eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0 | (待填) | | | -| 427 | 256 | "wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" ↩ % [empty_bias, hit_rows, loaded_bias] | empty_bias == 0 and hit_rows == 1 and loaded_bias == 2 | (待填) | | | -| 428 | 258 | "wr10②接线面:组装点注入的查询口有效且与宿主查询口同源" | woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for | (待填) | | | -| 429 | 264 | "wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" ↩ % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))] | wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2 | (待填) | | | -| 430 | 276 | "确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)" | n1 == _board_resource_dump(normal2) and not n1.is_empty() | (待填) | | | - -## S64 _smoke_type_gate(行 25–29 · 2 条 · 源 game/tests/unit/contracts/source_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 431 | 25 | "V7 类型闸 warning 配置在位(untyped_declaration=1 显形)〔迭代27 T5〕" | pj.contains("gdscript/warnings/untyped_declaration=1") | (待填) | | | -| 432 | 28 | "V7 类型审计器闸机在位(static_scan S9 → tools/audit_type_hints.py)〔迭代27 T5〕" | scan_src.contains("audit_type_hints.py") | (待填) | | | - -## S65 _smoke_hygiene_gates(行 56–77 · 6 条 · 源 game/tests/unit/contracts/source_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 433 | 56 | "V8 字符串 Callable 产品码零命中(实测应 0——方法引用换装闸)〔迭代27 T6〕" | callables == 0 | (待填) | | | -| 434 | 58 | "V8 FeatsEngine 整文件退役(class_name 注销=文件删除)〔迭代27 T6〕" | not FileAccess.file_exists("res://core/feats_engine.gd") | (待填) | | | -| 435 | 61 | "V8 死代码抽查·_attune_row 零残留〔迭代27 T6〕" | not bo.contains("func _attune_row") | (待填) | | | -| 436 | 64 | "V8 死代码抽查·attunement_counters 零残留〔迭代27 T6〕" | not sm.contains("func attunement_counters") | (待填) | | | -| 437 | 67 | "V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕" | not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger") | (待填) | | | -| 438 | 76 | "V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" ↩ % outside_calls | outside_calls == 0 | (待填) | | | - -## S66 _init(行 22–22 · 1 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 439 | 22 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | - -## S67 _test_damage_controller_vocabularies(行 43–57 · 4 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 440 | 43 | "五配方常量非空(开单面唯一分派键)" | nonempty | (待填) | | | -| 441 | 47 | "五配方常量互异(词表封闭无别名)" | unique_kinds.size() == kinds.size() | (待填) | | | -| 442 | 55 | "三目标规则常量互异(结账时刻现场解析词表)" | unique_targets.size() == targets.size() | (待填) | | | -| 443 | 56 | "双结账政策常量互异(A 即结 / B 攒批回切位)" | String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT) | (待填) | | | - -## S68 _test_rng_tag_closure(行 64–67 · 2 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 444 | 64 | "settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)" | RngServiceScript.PURPOSE_TAGS.has(stream_label) | (待填) | | | -| 445 | 66 | "RngService 用途标签词表非空且互异(封闭语义由流改标签)" | RngServiceScript.PURPOSE_TAGS.size() > 0 | (待填) | | | - -## S69 _test_ticket_nine_columns(行 83–85 · 3 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 446 | 83 | "单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)" | all_present | (待填) | | | -| 447 | 84 | "缺省无类型轴(attack_type 空 = 恒不查克制矩阵)" | String(t.attack_type) == "" | (待填) | | | -| 448 | 85 | "缺省无前置依赖(requires = -1)" | int(t.requires) == -1 | (待填) | | | - -## S70 _test_status_boolean_acceptance(行 107–121 · 5 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 449 | 107 | "前置:状态表非空" | not any_row.is_empty() | (待填) | | | -| 450 | 112 | "域闭授予拒绝(EC1——战斗域外 apply_effect = false)" | effects.apply_effect(probe_id, src) == false | (待填) | | | -| 451 | 113 | "未知 effect_id 拒绝(EC2)" | effects.apply_effect("no_such_effect_id__", src) == false | (待填) | | | -| 452 | 115 | "开域后受理已知 id(布尔受理口径 true 通道)" | effects.apply_effect(probe_id, src) == true | (待填) | | | -| 453 | 121 | "受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)" | projected | (待填) | | | - -## S71 _smoke_adventure_api(行 61–82 · 11 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 454 | 61 | "冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))" | adr.start_adventure(42) | (待填) | | | -| 455 | 62 | "进行中重复 start_adventure 拒绝(重开须待结算完毕)" | not adr.start_adventure(43) | (待填) | | | -| 456 | 63 | "停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)" | sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented" | (待填) | | | -| 457 | 64 | "入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)" | adr.runner().awaiting_choice() and (obs["entry_cands"] as Array).size() == 1 \\ and adr.runner().current_battle() == null | (待填) | | | -| 458 | 67 | "pick 入口候选受理(同一 _enter_node——与旧自动进入逐位等价)" | adr.runner().pick_next_node(int(adr.runner().next_candidate_ids()[0])) | (待填) | | | -| 459 | 68 | "pick 后 node_entered 到达 + 首战在飞(接口①开始关卡——停点闭合直达战斗节点)" | sig_seq.size() == 3 and String(sig_seq[2]) == "node_entered" and adr.runner().current_battle() != null | (待填) | | | -| 460 | 69 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not adr.abandon() | (待填) | | | -| 461 | 78 | "首战胜后选路态(非战斗期——current_battle 置空)" | adr.runner().current_battle() == null and adr.runner().awaiting_choice() | (待填) | | | -| 462 | 79 | "非战斗期 abandon 受理(选路态——同失败型结算)" | adr.abandon() | (待填) | | | -| 463 | 81 | "放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" ↩ % int(sum.get("keystone_total", -1)) | String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)) | (待填) | | | -| 464 | 82 | "结算后重复 abandon 拒绝(结算受理只算一次——state=idle)" | not adr.abandon() | (待填) | | | - -## S72 _smoke_adventure(行 118–443 · 57 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 465 | 118 | "序列关数 = 行集关数(三关正式序列生效)" | int(_db.level.rows.size()) == 3 | (待填) | | | -| 466 | 135 | "生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" ↩ % [cov_scan, cov_miss] | cov_scan > 0 and cov_miss == 0 | (待填) | | | -| 467 | 140 | "满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)" | String(st["result"]) == "completed" and bool(sum.get("cleared", false)) | (待填) | | | -| 468 | 141 | "完成几关 = 3(levels_cleared)" | int(sum.get("levels_cleared", -1)) == 3 | (待填) | | | -| 469 | 145 | "钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" ↩ % int(sum.get("keystone_total", -1)) | int(sum.get("keystone_total", -1)) == ks_exp | (待填) | | | -| 470 | 147 | "逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)" | drops.size() == 3 | (待填) | | | -| 471 | 148 | "摘要携带主种子(42)" | int(sum.get("master_seed", -1)) == 42 | (待填) | | | -| 472 | 159 | "关卡完成三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)" | picks.size() == 2 and resolves.size() == 2 + node_picks | (待填) | | | -| 473 | 170 | "三关生成图各含 ≥1 奖励节点(节点三选一候选位在图)〔v4〕" | reward_levels == 3 | (待填) | | | -| 474 | 174 | "奖励分支点选→三选一候选可取且停点在途(候选 %s 件)" ↩ % str((rw.get("cands", []) as Array).size()) | bool(rw.get("pending", false)) and (rw.get("cands", []) as Array).size() >= 1 | (待填) | | | -| 475 | 187 | "奖励三选一点选兑付受理(件=%s)" ↩ % String(rw_pc.get("kind", "")) | rw_pick_ok | (待填) | | | -| 476 | 193 | "进战载荷两样=关卡序号+节点深度(实测 depth=%s 句柄=%s)〔v6〕" ↩ % [str(enc_probe.get("node_depth", -1)), str(node_probe.get("id", -1))] | not enc_probe.is_empty() and int(enc_probe.get("node_depth", -1)) == int(node_probe.get("depth", -1)) and int(enc_probe.get("level_index", -1)) == 1 and int(node_probe.get("id", -1)) != int(node_probe.get("depth", -1)) | (待填) | | | -| 477 | 202 | "奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" ↩ % [node_resolves, node_picks] | node_resolves == node_picks | (待填) | | | -| 478 | 249 | "三选一候选 2–3 枚且零重复(令牌洗牌不放回——kind+tier 复合身份)" | cand_ok | (待填) | | | -| 479 | 250 | "关卡完成部分回血封顶(floor(观测上限×比例) 封顶——购血观测值精确断言,核代码增补)" | heal_ok | (待填) | | | -| 480 | 251 | "钥石逐杀入账(首杀档位 + 累计逐位)" | ks_gain_ok | (待填) | | | -| 481 | 252 | "点选恰兑付选中一件(值=令牌档位表值;未选项零生效)" | resolve_ok | (待填) | | | -| 482 | 263 | "各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" ↩ % exp_base | seq_ok | (待填) | | | -| 483 | 266 | "同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)" | st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"] | (待填) | | | -| 484 | 276 | "零配 L1 败北种子可达(seed=%s)" ↩ % str(lose.get("s", -1)) | not lose.is_empty() | (待填) | | | -| 485 | 279 | "L1 败北摘要:未打穿 · 完成 0 关 · 钥石 0 · 止步第 1 层" | not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \\ and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1 | (待填) | | | -| 486 | 283 | "L1 败北败退明细:层号/节点编号/节点类型在案(数值验证取数面 2026-09-22)" | int(dl.get("level_index", -1)) == 1 and int(dl.get("node_id", -1)) >= 0 \\ and String(dl.get("node_type", "")) in ["battle", "boss"] | (待填) | | | -| 487 | 292 | "半配(atk+50)L2 败北种子可达(seed=%s)" ↩ % str(half.get("s", -1)) | not half.is_empty() | (待填) | | | -| 488 | 295 | "L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层" | not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \\ and int(sh["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \\ and int(sh["stopped_at"]) == 2 | (待填) | | | -| 489 | 300 | "L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)" | int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \\ and String(dh.get("node_type", "")) in ["battle", "boss"] | (待填) | | | -| 490 | 306 | "重复拒绝场景:冒险启动(seed 7——生成图确定同上)" | adr6.start_adventure(7) | (待填) | | | -| 491 | 314 | "重复拒绝场景就位:选路态放弃 → 已结算" | adr6.abandon() | (待填) | | | -| 492 | 317 | "重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)" | adr6.summary == sum6_before | (待填) | | | -| 493 | 327 | "每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" ↩ % [gains, int(st["battles"])] | gains == int(st["battles"]) | (待填) | | | -| 494 | 328 | "钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)" | clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear" | (待填) | | | -| 495 | 330 | "三关商店房各到达一次(每关入口→终点路径恰穿一店的驱动投影)且首达余额 > 0(%d——GP 跨关延续)" ↩ % (shop_seq[0] if shop_seq.size() > 0 else -1) | shop_seq.size() == 3 and int(shop_seq[0]) > 0 | (待填) | | | -| 496 | 341 | "同账场景种子可达(seed=%s——L2 商店房就位)" ↩ % str(same.get("s", -1)) | not same.is_empty() | (待填) | | | -| 497 | 354 | "进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" ↩ % [hp_price + l2_price_bonus, hp_price, l2_price_bonus] | gp0 >= hp_price + l2_price_bonus | (待填) | | | -| 498 | 355 | "购买 hp_up 受理(出账 + 加算区)" | r8.buy_product("hp_up") | (待填) | | | -| 499 | 357 | "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" ↩ % acc_shop | acc_shop >= 3 | (待填) | | | -| 500 | 360 | "L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕" | not a8.stop()._pending.is_empty() | (待填) | | | -| 501 | 361 | "未兑付推进拒绝(选择停点零自动推进契约)" | not a8.continue_to_next_level() | (待填) | | | -| 502 | 364 | "三选一点选兑付受理(候选取首件)" | a8.stop().pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))) | (待填) | | | -| 503 | 367 | "三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" ↩ % [a8.journal().bonus_total("max_hp"), acc_shop, picked2] | a8.journal().bonus_total("max_hp") == expect_acc | (待填) | | | -| 504 | 369 | "关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" ↩ % [a8.stats().hero_hp, max_eff8] | a8.stats().hero_hp <= max_eff8 | (待填) | | | -| 505 | 370 | "兑付后推进受理(下一关开始)" | a8.continue_to_next_level() | (待填) | | | -| 506 | 373 | "恢复点数学:floor(100×30%)=30 · 封顶缺口 5 · 满血回 0 合法" | int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \\ and int(a9.apply_rest(30, 100, 100)) == 0 | (待填) | | | -| 507 | 377 | "同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)" | int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5 | (待填) | | | -| 508 | 381 | "放弃场景:冒险启动(seed 15——生成图确定同上)" | a10.start_adventure(15) | (待填) | | | -| 509 | 382 | "放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)" | a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])) | (待填) | | | -| 510 | 383 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not a10.abandon() | (待填) | | | -| 511 | 390 | "非战斗期(选路态)abandon 受理" | a10.runner().current_battle() == null and a10.abandon() | (待填) | | | -| 512 | 394 | "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)" | a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | -| 513 | 403 | "池耗即枯竭(attack/ap 令牌耗尽不出候选——%s)" ↩ % str(draw) | not draw_kinds.has("attack") and not draw_kinds.has("ap") | (待填) | | | -| 514 | 404 | "池耗后候选恒 ≥2(稀有档余量——防「三选项全不可选」死角)" | (draw as Array).size() >= 2 | (待填) | | | -| 515 | 413 | "池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)" | not treasure_seen | (待填) | | | -| 516 | 422 | "满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)" | int(cap["payload"].get("heal_gained", -1)) == 0 | (待填) | | | -| 517 | 425 | "节点共享池场景:冒险启动(seed 13)" | a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | -| 518 | 436 | "奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" ↩ % str(ncands13) | not has_attack13 | (待填) | | | -| 519 | 440 | "奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" ↩ % int(_db.adventure.curio_value("gp", "common")) | a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.adventure.curio_value("gp", "common")) | (待填) | | | -| 520 | 441 | "节点停点态兑付后清零(恰一次语义同构)" | a13.stop()._node_pending.is_empty() | (待填) | | | -| 521 | 443 | "放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)" | String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)) | (待填) | | | - -## S73 _smoke_feats(行 454–766 · 68 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 522 | 454 | "feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)" | _db.feats != null and (_db.feats.rows as Array).size() == 30 | (待填) | | | -| 523 | 455 | "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)" | _db.feats.validate().is_empty() | (待填) | | | -| 524 | 457 | "专长带场景:冒险启动(seed 71)" | a_r0.start_adventure(71) | (待填) | | | -| 525 | 465 | "专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕" | feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132 | (待填) | | | -| 526 | 467 | "旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕" | _db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty() | (待填) | | | -| 527 | 468 | "迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕" | _db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("seal_burst").is_empty() | (待填) | | | -| 528 | 475 | "层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕" | kinds_ok | (待填) | | | -| 529 | 481 | "棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕" | rewrite_rows == 0 | (待填) | | | -| 530 | 488 | "对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕" | bad_words == 0 | (待填) | | | -| 531 | 489 | "数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕" | float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0 and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5 and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3 | (待填) | | | -| 532 | 507 | "新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)" | rt.validate().is_empty() | (待填) | | | -| 533 | 511 | "专长带场景:冒险启动(seed 72——注入 5 行临时表)" | a_r.start_adventure(72) | (待填) | | | -| 534 | 516 | "专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107" | deck_relic == 5 and a_r._curio.deck.size() == 107 | (待填) | | | -| 535 | 527 | "关卡完成渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)" | feat_seen.size() >= 2 | (待填) | | | -| 536 | 528 | "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)" | not node_relic_seen | (待填) | | | -| 537 | 536 | "A5 专长凭证兑付受理(节点渠道——拿到即开)" | a_t.stop().apply_node_curio("feat_ticket", "excellent") | (待填) | | | -| 538 | 542 | "A5 拿到即开——停点候选已替换为专长三选一(全 relic)" | tkt_all_relic | (待填) | | | -| 539 | 543 | "A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)" | a_t.deck().pool_stock("attack") == int(stock_before["attack"]) and a_t.deck().pool_stock("gp") == int(stock_before["gp"]) | (待填) | | | -| 540 | 544 | "A5 无专长凭证残留持有集" | not (a_t.feats_held() as Array).has("feat_ticket") | (待填) | | | -| 541 | 547 | "A5 专长面兑付受理(专长凭证开出候选可正常入持有集)" | (a_t.feats_held() as Array).size() == feats_n + 1 | (待填) | | | -| 542 | 552 | "A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)" | a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | -| 543 | 553 | "A6 触发后清零" | a_t.stop()._deck.ticket_pity == 0 | (待填) | | | -| 544 | 556 | "A6 限购满不再触发(保底冻结)" | not a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | -| 545 | 564 | "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)" | offered | (待填) | | | -| 546 | 566 | "relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" ↩ % [deck_relic, deck_relic - 1] | a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1 | (待填) | | | -| 547 | 590 | "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" ↩ % (i + 1) | refund | (待填) | | | -| 548 | 592 | "双管镐反例:第 %d 次不返(未满 2 次)" ↩ % (i + 1) | not refund | (待填) | | | -| 549 | 596 | "地质学家:首开新类型集 1 点〔专长批 V3〕" | orch._hits_of(geo_hits, "counter_add").size() == 1 | (待填) | | | -| 550 | 600 | "地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕" | orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0 | (待填) | | | -| 551 | 602 | "地质学家反例:同类型重复首开不叠点〔专长批 V3〕" | orch._hits_of(geo_hits3, "counter_add").is_empty() | (待填) | | | -| 552 | 606 | "安全帽:开安全格攒层 ×2〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "helm_layers") == 2 | (待填) | | | -| 553 | 612 | "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕" | hh_reduce == 2 | (待填) | | | -| 554 | 616 | "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕" | orch._hits_of(sk_zero, "counter_add").size() == 1 | (待填) | | | -| 555 | 620 | "跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕" | orch._hits_of(sk_full, "reveal_tiles").size() == 1 | (待填) | | | -| 556 | 621 | "跳格采掘反例:普通开格不计点〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty() | (待填) | | | -| 557 | 628 | "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | -| 558 | 630 | "边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty() | (待填) | | | -| 559 | 638 | "封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕" | orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue" | (待填) | | | -| 560 | 647 | "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕" | sm_remove == 1 | (待填) | | | -| 561 | 649 | "封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round" | (待填) | | | -| 562 | 653 | "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1 | (待填) | | | -| 563 | 662 | "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕" | pm_bonus == 2 | (待填) | | | -| 564 | 669 | "保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty() | (待填) | | | -| 565 | 673 | "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕" | fw_shield == 0 | (待填) | | | -| 566 | 675 | "见血开窍:踩雷揭示 1 格〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1 | (待填) | | | -| 567 | 682 | "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕" | ti_val == 2 | (待填) | | | -| 568 | 687 | "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") == ct_before + 2 | (待填) | | | -| 569 | 690 | "陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕" | orch._hits_of(tb_hits, "blast_scaled").size() == 1 and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0 | (待填) | | | -| 570 | 694 | "血蜜:踩雷回复 N 生命〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1 | (待填) | | | -| 571 | 697 | "石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕" | orch._hits_of(ss_hits, "ac_bonus").size() == 1 and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3 | (待填) | | | -| 572 | 701 | "背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty() | (待填) | | | -| 573 | 706 | "破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty() | (待填) | | | -| 574 | 714 | "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕" | orch._hits_of(me_hits, "counter_add").size() == 1 | (待填) | | | -| 575 | 718 | "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕" | me_atk == 4 | (待填) | | | -| 576 | 719 | "弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty() | (待填) | | | -| 577 | 722 | "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕" | orch._hits_of(ld_hits, "grant_ap").size() == 1 | (待填) | | | -| 578 | 723 | "一滴不剩反例:上轮有余不发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty() | (待填) | | | -| 579 | 729 | "收盘:本轮开格≥8→+1 GP〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | -| 580 | 732 | "斩首学者:击杀揭示 1 格〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1 | (待填) | | | -| 581 | 735 | "处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕" | orch._hits_of(ex_hits, "target_attack_bonus").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty() | (待填) | | | -| 582 | 739 | "战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1 | (待填) | | | -| 583 | 742 | "血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk" | (待填) | | | -| 584 | 747 | "淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty() | (待填) | | | -| 585 | 751 | "封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1 | (待填) | | | -| 586 | 755 | "零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)" | orch0.fx_host().fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty() and orch0.fx_host().fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty() and orch0.fx_host().fx_emit("mine_hit", {"damage": 5}).is_empty() and orch0.fx_host().fx_emit("battle_won", {"round": 1}).is_empty() | (待填) | | | -| 587 | 763 | "特性段空态隐藏(视图空态纪律同特性段)" | not panel._feat_section.visible | (待填) | | | -| 588 | 765 | "特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)" | panel._feat_section.visible and panel._feat_list.get_child_count() == 2 | (待填) | | | -| 589 | 766 | "特性卡文案表值化:desc 行字段直读(零硬编码映射)" | String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "pain_memory", "display_name": "疼痛记忆", "desc": "触陷阱攒 1 层"})).contains("触陷阱攒 1 层") | (待填) | | | - -## S74 _smoke_layout_cell_formula(行 65–68 · 2 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 590 | 65 | "L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" ↩ % [L, b.width, b.height, grid_h, int(avail.y), int(cell)] | grid_h <= int(avail.y) - 24 | (待填) | | | -| 591 | 67 | "L%d 格边在 26–46 档内(%dpx)" ↩ % [L, int(cell)] | cell >= 26.0 and cell <= 46.0 | (待填) | | | - -## S75 _smoke_reward_identity_unit(行 83–116 · 10 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 592 | 83 | "roll_board 受理" | reward_economy.roll_board(eligible) | (待填) | | | -| 593 | 84 | "重复 roll_board 被拒(恰一次——double-roll 守卫)" | not reward_economy.roll_board(eligible) | (待填) | | | -| 594 | 95 | "query_identity 投影 = {reward_id, cell_type, tier} 且五型封闭(样本 %s)" ↩ % str(types_seen.keys()) | types_ok | (待填) | | | -| 595 | 97 | "清单外坐标查询 = null(封锁格无身份同口径)" | reward_economy.query_identity(Vector2i(99, 99)) == null | (待填) | | | -| 596 | 101 | "同格重复 grant 幂等(同一快照——受理恰一次)" | JSON.stringify(snap1) == JSON.stringify(snap2) | (待填) | | | -| 597 | 102 | "清单外 grant 拒绝" | reward_economy.grant(Vector2i(99, 99)) == null | (待填) | | | -| 598 | 106 | "清图批尾身份 = clear_blast(clear_default 唯一活行)" | String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast" | (待填) | | | -| 599 | 111 | "清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" ↩ % [cb_range[0], cb_range[1], int(cs1.power)] | int(cs1.power) >= int(cb_range[0]) and int(cs1.power) <= int(cb_range[1]) | (待填) | | | -| 600 | 113 | "grant_clear 同批次重调幂等" | JSON.stringify(cs1) == JSON.stringify(cs2) | (待填) | | | -| 601 | 116 | "映射退役后 grant 拒绝(生命周期闭合)" | reward_economy.grant(eligible[0].cell) == null | (待填) | | | - -## S76 _smoke_board_generator(行 135–210 · 24 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 602 | 135 | "BoardGen 行解析 (%d,0)" ↩ % L | not row.is_empty() | (待填) | | | -| 603 | 141 | "BoardGen 精确命中 (2,3)(覆盖批键域补齐)" | int(exact_row.get("node_depth", -1)) == 3 | (待填) | | | -| 604 | 151 | "R1 ceil 能力不失(夹具摘除 (2,3)→向上取整到 4·偏难口径)" | int(ceil_row.get("node_depth", -1)) == 4 | (待填) | | | -| 605 | 153 | "夹具表原样还原(行数与解析回到覆盖批现值)" | _db.board_gen.rows.size() == rows_before and int(_db.resolve_board_gen(2, 3).get("node_depth", -1)) == 3 | (待填) | | | -| 606 | 156 | "L%d 生成产物非空" ↩ % L | b1 != null | (待填) | | | -| 607 | 159 | "L%d clearable(安全格 8 邻连通)" ↩ % L | board_generator.is_clearable(b1) | (待填) | | | -| 608 | 160 | "L%d legality 全过" ↩ % L | board_generator.legality(b1, row) | (待填) | | | -| 609 | 167 | "L%d inferability %.3f %s target %.2f(accept_mode=%s)" ↩ % [L, inf, "≥" if inf >= float(row.inferability_target) else "<", float(row.inferability_target), mode] | inf >= float(row.inferability_target) or mode == "accept_best" | (待填) | | | -| 610 | 169 | "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" ↩ % L | b1.variants.is_empty() | (待填) | | | -| 611 | 170 | "L%d 指标集输出降级标记与目标雷数(三分出口表)" ↩ % L | b1.metrics.has("degraded_product") and b1.metrics.has("target_mines") | (待填) | | | -| 612 | 171 | "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" ↩ % [L, mode, str(b1.degraded_product)] | b1.degraded_product == (mode == "accept_best") | (待填) | | | -| 613 | 173 | "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" ↩ % [L, mode, str(b1.illegal_product)] | b1.illegal_product == (mode == "accept_worst") | (待填) | | | -| 614 | 175 | "L%d 降级/非法产物标记互斥(两出口不可同真)" ↩ % L | not b1.degraded_product or not b1.illegal_product | (待填) | | | -| 615 | 177 | "L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" ↩ % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)] | int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines) | (待填) | | | -| 616 | 179 | "L%d attempts_used ∈ [1,%d]" ↩ % [L, int(row.max_gen_attempts)] | b1.metrics.get("attempts_used", 99) >= 1 and b1.metrics.get("attempts_used", 99) <= int(row.max_gen_attempts) | (待填) | | | -| 617 | 181 | "L%d 非法产物标记 = false" ↩ % L | not b1.illegal_product | (待填) | | | -| 618 | 185 | "L%d 同种子两次生成逐格相同" ↩ % L | _board_dump(b2) == _board_dump(b3) | (待填) | | | -| 619 | 187 | "L%d 异种子产物不同(退化护栏)" ↩ % L | _board_dump(b2) != _board_dump(b4) | (待填) | | | -| 620 | 193 | "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" ↩ % [L, cap_val] | cap_val >= BoardData.SPAWN_AREA_CELLS | (待填) | | | -| 621 | 195 | "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" ↩ % [L, int(b1.metrics.r0_count), cap_val] | int(b1.metrics.r0_count) <= cap_val | (待填) | | | -| 622 | 197 | "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" ↩ % [L, int(b1.metrics.get("r0_cap_rejected", -1))] | int(b1.metrics.get("r0_cap_rejected", -1)) >= 0 | (待填) | | | -| 623 | 199 | "L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" ↩ % [L, int(b1.metrics.get("ring_removed", 99))] | int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5 | (待填) | | | -| 624 | 207 | "L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" ↩ % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"] | b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product) | (待填) | | | -| 625 | 209 | "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" ↩ % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))] | int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "" | (待填) | | | - -## S77 _smoke_power_interval(行 240–298 · 5 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 626 | 240 | "区间行配对齐全且 power = floor 中点(配置推导 %d 行:%s)" ↩ % [interval_rows.size(), str(interval_rows.keys())] | anchors_ok and interval_rows.size() >= 1 | (待填) | | | -| 627 | 282 | "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)" | in_domain | (待填) | | | -| 628 | 283 | "定值行快照 power 恒等表值——区间批零外溢" | fixed_ok | (待填) | | | -| 629 | 297 | "区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)" | variance_ok | (待填) | | | -| 630 | 298 | "定值行(lo=hi)roll 恒等表值" | fixed_rows_ok | (待填) | | | - -## S78 _init(行 17–17 · 1 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 631 | 17 | "数值表加载" | false | (待填) | | | - -## S79 _test_generate_shape_and_mines(行 42–55 · 5 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 632 | 42 | "生成返回棋盘" | false | (待填) | | | -| 633 | 47 | "棋盘尺寸与 board_gen 表一致(%dx%d)" ↩ % [want_w, want_h] | b.width == want_w and b.height == want_h | (待填) | | | -| 634 | 49 | "雷数非零且不超过总格数" | b.mines.size() > 0 and b.mines.size() < b.width * b.height | (待填) | | | -| 635 | 54 | "全部危险格在界内" | all_in_bounds | (待填) | | | -| 636 | 55 | "生成期指标集已落盘(metrics 非空)" | not b.metrics.is_empty() | (待填) | | | - -## S80 _test_target_mine_count_single_source(行 75–83 · 3 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 637 | 75 | "target_mine_count 与原式同参同值(%d 档对拍全等)〔wd14〕" ↩ % checked | checked > 0 and bad == 0 | (待填) | | | -| 638 | 78 | "生成返回棋盘(雷数单点段)" | false | (待填) | | | -| 639 | 81 | "产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕" | int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({ "w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}) | (待填) | | | - -## S81 _test_digits_equal_neighbor_mine_count(行 89–103 · 2 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 640 | 89 | "生成返回棋盘(数字段)" | false | (待填) | | | -| 641 | 103 | "每个安全格的数字恒等于八邻雷数(全盘逐格复算,错 %d 格)" ↩ % bad | bad == 0 | (待填) | | | - -## S82 _test_same_seed_bit_identical(行 110–113 · 3 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 642 | 110 | "生成返回棋盘(确定性段)" | false | (待填) | | | -| 643 | 112 | "同种子生成的雷集逐位一致" | str(a.mines) == str(b.mines) | (待填) | | | -| 644 | 113 | "同种子生成的数字集逐位一致" | str(a.digits) == str(b.digits) | (待填) | | | - -## S83 _test_clearable_and_inferability_selfconsistent(行 119–126 · 3 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 645 | 119 | "生成返回棋盘(可推理段)" | false | (待填) | | | -| 646 | 122 | "可推理性度量返回非空指标集" | not m.is_empty() | (待填) | | | -| 647 | 125 | "清图判定与安全格总数同在(安全格 %d 个,可清=%s)" ↩ % [safe_total, str(clearable)] | safe_total > 0 | (待填) | | | - -## S84 _test_all_links_valid(行 59–59 · 1 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 648 | 59 | "全对齿的技能池零错(正向面——实际报错 %s)" ↩ % str(_cats(errs)) | errs.is_empty() | (待填) | | | - -## S85 _test_dangling_skill_id(行 70–72 · 2 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 649 | 70 | "池内悬空 skill_id 被报(跨表护栏不静默放过)" | not errs.is_empty() | (待填) | | | -| 650 | 71 | "悬空项的错误类别可定位(category 非空)" | not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "" | (待填) | | | - -## S86 _test_cast_side_mismatch(行 84–84 · 1 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 651 | 84 | "buff 挂主角侧状态被报(施放类 side 守卫)" | _cats(errs).has("cast_side") | (待填) | | | - -## S87 _test_band_hit(行 58–62 · 2 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 652 | 58 | "带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕" | hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | -| 653 | 61 | "精确落带:请求深度 5 命中带 5〔fv1〕" | exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | - -## S88 _test_rowset_order_preserved(行 67–72 · 2 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 654 | 67 | "行集返回同带全部行(2 行)〔fv2〕" | rows.size() == 2 | (待填) | | | -| 655 | 71 | "行集按表文件行序返回(goblin 先于 bugbear——行序即契约)〔fv2〕" | ids == ["goblin", "bugbear"] | (待填) | | | - -## S89 _test_top_band_clamp(行 78–87 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 656 | 78 | "越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕" | rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | -| 657 | 84 | "越顶带发 warning resolve_depth_clamp〔fv3〕" | clamp_hit | (待填) | | | -| 658 | 87 | "同键重复请求警告去重(四元组注册表)〔fv3〕" | _diags.size() == warn_before | (待填) | | | - -## S90 _test_row_kind_routing(行 93–95 · 1 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 659 | 93 | "行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕" | battle_rows.size() == 2 and boss_rows.size() == 1 and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief" | (待填) | | | - -## S91 _test_request_oob(行 101–107 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 660 | 101 | "关卡序号 <1 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | -| 661 | 104 | "节点深度 <0 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | -| 662 | 107 | "row_kind 非法 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | - -## S92 _test_difficulty_clamp(行 119–122 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 663 | 119 | "难度乘数越域被 clamp:hp 99 → 上限 10" | absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001 | (待填) | | | -| 664 | 120 | "难度乘数越域被 clamp:atk 0.01 → 下限 0.1" | absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001 | (待填) | | | -| 665 | 121 | "未登记坐标返回空修饰(零值不伪装成 ×1)" | _resolver.resolve_difficulty_modifiers(3, 3).is_empty() | (待填) | | | - -## S93 _test_field_in_table(行 106–114 · 4 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 666 | 106 | "N 落表:level.tres 读数 2(实际 %d)〔fv6〕" ↩ % n_in_table | n_in_table == 2 | (待填) | | | -| 667 | 110 | "N=1 合法:validate 零错〔fv6〕" | t.validate().is_empty() | (待填) | | | -| 668 | 112 | "N 越域 0:validate 判 domain 错〔fv6〕" | _has_n_domain_err(t.validate()) | (待填) | | | -| 669 | 114 | "N 越域 -1:validate 判 domain 错〔fv6〕" | _has_n_domain_err(t.validate()) | (待填) | | | - -## S94 _test_layer_distance(行 131–133 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 670 | 131 | "层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕" | vis.has(4) | (待填) | | | -| 671 | 132 | "层距 N+1 不可见:锚层 1 排节点 5(层距 3)〔fv1〕" | not vis.has(5) | (待填) | | | -| 672 | 133 | "层距 N+2 不可见:锚层 1 排节点 6(层距 4)〔fv1〕" | not vis.has(6) | (待填) | | | - -## S95 _test_candidate_exempt(行 142–143 · 2 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 673 | 142 | "候选恒显:跨层跳边候选(层距 N+1)仍在可见集〔fv4〕" | vis.has(3) | (待填) | | | -| 674 | 143 | "候选豁免不改层规则:同锚点非候选节点 2(层距 4)不可见〔fv4〕" | not vis.has(2) | (待填) | | | - -## S96 _test_boss_exempt(行 152–155 · 4 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 675 | 152 | "入口停点锚层 0:层距 N 内(节点 2)可见〔fv1〕" | vis.has(2) | (待填) | | | -| 676 | 153 | "入口停点锚层 0:层距 N+2(节点 4)不可见〔fv1〕" | not vis.has(4) | (待填) | | | -| 677 | 154 | "Boss 恒显:层距 6 的末位 Boss 仍在可见集〔fv3〕" | vis.has(7) | (待填) | | | -| 678 | 155 | "Boss 豁免不扩散:同层距的非 Boss 节点 6 不在可见集〔fv3〕" | not vis.has(6) | (待填) | | | - -## S97 _test_single_circle(行 165–167 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 679 | 165 | "单圈回雾:已过远端节点 0(层距 5)不因 visited 豁免〔fv7〕" | not vis.has(0) | (待填) | | | -| 680 | 166 | "单圈回雾:已过中距节点 2(层距 4)不可见〔fv7〕" | not vis.has(2) | (待填) | | | -| 681 | 167 | "单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕" | vis.has(4) | (待填) | | | - -## S98 _test_single_chain_entry(行 176–179 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 682 | 176 | "单链退化形态:入口锚层 0,句柄 2 可见〔fv1〕" | vis.has(2) | (待填) | | | -| 683 | 177 | "单链退化形态:层距 N+1(句柄 3)不可见〔fv1〕" | not vis.has(3) | (待填) | | | -| 684 | 178 | "单链编号空间 = 图句柄 id:Boss 以句柄 6 入集(链上层距 4 的战斗节点 4 入雾)〔fv1〕" | vis.has(6) and not vis.has(4) | (待填) | | | - -## S99 _test_n_from_table(行 188–196 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 685 | 188 | "N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕" | not r.type_visible_ids().has(4) | (待填) | | | -| 686 | 190 | "N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕" | r.type_visible_ids().has(5) | (待填) | | | -| 687 | 196 | "N 读表:N ≥ 全图层高 → 全部非豁免节点可见〔fv6〕" | all_in | (待填) | | | - -## S100 _test_resume_rebuild(行 234–235 · 1 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 688 | 234 | "续玩重建同值:resume 路径与同停点直入 type_visible_ids() 逐位一致〔fv7〕" | resumed.type_visible_ids() == direct.type_visible_ids() | (待填) | | | - -## S101 _smoke_table_migration(行 262–269 · 4 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 689 | 262 | "shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕" | _db.shop.relic_base_price == 15.0 | (待填) | | | -| 690 | 263 | "shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕" | _db.shop.spend_discount == 0.8 | (待填) | | | -| 691 | 264 | "hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕" | _db.hero.display_name == "探险者" | (待填) | | | -| 692 | 268 | "spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕" | spawned and String(units.hero_unit().get("display_name", "")) == String(_db.hero.display_name) | (待填) | | | - -## S102 _smoke_generated_coverage(行 283–320 · 6 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 693 | 283 | "L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" ↩ % L | false | (待填) | | | -| 694 | 290 | "L%d 种子 %d 生成图为空(覆盖扫描面空转)〔v11〕" ↩ % [L, seed_val] | false | (待填) | | | -| 695 | 317 | "生成图覆盖扫描面非空(三关参数档×种子 %d–%d 共 %d 个节点读数)〔v11〕" ↩ % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, scanned] | scanned > 0 | (待填) | | | -| 696 | 318 | "生成图每个 battle/boss 节点深度的敌侧解析落带 ≥ 请求深度(band_of 非 -1·顶 clamp 消解禁)〔v11〕" | enemy_ok | (待填) | | | -| 697 | 319 | "生成图每个节点深度在 board_gen 精确命中(非空且 node_depth 等值)〔v11〕" | board_ok | (待填) | | | -| 698 | 320 | "多终点 Boss 各终点解析带键两两互异(L2/L3×20 种子)〔v11〕" | boss_band_ok | (待填) | | | - -## S103 _smoke_level(行 331–387 · 17 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 699 | 331 | "L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes1.size()] | not nodes1.is_empty() | (待填) | | | -| 700 | 334 | "关卡·满配(atk+200/hp+100)完成(实测:%s @节点 %d)" ↩ % [win.result, win.defeat_node_index] | win.result == "victory" | (待填) | | | -| 701 | 342 | "L1 关卡完成路径逐跳为图出边(入口起步·末节点无出边——句柄轴行进自洽)" | _path_in_graph(nodes1, win.visited_ids) | (待填) | | | -| 702 | 343 | "L1 关卡完成路径战斗/Boss 节点数 = 行进 battles 读数(路径 %d 节点·战斗 %d 颗·battles %d)" ↩ % [win.visited_ids.size(), path_battles, int(win.battles)] | path_battles == int(win.battles) and path_battles >= 2 | (待填) | | | -| 703 | 353 | "L1 终点 Boss 深度 = 参数档层高 %d 且关卡完成路径末节点落该深度(实测终点深度 %d·末句柄 %d)" ↩ % [int(params1.get("layers", -1)), boss_depth1, last_id] | boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1 | (待填) | | | -| 704 | 355 | "L1 终点恰一个(大岔口自 L2 起的图侧投影)" | _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1 | (待填) | | | -| 705 | 356 | "L1 生成图含岔口(出度 ≥2 节点 %d 处)" ↩ % _fork_ids(nodes1).size() | _fork_ids(nodes1).size() >= 1 | (待填) | | | -| 706 | 357 | "进 Boss HP 携带在域内(%d)" ↩ % win.hp_at_boss | win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3 | (待填) | | | -| 707 | 363 | "恢复点公式无源(现行图入口后一位无可直达的恢复候选)" | false | (待填) | | | -| 708 | 366 | "恢复点公式·缺口充分档:回复 = floor(生效max_hp×%d%%) = %d(生效 max %d,实测 %d)" ↩ % [pct1, expect_pct, int(rest_wide["max"]), int(rest_wide["healed"])] | int(rest_wide["healed"]) == expect_pct and expect_pct > 0 | (待填) | | | -| 709 | 370 | "恢复点公式缺口封顶档无源(同图二次驱动未达恢复候选)" | false | (待填) | | | -| 710 | 372 | "恢复点公式·缺口不足档:回复封顶到缺口 2(实测 %d,比例项 %d)" ↩ % [int(rest_narrow["healed"]), expect_pct] | int(rest_narrow["healed"]) == mini(expect_pct, 2) and expect_pct > 2 | (待填) | | | -| 711 | 376 | "关卡·裸装(atk≈1/hp≈10)败北路径可达(实测:%s)" ↩ % lose.result | lose.result == "defeat" | (待填) | | | -| 712 | 377 | "败北节点记录在域内(节点 %d,图节点数 %d)" ↩ % [lose.defeat_node_index, nodes1.size()] | lose.defeat_node_index >= 0 and lose.defeat_node_index < nodes1.size() | (待填) | | | -| 713 | 382 | "关卡回放断言:同种子两遍结算步骤流逐行相同" | JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows) | (待填) | | | -| 714 | 383 | "关卡步骤流非空(%d 行)" ↩ % rep1.rows.size() | rep1.rows.size() > 0 | (待填) | | | -| 715 | 385 | "模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" ↩ % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)] | int(_db.hero.atk) == int(hero_base.atk) and int(_db.hero.max_hp) == int(hero_base.hp) \\ and int(_db.hero.armor_class) == int(hero_base.ac) | (待填) | | | - -## S104 _smoke_level_graph(行 442–761 · 70 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 716 | 442 | "LevelTable 行级校验零错误(图合法性移交 validate_graph)" | _db.level.validate().is_empty() | (待填) | | | -| 717 | 446 | "L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes2.size()] | not nodes2.is_empty() | (待填) | | | -| 718 | 449 | "L2 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" ↩ % [nodes2.size(), int(params2.get("layers", -1)), _nodes_by_id(nodes2).size()] | nodes2.size() >= int(params2.get("layers", -1)) + 1 and _nodes_by_id(nodes2).size() == nodes2.size() | (待填) | | | -| 719 | 451 | "L2 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" ↩ % [tcount["battle"], tcount["boss"], tcount["reward"], tcount["rest"], tcount["shop"], nodes2.size()] | int(tcount["battle"]) + int(tcount["boss"]) + int(tcount["reward"]) + int(tcount["rest"]) + int(tcount["shop"]) == nodes2.size() | (待填) | | | -| 720 | 453 | "L2 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" ↩ % [tcount["battle"], int(params2.get("battle_floor", -1)), tcount["boss"], int(params2.get("exit_count", -1))] | int(tcount["battle"]) >= int(params2.get("battle_floor", -1)) and int(tcount["boss"]) == int(params2.get("exit_count", -1)) | (待填) | | | -| 721 | 455 | "L2 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" ↩ % [tcount["rest"], tcount["reward"], tcount["shop"]] | int(tcount["rest"]) >= 1 and int(tcount["reward"]) >= 1 and int(tcount["shop"]) >= 1 | (待填) | | | -| 722 | 457 | "L2 岔口(出度 ≥2 的节点)= %d 处(≥1)" ↩ % branch | branch >= 1 | (待填) | | | -| 723 | 467 | "图校验正例:两岔合流小图零错误〔v1〕" | LT.validate_graph(good).is_empty() | (待填) | | | -| 724 | 470 | "图校验拒绝:句柄重复(图内唯一句柄)〔v1〕" | _has_err_category(LT.validate_graph(dup), "dup_key") | (待填) | | | -| 725 | 473 | "图校验正例:多入口合法(入口唯一判据随模板池退役)〔v1〕" | LT.validate_graph(dual).is_empty() | (待填) | | | -| 726 | 476 | "图校验拒绝:Boss 带出边(终点不可外出)〔v1〕" | _has_err_category(LT.validate_graph(boss_out), "structural") | (待填) | | | -| 727 | 479 | "图校验拒绝:回边 1→0(目标深度须严格大于源)〔v1〕" | _has_err_category(LT.validate_graph(cyc), "structural") | (待填) | | | -| 728 | 482 | "图校验拒绝:出边指向不存在句柄(悬空致命)〔v1〕" | _has_err_category(LT.validate_graph(dang), "dangling_next") | (待填) | | | -| 729 | 488 | "图校验拒绝:非 Boss 终点(终点恒为 Boss)〔v1〕" | _has_err_category(LT.validate_graph(orphan), "structural") | (待填) | | | -| 730 | 491 | "L2·满配 min 路径完成(实测:%s)" ↩ % win_min.result | win_min.result == "victory" | (待填) | | | -| 731 | 496 | "L2 min 路径逐跳为图出边·末节点为 Boss(实测路径 %s·末句柄型别 %s)" ↩ % [str(win_min.visited_ids), n2_last_type] | _path_in_graph(nodes2, win_min.visited_ids) and n2_last_type == "boss" | (待填) | | | -| 732 | 498 | "L2 min 路径终点深度 = 参数档层高 %d(行进读数 %d——终点落最长路径步数)" ↩ % [int(params2.get("layers", -1)), int(win_min.boss_depth)] | int(win_min.boss_depth) == int(params2.get("layers", -1)) | (待填) | | | -| 733 | 500 | "L2 min 路径穿商店恰 1 家(路径句柄 %s 型别计数 = 1——必经恰一店的行进面)" ↩ % str(win_min.visited_ids) | _path_shop_count(nodes2, win_min.visited_ids) == 1 | (待填) | | | -| 734 | 502 | "选路口发生 %d 次(每节点兑付后——零自动推进)" ↩ % win_min.choices | win_min.choices >= 3 | (待填) | | | -| 735 | 504 | "L2·满配 max 路径(候选取最大句柄)完成(实测:%s·路径 %s)" ↩ % [win_max.result, str(win_max.visited_ids)] | win_max.result == "victory" | (待填) | | | -| 736 | 509 | "L2 max 路径逐跳为图出边·末节点为 Boss·穿商店恰 1 家" | _path_in_graph(nodes2, win_max.visited_ids) and n2_last_max_type == "boss" and _path_shop_count(nodes2, win_max.visited_ids) == 1 | (待填) | | | -| 737 | 512 | "L2 max 与 min 路径分岔(方向轴生效——两路终点句柄 %d / %d 不同)" ↩ % [n2_last, n2_last_max] | n2_last != n2_last_max and win_max.visited_ids != win_min.visited_ids | (待填) | | | -| 738 | 519 | "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;图 = 参数档 × 种子 %d)" ↩ % ACTIVE_GRAPH_SEED | runner.start_level(2, ACTIVE_GRAPH_SEED) | (待填) | | | -| 739 | 526 | "首战胜利后:settlement 释放 + 进选路态(战斗胜利不再自动推进——接口漂移清偿)" | String(res_box.get("result", "")) == "victory" and runner.current_battle() == null and runner.awaiting_choice() | (待填) | | | -| 740 | 529 | "runner 消费面图 = 生成面图(节点数 %d/%d 且句柄集逐位相同)" ↩ % [rnodes.size(), nodes2.size()] | rnodes.size() == nodes2.size() and _ids_of_type(rnodes, "battle").size() + _ids_of_type(rnodes, "boss").size() + _ids_of_type(rnodes, "reward").size() \\ + _ids_of_type(rnodes, "rest").size() + _ids_of_type(rnodes, "shop").size() == nodes2.size() and _nodes_by_id(rnodes) == _nodes_by_id(nodes2) | (待填) | | | -| 741 | 537 | "入口选路口候选非空且全在图内(候选 %s·类型 %s)" ↩ % [str(cand), str(cand_types)] | not cand.is_empty() and not cand_types.has("<不在图>") | (待填) | | | -| 742 | 538 | "候选下一跳类型两两互异(岔口去路类型不重复契约——实测 %s)" ↩ % str(cand_types) | cand_types.size() == _uniq(cand_types).size() | (待填) | | | -| 743 | 545 | "候选外句柄 %d 拒绝且仍待选(拒绝语义)" ↩ % outside_id | outside_id >= 0 and not runner.pick_next_node(outside_id) and runner.awaiting_choice() | (待填) | | | -| 744 | 555 | "入口岔口含奖励位与战斗位(句柄 %d/%d——候选型别可定位)" ↩ % [reward_cand, battle_cand] | reward_cand >= 0 and battle_cand >= 0 | (待填) | | | -| 745 | 556 | "选奖励候选 %d 受理" ↩ % reward_cand | runner.pick_next_node(reward_cand) | (待填) | | | -| 746 | 557 | "到达奖励点:直通兑付(确认前不出选路口)" | String(runner.current_node().get("type", "")) == "reward" and not runner.awaiting_choice() | (待填) | | | -| 747 | 559 | "兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = 该点图出边 %s" ↩ % str(reward_outs) | runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(reward_outs) | (待填) | | | -| 748 | 566 | "选下一跳战斗候选 %d 开战" ↩ % next_battle | next_battle >= 0 and runner.pick_next_node(next_battle) and runner.current_battle() != null | (待填) | | | -| 749 | 591 | "首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷" | green_ok | (待填) | | | -| 750 | 592 | "首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" ↩ % green_wins | green_wins >= 7 | (待填) | | | -| 751 | 593 | "首遇战胜后 HP 下探读数 %d(>0——绿档记录)" ↩ % green_min_hp | green_min_hp > 0 | (待填) | | | -| 752 | 597 | "跨表覆盖扫描面非空:L2 现行图战斗/Boss 节点 %d 颗" ↩ % int(cov2["nodes"]) | int(cov2["nodes"]) >= 1 | (待填) | | | -| 753 | 598 | "L2 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" ↩ % int(cov2["enemy_bad"]) | int(cov2["enemy_bad"]) == 0 | (待填) | | | -| 754 | 599 | "L2 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" ↩ % int(cov2["board_bad"]) | int(cov2["board_bad"]) == 0 | (待填) | | | -| 755 | 607 | "巨蜘蛛首遇带(2,battle,%d——扫表取最低带)行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max(固定 2 只——首遇教学位不加骰)〔fv22〕" ↩ % l2_spider_band | l2_spider_band >= 0 and l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1 \\ and int(l2_spider_set[0].get("unit_count", -1)) == 2 and not l2_spider_set[0].has("unit_count_max") | (待填) | | | -| 756 | 617 | "L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" ↩ % str(l2_terms) | l2_boss_ok | (待填) | | | -| 757 | 624 | "首批混编行集 (2,battle,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)〔fv18〕" | mix_shape_ok | (待填) | | | -| 758 | 628 | "首批混编行集 (2,battle,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])〔fv19〕" ↩ % mix_sum_max | mix_sum_max <= 4 | (待填) | | | -| 759 | 629 | "首批混编新行 (2,battle,5,giant_spider) 行值 = 复制巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)" | int(l2_mix_set[1].get("unit_count", 0)) == 2 and int(l2_mix_set[1].get("unit_count_max", 0)) == 3 \\ and int(l2_mix_set[1].get("hp", 0)) == 26 and int(l2_mix_set[1].get("armor_class", -1)) == 2 | (待填) | | | -| 760 | 636 | "L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes3.size()] | not nodes3.is_empty() | (待填) | | | -| 761 | 639 | "L3 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" ↩ % [nodes3.size(), int(params3.get("layers", -1)), _nodes_by_id(nodes3).size()] | nodes3.size() >= int(params3.get("layers", -1)) + 1 and _nodes_by_id(nodes3).size() == nodes3.size() | (待填) | | | -| 762 | 641 | "L3 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" ↩ % [tcount3["battle"], tcount3["boss"], tcount3["reward"], tcount3["rest"], tcount3["shop"], nodes3.size()] | int(tcount3["battle"]) + int(tcount3["boss"]) + int(tcount3["reward"]) + int(tcount3["rest"]) + int(tcount3["shop"]) == nodes3.size() | (待填) | | | -| 763 | 643 | "L3 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" ↩ % [tcount3["battle"], int(params3.get("battle_floor", -1)), tcount3["boss"], int(params3.get("exit_count", -1))] | int(tcount3["battle"]) >= int(params3.get("battle_floor", -1)) and int(tcount3["boss"]) == int(params3.get("exit_count", -1)) | (待填) | | | -| 764 | 645 | "L3 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" ↩ % [tcount3["rest"], tcount3["reward"], tcount3["shop"]] | int(tcount3["rest"]) >= 1 and int(tcount3["reward"]) >= 1 and int(tcount3["shop"]) >= 1 | (待填) | | | -| 765 | 647 | "L3 岔口(出度 ≥2 的节点)= %d 处(≥1)" ↩ % branch3 | branch3 >= 1 | (待填) | | | -| 766 | 648 | "L3 入口数 %d = 参数档 entry_count · 终点数 %d = exit_count ≥2(多终点大岔口自 L2 起)" ↩ % [_entry_ids(nodes3).size(), _terminal_ids(nodes3).size()] | _entry_ids(nodes3).size() == int(params3.get("entry_count", -1)) and _terminal_ids(nodes3).size() == int(params3.get("exit_count", -1)) and _terminal_ids(nodes3).size() >= 2 | (待填) | | | -| 767 | 652 | "跨表覆盖扫描面非空:L3 现行图战斗/Boss 节点 %d 颗" ↩ % int(cov3["nodes"]) | int(cov3["nodes"]) >= 1 | (待填) | | | -| 768 | 653 | "L3 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" ↩ % int(cov3["enemy_bad"]) | int(cov3["enemy_bad"]) == 0 | (待填) | | | -| 769 | 654 | "L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" ↩ % int(cov3["board_bad"]) | int(cov3["board_bad"]) == 0 | (待填) | | | -| 770 | 661 | "灰矮人首遇带(3,battle,%d——扫表取最低带)行集 = {duergar} 恰一行、首行重甲、固定 1 只不加骰〔fv23〕" ↩ % l3_lead_band | l3_lead_band >= 0 and l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 \\ and String(l3_lead_set[0].get("armor_type", "")) == "heavy" \\ and int(l3_lead_set[0].get("unit_count", -1)) == 1 and not l3_lead_set[0].has("unit_count_max") | (待填) | | | -| 771 | 672 | "L3 全部终点深度 %s 的 Boss 行集均 = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" ↩ % str(l3_terms) | l3_boss_ok | (待填) | | | -| 772 | 695 | "L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷" | green3_ok | (待填) | | | -| 773 | 696 | "L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" ↩ % green3_min_hp | green3_min_hp > 0 | (待填) | | | -| 774 | 706 | "混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)" | mix_sys.spawn(1, 5, int(_db.hero.max_hp)) | (待填) | | | -| 775 | 708 | "混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)" | mix_units.size() == 3 | (待填) | | | -| 776 | 709 | "混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)" | String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear" | (待填) | | | -| 777 | 710 | "混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)" | String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3 | (待填) | | | -| 778 | 714 | "行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)" | not over_sys.spawn(1, 6, int(_db.hero.max_hp)) | (待填) | | | -| 779 | 719 | "混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)" | int(_db.enemy.rows.size()) == 28 | (待填) | | | -| 780 | 725 | "可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)" | vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1 | (待填) | | | -| 781 | 731 | "可见性冒烟:入口停点候选集 ⊆ 类型可见集(下一跳恒可见)〔fv4〕" | vis_cand_in | (待填) | | | -| 782 | 736 | "可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" ↩ % str(_ids_of_type(nodes2, "boss")) | vis_boss_ok | (待填) | | | -| 783 | 738 | "可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" ↩ % vis_n | vis_n == 2 | (待填) | | | -| 784 | 760 | "可见性冒烟:入口锚层 0 时层距 ≤ %d 的节点全在可见集(扫描面 %d 颗·违例 0)〔fv1〕" ↩ % [vis_n, near_count] | near_count >= 1 and near_ok | (待填) | | | -| 785 | 761 | "可见性冒烟:入口锚层 0 时层距 > %d 的非终点节点全入雾(扫描面 %d 颗·违例 0)〔fv1〕" ↩ % [vis_n, fog_count] | fog_count >= 1 and fog_ok | (待填) | | | - -## S105 _smoke_gp_wallet(行 798–834 · 9 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 786 | 798 | "GP 到账:L1 启动(普通战档)" | runner.start_level(1, 42) | (待填) | | | -| 787 | 808 | "L1 首战胜利(battle_finished gp_dropped 载荷在案)" | fin.has("p") and String(fin["p"].get("result", "")) == "victory" | (待填) | | | -| 788 | 811 | "L1 普通战 gp_dropped − escrow ∈ [2+0, 4+0](掷定+关级加值 %d,escrow %+d)" ↩ % [gp_dropped - escrow_sum, escrow_sum] | gp_dropped - escrow_sum >= 2 and gp_dropped - escrow_sum <= 4 | (待填) | | | -| 789 | 816 | "钱包恰入账一次(gain 事件 ×%d)" ↩ % gains.size() | gains.size() == 1 | (待填) | | | -| 790 | 818 | "入账金额 = gp_dropped 载荷(%d)" ↩ % gp_dropped | int(gains[0]["amount"]) == gp_dropped | (待填) | | | -| 791 | 819 | "钱包读数 = 入账后投影(gp=%d,破财负值场景经 clamp ≥0)" ↩ % runner.gp | runner.gp == maxi(0, gp_dropped) and runner.gp == int(gains[0]["gp"]) | (待填) | | | -| 792 | 826 | "L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)" | boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 793 | 831 | "L2 Boss 战终局(%s——gp_dropped 于战斗末掷定上抛)" ↩ % String(boss.battle_result) | fin_b.has("p") | (待填) | | | -| 794 | 834 | "L2 Boss 战 gp_dropped − escrow ∈ [6+1, 10+1](掷定+关级加值 %d,escrow %+d)" ↩ % [gp_b - escrow_b, escrow_b] | gp_b - escrow_b >= 7 and gp_b - escrow_b <= 11 | (待填) | | | - -## S106 _smoke_gp_escrow(行 872–936 · 12 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 795 | 872 | "GP 袋(gp 型安全格)可挖掘局种子可达(seed=%s)" ↩ % str(scan.get("seed", -1)) | not scan.is_empty() | (待填) | | | -| 796 | 885 | "GP局战斗终局(先手 999 收束)" | settlement2.battle_result == "victory" | (待填) | | | -| 797 | 890 | "GP 袋兑付行在案(GP 袋 +3 → 战斗内缓冲,origin 指回来源格)" | gain_ok | (待填) | | | -| 798 | 891 | "escrow 累计缓冲 ≥3(实测 %d——战斗域中间态,不入钱包不入增益容器)" ↩ % int(settlement2.reward_grant().escrow) | int(settlement2.reward_grant().escrow) >= 3 | (待填) | | | -| 799 | 893 | "格面兑付计入 gp_dropped(%d − escrow %d ∈ [2,4]——战斗末一次到账,随上抛即弃)" ↩ % [gp_d, int(settlement2.reward_grant().escrow)] | gp_d - int(settlement2.reward_grant().escrow) >= 2 and gp_d - int(settlement2.reward_grant().escrow) <= 4 | (待填) | | | -| 800 | 899 | "破财直验战斗启动" | settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}) | (待填) | | | -| 801 | 903 | "破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)" | int(settle_l.reward_grant().escrow) == -6 | (待填) | | | -| 802 | 908 | "破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" ↩ % [ gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1] | gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1 | (待填) | | | -| 803 | 918 | "clamp 场景:L1 启动(冒险钱包开局 0)" | runner.start_level(1, 4242) | (待填) | | | -| 804 | 930 | "破财压制入账载荷为负(gp_dropped=%d)" ↩ % gp3 | gp3 < 0 | (待填) | | | -| 805 | 935 | "破财入账恰一次且投影读数 0(amount=%d,clamp 后 gp=0)" ↩ % gp3 | gain3.size() == 1 and int(gain3[0]["amount"]) == gp3 and int(gain3[0]["gp"]) == 0 | (待填) | | | -| 806 | 936 | "钱包全程不透支(gp=%d,事件投影最小读数 %d ≥ 0)" ↩ % [runner.gp, int(ev_box["min_gp"])] | runner.gp == 0 and int(ev_box["min_gp"]) >= 0 | (待填) | | | - -## S107 _smoke_shop_room(行 1002–1088 · 25 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 807 | 1002 | "商店房到达种子可达(seed=%s——进店 GP ≥ 16)" ↩ % str(shop_run.get("seed", -1)) | not shop_run.is_empty() | (待填) | | | -| 808 | 1015 | "L2 行进到商店房:当前节点型别 = shop 且出边恰一条指终点 Boss(句柄 %d·出边 %s)" ↩ % [shop_id, str(shop_outs)] | String(runner.current_node().get("type", "")) == "shop" and shop_outs.size() == 1 \\ and String(s_byid.get(int(shop_outs[0]), {}).get("type", "")) == "boss" \\ and (s_byid.get(int(shop_outs[0]), {}).get("next", []) as Array).is_empty() | (待填) | | | -| 809 | 1019 | "L2 到店路径穿商店恰 1 家(已到句柄 %s 型别计数——必经恰一店的行进面)" ↩ % str(runner.visited_ids()) | _path_shop_count(s_nodes, runner.visited_ids()) == 1 | (待填) | | | -| 810 | 1021 | "L2 商店节点(句柄 %d)入边恰一条(店位归终点的直接前驱——行进面复核)" ↩ % shop_id | shop_preds == 1 | (待填) | | | -| 811 | 1023 | "到店商店节点无出边(夹具停摆——去终点方向无法从图解析)" | false | (待填) | | | -| 812 | 1025 | "到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)" | not runner.awaiting_choice() and runner.next_candidate_ids().is_empty() | (待填) | | | -| 813 | 1028 | "三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)" | prods.size() == 3 | (待填) | | | -| 814 | 1030 | "L2 首购现价 = 基价 + 关级加值 2(atk_up 6 / hp_up 4 / armor_up 6)" | int(by_id["atk_up"]["price"]) == 6 and int(by_id["hp_up"]["price"]) == 4 and int(by_id["armor_up"]["price"]) == 6 | (待填) | | | -| 815 | 1033 | "购买 atk_up 受理(现价 6)" | runner.buy_product("atk_up") | (待填) | | | -| 816 | 1034 | "钱包扣现价 6(%d → %d)" ↩ % [gp0, runner.gp] | runner.gp == gp0 - 6 | (待填) | | | -| 817 | 1036 | "统一冒险加算区 atk +1(加算区住加算日记)" | runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0 | (待填) | | | -| 818 | 1038 | "atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)" | int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1 | (待填) | | | -| 819 | 1045 | "购买 hp_up 受理(现价 4)" | runner.buy_product("hp_up") | (待填) | | | -| 820 | 1046 | "钱包扣 4(%d)" ↩ % runner.gp | runner.gp == gp0 - 6 - 4 | (待填) | | | -| 821 | 1047 | "生命上限 +3(%d → %d——加算区 max_hp 轴)" ↩ % [mhp_before, runner.effective_max_hp()] | runner.effective_max_hp() == mhp_before + 3 | (待填) | | | -| 822 | 1048 | "当前生命同步 +3 封顶新上限(%d → %d)" ↩ % [hp_before, runner.hero_hp] | runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()) | (待填) | | | -| 823 | 1050 | "购买 armor_up 受理(现价 6)" | runner.buy_product("armor_up") | (待填) | | | -| 824 | 1051 | "AC = 10+1(armor_class 加算区轴,无百分比通道)" | runner.effective_ac() == int(_db.hero.armor_class) + 1 | (待填) | | | -| 825 | 1066 | "余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" ↩ % [gp_dry, hp_dry] | not runner.buy_product("hp_up") and runner.gp == gp_dry and runner.purchased_tier("hp_up") == tier_dry | (待填) | | | -| 826 | 1068 | "离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" ↩ % str(shop_outs) | runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs) | (待填) | | | -| 827 | 1071 | "主角攻击生效值 = 2+1(基础表值 + 加算区)" | runner.effective_atk() == int(_db.hero.atk) + 1 | (待填) | | | -| 828 | 1072 | "选终点 Boss(句柄 %d)开战" ↩ % int(shop_outs[0]) | runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null | (待填) | | | -| 829 | 1076 | "战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" ↩ % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys] | int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys | (待填) | | | -| 830 | 1087 | "冒险收束(Boss 战终局:%s)" ↩ % String(fin_end.get("p", {}).get("result", "")) | fin_end.has("p") and String(fin_end["p"].get("result", "")) in ["victory", "defeat"] | (待填) | | | -| 831 | 1088 | "冒险收束钱包清零(局内货币生命周期终点——不入存档不入摘要)" | runner.gp == 0 | (待填) | | | - -## S108 _smoke_shop_graph(行 1110–1167 · 9 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 832 | 1110 | "L%d 参数档缺行(商店编排断言无源可扫)" ↩ % L | false | (待填) | | | -| 833 | 1115 | "L%d 种子 %d 生成图为空(编排断言扫描面空转)" ↩ % [L, seed_val] | false | (待填) | | | -| 834 | 1141 | "编排断言扫描面非空(三关参数档 × 种子 %d–%d=图 %d 张·终点 %d·路径 %d)〔v5〕" ↩ % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, graph_total, term_total, path_total] | graph_total == 3 * (SWEEP_SEED_TO - SWEEP_SEED_FROM) and term_total >= 3 and path_total >= 3 | (待填) | | | -| 835 | 1143 | "性质① 每个终点的直接前驱恰一家商店(入边一条且源型别=shop·违例 %d/%d 终点)〔v5〕" ↩ % [pred_bad, term_total] | pred_bad == 0 | (待填) | | | -| 836 | 1144 | "性质② 每图商店节点数 = 终点数(违例 %d/%d 图)〔v5〕" ↩ % [count_bad, graph_total] | count_bad == 0 | (待填) | | | -| 837 | 1145 | "性质③ 每条入口→终点路径恰穿一家商店(违例 %d/%d 条路径)〔v5〕" ↩ % [path_bad, path_total] | path_bad == 0 | (待填) | | | -| 838 | 1151 | "L%d 现行图终点 %d 个 = 商店 %d 个·入口→终点路径 %d 条全穿恰一店" ↩ % [L, cur_terms.size(), cur_shops.size(), _all_entry_terminal_paths(cur_nodes).size()] | cur_terms.size() == cur_shops.size() and not cur_terms.is_empty() and _all_entry_terminal_paths(cur_nodes).size() >= cur_terms.size() and _paths_all_one_shop(cur_nodes) | (待填) | | | -| 839 | 1163 | "L%d 现行图终点深度 %s 逐点 Boss 行集解析命中且落带 ≥ 终点深度" ↩ % [L, str(tdepths)] | hit_ok | (待填) | | | -| 840 | 1166 | "L1 现行图跨表覆盖(战斗/Boss 节点 %d 颗非空·敌侧违例 %d·板侧违例 %d)" ↩ % [int(l1_cov["nodes"]), int(l1_cov["enemy_bad"]), int(l1_cov["board_bad"])] | int(l1_cov["nodes"]) >= 1 and int(l1_cov["enemy_bad"]) == 0 and int(l1_cov["board_bad"]) == 0 | (待填) | | | - -## S109 _smoke_multi_terminal_settlement(行 1175–1222 · 5 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 841 | 1175 | "L2 现行图为多终点(Boss 终点 %s 两颗以上——大岔口承载面)" ↩ % str(boss_ids) | boss_ids.size() >= 2 | (待填) | | | -| 842 | 1187 | "L2 min/max 两路各打进各自终点 Boss(末句柄 %d / %d 互异且都在图 Boss 集 %s 内;两路 result=%s/%s)" ↩ % [min_last, max_last, str(boss_ids), String(run_min.get("result", "")), String(run_max.get("result", ""))] | String(run_min.get("result", "")) == "victory" and String(run_max.get("result", "")) == "victory" and boss_ids.has(min_last) and boss_ids.has(max_last) and min_last != max_last | (待填) | | | -| 843 | 1191 | "min 路未到 max 终点 %d、max 路未到 min 终点 %d(区域隔离——走 A 区不误兑付 B 区 Boss)" ↩ % [max_last, min_last] | not min_vis.has(max_last) and not max_vis.has(min_last) | (待填) | | | -| 844 | 1198 | "L2 全程驱动启动受理" | runner.start_level(2, 42) | (待填) | | | -| 845 | 1220 | "任一 Boss 胜即本关收束恰一次(count=%d·载荷 result=%s·node_id=%s 在图 Boss 集 %s 内)" ↩ % [int(fin["count"]), String(payload.get("result", "")), str(payload.get("node_id", -1)), str(boss_ids)] | int(fin["count"]) == 1 and String(payload.get("result", "")) == "victory" and boss_ids.has(int(payload.get("node_id", -1))) | (待填) | | | - -## S110 _smoke_shop_pool(行 1257–1391 · 29 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 846 | 1257 | "池模式商店房场景可达(seed=%s)" ↩ % str(scen.get("s", -1)) | not scen.is_empty() | (待填) | | | -| 847 | 1265 | "进店抽 3(货架 %d 卡——同构交互)" ↩ % first_shelf_a.size() | first_shelf_a.size() == 3 | (待填) | | | -| 848 | 1271 | "刷新首价=基价(%d)" ↩ % cost0 | cost0 == int(_db.adventure.shop_refresh_cost_base) | (待填) | | | -| 849 | 1272 | "基础免费 0 次(Q2——天赋增益位后续添加)" | r8.shop_refresh_free_left() == 0 | (待填) | | | -| 850 | 1274 | "GP 刷新受理(gp=%d cost=%d)" ↩ % [gp0, cost0] | r8.shop_refresh() | (待填) | | | -| 851 | 1275 | "GP 刷新扣费(%d→%d)" ↩ % [gp0, int(r8.gp)] | int(r8.gp) == gp0 - cost0 | (待填) | | | -| 852 | 1276 | "刷新爬价 ×curve(%d)" ↩ % int(r8.shop_refresh_cost()) | int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.adventure.shop_refresh_cost_curve))) | (待填) | | | -| 853 | 1284 | "同种子同货架逐位(确定性重放承载——断点货架续玩语义)" | same_shelf | (待填) | | | -| 854 | 1294 | "relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" ↩ % relic_price_now | relic_price_now == relic_price_pre | (待填) | | | -| 855 | 1300 | "货架 kind 在封闭词表或保底 relic 卡(%s)" ↩ % ckind | is_relic or shelf_kinds.has(ckind) | (待填) | | | -| 856 | 1307 | "首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" ↩ % [ckind, int(c.get("price", -1)), expect_p] | int(c.get("price", -1)) == expect_p | (待填) | | | -| 857 | 1314 | "可购卡在架(GP %d——种子扫描保证首购空间)" ↩ % int(r8.gp) | not buy_c.is_empty() | (待填) | | | -| 858 | 1322 | "购买受理(pool 口——%s\|%s)" ↩ % [kind, tier] | r8.buy_pool_item(kind) | (待填) | | | -| 859 | 1323 | "钱包扣现价(%d → %d)" ↩ % [gp0, int(r8.gp)] | int(r8.gp) == gp0 - int(buy_c.get("price", 0)) | (待填) | | | -| 860 | 1324 | "加算区 +令牌档位件值(池件=奖励件本体——%s\|%s %+d)" ↩ % [kind, tier, int(_db.adventure.curio_value(kind, tier))] | a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)) | (待填) | | | -| 861 | 1325 | "共享池账本 -1(跨渠道单点——taken %d → %d)" ↩ % [taken0, int(a8._curio.taken.get(kind, 0))] | int(a8._curio.taken.get(kind, 0)) == taken0 + 1 | (待填) | | | -| 862 | 1337 | "A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" ↩ % expect_p2 | int(second.get("price", -1)) == expect_p2 | (待填) | | | -| 863 | 1338 | "同 visit 第二购受理(A 链软闸在价不在限)" | r8.buy_pool_item(String(second.get("kind_id", ""))) | (待填) | | | -| 864 | 1340 | "A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败" | false | (待填) | | | -| 865 | 1348 | "切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)" | not cut_seen | (待填) | | | -| 866 | 1359 | "库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)" | not attack_seen | (待填) | | | -| 867 | 1361 | "池空消费拒绝(handler 零副作用——买光即枯竭硬闸)" | not a8.stop().consume_pool_purchase("attack", "common") and a8.deck().pool_stock("attack") == stock_before | (待填) | | | -| 868 | 1364 | "商店消费口受理(停点件兑付单点同口——真单点库)〔拆件批 V3〕" | a8.stop().consume_pool_purchase("attack", "common") | (待填) | | | -| 869 | 1370 | "商店购买进奖励牌库摘牌(买一件=奖励面少一件——shop→reward 向共库)" | not reward_attack_seen | (待填) | | | -| 870 | 1375 | "离开商店 → 回选路口(货架态清零落档——零自动推进收口)" | r8.continue_after_node() and r8.awaiting_choice() | (待填) | | | -| 871 | 1377 | "断点扩键 shop_shelf 在案(离开店=空架合法态——P2 甲扩列)" | bp.has("shop_shelf") and (bp["shop_shelf"] as Array).is_empty() | (待填) | | | -| 872 | 1378 | "断点扩键 shop_visit_buys 在案(离开店清零——A 链输入不入档语义)" | bp.has("shop_visit_buys") and int(bp["shop_visit_buys"]) == 0 | (待填) | | | -| 873 | 1383 | "终局池读数六键出讫(run_adventure pool 面——四池种+T5 专长凭证+T6 heal 行,读数只锚一次载体)" | ps.size() == 6 | (待填) | | | -| 874 | 1391 | "终局池余量在域 [0,配额](%s)" ↩ % str(ps) | ps_ok | (待填) | | | - -## S111 _test_positive(行 46–56 · 3 条 · 源 game/tests/unit/core/level_table_validate_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 875 | 46 | "四检正例:多入口多终点图零错误〔v1〕" | _errs(multi).is_empty() | (待填) | | | -| 876 | 53 | "四检正例:含岔口单终点图零错误〔v1〕" | _errs(forked).is_empty() | (待填) | | | -| 877 | 56 | "四检正例:单链退化图零错误(不设必有岔口)〔v1〕" | _errs(chain).is_empty() | (待填) | | | - -## S112 _test_negative(行 63–89 · 11 条 · 源 game/tests/unit/core/level_table_validate_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 878 | 63 | "负例:终点非 Boss 命中「终点恒为 Boss」〔v1〕" | _has(_errs(non_boss_end), "structural", "终点 1 非 Boss") | (待填) | | | -| 879 | 66 | "负例:Boss 带出边被拒〔v1〕" | _has(_errs(boss_out), "structural", "Boss 节点 1 带出边") | (待填) | | | -| 880 | 68 | "负例:回边(目标深度不严格大于源)命中前进方向判据〔v1〕" | _has(_errs(back_edge), "structural", "违反前进方向") | (待填) | | | -| 881 | 71 | "负例:环(2→1 回边)被前进方向拦下〔v1〕" | _has(_errs(cycle), "structural", "违反前进方向") | (待填) | | | -| 882 | 77 | "判据②正证:两独立链(双入口双终点)不误伤·全员可达〔v1〕" | _errs(two_components).is_empty() | (待填) | | | -| 883 | 79 | "判据②兜底面:自环先被前进方向拦下(④ 与 ② 互斥的实证)〔v1〕" | _has(_errs(self_loop), "structural", "违反前进方向") | (待填) | | | -| 884 | 82 | "负例:悬空出边报 dangling_next〔v1〕" | _has(_errs(dangling), "dangling_next") | (待填) | | | -| 885 | 84 | "负例:句柄重复报 dup_key〔v1〕" | _has(_errs(dup), "dup_key") | (待填) | | | -| 886 | 86 | "负例:battle 节点缺深度被良构基底拒〔v1〕" | _has(_errs(no_depth), "structural", "缺深度") | (待填) | | | -| 887 | 87 | "负例:空节点集被拒〔v1〕" | _has(_errs([]), "structural", "图节点集为空") | (待填) | | | -| 888 | 89 | "负例:类型出域报 enum〔v1〕" | _has(_errs(bad_type), "enum") | (待填) | | | - -## S113 _test_scope_not_extended(行 97–102 · 3 条 · 源 game/tests/unit/core/level_table_validate_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 889 | 97 | "范围反证:无商店图不在加载校验内报错(商店位由冒烟断言把关)〔v1〕" | _errs(no_shop).is_empty() | (待填) | | | -| 890 | 99 | "范围反证:双入口合法(入口唯一判据已随模板池退役,个数归产出侧)〔v1〕" | _errs(two_entries).is_empty() | (待填) | | | -| 891 | 102 | "范围反证:节点缺 next 字段仍被良构基底拒〔v1〕" | _has(_errs(missing_key), "structural", "缺字段 next") | (待填) | | | - -## S114 _meta_pricing_and_buy(行 138–151 · 4 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 892 | 138 | "树购买受理(Lv 0→1、余额 %d−%d)" ↩ % [bal0, price0] | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 and sv.keystone_balance() == bal0 - price0 | (待填) | | | -| 893 | 141 | "购买持久(reload 后级与余额不变)" | sv_r.tree_level("sp_hp") == 1 and sv_r.keystone_balance() == bal0 - price0 | (待填) | | | -| 894 | 148 | "余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)" | not mp_dry.tree_buy("sp_hp") and sv_dry.keystone_balance() == 0 and sv_dry.tree_nodes() == tn_before and sv_dry.fork_sides() == fs_before | (待填) | | | -| 895 | 151 | "未知节点 false 零副作用(词表外出域)" | not mp_dry.tree_buy("no_such_node") | (待填) | | | - -## S115 _meta_settle_and_workshop(行 168–341 · 42 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 896 | 168 | "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5——明细随摘要归档)" | bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1)) and int(drop1.get("level", 0)) == 1 | (待填) | | | -| 897 | 171 | "明细 total = keystone_total(逐杀掉落全源——无二次派生加账)" | int(d1.get("total", 0)) == int(row1.get("first_kill", 0)) | (待填) | | | -| 898 | 173 | "余额 = 累计入账(逐杀掉落合计——无重复加账)" | sv.keystone_balance() == int(row1.get("first_kill", 0)) | (待填) | | | -| 899 | 181 | "再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)" | not bool(drop2.get("first", true)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)) | (待填) | | | -| 900 | 187 | "重复杀第三轮恒定不递减(验收5——无衰减曲线)" | not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)) | (待填) | | | -| 901 | 195 | "两关完成逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)" | not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false)) and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)) | (待填) | | | -| 902 | 203 | "败北零关卡完成摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)" | dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before | (待填) | | | -| 903 | 210 | "条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕" | not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye") and not mp_u.buy_treasure_unlock("ranged_pick") | (待填) | | | -| 904 | 213 | "余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕" | not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10 | (待填) | | | -| 905 | 215 | "钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕" | mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order") and sv_u.keystone_balance() == 0 | (待填) | | | -| 906 | 218 | "已解锁重复购买拒绝〔秘宝批 V3〕" | not mp_u.buy_treasure_unlock("morning_order") | (待填) | | | -| 907 | 220 | "条件未达不解锁(远程镐=冒险成功 1 次)〔秘宝批 V4〕" | mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick") | (待填) | | | -| 908 | 223 | "远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:冒险成功=打通整局冒险)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick") and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty() | (待填) | | | -| 909 | 227 | "刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("refresh_license") | (待填) | | | -| 910 | 230 | "天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕" | not sv_u.item_unlocked("sky_eye") | (待填) | | | -| 911 | 233 | "天眼双条件 AND(冒险成功≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("sky_eye") | (待填) | | | -| 912 | 236 | "信标网络=真视类工具累计使用≥10(行为计数新域——use_treasure 受理成功累计)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("beacon_network") | (待填) | | | -| 913 | 241 | "解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕" | r_n == 5 | (待填) | | | -| 914 | 244 | "摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)" | rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops") and (rs[1] as Dictionary).has("keystone_drops") and int(rs[0].get("master_seed", -1)) == 15 | (待填) | | | -| 915 | 253 | "摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)" | sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11 | (待填) | | | -| 916 | 258 | "白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)" | Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0 | (待填) | | | -| 917 | 260 | "满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕" | mp_a.toggle_attunement("morning_order") == false | (待填) | | | -| 918 | 265 | "Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)" | mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1 | (待填) | | | -| 919 | 270 | "完成 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕" | Attunement.slots(_db, sv_a) == 0 and sv_a.boss_kill_count() == 1 | (待填) | | | -| 920 | 273 | "树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)" | Attunement.slots(_db, sv_a) == 1 | (待填) | | | -| 921 | 275 | "稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕" | not mp_a.toggle_attunement("sky_eye") | (待填) | | | -| 922 | 277 | "装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕" | mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false | (待填) | | | -| 923 | 279 | "卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕" | mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order") | (待填) | | | -| 924 | 284 | "装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕" | (mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order") | (待填) | | | -| 925 | 288 | "充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕" | a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}) | (待填) | | | -| 926 | 290 | "充能按装备 active_charged 件种行(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面;读数=行派生)" | a12._loadout.charges_left("incense_stick", a12.runner().treasure_inventory) == 1 and a12._loadout.charges_left("sky_eye", a12.runner().treasure_inventory) == 1 | (待填) | | | -| 927 | 296 | "刷新双计数器——X 优先消耗(x1y2 → x0y2)" | a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2 | (待填) | | | -| 928 | 298 | "刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)" | a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0 and a12.stop().refresh_curio() == false | (待填) | | | -| 929 | 302 | "刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕" | a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0 | (待填) | | | -| 930 | 313 | "开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)" | rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2 | (待填) | | | -| 931 | 317 | "主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)" | (p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array) | (待填) | | | -| 932 | 319 | "主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)" | int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() | (待填) | | | -| 933 | 321 | "主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)" | (p_new.get("tree_rules", []) as Array) == rules_e | (待填) | | | -| 934 | 323 | "主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入" | bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool" | (待填) | | | -| 935 | 327 | "主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)" | (p_resume.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array) and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() and (p_resume.get("tree_rules", []) as Array) == rules_e | (待填) | | | -| 936 | 338 | "继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)" | adr_res.resume_adventure(bp_min, p_resume) | (待填) | | | -| 937 | 340 | "继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)" | (adr_res._tree_rules as Array) == rules_e | (待填) | | | - -## S116 _meta_injection(行 353–383 · 10 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 938 | 353 | "生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)" | int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)) | (待填) | | | -| 939 | 355 | "攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)" | int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0 | (待填) | | | -| 940 | 357 | "命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)" | not mp.hero_override().has("hit") | (待填) | | | -| 941 | 360 | "注入参数冒险启动(start_adventure params 通道)" | adr.start_adventure(920251, {"hero_override": mp.hero_override()}) | (待填) | | | -| 942 | 364 | "首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)" | int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)) | (待填) | | | -| 943 | 366 | "首战 hero_max_hp 与开局生命账本 = 表值 + 注入档" | int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)) and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)) | (待填) | | | -| 944 | 371 | "白板冒险启动(无 params 实参——B1 空参数路径)" | adr_w.start_adventure(920251) | (待填) | | | -| 945 | 374 | "零强化参数冒险启动(新档 hero_override 全零)" | adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}) | (待填) | | | -| 946 | 378 | "白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)" | int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp) and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp()) and int(rz.effective_ac()) == int(rw.effective_ac()) | (待填) | | | -| 947 | 382 | "ac 轴:白板恒表值、未换强化时注入零位移" | int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class) | (待填) | | | - -## S117 _smoke_forge_axes(行 392–540 · 33 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 948 | 392 | "hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕" | not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero) | (待填) | | | -| 949 | 395 | "锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕" | _db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light" | (待填) | | | -| 950 | 399 | "spawn 受理(三参形态——两轴缺省回退面)" | uc.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 951 | 400 | "主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕" | String(uc.hero_unit().get("attack_type", "")) == "normal" and String(uc.hero_unit().get("armor_type", "")) == "light" | (待填) | | | -| 952 | 410 | "长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1 and sv2.keystone_balance() == 80 | (待填) | | | -| 953 | 414 | "购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕" | sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80 | (待填) | | | -| 954 | 417 | "长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2 and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0 | (待填) | | | -| 955 | 424 | "余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕" | not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0 and sv2d.forge_routes_snapshot() == fr_dry | (待填) | | | -| 956 | 429 | "已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕" | not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear") | (待填) | | | -| 957 | 432 | "通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕" | mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff") and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward") | (待填) | | | -| 958 | 435 | "派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕" | mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1 and sv2.keystone_balance() == 30 | (待填) | | | -| 959 | 442 | "通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕" | not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward") and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward") | (待填) | | | -| 960 | 447 | "直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕" | mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward") and sv3.keystone_balance() == 300 | (待填) | | | -| 961 | 452 | "不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕" | mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff") and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear") | (待填) | | | -| 962 | 457 | "两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕" | mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20 and sv3.forge_route_level("ward") == 1 | (待填) | | | -| 963 | 462 | "选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕" | mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward" and sv3.keystone_balance() == ks6 | (待填) | | | -| 964 | 465 | "切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕" | sv3.forge_route_level("light") == 5 | (待填) | | | -| 965 | 468 | "未解锁路线拒选零副作用〔锻造工坊批 V6〕" | not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword" | (待填) | | | -| 966 | 471 | "类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕" | mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal" and mp2d.hero_armor_type() == "light" | (待填) | | | -| 967 | 480 | "build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕" | String(bp_params.get("attack_type", "")) == "normal" and String(bp_params.get("armor_type", "")) == "light" | (待填) | | | -| 968 | 484 | "长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)" | mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear") | (待填) | | | -| 969 | 487 | "build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕" | String(bp_sel.get("attack_type", "")) == "pierce" and String(bp_sel.get("armor_type", "")) == "light" | (待填) | | | -| 970 | 491 | "hero_override=锻造路线加值(选中长枪 Lv1→atk +1;甲 0 级→AC +0;树零分量;无 hit 键)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 and int(mp4.hero_override().get("max_hp", -1)) == 0 and int(mp4.hero_override().get("armor_class", -1)) == 0 and not mp4.hero_override().has("hit") | (待填) | | | -| 971 | 497 | "换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 | (待填) | | | -| 972 | 499 | "切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕" | mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3 | (待填) | | | -| 973 | 505 | "重甲直购解锁+首购+选中(三层 AC 夹具)" | mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy") | (待填) | | | -| 974 | 509 | "注入参数冒险启动(两轴+override 同面)" | adr5.start_adventure(77, p5) | (待填) | | | -| 975 | 511 | "入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕" | int(r5.effective_ac()) == 12 + 1 | (待填) | | | -| 976 | 513 | "参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕" | adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy" | (待填) | | | -| 977 | 516 | "冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕" | mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy" and int(r5.effective_ac()) == 12 + 1 | (待填) | | | -| 978 | 525 | "enc 两轴直达 spawn 英雄快照(hero_attack 行携带 pierce 键——注入链末端)〔锻造工坊批 V5〕" | pierce_rows >= 1 | (待填) | | | -| 979 | 529 | "spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕" | uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy") and String(uc2.hero_unit().get("attack_type", "")) == "magic" and String(uc2.hero_unit().get("armor_type", "")) == "heavy" | (待填) | | | -| 980 | 539 | "树零攻防来源复核(12 行 effects 键集无 atk/armor_class——契约 1)〔锻造工坊批 V10〕" | not tree_atk_ac | (待填) | | | - -## S118 _smoke_tree(行 548–621 · 25 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 981 | 548 | "meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕" | _db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12 | (待填) | | | -| 982 | 550 | "MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)" | _db.meta_tree.validate().is_empty() | (待填) | | | -| 983 | 558 | "easter_ley effects 键集 ⊆ {max_hp, hit}(攻/护分量已摘除——契约 9)〔天赋树批 V8 落点〕" | ley_ok | (待填) | | | -| 984 | 566 | "schema 反例:vein 出域被拒" | not t2.validate().is_empty() | (待填) | | | -| 985 | 571 | "schema 反例:fork_group 出域(fg_mastery 已退役组)被拒" | not t2.validate().is_empty() | (待填) | | | -| 986 | 576 | "schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕" | not t2.validate().is_empty() | (待填) | | | -| 987 | 581 | "schema 反例:requires 悬空被拒" | not t2.validate().is_empty() | (待填) | | | -| 988 | 586 | "树购买受理(脊柱生命 L1——树全钥石单币)" | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 | (待填) | | | -| 989 | 588 | "行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕" | not mp.tree_can_buy("sp_hp") | (待填) | | | -| 990 | 590 | "命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)" | mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1") | (待填) | | | -| 991 | 592 | "锻造工坊钥匙零币(free)" | int(_db.tree_node("forge_workshop").get("price", -1)) == 0 | (待填) | | | -| 992 | 593 | "行门槛恰达开放(总等级 4 ≥ 4×1)" | mp.tree_can_buy("sp_hp") | (待填) | | | -| 993 | 594 | "第 2 级购买受理(门槛恰达当刻开放)" | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2 | (待填) | | | -| 994 | 598 | "Fork 购买=解锁+默认 A 态(一节点双态——单行承载)" | mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a" | (待填) | | | -| 995 | 600 | "已解锁 Fork 重复购买拒绝(cap 1)" | not mp.tree_can_buy("fork_posture") | (待填) | | | -| 996 | 601 | "同态重复切换零操作(false)" | not mp.tree_toggle_fork("fg_posture", "a") | (待填) | | | -| 997 | 602 | "未解锁 Fork 切换拒绝(解锁唯一入口=购买面)" | not mp.tree_toggle_fork("fg_sense", "b") | (待填) | | | -| 998 | 604 | "零币切态 b(购买解锁后 A/B 可切)" | mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b" | (待填) | | | -| 999 | 606 | "切态零消费(钥石余额差分不动)" | int(sv.keystone_balance()) == bal_pre_toggle | (待填) | | | -| 1000 | 607 | "规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)" | (mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | -| 1001 | 609 | "切回 A 态键集随换(六键词表在册——V11 校验面归单元段)" | mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | -| 1002 | 612 | "彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)" | not mp.tree_can_buy("easter_ley") | (待填) | | | -| 1003 | 613 | "觉醒前置未满足不亮(两线未拉满)" | sv.tree_level("easter_ley") == 0 | (待填) | | | -| 1004 | 618 | "两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)" | sv.tree_level("easter_ley") == 1 | (待填) | | | -| 1005 | 621 | "觉醒零消费(钥石余额不动)" | int(sv.keystone_balance()) == bal_now | (待填) | | | - -## S119 _smoke_tree_rules(行 633–758 · 21 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1006 | 633 | "先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕" | (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1 | (待填) | | | -| 1007 | 635 | "先手压制:首轮窗外地不触发(第二轮/第二关首战首轮——「每战首轮」旧语义换代件)" | (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty() and (fx.dispatch(ctx_p, "round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty() | (待填) | | | -| 1008 | 638 | "稳健拓荒:首轮窗内受击出盾、窗外不出(shield damage_taken 同窗条件)" | (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1 and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty() | (待填) | | | -| 1009 | 654 | "锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)" | got_mult and got_add | (待填) | | | -| 1010 | 657 | "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕" | (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | -| 1011 | 663 | "锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕" | (fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | -| 1012 | 685 | "锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕" | nc_hit != "" | (待填) | | | -| 1013 | 687 | "惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕" | nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷") | (待填) | | | -| 1014 | 691 | "守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)" | orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["sense_guard"], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) | (待填) | | | -| 1015 | 703 | "守侧 rig:安全格定位(非危险格=标错负样本)" | safe.x >= 0 | (待填) | | | -| 1016 | 704 | "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)" | int(orch.action_economy.pending_ap_delta()) == 0 | (待填) | | | -| 1017 | 708 | "守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | -| 1018 | 713 | "守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | -| 1019 | 717 | "守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | -| 1020 | 721 | "守⑤ rig:次战编排启动(新战新队列实例)" | orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["sense_guard"], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) | (待填) | | | -| 1021 | 727 | "守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)" | int(orch2.action_economy.pending_ap_delta()) == 0 | (待填) | | | -| 1022 | 733 | "散未激活:商品标价原价(无 economy_spend 键)" | int(lr._spend_discounted(100)) == 100 | (待填) | | | -| 1023 | 736 | "散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)" | int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1 | (待填) | | | -| 1024 | 740 | "聚 rig:基线编排启动(无增益侧)" | orch3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": [], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) | (待填) | | | -| 1025 | 749 | "聚 rig:增益编排启动(tree_rules=economy_hoard)" | orch4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) | (待填) | | | -| 1026 | 757 | "聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)" | gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base | (待填) | | | - -## S120 _init(行 22–30 · 2 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1027 | 22 | "数值表加载" | false | (待填) | | | -| 1028 | 30 | "生成返回棋盘(前置)" | false | (待填) | | | - -## S121 _test_install_invariants(行 48–56 · 4 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1029 | 48 | "装板后 current_board 即所装棋盘" | _rules.current_board() == _board | (待填) | | | -| 1030 | 52 | "装板带初始扩张揭示(%d 格,非零且未铺满安全格 %d)" ↩ % [opened, safe_total] | opened > 0 and opened < safe_total | (待填) | | | -| 1031 | 54 | "安全格余量 = 安全格总数 − 已揭示数(%d − %d)" ↩ % [safe_total, opened] | _rules.safe_remaining() == safe_total - opened | (待填) | | | -| 1032 | 56 | "装板后仍有未揭示格" | _rules.has_unrevealed() | (待填) | | | - -## S122 _test_dig_face_rules(行 73–80 · 2 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1033 | 73 | "挖已揭示格被拒(board_face=revealed,禁重复挖)——载荷 %s" ↩ % str(o.get("sub_reason", "")) | String(o.get("outcome", "")) == "rejected" and String(o.get("sub_reason", "")) == "target_revealed" | (待填) | | | -| 1034 | 79 | "挖非相邻格被拒(邻接规则)——载荷 %s" ↩ % str(o2.get("sub_reason", "")) | String(o2.get("outcome", "")) == "rejected" | (待填) | | | - -## S123 _test_reveal_advances(行 86–93 · 4 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1035 | 86 | "存在未揭示安全格(揭示推进前置)" | false | (待填) | | | -| 1036 | 90 | "专长揭示受理(fx_reveal 不受邻接约束)" | ok | (待填) | | | -| 1037 | 91 | "fx_reveal 后该格已揭示" | _rules.is_revealed(c) | (待填) | | | -| 1038 | 92 | "fx_reveal 后安全格余量递减(%d → %d)" ↩ % [before, _rules.safe_remaining()] | _rules.safe_remaining() < before | (待填) | | | - -## S124 _test_seal_place(行 99–105 · 3 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1039 | 99 | "存在未揭示安全格(封印前置)" | false | (待填) | | | -| 1040 | 104 | "封印后该格在封印态" | _rules.has_seal(c) | (待填) | | | -| 1041 | 105 | "封印数计数 = 1" | _rules.seal_count() == 1 | (待填) | | | - -## S125 _test_reject_out_of_catalog(行 111–112 · 1 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1042 | 111 | "目录外意图被拒(不静默吞掉)——实际载荷 %s" ↩ % str(o).substr(0, 80) | out.size() == 1 and String(o.get("outcome", o.get("result", ""))) == "rejected" | (待填) | | | - -## S126 _test_output_ok(行 88–89 · 2 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1043 | 88 | "三档×20 种子产出图过产出约束五条+结构铁律(自证零错误)〔fv1〕" | all_ok | (待填) | | | -| 1044 | 89 | "入口数/终点数 = 参数档 entry_count/exit_count〔fv1〕" | entry_exit_ok | (待填) | | | - -## S127 _test_reproducible(行 99–119 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1045 | 99 | "同(种子,参数档)两次产图逐位同图〔fv3〕" | same | (待填) | | | -| 1046 | 109 | "counter 无关(不同号位态同种子同图)〔fv3〕" | _sig(gen_a.generate(_params(2), 42)) == _sig(gen_b.generate(_params(2), 42)) and _sig(gen_a.generate(_params(3), 7)) == _sig(gen_b.generate(_params(3), 7)) | (待填) | | | -| 1047 | 119 | "同参数档不同种子产出异图(10 种子去重数 ≥ 8)〔fv3〕" | distinct >= 8 | (待填) | | | - -## S128 _test_fork_rules(行 163–164 · 2 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1048 | 163 | "任意岔口各去路下一节点类型互不相同〔fv5〕" | next_hop_ok | (待填) | | | -| 1049 | 164 | "任意岔口各支路类型构成互异且各有独有类型〔fv5〕" | comp_ok | (待填) | | | - -## S129 _test_shop_and_attribution(行 228–230 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1050 | 228 | "任意入口→终点路径恰过一家店〔fv6〕" | paths_ok | (待填) | | | -| 1051 | 229 | "每终点前一位恰一店+商店总数=终点数(归属锁定)〔fv6〕" | lock_ok | (待填) | | | -| 1052 | 230 | "多终点落在互异节点深度(L2/L3)〔fv6〕" | depth_ok | (待填) | | | - -## S130 _test_battle_floor(行 257–257 · 1 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1053 | 257 | "任意合法产出战斗节点数 ≥ battle_floor〔fv7〕" | ok | (待填) | | | - -## S131 _test_exit_structure(行 285–287 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1054 | 285 | "三关参数档生成图各含 ≥1 岔口节点(第一关即含)〔fv9〕" | fork_all | (待填) | | | -| 1055 | 286 | "L1 生成图终点恰 1(出口恒 1)〔fv9〕" | l1_single | (待填) | | | -| 1056 | 287 | "L2/L3 含分区域大岔口(一节点可达 ≥2 终点且去路数=出口数)〔fv9〕" | big_fork_ok | (待填) | | | - -## S132 _test_rng_single_point(行 313–318 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1057 | 313 | "pure_stream 不推全局计数器(产图前后 counter 同值)〔fv10〕" | svc.counter() == before | (待填) | | | -| 1058 | 316 | "pure_stream 同 (seed,scope) 同流〔fv10〕" | a.randf() == b.randf() and a.randi() == b.randi() | (待填) | | | -| 1059 | 317 | "pure_stream 不推计数器且产图非空〔fv10〕" | svc.counter() == before and (g["nodes"] as Array).size() > 0 | (待填) | | | - -## S133 _test_negative_fixtures(行 328–412 · 13 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1060 | 328 | "句柄重复夹具报 dup_key〔fv1〕" | _has_cat(_gen.validate_output(dup), "dup_key") | (待填) | | | -| 1061 | 332 | "悬空出边夹具报 dangling_next〔fv1〕" | _has_cat(_gen.validate_output(dang), "dangling_next") | (待填) | | | -| 1062 | 336 | "类型出域夹具报 enum〔fv1〕" | _has_cat(_gen.validate_output(badty), "enum") | (待填) | | | -| 1063 | 343 | "战斗节点低于 battle_floor 的合法形状夹具命中「低于保底」判据〔fv7〕" | _has_msg(_gen.validate_output(thin_battle, {"battle_floor": 4}), "低于保底") | (待填) | | | -| 1064 | 345 | "同夹具在保底 1 下自证零错误(判据不误伤)〔fv7〕" | _gen.validate_output(thin_battle, {"battle_floor": 1}).is_empty() | (待填) | | | -| 1065 | 350 | "非 Boss 终点夹具命中「终点(无出边)非 Boss」判据〔fv1〕" | _has_msg(_gen.validate_output(noBoss), "终点(无出边)非 Boss") | (待填) | | | -| 1066 | 361 | "合法合流图(第二入口直插主链,深度取最长路 3)自证零错误〔fv1〕" | _gen.validate_output(conf_legal).is_empty() | (待填) | | | -| 1067 | 370 | "声明深度高于最长路径步数被拒(深度=自入口最长路径步数)〔fv1〕" | _has_msg(_gen.validate_output(conf_high), "自入口最长路径步数") | (待填) | | | -| 1068 | 377 | "回边夹具报前进方向违例(structural)〔fv1〕" | _has_cat(_gen.validate_output(loop), "structural") | (待填) | | | -| 1069 | 387 | "多终点同深度夹具报 structural〔fv6〕" | _has_cat(_gen.validate_output(twin), "structural") | (待填) | | | -| 1070 | 397 | "双终点异深+区域类型互异合法夹具自证零错误〔fv6〕" | _gen.validate_output(twin_ok).is_empty() | (待填) | | | -| 1071 | 404 | "终点前驱非店夹具报 structural(必经恰一店/商店数)〔fv6〕" | _has_cat(cats, "structural") | (待填) | | | -| 1072 | 412 | "岔口同类型下一跳夹具报 structural〔fv5〕" | _has_cat(_gen.validate_output(sameHop), "structural") | (待填) | | | - -## S134 _test_growth(行 57–58 · 2 条 · 源 game/tests/unit/core/node_map_growth_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1073 | 57 | "生长递增:三档最长路径步数逐关变大〔fv8〕" | steps_ok | (待填) | | | -| 1074 | 58 | "生长递增:节点总数逐关变大且增幅变大〔fv8〕" | count_ok | (待填) | | | - -## S135 _test_fail_fast_static(行 64–74 · 3 条 · 源 game/tests/unit/core/node_map_growth_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1075 | 64 | "生产三档参数档可行性判定零错误〔fv4〕" | NodeMapGenerator.check_params(db.level_gen_params_row(1)).is_empty() and NodeMapGenerator.check_params(db.level_gen_params_row(3)).is_empty() | (待填) | | | -| 1076 | 70 | "层深 6 放三出口无解 → check_params 拦截〔fv4〕" | not NodeMapGenerator.check_params(unsolvable).is_empty() | (待填) | | | -| 1077 | 74 | "战斗保底超可放数无解 → check_params 拦截〔fv4〕" | not NodeMapGenerator.check_params(overfloor).is_empty() | (待填) | | | - -## S136 _test_fatal_subprocess(行 82–91 · 2 条 · 源 game/tests/unit/core/node_map_growth_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1078 | 82 | "未找到引擎二进制,致命子进程通道未跑(如实标注)〔fv4〕" | false | (待填) | | | -| 1079 | 91 | "无解参数档置致命封印并由组合根停机(退出码恰 1,无残图交付)〔fv4〕" | code == 1 | (待填) | | | - -## S137 _smoke_save(行 81–329 · 60 条 · 源 game/tests/unit/core/save_manager_domain_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1080 | 81 | "apply 原语入账(钥石 7+3、关卡完成次数 1/1/0)" | w.keystone_balance() == 10 and w.clear_count(1) == 1 and w.clear_count(2) == 1 and w.clear_count(3) == 0 | (待填) | | | -| 1081 | 83 | "首杀标记随关卡完成置位(1/2 有、3 无)" | w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3) | (待填) | | | -| 1082 | 85 | "生涯终身计数随 apply 自增恰一(中止局:总局数 1、冒险成功局数 0——迭代 18 卡④)" | w.adventure_total() == 1 and w.adventure_cleared() == 0 | (待填) | | | -| 1083 | 87 | "写盘成功" | w.write_to_disk() | (待填) | | | -| 1084 | 89 | "读回成功" | r.load_from_disk() | (待填) | | | -| 1085 | 90 | "钥石余额往返相等(10)" | r.keystone_balance() == 10 | (待填) | | | -| 1086 | 91 | "树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)" | r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b" | (待填) | | | -| 1087 | 93 | "锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕" | r.forge_route_level("spear") == 3 and r.forge_route_level("sword") == 0 and r.forge_unlocked_purchases() == ["spear"] and r.forge_weapon() == "spear" and r.forge_armor() == "ward" | (待填) | | | -| 1088 | 97 | "道具解锁往返相等" | r.item_unlocked("probe_compass") and not r.item_unlocked("rope") | (待填) | | | -| 1089 | 98 | "关卡完成次数表往返相等" | r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0 | (待填) | | | -| 1090 | 99 | "首杀标记往返相等" | r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3) | (待填) | | | -| 1091 | 101 | "生涯终身计数往返相等(1/0)" | r.adventure_total() == 1 and r.adventure_cleared() == 0 | (待填) | | | -| 1092 | 103 | "冒险摘要往返相等(1 条,seed 99)" | r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3 | (待填) | | | -| 1093 | 108 | "新档合法态(load true + 全零默认)" | f2.load_from_disk() and f2.keystone_balance() == 0 and f2.tree_level("sp_hp") == 0 and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty() | (待填) | | | -| 1094 | 111 | "新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)" | f2.adventure_total() == 0 and f2.adventure_cleared() == 0 | (待填) | | | -| 1095 | 117 | "余额 5 花 8 拒绝且余额不动" | not f3.spend_keystone(8) and f3.keystone_balance() == 5 | (待填) | | | -| 1096 | 118 | "余额内消费成功(5→2)" | f3.spend_keystone(3) and f3.keystone_balance() == 2 | (待填) | | | -| 1097 | 126 | "高版本拒读(load false)" | not f5.load_from_disk() | (待填) | | | -| 1098 | 127 | "拒读后原文件字节不动" | FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text | (待填) | | | -| 1099 | 128 | "拒读态写盘拒绝且文件仍字节不动" | not f5.write_to_disk() and FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text | (待填) | | | -| 1100 | 137 | "损坏档兜底(默认数据继续)" | f6.load_from_disk() and f6.keystone_balance() == 0 and f6.recent_summaries().is_empty() | (待填) | | | -| 1101 | 139 | "坏档备份 .corrupt.bak 存在" | FileAccess.file_exists(paths[4] + ".corrupt.bak") | (待填) | | | -| 1102 | 140 | "备份内容 = 原坏档字节" | FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk | (待填) | | | -| 1103 | 151 | "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)" | f7.load_from_disk() and f7.keystone_balance() == 12 | (待填) | | | -| 1104 | 153 | "迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)" | f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7 | (待填) | | | -| 1105 | 165 | "域界干净——meta 域无局内资源键(发现 %s)" ↩ % str(leaked) | leaked.is_empty() | (待填) | | | -| 1106 | 173 | "摘要 cap 10(12 份 → 10 条)" | arr.size() == 10 | (待填) | | | -| 1107 | 174 | "最新在前(头 seed 11 / 尾 seed 2)" | int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2 | (待填) | | | -| 1108 | 182 | "断点:新档无断点(合法空态——「继续上局」隐藏)" | not fbp.has_breakpoint() | (待填) | | | -| 1109 | 193 | "断点:写入+写盘成功" | fbp.has_breakpoint() and fbp.write_to_disk() | (待填) | | | -| 1110 | 195 | "断点:读回在档" | rb.load_from_disk() and rb.has_breakpoint() | (待填) | | | -| 1111 | 197 | "断点七字段往返:关卡序号/节点位/生命" | int(bp.get("level_index", -1)) == 2 and int(bp.get("node_id", -1)) == 5 and int(bp.get("hero_hp", -1)) == 13 | (待填) | | | -| 1112 | 201 | "断点七字段往返:属性加成区" | int(bp_bonus.get("atk", -1)) == 2 and int(bp_bonus.get("max_hp", -1)) == 5 and int(bp_bonus.get("armor_class", -1)) == 1 | (待填) | | | -| 1113 | 204 | "断点七字段往返:GP 钱包" | int(bp.get("gp", -1)) == 17 | (待填) | | | -| 1114 | 206 | "断点七字段往返:商店已购档" | int(bp_shop.get("hp_small", -1)) == 2 and int(bp_shop.get("atk_small", -1)) == 1 | (待填) | | | -| 1115 | 208 | "断点七字段往返:生成图代差标记键" | int(bp.get("graph_gen_v2", 0)) == 1 | (待填) | | | -| 1116 | 209 | "断点附随件往返:主种子/计数器/钥石累计" | int(bp.get("master_seed", -1)) == 424242 and int(bp.get("rng_counter", -1)) == 38 and int(bp.get("keystone", -1)) == 6 | (待填) | | | -| 1117 | 213 | "断点附随件往返:已到节点序" | bp_visited.size() == 3 and int(bp_visited[0]) == 0 and int(bp_visited[2]) == 5 | (待填) | | | -| 1118 | 216 | "断点附随件往返:道具库存" | bp_treasure.size() == 1 and String(bp_treasure[0].get("item_id", "")) == "aux_scout" | (待填) | | | -| 1119 | 219 | "断点读面值拷贝(改读面不渗档)" | int((rb.breakpoint_snapshot() as Dictionary).get("hero_hp", -1)) == 13 | (待填) | | | -| 1120 | 222 | "断点:清除+写盘(三结局收束口径)" | not rb.has_breakpoint() and rb.write_to_disk() | (待填) | | | -| 1121 | 224 | "断点:清除后读回无断点" | rc.load_from_disk() and not rc.has_breakpoint() | (待填) | | | -| 1122 | 229 | "v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)" | fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint() | (待填) | | | -| 1123 | 231 | "v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)" | fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7 | (待填) | | | -| 1124 | 243 | "v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)" | fv2.load_from_disk() and fv2.has_breakpoint() and fv2.keystone_balance() == 7 | (待填) | | | -| 1125 | 245 | "v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)" | int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777 | (待填) | | | -| 1126 | 248 | "v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)" | adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}) | (待填) | | | -| 1127 | 257 | "v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)" | f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0 and f13.keystone_balance() == 9 | (待填) | | | -| 1128 | 260 | "升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)" | f13.write_to_disk() and f13.load_from_disk() and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 7 and f13.adventure_total() == 0 and f13.adventure_cleared() == 0 | (待填) | | | -| 1129 | 268 | "收束自增恰一·冒险成功局(三关全通 cleared=true:总局数+1 且冒险成功局数+1)" | f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1 | (待填) | | | -| 1130 | 272 | "收束自增恰一·中止局(放弃局:仅总局数+1,冒险成功局数不动)" | f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1 | (待填) | | | -| 1131 | 283 | "换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动" | f15.load_from_disk() and f15.keystone_balance() == 33 and f15.tree_nodes().is_empty() and f15.fork_sides().is_empty() | (待填) | | | -| 1132 | 288 | "废弃重置后后续购买正常受理(无残留脏节点)" | mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1 | (待填) | | | -| 1133 | 297 | "v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕" | f16.load_from_disk() and f16.keystone_balance() == 21 and f16.tree_level("sp_hp") == 1 and f16.adventure_total() == 4 and f16.adventure_cleared() == 1 | (待填) | | | -| 1134 | 300 | "v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕" | f16.forge_route_level("sword") == 0 and f16.forge_route_level("spear") == 0 and f16.forge_route_level("light") == 0 and f16.forge_route_level("ward") == 0 and f16.forge_unlocked_purchases().is_empty() and f16.forge_weapon() == "sword" and f16.forge_armor() == "light" | (待填) | | | -| 1135 | 307 | "forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕" | fr_keys == ["armor", "levels", "unlocked", "weapon"] | (待填) | | | -| 1136 | 311 | "levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕" | lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"] | (待填) | | | -| 1137 | 316 | "forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕" | f16.forge_route_level("sword") == 0 | (待填) | | | -| 1138 | 324 | "收束链自增·中止局(settle_adventure 真实挂点:总局数+1、冒险成功局数不动)" | f13b.adventure_total() == 3 and f13b.adventure_cleared() == 1 | (待填) | | | -| 1139 | 328 | "收束链自增·冒险成功局(三关全通:总局数+1 且冒险成功局数+1——全链恰一)" | f13b.adventure_total() == 4 and f13b.adventure_cleared() == 2 | (待填) | | | - -## S138 _smoke_save_slots(行 359–499 · 31 条 · 源 game/tests/unit/core/save_manager_domain_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1140 | 359 | "sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" ↩ % i | t.activate_slot(i) | (待填) | | | -| 1141 | 362 | "sv3 激活态写盘受理·槽 %d〔卡① sv1〕" ↩ % i | t.write_to_disk() | (待填) | | | -| 1142 | 366 | "sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" ↩ % i | t2.activate_slot(i) and t2.keystone_balance() == 100 * i and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0) | (待填) | | | -| 1143 | 372 | "sv3 写盘受理·槽 1 加值后〔卡① sv1〕" | t.write_to_disk() | (待填) | | | -| 1144 | 373 | "sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_before | (待填) | | | -| 1145 | 377 | "sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕" | t.activate_slot(3) | (待填) | | | -| 1146 | 378 | "sv6 删档受理·激活槽 3〔卡① sv6〕" | t.delete_slot(3) | (待填) | | | -| 1147 | 379 | "sv6 删后文件不在盘+无 .tmp 残留〔卡① sv6〕" | not FileAccess.file_exists(slot_tpl % 3) and not FileAccess.file_exists(slot_tpl % 3 + ".tmp") | (待填) | | | -| 1148 | 381 | "sv6 删激活槽激活指针连清除(该槽不再可写)〔卡① sv6〕" | t.active_slot() == 0 and not t.has_write_target() | (待填) | | | -| 1149 | 385 | "sv6 删非激活槽受理·槽 2〔卡① sv6〕" | t.delete_slot(2) | (待填) | | | -| 1150 | 386 | "sv6 删非激活槽:文件不在盘+激活指针保持〔卡① sv6〕" | not FileAccess.file_exists(slot_tpl % 2) and t.active_slot() == 1 | (待填) | | | -| 1151 | 388 | "sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕" | FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before | (待填) | | | -| 1152 | 397 | "sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕" | t.create_slot(3) | (待填) | | | -| 1153 | 398 | "sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕" | t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint() and t.adventure_total() == 0 | (待填) | | | -| 1154 | 402 | "sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕" | int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0 | (待填) | | | -| 1155 | 411 | "sv5 进档槽 1 读回(107)〔卡① sv10〕" | hv.activate_slot(1) and hv.keystone_balance() == 107 | (待填) | | | -| 1156 | 414 | "sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕" | not hv.activate_slot(2) | (待填) | | | -| 1157 | 415 | "sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕" | hv.active_slot() == 1 and hv.keystone_balance() == 107 | (待填) | | | -| 1158 | 418 | "sv10 恢复后写盘落激活槽 1(高版本槽 2 零误写)〔卡① sv10〕" | hv.write_to_disk() and FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_hv | (待填) | | | -| 1159 | 429 | "sv9 迁移链按槽成立·槽 %d(v5→v7 各槽独立+写盘 version=7)〔卡① sv9〕" ↩ % i | mi.activate_slot(i) and mi.keystone_balance() == i * 11 and mi.write_to_disk() | (待填) | | | -| 1160 | 444 | "sv7 窥视三项逐槽正确·槽 %d〔卡① sv7〕" ↩ % i | bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i | (待填) | | | -| 1161 | 447 | "sv6 删槽 3 备空槽窥视〔卡① sv6〕" | pv.delete_slot(3) | (待填) | | | -| 1162 | 449 | "sv7 空槽窥视=零值合法态〔卡① sv7〕" | bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0 and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0 | (待填) | | | -| 1163 | 458 | "sv8 坏槽窥视=显式损坏标记〔卡① sv8〕" | not bool(pb.get("readable", true)) | (待填) | | | -| 1164 | 459 | "sv8 坏槽窥视零副作用(字节+mtime 双比对不变+零 .corrupt.bak 改名)〔卡① sv8〕" | FileAccess.get_file_as_bytes(slot_tpl % 1) == bad_bytes and FileAccess.get_modified_time(slot_tpl % 1) == bad_mtime and not FileAccess.file_exists(slot_tpl % 1 + ".corrupt.bak") | (待填) | | | -| 1165 | 469 | "sv8 高版本槽窥视=不可读标记〔卡① sv8〕" | not bool(ph.get("readable", true)) | (待填) | | | -| 1166 | 470 | "sv8 高版本窥视零副作用(字节+mtime 不变+零改名)〔卡① sv8〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == hv_bytes and FileAccess.get_modified_time(slot_tpl % 2) == hv_mtime | (待填) | | | -| 1167 | 483 | "sv12 旧档字节迁入槽 1(逐域相等——余额 1234)〔卡① sv12〕" | int((migrated.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234 | (待填) | | | -| 1168 | 485 | "sv12 迁移后源改名留痕(.migrated.bak 在盘、原路径不在)〔卡① sv12〕" | not FileAccess.file_exists(legacy_src) and FileAccess.file_exists(legacy_src + ".migrated.bak") | (待填) | | | -| 1169 | 494 | "sv12 幂等:槽 1 已存在不重迁(预置 777 零覆盖、源保留)〔卡① sv12〕" | int((slot1_now.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234 and FileAccess.file_exists(legacy_src) | (待填) | | | -| 1170 | 497 | "sv12 迁移他槽零触碰(槽 2/3 字节不动)〔卡① sv12〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == s2_before and FileAccess.get_file_as_bytes(slot_tpl % 3) == s3_before | (待填) | | | - -## S139 _init(行 14–15 · 2 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1171 | 14 | "全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | -| 1172 | 15 | "is_ready 通道随加载置位" | db.is_ready | (待填) | | | - -## S140 _test_progress_resolution(行 55–83 · 7 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1173 | 55 | "进度解析精确命中返回原行(board_gen 首行)" | int(got.get("level_index", -99)) == int(probe_row.level_index) and int(got.get("node_depth", -99)) == int(probe_row.node_depth) and String(got.get("board_type", "")) == String(probe_row.get("board_type", "")) | (待填) | | | -| 1174 | 63 | "enemy 行集同坐标解析非空(双表精确行契约——battle 行类)" | not en_got.is_empty() | (待填) | | | -| 1175 | 68 | "深度越最大行 clamp 取该关最大深度行" | int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]) | (待填) | | | -| 1176 | 70 | "clamp 解析发警告诊断(resolve_depth_clamp)" | db.diagnostics_count("warning") >= 1 | (待填) | | | -| 1177 | 75 | "警告类别 = resolve_depth_clamp" | found_clamp | (待填) | | | -| 1178 | 78 | "同键重复请求警告去重(四元组注册表)" | db.diagnostics_count("warning") == warn_before | (待填) | | | -| 1179 | 83 | "关卡越最大行 clamp 取最大关卡行" | int(high.get("level_index", -99)) == max_l | (待填) | | | - -## S141 _test_variant_pool_gate(行 108–118 · 6 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1180 | 108 | "变异池 unlock 前不含未解锁行(level=unlock−1)" | not _rows_contain(before, gated_row) | (待填) | | | -| 1181 | 109 | "变异池 unlock 当级含该行" | _rows_contain(after, gated_row) | (待填) | | | -| 1182 | 113 | "∅ board_types 行通配(任一 board_type 命中)" | _rows_contain(any_pool, wildcard_row) | (待填) | | | -| 1183 | 116 | "global 行 boss 节点入池(board_types ∅ 通配)" | _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row) | (待填) | | | -| 1184 | 117 | "global 行缺省双参调用跳过(既有调用口径零扰动)" | not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag"), global_row) | (待填) | | | -| 1185 | 118 | "global 行 battle 节点静默跳过(门控兜底零报错)" | not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row) | (待填) | | | - -## S142 _test_shop_accessors(行 133–152 · 8 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1186 | 133 | "ShopTable products 行族非空(表已已入库前提)" | false | (待填) | | | -| 1187 | 135 | "ShopTable products 行族非空" | true | (待填) | | | -| 1188 | 138 | "shop_product_row 按 product_id 精确命中首行" | got == first | (待填) | | | -| 1189 | 139 | "shop_product_row 未知 id 返回空字典(容错通道)" | db.shop_product_row("no_such_product_id__").is_empty() | (待填) | | | -| 1190 | 143 | "shop_gp_drop_row 按 tier 命中" | not db.shop_gp_drop_row(tier0).is_empty() | (待填) | | | -| 1191 | 147 | "shop_level_gp_bonus 按关卡序号命中行值" | db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus) | (待填) | | | -| 1192 | 148 | "shop_level_gp_bonus 未登记关卡返回 0(兜底通道)" | db.shop_level_gp_bonus(-1) == 0 | (待填) | | | -| 1193 | 152 | "shop_price_level_bonus 按关卡序号命中行值" | db.shop_price_level_bonus(plv) == int(price_rows[0].bonus) | (待填) | | | - -## S143 _test_meta_accessors(行 162–166 · 2 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1194 | 162 | "meta_boss_keystone_row 按关卡序号命中" | db.meta_boss_keystone_row(klv) == key_rows[0] | (待填) | | | -| 1195 | 166 | "meta_conversion_row 按 attr 命中" | db.meta_conversion_row(cattr) == conv_rows[0] | (待填) | | | - -## S144 _test_manifest_dir_sync(行 188–188 · 1 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1196 | 188 | "TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)" | synced | (待填) | | | - -## S145 _test_rowset_discipline(行 208–273 · 8 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1197 | 208 | "混编行集加载通过(同带键两行不同敌种相邻——行集恰一敌种校验退役的正向面)" | errs.is_empty() | (待填) | | | -| 1198 | 214 | "Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)" | errs.is_empty() | (待填) | | | -| 1199 | 225 | "行序相邻校验:同带键行被隔断报 rowset_rule 且定位带键〔fv9〕〔fv20〕" | gap_hit | (待填) | | | -| 1200 | 235 | "同带键同敌种二行被 dup_key 新键拦(纪律①——enemy_id 唯一)〔fv8〕" | dup_hit | (待填) | | | -| 1201 | 246 | "单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)" | dom_hit | (待填) | | | -| 1202 | 256 | "非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕" | kind_hit | (待填) | | | -| 1203 | 265 | "缺 (battle, band 0) 锚行报 anchor〔fv10〕" | no_anchor_hit | (待填) | | | -| 1204 | 273 | "缺 Boss 行集报 anchor(每关 Boss 行集 ≥1)〔fv10〕" | no_boss_hit | (待填) | | | - -## S146 _init(行 17–17 · 1 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1205 | 17 | "生产 forge.tres 随全表加载 + validate 零违例〔锻造工坊批 V1〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | - -## S147 _test_production_rows(行 30–81 · 13 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1206 | 30 | "六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕" | rows.size() == 6 | (待填) | | | -| 1207 | 35 | "路线 id 全集 = {sword, spear, staff, light, heavy, ward}〔锻造工坊批 V1〕" | ids == ["heavy", "light", "spear", "staff", "sword", "ward"] | (待填) | | | -| 1208 | 46 | "六行字段键集恰 = ROW_KEYS 闭集(无特效参数位)〔锻造工坊批 V11〕" | keys_closed | (待填) | | | -| 1209 | 53 | "初始持有恰 sword+light〔锻造工坊批 V1〕" | initial == ["light", "sword"] | (待填) | | | -| 1210 | 55 | "默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕" | db.forge_default_attack_type() == "normal" | (待填) | | | -| 1211 | 56 | "默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕" | db.forge_default_armor_type() == "light" | (待填) | | | -| 1212 | 58 | "长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕" | String(db.forge_route("spear").get("prereq_route", "")) == "sword" and String(db.forge_route("staff").get("prereq_route", "")) == "sword" | (待填) | | | -| 1213 | 61 | "重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕" | String(db.forge_route("heavy").get("prereq_route", "")) == "light" and String(db.forge_route("ward").get("prereq_route", "")) == "light" | (待填) | | | -| 1214 | 64 | "四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕" | int(db.forge_route("spear").get("unlock_price", 0)) == 200 and int(db.forge_route("staff").get("unlock_price", 0)) == 200 and int(db.forge_route("heavy").get("unlock_price", 0)) == 200 and int(db.forge_route("ward").get("unlock_price", 0)) == 200 | (待填) | | | -| 1215 | 70 | "甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕" | int(db.forge_route("light").get("base_ac", 0)) == 10 and int(db.forge_route("heavy").get("base_ac", 0)) == 12 and int(db.forge_route("ward").get("base_ac", 0)) == 9 | (待填) | | | -| 1216 | 75 | "forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕" | String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward" | (待填) | | | -| 1217 | 77 | "forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕" | String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear" | (待填) | | | -| 1218 | 80 | "未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕" | db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty() | (待填) | | | - -## S148 _test_price_ladder(行 91–94 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1219 | 91 | "长剑各级价格 = 20/30/45/67/101(基础 20×150%^已购级数)〔锻造工坊批 V2〕" | prices == [20, 30, 45, 67, 101] | (待填) | | | -| 1220 | 93 | "已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕" | ForgeTableScript.level_price(row, 5) == 151 | (待填) | | | - -## S149 _test_negative_cases(行 133–189 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1221 | 133 | "负向夹具(合法六行)validate 零违例〔锻造工坊批 V1〕" | t_ok.validate().is_empty() | (待填) | | | -| 1222 | 189 | "锻造 schema 负向:%s〔锻造工坊批 V1〕" ↩ % label | _has_cat(errs, expect_cat) | (待填) | | | - -## S150 _test_effect_kinds_closed(行 197–200 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1223 | 197 | "效果引擎效果类型闭集 32 种(锻造工坊批零新增口径随迭代 31 卡② 扩签 forgive_seal_miss 换代)〔锻造工坊批 V11〕" | EffectEngineScript.EFFECT_KINDS.size() == 32 | (待填) | | | -| 1224 | 199 | "效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕" | EffectEngineScript.EVENTS.size() == 20 | (待填) | | | - -## S151 _init(行 16–17 · 1 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1225 | 16 | "生产 level_gen_params.tres 随全表加载 + validate 零违例〔fv2〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | - -## S152 _test_production(行 47–55 · 4 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1226 | 47 | "访问器读回 L1 行五旋钮齐备〔fv2〕" | int(r1.get("layers", 0)) == 6 and is_equal_approx(float(r1.get("branch_density", 0.0)), 0.3) and int(r1.get("exit_count", 0)) == 1 and int(r1.get("entry_count", 0)) == 1 | (待填) | | | -| 1227 | 50 | "访问器读回 L2/L3 行层深 9/13 递增〔fv2〕" | int(r2.get("layers", 0)) == 9 and int(r3.get("layers", 0)) == 13 | (待填) | | | -| 1228 | 52 | "三行出口 = 1/2/3、战斗保底 = 4/6/8 与 §8.1 表对上〔fv2〕" | int(r1.get("battle_floor", 0)) == 4 and int(r2.get("exit_count", 0)) == 2 and int(r3.get("exit_count", 0)) == 3 and int(r3.get("battle_floor", 0)) == 8 | (待填) | | | -| 1229 | 55 | "生产表 validate 零错误〔fv2〕" | db.level_gen_params.validate().is_empty() | (待填) | | | - -## S153 _test_accessor(行 62–62 · 1 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1230 | 62 | "row_by_level_index 缺行返回空字典〔fv2〕" | t.row_by_level_index(9).is_empty() | (待填) | | | - -## S154 _test_negative(行 72–105 · 11 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1231 | 72 | "合法三行域校验零错误(夹具自证)〔fv2〕" | _table(base).validate().is_empty() | (待填) | | | -| 1232 | 75 | "L1 出口=2 被拒(出口 L1 恒 1 已锁)〔fv2〕" | not _table(bad_l1_exit).validate().is_empty() | (待填) | | | -| 1233 | 78 | "配比合计 0.5 被拒(四类合计 1 已锁)〔fv2〕" | not _table(bad_ratio).validate().is_empty() | (待填) | | | -| 1234 | 81 | "层深 5<6 被拒(层深下限已锁)〔fv2〕" | not _table(bad_layers).validate().is_empty() | (待填) | | | -| 1235 | 85 | "层深增幅倒挂被拒(递增且增幅变大)〔fv2〕" | not _table(bad_growth).validate().is_empty() | (待填) | | | -| 1236 | 88 | "出口 4 越域 [1,3] 被拒〔fv2〕" | not _table(bad_exit).validate().is_empty() | (待填) | | | -| 1237 | 92 | "岔口去路数 (2,4) 被拒(恒 2/3 已锁)〔fv2〕" | not _table([bad_fork_row, base[1], base[2]]).validate().is_empty() | (待填) | | | -| 1238 | 97 | "字段键集非 ROW_KEYS 闭集被拒〔fv2〕" | not _table([bad_keys_row, base[1], base[2]]).validate().is_empty() | (待填) | | | -| 1239 | 100 | "键重复行被拒〔fv2〕" | not _table([base[0], base[0]]).validate().is_empty() | (待填) | | | -| 1240 | 102 | "战斗保底 -1 被拒(≥0)〔fv2〕" | not _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]]).validate().is_empty() | (待填) | | | -| 1241 | 105 | "空表域校验零错误(合法态)〔fv2〕" | _table([]).validate().is_empty() | (待填) | | | - -## S155 _test_derive_determinism(行 36–40 · 2 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1242 | 36 | "同种子两实例 derive_at(0..7) 逐位一致" | same | (待填) | | | -| 1243 | 40 | "derive_at 为纯函数不推计数器" | a2.counter() == before | (待填) | | | - -## S156 _test_counter_ledger(行 51–57 · 2 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1244 | 51 | "open_stream 逐次推进全局计数器(+1/次)" | c1 == c0 + 1 and c2 == c0 + 2 | (待填) | | | -| 1245 | 57 | "同 scope 异用途标签派生种子一致(标签不参与派生)" | s1 == s2 | (待填) | | | - -## S157 _test_stream_draw_determinism(行 71–71 · 1 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1246 | 71 | "同种子两实例同标签流取数 12 次逐位一致" | same | (待填) | | | - -## S158 _test_negative_seed_normalization(行 79–79 · 1 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1247 | 79 | "负种子 abs 归一化后派生与正种子一致" | pos.derive_at(0) == neg.derive_at(0) | (待填) | | | - -## S159 _smoke_initialize(行 28–53 · 13 条 · 源 game/tests/unit/data_config/rng_stream_boot_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1248 | 28 | "全部表加载 + 校验通过(含跨表护栏)" | load_ok | (待填) | | | -| 1249 | 37 | "统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)" | rng.derive_at(0) == 8672514112661456564 | (待填) | | | -| 1250 | 38 | "统一流向量 m=42 scope=1" | rng.derive_at(1) == 8834291164406037215 | (待填) | | | -| 1251 | 39 | "统一流向量 m=42 scope=2" | rng.derive_at(2) == 7714373262647006282 | (待填) | | | -| 1252 | 40 | "统一流向量 m=42 scope=3" | rng.derive_at(3) == 1280102845971471347 | (待填) | | | -| 1253 | 41 | "统一流向量 m=42 scope=4" | rng.derive_at(4) == 82585501446482399 | (待填) | | | -| 1254 | 42 | "统一流向量 m=42 scope=5" | rng.derive_at(5) == 8078924421474470922 | (待填) | | | -| 1255 | 43 | "统一流向量 m=42 scope=6" | rng.derive_at(6) == 6638052740430622424 | (待填) | | | -| 1256 | 44 | "统一流向量 m=42 scope=7" | rng.derive_at(7) == 8264196488417721294 | (待填) | | | -| 1257 | 45 | "derive_at 纯函数恒等" | rng.derive_at(0) == rng.derive_at(0) | (待填) | | | -| 1258 | 49 | "open_stream 全局计数器单调 +1 ×2" | rng.counter() == 2 | (待填) | | | -| 1259 | 50 | "首个取号 = scope 0(顺序确定)" | s0.seed == rng.derive_at(0) and s1.seed == rng.derive_at(1) | (待填) | | | -| 1260 | 53 | "new_adventure 重置全局计数器" | rng.counter() == 1 and s2.seed == rng.derive_at(0) | (待填) | | | - -## S160 _smoke_adventure_table(行 92–203 · 18 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1261 | 92 | "db.adventure 已加载(adventure.tres 入 manifest)" | _db.adventure != null | (待填) | | | -| 1262 | 95 | "AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)" | _db.adventure.validate().is_empty() | (待填) | | | -| 1263 | 96 | "clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" ↩ % int(_db.adventure.clear_heal_pct) | int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100 | (待填) | | | -| 1264 | 99 | "档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" ↩ % [tier, int(_db.adventure.TIER_QUOTAS[tier])] | int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]) | (待填) | | | -| 1265 | 101 | "令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕" | int(_db.adventure.deck_total()) == 102 | (待填) | | | -| 1266 | 102 | "数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2" | int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50 and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2 and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10 and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2 | (待填) | | | -| 1267 | 107 | "恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕" | int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1 | (待填) | | | -| 1268 | 109 | "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕" | int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99 | (待填) | | | -| 1269 | 111 | "同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, null) == 0 | (待填) | | | -| 1270 | 113 | "同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, _TreeSaveMock.new()) == 1 | (待填) | | | -| 1271 | 120 | "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕" | trows.size() == 12 and tid_set.size() == 12 | (待填) | | | -| 1272 | 131 | "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕" | names_ok | (待填) | | | -| 1273 | 159 | "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕" | alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2 | (待填) | | | -| 1274 | 162 | "装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕" | orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]}) and orch_l.attune_has("morning_order") | (待填) | | | -| 1275 | 166 | "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕" | orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) and not orch_e.attune_has("morning_order") | (待填) | | | -| 1276 | 181 | "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕" | v9_ok | (待填) | | | -| 1277 | 188 | "效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕" | tag_set.has("info") and tag_set.has("action") and tag_set.has("plan") | (待填) | | | -| 1278 | 202 | "装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕" | v18_ok and locked_n == 12 | (待填) | | | - -## S161 _smoke_enemy_bands(行 229–261 · 4 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1279 | 229 | "敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕" | _db.enemy.rows.size() == 28 and got_keys == expect_keys | (待填) | | | -| 1280 | 236 | "hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕" | int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \\ and int(hob.get("depth_band", -1)) == 3 | (待填) | | | -| 1281 | 239 | "hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕" | FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行") | (待填) | | | -| 1282 | 260 | "三关 Boss 行集带键 = 覆盖表(模板终点 4/9/9 在册+生成图终点 6/8/9/11/12/13 扩带)%s〔fv13〕" ↩ % boss_detail | boss_ok | (待填) | | | - -## S162 _smoke_ac_swap_guard(行 272–380 · 13 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1283 | 272 | "状态表 defense_up 行已退役(无双轨)" | _db.status_effect_row("defense_up").is_empty() | (待填) | | | -| 1284 | 274 | "ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)" | not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \\ and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \\ and String(ac_row.get("stack_policy", "")) == "refresh" | (待填) | | | -| 1285 | 282 | "奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)" | String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护" | (待填) | | | -| 1286 | 284 | "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)" | not _db.settlement.core.effect_pool_map.has("defense_up") and not _db.settlement.core.effect_pool_map.has("ac_up") | (待填) | | | -| 1287 | 299 | "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" ↩ % [ wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.core.pool_clamp_lo)] | wall_ok | (待填) | | | -| 1288 | 305 | "hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)" | hd_maps == 0 | (待填) | | | -| 1289 | 307 | "主角 armor_class 在域内(%d——暂定基准随表走)" ↩ % int(_db.hero.armor_class) | int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30 | (待填) | | | -| 1290 | 324 | "敌方 AC 行值在已裁带宽内(杂鱼 0~5·重击手/重装手 8~9·Boss 8~12——迭代 5 返工批 v1 重投,越带行%s)" ↩ % [ ac_detail if ac_detail != "" else "无"] | ac_band | (待填) | | | -| 1291 | 332 | "Boss AC 关卡台阶严格递增(%d < %d < %d)" ↩ % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))] | int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)) | (待填) | | | -| 1292 | 348 | "熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕" ↩ % l12_band | l12_band >= 0 and not l12_row.is_empty() and int(l12_row.get("hp", -1)) == 22 \\ and int(l12_row.get("armor_class", -1)) == 2 \\ and l12_pool == {"attack": 6, "charge": 2, "heavy": 2} | (待填) | | | -| 1293 | 370 | "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" ↩ % [ curve_detail if curve_detail != "" else "无"] | curve_ok | (待填) | | | -| 1294 | 375 | "掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)" | int(_db.settlement.damage_roll.roll_faces) == 20 and int(_db.settlement.damage_roll.crit_roll) == 20 \\ and int(_db.settlement.damage_roll.crit_multiplier) == 200 and int(_db.settlement.damage_roll.damage_floor) == 1 \\ and int(_db.settlement.damage_roll.attack_bonus) == 0 and String(_db.settlement.damage_roll.armor_roll_stream) == "armor_roll" | (待填) | | | -| 1295 | 379 | "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)" | bounds == [3, 7] and discounts == [75, 50, 25] | (待填) | | | - -## S163 _smoke_variation_framework(行 387–415 · 9 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1296 | 387 | "变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)" | _db.variation.rows.size() == 6 | (待填) | | | -| 1297 | 389 | "变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)" | _db.variation.validate().is_empty() | (待填) | | | -| 1298 | 394 | "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)" | pool1_ids == ["thorn", "vine"] | (待填) | | | -| 1299 | 398 | "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)" | pool2_ids == ["magma", "ash"] | (待填) | | | -| 1300 | 399 | "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)" | _db.variant_pool(1, "underdark").is_empty() | (待填) | | | -| 1301 | 403 | "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)" | pool2d_ids == ["thorn", "vine"] | (待填) | | | -| 1302 | 405 | "清单外 variant_id 返回空(读取通道在位)" | _db.variation.row_by_id("nonexistent").is_empty() | (待填) | | | -| 1303 | 411 | "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 1304 | 415 | "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒" | _errs_has_cross_field(vt.validate()) | (待填) | | | - -## S164 _smoke_difficulty_table(行 422–444 · 8 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1305 | 422 | "DifficultyTable MVP 空表加载(结构位)" | _db.difficulty != null and _db.difficulty.rows.is_empty() | (待填) | | | -| 1306 | 425 | "spawn 受理" | enemy_system.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 1307 | 428 | "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" ↩ % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)] | int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk) | (待填) | | | -| 1308 | 433 | "修饰后 spawn 受理" | enemy_system_b.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 1309 | 435 | "hp 修饰 ×1.5(%d→%d,max_hp 同步)" ↩ % [int(d0.hp), int(g2.hp)] | int(g2.hp) == int(round(float(d0.hp) * 1.5)) and int(g2.max_hp) == int(g2.hp) | (待填) | | | -| 1310 | 437 | "atk 修饰 ×2.0(%d→%d)" ↩ % [int(d0.atk), int(g2.atk)] | int(g2.atk) == int(d0.atk) * 2 | (待填) | | | -| 1311 | 442 | "越域修饰 spawn 受理" | enemy_system_c.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 1312 | 443 | "越域乘数 clamp 至 10(%d→%d)" ↩ % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)] | int(enemy_system_c.all_enemies()[0].hp) == int(d0.hp) * 10 | (待填) | | | - -## S165 _smoke_nine_types_gp_stream(行 452–494 · 6 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1313 | 452 | "格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)" | RewardTable.CELL_TYPES.size() == 10 and RewardTable.CELL_TYPES.has("gp") and RewardTable.CELL_TYPES.has("treasure_item") | (待填) | | | -| 1314 | 467 | "奖励字典 18 行(九型×正负 18 格——aux 挖格投放四行已退役 2026-10-01 用户拍板甲:战斗/挖格不掉道具,秘宝唯一来路=三选一共享池〔节点/关卡完成〕;清图槽独立批尾不计)" | cell_rows == 18 | (待填) | | | -| 1315 | 474 | "危险格负向直伤 %s power=%d(卡⑧投值)" ↩ % [rid, int(mine_power[rid])] | int(row.get("power", -1)) == int(mine_power[rid]) | (待填) | | | -| 1316 | 487 | "正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" ↩ % pos_weight | pos_all_in_pool and neg_all_zero | (待填) | | | -| 1317 | 489 | "gp 型两行(GP 袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)" | gp_pos == 2 and gp_neg == 0 | (待填) | | | -| 1318 | 493 | "derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)" | rng.counter() == 0 | (待填) | | | - -## S166 _smoke_meta(行 502–515 · 4 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1319 | 502 | "MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)" | meta_tbl != null and meta_tbl.validate().is_empty() | (待填) | | | -| 1320 | 504 | "MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)" | meta_tbl.boss_keystone.size() == 3 and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2 | (待填) | | | -| 1321 | 510 | "conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)" | attrs_ok | (待填) | | | -| 1322 | 515 | "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)" | boss_ok | (待填) | | | - -## S167 _init(行 13–14 · 2 条 · 源 game/tests/unit/example_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1323 | 13 | "headless SceneTree 协议可用(quit(code) 通道)" | _test_headless_protocol() | (待填) | | | -| 1324 | 14 | "命名规范可执行:test_[scenario]_[expected]" | _test_naming_convention_followed() | (待填) | | | - -## S168 _test_numeric_lines_carry_values(行 31–36 · 3 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1325 | 31 | "休整叙述带入回复量与当前血量(实际「%s」)" ↩ % rest | rest.contains("12") and rest.contains("30") and rest.contains("40") | (待填) | | | -| 1326 | 34 | "商店叙述带入 GP 数(实际「%s」)" ↩ % shop | shop.contains("77") | (待填) | | | -| 1327 | 35 | "奖励行与休整行互异(不是同一句套所有场景)" | NarratorScript.reward_line() != rest | (待填) | | | - -## S169 _test_end_text_differs_by_result(行 42–48 · 4 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1328 | 42 | "关卡收束态恒为「关卡完成!」(实际「%s」)" ↩ % level | level == "关卡完成!" | (待填) | | | -| 1329 | 45 | "单场胜负文案互异(胜「%s」/负「%s」)" ↩ % [win, lose] | win != lose | (待填) | | | -| 1330 | 46 | "胜利文案非空(实际「%s」)" ↩ % win | win != "" | (待填) | | | -| 1331 | 48 | "未知结果有兜底文案不吐空串(实际「%s」)" ↩ % unknown | unknown != "" | (待填) | | | - -## S170 _test_phase_text_nonempty(行 53–55 · 2 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1332 | 53 | "相位叙述非空(实际「%s」)" ↩ % t | t != "" | (待填) | | | -| 1333 | 55 | "Boss 在场时叙述带入 Boss 名(实际「%s」)" ↩ % boss | boss.contains("灰矮人石君王") | (待填) | | | - -## S171 _test_boss_name_uses_boss_rowset(行 82–83 · 2 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1334 | 82 | "Boss 名取自 Boss 行集首行(期望「%s」实际「%s」)〔fv24〕" ↩ % [boss_expect, got] | got == boss_expect | (待填) | | | -| 1335 | 83 | "Boss 名与同深度战斗行集名不同(期望「%s」≠「%s」——行类路由可判别)〔fv24〕" ↩ % [boss_expect, battle_name] | boss_expect != battle_name | (待填) | | | - -## S172 _test_level_clear_wording(行 90–101 · 4 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1336 | 90 | "关卡完成回血行带入回血量与生命区间(实际「%s」)〔口径换代 V1〕" ↩ % healed | healed.contains("关卡完成回血 +5(生命 10 → 15)") | (待填) | | | -| 1337 | 93 | "满血零回血合法态文案(实际「%s」)〔口径换代 V1〕" ↩ % full_hp | full_hp.contains("生命已满——关卡完成回血 0(合法态)") | (待填) | | | -| 1338 | 96 | "奖励节点头部行渠道句点明关卡完成三选一(实际「%s」)〔口径换代 V2〕" ↩ % node_header | node_header.contains("与关卡完成专长三选一同库分流") | (待填) | | | -| 1339 | 100 | "冒险摘要完成关数行(实际「%s」)〔口径换代 V3〕" ↩ % summary | summary.contains("完成 1 关 · 止步第 2 层") and not summary.contains("通关") | (待填) | | | - -## S173 _run(行 27–102 · 13 条 · 源 game/tests/unit/presentation/forge_workbench_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1340 | 27 | "ConfigDb 加载(工作间测试夹具前提)" | false | (待填) | | | -| 1341 | 44 | "锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕" | not ov._forge_workbench.visible and ov._forge_lock_l.visible | (待填) | | | -| 1342 | 46 | "锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕" | ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0 | (待填) | | | -| 1343 | 52 | "解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕" | ov._forge_workbench.visible | (待填) | | | -| 1344 | 53 | "两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕" | ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3 | (待填) | | | -| 1345 | 56 | "六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕" | rows.has("sword") and rows.has("spear") and rows.has("staff") and rows.has("light") and rows.has("heavy") and rows.has("ward") | (待填) | | | -| 1346 | 63 | "长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕" | rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled | (待填) | | | -| 1347 | 66 | "轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕" | rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20" and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled | (待填) | | | -| 1348 | 73 | "长枪行:解锁钮「解锁 200」+无购买钮+无选中钮〔锻造工坊批 V13〕" | rsp.get("unlock_btn") != null and String(rsp.unlock_btn.text) == "解锁 200" and not rsp.unlock_btn.disabled and rsp.get("buy_btn") == null and rsp.get("select_btn") == null | (待填) | | | -| 1349 | 76 | "魔法护甲行:解锁钮「解锁 200」+状态含并联双通道文案(练满轻甲锻造 或 200 钥石直购)〔锻造工坊批 V13〕" | rwa.get("unlock_btn") != null and String(rwa.status_l.text).contains("练满轻甲锻造") and String(rwa.status_l.text).contains("200") | (待填) | | | -| 1350 | 84 | "余额 0:长剑购买钮禁用+长枪解锁钮禁用(余额联动常态防线)〔锻造工坊批 V13〕" | rows.sword.buy_btn.disabled and rows.spear.unlock_btn.disabled | (待填) | | | -| 1351 | 92 | "长剑满级:无购买钮+状态「已满级」+长枪派生解锁出解锁钮消失(练满通道)〔锻造工坊批 V13〕" | rows.sword.get("buy_btn") == null and String(rows.sword.status_l.text) == "已满级" and rows.spear.get("unlock_btn") == null and rows.spear.get("buy_btn") != null | (待填) | | | -| 1352 | 101 | "三意图信号外发(buy=spear/unlock=heavy/select=spear——UI 只发意图)〔锻造工坊批 V13〕" | buys == ["spear"] and unlocks == ["heavy"] and selects == ["spear"] | (待填) | | | - -## S174 _test_accepted_text(行 20–20 · 1 条 · 源 game/tests/unit/presentation/hud_presenter_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1353 | 20 | "受理态文案固定为「执行完成。」(实际「%s」)" ↩ % t | t == "执行完成。" | (待填) | | | - -## S175 _test_reject_text_by_reason(行 36–38 · 2 条 · 源 game/tests/unit/presentation/hud_presenter_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1354 | 36 | "棋盘面各拒绝原因文案逐类正确(错 %d 项 %s)" ↩ % [bad.size(), str(bad)] | bad.is_empty() | (待填) | | | -| 1355 | 38 | "资源面拒绝文案为「AP 不足。」(实际「%s」)" ↩ % ap | ap == "AP 不足。" | (待填) | | | - -## S176 _test_unknown_reason_fallback(行 43–47 · 2 条 · 源 game/tests/unit/presentation/hud_presenter_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1356 | 43 | "未知拒绝原因兜底为「动作被拒。」而非空串(实际「%s」)" ↩ % t | t == "动作被拒。" | (待填) | | | -| 1357 | 47 | "棋盘面未知子因兜底带前缀可诊断(实际「%s」)" ↩ % sub | sub.begins_with("棋盘面拒绝: ") | (待填) | | | - -## S177 _test_fog_hides_type(行 51–56 · 3 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1358 | 51 | "可见集内节点:图标/类型文字均绘制〔fv2〕" | bool(shown["draw_icon"]) and bool(shown["draw_label"]) | (待填) | | | -| 1359 | 54 | "雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕" | not bool(fogged["draw_icon"]) and not bool(fogged["draw_label"]) | (待填) | | | -| 1360 | 56 | "雾下非 Boss 节点无 Boss 描边〔fv2〕" | not bool(fogged["boss_border"]) | (待填) | | | - -## S178 _test_boss_exempt_and_multi_boss(行 65–73 · 4 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1361 | 65 | "多终点:两个 Boss 节点均带 Boss 描边〔fv3〕" | bool(boss_a["boss_border"]) and bool(boss_b["boss_border"]) | (待填) | | | -| 1362 | 67 | "Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕" | bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"]) | (待填) | | | -| 1363 | 70 | "Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕" | bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"]) | (待填) | | | -| 1364 | 73 | "Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕" | not bool(fogged_battle["draw_icon"]) | (待填) | | | - -## S179 _test_details_never_shown(行 87–89 · 2 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1365 | 87 | "决策面不消费内容键:携带 enemy_id/reward_id 与不带者输出逐键相等〔fv5〕" | same | (待填) | | | -| 1366 | 88 | "label_text 不含内容值(实际「%s」)〔fv5〕" ↩ % String(b["label_text"]) | not String(b["label_text"]).contains("goblin_secret") and not String(b["label_text"]).contains("reward_secret") | (待填) | | | - -## S180 _test_label_three_states(行 96–101 · 3 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1367 | 96 | "当前位文案=「当前位置」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置" | (待填) | | | -| 1368 | 98 | "候选文案=「战斗 · 可选」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], false, true)["label_text"]) == "战斗 · 可选" | (待填) | | | -| 1369 | 100 | "其余文案=类型名「战斗」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], false, false)["label_text"]) == "战斗" | (待填) | | | - -## S181 _test_big_graph_fit(行 165–191 · 6 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1370 | 165 | "大图夹具规模:≥11 层 × ≥6 槽(实测最大层 %d/最大槽 %d)〔fv8〕" ↩ % [maxl, maxslot] | maxl >= 10 and maxslot >= 5 | (待填) | | | -| 1371 | 171 | "大图拟合:画布 min.x 覆盖最深节点(%.1f ≥ %.1f)〔fv8〕" ↩ % [min_size.x, need_w] | min_size.x >= need_w - 0.01 | (待填) | | | -| 1372 | 173 | "大图拟合:画布 min.y 覆盖最宽层底节点(%.1f ≥ %.1f)〔fv8〕" ↩ % [min_size.y, need_h] | min_size.y >= need_h - 0.01 | (待填) | | | -| 1373 | 181 | "大图布局:全节点方块落在拟合画布内(origin=(%.1f,%.1f) size=(%.1f,%.1f))〔fv8〕" ↩ % [origin.x, origin.y, min_size.x, min_size.y] | all_in | (待填) | | | -| 1374 | 188 | "大图呈现:两个终点 Boss 均有描边标记〔fv8〕" | disp.has(boss_ids[0]) and disp.has(boss_ids[1]) and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"]) | (待填) | | | -| 1375 | 191 | "大图呈现:入口停点远处战斗节点(句柄 32,层 6)类型入雾〔fv8〕" | not bool(disp[32]["draw_icon"]) | (待填) | | | - -## S182 _test_layout_cache(行 209–226 · 2 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1376 | 209 | "布局缓存:同 nodes 两次查询返回同一实例(第二次命中缓存)〔levelmap批 V1〕" | is_same(p1, p2) | (待填) | | | -| 1377 | 224 | "布局缓存换代:新图查询返回新实例非旧缓存(失效重算生效)〔levelmap批 V2〕" | not is_same(p1, pc1) and is_same(pc1, pc2) and (pc1[chain[0]] as Vector2) != (p1[nodes[0]] as Vector2) | (待填) | | | - -## S183 _init(行 105–149 · 11 条 · 源 game/tests/unit/presentation/pacing_queue_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1378 | 105 | "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)" | snap_before == snap_after | (待填) | | | -| 1379 | 107 | "结算行已产生(断言前提)" | rows_seen.size() > 0 | (待填) | | | -| 1380 | 108 | "排空后队列恒空(flush 完整性)" | main._row_queue.is_empty() | (待填) | | | -| 1381 | 117 | "末信号行原文在明细带(排空不吞行:『%s』)" ↩ % last_text | last_text != "" and main.log_view.parsed_text().contains(last_text) | (待填) | | | -| 1382 | 123 | "常态段间延迟 >0 且 ≥ 段内(%f/%f)" ↩ % [d_on, d_same] | d_on > 0.0 and d_on >= d_same | (待填) | | | -| 1383 | 125 | "减动效段间延迟归零(行不跳过——同帧直落快路)" | main.pace_delay_for("damage", "heal") == 0.0 | (待填) | | | -| 1384 | 135 | "结算标出按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕" | fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"] | (待填) | | | -| 1385 | 138 | "跨区弹道按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕" | fstage.calls.size() == 2 and fstage.calls[1] == ["goblin_b", "地精"] | (待填) | | | -| 1386 | 140 | "两路解析成功后演出层各接一拍(记参面非空)〔wd12〕" | ffx.calls.size() == 2 | (待填) | | | -| 1387 | 147 | "会话更替夹具已入队(前提:%d 行)〔wr4〕" ↩ % main._row_queue.size() | main._row_queue.size() == 2 | (待填) | | | -| 1388 | 149 | "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕" | main._row_queue.is_empty() | (待填) | | | - -## S184 _sweep_hero_panel(行 47–50 · 2 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1389 | 47 | "C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕" | feat_head != null and feat_head.text == "专长" | (待填) | | | -| 1390 | 49 | "C 区属性值提升段头==「属性值提升」(u2 换代——同屏两段同标撞车消除)〔wa1〕" | curio_head != null and curio_head.text == "属性值提升" | (待填) | | | - -## S185 _sweep_narrator(行 57–64 · 7 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1391 | 57 | "奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕" | head.contains("冒险牌库") | (待填) | | | -| 1392 | 58 | "奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕" | head.contains("属性值提升与补给") | (待填) | | | -| 1393 | 59 | "奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕" | head.contains("同库分流") | (待填) | | | -| 1394 | 60 | "奖励点头部零「同权重」旧话(u3 清零)〔wa4〕" | not head.contains("同权重") | (待填) | | | -| 1395 | 61 | "奖励点头部零「同上限」旧话(u3 清零)〔wa4〕" | not head.contains("同上限") | (待填) | | | -| 1396 | 62 | "奖励点头部零「提升池」旧话(u3 清零)〔wa4〕" | not head.contains("提升池") | (待填) | | | -| 1397 | 63 | "专长卡兜底==「专长\\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕" | String(narrator.feat_card_text({})) == "专长\\n???" | (待填) | | | - -## S186 _sweep_legend(行 75–79 · 5 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1398 | 75 | "图例封印行存在(wa5 夹具前提)〔wa5〕" | not seal_row.is_empty() | (待填) | | | -| 1399 | 76 | "图例封印行含「标空」(u4——与「标中」成对)〔wa5〕" | String(seal_row[2] if seal_row.size() > 2 else "").contains("标空") | (待填) | | | -| 1400 | 77 | "图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕" | String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚") | (待填) | | | -| 1401 | 78 | "图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕" | not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错") | (待填) | | | -| 1402 | 79 | "图例封印行零「挖雷」(u4 雷字清零)〔wa5〕" | not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷") | (待填) | | | - -## S187 _sweep_source_names(行 92–104 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1403 | 92 | "状态槽 mine_hit 效果行在座(wa6 夹具前提)〔wa6〕" | slot != null | (待填) | | | -| 1404 | 95 | "状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕" | row_view._tip_label.text.contains("来源:危险格") | (待填) | | | -| 1405 | 98 | "结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕" | String(_db.settlement.core.source_label_map.get("mine_hit", "")) == "危险格" | (待填) | | | -| 1406 | 103 | "战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕" | row_txt.contains("(危险格)") | (待填) | | | - -## S188 _sweep_board_glyph(行 133–142 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1407 | 133 | "棋盘夹具含挖掘射程内未开危险格(wa8 前提——实测 %d 步)〔wa8〕" ↩ % glow_steps | mine_cell.x >= 0 | (待填) | | | -| 1408 | 139 | "已开危险格 glyph==「危」(u7 换代)〔wa8〕" | u.glyph == "危" | (待填) | | | -| 1409 | 140 | "已开危险格 plate==true(u9 牌载哨兵包含项随动)〔wa8〕" | u.plate == true | (待填) | | | -| 1410 | 141 | "已开危险格 glyph_color==RED_MINE(u7 墨色不随字形换代漂移)〔wa8〕" | u.glyph_color == UiKit.RED_MINE | (待填) | | | - -## S189 _sweep_token_scans(行 148–158 · 2 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1411 | 148 | "board_view.gd 源零 \\"雷\\" 字面量(u7/u8/u9 哨兵族统一钉住)〔wa8〕" | board_src.count("\\"雷\\"") == 0 | (待填) | | | -| 1412 | 157 | "battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(u13——注释保留面不入域)〔wa12〕" | orch_hits == 0 | (待填) | | | - -## S190 _init(行 75–132 · 18 条 · 源 game/tests/unit/presentation/standee_fx_sweep_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1413 | 75 | "站桩 enemy_id 访问器读装载值〔wd12〕" | s1.enemy_id() == "goblin_a" and s2.enemy_id() == "goblin_b" | (待填) | | | -| 1414 | 76 | "同名敌按 id 精确解析(不做名子串撞车)〔wd12〕" | stage._card_by_id("goblin_b") == s2 and stage._card_by_id("goblin_a") == s1 | (待填) | | | -| 1415 | 78 | "空 id / 未登记 id 恒不命中(名兜底归调用点)〔wd12〕" | stage._card_by_id("") == null and stage._card_by_id("no_such") == null | (待填) | | | -| 1416 | 83 | "伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕" | s2._flash_count == 1 and s1._flash_count == 0 | (待填) | | | -| 1417 | 88 | "弹道落点按 target_id 取站桩中心〔wd12〕" | c_b != null and c_b == c_b_expect | (待填) | | | -| 1418 | 91 | "无 target_id 时按名兜底同旧行为(首命中)〔wd12〕" | c_fallback != null and c_fallback == c_fallback_expect | (待填) | | | -| 1419 | 95 | "蓄力技能(heavy)置 _intent_charge=true〔wd2〕" | s1._intent_charge | (待填) | | | -| 1420 | 97 | "非蓄力攻击技(goblin_slash)零置位〔wd2〕" | not s1._intent_charge | (待填) | | | -| 1421 | 99 | "增益系零置位〔wd2〕" | not s1._intent_charge | (待填) | | | -| 1422 | 101 | "普攻零表示路径复位 _intent_charge〔wd2〕" | not s1._intent_charge | (待填) | | | -| 1423 | 108 | "首开胜利画面创建庆祝句柄〔wd4〕" | first != null and first.is_valid() | (待填) | | | -| 1424 | 111 | "重开胜利画面:旧句柄已 kill、新句柄在位(至多一个活跃)〔wd4〕" | first != null and not first.is_valid() and second != null and second.is_valid() | (待填) | | | -| 1425 | 114 | "close 必杀庆祝句柄(句柄清空)〔wd4〕" | ov.get_meta(&"celebrate_tw", null) == null | (待填) | | | -| 1426 | 119 | "模拟运行期调试入口钮同置灰〔wd5〕" | dbg._dbg_btn.disabled | (待填) | | | -| 1427 | 121 | "模拟收尾调试入口钮恢复(未锁定)〔wd5〕" | not dbg._dbg_btn.disabled | (待填) | | | -| 1428 | 130 | "控制器运行中 is_running()=true(驱动回调内读数)〔wd8〕" | drv.seen == [true, true] | (待填) | | | -| 1429 | 131 | "控制器收尾 is_running()=false〔wd8〕" | not ctrl.is_running() and not panel.running | (待填) | | | -| 1430 | 132 | "两局报告全文注入面板(run 收尾链路完整)〔wd8〕" | String(panel.report) != "" | (待填) | | | - -## S191 _init(行 24–34 · 2 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1431 | 24 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | -| 1432 | 34 | "前置:board_gen 词表非空(%d 型)" ↩ % types.size() | types.size() > 0 | (待填) | | | - -## S192 _test_set_completeness(行 56–56 · 1 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1433 | 56 | "图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" ↩ % [String(bt), variants, str(missing)] | missing.is_empty() | (待填) | | | - -## S193 _test_hue_domains(行 72–82 · 4 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1434 | 72 | "图型 %s 抽检样本可读" ↩ % String(bt) | false | (待填) | | | -| 1435 | 77 | "图型 %s 墙顶均值色相在带 %s(实测 %.3f)" ↩ % [String(bt), str(band), float(wall["h"])] | float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1]) | (待填) | | | -| 1436 | 79 | "图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" ↩ % [String(bt), wall_v[bt], floor_v[bt]] | wall_v[bt] > floor_v[bt] | (待填) | | | -| 1437 | 82 | "deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)" | float(wall_v["deep_underdark"]) < float(wall_v["underdark"]) | (待填) | | | - -## S194 _test_digit_ink_modes(行 137–159 · 5 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1438 | 137 | "图型 %s 数字墨族断言样本可读" ↩ % String(bt) | false | (待填) | | | -| 1439 | 144 | "图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" ↩ % [ String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]), float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])] | float(mode["cr_mean_dark"]) < float(TilePaletteScript.DIGIT_INK_CR_MEAN_DARK) | (待填) | | | -| 1440 | 151 | "图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" ↩ % [String(bt), mode_name, float(TilePaletteScript.DIGIT_INK_CR_P90), cr_p90] | cr_p90 >= float(TilePaletteScript.DIGIT_INK_CR_P90) | (待填) | | | -| 1441 | 155 | "图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" ↩ % [String(bt), mode_name, mean_gate, cr_mean] | cr_mean >= mean_gate | (待填) | | | -| 1442 | 158 | "图型 %s 呈现模式期望=%s(实测=%s)" ↩ % [String(bt), "色牌" if bool(expect_plate[bt]) else "直画", mode_name] | plate == bool(expect_plate[bt]) | (待填) | | | - -## S195 _smoke_type_glow(行 215–375 · 24 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1443 | 215 | "V1 微光默认开(Q4 丙案——呈现态布尔缺省 true,生产写入面零显式开)" | bool(snap.get("enabled", false)) | (待填) | | | -| 1444 | 217 | "V1 夹具全集合法九型 ⇒ 清单外登记为空(禁静默空光通道零误报)" | (snap.get("errors", []) as Array).is_empty() | (待填) | | | -| 1445 | 220 | "V1 作用集恒等:微光格集合 == _diggable ∩ 身份可查(期望 %d,实测 %d)" ↩ % [expect.size(), (snap.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap.get("cells", {}), expect) | (待填) | | | -| 1446 | 222 | "V1 夹具前提:英雄未开邻格 ≥5(实测 %d——novice %d 步)" ↩ % [expect.size(), steps] | expect.size() >= 5 | (待填) | | | -| 1447 | 232 | "V1 已开格/r0 出生展开区零微光(S1)%s" ↩ % ("" if opened_leak == "" else "违例:" + opened_leak) | opened_leak == "" | (待填) | | | -| 1448 | 249 | "V2 夹具前提:未开邻格派定 ≥5 型(实测 %d)" ↩ % mapped | mapped >= 5 | (待填) | | | -| 1449 | 258 | "V2 九型→五组逐格映射(attack/defense/action/gp→增益/heal→治疗/pierce→穿刺/blast→群伤/magic→魔法)%s" ↩ % ("" if map_bad == "" else "违例:" + map_bad) | map_bad == "" | (待填) | | | -| 1450 | 272 | "V2 清单外 cell_type 显式登记(errors=%s)" ↩ % str(errs) | errs.has("keystone") | (待填) | | | -| 1451 | 273 | "V2 清单外格零微光(无静默空光——%s 不在作用集)" ↩ % str(bogus_cell) | not (snap.get("cells", {}) as Dictionary).has(bogus_cell) | (待填) | | | -| 1452 | 275 | "V2 清单外格剔除后作用集 = 派定数 −1(%d→%d)" ↩ % [mapped, (snap.get("cells", {}) as Dictionary).size()] | (snap.get("cells", {}) as Dictionary).size() == mapped - 1 | (待填) | | | -| 1453 | 288 | "V1 封锁格零微光(query null ⇒ 天然惰性——%s)" ↩ % str(blocked_cell) | not (snap.get("cells", {}) as Dictionary).has(blocked_cell) | (待填) | | | -| 1454 | 290 | "V1 封锁格零登记(errors 仍仅清单外一条——%s)" ↩ % str(snap.get("errors", [])) | (snap.get("errors", []) as Array).size() == 1 | (待填) | | | -| 1455 | 292 | "V1 封锁格剔除后作用集 = 派定数 −1(实测 %d)" ↩ % (snap.get("cells", {}) as Dictionary).size() | (snap.get("cells", {}) as Dictionary).size() == mapped - 1 | (待填) | | | -| 1456 | 298 | "V1 封锁解除后作用集复位恒等(期望 %d,实测 %d)" ↩ % [expect.size(), (snap.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap.get("cells", {}), expect) | (待填) | | | -| 1457 | 304 | "V7 off 态开关读数=false" | not bool(snap.get("enabled", true)) | (待填) | | | -| 1458 | 305 | "V7 off 态作用集为空(基线保护面)" | (snap.get("cells", {}) as Dictionary).is_empty() | (待填) | | | -| 1459 | 308 | "V7 on 态开关读数=true(复现)" | bool(snap_on.get("enabled", false)) | (待填) | | | -| 1460 | 309 | "V7 off→on 作用集逐位复现(关前 %d 格/开后 %d 格,组号逐位一致)" ↩ % [ (snap_before_off.get("cells", {}) as Dictionary).size(), (snap_on.get("cells", {}) as Dictionary).size()] | _glow_map_equal(snap_on.get("cells", {}), snap_before_off.get("cells", {})) | (待填) | | | -| 1461 | 312 | "V7 off→on 作用集与独立重算期望集恒等(%d 格)" ↩ % (snap_on.get("cells", {}) as Dictionary).size() | _glow_key_set_equal(snap_on.get("cells", {}), expect) | (待填) | | | -| 1462 | 316 | "V7 微光开关零玩法写入(已开集/英雄位/封印数三面对照逐位一致)" | revealed_before == revealed_after and hero_before == ms.hero_pos() and seals_before == ms.seal_count() | (待填) | | | -| 1463 | 329 | "V3 换图 rebuild 后作用集按新板重算(期望 %d,实测 %d——零旧板残留)" ↩ % [expect2.size(), (snap2.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap2.get("cells", {}), expect2) | (待填) | | | -| 1464 | 331 | "V3 换图清清单外登记(rebuild 清零)" | (snap2.get("errors", []) as Array).is_empty() | (待填) | | | -| 1465 | 372 | "契约 1 同板同序列 on/off 两态结算逐位一致(%d 步对照——A 开态/B 关态)%s" ↩ % [seq_steps, diverge] | diverge == "" | (待填) | | | -| 1466 | 375 | "契约 1 对照前提:同序列 ≥5 步(实测 %d)" ↩ % seq_steps | seq_steps >= 5 | (待填) | | | - -## S196 _smoke_battle_log_view(行 392–423 · 6 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1467 | 392 | "敌方伤害行格式化(baseline 行不读退役 attach)" | enemy_txt.contains("撕咬") and enemy_txt.contains("〔掷 15 vs 护甲 12") | (待填) | | | -| 1468 | 394 | "退役伏击/基线拆行文案不再出现" | not enemy_txt.contains("伏击") and not enemy_txt.contains("基线") | (待填) | | | -| 1469 | 401 | "主角穿刺型暴击行金框(单分量 payload 不触发三分量拆行)" | hero_txt.contains("暴击×") and hero_txt.contains("⚔击杀") and hero_txt.contains("■") | (待填) | | | -| 1470 | 403 | "退役三分量拆行文案不再出现" | not hero_txt.contains(" + 穿刺 ") | (待填) | | | -| 1471 | 409 | "未生效行聚合计数 ×2 呈现" | agg_txt.contains("⚠") and agg_txt.contains("×2") | (待填) | | | -| 1472 | 422 | "行型 %s 格式化落地(解析文本增长)" ↩ % String(row_type) | view.parsed_text().length() > before | (待填) | | | - -## S197 _smoke_level_open_line(行 430–436 · 3 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1473 | 430 | "开场行 boosted=true 尾接「(含面板加成)」〔wc1〕" | String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("(含面板加成)") | (待填) | | | -| 1474 | 432 | "开场行 boosted=false 零后缀(白板局不带)〔wc1〕" | not String(NarratorScript.level_open_line("第一层", 42, 5, 30, false)).contains("(含面板加成)") | (待填) | | | -| 1475 | 434 | "开场行种子/两值带入不受后缀位影响(数值带入回归)〔wc1〕" | String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("种子 42") and String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("攻击 5 / 生命 30") | (待填) | | | - -## S198 _smoke_global_theme(行 445–540 · 22 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1476 | 445 | "V1 项目主题挂载(project.godot gui/theme/custom → main_theme.tres)" | th != null | (待填) | | | -| 1477 | 454 | "V1 主题类型覆盖 %s" ↩ % t | type_set.has(t) | (待填) | | | -| 1478 | 456 | "V1 红线 Button 类型词条零接触" | not type_set.has("Button") | (待填) | | | -| 1479 | 464 | "V1 红线 focus 族词条零定义%s" ↩ % ("" if focus_hits == "" else "(违例:" + focus_hits + ")") | focus_hits == "" | (待填) | | | -| 1480 | 468 | "V1 滚动条槽/柄三态词条在位" | vsb.has_theme_stylebox("scroll") and vsb.has_theme_stylebox("grabber") and vsb.has_theme_stylebox("grabber_highlight") and vsb.has_theme_stylebox("grabber_pressed") | (待填) | | | -| 1481 | 471 | "V1 滚动条 scroll_focus 未定义(红线)" | not th.has_stylebox("scroll_focus", "VScrollBar") and not th.has_stylebox("scroll_focus", "HScrollBar") | (待填) | | | -| 1482 | 474 | "V1 滑轨槽/填充/柄词条在位" | hsl.has_theme_stylebox("slider") and hsl.has_theme_stylebox("grabber_area") and hsl.has_theme_stylebox("grabber_area_highlight") and hsl.has_theme_icon("grabber") and hsl.has_theme_icon("grabber_highlight") and hsl.has_theme_icon("grabber_disabled") | (待填) | | | -| 1483 | 480 | "V1 LineEdit 词条在位(含占位色 font_placeholder_color)" | le.has_theme_stylebox("normal") and le.has_theme_color("font_color") and le.has_theme_color("font_placeholder_color") | (待填) | | | -| 1484 | 484 | "V1 SpinBox 箭头词条在位(updown 合并槽)" | sp.has_theme_icon("updown") | (待填) | | | -| 1485 | 487 | "V1 分隔线词条在位" | hsep.has_theme_stylebox("separator") and vsep.has_theme_stylebox("separator") | (待填) | | | -| 1486 | 491 | "V1 TooltipPanel 词条在位" | tpanel.has_theme_stylebox("panel") | (待填) | | | -| 1487 | 492 | "V1 TooltipLabel 词条在位" | tlabel.has_theme_color("font_color") and tlabel.has_theme_font_size("font_size") | (待填) | | | -| 1488 | 500 | "V2 冻结值锚定(已填充段=BG_ROOT #3A2E22,槽=暖石 #B8A582)" | fill_c.is_equal_approx(Color8(58, 46, 34)) and track_c.is_equal_approx(Color8(184, 165, 130)) | (待填) | | | -| 1489 | 503 | "V2 滑轨两段对比 %.2f:1 ≥4.5:1(拖到中点一眼可辨)" ↩ % ratio | ratio >= 4.5 | (待填) | | | -| 1490 | 505 | "V2 滑轨 stylebox 类型均为 StyleBoxFlat" | false | (待填) | | | -| 1491 | 508 | "V3 CheckBox 四态图标非空" | cbx.get_theme_icon("checked") != null and cbx.get_theme_icon("unchecked") != null and cbx.get_theme_icon("checked_disabled") != null and cbx.get_theme_icon("unchecked_disabled") != null | (待填) | | | -| 1492 | 512 | "V3 CheckButton 四态图标非空(与 CheckBox 同套 checked/unchecked 名,4.7 源码实证)" | cbt.get_theme_icon("checked") != null and cbt.get_theme_icon("unchecked") != null and cbt.get_theme_icon("checked_disabled") != null and cbt.get_theme_icon("unchecked_disabled") != null | (待填) | | | -| 1493 | 516 | "V3 PopupMenu 词条存在(弹层预盖)" | pm.has_theme_stylebox("panel") and pm.has_theme_stylebox("hover") and pm.has_theme_font_size("font_size") | (待填) | | | -| 1494 | 520 | "V3 OptionButton 词条存在(预盖)" | ob.has_theme_stylebox("normal") and ob.has_theme_stylebox("hover") and ob.has_theme_stylebox("pressed") and ob.has_theme_color("font_color") | (待填) | | | -| 1495 | 534 | "V4 退役实现零残留(%s)%s" ↩ % [path, "" if bad == "" else "违例:" + bad] | bad == "" | (待填) | | | -| 1496 | 537 | "V6 主题 default_font 非空(挂主题顶掉 custom_font 也不丢中文字形)" | th.default_font != null | (待填) | | | -| 1497 | 540 | "V6 Label 实取字体非空(中文字形防御实证)" | lbl.get_theme_font("font") != null | (待填) | | | - -## S199 _smoke_button_five_states(行 553–625 · 23 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1498 | 553 | "V1 普通钮 %s 样式盒非空" ↩ % st | b.has_theme_stylebox_override(st) | (待填) | | | -| 1499 | 559 | "V1 普通钮 normal/hover/disabled 均为石纹九宫格贴图样式盒" | n_sb is StyleBoxTexture and h_sb is StyleBoxTexture and d_sb is StyleBoxTexture | (待填) | | | -| 1500 | 563 | "V1 普通钮 hover=modulate 提亮(乙案,异于常态 WHITE)" | h_mod != Color.WHITE and h_mod.r > 1.0 | (待填) | | | -| 1501 | 567 | "V1 普通钮 disabled=本族底 α0.45 幽灵化(甲案)" | is_equal_approx(d_mod.a, 0.45) and d_mod.r == 1.0 and d_mod.g == 1.0 | (待填) | | | -| 1502 | 570 | "V1 普通钮 disabled≠pressed(旧错配已修正)" | _sb_content_differs(d_sb, p_sb) | (待填) | | | -| 1503 | 572 | "V1 普通钮 focus 样式盒为扁平环(StyleBoxFlat)" | f_sb is StyleBoxFlat | (待填) | | | -| 1504 | 574 | "V1 焦点环色=INK(与 font_focus_color 同源)" | fsb.border_color == uikit_s.INK | (待填) | | | -| 1505 | 575 | "V1 焦点环宽=2px" | is_equal_approx(fsb.border_width_left, 2.0) and is_equal_approx(fsb.border_width_right, 2.0) and is_equal_approx(fsb.border_width_top, 2.0) and is_equal_approx(fsb.border_width_bottom, 2.0) | (待填) | | | -| 1506 | 578 | "V1 焦点环内缩=1px" | is_equal_approx(fsb.expand_margin_left, -1.0) and is_equal_approx(fsb.expand_margin_top, -1.0) and is_equal_approx(fsb.expand_margin_right, -1.0) and is_equal_approx(fsb.expand_margin_bottom, -1.0) | (待填) | | | -| 1507 | 581 | "V1 焦点环无底(叠加态不遮钮底)" | not fsb.draw_center and fsb.bg_color.a < 0.001 | (待填) | | | -| 1508 | 586 | "V1 主钮 %s 样式盒非空" ↩ % st | pb.has_theme_stylebox_override(st) | (待填) | | | -| 1509 | 590 | "V1 主钮 pressed/disabled=本族平涂(非石纹)" | ppn is StyleBoxFlat and pdd is StyleBoxFlat | (待填) | | | -| 1510 | 592 | "V1 主钮 pressed=烛光黄深档(现行值不动)" | (ppn as StyleBoxFlat).bg_color == Color8(214, 172, 88) | (待填) | | | -| 1511 | 594 | "V1 主钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((pdd as StyleBoxFlat).bg_color.a, 0.45) | (待填) | | | -| 1512 | 596 | "V1 主钮 disabled≠pressed" | _sb_content_differs(ppn, pdd) | (待填) | | | -| 1513 | 597 | "V1 主钮 hover=提亮档(承现行不动)" | (phv as StyleBoxFlat).bg_color == uikit_s.CANDLE.lightened(0.06) | (待填) | | | -| 1514 | 603 | "V1 危险钮 %s 样式盒非空" ↩ % st | db.has_theme_stylebox_override(st) | (待填) | | | -| 1515 | 606 | "V1 危险钮 pressed/disabled=本族平涂(非石纹,旧错配已修正)" | dpr is StyleBoxFlat and dds is StyleBoxFlat | (待填) | | | -| 1516 | 608 | "V1 危险钮 pressed=深红深档(暗于常态)" | (dpr as StyleBoxFlat).bg_color.r < 180.0 / 255.0 | (待填) | | | -| 1517 | 610 | "V1 危险钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45) | (待填) | | | -| 1518 | 612 | "V1 危险钮 disabled≠pressed" | _sb_content_differs(dpr, dds) | (待填) | | | -| 1519 | 617 | "V2 红线维持 Button 类型词条零接触" | not th.get_type_list().has("Button") | (待填) | | | -| 1520 | 624 | "V2 红线维持 focus 族词条零定义%s" ↩ % ("" if focus_hits == "" else "(违例:" + focus_hits + ")") | focus_hits == "" | (待填) | | | - -## S200 _smoke_texture_filter(行 632–674 · 9 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1521 | 632 | "V3 工程设置 default_texture_filter=0(Nearest,翻转面=立绘/瓦片/九宫格 crisp)" | v != null and int(v) == 0 | (待填) | | | -| 1522 | 636 | "V3 白名单在位:暗角 vignette 显式 LINEAR" | vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | -| 1523 | 640 | "V3 白名单在位:光柱 beams 显式 LINEAR" | bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | -| 1524 | 658 | "T4 光柱带心峰值 α≈0.05(8bit 截断量化下限 12/255,实测 %.5f)" ↩ % max_a | max_a >= 12.0 / 255.0 and max_a <= 0.05 | (待填) | | | -| 1525 | 660 | "T4 光柱带内连续 alpha 过渡像素 %d 个(旧零过渡硬边已修)" ↩ % mid_a | mid_a > 100 | (待填) | | | -| 1526 | 661 | "T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" ↩ % [top_sum, bot_sum] | top_sum > bot_sum * 2.0 | (待填) | | | -| 1527 | 665 | "V3 白名单在位:地图渐变 gradient_base 显式 LINEAR" | gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | -| 1528 | 670 | "V3 脚影显式 LINEAR 不丢(board_world.gd)" | bw_src.contains("shadow.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR") | (待填) | | | -| 1529 | 673 | "V3 状态图标显式 LINEAR 不丢(status_slot_row.gd)" | ss_src.contains("icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR") | (待填) | | | - -## S201 _smoke_contrast_inks(行 683–699 · 9 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1530 | 683 | "V4 顺位徽标/重甲徽标 GOLD_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" ↩ % r_order | r_order >= 4.5 | (待填) | | | -| 1531 | 685 | "V4 魔法护甲徽标 HERO_BLUE_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" ↩ % r_ward | r_ward >= 4.5 | (待填) | | | -| 1532 | 687 | "V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" ↩ % r_rap | r_rap >= 4.5 | (待填) | | | -| 1533 | 690 | "V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)" | sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT") | (待填) | | | -| 1534 | 692 | "V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT" | sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT") | (待填) | | | -| 1535 | 693 | "V4 重甲徽标墨=GOLD_BRIGHT" | sb_src.contains("col = UiKit.GOLD_BRIGHT") | (待填) | | | -| 1536 | 694 | "V4 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕" | sb_src.contains("_intent_charge = pid in CHARGE_SKILL_IDS") and not sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:") | (待填) | | | -| 1537 | 697 | "V4 RAP 反馈行墨=STONE_DEEP(原位行锚定)" | ap_src.contains('UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)') | (待填) | | | -| 1538 | 699 | "V4 RAP 反馈行旧墨 STONE_FAINT 清零(含注记面)" | not ap_src.contains("STONE_FAINT") | (待填) | | | - -## S202 _smoke_element_details(行 708–727 · 8 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1539 | 708 | "T5② BCard 图标槽白图标对比 %.2f:1 ≥3:1(硬约束)" ↩ % r_white | r_white >= 3.0 | (待填) | | | -| 1540 | 711 | "T5② 明度差降档生效(槽对卡面 %.2f:1 < 旧 %.2f:1)" ↩ % [r_new, r_old] | r_new < r_old | (待填) | | | -| 1541 | 713 | "T5② BCard 图标槽落柔和档(SLOT_BACKDROP_SOFT)" | uk_src.contains("UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)") | (待填) | | | -| 1542 | 716 | "T5② B 区状态图标槽仍用深档(SLOT_BACKDROP,未误伤)" | ss_src.contains("UiKit.SLOT_BACKDROP") | (待填) | | | -| 1543 | 720 | "T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)" | shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER") | (待填) | | | -| 1544 | 724 | "T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" ↩ % camp_min | camp_min == 3 | (待填) | | | -| 1545 | 726 | "T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" ↩ % camp_fill | camp_fill == 4 | (待填) | | | -| 1546 | 727 | "T5③ 三处锁文垂直居中在位" | camp_src.count("SIZE_SHRINK_CENTER") >= 3 | (待填) | | | - -## S203 _smoke_diag_button_retired(行 734–747 · 7 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1547 | 734 | "V6 主区无 build_diag_button 调用残留(旧实现同批清理)" | not main_src.contains("build_diag_button") | (待填) | | | -| 1548 | 736 | "V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)" | main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)") | (待填) | | | -| 1549 | 738 | "V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)" | main_src.contains("func _on_diagnose_pressed()") | (待填) | | | -| 1550 | 741 | "V6 build_diag_button 工厂已退役(ui_assembler 零残留)" | not asm_src.contains("build_diag_button") | (待填) | | | -| 1551 | 744 | "V6 调试浮窗 diagnose_requested 信号在位" | dbg_src.contains("signal diagnose_requested") | (待填) | | | -| 1552 | 745 | "V6 浮窗转储钮外发方法在位(纯视图边界)" | dbg_src.contains("_emit_diagnose_requested") | (待填) | | | -| 1553 | 746 | "V6 浮窗诊断转储钮在位(玩家画面角落钮清零后唯一入口)" | dbg_src.contains('UiKit.button("转储")') | (待填) | | | - -## S204 _status_panel_projection(行 763–793 · 10 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1554 | 763 | "状态面板开层可见(open_status 直开——P4 甲零动效路径)" | po.visible | (待填) | | | -| 1555 | 764 | "余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕" | String(po._balance_badge.value_label.text) == "钥石 77" | (待填) | | | -| 1556 | 766 | "属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕" | String(po._attrs_label.text) == "生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41" | (待填) | | | -| 1557 | 768 | "秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕" | String(po._curios_label.text) == "青苔密卷 · 锈蚀怀表(已用)" | (待填) | | | -| 1558 | 770 | "专长行投影 = 行串 join〔sv3〕" | String(po._feats_label.text) == "疼痛记忆 ×2 · 双管镐 ×1" | (待填) | | | -| 1559 | 777 | "面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕" | stale == "" | (待填) | | | -| 1560 | 778 | "旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕" | po.get("_workshop_holder") == null and po.get("_tree_label") == null | (待填) | | | -| 1561 | 785 | "空秘宝态句=「未携带秘宝」〔sv2〕" | String(po2._curios_label.text) == "未携带秘宝" | (待填) | | | -| 1562 | 786 | "空专长态句=「未获专长」〔sv3〕" | String(po2._feats_label.text) == "未获专长" | (待填) | | | -| 1563 | 792 | "提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕" | hint_txt == "查看本局冒险进度(只读)" | (待填) | | | - -## S205 _camp_recent_projection(行 818–819 · 1 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1564 | 818 | "营地最近冒险行完成关数措辞(实际「%s」)〔口径换代 V6〕" ↩ % recent_text | recent_text.contains("完成 2 关") and not recent_text.contains("通关") | (待填) | | | - -## S206 _career_overlay_projection(行 852–897 · 11 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1565 | 852 | "生涯页开层可见(open_career 直开——零动效路径)" | co.visible | (待填) | | | -| 1566 | 853 | "总局数投影 = adventure_total() 读数(%s)" ↩ % co._total_label.text | String(co._total_label.text) == "2 局" | (待填) | | | -| 1567 | 855 | "冒险成功局数投影 = adventure_cleared() 读数(%s)" ↩ % co._cleared_label.text | String(co._cleared_label.text) == "1 局(三关全通)" | (待填) | | | -| 1568 | 860 | "各关完成次数投影 = clear_count() 逐关读数(%s)" ↩ % co._clears_label.text | String(co._clears_label.text) == " · ".join(expect_clears) | (待填) | | | -| 1569 | 862 | "Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" ↩ % [co._boss_first_label.text, co._boss_kills_label.text] | String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次" | (待填) | | | -| 1570 | 864 | "钥石余额徽投影 = keystone_balance() 读数(%s)" ↩ % co._keystone_badge.value_label.text | String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance() | (待填) | | | -| 1571 | 871 | "生涯页行标签换代(冒险成功局数/各关完成次数)〔口径换代 V4〕" | career_labels_joined.contains("冒险成功局数") and career_labels_joined.contains("各关完成次数") | (待填) | | | -| 1572 | 873 | "生涯页 label 全集零「通关」残留〔口径换代 V4〕" | not career_labels_joined.contains("通关") | (待填) | | | -| 1573 | 874 | "秘宝解锁条件标签冒险成功口径(UiKit 单点常量)〔口径换代 V5〕" | UiKit.TREASURE_REQ_LABELS["adventure_cleared"] == "冒险成功" | (待填) | | | -| 1574 | 883 | "v5 现役档升 v6 载入成功(计数域缺省补零)" | v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0 | (待填) | | | -| 1575 | 894 | "旧档载入后生涯页正常显示(v5 升 v6:总局数/冒险成功局数显示 0,既有钥石余额原样呈现)" | co_v5.visible and String(co_v5._total_label.text) == "0 局" \\ and String(co_v5._cleared_label.text) == "0 局(三关全通)" \\ and String(co_v5._keystone_badge.value_label.text) == "钥石 3" | (待填) | | | - -## S207 _test_grouping_closed(行 61–79 · 7 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1576 | 61 | "九型封闭:%s → 组%d(期望组%d)" ↩ % [t, got, int(TYPE_EXPECT[t])] | got == int(TYPE_EXPECT[t]) | (待填) | | | -| 1577 | 65 | "清单外值「%s」→ GLOW_NONE(禁静默空光)" ↩ % bad | UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE | (待填) | | | -| 1578 | 70 | "两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)" | band_blast == UiKitScript.TG_MAGIC | (待填) | | | -| 1579 | 71 | "两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)" | UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE | (待填) | | | -| 1580 | 74 | "glow_color_for_group 群伤组 = TG_GLOW_AOE" | UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE | (待填) | | | -| 1581 | 76 | "glow_color_for_group 无组(GLOW_NONE)→ 全透明" | UiKitScript.glow_color_for_group(UiKitScript.GLOW_NONE) == Color(0, 0, 0, 0) | (待填) | | | -| 1582 | 78 | "glow_color_for_group 越界组号 → 全透明" | UiKitScript.glow_color_for_group(99) == Color(0, 0, 0, 0) | (待填) | | | - -## S208 _test_frozen_hue_face(行 88–102 · 5 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1583 | 88 | "实现值锚定:%s = #%02X%02X%02X(色值单点 ui_kit,逐位钉死)" ↩ % [CONST_NAME[g], int(m.r8), int(m.g8), int(m.b8)] | impl_const == m | (待填) | | | -| 1584 | 93 | "冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" ↩ % [g, dh * 360.0, ds] | dh <= 0.2 / 360.0 and ds <= 0.01 | (待填) | | | -| 1585 | 98 | "群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" ↩ % (absf(aoe_m.h - aoe_f.h) * 360.0) | absf(aoe_m.h - aoe_f.h) < 0.001 | (待填) | | | -| 1586 | 100 | "群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" ↩ % absf(aoe_m.s - aoe_f.s) | absf(aoe_m.s - aoe_f.s) <= 0.01 | (待填) | | | -| 1587 | 102 | "群伤组专项:V=0.65 明度档( provisional 域内)" | absf(aoe_m.v - 166.0 / 255.0) < 0.002 | (待填) | | | - -## S209 _test_render_params(行 107–113 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1588 | 107 | "泛光强度 α = 170/255(v2 呈现参数冻结)" | is_equal_approx(UiKitScript.TYPE_GLOW_ALPHA, 170.0 / 255.0) | (待填) | | | -| 1589 | 109 | "带宽 = cell÷8(TYPE_GLOW_BAND_FRAC=0.125)" | is_equal_approx(UiKitScript.TYPE_GLOW_BAND_FRAC, 0.125) | (待填) | | | -| 1590 | 111 | "组号常量域连续(GLOW_NONE=0…GLOW_MAGIC=5)" | UiKitScript.GLOW_NONE == 0 and UiKitScript.GLOW_BUFF == 1 and UiKitScript.GLOW_HEAL == 2 and UiKitScript.GLOW_PIERCE == 3 and UiKitScript.GLOW_AOE == 4 and UiKitScript.GLOW_MAGIC == 5 | (待填) | | | - -## S210 _test_gamma_ladder(行 128–139 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1591 | 128 | "阶梯单调:%s 合成亮度 %.4f < 上一档 %.4f(伽马合成空间)" ↩ % [g, lum, prev_lum] | lum < prev_lum | (待填) | | | -| 1592 | 134 | "相邻组模型 CR:%s/%s = %.3f ≥1.20(色弱互辨预判)" ↩ % [order[i], order[i + 1], cr] | cr >= 1.20 | (待填) | | | -| 1593 | 138 | "群伤/魔法线性亮度序:群伤 %.4f < 魔法 %.4f(首版 CIE L* 排反事故的守门)" ↩ % [aoe_lin, magic_lin] | aoe_lin < magic_lin | (待填) | | | - -## S211 _test_probe_api_shape(行 146–153 · 5 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1594 | 146 | "probe 键集 = {enabled, cells, errors}" | snap.has("enabled") and snap.has("cells") and snap.has("errors") and snap.size() == 3 | (待填) | | | -| 1595 | 148 | "probe 默认态:enabled=true(Q4 丙案生产默认开)" | bool(snap.get("enabled", false)) | (待填) | | | -| 1596 | 149 | "probe 默认态:作用集空(无板未派生)" | (snap.get("cells", {}) as Dictionary).is_empty() | (待填) | | | -| 1597 | 150 | "probe 默认态:清单外登记空" | (snap.get("errors", []) as Array).is_empty() | (待填) | | | -| 1598 | 152 | "set_type_glow_enabled(false) → probe enabled=false(无板不崩)" | not bool(view.type_glow_probe().get("enabled", true)) | (待填) | | | - -## S212 _init(行 16–28 · 6 条 · 源 game/tests/unit/presentation/ui_font_asset_test.gd · 去向:待分类回填) - -| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | -|---|---|---|---|---|---|---| -| 1599 | 16 | "随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)" | f != null | (待填) | | | -| 1600 | 18 | "中文字形在集(攻/雷/旗/封)" | f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \\ and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)) | (待填) | | | -| 1601 | 20 | "中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)" | f.has_char("危".unicode_at(0)) | (待填) | | | -| 1602 | 21 | "ASCII/数字字形在集(A/0/%)" | f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)) | (待填) | | | -| 1603 | 25 | "中文采样文本宽度 > 0(C6 headless 判据)" | w_cn > 0.0 and w_label > 0.0 | (待填) | | | -| 1604 | 28 | "字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)" | false | (待填) | | | - ---- -全源共 212 章、1604 条(静态调用点口径)。 diff --git a/reports/iter41-反哺点-2026-10-28.md b/reports/iter41-反哺点-2026-10-28.md new file mode 100644 index 00000000..85f8c3b9 --- /dev/null +++ b/reports/iter41-反哺点-2026-10-28.md @@ -0,0 +1,33 @@ +# 迭代 41 反哺点清单——两卡(核心数据面结构收敛批+呈现层结构收敛批) + +> 每条=哪个文件哪一节、改成什么、依据。落文归文档批(doc-design),本清单只是申请单。 + +## A. architecture.md(v1.22)——定稿注记的现状改写(卡① §6 ③) + +依据:卡① 全清裁决四项已实现,v1.19 定稿注记的「实现批后」表述全部过期;现行行号按 v1.22 实查(2026-10-28)。 + +1. **:88 ConfigDB 行**——「分组建档于实现卡,消费方改址随该批」改现状:三容器已建(`config_domain_combat/run/crossrun.gd`,类型化成员 7/7/5),公开面收敛至 8(push_diag/is_fatal/reset_diagnostics/diagnostics/diagnostics_count/load_tables/set_reload_gate/reload),类变量 9;resolver 经 load_tables 装配段注入三表(board_gen.resolve_row/enemy.resolve_rowset/difficulty.resolve_modifiers),resolver 文件与键零搬移。红线句「唯一加载与持有单点……经其持有的表对象」原样保留。 +2. **:89 SlotStore 行**——「实现批建件」改现状:已建件,槽位族 10 公开/类变量 4;save_manager 组装持有,消费方经 `save.slots.` 直连;_rename_overwrite 留 save_manager(IO 原语单点)。 +3. **:90 MetaStateStore 行**——「实现批建件」改现状:已建件,写口原语 13+只读投影 6(tree_state/forge_state/attune_state/career_state 四投影+keystone_balance/item_unlocked)=19 公开,add_keystone 私有 _add_keystone;零 _data 副本零缓存;消费方经 `save.meta_state.` 直连。 +4. **:126 meta_progression 行**——「随件退役」改现状:公开面 20 定稿(buy_treasure_unlock/refresh_treasure_unlocks/treasure_shop_status/forge_buy_level/forge_buy_unlock/forge_select/hero_type_axes/forge_shop_status/settle_adventure/hero_override/tree_can_buy/tree_buy/tree_toggle_fork/tree_buy_fork/tree_rule_keys/tree_camp_status/toggle_attunement/attune_camp_status/attune_refresh_charges/buy_attune_refresh);纯转发读已退役(消费方直连 meta_state);hero_override 内联锻造加值与树淬炼 flat;并组三组(forge_select(kind,route_id)/hero_type_axes()→{attack,armor}/tree_camp_status()→{rows,fork_groups});补 meta_state 成员装配。 +5. **:104 minesweeper_rules 行**——消费依赖列「A3 try_spend / last_round_end / 相位」补形态注记:last_round_end 已由属性改方法(迭代 41 K5,消费方 minesweeper_rules :495 一处改方法调用)。 +6. **其余在册行零改动注记**(卡① §6 原列 :90/:92/:93/:103/:104 为 v1.19 行号,v1.22 现行=:92 action_economy/:93 variation/:94 status_container/:103 board_generator/:105 unit_container/:106 reward_economy):公开 API 语义零变化,仅内部拆分/束化/私有化——注记「迭代 41 零改动」即可,不逐行改写。 + +## B. GDD 行号随动(卡① §6 ①②,陈述本体零变化) + +7. **design/gdd/systems/战斗循环/地图战斗资源.md :83**——代码引用行行号换代:`_rollable_rows()` 「第 207-213 行」→「第 325 行起」(reward_economy 拆分后漂移);「循环第 65-67 行」→「第 59-61 行」(roll_board 身份批循环现行位置;规范化段已提 `_normalize_eligible`)。同池同权重一视同仁的陈述本体不变。 +8. **design/gdd/systems/冒险循环/冒险结构.md 调参表**——落点列改指段路径(K4′ 拆段后标量槽随段件嵌套,单 .tres 保持): + - :268「adventure_table curio_value_*」→「adventure_table value_segment.curio_value_*」; + - :270「adventure_table token_* 配额列族」→「adventure_table quota_segment.curio_quota_*」; + - :271 clear_heal_pct 不动(knob 留置宿主,:267 同理不动)。 +9. **同表 :267 疑似先行漂移发现(非本批造成,归文档批核实裁决)**:落点列「adventure_table bonus_attack_flat / bonus_max_hp_flat / bonus_armor_flat」——全库 grep 零命中(.gd 与 .tres 均无),疑指已退役槽名;现行承载疑为 value_segment.curio_value_{attack,max_hp,armor}_{common,excellent} 六槽。本批未改该行(不属两卡清单),提请文档批核现行值源后换代。 + +## C. 卡② 同步面核对记录(architecture 预期零修订) + +10. **architecture.md 零改动核对**:两豁免段在位(:183 组合根句柄面豁免、:311 测量面豁免);模块归属表零行变化(零新件、ui_kit 无独立行、flow context/状态件均为宿主内形态);组合根句柄面条款不受影响。**落文动作=无,仅登记核对结论。** +11. **待办清单状态核对注记(卡① §6 ④)**:来源条目「gdstyle 存量债全量清偿——216 键按病根分波整改」两卡对应键账已清(卡① 85/85+卡② 67 清 2 豁免 2 改善,见实现报告 §2);条目状态推进与残余键(三宿主 12 键归迭代 39 主卡)的去向,归主会话/batch-closure 统一回写,本批未触碰该清单。 + +## D. game/ 内文档(批内机械同步,非反哺) + +12. game/tests/验证集清单.md——已机器重生成对账(68 源文件 1587 调用点;断言数与 master 侧一致,地址形态随改址换代),随批入库,无落文义务。 +13. game/tests/golden/README.md——未动(乙组建锚与甲组判读属测试动作,随特许移交;让步声明见实现报告 §八.2)。 diff --git a/reports/iter41-实现报告-2026-10-28.md b/reports/iter41-实现报告-2026-10-28.md new file mode 100644 index 00000000..6c5eb276 --- /dev/null +++ b/reports/iter41-实现报告-2026-10-28.md @@ -0,0 +1,91 @@ +# 迭代 41 实现报告——gdstyle 存量债清偿两卡(核心数据面 85 键+呈现层 71 键) + +- 批次:迭代 41(卡① 需求卡片-核心数据面结构收敛批-2026-10-27 v1.2;卡② 需求卡片-呈现层结构收敛批-2026-10-27 v1.2) +- 工作区:worktree `D:\code\g-plan2-iter41`(分支 iter41,基 7f49a8c2);卡① 提交 c56b816f→070a5340 九笔,卡② 提交 98ac923f→9bef58f9 十笔(含 T2a 修与 u14 补做),并 master 38c9a0eb +- **测试执行声明(2026-10-28 用户特许)**:「不用做任何测试……测试相关动作全部交给我来运行」——本批全部套件停跑、闸停跑(gdstyle 结构闸除外,为开发期读数反馈在跑);验收断言、golden 对拍、撤销证明复现、独立验收审计全部移交用户执行,清单见 §7。因此本报告的「验收」栏记的是**移交状态**,不是已验证状态。 + +## 一、各卡状态 + +| 卡 | 状态 | 读数 | +|---|---|---| +| 卡① 核心数据面结构收敛批 | T0–T9 完成(T10=本报告+反哺点清单) | 在册 85/85 键全消失;8 新件键账逐件在阈(§3);契约 1 行为零变化六款按拆分逐字执行(提交信息逐笔留痕) | +| 卡② 呈现层结构收敛批 | T0–T5 完成(u1–u25 全落地;u14 补做) | 71 键=67 清+2 豁免在册+2 在案改善未消(§2);u23/u25 豁免申报见 §4 | + +偏差两笔(都在 §6 立案):①卡② u14 在 T2b(525529ad)提交信息有记、实际漏改——已补做(9bef58f9);②卡①完成定义写「下降 86 键」,波 0 刷新基线中卡①范围在册实为 85 键(config_db_cross_validator `validate_skill_pool_links 局部` 键在刷新时已不在册)——按在册口径 85/85 清零。 + +## 二、S13 键账终局对拍 + +**开工(波 0 刷新基线 d219dd3d)168 键 → 终局 16 键在册(全部基线内,NEW=0、WORSENED=0)→ 消失 152 键。** + +基线 168 键构成与终局去向(逐键归类实测,脚本对拍 2026-10-28): + +| 范围 | 基线在册 | 终局去向 | +|---|---|---| +| 卡① 26 文件 | 85 | **85 全消失** | +| 卡② 25 文件 | 71 | 67 消失;main 类变量 49、tune_sim await 2 键豁免在册;board_cell_unit 类变量 31→30、standee_body 类变量 29→19 两键**改善未消**(成因见 §6-B) | +| 两卡之外(board_view 6/level_runner 3/battle_orchestrator 2/adventure_runner 1) | 12 | 12 全在册(禁触面——board_view/level_runner/adventure_runner 结构拆件归迭代 39 主卡,battle_orchestrator 类变量为 v1.22 豁免第三条;battle_orchestrator 文件长度 1037→1035 随卡① spawn 调用点改址顺带改善) | + +卡② 71 键逐单元对账(卡面 §8.4 账目 vs 实绩):69 清承诺中 66 键消失;u3 类变量键、u4 类变量键两键未消(展平计数,§6-B);u14 一键补做后消失。卡① 85 键逐 T 对账:T1 17+T2 23+T3 17+T4 8=65 拆分清偿;T5 存档拆件(save 52→7+_peek_summary_at 7→6+load_from_disk 8→6)+T6 meta 31→20 与 status/treasure+T7 config_db 44→8、25→9+T8 adventure 44→12+T9 action_economy 18→15、spawn 参数 10→1 等结构收敛清偿。 + +## 三、新件键账表(卡① 8 新件,实测 vs 卡面预演值) + +| 新件 | 公开方法 | 类变量 | 卡面预演 | 判定 | +|---|---|---|---|---| +| `core/slot_store.gd` | 10 | 4 | 10/4 | 在阈 | +| `core/meta_state_store.gd` | 19(写口 13+读投影 6,_add_keystone 私有) | 1 | 19/1 | 在阈 | +| `core/config_domain_combat.gd` | 0 | 7 | 7 | 在阈 | +| `core/config_domain_run.gd` | 0 | 7 | 7 | 在阈 | +| `core/config_domain_crossrun.gd` | 0 | 5 | 5 | 在阈 | +| `schemas/adventure_quota_segment.gd` | 0 | 13 | 13 | 在阈 | +| `schemas/adventure_value_segment.gd` | 0 | 13 | 13 | 在阈 | +| `schemas/adventure_cap_segment.gd` | 0 | 9 | 9 | 在阈 | + +## 四、豁免申报(卡② u23/u25,零重构零键清偿动作) + +1. **u25 组合根类变量**:`game/presentation/main.gd|max-class-variables = 49`(登记 49,持平)——依据 architecture「呈现层装配与会话生命周期·三屏壳与组合根句柄面」节 S13 豁免登记在册(v1.22 :183:组合根类变量面=探针契约面,约 46 句柄被探针挂载假设钉死,键永久基线内、禁再恶化)。u25 零改动;本批 main.gd 的其他改动均为卡① 改址与 u6/u19 消费点随动,S10 句柄面零增删。 +2. **u23 测速循环 await**:`game/presentation/tune_sim_controller.gd|await-in-loop`(裸键,在册持平)——依据 architecture「测试与质量域·S13 测量面豁免登记」段在册(v1.22 :311:逐帧让出=调参测量 wall-clock 步进承载语义,禁重构)。同件 print 与 run 局部两键已清;playtest_logger print×2 已删。 + +## 五、并 master 与合并期处置(2026-10-28,合并提交 38c9a0eb) + +迭代期间 master 前进(断言归位专项批 94 处、断言精简 A+B 档 19 处、单元闸挂死守护、undo_proof 退役)。合并冲突 6 文件,处置原则=**master 的断言改钉/删并+本批的容器/拆件改址,两者语义正交逐处并案**(adventure_feats×2/config_db_resolver/level_shop_economy/save_boot_no_activate/roll_multiplier);另发现两批改址的系统性盲区互补——master 新写行带旧表地址 5 处、**本批 T7 扫荡漏「无点形态」(空值守卫/赋值/传参位的裸表引用)17 处+访问器迁移漏网 3 处**,合并期全数修复(action_economy._cost、effect_engine feats 守卫、hud_presenter 两守卫、测试 14 处;终扫 PCRE 负向前瞻零残留,冻结标签字面量未动)。验证集清单机器重生成对账(68 源文件 1587 调用点,与 master 侧计数一致——本批改址零断言数变化)。 + +## 六、实现决定记录 + +**A. 事故与补做** +1. 卡① T7 首轮改址扫荡漏容器表名(`db.combat.rows` 形)→ 整树 `git checkout -- game/` 回滚、修正替换规则后重跑,净效果零(a9a163f9 提交信息在案)。撤销方式:无需(事故已消解于当卡)。 +2. 卡② u14 漏做(T2b 提交信息有记、diff 未含该文件)→ 合并前补做(9bef58f9),装配序逐行原序保持,S7 源锚句逐字在位。撤销方式:回退 9bef58f9 即回键在册态。 + +**B. 键以「在案改善」收口(卡面承诺键消失不可达)** +- u3 board_cell_unit 类变量(31→30)、u4 standee_body 类变量(29→19):gdstyle 0.4.0 对宿主内 inner class 状态件成员按宿主**展平计数**(开工探针实测),键值含状态件成员后必 >15;卡面契约 6 定死 inner class 单形态(零新文件),独立状态类形态被禁。处置=按闸口径收口(改善绿非恶化红),键留基线。代价:两键继续在册。撤销方式:后续批若改 gdstyle 计数口径(类级键排除 inner class)可自然消失;否则随键主文件的下一次结构批裁决。 +- 与卡面「71=69 清+2 豁免」的差额即此两键:实绩 67 清+2 豁免+2 改善。 + +**C. 白名单内等价微调(一行备案)** +1. legend_overlay 循环提函数时尾段成孤儿代码(不可达 NEW 键)→ 尾段归还原调用方。 +2. camp_shop_presenter 束读落地改构造期成员拷贝形态(ctx 键集不变)。 +3. main.gd 战末账行束提 `_end_victory_opts` 装配单点(u6 opts 形随动,键构成不变)。 +4. board_fx 循环按实际 `for i in 2` 形态提函数;battle_log `_fmt_damage` 宿主成员 `_db` 不进状态件。 +5. spawn 束化扫荡加引号奇偶守卫(@@SPAWN_SKIP@@),断言字符串字面量零触碰。 + +## 七、测试移交清单(用户运行——特许移交,全部未跑) + +**交付前验收唯一入口**:worktree 内跑 `python tools/run_all_gates.sh`(本批含 game/ 代码面+reports/ 文档面 → 不带参数全量档;worktree 冷检出注意素材齐盘预检——art 素材与 *.import 边车不入 git)。 + +按卡重点套件(闸绿之外的建议抽查面): +- 卡①:data_config 八套件+core 直引;board_generator/board_gen_reward+node_map 三件(同种子逐位一致);效果面六套件;save 断言面全套(save_manager_domain/save_slot_isolation/save_slot_summary/save_boot_no_activate/breakpoint_resume/save_rng_roundtrip+integration);tree_requires_level/meta_progression_forge;seal_polarity/roll_multiplier/damage_controller/system_contracts/treasure_items_test;minesweeper_rules_test/round_flow_skill_pool;level_shop_economy;adventure_feats/pool_case_d/feat_vouchers/table_structure_guard(**deck_total()=102 结构锚**)。 +- 卡②:probe_gate 全段+system_test;theme_projection/ui_struct/ui_contrast/ui_tune/ui_click/ui_probe;player_copy_sweep/standee_fx_sweep/reward_overlay/battle_narrator/level_map_info_fog/hud_presenter;render_gate(甲组在库五张 golden 改后对拍——**乙组三帧与丙组四态改前锚因码先行动未建,此义务让步**,见 §8)。 +- 撤销证明:两卡 §8.1 末列手法(读数型/迁移型/grep 型)——移交抽查执行。 +- 独立验收审计:两卡 §8.3 抽查重点(卡① ⑪ 项含契约 7 职责语句抽检;卡② ⑨ 项含 u1 子域分组与 board_view 随动改址抽 5 行)——未过不得合并。 +- 批后可裁事项:`tools/gdstyle_baseline.txt --refresh`(152 死键清账,只降不升纪律,人工专用)——用户裁决时机。 + +## 八、实机目检总清单(登记不阻塞,判读口径全部=「与改前一致」) + +1. 启动冒烟实机一屏(卡① 行为零变化批总入口)。 +2. 乙组三屏开合观感:营地/设置/战斗。 +3. 丙组四态开合观感:战末结算/三选一/图例/调试浮窗。 +4. 营地三柜台(树淬炼/锻造工坊/秘宝匣)与同调交互回归。 + +## 九、遗留读数义务 + +1. MC/对拍锚:本批零数值零 RNG 改动 → 零 MC 零对拍补跑(卡① §0 纪律),无遗留。 +2. golden 乙组三帧(camp/settings/battle)与丙组四态改前锚未建(特许时序:码先行动)——改后无法补建改前帧,唯一在库对拍面=甲组五张;呈现等价的其余证据承载=现役探针/单测断言面。 +3. gdstyle 基线 152 死键在册(闸绿不受影响)——refresh 时机归用户(§7 末条)。 diff --git a/reports/工程改动报告-单元闸挂死守护-2026-10-28.md b/reports/工程改动报告-单元闸挂死守护-2026-10-28.md new file mode 100644 index 00000000..7109bd33 --- /dev/null +++ b/reports/工程改动报告-单元闸挂死守护-2026-10-28.md @@ -0,0 +1,32 @@ +# 工程改动报告——单元闸挂死守护(run_all.gd 每套件阻塞面超时) + +**承接**:待办清单 2026-10-25 条目「闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时」件②(件① battle_log_view 已随迭代 38 卡⑥ 出账,probe_gate.sh 整树回收件 2026-10-28 已落;本批完成后该条目整条删除,git 历史可考)。 +**车道**:工程改动直改批(改动四类,不写卡不编迭代),触及代码面走 worktree 批——分支 `runall-hangguard-20261028`,改动提交 a2710f22,并 master 后合并提交 4afe6c47。 +**实证**:迭代 40 批末阻塞面挂死实拍三例(测试 `_init` 在 `quit()` 前中断,进程挂至外层超时 124),与件②同根。 + +## 改动—验收对照表 + +| 需求点 | 改动(文件:行为) | 验收测试 | 层级 | 真实入口 | 判据绑定面 | 结果 | +|---|---|---|---|---|---|---| +| 每套件截止时间守护(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖,非法值回落默认) | `run_all.gd` `_run_suite` 阻塞调用换 `OS.create_process` 非阻塞+200ms 轮询+截止判定;常量 `SUITE_TIMEOUT_*` 与 `_suite_timeout_sec()` | 机制验证:临时挂死夹具(`_init` 死循环不 quit)登记 manifest 后 `--only` 选跑,`RUN_ALL_SUITE_TIMEOUT_SEC=8` → 输出 `[run_all] FAIL …dev_hang_fixture… — 超时挂死(8s),进程树已整树回收(pid=…)`+`[run_all] 1 个套件失败`,进程退出码 1 | 闸基建 | `run_all.gd` `--script` 直跑(选跑态) | FAIL 行文本+批退出码 | 绿(夹具与 manifest 行验证后即删,未提交) | +| 超时整树回收零孤儿 | `taskkill /F /T /PID`(杀 cmd wrapper 连引擎孙进程)+等死确认 10s 宽限(确认不了报红留 PID)+临界窗口 `.code` 同删 | 杀树后 PowerShell 按命令行清点该 worktree 路径 Godot 进程=0 | 闸基建 | 同上+外部进程清点 | 清点输出 | 绿 | +| 退出码通道等价(文件通道值=原 cmd 回传值,判定形态不变) | 命令串追加 `& echo !ERRORLEVEL! > 文件`(`cmd /V:ON`);读毕即删+起跑前清陈值;缺文件/非数字 fail-closed 判红 | 负面套件 expect=1 照常判 PASS;全量 65 套件 `all green (65 suites)` 退出码 0 | 闸基建 | `run_all.gd` 全量直跑(与收口闸 02 同源) | PASS 行 exit 值+all green 行 | 绿 | +| README 超时记载换代 | `game/tests/README.md` 工具速查表 run_all 行补守护说明;Running Tests 段失真行「本地闸必须配外部 wall-clock 超时(run_all_gates.sh 已内置)」删除(该脚本实测无 timeout 包装),换代为内建守护说明 | 收口闸 06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核全绿 | 文档 | 闸机扫描 | 闸退出码 | 绿 | +| 非行为改动:文件头注重写 | 只描述现行行为;原头注「本地闸 02 闸包装外部超时:timeout 120s」与闸机现实不符,属失真句换代 | — | — | — | — | 非行为改动 | + +验证集清单(`game/tests/验证集清单.md`)零变化:本批不增删不改判任何套件断言,改动全部在运行器进程机制层。 + +## 机制验证的 API 实证(开工探针四件,30 秒级临时件,验证后即删) + +1. 现行 `OS.execute(…, out, true, false)` 实测同步等待(ping 约 3s 计时吻合)、返回退出码——第五参在 Godot 4.7.2 上不是阻塞开关,阻塞面挂死根因坐实; +2. `OS.create_process` 返回真实 Windows PID,`is_process_running` 存活期 true、taskkill 后 false; +3. `cmd /V:ON /C "… & echo !ERRORLEVEL! > 文件"` 退出码落盘通道成立(错误命令落 1); +4. `taskkill /F /T` 对 cmd→ping 树整树连坐闭环(存活计数 1→杀→0,cmd 自身 running=false)——镜像 probe_gate 已落件的同款机制。 + +## 闸况 + +全量域闸回执 2955c049(domain=full,dirty_game=False,总判定验收通过):影响面选闸判测量面(diff 全在 `game/tests/`)→ 00 冒烟/03/04 探针/05 渲染闸/11 回放确定性 SKIP;01 系统测试、02 单元与集成测试、06–08d 文档域闸、10 熵增闸全绿;09 静态扫描退出码 1=存量红(`level_map_screen.gd|quality/max-class-variables` 22 对基线登记 21 的恶化留账,还债归迭代 41 在案;指纹与入库基线一致,本批零新增)。合并后两件秒级事:`--editor --quit` 重刷类名缓存退出码 0+冒烟启动 17 项 0 失败三闸全过。 + +## 移交与观察 + +`game/tests/README.md`「套件 manifest 现况」行记 64 条,manifest 实数 65 条(62 单元+3 集成)——漂移先于本批存在(主区与任务分支同值),本批未动套件数,已登记待办清单新条目(tests/README manifest 计数行漂移订正)归文档批。 diff --git a/reports/工程改动报告-存量断言精简AB档-2026-10-28.md b/reports/工程改动报告-存量断言精简AB档-2026-10-28.md new file mode 100644 index 00000000..4c4472a5 --- /dev/null +++ b/reports/工程改动报告-存量断言精简AB档-2026-10-28.md @@ -0,0 +1,76 @@ +# 工程改动报告——存量断言精简(A+B 档首批删并) + +**承接**:待办清单 2026-10-28 条目「存量断言精简——验证内容不变下的断言收敛」执行形态的第一波(盘点先行→逐批裁决→分波执行→件核)。 +**车道**:工程改动触及代码面(`game/tests/*.gd`)走 worktree 批——分支 `assert-slim-20261028`,区 `../g-plan2-assert-slim-20261028`。 +**盘点方法**:机器初筛(同文件标签近似 223 对、cond 全同 42 组 109 条、跨文件同标签、护栏类 41 条)+七路只读子代理逐域语义裁决(battle_settlement/core 三路/data_config+contracts/presentation/integration+system_test)+主会话逐条抽验复核。子代理报称候选约 150 点,掺有运行时展开数与「不同查询函数间的算术蕴含」,按本批判据线核减至 19 点。 + +## 一、本批判据线(删除属显式判据改判,先立判据) + +只删满足 **同一读数面 + 同一输入 + 谓词字面等价或字面上是承接者的真子集** 的站点。两类不算冗余、本批不删: + +1. **不同查询函数之间的算术蕴含**——删了就失去对被调函数实现本身的守卫(例:`has_unrevealed()` 与 `revealed_cells().size()2 else ""` 取空串 ⟹ :76 必红 | +| 14 | #168 | roll_multiplier:131 | nat20 种子可达 | ① | #169 `:132` 行留档 roll=20+crit | `crit_seed>0` 与 `crit_rows.roll==crit_roll` 同一扫描同真同假 | +| 15 | #324 | seal_polarity:443 | 构造触达:标中标空入队 | ① | #323 `:442` 极性路由种子可达 | `_polarity_probe_accepts` 合取项(:249-252),可达即三腿同真 | +| 16 | #325 | seal_polarity:444 | 构造触达:挖危险格 ≥1 | ① | 同上 | 同上(:253) | +| 17 | #326 | seal_polarity:445 | 构造触达:挖安全格 ≥1 | ① | 同上 | 同上(:253) | +| 18 | —(口径外) | smoke_boot.gd:69 | run_scope 非空 | ① | `:70` run_scope 在树 | `!= null` ⊂ `!= null ∧ is_inside_tree()`;GDScript `and` 短路,无空引用崩溃风险 | +| 19 | —(口径外) | smoke_boot.gd:72 | _start_overlay 已装配 | ① | `:73` 启动态菜单可见 | `!= null` ⊂ `!= null ∧ visible` | + +**两条改判(标签变更,判据不变)** + +- seal_polarity:442 标签追加「缺腿:%s」,把第 15/16/17 行的三腿诊断信息折进保留条;三腿取值仍读同一 `probe`,与原三条的语义强度逐字相同。 +- roll_multiplier:132 标签追加「种子可达 seed=%d」,把第 14 行的 seed 读数折进保留条。 + +**随行清理**:theme_projection:614-625 的 `var th` 与 focus 扫描循环随其承载的两条断言一并删除(该循环只为已删断言供数据);type_glow 的 `aoe_f` 局部量随 H/S 两条删除后无消费者,一并删;两处注释改写为指认真正承载该事实的保留条。 + +## 三、件核(验证项目覆盖不变) + +| 核对项 | 结果 | +|---|---| +| 冻结清单条数 | 1604 → **1587**(−17;smoke_boot 两站点本不在清单源列,条数不受其影响) | +| `extract_checks.py --diff` | PASS——标签/cond/计数三对齐,零差异 | +| 被删 19 条标签全仓残留 grep(`tests/`+`design/`+`reports/`) | 零悬挂引用(唯一命中的「run_scope 非空」是 system_test 自己的 V2e 在册条,非本批对象) | +| 承接指认 | 15 条保留行的「处置」列逐条回填去向(旧序号+蕴含依据+批次名),新清单在册 | +| 判据等价复验 | 由代码域闸承担(见闸况)——本批不新增行为,删后被承接行为以保留断言复验 | +| 取证方式 | 未使用变异法与撤销类取证(2026-10-24/28 用户裁);删除凭据=上表逐条申报+承接者指认 | + +## 四、闸况 + +全量域闸(域=full,`dirty_game=false`,`all_green=true`,总判定「闸况与基线一致,无新增红」)回执 commit `4fbacc43`:01 系统测试、02 单元与集成测试、06/06b/07/08/08c/08d 文档域闸、10 熵增闸全绿;09 静态扫描退出码 1=存量红(`game/presentation/views/level_map_screen.gd|quality/max-class-variables` 22 对基线登记 21 的恶化留账,还债归迭代 41 在案;指纹 `8467b4588a20` 与入库基线一致,本批零新增);00 冒烟、03/04 探针、05 渲染闸、11 回放确定性由影响面选闸判 SKIP(本批 diff 全在 `game/tests/` 测量面)。 + +**本批复验缺口一条(合回后已补)**:第 18/19 两行改的是冒烟宿主自身 `smoke_boot.gd`,而 00 冒烟测试恰被影响面选闸跳过——该两站点在批末跑批中没有执行证据,只有静态蕴含论证(GDScript `and` 短路,保留条 `!= null and …` 在空值上判 false 不崩)。合回 master 后按 §3.9 code 批两件秒级事跑 `python tools/run_smoke.py`=三闸全过、`[smoke] 摘要:15 项检查,0 项失败`(原 17 项检查删两条后为 15 项计数),缺口就此闭合。第 1–17 行所在套件的复验由 01/02 全绿承担。 + +跑批踩坑一条备案:闸日志首跑写在仓库根,触 10 熵增闸「根目录文件越界」判红——我的临时件写错位置,非批次改动引发;日志挪到仓库外重跑后 10 归绿,代码面零改动,回执按同一 commit 重生成。 + +## 五、移交与观察(后续批次账) + +1. **类别② 在本仓基本不成立**:全仓钉内部路径/私有字段/中间态的断言约 52 条,被入口**严格承载 0 条**,约 51 条因覆盖入口打不到的边界/反例载荷必须保留(`fx.dispatch` 逐事件、`pending_ap_delta` 兑现时机、`_intent_charge`);且 §8.4 本身要求判据绑提交面。后续批次的可精简空间只在类别①。 +2. **前置护栏不可删**:`load_ok and diagnostics_count("error")==0` 全域 8 处属每进程 fail-fast,删致 `if load_ok:` 静默跳过→假绿,属负收益。 +3. **静默通道在冻结口径之外**:`Kit.check_quiet` 静态站点实测 **407 个**,提取器 `CALL_RE = check\(` 匹配不到该形态(test-conventions:38 已备案该工具局限)——清单条数与〔尾缀〕grep 均覆盖不到。因此「循环 quiet 与聚合条双钉」两处真冗余(node_map_growth 47/51 vs #1073/74、node_map_generator 163 vs #fv9 聚合条)删的是口径外站点,不减 1604 这个数,且聚合侧带〔fv8〕〔fv9〕验收尾缀,本批不动;若要收该账,先做提取器改版(工程批)。 +4. **恒真断言另立一账**(属判据缺陷,非重复,本批未动):feat_vouchers:51 `deck_refresh_before == 0 or true`、treasure_effects:263 `... or true`、level_shop_economy:593 与 :696 `green_min_hp > 0`(初值 99999、仅 hp>0 分支取 min,恒真);system_contracts:67 标签称「互异」而 cond 只查非空,属名实不符需改判据。建议按 §8.4 走「判据绑不上即停卡呈报」通道单独裁决。 +5. **未采的删并形态**:C 档「手写族→表驱动循环合并」(level_gen_params 逐类负向 10→1、rng_stream_boot 8 向量→1、player_copy 禁词族、standee_fx 三入参、ui_font 字形集、forge_workbench 双行)静态可再省约 25~35 点,代价=失败定位改用违例明细串(仓内已有 `forge_table_test:181-189`、`table_structure_guard:260/299` 范式);D 档「是否同一验证项目」待owner 认账项(theme:215/217 vs type_glow:148/150、level_shop_economy 现行图跨表覆盖簇 vs 〔v11〕扩面段的尾缀义务)。 +6. **example_test.gd** 两条硬编码占位断言在盘且随清单在册(旧 #S167),不进生产 manifest(run_all.gd:79-80 明示)——整件退役与否属样板件裁决,不入本账。 +7. 本批不动 `game/tests/README.md`(断言双口径行仍成立)与任何 `design/` 文件。 diff --git a/tools/doc_link_audit.py b/tools/doc_link_audit.py index 9701464f..d9a67e4e 100644 --- a/tools/doc_link_audit.py +++ b/tools/doc_link_audit.py @@ -23,8 +23,8 @@ r"""doc_link_audit.py — design 文档链接健康检查 python tools/doc_link_audit.py ... # 只扫指定文件 python tools/doc_link_audit.py --json # 机器可读 -口径:只扫 design/gdd/——design/art、reports/ -(历史素材/提案)不在扫描域,但作为链接目标被校验。排除 ``` 围栏代码块与 ; 开头注释行。 +口径:扫 design/gdd/ 与 design/art/(2026-10-28 美术族成文整备批收编)——reports/ +(提案过程件)不在扫描域,但作为链接目标被校验。排除 ``` 围栏代码块与 ; 开头注释行。 """ import argparse import json @@ -32,7 +32,7 @@ import re import sys from pathlib import Path -DEFAULT_DIRS = ["design/gdd"] +DEFAULT_DIRS = ["design/gdd", "design/art"] PROTO_PREFIX = "game" ALLOWED_EXTS = ("md", "yaml", "yml", "tres", "gd", "py", "json", "csv") diff --git a/tools/gdd_lint.py b/tools/gdd_lint.py index 3822258f..cc5de573 100644 --- a/tools/gdd_lint.py +++ b/tools/gdd_lint.py @@ -2,7 +2,7 @@ r"""gdd_lint.py — GDD lean 格式门禁 用法: - python tools/gdd_lint.py # 检查 design/gdd/ 根层 GDD + roadmap.md(默认范围) + python tools/gdd_lint.py # 检查 design/gdd/ 根层 GDD + design/art/ + roadmap.md(默认范围) python tools/gdd_lint.py --changelog-only # 只跑 R4 变更记录节禁令,扫描面扩到全 design/ 树 python tools/gdd_lint.py ... # 检查指定文件 python tools/gdd_lint.py --json # 机器可读输出(供 gate/CI 使用) @@ -45,6 +45,13 @@ RULE_EXEMPT = { "术语表.md": {"R5"}, } +# 美术域收编(2026-10-28 美术族成文整备批):design/art/ 为高密度规格/清单/提示词操作单体裁, +# 体裁类规则(R1 段长/R5 流程图/R7 括号跨度/R8 规则表/R9 节点行数)不适用、豁免防误报; +# R11 的锚表是 GDD 行协议/格面资源专属计数,对美术域同样不适用。 +# 跨体裁有效面照常执法:R3 修订注记、R4 变更记录节、R6 表格列数。 +ART_DOMAIN_EXEMPT = {"R1", "R5", "R7", "R8", "R9", "R11"} +ART_DIR = "design/art" + DEFAULT_DIR = "design/gdd" DEFAULT_EXCLUDE = {"game-concept.md", "game-pillars.md", "systems-index.md"} # R4 变更记录节标题(2026-10-19 用户裁「沿革只进 git」——原「必须有版本历史节」反转为此禁令, @@ -171,7 +178,11 @@ def lint_file(path): # R1 段落长度 fname = Path(path).name ex = RULE_EXEMPT.get(fname, set()) # 规则粒度分域豁免(见 RULE_EXEMPT 注) + if str(path).replace("\\", "/").startswith(ART_DIR.replace("\\", "/") + "/"): + ex = ex | ART_DOMAIN_EXEMPT for p in paras: + if "R1" in ex: + break if len(p) > MAX_PARA: issues.append(f"R1 散文段 {len(p)} 字(>{MAX_PARA}): {p[:50]}…") @@ -249,6 +260,7 @@ def lint_file(path): # R11 计数词漂移对照(锚表 CURRENT_COUNTS;轨迹措辞行豁免——变更记录节本身归 R4 禁设) in_code11 = False + r11_active = "R11" not in ex for raw in text.splitlines(): stripped = raw.strip() if stripped.startswith("```"): @@ -256,6 +268,8 @@ def lint_file(path): continue if in_code11 or stripped.startswith(";") or R11_TRACE_RE.search(raw): continue + if not r11_active: + continue # yaml/yml 的 # 行内注释是登记轨迹,不入正文扫描 line = raw.split("#", 1)[0] if str(path).endswith((".yaml", ".yml")) else raw for pat, fix in CURRENT_COUNTS: @@ -296,6 +310,10 @@ def main(): if p.name not in DEFAULT_EXCLUDE and not re.search(r"review|report|handoff", p.name, re.I) ) + targets += sorted( + p for p in Path(ART_DIR).glob("*.md") + if not re.search(r"review|report|handoff", p.name, re.I) + ) if not targets: print(f"未找到待检文件({DEFAULT_DIR}/)", file=sys.stderr) sys.exit(2) diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py index fe707606..57224f7b 100644 --- a/tools/pipeline_gate.py +++ b/tools/pipeline_gate.py @@ -52,6 +52,8 @@ LEDGER_RULE_DATE = (2026, 10, 21) LEDGER_HEADING_RE = re.compile(r"^#{2,4}\s*.*(验收对照表|改动[—\-]验收)") LEDGER_REQUIRED = ("需求点", "改动", "验收测试", "真实入口") LEDGER_PROOF_MARKS = ("撤销", "证明") +BINDING_RULE_DATE = (2026, 10, 28) # req-card §8:换代为判据绑定面列,撤销/变异证明列同日禁列 +BANNED_PROOF_MARKS = ("撤销证明", "变异证明") AUDIT_MARK = "验收审计" DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}") @@ -451,7 +453,7 @@ def card_date(path): return tuple(int(x) for x in found[-1].split("-")) -def check_ledger(text, path, issues): +def check_ledger(text, path, when, issues): """C3 改动—验收对照表结构检查(生效 2026-10-21)。 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。 @@ -491,13 +493,29 @@ def check_ledger(text, path, issues): "缺「%s」列" % name)) return idx[name] = hit - proof_hit = next((k for k, cell in enumerate(header) - if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None) - if proof_hit is None: - issues.append((str(path), header_line, "验收对照表缺列", - "缺「撤销证明」列")) - return - idx["证明"] = proof_hit + banned = next((cell for cell in header + if any(mark in cell for mark in BANNED_PROOF_MARKS)), None) + binding_hit = next((k for k, cell in enumerate(header) + if "判据绑定面" in cell), None) + if when >= BINDING_RULE_DATE: + if banned is not None: + issues.append((str(path), header_line, "验收对照表禁列", + "「%s」列已随 2026-10-24/28 两裁禁列(req-card §8.4," + "验收断言绑真实入口+判据绑定面)" % banned)) + return + if binding_hit is None: + issues.append((str(path), header_line, "验收对照表缺列", + "缺「判据绑定面」列(2026-10-28 判据口径换代,req-card §8)")) + return + idx["绑定面"] = binding_hit + else: + proof_hit = next((k for k, cell in enumerate(header) + if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None) + if proof_hit is None: + issues.append((str(path), header_line, "验收对照表缺列", + "缺「撤销证明」列")) + return + idx["证明"] = proof_hit rows = 0 for i in range(header_line, len(lines)): @@ -531,7 +549,8 @@ def check_cards(root, issues): C1 判据:「关联文档同步面」表内,目标格指向 design/ 或根规则文件、而义务格仍写 「随批」者红(2026-10-17 req-card §6 改口径后,这类点只能列 reports/ 反哺点清单)。 C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。 - C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口/证明列; + C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口+证明列 + (2026-10-28 起换代为「判据绑定面」列,并禁「撤销证明/变异证明」列); 至少一行;必填格非空;全文含「验收审计」声明。语义覆盖不在此判。 """ dirn = root / CARD_DIR @@ -550,7 +569,7 @@ def check_cards(root, issues): "须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一," "GDD 节+版本号/样张+art-bible 版本/工程形态证据指认)")) if when >= LEDGER_RULE_DATE: - check_ledger(text, path, issues) + check_ledger(text, path, when, issues) if AUDIT_MARK not in text: issues.append((str(path), 1, "独立验收审计未声明", "须在卡内写明独立验收审计入口与报告文件(2026-10-21 起," diff --git a/tools/undo_proof.py b/tools/undo_proof.py deleted file mode 100644 index 071fd430..00000000 --- a/tools/undo_proof.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -"""撤销证明自动化工具:一条命令完成「改前绿 → 真撤销复红 → 还原复绿」三连并出回执。 - -撤销证明的判据(test-conventions §8.4):只撤销该改动、跑能覆盖它的最窄测试、对应断言 -必须变红、还原后复绿;撤销后仍绿=断言没钉在改动上,判缺漏。本工具把这三连机械化—— -合法性属于「真撤销」(回退本批真实改动),全项目禁做变异类测试(改断言极性、置空状态、 -改名键、删调用行等人为制造红=伪造证据)。 - -用法: - python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-commit <该批提交> - python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-files --base - 可选 --out 落回执;--timeout <秒> 限制单次测试命令时长(缺省 3600)。 - - --revert-commit:按该笔提交触过的文件集回退到其父提交版本(改动须已提交在分支上)。 - --revert-files+--base:把指定文件回退到 (默认 HEAD)版本——适合改动尚未单独 - 成笔、混在当前尖里的场景。 - 测试命令在仓库根以 shell 执行;单套件直跑形态见 game/tests/README.md(测试工具手册)。 - -流程与退出码: - ① 改前基线:测试命令必须绿(红=改前就红,无证明意义,中止)。 - ② 真撤销:指定文件回退到基准版本 → 重跑命令,必须红(红=断言钉在该改动上)。 - ③ 还原:文件恢复到 HEAD → 重跑命令,必须绿。 - ④ 任何一步异常都先还原再退出;三步全过=退出码 0 并打印回执,否则退出码 1。 -""" - -from __future__ import annotations - -import argparse -import json -import subprocess -import sys -from pathlib import Path - -REPO_DEFAULT = Path(__file__).resolve().parent.parent -REPO = REPO_DEFAULT - - -def run(args: list[str], check: bool = True) -> str: - r = subprocess.run(["git", "-C", str(REPO)] + args, - capture_output=True, text=True, encoding="utf-8", errors="replace", - timeout=60) - if check and r.returncode != 0: - sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip())) - sys.exit(1) - return (r.stdout or "").strip() - - -def run_cmd(cmd: str, timeout: int) -> int: - r = subprocess.run(cmd, shell=True, cwd=str(REPO), capture_output=True, - text=True, encoding="utf-8", errors="replace", timeout=timeout) - if r.returncode != 0: - tail = "\n".join((r.stdout or "").splitlines()[-15:] or ["(无输出)"]) - sys.stderr.write(" | 测试命令失败(exit=%d),输出尾部:\n%s\n" % (r.returncode, tail)) - return r.returncode - - -def _in_rev(rev: str, f: str) -> bool: - r = subprocess.run(["git", "-C", str(REPO), "cat-file", "-e", "%s:%s" % (rev, f)], - capture_output=True, timeout=30) - return r.returncode == 0 - - -def apply_rev(rev: str, files: list[str]) -> None: - # 逐文件:基准版本存在 → 恢复到该版本;基准没有(该批新增的文件)→ 从工作区删除。 - for f in files: - if _in_rev(rev, f): - run(["checkout", rev, "--", f]) - else: - subprocess.run(["git", "-C", str(REPO), "rm", "-q", "-f", "--", f], - capture_output=True, text=True, timeout=30) - - -def main() -> None: - ap = argparse.ArgumentParser(description="自动三连出撤销证明红绿回执") - ap.add_argument("--cmd", required=True, help="最窄测试命令(shell 形,仓库根执行)") - ap.add_argument("--revert-commit", default=None, help="该批提交:按其触过的文件集回退到父版本") - ap.add_argument("--revert-files", default=None, help="逗号分隔的文件清单(与 --base 配用)") - ap.add_argument("--base", default="HEAD", help="回退基准版本(默认 HEAD)") - ap.add_argument("--timeout", type=int, default=3600, help="单次测试命令时长上限(秒)") - ap.add_argument("--repo", default=None, help="仓库/worktree 路径(缺省=本脚本所在仓库)") - ap.add_argument("--out", default=None, help="回执 JSON 落盘路径") - args = ap.parse_args() - - global REPO - REPO = Path(args.repo).resolve() if args.repo else REPO_DEFAULT - - try: - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") - except Exception: - pass - - if bool(args.revert_commit) == bool(args.revert_files): - ap.error("--revert-commit 与 --revert-files 二选一") - - if args.revert_commit: - files = [x.strip() for x in run( - ["-c", "core.quotepath=false", "show", "--name-only", "--format=", - args.revert_commit]).splitlines() if x.strip()] - base = "%s^" % args.revert_commit - if not files: - sys.stderr.write("[撤销证明] 提交 %s 未触碰任何文件,无可撤销\n" % args.revert_commit) - sys.exit(2) - else: - files = [x.strip() for x in args.revert_files.split(",") if x.strip()] - base = args.base - - dirty = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files) - if dirty: - sys.stderr.write("[撤销证明] 指定文件带未提交改动,撤销与还原的基准不清,先提交:\n%s\n" % dirty) - sys.exit(2) - - steps: list[dict[str, object]] = [] - result = "pass" - - def step(name: str, expect: str, code: int) -> bool: - ok = (code == 0) if expect == "绿" else (code != 0) - steps.append({"step": name, "expect": expect, "exit": code, "ok": ok}) - print("[撤销证明] %s:期望%s,实测 exit=%d → %s" - % (name, expect, code, "符合" if ok else "不符合")) - return ok - - print("[撤销证明] 对象文件 %d 个;基准 %s;命令:%s" % (len(files), base, args.cmd)) - try: - if not step("① 改前基线", "绿", run_cmd(args.cmd, args.timeout)): - result = "fail(改前就红——先修绿再做撤销证明)" - else: - apply_rev(base, files) - try: - if not step("② 真撤销复红", "红", run_cmd(args.cmd, args.timeout)): - result = "fail(撤销后仍绿=断言没钉在该改动上,判缺漏)" - else: - apply_rev("HEAD", files) - if not step("③ 还原复绿", "绿", run_cmd(args.cmd, args.timeout)): - result = "fail(还原后未复绿——还原面有残留,查工作区)" - finally: - apply_rev("HEAD", files) - except subprocess.TimeoutExpired: - result = "fail(测试命令超时)" - finally: - # 兜底还原:无论走到哪一步,退出前把对象文件恢复到 HEAD 态 - apply_rev("HEAD", files) - - residue = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files) - if residue: - result = "fail(退出前工作区仍有对象文件残留改动,手工核对)" - - print("─" * 56) - print("[撤销证明] 结论:%s" % result) - if args.out: - with open(args.out, "w", encoding="utf-8") as f: - json.dump({"cmd": args.cmd, "base": base, "files": files, - "steps": steps, "result": result}, f, ensure_ascii=False, indent=1) - print("[撤销证明] 回执已落 %s" % args.out) - sys.exit(0 if result == "pass" else 1) - - -if __name__ == "__main__": - main() diff --git a/待办清单.md b/待办清单.md index 71d3026b..f8834e34 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,14 +2,30 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 存量断言内部直查全量归位——判据达标专项批(2026-10-28 登记,来源=会话提及) +#### 验证与清单体系整改——四方向分阶段推进(2026-10-28 登记,来源=会话提及) -**实况**:目标=unit/integration 存量断言级内部直查 89 处(grep 口径,2026-10-28 本会话实测)按 test-conventions §8.2 新判据(145c1865 落文)全部改钉公开可观测面。证据=§8.2 受控例外条文+本会话 grep 计数;样板=treasure_effects_test(e62e90c8 试点批,12 处改钉零产品码全绿)。执行形态=工程改动 worktree 批(纯测试维护零行为改动,不写卡不编迭代):盘点先行(逐套件定四类改法——属性直换/公开面等价式/真实路径重塑/公开面对比——并标出需产品码只读访问器的呈报项)→ 按域分波每波闸 → 收尾件核(断言级直查 grep=0)。边界:只清断言级(`Kit.check` 行内 `._` 命中),setup 直调写口构造状态不动(§5.4 惯例);断言标签逐字不动(验证集清单锚);禁放宽/删断言;需访问器项逐处呈用户裁(产品码边界);非 quiet 断言改判同步验证集清单对账。时机=排在迭代 41 之前,与波 0 gdstyle 基线刷新互不影响(tests 不在扫描面)。 +**实况**:用户裁「验证及清单不合理」,经调研定四方向全做、分阶段推进:A 粒度整改(清单从断言级升到能力/行为级,逐断言账本退役)→ B 精简裁断(在新粒度上规则驱动删并)→ C 流程风险分级(P0/P1/P2 验收强度、审计抽样)→ D 文档减肥(待办条目限长、历史归 git);测试有效性度量暂不处理。阶段 0 盘点已毕:验证集清单零机器闸依赖(run_all_gates/hooks/五技能均不引用,仅 test-conventions §8.3 维护锚+2 份过程报告引用),退役无闸改动;但测试名尾缀仅覆盖 28/47 个有断言文件(19 个零尾缀,含 seal_polarity 69 条、variant_placement 42 条、roll_multiplier 35 条),故能力清单只能按「能力→代表用例」建,不要求逐断言尾缀。对照表列改动触 pipeline_gate+req-card/iter 两技能;待办正文压缩不触机检(无长度判据)。盘点明细见 reports/盘点-验证与清单体系-2026-10-28.md。**阶段 1 设计已闭合(2026-10-28 用户四裁点)**:能力清单 v1 落 `game/tests/README.md`——按系统归属与玩家旅程排序,首期核心四域(存档与随机、冒险循环、战斗结算、奖励与经济)29 行+跨域条件闸 1 行,共 30 行,每行真实入口+2–3 个代表用例(`文件:函数`),维护规则为每批收口核对本批受影响行并刷新「最后验证批次」;`design/engineering/test-conventions.md` §8.3 维护锚改写为「验证集清单整册冻结、能力清单承接覆盖索引、阶段 2 精简后删除」;`game/tests/验证集清单.md` 头注同步冻结声明,冻结快照(基点 `b9260138`/1587 条)留作阶段 2 对账基准;`tools/extract_checks.py` 保留为审计工具,不再承担常态同步。**阶段 2 已执行闭合(2026-10-28 C 档合回)**:存量断言精简 C 档 6 组手写断言族并为 7 个表驱动站点(33→7),冻结清单对账 1587→1561,验证集清单随阶段 2 完成退役(覆盖索引归能力清单,维护锚 test-conventions §8.3),删并申报与件核见 reports/工程改动报告-存量断言精简C档-2026-10-28.md;阶段 3–4 待各自设计推进:风险分级与审计抽样规则、条目限长是否入 pipeline_gate 均未定。 + +**改动面**:文档+游戏代码+工具代码 + +**状态**:文档设计已闭合 + +#### 恒真与名实不符断言五条——判据缺陷修正(2026-10-29 登记,来源=会话提及) + +**实况**:存量断言精简 A+B 批余账④转正:五条断言属判据缺陷(恒真占位或名实不符),不是冗余合并可解的账,按 test-conventions §8.4 真实入口口径逐条修正,不计入精简条数。逐条:①`game/tests/unit/core/feat_vouchers_test.gd:51` cond 为 `deck_refresh_before == 0 or true`——恒真;②`game/tests/unit/core/treasure_effects_test.gd:263` cond 尾缀 `or true`——恒真;③`game/tests/unit/core/level_shop_economy_test.gd:593` `green_min_hp > 0`——初值 99999、仅 hp>0 分支取 min,恒真;④同文件 `:696` `green3_min_hp > 0`——同型恒真;⑤`game/tests/unit/contracts/system_contracts_test.gd:66-67` 标签称「非空且互异」而 cond 只查 `size() > 0`——互异半缺失,名实不符。修正方向:能绑判据的改绑真实可观测面(互异改值集去重计数、HP 读数改与门槛比对),造不出可观测面的按 §8.4 出口停卡呈报。 **改动面**:游戏代码 **状态**:待执行 +#### 断言提取器改版——覆盖 Kit.check_quiet 静默通道(2026-10-29 登记,来源=会话提及) + +**实况**:`tools/extract_checks.py:67` 的 `CALL_RE = check\(` 只认即时通道,静默收集通道 `Kit.check_quiet(cond, desc)` 静态站点实测 407 个在提取口径外(证据=`grep -rn "check_quiet(" game/tests --include="*.gd"` 实数;test-conventions §5.2 已备案该工具局限)。能力清单以 `文件:函数` 为锚,不受影响;但要把断言面全账纳入机器对账(重建账本或全量断言面审计),须先改版提取器:识别 `check_quiet(` 并按反参序(cond 在前)提取标签与 cond。改版后须以全量套件跑一遍 `--diff` 自证提取面覆盖、且 `Kit.check(` 与 `check_quiet(` 不双计。 + +**改动面**:工具代码 + +**状态**:待执行 + #### gdstyle 存量债全量清偿——216 键按病根分波整改(2026-10-26 登记,来源=会话提及,优先级=P0) **实况**:S13 闸实测存量债 216 键/60 文件(2026-10-26,master f3b148ce,gdstyle v0.4.0 默认阈值;基线登记 234 键=实存 216+陈旧 18)。全量清单、波次方案与纪律见清点报告(本条目裁决源)。2026-10-26 用户裁:撤销「一条总账」与「按病根拆多条」两案,并为本条单账(存量债全部清除,紧急优先级);原「gdstyle 基线陈旧键待人工清理」条目并入波 0(基线对账刷新),原「结构审查未立项小件候审」条目并入波 7(卡已写;iter38 施工停卡后经用户裁于 2026-10-26 修订定稿 v1.3,待编排)。**2026-10-26 设计段闭合(逐波裁定全数落定)**:①段组织=逐波开设计段,纯代码波(波 0/2/3/4/5/6/8)写卡前确认零设计改动+按当时 master 重测读数即可写卡;②波 2 各文件内部拆、波 6 视图内部拆私有函数、波 8 config_db 纯代码收敛(三处零设计改动);③波 1 拆件定稿=轴甲两件——battle_orchestrator 拆出 BattleFxHost(效果引擎宿主域)+RewardGrant(奖励授予域),宿主保留状态机/facts 缓冲/订阅/段驱动/行发射基建/呈现投影,attune 族原位保留(迭代 39 副卡语义领地),emit 经 bind 注入 Callable,行数恶化债(c4c69701 挂账)本波还债归绿,残留债「改善非清零」口径预计 8–10 键;定稿落文 architecture.md v1.18(功能层两新件行+「战斗编排域拆件定稿」节);**同日独立审查折入**(grilling+godot-master 双子代理并行、双判回炉补裁级——骨架成立、接线未闭合):阻断 8 条去重 6 组+建议 5 条全数折入 v1.18——跨件单向接线裁死(RewardGrant 持 fx 件引用/fx 兑付走注入 Callable,禁 RefCounted 环)、settlement/ 段类随动(直持三引用封转发壳)、12 随迁键卡内收敛义务(禁波 0 refresh 吸收)、两新件消费依赖列补全+引用束注入、treasure_interact_controller 三触点分家(现值回调双取值面)、_fx_revealing 再入闸归 fx 件(裁定全文见清点报告 §六第 6 条);④全部波不改游戏规则与玩家可见行为,design/gdd/systems/ 零修订预期;波 5 已实查落定纯代码(open_ticket 12 参=单据声明形态)。波 7 的 wr5/wr6 验收载体重裁为写卡段用户裁点(卡 v1.2 在册,写卡补呈报)。裁定全文见清点报告 §六。**2026-10-28 迭代 40 合回出账(波 1+波 7 闭合,合并提交 514457d1)**:卡①(波 1)45 键清 44、宿主类变量键豁免第三条顺带落 architecture v1.22 S13 豁免登记,行数恶化债还清(实测 1036≤登记 1302;契约 2 残余 36 行系合并适配承接 master 迭代 39 内容带入,用户裁登记残余债归后续批);卡②(波 7)六件定点修+运行期违约通道负面套件(run_time_fatal_test)落地合回。余波 2/4/6/8 归迭代 41 两卡(在册)。 @@ -247,7 +263,7 @@ #### 棋盘格边长三方取值不一致——待扩板定档处置(2026-10-06 登记,来源=批次移交) -**实况**:扩板已落地(四档板型 10/12/13/14 在跑),2026-10-26 文档面已按单点维护归一:①格边长口径的唯一出处收敛到 [战斗屏规格](design/gdd/ux/battle.md)「棋盘格尺寸公式」——公式含格间距与垂直预留,可读下限 30px,1280 档算例 43/36/33/30 标注为随布局变化的非契约值;②[美术圣经](design/art/art-bible.md) §8.1 改为指路 battle.md、只锁资产侧两条比例,§8.2 甲类整数倍纪律已剔除砖面 tile(瓦片随格边长整格铺满,允许非整数倍);③[HUD框架](design/gdd/systems/表现与支撑/HUD框架.md) 只留装板可行性不等式,删去 36/45 定值档与旧算例;④demo 原型 44px 那一路随 `src/` 退役消失。剩余为代码面两处与口径不同步:`game/probe/layout_probe.gd:15` 的格子可读门槛 `MIN_CELL := 36.0` 仍取 12×12 的旧锚点值,比 battle.md 的可读下限 30px 严——现探针只进 10×10 的 L1 故仍全绿,一旦覆盖 13×13(33px)与 14×14(30px)即会误报红;`game/tests/unit/core/board_gen_reward_test.gd:53` 的格边长冒烟只跑 L1/L2,13×13 与 14×14 无覆盖。断言改动后须同批重生成 `game/tests/验证集清单.md`(`tools/extract_checks.py` 手工触发)。 +**实况**:扩板已落地(四档板型 10/12/13/14 在跑),2026-10-26 文档面已按单点维护归一:①格边长口径的唯一出处收敛到 [战斗屏规格](design/gdd/ux/battle.md)「棋盘格尺寸公式」——公式含格间距与垂直预留,可读下限 30px,1280 档算例 43/36/33/30 标注为随布局变化的非契约值;②[美术圣经](design/art/art-bible.md) §8.1 改为指路 battle.md、只锁资产侧两条比例,§8.2 甲类整数倍纪律已剔除砖面 tile(瓦片随格边长整格铺满,允许非整数倍);③[HUD框架](design/gdd/systems/表现与支撑/HUD框架.md) 只留装板可行性不等式,删去 36/45 定值档与旧算例;④demo 原型 44px 那一路随 `src/` 退役消失。剩余为代码面两处与口径不同步:`game/probe/layout_probe.gd:15` 的格子可读门槛 `MIN_CELL := 36.0` 仍取 12×12 的旧锚点值,比 battle.md 的可读下限 30px 严——现探针只进 10×10 的 L1 故仍全绿,一旦覆盖 13×13(33px)与 14×14(30px)即会误报红;`game/tests/unit/core/board_gen_reward_test.gd:53` 的格边长冒烟只跑 L1/L2,13×13 与 14×14 无覆盖。断言改动后按 `game/tests/README.md`「能力清单」核对受影响行(验证集清单已退役——test-conventions §8.3)。 **改动面**:游戏代码 @@ -295,17 +311,6 @@ **状态**:待写卡 -#### 圣经口号型条款机检化批(2026-10-06 登记,来源=审计发现) - -**实况**:审计 P2-4——圣经约 55% 条款属口号型(如「暖倾向无压抑」「同一圆角率」无值),与可机检条款混排。目标=逐条审查标注「可机检/目检/口号」三档分行,口号型逐步改写为带值或带判据条款。工程量=全文级(541 行逐条),非一轮能毕。**2026-10-06 三档标注格式已拍板**(←机检/←目检/←方向性,规范见 art-bible 头注「条款验收标注纪律」;数值类推荐「待定现用」态)——本批执行=按此格式分章补标,每章交付用户抽验。挂账:随整屏样张闸前整备批启动,分章推进。 - -**缺口**:还差整屏样张闸前整备批的启动——格式已定,差启动时点与分章执行。 -- 相关文档:[art-bible](design/art/art-bible.md) 全文 · 样张批前置整备 - -**改动面**:文档+工具代码 - -**状态**:待文档设计(备选) - #### 战斗区背景图替换——B 区舞台插画 + 临时光柱退役(2026-10-07 登记,来源=会话提及) **实况**:B 区舞台现行=程序化平色底(`UiKit.STAGE_BG`,`battle_stage_view.gd:47`)+ **顶光柱氛围层**(`:55-63` 四柱,x 基准 110/900/1055/1210@1280,60 段叠层矩形自顶向下渐宽窄渐隐〔顶 α26→0〕,层序=bg < 光柱 < 世界)——二者均系 §6.1 方向稿的**临时占位**,用户口径「光柱只是临时方案,换正式战斗背景后效果更好」。正式件已立资产契约:`stage/stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张),属审计 P1-2 查得的「B 区舞台背景插画 0 张」缺口。**光柱在背景到位后顺带退役或吸收进背景绘制**(不再作为独立程序层)。**与立绘探顶解耦**:大体档「允许向上溢出盒顶 N px」的判定(2026-10-07 用户裁甲案)**不依赖光柱是否存在**,换背景零返工。 @@ -397,33 +402,23 @@ **状态**:待写卡(备选) -#### 闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时(2026-10-25 登记,来源=批次移交) +#### tests/README manifest 计数行漂移订正——「64 条」对实际 65 条(2026-10-28 登记,来源=批次移交) -**实况**:证据=迭代 36 批末闸机观测(非本批引入)。原登记两件存量噪声,件①(battle_log_view.gd:17 类型声明错致运行期 TypedArray 错误、日志失真)已随迭代 38 卡⑥ 收口出账(git 提交 ce49a8ee),本条只剩件②:`level_shop_economy_test` 在闸批量上下文存在挂死形态(直跑恒绿),`tools/probe_gate.sh` 的 per-suite timeout 只杀 wrapper 不杀引擎子进程,孤儿泄漏会殃及后续全部 Godot 启动(批末四次闸跑批中断的根源,处置=清杀泄漏进程+删重建 `game/.godot`)。挂死防护=闸壳/测试基建,按改动四类属**工程改动走直改通道**。 -- 直改落点:tools/probe_gate.sh(per-suite 进程树回收)+game/tests/run_all.gd(OS.execute 阻塞面超时守护)——测试基建直改。**2026-10-28 用户裁四批全做,probe_gate 件已落**(run_suite 整树回收:启动记 WINPID → 超时先 `taskkill /T` 杀全树再 `kill -9` 收 MSYS 侧〔顺序颠倒会让 /T 走不到已死父进程〕→ 标志文件判超时〔taskkill 强杀后 MSYS wait 可能报 0〕;默认五段实跑全绿+超时/正常双路径机制验证通过)。run_all.gd 件属代码面(game/tests),按车道铁律留待触及测试基建的 worktree 批顺带,本条目保持待执行。**2026-10-28 迭代 40 批末补实证(阻塞面挂死实拍)**:迁移类改动的回退使测试 `_init` 在 `quit()` 前中断(红行形态 `Invalid call. Nonexistent function`),进程挂至外层超时 124——卡① wr1/wr4/wr9 取证轮三例复现,与件②同根(阻塞面无超时守护),实证链在案(实现报告 §三第 1 条)。 +**实况**:证据=单元闸挂死守护批收尾实数(合并提交 4afe6c47,报告 reports/工程改动报告-单元闸挂死守护-2026-10-28.md)——`game/tests/run_all.gd` manifest 实为 65 条(62 单元+3 集成,grep '{"path"' 计数),`game/tests/README.md`「套件 manifest 现况」行仍记「64 条(61 单元+3 集成,2026-10-28 实数)」。漂移先于该批存在(主区与任务分支同值),该批未动套件数不属其 scope,登记归文档批订正。 - -**改动面**:工具代码 +**改动面**:文档 **状态**:待执行 -#### undo_proof.py 退役——撤销证明禁令生效后的死工具(2026-10-28 登记,来源=批次移交) +#### 主角棋盘小人像素管线重产——原「美术资产链路审计两项 P0」之 P0②(2026-10-09 登记,来源=审计发现) -**实况**:证据=test-conventions §8.4 现行口径(2026-10-24/2026-10-28 用户两裁:禁变异类测试+禁撤销类取证,验收唯一主判据=真实入口断言当轮执行并通过;AGENTS.md 红线「验收闭合」与 req-card SKILL 第 8 条同批换代)。`tools/undo_proof.py`(159 行,挂死防护批产物)为撤销证明的自动化载体(三连红绿回执并自还原),服务对象已被禁令整体废止——工具本体退役删除。同族残留顺带裁:`.zcode|.qoder/skills/iter/evals/files/mock-card-iter40-1.md` 表头「撤销/变异证明」字样(两份,反向残留);`tools/pipeline_gate.py` 是否加「变异测试/撤销证明」短语级扫描防复发(**短语级**——按「变异」单词扫会命中地形变异等玩法词,实测假红面大)。 +**实况**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册;审计报告 HTML 件从未进版本库、现已删除,现状以本条正文为准。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。原 P0①(文档面:两处引用已丢失的冻结样张路径)2026-10-28 按用户裁乙销号——art-bible §1.4 冻结记录与 art-status-checklist P0 样张行删路径、留「产物原件已丢失」,机检报告凭据(git 提交 7a847c7)保留。 -**改动面**:工具代码 +**缺口**:还差美术面的重产(按像素管线重画主角棋盘小人),归美术设计段。 -**状态**:待执行 +**改动面**:美术素材 -#### 美术资产链路审计两项 P0 — 待用户裁决(2026-10-09 登记,来源=审计发现) - -**实况**:**P0①(文档面)**:`design/art/art-bible.md` 第 68/527 行与 art-status-checklist 第 13 行至今仍引用已丢失的冻结样张 sample-batch-v3-final-2026-09-30.png(磁盘实测零命中),死指涉待修复。**P0②(美术面)**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册。审计报告 HTML 件从未进版本库、现已删除,两项 P0 的现状以本条正文为准。裁=修复死指涉+登记重产,或判过期随旧管线退役。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。 - -**缺口**:还差用户的路线裁决(修复死指涉+登记重产,还是判过期随旧管线退役)——裁决后文档面修复与美术重产登记各按轨推进。 - -**改动面**:文档+美术素材 - -**状态**:待文档设计+待美术设计 +**状态**:待美术设计 #### 技术债观察登记——长函数余量与 lambda 连接防回归(2026-10-15 登记,来源=批次移交)