This commit is contained in:
2026-09-30 22:24:11 +08:00
parent 01c1698446
commit e38f0832a4
54 changed files with 1214 additions and 201 deletions
@@ -1,3 +1,4 @@
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using Assets.MyGame.Scripts.Net.WebResponses;
using Newtonsoft.Json.Linq;
using Spine.Unity;
@@ -41,10 +42,47 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
public GameObject flameRoundBulletPrefab;
[Tooltip("火球弹飞行速度(lua speed=30,按本项目世界比例可调)")]
public float flameRoundSpeed = 30f;
[Tooltip("火球弹存活时长(秒,兜底回收,防飞出屏外不回收)")]
public float flameRoundLifeTime = 4f;
[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)")]
@@ -78,6 +116,17 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
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()
@@ -169,6 +218,11 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
{
_deathCount++;
// 每次击杀重置火球弹/结算状态(对齐钻头蟹:一轮子弹一轮结算)
_flameRoundsAlive = 0;
_flameSettled = false;
_deferSettleEvent = null;
// ===== 解析服务端 param:animId 优先于 isKill =====
bool isKill;
int beishu;
@@ -208,6 +262,11 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
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)
{
// ===== 死亡:死亡动画 + 死亡特效 + 音效 → 延迟生成火球弹 → 回收本体 =====
@@ -286,6 +345,14 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
? dieToFlameRoundDelay - explosionToRewardDelay
: 0f);
SpawnFlameRounds(param);
// ⭐ 兜底:延迟结算已挂上、但一发火球弹都没发出去(预制体/节点缺失等异常)→ 立刻结算,
// 否则爆奖事件会一直等下去,既不结算也不回池(面板永远不出现)
if (_deferSettleEvent != null && _flameRoundsAlive <= 0)
{
Debug.LogWarning("[LieYanFengBao] 没有火球弹在飞 → 立即结算最终爆奖(避免面板不出)");
SettleFinalJackpot();
}
}
_flowRoutine = null;
@@ -325,6 +392,16 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
? 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);
@@ -335,29 +412,192 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
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,
// ⭐ 用 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}";
var bullet = bulletGo.GetComponent<BullitMove>();
if (bullet != null)
// ⭐ 关掉通用子弹脚本(它的反弹靠碰撞体名字匹配、碰鱼就回池),改用钻头那套 FlameRoundBullet:
// 法线反弹 + 碰鱼穿透继续飞 + 反弹次数到顶后原地爆炸回收
var bullit = bulletGo.GetComponent<BullitMove>();
if (bullit != 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,无法控制飞行");
// 先借它自己那套初始化:对齐 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>
@@ -380,6 +620,18 @@ public class LieYanFengBao_BOSS : MonsterControl_BossBase
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
{