From fe2805709cbd8cee561b475eda53674fbf06ef9e Mon Sep 17 00:00:00 2001
From: lcy <1459594991@qq.com>
Date: 星期二, 23 六月 2026 17:02:26 +0800
Subject: [PATCH] 662 武将试炼

---
 Main/System/ChallengeTab/ArenaTabHandler.cs                                                  |    5 
 Main/System/Battle/BattleConst.cs                                                            |    7 
 Main/Config/ConfigManager.cs                                                                 |    6 
 Main/System/HeroTrial/HeroTrialBattleWin.cs.meta                                             |   11 
 Main/System/ChallengeTab/BaseChallengeTabHandler.cs                                          |   14 
 Main/System/HeroTrial/HeroTrialFailWin.cs                                                    |   54 +
 Main/System/Redpoint/MainRedDot.cs                                                           |    1 
 Main/System/HeroTrial/HeroTrialVictoryWin.cs                                                 |   85 +
 Main/System/Battle/BattleFieldFactory.cs                                                     |    5 
 Main/System/HeroTrial/CarouselView.cs                                                        |  236 +++++
 Main/System/HeroTrial/HeroTrialBattleWin.cs                                                  |  292 ++++++
 Main/System/HeroTrial/HeroTrialWin.cs                                                        |  171 +++
 Main/System/Settlement/BattleSettlementManager.cs                                            |   16 
 Main/Config/Configs/HeroTrialConfig.cs                                                       |   80 +
 Main/System/HeroTrial/HeroTrialHeroItem.cs.meta                                              |   11 
 Main/Main.cs                                                                                 |    1 
 Main/Config/PartialConfigs/HeroTrialConfig.cs.meta                                           |   11 
 Main/System/ChallengeTab/ChallengeTabButton.cs                                               |   10 
 Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs                |   29 
 Main/Config/Configs/HeroTrialConfig.cs.meta                                                  |   11 
 Main/System/HeroTrial/HeroTrialManager.cs                                                    |  811 ++++++++++++++++++
 Main/Config/PartialConfigs/HeroTrialConfig.cs                                                |  109 ++
 Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs.meta           |   11 
 Main/Utility/EnumHelper.cs                                                                   |    1 
 Main/System/HeroTrial/HeroTrialCarouselView.cs.meta                                          |   11 
 Main/System/HeroTrial/HeroTrialCell.cs.meta                                                  |   11 
 Main/System/HeroTrial/HeroTrialManager.cs.meta                                               |   11 
 Main/System/ChallengeTab/HeroTrialTabHandler.cs.meta                                         |   11 
 Main/System/HeroTrial/HeroTrialWin.cs.meta                                                   |   11 
 Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs.meta |   11 
 Main/System/Battle/BattleField/HeroTrialBattleField.cs.meta                                  |   11 
 Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs      |   12 
 Main/System/HeroTrial/HeroTrialCarouselView.cs                                               |  104 ++
 Main/System/ChallengeTab/HeroTrialTabHandler.cs                                              |   57 +
 Main/System/Battle/BattleField/HeroTrialBattleField.cs                                       |  130 ++
 Main/System/HeroTrial/HeroTrialCell.cs                                                       |  114 ++
 Main/System/HeroTrial/HeroTrialHeroItem.cs                                                   |   85 +
 Main/System/HeroTrial/HeroTrialVictoryWin.cs.meta                                            |   11 
 Main/System/WarlordPavilion/TowerBaseWin.cs                                                  |    6 
 Main/Core/NetworkPackage/DataToCtl/PackageRegedit.cs                                         |    3 
 Main/System/HeroTrial.meta                                                                   |    8 
 Main/System/HeroTrial/HeroTrialFailWin.cs.meta                                               |   11 
 Main/System/ChallengeTab/WarlordPavilionTabHandler.cs                                        |    5 
 Main/System/ChallengeTab/QunyingTabHandler.cs                                                |    5 
 Main/System/ChallengeTab/BoneFieldTabHandler.cs                                              |    5 
 Main/System/ChallengeTab/TianziBillboradTabHandler.cs                                        |    5 
 Main/System/HeroTrial/CarouselView.cs.meta                                                   |   11 
 47 files changed, 2,614 insertions(+), 23 deletions(-)

