using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Game.Rewards;
using NeonSprawl.Server.Game.Skills;
namespace NeonSprawl.Server.Game.Gathering;
///
/// Orchestrates gather side effects: capacity pre-check, inventory grant, skill XP, depletion commit (NEO-62).
/// Wired from on resource_node interact (NEO-63).
/// NEO-64 telemetry hook sites: (resource_gathered, gather_node_depleted).
///
public static class GatherOperations
{
private static readonly GatherGrantApplied[] EmptyGrants = [];
///
/// Resolves one gather for at .
/// Mutations commit in order: inventory add → XP grant → depletion decrement (last).
///
public static GatherResult TryGather(
string playerId,
string interactableId,
IResourceNodeDefinitionRegistry nodeRegistry,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore xpStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IResourceNodeInstanceStore instanceStore,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
TimeProvider timeProvider)
{
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);
}
var remainingAfterGather = depletionOutcome.Snapshot!.Value.RemainingGathers;
// --- Telemetry hook site (NEO-64): future E9.M1 catalog event `resource_gathered` ---
// TODO(E9.M1): catalog emit — on every successful gather (inventory + XP + depletion committed).
// Planned payload fields: playerId, interactableId (node key), itemId, quantity (GatherGrantApplied),
// skillId, xpAmount, sourceKind (GatherXpGrantSummary), remainingGathers after commit.
// No ingest or ILogger here (comments-only). InteractionApi delegates here (no duplicate hooks).
if (remainingAfterGather == 0)
{
// --- Telemetry hook site (NEO-64): future E9.M1 catalog event `gather_node_depleted` ---
// TODO(E9.M1): catalog emit — once when this successful gather drives remainingGathers to 0
// (not on pre-gather node_depleted deny). Planned payload: interactableId, optional playerId
// (last gatherer), maxGathers from definition when wiring E9.M1.
}
QuestObjectiveWiring.TryProcessGatherSuccess(
playerId,
yield.ItemId,
yield.Quantity,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
xpStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
timeProvider);
return new GatherResult(
Success: true,
ReasonCode: null,
GrantsApplied: [new GatherGrantApplied(yield.ItemId, yield.Quantity)],
RemainingGathers: remainingAfterGather,
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);
}