iter41-卡2:呈现层结构收敛批:67清+2豁免+2展平改善——拆私有族+六状态件+ui_kit收敛+束收拢+print删;含u14补做、并master并案与收尾产出物
This commit is contained in:
1 parent
bc75dab615
commit
7beb9bfdd5
94 files changed
+3417
-5314
No files matched your search
@@ -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 # 基础挖格成本恒表值(钻头对价挂付费开掘侧)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 := ""
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 直接子层、
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 "")]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 暗底双色墨制
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+68
-12
@@ -43,17 +43,72 @@ tests/
|
||||
> **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = 单元测试;
|
||||
> 需要两个以上真实系统协同并走落盘/落库 = 集成测试;需要完整项目引导与场景树 = 冒烟/系统/UI 探针。
|
||||
|
||||
**集成旅程清单**(2026-10-28 立——跨域旅程用例的登记处:一个行为穿过多个领域的对外方法=旅程,验收落本层;缺口旅程由属主批/工程债批按批落用例后回填本表):
|
||||
**能力清单 v1**(2026-10-28 立——验证与清单体系整改阶段 1 的覆盖索引:一行=一个玩家或系统可观测的能力,代表用例只给指针、不复制断言细节;原「集成旅程清单」五条跨系统旅程已并入本表。首期覆盖核心四域(29 行)+跨域条件闸 1 行,共 30 行;其余域按同一形态扩面):
|
||||
|
||||
| 旅程 | 涉及边界 | 现行覆盖 | 缺口 | 登记批 |
|
||||
|---|---|---|---|---|
|
||||
| 存档⇄RNG 快照往返保真 | SaveManager×AdventureRunner×RngService | `save_rng_roundtrip_test.gd` 用例一(闸 02) | — | 迭代 29 T3 |
|
||||
| 旧档迁移后断点续玩零跳号 | SaveManager 迁移链×AdventureRunner×BoardGenerator | `save_rng_roundtrip_test.gd` 用例二(闸 02) | — | 迭代 29 T3 |
|
||||
| 断点附随件与事件日志会话 | AdventureRunner×SaveManager×日志面 | `cross_system_flow_test.gd`(闸 02) | 盘点候选 | 断言迁移批(2026-10-23) |
|
||||
| 关卡图断点重生成同图/生成图确定性 | LevelRunner×节点图生成器×AdventureRunner | `node_map_determinism_test.gd`(闸 02) | — | 断言迁移批(2026-10-23,迭代 35 卡②随动换代) |
|
||||
| 回放确定性(同种子逐位) | 全玩法链 | `parity_anchor.py`(闸 11 条件闸) | — | 迭代 29 T3 收编为条件闸件 |
|
||||
> **与验证集清单的关系**:`tests/验证集清单.md` 已于阶段 2「存量断言精简」完成后删除——冻结对账基准(基点 `b9260138`/1587 条)与精简对账结果(33 站点并为 7、1587→1561,合回 `87d241c1`)留 git 历史。本表是该清单退役后的唯一覆盖索引(维护锚=test-conventions §8.3)。本表不是闸,不替代改动—验收对照表、体验验证清单与静态纪律;每行的唯一事实源仍是 `game/tests/` 下的测试代码。
|
||||
>
|
||||
> **代表用例口径**:写 `文件:函数`,每行 2–3 个;函数内可含 `Kit.check_quiet` 静默通道(逐断言提取器口径外,test-conventions §5.2 已备案)——本表以函数为锚,不受该工具局限影响。
|
||||
>
|
||||
> **维护规则(每批收口核对本批受影响行)**:本批改到哪个能力,收口时核对该行代表用例可 grep、在本批真实跑批中执行、真实入口未绕过改动点,并把「最后验证批次」刷新为本批回执;新增能力加行,能力退役删行。全表核对频率与抽样规则归阶段 3。首期未覆盖:排雷循环(棋盘生成/排雷玩法/行动经济)、表现与支撑(HUD/主题/字体/地形瓦片/模态)、数据配置与跨系统契约——按同一形态后续扩面。
|
||||
>
|
||||
> **旅程补录方法**:新旅程=加一行,「系统」列写涉及边界;源=architecture「API 边界」表+「数据流」存档/读档路径+各 GDD 登记的 deferral 集成用例。
|
||||
>
|
||||
> 「最后验证批次」列记回执 commit;多数行初值 `20c13c3a`=存量断言精简 A+B 首批的全量域闸回执,RNG 行随 C 档批刷新为 `fd6cf968`。
|
||||
|
||||
候选旅程盘点方法:源=architecture「API 边界」表逐行+「数据流」存档/读档路径+各 GDD 登记的 deferral 集成用例;新旅程先登记「缺口」列,用例按批落地后回填「现行覆盖」。
|
||||
**旅程段一:开机与存档**
|
||||
|
||||
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|
||||
|---|---|---|---|---|---|
|
||||
| 存档写盘⇄读回往返保真 | 存档序列化 | `SaveManagerCore.write_to_disk()`/`load_from_disk()` | `save_manager_domain_test.gd:_smoke_save` | `20c13c3a` | — |
|
||||
| 三槽位隔离与激活失败回滚 | 存档序列化 | `SaveManagerCore.activate_slot()`/`create_slot()`/`delete_slot()` | `save_slot_isolation_test.gd:_test_three_slot_roundtrip_and_isolation`;`save_slot_isolation_test.gd:_test_activate_failure_restores_previous_slot` | `20c13c3a` | — |
|
||||
| 槽位摘要窥视与坏槽三态 | 存档序列化 | `SaveManagerCore.peek_slot_summary()` | `save_slot_summary_test.gd:_test_peek_three_slots_distinct_values`;`save_slot_summary_test.gd:_test_peek_empty_and_unreadable_states`;`save_slot_summary_test.gd:_test_peek_zero_side_effect` | `20c13c3a` | — |
|
||||
| 启动态无激活不写盘 | 存档序列化 | `SaveManagerCore.has_write_target()`/`write_to_disk()` | `save_boot_no_activate_test.gd:_test_write_rejected_without_target`;`save_boot_no_activate_test.gd:_test_out_of_range_slots_rejected` | `20c13c3a` | — |
|
||||
| 旧档迁移链与断点续玩零跳号(集成) | 存档序列化×冒险结构 | `SaveManagerCore.load_from_disk()`(内置迁移链)× `AdventureRunner.resume_adventure()` | `save_rng_roundtrip_test.gd:_test_old_version_save_migrates_and_resumes` | `20c13c3a` | — |
|
||||
| 存档⇄RNG 快照往返保真(集成) | 存档序列化×RNG 服务×冒险结构 | `AdventureRunner.start_adventure()` → `SaveManagerCore.write_to_disk()`/`load_from_disk()` | `save_rng_roundtrip_test.gd:_test_disk_roundtrip_preserves_breakpoint` | `20c13c3a` | — |
|
||||
| RNG 统一流派生与取号账本 | RNG 服务 | `RngServiceCore.derive_at()`/`open_stream()`/`counter()` | `rng_service_test.gd:_test_derive_determinism`;`rng_service_test.gd:_test_stream_draw_determinism`;`rng_stream_boot_test.gd:_smoke_initialize` | `fd6cf968` | — |
|
||||
| 断点片段往返与续玩幂等 | 冒险结构×关卡节点地图 | `LevelRunner.breakpoint_fragment()`/`restore_fragment()`;`AdventureRunner.resume_adventure()` | `breakpoint_resume_test.gd:_init` | `20c13c3a` | — |
|
||||
| 跨系统附随面:事件日志落盘与断点附随件(集成) | RNG 服务×存档序列化×冒险结构 | `RngServiceCore.new_adventure()`(开事件日志会话);`SaveManagerCore.breakpoint_snapshot()`(断点附随键) | `cross_system_flow_test.gd:_rng_event_log_session`;`cross_system_flow_test.gd:_feats_breakpoint_roundtrip` | `20c13c3a` | 日志面仅此一条会话覆盖 |
|
||||
|
||||
**旅程段二:冒险开局与选路**
|
||||
|
||||
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|
||||
|---|---|---|---|---|---|
|
||||
| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure` | `20c13c3a` | — |
|
||||
| 专长(奇物)授予与凭证保底 | 奇物 | `AdventureRunner.present_node_curio()`/`feats_held()` × 效果引擎兑付 | `adventure_feats_test.gd:_smoke_feats`;`feat_vouchers_test.gd:_init` | `20c13c3a` | — |
|
||||
| 节点图生成合法性与同种子复现 | 关卡节点图生成 | `NodeMapGenerator.generate()`/`validate_output()` | `node_map_generator_test.gd:_test_output_ok`;`node_map_generator_test.gd:_test_reproducible`;`node_map_generator_test.gd:_test_exit_structure` | `20c13c3a` | — |
|
||||
| 节点图生长递增与无解 fail-fast | 关卡节点图生成 | `NodeMapGenerator.generate()`/`is_fatal()` | `node_map_growth_test.gd:_test_growth`;`node_map_growth_test.gd:_test_fatal_subprocess` | `20c13c3a` | — |
|
||||
| 起关/选路/断点重生成同图(集成) | 关卡节点地图×关卡节点图生成×冒险结构 | `LevelRunner.start_level()`/`start_level_external()`/`pick_next_node()`;`AdventureRunner.resume_adventure()` | `node_map_determinism_test.gd:_smoke_generated_map_determinism` | `20c13c3a` | — |
|
||||
| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild` | `20c13c3a` | — |
|
||||
| 关卡表消费侧校验与关卡推进 | 关卡节点地图×数据配置 | `LevelTable.validate_graph()`;`LevelRunner.start_level()` | `level_table_validate_test.gd:_test_positive`;`level_table_validate_test.gd:_test_negative`;`level_shop_economy_test.gd:_smoke_level` | `20c13c3a` | — |
|
||||
|
||||
**旅程段三:战斗结算**
|
||||
|
||||
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|
||||
|---|---|---|---|---|---|
|
||||
| 伤害公式与掷点乘区 | 战斗结算 | `DamageMath.pool_damage()`/`roll_discount_percent()`/`rolled_component()` | `damage_math_test.gd:_test_pool_damage`;`damage_math_test.gd:_test_roll_discount_percent`;`roll_multiplier_test.gd:_smoke_armor_roll` | `20c13c3a` | — |
|
||||
| 伤害单据编排与回放 | 战斗结算 | `DamageController.submit()`/`open_ticket()`/`flush()` | `damage_controller_test.gd:_run_recipe_sequence`;`damage_controller_test.gd:_test_five_recipes_and_replay` | `20c13c3a` | — |
|
||||
| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments` | `20c13c3a` | — |
|
||||
| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence` | `20c13c3a` | — |
|
||||
| Boss 半血解锁技门控 | 敌人系统 | `BattleOrchestrator.begin_battle()`(Boss 行) | `skill_unlock_test.gd:_test_gate_predicate`;`skill_unlock_test.gd:_test_latch_irreversible` | `20c13c3a` | — |
|
||||
| 封印与极性路由/不可逆 | 地图战斗资源×战斗结算 | 效果引擎 × `BattleOrchestrator.begin_battle()` | `seal_polarity_test.gd:_smoke_seal_ap_exhausted`;`seal_polarity_test.gd:_smoke_seal_irreversible_quadrants`;`seal_polarity_test.gd:_smoke_polarity_routing` | `20c13c3a` | — |
|
||||
| 变异布置门控 | 地形变异 | `BattleOrchestrator.begin_battle()`(变异布置段) | `variant_placement_test.gd:_smoke_variant_placement`;`variant_placement_test.gd:_smoke_global_variant` | `20c13c3a` | — |
|
||||
|
||||
**旅程段四:奖励、经济与局外成长**
|
||||
|
||||
| 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | 最后验证批次 | 缺口 |
|
||||
|---|---|---|---|---|---|
|
||||
| 清图奖励双份与零路径 | 地图战斗资源 | `BattleOrchestrator.begin_battle()` → 步骤流行(`source_class=clear_reward`) | `seal_polarity_test.gd:_smoke_clear_reward_chain`;`seal_polarity_test.gd:_smoke_clear_reward_zero_paths` | `20c13c3a` | — |
|
||||
| 奖励三选一候选/刷新付费/呈现 | 奇物×节点奖励屏 | `AdventureRunner.present_node_curio()`(数据面);奖励节点覆盖层场景(呈现面) | `curio_refresh_paid_test.gd:_init`;`reward_overlay_test.gd:_run` | `20c13c3a` | — |
|
||||
| GP 钱包/escrow/商店收口/共享池 | 地图战斗资源×商店 | `LevelRunner.buy_product()`/`player_request_refresh()`/`close_wallet()`/`set_shop_pool()` | `level_shop_economy_test.gd:_smoke_gp_wallet`;`level_shop_economy_test.gd:_smoke_shop_room`;`level_shop_economy_test.gd:_smoke_shop_pool` | `20c13c3a` | — |
|
||||
| 元进度入账与锻造两轴 | 元进度×锻造工坊 | `SaveManagerCore.add_keystone()`/`spend_keystone()`/`set_forge_route_level()` | `meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_smoke_forge_axes` | `20c13c3a` | — |
|
||||
| 天赋树购买与六键合成 | 天赋树 | `SaveManagerCore.tree_nodes()`/`set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init` | `20c13c3a` | — |
|
||||
| 秘宝结构冻结与 12 件效果 | 秘宝 | `TreasureItems.add_item()`/`query_inventory()`/`validate_use()` | `treasure_items_test.gd:_init`;`treasure_effects_test.gd:_init` | `20c13c3a` | — |
|
||||
|
||||
**跨域条件闸**
|
||||
|
||||
| 能力 | 系统 | 真实入口 | 代表用例 | 最后验证批次 | 缺口 |
|
||||
|---|---|---|---|---|---|
|
||||
| 回放确定性(同种子三变体逐位) | 全玩法链 | `tools/parity_anchor.py`(闸 11 条件闸,按批发起) | `tools/parity_anchor.py`(整件为条件闸) | —(条件闸) | 条件闸未随本批执行 |
|
||||
|
||||
## 断言计数双口径(2026-10-08 迭代 19 卡① T4 订正)
|
||||
|
||||
@@ -70,7 +125,7 @@ tests/
|
||||
|---|---|---|---|
|
||||
| `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 |
|
||||
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
|
||||
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
|
||||
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) |
|
||||
| `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 |
|
||||
| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 |
|
||||
| `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
|
||||
@@ -96,8 +151,9 @@ python tools/run_system_test.py
|
||||
# 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位)
|
||||
godot --headless --path . res://probe/<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
@@ -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) != ""
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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("无条件特殊技"):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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〕")
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -69,37 +69,40 @@ func _test_negative() -> void:
|
||||
var base := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4),
|
||||
_row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6),
|
||||
_row(3, 13, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 3, 8)]
|
||||
Kit.check("合法三行域校验零错误(夹具自证)〔fv2〕", _table(base).validate().is_empty())
|
||||
# ① L1 出口改 2 → 出口恒 1 断言红(负样本夹具)
|
||||
var bad_l1_exit := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 2, 4), base[1], base[2]]
|
||||
Kit.check("L1 出口=2 被拒(出口 L1 恒 1 已锁)〔fv2〕", not _table(bad_l1_exit).validate().is_empty())
|
||||
# ② 配比合计 0.5 → 合计 1 断言红(负样本夹具)
|
||||
var bad_ratio := [_row(1, 6, 0.3, [0.2, 0.1, 0.1, 0.1], 1, 1, 4), base[1], base[2]]
|
||||
Kit.check("配比合计 0.5 被拒(四类合计 1 已锁)〔fv2〕", not _table(bad_ratio).validate().is_empty())
|
||||
# ③ 层深低于下限
|
||||
var bad_layers := [_row(1, 5, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4), base[1], base[2]]
|
||||
Kit.check("层深 5<6 被拒(层深下限已锁)〔fv2〕", not _table(bad_layers).validate().is_empty())
|
||||
# ④ 增幅未变大(6/9/11:Δ12=3、Δ23=2)
|
||||
var bad_growth := [base[0], _row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6),
|
||||
_row(3, 11, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 3, 8)]
|
||||
Kit.check("层深增幅倒挂被拒(递增且增幅变大)〔fv2〕", not _table(bad_growth).validate().is_empty())
|
||||
# ⑤ 出口越上界(L3 出口 4)
|
||||
var bad_exit := [base[0], base[1], _row(3, 13, 0.7, [0.55, 0.15, 0.15, 0.15], 2, 4, 8)]
|
||||
Kit.check("出口 4 越域 [1,3] 被拒〔fv2〕", not _table(bad_exit).validate().is_empty())
|
||||
# ⑥ 岔口去路数改 (2,4)
|
||||
var bad_fork_row := _row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4)
|
||||
bad_fork_row["fork_ways_max"] = 4
|
||||
Kit.check("岔口去路数 (2,4) 被拒(恒 2/3 已锁)〔fv2〕",
|
||||
not _table([bad_fork_row, base[1], base[2]]).validate().is_empty())
|
||||
# ⑦ 键集非闭集(多塞未知键)
|
||||
var bad_keys_row: Dictionary = _row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4)
|
||||
bad_keys_row["mystery"] = 1
|
||||
Kit.check("字段键集非 ROW_KEYS 闭集被拒〔fv2〕",
|
||||
not _table([bad_keys_row, base[1], base[2]]).validate().is_empty())
|
||||
# ⑧ level_index 重复
|
||||
Kit.check("键重复行被拒〔fv2〕", not _table([base[0], base[0]]).validate().is_empty())
|
||||
# ⑨ 战斗保底负值
|
||||
Kit.check("战斗保底 -1 被拒(≥0)〔fv2〕",
|
||||
not _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]]).validate().is_empty())
|
||||
# ⑩ 空表=合法态(沿 difficulty_table 先例)
|
||||
Kit.check("空表域校验零错误(合法态)〔fv2〕", _table([]).validate().is_empty())
|
||||
# 逐类负向表:[个案标签, 行集, 期望零错误]——每行仍逐案断言,消息带期望/实际与违例明细
|
||||
var cases: Array = [
|
||||
["合法三行域校验零错误(夹具自证)", base, true],
|
||||
["L1 出口=2 被拒(出口 L1 恒 1 已锁)", bad_l1_exit, false],
|
||||
["配比合计 0.5 被拒(四类合计 1 已锁)", bad_ratio, false],
|
||||
["层深 5<6 被拒(层深下限已锁)", bad_layers, false],
|
||||
["层深增幅倒挂被拒(递增且增幅变大)", bad_growth, false],
|
||||
["出口 4 越域 [1,3] 被拒", bad_exit, false],
|
||||
["岔口去路数 (2,4) 被拒(恒 2/3 已锁)", [bad_fork_row, base[1], base[2]], false],
|
||||
["字段键集非 ROW_KEYS 闭集被拒", [bad_keys_row, base[1], base[2]], false],
|
||||
["键重复行被拒", [base[0], base[0]], false],
|
||||
["战斗保底 -1 被拒(≥0)", [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]], false],
|
||||
["空表域校验零错误(合法态)", [], true],
|
||||
]
|
||||
for c in cases:
|
||||
var errs: Array = _table(c[1]).validate()
|
||||
var expect_label := "零错误" if c[2] else "有违例"
|
||||
var actual_label := "零错误" if errs.is_empty() else "违例 %d 条 %s" % [errs.size(), str(errs)]
|
||||
Kit.check("域校验:%s(期望%s,实际%s)〔fv2〕" % [c[0], expect_label, actual_label],
|
||||
errs.is_empty() == c[2])
|
||||
@@ -34,14 +34,11 @@ func _smoke_initialize() -> void:
|
||||
rng.set_master_seed(42)
|
||||
# 统一流黄金向量(数据配置 AC-3:单盐沿用 board_gen 盐,
|
||||
# scope 0-3 与 board_gen 向量逐位连续;Python 参考实现逐位交叉验证一致)
|
||||
Kit.check("统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)", rng.derive_at(0) == 8672514112661456564)
|
||||
Kit.check("统一流向量 m=42 scope=1", rng.derive_at(1) == 8834291164406037215)
|
||||
Kit.check("统一流向量 m=42 scope=2", rng.derive_at(2) == 7714373262647006282)
|
||||
Kit.check("统一流向量 m=42 scope=3", rng.derive_at(3) == 1280102845971471347)
|
||||
Kit.check("统一流向量 m=42 scope=4", rng.derive_at(4) == 82585501446482399)
|
||||
Kit.check("统一流向量 m=42 scope=5", rng.derive_at(5) == 8078924421474470922)
|
||||
Kit.check("统一流向量 m=42 scope=6", rng.derive_at(6) == 6638052740430622424)
|
||||
Kit.check("统一流向量 m=42 scope=7", rng.derive_at(7) == 8264196488417721294)
|
||||
var golden := [8672514112661456564, 8834291164406037215, 7714373262647006282, 1280102845971471347,
|
||||
82585501446482399, 8078924421474470922, 6638052740430622424, 8264196488417721294]
|
||||
for i in golden.size():
|
||||
Kit.check("统一流向量 m=42 scope=%d(黄金向量;scope0=旧 board_gen 连续性锚)" % i,
|
||||
rng.derive_at(i) == golden[i])
|
||||
Kit.check("derive_at 纯函数恒等", rng.derive_at(0) == rng.derive_at(0))
|
||||
# scope 取号语义:首个 open_stream = 全局 scope 0;计数器单调;用途标签不参与派生(只进事件日志)
|
||||
var s0 = rng.open_stream("board_gen")
|
||||
|
||||
@@ -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 退役)",
|
||||
|
||||
@@ -58,14 +58,12 @@ func _run() -> void:
|
||||
and rows.has("light") and rows.has("heavy") and rows.has("ward"))
|
||||
|
||||
# ③ 初始持有行(长剑/轻甲):购买钮现形带几何价、选中钮=当前禁用
|
||||
var rsw = rows.get("sword", {})
|
||||
var rli = rows.get("light", {})
|
||||
Kit.check("长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕",
|
||||
rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled
|
||||
and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled)
|
||||
Kit.check("轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕",
|
||||
rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20"
|
||||
and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled)
|
||||
# 两行同取谓词并集(轻甲行补 select_btn 非空 + buy_btn 可用两子句——原件少写,只增不减)
|
||||
for pair in [["sword", "长剑"], ["light", "轻甲"]]:
|
||||
var r: Dictionary = rows.get(pair[0], {})
|
||||
Kit.check("%s行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕" % pair[1],
|
||||
r.get("buy_btn") != null and String(r.buy_btn.text) == "锻造 20" and not r.buy_btn.disabled
|
||||
and r.get("select_btn") != null and String(r.select_btn.text) == "当前" and r.select_btn.disabled)
|
||||
|
||||
# ④ 待解锁行(长枪/法杖/重甲/魔法护甲):解锁钮 200、无购买钮、无选中钮、状态含双通道文案
|
||||
var rsp = rows.get("spear", {})
|
||||
|
||||
@@ -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,10 +56,8 @@ func _sweep_narrator() -> void:
|
||||
var head := String(narrator.node_curio_header())
|
||||
Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库"))
|
||||
Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给"))
|
||||
Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流"))
|
||||
Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重"))
|
||||
Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限"))
|
||||
Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池"))
|
||||
for w in ["同权重", "同上限", "提升池"]:
|
||||
Kit.check("奖励点头部零「%s」旧话(u3 清零)〔wa4〕" % w, not head.contains(w))
|
||||
Kit.check("专长卡兜底==「专长\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕",
|
||||
String(narrator.feat_card_text({})) == "专长\n???")
|
||||
|
||||
@@ -72,11 +70,11 @@ func _sweep_legend() -> void:
|
||||
if String(e[1]) == "封印":
|
||||
seal_row = e
|
||||
break
|
||||
Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty())
|
||||
Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空"))
|
||||
Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚"))
|
||||
Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错"))
|
||||
Kit.check("图例封印行零「挖雷」(u4 雷字清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷"))
|
||||
for w in ["插错", "挖雷"]:
|
||||
Kit.check("图例封印行零「%s」〔wa5〕" % w,
|
||||
not String(seal_row[2] if seal_row.size() > 2 else "").contains(w))
|
||||
|
||||
|
||||
# ⑤⑥ 来源名双源(u5/u6——状态槽消费半+表值 ConfigDB 真装载+battle_log 行消费半)
|
||||
|
||||
@@ -81,7 +81,7 @@ func _init() -> void:
|
||||
stage.on_step_row_fx({"type": "damage", "target": "地精", "target_id": "goblin_b"})
|
||||
await create_timer(0.6).timeout # 弹道到达拍 0.33s + 余量
|
||||
Kit.check("伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕",
|
||||
s2._flash_count == 1 and s1._flash_count == 0)
|
||||
s2.fx_probe().flash_count == 1 and s1.fx_probe().flash_count == 0)
|
||||
# 口二:enemy_standee_center —— id 先、display_name 兜底
|
||||
var c_b = stage.enemy_standee_center("goblin_b", "地精")
|
||||
var c_b_expect: Vector2 = stage.world_to_shell(s2.position + s2.box * 0.5)
|
||||
@@ -91,14 +91,16 @@ func _init() -> void:
|
||||
Kit.check("无 target_id 时按名兜底同旧行为(首命中)〔wd12〕", c_fallback != null and c_fallback == c_fallback_expect)
|
||||
|
||||
# ---- ① 意图蓄力档可达(d2)----
|
||||
s1.set_pending("damage", "heavy")
|
||||
Kit.check("蓄力技能(heavy)置 _intent_charge=true〔wd2〕", s1._intent_charge)
|
||||
s1.set_pending("damage", "goblin_slash")
|
||||
Kit.check("非蓄力攻击技(goblin_slash)零置位〔wd2〕", not s1._intent_charge)
|
||||
s1.set_pending("buff", "goblin_rage")
|
||||
Kit.check("增益系零置位〔wd2〕", not s1._intent_charge)
|
||||
s1.set_pending("damage", "basic")
|
||||
Kit.check("普攻零表示路径复位 _intent_charge〔wd2〕", not s1._intent_charge)
|
||||
# 蓄力判据表:[kind, skill_id, 期望 _intent_charge, 个案标签]
|
||||
var charge_cases := [
|
||||
["damage", "heavy", true, "蓄力技能(heavy)置 _intent_charge=true"],
|
||||
["damage", "goblin_slash", false, "非蓄力攻击技(goblin_slash)零置位"],
|
||||
["buff", "goblin_rage", false, "增益系零置位"],
|
||||
["damage", "basic", false, "普攻零表示路径复位 _intent_charge"],
|
||||
]
|
||||
for c in charge_cases:
|
||||
s1.set_pending(c[0], c[1])
|
||||
Kit.check("%s〔wd2〕" % c[3], s1._intent_charge == c[2])
|
||||
|
||||
# ---- ② 庆祝 tween 句柄纪律(d4)----
|
||||
var ov = BattleEndScript.new()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,19 @@ func _init() -> void:
|
||||
var f: Font = UiKitScript.load_font(path)
|
||||
Kit.check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null)
|
||||
if f != null:
|
||||
Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
|
||||
and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)))
|
||||
Kit.check("中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", f.has_char("危".unicode_at(0)))
|
||||
Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
|
||||
# 字形集表:[个案标签, 须在集字符]——缺字明细进消息
|
||||
var glyph_groups := [
|
||||
["中文字形在集(攻/雷/旗/封)", "攻雷旗封"],
|
||||
["中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", "危"],
|
||||
["ASCII/数字字形在集(A/0/%)", "A0%"],
|
||||
]
|
||||
for g in glyph_groups:
|
||||
var missing := ""
|
||||
for i in String(g[1]).length():
|
||||
var ch := String(g[1])[i]
|
||||
if not f.has_char(ch.unicode_at(0)):
|
||||
missing += ch
|
||||
Kit.check("%s(缺=%s)" % [g[0], missing if missing != "" else "无"], missing == "")
|
||||
var sz := 18
|
||||
var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
var w_label: float = f.get_string_size("攻击 2", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
|
||||
-2682
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user