using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Repositories.Mission; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Models.Dtos.Achievement; using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint.Controllers; /// /// /achievement/* — claim achievement rewards. Wire shape mirrors AchievementReceiveRewardTask.cs. /// [Route("achievement")] public class AchievementController : SVSimController { private const int FailureResultCode = 2; private readonly SVSimDbContext _db; private readonly IMissionCatalogRepository _catalog; private readonly IViewerMissionStateService _state; private readonly IMissionAssembler _assembler; private readonly RewardGrantService _grantService; public AchievementController( SVSimDbContext db, IMissionCatalogRepository catalog, IViewerMissionStateService state, IMissionAssembler assembler, RewardGrantService grantService) { _db = db; _catalog = catalog; _state = state; _assembler = assembler; _grantService = grantService; } [HttpPost("receive_reward")] public async Task ReceiveReward( AchievementReceiveRewardRequest request, CancellationToken ct) { if (!TryGetViewerId(out long viewerId)) return Unauthorized(); // Load viewer with all the collections RewardGrantService may need to mutate. var viewer = await _db.Viewers .Include(v => v.MissionData) .Include(v => v.Currency) .Include(v => v.Cards) .Include(v => v.Items) .Include(v => v.Sleeves) .Include(v => v.Emblems) .Include(v => v.Degrees) .Include(v => v.LeaderSkins) .Include(v => v.MyPageBackgrounds) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId, ct); await _state.EnsureCurrentAsync(viewer.Id, ct); await _db.SaveChangesAsync(ct); // Re-read viewer's achievement for this type after state-service materialization. var ach = await _db.ViewerAchievements .FirstOrDefaultAsync(a => a.ViewerId == viewerId && a.AchievementType == request.AchievementType, ct); if (ach is null || ach.Level != request.Level) { return Ok(new { result_code = FailureResultCode }); } var catalogRow = await _catalog.GetAchievementAsync(request.AchievementType, request.Level, ct); if (catalogRow is null) { return Ok(new { result_code = FailureResultCode }); } // Grant via the canonical RewardGrantService primitive. var granted = await _grantService.ApplyAsync( viewer, (UserGoodsType)catalogRow.RewardType, catalogRow.RewardDetailId, catalogRow.RewardNumber, ct); // Advance viewer's level by 1. If no catalog row exists at the new level (i.e. just // claimed the highest captured tier), max_level on the wire stays the same and the // UI shows "claimed at max" until catalog grows. ach.Level += 1; var maxLevelByType = await _catalog.GetMaxLevelByAchievementTypeAsync(ct); if (maxLevelByType.TryGetValue(request.AchievementType, out int maxLevel) && ach.Level > maxLevel) { ach.AchievementStatus = 2; } else { ach.AchievementStatus = 0; } ach.NowAchievedLevel = request.Level; await _db.SaveChangesAsync(ct); var dto = await _assembler.BuildAsync(viewer, ct); var resp = new AchievementReceiveRewardResponse { UserMissionList = dto.UserMissionList, UserAchievementList = dto.UserAchievementList, BattlePassMonthlyMission = dto.BattlePassMonthlyMission, IsChangeMission = dto.IsChangeMission, CanChangeMissionTime = dto.CanChangeMissionTime, IsChangeReceiveType = dto.IsChangeReceiveType, CanChangeReceiveTypeTime = dto.CanChangeReceiveTypeTime, MissionReceiveType = dto.MissionReceiveType, RewardList = granted.Select(g => new RewardGrantDto { RewardType = g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum, }).ToList(), TotalReceiveCountList = granted.Select(g => new TotalReceiveCountDto { RewardType = g.RewardType, RewardDetailId = g.RewardId, RewardCount = g.RewardNum, ItemType = 0, IsUsable = true, }).ToList(), }; return Ok(resp); } }