bounds_max_x) / 40) * 40 + 40); } /** * Where the sand begins (east of the grid). */ public static function sandFrom(City $city): float { return self::layoutMaxX($city) + self::APPROACH; } /** * Shoreline (sand meets sea). */ public static function sandTo(City $city): float { return self::sandFrom($city) + self::SAND_DEPTH; } /** * Far edge of the sea. */ public static function seaTo(City $city): float { return self::sandTo($city) + self::SEA_DEPTH; } /** * Geometry descriptor sent to the world payload (single source of * truth for the client-side beach build). * * @return array{sand_from: float, sand_to: float, sea_to: float, z_min: float, z_max: float} */ public static function geometry(City $city): array { return [ 'sand_from' => self::sandFrom($city), 'sand_to' => self::sandTo($city), 'sea_to' => self::seaTo($city), 'z_min' => -self::Z_HALF, 'z_max' => self::Z_HALF, ]; } /** * Whether a point lies on the leisure beach (sand up to the shoreline). */ public static function isOnBeach(City $city, float $x, float $z): bool { return $x >= self::sandFrom($city) - 1 && $x <= self::sandTo($city) + 2 && $z >= -self::Z_HALF && $z <= self::Z_HALF; } }