Files
City10/database/seeders/CityWorldSeeder.php
T
marcocosta10000 e7498e1566 feat(city): add airports, ATC agent, luminous signs, and AI model fallback chain
- Add two airports (north/south) with full geometry: runways, taxiways,
  aprons, terminals, hangars, stands, and helipads; walkable airport
  grounds extend beyond city bounds.
- Introduce ATC AI agent ("Torre de Controle City10") using AI_ATC_MODEL
  to generate takeoff clearances; includes dispatch endpoint with
  30-second cooldown and deterministic fallback on failure.
- Implement AI model fallback chain: preferred model → AI_FALLBACK_MODEL
  before failing, applied in ChatController and PersonTalkController.
- Add luminous sign labels (letreiros) for plazas, beach, food, hospital,
  school, city hall, and airports with day/night glow.
- Fix city presence cache: compute is_me per viewer instead of baking
  into shared per-second cache to prevent cross-user flag leakage.
- Add tests: AirportTest, CityPresenceTest, SignLabelsTest, and
  PersonTalkTest fallback model retry scenario.
2026-10-31 22:06:18 -03:00

190 lines
6.8 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Database\Seeders;
use App\Models\AiAgent;
use App\Models\Building;
use App\Models\Character;
use App\Models\City;
use App\Models\Job;
use App\Models\Mission;
use App\Models\Store;
use App\Models\User;
use App\Support\Airports;
use Illuminate\Database\Seeder;
/**
* Seeds the persistent world: default city, jobs and citizen characters.
*
* Backfills users.status for pre-existing accounts and ensures every active
* user has a character so the platform works after the status migration.
*/
class CityWorldSeeder extends Seeder
{
/**
* Seed the world tables.
*/
public function run(): void
{
$city = City::updateOrCreate(
['slug' => 'city10'],
[
'name' => 'City10',
// Performance-tuned size: 72 blocks (9 × 8) after the
// 40-unit road grid and its outer margin are derived from
// these bounds (layout −160..200 × −160..160).
'bounds_min_x' => -120,
'bounds_max_x' => 160,
'bounds_min_z' => -120,
'bounds_max_z' => 120,
'is_active' => true,
'expansions' => 0,
],
);
// --- Control tower agent (airport clearances) ---------------------
// Positioned at the north airport tower; model comes from
// AI_ATC_MODEL (poolside/laguna-xs-2.1:free by default) and is
// re-validated against the allow-list before use.
$tower = Airports::geometry($city)['sites'][0]['tower']['center'];
$atcModel = (string) config('ai.atc_model');
if (! in_array($atcModel, config('ai.allowed_models', []), true)) {
$atcModel = 'poolside/laguna-xs-2.1:free';
}
AiAgent::updateOrCreate(
['name' => 'Torre de Controle City10'],
[
'type' => 'atc',
'model' => $atcModel,
'system_prompt' => 'Você é a torre de controle aéreo do Aeroporto City10 Norte. '
.'Autorize decolagens e pousos com linguagem formal, curta e clássica de controle '
.'de tráfego aéreo, sempre em português, citando o voo, a companhia e a pista.',
'current_goal' => 'Gerenciar decolagens, pousos e o tráfego dos dois aeroportos com segurança',
'status' => 'active',
'pos_x' => (float) $tower[0],
'pos_y' => 0,
'pos_z' => (float) $tower[1],
],
);
$bakery = Store::where('type', 'bakery')->orderBy('id')->first();
$market = Store::where('type', 'market')->orderBy('id')->first();
Job::updateOrCreate(
['city_id' => $city->id, 'title' => 'Padeiro'],
['store_id' => $bakery?->id, 'salary' => 12, 'requires_education' => 0],
);
Job::updateOrCreate(
['city_id' => $city->id, 'title' => 'Operador de mercado'],
['store_id' => $market?->id, 'salary' => 10, 'requires_education' => 0],
);
Job::updateOrCreate(
['city_id' => $city->id, 'title' => 'Servidor público'],
['salary' => 15, 'requires_education' => 30],
);
Job::updateOrCreate(
['city_id' => $city->id, 'title' => 'Professor'],
['salary' => 14, 'requires_education' => 60],
);
// City hospital: accident victims are admitted here for one game hour.
Building::updateOrCreate(
[
'city_id' => $city->id,
'type' => Building::TYPE_HOSPITAL,
],
[
'owner_character_id' => null,
'name' => 'Hospital Municipal',
'pos_x' => 60,
'pos_z' => -60,
'width' => 14,
'depth' => 12,
'height' => 8,
'is_complete' => true,
'build_progress' => 100,
],
);
// Civic buildings: school and city hall.
Building::updateOrCreate(
['city_id' => $city->id, 'type' => 'school'],
[
'owner_character_id' => null,
'name' => 'Escola Municipal',
'pos_x' => -20,
'pos_z' => 60,
'width' => 16,
'depth' => 12,
'height' => 7,
'is_complete' => true,
'build_progress' => 100,
],
);
Building::updateOrCreate(
['city_id' => $city->id, 'type' => 'city_hall'],
[
'owner_character_id' => null,
'name' => 'Prefeitura',
'pos_x' => 20,
'pos_z' => 60,
'width' => 18,
'depth' => 14,
'height' => 10,
'is_complete' => true,
'build_progress' => 100,
],
);
Mission::updateOrCreate(
['code' => 'exercise-5'],
[
'city_id' => $city->id,
'title' => 'Shape em dia',
'description' => 'Complete 5 séries de ginástica nas praças.',
'metric' => 'exercise_count',
'target' => 5,
'reward' => 25,
'is_active' => true,
],
);
Mission::updateOrCreate(
['code' => 'build-home'],
[
'city_id' => $city->id,
'title' => 'Lar doce lar',
'description' => 'Construa a sua primeira casa.',
'metric' => 'buildings',
'target' => 1,
'reward' => 50,
'is_active' => true,
],
);
Mission::updateOrCreate(
['code' => 'learn-20'],
[
'city_id' => $city->id,
'title' => 'Estudioso',
'description' => 'Alcance 20% de educação.',
'metric' => 'education',
'target' => 20,
'reward' => 30,
'is_active' => true,
],
);
User::query()->where('is_active', true)->where('status', '!=', User::STATUS_ACTIVE)
->update(['status' => User::STATUS_ACTIVE]);
User::query()->where('is_active', false)->where('status', User::STATUS_PENDING)
->update(['status' => User::STATUS_BANNED]);
User::query()->where('status', User::STATUS_ACTIVE)->each(function (User $user) use ($city): void {
Character::ensureForUser($user, $city);
});
// Backfill fitness columns for characters created before the migration.
Character::query()->whereNull('health')->update(['health' => 100]);
Character::query()->whereNull('exercise_count')->update(['exercise_count' => 0]);
}
}