*/ use HasFactory; /** * Default personality pool for characters created without one. * * @var list */ public const AI_PERSONALITIES = [ 'Extrovertido e curioso, adora puxar conversa com quem passa.', 'Tranquilo e observador, fala pouco mas escuta tudo com atenção.', 'Bem-humorado e espirituoso, brinca com todo mundo por perto.', 'Sério e direto, vai ao ponto sem rodeios.', 'Gentil e prestativo, sempre oferece ajuda antes de perguntarem.', 'Aventureiro e barulhento, vive contando histórias da cidade.', ]; 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'); } /** * Marketplace listings published by this character. * * @return HasMany */ public function listings(): HasMany { return $this->hasMany(MarketListing::class, 'seller_character_id'); } /** * Missions this character is tracking. * * @return BelongsToMany */ public function missions(): BelongsToMany { return $this->belongsToMany(Mission::class, 'character_missions') ->withPivot(['progress', 'is_completed', 'completed_at']) ->withTimestamps(); } /** * 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, 'ai_age' => random_int(18, 68), 'ai_personality' => self::AI_PERSONALITIES[array_rand(self::AI_PERSONALITIES)], '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, 'face' => random_int(1, 3), 'beard' => random_int(0, 2), 'accessory' => random_int(0, 2), ]); // 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 [ 'ai_age' => 'integer', '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', 'face' => 'integer', 'beard' => 'integer', 'accessory' => 'integer', 'ride_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; } }