diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index 0ea7aadc..789536fb 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -48,6 +48,7 @@ var info_panel: HeroInfoPanelView # C 区 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 之上、模态之下) # 模态 overlay(z 序 = 登记序;pattern 6 层级纪律) var legend_overlay: LegendOverlay var review_overlay: SettlementReviewOverlay @@ -59,6 +60,7 @@ var _tune_sim = null # 调整板批量模拟控制器(tune_sim_c var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent) var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零) var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) +var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批) func _ready() -> void: @@ -89,6 +91,7 @@ func _build_ui() -> void: board_zone = battle_ui.board_zone board_view = battle_ui.board_view # 探针兼容别名(= board_zone.board_view) ap_bar = battle_ui.ap_bar # 探针兼容别名(= board_zone.ap_bar) + settlement_fx = battle_ui.settlement_fx log_view.review_requested.connect(_open_review_from_band) board_zone.action_requested.connect(_on_board_action) board_zone.stop_pressed.connect(_on_stop_pressed) @@ -392,6 +395,7 @@ func _wire_battle(settlement_ref) -> void: func _on_step_row(row: Dictionary) -> void: log_view.append_row(row) stage.on_step_row_fx(row) + _play_cross_zone_fx(row) # 跨区因果演出(结算演出链批):开格之因 → 站桩之果 # 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道 if String(row.get("type", "")) == "highlight": board_zone.fx.play_perfect_sweep() @@ -400,6 +404,27 @@ func _on_step_row(row: Dictionary) -> void: _refresh_presentations() +# 跨区因果演出(§Transitions 结算逐行兑现——动画密度终稿归战斗表现样张批): +# 伤害行 → ⚔ 弹道自棋盘飞向目标敌方站桩(crit 金档加重);治疗行 → ✚ 弹道飞向主角站桩; +# 命中反馈(闪白/血条递减/灰化)由站桩侧 on_step_row_fx/set_data 承接 +func _play_cross_zone_fx(row: Dictionary) -> void: + var from_v: Vector2 = board_view.get_global_rect().get_center() + match String(row.get("type", "")): + "damage": + var target := String(row.get("target", "")) + if target == "" or target == "hero": + return + var to_v = stage.enemy_standee_center(target) + if to_v != null: + var crit := bool(row.get("crit", false)) + settlement_fx.play_projectile(from_v, to_v, "⚔", + UiKit.GOLD if crit else UiKit.FLAG_RED, crit) + "heal": + if String(row.get("target", "")) != "hero" and String(row.get("target", "")) != "": + return + settlement_fx.play_projectile(from_v, stage.hero_standee_center(), "✚", Color("7cc47c")) + + func _on_round_started(ri: int) -> void: log_view.finish_settlement_round() # 上轮摘要行(挖掘期 1 行——明细带 pattern 3) log_view.append_bb("[color=#e0b860]── 第 %d 轮 ──[/color]" % ri) @@ -557,6 +582,9 @@ func _refresh_presentations() -> void: func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() + if settling and not _was_settling: + settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带(演出链批) + _was_settling = settling board_view.set_desaturated(settling) board_view.set_dig_glow_enabled(not settling) stage.set_settle_focus(settling) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd index 53de7db6..eadff315 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd @@ -7,8 +7,9 @@ extends RefCounted -# 战斗屏四区(A 进度条 / B 战斗画面 / C·D 两列——E 区道具面板已随可选类删除下线,2026-09-15 格面资源批)。 -# 返回句柄字典:{strip_view, stage, log_view, info_panel, board_zone, board_view, ap_bar} +# 战斗屏四区(A 进度条 / B 战斗画面 / C·D 两列——E 区道具面板已随可选类删除下线,2026-09-15 格面资源批) +# + 结算演出覆盖层(A–D 之上、模态之下——跨区弹道/光带载体,结算演出链批 2026-09-20)。 +# 返回句柄字典:{strip_view, stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx} 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 用户报告回归) @@ -41,6 +42,10 @@ static func build_battle_ui(root: Control, db) -> Dictionary: bottom.add_child(board_zone) board_zone.ap_bar.set_refresh_cost(int(db.action_economy.rap_refresh_cost)) # RAP 读数表值驱动 + # 结算演出覆盖层(A–D 之上——z 序 = addChild 序,模态覆盖层组在 build_overlays 中更晚挂入) + var settlement_fx := SettlementFxView.new() + root.add_child(settlement_fx) + return { "strip_view": strip_view, "stage": stage, @@ -49,6 +54,7 @@ static func build_battle_ui(root: Control, db) -> Dictionary: "board_zone": board_zone, "board_view": board_zone.board_view, "ap_bar": board_zone.ap_bar, + "settlement_fx": settlement_fx, } diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd index 4819e9dc..bdc52fc5 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/battle_stage_view.gd @@ -110,6 +110,18 @@ func _card_by_name(display_name: String) -> StandeeView: return null +# 跨区弹道目标坐标(结算演出链批——组合根传给 SettlementFxView 的全局锚点) +func enemy_standee_center(display_name: String): + var card := _card_by_name(display_name) + if card == null or not is_instance_valid(card): + return null + return card.get_global_rect().get_center() + + +func hero_standee_center() -> Vector2: + return hero_standee.get_global_rect().get_center() + + # battle_over:全场敌方站桩灰化兜底(短路击杀等未走 killed 行的边角) func gray_all_enemies() -> void: for c in _cards: @@ -117,6 +129,7 @@ func gray_all_enemies() -> void: c.mark_dead() -# 结算期 B 区第一注视点提示(演出区文案切换——能量 7/10 清算调性占位) +# 结算期 B 区第一注视点切换(能量 7/10 清算调性):结算中占位文案退位——兑现演出由 +# SettlementFxView 弹道/光带本体接管(演出链批);挖掘期恢复教学占位文案 func set_settle_focus(on: bool) -> void: - stage_center.text = "兑现演出区\n(结算中——逐行兑现)" if on else "兑现演出区\n(结算期:光带 → 弹道 → 受击)" + stage_center.text = "" if on else "兑现演出区\n(结算期:光带 → 弹道 → 受击)" diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/settlement_fx_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/settlement_fx_view.gd new file mode 100644 index 00000000..5c13b10b --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/settlement_fx_view.gd @@ -0,0 +1,56 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 实现 GDD:design/ux/battle.md(§Transitions 结算逐行兑现——B 区因果演出链骨架: +# 结算轮光带自棋盘区横扫 → 伤害/治疗弹道自棋盘飞向敌我站桩 → 受击闪白与血条递减由站桩侧承接)。 +# 位置语义:全屏覆盖层(A–D 区之上、模态之下),只做跨区飞行的演出载体——棋盘区内演出 +# (清图金粒/完美排雷横幅/标中旗弹道/Boss 影子)仍归 board_fx_view。 +# 高内聚边界:纯演出——组合根按结算信号调用 play_*,传全局坐标(组合根持有棋盘与站桩两域句柄); +# 本类零核心依赖、不自持游戏状态、mouse_filter 全忽略(不劫持输入)。 +# 动画密度/缓动曲线归战斗表现样张批终稿——本批落"因果全程可见"骨架(规划兑现 P2 义务)。 +# Date: 2026-09-20(结算演出链批) +class_name SettlementFxView +extends Control + + +func _init() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + set_anchors_preset(Control.PRESET_FULL_RECT) + + +# 结算轮光带:金橙半透明横带自棋盘上缘扫至下缘(一次 0.5s,自动回收)—— +# "棋盘能量向战斗画面汇聚"的开轮语义(§2.2 色彩转金橙主导;区分于 board_fx 的完美排雷金光带) +func play_round_sweep(board_rect: Rect2) -> void: + if board_rect.size == Vector2.ZERO: + return + var band := ColorRect.new() + band.color = Color(UiKit.GOLD, 0.22) + band.mouse_filter = Control.MOUSE_FILTER_IGNORE + band.position = Vector2(board_rect.position.x, board_rect.position.y) + band.size = Vector2(board_rect.size.x, 34.0) + add_child(band) + var tw := band.create_tween() + tw.tween_property(band, "position:y", board_rect.end.y - 34.0, 0.5).set_trans(Tween.TRANS_SINE) + tw.parallel().tween_property(band, "modulate:a", 0.0, 0.5).set_delay(0.15) + tw.tween_callback(band.queue_free) + + +# 跨区弹道:glyph 字形自 from 飞向 to(0.35s + 轻微上抛弧线 + 终点淡出,自动回收)—— +# 开格之因 → 站桩之果的因果可视承载(P2);crit 弹道由调用方传加重标记 +func play_projectile(from: Vector2, to: Vector2, glyph: String, col: Color, crit: bool = false) -> void: + var shot := UiKit.label(glyph, 22 if crit else 16, true) + shot.add_theme_color_override("font_color", col) + shot.mouse_filter = Control.MOUSE_FILTER_IGNORE + shot.size = Vector2(24, 24) + shot.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + shot.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + shot.position = from - shot.size * 0.5 + shot.pivot_offset = shot.size * 0.5 + shot.z_index = 5 + add_child(shot) + var mid := (from + to) * 0.5 + Vector2(0, -36.0) # 轻微上抛弧线 + var tw := shot.create_tween() + if crit: + tw.tween_property(shot, "scale", Vector2(1.5, 1.5), 0.08) + tw.tween_property(shot, "position", mid, 0.16).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) + tw.tween_property(shot, "position", to - shot.size * 0.5, 0.19).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) + tw.parallel().tween_property(shot, "modulate:a", 0.0, 0.12).set_delay(0.23) + tw.tween_callback(shot.queue_free) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/settlement_fx_view.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/settlement_fx_view.gd.uid new file mode 100644 index 00000000..a741a9f3 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/settlement_fx_view.gd.uid @@ -0,0 +1 @@ +uid://mkuyrjn8w8ew diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd index 0278df0c..93097766 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/standee_view.gd @@ -30,6 +30,7 @@ var _enemy_id := "" var _armor := "light" var _armor_class := 0 var _dead := false +var _hp_tween: Tween func _init(kind: String = "enemy") -> void: @@ -121,7 +122,7 @@ func set_data(d: Dictionary) -> void: _armor = String(d.get("armor_type", "light")) _armor_class = int(d.get("armor_class", 0)) _hp_bar.max_value = maxi(1, int(d.get("max_hp", 1))) - _hp_bar.value = int(d.get("hp", 0)) + _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", [])) @@ -133,11 +134,20 @@ func set_data(d: Dictionary) -> void: func set_hero(hp: int, max_hp: int) -> void: _hp_bar.max_value = maxi(1, max_hp) - _hp_bar.value = hp + _set_hp(hp) + + +# 血条短 tween 递减(§2.2 直角递减占位——密度/缓动终稿归战斗表现批);逐行刷新时重启 tween 不累积 +func _set_hp(hp: int) -> void: + var target := float(clampi(hp, 0, int(_hp_bar.max_value))) + if _hp_tween != null and _hp_tween.is_valid(): + _hp_tween.kill() + _hp_tween = create_tween() + _hp_tween.tween_property(_hp_bar, "value", target, 0.15) func set_idle() -> void: - _hp_bar.value = 0 + _set_hp(0) set_effects([]) @@ -227,7 +237,7 @@ func mark_dead() -> void: _refresh_portrait_glyph() if _intent_badge != null: _intent_badge.visible = false - _hp_bar.value = 0 + _set_hp(0) func is_dead() -> bool: