mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-16 14:25:51 +02:00
updated firmware build date; added [J] Games to home screen; added game sub screen; added basic Snake game
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
#define FIRMWARE_VER_CODE 11
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "12 May 2026"
|
||||
#define FIRMWARE_BUILD_DATE "15 May 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
#include "Tracescreen.h"
|
||||
#include "GamesMenuScreen.h"
|
||||
#include "SnakeScreen.h"
|
||||
#ifdef MECK_WEB_READER
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
@@ -700,6 +702,8 @@
|
||||
#include "LastHeardScreen.h"
|
||||
#include "PathEditorScreen.h"
|
||||
#include "Tracescreen.h"
|
||||
#include "GamesMenuScreen.h"
|
||||
#include "SnakeScreen.h"
|
||||
|
||||
static TouchDrvGT911 gt911Touch;
|
||||
static bool gt911Ready = false;
|
||||
@@ -1210,6 +1214,21 @@ static void lastHeardToggleContact() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Games menu: tap to select game entry
|
||||
if (ui_task.isOnGamesMenu()) {
|
||||
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
|
||||
if (gm) {
|
||||
int result = gm->selectRowAtVY(vy);
|
||||
if (result == 2) return '\r'; // Tapped current selection -- launch
|
||||
}
|
||||
return 0; // Cursor moved or miss -- just refresh
|
||||
}
|
||||
|
||||
// Snake screen: tap = Enter (start / restart)
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
return '\r';
|
||||
}
|
||||
|
||||
// Home screen FIRST page: tile taps (virtual coordinate hit test)
|
||||
if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) {
|
||||
const int tileW = 40, tileH = 22, gapX = 1, gapY = 1;
|
||||
@@ -1235,8 +1254,9 @@ static void lastHeardToggleContact() {
|
||||
#else
|
||||
if (row == 1 && col == 2) { ui_task.gotoDiscoveryScreen(); return 0; }
|
||||
#endif
|
||||
// Third row: only centre tile (col 1) is real; col 0 and col 2 fall through to page-flip
|
||||
if (row == 2 && col == 1) { ui_task.gotoTraceScreen(); return 0; }
|
||||
// Third row: Trace (col 0) + Games (col 1)
|
||||
if (row == 2 && col == 0) { ui_task.gotoTraceScreen(); return 0; }
|
||||
if (row == 2 && col == 1) { ui_task.gotoGamesMenu(); return 0; }
|
||||
}
|
||||
// Tap outside tiles — left half backward, right half forward
|
||||
return (vx < 64) ? (char)KEY_PREV : (char)KEY_NEXT;
|
||||
@@ -1473,6 +1493,23 @@ static void lastHeardToggleContact() {
|
||||
if (ui_task.isOnSMSScreen()) return 0;
|
||||
#endif
|
||||
|
||||
// Snake screen: swipes control direction
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
if (horizontal) {
|
||||
return (dx < 0) ? 'a' : 'd';
|
||||
} else {
|
||||
return (dy < 0) ? 'w' : 's';
|
||||
}
|
||||
}
|
||||
|
||||
// Games menu: vertical swipe scrolls list
|
||||
if (ui_task.isOnGamesMenu()) {
|
||||
if (!horizontal) {
|
||||
return (dy > 0) ? 's' : 'w';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reader (reading mode): swipe left/right for page turn
|
||||
if (ui_task.isOnTextReader()) {
|
||||
TextReaderScreen* reader = (TextReaderScreen*)ui_task.getTextReaderScreen();
|
||||
@@ -1545,6 +1582,16 @@ static void lastHeardToggleContact() {
|
||||
if (ui_task.isOnSMSScreen()) return 0;
|
||||
#endif
|
||||
|
||||
// Snake screen: long press exits to games menu
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
return 'q';
|
||||
}
|
||||
|
||||
// Games menu: long press exits to home
|
||||
if (ui_task.isOnGamesMenu()) {
|
||||
return 'q';
|
||||
}
|
||||
|
||||
// Home screen: long press = activate current page action
|
||||
// (BLE toggle, send advert, hibernate, GPS toggle, etc.)
|
||||
if (ui_task.isOnHomeScreen()) {
|
||||
@@ -3378,6 +3425,25 @@ void loop() {
|
||||
ui_task.gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
// Snake screen: check if Exit was triggered
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen();
|
||||
if (ss && ss->wantsExit()) {
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
}
|
||||
// Games menu: check if game launch was triggered
|
||||
if (ui_task.isOnGamesMenu()) {
|
||||
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
|
||||
if (gm && gm->wantsLaunch()) {
|
||||
GameID sel = gm->selectedGame();
|
||||
gm->clearFlags();
|
||||
switch (sel) {
|
||||
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Channel picker: check if long-press Enter was handled (wantsExit)
|
||||
if (ui_task.isOnChannelPickerScreen()) {
|
||||
ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen();
|
||||
@@ -3411,6 +3477,25 @@ void loop() {
|
||||
ui_task.gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
// Snake screen: check if Exit was triggered
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen();
|
||||
if (ss && ss->wantsExit()) {
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
}
|
||||
// Games menu: check if game launch was triggered
|
||||
if (ui_task.isOnGamesMenu()) {
|
||||
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
|
||||
if (gm && gm->wantsLaunch()) {
|
||||
GameID sel = gm->selectedGame();
|
||||
gm->clearFlags();
|
||||
switch (sel) {
|
||||
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Channel picker: check if Enter/Q was handled (wantsExit)
|
||||
if (ui_task.isOnChannelPickerScreen()) {
|
||||
ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen();
|
||||
@@ -3554,6 +3639,7 @@ void loop() {
|
||||
case 'f': ui_task.gotoDiscoveryScreen(); break;
|
||||
case 'h': ui_task.gotoLastHeardScreen(); break;
|
||||
case 'r': ui_task.gotoTraceScreen(); break;
|
||||
case 'j': ui_task.gotoGamesMenu(); break;
|
||||
case (char)0xF3: ui_task.injectKey(KEY_LEFT); break; // Left arrow → prev page
|
||||
case (char)0xF4: ui_task.injectKey(KEY_RIGHT); break; // Right arrow → next page
|
||||
#ifdef MECK_WEB_READER
|
||||
@@ -3604,7 +3690,15 @@ void loop() {
|
||||
if (!handled) {
|
||||
// ESC or Q → back navigation
|
||||
if (ckb == 0x1B || ckb == 'q') {
|
||||
if (ui_task.isOnChannelPickerScreen()) {
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
ui_task.injectKey('q');
|
||||
SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen();
|
||||
if (ss && ss->wantsExit()) {
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
} else if (ui_task.isOnGamesMenu()) {
|
||||
ui_task.gotoHomeScreen();
|
||||
} else if (ui_task.isOnChannelPickerScreen()) {
|
||||
ui_task.gotoHomeScreen();
|
||||
} else if (ui_task.isOnChannelScreen()) {
|
||||
ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen();
|
||||
@@ -3759,6 +3853,25 @@ void loop() {
|
||||
if (ts && ts->wantsExit()) {
|
||||
ui_task.gotoHomeScreen();
|
||||
}
|
||||
} else if (ui_task.isOnGamesMenu()) {
|
||||
// Games menu: Enter launches selected game
|
||||
ui_task.injectKey('\r');
|
||||
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
|
||||
if (gm && gm->wantsLaunch()) {
|
||||
GameID sel = gm->selectedGame();
|
||||
gm->clearFlags();
|
||||
switch (sel) {
|
||||
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
} else if (ui_task.isOnSnakeScreen()) {
|
||||
// Snake: Enter starts/restarts game
|
||||
ui_task.injectKey('\r');
|
||||
SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen();
|
||||
if (ss && ss->wantsExit()) {
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
} else if (ui_task.isOnChannelPickerScreen()) {
|
||||
// Channel picker: Enter selects channel
|
||||
ui_task.injectKey('\r');
|
||||
@@ -4865,6 +4978,7 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|
||||
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|
||||
|| ui_task.isOnTraceScreen()
|
||||
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
@@ -4883,6 +4997,7 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|
||||
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|
||||
|| ui_task.isOnTraceScreen()
|
||||
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
@@ -4905,6 +5020,7 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|
||||
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|
||||
|| ui_task.isOnTraceScreen()
|
||||
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
@@ -4923,6 +5039,7 @@ void handleKeyboardInput() {
|
||||
|| ui_task.isOnDiscoveryScreen() || ui_task.isOnLastHeardScreen()
|
||||
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|
||||
|| ui_task.isOnTraceScreen()
|
||||
|| ui_task.isOnGamesMenu() || ui_task.isOnSnakeScreen()
|
||||
#ifdef MECK_WEB_READER
|
||||
|| ui_task.isOnWebReader()
|
||||
#endif
|
||||
@@ -4945,6 +5062,7 @@ void handleKeyboardInput() {
|
||||
} else if (ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|
||||
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|
||||
|| ui_task.isOnTraceScreen()
|
||||
|| ui_task.isOnSnakeScreen()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
@@ -4963,6 +5081,7 @@ void handleKeyboardInput() {
|
||||
} else if (ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
|
||||
|| ui_task.isOnPathEditor() || ui_task.isOnChannelPickerScreen()
|
||||
|| ui_task.isOnTraceScreen()
|
||||
|| ui_task.isOnSnakeScreen()
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
|| ui_task.isOnAlarmScreen()
|
||||
#endif
|
||||
@@ -4991,6 +5110,25 @@ void handleKeyboardInput() {
|
||||
Serial.println("TraceScreen: Exit -- returning to home");
|
||||
ui_task.gotoHomeScreen();
|
||||
}
|
||||
} else if (ui_task.isOnGamesMenu()) {
|
||||
ui_task.injectKey('\r');
|
||||
GamesMenuScreen* gm = (GamesMenuScreen*)ui_task.getGamesMenuScreen();
|
||||
if (gm && gm->wantsLaunch()) {
|
||||
GameID sel = gm->selectedGame();
|
||||
gm->clearFlags();
|
||||
switch (sel) {
|
||||
case GAME_SNAKE: ui_task.gotoSnakeScreen(); break;
|
||||
// case GAME_MINESWEEPER: ui_task.gotoMinesweeperScreen(); break;
|
||||
// case GAME_2048: ui_task.goto2048Screen(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
} else if (ui_task.isOnSnakeScreen()) {
|
||||
ui_task.injectKey('\r');
|
||||
SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen();
|
||||
if (ss && ss->wantsExit()) {
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
} else if (ui_task.isOnChannelPickerScreen()) {
|
||||
ui_task.injectKey('\r'); // Picker handles Enter: selects channel + sets wantsExit
|
||||
ChannelPickerScreen* pick = (ChannelPickerScreen*)ui_task.getChannelPickerScreen();
|
||||
@@ -5188,6 +5326,14 @@ void handleKeyboardInput() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'j':
|
||||
// Open games menu from home screen
|
||||
if (ui_task.isOnHomeScreen()) {
|
||||
Serial.println("Opening games menu");
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
// L = Login/Admin — from DM conversation, open repeater admin with auto-login
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
@@ -5267,6 +5413,22 @@ void handleKeyboardInput() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Snake screen: Q goes back to games menu
|
||||
if (ui_task.isOnSnakeScreen()) {
|
||||
ui_task.injectKey('q');
|
||||
SnakeScreen* ss = (SnakeScreen*)ui_task.getSnakeScreen();
|
||||
if (ss && ss->wantsExit()) {
|
||||
Serial.println("Nav: Snake -> Games Menu");
|
||||
ui_task.gotoGamesMenu();
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Games menu: Q goes back to home
|
||||
if (ui_task.isOnGamesMenu()) {
|
||||
Serial.println("Nav: Games Menu -> Home");
|
||||
ui_task.gotoHomeScreen();
|
||||
break;
|
||||
}
|
||||
// Alarm screen: Q/backspace routing depends on sub-mode
|
||||
#ifdef MECK_AUDIO_VARIANT
|
||||
if (ui_task.isOnAlarmScreen()) {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// GamesMenuScreen -- Game launcher menu for Meck
|
||||
//
|
||||
// Lists available games. W/S to navigate, Enter to launch, Q to exit.
|
||||
// Uses wantsExit() and wantsLaunch() flags for navigation -- same pattern
|
||||
// as ChannelPickerScreen.
|
||||
// =============================================================================
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
|
||||
// Game identifiers -- add new entries here as games are added
|
||||
enum GameID {
|
||||
GAME_NONE = 0,
|
||||
GAME_SNAKE,
|
||||
// GAME_MINESWEEPER,
|
||||
// GAME_2048,
|
||||
GAME_COUNT // Must be last -- used for array sizing
|
||||
};
|
||||
|
||||
class GamesMenuScreen : public UIScreen {
|
||||
private:
|
||||
UITask* _task;
|
||||
bool _wantsExit;
|
||||
bool _wantsLaunch;
|
||||
int _cursor;
|
||||
GameID _selectedGame;
|
||||
|
||||
// Game registry -- add new games here
|
||||
struct GameEntry {
|
||||
GameID id;
|
||||
const char* name;
|
||||
const char* description;
|
||||
};
|
||||
|
||||
static constexpr int NUM_GAMES = 1; // Increment as games are added
|
||||
|
||||
static const GameEntry* getGames() {
|
||||
static const GameEntry games[NUM_GAMES] = {
|
||||
{ GAME_SNAKE, "Snake", "Classic Nokia-style" },
|
||||
// { GAME_MINESWEEPER, "Minesweeper", "Tap to reveal" },
|
||||
// { GAME_2048, "2048", "Slide and merge" },
|
||||
};
|
||||
return games;
|
||||
}
|
||||
|
||||
public:
|
||||
GamesMenuScreen(UITask* task)
|
||||
: _task(task), _wantsExit(false), _wantsLaunch(false),
|
||||
_cursor(0), _selectedGame(GAME_NONE) {}
|
||||
|
||||
bool wantsExit() const { return _wantsExit; }
|
||||
bool wantsLaunch() const { return _wantsLaunch; }
|
||||
GameID selectedGame() const { return _selectedGame; }
|
||||
|
||||
void enter() {
|
||||
_wantsExit = false;
|
||||
_wantsLaunch = false;
|
||||
_selectedGame = GAME_NONE;
|
||||
// Preserve cursor position so returning from a game stays on the same entry
|
||||
}
|
||||
|
||||
void clearFlags() {
|
||||
_wantsExit = false;
|
||||
_wantsLaunch = false;
|
||||
_selectedGame = GAME_NONE;
|
||||
}
|
||||
|
||||
// ------- Input -------
|
||||
bool handleInput(char c) override {
|
||||
switch (c) {
|
||||
case 'w': case 'W':
|
||||
if (_cursor > 0) _cursor--;
|
||||
return true;
|
||||
case 's': case 'S':
|
||||
if (_cursor < NUM_GAMES - 1) _cursor++;
|
||||
return true;
|
||||
case '\r':
|
||||
_selectedGame = getGames()[_cursor].id;
|
||||
_wantsLaunch = true;
|
||||
return true;
|
||||
case 'q': case 'Q':
|
||||
_wantsExit = true;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ------- Render -------
|
||||
int render(DisplayDriver& display) override {
|
||||
display.startFrame();
|
||||
display.setTextSize(1);
|
||||
|
||||
// --- Header ---
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.drawTextCentered(display.width() / 2, 2, "Games");
|
||||
#else
|
||||
display.setCursor(2, 2);
|
||||
display.print("Games");
|
||||
#endif
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 12, display.width(), 1);
|
||||
|
||||
// --- Game list ---
|
||||
int y = 18;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
int lineH = 16;
|
||||
#else
|
||||
int lineH = 20;
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < NUM_GAMES; i++) {
|
||||
bool selected = (i == _cursor);
|
||||
|
||||
if (selected) {
|
||||
// Highlight bar
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y - 1, display.width(), lineH);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// T5S3: name centred, description below
|
||||
display.drawTextCentered(display.width() / 2, y + 1, getGames()[i].name);
|
||||
if (!selected) display.setColor(DisplayDriver::GREEN);
|
||||
// Description on next line at smaller size
|
||||
display.setTextSize(0);
|
||||
display.drawTextCentered(display.width() / 2, y + 9, getGames()[i].description);
|
||||
display.setTextSize(1);
|
||||
lineH = 20; // Accommodate two-line entries
|
||||
#else
|
||||
// T-Deck Pro: name + description on one line
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s", getGames()[i].name);
|
||||
display.setCursor(6, y + 2);
|
||||
display.print(buf);
|
||||
|
||||
// Description in green (or dark if highlighted)
|
||||
if (!selected) display.setColor(DisplayDriver::GREEN);
|
||||
int nameW = display.getTextWidth(buf);
|
||||
display.setCursor(6 + nameW + 8, y + 2);
|
||||
display.print(getGames()[i].description);
|
||||
#endif
|
||||
|
||||
y += lineH;
|
||||
}
|
||||
|
||||
// --- Footer ---
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(0);
|
||||
display.drawTextCentered(display.width() / 2, display.height() - 8, "Tap to select");
|
||||
display.setTextSize(1);
|
||||
#else
|
||||
display.setTextSize(1);
|
||||
int fy = display.height() - 12;
|
||||
display.drawRect(0, fy - 2, display.width(), 1);
|
||||
display.setCursor(2, fy);
|
||||
display.print("Enter:Play Q:Back");
|
||||
#endif
|
||||
|
||||
return 5000; // Static menu -- slow refresh
|
||||
}
|
||||
|
||||
// --- T5S3 touch: tap to select game entry ---
|
||||
int selectRowAtVY(int vy) {
|
||||
int y = 18;
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
int lineH = 20;
|
||||
#else
|
||||
int lineH = 20;
|
||||
#endif
|
||||
if (vy < y) return 0; // Above list
|
||||
int row = (vy - y) / lineH;
|
||||
if (row >= NUM_GAMES) return 0; // Below list
|
||||
|
||||
if (row == _cursor) {
|
||||
// Tapped current selection -- launch
|
||||
_selectedGame = getGames()[_cursor].id;
|
||||
_wantsLaunch = true;
|
||||
return 2;
|
||||
}
|
||||
_cursor = row;
|
||||
return 1; // Moved cursor
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,489 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// SnakeScreen -- Classic Nokia-style Snake for Meck e-ink devices
|
||||
//
|
||||
// T-Deck Pro: 8x8 pixel cells on 240x320 display
|
||||
// T5S3: 4x4 pixel cells on 128x128 virtual display
|
||||
//
|
||||
// The 800ms partial refresh floor naturally produces Nokia-era tick speed.
|
||||
// Snake body stored as circular buffer -- ~1KB, no PSRAM needed.
|
||||
// Game state persists when switching screens (auto-pause on exit).
|
||||
//
|
||||
// High scores: top 10 scores with dates stored to /games/snake_hi.dat on SD.
|
||||
// =============================================================================
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <SD.h>
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
|
||||
// -- Grid sizing per platform --
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#define SNAKE_CELL 4
|
||||
#define SNAKE_HDR 14
|
||||
#define SNAKE_FTR 10
|
||||
#else
|
||||
#define SNAKE_CELL 8
|
||||
#define SNAKE_HDR 14
|
||||
#define SNAKE_FTR 14
|
||||
#endif
|
||||
|
||||
#define SNAKE_MAX_LEN 512
|
||||
#define SNAKE_HI_COUNT 10
|
||||
#define SNAKE_HI_PATH "/games/snake_hi.dat"
|
||||
#define SNAKE_HI_VERSION 1
|
||||
|
||||
class SnakeScreen : public UIScreen {
|
||||
public:
|
||||
enum GameState { READY, PLAYING, GAME_OVER };
|
||||
enum Direction { UP, DOWN, LEFT, RIGHT };
|
||||
|
||||
struct HiScoreEntry {
|
||||
uint16_t score;
|
||||
uint32_t timestamp; // Unix epoch from RTC
|
||||
};
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
bool _wantsExit;
|
||||
|
||||
// Grid dimensions (computed once from display size on first render)
|
||||
int _gridW, _gridH;
|
||||
int _offsetX, _offsetY;
|
||||
|
||||
// Snake body circular buffer
|
||||
struct Cell { uint8_t x, y; };
|
||||
Cell _body[SNAKE_MAX_LEN];
|
||||
int _headIdx;
|
||||
int _length;
|
||||
|
||||
// Game state
|
||||
GameState _state;
|
||||
Direction _dir;
|
||||
Direction _pendingDir;
|
||||
Cell _food;
|
||||
int _score;
|
||||
unsigned long _lastTick;
|
||||
unsigned long _tickInterval;
|
||||
|
||||
// High scores
|
||||
HiScoreEntry _hiScores[SNAKE_HI_COUNT];
|
||||
int _hiCount;
|
||||
bool _newHiScore;
|
||||
int _newHiRank; // 0-based rank of newly inserted score (-1 if none)
|
||||
|
||||
// Simple xorshift PRNG
|
||||
uint16_t _rngState;
|
||||
|
||||
uint16_t rng() {
|
||||
_rngState ^= _rngState << 7;
|
||||
_rngState ^= _rngState >> 9;
|
||||
_rngState ^= _rngState << 8;
|
||||
return _rngState;
|
||||
}
|
||||
|
||||
void spawnFood() {
|
||||
for (int attempt = 0; attempt < 50; attempt++) {
|
||||
int fx = rng() % _gridW;
|
||||
int fy = rng() % _gridH;
|
||||
if (!isSnakeAt(fx, fy)) {
|
||||
_food.x = fx;
|
||||
_food.y = fy;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int gy = 0; gy < _gridH; gy++) {
|
||||
for (int gx = 0; gx < _gridW; gx++) {
|
||||
if (!isSnakeAt(gx, gy)) {
|
||||
_food.x = gx;
|
||||
_food.y = gy;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isSnakeAt(int x, int y) const {
|
||||
for (int i = 0; i < _length; i++) {
|
||||
int idx = (_headIdx - i + SNAKE_MAX_LEN) % SNAKE_MAX_LEN;
|
||||
if (_body[idx].x == x && _body[idx].y == y) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Cell getHead() const { return _body[_headIdx]; }
|
||||
|
||||
void resetGame() {
|
||||
int cx = _gridW / 2;
|
||||
int cy = _gridH / 2;
|
||||
_length = 3;
|
||||
_headIdx = 2;
|
||||
_body[0] = { (uint8_t)(cx - 2), (uint8_t)cy };
|
||||
_body[1] = { (uint8_t)(cx - 1), (uint8_t)cy };
|
||||
_body[2] = { (uint8_t)cx, (uint8_t)cy };
|
||||
_dir = RIGHT;
|
||||
_pendingDir = RIGHT;
|
||||
_score = 0;
|
||||
_tickInterval = 500;
|
||||
_newHiScore = false;
|
||||
_newHiRank = -1;
|
||||
_rngState = (uint16_t)(millis() ^ 0xA5A5);
|
||||
spawnFood();
|
||||
}
|
||||
|
||||
bool tick() {
|
||||
_dir = _pendingDir;
|
||||
Cell head = getHead();
|
||||
int nx = head.x;
|
||||
int ny = head.y;
|
||||
switch (_dir) {
|
||||
case UP: ny--; break;
|
||||
case DOWN: ny++; break;
|
||||
case LEFT: nx--; break;
|
||||
case RIGHT: nx++; break;
|
||||
}
|
||||
|
||||
if (nx < 0 || nx >= _gridW || ny < 0 || ny >= _gridH) {
|
||||
onDeath();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool eating = (nx == _food.x && ny == _food.y);
|
||||
int checkLen = eating ? _length : (_length - 1);
|
||||
for (int i = 0; i < checkLen; i++) {
|
||||
int idx = (_headIdx - i + SNAKE_MAX_LEN) % SNAKE_MAX_LEN;
|
||||
if (_body[idx].x == nx && _body[idx].y == ny) {
|
||||
onDeath();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_headIdx = (_headIdx + 1) % SNAKE_MAX_LEN;
|
||||
_body[_headIdx] = { (uint8_t)nx, (uint8_t)ny };
|
||||
|
||||
if (eating) {
|
||||
_length++;
|
||||
if (_length >= SNAKE_MAX_LEN) _length = SNAKE_MAX_LEN;
|
||||
_score += 10;
|
||||
spawnFood();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void onDeath() {
|
||||
_state = GAME_OVER;
|
||||
if (_score > 0) {
|
||||
_newHiRank = insertHiScore(_score);
|
||||
_newHiScore = (_newHiRank >= 0);
|
||||
if (_newHiScore) saveHiScores();
|
||||
}
|
||||
}
|
||||
|
||||
void drawCellFilled(DisplayDriver& display, int gx, int gy) const {
|
||||
int px = _offsetX + gx * SNAKE_CELL;
|
||||
int py = _offsetY + gy * SNAKE_CELL;
|
||||
display.fillRect(px, py, SNAKE_CELL, SNAKE_CELL);
|
||||
}
|
||||
|
||||
void drawCellOutline(DisplayDriver& display, int gx, int gy) const {
|
||||
int px = _offsetX + gx * SNAKE_CELL;
|
||||
int py = _offsetY + gy * SNAKE_CELL;
|
||||
display.drawRect(px, py, SNAKE_CELL, SNAKE_CELL);
|
||||
}
|
||||
|
||||
// --- High score persistence ---
|
||||
|
||||
void loadHiScores() {
|
||||
_hiCount = 0;
|
||||
memset(_hiScores, 0, sizeof(_hiScores));
|
||||
if (!SD.exists(SNAKE_HI_PATH)) return;
|
||||
|
||||
File f = SD.open(SNAKE_HI_PATH, FILE_READ);
|
||||
if (!f) return;
|
||||
|
||||
uint8_t ver = 0;
|
||||
if (f.read(&ver, 1) != 1 || ver != SNAKE_HI_VERSION) { f.close(); return; }
|
||||
|
||||
uint8_t count = 0;
|
||||
if (f.read(&count, 1) != 1) { f.close(); return; }
|
||||
if (count > SNAKE_HI_COUNT) count = SNAKE_HI_COUNT;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint8_t buf[6];
|
||||
if (f.read(buf, 6) != 6) break;
|
||||
_hiScores[i].score = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
||||
_hiScores[i].timestamp = (uint32_t)buf[2] | ((uint32_t)buf[3] << 8)
|
||||
| ((uint32_t)buf[4] << 16) | ((uint32_t)buf[5] << 24);
|
||||
_hiCount++;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
void saveHiScores() {
|
||||
if (!SD.exists("/games")) SD.mkdir("/games");
|
||||
if (SD.exists(SNAKE_HI_PATH)) SD.remove(SNAKE_HI_PATH);
|
||||
|
||||
File f = SD.open(SNAKE_HI_PATH, FILE_WRITE);
|
||||
if (!f) return;
|
||||
|
||||
uint8_t ver = SNAKE_HI_VERSION;
|
||||
f.write(&ver, 1);
|
||||
uint8_t count = (uint8_t)_hiCount;
|
||||
f.write(&count, 1);
|
||||
|
||||
for (int i = 0; i < _hiCount; i++) {
|
||||
uint8_t buf[6];
|
||||
buf[0] = _hiScores[i].score & 0xFF;
|
||||
buf[1] = (_hiScores[i].score >> 8) & 0xFF;
|
||||
buf[2] = _hiScores[i].timestamp & 0xFF;
|
||||
buf[3] = (_hiScores[i].timestamp >> 8) & 0xFF;
|
||||
buf[4] = (_hiScores[i].timestamp >> 16) & 0xFF;
|
||||
buf[5] = (_hiScores[i].timestamp >> 24) & 0xFF;
|
||||
f.write(buf, 6);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
int insertHiScore(int score) {
|
||||
uint32_t now = (_rtc != nullptr) ? _rtc->getCurrentTime() : 0;
|
||||
int pos = _hiCount;
|
||||
for (int i = 0; i < _hiCount; i++) {
|
||||
if ((uint16_t)score > _hiScores[i].score) { pos = i; break; }
|
||||
}
|
||||
if (pos >= SNAKE_HI_COUNT) return -1;
|
||||
|
||||
int newCount = _hiCount + 1;
|
||||
if (newCount > SNAKE_HI_COUNT) newCount = SNAKE_HI_COUNT;
|
||||
for (int i = newCount - 1; i > pos; i--) _hiScores[i] = _hiScores[i - 1];
|
||||
|
||||
_hiScores[pos].score = (uint16_t)score;
|
||||
_hiScores[pos].timestamp = now;
|
||||
_hiCount = newCount;
|
||||
return pos;
|
||||
}
|
||||
|
||||
static void formatDate(uint32_t ts, char* buf, int bufLen) {
|
||||
if (ts == 0) { snprintf(buf, bufLen, "--"); return; }
|
||||
uint32_t days = ts / 86400;
|
||||
int year = 1970;
|
||||
while (true) {
|
||||
int diy = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) ? 366 : 365;
|
||||
if (days < (uint32_t)diy) break;
|
||||
days -= diy;
|
||||
year++;
|
||||
}
|
||||
static const int dim[] = {31,28,31,30,31,30,31,31,30,31,30,31};
|
||||
static const char* mn[] = {"Jan","Feb","Mar","Apr","May","Jun",
|
||||
"Jul","Aug","Sep","Oct","Nov","Dec"};
|
||||
bool leap = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0);
|
||||
int month = 0;
|
||||
for (month = 0; month < 12; month++) {
|
||||
int d = dim[month];
|
||||
if (month == 1 && leap) d = 29;
|
||||
if (days < (uint32_t)d) break;
|
||||
days -= d;
|
||||
}
|
||||
if (month > 11) month = 11;
|
||||
snprintf(buf, bufLen, "%d %s %d", (int)(days + 1), mn[month], year);
|
||||
}
|
||||
|
||||
public:
|
||||
SnakeScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _wantsExit(false), _gridW(0), _gridH(0),
|
||||
_offsetX(0), _offsetY(0), _headIdx(0), _length(0),
|
||||
_state(READY), _dir(RIGHT), _pendingDir(RIGHT),
|
||||
_score(0), _lastTick(0), _tickInterval(500), _rngState(0xBEEF),
|
||||
_hiCount(0), _newHiScore(false), _newHiRank(-1) {
|
||||
_food = {0, 0};
|
||||
memset(_body, 0, sizeof(_body));
|
||||
memset(_hiScores, 0, sizeof(_hiScores));
|
||||
}
|
||||
|
||||
bool wantsExit() const { return _wantsExit; }
|
||||
void clearExit() { _wantsExit = false; }
|
||||
GameState getState() const { return _state; }
|
||||
|
||||
void enter() {
|
||||
_wantsExit = false;
|
||||
loadHiScores();
|
||||
_lastTick = millis();
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
switch (_state) {
|
||||
case READY:
|
||||
if (c == '\r') { _state = PLAYING; _lastTick = millis(); return true; }
|
||||
if (c == 'q' || c == 'Q') { _wantsExit = true; return true; }
|
||||
return false;
|
||||
case PLAYING:
|
||||
switch (c) {
|
||||
case 'w': case 'W': if (_dir != DOWN) _pendingDir = UP; return true;
|
||||
case 's': case 'S': if (_dir != UP) _pendingDir = DOWN; return true;
|
||||
case 'a': case 'A': if (_dir != RIGHT) _pendingDir = LEFT; return true;
|
||||
case 'd': case 'D': if (_dir != LEFT) _pendingDir = RIGHT; return true;
|
||||
case 'q': case 'Q': _wantsExit = true; return true;
|
||||
default: return false;
|
||||
}
|
||||
case GAME_OVER:
|
||||
if (c == '\r') { resetGame(); _state = PLAYING; _lastTick = millis(); return true; }
|
||||
if (c == 'q' || c == 'Q') { _wantsExit = true; return true; }
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
if (_gridW == 0) {
|
||||
int usableW = display.width();
|
||||
int usableH = display.height() - SNAKE_HDR - SNAKE_FTR;
|
||||
_gridW = usableW / SNAKE_CELL;
|
||||
_gridH = usableH / SNAKE_CELL;
|
||||
_offsetX = (usableW - _gridW * SNAKE_CELL) / 2;
|
||||
_offsetY = SNAKE_HDR + (usableH - _gridH * SNAKE_CELL) / 2;
|
||||
resetGame();
|
||||
}
|
||||
|
||||
display.startFrame();
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.drawTextCentered(display.width() / 2, 2, "Snake");
|
||||
#else
|
||||
display.setCursor(2, 2);
|
||||
display.print("Snake");
|
||||
char scoreBuf[16];
|
||||
snprintf(scoreBuf, sizeof(scoreBuf), "Score: %d", _score);
|
||||
int sw = display.getTextWidth(scoreBuf);
|
||||
display.setCursor(display.width() - sw - 2, 2);
|
||||
display.print(scoreBuf);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, SNAKE_HDR - 2, display.width(), 1);
|
||||
|
||||
if (_state == READY) {
|
||||
int cx = display.width() / 2;
|
||||
int y = SNAKE_HDR + 6;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(cx, y, "Classic Snake");
|
||||
y += 14;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.drawTextCentered(cx, y, "Swipe to steer");
|
||||
#else
|
||||
display.drawTextCentered(cx, y, "W/S/A/D to steer");
|
||||
#endif
|
||||
y += 11;
|
||||
display.drawTextCentered(cx, y, "Eat food to grow");
|
||||
y += 11;
|
||||
display.drawTextCentered(cx, y, "Steer clear of walls");
|
||||
y += 16;
|
||||
|
||||
if (_hiCount > 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(cx, y, "-- High Scores --");
|
||||
y += 12;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
int showCount = (_hiCount < 5) ? _hiCount : 5;
|
||||
#else
|
||||
int showCount = (_hiCount < 10) ? _hiCount : 10;
|
||||
#endif
|
||||
for (int i = 0; i < showCount; i++) {
|
||||
char dateBuf[16];
|
||||
formatDate(_hiScores[i].timestamp, dateBuf, sizeof(dateBuf));
|
||||
char line[48];
|
||||
snprintf(line, sizeof(line), "%d. %d %s", i + 1, _hiScores[i].score, dateBuf);
|
||||
display.drawTextCentered(cx, y, line);
|
||||
y += 10;
|
||||
}
|
||||
y += 4;
|
||||
} else {
|
||||
y += 8;
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.drawTextCentered(cx, y, "Tap to start");
|
||||
#else
|
||||
display.drawTextCentered(cx, y, "Press Enter to start");
|
||||
#endif
|
||||
} else {
|
||||
if (_state == PLAYING) {
|
||||
unsigned long now = millis();
|
||||
if (now - _lastTick >= _tickInterval) { tick(); _lastTick = now; }
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(_offsetX - 1, _offsetY - 1,
|
||||
_gridW * SNAKE_CELL + 2, _gridH * SNAKE_CELL + 2);
|
||||
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
drawCellFilled(display, _food.x, _food.y);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
for (int i = 0; i < _length; i++) {
|
||||
int idx = (_headIdx - i + SNAKE_MAX_LEN) % SNAKE_MAX_LEN;
|
||||
if (i == 0) drawCellFilled(display, _body[idx].x, _body[idx].y);
|
||||
else drawCellOutline(display, _body[idx].x, _body[idx].y);
|
||||
}
|
||||
|
||||
if (_state == GAME_OVER) {
|
||||
int cx = display.width() / 2;
|
||||
int cy = display.height() / 2;
|
||||
int boxW = display.width() * 3 / 4;
|
||||
int boxH = _newHiScore ? 60 : 50;
|
||||
int boxX = cx - boxW / 2;
|
||||
int boxY = cy - boxH / 2;
|
||||
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.fillRect(boxX, boxY, boxW, boxH);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(boxX, boxY, boxW, boxH);
|
||||
|
||||
int ty = boxY + 8;
|
||||
display.drawTextCentered(cx, ty, "Game Over");
|
||||
ty += 14;
|
||||
char finalScore[24];
|
||||
snprintf(finalScore, sizeof(finalScore), "Score: %d", _score);
|
||||
display.drawTextCentered(cx, ty, finalScore);
|
||||
ty += 12;
|
||||
if (_newHiScore) {
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
char rankBuf[32];
|
||||
snprintf(rankBuf, sizeof(rankBuf), "New #%d High Score!", _newHiRank + 1);
|
||||
display.drawTextCentered(cx, ty, rankBuf);
|
||||
ty += 12;
|
||||
}
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.drawTextCentered(cx, ty, "Enter:Retry Q:Back");
|
||||
}
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
char footBuf[32];
|
||||
snprintf(footBuf, sizeof(footBuf), "Score: %d", _score);
|
||||
display.setTextSize(0);
|
||||
display.drawTextCentered(display.width() / 2, display.height() - 8, footBuf);
|
||||
display.setTextSize(1);
|
||||
#else
|
||||
display.setTextSize(1);
|
||||
int fy = display.height() - 12;
|
||||
display.drawRect(0, fy - 2, display.width(), 1);
|
||||
if (_state == PLAYING) {
|
||||
display.setCursor(2, fy);
|
||||
display.print("Q:Back");
|
||||
} else if (_state == READY) {
|
||||
display.setCursor(2, fy);
|
||||
display.print("Enter:Start Q:Back");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_state == PLAYING) return 100;
|
||||
return 5000;
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "DiscoveryScreen.h"
|
||||
#include "LastHeardScreen.h"
|
||||
#include "Tracescreen.h"
|
||||
#include "GamesMenuScreen.h"
|
||||
#include "SnakeScreen.h"
|
||||
#ifdef MECK_WEB_READER
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
@@ -536,20 +538,29 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Third row: single centred Trace tile (column 1 position only)
|
||||
// Third row: Trace (col 0) + Games (col 1)
|
||||
{
|
||||
int row3y = gridY + 2 * (tileH + gapY);
|
||||
int col1x = gridX + (tileW + gapX);
|
||||
|
||||
// Trace tile (column 0)
|
||||
int col0x = gridX;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(col1x, row3y, tileW, tileH);
|
||||
|
||||
int iconX = col1x + (tileW - HOME_ICON_W) / 2;
|
||||
display.drawRect(col0x, row3y, tileW, tileH);
|
||||
int iconX = col0x + (tileW - HOME_ICON_W) / 2;
|
||||
int iconY = row3y + 2;
|
||||
display.drawXbm(iconX, iconY, icon_trace, HOME_ICON_W, HOME_ICON_H);
|
||||
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.drawTextCentered(col1x + tileW / 2, row3y + 15, "Trace");
|
||||
display.drawTextCentered(col0x + tileW / 2, row3y + 15, "Trace");
|
||||
|
||||
// Games tile (column 1)
|
||||
int col1x = gridX + (tileW + gapX);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(col1x, row3y, tileW, tileH);
|
||||
iconX = col1x + (tileW - HOME_ICON_W) / 2;
|
||||
iconY = row3y + 2;
|
||||
display.drawXbm(iconX, iconY, icon_gamepad, HOME_ICON_W, HOME_ICON_H);
|
||||
display.setTextSize(_node_prefs->smallTextSize());
|
||||
display.drawTextCentered(col1x + tileW / 2, row3y + 15, "Games");
|
||||
}
|
||||
|
||||
// Nav hint at bottom of screen
|
||||
@@ -632,7 +643,8 @@ public:
|
||||
#endif
|
||||
y += menuLH;
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.drawTextCentered(display.width() / 2, y, "[R] Trace");
|
||||
display.setCursor(col1, y); display.print("[R] Trace");
|
||||
display.setCursor(col2, y); display.print("[J] Games");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
y += menuLH;
|
||||
y += 2;
|
||||
@@ -670,7 +682,7 @@ public:
|
||||
#endif
|
||||
y += 10;
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.drawTextCentered(display.width() / 2, y, "[R] Trace");
|
||||
display.drawTextCentered(display.width() / 2, y, "[R] Trace [J] Games ");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
y += 14;
|
||||
}
|
||||
@@ -1389,6 +1401,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
discovery_screen = new DiscoveryScreen(this, &rtc_clock);
|
||||
last_heard_screen = new LastHeardScreen(&rtc_clock);
|
||||
trace_screen = new TraceScreen(this, &rtc_clock);
|
||||
games_menu_screen = new GamesMenuScreen(this);
|
||||
snake_screen = new SnakeScreen(this, &rtc_clock);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
|
||||
lock_screen = new LockScreen(this, &rtc_clock, node_prefs);
|
||||
#endif
|
||||
@@ -3109,6 +3123,28 @@ void UITask::gotoTraceScreen() {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoGamesMenu() {
|
||||
GamesMenuScreen* gm = (GamesMenuScreen*)games_menu_screen;
|
||||
gm->enter();
|
||||
setCurrScreen(games_menu_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoSnakeScreen() {
|
||||
SnakeScreen* ss = (SnakeScreen*)snake_screen;
|
||||
ss->enter();
|
||||
setCurrScreen(snake_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::onTraceResult(uint32_t tag, uint8_t flags, const uint8_t* path_snrs,
|
||||
const uint8_t* path_hashes, uint8_t path_len, int8_t final_snr) {
|
||||
TraceScreen* ts = (TraceScreen*)trace_screen;
|
||||
|
||||
@@ -62,7 +62,7 @@ class UITask : public AbstractUITask {
|
||||
unsigned long _alert_expiry;
|
||||
bool _hintActive = false; // Boot navigation hint overlay
|
||||
unsigned long _hintExpiry = 0; // Auto-dismiss time for hint
|
||||
bool _pendingBootHint = false; // Deferred hint — show after splash screen
|
||||
bool _pendingBootHint = false; // Deferred hint -- show after splash screen
|
||||
int _msgcount;
|
||||
unsigned long ui_started_at, next_batt_chck;
|
||||
uint8_t _low_batt_count = 0; // Consecutive low-voltage readings for debounce
|
||||
@@ -100,6 +100,8 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* discovery_screen; // Node discovery scan screen
|
||||
UIScreen* last_heard_screen; // Last heard passive advert list
|
||||
UIScreen* trace_screen; // Trace path screen (standalone trace tool)
|
||||
UIScreen* games_menu_screen; // Games launcher menu
|
||||
UIScreen* snake_screen; // Snake game screen
|
||||
#ifdef MECK_WEB_READER
|
||||
UIScreen* web_reader; // Web reader screen (lazy-init, WiFi required)
|
||||
#endif
|
||||
@@ -119,7 +121,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* _screenBeforeVKB = nullptr;
|
||||
unsigned long _vkbOpenedAt = 0;
|
||||
|
||||
// Powersaving: light sleep when locked + idle (standalone only — no BLE/WiFi)
|
||||
// Powersaving: light sleep when locked + idle (standalone only -- no BLE/WiFi)
|
||||
// Wakes on LoRa packet (DIO1), boot button (GPIO0), or 30-min timer
|
||||
#if !defined(BLE_PIN_CODE) && !defined(MECK_WIFI_COMPANION)
|
||||
unsigned long _psLastActive = 0; // millis() at last wake or lock entry
|
||||
@@ -202,6 +204,8 @@ public:
|
||||
void gotoDiscoveryScreen(); // Navigate to node discovery scan
|
||||
void gotoLastHeardScreen(); // Navigate to last heard passive list
|
||||
void gotoTraceScreen(); // Navigate to trace path screen
|
||||
void gotoGamesMenu(); // Navigate to games launcher menu
|
||||
void gotoSnakeScreen(); // Navigate to snake game
|
||||
#if HAS_GPS
|
||||
void gotoMapScreen(); // Navigate to map tile screen
|
||||
#endif
|
||||
@@ -234,7 +238,7 @@ public:
|
||||
int getDMUnreadCount(int contactIdx) const;
|
||||
void clearDMUnread(int contactIdx);
|
||||
|
||||
// Flag: suppress room→conversation redirect on next login (L key admin access)
|
||||
// Flag: suppress room->conversation redirect on next login (L key admin access)
|
||||
bool _skipRoomRedirect = false;
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool isButtonPressed() const;
|
||||
@@ -259,6 +263,8 @@ public:
|
||||
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
|
||||
bool isOnLastHeardScreen() const { return curr == last_heard_screen; }
|
||||
bool isOnTraceScreen() const { return curr == trace_screen; }
|
||||
bool isOnGamesMenu() const { return curr == games_menu_screen; }
|
||||
bool isOnSnakeScreen() const { return curr == snake_screen; }
|
||||
bool isOnMapScreen() const { return curr == map_screen; }
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(LilyGo_TDeck_Pro)
|
||||
bool isLocked() const { return _locked; }
|
||||
@@ -344,6 +350,8 @@ public:
|
||||
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
|
||||
UIScreen* getLastHeardScreen() const { return last_heard_screen; }
|
||||
UIScreen* getTraceScreen() const { return trace_screen; }
|
||||
UIScreen* getGamesMenuScreen() const { return games_menu_screen; }
|
||||
UIScreen* getSnakeScreen() const { return snake_screen; }
|
||||
UIScreen* getMapScreen() const { return map_screen; }
|
||||
#ifdef MECK_WEB_READER
|
||||
UIScreen* getWebReaderScreen() const { return web_reader; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// =============================================================================
|
||||
// HomeIcons — 12x12 icon sprites for T5S3 home screen tiles
|
||||
// HomeIcons -- 12x12 icon sprites for T5S3 home screen tiles
|
||||
// MSB-first, 2 bytes per row (same format as emoji sprites)
|
||||
// =============================================================================
|
||||
|
||||
@@ -48,7 +48,7 @@ static const uint8_t icon_search[] PROGMEM = {
|
||||
0x3C,0x00, 0x03,0x00, 0x01,0x80, 0x00,0xC0, 0x00,0x40, 0x00,0x00,
|
||||
};
|
||||
|
||||
// ⏰ Alarm Clock (AlarmScreen) — 12x12 home tile icon
|
||||
// ⏰ Alarm Clock (AlarmScreen) -- 12x12 home tile icon
|
||||
static const uint8_t icon_alarm[] PROGMEM = {
|
||||
0x40,0x40, 0x9E,0x20, 0x20,0x80, 0x44,0x40, 0x44,0x40, 0x46,0x40,
|
||||
0x40,0x40, 0x20,0x80, 0x1F,0x00, 0x00,0x00, 0x20,0x40, 0x40,0x20,
|
||||
@@ -60,7 +60,22 @@ static const uint8_t icon_trace[] PROGMEM = {
|
||||
0xFF,0xF0, 0x00,0xE0, 0x00,0xC0, 0x00,0x80, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
|
||||
// 🔔 Bell — 7x8 status bar indicator (alarm enabled)
|
||||
// 🎮 Gamepad (Games)
|
||||
// ..########..
|
||||
// .#........#.
|
||||
// .#..#..##.#.
|
||||
// .#.###....#.
|
||||
// .#..#..##.#.
|
||||
// .#........#.
|
||||
// ..##....##..
|
||||
// ...#....#...
|
||||
// ...######...
|
||||
static const uint8_t icon_gamepad[] PROGMEM = {
|
||||
0x00,0x00, 0x3F,0xC0, 0x40,0x20, 0x49,0xA0, 0x5C,0x20, 0x49,0xA0,
|
||||
0x40,0x20, 0x30,0xC0, 0x10,0x80, 0x1F,0x80, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
|
||||
// 🔔 Bell -- 7x8 status bar indicator (alarm enabled)
|
||||
// MSB-first, 1 byte per row
|
||||
#define BELL_ICON_W 7
|
||||
#define BELL_ICON_H 8
|
||||
|
||||
Reference in New Issue
Block a user