feat(world): add persistent citizen simulation with needs, jobs, and social features
- Adds Character model with survival needs (hunger, thirst, energy) and state machine - Implements city:tick world ticker for offline progression every minute - Adds city territory expansion and building construction system - Introduces job economy, inventory, and transaction audit trail - Adds social graph (friends, marriage, children) and character chat - Implements admin approval workflow for new registrations - Adds hospital admission system for traffic accidents - Includes comprehensive feature tests and world seeders
This commit is contained in:
Executable
+68
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Player-constructed structure placed on free terrain inside a city.
|
||||
*
|
||||
* Construction advances through build_progress on each world tick until the
|
||||
* building is complete; finished houses become the owner's residence.
|
||||
*/
|
||||
#[Fillable([
|
||||
'city_id', 'owner_character_id', 'type', 'name',
|
||||
'pos_x', 'pos_z', 'width', 'depth', 'height',
|
||||
'is_complete', 'build_progress',
|
||||
])]
|
||||
class Building extends Model
|
||||
{
|
||||
public const TYPE_HOUSE = 'house';
|
||||
|
||||
/**
|
||||
* Public hospital where accident victims are admitted.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TYPE_HOSPITAL = 'hospital';
|
||||
|
||||
/**
|
||||
* City containing this building.
|
||||
*
|
||||
* @return BelongsTo<City, $this>
|
||||
*/
|
||||
public function city(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(City::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Character that commissioned the building.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'owner_character_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'pos_x' => 'float',
|
||||
'pos_z' => 'float',
|
||||
'width' => 'float',
|
||||
'depth' => 'float',
|
||||
'height' => 'float',
|
||||
'is_complete' => 'boolean',
|
||||
'build_progress' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+258
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\CharacterFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* In-world avatar of a platform user (or NPC child without a user).
|
||||
*
|
||||
* Stores position, survival needs, employment and social state. The world
|
||||
* ticker mutates needs and job progress continuously while nobody is online.
|
||||
*/
|
||||
#[Fillable([
|
||||
'user_id', 'city_id', 'name', 'skin', 'hair', 'shirt',
|
||||
'pos_x', 'pos_y', 'pos_z', 'money', 'energy', 'hunger', 'thirst',
|
||||
'state', 'job_id', 'house_id', 'education', 'is_online', 'last_tick_at',
|
||||
'hospitalized_until',
|
||||
])]
|
||||
class Character extends Model
|
||||
{
|
||||
/** @use HasFactory<CharacterFactory, $this> */
|
||||
use HasFactory;
|
||||
|
||||
public const STATE_AWAKE = 'awake';
|
||||
|
||||
public const STATE_SLEEPING = 'sleeping';
|
||||
|
||||
public const STATE_WORKING = 'working';
|
||||
|
||||
public const STATE_STUDYING = 'studying';
|
||||
|
||||
/**
|
||||
* Character admitted to the hospital after a traffic accident.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const STATE_HOSPITAL = 'hospital';
|
||||
|
||||
/**
|
||||
* Hospital stay length in seconds (two game days of 240s each).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const HOSPITAL_STAY_SECONDS = 480;
|
||||
|
||||
/**
|
||||
* Default hospital bed coordinates used when no hospital building exists.
|
||||
* Kept mid-block (away from road lanes) so vehicles never hit the bed.
|
||||
*
|
||||
* @var array{0: float, 1: float}
|
||||
*/
|
||||
public const HOSPITAL_POS = [60.0, -51.0];
|
||||
|
||||
/**
|
||||
* Owning user account (null for NPC children).
|
||||
*
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* City where this character lives.
|
||||
*
|
||||
* @return BelongsTo<City, $this>
|
||||
*/
|
||||
public function city(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(City::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current job position, when employed.
|
||||
*
|
||||
* @return BelongsTo<Job, $this>
|
||||
*/
|
||||
public function job(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Job::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete house owned by this character, when any.
|
||||
*
|
||||
* @return BelongsTo<Building, $this>
|
||||
*/
|
||||
public function house(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Building::class, 'house_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Buildings constructed by this character.
|
||||
*
|
||||
* @return HasMany<Building, $this>
|
||||
*/
|
||||
public function buildings(): HasMany
|
||||
{
|
||||
return $this->hasMany(Building::class, 'owner_character_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory stacks owned by this character.
|
||||
*
|
||||
* @return HasMany<InventoryItem, $this>
|
||||
*/
|
||||
public function inventory(): HasMany
|
||||
{
|
||||
return $this->hasMany(InventoryItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Character of a linked user, when this account has one.
|
||||
*/
|
||||
public function forUser(int $userId): bool
|
||||
{
|
||||
return (int) $this->user_id === $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the character for an approved user in the given city.
|
||||
*/
|
||||
public static function ensureForUser(User $user, ?City $city = null): self
|
||||
{
|
||||
$existing = self::query()->where('user_id', $user->id)->first();
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$city ??= City::active();
|
||||
abort_if($city === null, 500, 'No active city available.');
|
||||
|
||||
$firstName = trim(explode(' ', $user->name)[0] ?? $user->email);
|
||||
|
||||
$character = self::create([
|
||||
'user_id' => $user->id,
|
||||
'city_id' => $city->id,
|
||||
'name' => $firstName,
|
||||
'skin' => random_int(1, 4),
|
||||
'hair' => random_int(1, 4),
|
||||
'shirt' => random_int(1, 5),
|
||||
'pos_x' => 0,
|
||||
'pos_y' => 0,
|
||||
'pos_z' => 25,
|
||||
'money' => 100,
|
||||
'energy' => 100,
|
||||
'hunger' => 100,
|
||||
'thirst' => 100,
|
||||
'state' => self::STATE_AWAKE,
|
||||
'education' => 0,
|
||||
'is_online' => false,
|
||||
]);
|
||||
|
||||
// Starter kit so new citizens can eat, drink and build immediately.
|
||||
foreach ([
|
||||
InventoryItem::ITEM_FOOD => 3,
|
||||
InventoryItem::ITEM_WATER => 3,
|
||||
InventoryItem::ITEM_MATERIALS => 60,
|
||||
] as $item => $qty) {
|
||||
InventoryItem::create([
|
||||
'character_id' => $character->id,
|
||||
'item' => $item,
|
||||
'qty' => $qty,
|
||||
]);
|
||||
}
|
||||
|
||||
return $character;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'pos_x' => 'float',
|
||||
'pos_y' => 'float',
|
||||
'pos_z' => 'float',
|
||||
'money' => 'float',
|
||||
'energy' => 'integer',
|
||||
'hunger' => 'integer',
|
||||
'thirst' => 'integer',
|
||||
'education' => 'integer',
|
||||
'is_online' => 'boolean',
|
||||
'last_tick_at' => 'datetime',
|
||||
'hospitalized_until' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the character is currently admitted to the hospital.
|
||||
*/
|
||||
public function isHospitalized(): bool
|
||||
{
|
||||
return $this->state === self::STATE_HOSPITAL
|
||||
&& $this->hospitalized_until !== null
|
||||
&& $this->hospitalized_until->isFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Admit the character to the hospital for two game days after an accident.
|
||||
*
|
||||
* Teleports them to the city hospital bed and returns the bed position.
|
||||
*
|
||||
* @return array{0: float, 1: float}
|
||||
*/
|
||||
public function admitToHospital(): array
|
||||
{
|
||||
$hospital = Building::query()
|
||||
->where('city_id', $this->city_id)
|
||||
->where('type', Building::TYPE_HOSPITAL)
|
||||
->where('is_complete', true)
|
||||
->first();
|
||||
|
||||
$x = $hospital ? (float) $hospital->pos_x : self::HOSPITAL_POS[0];
|
||||
$z = $hospital ? (float) ($hospital->pos_z + $hospital->depth / 2 + 3) : self::HOSPITAL_POS[1];
|
||||
|
||||
$this->forceFill([
|
||||
'state' => self::STATE_HOSPITAL,
|
||||
'hospitalized_until' => now()->addSeconds(self::HOSPITAL_STAY_SECONDS),
|
||||
'pos_x' => $x,
|
||||
'pos_z' => $z,
|
||||
'pos_y' => 0,
|
||||
])->save();
|
||||
|
||||
return [$x, $z];
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the character from the hospital when the stay has elapsed.
|
||||
*/
|
||||
public function releaseIfHospitalStayOver(): bool
|
||||
{
|
||||
if ($this->state !== self::STATE_HOSPITAL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hospitalized_until === null || $this->hospitalized_until->isPast()) {
|
||||
$this->forceFill([
|
||||
'state' => self::STATE_AWAKE,
|
||||
'hospitalized_until' => null,
|
||||
])->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Chat message exchanged between two characters.
|
||||
*/
|
||||
#[Fillable(['from_character_id', 'to_character_id', 'body'])]
|
||||
class CharacterMessage extends Model
|
||||
{
|
||||
/**
|
||||
* Sending character.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function fromCharacter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'from_character_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiving character.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function toCharacter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'to_character_id');
|
||||
}
|
||||
}
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\CityFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Geographic territory of a virtual city.
|
||||
*
|
||||
* Bounds drive street layout generation, movement clamping and construction
|
||||
* limits. Administrators may expand bounds or create additional cities.
|
||||
*/
|
||||
#[Fillable([
|
||||
'name', 'slug', 'bounds_min_x', 'bounds_max_x', 'bounds_min_z', 'bounds_max_z',
|
||||
'is_active', 'expansions',
|
||||
])]
|
||||
class City extends Model
|
||||
{
|
||||
/** @use HasFactory<CityFactory, $this> */
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Maximum number of territory expansions allowed per city.
|
||||
*/
|
||||
public const MAX_EXPANSIONS = 10;
|
||||
|
||||
/**
|
||||
* Characters living in this city.
|
||||
*
|
||||
* @return HasMany<Character, $this>
|
||||
*/
|
||||
public function characters(): HasMany
|
||||
{
|
||||
return $this->hasMany(Character::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buildings placed inside this city.
|
||||
*
|
||||
* @return HasMany<Building, $this>
|
||||
*/
|
||||
public function buildings(): HasMany
|
||||
{
|
||||
return $this->hasMany(Building::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Job positions offered in this city.
|
||||
*
|
||||
* @return HasMany<Job, $this>
|
||||
*/
|
||||
public function jobs(): HasMany
|
||||
{
|
||||
return $this->hasMany(Job::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the active city used as the default world.
|
||||
*/
|
||||
public static function active(): ?self
|
||||
{
|
||||
return self::query()->where('is_active', true)->orderBy('id')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand territory eastward and northward by one grid step (40 units).
|
||||
*
|
||||
* Returns false when the expansion cap has been reached.
|
||||
*/
|
||||
public function expand(): bool
|
||||
{
|
||||
if ($this->expansions >= self::MAX_EXPANSIONS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->bounds_max_x = (float) $this->bounds_max_x + 40;
|
||||
$this->bounds_max_z = (float) $this->bounds_max_z + 40;
|
||||
$this->expansions = (int) $this->expansions + 1;
|
||||
$this->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a world coordinate lies inside the city bounds.
|
||||
*/
|
||||
public function contains(float $x, float $z): bool
|
||||
{
|
||||
return $x >= (float) $this->bounds_min_x && $x <= (float) $this->bounds_max_x
|
||||
&& $z >= (float) $this->bounds_min_z && $z <= (float) $this->bounds_max_z;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'bounds_min_x' => 'float',
|
||||
'bounds_max_x' => 'float',
|
||||
'bounds_min_z' => 'float',
|
||||
'bounds_max_z' => 'float',
|
||||
'is_active' => 'boolean',
|
||||
'expansions' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Stackable inventory row owned by a character.
|
||||
*/
|
||||
#[Fillable(['character_id', 'item', 'qty'])]
|
||||
class InventoryItem extends Model
|
||||
{
|
||||
public const ITEM_FOOD = 'food';
|
||||
|
||||
public const ITEM_WATER = 'water';
|
||||
|
||||
public const ITEM_MATERIALS = 'materials';
|
||||
|
||||
public const ITEM_PRODUCT = 'product';
|
||||
|
||||
/**
|
||||
* Owning character.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function character(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class);
|
||||
}
|
||||
}
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Employment position offered inside a city (table: job_positions).
|
||||
*
|
||||
* Characters hold at most one job; salaries accrue while state=working.
|
||||
*/
|
||||
#[Fillable(['city_id', 'store_id', 'title', 'salary', 'requires_education'])]
|
||||
class Job extends Model
|
||||
{
|
||||
/**
|
||||
* Eloquent table name (queue already owns "jobs".
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'job_positions';
|
||||
|
||||
/**
|
||||
* City offering the position.
|
||||
*
|
||||
* @return BelongsTo<City, $this>
|
||||
*/
|
||||
public function city(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(City::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store providing the position, when commercial.
|
||||
*
|
||||
* @return BelongsTo<Store, $this>
|
||||
*/
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'salary' => 'float',
|
||||
'requires_education' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Social edge between two characters (friend, spouse, parent-child).
|
||||
*
|
||||
* For parent_child links character_a is the child and character_b is the parent.
|
||||
*/
|
||||
#[Fillable(['character_a_id', 'character_b_id', 'type', 'status'])]
|
||||
class Relationship extends Model
|
||||
{
|
||||
public const TYPE_FRIEND = 'friend';
|
||||
|
||||
public const TYPE_SPOUSE = 'spouse';
|
||||
|
||||
public const TYPE_PARENT_CHILD = 'parent_child';
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const STATUS_ACCEPTED = 'accepted';
|
||||
|
||||
/**
|
||||
* First character participant.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function characterA(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'character_a_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Second character participant.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function characterB(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'character_b_id');
|
||||
}
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Audit record of an economic exchange involving characters and/or stores.
|
||||
*/
|
||||
#[Fillable(['from_character_id', 'to_character_id', 'store_id', 'item', 'qty', 'amount'])]
|
||||
class Transaction extends Model
|
||||
{
|
||||
/**
|
||||
* Character that paid or gave items.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function fromCharacter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'from_character_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Character that received payment or items.
|
||||
*
|
||||
* @return BelongsTo<Character, $this>
|
||||
*/
|
||||
public function toCharacter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Character::class, 'to_character_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store involved in the exchange, when any.
|
||||
*
|
||||
* @return BelongsTo<Store, $this>
|
||||
*/
|
||||
public function store(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Store::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'amount' => 'float',
|
||||
'qty' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
+51
-1
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
@@ -19,7 +20,7 @@ use Laravel\Sanctum\HasApiTokens;
|
||||
* acceptance of versioned legal documents (Terms, Privacy, Cookies)
|
||||
* required by the Brazilian LGPD.
|
||||
*/
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'locale', 'is_active'])]
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'status', 'locale', 'is_active'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
@@ -40,6 +41,37 @@ class User extends Authenticatable
|
||||
*/
|
||||
public const ROLE_USER = 'user';
|
||||
|
||||
/**
|
||||
* Account registered but not yet approved by an administrator.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
/**
|
||||
* Account approved and able to enter the platform.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
|
||||
/**
|
||||
* Account blocked by an administrator.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const STATUS_BANNED = 'banned';
|
||||
|
||||
/**
|
||||
* In-world character belonging to this user.
|
||||
*
|
||||
* @return HasOne<Character, $this>
|
||||
*/
|
||||
public function character(): HasOne
|
||||
{
|
||||
return $this->hasOne(Character::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores owned by this user.
|
||||
*
|
||||
@@ -80,6 +112,14 @@ class User extends Authenticatable
|
||||
return $this->role === self::ROLE_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the account has been approved by an administrator.
|
||||
*/
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user accepted every current legal document version.
|
||||
*/
|
||||
@@ -96,6 +136,16 @@ class User extends Authenticatable
|
||||
return $currentIds->diff($acceptedIds)->isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the legacy is_active flag mirrored from status on save.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $user): void {
|
||||
$user->is_active = $user->status === self::STATUS_ACTIVE;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user