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 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); } } }