lcy
2026-06-23 fe2805709cbd8cee561b475eda53674fbf06ef9e
662 武将试炼

# Conflicts:
# Main/Config/ConfigManager.cs
# Main/Manager/UIManager.UIAtlasPreloadMap.Generated.cs
# Main/System/WarlordPavilion/TowerBaseWin.cs
16个文件已修改
31个文件已添加
2615 ■■■■■ 已修改文件
Main/Config/ConfigManager.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Config/Configs/HeroTrialConfig.cs 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Config/Configs/HeroTrialConfig.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Config/PartialConfigs/HeroTrialConfig.cs 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Config/PartialConfigs/HeroTrialConfig.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/DataToCtl/PackageRegedit.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Main.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleConst.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/HeroTrialBattleField.cs 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleField/HeroTrialBattleField.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Battle/BattleFieldFactory.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/ArenaTabHandler.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/BaseChallengeTabHandler.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/BoneFieldTabHandler.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/ChallengeTabButton.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/HeroTrialTabHandler.cs 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/HeroTrialTabHandler.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/QunyingTabHandler.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/TianziBillboradTabHandler.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/ChallengeTab/WarlordPavilionTabHandler.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial.meta 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/CarouselView.cs 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/CarouselView.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialBattleWin.cs 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialBattleWin.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialCarouselView.cs 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialCarouselView.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialCell.cs 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialCell.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialFailWin.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialFailWin.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialHeroItem.cs 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialHeroItem.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialManager.cs 811 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialManager.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialVictoryWin.cs 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialVictoryWin.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialWin.cs 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/HeroTrial/HeroTrialWin.cs.meta 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Redpoint/MainRedDot.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/Settlement/BattleSettlementManager.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/System/WarlordPavilion/TowerBaseWin.cs 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Utility/EnumHelper.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Main/Config/ConfigManager.cs
@@ -90,6 +90,8 @@
            typeof(HeroLineupHaloConfig),
            typeof(HeroQualityLVConfig),
            typeof(HeroSkinAttrConfig),
            typeof(HeroTalentConfig),
            typeof(HeroTrialConfig),
            typeof(HorseClassConfig),
            typeof(HorseIDConfig),
            typeof(HorseSkinConfig),
@@ -384,6 +386,10 @@
        ClearConfigDictionary<HeroQualityLVConfig>();
        // 清空 HeroSkinAttrConfig 字典
        ClearConfigDictionary<HeroSkinAttrConfig>();
        // 清空 HeroTalentConfig 字典
        ClearConfigDictionary<HeroTalentConfig>();
        // 清空 HeroTrialConfig 字典
        ClearConfigDictionary<HeroTrialConfig>();
        // 清空 HorseClassConfig 字典
        ClearConfigDictionary<HorseClassConfig>();
        // 清空 HorseIDConfig 字典
Main/Config/Configs/HeroTrialConfig.cs
New file
@@ -0,0 +1,80 @@
//--------------------------------------------------------
//    [Author]:           YYL
//    [  Date ]:           Tuesday, June 9, 2026
//--------------------------------------------------------
using System.Collections.Generic;
using System;
using UnityEngine;
using LitJson;
public partial class HeroTrialConfig : ConfigBase<int, HeroTrialConfig>
{
    static HeroTrialConfig()
    {
        // 访问过静态构造函数
        visit = true;
    }
    public int CfgID;
    public int HeroID;
    public int LevelNum;
    public int LVLimit;
    public int RealmLimit;
    public int[][] PassAwardList;
    public int[] LineupIDList;
    public int NPCLV;
    public float Difficulty;
    public long FightPower;
    public int SortOrder;
    public override int LoadKey(string _key)
    {
        int key = GetKey(_key);
        return key;
    }
    public override void LoadConfig(string input)
    {
        try {
        string[] tables = input.Split('\t');
        int.TryParse(tables[0],out CfgID);
            int.TryParse(tables[1],out HeroID);
            int.TryParse(tables[2],out LevelNum);
            int.TryParse(tables[3],out LVLimit);
            int.TryParse(tables[4],out RealmLimit);
            PassAwardList = JsonMapper.ToObject<int[][]>(tables[5].Replace("(", "[").Replace(")", "]"));
            if (tables[6].Contains("["))
            {
                LineupIDList = JsonMapper.ToObject<int[]>(tables[6]);
            }
            else
            {
                string[] LineupIDListStringArray = tables[6].Trim().Split(StringUtility.splitSeparator,StringSplitOptions.RemoveEmptyEntries);
                LineupIDList = new int[LineupIDListStringArray.Length];
                for (int i=0;i<LineupIDListStringArray.Length;i++)
                {
                     int.TryParse(LineupIDListStringArray[i],out LineupIDList[i]);
                }
            }
            int.TryParse(tables[7],out NPCLV);
            float.TryParse(tables[8],out Difficulty);
            long.TryParse(tables[9],out FightPower);
            int.TryParse(tables[10],out SortOrder);
        }
        catch (Exception exception)
        {
            Debug.LogError(exception);
        }
    }
}
Main/Config/Configs/HeroTrialConfig.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 86da543a944b1b94c8cfd3581f709bd8
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Config/PartialConfigs/HeroTrialConfig.cs
New file
@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Linq;
public partial class HeroTrialConfig : ConfigBase<int, HeroTrialConfig>
{
    //<武将ID,<关卡编号,配置ID>>
    static Dictionary<int, Dictionary<int, int>> idDict = new Dictionary<int, Dictionary<int, int>>();
    protected override void OnConfigParseCompleted()
    {
        if (!idDict.ContainsKey(HeroID))
        {
            idDict[HeroID] = new Dictionary<int, int>();
        }
        idDict[HeroID][LevelNum] = CfgID;
    }
    public static bool TryGetHeroLevelDict(int heroID, out Dictionary<int, int> dict)
    {
        dict = null;
        return idDict != null && idDict.TryGetValue(heroID, out dict);
    }
    public static bool TryGetConfigID(int heroID, int levelNum, out int cfgID)
    {
        cfgID = 0;
        return idDict != null && idDict.TryGetValue(heroID, out var dict) && dict.TryGetValue(levelNum, out cfgID);
    }
    public static bool TryGetHeroTrialConfig(int heroID, int levelNum, out HeroTrialConfig config)
    {
        config = null;
        if (TryGetConfigID(heroID, levelNum, out int cfgID) && HasKey(cfgID))
        {
            config = Get(cfgID);
            return true;
        }
        return false;
    }
    public static bool TryGetHeroTrialConfig(int cfgID, out HeroTrialConfig config)
    {
        config = null;
        if (HasKey(cfgID))
        {
            config = Get(cfgID);
            return true;
        }
        return false;
    }
    public static List<int> GetSortedHeroIDList()
    {
        if (idDict.IsNullOrEmpty())
            return new List<int>();
        return idDict.Keys
            .OrderBy(GetHeroSortOrder)
            .ThenBy(heroID => heroID)
            .ToList();
    }
    public static bool TryGetHeroConfigList(int heroID, out List<HeroTrialConfig> configs)
    {
        configs = null;
        if (!TryGetHeroLevelDict(heroID, out var dict) || dict.IsNullOrEmpty())
            return false;
        configs = dict.Keys
            .OrderBy(levelNum => levelNum)
            .Select(levelNum => TryGetHeroTrialConfig(heroID, levelNum, out var config) ? config : null)
            .Where(config => config != null)
            .ToList();
        return !configs.IsNullOrEmpty();
    }
    public static bool TryGetFirstLevelConfig(int heroID, out HeroTrialConfig config)
    {
        config = null;
        if (!TryGetHeroLevelDict(heroID, out var dict) || dict.IsNullOrEmpty())
            return false;
        int levelNum = dict.Keys.Min();
        return TryGetHeroTrialConfig(heroID, levelNum, out config);
    }
    public static bool TryGetMaxLevelNum(int heroID, out int maxLevelNum)
    {
        maxLevelNum = 0;
        if (!TryGetHeroLevelDict(heroID, out var dict) || dict.IsNullOrEmpty())
            return false;
        maxLevelNum = dict.Keys.Max();
        return true;
    }
    public static bool TryGetNextLevelConfig(int heroID, int passLevelNum, out HeroTrialConfig config)
    {
        config = null;
        int nextLevelNum = passLevelNum + 1;
        return TryGetHeroTrialConfig(heroID, nextLevelNum, out config);
    }
    static int GetHeroSortOrder(int heroID)
    {
        return TryGetFirstLevelConfig(heroID, out var config) ? config.SortOrder : int.MaxValue;
    }
}
Main/Config/PartialConfigs/HeroTrialConfig.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b0e016a7a404a9fa2d61f7327a1fb3f
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs
New file
@@ -0,0 +1,12 @@
using UnityEngine;
using System.Collections;
// B2 03 武将试炼信息 #tagSCHeroTrialInfo
public class DTCB203_tagSCHeroTrialInfo : DtcBasic {
    public override void Done(GameNetPackBasic vNetPack) {
        base.Done(vNetPack);
        HB203_tagSCHeroTrialInfo vNetData = vNetPack as HB203_tagSCHeroTrialInfo;
        HeroTrialManager.Instance.UpdateHeroTrialInfo(vNetData);
    }
}
Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b6aa926e39b4ec4aa2d86f794c9ed8a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Core/NetworkPackage/DataToCtl/PackageRegedit.cs
@@ -141,6 +141,7 @@
        Register(typeof(HB040_tagSCTravelInfo), typeof(DTCB040_tagSCTravelInfo));
        Register(typeof(HA3C7_tagMCGubaoInfo), typeof(DTCA3C7_tagMCGubaoInfo));
        Register(typeof(HB202_tagSCDingjungeInfo), typeof(DTCB202_tagSCDingjungeInfo));
        Register(typeof(HB203_tagSCHeroTrialInfo), typeof(DTCB203_tagSCHeroTrialInfo));
        Register(typeof(HB432_tagSCViewNPCAttrRet), typeof(DTCB432_tagSCViewNPCAttrRet));
        Register(typeof(HA503_tagSCFamilyTaofaInfo), typeof(DTCA503_tagSCFamilyTaofaInfo));
        Register(typeof(HA504_tagSCFamilyTaofaAtkRet), typeof(DTCA504_tagSCFamilyTaofaAtkRet));
Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs
New file
@@ -0,0 +1,29 @@
using UnityEngine;
using System.Collections;
// B2 03 武将试炼信息 #tagSCHeroTrialInfo
public class HB203_tagSCHeroTrialInfo : GameNetPackBasic {
    public byte Count;
    public  tagSCHeroTrial[] HeroTrialList;
    public HB203_tagSCHeroTrialInfo () {
        _cmd = (ushort)0xB203;
    }
    public override void ReadFromBytes (byte[] vBytes) {
        TransBytes (out Count, vBytes, NetDataType.BYTE);
        HeroTrialList = new tagSCHeroTrial[Count];
        for (int i = 0; i < Count; i ++) {
            HeroTrialList[i] = new tagSCHeroTrial();
            TransBytes (out HeroTrialList[i].HeroID, vBytes, NetDataType.DWORD);
            TransBytes (out HeroTrialList[i].PassLevelNum, vBytes, NetDataType.BYTE);
        }
    }
    public class tagSCHeroTrial {
        public uint HeroID;        // 武将ID
        public byte PassLevelNum;        // 本武将已过关到的关卡编号
    }
}
Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c0adf674df224db49ab40b4853819da8
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/Main.cs
@@ -125,6 +125,7 @@
        managers.Add(SuperVipManager.Instance);
        managers.Add(HeroSkinFlashSaleManager.Instance);
        managers.Add(GuildAtkDefBatManager.Instance);
        managers.Add(HeroTrialManager.Instance);
        foreach (var manager in managers)
        {
Main/System/Battle/BattleConst.cs
@@ -13,6 +13,7 @@
        typeof(BoneFieldBattleWin),
        typeof(TianziBillboradBattleWin),
        typeof(WarlordPavilionBattleWin),
        typeof(HeroTrialBattleWin),
        typeof(PreviewBattleWin),
        typeof(QYBattleWin),
        typeof(QYBattleWin),
@@ -26,6 +27,7 @@
    public const string BoneBattleField = "BoneBattleField";
    public const string TianziBillboradBattleField = "TianziBillboradBattleField";
    public const string WarlordPavilionBattleField = "WarlordPavilionBattleField";
    public const string HeroTrialBattleField = "HeroTrialBattleField";
    public const string PriviewBattleField = "PriviewBattleField";  //预览战斗
    public const string QYBattleField = "QYBattleField";
    public const string GuildAtkDefBatField = "GuildAtkDefBatField";
@@ -38,6 +40,7 @@
        { BoneBattleField, "BoneFieldBattleWin" },
        { TianziBillboradBattleField, "TianziBillboradBattleWin" },
        { WarlordPavilionBattleField, "WarlordPavilionBattleWin" },
        { HeroTrialBattleField, "HeroTrialBattleWin" },
        { PriviewBattleField, "PreviewBattleWin" },
        { QYBattleField, "QYBattleWin" },
        { GuildAtkDefBatField, "GuildAtkDefBatBattleWin" },
@@ -54,6 +57,7 @@
        { PriviewBattleField, 6 },
        { QYBattleField, 7 },
        { GuildAtkDefBatField, 8 },
        { HeroTrialBattleField, 9 },
    };
    //和 CreateBattleField 里的对应
