feat(battle-xp): wire colosseum /finish through IBattleXpService, extend response DTO
- ColosseumBattleFinishResponseDto gains get_class_experience, class_experience, class_level. Client-safe: same BattleFinishResponsProcessing handles all three keys as ints (decomp verified). - ArenaColosseumBattleController.FinishInternal grants win/loss XP via IBattleXpService with BattleXpMode.Colosseum for both /colosseum_battle/finish and /colosseum_rank_battle/finish URLs. - Existing win test extended: XP wire assertions + persistence check. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,9 @@ using System.Text.Json;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SVSim.BattleNode.Bridge;
|
using SVSim.BattleNode.Bridge;
|
||||||
|
using SVSim.Database;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
|
using SVSim.Database.Services.BattleXp;
|
||||||
using SVSim.EmulatedEntrypoint.Constants;
|
using SVSim.EmulatedEntrypoint.Constants;
|
||||||
using SVSim.EmulatedEntrypoint.Matching;
|
using SVSim.EmulatedEntrypoint.Matching;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.ArenaColosseum;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.ArenaColosseum;
|
||||||
@@ -36,6 +38,9 @@ public sealed class ArenaColosseumBattleController : ControllerBase
|
|||||||
private readonly IMatchingResolver _resolver;
|
private readonly IMatchingResolver _resolver;
|
||||||
private readonly IArenaColosseumRunRepository _runs;
|
private readonly IArenaColosseumRunRepository _runs;
|
||||||
private readonly IColosseumProgressionService _progression;
|
private readonly IColosseumProgressionService _progression;
|
||||||
|
private readonly IViewerRepository _viewers;
|
||||||
|
private readonly IBattleXpService _xp;
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
private readonly ILogger<ArenaColosseumBattleController> _log;
|
private readonly ILogger<ArenaColosseumBattleController> _log;
|
||||||
|
|
||||||
public ArenaColosseumBattleController(
|
public ArenaColosseumBattleController(
|
||||||
@@ -43,12 +48,18 @@ public sealed class ArenaColosseumBattleController : ControllerBase
|
|||||||
IMatchingResolver resolver,
|
IMatchingResolver resolver,
|
||||||
IArenaColosseumRunRepository runs,
|
IArenaColosseumRunRepository runs,
|
||||||
IColosseumProgressionService progression,
|
IColosseumProgressionService progression,
|
||||||
|
IViewerRepository viewers,
|
||||||
|
IBattleXpService xp,
|
||||||
|
SVSimDbContext db,
|
||||||
ILogger<ArenaColosseumBattleController> log)
|
ILogger<ArenaColosseumBattleController> log)
|
||||||
{
|
{
|
||||||
_ctxBuilder = ctxBuilder;
|
_ctxBuilder = ctxBuilder;
|
||||||
_resolver = resolver;
|
_resolver = resolver;
|
||||||
_runs = runs;
|
_runs = runs;
|
||||||
_progression = progression;
|
_progression = progression;
|
||||||
|
_viewers = viewers;
|
||||||
|
_xp = xp;
|
||||||
|
_db = db;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +173,27 @@ public sealed class ArenaColosseumBattleController : ControllerBase
|
|||||||
await _runs.UpsertAsync(run);
|
await _runs.UpsertAsync(run);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, req.BattleResult == 1, BattleXpMode.Colosseum, ct);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
gainXp = xp.GetXp;
|
||||||
|
totalXp = xp.TotalXp;
|
||||||
|
level = xp.Level == 0 ? 1 : xp.Level;
|
||||||
|
}
|
||||||
|
|
||||||
// result_code 3502 ("battle already finished") is the idempotent-retry tolerance per
|
// result_code 3502 ("battle already finished") is the idempotent-retry tolerance per
|
||||||
// FinishTaskBase.IsEffectiveErrorCode. Server emits standard data; the translation
|
// FinishTaskBase.IsEffectiveErrorCode. Server emits standard data; the translation
|
||||||
// middleware sets the wire result_code via data_headers from the response envelope.
|
// middleware sets the wire result_code via data_headers from the response envelope.
|
||||||
return Ok(new ColosseumBattleFinishResponseDto { BattleResult = req.BattleResult });
|
return Ok(new ColosseumBattleFinishResponseDto
|
||||||
|
{
|
||||||
|
BattleResult = req.BattleResult,
|
||||||
|
GetClassExperience = gainXp,
|
||||||
|
ClassExperience = totalXp,
|
||||||
|
ClassLevel = level,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<int> ParseIntList(string json) =>
|
private static List<int> ParseIntList(string json) =>
|
||||||
|
|||||||
@@ -46,4 +46,13 @@ public sealed class ColosseumBattleFinishResponseDto
|
|||||||
{
|
{
|
||||||
[JsonPropertyName("battle_result")] [Key("battle_result")]
|
[JsonPropertyName("battle_result")] [Key("battle_result")]
|
||||||
public int BattleResult { get; set; }
|
public int BattleResult { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("get_class_experience")] [Key("get_class_experience")]
|
||||||
|
public int GetClassExperience { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("class_experience")] [Key("class_experience")]
|
||||||
|
public int ClassExperience { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("class_level")] [Key("class_level")]
|
||||||
|
public int ClassLevel { get; set; } = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ public class ArenaColosseumBattleControllerTests
|
|||||||
var resp = await client.PostAsync("/colosseum_battle/finish", JsonContent.Create(req));
|
var resp = await client.PostAsync("/colosseum_battle/finish", JsonContent.Create(req));
|
||||||
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
||||||
|
|
||||||
|
// XP wire assertions
|
||||||
|
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
|
||||||
|
Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100));
|
||||||
|
Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(50));
|
||||||
|
Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(2));
|
||||||
|
|
||||||
using var scope = factory.Services.CreateScope();
|
using var scope = factory.Services.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
var run = await db.ViewerArenaColosseumRuns.FirstAsync(r => r.ViewerId == vid);
|
var run = await db.ViewerArenaColosseumRuns.FirstAsync(r => r.ViewerId == vid);
|
||||||
@@ -125,6 +131,13 @@ public class ArenaColosseumBattleControllerTests
|
|||||||
Assert.That(run.LossCount, Is.EqualTo(0));
|
Assert.That(run.LossCount, Is.EqualTo(0));
|
||||||
Assert.That(run.BattleCountThisRound, Is.EqualTo(1));
|
Assert.That(run.BattleCountThisRound, Is.EqualTo(1));
|
||||||
Assert.That(run.ResultListJson, Is.EqualTo("[1]"));
|
Assert.That(run.ResultListJson, Is.EqualTo("[1]"));
|
||||||
|
|
||||||
|
// Persisted class XP on viewer
|
||||||
|
var v = await db.Viewers.Include(x => x.Classes).ThenInclude(c => c.Class)
|
||||||
|
.FirstAsync(x => x.Id == vid);
|
||||||
|
var cls1 = v.Classes.Single(c => c.Class.Id == 1);
|
||||||
|
Assert.That(cls1.Level, Is.EqualTo(2));
|
||||||
|
Assert.That(cls1.Exp, Is.EqualTo(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user