389 lines
16 KiB
GDScript
389 lines
16 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 战斗/冒险终局 overlay(模态):判定行+三行账+明细回看入口+继续/返回,失败侧暮蓝氛围。
|
||
## 摘要文本经 open_victory/open_defeat 由组合根装配喂入——本类零文案装配。
|
||
## 内部数据类 _EndOverlayState=宿主画面登记表(只被本文件填读,文件外零消费者——字段为公开数据成员)。
|
||
class_name BattleEndOverlay
|
||
extends Control
|
||
|
||
signal continue_pressed # 冒险中无回看入口——回看归「生涯」(06 登记)
|
||
|
||
const COL_GOLD := UiKit.GOLD
|
||
const COL_DUSK := UiKit.DUSK # 暮蓝(全作唯一暮蓝档)
|
||
const COL_WARM := UiKit.WARM
|
||
|
||
## 内部状态件(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:
|
||
# 半透明浮层文字不可读 → 实底石板皮面板形态
|
||
#(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与
|
||
# 按钮全部上板;金蜡封 = 面板头部静态徽记(坠落演出落点被面板遮挡)。
|
||
# 骨架经 UiKit.modal_shell;dim 引用摘出供 open_* 换氛围色。
|
||
var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12,
|
||
UiKit.panel_skinned(Vector4(16, 14, 16, 14)))
|
||
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)
|
||
st.seal = Panel.new()
|
||
st.seal.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
var sb := UiKit.panel_flat(COL_GOLD, UiKit.GOLD_DEEP, 200, 4)
|
||
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:
|
||
seal_img.texture = seal_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
|
||
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
|
||
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)
|
||
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())
|
||
|
||
|
||
# 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入。
|
||
# gp_summary(胜利总结结算展示 GP 总量+增量):组合根装配「战利品 +增量 · 合计总量」
|
||
# 传入(入账次序 = LevelRunner 入账先于组合根 handler,runner.gp 即战后总量);空串 = 不显示(失败侧恒空)。
|
||
# 可选尾参收拢 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(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
|
||
st.board_rect = board_rect
|
||
st.cele.visible = true
|
||
_play_celebrate()
|
||
visible = true
|
||
if UiKit.victory_fx_style == "dust":
|
||
_play_victory_dust() # 尘金粒子(P-C5 两套并存——开关=调试浮窗,会话级)
|
||
else:
|
||
_play_crest_assemble() # 碎砖拼纹章(§2.4;默认)
|
||
_play_seal_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 最静一拍)
|
||
st.dim.color = Color(COL_DUSK, 0.6) # 同族压暗
|
||
st.verdict.text = verdict_line
|
||
st.board_rect = board_rect
|
||
visible = true
|
||
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
|
||
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()
|
||
remove_meta(&"celebrate_tw")
|
||
_clear_crest_bricks()
|
||
|
||
|
||
func is_open() -> bool:
|
||
return visible
|
||
|
||
|
||
func verdict_text() -> String:
|
||
return st.verdict.text
|
||
|
||
|
||
# 两列账行工厂(审计差距④——名左/值右,定宽 360)
|
||
func _ledger_row() -> HBoxContainer:
|
||
var r := HBoxContainer.new()
|
||
r.custom_minimum_size = Vector2(360, 0)
|
||
return r
|
||
|
||
|
||
func _ledger_name(size: int, col: Color, _bold: bool) -> Label:
|
||
var l := UiKit.label("", size, true, col)
|
||
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
return l
|
||
|
||
|
||
func _ledger_value(size: int, col: Color, bold: bool) -> Label:
|
||
return UiKit.label("", size, bold, col)
|
||
|
||
|
||
# 拆名/值(组合根传入整串):gp_shape=固定名「战利品 GP」值取余串;其余行按「:」或「 · 」切
|
||
#(全角冒号不入值列——实机「:攻击 +1」缩进 Corrections);无分隔符=整串入值列、名空
|
||
func _set_ledger_row(name_l: Label, val_l: Label, s: String, gp_shape := false) -> void:
|
||
if gp_shape and s.begins_with("战利品 GP"):
|
||
name_l.text = "战利品 GP"
|
||
val_l.text = s.substr(6).strip_edges()
|
||
return
|
||
var sep := s.find(":")
|
||
if sep >= 0:
|
||
name_l.text = s.substr(0, sep)
|
||
val_l.text = s.substr(sep + 1).strip_edges()
|
||
return
|
||
sep = s.find(" · ")
|
||
if sep >= 0:
|
||
name_l.text = s.substr(0, sep)
|
||
val_l.text = s.substr(sep).strip_edges()
|
||
else:
|
||
name_l.text = ""
|
||
val_l.text = s
|
||
|
||
|
||
func _fx_center() -> Vector2:
|
||
return get_viewport_rect().size * 0.5
|
||
|
||
|
||
# 封印化光点收回(失败——插过的封印逐面化光点收回占位,§2.5)
|
||
func _play_seal_recall(count: int) -> void:
|
||
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:
|
||
_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,确定性散布,零新帧资产;计数与碎砖驻留面在 _EndOverlayState) ----
|
||
|
||
|
||
# 碎砖拼纹章(胜利 §2.4——蜡封落定后十枚碎砖自四周向蜡封外环收拢拼成纹章环,驻留至 close)
|
||
func _play_crest_assemble() -> void:
|
||
_clear_crest_bricks()
|
||
st.fx_counts["crest"] = 10
|
||
var c := _fx_center()
|
||
for i in 10:
|
||
_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:
|
||
st.fx_counts["dust"] = 8
|
||
var base := _fx_center()
|
||
for i in 8:
|
||
_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 静帧)
|
||
func _play_celebrate() -> void:
|
||
if UiKit.motion_scale < 1.0:
|
||
return
|
||
var prev: Tween = get_meta(&"celebrate_tw", null)
|
||
if prev != null and prev.is_valid():
|
||
prev.kill()
|
||
var tw := st.cele.create_tween()
|
||
set_meta(&"celebrate_tw", tw)
|
||
tw.set_loops()
|
||
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:
|
||
st.fx_counts["dust"] += 10
|
||
var c := _fx_center()
|
||
for i in 10:
|
||
_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 三次缓出;减动效直出终值)
|
||
func _roll_gp(gp_gain: int, gp_total: int) -> void:
|
||
if UiKit.motion_scale < 1.0:
|
||
_gp_val.text = "+%d · 合计 %d" % [gp_gain, gp_total]
|
||
return
|
||
var tw := create_tween()
|
||
tw.tween_method(func(v: float) -> void:
|
||
_gp_val.text = "+%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 st.crest_bricks:
|
||
if is_instance_valid(b):
|
||
b.queue_free()
|
||
st.crest_bricks.clear()
|
||
|
||
|
||
# 演出探针出口(fx_probe——胜利纹章/失败掸灰/封印光收回三断言面)
|
||
func fx_probe() -> Dictionary:
|
||
return {"crest_shards": st.fx_counts["crest"], "dust_motes": st.fx_counts["dust"], "seal_dots": st.fx_counts["seal"]}
|
||
|
||
|
||
func _emit_continue_pressed() -> void:
|
||
continue_pressed.emit()
|