- 底座下名字行改 HBox 组合:护甲类型图标占位 Label + 名字 Label;文本格式「名字 · 类型·AC%d」退役 - 图标形状+色相双通道占位(◇轻甲·灰石 / ◆魔法护甲·小人蓝 / ▣重甲·深金;避开 Boss 专属紫赤 #8F2F6A 与妨害紫),终稿图标资产归美术批 - _armor_class 字段与 set_data 读取删除(站桩视图零消费方;核心/数据层 armor_class 不动);_armor 保留供图标 - battle_stage_view._card_by_name 与 smoke/ui_probe 名字行断言同步新节点口径(ui_probe 增护甲图标非空检查) - 全量冒烟 557 项 0 失败退出码 0
334 lines
14 KiB
GDScript
334 lines
14 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——2026-09-20 用户看对照样张拍板,台账 99:
|
||
# 主角与敌人均为无框立绘+底座,英雄卡/敌人卡卡片式整体退役禁再提)。
|
||
# 形态契约:无框立绘(程序占位字形;set_portrait_texture = 资产挂点,战斗表现样张批落位零代码换装)
|
||
# + 底座;血条悬浮头顶;状态小图标一排挂血条正下方(2026-09-24 状态图标接入批:定稿 PNG 纹理
|
||
# 取代程序化占位字形,极性染色退役——绿↑红↓极性语言内化于图标本体;层数 ×N 角标保留;
|
||
# 词汇单点复用 StatusSlotRow.EFFECT_ICON,未知键同款 fail-fast + ? 占位)。
|
||
# 敌方专属:顺位徽标悬浮头顶;意图三档动作动画占位(表现层升级批拍板:不显示技能名——普攻零表示 /
|
||
# 攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠ / 蓄力 = ⚔ 金档;开轮摇定播一次性短动画后静帧保持,
|
||
# 挖掘期不循环——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 _status_row: HBoxContainer
|
||
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.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)
|
||
|
||
# 底座下 护甲类型图标+名字 常驻行(敌 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([])
|
||
|
||
|
||
# 主角/敌方共用——状态小图标行重建(B 区迷你版:定稿纹理 16px + 层数 ×N 角标;
|
||
# 完整 tooltip 义务归 C 区状态槽。2026-09-24 状态图标接入批:占位字形+极性染色退役)
|
||
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 tex := _icon_tex(String(eff.get("effect_id", "")))
|
||
var layers := maxi(1, int(eff.get("layers", 1)))
|
||
if tex != null:
|
||
var icon := TextureRect.new()
|
||
icon.texture = tex
|
||
icon.custom_minimum_size = Vector2(16, 16) # 32px 源 ÷2 整数档缩放
|
||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_status_row.add_child(icon)
|
||
else:
|
||
var ph := UiKit.label(StatusSlotRow.ICON_PLACEHOLDER, 12, true) # release 回退占位(debug 已断言)
|
||
ph.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_status_row.add_child(ph)
|
||
if layers > 1:
|
||
var lab := UiKit.label("×%d" % layers, 12, true)
|
||
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 回退字形。
|
||
# 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 = "敌"
|
||
|
||
|
||
# 状态图标键解析(词汇单点复用 C 区 StatusSlotRow——HUD框架:查不到的键按错误处理,
|
||
# 立即断言 + ? 占位 + 诊断,禁止静默显示空图标)
|
||
func _icon_tex(effect_id: String) -> Texture2D:
|
||
if StatusSlotRow.EFFECT_ICON.has(effect_id):
|
||
return StatusSlotRow.EFFECT_ICON[effect_id]
|
||
push_error("[站桩] 未知状态图标键(映射表外键——立即断言): %s" % effect_id)
|
||
assert(false, "未知状态图标键: " + effect_id)
|
||
return null
|