| | |
| | | int challengeRecordDay; |
| | | // 当天发起过挑战的武将不再显示红点,胜负无关。 |
| | | HashSet<int> challengedHeroIDsToday = new HashSet<int>(); |
| | | // 当天新开放的可提示武将,优先于普通候选排队顺延。 |
| | | List<int> pendingNewRedHeroIDsToday = new List<int>(); |
| | | // 当天新开放过的武将副本。这里保存“发生过新开放”的事实,待显示队列每次刷新时从它派生,避免持久化当前红点/队列进度。 |
| | | List<int> newOpenedHeroIDsToday = new List<int>(); |
| | | // 历史上客户端已经见过的可挑战武将副本,用于识别当天因等级/官职达成而新解锁的副本。 |
| | | HashSet<int> seenUnlockedHeroIDs = new HashSet<int>(); |
| | | int currentRedHeroID; |
| | | bool isChallengeRecordLoaded; |
| | | bool needSeedSeenUnlockedHeroIDs; |
| | | bool isRefreshingByPlayerProgress; |
| | | int lastObservedPlayerLV = -1; |
| | | int lastObservedRealmLevel = -1; |
| | | |
| | | 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()); } } |
| | | string newOpenedHeroIDsTodayKey { get { return StringUtility.Concat("HeroTrial_NewOpenedHeroIDsToday_", PlayerDatas.Instance.PlayerId.ToString()); } } |
| | | string seenUnlockedHeroIDsKey { get { return StringUtility.Concat("HeroTrial_SeenUnlockedHeroIDs_", PlayerDatas.Instance.PlayerId.ToString()); } } |
| | | |
| | | /// <summary>数据更新事件,UI订阅刷新</summary> |
| | | public event Action OnUpdateHeroTrialInfoEvent; |
| | |
| | | if (type != PlayerDataType.LV && type != PlayerDataType.RealmLevel) |
| | | return; |
| | | |
| | | RefreshHeroTrialState(); |
| | | isRefreshingByPlayerProgress = true; |
| | | try |
| | | { |
| | | RefreshHeroTrialState(); |
| | | } |
| | | finally |
| | | { |
| | | isRefreshingByPlayerProgress = false; |
| | | } |
| | | } |
| | | |
| | | void OnServerOpenDayRefresh() |
| | |
| | | EnsureTodayChallengeRecord(); |
| | | RefreshCurrentRedHeroID(); |
| | | UpdateRedPoint(); |
| | | RecordObservedPlayerProgress(); |
| | | OnUpdateHeroTrialInfoEvent?.Invoke(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | // 红点规则: |
| | | // 1. 当天新开放的武将优先显示,并按配置顺序顺延。 |
| | | // 2. 当天新开放队列被挑战完后,当天不再回流到旧武将副本。 |
| | | // 3. 今天没有新开放提示链路且普通红点未被挑战消费时,显示所有已解锁且有可挑战关卡的武将里,当前可挑战关卡战力最低的那个。 |
| | | // 1. 当天新开放武将优先,按配置顺序逐个提示;新开放来源包含开服天、等级、官职。 |
| | | // 2. 只保存“今天出现过哪些新开放武将”,当前显示目标每次刷新实时派生,避免本地存储队列进度。 |
| | | // 3. 今天出现过新开放武将后,队列耗尽也不回流旧武将;完全没有新开放时,旧武将一天只提示一次最低战力目标。 |
| | | List<int> heroIDList = GetHeroTrialHeroIDList(); |
| | | bool onlyProgressUnlocksCanBecomeNew = needSeedSeenUnlockedHeroIDs && isRefreshingByPlayerProgress; |
| | | if (needSeedSeenUnlockedHeroIDs && !onlyProgressUnlocksCanBecomeNew) |
| | | { |
| | | SeedSeenUnlockedHeroIDs(heroIDList); |
| | | needSeedSeenUnlockedHeroIDs = false; |
| | | isDirty = true; |
| | | } |
| | | |
| | | if (DetectNewOpenedHeroIDsToday(heroIDList, onlyProgressUnlocksCanBecomeNew)) |
| | | isDirty = true; |
| | | |
| | | if (onlyProgressUnlocksCanBecomeNew) |
| | | { |
| | | needSeedSeenUnlockedHeroIDs = false; |
| | | isDirty = true; |
| | | } |
| | | |
| | | 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)) |
| | | int nextNewOpenedHeroID = GetNextNewOpenedRedHeroID(heroIDList); |
| | | if (nextNewOpenedHeroID != 0) |
| | | { |
| | | isDirty = true; |
| | | if (currentRedHeroID != nextNewOpenedHeroID) |
| | | { |
| | | currentRedHeroID = nextNewOpenedHeroID; |
| | | isDirty = true; |
| | | } |
| | | } |
| | | else if (HasChallengedTodayOpenHeroTrialToday() && |
| | | pendingNewRedHeroIDsToday.Count == 0 && |
| | | (currentRedHeroID == 0 || !IsHeroTrialHeroOpenToday(currentRedHeroID))) |
| | | else if (newOpenedHeroIDsToday.Count > 0) |
| | | { |
| | | if (currentRedHeroID != 0) |
| | | { |
| | |
| | | isDirty = true; |
| | | } |
| | | } |
| | | else if (!normalRedConsumedToday && (currentRedHeroID == 0 || !IsHeroTrialHeroOpenToday(currentRedHeroID))) |
| | | else if (!normalRedConsumedToday) |
| | | { |
| | | int bestHeroID = GetLowestCanChallengeHeroID(heroIDList); |
| | | if (currentRedHeroID != bestHeroID) |
| | |
| | | SaveTodayChallengeRecord(); |
| | | } |
| | | |
| | | List<int> BuildCandidateHeroIDs(List<int> sortedHeroIDList) |
| | | bool DetectNewOpenedHeroIDsToday(List<int> sortedHeroIDList, bool onlyProgressUnlocksCanBecomeNew) |
| | | { |
| | | List<int> candidateHeroIDs = new List<int>(); |
| | | bool isDirty = false; |
| | | for (int i = 0; i < sortedHeroIDList.Count; i++) |
| | | { |
| | | int heroID = sortedHeroIDList[i]; |
| | | if (!CanShowHeroItemRed(heroID)) |
| | | if (!IsVisibleUnlockedHeroTrial(heroID)) |
| | | continue; |
| | | candidateHeroIDs.Add(heroID); |
| | | |
| | | bool unlockedByPlayerProgressNow = isRefreshingByPlayerProgress && IsHeroTrialHeroUnlockedByPlayerProgressNow(heroID); |
| | | // 首次见到一个已解锁且有可挑战关卡的武将,说明它刚由等级/官职条件进入可提示范围。 |
| | | if (seenUnlockedHeroIDs.Add(heroID)) |
| | | { |
| | | if (!onlyProgressUnlocksCanBecomeNew || unlockedByPlayerProgressNow) |
| | | AppendNewOpenedHeroIDToday(heroID); |
| | | isDirty = true; |
| | | } |
| | | else if (unlockedByPlayerProgressNow && AppendNewOpenedHeroIDToday(heroID)) |
| | | { |
| | | // 兼容旧本地记录:若历史已见集合曾误记该武将,本次明确跨过等级/官职门槛时仍按新开放处理。 |
| | | isDirty = true; |
| | | } |
| | | |
| | | // 开服天新开放不依赖历史已见集合;当天首次刷新就应进入新开放优先队列。 |
| | | if (IsHeroTrialHeroOpenByServerDayToday(heroID) && AppendNewOpenedHeroIDToday(heroID)) |
| | | isDirty = true; |
| | | } |
| | | return candidateHeroIDs; |
| | | |
| | | return isDirty; |
| | | } |
| | | |
| | | List<int> BuildTodayOpenHeroIDs(List<int> candidateHeroIDs) |
| | | bool IsHeroTrialHeroUnlockedByPlayerProgressNow(int heroID) |
| | | { |
| | | 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; |
| | | if (!HeroTrialConfig.TryGetFirstLevelConfig(heroID, out var config)) |
| | | return false; |
| | | |
| | | int currentLV = PlayerDatas.Instance.baseData.LV; |
| | | int currentRealmLevel = PlayerDatas.Instance.baseData.realmLevel; |
| | | bool levelJustReached = config.LVLimit > 0 && currentLV >= config.LVLimit && |
| | | ((lastObservedPlayerLV >= 0 && lastObservedPlayerLV < config.LVLimit) || |
| | | (lastObservedPlayerLV < 0 && currentLV == config.LVLimit)); |
| | | bool realmJustReached = config.RealmLimit > 0 && currentRealmLevel >= config.RealmLimit && |
| | | ((lastObservedRealmLevel >= 0 && lastObservedRealmLevel < config.RealmLimit) || |
| | | (lastObservedRealmLevel < 0 && currentRealmLevel == config.RealmLimit)); |
| | | |
| | | return levelJustReached || realmJustReached; |
| | | } |
| | | |
| | | bool HasChallengedTodayOpenHeroTrialToday() |
| | | void RecordObservedPlayerProgress() |
| | | { |
| | | foreach (int heroID in challengedHeroIDsToday) |
| | | { |
| | | if (IsHeroTrialHeroOpenToday(heroID)) |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | lastObservedPlayerLV = PlayerDatas.Instance.baseData.LV; |
| | | lastObservedRealmLevel = PlayerDatas.Instance.baseData.realmLevel; |
| | | } |
| | | |
| | | bool IsHeroTrialHeroOpenToday(int heroID) |
| | | void SeedSeenUnlockedHeroIDs(List<int> sortedHeroIDList) |
| | | { |
| | | // 没有历史已见记录时,先把当前可见副本作为旧状态记住,避免版本升级后把所有旧副本误判成当天新开放。 |
| | | for (int i = 0; i < sortedHeroIDList.Count; i++) |
| | | { |
| | | int heroID = sortedHeroIDList[i]; |
| | | if (IsVisibleUnlockedHeroTrial(heroID)) |
| | | seenUnlockedHeroIDs.Add(heroID); |
| | | } |
| | | } |
| | | |
| | | bool IsVisibleUnlockedHeroTrial(int heroID) |
| | | { |
| | | return IsHeroTrialHeroUnlocked(heroID) && HasChallengeableLevel(heroID); |
| | | } |
| | | |
| | | bool AppendNewOpenedHeroIDToday(int heroID) |
| | | { |
| | | if (heroID == 0 || newOpenedHeroIDsToday.Contains(heroID)) |
| | | return false; |
| | | |
| | | newOpenedHeroIDsToday.Add(heroID); |
| | | return true; |
| | | } |
| | | |
| | | bool IsHeroTrialHeroOpenByServerDayToday(int heroID) |
| | | { |
| | | HeroConfig heroConfig = HeroConfig.Get(heroID); |
| | | return heroConfig != null && |
| | |
| | | heroConfig.OpenCollectionDay == TimeUtility.OpenDay + 1; |
| | | } |
| | | |
| | | void RemoveHeroIDs(List<int> heroIDs, List<int> removeHeroIDs) |
| | | int GetNextNewOpenedRedHeroID(List<int> sortedHeroIDList) |
| | | { |
| | | if (removeHeroIDs.IsNullOrEmpty()) |
| | | return; |
| | | |
| | | for (int i = heroIDs.Count - 1; i >= 0; i--) |
| | | for (int i = 0; i < sortedHeroIDList.Count; 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; |
| | | int heroID = sortedHeroIDList[i]; |
| | | if (newOpenedHeroIDsToday.Contains(heroID) && CanShowHeroItemRed(heroID)) |
| | | return heroID; |
| | | } |
| | | |
| | | 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; |
| | | return 0; |
| | | } |
| | | |
| | | int GetLowestCanChallengeHeroID(List<int> heroIDList) |
| | |
| | | } |
| | | |
| | | 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> |
| | |
| | | lastChallengeHeroIDToday = config.HeroID; |
| | | SaveTodayChallengeRecord(); |
| | | } |
| | | bool isDirty = pendingNewRedHeroIDsToday.Remove(config.HeroID); |
| | | bool isDirty = false; |
| | | if (currentRedHeroID == config.HeroID) |
| | | { |
| | | if (!IsHeroTrialHeroOpenToday(config.HeroID)) |
| | | if (!newOpenedHeroIDsToday.Contains(config.HeroID)) |
| | | normalRedConsumedToday = true; |
| | | |
| | | currentRedHeroID = 0; |
| | |
| | | |
| | | challengeRecordDay = today; |
| | | challengedHeroIDsToday.Clear(); |
| | | newOpenedHeroIDsToday.Clear(); |
| | | lastChallengeHeroIDToday = 0; |
| | | normalRedConsumedToday = false; |
| | | pendingNewRedHeroIDsToday.Clear(); |
| | | currentRedHeroID = 0; |
| | | SaveTodayChallengeRecord(); |
| | | } |
| | |
| | | { |
| | | challengeRecordDay = 0; |
| | | challengedHeroIDsToday.Clear(); |
| | | newOpenedHeroIDsToday.Clear(); |
| | | seenUnlockedHeroIDs.Clear(); |
| | | lastChallengeHeroIDToday = 0; |
| | | normalRedConsumedToday = false; |
| | | pendingNewRedHeroIDsToday.Clear(); |
| | | currentRedHeroID = 0; |
| | | isChallengeRecordLoaded = false; |
| | | needSeedSeenUnlockedHeroIDs = false; |
| | | isRefreshingByPlayerProgress = false; |
| | | lastObservedPlayerLV = -1; |
| | | lastObservedRealmLevel = -1; |
| | | } |
| | | |
| | | void LoadTodayChallengeRecord() |
| | | { |
| | | isChallengeRecordLoaded = true; |
| | | challengeRecordDay = LocalSave.GetInt(challengeRecordDayKey); |
| | | needSeedSeenUnlockedHeroIDs = !LoadHeroIDSet(seenUnlockedHeroIDsKey, seenUnlockedHeroIDs); |
| | | if (challengeRecordDay != GetTodayID()) |
| | | { |
| | | challengeRecordDay = GetTodayID(); |
| | | challengedHeroIDsToday.Clear(); |
| | | newOpenedHeroIDsToday.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); |
| | | LoadHeroIDList(newOpenedHeroIDsTodayKey, newOpenedHeroIDsToday); |
| | | currentRedHeroID = 0; |
| | | } |
| | | |
| | | void SaveTodayChallengeRecord() |
| | |
| | | LocalSave.SetIntArray(challengedHeroIDsTodayKey, ToArray(challengedHeroIDsToday)); |
| | | LocalSave.SetInt(lastChallengeHeroIDTodayKey, lastChallengeHeroIDToday); |
| | | LocalSave.SetInt(normalRedConsumedTodayKey, normalRedConsumedToday ? 1 : 0); |
| | | LocalSave.SetIntArray(pendingNewRedHeroIDsTodayKey, pendingNewRedHeroIDsToday.ToArray()); |
| | | LocalSave.SetInt(currentRedHeroIDKey, currentRedHeroID); |
| | | LocalSave.SetIntArray(newOpenedHeroIDsTodayKey, newOpenedHeroIDsToday.ToArray()); |
| | | LocalSave.SetIntArray(seenUnlockedHeroIDsKey, ToArray(seenUnlockedHeroIDs)); |
| | | } |
| | | |
| | | void LoadHeroIDSet(string key, HashSet<int> heroIDSet) |
| | | bool LoadHeroIDSet(string key, HashSet<int> heroIDSet) |
| | | { |
| | | heroIDSet.Clear(); |
| | | int[] heroIDs = LocalSave.GetIntArray(key); |
| | | if (heroIDs == null) |
| | | return; |
| | | return false; |
| | | |
| | | for (int i = 0; i < heroIDs.Length; i++) |
| | | { |
| | | if (heroIDs[i] != 0) |
| | | heroIDSet.Add(heroIDs[i]); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | void LoadHeroIDList(string key, List<int> heroIDList) |