162 lines
5.1 KiB
PHP
162 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.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 Unlocked!',
|
|
'body' => "You've earned the \"{$achievement->name}\" achievement — {$achievement->short_description}",
|
|
'is_achievement' => true,
|
|
'achievement_id' => $achievement->id,
|
|
'expires_at' => null,
|
|
]);
|
|
}
|
|
}
|