Sign in
EN
games/doom-jumphp / src / Game / Actions.php · commit 8e1cb9f
<?phpnamespace App\Game;/** * The actions of the state machines: monster AI (p_enemy.c), damage and death (p_inter.c), items, the weapon * in the player's hands (p_pspr.c). */trait Actions{    private const DIR_X = [1, 0.70710678, 0, -0.70710678, -1, -0.70710678, 0, 0.70710678];    private const DIR_Y = [0, 0.70710678, 1, 0.70710678, 0, -0.70710678, -1, -0.70710678];    private const OPPOSITE = [4, 5, 6, 7, 0, 1, 2, 3, 8];    private const DIAGS = [3, 1, 5, 7];    private const NODIR = 8;    private function dist(Mobj $a, Mobj $b): float    {        // P_AproxDistance        $dx = abs($a->x - $b->x);        $dy = abs($a->y - $b->y);        return $dx + $dy - min($dx, $dy) / 2;    }    // ----------------------------------------------------------------------------------- monsters    private function lookForPlayer(Mobj $actor, bool $allAround): bool    {        $p = $this->player;        if ($p->dead || $p->mo->health <= 0) {            return false;        }        if (!$this->checkSight($actor, $p->mo)) {            return false;        }        if (!$allAround) {            $an = fmod(atan2($p->mo->y - $actor->y, $p->mo->x - $actor->x) - $actor->angle + 4 * M_PI, 2 * M_PI);            if ($an > M_PI / 2 && $an < 3 * M_PI / 2 && $this->dist($p->mo, $actor) > self::MELEERANGE) {                return false;      // behind its back            }        }        $actor->target = $p->mo;        return true;    }    private function aLook(Mobj $actor): void    {        $actor->threshold = 0;        $targ = $actor->sector?->soundTarget;        $see = false;        if ($targ !== null && $targ->is(Info::SHOOTABLE)) {            $actor->target = $targ;            $see = !$actor->is(Info::AMBUSH) || $this->checkSight($actor, $targ);        }        if (!$see && !$this->lookForPlayer($actor, false)) {            return;        }        if ($actor->info['seeSound'] !== null) {            $this->sound($actor->info['seeSound'], $actor->is(Info::BOSS) ? null : $actor);        }        $this->setState($actor, $actor->info['see']);    }    private function aChase(Mobj $actor): void    {        if ($actor->reactiontime > 0) {            $actor->reactiontime--;        }        if ($actor->threshold > 0) {            if ($actor->target === null || $actor->target->health <= 0) {                $actor->threshold = 0;            } else {                $actor->threshold--;            }        }        // turn towards the movement direction        if ($actor->movedir < 8) {            $want = $actor->movedir * M_PI / 4;            $actor->angle = round($actor->angle / (M_PI / 4)) * (M_PI / 4);            $delta = fmod($actor->angle - $want + 5 * M_PI, 2 * M_PI) - M_PI;            if ($delta > 0.01) {                $actor->angle -= M_PI / 4;            } elseif ($delta < -0.01) {                $actor->angle += M_PI / 4;            }            $actor->angle = fmod($actor->angle + 2 * M_PI, 2 * M_PI);        }        $t = $actor->target;        if ($t === null || !$t->is(Info::SHOOTABLE) || $t->removed) {            if ($this->lookForPlayer($actor, true)) {                return;            }            $this->setState($actor, $actor->info['spawn']);            return;        }        if ($actor->is(Info::JUSTATTACKED)) {            $actor->flags &= ~Info::JUSTATTACKED;            if ($this->skill !== self::SKILL_HARD) {                $this->newChaseDir($actor);            }            return;        }        if ($actor->info['melee'] !== null && $this->checkMeleeRange($actor)) {            if ($actor->info['attackSound'] !== null) {                $this->sound($actor->info['attackSound'], $actor);            }            $this->setState($actor, $actor->info['melee']);            return;        }        if ($actor->info['missile'] !== null && !($this->skill !== self::SKILL_HARD && $actor->movecount > 0)            && $this->checkMissileRange($actor)) {            $this->setState($actor, $actor->info['missile']);            $actor->flags |= Info::JUSTATTACKED;            return;        }        if (--$actor->movecount < 0 || !$this->moveActor($actor)) {            $this->newChaseDir($actor);        }        if ($actor->info['activeSound'] !== null && $this->random() < 3) {            $this->sound($actor->info['activeSound'], $actor);        }    }    private function checkMeleeRange(Mobj $actor): bool    {        $t = $actor->target;        if ($t === null) {            return false;        }        if ($this->dist($actor, $t) >= self::MELEERANGE - 20 + $t->radius) {            return false;        }        return $this->checkSight($actor, $t);    }    private function checkMissileRange(Mobj $actor): bool    {        if (!$this->checkSight($actor, $actor->target)) {            return false;        }        if ($actor->is(Info::JUSTHIT)) {            $actor->flags &= ~Info::JUSTHIT;            return true;        }        if ($actor->reactiontime > 0) {            return false;        }        $d = $this->dist($actor, $actor->target) - 64;        if ($actor->info['melee'] === null) {            $d -= 128;        }        if ($actor->is(Info::BOSS)) {            $d /= 2;        }        $d = min(200, $d);        return $this->random() >= $d;    }    /** P_Move: a step along movedir; opens doors in the way. */    private function moveActor(Mobj $actor): bool    {        if ($actor->movedir === self::NODIR) {            return false;        }        $speed = $actor->info['speed'];        $tx = $actor->x + $speed * self::DIR_X[$actor->movedir];        $ty = $actor->y + $speed * self::DIR_Y[$actor->movedir];        if (!$this->tryMove($actor, $tx, $ty)) {            if (!$this->specHit) {                return false;            }            $actor->movedir = self::NODIR;            $good = false;            foreach ($this->specHit as $l) {                if ($this->useSpecialLine($actor, $l)) {                    $good = true;                }            }            return $good;        }        $actor->z = $actor->floorz;        return true;    }    private function tryWalk(Mobj $actor): bool    {        if (!$this->moveActor($actor)) {            return false;        }        $actor->movecount = $this->random() & 15;        return true;    }    /** P_NewChaseDir */    private function newChaseDir(Mobj $actor): void    {        $t = $actor->target;        $old = $actor->movedir;        $turnaround = self::OPPOSITE[$old];        $dx = $t->x - $actor->x;        $dy = $t->y - $actor->y;        $d1 = $dx > 10 ? 0 : ($dx < -10 ? 4 : self::NODIR);        $d2 = $dy < -10 ? 6 : ($dy > 10 ? 2 : self::NODIR);        if ($d1 !== self::NODIR && $d2 !== self::NODIR) {            $actor->movedir = self::DIAGS[(($dy < 0) ? 2 : 0) + ($dx > 0 ? 1 : 0)];            if ($actor->movedir !== $turnaround && $this->tryWalk($actor)) {                return;            }        }        if ($this->random() > 200 || abs($dy) > abs($dx)) {            [$d1, $d2] = [$d2, $d1];        }        if ($d1 === $turnaround) {            $d1 = self::NODIR;        }        if ($d2 === $turnaround) {            $d2 = self::NODIR;        }        foreach ([$d1, $d2] as $d) {            if ($d !== self::NODIR) {                $actor->movedir = $d;                if ($this->tryWalk($actor)) {                    return;                }            }        }        if ($old !== self::NODIR) {            $actor->movedir = $old;            if ($this->tryWalk($actor)) {                return;            }        }        $dirs = ($this->random() & 1) ? range(0, 7) : range(7, 0);        foreach ($dirs as $d) {            if ($d !== $turnaround) {                $actor->movedir = $d;                if ($this->tryWalk($actor)) {                    return;                }            }        }        if ($turnaround < self::NODIR) {            $actor->movedir = $turnaround;            // @phpstan-ignore-next-line            if ($this->tryWalk($actor)) {                return;            }        }        $actor->movedir = self::NODIR;    }    private function aFaceTarget(Mobj $actor): void    {        if ($actor->target === null) {            return;        }        $actor->flags &= ~Info::AMBUSH;        $actor->angle = atan2($actor->target->y - $actor->y, $actor->target->x - $actor->x);    }    private function aHuskShoot(Mobj $actor): void    {        if ($actor->target === null) {            return;        }        $this->aFaceTarget($actor);        [$slope] = $this->aimLineAttack($actor, $actor->angle, self::MISSILERANGE);        $this->sound('pistol', $actor);        $angle = $actor->angle + ($this->random() - $this->random()) / 255 * 0.39;        $this->lineAttack($actor, $angle, self::MISSILERANGE, $slope, (($this->random() % 5) + 1) * 3);    }    private function aBite(Mobj $actor): void    {        if ($actor->target === null) {            return;        }        $this->aFaceTarget($actor);        if ($this->checkMeleeRange($actor)) {            $this->damage($actor->target, $actor, $actor, (($this->random() % 10) + 1) * 4);        }    }    private function aSpitAttack(Mobj $actor): void    {        if ($actor->target === null) {            return;        }        $this->aFaceTarget($actor);        if ($this->checkMeleeRange($actor)) {            $this->sound('claw', $actor);            $this->damage($actor->target, $actor, $actor, (($this->random() % 8) + 1) * 3);            return;        }        $this->spawnMissile($actor, $actor->target, 'acidball');    }    private function aWardenFire(Mobj $actor): void    {        if ($actor->target === null) {            return;        }        $this->aFaceTarget($actor);        $mo = $this->spawnMissile($actor, $actor->target, 'plasma', 60);        if ($mo !== null) {            // aims a little to the left of the cannon arm            $mo->x += cos($actor->angle - M_PI / 2) * 10;            $mo->y += sin($actor->angle - M_PI / 2) * 10;        }    }    private function aPain(Mobj $actor): void    {        if ($actor->info['painSound'] !== null) {            $this->sound($actor->info['painSound'], $actor);        }    }    private function aScream(Mobj $actor): void    {        if ($actor->info['deathSound'] !== null) {            $this->sound($actor->info['deathSound'], $actor->is(Info::BOSS) ? null : $actor);        }    }    private function aFall(Mobj $actor): void    {        $actor->flags &= ~Info::SOLID;    }    private function aExplode(Mobj $actor): void    {        $this->radiusAttack($actor, $actor->target, 128);    }    /** The warden's death opens the way out: every "boss" sector of the level opens. */    private function aBossDeath(Mobj $actor): void    {        foreach ($this->mobjs as $mo) {            if ($mo !== $actor && $mo->is(Info::BOSS) && $mo->health > 0) {                return;            }        }        foreach ($this->level->sectors as $s) {            if (!empty($s->def['boss'])) {                $this->openGate($s);            }        }        $this->message('THE WARDEN IS DOWN. THE CORE IS OPEN!');    }    /** P_SpawnMissile */    private function spawnMissile(Mobj $source, Mobj $dest, string $type, float $z = 32): ?Mobj    {        $mo = $this->spawn($type, $source->x, $source->y, $source->z + $z);        if ($mo->info['seeSound'] !== null) {            $this->sound($mo->info['seeSound'], $mo);        }        $mo->target = $source;        $mo->angle = atan2($dest->y - $source->y, $dest->x - $source->x);        $speed = $mo->info['speed'];        $mo->momx = $speed * cos($mo->angle);        $mo->momy = $speed * sin($mo->angle);        $dist = max(1.0, $this->dist($dest, $source) / $speed);        $mo->momz = ($dest->z + $dest->height / 2 - $mo->z) / $dist;        $this->checkMissileSpawn($mo);        return $mo->removed ? null : $mo;    }    private function checkMissileSpawn(Mobj $mo): void    {        $mo->tics = max(1, $mo->tics - ($this->random() & 3));        $mo->x += $mo->momx / 2;        $mo->y += $mo->momy / 2;        $mo->z += $mo->momz / 2;        if (!$this->tryMove($mo, $mo->x, $mo->y)) {            $this->explodeMissile($mo);        }    }    private function explodeMissile(Mobj $mo): void    {        $mo->momx = $mo->momy = $mo->momz = 0.0;        $mo->flags &= ~Info::MISSILE;        if ($mo->info['deathSound'] !== null) {            $this->sound($mo->info['deathSound'], $mo);        }        if ($this->setState($mo, $mo->info['death'])) {            $mo->tics = max(1, $mo->tics - ($this->random() & 3));        }    }    private function spawnPuff(float $x, float $y, float $z): void    {        $mo = $this->spawn('puff', $x, $y, $z + (($this->random() - $this->random()) / 64));        $mo->momz = 1.0;        $mo->tics = max(1, $mo->tics - ($this->random() & 3));    }    private function spawnBlood(float $x, float $y, float $z, int $damage): void    {        $mo = $this->spawn('blood', $x, $y, $z + (($this->random() - $this->random()) / 64));        $mo->momz = 2.0;        $mo->tics = max(1, $mo->tics - ($this->random() & 3));        if ($damage <= 12 && $damage >= 9) {            $this->setState($mo, 'BLUD2');        } elseif ($damage < 9) {            $this->setState($mo, 'BLUD3');        }    }    // ------------------------------------------------------------------------------- damage, death    /** P_DamageMobj */    public function damage(Mobj $target, ?Mobj $inflictor, ?Mobj $source, int $damage): void    {        if (!$target->is(Info::SHOOTABLE) || $target->health <= 0) {            return;        }        $p = $target->player;        if ($p !== null && $this->skill === self::SKILL_EASY) {            $damage >>= 1;        }        if ($inflictor !== null && !$target->is(Info::NOCLIP)) {            $ang = atan2($target->y - $inflictor->y, $target->x - $inflictor->x);            $thrust = $damage / 8 * 100 / $target->info['mass'];            $target->momx += $thrust * cos($ang);            $target->momy += $thrust * sin($ang);        }        if ($p !== null) {            if ($p->god) {                return;            }            if ($p->armorType > 0) {                $saved = $p->armorType === 1 ? intdiv($damage, 3) : intdiv($damage, 2);                if ($p->armor <= $saved) {                    $saved = $p->armor;                    $p->armorType = 0;                }                $p->armor -= $saved;                $damage -= $saved;            }            $p->health = max(0, $p->health - $damage);            $p->attacker = $source;            $p->damagecount = min(100, $p->damagecount + $damage);        }        $target->health -= $damage;        if ($target->health <= 0) {            $this->kill($source, $target);            return;        }        if ($this->random() < $target->info['painChance'] && $target->info['pain'] !== null) {            $target->flags |= Info::JUSTHIT;            $this->setState($target, $target->info['pain']);        } elseif ($p !== null && $this->random() < 160) {            $this->sound('plpain', $target);        }        $target->reactiontime = 0;        if ($target->threshold <= 0 && $source !== null && $source !== $target && $p === null) {            $target->target = $source;            $target->threshold = 100;            if ($target->state === $target->info['spawn'] || str_starts_with($target->state, substr($target->info['spawn'], 0, -1))) {                if ($target->info['see'] !== null) {                    $this->setState($target, $target->info['see']);                }            }        }    }    /** P_KillMobj */    private function kill(?Mobj $source, Mobj $target): void    {        $target->flags &= ~(Info::SHOOTABLE | Info::NOGRAVITY);        $target->flags |= Info::CORPSE | Info::DROPOFF;        $target->height /= 4;        if ($source !== null && $source->player !== null && $target->is(Info::COUNTKILL)) {            $source->player->kills++;        } elseif ($target->is(Info::COUNTKILL) && $source !== null && $source->player === null) {            // killed by another monster or a barrel: still counts for the player            $this->player->kills++;        }        if ($target->player !== null) {            $target->flags &= ~Info::SOLID;            $target->player->dead = true;            $target->player->health = 0;            $this->sound('pldeath', $target);            $this->setWeaponState('DOWN');            return;        }        if ($target->info['death'] !== null) {            $this->setState($target, $target->info['death']);            $target->tics = max(1, $target->tics - ($this->random() & 3));        } else {            $this->remove($target);        }        if ($target->info['drop'] !== null) {            $mo = $this->spawn($target->info['drop'], $target->x, $target->y, null);            $mo->flags |= Info::DROPPED;        }    }    // --------------------------------------------------------------------------------------- items    /** P_TouchSpecialThing */    private function touchSpecial(Mobj $special, Mobj $toucher): void    {        $p = $toucher->player;        if ($p === null || $toucher->health <= 0 || $p->dead) {            return;        }        $delta = $special->z - $toucher->z;        if ($delta > $toucher->height || $delta < -8) {            return;        }        $sound = 'item';        $msg = '';        $dropped = $special->is(Info::DROPPED);        switch ($special->type) {            case 'medkit':                if ($p->health >= 100) {                    return;                }                $msg = $p->health < 25 ? 'PICKED UP A MEDKIT THAT YOU REALLY NEED!' : 'PICKED UP A MEDKIT.';                $this->giveHealth($p, 25, 100);                break;            case 'stim':                if ($p->health >= 100) {                    return;                }                $this->giveHealth($p, 10, 100);                $msg = 'PICKED UP A STIMPACK.';                break;            case 'vial':                $this->giveHealth($p, 1, 200);                $msg = 'PICKED UP A HEALTH VIAL.';                break;            case 'orb':                $this->giveHealth($p, 100, 200);                $msg = 'OVERCHARGE!';                $sound = 'powerup';                break;            case 'armor1':                if (!$this->giveArmor($p, 1)) {                    return;                }                $msg = 'PICKED UP THE COMBAT VEST.';                break;            case 'armor2':                if (!$this->giveArmor($p, 2)) {                    return;                }                $msg = 'PICKED UP THE HEAVY PLATE!';                break;            case 'shard':                $p->armor = min(200, $p->armor + 1);                if ($p->armorType === 0) {                    $p->armorType = 1;                }                $msg = 'PICKED UP AN ARMOR SHARD.';                break;            case 'clip':                if (!$this->giveAmmo($p, 'bullets', $dropped ? 5 : 10)) {                    return;                }                $msg = 'PICKED UP A CLIP.';                break;            case 'ammobox':                if (!$this->giveAmmo($p, 'bullets', 50)) {                    return;                }                $msg = 'PICKED UP A BOX OF BULLETS.';                break;            case 'shells':                if (!$this->giveAmmo($p, 'shells', 4)) {                    return;                }                $msg = 'PICKED UP 4 SHELLS.';                break;            case 'shellbox':                if (!$this->giveAmmo($p, 'shells', 20)) {                    return;                }                $msg = 'PICKED UP A BOX OF SHELLS.';                break;            case 'cells':                if (!$this->giveAmmo($p, 'cells', 40)) {                    return;                }                $msg = 'PICKED UP AN ENERGY CELL.';                break;            case 'redkey':            case 'bluekey':            case 'yellowkey':                $k = substr($special->type, 0, -3);                $p->keys[$k] = true;                $msg = 'PICKED UP THE ' . strtoupper($k) . ' KEYCARD.';                break;            case 'scattergun':                $msg = 'YOU GOT THE SCATTERGUN!';                $this->giveWeapon($p, 3, 'shells', 8);                $sound = 'weapon';                break;            case 'riveter':                $msg = 'YOU GOT THE RIVETER!';                $this->giveWeapon($p, 4, 'bullets', 20);                $sound = 'weapon';                break;            case 'iongun':                $msg = 'YOU GOT THE ION LAUNCHER!';                $this->giveWeapon($p, 5, 'cells', 40);                $sound = 'weapon';                break;            default:                return;        }        if ($special->is(Info::COUNTITEM)) {            $p->items++;        }        $this->remove($special);        $p->bonuscount += 6;        $this->message($msg);        $this->sound($sound);    }    private function giveHealth(Player $p, int $n, int $max): void    {        $p->health = min($max, $p->health + $n);        $p->mo->health = $p->health;    }    private function giveArmor(Player $p, int $type): bool    {        $points = $type * 100;        if ($p->armor >= $points) {            return false;        }        $p->armorType = $type;        $p->armor = $points;        return true;    }    private function giveAmmo(Player $p, string $ammo, int $n): bool    {        if ($p->ammo[$ammo] >= Player::AMMO_MAX[$ammo]) {            return false;        }        if ($this->skill === self::SKILL_EASY) {            $n *= 2;        }        $had = $p->ammo[$ammo];        $p->ammo[$ammo] = min(Player::AMMO_MAX[$ammo], $had + $n);        // the original switches to a better weapon when the ammunition of an empty one comes back        if ($had === 0 && $p->weapon === 1 || ($had === 0 && Weapons::LIST[$p->weapon]['ammo'] === $ammo)) {            foreach ([5, 4, 3, 2] as $w) {                if (!empty($p->weapons[$w]) && Weapons::LIST[$w]['ammo'] === $ammo) {                    $p->pending = $w;                    break;                }            }        }        return true;    }    private function giveWeapon(Player $p, int $w, string $ammo, int $n): void    {        $new = empty($p->weapons[$w]);        $p->weapons[$w] = true;        $this->giveAmmo($p, $ammo, $n);        if ($new) {            $p->pending = $w;        }    }    // ------------------------------------------------------------------------ the weapon in the hands    public function setWeaponState(string $state): void    {        $p = $this->player;        $guard = 0;        do {            $st = Weapons::STATES[$state];            $p->wState = $state;            $p->wTics = $st[1];            if ($st[2] !== null) {                $before = $p->wState;                $this->{'w' . ucfirst($st[2])}();                if ($p->wState !== $before) {                    return;      // the action changed the state itself                }            }            $state = $st[3];        } while ($p->wTics === 0 && ++$guard < 8);    }    private function movePsprite(): void    {        $p = $this->player;        if ($p->wTics > 0 && --$p->wTics === 0) {            $this->setWeaponState(Weapons::STATES[$p->wState][3]);        }    }    /** The frame of the weapon to draw: [sprite, frame letter]. */    public function weaponFrame(): array    {        $p = $this->player;        return [Weapons::LIST[$p->weapon]['sprite'], Weapons::STATES[$p->wState][0]];    }    private function hasAmmo(): bool    {        $p = $this->player;        $w = Weapons::LIST[$p->weapon];        return $w['ammo'] === null || $p->ammo[$w['ammo']] >= $w['per'];    }    /** P_CheckAmmo: out of ammunition, the best weapon left comes up. */    private function checkAmmo(): bool    {        if ($this->hasAmmo()) {            return true;        }        $p = $this->player;        foreach ([5, 4, 3, 2, 1] as $w) {            $def = Weapons::LIST[$w];            if (!empty($p->weapons[$w]) && ($def['ammo'] === null || $p->ammo[$def['ammo']] >= $def['per'])) {                $p->pending = $w;                break;            }        }        $this->setWeaponState('DOWN');        return false;    }    private function fireWeapon(): void    {        if (!$this->checkAmmo()) {            return;        }        $this->setWeaponState(Weapons::LIST[$this->player->weapon]['fire']);        $this->noiseAlert($this->player->mo);    }    private function wWeaponReady(): void    {        $p = $this->player;        if ($p->pending !== null || $p->dead) {            $this->setWeaponState('DOWN');            return;        }        if ($p->attackDown) {            $this->fireWeapon();            return;        }        $p->refire = 0;        $a = 2 * M_PI * $this->tic / 64;        $p->wx = $p->bob * cos($a);        $p->wy = $p->bob * abs(sin($a));    }    private function wLower(): void    {        $p = $this->player;        $p->wy += Weapons::SPEED;        if ($p->wy < Weapons::LOWERED) {            return;        }        if ($p->dead) {            $p->wy = Weapons::LOWERED + 40;            return;        }        if ($p->pending !== null) {            $p->weapon = $p->pending;            $p->pending = null;        }        $this->setWeaponState('UP');    }    private function wRaise(): void    {        $p = $this->player;        $p->wx = 0;        $p->wy -= Weapons::SPEED;        if ($p->wy > 0) {            return;        }        $p->wy = 0;        $this->setWeaponState('READY');    }    private function wReFire(): void    {        $p = $this->player;        if ($p->attackDown && $p->pending === null && !$p->dead) {            $p->refire++;            $this->fireWeapon();        } else {            $p->refire = 0;            $this->checkAmmo();        }    }    private function useAmmo(): void    {        $p = $this->player;        $w = Weapons::LIST[$p->weapon];        if ($w['ammo'] !== null) {            $p->ammo[$w['ammo']] = max(0, $p->ammo[$w['ammo']] - $w['per']);        }    }    /** P_BulletSlope: vertical autoaim, looking a little left and right too. */    private function bulletSlope(): float    {        $mo = $this->player->mo;        foreach ([0.0, 0.098, -0.098] as $d) {            [$slope, $target] = $this->aimLineAttack($mo, $mo->angle + $d, 1024);            if ($target !== null) {                return $slope;            }        }        return 0.0;    }    private function gunShot(bool $accurate, float $slope): void    {        $mo = $this->player->mo;        $angle = $mo->angle;        if (!$accurate) {            $angle += ($this->random() - $this->random()) / 255 * 0.098;        }        $this->lineAttack($mo, $angle, self::MISSILERANGE, $slope, 5 * (($this->random() % 3) + 1));    }    private function wPunch(): void    {        $mo = $this->player->mo;        $damage = (($this->random() % 10) + 1) * 2;        $angle = $mo->angle + ($this->random() - $this->random()) / 255 * 0.05;        [$slope, $target] = $this->aimLineAttack($mo, $angle, self::MELEERANGE);        $this->sound('swing', $mo);        if ($target !== null) {            $this->lineAttack($mo, $angle, self::MELEERANGE, $slope, $damage);            $this->sound('punch', $mo);            $mo->angle = atan2($target->y - $mo->y, $target->x - $mo->x);        }    }    private function wFireSidearm(): void    {        $this->sound('pistol', $this->player->mo);        $this->useAmmo();        $this->player->flashTics = 4;        $this->gunShot($this->player->refire === 0, $this->bulletSlope());    }    private function wFireScatter(): void    {        $this->sound('shotgun', $this->player->mo);        $this->useAmmo();        $this->player->flashTics = 5;        $slope = $this->bulletSlope();        for ($i = 0; $i < 7; $i++) {            $this->gunShot(false, $slope);        }    }    private function wPump(): void    {        $this->sound('pump', $this->player->mo);    }    private function wFireRivet(): void    {        if (!$this->hasAmmo()) {            return;        }        $this->sound('rivet', $this->player->mo);        $this->useAmmo();        $this->player->flashTics = 3;        $this->gunShot($this->player->refire === 0, $this->bulletSlope());    }    private function wFireIon(): void    {        $this->useAmmo();        $this->player->flashTics = 3;        $mo = $this->player->mo;        $this->sound('ion', $mo);        $slope = 0.0;        foreach ([0.0, 0.098, -0.098] as $d) {            [$s, $target] = $this->aimLineAttack($mo, $mo->angle + $d, 1024);            if ($target !== null) {                $slope = $s;                break;            }        }        $ball = $this->spawn('ionball', $mo->x, $mo->y, $mo->z + 32);        $ball->target = $mo;        $ball->angle = $mo->angle;        $speed = $ball->info['speed'];        $ball->momx = $speed * cos($mo->angle);        $ball->momy = $speed * sin($mo->angle);        $ball->momz = $speed * $slope;        $this->checkMissileSpawn($ball);    }}