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;
[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 string user;//玩家唯一ID
public TextMeshProUGUI tmp_UserNumber;//ID:玩家编号
public TextMeshProUGUI tmp_UserName;//名字
public TextMeshProUGUI tmp_UserLevel;//LV:等级
public Slider slider_UserLevelEXP;//EXP:经验条
public Image img_Head;//头像
public Image img_Vip;
public List sprite_VipList;//这里一共有11个正好对应0-10级vip
public List sprite_VipLevelBG; //这里有5个 每两级对应一个0级用最基础的
[Header("金币")]
public long goldNum;
public long consumeGoldNum;
[Tooltip("金币数显示(TMP)。与 goldNum_UGUI 二选一:预制体用哪种组件就拖哪个")]
public TextMeshProUGUI tMP_GoldNum;
[Tooltip("金币数显示(旧版 UGUI Text)。与 tMP_GoldNum 二选一")]
public Text goldNum_UGUI;
[Tooltip("钻石数显示(TMP)。与 diamondNum_UGUI 二选一:预制体用哪种组件就拖哪个")]
public TextMeshProUGUI tMP_DiamondNum;
[Tooltip("钻石数显示(旧版 UGUI Text)。与 tMP_DiamondNum 二选一")]
public Text diamondNum_UGUI;
[Tooltip("炮倍消耗显示(TMP)。与 turretConsumeNum_UGUI 二选一:预制体用哪种组件就拖哪个")]
public TextMeshProUGUI tMP_TurretConsumeNum;
[Tooltip("炮倍消耗显示(旧版 UGUI Text)。与 tMP_TurretConsumeNum 二选一")]
public Text turretConsumeNum_UGUI;
[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;
///
/// 当前 XJMH 炮台在 中的索引(-1=未初始化)。
/// ⭐ XJMH 四把炮台的 turretNumber 全是 0(不对接后端、没有编号),
/// 炮台身份只能用列表索引表示:切换按钮、帧同步广播/接收统一都用索引。
///
[System.NonSerialized]
public int currentTurretIndex = -1;
/// 当前运行中的炮台实例
public TurretData currentTurret;
/// 当前运行中的战神实例(null=未显示)。炮台/战神显示二选一:有战神时 turretParent 隐藏
[System.NonSerialized]
public GameObject currentZhanShen;
/// 特殊炮台(钻头/激光)期间切换翅膀的暂存引用:只切换不显示,等 RestoreTurretVisual 恢复显示
private GameObject _pendingShowWing;
/// 战神实例上的 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;
/// 激光等待默认兜底秒数:prefab 未配 laserFXDelay(=0) 时使用——门不再默认敞开,特效显示必须等 SMB 动画事件放行(事件来提前开门,超时强制显示)
private const float LaserFXWaitFallback = 2f;
/// 上一帧是否有锁定点(检测锁定建立沿,触发准备等待)
private bool _laserLockActive;
/// 激光循环音效是否正在播放(跟随激光实际显示状态起停,避免每帧重复触发)
private bool _laserSoundOn;
/// 激光循环音效当前播放速度(1=正常,2=狂暴2倍速),用于边沿比较避免每帧重复调用
private float _laserSoundPitch = 1f;
/// 预制体形态激光的基准缩放(重建时取自 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;
[Tooltip("炮口最大偏转角度(度),相对基准方向:下方座位(0,1)基准=朝上0°,上方座位(2,3)基准=朝下180°。" +
"超出范围的角度会被钳制回边界(本地玩家/远程同步/bot 全部生效)。<=0 或 >=180 表示不限制")]
public float maxRotationAngle = 90f;
///
/// 炮台在世界坐标(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();
///
/// ⭐ 该锁位是否由玩家手动点选(索引与 tripleLockTargets 对齐)。
/// true = 玩家自己点的鱼 → 离场前一直跟,死了才换;手动身份会跟着鱼一起移入子锁位。
///
private readonly List tripleSlotManual = 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());
// ⭐ 自动攻击开关:默认隐藏,等玩家入座时由 ShowPlayer 按"是否本地座位"决定显隐。
// ⚠️ 以前这里是"没激活就无条件激活"(为修预制体把按钮藏了、点不到的问题),
// 但第二个人加入时他的座位才第一次激活、Awake 此刻才执行 → 会把他的"自动"按钮也打开,
// 屏幕上出现两个"自动"按钮。改为默认隐藏 + 入座时按本地/他座显隐(本地按钮不会再被藏)。
// 监听照旧:关闭时本地座位炮台切回 Idle(原 GameMainPanel 的 toggle_SkillAuto 逻辑)
if (toggle_AutoAttack != null)
{
if (toggle_AutoAttack.gameObject.activeSelf)
toggle_AutoAttack.gameObject.SetActive(false);
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 模式:隐藏钻石显示(只显示金币;TMP 与旧版 UGUI Text 哪个拖了藏哪个)
if (turretControlMode == TurretControlMode.XJMH)
{
if (tMP_DiamondNum != null)
tMP_DiamondNum.gameObject.SetActive(false);
if (diamondNum_UGUI != null)
diamondNum_UGUI.gameObject.SetActive(false);
}
// XJMH 模式:炮台是座位自带子对象,不需要从资源加载,只做初始化
// (整理炮台列表 + 激活默认炮台 = 列表索引 0)
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;
}
///
/// ⭐ 预热用:本次座位首次激活时先"只整理炮台列表、不点亮默认炮台"——
/// 把炮台 Spine 骨架的首初始化(几十~上百 ms)拆到下一帧单独做,避免座位激活那一帧
/// 同时扛"座位整层 OnEnable + 炮台骨架首建"两个大活。由 GameMainPanel_FishBase 预热流程设置;
/// 任何入座/兜底路径(ShowPlayer)都会强制复位并保证炮台被点亮,不会留空座位。
///
[System.NonSerialized] public bool deferDefaultTurretLightUp;
///
/// XJMH 模式:炮台是座位自带子对象,预制体里已摆好,不需要 LoadTurret 加载。
/// ⭐ 默认炮台 = 排序后索引 0 的那把(对应座位 GetChild(0) 的绿炮台)。
///
private void InitXJMHTurret()
{
if (xjmhTurretList == null || xjmhTurretList.Count == 0)
{
Debug.LogWarning($"[SitPosData] XJMH 炮台列表未配置,座位 {posID} 无炮台可用");
return;
}
NormalizeXJMHTurretList();
// ⭐ 预热路径:只整理列表,点亮留给预热协程下一帧调 ActivateDefaultXJMHTurretForPrewarm
if (deferDefaultTurretLightUp) return;
ApplyXJMHTurret(0);
}
///
/// 预热续帧:点亮默认炮台(配合 deferDefaultTurretLightUp,把炮台骨架首初始化拆到单独一帧)。
/// ⚠️ "是否已点亮"必须看列表里有没有"激活状态"的炮台——currentTurret 是可序列化字段,
/// 预制体里预置了引用,不代表炮台已激活(用它当判据会出现"炮台永远不点亮"的 bug)。
///
public void ActivateDefaultXJMHTurretForPrewarm()
{
if (turretControlMode != TurretControlMode.XJMH) return;
if (HasActiveXJMHTurret()) return; // 已点亮(正常入座路径已做过)
deferDefaultTurretLightUp = false;
InitXJMHTurret();
}
/// 列表里当前是否有"激活状态"的炮台("默认炮台是否已点亮"的正确判据)
public bool HasActiveXJMHTurret()
{
if (xjmhTurretList == null) return false;
for (int i = 0; i < xjmhTurretList.Count; i++)
if (xjmhTurretList[i] != null && xjmhTurretList[i].gameObject.activeSelf) return true;
return false;
}
///
/// 整理 XJMH 炮台列表:剔除空引用,并按座位下的兄弟顺序排序。
/// ⭐ 排序原因:切换面板的 4 个按钮固定是 绿/蓝/金/VIP(索引 0~3),
/// 而座位里这四把炮台的兄弟顺序正好就是 绿/蓝/金/VIP。
/// 不排序的话,Inspector 里拖拽的先后顺序会让按钮和炮台对不上。
///
private void NormalizeXJMHTurretList()
{
xjmhTurretList.RemoveAll(t => t == null);
xjmhTurretList.Sort((a, b) =>
a.transform.GetSiblingIndex().CompareTo(b.transform.GetSiblingIndex()));
}
///
/// XJMH:按列表索引激活炮台(只激活目标那把,其余全部停用)。
/// ⭐ 不能再用 turretNumber 匹配:XJMH 四把炮台的 turretNumber 全是 0,
/// 按编号匹配会把四把一次全激活,且切换到"同编号"的炮台会被判成无需切换(点按钮没反应)。
///
private void ApplyXJMHTurret(int index)
{
if (xjmhTurretList == null || index < 0 || index >= xjmhTurretList.Count) return;
TurretData target = xjmhTurretList[index];
if (target == null) return;
for (int i = 0; i < xjmhTurretList.Count; i++)
{
var td = xjmhTurretList[i];
if (td != null)
td.gameObject.SetActive(i == index);
}
currentTurretIndex = index;
currentTurret = target;
currentTurretNumber = target.turretNumber; // XJMH 下恒为 0,仅保持字段同步
tra_CurrTurret = GetTurretRotationPivot(target);
EnsureXJMHTurretMuzzle(target, tra_CurrTurret);
EnsureXJMHTurretAnim(target, tra_CurrTurret);
OnStateChange = target.ChangeState;
UpdateTurretWorldPos();
target.PlayAppearAnim(); // 出现动画(各 SkeletonGraphic 的 Spine startingAnimation)
Debug.Log($"[SitPosData] XJMH 炮台已切换: 索引{index} ({target.name})");
}
///
/// XJMH:修正子弹发射点。
/// ⭐ 预制体里绿/蓝/金三把炮台的 tra_BulletStartPosition 指到了"炮体美术节点"
/// (它的 pivot 在炮体盒子底部,相对旋转中心是 -29.8,等于旋转中心后方),子弹会从炮台后面出来。
/// 真正的炮口是旋转节点下那个空节点 Bullit(相对旋转中心 +50,正前方)——VIP 就是这么配的。
/// ⭐ 只在当前指向的是「带 Graphic 的美术节点」时才修正:自己拖进来的炮口标记节点(无 Graphic)不动。
///
private static void EnsureXJMHTurretMuzzle(TurretData turret, Transform pivot)
{
if (pivot == null) return;
// 当前指向的节点没有 Graphic(是炮口标记节点)→ 说明是手动配好的,不动
if (turret.tra_BulletStartPosition != null
&& turret.tra_BulletStartPosition.GetComponent() == null) return;
Transform muzzle = pivot.Find("Bullit");
if (muzzle == null) return;
Debug.LogWarning($"[SitPosData] XJMH 炮台 {turret.name} 的 tra_BulletStartPosition 指向了美术节点,已自动改用炮口节点 Bullit");
turret.tra_BulletStartPosition = muzzle;
}
///
/// XJMH:补上炮台动画组件。
/// ⭐ 预制体里绿/蓝/金三把炮台的 TurretData.skeletonAnimation 都是空的(只有 VIP 拖了),
/// 而 TurretData.PlaySkeleton 遇到 null 会直接 return → 这三把炮台动画完全不播。
/// 这里按 VIP 的同一套结构自动补:旋转节点(炮台本体组)下面那个 SkeletonGraphic 就是炮体动画,
/// VIP 的 skeletonAnimation 指的正是它。Inspector 里已经拖了的不会被覆盖。
///
private static void EnsureXJMHTurretAnim(TurretData turret, Transform pivot)
{
if (turret.skeletonAnimation != null || pivot == null) return;
turret.skeletonAnimation = pivot.GetComponentInChildren(true);
Debug.LogWarning(turret.skeletonAnimation != null
? $"[SitPosData] XJMH 炮台 {turret.name} 的 TurretData.skeletonAnimation 未配置,已自动取旋转节点下的 {turret.skeletonAnimation.name}"
: $"[SitPosData] XJMH 炮台 {turret.name} 的 TurretData.skeletonAnimation 未配置,且旋转节点下没找到 SkeletonGraphic,动画不会播放");
}
private void OnEnable()
{
// 默认显示等待界面
ShowWaiting();
}
private void OnDisable()
{
// 入场动画标记复位:座位再次激活(重进房/重新入座)时重新播
_xjmhEnterPlayed = false;
_xjmhEnterPending = false;
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()
{
UpdateXJMHEnterPending(); // ⭐ XJMH 入场动画挂起补播(面板/炮台就绪即播,避免加载期播了看不到)
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 (!IsPointerOnOpenPT(eventData.position)) return;
_openPTPressing = true;
_openPTPressTimer = 0f;
}
///
/// 按点是否落在 img_OpenPT 的屏幕矩形内。
/// ⭐ 用坐标矩形判定,而不是比较射线命中的对象:座位下还挂着昵称/表情/炮台等一堆兄弟 UI,
/// 实际被射线命中的往往是它们而不是 img_OpenPT,但它们同属座位层级,
/// 按下事件照样会冒泡到本组件,所以只有按坐标判定才可靠。
///
private bool IsPointerOnOpenPT(Vector2 screenPos)
{
if (img_OpenPT == null) return false;
RectTransform rect = img_OpenPT.rectTransform;
if (rect == null) return false;
Canvas canvas = rect.GetComponentInParent