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

325 lines
14 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(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 = "敌"