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:
2026-10-31 22:06:18 -03:00
parent a12807492f
commit e7498e1566
26 changed files with 2226 additions and 108 deletions
@@ -35,8 +35,12 @@ class CityPresenceController extends Controller
->first() ?? $city;
}
// Rows are shared across users for one second, so is_me must NOT
// be baked into the cached payload — it is computed per viewer
// below (otherwise user B sees user A's is_me flags and could see
// their own character as a remote citizen).
$cacheKey = 'presence.'.$city->id.'.'.now()->timestamp;
$citizens = Cache::remember($cacheKey, 1, function () use ($city, $me) {
$citizens = Cache::remember($cacheKey, 1, function () use ($city) {
return Character::query()
->where('city_id', $city->id)
->where(function ($q): void {
@@ -52,13 +56,18 @@ class CityPresenceController extends Controller
'status' => $c->state,
'position' => [(float) $c->pos_x, (float) $c->pos_y, (float) $c->pos_z],
'is_online' => (bool) $c->is_online,
'is_me' => (int) $c->id === (int) $me->id,
'owner_name' => $c->user?->name,
])
->values()
->all();
});
$viewerId = (int) $me->id;
$citizens = array_map(
static fn (array $c): array => $c + ['is_me' => (int) $c['id'] === $viewerId],
$citizens
);
return response()->json([
'city_id' => $city->id,
'server_time' => now()->toIso8601String(),