Files
5.24BuYu/Assets/MyGame/Scripts/Turret/SitPosData.cs
T
2026-08-29 17:28:00 +08:00

1031 lines
39 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;
// -------------------------
// 炮台管理(动态克隆)
// -------------------------
[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;
// -------------------------
// 战神激光(锁定时显示)
// -------------------------
/// <summary>战神激光对象列表(世界空间 LineRenderer,起点=子弹创建位置,终点=锁定鱼的位置)。三发锁定=3条</summary>
private readonly List<LineRenderer> _zhanShenLasers = new List<LineRenderer>();
// -------------------------
// 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;
private void Awake()
{
if (btn_TurretAdd != null)
btn_TurretAdd.onClick.AddListener(() => ChangeTurretAddConsumeNum());
if (btn_TurretMinus != null)
btn_TurretMinus.onClick.AddListener(() => ChangeTurretMinusConsumeNum());
// XJMH 模式:隐藏钻石显示(只显示金币)
if (turretControlMode == TurretControlMode.XJMH && tMP_DiamondNum != null)
tMP_DiamondNum.gameObject.SetActive(false);
// XJMH 模式:炮台是座位自带子对象,预制体里已摆好,不需要加载。
// 这里只做引用初始化,不做激活/停用。
if (turretControlMode == TurretControlMode.XJMH)
InitXJMHTurret();
}
/// <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();
}
private void Update()
{
UpdateZhanShenLaser();
}
// ==================== 战神激光(锁定时显示) ====================
/// <summary>
/// 战神锁定时显示激光(LineRenderer):起点=创建子弹的位置(GetBulletStartPos),终点=锁定鱼的位置。
/// ⭐ 一个锁定点一条线:单发锁定=1条,三发锁定=3条(主目标+2个三发目标)。
/// ⭐ 只是视觉表现:打中鱼仍按正常攻击频率由子弹完成(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);
}
}
}
// 没有锁定点:隐藏所有激光
if (_laserTargets.Count == 0)
{
for (int i = 0; i < _zhanShenLasers.Count; i++)
{
if (_zhanShenLasers[i] != null && _zhanShenLasers[i].gameObject.activeSelf)
_zhanShenLasers[i].gameObject.SetActive(false);
}
return;
}
// 起点:创建子弹的位置(Z=-50,战神激光显示层,和 FireBullet 的 startPos 同源仅 Z 不同)
Vector3 start = GetBulletStartPos();
start.z = -50f;
// 需要几条就创建几条(三发=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);
// 终点:该条锁定鱼的位置(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>
/// 创建一条战神激光(第 index 条):
/// - laserPrefab 非空 → 克隆预制体(LineRenderer 配置以预制体为准);
/// - laserPrefab 为空 → 直接创建一个新 GameObject 并加上 LineRenderer(用 TurretData 上的宽度/颜色)。
/// </summary>
private LineRenderer CreateZhanShenLaser(int index)
{
var zsData = currentZhanShenData;
GameObject laserGo;
LineRenderer line;
if (zsData.laserPrefab != null)
{
laserGo = Instantiate(zsData.laserPrefab);
laserGo.name = $"ZhanShenLaser_{index}";
line = laserGo.GetComponent<LineRenderer>();
if (line == null)
line = laserGo.AddComponent<LineRenderer>();
}
else
{
// ⭐ prefab 为空:直接新建对象 + LineRenderer
laserGo = new GameObject($"ZhanShenLaser_{index}");
line = laserGo.AddComponent<LineRenderer>();
}
// 公共配置:两个端点
line.positionCount = Mathf.Max(line.positionCount, 2);
laserGo.transform.SetParent(null); // 世界空间,不随座位/炮台旋转
if (zsData.laserPrefab == 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();
}
/// <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
// 不随旋转而变,否则瞄准参考系跟着转就会抖动。旋转由 turretRotate 负责。
if (turretRotate != null)
return turretRotate.parent != null ? turretRotate.parent : 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;
}
// ⭐ 炮台/战神显示二选一:显示战神时隐藏普通炮台
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();
}
// -------------------------
// 切换状态(GameMainPanel 调用)
// -------------------------
public void ChangeState(TurretState state)
{
OnStateChange?.Invoke(state);
}
// -------------------------
// 炮台旋转(GameMainPanel 调用)
// -------------------------
public void SetRotation(float angle)
{
// ⭐ 激光发射期间锁定方向:忽略旋转请求(激光沿原方向打到爆奖结束)
if (isLaserLocked) return;
currentAngle = angle;
// ⭐ 炮台旋转只作用于 turretRotate(所有需要旋转的父对象),其余不改
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);
}
}