804 lines
38 KiB
C#
804 lines
38 KiB
C#
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;
|
||
}
|
||
}
|