tick(1.0 / 60.0); $this->assertSame(Room::STATE_LOBBY, $room->state); } #[Test] public function zombies_spawn_during_play(): void { $room = new Room('test'); $room->state = Room::STATE_PLAYING; $engine = new GameEngine($room, new Spawner); $room->addPlayer(new Player('p1', 'Tester', new InputState(fire: true), $room->entities, $room, ['x' => 960.0, 'y' => 540.0])); $sawZombie = false; for ($i = 0; $i < 600; $i++) { $engine->tick(1.0 / 60.0); if ($room->entities->findByKind('zombie') !== []) { $sawZombie = true; } if ($room->state === Room::STATE_OVER) { break; } } $this->assertTrue($sawZombie, 'zombies should spawn during play'); } #[Test] public function game_ends_when_all_players_die(): void { $room = new Room('test', 1); $room->state = Room::STATE_PLAYING; $engine = new GameEngine($room, new Spawner); $player = new Player('p1', 'Tester', new InputState, $room->entities, $room, [ 'x' => 960.0, 'y' => 540.0, 'health' => 1, ]); $room->addPlayer($player); $zombie = new Zombie(944.4, 540.0, $room->entities, $room, ['emergeTime' => 0.0]); $zombie->target = $player; $room->entities->append($zombie); $engine->tick(1.0 / 60.0); $this->assertSame(Room::STATE_OVER, $room->state); $this->assertTrue($player->dead); } #[Test] public function bullets_damage_zombies(): void { $room = new Room('test'); $room->state = Room::STATE_PLAYING; $engine = new GameEngine($room, new Spawner); $input = new InputState(fire: true); $player = new Player('p1', 'T', $input, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]); $room->addPlayer($player); $zombie = new Zombie(1000.0, 500.0, $room->entities, $room, ['emergeTime' => 0.0]); $zombie->target = $player; $room->entities->append($zombie); $engine->tick(1.0 / 60.0); $bullets = $room->entities->findByKind('bullet'); $this->assertNotEmpty($bullets, 'player shot a bullet'); } #[Test] public function boss_spawns_at_score_threshold(): void { $room = new Room('test', 1); $room->state = Room::STATE_PLAYING; $engine = new GameEngine($room, new Spawner); $player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]); $room->addPlayer($player); $zombie = new Zombie(510.0, 500.0, $room->entities, $room, ['health' => 1, 'emergeTime' => 0.0, 'scoreValue' => 55]); $zombie->target = $player; $room->entities->append($zombie); $bullet = new Bullet(509.0, 500.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 1); $room->entities->append($bullet); $engine->tick(1.0 / 60.0); $this->assertGreaterThan(0, $player->score, 'player killed zombie at score threshold'); $this->assertTrue($room->hasBoss(), 'boss should spawn when score reaches threshold'); } #[Test] public function kills_increment_score(): void { $room = new Room('test'); $room->state = Room::STATE_PLAYING; $engine = new GameEngine($room, new Spawner); $player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]); $room->addPlayer($player); $zombie = new Zombie(510.0, 500.0, $room->entities, $room, ['health' => 1, 'emergeTime' => 0.0]); $zombie->target = $player; $room->entities->append($zombie); $bullet = new Bullet(509.0, 500.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 10); $room->entities->append($bullet); $engine->tick(1.0 / 60.0); $this->assertGreaterThan(0, $player->score, 'player scored from killing zombie'); } #[Test] public function zombies_appear_when_megatank_dies(): void { $room = new Room('test'); $room->state = Room::STATE_PLAYING; $room->elapsed = 400.0; $spawner = new Spawner; $engine = new GameEngine($room, $spawner); $player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 960.0, 'y' => 540.0]); $room->addPlayer($player); $mega = null; for ($i = 0; $i < 100 && $mega === null; $i++) { $spawner->spawnZombie($room); $megas = $room->entities->findByKind('megatank'); if ($megas !== []) { $mega = $megas[0]; } } $this->assertNotNull($mega, 'a megatank should spawn when elapsed is high'); $mega->pos = new Vector2(960.0, 250.0); $zombiesBefore = count($room->entities->findByKind('zombie')); $portalsBefore = $this->portalEventCount($room); $room->entities->append(new Bullet(960.0, 250.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 100)); $engine->tick(1.0 / 60.0); $this->assertSame([], $room->entities->findByKind('megatank'), 'megatank should be removed on death'); $this->assertSame($zombiesBefore + 3, count($room->entities->findByKind('zombie')), 'megatank death should spawn three zombies'); $this->assertSame($portalsBefore + 3, $this->portalEventCount($room), 'each spawned zombie should get a portal effect'); $this->assertContains('kill', array_map(fn (array $ev) => $ev['k'], $room->events)); } #[Test] public function witches_appear_when_mega_witch_dies(): void { $room = new Room('test'); $room->state = Room::STATE_PLAYING; $spawner = new Spawner; $engine = new GameEngine($room, $spawner); $player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 960.0, 'y' => 540.0]); $room->addPlayer($player); $spawner->spawnMegaWitch($room, 960.0, 250.0, 1.0, 1.0); $this->assertSame(1, count($room->entities->findByKind('megawitch'))); $portalsBefore = $this->portalEventCount($room); $mega = $room->entities->findByKind('megawitch')[0]; $room->entities->append(new Bullet($mega->pos->x, $mega->pos->y, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 100)); $engine->tick(1.0 / 60.0); $this->assertSame([], $room->entities->findByKind('megawitch')); $this->assertCount(3, $room->entities->findByKind('witch')); $this->assertSame($portalsBefore + 3, $this->portalEventCount($room)); } #[Test] public function emerging_minions_report_emerge_progress_in_snapshot(): void { $room = new Room('test'); $zombie = new Zombie(10.0, 10.0, $room->entities, $room, ['emergeTime' => 0.8]); $snap = $zombie->toSnapshot(); $this->assertSame(0.8, $snap['er']); $this->assertSame(0.8, $snap['et']); $zombie->update(0.3); $this->assertSame(0.5, $zombie->toSnapshot()['er'], 'emerge remaining should shrink as the zombie emerges'); $plain = new Zombie(30.0, 30.0, $room->entities, $room); $this->assertArrayNotHasKey('er', $plain->toSnapshot(), 'non-emerging entities need no emerge data'); } private function portalEventCount(Room $room): int { $count = 0; foreach ($room->events as $event) { if ($event['k'] === 'portal') { $count++; } } return $count; } #[Test] public function boss_defeated_event_emitted_on_boss_death(): void { $room = new Room('test'); $room->state = Room::STATE_PLAYING; $engine = new GameEngine($room, new Spawner); $player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]); $room->addPlayer($player); $boss = new Boss(505.0, 500.0, $room->entities, $room, $player, [ 'health' => 1, 'emergeTime' => 0.0, ]); $boss->target = $player; $room->entities->append($boss); $bullet = new Bullet(503.0, 500.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 10); $room->entities->append($bullet); $engine->tick(1.0 / 60.0); $eventKinds = array_map(fn (array $ev) => $ev['k'], $room->events); $this->assertContains('bossdefeated', $eventKinds); } }