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
@@ -19,7 +19,7 @@ void DungeonRenderer::create(lv_obj_t* parent) {
lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE);
status = lv_label_create(root);
lv_obj_set_style_text_color(status, lv_color_hex(0xF2E8D0), LV_PART_MAIN);
lv_obj_align(status, LV_ALIGN_TOP_MID, 0, 8);
lv_obj_align(status, LV_ALIGN_TOP_MID, 0, 6);
const lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
const lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
const uint16_t maxCellW = static_cast<uint16_t>((screenW - 22) / DUNGEON_COLS);
@@ -48,8 +48,7 @@ lv_color_t DungeonRenderer::tileColor(Tile tile) {
case Tile::Wall: return lv_color_hex(COLOR_WALL);
case Tile::Stairs: return lv_color_hex(COLOR_STAIRS);
case Tile::Treasure: return lv_color_hex(COLOR_TREASURE);
case Tile::Floor:
default: return lv_color_hex(COLOR_FLOOR);
case Tile::Floor: default: return lv_color_hex(COLOR_FLOOR);
}
}
@@ -65,48 +64,64 @@ lv_color_t DungeonRenderer::entityColor(EntityType type) {
switch (type) {
case EntityType::Player: return lv_color_hex(COLOR_PLAYER);
case EntityType::Bat: return lv_color_hex(COLOR_BAT);
case EntityType::Slime:
default: return lv_color_hex(COLOR_SLIME);
case EntityType::Slime: default: return lv_color_hex(COLOR_SLIME);
}
}
void DungeonRenderer::clearEntityLabels() {
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y) {
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y)
for (uint8_t x = 0; x < DUNGEON_COLS; ++x) {
lv_label_set_text(labels[y][x], "");
lv_obj_set_style_text_color(labels[y][x], lv_color_white(), LV_PART_MAIN);
}
}
}
void DungeonRenderer::render(const DungeonModel& model, MoveResult lastResult) {
const DungeonState& state = model.getState();
char buf[96];
std::snprintf(buf, sizeof(buf), "F%u HP:%d Gold:%u Best:%u/%u", state.floor, state.hp, state.gold, model.getBestFloor(), model.getBestGold());
if (model.isTutorial()) {
std::snprintf(buf, sizeof(buf), "TUTORIAL %u/2 HP:%d Gold:%u", model.tutorialId()+1, state.hp, state.gold);
} else {
std::snprintf(buf, sizeof(buf), "F%u HP:%d Gold:%u Best:%u/%u", model.displayFloor(), state.hp, state.gold, model.getBestFloor(), model.getBestGold());
}
lv_label_set_text(status, buf);
clearEntityLabels();
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y) {
bool visible = y < state.renderRows;
if (!visible) {
for (uint8_t x = 0; x < DUNGEON_COLS; ++x) lv_obj_add_flag(cells[y][x], LV_OBJ_FLAG_HIDDEN);
continue;
}
for (uint8_t x = 0; x < DUNGEON_COLS; ++x) {
lv_obj_clear_flag(cells[y][x], LV_OBJ_FLAG_HIDDEN);
const Tile tile = state.tiles[y][x];
lv_obj_set_style_bg_color(cells[y][x], tileColor(tile), LV_PART_MAIN);
lv_label_set_text(labels[y][x], tileText(tile));
}
}
if (state.renderRows == TUTORIAL_ROWS) {
lv_obj_set_height(board, grid.cellPx * TUTORIAL_ROWS);
lv_obj_align(board, LV_ALIGN_CENTER, 0, -10);
} else {
lv_obj_set_height(board, grid.rows * grid.cellPx);
lv_obj_align(board, LV_ALIGN_CENTER, 0, 8);
}
for (uint8_t i = 0; i < state.entityCount; ++i) {
const Entity& entity = state.entities[i];
if (!entity.alive) continue;
const char* symbol = entity.type == EntityType::Bat ? "b" : "s";
lv_label_set_text(labels[entity.pos.y][entity.pos.x], symbol);
lv_obj_set_style_text_color(labels[entity.pos.y][entity.pos.x], entityColor(entity.type), LV_PART_MAIN);
const Entity& e = state.entities[i];
if (!e.alive) continue;
const char* sym = e.type == EntityType::Bat ? "b" : "s";
lv_label_set_text(labels[e.pos.y][e.pos.x], sym);
lv_obj_set_style_text_color(labels[e.pos.y][e.pos.x], entityColor(e.type), LV_PART_MAIN);
}
lv_label_set_text(labels[state.playerPos.y][state.playerPos.x], "@");
lv_obj_set_style_text_color(labels[state.playerPos.y][state.playerPos.x], lv_color_hex(COLOR_PLAYER), LV_PART_MAIN);
const char* text = "Move with arrows, swipe, or tap a direction";
const char* text = model.isTutorial() ? (model.tutorialId()==0 ? "TUT1: @ to >" : "TUT2: Kill b, $ , >") : "Arrows/Swipe/Tap";
switch (lastResult) {
case MoveResult::Blocked: text = "A wall blocks the way"; break;
case MoveResult::Attacked: text = "You strike the monster"; break;
case MoveResult::Treasure: text = "Treasure found!"; break;
case MoveResult::Stairs: text = "Down the stairs..."; break;
case MoveResult::Blocked: text = "Wall blocks"; break;
case MoveResult::Attacked: text = "You strike!"; break;
case MoveResult::Treasure: text = "Treasure! +5"; break;
case MoveResult::Stairs: text = model.isTutorial() ? "Tutorial done!" : "Down..."; break;
case MoveResult::Dead: text = "Game over - tap to restart"; break;
default: break;
}