diff --git a/Assets/MyGame/Scripts/Turret/SitPosData.cs b/Assets/MyGame/Scripts/Turret/SitPosData.cs
index 63f2c34bb..28e5f0082 100644
--- a/Assets/MyGame/Scripts/Turret/SitPosData.cs
+++ b/Assets/MyGame/Scripts/Turret/SitPosData.cs
@@ -5,6 +5,15 @@ using TMPro;
using UnityEngine;
using UnityEngine.UI;
+/// 炮台控制模式
+public enum TurretControlMode
+{
+ /// 普通模式:动态克隆炮台,显示金币+钻石,按钮打开面板切换
+ Normal = 0,
+ /// 星际迷航模式:座位子对象激活/停用,只显示金币,长按炮台弹出扇形切换
+ XJMH = 1
+}
+
public class SitPosData : MonoBehaviour
{
/// 本地玩家的座位引用(由游戏面板初始化时设置)
@@ -41,6 +50,10 @@ public class SitPosData : MonoBehaviour
[Tooltip("炮台实例化的父节点")]
public Transform turretParent;
+ [Header("炮台控制模式")]
+ [Tooltip("Normal=普通模式(动态克隆炮台,显示金币+钻石,按钮切换); XJMH=星际迷航模式(座位子对象激活/停用,只显示金币,长按扇形切换)")]
+ public TurretControlMode turretControlMode = TurretControlMode.Normal;
+
[Tooltip("炮台锚点位置(相对 turretParent)")]
public Vector2 turretAnchoredPos = Vector2.zero;
@@ -50,6 +63,14 @@ public class SitPosData : MonoBehaviour
/// 当前运行中的炮台实例
public TurretData currentTurret;
+ // -------------------------
+ // XJMH 炮台列表(星际迷航专用)
+ // -------------------------
+ [Header("XJMH 炮台列表")]
+ [Tooltip("XJMH 模式专用:在 Inspector 中拖入座位下的所有炮台子对象(带 TurretData 组件)。" +
+ "数量不多,建议按炮台编号升序排列,便于扇形切换按索引选择。")]
+ public List xjmhTurretList = new List();
+
// 状态机入口(GameMainPanel 调用)
public Action OnStateChange;
@@ -90,8 +111,63 @@ public class SitPosData : MonoBehaviour
private void Awake()
{
- btn_TurretAdd.onClick.AddListener(() => ChangeTurretAddConsumeNum());
- btn_TurretMinus.onClick.AddListener(() => ChangeTurretMinusConsumeNum());
+ 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();
+ }
+
+ ///
+ /// XJMH 模式:炮台是座位自带子对象,预制体里已摆好激活状态,不需要 LoadTurret 加载。
+ /// 此方法只做引用初始化:从 取第一个激活的炮台赋给 currentTurret,
+ /// 并绑定旋转节点、状态机入口、世界坐标缓存。不做任何激活/停用操作。
+ ///
+ private void InitXJMHTurret()
+ {
+ if (xjmhTurretList == null || xjmhTurretList.Count == 0)
+ {
+ Debug.LogWarning($"[SitPosData] XJMH 炮台列表未配置,座位 {posID} 无炮台可用");
+ return;
+ }
+
+ // 优先取列表中第一个激活的炮台;若全部未激活,取 list[0]
+ TurretData found = null;
+ foreach (var td in xjmhTurretList)
+ {
+ if (td == null) continue;
+ if (td.gameObject.activeSelf)
+ {
+ found = td;
+ break;
+ }
+ }
+ if (found == null)
+ {
+ found = xjmhTurretList[0];
+ found.gameObject.SetActive(true);
+ }
+
+ currentTurret = found;
+ currentTurretNumber = found.turretNumber;
+
+ Transform parent = turretParent != null ? turretParent : transform;
+ tra_CurrTurret = currentTurret.transform.parent != parent
+ ? currentTurret.transform.parent
+ : currentTurret.transform;
+
+ OnStateChange = currentTurret.ChangeState;
+ UpdateTurretWorldPos();
+
+ Debug.Log($"[SitPosData] XJMH 炮台引用已初始化: #{currentTurretNumber}");
}
private void OnEnable()
@@ -127,13 +203,17 @@ public class SitPosData : MonoBehaviour
{
localPlayerSit = this;
- var cannonUsing = GameManager.Instance.user.cannonUsing;
- if (cannonUsing != null && int.TryParse(cannonUsing.number, out int num))
- LoadTurret(num);
+ // 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 wingUsing = GameManager.Instance.user.wingUsing;
+ if (wingUsing != null && int.TryParse(wingUsing.number, out int wingNum))
+ LoadWing(wingNum);
+ }
}
// 非人机玩家加入时,自动分配为首个房主
@@ -177,13 +257,28 @@ public class SitPosData : MonoBehaviour
}
}
- // ==================== 炮台加载(动态克隆) ====================
+ // ==================== 炮台加载 ====================
///
- /// 根据炮台编号,从 Addressables 加载预制体并克隆到 turretParent。
- /// 位置/旋转/缩放保持预制体原始值,不动。
+ /// 根据炮台编号加载炮台。
+ /// Normal 模式:从 Addressables 加载预制体并克隆到 turretParent。
+ /// XJMH 模式:炮台是座位子对象,激活对应编号子对象、停用其他。
///
public void LoadTurret(int turretNumber)
+ {
+ if (turretControlMode == TurretControlMode.XJMH)
+ {
+ SwitchTurretChild(turretNumber);
+ return;
+ }
+ LoadTurretNormal(turretNumber);
+ }
+
+ ///
+ /// 普通模式:从 Addressables 加载预制体并克隆到 turretParent。
+ /// 位置/旋转/缩放保持预制体原始值,不动。
+ ///
+ private void LoadTurretNormal(int turretNumber)
{
// ⭐ 换炮台时只清活跃子弹(清屏),保留池缓存(同 prefab 能复用)
// 用 ClearActive 而非 ClearAll:ClearAll 会清池导致永远不复用
@@ -225,17 +320,153 @@ public class SitPosData : MonoBehaviour
currentTurret.turretNumber = turretNumber;
// 缓存旋转节点
- //tra_CurrTurret = currentTurret.transform.GetChild(0);
tra_CurrTurret = currentTurret.transform.parent;
// 绑定状态机入口
OnStateChange = currentTurret.ChangeState;
// ⭐ 缓存炮台在世界坐标(Z=0平面)的位置,bot AI/锁定检测距离时用
- // 和子弹起点(GetBulletStartWorldPos)用相同的 UI→世界 转换方式
UpdateTurretWorldPos();
- Debug.Log($"[SitPosData] 炮台已加载: #{turretNumber}");
+ Debug.Log($"[SitPosData] 炮台已加载(Normal): #{turretNumber}");
+ }
+
+ ///
+ /// XJMH 模式:炮台都是座位的子对象,激活对应编号的炮台、停用其他。
+ /// 不需要 Instantiate/Destroy,只做激活/停用,无需额外处理。
+ /// 优先从 查找,列表为空时回退遍历子对象。
+ ///
+ 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();
+ 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;
+ // 炮台若挂在中间旋转容器下,用容器作为旋转节点;否则用炮台自身
+ tra_CurrTurret = currentTurret.transform.parent != parent
+ ? currentTurret.transform.parent
+ : currentTurret.transform;
+
+ OnStateChange = currentTurret.ChangeState;
+ UpdateTurretWorldPos();
+
+ Debug.Log($"[SitPosData] 炮台已切换(XJMH): #{turretNumber}");
+ }
+
+ ///
+ /// XJMH 模式:按列表索引直接切换炮台。
+ /// 用于扇形切换 UI 中按位置选择炮台,避免再做编号匹配。
+ ///
+ /// xjmhTurretList 中的索引
+ /// 切换成功返回 true;索引越界或列表为空返回 false
+ 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;
+ }
+
+ ///
+ /// XJMH 模式:根据当前炮台在 xjmhTurretList 中的索引返回下一个炮台编号。
+ /// 用于"切换到下一把"的快捷操作。循环切换,列表为空返回 -1。
+ ///
+ 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;
+ }
+
+ ///
+ /// 获取当前可用的炮台编号列表。
+ /// Normal 模式:从资源表按类型查询。
+ /// XJMH 模式:优先从 读取,列表为空时回退扫描座位子对象。
+ ///
+ public List GetAvailableTurretNumbers()
+ {
+ var result = new List();
+ 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();
+ if (td != null)
+ result.Add(td.turretNumber);
+ }
+ }
+ }
+ else
+ {
+ var list = TurretResourcesData.GetTurretDataByType(1);
+ if (list != null)
+ result = list.ConvertAll(d => d.turretNumber);
+ }
+ return result;
}
///
@@ -286,6 +517,9 @@ public class SitPosData : MonoBehaviour
///
public void LoadWing(int turretNumber)
{
+ // XJMH 模式没有翅膀概念,直接跳过
+ if (turretControlMode == TurretControlMode.XJMH) return;
+
// 先清理已有翅膀(保留预设的 Camera,它是用来渲染翅膀到 RawImage 的)
for (int i = wingParent.childCount - 1; i >= 0; i--)
{
diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs
index ee7bf08f3..ada2dba33 100644
--- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs
+++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs
@@ -196,6 +196,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
float changeTurretTime = 0f;
bool isChangingTurret = false;
+ // XJMH 长按炮台扇形切换
+ [Header("XJMH 炮台长按切换")]
+ [Tooltip("长按炮台触发扇形切换的阈值(秒)")]
+ public float turretLongPressThreshold = 0.5f;
+ /// 是否正在等待长按炮台判定(按下炮台但尚未超过阈值)
+ protected bool _isPendingTurretLongPress = false;
+ /// 炮台扇形切换UI是否已显示
+ protected bool _isTurretSwitcherActive = false;
+
int bulletCount = 0;
public GameObject boardCast_Boss;
@@ -390,6 +399,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
{
base.OnHide();
+ // ⭐ 清理 XJMH 扇形切换UI(如果还在显示)
+ CleanupTurretSwitcher();
+
UnLoadResources();
// 场景可能未加载或已卸载,做保护避免重复卸载报错
var scene = SceneManager.GetSceneByName(sceneName);
@@ -894,8 +906,14 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
if (mySitAnim != null && mySitAnim.gameObject.activeSelf)
mySitAnim.gameObject.SetActive(false);
+ // XJMH 模式:检测是否按在当前炮台上(长按触发扇形切换)
+ _isPendingTurretLongPress = !isChangingTurret
+ && currSitData.turretControlMode == TurretControlMode.XJMH
+ && IsTouchOnCurrentTurret(input.position);
+
if (!isChangingTurret)
{
+ // 按下即进入 Attack:短按→射击,长按超过阈值后切回 Idle 弹扇形
currSitData.ChangeState(TurretState.Attack);
SyncTurretState(TurretState.Attack);
}
@@ -904,12 +922,40 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
// 长按
if (input.isPressed)
{
+ // XJMH 长按炮台判定:超过阈值后弹出扇形切换UI
+ if (_isPendingTurretLongPress && !_isTurretSwitcherActive)
+ {
+ changeTurretTime += Time.deltaTime;
+ if (changeTurretTime >= turretLongPressThreshold)
+ {
+ _isPendingTurretLongPress = false;
+ _isTurretSwitcherActive = true;
+ // 扇形切换弹出时切回 Idle,停止射击动画
+ currSitData.ChangeState(TurretState.Idle);
+ SyncTurretState(TurretState.Idle);
+ OnTurretSwitcherShow(input.position);
+ }
+ }
+
+ if (_isTurretSwitcherActive)
+ {
+ OnTurretSwitcherUpdate(input.position);
+ return;
+ }
+
if (isChangingTurret)
{
HandleChangeTurretHold();
return;
}
+ // XJMH 待判定长按:不旋转(手指在炮台上旋转无意义),但仍允许开火(短按=射击)
+ if (_isPendingTurretLongPress)
+ {
+ TryFire();
+ return;
+ }
+
// 锁定模式:不手动旋转/开火,交给 HandleAutoRotate + HandleAutoFire
if (toggle_Lock.isOn) return;
@@ -923,6 +969,23 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
// 松开
if (input.wasReleasedThisFrame)
{
+ // XJMH 扇形切换释放:根据滑动位置选择炮台
+ if (_isTurretSwitcherActive)
+ {
+ _isTurretSwitcherActive = false;
+ _isPendingTurretLongPress = false;
+ changeTurretTime = 0;
+ OnTurretSwitcherHide(input.position);
+ return;
+ }
+
+ // XJMH 短按炮台释放(未触发长按)→ 正常回到 Idle
+ if (_isPendingTurretLongPress)
+ {
+ _isPendingTurretLongPress = false;
+ changeTurretTime = 0;
+ }
+
if (isChangingTurret)
{
isChangingTurret = false;
@@ -976,6 +1039,40 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
isChangingTurret = false;
}
+ // ==================== XJMH 炮台扇形切换 ====================
+
+ ///
+ /// 检测触摸点是否在当前炮台的 RectTransform 范围内(XJMH 长按切换用)。
+ ///
+ protected bool IsTouchOnCurrentTurret(Vector2 screenPos)
+ {
+ if (currSitData == null || currSitData.currentTurret == null) return false;
+ RectTransform rt = currSitData.currentTurret.GetComponent();
+ if (rt == null) return false;
+ Canvas canvas = UIManager.Instance != null && UIManager.Instance.uiRoot != null
+ ? UIManager.Instance.uiRoot.GetComponent