feat(mission): wire class_level_up emits from every XP-granting controller

Every _xp.GrantAsync callsite now conditionally emits class_level_up:{class}
when the returned BattleXpGrantResult.LeveledUp is true. Six callsites total:

- PracticeController.Finish
- RankBattleController.FinishInternal
- FreeBattleController.Finish
- ArenaTwoPickBattleController.Finish (LeveledUp + ClassId routed via
  BattleFinishResultDto — controller-side signals, not on wire)
- ArenaColosseumBattleController (missed in the original wire-up survey;
  colosseum-battle XP now also fires the level-up event when applicable)
- StoryController.Finish (LeveledUp + ClassId routed via new
  StoryFinishOutcome wrapper, mirroring the TK2 RunFinishOutcome pattern)

Story's IStoryService.FinishAsync now returns StoryFinishOutcome instead of
FinishResponse directly. Test callsites updated to unwrap .Response.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 13:31:04 -04:00
parent a2048a4d54
commit 8e41739bde
12 changed files with 87 additions and 23 deletions

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using SVSim.BattleNode.Bridge;
using SVSim.Database;
using SVSim.Database.Repositories.Viewer;
using SVSim.Database.Services;
using SVSim.Database.Services.BattleXp;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Matching;
@@ -40,6 +41,7 @@ public sealed class ArenaColosseumBattleController : ControllerBase
private readonly IColosseumProgressionService _progression;
private readonly IViewerRepository _viewers;
private readonly IBattleXpService _xp;
private readonly IMissionProgressService _missionProgress;
private readonly SVSimDbContext _db;
private readonly ILogger<ArenaColosseumBattleController> _log;
@@ -50,6 +52,7 @@ public sealed class ArenaColosseumBattleController : ControllerBase
IColosseumProgressionService progression,
IViewerRepository viewers,
IBattleXpService xp,
IMissionProgressService missionProgress,
SVSimDbContext db,
ILogger<ArenaColosseumBattleController> log)
{
@@ -59,6 +62,7 @@ public sealed class ArenaColosseumBattleController : ControllerBase
_progression = progression;
_viewers = viewers;
_xp = xp;
_missionProgress = missionProgress;
_db = db;
_log = log;
}
@@ -174,6 +178,7 @@ public sealed class ArenaColosseumBattleController : ControllerBase
}
int gainXp = 0, totalXp = 0, level = 1;
bool leveledUp = false;
var viewer = await _viewers.LoadForBattleXpGrantAsync(vid, ct);
if (viewer is not null)
{
@@ -182,6 +187,13 @@ public sealed class ArenaColosseumBattleController : ControllerBase
gainXp = xp.GetXp;
totalXp = xp.TotalXp;
level = xp.Level == 0 ? 1 : xp.Level;
leveledUp = xp.LeveledUp;
}
if (leveledUp)
{
await _missionProgress.RecordEventAsync(
vid, MissionEventKeys.ClassLevel.UpAll(req.ClassId), ct: ct);
}
// result_code 3502 ("battle already finished") is the idempotent-retry tolerance per