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) } }