initial commit
This commit is contained in:
commit
e8c3f9862a
193 files changed
+29795
No files matched your search
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
|
||||
final class Boss extends Zombie
|
||||
{
|
||||
private float $spawnInterval;
|
||||
|
||||
private int $spawnCount;
|
||||
|
||||
private float $spawnTimer;
|
||||
|
||||
/** @var null|callable(float,float,EntityList,Room,?Player):void */
|
||||
private $spawnZombie;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
?Player $initialTarget,
|
||||
array $config = [],
|
||||
?callable $onDeath = null,
|
||||
) {
|
||||
parent::__construct($x, $y, $container, $room, $config, $onDeath);
|
||||
if ($initialTarget !== null) {
|
||||
$this->target = $initialTarget;
|
||||
}
|
||||
$this->spawnInterval = $config['spawnInterval'] ?? 5.0;
|
||||
$this->spawnCount = $config['spawnCount'] ?? 1;
|
||||
$this->spawnTimer = 3.0;
|
||||
$this->spawnZombie = $config['spawnZombie'] ?? null;
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'boss';
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
parent::update($dt);
|
||||
|
||||
$this->spawnTimer -= $dt;
|
||||
if ($this->spawnTimer > 0.0 || $this->spawnZombie === null) {
|
||||
return;
|
||||
}
|
||||
$this->spawnTimer = $this->spawnInterval;
|
||||
|
||||
$minions = 0;
|
||||
foreach ($this->container as $obj) {
|
||||
if ($obj instanceof Zombie && ! ($obj instanceof self)) {
|
||||
$minions++;
|
||||
}
|
||||
}
|
||||
if ($minions >= 15) {
|
||||
return;
|
||||
}
|
||||
for ($i = 0; $i < $this->spawnCount; $i++) {
|
||||
$angle = ($i / $this->spawnCount) * M_PI * 2.0 + $this->rand01() * 0.6;
|
||||
($this->spawnZombie)(
|
||||
$this->pos->x + cos($angle) * 70.0,
|
||||
$this->pos->y + sin($angle) * 70.0,
|
||||
$this->container,
|
||||
$this->room,
|
||||
$this->target,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
$data = parent::toSnapshot();
|
||||
$data['type'] = 'boss';
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\GameObject;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
use App\Game\Vector2;
|
||||
|
||||
final class Bullet extends GameObject
|
||||
{
|
||||
private EntityList $container;
|
||||
|
||||
private Room $room;
|
||||
|
||||
public float $size;
|
||||
|
||||
public int $damage;
|
||||
|
||||
/** @var array<float> */
|
||||
public array $color;
|
||||
|
||||
public Player $owner;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
Vector2 $direction,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
Player $owner,
|
||||
float $speed = 800.0,
|
||||
float $size = 8.0,
|
||||
int $damage = 1,
|
||||
) {
|
||||
parent::__construct($x, $y);
|
||||
$this->container = $container;
|
||||
$this->room = $room;
|
||||
$this->owner = $owner;
|
||||
$this->velocity = $direction->mulS($speed);
|
||||
$this->size = $size;
|
||||
$this->damage = $damage;
|
||||
$this->color = [1.0, 0.2, 0.2, 1.0];
|
||||
$this->width = $size;
|
||||
$this->height = $size;
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'bullet';
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
parent::update($dt);
|
||||
|
||||
if (
|
||||
$this->pos->x < -$this->size
|
||||
|| $this->pos->x > $this->room->width + $this->size
|
||||
|| $this->pos->y < -$this->size
|
||||
|| $this->pos->y > $this->room->height + $this->size
|
||||
) {
|
||||
$this->container->remove($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
return [
|
||||
'eid' => $this->eid,
|
||||
'd' => $this->kind(),
|
||||
'x' => round($this->pos->x, 1),
|
||||
'y' => round($this->pos->y, 1),
|
||||
'vx' => round($this->velocity->x, 1),
|
||||
'vy' => round($this->velocity->y, 1),
|
||||
's' => $this->size,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
use App\Game\Vector2;
|
||||
|
||||
final class Leaper extends Zombie
|
||||
{
|
||||
private float $dashRange;
|
||||
|
||||
private float $windupTime;
|
||||
|
||||
private float $dashTime;
|
||||
|
||||
private float $dashSpeed;
|
||||
|
||||
private float $dashCooldown;
|
||||
|
||||
private float $dashTimer = 0.0;
|
||||
|
||||
private float $windupTimer = 0.0;
|
||||
|
||||
private float $dashCooldownTimer = 0.0;
|
||||
|
||||
private Vector2 $dashDirection;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
?Player $initialTarget,
|
||||
array $config = [],
|
||||
?callable $onDeath = null,
|
||||
) {
|
||||
parent::__construct($x, $y, $container, $room, $config, $onDeath);
|
||||
if ($initialTarget !== null) {
|
||||
$this->target = $initialTarget;
|
||||
}
|
||||
$this->dashRange = $config['dashRange'] ?? 300.0;
|
||||
$this->windupTime = $config['windupTime'] ?? 0.35;
|
||||
$this->dashTime = $config['dashTime'] ?? 0.28;
|
||||
$this->dashSpeed = $config['dashSpeed'] ?? 680.0;
|
||||
$this->dashCooldown = $config['dashCooldown'] ?? 1.8;
|
||||
$this->dashDirection = new Vector2(1.0, 0.0);
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'leaper';
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
$this->time += $dt;
|
||||
if ($this->updateEmerge($dt)) {
|
||||
return;
|
||||
}
|
||||
$this->dashCooldownTimer = max(0.0, $this->dashCooldownTimer - $dt);
|
||||
|
||||
$target = $this->target;
|
||||
$delta = $target !== null ? $target->pos->subV($this->pos) : Vector2::zero();
|
||||
$distance = $delta->length();
|
||||
|
||||
$move = Vector2::zero();
|
||||
if ($this->windupTimer > 0.0) {
|
||||
$this->windupTimer -= $dt;
|
||||
if ($this->windupTimer <= 0.0) {
|
||||
$this->dashTimer = $this->dashTime;
|
||||
$this->dashDirection = $distance > 1.0 ? $delta->divS($distance) : new Vector2(1.0, 0.0);
|
||||
}
|
||||
} elseif ($this->dashTimer > 0.0) {
|
||||
$this->dashTimer -= $dt;
|
||||
$move = $this->dashDirection->mulS($this->dashSpeed);
|
||||
} elseif ($distance > 1.0) {
|
||||
if ($distance < $this->dashRange && $this->dashCooldownTimer <= 0.0) {
|
||||
$this->windupTimer = $this->windupTime;
|
||||
} else {
|
||||
$move = $delta->divS($distance)->mulS($this->speed);
|
||||
}
|
||||
}
|
||||
|
||||
$this->velocity = $move->addV($this->hitKnockback);
|
||||
$this->move($dt);
|
||||
|
||||
$this->hitKnockback = $this->hitKnockback->mulS(max(0.0, 1 - $dt * $this->knockbackDamping));
|
||||
$this->flash = max(0.0, $this->flash - $dt);
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
$data = parent::toSnapshot();
|
||||
$data['dash'] = $this->dashTimer > 0.0;
|
||||
$data['wind'] = $this->windupTimer > 0.0;
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
|
||||
final class MegaTank extends Zombie
|
||||
{
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
?Player $initialTarget,
|
||||
array $config = [],
|
||||
?callable $onDeath = null,
|
||||
) {
|
||||
$spawnCount = $config['spawnCount'] ?? 3;
|
||||
$spawnOnDeath = $config['spawnOnDeath'] ?? null;
|
||||
parent::__construct($x, $y, $container, $room, $config, $onDeath ?? function (Zombie $mega) use ($spawnCount, $spawnOnDeath): void {
|
||||
if ($spawnOnDeath === null) {
|
||||
return;
|
||||
}
|
||||
for ($i = 0; $i < $spawnCount; $i++) {
|
||||
$angle = ($i / $spawnCount) * M_PI * 2.0;
|
||||
$offset = $mega->size + 28.0;
|
||||
$spawnOnDeath(
|
||||
$mega->pos->x + cos($angle) * $offset,
|
||||
$mega->pos->y + sin($angle) * $offset,
|
||||
$mega->container,
|
||||
$mega->room,
|
||||
$mega->target,
|
||||
);
|
||||
}
|
||||
});
|
||||
if ($initialTarget !== null) {
|
||||
$this->target = $initialTarget;
|
||||
}
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'megatank';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
use App\Game\Vector2;
|
||||
|
||||
final class MegaWitch extends Witch
|
||||
{
|
||||
private float $novaInterval;
|
||||
|
||||
private int $novaCount;
|
||||
|
||||
private float $novaProjectileSpeed;
|
||||
|
||||
private float $novaTimer;
|
||||
|
||||
private float $novaRotation;
|
||||
|
||||
/** @var array<int, array{0:float, 1:\Closure}> */
|
||||
private array $pendingNova = [];
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
?Player $initialTarget,
|
||||
array $config = [],
|
||||
?callable $onDeath = null,
|
||||
) {
|
||||
$spawnCount = $config['spawnCount'] ?? 3;
|
||||
$spawnOnDeath = $config['spawnOnDeath'] ?? null;
|
||||
parent::__construct($x, $y, $container, $room, $initialTarget, $config, $onDeath ?? function (Zombie $mega) use ($spawnCount, $spawnOnDeath): void {
|
||||
if ($spawnOnDeath === null) {
|
||||
return;
|
||||
}
|
||||
for ($i = 0; $i < $spawnCount; $i++) {
|
||||
$angle = ($i / $spawnCount) * M_PI * 2.0;
|
||||
$offset = $mega->size + 30.0;
|
||||
$spawnOnDeath(
|
||||
$mega->pos->x + cos($angle) * $offset,
|
||||
$mega->pos->y + sin($angle) * $offset,
|
||||
$mega->container,
|
||||
$mega->room,
|
||||
$mega->target,
|
||||
);
|
||||
}
|
||||
});
|
||||
if ($initialTarget !== null) {
|
||||
$this->target = $initialTarget;
|
||||
}
|
||||
$this->novaInterval = $config['novaInterval'] ?? 5.0;
|
||||
$this->novaCount = $config['novaCount'] ?? 16;
|
||||
$this->novaProjectileSpeed = $config['novaProjectileSpeed'] ?? 250.0;
|
||||
$this->novaTimer = $this->novaInterval * 0.5;
|
||||
$this->novaRotation = $config['novaRotation'] ?? $this->rand01() * M_PI * 2.0;
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'megawitch';
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
parent::update($dt);
|
||||
|
||||
$target = $this->target;
|
||||
if ($target !== null && $target->dead) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_keys($this->pendingNova) as $i) {
|
||||
$this->pendingNova[$i][0] -= $dt;
|
||||
if ($this->pendingNova[$i][0] <= 0.0) {
|
||||
$closure = $this->pendingNova[$i][1];
|
||||
unset($this->pendingNova[$i]);
|
||||
$closure();
|
||||
}
|
||||
}
|
||||
|
||||
$this->novaTimer -= $dt;
|
||||
if ($this->novaTimer <= 0.0) {
|
||||
$this->novaTimer = $this->novaInterval * (0.85 + $this->rand01() * 0.3);
|
||||
$this->nova();
|
||||
}
|
||||
}
|
||||
|
||||
private function nova(): void
|
||||
{
|
||||
$lost = 1.0 - max(0.0, $this->health / max(1, $this->maxHealth));
|
||||
$count = max(6, (int) round($this->novaCount * (0.75 + 0.25 * $lost)));
|
||||
$speed = $this->novaProjectileSpeed * (1.0 + $lost * 0.45);
|
||||
$size = max(8.0, $this->size * 0.3);
|
||||
$color = [1.0, 0.5, 0.3, 1.0];
|
||||
$this->novaRotation += 0.36;
|
||||
|
||||
$fireRing = function (float $offset) use ($count, $speed, $size, $color): void {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$angle = ($i / $count) * M_PI * 2.0 + $this->novaRotation + $offset;
|
||||
$direction = new Vector2(cos($angle), sin($angle));
|
||||
$this->container->append(new WitchProjectile(
|
||||
$this->pos->x,
|
||||
$this->pos->y,
|
||||
$direction,
|
||||
$this->container,
|
||||
$this->room,
|
||||
$speed,
|
||||
$size,
|
||||
$color,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
$fireRing(0.0);
|
||||
$this->pendingNova[] = [0.22, function () use ($fireRing, $count): void {
|
||||
$fireRing(M_PI / $count);
|
||||
}];
|
||||
if ($lost > 0.34) {
|
||||
$this->pendingNova[] = [0.44, function () use ($fireRing): void {
|
||||
$fireRing(0.0);
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
$data = parent::toSnapshot();
|
||||
$data['charge'] = 1.0 - max(0.0, $this->novaTimer) / $this->novaInterval;
|
||||
|
||||
if ($data['hp'] < $data['mhp'] && $data['charge'] > 0.72) {
|
||||
$data['nova'] = true;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\GameObject;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
|
||||
final class Pickup extends GameObject
|
||||
{
|
||||
public const TYPES = [
|
||||
'rapid' => 'Rapid Fire',
|
||||
'damage' => 'Damage +1',
|
||||
'multishot' => 'Multishot',
|
||||
'speed' => 'Bullet Speed',
|
||||
];
|
||||
|
||||
private EntityList $container;
|
||||
|
||||
private Room $room;
|
||||
|
||||
public string $type;
|
||||
|
||||
public float $life;
|
||||
|
||||
private float $time = 0.0;
|
||||
|
||||
public float $collectRadius;
|
||||
|
||||
public float $size;
|
||||
|
||||
/** @var array<float> */
|
||||
public array $color;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
string $type,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
float $life = 10.0,
|
||||
float $collectRadius = 46.0,
|
||||
) {
|
||||
parent::__construct($x, $y);
|
||||
$this->container = $container;
|
||||
$this->room = $room;
|
||||
$this->type = $type;
|
||||
$this->life = $life;
|
||||
$this->collectRadius = $collectRadius;
|
||||
$this->time = 0.0;
|
||||
$this->size = 16.0;
|
||||
$this->width = $this->size;
|
||||
$this->height = $this->size;
|
||||
$this->color = self::colorFor($type);
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'pickup';
|
||||
}
|
||||
|
||||
public static function colorFor(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
'rapid' => [1.0, 0.8, 0.2, 1.0],
|
||||
'damage' => [1.0, 0.3, 0.3, 1.0],
|
||||
'multishot' => [0.2, 0.8, 1.0, 1.0],
|
||||
'speed' => [0.6, 0.9, 1.0, 1.0],
|
||||
default => [1.0, 1.0, 1.0, 1.0],
|
||||
};
|
||||
}
|
||||
|
||||
public function collectableBy(Player $player): bool
|
||||
{
|
||||
if ($player instanceof Player && ! $player->dead) {
|
||||
return $player->maxUpgradeLevel > ($player->upgrades[$this->type] ?? 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
$this->time += $dt;
|
||||
if ($this->time >= $this->life) {
|
||||
$this->container->remove($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function remaining(): float
|
||||
{
|
||||
return max(0.0, $this->life - $this->time);
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
return [
|
||||
'eid' => $this->eid,
|
||||
'd' => $this->kind(),
|
||||
'x' => round($this->pos->x, 1),
|
||||
'y' => round($this->pos->y, 1),
|
||||
't' => $this->type,
|
||||
'life' => round($this->remaining(), 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
use App\Game\Vector2;
|
||||
|
||||
class Witch extends Zombie
|
||||
{
|
||||
public float $preferredDistance;
|
||||
|
||||
public float $fireInterval;
|
||||
|
||||
public float $projectileSpeed;
|
||||
|
||||
public int $projectileCount;
|
||||
|
||||
public float $projectileSpread;
|
||||
|
||||
/** @var array<float> */
|
||||
public array $projectileColor;
|
||||
|
||||
protected float $fireTimer = 0.0;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
?Player $initialTarget,
|
||||
array $config = [],
|
||||
?callable $onDeath = null,
|
||||
) {
|
||||
parent::__construct($x, $y, $container, $room, $config, $onDeath);
|
||||
if ($initialTarget !== null) {
|
||||
$this->target = $initialTarget;
|
||||
}
|
||||
$this->preferredDistance = $config['preferredDistance'] ?? 220.0;
|
||||
$this->fireInterval = $config['fireInterval'] ?? 2.2;
|
||||
$this->projectileSpeed = $config['projectileSpeed'] ?? 220.0;
|
||||
$this->projectileCount = $config['projectileCount'] ?? 1;
|
||||
$this->projectileSpread = $config['projectileSpread'] ?? 0.08;
|
||||
$this->projectileColor = $config['projectileColor'] ?? [0.7, 0.2, 1.0, 1.0];
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'witch';
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
$this->time += $dt;
|
||||
|
||||
if ($this->updateEmerge($dt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fireTimer -= $dt;
|
||||
|
||||
$target = $this->target;
|
||||
if ($target === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$delta = $target->pos->subV($this->pos);
|
||||
$distance = $delta->length();
|
||||
|
||||
$move = Vector2::zero();
|
||||
if ($distance > 1.0) {
|
||||
$dir = $delta->divS($distance);
|
||||
if ($distance < $this->preferredDistance) {
|
||||
$move = $dir->mulS(-$this->speed);
|
||||
} elseif ($distance > $this->preferredDistance + 40.0) {
|
||||
$move = $dir->mulS($this->speed);
|
||||
}
|
||||
}
|
||||
|
||||
$this->velocity = $move->addV($this->hitKnockback);
|
||||
$this->move($dt);
|
||||
|
||||
$this->hitKnockback = $this->hitKnockback->mulS(max(0.0, 1 - $dt * $this->knockbackDamping));
|
||||
$this->flash = max(0.0, $this->flash - $dt);
|
||||
|
||||
if ($this->fireTimer <= 0.0 && ! $target->dead && $distance < 480.0) {
|
||||
$this->fireTimer = $this->fireInterval * (0.8 + $this->rand01() * 0.4);
|
||||
$this->fire();
|
||||
}
|
||||
}
|
||||
|
||||
protected function onEmergeComplete(): void
|
||||
{
|
||||
$this->fireTimer = $this->fireInterval * (0.4 + $this->rand01() * 0.4);
|
||||
}
|
||||
|
||||
public function fire(): void
|
||||
{
|
||||
$target = $this->target;
|
||||
if ($target === null) {
|
||||
return;
|
||||
}
|
||||
$aim = $target->pos->subV($this->pos);
|
||||
$aimAngle = $aim->lengthSquared() > 1.0 ? atan2($aim->y, $aim->x) : $this->time;
|
||||
|
||||
$offset = (($this->projectileCount - 1) / 2.0) * $this->projectileSpread;
|
||||
for ($i = 0; $i < $this->projectileCount; $i++) {
|
||||
$angle = $aimAngle - $offset + $i * $this->projectileSpread;
|
||||
$direction = new Vector2(cos($angle), sin($angle));
|
||||
$this->container->append(new WitchProjectile(
|
||||
$this->pos->x,
|
||||
$this->pos->y,
|
||||
$direction,
|
||||
$this->container,
|
||||
$this->room,
|
||||
$this->projectileSpeed,
|
||||
$this->size * 0.4,
|
||||
$this->projectileColor,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\GameObject;
|
||||
use App\Game\Room;
|
||||
use App\Game\Vector2;
|
||||
|
||||
final class WitchProjectile extends GameObject
|
||||
{
|
||||
private EntityList $container;
|
||||
|
||||
private Room $room;
|
||||
|
||||
public float $size;
|
||||
|
||||
/** @var array<float> */
|
||||
public array $color;
|
||||
|
||||
private float $time = 0.0;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
Vector2 $direction,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
float $speed = 220.0,
|
||||
float $size = 12.0,
|
||||
array $color = [0.7, 0.2, 1.0, 1.0],
|
||||
) {
|
||||
parent::__construct($x, $y);
|
||||
$this->container = $container;
|
||||
$this->room = $room;
|
||||
$this->velocity = $direction->mulS($speed);
|
||||
$this->size = $size;
|
||||
$this->color = $color;
|
||||
$this->width = $size;
|
||||
$this->height = $size;
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'projectile';
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
parent::update($dt);
|
||||
$this->time += $dt;
|
||||
|
||||
if (
|
||||
$this->pos->x < -$this->size
|
||||
|| $this->pos->x > $this->room->width + $this->size
|
||||
|| $this->pos->y < -$this->size
|
||||
|| $this->pos->y > $this->room->height + $this->size
|
||||
) {
|
||||
$this->container->remove($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
return [
|
||||
'eid' => $this->eid,
|
||||
'd' => $this->kind(),
|
||||
'x' => round($this->pos->x, 1),
|
||||
'y' => round($this->pos->y, 1),
|
||||
'vx' => round($this->velocity->x, 1),
|
||||
'vy' => round($this->velocity->y, 1),
|
||||
's' => $this->size,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Game\Entities;
|
||||
|
||||
use App\Game\EntityList;
|
||||
use App\Game\GameObject;
|
||||
use App\Game\Player;
|
||||
use App\Game\Room;
|
||||
use App\Game\Vector2;
|
||||
|
||||
class Zombie extends GameObject
|
||||
{
|
||||
public ?Player $target = null;
|
||||
|
||||
protected EntityList $container;
|
||||
|
||||
protected Room $room;
|
||||
|
||||
public float $speed;
|
||||
|
||||
public float $size;
|
||||
|
||||
/** @var array<float> */
|
||||
public array $baseColor;
|
||||
|
||||
public int $health;
|
||||
|
||||
public int $maxHealth;
|
||||
|
||||
public float $knockback;
|
||||
|
||||
public float $knockbackDamping;
|
||||
|
||||
public int $scoreValue;
|
||||
|
||||
/** @var null|callable(Zombie):void */
|
||||
public $onDeath;
|
||||
|
||||
protected Vector2 $hitKnockback;
|
||||
|
||||
protected float $flash = 0.0;
|
||||
|
||||
protected float $time = 0.0;
|
||||
|
||||
public float $broken;
|
||||
|
||||
protected float $emergeTotal;
|
||||
|
||||
protected float $emerge;
|
||||
|
||||
public function __construct(
|
||||
float $x,
|
||||
float $y,
|
||||
EntityList $container,
|
||||
Room $room,
|
||||
array $config = [],
|
||||
?callable $onDeath = null,
|
||||
) {
|
||||
parent::__construct($x, $y);
|
||||
$this->container = $container;
|
||||
$this->room = $room;
|
||||
$this->speed = $config['speed'] ?? 110.0;
|
||||
$this->size = $config['size'] ?? 24.0;
|
||||
$this->baseColor = $config['color'] ?? [0.15, 0.65, 0.2, 1.0];
|
||||
$this->health = $config['health'] ?? 1;
|
||||
$this->maxHealth = $this->health;
|
||||
$this->knockback = $config['knockback'] ?? 260.0;
|
||||
$this->knockbackDamping = $config['knockbackDamping'] ?? 10.0;
|
||||
$this->scoreValue = $config['scoreValue'] ?? 1;
|
||||
$emergeTime = $config['emergeTime'] ?? 0.0;
|
||||
$this->emergeTotal = $emergeTime;
|
||||
$this->emerge = $emergeTime;
|
||||
$this->onDeath = $onDeath;
|
||||
$this->broken = rand(0, 100000) / 100000.0 * M_PI * 2.0;
|
||||
$this->hitKnockback = Vector2::zero();
|
||||
$this->width = $this->size;
|
||||
$this->height = $this->size;
|
||||
}
|
||||
|
||||
public function kind(): string
|
||||
{
|
||||
return 'zombie';
|
||||
}
|
||||
|
||||
public function die(): void
|
||||
{
|
||||
if ($this->onDeath !== null) {
|
||||
($this->onDeath)($this);
|
||||
}
|
||||
$this->removeAllReferences();
|
||||
$this->container->remove($this);
|
||||
}
|
||||
|
||||
protected function removeAllReferences(): void {}
|
||||
|
||||
public function hit(Vector2 $impulse, int $damage = 1): bool
|
||||
{
|
||||
$this->health -= $damage;
|
||||
if ($this->health < 0) {
|
||||
$this->health = 0;
|
||||
}
|
||||
$this->hitKnockback = $impulse->mulS($this->knockback);
|
||||
$this->flash = 0.08;
|
||||
|
||||
return $this->health <= 0;
|
||||
}
|
||||
|
||||
public function update(float $dt): void
|
||||
{
|
||||
$this->time += $dt;
|
||||
if ($this->updateEmerge($dt)) {
|
||||
return;
|
||||
}
|
||||
if ($this->target === null) {
|
||||
return;
|
||||
}
|
||||
$delta = $this->target->pos->subV($this->pos);
|
||||
$distance = $delta->length();
|
||||
$chaseVelocity = Vector2::zero();
|
||||
if ($distance > 1.0) {
|
||||
$chaseVelocity = $delta->divS($distance)->mulS($this->speed);
|
||||
}
|
||||
|
||||
$this->velocity = $chaseVelocity->addV($this->hitKnockback);
|
||||
parent::update($dt);
|
||||
|
||||
$this->hitKnockback = $this->hitKnockback->mulS(max(0.0, 1 - $dt * $this->knockbackDamping));
|
||||
$this->flash = max(0.0, $this->flash - $dt);
|
||||
}
|
||||
|
||||
public function updateEmerge(float $dt): bool
|
||||
{
|
||||
if ($this->emerge <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
$this->emerge -= $dt;
|
||||
if ($this->emerge <= 0.0) {
|
||||
$this->emerge = 0.0;
|
||||
$this->onEmergeComplete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function onEmergeComplete(): void {}
|
||||
|
||||
protected function rand01(): float
|
||||
{
|
||||
return random_int(0, 1000000) / 1000000.0;
|
||||
}
|
||||
|
||||
public function toSnapshot(): array
|
||||
{
|
||||
$snapshot = [
|
||||
'eid' => $this->eid,
|
||||
'd' => $this->kind(),
|
||||
'x' => round($this->pos->x, 1),
|
||||
'y' => round($this->pos->y, 1),
|
||||
's' => $this->size,
|
||||
'hp' => $this->health,
|
||||
'mhp' => $this->maxHealth,
|
||||
'c' => array_slice($this->baseColor, 0, 3),
|
||||
];
|
||||
if ($this->emergeTotal > 0.0) {
|
||||
$snapshot['et'] = round($this->emergeTotal, 2);
|
||||
$snapshot['er'] = round($this->emerge, 2);
|
||||
}
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user