JA

SONIC PHP

作者:davidnurdin · プラットフォーマー
5.0 (1)
▶ 遊ぶ 💬 フィードバック ソース 🕘 バージョン 編集 GitHub
. / src / Entities / Objects / Badnik.php · commit a20d00e
<?phpnamespace App\Entities\Objects;use App\Entities\GameObject;use App\Gfx\Gfx;class Badnik extends GameObject{    // Sprite rects are [x, y, w, h] in tileset-enemies.png; 'left' frames face left, 'right' frames are optional (else flipped).    public const DEFS = [        0x08 => ['mode' => 'crab', 'w' => 24, 'h' => 24, 'speed' => 30, 'fd' => 0.15,            'left' => [[8, 8, 24, 24], [40, 8, 24, 24], [72, 8, 24, 24], [104, 8, 24, 24]], 'symmetric' => true],        0x10 => ['mode' => 'walker', 'w' => 16, 'h' => 16, 'speed' => 60, 'fd' => 0.1,            'left' => [[152, 8, 16, 16], [176, 8, 16, 16]], 'right' => [[200, 8, 16, 16], [224, 8, 16, 16]]],        0x0E => ['mode' => 'flyer', 'w' => 24, 'h' => 25, 'speed' => 90, 'fd' => 0.05, 'bottom' => 32,            'left' => [[264, 3, 24, 25], [296, 3, 24, 25]]],        0x26 => ['mode' => 'jumper', 'w' => 13, 'h' => 16, 'speed' => 0, 'fd' => 0.12, 'bottom' => 4,            'left' => [[538, 8, 13, 16], [562, 8, 13, 16]]],        0x11 => ['mode' => 'newtron', 'w' => 14, 'h' => 28, 'speed' => 0, 'fd' => 0.3,            'left' => [[10, 58, 14, 28], [64, 58, 14, 28]]],        // Yadrin: separate art for each direction like the original (layouts 0-1 left, 2-3 right), never flipped        0x2D => ['mode' => 'yadrin', 'w' => 23, 'h' => 30, 'speed' => 45, 'fd' => 0.1, 'symmetric' => true,            'left' => [[137, 66, 23, 30], [170, 66, 22, 30]], 'right' => [[200, 66, 23, 30], [232, 66, 22, 30]]],        0x3C => ['mode' => 'swimmer', 'w' => 15, 'h' => 14, 'speed' => 30, 'fd' => 0.2,            'left' => [[281, 64, 15, 14], [313, 64, 15, 14]], 'right' => [[352, 64, 15, 14], [384, 64, 15, 14]]],        0x35 => ['mode' => 'unidus', 'w' => 14, 'h' => 15, 'speed' => 0, 'fd' => 0.2,            'left' => [[425, 64, 14, 15], [449, 64, 14, 15]]],        0x44 => ['mode' => 'walker', 'w' => 15, 'h' => 30, 'speed' => 40, 'fd' => 0.12,            'left' => [[505, 66, 15, 30], [529, 66, 15, 30], [553, 66, 15, 30]], 'right' => [[576, 66, 15, 30], [600, 66, 15, 30], [624, 66, 15, 30]]],        0x1F => ['mode' => 'walker', 'w' => 8, 'h' => 16, 'speed' => 20, 'fd' => 0.25, 'segments' => 3,            'left' => [[8, 112, 8, 16], [64, 112, 8, 16]]],        0x1B => ['mode' => 'hopper', 'w' => 12, 'h' => 29, 'speed' => 0, 'fd' => 0.2,            'left' => [[99, 127, 12, 25], [124, 122, 11, 29], [147, 127, 12, 25]], 'right' => [[169, 127, 12, 25], [193, 122, 11, 29], [217, 127, 12, 25]]],        0x32 => ['mode' => 'bomb', 'w' => 12, 'h' => 16, 'speed' => 0, 'fd' => 0.15,            'left' => [[282, 121, 12, 15], [306, 120, 12, 16]], 'right' => [[354, 121, 12, 15], [378, 120, 12, 16]]],    ];    private const SEGMENT = ['x' => 24, 'y' => 115, 'w' => 8, 'h' => 13];    private const GRAVITY = 800.0;    private array $def;    private string $mode;    private float $speed;    private float $startX;    private float $startY;    private float $timer = 0;    private bool $paused = false;    private bool $appeared = true;    private bool $airborne = false;    private array $pendingShots = [];    // Unidus ($35, doObjectCode_badnick_unidos): object position, angles of the 4 balls ($11-$14, -1 = thrown),    // throw counter ($15), 60Hz clock    private float $romX = 0;    private float $romY = 0;    private array $ballAngles = [0x00, 0x2A, 0x52, 0x7C];    private int $throwCounter = 0;    private int $frames = 0;    private float $clock = 0;    // _b031: ball offsets (x, y) for the angles 0-$A3 (by 2)    private const UNIDUS_ORBIT = [        [0x0C, 0x03], [0x0D, 0x03], [0x0E, 0x03], [0x0E, 0x04], [0x0F, 0x04], [0x10, 0x04], [0x10, 0x05], [0x11, 0x05],        [0x11, 0x06], [0x12, 0x06], [0x12, 0x07], [0x13, 0x07], [0x13, 0x08], [0x13, 0x09], [0x14, 0x09], [0x14, 0x0A],        [0x14, 0x0B], [0x15, 0x0B], [0x15, 0x0C], [0x15, 0x0D], [0x15, 0x0E], [0x15, 0x0F], [0x15, 0x10], [0x15, 0x11],        [0x14, 0x11], [0x14, 0x12], [0x14, 0x13], [0x13, 0x13], [0x13, 0x14], [0x13, 0x15], [0x12, 0x15], [0x12, 0x16],        [0x11, 0x16], [0x11, 0x17], [0x10, 0x17], [0x10, 0x18], [0x0F, 0x18], [0x0E, 0x18], [0x0E, 0x19], [0x0D, 0x19],        [0x0C, 0x19], [0x0B, 0x19], [0x0A, 0x19], [0x09, 0x19], [0x09, 0x18], [0x08, 0x18], [0x07, 0x18], [0x07, 0x17],        [0x06, 0x17], [0x06, 0x16], [0x05, 0x16], [0x05, 0x15], [0x04, 0x15], [0x04, 0x14], [0x04, 0x13], [0x03, 0x13],        [0x03, 0x12], [0x03, 0x11], [0x02, 0x11], [0x02, 0x10], [0x02, 0x0F], [0x02, 0x0E], [0x02, 0x0D], [0x02, 0x0C],        [0x02, 0x0B], [0x03, 0x0B], [0x03, 0x0A], [0x03, 0x09], [0x04, 0x09], [0x04, 0x08], [0x04, 0x07], [0x05, 0x07],        [0x05, 0x06], [0x06, 0x06], [0x06, 0x05], [0x07, 0x05], [0x07, 0x04], [0x08, 0x04], [0x09, 0x04], [0x09, 0x03],        [0x0A, 0x03], [0x0B, 0x03],    ];    public function __construct(Gfx $sdl, int $type, string $name, int $tileX, int $tileY)    {        $def = self::DEFS[$type];        parent::__construct($sdl, $type, $name, 0, 0, $def['w'], $def['h']);        $this->def = $def;        $this->mode = $def['mode'];        $this->speed = $def['speed'];        $this->tilesetName = 'enemies';        $this->frameDuration = $def['fd'];        $left = self::frames($def['left']);        $this->animations = [            'idle' => ['flags' => [], 'coords' => $left],            'left' => ['flags' => [], 'coords' => $left],            'right' => isset($def['right'])                ? ['flags' => [], 'coords' => self::frames($def['right'])]                : ['flags' => [], 'coords' => $left],        ];        $this->placeOnSpawn($tileX, $tileY, $def['bottom'] ?? 16);        $this->startX = $this->x;        $this->startY = $this->y;        $this->facing = 'left';        $this->vx = -$this->speed;        if ($this->mode === 'newtron') {            $this->appeared = false;            $this->visible = false;        }        $this->setAnimation('left');        if ($this->mode === 'bomb') {            $this->romX = $tileX * 32;            $this->x = $this->romX + 2;        }        if ($this->mode === 'unidus') {            // body sprite at X+8, Y+16 of the object (+1 line on the SMS)            $this->romX = $tileX * 32;            $this->romY = $tileY * 32;            $this->x = $this->romX + 9;            $this->y = $this->romY + 17;        }    }    public static function supports(int $type): bool    {        return isset(self::DEFS[$type]);    }    /** Called each tick by the manager; returns projectiles to spawn. */    public function think(float $deltaTime, float $playerX, float $playerY): array    {        if (!$this->active) return [];        $this->pendingShots = [];        $this->timer += $deltaTime;        match ($this->mode) {            'walker' => $this->walk($deltaTime),            'yadrin' => $this->yadrin($deltaTime),            'crab' => $this->crab($deltaTime),            'flyer' => $this->fly($deltaTime, $playerX, $playerY),            'jumper' => $this->jump($deltaTime),            'swimmer' => $this->patrolStraight($deltaTime, 96),            'floater' => $this->y = $this->startY + sin($this->timer * 2) * 12,            'unidus' => $this->unidus($deltaTime, $playerX, $playerY),            'bomb' => $this->bomb($deltaTime, $playerX),            'hopper' => $this->hop($deltaTime, $playerX),            'newtron' => $this->newtron($playerX, $playerY),            default => null,        };        $anim = $this->facing;        if ($this->currentAnimation !== $anim) {            $this->setAnimation($anim);        }        $this->flipX = $this->facing === 'right' && !isset($this->def['right']) && empty($this->def['symmetric']);        if (!$this->paused) {            $this->updateSprite($deltaTime);        }        return $this->pendingShots;    }    public function update(float $deltaTime): void    {    }    private function turn(): void    {        $this->facing = $this->facing === 'left' ? 'right' : 'left';        $this->vx = $this->facing === 'left' ? -$this->speed : $this->speed;    }    private function walk(float $deltaTime): void    {        if ($this->level === null) {            $this->x += $this->vx * $deltaTime;            return;        }        $centerX = (int)round($this->x + $this->width / 2);        if ($this->airborne || $this->level->findSurfaceY($centerX, (int)round($this->y + $this->height), 2) === null) {            $this->fall($deltaTime, $centerX);            return;        }        if ($this->paused) return;        $dir = $this->vx < 0 ? -1 : 1;        $nextX = $this->x + $this->vx * $deltaTime;        $bottom = (int)round($this->y + $this->height);        $nextCenter = (int)round($nextX + $this->width / 2);        $frontX = (int)round($nextX + $this->width / 2 + $dir * ($this->width / 2 + 2));        $groundCenter = $this->level->findSurfaceY($nextCenter, $bottom, 12);        $groundFront = $this->level->findSurfaceY($frontX, $bottom, 16);        $tooFar = ($nextX - $this->startX) * $dir > 160;        if ($groundCenter === null || $groundFront === null || $tooFar) {            $this->turn();            return;        }        $this->x = $nextX;        $this->y = $groundCenter - $this->height;    }    /**     * doObjectCode_badnick_yadrin: no edge detection, a timer drives it. A counter goes up every 8 frames:     * 0-$4F walks left at $C0/256 px/frame, $50-$9F walks right at $40/256 px/frame, then wraps.     * Gravity like every object; it only stops against walls.     */    private function yadrin(float $deltaTime): void    {        $cycle = fmod($this->timer * 60, 0xA0 * 8);        $goingRight = $cycle >= 0x50 * 8;        $this->facing = $goingRight ? 'right' : 'left';        $this->vx = $goingRight ? 0x40 / 256 * 60 : -0xC0 / 256 * 60;        if ($this->level === null) {            $this->x += $this->vx * $deltaTime;            return;        }        $centerX = (int)round($this->x + $this->width / 2);        if ($this->airborne || $this->level->findSurfaceY($centerX, (int)round($this->y + $this->height), 2) === null) {            $this->x += $this->vx * $deltaTime;            $this->fall($deltaTime, (int)round($this->x + $this->width / 2));            return;        }        $nextX = $this->x + $this->vx * $deltaTime;        $dir = $goingRight ? 1 : -1;        $frontX = (int)round($nextX + $this->width / 2 + $dir * ($this->width / 2 + 1));        $mapRight = $this->level->getMapWidth() * 32 - $this->width;        if ($this->level->isSolidAt($frontX, (int)round($this->y + $this->height - 10)) || $nextX < 0 || $nextX > $mapRight) {            return; // against a wall / the level edge        }        $this->x = $nextX;        $nextCenter = (int)round($this->x + $this->width / 2);        $ground = $this->level->findSurfaceY($nextCenter, (int)round($this->y + $this->height), 12);        if ($ground !== null) {            $this->y = $ground - $this->height;        }    }    private function fall(float $deltaTime, int $centerX): void    {        $this->airborne = true;        $this->vy = min($this->vy + self::GRAVITY * $deltaTime, 600);        $this->y += $this->vy * $deltaTime;        $bottom = (int)round($this->y + $this->height);        if ($this->level->isSolidAt($centerX, $bottom)) {            $surface = $this->level->findSurfaceY($centerX, $bottom, 32);            if ($surface !== null) {                $this->y = $surface - $this->height;            }            $this->vy = 0;            $this->airborne = false;        } elseif ($this->y > $this->startY + 2048) {            $this->destroy();        }    }    private function crab(float $deltaTime): void    {        // Walks ~2s, stops 1s then fires two arcing shots (left and right).        if ($this->paused) {            if ($this->timer >= 1.0) {                $cx = $this->x + $this->width / 2;                $this->pendingShots[] = [$cx - 10, $this->y + 4, -90, -300, true];                $this->pendingShots[] = [$cx + 10, $this->y + 4, 90, -300, true];                $this->paused = false;                $this->timer = 0;            }            return;        }        if ($this->timer >= 2.0) {            $this->paused = true;            $this->timer = 0;            return;        }        $this->walk($deltaTime);    }    private function fly(float $deltaTime, float $playerX, float $playerY): void    {        if ($this->paused) {            if ($this->timer >= 0.6) {                $this->paused = false;                $this->timer = -1.5;            }            return;        }        $this->patrolStraight($deltaTime, 128);        $dx = $playerX - ($this->x + $this->width / 2);        if ($this->timer > 0 && abs($dx) < 96 && $playerY > $this->y && $playerY - $this->y < 200) {            $dir = $this->facing === 'left' ? -1 : 1;            $this->pendingShots[] = [$this->x + $this->width / 2 + 6 * $dir, $this->y + $this->height, 120 * $dir, 180, false];            $this->paused = true;            $this->timer = 0;        }    }    private function patrolStraight(float $deltaTime, float $range): void    {        $this->x += $this->vx * $deltaTime;        if (($this->x < $this->startX - $range && $this->vx < 0) || ($this->x > $this->startX + $range && $this->vx > 0)) {            $this->turn();        }    }    private function jump(float $deltaTime): void    {        // Chopper (original): rests hidden under the bridge for 30 frames, then leaps at -4 px/frame with        // gravity 1/16 px/frame² and a fall speed capped at 4 px/frame (about 128 px high).        if (!$this->airborne) {            $this->visible = false;            if ($this->timer >= 0.5) {                $this->airborne = true;                $this->visible = true;                $this->vy = -240;            }            return;        }        $this->vy = min(240, $this->vy + 225 * $deltaTime);        $this->y += $this->vy * $deltaTime;        if ($this->vy > 0 && $this->y >= $this->startY) {            $this->y = $this->startY;            $this->vy = 0;            $this->airborne = false;            $this->visible = false;            $this->timer = 0;        }    }    private function hop(float $deltaTime, float $playerX): void    {        $this->facing = $playerX < $this->x ? 'left' : 'right';        if (!$this->airborne) {            if ($this->timer >= 1.2) {                $this->airborne = true;                $this->vy = -260;                $dir = $this->facing === 'left' ? -1 : 1;                $this->pendingShots[] = [$this->x + $this->width / 2, $this->y + 8, 70 * $dir, -120, true];            }            return;        }        $this->vy += self::GRAVITY * $deltaTime;        $this->y += $this->vy * $deltaTime;        if ($this->y >= $this->startY) {            $this->y = $this->startY;            $this->airborne = false;            $this->timer = 0;        }    }    private function newtron(float $playerX, float $playerY): void    {        $dx = $playerX - ($this->x + $this->width / 2);        if (!$this->appeared) {            if (abs($dx) < 128 && abs($playerY - $this->y) < 128) {                $this->appeared = true;                $this->visible = true;                $this->timer = 0;            }            return;        }        $this->facing = $dx < 0 ? 'left' : 'right';        if ($this->timer >= 1.0) {            $dir = $dx < 0 ? -1 : 1;            $this->pendingShots[] = [$this->x + $this->width / 2, $this->y + 10, 150 * $dir, 0, false];            $this->timer = -2.0;        }    }    /**     * Drifts towards Sonic (8/256 px/frame) while its 4 balls turn around it (1 step a frame, towards Sonic's     * side). In Labyrinth (solidity 3), once $15 reached $C8, a ball at the angle $4A is thrown at 1/2 px/frame     * (object $36) when Sonic is at its height (_af98).     */    private function unidus(float $deltaTime, float $playerX, float $playerY): void    {        $this->clock += $deltaTime;        while ($this->clock >= 1 / 60) {            $this->clock -= 1 / 60;            $this->frames++;            $heroLeft = $this->romX >= $playerX - 12;            $this->facing = $heroLeft ? 'left' : 'right';            $step = $heroLeft ? 1 : -1;            $heroTop = $playerY - 16;            if ($this->throwCounter >= 0xC8 && $this->level && $this->level->getSolidityIndex() === 3                && $heroTop > $this->romY - 0x30 && $heroTop < $this->romY + 0x2C) {                foreach ($this->ballAngles as $i => $angle) {                    if ($angle === 0x4A) {                        $this->ballAngles[$i] = -1;                        $this->pendingShots[] = [$this->romX + 0x12 + 4, $this->romY + 0x1E + 6, ($heroLeft ? -0x80 : 0x80) / 256 * 60, 0, false,                            RomSprites::UNIDUS_BALL, RomSprites::SHEET];                    }                }            }            $this->romX += ($heroLeft ? -8 : 8) / 256;            $this->x = $this->romX + 9;            foreach ($this->ballAngles as $i => $angle) {                if ($angle < 0) continue;                $angle += $step;                $this->ballAngles[$i] = $angle === -1 ? 0xA3 : ($angle === 0xA4 ? 0 : $angle);            }            if (($this->frames & 7) !== 0 && $this->throwCounter < 0xC8) {                $this->throwCounter++;            }        }    }    // Bomb ($32, doObjectCode_badnick_bomb): step counter ($11, +1 every 64 frames), frame of _ad0b shown    private int $bombStep = 0;    private int $bombFrame = 0;    /**     * Bomb: a step counter goes up every 64 frames (0-$45): it walks left 8/256 px a frame until $1E, waits,     * walks right from $23 to $41, waits. Sonic within X - $38 .. X + $2C lights it ($64): it stops and     * flashes ($64-$65), then at $67 bursts into 4 shrapnels ($0D) at (-2, -4), (3, -4), (-2, -2), (3, -2)     * px a frame (sfx $1B). It can't be destroyed: touching it hurts (_35fd).     */    private function bomb(float $deltaTime, float $playerX): void    {        $this->clock += $deltaTime;        while ($this->clock >= 1 / 60 && $this->active) {            $this->clock -= 1 / 60;            $this->frames++;            $heroX = $playerX - 12;            if ($this->bombStep < 0x64 && $heroX >= $this->romX - 0x38 && $heroX < $this->romX + 0x2C) {                $this->bombStep = 0x64;            }            $step = $this->bombStep;            $vx = 0;            if ($step < 0x1E) {                $vx = -8 / 256;                $this->bombFrame = intdiv($this->frames, 0x20) % 2;            } elseif ($step >= 0x64) {                if ($step < 0x66) {                    $this->bombFrame = intdiv($this->frames, 2) % 2 ? 4 : 1;                } else {                    $this->bombFrame = 4;                    if ($step === 0x67) {                        foreach ([[0, 0, -2, -4], [8, 0, 3, -4], [0, 8, -2, -2], [8, 8, 3, -2]] as [$dx, $dy, $sx, $sy]) {                            $this->pendingShots[] = [$this->romX + $dx + 4, $this->y + $dy + 5, $sx * 60, $sy * 60, true];                        }                        $this->pendingSound = 0x1B;                        $this->destroy();                        return;                    }                }            } elseif ($step < 0x23) {                $this->bombFrame = 1;            } elseif ($step < 0x41) {                $vx = 8 / 256;                $this->bombFrame = 2 + intdiv($this->frames, 0x20) % 2;            } else {                $this->bombFrame = 3;            }            $this->romX += $vx;            $this->x = $this->romX + 2;            if ($this->level !== null) {                $ground = $this->level->findSurfaceY((int)round($this->x + $this->width / 2), (int)round($this->y + $this->height), 8);                if ($ground !== null) $this->y = $ground - $this->height;            }            if ($this->frames % 64 === 0) {                $this->bombStep++;                if ($this->bombStep === 0x46) $this->bombStep = 0;            }        }    }    /** Sound the badnik made this frame (bomb burst), taken by the manager. */    public ?int $pendingSound = null;    /** The bomb can't be destroyed: touching it hurts. */    public function isInvulnerable(): bool    {        return $this->mode === 'bomb';    }    /** Areas that hurt Sonic whatever he does (the Unidus balls, _35fd). */    public function getHazards(): array    {        if ($this->mode !== 'unidus' || !$this->active) return [];        $boxes = [];        foreach ($this->ballAngles as $angle) {            if ($angle < 0) continue;            [$dx, $dy] = self::UNIDUS_ORBIT[$angle >> 1];            $boxes[] = ['x' => $this->romX + $dx + 1, 'y' => $this->romY + $dy + 2, 'width' => 6, 'height' => 6];        }        return $boxes;    }    public function render(float $cameraX, float $cameraY): void    {        if ($this->mode === 'bomb') {            $src = new \SDL_Rect;            [$src->x, $src->y, $src->w, $src->h] = RomSprites::BOMB[$this->bombFrame];            $dst = new \SDL_Rect;            [$dst->x, $dst->y, $dst->w, $dst->h] = [(int)round($this->romX - $cameraX), (int)round($this->y + 1 - $cameraY), 16, 16];            \SDL_RenderCopy($this->sdl->getRenderer()->getRenderer(), $this->sdl->getTextures(RomSprites::SHEET)[0]['texture'], $src, $dst);            return;        }        if ($this->mode === 'unidus') {            $texture = $this->sdl->getTextures(RomSprites::SHEET)[0]['texture'];            $src = new \SDL_Rect;            [$src->x, $src->y, $src->w, $src->h] = RomSprites::UNIDUS_BALL;            foreach ($this->getHazards() as $box) {                $dst = new \SDL_Rect;                $dst->x = (int)round($box['x'] - 1 - $cameraX);                $dst->y = (int)round($box['y'] - 2 + 1 - $cameraY);                $dst->w = 8;                $dst->h = 16;                \SDL_RenderCopy($this->sdl->getRenderer()->getRenderer(), $texture, $src, $dst);            }        }        if (!empty($this->def['segments'])) {            $texture = $this->getTexture()['texture'];            $src = new \SDL_Rect;            $src->x = self::SEGMENT['x'];            $src->y = self::SEGMENT['y'];            $src->w = self::SEGMENT['w'];            $src->h = self::SEGMENT['h'];            $dir = $this->facing === 'left' ? 1 : -1;            for ($i = $this->def['segments']; $i >= 1; $i--) {                $dst = new \SDL_Rect;                $wave = (int)round(sin($this->timer * 6 + $i) * 2);                $dst->x = (int)round($this->x + $dir * $i * 7 - $cameraX);                $dst->y = (int)round($this->y + $this->height - self::SEGMENT['h'] + $wave - $cameraY);                $dst->w = self::SEGMENT['w'];                $dst->h = self::SEGMENT['h'];                \SDL_RenderCopy($this->sdl->getRenderer()->getRenderer(), $texture, $src, $dst);            }        }        parent::render($cameraX, $cameraY);    }    public function destroy(): void    {        $this->active = false;        $this->visible = false;    }}