311 lines
9.2 KiB
PHP
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;
|
|
}
|
|
}
|