neon-sprawl/server/NeonSprawl.Server/Game/Quests/QuestObjectiveWiring.cs

483 lines
16 KiB
C#

using NeonSprawl.Server.Game.Items;
namespace NeonSprawl.Server.Game.Quests;
/// <summary>
/// NEO-118: best-effort quest objective evaluation on gather/craft/encounter success and
/// <see cref="QuestObjectiveKinds.InventoryHasItem"/> snapshot passes (accept / step advance / inventory mutations).
/// </summary>
public static class QuestObjectiveWiring
{
private const int MaxStepCompletionIterations = 8;
/// <summary>Credits <see cref="QuestObjectiveKinds.GatherItem"/> objectives after a successful gather grant.</summary>
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);
}
}
/// <summary>Credits <see cref="QuestObjectiveKinds.CraftRecipe"/> objectives after a successful craft.</summary>
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);
}
}
/// <summary>Credits <see cref="QuestObjectiveKinds.EncounterComplete"/> objectives after encounter completion.</summary>
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);
}
}
/// <summary>Re-evaluates <see cref="QuestObjectiveKinds.InventoryHasItem"/> counters for every active quest.</summary>
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);
}
}
/// <summary>Re-evaluates <see cref="QuestObjectiveKinds.InventoryHasItem"/> on the active step for one quest.</summary>
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 _);
}
}
/// <summary>Advances or completes the quest when the active step's objectives are satisfied.</summary>
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<string, int> 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<string, int> counters, string objectiveId) =>
counters.TryGetValue(objectiveId, out var count) ? count : 0;
}