Files
gplan-2/game/presentation/views/battle_end_overlay.gd
T

279 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(胜利/失败终局 overlay——金蜡封占位落于扫雷图区中央 + 判定行 +
# 明细回看入口 + 继续/返回;失败 = 暮蓝 #5A6C8C 氛围(明度不变)+ 旗化光点收回占位;
# pattern 6 模态 overlay 层级纪律——遮挡物隐藏/输入锁定断言归场景探针)。
# Date: 2026-09-12(扩展自 node_overlay 的战斗终局职责分离)
class_name BattleEndOverlay
extends Control
signal continue_pressed # M9:review_pressed 退役(P-C7——冒险中回看归「生涯」,06 登记)
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 # M9 三行账(GP/钥石/本关加成——demo ⑧;失败侧整行隐藏=P-C4 维持 OQ14)
var _gp_row: Label
var _ks_row: Label
var _bonus_row_l: Label
var _cele: TextureRect # M9 庆祝立绘(idle 占位浮动——正式帧待资产批)
var _btn: Button
var _board_rect := Rect2()
func _init() -> void:
# 二轮修改批 B(2026-09-29 用户目检裁决):半透明浮层文字不可读 → 实底石板皮面板形态
#(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与
# 按钮全部上板;金蜡封 = 面板头部静态徽记(原全屏坠落演出退役——落点被面板遮挡)。
# 骨架经 UiKit.modal_shell(T4 收口批);_dim 引用摘出供 open_* 换氛围色。
var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, true,
UiKit.panel_skinned(Vector4(16, 14, 16, 14)))
_dim = shell.dim
var box: VBoxContainer = shell.box
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
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)
var seal_img := TextureRect.new()
var flag_tex := UiKit.load_tex("res://assets/art/board/flag_seal.png")
if flag_tex != null:
seal_img.texture = flag_tex
seal_img.custom_minimum_size = Vector2(32, 32)
seal_img.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
seal_img.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
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("", 24, true, UiKit.INK_TITLE)
_verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER
header.add_child(_verdict)
# M9 庆祝立绘(demo ⑧——idle 帧浮动占位;正式帧归资产批;减动效=静帧)
_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)
_gp_row = UiKit.label("", 17, true, UiKit.GOLD_DEEP)
_gp_row.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_rows_box.add_child(_gp_row)
_ks_row = UiKit.label("", 15, true, UiKit.INK_LABEL)
_ks_row.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_ks_row.visible = false
_rows_box.add_child(_ks_row)
_bonus_row_l = UiKit.label("", 14, false, UiKit.INK_LABEL)
_bonus_row_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_bonus_row_l.visible = false
_rows_box.add_child(_bonus_row_l)
box.add_child(HSeparator.new())
# M9:明细回看钮退役(P-C7——冒险中回看归「生涯」)
_btn = UiKit.primary_button("继续")
_btn.pressed.connect(_emit_continue_pressed)
box.add_child(_btn)
visible = false
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入。
# gp_summary(OQ14 口径——胜利总结结算展示 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(COL_WARM, 0.28)
_verdict.text = verdict_line
_seal.visible = true # 胜利徽记(面板头部静态)
_seal.modulate = Color.WHITE
_gp_row.text = gp_summary
_ks_row.text = ks_summary
_ks_row.visible = ks_summary != ""
_bonus_row_l.text = bonus_summary
_bonus_row_l.visible = bonus_summary != ""
_rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != ""
_btn.text = continue_label
_board_rect = board_rect
_cele.visible = true
_play_celebrate()
visible = true
if UiKit.victory_fx_style == "dust":
_play_victory_dust() # 尘金粒子(P-C5 两套并存——开关=调试浮窗,会话级)
else:
_play_crest_assemble() # 碎砖拼纹章(§2.4——样张 v2 冻结批 T3 终稿;默认)
_play_flag_recall(0) # 胜利不放旗收回
if gp_gain >= 0:
_roll_gp(gp_gain, gp_total)
func open_defeat(verdict_line: String, continue_label: String, board_rect: Rect2) -> void:
# 明度不变的暮蓝色相氛围(全屏唯一暮蓝档——§2.5 最静一拍)
_dim.color = Color(COL_DUSK, 0.45)
_verdict.text = verdict_line
_board_rect = board_rect
visible = true
_seal.visible = true
_seal.modulate = Color(0.4, 0.4, 0.4) # M9:失败徽记灰化(demo ⑧;旧全隐藏退役)
_cele.visible = false
_rows_box.visible = false # P-C4 维持 OQ14:失败侧不呈现账行
_play_dust_sadle() # 坐砖掸灰(§2.5——终稿)
_play_flag_recall(8)
func close() -> void:
visible = false
_seal.visible = false
_seal.modulate = Color.WHITE
_cele.visible = false
_clear_crest_bricks()
func is_open() -> bool:
return visible
func verdict_text() -> String:
return _verdict.text
func _fx_center() -> Vector2:
return get_viewport_rect().size * 0.5
# 旗化光点收回(失败——插过的旗逐面化光点收回占位,§2.5)
func _play_flag_recall(count: int) -> void:
_fx_flag_dots = count
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)
# ---- 演出密度终稿(样张 v2 冻结批 T3——程序化 tween,确定性散布,零新帧资产) ----
var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收)
var _fx_crest_shards := 0
var _fx_dust_motes := 0
var _fx_flag_dots := 0
# 碎砖拼纹章(胜利 §2.4——蜡封落定后十枚碎砖自四周向蜡封外环收拢拼成纹章环,驻留至 close)
func _play_crest_assemble() -> void:
_clear_crest_bricks()
_fx_crest_shards = 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))
# 坐砖掸灰(失败 §2.5——暖灰尘絮自棋盘中带缓缓飘落,最静一拍的运动承载)
func _play_dust_sadle() -> void:
_fx_dust_motes = 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)
# M9 庆祝立绘浮动(idle 占位——UiKit.motion 守减动效;motion_scale<1 静帧)
func _play_celebrate() -> void:
if UiKit.motion_scale < 1.0:
return
var tw := _cele.create_tween()
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)
# M9 尘金粒子(P-C5 备选演出——金屑自面板上带飘落闪灭;确定性散布守 S2)
func _play_victory_dust() -> void:
_fx_dust_motes += 10
var c := _fx_center()
for i in 10:
var mote := ColorRect.new()
mote.color = Color8(246, 198, 103)
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)
# M9 GP 行数字滚动(demo ⑧——0.8s 三次缓出;减动效直出终值)
func _roll_gp(gp_gain: int, gp_total: int) -> void:
if UiKit.motion_scale < 1.0:
_gp_row.text = "战利品 GP +%d · 合计 %d" % [gp_gain, gp_total]
return
var tw := create_tween()
tw.tween_method(func(v: float) -> void:
_gp_row.text = "战利品 GP +%d · 合计 %d" % [roundi(gp_gain * v), gp_total],
0.0, 1.0, UiKit.motion(0.8)).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
func _clear_crest_bricks() -> void:
for b in _crest_bricks:
if is_instance_valid(b):
b.queue_free()
_crest_bricks.clear()
# 演出探针出口(fx_probe——胜利纹章/失败掸灰/旗光收回三断言面)
func fx_probe() -> Dictionary:
return {"crest_shards": _fx_crest_shards, "dust_motes": _fx_dust_motes, "flag_dots": _fx_flag_dots}
func _emit_continue_pressed() -> void:
continue_pressed.emit()