feat(port): add sea port with quay, terminal, berths, and AI port master

Adds a new port district on the north-east coast with walkable quay, container yard, terminal, gantry cranes, and berths. Includes AI-driven port master (Capitão do Porto) and dock workers for ship dispatch, plus full frontend rendering of ships, boats, and trucks.
This commit is contained in:
2026-10-31 23:33:06 -03:00
parent e230c391ad
commit 580c830ffb
13 changed files with 1338 additions and 42 deletions
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace App\Support;
use App\Models\City;
/**
* Sea port on the north-east coast (past the beach, on the same east
* shoreline).
*
* The port occupies only the FAR HALF of that side (z −430..−290): the
* near half (z −290..−150) is intentionally left empty so the city can
* keep growing north/east without ever colliding with the port lands.
*
* Geometry derives from the Beach waterline so the quay always meets
* the sea, and feeds the world payload: quay, container yard, terminal,
* gantry cranes, berths and the fleet descriptions. Ships' movements are
* authorised by the port AI agent (Capitão do Porto) through
* /city/port/dispatch — same pattern as the airports.
*/
class Port
{
/** West edge of the walkable port strip (east of the city bounds). */
public const VISIT_MIN_X = 150.0;
/** Visit Z range: from the port up to the start of the beach sand. */
public const VISIT_MIN_Z = -430.0;
public const VISIT_MAX_Z = -290.0;
/**
* Full port descriptor for the world payload.
*
* @return array<string, mixed>
*/
public static function geometry(City $city): array
{
$waterline = Beach::sandTo($city); // 264 for the shipped bounds
$gridEast = (float) (ceil(((float) $city->bounds_max_x) / 40) * 40 + 40); // 200
$quayX = $waterline + 2; // quay straddles the shore
$yardWest = $gridEast + 4;
$yardEast = $waterline - 6;
$yardCenterX = ($yardWest + $yardEast) / 2;
return [
'visit' => [
'min_x' => self::VISIT_MIN_X,
'max_x' => $quayX + 2, // walk to the quay edge (266)
'min_z' => self::VISIT_MIN_Z,
'max_z' => self::VISIT_MAX_Z,
],
'water' => [
// The east sea extends north to cover the port basin
// (initBeach widens its plane down to this Z).
'z_min' => self::VISIT_MIN_Z - 10,
],
// Far-half layout (zone z −430..−290): terminal at the city
// (south) gate, yard behind it, quay along the shore.
'quay' => ['center' => [$quayX, -360.0], 'size' => [8.0, 130.0]],
'yard' => ['center' => [$yardCenterX, -362.0], 'size' => [$yardEast - $yardWest, 116.0]],
'terminal' => ['center' => [$yardCenterX, -297.0], 'size' => [46.0, 14.0]],
'cranes' => [
['center' => [$quayX, -330.0]],
['center' => [$quayX, -395.0]],
],
// Berth centres spaced so full hulls never touch: 60-unit
// container ship + 88-unit liner need >=82 between centres.
'berths' => [
['id' => 1, 'center' => [$quayX + 16, -310.0]],
['id' => 2, 'center' => [$quayX + 20, -393.0]],
],
'ships' => [
['id' => 'c1', 'name' => 'CITY VOYAGER', 'company' => 'City10 Cargo', 'kind' => 'container', 'berth' => 1],
['id' => 'c2', 'name' => 'AZUL MAR', 'company' => 'AzulCéu Cargo', 'kind' => 'container', 'berth' => null],
['id' => 'l1', 'name' => 'ATLÂNTICO CELESTE', 'company' => 'NorteLine', 'kind' => 'liner', 'berth' => 2],
],
'boats' => [
['name' => 'Lancha V1'],
['name' => 'Lancha V2'],
['name' => 'Lancha V3'],
],
];
}
/**
* Whether a point is on the walkable port grounds/quay.
*/
public static function isOnPort(City $city, float $x, float $z): bool
{
$visit = self::geometry($city)['visit'];
return $x >= $visit['min_x']
&& $x <= $visit['max_x']
&& $z >= $visit['min_z']
&& $z <= $visit['max_z'];
}
}