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

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