using NeonSprawl.Server.Game.Items; namespace NeonSprawl.Server.Game.Quests; /// /// NEO-118: best-effort quest objective evaluation on gather/craft/encounter success and /// snapshot passes (accept / step advance / inventory mutations). /// public static class QuestObjectiveWiring { private const int MaxStepCompletionIterations = 8; /// Credits objectives after a successful gather grant. public static void TryProcessGatherSuccess( string playerId, string itemId, int quantityGranted, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry, TimeProvider timeProvider, ILogger? logger = null) { if (quantityGranted <= 0 || itemId.Trim().Length == 0) { return; } try { var normalizedItemId = itemId.Trim(); foreach (var definition in questRegistry.GetDefinitionsInIdOrder()) { if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress)) { continue; } ApplyGatherItemObjectives( playerId, definition, progress, normalizedItemId, quantityGranted, progressStore); } RefreshInventoryHasItemCountersForPlayer( playerId, questRegistry, progressStore, inventoryStore, itemRegistry); TryCompleteAllActiveQuests( playerId, questRegistry, progressStore, inventoryStore, itemRegistry, timeProvider); } catch (Exception ex) { logger?.LogDebug(ex, "Quest gather wiring skipped for player {PlayerId}", playerId); } } /// Credits objectives after a successful craft. public static void TryProcessCraftSuccess( string playerId, string recipeId, int batchQuantity, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry, TimeProvider timeProvider, ILogger? logger = null) { if (batchQuantity <= 0 || recipeId.Trim().Length == 0) { return; } try { var normalizedRecipeId = recipeId.Trim(); foreach (var definition in questRegistry.GetDefinitionsInIdOrder()) { if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress)) { continue; } ApplyCraftRecipeObjectives( playerId, definition, progress, normalizedRecipeId, batchQuantity, progressStore); } RefreshInventoryHasItemCountersForPlayer( playerId, questRegistry, progressStore, inventoryStore, itemRegistry); TryCompleteAllActiveQuests( playerId, questRegistry, progressStore, inventoryStore, itemRegistry, timeProvider); } catch (Exception ex) { logger?.LogDebug(ex, "Quest craft wiring skipped for player {PlayerId}", playerId); } } /// Credits objectives after encounter completion. public static void TryProcessEncounterCompleteSuccess( string playerId, string encounterId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry, TimeProvider timeProvider, ILogger? logger = null) { if (encounterId.Trim().Length == 0) { return; } try { var normalizedEncounterId = encounterId.Trim(); foreach (var definition in questRegistry.GetDefinitionsInIdOrder()) { if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress)) { continue; } ApplyEncounterCompleteObjectives( playerId, definition, progress, normalizedEncounterId, progressStore); } RefreshInventoryHasItemCountersForPlayer( playerId, questRegistry, progressStore, inventoryStore, itemRegistry); TryCompleteAllActiveQuests( playerId, questRegistry, progressStore, inventoryStore, itemRegistry, timeProvider); } catch (Exception ex) { logger?.LogDebug(ex, "Quest encounter wiring skipped for player {PlayerId}", playerId); } } /// Re-evaluates counters for every active quest. public static void RefreshInventoryHasItemCountersForPlayer( string playerId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry) { foreach (var definition in questRegistry.GetDefinitionsInIdOrder()) { TryProcessInventoryHasItemForQuest( playerId, definition.Id, questRegistry, progressStore, inventoryStore, itemRegistry); } } /// Re-evaluates on the active step for one quest. public static void TryProcessInventoryHasItemForQuest( string playerId, string questId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry) { if (!TryGetActiveProgress(playerId, questId, progressStore, out var progress) || !questRegistry.TryGetDefinition(questId, out var definition) || !inventoryStore.TryGetSnapshot(playerId, out var inventory)) { return; } var stepIndex = progress.CurrentStepIndex; if (stepIndex < 0 || stepIndex >= definition.Steps.Count) { return; } foreach (var objective in definition.Steps[stepIndex].Objectives) { if (!string.Equals(objective.Kind, QuestObjectiveKinds.InventoryHasItem, StringComparison.Ordinal) || objective.ItemId is null) { continue; } var required = GetRequiredQuantity(objective); var held = PlayerInventoryOperations.GetHeldQuantity(inventory, objective.ItemId, itemRegistry); var newCount = Math.Min(held, required); var current = GetCounter(progress, objective.Id); if (newCount == current) { continue; } progressStore.TryUpdateObjectiveCounter(playerId, questId, objective.Id, newCount, out _); } } /// Advances or completes the quest when the active step's objectives are satisfied. public static void TryCompleteStepIfSatisfied( string playerId, string questId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry, TimeProvider timeProvider) { for (var iteration = 0; iteration < MaxStepCompletionIterations; iteration++) { if (!TryGetActiveProgress(playerId, questId, progressStore, out var progress) || !questRegistry.TryGetDefinition(questId, out var definition)) { return; } var stepIndex = progress.CurrentStepIndex; if (stepIndex < 0 || stepIndex >= definition.Steps.Count) { return; } inventoryStore.TryGetSnapshot(playerId, out var inventory); var step = definition.Steps[stepIndex]; if (!IsStepSatisfied(step, progress.ObjectiveCounters, inventory, itemRegistry)) { return; } if (stepIndex >= definition.Steps.Count - 1) { QuestStateOperations.TryMarkComplete( playerId, questId, questRegistry, progressStore, timeProvider); return; } var advance = QuestStateOperations.TryAdvanceStep( playerId, questId, stepIndex + 1, questRegistry, progressStore, inventoryStore, itemRegistry, timeProvider); if (!advance.Success) { return; } } } private static void TryCompleteAllActiveQuests( string playerId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, IPlayerInventoryStore inventoryStore, IItemDefinitionRegistry itemRegistry, TimeProvider timeProvider) { foreach (var definition in questRegistry.GetDefinitionsInIdOrder()) { if (TryGetActiveProgress(playerId, definition.Id, progressStore, out _)) { TryCompleteStepIfSatisfied( playerId, definition.Id, questRegistry, progressStore, inventoryStore, itemRegistry, timeProvider); } } } private static void ApplyGatherItemObjectives( string playerId, QuestDefRow definition, QuestStepState progress, string itemId, int quantityGranted, IPlayerQuestStateStore progressStore) { var stepIndex = progress.CurrentStepIndex; if (stepIndex < 0 || stepIndex >= definition.Steps.Count) { return; } foreach (var objective in definition.Steps[stepIndex].Objectives) { if (!string.Equals(objective.Kind, QuestObjectiveKinds.GatherItem, StringComparison.Ordinal) || objective.ItemId is null || !string.Equals(objective.ItemId, itemId, StringComparison.Ordinal)) { continue; } var required = GetRequiredQuantity(objective); var current = GetCounter(progress, objective.Id); var newCount = Math.Min(current + quantityGranted, required); if (newCount == current) { continue; } progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, newCount, out _); progressStore.TryGetProgress(playerId, definition.Id, out progress); } } private static void ApplyCraftRecipeObjectives( string playerId, QuestDefRow definition, QuestStepState progress, string recipeId, int batchQuantity, IPlayerQuestStateStore progressStore) { var stepIndex = progress.CurrentStepIndex; if (stepIndex < 0 || stepIndex >= definition.Steps.Count) { return; } foreach (var objective in definition.Steps[stepIndex].Objectives) { if (!string.Equals(objective.Kind, QuestObjectiveKinds.CraftRecipe, StringComparison.Ordinal) || objective.RecipeId is null || !string.Equals(objective.RecipeId, recipeId, StringComparison.Ordinal)) { continue; } var required = GetRequiredQuantity(objective); var current = GetCounter(progress, objective.Id); var newCount = Math.Min(current + batchQuantity, required); if (newCount == current) { continue; } progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, newCount, out _); progressStore.TryGetProgress(playerId, definition.Id, out progress); } } private static void ApplyEncounterCompleteObjectives( string playerId, QuestDefRow definition, QuestStepState progress, string encounterId, IPlayerQuestStateStore progressStore) { var stepIndex = progress.CurrentStepIndex; if (stepIndex < 0 || stepIndex >= definition.Steps.Count) { return; } foreach (var objective in definition.Steps[stepIndex].Objectives) { if (!string.Equals(objective.Kind, QuestObjectiveKinds.EncounterComplete, StringComparison.Ordinal) || objective.EncounterId is null || !string.Equals(objective.EncounterId, encounterId, StringComparison.Ordinal)) { continue; } if (GetCounter(progress, objective.Id) >= 1) { continue; } progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, 1, out _); progressStore.TryGetProgress(playerId, definition.Id, out progress); } } private static bool IsStepSatisfied( QuestStepDefRow step, IReadOnlyDictionary counters, PlayerInventorySnapshot inventory, IItemDefinitionRegistry itemRegistry) { foreach (var objective in step.Objectives) { var required = GetRequiredQuantity(objective); if (string.Equals(objective.Kind, QuestObjectiveKinds.InventoryHasItem, StringComparison.Ordinal)) { if (objective.ItemId is null) { return false; } var held = PlayerInventoryOperations.GetHeldQuantity(inventory, objective.ItemId, itemRegistry); if (held < required) { return false; } continue; } if (GetCounter(counters, objective.Id) < required) { return false; } } return step.Objectives.Count > 0; } private static bool TryGetActiveProgress( string playerId, string questId, IPlayerQuestStateStore progressStore, out QuestStepState progress) => progressStore.TryGetProgress(playerId, questId, out progress!) && progress.Status == QuestProgressStatus.Active; private static int GetRequiredQuantity(QuestObjectiveDefRow objective) => objective.Quantity is > 0 ? objective.Quantity.Value : 1; private static int GetCounter(QuestStepState progress, string objectiveId) => GetCounter(progress.ObjectiveCounters, objectiveId); private static int GetCounter(IReadOnlyDictionary counters, string objectiveId) => counters.TryGetValue(objectiveId, out var count) ? count : 0; }