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

142 lines
5.6 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>Collects perk ids unlocked between two snapshots (this batch only).</summary>
public static void CollectPerksUnlockedSince(
PerkStateSnapshot before,
PerkStateSnapshot after,
ISet<string> sink)
{
foreach (var perkId in after.UnlockedPerkIds)
{
if (!before.UnlockedPerkIds.Contains(perkId))
{
sink.Add(perkId);
}
}
}
/// <summary>Reverts skill XP grants and perks unlocked during this failed batch commit.</summary>
public static bool TryRevertAppliedSkillGrants(
string playerId,
IReadOnlyList<RewardSkillXpGrantApplied> appliedGrants,
IReadOnlyCollection<string> perksUnlockedByBatch,
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 perksUnlockedByBatch.Count == 0
|| perkStore.TryRemoveUnlockedPerks(playerId, perksUnlockedByBatch);
}
}