로그인
KO
games/doom-jumphp / src / Game / Specials.php · commit 8e1cb9f
<?phpnamespace App\Game;use App\Engine\Map\LevelBuilder;use App\Engine\Map\Line;use App\Engine\Map\Sector;/** * Doors, lifts, switches, exits, light effects, damaging floors, secrets (p_doors.c, p_plats.c, p_switch.c, * p_lights.c, p_spec.c). */trait Specials{    private const DOOR_SPEED = 2.0;    private const DOOR_WAIT = 150;    private const LIFT_SPEED = 4.0;    private const LIFT_WAIT = 105;    private function spawnSpecials(): void    {        foreach ($this->level->sectors as $s) {            $fx = $s->def['fx'] ?? null;            if ($fx !== null) {                $min = $s->def['minlight'] ?? max(0, $this->lowestNeighbourLight($s));                $t = (object)['kind' => 'light', 'fx' => $fx, 'sector' => $s, 'max' => $s->light, 'min' => min($min, $s->light - 16),                    'count' => 1 + $this->random() % 32, 'dir' => -1, 'done' => false];                $this->thinkers[] = $t;            }            if ($s->kind === 'lift') {                // lifts start up: their floor at the highest neighbour, or their own height                $s->def['high'] = $s->floor;            }        }    }    private function lowestNeighbourLight(Sector $s): int    {        $min = $s->light;        foreach ($this->level->neighbours($s) as $o) {            $min = min($min, $o->light);        }        return $min;    }    private function runThinker(object $t): void    {        match ($t->kind) {            'door' => $this->doorThink($t),            'lift' => $this->liftThink($t),            'light' => $this->lightThink($t),            default => null,        };    }    // ----------------------------------------------------------------------------------- planes    /**     * T_MovePlane: moves a floor or a ceiling towards $dest. Returns 'ok', 'past' (arrived) or 'crushed'     * (something does not fit: the move is undone).     */    private function movePlane(Sector $s, float $speed, float $dest, bool $floor, int $dir): string    {        $old = $floor ? $s->floor : $s->ceil;        $new = $old + $speed * $dir;        $past = ($dir < 0 && $new <= $dest) || ($dir > 0 && $new >= $dest);        if ($past) {            $new = $dest;        }        if ($floor) {            $s->floor = $new;        } else {            $s->ceil = $new;        }        if (!$this->changeSector($s)) {            // only refuse when it closes on something (ceiling down, floor up)            if (($floor && $dir > 0) || (!$floor && $dir < 0)) {                if ($floor) {                    $s->floor = $old;                } else {                    $s->ceil = $old;                }                $this->changeSector($s);                return 'crushed';            }        }        return $past ? 'past' : 'ok';    }    /** P_ChangeSector: the things of a sector follow its floor; false when one does not fit. */    private function changeSector(Sector $s): bool    {        $fits = true;        foreach ($this->thingsTouching($s) as $mo) {            if ($mo->is(Info::CORPSE) && $mo->ceilingz - $mo->floorz < $mo->height) {                continue;            }            if (!$this->heightClip($mo) && ($mo->is(Info::SOLID) || $mo->is(Info::SHOOTABLE))) {                $fits = false;            }        }        return $fits;    }    // ------------------------------------------------------------------------------------ doors    /** EV_VerticalDoor: a door used from one of its sides. */    private function useDoor(Mobj $user, Line $line): bool    {        $s = $line->backSector;        if ($s === null) {            return false;        }        $key = $line->args['key'] ?? '';        if ($key !== '') {            if ($user->player === null) {                return false;            }            if (empty($user->player->keys[$key])) {                $this->message('YOU NEED THE ' . strtoupper($key) . ' KEYCARD TO OPEN THIS DOOR.');                $this->sound('noway', $user);                return false;            }        }        $d = $s->mover;        if ($d !== null) {            if ($d->kind !== 'door') {                return false;            }            if ($d->dir === -1) {                $d->dir = 1;            // going down: back up                $this->sound('dooropen', null);                return true;            }            if ($user->player === null) {                return false;            }            $d->dir = -1;               // open: close it            $this->sound('doorclose', null);            return true;        }        $this->openDoor($s, 'normal');        return true;    }    private function openDoor(Sector $s, string $type): void    {        $top = $this->level->lowestNeighbourCeiling($s) - 4;        $t = (object)['kind' => 'door', 'type' => $type, 'sector' => $s, 'dir' => 1, 'top' => $top, 'count' => 0, 'done' => false];        $s->mover = $t;        $this->thinkers[] = $t;        $this->soundAt('dooropen', $s);    }    /** A gate (opened by a switch or the warden's death): opens and stays open. */    private function openGate(Sector $s): void    {        if ($s->mover !== null || $s->ceil > $s->floor + 8) {            return;        }        $this->openDoor($s, 'open');    }    private function soundAt(string $name, Sector $s): void    {        $this->sounds[] = [$name, $s->cx, $s->cy];    }    private function doorThink(object $d): void    {        $s = $d->sector;        if ($d->dir === 0) {            if (--$d->count <= 0) {                $d->dir = -1;                $this->soundAt('doorclose', $s);            }            return;        }        if ($d->dir === 1) {            if ($this->movePlane($s, self::DOOR_SPEED, $d->top, false, 1) === 'past') {                if ($d->type === 'open') {                    $d->done = true;                    $s->mover = null;                    return;                }                $d->dir = 0;                $d->count = self::DOOR_WAIT;            }            return;        }        $res = $this->movePlane($s, self::DOOR_SPEED, $s->floor, false, -1);        if ($res === 'past') {            $d->done = true;            $s->mover = null;        } elseif ($res === 'crushed') {            $d->dir = 1;            $this->soundAt('dooropen', $s);        }    }    // ------------------------------------------------------------------------------------ lifts    /** EV_DoPlat downWaitUpStay */    private function useLift(Sector $s): bool    {        if ($s->mover !== null) {            return false;        }        $low = min($s->floor, $this->level->lowestNeighbourFloor($s));        $high = $s->def['high'] ?? $s->floor;        if ($low >= $s->floor) {            return false;        }        $t = (object)['kind' => 'lift', 'sector' => $s, 'low' => $low, 'high' => $high, 'status' => 'down', 'count' => 0, 'done' => false];        $s->mover = $t;        $this->thinkers[] = $t;        $this->soundAt('lift', $s);        return true;    }    private function liftThink(object $t): void    {        $s = $t->sector;        switch ($t->status) {            case 'up':                $res = $this->movePlane($s, self::LIFT_SPEED, $t->high, true, 1);                if ($res === 'crushed') {                    $t->count = self::LIFT_WAIT;                    $t->status = 'down';                    $this->soundAt('lift', $s);                } elseif ($res === 'past') {                    $t->done = true;                    $s->mover = null;                    $this->soundAt('liftstop', $s);                }                break;            case 'down':                if ($this->movePlane($s, self::LIFT_SPEED, $t->low, true, -1) === 'past') {                    $t->count = self::LIFT_WAIT;                    $t->status = 'waiting';                    $this->soundAt('liftstop', $s);                }                break;            case 'waiting':                if (--$t->count <= 0) {                    $t->status = $s->floor <= $t->low ? 'up' : 'down';                    $this->soundAt('lift', $s);                }                break;        }    }    // ----------------------------------------------------------------------------------- lights    private function lightThink(object $t): void    {        $s = $t->sector;        switch ($t->fx) {            case 'flicker':       // T_LightFlash                if (--$t->count > 0) {                    return;                }                if ($s->light === $t->max) {                    $s->light = $t->min;                    $t->count = ($this->random() & 7) + 1;                } else {                    $s->light = $t->max;                    $t->count = ($this->random() & 63) + 1;                }                break;            case 'strobe':        // T_StrobeFlash                if (--$t->count > 0) {                    return;                }                if ($s->light === $t->min) {                    $s->light = $t->max;                    $t->count = 5;                } else {                    $s->light = $t->min;                    $t->count = 15;                }                break;            case 'glow':          // T_Glow                $s->light += 8 * $t->dir;                if ($s->light <= $t->min) {                    $s->light = $t->min;                    $t->dir = 1;                } elseif ($s->light >= $t->max) {                    $s->light = $t->max;                    $t->dir = -1;                }                break;            case 'fire':          // T_FireFlicker                if (--$t->count > 0) {                    return;                }                $s->light = max($t->min, $t->max - ($this->random() & 3) * 16);                $t->count = 4;                break;        }    }    // ------------------------------------------------------------------------------ use, sectors    /** P_UseSpecialLine */    private function useSpecialLine(Mobj $user, Line $line): bool    {        if ($user->player === null) {            // monsters only open plain doors            return $line->special === 'door' && ($line->args['key'] ?? '') === '' && $this->useDoor($user, $line);        }        switch ($line->special) {            case 'door':                return $this->useDoor($user, $line);            case 'lift':                if ($line->backSector !== null) {                    return $this->useLift($line->backSector);                }                return false;            case 'exit':                $this->flipSwitch($line);                $this->exit = 'exit';                $this->exitDelay = 0;                return true;            case 'switch':                if (!empty($line->args['used'])) {                    return false;                }                $this->flipSwitch($line);                foreach ($this->level->sectorsOf($line->args['target']) as $s) {                    if ($s->kind === 'lift') {                        $this->useLift($s);                    } else {                        $this->openGate($s);                    }                }                return true;        }        return false;    }    private function flipSwitch(Line $line): void    {        $line->args['used'] = true;        $line->front->mid = $line->args['on'] ?? LevelBuilder::switchOn((string)$line->front->mid);        $this->sound('switch', $this->player->mo);    }    /** P_PlayerInSpecialSector: damage floors, secrets, exits, lifts that take the player down. */    private function playerInSpecialSector(Sector $s, bool $entered): void    {        $p = $this->player;        if ($s->damage > 0 && ($this->tic & 31) === 0) {            $this->damage($p->mo, null, null, $s->damage);        }        if ($s->secret) {            $s->secret = false;            $p->secrets++;            $this->message('A SECRET AREA!');            $this->sound('secret');        }        if ($s->kind === 'exit' && $this->exit === '') {            $this->exit = 'exit';        }        if ($entered && $s->kind === 'lift' && $s->mover === null && $s->floor >= ($s->def['high'] ?? $s->floor)) {            $this->useLift($s);        }    }    private function sectorSpecials(): void    {        // lights and moving sectors are thinkers; animated flats follow the tic count (Art::animate)    }}