This commit is contained in:
2026-09-30 16:14:09 +08:00
parent c978257ae8
commit 01c1698446
13 changed files with 2923 additions and 66 deletions
@@ -1,14 +1,34 @@
using Assets.MyGame.Scripts.Net.WebResponses;
using Newtonsoft.Json.Linq;
using Spine.Unity;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 烈焰风暴 Boss(精简版:只根据服务端 param 判定死亡/未死亡)。
/// 烈焰风暴 Boss(对齐 lua:pw_enemy/PW_FlameStrike.txt + pw_bullet/PW_BulletFlameStrike.txt)。
///
/// 本体结构(Art/XJMH/pw/pw_prefab/game/enemy/XJMHEnemy/PW_Enemy_115.prefab):
/// root(PW_Enemy_115, isSpine=1) → Body → Bone → { Stand(待机特效) / Die(死亡特效) / Explosion(爆炸特效) }
/// Wheel(轮盘,4 个子节点 = 火球弹发射点)
/// Spine 动画名:stand01(待机) / stand02(受击死亡) —— 与 lua SetAnimation(1)/(2) 一致。
///
/// 表现流程(lua):
/// OnCreate → ActiveCollider(true) + SetAnimation(1)(stand01 + 待机特效)
/// 被击中(未死亡) → isSelf 播 PW_FlameStrike_Explosion;ShowEffect(Explosion) → 0.3s 播爆奖
/// → 0.8s 关掉全部特效(回到待机特效)→ 恢复游动
/// 被打死 → SetAnimation(2)(stand02 + 死亡特效);isSelf 播 Explosion/Die 两个音效
/// · Free 子弹:ShowEffect(Explosion) → 0.3s 爆奖 → 0.8s 销毁本体
/// · 普通子弹:等 1.8s(Timer)→ 从 Wheel 的 4 个子节点朝外生成火球弹
/// (BulletType.FlameRound)→ 销毁本体
/// 黑洞吸收 → 原版显式免疫(本项目黑洞只吸“普通鱼”,Boss 天然不受影响,无需处理)
///
/// param: { "isKill": bool, "beishu": int, "fishId": "...", "animId": int }
/// animId==1 / isKill=false → 未死亡:炸一下 + bjEndUI,Boss 不消失,爆奖结束恢复游动
/// animId>=2 / isKill=true → 死亡:炸一下 + bjEndUI,Boss 消失
///
/// 无死鱼、无红包、无中间过程。
/// animId==1 / isKill=false → 未死亡(Boss 保留,爆奖结束恢复游动)
/// animId>=2 / isKill=true → 死亡(Boss 消失)
/// (可选)param.flameInfo = { originUID, ratio, sourceUIDList:[...] }:火球弹来源列表
/// param.bulletType = 1(Free) 时走"免费子弹"分支
/// </summary>
public class LieYanFengBao_BOSS : MonsterControl_BossBase
{
@@ -16,22 +36,49 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
[Tooltip("是否从 param 解析 isKill/animId(关闭则每次死亡都按死亡处理)")]
public bool useServerParam = true;
/// <summary>爆奖事件引用</summary>
[Header("火球弹(lua BulletType.FlameRound,从 Wheel 子节点发射)")]
[Tooltip("火球弹预制体(Assets/MyGame/aa/Game/XJMH/Prefabs/Bullit/PW_Bullet_FlameRound.prefab)")]
public GameObject flameRoundBulletPrefab;
[Tooltip("火球弹飞行速度(lua speed=30,按本项目世界比例可调)")]
public float flameRoundSpeed = 30f;
[Tooltip("火球弹存活时长(秒,兜底回收,防飞出屏外不回收)")]
public float flameRoundLifeTime = 4f;
[Tooltip("param 里没有 flameInfo 时的默认发射数量(最多 = Wheel 子节点数)")]
public int flameRoundFallbackCount = 4;
[Header("时序(对齐 lua 硬编码值)")]
[Tooltip("爆炸特效 → 播爆奖的延迟(lua 0.3s)")]
public float explosionToRewardDelay = 0.3f;
[Tooltip("未死亡:爆奖 → 关掉全部特效恢复待机的延迟(lua 0.8s)")]
public float rewardToResetDelay = 0.8f;
[Tooltip("死亡:爆炸 → 生成火球弹 + 销毁本体的延迟(lua Timer 1.8s)")]
public float dieToFlameRoundDelay = 1.8f;
/// <summary>爆奖事件引用(死亡时赋值,供外部检查爆奖是否结束)</summary>
[System.NonSerialized]
public LieYanFengBaoEvent currEvent;
/// <summary>当前死亡次数(仅日志用)</summary>
private int _deathCount = 0;
// ---- 与 lua 对齐的常量 ----
private const string AnimNormal = "stand01"; // lua SetAnimation(1)
private const string AnimHit = "stand02"; // lua SetAnimation(2)
private const int EffectStand = 1; // lua effectTab[1]
private const int EffectDie = 2; // lua effectTab[2]
private const int EffectExplosion = 3; // lua effectTab[3]
private const string BonePath = "Body/Bone";
private const string WheelName = "Wheel";
private static readonly string[] EffectNodeNames = { "Stand", "Die", "Explosion" }; // 下标 0/1/2
private const int BulletTypeFree = 1; // lua GC.Config.BulletType.Free
/// <summary>缓存死亡位置(删除 Boss 后 transform 不可靠)</summary>
// ---- 运行时状态 ----
private int _deathCount = 0;
private Vector3 _diePos;
private bool _diePosCached = false;
/// <summary>Boss 是否已从鱼列表回收(防重复回收)</summary>
private bool _removed = false;
/// <summary>爆奖事件是否已 Init(决定特效实例由谁回收)</summary>
private bool _awardInitCalled = false;
private SkeletonAnimation _spine; // 预制体 skeleton 字段为空 → 运行时自动解析
private readonly GameObject[] _effects = new GameObject[3];
private Transform _wheel; // 轮盘(火球弹发射点父节点)
private Coroutine _flowRoutine; // 表现流程协程(死亡/未死亡共用)
public override void OnEnable()
{
@@ -41,8 +88,83 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
_removed = false;
_awardInitCalled = false;
currEvent = null;
_flowRoutine = null;
ResolveNodes();
// lua OnCreate:ActiveCollider(true) + SetAnimation(1)
SetAnimation(EffectStand);
}
public override void OnDisable()
{
StopFlow();
base.OnDisable();
}
// ================================================================
// 节点解析 & 表现(对齐 lua SetAnimation / ShowEffect)
// ================================================================
/// <summary>
/// 解析本体节点:Bone 下三个特效(Stand/Die/Explosion)+ Wheel + SkeletonAnimation。
/// ⭐ 预制体 PW_Enemy_115 的 skeleton 字段为空,这里自动 GetComponentInChildren 兜底。
/// </summary>
private void ResolveNodes()
{
Transform bone = FindByPath(BonePath);
for (int i = 0; i < EffectNodeNames.Length; i++)
{
_effects[i] = FindEffectNode(bone, EffectNodeNames[i]);
}
_wheel = FindChildRecursive(transform, WheelName);
if (_spine == null)
_spine = GetComponentInChildren<SkeletonAnimation>(true);
if (bone == null)
Debug.LogWarning($"[LieYanFengBao] 找不到 {BonePath},待机/死亡/爆炸特效无法切换");
if (_wheel == null)
Debug.LogWarning($"[LieYanFengBao] 找不到 {WheelName},死亡时无法生成火球弹");
}
/// <summary>lua SetAnimation(type):切 Spine 动画(stand01/stand02,循环)+ ShowEffect(type)</summary>
private void SetAnimation(int type)
{
PlaySpineSafe(type == EffectDie ? AnimHit : AnimNormal, true);
ShowEffect(type);
}
/// <summary>lua ShowEffect(type):只激活 effectTab[type],type=0 时全部关闭</summary>
private void ShowEffect(int type)
{
for (int i = 0; i < _effects.Length; i++)
{
if (_effects[i] == null) continue;
bool on = (i + 1) == type; // effectTab[1..3] ↔ _effects[0..2]
if (_effects[i].activeSelf != on)
_effects[i].SetActive(on);
}
}
/// <summary>安全播放 Spine 动画(骨架/动画名缺失时静默跳过)</summary>
private void PlaySpineSafe(string animName, bool loop)
{
if (_spine == null || string.IsNullOrEmpty(animName)) return;
var data = _spine.SkeletonDataAsset != null ? _spine.SkeletonDataAsset.GetSkeletonData(false) : null;
if (data == null || data.FindAnimation(animName) == null)
{
Debug.LogWarning($"[LieYanFengBao] Spine 动画 {animName} 不存在,跳过");
return;
}
_spine.AnimationState.SetAnimation(0, animName, loop);
}
// ================================================================
// 死亡 / 被击(对齐 lua OnExplosion,两种分支)
// ================================================================
public override void DieFish(RoomKillNotice obj = null)
{
_deathCount++;
@@ -50,10 +172,11 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
// ===== 解析服务端 param:animId 优先于 isKill =====
bool isKill;
int beishu;
JToken param = obj?.param;
if (useServerParam && obj?.param != null)
if (useServerParam && param != null)
{
var p = BossKillParamParser.ParseMultiDeath(obj.param);
var p = BossKillParamParser.ParseMultiDeath(param);
if (p != null)
{
isKill = p.isKill;
@@ -64,7 +187,6 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
}
else
{
// param 解析失败:按死亡处理
isKill = true;
beishu = obj.beishu > 0 ? (int)obj.beishu : 1;
Debug.LogWarning($"[LieYanFengBao] param 解析失败,按死亡处理 beishu={beishu}");
@@ -72,37 +194,179 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
}
else
{
// 老消息无 param:按死亡处理
isKill = true;
beishu = obj?.beishu > 0 ? (int)obj.beishu : 1;
Debug.Log($"[LieYanFengBao] DieFish 第{_deathCount}次(无param,按死亡) beishu={beishu}");
}
// 缓存死亡位置(供爆奖特效定位)
// 缓存死亡位置(Boss 删除后 transform 不可靠)
_diePos = transform.position;
_diePosCached = true;
// 炸一下 + bjEndUI(爆奖特效初始化回调,死亡/未死亡都播)
// 爆奖特效初始化回调(CreateFishManager 在死亡位置创建 effect_DieAward 后回调)
action_effect_DieAward = (o) => InitAwardEffect(o, obj, beishu);
bool isSelfKill = obj != null && GameManager.Instance != null && obj.userId == GameManager.Instance.user.id;
if (isKill)
{
// ===== 死亡:Boss 消失 =====
// ===== 死亡:死亡动画 + 死亡特效 + 音效 → 延迟生成火球弹 → 回收本体 =====
isDieFish = true;
if (obj != null)
poolNum = (int)obj.gold;
StartCoroutine(RemoveBossNextFrame());
SetAnimation(EffectDie);
// 音效 + 震屏(lua:只有自己击杀才播 PW_FlameStrike_Explosion/Die + 震屏/震手机)
if (isSelfKill)
{
AudioManager.Instance.PlayerFXByName("PW_FlameStrike_Explosion");
AudioManager.Instance.PlayerFXByName("PW_FlameStrike_Die");
// ⭐ 震屏对齐 lua explosionShakeScreen(CameraShaker,参数与其它 Boss 死亡一致)
if (GameManager.Instance != null && GameManager.Instance.cameraShake != null)
GameManager.Instance.cameraShake.ShakePresets.Explosion2D(3f, 4f, 1f);
}
else
{
AudioManager.Instance.PlayerFXByName("PW_FlameStrike_Explosion");
}
bool isFreeBullet = ReadBulletType(param) == BulletTypeFree;
StopFlow();
_flowRoutine = StartCoroutine(DeadFlow(isFreeBullet, param));
}
else
{
// ===== 未死亡:爆炸特效一下 + 播爆奖 → 关全部特效 → 恢复游动 =====
// ⭐ 不再调 base.DieFish():原版未死亡时不震屏、不标 isDieFish(否则每次命中都会震屏)
if (isSelfKill)
AudioManager.Instance.PlayerFXByName("PW_FlameStrike_Explosion");
StopFlow();
_flowRoutine = StartCoroutine(NotDeadFlow());
}
}
/// <summary>未死亡流程(lua OnExplosion 的 else 分支):爆炸特效 → 0.3s 爆奖 → 0.8s 关特效 → 恢复游动</summary>
private IEnumerator NotDeadFlow()
{
ShowEffect(EffectExplosion);
Debug.Log($"[LieYanFengBao] 未死亡:播放爆奖({explosionToRewardDelay}s 后)");
yield return new WaitForSeconds(explosionToRewardDelay);
// 爆奖特效由 CreateFishManager 生成的 effect_DieAward(LieYanFengBaoEvent)负责
yield return new WaitForSeconds(rewardToResetDelay);
// 回到待机:关掉全部特效(lua ShowEffect(0))
ShowEffect(0);
_flowRoutine = null;
OnAwardFinished(); // 恢复游动(lua:Boss 继续走)
}
/// <summary>死亡流程(lua OnExplosion 的 isDead 分支):爆炸特效 → 爆奖 → 1.8s 火球弹 → 回收</summary>
private IEnumerator DeadFlow(bool isFreeBullet, JToken param)
{
ShowEffect(EffectExplosion);
yield return new WaitForSeconds(explosionToRewardDelay);
// 爆奖特效(effect_DieAward + 事件)由 CreateFishManager 负责
if (isFreeBullet)
{
// lua:Free 子弹打死 → 0.8s 后直接销毁本体(不生成火球弹)
yield return new WaitForSeconds(rewardToResetDelay);
}
else
{
// lua:Timer 1.8s → 生成火球弹 + 销毁本体
yield return new WaitForSeconds(dieToFlameRoundDelay - explosionToRewardDelay > 0f
? dieToFlameRoundDelay - explosionToRewardDelay
: 0f);
SpawnFlameRounds(param);
}
_flowRoutine = null;
RemoveBoss();
}
/// <summary>
/// 生成火球弹(lua:从 Wheel 的第 i 个子节点朝外发射 BulletType.FlameRound)。
/// sourceUIDList 来自 param.flameInfo;缺失时按 flameRoundFallbackCount 逐个 Wheel 子节点发射。
/// </summary>
private void SpawnFlameRounds(JToken param)
{
if (flameRoundBulletPrefab == null)
{
Debug.LogWarning("[LieYanFengBao] flameRoundBulletPrefab 未赋值,跳过火球弹生成");
return;
}
if (_wheel == null)
{
Debug.LogWarning("[LieYanFengBao] Wheel 节点缺失,跳过火球弹生成");
return;
}
if (CreateFishManager.Instance == null || CreateFishManager.Instance.currGameType == null)
return;
// 发射数量:优先用服务端 sourceUIDList;缺失则回退配置数量
var uidList = ReadSourceUIDList(param);
int count = uidList != null && uidList.Count > 0 ? uidList.Count : Mathf.Max(0, flameRoundFallbackCount);
count = Mathf.Min(count, _wheel.childCount);
if (count <= 0)
{
Debug.LogWarning($"[LieYanFengBao] 火球弹数量为 0(Wheel 子节点={_wheel.childCount}),跳过");
return;
}
Transform bulletParent = CreateFishManager.Instance.bjEffectRoot != null
? CreateFishManager.Instance.bjEffectRoot
: null;
for (int i = 0; i < count; i++)
{
Transform point = _wheel.GetChild(i);
if (point == null) continue;
Vector3 spawnPos = point.position;
// ⭐ 方向 = Wheel 子节点相对本体的方向(lua:dir = point - self.pos,再取角度)
Vector3 dir = spawnPos - transform.position;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
// BullitMove 的飞行方向是 transform.up → 角度要 -90° 才是 Z 旋转
var bulletGo = BulletPool.Instance.Get(flameRoundBulletPrefab, spawnPos,
Quaternion.Euler(0f, 0f, angle - 90f), bulletParent);
if (bulletGo == null) continue;
bulletGo.name = $"FlameRound_{guid}_{i}";
var bullet = bulletGo.GetComponent<BullitMove>();
if (bullet != null)
{
bullet.Init(); // ⭐ 池化复用必须重新对齐起点
bullet.moveSpeed = flameRoundSpeed;
bullet.desTime = flameRoundLifeTime;
bullet.userid = null;
bullet.playerID = null;
bullet.bulletID = bulletGo.name;
Debug.Log($"[LieYanFengBao] 火球弹[{i}] 发射 从{spawnPos} 角度={angle:0.0}(sourceUID={(uidList != null && i < uidList.Count ? uidList[i].ToString() : "-")})");
}
else
{
Debug.LogWarning($"[LieYanFengBao] 火球弹预制体上没有 BullitMove,无法控制飞行");
}
}
// 未死亡:不删除,等爆奖结束 OnAwardFinished 恢复游动
}
/// <summary>初始化爆奖事件(炸一下 + bjEndUI)</summary>
private void InitAwardEffect(GameObject o, RoomKillNotice obj, int beishu)
{
Vector3 diePos = _diePosCached ? _diePos : transform.position;
if (obj == null) return;
Vector3 diePos = _diePosCached ? _diePos : transform.position;
foreach (var item in CreateFishManager.Instance.currGameType.list_Sits)
{
if (item.user == obj.userId)
@@ -117,6 +381,10 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
evt.bossCtrl = this;
currEvent = evt;
}
else
{
Debug.LogError("[LieYanFengBao] 爆奖预制体上没有 LieYanFengBaoEvent!只走基类流程");
}
v.Init(
obj.gold,
@@ -132,30 +400,6 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
}
}
/// <summary>
/// 下一帧删除 Boss:Ws_DieFish 在 DieFish 返回后同帧会实例化爆奖特效并触发回调。
/// 等到下一帧:Boss 被删除;爆奖事件没初始化(座位没找到)则把特效实例回池。
/// </summary>
private System.Collections.IEnumerator RemoveBossNextFrame()
{
yield return null;
if (!_removed)
{
_removed = true;
if (!_awardInitCalled && currEffect_DieAward != null)
{
var effect = currEffect_DieAward;
currEffect_DieAward = null;
EffectPool.Instance.Return(effect);
}
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
Debug.Log($"[LieYanFengBao] Boss 已删除 guid={guid}");
}
}
/// <summary>爆奖流程结束回调(Event 调用):未死亡时恢复游动</summary>
public void OnAwardFinished()
{
@@ -163,16 +407,126 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
{
ChangeState(CharacterStateType.move);
isMove = true;
// ⭐ 复位死亡标记(同 XingJiXunHang):非最终死亡 Boss 不回池,
// isDead 卡 true 会让锁定子弹被 BullitMove 判 target.isDead 立即 NotLock
// ⭐ 复位死亡标记:非最终死亡 Boss 不回池,isDead 卡 true 会让锁定子弹立即 NotLock
isDead = false;
Debug.Log("[LieYanFengBao] 未死亡爆奖结束,恢复游动");
}
currEvent = null;
}
/// <summary>回收本体(等火球弹生成后再删,保证发射点位置正确)</summary>
private void RemoveBoss()
{
if (_removed) return;
_removed = true;
// 兜底:爆奖事件没初始化(击杀者不在座位列表)→ 把爆奖特效实例回池
if (!_awardInitCalled && currEffect_DieAward != null)
{
var effect = currEffect_DieAward;
currEffect_DieAward = null;
EffectPool.Instance.Return(effect);
}
if (CreateFishManager.Instance != null)
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
Debug.Log($"[LieYanFengBao] Boss 已回收 guid={guid}");
}
private void StopFlow()
{
if (_flowRoutine != null)
{
StopCoroutine(_flowRoutine);
_flowRoutine = null;
}
}
// ================================================================
// param 辅助解析(原版数据在 data.flameInfo / data.bulletType 上)
// ================================================================
/// <summary>读 param.bulletType(原版 data.bulletType,1=Free)</summary>
private static int ReadBulletType(JToken param)
{
if (param is JObject o && o["bulletType"] != null)
{
try { return o["bulletType"].Value<int>(); } catch { }
}
return 0;
}
/// <summary>读 param.flameInfo.sourceUIDList(火球弹来源列表)</summary>
private static List<JToken> ReadSourceUIDList(JToken param)
{
try
{
if (param is JObject o)
{
var info = o["flameInfo"] as JObject;
if (info != null && info["sourceUIDList"] is JArray arr)
return new List<JToken>(arr);
}
}
catch (Exception e)
{
Debug.LogWarning($"[LieYanFengBao] 解析 flameInfo 失败: {e.Message}");
}
return null;
}
// ================================================================
// 查找工具
// ================================================================
/// <summary>按 "Body/Bone" 这类路径逐级查找(忽略大小写)</summary>
private Transform FindByPath(string path)
{
if (string.IsNullOrEmpty(path)) return null;
Transform cur = transform;
foreach (var part in path.Split('/'))
{
Transform next = null;
for (int i = 0; i < cur.childCount; i++)
{
if (string.Equals(cur.GetChild(i).name, part, StringComparison.OrdinalIgnoreCase))
{
next = cur.GetChild(i);
break;
}
}
if (next == null) return null;
cur = next;
}
return cur;
}
/// <summary>在 Bone 下找特效节点,找不到再全树递归兜底</summary>
private GameObject FindEffectNode(Transform bone, string name)
{
Transform t = bone != null ? bone.Find(name) : null;
if (t == null) t = FindChildRecursive(transform, name);
if (t == null) Debug.LogWarning($"[LieYanFengBao] 找不到特效节点 {name}");
return t != null ? t.gameObject : null;
}
/// <summary>递归查找子物体(忽略大小写)</summary>
private static Transform FindChildRecursive(Transform root, string name)
{
if (root == null) return null;
for (int i = 0; i < root.childCount; i++)
{
Transform c = root.GetChild(i);
if (string.Equals(c.name, name, StringComparison.OrdinalIgnoreCase)) return c;
Transform r = FindChildRecursive(c, name);
if (r != null) return r;
}
return null;
}
private void OnDestroy()
{
StopAllCoroutines();
_flowRoutine = null;
}
}