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,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\City;
|
||||
|
||||
/**
|
||||
* Airport grounds north and south of the city grid.
|
||||
*
|
||||
* Geometry derives from the layout edges so both airports always sit on
|
||||
* open land beyond the roads. The same descriptor drives the client-side
|
||||
* build (runways, taxiways, aprons, terminals, hangars, stands, helipads)
|
||||
* and the walkable-visit zone used by the position clamp.
|
||||
*/
|
||||
class Airports
|
||||
{
|
||||
/** Half width (X) of the walkable airport strips. */
|
||||
public const VISIT_HALF_WIDTH = 110.0;
|
||||
|
||||
/** Walkable Z range inside the airport strips (both airports). */
|
||||
public const VISIT_MIN_Z = -400.0;
|
||||
|
||||
public const VISIT_MAX_Z = 400.0;
|
||||
|
||||
/**
|
||||
* Layout edge north of the city (same derivation the road grid uses).
|
||||
*/
|
||||
public static function layoutMaxZ(City $city): float
|
||||
{
|
||||
return (float) (ceil(((float) $city->bounds_max_z) / 40) * 40 + 40);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout edge south of the city.
|
||||
*/
|
||||
public static function layoutMinZ(City $city): float
|
||||
{
|
||||
return (float) (floor(((float) $city->bounds_min_z) / 40) * 40 - 40);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full airports descriptor for the world payload.
|
||||
*
|
||||
* @return array{visit: array<string, float>, sites: list<array<string, mixed>>}
|
||||
*/
|
||||
public static function geometry(City $city): array
|
||||
{
|
||||
return [
|
||||
'visit' => [
|
||||
'half_width' => self::VISIT_HALF_WIDTH,
|
||||
'min_z' => self::VISIT_MIN_Z,
|
||||
'max_z' => self::VISIT_MAX_Z,
|
||||
],
|
||||
'sites' => [
|
||||
self::northSite(self::layoutMaxZ($city)),
|
||||
self::southSite(self::layoutMinZ($city)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Large airport south of the city (+Z): east-west runway.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function northSite(float $lz): array
|
||||
{
|
||||
// lz = 160 for the shipped bounds: runway at 350, apron at 285.
|
||||
$standZ = $lz + 135;
|
||||
|
||||
return [
|
||||
'id' => 'sul',
|
||||
'name' => 'Aeroporto City10 Sul',
|
||||
'code' => 'CTS',
|
||||
'runway' => [
|
||||
'center' => [0.0, $lz + 190],
|
||||
'length' => 180.0,
|
||||
'width' => 18.0,
|
||||
'axis' => 'x',
|
||||
'designators' => ['09', '27'],
|
||||
],
|
||||
'taxiways' => [
|
||||
['center' => [0.0, $lz + 160], 'length' => 170.0, 'width' => 10.0, 'axis' => 'x'],
|
||||
['center' => [-80.0, $lz + 175], 'length' => 30.0, 'width' => 8.0, 'axis' => 'z'],
|
||||
['center' => [80.0, $lz + 175], 'length' => 30.0, 'width' => 8.0, 'axis' => 'z'],
|
||||
],
|
||||
'apron' => ['center' => [0.0, $lz + 125], 'size' => [170.0, 60.0]],
|
||||
'terminal' => ['center' => [-45.0, $lz + 105], 'size' => [80.0, 18.0]],
|
||||
'hangars' => [
|
||||
['center' => [35.0, $lz + 106], 'size' => [26.0, 20.0]],
|
||||
['center' => [65.0, $lz + 106], 'size' => [26.0, 20.0]],
|
||||
],
|
||||
'tower' => ['center' => [10.0, $lz + 118]],
|
||||
'stands' => [
|
||||
[-70.0, $standZ],
|
||||
[-35.0, $standZ],
|
||||
[0.0, $standZ],
|
||||
[35.0, $standZ],
|
||||
],
|
||||
'helipad' => ['center' => [-100.0, $lz + 165], 'radius' => 8.0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Regional airport north of the city (-Z): north-south runway.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function southSite(float $lz): array
|
||||
{
|
||||
// lz = -160 for the shipped bounds: runway centre at -300.
|
||||
$cZ = $lz - 140;
|
||||
|
||||
return [
|
||||
'id' => 'norte',
|
||||
'name' => 'Aeroporto Regional do Norte',
|
||||
'code' => 'CRN',
|
||||
'runway' => [
|
||||
'center' => [-50.0, $cZ],
|
||||
'length' => 160.0,
|
||||
'width' => 14.0,
|
||||
'axis' => 'z',
|
||||
'designators' => ['18', '36'],
|
||||
],
|
||||
'taxiways' => [
|
||||
['center' => [-30.0, $cZ], 'length' => 150.0, 'width' => 8.0, 'axis' => 'z'],
|
||||
// Connectors aligned with the takeoff lineup (-240) and the
|
||||
// landing rollout exit (-370) so taxi paths stay on pavement.
|
||||
['center' => [-40.0, $lz - 80], 'length' => 20.0, 'width' => 7.0, 'axis' => 'x'],
|
||||
['center' => [-40.0, $lz - 210], 'length' => 20.0, 'width' => 7.0, 'axis' => 'x'],
|
||||
],
|
||||
'apron' => ['center' => [15.0, $cZ], 'size' => [90.0, 140.0]],
|
||||
'terminal' => ['center' => [55.0, $lz - 95], 'size' => [34.0, 14.0]],
|
||||
'hangars' => [
|
||||
['center' => [55.0, $lz - 205], 'size' => [24.0, 18.0]],
|
||||
],
|
||||
'tower' => ['center' => [40.0, $lz - 80]],
|
||||
'stands' => [
|
||||
[0.0, $lz - 95],
|
||||
[0.0, $lz - 155],
|
||||
[30.0, $lz - 125],
|
||||
[30.0, $lz - 185],
|
||||
],
|
||||
'helipad' => ['center' => [75.0, $lz - 85], 'radius' => 7.0],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user