ZH

Elfs & Orcs Tower Defense

作者:malabke · 策略
5.0 (1)
▶ 开玩 💬 反馈 源码 🕘 版本 编辑 GitHub
games/elfs-orcs-tower-defense / src / Td / Tower.php · commit 65500d9
<?phpnamespace App\Td;/** * A tower on a building slot: its level (1..3), its specialization (level 4), what it aims at. */final class Tower{    public const TARGETING = ['first', 'last', 'strongest', 'closest'];    public const MAX_LEVEL = 3;    public int $level = 1;    public ?string $spec = null;    public string $targeting = 'first';    public float $cooldown = 0.0;    /** gold spent on it (selling gives back a part) */    public int $invested;    /** @var array<string, mixed> */    public array $stats;    /** seconds since the last attack (drawing) */    public float $sinceShot = 99.0;    /** where the last attack went (drawing) */    public float $aimX = 0.0;    public float $aimY = 0.0;    /** @var list<Soldier> barracks */    public array $soldiers = [];    /**     * What a barrier / trap / barracks puts on the road: its point (path, distance, x, y), and for a wall its health.     * @var array{path: int, distance: float, x: float, y: float, hp: float, maxHp: float, timer: float}|null     */    public ?array $road = null;    /** from the auras around it */    public float $buffDamage = 1.0;    public float $buffRate = 1.0;    /** @var array<int, bool> enemies already hit (ambush: the first hit is critical) */    public array $hitIds = [];    public float $incomeTimer = 0.0;    public function __construct(public readonly string $type, public readonly float $x, public readonly float $y,                                private GameData $data)    {        $this->invested = $data->upgradeCost($type, 1);        $this->stats = $data->towerStats($type, 1);        $this->aimX = $x;        $this->aimY = $y + 10;    }    /** What the next upgrade costs: null at the top. With a specialization name: its price. */    public function nextCost(?string $spec = null): ?int    {        if ($this->spec !== null) {            return null;        }        if ($this->level < self::MAX_LEVEL) {            return $this->data->upgradeCost($this->type, $this->level + 1);        }        $specs = $this->specNames();        return $spec !== null || $specs !== [] ? $this->data->upgradeCost($this->type, 4, $spec ?? $specs[0]) : null;    }    /** @return list<string> the two specializations (keys of towers.yaml) */    public function specNames(): array    {        return array_keys($this->data->tower($this->type)['specs'] ?? []);    }    public function canSpecialize(): bool    {        return $this->level === self::MAX_LEVEL && $this->spec === null && $this->specNames() !== [];    }    public function upgrade(): void    {        $this->invested += (int)$this->nextCost();        $this->level++;        $this->stats = $this->data->towerStats($this->type, $this->level);    }    public function specialize(string $spec): void    {        $this->invested += $this->data->upgradeCost($this->type, 4, $spec);        $this->spec = $spec;        $this->level = self::MAX_LEVEL + 1;        $this->stats = $this->data->towerStats($this->type, self::MAX_LEVEL, $spec);    }    public function sellValue(): int    {        return (int)floor($this->invested * 0.7);    }    public function range(): float    {        return (float)$this->stats['range'];    }    /** attack | barracks | barrier | trap | aura | income */    public function kind(): string    {        return (string)$this->stats['kind'];    }    /** It shoots (an aura or income tower may shoot too). */    public function attacks(): bool    {        return (float)$this->stats['damage'] > 0 && !in_array($this->kind(), ['barracks', 'barrier', 'trap'], true);    }    /** It aims at targets (a barracks / wall / trap does not, it has no targeting mode). */    public function hasTargeting(): bool    {        return $this->attacks() && empty($this->stats['pulse']);    }    public function canHit(Enemy $enemy): bool    {        $targets = (string)($this->stats['targets'] ?? 'both');        return $targets === 'both' || ($targets === 'air') === $enemy->isFlying();    }    public function cycleTargeting(): string    {        $i = array_search($this->targeting, self::TARGETING, true);        return $this->targeting = self::TARGETING[((int)$i + 1) % count(self::TARGETING)];    }    /**     * The enemies it attacks now, best first (its targeting mode).     * @param list<Enemy> $enemies     * @return list<Enemy>     */    public function pickTargets(array $enemies): array    {        $range2 = $this->range() ** 2;        $inRange = [];        foreach ($enemies as $enemy) {            if ($enemy->isTargetable() && $this->canHit($enemy)                && ($enemy->x - $this->x) ** 2 + ($enemy->y - $this->y) ** 2 <= $range2) {                $inRange[] = $enemy;            }        }        $key = match ($this->targeting) {            'last' => static fn (Enemy $e) => -$e->remaining(),            'strongest' => static fn (Enemy $e) => -$e->hp,            'closest' => fn (Enemy $e) => ($e->x - $this->x) ** 2 + ($e->y - $this->y) ** 2,            default => static fn (Enemy $e) => $e->remaining(),        };        usort($inRange, static fn (Enemy $a, Enemy $b) => [$key($a), $a->id] <=> [$key($b), $b->id]);        return array_slice($inRange, 0, max(1, (int)$this->stats['multishot']));    }}