iter41-卡2:呈现层结构收敛批:25文件71键——67清+2豁免在册(main类变量49=architecture:183、tune_sim await=:311测量面)+2键inner class展平计数在案改善(board_cell_unit 31→30、standee_body 29→19);视图拆私有函数族+六状态件收拢(_TreeState族/_MapState/_CellPaintState/_StandeeState/_EndOverlayState/_DebugState)+ui_kit公开面26→20(删2私4哨兵收拢)+flow context三束收拢+curio_interact引用束注入+print×3删除;含u14补做、并master冲突6文件并案与无点形态改址盲区20处修复

This commit is contained in:
DKChan committed 2026-10-28 23:51:46 +08:00
1 parent 41a1959305
commit b31d7de8b6
79 files changed
+4307 -3770

No files matched your search

+1 -1
View File
@@ -63,7 +63,7 @@
> **样张 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)。
> **样张 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 冻结不变。产物原件已丢失(未入 git、主工作区不可查),机检报告=git 提交 7a847c7【冻结】;气质终审=2026-09-29 用户验收豁免(新 v3.5 方向稿验收通过,状态记录 281)。
---
+1 -1
View File
@@ -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 角色级(样张通过后开工)
+1 -1
View File
@@ -54,7 +54,7 @@
11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter<N>-反哺点-<日期>.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. 测试执行两入口铁规(细则)
+1 -1
View File
@@ -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/ 五个循环
+2 -2
View File
@@ -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) | 在役 |
+1 -1
View File
@@ -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 # 基础挖格成本恒表值(钻头对价挂付费开掘侧)
+1 -1
View File
@@ -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):
+27 -22
View File
@@ -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 := ""
+9 -26
View File
@@ -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
+11 -11
View File
@@ -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
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
+9 -35
View File
@@ -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
+35 -61
View File
@@ -8,21 +8,8 @@ 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 取数面在根)
# u13 引用束注入:16 个 _init 抄写成员删除——正文读点改址 ctx 束(u20 三束键名;
# _pending_curio/_curio_context 纯内部态保持宿主)
var _pending_curio := {} # 三选一停点载荷暂存(战斗总结结算确认后开三选一面;续玩停点复用)
var _curio_context := "level_clear" # 三选一面上下文(level_clear=关卡完成停点→推进下一关 / reward_node=奖励节点→回图选路)
@@ -31,21 +18,8 @@ var _curio_context := "level_clear" # 三选一面上下文(level_clear=
func _init(ctx: CurioFlowContext) -> void:
# 构造参数收拢 context 对象(16 参长签名收一处命名面)
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
ctx.flow.fatal_screen = ctx.fatal_screen
ctx.flow.gp_summary = ctx.gp_summary
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
@@ -66,17 +40,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")
ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info")
var adventure = _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,22 +67,22 @@ 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 共用面)。
@@ -121,13 +95,13 @@ func present_node_curio() -> void:
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,12 +109,12 @@ 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))
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 = _adventure_get.call()
if adventure != null:
_feed_refresh_display(adventure) # 刷新面投喂单点
_refresh_presentations.call()
ctx.flow.refresh_presentations.call()
# 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构)
@@ -148,7 +122,7 @@ func _feed_refresh_display(adventure: AdventureRunner) -> void:
var runner = _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:
@@ -189,15 +163,15 @@ func on_refresh() -> void:
# 免费位(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)
@@ -218,31 +192,31 @@ 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()
ctx.flow.close_modal.call(ctx.views.curio_overlay)
ctx.views.curio_overlay.close_pick()
var adventure = _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);
+23 -8
View File
@@ -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])
+12 -8
View File
@@ -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()
+2 -2
View File
@@ -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
+18 -8
View File
@@ -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": func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}}))) # 构造参数收拢 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)
-2
View File
@@ -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
+18 -20
View File
@@ -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
+53 -39
View File
@@ -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 直接子层、
+20 -6
View File
@@ -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
+204 -152
View File
@@ -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,83 @@ 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)
## 单光点域(循环体提函数——延迟升空渐隐回收;确定性散布 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)
# ---- 演出密度(程序化 tween,确定性散布,零新帧资产) ----
var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收)
var _fx_crest_shards := 0
var _fx_dust_motes := 0
var _fx_seal_dots := 0
var st._crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收)
var st._fx_crest_shards := 0
var st._fx_dust_motes := 0
var st._fx_seal_dots := 0
# 碎砖拼纹章(胜利 §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 +335,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 +377,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:
+104 -53
View File
@@ -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
+52 -43
View File
@@ -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():
+313 -248
View File
@@ -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 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)
+15 -10
View File
@@ -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()
+47 -47
View File
@@ -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)
@@ -23,6 +23,12 @@ func _init() -> void:
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 4)
add_child(vb)
_build_views(vb)
_build_status_corner()
## 视图三件域:AP 条+棋盘区(含动效与 toast 叠层)
func _build_views(vb: VBoxContainer) -> void:
ap_bar = ApBarView.new()
vb.add_child(ap_bar)
var board_area := Control.new()
@@ -31,6 +37,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)
## 棋盘子件域:视图+动效+toast 三叠层挂接
func _build_board_children(board_area: Control) -> void:
board_view = BoardView.new()
board_view.set_anchors_preset(Control.PRESET_FULL_RECT)
board_area.add_child(board_view)
+376 -276
View File
@@ -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:
+1 -1
View File
@@ -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)
+43 -15
View File
@@ -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,32 @@ 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)
_choice_box = HBoxContainer.new()
_choice_box.add_theme_constant_override("separation", 26) # demo .cards gap 26(T7 对齐)
_choice_box.alignment = BoxContainer.ALIGNMENT_CENTER
_choice_box.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(_choice_box)
var foot := HBoxContainer.new()
foot.alignment = BoxContainer.ALIGNMENT_CENTER
foot.add_theme_constant_override("separation", 16)
box.add_child(foot)
_confirm_btn = UiKit.primary_button("确认选择")
_confirm_btn.pressed.connect(_on_confirm_pressed)
_confirm_btn.visible = false
foot.add_child(_confirm_btn)
_continue_btn = UiKit.button("继续")
_continue_btn.pressed.connect(_emit_continue_pressed)
_continue_btn.visible = false
foot.add_child(_continue_btn)
_foot_hint = UiKit.label("免费选择 · 刷新不消耗 AP",UiKit.FS_HINT, false, UiKit.WARM_FAINT)
foot.add_child(_foot_hint)
_label = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM_SOFT)
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(_label)
@@ -96,7 +128,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 +221,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
+116 -93
View File
@@ -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:
+120 -87
View File
@@ -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 "")]
+20 -12
View File
@@ -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
+262 -203
View File
@@ -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:
+1 -1
View File
@@ -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)
+26 -10
View File
@@ -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)
@@ -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))
+1 -1
View File
@@ -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)
+252 -219
View File
@@ -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 暗底双色墨制
+1 -1
View File
@@ -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
+71 -70
View File
@@ -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° 斜向光带双三角衰减:带内峰值×纵向衰减逐像素着色)
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)
+1 -1
View File
@@ -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()
+4 -3
View File
@@ -70,7 +70,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 +96,9 @@ python tools/run_system_test.py
# 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位)
godot --headless --path . res://probe/<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 条目数,
@@ -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 十条,主会话裁决三改判集成——
+69 -5
View File
@@ -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) != ""
-2
View File
@@ -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)")
+1 -3
View File
@@ -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〕")
@@ -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))
@@ -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:
@@ -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
@@ -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("无条件特殊技"):
+58 -47
View File
@@ -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([], [], [])
@@ -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)],
@@ -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()
@@ -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", "")) != "")
@@ -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", "")))
@@ -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"))
+37 -11
View File
@@ -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〕")
+4 -2
View File
@@ -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——汇总不回填)
@@ -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,
@@ -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
@@ -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:基线编排启动(无增益侧)",
@@ -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())
+3 -3
View File
@@ -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":
@@ -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,
+33 -2
View File
@@ -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()
+1 -1
View File
@@ -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
@@ -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 退役)",
@@ -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
@@ -56,7 +56,6 @@ 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("提升池"))
@@ -72,7 +71,6 @@ 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("插错"))
@@ -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)
@@ -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()
+3 -10
View File
@@ -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)
+1612 -1629
View File
File diff suppressed because it is too large. Load diff
@@ -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 计数行漂移订正)归文档批。
@@ -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()<safe_total`、单表 `validate()` 直调与 `load_tables` 诊断计数)。
2. **不同夹具实例的独立前置**——各场景自守一次,删则场景不可达时判读退化(例:另一局的 `abandon()` 受理前置、每进程的表加载零 error 护栏)。
**用户裁决记录(2026-10-28 会话)**:A+B 档 19 站点照删;另 4 处候选(cross_system_flow 127/128、theme_projection 312、theme_projection 503、board_gen_reward 181)全部保留——理由是它们的蕴含依赖夹具纪律或产品码未自证的不变量,不构成本批判据下的删除凭据。
## 二、删并申报表(逐条:旧清单序号→去向)
「旧 #」=删前 `game/tests/验证集清单.md`(冻结基点 89a36822)行序号;去向已逐条写入新清单「处置」列。
| # | 旧清单 | 删的位置(文件:行) | 标签摘要 | 类别 | 承接者(保留条) | 蕴含依据 |
|---|---|---|---|---|---|---|
| 1 | #65 | system_test.gd:176 | V2d boot 无断点在档 | ① | #67 `:181` V2-s2 boot 内存默认空态 | `not sm.has_breakpoint()` 是 :181 三合取的字面成员;〔卡② mn8〕尾缀另在 :193 有落点 |
| 2 | #1519 | theme_projection:617 | V2 红线维持 Button 零接触 | ① | #1478 `:456` V1 红线 Button 零接触 | 两处 `th` 均 `ThemeDB.get_project_theme()`,同对象同谓词;源码注释自认「同判据复述」 |
| 3 | #1520 | theme_projection:624 | V2 红线维持 focus 零定义 | ① | #1479 `:464` V1 红线 focus 零定义 | 同 type list、同六词条表、同 `focus_hits == ""` |
| 4 | #649 | config_db_cross_validator:70 | 池内悬空 skill_id 被报 | ① | #650 `:71` 悬空项 category 非空 | 同 `errs`,:71 = 本条 ∧ category 非空 |
| 5 | #654 | config_db_resolver:67 | 行集返回同带全部行(2 行) | ① | #655 `:71` 行集按表序返回 | `ids` 由同一 `rows` 逐行 append,`ids==[goblin,bugbear]` ⟹ size 2 |
| 6 | #1057 | node_map_generator:313 | pure_stream 不推全局计数器 | ① | #1059 `:317` 不推计数器且产图非空 | `svc.counter() == before` 字面重复于 :317 首合取项,同对象同序 |
| 7 | #201 | round_flow_skill_pool:61 | 结算步骤流非空 | ① | #202 `:75` 步骤流含判定行 | `has_verdict` 遍历同一 `run1.stream`,空流必红 :75 |
| 8 | #614 | board_gen_reward:175 | 降级/非法产物标记互斥 | ① | #612 `:171` + #613 `:173` 两等式 | 两等式 + mode 单值 ⟹ 互斥;:175 红必伴 :171 或 :173 红 |
| 9 | #1579 | type_glow:71 | 格面微光 blast→群伤组 | ① | #1576 `:61` 九型封闭循环 | `TYPE_EXPECT["blast"] == GLOW_AOE`(:27),同函数同入参同期望 |
| 10 | #1585 | type_glow:98 | 群伤组专项 H 零移动 | ① | #1584 `:93` 冻结色相保持 ΔH≤0.2° | 循环阈严于本条(0.2° < 0.36°),本条恒弱 |
| 11 | #1586 | type_glow:100 | 群伤组专项 S ≤0.01 | ① | #1584 `:93` 冻结色相保持 ΔS≤0.01 | 同对象同阈值字面重复;:102 的 V=0.65 档另立,保留 |
| 12 | #1393 | player_copy_sweep:59 | 头部含「同库分流」 | ① | #1338 battle_narrator:96 渠道句长串 | 同一 `node_curio_header()` 纯函数,长串含短串 |
| 13 | #1398 | player_copy_sweep:75 | 图例封印行存在 | ① | #1399 `:76` 封印行含「标空」 | 空行 ⟹ `seal_row[2] if 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/` 文件。
+29 -10
View File
@@ -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 起,"
-159
View File
@@ -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 <f1,f2> --base <rev>
可选 --out <json 路径> 落回执;--timeout <秒> 限制单次测试命令时长(缺省 3600)。
--revert-commit:按该笔提交触过的文件集回退到其父提交版本(改动须已提交在分支上)。
--revert-files+--base:把指定文件回退到 <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()
+10 -20
View File
@@ -2,9 +2,9 @@
本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [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 不在扫描面)。
**实况**:目标=减少现有断言数量、更精简,配合 §8.2 接口测试导向(对外方法、功能行为验证),前提=验证清单的验证项目覆盖逐条不变。证据=判据达标专项批已完成(本清单已销号条目,59 改钉+35 例外——全部断言已钉公开可观测面,每条断言的验证对象已显式化,做减法有了干净基线);验证集清单 1604 静态调用点为对照基准。口径界定:验证项目≠check 调用点——验证项目(卡片验收条目/GDD 登记验证面)覆盖逐条不降,check 调用点可降;可删并的两类=①同一行为被多条断言重复钉(同族合并)②钉内部路径细节而该行为已被入口断言严格承载(逐条申报承接者)。删除属显式判据改判:逐条申报理由+承接者指认(写入验证集清单「处置/去向」列),禁批量拍删;删后被承接行为以保留断言复验(套件绿),禁变异法。执行形态=工程改动 worktree 批:盘点先行(逐套件标冗余候选两类清单)→ 用户逐批裁决删并清单 → 分波执行每波闸 → 件核(验证项目覆盖不变+清单对账 PASS+条数下降留痕)。盘点与第一波执行凭据:机器初筛(同文件标签近似 223 对/cond 全同 42 组 109 条/护栏类 41 条)+七路只读语义裁决+逐条抽验,判据线收紧为「同一读数面+同一输入+谓词字面等价或字面是承接者真子集」,候选由约 150 点核减至 19 点;第一波(A+B 档 19 站点,其中 17 在册+`smoke_boot.gd` 2 点口径外)已合回——删并申报表与承接者逐条指认 `reports/工程改动报告-存量断言精简AB档-2026-10-28.md`,清单 1604→1587 且 `--diff` 对账 PASS、15 条保留行「处置」列去向在册,全量域闸回执 commit `20c13c3a`(domain=full/all_green/09 静态扫描存量红零新增),合并提交 65054b1d。余账四条:①类别② 实测不成立(全仓内部面断言约 52 条被入口严格承载 0 条,后续波次只在类别① 内找)②C 档手写族改表驱动循环合并(静态约再省 25~35 点,代价=定位改用违例明细串)③D 档「是否同一验证项目」待 owner 认账四处(本批用户裁全保留)④恒真与名实不符断言五条属判据缺陷另账,走 §8.4 呈报通道非精简账;另 静默收集通道 `Kit.check_quiet` 静态站点 407 个在冻结口径外(test-conventions:38 已备案工具局限),要收该账先做提取器改版。
**改动面**:游戏代码
@@ -397,33 +397,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 登记,来源=批次移交)