@@ -65,6 +69,7 @@
        {30010, BoneBattleField},
        {30020, TianziBillboradBattleField},
        {30030, WarlordPavilionBattleField},
        {30040, HeroTrialBattleField},
        {30000, PriviewBattleField},
        {32000, QYBattleField},
        {33000, GuildAtkDefBatField},
Main/System/Battle/BattleField/HeroTrialBattleField.cs
New file
@@ -0,0 +1,130 @@
using Cysharp.Threading.Tasks;
using LitJson;
using System.Collections.Generic;
using System.Linq;
public class HeroTrialBattleField : BattleField
{
    HeroTrialConfig trialConfig;
    public HeroTrialBattleField(string _guid) : base(_guid)
    {
    }
    public override async UniTask Init(int MapID, int FuncLineID, JsonData _extendData,
        List<TeamBase> _redTeamList, List<TeamBase> _blueTeamList, byte turnMax)
    {
        await base.Init(MapID, FuncLineID, _extendData, _redTeamList, _blueTeamList, turnMax);
        extendData = _extendData;
        HeroTrialManager.Instance.TryGetConfigByFuncLineID(FuncLineID, out trialConfig);
        SetBattleMode(BattleMode.Record);
    }
    public override void AutoSetBattleMode()
    {
        SetBattleMode(BattleMode.Record);
    }
    public override void TurnFightState(int TurnNum, int State,
        uint FuncLineID, JsonData extendData)
    {
        base.TurnFightState(TurnNum, State, FuncLineID, extendData);
        switch (State)
        {
            case 0:
                break;
            case 1:
                break;
            case 2:
                break;
            case 3:
                break;
            case 4:
                break;
            case 5:
                break;
            default:
                BattleDebug.LogError("recieve a unknown State");
                break;
        }
    }
    protected override void OnSettlement(JsonData turnFightStateData)
    {
        base.OnSettlement(turnFightStateData);
    }
    public override void WhaleFall()
    {
        AutoFightModel.Instance.isPause = false;
        Destroy();
        if (UIManager.Instance.IsOpened<HeroTrialBattleWin>())
        {
            UIManager.Instance.CloseWindow<HeroTrialBattleWin>();
            UIManager.Instance.OpenWindowAsync<TowerBaseWin>(1).Forget();
        }
    }
    public override void Run()
    {
        if (operationAgent == null)
        {
            return;
        }
        base.Run();
    }
    public override void DistributeNextPackage()
    {
        if (IsBattleFinish)
            return;
        BattleManager.Instance.DistributeNextReportPackage(guid);
    }
    public override async void ShowWindow(HB424_tagSCTurnFightInit vNetData)
    {
        HeroTrialBattleWin battleWin = UIManager.Instance.GetUI<HeroTrialBattleWin>();
        if (battleWin == null)
        {
            battleWin = await UIManager.Instance.OpenWindowAsync<HeroTrialBattleWin>();
        }
        battleWin.SetBattleField(this);
        if (UIManager.Instance.IsOpened<TowerBaseWin>())
        {
            UIManager.Instance.CloseWindow<TowerBaseWin>();
        }
    }
    public NPCLineupConfig GetBossLineupConfig()
    {
        if (trialConfig == null && !HeroTrialManager.Instance.TryGetConfigByFuncLineID(FuncLineID, out trialConfig))
            return null;
        int[] lineupIDList = trialConfig.LineupIDList;
        if (lineupIDList.IsNullOrEmpty())
            return null;
        int lineupID = lineupIDList[0];
        if (!NPCLineupConfig.HasKey(lineupID))
            return null;
        return NPCLineupConfig.Get(lineupID);
    }
    public override BattleObject FindBoss()
    {
        var config = GetBossLineupConfig();
        if (config != null)
        {
            int bossId = config.BossID;
            return battleObjMgr.allBattleObjDict.Values.FirstOrDefault(bo => bo.GetNPCID() == bossId);
        }
        return null;
    }
}
Main/System/Battle/BattleField/HeroTrialBattleField.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51d9ddba2fa6f604497431ded91e9081
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Battle/BattleFieldFactory.cs
@@ -32,6 +32,9 @@
            case 30030:
                battleField = new WarlordPavilionBattleField(guid);
                break;
            case 30040:
                battleField = new HeroTrialBattleField(guid);
                break;
            case 30000:
                battleField = new PriviewBattleField(guid);
                break;
