#include "PocketDungeon.h" #include extern "C" { #include } namespace PocketDungeon { static constexpr uint32_t TICK_MS = 200; static constexpr const char* PREF_BEST_FLOOR = "bestFloor"; static constexpr const char* PREF_BEST_GOLD = "bestGold"; static constexpr uint32_t COLOR_BG = 0x10111A; static constexpr uint32_t COLOR_PANEL = 0x1D2030; static constexpr uint32_t COLOR_ACCENT = 0x365CF5; static constexpr uint32_t COLOR_OVERLAY = 0x000000; void PocketDungeon::onShow(AppHandle app, lv_obj_t* parent) { lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(parent, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(parent, lv_color_hex(COLOR_BG), LV_PART_MAIN); root = parent; model.setBests(static_cast(prefs.getInt(PREF_BEST_FLOOR, 1)), static_cast(prefs.getInt(PREF_BEST_GOLD, 0))); model.reset(lv_tick_get(), true); if (sfx == nullptr) { sfx = new SfxEngine(); if (sfx->start()) sfx->applyVolumePreset(SfxEngine::VolumePreset::Quiet); } phase = AppPhase::Intro; onEnter(parent); loop.start(TICK_MS, this); (void) app; } void PocketDungeon::onHide(AppHandle app) { (void) app; loop.stop(); onExit(); saveBests(); if (sfx) { sfx->stop(); delete sfx; sfx = nullptr; } root = nullptr; } void PocketDungeon::onEnter(lv_obj_t* sceneRoot) { root = sceneRoot; GameKit::attachInput(root, this); showIntro(); lastResult = MoveResult::None; } void PocketDungeon::onExit() { clearIntro(); clearGame(); } void PocketDungeon::attachInputToCurrent() { if (root) GameKit::attachInput(root, this); if (introContainer) { lv_obj_add_flag(introContainer, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_flag(introContainer, LV_OBJ_FLAG_GESTURE_BUBBLE); GameKit::attachInput(introContainer, this); } if (gameContainer) { lv_obj_add_flag(gameContainer, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_flag(gameContainer, LV_OBJ_FLAG_GESTURE_BUBBLE); GameKit::attachInput(gameContainer, this); lv_obj_add_event_cb(gameContainer, onGameContainerLongPress, LV_EVENT_LONG_PRESSED, this); } if (pauseOverlay) { lv_obj_add_flag(pauseOverlay, LV_OBJ_FLAG_CLICKABLE); GameKit::attachInput(pauseOverlay, this); } } void PocketDungeon::clearIntro() { if (introContainer) { lv_obj_delete(introContainer); introContainer = nullptr; } } void PocketDungeon::clearGame() { if (pauseOverlay) { lv_obj_delete(pauseOverlay); pauseOverlay = nullptr; } if (gameContainer) { lv_obj_delete(gameContainer); gameContainer = nullptr; } } void PocketDungeon::showIntro() { clearGame(); clearIntro(); phase = AppPhase::Intro; introContainer = lv_obj_create(root); lv_obj_set_size(introContainer, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_all(introContainer, 0, LV_PART_MAIN); lv_obj_set_style_border_width(introContainer, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(introContainer, lv_color_hex(COLOR_BG), LV_PART_MAIN); lv_obj_remove_flag(introContainer, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(introContainer, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(introContainer, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(introContainer, 6, LV_PART_MAIN); lv_obj_add_flag(introContainer, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_flag(introContainer, LV_OBJ_FLAG_GESTURE_BUBBLE); renderIntro(); attachInputToCurrent(); } void PocketDungeon::showGame() { clearIntro(); clearGame(); phase = AppPhase::Playing; gameContainer = lv_obj_create(root); lv_obj_set_size(gameContainer, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_all(gameContainer, 0, LV_PART_MAIN); lv_obj_set_style_border_width(gameContainer, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(gameContainer, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_remove_flag(gameContainer, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(gameContainer, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_flag(gameContainer, LV_OBJ_FLAG_GESTURE_BUBBLE); renderer.create(gameContainer); lv_obj_t* pauseBtn = lv_btn_create(gameContainer); lv_obj_set_size(pauseBtn, 32, 26); lv_obj_set_style_bg_color(pauseBtn, lv_color_hex(0x2A2D40), LV_PART_MAIN); lv_obj_set_style_radius(pauseBtn, 6, LV_PART_MAIN); lv_obj_set_style_pad_all(pauseBtn, 0, LV_PART_MAIN); lv_obj_align(pauseBtn, LV_ALIGN_TOP_RIGHT, -6, 4); lv_obj_add_event_cb(pauseBtn, onExitButtonClicked, LV_EVENT_CLICKED, this); lv_obj_t* pauseLbl = lv_label_create(pauseBtn); lv_label_set_text(pauseLbl, LV_SYMBOL_CLOSE); lv_obj_center(pauseLbl); lastResult = MoveResult::None; attachInputToCurrent(); render(); if (sfx) sfx->play(SfxId::Warp); } void PocketDungeon::showPause() { if (pauseOverlay) return; phase = AppPhase::Paused; pauseOverlay = lv_obj_create(root); lv_obj_set_size(pauseOverlay, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_pad_all(pauseOverlay, 12, LV_PART_MAIN); lv_obj_set_style_border_width(pauseOverlay, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(pauseOverlay, lv_color_hex(COLOR_OVERLAY), LV_PART_MAIN); lv_obj_set_style_bg_opa(pauseOverlay, LV_OPA_70, LV_PART_MAIN); lv_obj_remove_flag(pauseOverlay, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(pauseOverlay, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(pauseOverlay, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(pauseOverlay, 10, LV_PART_MAIN); lv_obj_add_flag(pauseOverlay, LV_OBJ_FLAG_CLICKABLE); lv_obj_t* box = lv_obj_create(pauseOverlay); lv_obj_set_width(box, LV_PCT(80)); lv_obj_set_height(box, LV_SIZE_CONTENT); lv_obj_set_style_pad_all(box, 14, LV_PART_MAIN); lv_obj_set_style_bg_color(box, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); lv_obj_set_style_border_width(box, 0, LV_PART_MAIN); lv_obj_set_style_radius(box, 12, LV_PART_MAIN); lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(box, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(box, 10, LV_PART_MAIN); lv_obj_remove_flag(box, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t* t = lv_label_create(box); lv_label_set_text(t, "Paused"); lv_obj_set_style_text_color(t, lv_color_white(), LV_PART_MAIN); lv_obj_t* resumeBtn = lv_btn_create(box); lv_obj_set_width(resumeBtn, LV_PCT(100)); lv_obj_set_height(resumeBtn, 38); lv_obj_set_style_bg_color(resumeBtn, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); lv_obj_set_style_radius(resumeBtn, 10, LV_PART_MAIN); lv_obj_add_event_cb(resumeBtn, onResumeButtonClicked, LV_EVENT_CLICKED, this); lv_obj_t* rl = lv_label_create(resumeBtn); lv_label_set_text(rl, "Resume"); lv_obj_center(rl); lv_obj_t* exitBtn = lv_btn_create(box); lv_obj_set_width(exitBtn, LV_PCT(100)); lv_obj_set_height(exitBtn, 36); lv_obj_set_style_bg_color(exitBtn, lv_color_hex(0x2E303F), LV_PART_MAIN); lv_obj_set_style_radius(exitBtn, 10, LV_PART_MAIN); lv_obj_add_event_cb(exitBtn, onExitButtonClicked, LV_EVENT_CLICKED, this); lv_obj_t* el = lv_label_create(exitBtn); lv_label_set_text(el, "Exit Game"); lv_obj_center(el); if (sfx) sfx->play(SfxId::MenuOpen); attachInputToCurrent(); } void PocketDungeon::hidePause() { if (pauseOverlay) { lv_obj_delete(pauseOverlay); pauseOverlay = nullptr; } phase = AppPhase::Playing; if (sfx) sfx->play(SfxId::MenuClose); attachInputToCurrent(); render(); } void PocketDungeon::exitGame() { saveBests(); if (sfx) sfx->play(SfxId::Cancel); tt_app_stop(); } void PocketDungeon::renderIntro() { if (!introContainer) return; lv_obj_t* titleRow = lv_obj_create(introContainer); lv_obj_set_width(titleRow, LV_PCT(100)); lv_obj_set_height(titleRow, LV_SIZE_CONTENT); lv_obj_set_style_pad_all(titleRow, 6, LV_PART_MAIN); lv_obj_set_style_border_width(titleRow, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(titleRow, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_remove_flag(titleRow, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(titleRow, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(titleRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(titleRow, 2, LV_PART_MAIN); lv_obj_t* title = lv_label_create(titleRow); lv_label_set_text(title, "POCKET DUNGEON"); lv_obj_set_style_text_color(title, lv_color_hex(0xF8F3E6), LV_PART_MAIN); lv_obj_t* subtitle = lv_label_create(titleRow); lv_label_set_text(subtitle, "Tiny paper roguelike"); lv_obj_set_style_text_color(subtitle, lv_color_hex(0x8A9FBF), LV_PART_MAIN); char bestBuf[64]; std::snprintf(bestBuf, sizeof(bestBuf), "Best F%u G%u", model.getBestFloor(), model.getBestGold()); lv_obj_t* best = lv_label_create(titleRow); lv_label_set_text(best, bestBuf); lv_obj_set_style_text_color(best, lv_color_hex(0x7A8196), LV_PART_MAIN); lv_obj_t* mainRow = lv_obj_create(introContainer); lv_obj_set_width(mainRow, LV_PCT(100)); lv_obj_set_flex_grow(mainRow, 1); lv_obj_set_style_pad_all(mainRow, 4, LV_PART_MAIN); lv_obj_set_style_pad_column(mainRow, 8, LV_PART_MAIN); lv_obj_set_style_pad_bottom(mainRow, 8, LV_PART_MAIN); lv_obj_set_style_border_width(mainRow, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(mainRow, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_flex_flow(mainRow, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(mainRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); lv_obj_remove_flag(mainRow, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t* leftCol = lv_obj_create(mainRow); lv_obj_set_flex_grow(leftCol, 1); lv_obj_set_height(leftCol, LV_PCT(100)); lv_obj_set_style_pad_all(leftCol, 0, LV_PART_MAIN); lv_obj_set_style_pad_right(leftCol, 4, LV_PART_MAIN); lv_obj_set_style_pad_bottom(leftCol, 6, LV_PART_MAIN); lv_obj_set_style_border_width(leftCol, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(leftCol, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_flex_flow(leftCol, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(leftCol, 8, LV_PART_MAIN); lv_obj_add_flag(leftCol, LV_OBJ_FLAG_SCROLLABLE); auto makeCard = [](lv_obj_t* parent, const char* tTitle, const char* body, uint32_t col) { lv_obj_t* card = lv_obj_create(parent); lv_obj_set_width(card, LV_PCT(100)); lv_obj_set_height(card, LV_SIZE_CONTENT); lv_obj_set_style_pad_all(card, 9, LV_PART_MAIN); lv_obj_set_style_pad_row(card, 3, LV_PART_MAIN); lv_obj_set_style_bg_color(card, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); lv_obj_set_style_border_width(card, 0, LV_PART_MAIN); lv_obj_set_style_radius(card, 10, LV_PART_MAIN); lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t* t = lv_label_create(card); lv_label_set_text(t, tTitle); lv_obj_set_style_text_color(t, lv_color_hex(col), LV_PART_MAIN); lv_obj_t* b = lv_label_create(card); lv_label_set_text(b, body); lv_obj_set_style_text_color(b, lv_color_hex(0xD9DDE8), LV_PART_MAIN); lv_label_set_long_mode(b, LV_LABEL_LONG_WRAP); lv_obj_set_width(b, LV_PCT(100)); return card; }; makeCard(leftCol, "GOAL", "Reach deepest floor.\nCollect gold, survive.", 0xD99B23); makeCard(leftCol, "CONTROLS", "Arrows / WASD / Swipe\nTap quadrant = move\nQ / ESC = exit", 0x6D8CFF); lv_obj_t* rightRail = lv_obj_create(mainRow); lv_obj_set_width(rightRail, 44); lv_obj_set_height(rightRail, LV_SIZE_CONTENT); lv_obj_set_style_pad_all(rightRail, 0, LV_PART_MAIN); lv_obj_set_style_pad_row(rightRail, 10, LV_PART_MAIN); lv_obj_set_style_border_width(rightRail, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(rightRail, LV_OPA_TRANSP, LV_PART_MAIN); lv_obj_set_flex_flow(rightRail, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(rightRail, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_remove_flag(rightRail, LV_OBJ_FLAG_SCROLLABLE); auto makeIconBtn = [](lv_obj_t* parent, const char* symbol, uint32_t bg, lv_event_cb_t cb, void* user, int s) -> lv_obj_t* { lv_obj_t* btn = lv_btn_create(parent); lv_obj_set_size(btn, s, s); lv_obj_set_style_bg_color(btn, lv_color_hex(bg), LV_PART_MAIN); lv_obj_set_style_radius(btn, s / 3, LV_PART_MAIN); lv_obj_set_style_pad_all(btn, 0, LV_PART_MAIN); lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, user); lv_obj_t* lbl = lv_label_create(btn); lv_label_set_text(lbl, symbol); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_white(), LV_PART_MAIN); return btn; }; makeIconBtn(rightRail, LV_SYMBOL_PLAY, COLOR_ACCENT, onStartButtonClicked, this, 38); makeIconBtn(rightRail, LV_SYMBOL_CLOSE, 0x252836, onExitButtonClicked, this, 30); } void PocketDungeon::onStartButtonClicked(lv_event_t* e) { auto* s = static_cast(lv_event_get_user_data(e)); if (s) s->showGame(); lv_event_stop_bubbling(e); } void PocketDungeon::onResumeButtonClicked(lv_event_t* e) { auto* s = static_cast(lv_event_get_user_data(e)); if (s) s->hidePause(); lv_event_stop_bubbling(e); } void PocketDungeon::onExitButtonClicked(lv_event_t* e) { auto* s = static_cast(lv_event_get_user_data(e)); if (s) s->exitGame(); lv_event_stop_bubbling(e); } void PocketDungeon::onGameContainerLongPress(lv_event_t* e) { auto* s = static_cast(lv_event_get_user_data(e)); if (s && s->phase==AppPhase::Playing) s->showPause(); } void PocketDungeon::onInput(const GameKit::InputEvent& input) { if (input.kind == GameKit::InputKind::Cancel || input.kind == GameKit::InputKind::Menu) { exitGame(); return; } if (phase == AppPhase::Intro) { if (input.kind != GameKit::InputKind::Unknown) showGame(); return; } if (phase == AppPhase::Paused) { if (input.kind != GameKit::InputKind::Unknown) hidePause(); return; } int dx=0, dy=0; switch (input.kind) { case GameKit::InputKind::Up: dy=-1; break; case GameKit::InputKind::Down: dy=1; break; case GameKit::InputKind::Left: dx=-1; break; case GameKit::InputKind::Right: dx=1; break; default: break; } if (model.getState().gameOver) { if (dx!=0||dy!=0||input.kind==GameKit::InputKind::Confirm||input.kind==GameKit::InputKind::Touch) { model.reset(lv_tick_get(), true); lastResult=MoveResult::None; render(); } return; } if (dx!=0||dy!=0) { lastResult=model.movePlayer(dx,dy); playResultSound(lastResult); if (lastResult==MoveResult::Dead) saveBests(); render(); } } void PocketDungeon::onTick(const GameKit::Tick& tick){ (void)tick; } void PocketDungeon::render(){ if (phase==AppPhase::Intro||phase==AppPhase::Paused) return; if (!gameContainer) return; renderer.render(model, lastResult); } void PocketDungeon::saveBests(){ prefs.putInt(PREF_BEST_FLOOR, model.getBestFloor()); prefs.putInt(PREF_BEST_GOLD, model.getBestGold()); } void PocketDungeon::playResultSound(MoveResult r){ if (!sfx) return; switch(r){ case MoveResult::Attacked: sfx->play(SfxId::Hurt); break; case MoveResult::Treasure: sfx->play(SfxId::Coin); break; case MoveResult::Stairs: sfx->play(SfxId::Warp); break; case MoveResult::Dead: sfx->play(SfxId::GameOver); break; case MoveResult::Moved: sfx->play(SfxId::Blip); break; case MoveResult::Blocked: sfx->play(SfxId::Error); break; default: break; } } } // namespace PocketDungeon