From f85f221bfade9bd358e0260c5c8920b5d77df477 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 4 Jul 2026 08:54:52 -0400 Subject: [PATCH] refactor(battle-xp): retire ArenaTwoPickConfig.ClassXpPerBattle for shared BattleXpService - ArenaTwoPickService now delegates class-XP grants to IBattleXpService with BattleXpMode.ArenaTwoPick. Inline GrantClassXp/ResolveClassLevel helpers deleted. - ClassXpPerBattle field removed from ArenaTwoPickConfig; TK2 XP now configurable via ArenaTwoPickXpPerWin/XpPerLoss overrides or the global BattleXpConfig defaults (100/25). - Updated 5 test files (ArenaTwoPickService callers) for the new constructor arg. Added loss-XP assertion to Finish tests. Co-Authored-By: Claude Opus 4.7 --- .../Models/Config/ArenaTwoPickConfig.cs | 1 - .../Services/ArenaTwoPickService.cs | 28 ++++++------------- .../Services/ArenaTwoPickServiceDraftTests.cs | 1 + .../Services/ArenaTwoPickServiceEntryTests.cs | 1 + .../ArenaTwoPickServiceFinishTests.cs | 20 ++++++++++++- .../Services/ArenaTwoPickServiceTopTests.cs | 2 +- ...ArenaTwoPickServiceWeightedRewardsTests.cs | 1 + 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs b/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs index 79d865d7..d9b82dec 100644 --- a/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs +++ b/SVSim.Database/Models/Config/ArenaTwoPickConfig.cs @@ -10,7 +10,6 @@ public class ArenaTwoPickConfig { public int RewardScheduleId { get; set; } = 1; public int ChallengeId { get; set; } = 1; - public int ClassXpPerBattle { get; set; } = 100; public int SpotPointsPerBattle { get; set; } = 10; public double LegendaryRate { get; set; } = 0.06; diff --git a/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs b/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs index bac2a306..2bf22974 100644 --- a/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs +++ b/SVSim.EmulatedEntrypoint/Services/ArenaTwoPickService.cs @@ -6,6 +6,7 @@ using SVSim.Database.Models; using SVSim.Database.Repositories.Globals; using SVSim.Database.Repositories.Viewer; using SVSim.Database.Services; +using SVSim.Database.Services.BattleXp; using SVSim.Database.Services.Inventory; using SVSim.EmulatedEntrypoint.Models.Dtos.Common.ArenaTwoPick; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ArenaTwoPick; @@ -20,6 +21,7 @@ public class ArenaTwoPickService : IArenaTwoPickService private readonly IGameConfigService _config; private readonly IViewerRepository _viewers; private readonly IInventoryService _inv; + private readonly IBattleXpService _xp; private readonly IRandom _rng; private readonly SVSimDbContext _db; @@ -30,11 +32,12 @@ public class ArenaTwoPickService : IArenaTwoPickService IGameConfigService config, IViewerRepository viewers, IInventoryService inv, + IBattleXpService xp, IRandom rng, SVSimDbContext db) { _runs = runs; _rewards = rewards; _pool = pool; _config = config; - _viewers = viewers; _inv = inv; _rng = rng; _db = db; + _viewers = viewers; _inv = inv; _xp = xp; _rng = rng; _db = db; } public async Task GetTopAsync(long viewerId) @@ -375,8 +378,7 @@ public class ArenaTwoPickService : IArenaTwoPickService var viewer = await LoadViewerForGrantsAsync(viewerId); int before = (int)(viewer.Currency?.SpotPoints ?? 0); - int newClassXp = GrantClassXp(viewer, run.ClassId, aCfg.ClassXpPerBattle); - int classLevel = ResolveClassLevel(viewer, run.ClassId); + var xp = await _xp.GrantAsync(viewer, run.ClassId, isWin, BattleXpMode.ArenaTwoPick); viewer.Currency!.SpotPoints += (ulong)aCfg.SpotPointsPerBattle; int after = (int)viewer.Currency.SpotPoints; @@ -385,29 +387,15 @@ public class ArenaTwoPickService : IArenaTwoPickService return new BattleFinishResultDto { BattleResult = isWin ? 1 : 0, - GetClassExperience = aCfg.ClassXpPerBattle, - ClassExperience = newClassXp, - ClassLevel = classLevel, + GetClassExperience = xp.GetXp, + ClassExperience = xp.TotalXp, + ClassLevel = xp.Level, BeforeSpotPoint = before, AddSpotPoint = aCfg.SpotPointsPerBattle, AfterSpotPoint = after, }; } - private static int GrantClassXp(SVSim.Database.Models.Viewer viewer, int classId, int xp) - { - var row = viewer.Classes.FirstOrDefault(c => c.Class.Id == classId); - if (row is null) return 0; - row.Exp += xp; - return row.Exp; - } - - private static int ResolveClassLevel(SVSim.Database.Models.Viewer viewer, int classId) - { - var row = viewer.Classes.FirstOrDefault(c => c.Class.Id == classId); - return row is null ? 1 : row.Level; - } - // --- projection helpers (kept internal so test subclasses could exercise if needed) --- internal static EntryInfoDto ProjectEntryInfo(ViewerArenaTwoPickRun run, long viewerId) => new() diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceDraftTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceDraftTests.cs index d6aa9fae..58b33286 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceDraftTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceDraftTests.cs @@ -64,6 +64,7 @@ public class ArenaTwoPickServiceDraftTests scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), new SystemRandom(seed: 1), db); diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs index 86491a12..e2cf8b36 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceEntryTests.cs @@ -59,6 +59,7 @@ public class ArenaTwoPickServiceEntryTests config, scope.ServiceProvider.GetRequiredService(), inv, + scope.ServiceProvider.GetRequiredService(), new SystemRandom(seed: 1234), db); diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs index dddaae60..fed1d0f5 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs @@ -80,6 +80,7 @@ public class ArenaTwoPickServiceFinishTests scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), new SystemRandom(seed: 1), db); @@ -136,13 +137,30 @@ public class ArenaTwoPickServiceFinishTests var result = await svc.RecordBattleResultAsync(vid, isWin: true); Assert.That(result.BattleResult, Is.EqualTo(1)); - Assert.That(result.GetClassExperience, Is.EqualTo(100)); + Assert.That(result.GetClassExperience, Is.EqualTo(100), + "Default BattleXpConfig.XpPerWin"); Assert.That(result.AddSpotPoint, Is.EqualTo(10)); var run = await db.ViewerArenaTwoPickRuns.FirstAsync(r => r.ViewerId == vid); Assert.That(run.WinCount, Is.EqualTo(2)); Assert.That(JsonSerializer.Deserialize>(run.ResultListJson)!.Count, Is.EqualTo(2)); } + [Test] + public async Task RecordBattleResultAsync_loss_grants_loss_xp() + { + var (db, svc, vid) = await SetupWithRunAsync(winCount: 0, lossCount: 0); + await using var _ = db; + + var result = await svc.RecordBattleResultAsync(vid, isWin: false); + + Assert.That(result.BattleResult, Is.EqualTo(0)); + Assert.That(result.GetClassExperience, Is.EqualTo(25), + "Default BattleXpConfig.XpPerLoss"); + Assert.That(result.ClassExperience, Is.EqualTo(25), + "Fresh viewer with Exp=0, +25 loss XP < curve[1]=50 so no level-up."); + Assert.That(result.ClassLevel, Is.EqualTo(1)); + } + [Test] public async Task RecordBattleResultAsync_increments_loss_without_terminating() { diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceTopTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceTopTests.cs index 2bf71212..4c5a3d4a 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceTopTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceTopTests.cs @@ -82,6 +82,6 @@ public class ArenaTwoPickServiceTopTests { // GetTopAsync only uses _runs — every other dep can be null! because the test path // never touches them. - return new ArenaTwoPickService(runRepo, null!, null!, null!, null!, null!, null!, db); + return new ArenaTwoPickService(runRepo, null!, null!, null!, null!, null!, null!, null!, db); } } diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceWeightedRewardsTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceWeightedRewardsTests.cs index 591b24d5..6aa38926 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceWeightedRewardsTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceWeightedRewardsTests.cs @@ -90,6 +90,7 @@ public class ArenaTwoPickServiceWeightedRewardsTests scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), rng, db);