RU
games/doom-jumphp / src / Game / World.php · commit 8e1cb9f
<?phpnamespace App\Game;use App\Engine\Map\Level;use App\Engine\Map\Sector;/** * The world of a level, ticking 35 times a second like the original (g_game.c, p_tick.c, p_mobj.c, p_user.c): * things and their state machines, the player, moving sectors, lights. Movement and traces: Physics; * monster and weapon actions: Actions; doors, lifts, switches: Specials. */final class World{    use Physics;    use Actions;    use Specials;    public const TICRATE = 35;    public const FRICTION = 0.90625;    public const STOPSPEED = 0.0625;    public const MAXMOVE = 30.0;    public const GRAVITY = 1.0;    public const MAXSTEP = 24.0;    public const MELEERANGE = 64.0;    public const MISSILERANGE = 2048.0;    public const USERANGE = 64.0;    public const MAXRADIUS = 40.0;    public const SKILL_EASY = 1, SKILL_NORMAL = 2, SKILL_HARD = 3;    public Level $level;    public Player $player;    /** @var array<int, Mobj> */    public array $mobjs = [];    /** @var array<int, object> moving floors / ceilings, light effects */    public array $thinkers = [];    public int $tic = 0;    public int $skill;    public int $totalKills = 0;    public int $totalItems = 0;    public int $totalSecrets = 0;    /** sounds of this tic: [name, x, y] (x null = everywhere) */    public array $sounds = [];    public string $message = '';    public int $messageTics = 0;    /** '' while playing; 'exit' / 'secret' when the level is done */    public string $exit = '';    public int $exitDelay = 0;    /** @var array<int, array<int, Mobj>> thing block map */    public array $blockThings = [];    private int $nextId = 1;    private int $rnd;    public int $validCount = 0;    private ?Sector $lastSector = null;    public function __construct(Level $level, int $skill = self::SKILL_NORMAL, ?array $carry = null, int $seed = 1)    {        $this->level = $level;        $this->skill = $skill;        $this->rnd = abs($seed) % 65537;        $this->player = new Player();        if ($carry !== null) {            $this->player->restore($carry);        }        foreach ($level->things as $t) {            if ($t['type'] === 'player') {                $mo = $this->spawn('player', $t['x'], $t['y'], null);                $mo->angle = $t['angle'];                $mo->player = $this->player;                $this->player->mo = $mo;                $mo->health = $this->player->health;                continue;            }            if (!empty($t['def']['skill']) && !in_array($skill, (array)$t['def']['skill'], true)) {                continue;            }            $mo = $this->spawn($t['type'], $t['x'], $t['y'], null);            $mo->angle = $t['angle'];            if (!empty($t['def']['ambush'])) {                $mo->flags |= Info::AMBUSH;            }            if ($mo->is(Info::COUNTKILL)) {                $this->totalKills++;                if ($skill === self::SKILL_EASY && $mo->type === 'warden') {                    $mo->health = (int)($mo->health * 0.7);                }            }            if ($mo->is(Info::COUNTITEM)) {                $this->totalItems++;            }            // the original's random start: monsters do not move in step            if ($mo->tics > 0) {                $mo->tics = 1 + $this->random() % $mo->tics;            }        }        if (!isset($this->player->mo)) {            throw new \RuntimeException('The level has no player start');        }        foreach ($level->sectors as $s) {            if ($s->secret) {                $this->totalSecrets++;            }        }        $this->spawnSpecials();        $this->player->wState = 'READY';        $this->player->viewz = $this->player->pviewz = $this->player->mo->z + Player::VIEWHEIGHT;        $this->player->pangle = $this->player->mo->angle;        $this->setWeaponState('UP');        $this->player->wy = Weapons::LOWERED;    }    /** P_Random: 0..255, deterministic (demos, tests). */    public function random(): int    {        // a small LCG (75 x + 74 mod 65537): exact with 32-bit integers (the browser)        $this->rnd = ($this->rnd * 75 + 74) % 65537;        return $this->rnd & 255;    }    public function sound(string $name, ?Mobj $origin = null): void    {        $this->sounds[] = [$name, $origin?->x, $origin?->y];    }    public function message(string $text): void    {        $this->message = $text;        $this->messageTics = 4 * self::TICRATE;    }    // --------------------------------------------------------------------------------------- things    /** P_SpawnMobj. $z null = on the floor. */    public function spawn(string $type, float $x, float $y, ?float $z): Mobj    {        $mo = new Mobj($this->nextId++, $type, $x, $y, 0.0);        $sector = $this->level->sectorAt($x, $y);        $mo->floorz = $sector->floor;        $mo->ceilingz = $sector->ceil;        $mo->z = $mo->pz = $z ?? $sector->floor;        if ($this->skill === self::SKILL_HARD && $mo->is(Info::COUNTKILL)) {            $mo->reactiontime = (int)($mo->reactiontime / 2);        }        $this->mobjs[$mo->id] = $mo;        $this->link($mo);        // P_SpawnMobj: the first state without running its action        $st = Info::STATES[$mo->info['spawn']];        $mo->state = $mo->info['spawn'];        $mo->tics = $st[2];        $mo->sprite = $st[0];        $mo->frame = $st[1][0];        $mo->bright = strlen($st[1]) > 1;        return $mo;    }    /** Puts a thing in its sector's list and the thing block map. */    public function link(Mobj $mo): void    {        $sector = $this->level->sectorAt($mo->x, $mo->y);        if ($mo->sector !== $sector) {            if ($mo->sector !== null) {                unset($mo->sector->things[$mo->id]);            }            $mo->sector = $sector;            $sector->things[$mo->id] = $mo;        }        $lv = $this->level;        $bx = (int)floor(($mo->x - $lv->bmx) / Level::BLOCK);        $by = (int)floor(($mo->y - $lv->bmy) / Level::BLOCK);        $block = ($bx < 0 || $by < 0 || $bx >= $lv->bmw || $by >= $lv->bmh) ? -1 : $by * $lv->bmw + $bx;        if ($block !== $mo->block) {            if ($mo->block >= 0) {                unset($this->blockThings[$mo->block][$mo->id]);            }            $mo->block = $block;            if ($block >= 0) {                $this->blockThings[$block][$mo->id] = $mo;            }        }    }    public function remove(Mobj $mo): void    {        if ($mo->removed) {            return;        }        $mo->removed = true;        unset($this->mobjs[$mo->id]);        if ($mo->sector !== null) {            unset($mo->sector->things[$mo->id]);        }        if ($mo->block >= 0) {            unset($this->blockThings[$mo->block][$mo->id]);        }    }    /** P_SetMobjState: enters a state, runs its action; zero-tic states chain. */    public function setState(Mobj $mo, ?string $state): bool    {        do {            if ($state === null || $state === 'NULL' && $mo->type !== 'player') {                $mo->state = 'NULL';                $this->remove($mo);                return false;            }            $st = Info::STATES[$state];            $mo->state = $state;            $mo->tics = $st[2];            $mo->sprite = $st[0];            $mo->frame = $st[1][0];            $mo->bright = strlen($st[1]) > 1;            if ($st[3] !== null) {                $this->{'a' . ucfirst($st[3])}($mo);                if ($mo->removed) {                    return false;                }            }            $state = $st[4];            // @phpstan-ignore-next-line (no state of the table has 0 tics today)        } while ($mo->tics === 0 && $mo->state !== $state);        return true;    }    // ----------------------------------------------------------------------------------------- tic    /**     * One tic (1/35 s) of the world. $cmd: forward (-1..1, 2 = run), side, turn (radians), fire, use, weapon (1..5|null).     */    public function tic(array $cmd): void    {        $this->sounds = [];        $this->tic++;        foreach ($this->mobjs as $mo) {            $mo->px = $mo->x;            $mo->py = $mo->y;            $mo->pz = $mo->z;        }        $p = $this->player;        $p->pviewz = $p->viewz;        $p->pangle = $p->mo->angle;        $this->playerThink($cmd);        foreach ($this->thinkers as $k => $t) {            if ($t->done) {                unset($this->thinkers[$k]);                continue;            }            $this->runThinker($t);        }        foreach ($this->mobjs as $mo) {            if (!$mo->removed) {                $this->mobjThink($mo);            }        }        $this->sectorSpecials();        if ($this->messageTics > 0) {            $this->messageTics--;        }        if ($this->exitDelay > 0 && --$this->exitDelay === 0) {            $this->exit = $this->exit === '' ? 'exit' : $this->exit;        }    }    /** P_MobjThinker */    private function mobjThink(Mobj $mo): void    {        if ($mo->momx != 0 || $mo->momy != 0) {            $this->xyMovement($mo);            if ($mo->removed) {                return;            }        }        if ($mo->z != $mo->floorz || $mo->momz != 0) {            $this->zMovement($mo);            if ($mo->removed) {                return;            }        }        if ($mo->tics !== -1) {            $mo->tics--;            if ($mo->tics <= 0) {                $this->setState($mo, Info::STATES[$mo->state][4]);            }        }    }    // -------------------------------------------------------------------------------------- player    private function playerThink(array $cmd): void    {        $p = $this->player;        $mo = $p->mo;        if ($p->dead) {            $this->deathThink($cmd);            return;        }        // turning        $mo->angle = fmod($mo->angle + ($cmd['turn'] ?? 0.0) + 4 * M_PI, 2 * M_PI);        $onGround = $mo->z <= $mo->floorz;        $forward = (float)($cmd['forward'] ?? 0);        $side = (float)($cmd['side'] ?? 0);        if ($onGround) {            if ($forward != 0) {                $this->thrust($mo, $mo->angle, $forward * 25 / 32);            }            if ($side != 0) {                $this->thrust($mo, $mo->angle - M_PI / 2, $side * 24 / 32);            }        }        $this->calcHeight($onGround);        // the sector under the player        $sector = $mo->sector;        $entered = $sector !== $this->lastSector;        $this->lastSector = $sector;        if ($sector !== null && $mo->z <= $sector->floor) {            $this->playerInSpecialSector($sector, $entered);        }        // weapon change        $want = $cmd['weapon'] ?? null;        if ($want !== null && $want !== $p->weapon && !empty($p->weapons[$want]) && $p->pending === null) {            $p->pending = $want;        }        // use        if (!empty($cmd['use'])) {            if (!$p->useDown) {                $this->useLines();                $p->useDown = true;            }        } else {            $p->useDown = false;        }        $p->attackDown = !empty($cmd['fire']);        $this->movePsprite();        if ($p->damagecount > 0) {            $p->damagecount--;        }        if ($p->bonuscount > 0) {            $p->bonuscount--;        }        if ($p->flashTics > 0) {            $p->flashTics--;        }    }    public function thrust(Mobj $mo, float $angle, float $move): void    {        $mo->momx += $move * cos($angle);        $mo->momy += $move * sin($angle);    }    /** P_CalcHeight: view height, bobbing, smooth steps. */    private function calcHeight(bool $onGround): void    {        $p = $this->player;        $mo = $p->mo;        $p->bob = min(16.0, ($mo->momx ** 2 + $mo->momy ** 2) / 4);        if (!$onGround) {            $p->viewz = $mo->z + Player::VIEWHEIGHT;            if ($p->viewz > $mo->ceilingz - 4) {                $p->viewz = $mo->ceilingz - 4;            }            return;        }        $bob = $p->bob / 2 * sin(2 * M_PI * $this->tic / 20);        $p->viewheight += $p->deltaviewheight;        if ($p->viewheight > Player::VIEWHEIGHT) {            $p->viewheight = Player::VIEWHEIGHT;            $p->deltaviewheight = 0;        }        if ($p->viewheight < Player::VIEWHEIGHT / 2) {            $p->viewheight = Player::VIEWHEIGHT / 2;            if ($p->deltaviewheight <= 0) {                $p->deltaviewheight = 1 / 4;            }        }        if ($p->deltaviewheight != 0) {            $p->deltaviewheight += 1 / 4;            if ($p->deltaviewheight == 0) {                $p->deltaviewheight = 1 / 1024;            }        }        $p->viewz = $mo->z + $p->viewheight + $bob;        if ($p->viewz > $mo->ceilingz - 4) {            $p->viewz = $mo->ceilingz - 4;        }    }    private function deathThink(array $cmd): void    {        $p = $this->player;        $mo = $p->mo;        $this->movePsprite();        if ($p->viewheight > 6) {            $p->viewheight -= 1;        }        $p->viewz = $mo->z + $p->viewheight;        $p->bob = 0;        if ($p->attacker !== null && $p->attacker !== $mo && !$p->attacker->removed) {            $want = atan2($p->attacker->y - $mo->y, $p->attacker->x - $mo->x);            $delta = fmod($want - $mo->angle + 3 * M_PI, 2 * M_PI) - M_PI;            if (abs($delta) < 0.1) {                $mo->angle = $want;            } else {                $mo->angle += $delta > 0 ? 0.087 : -0.087;            }        }        if ($p->damagecount > 0) {            $p->damagecount--;        }    }    /** Fraction of the level done, for the intermission. */    public function stats(): array    {        $p = $this->player;        return [            'kills' => $this->totalKills ? (int)round($p->kills * 100 / $this->totalKills) : 100,            'items' => $this->totalItems ? (int)round($p->items * 100 / $this->totalItems) : 100,            'secrets' => $this->totalSecrets ? (int)round($p->secrets * 100 / $this->totalSecrets) : 100,            'time' => intdiv($this->tic, self::TICRATE),            'par' => $this->level->par,        ];    }    /** The sector things of a sector's bounding area (for moving floors / ceilings). @return Mobj[] */    public function thingsTouching(Sector $s): array    {        $out = [];        foreach ($this->mobjs as $mo) {            if ($mo->sector === $s) {                $out[] = $mo;                continue;            }            // a thing standing over the edge            foreach ($s->lines as $l) {                if ($mo->x + $mo->radius > $l->bbox[2] && $mo->x - $mo->radius < $l->bbox[3]                    && $mo->y + $mo->radius > $l->bbox[1] && $mo->y - $mo->radius < $l->bbox[0]                    && $this->boxOnLineSide($mo->y + $mo->radius, $mo->y - $mo->radius, $mo->x - $mo->radius, $mo->x + $mo->radius, $l) === -1) {                    $out[] = $mo;                    break;                }            }        }        return $out;    }}