ES
games/open-lemmings / src / Scene / PlayScene.php · commit 04e2e2a
<?phpnamespace App\Scene;use App\Service\Art;use App\Service\Audio;use App\Service\Fonts;use App\Service\Progress;use App\Sim\Autoplay;use App\Sim\LevelBuilder;use App\Sim\Levels;use App\Sim\Lumi;use App\Sim\World;use App\View\Camera3d;use App\View\WorldView;use JumPHP\Kernel\Platform;use JumPHP\Renderer\Renderer;use JumPHP\Scene\AbstractScene;use JumPHP\Scene\HotReloadable;/** * A level: the player picks a skill in the bar and clicks a Lumikin to give it. enter() params: level (0-based), * demo (true: the level plays its own solution, then the next one: the smoke test), auto (true: the solution plays with * the bar shown: screenshots of the site), angle (-45 / 45: starts in the 3D view, when GL exists: tests, screenshots), * force3d (tests: the 3D logic without GL), bloom (shots: the glow option on / off). * * The two arrows at the top right (keys [ and ]) turn the camera by 45 degrees: the level is then drawn in 3D by the GL * layer (World3d) and the clicks are cast on the level's middle plane (Camera3d::unproject()). At 0 degrees the 2D * view is drawn as it always was. Without GL the arrows are greyed and do nothing. */final class PlayScene extends AbstractScene implements HotReloadable{    public const HUD_Y = 232;    private const TICK = 1 / World::TICKS_PER_SECOND;    private int $level = 0;    private bool $demo = false;    private array $def = [];    private World $world;    private ?WorldView $view = null;    private ?Autoplay $auto = null;    private string $skill = 'dig';    private float $camX = 0;    private float $camY = 0;    /** 1 = the whole height of the level, up to 3 = three times closer */    private float $zoom = 1.0;    private ?\App\View\Pinch $pinch = null;    /** a pinch happened during this touch: no tap, no drag until every finger is up */    private bool $pinchLock = false;    /** frames without the pointer held (a pinch left on by a lost finger-up is dropped after a few) */    private int $idleFrames = 0;    /** phones: a touch is a tap (a skill given on release) until it moves */    private bool $tapPending = false;    private float $acc = 0;    private bool $fast = false;    private float $banner = 0;    private float $endTimer = 0;    private bool $ended = false;    private float $nukeArmed = 0;    private bool $dragging = false;    private float $dragFrom = 0;    private float $dragCam = 0;    private float $dragFromY = 0;    private float $dragCamY = 0;    /** @var float[]|null the level point grabbed by a drag in the 3D view: it stays under the pointer */    private ?array $dragLevel = null;    private ?Lumi $hover = null;    private float $time = 0;    private bool $mouseSeen = false;    private string $tip = '';    private float $tipTime = 0;    /** the camera's turn in degrees (eased) and where it goes: -45, 0 or 45 */    private float $yaw = 0;    private float $yawTarget = 0;    private bool $layerOn = false;    /** tests: the 3D logic (camera, clicks, meshes) without GL, nothing drawn */    private bool $force3d = false;    public const YAW_STEP = 45;    public function __construct(private Art $art, private Fonts $fonts, private Progress $progress, private Audio $audio)    {    }    public function enter(array $params = []): void    {        if (isset($params['bloom'])) {   // tests, screenshots (headless runs have a temporary var/): the glow shader on / off            $this->engine->options->set('bloom', (bool)$params['bloom']);        }        $this->art->load($this->engine);        $this->audio->load($this->sound());        $this->level = max(0, min(Levels::count() - 1, (int)($params['level'] ?? 0)));        $this->demo = (bool)($params['demo'] ?? false);        $this->def = Levels::get($this->level);        $this->world = LevelBuilder::build($this->def);        $this->view?->release();        $this->view = new WorldView($this->engine, $this->art, $this->world, $this->def['theme']);        $this->auto = $this->demo || !empty($params['auto']) ? new Autoplay($this->world, $this->def['solution']) : null;        $this->skill = 'dig';        foreach (World::SKILLS as $s) {            if ($this->world->skills[$s] > 0) {                $this->skill = $s;                break;            }        }        $this->zoom = 1.0;        $this->camY = 0;        $this->camX = $this->clampCam($this->world->hatch[0] - 140);        $input = $this->engine->input;        $controls = $input->getControls();        if (!$controls instanceof \App\View\Pinch) {            $controls = new \App\View\Pinch($controls);            $input->setControls($controls);        }        $this->pinch = $controls;        if (isset($params['zoom'])) {   // tests, screenshots            $this->zoomAt((float)$params['zoom'], 100, 150);        }        $this->acc = 0;        $this->fast = false;        $this->banner = $this->demo || !empty($params['auto']) ? 0 : 3.2;        $this->endTimer = 0;        $this->ended = false;        $this->nukeArmed = 0;        $this->dragging = false;        $this->pinchLock = false;        $this->tapPending = false;        $this->idleFrames = 0;        $this->hover = null;        $this->time = 0;        $this->mouseSeen = false;        $this->tip = '';        $this->tipTime = 0;        $this->yaw = 0;        $this->yawTarget = 0;        $this->force3d = !empty($params['force3d']);        $this->layer(false);        if (isset($params['angle']) && $this->can3d()) {   // tests, screenshots: straight into the 3D view            $this->yawTarget = max(-self::YAW_STEP, min(self::YAW_STEP, (float)$params['angle']));            $this->view->world3d()->prepare($this->camera3d($this->yawTarget), 1e6);            $this->yaw = $this->yawTarget;        }        $music = ['forest' => 'play', 'cave' => 'play2', 'forge' => 'play3', 'night' => 'play4', 'desert' => 'play5', 'ice' => 'play6',            'jungle' => 'play7', 'sky' => 'play8',            'autumn' => 'play9', 'reef' => 'play10', 'clockwork' => 'play11'][$this->def['theme']] ?? 'play';        if (!$this->demo && $this->sound()->getCurrentMusic() !== $music) {            $this->sound()->playMusic($music);        }        $this->progress->current = $this->level;    }    public function exit(): void    {        $this->layer(false);        $this->view?->release();        $this->view = null;    }    public function hotState(): array    {        return ['level' => $this->level, 'demo' => $this->demo];    }    public function hotRestore(array $state): void    {        if ($state['level'] !== $this->level || $state['demo'] !== $this->demo) {            $this->enter($state);        }    }    public function world(): World    {        return $this->world;    }    public function view(): WorldView    {        return $this->view ?? throw new \LogicException('not entered');    }    public function camX(): float    {        return $this->camX;    }    private function clampCam(float $x): float    {        $this->camY = max(0, min(WorldView::VIEW_H - WorldView::VIEW_H / $this->zoom, $this->camY));        return max(0, min($this->world->terrain->width - WorldView::VIEW_W / $this->zoom, $x));    }    // --- the 3D view ------------------------------------------------------------------------------------------    /** The 3D view needs GL (never in the title's demo). */    public function can3d(): bool    {        return !$this->demo && ($this->force3d || $this->engine->gl()->isAvailable());    }    public function yaw(): float    {        return $this->yaw;    }    public function yawTarget(): float    {        return $this->yawTarget;    }    /** True while the level is drawn in 3D (any angle but 0, transitions included). */    public function is3d(): bool    {        return $this->yaw != 0.0;    }    /** An arrow: the camera turns by 45 degrees to the left (-1) or the right (1), up to 45 degrees. */    public function turn(int $dir): void    {        if (!$this->can3d()) {            return;        }        $target = max(-self::YAW_STEP, min(self::YAW_STEP, $this->yawTarget + $dir * self::YAW_STEP));        if ($target != $this->yawTarget) {            $this->yawTarget = $target;            $this->sound()->playSfx('select');        }    }    public function camera3d(?float $yaw = null): Camera3d    {        $shake = $this->view !== null && $this->view->shake > 0 ? round(sin($this->view->time() * 90) * 2.5 * min(1, $this->view->shake * 4)) : 0.0;        return new Camera3d($this->camX, $this->camY, $this->zoom, $yaw ?? $this->yaw, $shake);    }    /** A view point to level coordinates: through the 3D camera when the view is turned. @return float[] [x, y] */    public function toLevel(float $vx, float $vy): array    {        if ($this->is3d()) {            $p = $this->camera3d()->unproject($vx, $vy);            if ($p !== null) {                return $p;            }        }        return [$vx / $this->zoom + $this->camX, $vy / $this->zoom + $this->camY];    }    /** The GL layer under the 2D: on in 3D only, so that the 2D path stays exactly as it was. */    private function layer(bool $on): void    {        if ($on === $this->layerOn) {            return;        }        $this->layerOn = $on;        $gl = $this->engine->gl();        if (!$gl->isAvailable()) {            return;        }        $on ? $gl->setLayer('below', 'view', null) : $gl->removeLayer();    }    /** Eases the camera's turn; the first turn waits until the terrain in view is built in 3D. */    private function updateYaw(float $dt): void    {        if ($this->yawTarget == 0.0 && $this->yaw == 0.0) {            return;        }        if (!$this->can3d()) {            $this->yaw = $this->yawTarget = 0;            return;        }        $starting = $this->yaw == 0.0;        $ready = $this->view()->world3d()->prepare($this->camera3d($starting ? $this->yawTarget : $this->yaw), $starting ? 14 : 4);        if ($starting && !$ready) {            return;        }        $this->yaw += ($this->yawTarget - $this->yaw) * min(1, $dt * 6);        if (abs($this->yawTarget - $this->yaw) < 0.3) {            $this->yaw = $this->yawTarget;        }    }    public function pinch(): ?\App\View\Pinch    {        return $this->pinch;    }    public function camY(): float    {        return $this->camY;    }    public function zoom(): float    {        return $this->zoom;    }    /** Zooms by $factor, keeping the level point under the view point ($vx, $vy) where it is. */    public function zoomAt(float $factor, float $vx, float $vy): void    {        $old = $this->zoom;        $this->zoom = max(1.0, min(3.0, $old * $factor));        if (abs($this->zoom - 1.0) < 0.02) {            $this->zoom = 1.0;        }        $this->camY += $vy / $old - $vy / $this->zoom;        $this->camX = $this->clampCam($this->camX + $vx / $old - $vx / $this->zoom);    }    // --- the bar's buttons: [x, y, w, h] -----------------------------------------------------------------------    /** @return array<string, array{0:int,1:int,2:int,3:int}> */    public static function buttons(): array    {        $b = [];        foreach (World::SKILLS as $i => $skill) {            $b[$skill] = [4 + $i * 35, 235, 32, 32];        }        $b['minus'] = [290, 235, 24, 15];        $b['plus'] = [316, 235, 24, 15];        $b['pause'] = [346, 235, 32, 32];        $b['fast'] = [381, 235, 32, 32];        $b['nuke'] = [416, 235, 32, 32];        return $b;    }    /** The two arrows that turn the camera, at the top right of the level. @return array<string, array{0:int,1:int,2:int,3:int}> */    public static function viewButtons(): array    {        return ['turn_left' => [425, 15, 24, 15], 'turn_right' => [451, 15, 24, 15]];    }    /** The arrow under a view point; without GL the greyed arrows let the clicks through to the level. */    private function viewButtonAt(float $x, float $y): ?string    {        if (!$this->can3d()) {            return null;        }        foreach (self::viewButtons() as $name => [$bx, $by, $bw, $bh]) {            if ($x >= $bx - 1 && $x < $bx + $bw + 1 && $y >= $by - 2 && $y < $by + $bh + 2) {                return $name;            }        }        return null;    }    private function buttonAt(float $x, float $y): ?string    {        foreach (self::buttons() as $name => [$bx, $by, $bw, $bh]) {            if ($x >= $bx - 1 && $x < $bx + $bw + 1 && $y >= $by - 2 && $y < $by + $bh + 2) {                return $name;            }        }        return null;    }    /** A click / tap on the bar. */    public function pressButton(string $name): void    {        if (in_array($name, World::SKILLS, true)) {            $this->skill = $name;            $this->sound()->playSfx('select');            $this->tip = 'SKILL ' . $name;            $this->tipTime = 2.5;            return;        }        switch ($name) {            case 'minus': $this->world->changeRate(-10); $this->sound()->playSfx('select'); break;            case 'plus': $this->world->changeRate(10); $this->sound()->playSfx('select'); break;            case 'pause': $this->scenes()->push(PauseScene::class, ['level' => $this->level]); break;            case 'fast': $this->fast = !$this->fast; $this->sound()->playSfx('select'); break;            case 'nuke':                if ($this->nukeArmed > 0) {                    $this->world->nuke();                    $this->nukeArmed = 0;                } else {                    $this->nukeArmed = 2.0;                    $this->sound()->playSfx('select');                    $this->tip = 'NUKE CONFIRM';                    $this->tipTime = 2.0;                }                break;        }    }    /** A click / tap in the level (view coordinates): gives the selected skill to the Lumikin there. */    public function clickAt(float $vx, float $vy): bool    {        [$x, $y] = $this->toLevel($vx, $vy);        $l = $this->world->pick($x, $y, $this->skill, Platform::isMobile() ? 12 : 8);        if ($l === null) {            return false;        }        if (!$this->world->assign($l, $this->skill)) {            $this->sound()->playSfx('deny');        }        return true;    }    public function update(float $dt): void    {        $this->time += $dt;        $this->banner = max(0, $this->banner - $dt);        $this->nukeArmed = max(0, $this->nukeArmed - $dt);        $this->tipTime = max(0, $this->tipTime - $dt);        $a = $this->actions();        if (!$this->demo) {            if ($a->isPressed('pause') || !$this->engine->lifecycle()->isVisible()) {                $this->scenes()->push(PauseScene::class, ['level' => $this->level]);                return;            }            $this->pointer();            foreach (World::SKILLS as $i => $skill) {                if ($a->isPressed('skill' . ($i + 1))) {                    $this->pressButton($skill);                }            }            if ($a->isPressed('turn_left')) {                $this->turn(-1);            }            if ($a->isPressed('turn_right')) {                $this->turn(1);            }            foreach (['fast' => 'fast', 'rate_up' => 'plus', 'rate_down' => 'minus', 'nuke' => 'nuke'] as $action => $button) {                if ($a->isPressed($action)) {                    $this->pressButton($button);                }            }            $this->camX = $this->clampCam($this->camX + $a->axis('left', 'right') * 260 * $dt / $this->zoom);        }        $this->acc = min(0.4, $this->acc + $dt * ($this->fast || ($this->demo && $this->world->spawned >= $this->world->total) ? 3 : 1));        while ($this->acc >= self::TICK && !$this->ended) {            $this->acc -= self::TICK;            $this->world->step();            $this->auto?->step();            if ($this->world->isOver()) {                $this->ended = true;            }        }        $this->view()->consume(!$this->demo || $this->scenes()->current() === $this);        $this->view()->update($dt);        $this->updateYaw($dt);        if ($this->demo) {            // the camera follows the Lumikin in front            $lead = null;            foreach ($this->world->lumis as $l) {                if ($l->alive && !$l->is(Lumi::STOP) && ($lead === null || $l->x > $lead->x)) {                    $lead = $l;                }            }            $target = $this->clampCam(($lead->x ?? $this->world->hatch[0]) - 240);            $this->camX += ($target - $this->camX) * min(1, $dt * 2.5);        }        if ($this->ended) {            $this->endTimer += $dt;            if ($this->endTimer > 1.2 && $this->scenes()->current() === $this) {                $this->finish();            }        }    }    private function finish(): void    {        if ($this->demo) {            $this->enter(['level' => ($this->level + 1) % Levels::count(), 'demo' => true]);            return;        }        $percent = (int)floor($this->world->saved * 100 / $this->world->total);        $won = $this->world->isWon();        if ($won) {            $this->progress->won($this->engine, $this->level, $percent);        }        $this->endTimer = -1e9;        $this->scenes()->push(ResultScene::class, [            'level' => $this->level, 'won' => $won, 'saved' => $this->world->saved, 'total' => $this->world->total, 'need' => $this->world->need,        ]);    }    /** A touch position (0..1 of the window, what SDL gives) as a view point, through the letterbox. @return array{0: float, 1: float} */    private function touchToView(float $fx, float $fy): array    {        $r = $this->sdl()->getRenderer();        [$ow, $oh] = $r->outputSize();        [$lx, $ly, $lw, $lh] = $r->letterbox();        if ($lw <= 0 || $lh <= 0 || $ow <= 0) {            return [$fx * $this->width(), $fy * $this->height()];        }        return [($fx * $ow - $lx) * $this->width() / $lw, ($fy * $oh - $ly) * $this->height() / $lh];    }    private function pointer(): void    {        $m = $this->engine->input->getMouse();        if ($m->x != 0 || $m->y != 0) {            $this->mouseSeen = true;        }        $viewButton = $this->viewButtonAt($m->x, $m->y);        $inLevel = $m->y < self::HUD_Y && $viewButton === null;        if ($inLevel && !$this->dragging && !Platform::isMobile()) {            [$hx, $hy] = $this->toLevel($m->x, $m->y);            $this->hover = $this->world->pick($hx, $hy, $this->skill);        } else {            $this->hover = null;        }        if ($m->isPressed(1)) {            if ($viewButton !== null) {                $this->turn($viewButton === 'turn_left' ? -1 : 1);            } elseif (!$inLevel) {                $button = $this->buttonAt($m->x, $m->y);                if ($button !== null) {                    $this->pressButton($button);                }            } elseif (Platform::isMobile()) {                // a finger: the skill is given when it lifts without having moved (else it was a drag or a pinch)                $this->tapPending = !$this->pinchLock;                $this->dragging = true;                $this->dragFrom = $m->x;                $this->dragCam = $this->camX;                $this->dragFromY = $m->y;                $this->dragCamY = $this->camY;                $this->dragLevel = $this->is3d() ? $this->toLevel($m->x, $m->y) : null;            } elseif (!$this->clickAt($m->x, $m->y)) {                $this->dragging = true;                $this->dragFrom = $m->x;                $this->dragCam = $this->camX;                $this->dragFromY = $m->y;                $this->dragCamY = $this->camY;                $this->dragLevel = $this->is3d() ? $this->toLevel($m->x, $m->y) : null;            }        }        if ($this->pinch?->active()) {            $this->pinchLock = true;            $this->tapPending = false;        }        if ($this->tapPending && abs($m->x - $this->dragFrom) + abs($m->y - $this->dragFromY) > 6) {            $this->tapPending = false;   // it moved: a drag, not a tap        }        if ($this->tapPending && $m->isReleased(1)) {            $this->tapPending = false;            $this->dragging = false;            $this->clickAt($this->dragFrom, $this->dragFromY);        }        if ($this->dragging && ($this->pinchLock || $this->tapPending)) {            if (!$m->isHeld(1)) {                $this->dragging = false;            }        } elseif ($this->dragging) {            if ($m->isHeld(1) && !$this->pinch?->active() && $this->dragLevel !== null && $this->is3d()) {                // turned view: the grabbed level point is cast again under the pointer, the camera moves by the difference                [$px, $py] = $this->toLevel($m->x, $m->y);                $this->camY += $this->dragLevel[1] - $py;                $this->camX = $this->clampCam($this->camX + $this->dragLevel[0] - $px);            } elseif ($m->isHeld(1) && !$this->pinch?->active()) {                $this->camY = $this->dragCamY - ($m->y - $this->dragFromY) / $this->zoom;                $this->camX = $this->clampCam($this->dragCam - ($m->x - $this->dragFrom) / $this->zoom);            } else {                $this->dragging = false;            }        } elseif ($this->mouseSeen && $inLevel && !Platform::isMobile()) {            if ($m->x >= WorldView::VIEW_W - 6) {                $this->camX = $this->clampCam($this->camX + 4);            } elseif ($m->x <= 5) {                $this->camX = $this->clampCam($this->camX - 4);            }        }        $keys = $this->engine->input->getKeyboard();        if ($m->wheelY != 0) {            if ($keys->isKeyHeld(\SDLK_LCTRL) || $keys->isKeyHeld(\SDLK_RCTRL)) {                $this->zoomAt($m->wheelY > 0 ? 1.25 : 0.8, $m->x, min($m->y, self::HUD_Y));   // Ctrl + wheel: zoom on the pointer            } else {                $this->camX = $this->clampCam($this->camX - $m->wheelY * 40 / $this->zoom);            }        }        if ($this->pinch !== null) {            [$factor, $px, $py] = $this->pinch->take();            if ($factor != 1.0 && $this->pinch->active()) {                [$vx, $vy] = $this->touchToView($px, $py);                $this->zoomAt(max(0.8, min(1.25, $factor)), max(0, min(WorldView::VIEW_W, $vx)), max(0, min(self::HUD_Y, $vy)));            }            $this->idleFrames = $m->isHeld(1) ? 0 : $this->idleFrames + 1;            if ($this->idleFrames > 10) {                $this->pinch->reset();            }        }        if (!$m->isHeld(1) && !$this->pinch?->active()) {            $this->pinchLock = false;        }        $a = $this->actions();        if ($a->isHeld('up') || $a->isHeld('down')) {   // keys: zoom on the middle of the view            $this->zoomAt($a->isHeld('up') ? 1.03 : 0.97, WorldView::VIEW_W / 2, self::HUD_Y / 2);        }        if (!$inLevel && !Platform::isMobile() && ($b = $this->buttonAt($m->x, $m->y)) !== null && in_array($b, World::SKILLS, true) && $this->tipTime <= 0.1) {            $this->tip = 'SKILL ' . $b;            $this->tipTime = 0.1;        }    }    public function render(Renderer $r): void    {        $w = $this->world;        $hoverOk = $this->hover !== null && $w->canAssign($this->hover, $this->skill);        $this->layer($this->is3d());        if ($this->is3d()) {            $this->view()->render3d($r, $this->camera3d(), $this->camX, $this->camY, $this->hover, $hoverOk);        } else {            $this->view()->render($r, $this->camX, $this->hover, $hoverOk, $this->zoom, $this->camY);        }        if ($this->demo) {            return;        }        $e = $this->engine;        $f = $this->fonts;        $sdl = $this->sdl();        // top line: who is out, who is home, the clock        $seconds = (int)ceil($w->ticksLeft / World::TICKS_PER_SECOND);        $f->text($e, $r, 'OUT {n}', 6, 3, 1, [255, 255, 255], 'left', ['n' => $w->alive()]);        $homeColor = $w->isWon() ? [120, 255, 150] : [255, 210, 60];        $f->text($e, $r, 'HOME {n}/{need}', 240, 3, 1, $homeColor, 'center', ['n' => $w->saved, 'need' => $w->need]);        $f->text($e, $r, sprintf('%d:%02d', intdiv($seconds, 60), $seconds % 60), 474, 3, 1, $seconds < 30 ? [255, 110, 90] : [255, 255, 255], 'right');        // the arrows that turn the camera (greyed without GL)        $can = $this->can3d();        foreach (self::viewButtons() as $name => [$x, $y, $bw, $bh]) {            $on = $can && (($name === 'turn_left' && $this->yawTarget < 0) || ($name === 'turn_right' && $this->yawTarget > 0));            $this->art->draw($r, 'btn-small' . ($on ? '-on' : ''), null, $x, $y);            $this->art->icon($r, $e, $name, $x + $bw / 2 - 6, $y + $bh / 2 - 6, $can ? '' : '-dim', 1.0);        }        // the bar        $this->art->draw($r, 'hud', null, 0, self::HUD_Y);        foreach (self::buttons() as $name => [$x, $y, $bw, $bh]) {            $isSkill = in_array($name, World::SKILLS, true);            if ($isSkill) {                $n = $w->skills[$name];                $on = $this->skill === $name;                $this->art->draw($r, $on ? 'btn-on' : ($n > 0 ? 'btn' : 'btn-off'), null, $x, $y);                $this->art->icon($r, $e, $name, $x + 7, $y + 1, $n > 0 ? '' : '-dim', 1.5);                $f->print($e, $r, (string)$n, $x + 16, $y + 18, 1, $n > 0 ? [255, 255, 255] : [110, 100, 140], 'center', [20, 12, 40]);                continue;            }            $on = ($name === 'fast' && $this->fast) || ($name === 'nuke' && $this->nukeArmed > 0 && (int)($this->time * 8) % 2 === 0) || ($name === 'nuke' && $w->nuked);            $small = $bh < 20;            $this->art->draw($r, ($small ? 'btn-small' : 'btn') . ($on ? '-on' : ''), null, $x, $y);            $k = $small ? 1.0 : 1.5;            $this->art->icon($r, $e, $name, $x + $bw / 2 - 6 * $k, $y + $bh / 2 - 6 * $k, '', $k);        }        $f->print($e, $r, (string)$w->rate, 315, 252, 1, [255, 210, 60], 'center', [20, 12, 40]);        $r->fillRect(452, 238, 24, 26, [12, 8, 24]);        $r->fillRect(452, 238 + 26 * (1 - $w->saved / max(1, $w->total)), 24, 26 * $w->saved / max(1, $w->total), [255, 190, 60]);        $r->fillRect(452, 238 + 26 * (1 - $w->need / max(1, $w->total)), 24, 1, [255, 255, 255]);        $r->drawRect(451, 237, 26, 28, [92, 74, 148]);        if ($this->tipTime > 0 && $this->banner <= 0) {            $f->text($e, $r, $this->tip, 6, self::HUD_Y - 15, 1, [255, 240, 190]);        }        if ($this->banner > 0) {            $k = min(1, $this->banner * 2, (3.2 - $this->banner) * 4);            $r->fillRect(0, 78, 480, 62, [10, 6, 24, (int)(190 * $k)]);            if ($k > 0.5) {                $f->text($e, $r, 'LEVEL {n}', 240, 82, 1, [255, 210, 60], 'center', ['n' => $this->level + 1]);                $f->text($e, $r, $this->def['name'], 240, 93, 1.8, [255, 255, 255], 'center');                $f->text($e, $r, $this->def['hint'], 240, 121, 1, [200, 230, 255], 'center');            }        }    }}