MOKTAR

par davidnurdin · Remakes
pas encore noté
▶ Jouer Sources Éditer GitHub
. / src / Titus / Game.php · commit 2fb9809
<?phpnamespace App\Titus;use JumPHP\Renderer\Sdl;/** * The port of the OpenTitus engine (Moktar). Every C module is a trait of this class, with the same * function names; the C globals (globals.h) are properties of the same name. * * The C code is blocking (it loops and flips the screen); here the blocking functions are * generators: `yield` = one flip of the screen. GameScene runs main() frame by frame. The value * yielded is the minimum time of that frame in ms (29 = the 35 Hz game loop, see NO_FAST_CPU). */final class Game{    use DrawTrait;    use ScrollTrait;    use SpritesTrait;    use ResetTrait;    use GatesTrait;    use ElevatorsTrait;    use ObjectsTrait;    use EnemiesTrait;    use PlayerTrait;    use AudioTrait;    use MenuTrait;    // --- globals.h constants ------------------------------------------------------------------------    public const TEST_ZONE = 4;    public const MAX_X = 4;    public const MAX_Y = 12;    public const MAP_LIMIT_Y = -1;    public const S_LINES = 12;    public const S_COLUMNS = 20;    public const LIMIT_TIMER = 22;    public const ALERT_X = 32;    public const screen_width = 20;    public const screen_height = 12;    public const FIRST_OBJET = 30;    public const FIRST_NMI = 101;    public const MAXIMUM_BONUS = 100;    public const MAXIMUM_ENERGY = 16;    public const MAXIMUM_DTRP = 4;    public const GESTION_X = 40;    public const GESTION_Y = 20;    public const MAX_SPEED_DEAD = 20;    public const HFLAG_NOWALL = 0;    public const HFLAG_WALL = 1;    public const HFLAG_BONUS = 2;    public const HFLAG_DEADLY = 3;    public const HFLAG_CODE = 4;    public const HFLAG_PADLOCK = 5;    public const HFLAG_LEVEL14 = 6;    public const FFLAG_NOFLOOR = 0;    public const FFLAG_FLOOR = 1;    public const FFLAG_SSFLOOR = 2;    public const FFLAG_SFLOOR = 3;    public const FFLAG_VSFLOOR = 4;    public const FFLAG_DROP = 5;    public const FFLAG_LADDER = 6;    public const FFLAG_BONUS = 7;    public const FFLAG_WATER = 8;    public const FFLAG_FIRE = 9;    public const FFLAG_SPIKES = 10;    public const FFLAG_CODE = 11;    public const FFLAG_PADLOCK = 12;    public const FFLAG_LEVEL14 = 13;    public const CFLAG_NOCEILING = 0;    public const CFLAG_CEILING = 1;    public const CFLAG_LADDER = 2;    public const CFLAG_PADLOCK = 3;    public const CFLAG_DEADLY = 4;    public const TITUS_ERROR_QUIT = -1;    // keys (logical names, see Input)    public const KEY_F1 = 'f1';    public const KEY_F2 = 'f2';    public const KEY_E = 'e';    public const KEY_F4 = 'f4';    public const KEY_LEFT = 'left';    public const KEY_RIGHT = 'right';    public const KEY_UP = 'up';    public const KEY_DOWN = 'down';    public const KEY_JUMP = 'jump';    public const KEY_SPACE = 'action';    public const KEY_ENTER = 'confirm';    public const KEY_RETURN = 'confirm';    public const KEY_ESC = 'esc';    public const KEY_P = 'pause';    public const KEY_NOCLIP = 'n';    public const KEY_GODMODE = 'g';    public const KEY_DEBUG = 'd';    public const KEY_MUSIC = 'music';    public const KEY_SKIP = 's';    public const SPRITECOUNT = 356;    public const LEVELCOUNT = 16;    /** Level files, in game order ("0J123456789BCEFG") */    public const LEVEL_FILES = ['LEVEL0', 'LEVELJ', 'LEVEL1', 'LEVEL2', 'LEVEL3', 'LEVEL4', 'LEVEL5', 'LEVEL6',        'LEVEL7', 'LEVEL8', 'LEVEL9', 'LEVELB', 'LEVELC', 'LEVELE', 'LEVELF', 'LEVELG'];    public const LEVEL_CODES = ['EFE8', '5165', '67D4', '2BDA', '11E5', '86EE', '4275', 'A0B9', '501C', 'E9ED',        'D4E6', 'A531', 'CE96', 'B1A4', 'EBEA', '3B9C'];    public const LEVEL_TITLES = [        '     A LA RECHERCHE DE LA ZOUBIDA',        '          LES QUARTIERS CHICS',        '           ATTENTION TRAVAUX',        '         LES COULOIRS DU METRO',        '       LES CATACOMBES INFERNALES',        '         ARRIVEE DANS LA CITE',        '       L IMMEUBLE DE LA ZOUBIDA',        '      SOUS LE CHEMIN DE MARRAKECH',        '            LA CITE ENFOUIE',        '             DESERT PRIVE',        '          LA VILLE DES SABLES',        '            LE PHARE OUEST',        '             UN BON TUYAU',        '           DE RETOUR AU PAYS',        '           DIRECTION BARBES',        '              BIG BISOUS',    ];    // --- globals.h variables ------------------------------------------------------------------------    public int $RESETLEVEL_FLAG = 0;    /** triggers a game over */    public bool $GAMEOVER_FLAG = false;    /** timer for health bar */    public int $BAR_FLAG = 0;    /** true if left or right key is pressed */    public bool $X_FLAG = false;    /** true if up or down key is pressed */    public bool $Y_FLAG = false;    /** headache timer */    public int $CHOC_FLAG = 0;    /** player sprite array */    public int $action = 0;    /** hit/burn timer */    public int $KICK_FLAG = 0;    /** If set, player will be "burned" when hit (fireballs) */    public bool $GRANDBRULE_FLAG = false;    /** True if in a ladder */    public bool $LADDER_FLAG = false;    /** True if player is forced into kneestanding because of low ceiling */    public bool $PRIER_FLAG = false;    /** 6 if free fall or in the middle of a jump, decremented if on solid surface. Must be 0 to initiate a jump. */    public int $SAUT_FLAG = 0;    /** Last action (kneestand + jump = silent walk) */    public int $LAST_ORDER = 0;    /** Silent walk timer */    public int $FURTIF_FLAG = 0;    /** True if an object is throwed forward */    public bool $DROP_FLAG = false;    public bool $DROPREADY_FLAG = false;    /** true if carrying something (add 16 to player sprite) */    public bool $CARRY_FLAG = false;    public bool $POSEREADY_FLAG = false;    /** Frames since last action change */    public int $ACTION_TIMER = 0;    /** When non-zero, boss is invulnerable */    public int $INVULNERABLE_FLAG = 0;    /** When non-zero, the flying carpet is flying */    public int $TAPISFLY_FLAG = 0;    /** When non-zero, fall through certain floors (after key down) */    public int $CROSS_FLAG = 0;    /** When zero, skip object gravity function */    public int $GRAVITY_FLAG = 0;    /** Smoke when object hits the floor */    public int $FUME_FLAG = 0;    /** Lives */    public int $LIFE = 0;    public int $YFALL = 0;    public bool $POCKET_FLAG = false;    /** If false, there are no animated tiles on the screen */    public bool $PERMUT_FLAG = false;    /** Increased every loop in game loop (0 to 3) */    public int $loop_cycle = 0;    /** Current tile animation (0-1-2), changed every 4th game loop cycle */    public int $tile_anim = 0;    /** Screen offset (X) in tiles */    public int $BITMAP_X = 0;    /** Point to the left tile in the tile screen (0 to 19) */    public int $BITMAP_XM = 0;    /** Screen offset (Y) in tiles */    public int $BITMAP_Y = 0;    /** Point to the top tile in the tile screen (0 to 11) */    public int $BITMAP_YM = 0;    /** If true, the screen will scroll in X */    public bool $XSCROLL_CENTER = false;    /** If scrolling: scroll until player is in this tile (X) */    public int $XLIMIT_SCROLL = 0;    /** The engine will not scroll past this tile before the player have crossed the line (X) */    public int $XLIMIT = 0;    /** If true, the screen will scroll in Y */    public bool $YSCROLL_CENTER = false;    /** If scrolling: scroll until player is in this tile (Y) */    public int $YLIMIT_SCROLL = 0;    /** The engine will not scroll below this tile before the player have gone below (Y) (uint8) */    public int $ALTITUDE_ZERO = 0;    /** Increased every loop in game loop (0 to 0x0FFF) */    public int $IMAGE_COUNTER = 0;    /** 1: walk right, 0: stand still, -1: walk left, triggers the ACTION_TIMER if it changes */    public int $SENSX = 0;    /** Incremented from 0 to 3 when accelerating while jumping, stop acceleration upwards if >= 3 */    public int $SAUT_COUNT = 0;    public bool $NOSCROLL_FLAG = false;    /** Finish a level */    public bool $NEWLEVEL_FLAG = false;    /** Number of bosses that needs to be killed to finish */    public int $BIGNMI_NBR = 0;    /** Used for enemies walking and popping up */    public int $TAUPE_FLAG = 0;    /** Flying carpet state */    public int $TAPISWAIT_FLAG = 0;    /** Counter when hit */    public int $SEECHOC_FLAG = 0;    /** Skip NO_FAST_CPU */    public bool $NFC_FLAG = false;    /** Lives of the boss */    public int $BIGNMI_POWER = 0;    /** True if the boss is alive */    public bool $boss_alive = false;    public int $AUDIOMODE = 1;    /** If true, the player will not interfere with the enemies */    public bool $GODMODE = false;    /** If true, the player will move noclip */    public bool $NOCLIP = false;    public bool $DISPLAYLOOPTIME = false;    /** cheat keys (G, N, S) */    public int $devmode = 0;    // --- port state ----------------------------------------------------------------------------------    public Level $level;    public Screen $screen;    /** @var SpriteData[] */    private array $sprites = [];    /** @var ObjectData[] */    private array $objects = [];    /** @var int[][] the C anim_player table (row 1 = the Moktar walk) */    public array $anim_player = [];    /** Game time in ms (SDL_GetTicks) */    private float $ms = 0.0;    /** First level of a demo / test run (enter() params) */    public ?int $startLevel = null;    /** Called when the player quits from the menu (desktop: closes the game) */    public ?\Closure $onQuit = null;    public function __construct(        public readonly Sdl $sdl,        public readonly Input $input,        public readonly string $dataDir,        public readonly ?\JumPHP\Audio\SoundManager $sound = null,    ) {        $this->level = new Level();        $this->screen = new Screen();        $this->anim_player = OriginalData::ANIM_PLAYER;        $this->anim_player[1] = OriginalData::ANIM_PLAYER_MOKTAR;    }    /** The data files are there (the game cannot run without them). */    public static function hasData(string $dataDir): bool    {        foreach (['SPRITES.SQZ', 'FONTS.SQZ', 'MENU.SQZ', 'LEVEL0.SQZ'] as $file) {            if (!is_file($dataDir . '/' . $file)) {                return false;            }        }        return true;    }    public function file(string $name): string    {        return $this->dataDir . '/' . $name;    }    /** GameScene: time of the frame. */    public function advanceTime(float $ms): void    {        $this->ms += $ms;    }    /** SDL_GetTicks() */    public function SDL_GetTicks(): int    {        return (int)$this->ms;    }    // --- opentitus.c --------------------------------------------------------------------------------    /** main(): intro text, logo, intro picture, then menu <-> game forever. */    public function main(): \Generator    {        $this->initoriginal();        $this->loadfonts();        $this->initaudio();        if ($this->startLevel !== null) { // demo / tests: straight into a level            yield from $this->playtitus($this->startLevel);        }        $state = true;        $retval = yield from $this->viewintrotext();        if ($retval < 0) {            $state = false;        }        if ($state) {            $retval = yield from $this->viewimage('TITUS.SQZ', 2, 0, 4000);        }        $this->SELECT_MUSIC(15);        if ($state && $retval >= 0) {            yield from $this->viewimage('TITRE.SQZ', 2, 0, 6500);        }        while (true) {            $retval = yield from $this->viewmenu('MENU.SQZ', 2);            if ($retval <= 0) {                // Escape in the menu: the original quits the program                if ($this->onQuit !== null) {                    ($this->onQuit)();                }                yield 0;                continue;            }            if ($retval <= self::LEVELCOUNT) {                yield from $this->playtitus($retval - 1);                $this->SELECT_MUSIC(15);            }        }    }    /** original.c initoriginal(): object data, sprites */    private function initoriginal(): void    {        $this->objects = [];        foreach (OriginalData::OBJECT_FLAG as $i => $flag) {            $o = new ObjectData();            $o->maxspeedY = OriginalData::OBJECT_MAXSPEEDY[$i];            $o->support = ($flag & 0x01) !== 0;            $o->bounce = ($flag & 0x02) !== 0;            $o->gravity = ($flag & 0x04) !== 0;            $o->droptobottom = ($flag & 0x08) !== 0;            $o->no_damage = ($flag & 0x10) !== 0;            $this->objects[$i] = $o;        }        $this->loadsprites();    }    /** sprites.c loadsprites() */    private function loadsprites(): void    {        $b = Sqz::load($this->file('SPRITES.SQZ'));        $offset = 0;        $this->sprites = [];        for ($i = 0; $i < self::SPRITECOUNT; $i++) {            $w = OriginalData::SPRITE_WIDTH[$i];            $h = OriginalData::SPRITE_HEIGHT[$i];            $this->sprites[$i] = new SpriteData($i, $w, $h,                OriginalData::SPRITE_COLL[$i * 2], OriginalData::SPRITE_COLL[$i * 2 + 1],                OriginalData::SPRITE_REF[$i * 2], -(OriginalData::SPRITE_REF[$i * 2 + 1] - $h),                Gfx::planar($b, $offset, $w, $h));            $offset += ($w * $h) >> 1;        }    }    /** level number (0-15) -> index in the original tables (getlevelid) */    public function getlevelid(int $levelnumber): int    {        foreach (OriginalData::LEVEL_ID as $i => $id) {            if ($id === $levelnumber + 1) {                return $i;            }        }        return 0;    }    /** engine.c playtitus(): plays from $firstlevel to the end (or game over / quit). */    private function playtitus(int $firstlevel): \Generator    {        $level = $this->level;        $level->lives = 2;        $level->extrabonus = 0;        for ($n = $firstlevel; $n < self::LEVELCOUNT; $n++) {            $level->load(Sqz::load($this->file(self::LEVEL_FILES[$n] . '.SQZ')), $n, $this->sprites, $this->objects);            $level->levelid = $this->getlevelid($n);            $level->levelnumber = $n;            $this->PERMUT_FLAG = $level->permut;            $this->ALTITUDE_ZERO = $level->altitudeZero & 0xFF;            $this->XLIMIT = $level->xLimit;            $this->screen->loadLevelGraphics($this->sdl, $level);            $first = true;            while (true) {                $this->SELECT_MUSIC(0);                $this->CLEAR_DATA($level);                $this->GODMODE = false;                $this->NOCLIP = false;                $this->DISPLAYLOOPTIME = false;                $retval = yield from $this->viewstatus($level, $first);                $first = false;                if ($retval < 0) {                    return $retval;                }                $this->SELECT_MUSIC(OriginalData::LEVEL_MUSIC[$level->levelid]);                yield from $this->INIT_SCREENM($level);                $this->TFR_SCREENM();                yield from $this->flip_screen(true);                $retval = yield from $this->playlevel($level);                if ($retval < 0) {                    return $retval;                }                if ($this->NEWLEVEL_FLAG) {                    break;                }                if ($this->RESETLEVEL_FLAG === 1) {                    if ($level->lives === 0) {                        $this->GAMEOVER_FLAG = true;                    } else {                        $level->lives--;                        yield from $this->death($level);                    }                }                if ($this->GAMEOVER_FLAG) {                    yield from $this->gameover($level);                    return 0;                }            }        }        return 0;    }    /** engine.c playlevel(): the game loop */    private function playlevel(Level $level): \Generator    {        $firstrun = true;        do {            if (!$firstrun) {                $this->DISPLAY_COUNT($level);                $this->RETURN_MUSIC();                yield from $this->flip_screen(true);            }            $firstrun = false;            $this->IMAGE_COUNTER = ($this->IMAGE_COUNTER + 1) & 0x0FFF;            $this->MOVE_TRP($level);            $this->move_objects($level);            $retval = yield from $this->move_player($level);            if ($retval === self::TITUS_ERROR_QUIT) {                return $retval;            }            $this->MOVE_NMI($level);            $this->MOVE_TRASH($level);            $this->SET_NMI($level);            yield from $this->CROSSING_GATE($level);            $this->SPRITES_ANIMATION($level);            $this->scroll($level);            $this->BLOC_ANIMATION($level);            $this->TFR_SCREENM();            $this->DISPLAY_SPRITES($level);            $retval = yield from $this->RESET_LEVEL($level);            if ($retval < 0) {                return $retval;            }        } while ($retval === 0);        return 0;    }    /** engine.c death() */    private function death(Level $level): \Generator    {        $player = $level->player;        $this->SELECT_MUSIC(1);        $this->FORCE_POSE($level);        $this->updatesprite($level, $player->sprite, 13, true); // death        $player->sprite->speedY = 15;        for ($i = 0; $i < 60; $i++) {            $this->TFR_SCREENM();            $this->DISPLAY_SPRITES($level);            yield from $this->flip_screen(true);            $player->sprite->speedY--;            if ($player->sprite->speedY < -16) {                $player->sprite->speedY = -16;            }            $player->sprite->y -= $player->sprite->speedY;        }        yield from $this->WAIT_SONG();        $this->SELECT_MUSIC(0);        yield from $this->CLOSE_SCREEN();    }    /** engine.c gameover() */    private function gameover(Level $level): \Generator    {        $player = $level->player;        $this->SELECT_MUSIC(2);        $this->updatesprite($level, $player->sprite, 13, true); // death        $this->updatesprite($level, $player->sprite2, 333, true); // game        $player->sprite2->x = ($this->BITMAP_X << 4) - (120 - 2);        $player->sprite2->y = ($this->BITMAP_Y << 4) + 100;        $this->updatesprite($level, $player->sprite3, 334, true); // over        $player->sprite3->x = ($this->BITMAP_X << 4) + (320 + 120 - 2);        $player->sprite3->y = ($this->BITMAP_Y << 4) + 100;        for ($i = 0; $i < 31; $i++) {            $this->TFR_SCREENM();            $this->DISPLAY_SPRITES($level);            yield from $this->flip_screen(true);            $player->sprite2->x += 8;            $player->sprite3->x -= 8;        }        $retval = yield from $this->waitforbutton();        if ($retval < 0) {            return $retval;        }        yield from $this->fadeout();        return 0;    }    // --- common.c / keyboard.c ------------------------------------------------------------------------    /** Decreases a value with 1 until it's zero */    public function subto0(int &$number): void    {        if ($number > 0) {            $number--;        }    }    /** keystate[key]: is the key held now */    public function keystate(string $key): bool    {        return $this->input->held($key);    }    /**     * SDL_PollEvent: the key events since the last step.     * @return array<int, array{0:string, 1:string}> ['down'|'up', key]     */    public function pollEvents(): array    {        return $this->input->takeEvents();    }    /** waitforbutton(): 0 = Return / Enter / Space, -1 = Escape */    public function waitforbutton(): \Generator    {        while (true) {            foreach ($this->pollEvents() as [$type, $key]) {                if ($type !== 'down') {                    continue;                }                if ($key === self::KEY_RETURN || $key === self::KEY_SPACE) {                    return 0;                }                if ($key === self::KEY_ESC) {                    return -1;                }                $this->musicKey($key);            }            yield 0;        }    }    /** KEY_MUSIC handling, common to every waiting loop */    public function musicKey(string $key): void    {        if ($key === self::KEY_MUSIC) {            $this->AUDIOMODE++;            if ($this->AUDIOMODE > 1) {                $this->AUDIOMODE = 0;            }            if ($this->AUDIOMODE === 1) {                $this->startmusic();            } else {                $this->stopmusic();            }        }    }    // --- C integer helpers ---------------------------------------------------------------------------    /** (uint8) */    public static function u8(int $v): int    {        return $v & 0xFF;    }    /** (int8) */    public static function s8(int $v): int    {        $v &= 0xFF;        return $v >= 0x80 ? $v - 0x100 : $v;    }    /** (uint16) */    public static function u16(int $v): int    {        return $v & 0xFFFF;    }    /** (int16) */    public static function s16(int $v): int    {        $v &= 0xFFFF;        return $v >= 0x8000 ? $v - 0x10000 : $v;    }    /** C integer division (truncates toward zero) */    public static function div(int $a, int $b): int    {        return intdiv($a, $b);    }}