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:
gamer147
2026-07-04 09:02:25 -04:00
parent 91ec14fc7e
commit a03bff3ea7
3 changed files with 51 additions and 1 deletions

View File

@@ -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) =>

View File

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

View File

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