refactor(ui): C 区精简批——信息归属拍板代码实装(2026-09-26):①C 区面板=攻击+护甲AC 数据框+魔法道具槽(预留 4 空槽)+本关强化列表(预留空态隐藏)——HP 条/立绘占位/状态槽/RAP/GP/钥石退役;②状态效果唯一展示面=B 区站桩(compact 档 StatusSlotRow 整体迁入——16px 图标+层数角标+悬停 tooltip 义务随迁,BUG-0001 回归组件同源;清空反馈逐槽化光+微文案随迁站桩);③RAP 读数迁 D 区 AP 条贴弃图刷新消费口(点阵+数字+清零脉冲微文案+拒绝脉冲载体随迁);④hud_presenter 单投喂改造(B/C 双投喂重复展示退役)+钱包投喂退役(GP/钥石迁出战斗屏,去向=关卡画面/结算/营地归对应 UX 批);冒烟 557/0+五探针全绿

This commit is contained in:
DKChan committed 2026-09-26 01:26:26 +08:00
1 parent d6eee49945
commit effb98e57d
9 files changed
+203 -224

No files matched your search

+16 -22
View File
@@ -15,30 +15,24 @@ extends RefCounted
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
# 2026-09-26 C 区精简批:钱包 GP/钥石投喂退役(中/大循环资源迁出战斗屏——信息归属拍板);
# HP/状态效果单投喂 B 区站桩(原 B/C 双投喂为重复展示——信息归属拍板单一展示面原则)
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
settlement, runner, level_active: bool, db, refresh_remaining: int, gp_label: Label = null,
keystone_label: Label = null, keystone_balance: int = 0) -> void:
# GP 余额常驻(GP+商店房实施批 2026-09-21——A 区顶栏邻接位;runner 缺位 = 非冒险态按 0 读数)
if gp_label != null:
gp_label.text = "金币 %d" % (int(runner.gp) if runner != null else 0)
# 钥石余额常驻(元进度实现批 2026-09-21——A 区顶栏邻接位;数据源 MetaProgression 账本,购买/收束后随全量重查刷新)
if keystone_label != null:
keystone_label.text = "钥石 %d" % keystone_balance
settlement, runner, level_active: bool, db, refresh_remaining: int) -> void:
if settlement == null:
_refresh_out_of_battle(stage, info, runner, level_active, db)
_refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db)
return
var s: Dictionary = settlement.presentation_snapshot()
var hero: Dictionary = s.hero
if hero.is_empty() or not bool(hero.get("alive", false)):
_refresh_out_of_battle(stage, info, runner, level_active, db)
_refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db)
return
_refresh_hero(stage, info, s)
_refresh_economy(info, ap_bar, s, db, refresh_remaining)
_refresh_economy(ap_bar, s, db, refresh_remaining)
stage.set_enemies(s.enemies)
info.set_status_effects(s.hero_effects)
# ---- B 区主角站桩 + C 区数据框 ----
# ---- B 区主角站桩(HP/状态唯一展示面)+ C 区数据框(攻击/AC) ----
static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Dictionary) -> void:
var hero: Dictionary = s.hero
@@ -55,19 +49,19 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di
var mod_text := ""
if mod_total != 0:
mod_text = "(%+d%% %s)" % [mod_total, "·".join(sources)]
# B 区站桩只挂血条+状态小图标(2026-09-20 全站桩拍板)——攻击值等规划数据归 C 区数据框
# B 区站桩 = HP 与状态效果唯一展示面(2026-09-26 信息归属拍板)——C 区不再重复投喂
stage.hero_standee.set_hero(hp, max_hp)
stage.hero_standee.set_effects(s.hero_effects)
info.set_hero(hp, max_hp)
info.set_attack(atk, mod_text)
# 我方 AC 唯一展示面 = C 区(2026-09-26 二轮拍板补入)——快照 armor_class 含战斗内 ac_up 折算
info.set_armor(int(hero.get("armor_class", 0)))
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态) ----
# ---- 行动经济(AP 点阵 / RAP / 弃图刷新按钮态——RAP 读数 2026-09-26 自 C 区迁入本条) ----
static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db, refresh_remaining: int) -> void:
ap_bar.set_ap(int(s.ap), int(db.action_economy.ap_per_round))
# RAP 活值投影(C 区点阵)——清零反馈分支(行动经济 D1 义务)依赖本处持续投喂才会可达
info.set_rap(int(s.rap), int(db.action_economy.rap_refresh_cost))
ap_bar.set_rap(int(s.rap))
var digging := String(s.phase) == ActionEconomy.PHASE_ACTIVE
ap_bar.set_refresh_state(refresh_remaining,
bool(s.can_refresh) and digging) # Waiting 代理已随 OQ12 退役——UI 仅挖掘期受理
@@ -76,16 +70,16 @@ static func _refresh_economy(info: HeroInfoPanelView, ap_bar: ApBarView, s: Dict
# 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值
static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView,
runner, level_active: bool, db) -> void:
ap_bar: ApBarView, runner, level_active: bool, db) -> void:
if not level_active:
return
if runner != null:
info.set_hero(runner.hero_hp, runner.effective_max_hp())
info.set_attack(runner.effective_atk(), "(关卡推进中)")
info.set_armor(runner.effective_ac())
stage.hero_standee.set_hero(runner.hero_hp, runner.effective_max_hp())
stage.hero_standee.set_effects([])
stage.clear_enemies() # 节点间隙清敌方站桩残位(死亡瞬间的灰化呈现已在结算行驱动完成)
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务)
ap_bar.set_rap(0) # battle_over RAP 清零反馈(行动经济 义务——载体 2026-09-26 迁 AP 条)
# 三源拒绝可感知(排雷玩法 reject_reason/sub_reason → 玩家文案;pattern 1 双源拒绝反馈)
+6 -11
View File
@@ -52,13 +52,11 @@ var _debug = null
# 由关卡画面本体选路覆盖层承载;E 区道具面板随可选类删除下线)
var stage: BattleStageView # B 区(含 log_view 明细带)
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区(含钱包行——原 A 区顶栏 GP/钥石余额常驻位)
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区——2026-09-26 信息归属拍板精简)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
var gp_label: Label # GP 余额常驻位(C 区钱包行——文案装配归 HudPresenter)
var keystone_label: Label # 钥石余额常驻位(C 区钱包行——文案装配归 HudPresenter)
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
var legend_overlay: LegendOverlay
var review_overlay: SettlementReviewOverlay
@@ -102,8 +100,6 @@ func _build_ui() -> void:
var root := VBoxContainer.new()
add_child(root)
var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db)
gp_label = battle_ui.gp_label
keystone_label = battle_ui.keystone_label
stage = battle_ui.stage
log_view = battle_ui.log_view
info_panel = battle_ui.info_panel
@@ -310,7 +306,7 @@ func _on_adventure_finished(summary: Dictionary) -> void:
settlement = null
_close_modal(route_overlay) # 终局兜底(放弃入口新场景:选路层在开态——走模态栈恢复其遮挡面;未开过时 close 容错)
_sync_settlement_visuals() # → 全关
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体 2026-09-26 迁 B 区站桩)
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
# 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
@@ -441,8 +437,8 @@ func _on_battle_finished(payload: Dictionary) -> void:
var result: String = String(payload.get("result", ""))
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
info_panel.set_rap(0, int(db.action_economy.rap_refresh_cost)) # RAP 清零滚动 + 微文案
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
ap_bar.set_rap(0) # RAP 清零脉冲 + 微文案(载体 2026-09-26 迁 D 区 AP 条)
stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体迁 B 区站桩)
var decision: Dictionary = _debug.on_battle_finished(payload)
match String(decision.outcome):
"next_layer":
@@ -734,7 +730,7 @@ func _on_refresh_confirmed() -> void:
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1})
if result == "rejected_rap":
board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
info_panel.pulse_rap()
ap_bar.pulse_rap()
else:
board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
board_view.refresh()
@@ -757,8 +753,7 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round)
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining,
gp_label, keystone_label, meta.keystone_balance() if meta != null else 0)
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining)
_sync_settlement_visuals()
+4 -5
View File
@@ -11,7 +11,8 @@ extends RefCounted
# 进度由关卡画面本体(选路覆盖层整图)承载,28px 纵向空间拨给 C/D 底行 520→548;E 区道具面板
# 已随可选类删除下线,2026-09-15 格面资源批)。
# + 结算演出覆盖层(A–D 之上、模态之下——跨区弹道/光带载体,结算演出链批 2026-09-20)。
# 返回句柄字典:{gp_label, keystone_label, stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx}
# 返回句柄字典:{stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx}
# (gp_label/keystone_label 随 2026-09-26 信息归属拍板退役——GP/钥石迁出战斗屏,句柄不再透出)
static func build_battle_ui(root: Control, db) -> Dictionary:
# set_anchors_and_offsets_preset(非 set_anchors_preset):后者会把控件重置回最小内容尺寸
# (实测 927×613——缩顶、棋盘 45px 格 ×10 行超出 D 区 333px 高被裁,2026-09-13 用户报告回归)
@@ -39,11 +40,9 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
var settlement_fx := SettlementFxView.new()
root.add_child(settlement_fx)
# GP/钥石余额常驻位(GP+商店房实施批 2026-09-21 原居 A 区顶栏——顶栏退役后迁 C 区底行,
# 句柄经 info_panel 视图成员透出;文案装配归 HudPresenter,此处只透传引用)
# GP/钥石余额位已随 2026-09-26 信息归属拍板退役(中/大循环资源迁出战斗屏——
# GP 去向 = 关卡画面+战斗结算画面、钥石去向 = 结算画面+营地,呈现归对应 UX spec 批次)
return {
"gp_label": info_panel.gp_label,
"keystone_label": info_panel.keystone_label,
"stage": stage,
"log_view": stage.log_view,
"info_panel": info_panel,
+56
View File
@@ -2,6 +2,8 @@
# 实现 GDD:design/ux/battle.md(D 区 AP 条 44px——点阵+数字〔pattern 5 点阵资源显示〕、
# 停手按钮居左、图例+弃图刷新按钮居右〔雷图右上角〕;弃图刷新确认微交互与常规确认不同位
# 〔pattern 7 破坏性确认异位:RAP 扣费 · 旧板旗作废显式代价〕)。
# 2026-09-26 信息归属批:RAP 读数(点阵+数字)自 C 区迁入本条、置弃图刷新按钮左邻
# (RAP 唯一消费口 = 弃图刷新——就近原则;RAP 清零反馈/资源拒绝脉冲载体随迁)。
# Date: 2026-09-12
# 高内聚边界:纯视图——意图经信号外发(stop_pressed/refresh_confirmed/legend_toggled),
# 状态由组合根经 setter 注入;刷新可用性预判 = RAP≥8 读数(行动经济 无推送信号——每次调用返回后重查)。
@@ -24,6 +26,10 @@ var _btn_refresh: Button
var _refresh_sub: Label
var _confirm_row: HBoxContainer
var _confirm_lab: Label
var _rap_dots: HBoxContainer
var _rap_label: Label
var _rap_feedback: Label
var _last_rap := 0 # 上次投影值(清零反馈判据——禁解析自身 label 文本当状态)
var refresh_cost: int = 0 # 表值驱动(装配经 set_refresh_cost 注入 ActionEconomyTable.rap_refresh_cost)
var _confirm_armed := false
var _arm_tween: Tween
@@ -56,6 +62,20 @@ func _init() -> void:
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(spacer)
# RAP 读数(2026-09-26 信息归属拍板自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮
# (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则)
_rap_dots = HBoxContainer.new()
_rap_dots.add_theme_constant_override("separation", 2)
_rap_dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER
row.add_child(_rap_dots)
_rap_label = UiKit.label("RAP 0", 14, true)
_rap_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
row.add_child(_rap_label)
_rap_feedback = UiKit.label("", 11, false)
_rap_feedback.add_theme_color_override("font_color", UiKit.STONE_FAINT)
_rap_feedback.size_flags_vertical = Control.SIZE_SHRINK_CENTER
row.add_child(_rap_feedback)
# 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位
_confirm_row = HBoxContainer.new()
_confirm_row.add_theme_constant_override("separation", 4)
@@ -117,6 +137,42 @@ func set_stop_enabled(on: bool) -> void:
_btn_stop.disabled = not on
# RAP 活值投影(点阵+数字——pattern 5 双承载);清零反馈分支(行动经济 D1 义务)依赖持续投喂可达
func set_rap(rap: int) -> void:
var prev := _last_rap
_last_rap = rap
_rap_label.text = "RAP %d" % rap
_render_rap_dots(rap)
if prev > 0 and rap == 0:
# battle_over RAP 清零可见反馈(行动经济 D1 义务):数字脉冲 + 微文案(载体自 C 区迁入)
_rap_feedback.text = "随战斗结束消散"
var tw := create_tween()
tw.tween_property(_rap_label, "modulate", Color(1.4, 1.2, 0.7), UiKit.motion(0.12))
tw.tween_property(_rap_label, "modulate", Color(1, 1, 1), UiKit.motion(0.3))
elif rap > 0:
_rap_feedback.text = ""
# 资源面拒绝反馈(pattern 1 resource_face 腿):RAP 数字脉冲(RAP<8 弃图刷新被拒)
func pulse_rap() -> void:
var tw := create_tween()
tw.tween_property(_rap_label, "modulate", Color(1.0, 0.4, 0.3), UiKit.motion(0.1))
tw.tween_property(_rap_label, "modulate", Color(1, 1, 1), UiKit.motion(0.25))
func _render_rap_dots(rap: int) -> void:
var shown := mini(rap, 16)
while _rap_dots.get_child_count() < shown:
var d := ColorRect.new()
d.custom_minimum_size = Vector2(8, 8)
d.size_flags_vertical = Control.SIZE_SHRINK_CENTER
d.mouse_filter = Control.MOUSE_FILTER_IGNORE
_rap_dots.add_child(d)
for i in _rap_dots.get_child_count():
var d: ColorRect = _rap_dots.get_child(i)
d.color = COL_GOLD if i < shown else COL_DOT_OFF
func set_legend_enabled(on: bool) -> void:
_btn_legend.disabled = not on
+55 -128
View File
@@ -1,29 +1,22 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(C 区角色信息 240×548〔底行 520→548——停点前移批 2026-09-23,
# 原 A 区顶栏空间并入〕——HP/攻击(+修正)/RAP 直角细线数据框
# 〔修正来源角标〕、状态效果槽 ×4、小人立绘占位;RAP 清零可见反馈〔行动经济 义务〕;
# 状态槽空态 = 槽隐藏上移补位〔EC12〕)。
# Date: 2026-09-12
# 高内聚边界:纯视图 setter 面——装配(含修正文案)归 hud_presenter.gd;定长槽 + clip_contents
# 实现 GDD:design/ux/battle.md(C 区角色信息 240×548——2026-09-26 信息归属拍板精简:
# 攻击(+修正来源角标)/护甲 AC 数据框 + 魔法道具槽(预留)+ 本关强化列表(预留)。
# HP 条/立绘占位/状态效果槽/RAP 池/GP/钥石退役——HP 与状态唯一展示面 = B 区站桩(同一数据
# hud_presenter 双投喂 B/C 两区的重复展示);RAP 迁 D 区 AP 条(贴弃图刷新消费口);
# GP(中循环)/钥石(大循环)迁出战斗屏)。
# Date: 2026-09-12(2026-09-26 C 区精简批重写)
# 高内聚边界:纯视图 setter 面——装配(含修正文案/AC 投喂)归 hud_presenter.gd;定长槽 + clip_contents
# (缺陷①回归约束)。
class_name HeroInfoPanelView
extends PanelContainer
const COL_INK := UiKit.INK
const COL_GOLD := UiKit.GOLD
const COL_DOT_OFF := UiKit.DOT_OFF
var _hp_bar: ProgressBar
var _hp_label: Label
var _atk_label: Label
var _rap_dots: HBoxContainer
var _rap_label: Label
var _rap_feedback: Label
var _status_feedback: Label
var slot_row: StatusSlotRow
var gp_label: Label # 冒险钱包余额常驻位(停点前移批 2026-09-23——原 A 区顶栏迁入 C 区底行)
var keystone_label: Label # 钥石余额常驻位(同上——文案装配归 HudPresenter)
var _last_rap := 0 # 上次投影值(清零反馈判据——禁解析自身 label 文本当状态)
var _armor_label: Label
var item_slots: HBoxContainer # 魔法道具槽(预留——4 空槽占位;数量/交互形态待魔法道具批定稿)
var _bonus_section: VBoxContainer # 本关强化(预留——空态隐藏,接线归大奖励/锻造呈现批)
var _bonus_list: VBoxContainer
func _init() -> void:
@@ -36,68 +29,35 @@ func _init() -> void:
add_child(col)
col.add_child(UiKit.label("探险者", 16, true))
_hp_bar = ProgressBar.new()
_hp_bar.custom_minimum_size = Vector2(0, 12)
_hp_bar.show_percentage = false
col.add_child(_hp_bar)
_hp_label = _data_line()
col.add_child(_hp_label)
_atk_label = _data_line()
col.add_child(_atk_label)
_armor_label = _data_line()
col.add_child(_armor_label)
# RAP 框(pattern 5 点阵资源显示——点阵图形 + 数字精确值双承载)
var rap_head := HBoxContainer.new()
rap_head.add_theme_constant_override("separation", 6)
col.add_child(rap_head)
_rap_dots = HBoxContainer.new()
_rap_dots.add_theme_constant_override("separation", 2)
rap_head.add_child(_rap_dots)
_rap_label = UiKit.label("RAP 0", 13, true)
rap_head.add_child(_rap_label)
_rap_feedback = UiKit.label("", 12, false)
_rap_feedback.add_theme_color_override("font_color", UiKit.STONE_FAINT)
col.add_child(_rap_feedback)
# 魔法道具槽(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此,
# 不预设交互(挖掘期指向交互归 E 区既有裁决);终稿样式/槽位数归魔法道具批
col.add_child(UiKit.label("魔法道具", 14, true))
item_slots = HBoxContainer.new()
item_slots.add_theme_constant_override("separation", 6)
col.add_child(item_slots)
for i in 4:
var slot := PanelContainer.new()
slot.custom_minimum_size = Vector2(40, 40)
slot.add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4)))
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
item_slots.add_child(slot)
col.add_child(UiKit.label("状态效果", 14, true))
slot_row = StatusSlotRow.new()
col.add_child(slot_row)
_status_feedback = UiKit.label("", 12, false)
_status_feedback.add_theme_color_override("font_color", UiKit.STONE_FAINT)
col.add_child(_status_feedback)
# 小人立绘占位(程序化色块——真实立绘归 战斗表现)
var portrait := ColorRect.new()
portrait.color = Color(UiKit.HERO_BLUE, 56.0 / 255.0) # 原 Color8(46, 110, 142, 56)
portrait.custom_minimum_size = Vector2(0, 120)
portrait.size_flags_vertical = Control.SIZE_EXPAND_FILL
col.add_child(portrait)
var portrait_glyph := UiKit.label("小人立绘占位", 13, false)
portrait_glyph.add_theme_color_override("font_color", COL_INK)
portrait_glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
portrait_glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
portrait_glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE
portrait_glyph.set_anchors_preset(Control.PRESET_FULL_RECT)
portrait.add_child(portrait_glyph)
# 钱包行(停点前移批 2026-09-23:A 区 28px 顶栏退役——GP/钥石余额常驻位迁本面板底行,
# 建节点归视图、文案装配归 HudPresenter,句柄经 ui_assembler 透传组合根)
var wallet_row := HBoxContainer.new()
wallet_row.add_theme_constant_override("separation", 10)
col.add_child(wallet_row)
gp_label = UiKit.label("金币 0", 15, true)
gp_label.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
wallet_row.add_child(gp_label)
keystone_label = UiKit.label("钥石 0", 15, true)
keystone_label.add_theme_color_override("font_color", UiKit.GOLD)
keystone_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
keystone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
wallet_row.add_child(keystone_label)
func set_hero(hp: int, max_hp: int) -> void:
_hp_bar.max_value = maxi(1, max_hp)
_hp_bar.value = hp
_hp_label.text = "HP %d / %d" % [hp, max_hp]
# 本关强化(预留):锻造/大奖励/关卡奖励节点的属性提升——中循环数据因改变战斗数学准入;
# 与战斗内状态效果(B 区站桩,随战斗消散)分区,防「战斗结束被清」误读;空态整节隐藏
_bonus_section = VBoxContainer.new()
_bonus_section.add_theme_constant_override("separation", 4)
_bonus_section.visible = false
col.add_child(_bonus_section)
_bonus_section.add_child(UiKit.label("本关强化", 14, true))
_bonus_list = VBoxContainer.new()
_bonus_list.add_theme_constant_override("separation", 2)
_bonus_section.add_child(_bonus_list)
func set_attack(atk: int, mod_text: String) -> void:
@@ -105,68 +65,35 @@ func set_attack(atk: int, mod_text: String) -> void:
_atk_label.text = "攻击 %d %s" % [atk, mod_text]
func set_rap(rap: int, cap_hint: int) -> void:
var prev := _last_rap
_last_rap = rap
_rap_label.text = "RAP %d(攒 %d 刷图)" % [rap, cap_hint]
_render_rap_dots(rap)
if prev > 0 and rap == 0:
# battle_over RAP 清零可见反馈(行动经济 D1 义务):数字滚动归零 + 微文案占位
_rap_feedback.text = "随战斗结束消散"
var tw := create_tween()
tw.tween_property(_rap_label, "modulate", Color(1.4, 1.2, 0.7), UiKit.motion(0.12))
tw.tween_property(_rap_label, "modulate", Color(1, 1, 1), UiKit.motion(0.3))
elif rap > 0:
_rap_feedback.text = ""
func set_armor(ac: int) -> void:
# 护甲 AC(2026-09-26 二轮拍板补入)——我方 AC 唯一展示面;掷点判定轴受击侧可读性
_armor_label.text = "护甲 AC %d" % ac
func set_status_effects(effects: Array) -> void:
slot_row.set_effects(effects)
_status_feedback.text = ""
# battle_over:状态槽全清可见反馈(逐槽化光 + 微文案——增益状态×4④)
func set_status_cleared_feedback() -> void:
slot_row.set_cleared_feedback()
_status_feedback.text = "状态随战斗结束消散"
# 本关强化列表(预留接线面):lines = 文案行数组(如「锻造 +2 攻击」)——空数组 = 整节隐藏
func set_level_bonuses(lines: Array) -> void:
for c in _bonus_list.get_children():
_bonus_list.remove_child(c)
c.queue_free()
for line in lines:
var l := UiKit.label(String(line), 13, false)
l.add_theme_color_override("font_color", COL_INK)
_bonus_list.add_child(l)
_bonus_section.visible = not lines.is_empty()
func set_idle() -> void:
_hp_bar.value = 0
_hp_label.text = "HP —"
_atk_label.text = "攻击 —"
_last_rap = 0
_render_rap_dots(0)
_rap_label.text = "RAP —"
slot_row.set_effects([])
_armor_label.text = "护甲 AC —"
set_level_bonuses([])
func hp_text() -> String:
return _hp_label.text
func attack_text() -> String:
return _atk_label.text
func rap_text() -> String:
return _rap_label.text
# 资源面拒绝反馈(pattern 1 resource_face 腿):RAP 数字脉冲
func pulse_rap() -> void:
var tw := create_tween()
tw.tween_property(_rap_label, "modulate", Color(1.0, 0.4, 0.3), UiKit.motion(0.1))
tw.tween_property(_rap_label, "modulate", Color(1, 1, 1), UiKit.motion(0.25))
func _render_rap_dots(rap: int) -> void:
var shown := mini(rap, 16)
while _rap_dots.get_child_count() < shown:
var d := ColorRect.new()
d.custom_minimum_size = Vector2(8, 8)
d.size_flags_vertical = Control.SIZE_SHRINK_CENTER
d.mouse_filter = Control.MOUSE_FILTER_IGNORE
_rap_dots.add_child(d)
for i in _rap_dots.get_child_count():
var d: ColorRect = _rap_dots.get_child(i)
d.color = COL_GOLD if i < shown else COL_DOT_OFF
func armor_text() -> String:
return _armor_label.text
func _data_line() -> Label:
+34 -43
View File
@@ -2,9 +2,9 @@
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
# 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装)
# + 底座;血条悬浮头顶;状态小图标一排挂血条正下方(2026-09-24 状态图标接入批:定稿 PNG 纹理
# 取代程序化占位字形,极性染色退役——绿↑红↓极性语言内化于图标本体;层数 ×N 角标保留;
# 词汇单点复用 StatusSlotRow.EFFECT_ICON,未知键同款 fail-fast + ? 占位)。
# + 底座;血条悬浮头顶;状态效果唯一展示面挂血条正下方(2026-09-26 C 区精简批:compact 档
# StatusSlotRow 组件整体迁入——定稿 PNG 图标+层数 ×N 角标+悬停 tooltip〔来源+语义+剩余〕,
# tooltip 义务自 C 区状态槽并入;空态 = 图标行隐藏)。
# 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 /
# 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持,
# 挖掘期不循环——art-bible §2.1 能量纪律,终稿动画密度归战斗表现批);底座下 护甲类型图标+名字
@@ -28,7 +28,9 @@ var _platform: Panel
var _hp_bar: ProgressBar
var _order_badge: Label = null
var _intent_badge: Label = null
var _status_row: HBoxContainer
var _top_row: VBoxContainer
var slot_row: StatusSlotRow # 状态效果唯一展示面(2026-09-26 C 区精简批——compact 档组件整体迁入;
# 悬停 tooltip 来源+语义+剩余 义务随之从 C 区状态槽并入,主角/敌方共用)
var _name_line: HBoxContainer = null
var _armor_icon: Label = null
var _name_label: Label = null
@@ -46,6 +48,7 @@ func _init(kind: String = "enemy") -> void:
# 头顶悬浮层:血条(敌附顺位徽标)+ 状态小图标行
var top := VBoxContainer.new()
_top_row = top
top.set_anchors_preset(Control.PRESET_TOP_WIDE)
top.offset_left = 8.0
top.offset_right = -8.0
@@ -64,10 +67,12 @@ func _init(kind: String = "enemy") -> void:
_hp_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
hp_row.add_child(_hp_bar)
_status_row = HBoxContainer.new()
_status_row.alignment = BoxContainer.ALIGNMENT_CENTER
_status_row.add_theme_constant_override("separation", 4)
top.add_child(_status_row)
slot_row = StatusSlotRow.new()
slot_row.compact = true # 站桩迷你档——图标 16px+层数角标,tooltip 全明细
slot_row.alignment = BoxContainer.ALIGNMENT_CENTER
slot_row.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
slot_row.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行容器放行鼠标事件给槽位(悬停 tooltip)
top.add_child(slot_row)
# 立绘(程序占位字形)
_portrait = UiKit.label("人" if kind == "hero" else "敌", 42, true)
@@ -164,32 +169,28 @@ func set_idle() -> void:
set_effects([])
# 主角/敌方共用——状态小图标行重建(B 区迷你版:定稿纹理 16px + 层数 ×N 角标;
# 完整 tooltip 义务归 C 区状态槽。2026-09-24 状态图标接入批:占位字形+极性染色退役)
# 主角/敌方共用——状态效果唯一展示面(2026-09-26 C 区精简批:compact 档 StatusSlotRow 整体迁入,
# 原 B 区迷你字形行退役;悬停 tooltip(来源+语义+剩余结算次数)义务自 C 区状态槽并入本组件;
# 空态 = 图标行隐藏不占位(增益状态 EC12))
func set_effects(effects: Array) -> void:
for c in _status_row.get_children():
_status_row.remove_child(c)
c.queue_free()
for eff in effects:
var tex := _icon_tex(String(eff.get("effect_id", "")))
var layers := maxi(1, int(eff.get("layers", 1)))
if tex != null:
var icon := TextureRect.new()
icon.texture = tex
icon.custom_minimum_size = Vector2(16, 16) # 32px 源 ÷2 整数档缩放
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
_status_row.add_child(icon)
else:
var ph := UiKit.label(StatusSlotRow.ICON_PLACEHOLDER, 12, true) # release 回退占位(debug 已断言)
ph.mouse_filter = Control.MOUSE_FILTER_IGNORE
_status_row.add_child(ph)
if layers > 1:
var lab := UiKit.label("×%d" % layers, 12, true)
lab.mouse_filter = Control.MOUSE_FILTER_IGNORE
_status_row.add_child(lab)
slot_row.set_effects(effects)
slot_row.visible = not effects.is_empty()
# battle_over:状态图标行全清可见反馈(增益状态×4④ 载体迁移——逐槽化光 + 微文案,
# 自 C 区状态槽迁入;主角侧调用,敌方不适用)
func set_effects_cleared_feedback() -> void:
slot_row.visible = true
slot_row.set_cleared_feedback()
var note := UiKit.label("状态随战斗结束消散", 11, false)
note.add_theme_color_override("font_color", UiKit.STONE_FAINT)
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
note.mouse_filter = Control.MOUSE_FILTER_IGNORE
_top_row.add_child(note)
var tw := create_tween()
tw.tween_interval(1.2)
tw.tween_property(note, "modulate:a", 0.0, UiKit.motion(0.8))
tw.tween_callback(note.queue_free)
func set_order(i: int) -> void:
@@ -321,13 +322,3 @@ func _refresh_portrait_glyph() -> void:
_portrait.text = "█" # 最大体量
_:
_portrait.text = "敌"
# 状态图标键解析(词汇单点复用 C 区 StatusSlotRow——HUD框架:查不到的键按错误处理,
# 立即断言 + ? 占位 + 诊断,禁止静默显示空图标)
func _icon_tex(effect_id: String) -> Texture2D:
if StatusSlotRow.EFFECT_ICON.has(effect_id):
return StatusSlotRow.EFFECT_ICON[effect_id]
push_error("[站桩] 未知状态图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知状态图标键: " + effect_id)
return null
+23 -8
View File
@@ -53,6 +53,7 @@ var _tip: PanelContainer
var _tip_label: Label
var _slots: Array[PanelContainer] = [] # 槽引用数组持有——tooltip _tip 悬停后入子节点表,按位序 get_child 会误取(BUG-0001)
var _cleared_feedback := false
var compact := false # B 区站桩迷你档(2026-09-26 C 区精简批):16px 图标+层数角标、无 meta 行——tooltip 义务随组件迁入站桩
func _init() -> void:
@@ -103,17 +104,22 @@ func _sync_slots(n: int) -> void:
func _make_slot() -> PanelContainer:
var slot := PanelContainer.new()
slot.custom_minimum_size = Vector2(50, 46)
var sb := UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER)
slot.add_theme_stylebox_override("panel", sb)
if compact:
slot.add_theme_stylebox_override("panel",
UiKit.panel_flat(Color(0, 0, 0, 0.18), UiKit.PANEL_BORDER, 2, 1, Vector4(2, 1, 2, 1)))
else:
slot.custom_minimum_size = Vector2(50, 46)
slot.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER))
slot.mouse_entered.connect(_on_slot_hover.bind(slot))
slot.mouse_exited.connect(func(): _tip.visible = false)
var col := VBoxContainer.new()
col.alignment = BoxContainer.ALIGNMENT_CENTER
if compact:
col.add_theme_constant_override("separation", 0)
slot.add_child(col)
# 定稿纹理图标(32px 源 → 24px 槽内呈现;线性过滤匹配抗锯齿矢量风源图)
var icon := TextureRect.new()
icon.custom_minimum_size = Vector2(24, 24)
icon.custom_minimum_size = Vector2(24, 24) if not compact else Vector2(16, 16)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
@@ -130,16 +136,25 @@ func _fill_slot(slot: PanelContainer, eff: Dictionary) -> void:
slot.set_meta("eff", eff)
var col := slot.get_child(0) as VBoxContainer
var icon := col.get_child(0) as TextureRect
var tex := _icon_tex(String(eff.effect_id))
icon.visible = tex != null
icon.texture = tex
var layers := int(eff.get("layers", 1))
if compact:
# 迷你档:层数角标挂图标下方(×N)——槽随内容自适应,无 meta 行/剩余点(悬停 tooltip 承载全部明细)
var tag := col.get_child(1) as Label
tag.text = "×%d" % layers if layers > 1 else ""
tag.add_theme_font_size_override("font_size", 10)
if tex == null:
tag.text = (ICON_PLACEHOLDER + tag.text).strip_edges()
return
var meta := col.get_child(1) as Label
# 层数角标 + 剩余结算次数点(● 未耗余量)——叠层数字呈现逻辑保持不动
var dots := ""
var left: int = int(eff.get("remaining_settlements", 0))
for i in maxi(left, 1):
dots += "●" if i < left else "○"
meta.text = "×%d %s" % [int(eff.get("layers", 1)), dots]
var tex := _icon_tex(String(eff.effect_id))
icon.visible = tex != null
icon.texture = tex
meta.text = "×%d %s" % [layers, dots]
if tex == null:
meta.text = ICON_PLACEHOLDER + " " + meta.text # release 回退占位(debug 已断言)
+1 -1
View File
@@ -220,7 +220,7 @@ func _run_modal_probes_once() -> void:
_problems.append("明细回看关闭后未恢复")
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
if not _tested_tooltip:
var row = main.info_panel.slot_row
var row = main.stage.hero_standee.slot_row # 状态 tooltip 载体 2026-09-26 迁 B 区站桩
for child in row.get_children():
if child is PanelContainer and child.visible and child.has_meta("eff"):
_tested_tooltip = true
+8 -6
View File
@@ -85,8 +85,8 @@ func _ready() -> void:
problems.append("棋盘格未生成(cells=0)")
if main.board_view.board() == null:
problems.append("board 未装载")
if main.info_panel.hp_text().begins_with("HP —") or main.info_panel.hp_text().contains("—"):
problems.append("C 区血量未显示(%s)" % main.info_panel.hp_text())
if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"):
problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()])
if main.settlement == null or main.settlement.battle_result != "":
problems.append("首战未处于进行中")
if not main.log_view.parsed_text().contains("节点 1/"):
@@ -102,8 +102,10 @@ func _ready() -> void:
problems.append("D 区弃图刷新按钮文案缺失")
if main.board_zone.toast == null:
problems.append("D 区 toast 槽缺失")
if main.info_panel.slot_row == null:
problems.append("C 区状态效果槽缺失")
if main.info_panel.item_slots == null or main.info_panel.item_slots.get_child_count() != 4:
problems.append("C 区魔法道具槽(预留)缺失")
if main.stage.hero_standee.slot_row == null:
problems.append("B 区站桩状态行缺失(状态效果唯一展示面——2026-09-26 C 区精简批)")
if main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0:
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
# E 区道具面板已随可选类删除下线(格面资源批 2026-09-15)——四区构建由其余断言覆盖
@@ -124,7 +126,7 @@ func _ready() -> void:
if main.stage.hero_standee == null or main.stage.hero_standee._hp_bar == null:
problems.append("B 区主角站桩缺失")
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
var slots_row = main.info_panel.slot_row
var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,
"remaining_settlements": 1, "source_class": "safe_cell", "round_index": 1, "effective_value": 50}]
slots_row.set_effects(probe_eff)
@@ -195,7 +197,7 @@ func _ready() -> void:
if problems.is_empty():
print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [
main.board_view._cells.size(), main.board_view.board().width, main.board_view.board().height,
main.info_panel.hp_text(),
main.info_panel.attack_text(),
main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()])
get_tree().quit(0)
else: