418 lines
20 KiB
C++
418 lines
20 KiB
C++
#include "game/scenes/game_scene.hpp"
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
#include <filesystem>
|
||
#include <iostream>
|
||
#include <iterator>
|
||
#include <utility>
|
||
#include <variant>
|
||
#include <vector>
|
||
|
||
#include "core/audio/audio.hpp"
|
||
#include "core/input/gamepad_manager.hpp"
|
||
#include "core/input/global_inputs.hpp"
|
||
#include "core/input/key_config.hpp"
|
||
#include "core/locale/locale.hpp"
|
||
#include "core/rendering/notifier.hpp"
|
||
#include "core/rendering/render_info.hpp"
|
||
#include "core/rendering/screen.hpp"
|
||
#include "core/rendering/text.hpp"
|
||
#include "core/resources/resource_cache.hpp"
|
||
#include "core/resources/resource_helper.hpp"
|
||
#include "core/system/event_buffer.hpp"
|
||
#include "core/ui/ui_config.hpp"
|
||
#include "external/fkyaml_node.hpp"
|
||
#include "game/entities/entity.hpp"
|
||
#include "game/entities/event_bus.hpp"
|
||
#include "game/entities/player.hpp"
|
||
#include "game/map/map.hpp"
|
||
#include "game/options.hpp"
|
||
#include "game/ui/hud.hpp"
|
||
#include "utils/defaults.hpp"
|
||
#include "utils/defines.hpp"
|
||
#include "utils/easing.hpp"
|
||
|
||
namespace {
|
||
|
||
// La geometria ha de quedar inicialitzada abans que Tilemap parsejar el
|
||
// mateix YAML i abans que el ctor de Player consulte Map::playfield_*.
|
||
// Construïm el Tilemap via un helper que primer carrega la geometria
|
||
// i després retorna el Tilemap per valor (move-construït).
|
||
auto makeTilemap(const std::string& yaml_path) -> Tilemap {
|
||
Map::loadFrom(yaml_path);
|
||
return Tilemap(yaml_path);
|
||
}
|
||
|
||
// Llegeix un camp sencer opcional del YAML del mapa. Si no hi és,
|
||
// retorna `fallback` (per defecte -1, que al Player indica "no pinta").
|
||
// S'usa per `player1_paint_tile` i `player2_paint_tile`: cada jugador
|
||
// pinta amb l'índex declarat al mapa, i un mapa sense aquests camps
|
||
// simplement no deixa rastre visual.
|
||
auto readOptionalInt(const std::string& yaml_path, const std::string& key, int fallback = -1) -> int {
|
||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||
if (bytes.empty()) { return fallback; }
|
||
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
||
const auto* end = begin + bytes.size();
|
||
auto yaml = fkyaml::node::deserialize(begin, end);
|
||
if (!yaml.contains(key)) { return fallback; }
|
||
return yaml[key].get_value<int>();
|
||
}
|
||
|
||
} // namespace
|
||
|
||
auto GameScene::defaultConfig() -> Config {
|
||
return {
|
||
.map_yaml = Defaults::Pyramid::MAP_YAML,
|
||
.player_sprite = Defaults::Pyramid::PLAYER_SPRITE,
|
||
.player_yaml = Defaults::Pyramid::PLAYER_YAML,
|
||
.music_track = Defaults::Pyramid::MUSIC_TRACK,
|
||
};
|
||
}
|
||
|
||
GameScene::GameScene(Config config)
|
||
: config_(std::move(config)),
|
||
tilemap_(makeTilemap(config_.map_yaml)) {
|
||
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
||
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
||
const auto SPRITE_KEY = std::filesystem::path(config_.player_sprite).filename().string();
|
||
const auto ANIM_KEY = std::filesystem::path(config_.player_yaml).filename().string();
|
||
Resource::Cache::get().loadSurface(SPRITE_KEY, config_.player_sprite);
|
||
Resource::Cache::get().loadAnimation(ANIM_KEY, config_.player_yaml);
|
||
|
||
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
|
||
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
|
||
// Player perquè el spawn consulta `collision_*`.
|
||
Player::loadFrom(config_.player_yaml);
|
||
|
||
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
|
||
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
|
||
// `makeTilemap`), perquè consulta `Map::tile_size` i `getSarcRect`.
|
||
// Les surfaces de les entitats es carreguen al Resource::Cache aquí.
|
||
entity_manager_.loadFromLevel(config_.map_yaml);
|
||
|
||
// Subscripcions bàsiques al bus del nivell: de moment només logs, sense
|
||
// tocar ni la vida del jugador ni l'estat de la escena. Serà la base
|
||
// sobre la qual cablejar HUD i fi de nivell més endavant.
|
||
entity_manager_.getBus().subscribe("level_complete", [](const Event& /*ev*/) {
|
||
std::cout << "[LEVEL COMPLETE]" << '\n';
|
||
});
|
||
entity_manager_.getBus().subscribe("player_hit", [](const Event& ev) {
|
||
const auto* dmg = std::get_if<int>(&ev.payload);
|
||
std::cout << "[PLAYER HIT] dmg=" << (dmg != nullptr ? *dmg : 0) << '\n';
|
||
});
|
||
|
||
// Jugador 0: fletxes del teclat (Defaults::KeysGame). Instanciar-ne un
|
||
// segon només requereix afegir una altra entrada amb uns Controls
|
||
// distints (p.ex. WASD).
|
||
const Player::Controls CONTROLS_P1{
|
||
.up = Defaults::KeysGame::UP,
|
||
.down = Defaults::KeysGame::DOWN,
|
||
.left = Defaults::KeysGame::LEFT,
|
||
.right = Defaults::KeysGame::RIGHT,
|
||
};
|
||
// Spawn centrat al passadís exterior superior: la caixa de col·lisió
|
||
// queda dins del passadís vertical del mig (entre columnes 1 i 2) i
|
||
// centrada en Y al passadís exterior superior. Així no naix dins de
|
||
// cap sarcòfag.
|
||
constexpr auto CORRIDOR_CENTER_X = static_cast<float>(GameCanvas::CENTER_X);
|
||
const auto CORRIDOR_CENTER_Y =
|
||
static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
|
||
const auto SPAWN_X = CORRIDOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
|
||
const auto SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
|
||
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
|
||
// (camps opcionals `player1_paint_tile` / `player2_paint_tile`). Si no
|
||
// hi són, `paint_tile_index_ = -1` i el Player no deixa rastre.
|
||
const int P1_PAINT = readOptionalInt(config_.map_yaml, "player1_paint_tile", -1);
|
||
players_.emplace_back(std::make_unique<Player>(ANIM_KEY, SPAWN_X, SPAWN_Y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||
|
||
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||
// `loadFromLevel` perquè l'EntityManager ja ha de tenir els sarcòfags
|
||
// registrats (tot i que el sarc escolta via fan-out, no directament
|
||
// al bus; l'ordre de subscripcions aquí és irrellevant per la
|
||
// correcció, però sí pel log d'arrencada). Connectat al bus del
|
||
// manager per subscriure's a `on_tile_entered` i publicar `tile_painted`.
|
||
paint_layer_ = std::make_unique<PaintLayer>(tilemap_, &entity_manager_.getBus());
|
||
|
||
// Connectem el Player al bus perquè emeti `on_tile_entered` i el
|
||
// PaintLayer (subscrit ja) processe el pintat; el fan-out cap als
|
||
// sarcòfags passa per `tile_painted`.
|
||
players_.back()->setEventBus(&entity_manager_.getBus());
|
||
|
||
// Marcador (HUD) de la franja superior. Reutilitza la font 8bithud i
|
||
// la paleta "ui" ja precarregades pel Director — no toca disc des
|
||
// d'aquí. Subscrit al bus per reaccionar a `player_hit`,
|
||
// `level_complete`, etc. amb shake/flash i futurs canvis de mode.
|
||
const std::string HUD_FONT_GIF_KEY = UiConfig::get().hudFont().name + ".gif";
|
||
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
|
||
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
|
||
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
|
||
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font);
|
||
|
||
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
||
// (preload al Director), així que playMusic només mou el punter cap a
|
||
// jail_audio i obri l'stream. Audio::get() està garantit no-null:
|
||
// Director::init crida Audio::init abans d'instanciar GameScene.
|
||
Audio::get()->playMusic(config_.music_track, -1);
|
||
|
||
// Font de l'overlay de pausa: reutilitzem la Surface ja cachejada al
|
||
// Director (PLACEHOLDER_NAME + ".gif"). Text només parseja el .fnt a
|
||
// l'arrencada de l'escena, un sol cop.
|
||
const std::string PAUSE_FONT_KEY = UiConfig::get().placeholderFont().name + ".gif";
|
||
auto pause_surface = Resource::Cache::get().getSurface(PAUSE_FONT_KEY);
|
||
pause_font_ = std::make_shared<Text>(pause_surface, UiConfig::get().placeholderFont().fnt);
|
||
pause_font_->setDefaultKerning(UiConfig::get().placeholderFont().kerning);
|
||
}
|
||
|
||
GameScene::~GameScene() = default;
|
||
|
||
void GameScene::handlePauseInput() {
|
||
#ifdef _DEBUG
|
||
// Mutex amb l'editor: si està actiu, ignorem la tecla de pausa.
|
||
if (editor_.active()) { return; }
|
||
#endif
|
||
const SDL_Scancode PAUSE_SC = KeyConfig::get().scancode("pause");
|
||
SDL_Gamepad* pad = GamepadManager::get().getPrimary();
|
||
for (const auto& event : EventBuffer::events) {
|
||
const bool KEY_HIT = (event.type == SDL_EVENT_KEY_DOWN && PAUSE_SC != SDL_SCANCODE_UNKNOWN && event.key.scancode == PAUSE_SC);
|
||
const bool PAD_HIT = (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN && pad != nullptr && event.gbutton.which == SDL_GetGamepadID(pad) && event.gbutton.button == SDL_GAMEPAD_BUTTON_START);
|
||
if (!KEY_HIT && !PAD_HIT) { continue; }
|
||
|
||
// Determinem cap a quin estat anem. Si estem quiets (IDLE o
|
||
// PAUSED) arranquem la transició contrària. Si estem en marxa
|
||
// (ENTERING o EXITING) invertim: la `scale` actual es manté com
|
||
// a `from_scale` nou i la durada es recalcula proporcional a la
|
||
// distància restant — així mai es produeix un salt de velocitat.
|
||
const bool GOING_TO_PAUSE = (pause_.state == PauseState::IDLE || pause_.state == PauseState::EXITING);
|
||
|
||
// Reprenem l'stream ABANS de començar a pujar el volum: quan
|
||
// venim de PAUSED l'àudio està aturat de debò (ho fem al final
|
||
// d'ENTERING, veure updatePauseTransition); reprenem just al
|
||
// moment de començar EXITING per no perdre el primer frame
|
||
// d'stream. A les inversions mid-flight (ENTERING → EXITING
|
||
// sense tocar PAUSED) l'stream no s'ha aturat mai, així que no
|
||
// cal reprendre.
|
||
if (!GOING_TO_PAUSE && pause_.state == PauseState::PAUSED) {
|
||
Audio::get()->resumeMusic();
|
||
}
|
||
|
||
pause_.from_scale = pause_.scale;
|
||
pause_.to_scale = GOING_TO_PAUSE ? 0.0F : 1.0F;
|
||
const int TARGET_MS = GOING_TO_PAUSE ? UiConfig::get().pauseEnterMs() : UiConfig::get().pauseExitMs();
|
||
const float DISTANCE = std::fabs(pause_.to_scale - pause_.from_scale);
|
||
pause_.duration_ms = std::max(1, static_cast<int>(static_cast<float>(TARGET_MS) * DISTANCE));
|
||
pause_.elapsed_ms = 0;
|
||
pause_.curve = GOING_TO_PAUSE ? UiConfig::get().pauseEnterCurve() : UiConfig::get().pauseExitCurve();
|
||
pause_.state = GOING_TO_PAUSE ? PauseState::ENTERING : PauseState::EXITING;
|
||
|
||
std::cout << "[PAUSE] " << (GOING_TO_PAUSE ? "entering" : "exiting") << " from_scale=" << pause_.from_scale << " duration_ms=" << pause_.duration_ms << '\n';
|
||
}
|
||
}
|
||
|
||
void GameScene::updatePauseTransition(int delta_ms) {
|
||
if (pause_.state != PauseState::ENTERING && pause_.state != PauseState::EXITING) { return; }
|
||
|
||
pause_.elapsed_ms += delta_ms;
|
||
bool just_reached_pause = false;
|
||
if (pause_.elapsed_ms >= pause_.duration_ms) {
|
||
// Transició completa: enganxem al valor objectiu exacte i
|
||
// promocionem l'estat al terminal corresponent.
|
||
pause_.scale = pause_.to_scale;
|
||
const PauseState NEXT = (pause_.to_scale == 0.0F) ? PauseState::PAUSED : PauseState::IDLE;
|
||
just_reached_pause = (NEXT == PauseState::PAUSED);
|
||
pause_.state = NEXT;
|
||
} else {
|
||
const float T = static_cast<float>(pause_.elapsed_ms) / static_cast<float>(pause_.duration_ms);
|
||
const auto CURVE = (pause_.curve != nullptr) ? pause_.curve : &Easing::linear;
|
||
pause_.scale = std::lerp(pause_.from_scale, pause_.to_scale, CURVE(T));
|
||
}
|
||
|
||
// Fade del volum de música durant la transició. La base és el que
|
||
// l'usuari ha configurat a Options; multipliquem per la `scale` per
|
||
// obtenir el volum efectiu aquest frame.
|
||
const float BASE_VOLUME = Options::audio().music.volume;
|
||
Audio::get()->setMusicVolume(BASE_VOLUME * pause_.scale);
|
||
|
||
// En arribar a PAUSED aturem l'stream: així quan despausem la música
|
||
// tornarà des d'on s'ha quedat, no des d'on hauria anat a parar
|
||
// durant el temps que el joc ha estat en pausa. El `resumeMusic` es
|
||
// fa a `handlePauseInput` al començar l'EXITING, perquè la música
|
||
// sone ja mentre el volum puja des de 0.
|
||
if (just_reached_pause) {
|
||
Audio::get()->pauseMusic();
|
||
}
|
||
}
|
||
|
||
#ifdef _DEBUG
|
||
namespace {
|
||
auto detectEditorToggle() -> bool {
|
||
const SDL_Scancode SC = KeyConfig::get().scancode("editor_toggle");
|
||
if (SC == SDL_SCANCODE_UNKNOWN) { return false; }
|
||
return std::ranges::any_of(EventBuffer::events, [SC](const SDL_Event& ev) {
|
||
return ev.type == SDL_EVENT_KEY_DOWN && ev.key.scancode == SC;
|
||
});
|
||
}
|
||
} // namespace
|
||
#endif
|
||
|
||
void GameScene::update(int delta_ms) {
|
||
handlePauseInput();
|
||
updatePauseTransition(delta_ms);
|
||
|
||
#ifdef _DEBUG
|
||
// Toggle de l'editor: només si no hi ha cap transició de pausa ni
|
||
// pausa consolidada. L'editor mateix no consumeix la tecla — la
|
||
// llegim ací per decidir la coexistència.
|
||
if (pause_.state == PauseState::IDLE && detectEditorToggle()) {
|
||
editor_.toggle(tilemap_);
|
||
}
|
||
#endif
|
||
|
||
GlobalInputs::update(delta_ms);
|
||
if (GlobalInputs::wantsQuit()) { wants_quit_ = true; }
|
||
|
||
// El món avança amb `delta_ms * pause_.scale`: scale=1 corre a velocitat
|
||
// real, scale=0 el congela. Quan l'editor de debug està actiu també
|
||
// es salta completament (no l'escalem, el saltem). Notifier,
|
||
// RenderInfo i HUD reben el delta real, per això viuen fora d'aquest
|
||
// bloc i no es ralenteixen amb el dial de pausa.
|
||
#ifdef _DEBUG
|
||
const bool EDITOR_FROZEN = editor_.active();
|
||
#else
|
||
constexpr bool EDITOR_FROZEN = false;
|
||
#endif
|
||
const int SCALED_DELTA_MS = static_cast<int>(static_cast<float>(delta_ms) * pause_.scale);
|
||
if (!EDITOR_FROZEN && SCALED_DELTA_MS > 0) {
|
||
const float DT = static_cast<float>(SCALED_DELTA_MS) / 1000.0F;
|
||
for (auto& player : players_) { player->update(DT); }
|
||
// El tick d'entitats DESPRÉS del tick dels jugadors: si el jugador ha
|
||
// emès `on_tile_entered` aquest frame, l'EntityManager ja l'ha encaminat
|
||
// als sarcòfags via la subscripció del bus, i aquest `update` pot
|
||
// aplicar la transició OPENING → OPEN en el mateix frame.
|
||
entity_manager_.update(DT);
|
||
|
||
// Detecció de contacte player↔entitat després que totes les entitats
|
||
// hagin mogut aquest frame. La porta (si està oberta) emet
|
||
// `level_complete`; la momia/trampa emet `player_hit`. Els sarcs, encara
|
||
// que tinguen collides=true, tenen la caixa lògica fora del camí del
|
||
// jugador (gràcies a Map::collides al Player::update), així que aquí
|
||
// no generen falsos contactes.
|
||
std::vector<Player*> player_ptrs;
|
||
player_ptrs.reserve(players_.size());
|
||
std::ranges::transform(players_, std::back_inserter(player_ptrs), [](const auto& p) { return p.get(); });
|
||
entity_manager_.checkPlayerCollisions(player_ptrs);
|
||
}
|
||
|
||
#ifdef _DEBUG
|
||
editor_.update(delta_ms, tilemap_);
|
||
#endif
|
||
|
||
// El HUD continua avançant les seues animacions encara que el món
|
||
// estigui congelat (pausa o editor): així l'animació d'entrada acaba
|
||
// sola i quan es despausa ja està al seu lloc, igual que el Notifier.
|
||
if (hud_) { hud_->update(delta_ms); }
|
||
|
||
RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiConfig::get().renderInfoText(), UiConfig::get().renderInfoShadow()));
|
||
Notifier::get().update(delta_ms);
|
||
}
|
||
|
||
void GameScene::render() {
|
||
const auto BG_ARGB = Resource::Cache::get().getPalette("ui")[UiConfig::get().canvasBg()];
|
||
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BG_ARGB);
|
||
|
||
// Mapa visual: textura prerenderitzada del tilemap. Els sarcòfags
|
||
// queden "foradats" (cel·les transparents) perquè passaran a ser
|
||
// actors independents; Map::collides encara els bloqueja per a les
|
||
// col·lisions del jugador.
|
||
tilemap_.render();
|
||
|
||
#ifdef _DEBUG
|
||
// En mode editor amaguem entitats i jugador: el tilemap queda net
|
||
// per a poder clicar cel·la per cel·la sense interferència visual.
|
||
// El HUD futur (quan s'afegeixi) també farà cas d'aquest flag.
|
||
const bool SKIP_ACTORS = editor_.active();
|
||
#else
|
||
constexpr bool SKIP_ACTORS = false;
|
||
#endif
|
||
// Capa de pintat: va entre el tilemap base i els actors, perquè els
|
||
// sarcòfags i el jugador la tapen. S'amaga al mode editor (decisió
|
||
// explícita: l'editor edita el tilemap, no la capa de pintat).
|
||
if (!SKIP_ACTORS && paint_layer_) { paint_layer_->render(); }
|
||
if (!SKIP_ACTORS) {
|
||
// Z-order interleaving entitats ↔ players per `getFeetY()` ascendent:
|
||
// els actors amb els peus més amunt queden dibuixats primer (al fons),
|
||
// els més avall al davant. Així un sarc situat per darrere d'un player
|
||
// queda tapat per ell, i un sarc a davant el tapa a ell.
|
||
struct Renderable {
|
||
float feet_y;
|
||
Entity* entity; // nullable; si és nullptr, s'usa `player`.
|
||
Player* player; // nullable; si és nullptr, s'usa `entity`.
|
||
};
|
||
std::vector<Renderable> order;
|
||
order.reserve(entity_manager_.entityCount() + players_.size());
|
||
const auto ENTITIES = entity_manager_.getEntities();
|
||
std::ranges::transform(ENTITIES, std::back_inserter(order), [](Entity* e) {
|
||
return Renderable{.feet_y = e->getFeetY(), .entity = e, .player = nullptr};
|
||
});
|
||
std::ranges::transform(players_, std::back_inserter(order), [](const auto& p) {
|
||
return Renderable{.feet_y = p->getFeetY(), .entity = nullptr, .player = p.get()};
|
||
});
|
||
std::ranges::stable_sort(order, [](const Renderable& a, const Renderable& b) {
|
||
return a.feet_y < b.feet_y;
|
||
});
|
||
for (const auto& r : order) {
|
||
if (r.entity != nullptr) {
|
||
EntityManager::renderEntity(*r.entity);
|
||
} else if (r.player != nullptr) {
|
||
r.player->render();
|
||
}
|
||
}
|
||
// HUD: a sobre dels actors perquè la franja superior tape qualsevol
|
||
// sprite que (durant l'animació d'entrada) puga sortir més amunt
|
||
// del playfield. S'amaga en mode editor (l'editor edita el tilemap;
|
||
// el HUD distrauria) però segueix actualitzant-se per fora.
|
||
if (hud_) { hud_->render(); }
|
||
}
|
||
|
||
#ifdef _DEBUG
|
||
editor_.render(tilemap_);
|
||
#endif
|
||
|
||
// Overlay de pausa: text amb stroke blanc (índex 255) centrat amb X
|
||
// depenent de l'estat del dial. Entra lliscant des de l'esquerra
|
||
// (ENTERING), es queda al centre (PAUSED) i surt cap a la dreta
|
||
// (EXITING). La X es deriva directament de `pause_.scale`, que ja
|
||
// porta la corba aplicada — així text i món frenen alhora.
|
||
if (pause_.state != PauseState::IDLE && pause_font_) {
|
||
constexpr Uint8 OUTLINE_WHITE = 255;
|
||
constexpr Uint8 STROKE_DIST = 1;
|
||
constexpr int PAUSE_KERNING = 0;
|
||
const std::string TEXT = Locale::get().text("input.paused");
|
||
const int TEXT_WIDTH = pause_font_->length(TEXT, PAUSE_KERNING);
|
||
const int OFF_LEFT = -TEXT_WIDTH;
|
||
const int OFF_RIGHT = GameCanvas::WIDTH + TEXT_WIDTH;
|
||
int x = GameCanvas::CENTER_X;
|
||
if (pause_.state == PauseState::ENTERING) {
|
||
x = Easing::lerpInt(OFF_LEFT, GameCanvas::CENTER_X, 1.0F - pause_.scale);
|
||
} else if (pause_.state == PauseState::EXITING) {
|
||
x = Easing::lerpInt(GameCanvas::CENTER_X, OFF_RIGHT, pause_.scale);
|
||
}
|
||
const int Y = GameCanvas::CENTER_Y - (pause_font_->getLineHeight() / 2);
|
||
pause_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
|
||
x,
|
||
Y,
|
||
TEXT,
|
||
PAUSE_KERNING,
|
||
UiConfig::get().notificationText(),
|
||
STROKE_DIST,
|
||
OUTLINE_WHITE);
|
||
}
|
||
|
||
Notifier::get().render();
|
||
RenderInfo::get().render();
|
||
Screen::get().present();
|
||
}
|