登录
ZH
games/combat-sprite-simulator / src / Scene / PlayScene.php · commit efee358
<?phpnamespace App\Scene;use App\Entity\Hitbox;use App\Entity\EnemyProfile;use App\Geo\AddressLookup;use App\Geo\AircraftShadow;use App\Geo\MissionTerrain;use App\Geo\OrthoTileManager;use JumPHP\Renderer\Renderer;use JumPHP\Scene\AbstractScene;final class PlayScene extends AbstractScene{    private float $time = 0;    private float $scroll = 0;    private float $x = 160;    private float $y = 405;    private float $fireDelay = 0;    private float $spawnDelay = 0;    private float $bossDelay = 0;    private float $bonusDelay = 9;    private float $speedBoost = 0;    private float $rapidBoost = 0;    private float $muzzleFlash = 0;    private float $invulnerable = 0;    private float $bossX = 160;    private float $bossY = -90;    private int $lives = 3;    private int $score = 0;    private int $bossHp = 100;    private string $phase = 'wave';    private bool $paused = false;    private bool $demo = false;    private array $enemies = [];    private array $shots = [];    private array $sparks = [];    private array $bonuses = [];    private int $bonusCount = 0;    private int $spawnCount = 0;    private $hurricaneTex = null;    private $junkersTex = null;    private $bf109Tex = null;    private $ju87Tex = null;    private $groundTex = null;    private $effectsTex = null;    private ?MissionTerrain $missionTerrain = null;    private ?OrthoTileManager $missionTiles = null;    private string $missionAddress = '';    private string $geoState = 'none';    private string $geoMessage = '';    private float $geoElapsed = 0.0;    private int $geoGeneration = 0;    private $playerShadowTex = null;    private $bf109ShadowTex = null;    private $ju87ShadowTex = null;    private $bossShadowTex = null;    private $siteMissionListener = null;    public function enter(array $params = []): void    {        // A restart can call enter() without a scene exit: avoid duplicate handlers.        if ($this->siteMissionListener !== null) {            $this->engine->bridge->removeListener('mission-at-address', $this->siteMissionListener);            $this->siteMissionListener = null;        }        // Cancel outstanding geocoding and tile responses from previous missions.        ++$this->geoGeneration;        $this->missionTiles?->stop();        $this->missionTiles = null;        $this->missionTerrain = null;        $this->geoElapsed = 0;        $this->missionAddress = trim((string) ($params['address'] ?? ''));        $this->geoState = $this->missionAddress === '' ? 'none' : 'search';        $this->geoMessage = '';        $this->releaseShadows();        $this->time = $this->scroll = $this->fireDelay = $this->spawnDelay = $this->bossDelay = 0;        $this->bonusDelay = 9;        $this->speedBoost = $this->rapidBoost = $this->muzzleFlash = 0;        $this->bonuses = [];        $this->bonusCount = 0;        $this->x = 160;        $this->y = 405;        $this->bossX = 160;        $this->bossY = -90;        $this->invulnerable = 2;        $this->lives = 3;        $this->score = 0;        $this->bossHp = 100;        $this->phase = 'wave';        $this->paused = false;        $this->demo = !empty($params['demo']);        $this->enemies = $this->shots = $this->sparks = [];        if (isset($params['testBonus']) && in_array($params['testBonus'], ['speed', 'rapid'], true)) {            $this->bonuses[] = ['x' => $this->x, 'y' => $this->y, 'type' => $params['testBonus']];        }        $this->spawnCount = 0;        if (!empty($params['boss'])) {            $this->phase = 'boss';            $this->bossY = 85;            $this->bossHp = (int)($params['bossHp'] ?? 100);        }        // Flight mode can pre-load the Hurricane before the combat scene.        // Load each asset independently so the other planes are never skipped.        foreach ([            'combat-hurricane-ww2' => 'assets/sprites/hurricane-mk1-atlas.png',            'combat-junkers' => 'assets/sprites/junkers-atlas.png',            'combat-ground' => 'assets/tiles/ign-beauce-2023.png',            'combat-bf109' => 'assets/sprites/bf109-e4-atlas.png',            'combat-ju87' => 'assets/sprites/ju87-b2-atlas.png',            'combat-effects' => 'assets/sprites/effects-atlas.png',        ] as $name => $path) {            if (!$this->sdl()->hasTexture($name)) {                $this->sdl()->loadTexture($name, $path);            }        }        $this->hurricaneTex = $this->sdl()->getTexture('combat-hurricane-ww2')['texture'];        $this->junkersTex = $this->sdl()->getTexture('combat-junkers')['texture'];        $this->bf109Tex = $this->sdl()->getTexture('combat-bf109')['texture'];        $this->ju87Tex = $this->sdl()->getTexture('combat-ju87')['texture'];        $this->groundTex = $this->sdl()->getTexture('combat-ground')['texture'];        $this->effectsTex = $this->sdl()->getTexture('combat-effects')['texture'];        $this->siteMissionListener = function (array $event): void {            if ($this->scenes()->current() !== $this) {                return;            }            $address = trim((string) ($event['address'] ?? ''));            if ($address !== '') {                $this->enter(['address' => $address]);            }        };        $this->engine->bridge->on('mission-at-address', $this->siteMissionListener);        if ($this->missionAddress !== '') {            if (isset($params['latitude'], $params['longitude'])                && is_numeric($params['latitude']) && is_numeric($params['longitude'])) {                // Pre-resolved coordinates: used on restart and in deterministic tests.                $this->startAtCoordinates(                    (float) $params['latitude'], (float) $params['longitude'],                    (string) ($params['label'] ?? $this->missionAddress)                );            } else {                $generation = $this->geoGeneration;                (new AddressLookup($this->engine->http()))->search(                    $this->missionAddress,                    function (float $lat, float $lon, string $label) use ($generation): void {                        if ($generation === $this->geoGeneration) {                            $this->startAtCoordinates($lat, $lon, $label);                        }                    },                    function (string $message) use ($generation): void {                        if ($generation === $this->geoGeneration) {                            $this->geoState = 'error';                            $this->geoMessage = $message;                        }                    }                );            }        }    }    public function exit(): void    {        if ($this->siteMissionListener !== null) {            $this->engine->bridge->removeListener('mission-at-address', $this->siteMissionListener);            $this->siteMissionListener = null;        }        ++$this->geoGeneration;        $this->missionTiles?->stop();        $this->missionTiles = null;        $this->releaseShadows();    }    private function startAtCoordinates(float $lat, float $lon, string $label): void    {        if ($lat < 41.0 || $lat > 51.5 || $lon < -6.0 || $lon > 10.0) {            $this->geoState = 'error';            $this->geoMessage = 'Adresse hors de France metropolitaine';            return;        }        $this->missionTiles?->stop();        $this->missionTerrain = new MissionTerrain($lat, $lon, $label);        $this->missionTiles = new OrthoTileManager($this->engine->http(), $this->sdl());        $this->geoElapsed = 0.0;        $this->geoState = 'loading';        $this->geoMessage = 'Chargement des photographies IGN';        $this->createShadows();    }    private function createShadows(): void    {        $this->releaseShadows();        $this->playerShadowTex = AircraftShadow::create($this->sdl(),            $this->engine->config->path('assets/sprites/hurricane-mk1-atlas.png'),            512, 64, 'mission-shadow-hurricane');        $this->bf109ShadowTex = AircraftShadow::create($this->sdl(),            $this->engine->config->path('assets/sprites/bf109-e4-atlas.png'),            512, 64, 'mission-shadow-bf109');        $this->ju87ShadowTex = AircraftShadow::create($this->sdl(),            $this->engine->config->path('assets/sprites/ju87-b2-atlas.png'),            512, 64, 'mission-shadow-ju87');        $this->bossShadowTex = AircraftShadow::create($this->sdl(),            $this->engine->config->path('assets/sprites/junkers-atlas.png'),            512, 64, 'mission-shadow-boss', 0, 0, 256);    }    private function releaseShadows(): void    {        foreach (['mission-shadow-hurricane', 'mission-shadow-bf109',            'mission-shadow-ju87', 'mission-shadow-boss'] as $name) {            if ($this->sdl()->hasTexture($name)) {                $this->sdl()->releaseTexture($name);            }        }        $this->playerShadowTex = $this->bf109ShadowTex = null;        $this->ju87ShadowTex = $this->bossShadowTex = null;    }    /** Refresh tile requests while the level scrolls through the same real-world area. */    private function updateTerrainTiles(): void    {        if ($this->missionTerrain === null || $this->missionTiles === null) {            return;        }        [$cameraX, $cameraY] = $this->missionTerrain->camera($this->scroll);        $this->missionTiles->tick($cameraX, $cameraY, $this->width(),            $this->height(), $this->geoElapsed);    }    public function update(float $dt): void    {        // Reopen the same native address field while keeping combat and map modes separate.        if ($this->actions()->isPressed('search_address')) {            $this->scenes()->switchTo(AddressEntryScene::class,                ['purpose' => 'mission', 'address' => $this->missionAddress], false);            return;        }        if ($this->geoState === 'error') {            if ($this->actions()->isPressed('confirm') || $this->actions()->isPressed('back')) {                $this->scenes()->switchTo(AddressEntryScene::class,                    ['purpose' => 'mission', 'address' => $this->missionAddress], false);            }            return;        }        if ($this->geoState === 'search' || $this->geoState === 'loading') {            $this->geoElapsed += $dt;            if ($this->geoState === 'loading') {                $this->updateTerrainTiles();                if (($this->missionTiles?->readyCount() ?? 0) >= 2) {                    $this->geoState = 'ready';                    $this->geoMessage = '';                } elseif ($this->geoElapsed >= 12.0) {                    // Never pretend a cached demonstration image is the chosen address.                    // Keep the game playable with a persistent explicit warning.                    $this->geoState = 'fallback';                    $this->geoMessage = 'IGN INDISPONIBLE - F POUR CHANGER';                }            }            return; // No enemies or mission timer while locating and loading terrain.        }        if ($this->actions()->isPressed('pause')) $this->paused = !$this->paused;        if ($this->paused) return;        if ($this->phase === 'victory' || $this->phase === 'defeat') {            if ($this->actions()->isPressed('confirm') || $this->actions()->isPressed('fire')) {                $restart = $this->missionAddress === '' ? [] : ['address' => $this->missionAddress];                if ($this->missionTerrain !== null) {                    $restart['latitude'] = $this->missionTerrain->latitude;                    $restart['longitude'] = $this->missionTerrain->longitude;                    $restart['label'] = $this->missionTerrain->label;                }                $this->enter($restart);            }            return;        }        $this->time += $dt;        // 250 source pixels/s ≈ 50 m/s in metropolitan France; old demo remains unchanged.        $this->scroll += $dt * ($this->missionTerrain === null ? 75.0 : 250.0);        $this->geoElapsed += $dt;        $this->updateTerrainTiles();        if ($this->geoState === 'fallback' && ($this->missionTiles?->readyCount() ?? 0) >= 2) {            $this->geoState = 'ready';            $this->geoMessage = '';        }        $this->invulnerable = max(0, $this->invulnerable - $dt);        $this->speedBoost = max(0, $this->speedBoost - $dt);        $this->rapidBoost = max(0, $this->rapidBoost - $dt);        $this->muzzleFlash = max(0, $this->muzzleFlash - $dt);        $dx = $this->demo ? sin($this->time * 1.4) * 0.6 : $this->actions()->axis('left', 'right');        $dy = $this->demo ? 0 : $this->actions()->axis('up', 'down');        $speed = $this->speedBoost > 0 ? 255 : 175;        $this->x = max(28, min(292, $this->x + $dx * $speed * $dt));        $this->y = max(100, min(440, $this->y + $dy * $speed * $dt));        $this->fireDelay -= $dt;        if (($this->demo || $this->actions()->isHeld('fire')) && $this->fireDelay <= 0) {            // Four visible muzzle positions stand for the Hurricane's wing gun groups.            foreach ([-22, -13, 13, 22] as $offset) {                $this->shots[] = ['x' => $this->x + $offset, 'y' => $this->y - 17,                    'vy' => -400, 'friendly' => true];            }            $this->muzzleFlash = 0.07;            $this->fireDelay = $this->rapidBoost > 0 ? 0.09 : 0.19;        }        $this->bonusDelay -= $dt;        if ($this->bonusDelay <= 0 && $this->phase === 'wave') {            $this->bonuses[] = ['x' => 55 + ($this->bonusCount * 97) % 210,                'y' => -18, 'type' => $this->bonusCount % 2 === 0 ? 'speed' : 'rapid'];            $this->bonusCount++;            $this->bonusDelay = 11;        }        foreach ($this->bonuses as $i => &$bonus) {            $bonus['y'] += 88 * $dt;            if ($bonus['y'] > 500) { unset($this->bonuses[$i]); continue; }            if (Hitbox::overlap($bonus['x'] - 12, $bonus['y'] - 12, 24, 24,                $this->x - 15, $this->y - 15, 30, 30)) {                if ($bonus['type'] === 'speed') $this->speedBoost = 8;                else $this->rapidBoost = 8;                $this->score += 25;                $this->burst($bonus['x'], $bonus['y']);                unset($this->bonuses[$i]);            }        }        unset($bonus);        if ($this->phase === 'wave') {            $this->spawnDelay -= $dt;            if ($this->spawnDelay <= 0) {                $lane = ($this->spawnCount * 3 + 1) % 5;                $type = ['bf109', 'ju87', 'bf109', 'bf109', 'ju87'][$this->spawnCount % 5];                $this->enemies[] = EnemyProfile::spawn($type, 40 + $lane * 60);                $this->spawnCount++;                $this->spawnDelay = 1.15;            }            if ($this->time >= 50) {                $this->phase = 'boss';                $this->enemies = [];            }        } else {            $this->bossY = min(85, $this->bossY + 45 * $dt);            $this->bossX = 160 + sin($this->time * 0.8) * 60;            $this->bossDelay -= $dt;            if ($this->bossY >= 80 && $this->bossDelay <= 0) {                foreach ([-36, 0, 36] as $offset) $this->shots[] = ['x' => $this->bossX + $offset, 'y' => $this->bossY + 34, 'vy' => 160, 'friendly' => false];                $this->bossDelay = 1.5;            }        }        foreach ($this->enemies as $i => &$enemy) {            EnemyProfile::step($enemy, $dt);            if ($enemy['fireDelay'] <= 0 && $enemy['y'] > 24 && $enemy['y'] < 310) {                $this->shots[] = ['x' => $enemy['x'], 'y' => $enemy['y'] + $enemy['hitHeight'] / 2,                    'vy' => $enemy['type'] === 'bf109' ? 145 : 110, 'friendly' => false];                $enemy['fireDelay'] = $enemy['type'] === 'bf109' ? 1.8 : 2.8;            }            if ($enemy['y'] > 500) unset($this->enemies[$i]);            else {                [$ex, $ey, $ew, $eh] = EnemyProfile::hitbox($enemy);                if ($this->invulnerable <= 0 && Hitbox::overlap($ex, $ey, $ew, $eh, $this->x - 15, $this->y - 15, 30, 30)) $this->damage();            }        }        unset($enemy);        foreach ($this->shots as $i => &$shot) {            $shot['y'] += $shot['vy'] * $dt;            if ($shot['y'] < -10 || $shot['y'] > 490) { unset($this->shots[$i]); continue; }            if (!$shot['friendly']) {                if ($this->invulnerable <= 0 && Hitbox::overlap($shot['x'] - 3, $shot['y'] - 5, 6, 10, $this->x - 15, $this->y - 15, 30, 30)) {                    unset($this->shots[$i]);                    $this->damage();                }                continue;            }            foreach ($this->enemies as $j => &$enemy) {                [$ex, $ey, $ew, $eh] = EnemyProfile::hitbox($enemy);                if (!Hitbox::overlap($shot['x'] - 2, $shot['y'] - 7, 4, 14, $ex, $ey, $ew, $eh)) continue;                unset($this->shots[$i]);                $this->burst($shot['x'], $shot['y']);                if (--$enemy['hp'] <= 0) { $this->score += $enemy['type'] === 'bf109' ? 100 : 250; unset($this->enemies[$j]); }                break;            }            unset($enemy);            if ($this->phase === 'boss' && isset($this->shots[$i]) && $this->bossY >= 70 && Hitbox::overlap($shot['x'] - 2, $shot['y'] - 7, 4, 14, $this->bossX - 75, $this->bossY - 35, 150, 70)) {                unset($this->shots[$i]);                $this->burst($shot['x'], $shot['y']);                if (--$this->bossHp <= 0) { $this->phase = 'victory'; $this->score += 2000; $this->burst($this->bossX, $this->bossY); }            }        }        unset($shot);        foreach ($this->sparks as $i => &$spark) {            $spark['life'] -= $dt;            $spark['x'] += $spark['vx'] * $dt;            $spark['y'] += $spark['vy'] * $dt;            if ($spark['life'] <= 0) unset($this->sparks[$i]);        }        unset($spark);    }    private function damage(): void    {        $this->burst($this->x, $this->y);        $this->invulnerable = 2;        if (--$this->lives <= 0) $this->phase = 'defeat';    }    private function burst(float $x, float $y): void    {        for ($i = 0; $i < 8; $i++) {            $a = $i * 0.785;            $this->sparks[] = ['x' => $x, 'y' => $y, 'vx' => cos($a) * 60, 'vy' => sin($a) * 60, 'life' => 0.45];        }    }    /** Cast the Blender alpha silhouettes onto the orthophoto, below the planes. */    private function drawMissionShadows(Renderer $r): void    {        if ($this->missionTerrain === null) {            return;        }        // A fixed NW sunlight direction and high sun (68 degrees).        $metersPerPixel = $this->missionTerrain->metersPerPixel();        $offsetScale = 1.0 / tan(deg2rad(68)) / $metersPerPixel;        $bearing = deg2rad(135); // Shadow SE, opposite the NW sunlight.        $offset = static fn (float $altitude): array => [            sin($bearing) * $altitude * $offsetScale,            -cos($bearing) * $altitude * $offsetScale,        ];        foreach ($this->enemies as $enemy) {            $texture = $enemy['type'] === 'bf109' ? $this->bf109ShadowTex : $this->ju87ShadowTex;            if ($texture === null) {                continue;            }            [$ox, $oy] = $offset($enemy['type'] === 'bf109' ? 24.0 : 20.0);            $r->copy($texture, null, $enemy['x'] - $enemy['width'] / 2 + $ox,                $enemy['y'] - $enemy['height'] / 2 + $oy, $enemy['width'], $enemy['height']);        }        if ($this->phase === 'boss' && $this->bossShadowTex !== null) {            [$ox, $oy] = $offset(35.0);            $r->copy($this->bossShadowTex, null,                $this->bossX - 80 + $ox, $this->bossY - 40 + $oy, 160, 80);        }        if ($this->playerShadowTex !== null) {            [$ox, $oy] = $offset(15.0);            $r->copy($this->playerShadowTex, null, $this->x - 30 + $ox,                $this->y - 30 + $oy, 60, 60);        }    }    private function asciiLabel(string $text): string    {        $text = strtr($text, [            'é'=>'e', 'è'=>'e', 'ê'=>'e', 'É'=>'E', 'à'=>'a', 'À'=>'A',            'ç'=>'c', 'Ç'=>'C', 'î'=>'i', 'ô'=>'o', 'ù'=>'u', 'û'=>'u',        ]);        return strtoupper(substr(preg_replace('/[^\x20-\x7E]/', ' ', $text) ?? '', 0, 42));    }    public function render(Renderer $r): void    {        $r->clear([38, 63, 53]);        $offset = fmod($this->scroll, 960);        for ($i = -1; $i < 2; $i++) {            $y = $offset + $i * 960;            $r->copy($this->groundTex, null, 0, $y, 320, 480);            $r->copy($this->groundTex, null, 0, $y + 480, 320, 480, false, true);        }        if ($this->missionTerrain !== null && $this->missionTiles !== null) {            [$cameraX, $cameraY] = $this->missionTerrain->camera($this->scroll);            $this->missionTiles->draw($r, $cameraX, $cameraY, $this->width(), $this->height());        }        $r->fillRect(0, 0, 320, 480, [10, 28, 23, 70]);        $this->drawMissionShadows($r);        foreach ($this->enemies as $e) {            $texture = $e['type'] === 'bf109' ? $this->bf109Tex : $this->ju87Tex;            $r->copy($texture, EnemyProfile::frame($e), $e['x'] - $e['width'] / 2,                $e['y'] - $e['height'] / 2, $e['width'], $e['height']);        }        if ($this->phase === 'boss') {            $damageFrame = $this->bossHp < 50 ? 1 : 0;            $r->copy($this->junkersTex, [$damageFrame * 512, 0, 512, 256], $this->bossX - 80, $this->bossY - 40, 160, 80);        }        foreach ($this->bonuses as $bonus) {            $tile = $bonus['type'] === 'speed' ? 128 : 192;            $r->copy($this->effectsTex, [$tile, 0, 64, 64],                $bonus['x'] - 13, $bonus['y'] - 13, 26, 26);        }        foreach ($this->shots as $s) {            $r->copy($this->effectsTex, [$s['friendly'] ? 0 : 64, 0, 64, 64],                $s['x'] - 5, $s['y'] - 10, 10, 20);        }        if ($this->invulnerable <= 0 || (int)($this->time * 10) % 2 === 0) {            $bankFrame = $this->demo ? 1 : ($this->actions()->isHeld('left') ? 0 : ($this->actions()->isHeld('right') ? 2 : 1));            $propFrame = ((int)($this->time * 18)) % 3;            $r->copy($this->hurricaneTex, [$propFrame * 512, $bankFrame * 512, 512, 512], $this->x - 30, $this->y - 30, 60, 60);            if ($this->muzzleFlash > 0) {                foreach ([-22, -13, 13, 22] as $offset) {                    $r->copy($this->effectsTex, [256, 0, 64, 64],                        $this->x + $offset - 6, $this->y - 26, 12, 12);                }            }        }        foreach ($this->sparks as $s) $r->fillRect($s['x'], $s['y'], 4, 4, [255, 177, 63]);        $r->fillRect(0, 0, 320, 31, [20, 30, 34, 225]);        $this->font()->draw($r, 'LIVES ' . $this->lives . '   SCORE ' . $this->score, 7, 8, 1, [255, 248, 215]);        if ($this->speedBoost > 0 || $this->rapidBoost > 0) {            $boost = $this->speedBoost > 0 ? 'SPEED ' . (int) ceil($this->speedBoost) : '';            if ($this->rapidBoost > 0) $boost .= ($boost === '' ? '' : '  ') . 'RAPID ' . (int) ceil($this->rapidBoost);            $this->font()->draw($r, $boost, 314, 8, 1, [144, 241, 208], 'right');        }        $this->font()->draw($r, '(C) IGN - BD ORTHO', 315, 466, 1, [255, 248, 215], 'right', [0, 0, 0]);        if ($this->missionTerrain !== null) {            $r->fillRect(0, 31, 320, 18, [17, 28, 32, 217]);            $zone = $this->asciiLabel($this->missionTerrain->label);            $line = $this->geoState === 'fallback' ? 'IGN HS - F: ADRESSE'                : 'ZONE: ' . substr($zone, 0, 32);            $this->font()->draw($r, $line, 6, 36, 1, [250, 231, 185]);        }        if ($this->phase === 'boss') {            $bossBarY = $this->missionTerrain !== null ? 57 : 40;            $r->fillRect(55, $bossBarY, 210, 9, [27, 31, 31]);            $r->fillRect(57, $bossBarY + 2, 206 * $this->bossHp / 100, 5, [217, 79, 58]);            $this->font()->draw($r, 'JUNKERS G 38', 160, $bossBarY + 14, 1, [255, 248, 215], 'center');        }        if ($this->phase === 'victory' || $this->phase === 'defeat') {            $r->fillRect(20, 190, 280, 90, [18, 28, 32, 235]);            $this->font()->draw($r, $this->phase === 'victory' ? 'VICTORY' : 'MISSION FAILED', 160, 212, 2, [255, 230, 180], 'center');            $this->font()->draw($r, 'ENTER OR SPACE TO RESTART', 160, 253, 1, [220, 220, 210], 'center');        }        if ($this->geoState === 'search' || $this->geoState === 'loading'            || $this->geoState === 'error') {            $r->fillRect(16, 178, 288, 126, [11, 25, 32, 241]);            $r->drawRect(16, 178, 288, 126, [244, 210, 142]);            $title = match ($this->geoState) {                'search' => 'RECHERCHE DE L ADRESSE',                'loading' => 'CHARGEMENT PHOTOS IGN',                default => 'ADRESSE INTROUVABLE',            };            $this->font()->draw($r, $title, 160, 202, 1, [255, 234, 168], 'center');            $message = $this->asciiLabel($this->geoMessage);            $this->font()->draw($r, substr($message, 0, 36), 160, 232, 1, [230, 229, 208], 'center');            if ($this->geoState === 'error') {                $this->font()->draw($r, 'ENTREE OU F : AUTRE ADRESSE', 160, 270, 1,                    [255, 242, 201], 'center');            }        }        if ($this->paused) {            $r->fillRect(30, 206, 260, 65, [18, 28, 32, 235]);            $this->font()->draw($r, 'PAUSED', 160, 221, 2, [255, 230, 180], 'center');            $this->font()->draw($r, 'ESC TO RESUME', 160, 253, 1, [220, 220, 210], 'center');        }    }}