227 lines
8.9 KiB
PHP
227 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit\Game;
|
|
|
|
use App\Game\Entities\Boss;
|
|
use App\Game\Entities\Bullet;
|
|
use App\Game\Entities\Zombie;
|
|
use App\Game\GameEngine;
|
|
use App\Game\InputState;
|
|
use App\Game\Player;
|
|
use App\Game\Room;
|
|
use App\Game\Spawner;
|
|
use App\Game\Vector2;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class GameEngineTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function tick_does_nothing_when_not_playing(): void
|
|
{
|
|
$room = new Room('test');
|
|
$engine = new GameEngine($room, new Spawner);
|
|
$engine->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);
|
|
}
|
|
}
|