- 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.
89 lines
2.5 KiB
PHP
89 lines
2.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Character;
|
|
use App\Models\User;
|
|
use Database\Seeders\CityWorldSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Presence feed correctness: the per-second rows are shared, but the
|
|
* is_me flag must always reflect the viewer of the current request —
|
|
* otherwise a citizen sees their own character as a remote player and
|
|
* the bump-to-talk chat opens with themselves.
|
|
*/
|
|
class CityPresenceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
/**
|
|
* Boot world fixtures.
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->seed(CityWorldSeeder::class);
|
|
}
|
|
|
|
/**
|
|
* Two viewers polling within the same second each get their own
|
|
* is_me flags from the shared cache.
|
|
*/
|
|
public function test_is_me_is_per_viewer_with_shared_cache(): void
|
|
{
|
|
[$userA, $charA] = $this->makeCitizen();
|
|
[$userB, $charB] = $this->makeCitizen();
|
|
|
|
// Viewer A seeds the shared per-second cache first.
|
|
$first = $this->actingAs($userA)
|
|
->getJson('/city/presence')
|
|
->assertOk()
|
|
->json('citizens');
|
|
|
|
$this->assertTrue($this->flagFor($first, $charA->id), 'A must see itself as me');
|
|
$this->assertFalse($this->flagFor($first, $charB->id), 'A must not see B as me');
|
|
|
|
// Viewer B hits the SAME cache entry moments later.
|
|
$second = $this->actingAs($userB)
|
|
->getJson('/city/presence')
|
|
->assertOk()
|
|
->json('citizens');
|
|
|
|
$this->assertTrue($this->flagFor($second, $charB->id), 'B must see itself as me');
|
|
$this->assertFalse($this->flagFor($second, $charA->id), 'B must not see A as me');
|
|
}
|
|
|
|
/**
|
|
* Create an active user paired with its in-world character.
|
|
*
|
|
* @return array{0: User, 1: Character}
|
|
*/
|
|
private function makeCitizen(): array
|
|
{
|
|
$user = User::factory()->create([
|
|
'status' => User::STATUS_ACTIVE,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
return [$user, Character::ensureForUser($user)];
|
|
}
|
|
|
|
/**
|
|
* Extract the is_me flag for a character id from a presence payload.
|
|
*
|
|
* @param list<array<string, mixed>> $citizens
|
|
*/
|
|
private function flagFor(array $citizens, int $characterId): bool
|
|
{
|
|
foreach ($citizens as $c) {
|
|
if ((int) $c['id'] === $characterId) {
|
|
return (bool) $c['is_me'];
|
|
}
|
|
}
|
|
|
|
$this->fail("Character {$characterId} missing from presence payload");
|
|
}
|
|
}
|