Files
City10/database/seeders/CityWorldSeeder.php
T
marcocosta10000 ecf5291027 feat(city): add humanoid worker robots as city agents
Seed five worker robot agents (TR-01 to TR-05) with Portuguese
system prompts and active status. Extend the 3D humanoid renderer
with metallic materials, glowing visor, antenna beacon, and chest
status light for the worker_robot type. Add feature tests covering
payload serialization and city bounds validation.
2026-10-31 22:31:03 -03:00

280 lines
11 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],
],
);
// --- 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,
],
);
}
$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]);
}
}