Files
City10/database/seeders/CityWorldSeeder.php
T
marcocosta10000 46b15ec7be feat(city): add city landmarks, stores, luminous signs, and ATC echo guard
Add university and fire station civic buildings plus 7 new store types,
each with facade and luminous blade signs. Extend landmark material
colors and sign accents. Fix ATC dispatch to reject prompt-echoing AI
responses via heuristic guard. Add CityPlacesTest for collision and
roadway avoidance, and extend AirportTest and SignLabelsTest.
2026-10-31 22:22:44 -03:00

249 lines
9.4 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,
],
);
// More civic landmarks (rendered with luminous letreiros).
Building::updateOrCreate(
['city_id' => $city->id, 'type' => 'university'],
[
'owner_character_id' => null,
'name' => 'Universidade City10',
'pos_x' => 100,
'pos_z' => 140,
'width' => 26,
'depth' => 18,
'height' => 12,
'is_complete' => true,
'build_progress' => 100,
],
);
Building::updateOrCreate(
['city_id' => $city->id, 'type' => 'fire_station'],
[
'owner_character_id' => null,
'name' => 'Corpo de Bombeiros',
'pos_x' => -100,
'pos_z' => -20,
'width' => 20,
'depth' => 14,
'height' => 9,
'is_complete' => true,
'build_progress' => 100,
],
);
// ---- City places (all get facade + front board + blade letreiro)
// Each sits on its own free block zone (not on a plaza row), so
// the plaza pattern and the AABB audit both stay clean.
$places = [
['Supermercado Bom Preço', 'supermarket', 100, -20, 22, 9, 16, 'Grande sortimento, hortifrúti e padaria interna.'],
['Drogaria Saúde Total', 'pharmacy', 60, 20, 14, 7, 12, 'Remédios, cosméticos e atendimento 24 horas.'],
['Restaurante Sabor da City', 'restaurant', 100, 100, 16, 8, 14, 'Comida caseira, churrasco e culinária regional.'],
['Cabeleireiro Estilo', 'salon', -60, 20, 12, 6, 10, 'Cortes, colorações e escovas com hora marcada.'],
['Banco Central City10', 'bank', 140, 20, 16, 10, 14, 'Conta corrente, crédito e cooperativa da cidade.'],
['Hotel City10 Palace', 'hotel', 60, 100, 20, 14, 16, 'Suítes com vista para a praça e Wi-Fi gratuito.'],
['Shopping City10', 'mall', -60, 100, 26, 16, 22, 'Lojas, praça de alimentação e cinema.'],
];
foreach ($places as [$name, $type, $px, $pz, $width, $height, $depth, $description]) {
Store::updateOrCreate(
['name' => $name],
[
'description' => $description,
'type' => $type,
'pos_x' => $px,
'pos_y' => 0,
'pos_z' => $pz,
'width' => $width,
'height' => $height,
'depth' => $depth,
'is_open' => true,
],
);
}
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]);
}
}