Files
5.24BuYu/Assets/MyGame/Scripts/Turret/SitPosData.cs
T
2026-08-31 16:40:04 +08:00

1341 lines
55 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>炮台控制模式</summary>
public enum TurretControlMode
{
/// <summary>普通模式:动态克隆炮台,显示金币+钻石,按钮打开面板切换</summary>
Normal = 0,
/// <summary>星际迷航模式:座位子对象激活/停用,只显示金币,长按炮台弹出扇形切换</summary>
XJMH = 1
}
public class SitPosData : MonoBehaviour
{
public bool IsBaoJiangingActive;
public Transform obj_BaoJiangZhongPanelPosition;
/// <summary>本地玩家的座位引用(由游戏面板初始化时设置)</summary>
public static SitPosData localPlayerSit;
/// <summary>当前房间房主座位(人机请求由房主发送,房主退出后自动继承)</summary>
public static SitPosData roomOwnerSit;
// -------------------------
// 玩家基础信息
// -------------------------
[Header("玩家信息")]
public string user;
[Tooltip("座位编号 0~3。0,1=下方 2,3=上方")]
public int posID;
[Tooltip("是否为人机")]
public bool isBot = false;
/// <summary>posID>=2 = 上方,需要翻转(0,1=下方;2,3=上方)</summary>
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;
/// <summary>
/// 散射激光模式中(GameMainPanel 每帧刷新:三发+锁定+已锁目标)。
/// ⭐ 激活左右起点球父对象用这个标记,不依赖实际锁定点数量(目标不足2个时散射模式开着也要显示起点球)
/// </summary>
[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;
/// <summary>当前运行中的炮台实例</summary>
public TurretData currentTurret;
/// <summary>当前运行中的战神实例(null=未显示)。炮台/战神显示二选一:有战神时 turretParent 隐藏</summary>
[System.NonSerialized]
public GameObject currentZhanShen;
/// <summary>战神实例上的 TurretData/ZhanShenTurretData(开火时优先取战神的子弹预制体,null=无战神或未挂组件)</summary>
[System.NonSerialized]
public TurretData currentZhanShenData;
// -------------------------
// 战神激光(锁定时显示):两种形态
// - laserPrefab 非空 → 预制体形态:JiGuangControl 工具类驱动(克隆激光预制体,方向/长度每帧刷新)
// - laserPrefab 为空 → LineRenderer 兜底形态:直接新建 GameObject + LineRenderer
// -------------------------
/// <summary>LineRenderer 形态激光列表。三发锁定=3条。起点:0=主激光(座位点+selfOffsetJGStart),1/2=散射左右(JGFX父对象位置)</summary>
private readonly List<LineRenderer> _zhanShenLasers = new List<LineRenderer>();
/// <summary>预制体形态激光实例列表(世界空间,由 JiGuangControl.JiGuangCreate 按目标数量重建)</summary>
private readonly List<Transform> _laserFx = new List<Transform>();
/// <summary>预制体形态激光的基准缩放(重建时取自 laserPrefab.transform.localScale)</summary>
private Vector3 _laserFxBaseScale = Vector3.one;
/// <summary>每条激光的起点缓存(索引与 _laserTargets 对齐,每帧复用避免 GC)</summary>
private readonly List<Vector3> _laserStarts = new List<Vector3>();
// -------------------------
// XJMH 炮台列表(星际迷航专用)
// -------------------------
[Header("XJMH 炮台列表")]
[Tooltip("XJMH 模式专用:在 Inspector 中拖入座位下的所有炮台子对象(带 TurretData 组件)。" +
"数量不多,建议按炮台编号升序排列,便于扇形切换按索引选择。")]
public List<TurretData> xjmhTurretList = new List<TurretData>();
// 状态机入口(GameMainPanel 调用)
public Action<TurretState> OnStateChange;
// -------------------------
// 炮台旋转
// -------------------------
[Header("炮台旋转")]
public Transform tra_CurrTurret;
public float currentAngle;
/// <summary>
/// 炮台在世界坐标(Z=0平面)的位置缓存。
/// 因为炮台是 UGUI 元素,tra_CurrTurret.position 是 Canvas 坐标,不能直接和鱼的世界坐标比较距离。
/// 这里用和子弹起点(GetBulletStartWorldPos)相同的转换方式,把 UI 坐标转成世界坐标。
/// 由 LoadTurret 加载完炮台后计算并缓存,bot AI/锁定检测距离时用此字段。
/// </summary>
[System.NonSerialized]
public Vector3 turretWorldPos;
// -------------------------
// 锁定系统
// -------------------------
[Header("锁定系统")]
public MonsterContral monCotl_Lock;
public Transform monCotl_Lock_Position;
public bool isLock = false;
/// <summary>
/// ⭐ 当前锁定是否为玩家手动锁定。
/// - true:玩家点击锁定的鱼 → 不受路径进度(0.1~0.9)限制,鱼离场前一直锁定
/// - false:自动锁定/三发查找/bot → 受路径进度限制
/// 由 GameMainPanel_FishBase 在 SetLockTarget 前设置
/// </summary>
[System.NonSerialized]
public bool isManualLock = false;
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>();
// -------------------------
// 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());
// ⭐ 散射起点球父对象兜底:预制体没拖引用就自动创建(挂 beij 下,炮台两侧),
// 保证 UpdateJGFXParentsActive 永远有对象可激活,不依赖 Inspector 配置
EnsureJGFXParents();
// XJMH 模式:隐藏钻石显示(只显示金币)
if (turretControlMode == TurretControlMode.XJMH && tMP_DiamondNum != null)
tMP_DiamondNum.gameObject.SetActive(false);
// XJMH 模式:炮台是座位自带子对象,预制体里已摆好,不需要加载。
// 这里只做引用初始化,不做激活/停用。
if (turretControlMode == TurretControlMode.XJMH)
InitXJMHTurret();
}
/// <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;
}
/// <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;
// 缓存旋转节点:优先用 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();
}
// ==================== 战神激光(锁定时显示) ====================
/// <summary>
/// 战神锁定时显示激光,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 时不显示)。
/// </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);
// 三发锁定:每个有效目标一条激光(主目标已在上面,这里只补额外的)
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);
}
}
}
// ⭐ 散射激光使用中就激活左右起点父对象显示起点球:
// isScatterLaserOn=三发+锁定开着(面板每帧刷新,本地座位);锁定点>1=三发目标已凑满(远程/bot座位)
// 两个条件满足其一即激活——散射模式开着但目标不足时也要显示起点球
UpdateJGFXParentsActive(currentZhanShenData != null && (_laserTargets.Count > 1 || isScatterLaserOn));
#if UNITY_EDITOR
DebugJGFXState();
#endif
// 没有锁定点:隐藏所有激光
if (_laserTargets.Count == 0)
{
HideZhanShenLasers();
return;
}
// 按配置走对应形态:laserPrefab 非空=预制体形态,为空=LineRenderer 兜底
if (currentZhanShenData.laserPrefab != null)
UpdateZhanShenLaserFx();
else
UpdateZhanShenLaserLine();
}
/// <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);
}
}
/// <summary>
/// 预制体形态(laserPrefab 非空):JiGuangControl 工具类驱动。
/// 目标数量变化时全量重建(JiGuangCreate),每帧刷新位置/朝向/长度(JiGuangDirection/JiGuangLength,Z 强制 -50)。
/// ⭐ 每条激光独立起点(多起点重载):0=主锁定(座位点+偏移),1/2=散射左右(JGFX父对象位置)。
/// </summary>
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;
JiGuangControl.JiGuangCreate(zsData.laserPrefab, _laserStarts, _laserTargets, _laserFx, null, -50f);
}
// 起点需每帧同步(座位布局/起点球位置变化);隐藏中的重新激活
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)
JiGuangControl.JiGuangDirection(_laserStarts, _laserTargets, _laserFx, -50f);
JiGuangControl.JiGuangLength(_laserStarts, _laserTargets, _laserFx, _laserFxBaseScale, zsData.laserLengthCorrect, -50f);
}
/// <summary>LineRenderer 兜底形态(laserPrefab 为空):按需创建、多余的隐藏。每条激光独立起点</summary>
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);
}
}
/// <summary>激光终点缓存列表(每帧复用,避免 GC)</summary>
private readonly List<Transform> _laserTargets = new List<Transform>();
/// <summary>
/// 创建一条 LineRenderer 形态的战神激光(第 index 条)。
/// 仅 laserPrefab 为空时使用:直接创建一个新 GameObject 并加上 LineRenderer(用 TurretData 上的宽度/颜色)。
/// </summary>
private LineRenderer CreateZhanShenLaser(int index)
{
var zsData = currentZhanShenData;
GameObject laserGo = new GameObject($"ZhanShenLaser_{index}");
LineRenderer line = laserGo.AddComponent<LineRenderer>();
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;
}
/// <summary>销毁所有战神激光对象(两种形态都清,清战神/座位禁用时调用)</summary>
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();
}
// ==================== 战神激光起点(每条激光独立起点) ====================
/// <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
/// <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;
// XJMH 模式:炮台是座位自带子对象,Awake 已初始化引用,不需要加载
if (turretControlMode != TurretControlMode.XJMH)
{
var cannonUsing = GameManager.Instance.user.cannonUsing;
if (cannonUsing != null && int.TryParse(cannonUsing.number, out int num))
LoadTurret(num);
var wingUsing = GameManager.Instance.user.wingUsing;
if (wingUsing != null && int.TryParse(wingUsing.number, out int wingNum))
LoadWing(wingNum);
var godWarUsing = GameManager.Instance.user.godWarUsing;
if (godWarUsing != null && int.TryParse(godWarUsing.number, out int zsNum))
LoadZhanShen(zsNum);
}
}
// 非人机玩家加入时,自动分配为首个房主
if (!isBot)
AssignRoomOwner(this);
}
/// <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);
}
/// <summary>房主退出后,将房主身份转移给第一个活跃的非人机玩家</summary>
/// <remarks>鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。</remarks>
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>
/// <remarks>鱼创建已由服务端 OnFishSpawn 统一下发,此处仅维护 roomOwnerSit 标识(用于 bot 控制/AFK 等场景)。</remarks>
public static void AssignRoomOwner(SitPosData sit)
{
if (roomOwnerSit == null && !sit.isBot)
{
roomOwnerSit = sit;
}
}
// ==================== 炮台加载 ====================
/// <summary>
/// 获取炮台的旋转节点。
/// - 若 TurretData.tra_RotationPivot 已赋值 → 用它(精确指定旋转中心)
/// - 否则回退旧逻辑:炮台父节点(Normal 克隆的中间容器 / XJMH 子对象的父节点)
/// </summary>
private Transform GetTurretRotationPivot(TurretData turret)
{
if (turret == null) return null;
// ⭐ 已分离旋转(用 turretRotate 旋转)时:tra_CurrTurret 指向 turretRotate 本身。
// 瞄准代码用 tra_CurrTurret.transform.parent(=beij,固定不转)作为参考系,
// 上方座位 beij 预设的 180° 翻转与面板 IsFlipped+180 补偿相互抵消,
// currentAngle 恢复 aim-90 的统一约定(炮口/子弹/激光方向全部一致)。
// turretRotate.anchoredPosition 与旧 turretParent 同节点同值,瞄准原点不变;
// beij 不参与旋转,参考系不会抖动。
if (turretRotate != null)
return turretRotate;
// 优先用 TurretData 上配置的旋转节点
if (turret.tra_RotationPivot != null)
return turret.tra_RotationPivot;
// 回退:用父节点(兼容旧逻辑)
Transform parent = turret.transform.parent;
return parent != null ? parent : turret.transform;
}
/// <summary>
/// 根据炮台编号加载炮台。
/// Normal 模式:从 Addressables 加载预制体并克隆到 turretParent。
/// XJMH 模式:炮台是座位子对象,激活对应编号子对象、停用其他。
/// </summary>
public void LoadTurret(int turretNumber)
{
if (turretControlMode == TurretControlMode.XJMH)
{
SwitchTurretChild(turretNumber);
return;
}
LoadTurretNormal(turretNumber);
}
/// <summary>
/// 普通模式:从 Addressables 加载预制体并克隆到 turretParent。
/// 位置/旋转/缩放保持预制体原始值,不动。
/// </summary>
private void LoadTurretNormal(int turretNumber)
{
// ⭐ 炮台/战神显示二选一:切回普通炮台时清除战神并恢复 turretParent 显示
ClearZhanShen();
// ⭐ 换炮台时只清活跃子弹(清屏),保留池缓存(同 prefab 能复用)
// 用 ClearActive 而非 ClearAll:ClearAll 会清池导致永远不复用
BulletPool.Instance.ClearActive();
if (currentTurret != null)
{
Destroy(currentTurret.gameObject);
currentTurret = null;
}
currentTurretNumber = turretNumber;
// 从 TurretResourcesData 拿预制体
GameObject prefab = TurretResourcesData.GetTurretPrefab(turretNumber);
if (prefab == null)
{
Debug.LogWarning($"[SitPosData] 炮台预制体未加载: #{turretNumber}");
return;
}
// 克隆到父节点
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;
// 缓存旋转节点:优先用 TurretData.tra_RotationPivot,否则回退父节点
tra_CurrTurret = GetTurretRotationPivot(currentTurret);
// 绑定状态机入口
OnStateChange = currentTurret.ChangeState;
// ⭐ 缓存炮台在世界坐标(Z=0平面)的位置,bot AI/锁定检测距离时用
UpdateTurretWorldPos();
Debug.Log($"[SitPosData] 炮台已加载(Normal): #{turretNumber}");
}
/// <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;
// 缓存旋转节点:优先用 TurretData.tra_RotationPivot,否则回退父节点
tra_CurrTurret = GetTurretRotationPivot(currentTurret);
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;
}
/// <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;
}
}
/// <summary>根据炮台编号字符串加载。</summary>
public void LoadTurret(string turretNumberStr)
{
if (int.TryParse(turretNumberStr, out int num))
LoadTurret(num);
}
// ==================== 翅膀加载(动态克隆) ====================
/// <summary>
/// 根据炮台编号,从 Addressables 加载预制体并克隆到 turretParent。
/// 位置/旋转/缩放保持预制体原始值,不动。
/// </summary>
public void LoadWing(int turretNumber)
{
// XJMH 模式没有翅膀概念,直接跳过
if (turretControlMode == TurretControlMode.XJMH) return;
// 先清理已有翅膀(保留预设的 Camera,它是用来渲染翅膀到 RawImage 的)
for (int i = wingParent.childCount - 1; i >= 0; i--)
{
var child = wingParent.GetChild(i);
if (child.GetComponent<Camera>() != null)
continue; // 保留 Camera
Destroy(child.gameObject);
}
// 从 TurretResourcesData 拿预制体
GameObject prefab = TurretResourcesData.GetTurretPrefab(turretNumber);
if (prefab == null)
{
Debug.LogWarning($"[SitPosData] 翅膀预制体未加载: #{turretNumber}");
return;
}
// 克隆到 wingParent,保持预制体原始位置/旋转/缩放
GameObject instance = Instantiate(prefab, wingParent, false);
instance.SetActive(true);
Debug.Log($"[SitPosData] 翅膀已加载: #{turretNumber}");
}
/// <summary>根据炮台编号字符串加载。</summary>
public void LoadWing(string turretNumberStr)
{
if (int.TryParse(turretNumberStr, out int num))
LoadWing(num);
}
// ==================== 战神加载(动态克隆,同翅膀) ====================
/// <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);
instance.SetActive(true);
currentZhanShen = instance;
currentZhanShenNumber = zhanShenNumber;
// 绑定状态机入口:战神接管开火/待机动画(PlayFire 约定,见 TurretData.PlayWarGodAnimator)
// 同时缓存 TurretData:开火时优先用战神的子弹预制体(bulletPrefab/bulletPrefab_Two)
var zsData = instance.GetComponent<TurretData>();
currentZhanShenData = zsData;
if (zsData != null)
{
zsData.turretNumber = zhanShenNumber;
zsData.setPosData = this; // 注入座位引用:FireIndex 动态切换(锁定=1激光/非锁定=0子弹)需要查锁定状态
OnStateChange = zsData.ChangeState;
}
// ⭐ 出场/换战神:清空左右起点父对象并重新克隆 JGQiuStartPrefab(当前战神配置的起点球)
RefreshJGQiuStartFX();
// ⭐ 炮台/战神显示二选一:显示战神时隐藏普通炮台
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;
// 恢复普通炮台显示
if (turretParent != null)
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;
// 战神清了激光也没了
DestroyZhanShenLaser();
// 散射起点球也清掉(下次出场/换战神时重建)
ClearJGFXParents();
}
// -------------------------
// 切换状态(GameMainPanel 调用)
// -------------------------
public void ChangeState(TurretState state)
{
OnStateChange?.Invoke(state);
}
// -------------------------
// 炮台旋转(GameMainPanel 调用)
// -------------------------
public void SetRotation(float angle)
{
// ⭐ 激光发射期间锁定方向:忽略旋转请求(激光沿原方向打到爆奖结束)
if (isLaserLocked) return;
currentAngle = angle;
// ⭐ 炮台旋转只作用于 turretRotate,其余不改
// ⭐ 必须用【世界旋转】:瞄准参考系是 beij(tra_CurrTurret 的父节点),currentAngle
// 已是和 beij 翻转抵消后的统一角度(aim-90),直接写世界旋转炮口即对准目标;
// 若改用本地旋转,上方座位 beij 的 180° 翻转会叠加进来,炮口反向 180°
if (turretRotate != null)
turretRotate.rotation = Quaternion.Euler(0, 0, angle);
}
// -------------------------
// 获取发射点(给 GameMainPanel)
// -------------------------
public Vector2 GetBulletStartPos()
{
return currentTurret != null
? currentTurret.GetBulletStartWorldPos()
: Vector2.zero;
}
// ==================== 金币消耗 ====================
public bool ConsumeGold(int mul = 1, long fireConsumeGold = 0)
{
// fireConsumeGold>0: 用服务端返回的该发子弹权威扣费金额
// fireConsumeGold=0: 回退到本地 consumeGoldNum * mul
long useConsume = fireConsumeGold > 0 ? fireConsumeGold : consumeGoldNum * mul;
long temp = goldNum - useConsume;
if (temp < 0) return false;
long oldGold = goldNum;
goldNum = temp;
tMP_GoldNum.text = goldNum.ToString();
// ⭐ 记录金币流水:已扣钱(开火消耗,本地已执行)
GameManager.LogGoldFlowAppliedCost("Fire", $"开火消耗 原有金币={oldGold} 消耗金币=-{useConsume} 剩余金币={goldNum} 座位={posID} 房间={GameManager.Instance.gameData?.currRoomData?.roomId}");
return true;
}
int indexConsume = 0;
/// <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);
indexConsume = idx >= 0 ? idx : 0;
}
public void ChangeTurretAddConsumeNum()
{
if (CreateFishManager.Instance.currGameType.paoBeiList.Count - 1 > indexConsume)
indexConsume++;
else
indexConsume = 0;
AudioManager.Instance.PlayerFX(62011);
// ⭐ 不立即修改 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);
}
public void ChangeTurretMinusConsumeNum()
{
if (indexConsume > 0)
indexConsume--;
else
indexConsume = CreateFishManager.Instance.currGameType.paoBeiList.Count - 1;
AudioManager.Instance.PlayerFX(62010);
// ⭐ 不立即修改 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);
}
// ==================== 锁定系统 ====================
public void SetLockTarget(MonsterContral monster, Transform hitPos)
{
monCotl_Lock = monster;
monCotl_Lock_Position = hitPos;
isLock = monster != null;
lockFX.SetActive(isLock);
// 非三发锁定 → 隐藏额外图标
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();
}
public void ClearLock()
{
monCotl_Lock = null;
monCotl_Lock_Position = null;
isLock = false;
// ⭐ 清除锁定时重置手动锁定标记
isManualLock = false;
lockFX.SetActive(false);
HideTripleLockIcons();
ChangeState(TurretState.Idle);
}
}