From cabb6b07bff206df37f03d0f2ce39dd4bd033b76 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 29 Sep 2026 04:09:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(t3):=20=E4=B8=BB=E8=A7=92=E4=B8=89?= =?UTF-8?q?=E6=80=81=E5=88=87=E6=8D=A2=E2=80=94=E2=80=94portrait=20?= =?UTF-8?q?=E6=8D=A2=E8=A3=85=20new-normal+attack/hit=20=E5=85=A5=E5=8C=85?= =?UTF-8?q?+=E6=8C=82=E4=BA=8B=E4=BB=B6=E5=88=87=E6=8D=A2+=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=9B=9E=20idle=20=E2=91=A048px=20=E6=A1=A3=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E9=BB=98=E8=AE=A4=E8=A1=A5=20attack/hit=20?= =?UTF-8?q?=E4=B8=A4=E6=A1=A3(hero-ref/edit=201x=20=E6=BA=90=E2=86=92chara?= =?UTF-8?q?cter=5Fhero=5Fattack/hit.png;new-hit.pxo=2009-27=20=E5=B7=B2?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=AF=BC=E5=87=BA=20hit=5F1x=20=E5=9C=A8?= =?UTF-8?q?=E4=BD=8D,U1=20=E6=89=8B=E5=87=BA=E9=A2=84=E6=A1=88=E4=B8=8D?= =?UTF-8?q?=E8=A7=A6=E5=8F=91);=E6=9C=BA=E6=A3=80=2011/11=20=E8=BF=87=20?= =?UTF-8?q?=E2=91=A1ui=5Fkit=20=E5=A2=9E=20TEX=5FHERO=5FATTACK/TEX=5FHERO?= =?UTF-8?q?=5FHIT;TEX=5FHERO=5FPORTRAIT=20=E6=B3=A8=E8=AE=B0=E6=94=B9=20no?= =?UTF-8?q?rmal=20=E6=80=81(48px=20=E6=A1=A3=E6=8D=A2=E8=A3=85)=20?= =?UTF-8?q?=E2=91=A2standee=5Fbody=20=E7=8A=B6=E6=80=81=E6=9C=BA:register?= =?UTF-8?q?=5Fhero=5Fstate(=E7=BB=84=E5=90=88=E7=82=B9=E6=B3=A8=E5=86=8C,?= =?UTF-8?q?=E7=BC=BA=E4=BD=8D=E4=B8=8D=E6=B3=A8=E5=86=8C=3D=E8=AF=A5?= =?UTF-8?q?=E6=80=81=E4=BF=9D=E6=8C=81=E7=8E=B0=E7=BA=B9=E7=90=86)/set=5Fh?= =?UTF-8?q?ero=5Fstate(=E7=9F=AD=E9=A9=BB=200.35s=20=E8=87=AA=E5=9B=9E=20i?= =?UTF-8?q?dle;=E6=9C=AA=E6=B3=A8=E5=86=8C=E6=80=81=E4=BF=9D=E6=8C=81)/her?= =?UTF-8?q?o=5Fstate=5Fprobe;=5Frevive=20=E5=A2=9E=E6=80=81=E5=9B=9E=20idl?= =?UTF-8?q?e;set=5Fhero=20=E5=A2=9E=E6=AD=BB=E4=BA=A1=E6=AE=8B=E4=BD=8D?= =?UTF-8?q?=E5=A4=8D=E6=B4=BB(hp>0=E2=80=94=E2=80=94=E6=95=8C=E4=BE=A7=20s?= =?UTF-8?q?et=5Fdata=20=E5=90=8C=E6=AC=BE)=20=E2=91=A3battle=5Fstage=5Fvie?= =?UTF-8?q?w:=E4=B8=89=E6=80=81=E6=B3=A8=E5=86=8C+=5Finit;on=5Fstep=5Frow?= =?UTF-8?q?=5Ffx=20=E4=B8=BB=E8=A7=92=E6=94=BB=E5=87=BB=E8=A1=8C=E2=86=92a?= =?UTF-8?q?ttack=20=E6=80=81/=E5=8F=97=E5=87=BB=E8=A1=8C=E2=86=92hit=20?= =?UTF-8?q?=E6=80=81(=E9=97=AA=E7=99=BD=E5=BC=B9=E9=81=93=E5=BD=92=20T5=20?= =?UTF-8?q?=E6=8B=86=E6=97=A9=E9=80=80);settle=5Fhero=5Fidle=20=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E6=94=B6=E5=B0=BE=E6=8C=82=20main=20=E4=B8=A4?= =?UTF-8?q?=E4=BE=A7=20battle=5Ffinished/adventure=5Ffinished=20=E2=91=A4f?= =?UTF-8?q?x=5Fprobe=20=E6=89=A9=E7=AC=AC=E4=BA=94=E6=AE=B5:=E4=B8=89?= =?UTF-8?q?=E6=80=81=E5=88=87=E6=8D=A2+=E7=9F=AD=E9=A9=BB=E8=87=AA?= =?UTF-8?q?=E5=9B=9E+=E6=AD=BB=E4=BA=A1=E5=A4=8D=E6=B4=BB=E6=96=AD?= =?UTF-8?q?=E8=A8=80=20=E8=BD=BB=E9=97=B8=3D=E5=86=92=E7=83=9F=20576/0+?= =?UTF-8?q?=E4=B8=89=E6=8E=A2=E9=92=88=20PASS(=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E5=88=B7=E7=BC=93=E5=AD=98=E5=90=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/main.gd | 2 + game/presentation/views/battle_stage_view.gd | 17 ++++++- game/presentation/views/standee_body.gd | 47 ++++++++++++++++++++ game/presentation/views/ui_kit.gd | 4 +- game/smoke/fx_probe.gd | 31 ++++++++++++- tools/pixelize_candidates.py | 4 +- 6 files changed, 99 insertions(+), 6 deletions(-) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 3db580ee..4c0b7df3 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -314,6 +314,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: _sync_settlement_visuals() # → 全关 stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体 2026-09-26 迁 B 区站桩) stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) + stage.settle_hero_idle() # 主角态回 idle(迭代 6 T3 三态收尾) # 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary = {} @@ -443,6 +444,7 @@ func _on_battle_finished(payload: Dictionary) -> void: var result: String = String(payload.get("result", "")) _sync_settlement_visuals() # → 全关(battle_over 反馈面) stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) + stage.settle_hero_idle() # 主角态回 idle(迭代 6 T3 三态收尾) ap_bar.set_rap(0) # RAP 清零脉冲 + 微文案(载体 2026-09-26 迁 D 区 AP 条) stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体迁 B 区站桩) var decision: Dictionary = _debug.on_battle_finished(payload) diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index c2111474..e792d188 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -113,7 +113,12 @@ func _init(db_ref) -> void: # 主角站桩本体(世界)+ 状态行伴随件(壳层) hero_standee = _STANDEE_BODY.new("hero") - hero_standee.set_portrait_texture(UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT), true) # B 区立绘 48×48 朝右(2026-09-26 用户点名 hero_idle_01 换装+水平向右;素材缺位 null 回退字形「人」) + var hero_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) + hero_standee.set_portrait_texture(hero_tex, true) # B 区立绘 48px 档·normal 态朝右(迭代 6 T3 换装=new-normal 源;素材缺位 null 回退字形「人」) + # 主角三态注册(迭代 6 T3——attack/hit 缺位不注册=该态保持现纹理,缺位回退链延伸) + hero_standee.register_hero_state("idle", hero_tex) + hero_standee.register_hero_state("attack", UiKit.load_tex(UiKit.TEX_HERO_ATTACK)) + hero_standee.register_hero_state("hit", UiKit.load_tex(UiKit.TEX_HERO_HIT)) world.add_child(hero_standee) hero_standee.slot_row = _make_slot_companion(_companion_layer) @@ -272,14 +277,17 @@ func set_round_text(text: String) -> void: round_label.text = text -# 结算行演出分发(占位级):伤害行 → 目标站桩闪白 / 击杀 → 灰化塌陷残位 +# 结算行演出分发(占位级):伤害行 → 目标站桩闪白 / 击杀 → 灰化塌陷残位; +# 主角三态切换(迭代 6 T3)——主角攻击行→attack 态(短驻自回)/ 主角受击行→hit 态 func on_step_row_fx(row: Dictionary) -> void: var t := String(row.get("type", "")) if t != "damage": return var target := String(row.get("target", "")) if target == "hero" or target == "": + hero_standee.set_hero_state("hit") # 受击态(短驻自回 idle;闪白/弹道归迭代 6 T5 拆早退) return + hero_standee.set_hero_state("attack") # 我方攻击行——attack 态(短驻自回) var card: Node2D = _card_by_name(target) if card == null: return @@ -316,6 +324,11 @@ func gray_all_enemies() -> void: c.mark_dead() +# 结算收尾:主角态显式回 idle(迭代 6 T3——「结算回 idle」事件;0.35s 短驻自回的兜底) +func settle_hero_idle() -> void: + hero_standee.set_hero_state("idle") + + # 结算期 B 区第一注视点切换(能量 7/10 清算调性):结算中占位文案退位——兑现演出由 # SettlementFxView 弹道/光带本体接管(演出链批);挖掘期恢复教学占位文案 func set_settle_focus(on: bool) -> void: diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 7412cdb0..2ea2d6c0 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -31,6 +31,11 @@ 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 := {} +var _hero_state := "idle" +var _state_revert_tw: Tween var _order := 1 var _intent_glyph := "" var _intent_col := Color.WHITE @@ -105,6 +110,8 @@ func set_data(d: Dictionary) -> void: 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 不累积 @@ -300,6 +307,9 @@ func _revive() -> void: _dead = false modulate = Color(1, 1, 1) rotation_degrees = 0.0 + if _kind == "hero": + _hero_state = "reset" # 强制回 idle 态(绕过同态早退) + set_hero_state("idle") _refresh_portrait_glyph() if slot_row != null: slot_row.visible = true @@ -325,6 +335,43 @@ 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: + if _kind != "hero" or state == _hero_state: + return + if not _hero_tex.has(state) and state != "idle": + return + _hero_state = state + 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 = "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": _hero_state, "registered": _hero_tex.keys()} + + func _portrait_color() -> Color: return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index e02da719..8d070f1a 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -90,7 +90,9 @@ static func motion(dur: float) -> float: # 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线) const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png" const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png" -const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘 48×48(源档 candidates/hero-idle/hero_idle_01.png,2026-09-26 用户点名换装) +const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(48px 档,迭代 6 T3 换装=new-normal 源) +const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(迭代 6 T3 三态切换) +const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(迭代 6 T3 三态切换) # 敌种立绘(B 区站桩 2026-09-26 槽接线批)——文件键 = 数据表主键 enemy_id(圣经 §8.3 命名规范), # 键集 = art-bible §4.6.4 八敌种;素材批入库前全部缺位(load_tex→null→字形回退),入库即显示零代码换装 const TEX_ENEMY_GOBLIN := "res://assets/art/character/character_enemy_goblin.png" # 地精 diff --git a/game/smoke/fx_probe.gd b/game/smoke/fx_probe.gd index fa262006..65e3428c 100644 --- a/game/smoke/fx_probe.gd +++ b/game/smoke/fx_probe.gd @@ -67,9 +67,38 @@ func _ready() -> void: if n_fx < 2: problems.append("结算光带应含带体+前导缘线 ≥2 节点,实得 %d" % n_fx) + # ---- ⑤ 主角三态切换(迭代 6 T3——attack/hit 短驻自回 + 死亡复活态回 idle) ---- + var hero_sb = sb_script.new("hero") + hero_sb.box = Vector2(150, 176) + add_child(hero_sb) + var t_idle: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) + var t_atk: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_ATTACK) + var t_hit: Texture2D = UiKit.load_tex(UiKit.TEX_HERO_HIT) + if t_idle == null or t_atk == null or t_hit == null: + problems.append("主角三态素材缺位(portrait/attack/hit——迭代 6 T3)") + else: + hero_sb.set_portrait_texture(t_idle, true) + hero_sb.register_hero_state("idle", t_idle) + hero_sb.register_hero_state("attack", t_atk) + hero_sb.register_hero_state("hit", t_hit) + hero_sb.set_hero_state("attack") + if String(hero_sb.hero_state_probe().get("state", "")) != "attack": + problems.append("主角攻击态切换失败(set_hero_state attack)") + await get_tree().create_timer(UiKit.motion(0.5)).timeout + if String(hero_sb.hero_state_probe().get("state", "")) != "idle": + problems.append("attack 态未短驻自回 idle(0.35s)") + hero_sb.set_hero_state("hit") + if String(hero_sb.hero_state_probe().get("state", "")) != "hit": + problems.append("主角受击态切换失败(set_hero_state hit)") + hero_sb.set_hero_state("idle") + hero_sb.mark_dead() + hero_sb.set_hero(6, 10) # 新战斗开场 HP 回正——死亡残位复活(敌侧 set_data 同款语义) + if hero_sb.is_dead() or String(hero_sb.hero_state_probe().get("state", "")) != "idle": + problems.append("主角死亡后 set_hero 复活失败(is_dead 应假+态回 idle)") + # ---- 汇总 ---- if problems.is_empty(): - print("[fx_probe] PASS — 死亡碎裂5块/意图三档+零表示/胜利纹章10/失败掸灰8+旗光8/结算光带+缘线 全链路通过") + print("[fx_probe] PASS — 死亡碎裂5块/意图三档+零表示/胜利纹章10/失败掸灰8+旗光8/结算光带+缘线/主角三态切换 全链路通过") get_tree().quit(0) else: for p in problems: diff --git a/tools/pixelize_candidates.py b/tools/pixelize_candidates.py index 8ddc9a95..dba8975a 100644 --- a/tools/pixelize_candidates.py +++ b/tools/pixelize_candidates.py @@ -439,8 +439,8 @@ def dom_block_resize(im, nw, nh): return out -def build_standee48(batch_root, out_dir, extra_hero_states=()): - """九张 48px 站桩槽位档:源=八敌 pixel-1+主角 normal_1x(extra_hero_states 追加 attack/hit)。 +def build_standee48(batch_root, out_dir, extra_hero_states=("attack", "hit")): + """九张 48px 站桩槽位档:源=八敌 pixel-1+主角 normal_1x(extra_hero_states 追加 attack/hit,T3 起默认)。 口径(代拍板待追认): 敌八种统一比例(最大体量归 48 高,相对体量语义=字形回退 ▂▆█ 同构); 主角自适比例(48/56);NEAREST 零插值;内容 bbox 裁边后底中锚贴 (w,48) 透明画布。