From 333f7297a871443a47201d92cf77fb99a47b5a41 Mon Sep 17 00:00:00 2001 From: aimerfan Date: Sun, 10 May 2026 00:32:14 +0800 Subject: [PATCH] Add quest completion notifier Add a notifier that reports when effective quest progress reaches completion through the existing tracker priority order. Hook the evaluator into QuestTracker and legacy QuestProgress update paths, and notification settings. --- .../Data/Quest/QuestCompletionEvaluator.cs | 94 +++++++++++++++++++ .../Data/Quest/QuestProgressManager.cs | 13 ++- .../Notifier/NotifierManager.cs | 4 + .../Notifier/NotifierQuestCompletion.cs | 23 +++++ .../Notifier/NotifierRes.en.resx | 6 ++ ElectronicObserver/Notifier/NotifierRes.resx | 6 ++ .../Notifier/NotifierRes.zh-TW.resx | 6 ++ .../Dialog/ConfigurationResources.en.resx | 3 + .../Window/Dialog/ConfigurationResources.resx | 3 + .../Dialog/ConfigurationResources.zh-TW.resx | 3 + ElectronicObserver/Utility/Configuration.cs | 4 + .../QuestTrackerManagerBase.cs | 18 ++++ ...urationNotificationTranslationViewModel.cs | 1 + .../ConfigurationNotificationUserControl.xaml | 7 ++ .../ConfigurationNotificationViewModel.cs | 2 + 15 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 ElectronicObserver/Data/Quest/QuestCompletionEvaluator.cs create mode 100644 ElectronicObserver/Notifier/NotifierQuestCompletion.cs diff --git a/ElectronicObserver/Data/Quest/QuestCompletionEvaluator.cs b/ElectronicObserver/Data/Quest/QuestCompletionEvaluator.cs new file mode 100644 index 000000000..7e0a5711f --- /dev/null +++ b/ElectronicObserver/Data/Quest/QuestCompletionEvaluator.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ElectronicObserver.Window.Dialog.QuestTrackerManager.ViewModels; + +namespace ElectronicObserver.Data.Quest; + +public sealed class QuestCompletionEvaluator +{ + private static readonly QuestCompletionEvaluator instance = new(); + + public static QuestCompletionEvaluator Instance => instance; + + // Completion baseline keyed by quest ID from the last refresh or evaluation. + private Dictionary CompletionStates { get; } = new(); + + // Raised when an active quest transitions from incomplete to complete. + public event Action QuestCompleted = delegate { }; + + private QuestCompletionEvaluator() + { + } + + /// + /// Refreshes the baseline from the current completion states without emitting completion notifications. + /// + public void RefreshBaseline() + { + KCDatabase db = KCDatabase.Instance; + + // Snapshot completion states for all currently active quests. + foreach (QuestData quest in db.Quest.Quests.Values) + { + CompletionStates[quest.QuestID] = IsComplete(quest); + } + + // Remove completion states for quests that are no longer active. + foreach (int questId in CompletionStates.Keys.Except(db.Quest.Quests.Keys).ToList()) + { + CompletionStates.Remove(questId); + } + } + + /// + /// Compares the current completion states with the baseline and emits notifications for newly completed quests. + /// + public void Evaluate() + { + KCDatabase db = KCDatabase.Instance; + + // Check active quests for a transition from incomplete to complete. + foreach (QuestData quest in db.Quest.Quests.Values) + { + bool isComplete = IsComplete(quest); + + if (CompletionStates.TryGetValue(quest.QuestID, out bool wasComplete) && !wasComplete && isComplete) + { + QuestCompleted(quest); + } + + CompletionStates[quest.QuestID] = isComplete; + } + + // Remove completion states for quests that are no longer active. + foreach (int questId in CompletionStates.Keys.Except(db.Quest.Quests.Keys).ToList()) + { + CompletionStates.Remove(questId); + } + } + + private static bool IsComplete(QuestData quest) => GetEffectiveProgress(quest) >= 1.0; + + private static double GetEffectiveProgress(QuestData quest) + { + // Keep this priority aligned with the quest list progress display in Window/Wpf/Quest/QuestViewModel.cs. + // It uses the most precise tracker first, then falls back to legacy QuestProgress and API progress flags. + if (quest.State == 3) return 1.0; + + KCDatabase db = KCDatabase.Instance; + TrackerViewModel? tracker = db.QuestTrackerManagers.GetTrackerById(quest.QuestID); + TrackerViewModel? systemTracker = db.SystemQuestTrackerManager.GetTrackerById(quest.QuestID); + + if (tracker is { Tasks.Count: > 0 }) return tracker.Progress; + if (systemTracker is { Tasks.Count: > 0 }) return systemTracker.Progress; + if (db.QuestProgress.Progresses.ContainsKey(quest.QuestID)) return db.QuestProgress[quest.QuestID].ProgressPercentage; + + return quest.Progress switch + { + 1 => 0.5, + 2 => 0.8, + _ => 0.0, + }; + } +} diff --git a/ElectronicObserver/Data/Quest/QuestProgressManager.cs b/ElectronicObserver/Data/Quest/QuestProgressManager.cs index e2873ea5c..647f6c5b2 100644 --- a/ElectronicObserver/Data/Quest/QuestProgressManager.cs +++ b/ElectronicObserver/Data/Quest/QuestProgressManager.cs @@ -1268,7 +1268,7 @@ void QuestUpdated(string apiname, dynamic data) } LastUpdateTime = DateTime.Now; - OnProgressChanged(); + OnProgressChanged(false); } @@ -1522,8 +1522,17 @@ public void Save() Save(DefaultFilePath); } - private void OnProgressChanged() + private void OnProgressChanged(bool evaluateCompletion = true) { + if (evaluateCompletion) + { + QuestCompletionEvaluator.Instance.Evaluate(); + } + else + { + QuestCompletionEvaluator.Instance.RefreshBaseline(); + } + KCDatabase.Instance.Quest.OnQuestUpdated(); } } diff --git a/ElectronicObserver/Notifier/NotifierManager.cs b/ElectronicObserver/Notifier/NotifierManager.cs index 758f11f0b..b622db8ed 100644 --- a/ElectronicObserver/Notifier/NotifierManager.cs +++ b/ElectronicObserver/Notifier/NotifierManager.cs @@ -30,6 +30,7 @@ public sealed class NotifierManager public NotifierBattleEnd BattleEnd { get; private set; } public NotifierRemodelLevel RemodelLevel { get; private set; } public NotifierBase TrainingPlan { get; private set; } + public NotifierQuestCompletion QuestCompletion { get; private set; } private NotifierManager() { @@ -54,6 +55,7 @@ public void Initialize(object parent) BattleEnd = new NotifierBattleEnd(c.NotifierBattleEnd); RemodelLevel = new NotifierRemodelLevel(c.NotifierRemodelLevel); TrainingPlan = new NotifierTrainingPlan(c.NotifierTrainingPlan); + QuestCompletion = new NotifierQuestCompletion(c.NotifierQuestCompletion); } public void ApplyToConfiguration() @@ -72,6 +74,7 @@ public void ApplyToConfiguration() BattleEnd.ApplyToConfiguration(c.NotifierBattleEnd); RemodelLevel.ApplyToConfiguration(c.NotifierRemodelLevel); TrainingPlan.ApplyToConfiguration(c.NotifierTrainingPlan); + QuestCompletion.ApplyToConfiguration(c.NotifierQuestCompletion); } public void ShowNotifier(ElectronicObserver.Window.Dialog.DialogNotifier form) @@ -110,6 +113,7 @@ public IEnumerable GetNotifiers() yield return BattleEnd; yield return RemodelLevel; yield return TrainingPlan; + yield return QuestCompletion; } } diff --git a/ElectronicObserver/Notifier/NotifierQuestCompletion.cs b/ElectronicObserver/Notifier/NotifierQuestCompletion.cs new file mode 100644 index 000000000..f24ad1628 --- /dev/null +++ b/ElectronicObserver/Notifier/NotifierQuestCompletion.cs @@ -0,0 +1,23 @@ +using ElectronicObserver.Data; +using ElectronicObserver.Data.Quest; + +namespace ElectronicObserver.Notifier; + +public class NotifierQuestCompletion : NotifierBase +{ + public NotifierQuestCompletion(Utility.Configuration.ConfigurationData.ConfigNotifierBase config) + : base(config) + { + DialogData.Title = NotifierRes.QuestCompletionTitle; + + QuestCompletionEvaluator.Instance.QuestCompleted += Notify; + QuestCompletionEvaluator.Instance.RefreshBaseline(); + } + + private void Notify(QuestData quest) + { + DialogData.Message = string.Format(NotifierRes.QuestCompletionText, quest.NameWithCode); + + base.Notify(); + } +} diff --git a/ElectronicObserver/Notifier/NotifierRes.en.resx b/ElectronicObserver/Notifier/NotifierRes.en.resx index d4e7924cb..db3d6cd26 100644 --- a/ElectronicObserver/Notifier/NotifierRes.en.resx +++ b/ElectronicObserver/Notifier/NotifierRes.en.resx @@ -228,4 +228,10 @@ Home port supply fleet {0} will return from expedition in {1}. + + Quest completion + + + Quest "{0}" is ready to complete. + \ No newline at end of file diff --git a/ElectronicObserver/Notifier/NotifierRes.resx b/ElectronicObserver/Notifier/NotifierRes.resx index 7fb9a9e69..1c0592a1c 100644 --- a/ElectronicObserver/Notifier/NotifierRes.resx +++ b/ElectronicObserver/Notifier/NotifierRes.resx @@ -231,4 +231,10 @@ 母港給糧艦隊「{0}」が遠征からあと{1}で帰投します。 + + 任務達成通知 + + + 任務『{0}』の達成条件を満たしました。 + \ No newline at end of file diff --git a/ElectronicObserver/Notifier/NotifierRes.zh-TW.resx b/ElectronicObserver/Notifier/NotifierRes.zh-TW.resx index 1dae6133b..5a55c66a1 100644 --- a/ElectronicObserver/Notifier/NotifierRes.zh-TW.resx +++ b/ElectronicObserver/Notifier/NotifierRes.zh-TW.resx @@ -231,4 +231,10 @@ 母港給糧艦隊「{0}」已經從遠征 {1} 返回。 + + 任務達成通知 + + + 任務「{0}」已達成完成條件。 + \ No newline at end of file diff --git a/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.en.resx b/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.en.resx index d68ef3446..c1507a3d5 100644 --- a/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.en.resx +++ b/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.en.resx @@ -848,4 +848,7 @@ When enabled, the preview will be ignored and all compositions will be shown. Chinese (Traditional) + + Quest completion notification + diff --git a/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.resx b/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.resx index 346e6ad59..83e322d80 100644 --- a/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.resx +++ b/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.resx @@ -863,4 +863,7 @@ APIリストの書式や用法はオンラインヘルプを参照してくだ 繁体中国語 + + 任務達成通知 + diff --git a/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.zh-TW.resx b/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.zh-TW.resx index 6443d7858..03f7274d6 100644 --- a/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.zh-TW.resx +++ b/ElectronicObserver/Properties/Window/Dialog/ConfigurationResources.zh-TW.resx @@ -856,4 +856,7 @@ API列表的格式和使用方法請參見線上幫助。 繁體中文 + + 任務達成通知 + diff --git a/ElectronicObserver/Utility/Configuration.cs b/ElectronicObserver/Utility/Configuration.cs index ebf90d8d8..d1121bbc5 100644 --- a/ElectronicObserver/Utility/Configuration.cs +++ b/ElectronicObserver/Utility/Configuration.cs @@ -1874,6 +1874,9 @@ public ConfigNotifierBattleEnd() [DataMember] public ConfigNotifierBase NotifierTrainingPlan { get; private set; } + [DataMember] + public ConfigNotifierBase NotifierQuestCompletion { get; private set; } + /// /// SyncBGMPlayer の設定を扱います。 /// @@ -2033,6 +2036,7 @@ public override void Initialize() NotifierBattleEnd = new ConfigNotifierBattleEnd(); NotifierRemodelLevel = new ConfigNotifierBase(); NotifierTrainingPlan = new ConfigNotifierBase(); + NotifierQuestCompletion = new ConfigNotifierBase(); BGMPlayer = new ConfigBGMPlayer(); FleetImageGenerator = new ConfigFleetImageGenerator(); diff --git a/ElectronicObserver/Window/Dialog/QuestTrackerManager/QuestTrackerManagerBase.cs b/ElectronicObserver/Window/Dialog/QuestTrackerManager/QuestTrackerManagerBase.cs index 73fa36c7e..931c6189e 100644 --- a/ElectronicObserver/Window/Dialog/QuestTrackerManager/QuestTrackerManagerBase.cs +++ b/ElectronicObserver/Window/Dialog/QuestTrackerManager/QuestTrackerManagerBase.cs @@ -7,6 +7,7 @@ using ElectronicObserver.Core.Types; using ElectronicObserver.Core.Types.Quests; using ElectronicObserver.Data; +using ElectronicObserver.Data.Quest; using ElectronicObserver.Observer; using ElectronicObserver.Utility.Mathematics; using ElectronicObserver.Window.Dialog.QuestTrackerManager.Models; @@ -108,6 +109,7 @@ private void QuestUpdated(string apiname, dynamic data) } LastQuestListUpdate = DateTime.Now; + QuestCompletionEvaluator.Instance.RefreshBaseline(); } private void StartSortie(string apiname, dynamic data) @@ -122,6 +124,8 @@ private void StartSortie(string apiname, dynamic data) { tracker.Increment(fleet, compass.MapAreaID, compass.MapInfoID, compass.CellId); } + + QuestCompletionEvaluator.Instance.Evaluate(); } private void NextSortie(string apiname, dynamic data) @@ -136,6 +140,8 @@ private void NextSortie(string apiname, dynamic data) { tracker.Increment(fleet, compass.MapAreaID, compass.MapInfoID, compass.CellId); } + + QuestCompletionEvaluator.Instance.Evaluate(); } private void BattleFinished(string apiname, dynamic data) @@ -152,6 +158,8 @@ private void BattleFinished(string apiname, dynamic data) { tracker.Increment(fleet, bm.Result.Rank, bm.Compass.MapAreaID, bm.Compass.MapInfoID, bm.Compass.CellId); } + + QuestCompletionEvaluator.Instance.Evaluate(); } private void BossBattleFinished(string apiname, dynamic data) @@ -170,6 +178,8 @@ private void BossBattleFinished(string apiname, dynamic data) tracker.Increment(fleet, bm.Result.Rank, bm.Compass.MapAreaID, bm.Compass.MapInfoID); } + QuestCompletionEvaluator.Instance.Evaluate(); + // p.Increment(bm.Result.Rank, bm.Compass.MapAreaID * 10 + bm.Compass.MapInfoID, bm.Compass.EventID == 5); } @@ -190,6 +200,8 @@ private void MapClearedFirstTime(string apiname, dynamic data) { tracker.Increment(fleet, bm.Compass.MapAreaID, bm.Compass.MapInfoID); } + + QuestCompletionEvaluator.Instance.Evaluate(); } private void PracticeFinished(string apiname, dynamic data) @@ -203,6 +215,8 @@ private void PracticeFinished(string apiname, dynamic data) { tracker.Increment(fleet, (string)data.api_win_rank); } + + QuestCompletionEvaluator.Instance.Evaluate(); } private void ExpeditionCompleted(string apiname, dynamic data) @@ -221,6 +235,8 @@ private void ExpeditionCompleted(string apiname, dynamic data) { tracker.Increment(fleet, areaId); } + + QuestCompletionEvaluator.Instance.Evaluate(); } public void EquipmentDiscarded(string apiname, Dictionary data) @@ -248,6 +264,8 @@ public void EquipmentDiscarded(string apiname, Dictionary data) { tracker.Increment(discardedEquipment.Select(e => e.MasterEquipment.IconTypeTyped)); } + + QuestCompletionEvaluator.Instance.Evaluate(); } public abstract void Save(); diff --git a/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationTranslationViewModel.cs b/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationTranslationViewModel.cs index 35f9d36db..96511970e 100644 --- a/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationTranslationViewModel.cs +++ b/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationTranslationViewModel.cs @@ -17,6 +17,7 @@ public class ConfigurationNotificationTranslationViewModel : TranslationBaseView public string Notification_Expedition => ConfigRes.ExpedReturn + ConfigRes.NotificationSetting; public string Notification_RemodelLevel => ConfigurationResources.Notification_RemodelLevel; public string Notification_Training => ConfigurationResources.Notification_Training; + public string Notification_QuestCompletion => ConfigurationResources.Notification_QuestCompletion; public string Notification_BaseAirCorps => ConfigurationResources.Notification_BaseAirCorps; public string Notification_BattleEnd => ConfigurationResources.Notification_BattleEnd; public string Notification_HomePortSupply => NotifierRes.HomePortSupply + ConfigRes.NotificationSetting; diff --git a/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationUserControl.xaml b/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationUserControl.xaml index 0b3218820..2327df638 100644 --- a/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationUserControl.xaml +++ b/ElectronicObserver/Window/Settings/Notification/ConfigurationNotificationUserControl.xaml @@ -94,5 +94,12 @@ CommandParameter="{Binding Training}" Content="{Binding Translation.Notification_Training}" /> + +