using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Skills;
namespace NeonSprawl.Server.Game.Crafting;
///
/// Orchestrates craft side effects: recipe resolve, inventory pre-flight, input removal, output grant, refine XP (NEO-69).
/// Craft HTTP: (NEO-70).
///
public static class CraftOperations
{
private static readonly CraftIoApplied[] EmptyIo = [];
///
/// Resolves one craft for and at batch .
/// Mutations commit in order: remove inputs → add outputs → refine XP; compensating rollback on output or XP failure.
///
public static CraftResult TryCraft(
string playerId,
string recipeId,
int quantity,
IRecipeDefinitionRegistry recipeRegistry,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore xpStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine)
{
if (quantity <= 0)
{
return Deny(CraftReasonCodes.InvalidQuantity);
}
var recipeKey = recipeId.Trim();
if (recipeKey.Length == 0 || !recipeRegistry.TryGetDefinition(recipeKey, out var recipe))
{
return Deny(CraftReasonCodes.UnknownRecipe);
}
if (!inventoryStore.TryGetSnapshot(playerId, out var snapshot))
{
return Deny(CraftReasonCodes.InventoryStoreMissing);
}
var scaledInputs = ScaleIoRows(recipe.Inputs, quantity);
var scaledOutputs = ScaleIoRows(recipe.Outputs, quantity);
foreach (var input in scaledInputs)
{
var held = PlayerInventoryOperations.GetHeldQuantity(snapshot, input.ItemId, itemRegistry);
if (held < input.Quantity)
{
return Deny(CraftReasonCodes.InsufficientMaterials);
}
}
var simulated = CloneSnapshot(snapshot);
foreach (var input in scaledInputs)
{
if (!PlayerInventoryOperations.TrySimulateRemoveStack(ref simulated, input.ItemId, input.Quantity, itemRegistry))
{
return Deny(CraftReasonCodes.InsufficientMaterials);
}
}
foreach (var output in scaledOutputs)
{
if (!PlayerInventoryOperations.TrySimulateAddStack(ref simulated, output.ItemId, output.Quantity, itemRegistry))
{
return Deny(CraftReasonCodes.InventoryFull);
}
}
var inputsRemoved = new List(scaledInputs.Length);
foreach (var input in scaledInputs)
{
var removeOutcome = PlayerInventoryOperations.TryRemoveStack(
playerId,
input.ItemId,
input.Quantity,
itemRegistry,
inventoryStore);
if (removeOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
{
CompensatingRestoreInputs(playerId, inputsRemoved, itemRegistry, inventoryStore);
return Deny(CraftReasonCodes.InventoryStoreMissing);
}
if (removeOutcome.Kind == PlayerInventoryMutationKind.Denied)
{
CompensatingRestoreInputs(playerId, inputsRemoved, itemRegistry, inventoryStore);
return Deny(CraftReasonCodes.InsufficientMaterials);
}
inputsRemoved.Add(input);
}
var outputsApplied = new List(scaledOutputs.Length);
foreach (var output in scaledOutputs)
{
var addOutcome = PlayerInventoryOperations.TryAddStack(
playerId,
output.ItemId,
output.Quantity,
itemRegistry,
inventoryStore);
if (addOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
{
CompensatingRemoveOutputs(playerId, outputsApplied, itemRegistry, inventoryStore);
CompensatingRestoreInputs(playerId, scaledInputs, itemRegistry, inventoryStore);
return Deny(CraftReasonCodes.InventoryStoreMissing);
}
if (addOutcome.Kind == PlayerInventoryMutationKind.Denied)
{
CompensatingRemoveOutputs(playerId, outputsApplied, itemRegistry, inventoryStore);
CompensatingRestoreInputs(playerId, scaledInputs, itemRegistry, inventoryStore);
return Deny(CraftReasonCodes.InventoryFull);
}
outputsApplied.Add(output);
}
var xpOutcome = SkillProgressionGrantOperations.TryApplyGrant(
playerId,
RefineSkillXpConstants.RefineSkillId,
RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion,
RefineSkillXpConstants.ActivitySourceKind,
skillRegistry,
xpStore,
levelCurve,
perkUnlockEngine);
if (xpOutcome.Kind != SkillProgressionGrantApplyKind.Granted)
{
CompensatingRemoveOutputs(playerId, outputsApplied, itemRegistry, inventoryStore);
CompensatingRestoreInputs(playerId, scaledInputs, itemRegistry, inventoryStore);
var xpReason = xpOutcome.Kind == SkillProgressionGrantApplyKind.ProgressionStoreMissing
? CraftReasonCodes.ProgressionStoreMissing
: xpOutcome.Response?.ReasonCode ?? CraftReasonCodes.ProgressionStoreMissing;
return Deny(xpReason);
}
return new CraftResult(
Success: true,
ReasonCode: null,
InputsConsumed: scaledInputs,
OutputsGranted: outputsApplied,
XpGrantSummary: new CraftXpGrantSummary(
RefineSkillXpConstants.RefineSkillId,
RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion,
RefineSkillXpConstants.ActivitySourceKind));
}
private static CraftIoApplied[] ScaleIoRows(IReadOnlyList rows, int quantity)
{
var scaled = new CraftIoApplied[rows.Count];
for (var i = 0; i < rows.Count; i++)
{
var row = rows[i];
scaled[i] = new CraftIoApplied(row.ItemId, row.Quantity * quantity);
}
return scaled;
}
private static PlayerInventorySnapshot CloneSnapshot(PlayerInventorySnapshot snapshot) =>
new()
{
BagSlots = PlayerInventorySnapshot.CloneSlots(snapshot.BagSlots),
EquipmentSlots = PlayerInventorySnapshot.CloneSlots(snapshot.EquipmentSlots),
};
private static void CompensatingRestoreInputs(
string playerId,
IReadOnlyList inputs,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore)
{
foreach (var input in inputs)
{
_ = PlayerInventoryOperations.TryAddStack(
playerId,
input.ItemId,
input.Quantity,
itemRegistry,
inventoryStore);
}
}
private static void CompensatingRemoveOutputs(
string playerId,
IReadOnlyList outputs,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore)
{
foreach (var output in outputs)
{
_ = PlayerInventoryOperations.TryRemoveStack(
playerId,
output.ItemId,
output.Quantity,
itemRegistry,
inventoryStore);
}
}
private static CraftResult Deny(string reasonCode) =>
new(
Success: false,
ReasonCode: reasonCode,
InputsConsumed: EmptyIo,
OutputsGranted: EmptyIo,
XpGrantSummary: null);
}