room->state !== Room::STATE_PLAYING) { return; } $this->room->elapsed += $dt; $this->room->bossCooldown = max(0.0, $this->room->bossCooldown - $dt); $this->assignTargets(); $this->updateEntities($dt); $this->updateSpawns($dt); $this->updatePickupTidy($dt); $this->resolveBulletHits(); $this->resolveProjectileHits(); $this->resolvePlayerContact(); $this->resolvePickups(); } private function assignTargets(): void { $alive = $this->room->alivePlayers(); if ($alive === []) { return; } foreach ($this->room->entities as $entity) { if (! $entity instanceof Zombie) { continue; } $entity->target = $this->nearestPlayer($entity, $alive); } } /** @param array $alive */ private function nearestPlayer(Zombie $zombie, array $alive): ?Player { $best = null; $bestDist = INF; foreach ($alive as $player) { $dist = $player->pos->subV($zombie->pos)->lengthSquared(); if ($dist < $bestDist) { $bestDist = $dist; $best = $player; } } return $best; } private function updateEntities(float $dt): void { // Players first so bullets spawn before resolution this frame. foreach ($this->room->players as $player) { if (! $player->dead) { $player->update($dt); } } foreach ($this->room->entities->copy() as $entity) { if ($entity instanceof Player) { continue; } $entity->update($dt); } } private function updateSpawns(float $dt): void { $this->room->spawnTimer += $dt; $interval = $this->spawner->spawnInterval($this->room->elapsed); if ($this->room->spawnTimer >= $interval) { $this->room->spawnTimer = 0.0; $zombieCount = 0; foreach ($this->room->entities as $entity) { if ($entity instanceof Zombie) { $zombieCount++; } } if ($zombieCount < self::MAX_ZOMBIES) { $this->spawner->spawnZombie($this->room); } } } private function updatePickupTidy(float $dt): void { foreach ($this->room->entities->copy() as $pickup) { if (! $pickup instanceof Pickup) { continue; } $anyNeeds = false; foreach ($this->room->alivePlayers() as $player) { if (($player->upgrades[$pickup->type] ?? 0) < $player->maxUpgradeLevel) { $anyNeeds = true; break; } } if (! $anyNeeds) { $pickup->removeFrom($this->room->entities); } } $this->room->pickupTimer -= $dt; if ($this->room->pickupTimer <= 0.0) { $this->room->pickupTimer = self::PICKUP_INTERVAL; if ($this->room->alivePlayers() !== []) { $this->spawner->spawnPickup($this->room); } } } private function resolveBulletHits(): void { foreach ($this->room->entities->copy() as $bullet) { if (! $bullet instanceof Bullet) { continue; } foreach ($this->room->entities->copy() as $zombie) { if (! $zombie instanceof Zombie || ! $bullet->intersects($zombie)) { continue; } $impulse = $zombie->pos->subV($bullet->pos); $impulse = $impulse->lengthSquared() > 1.0 ? $impulse->divS($impulse->length()) : new Vector2(1.0, 0.0); $bullet->removeFrom($this->room->entities); $this->room->pushEvent('bullethit', [ 'x' => $bullet->pos->x, 'y' => $bullet->pos->y, 'color' => array_slice($zombie->baseColor, 0, 3), ]); if (! $zombie->hit($impulse, $bullet->damage)) { break; } $zombie->die(); $owner = $bullet->owner; if (! $owner->dead) { $owner->addScore($zombie->scoreValue); } $killColor = array_slice($zombie->baseColor, 0, 3); $this->room->pushEvent('kill', [ 'x' => $zombie->pos->x, 'y' => $zombie->pos->y, 'color' => $killColor, 'boss' => $zombie instanceof Boss, 'big' => $zombie->maxHealth > 1, ]); if ($zombie instanceof Boss) { $this->room->nextBossScore *= 2.0; $this->room->bossCooldown = 60.0; $this->room->pushEvent('bossdefeated', [ 'x' => $zombie->pos->x, 'y' => $zombie->pos->y, ]); } elseif ( $owner->score >= $this->room->nextBossScore && $this->room->bossCooldown <= 0.0 && ! $this->room->hasBoss() ) { $this->spawner->spawnBoss($this->room); } break; } } } private function resolveProjectileHits(): void { foreach ($this->room->entities->copy() as $projectile) { if (! $projectile instanceof WitchProjectile) { continue; } foreach ($this->room->alivePlayers() as $player) { if (! $projectile->intersects($player)) { continue; } $projectile->removeFrom($this->room->entities); $player->takeDamage(); $this->room->pushEvent('playerdamage', [ 'x' => $projectile->pos->x, 'y' => $projectile->pos->y, 'id' => $player->id, ]); if ($player->dead) { $this->room->pushEvent('playerdeath', [ 'x' => $player->pos->x, 'y' => $player->pos->y, 'id' => $player->id, ]); $player->removeFrom($this->room->entities); $this->checkGameOver(); } } } } private function resolvePlayerContact(): void { foreach ($this->room->entities->copy() as $zombie) { if (! $zombie instanceof Zombie) { continue; } foreach ($this->room->alivePlayers() as $player) { if (! $zombie->intersects($player)) { continue; } if (! $zombie instanceof Boss && ! $zombie instanceof Witch) { $zombie->removeFrom($this->room->entities); $this->room->pushEvent('bloodpool', [ 'x' => $zombie->pos->x, 'y' => $zombie->pos->y, 's' => $zombie->size, ]); } $player->takeDamage(); $this->room->pushEvent('playerdamage', [ 'x' => $player->pos->x, 'y' => $player->pos->y, 'id' => $player->id, ]); if ($player->dead) { $this->room->pushEvent('playerdeath', [ 'x' => $player->pos->x, 'y' => $player->pos->y, 'id' => $player->id, ]); $player->removeFrom($this->room->entities); $this->checkGameOver(); } } } } private function resolvePickups(): void { foreach ($this->room->entities->copy() as $pickup) { if (! $pickup instanceof Pickup) { continue; } foreach ($this->room->alivePlayers() as $player) { if (! $this->pickupIntersects($pickup, $player)) { continue; } $maxed = ($player->upgrades[$pickup->type] ?? 0) >= $player->maxUpgradeLevel; if ($maxed) { $this->room->pushEvent('pickupmax', [ 'x' => $player->pos->x, 'y' => $player->pos->y, 'type' => $pickup->type, 'id' => $player->id, ]); continue; } $pickup->removeFrom($this->room->entities); $player->applyUpgrade($pickup->type); $this->room->pushEvent('pickup', [ 'x' => $pickup->pos->x, 'y' => $pickup->pos->y, 'type' => $pickup->type, 'id' => $player->id, ]); break; } } } private function pickupIntersects(Pickup $pickup, Player $player): bool { $dx = $pickup->pos->x - $player->pos->x; $dy = $pickup->pos->y - $player->pos->y; return $dx * $dx + $dy * $dy <= $pickup->collectRadius * $pickup->collectRadius || $pickup->intersects($player); } private function checkGameOver(): void { if ($this->room->alivePlayers() !== []) { return; } $this->room->state = Room::STATE_OVER; foreach ($this->room->entities->copy() as $entity) { if ($entity instanceof Zombie) { $entity->removeFrom($this->room->entities); } } $this->room->pushEvent('gameover'); } }