1
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 子弹专用对象池 — 按 prefab 实例ID 分桶,与鱼的 Pool 完全隔离
|
||||
/// 用法:
|
||||
/// var go = BulletPool.Instance.Get(prefab, pos, rot, parent);
|
||||
/// BulletPool.Instance.Return(go); // 代替 Destroy
|
||||
/// </summary>
|
||||
public class BulletPool : Singleton<BulletPool>
|
||||
{
|
||||
// prefab.GetInstanceID() → 该 prefab 的池
|
||||
private Dictionary<int, BulletPoolData> dic = new Dictionary<int, BulletPoolData>();
|
||||
|
||||
// 每个池子的最大缓存数(超出直接 Destroy,防止内存膨胀)
|
||||
private const int MaxCachePerPrefab = 200;
|
||||
|
||||
// 池子根节点(隐藏在场景里)
|
||||
private GameObject poolRoot;
|
||||
|
||||
/// <summary>从池中取一个子弹(池空则 Instantiate)</summary>
|
||||
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent)
|
||||
{
|
||||
if (prefab == null) return null;
|
||||
|
||||
int id = prefab.GetInstanceID();
|
||||
GameObject obj;
|
||||
|
||||
if (dic.TryGetValue(id, out var data) && data.poolList.Count > 0)
|
||||
{
|
||||
obj = data.GetObj();
|
||||
obj.transform.position = position;
|
||||
obj.transform.rotation = rotation;
|
||||
obj.transform.SetParent(parent, false);
|
||||
obj.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
obj = GameObject.Instantiate(prefab, position, rotation, parent);
|
||||
obj.SetActive(true);
|
||||
}
|
||||
|
||||
// 确保挂上 tracker 记录 prefabID(回收时凭此找到对应池)
|
||||
var tracker = obj.GetComponent<BulletPoolTracker>();
|
||||
if (tracker == null)
|
||||
tracker = obj.AddComponent<BulletPoolTracker>();
|
||||
tracker.prefabID = id;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>无 parent 重载</summary>
|
||||
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
return Get(prefab, position, rotation, null);
|
||||
}
|
||||
|
||||
/// <summary>回收子弹到池(超容量则 Destroy)</summary>
|
||||
public void Return(GameObject obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
|
||||
var tracker = obj.GetComponent<BulletPoolTracker>();
|
||||
int id = tracker != null ? tracker.prefabID : 0;
|
||||
|
||||
// 取不到 id → 兜底 Destroy(非池内对象)
|
||||
if (id == 0)
|
||||
{
|
||||
GameObject.Destroy(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dic.TryGetValue(id, out var data))
|
||||
dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id);
|
||||
|
||||
if (data.poolList.Count >= MaxCachePerPrefab)
|
||||
{
|
||||
GameObject.Destroy(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置子弹状态(若有 BullitMove 组件)
|
||||
var bm = obj.GetComponent<BullitMove>();
|
||||
if (bm != null) bm.ResetState();
|
||||
|
||||
data.PushObj(obj);
|
||||
}
|
||||
|
||||
/// <summary>预加载 N 个某 prefab 到池</summary>
|
||||
public void Preload(GameObject prefab, int count)
|
||||
{
|
||||
if (prefab == null || count <= 0) return;
|
||||
int id = prefab.GetInstanceID();
|
||||
|
||||
if (!dic.TryGetValue(id, out var data))
|
||||
dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var obj = GameObject.Instantiate(prefab, EnsurePoolRoot().transform);
|
||||
var tracker = obj.GetComponent<BulletPoolTracker>();
|
||||
if (tracker == null)
|
||||
tracker = obj.AddComponent<BulletPoolTracker>();
|
||||
tracker.prefabID = id;
|
||||
data.PushObj(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>清理所有子弹池(切场景时调用)</summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var kv in dic.Values)
|
||||
kv.Clear();
|
||||
dic.Clear();
|
||||
|
||||
if (poolRoot != null)
|
||||
{
|
||||
GameObject.Destroy(poolRoot);
|
||||
poolRoot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject EnsurePoolRoot()
|
||||
{
|
||||
if (poolRoot == null)
|
||||
{
|
||||
poolRoot = new GameObject("[BulletPool]");
|
||||
poolRoot.SetActive(false); // 池根节点隐藏,池内对象跟随隐藏
|
||||
}
|
||||
return poolRoot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>挂在池内对象上,记录所属 prefabID(回收时凭此找到对应池)</summary>
|
||||
public class BulletPoolTracker : MonoBehaviour
|
||||
{
|
||||
public int prefabID;
|
||||
}
|
||||
|
||||
/// <summary>单个 prefab 的子弹池数据</summary>
|
||||
public class BulletPoolData
|
||||
{
|
||||
public Transform fatherObj;
|
||||
public List<GameObject> poolList;
|
||||
|
||||
public BulletPoolData(Transform root, int id)
|
||||
{
|
||||
fatherObj = new GameObject("bulletPool_" + id).transform;
|
||||
fatherObj.SetParent(root, false);
|
||||
poolList = new List<GameObject>();
|
||||
}
|
||||
|
||||
public void PushObj(GameObject obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
poolList.Add(obj);
|
||||
obj.transform.SetParent(fatherObj, false);
|
||||
obj.SetActive(false);
|
||||
}
|
||||
|
||||
public GameObject GetObj()
|
||||
{
|
||||
GameObject obj = poolList[poolList.Count - 1];
|
||||
poolList.RemoveAt(poolList.Count - 1);
|
||||
return obj;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var obj in poolList)
|
||||
{
|
||||
if (obj != null) GameObject.Destroy(obj);
|
||||
}
|
||||
poolList.Clear();
|
||||
|
||||
if (fatherObj != null)
|
||||
GameObject.Destroy(fatherObj.gameObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b450d462c4ecd3349b396f1ff2014f40
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
@@ -17,11 +17,12 @@ public class PoolData
|
||||
|
||||
public void PushObj(GameObject obj,int id)
|
||||
{
|
||||
obj.SetActive(false);
|
||||
// 先实例化,再隐藏(原代码对 prefab 调 SetActive(false) 无效且语义混乱)
|
||||
obj = GameObject.Instantiate(obj);
|
||||
obj.name = id.ToString();
|
||||
poolList.Add(obj);
|
||||
obj.transform.parent = fatherObj.transform;
|
||||
obj.SetActive(false);
|
||||
poolList.Add(obj);
|
||||
}
|
||||
public void PushObj(GameObject obj)
|
||||
{
|
||||
|
||||
@@ -62,13 +62,30 @@ public class BullitMove : MonoBehaviour
|
||||
transform.position = vector3_startPosition;
|
||||
}
|
||||
|
||||
/// <summary>对象池回收前调用:重置所有运行时状态,避免复用时残留旧数据</summary>
|
||||
public void ResetState()
|
||||
{
|
||||
userid = null;
|
||||
playerID = null;
|
||||
bulletID = null;
|
||||
currShowTime = 0f;
|
||||
isTriggerFirst = true;
|
||||
target = null;
|
||||
targetTra = null;
|
||||
// 解除锁定 + 恢复碰撞器
|
||||
NotLock();
|
||||
// 重置起始位置(z=-400 是原 Awake→Init 的默认值)
|
||||
vector3_startPosition = transform.position;
|
||||
vector3_startPosition.z = -400;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 子弹生命周期:超时自动销毁(默认10秒)
|
||||
currShowTime += Time.deltaTime;
|
||||
if (currShowTime >= desTime)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
BulletPool.Instance.Return(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,7 +153,7 @@ public class BullitMove : MonoBehaviour
|
||||
monster.monsterData.charactersHealth.OnHurt(10, CharacterHurtType.none);
|
||||
Destroy(monster.gameObject);
|
||||
}
|
||||
Destroy(gameObject);
|
||||
BulletPool.Instance.Return(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,6 +245,6 @@ public class BullitMove : MonoBehaviour
|
||||
}
|
||||
|
||||
monster.OnHurt();
|
||||
Destroy(gameObject);
|
||||
BulletPool.Instance.Return(gameObject);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,146 @@ public class BotConfig
|
||||
|
||||
[Header("aggressive 状态参数")]
|
||||
public BotBehaviorConfig aggressiveBehavior = new BotBehaviorConfig();
|
||||
|
||||
[Header("倍数策略")]
|
||||
public BotMultiplierConfig multiplier = new BotMultiplierConfig();
|
||||
|
||||
[Header("Boss策略")]
|
||||
public BotBossStrategyConfig bossStrategy = new BotBossStrategyConfig();
|
||||
|
||||
[Header("退出条件")]
|
||||
public BotExitConfig exitCondition = new BotExitConfig();
|
||||
}
|
||||
|
||||
/// <summary>机器人倍数(炮台档位)策略配置</summary>
|
||||
[Serializable]
|
||||
public class BotMultiplierConfig
|
||||
{
|
||||
[Tooltip("进场初始炮台档位索引(对应 paoBeiList 的下标, 0=最低档)")]
|
||||
public int initialLevelIndex = 0;
|
||||
|
||||
[Tooltip("低于此倍数(当前金币/进场金币) → 降档")]
|
||||
public float decreaseThreshold = 0.5f;
|
||||
|
||||
[Tooltip("高于此倍数(当前金币/进场金币) → 升档")]
|
||||
public float increaseThreshold = 1.5f;
|
||||
|
||||
[Tooltip("每次调整的档位数")]
|
||||
public int step = 1;
|
||||
|
||||
[Tooltip("倍数检查间隔(秒)")]
|
||||
public float checkInterval = 3f;
|
||||
|
||||
[Tooltip("最低保持档位索引(不会降到低于此值)")]
|
||||
public int minLevelIndex = 0;
|
||||
|
||||
[Tooltip("最高保持档位索引(不会升到高于此值), -1=不限制")]
|
||||
public int maxLevelIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>机器人Boss策略配置</summary>
|
||||
[Serializable]
|
||||
public class BotBossStrategyConfig
|
||||
{
|
||||
[Tooltip("视为Boss的鱼类型 (按 FishTypeEnum 的int值)")]
|
||||
public int[] bossFishTypes = { 2, 5 }; // Boss鱼=2, 特殊Boss=5
|
||||
|
||||
[Tooltip("无Boss时启用散射乱射模式")]
|
||||
public bool scatterWhenNoBoss = true;
|
||||
|
||||
[Tooltip("无Boss时定点三发概率(0~1), 否则单射")]
|
||||
[Range(0f, 1f)] public float tripleShotProb = 0.5f;
|
||||
|
||||
[Tooltip("有Boss时优先锁定最大倍率(赔率)的Boss")]
|
||||
public bool preferMaxMultiplierBoss = true;
|
||||
|
||||
[Tooltip("Boss数量超过此值 → 开启散射模式")]
|
||||
public int scatterModeThreshold = 3;
|
||||
|
||||
[Tooltip("Boss数量<=此值时随机狂暴技能概率(0~1)")]
|
||||
[Range(0f, 1f)] public float frenzyProb = 0.6f;
|
||||
|
||||
[Tooltip("狂暴持续时间下限(秒)")]
|
||||
public float frenzyMinTime = 3f;
|
||||
|
||||
[Tooltip("狂暴持续时间上限(秒)")]
|
||||
public float frenzyMaxTime = 8f;
|
||||
}
|
||||
|
||||
/// <summary>机器人退出条件配置</summary>
|
||||
[Serializable]
|
||||
public class BotExitConfig
|
||||
{
|
||||
[Tooltip("金币超过此值时退出(盈利离场), 0=不检测")]
|
||||
public long exitWhenGoldAbove = 0;
|
||||
|
||||
[Tooltip("金币低于此值时退出(输完离场)")]
|
||||
public long exitWhenGoldBelow = 100;
|
||||
|
||||
[Tooltip("退出检查间隔(秒)")]
|
||||
public float checkInterval = 5f;
|
||||
|
||||
[Tooltip("退出时是否发送离房网络消息")]
|
||||
public bool sendLeaveRoom = true;
|
||||
|
||||
[Tooltip("退出后座位是否显示等待状态(等待下一个机器人)")]
|
||||
public bool showWaitingAfterExit = true;
|
||||
}
|
||||
|
||||
/// <summary>机器人当前行为模式</summary>
|
||||
public enum BotBehaviorMode
|
||||
{
|
||||
Idle, // 待机(未开始)
|
||||
ScatterRandom, // 散射乱射(无Boss): 随机定点三发或单射
|
||||
LockBoss, // 锁定Boss
|
||||
ScatterMode, // 散射模式(Boss数量>3)
|
||||
RandomFrenzy, // 随机狂暴(Boss数量<=3)
|
||||
Exited // 已退出
|
||||
}
|
||||
|
||||
/// <summary>每个 bot 座位的运行时状态(进场金币/倍数/模式/退出)</summary>
|
||||
public class BotRuntimeState
|
||||
{
|
||||
// ===== 进场 =====
|
||||
[Tooltip("进场时的金币(基准)")]
|
||||
public long entryGold;
|
||||
|
||||
[Tooltip("进场时间")]
|
||||
public float entryTime;
|
||||
|
||||
// ===== 倍数 =====
|
||||
[Tooltip("当前炮台档位索引")]
|
||||
public int currentLevelIndex;
|
||||
|
||||
[Tooltip("下次倍数检查时间")]
|
||||
public float nextMultiplierCheckTime;
|
||||
|
||||
// ===== 行为模式 =====
|
||||
[Tooltip("当前行为模式")]
|
||||
public BotBehaviorMode currentMode = BotBehaviorMode.Idle;
|
||||
|
||||
[Tooltip("上次Boss检测时间")]
|
||||
public float lastBossCheckTime;
|
||||
|
||||
// ===== 狂暴(Boss<=3时随机) =====
|
||||
[Tooltip("狂暴是否激活")]
|
||||
public bool frenzyActive;
|
||||
|
||||
[Tooltip("狂暴结束时间")]
|
||||
public float frenzyEndTime;
|
||||
|
||||
// ===== 退出 =====
|
||||
[Tooltip("下次退出检查时间")]
|
||||
public float nextExitCheckTime;
|
||||
|
||||
[Tooltip("是否已退出")]
|
||||
public bool hasExited;
|
||||
|
||||
/// <summary>当前金币相对进场金币的倍数</summary>
|
||||
public float GetGoldRatio(long currentGold)
|
||||
{
|
||||
return entryGold > 0 ? (float)currentGold / entryGold : 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
@@ -76,13 +76,63 @@ namespace Assets.MyGame.Scripts.Net.WebResponses.ChineseChess
|
||||
public string createUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态 1 未开始 2 进行中 3 已结束
|
||||
/// 状态 0 空房间 1 未开始 2 进行中 3 已结束
|
||||
/// </summary>
|
||||
public int state { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// 金币数量
|
||||
/// </summary>
|
||||
public long gold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 机器人数量(房间内 AI 玩家数,计入总人数)
|
||||
/// </summary>
|
||||
public int robotCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束游戏请求记录(前端不用,仅占位反序列化)
|
||||
/// </summary>
|
||||
public object endGameRequests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后活跃时间(Unix 秒)
|
||||
/// </summary>
|
||||
public long lastActiveTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时秒数
|
||||
/// </summary>
|
||||
public int timeoutSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// 超时输方用户ID
|
||||
/// </summary>
|
||||
public string timeoutLoserUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 待定赢方用户ID(可能为 null)
|
||||
/// </summary>
|
||||
public string pendingWinUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 游戏是否已结束
|
||||
/// </summary>
|
||||
public bool isGameEnded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 房间唯一 GUID(与 roomId 不同,roomId 是业务编号)
|
||||
/// </summary>
|
||||
public string id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已删除
|
||||
/// </summary>
|
||||
public bool isDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建信息(前端不用,仅占位反序列化)
|
||||
/// </summary>
|
||||
public object createInfo { get; set; }
|
||||
}
|
||||
[Serializable]
|
||||
/// <summary>
|
||||
|
||||
@@ -73,13 +73,14 @@ public class UIChineseChessDistrict : SubUIBase
|
||||
{
|
||||
switch (ChineseChessMainPanel.dic_RoomData[item.roomId].state)
|
||||
{
|
||||
case 1:
|
||||
case 0: // 空房间
|
||||
case 1: // 未开始
|
||||
int temp = ChineseChessMainPanel.dic_RoomData[item.roomId].users.Count;
|
||||
if (temp == 2)
|
||||
if (temp >= 2)
|
||||
{
|
||||
UIManager.Instance.ShowBoardError("玩家满员");
|
||||
}
|
||||
else if (temp == 1)
|
||||
else
|
||||
{
|
||||
uIManager.ShowAsync(UIPanel.ZGXQJoinGame, item);
|
||||
}
|
||||
@@ -88,7 +89,6 @@ public class UIChineseChessDistrict : SubUIBase
|
||||
UIManager.Instance.ShowBoardError("房间已经开始");
|
||||
break;
|
||||
case 3:
|
||||
case 0:
|
||||
uIManager.ShowAsync(UIPanel.ZGXQCreateRoom, item);
|
||||
break;
|
||||
default:
|
||||
@@ -100,7 +100,8 @@ public class UIChineseChessDistrict : SubUIBase
|
||||
ChineseChessMainPanel.dic_RoomData[item.roomId] = item;
|
||||
|
||||
var slider = tran.transform.GetChild(0).GetComponent<Slider>();
|
||||
int allPlayerCount = item.users.Count;
|
||||
// 人数 = 真实玩家 + 机器人(users.Count 只含真实玩家,robotCount 是 AI)
|
||||
int allPlayerCount = item.users.Count + item.robotCount;
|
||||
slider.value = (float)allPlayerCount / 2;
|
||||
if (allPlayerCount == 2)
|
||||
slider.fillRect.GetChild(0).gameObject.SetActive(true);
|
||||
@@ -193,13 +194,15 @@ public class UIChineseChessDistrict : SubUIBase
|
||||
ChineseChessMainPanel.dic_RoomData[roomID] = item;
|
||||
|
||||
var tran = itemListDic[roomID];
|
||||
var slider = tran.transform.GetChild(1).GetComponent<Slider>();
|
||||
int allPlayerCount = item.users.Count;
|
||||
// 子节点索引与 Show 保持一致:0=Slider, 1=人数文本, 2=房间号
|
||||
var slider = tran.transform.GetChild(0).GetComponent<Slider>();
|
||||
// 人数 = 真实玩家 + 机器人
|
||||
int allPlayerCount = item.users.Count + item.robotCount;
|
||||
slider.value = (float)allPlayerCount / 2;
|
||||
if (allPlayerCount == 2)
|
||||
slider.fillRect.GetChild(0).gameObject.SetActive(true);
|
||||
else
|
||||
slider.fillRect.GetChild(0).gameObject.SetActive(false);
|
||||
tran.transform.GetChild(2).GetComponent<TextMeshProUGUI>().text = allPlayerCount + "/" + 2;
|
||||
tran.transform.GetChild(1).GetComponent<TextMeshProUGUI>().text = allPlayerCount + "/" + 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
private Dictionary<int, float> botIdleTimer = new Dictionary<int, float>();
|
||||
// bot 最近的 FrameData_FireBullet(给其他客户端还原技能状态)
|
||||
private Dictionary<string, FrameData_FireBullet> botLastFireData = new Dictionary<string, FrameData_FireBullet>();
|
||||
// bot 运行时状态(进场金币/倍数档位/行为模式/退出)
|
||||
private Dictionary<int, BotRuntimeState> botRuntimeStates = new Dictionary<int, BotRuntimeState>();
|
||||
public GameObject bullet_Test;
|
||||
public GameObject bulletParent;
|
||||
float changeTurretTime = 0f;
|
||||
@@ -1510,8 +1512,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
if (!sit.ConsumeGold(isDouble ? 2 : 1))
|
||||
return null;
|
||||
|
||||
var bullet = Instantiate(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform)
|
||||
.GetComponent<BullitMove>();
|
||||
// ⭐ 从对象池取子弹(池空时内部会 Instantiate)
|
||||
var bulletGo = BulletPool.Instance.Get(prefab, pos, Quaternion.Euler(0, 0, angle), bulletParent.transform);
|
||||
if (bulletGo == null) return null;
|
||||
var bullet = bulletGo.GetComponent<BullitMove>();
|
||||
|
||||
bullet.userid = sit.user;
|
||||
bullet.playerID = sit.user;
|
||||
@@ -1896,6 +1900,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
if (!string.IsNullOrEmpty(user) && user.StartsWith("bot_"))
|
||||
sit.isBot = true;
|
||||
|
||||
// bot 进场:记录运行时状态(进场金币 / 初始档位)
|
||||
if (sit.isBot)
|
||||
InitBotRuntimeState(sit, goldNum);
|
||||
|
||||
sit.tMP_GoldNum.text = goldNum.ToString();
|
||||
sit.tMP_DiamondNum.text = diamondNum.ToString();
|
||||
sit.tMP_TurretConsumeNum.text = consumeGold.ToString();
|
||||
@@ -1945,20 +1953,35 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
if (!sit.isBot || sit.gameObject.activeSelf == false) continue;
|
||||
if (sit.tra_CurrTurret == null) continue;
|
||||
|
||||
// ====== 运行时状态 ======
|
||||
BotRuntimeState state = GetBotRuntimeState(sit);
|
||||
if (state == null || state.hasExited) continue;
|
||||
|
||||
// ====== 退出检查 ======
|
||||
if (CheckBotExit(sit, state, cfg)) continue;
|
||||
|
||||
Vector3 turretPos = sit.tra_CurrTurret.position;
|
||||
|
||||
// ====== 技能刷新 ======
|
||||
// ====== 技能刷新(底层随机技能) ======
|
||||
if (cfg != null && cfg.skills != null)
|
||||
RefreshBotSkills(sit, cfg.skills);
|
||||
var skill = botSkills.ContainsKey(sit.posID) ? botSkills[sit.posID] : null;
|
||||
|
||||
// ====== 目标选择(记住当前目标,死了/飞走了才换) ======
|
||||
MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill);
|
||||
// ====== Boss 检测 → 决策行为模式 ======
|
||||
List<MonsterContral> bosses = FindBossesInField(sit, fishList, turretPos, cfg);
|
||||
BotBehaviorMode mode = DecideBotMode(sit, state, bosses, cfg, skill);
|
||||
state.currentMode = mode;
|
||||
|
||||
// ====== 倍数调整(第一次:基于进场金币比率) ======
|
||||
AdjustBotMultiplier(sit, state, cfg);
|
||||
|
||||
// ====== 目标选择 ======
|
||||
MonsterContral target = GetOrFindBotTarget(sit, fishList, turretPos, skill, mode, bosses, cfg);
|
||||
if (target == null) continue;
|
||||
|
||||
// ====== 锁定技能:走真实锁定系统(显示锁定图标) ======
|
||||
if (skill != null && skill.lockActive)
|
||||
// ====== 锁定技能:走真实锁定系统 ======
|
||||
bool wantLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
|
||||
if (wantLock)
|
||||
{
|
||||
if (sit.monCotl_Lock != target)
|
||||
{
|
||||
@@ -1979,10 +2002,13 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 旋转(始终运行,Lerp自然减速) ======
|
||||
// ====== 旋转 ======
|
||||
RotateBotToTarget(sit, target.transform.position, rotateSpeed);
|
||||
|
||||
// ====== 持续射击 ======
|
||||
// ====== 倍数再调整(第二次:策略执行后再校验) ======
|
||||
AdjustBotMultiplier(sit, state, cfg);
|
||||
|
||||
// ====== 射击 ======
|
||||
if (!botFireTimer.ContainsKey(sit.posID))
|
||||
botFireTimer[sit.posID] = 0f;
|
||||
botFireTimer[sit.posID] += Time.deltaTime;
|
||||
@@ -1996,48 +2022,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
if (botFireTimer[sit.posID] >= fireInterval)
|
||||
{
|
||||
botFireTimer[sit.posID] = 0f;
|
||||
|
||||
// 攻击动画
|
||||
sit.ChangeState(TurretState.Attack);
|
||||
if (!botIdleTimer.ContainsKey(sit.posID))
|
||||
botIdleTimer[sit.posID] = 0f;
|
||||
botIdleTimer[sit.posID] = 0f;
|
||||
|
||||
float angle = sit.currentAngle;
|
||||
botLastAngle[sit.user] = angle;
|
||||
|
||||
// 技能参数(模仿真人 SyncFire 的行为)
|
||||
bool isSanShe = skill != null && skill.tripleActive;
|
||||
bool isDouble = skill != null && skill.doubleActive;
|
||||
bool isLock = skill != null && skill.lockActive;
|
||||
int count = isSanShe ? 3 : 1;
|
||||
long gold = isDouble ? sit.consumeGoldNum * 2 : sit.consumeGoldNum;
|
||||
|
||||
// 三发+锁定 → 找 N 个目标鱼的 GUID
|
||||
List<string> monsterIDs = null;
|
||||
if (isSanShe && isLock && target != null)
|
||||
{
|
||||
monsterIDs = new List<string> { target.guid };
|
||||
// 额外找 2 个目标(按优先级)
|
||||
var extraTargets = FindBotLockTargets(sit, target, 2);
|
||||
foreach (var m in extraTargets) monsterIDs.Add(m.guid);
|
||||
}
|
||||
|
||||
// 存储完整 fireData 供其他客户端还原
|
||||
botLastFireData[sit.user] = new FrameData_FireBullet
|
||||
{
|
||||
angle = angle,
|
||||
gold = gold,
|
||||
isSanShe = isSanShe,
|
||||
twoRTruuect = isDouble,
|
||||
isSuoDing = isLock,
|
||||
monsterIDs = monsterIDs
|
||||
};
|
||||
|
||||
WebSocketSendList.BotFireCannon(
|
||||
GameManager.Instance.gameData.currRoomData.roomId,
|
||||
sit.user, gold, count);
|
||||
SyncFire_ForBot(sit, angle, gold, isSanShe, isDouble, isLock, monsterIDs);
|
||||
BotFire(sit, target, skill, mode, cfg, behavior);
|
||||
}
|
||||
|
||||
// 空闲超时切回 Idle 动画
|
||||
@@ -2052,12 +2037,245 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>bot 进场时初始化运行时状态</summary>
|
||||
private void InitBotRuntimeState(SitPosData sit, long entryGold)
|
||||
{
|
||||
var cfg = GameManager.Instance.botConfig;
|
||||
int initIdx = cfg != null && cfg.multiplier != null ? cfg.multiplier.initialLevelIndex : 0;
|
||||
|
||||
var s = new BotRuntimeState
|
||||
{
|
||||
entryGold = entryGold,
|
||||
entryTime = Time.time,
|
||||
currentLevelIndex = initIdx,
|
||||
nextMultiplierCheckTime = Time.time + 2f,
|
||||
nextExitCheckTime = Time.time + 5f,
|
||||
hasExited = false
|
||||
};
|
||||
botRuntimeStates[sit.posID] = s;
|
||||
|
||||
// 把初始档位应用到炮台
|
||||
ApplyBotLevelIndex(sit, initIdx);
|
||||
}
|
||||
|
||||
/// <summary>获取 bot 运行时状态</summary>
|
||||
private BotRuntimeState GetBotRuntimeState(SitPosData sit)
|
||||
{
|
||||
if (botRuntimeStates.TryGetValue(sit.posID, out var s)) return s;
|
||||
// 兜底:未初始化时按当前金币建一份
|
||||
InitBotRuntimeState(sit, sit.goldNum);
|
||||
return botRuntimeStates.TryGetValue(sit.posID, out s) ? s : null;
|
||||
}
|
||||
|
||||
/// <summary>查找场内炮台前方的 Boss 列表</summary>
|
||||
private List<MonsterContral> FindBossesInField(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotConfig cfg)
|
||||
{
|
||||
var result = new List<MonsterContral>();
|
||||
if (cfg == null || cfg.bossStrategy == null) return result;
|
||||
var bossTypes = cfg.bossStrategy.bossFishTypes;
|
||||
if (bossTypes == null || bossTypes.Length == 0) return result;
|
||||
|
||||
foreach (var kv in fishList.fishDatas_hashCodekey)
|
||||
{
|
||||
var m = kv.Value;
|
||||
if (m == null || !m.gameObject.activeInHierarchy) continue;
|
||||
|
||||
int ft = (int)m.fishenum;
|
||||
bool isBoss = false;
|
||||
foreach (var bt in bossTypes) { if (bt == ft) { isBoss = true; break; } }
|
||||
if (!isBoss) continue;
|
||||
|
||||
Vector3 fishPos = m.transform.position;
|
||||
if (sit.IsFlipped && fishPos.y >= turretPos.y) continue;
|
||||
if (!sit.IsFlipped && fishPos.y <= turretPos.y) continue;
|
||||
|
||||
result.Add(m);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>根据 Boss 存在情况决策行为模式,并覆盖技能开关</summary>
|
||||
private BotBehaviorMode DecideBotMode(SitPosData sit, BotRuntimeState state, List<MonsterContral> bosses, BotConfig cfg, BotSkillRuntime skill)
|
||||
{
|
||||
if (cfg == null || cfg.bossStrategy == null) return BotBehaviorMode.ScatterRandom;
|
||||
var bs = cfg.bossStrategy;
|
||||
|
||||
// 狂暴到期清理
|
||||
if (state.frenzyActive && Time.time >= state.frenzyEndTime)
|
||||
state.frenzyActive = false;
|
||||
|
||||
if (bosses == null || bosses.Count == 0)
|
||||
{
|
||||
// ===== 无Boss → 散射乱射 =====
|
||||
// 随机选择定点三发或单射:覆盖 skill.tripleActive
|
||||
if (skill != null)
|
||||
skill.tripleActive = Random.value < bs.tripleShotProb;
|
||||
// 无Boss时关闭锁定Boss模式
|
||||
if (skill != null) skill.lockActive = false;
|
||||
return bs.scatterWhenNoBoss ? BotBehaviorMode.ScatterRandom : BotBehaviorMode.Idle;
|
||||
}
|
||||
|
||||
// ===== 有Boss =====
|
||||
if (bosses.Count > bs.scatterModeThreshold)
|
||||
{
|
||||
// Boss > 阈值 → 散射模式
|
||||
if (skill != null) skill.tripleActive = true;
|
||||
return BotBehaviorMode.ScatterMode;
|
||||
}
|
||||
|
||||
// Boss <= 阈值 → 随机狂暴
|
||||
if (!state.frenzyActive && Random.value < bs.frenzyProb)
|
||||
{
|
||||
state.frenzyActive = true;
|
||||
state.frenzyEndTime = Time.time + Random.Range(bs.frenzyMinTime, bs.frenzyMaxTime);
|
||||
}
|
||||
if (state.frenzyActive && skill != null)
|
||||
skill.frenzyActive = true;
|
||||
|
||||
// 锁定最大倍数Boss
|
||||
return BotBehaviorMode.LockBoss;
|
||||
}
|
||||
|
||||
/// <summary>倍数升降:当前金币/进场金币 < 阈值降档, > 阈值升档</summary>
|
||||
private void AdjustBotMultiplier(SitPosData sit, BotRuntimeState state, BotConfig cfg)
|
||||
{
|
||||
if (cfg == null || cfg.multiplier == null) return;
|
||||
var mc = cfg.multiplier;
|
||||
if (Time.time < state.nextMultiplierCheckTime) return;
|
||||
state.nextMultiplierCheckTime = Time.time + mc.checkInterval;
|
||||
|
||||
float ratio = state.GetGoldRatio(sit.goldNum);
|
||||
int maxIdx = mc.maxLevelIndex < 0 ? GetPaoBeiCount() - 1 : mc.maxLevelIndex;
|
||||
int minIdx = Mathf.Max(0, mc.minLevelIndex);
|
||||
|
||||
if (ratio < mc.decreaseThreshold && state.currentLevelIndex > minIdx)
|
||||
{
|
||||
state.currentLevelIndex = Mathf.Max(minIdx, state.currentLevelIndex - mc.step);
|
||||
ApplyBotLevelIndex(sit, state.currentLevelIndex);
|
||||
}
|
||||
else if (ratio > mc.increaseThreshold && state.currentLevelIndex < maxIdx)
|
||||
{
|
||||
state.currentLevelIndex = Mathf.Min(maxIdx, state.currentLevelIndex + mc.step);
|
||||
ApplyBotLevelIndex(sit, state.currentLevelIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前炮倍档位总数(paoBeiList 长度)</summary>
|
||||
private int GetPaoBeiCount()
|
||||
{
|
||||
if (paoBeiList != null && paoBeiList.Count > 0) return paoBeiList.Count;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>按档位索引设置炮台消耗(直接对齐 consumeGoldNum,避免触发音效/SyncCannonMultiple)</summary>
|
||||
private void ApplyBotLevelIndex(SitPosData sit, int targetIndex)
|
||||
{
|
||||
int count = GetPaoBeiCount();
|
||||
if (count <= 0 || paoBeiList == null || paoBeiList.Count == 0) return;
|
||||
targetIndex = Mathf.Clamp(targetIndex, 0, count - 1);
|
||||
sit.consumeGoldNum = paoBeiList[targetIndex];
|
||||
if (sit.tMP_TurretConsumeNum != null)
|
||||
sit.tMP_TurretConsumeNum.text = sit.consumeGoldNum.ToString();
|
||||
}
|
||||
|
||||
/// <summary>退出条件检查:金币>设定 或 金币<设定</summary>
|
||||
private bool CheckBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
|
||||
{
|
||||
if (cfg == null || cfg.exitCondition == null) return false;
|
||||
var ec = cfg.exitCondition;
|
||||
if (Time.time < state.nextExitCheckTime) return false;
|
||||
state.nextExitCheckTime = Time.time + ec.checkInterval;
|
||||
|
||||
bool shouldExit = false;
|
||||
if (ec.exitWhenGoldAbove > 0 && sit.goldNum >= ec.exitWhenGoldAbove) shouldExit = true;
|
||||
else if (sit.goldNum <= ec.exitWhenGoldBelow) shouldExit = true;
|
||||
|
||||
if (!shouldExit) return false;
|
||||
|
||||
HandleBotExit(sit, state, cfg);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>处理 bot 退出:发离房消息、座位清理</summary>
|
||||
private void HandleBotExit(SitPosData sit, BotRuntimeState state, BotConfig cfg)
|
||||
{
|
||||
state.hasExited = true;
|
||||
state.currentMode = BotBehaviorMode.Exited;
|
||||
|
||||
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.sendLeaveRoom)
|
||||
{
|
||||
string roomId = GameManager.Instance.gameData.currRoomData.roomId;
|
||||
WebSocketSendList.LeaveRoom(roomId, sit.user);
|
||||
}
|
||||
|
||||
// 清理锁定
|
||||
sit.isLock = false;
|
||||
sit.monCotl_Lock = null;
|
||||
if (sit.lockFX != null) sit.lockFX.SetActive(false);
|
||||
|
||||
// 清理记忆
|
||||
botTargetGuid.Remove(sit.posID);
|
||||
botFireTimer.Remove(sit.posID);
|
||||
botIdleTimer.Remove(sit.posID);
|
||||
botSkills.Remove(sit.posID);
|
||||
botLastAngle.Remove(sit.user);
|
||||
botLastFireData.Remove(sit.user);
|
||||
|
||||
if (cfg != null && cfg.exitCondition != null && cfg.exitCondition.showWaitingAfterExit)
|
||||
sit.ShowWaiting();
|
||||
else
|
||||
sit.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>bot 开火执行(从 HandleBotAI 抽出)</summary>
|
||||
private void BotFire(SitPosData sit, MonsterContral target, BotSkillRuntime skill, BotBehaviorMode mode, BotConfig cfg, BotBehaviorConfig behavior)
|
||||
{
|
||||
sit.ChangeState(TurretState.Attack);
|
||||
if (!botIdleTimer.ContainsKey(sit.posID))
|
||||
botIdleTimer[sit.posID] = 0f;
|
||||
botIdleTimer[sit.posID] = 0f;
|
||||
|
||||
float angle = sit.currentAngle;
|
||||
botLastAngle[sit.user] = angle;
|
||||
|
||||
bool isSanShe = (skill != null && skill.tripleActive) || mode == BotBehaviorMode.ScatterMode || mode == BotBehaviorMode.ScatterRandom;
|
||||
bool isDouble = skill != null && skill.doubleActive;
|
||||
bool isLock = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
|
||||
int count = isSanShe ? 3 : 1;
|
||||
long gold = isDouble ? sit.consumeGoldNum * 2 : sit.consumeGoldNum;
|
||||
|
||||
// 三发+锁定 → 找 N 个目标鱼的 GUID
|
||||
List<string> monsterIDs = null;
|
||||
if (isSanShe && isLock && target != null)
|
||||
{
|
||||
monsterIDs = new List<string> { target.guid };
|
||||
var extraTargets = FindBotLockTargets(sit, target, 2);
|
||||
foreach (var m in extraTargets) monsterIDs.Add(m.guid);
|
||||
}
|
||||
|
||||
botLastFireData[sit.user] = new FrameData_FireBullet
|
||||
{
|
||||
angle = angle,
|
||||
gold = gold,
|
||||
isSanShe = isSanShe,
|
||||
twoRTruuect = isDouble,
|
||||
isSuoDing = isLock,
|
||||
monsterIDs = monsterIDs
|
||||
};
|
||||
|
||||
WebSocketSendList.BotFireCannon(
|
||||
GameManager.Instance.gameData.currRoomData.roomId,
|
||||
sit.user, gold, count);
|
||||
SyncFire_ForBot(sit, angle, gold, isSanShe, isDouble, isLock, monsterIDs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或寻找目标:优先用记忆的目标(还在前方就沿用),死了/不在前方了再重新找
|
||||
/// </summary>
|
||||
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill)
|
||||
private MonsterContral GetOrFindBotTarget(SitPosData sit, CreateFishManager fishList, Vector3 turretPos, BotSkillRuntime skill,
|
||||
BotBehaviorMode mode, List<MonsterContral> bosses, BotConfig cfg)
|
||||
{
|
||||
bool isLocked = skill != null && skill.lockActive;
|
||||
bool isLocked = (skill != null && skill.lockActive) || mode == BotBehaviorMode.LockBoss;
|
||||
|
||||
// 1. 检查记忆目标是否还有效
|
||||
if (botTargetGuid.TryGetValue(sit.posID, out string guid))
|
||||
@@ -2076,13 +2294,50 @@ public class GameMainPanel_FishBase : GameMainPanel_Base
|
||||
botTargetGuid.Remove(sit.posID);
|
||||
}
|
||||
|
||||
// 2. 重新找目标(Boss > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近)
|
||||
// 2. 锁定Boss模式 → 优先最大倍率Boss
|
||||
if (mode == BotBehaviorMode.LockBoss && bosses != null && bosses.Count > 0
|
||||
&& cfg != null && cfg.bossStrategy != null && cfg.bossStrategy.preferMaxMultiplierBoss)
|
||||
{
|
||||
MonsterContral bossTarget = SelectMaxMultiplierBoss(bosses);
|
||||
if (bossTarget != null)
|
||||
{
|
||||
botTargetGuid[sit.posID] = bossTarget.guid;
|
||||
return bossTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 散射模式 → 在 Boss 中随机选一个(覆盖面广)
|
||||
if (mode == BotBehaviorMode.ScatterMode && bosses != null && bosses.Count > 0)
|
||||
{
|
||||
MonsterContral bossTarget = bosses[Random.Range(0, bosses.Count)];
|
||||
botTargetGuid[sit.posID] = bossTarget.guid;
|
||||
return bossTarget;
|
||||
}
|
||||
|
||||
// 4. 重新找目标(Boss > 黄金鱼 > 特殊鱼 > 普通鱼,同类型打最近)
|
||||
MonsterContral target = SelectBestTarget(sit, fishList, turretPos);
|
||||
if (target != null)
|
||||
botTargetGuid[sit.posID] = target.guid;
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <summary>从 Boss 列表中选倍率(beilv)最大的</summary>
|
||||
private MonsterContral SelectMaxMultiplierBoss(List<MonsterContral> bosses)
|
||||
{
|
||||
MonsterContral best = null;
|
||||
int maxBeiLv = int.MinValue;
|
||||
foreach (var b in bosses)
|
||||
{
|
||||
if (b == null) continue;
|
||||
if (b.beilv > maxBeiLv)
|
||||
{
|
||||
maxBeiLv = b.beilv;
|
||||
best = b;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按优先级选目标:特殊Boss(5) > Boss(2) > 黄金鱼(3) > 特殊鱼(4) > 普通鱼(1)
|
||||
/// 同一优先级内选最近的,只选炮台前方的鱼
|
||||
|
||||
@@ -14640,13 +14640,16 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
img_MianFei: {fileID: 4217081698748454709}
|
||||
img_MianFeiDuiGou: {fileID: 4330799887900817023}
|
||||
img_MianFeiBackForword: {fileID: 0}
|
||||
tmp_MianFeiPropCount: {fileID: 2852674146106605074}
|
||||
img_JingYing: {fileID: 7888319788910343562}
|
||||
img_JingYingDuiGou: {fileID: 8171562548857813552}
|
||||
img_JingYingBackForword: {fileID: 0}
|
||||
img_JingYingLocal: {fileID: 2255445385472211906}
|
||||
tmp_JingYingPropCount: {fileID: 9179038560444378523}
|
||||
img_ZhiZun: {fileID: 5489928632928310148}
|
||||
img_ZhiZunDuiGou: {fileID: 7680212169677354408}
|
||||
img_ZhiZunBackForword: {fileID: 0}
|
||||
img_ZhiZunLocal: {fileID: 4908228247286929228}
|
||||
tmp_ZhiZunPropCount: {fileID: 5372547274870681584}
|
||||
tmp_currLevel: {fileID: 1548048096998792596}
|
||||
@@ -21780,13 +21783,16 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier:
|
||||
img_MianFei: {fileID: 4680046685106973707}
|
||||
img_MianFeiDuiGou: {fileID: 7485202241167141595}
|
||||
img_MianFeiBackForword: {fileID: 0}
|
||||
tmp_MianFeiPropCount: {fileID: 2402272884510985401}
|
||||
img_JingYing: {fileID: 1845906190580021354}
|
||||
img_JingYingDuiGou: {fileID: 5578691452543731326}
|
||||
img_JingYingBackForword: {fileID: 0}
|
||||
img_JingYingLocal: {fileID: 1603058651523562701}
|
||||
tmp_JingYingPropCount: {fileID: 962158706182398437}
|
||||
img_ZhiZun: {fileID: 6578186223567257880}
|
||||
img_ZhiZunDuiGou: {fileID: 1456507539317513943}
|
||||
img_ZhiZunBackForword: {fileID: 0}
|
||||
img_ZhiZunLocal: {fileID: 3480563155218242293}
|
||||
tmp_ZhiZunPropCount: {fileID: 4221848766627442506}
|
||||
tmp_currLevel: {fileID: 2337044026528177313}
|
||||
@@ -25849,7 +25855,7 @@ RectTransform:
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 117.685, y: -25}
|
||||
m_SizeDelta: {x: 235.37, y: 50}
|
||||
m_SizeDelta: {x: 0, y: 50}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4614401808319948171
|
||||
CanvasRenderer:
|
||||
|
||||
Reference in New Issue
Block a user