kind = match ($objectType) { 0x0B => 'sink', 0x0C => 'weight', 0x0F => 'leftRight', 0x27, 0x28 => 'fallLog', 0x29 => 'roll', 0x38 => 'flyRight', 0x3B => 'upDown', 0x45 => 'float', default => 'none', }; if ($sheet !== null) { $this->tilesetName = $sheet; } $frames = $this->kind === 'roll' ? RomSprites::LOG_ROLL : [$sprite]; foreach ($frames as $i => $frame) { $this->animations["f$i"] = ['flags' => [], 'coords' => self::frames([$frame])]; } $this->setAnimation('f0'); // The ROM layouts start with an empty row: the sprite is 16px under the object, +1 line on the SMS. $this->x = $tileX * 32; $this->y = $tileY * 32 + 17; if ($this->kind === 'float') { $this->y = $tileY * 32 + 15; // Y - 1, sprite on the second row } elseif ($this->kind === 'roll') { $this->y -= 24; // Y - 24 on the first frame } $this->startX = $this->x; $this->startY = $this->y; } public function update(float $deltaTime): void { if (!$this->active) return; $oldX = $this->x; $oldY = $this->y; $this->pinX = null; $this->clock += $deltaTime; while ($this->clock >= self::FRAME) { $this->clock -= self::FRAME; $this->stood = $this->touched; $this->touched = false; $this->tick(); $this->frames++; } $this->deltaX = $this->x - $oldX; $this->carryX = $this->deltaX; $this->deltaY = $this->y - $oldY; if ($this->kind === 'roll' && $this->stood) { // _7ee6: Sonic is held on the log (X = log X - 4 for his 24px wide object) $this->pinX = $this->x + $this->width / 2; } } private function tick(): void { switch ($this->kind) { case 'sink': $offset = $this->y - $this->startY; if ($this->stood) { $this->y = $offset < 16 ? min($this->startY + 16, $this->y + 0.5) : $this->y; } elseif ($offset > 0) { $this->y = max($this->startY, $this->y - 0.25); } break; case 'weight': if ($this->stood) $this->falling = true; if ($this->falling && ++$this->counter >= 0x18) { $this->speedY += 0x40 / 256; } $this->y += $this->speedY; // removed once below the screen if ($this->y > $this->startY + 480) { $this->active = false; $this->visible = false; } break; case 'leftRight': if (++$this->counter >= 0xA0) { $this->counter = 0; $this->direction = -$this->direction; } $this->x += $this->direction; break; case 'fallLog': $this->y += 0.5; if (($this->frames & 3) === 0 && ++$this->counter >= ($this->objectType === 0x27 ? 0xC0 : 0xC6)) { $this->counter = 0; $this->y = $this->startY; } break; case 'roll': $this->y += $this->counter < 0x14 ? 0.25 : -0.25; $this->counter = ($this->counter + 1) % 0x28; $frame = 0; if ($this->stood && $this->playerSpeed != 0) { $speed = $this->playerSpeed / 60; // px/frame if (!$this->blockedAhead($speed > 0)) { $this->x += $speed / 2; } $this->rolled = fmod($this->rolled + abs($speed), 9); $frame = intdiv((int)$this->rolled, 3); } $this->setAnimation("f$frame"); break; case 'flyRight': if ($this->stood) $this->startX += 0.5; $this->x = $this->startX; $this->y = $this->startY + $this->wave; break; case 'upDown': $this->speedY += $this->counter < 0x50 ? 1 / 16 : -1 / 16; $this->speedY = max(-2, min(2, $this->speedY)); $this->y += $this->speedY; $this->counter = ($this->counter + 1) % 0xA0; break; case 'float': $this->tickFloat(); break; } } /** $29: the log stops when the block in front of it (X+18 or X-2, at the object Y) is solid. */ private function blockedAhead(bool $right): bool { if (!$this->level) return false; $x = $right ? $this->x + 0x12 : $this->x - 2; $block = $this->level->getTile((int)floor($x / 32), (int)floor(($this->y - 17) / 32)); $solidity = $block === null ? null : ($this->level->getSolidity()[$block] ?? null); return $solidity !== null && (ord($solidity) & 0x3F) !== 0; } /** * doObjectCode_platform_float: once stood on, accelerates upwards by 8/256 px/frame² up to 1px/frame and * stops when the block under it has the solidity $1E. Goes back home when Sonic is far away (off-screen). */ private function tickFloat(): void { if ($this->stood) $this->falling = true; // "touched" once if (!$this->falling || !$this->level) return; $block = $this->level->getTile((int)floor(($this->x + 16) / 32), (int)floor(($this->y + 16) / 32)); $solidity = $this->level->getSolidity()[$block] ?? null; if ($solidity !== null && (ord($solidity) & 0x3F) === 0x1E) { $this->speedY = 0; return; } $this->speedY = max(-1, $this->speedY - 8 / 256); $this->y += $this->speedY; } public function render(float $cameraX, float $cameraY): void { parent::render($cameraX, $cameraY); if ($this->kind === 'flyRight') { // _b388: the propeller on the left side, a new frame every frame $frame = RomSprites::PROPELLER[$this->frames & 3]; $src = new \SDL_Rect; [$src->x, $src->y, $src->w, $src->h] = $frame; $dst = new \SDL_Rect; $dst->x = (int)round($this->x - 8 - $cameraX); $dst->y = (int)round($this->y - 8 + ($frame[3] === 16 ? 4 : 0) - $cameraY); $dst->w = $frame[2]; $dst->h = $frame[3]; \SDL_RenderCopy($this->sdl->getRenderer()->getRenderer(), $this->getTexture()['texture'], $src, $dst); } } /** Called by the GameObjectManager while Sonic stands on the platform. */ public function touch(): void { $this->touched = true; } public function setPlayerSpeed(float $vx): void { $this->playerSpeed = $vx; } public function setWave(float $wave): void { $this->wave = $wave; } /** Centre X Sonic is held at ($29 floating log), null when he moves freely. */ public function getPinX(): ?float { return $this->pinX; } public function isFloating(): bool { return $this->kind === 'float'; } /** Stood on: a weight falls, a floating platform rises (online: stood on on another machine). */ public function isFalling(): bool { return $this->falling; } public function startFalling(): void { if ($this->kind === 'weight' || $this->kind === 'float') $this->falling = true; } /** Back to the spawn point (the original does it when the platform leaves the screen). */ public function resetPosition(): void { $this->x = $this->startX; $this->y = $this->startY; $this->falling = false; $this->speedY = 0; } public function isPlayerOnTop(array $playerRect, float $playerVy): bool { if (!$this->active || $playerVy < 0) return false; $feet = $playerRect['y'] + $playerRect['height']; return $feet >= $this->y - 4 - max(0, $this->deltaY) && $feet <= $this->y + 10 + max(0, $this->deltaY) && $playerRect['x'] + $playerRect['width'] > $this->x + 2 && $playerRect['x'] < $this->x + $this->width - 2; } public function getDeltaX(): float { return $this->deltaX; } /** * Horizontal move of the last update, handed out once: Sonic's collision runs several steps per frame, * he must be carried by the platform once, not once per step (he slid along it). */ public function takeDeltaX(): float { $dx = $this->carryX; $this->carryX = 0; return $dx; } public function getDeltaY(): float { return $this->deltaY; } }