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) 透明画布。