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 RewardScheduleId { get; set; } = 1;
|
||||||
public int ChallengeId { get; set; } = 1;
|
public int ChallengeId { get; set; } = 1;
|
||||||
public int ClassXpPerBattle { get; set; } = 100;
|
|
||||||
public int SpotPointsPerBattle { get; set; } = 10;
|
public int SpotPointsPerBattle { get; set; } = 10;
|
||||||
|
|
||||||
public double LegendaryRate { get; set; } = 0.06;
|
public double LegendaryRate { get; set; } = 0.06;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using SVSim.Database.Models;
|
|||||||
using SVSim.Database.Repositories.Globals;
|
using SVSim.Database.Repositories.Globals;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
using SVSim.Database.Services;
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.Database.Services.BattleXp;
|
||||||
using SVSim.Database.Services.Inventory;
|
using SVSim.Database.Services.Inventory;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.ArenaTwoPick;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.ArenaTwoPick;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ArenaTwoPick;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ArenaTwoPick;
|
||||||
@@ -20,6 +21,7 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
|||||||
private readonly IGameConfigService _config;
|
private readonly IGameConfigService _config;
|
||||||
private readonly IViewerRepository _viewers;
|
private readonly IViewerRepository _viewers;
|
||||||
private readonly IInventoryService _inv;
|
private readonly IInventoryService _inv;
|
||||||
|
private readonly IBattleXpService _xp;
|
||||||
private readonly IRandom _rng;
|
private readonly IRandom _rng;
|
||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
|
|
||||||
@@ -30,11 +32,12 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
|||||||
IGameConfigService config,
|
IGameConfigService config,
|
||||||
IViewerRepository viewers,
|
IViewerRepository viewers,
|
||||||
IInventoryService inv,
|
IInventoryService inv,
|
||||||
|
IBattleXpService xp,
|
||||||
IRandom rng,
|
IRandom rng,
|
||||||
SVSimDbContext db)
|
SVSimDbContext db)
|
||||||
{
|
{
|
||||||
_runs = runs; _rewards = rewards; _pool = pool; _config = config;
|
_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)
|
public async Task<TopResponseDto> GetTopAsync(long viewerId)
|
||||||
@@ -375,8 +378,7 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
|||||||
var viewer = await LoadViewerForGrantsAsync(viewerId);
|
var viewer = await LoadViewerForGrantsAsync(viewerId);
|
||||||
int before = (int)(viewer.Currency?.SpotPoints ?? 0);
|
int before = (int)(viewer.Currency?.SpotPoints ?? 0);
|
||||||
|
|
||||||
int newClassXp = GrantClassXp(viewer, run.ClassId, aCfg.ClassXpPerBattle);
|
var xp = await _xp.GrantAsync(viewer, run.ClassId, isWin, BattleXpMode.ArenaTwoPick);
|
||||||
int classLevel = ResolveClassLevel(viewer, run.ClassId);
|
|
||||||
|
|
||||||
viewer.Currency!.SpotPoints += (ulong)aCfg.SpotPointsPerBattle;
|
viewer.Currency!.SpotPoints += (ulong)aCfg.SpotPointsPerBattle;
|
||||||
int after = (int)viewer.Currency.SpotPoints;
|
int after = (int)viewer.Currency.SpotPoints;
|
||||||
@@ -385,29 +387,15 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
|||||||
return new BattleFinishResultDto
|
return new BattleFinishResultDto
|
||||||
{
|
{
|
||||||
BattleResult = isWin ? 1 : 0,
|
BattleResult = isWin ? 1 : 0,
|
||||||
GetClassExperience = aCfg.ClassXpPerBattle,
|
GetClassExperience = xp.GetXp,
|
||||||
ClassExperience = newClassXp,
|
ClassExperience = xp.TotalXp,
|
||||||
ClassLevel = classLevel,
|
ClassLevel = xp.Level,
|
||||||
BeforeSpotPoint = before,
|
BeforeSpotPoint = before,
|
||||||
AddSpotPoint = aCfg.SpotPointsPerBattle,
|
AddSpotPoint = aCfg.SpotPointsPerBattle,
|
||||||
AfterSpotPoint = after,
|
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) ---
|
// --- projection helpers (kept internal so test subclasses could exercise if needed) ---
|
||||||
|
|
||||||
internal static EntryInfoDto ProjectEntryInfo(ViewerArenaTwoPickRun run, long viewerId) => new()
|
internal static EntryInfoDto ProjectEntryInfo(ViewerArenaTwoPickRun run, long viewerId) => new()
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public class ArenaTwoPickServiceDraftTests
|
|||||||
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
|
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
|
||||||
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
||||||
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
|
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
|
||||||
|
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
|
||||||
new SystemRandom(seed: 1),
|
new SystemRandom(seed: 1),
|
||||||
db);
|
db);
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public class ArenaTwoPickServiceEntryTests
|
|||||||
config,
|
config,
|
||||||
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
||||||
inv,
|
inv,
|
||||||
|
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
|
||||||
new SystemRandom(seed: 1234),
|
new SystemRandom(seed: 1234),
|
||||||
db);
|
db);
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ public class ArenaTwoPickServiceFinishTests
|
|||||||
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
|
scope.ServiceProvider.GetRequiredService<IGameConfigService>(),
|
||||||
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
||||||
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
|
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
|
||||||
|
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
|
||||||
new SystemRandom(seed: 1),
|
new SystemRandom(seed: 1),
|
||||||
db);
|
db);
|
||||||
|
|
||||||
@@ -136,13 +137,30 @@ public class ArenaTwoPickServiceFinishTests
|
|||||||
var result = await svc.RecordBattleResultAsync(vid, isWin: true);
|
var result = await svc.RecordBattleResultAsync(vid, isWin: true);
|
||||||
|
|
||||||
Assert.That(result.BattleResult, Is.EqualTo(1));
|
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));
|
Assert.That(result.AddSpotPoint, Is.EqualTo(10));
|
||||||
var run = await db.ViewerArenaTwoPickRuns.FirstAsync(r => r.ViewerId == vid);
|
var run = await db.ViewerArenaTwoPickRuns.FirstAsync(r => r.ViewerId == vid);
|
||||||
Assert.That(run.WinCount, Is.EqualTo(2));
|
Assert.That(run.WinCount, Is.EqualTo(2));
|
||||||
Assert.That(JsonSerializer.Deserialize<List<bool>>(run.ResultListJson)!.Count, 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]
|
[Test]
|
||||||
public async Task RecordBattleResultAsync_increments_loss_without_terminating()
|
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
|
// GetTopAsync only uses _runs — every other dep can be null! because the test path
|
||||||
// never touches them.
|
// 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<IGameConfigService>(),
|
||||||
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
scope.ServiceProvider.GetRequiredService<IViewerRepository>(),
|
||||||
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
|
scope.ServiceProvider.GetRequiredService<IInventoryService>(),
|
||||||
|
scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
|
||||||
rng,
|
rng,
|
||||||
db);
|
db);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user