!ABCDEFGHIJKLMNOPQRSTUVWXYZ.'; private const NUM_LETTERS = 30; private const NAME_MAX = 16; private const MENU_ITEMS = 6; private const IDLE_TO_ATTRACT = 50 * 40; private const MODE_NAMES = ['CLASSIQUE', 'AVENTURE']; private const AID_NAMES = ['AUCUNE', 'FACILE', 'SANS LIMITE DE TEMPS', 'FACILE ET SANS TEMPS']; private const SCR_ATTRACT = 0; private const SCR_MENU = 1; private const SCR_DEMO = 2; private const SCR_PLAY = 3; private const SCR_DEATH = 4; private const SCR_SCORE = 5; private const SCR_NAME = 6; private Assets $A; private Screen $screen; private Storage $storage; /** @var array */ private array $halls = []; private int $frame = 0; private int $screenId = self::SCR_ATTRACT; private int $page = 0; private int $timer = 0; private int $idle = 0; private int $scrollX = 0; private int $message = 0; private int $demoNo = 0; private ?Game $game = null; private bool $fireHeld = true; private int $lastKeys = 0; private float $stepAcc = 0; // menu private int $menuSel = 0; private int $levelIdx = 0; private bool $adventure = false; private int $aidMode = 0; private int $playedLevel = 0; private bool $playedAdventure = false; private bool $wonOnce = false; private bool $assisted = false; // score / name entry private int $finalScore = 0; private int $todayRank = -1; private int $allRank = -1; private string $name = ''; private bool $nameDone = false; private int $letter = 0; private int $repeat = 0; private bool $mapOverlay = false; // test / forcing options (like the C++ command line options) private int $forceRoom = -1; private int $forceY = 0; private int $forceX = 0; private int $forceHeld = -1; private int $forceItems = -1; private int $forceRewards = -1; private int $demoParam = -1; private float $seed = 0; public function enter(array $params = []): void { $this->A = Assets::get($this->engine->config); $this->screen = new Screen($this->A, $this->sdl()->getRenderer(), $this->sdl()); $this->screen->load(); $this->storage = $this->engine->storage; $this->loadSfx(); $this->levelIdx = (int)($params['level'] ?? 0); $this->adventure = (bool)($params['adventure'] ?? false); $this->aidMode = (int)($params['aids'] ?? 0); $this->seed = (float)($params['seed'] ?? fmod($this->engine->time() * 1000.0, 2147483648.0)); $this->forceRoom = (int)($params['room'] ?? -1); $this->forceY = (int)($params['y'] ?? 0); $this->forceX = (int)($params['x'] ?? 0); $this->forceHeld = (int)($params['held'] ?? -1); $this->forceItems = (int)($params['items'] ?? -1); $this->forceRewards = (int)($params['rewards'] ?? -1); $this->demoParam = (int)($params['demoNo'] ?? -1); $start = (string)($params['screen'] ?? 'attract'); if ($start === 'play') { $this->startGame(); } elseif ($start === 'demo') { $this->startDemo(); } else { $this->toAttract(); } if ((bool)($params['demo'] ?? false) && $this->screenId === self::SCR_ATTRACT) { $this->startDemo(); // the smoke test: the game plays itself } } private function loadSfx(): void { static $done = false; if ($done) { return; } $done = true; $sound = $this->sound(); foreach (Assets::SFX_FILES as $name) { if (!$sound->hasSfx($name)) { $sound->loadSfx($name, 'assets/sfx/' . $name . '.wav'); } } $sound->loadMusic('title', 'assets/music/title.mp3'); // playMusic() is idempotent } // ---- the halls of fame: one per level and mode ---- private function hall(): Hall { $L = $this->A->levels[$this->levelIdx]; $key = $L->key . (($this->adventure && !$L->dark) ? '_adventure' : ''); return $this->halls[$key] ??= new Hall($this->A, $this->storage); } // ---- flow ---- private function music(bool $on): void { if ($on) { $this->sound()->playMusic('title', true); } else { $this->sound()->stopMusic(); } } private function toAttract(): void { $this->screenId = self::SCR_ATTRACT; $this->page = 0; $this->timer = 0; $this->game = null; $this->mapOverlay = false; $this->music(true); } private function toMenu(): void { $this->screenId = self::SCR_MENU; $this->timer = 0; $this->idle = 0; $this->game = null; $this->mapOverlay = false; $this->music(true); } private function startGame(): void { $this->music(false); $this->screen->resetPicture(); $game = new Game($this->A, fmod($this->seed + fmod($this->frame * 2654435761.0, 4294967296.0), 4294967296.0)); $L = $this->A->levels[min($this->levelIdx, count($this->A->levels) - 1)]; $game->setLevel($L, $this->adventure); $game->setAssist(($this->aidMode & 1) === 1, ($this->aidMode & 2) === 2); $this->assisted = false; $this->mapOverlay = false; $this->playedLevel = $this->levelIdx; $this->playedAdventure = $this->adventure; $initiation = $this->wonOnce && $L->original && ($this->keys() & Values::IN_CHEAT) !== 0; $game->newGame($this->forceRoom, $this->forceY, $this->forceX, $initiation); $this->timer = 0; if ($this->forceItems >= 0) { $game->debugMagicItems($this->forceItems); } if ($this->forceHeld >= 0) { $game->debugHeld($this->forceHeld); } if ($this->forceRewards >= 0) { $game->debugRewards($this->forceRewards); } $this->game = $game; $this->screenId = self::SCR_PLAY; } private function startDemo(): void { $this->music(false); $this->screen->resetPicture(); $no = $this->demoParam >= 0 ? $this->demoParam : $this->demoNo; if ($this->demoParam < 0) { $this->demoNo++; } $game = new Game($this->A, fmod($this->seed + $this->frame, 4294967296.0)); $game->startDemo($this->A->demoScripts[$no % count($this->A->demoScripts)]); $this->game = $game; $this->screenId = self::SCR_DEMO; $this->fireHeld = true; } private function keys(): int { $a = $this->actions(); $keys = 0; $keys |= $a->isHeld('up') ? Values::IN_UP : 0; $keys |= $a->isHeld('down') ? Values::IN_DOWN : 0; $keys |= $a->isHeld('left') ? Values::IN_LEFT : 0; $keys |= $a->isHeld('right') ? Values::IN_RIGHT : 0; $keys |= $a->isHeld('confirm') ? Values::IN_ACTION | Values::IN_FIRE1 : 0; $keys |= $a->isHeld('cheat') ? Values::IN_CHEAT : 0; return $keys; } public function update(float $dt): void { $this->frame++; $this->screen->newFrame(); $keys = $this->keys(); $fire = ($keys & (Values::IN_ACTION | Values::IN_FIRE1)) !== 0; $firePressed = $fire && !$this->fireHeld; $this->fireHeld = $fire; switch ($this->screenId) { case self::SCR_ATTRACT: $this->timer++; if ($firePressed) { $this->toMenu(); break; } if ($this->page === 2) { if ($this->timer === 150 && $this->A->scrollMessages !== []) { $this->scrollX = Screen::SCREEN_W; } if ($this->timer > 150) { $this->scrollX -= 2; $m = $this->A->scrollMessages[$this->message % max(1, count($this->A->scrollMessages))] ?? ''; if (strlen($m) === 0 || $this->scrollX + strlen($m) * 8 < 0) { $this->message++; if ($this->message % max(1, count($this->A->scrollMessages)) === 4) { $this->page = 3; $this->timer = 0; $this->music(false); } else { $this->startDemo(); } } } } elseif ($this->page === 3) { if ($this->timer >= 175) { $this->startDemo(); } } elseif ($this->timer >= self::PAGE_STEPS) { $this->page++; $this->timer = 0; } break; case self::SCR_MENU: $this->stepMenu($keys, $firePressed); break; case self::SCR_DEMO: $this->stepGame($dt, false); $this->playSounds(); if ($firePressed) { $this->toMenu(); } elseif ($this->game->phaseOver()) { $this->toAttract(); $this->page = 0; } break; case self::SCR_PLAY: $this->game->setInput($keys); $this->stepGame($dt, true); $this->playSounds(); if ($this->game->secondQuest()) { $this->wonOnce = true; } if ($this->game->phaseOver()) { $this->screenId = self::SCR_DEATH; $this->timer = 0; $this->mapOverlay = false; if (!($this->game->endFlags() & Values::END_WON)) { $this->sound()->playSfx(Sfx::GAME_OVER); } } break; case self::SCR_DEATH: if (++$this->timer >= 150) { $this->screenId = self::SCR_SCORE; $this->timer = 0; } break; case self::SCR_SCORE: if (++$this->timer > 100 && $firePressed) { $this->finalScore = $this->game->score; $this->todayRank = ($this->assisted || $this->game->assisted()) ? -1 : $this->hall()->rank($this->finalScore, false); $this->allRank = $this->todayRank < 0 ? -1 : $this->hall()->rank($this->finalScore, true); if ($this->todayRank < 0) { $this->toMenu(); break; } $this->screenId = self::SCR_NAME; $this->letter = 0; $this->repeat = 0; $this->name = ''; $this->nameDone = false; $this->timer = 0; } break; case self::SCR_NAME: $this->stepNameEntry($keys, $firePressed); break; } $this->lastKeys = $keys; // pause / map / back $a = $this->actions(); if ($this->screenId === self::SCR_PLAY) { if ($a->isPressed('pause')) { $this->game->setPaused(!$this->game->paused()); } if ($a->isPressed('map')) { $this->mapOverlay = !$this->mapOverlay; } if ($a->isPressed('back')) { $this->toMenu(); } } } /** Runs the 50 game steps per second (3 logic ticks every 4 steps) from the frame time. */ private function stepGame(float $dt, bool $play): void { $this->stepAcc += $dt * 50.0; $n = (int)$this->stepAcc; $this->stepAcc -= $n; for ($i = 0; $i < $n; $i++) { $this->game->step(); if ($play) { $this->timer++; } } } private function playSounds(): void { foreach ($this->game->takeSounds() as $s) { $this->sound()->playSfx($s); } } // ---- menu ---- private function stepMenu(int $keys, bool $firePressed): void { $pressed = $keys & ~$this->lastKeys; if ($keys !== $this->lastKeys || $firePressed) { $this->idle = 0; } if (++$this->idle > self::IDLE_TO_ATTRACT) { $this->toAttract(); return; } $L = $this->A->levels[$this->levelIdx]; if ($pressed & Values::IN_UP) { $this->menuSel = ($this->menuSel + self::MENU_ITEMS - 1) % self::MENU_ITEMS; } if ($pressed & Values::IN_DOWN) { $this->menuSel = ($this->menuSel + 1) % self::MENU_ITEMS; } $d = ($pressed & Values::IN_RIGHT) ? 1 : (($pressed & Values::IN_LEFT) ? -1 : 0); if ($d !== 0 || ($firePressed && ($this->menuSel >= 1 && $this->menuSel <= 3))) { if ($d === 0) { $d = 1; } if ($this->menuSel === 1) { $this->levelIdx = ($this->levelIdx + count($this->A->levels) + $d) % count($this->A->levels); } elseif ($this->menuSel === 2 && !$L->dark) { $this->adventure = !$this->adventure; } elseif ($this->menuSel === 3) { $this->aidMode = ($this->aidMode + 4 + $d) % 4; } if ($this->menuSel >= 1 && $this->menuSel <= 3) { $this->sound()->playSfx(Sfx::CLICK); } } if ($firePressed && $d === 0) { if ($this->menuSel === 0) { $this->startGame(); } elseif ($this->menuSel === 4) { $this->toAttract(); $this->page = 0; $this->timer = 0; } elseif ($this->menuSel === 5) { $this->engine->quit(); } } } // ---- name entry ---- private function stepNameEntry(int $keys, bool $firePressed): void { if ($this->nameDone) { if (++$this->timer >= 75) { $n = $this->hall()->clean($this->name); $this->hall()->insert($n, $this->finalScore); $this->toMenu(); } return; } if (++$this->repeat >= 5) { $this->repeat = 0; if ($keys & Values::IN_LEFT) { $this->letter = ($this->letter + Hall::NUM_LETTERS - 1) % Hall::NUM_LETTERS; } elseif ($keys & Values::IN_RIGHT) { $this->letter = ($this->letter + 1) % Hall::NUM_LETTERS; } } if ($firePressed) { $ch = self::LETTERS[$this->letter]; if ($ch === '>') { $this->nameDone = true; } elseif ($ch === '<') { if ($this->name !== '') { $this->name = substr($this->name, 0, -1); } } else { $this->name .= $ch; if (strlen($this->name) >= self::NAME_MAX) { $this->nameDone = true; } } if ($this->nameDone) { $this->name = $this->hall()->clean($this->name); $this->timer = 0; } } } // ---- drawing ---- public function render($r): void { switch ($this->screenId) { case self::SCR_ATTRACT: if ($this->page === 0) { $this->drawTable($this->hall()->today(), 'LES PLUS GRANDS SORCIERS DE NOS JOURS'); } elseif ($this->page === 1) { $this->drawTable($this->hall()->allTime(), 'LE GRAND SORCIER DES TEMPS'); } elseif ($this->page === 3) { $this->screen->drawPicture($this->timer * 220); } else { $this->screen->drawTitle($this->timer * 6); $this->screen->text(15, 19, 'SORCERY+', 1); if ($this->timer > 150 && $this->A->scrollMessages !== []) { $m = $this->A->scrollMessages[$this->message % count($this->A->scrollMessages)]; $this->screen->textAtPixel($this->scrollX, 168, $m, 3); } if ((int)($this->frame / 25) % 2 === 0) { $this->screen->text(10, 24, 'APPUYEZ SUR TIR', 2); } } break; case self::SCR_MENU: $this->drawMenu(); break; case self::SCR_DEMO: $set = $this->demoNo % 2 === 0 ? 2 : 1; $this->screen->drawGame($this->game, $set, true); break; case self::SCR_PLAY: $this->screen->drawGame($this->game, 1, true); if ($this->mapOverlay) { $this->screen->drawMapOverlay($this->game); } if ($this->game->initiation() && $this->timer < 75) { $this->screen->text(1, 2, 'TRICHEZ...', 3); } if ($this->game->paused()) { $this->screen->text(16, 12, ' PAUSE ', 3); } break; case self::SCR_DEATH: $this->screen->drawGame($this->game, 1, true); $f = $this->game->endFlags(); $pack = !$this->game->lvl->original; if ($f & Values::END_WON) { $msg = $pack ? 'NIVEAU TERMINE: BRAVO!' : 'VOUS AVEZ VAINCU LE NECROMANCIEN'; } elseif ($f & Values::END_DROWNED) { $msg = 'VOUS ETES NOYE DANS LA RIVIERE.'; } elseif ($f & Values::END_NO_ENERGY) { $msg = "VOUS N'AVEZ PLUS D'ENERGIE."; } else { $msg = 'VOTRE TEMPS CE SONT EPUISES.'; } $this->screen->text(0, 20, $msg, 3); if (($f & Values::END_WON) && !$pack) { $this->screen->text(0, 22, '....CETTE FOIS....', 3); } break; case self::SCR_SCORE: $this->drawScore(); break; case self::SCR_NAME: $this->drawNameEntry(); break; } } private function drawTable(array $t, string $title): void { $this->screen->clearScreen(0); $this->screen->text(2, 3, $title, 1); foreach ($t as $i => $e) { $row = 8 + 2 * $i; $n = $e[0] . str_repeat('.', max(0, 21 - strlen($e[0]))); $this->screen->text(6, $row, substr($n, 0, 21), 1); $this->screen->text(27, $row, sprintf('%06d', $e[1]), 3); } } private function drawNameEntry(): void { $this->screen->clearScreen(0); $this->screen->text(14, 3, 'COMPLIMENTS!', 1); $this->screen->text(7, 5, 'VOTRE SCORE EST ELEVE!', 3); $this->screen->text(10, 7, 'ENTREZ VOTRE NOM', 1); for ($i = 0; $i < self::NUM_LETTERS; $i++) { $this->screen->text(5 + $i, 10, self::LETTERS[$i], 3); } $this->screen->text(5 + $this->letter, 11, '^', 2); $n = $this->name . str_repeat('.', max(0, self::NAME_MAX - strlen($this->name))); $this->screen->text(8, 15, substr($n, 0, self::NAME_MAX), 3); if (!$this->nameDone) { $this->screen->text(8 + strlen($this->name), 16, '^', 2); } } private function drawMenu(): void { $this->screen->drawTitle(720); $this->screen->rect(24, 12, 272, 176, [8, 8, 24, 200]); $this->screen->frameRect(24, 12, 272, 176, Assets::inkRgb(24)); $this->screen->textAtPixel(112, 20, 'SORCERY+', 1); $this->screen->textAtPixel(60, 32, 'CHOISISSEZ VOTRE AVENTURE', 3); $L = $this->A->levels[$this->levelIdx]; $lines = [ 'JOUER', 'NIVEAU : ' . $L->name, 'MODE : ' . ($L->dark ? 'AVENTURE' : self::MODE_NAMES[$this->adventure ? 1 : 0]), 'AIDES : ' . self::AID_NAMES[$this->aidMode], 'MEILLEURS SCORES', 'QUITTER', ]; foreach ($lines as $i => $line) { $y = 52 + $i * 14; $sel = $i === $this->menuSel; if ($sel) { $this->screen->textAtPixel(36, $y, '>', 2); } $this->screen->textAtPixel(48, $y, $line, $sel ? 1 : 3); } $this->screen->rect(32, 140, 256, 1, Assets::inkRgb(13)); if ($this->menuSel === 1 || $this->menuSel === 0) { $info = $L->description[0] ?? ''; } elseif ($this->menuSel === 2) { $info = ($L->dark || $this->adventure) ? 'OBSCURITE: TROUVEZ LES LANTERNES, UTILISEZ LES TELEPORTEURS' : "LES REGLES DE 1985, SANS RIEN D'AUTRE"; } elseif ($this->menuSel === 3) { $info = 'AIDES: SCORE NON ENREGISTRE'; } else { $info = 'HAUT/BAS: CHOISIR GAUCHE/DROITE: CHANGER'; } $cut = strlen($info) <= 36 ? strlen($info) : (int)(strrpos(substr($info, 0, 36), ' ') ?: 36); if ($cut === 0) { $cut = 36; } $this->screen->textAtPixel(32, 148, substr($info, 0, $cut), 3); if ($cut < strlen($info)) { $this->screen->textAtPixel(32, 160, substr($info, $cut + ($info[$cut] === ' ' ? 1 : 0), 36), 3); } $this->screen->textAtPixel(32, 176, 'TIR: VALIDER', 2); } private function drawScore(): void { $this->screen->clearScreen(0); $game = $this->game; $pack = !$game->lvl->original; $this->screen->text(12, 6, 'VOTRE SCORE EST', 3); $this->screen->text(17, 8, sprintf('%06d', $game->score), 1); $n = $game->rescued(); $this->screen->text(2, 11, $pack ? sprintf('ET VOUS AVEZ TROUVE %d OBJET%s MAGIQUE%s.', $n, $n === 1 ? '' : 'S', $n === 1 ? '' : 'S') : sprintf('ET VOUS AVEZ SAUVE %d SORCIER%s.', $n, $n === 1 ? '' : 'S'), 3); if (($game->endFlags() & Values::END_WON) && !$pack) { $this->screen->text(0, 14, 'FELICITATIONS! VOUS VENEZ DE LIBERER CE', 3); $this->screen->text(0, 16, 'ROYAUME DE LA FORCE DU MAL ET DU', 3); $this->screen->text(0, 18, 'NECROMANCIEN', 3); $this->screen->text(0, 20, 'CETTE FOIS....', 3); } elseif ($game->endFlags() & Values::END_WON) { $this->screen->text(6, 15, $game->lvl->name, 1); } $st = $game->stats; $sec = (int)($st->ticks * 2 / 75); $this->screen->text(2, 22, sprintf('TEMPS %d:%02d MONSTRES %d SALLES %d', (int)($sec / 60), $sec % 60, $st->kills, $st->rooms), 1); if ($this->assisted || $game->assisted()) { $this->screen->text(2, 23, 'AVEC AIDES: SCORE NON ENREGISTRE', 2); } if ($this->timer > 100 && (int)($this->frame / 25) % 2 === 0) { $this->screen->text(12, 24, 'APPUYEZ SUR TIR', 2); } } }