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 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 08:54:52 -04:00
parent 81771bb0a0
commit f85f221bfa
7 changed files with 31 additions and 23 deletions

View File

@@ -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;

View File

@@ -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<TopResponseDto> 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()

View File

@@ -64,6 +64,7 @@ public class ArenaTwoPickServiceDraftTests
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
new SystemRandom(seed: 1),
db);

View File

@@ -59,6 +59,7 @@ public class ArenaTwoPickServiceEntryTests
config,
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
inv,
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
new SystemRandom(seed: 1234),
db);

View File

@@ -80,6 +80,7 @@ public class ArenaTwoPickServiceFinishTests
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
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<List<bool>>(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()
{

View File

@@ -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);
}
}

View File

@@ -90,6 +90,7 @@ public class ArenaTwoPickServiceWeightedRewardsTests
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
rng,
db);