From 75780e31fa0d6b755d36adfa8e695d39532bce7a Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 08:26:46 +0800 Subject: [PATCH] =?UTF-8?q?iter45-=E5=8D=A1=E2=91=A0T2:=20=E7=AB=99?= =?UTF-8?q?=E6=A1=A9=E4=B8=A4=E7=8A=B6=E6=80=81=E4=BB=B6=E5=BB=BA=E4=BB=B6?= =?UTF-8?q?=E4=B8=8E=E5=AE=BF=E4=B8=BB=E6=94=B9=E9=80=A0=E2=80=94=E2=80=94?= =?UTF-8?q?=5FStandeeState=20=E7=9C=9F=E5=AE=9E25=E6=88=90=E5=91=98?= =?UTF-8?q?=E6=B2=BF=E8=BA=AB=E4=BB=BD=E5=91=BD=E5=80=BC/=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E6=BC=94=E5=87=BA=E7=BC=9D=E6=8B=86=20standee=5Fvital?= =?UTF-8?q?s(12=E6=88=90=E5=91=98,apply=5Fhp=3D=5Fset=5Fhp=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E6=90=AC=E7=A7=BB,hp=5Fvalue=20setter=E9=80=90?= =?UTF-8?q?=E5=AD=97)/standee=5Fmotion(11=E6=88=90=E5=91=98,=E5=85=AD?= =?UTF-8?q?=E8=BF=81=E5=85=A5=E5=87=BD=E6=95=B0:set=5Fportrait=5Ftexture(t?= =?UTF-8?q?ex,flip,box)/register=5Fhero=5Fstate/apply=5Fhero=5Fstate(is=5F?= =?UTF-8?q?hero=E5=8F=82=E5=88=A4,=E4=B8=89=E8=BF=9E=E6=97=A9=E9=80=80?= =?UTF-8?q?=E5=BA=8F=E9=80=90=E5=AD=97)/reset=5Fstate(RESET=E7=BB=95?= =?UTF-8?q?=E5=90=8C=E6=80=81=E6=97=A9=E9=80=80=E4=B8=A4=E6=8B=8D=E5=B0=81?= =?UTF-8?q?=E8=A3=85)/play=5Fintent=5Fmotion(charge=E5=8F=82=E5=88=A4)/sta?= =?UTF-8?q?te=5Fprobe),HeroState=E6=9E=9A=E4=B8=BE=E4=B8=8E=5FHS=5FNAME/?= =?UTF-8?q?=5FHS=5FVALUE=E9=9A=8F=E4=B8=89=E6=80=81=E6=9C=BA=E5=BD=92motio?= =?UTF-8?q?n(=E5=85=A8=E4=BB=93=E9=9B=B6=E5=A4=96=E9=83=A8=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E5=AE=9E=E6=B5=8B);=E5=AE=BF=E4=B8=BB10=E6=88=90?= =?UTF-8?q?=E5=91=98(=E4=BA=94=E5=86=BB=E7=BB=93var+st/mo+=E4=B8=89?= =?UTF-8?q?=E6=BC=94=E5=87=BA=E6=8E=A2=E9=92=88=E8=AE=A1=E6=95=B0=E5=99=A8?= =?UTF-8?q?=E4=B8=8A=E6=94=B6),=E8=BD=AC=E8=B0=83=E5=A3=B3=E4=B8=89?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=A4=96=E5=BD=A2=E9=9B=B6=E5=8F=98,fx=5Fpro?= =?UTF-8?q?be=E6=94=B9=E8=AF=BB=E5=AE=BF=E4=B8=BB=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E5=99=A8,tween=E8=B7=AF=E5=BE=84=E4=B8=B2st.hp=5Fvalue/mo.=5Fi?= =?UTF-8?q?ntent=5Fscale/mo.=5Fglyph=5Frot,=E6=BA=90=E7=A0=81=E9=94=9A?= =?UTF-8?q?=E4=B8=89=E5=87=BD=E6=95=B0(=5Fdraw=5Fhp=5Fbar/=5Fdraw=5Farmor?= =?UTF-8?q?=5Fname=5Fline/set=5Fpending)=E7=95=99=E5=AE=BF=E4=B8=BB?= =?UTF-8?q?=E4=B8=94=E9=94=9A=E8=A1=8C=E9=80=90=E5=AD=97(st=3Dvitals?= =?UTF-8?q?=E5=90=8D=E6=B2=BF=E7=94=A8=E6=95=85=E8=A1=80=E6=9D=A1/?= =?UTF-8?q?=E5=90=8D=E5=AD=97=E8=A1=8C=E9=9B=B6=E6=94=B9=E5=86=99)?= =?UTF-8?q?=E3=80=94=E7=8A=B6=E6=80=81=E4=BB=B6=20V3=E3=80=95;battle=5Fsta?= =?UTF-8?q?ge=5Fview=E4=B8=80=E8=A1=8C=E6=9C=AA=E6=94=B9=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?;=E5=AD=A4=E7=AB=8B=E9=AA=8C=E8=AF=81run=5Fall=E4=B8=A4?= =?UTF-8?q?=E5=A5=97=E4=BB=B6=E5=85=A8=E7=BB=BF+run=5Fsmoke?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/views/standee_body.gd | 207 +++++----------------- game/presentation/views/standee_motion.gd | 128 +++++++++++++ game/presentation/views/standee_vitals.gd | 36 ++++ 3 files changed, 210 insertions(+), 161 deletions(-) create mode 100644 game/presentation/views/standee_motion.gd create mode 100644 game/presentation/views/standee_vitals.gd diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index b82c32a7..a5a34489 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -2,9 +2,9 @@ ## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。 ## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。 ## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。 -## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出, -## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar) -## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。 +## 内部状态两件(u4 拆件——st=身份命值态 standee_vitals、mo=资产演出态 standee_motion,_init 自建); +## 外部直访冻结成员(box/font/slot_row/has_world_hp_bar)与源码锚承载成员(_intent_charge——theme +## 源锚句所在行逐字保留)与三个演出探针计数器住宿主。 extends Node2D const CHARGE_SKILL_IDS := ["heavy"] @@ -16,65 +16,28 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) +const _VITALS := preload("res://presentation/views/standee_vitals.gd") +const _MOTION := preload("res://presentation/views/standee_motion.gd") -## 站桩演出状态(全部内部态单点——含 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 st: _VITALS # 身份与命值态件实例(名字沿用——外部路径串 "st.hp_value" 与约 40 处引用零改) +var mo: _MOTION # 资产与演出态件实例(_init 自建) 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 +# 演出探针计数器(写入方=宿主演出函数、读取方=宿主 fx_probe 出口——仪器跟着驱动它的函数走) +var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数) +var _hit_sparks := 0 # 探针出口计数(命中火花累计) +var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面) func _init(kind: String = "enemy") -> void: - st._owner = self - st._hero_state = HeroState.IDLE + st = _VITALS.new(self) + mo = _MOTION.new(self) 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} + mo._hero_state = _MOTION.HeroState.IDLE func _efont() -> Font: @@ -92,7 +55,7 @@ func enemy_id() -> String: # 意图徽标探针出口 func intent_probe() -> Dictionary: - return {"visible": st._intent_visible, "text": st._intent_glyph} + return {"visible": mo._intent_visible, "text": mo._intent_glyph} # 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、 @@ -118,7 +81,7 @@ func set_data(d: Dictionary) -> void: 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))) + st.apply_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", [])) @@ -129,21 +92,11 @@ func set_data(d: Dictionary) -> void: func set_hero(hp: int, max_hp: int) -> void: st._hp_max = float(maxi(1, max_hp)) - _set_hp(hp) + st.apply_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: @@ -182,12 +135,12 @@ func _set_order(i: int) -> void: func set_pending(ptype: String, pid: String) -> void: if st._dead: _intent_charge = false - st._intent_visible = false + mo._intent_visible = false queue_redraw() return if ptype == "" or ptype == "basic": _intent_charge = false - st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) + mo._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) queue_redraw() return var glyph := "⚔" # 攻击系(damage) @@ -201,60 +154,15 @@ 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) - st._intent_glyph = glyph - st._intent_col = col - st._intent_visible = true - _play_intent_motion() + mo._intent_glyph = glyph + mo._intent_col = col + mo._intent_visible = true + mo.play_intent_motion(_intent_charge) -# 意图三档动作动画(§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 禁非整数缩放)——源换代时随源重算。 +# 资产挂点转调壳(签名外形零变——box 由宿主传入件内) 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) + mo.set_portrait_texture(tex, flip, box) func mark_dead() -> void: @@ -268,8 +176,8 @@ func mark_dead() -> void: 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) + mo._intent_visible = false # 死亡残位意图退位 + st.apply_hp(0) if slot_row != null: slot_row.visible = false # 状态行随 mark_dead 同拍隐藏 @@ -279,7 +187,7 @@ func mark_dead() -> void: # 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机) func _spawn_death_shards() -> void: - st._death_shards = DEATH_SHARD_N + _death_shards = DEATH_SHARD_N var center := _portrait_rect().get_center() for i in DEATH_SHARD_N: _spawn_death_shard(i, center) @@ -305,7 +213,7 @@ func _spawn_death_shard(i: int, center: Vector2) -> void: # 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面) func fx_probe() -> Dictionary: - return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count} + return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count} # 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向 @@ -319,7 +227,7 @@ func play_hit_impact() -> void: ## 单火花域(循环体提函数——放射弹道+渐隐+回收) func _spawn_hit_spark(i: int, center: Vector2) -> void: - st._hit_sparks += 1 + _hit_sparks += 1 var spark := ColorRect.new() spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM spark.size = Vector2(4, 3) @@ -340,7 +248,7 @@ func is_dead() -> bool: # 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行) func flash_hit() -> void: - st._flash_count += 1 + _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() @@ -353,8 +261,7 @@ func _revive() -> void: modulate = Color(1, 1, 1) rotation_degrees = 0.0 if st._kind == "hero": - st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) - set_hero_state("idle") + mo.reset_state() if slot_row != null: slot_row.visible = true queue_redraw() @@ -372,45 +279,24 @@ func _portrait_glyph() -> String: # 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面) func portrait_probe() -> Dictionary: - return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null} + return {"has_tex": mo._portrait_sprite != null and mo._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 + mo.register_hero_state(state, tex) -# 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。 -# 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。 +# 状态切换转调壳(签名外形零变——is_hero 由宿主 vitals 参判传入,早退序逐字在件内) 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) + mo.apply_hero_state(state, st._kind == "hero") -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——当前态名+注册态集合) +# 三态探针出口(fx_probe——当前态名+注册态集合;读件投影外形零变) func hero_state_probe() -> Dictionary: - return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()} + return mo.state_probe() func _portrait_color() -> Color: @@ -535,7 +421,7 @@ func _draw_hp_bar(f: Font) -> void: ## 立绘域:贴图在位 = 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: + if mo._portrait_sprite != null and mo._portrait_sprite.texture != null and mo._portrait_sprite.visible: return var prect := _portrait_rect() var glyph := _portrait_glyph() @@ -543,7 +429,7 @@ func _draw_portrait(f: Font) -> void: # 坐标系注记: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) + draw_set_transform(center, deg_to_rad(mo._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()) @@ -552,18 +438,17 @@ func _draw_portrait(f: Font) -> void: ## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画 func _draw_intent_badge(f: Font) -> void: - if not st._intent_visible: + if not mo._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) + draw_set_transform(icenter, 0.0, Vector2(mo._intent_scale, mo._intent_scale)) + var its := f.get_string_size(mo._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) + f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), mo._intent_glyph, + HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mo._intent_col) # 局部坐标(同上注记——双重偏移修复) ## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2 diff --git a/game/presentation/views/standee_motion.gd b/game/presentation/views/standee_motion.gd new file mode 100644 index 00000000..e162ac4e --- /dev/null +++ b/game/presentation/views/standee_motion.gd @@ -0,0 +1,128 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 站桩资产与演出态件(StandeeBody 拆件——单一变化原因=美术素材落位规则与演出编排设计): +## 站桩的资产挂点与一次性演出——立绘精灵落位与换装、主角三态注册与 0.35s 短驻回归、意图徽标弹入动画。 +## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(mo 实例); +## 两个属性 setter 经 _owner 回引触发宿主重绘语义逐字;tween 经 _owner 创建(同一 Node 生命绑定)。 +extends RefCounted + +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 _owner: Node2D = null +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 _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() + + +func _init(owner: Node2D) -> void: + _owner = owner + + +# 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 +# flip = 立绘朝向翻转(素材基准朝左时传 true 呈现朝右)。盒高 96px = 48px 立绘源整数 2× +# (圣经 §8.2 禁非整数缩放)——源换代时随源重算。(box = 宿主冻结 var,由宿主传入) +func set_portrait_texture(tex: Texture2D, flip: bool, box: Vector2) -> void: + if tex == null: + if _portrait_sprite != null: + _portrait_sprite.visible = false + _owner.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 # 像素帧放大防糊 + _owner.add_child(_portrait_sprite) + _portrait_sprite.texture = tex + _portrait_sprite.flip_h = flip + _portrait_sprite.visible = true + _place_portrait(box) + + +func _place_portrait(box: Vector2) -> 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) + + +# 组合点注册(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 兜底)。 +#(is_hero = 宿主 vitals._kind == "hero" 参判传入——三连早退条件序逐字) +func apply_hero_state(state: String, is_hero: bool) -> void: + var target: int = _HS_VALUE.get(state, -1) + if not is_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 = _owner.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"] + + +# 复位强制绕同态早退两拍封装(原 _revive 内 hero 分支——先置 RESET 哨兵再回 idle) +func reset_state() -> void: + _hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) + apply_hero_state("idle", true) + + +# 意图三档动作动画(§4.6.4 三档:①普通攻击=零表示〔set_pending 早退〕 +# ②特殊准备=徽标弹入+二次弹跳(高举语义,运动手段 §2)③重击/硬化=缓推蓄力+定格微顿+回落; +# 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。 +#(charge = 宿主 _intent_charge 分档布尔参判传入) +func play_intent_motion(charge: bool) -> void: + var tw := _owner.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 charge: + 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)) + + +# 三态探针出口(fx_probe——当前态名+注册态集合;宿主 hero_state_probe 读件投影,外形零变) +func state_probe() -> Dictionary: + return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()} diff --git a/game/presentation/views/standee_vitals.gd b/game/presentation/views/standee_vitals.gd new file mode 100644 index 00000000..593cd38d --- /dev/null +++ b/game/presentation/views/standee_vitals.gd @@ -0,0 +1,36 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 站桩身份与命值态件(StandeeBody 拆件——单一变化原因=战斗结算快照投影的字段形态): +## 站桩是谁、活没活、血量几何的唯一账面——身份五元组(种类/名/id/护甲/顺位)+存活位+血量账与两条血量系 tween 槽。 +## 无 class_name(类名缓存面纪律)——经宿主 standee_body preload 引用(st 实例名沿用); +## hp_value 属性 setter 经 _owner 回引触发宿主重绘语义逐字。 +extends RefCounted + +var _owner: Node2D = null +var _kind := "enemy" +var _display_name := "" +var _enemy_id := "" +var _armor := "light" +var _order := 1 +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 + + +func _init(owner: Node2D) -> void: + _owner = owner + + +# 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积 +func apply_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 = _owner.create_tween() + _hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15))