Inloggen
NL
games/satisfying-bounce / src / Scene / MultiplyScene.php · commit 8bdd6e4
<?phpnamespace App\Scene;use App\Render\Gfx;use JumPHP\Physics2d\Ball;use JumPHP\Physics2d\Ring;use JumPHP\Renderer\Renderer;/** * One ball in a closed circle; every time a ball hits the wall, a new one is born. Never more than MAX_BALLS: when the circle is * full the oldest ball fades away (a ghost shrinks where it was), so the fun goes on and every bounce stays easy to see and hear. */final class MultiplyScene extends VideoScene{    public const SECONDS = 22.0;    public const MAX_BALLS = 10;    protected float $punch = 0.02;    /** @var list<array{x: float, y: float, r: float, rgb: array, age: float}> balls that faded away */    private array $ghosts = [];    private int $born = 0;    protected function melodyNotes(): string    {        // the opening of Fur Elise (Beethoven, public domain), then a rising arpeggio        return 'E5 D#5 E5 D#5 E5 B4 D5 C5 A4 C4 E4 A4 B4 E4 G#4 B4 C5 E4 E5 D#5 E5 D#5 E5 B4 D5 C5 A4';    }    protected function headingText(): string    {        return 'IT KEEPS MULTIPLYING';    }    protected function hookText(): string    {        return 'EVERY HIT MAKES A NEW BALL';    }    protected function captionText(): string    {        return $this->t('BALLS {n}   BOUNCES {m}', ['n' => count($this->world->balls()), 'm' => $this->bounces]);    }    protected function build(): void    {        $this->world->reset($this->seed + $this->round * 7919);        $this->world->gravity = 150;        $this->world->ballsCollide = true;        $this->world->maxSpeed = 260;        $this->ghosts = [];        $this->born = 0;        $this->world->addRing(new Ring(self::CX, self::CY, 118, thickness: 4, spin: 0.3, color: '#7fd8ff'));        $a = $this->world->random->between(0, 2 * M_PI);        $this->world->addBall(new Ball(self::CX, self::CY, cos($a) * 120, sin($a) * 120, 9, self::hue(0.9), 80));    }    protected function tick(float $dt): void    {        foreach ($this->ghosts as $i => &$g) {            $g['age'] += $dt * 2.2;        }        unset($g);        $this->ghosts = array_values(array_filter($this->ghosts, fn (array $g) => $g['age'] < 1));    }    protected function onHit(array $hit): void    {        if ($hit['type'] !== 'ring') {            return;        }        $this->bounces++;        /** @var Ring $ring */        $ring = $hit['body'];        /** @var Ball $ball */        $ball = $hit['ball'];        $ring->data['flash'] = 1.0;        $this->squash($ball, (float)$hit['x'], (float)$hit['y']);        $this->burst((float)$hit['x'], (float)$hit['y'], $ball->color, 5, 70);        $this->note(1.0, 0.4, 0.26, 0.085);        $rand = $this->world->random;        $a = $rand->between(0, 2 * M_PI);        $this->born++;        $this->world->addBall(new Ball($ball->x - ($ball->x - $ring->x) * 0.25, $ball->y - ($ball->y - $ring->y) * 0.25, cos($a) * 100, sin($a) * 100,            9, self::hue(0.9 + $this->born * 0.061), 80));        $balls = $this->world->balls();        if (count($balls) > self::MAX_BALLS) {            $old = $balls[0];            $this->ghosts[] = ['x' => $old->x, 'y' => $old->y, 'r' => $old->radius, 'rgb' => Gfx::rgb($old->color), 'age' => 0.0];            $this->world->removeBall($old);        }    }    protected function drawWorld(Renderer $r): void    {        foreach ($this->world->rings() as $ring) {            $this->drawRing($r, $ring);        }        foreach ($this->ghosts as $g) {            Gfx::arc($r, $g['x'], $g['y'], $g['r'] * (1 + $g['age'] * 1.5), 0, 2 * M_PI, 1.2 * (1 - $g['age']), Gfx::mix(Gfx::BG_BOTTOM, $g['rgb'], 1 - $g['age']));        }        foreach ($this->world->balls() as $ball) {            $this->drawBall($r, $ball);        }    }}