neon-sprawl/server/NeonSprawl.Server/Game/Gathering/GatherOperations.cs

133 lines
5.0 KiB
C#

using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Skills;
namespace NeonSprawl.Server.Game.Gathering;
/// <summary>
/// Orchestrates gather side effects: capacity pre-check, inventory grant, skill XP, depletion commit (NEO-62).
/// Wired from <see cref="NeonSprawl.Server.Game.Interaction.InteractionApi"/> on <c>resource_node</c> interact (NEO-63).
/// </summary>
public static class GatherOperations
{
private static readonly GatherGrantApplied[] EmptyGrants = [];
/// <summary>
/// Resolves one gather for <paramref name="playerId"/> at <paramref name="interactableId"/>.
/// Mutations commit in order: inventory add → XP grant → depletion decrement (last).
/// </summary>
public static GatherResult TryGather(
string playerId,
string interactableId,
IResourceNodeDefinitionRegistry nodeRegistry,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore xpStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IResourceNodeInstanceStore instanceStore)
{
var key = ResourceNodeInstanceIds.Normalize(interactableId);
if (key.Length == 0 ||
!nodeRegistry.TryGetDefinition(key, out var definition) ||
!nodeRegistry.TryGetYield(key, out var yield))
{
return Deny(GatherReasonCodes.UnknownNode, null);
}
if (!instanceStore.TryEnsureInitialized(key, definition.MaxGathers))
{
throw new InvalidOperationException(
$"Failed to initialize resource node instance '{key}' with maxGathers {definition.MaxGathers}.");
}
if (!instanceStore.TryGetRemainingGathers(key, out var capacity) || capacity.RemainingGathers <= 0)
{
var remaining = capacity.RemainingGathers;
return Deny(GatherReasonCodes.NodeDepleted, remaining);
}
var inventoryOutcome = PlayerInventoryOperations.TryAddStack(
playerId,
yield.ItemId,
yield.Quantity,
itemRegistry,
inventoryStore);
if (inventoryOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
{
return Deny(GatherReasonCodes.InventoryStoreMissing, capacity.RemainingGathers);
}
if (inventoryOutcome.Kind == PlayerInventoryMutationKind.Denied)
{
return Deny(inventoryOutcome.ReasonCode ?? GatherReasonCodes.InventoryFull, capacity.RemainingGathers);
}
var xpOutcome = SkillProgressionGrantOperations.TryApplyGrant(
playerId,
GatherSkillXpConstants.SalvageSkillId,
GatherSkillXpConstants.ActivityXpPerResourceNodeGather,
GatherSkillXpConstants.ActivitySourceKind,
skillRegistry,
xpStore,
levelCurve,
perkUnlockEngine);
if (xpOutcome.Kind != SkillProgressionGrantApplyKind.Granted)
{
var xpReason = xpOutcome.Kind == SkillProgressionGrantApplyKind.ProgressionStoreMissing
? GatherReasonCodes.ProgressionStoreMissing
: xpOutcome.Response?.ReasonCode ?? GatherReasonCodes.ProgressionStoreMissing;
_ = PlayerInventoryOperations.TryRemoveStack(
playerId,
yield.ItemId,
yield.Quantity,
itemRegistry,
inventoryStore);
return Deny(xpReason, capacity.RemainingGathers);
}
var depletionOutcome = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(
key,
nodeRegistry,
instanceStore);
if (depletionOutcome.Kind != ResourceNodeInstanceMutationKind.Applied)
{
_ = PlayerInventoryOperations.TryRemoveStack(
playerId,
yield.ItemId,
yield.Quantity,
itemRegistry,
inventoryStore);
var depletedRemaining = depletionOutcome.Snapshot?.RemainingGathers ?? 0;
return Deny(
depletionOutcome.ReasonCode ?? GatherReasonCodes.NodeDepleted,
depletedRemaining);
}
return new GatherResult(
Success: true,
ReasonCode: null,
GrantsApplied: [new GatherGrantApplied(yield.ItemId, yield.Quantity)],
RemainingGathers: depletionOutcome.Snapshot!.Value.RemainingGathers,
XpGrantSummary: new GatherXpGrantSummary(
GatherSkillXpConstants.SalvageSkillId,
GatherSkillXpConstants.ActivityXpPerResourceNodeGather,
GatherSkillXpConstants.ActivitySourceKind));
}
private static GatherResult Deny(string reasonCode, int? remainingGathers) =>
new(
Success: false,
ReasonCode: reasonCode,
GrantsApplied: EmptyGrants,
RemainingGathers: remainingGathers,
XpGrantSummary: null);
}