diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index d946c735..ed2b1f7e 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -2,6 +2,9 @@ ## 站桩本体: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"] @@ -12,50 +15,68 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} # ≥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 壳层,本体进世界) +const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) +const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) -var _kind := "enemy" -var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) -var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) -# 主角三态切换: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_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据) -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 + +## 站桩演出状态(全部内部态单点——含 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: - _kind = kind + 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: @@ -63,26 +84,22 @@ func _efont() -> Font: func display_name() -> String: - return _display_name + return st._display_name # 敌单位身份出口(d12 站桩解析主键——同名敌按 id 精确匹配;主角站桩恒空串) func enemy_id() -> String: - return _enemy_id - - -# 血条存在性探针出口(世界直角血条常在) -var has_world_hp_bar := true + return st._enemy_id # 意图徽标探针出口 func intent_probe() -> Dictionary: - return {"visible": _intent_visible, "text": _intent_glyph} + return {"visible": st._intent_visible, "text": st._intent_glyph} # 名字行探针出口 func name_line_probe() -> Dictionary: - return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))} + return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))} # ---- setter 面(presenter/组合根零改排) ---- @@ -90,35 +107,34 @@ func name_line_probe() -> Dictionary: # 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)))) + 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", [])) - _refresh_portrait_glyph() - if _dead and int(d.get("hp", 0)) > 0: - _revive() queue_redraw() + if st._dead and int(d.get("hp", 0)) > 0: + _revive() func set_hero(hp: int, max_hp: int) -> void: - _hp_max = float(maxi(1, max_hp)) + st._hp_max = float(maxi(1, max_hp)) _set_hp(hp) - if _dead and hp > 0: + if st._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)) + 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(self, "st.hp_value", target, UiKit.motion(0.15)) # 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位) @@ -150,21 +166,21 @@ func set_effects_cleared_feedback() -> void: func _set_order(i: int) -> void: # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) - _order = i + st._order = i queue_redraw() # 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff; # "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。 func set_pending(ptype: String, pid: String) -> void: - if _dead: + if st._dead: _intent_charge = false - _intent_visible = false + st._intent_visible = false queue_redraw() return if ptype == "" or ptype == "basic": _intent_charge = false - _intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) + st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) queue_redraw() return var glyph := "⚔" # 攻击系(damage) @@ -178,9 +194,9 @@ func set_pending(ptype: String, pid: String) -> void: _intent_charge = pid in CHARGE_SKILL_IDS if _intent_charge: col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1) - _intent_glyph = glyph - _intent_col = col - _intent_visible = true + st._intent_glyph = glyph + st._intent_col = col + st._intent_visible = true _play_intent_motion() @@ -189,17 +205,17 @@ func set_pending(ptype: String, pid: String) -> void: # 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。 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)) + tw.tween_property(self, "st._intent_scale", 1.6, UiKit.motion(0.10)) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.16)) + tw.parallel().tween_property(self, "st._glyph_rot", 4.0, UiKit.motion(0.10)) + tw.tween_property(self, "st._glyph_rot", 0.0, UiKit.motion(0.14)) if _intent_charge: - tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推 + tw.tween_property(self, "st._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)) + tw.tween_property(self, "st._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)) + tw.tween_property(self, "st._intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.13)) # 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 @@ -207,45 +223,45 @@ func _play_intent_motion() -> void: # (圣经 §8.2 禁非整数缩放)——源换代时随源重算。 func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: if tex == null: - if _portrait_sprite != null: - _portrait_sprite.visible = false + if st._portrait_sprite != null: + st._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 + 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 _portrait_sprite == null or _portrait_tex == null: + if st._portrait_sprite == null or st._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) + 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 _dead: + if st._dead: return - _dead = true + 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) - _refresh_portrait_glyph() - _intent_visible = false # 死亡残位意图退位 + _intent_charge = false + st._intent_visible = false # 死亡残位意图退位 _set_hp(0) if slot_row != null: slot_row.visible = false # 状态行随 mark_dead 同拍隐藏 @@ -253,37 +269,36 @@ func mark_dead() -> void: # ---- 演出密度(程序化 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 + st._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) + _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": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count} + return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count} # 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向 @@ -292,65 +307,65 @@ func fx_probe() -> Dictionary: 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) + _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 _dead + return st._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)) + 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: - _dead = false + st._dead = false modulate = Color(1, 1, 1) rotation_degrees = 0.0 - if _kind == "hero": - _hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) + if st._kind == "hero": + st._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": + if st._kind == "hero": return "人" - if _dead: + if st._dead: return "✕" return "敌" # 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面) func portrait_probe() -> Dictionary: - return {"has_tex": _portrait_sprite != null and _portrait_sprite.texture != null} + return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null} # ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ---- @@ -358,41 +373,41 @@ func portrait_probe() -> Dictionary: # 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理) func register_hero_state(state: String, tex: Texture2D) -> void: if tex != null: - _hero_tex[state] = tex + 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 _kind != "hero" or target == _hero_state: + if st._kind != "hero" or target == st._hero_state: return - if not _hero_tex.has(state) and state != "idle": + if not st._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() + 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": - _state_revert_tw = create_tween() - _state_revert_tw.tween_interval(UiKit.motion(0.35)) - _state_revert_tw.tween_callback(_settle_hero_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: - _hero_state = HeroState.IDLE - if _portrait_sprite != null and _hero_tex.has("idle"): - _portrait_sprite.texture = _hero_tex["idle"] + 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(_hero_state, "idle"), "registered": _hero_tex.keys()} + return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()} func _portrait_color() -> Color: - return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP + return UiKit.HERO_BLUE if st._kind == "hero" else UiKit.RED_DEEP # 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用 @@ -413,8 +428,19 @@ func content_rect() -> Rect2: func _draw() -> void: var f := _efont() - # 椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台 - # 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半) + _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、横半径=台宽一半) +## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 +## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕) +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 @@ -423,28 +449,28 @@ func _draw() -> void: 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 + +## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8; +## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中; +## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) +func _draw_hp_bar(f: Font) -> void: var bar_x := 8.0 - if _kind == "enemy": - var order_txt := String("①②③"[clampi(_order - 1, 0, 2)]) + 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, 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 + var fill_w := bar_w * st.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 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 数字色牌原则) @@ -453,46 +479,53 @@ func _draw() -> void: 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) +## 立绘域:贴图在位 = 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——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 暗底双色墨制 + +## 意图徽标域(敌 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 数值不展示,类型图标化) +func _draw_armor_name_line(f: Font) -> void: + var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇")) + var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) + match st._armor: + "ward": + col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 + "heavy": + col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 + var name_w := f.get_string_size(st._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), st._display_name, + HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制