refactor(arena-tk2): FinishAsync returns RunFinishOutcome with WasFullClear signal
Introduces RunFinishOutcome — a semantic record that wraps the wire FinishResponseDto with the controller-side WasFullClear signal used to fire the challenge_full_clear mission event. The wire response isn't affected (controller unwraps the .Response field before returning to the client); RetireAsync stays returning FinishResponseDto because retire cannot full-clear. Full-clear detection compares run.WinCount against ResolveMaxBattleCountAsync rather than hard-coding 5, so a future TK2 rules-config change tracks automatically. New test covers the full-clear signal explicitly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,11 @@ public class ArenaTwoPickController : SVSimController
|
||||
public async Task<IActionResult> Finish([FromBody] FinishRequest _)
|
||||
{
|
||||
if (!TryGetViewerId(out var vid)) return Unauthorized();
|
||||
return await GuardAsync(() => _svc.FinishAsync(vid));
|
||||
return await GuardAsync(async () =>
|
||||
{
|
||||
var outcome = await _svc.FinishAsync(vid);
|
||||
return outcome.Response;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GuardAsync<T>(Func<Task<T>> action)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ArenaTwoPick;
|
||||
|
||||
/// <summary>
|
||||
/// What <see cref="Services.IArenaTwoPickService.FinishAsync(long)"/> returns to
|
||||
/// <c>ArenaTwoPickController.Finish</c>. The wire response is <see cref="Response"/>;
|
||||
/// <see cref="WasFullClear"/> is a controller-side signal for the
|
||||
/// <c>challenge_full_clear</c> mission emit and is never serialized.
|
||||
/// </summary>
|
||||
public sealed record RunFinishOutcome(FinishResponseDto Response, bool WasFullClear);
|
||||
@@ -281,11 +281,27 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
public Task<FinishResponseDto> RetireAsync(long viewerId) => GrantRunRewardsAndDeleteAsync(viewerId, requireComplete: false);
|
||||
public async Task<FinishResponseDto> RetireAsync(long viewerId)
|
||||
{
|
||||
var (response, _) = await GrantRunRewardsAndDeleteAsync(viewerId, requireComplete: false);
|
||||
return response;
|
||||
}
|
||||
|
||||
public Task<FinishResponseDto> FinishAsync(long viewerId) => GrantRunRewardsAndDeleteAsync(viewerId, requireComplete: true);
|
||||
public async Task<RunFinishOutcome> FinishAsync(long viewerId)
|
||||
{
|
||||
var (response, winCount) = await GrantRunRewardsAndDeleteAsync(viewerId, requireComplete: true);
|
||||
// TK2 is 5 battles per run; full clear == 5 wins == 0 losses. Reference the reward
|
||||
// catalog's max WinCount rather than a magic 5 so a future rules-config change tracks.
|
||||
int maxBattles = await ResolveMaxBattleCountAsync();
|
||||
return new RunFinishOutcome(response, WasFullClear: winCount >= maxBattles);
|
||||
}
|
||||
|
||||
private async Task<FinishResponseDto> GrantRunRewardsAndDeleteAsync(long viewerId, bool requireComplete)
|
||||
/// <summary>
|
||||
/// Grants rewards for the given run tier and deletes the run row. Returns the wire
|
||||
/// response DTO plus the run's final WinCount — the latter feeds
|
||||
/// <see cref="FinishAsync"/>'s full-clear detection but isn't exposed on the wire.
|
||||
/// </summary>
|
||||
private async Task<(FinishResponseDto Response, int WinCount)> GrantRunRewardsAndDeleteAsync(long viewerId, bool requireComplete)
|
||||
{
|
||||
var run = await _runs.GetByViewerIdAsync(viewerId)
|
||||
?? throw new ArenaTwoPickException("arena_two_pick_no_active_run");
|
||||
@@ -345,8 +361,9 @@ public class ArenaTwoPickService : IArenaTwoPickService
|
||||
.Select(g => new RewardEntryDto { RewardType = (int)g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum })
|
||||
.ToList();
|
||||
|
||||
int winCountAtFinish = run.WinCount;
|
||||
await _runs.DeleteAsync(viewerId);
|
||||
return new FinishResponseDto { Rewards = deltas, RewardList = postStates };
|
||||
return (new FinishResponseDto { Rewards = deltas, RewardList = postStates }, winCountAtFinish);
|
||||
}
|
||||
|
||||
private static SVSim.Database.Models.ArenaTwoPickReward WeightedPick(
|
||||
|
||||
@@ -9,7 +9,12 @@ public interface IArenaTwoPickService
|
||||
Task<ClassChooseResponseDto> ChooseClassAsync(long viewerId, int classId);
|
||||
Task<CardChooseResponseDto> ChooseCardAsync(long viewerId, long selectedId);
|
||||
Task<FinishResponseDto> RetireAsync(long viewerId);
|
||||
Task<FinishResponseDto> FinishAsync(long viewerId);
|
||||
/// <summary>
|
||||
/// Ends a completed TK2 run (5 battles played) and grants the reward tier for
|
||||
/// <c>run.WinCount</c>. Returns both the wire-shape rewards and a controller-side
|
||||
/// signal for mission emit (<see cref="RunFinishOutcome.WasFullClear"/>).
|
||||
/// </summary>
|
||||
Task<RunFinishOutcome> FinishAsync(long viewerId);
|
||||
Task<BattleFinishResultDto> RecordBattleResultAsync(long viewerId, bool isWin);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user