using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Spine.Unity;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// 炮台控制模式
public enum TurretControlMode
{
/// 普通模式:动态克隆炮台,显示金币+钻石,按钮打开面板切换
Normal = 0,
/// 星际迷航模式:座位子对象激活/停用,只显示金币,长按炮台弹出扇形切换
XJMH = 1
}
public class SitPosData : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
public GameObject spineObj;//他的子对象child0带SkeletonGraphic
public GameObject ziObj;//他的子对象child0带textmeshprougui
public Button btn_SelectPaoBei; //点击打开关闭选择面板
public GameObject selectPaoBeiPanel; //他的出现效果是从上往下滑动的动画效果 关闭是从下往上滑动的动画效果 所称height变成0和还原那种的时间0.5s
public GameObject paobeiItemPrefab;//自己的gameobject上面有image组件和button组件 选择后直接关闭 然后他的getchild0上有个textmeshprougui 5000以下的炮倍是蓝色的 5000-10000是紫色 10000以上是红色的 黄色是选中状态的
public Transform paobeiItemParent;
public Sprite redBG;
public Sprite purpleBG;
public Sprite blueBG;
public Sprite yellowBG;
[Tooltip("自动攻击开关(由 GameMainPanel 的 toggle_SkillAuto 移植):开着=自动扫射+自动开火;关闭时本地座位炮台切回 Idle 并触发 OnAutoAttackOff")]
public Toggle toggle_AutoAttack;
/// 自动攻击从开→关时触发一次(GameMainPanel 订阅后广播 Idle 状态给其他客户端,仅本地玩家座位会触发)
public event Action OnAutoAttackOff;
/// 自动攻击是否开启(toggle 没拖引用时恒为 false,等同未开自动攻击)
public bool IsAutoAttackOn => toggle_AutoAttack != null && toggle_AutoAttack.isOn;
[Tooltip("长按判定时间(秒):按住 img_OpenPT 超过此时间触发 OnOpenPTLongPress")]
public float openPTLongPressTime = 0.5f;
public Image img_OpenPT;
/// 长按 img_OpenPT 触发一次(GameMainPanel 订阅后用它开关炮台面板,仅本地玩家座位有意义)
public event Action OnOpenPTLongPress;
private bool _openPTPressing;
private float _openPTPressTimer;
public bool IsBaoJiangingActive;
public Transform obj_BaoJiangZhongPanelPosition;
/// 本地玩家的座位引用(由游戏面板初始化时设置)
public static SitPosData localPlayerSit;
/// 当前房间房主座位(人机请求由房主发送,房主退出后自动继承)
public static SitPosData roomOwnerSit;
// -------------------------
// 玩家基础信息
// -------------------------
[Header("玩家信息")]
public string user;
[Tooltip("座位编号 0~3。0,1=下方 2,3=上方")]
public int posID;
[Tooltip("是否为人机")]
public bool isBot = false;
/// posID>=2 = 上方,需要翻转(0,1=下方;2,3=上方)
public bool IsFlipped => posID >= 2;
[Header("金币")]
public long goldNum;
public long consumeGoldNum;
public TextMeshProUGUI tMP_GoldNum;
public TextMeshProUGUI tMP_DiamondNum;
public TextMeshProUGUI tMP_TurretConsumeNum;
[Header("翅膀")]
[Tooltip("翅膀实例化的父节点")]
public Transform wingParent;
[Header("战神")]
[Tooltip("战神实例化的父节点")]
public Transform zhanShenParent;
[Tooltip("当前装备的战神编号(0=无战神)。炮台/战神显示二选一:有战神时 turretParent 隐藏")]
public int currentZhanShenNumber;
[Header("特殊鱼炮台")]
[Tooltip("特殊鱼炮台实例化的父节点")]
public Transform tSYPTParent;
[Tooltip("特殊鱼炮台(钻头)是否正在使用:true=期间禁用普通开火,点击只发射钻头")]
public bool isDrillActive;
[Tooltip("激光锁定:true=激光发射期间炮台锁定方向,SetRotation 不生效(原方向打到爆奖结束)")]
public bool isLaserLocked;
///
/// 散射激光模式中(GameMainPanel 每帧刷新:三发+锁定+已锁目标)。
/// ⭐ 激活左右起点球父对象用这个标记,不依赖实际锁定点数量(目标不足2个时散射模式开着也要显示起点球)
///
[System.NonSerialized]
public bool isScatterLaserOn;
// -------------------------
// 炮台管理(动态克隆)
// -------------------------
[Header("炮台")]
[Tooltip("炮台实例化的父节点")]
public Transform turretParent;
public Transform turretRotate;
[Header("炮台控制模式")]
[Tooltip("Normal=普通模式(动态克隆炮台,显示金币+钻石,按钮切换); XJMH=星际迷航模式(座位子对象激活/停用,只显示金币,长按扇形切换)")]
public TurretControlMode turretControlMode = TurretControlMode.Normal;
[Tooltip("炮台锚点位置(相对 turretParent)")]
public Vector2 turretAnchoredPos = Vector2.zero;
[Tooltip("当前装备的炮台编号")]
public int currentTurretNumber;
/// 当前运行中的炮台实例
public TurretData currentTurret;
/// 当前运行中的战神实例(null=未显示)。炮台/战神显示二选一:有战神时 turretParent 隐藏
[System.NonSerialized]
public GameObject currentZhanShen;
/// 战神实例上的 TurretData/ZhanShenTurretData(开火时优先取战神的子弹预制体,null=无战神或未挂组件)
[System.NonSerialized]
public TurretData currentZhanShenData;
// -------------------------
// 战神激光(锁定时显示):两种形态
// - laserPrefab 非空 → 预制体形态:JiGuangControl 工具类驱动(克隆激光预制体,方向/长度每帧刷新)
// - laserPrefab 为空 → LineRenderer 兜底形态:直接新建 GameObject + LineRenderer
// -------------------------
/// LineRenderer 形态激光列表。三发锁定=3条。起点:0=主激光(座位点+selfOffsetJGStart),1/2=散射左右(JGFX父对象位置)
private readonly List _zhanShenLasers = new List();
/// 预制体形态激光实例列表(世界空间,由 JiGuangControl.JiGuangCreate 按目标数量重建)
private readonly List _laserFx = new List();
/// 激光起点特效实例(UI 层:0=挂 turretRotate 下 selfOffsetJGStart 处,1/2=挂 JGFX 左右父对象下,跟随父节点)
private readonly List _laserStartFx = new List();
/// 激光命中特效实例(与 _laserFx 一一对应,世界空间 Z=-50 层,跟随锁定目标)
private readonly List _laserHitFx = new List();
/// 激光准备等待剩余秒数:锁定刚建立时读 ZhanShenTurretData.laserFXDelay,>0 期间激光及特效隐藏(等准备动画播完)
private float _laserPrepWait;
/// 上一帧是否有锁定点(检测锁定建立沿,触发准备等待)
private bool _laserLockActive;
/// 预制体形态激光的基准缩放(重建时取自 laserPrefab.transform.localScale)
private Vector3 _laserFxBaseScale = Vector3.one;
/// 预制体形态激光的角度偏移:激光预制体自带的 Z 轴旋转(重建时缓存,绕世界 Z 叠加在计算朝向上)
private float _laserFxRotZOffset;
/// 每条激光的起点缓存(索引与 _laserTargets 对齐,每帧复用避免 GC)
private readonly List _laserStarts = new List();
// -------------------------
// XJMH 炮台列表(星际迷航专用)
// -------------------------
[Header("XJMH 炮台列表")]
[Tooltip("XJMH 模式专用:在 Inspector 中拖入座位下的所有炮台子对象(带 TurretData 组件)。" +
"数量不多,建议按炮台编号升序排列,便于扇形切换按索引选择。")]
public List xjmhTurretList = new List();
// 状态机入口(GameMainPanel 调用)
public Action OnStateChange;
// -------------------------
// 炮台旋转
// -------------------------
[Header("炮台旋转")]
public Transform tra_CurrTurret;
public float currentAngle;
///
/// 炮台在世界坐标(Z=0平面)的位置缓存。
/// 因为炮台是 UGUI 元素,tra_CurrTurret.position 是 Canvas 坐标,不能直接和鱼的世界坐标比较距离。
/// 这里用和子弹起点(GetBulletStartWorldPos)相同的转换方式,把 UI 坐标转成世界坐标。
/// 由 LoadTurret 加载完炮台后计算并缓存,bot AI/锁定检测距离时用此字段。
///
[System.NonSerialized]
public Vector3 turretWorldPos;
// -------------------------
// 锁定系统
// -------------------------
[Header("锁定系统")]
public MonsterContral monCotl_Lock;
public Transform monCotl_Lock_Position;
public bool isLock = false;
///
/// ⭐ 当前锁定是否为玩家手动锁定。
/// - true:玩家点击锁定的鱼 → 不受路径进度(0.1~0.9)限制,鱼离场前一直锁定
/// - false:自动锁定/三发查找/bot → 受路径进度限制
/// 由 GameMainPanel_FishBase 在 SetLockTarget 前设置
///
[System.NonSerialized]
public bool isManualLock = false;
public GameObject lockFX; // 主锁定图标(单发/双倍用)
public List lockFX_Triple = new List(); // 三发锁定额外2个图标
public List tripleLockTargets = new List();
public List tripleLockPositions = new List();
// -------------------------
// UI
// -------------------------
[Header("UI")]
public Button btn_TurretAdd;
public Button btn_TurretMinus;
[Header("激光效果父对象")]
[Tooltip("散射激光左起点球的父对象:出场/换战神时克隆 ZhanShenTurretData.JGQiuStartPrefab 到它下面,散射激光激活时显示。不拖也没关系:Awake 会自动创建兜底节点")]
public Transform JGFXLeftParent;
[Tooltip("散射激光右起点球的父对象:出场/换战神时克隆 ZhanShenTurretData.JGQiuStartPrefab 到它下面,散射激光激活时显示。不拖也没关系:Awake 会自动创建兜底节点")]
public Transform JGFXRightParent;
private void Awake()
{
if (btn_TurretAdd != null)
btn_TurretAdd.onClick.AddListener(() => ChangeTurretAddConsumeNum());
if (btn_TurretMinus != null)
btn_TurretMinus.onClick.AddListener(() => ChangeTurretMinusConsumeNum());
// ⭐ 自动攻击开关:关闭时本地座位炮台切回 Idle(原 GameMainPanel 的 toggle_SkillAuto 监听逻辑)
if (toggle_AutoAttack != null)
toggle_AutoAttack.onValueChanged.AddListener(OnAutoAttackToggleChanged);
// ⭐ 炮倍选择面板:按钮开/关;Awake 记录面板原始高度(动画还原目标值,之后会被压到 0)
if (btn_SelectPaoBei != null)
btn_SelectPaoBei.onClick.AddListener(OnClickSelectPaoBei);
EnsurePaoBeiPanelHeight();
// ⭐ 散射起点球父对象兜底:预制体没拖引用就自动创建(挂 beij 下,炮台两侧),
// 保证 UpdateJGFXParentsActive 永远有对象可激活,不依赖 Inspector 配置。
// XJMH(星际迷航)没有战神/激光概念,跳过不给它加节点。
if (turretControlMode != TurretControlMode.XJMH)
EnsureJGFXParents();
// XJMH 模式:隐藏钻石显示(只显示金币)
if (turretControlMode == TurretControlMode.XJMH && tMP_DiamondNum != null)
tMP_DiamondNum.gameObject.SetActive(false);
// XJMH 模式:炮台是座位自带子对象,预制体里已摆好,不需要加载。
// 这里只做引用初始化,不做激活/停用。
if (turretControlMode == TurretControlMode.XJMH)
InitXJMHTurret();
}
///
/// 自动攻击开关变化:关闭时本地座位炮台立即切回 Idle(自动开火停止)。
/// ⭐ 只对本地玩家座位生效:同一个 toggle 被拖到多个座位时,非本地座位不响应,
/// 避免远程座位的状态机被本地点击误切。
///
private void OnAutoAttackToggleChanged(bool on)
{
if (on) return;
if (this != localPlayerSit) return;
ChangeState(TurretState.Idle);
OnAutoAttackOff?.Invoke();
}
///
/// 散射起点球父对象兜底:Inspector 没拖引用时自动创建两个 RectTransform
/// (挂在 turretRotate 的父节点 beij 下,炮台左右两侧 ±50),并写回字段。
/// 拖了引用(预制体配置的座位)则什么都不做。
///
private void EnsureJGFXParents()
{
if (JGFXLeftParent != null && JGFXRightParent != null) return;
// 挂载点:turretRotate 的父节点(beij);拿不到就挂座位根
Transform attach = turretRotate != null && turretRotate.parent != null
? turretRotate.parent
: transform;
if (JGFXLeftParent == null)
JGFXLeftParent = CreateJGFXParent(attach, "JGFXLeftParent", -50f);
if (JGFXRightParent == null)
JGFXRightParent = CreateJGFXParent(attach, "JGFXRightParent", 50f);
}
/// 创建一个散射起点球父节点(beij 下,x 偏移 ±50、y 100,默认隐藏)
private static RectTransform CreateJGFXParent(Transform parent, string name, float xOffset)
{
GameObject go = new GameObject(name, typeof(RectTransform));
RectTransform rt = (RectTransform)go.transform;
rt.SetParent(parent, false);
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.sizeDelta = new Vector2(44f, 37f);
rt.anchoredPosition = new Vector2(xOffset, 100f);
go.SetActive(false);
return rt;
}
///
/// XJMH 模式:炮台是座位自带子对象,预制体里已摆好激活状态,不需要 LoadTurret 加载。
/// 此方法只做引用初始化:从 取第一个激活的炮台赋给 currentTurret,
/// 并绑定旋转节点、状态机入口、世界坐标缓存。不做任何激活/停用操作。
///
private void InitXJMHTurret()
{
if (xjmhTurretList == null || xjmhTurretList.Count == 0)
{
Debug.LogWarning($"[SitPosData] XJMH 炮台列表未配置,座位 {posID} 无炮台可用");
return;
}
// 优先取列表中第一个激活的炮台;若全部未激活,取 list[0]
TurretData found = null;
foreach (var td in xjmhTurretList)
{
if (td == null) continue;
if (td.gameObject.activeSelf)
{
found = td;
break;
}
}
if (found == null)
{
found = xjmhTurretList[0];
found.gameObject.SetActive(true);
}
currentTurret = found;
currentTurretNumber = found.turretNumber;
// 缓存旋转节点:优先用 TurretData.tra_RotationPivot,否则回退父节点
tra_CurrTurret = GetTurretRotationPivot(currentTurret);
OnStateChange = currentTurret.ChangeState;
UpdateTurretWorldPos();
Debug.Log($"[SitPosData] XJMH 炮台引用已初始化: #{currentTurretNumber}");
}
private void OnEnable()
{
// 默认显示等待界面
ShowWaiting();
}
private void OnDisable()
{
if (localPlayerSit == this)
localPlayerSit = null;
// 房主退出 → 自动继承给第一个非人机玩家
if (roomOwnerSit == this)
{
roomOwnerSit = null;
TransferRoomOwner();
}
// 座位禁用时清理战神激光(世界空间对象,不随座位隐藏)
DestroyZhanShenLaser();
// 散射起点父对象藏起来(子对象保留,下次出场/换战神时重建)
if (JGFXLeftParent != null)
JGFXLeftParent.gameObject.SetActive(false);
if (JGFXRightParent != null)
JGFXRightParent.gameObject.SetActive(false);
}
private void Update()
{
UpdateZhanShenLaser();
// ⭐ 长按 img_OpenPT 计时:到达阈值触发一次(提前松手不触发)
if (_openPTPressing)
{
_openPTPressTimer += Time.deltaTime;
if (_openPTPressTimer >= openPTLongPressTime)
{
_openPTPressing = false;
_openPTPressTimer = 0f;
OnOpenPTLongPress?.Invoke();
}
}
}
// ==================== 长按 img_OpenPT(打开炮台面板) ====================
/// 按下:命中 img_OpenPT(或其子对象)才开始长按计时(其他区域不响应)
public void OnPointerDown(PointerEventData eventData)
{
if (img_OpenPT == null) return;
var hit = eventData.pointerCurrentRaycast.gameObject;
if (hit == null) return;
if (hit != img_OpenPT.gameObject && !hit.transform.IsChildOf(img_OpenPT.transform)) return;
_openPTPressing = true;
_openPTPressTimer = 0f;
}
/// 松开:取消长按计时(未达阈值不触发)
public void OnPointerUp(PointerEventData eventData)
{
_openPTPressing = false;
_openPTPressTimer = 0f;
}
// ==================== 战神激光(锁定时显示) ====================
///
/// 战神锁定时显示激光,Z 统一压到 -50 显示层。
/// ⭐ 一个锁定点一条激光:单发锁定=1条,三发锁定=3条(主目标+2个三发目标)。
/// ⭐ 每条激光独立起点:第0条(主锁定)=turretRotate 子对象偏移 selfOffsetJGStart(⭐跟随炮台旋转),
/// 第1/2条(散射左右)=JGFXLeftParent/JGFXRightParent 的位置(座位 UI 上的起点球父对象)。
/// ⭐ 使用散射激光时激活 JGFXLeftParent/JGFXRightParent 显示起点球,否则隐藏:
/// 本地座位=三发+锁定开着(isScatterLaserOn,面板每帧刷新);远程/bot座位=锁定点>1(三发目标凑满)。
/// ⭐ 两种形态:laserPrefab 非空 → 预制体形态(JiGuangControl 工具类);为空 → LineRenderer 兜底形态。
/// ⭐ 只是视觉表现:打中鱼仍按正常攻击频率由子弹完成(HandleAutoFire 以 attackTime 发射锁定子弹)。
/// 普通炮台没有此效果(currentZhanShenData 为 null 时不显示)。
///
private void UpdateZhanShenLaser()
{
// 收集所有有效锁定点:主目标 + 三发锁定的额外目标
_laserTargets.Clear();
if (currentZhanShenData != null && isLock)
{
if (monCotl_Lock_Position != null && monCotl_Lock_Position.gameObject.activeInHierarchy)
_laserTargets.Add(monCotl_Lock_Position);
// 三发锁定:每个有效目标一条激光(主目标已在上面,这里只补额外的)
if (tripleLockTargets.Count > 0)
{
for (int i = 0; i < tripleLockTargets.Count && i < tripleLockPositions.Count; i++)
{
var pos = tripleLockPositions[i];
var mon = tripleLockTargets[i];
// 主目标(monCotl_Lock)已在上面加过,跳过避免重复
if (mon == monCotl_Lock) continue;
if (mon == null || mon.isDead || pos == null || !pos.gameObject.activeInHierarchy) continue;
_laserTargets.Add(pos);
}
}
}
// ⭐ 准备动画等待:锁定刚建立时读 ZhanShenTurretData.laserFXDelay,等待期间激光及全部特效
// (光束/起点球/起点特效/命中特效)先不显示,等战神摆姿势动画播完再出现;锁定丢失后重新锁定会重新等待
bool lockActive = _laserTargets.Count > 0;
if (lockActive && !_laserLockActive)
{
var zsDelay = currentZhanShenData as ZhanShenTurretData;
_laserPrepWait = zsDelay != null ? zsDelay.laserFXDelay : 0f;
}
_laserLockActive = lockActive;
if (_laserPrepWait > 0f)
{
_laserPrepWait -= Time.deltaTime;
lockActive = false; // 等待期间当作未锁定:激光和特效全部隐藏
}
// ⭐ 散射激光使用中就激活左右起点父对象显示起点球:
// isScatterLaserOn=三发+锁定开着(面板每帧刷新,本地座位);锁定点>1=三发目标已凑满(远程/bot座位)
// 两个条件满足其一即激活——散射模式开着但目标不足时也要显示起点球
UpdateJGFXParentsActive(lockActive && (_laserTargets.Count > 1 || isScatterLaserOn));
#if UNITY_EDITOR
DebugJGFXState();
#endif
// 没有锁定点(或准备动画等待中):隐藏所有激光
if (!lockActive)
{
HideZhanShenLasers();
return;
}
// 按配置走对应形态:laserPrefab 非空=预制体形态,为空=LineRenderer 兜底
if (currentZhanShenData.laserPrefab != null)
UpdateZhanShenLaserFx();
else
UpdateZhanShenLaserLine();
}
///
/// 战神 loop 动画开始通知(TurretData 转发:ZhanShenLaserLoopSMB 状态机行为 / 剪辑动画事件):
/// 立即放行激光及全部特效显示(提前结束 laserFXDelay 等待;未在等待中则无副作用)。
///
public void NotifyZhanShenLaserLoopAnim()
{
_laserPrepWait = 0f;
}
/// 隐藏全部战神激光(两种形态都处理)
private void HideZhanShenLasers()
{
for (int i = 0; i < _zhanShenLasers.Count; i++)
{
if (_zhanShenLasers[i] != null && _zhanShenLasers[i].gameObject.activeSelf)
_zhanShenLasers[i].gameObject.SetActive(false);
}
for (int i = 0; i < _laserFx.Count; i++)
{
if (_laserFx[i] != null && _laserFx[i].gameObject.activeSelf)
_laserFx[i].gameObject.SetActive(false);
}
// 起点/命中特效一并隐藏
for (int i = 0; i < _laserStartFx.Count; i++)
{
if (_laserStartFx[i] != null && _laserStartFx[i].activeSelf)
_laserStartFx[i].SetActive(false);
}
for (int i = 0; i < _laserHitFx.Count; i++)
{
if (_laserHitFx[i] != null && _laserHitFx[i].activeSelf)
_laserHitFx[i].SetActive(false);
}
}
///
/// 预制体形态(laserPrefab 非空):JiGuangControl 工具类驱动。
/// 目标数量变化时全量重建(JiGuangCreate),每帧刷新位置/朝向/长度(JiGuangDirection/JiGuangLength,Z 强制 -50)。
/// ⭐ 每条激光独立起点(多起点重载):0=主锁定(座位点+偏移),1/2=散射左右(JGFX父对象位置)。
///
private void UpdateZhanShenLaserFx()
{
var zsData = currentZhanShenData;
// 收集每条激光的起点(与 _laserTargets 一一对应)
_laserStarts.Clear();
for (int i = 0; i < _laserTargets.Count; i++)
_laserStarts.Add(GetZhanShenLaserStart(i));
// 目标数量变化时全量重建(锁定建立/解除/三发目标增减时触发),并刷新基准缩放 + 预制体初始旋转偏移
if (_laserFx.Count != _laserTargets.Count)
{
_laserFxBaseScale = zsData.laserPrefab.transform.localScale;
// ⭐ 预制体自带的 Z 轴旋转:在激光预制体上转 Z = 激光平面内旋转(X/Y 不参与,防止甩出平面)
_laserFxRotZOffset = zsData.laserPrefab.transform.localEulerAngles.z;
JiGuangControl.JiGuangCreate(zsData.laserPrefab, _laserStarts, _laserTargets, _laserFx, null, -50f);
// ⭐ 起点/命中特效随激光全量重建(ZhanShenTurretData 上没拖预制体则不生成)
RebuildZhanShenLaserPointFx();
}
// 起点需每帧同步(座位布局/起点球位置变化);隐藏中的重新激活
for (int i = 0; i < _laserFx.Count; i++)
{
if (_laserFx[i] == null) continue;
if (!_laserFx[i].gameObject.activeSelf)
_laserFx[i].gameObject.SetActive(true);
if (i < _laserStarts.Count)
_laserFx[i].position = _laserStarts[i];
}
// 朝向 + 长度(多起点重载:起终点同层 Z=-50;朝向绕世界 Z 叠加预制体 Z 轴旋转偏移)
JiGuangControl.JiGuangDirection(_laserStarts, _laserTargets, _laserFx, -50f, _laserFxRotZOffset);
JiGuangControl.JiGuangLength(_laserStarts, _laserTargets, _laserFx, _laserFxBaseScale, zsData.laserLengthCorrect, -50f);
// ⭐ 起点特效挂 UI 起点节点下(位置由父节点决定自动跟随,无需刷),这里只负责重新激活;命中特效每帧跟随目标
for (int i = 0; i < _laserStartFx.Count; i++)
{
if (_laserStartFx[i] == null) continue;
if (!_laserStartFx[i].activeSelf)
_laserStartFx[i].SetActive(true);
}
for (int i = 0; i < _laserHitFx.Count; i++)
{
if (_laserHitFx[i] == null) continue;
if (!_laserHitFx[i].activeSelf)
_laserHitFx[i].SetActive(true);
if (i < _laserTargets.Count && _laserTargets[i] != null)
{
Vector3 hitPos = _laserTargets[i].position;
hitPos.z = -50f;
_laserHitFx[i].transform.position = hitPos;
}
}
}
/// LineRenderer 兜底形态(laserPrefab 为空):按需创建、多余的隐藏。每条激光独立起点
private void UpdateZhanShenLaserLine()
{
// 需要几条就创建几条(三发=3个 LineRenderer),多余的隐藏
for (int i = 0; i < _laserTargets.Count; i++)
{
var line = i < _zhanShenLasers.Count ? _zhanShenLasers[i] : CreateZhanShenLaser(i);
if (line == null) continue;
if (!line.gameObject.activeSelf)
line.gameObject.SetActive(true);
// 起点:第0条=主锁定(座位点+偏移),1/2=散射左右(JGFX父对象位置),Z=-50
Vector3 start = GetZhanShenLaserStart(i);
// 终点:该条锁定鱼的位置(Z=-50,与起点同层)
Vector3 end = _laserTargets[i].position;
end.z = -50f;
line.SetPosition(0, start);
line.SetPosition(1, end);
}
for (int i = _laserTargets.Count; i < _zhanShenLasers.Count; i++)
{
if (_zhanShenLasers[i] != null && _zhanShenLasers[i].gameObject.activeSelf)
_zhanShenLasers[i].gameObject.SetActive(false);
}
}
/// 激光终点缓存列表(每帧复用,避免 GC)
private readonly List _laserTargets = new List();
///
/// 创建一条 LineRenderer 形态的战神激光(第 index 条)。
/// 仅 laserPrefab 为空时使用:直接创建一个新 GameObject 并加上 LineRenderer(用 TurretData 上的宽度/颜色)。
///
private LineRenderer CreateZhanShenLaser(int index)
{
var zsData = currentZhanShenData;
GameObject laserGo = new GameObject($"ZhanShenLaser_{index}");
LineRenderer line = laserGo.AddComponent();
line.positionCount = Mathf.Max(line.positionCount, 2);
laserGo.transform.SetParent(null); // 世界空间,不随座位/炮台旋转
// 应用 TurretData 上的宽度/颜色/材质
line.startWidth = zsData.laserWidth;
line.endWidth = zsData.laserWidth;
line.startColor = zsData.laserColor;
line.endColor = zsData.laserColor;
var shader = Shader.Find("Sprites/Default");
if (shader != null)
line.material = new Material(shader) { color = Color.white };
line.useWorldSpace = true;
_zhanShenLasers.Add(line);
return line;
}
/// 销毁所有战神激光对象(两种形态都清,清战神/座位禁用时调用)
private void DestroyZhanShenLaser()
{
for (int i = 0; i < _zhanShenLasers.Count; i++)
{
if (_zhanShenLasers[i] != null)
Destroy(_zhanShenLasers[i].gameObject);
}
_zhanShenLasers.Clear();
for (int i = 0; i < _laserFx.Count; i++)
{
if (_laserFx[i] != null)
Destroy(_laserFx[i].gameObject);
}
_laserFx.Clear();
// 起点/命中特效一并销毁
ClearZhanShenLaserPointFx();
// 准备动画等待状态复位(下次锁定重新计时)
_laserPrepWait = 0f;
_laserLockActive = false;
}
// ==================== 激光起点/命中特效(三发+锁定) ====================
///
/// 激光全量重建时同步重建起点/命中特效(数量与激光一一对应)。
/// 起点=UI 层挂起点节点下(0=turretRotate 的 selfOffsetJGStart 处,1/2=JGFX 左右父对象);命中=世界层 Z=-50 跟随锁定目标。
/// ZhanShenTurretData 上对应预制体没拖(为空)时不生成。
///
private void RebuildZhanShenLaserPointFx()
{
ClearZhanShenLaserPointFx();
var zsDataEx = currentZhanShenData as ZhanShenTurretData;
if (zsDataEx == null) return;
if (zsDataEx.JGStartFXPrefab == null && zsDataEx.JGHitFXPrefab == null) return;
for (int i = 0; i < _laserTargets.Count; i++)
{
if (_laserTargets[i] == null) continue;
// ⭐ 起点特效(UI 层):挂到该条激光的起点节点下,位置由父节点决定(自动跟随,无需每帧刷)
// 0=主激光 turretRotate 下 selfOffsetJGStart 处(子对象语义,跟随炮台旋转);1/2=JGFX 左右父对象下
if (zsDataEx.JGStartFXPrefab != null)
{
Transform startParent = null;
Vector3 startLocal = Vector3.zero;
if (i == 0 && turretRotate != null)
{
startParent = turretRotate;
startLocal = new Vector3(zsDataEx.selfOffsetJGStart.x, zsDataEx.selfOffsetJGStart.y, 0f);
}
else if (i == 1 && JGFXLeftParent != null)
{
startParent = JGFXLeftParent;
}
else if (i == 2 && JGFXRightParent != null)
{
startParent = JGFXRightParent;
}
if (startParent != null)
{
GameObject startGo = Instantiate(zsDataEx.JGStartFXPrefab, startParent, false);
startGo.transform.localPosition = startLocal;
_laserStartFx.Add(startGo);
}
}
// 命中特效:克隆到该条激光的终点(锁定目标身上,Z 压 -50 与激光同层)
if (zsDataEx.JGHitFXPrefab != null)
{
Vector3 hitPos = _laserTargets[i].position;
hitPos.z = -50f;
_laserHitFx.Add(Instantiate(zsDataEx.JGHitFXPrefab, hitPos, Quaternion.identity));
}
}
}
/// 销毁全部起点/命中特效实例(激光重建/销毁时调用)
private void ClearZhanShenLaserPointFx()
{
for (int i = 0; i < _laserStartFx.Count; i++)
{
if (_laserStartFx[i] != null)
Destroy(_laserStartFx[i]);
}
_laserStartFx.Clear();
for (int i = 0; i < _laserHitFx.Count; i++)
{
if (_laserHitFx[i] != null)
Destroy(_laserHitFx[i]);
}
_laserHitFx.Clear();
}
// ==================== 战神激光起点(每条激光独立起点) ====================
///
/// 战神激光第 index 条的起点(鱼群世界坐标,Z=-50 显示层):
/// - 第 0 条(主锁定):turretRotate 子对象偏移(selfOffsetJGStart 是 turretRotate 的本地偏移,
/// ⭐ 跟随炮台旋转:炮口转向时起点绕炮台中心跟着转)
/// - 第 1 条(散射左):JGFXLeftParent 的位置
/// - 第 2 条(散射右):JGFXRightParent 的位置
/// - 越界/未配置:回退子弹炮口位置(旧版行为)
///
private Vector3 GetZhanShenLaserStart(int index)
{
var zs = currentZhanShenData as ZhanShenTurretData;
// 主起点:turretRotate 的子对象偏移(炮台旋转时该点跟着转,TransformPoint 含旋转+缩放)
if (index == 0 && zs != null)
{
Vector3 offset = new Vector3(zs.selfOffsetJGStart.x, zs.selfOffsetJGStart.y, 0f);
Vector3 startPoint = turretRotate != null
? turretRotate.TransformPoint(offset)
: transform.position + offset;
return SeatUIToFishWorld(startPoint, -50f);
}
// 散射左右起点:座位上左右两个起点球父对象的位置
if (index == 1 && JGFXLeftParent != null)
return SeatUIToFishWorld(JGFXLeftParent.position, -50f);
if (index == 2 && JGFXRightParent != null)
return SeatUIToFishWorld(JGFXRightParent.position, -50f);
// 回退:子弹炮口(旧版行为,未配置左右父对象/未挂 ZhanShenTurretData 时)
Vector3 fallback = GetBulletStartPos();
fallback.z = -50f;
return fallback;
}
///
/// 座位 UI 上的世界坐标(座位根节点/座位子节点位置)→ 鱼群世界坐标(子弹/激光所在空间)。
/// 与 TurretData.GetBulletStartWorldPos 同一套换算(UI→屏幕→世界),Z 压到指定显示层。
///
private Vector3 SeatUIToFishWorld(Vector3 uiWorldPos, float z)
{
Canvas canvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
? UIManager.Instance.uiRoot.GetComponent