diff --git a/SVSim.EmulatedEntrypoint/Controllers/FreeBattleController.cs b/SVSim.EmulatedEntrypoint/Controllers/FreeBattleController.cs index 771b5fb3..a290620a 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/FreeBattleController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/FreeBattleController.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SVSim.BattleNode.Bridge; +using SVSim.Database; using SVSim.Database.Enums; +using SVSim.Database.Repositories.Viewer; +using SVSim.Database.Services.BattleXp; using SVSim.Database.Services.Friend; using SVSim.Database.Services.Replay; using SVSim.EmulatedEntrypoint.Constants; @@ -32,6 +35,9 @@ public sealed class FreeBattleController : ControllerBase private readonly IBattleContextStore _battleContextStore; private readonly IBattleHistoryWriter _historyWriter; private readonly IPlayedTogetherWriter _playedTogetherWriter; + private readonly IViewerRepository _viewers; + private readonly IBattleXpService _xp; + private readonly SVSimDbContext _db; private readonly ILogger _log; public FreeBattleController( @@ -40,6 +46,9 @@ public sealed class FreeBattleController : ControllerBase IBattleContextStore battleContextStore, IBattleHistoryWriter historyWriter, IPlayedTogetherWriter playedTogetherWriter, + IViewerRepository viewers, + IBattleXpService xp, + SVSimDbContext db, ILogger log) { _resolver = resolver; @@ -47,6 +56,9 @@ public sealed class FreeBattleController : ControllerBase _battleContextStore = battleContextStore; _historyWriter = historyWriter; _playedTogetherWriter = playedTogetherWriter; + _viewers = viewers; + _xp = xp; + _db = db; _log = log; } @@ -97,10 +109,23 @@ public sealed class FreeBattleController : ControllerBase ct); } + int gainXp = 0, totalXp = 0, level = 1; + var viewer = await _viewers.LoadForBattleXpGrantAsync(vid, ct); + if (viewer is not null) + { + var xp = await _xp.GrantAsync(viewer, req.ClassId, isWin, BattleXpMode.Free, ct); + await _db.SaveChangesAsync(ct); + gainXp = xp.GetXp; + totalXp = xp.TotalXp; + level = xp.Level == 0 ? 1 : xp.Level; + } + return Ok(new FreeBattleFinishResponseDto { BattleResult = req.BattleResult, - // All other fields default to 0 (ClassLevel defaults to 1). + GetClassExperience = gainXp, + ClassExperience = totalXp, + ClassLevel = level, }); } diff --git a/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs b/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs index 4c8c252f..5dfdff3f 100644 --- a/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs @@ -114,22 +114,24 @@ public class FreeBattleControllerTests } [Test] - public async Task Finish_emits_battle_result_echo_and_four_field_minimum() + public async Task Finish_win_grants_class_xp_with_strict_field_subset() { await using var factory = new SVSimTestFactory(); var viewerId = await factory.SeedViewerAsync(); var client = factory.CreateAuthenticatedClient(viewerId); - var resp = await client.PostAsJsonAsync("/unlimited_free_battle/finish", FinishBody(battleResult: 1)); + var resp = await client.PostAsJsonAsync( + "/unlimited_free_battle/finish", FinishBody(battleResult: 1, classId: 1)); Assert.That(resp.IsSuccessStatusCode, Is.True); var raw = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(raw); var data = doc.RootElement; Assert.That(data.GetProperty("battle_result").GetInt32(), Is.EqualTo(1)); - Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(0)); - Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); - Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(1)); + // XpPerWin=100, classexp.csv L1=50 → L2 with 50 carry. + Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100)); + Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(50)); + Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); // Strict subset — no rank fields. The client doesn't read them on free-battle // finish; emitting them would be wire-format pollution. @@ -140,16 +142,20 @@ public class FreeBattleControllerTests } [Test] - public async Task Finish_with_consistency_result_echoes_2_on_rotation_url() + public async Task Finish_loss_grants_loss_xp_on_rotation_url() { await using var factory = new SVSimTestFactory(); var viewerId = await factory.SeedViewerAsync(); var client = factory.CreateAuthenticatedClient(viewerId); - var resp = await client.PostAsJsonAsync("/rotation_free_battle/finish", FinishBody(battleResult: 2)); + var resp = await client.PostAsJsonAsync( + "/rotation_free_battle/finish", FinishBody(battleResult: 2, classId: 1)); using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); Assert.That(doc.RootElement.GetProperty("battle_result").GetInt32(), Is.EqualTo(2)); + Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(25)); + Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(25)); + Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(1)); } [Test]