RU
games/sorcery / src / Render / Screen.php · commit 613f5fb
<?phpnamespace App\Render;use App\Data\Assets;use App\Data\MapLayout;use App\Game\Game;use App\Game\RoomExtra;use App\Game\Values;use JumPHP\Renderer\Renderer;use JumPHP\Renderer\Sdl;/** * The renderer of Sorcery+ (a port of modern/src/render.cpp): the 352x280 frame with a 16/20 border, the 320x240 * screen, the playfield of 320x144 (mode-0 pixels drawn 2 screen px wide) and the status area below it. */final class Screen{    public const FRAME_W = 352;    public const FRAME_H = 280;    public const BORDER_X = 16;    public const BORDER_Y = 20;    public const SCREEN_W = 320;    public const SCREEN_H = 240;    /** @var array<int, array<int, int>> [r, g, b] of the 16 pens of the playfield */    private array $scene = [];    /** @var array<int, array<int, int>> [r, g, b] of the 4 pens of the text */    private array $textPal = [];    private array $border = [0, 0, 0];    /** @var array<string, mixed> the SDL textures */    private array $tex = [];    private bool $loaded = false;    /** @var array<string, MapLayout> */    private array $layouts = [];    private int $frameCounter = 0;    /** the render target of the Strangeloop picture: the bytes revealed so far stay in it */    private mixed $picTarget = null;    private int $picRevealed = -1;    /** @var array<int, int> */    private array $picturePens = [];    public function __construct(private Assets $A, private Renderer $r, private Sdl $sdl)    {    }    public function load(): void    {        if ($this->loaded) {            return;        }        $this->loaded = true;        foreach ($this->A->scenePalette as $i => $ink) {            $this->scene[$i] = Assets::inkRgb($ink);        }        foreach ($this->A->textPalette as $i => $hw) {            $this->textPal[$i] = Assets::hardwareRgb($hw);        }        $this->border = Assets::hardwareRgb($this->A->borderColour);        $this->tex['tiles1'] = $this->loadTex('tiles1', 'assets/tiles/tiles1.png');        $this->tex['tiles2'] = $this->loadTex('tiles2', 'assets/tiles/tiles2.png');        $this->tex['sprites1'] = $this->loadTex('sprites1', 'assets/sprites/sprites1.png');        $this->tex['sprites2'] = $this->loadTex('sprites2', 'assets/sprites/sprites2.png');        $this->tex['extra'] = $this->loadTex('extra', 'assets/sprites/extra.png');        $this->tex['font1'] = $this->loadTex('font1', 'assets/fonts/font1.png');        $this->tex['font2'] = $this->loadTex('font2', 'assets/fonts/font2.png');        $this->tex['font3'] = $this->loadTex('font3', 'assets/fonts/font3.png');        $this->picTarget = $this->r->createTargetTexture(self::SCREEN_W, 200);        $this->picRevealed = -1;    }    /** Loads (or reuses) a texture of the game folder. */    public function loadTex(string $name, string $path): mixed    {        if (!$this->sdl->hasTexture($name)) {            $this->sdl->loadTexture($name, $path);        }        return $this->sdl->getTexture($name)['texture'];    }    /** A level pack brings its own tile sheet. */    public function setLevelTiles(string $key): void    {        if ($key === '' || $this->sdl->hasTexture('leveltiles')) {            return;        }        $this->tex['leveltiles'] = $this->loadTex('leveltiles', 'assets/levels/' . $key . '/tiles.png');    }    // ---- the primitives of the original renderer ----    /** put(): screen coordinates (x 0..319, y 0..239 after yOff), shifted by the border. */    private function put(int $x, int $y, array $c, int $yOff = 0): void    {        $y += $yOff;        if ($x < 0 || $x >= self::SCREEN_W || $y < 0 || $y >= self::SCREEN_H) {            return;        }        $this->r->fillRect(self::BORDER_X + $x, self::BORDER_Y + $y, 1, 1, $c);    }    public function rect(int $x, int $y, int $w, int $h, array $c, int $yOff = 0): void    {        $this->r->fillRect(self::BORDER_X + $x, self::BORDER_Y + $yOff + $y, $w, $h, $c);    }    public function frameRect(int $x, int $y, int $w, int $h, array $c, int $yOff = 0): void    {        $this->r->drawRect(self::BORDER_X + $x, self::BORDER_Y + $yOff + $y, $w, $h, $c);    }    private function line(int $x0, int $y0, int $x1, int $y1, array $c, int $yOff = 0): void    {        $this->r->drawLine(self::BORDER_X + $x0, self::BORDER_Y + $yOff + $y0, self::BORDER_X + $x1, self::BORDER_Y + $yOff + $y1, $c);    }    private function fillScreen(array $c, int $yOff): void    {        $this->r->fillRect(0, 0, self::FRAME_W, self::FRAME_H, $this->border);        $this->r->fillRect(self::BORDER_X, self::BORDER_Y, self::SCREEN_W, self::SCREEN_H, $c);    }    private function tile(int $id, int $x, int $y, int $set, string $levelKey, int $yOff = 0): void    {        $tex = $levelKey !== '' ? $this->tex['leveltiles'] : ($set === 2 ? $this->tex['tiles2'] : $this->tex['tiles1']);        $this->r->copy($tex, [($id % 16) * 8, (int)($id / 16) * 8, 8, 8],            self::BORDER_X + $x * 2, self::BORDER_Y + $yOff + $y);    }    private function sprite(int $frame, int $x, int $y, int $rows = 24, int $set = 1, int $yOff = 0): void    {        $rows = min($rows, 24);        if ($rows <= 0) {            return;        }        [$tex, $col, $row] = $frame >= 96            ? [$this->tex['extra'], ($frame - 96) % 12, (int)(($frame - 96) / 12)]            : [$set === 2 ? $this->tex['sprites2'] : $this->tex['sprites1'], $frame % 12, (int)($frame / 12)];        // the original clips per line: crop the source instead        $cropTop = max(0, -$y);        $cropBot = max(0, $y + $rows - Values::FIELD_H);        $srcY = $row * 24 + $cropTop;        $srcH = $rows - $cropTop - $cropBot;        if ($srcH <= 0) {            return;        }        $this->r->copy($tex, [$col * 24, $srcY, 24, $srcH],            self::BORDER_X + $x * 2, self::BORDER_Y + $yOff + $y + $cropTop);    }    /** A glyph as coloured pixels: the font atlases hold the pixels of pen 1, 2 and 3 separately. */    private function glyphCell(int $code, float $x, float $y, int $pens): void    {        $g = $code - 0x20;        if ($g < 0 || $g >= 16 * 9) {            return;        }        $sx = ($g % 16) * 8;        $sy = (int)($g / 16) * 8;        if ($pens & 1) {            $this->r->copy($this->tex['font1'], [$sx, $sy, 8, 8], self::BORDER_X + $x, self::BORDER_Y + $y, 8, 8);        }        if ($pens & 2) {            $this->r->copy($this->tex['font2'], [$sx, $sy, 8, 8], self::BORDER_X + $x, self::BORDER_Y + $y, 8, 8);        }        if ($pens & 3) {            $this->r->copy($this->tex['font3'], [$sx, $sy, 8, 8], self::BORDER_X + $x, self::BORDER_Y + $y, 8, 8);        }    }    public function textAtPixel(float $x, float $y, string $s, int $pens, int $yOff = 0): void    {        foreach (str_split($s) as $ch) {            $this->glyphCell(ord($ch), $x, $y + $yOff, $pens);            $x += 8;        }    }    public function text(int $col, int $row, string $s, int $pens, int $yOff = 0): void    {        $this->textAtPixel($col * 8, $row * 8, $s, $pens, $yOff);    }    // ---- the screens ----    public function drawPicture(int $revealed): void    {        $yOff = 20;        $this->fillScreen($this->textPal[0], $yOff);        $this->rect(0, -20, self::SCREEN_W, self::SCREEN_H, Assets::inkRgb($this->A->picturePalette[0]), $yOff);        if ($this->picRevealed < 0) {            $prev = $this->r->setTarget($this->picTarget);            $this->r->fillRect(0, 0, self::SCREEN_W, 200, Assets::inkRgb($this->A->picturePalette[0]));            $this->r->setTarget($prev);            $this->picRevealed = 0;        }        // draw the newly revealed bytes into the target: screen memory in the original order, step 0x22D        if ($revealed > $this->picRevealed) {            $pens = $this->picturePens();            $pal = [];            foreach ($this->A->picturePalette as $i) {                $pal[$i] = Assets::inkRgb($i);            }            $prev = $this->r->setTarget($this->picTarget);            $off = (int)(($this->picRevealed * 0x22D) & 0x3FFF);            for ($k = $this->picRevealed; $k < $revealed && $k < 0x4000; $k++) {                $off = ($off + 0x22D) & 0x3FFF;                $blk = $off >> 11;                $rest = $off & 0x7FF;                if ($rest >= 2000) {                    continue;                }                $y = (int)($rest / 80) * 8 + $blk;                $bx = $rest % 80;                for ($p = 0; $p < 4; $p++) {                    $c = $pal[$pens[$y * 320 + $bx * 4 + $p]];                    $this->r->fillRect($bx * 4 + $p, $y, 1, 1, $c);                }            }            $this->r->setTarget($prev);            $this->picRevealed = min($revealed, 0x4000);        }        $this->r->copy($this->picTarget, null, self::BORDER_X, self::BORDER_Y - 20);    }    private function picturePens(): array    {        if ($this->picturePens !== []) {            return $this->picturePens;        }        $im = @imagecreatefrompng($this->A->path('assets/pictures/title.png'));        if ($im === false) {            throw new \RuntimeException('bad assets/pictures/title.png');        }        $pal = [];        foreach ($this->A->picturePalette as $i) {            $c = Assets::inkRgb($i);            $pal[$c[0] << 16 | $c[1] << 8 | $c[2]] = $i;        }        $pens = [];        for ($y = 0; $y < 200; $y++) {            for ($x = 0; $x < 320; $x++) {                $c = imagecolorat($im, $x, $y);                $pens[$y * 320 + $x] = $pal[$c] ?? 0;            }        }        imagedestroy($im);        return $this->picturePens = $pens;    }    public function drawTitle(int $revealed): void    {        $yOff = 20;        $this->fillScreen($this->textPal[0], $yOff);        $this->rect(0, -20, self::SCREEN_W, self::SCREEN_H, $this->scene[0], $yOff);        $tiles = $this->titleTiles();        for ($k = 0; $k < $revealed && $k < Values::MAP_W * Values::MAP_H; $k++) {            $i = ($k * 0xAD) % (Values::MAP_W * Values::MAP_H);            if ($tiles[$i]) {                $this->tile($tiles[$i], ($i % Values::MAP_W) * Values::TILE_W, (int)($i / Values::MAP_W) * Values::TILE_H, 2, '', $yOff);            }        }    }    /** @return int[] the 720 tile ids of the title scenery */    public function titleTiles(): array    {        static $t = null;        if ($t !== null) {            return $t;        }        $bin = (string)file_get_contents($this->A->path('assets/data/TISCRN.bin'));        $t = [];        for ($i = 0; $i < Values::MAP_W * Values::MAP_H; $i++) {            $t[] = ord($bin[$i]);        }        return $t;    }    public function drawGame(Game $g, int $graphicSet, bool $withSprites): void    {        $yOff = 0;        $levelKey = $g->lvl->key === 'original' ? '' : $g->lvl->key;        if ($levelKey !== '') {            $this->setLevelTiles($levelKey);        }        $set = $g->initiation() ? 2 : $graphicSet;        $this->fillScreen($this->textPal[0], $yOff);        $this->rect(0, -20, self::SCREEN_W, self::SCREEN_H, $this->scene[0], $yOff);        // tiles: animated tiles take two cells (a pair) and cycle through their frames        $ANIM = [[0x1E, 4, 0], [0x46, 4, 1], [0x6E, 2, 2]];        for ($i = 0; $i < Values::MAP_W * Values::MAP_H; $i++) {            $id = $g->tileId($i);            $x = ($i % Values::MAP_W) * Values::TILE_W;            $y = (int)($i / Values::MAP_W) * Values::TILE_H;            if ($id === 0) {                continue;            }            $animated = false;            foreach ($ANIM as $a) {                if ($id !== $a[0] && $id !== $a[0] + 1) {                    continue;                }                $f = $g->animFrame($a[2]) % $a[1];                $this->tile($a[0] + 2 * $f, $x, $y, $set, $levelKey, $yOff);                if ($i + 1 < Values::MAP_W * Values::MAP_H && ($i % Values::MAP_W) < Values::MAP_W - 1) {                    $this->tile($a[0] + 2 * $f + 1, $x + Values::TILE_W, $y, $set, $levelKey, $yOff);                }                $i++;                $animated = true;                break;            }            if (!$animated) {                $this->tile($id, $x, $y, $set, $levelKey, $yOff);            }        }        $rs = $g->roomState();        if (!$withSprites) {            $this->hud($g, $yOff);            return;        }        if (!$g->bareScene()) {            foreach ($rs->floor as $f) {                if ($f->item !== Values::NONE) {                    $this->sprite($f->item & 0x7F, $f->x, $f->y, 24, $set, $yOff);                }            }            foreach ($rs->doors as $i => $d) {                if ($d->y !== Values::NONE && $g->doorStampVisible($i)) {                    $this->sprite($i < 2 ? 0x31 : 0x35, $d->x, $d->y, 24, $set, $yOff);                }            }            if ($g->room() === Values::ROOM_LABYRINTH) {                for ($i = 0; $i < 3; $i++) {                    $this->sprite(0x31, 0, 0x30 + 0x20 * $i, 24, $set, $yOff);                }            }            if ($rs->heart->y !== Values::NONE) {                $this->sprite(0x36, $rs->heart->x, $rs->heart->y, 24, $set, $yOff);            }            if ($rs->hazard->y !== Values::NONE) {                $this->sprite(0x36, $rs->hazard->x, $rs->hazard->y, 24, $set, $yOff);            }            if ($rs->lockA->y !== Values::NONE) {                $this->sprite(0x43, $rs->lockA->x, $rs->lockA->y, 24, $set, $yOff);            }            if ($rs->lockB->y !== Values::NONE) {                $this->sprite(0x42, $rs->lockB->x, $rs->lockB->y, 24, $set, $yOff);            }            $step = $g->animStepCount() % 4;            foreach ($rs->extras as $x) {                if ($x->type === RoomExtra::TELEPORTER) {                    $this->sprite(Assets::FRAME_TELEPORTER + $step, $x->x, $x->y, 24, $set, $yOff);                } elseif (!$x->taken) {                    $this->sprite(Assets::FRAME_LANTERN + ($step === 2 ? 1 : 0), $x->x, $x->y + ($step === 1 || $step === 2 ? 1 : 0), 24, $set, $yOff);                }            }            if ($g->animStepCount() % 2 === 0) {                foreach ([$rs->heart, $rs->hazard] as $c) {                    if ($c->y === Values::NONE) {                        continue;                    }                    for ($row = 0; $row < 4; $row++) {                        for ($px = 0; $px < 10; $px++) {                            $pen = $this->A->sparkle[$row][$px];                            if ($pen) {                                $this->put(($c->x + $px) * 2, $c->y + $row, $this->scene[$pen], $yOff);                                $this->put(($c->x + $px) * 2 + 1, $c->y + $row, $this->scene[$pen], $yOff);                            }                        }                    }                }            }        }        foreach ($g->bossIcons() as $e) {            if ($e->used()) {                $this->sprite($e->frame, $e->x, $e->y, 24, $set, $yOff);            }        }        if ($g->trVisible) {            $this->sprite($g->trFrame, $g->trX, $g->trY, 24, $set, $yOff);        }        foreach ($rs->ents as $e) {            if ($e->used() && $e->drawn) {                $this->sprite($e->frame, $e->x, $e->y, $e->rows, $set, $yOff);            }        }        foreach ($g->slots() as $e) {            if ($e->used() && $e->drawn) {                $this->sprite($e->frame, $e->x, $e->y, $e->rows, $set, $yOff);            }        }        $p = $g->player;        if ($p->visible) {            $this->sprite($p->frame, $p->x, $p->y, $p->rows, $set, $yOff);        }        foreach ($g->victorySprites() as $o) {            if ($o->visible) {                $this->sprite($o->frame, $o->x, $o->y, 24, $set, $yOff);            }        }        // spells and shots: 2x2 pixel marks        foreach ([$g->bolts(), $g->debris()] as $k => $ps) {            $c = $this->scene[$k ? 12 : 3];            foreach ($ps as $pt) {                if (!$pt->used) {                    continue;                }                $x = $pt->x & ~1;                $this->r->fillRect(self::BORDER_X + $x * 2, self::BORDER_Y + $yOff + $pt->y, 4, 2, $c);            }        }        if ($g->fieldWhite()) {            $this->rect(0, -20, self::SCREEN_W, Values::FIELD_H, [255, 255, 255], $yOff);        }        if ($g->darkEnabled()) {            $this->darken($g, $yOff);        }        $this->hud($g, $yOff);    }    private function hud(Game $g, int $yOff): void    {        $this->text(0, 20, $g->line1(), 3, $yOff);        $this->text(0, 22, $g->line2(), 3, $yOff);        if ($g->bareScene()) {            return;        }        $this->text(0, 24, 'ENERGIE.....', 3, $yOff);        $this->text(12, 24, sprintf('%02d', $g->player->energy), 2, $yOff);        $this->text(14, 24, '%', 3, $yOff);        $this->text(19, 24, 'SCORE:', 1, $yOff);        $this->text(25, 24, sprintf('%06d', $g->score), 1, $yOff);        // the strip under the status area: lantern and map of the rooms        $this->rect(0, 200, self::SCREEN_W, 40, $this->textPal[0], $yOff);        $this->frameRect(2, 202, self::SCREEN_W - 4, 36, Assets::inkRgb(13), $yOff);        $this->textAtPixel(8, 205, 'LANTERNE', 1, $yOff);        $bx = 8;        $by = 216;        $bw = 64;        $this->frameRect($bx, $by, $bw, 8, Assets::inkRgb(26), $yOff);        if ($g->darkEnabled()) {            $fill = (int)($bw * $g->lampTicks() / Values::LAMP_MAX);            $col = $g->lampTicks() < 750 ? Assets::inkRgb(6) : Assets::inkRgb(24);            $this->rect($bx + 1, $by + 1, max(0, min($bw - 2, $fill - 2)), 6, $col, $yOff);            $sec = (int)($g->lampTicks() * 2 / 75);            $this->textAtPixel(8, 227, sprintf('%d:%02d', (int)($sec / 60), $sec % 60), 3, $yOff);        } else {            $this->textAtPixel(8, 227, '-----', 2, $yOff);        }        $this->drawMapPanel($g, 84, 204, 226, 32, false, $yOff);        // the book: 8x5 glyphs; it empties pixel by pixel, line by line, from the left        $steps = min($g->barSteps(), Values::BAR_TOTAL);        $fullLines = (int)($steps / 64);        $partial = $steps % 64;        $base = $g->demo() ? 0x88 : 0x60;        for ($gr = 0; $gr < 5; $gr++) {            if ($fullLines >= $gr * 8 + 8) {                continue;                                       // the whole glyph row is gone            }            for ($gc = 0; $gc < 8; $gc++) {                $code = $base + $gr * 8 + $gc;                $gl = $code - 0x20;                if ($gl < 0 || $gl >= 16 * 9) {                    continue;                }                $sx = ($gl % 16) * 8;                $sy = (int)($gl / 16) * 8;                $dx = self::BORDER_X + 256 + $gc * 8;                $dy = self::BORDER_Y + 160 + $gr * 8 + $yOff;                $this->r->copy($this->tex['font1'], [$sx, $sy, 8, 8], $dx, $dy, 8, 8);                $this->r->copy($this->tex['font2'], [$sx, $sy, 8, 8], $dx, $dy, 8, 8);                $this->r->copy($this->tex['font3'], [$sx, $sy, 8, 8], $dx, $dy, 8, 8);            }            // the gone pixels of this glyph row: the full lines above the partial one, and the partial line from the left            if ($fullLines > $gr * 8) {                $whole = min(8, $fullLines - $gr * 8);                $this->r->fillRect(self::BORDER_X + 256, self::BORDER_Y + 160 + $gr * 8 + $yOff, 64, $whole, $this->textPal[0]);                if ($whole < 8) {                    $this->r->fillRect(self::BORDER_X + 256, self::BORDER_Y + 160 + $fullLines + $yOff, $partial, 1, $this->textPal[0]);                }            }        }    }    // ---- darkness (adventure mode) ----    private function darken(Game $g, int $yOff): void    {        // the light map at block resolution (16x8 screen px): the lantern, fires, cauldrons and spells light up        $bw = self::SCREEN_W / 16;        $bh = (int)(Values::FIELD_H / 8);        $light = array_fill(0, (int)($bw * $bh), 0.05);        $add = function (float $cx, float $cy, float $rx, float $ry, float $s) use (&$light, $bw, $bh): void {            for ($y = max(0, (int)(($cy - $ry) / 8)); $y <= min($bh - 1, (int)(($cy + $ry) / 8)); $y++) {                for ($x = max(0, (int)(($cx - $rx) / 16)); $x <= min((int)$bw - 1, (int)(($cx + $rx) / 16)); $x++) {                    $px = $x * 16 + 8.0;                    $py = $y * 8 + 4.0;                    $dx = ($px - $cx) / $rx;                    $dy = ($py - $cy) / $ry;                    $d = $dx * $dx + $dy * $dy;                    if ($d >= 1) {                        continue;                    }                    $light[$y * (int)$bw + $x] += (1 - $d) * (1 - $d) * $s;                }            }        };        $p = $g->player;        $lamp = $g->lampFactor();        $flick = $lamp > 0 && $lamp < 1 ? 0.85 + 0.15 * ((int)($this->frameCounter / 3) % 2) : 1.0;        if ($p->visible) {            $add(($p->x + 6) * 2.0, $p->y + 12.0, (float)((26 + 72 * $lamp) * $flick), (float)((22 + 52 * $lamp) * $flick), $lamp > 0 ? 1.0 : 0.8);        }        for ($i = 0; $i < Values::MAP_W * Values::MAP_H; $i++) {            if ($g->cellClass($i) === Values::C_FIRE) {                $add(($i % Values::MAP_W) * 8 + 4.0, (int)($i / Values::MAP_W) * 8 + 4.0, 24.0, 20.0, 0.9);            }        }        $rs = $g->roomState();        if ($rs->heart->y !== Values::NONE) {            $add(($rs->heart->x + 6) * 2.0, $rs->heart->y + 12.0, 34.0, 30.0, 0.8);        }        if ($rs->hazard->y !== Values::NONE) {            $add(($rs->hazard->x + 6) * 2.0, $rs->hazard->y + 12.0, 34.0, 30.0, 0.8);        }        foreach ($g->slots() as $e) {            if ($e->used() && $e->drawn && $e->kind === Values::K_MAGIC_ITEM) {                $add(($e->x + 6) * 2.0, $e->y + 12.0, 34.0, 30.0, 0.9);            }        }        foreach ($rs->extras as $x) {            if ($x->type === RoomExtra::LANTERN && !$x->taken) {                $add(($x->x + 6) * 2.0, $x->y + 12.0, 40.0, 34.0, 1.0);            }            if ($x->type === RoomExtra::TELEPORTER) {                $add(($x->x + 6) * 2.0, $x->y + 12.0, 36.0, 40.0, 0.9);            }        }        foreach ([$g->debris(), $g->bolts()] as $k => $ps) {            foreach ($ps as $pt) {                if ($pt->used) {                    $add(($pt->x & ~1) * 2.0, (float)$pt->y, $k ? 16.0 : 22.0, $k ? 12.0 : 18.0, 0.9);                }            }        }        for ($i = 0; $i < count($light); $i++) {            $light[$i] = min(1.0, $light[$i]);        }        for ($by = 0; $by < $bh; $by++) {            for ($bx = 0; $bx < $bw; $bx++) {                $k = $light[$by * (int)$bw + $bx];                if ($k >= 1.0) {                    continue;                }                $a = (int)(255 * (1 - $k));                if ($a <= 0) {                    continue;                }                $this->r->fillRect(self::BORDER_X + $bx * 16, self::BORDER_Y + $yOff + $by * 8, 16, 8, [0, 0, 0, $a]);            }        }    }    // ---- map panel ----    private function layoutOf(object $level): MapLayout    {        return $this->layouts[$level->key] ??= MapLayout::build($level);    }    /** Seen rooms are drawn full, the rooms next to them as empty boxes. */    private function drawMapPanel(Game $g, int $x0, int $y0, int $w, int $h, bool $big, int $yOff): void    {        $L = $this->layoutOf($g->lvl);        if ($L->cols === 0) {            return;        }        $n = count($L->pos);        $known = array_fill(0, $n, 0);        for ($r = 0; $r < $n; $r++) {            if (!$g->visited($r)) {                continue;            }            $known[$r] = 2;            $s = $g->savedRoom($r);            if ($s === null) {                continue;            }            foreach ($s->doors as $d) {                if ($d->y !== Values::NONE && $d->dest < $n && !$known[$d->dest]) {                    $known[$d->dest] = 1;                }            }            foreach ($s->extras as $x) {                if ($x->type === RoomExtra::TELEPORTER && $x->toRoom < $n && !$known[$x->toRoom]) {                    $known[$x->toRoom] = 1;                }            }        }        $cw = max(3, min($big ? 12 : 9, (int)($w / $L->cols)));        $chh = max(3, min($big ? 16 : 6, (int)($h / max(1, $L->rows))));        $ox = $x0 + (int)(($w - $cw * $L->cols) / 2);        $oy = $y0 + (int)(($h - $chh * $L->rows) / 2);        $center = function (int $r) use ($ox, $oy, $cw, $chh, $L): array {            return [$ox + $L->pos[$r]['col'] * $cw + (int)($cw / 2), $oy + $L->pos[$r]['row'] * $chh + (int)($chh / 2)];        };        $gray = Assets::inkRgb(13);        $white = Assets::inkRgb(26);        $gold = Assets::inkRgb(24);        $red = Assets::inkRgb(6);        $cyan = Assets::inkRgb(20);        for ($r = 0; $r < $n; $r++) {            if ($known[$r] !== 2 || $L->pos[$r]['col'] < 0) {                continue;            }            $s = $g->savedRoom($r);            if ($s === null) {                continue;            }            foreach ($s->doors as $d) {                if ($d->y === Values::NONE || $d->dest >= $n || !$known[$d->dest] || $L->pos[$d->dest]['col'] < 0) {                    continue;                }                [$ax, $ay] = $center($r);                [$bx2, $by2] = $center($d->dest);                $this->line($ax, $ay, $bx2, $by2, $gray, $yOff);            }            foreach ($s->extras as $x) {                if ($x->type !== RoomExtra::TELEPORTER || $x->toRoom >= $n || $L->pos[$x->toRoom]['col'] < 0) {                    continue;                }                [$ax, $ay] = $center($r);                [$bx2, $by2] = $center($x->toRoom);                $this->line($ax, $ay, $bx2, $by2, $cyan, $yOff);            }        }        $bw = max(2, $cw - 3);        $bhh = max(2, $chh - 3);        for ($r = 0; $r < $n; $r++) {            if (!$known[$r] || $L->pos[$r]['col'] < 0) {                continue;            }            [$cx, $cy] = $center($r);            $xx = $cx - (int)($bw / 2);            $yy = $cy - (int)($bhh / 2);            if ($r === $g->room()) {                $this->rect($xx - 1, $yy - 1, $bw + 2, $bhh + 2, ((int)($this->frameCounter / 12) % 2 === 0) ? $white : $red, $yOff);            } elseif ($known[$r] === 2) {                $this->rect($xx, $yy, $bw, $bhh, $gold, $yOff);            } else {                $this->frameRect($xx, $yy, $bw, $bhh, $gray, $yOff);            }        }    }    public function drawMapOverlay(Game $g): void    {        $this->rect(8, 6, 304, 132, [0x20, 0x10, 0xB0, 176]);        $this->frameRect(8, 6, 304, 132, Assets::inkRgb(26));        $this->textAtPixel(16, 12, 'CARTE', 1);        $this->textAtPixel(200, 12, 'SALLES ' . $g->stats->rooms, 3);        $this->drawMapPanel($g, 16, 26, 288, 104, true, 0);    }    public function clearScreen(int $pen): void    {        $this->fillScreen($this->textPal[$pen], 20);    }    public function newFrame(): void    {        $this->frameCounter++;    }    public function resetPicture(): void    {        $this->picRevealed = -1;    }}