neon-sprawl/server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperat...

135 lines
5.5 KiB
C#

using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Rewards;
namespace NeonSprawl.Server.Game.Skills;
/// <summary>Shared NEO-38 skill XP grant apply (HTTP POST, gather engine via NEO-63 interact, and other callers).</summary>
public static class SkillProgressionGrantOperations
{
/// <summary>
/// Applies one grant if validation passes and <see cref="IPlayerSkillProgressionStore.TryApplyXpDelta"/> succeeds.
/// Caller must ensure <paramref name="playerId"/> is already authorized for progression (e.g. known to position state).
/// </summary>
public static SkillProgressionGrantApplyOutcome TryApplyGrant(
string playerId,
string skillIdRaw,
int amount,
string sourceKindRaw,
ISkillDefinitionRegistry registry,
IPlayerSkillProgressionStore xpStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine)
{
static SkillProgressionGrantResponse Deny(SkillProgressionSnapshotResponse snapshot, string reason) =>
new()
{
Granted = false,
ReasonCode = reason,
Progression = snapshot,
LevelUps = [],
};
var beforeSnapshot = SkillProgressionSnapshotApi.BuildSnapshot(playerId, registry, xpStore, levelCurve);
if (amount <= 0)
{
return new SkillProgressionGrantApplyOutcome(
SkillProgressionGrantApplyKind.Denied,
Deny(beforeSnapshot, SkillProgressionSnapshotApi.ReasonInvalidAmount));
}
var skillLookup = skillIdRaw.Trim();
if (skillLookup.Length == 0 || !registry.TryGetDefinition(skillLookup, out var def))
{
return new SkillProgressionGrantApplyOutcome(
SkillProgressionGrantApplyKind.Denied,
Deny(beforeSnapshot, SkillProgressionSnapshotApi.ReasonUnknownSkill));
}
var source = sourceKindRaw.Trim();
var allowed = def.AllowedXpSourceKinds.Any(k =>
string.Equals(k, source, StringComparison.OrdinalIgnoreCase));
if (!allowed)
{
return new SkillProgressionGrantApplyOutcome(
SkillProgressionGrantApplyKind.Denied,
Deny(beforeSnapshot, SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed));
}
if (!xpStore.TryApplyXpDelta(playerId, def.Id, amount, out var prevXp, out var newXp))
{
return new SkillProgressionGrantApplyOutcome(SkillProgressionGrantApplyKind.ProgressionStoreMissing, null);
}
// --- Telemetry hook site (NEO-40): future E9.M1 catalog event `xp_grant` ---
// No ingest here until the telemetry catalog exists. Planned payload fields: playerId,
// skillId, amount, sourceKind; optional correlation/request ids when Slice 3 wires callers.
var prevLevel = levelCurve.LevelFromTotalXp(prevXp);
var nextLevel = levelCurve.LevelFromTotalXp(newXp);
var levelUps = new List<SkillLevelUpJson>();
if (nextLevel > prevLevel)
{
// --- Telemetry hook site (NEO-40): future E9.M1 catalog event `level_up` ---
// Planned fields: playerId, skillId, previousLevel, newLevel, prevXp, newXp (totals before/after this grant). No logging (comments-only).
// Time-to-first-level-up: needs first-seen / milestone persistence not present here; wire
// when catalog + storage exist (likely derived from `level_up` or a dedicated milestone).
levelUps.Add(
new SkillLevelUpJson
{
SkillId = def.Id,
PreviousLevel = prevLevel,
NewLevel = nextLevel,
});
}
if (levelUps.Count > 0)
{
foreach (var levelUp in levelUps)
{
_ = perkUnlockEngine.ReevaluateAfterLevelUp(playerId, levelUp.SkillId, levelUp.NewLevel);
}
}
var afterSnapshot = SkillProgressionSnapshotApi.BuildSnapshot(playerId, registry, xpStore, levelCurve);
return new SkillProgressionGrantApplyOutcome(
SkillProgressionGrantApplyKind.Granted,
new SkillProgressionGrantResponse
{
Granted = true,
Progression = afterSnapshot,
LevelUps = levelUps,
});
}
/// <summary>Captures skill XP totals and perk snapshot before a grant batch for compensating rollback.</summary>
public static SkillProgressionGrantRollbackPoint CaptureRollbackPoint(
string playerId,
IPlayerSkillProgressionStore xpStore,
IPlayerPerkStateStore perkStore) =>
new(
new Dictionary<string, int>(xpStore.GetXpTotals(playerId), StringComparer.Ordinal),
perkStore.GetSnapshot(playerId));
/// <summary>Restores skill XP and perk state captured before a failed grant batch commit.</summary>
public static bool TryRestoreRollbackPoint(
string playerId,
SkillProgressionGrantRollbackPoint rollbackPoint,
IReadOnlyList<RewardSkillXpGrantApplied> appliedGrants,
IPlayerSkillProgressionStore xpStore,
IPlayerPerkStateStore perkStore)
{
for (var i = appliedGrants.Count - 1; i >= 0; i--)
{
var grant = appliedGrants[i];
if (!xpStore.TryApplyXpDelta(playerId, grant.SkillId, -grant.Amount, out _, out _))
{
return false;
}
}
return perkStore.TryRestoreSnapshot(playerId, rollbackPoint.PerkState);
}
}