diff --git a/Main/Config/ConfigManager.cs b/Main/Config/ConfigManager.cs
index e5612c4..5c842f4 100644
--- a/Main/Config/ConfigManager.cs
+++ b/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 瀛楀吀
diff --git a/Main/Config/Configs/HeroTrialConfig.cs b/Main/Config/Configs/HeroTrialConfig.cs
new file mode 100644
index 0000000..6e0a945
--- /dev/null
+++ b/Main/Config/Configs/HeroTrialConfig.cs
@@ -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);
+        }
+    }
+}
diff --git a/Main/Config/Configs/HeroTrialConfig.cs.meta b/Main/Config/Configs/HeroTrialConfig.cs.meta
new file mode 100644
index 0000000..aa0347c
--- /dev/null
+++ b/Main/Config/Configs/HeroTrialConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 86da543a944b1b94c8cfd3581f709bd8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/Config/PartialConfigs/HeroTrialConfig.cs b/Main/Config/PartialConfigs/HeroTrialConfig.cs
new file mode 100644
index 0000000..0894010
--- /dev/null
+++ b/Main/Config/PartialConfigs/HeroTrialConfig.cs
@@ -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;
+    }
+}
diff --git a/Main/Config/PartialConfigs/HeroTrialConfig.cs.meta b/Main/Config/PartialConfigs/HeroTrialConfig.cs.meta
new file mode 100644
index 0000000..994d636
--- /dev/null
+++ b/Main/Config/PartialConfigs/HeroTrialConfig.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8b0e016a7a404a9fa2d61f7327a1fb3f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs b/Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs
new file mode 100644
index 0000000..ef59fc6
--- /dev/null
+++ b/Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs
@@ -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);
+    }
+}
diff --git a/Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs.meta b/Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs.meta
new file mode 100644
index 0000000..8419bed
--- /dev/null
+++ b/Main/Core/NetworkPackage/DTCFile/ServerPack/HB2_ActionMap/DTCB203_tagSCHeroTrialInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0b6aa926e39b4ec4aa2d86f794c9ed8a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/Core/NetworkPackage/DataToCtl/PackageRegedit.cs b/Main/Core/NetworkPackage/DataToCtl/PackageRegedit.cs
index d5a1737..57c672d 100644
--- a/Main/Core/NetworkPackage/DataToCtl/PackageRegedit.cs
+++ b/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));
@@ -259,4 +260,4 @@
         }
         return null;
     }
-}
\ No newline at end of file
+}
diff --git a/Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs b/Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs
new file mode 100644
index 0000000..da3504d
--- /dev/null
+++ b/Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs
@@ -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;        // 鏈灏嗗凡杩囧叧鍒扮殑鍏冲崱缂栧彿
+    }
+
+}
diff --git a/Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs.meta b/Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs.meta
new file mode 100644
index 0000000..5a7b0a0
--- /dev/null
+++ b/Main/Core/NetworkPackage/ServerPack/HB2_ActionMap/HB203_tagSCHeroTrialInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c0adf674df224db49ab40b4853819da8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/Main.cs b/Main/Main.cs
index b6a8b0f..2d87679 100644
--- a/Main/Main.cs
+++ b/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)
         {
diff --git a/Main/System/Battle/BattleConst.cs b/Main/System/Battle/BattleConst.cs
index 8c83f47..c425101 100644
--- a/Main/System/Battle/BattleConst.cs
+++ b/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},
@@ -294,4 +299,4 @@
     public const int DodgeSoundID = 5999999; // 闂伩闊虫晥ID
 
     #endregion
