.json. */ final class Level { public const BLOCK = 128; /** @var Sector[] */ public array $sectors; /** @var Line[] */ public array $lines; /** @var Node[] */ public array $nodes; /** @var Subsector[] */ public array $subsectors; public int $root; public array $things; public string $name; public string $id; public int $par; public string $sky; /** block map: origin, size, lines per block */ public float $bmx = 0.0; public float $bmy = 0.0; public int $bmw = 0; public int $bmh = 0; /** @var array */ public array $blocks = []; public int $validCount = 0; /** bounds of the map */ public array $bounds; public array $data; public function __construct(string $id, array $data, array $thingLegend) { $this->id = $id; $this->data = $data; $b = new LevelBuilder($data, $thingLegend); $this->sectors = $b->sectors; $this->lines = $b->lines; $this->things = $b->things; $nb = new NodeBuilder($this->lines); $this->nodes = $nb->nodes; $this->subsectors = $nb->subsectors; $this->root = $nb->root; $this->name = $data['name'] ?? strtoupper($id); $this->par = (int)($data['par'] ?? 120); $this->sky = $data['sky'] ?? 'SKY'; $this->blockMap(); } public static function load(string $file, array $thingLegend): self { $data = json_decode((string)file_get_contents($file), true); if (!is_array($data)) { throw new \RuntimeException("Cannot read the level $file"); } return new self(basename($file, '.json'), $data, $thingLegend); } private function blockMap(): void { $minx = $miny = INF; $maxx = $maxy = -INF; foreach ($this->lines as $l) { $minx = min($minx, $l->x1, $l->x2); $maxx = max($maxx, $l->x1, $l->x2); $miny = min($miny, $l->y1, $l->y2); $maxy = max($maxy, $l->y1, $l->y2); } $this->bounds = [$minx, $miny, $maxx, $maxy]; $this->bmx = $minx - 8; $this->bmy = $miny - 8; $this->bmw = (int)(($maxx - $this->bmx) / self::BLOCK) + 2; $this->bmh = (int)(($maxy - $this->bmy) / self::BLOCK) + 2; $this->blocks = []; foreach ($this->lines as $l) { $x0 = (int)floor((min($l->x1, $l->x2) - $this->bmx) / self::BLOCK); $x1 = (int)floor((max($l->x1, $l->x2) - $this->bmx) / self::BLOCK); $y0 = (int)floor((min($l->y1, $l->y2) - $this->bmy) / self::BLOCK); $y1 = (int)floor((max($l->y1, $l->y2) - $this->bmy) / self::BLOCK); for ($by = $y0; $by <= $y1; $by++) { for ($bx = $x0; $bx <= $x1; $bx++) { // keep it simple: a line goes in every block of its box (diagonals: a few extra checks) $this->blocks[$by * $this->bmw + $bx][] = $l; } } } } /** Lines whose blocks touch the box [x1, y1, x2, y2], each once. @return Line[] */ public function linesInBox(float $x1, float $y1, float $x2, float $y2): array { $bx0 = max(0, (int)floor(($x1 - $this->bmx) / self::BLOCK)); $bx1 = min($this->bmw - 1, (int)floor(($x2 - $this->bmx) / self::BLOCK)); $by0 = max(0, (int)floor(($y1 - $this->bmy) / self::BLOCK)); $by1 = min($this->bmh - 1, (int)floor(($y2 - $this->bmy) / self::BLOCK)); $vc = ++$this->validCount; $out = []; for ($by = $by0; $by <= $by1; $by++) { for ($bx = $bx0; $bx <= $bx1; $bx++) { foreach ($this->blocks[$by * $this->bmw + $bx] ?? [] as $l) { if ($l->validCount !== $vc) { $l->validCount = $vc; $out[] = $l; } } } } return $out; } /** R_PointInSubsector */ public function subsectorAt(float $x, float $y): Subsector { $n = $this->root; if ($n < 0) { return $this->subsectors[-1 - $n]; } while ($n >= 0) { $node = $this->nodes[$n]; $n = $node->children[(($x - $node->x) * $node->dy - ($y - $node->y) * $node->dx) >= 0 ? 0 : 1]; } return $this->subsectors[-1 - $n]; } public function sectorAt(float $x, float $y): Sector { return $this->subsectorAt($x, $y)->sector; } /** @return Sector[] sectors sharing a line with $s */ public function neighbours(Sector $s): array { $out = []; foreach ($s->lines as $l) { if ($l->backSector === null) { continue; } $o = $l->frontSector === $s ? $l->backSector : $l->frontSector; $out[$o->id] = $o; } return array_values($out); } public function lowestNeighbourCeiling(Sector $s): float { $h = INF; foreach ($this->neighbours($s) as $o) { $h = min($h, $o->ceil); } return $h === INF ? $s->ceil : $h; } public function lowestNeighbourFloor(Sector $s): float { $h = INF; foreach ($this->neighbours($s) as $o) { $h = min($h, $o->floor); } return $h === INF ? $s->floor : $h; } public function highestNeighbourFloor(Sector $s): float { $h = -INF; foreach ($this->neighbours($s) as $o) { $h = max($h, $o->floor); } return $h === -INF ? $s->floor : $h; } /** @return Sector[] every sector made of the legend character $char */ public function sectorsOf(string $char): array { return array_values(array_filter($this->sectors, fn (Sector $s) => $s->char === $char)); } }