Files
2026-04-28 22:16:21 +10:00

164 lines
5.1 KiB
PHP

<?php
namespace App\Services\Achievements;
use App\Models\Achievement;
use App\Models\Notification;
use App\Models\User;
use App\Models\UserAchievement;
use App\Models\UserFlight;
use App\Services\Achievements\Checkers\AchievementCheckerInterface;
use App\Services\Achievements\Checkers\AircraftChecker;
use App\Services\Achievements\Checkers\CountriesAndContinentsChecker;
use App\Services\Achievements\Checkers\AirlinesAndAlliancesChecker;
use App\Services\Achievements\Checkers\FunChallengesChecker;
use App\Services\Achievements\Checkers\GeneralFlyingChecker;
use Illuminate\Support\Collection;
class AchievementService
{
/** Cached achievement lookups so checkers don't each hit the DB. */
private Collection $achievementCache;
/** The user currently being evaluated — set per calculate() call. */
private User $user;
private array $checkers = [
GeneralFlyingChecker::class,
CountriesAndContinentsChecker::class,
AircraftChecker::class,
AirlinesAndAlliancesChecker::class,
FunChallengesChecker::class,
];
public function __construct()
{
$this->achievementCache = collect();
}
// ---------------------------------------------------------------
// Orchestration
// ---------------------------------------------------------------
/**
* @var Collection<int,UserFlight> $flights
*/
private Collection $flights;
public function calculate(User $user): void
{
$this->user = $user;
$this->achievementCache = Achievement::all()->keyBy('internal_name');
// Load once with every relationship any checker could need
$this->flights = $user->flights()->with([
'airline.alliance',
'aircraft',
'flightClass',
'departureAirport.region',
'arrivalAirport.region',
'departureAirport.region.continent',
'arrivalAirport.region.continent',
])->get();
foreach ($this->checkers as $checkerClass) {
$checker = new $checkerClass($this);
$checker->check($user);
}
}
/**
* @return Collection<int,UserFlight>
*/
public function getFlights(): Collection
{
return $this->flights;
}
// ---------------------------------------------------------------
// Award / revoke
// ---------------------------------------------------------------
/**
* Upsert a user_achievement row and fire a notification if this
* is a newly unlocked achievement.
*/
public function award(Achievement $achievement, User $user, ?int $progress = null): void
{
$existing = UserAchievement::where('user_id', $user->id)
->where('achievement_id', $achievement->id)
->first();
$alreadyUnlocked = $existing !== null;
UserAchievement::updateOrCreate(
['user_id' => $user->id, 'achievement_id' => $achievement->id],
['progress' => $progress],
);
if (! $alreadyUnlocked) {
$this->createAchievementNotification($user, $achievement);
}
}
/**
* Update progress on a progressive achievement without marking it
* as unlocked (i.e. progress exists but threshold not yet met).
* Creates the row if it doesn't exist yet.
*/
public function updateProgress(Achievement $achievement, User $user, int $progress): void
{
UserAchievement::updateOrCreate(
['user_id' => $user->id, 'achievement_id' => $achievement->id],
['progress' => $progress],
);
}
/**
* Remove a user_achievement row and its associated notification
* if the achievement is no longer valid for this user.
*/
public function revoke(Achievement $achievement, User $user): void
{
UserAchievement::where('user_id', $user->id)
->where('achievement_id', $achievement->id)
->delete();
Notification::where('user_id', $user->id)
->where('achievement_id', $achievement->id)
->delete();
}
// ---------------------------------------------------------------
// Helpers for checkers
// ---------------------------------------------------------------
public function resolveAchievement(string $internalName): ?Achievement
{
$achievement = $this->achievementCache->get($internalName);
return $achievement instanceof Achievement ? $achievement : null;
}
public function currentUser(): User
{
return $this->user;
}
// ---------------------------------------------------------------
// Notifications
// ---------------------------------------------------------------
private function createAchievementNotification(User $user, Achievement $achievement): void
{
Notification::create([
'user_id' => $user->id,
'title' => $achievement->name,
'body' => $achievement->short_description,
'is_achievement' => true,
'achievement_id' => $achievement->id,
'expires_at' => null,
]);
}
}