-}
\ No newline at end of file
+}
diff --git a/Main/System/Battle/BattleField/HeroTrialBattleField.cs b/Main/System/Battle/BattleField/HeroTrialBattleField.cs
new file mode 100644
index 0000000..73ef1a5
--- /dev/null
+++ b/Main/System/Battle/BattleField/HeroTrialBattleField.cs
@@ -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;
+    }
+}
diff --git a/Main/System/Battle/BattleField/HeroTrialBattleField.cs.meta b/Main/System/Battle/BattleField/HeroTrialBattleField.cs.meta
new file mode 100644
index 0000000..ea30de5
--- /dev/null
+++ b/Main/System/Battle/BattleField/HeroTrialBattleField.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 51d9ddba2fa6f604497431ded91e9081
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Battle/BattleFieldFactory.cs b/Main/System/Battle/BattleFieldFactory.cs
index df43786..d62cb1c 100644
--- a/Main/System/Battle/BattleFieldFactory.cs
+++ b/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;
@@ -52,4 +55,4 @@
     }
 
 
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/ArenaTabHandler.cs b/Main/System/ChallengeTab/ArenaTabHandler.cs
index 72d02ee..16798f2 100644
--- a/Main/System/ChallengeTab/ArenaTabHandler.cs
+++ b/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;
@@ -63,4 +64,4 @@
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/BaseChallengeTabHandler.cs b/Main/System/ChallengeTab/BaseChallengeTabHandler.cs
index 04b67b8..49ba4ad 100644
--- a/Main/System/ChallengeTab/BaseChallengeTabHandler.cs
+++ b/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鍜孨ame锛�
+    /// 鑾峰彇Tab鍥炬爣Key
     /// </summary>
-    protected abstract int GetIndex();
+    protected abstract string GetIconKey();
+
+    /// <summary>
+    /// 鑾峰彇Tab鏄剧ず鍚嶇О
+    /// </summary>
+    protected abstract string GetNameText();
 
     /// <summary>
     /// 鑾峰彇寮�鍚柟寮� (0=FuncID, 1=娲诲姩)
@@ -105,4 +111,4 @@
     /// 鍙栨秷璁㈤槄姝ab鐗规湁鐨勪簨浠�
     /// </summary>
     protected abstract void UnsubscribeFromSpecificEvents();
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/BoneFieldTabHandler.cs b/Main/System/ChallengeTab/BoneFieldTabHandler.cs
index d3f48a6..388c1ad 100644
--- a/Main/System/ChallengeTab/BoneFieldTabHandler.cs
+++ b/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;
@@ -75,4 +76,4 @@
             Refresh();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/ChallengeTabButton.cs b/Main/System/ChallengeTab/ChallengeTabButton.cs
index 591620d..67282b2 100644
--- a/Main/System/ChallengeTab/ChallengeTabButton.cs
+++ b/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;
@@ -79,4 +79,4 @@
         // 瀛樺偍鐐瑰嚮鍥炶皟
         this._onClickAction = data.OnClickAction;
     }
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/HeroTrialTabHandler.cs b/Main/System/ChallengeTab/HeroTrialTabHandler.cs
new file mode 100644
index 0000000..0c83d3a
--- /dev/null
+++ b/Main/System/ChallengeTab/HeroTrialTabHandler.cs
@@ -0,0 +1,57 @@
+锘縰sing 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;
+    }
+}
diff --git a/Main/System/ChallengeTab/HeroTrialTabHandler.cs.meta b/Main/System/ChallengeTab/HeroTrialTabHandler.cs.meta
new file mode 100644
index 0000000..eae34bc
--- /dev/null
+++ b/Main/System/ChallengeTab/HeroTrialTabHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c410f4b7a26f00640b6cf94a2817d3c1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/ChallengeTab/QunyingTabHandler.cs b/Main/System/ChallengeTab/QunyingTabHandler.cs
index 289f9c8..210bcc2 100644
--- a/Main/System/ChallengeTab/QunyingTabHandler.cs
+++ b/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;
@@ -64,4 +65,4 @@
             Refresh();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/TianziBillboradTabHandler.cs b/Main/System/ChallengeTab/TianziBillboradTabHandler.cs
