<?phpnamespace App\Scene;use App\Audio\Scale;use App\Render\Gfx;use JumPHP\Physics2d\Ball;use JumPHP\Physics2d\Wall;use JumPHP\Renderer\Renderer;/** Marbles drop down a zig-zag of ramps; every ramp is a note of a scale, so the falling marbles play a rippling arpeggio. */final class MarbleScene extends VideoScene{ public const SECONDS = 24.0; public const MAX_ALIVE = 22; public const RAMPS = 6; private float $nextDrop = 0; private int $dropped = 0; private int $done = 0; /** @var array<int, float> ramp (object id) => flash */ private array $flash = []; /** @var array<int, int> ramp (object id) => its degree on the scale */ private array $degree = []; protected function melodyNotes(): string { return 'C4'; } protected function headingText(): string { return 'MARBLE RUN'; } protected function hookText(): string { return 'LISTEN TO THE RAMPS'; } protected function captionText(): string { return $this->t('DROPPED {n} DONE {m}', ['n' => $this->dropped, 'm' => $this->done]); } protected function build(): void { $this->world->reset($this->seed + $this->round * 7919); $this->world->gravity = 700; $this->world->restitution = 0.62; $this->nextDrop = 0.2; $this->dropped = 0; $this->done = 0; $this->flash = []; $this->degree = []; $this->world->addWall(new Wall(6, 90, 6, 470, '#2c3157')); $this->world->addWall(new Wall(264, 90, 264, 470, '#2c3157')); for ($i = 0; $i < self::RAMPS; $i++) { $y = 120 + $i * 58; $color = self::hue($i / self::RAMPS * 0.8 + 0.05); $wall = $i % 2 === 0 ? new Wall(8, $y, 220, $y + 40, $color) : new Wall(262, $y, 50, $y + 40, $color); $this->world->addWall($wall); $this->degree[spl_object_id($wall)] = 11 - $i * 2; } } protected function tick(float $dt): void { $this->nextDrop -= $dt; if ($this->nextDrop <= 0 && count($this->world->balls()) < self::MAX_ALIVE) { $this->nextDrop = 0.42; $this->world->addBall(new Ball(30 + $this->world->random->between(0, 30), 94, 0, 0, 6, self::hue($this->dropped * 0.071), 60)); $this->dropped++; } foreach ($this->world->balls() as $ball) { if ($ball->y > 500) { $this->world->removeBall($ball); $this->done++; } } foreach ($this->flash as $id => $f) { $this->flash[$id] = max(0.0, $f - $dt * 3); } } protected function onHit(array $hit): void { $id = spl_object_id($hit['body']); if ($hit['type'] !== 'wall' || !isset($this->degree[$id]) || $hit['speed'] < 70) { return; } $this->bounces++; $this->flash[$id] = 1.0; $this->squash($hit['ball'], (float)$hit['x'], (float)$hit['y']); $this->burst((float)$hit['x'], (float)$hit['y'], $hit['ball']->color, 3, 55); $this->play(Scale::freq(Scale::MAJOR_PENTA, $this->degree[$id]), 'marimba', 0.5, min(0.3, 0.14 + $hit['speed'] / 2500), 0.05); } protected function drawWorld(Renderer $r): void { foreach ($this->world->walls() as $w) { $f = $this->flash[spl_object_id($w)] ?? 0; $rgb = Gfx::rgb($w->color); if ($f > 0.02) { Gfx::bar($r, $w->x1, $w->y1, $w->x2, $w->y2, 6 + $f * 4, Gfx::mix(Gfx::BG_BOTTOM, $rgb, 0.14 + 0.2 * $f)); } Gfx::bar($r, $w->x1, $w->y1, $w->x2, $w->y2, 2.6 + $f * 1.4, Gfx::mix($rgb, [255, 255, 255], $f * 0.8)); } foreach ($this->world->balls() as $ball) { $this->drawBall($r, $ball); } }}