Main/System/ChallengeTab/ArenaTabHandler.cs
@@ -3,7 +3,8 @@
public class ArenaTabHandler : BaseChallengeTabHandler
{
    protected override int GetIndex() => 1;
    protected override string GetIconKey() => "ChallengeTab1";
    protected override string GetNameText() => Language.Get("ChallengeTab1");
    protected override int GetOpenState() => 0; // 0=FuncID
    protected override int GetFuncId() => ArenaManager.Instance.funcId;
    protected override int GetRedpointId() => MainRedDot.ArenaRepoint;
Main/System/ChallengeTab/BaseChallengeTabHandler.cs
@@ -17,7 +17,8 @@
        // 初始化一次 DisplayData,之后只修改变化的字段
        displayData = new ChallengeTabButton.DisplayData
        {
            Index = GetIndex(),
            IconKey = GetIconKey(),
            NameText = GetNameText(),
            RedpointId = GetRedpointId(),
            OpenState = GetOpenState(),
            FuncId = GetFuncId(),
@@ -67,9 +68,14 @@
    }
    /// <summary>
    /// 获取Tab的索引(用于Icon和Name)
    /// 获取Tab图标Key
    /// </summary>
    protected abstract int GetIndex();
    protected abstract string GetIconKey();
    /// <summary>
    /// 获取Tab显示名称
    /// </summary>
    protected abstract string GetNameText();
    /// <summary>
    /// 获取开启方式 (0=FuncID, 1=活动)
Main/System/ChallengeTab/BoneFieldTabHandler.cs
@@ -3,7 +3,8 @@
public class BoneFieldTabHandler : BaseChallengeTabHandler
{
    protected override int GetIndex() => 2;
    protected override string GetIconKey() => "ChallengeTab2";
    protected override string GetNameText() => Language.Get("ChallengeTab2");
    protected override int GetOpenState() => 0; // 0=FuncID
    protected override int GetFuncId() => (int)FuncOpenEnum.BoneBattle;
    protected override int GetRedpointId() => MainRedDot.BoneFieldRepoint;
Main/System/ChallengeTab/ChallengeTabButton.cs
@@ -16,7 +16,8 @@
    public struct DisplayData
    {
        public int Index;
        public string IconKey;
        public string NameText;
        public int RedpointId;
        public int OpenState;//0 FuncID 1 活动
        public int FuncId;
@@ -68,9 +69,8 @@
        txtLockInfo.SetActive(!isOpen);
        // 设置图标和名称
        string spriteAndLangKey = StringUtility.Concat("ChallengeTab", data.Index.ToString());
        imgIcon.SetSprite(spriteAndLangKey);
        txtName.text = Language.Get(spriteAndLangKey);
        imgIcon.SetSprite(data.IconKey);
        txtName.text = data.NameText;
        // 设置TIPS文本
        txtCount.text = data.CountInfo;
Main/System/ChallengeTab/HeroTrialTabHandler.cs
New file
@@ -0,0 +1,57 @@
using System;
using UnityEngine;
using Cysharp.Threading.Tasks;
public class HeroTrialTabHandler : BaseChallengeTabHandler
{
    protected override string GetIconKey() => "ChallengeTab7";
    protected override string GetNameText() => Language.Get("ChallengeTab7");
    protected override int GetOpenState() => 0; // 0=FuncID
    protected override int GetFuncId() => (int)FuncOpenEnum.HeroTrial;
    protected override int GetRedpointId() => MainRedDot.HeroTrialRepoint;
    protected override string GetCountInfo()
    {
        return HeroTrialManager.Instance.HasAnyChallengeableLevel() ? Language.Get("HeroTrial06") : string.Empty;
    }
    protected override Action GetOnClickAction()
    {
        return HandleHeroTrialNavigation;
    }
    private async void HandleHeroTrialNavigation()
    {
        if (!FuncOpen.Instance.IsFuncOpen(GetFuncId(), true))
            return;
        UIManager.Instance.CloseWindow<ChallengeTabWin>();
        BattleField battleField = BattleManager.Instance.GetBattleFieldByMapID(HeroTrialManager.Instance.DataMapID);
        if (battleField != null)
        {
            HeroTrialBattleWin battleWin;
            if (!UIManager.Instance.IsOpened<HeroTrialBattleWin>())
            {
                battleWin = await UIManager.Instance.OpenWindowAsync<HeroTrialBattleWin>();
            }
            else
            {
                battleWin = UIManager.Instance.GetUI<HeroTrialBattleWin>();
            }
            battleWin.SetBattleField(battleField as HeroTrialBattleField);
        }
        else
        {
            UIManager.Instance.OpenWindowAsync<TowerBaseWin>(1).Forget();
        }
    }
    protected override void SubscribeToSpecificEvents()
    {
        HeroTrialManager.Instance.OnUpdateHeroTrialInfoEvent += Refresh;
    }
    protected override void UnsubscribeFromSpecificEvents()
    {
        HeroTrialManager.Instance.OnUpdateHeroTrialInfoEvent -= Refresh;
    }
}
Main/System/ChallengeTab/HeroTrialTabHandler.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c410f4b7a26f00640b6cf94a2817d3c1
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/ChallengeTab/QunyingTabHandler.cs
@@ -3,7 +3,8 @@
public class QunyingTabHandler : BaseChallengeTabHandler
{
    protected override int GetIndex() => 5;
    protected override string GetIconKey() => "ChallengeTab5";
    protected override string GetNameText() => Language.Get("ChallengeTab5");
    protected override int GetOpenState() => 0; // 0=FuncID
    protected override int GetFuncId() => (int)FuncOpenEnum.Qunying;
    protected override int GetRedpointId() => MainRedDot.Qunying;
Main/System/ChallengeTab/TianziBillboradTabHandler.cs
@@ -3,7 +3,8 @@
public class TianziBillboradTabHandler : BaseChallengeTabHandler
{
    protected override int GetIndex() => 3;
    protected override string GetIconKey() => "ChallengeTab3";
    protected override string GetNameText() => Language.Get("ChallengeTab3");
    protected override int GetOpenState() => 0; // 0=FuncID
    protected override int GetFuncId() => TianziBillboradManager.Instance.funcId;
    protected override int GetRedpointId() => MainRedDot.TianziBillboradRepoint;
Main/System/ChallengeTab/WarlordPavilionTabHandler.cs
@@ -3,7 +3,8 @@
public class WarlordPavilionTabHandler : BaseChallengeTabHandler
{
    protected override int GetIndex() => 4;
    protected override string GetIconKey() => "ChallengeTab4";
    protected override string GetNameText() => Language.Get("ChallengeTab4");
    protected override int GetOpenState() => 0; // 0=FuncID
    protected override int GetFuncId() => (int)FuncOpenEnum.WarlordPavilion;
    protected override int GetRedpointId() => MainRedDot.WarlordPavilionRepoint;
Main/System/HeroTrial.meta
New file
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4fe029ba0b48b2646908738defb90bca
folderAsset: yes
DefaultImporter:
  externalObjects: {}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/CarouselView.cs
New file
@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public interface ICarouselItem
{
    RectTransform RectTransform { get; }
    void SetSelected(bool selected);
}
public abstract class CarouselView<TItem, TData> : MonoBehaviour, IDragHandler, IEndDragHandler, IBeginDragHandler
    where TItem : MonoBehaviour, ICarouselItem
{
    public event Action OnSelectedIndexChanged;
    readonly List<TItem> items = new List<TItem>();
    List<TData> dataList;
    float currentCenterOffset;
    float targetCenterOffset;
    bool isDragging;
    int currentSelectedIndex;
    protected abstract RectTransform Content { get; }
    protected abstract GameObject ItemPrefab { get; }
    protected abstract float ItemSpacing { get; }
    protected abstract float ScaleMax { get; }
    protected abstract float ScaleMin { get; }
    protected virtual int PoolSize => 7;
    protected int CurrentSelectedIndex => currentSelectedIndex;
    protected IReadOnlyList<TData> DataList => dataList;
    public void InitPool()
    {
        if (items.Count > 0)
            return;
        for (int i = 0; i < PoolSize; i++)
        {
            GameObject go = Instantiate(ItemPrefab, Content);
            TItem item = go.GetComponent<TItem>();
            if (item == null)
                continue;
            items.Add(item);
        }
    }
    public void InitWithIndex(int index)
    {
        if (dataList.IsNullOrEmpty())
            return;
        index = Mathf.Clamp(index, 0, dataList.Count - 1);
        currentSelectedIndex = index;
        currentCenterOffset = index;
        targetCenterOffset = index;
        UpdateItems();
    }
    public void SelectIndex(int index)
    {
        if (dataList.IsNullOrEmpty())
            return;
        index = GetRealIndex(index);
        int oldIndex = currentSelectedIndex;
        currentSelectedIndex = index;
        targetCenterOffset = index;
        currentCenterOffset = index;
        UpdateItems();
        if (oldIndex != currentSelectedIndex)
            OnSelectedIndexChanged?.Invoke();
    }
    public void SelectPrevious()
    {
        if (dataList.IsNullOrEmpty())
            return;
        int newIndex = (currentSelectedIndex - 1 + dataList.Count) % dataList.Count;
        SelectIndex(newIndex);
    }
    public void SelectNext()
    {
        if (dataList.IsNullOrEmpty())
            return;
        int newIndex = (currentSelectedIndex + 1) % dataList.Count;
        SelectIndex(newIndex);
    }
    public void RefreshAllItems()
    {
        if (!dataList.IsNullOrEmpty())
            UpdateItems();
    }
    protected void SetDataList(List<TData> newDataList, int selectedIndex)
    {
        dataList = newDataList;
        if (dataList.IsNullOrEmpty())
        {
            HideAllItems();
            return;
        }
        InitWithIndex(selectedIndex);
    }
    protected bool TryGetCurrentData(out TData data)
    {
        data = default(TData);
        if (dataList.IsNullOrEmpty() || currentSelectedIndex < 0)
            return false;
        int realIndex = GetRealIndex(currentSelectedIndex);
        data = dataList[realIndex];
        return true;
    }
    protected void HideAllItems()
    {
        for (int i = 0; i < items.Count; i++)
        {
            if (items[i] != null)
                items[i].gameObject.SetActive(false);
        }
    }
    protected virtual void OnDestroy()
    {
        items.Clear();
    }
    protected virtual void Update()
    {
        if (!isDragging && Mathf.Abs(currentCenterOffset - targetCenterOffset) > 0.001f)
        {
            currentCenterOffset = Mathf.Lerp(currentCenterOffset, targetCenterOffset, Time.deltaTime * 10f);
            if (Mathf.Abs(currentCenterOffset - targetCenterOffset) <= 0.001f)
                currentCenterOffset = targetCenterOffset;
            UpdateItems();
        }
    }
    void UpdateItems()
    {
        if (dataList.IsNullOrEmpty())
            return;
        int poolSize = items.Count;
        int startOffset = Mathf.FloorToInt(currentCenterOffset) - (poolSize / 2);
        int selectedLogicIndex = Mathf.RoundToInt(targetCenterOffset);
        for (int i = 0; i < poolSize; i++)
        {
            int logicIndex = startOffset + i;
            int realIndex = GetRealIndex(logicIndex);
            float distanceToCenter = logicIndex - currentCenterOffset;
            float posX = distanceToCenter * ItemSpacing;
            TItem itemUI = items[i];
            if (itemUI == null || itemUI.RectTransform == null)
                continue;
            itemUI.gameObject.SetActive(true);
            itemUI.RectTransform.anchoredPosition = new Vector2(posX, 0);
            float absDist = Mathf.Abs(distanceToCenter);
            float scaleProgress = Mathf.Clamp01(1f - absDist);
            float currentScale = Mathf.Lerp(ScaleMin, ScaleMax, scaleProgress);
            itemUI.RectTransform.localScale = Vector3.one * currentScale;
            bool isSelected = logicIndex == selectedLogicIndex;
            RefreshItem(itemUI, dataList[realIndex], realIndex, isSelected);
            itemUI.SetSelected(isSelected);
        }
    }
    int GetRealIndex(int logicIndex)
    {
        return (logicIndex % dataList.Count + dataList.Count) % dataList.Count;
    }
    protected abstract void RefreshItem(TItem item, TData data, int realIndex, bool isSelected);
    public void OnBeginDrag(PointerEventData eventData)
    {
        isDragging = true;
    }
    public void OnDrag(PointerEventData eventData)
    {
        if (dataList.IsNullOrEmpty())
            return;
        float dragDeltaX = eventData.delta.x;
        currentCenterOffset -= dragDeltaX / ItemSpacing;
        UpdateItems();
    }
    public void OnEndDrag(PointerEventData eventData)
    {
        if (dataList.IsNullOrEmpty())
            return;
        isDragging = false;
        float speedX = eventData.delta.x;
        if (speedX > 5f)
        {
            targetCenterOffset = Mathf.Floor(currentCenterOffset);
        }
        else if (speedX < -5f)
        {
            targetCenterOffset = Mathf.Ceil(currentCenterOffset);
        }
        else
        {
            targetCenterOffset = Mathf.Round(currentCenterOffset);
        }
        int oldIndex = currentSelectedIndex;
        currentSelectedIndex = GetRealIndex(Mathf.RoundToInt(targetCenterOffset));
        UpdateItems();
        if (oldIndex != currentSelectedIndex)
            OnSelectedIndexChanged?.Invoke();
    }
}
Main/System/HeroTrial/CarouselView.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 552093c27b834d40ae2245fc840daa24
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialBattleWin.cs
New file
@@ -0,0 +1,292 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class HeroTrialBattleWin : BaseBattleWin
{
    [SerializeField] Transform transButtons;
    [SerializeField] BossLifeBar bossLifeBar;
    [SerializeField] SkillWordCell[] skillWordCells;
    [SerializeField] BossHeadCell bossHeadCell;
    [SerializeField] Text txtBossName;
    [SerializeField] List<BattleBuffCell> buffCells;
    [SerializeField] HeroCountryComponent myCountry;
    [SerializeField] HeroCountryComponent enemyCountry;
    [SerializeField] ButtonEx buffInfoButton;
    private BattleObject bossBattleObject = null;
    protected override void OnPreOpen()
    {
        base.OnPreOpen();
        MainWin.TabChangeEvent += OnTabChangeEvent;
        UIManager.Instance.OnOpenWindow += OnOpenWindow;
        bool isOpenBattleChangeTab = IsOpenBattleChangeTab();
        transButtons.localPosition = new Vector3(0, isOpenBattleChangeTab ? 130 : 0, 0);
        if (isOpenBattleChangeTab)
        {
            UIManager.Instance.GetUI<MainWin>()?.CloseSubUI();
        }
        else
        {
            UIManager.Instance.CloseWindow<MainWin>();
        }
        isClickSkip = false;
    }
    protected override void OnPreClose()
    {
        base.OnPreClose();
        MainWin.TabChangeEvent -= OnTabChangeEvent;
        UIManager.Instance.OnOpenWindow -= OnOpenWindow;
        bool isOpenBattleChangeTab = IsOpenBattleChangeTab();
        if (isOpenBattleChangeTab)
        {
            UIManager.Instance.GetUI<MainWin>()?.RestoreSubUI();
        }
        else
        {
            UIManager.Instance.OpenWindowAsync<MainWin>().Forget();
        }
        if (bossBattleObject != null)
        {
            var buffMgr = bossBattleObject.GetBuffMgr();
            if (buffMgr != null)
            {
                buffMgr.onBuffChanged -= OnBuffChanged;
            }
            bossBattleObject = null;
        }
        if (isClickSkip)
        {
            isClickSkip = false;
            TryPass();
        }
    }
    void OnOpenWindow(UIBase win)
    {
        if (win is HeroTrialVictoryWin || win is HeroTrialFailWin)
        {
            isClickSkip = false;
        }
    }
    bool isClickSkip = false;
    protected override void OnClickPass()
    {
        if (!IsPass())
            return;
        isClickSkip = true;
        clickTime = Time.time;  // 记录点击时间
        battleField.ForceFinish();
    }
    float stayTime = 2f;
    float clickTime = 0f;
    void LateUpdate()
    {
        if (isClickSkip && Time.time - clickTime >= stayTime)
        {
            isClickSkip = false;
            TryPass();
        }
    }
    private void TryPass()
    {
        if (UIManager.Instance.IsOpened<HeroTrialVictoryWin>() ||
            UIManager.Instance.IsOpened<HeroTrialFailWin>())
            return;
        CloseWindow();
        Debug.LogError($"OnBattleEnd 异常关闭");
        BattleSettlementManager.Instance.WinShowOver(BattleConst.HeroTrialBattleField);
    }
    private void OnTabChangeEvent()
    {
        UIManager.Instance.CloseWindow<HeroTrialBattleWin>();
    }
    protected override void OnCreateBattleField(string guid, BattleField field)
    {
        if (field is HeroTrialBattleField)
        {
            SetBattleField(field);
        }
    }
    protected override void RefreshSpecific()
    {
        HeroTrialBattleField heroTrialField = battleField as HeroTrialBattleField;
        if (heroTrialField == null) return;
        NPCLineupConfig lineupConfig = heroTrialField.GetBossLineupConfig();
        if (bossBattleObject != null)
        {
            var buffMgr = bossBattleObject.GetBuffMgr();
            if (buffMgr != null)
            {
                buffMgr.onBuffChanged -= OnBuffChanged;
            }
            bossBattleObject = null;
        }
        bossBattleObject = heroTrialField.FindBoss();
        DisplaySkillWordsList(lineupConfig);
        if (null != bossBattleObject && bossBattleObject is HeroBattleObject heroBattleObject)
        {
            TeamHero teamHero = heroBattleObject.teamHero;
            bossHeadCell.SetTeamHero(teamHero);
            txtBossName.text = teamHero.name;
            NPCConfig npcConfig = NPCConfig.Get(teamHero.NPCID);
            bossLifeBar.SetBaseInfo(Mathf.Max(1, npcConfig.LifeBarCount), (ulong)teamHero.curHp, (ulong)teamHero.maxHp);
            var buffMgr = bossBattleObject.GetBuffMgr();
            if (buffMgr != null)
            {
                buffMgr.onBuffChanged -= OnBuffChanged;
                buffMgr.onBuffChanged += OnBuffChanged;
            }
        }
        else
        {
            bossHeadCell.SetTeamHero(null);
            txtBossName.text = string.Empty;
            bossLifeBar.SetBaseInfo(2, 2, 2);
            Debug.LogError("找不到boss");
        }
        OnRoundChange(battleField.round, battleField.turnMax); // 确保回合显示被调用
        OnBuffChanged();
        // 获取我方(红方)队伍数据
        List<BattleObject> myTeam = battleField.battleObjMgr.GetBattleObjList(BattleCamp.Red);
        // 获取敌方(蓝方)队伍数据
        List<BattleObject> enemyTeam = battleField.battleObjMgr.GetBattleObjList(BattleCamp.Blue);
        myCountry.RefreshOnTeamCountry(GetTeamHeroList(myTeam), true);
        enemyCountry.RefreshOnTeamCountry(GetTeamHeroList(enemyTeam), true);
    }
    private void OnBuffChanged()
    {
        var buffList = new List<HB428_tagSCBuffRefresh>();
        if (null != bossBattleObject)
        {
            var buffMgr = bossBattleObject.GetBuffMgr();
            buffList = buffMgr != null ? buffMgr.GetBuffIconList() : new List<HB428_tagSCBuffRefresh>();
        }
        RefreshBuff(buffList);
    }
    private void RefreshHP()
    {
        if (null != bossBattleObject && bossBattleObject is HeroBattleObject heroBattleObject)
        {
            TeamHero teamHero = heroBattleObject.teamHero;
            bossLifeBar.Show((ulong)teamHero.curHp, (ulong)teamHero.maxHp);
        }
    }
    protected override void OnDamageTaken(BattleDmgInfo info)
    {
        // Debug.LogError("OnDamageTaken 被调用 调用者是 " + info.battleHurtParam.caster.casterObj?.teamHero.name + " 对象 " + info.battleHurtParam.hurter.hurtObj?.teamHero.name);
        base.OnDamageTaken(info);
        if (battleField == null || info.battleFieldGuid != battleField.guid)
            return;
        if (null == bossBattleObject)
            return;
        // ★★★ 完全使用 StoryBossBattleWin 的逻辑 ★★★
        if (info.battleHurtParam.hurter.hurtObj != null && bossBattleObject.ObjID == info.battleHurtParam.hurter.hurtObj.ObjID)
        {
            bossLifeBar.Show((ulong)info.battleHurtParam.hurter.toHp, (ulong)bossBattleObject.GetMaxHp());
        }
        else if (info.battleHurtParam.caster.casterObj != null && bossBattleObject.ObjID == info.battleHurtParam.caster.casterObj.ObjID)
        {
            bossLifeBar.Show((ulong)info.battleHurtParam.caster.toHp, (ulong)bossBattleObject.GetMaxHp());
        }
    }
    protected override void OnClose()
    {
        base.OnClose();
    }
    public void DisplaySkillWordsList(NPCLineupConfig lineUPConfig)
    {
        if (skillWordCells.IsNullOrEmpty())
            return;
        if (null == lineUPConfig)
            return;
        for (int i = 0; i < skillWordCells.Length; i++)
        {
            if (i < lineUPConfig.SkillIDExList.Length)
            {
                skillWordCells[i].SetActive(true);
                int skillID = lineUPConfig.SkillIDExList[i];
                skillWordCells[i].Init(skillID, () =>
                {
                    SmallTipWin.showText = Language.Get("SmallTipFomat", SkillConfig.Get(skillID)?.SkillName, SkillConfig.Get(skillID)?.Description);
                    SmallTipWin.worldPos = CameraManager.uiCamera.ScreenToWorldPoint(Input.mousePosition);
                    SmallTipWin.isDownShow = true;
                    UIManager.Instance.OpenWindowAsync<SmallTipWin>().Forget();
                }).Forget();
            }
            else
            {
                skillWordCells[i].SetActive(false);
            }
        }
    }
    public void RefreshBuff(List<HB428_tagSCBuffRefresh> datas)
    {
        RefreshBuffCells(buffCells, datas);
        buffInfoButton.SetListener(() =>
        {
            if (bossBattleObject == null || datas.IsNullOrEmpty()) return;
            EventBroadcast.Instance.Broadcast(EventName.BATTLE_CLICK_BUFF, new BattleClickBuffData()
            {
                isMySide = false,
                heroID = (bossBattleObject as HeroBattleObject)?.teamHero?.heroId ?? 0,
                skinID = (bossBattleObject as HeroBattleObject)?.teamHero?.SkinID ?? 0,
                datas = datas,
            });
        });
    }
    bool IsOpenBattleChangeTab()
    {
        return FuncOpen.Instance.IsFuncOpen(ArenaManager.Instance.BattleChangeTabFuncId);
    }
    List<TeamHero> GetTeamHeroList(List<BattleObject> teams)
    {
        List<TeamHero> teamHeroes = new List<TeamHero>();
        if (teams.IsNullOrEmpty())
            return teamHeroes;
        foreach (var item in teams)
        {
            if (item is HeroBattleObject heroBattleObject)
            {
                teamHeroes.Add(heroBattleObject.teamHero);
            }
        }
        return teamHeroes;
    }
}
Main/System/HeroTrial/HeroTrialBattleWin.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25165ebda169aa7428e88278f78606b8
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialCarouselView.cs
New file
@@ -0,0 +1,104 @@
using System.Collections.Generic;
using UnityEngine;
public class HeroTrialCarouselView : CarouselView<HeroTrialHeroItem, int>
{
    [Header("UI References")]
    public RectTransform content;
    public GameObject heroTrialItemPrefab;
    [Header("Carousel Settings")]
    public float itemSpacing = 150f;
    public float scaleMax = 1.3f;
    public float scaleMin = 0.8f;
    bool isEventBound;
    protected override RectTransform Content => content;
    protected override GameObject ItemPrefab => heroTrialItemPrefab;
    protected override float ItemSpacing => itemSpacing;
    protected override float ScaleMax => scaleMax;
    protected override float ScaleMin => scaleMin;
    public uint CurrentHeroID
    {
        get
        {
            if (TryGetCurrentData(out int heroID))
                return (uint)heroID;
            return 0;
        }
    }
    public byte CurrentPassLevelNum
    {
        get
        {
            uint heroID = CurrentHeroID;
            if (heroID != 0 && HeroTrialManager.Instance.TryGetHeroPassLevelNum(heroID, out byte passLevelNum))
                return passLevelNum;
            return 0;
        }
    }
    protected override void OnDestroy()
    {
        UnbindEvents();
        base.OnDestroy();
    }
    public void BindEvents()
    {
        if (isEventBound)
            return;
        isEventBound = true;
        if (HeroTrialManager.Instance != null)
            HeroTrialManager.Instance.OnUpdateHeroTrialInfoEvent += OnHeroTrialDataUpdate;
    }
    public void UnbindEvents()
    {
        if (!isEventBound)
            return;
        isEventBound = false;
        if (HeroTrialManager.Instance != null)
            HeroTrialManager.Instance.OnUpdateHeroTrialInfoEvent -= OnHeroTrialDataUpdate;
    }
    void OnHeroTrialDataUpdate()
    {
        RefreshData();
    }
    public void RefreshData()
    {
        uint prevHeroID = CurrentHeroID;
        List<int> heroIDs = HeroTrialManager.Instance.GetHeroTrialHeroIDList();
        int newIndex = FindHeroIndex(heroIDs, prevHeroID);
        SetDataList(heroIDs, newIndex >= 0 ? newIndex : 0);
    }
    int FindHeroIndex(List<int> heroIDs, uint heroID)
    {
        if (heroID == 0 || heroIDs.IsNullOrEmpty())
            return -1;
        for (int i = 0; i < heroIDs.Count; i++)
        {
            if ((uint)heroIDs[i] == heroID)
                return i;
        }
        return -1;
    }
    protected override void RefreshItem(HeroTrialHeroItem item, int heroID, int realIndex, bool isSelected)
    {
        HeroTrialManager.Instance.TryGetHeroPassLevelNum((uint)heroID, out byte passLevelNum);
        item.UpdateData((uint)heroID, passLevelNum, realIndex);
        item.onItemClick = SelectIndex;
    }
}
Main/System/HeroTrial/HeroTrialCarouselView.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97cf454b383948fcb6b584596e6ac35d
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialCell.cs
New file
@@ -0,0 +1,114 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
public class HeroTrialCell : CellView
{
    [SerializeField] ImageEx heroHeadImage;
    [SerializeField] TextEx fightPowerText;
    [SerializeField] TextEx heroNameText;
    [SerializeField] ItemCell[] itemCells;
    [SerializeField] TextEx lockText;
    [SerializeField] ImageEx passMark;
    [SerializeField] ButtonEx challengeBtn;
    [SerializeField] ImageEx challengeImg;
    [SerializeField] TextEx challengeText1;
    [SerializeField] TextEx challengeText2;
    [SerializeField] ImageEx challengeImage;
    [SerializeField] ImageEx redImage;
    HeroTrialConfig currentConfig;
    public void UpdateData(HeroTrialConfig config, byte passLevelNum, bool heroUnlocked, string heroUnlockTip)
    {
        if (config == null)
            return;
        currentConfig = config;
        RefreshHeroInfo(config.HeroID);
        RefreshAwards(config.PassAwardList);
        fightPowerText.text = UIHelper.ReplaceLargeArtNum(config.FightPower);
        bool isPassed = heroUnlocked && config.LevelNum <= passLevelNum;
        string challengeTip = string.Empty;
        bool canChallenge = heroUnlocked && !isPassed && HeroTrialManager.Instance.CanChallenge(config, out challengeTip);
        bool showRed = canChallenge && HeroTrialManager.Instance.IsShowHeroTrialLevelRed(config);
        string lockTip = GetLockTip(config, isPassed, canChallenge, heroUnlocked, heroUnlockTip, challengeTip);
        RefreshState(isPassed, canChallenge, lockTip, showRed);
        challengeBtn.SetListener(OnClickChallenge);
    }
    string GetLockTip(HeroTrialConfig config, bool isPassed, bool canChallenge, bool heroUnlocked, string heroUnlockTip, string challengeTip)
    {
        if (isPassed || canChallenge)
            return string.Empty;
        bool isFirstLevel = HeroTrialConfig.TryGetFirstLevelConfig(config.HeroID, out var firstConfig) && firstConfig.CfgID == config.CfgID;
        if (!heroUnlocked && isFirstLevel && !string.IsNullOrEmpty(heroUnlockTip))
            return heroUnlockTip;
        if (!string.IsNullOrEmpty(challengeTip))
            return challengeTip;
        return Language.Get("HeroTrial05");
    }
    void RefreshHeroInfo(int heroID)
    {
        HeroConfig heroConfig = HeroConfig.Get(heroID);
        if (heroConfig == null)
            return;
        heroNameText.text = heroConfig.Name;
        if (!heroConfig.SkinIDList.IsNullOrEmpty())
        {
            int skinID = heroConfig.SkinIDList[0];
            var skinConfig = HeroSkinConfig.Get(skinID);
            if (skinConfig != null)
            {
                UILoader.LoadSprite("HeroHead", skinConfig.SquareIcon, heroHeadImage, "herohead_default").Forget();
            }
        }
    }
    void RefreshAwards(int[][] awards)
    {
        if (itemCells.IsNullOrEmpty())
            return;
        for (int i = 0; i < itemCells.Length; i++)
        {
            bool hasAward = awards != null && i < awards.Length && awards[i] != null && awards[i].Length >= 2;
            itemCells[i].SetActive(hasAward);
            if (hasAward)
            {
                int itemID = awards[i][0];
                itemCells[i].Init(new ItemCellModel(awards[i][0], true, awards[i][1]));
                itemCells[i].SetClickListener(() => ItemTipUtility.Show(itemID));
            }
        }
    }
    void RefreshState(bool isPassed, bool canChallenge, string lockTip, bool showRed)
    {
        bool isLocked = !string.IsNullOrEmpty(lockTip);
        lockText.SetActive(isLocked);
        lockText.text = lockTip;
        passMark.SetActive(isPassed);
        challengeBtn.SetActive(!isPassed);
        challengeBtn.interactable = canChallenge;
        challengeImg.gray = !canChallenge;
        challengeText1.SetActive(!canChallenge);
        challengeText2.SetActive(canChallenge);
        challengeImage.SetActive(canChallenge);
        redImage.SetActive(showRed);
    }
    void OnClickChallenge()
    {
        HeroTrialManager.Instance.SendChallenge(currentConfig);
    }
}
Main/System/HeroTrial/HeroTrialCell.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b102d9604f5b5f040873d38526654381
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialFailWin.cs
New file
@@ -0,0 +1,54 @@
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
//战场结算界面,存在多个的情况
public class HeroTrialFailWin : UIBase
{
    [SerializeField] TextEx txtFuncName;
    [SerializeField] Button tipHeroCultivateBtn;
    [SerializeField] Button tipEquipBtn;
    [SerializeField] Button tipHeroPosBtn;
    [SerializeField] ButtonEx detailBtn;
    string battleName = BattleConst.HeroTrialBattleField;
    protected override void InitComponent()
    {
        tipHeroCultivateBtn.AddListener(() =>
        {
            CloseWindow();
        });
        tipEquipBtn.AddListener(() =>
        {
            CloseWindow();
        });
        tipHeroPosBtn.AddListener(() =>
        {
            CloseWindow();
            UIManager.Instance.OpenWindowAsync<HeroPosWin>().Forget();
        });
        detailBtn.SetListener(() =>
        {
            BattleSettlementManager.Instance.OpenBattleDetailWin(battleName);
        });
    }
    protected override void OnPreOpen()
    {
        int funcId = (int)FuncOpenEnum.HeroTrial;
        txtFuncName.text = FuncOpenLVConfig.HasKey(funcId) ? FuncOpenLVConfig.Get(funcId).Name : string.Empty;
        if (!FirstChargeManager.Instance.GetLocalFail())
        {
            FirstChargeManager.Instance.SetLocalFail();
        }
        FirstChargeManager.Instance.TryPopWin();
    }
    protected override void OnPreClose()
    {
        BattleSettlementManager.Instance.WinShowOver(battleName);
    }
}
Main/System/HeroTrial/HeroTrialFailWin.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db691e748d974d246aecac137f9b0035
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialHeroItem.cs
New file
@@ -0,0 +1,85 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
public class HeroTrialHeroItem : MonoBehaviour, ICarouselItem
{
    [SerializeField] Image bgImage;
    [SerializeField] Image isActImage;
    [SerializeField] Image qualityBgImage;
    [SerializeField] Image heroHeadImage;
    [SerializeField] Transform lockRect;
    [SerializeField] TextEx lockTipText;
    [SerializeField] TextEx goText;
    [SerializeField] ButtonEx clickButton;
    [SerializeField] Image redImage;
    public RectTransform rectTransform;
    public Action<int> onItemClick;
    public int dataIndex;
    public RectTransform RectTransform
    {
        get
        {
            if (rectTransform == null)
                rectTransform = GetComponent<RectTransform>();
            return rectTransform;
        }
    }
    void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }
    public void UpdateData(uint heroID, byte passLevelNum, int index)
    {
        dataIndex = index;
        HeroConfig heroConfig = HeroConfig.Get((int)heroID);
        if (heroConfig == null)
            return;
        isActImage.SetActive(heroConfig.IsActLimit > 0);
        qualityBgImage.SetSprite("heroheadBG" + heroConfig.Quality);
        if (!heroConfig.SkinIDList.IsNullOrEmpty())
        {
            int skinID = heroConfig.SkinIDList[0];
            var skinConfig = HeroSkinConfig.Get(skinID);
            if (skinConfig != null)
                UILoader.LoadSprite("HeroHead", skinConfig.SquareIcon, heroHeadImage, "herohead_default").Forget();
        }
        UpdateLockState(heroConfig);
        RefreshRed(heroConfig.HeroID);
        clickButton.SetListener(() => onItemClick?.Invoke(dataIndex));
    }
    void RefreshRed(int heroID)
    {
        redImage.SetActive(HeroTrialManager.Instance.IsShowHeroItemRed(heroID));
    }
    /// <summary>
    /// 更新锁定状态
    /// </summary>
    void UpdateLockState(HeroConfig heroConfig)
    {
        bool isLocked = !HeroTrialManager.Instance.IsHeroTrialHeroUnlocked(heroConfig.HeroID, out string tip);
        lockRect.SetActive(isLocked);
        goText.SetActive(!isLocked);
        lockTipText.text = tip;
    }
    public void SetSelected(bool selected)
    {
        string bgKey = selected ? "HeroTrialHeroBgSelect" : "HeroTrialHeroBgUnSelect";
        bgImage.SetSprite(bgKey);
    }
}
Main/System/HeroTrial/HeroTrialHeroItem.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3bb421ee0c4449d1a1f06efbc03bc29a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialManager.cs
New file
@@ -0,0 +1,811 @@
using System;
using System.Collections.Generic;
public class HeroTrialManager : GameSystemManager<HeroTrialManager>
{
    public readonly int funcId = (int)FuncOpenEnum.HeroTrial;   // 68 武将试炼功能ID
    public readonly int DataMapID = 30040;
    /// <summary>武将试炼进度字典 <HeroID, PassLevelNum></summary>
    public Dictionary<uint, byte> heroTrialDict = new Dictionary<uint, byte>();
    int challengeRecordDay;
    // 当天发起过挑战的武将不再显示红点,胜负无关。
    HashSet<int> challengedHeroIDsToday = new HashSet<int>();
    // 当天新开放的可提示武将,优先于普通候选排队顺延。
    List<int> pendingNewRedHeroIDsToday = new List<int>();
    int currentRedHeroID;
    bool isChallengeRecordLoaded;
    string challengeRecordDayKey { get { return StringUtility.Concat("HeroTrial_ChallengeRecordDay_", PlayerDatas.Instance.PlayerId.ToString()); } }
    string lastChallengeHeroIDTodayKey { get { return StringUtility.Concat("HeroTrial_LastChallengeHeroIDToday_", PlayerDatas.Instance.PlayerId.ToString()); } }
    string challengedHeroIDsTodayKey { get { return StringUtility.Concat("HeroTrial_ChallengedHeroIDsToday_", PlayerDatas.Instance.PlayerId.ToString()); } }
    string normalRedConsumedTodayKey { get { return StringUtility.Concat("HeroTrial_NormalRedConsumedToday_", PlayerDatas.Instance.PlayerId.ToString()); } }
    string pendingNewRedHeroIDsTodayKey { get { return StringUtility.Concat("HeroTrial_PendingNewRedHeroIDsToday_", PlayerDatas.Instance.PlayerId.ToString()); } }
    string currentRedHeroIDKey { get { return StringUtility.Concat("HeroTrial_CurrentRedHeroID_", PlayerDatas.Instance.PlayerId.ToString()); } }
    /// <summary>数据更新事件,UI订阅刷新</summary>
    public event Action OnUpdateHeroTrialInfoEvent;
    /// <summary>主挑战红点</summary>
    public Redpoint parentRedpoint = new Redpoint(MainRedDot.MainChallengeRedpoint, MainRedDot.HeroTrialRepoint);
    // 当天最后一次发起挑战的武将副本,用于界面首次打开时的默认跳转。
    int lastChallengeHeroIDToday;
    // 普通红点当天只提示一次;当天新开放武将副本仍可独立排队提示。
    bool normalRedConsumedToday;
    public override void Init()
    {
        DTC0102_tagCDBPlayer.beforePlayerDataInitializeEventOnRelogin += OnBeforePlayerDataInitializeEventOnRelogin;
        FuncOpen.Instance.OnFuncStateChangeEvent += OnFuncStateChangeEvent;
        PlayerDatas.Instance.playerDataRefreshEvent += OnPlayerDataRefreshEvent;
        TimeUtility.OnServerOpenDayRefresh += OnServerOpenDayRefresh;
        TimeUtility.OnServerTimeRefresh += OnServerTimeRefresh;
    }
    public override void Release()
    {
        DTC0102_tagCDBPlayer.beforePlayerDataInitializeEventOnRelogin -= OnBeforePlayerDataInitializeEventOnRelogin;
        FuncOpen.Instance.OnFuncStateChangeEvent -= OnFuncStateChangeEvent;
        PlayerDatas.Instance.playerDataRefreshEvent -= OnPlayerDataRefreshEvent;
        TimeUtility.OnServerOpenDayRefresh -= OnServerOpenDayRefresh;
        TimeUtility.OnServerTimeRefresh -= OnServerTimeRefresh;
    }
    void OnBeforePlayerDataInitializeEventOnRelogin()
    {
        heroTrialDict.Clear();
        ResetTodayChallengeRecordCache();
        parentRedpoint.state = RedPointState.None;
    }
    void OnFuncStateChangeEvent(int obj)
    {
        if (obj != funcId)
            return;
        RefreshHeroTrialState();
    }
    void OnPlayerDataRefreshEvent(PlayerDataType type)
    {
        if (type != PlayerDataType.LV && type != PlayerDataType.RealmLevel)
            return;
        RefreshHeroTrialState();
    }
    void OnServerOpenDayRefresh()
    {
        RefreshHeroTrialState();
    }
    void OnServerTimeRefresh()
    {
        int oldDay = challengeRecordDay;
        EnsureTodayChallengeRecord();
        if (oldDay != challengeRecordDay)
            RefreshHeroTrialState();
    }
    /// <summary>
    /// 处理 B203 封包,更新武将试炼数据
    /// </summary>
    public void UpdateHeroTrialInfo(HB203_tagSCHeroTrialInfo vNetData)
    {
        if (vNetData == null || vNetData.HeroTrialList == null)
            return;
        for (int i = 0; i < vNetData.HeroTrialList.Length; i++)
        {
            var item = vNetData.HeroTrialList[i];
            heroTrialDict[item.HeroID] = item.PassLevelNum;
        }
        RefreshHeroTrialState();
    }
    void RefreshHeroTrialState()
    {
        EnsureTodayChallengeRecord();
        RefreshCurrentRedHeroID();
        UpdateRedPoint();
        OnUpdateHeroTrialInfoEvent?.Invoke();
    }
    void RefreshCurrentRedHeroID()
    {
        bool isDirty = false;
        if (!FuncOpen.Instance.IsFuncOpen(funcId))
        {
            if (currentRedHeroID != 0)
            {
                currentRedHeroID = 0;
                isDirty = true;
            }
            if (isDirty)
                SaveTodayChallengeRecord();
            return;
        }
        // 红点规则:
        // 1. 当天新开放的武将优先显示,并按配置顺序顺延。
        // 2. 当天新开放队列被挑战完后,当天不再回流到旧武将副本。
        // 3. 今天没有新开放提示链路且普通红点未被挑战消费时,显示所有已解锁且有可挑战关卡的武将里,当前可挑战关卡战力最低的那个。
        if (currentRedHeroID != 0 && !CanShowHeroItemRed(currentRedHeroID))
        {
            currentRedHeroID = 0;
            isDirty = true;
        }
        List<int> heroIDList = GetHeroTrialHeroIDList();
        List<int> candidateHeroIDs = BuildCandidateHeroIDs(heroIDList);
        List<int> todayOpenHeroIDs = BuildTodayOpenHeroIDs(candidateHeroIDs);
        RemoveHeroIDs(candidateHeroIDs, todayOpenHeroIDs);
        if (NormalizePendingNewRedHeroIDs(heroIDList))
            isDirty = true;
        if (AppendTodayOpenPendingCandidates(todayOpenHeroIDs))
            isDirty = true;
        if (NormalizePendingNewRedHeroIDs(heroIDList))
            isDirty = true;
        if (PromoteTodayOpenPendingRed(todayOpenHeroIDs))
        {
            isDirty = true;
        }
        else if (HasChallengedTodayOpenHeroTrialToday() &&
            pendingNewRedHeroIDsToday.Count == 0 &&
            (currentRedHeroID == 0 || !IsHeroTrialHeroOpenToday(currentRedHeroID)))
        {
            if (currentRedHeroID != 0)
            {
                currentRedHeroID = 0;
                isDirty = true;
            }
        }
        else if (!normalRedConsumedToday && (currentRedHeroID == 0 || !IsHeroTrialHeroOpenToday(currentRedHeroID)))
        {
            int bestHeroID = GetLowestCanChallengeHeroID(heroIDList);
            if (currentRedHeroID != bestHeroID)
            {
                currentRedHeroID = bestHeroID;
                isDirty = true;
            }
        }
        if (isDirty)
            SaveTodayChallengeRecord();
    }
    List<int> BuildCandidateHeroIDs(List<int> sortedHeroIDList)
    {
        List<int> candidateHeroIDs = new List<int>();
        for (int i = 0; i < sortedHeroIDList.Count; i++)
        {
            int heroID = sortedHeroIDList[i];
            if (!CanShowHeroItemRed(heroID))
                continue;
            candidateHeroIDs.Add(heroID);
        }
        return candidateHeroIDs;
    }
    List<int> BuildTodayOpenHeroIDs(List<int> candidateHeroIDs)
    {
        List<int> todayOpenHeroIDs = new List<int>();
        for (int i = 0; i < candidateHeroIDs.Count; i++)
        {
            int heroID = candidateHeroIDs[i];
            if (IsHeroTrialHeroOpenToday(heroID))
                todayOpenHeroIDs.Add(heroID);
        }
        return todayOpenHeroIDs;
    }
    bool HasChallengedTodayOpenHeroTrialToday()
    {
        foreach (int heroID in challengedHeroIDsToday)
        {
            if (IsHeroTrialHeroOpenToday(heroID))
                return true;
        }
        return false;
    }
    bool IsHeroTrialHeroOpenToday(int heroID)
    {
        HeroConfig heroConfig = HeroConfig.Get(heroID);
        return heroConfig != null &&
            heroConfig.OpenCollectionDay > 0 &&
            heroConfig.OpenCollectionDay == TimeUtility.OpenDay + 1;
    }
    void RemoveHeroIDs(List<int> heroIDs, List<int> removeHeroIDs)
    {
        if (removeHeroIDs.IsNullOrEmpty())
            return;
        for (int i = heroIDs.Count - 1; i >= 0; i--)
        {
            if (removeHeroIDs.Contains(heroIDs[i]))
                heroIDs.RemoveAt(i);
        }
    }
    bool AppendTodayOpenPendingCandidates(List<int> candidateHeroIDs)
    {
        bool isDirty = false;
        for (int i = 0; i < candidateHeroIDs.Count; i++)
        {
            int heroID = candidateHeroIDs[i];
            if (AppendPendingCandidate(heroID))
                isDirty = true;
        }
        return isDirty;
    }
    bool AppendPendingCandidate(int heroID)
    {
        if (heroID == currentRedHeroID || pendingNewRedHeroIDsToday.Contains(heroID))
            return false;
        pendingNewRedHeroIDsToday.Add(heroID);
        return true;
    }
    bool NormalizePendingNewRedHeroIDs(List<int> sortedHeroIDList)
    {
        // 队列只保留仍可提示的当天新开放武将,并按配置顺序归一,保证 E -> F -> G 顺延。
        List<int> normalizedHeroIDs = new List<int>();
        for (int i = 0; i < pendingNewRedHeroIDsToday.Count; i++)
        {
            int heroID = pendingNewRedHeroIDsToday[i];
            if (heroID == currentRedHeroID)
                continue;
            if (!IsHeroTrialHeroOpenToday(heroID))
                continue;
            if (!CanShowHeroItemRed(heroID))
                continue;
            if (!normalizedHeroIDs.Contains(heroID))
                normalizedHeroIDs.Add(heroID);
        }
        normalizedHeroIDs.Sort((a, b) => GetHeroSortIndex(sortedHeroIDList, a).CompareTo(GetHeroSortIndex(sortedHeroIDList, b)));
        if (IsSameHeroIDList(pendingNewRedHeroIDsToday, normalizedHeroIDs))
            return false;
        pendingNewRedHeroIDsToday.Clear();
        pendingNewRedHeroIDsToday.AddRange(normalizedHeroIDs);
        return true;
    }
    int GetHeroSortIndex(List<int> sortedHeroIDList, int heroID)
    {
        int index = sortedHeroIDList.IndexOf(heroID);
        return index >= 0 ? index : int.MaxValue;
    }
    bool IsSameHeroIDList(List<int> listA, List<int> listB)
    {
        if (listA.Count != listB.Count)
            return false;
        for (int i = 0; i < listA.Count; i++)
        {
            if (listA[i] != listB[i])
                return false;
        }
        return true;
    }
    int GetLowestCanChallengeHeroID(List<int> heroIDList)
    {
        int bestHeroID = 0;
        long bestFightPower = long.MaxValue;
        int bestSortIndex = int.MaxValue;
        for (int i = 0; i < heroIDList.Count; i++)
        {
            int heroID = heroIDList[i];
            if (!CanShowHeroItemRed(heroID))
                continue;
            if (!TryGetLowestCanChallengeConfig(heroID, out var config))
                continue;
            if (config.FightPower < bestFightPower)
            {
                bestHeroID = heroID;
                bestFightPower = config.FightPower;
                bestSortIndex = i;
                continue;
            }
            if (config.FightPower == bestFightPower && i < bestSortIndex)
            {
                bestHeroID = heroID;
                bestSortIndex = i;
            }
        }
        return bestHeroID;
    }
    int GetLowestEnterHeroID(List<int> heroIDList)
    {
        int bestHeroID = 0;
        long bestFightPower = long.MaxValue;
        int bestSortIndex = int.MaxValue;
        for (int i = 0; i < heroIDList.Count; i++)
        {
            int heroID = heroIDList[i];
            if (!IsHeroTrialHeroUnlocked(heroID))
                continue;
            if (!HasChallengeableLevel(heroID))
                continue;
            if (!TryGetLowestCanChallengeConfig(heroID, out var config))
                continue;
            if (config.FightPower < bestFightPower)
            {
                bestHeroID = heroID;
                bestFightPower = config.FightPower;
                bestSortIndex = i;
                continue;
            }
            if (config.FightPower == bestFightPower && i < bestSortIndex)
            {
                bestHeroID = heroID;
                bestSortIndex = i;
            }
        }
        return bestHeroID;
    }
    bool TryGetLowestCanChallengeConfig(int heroID, out HeroTrialConfig config)
    {
        config = null;
        if (!HeroTrialConfig.TryGetHeroConfigList(heroID, out var configs))
            return false;
        for (int i = 0; i < configs.Count; i++)
        {
            if (!CanChallenge(configs[i], out _))
                continue;
            if (config == null || configs[i].FightPower < config.FightPower)
                config = configs[i];
        }
        return config != null;
    }
    bool PromoteTodayOpenPendingRed(List<int> todayOpenHeroIDs)
    {
        if (todayOpenHeroIDs.IsNullOrEmpty())
            return false;
        if (currentRedHeroID != 0 && todayOpenHeroIDs.Contains(currentRedHeroID))
            return false;
        for (int i = 0; i < pendingNewRedHeroIDsToday.Count; i++)
        {
            int heroID = pendingNewRedHeroIDsToday[i];
            if (!todayOpenHeroIDs.Contains(heroID))
                continue;
            // 今天新开放的武将优先展示;队列耗尽后当天不再回流到旧武将副本。
            currentRedHeroID = heroID;
            pendingNewRedHeroIDsToday.RemoveAt(i);
            return true;
        }
        return false;
    }
    /// <summary>
    /// 更新红点状态
    /// </summary>
    public void UpdateRedPoint()
    {
        parentRedpoint.state = RedPointState.None;
        if (!FuncOpen.Instance.IsFuncOpen(funcId))
            return;
        if (currentRedHeroID != 0)
        {
            parentRedpoint.state = RedPointState.Simple;
        }
    }
    public int GetFuncLineID(HeroTrialConfig config)
    {
        if (config == null)
            return 0;
        return GetFuncLineID(config.HeroID, config.LevelNum);
    }
    public int GetFuncLineID(int heroID, int levelNum)
    {
        return heroID * 1000 + levelNum;
    }
    public void AnalysisFuncLineID(int funcLineID, out int heroID, out int levelNum)
    {
        heroID = funcLineID / 1000;
        levelNum = funcLineID % 1000;
    }
    public bool TryGetConfigByFuncLineID(int funcLineID, out HeroTrialConfig config)
    {
        AnalysisFuncLineID(funcLineID, out int heroID, out int levelNum);
        return HeroTrialConfig.TryGetHeroTrialConfig(heroID, levelNum, out config);
    }
    public bool CanChallenge(HeroTrialConfig config, out string tip)
    {
        tip = string.Empty;
        if (config == null)
            return false;
        if (!IsHeroTrialHeroUnlocked(config.HeroID, out string heroUnlockTip))
        {
            tip = heroUnlockTip;
            return false;
        }
        if (!IsTrialLevelLimitMet(config, out tip))
            return false;
        bool hasProgress = TryGetHeroPassLevelNum((uint)config.HeroID, out byte passLevelNum);
        bool isFirstLevel = HeroTrialConfig.TryGetFirstLevelConfig(config.HeroID, out var firstConfig) && firstConfig.CfgID == config.CfgID;
        if (!hasProgress)
        {
            passLevelNum = 0;
            if (isFirstLevel && passLevelNum < config.LevelNum)
                return true;
            tip = Language.Get("HeroTrial05");
            return false;
        }
        if (isFirstLevel && passLevelNum < config.LevelNum)
            return true;
        if (config.LevelNum != passLevelNum + 1)
        {
            tip = Language.Get("HeroTrial05");
            return false;
        }
        return true;
    }
    bool IsTrialLevelLimitMet(HeroTrialConfig config, out string tip)
    {
        tip = string.Empty;
        if (config == null)
            return false;
        if (config.LVLimit > 0 && PlayerDatas.Instance.baseData.LV < config.LVLimit)
        {
            tip = Language.Get("HeroTrial04", config.LVLimit);
            return false;
        }
        if (config.RealmLimit > 0 && PlayerDatas.Instance.baseData.realmLevel < config.RealmLimit)
        {
            var realmConfig = RealmConfig.Get(config.RealmLimit);
            tip = Language.Get("HeroTrial03", realmConfig != null ? realmConfig.Name : string.Empty);
            return false;
        }
        return true;
    }
    public bool IsShowHeroItemRed(int heroID)
    {
        return heroID != 0 &&
            heroID == currentRedHeroID &&
            CanShowHeroItemRed(heroID) &&
            TryGetLowestCanChallengeConfig(heroID, out _);
    }
    public bool IsShowHeroTrialLevelRed(HeroTrialConfig config)
    {
        if (config == null || config.HeroID != currentRedHeroID)
            return false;
        if (!CanChallenge(config, out _))
            return false;
        return TryGetLowestCanChallengeConfig(config.HeroID, out var lowestConfig) &&
            lowestConfig != null &&
            lowestConfig.CfgID == config.CfgID;
    }
    public bool HasAnyChallengeableLevel()
    {
        List<int> heroIDList = GetHeroTrialHeroIDList();
        for (int i = 0; i < heroIDList.Count; i++)
        {
            if (HasChallengeableLevel(heroIDList[i]))
                return true;
        }
        return false;
    }
    bool CanShowHeroItemRed(int heroID)
    {
        return FuncOpen.Instance.IsFuncOpen(funcId) &&
            IsHeroTrialHeroUnlocked(heroID) &&
            !HasChallengedHeroTrialToday(heroID) &&
            HasChallengeableLevel(heroID);
    }
    bool HasChallengeableLevel(int heroID)
    {
        if (!HeroTrialConfig.TryGetHeroConfigList(heroID, out var configs))
            return false;
        for (int i = 0; i < configs.Count; i++)
        {
            if (CanChallenge(configs[i], out _))
                return true;
        }
        return false;
    }
    public void SendChallenge(HeroTrialConfig config)
    {
        if (!CanChallenge(config, out string tip))
        {
            if (!string.IsNullOrEmpty(tip))
            {
                SysNotifyMgr.Instance.ShowStringTip(tip);
            }
            return;
        }
        MarkHeroTrialChallengedToday(config.HeroID);
        if (lastChallengeHeroIDToday != config.HeroID)
        {
            lastChallengeHeroIDToday = config.HeroID;
            SaveTodayChallengeRecord();
        }
        bool isDirty = pendingNewRedHeroIDsToday.Remove(config.HeroID);
        if (currentRedHeroID == config.HeroID)
        {
            if (!IsHeroTrialHeroOpenToday(config.HeroID))
                normalRedConsumedToday = true;
            currentRedHeroID = 0;
            isDirty = true;
        }
        if (isDirty)
            SaveTodayChallengeRecord();
        RefreshHeroTrialState();
        BattleManager.Instance.SendTurnFight((uint)DataMapID, (uint)GetFuncLineID(config));
    }
    public bool HasChallengedHeroTrialToday(int heroID)
    {
        EnsureTodayChallengeRecord();
        return challengedHeroIDsToday.Contains(heroID);
    }
    /// <summary>
    /// 界面首次打开时的默认落点:
    /// 1. 当前红点武将;
    /// 2. 上次挑战过且当前仍可挑战的武将;
    /// 3. 所有已解锁且可挑战关卡中战力最低的武将。
    /// </summary>
    public int GetHeroTrialEnterHeroID()
    {
        EnsureTodayChallengeRecord();
        if (FuncOpen.Instance.IsFuncOpen(funcId))
        {
            if (currentRedHeroID != 0 && IsShowHeroItemRed(currentRedHeroID))
                return currentRedHeroID;
        }
        if (lastChallengeHeroIDToday != 0 && HasChallengeableLevel(lastChallengeHeroIDToday))
            return lastChallengeHeroIDToday;
        List<int> heroIDList = GetHeroTrialHeroIDList();
        return GetLowestEnterHeroID(heroIDList);
    }
    void MarkHeroTrialChallengedToday(int heroID)
    {
        EnsureTodayChallengeRecord();
        if (challengedHeroIDsToday.Add(heroID))
            SaveTodayChallengeRecord();
    }
    void EnsureTodayChallengeRecord()
    {
        if (!isChallengeRecordLoaded)
            LoadTodayChallengeRecord();
        int today = GetTodayID();
        if (challengeRecordDay == today)
            return;
        challengeRecordDay = today;
        challengedHeroIDsToday.Clear();
        lastChallengeHeroIDToday = 0;
        normalRedConsumedToday = false;
        pendingNewRedHeroIDsToday.Clear();
        currentRedHeroID = 0;
        SaveTodayChallengeRecord();
    }
    void ResetTodayChallengeRecordCache()
    {
        challengeRecordDay = 0;
        challengedHeroIDsToday.Clear();
        lastChallengeHeroIDToday = 0;
        normalRedConsumedToday = false;
        pendingNewRedHeroIDsToday.Clear();
        currentRedHeroID = 0;
        isChallengeRecordLoaded = false;
    }
    void LoadTodayChallengeRecord()
    {
        isChallengeRecordLoaded = true;
        challengeRecordDay = LocalSave.GetInt(challengeRecordDayKey);
        if (challengeRecordDay != GetTodayID())
        {
            challengeRecordDay = GetTodayID();
            challengedHeroIDsToday.Clear();
            lastChallengeHeroIDToday = 0;
            normalRedConsumedToday = false;
            pendingNewRedHeroIDsToday.Clear();
            currentRedHeroID = 0;
            SaveTodayChallengeRecord();
            return;
        }
        LoadHeroIDSet(challengedHeroIDsTodayKey, challengedHeroIDsToday);
        lastChallengeHeroIDToday = LocalSave.GetInt(lastChallengeHeroIDTodayKey);
        normalRedConsumedToday = LocalSave.GetInt(normalRedConsumedTodayKey) != 0;
        LoadHeroIDList(pendingNewRedHeroIDsTodayKey, pendingNewRedHeroIDsToday);
        currentRedHeroID = LocalSave.GetInt(currentRedHeroIDKey);
    }
    void SaveTodayChallengeRecord()
    {
        LocalSave.SetInt(challengeRecordDayKey, challengeRecordDay);
        LocalSave.SetIntArray(challengedHeroIDsTodayKey, ToArray(challengedHeroIDsToday));
        LocalSave.SetInt(lastChallengeHeroIDTodayKey, lastChallengeHeroIDToday);
        LocalSave.SetInt(normalRedConsumedTodayKey, normalRedConsumedToday ? 1 : 0);
        LocalSave.SetIntArray(pendingNewRedHeroIDsTodayKey, pendingNewRedHeroIDsToday.ToArray());
        LocalSave.SetInt(currentRedHeroIDKey, currentRedHeroID);
    }
    void LoadHeroIDSet(string key, HashSet<int> heroIDSet)
    {
        heroIDSet.Clear();
        int[] heroIDs = LocalSave.GetIntArray(key);
        if (heroIDs == null)
            return;
        for (int i = 0; i < heroIDs.Length; i++)
        {
            if (heroIDs[i] != 0)
                heroIDSet.Add(heroIDs[i]);
        }
    }
    void LoadHeroIDList(string key, List<int> heroIDList)
    {
        heroIDList.Clear();
        int[] heroIDs = LocalSave.GetIntArray(key);
        if (heroIDs == null)
            return;
        for (int i = 0; i < heroIDs.Length; i++)
        {
            if (heroIDs[i] != 0 && !heroIDList.Contains(heroIDs[i]))
                heroIDList.Add(heroIDs[i]);
        }
    }
    int[] ToArray(HashSet<int> heroIDSet)
    {
        int[] heroIDs = new int[heroIDSet.Count];
        heroIDSet.CopyTo(heroIDs);
        return heroIDs;
    }
    int GetTodayID()
    {
        DateTime now = TimeUtility.ServerNow;
        return now.Year * 10000 + now.Month * 100 + now.Day;
    }
    /// <summary>
    /// 尝试获取某武将的通关进度
    /// </summary>
    public bool TryGetHeroPassLevelNum(uint heroID, out byte passLevelNum)
    {
        return heroTrialDict.TryGetValue(heroID, out passLevelNum);
    }
    public bool IsHeroTrialHeroUnlocked(int heroID)
    {
        return IsHeroTrialHeroUnlocked(heroID, out _);
    }
    public bool IsHeroTrialHeroUnlocked(int heroID, out string lockTip)
    {
        lockTip = string.Empty;
        HeroConfig heroConfig = HeroConfig.Get(heroID);
        if (heroConfig == null)
            return false;
        if (heroConfig.IsActLimit > 0)
        {
            bool isOpenCollectionDayMet = heroConfig.OpenCollectionDay <= 0 ||
                TimeUtility.OpenDay + 1 >= heroConfig.OpenCollectionDay;
            if (!isOpenCollectionDayMet)
            {
                lockTip = Language.Get("HeroTrial02", heroConfig.Name);
                return false;
            }
        }
        else
        {
            if (HeroTrialConfig.TryGetFirstLevelConfig(heroConfig.HeroID, out var trialConfig))
            {
                if (trialConfig.LVLimit > 0)
                {
                    if (PlayerDatas.Instance.baseData.LV < trialConfig.LVLimit)
                    {
                        lockTip = Language.Get("HeroTrial04", trialConfig.LVLimit);
                        return false;
                    }
                }
                else if (trialConfig.RealmLimit > 0)
                {
                    if (PlayerDatas.Instance.baseData.realmLevel < trialConfig.RealmLimit)
                    {
                        var config = RealmConfig.Get(trialConfig.RealmLimit);
                        lockTip = Language.Get("HeroTrial03", config != null ? config.Name : string.Empty);
                        return false;
                    }
                }
            }
        }
        return true;
    }
    /// <summary>
    /// 获取武将试炼武将列表:按配置表排序,不依赖服务器进度数据
    /// </summary>
    public List<int> GetHeroTrialHeroIDList()
    {
        return HeroTrialConfig.GetSortedHeroIDList();
    }
}
Main/System/HeroTrial/HeroTrialManager.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 53a5e2b3e306f7e4eac9cb9750168c6a
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialVictoryWin.cs
New file
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class HeroTrialVictoryWin : UIBase
{
    [SerializeField] ButtonEx detailBtn;
    [SerializeField] TextEx txtFuncName;
    [SerializeField] ScrollerController scroller;
    string battleName = BattleConst.HeroTrialBattleField;
    protected override void InitComponent()
    {
        detailBtn.SetListener(() =>
        {
            BattleSettlementManager.Instance.OpenBattleDetailWin(battleName);
        });
    }
    protected override void OnPreOpen()
    {
        scroller.OnRefreshCell += OnRefreshCell;
        CreateScroller();
        int funcId = (int)FuncOpenEnum.HeroTrial;
        txtFuncName.text = FuncOpenLVConfig.HasKey(funcId) ? FuncOpenLVConfig.Get(funcId).Name : string.Empty;
    }
    protected override void OnPreClose()
    {
        scroller.OnRefreshCell -= OnRefreshCell;
        BattleSettlementManager.Instance.WinShowOver(battleName);
    }
    List<Item> showItems = new List<Item>();
    void CreateScroller()
    {
        var jsonData = BattleSettlementManager.Instance.GetBattleSettlement(battleName);
        if (jsonData == null)
        {
            DelayCloseWindow().Forget();
            return;
        }
        showItems.Clear();
        scroller.Refresh();
        if (!jsonData.ContainsKey("itemInfo"))
        {
            return;
        }
        var resultStr = jsonData["itemInfo"];
        for (int i = 0; i < resultStr.Count; i++)
        {
            showItems.Add(new Item((int)resultStr[i]["ItemID"], (long)resultStr[i]["Count"]));
        }
        showItems.Sort(SortItem);
        for (int i = 0; i < showItems.Count; i++)
        {
            scroller.AddCell(ScrollerDataType.Header, i);
        }
        scroller.Restart();
    }
    int SortItem(Item itemA, Item itemB)
    {
        var itemConfigA = ItemConfig.Get(itemA.id);
        var itemConfigB = ItemConfig.Get(itemB.id);
        return itemConfigB.ItemColor - itemConfigA.ItemColor;
    }
    void OnRefreshCell(ScrollerDataType type, CellView cell)
    {
        var _cell = cell as SettlementAwardCell;
        var item = showItems[cell.index];
        _cell?.Display(item.id, item.countEx);
    }
}
Main/System/HeroTrial/HeroTrialVictoryWin.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35108f3aae26e9e459e60e4368a648f3
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/HeroTrial/HeroTrialWin.cs
New file
@@ -0,0 +1,171 @@
using System.Collections.Generic;
using UnityEngine;
public class HeroTrialWin : UIBase
{
    [SerializeField] HeroTrialCarouselView carouselView;
    [SerializeField] ScrollerController scroller;
    List<HeroTrialConfig> currentConfigs = new List<HeroTrialConfig>();
    uint currentHeroID;
    byte currentPassLevelNum;
    bool currentHeroUnlocked;
    string currentHeroUnlockTip = string.Empty;
    // OnPreOpen 首次打开时用于定位到红点关卡的 dataIndex;-1 表示不跳转。
    int jumpTargetDataIndex = -1;
    protected override void InitComponent()
    {
    }
    protected override void OnPreOpen()
    {
        carouselView.InitPool();
        carouselView.BindEvents();
        carouselView.OnSelectedIndexChanged += OnSelectedIndexChanged;
        scroller.OnRefreshCell += OnRefreshCell;
        HeroTrialManager.Instance.OnUpdateHeroTrialInfoEvent += OnHeroTrialInfoUpdate;
        carouselView.RefreshData();
        carouselView.InitWithIndex(GetDefaultHeroIndex());
        RefreshScroller();
    }
    protected override void OnPreClose()
    {
        carouselView.UnbindEvents();
        carouselView.OnSelectedIndexChanged -= OnSelectedIndexChanged;
        scroller.OnRefreshCell -= OnRefreshCell;
        HeroTrialManager.Instance.OnUpdateHeroTrialInfoEvent -= OnHeroTrialInfoUpdate;
    }
    void OnSelectedIndexChanged()
    {
        RefreshScroller();
    }
    void OnHeroTrialInfoUpdate()
    {
        int redHeroIndex = FindRedHeroIndex();
        if (redHeroIndex >= 0)
        {
            List<int> heroIDList = HeroTrialManager.Instance.GetHeroTrialHeroIDList();
            int redHeroID = heroIDList[redHeroIndex];
            if (carouselView.CurrentHeroID != redHeroID)
            {
                carouselView.SelectIndex(redHeroIndex);
                return;
            }
        }
        RefreshScroller();
    }
    int GetDefaultHeroIndex()
    {
        List<int> heroIDList = HeroTrialManager.Instance.GetHeroTrialHeroIDList();
        if (heroIDList.IsNullOrEmpty())
            return 0;
        int targetHeroID = HeroTrialManager.Instance.GetHeroTrialEnterHeroID();
        if (targetHeroID != 0)
        {
            for (int i = 0; i < heroIDList.Count; i++)
            {
                if (heroIDList[i] == targetHeroID)
                    return i;
            }
        }
        return 0;
    }
    int FindRedHeroIndex()
    {
        List<int> heroIDList = HeroTrialManager.Instance.GetHeroTrialHeroIDList();
        if (heroIDList.IsNullOrEmpty())
            return -1;
        for (int i = 0; i < heroIDList.Count; i++)
        {
            if (HeroTrialManager.Instance.IsShowHeroItemRed(heroIDList[i]))
                return i;
        }
        return -1;
    }
    /// <summary>
    /// 根据当前选中的武将,刷新下方的关卡Scroll
    /// </summary>
    void RefreshScroller()
    {
        currentConfigs.Clear();
        currentHeroID = carouselView.CurrentHeroID;
        currentHeroUnlockTip = string.Empty;
        currentHeroUnlocked = currentHeroID != 0 && HeroTrialManager.Instance.IsHeroTrialHeroUnlocked((int)currentHeroID, out currentHeroUnlockTip);
        if (currentHeroID == 0 || !HeroTrialManager.Instance.TryGetHeroPassLevelNum(currentHeroID, out currentPassLevelNum))
        {
            currentPassLevelNum = 0;
        }
        if (!currentHeroUnlocked)
        {
            currentPassLevelNum = 0;
        }
        scroller.Refresh();
        if (currentHeroID != 0 && HeroTrialConfig.TryGetHeroConfigList((int)currentHeroID, out var configs))
        {
            currentConfigs.AddRange(configs);
            for (int i = 0; i < currentConfigs.Count; i++)
            {
                scroller.AddCell(ScrollerDataType.Header, currentConfigs[i].CfgID);
            }
        }
        scroller.Restart();
        // 首次打开时,定位到当前武将副本下第一个能挑战的关卡;没有则保持默认置顶。
        jumpTargetDataIndex = FindJumpTargetDataIndex();
        if (jumpTargetDataIndex >= 0)
            scroller.JumpIndex(jumpTargetDataIndex);
    }
    /// <summary>
    /// 在 currentConfigs 中查找当前武将副本第一个能挑战的关卡对应的 dataIndex;
    /// 若当前武将无任何可挑战关卡或数据异常则返回 -1。
    /// </summary>
    int FindJumpTargetDataIndex()
    {
        if (currentHeroID == 0)
            return -1;
        if (currentConfigs.IsNullOrEmpty())
            return -1;
        for (int i = 0; i < currentConfigs.Count; i++)
        {
            var config = currentConfigs[i];
            if (config == null || config.HeroID != currentHeroID)
                continue;
            if (HeroTrialManager.Instance.CanChallenge(config, out _))
                return i;
        }
        return -1;
    }
    /// <summary>
    /// 刷新关卡Cell显示
    /// </summary>
    void OnRefreshCell(ScrollerDataType type, CellView cell)
    {
        var trialCell = cell as HeroTrialCell;
        if (trialCell == null)
            return;
        if (!HeroTrialConfig.TryGetHeroTrialConfig(cell.index, out var config))
            return;
        trialCell.UpdateData(config, currentPassLevelNum, currentHeroUnlocked, currentHeroUnlockTip);
    }
}
Main/System/HeroTrial/HeroTrialWin.cs.meta
New file
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6e3215d80964334f807ea63e0a306b8
MonoImporter:
  externalObjects: {}
  serializedVersion: 2
  defaultReferences: []
  executionOrder: 0
  icon: {instanceID: 0}
  userData:
  assetBundleName:
  assetBundleVariant:
Main/System/Redpoint/MainRedDot.cs
@@ -160,6 +160,7 @@
    public const int HeroSkinFlashSaleRepoint = 486;
    public const int SuperVIPRepoint = 487;
    public const int GuildAtkDefBatRepoint = 488;//公会攻防战
    public const int HeroTrialRepoint = 489;//武将试炼
    public void Register()
    {
Main/System/Settlement/BattleSettlementManager.cs
@@ -50,6 +50,9 @@
            case BattleConst.BoneBattleField:
                PopupWindowsProcessor.Instance.Add(isWin ? "BoneBattleVictoryWin" : "BoneBattleFailWin", false, BattleConst.BoneBattleField);
                break;
            case BattleConst.HeroTrialBattleField:
                PopupWindowsProcessor.Instance.Add(isWin ? "HeroTrialVictoryWin" : "HeroTrialFailWin", false, BattleConst.HeroTrialBattleField);
                break;
            case BattleConst.TianziBillboradBattleField:
                PopupWindowsProcessor.Instance.Add("TianziBillboradVictoryWin", false, BattleConst.TianziBillboradBattleField);
                break;
@@ -99,6 +102,16 @@
                else
                {
                    UIManager.Instance.OpenWindow<BoneBattleFailWin>();
                }
                break;
            case BattleConst.HeroTrialBattleField:
                if (isWin)
                {
                    UIManager.Instance.OpenWindowAsync<HeroTrialVictoryWin>().Forget();
                }
                else
                {
                    UIManager.Instance.OpenWindowAsync<HeroTrialFailWin>().Forget();
                }
                break;
            case BattleConst.TianziBillboradBattleField:
@@ -376,6 +389,7 @@
            // 敌方名字是Boss名字
            case BattleConst.StoryBossBattleField:
            case BattleConst.BoneBattleField:
            case BattleConst.HeroTrialBattleField:
            case BattleConst.TianziBillboradBattleField:
            case BattleConst.WarlordPavilionBattleField:
                BattleObject bossBattleObject = battleField.FindBoss();
Main/System/WarlordPavilion/TowerBaseWin.cs
@@ -3,14 +3,20 @@
public class TowerBaseWin : OneLevelWin
{
    [SerializeField] Transform WarlordPavilionWinTop;
    [SerializeField] Transform HeroTrialWinTop;
    protected override void OpenSubUIByTabIndex()
    {
        WarlordPavilionWinTop.SetActive(functionOrder == 0);
        HeroTrialWinTop.SetActive(functionOrder == 1);
        switch (functionOrder)
        {
            case 0:
                currentSubUI = UIManager.Instance.OpenWindow<WarlordPavilionWin>();
                break;
            case 1:
                currentSubUI = UIManager.Instance.OpenWindow<HeroTrialWin>();
                break;
            default:
                Debug.LogWarning("未知的标签索引: " + functionOrder);
                break;
Main/Utility/EnumHelper.cs
@@ -862,6 +862,7 @@
    FestivalActivity = 65,//节日活动
    HeroSkinFlashSale = 66,//时装特卖
    GuildAtkDefBat = 67,//公会攻防战
    HeroTrial = 68,//武将试炼
}