<?phpnamespace Tests;use App\Render\ZoomView;use App\Scene\ZoomRingsScene;use JumPHP\Physics2d\Ring;use JumPHP\Physics2d\World;use JumPHP\Testing\GameTestCase;/** The infinite zoom: the camera, and the rings that go on for ever. */final class ZoomTest extends GameTestCase{ public function testTheCameraGlidesInLogSpaceWithAnEase(): void { $view = new ZoomView(0, 0, 135, 240); $view->zoomTo(0.25, 1.0); $view->update(0.5); self::assertEqualsWithDelta(0.5, $view->scale, 1e-9, 'half way in time is the geometric mean: x1 -> x0.25 passes x0.5'); $view->update(0.1); self::assertGreaterThan(0.25, $view->scale); self::assertTrue($view->isZooming()); $view->update(1.0); self::assertSame(0.25, $view->scale); self::assertFalse($view->isZooming()); self::assertEqualsWithDelta(135.0, $view->x(0), 1e-9); self::assertEqualsWithDelta(10.0, $view->len(40), 1e-9); } public function testTheEaseStartsAndEndsSlowly(): void { self::assertSame(0.0, ZoomView::ease(0)); self::assertSame(1.0, ZoomView::ease(1)); self::assertLessThan(0.05, ZoomView::ease(0.2)); self::assertGreaterThan(0.95, ZoomView::ease(0.8)); } public function testAWorldFlaggedAsLeftARingForgetsItWhenTheRingIsRemoved(): void { $world = new World(); $world->gravity = 0; $ring = $world->addRing(new Ring(0, 0, 10, gap: 6.0)); $ball = $world->addBall(new \JumPHP\Physics2d\Ball(0, 0, 100, 0, 2)); $world->update(0.3); self::assertSame('escape', $world->hits()[0]['type']); $world->removeRing($ring); $again = $world->addRing(new Ring(0, 0, 100)); // PHP may give it the id of the one just freed $ball->x = 98; $ball->y = 0; $ball->vx = 100; $world->update(0.1); $types = array_column($world->hits(), 'type'); self::assertContains('ring', $types, 'the new ring is not skipped as one the ball already left'); self::assertNotNull($again); } public function testTheFirstEscapeAndZoomComeInTheHook(): void { foreach ([1, 2, 3, 4, 5] as $seed) { $this->game->start(ZoomRingsScene::class, ['seed' => $seed]); $this->advance(60 * 3); self::assertGreaterThanOrEqual(1, $this->game->peek('passed'), "seed $seed: out of the first ring in 3 s"); } } public function testTheBallGetsOutOfRingAfterRingAndNewRingsKeepComing(): void { $this->game->start(ZoomRingsScene::class, ['seed' => 3]); $world = $this->world(); $first = $world->rings()[0]; $this->game->run(20); $this->advance(60 * 9); self::assertGreaterThanOrEqual(3, $this->game->peek('passed')); self::assertNotContains($first, $world->rings(), 'the ring it left is gone'); self::assertCount(ZoomRingsScene::AHEAD, $world->rings(), 'and a new one was made outside'); $radii = array_map(fn (Ring $r) => $r->radius, $world->rings()); $min = min($radii); $view = $this->game->peek('view'); self::assertGreaterThan(ZoomRingsScene::R0 * 0.99, $view->len($min)); self::assertLessThan(ZoomRingsScene::R0 * ZoomRingsScene::RATIO * 1.01, $view->len($min), 'the ring it is in is about the same size on screen (the camera follows it)'); } public function testThePhysicsNeverSeesTheCameraAndTheBallKeepsItsSizeOnScreen(): void { $this->game->start(ZoomRingsScene::class, ['seed' => 3]); $view = $this->game->peek('view'); $sizes = []; $this->game->run(20); for ($i = 0; $i < 60; $i++) { $this->advance(10); $ball = $this->world()->balls()[0]; $sizes[] = $view->len($ball->radius); } foreach ($sizes as $s) { self::assertEqualsWithDelta(ZoomRingsScene::BALL, $s, 0.01); } self::assertGreaterThan(0, $this->game->peek('passed')); } public function testTheWorldIsRebasedSoTheNumbersStaySmallHoweverFarItGoes(): void { $this->game->start(ZoomRingsScene::class, ['seed' => 3]); $this->advance(60 * 20); $this->game->run(1); $passed = $this->game->peek('passed'); self::assertGreaterThanOrEqual(5, $passed); foreach ($this->world()->rings() as $ring) { self::assertLessThan(ZoomRingsScene::R0 * 12, $ring->radius); } $ball = $this->world()->balls()[0]; self::assertLessThan(2000, abs($ball->x - 135) + abs($ball->y - 250)); $this->game->shot('zoom'); } public function testEachPassageRingsABellAndBouncesPlayTheMarimba(): void { $this->game->start(ZoomRingsScene::class, ['seed' => 3]); $this->advance(60 * 6); $waves = array_count_values(array_column($this->recorder()->notes(), 'wave')); self::assertGreaterThanOrEqual($this->game->peek('passed'), $waves['bell'] ?? 0); self::assertGreaterThan(2, $waves['marimba'] ?? 0); } private function advance(int $frames): void { $scene = $this->game->scene(); for ($i = 0; $i < $frames; $i++) { $scene->update(1 / 60); } } private function world(): World { return $this->game->peek('world'); } private function recorder(): \JumPHP\Shorts\Recorder { return $this->game->peek('recorder'); }}