1866 lines
79 KiB
C#
1866 lines
79 KiB
C#
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
|
||
using DG.Tweening;
|
||
using Newtonsoft.Json.Linq;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 星际巡航 Boss 爆奖事件(继承 BJEventBase)。
|
||
///
|
||
/// ==================== 美术规格(4种形态) ====================
|
||
///
|
||
/// 形态一(富甲天下,无道具武器):
|
||
/// Settle: PW_Effect_Settle_FuJiaTianXia (Node子物体指认Source Image为tj_fcboss_0)
|
||
///
|
||
/// 形态二(铁拳 IronFist, weaponID=6):
|
||
/// 1. FullScreen(PW_Effect_FullScreen_Symmetra): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_IronFist) → 隐藏Tip
|
||
/// 2. Prop(PW_Prop_IronFist): 激活Hole子对象 → Hole从死亡位置移动到屏幕左下角 → 隐藏Hole → 激活Hand
|
||
/// 3. TextCrit: PW_Effect_TextCrit_Other / PW_Effect_TextCrit_Self (满屏暴击)
|
||
/// 4. AwardFrame: PW_Effect_AwardFrame_IronFist
|
||
/// 5. Settle: PW_Effect_Settle_IronFist_Other
|
||
///
|
||
/// 形态三(光子 Symmetra, weaponID=8):
|
||
/// 1. FullScreen(PW_Effect_FullScreen_Symmetra): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_Symmetra) → 隐藏Tip
|
||
/// 2. Prop(PW_Prop_Symmetra): 从死亡位置移动到屏幕中心 → 隐藏Lock → PropL和PropR分别移动到屏幕两边 → 激活laser子物体
|
||
/// 3. AwardFrame: PW_Effect_AwardFrame_Symmetra
|
||
/// 4. Settle: PW_Effect_Settle_Symmetra_Other / PW_Effect_Settle_Symmetra_Self
|
||
///
|
||
/// 形态四(伽马风暴 GammaStorm, weaponID=7):
|
||
/// 1. FullScreen(PW_Effect_FullScreen_GammaStorm): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_GammaStorm) → 隐藏Tip
|
||
/// 2. Prop(PW_Prop_GammaStorm): 从死亡位置移动到屏幕中心 → 根据视频内容激活特效
|
||
/// 3. TextCrit: PW_Effect_TextCrit_Other / PW_Effect_TextCrit_Self (满屏暴击)
|
||
/// 4. Crit: PW_Effect_Crit_GammaStorm (暴击倍率)
|
||
/// 5. AwardFrame: PW_Effect_AwardFrame_GammaStorm
|
||
/// 6. Settle: PW_Effect_Settle_GammaStorm_Other
|
||
///
|
||
/// ⭐ 注意: 铁拳(形态二)的全屏 UI 框按美术规格用 PW_Effect_FullScreen_Symmetra,
|
||
/// 海报(Source Image) 仍设置 Prop_Poster_IronFist。
|
||
///
|
||
/// ==================== 完整流程 ====================
|
||
///
|
||
/// Init → 解析 weaponID → 道具出场 → 全屏特效(tap_tip/knock_tip序列) → 道具动画
|
||
/// → 奖励框 → CallBJ发红包 → 暴击文字 → 结算特效 → EffEnd
|
||
/// → DelayThenScore → StartAddScore → MoveToSit → OnScoreEnd → JiaFen → die
|
||
/// </summary>
|
||
public class XingJiXunHangEvent : BJRedPacketEvent
|
||
{
|
||
// ==================== 形态一:富甲天下(无道具武器) ====================
|
||
|
||
[Header("形态一 - 富甲天下")]
|
||
[Tooltip("富甲天下结算特效(PW_Effect_Settle_FuJiaTianXia_XingJiXunHang)")]
|
||
public GameObject settleFuJiaTianXia;
|
||
|
||
// ==================== 形态二:铁拳 (weaponID=6) ====================
|
||
|
||
[Header("形态二 - 铁拳 IronFist")]
|
||
[Tooltip("铁拳道具预制体(PW_Prop_IronFist Variant)")]
|
||
public GameObject propIronFist;
|
||
[Tooltip("铁拳全屏特效(PW_Effect_FullScreen_IronFist_XingJiXunHang)")]
|
||
public GameObject fullScreenIronFist;
|
||
[Tooltip("铁拳奖励框(PW_Effect_AwardFrame_IronFist_XingJiXunHang)")]
|
||
public GameObject awardFrameIronFist;
|
||
[Tooltip("铁拳结算特效-自己(PW_Effect_Settle_IronFist_Self_XingJiXunHang)")]
|
||
public GameObject settleIronFistSelf;
|
||
[Tooltip("铁拳结算特效-他人(PW_Effect_Settle_IronFist_Other_XingJiXunHang)")]
|
||
public GameObject settleIronFistOther;
|
||
[Tooltip("铁拳海报Sprite(Prop_Poster_IronFist),设置到FullScreen的knock_tip上")]
|
||
public Sprite posterIronFist;
|
||
|
||
// ==================== 形态三:光子 (weaponID=8) ====================
|
||
|
||
[Header("形态三 - 光子 Symmetra")]
|
||
[Tooltip("光子道具预制体(PW_Prop_Symmetra Variant)")]
|
||
public GameObject propSymmetra;
|
||
[Tooltip("光子全屏特效(PW_Effect_FullScreen_Symmetra_XingJiXunHang)")]
|
||
public GameObject fullScreenSymmetra;
|
||
[Tooltip("光子奖励框(PW_Effect_AwardFrame_Symmetra_XingJiXunHang)")]
|
||
public GameObject awardFrameSymmetra;
|
||
[Tooltip("光子结算特效-自己(PW_Effect_Settle_Symmetra_Self_XingJiXunHang)")]
|
||
public GameObject settleSymmetraSelf;
|
||
[Tooltip("光子结算特效-他人(PW_Effect_Settle_Symmetra_Other_XingJiXunHang)")]
|
||
public GameObject settleSymmetraOther;
|
||
[Tooltip("光子海报Sprite(Prop_Poster_Symmetra),设置到FullScreen的knock_tip上")]
|
||
public Sprite posterSymmetra;
|
||
|
||
// ==================== 形态四:伽马风暴 (weaponID=7) ====================
|
||
|
||
[Header("形态四 - 伽马风暴 GammaStorm")]
|
||
[Tooltip("伽马风暴道具预制体(PW_Prop_GammaStorm Variant)")]
|
||
public GameObject propGammaStorm;
|
||
[Tooltip("伽马风暴全屏特效(PW_Effect_FullScreen_GammaStorm_XingJiXunHang)")]
|
||
public GameObject fullScreenGammaStorm;
|
||
[Tooltip("伽马风暴奖励框(PW_Effect_AwardFrame_GammaStorm_XingJiXunHang)")]
|
||
public GameObject awardFrameGammaStorm;
|
||
[Tooltip("伽马风暴结算特效-自己(PW_Effect_Settle_GammaStorm_Self_XingJiXunHang)")]
|
||
public GameObject settleGammaStormSelf;
|
||
[Tooltip("伽马风暴结算特效-他人(PW_Effect_Settle_GammaStorm_Other_XingJiXunHang)")]
|
||
public GameObject settleGammaStormOther;
|
||
[Tooltip("伽马风暴暴击框(PW_Effect_Crit_GammaStorm_XingJiXunHang)")]
|
||
public GameObject critFrameGammaStorm;
|
||
[Tooltip("伽马风暴海报Sprite(Prop_Poster_GammaStorm),设置到FullScreen的knock_tip上")]
|
||
public Sprite posterGammaStorm;
|
||
|
||
// ==================== 满屏暴击文字(铁拳/伽马风暴共用) ====================
|
||
|
||
[Header("满屏暴击文字")]
|
||
[Tooltip("满屏暴击-自己(PW_Effect_TextCrit_Self_XingJiXunHang)")]
|
||
public GameObject textCritSelf;
|
||
[Tooltip("满屏暴击-他人(PW_Effect_TextCrit_Other_XingJiXunHang)")]
|
||
public GameObject textCritOther;
|
||
[Tooltip("暴击分数 TMP 所在子物体名(预制体里数字节点,默认 Text)")]
|
||
public string critTextNodeName = "Text";
|
||
[Tooltip("暴击图子物体名(预制体里\"暴击\"图,从 0 延时弹出,默认 Crit)")]
|
||
public string critNodeName = "Crit";
|
||
[Tooltip("暴击文字弹出后停留时长(秒),之后整体缩小消失(默认0.8)")]
|
||
public float critHoldDuration = 0.8f;
|
||
|
||
// ==================== 道具出场特效 ====================
|
||
|
||
[Header("道具出场")]
|
||
[Tooltip("道具出场特效(三种武器共用,PW_Effect_Appear_Prop)")]
|
||
public GameObject appearPropEffect;
|
||
|
||
// ==================== FullScreen 子物体名称配置 ====================
|
||
|
||
[Header("FullScreen 子物体名称")]
|
||
[Tooltip("tap_tip 子物体名(点击提示)")]
|
||
public string tapTipName = "tap_tip";
|
||
[Tooltip("knock_tip 子物体名(敲击提示,需设置海报Sprite)")]
|
||
public string knockTipName = "knock_tip";
|
||
[Tooltip("Tip 容器子物体名(tap_tip/knock_tip 的父级,序列结束后隐藏,所有形态通用)")]
|
||
public string tipContainerName = "Tip";
|
||
|
||
// ==================== Prop 子物体名称配置 ====================
|
||
|
||
[Header("铁拳 Prop 子物体名称")]
|
||
[Tooltip("铁拳-Hole子物体名(从死亡位置移到屏幕左下角)")]
|
||
public string ironFistHoleName = "Hole";
|
||
[Tooltip("铁拳-Hand子物体名(Hole隐藏后激活)")]
|
||
public string ironFistHandName = "Hand";
|
||
[Tooltip("铁拳-Hole终点偏移(世界单位,往屏幕内偏移,如 (1.5, 1, 0) 从左下角往右上挪一点)")]
|
||
public Vector3 ironFistCornerOffset = new Vector3(1.5f, 1f, 0f);
|
||
|
||
[Tooltip("铁拳-Hole 到位后停留时长(秒),让玩家看清再释放 Hand(默认0.5)")]
|
||
public float ironFistHoldDelay = 0.5f;
|
||
|
||
[Header("铁拳 - 击打杀鱼")]
|
||
[Tooltip("铁拳杀鱼检测半径(世界单位,Hand 击打范围)")]
|
||
public float ironFistKillRadius = 2.5f;
|
||
[Tooltip("铁拳杀鱼检测层(Fish)")]
|
||
public LayerMask ironFistKillLayer;
|
||
[Tooltip("铁拳是否杀鱼(被击中的飞机死亡,金币从左下角飞向炮台)")]
|
||
public bool ironFistKillFish = true;
|
||
[Tooltip("从近到远逐个杀鱼的总时长(秒,默认1秒)")]
|
||
public float ironFistKillTotalTime = 1f;
|
||
[Tooltip("Hand 激活后延迟(秒,默认2),延迟结束隐藏 Hand 并开始死飞机/爆金币")]
|
||
public float ironFistAwardDelay = 2f;
|
||
|
||
[Header("光子 Prop 子物体名称")]
|
||
[Tooltip("光子-Lock子物体名(到屏幕中心后隐藏)")]
|
||
public string symmetraLockName = "Lock";
|
||
[Tooltip("光子-PropL子物体名(移到屏幕左边)")]
|
||
public string symmetraPropLName = "PropL";
|
||
[Tooltip("光子-PropR子物体名(移到屏幕右边)")]
|
||
public string symmetraPropRName = "PropR";
|
||
[Tooltip("光子-laser子物体名(PropL/PropR到位后激活)")]
|
||
public string symmetraLaserName = "laser";
|
||
|
||
[Tooltip("光子-laser 激活后上下摆动幅度(世界单位)")]
|
||
public float laserSwingAmplitude = 0.5f;
|
||
|
||
[Tooltip("光子-laser 激活后上下摆动来回次数(0=不摆动)")]
|
||
public int laserSwingCycles = 0;
|
||
|
||
[Header("光子 - 4条激光伸展")]
|
||
[Tooltip("laser1 子物体名(向 X 正方向伸到屏幕最右)")]
|
||
public string laser1Name = "laser1";
|
||
[Tooltip("laser2 子物体名(向 -Y 方向伸到屏幕最下)")]
|
||
public string laser2Name = "laser2";
|
||
[Tooltip("laser3 子物体名(向 +Y 方向伸到屏幕最上)")]
|
||
public string laser3Name = "laser3";
|
||
[Tooltip("laser4 子物体名(向 -X 方向伸到屏幕最左)")]
|
||
public string laser4Name = "laser4";
|
||
[Tooltip("4条激光伸展到最长位置的耗时(秒)")]
|
||
public float laserExpandSpeed = 0.6f;
|
||
|
||
[Tooltip("光子激光结束后等待时长(秒),然后直接隐藏 Prop(默认1)")]
|
||
public float symmetraPropHideDelay = 1f;
|
||
|
||
[Header("光子 - 电网扫动杀鱼")]
|
||
[Tooltip("电网(Prop整体)上下扫动幅度(世界单位)")]
|
||
public float netSweepAmplitude = 5f;
|
||
|
||
[Tooltip("电网上下扫动来回次数(0=不扫动)")]
|
||
public int netSweepCycles = 3;
|
||
|
||
[Tooltip("电网每次扫动耗时(秒)")]
|
||
public float netSweepSpeed = 0.8f;
|
||
|
||
[Tooltip("电网杀鱼检测半径(世界单位,电网线两侧厚度)")]
|
||
public float netKillRadius = 1f;
|
||
|
||
[Tooltip("电网杀鱼检测层(Fish,Inspector 选 Fish 层)")]
|
||
public LayerMask netKillLayer;
|
||
|
||
[Tooltip("电网是否杀鱼(碰到飞机飞机爆炸)")]
|
||
public bool netKillFish = true;
|
||
|
||
[Header("伽马风暴 - 四射线旋转")]
|
||
[Tooltip("四根射线子物体名(伽马风暴激光,按顺序填;预制体内为小写 laser1~4)")]
|
||
public string[] gammaLaserNames = { "laser1", "laser2", "laser3", "laser4" };
|
||
[Tooltip("射线显示后到开始旋转的等待(秒,默认1)")]
|
||
public float gammaRotateDelay = 1f;
|
||
[Tooltip("总旋转角度(度,默认90°,转完为止)")]
|
||
public float gammaRotateAngle = 90f;
|
||
[Tooltip("转到该角度后激光开始一点一点缩短(度,默认45°)")]
|
||
public float gammaShrinkStartAngle = 45f;
|
||
[Tooltip("旋转速度(度/秒,正=逆时针)")]
|
||
public float gammaRotateSpeed = 45f;
|
||
[Tooltip("伽马风暴射线杀鱼检测层(Fish)")]
|
||
public LayerMask gammaKillLayer;
|
||
[Tooltip("伽马风暴射线是否杀鱼(碰到激光的飞机直接死亡)")]
|
||
public bool gammaKillFish = true;
|
||
|
||
// ==================== 爆奖配置 ====================
|
||
|
||
[Header("爆奖配置")]
|
||
[Tooltip("爆奖红包次数(CallBJ 调用次数)")]
|
||
public int bjCount = 1;
|
||
[Tooltip("每次 CallBJ 最小红包数")]
|
||
public int bjMinCount = 10;
|
||
[Tooltip("每次 CallBJ 最大红包数")]
|
||
public int bjMaxCount = 15;
|
||
|
||
[Header("时间配置")]
|
||
[Tooltip("道具出场特效持续时间")]
|
||
public float appearDuration = 1f;
|
||
[Tooltip("tap_tip 显示持续时间")]
|
||
public float tapTipDuration = 0.5f;
|
||
[Tooltip("knock_tip 显示持续时间")]
|
||
public float knockTipDuration = 0.8f;
|
||
[Tooltip("全屏特效总持续时间")]
|
||
public float fullScreenDuration = 2f;
|
||
[Tooltip("道具动画持续时间(Hole移动/Prop移动等)")]
|
||
public float propAnimDuration = 1.5f;
|
||
[Tooltip("结算特效持续时间")]
|
||
public float settleDuration = 3f;
|
||
|
||
// ==================== 运行时状态 ====================
|
||
|
||
/// <summary>Boss 引用(爆奖结束时通知 Boss 回收)</summary>
|
||
[System.NonSerialized]
|
||
public XingJiXunHang_BOSS bossCtrl;
|
||
|
||
/// <summary>Boss 死亡位置(世界坐标,由 Boss 传入)</summary>
|
||
[HideInInspector]
|
||
public Vector3 diePos = Vector3.zero;
|
||
|
||
/// <summary>服务端返回的武器 ID(6=IronFist, 7=GammaStorm, 8=Symmetra, 0=富甲天下)</summary>
|
||
private int _weaponId = 0;
|
||
|
||
/// <summary>武器标签(GammaStorm / IronFist / Symmetra / FuJiaTianXia)</summary>
|
||
private string _weaponTag = "";
|
||
|
||
/// <summary>当前选中的结算预制体</summary>
|
||
private GameObject _settlePrefab;
|
||
|
||
/// <summary>当前选中的奖励框预制体</summary>
|
||
private GameObject _awardFramePrefab;
|
||
|
||
/// <summary>当前选中的全屏特效预制体</summary>
|
||
private GameObject _fullScreenPrefab;
|
||
|
||
/// <summary>当前选中的道具预制体</summary>
|
||
private GameObject _propPrefab;
|
||
|
||
/// <summary>当前选中的海报Sprite</summary>
|
||
private Sprite _posterSprite;
|
||
|
||
/// <summary>本局电网已炸的鱼 guid(防止扫动中重复炸同一鱼)</summary>
|
||
private readonly HashSet<string> _netKilledFish = new HashSet<string>();
|
||
|
||
/// <summary>Boss 分数面板 TMP(挂在独立 UI 层,Boss 删除后仍有效)</summary>
|
||
private TMPro.TextMeshProUGUI _bossScoreTmp;
|
||
|
||
/// <summary>本局杀鱼累计加到 Boss 面板的分值</summary>
|
||
private long _bossPanelScore = 0;
|
||
|
||
/// <summary>爆奖过程中实例化的所有临时特效(die() 时统一销毁,防止残留)</summary>
|
||
private readonly System.Collections.Generic.List<GameObject> _spawnedEffects = new System.Collections.Generic.List<GameObject>();
|
||
|
||
/// <summary>UI 层父节点(UI 类特效:FullScreen/AwardFrame/Settle/Crit 挂这里)</summary>
|
||
private Transform _uiLayer;
|
||
|
||
/// <summary>UI Canvas(世界坐标 → UI 坐标转换用)</summary>
|
||
private Canvas _uiCanvas;
|
||
|
||
// ==================== 初始化 ====================
|
||
|
||
public override void Init(long goldCount, int posID, long paobei, long fishScore, int beishu,
|
||
Action<int> action, Action action1 = null)
|
||
{
|
||
base.Init(goldCount, posID, paobei, fishScore, beishu, action, action1);
|
||
|
||
// 恢复位置到 Boss 死亡位置
|
||
transform.position = diePos;
|
||
|
||
// 设置爆奖次数
|
||
scoreCount = bjCount;
|
||
totalBJCount = bjCount;
|
||
|
||
// ⭐ 星际巡航不需要 ScoreNumScroll(Clone) 加分数字滚动特效(杀鱼分已加 Boss 面板)
|
||
showAddScoreFX = false;
|
||
// ⭐ 爆奖只分包不关联场上鱼(已有完整的道具武器/全屏/结算动画,
|
||
// 避免全屏随机位置死飞机爆奖)
|
||
bigPrizeFishMode = BigPrizeFishMode.NoFish;
|
||
|
||
// 解析武器 ID
|
||
ParseWeaponId();
|
||
|
||
// 根据武器 ID 选择预制体
|
||
SelectWeaponPrefabs();
|
||
|
||
// 启动完整爆奖流程
|
||
StartCoroutine(PropWeaponFlow());
|
||
}
|
||
|
||
/// <summary>重写:不移动到屏幕中心,但缓存座位引用</summary>
|
||
protected override void ApplySeatOffset()
|
||
{
|
||
GetSeatPosition();
|
||
}
|
||
|
||
/// <summary>重置子类运行时状态</summary>
|
||
protected override void ResetState()
|
||
{
|
||
base.ResetState();
|
||
_weaponId = 0;
|
||
_weaponTag = "";
|
||
_settlePrefab = null;
|
||
_awardFramePrefab = null;
|
||
_fullScreenPrefab = null;
|
||
_propPrefab = null;
|
||
_posterSprite = null;
|
||
bossCtrl = null;
|
||
diePos = Vector3.zero;
|
||
_netKilledFish.Clear();
|
||
_bossScoreTmp = null;
|
||
_bossPanelScore = 0;
|
||
|
||
// ⭐ 对象池复用:清理上次残留的临时特效
|
||
for (int i = 0; i < _spawnedEffects.Count; i++)
|
||
{
|
||
if (_spawnedEffects[i] != null)
|
||
Destroy(_spawnedEffects[i]);
|
||
}
|
||
_spawnedEffects.Clear();
|
||
}
|
||
|
||
// ==================== 武器 ID 解析 ====================
|
||
|
||
private void ParseWeaponId()
|
||
{
|
||
if (obj == null)
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] obj 为 null,无法解析 weaponID,走富甲天下");
|
||
_weaponId = 0;
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
JToken root = obj is JToken t ? t : JToken.Parse(obj.ToString());
|
||
|
||
_weaponId = root["weaponID"]?.Value<int>()
|
||
?? root["weaponId"]?.Value<int>()
|
||
?? root["weapon_id"]?.Value<int>()
|
||
?? 0;
|
||
|
||
// ⭐ 服务端现在用 animId 区分三种带死亡的爆奖(weaponID 可能不再下发):
|
||
// animId=2 → 形态二 铁拳 IronFist (weaponID=6)
|
||
// animId=3 → 形态三 光子 Symmetra (weaponID=8)
|
||
// animId=4 → 形态四 伽马风暴 GammaStorm (weaponID=7)
|
||
if (_weaponId == 0)
|
||
{
|
||
int animId = root["animId"]?.Value<int>() ?? 0;
|
||
switch (animId)
|
||
{
|
||
case 2: _weaponId = 6; break; // 铁拳
|
||
case 3: _weaponId = 8; break; // 光子
|
||
case 4: _weaponId = 7; break; // 伽马风暴
|
||
}
|
||
if (animId >= 2)
|
||
Debug.Log($"[XingJiXunHang] 无 weaponID,按 animId={animId} 映射为 weaponID={_weaponId}");
|
||
}
|
||
|
||
Debug.Log($"[XingJiXunHang] 解析武器 ID: weaponID={_weaponId}");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[XingJiXunHang] 解析 weaponID 失败: {e.Message}; 原始={obj}");
|
||
_weaponId = 0;
|
||
}
|
||
}
|
||
|
||
/// <summary>根据武器 ID 选择对应的预制体和标签</summary>
|
||
private void SelectWeaponPrefabs()
|
||
{
|
||
_weaponId = 6;
|
||
switch (_weaponId)
|
||
{
|
||
case 0:
|
||
// 形态一:富甲天下(无道具武器)
|
||
_weaponTag = "FuJiaTianXia";
|
||
_settlePrefab = settleFuJiaTianXia;
|
||
_awardFramePrefab = null;
|
||
_fullScreenPrefab = null;
|
||
_propPrefab = null;
|
||
_posterSprite = null;
|
||
break;
|
||
//铁拳
|
||
case 6:
|
||
_weaponTag = "IronFist";
|
||
_settlePrefab = isSelf ? settleIronFistSelf : settleIronFistOther;
|
||
_awardFramePrefab = awardFrameIronFist;
|
||
// ⭐ 美术规格:铁拳的全屏 UI 框用 PW_Effect_FullScreen_Symmetra(海报仍为 Prop_Poster_IronFist)
|
||
_fullScreenPrefab = fullScreenSymmetra;
|
||
_propPrefab = propIronFist;
|
||
_posterSprite = posterIronFist;
|
||
break;
|
||
|
||
case 7:
|
||
_weaponTag = "GammaStorm";
|
||
_settlePrefab = isSelf ? settleGammaStormSelf : settleGammaStormOther;
|
||
_awardFramePrefab = awardFrameGammaStorm;
|
||
_fullScreenPrefab = fullScreenGammaStorm;
|
||
_propPrefab = propGammaStorm;
|
||
_posterSprite = posterGammaStorm;
|
||
break;
|
||
//激光
|
||
case 8:
|
||
_weaponTag = "Symmetra";
|
||
_settlePrefab = isSelf ? settleSymmetraSelf : settleSymmetraOther;
|
||
_awardFramePrefab = awardFrameSymmetra;
|
||
_fullScreenPrefab = fullScreenSymmetra;
|
||
_propPrefab = propSymmetra;
|
||
_posterSprite = posterSymmetra;
|
||
break;
|
||
|
||
default:
|
||
Debug.LogWarning($"[XingJiXunHang] 未知 weaponID={_weaponId},走富甲天下");
|
||
goto case 0;
|
||
}
|
||
|
||
Debug.Log($"[XingJiXunHang] 选择武器: tag={_weaponTag} isSelf={isSelf}" +
|
||
$" settle={(_settlePrefab != null ? _settlePrefab.name : "null")}" +
|
||
$" awardFrame={(_awardFramePrefab != null ? _awardFramePrefab.name : "null")}" +
|
||
$" fullScreen={(_fullScreenPrefab != null ? _fullScreenPrefab.name : "null")}" +
|
||
$" prop={(_propPrefab != null ? _propPrefab.name : "null")}");
|
||
}
|
||
|
||
// ==================== 爆奖主流程 ====================
|
||
|
||
/// <summary>
|
||
/// 道具武器完整流程:
|
||
/// 1. 道具出场特效 (AppearProp)
|
||
/// 2. 全屏特效 (FullScreen: tap_tip → knock_tip → 隐藏Tip,仅 isSelf)
|
||
/// 3. 道具动画 (Prop: 各武器不同的子物体操作)
|
||
/// 4. 奖励框 (AwardFrame)
|
||
/// 5. CallBJ 发红包
|
||
/// 6. 满屏暴击文字 (TextCrit,仅 IronFist/GammaStorm)
|
||
/// 7. 结算特效 (Settle)
|
||
/// 8. EffEnd → 标准收尾
|
||
/// </summary>
|
||
private IEnumerator PropWeaponFlow()
|
||
{
|
||
Debug.Log($"[XingJiXunHang] 开始爆奖流程 tag={_weaponTag} isSelf={isSelf} diePos={diePos}");
|
||
|
||
// -------- 形态一(富甲天下):直接结算,无道具流程 --------
|
||
if (_weaponId == 0)
|
||
{
|
||
yield return StartCoroutine(FuJiaTianXiaFlow());
|
||
yield break;
|
||
}
|
||
|
||
// -------- Phase 1: 道具出场特效 --------
|
||
if (appearPropEffect != null)
|
||
{
|
||
var appearObj = SpawnWorldEffect(appearPropEffect, diePos);
|
||
Debug.Log("[XingJiXunHang] Phase1: 道具出场特效");
|
||
yield return new WaitForSeconds(appearDuration);
|
||
if (appearObj != null) Destroy(appearObj); // 出场特效播完即删(不等 die)
|
||
}
|
||
else
|
||
{
|
||
yield return new WaitForSeconds(appearDuration);
|
||
}
|
||
|
||
// -------- Phase 2: 全屏特效(仅 isSelf) --------
|
||
GameObject fullScreenObj = null;
|
||
if (isSelf && _fullScreenPrefab != null)
|
||
{
|
||
// ⭐ 全屏特效是 UI 预制体,锚点/anchoredPosition 已在预制体内写好,
|
||
// 只挂到父节点即可,不要传 position/rotation(否则会覆盖写好的值)
|
||
fullScreenObj = Instantiate(_fullScreenPrefab, CreateFishManager.Instance.bjEffectRoot);
|
||
_spawnedEffects.Add(fullScreenObj); // 加入统一销毁列表
|
||
Debug.Log("[XingJiXunHang] Phase2: 全屏特效(自己)");
|
||
|
||
// 执行 tap_tip → knock_tip 序列(序列内部已把 Tip 隐藏)
|
||
yield return StartCoroutine(PlayFullScreenTipSequence(fullScreenObj));
|
||
|
||
// ⭐ FullScreen 克隆对象本身不提前销毁(只隐藏 Tip),保留到爆奖结束,
|
||
// 由 die() 统一销毁/回收(_spawnedEffects 列表管理)
|
||
Debug.Log("[XingJiXunHang] FullScreen UI 框保留到爆奖结束(Tip 已隐藏)");
|
||
}
|
||
else
|
||
{
|
||
yield return new WaitForSeconds(0.1f);
|
||
}
|
||
|
||
// -------- Phase 3: 道具动画 --------
|
||
if (_propPrefab != null)
|
||
{
|
||
yield return StartCoroutine(PlayPropAnimation());
|
||
}
|
||
|
||
// -------- Phase 4: 奖励框(⭐ 不再额外 Spawn:奖励框即加分面板,已在杀鱼加分时由
|
||
// EnsureBossScoreTmp 以 _awardFramePrefab 创建并显示分数) --------
|
||
Debug.Log("[XingJiXunHang] Phase4: 奖励框即加分面板(EnsureBossScoreTmp 已按武器创建)");
|
||
|
||
// -------- Phase 5: CallBJ 发红包 --------
|
||
for (int i = 0; i < bjCount; i++)
|
||
{
|
||
Debug.Log($"[XingJiXunHang] Phase5: CallBJ {i + 1}/{bjCount}");
|
||
CallBJ(bjMinCount, bjMaxCount);
|
||
yield return new WaitForSeconds(0.5f);
|
||
}
|
||
|
||
// -------- Phase 6: 满屏暴击文字(仅 IronFist/GammaStorm) --------
|
||
if (_weaponId == 6 || _weaponId == 7)
|
||
{
|
||
PlayTextCrit();
|
||
yield return new WaitForSeconds(0.5f);
|
||
}
|
||
|
||
// -------- Phase 6.5: 伽马风暴暴击框 --------
|
||
if (_weaponId == 7 && critFrameGammaStorm != null)
|
||
{
|
||
PlayGammaStormCritFrame();
|
||
yield return new WaitForSeconds(0.5f);
|
||
}
|
||
|
||
// -------- Phase 7: 等待金币协程完成 --------
|
||
if (_activeCoinRoutines > 0)
|
||
{
|
||
yield return new WaitUntil(() => _activeCoinRoutines <= 0);
|
||
}
|
||
|
||
// -------- Phase 8: 结算特效(⭐ UI,isSelf 放 UI 中心,不再放世界 0,0) --------
|
||
if (_settlePrefab != null)
|
||
{
|
||
if (isSelf)
|
||
{
|
||
// ⭐ 结算特效是 UI 预制体 → 挂 UI 层放屏幕中心,加入统一销毁列表(die() 时删除)
|
||
var settleObj = SpawnUiEffect(_settlePrefab);
|
||
// ⭐ 中央结算面板上挂着 BJEndUI(美术已接好 tMP_beishu / tMP_score)→ 写入倍数 + 最终总分
|
||
InitSettleBjEndUI(settleObj);
|
||
Debug.Log("[XingJiXunHang] Phase8: 结算特效(自己,UI中心)");
|
||
}
|
||
else
|
||
{
|
||
// 他人视角:显示在击杀者座位上方(世界坐标)
|
||
Vector3 settlePos = GetSeatPosition() + Vector3.up * 2f;
|
||
var settleObj = SpawnWorldEffect(_settlePrefab, settlePos);
|
||
if (settleObj != null)
|
||
settleObj.transform.localScale = Vector3.one * 0.6f;
|
||
// ⭐ 他人视角结算面板同样写分数(银币精灵,isSelf=false)
|
||
InitSettleBjEndUI(settleObj);
|
||
Debug.Log($"[XingJiXunHang] Phase8: 结算特效(他人) pos={settlePos}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[XingJiXunHang] 结算预制体为空 tag={_weaponTag}");
|
||
}
|
||
|
||
yield return new WaitForSeconds(settleDuration);
|
||
|
||
// -------- Phase 10: 触发 EffEnd → 标准收尾 --------
|
||
if (!isEffEnd && !isScoreEnd)
|
||
{
|
||
Debug.Log("[XingJiXunHang] Phase10: 触发 EffEnd");
|
||
isGoldEffectStart = false;
|
||
EffEnd();
|
||
}
|
||
}
|
||
|
||
// ==================== 形态一:富甲天下流程 ====================
|
||
|
||
/// <summary>
|
||
/// 富甲天下流程:无道具武器,直接 CallBJ → 结算。
|
||
/// Settle: PW_Effect_Settle_FuJiaTianXia (Node子物体指认Source Image为tj_fcboss_0)
|
||
/// </summary>
|
||
private IEnumerator FuJiaTianXiaFlow()
|
||
{
|
||
Debug.Log("[XingJiXunHang] 富甲天下流程开始");
|
||
|
||
// CallBJ 发红包
|
||
for (int i = 0; i < bjCount; i++)
|
||
{
|
||
CallBJ(bjMinCount, bjMaxCount);
|
||
yield return new WaitForSeconds(0.5f);
|
||
}
|
||
|
||
// 等待金币协程完成
|
||
if (_activeCoinRoutines > 0)
|
||
{
|
||
yield return new WaitUntil(() => _activeCoinRoutines <= 0);
|
||
}
|
||
|
||
// 结算特效(⭐ UI,isSelf 放 UI 中心)
|
||
if (settleFuJiaTianXia != null)
|
||
{
|
||
// 音效:富甲天下结算——金币喷出 + 语音(PW_Settle_FuJiaTianXia1 / PW_Settle_FuJiaTianXia2)
|
||
AudioManager.Instance.PlayerFXByName("PW_Settle_FuJiaTianXia1");
|
||
AudioManager.Instance.PlayerFXByName("PW_Settle_FuJiaTianXia2");
|
||
|
||
if (isSelf)
|
||
{
|
||
var settleObj = SpawnUiEffect(settleFuJiaTianXia);
|
||
// ⭐ 中央结算面板 BJEndUI 写入倍数 + 最终总分
|
||
InitSettleBjEndUI(settleObj);
|
||
Debug.Log("[XingJiXunHang] 富甲天下结算(自己,UI中心)");
|
||
}
|
||
else
|
||
{
|
||
Vector3 settlePos = GetSeatPosition() + Vector3.up * 2f;
|
||
var settleObj = SpawnWorldEffect(settleFuJiaTianXia, settlePos);
|
||
if (settleObj != null)
|
||
settleObj.transform.localScale = Vector3.one * 0.6f;
|
||
// ⭐ 他人视角结算面板同样写分数
|
||
InitSettleBjEndUI(settleObj);
|
||
Debug.Log($"[XingJiXunHang] 富甲天下结算(他人) pos={settlePos}");
|
||
}
|
||
}
|
||
|
||
yield return new WaitForSeconds(settleDuration);
|
||
|
||
// 触发 EffEnd
|
||
if (!isEffEnd && !isScoreEnd)
|
||
{
|
||
isGoldEffectStart = false;
|
||
EffEnd();
|
||
}
|
||
}
|
||
|
||
// ==================== FullScreen tap_tip/knock_tip 序列 ====================
|
||
|
||
/// <summary>
|
||
/// 全屏特效的 tap_tip → knock_tip 序列(美术规格要求)。
|
||
/// 1. 激活 Tip 容器(tap_tip/knock_tip 的父级,保证子物体能显示)
|
||
/// 2. 激活 tap_tip
|
||
/// 3. 等待 tapTipDuration
|
||
/// 4. 激活 knock_tip(设置海报Sprite)
|
||
/// 5. 等待 knockTipDuration
|
||
/// 6. 隐藏 Tip 容器(连带 tap_tip/knock_tip 一起隐藏,所有形态通用)
|
||
/// </summary>
|
||
private IEnumerator PlayFullScreenTipSequence(GameObject fullScreenObj)
|
||
{
|
||
if (fullScreenObj == null) yield break;
|
||
|
||
// 查找子物体
|
||
Transform tapTip = FindChildRecursive(fullScreenObj.transform, tapTipName);
|
||
Transform knockTip = FindChildRecursive(fullScreenObj.transform, knockTipName);
|
||
Transform tipContainer = FindChildRecursive(fullScreenObj.transform, tipContainerName);
|
||
|
||
// ⭐ 序列开始前确保 Tip 容器激活(否则里面的 tap_tip/knock_tip 激活了也看不到)
|
||
if (tipContainer != null) tipContainer.gameObject.SetActive(true);
|
||
|
||
// 初始隐藏
|
||
if (tapTip != null) tapTip.gameObject.SetActive(false);
|
||
if (knockTip != null) knockTip.gameObject.SetActive(false);
|
||
|
||
// 1. 激活 tap_tip
|
||
if (tapTip != null)
|
||
{
|
||
tapTip.gameObject.SetActive(true);
|
||
Debug.Log($"[XingJiXunHang] FullScreen: 激活 {tapTipName}");
|
||
}
|
||
|
||
yield return new WaitForSeconds(tapTipDuration);
|
||
|
||
// 2. 激活 knock_tip(设置海报Sprite)
|
||
if (knockTip != null)
|
||
{
|
||
knockTip.gameObject.SetActive(true);
|
||
|
||
// 设置海报Sprite到 knock_tip 的 Image 组件
|
||
if (_posterSprite != null)
|
||
{
|
||
var img = knockTip.GetComponent<UnityEngine.UI.Image>();
|
||
if (img == null)
|
||
img = knockTip.GetComponentInChildren<UnityEngine.UI.Image>();
|
||
if (img != null)
|
||
{
|
||
img.sprite = _posterSprite;
|
||
Debug.Log($"[XingJiXunHang] FullScreen: 设置 {knockTipName} 海报={_posterSprite.name}");
|
||
}
|
||
}
|
||
|
||
Debug.Log($"[XingJiXunHang] FullScreen: 激活 {knockTipName}");
|
||
}
|
||
|
||
yield return new WaitForSeconds(knockTipDuration);
|
||
|
||
// 3. ⭐ 隐藏 Tip(隐藏 Tip 容器 + 显式隐藏 tap_tip/knock_tip,所有形态通用)
|
||
if (tapTip != null) tapTip.gameObject.SetActive(false);
|
||
if (knockTip != null) knockTip.gameObject.SetActive(false);
|
||
if (tipContainer != null)
|
||
{
|
||
tipContainer.gameObject.SetActive(false);
|
||
Debug.Log($"[XingJiXunHang] FullScreen: 隐藏 Tip 容器 {tipContainerName}");
|
||
}
|
||
|
||
Debug.Log("[XingJiXunHang] FullScreen: 隐藏 Tip");
|
||
}
|
||
|
||
// ==================== 道具动画(各武器不同) ====================
|
||
|
||
/// <summary>
|
||
/// 道具动画,根据武器类型执行不同的子物体操作。
|
||
/// </summary>
|
||
private IEnumerator PlayPropAnimation()
|
||
{
|
||
GameObject propObj = SpawnWorldEffect(_propPrefab, diePos);
|
||
|
||
Debug.Log($"[XingJiXunHang] Phase3: 道具动画 tag={_weaponTag} pos={diePos}");
|
||
|
||
switch (_weaponId)
|
||
{
|
||
case 6:
|
||
yield return StartCoroutine(IronFistPropAnim(propObj));
|
||
break;
|
||
case 7:
|
||
yield return StartCoroutine(GammaStormPropAnim(propObj));
|
||
break;
|
||
case 8:
|
||
yield return StartCoroutine(SymmetraPropAnim(propObj));
|
||
break;
|
||
}
|
||
// ⭐ 道具不再延迟销毁,统一由 die() 时清空 _spawnedEffects
|
||
}
|
||
|
||
/// <summary>
|
||
/// 铁拳道具动画(美术规格):
|
||
/// 1. 激活 Hole 子对象
|
||
/// 2. Hole 从死亡位置移动到屏幕左下角
|
||
/// 3. 隐藏 Hole
|
||
/// 4. 激活 Hand
|
||
/// </summary>
|
||
private IEnumerator IronFistPropAnim(GameObject propObj)
|
||
{
|
||
Transform hole = FindChildRecursive(propObj.transform, ironFistHoleName);
|
||
Transform hand = FindChildRecursive(propObj.transform, ironFistHandName);
|
||
|
||
// 初始状态
|
||
if (hole != null) hole.gameObject.SetActive(false);
|
||
if (hand != null) hand.gameObject.SetActive(false);
|
||
|
||
// 1. 激活 Hole
|
||
if (hole != null)
|
||
{
|
||
hole.gameObject.SetActive(true);
|
||
hole.position = diePos;
|
||
Debug.Log("[XingJiXunHang] 铁拳: 激活Hole");
|
||
|
||
// 音效:道具铁拳展示(PW_Prop_IronFist_Show)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_IronFist_Show");
|
||
}
|
||
|
||
yield return new WaitForSeconds(0.2f);
|
||
|
||
// 2. Hole 移动到屏幕左下角(加偏移,往屏幕内挪一点)
|
||
Vector3 bottomLeft = GetScreenBottomLeft() + ironFistCornerOffset;
|
||
if (hole != null)
|
||
{
|
||
yield return StartCoroutine(MoveTo(hole, bottomLeft, propAnimDuration));
|
||
Debug.Log($"[XingJiXunHang] 铁拳: Hole移动到左下角+偏移 {bottomLeft}");
|
||
|
||
// ⭐ Hole 到位后停留 0.5 秒,让玩家看清再释放(之前太快直接释放 Hand)
|
||
yield return new WaitForSeconds(ironFistHoldDelay);
|
||
Debug.Log($"[XingJiXunHang] 铁拳: Hole 到位停留 {ironFistHoldDelay}s");
|
||
}
|
||
|
||
// 3. 隐藏 Hole
|
||
if (hole != null) hole.gameObject.SetActive(false);
|
||
|
||
// 4. 激活 Hand(击打)
|
||
Vector3 handPos = bottomLeft;
|
||
if (hand != null)
|
||
{
|
||
hand.gameObject.SetActive(true);
|
||
hand.position = handPos;
|
||
Debug.Log("[XingJiXunHang] 铁拳: 激活Hand");
|
||
|
||
// 音效:道具铁拳击打节拍(PW_Prop_IronFist_Knock_Beat)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_IronFist_Knock_Beat");
|
||
}
|
||
|
||
// ⭐ Hand 激活后延迟 2 秒,然后直接把 Hand 隐藏
|
||
yield return new WaitForSeconds(ironFistAwardDelay);
|
||
Debug.Log($"[XingJiXunHang] 铁拳: Hand 激活后延迟 {ironFistAwardDelay}s,隐藏 Hand");
|
||
if (hand != null) hand.gameObject.SetActive(false);
|
||
|
||
// ⭐ 铁拳击打:范围内普通鱼从近到远逐个死亡(总时长约1秒),金币从左下角飞向炮台
|
||
if (ironFistKillFish)
|
||
{
|
||
yield return StartCoroutine(KillFishByIronFistRoutine(handPos));
|
||
}
|
||
|
||
yield return new WaitForSeconds(0.3f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 铁拳击打杀鱼:检测 Hand 半径 ironFistKillRadius 范围内的普通鱼,
|
||
/// 计算鱼与预制体(Hand)的距离,从近到远逐个死亡(总时长约 ironFistKillTotalTime 秒),
|
||
/// 金币从鱼的位置(左下角)飞向炮台座位(从下往上延伸)。
|
||
/// </summary>
|
||
private IEnumerator KillFishByIronFistRoutine(Vector3 center)
|
||
{
|
||
if (ironFistKillLayer.value == 0)
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] 铁拳杀鱼层 ironFistKillLayer 未设置(Inspector 选 Fish 层),跳过杀鱼");
|
||
ironFistKillFish = false; // 只提示一次
|
||
yield break;
|
||
}
|
||
|
||
Collider2D[] hits = Physics2D.OverlapCircleAll(center, Mathf.Max(0.1f, ironFistKillRadius), ironFistKillLayer);
|
||
if (hits == null || hits.Length == 0) yield break;
|
||
|
||
// 收集范围内未炸过的普通鱼
|
||
List<MonsterContral> fishes = new List<MonsterContral>();
|
||
foreach (var hit in hits)
|
||
{
|
||
if (hit == null || !hit.CompareTag("Fish")) continue;
|
||
var monster = hit.GetComponentInParent<MonsterContral>();
|
||
if (monster == null) continue;
|
||
if (monster.fishenum != FishTypeEnum.普通鱼) continue; // 只炸普通鱼
|
||
if (_netKilledFish.Contains(monster.guid)) continue; // 已炸过跳过
|
||
fishes.Add(monster);
|
||
}
|
||
if (fishes.Count == 0) yield break;
|
||
|
||
// ⭐ 按距离 Hand(预制体)从近到远排序
|
||
fishes.Sort((a, b) =>
|
||
Vector2.Distance(a.transform.position, center)
|
||
.CompareTo(Vector2.Distance(b.transform.position, center)));
|
||
|
||
// ⭐ 逐个杀:每只间隔 = 总时长/数量,整体约 1 秒
|
||
float interval = ironFistKillTotalTime / fishes.Count;
|
||
Vector3 turretPos = GetSeatPosition(); // 金币飞向炮台
|
||
int killed = 0;
|
||
foreach (var monster in fishes)
|
||
{
|
||
// ⭐ 分数加满(=maxScore)后停止杀鱼
|
||
if (BossScoreFull)
|
||
{
|
||
Debug.Log($"[XingJiXunHang] 铁拳: 面板分已达上限 {maxScore},停止杀鱼");
|
||
break;
|
||
}
|
||
if (monster == null) continue;
|
||
if (!_netKilledFish.Add(monster.guid)) continue;
|
||
|
||
var coinNum = new RedPacketRecord
|
||
{
|
||
Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore
|
||
};
|
||
// ⭐ 飞机死亡掉金币(飞向炮台),分数同时加到 Boss 面板(封顶 maxScore)
|
||
CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, turretPos, isLocal: isSelf);
|
||
AddBossPanelScore(coinNum.Amount);
|
||
killed++;
|
||
Debug.Log($"[XingJiXunHang] 铁拳: 从近到远杀鱼[{killed}/{fishes.Count}] 距离={Vector2.Distance(monster.transform.position, center):0.00}");
|
||
yield return new WaitForSeconds(interval);
|
||
}
|
||
Debug.Log($"[XingJiXunHang] 铁拳: 击打杀鱼完成 {killed} 条(总时长约{ironFistKillTotalTime}s)");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 光子道具动画(美术规格):
|
||
/// 1. Prop 从死亡位置移动到屏幕中心
|
||
/// 2. 隐藏 Lock
|
||
/// 3. PropL 和 PropR 分别移动到屏幕两边
|
||
/// 4. 激活 laser 子物体
|
||
/// </summary>
|
||
private IEnumerator SymmetraPropAnim(GameObject propObj)
|
||
{
|
||
Transform lockObj = FindChildRecursive(propObj.transform, symmetraLockName);
|
||
Transform propL = FindChildRecursive(propObj.transform, symmetraPropLName);
|
||
Transform propR = FindChildRecursive(propObj.transform, symmetraPropRName);
|
||
Transform laser = FindChildRecursive(propObj.transform, symmetraLaserName);
|
||
|
||
// 初始状态
|
||
if (lockObj != null) lockObj.gameObject.SetActive(true);
|
||
if (propL != null) propL.gameObject.SetActive(false);
|
||
if (propR != null) propR.gameObject.SetActive(false);
|
||
if (laser != null) laser.gameObject.SetActive(false);
|
||
|
||
// 1. Prop 移动到屏幕中心
|
||
propObj.transform.position = diePos;
|
||
yield return StartCoroutine(MoveTo(propObj.transform, Vector3.zero, propAnimDuration));
|
||
Debug.Log("[XingJiXunHang] 光子: 移动到屏幕中心");
|
||
|
||
// 2. 隐藏 Lock
|
||
if (lockObj != null)
|
||
{
|
||
lockObj.gameObject.SetActive(false);
|
||
Debug.Log("[XingJiXunHang] 光子: 隐藏Lock");
|
||
}
|
||
|
||
// 3. PropL/PropR 移动到屏幕两边
|
||
Vector3 leftPos = GetScreenLeft();
|
||
Vector3 rightPos = GetScreenRight();
|
||
|
||
if (propL != null && propR != null)
|
||
{
|
||
propL.gameObject.SetActive(true);
|
||
propR.gameObject.SetActive(true);
|
||
propL.position = Vector3.zero;
|
||
propR.position = Vector3.zero;
|
||
|
||
// 音效:道具对称体到达边缘(PW_Prop_Symmetra_To_Edge)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_Symmetra_To_Edge");
|
||
|
||
StartCoroutine(MoveTo(propL, leftPos, propAnimDuration * 0.5f));
|
||
yield return StartCoroutine(MoveTo(propR, rightPos, propAnimDuration * 0.5f));
|
||
Debug.Log("[XingJiXunHang] 光子: PropL/PropR移动到两边");
|
||
}
|
||
|
||
// 4. 激活 laser
|
||
if (laser != null)
|
||
{
|
||
laser.gameObject.SetActive(true);
|
||
|
||
// 音效:道具对称体接触(PW_Prop_Symmetra_Contact)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_Symmetra_Contact");
|
||
|
||
// ⭐ laser 里的 LineRenderer:端点用 左右(PropL/PropR)里面各自的 laser 子对象位置
|
||
var lr = laser.GetComponent<LineRenderer>();
|
||
if (lr != null && propL != null && propR != null)
|
||
{
|
||
// 找左右里面的 laser 发射点对象(找不到则回退用 PropL/PropR 自身)
|
||
Transform laserL = FindChildRecursive(propL, symmetraLaserName);
|
||
Transform laserR = FindChildRecursive(propR, symmetraLaserName);
|
||
Vector3 endL = laserL != null ? laserL.position : propL.position;
|
||
Vector3 endR = laserR != null ? laserR.position : propR.position;
|
||
|
||
lr.positionCount = 2;
|
||
if (lr.useWorldSpace)
|
||
{
|
||
lr.SetPosition(0, endL);
|
||
lr.SetPosition(1, endR);
|
||
}
|
||
else
|
||
{
|
||
// 本地坐标空间:转成 laser 的本地坐标
|
||
lr.SetPosition(0, laser.InverseTransformPoint(endL));
|
||
lr.SetPosition(1, laser.InverseTransformPoint(endR));
|
||
}
|
||
Debug.Log($"[XingJiXunHang] 光子: laser LineRenderer 端点 L={endL} R={endR} useWorldSpace={lr.useWorldSpace}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[XingJiXunHang] 光子: laser 无 LineRenderer 或 PropL/PropR 为空(lr={lr != null} propL={propL != null} propR={propR != null})");
|
||
}
|
||
|
||
// ⭐ laser 设置好并激活后,上下摆动(让激光看起来在动)
|
||
if (laserSwingCycles > 0)
|
||
{
|
||
yield return StartCoroutine(LaserSwing(laser, laserSwingAmplitude, laserSwingCycles));
|
||
}
|
||
}
|
||
|
||
// ⭐ 4条激光沿各自方向伸展到最长位置(laser1:+X → laser2:-Y → laser3:+Y → laser4:-X)
|
||
yield return StartCoroutine(LaserExpand(propObj));
|
||
|
||
// ⭐ 电网:Prop 整体上下扫动 + 碰到飞机(鱼)爆炸
|
||
if (netSweepCycles > 0)
|
||
{
|
||
// 音效:道具对称体攻击(PW_Prop_Symmetra_Attack,电网扫动)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_Symmetra_Attack");
|
||
|
||
yield return StartCoroutine(ElectricNetSweep(propObj, propL, propR));
|
||
}
|
||
|
||
// ⭐ 激光结束:等 1 秒后直接隐藏 Prop(不再一直显示到爆奖结束才回收)
|
||
yield return new WaitForSeconds(symmetraPropHideDelay);
|
||
if (propObj != null)
|
||
{
|
||
propObj.SetActive(false);
|
||
Debug.Log($"[XingJiXunHang] 光子: 激光结束等待 {symmetraPropHideDelay}s 后隐藏 Prop");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 4条激光伸展:laser1 向 +X(屏幕最右)、laser2 向 -Y(屏幕最下)、
|
||
/// laser3 向 +Y(屏幕最上)、laser4 向 -X(屏幕最左),到达最长位置。
|
||
/// </summary>
|
||
private IEnumerator LaserExpand(GameObject propObj)
|
||
{
|
||
if (propObj == null) yield break;
|
||
|
||
Transform l1 = FindChildRecursive(propObj.transform, laser1Name);
|
||
Transform l2 = FindChildRecursive(propObj.transform, laser2Name);
|
||
Transform l3 = FindChildRecursive(propObj.transform, laser3Name);
|
||
Transform l4 = FindChildRecursive(propObj.transform, laser4Name);
|
||
|
||
int count = 0;
|
||
// laser1 → +X 正方向(保持当前 Y,X 到屏幕最右)
|
||
if (l1 != null)
|
||
{
|
||
Vector3 dest = new Vector3(GetScreenRight().x, l1.position.y, 0);
|
||
StartCoroutine(MoveTo(l1, dest, laserExpandSpeed));
|
||
count++;
|
||
}
|
||
// laser2 → -Y 方向(保持当前 X,Y 到屏幕最下)
|
||
if (l2 != null)
|
||
{
|
||
Vector3 dest = new Vector3(l2.position.x, GetScreenBottom().y, 0);
|
||
StartCoroutine(MoveTo(l2, dest, laserExpandSpeed));
|
||
count++;
|
||
}
|
||
// laser3 → +Y 方向(保持当前 X,Y 到屏幕最上)
|
||
if (l3 != null)
|
||
{
|
||
Vector3 dest = new Vector3(l3.position.x, GetScreenTop().y, 0);
|
||
StartCoroutine(MoveTo(l3, dest, laserExpandSpeed));
|
||
count++;
|
||
}
|
||
// laser4 → -X 方向(保持当前 Y,X 到屏幕最左)
|
||
if (l4 != null)
|
||
{
|
||
Vector3 dest = new Vector3(GetScreenLeft().x, l4.position.y, 0);
|
||
StartCoroutine(MoveTo(l4, dest, laserExpandSpeed));
|
||
count++;
|
||
}
|
||
|
||
yield return new WaitForSeconds(laserExpandSpeed + 0.2f);
|
||
Debug.Log($"[XingJiXunHang] 光子: 4条激光伸展完成 找到{count}条({laser1Name}/{laser2Name}/{laser3Name}/{laser4Name})");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 电网扫动:Prop 整体上下移动(幅度 netSweepAmplitude,最多 netSweepCycles 次),
|
||
/// 扫动过程中每帧检测 PropL~PropR 之间的矩形区域,碰到普通鱼 → 爆炸(Id_DieFish_FX)。
|
||
/// ⭐ 不需要一直按设定的圈数移动:面板分加满(=maxScore)后提前停止扫动。
|
||
/// </summary>
|
||
private IEnumerator ElectricNetSweep(GameObject propObj, Transform propL, Transform propR)
|
||
{
|
||
if (propObj == null) yield break;
|
||
|
||
Vector3 basePos = propObj.transform.position;
|
||
for (int i = 0; i < netSweepCycles; i++)
|
||
{
|
||
// ⭐ 面板分加满:提前停止扫动(不需要一直按圈数移动)
|
||
if (BossScoreFull)
|
||
{
|
||
Debug.Log($"[XingJiXunHang] 光子: 面板分已达上限 {maxScore},提前停止电网扫动");
|
||
break;
|
||
}
|
||
// 上扫
|
||
yield return StartCoroutine(SweepSegment(propObj, propL, propR, basePos + Vector3.up * netSweepAmplitude));
|
||
if (BossScoreFull) break;
|
||
// 下扫
|
||
yield return StartCoroutine(SweepSegment(propObj, propL, propR, basePos - Vector3.up * netSweepAmplitude));
|
||
}
|
||
// 回原位
|
||
yield return StartCoroutine(SweepSegment(propObj, propL, propR, basePos));
|
||
Debug.Log($"[XingJiXunHang] 光子: 电网扫动结束(计划{netSweepCycles}圈)炸鱼数={_netKilledFish.Count}");
|
||
}
|
||
|
||
/// <summary>电网从当前位置移动到 dest(逐帧插值 + 途中杀鱼)</summary>
|
||
private IEnumerator SweepSegment(GameObject propObj, Transform propL, Transform propR, Vector3 dest)
|
||
{
|
||
if (propObj == null) yield break;
|
||
|
||
Vector3 start = propObj.transform.position;
|
||
float elapsed = 0f;
|
||
while (elapsed < netSweepSpeed)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
float t = netSweepSpeed > 0 ? elapsed / netSweepSpeed : 1f;
|
||
if (t > 1f) t = 1f;
|
||
propObj.transform.position = Vector3.Lerp(start, dest, t);
|
||
if (netKillFish)
|
||
KillFishInNet(propL, propR);
|
||
// ⭐ 分数加满:本段提前结束
|
||
if (BossScoreFull) break;
|
||
yield return null;
|
||
}
|
||
propObj.transform.position = dest;
|
||
if (netKillFish && !BossScoreFull)
|
||
KillFishInNet(propL, propR);
|
||
}
|
||
|
||
/// <summary>检测 PropL~PropR 之间的矩形电网区域,普通鱼直接爆炸</summary>
|
||
private void KillFishInNet(Transform propL, Transform propR)
|
||
{
|
||
if (propL == null || propR == null) return;
|
||
if (netKillLayer.value == 0)
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] 电网杀鱼层 netKillLayer 未设置(Inspector 选 Fish 层),跳过杀鱼");
|
||
netKillFish = false; // 只提示一次
|
||
return;
|
||
}
|
||
|
||
// 电网矩形区域:左右端 ± 半径
|
||
float radius = Mathf.Max(0.1f, netKillRadius);
|
||
Vector2 min = new Vector2(
|
||
Mathf.Min(propL.position.x, propR.position.x) - radius,
|
||
Mathf.Min(propL.position.y, propR.position.y) - radius);
|
||
Vector2 max = new Vector2(
|
||
Mathf.Max(propL.position.x, propR.position.x) + radius,
|
||
Mathf.Max(propL.position.y, propR.position.y) + radius);
|
||
|
||
Collider2D[] hits = Physics2D.OverlapAreaAll(min, max, netKillLayer);
|
||
if (hits == null || hits.Length == 0) return;
|
||
|
||
Vector3 turretPos = GetSeatPosition(); // 金币飞向炮台
|
||
int killed = 0;
|
||
foreach (var hit in hits)
|
||
{
|
||
// ⭐ 分数加满(=maxScore)后停止杀鱼
|
||
if (BossScoreFull)
|
||
{
|
||
Debug.Log($"[XingJiXunHang] 电网: 面板分已达上限 {maxScore},停止杀鱼");
|
||
break;
|
||
}
|
||
if (hit == null || !hit.CompareTag("Fish")) continue;
|
||
var monster = hit.GetComponentInParent<MonsterContral>();
|
||
if (monster == null) continue;
|
||
if (monster.fishenum != FishTypeEnum.普通鱼) continue; // 只炸普通鱼
|
||
if (!_netKilledFish.Add(monster.guid)) continue; // 已炸过跳过
|
||
|
||
var coinNum = new RedPacketRecord
|
||
{
|
||
Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore
|
||
};
|
||
// ⭐ 飞机死亡掉金币(飞向炮台),分数同时加到 Boss 面板(封顶 maxScore)
|
||
CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, turretPos, isLocal: isSelf);
|
||
AddBossPanelScore(coinNum.Amount);
|
||
killed++;
|
||
}
|
||
if (killed > 0)
|
||
Debug.Log($"[XingJiXunHang] 电网炸鱼 {killed} 条");
|
||
}
|
||
|
||
/// <summary>laser 上下摆动动画(幅度 amplitude,来回 cycles 次,结束后回到原位)</summary>
|
||
private IEnumerator LaserSwing(Transform target, float amplitude, int cycles)
|
||
{
|
||
if (target == null) yield break;
|
||
|
||
Vector3 basePos = target.position;
|
||
for (int i = 0; i < cycles; i++)
|
||
{
|
||
yield return StartCoroutine(MoveTo(target, basePos + Vector3.up * amplitude, 0.3f));
|
||
yield return StartCoroutine(MoveTo(target, basePos - Vector3.up * amplitude, 0.3f));
|
||
}
|
||
// 回到原位
|
||
yield return StartCoroutine(MoveTo(target, basePos, 0.2f));
|
||
Debug.Log($"[XingJiXunHang] 光子: laser 上下摆动完成 cycles={cycles}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 伽马风暴道具动画(美术规格):
|
||
/// 1. Prop 从死亡位置移动到屏幕中心
|
||
/// 2. 显示四根射线(激光)
|
||
/// 3. 等待 gammaRotateDelay(默认1秒)
|
||
/// 4. 开始旋转(Prop 整体绕 Z 轴),旋转中碰到激光的普通鱼直接死亡
|
||
/// </summary>
|
||
private IEnumerator GammaStormPropAnim(GameObject propObj)
|
||
{
|
||
// 1. 移动到屏幕中心
|
||
propObj.transform.position = diePos;
|
||
yield return StartCoroutine(MoveTo(propObj.transform, Vector3.zero, propAnimDuration));
|
||
Debug.Log("[XingJiXunHang] 伽马风暴: 移动到屏幕中心");
|
||
|
||
// 2. 显示四根射线(激光,默认隐藏则激活)
|
||
Transform[] lasers = FindGammaLasers(propObj);
|
||
int shown = 0;
|
||
foreach (var l in lasers)
|
||
{
|
||
if (l != null)
|
||
{
|
||
l.gameObject.SetActive(true);
|
||
shown++;
|
||
}
|
||
}
|
||
Debug.Log($"[XingJiXunHang] 伽马风暴: 显示激光 {shown}/{lasers.Length} 根");
|
||
|
||
// 音效:道具伽马风暴准备(PW_Prop_GammaStorm_Ready)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_GammaStorm_Ready");
|
||
|
||
// ⭐ 射线长度 = 斜边(直角边 = 屏幕宽度一半 / 高度一半),延伸到屏幕角落
|
||
float diag = GetScreenHalfDiagonal();
|
||
UpdateGammaLaserEndpoints(lasers, propObj.transform.position, diag);
|
||
Debug.Log($"[XingJiXunHang] 伽马风暴: 射线长度设为斜边 {diag}");
|
||
|
||
// 3. 等待 1 秒(Tip 隐藏后停顿,让玩家看清激光)
|
||
yield return new WaitForSeconds(gammaRotateDelay);
|
||
Debug.Log($"[XingJiXunHang] 伽马风暴: 激光显示等待 {gammaRotateDelay}s 后开始旋转");
|
||
|
||
// 音效:道具伽马风暴攻击(PW_Prop_GammaStorm_Attack,开始旋转)
|
||
AudioManager.Instance.PlayerFXByName("PW_Prop_GammaStorm_Attack");
|
||
|
||
// 4. 旋转 + 射线杀鱼
|
||
yield return StartCoroutine(GammaRotateAndKill(propObj, lasers, diag));
|
||
}
|
||
|
||
/// <summary>按 gammaLaserNames 查找四根射线子物体</summary>
|
||
private Transform[] FindGammaLasers(GameObject propObj)
|
||
{
|
||
if (propObj == null || gammaLaserNames == null || gammaLaserNames.Length == 0)
|
||
return new Transform[0];
|
||
|
||
Transform[] lasers = new Transform[gammaLaserNames.Length];
|
||
for (int i = 0; i < gammaLaserNames.Length; i++)
|
||
{
|
||
// ⭐ 先忽略大小写找(预制体实际小写 laser1~4),找不到再精确匹配
|
||
lasers[i] = FindChildIgnoreCase(propObj.transform, gammaLaserNames[i]);
|
||
if (lasers[i] == null)
|
||
lasers[i] = FindChildRecursive(propObj.transform, gammaLaserNames[i]);
|
||
if (lasers[i] == null)
|
||
Debug.LogWarning($"[XingJiXunHang] 伽马风暴: 找不到射线子物体 {gammaLaserNames[i]}");
|
||
}
|
||
return lasers;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 伽马风暴:Prop 整体旋转 gammaRotateAngle 度(默认80°)。
|
||
/// 转到 gammaShrinkStartAngle(45°)后激光开始慢慢缩短,转到终点时缩到看不见(长度 0)。
|
||
/// 旋转中射线扫到的普通鱼死亡。
|
||
/// </summary>
|
||
private IEnumerator GammaRotateAndKill(GameObject propObj, Transform[] lasers, float laserLength)
|
||
{
|
||
if (propObj == null) yield break;
|
||
|
||
float rotated = 0f;
|
||
int killCount = 0;
|
||
float shrinkSpan = Mathf.Max(0.01f, gammaRotateAngle - gammaShrinkStartAngle);
|
||
|
||
while (rotated < gammaRotateAngle)
|
||
{
|
||
float step = gammaRotateSpeed * Time.deltaTime;
|
||
propObj.transform.Rotate(0f, 0f, step);
|
||
rotated += step;
|
||
|
||
// ⭐ 激光长度:前 gammaShrinkStartAngle 度保持全斜边,之后线性缩短到 0(看不见)
|
||
float curLen = laserLength;
|
||
if (rotated > gammaShrinkStartAngle)
|
||
{
|
||
float t = (rotated - gammaShrinkStartAngle) / shrinkSpan;
|
||
curLen = laserLength * (1f - Mathf.Clamp01(t));
|
||
}
|
||
|
||
// 每帧更新射线端点(LineRenderer 世界坐标不随父旋转,必须跟随激光新朝向)
|
||
UpdateGammaLaserEndpoints(lasers, propObj.transform.position, curLen);
|
||
|
||
if (gammaKillFish)
|
||
KillFishByGammaLasers(propObj.transform, lasers, curLen, ref killCount);
|
||
yield return null;
|
||
}
|
||
|
||
// 终点:激光缩到 0 长度(隐藏)
|
||
UpdateGammaLaserEndpoints(lasers, propObj.transform.position, 0f);
|
||
Debug.Log($"[XingJiXunHang] 伽马风暴: 旋转完成 {rotated:0.#}°(45°后激光缩短至消失)炸鱼 {killCount} 条");
|
||
|
||
// ⭐ 分不够就直接补齐到 maxScore(旋转杀鱼没杀满时,面板补满)
|
||
if (!BossScoreFull)
|
||
{
|
||
long need = maxScore - _bossPanelScore;
|
||
if (need > 0)
|
||
{
|
||
AddBossPanelScore(need);
|
||
Debug.Log($"[XingJiXunHang] 伽马风暴: 分不够,直接补齐 {need} → {_bossPanelScore}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置四根射线长度为斜边:LineRenderer 端点 = 中心 → 中心 + 射线朝向×长度。
|
||
/// 射线朝向优先取"激光当前位置-中心"(随 Prop 旋转自动更新),零向量时用激光本地 Y 轴方向。
|
||
/// </summary>
|
||
private void UpdateGammaLaserEndpoints(Transform[] lasers, Vector3 center, float length)
|
||
{
|
||
if (lasers == null) return;
|
||
|
||
foreach (var laser in lasers)
|
||
{
|
||
if (laser == null) continue;
|
||
|
||
// 射线方向:激光相对中心的位置(旋转后自动更新);为 0 则用激光自身的朝向
|
||
Vector2 dir = (Vector2)laser.position - (Vector2)center;
|
||
if (dir.sqrMagnitude < 0.0001f)
|
||
dir = laser.up; // 本地 Y 轴世界方向(或 right,按预制体实际朝向)
|
||
if (dir.sqrMagnitude < 0.0001f) dir = Vector2.right;
|
||
dir.Normalize();
|
||
|
||
Vector3 endWorld = center + (Vector3)dir * length;
|
||
|
||
var lr = laser.GetComponent<LineRenderer>();
|
||
if (lr != null)
|
||
{
|
||
lr.positionCount = 2;
|
||
if (lr.useWorldSpace)
|
||
{
|
||
lr.SetPosition(0, center);
|
||
lr.SetPosition(1, endWorld);
|
||
}
|
||
else
|
||
{
|
||
lr.SetPosition(0, laser.InverseTransformPoint(center));
|
||
lr.SetPosition(1, laser.InverseTransformPoint(endWorld));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 非 LineRenderer:把激光整体拉伸到目标长度(假定沿本地 Y 轴伸长)
|
||
float curLen = laser.localScale.y;
|
||
if (curLen < 0.001f) curLen = 1f;
|
||
laser.localScale = new Vector3(laser.localScale.x, length / curLen, laser.localScale.z);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>屏幕半宽半高为直角边的斜边长度(世界单位)= 屏幕中心到角落的距离</summary>
|
||
private float GetScreenHalfDiagonal()
|
||
{
|
||
var cam = Camera.main;
|
||
if (cam == null) return 10f;
|
||
|
||
float z = cam.transform.position.z * -1f;
|
||
Vector3 center = cam.ScreenToWorldPoint(new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, z));
|
||
Vector3 corner = cam.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, z));
|
||
float halfW = Mathf.Abs(corner.x - center.x);
|
||
float halfH = Mathf.Abs(corner.y - center.y);
|
||
return Mathf.Sqrt(halfW * halfW + halfH * halfH);
|
||
}
|
||
|
||
/// <summary>沿每根射线方向 Raycast 检测(长度=斜边),扫到的普通鱼直接爆炸(防重复)</summary>
|
||
private void KillFishByGammaLasers(Transform center, Transform[] lasers, float laserLength, ref int killCount)
|
||
{
|
||
if (center == null || lasers == null || lasers.Length == 0) return;
|
||
if (gammaKillLayer.value == 0)
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] 伽马风暴杀鱼层 gammaKillLayer 未设置(Inspector 选 Fish 层),跳过杀鱼");
|
||
gammaKillFish = false; // 只提示一次
|
||
return;
|
||
}
|
||
|
||
Vector3 turretPos = GetSeatPosition(); // 金币飞向炮台
|
||
foreach (var laser in lasers)
|
||
{
|
||
if (laser == null) continue;
|
||
|
||
// 射线:从 Prop 中心朝激光朝向,距离 = 斜边长度(整根激光覆盖范围)
|
||
Vector2 origin = center.position;
|
||
Vector2 toEnd = (Vector2)laser.position - origin;
|
||
Vector2 dir = toEnd.sqrMagnitude < 0.0001f ? (Vector2)laser.up : toEnd.normalized;
|
||
if (dir.sqrMagnitude < 0.0001f) dir = Vector2.right;
|
||
dir.Normalize();
|
||
|
||
RaycastHit2D[] hits = Physics2D.RaycastAll(origin, dir, laserLength, gammaKillLayer);
|
||
if (hits == null || hits.Length == 0) continue;
|
||
|
||
foreach (var hit in hits)
|
||
{
|
||
// ⭐ 分数加满(=maxScore)后停止杀鱼
|
||
if (BossScoreFull)
|
||
{
|
||
Debug.Log($"[XingJiXunHang] 伽马射线: 面板分已达上限 {maxScore},停止杀鱼");
|
||
return;
|
||
}
|
||
if (hit.collider == null || !hit.collider.CompareTag("Fish")) continue;
|
||
var monster = hit.collider.GetComponentInParent<MonsterContral>();
|
||
if (monster == null) continue;
|
||
if (monster.fishenum != FishTypeEnum.普通鱼) continue; // 只炸普通鱼
|
||
if (!_netKilledFish.Add(monster.guid)) continue; // 已炸过跳过
|
||
|
||
var coinNum = new RedPacketRecord
|
||
{
|
||
Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore
|
||
};
|
||
// ⭐ 飞机死亡掉金币(飞向炮台),分数同时加到 Boss 面板(封顶 maxScore)
|
||
CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, turretPos, isLocal: isSelf);
|
||
AddBossPanelScore(coinNum.Amount);
|
||
killCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 满屏暴击文字 ====================
|
||
|
||
/// <summary>
|
||
/// 播放满屏暴击文字(IronFist/GammaStorm 用)。
|
||
/// 自己看 TextCrit_Self,他人看 TextCrit_Other。
|
||
///
|
||
/// ⭐ 文字内容:预制体里写死的是占位 '123',必须由代码写入「爆奖小鱼死亡累加的面板分」
|
||
/// (<see cref="_bossPanelScore"/>,与奖励框上滚动的分同一个值,封顶 maxScore),前缀 "+"。
|
||
/// ⭐ 两个预制体都自带 Canvas(RenderMode=WorldSpace,美术里是无效层ID + order 5),
|
||
/// 必须统一挂到游戏 UI 层、用 anchoredPosition 定位:
|
||
/// 自己 → 屏幕中心;他人 → 击杀者座位(座位世界坐标 → 画布坐标)。
|
||
/// (之前"他人"走 SpawnWorldEffect 挂到 bjEffectRoot(场景根、不在 Canvas 下),
|
||
/// 它的 WorldSpace 画布会按世界单位渲染 100×100 → 尺寸/位置全错。)
|
||
/// ⭐ 层级:预制体自带的是无效层ID + order 5,会被 FullScreen 框(Default, order 12)
|
||
/// 压在下面 → 这里显式改成 Default + 更高 order。
|
||
/// ⭐ 动画:对齐 lua CritTextEffect —— Text/Crit 从 0 弹出、停留后整体缩小消失并销毁。
|
||
/// </summary>
|
||
private void PlayTextCrit()
|
||
{
|
||
GameObject critPrefab = isSelf ? textCritSelf : textCritOther;
|
||
if (critPrefab == null)
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] 暴击文字预制体为空");
|
||
return;
|
||
}
|
||
|
||
// ⭐ 自己/他人统一挂 UI 层(同一个渲染空间),位置:自己=屏幕中心,他人=击杀者座位
|
||
GameObject critObj = SpawnUiEffect(critPrefab);
|
||
if (critObj == null)
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] 暴击文字生成失败,跳过分数写入");
|
||
return;
|
||
}
|
||
|
||
if (isSelf)
|
||
{
|
||
Debug.Log("[XingJiXunHang] 播放满屏暴击(自己,UI中心)");
|
||
}
|
||
else
|
||
{
|
||
// ⭐ 他人:座位世界坐标 → 画布 anchoredPosition(与其它 UI 飘字同一套换算)
|
||
var critRt = critObj.GetComponent<RectTransform>();
|
||
EnsureUiLayer();
|
||
if (critRt != null && _uiCanvas != null)
|
||
{
|
||
Vector3 seatPos = GetSeatPosition() + Vector3.up * 2f;
|
||
critRt.anchoredPosition =
|
||
CoordinateTransformation.WorldPos_ToCanvas(seatPos, _uiCanvas);
|
||
Debug.Log($"[XingJiXunHang] 播放满屏暴击(他人,UI层) seat={seatPos} anchored={critRt.anchoredPosition}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("[XingJiXunHang] 播放满屏暴击(他人):Canvas 缺失,退回 UI 中心");
|
||
}
|
||
}
|
||
|
||
// ⭐ 取分数 TMP:优先按名字找数字节点(lua 也是按名字取 Text),回退第一个 TMP
|
||
Transform textNode = string.IsNullOrEmpty(critTextNodeName)
|
||
? null
|
||
: FindChildRecursive(critObj.transform, critTextNodeName);
|
||
var critTmp = textNode != null
|
||
? textNode.GetComponent<TMPro.TextMeshProUGUI>()
|
||
: critObj.GetComponentInChildren<TMPro.TextMeshProUGUI>(true);
|
||
|
||
if (critTmp != null)
|
||
{
|
||
// lua: "+" .. num(该 TMP 的 spriteAsset 已配好「数字→图片」映射)
|
||
critTmp.text = "+" + _bossPanelScore;
|
||
Debug.Log($"[XingJiXunHang] 暴击文字分数 = +{_bossPanelScore}(上限{maxScore})");
|
||
if (_bossPanelScore <= 0)
|
||
Debug.LogWarning("[XingJiXunHang] 暴击文字分数为 0:本次没有小鱼死亡累加(面板分仍为 0)");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[XingJiXunHang] 暴击文字 {critObj.name} 上没找到 TMP,分数未写入");
|
||
}
|
||
|
||
// ⭐ 修层级:预制体自带 Canvas → Default(0) + 高于 FullScreen(order 12),
|
||
// 否则暴击文字会被全屏特效框挡住(美术里的 -559336389 在本项目 TagManager 里不存在)
|
||
var critCanvas = critObj.GetComponent<Canvas>();
|
||
if (critCanvas == null)
|
||
critCanvas = critObj.GetComponentInChildren<Canvas>(true);
|
||
if (critCanvas != null)
|
||
{
|
||
critCanvas.overrideSorting = true;
|
||
critCanvas.sortingLayerID = 0; // Default(本项目只有 Default 一个排序层)
|
||
critCanvas.sortingOrder = 15; // 高于 FullScreen 的 12
|
||
Debug.Log($"[XingJiXunHang] 暴击文字 Canvas 层级修正: Default / order={critCanvas.sortingOrder}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[XingJiXunHang] 暴击文字 {critObj.name} 上没找到 Canvas,层级未修正");
|
||
}
|
||
|
||
// ⭐ 弹出动画 + 定时消失(原版有,之前完全没做 → 静止挂在屏幕中央直到爆奖结束)
|
||
PlayCritAnim(critObj, textNode);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 暴击文字弹出动画(对齐 lua CritTextEffect):
|
||
/// Text / Crit 从 scale 0 弹回原始 scale(OutElastic,Crit 延时 0.15s)
|
||
/// → 停留 critHoldDuration 秒 → 整体缩小到 0 并销毁。
|
||
/// ⭐ 弹回目标用"原始 scale":预制体里 Text 带美术缩放(如 1.727),直接弹到 1 会变小。
|
||
/// </summary>
|
||
private void PlayCritAnim(GameObject critObj, Transform textNode)
|
||
{
|
||
if (critObj == null) return;
|
||
|
||
if (textNode != null)
|
||
{
|
||
Vector3 target = textNode.localScale;
|
||
textNode.localScale = Vector3.zero;
|
||
textNode.DOScale(target, 0.3f).SetEase(Ease.OutElastic);
|
||
}
|
||
|
||
Transform critNode = string.IsNullOrEmpty(critNodeName)
|
||
? null
|
||
: FindChildRecursive(critObj.transform, critNodeName);
|
||
if (critNode != null)
|
||
{
|
||
Vector3 target = critNode.localScale;
|
||
critNode.localScale = Vector3.zero;
|
||
critNode.DOScale(target, 0.2f).SetEase(Ease.OutElastic).SetDelay(0.15f);
|
||
}
|
||
|
||
critObj.transform.DOScale(Vector3.zero, 0.2f)
|
||
.SetEase(Ease.InBack)
|
||
.SetDelay(Mathf.Max(0f, critHoldDuration))
|
||
.OnComplete(() =>
|
||
{
|
||
if (critObj != null)
|
||
{
|
||
// 已自行销毁 → 从统一销毁列表移除,避免 die() 时重复 Destroy
|
||
_spawnedEffects.Remove(critObj);
|
||
Destroy(critObj);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==================== 伽马风暴暴击框 ====================
|
||
|
||
/// <summary>
|
||
/// 播放伽马风暴暴击框(PW_Effect_Crit_GammaStorm)。
|
||
/// 仅 weaponID=7 (GammaStorm) 有效。
|
||
/// </summary>
|
||
public void PlayGammaStormCritFrame()
|
||
{
|
||
if (_weaponTag != "GammaStorm" || critFrameGammaStorm == null) return;
|
||
|
||
// ⭐ 伽马暴击框是 UI → 放 UI 中心,加入统一销毁列表
|
||
SpawnUiEffect(critFrameGammaStorm);
|
||
Debug.Log("[XingJiXunHang] 播放伽马风暴暴击框(UI中心)");
|
||
}
|
||
|
||
// ==================== 辅助方法 ====================
|
||
|
||
/// <summary>每死亡一个飞机,就往 Boss 分数面板上加一个分(面板独立于 Boss 本体,删除后仍有效)</summary>
|
||
private void AddBossPanelScore(long amount)
|
||
{
|
||
if (amount <= 0) return;
|
||
|
||
// ⭐ 面板加分不能超过金币上限 maxScore(封顶)
|
||
long newScore = _bossPanelScore + amount;
|
||
if (newScore > maxScore)
|
||
newScore = maxScore;
|
||
if (newScore == _bossPanelScore) return; // 已封顶,无变化
|
||
|
||
_bossPanelScore = newScore;
|
||
EnsureBossScoreTmp();
|
||
if (_bossScoreTmp != null)
|
||
{
|
||
_bossScoreTmp.text = _bossPanelScore.ToString();
|
||
Debug.Log($"[XingJiXunHang] Boss分数面板 +{amount} = {_bossPanelScore}(上限{maxScore})");
|
||
}
|
||
}
|
||
|
||
/// <summary>面板分是否已加满(= maxScore 后停止杀鱼)</summary>
|
||
private bool BossScoreFull => _bossPanelScore >= maxScore;
|
||
|
||
/// <summary>确保 Boss 分数面板 TMP 可用(优先用 bossCtrl 已创建的,否则手动创建)</summary>
|
||
private void EnsureBossScoreTmp()
|
||
{
|
||
if (_bossScoreTmp != null) return;
|
||
|
||
// 1. 从 bossCtrl 拿(Boss 删除前已缓存的面板引用)
|
||
if (bossCtrl != null && bossCtrl.tMP_CurrBossScore != null)
|
||
{
|
||
_bossScoreTmp = bossCtrl.tMP_CurrBossScore;
|
||
return;
|
||
}
|
||
|
||
// 2. ⭐ 用当前武器对应的奖励框作为加分面板(详细指定出的分数预制体),
|
||
// 不再使用 Manager 默认的 BJEventBase 分数预制体(dieFXPrefabAwardTmp)
|
||
var prefab = _awardFramePrefab;
|
||
var parent = CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null
|
||
? CreateFishManager.Instance.currGameType.subPanelParent : null;
|
||
if (prefab == null || parent == null) return;
|
||
|
||
var board = Instantiate(prefab, parent);
|
||
// ⭐ 加分面板位置放在座位上方才是正常的
|
||
board.transform.position = GetSeatPosition() + Vector3.up * 150f;
|
||
|
||
// 从 BJImage 拿加分 TMP(指向奖励框自身的 Num),并按 isSelf 应用金/银 sprite 样式
|
||
var bjImage = board.GetComponentInChildren<BJImage>();
|
||
if (bjImage != null && bjImage.tmp_CoinNum != null)
|
||
{
|
||
_bossScoreTmp = bjImage.tmp_CoinNum;
|
||
_bossScoreTmp.text = "0";
|
||
var spriteAsset = isSelf ? bjImage.goldSprite : bjImage.silverSprite;
|
||
if (spriteAsset != null)
|
||
_bossScoreTmp.spriteAsset = spriteAsset;
|
||
}
|
||
else
|
||
{
|
||
_bossScoreTmp = board.GetComponentInChildren<TMPro.TextMeshProUGUI>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// ⭐ 给结算特效上的 BJEndUI 写入爆奖分数(倍数 + 最终总分)。
|
||
///
|
||
/// 结算预制体(PW_Effect_Settle_*_XingJiXunHang,Art/PW_Effect_Settle_* 的变体)
|
||
/// 根节点上挂了 BJEndUI,tMP_beishu / tMP_score 已由美术接好,代码只需 Init:
|
||
/// isSelf → 面板挂在 UI 层、屏幕中心(SpawnUiEffect);他人 → 击杀者座位上方(银币样式)。
|
||
/// 不写会一直显示预制体里的占位文本,所以爆奖结束前必须 Init 一次。
|
||
/// </summary>
|
||
/// <param name="settleObj">已实例化的结算特效对象(SpawnUiEffect / SpawnWorldEffect 的返回值)</param>
|
||
private void InitSettleBjEndUI(GameObject settleObj)
|
||
{
|
||
if (settleObj == null) return;
|
||
|
||
// 优先根节点(预制体挂在根上),回退子节点(防止后续挪到子物体)
|
||
var endUI = settleObj.GetComponent<BJEndUI>();
|
||
if (endUI == null)
|
||
endUI = settleObj.GetComponentInChildren<BJEndUI>(true);
|
||
|
||
if (endUI == null)
|
||
{
|
||
Debug.LogWarning($"[XingJiXunHang] 结算特效 {settleObj.name} 上未找到 BJEndUI,跳过分数写入");
|
||
return;
|
||
}
|
||
|
||
// ⭐ 空安全:TMP 引用全空(预制体没接)时不调 Init,避免空引用
|
||
if (endUI.tMP_beishu == null && endUI.tMP_score == null)
|
||
{
|
||
Debug.LogWarning($"[XingJiXunHang] 结算面板 BJEndUI 未接 tMP_beishu/tMP_score({settleObj.name}),跳过分数写入");
|
||
return;
|
||
}
|
||
|
||
// ⭐ 分数 = maxScore(服务端下发的爆奖最终总分),与基类 ShowBJEndUI 保持一致;
|
||
// 他人视角传 isSelf=false → 银币精灵
|
||
endUI.Init(bJbeishu, maxScore, isSelf);
|
||
Debug.Log($"[XingJiXunHang] 结算面板 BJEndUI 写入分数: 倍数={bJbeishu} 分数={maxScore} " +
|
||
$"isSelf={isSelf} obj={settleObj.name}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 缓存 UI 层父节点 + Canvas:
|
||
/// 父节点 = currGameType.subPanelParent.parent(与爆奖预制体同层,随 UI 卸载自动清理)
|
||
/// Canvas = UIManager.uiRoot 上的 Canvas
|
||
/// </summary>
|
||
private void EnsureUiLayer()
|
||
{
|
||
if (_uiLayer != null && _uiCanvas != null) return;
|
||
|
||
if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null)
|
||
{
|
||
var subPanel = CreateFishManager.Instance.currGameType.subPanelParent;
|
||
if (subPanel != null)
|
||
_uiLayer = subPanel.parent;
|
||
}
|
||
|
||
if (UIManager.Instance != null && UIManager.Instance.uiRoot != null)
|
||
_uiCanvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
|
||
|
||
if (_uiCanvas == null && _uiLayer != null)
|
||
_uiCanvas = _uiLayer.GetComponentInParent<Canvas>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// ⭐ 在 UI 层实例化 UI 类特效(FullScreen/AwardFrame/Settle/Crit 等),放到 UI 中心。
|
||
/// 不再放世界坐标 (0,0)。自动加入 _spawnedEffects,die() 时统一销毁。
|
||
/// </summary>
|
||
private GameObject SpawnUiEffect(GameObject prefab)
|
||
{
|
||
if (prefab == null) return null;
|
||
|
||
EnsureUiLayer();
|
||
if (_uiLayer == null || _uiCanvas == null)
|
||
{
|
||
// 兜底:UI 层拿不到才放世界层(原逻辑)
|
||
var fallback = Instantiate(prefab, Vector3.zero, Quaternion.identity,
|
||
CreateFishManager.Instance.bjEffectRoot);
|
||
_spawnedEffects.Add(fallback);
|
||
return fallback;
|
||
}
|
||
|
||
var obj = Instantiate(prefab, _uiLayer);
|
||
var rt = obj.GetComponent<RectTransform>();
|
||
if (rt != null)
|
||
{
|
||
rt.anchoredPosition = CoordinateTransformation.ScreenPoint_ToCanvas(
|
||
new Vector2(Screen.width / 2f, Screen.height / 2f), _uiCanvas);
|
||
}
|
||
|
||
_spawnedEffects.Add(obj);
|
||
return obj;
|
||
}
|
||
|
||
/// <summary>在世界层实例化 2D 类特效(道具出场/道具动画),保持世界坐标,加入统一销毁列表</summary>
|
||
private GameObject SpawnWorldEffect(GameObject prefab, Vector3 pos)
|
||
{
|
||
if (prefab == null) return null;
|
||
|
||
var obj = Instantiate(prefab, pos, Quaternion.identity,
|
||
CreateFishManager.Instance.bjEffectRoot);
|
||
_spawnedEffects.Add(obj);
|
||
return obj;
|
||
}
|
||
|
||
/// <summary>递归查找子物体(⭐ 忽略大小写,防预制体命名 laser1/Laser1 不一致)</summary>
|
||
private Transform FindChildRecursive(Transform parent, string name)
|
||
{
|
||
if (parent.name == name)
|
||
return parent;
|
||
|
||
for (int i = 0; i < parent.childCount; i++)
|
||
{
|
||
var result = FindChildRecursive(parent.GetChild(i), name);
|
||
if (result != null) return result;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>递归查找子物体(忽略大小写版本,找不到再试精确)</summary>
|
||
private Transform FindChildIgnoreCase(Transform parent, string name)
|
||
{
|
||
if (string.Equals(parent.name, name, System.StringComparison.OrdinalIgnoreCase))
|
||
return parent;
|
||
|
||
for (int i = 0; i < parent.childCount; i++)
|
||
{
|
||
var result = FindChildIgnoreCase(parent.GetChild(i), name);
|
||
if (result != null) return result;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>物体移动协程</summary>
|
||
private IEnumerator MoveTo(Transform target, Vector3 destination, float duration)
|
||
{
|
||
if (target == null) yield break;
|
||
|
||
Vector3 start = target.position;
|
||
float elapsed = 0f;
|
||
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
float t = elapsed / duration;
|
||
target.position = Vector3.Lerp(start, destination, t);
|
||
yield return null;
|
||
}
|
||
|
||
target.position = destination;
|
||
}
|
||
|
||
/// <summary>获取屏幕左下角世界坐标</summary>
|
||
private Vector3 GetScreenBottomLeft()
|
||
{
|
||
var cam = Camera.main;
|
||
if (cam != null)
|
||
return cam.ScreenToWorldPoint(new Vector3(0, 0, cam.transform.position.z * -1f));
|
||
return new Vector3(-5, -3, 0);
|
||
}
|
||
|
||
/// <summary>获取屏幕左边世界坐标</summary>
|
||
private Vector3 GetScreenLeft()
|
||
{
|
||
var cam = Camera.main;
|
||
if (cam != null)
|
||
return cam.ScreenToWorldPoint(new Vector3(0, Screen.height * 0.5f, cam.transform.position.z * -1f));
|
||
return new Vector3(-5, 0, 0);
|
||
}
|
||
|
||
/// <summary>获取屏幕右边世界坐标</summary>
|
||
private Vector3 GetScreenRight()
|
||
{
|
||
var cam = Camera.main;
|
||
if (cam != null)
|
||
return cam.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height * 0.5f, cam.transform.position.z * -1f));
|
||
return new Vector3(5, 0, 0);
|
||
}
|
||
|
||
/// <summary>获取屏幕顶部世界坐标</summary>
|
||
private Vector3 GetScreenTop()
|
||
{
|
||
var cam = Camera.main;
|
||
if (cam != null)
|
||
return cam.ScreenToWorldPoint(new Vector3(Screen.width * 0.5f, Screen.height, cam.transform.position.z * -1f));
|
||
return new Vector3(0, 5, 0);
|
||
}
|
||
|
||
/// <summary>获取屏幕底部世界坐标</summary>
|
||
private Vector3 GetScreenBottom()
|
||
{
|
||
var cam = Camera.main;
|
||
if (cam != null)
|
||
return cam.ScreenToWorldPoint(new Vector3(Screen.width * 0.5f, 0, cam.transform.position.z * -1f));
|
||
return new Vector3(0, -5, 0);
|
||
}
|
||
|
||
// ==================== 销毁 ====================
|
||
|
||
/// <summary>
|
||
/// 回收爆奖预制体 + 回收 Boss 本体 + 统一销毁所有临时特效。
|
||
/// </summary>
|
||
public override void die()
|
||
{
|
||
CleanupBeforeDie();
|
||
|
||
// ⭐ 爆奖结束:统一销毁所有临时特效(FullScreen/AwardFrame/Settle/Crit/道具动画等)
|
||
// 防止 PW_Effect_AwardFrame_XXX(Clone) / PW_Effect_Settle_XXX(Clone) 残留
|
||
for (int i = 0; i < _spawnedEffects.Count; i++)
|
||
{
|
||
if (_spawnedEffects[i] != null)
|
||
Destroy(_spawnedEffects[i]);
|
||
}
|
||
_spawnedEffects.Clear();
|
||
|
||
if (bossCtrl != null)
|
||
{
|
||
// ⭐ Boss 本体已在最终死亡(isKill=true)时由 XingJiXunHang_BOSS 直接删除,
|
||
// 这里不需要再调 Id_DieFish_NotFX(重复调用会报"找不到鱼"警告)
|
||
bossCtrl.NotifyBossRemoved();
|
||
bossCtrl = null;
|
||
}
|
||
|
||
EffectPool.Instance.Return(gameObject);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加分完成:先走基类(显示 BJEndUI + 加分 + 收尾),
|
||
/// 然后删除/回收手动创建的 Boss 分数面板(_bossScoreTmp)。
|
||
/// </summary>
|
||
protected override void OnScoreEnd()
|
||
{
|
||
base.OnScoreEnd(); // ⭐ 弹出 BJEndUI
|
||
|
||
// ⭐ 弹出 BJEndUI 时回收/删除手动创建的 Boss 分数面板
|
||
if (_bossScoreTmp != null)
|
||
{
|
||
var board = _bossScoreTmp.transform.parent;
|
||
if (board != null)
|
||
Destroy(board.gameObject);
|
||
_bossScoreTmp = null;
|
||
Debug.Log("[XingJiXunHang] 弹出 BJEndUI:Boss 分数面板已删除");
|
||
}
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
StopAllCoroutines();
|
||
}
|
||
}
|