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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\CityWorldSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Luminous-sign labels must exist in every locale (RULES.md 1.4:
|
||||
* every user-visible string ships in pt_BR, en and es).
|
||||
*/
|
||||
class SignLabelsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Locales covered by the platform.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const LOCALES = ['pt_BR', 'en', 'es'];
|
||||
|
||||
/**
|
||||
* Sign label keys rendered by the 3D letreiros.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const KEYS = [
|
||||
'city.sign_plaza',
|
||||
'city.sign_beach',
|
||||
'city.sign_food',
|
||||
'city.sign_hospital',
|
||||
'city.sign_school',
|
||||
'city.sign_cityhall',
|
||||
];
|
||||
|
||||
/**
|
||||
* Every sign label exists (non-empty) in all three languages.
|
||||
*/
|
||||
public function test_sign_labels_exist_in_all_locales(): void
|
||||
{
|
||||
foreach (self::LOCALES as $locale) {
|
||||
$path = base_path("lang/{$locale}.json");
|
||||
$this->assertFileExists($path);
|
||||
|
||||
$labels = json_decode((string) file_get_contents($path), true);
|
||||
$this->assertIsArray($labels);
|
||||
|
||||
foreach (self::KEYS as $key) {
|
||||
$this->assertArrayHasKey($key, $labels, "Missing {$key} in {$locale}");
|
||||
$this->assertNotSame('', trim((string) $labels[$key]), "Empty {$key} in {$locale}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The labels reach the viewer page for the active locale.
|
||||
*/
|
||||
public function test_viewer_page_carries_sign_labels(): void
|
||||
{
|
||||
$this->seed(CityWorldSeeder::class);
|
||||
$user = User::factory()->create([
|
||||
'status' => User::STATUS_ACTIVE,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$html = $this->get('/city')->assertOk()->getContent();
|
||||
|
||||
foreach (['signPlaza', 'signBeach', 'signFood', 'signHospital', 'signSchool', 'signCityhall'] as $jsKey) {
|
||||
$this->assertStringContainsString($jsKey.':', $html, "Missing i18n entry {$jsKey}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user