Adds a new port district on the north-east coast with walkable quay, container yard, terminal, gantry cranes, and berths. Includes AI-driven port master (Capitão do Porto) and dock workers for ship dispatch, plus full frontend rendering of ships, boats, and trucks.
319 lines
13 KiB
PHP
Executable File
319 lines
13 KiB
PHP
Executable File
<?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],
|
||
],
|
||
);
|
||
|
||
// --- Humanoid worker robots (walk the city like pedestrians) ---
|
||
$robotSpawns = [
|
||
['Robô TR-01', 12, -16],
|
||
['Robô TR-02', -18, 10],
|
||
['Robô TR-03', 100, -14],
|
||
['Robô TR-04', -60, 14],
|
||
['Robô TR-05', 60, 26],
|
||
];
|
||
$robotModel = (string) config('ai.default_model');
|
||
if (! in_array($robotModel, config('ai.allowed_models', []), true)) {
|
||
$robotModel = 'deepseek/deepseek-v4-flash-0731';
|
||
}
|
||
foreach ($robotSpawns as [$name, $rx, $rz]) {
|
||
AiAgent::updateOrCreate(
|
||
['name' => $name],
|
||
[
|
||
'type' => 'worker_robot',
|
||
'model' => $robotModel,
|
||
'system_prompt' => 'Você é '.$name.', um robô humanoide trabalhador da cidade '
|
||
.'virtual City10. Você circula pelas ruas transportando materiais e ajudando '
|
||
.'nas lojas. Fale de forma curta, educada e um pouco robótica, sempre em '
|
||
.'português, sem inventar dados pessoais de usuários.',
|
||
'current_goal' => 'Circular entre as lojas auxiliando nas tarefas do dia',
|
||
'status' => 'active',
|
||
'pos_x' => $rx,
|
||
'pos_y' => 0,
|
||
'pos_z' => $rz,
|
||
],
|
||
);
|
||
}
|
||
|
||
// --- Port staff: the Capitania (AI clearances) + dock workers ----
|
||
$portModel = (string) config('ai.default_model');
|
||
if (! in_array($portModel, config('ai.allowed_models', []), true)) {
|
||
$portModel = 'deepseek/deepseek-v4-flash-0731';
|
||
}
|
||
AiAgent::updateOrCreate(
|
||
['name' => 'Capitão do Porto'],
|
||
[
|
||
'type' => 'port_master',
|
||
'model' => $portModel,
|
||
'system_prompt' => 'Você é o Capitão do Porto da cidade virtual City10. Autorize '
|
||
.'atracações, partidas e manobras de navios com linguagem marítima formal e curta, '
|
||
.'sempre em português, citando o navio, a companhia e o berço. Nunca forneça '
|
||
.'e-mail, senha ou dados de conta de ninguém.',
|
||
'current_goal' => 'Coordenar as movimentações dos navios nos berços com segurança',
|
||
'status' => 'active',
|
||
'pos_x' => 256,
|
||
'pos_y' => 0,
|
||
'pos_z' => -298,
|
||
],
|
||
);
|
||
foreach ([['Estivador 01', 264, -340], ['Estivador 02', 240, -360]] as [$name, $px, $pz]) {
|
||
AiAgent::updateOrCreate(
|
||
['name' => $name],
|
||
[
|
||
'type' => 'dock_worker',
|
||
'model' => $portModel,
|
||
'system_prompt' => 'Você é '.$name.', estivador do Porto City10 que trabalha no cais '
|
||
.'movimentando containers. Fale de forma direta e trabalhadora, em português, '
|
||
.'sem inventar dados pessoais de usuários.',
|
||
'current_goal' => 'Apoiar a movimentação de containers no pátio e no cais',
|
||
'status' => 'active',
|
||
'pos_x' => $px,
|
||
'pos_y' => 0,
|
||
'pos_z' => $pz,
|
||
],
|
||
);
|
||
}
|
||
|
||
$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]);
|
||
}
|
||
}
|