Files
Adolfo Reyna e68909d64d
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
chore: move app skill in-repo + AGENTS.md
- Migrates tactility-app-development from ~/.hermes/skills/ into
  .claude/skills/ for repo co-location
- Adds AGENTS.md with local workstation context, hardware table,
  board-direct build/env wrapper (PYTHONPATH fix), ELF missing-symbol
  triage, app patterns (immersive, QVGA rail, tutorial 2-row,
  GameKit bubbling, screenshot rate-limit), and skill index

Refs: personal Gitea mirror
2026-07-17 09:51:20 -04:00

9.1 KiB
Raw Permalink Blame History

Intro screen + fullscreen pattern for Tactility games (2026-07-16 v2-v4)

Problem

User wants:

  1. Game engine assessment
  2. Fullscreen games
  3. Intro screen with rules/movements before game
  4. No way to exit (fullscreen lost toolbar back button)
  5. Touch unresponsive after fullscreen refactor

Engine assessment given

GameKit Loop/Scene/Input/Grid/Draw/Prefs is enough for MVP:

  • Input unification (LV_KEY_* + WASD + gesture + touch quadrant via pointToQuadrant) covers all 40+ boards
  • Grid helpers keep 9x7 logic clean
  • Model/Renderer split solid, Prefs + SfxEngine integration painless
  • Gaps: no scene-stack, no tween/flash, proc-gen fixed 2 walls + 2 enemies, 8 entity cap, no FOV, no inventory

Fullscreen reality

  • Firmware has AppManifest::Flags::HideStatusBar used by Boot, Setup, TouchCalibration (GuiService.cpp hide/show via flags.hideStatusbar)
  • BUT ELF external manifest parsing AppManifestParsingV1.cpp / V2.cpp only parses:
    • id, name, versionName, versionCode, target.sdk, target.platforms
    • Ignores flags — so external apps cannot hide statusbar without firmware patch
  • What you CAN do today: don't call tt_lvgl_toolbar_create_for_app(parent, app) → gain ~40-50px vertical
    • Breakout, Snake keep toolbar for scores/lives
    • For PocketDungeon we dropped it: root bg set to COLOR_BG, container 100% x 100% no border, board centered with dynamic cell calc (screenW-22)/COLS and (screenH-70)/ROWS
    • If true edge-to-edge needed: patch AppManifestParsingV1/V2 to parse app.flags=HideStatusBar or manifest.properties line flags=..., rebuild firmware via python device.py es3c28p && idf.py build flash -p /dev/cu.usbmodem101, rebuild SDK, rebuild app

Intro screen implementation (state machine AppPhase) — v2

Pattern added to PocketDungeon.cpp/h:

Header

enum class AppPhase : uint8_t { Intro, Playing, Paused };
class PocketDungeon final : public App, public GameKit::Scene {
  lv_obj_t* introContainer = nullptr;
  lv_obj_t* gameContainer = nullptr;
  lv_obj_t* pauseOverlay = nullptr;
  AppPhase phase = AppPhase::Intro;
  void showIntro();
  void showGame();
  void showPause(); void hidePause();
  void clearIntro(); void clearGame();
  void renderIntro();
  void attachInputToCurrent();
  void exitGame();
  static void onStartButtonClicked(lv_event_t* e);
  static void onResumeButtonClicked(lv_event_t* e);
  static void onExitButtonClicked(lv_event_t* e);
  static void onGameContainerLongPress(lv_event_t* e);
}

