*/ use HasFactory; public const STATE_AWAKE = 'awake'; public const STATE_SLEEPING = 'sleeping'; public const STATE_WORKING = 'working'; public const STATE_STUDYING = 'studying'; /** * Character is exercising on public plaza gym equipment. * * @var string */ public const STATE_EXERCISING = 'exercising'; /** * Character admitted to the hospital after a traffic accident. * * @var string */ public const STATE_HOSPITAL = 'hospital'; /** * Hospital stay length in seconds (one game hour). * * The day/night cycle is 240 real seconds per 24 game hours, so one * game hour equals 10 real seconds. * * @var int */ public const HOSPITAL_STAY_SECONDS = 10; /** * 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 */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * City where this character lives. * * @return BelongsTo */ public function city(): BelongsTo { return $this->belongsTo(City::class); } /** * Current job position, when employed. * * @return BelongsTo */ public function job(): BelongsTo { return $this->belongsTo(Job::class); } /** * Complete house owned by this character, when any. * * @return BelongsTo */ public function house(): BelongsTo { return $this->belongsTo(Building::class, 'house_id'); } /** * Buildings constructed by this character. * * @return HasMany */ public function buildings(): HasMany { return $this->hasMany(Building::class, 'owner_character_id'); } /** * Inventory stacks owned by this character. * * @return HasMany */ 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, 'health' => 100, 'state' => self::STATE_AWAKE, 'education' => 0, 'exercise_count' => 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 */ protected function casts(): array { return [ 'pos_x' => 'float', 'pos_y' => 'float', 'pos_z' => 'float', 'money' => 'float', 'energy' => 'integer', 'hunger' => 'integer', 'thirst' => 'integer', 'health' => 'integer', 'education' => 'integer', 'exercise_count' => '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 one game hour 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 one-hour 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; } }