diff --git a/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs b/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs index 0239c2ea..e7d1e109 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/RankBattleController.cs @@ -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 { diff --git a/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs b/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs index da6e89cb..f107f93f 100644 --- a/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs @@ -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(); + 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)); + } }