using Google.Protobuf.WellKnownTypes; using Grpc.Core; using MariesWonderland.Data; using MariesWonderland.Extensions; using MariesWonderland.Helpers; using MariesWonderland.Models.Entities; using MariesWonderland.Models.Type; using MariesWonderland.Proto.BigHunt; namespace MariesWonderland.Services; public class BigHuntService(UserDataStore store, DarkMasterMemoryDatabase masterDb) : MariesWonderland.Proto.BigHunt.BighuntService.BighuntServiceBase { private readonly UserDataStore _store = store; private readonly DarkMasterMemoryDatabase _masterDb = masterDb; /// /// Begins a BigHunt quest run: deducts stamina for the selected quest, records the player's deck choice, /// and initialises per-boss-quest status tracking. /// public override Task StartBigHuntQuest(StartBigHuntQuestRequest request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); EntityMBigHuntQuest? bhQuest = _masterDb.EntityMBigHuntQuest .FirstOrDefault(q => q.BigHuntQuestId == request.BigHuntQuestId); if (bhQuest is not null) { HandleBigHuntQuestStart(userDb, bhQuest.QuestId, request.UserDeckNumber, nowMs); } // Set progress status EntityIUserBigHuntProgressStatus progress = GetOrCreateProgress(userDb); progress.CurrentBigHuntBossQuestId = request.BigHuntBossQuestId; progress.CurrentBigHuntQuestId = request.BigHuntQuestId; progress.CurrentQuestSceneId = 0; progress.IsDryRun = request.IsDryRun; // Store deck number in server-side session EntitySBigHuntSession session = GetOrCreateSession(userDb); session.DeckNumber = request.UserDeckNumber; // Update per-boss-quest status EntityIUserBigHuntStatus status = GetOrCreateStatus(userDb, request.BigHuntBossQuestId); status.DailyChallengeCount++; status.LatestChallengeDatetime = nowMs; return Task.FromResult(new StartBigHuntQuestResponse()); } /// /// Advances the player's current quest scene checkpoint during an active hunt run. /// public override Task UpdateBigHuntQuestSceneProgress(UpdateBigHuntQuestSceneProgressRequest request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); EntityIUserBigHuntProgressStatus progress = GetOrCreateProgress(userDb); progress.CurrentQuestSceneId = request.QuestSceneId; return Task.FromResult(new UpdateBigHuntQuestSceneProgressResponse()); } /// /// Concludes a hunt run: computes the final score from damage and permil bonuses (difficulty, alive, combo), /// updates high scores at boss/schedule/weekly levels, issues newly unlocked threshold rewards, and clears /// the in-progress session state. Returns an empty score info for retired or dry-run runs. /// public override Task FinishBigHuntQuest(FinishBigHuntQuestRequest request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); EntityMBigHuntQuest? bhQuest = _masterDb.EntityMBigHuntQuest .FirstOrDefault(q => q.BigHuntQuestId == request.BigHuntQuestId); EntityMBigHuntBossQuest? bossQuest = _masterDb.EntityMBigHuntBossQuest .FirstOrDefault(q => q.BigHuntBossQuestId == request.BigHuntBossQuestId); EntityMBigHuntBoss? boss = bossQuest is not null ? _masterDb.EntityMBigHuntBoss.FirstOrDefault(b => b.BigHuntBossId == bossQuest.BigHuntBossId) : null; if (bhQuest is not null) { HandleBigHuntQuestFinish(userDb, bhQuest.QuestId, request.IsRetired, nowMs); } EntityIUserBigHuntProgressStatus progress = GetOrCreateProgress(userDb); EntitySBigHuntSession session = GetOrCreateSession(userDb); // Retired or dry run — clear progress and return empty score info. if (request.IsRetired || progress.IsDryRun) { ClearProgress(progress); return Task.FromResult(new FinishBigHuntQuestResponse { ScoreInfo = new BigHuntScoreInfo(), BattleReport = new BigHuntBattleReport() }); } // --- Scoring engine --- long totalDamage = session.TotalDamage; long baseScore = totalDamage; int difficultyBonusPermil = 0; if (bhQuest is not null) { EntityMBigHuntQuestScoreCoefficient? coeff = _masterDb.EntityMBigHuntQuestScoreCoefficient .FirstOrDefault(c => c.BigHuntQuestScoreCoefficientId == bhQuest.BigHuntQuestScoreCoefficientId); if (coeff is not null) { difficultyBonusPermil = coeff.ScoreDifficultBonusPermil; } } int aliveBonusPermil = 500; int maxComboBonusPermil = session.MaxComboCount switch { >= 100 => 300, >= 50 => 200, >= 20 => 100, _ => 0 }; long userScore = baseScore * (1000 + difficultyBonusPermil + aliveBonusPermil + maxComboBonusPermil) / 1000; // --- High-score tracking (per-boss) --- bool isHighScore = false; long oldMaxScore = 0; if (bossQuest is not null) { EntityIUserBigHuntMaxScore? maxScoreEntry = userDb.EntityIUserBigHuntMaxScore .FirstOrDefault(m => m.BigHuntBossId == bossQuest.BigHuntBossId); oldMaxScore = maxScoreEntry?.MaxScore ?? 0; if (userScore > oldMaxScore) { isHighScore = true; if (maxScoreEntry is null) { userDb.EntityIUserBigHuntMaxScore.Add(new EntityIUserBigHuntMaxScore { UserId = userId, BigHuntBossId = bossQuest.BigHuntBossId, MaxScore = userScore, MaxScoreUpdateDatetime = nowMs }); } else { maxScoreEntry.MaxScore = userScore; maxScoreEntry.MaxScoreUpdateDatetime = nowMs; } } } // --- High-score tracking (per-schedule) --- int activeScheduleId = ResolveActiveScheduleId(nowMs); if (bossQuest is not null && activeScheduleId > 0) { EntityIUserBigHuntScheduleMaxScore? schedMax = userDb.EntityIUserBigHuntScheduleMaxScore .FirstOrDefault(m => m.UserId == userId && m.BigHuntScheduleId == activeScheduleId && m.BigHuntBossId == bossQuest.BigHuntBossId); if (schedMax is null) { if (userScore > 0) { userDb.EntityIUserBigHuntScheduleMaxScore.Add(new EntityIUserBigHuntScheduleMaxScore { UserId = userId, BigHuntScheduleId = activeScheduleId, BigHuntBossId = bossQuest.BigHuntBossId, MaxScore = userScore, MaxScoreUpdateDatetime = nowMs }); } } else if (userScore > schedMax.MaxScore) { schedMax.MaxScore = userScore; schedMax.MaxScoreUpdateDatetime = nowMs; } } // --- High-score tracking (per-weekly-attribute) --- long weeklyVersion = WeeklyVersion(nowMs); if (boss is not null) { EntityIUserBigHuntWeeklyMaxScore? weeklyMax = userDb.EntityIUserBigHuntWeeklyMaxScore .FirstOrDefault(m => m.UserId == userId && m.BigHuntWeeklyVersion == weeklyVersion && m.AttributeType == boss.AttributeType); if (weeklyMax is null) { if (userScore > 0) { userDb.EntityIUserBigHuntWeeklyMaxScore.Add(new EntityIUserBigHuntWeeklyMaxScore { UserId = userId, BigHuntWeeklyVersion = weeklyVersion, AttributeType = boss.AttributeType, MaxScore = userScore }); } } else if (userScore > weeklyMax.MaxScore) { weeklyMax.MaxScore = userScore; } } // Grade icon int assetGradeIconId = bossQuest is not null ? ResolveGradeIconId(bossQuest.BigHuntBossId, userScore) : 0; BigHuntScoreInfo scoreInfo = new() { UserScore = userScore, IsHighScore = isHighScore, TotalDamage = totalDamage, BaseScore = baseScore, DifficultyBonusPermil = difficultyBonusPermil, AliveBonusPermil = aliveBonusPermil, MaxComboBonusPermil = maxComboBonusPermil, AssetGradeIconId = assetGradeIconId }; // --- Reward collection on high score --- List scoreRewards = []; if (isHighScore && bossQuest is not null) { int rewardGroupId = ResolveActiveScoreRewardGroupId(bossQuest.BigHuntScoreRewardGroupScheduleId, nowMs); if (rewardGroupId > 0) { List<(PossessionType Type, int Id, int Count)> newItems = CollectNewRewards(rewardGroupId, oldMaxScore, userScore); foreach ((PossessionType type, int id, int count) in newItems) { GrantPossessionViaPossessionHelper(userDb, type, id, count); scoreRewards.Add(new BigHuntReward { PossessionType = (int)type, PossessionId = id, Count = count }); } } } // Clear progress and battle state ClearProgress(progress); session.BattleBinary = []; session.TotalDamage = 0; session.MaxComboCount = 0; session.BossKnockDownCount = 0; session.DeckType = 0; session.UserTripleDeckNumber = 0; FinishBigHuntQuestResponse response = new() { ScoreInfo = scoreInfo, BattleReport = new BigHuntBattleReport() }; response.ScoreReward.AddRange(scoreRewards); return Task.FromResult(response); } /// /// Resumes a BigHunt quest after a disconnect: restores the saved battle binary and deck choice, /// and increments the daily challenge count for the boss quest. /// public override Task RestartBigHuntQuest(RestartBigHuntQuestRequest request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); EntityMBigHuntQuest? bhQuest = _masterDb.EntityMBigHuntQuest .FirstOrDefault(q => q.BigHuntQuestId == request.BigHuntQuestId); EntitySBigHuntSession session = GetOrCreateSession(userDb); if (bhQuest is not null) { HandleBigHuntQuestStart(userDb, bhQuest.QuestId, session.DeckNumber, nowMs); } // Reset scene progress EntityIUserBigHuntProgressStatus progress = GetOrCreateProgress(userDb); progress.CurrentQuestSceneId = 0; // Increment daily challenge count EntityIUserBigHuntStatus status = GetOrCreateStatus(userDb, request.BigHuntBossQuestId); status.DailyChallengeCount++; status.LatestChallengeDatetime = nowMs; RestartBigHuntQuestResponse response= new() { BattleBinary = Google.Protobuf.ByteString.CopyFrom(session.BattleBinary), DeckNumber = session.DeckNumber }; return Task.FromResult(response); } /// /// Applies a bulk skip of one or more hunt attempts, incrementing the daily challenge counter /// without entering combat. /// public override Task SkipBigHuntQuest(SkipBigHuntQuestRequest request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); EntityIUserBigHuntStatus status = GetOrCreateStatus(userDb, request.BigHuntBossQuestId); status.DailyChallengeCount += request.SkipCount; status.LatestChallengeDatetime = nowMs; return Task.FromResult(new SkipBigHuntQuestResponse()); } /// /// Persists mid-battle state to the server session: the raw battle binary snapshot plus /// accumulated damage, combo, and boss knockdown statistics for restart recovery and scoring. /// /// /// Persists the battle binary and detail (damage, combo, knock-downs) from the client into the session. /// public override Task SaveBigHuntBattleInfo(SaveBigHuntBattleInfoRequest request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); EntitySBigHuntSession session = GetOrCreateSession(userDb); session.BattleBinary= request.BattleBinary.ToByteArray(); if (request.BigHuntBattleDetail is not null) { // Sum damage across all costumes in the party long totalDamage = 0; foreach (Proto.Battle.CostumeBattleInfo ci in request.BigHuntBattleDetail.CostumeBattleInfo) { if (ci is not null) { totalDamage += ci.TotalDamage; } } // Persist battle statistics used for scoring and restart session.DeckType = request.BigHuntBattleDetail.DeckType; session.UserTripleDeckNumber = request.BigHuntBattleDetail.UserTripleDeckNumber; session.BossKnockDownCount = request.BigHuntBattleDetail.BossKnockDownCount; session.MaxComboCount = request.BigHuntBattleDetail.MaxComboCount; session.TotalDamage = totalDamage; } return Task.FromResult(new SaveBigHuntBattleInfoResponse()); } /// /// Returns the player's BigHunt overview: weekly score results and grade icons per boss attribute, /// the current week's uncollected rewards, and last week's earned rewards. /// /// /// Returns the top-level summary view: weekly score results, weekly rewards, and last week's rewards for all bosses. /// public override Task GetBigHuntTopData(Empty request, ServerCallContext context) { long userId = context.GetUserId(); DarkUserMemoryDatabase userDb = _store.GetOrCreate(userId); long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); long weeklyVersion = WeeklyVersion(nowMs); // Build weekly score results for each boss List weeklyScoreResults = []; foreach (EntityMBigHuntBoss boss in _masterDb.EntityMBigHuntBoss) { EntityIUserBigHuntWeeklyMaxScore? ws = userDb.EntityIUserBigHuntWeeklyMaxScore .FirstOrDefault(m => m.UserId == userId && m.BigHuntWeeklyVersion == weeklyVersion && m.AttributeType == boss.AttributeType); long maxScore = ws?.MaxScore ?? 0; int gradeIconId = ResolveGradeIconId(boss.BigHuntBossId, maxScore); weeklyScoreResults.Add(new WeeklyScoreResult { AttributeType = (int)boss.AttributeType, BeforeMaxScore = maxScore, CurrentMaxScore = maxScore, BeforeAssetGradeIconId = gradeIconId, CurrentAssetGradeIconId = gradeIconId, AfterMaxScore = maxScore, AfterAssetGradeIconId = gradeIconId }); } // Check if weekly reward was already received EntityIUserBigHuntWeeklyStatus? weeklyStatus = userDb.EntityIUserBigHuntWeeklyStatus .FirstOrDefault(s => s.BigHuntWeeklyVersion == weeklyVersion); // Resolve current week rewards List weeklyRewards = ResolveWeeklyRewards(userDb, weeklyVersion, nowMs); // Resolve last week rewards long lastWeekVersion = weeklyVersion - (7L * 24 * 60 * 60 * 1000); List lastWeekRewards = ResolveWeeklyRewards(userDb, lastWeekVersion, nowMs); GetBigHuntTopDataResponse response = new() { IsReceivedWeeklyScoreReward = weeklyStatus?.IsReceivedWeeklyReward ?? false }; response.WeeklyScoreResult.AddRange(weeklyScoreResults); response.WeeklyScoreReward.AddRange(weeklyRewards); response.LastWeekWeeklyScoreReward.AddRange(lastWeekRewards); return Task.FromResult(response); } // ────────── Quest state helpers ────────── /// /// Initializes quest and mission state for the underlying quest, and transitions its state to active. /// private void HandleBigHuntQuestStart(DarkUserMemoryDatabase userDb, int questId, int deckNumber, long nowMs) { EntityMQuest? masterQuest = _masterDb.EntityMQuest.FirstOrDefault(q => q.QuestId == questId); EntityIUserQuest userQuest = userDb.EntityIUserQuest .FirstOrDefault(q => q.QuestId == questId) ?? AddEntity(userDb.EntityIUserQuest, new EntityIUserQuest { UserId = userDb.UserId, QuestId = questId }); // Initialize quest missions if (masterQuest is not null && masterQuest.QuestMissionGroupId != 0) { foreach (EntityMQuestMissionGroup missionGroupRow in _masterDb.EntityMQuestMissionGroup .Where(g => g.QuestMissionGroupId == masterQuest.QuestMissionGroupId)) { if (!userDb.EntityIUserQuestMission.Any(m => m.QuestId == questId && m.QuestMissionId == missionGroupRow.QuestMissionId)) userDb.EntityIUserQuestMission.Add(new EntityIUserQuestMission { UserId = userDb.UserId, QuestId = questId, QuestMissionId = missionGroupRow.QuestMissionId }); } } userQuest.QuestStateType = (int)QuestStateType.ACTIVE; userQuest.LatestStartDatetime = nowMs; } /// /// Marks the quest cleared, applies first-clear and drop rewards on success, /// and clears quest missions. /// private void HandleBigHuntQuestFinish(DarkUserMemoryDatabase userDb, int questId, bool isRetired, long nowMs) { EntityMQuest? masterQuest = _masterDb.EntityMQuest.FirstOrDefault(q => q.QuestId == questId); EntityIUserQuest? userQuest = userDb.EntityIUserQuest .FirstOrDefault(q => q.QuestId == questId); if (userQuest is not null && !isRetired) { // First-clear rewards if (userQuest.ClearCount == 0 && masterQuest is not null && masterQuest.QuestFirstClearRewardGroupId != 0) { int rewardGroupId = masterQuest.QuestFirstClearRewardGroupId; EntityMQuestFirstClearRewardSwitch? switchRow = _masterDb.EntityMQuestFirstClearRewardSwitch .FirstOrDefault(s => s.QuestId == questId); if (switchRow != null) { EntityIUserQuest? prereq = userDb.EntityIUserQuest .FirstOrDefault(q => q.QuestId == switchRow.SwitchConditionClearQuestId); if (prereq != null && prereq.QuestStateType == (int)QuestStateType.CLEARED) rewardGroupId = switchRow.QuestFirstClearRewardGroupId; } foreach (EntityMQuestFirstClearRewardGroup reward in _masterDb.EntityMQuestFirstClearRewardGroup .Where(r => r.QuestFirstClearRewardGroupId == rewardGroupId)) { PossessionHelper.Apply(userDb, reward.PossessionType, reward.PossessionId, reward.Count, _masterDb); } } // Drop rewards if (masterQuest is not null && masterQuest.QuestPickupRewardGroupId != 0) { foreach (EntityMQuestPickupRewardGroup pickup in _masterDb.EntityMQuestPickupRewardGroup .Where(p => p.QuestPickupRewardGroupId == masterQuest.QuestPickupRewardGroupId)) { EntityMBattleDropReward? drop = _masterDb.EntityMBattleDropReward .FirstOrDefault(d => d.BattleDropRewardId == pickup.BattleDropRewardId); if (drop != null) PossessionHelper.Apply(userDb, drop.PossessionType, drop.PossessionId, drop.Count, _masterDb); } } userQuest.QuestStateType = (int)QuestStateType.CLEARED; userQuest.ClearCount++; userQuest.DailyClearCount++; userQuest.LastClearDatetime = nowMs; } // Clear quest missions if (masterQuest is not null && masterQuest.QuestMissionGroupId != 0) { foreach (EntityMQuestMissionGroup missionGroupRow in _masterDb.EntityMQuestMissionGroup .Where(g => g.QuestMissionGroupId == masterQuest.QuestMissionGroupId)) { EntityIUserQuestMission? userMission = userDb.EntityIUserQuestMission .FirstOrDefault(m => m.QuestId == questId && m.QuestMissionId == missionGroupRow.QuestMissionId); if (userMission is null) { userMission = new EntityIUserQuestMission { UserId = userDb.UserId, QuestId = questId, QuestMissionId = missionGroupRow.QuestMissionId }; userDb.EntityIUserQuestMission.Add(userMission); } userMission.IsClear = true; userMission.ProgressValue = 1; userMission.LatestClearDatetime = nowMs; } } } // ────────── Catalog resolution helpers ────────── /// /// Returns the ID of the schedule whose challenge window contains , /// falling back to the schedule with the most recently ended window. /// private int ResolveActiveScheduleId(long nowMs) { EntityMBigHuntSchedule? active = _masterDb.EntityMBigHuntSchedule .FirstOrDefault(s => nowMs >= s.ChallengeStartDatetime && nowMs <= s.ChallengeEndDatetime); if (active is not null) { return active.BigHuntScheduleId; } // Fall back to schedule with latest end datetime. return _masterDb.EntityMBigHuntSchedule .OrderByDescending(s => s.ChallengeEndDatetime) .Select(s => s.BigHuntScheduleId) .FirstOrDefault(); } /// /// Returns the highest grade icon ID whose score threshold the player's score meets for the given boss, /// or 0 if no threshold is reached. /// private int ResolveGradeIconId(int bossId, long score) { EntityMBigHuntBoss? boss = _masterDb.EntityMBigHuntBoss .FirstOrDefault(b => b.BigHuntBossId == bossId); if (boss is null) { return 0; } // Thresholds sorted ascending by NecessaryScore; last one where score >= threshold wins. int iconId = 0; foreach (EntityMBigHuntBossGradeGroup g in _masterDb.EntityMBigHuntBossGradeGroup .Where(g => g.BigHuntBossGradeGroupId == boss.BigHuntBossGradeGroupId) .OrderBy(g => g.NecessaryScore)) { if (score >= g.NecessaryScore) { iconId = g.AssetGradeIconId; } else { break; } } return iconId; } /// /// Returns the score reward group ID in effect for a given boss-quest score reward group schedule at . /// private int ResolveActiveScoreRewardGroupId(int scheduleId, long nowMs) { // Entries sorted descending by start datetime; first one where nowMs >= start wins. List entries = [.. _masterDb.EntityMBigHuntScoreRewardGroupSchedule .Where(e => e.BigHuntScoreRewardGroupScheduleId == scheduleId) .OrderByDescending(e => e.StartDatetime)]; foreach (EntityMBigHuntScoreRewardGroupSchedule entry in entries) { if (nowMs >= entry.StartDatetime) { return entry.BigHuntScoreRewardGroupId; } } return entries.Count > 0 ? entries[^1].BigHuntScoreRewardGroupId : 0; } /// /// Returns the weekly score reward group ID in effect for a given schedule and boss attribute type at . /// private int ResolveActiveWeeklyRewardGroupId(int scheduleId, AttributeType attributeType, long nowMs) { List entries = [.. _masterDb.EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule .Where(e => e.BigHuntWeeklyAttributeScoreRewardGroupScheduleId == scheduleId && e.AttributeType == attributeType) .OrderByDescending(e => e.StartDatetime)]; foreach (EntityMBigHuntWeeklyAttributeScoreRewardGroupSchedule entry in entries) { if (nowMs >= entry.StartDatetime) { return entry.BigHuntScoreRewardGroupId; } } return entries.Count > 0 ? entries[^1].BigHuntScoreRewardGroupId : 0; } /// /// Returns all reward items whose score thresholds fall within the range (, ], /// i.e. thresholds newly crossed by the player's improved score. /// private List<(PossessionType Type, int Id, int Count)> CollectNewRewards(int scoreRewardGroupId, long oldMax, long newMax) { List<(PossessionType, int, int)> items = []; foreach (EntityMBigHuntScoreRewardGroup threshold in _masterDb.EntityMBigHuntScoreRewardGroup .Where(t => t.BigHuntScoreRewardGroupId == scoreRewardGroupId) .OrderBy(t => t.NecessaryScore)) { if (threshold.NecessaryScore > oldMax && threshold.NecessaryScore <= newMax) { foreach (EntityMBigHuntRewardGroup reward in _masterDb.EntityMBigHuntRewardGroup .Where(r => r.BigHuntRewardGroupId == threshold.BigHuntRewardGroupId)) { items.Add((reward.PossessionType, reward.PossessionId, reward.Count)); } } } return items; } /// /// Builds the list of weekly reward items earned across all boss attributes for a given week version, /// based on the player's weekly max scores. /// private List ResolveWeeklyRewards(DarkUserMemoryDatabase userDb, long weeklyVersion, long nowMs) { List rewards = []; foreach (EntityMBigHuntBoss boss in _masterDb.EntityMBigHuntBoss) { int rewardGroupId = ResolveActiveWeeklyRewardGroupId(1, boss.AttributeType, nowMs); if (rewardGroupId == 0) { continue; } EntityIUserBigHuntWeeklyMaxScore? ws = userDb.EntityIUserBigHuntWeeklyMaxScore .FirstOrDefault(m => m.UserId == userDb.UserId && m.BigHuntWeeklyVersion == weeklyVersion && m.AttributeType == boss.AttributeType); long maxScore = ws?.MaxScore ?? 0; foreach ((PossessionType type, int id, int count) in CollectNewRewards(rewardGroupId, 0, maxScore)) { rewards.Add(new BigHuntReward { PossessionType = (int)type, PossessionId = id, Count = count }); } } return rewards; } // ────────── State accessors ────────── /// /// Gets or initialises the player's BigHunt in-progress quest status record. /// private static EntityIUserBigHuntProgressStatus GetOrCreateProgress(DarkUserMemoryDatabase userDb) { return userDb.EntityIUserBigHuntProgressStatus .FirstOrDefault(p => p.UserId == userDb.UserId) ?? AddEntity(userDb.EntityIUserBigHuntProgressStatus, new EntityIUserBigHuntProgressStatus { UserId = userDb.UserId }); } /// /// Gets or initialises the player's per-boss-quest challenge status record. /// private static EntityIUserBigHuntStatus GetOrCreateStatus(DarkUserMemoryDatabase userDb, int bossQuestId) { return userDb.EntityIUserBigHuntStatus .FirstOrDefault(s => s.BigHuntBossQuestId == bossQuestId) ?? AddEntity(userDb.EntityIUserBigHuntStatus, new EntityIUserBigHuntStatus { UserId = userDb.UserId, BigHuntBossQuestId = bossQuestId }); } /// /// Gets or initialises the player's server-side battle session record. /// private static EntitySBigHuntSession GetOrCreateSession(DarkUserMemoryDatabase userDb) { return userDb.EntitySBigHuntSession .FirstOrDefault(s => s.UserId == userDb.UserId) ?? AddEntity(userDb.EntitySBigHuntSession, new EntitySBigHuntSession { UserId = userDb.UserId }); } /// /// Resets all fields on a BigHunt progress status record to their default (no active hunt) values. /// private static void ClearProgress(EntityIUserBigHuntProgressStatus progress) { progress.CurrentBigHuntBossQuestId = 0; progress.CurrentBigHuntQuestId = 0; progress.CurrentQuestSceneId = 0; progress.IsDryRun = false; } // ────────── Possession grant ────────── /// /// Routes possession grants through PossessionHelper.Apply for consistent handling. /// private void GrantPossessionViaPossessionHelper(DarkUserMemoryDatabase userDb, PossessionType possessionType, int possessionId, int count) { PossessionHelper.Apply(userDb, possessionType, possessionId, count, _masterDb); } // ────────── Time helpers ────────── /// /// Returns Monday 00:00 UTC of the week containing the given timestamp, as millis. /// private static long WeeklyVersion(long millis) { DateTimeOffset dto = DateTimeOffset.FromUnixTimeMilliseconds(millis); DateTime utc = dto.UtcDateTime; int weekday = (int)utc.DayOfWeek; if (weekday == 0) { weekday = 7; } // Sunday = 7 DateTime monday = utc.Date.AddDays(-(weekday - 1)); return new DateTimeOffset(monday, TimeSpan.Zero).ToUnixTimeMilliseconds(); } /// /// Appends an entity to the given list and returns it, enabling inline initialisation. /// private static T AddEntity(List list, T entity) { list.Add(entity); return entity; } }