Anmelden
DE
games/sorcery / src / Scene / MainScene.php · commit 613f5fb
<?phpnamespace App\Scene;use App\Data\Assets;use App\Data\Hall;use App\Data\Level;use App\Game\Game;use App\Game\Sfx;use App\Game\Values;use App\Render\Screen;use JumPHP\Input\ActionMap;use JumPHP\Save\Storage;use JumPHP\Audio\SoundManager;use JumPHP\Renderer\Sdl;use JumPHP\Scene\AbstractScene;/** * The whole flow of Sorcery+ (a port of modern/src/app.cpp, without the level editor): * attract mode (score tables, title, picture, two self playing demos), menu, play, death, score, name entry. * update() runs 60 times per second; the game itself runs at 50 steps per second. */final class MainScene extends AbstractScene{    private const PAGE_STEPS = 200;    private const LETTERS = '<>!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<string, Hall> */    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);        }    }}