feat(rank): AI-start SelfInfo carries viewer rank progress
/ai_*_rank_battle/start now reads ViewerRankProgress for the target format and populates SelfInfo.rank / battlePoint / isMasterRank / masterPoint so the pre-battle screen matches the viewer's real state. Read-only path uses IRankProgressService.GetAsync (added in this commit). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -284,6 +284,13 @@ public sealed class RankBattleController : ControllerBase
|
||||
var bot = await _botRoster.PickAsync(selfCtx, pending.BattleId, ct);
|
||||
var seed = Random.Shared.Next();
|
||||
|
||||
// Read the viewer's rank progression for this format so the pre-battle screen
|
||||
// shows the current tier/point rather than "Beginner 0 with 0 pts."
|
||||
var selfViewer = await _viewers.LoadForRankProgressAsync(vid, ct);
|
||||
var selfRank = selfViewer is null
|
||||
? new RankProgressResult(1, 0, 0, 0, 0, false, false)
|
||||
: await _rankProgress.GetAsync(selfViewer, format, ct);
|
||||
|
||||
// Stash battle context for the upcoming /finish so the replay-history hook can
|
||||
// compose a ViewerBattleHistory row. See docs/superpowers/specs/2026-06-10-replay-info-design.md.
|
||||
if (long.TryParse(pending.BattleId, out var battleIdLong))
|
||||
@@ -327,12 +334,12 @@ public sealed class RankBattleController : ControllerBase
|
||||
IsOfficial = selfCtx.IsOfficial,
|
||||
OppoId = bot.AiId,
|
||||
Seed = seed,
|
||||
Rank = 0,
|
||||
BattlePoint = 0,
|
||||
Rank = selfRank.Rank,
|
||||
BattlePoint = selfRank.AfterBattlePoint,
|
||||
ClassId = (int)selfCtx.ClassId,
|
||||
CharaId = int.TryParse(selfCtx.CharaId, out var chId) ? chId : -1,
|
||||
IsMasterRank = 0,
|
||||
MasterPoint = 0,
|
||||
IsMasterRank = selfRank.IsMasterRank ? 1 : 0,
|
||||
MasterPoint = selfRank.AfterMasterPoint,
|
||||
},
|
||||
OppoInfo = new AiBattlePlayerInfo
|
||||
{
|
||||
|
||||
@@ -367,4 +367,39 @@ public class RankBattleControllerTests
|
||||
var v = await db.Viewers.Include(x => x.RankProgress).FirstAsync(x => x.Id == viewerId);
|
||||
Assert.That(v.RankProgress.Single(r => r.Format == Format.Unlimited).Point, Is.EqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AiStart_SelfInfo_carries_viewer_rank_progress()
|
||||
{
|
||||
await using var factory = new SVSimTestFactory();
|
||||
var viewerId = await factory.SeedViewerAsync();
|
||||
await factory.SeedGlobalsAsync();
|
||||
await factory.SeedDeckAsync(viewerId, Format.Unlimited, 1);
|
||||
|
||||
// Preload Point=200 (Beginner 2 = rank_id 3) on Unlimited.
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var v = await db.Viewers.Include(x => x.RankProgress).FirstAsync(x => x.Id == viewerId);
|
||||
v.RankProgress.Add(new ViewerRankProgress
|
||||
{
|
||||
Format = Format.Unlimited, Point = 200, MasterPoint = 0,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await RegisterBotBattleAsync(factory, viewerId, Format.Unlimited, deckNo: 1);
|
||||
var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
var resp = await client.PostAsJsonAsync("/ai_unlimited_rank_battle/start", EmptyAuthedBody);
|
||||
|
||||
Assert.That(resp.IsSuccessStatusCode, Is.True);
|
||||
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
|
||||
// AiBattlePlayerInfo wire keys are camelCase (self_info/oppo_info aside);
|
||||
// see AiBattleStartResponseDto.cs class comment.
|
||||
var self = doc.RootElement.GetProperty("self_info");
|
||||
Assert.That(self.GetProperty("rank").GetInt32(), Is.EqualTo(3)); // 200 → Beginner 2
|
||||
Assert.That(self.GetProperty("battlePoint").GetInt32(), Is.EqualTo(200));
|
||||
Assert.That(self.GetProperty("isMasterRank").GetInt32(), Is.EqualTo(0));
|
||||
Assert.That(self.GetProperty("masterPoint").GetInt32(), Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user