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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
interface AchievementCheckerInterface
|
||||
{
|
||||
/**
|
||||
* Check all achievements in this category for the given user.
|
||||
* Implementations should call $this->service->award() or $this->service->revoke()
|
||||
* — never touch user_achievements directly.
|
||||
*/
|
||||
public function check(): void;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class AircraftChecker extends BaseChecker
|
||||
{
|
||||
private const array BOEING_FAMILIES = [
|
||||
'707' => ['B701', 'B703', 'B720'],
|
||||
'717' => ['B712', 'B717'],
|
||||
'727' => ['B721', 'B722', 'B727'],
|
||||
'737' => ['B731', 'B732', 'B733', 'B734', 'B735', 'B736', 'B737', 'B738', 'B739', 'B37M', 'B38M', 'B39M'],
|
||||
'747' => ['B741', 'B742', 'B743', 'B744', 'B748', 'B74D', 'B74R', 'B74S'],
|
||||
'757' => ['B752', 'B753', 'B757'],
|
||||
'767' => ['B762', 'B763', 'B764', 'B767'],
|
||||
'777' => ['B772', 'B773', 'B77L', 'B77W', 'B778', 'B779'],
|
||||
'787' => ['B788', 'B789', 'B78X'],
|
||||
];
|
||||
|
||||
private const array AIRBUS_FAMILIES = [
|
||||
'A300' => ['A30B', 'A300', 'A306'],
|
||||
'A310' => ['A310', 'A312', 'A313'],
|
||||
'A318' => ['A318'],
|
||||
'A319' => ['A319', 'A31X'],
|
||||
'A320' => ['A320', 'A20N'],
|
||||
'A321' => ['A321', 'A21N'],
|
||||
'A330' => ['A330', 'A332', 'A333', 'A338', 'A339'],
|
||||
'A340' => ['A340', 'A342', 'A343', 'A345', 'A346'],
|
||||
'A350' => ['A350', 'A358', 'A359', 'A35K'],
|
||||
'A380' => ['A380', 'A388'],
|
||||
];
|
||||
|
||||
private const array DOUBLE_DECKER_DESIGNATORS = [
|
||||
// A380
|
||||
'A380', 'A388',
|
||||
// 747 variants
|
||||
'B741', 'B742', 'B743', 'B744', 'B748', 'B74D', 'B74R', 'B74S',
|
||||
];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
/**
|
||||
* @var $flights Collection<int, UserFlight>
|
||||
*/
|
||||
$flights = $this->flights();
|
||||
|
||||
|
||||
$flightsWithAircraft = $flights->filter(fn($f) => $f->aircraft !== null);
|
||||
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->aircraft_description, ['LandPlane', 'SeaPlane'])
|
||||
),
|
||||
'aircraft.fly_on_a_plane'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->aircraft->aircraft_description === 'Helicopter'
|
||||
),
|
||||
'aircraft.fly_on_a_helicopter'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->aircraft->engine_type === 'Jet'
|
||||
),
|
||||
'aircraft.fly_on_a_jet'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->aircraft->engine_type === 'Turboprop/Turboshaft' || $f->aircraft->engine_type === 'Piston'
|
||||
),
|
||||
'aircraft.fly_on_a_prop'
|
||||
);
|
||||
|
||||
// --- Engine count achievements ---
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 1 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.single_engine'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 2 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.twin_engine'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 3 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.tri_engine'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(fn(UserFlight $f) => $f->aircraft->engine_count === 4 && $f->aircraft->aircraft_description !== 'Helicopter'),
|
||||
'aircraft.quad_engine'
|
||||
);
|
||||
|
||||
// --- Double decker ---
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->designator, self::DOUBLE_DECKER_DESIGNATORS)
|
||||
),
|
||||
'aircraft.double_decker'
|
||||
);
|
||||
|
||||
// --- Smaller manufacturer (scheduled service only) ---
|
||||
|
||||
$this->awardIf(
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => $f->airline && $f->flight_number && !in_array(strtolower($f->aircraft->manufacturer_code), ['boeing', 'airbus'])
|
||||
),
|
||||
'aircraft.smaller_manufacturer'
|
||||
);
|
||||
|
||||
// --- Boeing 7x7 families ---
|
||||
|
||||
$flownBoeingFamilies = collect(self::BOEING_FAMILIES)
|
||||
->filter(fn($designators) =>
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
|
||||
)
|
||||
)
|
||||
->count();
|
||||
|
||||
$this->awardProgress($flownBoeingFamilies, 'aircraft.all_boeing_7x7');
|
||||
|
||||
// --- Airbus A3xx families ---
|
||||
|
||||
$flownAirbusFamilie = collect(self::AIRBUS_FAMILIES)
|
||||
->filter(fn($designators) =>
|
||||
$flightsWithAircraft->contains(
|
||||
fn(UserFlight $f) => in_array($f->aircraft->designator, $designators)
|
||||
)
|
||||
)
|
||||
->count();
|
||||
|
||||
$this->awardProgress($flownAirbusFamilie, 'aircraft.all_airbus_a3xx');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\Alliance;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
|
||||
class AirlinesAndAlliancesChecker extends BaseChecker
|
||||
{
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
|
||||
$alliances = Alliance::withCount('airlines')->pluck('id', 'internal_name');
|
||||
|
||||
$flownAllianceAirlines = $flights
|
||||
->filter(fn(UserFlight $f) => $f->airline?->alliance !== null)
|
||||
->groupBy(fn(UserFlight $f) => $f->airline->alliance->internal_name)
|
||||
->map(fn($group) => $group->pluck('airline.internal_name')->unique()->count());
|
||||
|
||||
$check = fn(string $alliance): int => $flownAllianceAirlines->get($alliance, 0);
|
||||
|
||||
$this->awardProgress($check('skyteam'), 'airlines_alliances.all_skyteam');
|
||||
$this->awardProgress($check('oneworld'), 'airlines_alliances.all_oneworld');
|
||||
$this->awardProgress($check('star_alliance'), 'airlines_alliances.all_star_alliance');
|
||||
$this->awardProgress($check('vanilla_alliance'), 'airlines_alliances.all_vanilla_alliance');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Services\Achievements\AchievementService;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class BaseChecker implements AchievementCheckerInterface
|
||||
{
|
||||
public function __construct(protected AchievementService $service) {}
|
||||
|
||||
protected function flights(): Collection
|
||||
{
|
||||
return $this->service->getFlights();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an achievement ID from its internal name.
|
||||
* Results are cached on the service so repeated lookups are free.
|
||||
*/
|
||||
protected function achievement(string $internalName): ?Achievement
|
||||
{
|
||||
return $this->service->resolveAchievement($internalName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a boolean (non-progressive) achievement if the condition is met,
|
||||
* or revoke it if the condition is no longer met.
|
||||
*/
|
||||
protected function awardIf(bool $condition, string $internalName): void
|
||||
{
|
||||
$achievement = $this->achievement($internalName);
|
||||
|
||||
if (! $achievement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($condition) {
|
||||
$this->service->award($achievement, $this->currentUser());
|
||||
} else {
|
||||
$this->service->revoke($achievement, $this->currentUser());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a progressive achievement, updating progress and
|
||||
* unlocking/revoking based on whether progress meets the threshold.
|
||||
*/
|
||||
protected function awardProgress(int $progress, string $internalName): void
|
||||
{
|
||||
$achievement = $this->achievement($internalName);
|
||||
if (! $achievement) return;
|
||||
|
||||
if ($progress >= $achievement->threshold) {
|
||||
$this->service->award($achievement, $this->currentUser(), $progress);
|
||||
} else {
|
||||
$this->service->updateProgress($achievement, $this->currentUser(), $progress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user currently being evaluated.
|
||||
* Set by AchievementService before calling check().
|
||||
*/
|
||||
protected function currentUser(): \App\Models\User
|
||||
{
|
||||
return $this->service->currentUser();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\Continent;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CountriesAndContinentsChecker extends BaseChecker
|
||||
{
|
||||
private const INHABITED_CONTINENTS = [
|
||||
'africa', 'asia', 'europe', 'north_america', 'oceania', 'south_america',
|
||||
];
|
||||
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
// Resolve internal_name → id once, used throughout
|
||||
$continents = Continent::pluck('id', 'internal_name');
|
||||
|
||||
$arrivalContinentIds = $flights->pluck('arrivalAirport.region.continent_id')->unique();
|
||||
|
||||
$has = fn(string $name) => $arrivalContinentIds->contains($continents[$name]);
|
||||
|
||||
// --- Simple "fly to X continent" achievements ---
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn(UserFlight $f) =>
|
||||
$f->departureAirport->region->continent_id !== $f->arrivalAirport->region->continent_id
|
||||
),
|
||||
'countries_continents.intercontinental'
|
||||
);
|
||||
|
||||
$this->awardIf($has('africa'), 'countries_continents.fly_to_africa');
|
||||
$this->awardIf($has('asia'), 'countries_continents.fly_to_asia');
|
||||
$this->awardIf($has('oceania'), 'countries_continents.fly_to_oceania');
|
||||
$this->awardIf($has('antarctica'), 'countries_continents.fly_to_antarctica');
|
||||
$this->awardIf($has('europe'), 'countries_continents.fly_to_europe');
|
||||
$this->awardIf($has('south_america'), 'countries_continents.fly_to_south_america');
|
||||
$this->awardIf($has('north_america'), 'countries_continents.fly_to_north_america');
|
||||
|
||||
// --- All inhabited continents ---
|
||||
|
||||
$inhabitedIds = collect(self::INHABITED_CONTINENTS)->map(fn($name) => $continents[$name]);
|
||||
|
||||
$visitedInhabited = $arrivalContinentIds
|
||||
->intersect($inhabitedIds)
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($visitedInhabited, 'countries_continents.all_inhabited_continents');
|
||||
|
||||
// --- All continents including Antarctica ---
|
||||
|
||||
$this->awardProgress(
|
||||
$arrivalContinentIds->unique()->count(),
|
||||
'countries_continents.all_continents'
|
||||
);
|
||||
|
||||
// --- Continent route pairs ---
|
||||
|
||||
$this->checkContinentPairs($flights);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, UserFlight> $flights
|
||||
* @return void
|
||||
*/
|
||||
private function checkContinentPairs(Collection $flights): void
|
||||
{
|
||||
$oneWayRoutes = collect();
|
||||
$directedRoutes = collect();
|
||||
|
||||
foreach ($flights as $flight) {
|
||||
$dep = $flight->departureAirport->region->continent->internal_name;
|
||||
$arr = $flight->arrivalAirport->region->continent->internal_name;
|
||||
|
||||
// Directed route key e.g. "europe→asia"
|
||||
$directedRoutes->push("{$dep}→{$arr}");
|
||||
|
||||
// Undirected — sort alphabetically so "europe→asia" and "asia→europe" are the same
|
||||
$undirected = implode('→', collect([$dep, $arr])->sort()->values()->all());
|
||||
$oneWayRoutes->push($undirected);
|
||||
}
|
||||
|
||||
$this->awardProgress(
|
||||
$oneWayRoutes->unique()->count(),
|
||||
'countries_continents.all_continent_pairs_one_way'
|
||||
);
|
||||
|
||||
$this->awardProgress(
|
||||
$directedRoutes->unique()->count(),
|
||||
'countries_continents.all_continent_pairs_both_ways'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class FunChallengesChecker extends BaseChecker
|
||||
{
|
||||
public function check(): void
|
||||
{
|
||||
$flights = $this->flights();
|
||||
|
||||
$airlineLetters = $flights
|
||||
->filter(fn($f) => $f->airline?->IATA_code !== null)
|
||||
->map(fn($f) => strtoupper($f->airline->IATA_code[0]))
|
||||
->filter(fn($letter) => ctype_alpha($letter))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($airlineLetters, 'fun_challenges.airline_alphabet');
|
||||
|
||||
// --- Visit the Alphabet ---
|
||||
// Collect first letters from both departure and arrival airport IATA codes
|
||||
|
||||
$airportLetters = $flights
|
||||
->flatMap(fn($f) => [
|
||||
$f->departureAirport?->iata_code,
|
||||
$f->arrivalAirport?->iata_code,
|
||||
])
|
||||
->filter()
|
||||
->map(fn($code) => strtoupper($code[0]))
|
||||
->unique()
|
||||
->count();
|
||||
|
||||
$this->awardProgress($airportLetters, 'fun_challenges.airport_alphabet');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Achievements\Checkers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserFlight;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class GeneralFlyingChecker extends BaseChecker
|
||||
{
|
||||
public function check(): void
|
||||
{
|
||||
/**
|
||||
* @var $flights Collection<int, UserFlight>
|
||||
*/
|
||||
$flights = $this->flights();
|
||||
$count = $flights->count();
|
||||
|
||||
// --- Boolean achievements ---
|
||||
|
||||
$this->awardIf($count >= 1, 'general_flying.first_flight');
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->isDomestic()),
|
||||
'general_flying.domestic_flight'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->isInternational()),
|
||||
'general_flying.international_flight'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'business'),
|
||||
'general_flying.business_class'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'first'),
|
||||
'general_flying.first_class'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'premium_economy'),
|
||||
'general_flying.premium_economy'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => in_array($f->flightClass->internal_name, ['business', 'first'])),
|
||||
'general_flying.business_or_first'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'private'),
|
||||
'general_flying.fly_private'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->contains(fn ($f) => $f->flightClass->internal_name === 'general_aviation'),
|
||||
'general_flying.general_aviation'
|
||||
);
|
||||
|
||||
$this->awardIf(
|
||||
$flights->filter(fn (UserFlight $f) => $f->isDomestic())
|
||||
->map(fn (UserFlight $f) => $f->departureAirport->region->country_id)
|
||||
->unique()
|
||||
->count() >= 2,
|
||||
'general_flying.domestic_two_countries'
|
||||
);
|
||||
|
||||
// --- Progressive achievements ---
|
||||
|
||||
$this->awardProgress($count,'general_flying.10_flights');
|
||||
$this->awardProgress($count,'general_flying.100_flights');
|
||||
$this->awardProgress($count,'general_flying.500_flights');
|
||||
$this->awardProgress($count,'general_flying.1000_flights');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user