diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index b8c104cc..09088550 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -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 双源拒绝反馈) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 59920f0e..ad9359f1 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -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() diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index 38a65783..a7aa7b8c 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -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, diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index 409c042c..4c71dfd9 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -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 diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 0b023817..acbcba0d 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -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: diff --git a/game/presentation/views/standee_view.gd b/game/presentation/views/standee_view.gd index e44dfe1f..670d6aa8 100644 --- a/game/presentation/views/standee_view.gd +++ b/game/presentation/views/standee_view.gd @@ -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 diff --git a/game/presentation/views/status_slot_row.gd b/game/presentation/views/status_slot_row.gd index 6197e5fa..5973b54a 100644 --- a/game/presentation/views/status_slot_row.gd +++ b/game/presentation/views/status_slot_row.gd @@ -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 已断言) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 4994f8d3..aef57215 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -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 diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index b77a7c1a..25d8b0c8 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -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: