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