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

592 lines
27 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()。
## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出,
## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar)
## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。
extends Node2D
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型图标三键(light/ward/heavy)——终稿 = 三枚 24×24 PNG(UiKit.TEX_ARMOR_*,art-bible §5.3
# 工单:形状承载身份、色相不承载身份;挂载底色 = 名字行素底 HP_BAR_BACK)。字形占位仅作素材缺位
# 回退(与立绘缺位回退链同构——素材不入 git,缺位不炸)。
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
## 站桩演出状态(全部内部态单点——含 setter 触发宿主重绘语义,_owner 回引保持逐字)
class _StandeeState extends RefCounted:
var _owner: Node2D = null
var _kind := "enemy"
var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位)
var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据)
# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
var _hero_tex := {}
var _hero_state: int = 0
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
_owner.queue_redraw()
var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画)
set(v):
_glyph_rot = v
_owner.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)
_owner.queue_redraw()
var _hp_tween: Tween
var _hp_flash_tw: Tween
var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数)
var _hit_sparks := 0 # 探针出口计数(命中火花累计)
var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面)
var st := _StandeeState.new()
var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入;外部直访冻结)
var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入;外部直访冻结)
var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界;外部直访冻结)
var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据;源码锚承载成员住宿主)
# 血条存在性探针出口(世界直角血条常在;外部直访冻结)
var has_world_hp_bar := true
func _init(kind: String = "enemy") -> void:
st._owner = self
st._hero_state = HeroState.IDLE
st._kind = kind
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}
func _efont() -> Font:
return font if font != null else ThemeDB.fallback_font
func display_name() -> String:
return st._display_name
# 敌单位身份出口(d12 站桩解析主键——同名敌按 id 精确匹配;主角站桩恒空串)
func enemy_id() -> String:
return st._enemy_id
# 意图徽标探针出口
func intent_probe() -> Dictionary:
return {"visible": st._intent_visible, "text": st._intent_glyph}
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y)
func name_line_probe() -> Dictionary:
var f := _efont()
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
var plate := _name_plate_rect(f, tex)
return {"name": st._display_name,
"armor_icon": _armor_tex_path(st._armor) if tex != null else String(ARMOR_ICONS.get(st._armor, "◇")),
"armor_tex_bound": tex != null,
"plate": plate, "baseline_y": _name_baseline(plate).y,
"plinth_top_y": _pedestal_rect().position.y,
"plate_color": UiKit.HP_BAR_BACK}
# ---- 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:
st._display_name = String(d.get("display_name", "敌人"))
st._enemy_id = String(d.get("enemy_id", ""))
st._armor = String(d.get("armor_type", "light"))
st._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", []))
queue_redraw()
if st._dead and int(d.get("hp", 0)) > 0:
_revive()
func set_hero(hp: int, max_hp: int) -> void:
st._hp_max = float(maxi(1, max_hp))
_set_hp(hp)
if st._dead and hp > 0:
_revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义)
# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积
func _set_hp(hp: int) -> void:
st._hp_cur = clampi(hp, 0, int(st._hp_max))
var target := float(st._hp_cur) / st._hp_max
if st._hp_tween != null and st._hp_tween.is_valid():
st._hp_tween.kill()
st._hp_tween = create_tween()
st._hp_tween.tween_property(st, "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:
# 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务)
st._order = i
queue_redraw()
# 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff;
# "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。
func set_pending(ptype: String, pid: String) -> void:
if st._dead:
_intent_charge = false
st._intent_visible = false
queue_redraw()
return
if ptype == "" or ptype == "basic":
_intent_charge = false
st._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
_intent_charge = pid in CHARGE_SKILL_IDS
if _intent_charge:
col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1)
st._intent_glyph = glyph
st._intent_col = col
st._intent_visible = true
_play_intent_motion()
# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕
# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落;
# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。
func _play_intent_motion() -> void:
var tw := create_tween()
tw.tween_property(st, "_intent_scale", 1.6, UiKit.motion(0.10))
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.16))
tw.parallel().tween_property(st, "_glyph_rot", 4.0, UiKit.motion(0.10))
tw.tween_property(st, "_glyph_rot", 0.0, UiKit.motion(0.14))
if _intent_charge:
tw.tween_property(st, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推
tw.tween_interval(UiKit.motion(0.40)) # 定格微顿
tw.tween_property(st, "_intent_scale", 1.0, UiKit.motion(0.18))
else:
tw.tween_property(st, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举)
tw.tween_property(st, "_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:
if tex == null:
if st._portrait_sprite != null:
st._portrait_sprite.visible = false
queue_redraw()
return
st._portrait_tex = tex
if st._portrait_sprite == null:
st._portrait_sprite = Sprite2D.new()
st._portrait_sprite.centered = false
st._portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊
add_child(st._portrait_sprite)
st._portrait_sprite.texture = tex
st._portrait_sprite.flip_h = flip
st._portrait_sprite.visible = true
_place_portrait()
func _place_portrait() -> void:
# 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中
if st._portrait_sprite == null or st._portrait_tex == null:
return
var box_h := box.y - 44.0 - 36.0
var w := st._portrait_tex.get_width() * 2.0
var h := st._portrait_tex.get_height() * 2.0
st._portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5)
st._portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5)
func mark_dead() -> void:
if st._dead:
return
st._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)
_intent_charge = false
st._intent_visible = false # 死亡残位意图退位
_set_hp(0)
if slot_row != null:
slot_row.visible = false # 状态行随 mark_dead 同拍隐藏
# ---- 演出密度(程序化 tween,零新帧资产) ----
# 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机)
func _spawn_death_shards() -> void:
st._death_shards = DEATH_SHARD_N
var center := _portrait_rect().get_center()
for i in DEATH_SHARD_N:
_spawn_death_shard(i, center)
## 单碎块域(循环体提函数——弹道两段+渐隐+回收)
func _spawn_death_shard(i: int, center: Vector2) -> void:
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": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._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:
_spawn_hit_spark(i, center)
## 单火花域(循环体提函数——放射弹道+渐隐+回收)
func _spawn_hit_spark(i: int, center: Vector2) -> void:
st._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 st._dead
# 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行)
func flash_hit() -> void:
st._flash_count += 1
if st._hp_flash_tw != null and st._hp_flash_tw.is_valid():
st._hp_flash_tw.kill()
st._hp_flash_tw = create_tween()
st._hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08))
st._hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not st._dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2))
func _revive() -> void:
st._dead = false
modulate = Color(1, 1, 1)
rotation_degrees = 0.0
if st._kind == "hero":
st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退)
set_hero_state("idle")
if slot_row != null:
slot_row.visible = true
queue_redraw()
func _portrait_glyph() -> String:
# 字形回退仅作缺位兜底(八敌真图落位;相对体量语义由 48px 档统一比例承载)。
# 缺位态:主角「人」/死亡「✕」/其余「敌」。
if st._kind == "hero":
return "人"
if st._dead:
return "✕"
return "敌"
# 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面)
func portrait_probe() -> Dictionary:
return {"has_tex": st._portrait_sprite != null and st._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:
st._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 st._kind != "hero" or target == st._hero_state:
return
if not st._hero_tex.has(state) and state != "idle":
return
st._hero_state = target
if st._portrait_sprite != null and st._hero_tex.has(state):
st._portrait_sprite.texture = st._hero_tex[state]
if st._state_revert_tw != null and st._state_revert_tw.is_valid():
st._state_revert_tw.kill()
if state != "idle":
st._state_revert_tw = create_tween()
st._state_revert_tw.tween_interval(UiKit.motion(0.35))
st._state_revert_tw.tween_callback(_settle_hero_idle)
func _settle_hero_idle() -> void:
st._hero_state = HeroState.IDLE
if st._portrait_sprite != null and st._hero_tex.has("idle"):
st._portrait_sprite.texture = st._hero_tex["idle"]
# 三态探针出口(fx_probe——当前态名+注册态集合)
func hero_state_probe() -> Dictionary:
return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()}
func _portrait_color() -> Color:
return UiKit.HERO_BLUE if st._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)
# 护甲图标纹理路径(甲档键 → UiKit.TEX_ARMOR_*——显式 match 单点,禁动态拼键取常量;
# 未知键回退轻甲路径,由 load_tex exists 守卫接住)
func _armor_tex_path(armor: String) -> String:
match armor:
"ward":
return UiKit.TEX_ARMOR_WARD
"heavy":
return UiKit.TEX_ARMOR_HEAVY
_:
return UiKit.TEX_ARMOR_LIGHT
# 名字行素底牌矩形(_draw 与 name_line_probe 共用几何——单一来源防两处漂移):
# 牌高 26 = 护甲图标 24px(甲类 1× 禁缩放)+ 上下各 1px;牌底沿 = 地台顶(box.y-18)——
# 文字禁直接压地台(§6.1/§7.2 素底落法);牌宽 = 行宽(图标+间距+名字)+ 8,水平居中
func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2:
var icon_w := 24.0
if tex == null:
icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")),
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
var total_w := icon_w + 3.0 + name_w
return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0)
# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,12px 字形基线=中心+4)
func _name_baseline(plate: Rect2) -> Vector2:
return plate.position + Vector2(4.0, 17.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()
_draw_ground(f)
_draw_hp_bar(f)
_draw_portrait(f)
_draw_intent_badge(f)
if st._kind == "enemy":
_draw_armor_name_line(f)
## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
## + 石板地台(§6.1 近景地台独立件:TEX_STAGE_PLAZA 源 48×12 九宫格横向拉伸、顶缘受光线 2px
## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔STAND_STONE 族方向稿取色,几何同源
## _pedestal_rect——与 panel_skinned 缺位回退同构〕)
func _draw_ground(f: Font) -> void:
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))
var ped := _pedestal_rect()
var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA)
if plaza != null:
_draw_plinth_stretched(plaza, ped)
else:
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)
## 地台九宫格横向拉伸域(循环体提函数——左右 4px 端头切片 + 中段拉伸;纵向恒 12px 源档,
## 顶缘受光线 2px 落纵向不缩放区,工单 §5.2 工艺要点)
func _draw_plinth_stretched(tex: Texture2D, ped: Rect2) -> void:
draw_texture_rect_region(tex, Rect2(ped.position, Vector2(4.0, 12.0)), Rect2(0, 0, 4, 12))
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(4.0, 0.0),
Vector2(ped.size.x - 8.0, 12.0)), Rect2(4, 0, 40, 12))
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(ped.size.x - 4.0, 0.0),
Vector2(4.0, 12.0)), Rect2(44, 0, 4, 12))
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③,字号 FS_HINT——art-bible §7.4 清单外
## 11–13px 提档归档);血条几何左 8 / 右 -8;血条填充 = 危险红 HP_BAR_FILL(§4.2 语义物件——
## 不因敌我分色);条底 = HP_BAR_TRACK;血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于
## 10px 条带垂直居中;数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
func _draw_hp_bar(f: Font) -> void:
var bar_x := 8.0
if st._kind == "enemy":
var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)])
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, 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), UiKit.HP_BAR_TRACK) # 条底(值单点收 ui_kit)
var fill_w := bar_w * st.hp_value
if fill_w > 0.0:
draw_rect(Rect2(bar_x, 2, fill_w, 10), UiKit.HP_BAR_FILL) # 直角填充(危险红——§4.2 血条显式句)
var hp_txt := "%d/%d" % [st._hp_cur, int(st._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 落位);缺位 = 字形回退
func _draw_portrait(f: Font) -> void:
if st._portrait_sprite != null and st._portrait_sprite.texture != null and st._portrait_sprite.visible:
return
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(st._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 动画
func _draw_intent_badge(f: Font) -> void:
if not st._intent_visible:
return
var icenter := Vector2(box.x * 0.5, 86.0)
draw_set_transform(icenter, 0.0, Vector2(st._intent_scale, st._intent_scale))
var its := f.get_string_size(st._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), st._intent_glyph,
HORIZONTAL_ALIGNMENT_LEFT, -1, 15, st._intent_col) # 局部坐标(同上注记——双重偏移修复)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2
## 「文字禁直接压地台」素底落法——牌底沿 = 地台顶,几何单点 _name_plate_rect);护甲图标 =
## 24×24 PNG(甲类 1×,缺位回退字形,色随甲档亮档族);名字 = 暖白(暗底双色墨制)
func _draw_armor_name_line(f: Font) -> void:
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
var plate := _name_plate_rect(f, tex)
draw_rect(plate, UiKit.HP_BAR_BACK, true)
var baseline := _name_baseline(plate)
var icon_w := 24.0
if tex != null:
draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false)
else:
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
match st._armor:
"ward":
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
"heavy":
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, col)
f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, UiKit.WARM) # v3.5 暗底双色墨制