Files
zombie-game/app/Game/Player.php
T
2026-09-07 20:37:40 +02:00

311 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Game;
use App\Game\Entities\Bullet;
use App\Game\Entities\Zombie;
final class Player extends GameObject
{
public InputState $input;
private EntityList $container;
private Room $room;
public float $moveSpeed;
public float $fireRate;
public float $recoilSpeed;
public float $recoilDamping;
public float $bulletSpeed;
public float $bulletSize;
public int $bulletCount;
public float $bulletSpread;
public int $damage;
private float $baseFireRate;
private int $baseDamage;
private int $baseBulletCount;
private float $baseBulletSpeed;
public int $maxUpgradeLevel;
/** @var array<string, int> */
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;
public bool $autoAim = 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;
$autoAimActive = $this->input->autoAim || $this->autoAim;
$hasTarget = false;
if ($autoAimActive) {
$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 && ! $autoAimActive) {
// Raw normalized direction from the client when not auto-aiming.
$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 || ($autoAimActive && $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;
}
}