#include "DungeonRenderer.h" #include namespace PocketDungeon { static constexpr uint32_t COLOR_BG = 0x10111A; static constexpr uint32_t COLOR_PANEL = 0x1D2030; static constexpr uint32_t COLOR_FLOOR = 0xD8C7A1; static constexpr uint32_t COLOR_WALL = 0x5D574C; static constexpr uint32_t COLOR_STAIRS = 0x6D4CC2; static constexpr uint32_t COLOR_TREASURE = 0xD99B23; static constexpr uint32_t COLOR_PLAYER = 0x365CF5; static constexpr uint32_t COLOR_SLIME = 0x3B9D58; static constexpr uint32_t COLOR_BAT = 0x953B9D; void DungeonRenderer::create(lv_obj_t* parent) { root = parent; lv_obj_set_style_bg_color(root, lv_color_hex(COLOR_BG), LV_PART_MAIN); 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, 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((screenW - 22) / DUNGEON_COLS); const uint16_t maxCellH = static_cast((screenH - 70) / DUNGEON_ROWS); const uint16_t cell = maxCellW < maxCellH ? maxCellW : maxCellH; grid = GameKit::GridSpec { DUNGEON_COLS, DUNGEON_ROWS, static_cast(cell > 10 ? cell : 10), 0, 0 }; board = GameKit::makePanel(root, lv_color_hex(COLOR_PANEL), 10); lv_obj_set_size(board, grid.cols * grid.cellPx, grid.rows * grid.cellPx); lv_obj_align(board, LV_ALIGN_CENTER, 0, 8); for (uint8_t y = 0; y < DUNGEON_ROWS; ++y) { for (uint8_t x = 0; x < DUNGEON_COLS; ++x) { cells[y][x] = GameKit::makeCell(board, grid.cellPx - 2, lv_color_hex(COLOR_FLOOR), 4); GameKit::setCell(cells[y][x], x, y, grid.cellPx); labels[y][x] = lv_label_create(cells[y][x]); lv_obj_set_style_text_color(labels[y][x], lv_color_white(), LV_PART_MAIN); lv_obj_center(labels[y][x]); } } message = lv_label_create(root); lv_obj_set_style_text_color(message, lv_color_hex(0xB8BCD8), LV_PART_MAIN); lv_obj_align(message, LV_ALIGN_BOTTOM_MID, 0, -8); } lv_color_t DungeonRenderer::tileColor(Tile tile) { switch (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); } } const char* DungeonRenderer::tileText(Tile tile) { switch (tile) { case Tile::Stairs: return ">"; case Tile::Treasure: return "$"; default: return ""; } } 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); } } void DungeonRenderer::clearEntityLabels() { 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]; 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& 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 = model.isTutorial() ? (model.tutorialId()==0 ? "TUT1: @ to >" : "TUT2: Kill b, $ , >") : "Arrows/Swipe/Tap"; switch (lastResult) { 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; } lv_label_set_text(message, text); } } // namespace PocketDungeon