- 删除 design/dev/architecture/adr-0001~0007 共 7 份文件(内容留 git 历史可查) - architecture.md:删「ADR 审计」「ADR 立项清单」两节;清除全文 ADR-000X 注记、QQ 表/S 表 ADR 引用;未决表关闭已落定项(QQ-01/02/03),立项清单在途事项转登记 QQ-11~14;补 A1 生成姿态事实、autoload 四件清单、board_generator 性能姿态;文档状态立「事实源地位」条 - CLAUDE.md:恢复 RNG 单点硬性红线(去 ADR-0007 引用);新增「架构事实唯一源」红线;规范导航架构行标注唯一事实源 - doc-conventions:§5 增「架构事实唯一源」行;§11 活文档清单补架构文档 - registry:architecture.yaml 删 18 处 adr 字段、autoload_set 补 SaveManager/MetaProgression、board_rending 更新为 Node2D 已落地事实;entities.yaml 12 处引用清除 - game/ 代码注释约 60 处清 ADR-000X 引用(事实与日期保留);豁免标记 ADR-0007-exempt → rng-seed-exempt(main.gd/static_scan.sh/architecture.md/06/iter27 五处同步) - GDD/qa 实现报告/research 卡片/roadmap/tests README 引用全清;死链校验 1206 链接 0 死链、gdd_lint 0 不通过 - 代码中 adr/adr5 等为 adventure_runner 既有局部变量名(非 ADR 引用),未动
505 lines
25 KiB
GDScript
505 lines
25 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 实现 GDD:design/ux/battle.md(B 区全站桩形态——主角与敌人均为无框立绘+底座,
|
||
# 英雄卡/敌人卡卡片式整体退役禁再提)。
|
||
# 站桩本体 = Node2D 世界精灵(B 区战斗舞台归 Node2D)。行为面契约逐项保持:
|
||
# set_data/set_hero/_set_hp/set_idle/set_effects/set_effects_cleared_feedback/set_order/set_pending/
|
||
# _play_intent_motion/set_portrait_texture(tex,flip)/mark_dead/is_dead/flash_hit/_revive/
|
||
# _update_name_line/_refresh_portrait_glyph(调用点 hud_presenter/main/探针零改造目标——C3)。
|
||
# 表现基线(C9):立绘贴图(flip/NEAREST/整数 2× 档)+ 底座 + 血条
|
||
# (直角递减 tween)+ 顺位徽标 + 意图徽标(弹入+立绘前倾一次性动画)+ 名字行(纯名+护甲类型
|
||
# 图标——AC 不展示)+ 受击闪白 + 死亡灰化摇摆塌陷(禁瞬消)+ 复活 + 字形占位
|
||
# 回退(人/▂/▆/█/✕/敌)。代拍板默认:①状态图标行(slot_row)与悬停 tooltip 保留
|
||
# Control 载体挂壳层(tooltip 机制零返工)——本体的 slot_row 成员 = 壳层伴随件引用;
|
||
# ②状态行隐藏时序 = 随 mark_dead 同拍隐藏;③清空反馈微文案挂伴随件父层(壳层)。
|
||
# 文字全部经 draw_string + 世界字体(world.world_font 或 ThemeDB.fallback_font——C6 挂点链)。
|
||
# 坐标空间:本节点 position/box = SubViewport 世界内局部排布;壳层换算归
|
||
# BattleStageView.world_to_shell 单点(C4)。
|
||
# 无 class_name(类名缓存面纪律——BattleStageView 经 preload 使用)。
|
||
extends Node2D
|
||
|
||
const CHARGE_SKILL_IDS := ["heavy"]
|
||
# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 /
|
||
# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。(自旧 StandeeView 平移)
|
||
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
|
||
# 名行/底座纵向预算余量(UI 设计对齐批 N2 2026-10-06——旧名行基线 = 盒底−1px「声明值刚好=容器值」
|
||
# 同型高危写法返修;≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit)
|
||
const STANDEE_FOOT_MARGIN := 12.0
|
||
|
||
var box := Vector2(150, 176) # 站桩盒尺寸(旧 Control custom_minimum_size;实际高随壳层 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
|
||
# 主角三态切换(迭代 6 T3):state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理
|
||
# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。
|
||
var _hero_tex := {}
|
||
enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(迭代 27 T6——裸字符串 enum 化;外部 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: # 意图弹入动画参数(旧 tween scale 等价)
|
||
set(v):
|
||
_intent_scale = v
|
||
queue_redraw()
|
||
var _glyph_rot := 0.0: # 立绘前倾角(旧只转字形 Label——贴图态无此动画,等价平移)
|
||
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框架 验收「血量数字可读」——2026-09-26 用户点名补数)
|
||
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
|
||
|
||
|
||
# 血条存在性探针出口(T5 断言等价改写——旧 _hp_bar ProgressBar 退役,世界直角血条常在)
|
||
var has_world_hp_bar := true
|
||
|
||
|
||
# 意图徽标探针出口(T5——旧 _intent_badge.visible/text 改判)
|
||
func intent_probe() -> Dictionary:
|
||
return {"visible": _intent_visible, "text": _intent_glyph}
|
||
|
||
|
||
# 名字行探针出口(T5——旧 _name_label/_armor_icon.text 改判)
|
||
func name_line_probe() -> Dictionary:
|
||
return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))}
|
||
|
||
|
||
# ---- setter 面(投影契约与旧 StandeeView 一致——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 # 攻击预告红(危险红族——旗朱退役 2026-09-26,预告语义归封印色独占〔现行封印黑〕)
|
||
if ptype == "buff":
|
||
glyph = "✦" # 增益系
|
||
col = UiKit.HERO_BLUE_DEEP # 深档墨(N1-4——浅垫牌上 ≥4.5:1;原 HERO_BLUE 亮档对垫牌 2.5:1)
|
||
elif ptype == "debuff":
|
||
glyph = "☠" # 妨害系(新效应词汇批 2026-09-19)
|
||
col = UiKit.DEBUFF_PURPLE
|
||
if pid in CHARGE_SKILL_IDS:
|
||
col = UiKit.GOLD_INK # 重击类蓄力档(深金墨 N1——浅垫牌上 ≥4.5:1;原 GOLD_DEEP 2.5:1)
|
||
_intent_glyph = glyph
|
||
_intent_col = col
|
||
_intent_visible = true
|
||
_play_intent_motion()
|
||
|
||
|
||
# 意图三档动作动画终稿(样张 v2 冻结批 T3——§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 # 像素帧放大防糊(棋盘小人贴图批 2026-09-25)
|
||
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)(旧 FULL_RECT offsets 等价);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 同拍隐藏(批内代拍板②待追认)
|
||
|
||
|
||
# ---- 演出密度终稿(样张 v2 冻结批 T3——程序化 tween,零新帧资产) ----
|
||
|
||
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
|
||
const HIT_SPARK_N := 6 # 命中火花数(迭代 6 T4 终稿——打击瞬间可读性一拍)
|
||
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}
|
||
|
||
|
||
# 命中瞬间打击特效(迭代 6 T4 终稿):立绘盒中心六枚短火花放射(程序化色块,确定性方向
|
||
# 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 受击反馈;强度时长复核迭代 6 T4=维持现行)
|
||
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:
|
||
# 字形回退已退役为纯缺位兜底(迭代 6 T2——八敌真图落位;旧三档体量字形 ▂/▆/█ 随退役,
|
||
# 相对体量语义由 48px 档统一比例承载)。缺位态:主角「人」/死亡「✕」/其余「敌」。
|
||
if _kind == "hero":
|
||
return "人"
|
||
if _dead:
|
||
return "✕"
|
||
return "敌"
|
||
|
||
|
||
# 立绘落位探针出口(迭代 6 T2——站桩纹理非空断言面;缺位回退链保持的对照面)
|
||
func portrait_probe() -> Dictionary:
|
||
return {"has_tex": _portrait_sprite != null and _portrait_sprite.texture != null}
|
||
|
||
|
||
# ---- 主角三态切换(迭代 6 T3——攻击演出→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」断言读数面——
|
||
# 最低绘制元素 = 底座下缘;N2 纵向余量纪律 2026-10-06)
|
||
func content_rect() -> Rect2:
|
||
return Rect2(0.0, 0.0, box.x, _pedestal_rect().end.y)
|
||
|
||
|
||
func _draw() -> void:
|
||
var f := _efont()
|
||
# 椭圆落影(圣经 v3.5 批 B4 2026-09-30——§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 族
|
||
# (T4 裸色收口);几何沿用现行底座位〔margin 26/高 10→方向稿 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 ①②③——迭代 26 T3 换墨,旧 GOLD_DEEP 对舞台底 3.3:1 越带)——旧 top VBox 几何等价(左 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)
|
||
# 对比度垫牌(UI 设计对齐批 N1-1/N1-2 2026-10-06——白 α.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()
|
||
# 坐标系注记(2026-09-26 错位修复):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)
|
||
# 对比度垫牌(UI 设计对齐批 N1-3/N1-4 2026-10-06——四色墨直压立绘纹理/暗舞台底 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——2026-09-26 用户终裁: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 # 魔法护甲 = 小人蓝亮档(迭代 26 T3 换墨,旧 HERO_BLUE 对舞台底 3.2:1 越带)
|
||
"heavy":
|
||
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档(迭代 26 T3 换墨,旧 GOLD_DEEP 对舞台底 3.3:1 越带)
|
||
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) # N2 余量纪律(2026-10-06——旧值 box.y-1.0 离盒底 1px)
|
||
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 暗底双色墨制(原 INK_TITLE 深墨对暗底不可读)
|