Files
5.24BuYu/Assets/MyGame/Scripts/Turret/SitPosData.cs
T

2177 lines
94 KiB
C#
Raw Normal View History

2026-02-03 11:35:19 +08:00
using System;
2026-09-04 00:31:26 +08:00
using System.Collections;
2026-02-03 11:35:19 +08:00
using System.Collections.Generic;
using System.Linq;
2026-09-04 23:38:33 +08:00
using Spine.Unity;
2026-02-03 11:35:19 +08:00
using TMPro;
using UnityEngine;
2026-09-02 23:40:52 +08:00
using UnityEngine.EventSystems;
2026-03-09 09:25:01 +08:00
using UnityEngine.UI;
2026-02-03 11:35:19 +08:00
2026-07-28 18:29:49 +08:00
/// <summary>炮台控制模式</summary>
public enum TurretControlMode
{
/// <summary>普通模式:动态克隆炮台,显示金币+钻石,按钮打开面板切换</summary>
Normal = 0,
/// <summary>星际迷航模式:座位子对象激活/停用,只显示金币,长按炮台弹出扇形切换</summary>
XJMH = 1
}
2026-09-02 23:40:52 +08:00
public class SitPosData : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
2026-02-03 11:35:19 +08:00
{
2026-09-04 23:38:33 +08:00
public GameObject spineObj;//他的子对象child0带SkeletonGraphic
public GameObject ziObj;//他的子对象child0带textmeshprougui
2026-09-03 19:39:06 +08:00
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;
/// <summary>自动攻击从开→关时触发一次(GameMainPanel 订阅后广播 Idle 状态给其他客户端,仅本地玩家座位会触发)</summary>
public event Action OnAutoAttackOff;
/// <summary>自动攻击是否开启(toggle 没拖引用时恒为 false,等同未开自动攻击)</summary>
public bool IsAutoAttackOn => toggle_AutoAttack != null && toggle_AutoAttack.isOn;
2026-09-02 23:40:52 +08:00
[Tooltip("长按判定时间(秒):按住 img_OpenPT 超过此时间触发 OnOpenPTLongPress")]
public float openPTLongPressTime = 0.5f;
public Image img_OpenPT;
/// <summary>长按 img_OpenPT 触发一次(GameMainPanel 订阅后用它开关炮台面板,仅本地玩家座位有意义)</summary>
public event Action OnOpenPTLongPress;
private bool _openPTPressing;
private float _openPTPressTimer;
2026-07-30 14:18:17 +08:00
public bool IsBaoJiangingActive;
2026-07-30 22:09:55 +08:00
public Transform obj_BaoJiangZhongPanelPosition;
2026-07-30 14:18:17 +08:00
2026-06-08 21:23:40 +08:00
/// <summary>本地玩家的座位引用(由游戏面板初始化时设置)</summary>
public static SitPosData localPlayerSit;
2026-06-16 19:12:37 +08:00
/// <summary>当前房间房主座位(人机请求由房主发送,房主退出后自动继承)</summary>
public static SitPosData roomOwnerSit;
2026-06-13 01:25:41 +08:00
[Tooltip("座位编号 0~3。0,1=下方 2,3=上方")]
2026-02-03 11:35:19 +08:00
public int posID;
2026-06-16 19:12:37 +08:00
[Tooltip("是否为人机")]
public bool isBot = false;
2026-05-02 23:52:48 +08:00
2026-06-13 01:25:41 +08:00
/// <summary>posID>=2 = 上方,需要翻转(0,1=下方;2,3=上方)</summary>
public bool IsFlipped => posID >= 2;
2026-09-11 19:54:38 +08:00
// -------------------------
// 玩家基础信息
// -------------------------
[Header("玩家信息")]
2026-09-15 16:30:20 +08:00
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;//头像
2026-09-11 19:54:38 +08:00
public Image img_Vip;
public List<GameObject> sprite_VipList;//这里一共有11个正好对应0-10级vip
public List<GameObject> sprite_VipLevelBG; //这里有5个 每两级对应一个0级用最基础的
2026-03-27 15:18:35 +08:00
[Header("金币")]
2026-05-02 23:52:48 +08:00
public long goldNum;
public long consumeGoldNum;
2026-02-03 11:35:19 +08:00
public TextMeshProUGUI tMP_GoldNum;
2026-06-19 18:18:04 +08:00
public TextMeshProUGUI tMP_DiamondNum;
2026-05-04 17:21:52 +08:00
public TextMeshProUGUI tMP_TurretConsumeNum;
2026-09-11 19:54:38 +08:00
2026-06-29 20:40:30 +08:00
[Header("翅膀")]
[Tooltip("翅膀实例化的父节点")]
public Transform wingParent;
2026-08-27 19:06:35 +08:00
[Header("战神")]
[Tooltip("战神实例化的父节点")]
public Transform zhanShenParent;
2026-08-28 22:33:35 +08:00
[Tooltip("当前装备的战神编号(0=无战神)。炮台/战神显示二选一:有战神时 turretParent 隐藏")]
public int currentZhanShenNumber;
2026-08-27 19:06:35 +08:00
[Header("特殊鱼炮台")]
[Tooltip("特殊鱼炮台实例化的父节点")]
public Transform tSYPTParent;
2026-08-28 18:57:36 +08:00
[Tooltip("特殊鱼炮台(钻头)是否正在使用:true=期间禁用普通开火,点击只发射钻头")]
public bool isDrillActive;
2026-08-29 17:28:00 +08:00
[Tooltip("激光锁定:true=激光发射期间炮台锁定方向,SetRotation 不生效(原方向打到爆奖结束)")]
public bool isLaserLocked;
2026-08-31 16:40:04 +08:00
/// <summary>
/// 散射激光模式中(GameMainPanel 每帧刷新:三发+锁定+已锁目标)。
/// ⭐ 激活左右起点球父对象用这个标记,不依赖实际锁定点数量(目标不足2个时散射模式开着也要显示起点球)
/// </summary>
[System.NonSerialized]
public bool isScatterLaserOn;
2026-05-02 23:52:48 +08:00
// -------------------------
2026-06-08 21:23:40 +08:00
// 炮台管理(动态克隆)
2026-05-02 23:52:48 +08:00
// -------------------------
2026-05-04 17:21:52 +08:00
[Header("炮台")]
2026-06-08 21:23:40 +08:00
[Tooltip("炮台实例化的父节点")]
public Transform turretParent;
2026-08-27 17:28:19 +08:00
public Transform turretRotate;
2026-05-04 17:21:52 +08:00
2026-07-28 18:29:49 +08:00
[Header("炮台控制模式")]
[Tooltip("Normal=普通模式(动态克隆炮台,显示金币+钻石,按钮切换); XJMH=星际迷航模式(座位子对象激活/停用,只显示金币,长按扇形切换)")]
public TurretControlMode turretControlMode = TurretControlMode.Normal;
2026-06-08 21:23:40 +08:00
[Tooltip("炮台锚点位置(相对 turretParent)")]
public Vector2 turretAnchoredPos = Vector2.zero;
[Tooltip("当前装备的炮台编号")]
public int currentTurretNumber;
/// <summary>当前运行中的炮台实例</summary>
2026-02-03 11:35:19 +08:00
public TurretData currentTurret;
2026-05-02 23:52:48 +08:00
2026-08-28 22:33:35 +08:00
/// <summary>当前运行中的战神实例(null=未显示)。炮台/战神显示二选一:有战神时 turretParent 隐藏</summary>
[System.NonSerialized]
public GameObject currentZhanShen;
2026-09-24 08:49:24 +08:00
/// <summary>特殊炮台(钻头/激光)期间切换翅膀的暂存引用:只切换不显示,等 RestoreTurretVisual 恢复显示</summary>
private GameObject _pendingShowWing;
2026-08-28 22:33:35 +08:00
/// <summary>战神实例上的 TurretData/ZhanShenTurretData(开火时优先取战神的子弹预制体,null=无战神或未挂组件)</summary>
[System.NonSerialized]
public TurretData currentZhanShenData;
2026-08-29 01:02:09 +08:00
// -------------------------
2026-08-30 15:29:02 +08:00
// 战神激光(锁定时显示):两种形态
// - laserPrefab 非空 → 预制体形态:JiGuangControl 工具类驱动(克隆激光预制体,方向/长度每帧刷新)
// - laserPrefab 为空 → LineRenderer 兜底形态:直接新建 GameObject + LineRenderer
2026-08-29 01:02:09 +08:00
// -------------------------
2026-08-31 16:40:04 +08:00
/// <summary>LineRenderer 形态激光列表。三发锁定=3条。起点:0=主激光(座位点+selfOffsetJGStart),1/2=散射左右(JGFX父对象位置)</summary>
2026-08-29 01:02:09 +08:00
private readonly List<LineRenderer> _zhanShenLasers = new List<LineRenderer>();
2026-08-30 15:29:02 +08:00
/// <summary>预制体形态激光实例列表(世界空间,由 JiGuangControl.JiGuangCreate 按目标数量重建)</summary>
private readonly List<Transform> _laserFx = new List<Transform>();
2026-09-05 23:22:30 +08:00
/// <summary>激光起点特效实例(UI 层:0=挂 turretRotate 下 selfOffsetJGStart 处,1/2=挂 JGFX 左右父对象下,跟随父节点)</summary>
private readonly List<GameObject> _laserStartFx = new List<GameObject>();
/// <summary>激光命中特效实例(与 _laserFx 一一对应,世界空间 Z=-50 层,跟随锁定目标)</summary>
private readonly List<GameObject> _laserHitFx = new List<GameObject>();
2026-09-06 19:49:25 +08:00
/// <summary>激光准备等待剩余秒数:锁定刚建立时读 ZhanShenTurretData.laserFXDelay,>0 期间激光及特效隐藏(等准备动画播完)</summary>
private float _laserPrepWait;
2026-09-10 01:07:35 +08:00
/// <summary>激光等待默认兜底秒数:prefab 未配 laserFXDelay(=0) 时使用——门不再默认敞开,特效显示必须等 SMB 动画事件放行(事件来提前开门,超时强制显示)</summary>
private const float LaserFXWaitFallback = 2f;
2026-09-06 19:49:25 +08:00
/// <summary>上一帧是否有锁定点(检测锁定建立沿,触发准备等待)</summary>
private bool _laserLockActive;
2026-09-28 20:19:27 +08:00
/// <summary>激光循环音效是否正在播放(跟随激光实际显示状态起停,避免每帧重复触发)</summary>
private bool _laserSoundOn;
/// <summary>激光循环音效当前播放速度(1=正常,2=狂暴2倍速),用于边沿比较避免每帧重复调用</summary>
private float _laserSoundPitch = 1f;
2026-08-30 15:29:02 +08:00
/// <summary>预制体形态激光的基准缩放(重建时取自 laserPrefab.transform.localScale)</summary>
private Vector3 _laserFxBaseScale = Vector3.one;
2026-09-05 23:22:30 +08:00
/// <summary>预制体形态激光的角度偏移:激光预制体自带的 Z 轴旋转(重建时缓存,绕世界 Z 叠加在计算朝向上)</summary>
private float _laserFxRotZOffset;
2026-08-31 16:40:04 +08:00
/// <summary>每条激光的起点缓存(索引与 _laserTargets 对齐,每帧复用避免 GC)</summary>
private readonly List<Vector3> _laserStarts = new List<Vector3>();
2026-08-29 01:02:09 +08:00
2026-07-28 18:29:49 +08:00
// -------------------------
// XJMH 炮台列表(星际迷航专用)
// -------------------------
[Header("XJMH 炮台列表")]
[Tooltip("XJMH 模式专用:在 Inspector 中拖入座位下的所有炮台子对象(带 TurretData 组件)。" +
"数量不多,建议按炮台编号升序排列,便于扇形切换按索引选择。")]
public List<TurretData> xjmhTurretList = new List<TurretData>();
2026-05-02 23:52:48 +08:00
// 状态机入口(GameMainPanel 调用)
public Action<TurretState> OnStateChange;
// -------------------------
// 炮台旋转
// -------------------------
2026-05-04 17:21:52 +08:00
[Header("炮台旋转")]
2026-05-02 23:52:48 +08:00
public Transform tra_CurrTurret;
public float currentAngle;
2026-09-21 16:39:08 +08:00
[Tooltip("炮口最大偏转角度(度),相对基准方向:下方座位(0,1)基准=朝上0°,上方座位(2,3)基准=朝下180°。" +
"超出范围的角度会被钳制回边界(本地玩家/远程同步/bot 全部生效)。<=0 或 >=180 表示不限制")]
public float maxRotationAngle = 90f;
2026-05-02 23:52:48 +08:00
2026-07-16 20:16:05 +08:00
/// <summary>
/// 炮台在世界坐标(Z=0平面)的位置缓存。
/// 因为炮台是 UGUI 元素,tra_CurrTurret.position 是 Canvas 坐标,不能直接和鱼的世界坐标比较距离。
/// 这里用和子弹起点(GetBulletStartWorldPos)相同的转换方式,把 UI 坐标转成世界坐标。
/// 由 LoadTurret 加载完炮台后计算并缓存,bot AI/锁定检测距离时用此字段。
/// </summary>
[System.NonSerialized]
public Vector3 turretWorldPos;
2026-05-02 23:52:48 +08:00
// -------------------------
// 锁定系统
// -------------------------
2026-05-04 17:21:52 +08:00
[Header("锁定系统")]
2026-03-09 09:25:01 +08:00
public MonsterContral monCotl_Lock;
2026-05-01 06:26:50 +08:00
public Transform monCotl_Lock_Position;
2026-05-04 17:21:52 +08:00
public bool isLock = false;
2026-07-31 17:57:24 +08:00
/// <summary>
/// ⭐ 当前锁定是否为玩家手动锁定。
/// - true:玩家点击锁定的鱼 → 不受路径进度(0.1~0.9)限制,鱼离场前一直锁定
/// - false:自动锁定/三发查找/bot → 受路径进度限制
/// 由 GameMainPanel_FishBase 在 SetLockTarget 前设置
/// </summary>
[System.NonSerialized]
public bool isManualLock = false;
2026-06-21 00:28:59 +08:00
public GameObject lockFX; // 主锁定图标(单发/双倍用)
public List<GameObject> lockFX_Triple = new List<GameObject>(); // 三发锁定额外2个图标
public List<MonsterContral> tripleLockTargets = new List<MonsterContral>();
public List<Transform> tripleLockPositions = new List<Transform>();
2026-02-03 11:35:19 +08:00
2026-05-02 23:52:48 +08:00
// -------------------------
// UI
// -------------------------
2026-05-04 17:21:52 +08:00
[Header("UI")]
2026-05-02 23:52:48 +08:00
public Button btn_TurretAdd;
public Button btn_TurretMinus;
2026-03-27 15:18:35 +08:00
2026-08-31 00:59:44 +08:00
[Header("激光效果父对象")]
2026-08-31 16:40:04 +08:00
[Tooltip("散射激光左起点球的父对象:出场/换战神时克隆 ZhanShenTurretData.JGQiuStartPrefab 到它下面,散射激光激活时显示。不拖也没关系:Awake 会自动创建兜底节点")]
2026-08-31 00:59:44 +08:00
public Transform JGFXLeftParent;
2026-08-31 16:40:04 +08:00
[Tooltip("散射激光右起点球的父对象:出场/换战神时克隆 ZhanShenTurretData.JGQiuStartPrefab 到它下面,散射激光激活时显示。不拖也没关系:Awake 会自动创建兜底节点")]
2026-08-31 00:59:44 +08:00
public Transform JGFXRightParent;
2026-05-02 23:52:48 +08:00
private void Awake()
2026-02-03 11:35:19 +08:00
{
2026-07-28 18:29:49 +08:00
if (btn_TurretAdd != null)
btn_TurretAdd.onClick.AddListener(() => ChangeTurretAddConsumeNum());
if (btn_TurretMinus != null)
btn_TurretMinus.onClick.AddListener(() => ChangeTurretMinusConsumeNum());
2026-09-03 19:39:06 +08:00
// ⭐ 自动攻击开关:关闭时本地座位炮台切回 Idle(原 GameMainPanel 的 toggle_SkillAuto 监听逻辑)
if (toggle_AutoAttack != null)
toggle_AutoAttack.onValueChanged.AddListener(OnAutoAttackToggleChanged);
2026-09-04 00:31:26 +08:00
// ⭐ 炮倍选择面板:按钮开/关;Awake 记录面板原始高度(动画还原目标值,之后会被压到 0)
if (btn_SelectPaoBei != null)
btn_SelectPaoBei.onClick.AddListener(OnClickSelectPaoBei);
EnsurePaoBeiPanelHeight();
2026-08-31 16:40:04 +08:00
// ⭐ 散射起点球父对象兜底:预制体没拖引用就自动创建(挂 beij 下,炮台两侧),
2026-09-01 23:35:24 +08:00
// 保证 UpdateJGFXParentsActive 永远有对象可激活,不依赖 Inspector 配置。
// XJMH(星际迷航)没有战神/激光概念,跳过不给它加节点。
if (turretControlMode != TurretControlMode.XJMH)
EnsureJGFXParents();
2026-08-31 16:40:04 +08:00
2026-07-28 18:29:49 +08:00
// XJMH 模式:隐藏钻石显示(只显示金币)
if (turretControlMode == TurretControlMode.XJMH && tMP_DiamondNum != null)
tMP_DiamondNum.gameObject.SetActive(false);
// XJMH 模式:炮台是座位自带子对象,预制体里已摆好,不需要加载。
// 这里只做引用初始化,不做激活/停用。
if (turretControlMode == TurretControlMode.XJMH)
InitXJMHTurret();
}
2026-09-03 19:39:06 +08:00
/// <summary>
/// 自动攻击开关变化:关闭时本地座位炮台立即切回 Idle(自动开火停止)。
/// ⭐ 只对本地玩家座位生效:同一个 toggle 被拖到多个座位时,非本地座位不响应,
/// 避免远程座位的状态机被本地点击误切。
/// </summary>
private void OnAutoAttackToggleChanged(bool on)
{
if (on) return;
if (this != localPlayerSit) return;
ChangeState(TurretState.Idle);
OnAutoAttackOff?.Invoke();
}
2026-08-31 16:40:04 +08:00
/// <summary>
/// 散射起点球父对象兜底:Inspector 没拖引用时自动创建两个 RectTransform
/// (挂在 turretRotate 的父节点 beij 下,炮台左右两侧 ±50),并写回字段。
/// 拖了引用(预制体配置的座位)则什么都不做。
/// </summary>
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);
}
/// <summary>创建一个散射起点球父节点(beij 下,x 偏移 ±50、y 100,默认隐藏)</summary>
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;
}
2026-07-28 18:29:49 +08:00
/// <summary>
/// XJMH 模式:炮台是座位自带子对象,预制体里已摆好激活状态,不需要 LoadTurret 加载。
/// 此方法只做引用初始化:从 <see cref="xjmhTurretList"/> 取第一个激活的炮台赋给 currentTurret,
/// 并绑定旋转节点、状态机入口、世界坐标缓存。不做任何激活/停用操作。
/// </summary>
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;
2026-07-28 21:11:57 +08:00
// 缓存旋转节点:优先用 TurretData.tra_RotationPivot,否则回退父节点
tra_CurrTurret = GetTurretRotationPivot(currentTurret);
2026-07-28 18:29:49 +08:00
OnStateChange = currentTurret.ChangeState;
UpdateTurretWorldPos();
Debug.Log($"[SitPosData] XJMH 炮台引用已初始化: #{currentTurretNumber}");
2026-05-27 19:47:06 +08:00
}
2026-06-08 21:23:40 +08:00
private void OnEnable()
{
2026-07-02 18:28:50 +08:00
// 默认显示等待界面
ShowWaiting();
2026-06-08 21:23:40 +08:00
}
private void OnDisable()
{
if (localPlayerSit == this)
localPlayerSit = null;
2026-06-16 19:12:37 +08:00
// 房主退出 → 自动继承给第一个非人机玩家
if (roomOwnerSit == this)
{
roomOwnerSit = null;
TransferRoomOwner();
}
2026-08-29 01:02:09 +08:00
// 座位禁用时清理战神激光(世界空间对象,不随座位隐藏)
DestroyZhanShenLaser();
2026-08-31 16:40:04 +08:00
// 散射起点父对象藏起来(子对象保留,下次出场/换战神时重建)
if (JGFXLeftParent != null)
JGFXLeftParent.gameObject.SetActive(false);
if (JGFXRightParent != null)
JGFXRightParent.gameObject.SetActive(false);
2026-08-29 01:02:09 +08:00
}
private void Update()
{
UpdateZhanShenLaser();
2026-09-02 23:40:52 +08:00
// ⭐ 长按 img_OpenPT 计时:到达阈值触发一次(提前松手不触发)
if (_openPTPressing)
{
_openPTPressTimer += Time.deltaTime;
if (_openPTPressTimer >= openPTLongPressTime)
{
_openPTPressing = false;
_openPTPressTimer = 0f;
OnOpenPTLongPress?.Invoke();
}
}
}
// ==================== 长按 img_OpenPT(打开炮台面板) ====================
/// <summary>按下:命中 img_OpenPT(或其子对象)才开始长按计时(其他区域不响应)</summary>
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;
}
/// <summary>松开:取消长按计时(未达阈值不触发)</summary>
public void OnPointerUp(PointerEventData eventData)
{
_openPTPressing = false;
_openPTPressTimer = 0f;
2026-08-29 01:02:09 +08:00
}
// ==================== 战神激光(锁定时显示) ====================
/// <summary>
2026-08-31 16:40:04 +08:00
/// 战神锁定时显示激光,Z 统一压到 -50 显示层。
2026-08-30 15:29:02 +08:00
/// ⭐ 一个锁定点一条激光:单发锁定=1条,三发锁定=3条(主目标+2个三发目标)。
2026-08-31 16:40:04 +08:00
/// ⭐ 每条激光独立起点:第0条(主锁定)=turretRotate 子对象偏移 selfOffsetJGStart(⭐跟随炮台旋转),
/// 第1/2条(散射左右)=JGFXLeftParent/JGFXRightParent 的位置(座位 UI 上的起点球父对象)。
/// ⭐ 使用散射激光时激活 JGFXLeftParent/JGFXRightParent 显示起点球,否则隐藏:
/// 本地座位=三发+锁定开着(isScatterLaserOn,面板每帧刷新);远程/bot座位=锁定点>1(三发目标凑满)。
2026-08-30 15:29:02 +08:00
/// ⭐ 两种形态:laserPrefab 非空 → 预制体形态(JiGuangControl 工具类);为空 → LineRenderer 兜底形态。
2026-08-29 01:02:09 +08:00
/// ⭐ 只是视觉表现:打中鱼仍按正常攻击频率由子弹完成(HandleAutoFire 以 attackTime 发射锁定子弹)。
/// 普通炮台没有此效果(currentZhanShenData 为 null 时不显示)。
/// </summary>
private void UpdateZhanShenLaser()
{
// 收集所有有效锁定点:主目标 + 三发锁定的额外目标
_laserTargets.Clear();
if (currentZhanShenData != null && isLock)
{
if (monCotl_Lock_Position != null && monCotl_Lock_Position.gameObject.activeInHierarchy)
_laserTargets.Add(monCotl_Lock_Position);
2026-08-30 15:29:02 +08:00
// 三发锁定:每个有效目标一条激光(主目标已在上面,这里只补额外的)
2026-08-29 01:02:09 +08:00
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);
}
}
}
2026-09-06 19:49:25 +08:00
// ⭐ 准备动画等待:锁定刚建立时读 ZhanShenTurretData.laserFXDelay,等待期间激光及全部特效
// (光束/起点球/起点特效/命中特效)先不显示,等战神摆姿势动画播完再出现;锁定丢失后重新锁定会重新等待
2026-09-10 01:07:35 +08:00
// ⭐ 0 视为"未配置"→ 用兜底秒数(原语义 0=锁定立即显示会让特效绕过 SMB 动画事件直接显示)
2026-09-06 19:49:25 +08:00
bool lockActive = _laserTargets.Count > 0;
if (lockActive && !_laserLockActive)
{
var zsDelay = currentZhanShenData as ZhanShenTurretData;
2026-09-10 01:07:35 +08:00
_laserPrepWait = zsDelay != null && zsDelay.laserFXDelay > 0f ? zsDelay.laserFXDelay : LaserFXWaitFallback;
2026-09-08 21:52:22 +08:00
// ⭐ 锁定建立:刷新 FireIndex=1 → Fire0→Fire1 过渡生效,激光动画的 SMB 事件负责开门
currentZhanShenData?.RefreshWarGodFireMode();
}
else if (!lockActive && _laserLockActive)
{
// ⭐ 锁定丢失:刷新 FireIndex=0 → 退回 Fire0 子弹动画(过渡条件 FireIndex==0)
currentZhanShenData?.RefreshWarGodFireMode();
2026-09-06 19:49:25 +08:00
}
_laserLockActive = lockActive;
if (_laserPrepWait > 0f)
{
_laserPrepWait -= Time.deltaTime;
lockActive = false; // 等待期间当作未锁定:激光和特效全部隐藏
}
2026-08-31 16:40:04 +08:00
// ⭐ 散射激光使用中就激活左右起点父对象显示起点球:
// isScatterLaserOn=三发+锁定开着(面板每帧刷新,本地座位);锁定点>1=三发目标已凑满(远程/bot座位)
// 两个条件满足其一即激活——散射模式开着但目标不足时也要显示起点球
2026-09-06 19:49:25 +08:00
UpdateJGFXParentsActive(lockActive && (_laserTargets.Count > 1 || isScatterLaserOn));
2026-08-31 16:40:04 +08:00
2026-09-28 20:19:27 +08:00
// ⭐ 激光循环音效:跟随"激光实际显示"状态起停(激光音效很长,逐发播会重叠,改为循环)
UpdateZhanShenLaserSound(lockActive);
2026-08-31 16:40:04 +08:00
#if UNITY_EDITOR
DebugJGFXState();
#endif
2026-09-06 19:49:25 +08:00
// 没有锁定点(或准备动画等待中):隐藏所有激光
if (!lockActive)
2026-08-29 01:02:09 +08:00
{
2026-08-30 15:29:02 +08:00
HideZhanShenLasers();
2026-08-29 01:02:09 +08:00
return;
}
2026-08-30 15:29:02 +08:00
// 按配置走对应形态:laserPrefab 非空=预制体形态,为空=LineRenderer 兜底
if (currentZhanShenData.laserPrefab != null)
UpdateZhanShenLaserFx();
else
UpdateZhanShenLaserLine();
}
2026-09-06 19:49:25 +08:00
/// <summary>
/// 战神 loop 动画开始通知(TurretData 转发:ZhanShenLaserLoopSMB 状态机行为 / 剪辑动画事件):
/// 立即放行激光及全部特效显示(提前结束 laserFXDelay 等待;未在等待中则无副作用)。
/// </summary>
public void NotifyZhanShenLaserLoopAnim()
{
_laserPrepWait = 0f;
}
2026-09-28 20:19:27 +08:00
/// <summary>
/// 激光循环音效:跟随"激光实际显示"状态起停(仅本地座位,且战神配置了 laserAudio 才播)。
/// ⭐ 用 AudioManager.PlayFXLoop 走专用循环 AudioSource——激光音效很长,逐发 PlayOneShot 会重叠;
/// 起停走状态边沿,锁定期间不重复触发;激光消失/清战神/座位禁用时由 DestroyZhanShenLaser 停止。
/// ⭐ 狂暴(暴走道具,开火速度翻倍)时同步把激光音效提到 2 倍速。
/// </summary>
private void UpdateZhanShenLaserSound(bool laserVisible)
{
var zsData = currentZhanShenData as ZhanShenTurretData;
bool shouldPlay = laserVisible && this == localPlayerSit && zsData != null && zsData.laserAudio != null;
if (!shouldPlay)
{
// 激光消失/非本地/未配置音效:停止循环(仅本座位在播时才停,避免误停他人座位)
if (_laserSoundOn)
{
_laserSoundOn = false;
AudioManager.Instance.StopFXLoop();
}
return;
}
// ⭐ 狂暴:激光音效同步 2 倍速(与开火速度翻倍一致)
float pitch = IsFrenzyNow() ? 2f : 1f;
if (_laserSoundOn && Mathf.Approximately(pitch, _laserSoundPitch)) return; // 已在播且速度未变 → 无需处理
_laserSoundOn = true;
_laserSoundPitch = pitch;
AudioManager.Instance.PlayFXLoop(zsData.laserAudio, pitch); // 已在播同一条时只变速,不重头播
}
/// <summary>本地座位当前是否狂暴(开火速度翻倍):狂暴标志由面板 SetFrenzy 维护在炮台/战神数据上</summary>
private bool IsFrenzyNow()
{
if (currentZhanShenData != null && currentZhanShenData.isFrenzy) return true;
return currentTurret != null && currentTurret.isFrenzy;
}
2026-08-30 15:29:02 +08:00
/// <summary>隐藏全部战神激光(两种形态都处理)</summary>
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);
}
2026-09-05 23:22:30 +08:00
// 起点/命中特效一并隐藏
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);
}
2026-08-30 15:29:02 +08:00
}
/// <summary>
/// 预制体形态(laserPrefab 非空):JiGuangControl 工具类驱动。
/// 目标数量变化时全量重建(JiGuangCreate),每帧刷新位置/朝向/长度(JiGuangDirection/JiGuangLength,Z 强制 -50)。
2026-08-31 16:40:04 +08:00
/// ⭐ 每条激光独立起点(多起点重载):0=主锁定(座位点+偏移),1/2=散射左右(JGFX父对象位置)。
2026-08-30 15:29:02 +08:00
/// </summary>
private void UpdateZhanShenLaserFx()
{
var zsData = currentZhanShenData;
2026-08-31 16:40:04 +08:00
// 收集每条激光的起点(与 _laserTargets 一一对应)
_laserStarts.Clear();
for (int i = 0; i < _laserTargets.Count; i++)
_laserStarts.Add(GetZhanShenLaserStart(i));
2026-08-30 15:29:02 +08:00
2026-09-05 23:22:30 +08:00
// 目标数量变化时全量重建(锁定建立/解除/三发目标增减时触发),并刷新基准缩放 + 预制体初始旋转偏移
2026-08-30 15:29:02 +08:00
if (_laserFx.Count != _laserTargets.Count)
{
_laserFxBaseScale = zsData.laserPrefab.transform.localScale;
2026-09-05 23:22:30 +08:00
// ⭐ 预制体自带的 Z 轴旋转:在激光预制体上转 Z = 激光平面内旋转(X/Y 不参与,防止甩出平面)
_laserFxRotZOffset = zsData.laserPrefab.transform.localEulerAngles.z;
2026-08-31 16:40:04 +08:00
JiGuangControl.JiGuangCreate(zsData.laserPrefab, _laserStarts, _laserTargets, _laserFx, null, -50f);
2026-09-05 23:22:30 +08:00
// ⭐ 起点/命中特效随激光全量重建(ZhanShenTurretData 上没拖预制体则不生成)
RebuildZhanShenLaserPointFx();
2026-08-30 15:29:02 +08:00
}
2026-08-31 16:40:04 +08:00
// 起点需每帧同步(座位布局/起点球位置变化);隐藏中的重新激活
2026-08-30 15:29:02 +08:00
for (int i = 0; i < _laserFx.Count; i++)
{
if (_laserFx[i] == null) continue;
if (!_laserFx[i].gameObject.activeSelf)
_laserFx[i].gameObject.SetActive(true);
2026-08-31 16:40:04 +08:00
if (i < _laserStarts.Count)
_laserFx[i].position = _laserStarts[i];
2026-08-30 15:29:02 +08:00
}
2026-09-05 23:22:30 +08:00
// 朝向 + 长度(多起点重载:起终点同层 Z=-50;朝向绕世界 Z 叠加预制体 Z 轴旋转偏移)
JiGuangControl.JiGuangDirection(_laserStarts, _laserTargets, _laserFx, -50f, _laserFxRotZOffset);
2026-08-31 16:40:04 +08:00
JiGuangControl.JiGuangLength(_laserStarts, _laserTargets, _laserFx, _laserFxBaseScale, zsData.laserLengthCorrect, -50f);
2026-09-05 23:22:30 +08:00
// ⭐ 起点特效挂 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;
}
}
2026-08-30 15:29:02 +08:00
}
2026-08-31 16:40:04 +08:00
/// <summary>LineRenderer 兜底形态(laserPrefab 为空):按需创建、多余的隐藏。每条激光独立起点</summary>
2026-08-30 15:29:02 +08:00
private void UpdateZhanShenLaserLine()
{
2026-08-29 01:02:09 +08:00
// 需要几条就创建几条(三发=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);
2026-08-31 16:40:04 +08:00
// 起点:第0条=主锁定(座位点+偏移),1/2=散射左右(JGFX父对象位置),Z=-50
Vector3 start = GetZhanShenLaserStart(i);
2026-08-29 17:28:00 +08:00
// 终点:该条锁定鱼的位置(Z=-50,与起点同层)
2026-08-29 01:02:09 +08:00
Vector3 end = _laserTargets[i].position;
2026-08-29 17:28:00 +08:00
end.z = -50f;
2026-08-29 01:02:09 +08:00
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);
}
}
/// <summary>激光终点缓存列表(每帧复用,避免 GC)</summary>
private readonly List<Transform> _laserTargets = new List<Transform>();
/// <summary>
2026-08-30 15:29:02 +08:00
/// 创建一条 LineRenderer 形态的战神激光(第 index 条)。
/// 仅 laserPrefab 为空时使用:直接创建一个新 GameObject 并加上 LineRenderer(用 TurretData 上的宽度/颜色)。
2026-08-29 01:02:09 +08:00
/// </summary>
private LineRenderer CreateZhanShenLaser(int index)
{
var zsData = currentZhanShenData;
2026-08-30 15:29:02 +08:00
GameObject laserGo = new GameObject($"ZhanShenLaser_{index}");
LineRenderer line = laserGo.AddComponent<LineRenderer>();
2026-08-29 01:02:09 +08:00
line.positionCount = Mathf.Max(line.positionCount, 2);
laserGo.transform.SetParent(null); // 世界空间,不随座位/炮台旋转
2026-08-30 15:29:02 +08:00
// 应用 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;
2026-08-29 01:02:09 +08:00
_zhanShenLasers.Add(line);
return line;
}
2026-08-30 15:29:02 +08:00
/// <summary>销毁所有战神激光对象(两种形态都清,清战神/座位禁用时调用)</summary>
2026-08-29 01:02:09 +08:00
private void DestroyZhanShenLaser()
{
for (int i = 0; i < _zhanShenLasers.Count; i++)
{
if (_zhanShenLasers[i] != null)
Destroy(_zhanShenLasers[i].gameObject);
}
_zhanShenLasers.Clear();
2026-08-30 15:29:02 +08:00
for (int i = 0; i < _laserFx.Count; i++)
{
if (_laserFx[i] != null)
Destroy(_laserFx[i].gameObject);
}
_laserFx.Clear();
2026-09-05 23:22:30 +08:00
// 起点/命中特效一并销毁
ClearZhanShenLaserPointFx();
2026-09-06 19:49:25 +08:00
// 准备动画等待状态复位(下次锁定重新计时)
_laserPrepWait = 0f;
_laserLockActive = false;
2026-09-28 20:19:27 +08:00
// 激光循环音效停止(仅本座位在播时才停,避免误停他人座位)
if (_laserSoundOn)
{
_laserSoundOn = false;
AudioManager.Instance.StopFXLoop();
}
2026-09-05 23:22:30 +08:00
}
// ==================== 激光起点/命中特效(三发+锁定) ====================
/// <summary>
/// 激光全量重建时同步重建起点/命中特效(数量与激光一一对应)。
/// 起点=UI 层挂起点节点下(0=turretRotate 的 selfOffsetJGStart 处,1/2=JGFX 左右父对象);命中=世界层 Z=-50 跟随锁定目标。
/// ZhanShenTurretData 上对应预制体没拖(为空)时不生成。
/// </summary>
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));
}
}
}
/// <summary>销毁全部起点/命中特效实例(激光重建/销毁时调用)</summary>
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();
2026-06-16 19:12:37 +08:00
}
2026-08-31 16:40:04 +08:00
// ==================== 战神激光起点(每条激光独立起点) ====================
/// <summary>
/// 战神激光第 index 条的起点(鱼群世界坐标,Z=-50 显示层):
/// - 第 0 条(主锁定):turretRotate 子对象偏移(selfOffsetJGStart 是 turretRotate 的本地偏移,
/// ⭐ 跟随炮台旋转:炮口转向时起点绕炮台中心跟着转)
/// - 第 1 条(散射左):JGFXLeftParent 的位置
/// - 第 2 条(散射右):JGFXRightParent 的位置
/// - 越界/未配置:回退子弹炮口位置(旧版行为)
/// </summary>
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;
}
/// <summary>
/// 座位 UI 上的世界坐标(座位根节点/座位子节点位置)→ 鱼群世界坐标(子弹/激光所在空间)。
/// 与 TurretData.GetBulletStartWorldPos 同一套换算(UI→屏幕→世界),Z 压到指定显示层。
/// </summary>
private Vector3 SeatUIToFishWorld(Vector3 uiWorldPos, float z)
{
Canvas canvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
? UIManager.Instance.uiRoot.GetComponent<Canvas>()
: null;
Camera cam = Camera.main;
if (canvas == null || cam == null)
{
// 编辑器没跑起来时兜底:直接用 UI 坐标压 Z
Vector3 fallback = uiWorldPos;
fallback.z = z;
return fallback;
}
Vector3 screenPos = RectTransformUtility.WorldToScreenPoint(
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera,
uiWorldPos);
float distance = Mathf.Abs(cam.transform.position.z - 0f);
Vector3 worldPos = cam.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, distance));
worldPos.z = z;
return worldPos;
}
/// <summary>
/// 散射激光激活/隐藏左右起点父对象(JGFXLeftParent/JGFXRightParent):
/// 使用散射激光(三发+锁定开着,或锁定点>1)时激活显示起点球,关闭/单发时隐藏。
/// </summary>
private void UpdateJGFXParentsActive(bool scatterActive)
{
if (JGFXLeftParent != null && JGFXLeftParent.gameObject.activeSelf != scatterActive)
JGFXLeftParent.gameObject.SetActive(scatterActive);
if (JGFXRightParent != null && JGFXRightParent.gameObject.activeSelf != scatterActive)
JGFXRightParent.gameObject.SetActive(scatterActive);
}
#if UNITY_EDITOR
/// <summary>散射起点诊断计时器(编辑器排查用,3秒一条,只在战神+锁定期间打印)</summary>
private float _jgfxLogTimer = -99f;
/// <summary>
/// 编辑器诊断:锁定期间每3秒打印散射起点球的判定条件。
/// 用来定位「三发+锁定开了但左右父对象不激活」断在哪个环节:
/// 三发目标数 / 激光条数 / 散射标记 / 左右父对象引用是否拖了 / 当前激活状态。
/// </summary>
private void DebugJGFXState()
{
if (currentZhanShenData == null || !isLock) return;
if (Time.unscaledTime - _jgfxLogTimer < 3f) return;
_jgfxLogTimer = Time.unscaledTime;
Debug.Log($"[SitPosData] 座位{posID} 散射起点诊断: " +
$"三发目标数={tripleLockTargets.Count} 激光条数={_laserTargets.Count} " +
$"散射标记={isScatterLaserOn} " +
$"左父对象={(JGFXLeftParent != null ? "已拖" : "NULL!")} " +
$"右父对象={(JGFXRightParent != null ? "已拖" : "NULL!")} " +
$"左激活={(JGFXLeftParent != null && JGFXLeftParent.gameObject.activeSelf)} " +
$"左子对象数={(JGFXLeftParent != null ? JGFXLeftParent.childCount : -1)}");
}
#endif
/// <summary>
/// 出场/换战神时刷新散射激光起点球:
/// 先清空 JGFXLeftParent/JGFXRightParent 的旧子对象,再各自克隆一份
/// ZhanShenTurretData.JGQiuStartPrefab(保持预制体原始位置/旋转/缩放)。
/// 没配置 JGQiuStartPrefab/组件就只清空并打警告;父对象默认隐藏,散射激光激活时由 UpdateZhanShenLaser 打开。
/// </summary>
private void RefreshJGQiuStartFX()
{
var zs = currentZhanShenData as ZhanShenTurretData;
ClearJGFXParents();
if (zs == null)
{
Debug.LogWarning($"[SitPosData] 战神 #{currentZhanShenNumber} 未挂 ZhanShenTurretData 组件,散射起点球无法创建(座位 {posID})");
return;
}
if (zs.JGQiuStartPrefab == null)
{
Debug.LogWarning($"[SitPosData] 战神 #{currentZhanShenNumber} 的 ZhanShenTurretData.JGQiuStartPrefab 未配置,散射起点球无法创建(座位 {posID})——请在战神预制体上拖入起点球预制体");
return;
}
// 未配置父对象就不克隆(避免起点球落到场景根下),并提示去座位预制体上拖引用
if (JGFXLeftParent == null || JGFXRightParent == null)
Debug.LogWarning($"[SitPosData] 座位{posID} 的 JGFXLeftParent/JGFXRightParent 未拖引用——散射起点球无处挂载,请在座位预制体上把左右起点球父对象拖进 SitPosData(座位 {posID} 战神 #{currentZhanShenNumber})");
if (JGFXLeftParent != null)
Instantiate(zs.JGQiuStartPrefab, JGFXLeftParent, false);
if (JGFXRightParent != null)
Instantiate(zs.JGQiuStartPrefab, JGFXRightParent, false);
}
/// <summary>清空左右起点父对象的子对象并隐藏(清战神/重装起点球前调用)</summary>
private void ClearJGFXParents()
{
ClearChildren(JGFXLeftParent);
ClearChildren(JGFXRightParent);
if (JGFXLeftParent != null)
JGFXLeftParent.gameObject.SetActive(false);
if (JGFXRightParent != null)
JGFXRightParent.gameObject.SetActive(false);
}
/// <summary>清空一个父节点下的所有子对象</summary>
private static void ClearChildren(Transform parent)
{
if (parent == null) return;
for (int i = parent.childCount - 1; i >= 0; i--)
Destroy(parent.GetChild(i).gameObject);
}
#if UNITY_EDITOR
/// <summary>
/// 编辑器可视化:战神主激光起点(turretRotate 子对象偏移 selfOffsetJGStart,⭐ 跟随炮台旋转)。
/// 运行中加载战神后在 Scene 视图可见(旋转炮台时球会绕炮台中心跟着转):
/// 黄色小球=turretRotate 位置(偏移基准),洋红线框球=主激光起点,连线=偏移方向。
/// </summary>
private void OnDrawGizmos()
{
var zs = currentZhanShenData as ZhanShenTurretData;
if (zs == null) return;
// 主起点:turretRotate 的本地偏移(跟随旋转,与 GetZhanShenLaserStart(0) 同一套计算)
Vector3 offset = new Vector3(zs.selfOffsetJGStart.x, zs.selfOffsetJGStart.y, 0f);
Vector3 basePoint = turretRotate != null ? turretRotate.position : transform.position;
Vector3 startPoint = turretRotate != null
? turretRotate.TransformPoint(offset)
: basePoint + offset;
// turretRotate 位置(偏移基准)
Gizmos.color = Color.yellow;
Gizmos.DrawSphere(basePoint, 8f);
// 主激光起点 + 基准点连线(显示偏移关系)
Gizmos.color = Color.magenta;
Gizmos.DrawWireSphere(startPoint, 20f);
Gizmos.DrawLine(basePoint, startPoint);
UnityEditor.Handles.Label(startPoint + Vector3.up * 25f, "主激光起点(跟随炮台旋转)");
}
#endif
2026-07-02 18:28:50 +08:00
/// <summary>
/// 玩家加入 → 显示玩家 UI(child0),隐藏等待界面(child1)。
/// </summary>
public void ShowPlayer()
{
if (transform.childCount > 0)
transform.GetChild(0).gameObject.SetActive(true);
if (transform.childCount > 1)
transform.GetChild(1).gameObject.SetActive(false);
// 如果是本地玩家的座位,自动注册并加载装备
if (user == GameManager.Instance?.user?.id)
{
localPlayerSit = this;
2026-07-28 18:29:49 +08:00
// XJMH 模式:炮台是座位自带子对象,Awake 已初始化引用,不需要加载
if (turretControlMode != TurretControlMode.XJMH)
{
var cannonUsing = GameManager.Instance.user.cannonUsing;
if (cannonUsing != null && int.TryParse(cannonUsing.number, out int num))
LoadTurret(num);
2026-07-02 18:28:50 +08:00
2026-07-28 18:29:49 +08:00
var wingUsing = GameManager.Instance.user.wingUsing;
if (wingUsing != null && int.TryParse(wingUsing.number, out int wingNum))
LoadWing(wingNum);
2026-08-28 22:33:35 +08:00
var godWarUsing = GameManager.Instance.user.godWarUsing;
if (godWarUsing != null && int.TryParse(godWarUsing.number, out int zsNum))
LoadZhanShen(zsNum);
2026-07-28 18:29:49 +08:00
}
2026-07-02 18:28:50 +08:00
}
2026-07-02 19:09:00 +08:00
// 非人机玩家加入时,自动分配为首个房主
if (!isBot)
AssignRoomOwner(this);
2026-07-02 18:28:50 +08:00
}
/// <summary>
/// 玩家退出/等待 → 显示等待界面(child1),隐藏玩家 UI(child0)。
/// </summary>
public void ShowWaiting()
{
if (transform.childCount > 0)
transform.GetChild(0).gameObject.SetActive(false);
if (transform.childCount > 1)
transform.GetChild(1).gameObject.SetActive(true);
}
2026-09-11 19:54:38 +08:00
/// <summary>
/// 设置座位上玩家的显示信息(进房时由 GameMainPanel.JoinGame 调用,
/// 数据来自服务端下发的 RoomInfo.players / RoomJoinNotice):
/// 玩家ID、昵称、头像、VIP 图标(0~10 级一一对应)、VIP 等级背景(每两级一个)。
/// </summary>
2026-09-13 01:40:32 +08:00
/// <param name="userId">玩家唯一 ID(用于判断是否本人)</param>
2026-09-11 19:54:38 +08:00
/// <param name="userName">玩家昵称(显示在 tmp_UserName)</param>
/// <param name="avatar">头像名/URL(走 GameManager 头像缓存,找不到回退默认头像)</param>
/// <param name="vipLevel">VIP 等级 0~10</param>
2026-09-13 01:40:32 +08:00
/// <param name="userNumber">玩家编号(短ID,显示在 tmp_UserNumber;空则回退显示 userId)</param>
2026-09-15 16:30:20 +08:00
/// <param name="level">玩家等级(显示在 tmp_UserLevel)</param>
2026-09-17 22:57:46 +08:00
/// <param name="exp">玩家当前经验(原始值)</param>
/// <param name="maxExp">升级所需总经验(经验条进度 = exp / maxExp)</param>
public void SetUserInfo(string userId, string userName, string avatar, int vipLevel, string userNumber = null, int level = 0, long exp = 0, long maxExp = 0)
2026-09-11 19:54:38 +08:00
{
2026-09-15 16:30:20 +08:00
// 玩家 ID:优先显示后端下发的短编号 UserNumber,没有再用 userId。带 "ID:" 前缀
2026-09-13 01:40:32 +08:00
if (tmp_UserNumber != null)
{
string displayId = string.IsNullOrEmpty(userNumber) ? userId : userNumber;
if (!string.IsNullOrEmpty(displayId))
2026-09-15 16:30:20 +08:00
tmp_UserNumber.text = "ID:" + displayId;
2026-09-13 01:40:32 +08:00
}
2026-09-11 19:54:38 +08:00
2026-09-13 01:40:32 +08:00
// 昵称:非本人的名字打码(前2后2保留,中间***),自己原样显示
2026-09-11 19:54:38 +08:00
if (tmp_UserName != null && !string.IsNullOrEmpty(userName))
2026-09-13 01:40:32 +08:00
tmp_UserName.text = userId == GameManager.Instance?.user?.id
? userName
: MaskUserName(userName);
2026-09-11 19:54:38 +08:00
2026-09-15 16:30:20 +08:00
// 等级:带 "LV:" 前缀显示玩家等级
if (tmp_UserLevel != null && level > 0)
tmp_UserLevel.text = "LV:" + level;
2026-09-17 22:57:46 +08:00
// 经验条:后端下发原始经验值 + 升级所需总经验,换算成 0~1 进度
2026-09-15 16:30:20 +08:00
if (slider_UserLevelEXP != null)
2026-09-17 22:57:46 +08:00
slider_UserLevelEXP.value = maxExp > 0 ? Mathf.Clamp01((float)exp / maxExp) : 0f;
2026-09-15 16:30:20 +08:00
2026-09-11 19:54:38 +08:00
// 头像:优先 GameManager 头像缓存(Resources/Avatar),查不到用默认头像
if (img_Head != null)
{
Sprite head = GameManager.Instance.GetAvatarSprite(avatar);
if (head != null)
img_Head.sprite = head;
}
// VIP 图标:sprite_VipList 共 11 个正好对应 0~10 级,越界取两端,只激活对应的一个
if (sprite_VipList != null && sprite_VipList.Count > 0)
{
int vipIndex = Mathf.Clamp(vipLevel, 0, sprite_VipList.Count - 1);
for (int i = 0; i < sprite_VipList.Count; i++)
{
if (sprite_VipList[i] != null && sprite_VipList[i].activeSelf != (i == vipIndex))
sprite_VipList[i].SetActive(i == vipIndex);
}
}
// VIP 等级背景:sprite_VipLevelBG 共 5 个,每两级对应一个(0 级用最基础的),只激活对应的一个
if (sprite_VipLevelBG != null && sprite_VipLevelBG.Count > 0)
{
int bgIndex = Mathf.Clamp(vipLevel / 2, 0, sprite_VipLevelBG.Count - 1);
for (int i = 0; i < sprite_VipLevelBG.Count; i++)
{
if (sprite_VipLevelBG[i] != null && sprite_VipLevelBG[i].activeSelf != (i == bgIndex))
sprite_VipLevelBG[i].SetActive(i == bgIndex);
}
}
}
2026-09-13 01:40:32 +08:00
/// <summary>
/// 非本人昵称打码:保留前 2 位和后 2 位,中间用 *** 代替。
/// 例:123456789 → 12***89;12345 → 12***45。不足 5 位原样显示。
/// </summary>
private static string MaskUserName(string name)
{
if (string.IsNullOrEmpty(name) || name.Length < 5)
return name;
return name.Substring(0, 1) + "***" + name.Substring(name.Length - 1);
}
2026-09-04 23:38:33 +08:00
#region 表情包显示(spineObj / ziObj)
/// <summary>表情显示时长(秒),超时自动隐藏</summary>
public float biaoQingDuration = 3f;
/// <summary>正在运行的表情隐藏协程(新表情顶掉旧表情)</summary>
private Coroutine _biaoQingCoroutine;
/// <summary>
/// 显示 Spine 动画表情:spineObj 的 child0(SkeletonGraphic)换骨骼资源并播放动画,
/// biaoQingDuration 秒后自动隐藏。与文字表情互斥(同时只显示一种)。
/// </summary>
/// <param name="asset">spine 骨骼资源</param>
/// <param name="animName">动画名</param>
public void ShowBiaoQingSpine(SkeletonDataAsset asset, string animName)
{
if (spineObj == null || asset == null || string.IsNullOrEmpty(animName)) return;
// child0 = SkeletonGraphic
var sg = spineObj.transform.childCount > 0
? spineObj.transform.GetChild(0).GetComponent<SkeletonGraphic>()
: null;
if (sg == null) return;
if (ziObj != null) ziObj.SetActive(false);
if (_biaoQingCoroutine != null) { StopCoroutine(_biaoQingCoroutine); _biaoQingCoroutine = null; }
sg.skeletonDataAsset = asset;
sg.Initialize(false);
sg.AnimationState.SetAnimation(0, animName, true);
spineObj.SetActive(true);
_biaoQingCoroutine = StartCoroutine(HideBiaoQingAfter(spineObj));
}
/// <summary>
2026-09-06 15:51:51 +08:00
/// 显示文字表情:先设置文字再激活 ziObj,激活后立即强制刷新两级 ContentSizeFitter
/// (child0 的 TMP 自身 + 父物体),避免宽度延迟一帧才更新。
2026-09-04 23:38:33 +08:00
/// biaoQingDuration 秒后自动隐藏。与 spine 表情互斥(同时只显示一种)。
/// </summary>
/// <param name="text">文字内容</param>
public void ShowBiaoQingZi(string text)
{
if (ziObj == null || string.IsNullOrEmpty(text)) return;
// child0 = TextMeshProUGUI
var tmp = ziObj.transform.childCount > 0
? ziObj.transform.GetChild(0).GetComponent<TextMeshProUGUI>()
: null;
if (tmp == null) return;
if (spineObj != null) spineObj.SetActive(false);
if (_biaoQingCoroutine != null) { StopCoroutine(_biaoQingCoroutine); _biaoQingCoroutine = null; }
2026-09-06 15:51:51 +08:00
// ⭐ 先设置字,再激活:TMP 非激活时改字不会向布局系统注册重建,激活后统一处理
tmp.text = text;
2026-09-04 23:38:33 +08:00
ziObj.SetActive(true);
2026-09-06 00:54:41 +08:00
2026-09-06 15:51:51 +08:00
// ⭐ 激活后立即按"先子后父"顺序强制刷新两级 ContentSizeFitter:
// 子级 TMP 先算出新宽度,父级 Fitter 再根据子级新宽度收缩,
// 否则布局要等帧末才重建,当帧读到的还是旧宽度
LayoutRebuilder.ForceRebuildLayoutImmediate(tmp.rectTransform);
LayoutRebuilder.ForceRebuildLayoutImmediate(ziObj.transform as RectTransform);
Canvas.ForceUpdateCanvases();
_biaoQingCoroutine = StartCoroutine(HideBiaoQingAfter(ziObj));
2026-09-04 23:38:33 +08:00
}
/// <summary>表情显示 biaoQingDuration 秒后自动隐藏</summary>
private IEnumerator HideBiaoQingAfter(GameObject obj)
{
yield return new WaitForSeconds(biaoQingDuration);
if (obj != null) obj.SetActive(false);
_biaoQingCoroutine = null;
}
#endregion
2026-06-16 19:12:37 +08:00
/// <summary>房主退出后,将房主身份转移给第一个活跃的非人机玩家</summary>
2026-07-25 23:05:24 +08:00
/// <remarks>鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。</remarks>
2026-06-16 19:12:37 +08:00
public static void TransferRoomOwner()
{
var list = FindObjectsOfType<SitPosData>();
foreach (var sit in list)
{
if (!sit.isBot && sit.gameObject.activeInHierarchy && sit != roomOwnerSit)
{
roomOwnerSit = sit;
break;
}
}
}
/// <summary>分配房主(首个非人机玩家)</summary>
2026-07-25 23:05:24 +08:00
/// <remarks>鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。</remarks>
2026-06-16 19:12:37 +08:00
public static void AssignRoomOwner(SitPosData sit)
{
if (roomOwnerSit == null && !sit.isBot)
2026-07-14 02:48:54 +08:00
{
2026-06-16 19:12:37 +08:00
roomOwnerSit = sit;
2026-07-14 02:48:54 +08:00
}
2026-06-08 21:23:40 +08:00
}
2026-07-28 18:29:49 +08:00
// ==================== 炮台加载 ====================
2026-06-08 21:23:40 +08:00
2026-07-28 21:11:57 +08:00
/// <summary>
/// 获取炮台的旋转节点。
/// - 若 TurretData.tra_RotationPivot 已赋值 → 用它(精确指定旋转中心)
/// - 否则回退旧逻辑:炮台父节点(Normal 克隆的中间容器 / XJMH 子对象的父节点)
/// </summary>
private Transform GetTurretRotationPivot(TurretData turret)
{
if (turret == null) return null;
2026-08-30 19:57:45 +08:00
// ⭐ 已分离旋转(用 turretRotate 旋转)时:tra_CurrTurret 指向 turretRotate 本身。
// 瞄准代码用 tra_CurrTurret.transform.parent(=beij,固定不转)作为参考系,
// 上方座位 beij 预设的 180° 翻转与面板 IsFlipped+180 补偿相互抵消,
// currentAngle 恢复 aim-90 的统一约定(炮口/子弹/激光方向全部一致)。
// turretRotate.anchoredPosition 与旧 turretParent 同节点同值,瞄准原点不变;
// beij 不参与旋转,参考系不会抖动。
2026-08-27 17:28:19 +08:00
if (turretRotate != null)
2026-08-30 19:57:45 +08:00
return turretRotate;
2026-08-27 17:28:19 +08:00
2026-07-28 21:11:57 +08:00
// 优先用 TurretData 上配置的旋转节点
if (turret.tra_RotationPivot != null)
return turret.tra_RotationPivot;
// 回退:用父节点(兼容旧逻辑)
Transform parent = turret.transform.parent;
return parent != null ? parent : turret.transform;
}
2026-06-08 21:23:40 +08:00
/// <summary>
2026-07-28 18:29:49 +08:00
/// 根据炮台编号加载炮台。
/// Normal 模式:从 Addressables 加载预制体并克隆到 turretParent。
/// XJMH 模式:炮台是座位子对象,激活对应编号子对象、停用其他。
2026-06-08 21:23:40 +08:00
/// </summary>
public void LoadTurret(int turretNumber)
2026-07-28 18:29:49 +08:00
{
if (turretControlMode == TurretControlMode.XJMH)
{
SwitchTurretChild(turretNumber);
return;
}
LoadTurretNormal(turretNumber);
}
/// <summary>
/// 普通模式:从 Addressables 加载预制体并克隆到 turretParent。
/// 位置/旋转/缩放保持预制体原始值,不动。
/// </summary>
private void LoadTurretNormal(int turretNumber)
2026-02-09 10:44:38 +08:00
{
2026-09-04 23:38:33 +08:00
// ⭐ 装备战神时切炮台不顶掉战神:战神保持显示并继续接管开火状态机,
// 新炮台照常加载到 turretParent(战神装备中它是隐藏的),卸下战神(ClearZhanShen)后自动显示新炮台
bool zhanShenActive = currentZhanShenData != null;
2026-08-28 22:33:35 +08:00
2026-07-13 04:48:18 +08:00
// ⭐ 换炮台时只清活跃子弹(清屏),保留池缓存(同 prefab 能复用)
// 用 ClearActive 而非 ClearAll:ClearAll 会清池导致永远不复用
BulletPool.Instance.ClearActive();
2026-02-03 11:35:19 +08:00
if (currentTurret != null)
2026-06-08 21:23:40 +08:00
{
Destroy(currentTurret.gameObject);
currentTurret = null;
}
2026-05-02 23:52:48 +08:00
2026-06-08 21:23:40 +08:00
currentTurretNumber = turretNumber;
2026-05-04 17:21:52 +08:00
2026-06-08 21:23:40 +08:00
// 从 TurretResourcesData 拿预制体
GameObject prefab = TurretResourcesData.GetTurretPrefab(turretNumber);
if (prefab == null)
{
Debug.LogWarning($"[SitPosData] 炮台预制体未加载: #{turretNumber}");
return;
}
2026-02-03 11:35:19 +08:00
2026-06-08 21:23:40 +08:00
// 克隆到父节点
GameObject instance = Instantiate(prefab, turretParent, false);
instance.SetActive(true);
// 固定位置归零,由外部父节点定位
var rt = instance.GetComponent<RectTransform>();
if (rt != null)
rt.anchoredPosition = turretAnchoredPos;
currentTurret = instance.GetComponent<TurretData>();
if (currentTurret == null)
{
Debug.LogError($"[SitPosData] 预制体上未找到 TurretData 组件: #{turretNumber}");
Destroy(instance);
return;
}
currentTurret.turretNumber = turretNumber;
2026-07-28 21:11:57 +08:00
// 缓存旋转节点:优先用 TurretData.tra_RotationPivot,否则回退父节点
tra_CurrTurret = GetTurretRotationPivot(currentTurret);
2026-06-08 21:23:40 +08:00
2026-09-04 23:38:33 +08:00
// 绑定状态机入口:战神装备中不抢(保持战神接管开火),无战神时才交给炮台
if (!zhanShenActive)
OnStateChange = currentTurret.ChangeState;
// 无战神时确保炮台可见;战神装备中保持 turretParent 隐藏(卸下时由 ClearZhanShen 恢复显示)
if (turretParent != null && !zhanShenActive)
turretParent.gameObject.SetActive(true);
2026-02-03 11:35:19 +08:00
2026-07-16 20:16:05 +08:00
// ⭐ 缓存炮台在世界坐标(Z=0平面)的位置,bot AI/锁定检测距离时用
UpdateTurretWorldPos();
2026-09-04 23:38:33 +08:00
Debug.Log($"[SitPosData] 炮台已加载(Normal): #{turretNumber}" + (zhanShenActive ? "(战神装备中,炮台待命)" : ""));
2026-07-28 18:29:49 +08:00
}
/// <summary>
/// XJMH 模式:炮台都是座位的子对象,激活对应编号的炮台、停用其他。
/// 不需要 Instantiate/Destroy,只做激活/停用,无需额外处理。
/// 优先从 <see cref="xjmhTurretList"/> 查找,列表为空时回退遍历子对象。
/// </summary>
private void SwitchTurretChild(int turretNumber)
{
BulletPool.Instance.ClearActive();
// 炮台挂载根:turretParent 优先,否则用座位自身
Transform parent = turretParent != null ? turretParent : transform;
// 优先用 Inspector 配置的炮台列表
TurretData found = null;
if (xjmhTurretList != null && xjmhTurretList.Count > 0)
{
foreach (var td in xjmhTurretList)
{
if (td == null) continue;
bool active = td.turretNumber == turretNumber;
td.gameObject.SetActive(active);
if (active) found = td;
}
}
else
{
// 回退:列表未配置时遍历子对象
for (int i = 0; i < parent.childCount; i++)
{
var child = parent.GetChild(i);
var td = child.GetComponent<TurretData>();
if (td == null) continue;
if (td.turretNumber == turretNumber)
{
child.gameObject.SetActive(true);
found = td;
}
else
{
child.gameObject.SetActive(false);
}
}
}
if (found == null)
{
Debug.LogWarning($"[SitPosData] XJMH 子对象中未找到炮台 #{turretNumber}");
return;
}
currentTurretNumber = turretNumber;
currentTurret = found;
2026-07-28 21:11:57 +08:00
// 缓存旋转节点:优先用 TurretData.tra_RotationPivot,否则回退父节点
tra_CurrTurret = GetTurretRotationPivot(currentTurret);
2026-07-28 18:29:49 +08:00
OnStateChange = currentTurret.ChangeState;
UpdateTurretWorldPos();
Debug.Log($"[SitPosData] 炮台已切换(XJMH): #{turretNumber}");
}
/// <summary>
/// XJMH 模式:按列表索引直接切换炮台。
/// 用于扇形切换 UI 中按位置选择炮台,避免再做编号匹配。
/// </summary>
/// <param name="index">xjmhTurretList 中的索引</param>
/// <returns>切换成功返回 true;索引越界或列表为空返回 false</returns>
public bool SwitchTurretByIndex(int index)
{
if (turretControlMode != TurretControlMode.XJMH) return false;
if (xjmhTurretList == null || index < 0 || index >= xjmhTurretList.Count) return false;
var td = xjmhTurretList[index];
if (td == null) return false;
// 命中当前炮台则无需切换
if (td.turretNumber == currentTurretNumber) return true;
SwitchTurretChild(td.turretNumber);
return true;
}
/// <summary>
/// XJMH 模式:根据当前炮台在 xjmhTurretList 中的索引返回下一个炮台编号。
/// 用于"切换到下一把"的快捷操作。循环切换,列表为空返回 -1。
/// </summary>
public int GetNextTurretNumber()
{
if (turretControlMode != TurretControlMode.XJMH) return -1;
if (xjmhTurretList == null || xjmhTurretList.Count == 0) return -1;
int currIdx = xjmhTurretList.FindIndex(td => td != null && td.turretNumber == currentTurretNumber);
if (currIdx < 0) return xjmhTurretList[0].turretNumber;
int nextIdx = (currIdx + 1) % xjmhTurretList.Count;
return xjmhTurretList[nextIdx].turretNumber;
}
/// <summary>
/// 获取当前可用的炮台编号列表。
/// Normal 模式:从资源表按类型查询。
/// XJMH 模式:优先从 <see cref="xjmhTurretList"/> 读取,列表为空时回退扫描座位子对象。
/// </summary>
public List<int> GetAvailableTurretNumbers()
{
var result = new List<int>();
if (turretControlMode == TurretControlMode.XJMH)
{
// 优先用配置的炮台列表
if (xjmhTurretList != null && xjmhTurretList.Count > 0)
{
foreach (var td in xjmhTurretList)
if (td != null) result.Add(td.turretNumber);
}
else
{
// 回退:扫描座位子对象
Transform parent = turretParent != null ? turretParent : transform;
for (int i = 0; i < parent.childCount; i++)
{
var td = parent.GetChild(i).GetComponent<TurretData>();
if (td != null)
result.Add(td.turretNumber);
}
}
}
else
{
var list = TurretResourcesData.GetTurretDataByType(1);
if (list != null)
result = list.ConvertAll(d => d.turretNumber);
}
return result;
2026-06-08 21:23:40 +08:00
}
2026-05-18 18:39:11 +08:00
2026-07-16 20:16:05 +08:00
/// <summary>
/// 把炮台 UI 坐标转换成世界坐标(Z=0平面)并缓存到 turretWorldPos。
/// 转换方式和 TurretData.GetBulletStartWorldPos 一致。
/// </summary>
public void UpdateTurretWorldPos()
{
if (tra_CurrTurret == null)
{
turretWorldPos = Vector3.zero;
return;
}
RectTransform rect = tra_CurrTurret.GetComponent<RectTransform>();
if (rect != null && UIManager.Instance != null && UIManager.Instance.uiRoot != null)
{
Canvas canvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
Camera cam = Camera.main;
Vector3 screenPos = RectTransformUtility.WorldToScreenPoint(
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera,
rect.position
);
float distance = Mathf.Abs(cam.transform.position.z - 0f);
Vector3 worldPos = cam.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, distance));
worldPos.z = 0f;
turretWorldPos = worldPos;
}
else
{
// 非 UI(3D 物体)直接用 position
turretWorldPos = tra_CurrTurret.position;
}
}
2026-06-08 21:23:40 +08:00
/// <summary>根据炮台编号字符串加载。</summary>
public void LoadTurret(string turretNumberStr)
{
if (int.TryParse(turretNumberStr, out int num))
LoadTurret(num);
2026-02-03 11:35:19 +08:00
}
2026-06-29 20:40:30 +08:00
// ==================== 翅膀加载(动态克隆) ====================
2026-05-02 23:52:48 +08:00
2026-06-29 20:40:30 +08:00
/// <summary>
/// 根据炮台编号,从 Addressables 加载预制体并克隆到 turretParent。
/// 位置/旋转/缩放保持预制体原始值,不动。
/// </summary>
public void LoadWing(int turretNumber)
{
2026-07-28 18:29:49 +08:00
// XJMH 模式没有翅膀概念,直接跳过
if (turretControlMode == TurretControlMode.XJMH) return;
2026-09-03 16:20:30 +08:00
// turretNumber<=0 = 卸下(与战神同一约定):只清理不加载,不打警告
if (turretNumber <= 0)
2026-06-29 21:06:36 +08:00
{
2026-09-03 16:20:30 +08:00
ClearWing();
return;
2026-06-29 21:06:36 +08:00
}
2026-09-03 16:20:30 +08:00
// 先清理已有翅膀
ClearWing();
2026-06-29 21:06:36 +08:00
// 从 TurretResourcesData 拿预制体
GameObject prefab = TurretResourcesData.GetTurretPrefab(turretNumber);
if (prefab == null)
{
Debug.LogWarning($"[SitPosData] 翅膀预制体未加载: #{turretNumber}");
return;
}
// 克隆到 wingParent,保持预制体原始位置/旋转/缩放
GameObject instance = Instantiate(prefab, wingParent, false);
2026-09-24 08:49:24 +08:00
// ⭐ 特殊鱼炮台(钻头/激光)使用中:只切换不显示,等爆奖流程结束由 RestoreTurretVisual 恢复显示
instance.SetActive(!isDrillActive);
if (isDrillActive) _pendingShowWing = instance;
2026-06-29 21:06:36 +08:00
2026-09-24 08:49:24 +08:00
Debug.Log($"[SitPosData] 翅膀已加载: #{turretNumber}{(isDrillActive ? "(特殊炮台使用中,暂缓显示)" : "")}");
2026-06-29 20:40:30 +08:00
}
2026-09-03 16:20:30 +08:00
/// <summary>卸下翅膀:清空 wingParent 下的翅膀模型(保留预设的 Camera,它是用来渲染翅膀到 RawImage 的)。</summary>
public void ClearWing()
{
if (turretControlMode == TurretControlMode.XJMH) return;
for (int i = wingParent.childCount - 1; i >= 0; i--)
{
var child = wingParent.GetChild(i);
if (child.GetComponent<Camera>() != null)
continue; // 保留 Camera
Destroy(child.gameObject);
}
}
2026-06-29 20:40:30 +08:00
/// <summary>根据炮台编号字符串加载。</summary>
public void LoadWing(string turretNumberStr)
{
2026-06-29 21:06:36 +08:00
if (int.TryParse(turretNumberStr, out int num))
LoadWing(num);
2026-06-29 20:40:30 +08:00
}
2026-08-28 22:33:35 +08:00
// ==================== 战神加载(动态克隆,同翅膀) ====================
/// <summary>
/// 根据战神编号,加载游戏内使用的战神模型(ZSYX 标签的那个)。
/// 处理方式和翅膀一样:直接克隆到 zhanShenParent、保持预制体原始位置/旋转/缩放,不再管其他的
/// (渲染由座位预设的 Camera + RawImage 完成,显示节点挂在 turretRotate 下会跟随瞄准旋转)。
/// ⭐ 炮台/战神显示二选一:加载战神后隐藏 turretParent(仅游戏内,切换面板 UI 不受影响)。
/// </summary>
public void LoadZhanShen(int zhanShenNumber)
{
// XJMH 模式没有战神概念,直接跳过(和翅膀一致)
if (turretControlMode == TurretControlMode.XJMH) return;
// 先清理已有战神(保留预设的 Camera,它是用来渲染战神到 RawImage 的)
ClearZhanShenInstance();
// 从 TurretResourcesData 拿游戏内使用的战神预制体
GameObject prefab = TurretResourcesData.GetZhanShenPrefab(zhanShenNumber, ZhanShenUsage.GamePlay);
if (prefab == null)
{
Debug.LogWarning($"[SitPosData] 战神使用预制体未加载: #{zhanShenNumber}(检查 ZSYX 标签配置)");
return;
}
// 克隆到 zhanShenParent,保持预制体原始位置/旋转/缩放
GameObject instance = Instantiate(prefab, zhanShenParent, false);
2026-09-24 08:49:24 +08:00
// ⭐ 特殊鱼炮台(钻头/激光)使用中:只切换不显示,等爆奖流程结束由 RestoreTurretVisual 恢复显示
instance.SetActive(!isDrillActive);
2026-08-28 22:33:35 +08:00
// 绑定状态机入口:战神接管开火/待机动画(PlayFire 约定,见 TurretData.PlayWarGodAnimator)
// 同时缓存 TurretData:开火时优先用战神的子弹预制体(bulletPrefab/bulletPrefab_Two)
var zsData = instance.GetComponent<TurretData>();
2026-09-01 21:19:15 +08:00
// ⭐ 战神初始旋转偏移:从 ZhanShenTurretData 读取(每个战神角度不一样,跟战神预制体配置)。
// 上/下座位视角不一样分开配置(IsFlipped=上方);直接设置欧拉角(覆盖预制体原始旋转);全0=保持默认朝向。
var zsDataEx = zsData as ZhanShenTurretData;
if (zsDataEx != null)
{
Vector3 zsRotateInit = IsFlipped ? zsDataEx.topOffsetZSRotateInit : zsDataEx.buttomOffsetZSRotateInit;
if (zsRotateInit != Vector3.zero)
instance.transform.localEulerAngles = zsRotateInit;
}
currentZhanShen = instance;
currentZhanShenNumber = zhanShenNumber;
2026-08-28 22:33:35 +08:00
currentZhanShenData = zsData;
if (zsData != null)
{
zsData.turretNumber = zhanShenNumber;
2026-08-29 01:02:09 +08:00
zsData.setPosData = this; // 注入座位引用:FireIndex 动态切换(锁定=1激光/非锁定=0子弹)需要查锁定状态
2026-08-28 22:33:35 +08:00
OnStateChange = zsData.ChangeState;
}
2026-08-31 16:40:04 +08:00
// ⭐ 出场/换战神:清空左右起点父对象并重新克隆 JGQiuStartPrefab(当前战神配置的起点球)
RefreshJGQiuStartFX();
2026-08-28 22:33:35 +08:00
// ⭐ 炮台/战神显示二选一:显示战神时隐藏普通炮台
if (turretParent != null)
turretParent.gameObject.SetActive(false);
Debug.Log($"[SitPosData] 战神已加载: #{zhanShenNumber}");
}
/// <summary>根据战神编号字符串加载。</summary>
public void LoadZhanShen(string zhanShenNumberStr)
{
if (int.TryParse(zhanShenNumberStr, out int num))
LoadZhanShen(num);
}
/// <summary>清除战神并恢复普通炮台显示(炮台/战神显示二选一的另一半)</summary>
public void ClearZhanShen()
{
ClearZhanShenInstance();
currentZhanShenNumber = 0;
2026-09-24 08:49:24 +08:00
// 恢复普通炮台显示(特殊鱼炮台使用中不显示:等 RestoreTurretVisual 统一恢复)
if (turretParent != null && !isDrillActive)
2026-08-28 22:33:35 +08:00
turretParent.gameObject.SetActive(true);
// 状态机入口还给普通炮台(若已加载)
if (currentTurret != null)
OnStateChange = currentTurret.ChangeState;
}
/// <summary>只清 zhanShenParent 下的战神实例(保留 Camera),不动炮台显隐</summary>
private void ClearZhanShenInstance()
{
if (zhanShenParent != null)
{
for (int i = zhanShenParent.childCount - 1; i >= 0; i--)
{
var child = zhanShenParent.GetChild(i);
if (child.GetComponent<Camera>() != null)
continue; // 保留 Camera
Destroy(child.gameObject);
}
}
currentZhanShen = null;
currentZhanShenData = null;
2026-08-29 01:02:09 +08:00
// 战神清了激光也没了
DestroyZhanShenLaser();
2026-08-31 16:40:04 +08:00
// 散射起点球也清掉(下次出场/换战神时重建)
ClearJGFXParents();
2026-08-28 22:33:35 +08:00
}
2026-09-24 08:49:24 +08:00
/// <summary>
/// 特殊鱼炮台(钻头/激光)爆奖结束后的统一显示恢复入口(ZuanTouXie_DieEvent 调用)。
/// ⭐ 炮台/战神二选一按"当前装备"恢复,不依赖爆奖前的记录:
/// 1. 爆奖期间切换的翅膀(_pendingShowWing) → 恢复显示;
/// 2. 有战神(含爆奖期间新切的,currentZhanShen!=null) → 显示战神,普通炮台保持隐藏;
/// 3. 无战神 → 显示普通炮台 turretParent。
/// 显示后强制 None→Idle 归位:炮台/战神被隐藏期间 Animator 状态会丢失,
/// 若停在 Attack 状态,下次开火 ChangeState(Attack) 会被同状态短路 → 动画定格不播。
/// </summary>
public void RestoreTurretVisual()
{
// 挂起的翅膀:恢复显示(引用可能已被 ClearWing/再次切换销毁,Unity 判空天然处理)
if (_pendingShowWing != null)
{
_pendingShowWing.SetActive(true);
_pendingShowWing = null;
}
if (currentZhanShen != null)
{
// 战神出战:普通炮台保持隐藏(炮台/战神二选一)
if (turretParent != null)
turretParent.gameObject.SetActive(false);
currentZhanShen.SetActive(true);
}
else if (turretParent != null)
{
turretParent.gameObject.SetActive(true);
}
// 动画归位:先 None 再 Idle,强制重进 Idle(避免 Attack 定格)
ChangeState(TurretState.None);
ChangeState(TurretState.Idle);
}
2026-05-02 23:52:48 +08:00
// -------------------------
// 切换状态(GameMainPanel 调用)
// -------------------------
public void ChangeState(TurretState state)
2026-02-03 11:35:19 +08:00
{
2026-05-02 23:52:48 +08:00
OnStateChange?.Invoke(state);
2026-05-01 06:26:50 +08:00
}
2026-05-02 23:52:48 +08:00
// -------------------------
2026-05-04 17:21:52 +08:00
// 炮台旋转(GameMainPanel 调用)
2026-05-02 23:52:48 +08:00
// -------------------------
public void SetRotation(float angle)
{
2026-08-29 17:28:00 +08:00
// ⭐ 激光发射期间锁定方向:忽略旋转请求(激光沿原方向打到爆奖结束)
if (isLaserLocked) return;
2026-09-21 16:39:08 +08:00
// ⭐ 最大旋转角度约束:炮口只能偏离基准方向(下方座位朝上=0°,上方座位朝下=180°)±maxRotationAngle
// - 本地玩家瞄准/远程同步/bot 旋转全部经过这里,统一钳制
// - 起飞炮台飞行期间豁免(炮口固定朝上,PointUp 传 0° 会被上方座位基准 180° 错误钳制)
// - maxRotationAngle<=0 或 >=180 表示不限制
if (maxRotationAngle > 0f && maxRotationAngle < 180f
&& !(currentTurret is FlyTurretData flyTurret && flyTurret.IsFlying))
{
float baseAngle = IsFlipped ? 180f : 0f;
// DeltaAngle 取 [-180,180] 最短差,正确处理 350°/-10° 等角度回绕
float delta = Mathf.DeltaAngle(baseAngle, angle);
delta = Mathf.Clamp(delta, -maxRotationAngle, maxRotationAngle);
angle = baseAngle + delta;
}
2026-05-02 23:52:48 +08:00
currentAngle = angle;
2026-09-01 23:35:24 +08:00
// ⭐ 两个分支(Normal / XJMH)分开处理:
// - Normal(turretRotate 已配置):旋转只作用于 turretRotate,用【世界旋转】——瞄准参考系是 beij
// (tra_CurrTurret 的父节点),currentAngle 已是和 beij 翻转抵消后的统一角度(aim-90),
// 直接写世界旋转炮口即对准目标;若用本地旋转,上方座位 beij 的 180° 翻转会叠加进来导致反向。
// - XJMH(星际迷航,turretRotate 未配置):保持旧版行为,直接转 tra_CurrTurret(世界旋转),
// 该分支不动。
2026-08-27 17:28:19 +08:00
if (turretRotate != null)
turretRotate.rotation = Quaternion.Euler(0, 0, angle);
2026-09-01 23:35:24 +08:00
else if (tra_CurrTurret != null)
tra_CurrTurret.rotation = Quaternion.Euler(0, 0, angle);
2026-05-02 23:52:48 +08:00
}
// -------------------------
// 获取发射点(给 GameMainPanel)
// -------------------------
public Vector2 GetBulletStartPos()
{
2026-06-08 21:23:40 +08:00
return currentTurret != null
? currentTurret.GetBulletStartWorldPos()
: Vector2.zero;
2026-05-02 23:52:48 +08:00
}
2026-06-08 21:23:40 +08:00
// ==================== 金币消耗 ====================
2026-07-30 23:54:35 +08:00
public bool ConsumeGold(int mul = 1, long fireConsumeGold = 0)
2026-05-02 23:52:48 +08:00
{
2026-07-30 23:54:35 +08:00
// fireConsumeGold>0: 用服务端返回的该发子弹权威扣费金额
// fireConsumeGold=0: 回退到本地 consumeGoldNum * mul
long useConsume = fireConsumeGold > 0 ? fireConsumeGold : consumeGoldNum * mul;
long temp = goldNum - useConsume;
2026-05-02 23:52:48 +08:00
if (temp < 0) return false;
2026-07-29 17:17:14 +08:00
long oldGold = goldNum;
2026-05-02 23:52:48 +08:00
goldNum = temp;
tMP_GoldNum.text = goldNum.ToString();
2026-07-29 22:59:26 +08:00
// ⭐ 记录金币流水:已扣钱(开火消耗,本地已执行)
2026-07-30 23:54:35 +08:00
GameManager.LogGoldFlowAppliedCost("Fire", $"开火消耗 原有金币={oldGold} 消耗金币=-{useConsume} 剩余金币={goldNum} 座位={posID} 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
2026-05-02 23:52:48 +08:00
return true;
}
2026-06-08 21:23:40 +08:00
int indexConsume = 0;
2026-07-19 18:24:32 +08:00
/// <summary>
/// 根据后端下发的炮倍值同步 indexConsume 到 paoBeiList 对应档位,
/// 避免加减炮倍时从 0 档开始导致跳变。
/// </summary>
public void SyncIndexByConsume(long consumeGold)
{
var list = CreateFishManager.Instance?.currGameType?.paoBeiList;
if (list == null || list.Count == 0) return;
int idx = list.IndexOf((int)consumeGold);
2026-09-09 00:01:11 +08:00
// ⭐ paoBeiList 为从大到小排列:查不到值时兜底到最小档(列表末尾),避免默认落到最大炮倍
indexConsume = idx >= 0 ? idx : list.Count - 1;
2026-07-19 18:24:32 +08:00
}
2026-05-27 19:47:06 +08:00
public void ChangeTurretAddConsumeNum()
2026-05-01 06:26:50 +08:00
{
2026-09-09 00:01:11 +08:00
// ⭐ paoBeiList 为从大到小排列:加号=更大的炮倍=更小的索引,到头(最大档)回绕到末尾(最小档)
if (indexConsume > 0)
indexConsume--;
2026-05-27 19:47:06 +08:00
else
2026-09-09 00:01:11 +08:00
indexConsume = CreateFishManager.Instance.currGameType.paoBeiList.Count - 1;
2026-06-16 02:28:58 +08:00
AudioManager.Instance.PlayerFX(62011);
2026-06-08 21:23:40 +08:00
2026-07-30 23:54:35 +08:00
// ⭐ 不立即修改 consumeGoldNum 和 UI,等服务端 ChangeGunMultipleNotice 确认后再应用
// 避免:客户端先切换炮倍 → 开火按新炮倍扣费 → 服务端还没确认按旧炮倍扣费 → 差值
int newConsume = CreateFishManager.Instance.currGameType.paoBeiList[indexConsume];
string roomId = GameManager.Instance.gameData?.currRoomData?.roomId;
if (!string.IsNullOrEmpty(roomId))
WebSocketSendList.ChangeGunMultiple(roomId, user, newConsume);
2026-05-01 06:26:50 +08:00
}
2026-05-02 23:52:48 +08:00
2026-09-15 16:30:20 +08:00
/// <summary>
/// 非本地座位隐藏加减炮倍 UI(GameMainPanel 进房时对自己以外的座位调用):
/// - Normal:隐藏座位上的 btn_TurretAdd / btn_TurretMinus(未拖引用为 null 时跳过)
/// - XJMH:加减倍按钮适配在炮台上(TurretData.obj_Add / obj_Down),座位上没有按钮引用,
/// 把 XJMH 炮台列表里每把炮台的加减倍都隐藏(全部藏,之后切换炮台新炮台的也不会再显示)
/// </summary>
public void HideTurretAddMinusUI()
{
if (btn_TurretAdd != null)
btn_TurretAdd.gameObject.SetActive(false);
if (btn_TurretMinus != null)
btn_TurretMinus.gameObject.SetActive(false);
if (turretControlMode != TurretControlMode.XJMH) return;
if (xjmhTurretList != null && xjmhTurretList.Count > 0)
{
foreach (var td in xjmhTurretList)
HideTurretAddDown(td);
}
else
{
// 兜底:列表未配置时扫 turretParent 子对象(与 SwitchTurretChild 回退一致)
Transform parent = turretParent != null ? turretParent : transform;
for (int i = 0; i < parent.childCount; i++)
HideTurretAddDown(parent.GetChild(i).GetComponent<TurretData>());
}
}
/// <summary>隐藏一把 XJMH 炮台上的加减倍对象(obj_Add / obj_Down,未拖引用跳过)</summary>
private static void HideTurretAddDown(TurretData td)
{
if (td == null) return;
if (td.obj_Add != null)
td.obj_Add.SetActive(false);
if (td.obj_Down != null)
td.obj_Down.SetActive(false);
}
2026-05-27 19:47:06 +08:00
public void ChangeTurretMinusConsumeNum()
2026-05-01 06:26:50 +08:00
{
2026-09-09 00:01:11 +08:00
// ⭐ paoBeiList 为从大到小排列:减号=更小的炮倍=更大的索引,到尾(最小档)回绕到头部(最大档)
if (CreateFishManager.Instance.currGameType.paoBeiList.Count - 1 > indexConsume)
indexConsume++;
2026-05-27 19:47:06 +08:00
else
2026-09-09 00:01:11 +08:00
indexConsume = 0;
2026-06-16 02:28:58 +08:00
AudioManager.Instance.PlayerFX(62010);
2026-06-08 21:23:40 +08:00
2026-07-30 23:54:35 +08:00
// ⭐ 不立即修改 consumeGoldNum 和 UI,等服务端 ChangeGunMultipleNotice 确认后再应用
// 避免:客户端先切换炮倍 → 开火按新炮倍扣费 → 服务端还没确认按旧炮倍扣费 → 差值
int newConsume = CreateFishManager.Instance.currGameType.paoBeiList[indexConsume];
string roomId = GameManager.Instance.gameData?.currRoomData?.roomId;
if (!string.IsNullOrEmpty(roomId))
WebSocketSendList.ChangeGunMultiple(roomId, user, newConsume);
2026-02-03 11:35:19 +08:00
}
2026-05-04 17:21:52 +08:00
2026-09-04 00:31:26 +08:00
// ==================== 炮倍选择面板 ====================
private Coroutine _paoBeiPanelAnim;
/// <summary>炮倍面板展开时的原始高度(Awake 记录,动画在 0 ↔ 该值之间滑动)</summary>
private float _paoBeiPanelOpenHeight = -1f;
/// <summary>记录炮倍面板的原始高度(Awake 时面板还没被动画改过,rect.height 即还原态高度)</summary>
private void EnsurePaoBeiPanelHeight()
{
if (_paoBeiPanelOpenHeight >= 0f || selectPaoBeiPanel == null) return;
var rt = selectPaoBeiPanel.GetComponent<RectTransform>();
if (rt == null) return;
_paoBeiPanelOpenHeight = rt.rect.height;
if (_paoBeiPanelOpenHeight <= 0f)
Debug.LogWarning("[SitPosData] 炮倍面板初始高度为 0,无法记录展开高度,检查 selectPaoBeiPanel 的 RectTransform 配置");
}
/// <summary>
/// 点击炮倍按钮:开关选择面板。
/// ⭐ 只有本地玩家座位可以打开(远程/bot 座位点了没反应,不能替别人换炮倍)。
/// </summary>
private void OnClickSelectPaoBei()
{
if (this != localPlayerSit) return;
if (selectPaoBeiPanel == null) return;
bool opening = !selectPaoBeiPanel.activeSelf;
if (opening) RefreshPaoBeiItems();
SetPaoBeiPanelOpen(opening);
}
/// <summary>
/// 开/关炮倍面板:height 在 0 ↔ 原始高度之间动画 0.5s。
/// ⭐ 不改面板 pivot/anchoredPosition,滑动方向完全由预制体里配好的 RectTransform 决定。
/// </summary>
private void SetPaoBeiPanelOpen(bool open)
{
var rt = selectPaoBeiPanel.GetComponent<RectTransform>();
if (rt == null)
{
selectPaoBeiPanel.SetActive(open);
return;
}
if (_paoBeiPanelAnim != null)
{
StopCoroutine(_paoBeiPanelAnim);
_paoBeiPanelAnim = null;
}
if (open)
{
selectPaoBeiPanel.SetActive(true);
_paoBeiPanelAnim = StartCoroutine(AnimPaoBeiHeight(rt, rt.rect.height, _paoBeiPanelOpenHeight));
}
else
{
_paoBeiPanelAnim = StartCoroutine(AnimPaoBeiHeight(rt, rt.rect.height, 0f,
() => selectPaoBeiPanel.SetActive(false)));
}
}
/// <summary>height 动画(0.5s);动画结束回调在收起时用于隐藏面板</summary>
private IEnumerator AnimPaoBeiHeight(RectTransform rt, float from, float to, Action onComplete = null)
{
const float duration = 0.1f;
float t = 0f;
while (t < duration)
{
t += Time.deltaTime;
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Mathf.Lerp(from, to, Mathf.Clamp01(t / duration)));
yield return null;
}
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, to);
onComplete?.Invoke();
_paoBeiPanelAnim = null;
}
/// <summary>
/// 按配置 paoBeiList 生成炮倍档位 item:
/// child0 的 TextMeshProUGUI 显示炮倍值;背景按值上色(&lt;5000 蓝 / 5000~10000 紫 / &gt;=10000 红),当前档位黄色。
/// 点击 item:换到该档位(走 ChangeGunMultiple 服务端确认协议)并直接关闭面板。
/// </summary>
private void RefreshPaoBeiItems()
{
if (paobeiItemPrefab == null || paobeiItemParent == null) return;
var list = CreateFishManager.Instance?.currGameType?.paoBeiList;
if (list == null || list.Count == 0)
{
Debug.LogWarning("[SitPosData] paoBeiList 为空,炮倍面板没有可选项");
return;
}
for (int i = paobeiItemParent.childCount - 1; i >= 0; i--)
Destroy(paobeiItemParent.GetChild(i).gameObject);
for (int i = 0; i < list.Count; i++)
{
int index = i;
GameObject item = Instantiate(paobeiItemPrefab, paobeiItemParent, false);
item.SetActive(true);
var tmp = item.transform.childCount > 0 ? item.transform.GetChild(0).GetComponent<TextMeshProUGUI>() : null;
if (tmp != null)
tmp.text = list[i].ToString();
var img = item.GetComponent<Image>();
if (img != null)
img.sprite = index == indexConsume ? yellowBG : GetPaoBeiSprite(list[i]);
var btn = item.GetComponent<Button>();
if (btn != null)
btn.onClick.AddListener(() =>
{
if (this != localPlayerSit) return; // 只有自己座位可以换炮倍
SelectPaoBeiByIndex(index);
SetPaoBeiPanelOpen(false); // 选择后直接关闭
});
}
}
/// <summary>炮倍档位背景色:5000 以下蓝,5000~10000 紫,10000 以上红(黄色留给选中态)</summary>
private Sprite GetPaoBeiSprite(int value)
{
if (value >= 10000) return redBG;
if (value >= 5000) return purpleBG;
return blueBG;
}
/// <summary>
/// 选择面板点选档位:本地记录索引并发起 ChangeGunMultiple 请求,
/// consumeGoldNum 和 UI 等服务端 ChangeGunMultipleNotice 确认后再应用(与加减按钮同一协议,防止扣费差值)。
/// </summary>
public void SelectPaoBeiByIndex(int index)
{
var list = CreateFishManager.Instance?.currGameType?.paoBeiList;
if (list == null || index < 0 || index >= list.Count) return;
indexConsume = index;
AudioManager.Instance.PlayerFX(62011);
int newConsume = list[index];
string roomId = GameManager.Instance.gameData?.currRoomData?.roomId;
if (!string.IsNullOrEmpty(roomId))
WebSocketSendList.ChangeGunMultiple(roomId, user, newConsume);
}
2026-06-08 21:23:40 +08:00
// ==================== 锁定系统 ====================
2026-05-04 17:21:52 +08:00
public void SetLockTarget(MonsterContral monster, Transform hitPos)
{
monCotl_Lock = monster;
monCotl_Lock_Position = hitPos;
isLock = monster != null;
lockFX.SetActive(isLock);
2026-06-21 00:28:59 +08:00
// 非三发锁定 → 隐藏额外图标
if (!isLock) HideTripleLockIcons();
}
/// <summary>
/// 设置三发锁定目标(3个目标分别追踪)
/// </summary>
public void SetTripleLockTargets(List<(MonsterContral monster, Transform hitPos)> targets)
{
if (targets == null || targets.Count == 0)
{
HideTripleLockIcons();
return;
}
// 确保 lockFX_Triple 有2个克隆的图标(+主图标=3个)
EnsureTripleLockIcons();
tripleLockTargets.Clear();
tripleLockPositions.Clear();
// 第一个目标用主 lockFX
var primary = targets[0];
monCotl_Lock = primary.monster;
monCotl_Lock_Position = primary.hitPos;
isLock = true;
lockFX.SetActive(true);
tripleLockTargets.Add(primary.monster);
tripleLockPositions.Add(primary.hitPos);
// 第二个和第三个目标用 lockFX_Triple
for (int i = 1; i < targets.Count && i < 3; i++)
{
int idx = i - 1;
if (idx < lockFX_Triple.Count && lockFX_Triple[idx] != null)
{
lockFX_Triple[idx].SetActive(true);
tripleLockTargets.Add(targets[i].monster);
tripleLockPositions.Add(targets[i].hitPos);
}
}
// 隐藏多余的
for (int i = targets.Count - 1; i < lockFX_Triple.Count; i++)
{
if (lockFX_Triple[i] != null)
lockFX_Triple[i].SetActive(false);
}
}
private void EnsureTripleLockIcons()
{
while (lockFX_Triple.Count < 2 && lockFX != null)
{
var clone = GameObject.Instantiate(lockFX, lockFX.transform.parent);
clone.name = lockFX.name + "_Triple" + (lockFX_Triple.Count + 1);
clone.transform.localScale = lockFX.transform.localScale * 0.5f;
lockFX_Triple.Add(clone);
}
}
public void HideTripleLockIcons()
{
foreach (var icon in lockFX_Triple)
{
if (icon != null)
icon.SetActive(false);
}
tripleLockTargets.Clear();
tripleLockPositions.Clear();
2026-05-04 17:21:52 +08:00
}
2026-09-28 02:12:29 +08:00
// ==================== 三发锁位(每个锁位独立维护) ====================
// ⭐ index=0 是主锁位(monCotl_Lock + 主 lockFX),1..2 对应 lockFX_Triple[index-1]
/// <summary>取 index(1..2) 对应的额外锁定图标</summary>
private GameObject GetTripleIcon(int index)
{
int iconIdx = index - 1;
if (lockFX_Triple == null || iconIdx < 0 || iconIdx >= lockFX_Triple.Count) return null;
return lockFX_Triple[iconIdx];
}
/// <summary>替换单个三发锁位(锁位独立:某条鱼死了只补这一个锁位,其他锁位不动)</summary>
public void SetTripleLockSlot(int index, MonsterContral monster, Transform hitPos)
{
if (index < 0) return;
while (tripleLockTargets.Count <= index)
{
tripleLockTargets.Add(null);
tripleLockPositions.Add(null);
}
tripleLockTargets[index] = monster;
tripleLockPositions[index] = hitPos;
if (index == 0)
{
monCotl_Lock = monster;
monCotl_Lock_Position = hitPos;
isLock = monster != null;
if (lockFX != null) lockFX.SetActive(isLock);
}
else
{
var icon = GetTripleIcon(index);
if (icon != null) icon.SetActive(monster != null);
}
}
/// <summary>清空单个三发锁位(该锁位暂时没鱼可补):保留槽位占位,只隐藏该锁位图标</summary>
public void ClearTripleLockSlot(int index)
{
if (index < 0 || index >= tripleLockTargets.Count) return;
tripleLockTargets[index] = null;
if (index < tripleLockPositions.Count) tripleLockPositions[index] = null;
if (index == 0)
{
monCotl_Lock = null;
monCotl_Lock_Position = null;
isLock = false;
if (lockFX != null) lockFX.SetActive(false);
}
else
{
var icon = GetTripleIcon(index);
if (icon != null) icon.SetActive(false);
}
}
/// <summary>
/// 主锁位空缺时,把其他存活的锁位提升为主锁,避免主锁死掉导致整个三发模式中断。
/// 提升后原锁位留空,下一帧会自动补新目标。成功返回 true。
/// </summary>
public bool PromoteTripleLockSlotToMain()
{
for (int i = 1; i < tripleLockTargets.Count; i++)
{
var mon = tripleLockTargets[i];
if (mon == null || !mon.gameObject.activeInHierarchy) continue;
Transform pos = i < tripleLockPositions.Count ? tripleLockPositions[i] : null;
tripleLockTargets[0] = mon;
tripleLockPositions[0] = pos;
tripleLockTargets[i] = null;
tripleLockPositions[i] = null;
var icon = GetTripleIcon(i);
if (icon != null) icon.SetActive(false);
monCotl_Lock = mon;
monCotl_Lock_Position = pos;
isLock = true;
if (lockFX != null) lockFX.SetActive(true);
return true;
}
return false;
}
2026-05-04 17:21:52 +08:00
public void ClearLock()
{
monCotl_Lock = null;
monCotl_Lock_Position = null;
isLock = false;
2026-07-31 17:57:24 +08:00
// ⭐ 清除锁定时重置手动锁定标记
isManualLock = false;
2026-05-04 17:21:52 +08:00
lockFX.SetActive(false);
2026-06-21 00:28:59 +08:00
HideTripleLockIcons();
2026-05-27 19:47:06 +08:00
ChangeState(TurretState.Idle);
2026-05-04 17:21:52 +08:00
}
2026-02-03 11:35:19 +08:00
}