namespace NeonSprawl.Server.Game.Quests; /// /// Server-authoritative quest accept, step advance, and complete with reason codes (NEO-117). /// Objective wiring: E7M1-07 (NEO-118). HTTP: E7M1-08/09 (NEO-119/NEO-120). /// public static class QuestStateOperations { /// /// Validates quest id and prerequisites, then activates at step 0 when not started. /// public static QuestStateOperationResult TryAccept( string playerId, string questId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore) { if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) || !questRegistry.TryGetDefinition(normalizedQuestId, out var definition)) { return Deny(QuestStateReasonCodes.UnknownQuest); } if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing)) { return existing.Status switch { QuestProgressStatus.Completed => Deny(QuestStateReasonCodes.AlreadyCompleted, existing), QuestProgressStatus.Active => Deny(QuestStateReasonCodes.AlreadyActive, existing), _ => Deny(QuestStateReasonCodes.UnknownQuest), }; } foreach (var prerequisiteId in definition.PrerequisiteQuestIds) { if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) || prerequisite.Status != QuestProgressStatus.Completed) { return Deny(QuestStateReasonCodes.PrerequisiteIncomplete); } } if (progressStore.TryActivate(playerId, normalizedQuestId, out var snapshot)) { return Success(snapshot); } return Deny(QuestStateReasonCodes.UnknownPlayer); } /// /// Advances an active quest to when within catalog bounds. /// public static QuestStateOperationResult TryAdvanceStep( string playerId, string questId, int newStepIndex, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore) { if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) || !questRegistry.TryGetDefinition(normalizedQuestId, out var definition)) { return Deny(QuestStateReasonCodes.UnknownQuest); } if (!progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing)) { return Deny(QuestStateReasonCodes.NotActive); } if (existing.Status == QuestProgressStatus.Completed) { return Deny(QuestStateReasonCodes.AlreadyCompleted, existing); } if (existing.Status != QuestProgressStatus.Active) { return Deny(QuestStateReasonCodes.NotActive); } if (newStepIndex <= existing.CurrentStepIndex || newStepIndex >= definition.Steps.Count) { return Deny(QuestStateReasonCodes.InvalidStepIndex, existing); } if (progressStore.TryAdvanceStep(playerId, normalizedQuestId, newStepIndex, out var snapshot)) { return Success(snapshot); } return Deny(QuestStateReasonCodes.UnknownPlayer, existing); } /// /// Marks a quest complete. Idempotent: replays return success with the existing snapshot. /// public static QuestStateOperationResult TryMarkComplete( string playerId, string questId, IQuestDefinitionRegistry questRegistry, IPlayerQuestStateStore progressStore, TimeProvider timeProvider) { if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) || !questRegistry.TryGetDefinition(normalizedQuestId, out _)) { return Deny(QuestStateReasonCodes.UnknownQuest); } if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing) && existing.Status == QuestProgressStatus.Completed) { return Success(existing); } if (!progressStore.TryGetProgress(playerId, normalizedQuestId, out var active) || active.Status != QuestProgressStatus.Active) { return Deny(QuestStateReasonCodes.NotActive); } if (progressStore.TryMarkComplete(playerId, normalizedQuestId, timeProvider.GetUtcNow(), out var snapshot)) { return Success(snapshot); } if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var afterAttempt) && afterAttempt.Status == QuestProgressStatus.Completed) { return Success(afterAttempt); } return Deny(QuestStateReasonCodes.UnknownPlayer); } private static QuestStateOperationResult Success(QuestStepState snapshot) => new(Success: true, ReasonCode: null, Snapshot: snapshot); private static QuestStateOperationResult Deny(string reasonCode, QuestStepState? snapshot = null) => new(Success: false, ReasonCode: reasonCode, Snapshot: snapshot); }