feat(pocketdungeon): intro GOAL+CONTROLS left icons right + 2x2 tutorial levels
Main / Build (BookPlayer) (push) Has been cancelled
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled

- Intro redesign QVGA: left scrollable column 2 cards GOAL+CONTROLS only,
  right 44px rail small icons Play 38 accent + Exit 30 muted (fixes cramped)
- Tutorial by play: floor0 TUTORIAL_ROWS=2 player+stairs, floor1 bat+gold+stairs,
  real game from floor2 infinite with displayFloor mapping
- Model: renderRows, isTutorial(), tutorialId(), clearAllTiles(), generateTutorialFloor
- Renderer: respects renderRows HIDDEN, board height 2 rows centered, TUT messages
- Game: reset tutorials=true, skip moveMonsters in tutorial, bests only real floors
- Keep audio-stream migration: SDK 0.8.0-dev IDF5.5, CMake GLOB SfxEngine, SfxEngine audio-stream
- No unexported LVGL: no max_width/scrollbar_mode -> 0 missing symbols

Installed 192.168.68.134 verified screenshot 6.1K airy left+right
This commit is contained in:
Adolfo
2026-07-19 23:05:43 -04:00
parent b2e9046438
commit 2a45c98fb3
9 changed files with 457 additions and 130 deletions
+281 -34
View File
@@ -1,5 +1,8 @@
#include "PocketDungeon.h"
#include <tt_lvgl_toolbar.h>
#include <cstdio>
extern "C" {
#include <tt_app.h>
}
namespace PocketDungeon {
@@ -7,19 +10,26 @@ 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;
(void) tt_lvgl_toolbar_create_for_app(parent, app);
model.setBests(static_cast<uint16_t>(prefs.getInt(PREF_BEST_FLOOR, 1)), static_cast<uint16_t>(prefs.getInt(PREF_BEST_GOLD, 0)));
model.reset(lv_tick_get());
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) {
@@ -27,63 +37,300 @@ void PocketDungeon::onHide(AppHandle app) {
loop.stop();
onExit();
saveBests();
if (sfx != nullptr) {
sfx->stop();
delete sfx;
sfx = nullptr;
}
if (sfx) { sfx->stop(); delete sfx; sfx = nullptr; }
root = nullptr;
}
void PocketDungeon::onEnter(lv_obj_t* sceneRoot) {
renderer.create(sceneRoot);
GameKit::attachInput(sceneRoot, this);
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::onExit() {}
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<PocketDungeon*>(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<PocketDungeon*>(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<PocketDungeon*>(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<PocketDungeon*>(lv_event_get_user_data(e)); if (s && s->phase==AppPhase::Playing) s->showPause(); }
void PocketDungeon::onInput(const GameKit::InputEvent& input) {
int dx = 0;
int dy = 0;
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;
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) {
model.reset(lv_tick_get());
lastResult = MoveResult::None;
render();
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);
if (dx!=0||dy!=0) {
lastResult=model.movePlayer(dx,dy);
playResultSound(lastResult);
if (lastResult == MoveResult::Dead) saveBests();
if (lastResult==MoveResult::Dead) saveBests();
render();
}
}
void PocketDungeon::onTick(const GameKit::Tick& tick) {
(void) tick;
}
void PocketDungeon::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() {
void PocketDungeon::saveBests(){
prefs.putInt(PREF_BEST_FLOOR, model.getBestFloor());
prefs.putInt(PREF_BEST_GOLD, model.getBestGold());
}
void PocketDungeon::playResultSound(MoveResult result) {
if (sfx == nullptr) return;
switch (result) {
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;