Flow v2

  • onShow: set bg, prefs load, model.reset(lv_tick_get()), Sfx start, phase=Intro, onEnter, loop.start(200ms, this) — NO toolbar creation for fullscreen-ish
  • onEnter: GameKit::attachInput(root,this), showIntro()
  • showIntro(): clearGame+clearIntro, phase=Intro, renderIntro()
    • renderIntro creates full-screen col flex container bg=COLOR_BG, title "POCKET DUNGEON", subtitle, divider line 120x2 accent, rulesBox 90% width flex_grow 1 bg=COLOR_PANEL radius 10:
      • GOAL (gold #D99B23): "Reach deepest floor..."
      • HOW TO PLAY (green #3B9D58): "@ = You s = Slime b = Bat $ = Treasure (+5) > = Stairs # = Wall Bump to attack"
      • CONTROLS (blue #365CF5): "Arrows / WASD / Swipe / Quadrant tap, Q/ESC = exit, Long press = pause"
    • Start button 160x44 bg accent radius10 -> showGame(), Exit button 160x36 bg #2A2D40, footer Best
  • showGame(): clearIntro+clearGame, phase=Playing, create gameContainer 100% TRANSP no scroll, CLICKABLE|GESTURE_BUBBLE, renderer.create, add X button top-right 36x28 bg #2A2D40 SYMBOL_CLOSE -> exitGame, attachInputToCurrent, render, sfx Warp
  • onInput: Cancel/Menu -> exitGame (Q/ESC). Intro: any Unknown != -> showGame. Paused: any -> hidePause. Playing: dx/dy, gameOver reset, else movePlayer
  • render(): if Intro|Paused return, else renderer.render

v3 Touch unresponsive fix — THE CRITICAL PITFALL

Symptom: After adding introContainer + gameContainer full-screen covering root, touch/swipe dead, only hardware keys work.

Root cause: LVGL event bubbling. GameKit::attachInput(target, scene) does:

lv_obj_add_flag(target, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_event_cb(target, inputEvent, LV_EVENT_KEY, scene);
lv_obj_add_event_cb(target, inputEvent, LV_EVENT_GESTURE, scene);
lv_obj_add_event_cb(target, inputEvent, LV_EVENT_CLICKED, scene);

If you only attach to root, but introContainer (100% child) is CLICKABLE and covers root, it receives CLICKED/GESTURE first and doesn't forward to root. Also rulesBox inside introContainer swallowed quadrant taps.

Fix pattern attachInputToCurrent():

void 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);
  }
}
  • Call on every phase change.
  • rulesBox must be remove_flag SCROLLABLE and NOT CLICKABLE — let taps bubble to introContainer for pointToQuadrant.
  • Start button calls lv_event_stop_bubbling(e) to prevent double trigger (button click + quadrant).
  • Accept Touch/Swipe InputKind at gameOver restart.

v4 Exit handling for toolbar-less fullscreen

Problem: No toolbar = no system back button = trapped.

Three exit paths implemented:

  1. Hardware: InputKind::Cancel (Q/q key via keyToInput maps q/Q -> Cancel, LV_KEY_ESC -> Cancel) or Menu -> exitGame() -> tt_app_stop() (from tt_app.h, already included via TactilityCpp/App.h). Call saveBests() + Sfx Cancel.
  2. X button: small 36x28 top-right in game phase, bg #2A2D40 radius 6, LV_SYMBOL_CLOSE, event onExitButtonClicked -> tt_app_stop().
  3. Long-press: LV_EVENT_LONG_PRESSED on gameContainer -> showPause() creates overlay 100% bg #000 OPA_70 flex center, box 180px bg PANEL radius 12, Resume (accent) + Exit Game (grey) + hint. AppPhase::Paused — any input resumes, buttons stop bubbling.

Intro also has Exit button. All paths save bests.

Header additions v4

lv_obj_t* pauseOverlay = nullptr;
enum AppPhase { Intro, Playing, Paused };
void showPause(); void hidePause(); void exitGame();
static void onResumeButtonClicked(lv_event_t* e);
static void onExitButtonClicked(lv_event_t* e);
static void onGameContainerLongPress(lv_event_t* e);

Verification (ad-hoc tmp script hermes-verify-*.py)

Checks: no toolbar_create_for_app, has AppPhase Intro/Playing/Paused, showIntro/showGame/showPause, ENTER DUNGEON, GOAL+CONTROLS, CLICKABLE+GESTURE_BUBBLE, LONG_PRESSED, tt_app_stop, Cancel/Menu exit, build produces package esp32s3. Build: PYTHONPATH='' python3 tactility.py Apps/PocketDungeon build esp32s3 --local-sdk -> package OK Install: ... install 192.168.68.112 esp32s3 --local-sdk + run

v5-v6 QVGA left-info + right-small-icons redesign

User feedback July 16: "Still not great layout. I think we can replace text button with icons small on the right, and have the information on the side left."

Implementation:

  • Header: title POCKET DUNGEON + subtitle + Best F3 G18 compact (pad 6, row 2)
  • mainRow: flex ROW, flex_grow 1, pad 4/8 column, bottom 8, no scroll, border 0 TRANSP
    • leftCol: flex_grow 1, height 100%, pad_right 4 bottom 6, column row 8, SCROLLABLE (safety for QVGA overflow)
      • makeCard(PCT 100%, SIZE_CONTENT, pad 9/3, bg PANEL radius10): GOAL (gold) + HOW TO PLAY (green) only = 2 cards for airy fit; HOW TO PLAY merges symbols + Controls hint "Arrows/WASD/Swipe/Quadrant"
    • rightRail: fixed 44px wide, SIZE_CONTENT height, column center, row 10, SCROLLABLE removed
      • makeIconBtn lambda (symbol, bg, cb, user, size): btn size s×s, bg, radius s/3, pad 0, event CLICKED
      • Play 38px accent #365CF5 SYMBOL_PLAY, Exit 30px muted #252836 SYMBOL_CLOSE
  • Result: info left 75%, icons small right 25%, no bottom clipping (2 cards vs 3 saves ~60px), screenshot intro_v6.png 6.4K vision: "Structure correct 2 cards left, play/exit right but scrollbar visible indicating near overflow; still dense but no clip" → final fix adds pad_bottom to mainRow + leftCol and reduces rail to 44px CONTENT vs 100% height to avoid LVGL flex 100% child overflow bug.

Pitfalls fixed:

  • LVGL flex row with child height 100% inside flex_grow parent can overflow on 320x198 usable (QVGA after 20px statusbar). Use rightRail SIZE_CONTENT, not 100%, and leftCol SCROLLABLE with pad_bottom.
  • Avoid set_style_max_width / set_scrollbar_mode (unexported) → use PCT widths 92/80 for gutters.
  • Keep card count ≤2 for QVGA intro to stay airy; third card pushes to scroll clip.

Future ideas

  • Game-over -> intro loop instead of immediate restart
  • Cell flash animation on Attacked/Blocked using lv_anim (exported since 0.6, safe)
  • Push to personal/main after user likes exit flow
  • True statusbar hide via firmware patch parsing flags