Added achievement data

This commit is contained in:
2026-04-26 20:00:11 +10:00
parent f6d5b97784
commit 14aed7bf6e
18 changed files with 950 additions and 6 deletions
+2
View File
@@ -37,10 +37,12 @@ class Achievement extends Model
'difficulty_description',
'achievement_category_id',
'achievement_difficulty_id',
'threshold',
];
protected $casts = [
'progressive' => 'boolean',
'threshold' => 'integer',
];
// ---------------------------------------------------------------
+6 -1
View File
@@ -17,6 +17,7 @@ class Airline extends Model
'name',
'internal_name',
'country_id',
'alliance_id',
'active',
'logo',
];
@@ -25,7 +26,6 @@ class Airline extends Model
'active' => 'boolean',
];
public $timestamps = false;
protected $appends = [
'display_name',
@@ -50,6 +50,11 @@ class Airline extends Model
);
}
public function alliance(): BelongsTo
{
return $this->belongsTo(Alliance::class);
}
public function country(): BelongsTo
{
return $this->belongsTo(Country::class);
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Alliance extends Model
{
protected $table = 'alliances';
protected $fillable = [
'internal_name',
'name',
];
protected $casts = [
'internal_name' => 'string',
];
public function airlines(): HasMany
{
return $this->hasMany(Airline::class);
}
}
+4 -1
View File
@@ -13,12 +13,15 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use App\Traits\HasAchievements;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasFactory, Notifiable, HasAchievements;
/**
* Get the attributes that should be cast.
+8
View File
@@ -148,6 +148,14 @@ class UserFlight extends Model
);
}
public function isDomestic() : bool{
return $this->departureAirport->region->country_id == $this->arrivalAirport->region->country_id;
}
public function isInternational() : bool{
return !$this->isDomestic();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Observers;
use App\Models\Achievement;
use App\Models\Airline;
use App\Models\Alliance;
class AirlineObserver
{
public function created(Airline $airline): void
{
if ($airline->alliance_id !== null) {
$this->syncAllianceThresholds();
}
}
public function updated(Airline $airline): void
{
if ($airline->wasChanged('alliance_id')) {
$this->syncAllianceThresholds();
}
}
public function deleted(Airline $airline): void
{
$this->syncAllianceThresholds();
}
private function syncAllianceThresholds(): void
{
Alliance::withCount('airlines')->each(function (Alliance $alliance) {
Achievement::where('internal_name', "airlines_alliances.all_{$alliance->internal_name}")
->update(['threshold' => $alliance->airlines_count]);
});
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Observers;
use App\Models\UserFlight;
class FlightObserver
{
/**
* Recalculate after a flight is created.
*/
public function created(UserFlight $flight): void
{
$flight->user->calculateAchievements();
}
/**
* Recalculate after a flight is updated.
* Cabin class, flight type, airline, etc. may have changed,
* which could unlock or revoke achievements.
*/
public function updated(UserFlight $flight): void
{
$flight->user->calculateAchievements();
}
/**
* Recalculate after a flight is deleted.
* Previously earned achievements may no longer be valid.
*/
public function deleted(UserFlight $flight): void
{
$flight->user->calculateAchievements();
}
}
+6
View File
@@ -2,6 +2,10 @@
namespace App\Providers;
use App\Models\Airline;
use App\Models\UserFlight;
use App\Observers\AirlineObserver;
use App\Observers\FlightObserver;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
@@ -21,5 +25,7 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
Vite::prefetch(concurrency: 3);
UserFlight::observe(FlightObserver::class);
Airline::observe(AirlineObserver::class);
}
}
@@ -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');
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Traits;
use App\Models\User;
use App\Services\Achievements\AchievementService;
/**
* @mixin User
*/
trait HasAchievements
{
public function calculateAchievements(): void
{
/** @var User $this */
app(AchievementService::class)->calculate($this);
}
}
@@ -1,5 +1,7 @@
<?php
use App\Models\Airline;
use App\Models\Alliance;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
@@ -122,11 +124,14 @@ return new class extends Migration
$table->string('short_description')->after('internal_name');
$table->text('long_description')->after('short_description');
// Progressive tracking
$table->boolean('progressive')->default(false)->after('long_description');
// Difficulty flavour text specific to this achievement
$table->text('difficulty_description')->nullable()->after('progressive');
$table->unsignedInteger('threshold')->nullable()->after('progressive');
// Foreign keys
$table->foreignId('achievement_category_id')
@@ -182,6 +187,117 @@ return new class extends Migration
$this->seedAchievements();
$this->createAlliances();
}
public function createAlliances(): void
{
Schema::create('alliances', function (Blueprint $table) {
$table->id();
$table->string('internal_name')->unique();
$table->string('name');
$table->timestamps();
});
Schema::table('airlines', function (Blueprint $table) {
$table->foreignId('alliance_id')
->nullable()
->constrained('alliances')
->nullOnDelete();
});
DB::table('alliances')->insert([
['internal_name' => 'skyteam', 'name' => 'SkyTeam', 'created_at' => now(), 'updated_at' => now()],
['internal_name' => 'oneworld', 'name' => 'Oneworld', 'created_at' => now(), 'updated_at' => now()],
['internal_name' => 'star_alliance', 'name' => 'Star Alliance', 'created_at' => now(), 'updated_at' => now()],
['internal_name' => 'vanilla_alliance','name' => 'Vanilla Alliance', 'created_at' => now(), 'updated_at' => now()],
]);
Airline::whereInternalName('xiamen-airlines')->update(['internal_name' => 'xiamen-air', 'name' => 'XiamenAir']);
$skyteam = Alliance::where('internal_name', 'skyteam')->first();
$skyteamMembers = [
'aerolineas-argentinas',
'aeromexico',
'air-europa',
'air-france',
'china-airlines',
'china-eastern',
'delta',
'garuda-indonesia',
'kenya-airways',
'klm',
'korean-air',
'middle-east-airlines',
'saudia',
'sas',
'tarom',
'vietnam-airlines',
'virgin-atlantic',
'xiamen-air'
];
$star = Alliance::where('internal_name', 'star_alliance')->first();
$starMembers = [
'aegean-airlines',
'air-canada',
'air-china',
'air-india',
'air-new-zealand',
'all-nippon-airways',
'asiana',
'austrian',
'avianca',
'brussels-airlines',
'copa-airlines',
'croatia-airlines',
'egyptair',
'ethiopian-airlines',
'eva-air',
'ita-airways',
'lot-polish-airlines',
'lufthansa',
'shenzhen-airlines',
'singapore-airlines',
'swiss',
'tap-portugal',
'thai-airways-international',
'turkish-airlines',
'united-airlines',
];
$oneworld = Alliance::where('internal_name', 'oneworld')->first();
$oneworldMembers = [
'alaska-airlines',
'american-airlines',
'british-airways',
'cathay-pacific',
'fiji-airways',
'finnair',
'hawaiian-airlines',
'iberia',
'japan-airlines',
'malaysia-airlines',
'oman-air',
'qantas',
'qatar-airways',
'royal-air-maroc',
'royal-jordanian',
'srilankan'
];
$vanilla = Alliance::where('internal_name', 'vanilla_alliance')->first();
$vanillaMembers = [
'air-austral',
'air-madagascar',
'air-seychelles',
'air-mauritius'
];
Airline::whereIn('internal_name', $skyteamMembers)->update(['alliance_id' => $skyteam->id]);
Airline::whereIn('internal_name', $starMembers)->update(['alliance_id' => $star->id]);
Airline::whereIn('internal_name', $oneworldMembers)->update(['alliance_id' => $oneworld->id]);
Airline::whereIn('internal_name', $vanillaMembers)->update(['alliance_id' => $vanilla->id]);
}
private function seedAchievements(): void
@@ -203,7 +319,6 @@ return new class extends Migration
$funChallenges = $categories['fun_challenges'];
$icon = 'standard_achievement.png';
$now = now();
$achievements = [
@@ -217,6 +332,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $easy,
@@ -228,6 +344,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $easy,
@@ -239,6 +356,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $easy,
@@ -249,6 +367,7 @@ return new class extends Migration
'short_description' => 'Fly in Business Class.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
@@ -260,6 +379,7 @@ return new class extends Migration
'short_description' => 'Fly in First Class.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
@@ -272,6 +392,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $expensive,
@@ -283,6 +404,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $moderate,
@@ -294,6 +416,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $expensive,
@@ -305,6 +428,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $moderate,
@@ -316,6 +440,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $moderate,
@@ -327,6 +452,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 10,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $easy,
@@ -338,6 +464,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 100,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $moderate,
@@ -349,6 +476,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 500,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $hard,
@@ -360,6 +488,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 1000,
'difficulty_description' => null,
'achievement_category_id' => $generalFlying,
'achievement_difficulty_id'=> $expensive,
@@ -375,6 +504,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id'=> $moderate,
@@ -386,6 +516,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id'=> $easy,
@@ -397,6 +528,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id'=> $easy,
@@ -407,6 +539,7 @@ return new class extends Migration
'short_description' => 'Fly to Oceania.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
@@ -418,6 +551,7 @@ return new class extends Migration
'short_description' => 'Fly to Antarctica.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => 'Very few commercial or charter services operate to Antarctica, making this extremely difficult to pull off.',
'achievement_category_id' => $countriesAndContinents,
@@ -430,6 +564,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id'=> $easy,
@@ -441,6 +576,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id'=> $easy,
@@ -452,6 +588,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id'=> $easy,
@@ -467,6 +604,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => false,
'threshold' => null,
'difficulty_description' => null,
'achievement_category_id' => $aircraft,
'achievement_difficulty_id'=> $easy,
@@ -477,6 +615,7 @@ return new class extends Migration
'short_description' => 'Fly on a helicopter.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $aircraft,
@@ -488,6 +627,7 @@ return new class extends Migration
'short_description' => 'Fly on a jet-powered aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $aircraft,
@@ -499,6 +639,7 @@ return new class extends Migration
'short_description' => 'Fly on a propeller driven aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $aircraft,
@@ -512,6 +653,7 @@ return new class extends Migration
'icon' => $icon,
'progressive' => true,
'difficulty_description' => 'Requires flying the 707, 717, 727, 737, 747, 757, 767, 777, and 787 — some of which are no longer in commercial service.',
'threshold' => 9,
'achievement_category_id' => $aircraft,
'achievement_difficulty_id'=> $impossible,
],
@@ -522,6 +664,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 10,
'difficulty_description' => 'Covers the A300, A310, A318A321, A330, A340, A350, and A380 families. You are likely going to have to go to Iran to get this one.',
'achievement_category_id' => $aircraft,
'achievement_difficulty_id'=> $nearImposs,
@@ -532,6 +675,7 @@ return new class extends Migration
'short_description' => 'Fly on a four-engine aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => 'Four-engine jets are becoming increasingly rare as twin-engine widebodies dominate long-haul routes.',
'achievement_category_id' => $aircraft,
@@ -543,6 +687,7 @@ return new class extends Migration
'short_description' => 'Fly on a double-decker aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => 'Primarily the A380 and 747, which operate on a limited and shrinking number of routes.',
'achievement_category_id' => $aircraft,
@@ -554,17 +699,31 @@ return new class extends Migration
'short_description' => 'Fly on a single-engine aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $aircraft,
'achievement_difficulty_id'=> $moderate,
],
[
'internal_name' => 'aircraft.twin_engine',
'name' => 'Twinsies',
'short_description' => 'Fly on a twin-engine aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => 'Most planes in service today meet this criteria!',
'achievement_category_id' => $aircraft,
'achievement_difficulty_id'=> $easy,
],
[
'internal_name' => 'aircraft.tri_engine',
'name' => 'Triple Threat',
'short_description' => 'Fly on a tri-engine aircraft.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => 'Most tri-jets are out of service nowadays, and tri-props even rarer',
'achievement_category_id' => $aircraft,
@@ -573,9 +732,10 @@ return new class extends Migration
[
'internal_name' => 'aircraft.smaller_manufacturer',
'name' => 'Break the Duopoly',
'short_description' => 'Fly on an aircraft from a manufacturer other than Boeing or Airbus.',
'short_description' => 'Fly on a scheduled flight on an aircraft from a manufacturer other than Boeing or Airbus.',
'long_description' => '',
'icon' => $icon,
'threshold' => null,
'progressive' => false,
'difficulty_description' => null,
'achievement_category_id' => $aircraft,
@@ -592,6 +752,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 18,
'difficulty_description' => null,
'achievement_category_id' => $airlinesAndAlliances,
'achievement_difficulty_id'=> $hard,
@@ -603,6 +764,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 16,
'difficulty_description' => null,
'achievement_category_id' => $airlinesAndAlliances,
'achievement_difficulty_id'=> $hard,
@@ -614,6 +776,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 26,
'difficulty_description' => null,
'achievement_category_id' => $airlinesAndAlliances,
'achievement_difficulty_id'=> $hard,
@@ -625,6 +788,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 4,
'difficulty_description' => null,
'achievement_category_id' => $airlinesAndAlliances,
'achievement_difficulty_id'=> $hard,
@@ -640,6 +804,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 26,
'difficulty_description' => 'Some letters have very few airlines with a matching IATA code, requiring creative routing.',
'achievement_category_id' => $funChallenges,
'achievement_difficulty_id' => $hard,
@@ -651,6 +816,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 26,
'difficulty_description' => 'Certain letters — particularly Q, X, and Z — have extremely limited airport coverage, making this a serious challenge.',
'achievement_category_id' => $funChallenges,
'achievement_difficulty_id' => $nearImposs,
@@ -666,6 +832,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 6,
'difficulty_description' => null,
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id' => $hard,
@@ -677,6 +844,7 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'threshold' => 7,
'difficulty_description' => 'Antarctica has no commercial scheduled service — expect a specialist charter or expedition flight.',
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id' => $nearImposs,
@@ -689,7 +857,8 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'difficulty_description' => 'There are 15 unique continent pairs (excluding Antarctica)',
'threshold' => 21,
'difficulty_description' => 'There are 21 unique continent pairs (excluding Antarctica)',
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id' => $hard,
],
@@ -700,7 +869,8 @@ return new class extends Migration
'long_description' => '',
'icon' => $icon,
'progressive' => true,
'difficulty_description' => "30 different intercontinental flights required, good luck.",
'threshold' => 36,
'difficulty_description' => "36 different intercontinental flights required, good luck.",
'achievement_category_id' => $countriesAndContinents,
'achievement_difficulty_id' => $nearImposs,
],
@@ -711,6 +881,8 @@ return new class extends Migration
DB::table('achievements')->insert($achievements);
}
public function down(): void
{
// Reverse user_achievements