105 lines
4.3 KiB
C#
105 lines
4.3 KiB
C#
using NeonSprawl.Server.Game.Mastery;
|
|
|
|
namespace NeonSprawl.Server.Game.Skills;
|
|
|
|
/// <summary>Shared NEO-38 skill XP grant apply (used by HTTP POST and NEO-41 gather interact hook).</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,
|
|
});
|
|
}
|
|
}
|