index a432e2a..192a7ac 100644
--- a/Main/System/ChallengeTab/TianziBillboradTabHandler.cs
+++ b/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;
@@ -72,4 +73,4 @@
             Refresh();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Main/System/ChallengeTab/WarlordPavilionTabHandler.cs b/Main/System/ChallengeTab/WarlordPavilionTabHandler.cs
index 2734406..d197859 100644
--- a/Main/System/ChallengeTab/WarlordPavilionTabHandler.cs
+++ b/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;
@@ -60,4 +61,4 @@
         Refresh();
     }
 
-}
\ No newline at end of file
+}
diff --git a/Main/System/HeroTrial.meta b/Main/System/HeroTrial.meta
new file mode 100644
index 0000000..a6a4d6e
--- /dev/null
+++ b/Main/System/HeroTrial.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4fe029ba0b48b2646908738defb90bca
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/CarouselView.cs b/Main/System/HeroTrial/CarouselView.cs
new file mode 100644
index 0000000..22b2726
--- /dev/null
+++ b/Main/System/HeroTrial/CarouselView.cs
@@ -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();
+    }
+}
diff --git a/Main/System/HeroTrial/CarouselView.cs.meta b/Main/System/HeroTrial/CarouselView.cs.meta
new file mode 100644
index 0000000..4aa11dd
--- /dev/null
+++ b/Main/System/HeroTrial/CarouselView.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 552093c27b834d40ae2245fc840daa24
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialBattleWin.cs b/Main/System/HeroTrial/HeroTrialBattleWin.cs
new file mode 100644
index 0000000..4e52036
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialBattleWin.cs
@@ -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("鎵句笉鍒癰oss");
+        }
+
+        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;
+
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialBattleWin.cs.meta b/Main/System/HeroTrial/HeroTrialBattleWin.cs.meta
new file mode 100644
index 0000000..2772d77
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialBattleWin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 25165ebda169aa7428e88278f78606b8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialCarouselView.cs b/Main/System/HeroTrial/HeroTrialCarouselView.cs
new file mode 100644
index 0000000..a2b086b
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialCarouselView.cs
@@ -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;
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialCarouselView.cs.meta b/Main/System/HeroTrial/HeroTrialCarouselView.cs.meta
new file mode 100644
index 0000000..475bfa0
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialCarouselView.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 97cf454b383948fcb6b584596e6ac35d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialCell.cs b/Main/System/HeroTrial/HeroTrialCell.cs
new file mode 100644
index 0000000..c493a33
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialCell.cs
@@ -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);
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialCell.cs.meta b/Main/System/HeroTrial/HeroTrialCell.cs.meta
new file mode 100644
index 0000000..b1fa72e
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialCell.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b102d9604f5b5f040873d38526654381
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialFailWin.cs b/Main/System/HeroTrial/HeroTrialFailWin.cs
new file mode 100644
index 0000000..603be9b
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialFailWin.cs
@@ -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);
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialFailWin.cs.meta b/Main/System/HeroTrial/HeroTrialFailWin.cs.meta
new file mode 100644
index 0000000..ef6e34c
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialFailWin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: db691e748d974d246aecac137f9b0035
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialHeroItem.cs b/Main/System/HeroTrial/HeroTrialHeroItem.cs
new file mode 100644
index 0000000..ab8e637
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialHeroItem.cs
@@ -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);
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialHeroItem.cs.meta b/Main/System/HeroTrial/HeroTrialHeroItem.cs.meta
new file mode 100644
index 0000000..943e4bd
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialHeroItem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3bb421ee0c4449d1a1f06efbc03bc29a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialManager.cs b/Main/System/HeroTrial/HeroTrialManager.cs
new file mode 100644
index 0000000..1add955
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialManager.cs
@@ -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>鏁版嵁鏇存柊浜嬩欢锛孶I璁㈤槄鍒锋柊</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. 浠婂ぉ娌℃湁鏂板紑鏀炬彁绀洪摼璺笖鏅�氱孩鐐规湭琚寫鎴樻秷璐规椂锛屾樉绀烘墍鏈夊凡瑙i攣涓旀湁鍙寫鎴樺叧鍗$殑姝﹀皢閲岋紝褰撳墠鍙寫鎴樺叧鍗℃垬鍔涙渶浣庣殑閭d釜銆�
+        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. 鎵�鏈夊凡瑙i攣涓斿彲鎸戞垬鍏冲崱涓垬鍔涙渶浣庣殑姝﹀皢銆�
+    /// </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();
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialManager.cs.meta b/Main/System/HeroTrial/HeroTrialManager.cs.meta
new file mode 100644
index 0000000..98890e0
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 53a5e2b3e306f7e4eac9cb9750168c6a
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialVictoryWin.cs b/Main/System/HeroTrial/HeroTrialVictoryWin.cs
new file mode 100644
index 0000000..f8e3571
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialVictoryWin.cs
@@ -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);
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialVictoryWin.cs.meta b/Main/System/HeroTrial/HeroTrialVictoryWin.cs.meta
new file mode 100644
index 0000000..72e2eac
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialVictoryWin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 35108f3aae26e9e459e60e4368a648f3
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/HeroTrial/HeroTrialWin.cs b/Main/System/HeroTrial/HeroTrialWin.cs
new file mode 100644
index 0000000..da57a92
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialWin.cs
@@ -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>
+    /// 鏍规嵁褰撳墠閫変腑鐨勬灏嗭紝鍒锋柊涓嬫柟鐨勫叧鍗croll
+    /// </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);
+    }
+}
diff --git a/Main/System/HeroTrial/HeroTrialWin.cs.meta b/Main/System/HeroTrial/HeroTrialWin.cs.meta
new file mode 100644
index 0000000..91b7f09
--- /dev/null
+++ b/Main/System/HeroTrial/HeroTrialWin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d6e3215d80964334f807ea63e0a306b8
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Main/System/Redpoint/MainRedDot.cs b/Main/System/Redpoint/MainRedDot.cs
index aa9e5aa..ed6ecab 100644
--- a/Main/System/Redpoint/MainRedDot.cs
+++ b/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()
     {
 
diff --git a/Main/System/Settlement/BattleSettlementManager.cs b/Main/System/Settlement/BattleSettlementManager.cs
index 48325ca..73f544d 100644
--- a/Main/System/Settlement/BattleSettlementManager.cs
+++ b/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 @@
             // 鏁屾柟鍚嶅瓧鏄疊oss鍚嶅瓧
             case BattleConst.StoryBossBattleField:
             case BattleConst.BoneBattleField:
+            case BattleConst.HeroTrialBattleField:
             case BattleConst.TianziBillboradBattleField:
             case BattleConst.WarlordPavilionBattleField:
                 BattleObject bossBattleObject = battleField.FindBoss();
@@ -488,4 +502,4 @@
 {
     public int ItemID;
     public int Count;
-}
\ No newline at end of file
+}
diff --git a/Main/System/WarlordPavilion/TowerBaseWin.cs b/Main/System/WarlordPavilion/TowerBaseWin.cs
index 40e5ab0..74840fd 100644
--- a/Main/System/WarlordPavilion/TowerBaseWin.cs
+++ b/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;
diff --git a/Main/Utility/EnumHelper.cs b/Main/Utility/EnumHelper.cs
index 3415f88..83f3962 100644
--- a/Main/Utility/EnumHelper.cs
+++ b/Main/Utility/EnumHelper.cs
@@ -862,6 +862,7 @@
     FestivalActivity = 65,//鑺傛棩娲诲姩
     HeroSkinFlashSale = 66,//鏃惰鐗瑰崠
     GuildAtkDefBat = 67,//鍏細鏀婚槻鎴�
+    HeroTrial = 68,//姝﹀皢璇曠偧
 }
 
 

--
Gitblit v1.8.0