feat(rank): wire RankProgressService into RankBattleController.Finish

Replaces the phase-3 zero stub. Real progression math now lands in the
response body (rank / after_battle_point / after_master_point /
battle_point / master_point). Format is authored by URL (not derived from
BattleContext) so /finish still works when hit without a prior
/do_matching — mirrors the client's URL-per-format Wizard/RankBattleFinishTask
dispatch (decompile lines 12-35).

3 existing tests updated with real progression assertions; 3 new tests
cover D-tier floor loss, format separation, and AI-variant persistence.
Full solution suite: 1583/1583 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 10:21:12 -04:00
parent f18cfb58b8
commit 7ff8a7bd92
3 changed files with 122 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ using SVSim.Database.Enums;
using SVSim.Database.Repositories.Viewer;
using SVSim.Database.Services.BattleXp;
using SVSim.Database.Services.Friend;
using SVSim.Database.Services.RankProgress;
using SVSim.Database.Services.Replay;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Extensions;
@@ -38,6 +39,7 @@ public sealed class RankBattleController : ControllerBase
private readonly IPlayedTogetherWriter _playedTogetherWriter;
private readonly IViewerRepository _viewers;
private readonly IBattleXpService _xp;
private readonly IRankProgressService _rankProgress;
private readonly SVSimDbContext _db;
private readonly ILogger<RankBattleController> _log;
@@ -51,6 +53,7 @@ public sealed class RankBattleController : ControllerBase
IPlayedTogetherWriter playedTogetherWriter,
IViewerRepository viewers,
IBattleXpService xp,
IRankProgressService rankProgress,
SVSimDbContext db,
ILogger<RankBattleController> log)
{
@@ -63,6 +66,7 @@ public sealed class RankBattleController : ControllerBase
_playedTogetherWriter = playedTogetherWriter;
_viewers = viewers;
_xp = xp;
_rankProgress = rankProgress;
_db = db;
_log = log;
}
@@ -93,17 +97,35 @@ public sealed class RankBattleController : ControllerBase
public Task<IActionResult> AiStartUnlimited([FromBody] BaseRequest _, CancellationToken ct)
=> AiStartInternal(Format.Unlimited, ct);
// Finish routes route by URL to the shared handler with the format baked in — the URL
// is authoritative (mirrors DoMatchingRotation/DoMatchingUnlimited). Deriving format
// from the BattleContext is fragile: the client may reach /finish without a prior
// /do_matching (e.g. reconnect, replay-of-log flows), and we still need rank
// progression to land in the right format bucket. Client-side, RankBattleFinishTask
// picks the URL by (Data.CurrentFormat, IsAINetwork) — see decompile lines 12-35.
[HttpPost("/rotation_rank_battle/finish")]
public Task<IActionResult> FinishRotation([FromBody] RankBattleFinishRequestDto req, CancellationToken ct)
=> FinishInternal(Format.Rotation, req, ct);
[HttpPost("/unlimited_rank_battle/finish")]
public Task<IActionResult> FinishUnlimited([FromBody] RankBattleFinishRequestDto req, CancellationToken ct)
=> FinishInternal(Format.Unlimited, req, ct);
[HttpPost("/ai_rotation_rank_battle/finish")]
public Task<IActionResult> AiFinishRotation([FromBody] RankBattleFinishRequestDto req, CancellationToken ct)
=> FinishInternal(Format.Rotation, req, ct);
[HttpPost("/ai_unlimited_rank_battle/finish")]
public Task<IActionResult> AiFinishUnlimited([FromBody] RankBattleFinishRequestDto req, CancellationToken ct)
=> FinishInternal(Format.Unlimited, req, ct);
/// <summary>
/// Shared finish handler — RankBattleFinishTask parses the same wire shape for
/// all four URLs and routes server-side by URL (vs IsAINetwork flag in the client).
/// Stubbed for Phase 3: echo battle_result, emit zeros elsewhere. Real rank
/// progression math is a separate spec.
/// all four URLs. Grants class XP + rank progression (+100 win / -50 loss,
/// tier-floored). Format is baked into the route.
/// </summary>
[HttpPost("/rotation_rank_battle/finish")]
[HttpPost("/unlimited_rank_battle/finish")]
[HttpPost("/ai_rotation_rank_battle/finish")]
[HttpPost("/ai_unlimited_rank_battle/finish")]
public async Task<IActionResult> Finish([FromBody] RankBattleFinishRequestDto req, CancellationToken ct)
private async Task<IActionResult> FinishInternal(Format format, RankBattleFinishRequestDto req, CancellationToken ct)
{
if (!TryGetViewerId(out var vid)) return Unauthorized();
@@ -127,10 +149,12 @@ public sealed class RankBattleController : ControllerBase
}
int gainXp = 0, totalXp = 0, level = 1;
var viewer = await _viewers.LoadForBattleXpGrantAsync(vid, ct);
var rankResult = new RankProgressResult(0, 0, 0, 0, 0, false, false);
var viewer = await _viewers.LoadForRankProgressAsync(vid, ct);
if (viewer is not null)
{
var xp = await _xp.GrantAsync(viewer, req.ClassId, isWin, BattleXpMode.Rank, ct);
rankResult = await _rankProgress.GrantAsync(viewer, format, isWin, ct);
await _db.SaveChangesAsync(ct);
gainXp = xp.GetXp;
totalXp = xp.TotalXp;
@@ -139,10 +163,15 @@ public sealed class RankBattleController : ControllerBase
return Ok(new RankBattleFinishResponseDto
{
BattleResult = req.BattleResult,
BattleResult = req.BattleResult,
GetClassExperience = gainXp,
ClassExperience = totalXp,
ClassLevel = level,
ClassExperience = totalXp,
ClassLevel = level,
Rank = rankResult.Rank,
AfterBattlePoint = rankResult.AfterBattlePoint,
AfterMasterPoint = rankResult.AfterMasterPoint,
BattlePoint = rankResult.BattlePoint,
MasterPoint = rankResult.MasterPoint,
});
}

View File

@@ -101,6 +101,8 @@ public class Program
SVSim.Database.Services.Inventory.InventoryService>();
builder.Services.AddScoped<SVSim.Database.Services.BattleXp.IBattleXpService,
SVSim.Database.Services.BattleXp.BattleXpService>();
builder.Services.AddScoped<SVSim.Database.Services.RankProgress.IRankProgressService,
SVSim.Database.Services.RankProgress.RankProgressService>();
builder.Services.AddScoped<SVSim.Database.Repositories.BattlePass.IBattlePassRepository,
SVSim.Database.Repositories.BattlePass.BattlePassRepository>();
builder.Services.AddScoped<SVSim.Database.Repositories.BattlePass.IViewerBattlePassRepository,