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
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: ec57fb26f5bb61544b2831e9485b32fe, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_Compression: 2
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: 9971f2f92adebd44faa29cc332720d65, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_Compression: 2
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: 59a1dc2550990aa4091c8db201571a43, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_Compression: 2
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_Group: {fileID: 11400000, guid: aa56712de737c4b4ea481409576c9b09, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_Compression: 2
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
@@ -2,34 +2,28 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: FJH_yanhuo_02
m_Shader: {fileID: 200, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
_MainTex:
m_Texture: {fileID: 2800000, guid: c5d4efd524ef05b479430819e44412d2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _InvFade: 1
_InvFade: 1
m_Colors:
- _TintColor: {r: 0.8, g: 0.8, b: 0.8, a: 0.5}
m_BuildTextureStacks: []
_TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
@@ -2,34 +2,28 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: FJH_yanhuo_03
m_Shader: {fileID: 200, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
_MainTex:
m_Texture: {fileID: 2800000, guid: bfa85d8c83b5e2e41bfd646da11ea2d9, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _InvFade: 1
_InvFade: 1
m_Colors:
- _TintColor: {r: 0.8, g: 0.8, b: 0.8, a: 0.5}
m_BuildTextureStacks: []
_TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
@@ -2,34 +2,28 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: FJ_lizi_01
m_Shader: {fileID: 200, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
_MainTex:
m_Texture: {fileID: 2800000, guid: b75023ac4ff165a4f8105dc332dd228c, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _InvFade: 1
_InvFade: 1
m_Colors:
- _TintColor: {r: 1, g: 1, b: 1, a: 0.59607846}
m_BuildTextureStacks: []
_TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
@@ -2,34 +2,28 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: FJ_lizi_01_a
m_Shader: {fileID: 200, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
_MainTex:
m_Texture: {fileID: 2800000, guid: b75023ac4ff165a4f8105dc332dd228c, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _InvFade: 1
_InvFade: 1
m_Colors:
- _TintColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_BuildTextureStacks: []
_TintColor: {r: 0.5019608, g: 0.5019608, b: 0.5019608, a: 1}
@@ -2,34 +2,28 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
serializedVersion: 6
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: FJ_xulie_23
m_Shader: {fileID: 200, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_ShaderKeywords:
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
_MainTex:
m_Texture: {fileID: 2800000, guid: 7868e9e5d8d07ea4790ad0ffeebb1765, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _InvFade: 1
_InvFade: 1
m_Colors:
- _TintColor: {r: 0.8, g: 0.8, b: 0.8, a: 0.5}
m_BuildTextureStacks: []
_TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 55 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 79 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 130 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 110 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 96 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 34 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 52 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -93,7 +93,7 @@ TextureImporter:
uncompressedFormat: 0
desiredFormat: 0
usageMode: 0
colorSpace: 1
colorSpace: 0
androidETC2FallbackFormat: 0
width: 0
height: 0
@@ -446,7 +446,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1908357602512059}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -662,7 +662,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1713733470841517}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -537,7 +537,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1657386649342399}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -658,7 +658,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1705568770173308}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -389,7 +389,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1068391174607537}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -318,7 +318,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1105481484750792}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -54,7 +54,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1120841903074299}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3cf5a44414476512e00c3e7a2569a919, type: 3}
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
@@ -54,7 +54,7 @@ MonoBehaviour:
m_GameObject: {fileID: 1299482983134209}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8b6b0933debec03db3ed40115224c1a8, type: 3}
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 2100000, guid: b1d7ed7dd8acb9c45aef67b022b87c28, type: 2}
@@ -8,6 +8,21 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 2627120296159640219, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
propertyPath: m_LocalScale.x
value: 2
objectReference: {fileID: 0}
- target: {fileID: 2627120296159640219, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
propertyPath: m_LocalScale.y
value: 2
objectReference: {fileID: 0}
- target: {fileID: 2627120296159640219, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
propertyPath: m_LocalScale.z
value: 2
objectReference: {fileID: 0}
- target: {fileID: 2627120296159640219, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
propertyPath: m_LocalPosition.x
@@ -58,6 +73,11 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: -0
objectReference: {fileID: 0}
- target: {fileID: 2627120296159640219, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
propertyPath: m_ConstrainProportionsScale
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2628375401523424628, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
propertyPath: m_Name
@@ -0,0 +1,724 @@
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using DG.Tweening;
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 烈焰风暴火球弹(PW_Bullet_FlameRound)的飞行 / 反弹 / 命中控制。
///
/// 「击杀 + 反弹」框架参考钻头子弹 MyGame/aa/Game/FLZY/TSY/ZuanTou/SpecialBulletMove.cs:
/// · 撞墙走**法线反弹**(ClosestPoint 求法线 → Vector2.Reflect),反弹后沿法线推回墙外防穿模;
/// · 反弹冷却 0.05s 防一帧内连触发 / 穿墙;反弹刷新兜底计时(还在弹就永不超时);
/// · 碰鱼只处理「普通鱼」,同一条鱼只报一次,之后**穿透继续飞**(跟钻头一样,杀完继续打);
/// · 反弹次数到上限 / 兜底超时 → 收尾爆发,回收。
///
/// 表现参数对齐 lua pw_bullet/PW_BulletFlameStrike.txt:
/// · speed 30;sprite 自转 2400°/s(lua 用第一次算的 dt 缓存四元数,这里同样缓存);
/// · 反弹时(自己视角)震屏 + 播 PW_FlameStrike_WheelFly(同 1 秒内只播一次);
/// · 正下方(±90°)撞到上下墙 → 改成 ±30° 斜弹并按 lua 翻转 flag,防止贴着上下边来回弹;
/// · 碰鱼:命中特效(lua PW_Effect_Hit_Prop6)+ 上报击杀(KillFish,只有击杀者客户端发)+ OnHurt,
/// 子弹**不消失**(lua Collider() 也只上报、不销毁);
/// · 收尾(lua Disappear()):PW_FlameStrike_WheelExplosion 音效 + sprite 隐藏 + 切 Explosion 特效
/// + 0.6s 后回收。
///
/// 用法(不改预制体):由 <see cref="LieYanFengBao_BOSS"/> 在 BulletPool.Get 之后运行时 AddComponent,
/// 并把预制体上原来的 BullitMove 关掉,避免两套移动逻辑打架。
/// </summary>
public class FlameRoundBullet : MonoBehaviour
{
// ================================================================ Inspector
[Header("移动")]
[Tooltip("飞行速度(lua speed=30)")]
public float moveSpeed = 30f;
[Tooltip("兜底存活时间(秒):到点还没被反弹上限收尾就强制爆炸。\n" +
"速度 30 → 8 秒飞约 240 单位(屏幕对角线约 25~28 单位),正常会先被 bounceLimit 收尾")]
public float lifeTime = 8f;
[Tooltip("sprite 自转速度(度/秒,lua spinSpeed=2400)")]
public float spriteSpinSpeed = 2400f;
[Header("反弹")]
[Tooltip("撞墙反弹次数上限,到达后进入收尾爆炸回收(<=0 表示不限次数,只靠 lifeTime 兜底)。\n" +
"⭐ 每撞一次墙算 1 次 → 4 次大约 2~3.5 秒收尾(钻头蟹 7+5=12 次是三段式,火球弹是单一阶段)")]
public int bounceLimit = 4;
[Tooltip("反弹冷却(秒):防同一帧/连续多帧重复触发导致穿墙")]
public float bounceCooldown = 0.05f;
[Tooltip("同一面墙的再次反弹冷却(秒):防止卡在墙角里被连续计次,几下就打满反弹上限")]
public float sameWallCooldown = 0.25f;
[Tooltip("撞墙反弹是否刷新兜底计时(钻头做法)。关掉=严格按 lifeTime 超时,行为更可预测")]
public bool refreshLifeOnBounce = false;
[Tooltip("垂直(正上/正下)撞到上下墙时改成的斜角(lua ±30°),防止贴着上下边来回弹")]
public float verticalDeflectAngle = 30f;
[Tooltip("反弹时震屏 + 播 WheelFly 音效(仅自己视角)")]
public bool bounceFeedback = true;
[Tooltip("反弹震屏强度(ShortShake2D 的 positionStrength/rotationStrength)")]
public float bounceShakeStrength = 0.5f;
[Tooltip("反弹音效最小间隔(秒,lua 用时间戳做的 1 秒节流)")]
public float bounceSoundInterval = 1f;
[Header("命中鱼")]
[Tooltip("命中特效预制体(lua: PW_Effect_Hit_Prop6)")]
public GameObject hitEffectPrefab;
[Tooltip("⭐ 兜底上报开关:仅当创建方没给 onHitFish 回调时才用(走 WebSocketSendList.KillFish)。\n" +
"对齐钻头蟹时由创建方给回调走本地帧同步杀鱼(Id_DieFish_FX),此时保持 false 避免重复结算")]
public bool reportKill = false;
[Tooltip("打详细日志(排查用:每次 Trigger、每次碰鱼、被动回收都会打)")]
public bool debugLog = true;
[Tooltip("自动检测鱼(每帧 OverlapCircle 主动扫,不依赖 OnTrigger 事件):\n" +
"即使触发器因层/标签/帧率(速度30每帧0.6单位)漏触发,也一定能命中并穿透")]
public bool detectFishByOverlap = true;
[Tooltip("主动检测的额外半径(在碰撞体半径 + 每帧步长 之上再加的安全余量)")]
public float overlapExtraRadius = 0.35f;
[Tooltip("回收时不用 BulletPool、直接 Destroy:\n" +
"⭐ 关键保险 —— 本项目换炮台/BulletPool.ClearActive() 会销毁所有\"活跃子弹\",\n" +
"火球弹不属于任何座位,混在池里会被误清;自管理后外部清理一律带不走它")]
public bool selfManaged = true;
[Tooltip("墙层(留空自动取 Wall 层,另兼容 Wall 标签)")]
public LayerMask wallLayer;
[Tooltip("鱼层(留空自动取 Fish 层,另兼容 Fish 标签)")]
public LayerMask fishLayer;
[Header("节点名 / 收尾")]
[Tooltip("弹体 sprite 节点名(自转用)")]
public string spriteNodeName = "sprite";
[Tooltip("飞行中显示的特效节点名(lua ShowEffect(1)=Glow)")]
public string glowNodeName = "Glow";
[Tooltip("收尾时显示的特效节点名(lua ShowEffect(2)=Explosion)")]
public string explosionNodeName = "Explosion";
[Tooltip("收尾爆炸后停留多久回池(lua 0.6s)")]
public float explosionHoldTime = 0.6f;
[Header("收尾爆炸炸鱼(对齐钻头蟹 SpecialBulletMove.Explode)")]
[Tooltip("收尾爆炸的杀鱼半径(世界单位):炸一圈小鱼。<=0 表示不炸鱼")]
public float explosionRadius = 3f;
[Tooltip("收尾爆炸是否炸死范围小鱼(对齐钻头蟹 killFishOnFinal)")]
public bool killFishOnFinal = true;
[Tooltip("范围炸鱼时是否也各播一次命中特效(钻头蟹不播,靠爆炸特效;默认关,避免同点叠一堆)")]
public bool explosionSpawnHitEffect = false;
[Header("收尾爆奖(BJEndUI)")]
[Tooltip("⭐ 是否由本发子弹自己出爆奖面板。\n" +
"默认 **关** —— 4 发子弹各出一份 = 屏幕上叠 4 个面板。\n" +
"正确做法(对齐钻头蟹):由创建方 Boss 统计所有子弹都收尾后,统一出**一次**面板(走 onFinished 回调)")]
public bool spawnJackpotOnFinish = false;
[Tooltip("爆奖结算面板预制体(BJEndUI)。仅 spawnJackpotOnFinish=true 时用")]
public GameObject bjEndUIPrefab;
[Tooltip("爆奖倍率(beishu)。<=0 时自动用炮倍 paoBei")]
public int jackpotBeishu = 0;
[Tooltip("⭐ 爆奖总分:<0 表示自动累计本发击杀鱼的分值(die_Score) 作兜底;>=0 表示用该注入值(建议由 Boss 从后端 gold 注入)")]
public long jackpotScore = -1;
[Tooltip("爆奖面板相对击杀者座位的 Y 偏移(世界单位,座位上方;与其它爆奖一致)")]
public float bjEndUIOffsetY = 150f;
// ================================================================ 创建方注入
/// <summary>Boss 击杀者 userId(决定谁上报击杀;只有等于本机用户时才发请求)</summary>
[NonSerialized] public string killerUserId;
/// <summary>子弹标识(上报用)</summary>
[NonSerialized] public string bulletId;
/// <summary>炮倍(上报用,取击杀者座位 consumeGoldNum)</summary>
[NonSerialized] public int paoBei = 1;
/// <summary>sourceUID(lua 用它决定 180° 斜弹往左还是往右)</summary>
[NonSerialized] public int sourceUID;
/// <summary>是否本机击杀者(震屏/音效门限)</summary>
[NonSerialized] public bool isSelf;
/// <summary>碰鱼回调(创建方可追加:加分等),子弹不会因为回调而停止</summary>
[NonSerialized] public Action<MonsterContral> onHitFish;
/// <summary>收尾完成回调(回收前触发)</summary>
[NonSerialized] public Action onFinished;
// ================================================================ 内部状态
private Transform _sprite;
private GameObject _glow;
private GameObject _explosion;
private Rigidbody2D _rb;
/// <summary>同一个物体上的通用子弹脚本:本脚本接管后它必须保持关闭(开着就会"碰鱼即回池")</summary>
private BullitMove _bullit;
private Vector2 _heading = Vector2.up; // 前进方向(2D 平面),与模型旋转解耦
private Quaternion _baseRot = Quaternion.identity; // 预制体姿态(去掉 Z 分量后的基准)
private float _time; // 兜底计时
private float _bounceCd; // 反弹冷却
private float _lastBounceSoundTime = -99f; // 反弹音效节流
private int _bounceCount;
private int _flag = 1; // lua 的 flag:180° 斜弹时来回翻
private bool _finished;
private Tween _releaseTween;
// 主动检测鱼 / 同墙冷却
private float _baseRadius = 0.5f; // 自身碰撞体半径(Init 里取一次)
private Collider2D _lastWall; // 上一次反弹的墙
private float _lastWallTime = -99f;
private static readonly Collider2D[] _overlapBuf = new Collider2D[32];
private readonly HashSet<string> _hitFish = new HashSet<string>();
private long _accumScore; // 累计击杀分值(收尾爆奖兜底显示用)
// ================================================================ 生命周期
/// <summary>
/// 初始化并设置前进方向。
/// heading 语义与钻头 SpecialBulletMove 一致:世界方向;模型只转 Z 轴(保留预制体基准姿态)。
/// </summary>
public void Init(Vector2 heading)
{
KillReleaseTween();
_time = 0f;
_bounceCd = 0f;
_bounceCount = 0;
_flag = 1;
_finished = false;
_lastBounceSoundTime = -99f;
_lastWall = null;
_lastWallTime = -99f;
_hitFish.Clear();
_accumScore = 0;
if (wallLayer.value == 0) wallLayer = LayerMask.GetMask("Wall");
if (fishLayer.value == 0) fishLayer = LayerMask.GetMask("Fish");
// ⭐ 铁律:通用子弹脚本开着就"碰鱼即回池",本脚本接管期间必须保证它是关的。
// 再补一道保险:把 isTriggerFirst 置 false —— 即使它被重新启用,碰撞分支也整段跳过。
_bullit = GetComponent<BullitMove>();
if (_bullit != null)
{
if (_bullit.enabled)
Debug.LogWarning("[烈焰风暴火球弹] 检测到 BullitMove 处于开启状态 → 已强制关闭(否则会一碰鱼就回池)");
_bullit.enabled = false;
_bullit.isTriggerFirst = false;
}
_rb = GetComponent<Rigidbody2D>();
if (_rb != null)
{
// 位置由脚本每帧直接写,物理只用来收 trigger
_rb.gravityScale = 0f;
_rb.velocity = Vector2.zero;
_rb.angularVelocity = 0f;
}
// 自身碰撞体半径(主动检测鱼时用:半径 + 本帧位移 + 余量)
var col = GetComponent<Collider2D>();
_baseRadius = col != null ? Mathf.Max(col.bounds.extents.x, col.bounds.extents.y) : 0.5f;
if (_sprite == null) ResolveNodes(); // 池化复用不必每次重找(结构不会变)
// 视觉复位:上次收尾把 sprite 关掉了,池化复用必须打开(否则第二发看不到弹体)
if (_sprite != null) _sprite.gameObject.SetActive(true);
ShowExplosion(false); // 飞行中:Glow 亮、Explosion 灭
// 基准姿态 = 当前旋转去掉 Z 分量(本预制体旋转纯 Z,所以实际就是 identity)
float z0 = transform.eulerAngles.z;
_baseRot = Quaternion.Inverse(Quaternion.Euler(0f, 0f, z0)) * transform.rotation;
// 出膛方向:预制体已被创建方按朝向摆好 → 直接用 transform.up 取方向
SetHeading(transform.up);
Debug.Log($"[烈焰风暴火球弹 v3] 生命周期开始:接管={(_bullit != null ? "已接管(PW_Bullet_FlameRound)" : "⚠ 预制体上没有 BullitMove")} " +
$"pos={transform.position} dir={_heading} 速度={moveSpeed} 兜底={lifeTime}s " +
$"反弹上限={bounceLimit} 主动检测={detectFishByOverlap} 自管理={selfManaged} " +
$"sourceUID={sourceUID} 上报者={killerUserId}");
if (_bullit != null && _bullit.enabled)
Debug.LogWarning("[烈焰风暴火球弹] ⚠ BullitMove 处于开启状态 —— 它一碰鱼就会把子弹回池(表现为\"碰到鱼就消失\")");
// ⭐ 兜底时长提醒:屏幕对角线约 25~28 单位,速度 30 时 1 秒 ≈ 一屏多一点。
// 太短会看起来像"刚打出去就没了",所以只对"短到不合理"的配置告警(默认 8s 不触发)。
if (lifeTime > 0f && lifeTime < 3f)
{
Debug.LogWarning($"[烈焰风暴火球弹] ⚠ lifeTime={lifeTime}s 很短:速度 {moveSpeed} 时只飞约 " +
$"{lifeTime * moveSpeed:0} 单位(屏幕对角线约 25~28)就会被兜底回收");
}
}
private void OnDisable()
{
KillReleaseTween();
// ⭐ 诊断:正常收尾会先把 _finished 置 true 再回收;没置就走到这里 = **有别的东西**把子弹回收/隐藏了
if (!_finished && debugLog)
{
Debug.LogWarning($"[烈焰风暴火球弹] ⚠ 未收尾就被回收/隐藏 —— 不是本脚本回收的(很可能是 BullitMove 仍开着," +
$"或外部调了 BulletPool.ClearActive/ClearAll)。已飞 {_time:0.00}s、反弹 {_bounceCount} 次、" +
$"位置={transform.position}\n调用栈:{new System.Diagnostics.StackTrace(true)}");
}
}
/// <summary>被真正销毁时的最后一道证据:打印调用栈,一眼看出是谁 Destroy 的</summary>
private void OnDestroy()
{
if (!_finished && debugLog)
{
Debug.LogWarning($"[烈焰风暴火球弹] ⚠ 子弹在未收尾状态下被 Destroy(已飞 {_time:0.00}s、反弹 {_bounceCount} 次)\n" +
$"调用栈:{new System.Diagnostics.StackTrace(true)}");
}
}
// ================================================================ 每帧
private void Update()
{
if (_finished) return;
// 自愈:万一 BullitMove 被别处重新打开 → 立刻关掉(它一碰鱼就会 BulletPool.Return)
if (_bullit != null && (_bullit.enabled || _bullit.isTriggerFirst))
{
_bullit.enabled = false;
_bullit.isTriggerFirst = false;
Debug.LogWarning("[烈焰风暴火球弹] BullitMove 被重新打开 → 已强制关闭并封掉它的碰撞分支");
}
_time += Time.deltaTime;
if (_bounceCd > 0f) _bounceCd -= Time.deltaTime;
// 兜底:按 lifeTime 严格超时(refreshLifeOnBounce=true 时撞墙会重置,跟钻头一致)
if (lifeTime > 0f && _time >= lifeTime)
{
Debug.LogWarning($"[烈焰风暴火球弹] 兜底收尾:{lifeTime}s(已反弹 {_bounceCount}/{bounceLimit})pos={transform.position}");
Finish();
return;
}
SpriteSpin();
// ⭐ 主动命中检测:不依赖 OnTrigger 事件(速度 30 每帧走 0.6 单位,触发器在
// 层/标签/帧率异常时可能漏触发)→ 每帧扫一次自身周围的鱼,保证"穿透"一定生效
if (detectFishByOverlap) DetectFishByOverlap();
transform.position += (Vector3)(_heading * (moveSpeed * Time.deltaTime));
}
/// <summary>
/// 每帧主动扫周围的鱼(OverlapCircle)→ 命中走与 Trigger 完全相同的处理(同一条只报一次)。
/// 半径 = 自身碰撞体半径 + 本帧位移 + 额外余量,避免高速穿模漏检。
/// </summary>
private void DetectFishByOverlap()
{
float radius = _baseRadius + moveSpeed * Time.deltaTime + Mathf.Max(0f, overlapExtraRadius);
int n = Physics2D.OverlapCircleNonAlloc(transform.position, radius, _overlapBuf);
for (int i = 0; i < n; i++)
{
var c = _overlapBuf[i];
if (c == null) continue;
var fish = c.GetComponentInParent<MonsterContral>();
if (fish == null) continue;
ProcessFishHit(fish);
}
}
/// <summary>lua SpriteSpin:弹体 sprite 持续自转(2400°/s)</summary>
private void SpriteSpin()
{
if (_sprite == null || Mathf.Approximately(spriteSpinSpeed, 0f)) return;
_sprite.localRotation = Quaternion.AngleAxis(spriteSpinSpeed * Time.deltaTime, Vector3.forward)
* _sprite.localRotation;
}
// ================================================================ 碰撞
private void OnTriggerEnter2D(Collider2D other)
{
if (_finished) return;
if (debugLog && other != null)
{
Debug.Log($"[烈焰风暴火球弹] 收到 Trigger: {other.name} tag={other.tag} " +
$"layer={LayerMask.LayerToName(other.gameObject.layer)} 已反弹={_bounceCount}");
}
// —— 1. 撞墙:法线反弹 ——
if (IsWall(other))
{
if (_bounceCd > 0f) return; // 冷却中,忽略同一次碰撞的后续帧
// ⭐ 同一面墙的再次反弹冷却:子弹卡在墙角时会被连续计次,几下就打满反弹上限 → 子弹"无故消失"
if (_lastWall == other && Time.time - _lastWallTime < sameWallCooldown) return;
Bounce(other);
_bounceCd = bounceCooldown;
_lastWall = other;
_lastWallTime = Time.time;
if (refreshLifeOnBounce) _time = 0f; // 默认关:严格按 lifeTime 超时,行为可预测
_bounceCount++;
PlayBounceFeedback();
if (debugLog)
Debug.Log($"[烈焰风暴火球弹] 撞墙反弹 第{_bounceCount}/{bounceLimit} 次 → 新方向 {_heading}");
if (bounceLimit > 0 && _bounceCount >= bounceLimit)
{
Debug.Log($"[烈焰风暴火球弹] 反弹 {_bounceCount} 次达到上限 → 收尾爆炸");
Finish();
}
return;
}
// —— 2. 碰鱼:上报 + 穿透继续飞(同一条鱼只报一次)——
if (!IsFish(other)) return;
ProcessFishHit(other.GetComponentInParent<MonsterContral>());
}
/// <summary>
/// 命中一条鱼(Trigger、主动 Overlap、收尾爆炸范围杀鱼 共用):
/// 只处理「普通鱼」→ 同一条只报一次 → 命中特效 + 上报击杀 + OnHurt → **不回池、不销毁**(穿透继续飞)。
/// </summary>
/// <param name="spawnHitEffect">是否播命中特效(收尾爆炸时传 false,避免同点叠一堆)</param>
private void ProcessFishHit(MonsterContral fish, bool spawnHitEffect = true)
{
// ⚠ 注意:收尾爆炸范围杀鱼是在 Finish() 里、且必须在 _finished 置 true **之前** 调用
if (_finished) return;
if (fish == null || fish.isDead) return;
if (fish.fishenum != FishTypeEnum.普通鱼) return; // ⭐ 只打小鱼(黄金鱼/特殊鱼/Boss 穿过)
if (!_hitFish.Add(fish.guid)) return; // ⭐ 同一条鱼只报一次
// 累计击杀分值(创建方没注入 jackpotScore 时的兜底显示值;口径与杀鱼一致:炮倍×鱼分)
if (fish.monsterData != null && fish.monsterData.fishTypeConfig != null)
_accumScore += (long)paoBei * fish.monsterData.fishTypeConfig.fishScore;
if (spawnHitEffect && hitEffectPrefab != null && EffectPool.Instance != null)
EffectPool.Instance.Get(hitEffectPrefab, transform.position, Quaternion.identity, transform.parent);
// ⭐ 杀鱼(对齐钻头蟹 SpecialBulletMove.ProcessHit → 创建方 KillSmallFish):
// 优先由创建方回调处理(本地帧同步 CreateFishManager.Id_DieFish_FX → 小鱼立刻死、全房间同步);
// 创建方没给回调时才退回内置 KillFish 上报(reportKill 默认 false,避免和服务端重复结算)
if (onHitFish != null)
{
onHitFish(fish);
}
else if (reportKill)
{
ReportKill(fish);
}
// 命中表现(lua:EnemyMgr.OnHit;只在这里调一次,ReportKill 里不再重复调)
fish.OnHurt();
// ⚠ 这里绝不能回池 / 不能销毁:lua 火球弹是穿透的(一发能连打多条鱼),
// 一旦这里消失 = 表现为"子弹碰到鱼就没了"
if (debugLog)
Debug.Log($"[烈焰风暴火球弹] 击杀 {fish.guid}(fishenum={fish.fishenum}) 命中特效={spawnHitEffect} " +
$"杀鱼={(onHitFish != null ? "创建方回调(帧同步)" : (reportKill ? "内置KillFish上报" : "⚠ 无任何杀鱼路径"))} " +
$"(本发已打第 {_hitFish.Count} 条,反弹 {_bounceCount}/{bounceLimit})");
}
private bool IsWall(Collider2D c)
{
if (c == null) return false;
if (c.CompareTag("Wall")) return true;
return wallLayer.value != 0 && ((1 << c.gameObject.layer) & wallLayer.value) != 0;
}
private bool IsFish(Collider2D c)
{
if (c == null) return false;
if (c.CompareTag("Fish")) return true;
return fishLayer.value != 0 && ((1 << c.gameObject.layer) & fishLayer.value) != 0;
}
/// <summary>
/// 法线反弹(照搬钻头 SpecialBulletMove.Bounce):
/// ClosestPoint 求近似法线 → Reflect;另处理 lua 的"正上/正下撞上下墙改斜角"特判;
/// 最后沿法线把弹体推回墙外,防止高速下一帧跨进墙内造成穿模/连触发。
/// </summary>
private void Bounce(Collider2D wall)
{
Vector3 worldPos = transform.position;
float keepZ = worldPos.z; // ⭐ 本项目子弹 z=-400(BullitMove.Init 设的),反弹后必须保留
Vector2 pos = worldPos;
Vector2 closest = wall.ClosestPoint(pos);
Vector2 normal = pos - closest;
if (normal.sqrMagnitude < 0.0001f)
normal = pos - (Vector2)wall.bounds.center;
if (normal.sqrMagnitude < 0.0001f) return;
normal.Normalize();
Vector2 reflected = Vector2.Reflect(_heading, normal);
if (reflected.sqrMagnitude < 0.0001f) return;
reflected.Normalize();
// lua 特判:撞的是上下墙(法线以 Y 为主)且当前是垂直飞行 → 改成 ±30° 斜弹
if (Mathf.Abs(normal.y) >= Mathf.Abs(normal.x))
{
float curAngle = Mathf.Atan2(_heading.y, _heading.x) * Mathf.Rad2Deg;
if (Mathf.Abs(Mathf.Abs(curAngle) - 90f) < 1f)
{
// lua:180 + (IsOuNumber(sourceUID) and 30 or -30) * flag,之后 flag 取反
// IsOuNumber = 偶数 → 偶数往 +30 偏,奇数往 -30 偏
float deflect = ((Mathf.Abs(sourceUID) % 2 == 0) ? verticalDeflectAngle : -verticalDeflectAngle) * _flag;
float target = curAngle + deflect;
reflected = new Vector2(Mathf.Cos(target * Mathf.Deg2Rad), Mathf.Sin(target * Mathf.Deg2Rad));
_flag = -_flag;
Debug.Log($"[烈焰风暴火球弹] 垂直撞上下墙 → 斜弹 {target:0.0}°(flag={_flag})");
}
}
SetHeading(reflected);
var selfCol = GetComponent<Collider2D>();
float radius = selfCol != null
? Mathf.Max(selfCol.bounds.extents.x, selfCol.bounds.extents.y)
: 0.5f;
float margin = moveSpeed * Time.deltaTime + 0.05f;
Vector2 pushed = closest + normal * (radius + margin);
transform.position = new Vector3(pushed.x, pushed.y, keepZ);
}
/// <summary>设置前进方向:世界 Z 轴旋转叠在基准姿态上</summary>
private void SetHeading(Vector2 heading)
{
_heading = heading.sqrMagnitude < 0.0001f ? Vector2.up : heading.normalized;
float z = Mathf.Atan2(_heading.y, _heading.x) * Mathf.Rad2Deg - 90f;
transform.rotation = Quaternion.Euler(0f, 0f, z) * _baseRot;
}
/// <summary>lua:反弹时 isSelf → ShakeScreen(0.5) + 播 PW_FlameStrike_WheelFly(1 秒内只播一次)</summary>
private void PlayBounceFeedback()
{
if (!bounceFeedback || !isSelf) return;
if (GameManager.Instance != null && GameManager.Instance.cameraShake != null)
GameManager.Instance.cameraShake.ShakePresets.ShortShake2D(
bounceShakeStrength, bounceShakeStrength, 25f, 4);
if (Time.unscaledTime - _lastBounceSoundTime >= bounceSoundInterval)
{
AudioManager.Instance.PlayerFXByName("PW_FlameStrike_WheelFly");
_lastBounceSoundTime = Time.unscaledTime;
}
}
// ================================================================ 击杀上报
/// <summary>
/// 上报命中(与普通子弹 BullitMove.AttackFish 完全同一套协议):
/// 只有击杀者本机才发 KillFish;机器人由房主代发。
/// </summary>
private void ReportKill(MonsterContral fish)
{
if (!reportKill || fish == null || fish.isDead) return;
var gm = GameManager.Instance;
if (gm == null) return;
string selfId = gm.user != null ? gm.user.id : null;
int paoBeiInt = paoBei > 0 ? paoBei : 1;
string fishNumber = fish.monsterData != null && fish.monsterData.fishTypeConfig != null
? fish.monsterData.fishTypeConfig.fishNumber
: null;
bool sent = false;
if (!string.IsNullOrEmpty(killerUserId) && killerUserId == selfId)
{
// 真人玩家击杀:击杀者本机上报(包括火球弹替击杀者继续收鱼)
var room = gm.gameData != null ? gm.gameData.currRoomData : null;
if (room != null)
{
WebSocketSendList.KillFish(room.roomId, killerUserId, fishNumber, fish.guid,
paoBeiInt, bulletId, "63215f0afa3f5b25");
sent = true;
}
}
else if (!string.IsNullOrEmpty(killerUserId) && killerUserId.StartsWith("bot_")
&& SitPosData.roomOwnerSit != null && SitPosData.roomOwnerSit.user == selfId)
{
// 机器人击杀:只有房主客户端代发
var room = gm.gameData != null ? gm.gameData.currRoomData : null;
if (room != null)
{
WebSocketSendList.BotKillFish(room.roomId, killerUserId, fishNumber, fish.guid,
paoBeiInt, bulletId);
sent = true;
}
}
// 注意:命中表现 fish.OnHurt() 由 ProcessFishHit 统一调用,这里不再重复调
if (sent)
Debug.Log($"[烈焰风暴火球弹] 上报击杀 fish={fish.guid} fishNumber={fishNumber} 炮倍={paoBeiInt} 子弹={bulletId}");
}
// ================================================================ 收尾
/// <summary>
/// lua Disappear():先炸一圈小鱼 → PW_FlameStrike_WheelExplosion 音效 + sprite 隐藏 + 切 Explosion 特效
/// → 停留 explosionHoldTime → 回收(默认 Destroy)。
/// ⭐ 收尾触发点只有两处:反弹次数打满 / lifeTime 兜底超时 —— 日志里会带调用栈,方便排查
/// "子弹为什么消失"(碰鱼是**绝不**收尾的)。
/// </summary>
private void Finish()
{
if (_finished) return;
if (debugLog)
Debug.Log($"[烈焰风暴火球弹] 进入收尾:已飞 {_time:0.00}/{lifeTime}s、反弹 {_bounceCount}/{bounceLimit}、" +
$"已杀 {_hitFish.Count} 条、pos={transform.position}\n调用来源:{new System.Diagnostics.StackTrace(true)}");
// ⭐ ① 收尾爆炸先炸一圈小鱼(对齐钻头蟹 SpecialBulletMove.Explode):
// 必须在 _finished 置 true 之前调用 —— ProcessFishHit 开头会检查 _finished
KillFishInExplosionRange();
_finished = true;
_heading = Vector2.zero;
if (isSelf) AudioManager.Instance.PlayerFXByName("PW_FlameStrike_WheelExplosion");
if (_sprite != null) _sprite.gameObject.SetActive(false);
ShowExplosion(true);
// ② 爆炸特效播完后(Release)回收;Release 里会回调 onFinished 交给创建方(Boss 统计)
if (explosionHoldTime <= 0f)
{
Release();
return;
}
_releaseTween = DOVirtual.DelayedCall(explosionHoldTime, Release);
}
// ================================================================ 收尾爆炸范围杀鱼
/// <summary>
/// 收尾爆炸炸一圈小鱼(对齐钻头蟹 SpecialBulletMove.Explode):
/// 以爆炸点为中心 OverlapCircle 扫普通鱼 → 复用 ProcessFishHit(同一条鱼只算一次、累计分值、上报击杀)。
/// ⭐ 必须在 Finish() 里、_finished 置 true 之前调用。
/// </summary>
private void KillFishInExplosionRange()
{
if (!killFishOnFinal || explosionRadius <= 0f) return;
int n = Physics2D.OverlapCircleNonAlloc(transform.position, explosionRadius, _overlapBuf);
int killed = 0;
for (int i = 0; i < n; i++)
{
var c = _overlapBuf[i];
if (c == null) continue;
var fish = c.GetComponentInParent<MonsterContral>();
if (fish == null || fish.isDead) continue;
if (fish.fishenum != FishTypeEnum.普通鱼) continue;
if (_hitFish.Contains(fish.guid)) continue; // 已经打过的跳过(ProcessFishHit 也会拦,这里提前省一点)
ProcessFishHit(fish, explosionSpawnHitEffect);
killed++;
}
if (debugLog)
Debug.Log($"[烈焰风暴火球弹] 收尾爆炸范围杀鱼:半径={explosionRadius} 扫到={n} 新杀={killed}");
}
// ================================================================ 收尾爆奖
/// <summary>
/// 收尾爆奖:在击杀者座位上方生成 BJEndUI 结算面板。
/// score:jackpotScore >= 0 用注入值(建议 Boss 从后端 gold 注入);否则用本发累计击杀分值(die_Score) 兜底。
/// beishu:jackpotBeishu > 0 用之;否则用炮倍 paoBei。
/// isMine:直接用 isSelf(自己击杀=金币样式,他人=银币样式)。
/// ⭐ 面板是独立 GameObject(挂在座位/bjEffectRoot 下,自带 autoDestroyDelay),子弹回收(Destroy)不影响它。
/// </summary>
private void SpawnJackpot()
{
if (bjEndUIPrefab == null)
{
if (debugLog) Debug.Log("[烈焰风暴火球弹] 收尾爆奖跳过:bjEndUIPrefab 未赋值");
return;
}
long score = jackpotScore >= 0 ? jackpotScore : _accumScore;
int beishu = jackpotBeishu > 0 ? jackpotBeishu : (paoBei > 0 ? paoBei : 1);
// 定位:击杀者座位上方(与其它爆奖一致);找不到座位回退到子弹当前位置
Vector3 uiPos = GetKillerSeatPosition() + Vector3.up * bjEndUIOffsetY;
var endUI = Instantiate(bjEndUIPrefab, uiPos, Quaternion.identity, GetKillerSeatParent());
var bj = endUI.GetComponent<BJEndUI>();
if (bj != null)
{
bj.Init(beishu, score, isSelf);
if (debugLog)
Debug.Log($"[烈焰风暴火球弹] 收尾爆奖 BJEndUI:score={score} beishu={beishu} isSelf={isSelf} pos={uiPos}");
}
else
{
Debug.LogWarning("[烈焰风暴火球弹] bjEndUIPrefab 上找不到 BJEndUI 组件");
}
}
/// <summary>击杀者座位世界坐标(找不到回退子弹当前位置)</summary>
private Vector3 GetKillerSeatPosition()
{
var seat = GetKillerSeat();
return seat != null ? seat.position : transform.position;
}
/// <summary>击杀者座位的 Transform(作为 BJEndUI 父物体,与其它爆奖一致);
/// 找不到回退 bjEffectRoot;都没有则 null(挂世界根)</summary>
private Transform GetKillerSeatParent()
{
var seat = GetKillerSeat();
if (seat != null) return seat;
return CreateFishManager.Instance != null ? CreateFishManager.Instance.bjEffectRoot : null;
}
/// <summary>按 killerUserId 在座位列表里找击杀者座位</summary>
private Transform GetKillerSeat()
{
if (string.IsNullOrEmpty(killerUserId)) return null;
var mgr = CreateFishManager.Instance;
if (mgr == null || mgr.currGameType == null || mgr.currGameType.list_Sits == null) return null;
var sit = mgr.currGameType.list_Sits.Find(s => s.user == killerUserId);
return sit != null ? sit.transform : null;
}
/// <summary>回收:默认自管理(Destroy),不还给 BulletPool</summary>
private void Release()
{
_releaseTween = null;
if (this == null) return;
// ⭐ 先通知创建方(Boss 靠这个统计"所有火球弹是否都已收尾"→ 全部结束才统一出**一次**爆奖面板)
onFinished?.Invoke();
onFinished = null;
// 兜底:没接回调、且显式打开开关时,才由本发子弹自己出面板(默认关,避免 4 发叠 4 个面板)
if (spawnJackpotOnFinish) SpawnJackpot();
// ⭐ 默认直接销毁,不走 BulletPool:火球弹不属于任何座位(userid=null),
// 一旦进了池的"活跃子弹"表,换炮台时的 BulletPool.ClearActive() 会把正在飞的
// 火球弹一并 Destroy —— 那正是"子弹凭空消失"的经典来源。自管理后与池彻底解耦。
if (selfManaged || BulletPool.Instance == null)
Destroy(gameObject);
else
BulletPool.Instance.Return(gameObject);
}
private void KillReleaseTween()
{
if (_releaseTween != null && _releaseTween.IsActive()) _releaseTween.Kill();
_releaseTween = null;
}
// ================================================================ 节点
private void ResolveNodes()
{
_sprite = FindDeep(transform, spriteNodeName);
_glow = FindDeepGo(transform, glowNodeName);
_explosion = FindDeepGo(transform, explosionNodeName);
if (_sprite == null)
Debug.LogWarning($"[烈焰风暴火球弹] 找不到 sprite 节点「{spriteNodeName}」,自转跳过");
if (_glow == null && _explosion == null)
Debug.LogWarning("[烈焰风暴火球弹] 找不到 Glow/Explosion 节点,收尾特效切换跳过");
}
/// <summary>飞行中:Glow 亮、Explosion 灭;收尾:反过来</summary>
private void ShowExplosion(bool exploded)
{
if (_glow != null) _glow.SetActive(!exploded);
if (_explosion != null) _explosion.SetActive(exploded);
}
private static Transform FindDeep(Transform root, string name)
{
if (root == null || string.IsNullOrEmpty(name)) return null;
if (string.Equals(root.name, name, StringComparison.OrdinalIgnoreCase)) return root;
for (int i = 0; i < root.childCount; i++)
{
var r = FindDeep(root.GetChild(i), name);
if (r != null) return r;
}
return null;
}
private static GameObject FindDeepGo(Transform root, string name)
{
var t = FindDeep(root, name);
return t != null ? t.gameObject : null;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c9eb962ba40af747b61e31a7adce76e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -6,13 +6,18 @@ using UnityEngine;
/// 烈焰风暴 Boss 爆奖事件(精简版)。
///
/// 无死鱼、无红包、无滚分,流程只有:
/// 炸一下(播放爆炸特效,不死任何鱼)→ 等特效播完 → 显示 bjEndUI + 回加金币 → 回收。
/// 死亡/未死亡由 Boss 解析服务端 param 决定,Event 不关心。
/// 炸一下(播放爆炸特效,不死任何鱼)→ [等火球弹打完] → 显示 bjEndUI + 回加金币 → 回收。
///
/// ⭐ 两种结算时机(deferSettle):
/// · false(默认):爆炸播完**立刻** JiaFen + 出面板 —— 未死亡 / 被免费子弹打死(不发火球弹)走这条。
/// · true:只播爆炸、不回池也不结算,等 Boss 侧所有火球弹都收尾爆炸完成后调
/// <see cref="SettleNow"/> —— 对齐钻头蟹 ZuanTouXie_DieEvent 的 "钻头跑完 → ShowEndUI"。
/// 死亡 + 会发火球弹时由 <see cref="LieYanFengBao_BOSS.InitAwardEffect"/> 打开。
/// </summary>
public class LieYanFengBaoEvent : BJRedPacketEvent
{
[Header("爆炸配置")]
[Tooltip("爆炸特效播放时长(播完后显示 bjEndUI)")]
[Tooltip("爆炸特效播放时长(非延迟结算时,播完后回池)")]
public float explosionDuration = 2f;
/// <summary>爆炸中心(世界坐标,由 Boss 传入=死亡位置)</summary>
@@ -23,11 +28,21 @@ public class LieYanFengBaoEvent : BJRedPacketEvent
[System.NonSerialized]
public LieYanFengBao_BOSS bossCtrl;
/// <summary>
/// ⭐ 延迟结算开关:true 时不在爆炸后立刻结算,等 Boss 调 <see cref="SettleNow"/>。
/// ⚠ 由 Boss 在 <c>InitAwardEffect</c> 里**显式**赋值(true/false 都赋),避免池化复用时残留上一轮的值。
/// </summary>
[System.NonSerialized]
public bool deferSettle = false;
private bool _settled; // 已结算(防重入)
public override void Init(long goldCount, int posID, long paobei, long fishScore, int beishu,
Action<int> action, Action action1 = null)
{
// 不需要分数面板/滚分/红包,纯特效 + bjEndUI
useScorePanel = false;
_settled = false;
base.Init(goldCount, posID, paobei, fishScore, beishu, action, action1);
// ApplySeatOffset 已重写为空;恢复到死亡位置
transform.position = explosionCenter;
@@ -40,7 +55,7 @@ public class LieYanFengBaoEvent : BJRedPacketEvent
GetSeatPosition();
}
/// <summary>炸一下 + 立即显示 bjEndUI;爆炸特效后台自己播,播完后静默回池</summary>
/// <summary>炸一下;延迟结算时停在"已爆炸、等通知",否则直接结算</summary>
private IEnumerator PlayExplosionThenEnd()
{
// 音效:爆炸(PW_Explosion)
@@ -49,11 +64,31 @@ public class LieYanFengBaoEvent : BJRedPacketEvent
// 重播粒子/动画(对象池复用后需手动重播)
ReplayExplosionEffect();
// ===== 无延迟立即执行 =====
// 回加金币
if (deferSettle)
{
// ⭐ 火球弹还在飞:不结算、不回池(回池会 SetActive(false),爆炸就看不到了),
// 由 Boss 在"所有火球弹收尾爆炸完成"后调 SettleNow()
Debug.Log("[LieYanFengBaoEvent] 爆炸已播放,等待火球弹全部收尾后再结算最终面板");
yield break;
}
// 未延迟(未死亡 / 免费子弹):爆炸即结算
SettleNow();
}
/// <summary>
/// 结算:回加金币 → 显示 bjEndUI(座位上方)→ 通知 Boss → 停留后回池。
/// 由 <see cref="PlayExplosionThenEnd"/>(立即)或 Boss(等火球弹打完)调用,内部防重入。
/// </summary>
public void SettleNow()
{
if (_settled) return;
_settled = true;
// ===== 回加金币 =====
JiaFen?.Invoke();
// 显示 bjEndUI(座位上方)
// ===== 显示 bjEndUI(座位上方)=====
if (bjEndUI != null)
{
float y = bjEndUIOffsetY;
@@ -61,18 +96,29 @@ public class LieYanFengBaoEvent : BJRedPacketEvent
Vector3 uiPos = GetSeatPosition() + Vector3.up * y;
var endUI = Instantiate(bjEndUI, uiPos, Quaternion.identity, GetBJEndUIParent());
endUI.Init(bJbeishu, maxScore, isSelf);
Debug.Log($"[LieYanFengBaoEvent] 最终爆奖面板已显示:倍率={bJbeishu} 分数={maxScore} isSelf={isSelf} pos={uiPos}");
}
else
{
Debug.LogWarning("[LieYanFengBaoEvent] bjEndUI 未赋值,最终爆奖面板跳过");
}
// 通知 Boss 爆奖结束(非死亡时恢复游动)
// ===== 通知 Boss 爆奖结束(非死亡时恢复游动;此时 Boss 通常已销毁,假 null 会被拦掉)=====
if (bossCtrl != null)
{
bossCtrl.OnAwardFinished();
bossCtrl = null;
}
// 爆炸特效播完后才回池(Return 会 SetActive(false),立即回池爆炸就看不到了)
yield return new WaitForSeconds(explosionDuration);
EffectPool.Instance.Return(gameObject);
// 延迟结算时爆炸早就播完了,这里只留一小段给面板淡入,然后回池
StartCoroutine(ReturnAfterDelay(explosionDuration));
}
/// <summary>停留 delay 秒后回池(Return 会 SetActive(false),所以必须等表现播完)</summary>
private IEnumerator ReturnAfterDelay(float delay)
{
if (delay > 0f) yield return new WaitForSeconds(delay);
if (EffectPool.Instance != null) EffectPool.Instance.Return(gameObject);
}
/// <summary>重播爆炸特效(粒子 + Animator)</summary>
@@ -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() : "-")})");
// 先借它自己那套初始化:对齐 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;
}
else
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)
{
Debug.LogWarning($"[LieYanFengBao] 火球弹预制体上没有 BullitMove,无法控制飞行");
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
{
@@ -152,6 +152,7 @@ MonoBehaviour:
createVideoClip: {fileID: 0}
rotationPY: 60
useMirrorCompensate: 1
useRollSignFlip: 0
useRouteFlipX: 1
baseMmove: 0
isRotate: 1
@@ -160,6 +161,9 @@ MonoBehaviour:
depthOffset: 0
is2DFish: 1
sortOrder2D: 221
drawMoveLine: 0
drawLineSegments: 60
drawLineColor: {r: 1, g: 0.92156863, b: 0.015686275, a: 1}
bossName: "\u70C8\u7130\u98CE\u66B4"
isToUI: 1
tMP_CurrBossScore: {fileID: 0}
@@ -177,8 +181,20 @@ MonoBehaviour:
flameRoundBulletPrefab: {fileID: 2628375401523424628, guid: 9ed49cd2acc114b4d839b06e3766ccaf,
type: 3}
flameRoundSpeed: 30
flameRoundLifeTime: 4
flameRoundLifeTime: 8
flameRoundFallbackCount: 4
flameRoundBounceLimit: 4
flameRoundBounceShake: 0.5
flameRoundHitEffectPrefab: {fileID: 1307567150641513, guid: bf68320e2b1dec44ca6285fee1fc48f3,
type: 3}
flameRoundExplosionRadius: 3
flameRoundKillFishOnFinal: 1
flameRoundBJEndUIPrefab: {fileID: 0}
flameRoundJackpotScore: -1
flameRoundJackpotBeishu: 0
flameRoundDeferSettle: 1
flameRoundDebugLog: 1
flameRoundUseLocalInstantKill: 1
explosionToRewardDelay: 0.3
rewardToResetDelay: 0.8
dieToFlameRoundDelay: 1.8
@@ -11,7 +11,7 @@ GameObject:
- component: {fileID: 2623929118217137342}
m_Layer: 7
m_Name: Explosion
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -43,10 +43,9 @@ GameObject:
- component: {fileID: 2627120296159640219}
- component: {fileID: 2639909595211502100}
- component: {fileID: 2650863340803371353}
- component: {fileID: 3916674750202535009}
m_Layer: 7
m_Name: PW_Bullet_FlameRound
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -61,8 +60,8 @@ Transform:
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.9, y: 0.9, z: 0.9}
m_ConstrainProportionsScale: 0
m_LocalScale: {x: 2, y: 2, z: 2}
m_ConstrainProportionsScale: 1
m_Children:
- {fileID: 2623919183334045106}
- {fileID: 2627003230944060665}
@@ -142,31 +141,6 @@ Rigidbody2D:
m_SleepingMode: 1
m_CollisionDetection: 0
m_Constraints: 0
--- !u!114 &3916674750202535009
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2628375401523424628}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59323a063846feb4cb2ab243e7887554, type: 3}
m_Name:
m_EditorClassIdentifier:
userid:
moveSpeed: 100
desTime: 10
playerID:
effectPrefab: {fileID: 0}
diePrefab: {fileID: 0}
isTriggerFirst: 1
targetTra: {fileID: 0}
bulletID:
maxTurnSpeed: 15
minTurnSpeed: 2
distanceForMaxTurn: 1
sortOrder: 500
--- !u!1 &2628988624152764319
GameObject:
m_ObjectHideFlags: 0
@@ -180,7 +154,7 @@ GameObject:
- component: {fileID: 2792228970830937200}
m_Layer: 7
m_Name: Glow03
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -2538,7 +2512,7 @@ GameObject:
- component: {fileID: 2791901242524847643}
m_Layer: 7
m_Name: FJ_path_07
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -4882,7 +4856,7 @@ GameObject:
- component: {fileID: 2791801619860721909}
m_Layer: 7
m_Name: FJ_glow_20
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -7226,7 +7200,7 @@ GameObject:
- component: {fileID: 2790416861880907065}
m_Layer: 7
m_Name: Glow04
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -9584,7 +9558,7 @@ GameObject:
- component: {fileID: 2791899652305851451}
m_Layer: 7
m_Name: sm
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -12014,7 +11988,7 @@ GameObject:
- component: {fileID: 2791950503187219054}
m_Layer: 7
m_Name: FJ_glow_18
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -14358,7 +14332,7 @@ GameObject:
- component: {fileID: 2790348422175808980}
m_Layer: 7
m_Name: FJ_glow_05
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -16712,7 +16686,7 @@ GameObject:
- component: {fileID: 2792164536762776403}
m_Layer: 7
m_Name: FJ_glow_21
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -19056,7 +19030,7 @@ GameObject:
- component: {fileID: 2792060487723656805}
m_Layer: 7
m_Name: FJ_glow_07_a
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -21418,7 +21392,7 @@ GameObject:
- component: {fileID: 2791836392543598837}
m_Layer: 7
m_Name: Fire
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -23848,7 +23822,7 @@ GameObject:
- component: {fileID: 2791873330538028299}
m_Layer: 7
m_Name: Glow
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -26210,7 +26184,7 @@ GameObject:
- component: {fileID: 2778286923791984627}
m_Layer: 7
m_Name: sprite
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -26294,7 +26268,7 @@ GameObject:
- component: {fileID: 2675825424464300882}
m_Layer: 7
m_Name: Trail
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -26440,7 +26414,7 @@ GameObject:
- component: {fileID: 2791818756796623345}
m_Layer: 7
m_Name: FJ_fangsheguang_01
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -28784,7 +28758,7 @@ GameObject:
- component: {fileID: 2790340097591223507}
m_Layer: 7
m_Name: Dot
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -31214,7 +31188,7 @@ GameObject:
- component: {fileID: 2792210948632500358}
m_Layer: 7
m_Name: FJ_xulie_11
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -33540,7 +33514,7 @@ GameObject:
- component: {fileID: 2791691693945874286}
m_Layer: 7
m_Name: FJ_yanhuo_02
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -35884,7 +35858,7 @@ GameObject:
- component: {fileID: 2792150393867024466}
m_Layer: 7
m_Name: FJ_fangsheguang_02
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -38228,7 +38202,7 @@ GameObject:
- component: {fileID: 2791889907625872779}
m_Layer: 7
m_Name: FJ_lizi_01
m_TagString: Untagged
m_TagString: Bullet
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
@@ -20,7 +20,7 @@ Texture2D:
m_MipsStripped: 0
m_TextureFormat: 1
m_MipCount: 1
m_IsReadable: 1
m_IsReadable: 0
m_IsPreProcessed: 0
m_IgnoreMipmapLimit: 0
m_MipmapLimitGroupName:
@@ -68,7 +68,7 @@ Texture2D:
m_MipsStripped: 0
m_TextureFormat: 1
m_MipCount: 1
m_IsReadable: 1
m_IsReadable: 0
m_IsPreProcessed: 0
m_IgnoreMipmapLimit: 0
m_MipmapLimitGroupName:
@@ -115,8 +115,8 @@ MonoBehaviour:
m_SourceFontFileGUID: fd74a459621d04a4aab7bfba702e212f
m_SourceFontFile_EditorRef: {fileID: 12800000, guid: fd74a459621d04a4aab7bfba702e212f,
type: 3}
m_SourceFontFile: {fileID: 12800000, guid: fd74a459621d04a4aab7bfba702e212f, type: 3}
m_AtlasPopulationMode: 1
m_SourceFontFile: {fileID: 0}
m_AtlasPopulationMode: 0
m_FaceInfo:
m_FaceIndex: 0
m_FamilyName: Source Han Sans
@@ -1670,6 +1670,36 @@ MonoBehaviour:
m_Scale: 1
m_AtlasIndex: 1
m_ClassDefinitionType: 0
- m_Index: 1417
m_Metrics:
m_Width: 8.359375
m_Height: 26.671875
m_HorizontalBearingX: 19.484375
m_HorizontalBearingY: 24.359375
m_HorizontalAdvance: 29
m_GlyphRect:
m_X: 310
m_Y: 1488
m_Width: 9
m_Height: 28
m_Scale: 1
m_AtlasIndex: 1
m_ClassDefinitionType: 0
- m_Index: 1418
m_Metrics:
m_Width: 8.359375
m_Height: 26.671875
m_HorizontalBearingX: 1.15625
m_HorizontalBearingY: 24.359375
m_HorizontalAdvance: 29
m_GlyphRect:
m_X: 310
m_Y: 1523
m_Width: 9
m_Height: 28
m_Scale: 1
m_AtlasIndex: 1
m_ClassDefinitionType: 0
- m_Index: 1421
m_Metrics:
m_Width: 6.25
@@ -106610,36 +106640,6 @@ MonoBehaviour:
m_Scale: 1
m_AtlasIndex: 0
m_ClassDefinitionType: 0
- m_Index: 1417
m_Metrics:
m_Width: 8.359375
m_Height: 26.671875
m_HorizontalBearingX: 19.484375
m_HorizontalBearingY: 24.359375
m_HorizontalAdvance: 29
m_GlyphRect:
m_X: 310
m_Y: 1488
m_Width: 9
m_Height: 28
m_Scale: 1
m_AtlasIndex: 1
m_ClassDefinitionType: 0
- m_Index: 1418
m_Metrics:
m_Width: 8.359375
m_Height: 26.671875
m_HorizontalBearingX: 1.15625
m_HorizontalBearingY: 24.359375
m_HorizontalAdvance: 29
m_GlyphRect:
m_X: 310
m_Y: 1523
m_Width: 9
m_Height: 28
m_Scale: 1
m_AtlasIndex: 1
m_ClassDefinitionType: 0
m_CharacterTable:
- m_ElementType: 1
m_Unicode: 32
@@ -107053,6 +107053,14 @@ MonoBehaviour:
m_Unicode: 12299
m_GlyphIndex: 1412
m_Scale: 1
- m_ElementType: 1
m_Unicode: 12304
m_GlyphIndex: 1417
m_Scale: 1
- m_ElementType: 1
m_Unicode: 12305
m_GlyphIndex: 1418
m_Scale: 1
- m_ElementType: 1
m_Unicode: 12308
m_GlyphIndex: 1421
@@ -135037,14 +135045,6 @@ MonoBehaviour:
m_Unicode: 65509
m_GlyphIndex: 59218
m_Scale: 1
- m_ElementType: 1
m_Unicode: 12304
m_GlyphIndex: 1417
m_Scale: 1
- m_ElementType: 1
m_Unicode: 12305
m_GlyphIndex: 1418
m_Scale: 1
m_AtlasTextures:
- {fileID: -5242640295693195929}
- {fileID: -4985352658726325108}