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

293 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(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
# 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装)
# + 底座;血条悬浮头顶;状态小图标一排挂血条正下方(形状+色相双通道、层数角标同 C 区状态槽键——
# 词汇单点复用 StatusSlotRow.EFFECT_ICON,未知键同款 fail-fast + ? 占位)。
# 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 /
# 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持,
# 挖掘期不循环——art-bible §2.1 能量纪律,终稿动画密度归战斗表现批);底座下名字·护甲·AC 常驻行
# (护甲类型+AC 恒常驻——敌人技能池批克制对策可读性义务)。
# 死亡呈现(敌人系统 资产下限):灰化 + 摇摆塌陷残位,禁瞬消;受击闪白沿用。
# Date: 2026-09-20(站桩改造批——hero_card_view/enemy_card_view 退役删除)
class_name StandeeView
extends Control
const CHARGE_SKILL_IDS := ["heavy"]
const ARMOR_NAMES := {"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 _status_row: HBoxContainer
var _name_line: Label = null
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _armor_class := 0
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.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)
_status_row = HBoxContainer.new()
_status_row.alignment = BoxContainer.ALIGNMENT_CENTER
_status_row.add_theme_constant_override("separation", 4)
top.add_child(_status_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)
# 底座下名字·护甲·AC 常驻行(敌 only)
if kind == "enemy":
_name_line = UiKit.label("", 12, false)
_name_line.add_theme_color_override("font_color", UiKit.INK_TITLE)
_name_line.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_name_line.clip_text = true
_name_line.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
_name_line.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_name_line.offset_top = -8.0
_name_line.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_name_line)
# ---- 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"))
_armor_class = int(d.get("armor_class", 0))
_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([])
# 主角/敌方共用——状态小图标行重建(B 区迷你版:icon + 层数角标;完整 tooltip 义务归 C 区状态槽)
func set_effects(effects: Array) -> void:
for c in _status_row.get_children():
_status_row.remove_child(c)
c.queue_free()
for eff in effects:
var cfg: Array = _icon_cfg(String(eff.get("effect_id", "")))
var layers := maxi(1, int(eff.get("layers", 1)))
var lab := UiKit.label(String(cfg[0]) + ("×%d" % layers if layers > 1 else ""), 12, true)
lab.add_theme_color_override("font_color", cfg[1])
lab.mouse_filter = Control.MOUSE_FILTER_IGNORE
_status_row.add_child(lab)
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 回退字形
func set_portrait_texture(tex: Texture2D) -> 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.set_anchors_preset(Control.PRESET_FULL_RECT)
_portrait_tex.offset_top = 44.0
_portrait_tex.offset_bottom = -26.0
_portrait_tex.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_portrait_tex)
_portrait_tex.texture = tex
_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:
_name_line.text = "%s · %s·AC%d" % [_display_name, String(ARMOR_NAMES.get(_armor, "轻甲")), _armor_class]
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 = "敌"
# 状态图标键解析(词汇单点复用 C 区 StatusSlotRow——HUD框架:查不到的键按错误处理,
# 立即断言 + ? 占位纹样 + 诊断,禁止静默显示空图标)
func _icon_cfg(effect_id: String) -> Array:
if StatusSlotRow.EFFECT_ICON.has(effect_id):
return StatusSlotRow.EFFECT_ICON[effect_id]
push_error("[站桩] 未知状态图标键(映射表外键——立即断言): %s" % effect_id)
assert(false, "未知状态图标键: " + effect_id)
return ["?", UiKit.PANEL_BORDER, "?"]