1
This commit is contained in:
@@ -128,6 +128,7 @@ public class EffectAutoReturn : MonoBehaviour
|
||||
private ParticleSystem[] particles;
|
||||
private float checkTimer;
|
||||
private const float MinPlayTime = 0.3f; // 至少播 0.3s 再检测,避免第一帧 IsAlive=false 误判
|
||||
private const float MaxLifetime = 6f; // ⭐ 兜底:超过 6 秒强制回池,防止 looping 残留导致特效永生
|
||||
|
||||
/// <summary>重新播放所有粒子</summary>
|
||||
public void Play()
|
||||
@@ -140,6 +141,11 @@ public class EffectAutoReturn : MonoBehaviour
|
||||
{
|
||||
if (p != null)
|
||||
{
|
||||
// ⭐ 强制关闭 looping:有些预制体子级 ParticleSystem 设了 looping=1,
|
||||
// 会导致 IsAlive(true) 永远返回 true,特效一直累加直至内存爆满死机
|
||||
var main = p.main;
|
||||
main.loop = false;
|
||||
|
||||
p.Clear(true);
|
||||
p.Play(true);
|
||||
}
|
||||
@@ -151,6 +157,13 @@ public class EffectAutoReturn : MonoBehaviour
|
||||
checkTimer += Time.deltaTime;
|
||||
if (checkTimer < MinPlayTime) return;
|
||||
|
||||
// ⭐ 6 秒兜底:无论是否还存活,超过 6 秒强制回池
|
||||
if (checkTimer >= MaxLifetime)
|
||||
{
|
||||
EffectPool.Instance.Return(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
// 所有粒子都播完 → 回池
|
||||
if (particles == null || particles.Length == 0)
|
||||
{
|
||||
|
||||
@@ -1059,6 +1059,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
public void CreateFish(SyncGameData syncGameData)
|
||||
{
|
||||
if (isFrozen) return; // 冰冻中不创建鱼
|
||||
// ⭐ 场景卸载/CloseSpawnSystem 后 spawnDic 会变 null,此时收到的广播直接忽略
|
||||
if (spawnDic == null) return;
|
||||
var rotateData = FrameDataParser.Parse<FrameData_CreateFish>(syncGameData.frameData);
|
||||
if (rotateData != null)
|
||||
SpawnFishMainThread(rotateData, syncGameData.time);
|
||||
@@ -1068,6 +1070,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
/// </summary>
|
||||
private void SpawnFishMainThread(FrameData_CreateFish createFishData, long time)
|
||||
{
|
||||
// ⭐ spawnDic 可能在场景卸载时被置 null,这里再保护一次
|
||||
if (spawnDic == null) return;
|
||||
if (!spawnDic.TryGetValue(createFishData.fishNumber, out var cfg))
|
||||
{
|
||||
Debug.LogWarning($"[CreateFishManager] 找不到 spawnDic 配置: {createFishData.fishNumber}");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Assets.MyGame.Scripts.Net.API;
|
||||
using Assets.MyGame.Scripts.Net.APIRequest;
|
||||
using Best.HTTP.SecureProtocol.Org.BouncyCastle.Ocsp;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@@ -166,6 +165,7 @@ namespace Assets.MyGame.Scripts.Net
|
||||
dic_API[APIType.Hall_GetChessRoomPartitionCount] = "/api/Hall/GetChessRoomPartitionCount";
|
||||
dic_API[APIType.Hall_SendBroadcast] = "/api/Hall/SendBroadcast";
|
||||
dic_API[APIType.Room_Assign] = "/api/Room/Assign";
|
||||
dic_API[APIType.Hall_UseVipExp] = "/api/Hall/UseVipExp";
|
||||
|
||||
// ---------------- Customer ----------------
|
||||
dic_API[APIType.Customer_Start] = "/customerapi/Customer/start";
|
||||
|
||||
@@ -320,6 +320,15 @@ namespace Assets.MyGame.Scripts.Net
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用VIP经验包(GET /api/Hall/UseVipExp)
|
||||
/// 返回:string
|
||||
/// </summary>
|
||||
public static void UseVipExp(Action<string> callback = null)
|
||||
{
|
||||
APIManager.Response<string>(APIType.Hall_UseVipExp, callback);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -22,6 +22,7 @@ namespace Assets.MyGame.Scripts.Net.APIResponses.Enum
|
||||
大喇叭=9,
|
||||
技能=10,
|
||||
召唤卡=11,
|
||||
VIP经验=12,
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -342,6 +342,9 @@ namespace Assets.MyGame.Scripts.Net
|
||||
/// <summary>获取房间专属 socket 地址(GET /api/Room/Assign?roomId=)</summary>
|
||||
Room_Assign,
|
||||
|
||||
/// <summary>使用VIP经验包(GET /api/Hall/UseVipExp)</summary>
|
||||
Hall_UseVipExp,
|
||||
|
||||
|
||||
// ---------------- Customer ----------------
|
||||
/// <summary>发起会话(POST)</summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
@@ -119,4 +119,7 @@ public class RoomPlayerInfo
|
||||
/// 玩家因子
|
||||
/// </summary>
|
||||
public object playerFactor;
|
||||
|
||||
/// <summary>是否为机器人(后端下发)</summary>
|
||||
public bool isBot;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Assets.MyGame.Scripts.Net.WebResponses;
|
||||
using Assets.MyGame.Scripts.Net.WebResponses;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace Assets.MyGame.Scripts.Net.WebResponses
|
||||
@@ -93,5 +93,8 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
|
||||
/// 玩家状态(0 正常,其他状态可扩展)
|
||||
/// </summary>
|
||||
public int state;
|
||||
|
||||
/// <summary>是否为机器人(后端下发)</summary>
|
||||
public bool isBot;
|
||||
}
|
||||
}
|
||||
@@ -452,8 +452,10 @@ namespace Assets.MyGame.Scripts.Net
|
||||
}
|
||||
else
|
||||
{
|
||||
// 房主是其他真人:本地停止出鱼,避免重复发包
|
||||
CreateFishManager.Instance.fangzhu = false;
|
||||
// ⭐ 只在 userId 非空且确实是真人玩家时才停止本地出鱼
|
||||
// userId 为空(异常情况)时保持原状,避免误关出鱼导致 bot 退出后不出鱼
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
CreateFishManager.Instance.fangzhu = false;
|
||||
}
|
||||
break;
|
||||
case "Exception":
|
||||
|
||||
@@ -60,6 +60,15 @@ public class SitPosData : MonoBehaviour
|
||||
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;
|
||||
|
||||
// -------------------------
|
||||
// 锁定系统
|
||||
// -------------------------
|
||||
@@ -232,9 +241,47 @@ public class SitPosData : MonoBehaviour
|
||||
// 绑定状态机入口
|
||||
OnStateChange = currentTurret.ChangeState;
|
||||
|
||||
// ⭐ 缓存炮台在世界坐标(Z=0平面)的位置,bot AI/锁定检测距离时用
|
||||
// 和子弹起点(GetBulletStartWorldPos)用相同的 UI→世界 转换方式
|
||||
UpdateTurretWorldPos();
|
||||
|
||||
Debug.Log($"[SitPosData] 炮台已加载: #{turretNumber}");
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
|
||||
@@ -59,16 +59,6 @@
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11007,
|
||||
"turretName": "萌娃驾到",
|
||||
"turretType": 1,
|
||||
"prefabTurretResourcesPath": "paotai11007",
|
||||
"animationType": 1,
|
||||
"prefabType": 0,
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11008,
|
||||
"turretName": "创世之心",
|
||||
@@ -229,16 +219,6 @@
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11024,
|
||||
"turretName": "醒狮纳吉1",
|
||||
"turretType": 1,
|
||||
"prefabTurretResourcesPath": "paotai11024",
|
||||
"animationType": 1,
|
||||
"prefabType": 0,
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11025,
|
||||
"turretName": "鸿兔先锋",
|
||||
@@ -409,16 +389,6 @@
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11042,
|
||||
"turretName": "战魂之刃",
|
||||
"turretType": 1,
|
||||
"prefabTurretResourcesPath": "paotai11042",
|
||||
"animationType": 1,
|
||||
"prefabType": 0,
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11043,
|
||||
"turretName": "战神之怒",
|
||||
@@ -439,16 +409,6 @@
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 11045,
|
||||
"turretName": "裂魂之刃",
|
||||
"turretType": 1,
|
||||
"prefabTurretResourcesPath": "paotai11045",
|
||||
"animationType": 1,
|
||||
"prefabType": 0,
|
||||
"attackAudioID": 0,
|
||||
"bulletID": []
|
||||
},
|
||||
{
|
||||
"turretNumber": 12001,
|
||||
"turretName": "自由之翼",
|
||||
|
||||
@@ -111,7 +111,7 @@ PrefabInstance:
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
propertyPath: idleAnim
|
||||
value: idle
|
||||
value: idel
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
|
||||
@@ -116,7 +116,7 @@ PrefabInstance:
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
propertyPath: attackAnim
|
||||
value: attack_1
|
||||
value: attack
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
|
||||
@@ -116,7 +116,7 @@ PrefabInstance:
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
propertyPath: attackAnim
|
||||
value: attack_1
|
||||
value: attcak_1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
|
||||
@@ -116,7 +116,7 @@ PrefabInstance:
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
propertyPath: attackAnim
|
||||
value: attack_1
|
||||
value: attcak_1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
|
||||
@@ -116,7 +116,7 @@ PrefabInstance:
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
propertyPath: attackAnim
|
||||
value: attack_1
|
||||
value: attcak_1
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 5431233218450413364, guid: c2f0865344a970d459ea7df4bebb12fb,
|
||||
type: 3}
|
||||
|
||||
@@ -217,10 +217,14 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
foreach (var item in GameManager.Instance.gameData.currRoomData.players)
|
||||
{
|
||||
JoinGame(item.position, item.gold, item.diamond, item.cannonMultiple, 0, item.userId, item.cannonId, item.wingsId);
|
||||
// 后端 players 列表里 userId 以 "bot_" 开头的是机器人
|
||||
// ⭐ 优先用后端 isBot 字段,兼容 userId.StartsWith("bot_")
|
||||
var sit = list_Sits.Find(s => s.posID == item.position);
|
||||
if (sit != null && !string.IsNullOrEmpty(item.userId) && item.userId.StartsWith("bot_"))
|
||||
sit.isBot = true;
|
||||
if (sit != null)
|
||||
{
|
||||
bool isBot = item.isBot ||
|
||||
(!string.IsNullOrEmpty(item.userId) && item.userId.StartsWith("bot_"));
|
||||
if (isBot) sit.isBot = true;
|
||||
}
|
||||
}
|
||||
GameManager.Instance.gameData.roomJoinNotice = v.arguments[0];
|
||||
isfirstShow = false;
|
||||
@@ -249,12 +253,25 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
}
|
||||
else
|
||||
{
|
||||
// 玩家加入 → 如果座位被bot占用则替换
|
||||
var targetSit = list_Sits.Find(s => s.posID == roomJoinNotice.position);
|
||||
if (targetSit != null && targetSit.isBot)
|
||||
// ⭐ 优先用后端 isBot 字段判断,兼容 userId.StartsWith("bot_")
|
||||
bool isBotJoin = roomJoinNotice.isBot ||
|
||||
(!string.IsNullOrEmpty(roomJoinNotice.userId) && roomJoinNotice.userId.StartsWith("bot_"));
|
||||
|
||||
if (isBotJoin)
|
||||
{
|
||||
targetSit.isBot = false;
|
||||
targetSit.ShowWaiting();
|
||||
// bot 加入 → 标记 isBot=true (JoinGame 里会自动 SetActive(true) 座位)
|
||||
if (targetSit != null)
|
||||
targetSit.isBot = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 真人玩家加入 → 如果座位被bot占用则替换
|
||||
if (targetSit != null && targetSit.isBot)
|
||||
{
|
||||
targetSit.isBot = false;
|
||||
targetSit.ShowWaiting();
|
||||
}
|
||||
}
|
||||
JoinGame(roomJoinNotice.position, roomJoinNotice.gold, roomJoinNotice.diamond, roomJoinNotice.cannonMultiple, 0, roomJoinNotice.userId, roomJoinNotice.cannonId, roomJoinNotice.wingsId);
|
||||
|
||||
@@ -1171,6 +1188,81 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region 锁定位置解析(统一工具)
|
||||
|
||||
/// <summary>
|
||||
/// 统一锁定位置解析:colliderList 第一个活跃碰撞器 → lockPoint → monster.transform
|
||||
/// 所有设置锁定位置的地方都应该调用此方法,避免不同地方用不同优先级导致锁定图标位置和子弹终点不一致。
|
||||
/// 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null 判断。
|
||||
/// </summary>
|
||||
private Transform ResolveLockTransform(MonsterContral monster, out int colliderIndex)
|
||||
{
|
||||
colliderIndex = -1;
|
||||
if (monster == null) return null;
|
||||
|
||||
// 1. 优先用 colliderList 中第一个活跃的碰撞器
|
||||
if (monster.colliderList != null && monster.colliderList.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < monster.colliderList.Count; i++)
|
||||
{
|
||||
var col = monster.colliderList[i];
|
||||
if (col != null && col.gameObject.activeInHierarchy)
|
||||
{
|
||||
colliderIndex = i;
|
||||
return col;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 其次用 lockPoint(锁定标志锚点,通常是屏幕中间/鱼身中心)
|
||||
if (monster.lockPoint != null)
|
||||
return monster.lockPoint;
|
||||
|
||||
// 3. 兜底用 monster.transform
|
||||
return monster.transform;
|
||||
}
|
||||
|
||||
/// <summary>简化重载,不关心 colliderIndex 时用</summary>
|
||||
private Transform ResolveLockTransform(MonsterContral monster)
|
||||
{
|
||||
return ResolveLockTransform(monster, out _);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 路径进度过滤(锁定范围控制)
|
||||
|
||||
/// <summary>bot/玩家锁定时,鱼路径进度下限:小于此值表示鱼刚出生,不锁定</summary>
|
||||
private const float BotLockPathMin = 0.2f;
|
||||
/// <summary>bot/玩家锁定时,鱼路径进度上限:大于此值表示鱼即将离场,不锁定</summary>
|
||||
private const float BotLockPathMax = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// 计算鱼在路径上的进度 [0, 1]。
|
||||
/// 进度 = moveTime / monsterData.moveLine.moveTime
|
||||
/// 0 = 刚出生,1 = 即将离场
|
||||
/// </summary>
|
||||
private float GetFishPathProgress(MonsterContral m)
|
||||
{
|
||||
if (m == null || m.monsterData == null || m.monsterData.moveLine == null) return 1f;
|
||||
float total = m.monsterData.moveLine.moveTime;
|
||||
if (total <= 0f) return 1f;
|
||||
return Mathf.Clamp01(m.moveTime / total);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断鱼的路径进度是否在可锁定范围内 [BotLockPathMin, BotLockPathMax]
|
||||
/// 刚出生(< 0.2)或即将离场(> 0.8)的鱼不能锁定,避免锁定后立即丢失目标
|
||||
/// </summary>
|
||||
private bool IsFishPathLockable(MonsterContral m)
|
||||
{
|
||||
if (m == null) return false;
|
||||
float progress = GetFishPathProgress(m);
|
||||
return progress >= BotLockPathMin && progress <= BotLockPathMax;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 多目标查找(TripleLock)
|
||||
@@ -1229,15 +1321,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
MonsterContral m = kv.Value;
|
||||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||||
if (selected.Contains(m.guid)) continue; // 跳过已选中的
|
||||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
|
||||
if (!IsFishPathLockable(m)) continue;
|
||||
|
||||
Transform pos = m.transform;
|
||||
if (m.colliderList != null && m.colliderList.Count > 0)
|
||||
{
|
||||
foreach (var col in m.colliderList)
|
||||
{
|
||||
if (col.gameObject.activeInHierarchy) { pos = col; break; }
|
||||
}
|
||||
}
|
||||
// ⭐ 统一用 ResolveLockTransform:colliderList → lockPoint → transform
|
||||
Transform pos = ResolveLockTransform(m);
|
||||
|
||||
result.Add((m, pos));
|
||||
selected.Add(m.guid);
|
||||
@@ -1356,17 +1444,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
|
||||
// 处理选中的最佳命中
|
||||
MonsterContral monster = bestHit.collider.GetComponentInParent<MonsterContral>();
|
||||
// ⭐ 两个判断:lockPoint(屏幕中间/锁定锚点) 优先 → colliderList 中命中的碰撞器 → 兜底命中碰撞器
|
||||
// 不用 fish.gameObject.position;与 bot 锁定逻辑保持一致
|
||||
Transform lockTr;
|
||||
if (monster.lockPoint != null)
|
||||
lockTr = monster.lockPoint;
|
||||
else if (monster.colliderList != null && monster.colliderList.Count > 0)
|
||||
lockTr = bestHit.collider.transform; // 命中的碰撞器(在 colliderList 内)
|
||||
else
|
||||
lockTr = bestHit.collider.transform; // 兜底
|
||||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||||
// 返回的 colliderIndex 用于网络同步,其他客户端按此索引取同一碰撞器
|
||||
int colliderIndex;
|
||||
Transform lockTr = ResolveLockTransform(monster, out colliderIndex);
|
||||
currSitData.SetLockTarget(monster, lockTr);
|
||||
int colliderIndex = (monster.colliderList != null) ? monster.colliderList.IndexOf(bestHit.collider.transform) : -1;
|
||||
SyncLock(new List<string> { monster?.guid }, colliderIndex);
|
||||
}
|
||||
private void UpdateLockFX()
|
||||
@@ -1397,35 +1479,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
|
||||
if (!hitPos.gameObject.activeInHierarchy)
|
||||
{
|
||||
if (monster.colliderList != null && monster.colliderList.Count > 0)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (Transform t in monster.colliderList)
|
||||
{
|
||||
if (t.gameObject.activeInHierarchy)
|
||||
{
|
||||
pos = t.position;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
if (icon.activeSelf) icon.SetActive(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null
|
||||
var lp = (monster.lockPoint != null) ? monster.lockPoint : monster.transform;
|
||||
pos = lp.position;
|
||||
}
|
||||
if (!hitPos.gameObject.activeInHierarchy)
|
||||
// ⭐ 原始 hitPos 失效 → 用 ResolveLockTransform 重新找一个有效位置
|
||||
// 统一逻辑:colliderList 第一个活跃 → lockPoint → transform
|
||||
Transform fallback = ResolveLockTransform(monster);
|
||||
if (fallback == null || !fallback.gameObject.activeInHierarchy)
|
||||
{
|
||||
if (icon.activeSelf) icon.SetActive(false);
|
||||
return;
|
||||
}
|
||||
pos = fallback.position;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1472,30 +1534,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
MonsterContral target = FindAutoLockTarget();
|
||||
if (target != null)
|
||||
{
|
||||
// ⭐ 锁定位置与 HandleLock 一致:lockPoint 优先 → colliderList 第一个活跃碰撞器 → target.transform
|
||||
Transform lockTr;
|
||||
int colliderIndex = -1;
|
||||
if (target.lockPoint != null)
|
||||
{
|
||||
lockTr = target.lockPoint;
|
||||
}
|
||||
else if (target.colliderList != null && target.colliderList.Count > 0)
|
||||
{
|
||||
lockTr = target.transform;
|
||||
for (int i = 0; i < target.colliderList.Count; i++)
|
||||
{
|
||||
if (target.colliderList[i].gameObject.activeInHierarchy)
|
||||
{
|
||||
lockTr = target.colliderList[i];
|
||||
colliderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lockTr = target.transform;
|
||||
}
|
||||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||||
int colliderIndex;
|
||||
Transform lockTr = ResolveLockTransform(target, out colliderIndex);
|
||||
currSitData.SetLockTarget(target, lockTr);
|
||||
SyncLock(new List<string> { target?.guid }, colliderIndex);
|
||||
}
|
||||
@@ -1517,12 +1558,15 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
MonsterContral best = null;
|
||||
float bestSqr = float.MaxValue;
|
||||
|
||||
Vector3 origin = currSitData.tra_CurrTurret.position;
|
||||
// ⭐ 用世界坐标(Z=0平面),和 bot AI 一致,否则距离计算会异常(出现 3.4E+38 之类)
|
||||
Vector3 origin = currSitData.turretWorldPos;
|
||||
|
||||
foreach (var kv in CreateFishManager.Instance.fishDatas_hashCodekey)
|
||||
{
|
||||
MonsterContral m = kv.Value;
|
||||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为自动锁定目标
|
||||
if (!IsFishPathLockable(m)) continue;
|
||||
|
||||
float sqr = (m.transform.position - origin).sqrMagnitude;
|
||||
if (sqr < bestSqr)
|
||||
@@ -1840,15 +1884,11 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
}
|
||||
|
||||
MonsterContral monster = CreateFishManager.Instance.fishDatas_hashCodekey[data.targetGuid[0]];
|
||||
Transform hitPos;
|
||||
|
||||
// ⭐ 与本地 HandleLock 保持一致:lockPoint(屏幕中间) 优先 → colliderList[colliderIndex] → fish.transform
|
||||
if (monster.lockPoint != null)
|
||||
hitPos = monster.lockPoint;
|
||||
else if (monster.colliderList != null && monster.colliderList.Count > 0 && data.colliderIndex != -1)
|
||||
hitPos = monster.colliderList[data.colliderIndex];
|
||||
else
|
||||
hitPos = monster.transform;
|
||||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||||
// 注:网络同步过来的 colliderIndex 在统一逻辑下不再使用,因为客户端之间
|
||||
// colliderList 顺序可能不一致,统一用"第一个活跃"更稳定
|
||||
Transform hitPos = ResolveLockTransform(monster);
|
||||
|
||||
list_Sits[sync.sitPos].SetLockTarget(monster, hitPos);
|
||||
}
|
||||
@@ -2004,6 +2044,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
{
|
||||
if (sit.posID != sitNum) continue;
|
||||
|
||||
// ⭐ 确保座位 GameObject 本身是激活的(可能被 HandleBotExit 隐藏或初始为 inactive)
|
||||
if (!sit.gameObject.activeSelf)
|
||||
sit.gameObject.SetActive(true);
|
||||
|
||||
sit.user = user ?? GameManager.Instance.user.id;
|
||||
sit.goldNum = goldNum;
|
||||
sit.consumeGoldNum = consumeGold;
|
||||
@@ -2074,7 +2118,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
// ====== 退出检查 ======
|
||||
if (CheckBotExit(sit, state, cfg)) continue;
|
||||
|
||||
Vector3 turretPos = sit.tra_CurrTurret.position;
|
||||
Vector3 turretPos = sit.turretWorldPos;
|
||||
|
||||
// ====== 技能刷新(底层随机技能) ======
|
||||
if (cfg != null && cfg.skills != null)
|
||||
@@ -2101,10 +2145,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
{
|
||||
sit.isLock = true;
|
||||
sit.monCotl_Lock = target;
|
||||
// ⭐ 锁定位置优先用 lockPoint(锁定标志锚点),与玩家锁定保持一致
|
||||
sit.monCotl_Lock_Position = (target.lockPoint != null) ? target.lockPoint
|
||||
: (target.colliderList != null && target.colliderList.Count > 0
|
||||
? target.colliderList[0].transform : target.transform);
|
||||
// ⭐ 统一用 ResolveLockTransform:colliderList 第一个活跃 → lockPoint → transform
|
||||
sit.monCotl_Lock_Position = ResolveLockTransform(target);
|
||||
if (sit.lockFX != null) sit.lockFX.SetActive(true);
|
||||
}
|
||||
}
|
||||
@@ -2195,6 +2237,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
{
|
||||
var m = kv.Value;
|
||||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的 Boss 不作为目标
|
||||
if (!IsFishPathLockable(m)) continue;
|
||||
|
||||
int ft = (int)m.fishenum;
|
||||
bool isBoss = false;
|
||||
@@ -2312,7 +2356,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>处理 bot 退出:发离房消息、座位清理</summary>
|
||||
/// <summary>处理 bot 退出:发离房消息、座位清理、转移房主、保证出鱼</summary>
|
||||
private void HandleBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
|
||||
{
|
||||
state.hasExited = true;
|
||||
@@ -2337,6 +2381,27 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
botLastAngle.Remove(sit.user);
|
||||
botLastFireData.Remove(sit.user);
|
||||
|
||||
// ⭐ bot 退出后转移房主,否则房主是已退出的 bot 会导致出鱼停止
|
||||
// 1) 清除 bot 的房主引用(如果它是房主)
|
||||
// 2) TransferRoomOwner 会把房主交给第一个活跃的非 bot 玩家
|
||||
if (SitPosData.roomOwnerSit == sit)
|
||||
{
|
||||
SitPosData.roomOwnerSit = null;
|
||||
SitPosData.TransferRoomOwner();
|
||||
}
|
||||
|
||||
// ⭐ 强制保证本地玩家能继续出鱼:
|
||||
// bot 退出后如果本地玩家是新的房主(或房主为空但本地玩家在场),开启 fangzhu
|
||||
// 这是对 ChangeControllerUser 网络消息的兜底,防止网络消息丢失或时序问题导致不出鱼
|
||||
if (CreateFishManager.Instance != null)
|
||||
{
|
||||
bool localIsOwner = SitPosData.roomOwnerSit != null &&
|
||||
SitPosData.roomOwnerSit.user == GameManager.Instance.user.id;
|
||||
bool noOwner = SitPosData.roomOwnerSit == null;
|
||||
if (localIsOwner || noOwner)
|
||||
CreateFishManager.Instance.fangzhu = true;
|
||||
}
|
||||
|
||||
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
|
||||
sit.ShowWaiting();
|
||||
else
|
||||
@@ -2399,15 +2464,30 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
if (fishList.fishDatas_hashCodekey.TryGetValue(guid, out var m)
|
||||
&& m != null && m.gameObject.activeInHierarchy)
|
||||
{
|
||||
// 锁定时不检查 inFront,始终跟踪目标
|
||||
if (isLocked) return m;
|
||||
|
||||
Vector3 fp = m.transform.position;
|
||||
bool inFront = sit.IsFlipped ? fp.y < turretPos.y : fp.y > turretPos.y;
|
||||
if (inFront) return m;
|
||||
// ⭐ 路径进度过滤:如果记忆目标已经超出可锁定范围(即将离场),清除记忆重新找
|
||||
if (!IsFishPathLockable(m))
|
||||
{
|
||||
botTargetGuid.Remove(sit.posID);
|
||||
}
|
||||
else if (isLocked)
|
||||
{
|
||||
// 锁定时不检查 inFront,始终跟踪目标
|
||||
return m;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 fp = m.transform.position;
|
||||
bool inFront = sit.IsFlipped ? fp.y < turretPos.y : fp.y > turretPos.y;
|
||||
if (inFront) return m;
|
||||
// 不在前方 → 清除记忆
|
||||
botTargetGuid.Remove(sit.posID);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 目标无效 → 清除记忆
|
||||
botTargetGuid.Remove(sit.posID);
|
||||
}
|
||||
// 目标无效 → 清除记忆
|
||||
botTargetGuid.Remove(sit.posID);
|
||||
}
|
||||
|
||||
// 2. 锁定Boss模式 → 优先最大倍率Boss
|
||||
@@ -2468,6 +2548,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
{
|
||||
var m = kv.Value;
|
||||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
|
||||
if (!IsFishPathLockable(m)) continue;
|
||||
|
||||
Vector3 fishPos = m.transform.position;
|
||||
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
|
||||
@@ -2505,13 +2587,16 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
var result = new List<MonsterContral>();
|
||||
if (CreateFishManager.Instance == null) return result;
|
||||
var fishList = CreateFishManager.Instance;
|
||||
Vector3 turretPos = sit.tra_CurrTurret.position;
|
||||
// ⭐ 用世界坐标(Z=0平面),和 bot AI / 玩家自动锁定一致
|
||||
Vector3 turretPos = sit.turretWorldPos;
|
||||
|
||||
foreach (var kv in fishList.fishDatas_hashCodekey)
|
||||
{
|
||||
var m = kv.Value;
|
||||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||||
if (m == exclude) continue;
|
||||
// ⭐ 路径进度过滤:刚出生(< 0.2)或即将离场(> 0.8)的鱼不作为目标
|
||||
if (!IsFishPathLockable(m)) continue;
|
||||
|
||||
Vector3 fishPos = m.transform.position;
|
||||
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
|
||||
|
||||
@@ -23,6 +23,7 @@ public enum PropEnum
|
||||
大喇叭 = 9,
|
||||
技能 = 10,
|
||||
召唤卡 = 11,
|
||||
VIP经验 = 12,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -196105,9 +196105,9 @@ RectTransform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 1
|
||||
m_Children:
|
||||
- {fileID: 7752753625073944563}
|
||||
- {fileID: 7718285068269790146}
|
||||
- {fileID: 1106327632507529043}
|
||||
- {fileID: 7752753625073944563}
|
||||
m_Father: {fileID: 408568649757843747}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
@@ -201581,6 +201581,21 @@ PrefabInstance:
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: -532.66003
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1465445835306222036, guid: 9351da778e1c18c4f9243b73d7569c66,
|
||||
type: 3}
|
||||
propertyPath: m_Spacing.x
|
||||
value: 35
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1465445835306222036, guid: 9351da778e1c18c4f9243b73d7569c66,
|
||||
type: 3}
|
||||
propertyPath: m_Padding.m_Top
|
||||
value: 20
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1465445835306222036, guid: 9351da778e1c18c4f9243b73d7569c66,
|
||||
type: 3}
|
||||
propertyPath: m_Padding.m_Left
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1987338642318344794, guid: 9351da778e1c18c4f9243b73d7569c66,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -217,6 +217,20 @@ public class UIHome_BY_BackPackPanel : SubUIBase
|
||||
break;
|
||||
case PropEnum.技能: break;
|
||||
case PropEnum.召唤卡: break;
|
||||
case PropEnum.VIP经验:
|
||||
UIManager.Instance.ShowTiShi($"确定要使用VIP经验包吗?", () =>
|
||||
{
|
||||
APIRequestList.UseVipExp((result) =>
|
||||
{
|
||||
UIManager.Instance.ShowBoardError("使用成功");
|
||||
// 刷新用户数据
|
||||
APIRequestList.GetUser(GameManager.Instance.user.id, (user) =>
|
||||
{
|
||||
GameManager.Instance.UpdataUserData(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user