lcy
2026-06-12 e7e206de7ea26e0037d60ff0c3e9943950340a18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
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()
    {
        hasClosedMainGuildAtkDefBatTip = false;
        ResetBattleData();
    }
 
    private void OnEnterOrQuitGuildEvent(bool isEnter)
    {
        if (!isEnter)
        {
            ResetBattleData();
            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.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);
    }
 
    private bool hasClosedMainGuildAtkDefBatTip;
 
    public bool ShouldShowMainGuildAtkDefBatTip()
    {
        if (hasClosedMainGuildAtkDefBatTip)
            return false;
        if (!IsOpen())
            return false;
        return BatState == 3 && HasFamilyJoinEligibility() && HasMyJoinEligibility();
    }
 
    public void ClearMainGuildAtkDefBatTip()
    {
        hasClosedMainGuildAtkDefBatTip = true;
    }
 
    /// <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) return 4;
            if (overState == 2) 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.Where(member => member.BatCnt > 0).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)可开启
    /// 参战且已战斗成员可开启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.BatCnt <= 0)
            return 0;
 
        // 已开启过则不能再次开启
        if (myMem.GridNum > 0)
            return 0;
 
        // 参战且已战斗成员可开启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;
}