Accedi
IT
games/satisfying-bounce / src / Scene / PlinkoScene.php · commit 8bdd6e4
<?phpnamespace App\Scene;use App\Audio\Scale;use App\Render\Gfx;use JumPHP\Physics2d\Ball;use JumPHP\Physics2d\Peg;use JumPHP\Renderer\Renderer;/** Balls drop through a field of pegs; every peg is a note of a scale (the further right, the higher), lights up when hit, and a chime rings at the bottom. */final class PlinkoScene extends VideoScene{    public const SECONDS = 24.0;    public const MAX_ALIVE = 7;    public const ROWS = 13;    /** the value of each bin, left to right: the rare ones at the edges are worth the most */    public const VALUES = [50, 10, 5, 2, 1, 1, 2, 5, 10, 50];    public const BIN_W = 27.0;    private int $score = 0;    private float $scorePulse = 0.0;    /** @var int[] the bin of every ball that landed */    private array $bins = [];    /** @var array<int, array{0: float, 1: float, 2: float, 3: int, 4: array}> floating "+N": x, y, age, value, rgb */    private array $pops = [];    private float $nextDrop = 0.0;    private int $dropped = 0;    private int $landed = 0;    /** @var array<int, float> peg (object id) => flash */    private array $flash = [];    protected function melodyNotes(): string    {        return 'C4';    }    protected function headingText(): string    {        return 'PEGS THAT PLAY MUSIC';    }    protected function hookText(): string    {        return 'WHERE WILL IT LAND?';    }    protected function captionText(): string    {        return $this->t('BALLS {n}   LANDED {m}', ['n' => $this->dropped, 'm' => $this->landed]);    }    /** @return int[] */    public function bins(): array    {        return $this->bins;    }    public function score(): int    {        return $this->score;    }    public function landed(): int    {        return $this->landed;    }    protected function build(): void    {        $this->world->reset($this->seed + $this->round * 7919);        $this->world->gravity = 430;        $this->world->restitution = 0.5;        $this->nextDrop = 0.15;        $this->dropped = 0;        $this->landed = 0;        $this->flash = [];        $this->score = 0;        $this->scorePulse = 0.0;        $this->bins = [];        $this->pops = [];        for ($row = 0; $row < self::ROWS; $row++) {            $cols = $row % 2 === 0 ? 14 : 13;            for ($c = 0; $c < $cols; $c++) {                $x = ($row % 2 === 0 ? 5 : 15) + $c * 20;                $peg = new Peg($x, 168 + $row * 18, 2.8, self::hue($c / 14 * 0.8 + 0.05));                $peg->hits = 0;                $this->world->addPeg($peg);            }        }    }    protected function tick(float $dt): void    {        $this->nextDrop -= $dt;        if ($this->nextDrop <= 0 && count($this->world->balls()) < self::MAX_ALIVE) {            $this->nextDrop = 0.8;            $ball = new Ball(135 + $this->world->random->between(-24, 24), 148, $this->world->random->between(-8, 8), 0, 4.4, self::hue($this->dropped * 0.083), 50);            $this->world->addBall($ball);            $this->dropped++;        }        foreach ($this->world->balls() as $ball) {            if ($ball->y > 410 && empty($ball->data['done'])) {                $ball->data['done'] = true;                $this->landed++;                $bin = max(0, min(9, (int)floor($ball->x / self::BIN_W)));                $value = self::VALUES[$bin];                $this->bins[] = $bin;                $this->score += $value;                $this->scorePulse = 1.0;                $this->pops[] = [($bin + 0.5) * self::BIN_W, 404.0, 0.0, $value, Gfx::rgb($ball->color)];                $this->recorder->note($this->time, Scale::freq(Scale::MAJOR_PENTA, 5 + $bin, 60), 1.1, 'bell', 0.2);                if ($value >= 10) {                    $this->recorder->note($this->time, Scale::freq(Scale::MAJOR_PENTA, 12 + $bin % 3, 60), 1.4, 'bell', 0.12);                }                $this->burst($ball->x, 415, $ball->color, 10 + (int)($value / 5), 70);            }            if ($ball->y > 470) {                $this->world->removeBall($ball);            }        }        $this->scorePulse = max(0.0, $this->scorePulse - $dt * 3.5);        foreach ($this->pops as $i => $pop) {            $this->pops[$i][2] += $dt;            if ($this->pops[$i][2] > 1.1) {                unset($this->pops[$i]);            }        }        foreach ($this->flash as $id => $f) {            $this->flash[$id] = max(0.0, $f - $dt * 3.2);        }    }    protected function onHit(array $hit): void    {        if ($hit['type'] !== 'peg') {            return;        }        $this->bounces++;        /** @var Peg $peg */        $peg = $hit['body'];        $this->flash[spl_object_id($peg)] = 1.0;        $this->squash($hit['ball'], (float)$hit['x'], (float)$hit['y']);        $this->burst((float)$hit['x'], (float)$hit['y'], $hit['ball']->color, 2, 40);        $this->play(Scale::freq(Scale::MAJOR_PENTA, 2 + (int)floor($peg->x / 270 * 10) + ((int)round(($peg->y - 150) / 27) % 2), 60), 'marimba', 0.4, 0.22, 0.06);    }    protected function drawWorld(Renderer $r): void    {        foreach ($this->world->pegs() as $peg) {            $f = $this->flash[spl_object_id($peg)] ?? 0.0;            $rgb = Gfx::rgb($peg->color);            if ($f > 0.02) {                Gfx::glow($r, $peg->x, $peg->y, $peg->radius, $rgb, $f);            }            Gfx::disc($r, $peg->x, $peg->y, $peg->radius + $f * 1.2, Gfx::mix(Gfx::mix(Gfx::BG_BOTTOM, $rgb, 0.85), [255, 255, 255], $f));        }        $font = $this->font();        for ($i = 0; $i < 10; $i++) {            $v = self::VALUES[$i];            $rgb = Gfx::hsv(0.62 - min(1.0, log($v, 50)) * 0.62);            $cx = ($i + 0.5) * self::BIN_W;            $r->fillRect($i * self::BIN_W + 1, 394, self::BIN_W - 2, 34, [$rgb[0], $rgb[1], $rgb[2], 38]);            Gfx::bar($r, $i * self::BIN_W, 394, $i * self::BIN_W, 428, 1.4, Gfx::mix(Gfx::BG_BOTTOM, $rgb, 0.6));            $font->draw($r, (string)$v, $cx, 404, $v >= 10 ? 1.9 : 1.6, Gfx::mix([255, 255, 255], $rgb, $v >= 10 ? 0.0 : 0.35), 'center', [8, 8, 24]);        }        Gfx::bar($r, 270, 394, 270, 428, 1.4, Gfx::mix(Gfx::BG_BOTTOM, [200, 200, 255], 0.5));        $scale = 3.0 + $this->scorePulse * 0.9;        $font->draw($r, (string)$this->score, 135, 122 - $this->scorePulse * 2, $scale, Gfx::mix([255, 226, 122], [255, 255, 255], $this->scorePulse), 'center', [8, 8, 24]);        foreach ($this->pops as [$px, $py, $age, $val, $rgb]) {            $font->draw($r, '+' . $val, $px, $py - $age * 38, 1.9, Gfx::mix($rgb, [255, 255, 255], 0.5), 'center', [8, 8, 24]);        }        foreach ($this->world->balls() as $ball) {            $this->drawBall($r, $ball);        }    }}