BattleXpGrantResult gains a LeveledUp bool set true iff at least one curve threshold was crossed during the grant (comparing pre-Level to post-Level after the level-up loop). Callers will gate class_level_up mission emits on this flag rather than caching pre-state themselves. No behavior change for existing callers — they read GetXp/TotalXp/Level today and never check LeveledUp yet. Tests cover: unchanged level → false, single threshold → true, multi-threshold within one grant → still true, unknown class guardrail → false. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
40 lines
1.8 KiB
C#
40 lines
1.8 KiB
C#
using SVSim.Database.Models;
|
|
|
|
namespace SVSim.Database.Services.BattleXp;
|
|
|
|
/// <summary>
|
|
/// Amounts returned to callers after a class-XP grant. <see cref="TotalXp"/> and
|
|
/// <see cref="Level"/> are POST-grant, POST-level-up (matching the wire shape's
|
|
/// <c>class_experience</c> + <c>class_level</c> post-state semantics).
|
|
/// <see cref="LeveledUp"/> is <c>true</c> iff at least one level threshold was
|
|
/// crossed during this grant — callers gate <c>class_level_up</c> mission emits
|
|
/// on this flag rather than caching pre-state themselves.
|
|
/// </summary>
|
|
public sealed record BattleXpGrantResult(int GetXp, int TotalXp, int Level, bool LeveledUp);
|
|
|
|
public interface IBattleXpService
|
|
{
|
|
/// <summary>
|
|
/// Grants class XP for a battle finish. Caller supplies a viewer loaded via
|
|
/// <see cref="Repositories.Viewer.IViewerRepository.LoadForBattleXpGrantAsync"/>
|
|
/// (or equivalent, with <c>.Include(v => v.Classes).ThenInclude(c => c.Class)</c>).
|
|
/// Caller <c>SaveChangesAsync</c> after this returns.
|
|
/// <para>
|
|
/// Amount resolution: mode-specific config override if non-null, else global
|
|
/// <c>XpPerWin</c>/<c>XpPerLoss</c>. Story ignores <paramref name="isWin"/> (always
|
|
/// treated as a clear).
|
|
/// </para>
|
|
/// <para>
|
|
/// Level-up: loops on <c>ClassExpEntry</c> curve; <c>row.Exp</c> stores level-relative
|
|
/// XP and carries overflow after each level-up. Saturates at curve max level (excess
|
|
/// piles in <c>Exp</c>).
|
|
/// </para>
|
|
/// <para>
|
|
/// Guardrail: viewer has no <see cref="ViewerClassData"/> row for
|
|
/// <paramref name="classId"/> → returns <c>(0, 0, 1)</c>, no mutation, logs Warning.
|
|
/// </para>
|
|
/// </summary>
|
|
Task<BattleXpGrantResult> GrantAsync(
|
|
Viewer viewer, int classId, bool isWin, BattleXpMode mode, CancellationToken ct = default);
|
|
}
|