FR

Slave Rift

par tueb2024 · Tir
5,0 (1)
▶ Jouer 💬 Avis Sources 🕘 Versions Éditer GitHub
games/slave-rift / src / Scene / PlayScene.php · commit 9ea6094
<?phpnamespace App\Scene;use App\Audio\RiftMusic;use App\Audio\RiftSfx;use App\Game\AssembledLevel;use App\Game\BrushMap;use App\Game\DifficultyDirector;use App\Game\LevelAssembler;use App\Game\LevelGraph;use App\Game\RhythmClock;use App\Game\Run;use App\Game\TreasureDirector;use App\Gfx\WeaponRenderer;use App\Gfx\World;use JumPHP\Loop\FixedStep;use JumPHP\Renderer\Renderer;use JumPHP\Scene\AbstractScene;use JumPHP\Ui\Hud;final class PlayScene extends AbstractScene{    private Run $run;    private World $world;    private WeaponRenderer $weaponRenderer;    private FixedStep $step;    private RiftMusic $music;    private RhythmClock $rhythm;    private DifficultyDirector $difficulty;    private bool $demo = false;    private bool $debug = false;    private bool $procedural = false;    private int $levelSeed = 1337;    private int $proceduralDepth = 7;    private ?AssembledLevel $assembledLevel = null;    private ?TreasureDirector $objective = null;    private float $frameMs = 16.7;    private bool $paused = false;    private bool $jumpPending = false;    private float $restartDelay = 0.0;    private float $pitch = 0.0;    private float $prevMuzzle = 0.0;    private float $prevPulse = 0.0;    private float $prevSpecial = 0.0;    private float $weaponSfxCooldown = 0.0;    private string $prevState = 'play';    private int $prevXp = 0;    private int $upgradeCursor = 0;    public function enter(array $params = []): void    {        $this->demo = (bool)($params['demo'] ?? false);        $this->debug = (bool)($params['debug'] ?? false);        $this->procedural = (bool)($params['procedural'] ?? true);        $this->levelSeed = (int)($params['seed'] ?? ($this->demo ? 1337 : random_int(1, 65520)));        $this->proceduralDepth = max(5, min(24, (int)($params['depth'] ?? 7)));        if ($this->procedural) {            $graph = new LevelGraph($this->levelSeed, $this->proceduralDepth);            $this->assembledLevel = (new LevelAssembler())->assemble($graph);            $map = $this->assembledLevel->map;        } else {            $this->assembledLevel = null;            $map = BrushMap::load($this->engine->config->path('assets/maps/quake-dm1.map'));        }        $this->run = new Run($map, $this->levelSeed);        $this->objective = $this->assembledLevel !== null            ? TreasureDirector::fromLevel($this->assembledLevel, max(12, min(24, 8 + $this->proceduralDepth)))            : null;        $this->difficulty = new DifficultyDirector();        $this->world ??= new World();        $this->weaponRenderer ??= new WeaponRenderer();        $this->weaponRenderer->reset($this->run->time);        $this->step = new FixedStep(60, 8);        $this->rhythm = new RhythmClock();        $this->restartDelay = 0.0;        $this->paused = false;        $this->jumpPending = false;        $this->pitch = 0.0;        $this->prevMuzzle = $this->prevPulse = $this->prevSpecial = 0.0;        $this->weaponSfxCooldown = 0.0;        $this->prevState = 'play';        $this->prevXp = 0;        $this->upgradeCursor = 0;        $this->world->prepare($this->engine->gl(), $this->run);        $this->loadAssetLayer();        if (!isset($this->music)) {            $this->music = new RiftMusic($this->sound());            $this->music->register();        }    }    private function loadAssetLayer(): void    {        foreach ([            'weapon_colt' => 'assets/sprites/weapons/fps-colt.png',            'weapon_mp44' => 'assets/sprites/weapons/fps-mp44.png',            'weapon_bar' => 'assets/sprites/weapons/fps-bar.png',            'weapon_gatling' => 'assets/sprites/weapons/fps-gatling.png',        ] as $key => $path) {            if (!$this->sdl()->hasTexture($key)) $this->sdl()->loadTexture($key, $path);        }        RiftSfx::register($this->sound());    }    public function update(float $dt): void    {        $this->frameMs = $this->frameMs * 0.9 + $dt * 1000.0 * 0.1;        if ($this->actions()->isPressed('pause_game')) $this->paused = !$this->paused;        if ($this->paused) return;        if ($this->run->state === 'upgrade') {            if ($this->demo) {                $this->run->chooseUpgrade(0);                return;            }            $count = count($this->run->choices);            if ($count > 0) {                if ($this->actions()->isPressed('upgrade_prev')) {                    $this->upgradeCursor = ($this->upgradeCursor - 1 + $count) % $count;                }                if ($this->actions()->isPressed('upgrade_next')) {                    $this->upgradeCursor = ($this->upgradeCursor + 1) % $count;                }                if ($this->actions()->isPressed('upgrade_select')) {                    $this->run->chooseUpgrade($this->upgradeCursor);                    return;                }            }            foreach (['upgrade_one', 'upgrade_two', 'upgrade_three'] as $slot => $action) {                if ($this->actions()->isPressed($action)) {                    $this->upgradeCursor = $slot;                    $this->run->chooseUpgrade($slot);                    return;                }            }            return;        }        if ($this->run->state === 'dead') {            $this->restartDelay += $dt;            if (($this->demo && $this->restartDelay > 1.0) || $this->actions()->isPressed('restart')) {                $this->enter([                    'demo' => $this->demo,                    'debug' => $this->debug,                    'procedural' => $this->procedural,                    'seed' => $this->levelSeed,                    'depth' => $this->proceduralDepth,                ]);            }            return;        }        if ($this->demo) {            [$forward, $strafe, $turn, $fire] = $this->run->autopilot();        } else {            $forward = $this->actions()->axis('move_back', 'move_forward');            $strafe = $this->actions()->axis('strafe_left', 'strafe_right');            $turn = $this->actions()->axis('turn_left', 'turn_right');            $fire = false;            $this->jumpPending = $this->jumpPending || $this->actions()->isPressed('jump');        }        $this->advanceObjectiveBeforeStep();        $beforeState = $this->run->state;        $beforeXp = $this->run->xp;        $this->step->run($dt, function (float $h) use ($forward, $strafe, $turn, $fire): void {            $this->difficulty->beforeStep($this->run);            $rhythmSteps = $this->rhythm->advance($h, $this->run->level);            $this->run->step($h, $forward, $strafe, $turn, $fire, $this->jumpPending, false, $rhythmSteps);            $this->difficulty->afterStep($this->run);            $this->jumpPending = false;        });        $this->playAudioEvents($beforeState, $beforeXp, $dt);        $this->music->playSteps($this->rhythm->drain(), $this->run->level);    }    private function advanceObjectiveBeforeStep(): void    {        if ($this->objective === null || $this->run->state !== 'play') return;        if ($this->objective->updatePlayer($this->run->x, $this->run->y, $this->run->z)) {            // Keep anything still alive or still scheduled from the current wave:            // reaching B does not erase unfinished cleanup. The dedicated final            // room wave is added on top and must be completely cleared.            $this->run->remaining += $this->objective->finalWaveSize;            $this->run->intermission = 0.0;            $this->run->waveTime = 0.0;            $this->run->message = 'SALLE FINALE - NETTOIE TOUT';            $this->run->messageTime = 3.2;        }        if ($this->objective->state === TreasureDirector::FINAL_WAVE) {            // The legacy survival loop ends ordinary waves after 26 seconds.            // A final-room cleanup is objective-based, never time-based.            $this->run->waveTime = min($this->run->waveTime, 24.5);            if ($this->run->remaining === 0 && $this->run->enemies === []) {                if ($this->objective->notifyFinalWaveCleared()) {                    // Hold the legacy wave scheduler while boss combat takes over.                    $this->run->intermission = 3600.0;                    $this->run->message = 'SALLE NETTOYEE - BOSS';                    $this->run->messageTime = 3.0;                }            }        } elseif (in_array($this->objective->state, [TreasureDirector::BOSS_READY, TreasureDirector::BOSS, TreasureDirector::TREASURE_READY, TreasureDirector::COMPLETE], true)) {            $this->run->intermission = max($this->run->intermission, 3600.0);            $this->run->remaining = 0;        }    }    private function playAudioEvents(string $beforeState, int $beforeXp, float $dt): void    {        foreach ($this->run->drainPrimaryAudioEvents() as $stage) {            $this->sound()->playSfx('rift_hat', 0, false);            $name = $stage <= 1 ? 'colt_fire' : ($stage <= 3 ? 'sten_fire' : 'heavy_fire');            $this->sound()->playSfx($name, 0, true);        }        if ($this->run->pulse > $this->prevPulse + 0.01) $this->sound()->playSfx('pulse_sfx', 0, false);        if ($this->run->specialFlash > $this->prevSpecial + 0.01) $this->sound()->playSfx('special_sfx', 0, false);        if ($beforeState === 'play' && $this->run->state === 'upgrade') {            $this->upgradeCursor = 0;            $this->sound()->playSfx('level_up', 0, false);        }        if ($this->run->xp > $beforeXp) $this->sound()->playSfx('pickup_xp', 0, false);        $this->prevMuzzle = $this->run->muzzle;        $this->prevPulse = $this->run->pulse;        $this->prevSpecial = $this->run->specialFlash;        $this->prevState = $this->run->state;        $this->prevXp = $this->run->xp;    }    public function render(Renderer $r): void    {        $run = $this->run;        $this->world->render($this->engine, $r, $run, $this->pitch);        $font = $this->font();        $w = $this->width(); $h = $this->height(); $cx = intdiv($w, 2); $cy = intdiv($h, 2);        $cream = [240,215,170]; $cyan = [90,235,225];        $r->fillRect(0, 0, $w, 23, [6,5,12,225]);        $font->draw($r, 'HP ' . $run->health . '/' . $run->maxHealth, 4, 3, 1, $cream);        $font->draw($r, 'W' . $run->wave . '  K' . $run->kills . '  ' . $this->clock($run->time), $w - 4, 3, 1, $cream, 'right');        $font->draw($r, 'SCORE ' . number_format($run->score, 0, '.', ' ') . '  x' . number_format($run->multiplier, 1), $cx, 3, 1, [255,205,90], 'center');        Hud::bar($r, 4, 13, 82, 4, max(0.0, $run->health / $run->maxHealth));        $font->draw($r, 'LV' . $run->level . ' XP ' . $run->xp . '/' . $run->nextXp, 96, 13, 1, $cyan);        if ($run->combo > 0) $font->draw($r, 'CHAIN ' . $run->combo, 170, 13, 1, [255,155,90]);        Hud::bar($r, $w - 70, 14, 66, 3, min(1.0, $run->xp / $run->nextXp));        $cross = $run->flameActive > 0 ? [255,145,75] : ($run->muzzle > 0 ? [255,225,120] : [185,190,175]);        $r->drawLine($cx - 4, $cy, $cx - 2, $cy, $cross); $r->drawLine($cx + 2, $cy, $cx + 4, $cy, $cross);        $r->drawLine($cx, $cy - 4, $cx, $cy - 2, $cross); $r->drawLine($cx, $cy + 2, $cx, $cy + 4, $cross);        $weaponKey = $this->weaponRenderer->textureKey($run->primaryStage);        $weapon = $this->sdl()->getTexture($weaponKey);        $this->weaponRenderer->render($r, $run, $weapon);        if ($run->flameActive > 0) $font->draw($r, 'FLAME SWEEP', $cx, $h - 40, 1, [255,170,90], 'center');        $r->fillRect(0, $h - 10, $w, 10, [6,5,12,225]);        $arsenal = $run->arsenalLabel();        $font->draw($r, $arsenal, 4, $h - 8, 1, $cyan);        $font->draw($r, count($run->enemies) . ' HOSTILES', $w - 4, $h - 8, 1, $cream, 'right');        $messageY = 29;        if ($this->procedural && $this->assembledLevel !== null && $this->assembledLevel->treasureAnchor !== null && $run->state === 'play') {            if ($this->objective !== null && $this->objective->state !== TreasureDirector::EXPLORING) {                $objectiveText = $this->objective->objectiveLabel();                if ($this->objective->state === TreasureDirector::FINAL_WAVE) {                    $objectiveText .= '  ' . ($run->remaining + count($run->enemies));                }                $font->draw($r, $objectiveText, $cx, 27, 1, [255,205,90], 'center', [8,5,12]);            } else {                $treasure = $this->assembledLevel->treasureAnchor;                $dx = $treasure['x'] - $run->x;                $dz = $treasure['z'] - $run->z;                $distance = hypot($dx, $dz);                $delta = fmod(atan2($dx, $dz) - $run->yaw + M_PI, 2 * M_PI);                if ($delta < 0) $delta += 2 * M_PI;                $delta -= M_PI;                $marker = abs($delta) < 0.22 ? '^' : ($delta > 0 ? '>' : '<');                $metres = (int)round($distance);                $label = $marker === '<' ? '<  TRESOR ' . $metres . 'm' : ($marker === '>' ? 'TRESOR ' . $metres . 'm  >' : '^ TRESOR ' . $metres . 'm');                $font->draw($r, $label, $cx, 27, 1, [255,205,90], 'center', [8,5,12]);            }            $messageY = 38;        }        if ($run->messageTime > 0 && $run->state === 'play') $font->draw($r, $run->message, $cx, $messageY, 1, $cream, 'center', [8,5,12]);        if ($run->time < 6 && !$this->demo && $run->state === 'play') {            $font->draw($r, 'NETTOIE LE NIVEAU / TROUVE LE TRESOR / AMELIORE TON SCORE', $cx, $cy - 42, 1, $cream, 'center', [8,5,12]);        }        if ($run->time < 8 && !$this->demo) $font->draw($r, 'Z/S MOVE / Q/D STRAFE / LEFT-RIGHT TURN / WEAPONS AUTO / SPACE JUMP', $cx, $h - 28, 1, $cream, 'center', [8,5,12]);        if ($run->hurt > 0) { $r->fillRect(0, 23, 3, $h - 33, [240,55,55]); $r->fillRect($w - 3, 23, 3, $h - 33, [240,55,55]); }        if ($run->state === 'upgrade') {            $r->fillRect(12, 42, $w - 24, 104, [8,6,16,245]);            $font->draw($r, 'LEVEL ' . $run->level . ' - CHOOSE', $cx, 49, 2, $cyan, 'center');            foreach ($run->choices as $slot => $choice) {                $selected = $slot === $this->upgradeCursor;                $prefix = $selected ? '> ' : '  ';                $font->draw($r, $prefix . ($slot + 1) . '  ' . $choice, 24, 72 + $slot * 22, 1, $selected ? $cyan : $cream);                $font->draw($r, $run->upgradeDescription($choice), 24, 81 + $slot * 22, 1, $cyan);            }            $font->draw($r, 'UP-DOWN CHOOSE / ENTER OR SPACE CONFIRM', $cx, 135, 1, $cream, 'center');        } elseif ($run->state === 'dead') {            $r->fillRect(26, 56, $w - 52, 69, [8,5,12,235]);            $font->draw($r, 'RIFT CLAIMED YOU', $cx, 65, 2, [245,90,76], 'center');            $font->draw($r, 'WAVE ' . $run->wave . ' / ' . $run->kills . ' KILLS / ' . $this->clock($run->time), $cx, 88, 1, $cream, 'center');            $font->draw($r, $this->demo ? 'DEMO RESTART' : 'ENTER TO TRY AGAIN', $cx, 109, 1, $cyan, 'center');        }        if ($this->paused) { $r->fillRect(82, 72, $w - 164, 30, [8,5,12,235]); $font->draw($r, 'PAUSED - P', $cx, 83, 1, $cream, 'center'); }        if ($this->demo) $font->draw($r, 'DEMO', $w - 4, 27, 1, $cream, 'right');        if ($this->debug) {            $balance = $this->difficulty->snapshot($run);            $font->draw($r, sprintf('FPS %.0f  %.1f MS  E %d/%d  RLOS %d  SLOS %d  MAP %d  NEI %d',                1000.0 / max(1.0, $this->frameMs), $this->frameMs, $this->world->visibleEnemies,                count($run->enemies), $this->world->renderLosTests, $run->losTests,                $this->world->mapChunksDrawn, $run->neighborChecks), 4, 28, 1, $cyan);            $font->draw($r, sprintf('TR %d  COR %d  IMP %d  COL %d  PICK %d', count($run->traces), count($run->corpses),                min(count($run->traces), World::MAX_IMPACTS), $run->collisionTests, count($run->pickups)), 4, 38, 1, $cyan);            $font->draw($r, sprintf('INPUT KEYBOARD  TURN %.2f RAD/S  %s  BPM %.0f  WPN %s', Run::TURN_SPEED, $arsenal, $this->rhythm->bpm($run->level), $this->weaponRenderer->state()), 4, 48, 1, $cyan);            $font->draw($r, sprintf('BAL PWR x%.2f  DPS %.0f  ENEMY HP x%.2f  DMG x%.2f',                $balance['powerIndex'], $balance['primaryDps'], $balance['healthScale'], $balance['damageScale']), 4, 58, 1, $cyan);            if ($this->procedural && $this->assembledLevel !== null) {                $objectiveState = $this->objective?->state ?? 'none';                $font->draw($r, sprintf('PROC SEED %d  ROOMS %d  DEPTH %d  OBJ %s', $this->levelSeed, count($this->assembledLevel->rooms), $this->proceduralDepth, $objectiveState), 4, 68, 1, $cyan);            }        }    }    private function clock(float $time): string    {        return sprintf('%02d:%02d', intdiv((int)$time, 60), (int)$time % 60);    }}