From 8e355019540c2a30eaf1ac88217b9c544c4a5d94 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 27 May 2026 10:51:05 -0400 Subject: [PATCH] feat(missions): emit progress events on story/finish and practice/finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Story emits story_chapter_finish::. Practice emits practice_win:: on win only. Practice catalog rows use opponent NAMES (e.g. practice_win:elite:arisa) not numeric class_ids, so captured catalog rows won't match yet. The infrastructure is in place; bridging numeric→name is a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Controllers/PracticeController.cs | 28 ++++++++++++++--- .../Controllers/StoryController.cs | 30 +++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/PracticeController.cs b/SVSim.EmulatedEntrypoint/Controllers/PracticeController.cs index 6e2ff3e..7a31345 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/PracticeController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/PracticeController.cs @@ -7,6 +7,7 @@ using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice; using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice; +using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint.Controllers; @@ -14,11 +15,16 @@ public class PracticeController : SVSimController { private readonly IDeckRepository _deckRepository; private readonly IGlobalsRepository _globalsRepository; + private readonly IMissionProgressService _missionProgress; - public PracticeController(IDeckRepository deckRepository, IGlobalsRepository globalsRepository) + public PracticeController( + IDeckRepository deckRepository, + IGlobalsRepository globalsRepository, + IMissionProgressService missionProgress) { _deckRepository = deckRepository; _globalsRepository = globalsRepository; + _missionProgress = missionProgress; } /// @@ -83,15 +89,29 @@ public class PracticeController : SVSimController /// XP / no rewards. Class XP bookkeeping is deferred until a per-class XP store exists. /// [HttpPost("finish")] - public Task Finish(PracticeFinishRequest request) + public async Task Finish(PracticeFinishRequest request) { - return Task.FromResult(new PracticeFinishResponse + // Mission/achievement progress hook. Catalog rows for practice_win achievements use + // opponent NAMES (e.g. "practice_win:elite:arisa") — we only have numeric class_id / + // difficulty here, so we emit numeric forms. Bridging numeric→name to match captured + // catalog rows is a follow-up; the infrastructure is in place. + if (request.IsWin == 1 && TryGetViewerId(out long viewerId)) + { + await _missionProgress.RecordEventAsync(viewerId, new[] + { + "practice_win", + $"practice_win:{request.Difficulty}", + $"practice_win:{request.Difficulty}:{request.EnemyClassId}", + }); + } + + return new PracticeFinishResponse { GetClassExperience = 0, ClassExperience = 0, ClassLevel = 1, AchievedInfo = new Dictionary(), RewardList = new List() - }); + }; } } diff --git a/SVSim.EmulatedEntrypoint/Controllers/StoryController.cs b/SVSim.EmulatedEntrypoint/Controllers/StoryController.cs index 46a24c4..db4b372 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/StoryController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/StoryController.cs @@ -11,7 +11,12 @@ namespace SVSim.EmulatedEntrypoint.Controllers; public class StoryController : SVSimController { private readonly IStoryService _service; - public StoryController(IStoryService service) { _service = service; } + private readonly IMissionProgressService _missionProgress; + public StoryController(IStoryService service, IMissionProgressService missionProgress) + { + _service = service; + _missionProgress = missionProgress; + } [HttpPost("/story/section")] [HttpPost("/main_story/section")] @@ -65,7 +70,28 @@ public class StoryController : SVSimController public async Task> Finish(FinishRequest req) { if (!TryGetViewerId(out long vid)) return Unauthorized(); - return await _service.FinishAsync(ResolveApiType(), req, vid); + var result = await _service.FinishAsync(ResolveApiType(), req, vid); + + // Emit story-chapter-finish events for mission/achievement progress. + var apiType = ResolveApiType(); + var prefix = apiType switch + { + StoryApiType.Main => "main", + StoryApiType.Limited => "limited", + StoryApiType.Event => "event", + _ => null, + }; + if (prefix is not null && req.StoryId != 0) + { + await _missionProgress.RecordEventAsync(vid, new[] + { + "story_chapter_finish", + $"story_chapter_finish:{prefix}", + $"story_chapter_finish:{prefix}:{req.StoryId}", + }); + } + + return result; } [HttpPost("/main_story/all_finish")]