Files
5.24BuYu/Assets/MyGame/aa/Game/XJMH/Boss/LieYanFengBao/LieYanFengBao_BOSS.cs
T
2026-10-11 14:05:54 +08:00

804 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
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(对齐 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 → 未死亡(Boss 保留,爆奖结束恢复游动)
/// animId>=2 / isKill=true → 死亡(Boss 消失)
/// (可选)param.flameInfo = { originUID, ratio, sourceUIDList:[...] }:火球弹来源列表
/// param.bulletType = 1(Free) 时走"免费子弹"分支
/// </summary>
public class LieYanFengBao_BOSS : MonsterControl_BossBase
{
[Header("服务端参数解析")]
[Tooltip("是否从 param 解析 isKill/animId(关闭则每次死亡都按死亡处理)")]
public bool useServerParam = true;
[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("火球弹存活时长(秒):到点还没被反弹上限收尾就强制爆炸。\n" +
"⭐ 速度 30 → 8 秒 = 飞约 240 单位(屏幕对角线约 25~28 单位),正常会被下面的反弹次数先收尾。\n" +
" 想让子弹飞更久/更短就调这里(钻头蟹的兜底是 12s)。设 0 = 不限时,只靠反弹次数收尾")]
public float flameRoundLifeTime = 8f;
[Tooltip("param 里没有 flameInfo 时的默认发射数量(最多 = Wheel 子节点数)")]
public int flameRoundFallbackCount = 4;
[Tooltip("火球弹撞墙反弹次数上限,到达后原地爆炸回收(<=0 表示不限次数)。\n" +
"⭐ 每撞一次墙算 1 次,屏幕对角线约 25~28 单位、一屏横穿约 0.8s → 4 次大约 2~3.5 秒收尾。\n" +
" (钻头蟹是 7+5=12 次三段式;火球弹是单一阶段,建议 3~5)")]
public int flameRoundBounceLimit = 4;
[Tooltip("火球弹反弹时的震屏强度(仅自己击杀视角,lua ShakeScreen(0.5))")]
public float flameRoundBounceShake = 0.5f;
[Tooltip("火球弹命中特效(可拖 Art/XJMH/pw/pw_prefab/game/effect/PW_Effect_Hit_Prop6.prefab,留空则不出)")]
public GameObject flameRoundHitEffectPrefab;
[Header("火球弹收尾爆炸(炸一圈小鱼)")]
[Tooltip("火球弹收尾爆炸的杀鱼半径(世界单位,对齐钻头蟹 explosionRadius)")]
public float flameRoundExplosionRadius = 3f;
[Tooltip("火球弹收尾爆炸是否炸死范围小鱼(对齐钻头蟹 killFishOnFinal)")]
public bool flameRoundKillFishOnFinal = true;
[Header("火球弹爆奖(收尾 BJEndUI)")]
[Tooltip("BJEndUI 结算面板预制体(**兜底**用:爆奖事件取不到 bjEndUI 时,Boss 直接用它出最终面板)")]
public GameObject flameRoundBJEndUIPrefab;
[Tooltip("注入给火球弹的爆奖总分(<0 表示让子弹用累计击杀分值兜底)。建议从后端 gold 注入")]
public long flameRoundJackpotScore = -1;
[Tooltip("注入给火球弹的爆奖倍率(<=0 用子弹自身炮倍 paoBei)")]
public int flameRoundJackpotBeishu = 0;
[Tooltip("⭐ 等**所有**火球弹(含各自的收尾爆炸)都跑完,才显示最终爆奖面板。\n" +
"对齐钻头蟹:钻头跑完 → ShowEndUI(加分 + BJEndUI)。\n" +
"关掉 = 沿用旧行为(Boss 一死就把面板弹出来,火球弹还在飞)")]
public bool flameRoundDeferSettle = true;
[Tooltip("火球弹相关详细日志(发射/杀鱼/收尾/结算)")]
public bool flameRoundDebugLog = true;
[Tooltip("⭐ 火球弹杀鱼方式(默认开 = 钻头蟹 / 激光蟹 的既有做法):\n" +
"开(默认):本地帧同步 CreateFishManager.Id_DieFish_FX —— 小鱼**立刻**死 + 死亡特效 + 金币飞向击杀者炮台;\n" +
" 与 ZuanTouXie_DieEvent.KillSmallFish / JiGuangXie_DieEvent(SpecialLaserMove.onHitFish) 完全同款。\n" +
"关:改为只发 KillFish 服务端上报。\n" +
" ⚠ 火球弹不是服务端开火(FireCannon)产生的,bulletId 是客户端自造的 → 服务端大概率不认这条击杀,\n" +
" 表现就是\"碰到小鱼但鱼不炸\"。除非服务端专门支持 FlameRound,否则别关。")]
public bool flameRoundUseLocalInstantKill = true;
[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;
// ---- 与 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
// ---- 运行时状态 ----
private int _deathCount = 0;
private Vector3 _diePos;
private bool _diePosCached = false;
private bool _removed = false;
private bool _awardInitCalled = false;
private SkeletonAnimation _spine; // 预制体 skeleton 字段为空 → 运行时自动解析
private readonly GameObject[] _effects = new GameObject[3];
private Transform _wheel; // 轮盘(火球弹发射点父节点)
// 火球弹的击杀归属(死亡时的 Boss 击杀者):谁打死 Boss,火球弹收的鱼就记在谁头上
private string _killerUserId;
private int _killerPaoBei = 1;
private bool _killerIsSelf;
// 火球弹收尾统计(对齐钻头蟹:所有子弹都炸完 → 才显示最终爆奖面板)
private int _flameRoundsAlive; // 还在飞的火球弹数量
private int _flameRoundRound; // 齐射轮次(子弹回调按轮次校验,防 Boss 回收复用后误触发)
private LieYanFengBaoEvent _deferSettleEvent; // 等"火球弹全部结束"再结算的爆奖事件
private bool _flameSettled; // 本次爆奖已结算(防重入)
private Coroutine _flowRoutine; // 表现流程协程(死亡/未死亡共用)
public override void OnEnable()
{
base.OnEnable();
_deathCount = 0;
_diePosCached = false;
_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);
}
// ================================================================
// 锁定
// ================================================================
/// <summary>
/// ⭐ 爆奖不死(非最终死亡)时不影响各座位对该 Boss 的锁定:不清锁,玩家不用重新点一次。
/// 判定与 DieFish 保持一致:animId 优先于 isKill(animId>=2 才算最终死亡)。
/// </summary>
public override bool ShouldClearLockOnKill(RoomKillNotice killNotice)
{
if (!useServerParam || killNotice?.param == null || killNotice.param.Type == JTokenType.Null)
return true; // 没有 param → 按"会死"处理,走原来的清锁逻辑
var p = BossKillParamParser.ParseMultiDeath(killNotice.param);
if (p == null) return true;
return p.animId >= 1 ? p.animId >= 2 : p.isKill;
}
// ================================================================
// 死亡 / 被击(对齐 lua OnExplosion,两种分支)
// ================================================================
public override void DieFish(RoomKillNotice obj = null)
{
_deathCount++;
// 每次击杀重置火球弹/结算状态(对齐钻头蟹:一轮子弹一轮结算)
_flameRoundsAlive = 0;
_flameSettled = false;
_deferSettleEvent = null;
// ===== 解析服务端 param:animId 优先于 isKill =====
bool isKill;
int beishu;
JToken param = obj?.param;
if (useServerParam && param != null)
{
var p = BossKillParamParser.ParseMultiDeath(param);
if (p != null)
{
isKill = p.isKill;
beishu = p.beishu > 0 ? p.beishu : (obj.beishu > 0 ? (int)obj.beishu : 1);
if (p.animId >= 1)
isKill = p.animId >= 2;
Debug.Log($"[LieYanFengBao] DieFish 第{_deathCount}次(服务端) animId={p.animId} isKill={isKill} beishu={beishu} gold={obj.gold}");
}
else
{
isKill = true;
beishu = obj.beishu > 0 ? (int)obj.beishu : 1;
Debug.LogWarning($"[LieYanFengBao] param 解析失败,按死亡处理 beishu={beishu}");
}
}
else
{
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;
// 爆奖特效初始化回调(CreateFishManager 在死亡位置创建 effect_DieAward 后回调)
action_effect_DieAward = (o) => InitAwardEffect(o, obj, beishu);
bool isSelfKill = obj != null && GameManager.Instance != null && obj.userId == GameManager.Instance.user.id;
// ⭐ 火球弹的击杀归属:Boss 击杀者(谁打死的 Boss,之后火球弹收的鱼就记在谁头上)
_killerUserId = obj != null ? obj.userId : null;
_killerIsSelf = isSelfKill;
_killerPaoBei = ResolvePaoBei(_killerUserId);
if (isKill)
{
// ===== 死亡:死亡动画 + 死亡特效 + 音效 → 延迟生成火球弹 → 回收本体 =====
isDieFish = true;
if (obj != null)
poolNum = (int)obj.gold;
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);
// ⭐ 兜底:延迟结算已挂上、但一发火球弹都没发出去(预制体/节点缺失等异常)→ 立刻结算,
// 否则爆奖事件会一直等下去,既不结算也不回池(面板永远不出现)
if (_deferSettleEvent != null && _flameRoundsAlive <= 0)
{
Debug.LogWarning("[LieYanFengBao] 没有火球弹在飞 → 立即结算最终爆奖(避免面板不出)");
SettleFinalJackpot();
}
}
_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;
// 本次齐射统计(全部收尾后才结算最终爆奖)
_flameRoundsAlive = 0;
_flameSettled = false;
_flameRoundRound++; // 轮次 +1:上一轮遗留的子弹回调会被轮次校验忽略
int round = _flameRoundRound;
// 归属快照:子弹可能活到 Boss 被回收/复用之后,闭包里用快照而不是实时字段
string killerId = _killerUserId;
int paoBeiSnap = _killerPaoBei;
bool isSelfKill = _killerIsSelf;
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;
// ⭐ 用 Instantiate 而非 BulletPool:火球弹不属于任何座位(userid 为空),
// 一旦进了子弹池的"活跃子弹"表,换炮台时的 BulletPool.ClearActive() 会把正在飞的
// 火球弹一起 Destroy(表现为"子弹凭空消失")。自管理(FlameRoundBullet 里 Destroy)更稳。
var bulletGo = Instantiate(flameRoundBulletPrefab, spawnPos,
Quaternion.Euler(0f, 0f, angle - 90f), bulletParent);
if (bulletGo == null) continue;
bulletGo.name = $"FlameRound_{guid}_{i}";
// ⭐ 关掉通用子弹脚本(它的反弹靠碰撞体名字匹配、碰鱼就回池),改用钻头那套 FlameRoundBullet:
// 法线反弹 + 碰鱼穿透继续飞 + 反弹次数到顶后原地爆炸回收
var bullit = bulletGo.GetComponent<BullitMove>();
if (bullit != null)
{
// 先借它自己那套初始化:对齐 z=-400 的渲染深度 + 重新应用 sortingOrder(池化复用必需)
bullit.moveSpeed = flameRoundSpeed;
bullit.desTime = flameRoundLifeTime;
bullit.bulletID = bulletGo.name;
bullit.Init();
// ⭐ 双保险:enabled=false 挡掉它的所有 Unity 回调;
// isTriggerFirst=false 再挡一层(BullitMove 的碰撞分支整段包在 if (isTriggerFirst) 里,
// 这样即使它被重新启用,也不会碰鱼就 BulletPool.Return)
bullit.enabled = false;
bullit.isTriggerFirst = false;
}
var flame = bulletGo.GetComponent<FlameRoundBullet>();
if (flame == null) flame = bulletGo.AddComponent<FlameRoundBullet>();
flame.enabled = true;
flame.moveSpeed = flameRoundSpeed;
flame.lifeTime = flameRoundLifeTime;
flame.bounceLimit = flameRoundBounceLimit;
flame.bounceShakeStrength = flameRoundBounceShake;
flame.hitEffectPrefab = flameRoundHitEffectPrefab;
flame.explosionRadius = flameRoundExplosionRadius;
flame.killFishOnFinal = flameRoundKillFishOnFinal;
flame.bjEndUIPrefab = flameRoundBJEndUIPrefab;
flame.jackpotScore = flameRoundJackpotScore;
flame.jackpotBeishu = flameRoundJackpotBeishu;
flame.killerUserId = _killerUserId;
flame.paoBei = _killerPaoBei;
flame.isSelf = _killerIsSelf;
flame.bulletId = bulletGo.name;
flame.sourceUID = (uidList != null && i < uidList.Count) ? uidList[i].Value<int>() : (i + 1);
// ⭐ 火球弹杀鱼(默认 = 钻头蟹/激光蟹同款:本地帧同步 Id_DieFish_FX,碰到小鱼立刻炸)
// · flameRoundUseLocalInstantKill = true(默认):Id_DieFish_FX → 小鱼立刻死 + 金币飞。
// 与 ZuanTouXie_DieEvent.KillSmallFish、JiGuangXie_DieEvent 的注入方式完全一致
// (`if (isKiller) onHitFish = KillSmallFish` / `isKiller ? KillSmallFish : null`)。
// · = false:只发 KillFish 服务端上报。⚠ 火球弹不是服务端开火产生的子弹,
// bulletId 为客户端自造 → 服务端不认 → 表现就是"碰到小鱼但鱼不炸"(别再关)。
// ⚠ 用闭包快照(killerId/paoBei/isSelf/round):子弹寿命可能长于 Boss(Boss 被回收复用后
// 字段会被新击杀覆盖),快照保证归属正确;round 保证上一轮遗留回调不误触发结算。
flame.onHitFish = (flameRoundUseLocalInstantKill && isSelfKill)
? (Action<MonsterContral>)(f => KillSmallFishByFlameRound(f, killerId, paoBeiSnap, isSelfKill, round))
: null;
// 两种方式互斥:本地已经杀掉鱼并移除字典,若再上报会让本端被服务端回包"跳过"(白白多一次无效请求)
flame.reportKill = !flameRoundUseLocalInstantKill;
// ⭐ 收尾回调:Boss 统计"所有火球弹都收尾了没",全部结束才统一出**一次**最终爆奖面板
flame.onFinished = () => OnOneFlameRoundFinished(round);
flame.spawnJackpotOnFinish = false;
_flameRoundsAlive++;
// 出膛方向 = 已摆好的朝向(transform.up 就是 (cos angle, sin angle))
flame.Init(bulletGo.transform.up);
Debug.Log($"[LieYanFengBao] 火球弹[{i}] 发射 从{spawnPos} 角度={angle:0.0} " +
$"sourceUID={flame.sourceUID} 击杀者={_killerUserId} 炮倍={_killerPaoBei} " +
$"杀鱼回调={(_killerIsSelf ? "本地帧同步(击杀者端)" : "无(非击杀者端)")} " +
$"|BullitMove={(bullit == null ? "无" : (bullit.enabled ? "仍开着!" : "已关"))} " +
$"FlameRoundBullet=已挂");
}
Debug.Log($"[LieYanFengBao] 火球弹齐射完成:{_flameRoundsAlive} 发在飞,全部收尾后才显示最终爆奖面板");
// 兜底:一发都没发出去(预制体/节点异常)→ 直接结算,避免爆奖卡住不出面板
if (_flameRoundsAlive == 0) SettleFinalJackpot();
}
/// <summary>
/// 火球弹本地帧同步杀小鱼(与钻头蟹 ZuanTouXie_DieEvent.KillSmallFish、
/// 激光蟹 JiGuangXie_DieEvent → SpecialLaserMove.onHitFish 完全同款):
/// 金币 = 击杀者炮倍 × 鱼分,走 <c>CreateFishManager.Id_DieFish_FX</c> → 小鱼**立刻**死 +
/// 死亡特效 + 金币飞向击杀者炮台。
///
/// 为什么必须走本地(而不是 KillFish 服务端上报):
/// 火球弹不是服务端 FireCannon 产生的子弹、bulletId 是客户端自造的,服务端不认这条击杀 →
/// 上报路径表现为"碰到小鱼但鱼不炸"。钻头/激光在本项目里就是本地帧同步这条路。
/// ⭐ 只有击杀者端会注册成 onHitFish 回调(与钻头 `if (isKiller)`、激光 `isKiller ? KillSmallFish : null` 一致)。
/// ⚠ 归属参数由子弹创建时**快照**传入(子弹寿命 20s 可能长于 Boss 对象生命周期)。
/// </summary>
private void KillSmallFishByFlameRound(MonsterContral fish, string killerId, int paoBei, bool isSelfKill, int round)
{
if (round != _flameRoundRound) return; // 上一轮齐射的遗留回调,忽略
if (fish == null || fish.isDead) return;
if (fish.fishenum != FishTypeEnum.普通鱼) return; // 只杀小鱼(黄金鱼/特殊鱼/Boss 穿过)
if (CreateFishManager.Instance == null) return;
SitPosData sit = FindSitByUser(killerId);
if (sit == null)
{
Debug.LogWarning($"[LieYanFengBao] 火球弹杀鱼找不到击杀者座位({killerId}),跳过本次击杀");
return;
}
// 金币飞向点:炮台世界坐标(与普通杀鱼/钻头一致)
Vector3 flyTo = sit.currentTurret != null
? (Vector3)sit.currentTurret.GetBulletStartWorldPos()
: sit.turretWorldPos;
long amount = (long)paoBei *
(fish.monsterData != null && fish.monsterData.fishTypeConfig != null
? fish.monsterData.fishTypeConfig.fishScore
: 0);
CreateFishManager.Instance.Id_DieFish_FX(
fish.guid,
new RedPacketRecord { Amount = amount },
flyTo,
isSelfKill);
if (flameRoundDebugLog)
Debug.Log($"[LieYanFengBao] 火球弹杀小鱼 {fish.guid} 金币={amount}(炮倍{paoBei}×鱼分) 飞向={flyTo}");
}
/// <summary>一发火球弹收尾完成(含它的收尾爆炸炸鱼);全部结束 → 统一结算最终爆奖</summary>
private void OnOneFlameRoundFinished(int round)
{
if (round != _flameRoundRound) return; // 过期轮次,忽略(防 Boss 回收复用后误触发结算)
_flameRoundsAlive--;
Debug.Log($"[LieYanFengBao] 一发火球弹收尾完成,剩余 {Mathf.Max(0, _flameRoundsAlive)} 发");
if (_flameRoundsAlive > 0) return;
SettleFinalJackpot();
}
/// <summary>
/// 最终爆奖结算(对齐钻头蟹:所有子弹跑完 → ShowEndUI):
/// 交回爆奖事件结算(JiaFen 回加金币 + ShowBJEndUI 出一次面板 + 通知本类恢复/回收)。
/// 事件不可用时,Boss 直接用 flameRoundBJEndUIPrefab 出面板 + 调 JiaFen 兜底。
/// </summary>
private void SettleFinalJackpot()
{
if (_flameSettled) return;
_flameSettled = true;
var evt = _deferSettleEvent;
_deferSettleEvent = null;
if (evt != null)
{
Debug.Log("[LieYanFengBao] 火球弹全部结束 → 通知爆奖事件结算最终面板");
evt.SettleNow();
return;
}
// 兜底:爆奖事件没走延迟结算(未初始化/未死亡等)→ Boss 自己出面板
// 事件正常路径下已经在初始化时结算过,这里只处理异常兜底,避免面板完全不出现
Debug.LogWarning("[LieYanFengBao] 火球弹全部结束但没有待结算的爆奖事件(面板可能已由事件提前显示)");
}
/// <summary>按 userId 查座位(找不到返回 null)</summary>
private SitPosData FindSitByUser(string userId)
{
var mgr = CreateFishManager.Instance;
if (mgr == null || mgr.currGameType == null || mgr.currGameType.list_Sits == null) return null;
if (string.IsNullOrEmpty(userId)) return null;
return mgr.currGameType.list_Sits.Find(s => s.user == userId);
}
/// <summary>击杀者座位的炮倍(火球弹上报击杀用);取不到就退回本机当前座位,再不行给 1。</summary>
private int ResolvePaoBei(string userId)
{
var mgr = CreateFishManager.Instance;
if (mgr != null && mgr.currGameType != null)
{
if (!string.IsNullOrEmpty(userId) && mgr.currGameType.list_Sits != null)
{
var sit = mgr.currGameType.list_Sits.Find(s => s.user == userId);
if (sit != null && sit.consumeGoldNum > 0) return (int)sit.consumeGoldNum;
}
if (mgr.currGameType.currSitData != null && mgr.currGameType.currSitData.consumeGoldNum > 0)
return (int)mgr.currGameType.currSitData.consumeGoldNum;
}
return 1;
}
/// <summary>初始化爆奖事件(炸一下 + bjEndUI)</summary>
private void InitAwardEffect(GameObject o, RoomKillNotice obj, int beishu)
{
if (obj == null) return;
Vector3 diePos = _diePosCached ? _diePos : transform.position;
foreach (var item in CreateFishManager.Instance.currGameType.list_Sits)
{
if (item.user == obj.userId)
{
_awardInitCalled = true;
var v = o.GetComponent<BJEventBase>();
var evt = v as LieYanFengBaoEvent;
if (evt != null)
{
evt.explosionCenter = diePos;
evt.bossCtrl = this;
currEvent = evt;
// ⭐ 会发火球弹的死亡(isDieFish 且不是被免费子弹打死)→ 让爆奖事件
// **等所有火球弹收尾爆炸完成后**再结算(JiaFen + 面板),对齐钻头蟹的 ShowEndUI 时机
bool willSpawnFlame = isDieFish && ReadBulletType(obj.param) != BulletTypeFree;
bool defer = flameRoundDeferSettle && willSpawnFlame;
// ⚠ 显式赋值 true/false:事件是池化对象,避免残留上一轮的延迟标记
evt.deferSettle = defer;
if (defer)
{
_deferSettleEvent = evt;
Debug.Log("[LieYanFengBao] 最终爆奖已延迟:等所有火球弹收尾后再出面板");
}
}
else
{
Debug.LogError("[LieYanFengBao] 爆奖预制体上没有 LieYanFengBaoEvent!只走基类流程");
}
v.Init(
obj.gold,
item.posID,
obj.skillsGold,
obj.fishScore,
beishu,
JiaFen,
() => { v.obj = obj.param; }
);
break;
}
}
}
/// <summary>爆奖流程结束回调(Event 调用):未死亡时恢复游动</summary>
public void OnAwardFinished()
{
if (!isDieFish && isInitialized)
{
ChangeState(CharacterStateType.move);
isMove = true;
// ⭐ 复位死亡标记:非最终死亡 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;
}
}