Files
zombie-game/app/Game/Spawner.php
T
2026-09-08 22:33:51 +02:00

332 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Game;
use App\Game\Entities\Boss;
use App\Game\Entities\Leaper;
use App\Game\Entities\MegaTank;
use App\Game\Entities\MegaWitch;
use App\Game\Entities\Pickup;
use App\Game\Entities\Witch;
use App\Game\Entities\Zombie;
final class Spawner
{
public function spawnInterval(float $elapsed): float
{
return max(0.22, 1.0 - $elapsed * 0.01);
}
public function speedMultiplier(float $elapsed): float
{
return min(2.6, 1.0 + $elapsed * 0.006);
}
private function tankChance(float $elapsed): float
{
return min(0.55, 0.18 + $elapsed * 0.0025);
}
private function megaTankChance(float $elapsed): float
{
return min(0.3, $elapsed * 0.002);
}
private function runnerChance(float $elapsed): float
{
return min(0.45, 0.22 + $elapsed * 0.0015);
}
private function leaperChance(float $elapsed): float
{
return min(0.16, 0.05 + $elapsed * 0.08);
}
private function witchChance(float $elapsed): float
{
return min(0.35, 0.12 + $elapsed * 0.0025);
}
private function megaWitchChance(float $elapsed): float
{
return $elapsed < 40.0 ? 0.0 : min(0.16, ($elapsed - 40.0) * 0.001);
}
public function spawnZombie(Room $room): void
{
$elapsed = $room->elapsed;
$width = $room->width;
$height = $room->height;
$speedMultiplier = $this->speedMultiplier($elapsed);
$isMegaTank = $this->chance($this->megaTankChance($elapsed));
$isMegaWitch = ! $isMegaTank && $this->chance($this->megaWitchChance($elapsed));
$isWitch = ! $isMegaTank && ! $isMegaWitch && $this->chance($this->witchChance($elapsed));
$isRunner = ! $isMegaTank && ! $isMegaWitch && ! $isWitch && $this->chance($this->runnerChance($elapsed));
$isLeaper = ! $isMegaTank && ! $isMegaWitch && ! $isWitch && ! $isRunner && $this->chance($this->leaperChance($elapsed));
$isTank = ! $isMegaTank && ! $isMegaWitch && ! $isWitch && ! $isRunner && ! $isLeaper && $this->chance($this->tankChance($elapsed));
$size = $isMegaWitch ? 52.0 : ($isWitch ? 30.0 : ($isRunner ? 14.0 : ($isLeaper ? 28.0 : ($isTank ? 36.0 : 24.0))));
[$x, $y] = $this->edgePosition($room, $size);
if ($isMegaTank) {
$room->entities->append(new MegaTank($x, $y, $room->entities, $room, $this->initialTarget($room), [
'speed' => 28.0 + $this->rand() * 12.0,
'size' => 64.0,
'health' => 18,
'knockback' => 90.0,
'scoreValue' => 15,
'spawnCount' => 3,
'color' => [0.55, 0.28, 0.12, 1.0],
'spawnOnDeath' => function (float $sx, float $sy, EntityList $container, Room $sroom, ?Player $starget): void {
$container->append(new Zombie($sx, $sy, $container, $sroom, [
'speed' => (70.0 + $this->rand() * 60.0) * $this->speedMultiplier($sroom->elapsed),
'size' => 24.0,
'health' => 1,
'knockback' => 260.0,
'scoreValue' => 1,
'color' => [0.15, 0.65, 0.2, 1.0],
'emergeTime' => 0.8,
], fn (Zombie $z) => $z->target = $starget));
$sroom->pushEvent('portal', ['x' => $sx, 'y' => $sy, 'color' => [0.15, 0.65, 0.2, 1.0], 'scale' => 0.8]);
},
], null));
$room->pushEvent('portal', ['x' => $x, 'y' => $y, 'color' => [0.55, 0.28, 0.12, 1.0], 'scale' => 2.2]);
return;
}
if ($isMegaWitch) {
$megaPower = min(1.0, max(0.0, ($elapsed - 45.0) / 90.0));
$this->spawnMegaWitch($room, $x, $y, $speedMultiplier, $megaPower);
return;
}
if ($isWitch) {
$this->spawnWitch($room, $x, $y, $speedMultiplier);
return;
}
if ($isRunner) {
$room->entities->append(new Zombie($x, $y, $room->entities, $room, [
'speed' => (180.0 + $this->rand() * 60.0) * $speedMultiplier,
'size' => $size,
'health' => 1,
'knockback' => 380.0,
'scoreValue' => 2,
'color' => [0.85, 0.1, 0.2, 1.0],
], fn (Zombie $z) => $z->target = $this->initialTarget($room)));
$room->pushEvent('portal', ['x' => $x, 'y' => $y, 'color' => [0.85, 0.1, 0.2, 1.0], 'scale' => 0.9]);
return;
}
if ($isLeaper) {
$this->spawnLeaper($room, $x, $y, $speedMultiplier);
return;
}
$zombieColor = $isTank ? [0.3, 0.35, 0.15, 1.0] : [0.15, 0.65, 0.2, 1.0];
$room->entities->append(new Zombie($x, $y, $room->entities, $room, [
'speed' => ($isTank ? 45.0 : 80.0 + $this->rand() * 60.0) * $speedMultiplier,
'size' => $size,
'health' => $isTank ? 4 : 1,
'knockback' => $isTank ? 160.0 : 260.0,
'scoreValue' => $isTank ? 3 : 1,
'color' => $zombieColor,
], fn (Zombie $z) => $z->target = $this->initialTarget($room)));
$room->pushEvent('portal', ['x' => $x, 'y' => $y, 'color' => $zombieColor, 'scale' => $isTank ? 1.5 : 1.0]);
}
public function spawnWitch(Room $room, float $x, float $y, float $speedMultiplier = 1.0, int $emergeTime = 0): void
{
$room->entities->append(new Witch($x, $y, $room->entities, $room, $this->initialTarget($room), [
'speed' => (70.0 + $this->rand() * 40.0) * $speedMultiplier,
'size' => 30.0,
'health' => 5,
'knockback' => 180.0,
'scoreValue' => 4,
'color' => [0.55, 0.2, 0.8, 1.0],
'preferredDistance' => 220.0,
'fireInterval' => 2.2,
'projectileSpeed' => 220.0,
'emergeTime' => $emergeTime,
], fn (Zombie $z) => $z->target = $this->initialTarget($room)));
$room->pushEvent('portal', ['x' => $x, 'y' => $y, 'color' => [0.55, 0.2, 0.8, 1.0], 'scale' => 1.2]);
}
public function spawnMegaWitch(Room $room, float $x, float $y, float $speedMultiplier = 1.0, float $power = 1.0): void
{
$room->entities->append(new MegaWitch($x, $y, $room->entities, $room, $this->initialTarget($room), [
'speed' => 55.0 * $speedMultiplier,
'size' => 52.0,
'health' => (int) round(8.0 + 14.0 * $power),
'knockback' => 130.0,
'scoreValue' => 12,
'color' => [0.8, 0.15, 0.7, 1.0],
'preferredDistance' => 240.0,
'fireInterval' => 2.0,
'projectileSpeed' => 240.0,
'projectileCount' => 3,
'projectileSpread' => 0.35,
'projectileColor' => [1.0, 0.3, 0.3, 1.0],
'novaInterval' => 6.0 - $power,
'novaCount' => (int) round(10.0 + 6.0 * $power),
'novaProjectileSpeed' => 210.0 + 40.0 * $power,
'spawnCount' => 3,
'spawnOnDeath' => function (float $sx, float $sy, EntityList $container, Room $sroom, ?Player $starget): void {
$container->append(new Witch($sx, $sy, $container, $sroom, $starget, [
'speed' => 70.0 + $this->rand() * 40.0,
'size' => 30.0,
'health' => 5,
'knockback' => 180.0,
'scoreValue' => 4,
'color' => [0.55, 0.2, 0.8, 1.0],
'preferredDistance' => 220.0,
'fireInterval' => 2.2,
'projectileSpeed' => 220.0,
'emergeTime' => 1.1,
], fn (Zombie $z) => $z->target = $starget));
$sroom->pushEvent('portal', ['x' => $sx, 'y' => $sy, 'color' => [0.55, 0.2, 0.8, 1.0], 'scale' => 1.2]);
},
], null));
$room->pushEvent('portal', ['x' => $x, 'y' => $y, 'color' => [0.8, 0.15, 0.7, 1.0], 'scale' => 2.2]);
}
public function spawnLeaper(Room $room, float $x, float $y, float $speedMultiplier = 1.0, int $emergeTime = 0): void
{
$room->entities->append(new Leaper($x, $y, $room->entities, $room, $this->initialTarget($room), [
'speed' => (80.0 + $this->rand() * 40.0) * $speedMultiplier,
'size' => 28.0,
'health' => 10,
'knockback' => 240.0,
'scoreValue' => 6,
'color' => [0.15, 0.25, 0.75, 1.0],
'dashRange' => 300.0,
'windupTime' => 0.35,
'dashTime' => 0.28,
'dashSpeed' => 680.0,
'dashCooldown' => 1.8,
'emergeTime' => $emergeTime,
], fn (Zombie $z) => $z->target = $this->initialTarget($room)));
$room->pushEvent('portal', ['x' => $x, 'y' => $y, 'color' => [0.15, 0.25, 0.75, 1.0], 'scale' => 1.2]);
}
public function spawnBoss(Room $room): void
{
$width = $room->width;
$witchCount = min(4, 2 ** $room->bossCount);
$room->bossCount += 1;
$room->entities->append(new Boss($width / 2.0, -80.0, $room->entities, $room, $this->initialTarget($room), [
'speed' => 36.0 + $this->rand() * 10.0,
'size' => 90.0,
'health' => 80,
'knockback' => 40.0,
'scoreValue' => 50,
'color' => [0.45, 0.05, 0.1, 1.0],
'spawnInterval' => 5.0,
'spawnCount' => $witchCount,
'spawnZombie' => function (float $sx, float $sy, EntityList $container, Room $sroom, ?Player $starget): void {
if ($this->chance(0.5)) {
$container->append(new Witch($sx, $sy, $container, $sroom, $starget, [
'speed' => (70.0 + $this->rand() * 40.0) * $this->speedMultiplier($sroom->elapsed),
'size' => 30.0,
'health' => 5,
'knockback' => 180.0,
'scoreValue' => 4,
'color' => [0.55, 0.2, 0.8, 1.0],
'preferredDistance' => 220.0,
'fireInterval' => 2.2,
'projectileSpeed' => 220.0,
'emergeTime' => 0.9,
], fn (Zombie $z) => $z->target = $starget));
} else {
$container->append(new Leaper($sx, $sy, $container, $sroom, $starget, [
'speed' => (80.0 + $this->rand() * 40.0) * $this->speedMultiplier($sroom->elapsed),
'size' => 28.0,
'health' => 3,
'knockback' => 240.0,
'scoreValue' => 6,
'color' => [0.15, 0.25, 0.75, 1.0],
'dashRange' => 300.0,
'windupTime' => 0.35,
'dashTime' => 0.28,
'dashSpeed' => 680.0,
'dashCooldown' => 1.8,
'emergeTime' => 0.9,
], fn (Zombie $z) => $z->target = $starget));
}
},
], fn (Zombie $z) => $z->target = $this->initialTarget($room)));
$room->pushEvent('portal', ['x' => $width / 2.0, 'y' => -80.0, 'color' => [0.45, 0.05, 0.1, 1.0], 'scale' => 3.5]);
$room->pushEvent('boss', [
'x' => $width / 2.0,
'y' => $room->height / 2.0,
'title' => 'BOSS APPROACHES',
]);
}
public function spawnPickup(Room $room): void
{
$available = [];
foreach (array_keys(Pickup::TYPES) as $type) {
$needed = false;
foreach ($room->alivePlayers() as $player) {
if (($player->upgrades[$type] ?? 0) < $player->maxUpgradeLevel) {
$needed = true;
break;
}
}
if ($needed) {
$available[] = $type;
}
}
if ($available === []) {
return;
}
$margin = 60.0;
$x = $margin + $this->rand() * ($room->width - $margin * 2.0);
$y = $margin + $this->rand() * ($room->height - $margin * 2.0);
$type = $available[(int) floor($this->rand() * count($available))];
$room->entities->append(new Pickup($x, $y, $type, $room->entities, $room));
}
/** @return array{0:float, 1:float} */
private function edgePosition(Room $room, float $size): array
{
$width = $room->width;
$height = $room->height;
$edge = (int) floor($this->rand() * 4.0);
return match ($edge) {
0 => [$this->rand() * $width, -$size],
1 => [$this->rand() * $width, $height + $size],
2 => [-$size, $this->rand() * $height],
default => [$width + $size, $this->rand() * $height],
};
}
private function initialTarget(Room $room): ?Player
{
$alive = $room->alivePlayers();
return $alive === [] ? null : $alive[array_rand($alive)];
}
private function chance(float $probability): bool
{
return $this->rand() < $probability;
}
private function rand(): float
{
return random_int(0, 1000000) / 1000000.0;
}
}