*/ public array $upgrades = ['rapid' => 0, 'damage' => 0, 'multishot' => 0, 'speed' => 0]; public float $size; public string $name; public string $id; public int $health; public int $maxHealth; public int $score = 0; public bool $dead = false; private float $cooldown = 0.0; private Vector2 $aimDirection; private Vector2 $recoil; private float $damageCooldown = 0.0; private float $time = 0.0; public function __construct( string $playerId, string $name, InputState $input, EntityList $container, Room $room, array $config = [], ) { parent::__construct( $config['x'] ?? $room->width / 2.0, $config['y'] ?? $room->height / 2.0, ); $this->input = $input; $this->container = $container; $this->room = $room; $this->id = $playerId; $this->name = $name; $this->moveSpeed = $config['moveSpeed'] ?? 420.0; $this->fireRate = $config['fireRate'] ?? 0.2; $this->recoilSpeed = $config['recoilSpeed'] ?? 100.0; $this->recoilDamping = $config['recoilDamping'] ?? 15.0; $this->bulletSpeed = $config['bulletSpeed'] ?? 800.0; $this->bulletSize = $config['bulletSize'] ?? 8.0; $this->bulletCount = $config['bulletCount'] ?? 1; $this->bulletSpread = $config['bulletSpread'] ?? 0.09; $this->damage = $config['damage'] ?? 1; $this->baseFireRate = $this->fireRate; $this->baseDamage = $this->damage; $this->baseBulletCount = $this->bulletCount; $this->baseBulletSpeed = $this->bulletSpeed; $this->maxUpgradeLevel = $config['maxUpgradeLevel'] ?? 3; $this->health = $config['health'] ?? 3; $this->maxHealth = $this->health; $this->size = $config['size'] ?? 24.0; $this->width = $this->size; $this->height = $this->size; $this->aimDirection = new Vector2(1.0, 0.0); $this->recoil = Vector2::zero(); } public function kind(): string { return 'player'; } public function update(float $dt): void { $this->time += $dt; $movement = new Vector2(0.0, 0.0); $keys = $this->input->keys; if (in_array('w', $keys, true) || in_array('arrowup', $keys, true)) { $movement = new Vector2($movement->x, $movement->y - 1.0); } if (in_array('s', $keys, true) || in_array('arrowdown', $keys, true)) { $movement = new Vector2($movement->x, $movement->y + 1.0); } if (in_array('a', $keys, true) || in_array('arrowleft', $keys, true)) { $movement = new Vector2($movement->x - 1.0, $movement->y); } if (in_array('d', $keys, true) || in_array('arrowright', $keys, true)) { $movement = new Vector2($movement->x + 1.0, $movement->y); } $moveVelocity = Vector2::zero(); if ($movement->lengthSquared() > 0.0) { $moveVelocity = $movement->normalize()->mulS($this->moveSpeed); } $this->velocity = $this->recoil->lengthSquared() > 1.0 ? $moveVelocity->addV($this->recoil) : $moveVelocity; parent::update($dt); $maxX = max($this->width / 2.0, 0.0); $maxY = max($this->height / 2.0, 0.0); $this->pos = new Vector2( min(max($this->pos->x, $maxX), $this->room->width - $maxX), min(max($this->pos->y, $maxY), $this->room->height - $maxY), ); $aim = $this->input->aim; $hasTarget = false; $nearest = $this->nearestZombie(); if ($nearest !== null) { $hasTarget = true; $toEnemy = $nearest->pos->subV($this->pos); if ($toEnemy->lengthSquared() > 1.0) { $aim = $toEnemy; } } if ($aim->lengthSquared() > 1.0) { $this->aimDirection = $aim->divS($aim->length()); } elseif ($aim->lengthSquared() > 0.01 && $this->input->aim !== null) { // Use the raw client direction when no auto-aim target is near. $this->aimDirection = $aim->normalize(); } $this->recoil = $this->recoil->mulS(max(0.0, 1 - $dt * $this->recoilDamping)); $this->damageCooldown = max(0.0, $this->damageCooldown - $dt); $this->cooldown -= $dt; if ($this->cooldown <= 0.0 && ($this->input->fire || $hasTarget)) { $this->cooldown = $this->fireRate; $this->shoot(); } } public function nearestZombie(): ?Zombie { $best = null; $bestDistSq = INF; foreach ($this->container as $obj) { if (! $obj instanceof Zombie) { continue; } $delta = $obj->pos->subV($this->pos); $distSq = $delta->lengthSquared(); if ($distSq < $bestDistSq) { $bestDistSq = $distSq; $best = $obj; } } return $best; } public function shoot(): void { $aimAngle = atan2($this->aimDirection->y, $this->aimDirection->x); $this->recoil = $this->aimDirection->mulS(-$this->recoilSpeed); $offset = (($this->bulletCount - 1) / 2.0) * $this->bulletSpread; for ($i = 0; $i < $this->bulletCount; $i++) { $angle = $aimAngle - $offset + $i * $this->bulletSpread; $direction = new Vector2(cos($angle), sin($angle)); $bullet = new Bullet( $this->pos->x, $this->pos->y, $direction, $this->container, $this->room, $this, $this->bulletSpeed, $this->bulletSize, $this->damage, ); $this->container->append($bullet); } } public function applyUpgrade(string $type): bool { if (! array_key_exists($type, $this->upgrades)) { return false; } if ($this->upgrades[$type] >= $this->maxUpgradeLevel) { return false; } $this->upgrades[$type] += 1; switch ($type) { case 'rapid': $this->fireRate = max(0.06, $this->fireRate * 0.8); break; case 'damage': $this->damage += 1; break; case 'multishot': $this->bulletCount = min(7, $this->bulletCount + 1); break; case 'speed': $this->bulletSpeed = min(1800.0, $this->bulletSpeed * 1.15); break; } return true; } public function toSnapshot(): array { return [ 'id' => $this->id, 'name' => $this->name, 'x' => round($this->pos->x, 1), 'y' => round($this->pos->y, 1), 'aimX' => round($this->aimDirection->x, 3), 'aimY' => round($this->aimDirection->y, 3), 'hp' => $this->health, 'mhp' => $this->maxHealth, 'score' => $this->score, 'dead' => $this->dead, 'host' => $this->room->hostId === $this->id, 'up' => $this->upgrades, ]; } /** Reset per-round state back to lobby defaults (keeps id/name/host). */ public function resetForRound(): void { $this->pos = new Vector2($this->room->width / 2.0, $this->room->height / 2.0); $this->velocity = Vector2::zero(); $this->recoil = Vector2::zero(); $this->aimDirection = new Vector2(1.0, 0.0); $this->health = $this->maxHealth; $this->dead = false; $this->score = 0; $this->upgrades = ['rapid' => 0, 'damage' => 0, 'multishot' => 0, 'speed' => 0]; $this->fireRate = $this->baseFireRate; $this->bulletSpeed = $this->baseBulletSpeed; $this->bulletCount = $this->baseBulletCount; $this->damage = $this->baseDamage; $this->cooldown = 0.0; $this->damageCooldown = 0.0; $this->time = 0.0; } public function takeDamage(): void { if ($this->damageCooldown > 0.0) { return; } $this->damageCooldown = 0.5; $this->health -= 1; if ($this->health <= 0 && ! $this->dead) { $this->dead = true; } } public function addScore(int $points): void { $this->score += $points; } }