feat(missions): emit progress events on story/finish and practice/finish
Story emits story_chapter_finish:<main|limited|event>:<story_id>. Practice emits practice_win:<difficulty>:<enemy_class_id> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
|||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
@@ -14,11 +15,16 @@ public class PracticeController : SVSimController
|
|||||||
{
|
{
|
||||||
private readonly IDeckRepository _deckRepository;
|
private readonly IDeckRepository _deckRepository;
|
||||||
private readonly IGlobalsRepository _globalsRepository;
|
private readonly IGlobalsRepository _globalsRepository;
|
||||||
|
private readonly IMissionProgressService _missionProgress;
|
||||||
|
|
||||||
public PracticeController(IDeckRepository deckRepository, IGlobalsRepository globalsRepository)
|
public PracticeController(
|
||||||
|
IDeckRepository deckRepository,
|
||||||
|
IGlobalsRepository globalsRepository,
|
||||||
|
IMissionProgressService missionProgress)
|
||||||
{
|
{
|
||||||
_deckRepository = deckRepository;
|
_deckRepository = deckRepository;
|
||||||
_globalsRepository = globalsRepository;
|
_globalsRepository = globalsRepository;
|
||||||
|
_missionProgress = missionProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,15 +89,29 @@ public class PracticeController : SVSimController
|
|||||||
/// XP / no rewards. Class XP bookkeeping is deferred until a per-class XP store exists.
|
/// XP / no rewards. Class XP bookkeeping is deferred until a per-class XP store exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("finish")]
|
[HttpPost("finish")]
|
||||||
public Task<PracticeFinishResponse> Finish(PracticeFinishRequest request)
|
public async Task<PracticeFinishResponse> 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,
|
GetClassExperience = 0,
|
||||||
ClassExperience = 0,
|
ClassExperience = 0,
|
||||||
ClassLevel = 1,
|
ClassLevel = 1,
|
||||||
AchievedInfo = new Dictionary<string, object>(),
|
AchievedInfo = new Dictionary<string, object>(),
|
||||||
RewardList = new List<Models.Dtos.Common.Reward>()
|
RewardList = new List<Models.Dtos.Common.Reward>()
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
|||||||
public class StoryController : SVSimController
|
public class StoryController : SVSimController
|
||||||
{
|
{
|
||||||
private readonly IStoryService _service;
|
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("/story/section")]
|
||||||
[HttpPost("/main_story/section")]
|
[HttpPost("/main_story/section")]
|
||||||
@@ -65,7 +70,28 @@ public class StoryController : SVSimController
|
|||||||
public async Task<ActionResult<FinishResponse>> Finish(FinishRequest req)
|
public async Task<ActionResult<FinishResponse>> Finish(FinishRequest req)
|
||||||
{
|
{
|
||||||
if (!TryGetViewerId(out long vid)) return Unauthorized();
|
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")]
|
[HttpPost("/main_story/all_finish")]
|
||||||
|
|||||||
Reference in New Issue
Block a user