resolveUser($user); $unlocked = []; foreach ($this->unlockableDefinitions($currentUser) as $achievement) { if ($this->unlockAchievement($currentUser, $achievement)) { $unlocked[] = $achievement->slug; } } $this->forgetSummaryCache((int) $currentUser->id); return $unlocked; } public function previewUnlocks(User|int $user): array { $currentUser = $this->resolveUser($user); return $this->unlockableDefinitions($currentUser) ->pluck('slug') ->values() ->all(); } public function unlockAchievement(User|int $user, Achievement|int $achievement): bool { $currentUser = $user instanceof User ? $user : User::query()->findOrFail($user); $currentAchievement = $achievement instanceof Achievement ? $achievement : Achievement::query()->findOrFail($achievement); $inserted = false; DB::transaction(function () use ($currentUser, $currentAchievement, &$inserted): void { $result = UserAchievement::query()->insertOrIgnore([ 'user_id' => (int) $currentUser->id, 'achievement_id' => (int) $currentAchievement->id, 'unlocked_at' => now(), ]); if ($result === 0) { return; } $inserted = true; }); if (! $inserted) { return false; } if ((int) $currentAchievement->xp_reward > 0) { $this->xp->addXP( (int) $currentUser->id, (int) $currentAchievement->xp_reward, 'achievement_unlocked:' . $currentAchievement->slug, (int) $currentAchievement->id, false, ); } $currentUser->notify(new AchievementUnlockedNotification($currentAchievement)); $this->achievementPosts->achievementUnlocked($currentUser, $currentAchievement); $this->forgetSummaryCache((int) $currentUser->id); return true; } public function hasAchievement(User|int $user, string $achievementSlug): bool { $userId = $user instanceof User ? (int) $user->id : $user; return UserAchievement::query() ->where('user_id', $userId) ->whereHas('achievement', fn ($query) => $query->where('slug', $achievementSlug)) ->exists(); } public function summary(User|int $user): array { $userId = $user instanceof User ? (int) $user->id : $user; return Cache::remember($this->summaryCacheKey($userId), now()->addMinutes(10), function () use ($userId): array { $currentUser = User::query()->with('statistics')->findOrFail($userId); $progress = $this->progressSnapshot($currentUser); $unlockedMap = UserAchievement::query() ->where('user_id', $userId) ->get() ->keyBy('achievement_id'); $items = $this->definitions()->map(function (Achievement $achievement) use ($progress, $unlockedMap): array { $progressValue = $this->progressValue($progress, $achievement); /** @var UserAchievement|null $unlocked */ $unlocked = $unlockedMap->get($achievement->id); return [ 'id' => (int) $achievement->id, 'name' => $achievement->name, 'slug' => $achievement->slug, 'description' => $achievement->description, 'icon' => $achievement->icon, 'xp_reward' => (int) $achievement->xp_reward, 'type' => $achievement->type, 'condition_type' => $achievement->condition_type, 'condition_value' => (int) $achievement->condition_value, 'progress' => min((int) $achievement->condition_value, $progressValue), 'progress_percent' => $achievement->condition_value > 0 ? (int) round((min((int) $achievement->condition_value, $progressValue) / (int) $achievement->condition_value) * 100) : 100, 'unlocked' => $unlocked !== null, 'unlocked_at' => $unlocked?->unlocked_at?->toIso8601String(), ]; }); return [ 'unlocked' => $items->where('unlocked', true)->sortByDesc('unlocked_at')->values()->all(), 'locked' => $items->where('unlocked', false)->values()->all(), 'recent' => $items->where('unlocked', true)->sortByDesc('unlocked_at')->take(4)->values()->all(), 'counts' => [ 'total' => $items->count(), 'unlocked' => $items->where('unlocked', true)->count(), 'locked' => $items->where('unlocked', false)->count(), ], ]; }); } public function definitions() { return Cache::remember('achievements:definitions', now()->addHour(), function () { return Achievement::query()->orderBy('type')->orderBy('condition_value')->get(); }); } public function forgetDefinitionsCache(): void { Cache::forget('achievements:definitions'); } private function progressValue(array $progress, Achievement $achievement): int { return (int) ($progress[$achievement->condition_type] ?? 0); } private function resolveUser(User|int $user): User { return $user instanceof User ? $user->loadMissing('statistics') : User::query()->with('statistics')->findOrFail($user); } private function unlockableDefinitions(User $user): Collection { $progress = $this->progressSnapshot($user); $unlockedSlugs = $this->unlockedSlugs((int) $user->id); return $this->definitions()->filter(function (Achievement $achievement) use ($progress, $unlockedSlugs): bool { if ($this->progressValue($progress, $achievement) < (int) $achievement->condition_value) { return false; } return ! isset($unlockedSlugs[$achievement->slug]); })->values(); } private function progressSnapshot(User $user): array { return [ 'upload_count' => Artwork::query() ->published() ->where('user_id', $user->id) ->count(), 'likes_received' => (int) DB::table('artwork_likes as likes') ->join('artworks as artworks', 'artworks.id', '=', 'likes.artwork_id') ->where('artworks.user_id', $user->id) ->count(), 'followers_count' => (int) ($user->statistics?->followers_count ?? $user->followers()->count()), 'stories_published' => Story::query()->published()->where('creator_id', $user->id)->count(), 'level_reached' => (int) ($user->level ?? 1), ]; } private function unlockedSlugs(int $userId): array { return UserAchievement::query() ->where('user_id', $userId) ->join('achievements', 'achievements.id', '=', 'user_achievements.achievement_id') ->pluck('achievements.slug') ->flip() ->all(); } private function forgetSummaryCache(int $userId): void { Cache::forget($this->summaryCacheKey($userId)); } private function summaryCacheKey(int $userId): string { return 'achievements:summary:' . $userId; } }