登录
ZH
games/satisfying-bounce / src / Scene / MarbleScene.php · commit 8bdd6e4
<?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);        }    }}