using System;
|
using System.Collections.Generic;
|
using System.Linq;
|
using Cysharp.Threading.Tasks;
|
using LitJson;
|
using UnityEngine;
|
|
//公会攻防战
|
public class GuildAtkDefBatManager : GameSystemManager<GuildAtkDefBatManager>
|
{
|
/// <summary> 攻防战的功能MapID </summary>
|
public const int DataMapID = 33000;
|
|
|
/// <summary>周期配置 [[休战期星期x, ...], [备战期星期x, ...], [激战期星期x, ...]]</summary>
|
public int[][] cycleConfig;
|
/// <summary>每人可挑战次数,胜负都算次数</summary>
|
public int challengeCount;
|
/// <summary>备战期开始时参与条件:公会成员达x人</summary>
|
public int prepareMemberLimit;
|
/// <summary>备战开始前x天</summary>
|
public int prepareDayBefore;
|
/// <summary>累计活跃达Y</summary>
|
public int prepareActiveLimit;
|
/// <summary>按历史总战力倒序排序后每X个随机匹配</summary>
|
public int matchGroupSize;
|
/// <summary>可调整成员位置的权限所需成员职位:2-副盟,3-盟主</summary>
|
public int adjustPositionJobLevel;
|
/// <summary>挑战胜利奖励 [[物品ID, 个数], ...]</summary>
|
public int[][] winAward;
|
/// <summary>挑战失败奖励 [[物品ID, 个数], ...],也是扫荡奖励</summary>
|
public int[][] loseAward;
|
/// <summary>公会胜利奖励 [[物品ID, 个数], ...]</summary>
|
public int[][] guildWinAward;
|
/// <summary>公会失败奖励 [[物品ID, 个数], ...]</summary>
|
public int[][] guildLoseAward;
|
/// <summary>品质格子最大产出数 {品质:最大产出数},如 {2:9, 3:1},1-普通;2-稀有;3-珍稀,普通品质不限制</summary>
|
public Dictionary<int, int> qualityMaxOutput;
|
/// <summary>默认最大格子数,当参与成员小于等于该配置时生效</summary>
|
public int defaultMaxGrid;
|
/// <summary>动态扩容格子数,当参与成员大于默认最大格子数时,每次动态扩总格子数。最大总格子=默认最大格子数+动态扩容格子数*n,n=向上取整((参与成员-默认最大格子数)/扩容格子数)</summary>
|
public int expandGridCount;
|
|
public int shopType;
|
/// <summary>日志最多展示条数</summary>
|
public int maxLogCount;
|
|
|
public override void Init()
|
{
|
DTC0102_tagCDBPlayer.beforePlayerDataInitializeEventOnRelogin += OnBeforePlayerDataInitializeEventOnRelogin;
|
GuildManager.Instance.EnterOrQuitGuildEvent += OnEnterOrQuitGuildEvent;
|
StoreModel.Instance.RefreshBuyShopLimitEvent += OnRefreshShopLimit;
|
ParseConfig();
|
}
|
|
public override void Release()
|
{
|
DTC0102_tagCDBPlayer.beforePlayerDataInitializeEventOnRelogin -= OnBeforePlayerDataInitializeEventOnRelogin;
|
GuildManager.Instance.EnterOrQuitGuildEvent -= OnEnterOrQuitGuildEvent;
|
StoreModel.Instance.RefreshBuyShopLimitEvent -= OnRefreshShopLimit;
|
}
|
|
private void OnRefreshShopLimit()
|
{
|
UpdateRedpoint();
|
}
|
|
private void OnBeforePlayerDataInitializeEventOnRelogin()
|
{
|
ResetBattleData();
|
}
|
|
private void OnEnterOrQuitGuildEvent(bool isEnter)
|
{
|
ResetBattleData();
|
BatState = 0;
|
CurrentChallengeTargetPlayerID = 0;
|
OnUpdateAtkDefBatInfoEvent?.Invoke();
|
UpdateRedpoint();
|
}
|
|
void ParseConfig()
|
{
|
var configSet = FuncConfigConfig.Get("FmAtkDefBatSet");
|
cycleConfig = JsonMapper.ToObject<int[][]>(configSet.Numerical1);
|
challengeCount = int.Parse(configSet.Numerical2);
|
|
var configPrepare = FuncConfigConfig.Get("FmAtkDefBatPrepare");
|
prepareMemberLimit = int.Parse(configPrepare.Numerical1);
|
var prepareArr = configPrepare.Numerical2.Split('|');
|
prepareDayBefore = int.Parse(prepareArr[0]);
|
prepareActiveLimit = int.Parse(prepareArr[1]);
|
matchGroupSize = int.Parse(configPrepare.Numerical3);
|
adjustPositionJobLevel = int.Parse(configPrepare.Numerical4);
|
|
var configAward = FuncConfigConfig.Get("FmAtkDefBatAward");
|
winAward = JsonMapper.ToObject<int[][]>(configAward.Numerical1);
|
loseAward = JsonMapper.ToObject<int[][]>(configAward.Numerical2);
|
guildWinAward = JsonMapper.ToObject<int[][]>(configAward.Numerical3);
|
guildLoseAward = JsonMapper.ToObject<int[][]>(configAward.Numerical4);
|
|
var configTreasure = FuncConfigConfig.Get("FmAtkDefBatTreasure");
|
qualityMaxOutput = ConfigParse.ParseIntDict(configTreasure.Numerical1);
|
defaultMaxGrid = int.Parse(configTreasure.Numerical2);
|
expandGridCount = int.Parse(configTreasure.Numerical3);
|
|
var configShop = FuncConfigConfig.Get("FmAtkDefBatShop");
|
shopType = int.Parse(configShop.Numerical1);
|
|
var configRecord = FuncConfigConfig.Get("FmAtkDefBatRecord");
|
maxLogCount = int.Parse(configRecord.Numerical1);
|
}
|
|
/// <summary> 当前所在时期信息通知:0-无;1-休战期;2-备战期;3-激战期 </summary>
|
public int BatState { get; private set; }
|
|
/// <summary> 己方公会基础信息 </summary>
|
public HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatFamily MyFamilyInfo { get; private set; }
|
|
/// <summary> 敌方公会基础信息 (若无参战资格或未匹配到则为null) </summary>
|
public HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatFamily EnemyFamilyInfo { get; private set; }
|
|
/// <summary> 己方参与成员字典,Key为PlayerID,方便差异更新与删除查找 </summary>
|
public Dictionary<uint, HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem> MyFamilyMembers = new Dictionary<uint, HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem>();
|
|
/// <summary> 当前挑战的防守方成员ID(用于战场界面显示敌方信息) </summary>
|
public uint CurrentChallengeTargetPlayerID { get; set; }
|
|
/// <summary> 敌方参与成员字典,Key为PlayerID,方便差异更新与删除查找 </summary>
|
public Dictionary<uint, HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem> EnemyFamilyMembers = new Dictionary<uint, HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem>();
|
|
public int MyFamilyID => PlayerDatas.Instance?.fairyData?.fairy?.FamilyID ?? 0;
|
|
public event Action OnUpdateAtkDefBatInfoEvent;
|
|
/// <summary>
|
/// 接收 A525 封包,处理全量/增量/删除
|
/// </summary>
|
public void UpdateAtkDefBatInfo(HA525_tagSCFamilyAtkDefBatInfo pack)
|
{
|
if (pack == null) return;
|
|
// 1. 数据重置判断:从非备战期变为备战期,或者收到无参与资格(公会数=1 且 参战成员=0)
|
bool isEnterPrepare = BatState != 2 && pack.BatState == 2;
|
bool isNotQualified = pack?.FamilyCount == 1 && pack?.BatFamilyList?.Length > 0 && pack?.BatFamilyList[0]?.BatMemCount == 0;
|
if (isEnterPrepare || isNotQualified)
|
ResetBattleData();
|
|
BatState = pack.BatState;
|
|
// 3. 差异更新公会与成员数据
|
if (pack.BatFamilyList != null)
|
{
|
for (int i = 0; i < pack.FamilyCount; i++)
|
{
|
var familyData = pack.BatFamilyList[i];
|
if (familyData == null) continue;
|
|
bool isMyFamily = familyData.FamilyID == MyFamilyID;
|
if (isMyFamily)
|
{
|
MyFamilyInfo = UpdateFamilyBaseInfo(MyFamilyInfo, familyData);
|
UpdateMembersDiff(MyFamilyMembers, familyData.BatMemList);
|
}
|
else
|
{
|
EnemyFamilyInfo = UpdateFamilyBaseInfo(EnemyFamilyInfo, familyData);
|
UpdateMembersDiff(EnemyFamilyMembers, familyData.BatMemList);
|
}
|
}
|
}
|
|
// 4. 处理成员退出删除同步
|
if (pack.DelMemID > 0)
|
{
|
if (MyFamilyMembers.ContainsKey(pack.DelMemID))
|
{
|
MyFamilyMembers.Remove(pack.DelMemID);
|
}
|
else if (EnemyFamilyMembers.ContainsKey(pack.DelMemID))
|
{
|
EnemyFamilyMembers.Remove(pack.DelMemID);
|
}
|
}
|
|
OnUpdateAtkDefBatInfoEvent?.Invoke();
|
|
// 解析宝库格子记录
|
ParseTreasureGridRecords();
|
|
UpdateRedpoint();
|
}
|
|
/// <summary>
|
/// 重置新一轮攻防战数据缓存
|
/// </summary>
|
private void ResetBattleData()
|
{
|
MyFamilyInfo = null;
|
EnemyFamilyInfo = null;
|
MyFamilyMembers.Clear();
|
EnemyFamilyMembers.Clear();
|
atkDefBatLogs.Clear();
|
treasureGridRecords.Clear();
|
hasShowedMainRedpoint = false;
|
}
|
|
/// <summary>
|
/// 更新公会基础信息(剥离掉成员数组单独管理)
|
/// </summary>
|
private HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatFamily UpdateFamilyBaseInfo(
|
HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatFamily targetInfo,
|
HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatFamily sourceData)
|
{
|
if (targetInfo == null)
|
targetInfo = new HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatFamily();
|
targetInfo.FamilyID = sourceData.FamilyID;
|
targetInfo.FamilyName = UIHelper.ServerStringTrim(sourceData.FamilyName);
|
targetInfo.ServerID = sourceData.ServerID;
|
targetInfo.EmblemID = sourceData.EmblemID;
|
targetInfo.EmblemWord = UIHelper.ServerStringTrim(sourceData.EmblemWord);
|
targetInfo.IsDismiss = sourceData.IsDismiss;
|
targetInfo.TotalStar = sourceData.TotalStar;
|
targetInfo.OverState = sourceData.OverState;
|
targetInfo.BatMemCount = sourceData.BatMemCount;
|
return targetInfo;
|
}
|
|
/// <summary>
|
/// 对参战成员进行差异替换更新
|
/// </summary>
|
private void UpdateMembersDiff(Dictionary<uint, HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem> memberDict, HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem[] memList)
|
{
|
if (memList == null) return;
|
|
for (int i = 0; i < memList.Length; i++)
|
{
|
var mem = memList[i];
|
if (mem == null) continue;
|
|
memberDict[mem.PlayerID] = mem;
|
}
|
}
|
|
/// <summary>
|
/// 前端自行排序:获取防守成员列表(按防守类型从小到大 -> 站位编号从小到大排序)
|
/// </summary>
|
public List<HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem> GetSortedDefenders(bool isMyFamily)
|
{
|
var dict = isMyFamily ? MyFamilyMembers : EnemyFamilyMembers;
|
var list = new List<HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem>();
|
|
foreach (var mem in dict.Values)
|
{
|
if (mem.DefType != 0) // DefType非0代表是防守人员
|
list.Add(mem);
|
}
|
|
// 排序规则:主将(1) -> 副将(2) -> 镇卫(3),同类型内按PosNum从小到大排序
|
list.Sort((a, b) =>
|
{
|
int defTypeCompare = a.DefType.CompareTo(b.DefType);
|
if (defTypeCompare != 0)
|
return defTypeCompare;
|
return a.PosNum.CompareTo(b.PosNum);
|
});
|
|
return list;
|
}
|
|
/// <summary>
|
/// 计算成员总战力(long类型)
|
/// 服务端将战力拆分为 FightPower(余亿部分)和 FightPowerEx(整除亿部分)两个uint字段
|
/// </summary>
|
public static long CalcTotalFightPower(uint fightPower, uint fightPowerEx)
|
{
|
return fightPower + fightPowerEx * Constants.ExpPointValue;
|
}
|
|
/// <summary>
|
/// 获取用于核心成员展示的排序列表(最多4个)
|
/// </summary>
|
/// <param name="isMyFamily">是否己方公会</param>
|
/// <param name="displayState">当前显示状态: 2-备战期, 3-激战期, 4/5-休战期</param>
|
/// <returns>排序后的成员列表(最多4个)</returns>
|
public List<HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem> GetSortedCoreMembers(bool isMyFamily, int displayState)
|
{
|
var dict = isMyFamily ? MyFamilyMembers : EnemyFamilyMembers;
|
var list = new List<HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem>(dict.Values);
|
|
if (list.Count <= 0) return list;
|
|
bool isPrepareOrBattle = displayState == 2 || displayState == 3;
|
|
if (isPrepareOrBattle)
|
{
|
if (isMyFamily)
|
{
|
// 己方:按职位(FmLV: 3>2>1>0)降序 → 战力降序
|
var fairyData = PlayerDatas.Instance?.fairyData;
|
list.Sort((a, b) =>
|
{
|
int fmLVA = fairyData?.GetMember((int)a.PlayerID)?.FmLV ?? 0;
|
int fmLVB = fairyData?.GetMember((int)b.PlayerID)?.FmLV ?? 0;
|
if (fmLVB != fmLVA)
|
return fmLVB.CompareTo(fmLVA);
|
|
long powerA = CalcTotalFightPower(a.FightPower, a.FightPowerEx);
|
long powerB = CalcTotalFightPower(b.FightPower, b.FightPowerEx);
|
return powerB.CompareTo(powerA);
|
});
|
}
|
else
|
{
|
// 敌方:按职位(FmLV: 3>2>1>0)降序 → 战力降序
|
list.Sort((a, b) =>
|
{
|
if (b.Job != a.Job)
|
return b.Job.CompareTo(a.Job);
|
long powerA = CalcTotalFightPower(a.FightPower, a.FightPowerEx);
|
long powerB = CalcTotalFightPower(b.FightPower, b.FightPowerEx);
|
return powerB.CompareTo(powerA);
|
});
|
}
|
}
|
else
|
{
|
// 休战期(4/5):按星星(Star)降序 → 已战斗次数(BatCnt)降序
|
list.Sort((a, b) =>
|
{
|
if (b.Star != a.Star)
|
return b.Star.CompareTo(a.Star);
|
return b.BatCnt.CompareTo(a.BatCnt);
|
});
|
}
|
|
// 最多取4个
|
if (list.Count > 4)
|
list = list.GetRange(0, 4);
|
|
return list;
|
}
|
|
/// <summary>
|
/// 判断是否是系统公会 (按照文档:ID >= 10亿)
|
/// </summary>
|
public bool IsSystemFamily(uint familyID)
|
{
|
return familyID >= 1000000000;
|
}
|
|
/// <summary>
|
/// 解析 KilledState,判定防守人员的某个属性百分比是否已经被击败
|
/// </summary>
|
/// <param name="propertyIndex">属性索引位(从0开始计)</param>
|
public bool IsPropertyKilled(byte killedState, int propertyIndex)
|
{
|
return (killedState & (1 << propertyIndex)) != 0;
|
}
|
|
|
/// <summary>发送更换参战成员请求</summary>
|
/// <param name="playerIDA">要被替换的成员ID</param>
|
/// <param name="playerIDB">替换A成员的成员ID</param>
|
public void SendChangeBatMember(uint playerIDA, uint playerIDB)
|
{
|
var pack = new CA635_tagCSFamilyAtkDefBatChangeMem();
|
pack.PlayerIDA = playerIDA;
|
pack.PlayerIDB = playerIDB;
|
GameNetSystem.Instance.SendInfo(pack);
|
}
|
|
public bool IsOpen(bool tip = false)
|
{
|
return FuncOpen.Instance.IsFuncOpen((int)FuncOpenEnum.GuildAtkDefBat, tip);
|
}
|
|
public bool HasFamilyJoinEligibility()
|
{
|
return !MyFamilyMembers.IsNullOrEmpty() && MyFamilyMembers.Count > 0;
|
}
|
|
public bool HasMyJoinEligibility()
|
{
|
if (!HasFamilyJoinEligibility()) return false;
|
if (MyFamilyMembers.IsNullOrEmpty()) return false;
|
return MyFamilyMembers.ContainsKey(PlayerDatas.Instance.PlayerId);
|
}
|
|
/// <summary>
|
/// 获取攻防战的当前显示状态标识
|
/// </summary>
|
/// <returns>1-未参战, 2-备战中, 3-激战中, 4-胜利(休战中), 5-失败(休战中)</returns>
|
public int GetGuildBattleDisplayState()
|
{
|
if (!HasFamilyJoinEligibility())
|
return 1;
|
|
if (BatState == 2) return 2;
|
if (BatState == 3) return 3;
|
|
// 休战期:根据胜负结果判定
|
if (BatState == 1 && MyFamilyInfo != null)
|
{
|
int overState = MyFamilyInfo.OverState;
|
if (overState == 1 || overState == 3) return 4;
|
if (overState == 2 || overState == 4) return 5;
|
}
|
|
return 1;
|
}
|
|
/// <summary>
|
/// 获取当前玩家剩余进攻次数
|
/// </summary>
|
public int GetMyRemainingChallengeCount()
|
{
|
uint myPlayerId = PlayerDatas.Instance.PlayerId;
|
if (MyFamilyMembers != null && MyFamilyMembers.TryGetValue(myPlayerId, out var myData))
|
{
|
return challengeCount - myData.BatCnt;
|
}
|
return 0;
|
}
|
|
/// <summary>
|
/// 获取己方公会排序后的参战成员列表
|
/// 排序规则:Star降序 → BatCnt升序 → PlayerID升序
|
/// </summary>
|
public List<HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem> GetSortedFamilyMembers()
|
{
|
var members = MyFamilyMembers;
|
if (members == null || members.Count <= 0)
|
return new List<HA525_tagSCFamilyAtkDefBatInfo.tagSCFamilyAtkDefBatMem>();
|
|
var list = members.Values.ToList();
|
list.Sort((a, b) =>
|
{
|
if (b.Star != a.Star)
|
return b.Star.CompareTo(a.Star);
|
if (a.BatCnt != b.BatCnt)
|
return a.BatCnt.CompareTo(b.BatCnt);
|
return a.PlayerID.CompareTo(b.PlayerID);
|
});
|
return list;
|
}
|
#region 红点
|
Redpoint mainRedpoint = new Redpoint(MainRedDot.MainGuildRedpoint, MainRedDot.GuildAtkDefBatRepoint);
|
Redpoint fightRedpoint = new Redpoint(MainRedDot.GuildAtkDefBatRepoint, MainRedDot.GuildAtkDefBatRepoint * 10 + 1);
|
Redpoint treasureRedpoint = new Redpoint(MainRedDot.GuildAtkDefBatRepoint, MainRedDot.GuildAtkDefBatRepoint * 10 + 2);
|
Redpoint shopRedpoint = new Redpoint(MainRedDot.GuildAtkDefBatRepoint, MainRedDot.GuildAtkDefBatRepoint * 10 + 3);
|
private bool hasShowedMainRedpoint;
|
|
public void UpdateRedpoint()
|
{
|
mainRedpoint.state = RedPointState.None;
|
fightRedpoint.state = RedPointState.None;
|
treasureRedpoint.state = RedPointState.None;
|
shopRedpoint.state = RedPointState.None;
|
|
if (!IsOpen())
|
return;
|
|
// 商店:有免费商品可领取
|
if (HasShopFreeItem())
|
shopRedpoint.state = RedPointState.Simple;
|
|
if (!HasFamilyJoinEligibility())
|
return;
|
uint myPlayerId = PlayerDatas.Instance.PlayerId;
|
bool isMyParticipant = MyFamilyMembers.ContainsKey(myPlayerId);
|
|
// 备战期:参战成员显示mainRedpoint,进入主界面后消失
|
if (BatState == 2 && isMyParticipant && !hasShowedMainRedpoint)
|
mainRedpoint.state = RedPointState.Simple;
|
|
// 激战期:有剩余挑战次数的参战成员
|
if (BatState == 3 && isMyParticipant && GetMyRemainingChallengeCount() > 0)
|
fightRedpoint.state = RedPointState.Simple;
|
|
// 休战期:有宝库开启次数
|
if (BatState == 1 && GetMyOpenRemainCount() > 0)
|
treasureRedpoint.state = RedPointState.Simple;
|
}
|
|
public void ClearMainRedpoint()
|
{
|
hasShowedMainRedpoint = true;
|
UpdateRedpoint();
|
}
|
|
private bool HasShopFreeItem()
|
{
|
if (!StoreModel.Instance.freeShopDict.ContainsKey(shopType))
|
return false;
|
|
var freeShopList = StoreModel.Instance.freeShopDict[shopType];
|
for (int i = 0; i < freeShopList.Count; i++)
|
{
|
int shopID = freeShopList[i];
|
var config = StoreConfig.Get(shopID);
|
if (config != null && StoreModel.Instance.GetShopLimitBuyCount(shopID) < config.LimitCnt)
|
return true;
|
}
|
return false;
|
}
|
#endregion
|
|
#region 跨服与周期推算相关
|
|
/// <summary>
|
/// 判断当前攻防战是否处于跨服状态
|
/// </summary>
|
public bool IsCrossServer()
|
{
|
return CrossServerBaseManager.Instance.IsOpenCrossServer(DataMapID);
|
}
|
|
/// <summary>
|
/// 获取当前公会所处的 ZoneID (用于判断取本服还是跨服时间及其他跨服数据)
|
/// </summary>
|
/// <returns>返回0代表本服,大于0代表跨服分区ID</returns>
|
public int GetZoneID()
|
{
|
var crossInfo = CrossServerBaseManager.Instance.GetCrossZoneInfo(DataMapID);
|
return (int)(crossInfo?.ZoneID ?? 0);
|
}
|
|
/// <summary>
|
/// 根据本地时间和 cycleConfig 配置推算当前处于哪个阶段
|
/// 返回对应的 BatState 状态值:0-无, 1-休战期, 2-备战期, 3-激战期
|
/// </summary>
|
public int GetCurrentPeriodByConfig()
|
{
|
if (cycleConfig.IsNullOrEmpty())
|
return 0;
|
|
int currentWeekday = TimeUtility.GetCommonWeekday(GetZoneID());
|
for (int i = 0; i < cycleConfig.Length; i++)
|
{
|
if (cycleConfig[i] != null && Array.IndexOf(cycleConfig[i], currentWeekday) >= 0)
|
return i + 1;
|
}
|
return 0;
|
}
|
|
public string GetGetCountdownToCurrentPeriodEndString(string key = "")
|
{
|
int time = GetCountdownToCurrentPeriodEnd();
|
if (time <= 0) return string.Empty;
|
|
string timeStr = TimeUtility.SecondsToShortDHMS(time);
|
return string.IsNullOrEmpty(key) ? timeStr : Language.Get(key, timeStr);
|
}
|
|
/// <summary>
|
/// 获取【当前阶段结束】的倒计时(自动计算到下一个不同阶段的0点)
|
/// </summary>
|
public int GetCountdownToCurrentPeriodEnd()
|
{
|
int currentState = GetCurrentPeriodByConfig();
|
if (currentState == 0) return 0;
|
|
// 下一个阶段: 1→2, 2→3, 3→1
|
return GetCountdownToNextPeriod(currentState % 3 + 1);
|
}
|
|
/// <summary>
|
/// 传入目标时期(BatState),获取距离该周期开始(当天0点)的倒计时秒数
|
/// </summary>
|
public int GetCountdownToNextPeriod(int targetBatState)
|
{
|
int targetIdx = targetBatState - 1;
|
if (cycleConfig == null || targetIdx < 0 || targetIdx >= cycleConfig.Length)
|
return 0;
|
|
int[] targetDays = cycleConfig[targetIdx];
|
if (targetDays == null || targetDays.Length == 0)
|
return 0;
|
|
int zoneID = GetZoneID();
|
DateTime now = TimeUtility.GetCommServerNow(zoneID);
|
int currentWeekday = TimeUtility.GetCommonWeekday(zoneID);
|
|
// 找到距离最近的配置日(取最小间隔),无需排序
|
int minDayDiff = 7; // 最大不可能超过7
|
foreach (int day in targetDays)
|
{
|
int diff = day > currentWeekday ? day - currentWeekday : 7 - currentWeekday + day;
|
if (diff < minDayDiff) minDayDiff = diff;
|
}
|
|
// 计算目标日期0点
|
DateTime targetTime = now.AddDays(minDayDiff).Date;
|
return Mathf.Max(0, (int)(targetTime - now).TotalSeconds);
|
}
|
|
#endregion
|
|
#region 日志
|
/// <summary>公会攻防战进攻日志 ActionType</summary>
|
public const int GuildAtkDefBatLogActionType = 22;
|
|
/// <summary>缓存解析后的周x→累计贡献字典 (key: 1-7, value: 累计贡献)</summary>
|
private Dictionary<int, long> activityData = new Dictionary<int, long>();
|
|
|
public class GuildAtkDefBatPlayerInfo
|
{
|
public int serverID;
|
public int face;
|
public int facePic;
|
public int fmLV; //官职
|
public int titleID; //称号
|
public string name;
|
public string fightPower;
|
}
|
|
public class GuildAtkDefBatLogEntry
|
{
|
public uint familyID;
|
public HA513_tagMCFamilyActionInfo.tagMCFamilyAction action;
|
public GuildAtkDefBatPlayerInfo atkPlayerInfo;
|
public GuildAtkDefBatPlayerInfo defPlayerInfo;
|
}
|
public event Action OnUpdateAtkDefBatLogEvent;
|
public bool UpdateAtkDefBatLogInfo(HA513_tagMCFamilyActionInfo vNetData)
|
{
|
if (vNetData.ActionType != GuildAtkDefBatLogActionType) return false;
|
if (vNetData.FamilyActionList.IsNullOrEmpty()) return false;
|
|
for (int i = 0; i < vNetData.FamilyActionList.Length; i++)
|
{
|
var action = vNetData.FamilyActionList[i];
|
if (action == null) continue;
|
|
var entry = new GuildAtkDefBatLogEntry
|
{
|
familyID = vNetData.FamilyID,
|
action = action,
|
};
|
|
ParseLogUseData(action.UseData, entry);
|
atkDefBatLogs.Add(entry);
|
}
|
|
// 按时间倒序排列(最新在前)
|
atkDefBatLogs.Sort((a, b) => b.action.Time.CompareTo(a.action.Time));
|
|
OnUpdateAtkDefBatLogEvent?.Invoke();
|
return true;
|
}
|
|
private void ParseLogUseData(string useData, GuildAtkDefBatLogEntry entry)
|
{
|
if (string.IsNullOrEmpty(useData)) return;
|
|
try
|
{
|
JsonData jsonData = JsonMapper.ToObject(useData);
|
if (jsonData == null || !jsonData.IsObject) return;
|
|
entry.atkPlayerInfo = ParsePlayerInfo(jsonData["atkInfo"]);
|
entry.defPlayerInfo = ParsePlayerInfo(jsonData["defInfo"]);
|
}
|
catch (Exception e)
|
{
|
Debug.LogError($"[GuildAtkDefBat] ParseLogUseData error: {e.Message}");
|
}
|
}
|
|
|
GuildAtkDefBatPlayerInfo ParsePlayerInfo(JsonData infoArray)
|
{
|
if (infoArray == null || !infoArray.IsArray || infoArray.Count < 7)
|
return null;
|
|
try
|
{
|
return new GuildAtkDefBatPlayerInfo
|
{
|
serverID = int.Parse(infoArray[0].ToString()),
|
face = int.Parse(infoArray[1].ToString()),
|
facePic = int.Parse(infoArray[2].ToString()),
|
fmLV = int.Parse(infoArray[3].ToString()),
|
titleID = int.Parse(infoArray[4].ToString()),
|
name = UIHelper.ServerStringTrim(infoArray[5].ToString()),
|
fightPower = UIHelper.ReplaceLargeArtNum(long.Parse(infoArray[6].ToString())),
|
};
|
}
|
catch
|
{
|
return null;
|
}
|
}
|
#endregion
|
|
#region 累计活跃度
|
|
/// <summary>公会攻防战家族行为 ActionType(累计活跃)</summary>
|
public const int GuildAtkDefBatActionType = 19;
|
|
/// <summary> 攻防战日志列表(按时间倒序) </summary>
|
public List<GuildAtkDefBatLogEntry> atkDefBatLogs = new List<GuildAtkDefBatLogEntry>();
|
public event Action OnUpdateeFamilyActionInfoEvent;
|
public bool UpdateFamilyActionInfo(HA513_tagMCFamilyActionInfo vNetData)
|
{
|
if (vNetData.ActionType != GuildAtkDefBatActionType) return false;
|
if (vNetData.FamilyID != MyFamilyID) return false;
|
if (vNetData.FamilyActionList.IsNullOrEmpty()) return false;
|
foreach (var item in vNetData.FamilyActionList)
|
{
|
ParseUseData(item.UseData);
|
}
|
OnUpdateeFamilyActionInfoEvent?.Invoke();
|
return true;
|
}
|
/// <summary>
|
/// 解析 UseData JSON,提取周x→累计贡献的映射
|
/// UseData 格式: {"1":2000,"2":3000,"4":1000}
|
/// key 为周几(1-7, 1=周一), value 为当日累计贡献
|
/// </summary>
|
private void ParseUseData(string useData)
|
{
|
if (string.IsNullOrEmpty(useData)) return;
|
activityData.Clear();
|
|
try
|
{
|
JsonData jsonData = JsonMapper.ToObject(useData);
|
if (jsonData == null || !jsonData.IsObject) return;
|
|
foreach (string key in jsonData.Keys)
|
{
|
if (int.TryParse(key, out int weekday))
|
activityData[weekday] = (long)jsonData[key];
|
}
|
}
|
catch (Exception e)
|
{
|
Debug.LogError($"[GuildAtkDefBat] ParseUseData error: {e.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 获取界面展示用的公会攻防战活跃值。
|
/// 口径:显示下一次备战名单生成所使用的 prepareDayBefore 天窗口。
|
/// 后端会在进入备战期 0 点判定参战条件,统计备战日前 prepareDayBefore 天。
|
/// 前端按 cycleConfig[1] 找下一次备战日,再倒推 prepareDayBefore 天;窗口中尚未发生的未来日期按 0 处理,
|
/// 避免 activityData 只有星期 key 时误读到上一轮同星期的旧活跃。
|
/// </summary>
|
public long GetMyActivityLevel()
|
{
|
int daysUntilPrepare = GetDaysUntilNextPrepareStart();
|
return GetActivityLevelBeforePrepare(daysUntilPrepare);
|
}
|
|
/// <summary>
|
/// 获取距离下一次备战期开始还有几天。
|
/// 只从 cycleConfig[1] 读取备战期配置,支持策划自由配置备战星期。
|
/// 如果今天就是备战日,也取下一次备战日,避免把已进入备战期后的活跃算入当前名单口径。
|
/// </summary>
|
private int GetDaysUntilNextPrepareStart()
|
{
|
int currentWeekday = TimeUtility.GetCommonWeekday(GetZoneID());
|
int[] prepareWeekdays = cycleConfig != null && cycleConfig.Length > 1 ? cycleConfig[1] : null;
|
if (prepareWeekdays.IsNullOrEmpty())
|
return 1;
|
|
int minDaysUntilPrepare = 7;
|
foreach (int prepareWeekday in prepareWeekdays)
|
{
|
int daysUntilPrepare = prepareWeekday > currentWeekday
|
? prepareWeekday - currentWeekday
|
: prepareWeekday + 7 - currentWeekday;
|
|
if (daysUntilPrepare < minDaysUntilPrepare)
|
minDaysUntilPrepare = daysUntilPrepare;
|
}
|
|
return minDaysUntilPrepare;
|
}
|
|
/// <summary>
|
/// 统计下一次备战日前 prepareDayBefore 天的活跃值。
|
/// offsetFromPrepare 表示目标日期距离备战开始还有几天:1=备战前一天,prepareDayBefore=窗口最早一天。
|
/// dayOffsetFromToday 大于 0 表示目标日期在今天之后,未来活跃按 0,不读取 activityData。
|
/// </summary>
|
private long GetActivityLevelBeforePrepare(int daysUntilPrepare)
|
{
|
long total = 0;
|
int currentWeekday = TimeUtility.GetCommonWeekday(GetZoneID());
|
for (int offsetFromPrepare = prepareDayBefore; offsetFromPrepare >= 1; offsetFromPrepare--)
|
{
|
int dayOffsetFromToday = daysUntilPrepare - offsetFromPrepare;
|
if (dayOffsetFromToday > 0)
|
continue;
|
|
int targetWeekday = ((currentWeekday + dayOffsetFromToday - 1 + 7) % 7) + 1;
|
|
if (activityData.TryGetValue(targetWeekday, out long contribution))
|
total += contribution;
|
}
|
|
return total;
|
}
|
|
|
#endregion
|
|
#region 宝库
|
|
/// <summary>宝库奖励类型ID (对应CA504的RewardType)</summary>
|
public const byte TREASURE_REWARD_TYPE = 11;
|
|
/// <summary>缓存格子开启记录: Key=格子编号(1~n), Value=开启记录</summary>
|
public Dictionary<int, GuildAtkDefBatTreasureRecord> treasureGridRecords = new Dictionary<int, GuildAtkDefBatTreasureRecord>();
|
|
/// <summary>标记宝库窗口是否打开,用于收到A525时自动刷新</summary>
|
public bool isTreasureWinOpen = false;
|
|
/// <summary>等待播放开启动画的格子编号(A504发包时记录)</summary>
|
public int pendingOpenGridIndex = 0;
|
|
/// <summary>宝库数据刷新事件(供窗口监听)</summary>
|
public event Action OnTreasureDataUpdateEvent;
|
|
/// <summary>宝库格子开启动画事件(参数: 格子编号, AwardID)</summary>
|
public event Action<int, int> OnTreasureGridOpenEvent;
|
|
/// <summary>
|
/// 从A525成员数据中解析宝库格子记录
|
/// </summary>
|
public void ParseTreasureGridRecords()
|
{
|
treasureGridRecords.Clear();
|
|
foreach (var mem in MyFamilyMembers.Values)
|
{
|
if (mem.GridNum > 0)
|
{
|
treasureGridRecords[mem.GridNum] = new GuildAtkDefBatTreasureRecord
|
{
|
AwardID = mem.AwardID,
|
PlayerName = UIHelper.ServerStringTrim(mem.Name),
|
};
|
}
|
}
|
|
if (isTreasureWinOpen)
|
{
|
int openGrid = pendingOpenGridIndex;
|
GuildAtkDefBatTreasureRecord treasureRecord;
|
if (openGrid > 0 && treasureGridRecords.TryGetValue(openGrid, out treasureRecord))
|
{
|
pendingOpenGridIndex = 0;
|
OnTreasureGridOpenEvent?.Invoke(openGrid, treasureRecord.AwardID);
|
}
|
OnTreasureDataUpdateEvent?.Invoke();
|
}
|
}
|
|
/// <summary>
|
/// 获取我的剩余开启次数
|
/// 仅在休战期(BatState==1)可开启
|
/// 对手公会未解散: 有参与战斗(BatCnt>0)的玩家可开启1次
|
/// 对手公会已解散: 所有参赛成员均可开启1次
|
/// 每人最多1次
|
/// </summary>
|
public int GetMyOpenRemainCount()
|
{
|
if (MyFamilyInfo == null)
|
return 0;
|
|
// 仅在休战期可开启宝库
|
if (BatState != 1)
|
return 0;
|
|
uint myPlayerId = PlayerDatas.Instance.PlayerId;
|
if (!MyFamilyMembers.TryGetValue(myPlayerId, out var myMem))
|
return 0;
|
|
// 已开启过则不能再次开启
|
if (myMem.GridNum > 0)
|
return 0;
|
|
// 对手公会已解散: 所有参赛成员均可开启
|
if (EnemyFamilyInfo != null && EnemyFamilyInfo.IsDismiss == 1)
|
return 1;
|
|
// 对手公会未解散: 参战成员均可开启1次(已开启的被 GridNum 过滤)
|
return 1;
|
}
|
|
/// <summary>
|
/// 获取我的最大可开启次数(用于显示 开启次数:{0}/{1})
|
/// </summary>
|
public int GetMyMaxOpenCount()
|
{
|
return 1;
|
}
|
|
/// <summary>
|
/// 发送宝库开启请求 A504
|
/// </summary>
|
public void SendOpenTreasure(byte gridNum)
|
{
|
pendingOpenGridIndex = gridNum;
|
var pack = new CA504_tagCMPlayerGetReward();
|
pack.RewardType = TREASURE_REWARD_TYPE;
|
pack.DataEx = gridNum;
|
GameNetSystem.Instance.SendInfo(pack);
|
}
|
|
/// <summary>
|
/// 获取宝库的库类型(1-胜者库, 2-败者库)
|
/// </summary>
|
public int GetTreasureLibType()
|
{
|
int displayState = GetGuildBattleDisplayState();
|
return displayState == 4 ? 1 : 2;
|
}
|
|
#region 宝库奖励延迟展示
|
|
/// <summary>缓存待展示的宝库奖励物品列表</summary>
|
private List<Item> pendingTreasureItems = new List<Item>();
|
/// <summary>缓存待展示的宝库奖励信息文本</summary>
|
private string pendingTreasureInfo = string.Empty;
|
|
/// <summary>
|
/// 缓存宝库奖励数据(由 BoxGetItemModel.ReceiveAwardNotify 调用)
|
/// 延迟到宝库开启动画结束后再展示
|
/// </summary>
|
public void CacheTreasureAward(HA801_tagMCGiveAwardInfo netPack)
|
{
|
pendingTreasureItems.Clear();
|
pendingTreasureInfo = string.Empty;
|
|
// 解析经验
|
if (netPack.Exp != 0 || netPack.ExpPoint != 0)
|
{
|
long expValue = netPack.Exp + netPack.ExpPoint * Constants.ExpPointValue;
|
pendingTreasureItems.Add(new Item(GeneralDefine.expDisplayId, expValue));
|
}
|
|
// 解析货币
|
if (netPack.MoneyList.Length != 0)
|
{
|
for (int i = 0; i < netPack.MoneyLen; i++)
|
{
|
var moneyType = netPack.MoneyList[i].MoneyType;
|
if (GeneralDefine.MoneyDisplayModel.ContainsKey(moneyType) && netPack.MoneyList[i].MoneyValue != 0)
|
{
|
pendingTreasureItems.Add(new Item(GeneralDefine.MoneyDisplayModel[moneyType], netPack.MoneyList[i].MoneyValue, netPack.MoneyList[i].IsBind));
|
}
|
}
|
}
|
|
// 解析物品
|
if (netPack.ItemList.Length != 0)
|
{
|
for (int i = 0; i < netPack.ItemLen; i++)
|
{
|
pendingTreasureItems.Add(new Item(
|
(int)netPack.ItemList[i].ItemID,
|
netPack.ItemList[i].Count + netPack.ItemList[i].CountEx * Constants.ExpPointValue,
|
netPack.ItemList[i].IsBind));
|
}
|
}
|
|
// 奖励说明文本
|
if (LanguageConfig.HasKey("commonShowAwardEvents_AtkDefBatTreasure"))
|
pendingTreasureInfo = Language.Get("commonShowAwardEvents_AtkDefBatTreasure");
|
}
|
|
/// <summary>
|
/// 展示缓存的宝库奖励(由格子开启动画 onComplete 回调调用)
|
/// </summary>
|
public void ShowTreasureAward()
|
{
|
if (pendingTreasureItems.Count == 0)
|
return;
|
|
ItemLogicUtility.Instance.ShowGetItem(pendingTreasureItems, "AtkDefBatTreasure");
|
pendingTreasureItems.Clear();
|
pendingTreasureInfo = string.Empty;
|
}
|
|
#endregion
|
|
#endregion
|
|
#region 扫荡
|
|
/// <summary> 是否为扫荡的胜利结算 </summary>
|
public bool isSweepVictory = false;
|
/// <summary> 扫荡奖励物品列表 </summary>
|
public List<Item> sweepItemInfos = new List<Item>();
|
/// <summary> 扫荡目标玩家ID </summary>
|
public uint sweepTagID = 0;
|
|
/// <summary>接收 0320 副本结束通知(扫荡结果)</summary>
|
public void UpdateFBEnd(H0320_tagFBEnd vNetData)
|
{
|
if (string.IsNullOrEmpty(vNetData?.Msg))
|
return;
|
|
JsonData jsonData;
|
try { jsonData = JsonMapper.ToObject(vNetData.Msg); }
|
catch { return; }
|
|
// 判断是否是公会攻防战的扫荡结果
|
if (!jsonData.ContainsKey("dataMapID") || (int)jsonData["dataMapID"] != DataMapID)
|
return;
|
if (!jsonData.ContainsKey("isSweep") || (int)jsonData["isSweep"] != 1)
|
return;
|
|
isSweepVictory = true;
|
|
// 解析目标玩家ID
|
sweepTagID = jsonData.ContainsKey("tagID") ? (uint)(int)jsonData["tagID"] : 0;
|
|
// 解析奖励物品
|
sweepItemInfos.Clear();
|
if (jsonData.ContainsKey("itemInfo") && jsonData["itemInfo"].IsArray)
|
{
|
foreach (JsonData itemData in jsonData["itemInfo"])
|
{
|
sweepItemInfos.Add(new Item((int)itemData["ItemID"], (long)itemData["Count"]));
|
}
|
}
|
|
UIManager.Instance.OpenWindow<GuildAtkDefBatVictoryWin>();
|
}
|
|
|
#endregion
|
|
#region 奖励预览
|
/// <summary>实时计算最大总格子数</summary>
|
public int CalculateMaxGridCount()
|
{
|
int participantCount = MyFamilyMembers?.Count ?? 0;
|
if (participantCount <= defaultMaxGrid)
|
return defaultMaxGrid;
|
|
// 最大总格子=默认最大格子数+动态扩容格子数*n,n大于等于0
|
// n=向上取整((参与成员-默认最大格子数)/扩容格子数))
|
int n = Mathf.CeilToInt((float)(participantCount - defaultMaxGrid) / expandGridCount);
|
return defaultMaxGrid + expandGridCount * n;
|
}
|
|
/// <summary>计算非普通品质的总格子产出上限</summary>
|
public int GetNonNormalQualityTotalOutput()
|
{
|
int total = 0;
|
if (qualityMaxOutput != null)
|
{
|
foreach (var kv in qualityMaxOutput)
|
{
|
if (kv.Key != 1)
|
total += kv.Value;
|
}
|
}
|
return total;
|
}
|
#endregion
|
}
|
|
public class GuildAtkDefBatTreasureRecord
|
{
|
public int AwardID;
|
public string PlayerName;
|
}
|