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:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user