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; } }