# Development The playable app lives in `web/`. Stack: React 18, Vite 5, Tailwind 3. Android is the same app inside a Capacitor WebView (`web/android/`). No TypeScript, no backend. ## Screens `App.jsx` switches three states: 1. **Loading** — preload images / audio / the settings video, then **Nadaljuj** 2. **Settings** — name, operations, difficulty, audio, start modal 3. **Game** — one session, then game-over overlay Audio prefs persist as `matematika_music`. Game settings persist as `matematika_settings`. Language persists as `matematika_lang` (`sl` | `en`). UI copy lives in `web/src/i18n/sl.json` and `en.json`. Screens call `t('key')` from `useI18n()`. Difficulty is stored as `easy` / `medium` / `hard`, never as a translated label. To add a language, add a JSON file with the same keys, register it in `i18n/core.js`, and add a button in Settings. ## Session loop `Game.jsx` only runs a session. It does not invent operands. 1. `buildSession(config, level)` in `questionGenerator.js` builds the full list. 2. `showQuestion(i)` shows item `i` from that list. 3. `resolveAnswer(given)` scores a tap or keypad OK. 4. After the last item, `GameOver` offers restart (rebuilds the pool) or back to settings. Question index is a ref, so restart cannot immediately re-trigger game-over from a stale counter. Each question looks like `{ op, a, b, result, text }`. `op` is one of `add` | `sub` | `mul` | `div` (`src/config.js`). ## Difficulty vs settings - **Settings** choose operations, question count, add/sub range (`max_number`), multiply factors, divide divisors. - **Difficulty** (`src/levels.js`) chooses input UI and some caps: | Key | Input | Choices | Multiply other-factor cap | |---|---|---|---| | `easy` | choices | 4 | 1–10 | | `medium` | choices | 6 | 1–12 | | `hard` | keypad | — | 1–20 | Negatives are off (`allowNegative: false`). The keypad hides `±` unless that flag is flipped. Full product wording: [LEVELS.md](../LEVELS.md). ## Source map ``` web/src/ App.jsx main.jsx boot + service worker register config.js defaultConfig, OPS, mergeConfig levels.js easy / medium / hard questionGenerator.js + − × ÷ factory + distractors loadAssets.js hooks/useAnimatedBackgrounds.js components/ Loading.jsx Settings.jsx Game.jsx session only AnswerChoices.jsx Keypad.jsx GameQuestionCard.jsx GameToolbar.jsx GameOver.jsx ExitConfirm.jsx BackgroundMusic.jsx ToastContainer.jsx UpdateBanner.jsx NumberPills.jsx ``` `public/sw.js` caches the shell using paths relative to its own scope (safe with Vite `base: './'`). ## Adding an operation later Keep generation out of `Game.jsx`: 1. Add a named op in `config.js` if needed. 2. Teach `buildSession` how to emit `{ op, a, b, result, text }`. 3. Add a settings toggle only when the UI exists. 4. Cover it in `questionGenerator.test.js`. Rounding, dice, calculator, equations, and text problems were removed from the live path on purpose. ## Local checks ```bash npm test npm run build ``` Android store build: [ANDROID.md](ANDROID.md). Optional Playwright smoke (`scripts/smoke.mjs`) walks loading → easy → restart → medium (wrong then right) → hard keypad (empty OK disabled, no `±`) → a 390×844 viewport.