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
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:
@@ -1,9 +1,7 @@
|
||||
#include "DungeonModel.h"
|
||||
|
||||
#ifndef LV_ABS
|
||||
#define LV_ABS(x) ((x) > 0 ? (x) : -(x))
|
||||
#endif
|
||||
|
||||
namespace PocketDungeon {
|
||||
|
||||
uint32_t DungeonModel::nextRandom() {
|
||||
@@ -11,48 +9,75 @@ uint32_t DungeonModel::nextRandom() {
|
||||
return (seed >> 16u) & 0x7FFFu;
|
||||
}
|
||||
|
||||
void DungeonModel::reset(uint32_t seedValue) {
|
||||
void DungeonModel::reset(uint32_t seedValue, bool tutorials) {
|
||||
seed = seedValue;
|
||||
state = DungeonState {};
|
||||
state.floor = 1;
|
||||
state.floor = tutorials ? 0 : 2;
|
||||
state.hp = 5;
|
||||
state.gold = 0;
|
||||
state.gameOver = false;
|
||||
generateFloor();
|
||||
}
|
||||
|
||||
void DungeonModel::clearAllTiles() {
|
||||
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y)
|
||||
for (uint8_t x = 0; x < DUNGEON_COLS; ++x)
|
||||
state.tiles[y][x] = Tile::Floor;
|
||||
for (uint8_t x = 0; x < DUNGEON_COLS; ++x) { state.tiles[0][x] = Tile::Wall; state.tiles[DUNGEON_ROWS-1][x] = Tile::Wall; }
|
||||
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y) { state.tiles[y][0] = Tile::Wall; state.tiles[y][DUNGEON_COLS-1] = Tile::Wall; }
|
||||
}
|
||||
|
||||
void DungeonModel::addEnemy(EntityType type, GameKit::GridPos pos, int8_t hp) {
|
||||
if (state.entityCount >= MAX_ENTITIES) return;
|
||||
state.entities[state.entityCount++] = Entity { type, pos, hp, true };
|
||||
}
|
||||
|
||||
void DungeonModel::generateTutorialFloor(uint8_t tId) {
|
||||
clearAllTiles();
|
||||
state.entityCount = 0;
|
||||
state.renderRows = TUTORIAL_ROWS;
|
||||
for (uint8_t y = TUTORIAL_ROWS + 1; y < DUNGEON_ROWS; ++y)
|
||||
for (uint8_t x = 0; x < DUNGEON_COLS; ++x)
|
||||
state.tiles[y][x] = Tile::Wall;
|
||||
state.playerPos = {1, 1};
|
||||
if (tId == 0) {
|
||||
state.tiles[1][DUNGEON_COLS - 2] = Tile::Stairs;
|
||||
} else {
|
||||
state.tiles[1][3] = Tile::Treasure;
|
||||
state.tiles[1][DUNGEON_COLS - 2] = Tile::Stairs;
|
||||
addEnemy(EntityType::Bat, {5, 1}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void DungeonModel::generateFloor() {
|
||||
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y) {
|
||||
if (state.floor < 2) { generateTutorialFloor(static_cast<uint8_t>(state.floor)); return; }
|
||||
state.renderRows = DUNGEON_ROWS;
|
||||
for (uint8_t y = 0; y < DUNGEON_ROWS; ++y)
|
||||
for (uint8_t x = 0; x < DUNGEON_COLS; ++x) {
|
||||
const bool edge = (x == 0 || y == 0 || x == DUNGEON_COLS - 1 || y == DUNGEON_ROWS - 1);
|
||||
state.tiles[y][x] = edge ? Tile::Wall : Tile::Floor;
|
||||
}
|
||||
}
|
||||
state.playerPos = {1, 1};
|
||||
state.entityCount = 0;
|
||||
state.tiles[2][4] = Tile::Wall;
|
||||
state.tiles[3][4] = Tile::Wall;
|
||||
state.tiles[5][2 + (state.floor % 2)] = Tile::Treasure;
|
||||
state.tiles[5][7] = Tile::Stairs;
|
||||
if (state.floor > 1) {
|
||||
if (state.floor > 2) {
|
||||
auto x = static_cast<uint8_t>(2 + (nextRandom() % 5));
|
||||
auto y = static_cast<uint8_t>(1 + (nextRandom() % 5));
|
||||
if (!(x == 1 && y == 1) && !(x == 7 && y == 5)) state.tiles[y][x] = Tile::Wall;
|
||||
}
|
||||
addEnemy(EntityType::Slime, {5, 2}, 1 + static_cast<int8_t>(state.floor / 3));
|
||||
uint16_t realFloor = state.floor - 1;
|
||||
addEnemy(EntityType::Slime, {5, 2}, 1 + static_cast<int8_t>(realFloor / 3));
|
||||
addEnemy(EntityType::Bat, {3, 5}, 1);
|
||||
if (state.floor >= 3) addEnemy(EntityType::Slime, {7, 4}, 2);
|
||||
if (realFloor >= 3) addEnemy(EntityType::Slime, {7, 4}, 2);
|
||||
}
|
||||
|
||||
int DungeonModel::findEnemyAt(GameKit::GridPos pos) const {
|
||||
for (uint8_t i = 0; i < state.entityCount; ++i) {
|
||||
const auto& entity = state.entities[i];
|
||||
if (entity.alive && entity.pos == pos) return i;
|
||||
const auto& e = state.entities[i];
|
||||
if (e.alive && e.pos == pos) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -70,11 +95,8 @@ MoveResult DungeonModel::movePlayer(int dx, int dy) {
|
||||
if (enemyIdx >= 0) {
|
||||
Entity& enemy = state.entities[enemyIdx];
|
||||
enemy.hp -= 1;
|
||||
if (enemy.hp <= 0) {
|
||||
enemy.alive = false;
|
||||
state.gold += enemy.type == EntityType::Bat ? 2 : 1;
|
||||
}
|
||||
moveMonsters();
|
||||
if (enemy.hp <= 0) { enemy.alive = false; state.gold += enemy.type == EntityType::Bat ? 2 : 1; }
|
||||
if (!isTutorial()) moveMonsters();
|
||||
if (state.hp <= 0) { state.gameOver = true; return MoveResult::Dead; }
|
||||
return MoveResult::Attacked;
|
||||
}
|
||||
@@ -87,15 +109,19 @@ MoveResult DungeonModel::movePlayer(int dx, int dy) {
|
||||
result = MoveResult::Treasure;
|
||||
} else if (tile == Tile::Stairs) {
|
||||
state.floor += 1;
|
||||
if (state.floor > bestFloor) bestFloor = state.floor;
|
||||
if (state.gold > bestGold) bestGold = state.gold;
|
||||
if (state.floor >= 2) {
|
||||
if (state.floor - 1 > bestFloor) bestFloor = state.floor - 1;
|
||||
if (state.gold > bestGold) bestGold = state.gold;
|
||||
}
|
||||
generateFloor();
|
||||
return MoveResult::Stairs;
|
||||
}
|
||||
moveMonsters();
|
||||
if (!isTutorial()) moveMonsters();
|
||||
if (state.hp <= 0) { state.gameOver = true; return MoveResult::Dead; }
|
||||
if (state.floor > bestFloor) bestFloor = state.floor;
|
||||
if (state.gold > bestGold) bestGold = state.gold;
|
||||
if (state.floor >= 2) {
|
||||
if (state.floor - 1 > bestFloor) bestFloor = state.floor - 1;
|
||||
if (state.gold > bestGold) bestGold = state.gold;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -105,26 +131,19 @@ void DungeonModel::moveMonsters() {
|
||||
if (!enemy.alive) continue;
|
||||
const int16_t dx = state.playerPos.x - enemy.pos.x;
|
||||
const int16_t dy = state.playerPos.y - enemy.pos.y;
|
||||
if ((dx == 0 && (dy == 1 || dy == -1)) || (dy == 0 && (dx == 1 || dx == -1))) {
|
||||
state.hp -= 1;
|
||||
continue;
|
||||
}
|
||||
if ((dx == 0 && (dy == 1 || dy == -1)) || (dy == 0 && (dx == 1 || dx == -1))) { state.hp -= 1; continue; }
|
||||
GameKit::GridPos target = enemy.pos;
|
||||
if (enemy.type == EntityType::Bat && (nextRandom() % 3 == 0)) {
|
||||
const int dirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||
const int* dir = dirs[nextRandom() % 4];
|
||||
target.x += dir[0];
|
||||
target.y += dir[1];
|
||||
target.x += dir[0]; target.y += dir[1];
|
||||
} else if (LV_ABS(dx) > LV_ABS(dy)) {
|
||||
target.x += dx > 0 ? 1 : -1;
|
||||
} else if (dy != 0) {
|
||||
target.y += dy > 0 ? 1 : -1;
|
||||
}
|
||||
if (target == state.playerPos) {
|
||||
state.hp -= 1;
|
||||
} else if (isWalkable(target) && findEnemyAt(target) < 0) {
|
||||
enemy.pos = target;
|
||||
}
|
||||
if (target == state.playerPos) state.hp -= 1;
|
||||
else if (isWalkable(target) && findEnemyAt(target) < 0) enemy.pos = target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user