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

492 lines
22 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
## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。
## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。
## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。
extends Node2D
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 /
# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
# 名行/底座纵向预算余量(盒底−1px「声明值刚好=容器值」系高危写法;
# ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit)
const STANDEE_FOOT_MARGIN := 12.0
var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入)
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入)
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界)
var _kind := "enemy"
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
var _portrait_flip := false
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
var _hero_tex := {}
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化)
const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"}
const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT}
var _hero_state: int = HeroState.IDLE
var _state_revert_tw: Tween
var _order := 1
var _intent_glyph := ""
var _intent_col := Color.WHITE
var _intent_visible := false
var _intent_scale := 1.0: # 意图弹入动画参数
set(v):
_intent_scale = v
queue_redraw()
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
set(v):
_glyph_rot = v
queue_redraw()
var _display_name := ""
var _enemy_id := ""
var _armor := "light"
var _dead := false
var _hp_max := 1.0
var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」)
var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积)
set(v):
hp_value = clampf(v, 0.0, 1.0)
queue_redraw()
var _hp_tween: Tween
var _hp_flash_tw: Tween
func _init(kind: String = "enemy") -> void:
_kind = kind
func _efont() -> Font:
return font if font != null else ThemeDB.fallback_font
func display_name() -> String:
return _display_name
# 血条存在性探针出口(世界直角血条常在)
var has_world_hp_bar := true
# 意图徽标探针出口
func intent_probe() -> Dictionary:
return {"visible": _intent_visible, "text": _intent_glyph}
# 名字行探针出口
func name_line_probe() -> Dictionary:
return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))}
# ---- 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_max = float(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", []))
_refresh_portrait_glyph()
if _dead and int(d.get("hp", 0)) > 0:
_revive()
queue_redraw()
func set_hero(hp: int, max_hp: int) -> void:
_hp_max = float(maxi(1, max_hp))
_set_hp(hp)
if _dead and hp > 0:
_revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义)
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
func _set_hp(hp: int) -> void:
_hp_cur = clampi(hp, 0, int(_hp_max))
var target := float(_hp_cur) / _hp_max
if _hp_tween != null and _hp_tween.is_valid():
_hp_tween.kill()
_hp_tween = create_tween()
_hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))
# 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位)
func set_effects(effects: Array) -> void:
if slot_row == null:
return
slot_row.set_effects(effects)
slot_row.visible = not effects.is_empty()
# battle_over:状态图标行全清可见反馈(微文案挂伴随件父层 = 壳层)
func set_effects_cleared_feedback() -> void:
if slot_row == null:
return
slot_row.visible = true
slot_row.set_cleared_feedback()
var note := UiKit.label("状态随战斗结束消散",UiKit.FS_CAPTION, false, UiKit.STONE_FAINT)
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
note.mouse_filter = Control.MOUSE_FILTER_IGNORE
var parent := slot_row.get_parent()
if parent != null:
parent.add_child(note)
note.position = slot_row.position + Vector2((slot_row.size.x - 134.0) * 0.5, slot_row.size.y)
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:
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
_order = i
queue_redraw()
# 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;
# "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。
func set_pending(ptype: String, pid: String) -> void:
if _dead:
_intent_visible = false
queue_redraw()
return
if ptype == "" or ptype == "basic":
_intent_visible = false # 普通攻击零表示(完全不可预测——§2.4)
queue_redraw()
return
var glyph := "⚔" # 攻击系(damage)
var col: Color = UiKit.RED_MINE # 攻击预告红(危险红族——预告语义归封印色独占〔现行封印黑〕)
if ptype == "buff":
glyph = "✦" # 增益系
col = UiKit.HERO_BLUE_DEEP # 深档墨(浅垫牌上 ≥4.5:1)
elif ptype == "debuff":
glyph = "☠" # 妨害系
col = UiKit.DEBUFF_PURPLE
if pid in CHARGE_SKILL_IDS:
col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1)
_intent_glyph = glyph
_intent_col = col
_intent_visible = true
_play_intent_motion()
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
func _play_intent_motion() -> void:
var tw := create_tween()
tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10))
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16))
tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10))
tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14))
if _intent_col == UiKit.GOLD_DEEP:
tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.18))
else:
tw.tween_property(self, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.13))
# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。
# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2×
# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。
func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void:
_portrait_flip = flip
if tex == null:
if _portrait_sprite != null:
_portrait_sprite.visible = false
queue_redraw()
return
_portrait_tex = tex
if _portrait_sprite == null:
_portrait_sprite = Sprite2D.new()
_portrait_sprite.centered = false
_portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
add_child(_portrait_sprite)
_portrait_sprite.texture = tex
_portrait_sprite.flip_h = flip
_portrait_sprite.visible = true
_place_portrait()
func _place_portrait() -> void:
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
if _portrait_sprite == null or _portrait_tex == null:
return
var box_h := box.y - 44.0 - 36.0
var w := _portrait_tex.get_width() * 2.0
var h := _portrait_tex.get_height() * 2.0
_portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
_portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
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))
_spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s)
_refresh_portrait_glyph()
_intent_visible = false # 死亡残位意图退位
_set_hp(0)
if slot_row != null:
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏
# ---- 演出密度(程序化 tween,零新帧资产) ----
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
# 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机)
func _spawn_death_shards() -> void:
_death_shards = DEATH_SHARD_N
var center := _portrait_rect().get_center()
for i in DEATH_SHARD_N:
var shard := ColorRect.new()
shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2)
shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2)
shard.mouse_filter = Control.MOUSE_FILTER_IGNORE
shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0
shard.rotation = float(i) * 0.7
add_child(shard)
var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3))
var peak := shard.position + dir
var stw := shard.create_tween()
stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27))
stw.tween_callback(shard.queue_free)
# 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面)
func fx_probe() -> Dictionary:
return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count}
# 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向
# i*60°——禁全局随机;亮金/暖白交替=物理打击语义,不占 危险红=雷 与 奖励金=收益 的语义位)。
# 弹道到达拍由 battle_stage_view 调度(on_step_row_fx 延迟 0.33s≈弹道飞行)。
func play_hit_impact() -> void:
var center := _portrait_rect().get_center()
for i in HIT_SPARK_N:
_hit_sparks += 1
var spark := ColorRect.new()
spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM
spark.size = Vector2(4, 3)
spark.mouse_filter = Control.MOUSE_FILTER_IGNORE
spark.position = center - spark.size * 0.5
add_child(spark)
var ang := TAU * float(i) / float(HIT_SPARK_N)
var dir := Vector2(cos(ang), sin(ang)) * 16.0
var stw := spark.create_tween()
stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12))
stw.tween_callback(spark.queue_free)
func is_dead() -> bool:
return _dead
# 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行)
func flash_hit() -> void:
_flash_count += 1
if _hp_flash_tw != null and _hp_flash_tw.is_valid():
_hp_flash_tw.kill()
_hp_flash_tw = create_tween()
_hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
_hp_flash_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
if _kind == "hero":
_hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
set_hero_state("idle")
_refresh_portrait_glyph()
if slot_row != null:
slot_row.visible = true
queue_redraw()
func _refresh_portrait_glyph() -> void:
queue_redraw()
func _portrait_glyph() -> String:
# 字形回退仅作缺位兜底(八敌真图落位;相对体量语义由 48px 档统一比例承载)。
# 缺位态:主角「人」/死亡「✕」/其余「敌」。
if _kind == "hero":
return "人"
if _dead:
return "✕"
return "敌"
# 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面)
func portrait_probe() -> Dictionary:
return {"has_tex": _portrait_sprite != null and _portrait_sprite.texture != null}
# ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ----
# 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理)
func register_hero_state(state: String, tex: Texture2D) -> void:
if tex != null:
_hero_tex[state] = tex
# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。
# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。
func set_hero_state(state: String) -> void:
var target: int = _HS_VALUE.get(state, -1)
if _kind != "hero" or target == _hero_state:
return
if not _hero_tex.has(state) and state != "idle":
return
_hero_state = target
if _portrait_sprite != null and _hero_tex.has(state):
_portrait_sprite.texture = _hero_tex[state]
if _state_revert_tw != null and _state_revert_tw.is_valid():
_state_revert_tw.kill()
if state != "idle":
_state_revert_tw = create_tween()
_state_revert_tw.tween_interval(UiKit.motion(0.35))
_state_revert_tw.tween_callback(_settle_hero_idle)
func _settle_hero_idle() -> void:
_hero_state = HeroState.IDLE
if _portrait_sprite != null and _hero_tex.has("idle"):
_portrait_sprite.texture = _hero_tex["idle"]
# 三态探针出口(fx_probe——当前态名+注册态集合)
func hero_state_probe() -> Dictionary:
return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()}
func _portrait_color() -> Color:
return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP
# 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用
func _portrait_rect() -> Rect2:
return Rect2(0, 44, box.x, box.y - 80.0)
# 石板地台矩形(_draw 与 content_rect 共用几何——单一来源防两处漂移)
func _pedestal_rect() -> Rect2:
return Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0)
# 绘制内容矩形(世界局部;ui_struct_probe ⑥standee「不出框+下余量≥8px」断言读数面——
# 最低绘制元素 = 底座下缘)
func content_rect() -> Rect2:
return Rect2(0.0, 0.0, box.x, _pedestal_rect().end.y)
func _draw() -> void:
var f := _efont()
# 椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
# 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
var sh_center := Vector2(box.x * 0.5, box.y - 24.0)
var sh_rx := (box.x - 36.0) * 0.5
var sh_ry := 9.0
var pts := PackedVector2Array()
for i in 24:
var ang := TAU * float(i) / 24.0
pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry))
draw_colored_polygon(pts, Color(0, 0, 0, 0.27))
# 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族
# ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕)
var ped := _pedestal_rect()
draw_rect(ped, UiKit.STAND_STONE, true)
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
# 血条(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8
var bar_x := 8.0
if _kind == "enemy":
var order_txt := String("①②③"[clampi(_order - 1, 0, 2)])
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT)
bar_x += 20.0
var bar_w := box.x - 8.0 - bar_x
draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移)
var fill_w := bar_w * hp_value
if fill_w > 0.0:
draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减)
# 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中;
# 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
var hp_txt := "%d/%d" % [_hp_cur, int(_hp_max)]
var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11)
# 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2;
# 中央垫 HP_BAR_BACK 深素牌 = 14.3:1;牌宽=字宽+8〔白名单取 8〕,高=条带全高;§4.1 数字色牌原则)
draw_rect(Rect2(bar_x + (bar_w - hts.x) * 0.5 - 4.0, 2.0, hts.x + 8.0, 10.0), UiKit.HP_BAR_BACK, true)
var hbase := Vector2(bar_x + (bar_w - hts.x) * 0.5, 2.0 + 5.0 + 4.0)
f.draw_string(get_canvas_item(), hbase + Vector2(1, 1), hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.10, 0.08, 0.06, 0.85))
f.draw_string(get_canvas_item(), hbase, hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(1, 1, 1, 0.95))
# 立绘:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退
if _portrait_sprite == null or _portrait_sprite.texture == null or not _portrait_sprite.visible:
var prect := _portrait_rect()
var glyph := _portrait_glyph()
var center := prect.get_center()
# 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的
# pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移);
# 传绝对 center 会成双重偏移、把字形画到盒外。
draw_set_transform(center, deg_to_rad(_glyph_rot), Vector2.ONE)
var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42)
f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color())
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
# 意图徽标(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画
if _intent_visible:
var icenter := Vector2(box.x * 0.5, 86.0)
draw_set_transform(icenter, 0.0, Vector2(_intent_scale, _intent_scale))
var its := f.get_string_size(_intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15)
# 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2
# 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1;
# atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放)
draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true)
f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), _intent_glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, _intent_col) # 局部坐标(同上注记——双重偏移修复)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
# 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化)
if _kind == "enemy":
var armor_txt := String(ARMOR_ICONS.get(_armor, "◇"))
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
match _armor:
"ward":
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
"heavy":
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
var name_w := f.get_string_size(_display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
var total_w := armor_w + 3.0 + name_w
var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), _display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制