# 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 ```cpp 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: ```cpp 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()`**: ```cpp 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 ```cpp 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