登录
ZH
games/doom-jumphp / src / Engine / Map / Level.php · commit 8e1cb9f
<?phpnamespace App\Engine\Map;/** * A playable level: geometry, BSP tree, block map of lines (p_setup.c). Built from assets/maps/<id>.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<int, Line[]> */    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));    }}