<?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; }}