332 lines
13 KiB
PHP
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;
|
|
}
|
|
}
|