RAINBOW ISLANDS

par davidnurdin · Remakes
pas encore noté
▶ Jouer Sources Éditer GitHub
games/rainbow-island / src / Arcade / Artwork.php · commit d529485
<?phpnamespace App\Arcade;use App\World\Island;use App\World\Round;use JumPHP\Kernel\Engine;use JumPHP\Renderer\Renderer;/** * Every picture of the game, all extracted from the original disc (see tools/extract): * per island a background tile sheet, an overlay tile sheet (rainbows, items) and a sprite sheet, * plus the font, the loading screen and the rainbow's shape. * * Sprite frames are numbered like in the original: 0-3 Bub walking, 4 Bub firing, 5-7 Bub dying, * 11 a star, 12-15 the spin of a hit enemy, 16-28 the enemies and the boss of the island. */final class Artwork{    public const FRAME_FIRE = 4;    public const FRAME_DEATH = 5;    public const FRAMES_SPIN = [12, 13, 14, 15];    /** Overlay tile of the first item; item k is the 2x2 block of tiles FIRST_ITEM_TILE + 4k (TL, TR, BL, BR). */    private const FIRST_ITEM_TILE = 0x13;    private bool $booted = false;    /** @var Island[] */    private array $islands = [];    /** @var array<int, array<int, ?array{x: int, y: int, w: int, h: int}>> sprite rectangles by island */    private array $frames = [];    /** @var int[][] the rainbow: overlay tiles, 4 rows of 6 */    public array $rainbowTiles = [];    /** @var int[] top of the rainbow (pixels from its top) in each of its 48 columns */    public array $rainbowProfile = [];    public function __construct(private Engine $engine)    {    }    /** Loads the shared assets once: every scene calls it in enter() (the smoke test may start anywhere). */    public function boot(): void    {        if ($this->booted) {            return;        }        $this->booted = true;        $config = $this->engine->config;        $this->engine->setFont(new ArcadeFont($this->engine->sdl));        foreach (json_decode((string)file_get_contents($config->path('assets/data/islands.json')), true) as $i => $data) {            $this->islands[$i + 1] = Island::fromArray($i + 1, $data);        }        $rainbow = json_decode((string)file_get_contents($config->path('assets/data/rainbow.json')), true);        $this->rainbowTiles = $rainbow['tiles'];        $this->rainbowProfile = $rainbow['profile'];        $sound = $this->engine->sound;        foreach (['jump', 'rainbow', 'bonus', 'collect', 'break', 'credit', 'tally', 'splash', 'hit', 'extralife'] as $sfx) {            $sound->loadSfx($sfx, "assets/sfx/$sfx.ogg");        }        foreach (['theme', 'boss', 'hurry', 'goal', 'gameover', 'death'] as $music) {            $sound->loadMusic($music, "assets/music/$music.ogg");        }        // the menus use the original "credit" and "rainbow" effects        $sound->loadSfx('menu_move', 'assets/sfx/credit.ogg');        $sound->loadSfx('menu_confirm', 'assets/sfx/rainbow.ogg');    }    public function island(int $number): Island    {        return $this->islands[$number];    }    public function loadRound(int $number): Round    {        $island = $this->island(intdiv($number - 1, 4) + 1);        $this->loadIsland($island->number);        return Round::load($this->engine->config->path(sprintf('assets/maps/round-%02d.json', $number)), $island);    }    /** Textures of an island (only one island is kept in memory). */    public function loadIsland(int $number): void    {        $sdl = $this->engine->sdl;        if (isset($this->frames[$number])) {            return;        }        foreach (array_keys($this->frames) as $old) {            foreach (["island-$old-tiles", "island-$old-overlay", "island-$old-sprites"] as $name) {                $sdl->releaseTexture($name);            }        }        $this->frames = [];        $sdl->loadTexture("island-$number-tiles", "assets/tiles/island-$number.png");        $sdl->loadTexture("island-$number-overlay", "assets/tiles/overlay-$number.png");        $sdl->loadTexture("island-$number-sprites", "assets/sprites/island-$number.png");        $this->frames[$number] = json_decode((string)file_get_contents(            $this->engine->config->path("assets/sprites/island-$number.json")), true);    }    /** Size [w, h] of a sprite frame. */    public function frameSize(int $island, int $frame): array    {        $rect = $this->frames[$island][$frame] ?? null;        return $rect === null ? [0, 0] : [$rect['w'], $rect['h']];    }    /** Draws a sprite frame with its top-left corner at (x, y); sprites look to the right, $flip mirrors them. */    public function drawFrame(Renderer $r, int $island, int $frame, float $x, float $y, bool $flip = false): void    {        $rect = $this->frames[$island][$frame] ?? null;        if ($rect === null) {            return;        }        $texture = $this->engine->sdl->getTexture("island-$island-sprites")['texture'];        $r->copy($texture, [$rect['x'], $rect['y'], $rect['w'], $rect['h']], $x, $y, null, null, $flip);    }    /**     * Size [w, h] of the boss of an island as drawn: the big bosses (islands 1 and 7) are stored as their     * left half; the CPC version keeps no big boss picture in the sprite table of the other islands,     * so their biggest sprite stands in for it, drawn twice as big.     */    public function bossSize(Island $island): array    {        [$w, $h] = $this->frameSize($island->number, $island->bossFrame);        return $island->bossMirrored ? [$w * 2, $h] : [$w * 2, $h * 2];    }    public function drawBoss(Renderer $r, Island $island, float $x, float $y): void    {        $rect = $this->frames[$island->number][$island->bossFrame] ?? null;        if ($rect === null) {            return;        }        $texture = $this->engine->sdl->getTexture("island-{$island->number}-sprites")['texture'];        $src = [$rect['x'], $rect['y'], $rect['w'], $rect['h']];        if ($island->bossMirrored) {            $r->copy($texture, $src, $x, $y);            $r->copy($texture, $src, $x + $rect['w'], $y, null, null, true);        } else {            $r->copy($texture, $src, $x, $y, $rect['w'] * 2, $rect['h'] * 2);        }    }    public function drawOverlayTile(Renderer $r, int $island, int $tile, float $x, float $y): void    {        $texture = $this->engine->sdl->getTexture("island-$island-overlay")['texture'];        $r->copy($texture, [($tile % 16) * Round::TILE, intdiv($tile, 16) * Round::TILE, Round::TILE, Round::TILE], $x, $y);    }    /** Item $kind (0-7 fruits of the island, 8-14 the seven small diamonds, then bonus objects), 16x16. */    public function drawItem(Renderer $r, int $island, int $kind, float $x, float $y): void    {        $tile = self::FIRST_ITEM_TILE + $kind * 4;        $this->drawOverlayTile($r, $island, $tile, $x, $y);        $this->drawOverlayTile($r, $island, $tile + 1, $x + 8, $y);        $this->drawOverlayTile($r, $island, $tile + 2, $x, $y + 8);        $this->drawOverlayTile($r, $island, $tile + 3, $x + 8, $y + 8);    }    /**     * The whole background of a round in one texture (drawn once with GD from the tile sheet),     * so a frame draws it with a single copy.     */    public function roundTexture(Round $round): mixed    {        $sdl = $this->engine->sdl;        $name = 'round-background';        if ($sdl->hasTexture($name)) {            $sdl->releaseTexture($name);        }        $number = $round->island->number;        $sheet = imagecreatefrompng($this->engine->config->path("assets/tiles/island-$number.png"));        $mirrored = imagecreatefrompng($this->engine->config->path("assets/tiles/island-$number.png"));        imageflip($mirrored, IMG_FLIP_HORIZONTAL);        $rows = $round->rows();        $image = imagecreatetruecolor(Round::WIDTH, count($rows) * Round::TILE);        foreach ($rows as $y => $row) {            foreach ($row as $x => $tile) {                $index = $tile & 0x7f;                $column = $index % 16;                if ($tile & 0x80) {                    imagecopy($image, $mirrored, $x * 8, $y * 8, (15 - $column) * 8, intdiv($index, 16) * 8, 8, 8);                } else {                    imagecopy($image, $sheet, $x * 8, $y * 8, $column * 8, intdiv($index, 16) * 8, 8, 8);                }            }        }        return $sdl->createTextureFromGd($name, $image)[0]['texture'];    }    /** The small rainbow arcs that count the lives at the bottom of the screen. */    public function drawLifeIcon(Renderer $r, float $x, float $y): void    {        $sdl = $this->engine->sdl;        if (!$sdl->hasTexture('life')) {            $sdl->createTextureFromPixels('life', [                '..oooo..',                '.oyyyyo.',                'oyo..oyo',                'yo....oy',                'y......y',                'y......y',            ], ['o' => '#ff8000', 'y' => '#ffff80']);        }        $r->copy($sdl->getTexture('life')['texture'], null, $x, $y);    }}