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:
@@ -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
|
||||
|
||||
@@ -128,6 +128,11 @@ public class ArenaTwoPickBattleController : SVSimController
|
||||
vid,
|
||||
isWin ? MissionEventKeys.Challenge.MatchWinAll() : MissionEventKeys.Challenge.MatchPlayAll(),
|
||||
ct: ct);
|
||||
if (result.LeveledUp)
|
||||
{
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.ClassLevel.UpAll(result.ClassId), ct: ct);
|
||||
}
|
||||
|
||||
return Ok(new BattleFinishResponseDto
|
||||
{
|
||||
|
||||
@@ -114,6 +114,7 @@ public sealed class FreeBattleController : ControllerBase
|
||||
}
|
||||
|
||||
int gainXp = 0, totalXp = 0, level = 1;
|
||||
bool leveledUp = false;
|
||||
var viewer = await _viewers.LoadForBattleXpGrantAsync(vid, ct);
|
||||
if (viewer is not null)
|
||||
{
|
||||
@@ -122,6 +123,7 @@ public sealed class FreeBattleController : ControllerBase
|
||||
gainXp = xp.GetXp;
|
||||
totalXp = xp.TotalXp;
|
||||
level = xp.Level == 0 ? 1 : xp.Level;
|
||||
leveledUp = xp.LeveledUp;
|
||||
}
|
||||
|
||||
// Mission counters — unranked wins feed ranked_or_arena_win + daily_match_win.
|
||||
@@ -130,6 +132,11 @@ public sealed class FreeBattleController : ControllerBase
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.Free.WinAll(), ct: ct);
|
||||
}
|
||||
if (leveledUp)
|
||||
{
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.ClassLevel.UpAll(req.ClassId), ct: ct);
|
||||
}
|
||||
|
||||
return Ok(new FreeBattleFinishResponseDto
|
||||
{
|
||||
|
||||
@@ -122,6 +122,7 @@ public class PracticeController : SVSimController
|
||||
}
|
||||
|
||||
int gainXp = 0, totalXp = 0, level = 1;
|
||||
bool leveledUp = false;
|
||||
var viewer = await _viewers.LoadForBattleXpGrantAsync(viewerId);
|
||||
if (viewer is not null)
|
||||
{
|
||||
@@ -130,6 +131,13 @@ public class PracticeController : SVSimController
|
||||
gainXp = xp.GetXp;
|
||||
totalXp = xp.TotalXp;
|
||||
level = xp.Level == 0 ? 1 : xp.Level;
|
||||
leveledUp = xp.LeveledUp;
|
||||
}
|
||||
|
||||
if (leveledUp)
|
||||
{
|
||||
await _missionProgress.RecordEventAsync(
|
||||
viewerId, MissionEventKeys.ClassLevel.UpAll(request.ClassId));
|
||||
}
|
||||
|
||||
return new PracticeFinishResponse
|
||||
|
||||
@@ -153,6 +153,7 @@ public sealed class RankBattleController : ControllerBase
|
||||
}
|
||||
|
||||
int gainXp = 0, totalXp = 0, level = 1;
|
||||
bool leveledUp = false;
|
||||
var rankResult = new RankProgressResult(0, 0, 0, 0, 0, false, false);
|
||||
var viewer = await _viewers.LoadForRankProgressAsync(vid, ct);
|
||||
if (viewer is not null)
|
||||
@@ -163,6 +164,7 @@ public sealed class RankBattleController : ControllerBase
|
||||
gainXp = xp.GetXp;
|
||||
totalXp = xp.TotalXp;
|
||||
level = xp.Level == 0 ? 1 : xp.Level;
|
||||
leveledUp = xp.LeveledUp;
|
||||
}
|
||||
|
||||
// Mission counters — AI-rank routes emit the same keys as human-rank on a private server.
|
||||
@@ -171,6 +173,11 @@ public sealed class RankBattleController : ControllerBase
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.Ranked.WinAll(req.ClassId), ct: ct);
|
||||
}
|
||||
if (leveledUp)
|
||||
{
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.ClassLevel.UpAll(req.ClassId), ct: ct);
|
||||
}
|
||||
|
||||
return Ok(new RankBattleFinishResponseDto
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@ public class StoryController : SVSimController
|
||||
public async Task<ActionResult<FinishResponse>> Finish(FinishRequest req)
|
||||
{
|
||||
if (!TryGetViewerId(out long vid)) return Unauthorized();
|
||||
var result = await _service.FinishAsync(ResolveApiType(), req, vid);
|
||||
var outcome = await _service.FinishAsync(ResolveApiType(), req, vid);
|
||||
|
||||
// Emit story-chapter-finish events for mission/achievement progress.
|
||||
var apiType = ResolveApiType();
|
||||
@@ -87,8 +87,13 @@ public class StoryController : SVSimController
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.Story.ChapterFinishAll(prefix, req.StoryId));
|
||||
}
|
||||
if (outcome.LeveledUp && outcome.ClassId.HasValue)
|
||||
{
|
||||
await _missionProgress.RecordEventAsync(
|
||||
vid, MissionEventKeys.ClassLevel.UpAll(outcome.ClassId.Value));
|
||||
}
|
||||
|
||||
return result;
|
||||
return outcome.Response;
|
||||
}
|
||||
|
||||
[HttpPost("/main_story/all_finish")]
|
||||
|
||||
Reference in New Issue
Block a user