SONIC PHP

par davidnurdin · Plates-formes
pas encore noté
▶ Jouer Sources Éditer GitHub
. / src / Stage / Stage.php · commit d114180
<?phpnamespace App\Stage;use Evenement\EventEmitter;use App\Entities\GameObjectManager;use App\Entities\Objects\Flower;use App\Entities\Player;use App\Level\Level;use App\Level\LevelManager;use App\Gfx\Gfx;use App\Audio\SoundManager;class Stage extends EventEmitter{    private int $debugMode = 0;    private ?Level $currentLevel = null ;    private array $collisionTiles = [];    private ?GameObjectManager $objectManager = null;    private ?HUD $hud = null;    public function __construct(private Camera $camera, private Gfx $sdl,private Player $player)    {        $this->camera->setScene($this);        $this->camera->stickTo($this->player);        $this->hud = new HUD($sdl);    }    public function setObjectManager(GameObjectManager $objectManager): void    {        $this->objectManager = $objectManager;    }    public function getPlayer()    {        return $this->player;    }    public function getCurrentLevel()    {         return $this->currentLevel;    }    /**     * Écoute l'événement collisionTile du CollisionSystem     */    public function listenToCollisionEvents($collisionSystem)    {        $collisionSystem->on('collisionTile', function($data) {            $this->handleCollisionTile($data);        });    }    /**     * Gère l'événement collisionTile     */    private function handleCollisionTile($data)    {        $tileKey = $data['tileX'] . ',' . $data['tileY'];                // Stocke les informations de collision pour debug        $this->collisionTiles[$tileKey] = [            'tileX' => $data['tileX'],            'tileY' => $data['tileY'],            'tileValue' => $data['tileValue'],            'timestamp' => microtime(true)        ];        // Debug: affiche les informations de collision        // echo "Scene: Collision tile détectée à ({$data['tileX']}, {$data['tileY']}) - Tile value: {$data['tileValue']}\n";                // Émet un événement pour informer d'autres composants        // $this->emit('collisionTileRendered', $data);    }    /**     * Récupère les tiles de collision pour debug     */    public function getCollisionTiles(): array    {        return $this->collisionTiles;    }    /**     * Nettoie les anciennes tiles de collision (plus de 1 seconde)     */    public function cleanupOldCollisionTiles()    {        $currentTime = microtime(true);        $this->collisionTiles = array_filter($this->collisionTiles, function($tile) use ($currentTime) {            return ($currentTime - $tile['timestamp']) < 1.0; // Garde seulement 1 seconde        });    }    public function drawScene($font, Level $level)    {        if ($this->currentLevel === null)        {            // init level            $this->currentLevel = &$level ;            // init player            // the act starts again from the checkpoint after a death            [$startX, $startY] = $this->objectManager?->getCheckpoint() ?? [$level->getCurrentPositionHeroXinTile() * 32, $level->getCurrentPositionHeroYinTile() * 32];            $this->getPlayer()->setXY($startX, $startY);            // init camera            $this->camera->startLevel($level);			$this->camera->noSmooth = true;            $this->camera->stickTo($this->player);			$this->camera->update();        }        $this->drawWorld($font, $level, $this->player);        // Labyrinth: below the water line everything uses the underwater palette        $waterLine = $this->objectManager?->getWaterLine();        if ($waterLine !== null && $this->debugMode != 1) {            $screenLine = (int)round($waterLine - $this->camera->getY());            if ($screenLine < $this->sdl->getWindow()->getHeight()) {                $this->drawUnderwater($font, $level, max(0, $screenLine));            }        }        if ($this->debugMode == 0) {            $this->maskOutsideLevel();        }        // Afficher le HUD en dernier (par-dessus tout)        if ($this->hud && $this->objectManager) {            $this->hud->draw($this->objectManager, $level);            $this->drawAirCountdown();        }    }    private function drawWorld($font, Level $level, Player $player): void    {        // Ne pas afficher les tiles normales en mode debug 1        if ($this->debugMode != 1) {            $this->drawTiles($level);        }        // Afficher le debug en premier (en arrière-plan)        if ($this->debugMode) {            $this->drawDebug($font);        }        // Afficher les game objects (enemies, monitors, etc.)        $this->drawGameObjects();        // ghost of the best run (title menu > options), behind Sonic        $this->drawGhost();        // multiplayer: the other Sonics and the flag (Versus::drawWorld)        if ($this->worldOverlay !== null) {            ($this->worldOverlay)($this->camera->getX(), $this->camera->getY());        }        // Afficher le joueur par-dessus        $this->drawPlayer($player);        // Blocs prioritaires (arbres, piliers...) devant les sprites, comme le VDP de la SMS        if ($this->debugMode != 1) {            $this->drawForeground($level);        }    }    /** Black outside of the area the original screen could show (Camera::getVisibleArea). */    private function maskOutsideLevel(): void    {        [$left, $right, $top, $bottom] = $this->camera->getVisibleArea();        $w = $this->sdl->getWindow()->getWidth();        $h = $this->sdl->getWindow()->getHeight();        $x0 = (int)round($left - $this->camera->getX());        $x1 = (int)round($right - $this->camera->getX());        $y0 = (int)round($top - $this->camera->getY());        $y1 = (int)round($bottom - $this->camera->getY());        $renderer = $this->sdl->getRenderer()->getRenderer();        \SDL_SetRenderDrawColor($renderer, 0, 0, 0, 255);        foreach ([[0, 0, $x0, $h], [$x1, 0, $w - $x1, $h], [0, 0, $w, $y0], [0, $y1, $w, $h - $y1]] as [$x, $y, $rw, $rh]) {            if ($rw <= 0 || $rh <= 0) continue;            $rect = new \SDL_Rect;            $rect->x = $x;            $rect->y = $y;            $rect->w = $rw;            $rect->h = $rh;            \SDL_RenderFillRect($renderer, $rect);        }    }    /** @var resource|null render target of the underwater pass */    private $waterTexture = null;    private bool $underwaterPass = false;    private int $lastLightningPhase = 0;    private array $waterSize = [0, 0];    private const WATER_WAVE = [152, 56, 16, 7];    // _8e16: X of the surface sprites, 4 of them per frame    private const WATER_WAVE_X = [0x00, 0x40, 0x80, 0xC0, 0x10, 0x50, 0x90, 0xD0, 0x20, 0x60, 0xA0, 0xE0, 0x30, 0x70, 0xB0, 0xF0];    /**     * The original reloads the palette with S1_UnderwaterPalette on a line interrupt at the water line:     * the world is drawn a second time with the "-water" textures (tools/waterpalette.py) into another     * target and only the part below the line is copied over. The surface sprites follow (object $40).     */    private function drawUnderwater($font, Level $level, int $screenLine): void    {        $renderer = $this->sdl->getRenderer()->getRenderer();        $winW = $this->sdl->getWindow()->getWidth();        $winH = $this->sdl->getWindow()->getHeight();        if ($this->waterTexture !== null && $this->waterSize !== [$winW, $winH]) {            \SDL_DestroyTexture($this->waterTexture);   // the view size changed (zoom)            $this->waterTexture = null;        }        $this->waterTexture ??= \SDL_CreateTexture($renderer, \SDL_PIXELFORMAT_RGBA8888, \SDL_TEXTUREACCESS_TARGET, $winW, $winH);        $this->waterSize = [$winW, $winH];        \SDL_SetRenderTarget($renderer, $this->waterTexture);        $this->sdl->getRenderer()->clear();        $this->sdl->setTextureVariant('-water');        $this->underwaterPass = true;        $this->drawWorld($font, $level, $this->player);        $this->underwaterPass = false;        $this->sdl->setTextureVariant('');        \SDL_SetRenderTarget($renderer, $this->sdl->getRenderer()->getRenderTexture());        $rect = new \SDL_Rect;        $rect->x = 0;        $rect->y = $screenLine;        $rect->w = $winW;        $rect->h = $winH - $screenLine;        \SDL_RenderCopy($renderer, $this->waterTexture, $rect, $rect);        // surface sprites, scrolling 1px per frame over a 16px period        $frames = $this->objectManager->getWaterFrames();        $y = (int)round($this->objectManager->getWaterLine() - $this->camera->getY()) - 3;        if ($y < -8) return;        $texture = $this->sdl->getTextures('objects')[0]['texture'];        $src = new \SDL_Rect;        [$src->x, $src->y, $src->w, $src->h] = self::WATER_WAVE;        for ($base = 0; $base < $winW; $base += 256) {            foreach (self::WATER_WAVE_X as $x) {                $dst = new \SDL_Rect;                $dst->x = $base + $x + ($frames & 0x0F);                $dst->y = $y;                $dst->w = $src->w;                $dst->h = $src->h;                \SDL_RenderCopy($renderer, $texture, $src, $dst);            }        }    }    /** Drowning countdown over Sonic (_5009: Sonic X + 8, Y - 20, shown every other frame). */    private function drawAirCountdown(): void    {        $digit = $this->objectManager->getAirCountdown();        if ($digit === null || ((int)(fmod(microtime(true), 86400.0) * 30) & 1)) return;        $src = new \SDL_Rect;        [$src->x, $src->y, $src->w, $src->h] = [544 + 8 * $digit, 107, 8, 13];        $dst = new \SDL_Rect;        $dst->x = (int)round($this->player->getX() - $this->camera->getX() + 8);        $dst->y = (int)round($this->player->getY() - $this->camera->getY() - 20);        $dst->w = 8;        $dst->h = 13;        \SDL_RenderCopy($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures('objects')[0]['texture'], $src, $dst);    }    /** Drawn between the objects and Sonic: fn(float $cameraX, float $cameraY) (multiplayer). */    public ?\Closure $worldOverlay = null;    /** Sonic blinks (multiplayer: hit or put back by the match). */    public bool $playerBlink = false;    public function drawPlayer(Player $player)    {        if ($this->objectManager && $this->objectManager->isPlayerHidden()) return;        if (($this->objectManager && $this->objectManager->isPlayerHurt()) || $this->playerBlink) {            $blink = (int)(fmod(microtime(true), 86400.0) * 10) % 2;            if ($blink === 0) return;        }        $destRect = new \SDL_Rect;        // the frames of the sheet are 32px wide with Sonic's 24px sprite 4px in: the sprite is drawn over the        // 24px box of the original (the collision rect), so that he touches the walls without entering them        $destRect->x = $player->getX() - 4 - $this->camera->getX();        $destRect->y = $player->getY() - $this->camera->getY() ; // + 16;        $destRect->w = 32;        $destRect->h = 32;        // thrown up by a spring / a balance (animation $13): frame $26 of the ROM art; water slide (animation $11)        $spring = $player->getSpringFrame();        $slide = $player->getSlideFrame();        if ($slide !== null && !($this->objectManager && $this->objectManager->hasShield() && (int)floor(fmod(microtime(true), 86400.0) * 60) % 2 === 0)) {            $src = new \SDL_Rect;            [$src->x, $src->y, $src->w, $src->h] = \App\Entities\Objects\RomSprites::HERO_SLIDE[$slide];            $dst = new \SDL_Rect;            [$dst->x, $dst->y, $dst->w, $dst->h] = [$destRect->x + 4, $destRect->y, 24, 32];            \SDL_RenderCopyEx($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures($player->getRomSheet())[0]['texture'],                $src, $dst, 0, null, $player->getFacing() === 'left' ? \SDL_FLIP_HORIZONTAL : \SDL_FLIP_NONE);            if ($this->objectManager && $this->objectManager->hasInvincibility()) {                self::drawInvincibilityStars($this->sdl, $destRect->x + 16, $destRect->y + 16);            }            return;        }        if ($spring !== null && !($this->objectManager && $this->objectManager->hasShield() && (int)floor(fmod(microtime(true), 86400.0) * 60) % 2 === 0)) {            $src = new \SDL_Rect;            [$src->x, $src->y, $src->w, $src->h] = \App\Entities\Objects\RomSprites::HERO_SPRING;            $dst = new \SDL_Rect;            [$dst->x, $dst->y, $dst->w, $dst->h] = [$destRect->x + 4, $destRect->y, 24, 32];            \SDL_RenderCopyEx($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures($player->getRomSheet())[0]['texture'],                $src, $dst, 0, null, $player->getFacing() === 'left' ? \SDL_FLIP_HORIZONTAL : \SDL_FLIP_NONE);            if ($this->objectManager && $this->objectManager->hasInvincibility()) {                self::drawInvincibilityStars($this->sdl, $destRect->x + 16, $destRect->y + 16);            }            return;        }        // waiting (animation $0D): frames of the ROM art        $wait = $player->getWaitFrame();        if ($wait !== null && !($this->objectManager && $this->objectManager->hasShield() && (int)floor(fmod(microtime(true), 86400.0) * 60) % 2 === 0)) {            $src = new \SDL_Rect;            [$src->x, $src->y, $src->w, $src->h] = \App\Entities\Objects\RomSprites::HERO_WAIT[$wait];            $dst = new \SDL_Rect;            [$dst->x, $dst->y, $dst->w, $dst->h] = [$destRect->x + 4, $destRect->y, 24, 32];            \SDL_RenderCopyEx($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures($player->getRomSheet())[0]['texture'],                $src, $dst, 0, null, $player->getFacing() === 'left' ? \SDL_FLIP_HORIZONTAL : \SDL_FLIP_NONE);            return;        }        // shield (_5206): every other frame Sonic's sprite is replaced by the ring of frame $18        if ($this->objectManager && $this->objectManager->hasShield() && (int)floor(fmod(microtime(true), 86400.0) * 60) % 2 === 0) {            $src = new \SDL_Rect;            [$src->x, $src->y, $src->w, $src->h] = \App\Entities\Objects\RomSprites::SHIELD;            $dst = new \SDL_Rect;            [$dst->x, $dst->y, $dst->w, $dst->h] = [$destRect->x + 4, $destRect->y, 24, 32];            \SDL_RenderCopy($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures(\App\Entities\Objects\RomSprites::SHEET)[0]['texture'], $src, $dst);        } else {            $player->draw($destRect);        }        if ($this->objectManager && $this->objectManager->hasInvincibility()) {            self::drawInvincibilityStars($this->sdl, $destRect->x + 16, $destRect->y + 16);        }    }    /** Four stars turning around ($centerX, $centerY) (view pixels); also the other Sonics of a match (Versus). */    public static function drawInvincibilityStars(Gfx $sdl, int $centerX, int $centerY): void    {        $t = microtime(true);        $src = new \SDL_Rect;        $src->x = ((int)($t * 12) % 2) ? 560 : 608;        $src->y = 72;        $src->w = 16;        $src->h = 16;        for ($i = 0; $i < 4; $i++) {            $angle = $t * 5 + $i * M_PI / 2;            $dst = new \SDL_Rect;            $dst->x = (int)round($centerX + cos($angle) * 18 - 8);            $dst->y = (int)round($centerY + sin($angle) * 18 - 8);            $dst->w = 16;            $dst->h = 16;            \SDL_RenderCopy($sdl->getRenderer()->getRenderer(), $sdl->getTextures('objects')[0]['texture'], $src, $dst);        }    }    public function getCamera()    {        return $this->camera;    }    /** Blocks kept around the view in each cache (on every side). */    private const TILE_CACHE_MARGIN = 3;    private const TILE_CACHES = 8;    /**     * Render targets holding the blocks around the view, one per tileset texture (palette cycling step,     * underwater...): [texture, w, h, level, version, col, row, cols, rows, rings, foreground, animated]. Drawing ~400     * blocks costs ~5 ms of PHP per frame in the browser: they are drawn again only when the view leaves the     * cached area or a block changes, the frame copies one texture.     */    private array $tileCaches = [];    public function drawTiles(Level $level)    {        $tileSize = 32;        $cameraX = $this->camera->getX();        $cameraY = $this->camera->getY();        $winW = $this->sdl->getWindow()->getWidth();        $winH = $this->sdl->getWindow()->getHeight();        $name = $this->currentTilesetTextureName($level);        $key = $name . ($this->underwaterPass ? '-water' : '');        // blocks of the view (one more for the part of a block on each edge)        $col0 = (int)floor($cameraX / $tileSize);        $row0 = (int)floor($cameraY / $tileSize);        $viewCols = intdiv($winW, $tileSize) + 2;        $viewRows = intdiv($winH, $tileSize) + 2;        $cols = $viewCols + 2 * self::TILE_CACHE_MARGIN;        $rows = $viewRows + 2 * self::TILE_CACHE_MARGIN;        $cache = $this->tileCaches[$key] ?? null;        if ($cache === null || $cache[1] !== $cols * $tileSize || $cache[2] !== $rows * $tileSize || $cache[3] !== $level            || $cache[4] !== $level->getVersion()            || $col0 < $cache[5] || $col0 + $viewCols > $cache[5] + $cols || $row0 < $cache[6] || $row0 + $viewRows > $cache[6] + $rows) {            $cache = $this->renderTileCache($level, $name, $key, $col0 - self::TILE_CACHE_MARGIN, $row0 - self::TILE_CACHE_MARGIN, $cols, $rows, $cache);        } else {            // most recently used last (the oldest one is dropped first)            unset($this->tileCaches[$key]);            $this->tileCaches[$key] = $cache;        }        $renderer = $this->sdl->getRenderer()->getRenderer();        $originX = $cache[5] * $tileSize - $cameraX;        $originY = $cache[6] * $tileSize - $cameraY;        $dst = new \SDL_Rect;        $dst->x = (int)floor($originX);        $dst->y = (int)floor($originY);        $dst->w = $cache[1];        $dst->h = $cache[2];        \SDL_RenderCopy($renderer, $cache[0], null, $dst);        // rings (their blocks may have changed since) and priority blocks of the view        $ringOverlays = [];        foreach ($cache[9] as [$x, $y]) {            if ($x < $col0 || $x >= $col0 + $viewCols || $y < $row0 || $y >= $row0 + $viewRows) continue;            $block = $level->getTile($x, $y);            if ($block !== null && GameObjectManager::isRingBlock($block)) {                $ringOverlays[] = [$x * $tileSize - $cameraX, $y * $tileSize - $cameraY, $block];            }        }        $this->foregroundBlocks = [];        foreach ($cache[10] as [$x, $y, $block]) {            if ($x < $col0 || $x >= $col0 + $viewCols || $y < $row0 || $y >= $row0 + $viewRows) continue;            $this->foregroundBlocks[] = [$x * $tileSize - $cameraX, $y * $tileSize - $cameraY, $block];        }        $this->drawRingOverlays($ringOverlays);        // animated blocks (levels of the site): left out of the cache, the block of the moment drawn each frame        if ($cache[11]) {            $tileSet = $level->getTileSet();            $textures = $this->sdl->getTextures($name);            $frame = self::frame();            foreach ($cache[11] as [$x, $y, $block]) {                if ($x < $col0 || $x >= $col0 + $viewCols || $y < $row0 || $y >= $row0 + $viewRows) continue;                $shown = $level->animatedBlock($block, $frame);                $src = $tileSet->getTile($shown);                $texture = $textures[$tileSet->getTextureIndex($shown)]['texture'] ?? null;                if (!$src || !$texture) continue;                $dst = new \SDL_Rect;                $dst->x = (int)floor($x * $tileSize - $cameraX);                $dst->y = (int)floor($y * $tileSize - $cameraY);                $dst->w = $tileSize;                $dst->h = $tileSize;                \SDL_RenderCopy($renderer, $texture, $src, $dst);            }        }    }    /** The frame of the moment (60Hz), the same for every player. */    private static function frame(): int    {        return (int)floor(fmod(microtime(true), 86400.0) * 60);    }    /** Draws the blocks from ($col, $row) into the cache $key (made, or remade at a new size). */    private function renderTileCache(Level $level, string $name, string $key, int $col, int $row, int $cols, int $rows, ?array $old): array    {        $tileSize = 32;        $renderer = $this->sdl->getRenderer()->getRenderer();        $w = $cols * $tileSize;        $h = $rows * $tileSize;        $texture = $old[0] ?? null;        if ($texture !== null && ($old[1] !== $w || $old[2] !== $h)) {            \SDL_DestroyTexture($texture);   // the view size changed (zoom)            $texture = null;        }        unset($this->tileCaches[$key]);        if ($texture === null && count($this->tileCaches) >= self::TILE_CACHES) {            $oldest = array_key_first($this->tileCaches);            \SDL_DestroyTexture($this->tileCaches[$oldest][0]);            unset($this->tileCaches[$oldest]);        }        $texture ??= \SDL_CreateTexture($renderer, \SDL_PIXELFORMAT_RGBA8888, \SDL_TEXTUREACCESS_TARGET, $w, $h);        \SDL_SetRenderTarget($renderer, $texture);        \SDL_SetRenderDrawColor($renderer, 0, 0, 0, 0);        \SDL_RenderClear($renderer);        \SDL_SetRenderDrawColor($renderer, 0, 0, 0, 255);        $tileSet = $level->getTileSet();        $textures = $this->sdl->getTextures($name);        $rings = [];        $foreground = [];        $animated = [];        $dst = new \SDL_Rect;        $dst->w = $tileSize;        $dst->h = $tileSize;        for ($y = max(0, $row); $y < min($level->getMapHeight(), $row + $rows); $y++) {            for ($x = max(0, $col); $x < min($level->getMapWidth(), $col + $cols); $x++) {                $tileValue = $level->getTile($x, $y);                if ($tileValue === null) continue;                $drawn = Level::drawnBlock($tileValue);                // ring blocks (and empty ones, rings can come back): the rings are drawn over them each frame                if ($drawn === GameObjectManager::RING_BLOCK_EMPTY) {                    $rings[] = [$x, $y];                } elseif ($level->isForegroundBlock($tileValue)) {                    $foreground[] = [$x, $y, $tileValue];                }                if ($level->isAnimatedBlock($drawn)) {                    $animated[] = [$x, $y, $drawn];                    continue;                }                $tileRect = $tileSet->getTile($drawn);                if (!$tileRect) continue;                $dst->x = ($x - $col) * $tileSize;                $dst->y = ($y - $row) * $tileSize;                \SDL_RenderCopy($renderer, $textures[$tileSet->getTextureIndex($drawn)]['texture'], $tileRect, $dst);            }        }        // back to the target being drawn (the frame, or the underwater pass)        \SDL_SetRenderTarget($renderer, $this->underwaterPass ? $this->waterTexture : $this->sdl->getRenderer()->getRenderTexture());        return $this->tileCaches[$key] = [$texture, $w, $h, $level, $level->getVersion(), $col, $row, $cols, $rows, $rings, $foreground, $animated];    }    // Ring rotation frames (tileset-objects.png), 16x16 cells centred on each frame; the original shows each for 10 frames at 60Hz.    private const RING_FRAMES = [[536, 72], [520, 72], [504, 72], [488, 72], [472, 72], [456, 72]];    private function drawRingOverlays(array $overlays): void    {        if (empty($overlays)) return;        $texture = $this->sdl->getTextures('objects')[0]['texture'];        $frame = self::RING_FRAMES[(int)floor(fmod(microtime(true), 86400.0) * 6) % 6];        $src = new \SDL_Rect;        $src->x = $frame[0];        $src->y = $frame[1];        $src->w = 16;        $src->h = 16;        $renderer = $this->sdl->getRenderer()->getRenderer();        foreach ($overlays as [$sx, $sy, $block]) {            foreach ([1 => 0, 2 => 16] as $bit => $offsetX) {                if (!($block & $bit)) continue;                $dst = new \SDL_Rect;                $dst->x = $sx + $offsetX;                $dst->y = $sy + 8;                $dst->w = 16;                $dst->h = 16;                \SDL_RenderCopy($renderer, $texture, $src, $dst);            }        }    }    /** Blocks collected by drawTiles() whose solidity has the priority bit: redrawn over the sprites. */    private array $foregroundBlocks = [];    private function drawForeground(Level $level): void    {        if (empty($this->foregroundBlocks)) return;        $name = $this->currentTilesetTextureName($level) . '-fg';        if (!$this->sdl->hasTexture($name)) {            $name = 'tileset' . $level->getLevel() . '-fg';            if (!$this->sdl->hasTexture($name)) return;        }        $textures = $this->sdl->getTextures($name);        $tileSet = $level->getTileSet();        $renderer = $this->sdl->getRenderer()->getRenderer();        $frame = self::frame();        foreach ($this->foregroundBlocks as [$sx, $sy, $block]) {            $block = $level->animatedBlock($block, $frame);            $src = $tileSet->getTile($block);            $texture = $textures[$tileSet->getTextureIndex($block)]['texture'] ?? null;            if (!$src || !$texture) continue;            $dst = new \SDL_Rect;            $dst->x = $sx;            $dst->y = $sy;            $dst->w = 32;            $dst->h = 32;            \SDL_RenderCopy($renderer, $texture, $src, $dst);        }    }    /** Colour cycling like the original: step every CS frames (60Hz) through CC palettes. */    private function currentTilesetTextureName(Level $level): string    {        $base = 'tileset' . $level->getLevel();        // no palette cycling under water, the underwater palette is fixed        if ($this->underwaterPass && $this->sdl->hasTexture($base . '-water')) {            return $base;        }        if ($this->objectManager && $this->objectManager->isPaletteFlashWhite() && $this->sdl->hasTexture($base . '-flash')) {            return $base . '-flash';        }        $count = $level->getMeta('CC');        $speed = max(1, $level->getMeta('CS'));        // TL bit 1, Sky Base 1 lightning (_1f49): 171-frame loop, flash palettes from frame 100 and 130        // (thunder at 130), every palette cycling 4 steps at speed 2        if (($level->getMeta('TL') & 0x02) && $this->sdl->hasTexture($base . '-light1-cycle0')) {            $frame = (int)floor(fmod(microtime(true), 86400.0) * 60);            $phase = $frame % 171;            $light = $phase >= 130 ? 2 : ($phase >= 100 ? 1 : 0);            if ($light === 2 && $this->lastLightningPhase < 130) {                $this->emit('sfx', [SoundManager::SFX_ELECTRIC]);            }            $this->lastLightningPhase = $phase;            $step = intdiv($frame, 2) % 4;            return $light ? $base . '-light' . $light . '-cycle' . $step : $base . '-cycle' . $step;        }        if ($count <= 1 || !$this->sdl->hasTexture($base . '-cycle0')) {            return $base;        }        $step = (int)floor(fmod(microtime(true), 86400.0) * 60 / $speed) % $count;        return $base . '-cycle' . $step;    }    /** Ghost frame to draw this frame: [x, y, sx, sy, sw, sh, flip] (GhostRecorder), or null. */    private ?array $ghostFrame = null;    public function setGhostFrame(?array $frame): void    {        $this->ghostFrame = $frame;    }    private function drawGhost(): void    {        if ($this->ghostFrame === null) return;        [$x, $y, $sx, $sy, $sw, $sh, $flip] = $this->ghostFrame;        $src = new \SDL_Rect;        [$src->x, $src->y, $src->w, $src->h] = [$sx, $sy, $sw, $sh];        $dst = new \SDL_Rect;        // same place as Sonic's sprite (drawPlayer): 4px left of his 24px box, 32x32        [$dst->x, $dst->y, $dst->w, $dst->h] = [(int)round($x - 4 - $this->camera->getX()), (int)round($y - $this->camera->getY()), 32, 32];        \SDL_RenderCopyEx($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures('hero-ghost')[0]['texture'],            $src, $dst, 0, null, $flip ? \SDL_FLIP_HORIZONTAL : \SDL_FLIP_NONE);    }    public function setDebugMode(int $debugMode)    {        $this->debugMode = $debugMode;    }    private function drawDebug($fontTab)    {        $this->drawDebugTiles($fontTab, $this->debugMode);    }    private function drawDebugTiles($fontTab, $debugType = 1)    {        if (!$this->currentLevel) {            return;        }        $level = $this->currentLevel;        $cameraX = $this->camera->getX();        $cameraY = $this->camera->getY();        $tileSize = 32;        $winW = $this->sdl->getWindow()->getWidth();        $winH = $this->sdl->getWindow()->getHeight();        $startCol = (int)floor($cameraX / $tileSize);        $endCol = (int)ceil(($winW/$tileSize) + ($cameraX/$tileSize));        $offsetX = -(int)($cameraX % $tileSize);        $startRow = (int)floor($cameraY / $tileSize);        $endRow = (int)ceil(($winH/$tileSize) + ($cameraY/$tileSize));        $offsetY = -(int)($cameraY % $tileSize);        $startRow = max(0, $startRow);        $endRow = min($level->getMapHeight(), $endRow);        $startCol = max(0, $startCol);        $endCol = min($level->getMapWidth(), $endCol);        $previewMore = 2;        // Définir la couleur jaune pour les borders        \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), 255, 255, 0, 255);        // Afficher les valeurs des tiles        for ($y = $startRow; $y < $endRow + $previewMore; $y++) {            for ($x = $startCol; $x < $endCol + $previewMore; $x++) {                $tileValue = $level->getTile($x, $y);                if ($tileValue === null) {                    continue;                }                // Calculer l'index de la tile                $tileIndex = $x + $y * $level->getMapWidth();                // Choisir la valeur à afficher selon le type de debug                if ($debugType == 1) {                    // Mode 1 : Valeurs des tiles                    $char = strtoupper(dechex($tileValue));                    if (strlen($char) == 1) {                        $char = '0' . $char;                    }                }                elseif ($debugType == 4)                {                    // Mode 4 : Coordonnées logiques des tiles (X/Y)                    // Afficher X et Y séparément, en limitant à un chiffre chacun pour la lisibilité                    $charX = substr((string)$x, -1); // Dernier chiffre de X                    $charY = substr((string)$y, -1); // Dernier chiffre de Y                    $char = $charX . $charY;                }                else {                                        // Mode 2 : Valeurs de collision                    $solidity = $level->getSolidity();                    if (isset($solidity[$tileValue])) {                        $colisionIndexTile = ord($solidity[$tileValue]);                                                $char = strtoupper(dechex($colisionIndexTile));                        if (strlen($char) == 1) {                            $char = '0' . $char;                        }                    } else {                        $char = '00'; // Pas de collision                    }                }                $dstRect = new \SDL_Rect;                $dstRect->x = ($x - $startCol) * $tileSize + $offsetX;                $dstRect->y = ($y - $startRow) * $tileSize + $offsetY;                $dstRect->w = $tileSize;                $dstRect->h = $tileSize;                // Dessiner le border jaune autour de la tile (seulement le contour)                $x1 = $dstRect->x;                $y1 = $dstRect->y;                $x2 = $dstRect->x + $dstRect->w - 1;                $y2 = $dstRect->y + $dstRect->h - 1;                                // Rectangle jaune plein (border)                $borderRect = new \SDL_Rect;                $borderRect->x = $x1;                $borderRect->y = $y1;                $borderRect->w = $dstRect->w;                $borderRect->h = $dstRect->h;                \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), 255, 255, 0, 255); // Jaune                \SDL_RenderFillRect($this->sdl->getRenderer()->getRenderer(), $borderRect);                                // Rectangle noir plus petit à l'intérieur                $innerRect = new \SDL_Rect;                $innerRect->x = $x1 + 2;                $innerRect->y = $y1 + 2;                $innerRect->w = $dstRect->w - 4;                $innerRect->h = $dstRect->h - 4;                \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), 0, 0, 0, 255); // Noir                \SDL_RenderFillRect($this->sdl->getRenderer()->getRenderer(), $innerRect);                                // Remettre la couleur jaune pour le texte                \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), 255, 255, 0, 255);                // Afficher les 2 caractères hexadécimaux séparément                if (isset($fontTab[substr($char, 0, 1)]) && isset($fontTab[substr($char, 1, 1)])) {                    // Premier caractère (gauche)                    $dstRectFont = new \SDL_Rect;                    $dstRectFont->x = $dstRect->x + 4;                    $dstRectFont->y = $dstRect->y + 4;                    $dstRectFont->w = 12;                    $dstRectFont->h = 24;                                        \SDL_RenderCopyEx(                        $this->sdl->getRenderer()->getRenderer(),                        $fontTab[substr($char, 0, 1)],                        new \SDL_Rect(0, 0, 32, 32),                        $dstRectFont,                        0,                        null,                        \SDL_FLIP_NONE                    );                                        // Deuxième caractère (droite)                    $dstRectFont2 = new \SDL_Rect;                    $dstRectFont2->x = $dstRect->x + 16;                    $dstRectFont2->y = $dstRect->y + 4;                    $dstRectFont2->w = 12;                    $dstRectFont2->h = 24;                                        \SDL_RenderCopyEx(                        $this->sdl->getRenderer()->getRenderer(),                        $fontTab[substr($char, 1, 1)],                        new \SDL_Rect(0, 0, 32, 32),                        $dstRectFont2,                        0,                        null,                        \SDL_FLIP_NONE                    );                }                                // En mode debug 2, afficher les zones de collision                if ($debugType == 3) {                    if ($colision = $level->getTileColisionAt($x, $y))                    {                        \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), 0, 255, 255, 128); // Semi-transparent                                                    // Calculer les zones de collision et les remplir                        $this->fillCollisionZones($level->getTileColisionAt($x, $y) , $dstRect->x, $dstRect->y);                                                // $this->drawCollisionPixelRand($colision, $dstRect->x, $dstRect->y);                        // Remettre la couleur jaune pour les borders                        \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), 255, 255, 0, 255);                    }                }                            }        }        // En mode debug 2, dessiner les rectangles multicolors autour des tiles en collision        if ($debugType == 2) {            $this->drawCollisionTilesMulticolor();        }    }    /**     * Dessine des rectangles multicolors autour des tiles en collision     */    private function drawCollisionTilesMulticolor()    {        $cameraX = $this->camera->getX();        $cameraY = $this->camera->getY();        $tileSize = 32;        foreach ($this->collisionTiles as $tileKey => $tileData) {            $tileX = $tileData['tileX'];            $tileY = $tileData['tileY'];                        // Calculer la position à l'écran            $screenX = $tileX * $tileSize - $cameraX;            $screenY = $tileY * $tileSize - $cameraY;                        // Vérifier si la tile est visible à l'écran            if ($screenX >= -$tileSize && $screenX <= $this->sdl->getWindow()->getWidth() &&                $screenY >= -$tileSize && $screenY <= $this->sdl->getWindow()->getHeight()) {                                // Créer un rectangle multicolor (arc-en-ciel)                $this->drawRainbowBorder($screenX, $screenY, $tileSize);            }        }    }    /**     * Dessine une bordure arc-en-ciel autour d'une tile     */    private function drawRainbowBorder($x, $y, $size)    {        $colors = [            [255, 0, 0],    // Rouge            [255, 127, 0],  // Orange            [255, 255, 0],  // Jaune            [0, 255, 0],    // Vert            [0, 0, 255],    // Bleu            [75, 0, 130],   // Indigo            [148, 0, 211]   // Violet        ];                        shuffle($colors);                $colorIndex = 0;        $borderWidth = 2;                // Dessiner les 4 côtés avec des couleurs différentes        for ($i = 0; $i < 4; $i++) {            $color = $colors[$colorIndex % count($colors)];            \SDL_SetRenderDrawColor($this->sdl->getRenderer()->getRenderer(), $color[0], $color[1], $color[2], 255);                        $rect = new \SDL_Rect;                        switch ($i) {                case 0: // Haut                    $rect->x = $x;                    $rect->y = $y;                    $rect->w = $size;                    $rect->h = $borderWidth;                    break;                case 1: // Droite                    $rect->x = $x + $size - $borderWidth;                    $rect->y = $y;                    $rect->w = $borderWidth;                    $rect->h = $size;                    break;                case 2: // Bas                    $rect->x = $x;                    $rect->y = $y + $size - $borderWidth;                    $rect->w = $size;                    $rect->h = $borderWidth;                    break;                case 3: // Gauche                    $rect->x = $x;                    $rect->y = $y;                    $rect->w = $borderWidth;                    $rect->h = $size;                    break;            }                        \SDL_RenderFillRect($this->sdl->getRenderer()->getRenderer(), $rect);            $colorIndex++;        }    }    private function drawCollisionPixelRand($tileColisionData, $tileX, $tileY)    {        // Parcourir les 32x32 pixels de la tile        for ($y = 0; $y < 32; $y++) {            for ($x = 0; $x < 32; $x++) {                // Vérifier si rand(1,10) == 5 (10% de chance)                if (rand(1, 20) == 5) {                    // Vérifier si le pixel est dans une zone de collision                    if (isset($tileColisionData[$y][$x]) && $tileColisionData[$y][$x] == 1) {                        // Dessiner un pixel à cette position                        $pixelX = $tileX + $x;                        $pixelY = $tileY + $y;                                                // Utiliser SDL_RenderDrawPoint pour dessiner un pixel                        \SDL_RenderDrawPoint(                            $this->sdl->getRenderer()->getRenderer(),                            $pixelX,                            $pixelY                        );                    }                }            }        }    }    private function fillCollisionZones($tileColisionData, $tileX, $tileY)    {        // Algorithme simple : détecter les lignes horizontales de collision        for ($y = 0; $y < 32; $y++) {            $startX = -1;            $endX = -1;                        // Trouver le début et la fin de chaque ligne de collision            for ($x = 0; $x < 32; $x++) {                if (isset($tileColisionData[$y][$x]) && $tileColisionData[$y][$x] == 1) {                    if ($startX == -1) {                        $startX = $x;                    }                    $endX = $x;                } else {                    // Si on trouve un pixel sans collision, dessiner le rectangle précédent                    if ($startX != -1 && $endX != -1) {                        $rect = new \SDL_Rect;                        $rect->x = $tileX + $startX;                        $rect->y = $tileY + $y;                        $rect->w = $endX - $startX + 1;                        $rect->h = 1;                        \SDL_RenderFillRect($this->sdl->getRenderer()->getRenderer(), $rect);                        $startX = -1;                        $endX = -1;                    }                }            }                        // Dessiner le dernier rectangle de la ligne si nécessaire            if ($startX != -1 && $endX != -1) {                $rect = new \SDL_Rect;                $rect->x = $tileX + $startX;                $rect->y = $tileY + $y;                $rect->w = $endX - $startX + 1;                $rect->h = 1;                \SDL_RenderFillRect($this->sdl->getRenderer()->getRenderer(), $rect);            }        }    }    private function drawGameObjects(): void    {        if (!$this->objectManager || !$this->currentLevel) return;        $cameraX = $this->camera->getX();        $cameraY = $this->camera->getY();        $winW = $this->sdl->getWindow()->getWidth();        $winH = $this->sdl->getWindow()->getHeight();        $visibleObjects = $this->objectManager->getVisibleObjects($cameraX, $cameraY, $winW, $winH);        // flowers are background tiles in the original: behind every sprite        usort($visibleObjects, fn($a, $b) => ($b instanceof Flower) <=> ($a instanceof Flower));        foreach ($visibleObjects as $obj) {            $obj->render($cameraX, $cameraY);        }    }    public function setPlayer(Player $player)    {        $this->player = $player;    }    public function resetLevel()    {        $this->currentLevel = null;    }}