feat(rank): RankProgressService — pure +100/-50 tier-floored math
Reads RankInfoEntry.LowerLimitPoint from ranks.csv (via IGlobalsRepository) for main-tier floors — no hardcoded thresholds. Master (Point=50000) and Grand Master (MP-based, floor 5000) branch handles the post-Master path. GetAsync is a read-only snapshot used later by AI-start's SelfInfo. 13 unit tests cover first-win, loss floors (0, Beginner→D, D→D0), win crossing 50000, MP awards at Master, GM promotion at MP≥5000, GM floor protecting against Master demotion, format separation, Crossover throw. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
46
SVSim.Database/Services/RankProgress/IRankProgressService.cs
Normal file
46
SVSim.Database/Services/RankProgress/IRankProgressService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.Database.Services.RankProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Wire-shape result of a single rank-battle finish grant. All fields map 1:1 to
|
||||
/// <c>RankBattleFinishResponseDto</c> keys the client reads via GetValueOrDefault
|
||||
/// in Wizard/RankBattleFinishTask.cs:57-63.
|
||||
/// </summary>
|
||||
/// <param name="Rank">rank_id post-grant (1..29).</param>
|
||||
/// <param name="AfterBattlePoint">Point post-grant (accumulated).</param>
|
||||
/// <param name="AfterMasterPoint">MasterPoint post-grant (accumulated).</param>
|
||||
/// <param name="BattlePoint">Signed Point delta (+100/-50/0).</param>
|
||||
/// <param name="MasterPoint">Signed MasterPoint delta.</param>
|
||||
/// <param name="IsMasterRank">True iff Rank == 25.</param>
|
||||
/// <param name="IsGrandMasterRank">True iff Rank >= 26.</param>
|
||||
public sealed record RankProgressResult(
|
||||
int Rank,
|
||||
int AfterBattlePoint,
|
||||
int AfterMasterPoint,
|
||||
int BattlePoint,
|
||||
int MasterPoint,
|
||||
bool IsMasterRank,
|
||||
bool IsGrandMasterRank);
|
||||
|
||||
public interface IRankProgressService
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a +100 (win) or -50 (loss) delta to the viewer's rank progression in the
|
||||
/// given format, respecting tier floors from <c>ranks.csv</c>'s LowerLimitPoint column.
|
||||
/// Creates a ViewerRankProgress row for (viewer, format) if none exists. Caller must
|
||||
/// have loaded the viewer with <c>.Include(v => v.RankProgress)</c> and must call
|
||||
/// <c>SaveChangesAsync</c> after this returns.
|
||||
/// </summary>
|
||||
/// <param name="format">Must be Format.Rotation or Format.Unlimited.</param>
|
||||
Task<RankProgressResult> GrantAsync(
|
||||
Viewer viewer, Format format, bool isWin, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Read-only current progression snapshot for (viewer, format). Returns a zero-value
|
||||
/// result if no row exists. Does not mutate the viewer or hit the DB.
|
||||
/// </summary>
|
||||
Task<RankProgressResult> GetAsync(
|
||||
Viewer viewer, Format format, CancellationToken ct = default);
|
||||
}
|
||||
Reference in New Issue
Block a user