325 lines
14 KiB
GDScript
325 lines
14 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
|
||
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
|
||
# 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装)
|
||
# + 底座;血条悬浮头顶;状态效果唯一展示面挂血条正下方(2026-09-26 C 区精简批:compact 档
|
||
# StatusSlotRow 组件整体迁入——定稿 PNG 图标+层数 ×N 角标+悬停 tooltip〔来源+语义+剩余〕,
|
||
# tooltip 义务自 C 区状态槽并入;空态 = 图标行隐藏)。
|
||
# 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 /
|
||
# 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持,
|
||
# 挖掘期不循环——art-bible §2.1 能量纪律,终稿动画密度归战斗表现批);底座下 护甲类型图标+名字
|
||
# 常驻行(2026-09-26 用户终裁:AC 数值不展示、护甲类型图标化——敌人技能池批克制对策可读性
|
||
# 义务降级承载:类型保留可读、数值移除;HP 信息已足够)。
|
||
# 死亡呈现(敌人系统 资产下限):灰化 + 摇摆塌陷残位,禁瞬消;受击闪白沿用。
|
||
# Date: 2026-09-20(站桩改造批——hero_card_view/enemy_card_view 退役删除)
|
||
class_name StandeeView
|
||
extends Control
|
||
|
||
const CHARGE_SKILL_IDS := ["heavy"]
|
||
# 护甲类型图标占位——终稿图标资产归美术批(2026-09-26 用户终裁:AC 数值不展示,类型图标化)。
|
||
# 形状+色相双通道(art-bible):轻甲=◇ 空心菱(轻薄)·灰石中性 / 魔法护甲=◆ 实心晶菱·小人蓝
|
||
# (避开 Boss 专属紫赤 #8F2F6A 与妨害紫——两者各有专属语义)/ 重甲=▣ 实心方板(厚重板甲)·深金。
|
||
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||
|
||
var _kind := "enemy"
|
||
var _portrait: Label
|
||
var _portrait_tex: TextureRect = null # 资产挂点(null = 未落位,字形在位)
|
||
var _platform: Panel
|
||
var _hp_bar: ProgressBar
|
||
var _order_badge: Label = null
|
||
var _intent_badge: Label = null
|
||
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
|
||
var _display_name := ""
|
||
var _enemy_id := ""
|
||
var _armor := "light"
|
||
var _dead := false
|
||
var _hp_tween: Tween
|
||
|
||
|
||
func _init(kind: String = "enemy") -> void:
|
||
_kind = kind
|
||
custom_minimum_size = Vector2(150, 176)
|
||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
|
||
# 头顶悬浮层:血条(敌附顺位徽标)+ 状态小图标行
|
||
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
|
||
top.add_theme_constant_override("separation", 2)
|
||
add_child(top)
|
||
var hp_row := HBoxContainer.new()
|
||
hp_row.add_theme_constant_override("separation", 4)
|
||
top.add_child(hp_row)
|
||
if kind == "enemy":
|
||
_order_badge = UiKit.label("①", 13, true)
|
||
_order_badge.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
|
||
hp_row.add_child(_order_badge)
|
||
_hp_bar = ProgressBar.new()
|
||
_hp_bar.custom_minimum_size = Vector2(0, 10)
|
||
_hp_bar.show_percentage = false
|
||
_hp_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_hp_bar.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
hp_row.add_child(_hp_bar)
|
||
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)
|
||
_portrait.add_theme_color_override("font_color", UiKit.HERO_BLUE if kind == "hero" else UiKit.RED_DEEP)
|
||
_portrait.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_portrait.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
_portrait.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_portrait.offset_top = 44.0
|
||
_portrait.offset_bottom = -26.0
|
||
_portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(_portrait)
|
||
|
||
# 意图指示(敌 only——立绘上半部居中悬浮)
|
||
if kind == "enemy":
|
||
_intent_badge = UiKit.label("", 15, true)
|
||
_intent_badge.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
||
_intent_badge.offset_left = -20.0
|
||
_intent_badge.offset_right = 20.0
|
||
_intent_badge.offset_top = 74.0
|
||
_intent_badge.offset_bottom = 98.0
|
||
_intent_badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_intent_badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_intent_badge.visible = false
|
||
add_child(_intent_badge)
|
||
|
||
# 底座
|
||
_platform = Panel.new()
|
||
_platform.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
||
_platform.offset_left = 26.0
|
||
_platform.offset_right = -26.0
|
||
_platform.offset_top = -18.0
|
||
_platform.offset_bottom = -8.0
|
||
_platform.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_platform.add_theme_stylebox_override("panel",
|
||
UiKit.panel_flat(UiKit.STONE.darkened(0.35), UiKit.STONE.darkened(0.1), 6, 1))
|
||
add_child(_platform)
|
||
|
||
# 底座下 护甲类型图标+名字 常驻行(敌 only——2026-09-26 用户终裁:AC 数值不展示,类型图标化)
|
||
if kind == "enemy":
|
||
_name_line = HBoxContainer.new()
|
||
_name_line.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
_name_line.add_theme_constant_override("separation", 3)
|
||
_name_line.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_name_line.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
||
_name_line.offset_top = -8.0
|
||
add_child(_name_line)
|
||
_armor_icon = UiKit.label("", 12, true)
|
||
_armor_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_name_line.add_child(_armor_icon)
|
||
_name_label = UiKit.label("", 12, false)
|
||
_name_label.add_theme_color_override("font_color", UiKit.INK_TITLE)
|
||
_name_label.clip_text = true
|
||
_name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
_name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_name_line.add_child(_name_label)
|
||
|
||
|
||
# ---- setter 面(投影契约与旧敌卡一致——presenter 装配面零改排) ----
|
||
|
||
# data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id,
|
||
# pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)}
|
||
func set_data(d: Dictionary) -> void:
|
||
_display_name = String(d.get("display_name", "敌人"))
|
||
_enemy_id = String(d.get("enemy_id", ""))
|
||
_armor = String(d.get("armor_type", "light"))
|
||
_hp_bar.max_value = maxi(1, int(d.get("max_hp", 1)))
|
||
_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", []))
|
||
_update_name_line()
|
||
_refresh_portrait_glyph()
|
||
if _dead and int(d.get("hp", 0)) > 0:
|
||
_revive()
|
||
|
||
|
||
func set_hero(hp: int, max_hp: int) -> void:
|
||
_hp_bar.max_value = maxi(1, max_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, UiKit.motion(0.15))
|
||
|
||
|
||
func set_idle() -> void:
|
||
_set_hp(0)
|
||
set_effects([])
|
||
|
||
|
||
# 主角/敌方共用——状态效果唯一展示面(2026-09-26 C 区精简批:compact 档 StatusSlotRow 整体迁入,
|
||
# 原 B 区迷你字形行退役;悬停 tooltip(来源+语义+剩余结算次数)义务自 C 区状态槽并入本组件;
|
||
# 空态 = 图标行隐藏不占位(增益状态 EC12))
|
||
func set_effects(effects: Array) -> void:
|
||
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:
|
||
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
|
||
if _order_badge != null:
|
||
_order_badge.text = String("①②③"[clampi(i - 1, 0, 2)])
|
||
|
||
|
||
# 摇定动作渲染(敌人技能池批 三档可见性;渲染映射唯一定义处归本类——code-review 2026-09-12 先例):
|
||
# pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;"" = 未摇定/死亡残位)。
|
||
# 表现层升级批拍板:不显示技能名——类型级图标 + 一次性动作动画(_play_intent_motion)。
|
||
# 信号/入参契约:set_data 每结算行刷新、set_pending 每轮开轮刷新(开轮摇号时点)。
|
||
func set_pending(ptype: String, pid: String) -> void:
|
||
if _intent_badge == null or _dead:
|
||
return
|
||
if ptype == "" or ptype == "basic":
|
||
_intent_badge.visible = false # 普通攻击零表示(完全不可预测——§2.4)
|
||
return
|
||
var glyph := "⚔" # 攻击系(damage)
|
||
var col: Color = UiKit.FLAG_RED
|
||
if ptype == "buff":
|
||
glyph = "✦" # 增益系
|
||
col = UiKit.HERO_BLUE
|
||
elif ptype == "debuff":
|
||
glyph = "☠" # 妨害系(新效应词汇批 2026-09-19)
|
||
col = UiKit.DEBUFF_PURPLE
|
||
if pid in CHARGE_SKILL_IDS:
|
||
col = UiKit.GOLD_DEEP # 重击类蓄力档(2026-09-18 用户拍板独立蓄力提示)
|
||
_intent_badge.text = glyph
|
||
_intent_badge.add_theme_color_override("font_color", col)
|
||
_intent_badge.visible = true
|
||
_play_intent_motion()
|
||
|
||
|
||
# 三档动作动画占位:摇定时一次性短动画(意图图标弹入 + 立绘前倾回正),随后静帧保持——
|
||
# 挖掘期唯一在动元素 = 可挖域微光(art-bible §2.1);终稿动画密度归战斗表现样张批
|
||
func _play_intent_motion() -> void:
|
||
_intent_badge.pivot_offset = _intent_badge.size * 0.5
|
||
_portrait.pivot_offset = _portrait.size * 0.5
|
||
var tw := create_tween()
|
||
tw.tween_property(_intent_badge, "scale", Vector2(1.6, 1.6), UiKit.motion(0.10))
|
||
tw.tween_property(_intent_badge, "scale", Vector2.ONE, UiKit.motion(0.16))
|
||
tw.parallel().tween_property(_portrait, "rotation_degrees", 4.0, UiKit.motion(0.10))
|
||
tw.tween_property(_portrait, "rotation_degrees", 0.0, UiKit.motion(0.14))
|
||
|
||
|
||
# 资产挂点(战斗表现样张批):tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
|
||
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右——2026-09-26 用户点名英雄立绘水平向右)。
|
||
# 盒高 96px = 48px 立绘源整数 2×(圣经 §8.2 禁非整数缩放;offset_top 44 = 血条+状态行之下净空)——
|
||
# 源换代时随源重算盒高。
|
||
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
|
||
if _portrait_tex == null:
|
||
_portrait_tex = TextureRect.new()
|
||
_portrait_tex.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
_portrait_tex.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
_portrait_tex.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊(棋盘小人贴图批 2026-09-25)
|
||
_portrait_tex.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_portrait_tex.offset_top = 44.0
|
||
_portrait_tex.offset_bottom = -36.0 # 盒高 96 = 48 源 ×2 整数档(原 −26 盒高 106 系非整数缩放)
|
||
_portrait_tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(_portrait_tex)
|
||
_portrait_tex.texture = tex
|
||
_portrait_tex.flip_h = flip
|
||
_portrait_tex.visible = tex != null
|
||
_portrait.visible = tex == null
|
||
|
||
|
||
func mark_dead() -> void:
|
||
if _dead:
|
||
return
|
||
_dead = true
|
||
modulate = Color(0.62, 0.62, 0.62, 0.9)
|
||
var tw := create_tween()
|
||
tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12))
|
||
tw.tween_property(self, "rotation_degrees", -1.5, UiKit.motion(0.1))
|
||
tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08))
|
||
_refresh_portrait_glyph()
|
||
if _intent_badge != null:
|
||
_intent_badge.visible = false
|
||
_set_hp(0)
|
||
|
||
|
||
func is_dead() -> bool:
|
||
return _dead
|
||
|
||
|
||
# 受击闪白(结算演出占位——§2.2 受击反馈)
|
||
func flash_hit() -> void:
|
||
var tw := create_tween()
|
||
tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
|
||
tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2))
|
||
|
||
|
||
func _revive() -> void:
|
||
_dead = false
|
||
modulate = Color(1, 1, 1)
|
||
rotation_degrees = 0.0
|
||
_refresh_portrait_glyph()
|
||
_update_name_line()
|
||
|
||
|
||
func _update_name_line() -> void:
|
||
if _name_line == null:
|
||
return
|
||
_name_label.text = _display_name
|
||
var col: Color = UiKit.STONE # 轻甲 = 灰石(中性轻薄)
|
||
match _armor:
|
||
"ward":
|
||
col = UiKit.HERO_BLUE # 魔法护甲 = 小人蓝(避开 Boss 专属紫赤/妨害紫)
|
||
"heavy":
|
||
col = UiKit.GOLD_DEEP # 重甲 = 深金(厚重达意金)
|
||
_armor_icon.text = String(ARMOR_ICONS.get(_armor, "◇"))
|
||
_armor_icon.add_theme_color_override("font_color", col)
|
||
|
||
|
||
func _refresh_portrait_glyph() -> void:
|
||
if _kind == "hero":
|
||
_portrait.text = "人"
|
||
return
|
||
if _dead:
|
||
_portrait.text = "✕"
|
||
return
|
||
match _enemy_id:
|
||
"goblin":
|
||
_portrait.text = "▂" # 小体量墩形
|
||
"bugbear":
|
||
_portrait.text = "▆" # 垂直延伸体量
|
||
"ogre_chief":
|
||
_portrait.text = "█" # 最大体量
|
||
_:
|
||
_portrait.text = "敌"
|