178 lines
7.0 KiB
C#
178 lines
7.0 KiB
C#
using NeonSprawl.Server.Game.Factions;
|
|
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;
|
|
|
|
/// <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).
|
|
/// NEO-64 telemetry hook sites: <see cref="TryGather"/> (<c>resource_gathered</c>, <c>gather_node_depleted</c>).
|
|
/// </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,
|
|
IQuestDefinitionRegistry questRegistry,
|
|
IPlayerQuestStateStore progressStore,
|
|
IPlayerPerkStateStore perkStore,
|
|
IRewardDeliveryStore deliveryStore,
|
|
IFactionStandingStore standingStore,
|
|
IReputationDeltaStore auditStore,
|
|
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,
|
|
standingStore,
|
|
auditStore,
|
|
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);
|
|
}
|