Added achievement data
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user