diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 4ac57a101..000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,63 +0,0 @@ -# Terratomic – AI coding agent guide - -This repo is a TypeScript, ESM-first mono-app: a browser client built with Webpack talking to a clustered Node server. The shared game logic lives in `src/core` and runs in both environments. - -## Architecture map - -- Client (entry: `src/client/Main.ts`) - - Lit-based web components, PIXI rendering, Tailwind styles. - - Dev server on port 9000; proxies to backend and worker websockets. - - Example: joining games and UI wiring lives in `src/client/*Modal.ts`, networking/events in `src/client/Transport.ts`. -- Core shared logic (`src/core`) - - Game model, executors, schemas, pathfinding, validations. - - Example: `src/core/GameRunner.ts` drives turns and filters updates (e.g., submarine stealth via `filterUpdatesForClient`). - - Lightweight event system: `src/core/EventBus.ts`. -- Server (entry: `src/server/Server.ts`) - - Node cluster master/worker split (`Master.ts`, `Worker.ts`), game coordination in `GameManager.ts` and websocket handling in `GameServer.ts`/`Client.ts`. - - Config comes from `src/core/configuration/ConfigLoader` and `Config` enums; `GAME_ENV=dev|prod` gates behavior. - - Gatekeeping/rate-limits live under `src/server/gatekeeper` (lint-ignored for now). -- Assets & data - - Static assets in `resources/` copied to `static/` on build (maps under `resources/maps` are excluded). - - Proprietary images (optional) in `proprietary/images` also copied to `static/images`. - -## Dev, build, test - -- Install and run locally - - `npm install` - - `npm run dev` starts client (9000 with proxies to server) and server workers with `GAME_ENV=dev`. -- Server-only - - `npm run start:server-dev` (dev env); `npm run start:server` (prod env, no proxies). -- Bundles - - `npm run build-dev` | `npm run build-prod`. Outputs to `static/` with content-hashed filenames. Entry is `src/client/Main.ts`. -- Utilities - - Generate terrain maps: `npm run build-map` (TS loader via ts-node/esm). - - Perf micro-benchmarks: `npm run perf` runs `tests/perf/*.ts` via tsx. -- Tests & lint - - `npm test` uses Jest + @swc/jest with ESM TS. Coverage thresholds set in `jest.config.ts`. - - `npm run lint` (ESLint flat config + Prettier). Lint-staged + Husky enforce fixes on commit. - -## Conventions and pitfalls specific to this repo - -- ESM everywhere - - `"type": "module"`; use `import`/`export` only—no `require`/`module.exports`. - - Jest maps extensioned imports: `"^(\\.{1,2}/.*)\\.js$": "$1"`. In TS source, import without `.js` and let tooling handle it. -- Shared code boundaries - - Code in `src/core` must stay isomorphic (no DOM/Node-only APIs). Server/client specifics live in `src/server` or `src/client`. -- Networking - - Client dev server proxies: `/socket` → backend (ws:3000), `/w0|/w1|/w2` → worker processes (ws/http ports 3001–3003). Keep these paths stable when adding endpoints. -- Build wiring - - Webpack copies `resources/**` to `static/` (except `resources/maps/**`) and `proprietary/images/**` to `static/images/`. Add new static assets under these roots. -- Logging/telemetry - - Server uses Winston; optional OpenTelemetry exporters are present—prefer existing `OtelResource.ts` if instrumenting. - -## Good starting points (examples) - -- Add a new UI module: place a Lit element under `src/client/components/`, import it in `Main.ts`, wire events via `EventBus`. -- Extend game logic: add execution under `src/core/execution/**` and surface view updates through `GameUpdates` consumed by `GameRunner`. -- Add a server API: implement handler in `src/server/GameServer.ts` (or worker), ensure proxy path is listed in `webpack.config.js` devServer `proxy`. - -## Licensing notes - -- Code: AGPLv3 (see `LICENSE`). -- Assets in `resources/`: CC BY-SA 4.0; keep attribution; do not relicense. -- `proprietary/` has separate terms; see `CLA.md` and `proprietary/LICENSE`. diff --git a/.gitignore b/.gitignore index 70b86f8b1..083cb5fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ out/ static/ coverage/ TODO.txt +PROGRESS.md resources/images/.DS_Store resources/.DS_Store .env* @@ -13,3 +14,4 @@ errors.txt Project_Info.md commands.sh gemini/ +.github/copilot-instructions.md diff --git a/README.md b/README.md index 96b46df1a..6742640b1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,18 @@ Until then, open issues, submit pull requests, or join the discussion [on Discor - `src/scripts` – Dev or build-time scripts - `resources/` – Static assets (flags, fonts, icons, maps, sprites, images) - `tests/` – Unit and integration tests for client, core logic, and utilities +- `docs/mobile/` – Mobile UI documentation (see below) + +--- + +## 📱 Mobile UI Docs + +Touch-first mobile layer built with Lit web components under `src/client/mobile/`. + +- [Architecture](docs/mobile/MOBILE-ARCHITECTURE.md) — component map, activation lifecycle, event bus, file inventory +- [Feature Matrix](docs/mobile/MOBILE-FEATURE-MATRIX.md) — desktop → mobile parity table, HUD layout, z-index map +- [Gestures & Haptics](docs/mobile/MOBILE-GESTURES-HAPTICS.md) — gesture detection config, haptic patterns, usage by area +- [Action Grid Catalog](docs/mobile/MOBILE-ACTION-GRID-CATALOG.md) — tile category resolution, full action tables per context --- diff --git a/docs/mobile/MOBILE-ACTION-GRID-CATALOG.md b/docs/mobile/MOBILE-ACTION-GRID-CATALOG.md new file mode 100644 index 000000000..cda745c5b --- /dev/null +++ b/docs/mobile/MOBILE-ACTION-GRID-CATALOG.md @@ -0,0 +1,196 @@ +# Mobile Action Grid Catalog + +> Last updated: 2026-02-22 + +Complete action inventory for `MobileActionGrid`, sourced directly from code. + +Related docs: [Architecture](MOBILE-ARCHITECTURE.md) · [Feature Matrix](MOBILE-FEATURE-MATRIX.md) · [Gestures & Haptics](MOBILE-GESTURES-HAPTICS.md) + +--- + +## Category Resolution + +`MobileActionGrid.determineTileCategory()` maps each tile tap to one of these categories: + +| Category | Condition | +| ------------------------- | -------------------------------------------------------------- | +| `spawn-phase` | Game in spawn phase | +| `own-land` | Own tile, land (includes shore — shore uses same land actions) | +| `own-shore` | Own tile, shoreline (delegates to own-land actions) | +| `own-water` | Own tile, water | +| `enemy-can-attack` | Enemy/player tile, `canAttack` is true | +| `enemy-can-boat-attack` | Enemy/player land tile, reachable by transport ship only | +| `enemy-no-attack` | Enemy/player tile, no ground or boat attack possible | +| `neutral-can-attack` | Neutral tile, ocean (ship building) or land with `canAttack` | +| `neutral-can-boat-attack` | Neutral land tile, reachable by transport ship only | + +Own-land/own-shore/own-water also append a **Stack Mode toggle** at the end of the actions list. + +All categories (except spawn-phase) also prepend **Unit Selection actions** when a selectable unit (Warship, Submarine, Fighter Jet, Artillery) is within 40px screen distance of the tap position. + +--- + +## Actions by Category + +### Spawn Phase + +| Action | ID | Priority | Condition | +| ---------- | ------- | -------- | ----------------- | +| Spawn Here | `spawn` | high | Unowned land tile | + +--- + +### Unit Selection (All Categories) + +When tapping near a player-owned selectable unit (within 40px screen distance), a "Select [Unit]" action is prepended to the grid. This works on **all** tile categories — own, enemy, or neutral — because units can be on any tile (e.g. ships on unowned ocean, jets over enemy land). + +| Action | ID | Priority | Condition | +| ------------------ | ----------------------------- | -------- | ---------------------------------- | +| Select Warship | `unit:select:Warship:` | high | Own Warship within 40px of tap | +| Select Submarine | `unit:select:Submarine:` | high | Own Submarine within 40px of tap | +| Select Fighter Jet | `unit:select:FighterJet:` | high | Own Fighter Jet within 40px of tap | +| Select Artillery | `unit:select:Artillery:` | high | Own Artillery within 40px of tap | + +After selecting a unit, the action grid closes and a **floating banner** appears ("📍 [Unit] selected — tap to redirect ✕"). Tapping any valid tile immediately emits the corresponding `Move*IntentEvent`. Artillery has an additional range check. Tapping ✕ cancels selection. + +--- + +### Own Land / Own Shore + +All unlocked land structures. Disabled (greyed) if gold insufficient. Research-locked structures hidden until prerequisite unlocked. + +| Action | ID | Priority | Requirement | +| --------------- | ---------------------- | -------- | ------------------------------ | +| Port | `build:Port` | high | Nearby ocean shore (≤10 tiles) | +| City | `build:City` | high | — | +| Factory | `build:Factory` | high | — | +| Defense Post | `build:DefensePost` | normal | — | +| Airfield | `build:Airfield` | high | — | +| Hospital | `build:Hospital` | normal | `HospitalResearch` | +| Missile Silo | `build:MissileSilo` | normal | `NuclearFission` | +| Research Lab | `build:ResearchLab` | normal | — | +| Academy | `build:Academy` | normal | — | +| SAM Launcher | `build:SAMLauncher` | normal | — | +| Doomsday Device | `build:DoomsdayDevice` | normal | `DoomsdayDeviceResearch` | +| Artillery | `build:Artillery` | normal | Factory + `ArtilleryResearch` | +| Fighter Jet | `build:FighterJet` | normal | Airfield + `JetEngines` | +| Stack Mode | `mode:stack-toggle` | — | Always (last item) | + +Port only appears when a BFS within 10 tiles finds an owned ocean-shore tile. + +--- + +### Own Water + +| Action | ID | Priority | Requirement | +| ----------- | ------------------- | -------- | -------------------------- | +| Port | `build:Port` | high | Nearby ocean shore (≤10) | +| Warship | `build:Warship` | high | Owns a Port | +| Submarine | `build:Submarine` | high | Port + `SubmarineResearch` | +| Fighter Jet | `build:FighterJet` | normal | Airfield + `JetEngines` | +| Stack Mode | `mode:stack-toggle` | — | Always (last item) | + +--- + +### Enemy — Can Ground Attack + +| Action | ID | Priority | Requirement | +| ---------------- | -------------------------- | -------- | ----------------------------------- | +| Ground Attack | `attack:ground` | high | Troops > 0 | +| Paratroopers | `attack:airstrike` | normal | Airfield + `JetEngines` | +| Bomber Run | `attack:bomber` | normal | Airfield + at war | +| Fighter Jet | `build:FighterJet` | normal | Airfield + `JetEngines` | +| Request Peace | `diplomacy:request-peace` | normal | At war with target | +| Break Alliance | `diplomacy:break-alliance` | normal | Allied with target | +| Propose Alliance | `diplomacy:propose-ally` | normal | Neutral relationship | +| Declare War | `attack:declare-war` | normal | Not at war | +| Atom Bomb | `attack:nuke-atom` | normal | Silo + `NuclearFission` + 5K gold | +| H-Bomb | `attack:nuke-hbomb` | normal | Silo + `ThermonuclearStaging` + 15K | +| MIRV | `attack:nuke-mirv` | normal | Silo + `MIRVTechnology` + 50K | + +--- + +### Enemy — Can Boat Attack + +| Action | ID | Priority | Requirement | +| ---------------- | -------------------------- | -------- | ----------------------------------- | +| Naval Assault | `attack:naval` | high | Troops > 0 | +| Paratroopers | `attack:airstrike` | normal | Airfield + `JetEngines` | +| Bomber Run | `attack:bomber` | normal | Airfield + at war | +| Request Peace | `diplomacy:request-peace` | normal | At war with target | +| Break Alliance | `diplomacy:break-alliance` | normal | Allied with target | +| Propose Alliance | `diplomacy:propose-ally` | normal | Neutral relationship | +| Declare War | `attack:declare-war` | normal | Not at war | +| Atom Bomb | `attack:nuke-atom` | normal | Silo + `NuclearFission` + 5K gold | +| H-Bomb | `attack:nuke-hbomb` | normal | Silo + `ThermonuclearStaging` + 15K | +| MIRV | `attack:nuke-mirv` | normal | Silo + `MIRVTechnology` + 50K | + +--- + +### Enemy — No Attack Possible + +Diplomacy + air + nukes only. Peace and alliance are promoted to high priority. + +| Action | ID | Priority | Requirement | +| ---------------- | -------------------------- | -------- | ----------------------------------- | +| Paratroopers | `attack:airstrike` | **high** | Airfield + `JetEngines` | +| Bomber Run | `attack:bomber` | **high** | Airfield + at war | +| Request Peace | `diplomacy:request-peace` | **high** | At war with target | +| Propose Alliance | `diplomacy:propose-ally` | **high** | Neutral relationship | +| Break Alliance | `diplomacy:break-alliance` | normal | Allied with target | +| Declare War | `attack:declare-war` | normal | Not at war | +| Atom Bomb | `attack:nuke-atom` | normal | Silo + `NuclearFission` + 5K gold | +| H-Bomb | `attack:nuke-hbomb` | normal | Silo + `ThermonuclearStaging` + 15K | +| MIRV | `attack:nuke-mirv` | normal | Silo + `MIRVTechnology` + 50K | + +--- + +### Neutral — Can Attack (Land) + +| Action | ID | Priority | Requirement | +| ------ | --------------- | -------- | ----------- | +| Attack | `attack:ground` | high | Troops > 0 | + +--- + +### Neutral — Can Attack (Ocean) + +| Action | ID | Priority | Requirement | +| ----------- | ------------------ | -------- | -------------------------- | +| Port | `build:Port` | high | Nearby ocean shore (≤10) | +| Warship | `build:Warship` | high | Owns a Port | +| Submarine | `build:Submarine` | high | Port + `SubmarineResearch` | +| Fighter Jet | `build:FighterJet` | normal | Airfield + `JetEngines` | + +--- + +### Neutral — Boat Attack + +| Action | ID | Priority | Requirement | +| ------------- | -------------- | -------- | ----------- | +| Naval Assault | `attack:naval` | high | Troops > 0 | + +--- + +## Grid Layout + +- Bottom-anchored sheet, max 60vh, auto-fill grid with 65px min column width +- High-priority items sorted first +- Top row items auto-expand to fill incomplete rows (percentage-based column spans) +- Disabled tiles: greyed + reason text +- Locked tiles: hidden (research-locked structures not shown until prerequisite met) +- Nukes: only shown when silo + research + gold requirements are all met +- Diplomacy: exactly one of peace/alliance/break-alliance shown based on current relationship; declare-war shown when not at war +- 300ms debounce prevents accidental backdrop closure +- Haptic: `TAP` on valid action, `ERROR` on disabled/locked + +--- + +## Stack Mode + +When stack mode is toggled ON: + +- Action grid collapses to a single full-width "Stack ON" toggle button +- Map taps upgrade the nearest stackable structure within hit radius (28px screen, 72px sticky) +- Stackable types: City, Port, Airfield, Hospital, Academy, ResearchLab, Factory, MissileSilo, SAMLauncher +- Sticky targeting remembers the last upgraded structure for easier repeated taps diff --git a/docs/mobile/MOBILE-ARCHITECTURE.md b/docs/mobile/MOBILE-ARCHITECTURE.md new file mode 100644 index 000000000..01a819696 --- /dev/null +++ b/docs/mobile/MOBILE-ARCHITECTURE.md @@ -0,0 +1,201 @@ +# Mobile UI Architecture + +> Last updated: 2026-02-22 + +Quick-reference architecture map for the mobile UI layer. All source lives under `src/client/mobile/`. + +Related docs: [Feature Matrix](MOBILE-FEATURE-MATRIX.md) · [Gestures & Haptics](MOBILE-GESTURES-HAPTICS.md) · [Action Grid Catalog](MOBILE-ACTION-GRID-CATALOG.md) · [Responsive Scaling Plan](MOBILE-RESPONSIVE-SCALING-PLAN.md) + +--- + +## Component Map + +``` +MobileUI (orchestrator, 873 lines) +├── MobileDetector — device / orientation / safe-area detection +├── MobileTopBar — fixed top status bar (population, gold, clock, settings) +├── MobileActionGrid — bottom-sheet context-aware action tiles +├── MobileUIStyles — injected global mobile CSS payload +├── MobileUIButtons — factory for tab/zoom control buttons +├── MobileUIEventSetup — centralized listener registration for UI controls +├── MobileUIStateSync — game tick → topbar/tab/trade-indicator synchronization +├── MobileUIOverlayCoordinator — overlay positioning + per-tick update orchestration +├── MobileUIInteractions — attack/diplomacy/spawn/chat/emoji/donation handlers +├── MobileUIMapStack — screen→tile conversion + stack upgrade targeting helpers +├── MobileUIStats — trade-income parsing from tick updates +├── MobileUIActionUtils — build-action parsing + bomber target utility +├── MobileUIEventBindings — small shared event-binding helper wrappers +├── MobileUnitSelection — screen-distance unit detection for tap-to-select (40px hit radius) +├── MobileViewportProfile — viewport class/orientation profiling + responsive token generation +│ +├── gestures/ +│ └── GestureDetector — touch state machine (tap, long-press, drag, pinch, edge-swipe) +│ +├── overlays/ +│ ├── MobileEconomyOverlay — full-screen economy panel (investment sliders, troop/attack ratios) +│ ├── MobileIntelSidebar — left-slide sidebar (Players leaderboard + Teams + Events tabs) +│ ├── MobilePlayerToast — slide-down player info toast (long-press trigger) +│ ├── MobileAttackBar — HUD bubbles for active attacks/boats/paratroopers/trade income +│ ├── MobileChatEmojiBar — HUD bubbles for chat + emoji messages +│ ├── MobileAllianceNotifications — alliance request + extension warning notifications +│ ├── MobileEventsDisplay — events log (embedded in Intel sidebar Events tab) +│ ├── MobileResearchSidebar — right-slide sidebar hosting MobileResearchPanel +│ ├── MobileResearchPriorityModal — research category priority picker +│ ├── MobileResearchPriorityToast — confirmation toast after priority selection +│ ├── MobileSettingsSidebar — right-slide sidebar hosting MobileSettingsPanel +│ ├── MobileTechUnlockToast — tech unlock notification toast +│ └── MobileWinModal — mobile-first game-over / victory sheet +│ +├── components/ +│ ├── MobileResearchPanel — full research tech tree with category tabs +│ └── MobileSettingsPanel — settings toggles, replay controls, exit game +│ +└── utils/ + ├── HapticFeedback — navigator.vibrate() wrapper (TAP / LONG_PRESS / ERROR / SUCCESS / WARNING) + ├── Icons — unit type → icon path mappings + └── OverlayPositioning — shared attack-bar anchoring helpers for toasts/modals +``` + +--- + +## Activation + +Mobile UI activates when `MobileDetector.isMobile()` returns `true` (screen width, UA, touch capability). `MobileUI` is instantiated in `Main.ts` and stored on `window.__MOBILE_UI__`. `ClientGameRunner` wires it to the game canvas and renderer. + +When active, `MobileUI` now computes a responsive viewport profile (`compact`/`regular`/`large` + `portrait`/`landscape`) and publishes mobile sizing tokens on `document.body` (for example ActionGrid tile/spacing/height caps). This keeps `430x932` as the no-regression reference profile while applying compact-landscape reductions to smaller viewports. + +Key lifecycle calls: + +| Method | Caller | Purpose | +| ------------------------------ | ------------------ | ------------------------------------------- | +| `setActive(true/false)` | `Main.ts` | Attach/detach components, toggle CSS class | +| `initializeGestureDetection()` | `ClientGameRunner` | Bind gesture detector to canvas | +| `setTransformHandler()` | `ClientGameRunner` | Screen↔world coordinate conversion | +| `updateGameState(game)` | `ClientGameRunner` | Propagate live `GameView` to all components | + +When active, `body.mobile-ui-enabled` hides all desktop HUD elements (radial menu, control panels, desktop top bar, etc.). + +Winner/game-over flow on mobile is handled by `MobileWinModal`; desktop `WinModal` skips processing while mobile UI is active. + +--- + +## EventBus Events (mobile → server) + +| Event | Source | +| ---------------------------------- | ------------------------------- | +| `SendSpawnIntentEvent` | Tap unowned land (spawn phase) | +| `BuildUnitIntentEvent` | Action grid build tile | +| `SendUpgradeStructureIntentEvent` | Stack mode tap | +| `SendAttackIntentEvent` | Ground attack action | +| `SendBoatAttackIntentEvent` | Naval assault action | +| `SendParatrooperAttackIntentEvent` | Paratrooper action | +| `SendBomberIntentEvent` | Bomber run action | +| `SendAllianceRequestIntentEvent` | Propose alliance action | +| `SendBreakAllianceIntentEvent` | Break alliance action | +| `SendPeaceRequestIntentEvent` | Request peace action | +| `SendDeclareWarIntentEvent` | Declare war action | +| `SendEmojiIntentEvent` | Player toast → emoji table | +| `SendDonateTroopsIntentEvent` | Player toast donate troops | +| `SendDonateGoldIntentEvent` | Player toast donate gold | +| `ToggleUpgradeModeEvent` | Stack mode toggle | +| `ZoomEvent` | Pinch gesture / zoom buttons | +| `DragEvent` | Drag gesture (map pan) | +| `CenterCameraEvent` | Center zoom button | +| `MoveWarshipIntentEvent` | Unit redirect (warship) | +| `MoveSubmarineIntentEvent` | Unit redirect (submarine) | +| `MoveFighterJetIntentEvent` | Unit redirect (fighter jet) | +| `MoveArtilleryIntentEvent` | Unit redirect (artillery) | +| `UnitSelectionEvent` | Unit select / deselect visual | +| `ArtilleryOutOfRangeEvent` | Artillery redirect out of range | + +--- + +## Interaction Flows + +### Tap → Action Grid → Intent + +1. `GestureDetector` fires `tap` → `MobileUI.handleMapTap()` converts screen→tile +2. `MobileActionGrid.showForTile()` resolves tile category and renders actions +3. User taps an action tile → `MobileUI.handleActionSelected()` routes by prefix (`build:`, `attack:`, `diplomacy:`, `spawn`, `mode:`) +4. Intent event emitted on `EventBus` → `Transport` → server + +### Sidebar Access + +| Trigger | Opens | +| ------------------------- | ----------------------------- | +| Economy tab button | MobileEconomyOverlay | +| Intel tab button | MobileIntelSidebar (left) | +| Research tab button | MobileResearchSidebar (right) | +| TopBar settings icon | MobileSettingsSidebar (right) | +| Edge swipe from left | MobileIntelSidebar | +| Edge swipe from right | MobileResearchSidebar | +| Long-press on player tile | MobilePlayerToast | +| Long-press on other tile | MobileEconomyOverlay | + +### Stack Mode + +Toggle in action grid enables upgrade-mode: subsequent taps upgrade the nearest stackable structure (City, Port, Factory, etc.) using sticky targeting. + +### Unit Selection & Redirect + +1. `GestureDetector` fires `tap` → `MobileUI.handleMapTap()` converts screen→tile +2. `MobileActionGrid.showForTile()` receives screen position + `TransformHandler`; `getUnitSelectionActions()` uses `MobileUnitSelection.findSelectableUnitsNearTap()` to find own units within 40px +3. Grid shows "Select [Unit]" tiles at the top of any category +4. User taps → `MobileUI.handleUnitSelectAction()` enters redirect mode, shows floating banner, emits `UnitSelectionEvent` +5. Next tap → `handleUnitRedirectTap()` validates target terrain + artillery range, emits `Move*IntentEvent`, clears selection +6. Cancel via banner ✕ button or upon successful redirect + +--- + +## File Inventory + +| File | Lines | +| ----------------------------------------- | ----- | +| `MobileActionGrid.ts` | 1449 | +| `MobileUI.ts` | 878 | +| `MobileTopBar.ts` | 394 | +| `MobileDetector.ts` | 120 | +| `MobileUIStyles.ts` | 322 | +| `MobileUIInteractions.ts` | 286 | +| `MobileUIMapStack.ts` | 131 | +| `MobileUIEventSetup.ts` | 113 | +| `MobileUIStateSync.ts` | 112 | +| `MobileUIButtons.ts` | 99 | +| `MobileUIStats.ts` | 32 | +| `MobileUIActionUtils.ts` | 23 | +| `MobileUIEventBindings.ts` | 19 | +| `MobileViewportProfile.ts` | 232 | +| `gestures/GestureDetector.ts` | 287 | +| `components/MobileResearchPanel.ts` | 604 | +| `components/MobileSettingsPanel.ts` | 486 | +| `overlays/MobileEconomyOverlay.ts` | 729 | +| `overlays/MobileIntelSidebar.ts` | 677 | +| `overlays/MobileAttackBar.ts` | 556 | +| `overlays/MobileEventsDisplay.ts` | 515 | +| `overlays/MobilePlayerToast.ts` | 501 | +| `overlays/MobileAllianceNotifications.ts` | 430 | +| `overlays/MobileResearchPriorityModal.ts` | 326 | +| `overlays/MobileTechUnlockToast.ts` | 246 | +| `overlays/MobileChatEmojiBar.ts` | 225 | +| `overlays/MobileResearchPriorityToast.ts` | 209 | +| `overlays/MobileResearchSidebar.ts` | 187 | +| `overlays/MobileSettingsSidebar.ts` | 184 | +| `overlays/MobileWinModal.ts` | 358 | +| `utils/HapticFeedback.ts` | 107 | +| `utils/Icons.ts` | 70 | +| `utils/OverlayPositioning.ts` | 46 | +| `MobileUIOverlayCoordinator.ts` | 48 | + +--- + +## Lobby / Pre-game UI + +The public lobby has its own responsive stylesheet separate from the in-game mobile UI layer: + +| File | Lines | Notes | +| ----------------------------------------- | ----- | --------------------------------------------------------- | +| `src/client/styles/mobile/main-lobby.css` | 2043 | Landscape + portrait responsive layout for the main lobby | + +The file is imported via `src/client/styles.css` and applies media-query-driven layout adjustments for the lobby screen (game list, map previews, join flow). It is independent of `MobileUIStyles.ts` and only active on the lobby route — no in-game components depend on it. + +Map names displayed in the lobby are resolved through `GameMapType` for correct i18n lookup (`src/client/PublicLobby.ts`). diff --git a/docs/mobile/MOBILE-FEATURE-MATRIX.md b/docs/mobile/MOBILE-FEATURE-MATRIX.md new file mode 100644 index 000000000..e769781e0 --- /dev/null +++ b/docs/mobile/MOBILE-FEATURE-MATRIX.md @@ -0,0 +1,137 @@ +# Mobile Feature Matrix + +> Last updated: 2026-02-22 + +Desktop → mobile parity overview. All source under `src/client/mobile/`. + +Related docs: [Architecture](MOBILE-ARCHITECTURE.md) · [Gestures & Haptics](MOBILE-GESTURES-HAPTICS.md) · [Action Grid Catalog](MOBILE-ACTION-GRID-CATALOG.md) · [QA Matrix](MOBILE-GESTURE-HAPTIC-QA-MATRIX.md) + +--- + +## Responsive Scaling Implementation Status + +Implemented responsive behavior (ActionGrid-first scope): + +- **Baseline lock:** iPhone 14 Pro Max emulation `430x932` remains no-regression reference (portrait + landscape). +- **Compact landscape phones:** ActionGrid uses tighter token set (tile/icon/text sizing + max-height reduction) to preserve map visibility. +- **Regular landscape phones:** ActionGrid applies medium compaction to avoid oversized grid coverage. +- **Large tablet profiles:** ActionGrid receives a tablet readability bump (slightly larger tiles/icons/text vs phone compact profiles). +- **Tablet activation:** iPad Air/Pro class devices now enter mobile UI path reliably (including iPadOS desktop-style UA cases). + +Primary implementation files: + +- `src/client/mobile/MobileViewportProfile.ts` +- `src/client/mobile/MobileUI.ts` +- `src/client/mobile/MobileActionGrid.ts` +- `src/client/mobile/MobileDetector.ts` + +--- + +## Desktop → Mobile Feature Parity + +Note: `MobileUI` now acts primarily as an orchestrator; most action routing, event setup, and game-tick sync policies are delegated to helper modules (`MobileUIInteractions`, `MobileUIEventSetup`, `MobileUIStateSync`, etc.). + +| Desktop Feature | Mobile Equivalent | Trigger | Status | +| --------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------- | ------------- | +| Radial/Context Menu | `MobileActionGrid` (bottom sheet) | Tap any tile | ✅ Working | +| Responsive ActionGrid Scale | Token-driven profile scaling | Auto from viewport class + orientation | ✅ Working | +| Ground Attack | ActionGrid `attack:ground` | Tap enemy tile → "Ground Attack" | ✅ Working | +| Boat / Naval Assault | ActionGrid `attack:naval` | Tap enemy tile → "Naval Assault" | ✅ Working | +| Paratroopers | ActionGrid `attack:airstrike` | Tap enemy tile → "Paratroopers" | ✅ Working | +| Bomber Run | ActionGrid `attack:bomber` | Tap enemy tile → "Bomber Run" | ✅ Working | +| Alliance Request | ActionGrid `diplomacy:propose-ally` | Tap enemy tile → "Propose Alliance" | ✅ Working | +| Break Alliance | ActionGrid `diplomacy:break-alliance` | Tap allied tile → "Break Alliance" | ✅ Working | +| Request Peace | ActionGrid `diplomacy:request-peace` | Tap enemy-at-war tile → "Request Peace" | ✅ Working | +| Declare War | ActionGrid `attack:declare-war` | Tap enemy tile → "Declare War" | ✅ Working | +| Spawn | Direct tap (spawn phase) | Tap unclaimed land | ✅ Working | +| Build Structures | ActionGrid `build:*` tiles | Tap own tile → grid shows buildable structures | ✅ Working | +| Build Nukes | ActionGrid `attack:nuke-*` | Tap enemy tile (requires silo + research) | ✅ Working | +| Build Naval Units | ActionGrid `build:Warship`, `build:Submarine` | Tap own water tile (requires Port) | ✅ Working | +| Build Fighter Jet | ActionGrid `build:FighterJet` | Tap own tile (requires Airfield + Jet Engines) | ✅ Working | +| Build Artillery | ActionGrid `build:Artillery` | Tap own tile (requires Factory + research) | ✅ Working | +| Stack/Upgrade Structures | ActionGrid stack mode toggle | Toggle in grid, tap structures to upgrade | ✅ Working | +| Troop/Worker Ratio | `MobileEconomyOverlay` slider | Economy overlay or long-press map | ✅ Working | +| Attack Ratio | `MobileEconomyOverlay` slider | Economy overlay | ✅ Working | +| Investment Sliders | `MobileEconomyOverlay` | Economy overlay (production/road/research) | ✅ Working | +| Population & Gold | `MobileTopBar` | Always visible at top | ✅ Working | +| Game Clock | `MobileTopBar` | Always visible (counts after spawn phase) | ✅ Working | +| Leaderboard | `MobileIntelSidebar` (Players tab) | Intel tab button or edge swipe left | ✅ Working | +| Team Leaderboard | `MobileIntelSidebar` (Teams tab) | Intel sidebar → Teams tab | ✅ Working | +| Player Info | `MobilePlayerToast` | Long-press any player-owned tile | ✅ Working | +| Events Log | `MobileEventsDisplay` (in Intel sidebar) | Intel sidebar → Events tab | ✅ Working | +| Chat | Opens desktop `chat-modal` | Player toast → chat button | ✅ Working | +| Emoji | Opens desktop `emoji-table` | Player toast → emoji button or ActionGrid | ✅ Working | +| Donate Troops | Player toast → donate troops | Long-press player tile → donate button | ✅ Working | +| Donate Gold | Player toast → donate gold | Long-press player tile → donate button | ✅ Working | +| Trade Income Indicator | `MobileAttackBar` (trade bubble) | Auto-shown on trade income ticks | ✅ Working | +| Attack Notifications | `MobileAttackBar` (attack bubbles) | Auto-shown on active attacks | ✅ Working | +| Chat/Emoji Bubbles | `MobileChatEmojiBar` | Auto-shown on incoming chat/emoji | ✅ Working | +| Alliance Notifications | `MobileAllianceNotifications` | Auto-shown on alliance requests/warnings | ✅ Working | +| Tech Unlock Notification | `MobileTechUnlockToast` | Auto-shown on tech unlock | ✅ Working | +| Research Toggle | `MobileResearchSidebar` | Research tab button or edge swipe right | ✅ Working | +| Research Priority Selection | `MobileResearchPriorityModal` | Research panel interaction | ✅ Working | +| Options / Settings | `MobileSettingsSidebar` | TopBar settings icon | ✅ Working | +| Zoom In/Out | Zoom +/- buttons (left side) · step: `MOBILE_BUTTON_ZOOM_DELTA = 200` | Tap zoom buttons | ✅ Working | +| Center Camera | Center button (left side) | Tap center button | ✅ Working | +| Pan / Zoom (touch) | `GestureDetector` drag + pinch · sensitivity: `MOBILE_PINCH_ZOOM_MULTIPLIER = 50` | Touch drag / pinch | ✅ Working | +| Replay Panel | `MobileSettingsPanel` (replay controls) | Settings sidebar | ✅ Working | +| Win / Game Over Modal | `MobileWinModal` | Auto-shown on death/win updates | ✅ Working | +| Unit Selection & Redirect | ActionGrid `unit:select:*` + banner redirect | Tap near own unit → select → tap destination | ✅ Working | +| Alternate View (Space) | — | — | ❌ Not ported | +| Multi-Build Mode | — | — | ❌ Not ported | + +--- + +## HUD Layout (top → bottom) + +| Layer | Z-Index | Component | +| --------------- | ------- | ---------------------------------------- | +| Top bar | 1650 | `MobileTopBar` | +| Attack bar | 1760 | `MobileAttackBar` | +| Chat/emoji bar | 1758 | `MobileChatEmojiBar` | +| Alliance notes | 1500 | `MobileAllianceNotifications` | +| Tab buttons | 1700 | Economy / Intel / Research tabs | +| Zoom buttons | 1700 | +/−/center (left side) | +| Action grid | 2000 | `MobileActionGrid` (bottom) | +| Player toast | 2500 | `MobilePlayerToast` | +| Sidebars | 3000 | Intel / Research / Settings | +| Economy overlay | 1800 | `MobileEconomyOverlay` | +| Tech toasts | 4050+ | `MobileTechUnlockToast` / priority modal | + +--- + +## Tab Buttons (right side) + +Three fixed buttons appear during gameplay (hidden during spawn phase): + +| Button | Position | Color | Opens | +| -------- | -------- | ------ | ----------------------- | +| Economy | Top | Gold | `MobileEconomyOverlay` | +| Research | Middle | Purple | `MobileResearchSidebar` | +| Intel | Bottom | Blue | `MobileIntelSidebar` | + +--- + +## Runtime Visibility Rules + +`MobileUI` applies a runtime visibility mode each frame based on game/loading state. + +| Runtime State | Behavior | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Loading modal visible (`game-starting-modal`) | Suppress all in-game mobile UI (top bar, action grid, tabs, sidebars/overlays, zoom buttons). Map gestures remain active. | +| Spectator (`game.myPlayer() === null`) | Suppress all in-game mobile UI while spectating. Map gestures remain active. | +| Alive player (normal gameplay) | Full mobile UI available, including action grid and overlays. | +| Loss while still alive/continuing | Keep full mobile UI available; `MobileWinModal` may appear without suppressing controls. | +| Dead player (`!isAlive`, post-spawn) | Keep top bar/tabs/zoom/sidebars available; action grid is disabled (never shown). | + +--- + +## Mobile Lobby UI + +The main lobby has a dedicated responsive stylesheet (`src/client/styles/mobile/main-lobby.css`, 2043 lines) providing landscape and portrait layouts independent of the in-game mobile UI layer. + +| Feature | Implementation | Status | +| ---------------------- | ---------------------------------------------- | ---------- | +| Portrait lobby layout | `main-lobby.css` portrait media query block | ✅ Working | +| Landscape lobby layout | `main-lobby.css` landscape media query block | ✅ Working | +| Map name i18n in lobby | Resolved via `GameMapType` in `PublicLobby.ts` | ✅ Fixed | diff --git a/docs/mobile/MOBILE-GESTURE-HAPTIC-QA-MATRIX.md b/docs/mobile/MOBILE-GESTURE-HAPTIC-QA-MATRIX.md new file mode 100644 index 000000000..0b1a1cbc9 --- /dev/null +++ b/docs/mobile/MOBILE-GESTURE-HAPTIC-QA-MATRIX.md @@ -0,0 +1,96 @@ +# Mobile Gesture + Haptic QA Matrix + +> Last updated: 2026-02-15 + +Manual verification matrix for mobile touch interactions and haptic feedback parity. + +Related docs: [Gestures & Haptics](MOBILE-GESTURES-HAPTICS.md) · [Architecture](MOBILE-ARCHITECTURE.md) · [Feature Matrix](MOBILE-FEATURE-MATRIX.md) + +--- + +## Test Devices + +| Device | OS Version | Browser | Tester | Date | Result | +| ------------- | ---------- | ------- | ------ | ---- | ---------- | +| Android phone | | Chrome | | | ⏳ Pending | +| iPhone | | Safari | | | ⏳ Pending | +| iPad mini | | Safari | | | ⏳ Pending | +| iPad Air/Pro | | Safari | | | ⏳ Pending | + +--- + +## Responsive Viewport Matrix + +| Profile | Expected Behavior | Android | iOS | Notes | +| ---------------------------- | ---------------------------------------------------------------------------------- | ------- | --- | ----- | +| Baseline `430x932` portrait | No regression from prior accepted sizing | ⏳ | ⏳ | | +| Baseline `430x932` landscape | No regression from prior accepted sizing | ⏳ | ⏳ | | +| Compact phone landscape | ActionGrid appears in compact mode; map remains comfortably usable | ⏳ | ⏳ | | +| Regular phone landscape | ActionGrid medium compaction; no oversized coverage | ⏳ | ⏳ | | +| iPad mini | Mobile UI path active; ActionGrid readable and not tiny | ⏳ | ⏳ | | +| iPad Air/Pro | Mobile UI path active (not desktop fallback); ActionGrid readable with tablet bump | ⏳ | ⏳ | | + +--- + +## Gesture Matrix + +| Area | Gesture | Expected Behavior | Android | iOS | Notes | +| ---------- | --------------------- | ----------------------------------------------------------------------- | ------- | --- | ----- | +| Map canvas | Tap | Select tile / open action grid / spawn in spawn-phase | ⏳ | ⏳ | | +| Map canvas | Long press | Player tile opens `MobilePlayerToast`; other tiles open economy overlay | ⏳ | ⏳ | | +| Map canvas | Drag | Pans map smoothly with no stuck touch state | ⏳ | ⏳ | | +| Map canvas | Pinch | Zooms in/out around gesture center | ⏳ | ⏳ | | +| Edge left | Swipe from left edge | Toggles Intel sidebar | ⏳ | ⏳ | | +| Edge right | Swipe from right edge | Toggles Research sidebar | ⏳ | ⏳ | | + +--- + +## Haptic Matrix + +| Area | Interaction | Expected Haptic | Android | iOS | Notes | +| ------------------------- | --------------------------------------- | ------------------------------- | ------- | --- | ----- | +| Gesture detector | Tap | light (`custom(10)`) | ⏳ | ⏳ | | +| Gesture detector | Long press | medium (`custom(50)`) | ⏳ | ⏳ | | +| Gesture detector | Edge swipe | light (`custom(25)`) | ⏳ | ⏳ | | +| Action grid | Enabled action tile | `TAP` | ⏳ | ⏳ | | +| Action grid | Disabled tile | `ERROR` | ⏳ | ⏳ | | +| Stack mode | Toggle on/off | `TAP` | ⏳ | ⏳ | | +| Stack mode | Missed upgrade target | `ERROR` | ⏳ | ⏳ | | +| Attack bar | Focus incoming attack bubble | `TAP` | ⏳ | ⏳ | | +| Attack bar | Cancel outgoing/boat/paratrooper bubble | `TAP` | ⏳ | ⏳ | | +| Chat/emoji bar | Tap bubble to focus sender | `TAP` | ⏳ | ⏳ | | +| Alliance notifications | Accept / Renew | `SUCCESS` | ⏳ | ⏳ | | +| Alliance notifications | Reject / Dismiss | `TAP` | ⏳ | ⏳ | | +| Player toast | Chat/emoji open | `TAP` | ⏳ | ⏳ | | +| Player toast | Donate confirm actions | `SUCCESS` | ⏳ | ⏳ | | +| Win modal (mobile layout) | Keep / Save / Copy / Download / Discord | `TAP` or `SUCCESS` on exit/copy | ⏳ | ⏳ | | + +--- + +## Win Modal Mobile Verification + +| Check | Expected | Android | iOS | Notes | +| ----------- | ---------------------------------------------------------------------- | ------- | --- | ----- | +| Layout mode | Modal uses mobile layout when `body.mobile-ui-enabled` is active | ⏳ | ⏳ | | +| Placement | Anchored near bottom with safe-area padding, not centered desktop card | ⏳ | ⏳ | | +| Buttons | 44px+ touch targets, stacked vertically | ⏳ | ⏳ | | +| Actions | Exit/Keep/Replay/Discord actions are all tappable and stable | ⏳ | ⏳ | | + +--- + +## Regression Spot Checks + +| Area | Check | Android | iOS | Notes | +| ------------- | -------------------------------------------------------------------------------- | ------- | --- | ----- | +| Top overlays | Attack bar + chat/emoji bar do not overlap incorrectly after orientation changes | ⏳ | ⏳ | | +| Sidebars | Intel/Research/Settings open and close with no scroll lock leaks | ⏳ | ⏳ | | +| Performance | No obvious frame hitching during rapid gesture input | ⏳ | ⏳ | | +| Accessibility | Tap targets are reachable and not clipped by safe areas | ⏳ | ⏳ | | + +--- + +## Pass Criteria + +- All gesture checks pass on Android Chrome and iOS Safari. +- Haptic checks pass wherever browser/hardware supports vibration APIs. +- Any non-parity behavior is documented with repro steps and severity. diff --git a/docs/mobile/MOBILE-GESTURES-HAPTICS.md b/docs/mobile/MOBILE-GESTURES-HAPTICS.md new file mode 100644 index 000000000..f0089d801 --- /dev/null +++ b/docs/mobile/MOBILE-GESTURES-HAPTICS.md @@ -0,0 +1,93 @@ +# Mobile Gestures & Haptics + +> Last updated: 2026-02-15 + +Current gesture detection and haptic feedback behavior, sourced from code. + +Related docs: [Architecture](MOBILE-ARCHITECTURE.md) · [Feature Matrix](MOBILE-FEATURE-MATRIX.md) · [Action Grid Catalog](MOBILE-ACTION-GRID-CATALOG.md) · [QA Matrix](MOBILE-GESTURE-HAPTIC-QA-MATRIX.md) + +--- + +## Responsive Note (Scaling Implementation) + +- Gesture and haptic semantics are unchanged by the responsive scaling rollout. +- ActionGrid visual size/density now adapts by viewport profile (compact/regular/large + portrait/landscape). +- Profile-to-token mapping is defined in `src/client/mobile/MobileViewportProfile.ts` and applied at runtime by `MobileUI`. +- Tablet support in the mobile path now includes iPad desktop-style user-agent detection in `MobileDetector`. + +--- + +## Gesture Detection + +Source: `src/client/mobile/gestures/GestureDetector.ts` (287 lines) + +| Gesture | Detection | Fires | Action in MobileUI | +| ---------------- | ----------------------------------------------------- | ------------------ | ----------------------------------------------------- | +| Tap | Single touch, `<200ms`, movement `<10px` | `tap` | Tile selection → ActionGrid or spawn | +| Long press | Hold `600ms`, movement `<10px` (cancelled on drag) | `long-press` | Player toast (player tile) or economy overlay (other) | +| Drag | Movement `>10px`, incremental delta | `drag(dx, dy)` | Map pan via `DragEvent` | +| Pinch | 2+ fingers, scale from initial distance | `pinch(scale)` | Map zoom via `ZoomEvent` | +| Edge swipe left | Start `<20px` from left edge, `>50px` right, `<150ms` | `edge-swipe-left` | Toggle Intel sidebar | +| Edge swipe right | Start `<20px` from right edge, `>50px` left, `<150ms` | `edge-swipe-right` | Toggle Research sidebar | + +### Gesture Configuration + +| Constant | Value | +| ------------------------- | -------- | +| `LONG_PRESS_DURATION` | 600ms | +| `MOVEMENT_THRESHOLD` | 10px | +| `EDGE_THRESHOLD` | 20px | +| `PALM_RADIUS_THRESHOLD` | 30px | +| `EDGE_SWIPE_MIN_VELOCITY` | 150 px/s | + +Palm rejection filters touches with `radiusX` or `radiusY` > 30px (iOS contact-area data). + +--- + +## Haptic Feedback + +Source: `src/client/mobile/utils/HapticFeedback.ts` (107 lines) + +Centralized `navigator.vibrate()` wrapper with enable/disable toggle. + +### Patterns + +| Pattern | Duration | Semantic | +| ------------ | -------- | ------------------------------------ | +| `TAP` | 10ms | Button taps, toggles, menu opens | +| `LONG_PRESS` | 50ms | Long-press trigger confirmation | +| `SUCCESS` | 15ms | Build/attack/diplomacy confirmations | +| `WARNING` | 30ms | Warnings, confirmations | +| `ERROR` | 100ms | Invalid actions, locked tiles | + +Also supports `custom(duration)` and `pattern(number[])` for special cases. + +### Current Haptic Usage by Area + +| Area | Trigger | Pattern | +| ---------------------------- | ------------------------------- | --------------- | +| GestureDetector | Tap | custom(10) | +| GestureDetector | Long press | custom(50) | +| GestureDetector | Edge swipe | custom(25) | +| ActionGrid | Valid tile tap | `TAP` | +| ActionGrid | Disabled/locked tile tap | `ERROR` | +| ActionGrid | Backdrop close | `TAP` | +| MobileUI build actions | Successful build | `SUCCESS` | +| MobileUI attack actions | Successful attack intent | `SUCCESS` | +| MobileUI diplomacy actions | Alliance/peace/war confirmation | `SUCCESS` | +| MobileUI diplomacy actions | Chat/emoji/view-player opens | `TAP` | +| MobileUI stack mode | Stack toggle / miss | `TAP`/`ERROR` | +| MobileUI map tap | Stack upgrade success | `SUCCESS` | +| TopBar | Settings/stats tap | `TAP` | +| EconomyOverlay | Lock toggles | `TAP` | +| Intel/Settings/Research bars | Open/tab/player-select | `TAP` | +| Settings panel | Save replay | `SUCCESS` | +| Player toast | Confirm actions | `SUCCESS` | +| Player toast | Dismiss/light actions | `TAP` | +| Alliance notifications | Accept/reject | `SUCCESS`/`TAP` | +| Chat/emoji bubble bar | Focus sender bubble | `TAP` | +| Attack bar | Focus incoming attack bubble | `TAP` | +| Attack bar | Cancel outgoing attack bubble | `TAP` | +| Mobile win modal | Keep/save/download/discord | `TAP` | +| Mobile win modal | Exit / copy replay | `SUCCESS` | +| Lobby flows | No haptic | — | diff --git a/jest.config.ts b/jest.config.ts index 5628b26fe..43a473ff7 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,10 +13,12 @@ export default { "^bad-words$": "/tests/__mocks__/bad-words.ts", }, transform: { - "^.+\\.tsx?$": ["@swc/jest"], + "^.+\\.[tj]sx?$": ["@swc/jest"], }, - transformIgnorePatterns: ["node_modules/(?!(node:)/)"], + transformIgnorePatterns: [ + "node_modules/(?!(node:|lit|lit-html|lit-element|@lit)/)", + ], collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"], coverageThreshold: { global: { diff --git a/resources/images/mobile-bg-grey.png b/resources/images/mobile-bg-grey.png new file mode 100644 index 000000000..6b8d992d6 Binary files /dev/null and b/resources/images/mobile-bg-grey.png differ diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index fbd598873..61ac359b5 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -54,6 +54,8 @@ import { import { createCanvas } from "./Utils"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; import { WinModal } from "./graphics/layers/WinModal"; +import { MobileDetector } from "./mobile/MobileDetector"; +import type { MobileUI } from "./mobile/MobileUI"; import { AVAILABLE_STATS, computeStatValue } from "./stats/StatDefinitions"; import statsStore from "./stats/StatsStore"; import { PerformanceMetrics } from "./utilities/PerformanceMetrics"; @@ -173,6 +175,17 @@ export async function createClientGame( const canvas = createCanvas(); const gameRenderer = createRenderer(canvas, gameView, eventBus); + if (MobileDetector.isMobile()) { + const mobileUI = (window as Window & { __MOBILE_UI__?: MobileUI }) + .__MOBILE_UI__; + if (mobileUI) { + mobileUI.setActive(true); + mobileUI.setTransformHandler(gameRenderer.transformHandler); + mobileUI.initializeGestureDetection(canvas); + mobileUI.updateGameState(gameView); + } + } + console.log( `creating private game got difficulty: ${lobbyConfig.gameStartInfo.config.difficulty}`, ); @@ -275,6 +288,13 @@ export class ClientGameRunner { if (winModal) { winModal.setGameRecord(record); } + + const mobileWinModal = document.querySelector("mobile-win-modal") as + | (HTMLElement & { + setGameRecord?: (gameRecord: GameRecord) => void; + }) + | null; + mobileWinModal?.setGameRecord?.(record); } private handleSaveReplayRequest() { @@ -307,6 +327,22 @@ export class ClientGameRunner { undefined, // No winner yet ); + const isMobileUiActive = + typeof document !== "undefined" && + document.body.classList.contains("mobile-ui-enabled"); + + if (isMobileUiActive) { + const mobileWinModal = document.querySelector("mobile-win-modal") as + | (HTMLElement & { + showSaveReplay?: (gameRecord: GameRecord) => void; + }) + | null; + if (mobileWinModal?.showSaveReplay) { + mobileWinModal.showSaveReplay(record); + return; + } + } + const winModal = document.querySelector("win-modal") as WinModal; if (winModal) { winModal.showSaveReplay(record); diff --git a/src/client/GameStartingModal.ts b/src/client/GameStartingModal.ts index 2852c003d..315160d58 100644 --- a/src/client/GameStartingModal.ts +++ b/src/client/GameStartingModal.ts @@ -2,6 +2,9 @@ import { LitElement, css, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { translateText } from "./Utils"; +export const GAME_LOADING_VISIBILITY_CHANGE_EVENT = + "game-loading-visibility-change"; + @customElement("game-starting-modal") export class GameStartingModal extends LitElement { @state() @@ -23,6 +26,8 @@ export class GameStartingModal extends LitElement { color: var(--ui-text-default); width: 300px; text-align: center; + overflow: hidden; + position: fixed; transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; @@ -57,6 +62,92 @@ export class GameStartingModal extends LitElement { border-radius: 5px; } + @media (max-width: 1024px) and (pointer: coarse) { + .modal { + width: min(88vw, 420px); + padding: 16px 14px 14px; + border-radius: 8px 12px 9px 7px; + border: 1px solid rgba(122, 134, 151, 0.68); + background: + linear-gradient( + 124deg, + rgba(207, 216, 229, 0.12) 0 16%, + rgba(0, 0, 0, 0) 41% + ), + linear-gradient( + 305deg, + rgba(88, 98, 112, 0.14) 0 20%, + rgba(0, 0, 0, 0) 44% + ), + linear-gradient( + 180deg, + rgba(84, 95, 111, 0.988), + rgba(61, 71, 85, 0.994) 48%, + rgba(45, 53, 64, 0.996) + ); + box-shadow: + 0 0 0 1px rgba(210, 149, 109, 0.45), + 0 12px 28px rgba(0, 0, 0, 0.42), + inset 0 1px 0 rgba(226, 235, 246, 0.18), + inset 0 -12px 18px rgba(0, 0, 0, 0.2); + } + + .modal::before { + content: ""; + position: absolute; + inset: 8px; + border-radius: 6px 9px 7px 5px; + border: 1px solid rgba(97, 109, 124, 0.56); + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.24); + pointer-events: none; + } + + .modal::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: + radial-gradient( + circle at 10px 10px, + rgba(184, 194, 206, 0.72) 0 1px, + rgba(30, 36, 44, 0.9) 1.2px 3.5px, + rgba(0, 0, 0, 0) 3.8px + ), + radial-gradient( + circle at calc(100% - 11px) calc(100% - 10px), + rgba(152, 163, 178, 0.44) 0 0.8px, + rgba(22, 27, 34, 0.82) 1px 3px, + rgba(0, 0, 0, 0) 3.4px + ); + } + + .modal h2 { + position: relative; + z-index: 1; + margin-bottom: 12px; + font-size: 20px; + color: rgba(236, 242, 252, 0.98); + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); + } + + .modal p { + position: relative; + z-index: 1; + margin-bottom: 0; + padding: 10px 12px; + border-radius: 6px 8px 7px 5px; + border: 1px solid rgba(116, 129, 147, 0.54); + background: linear-gradient( + 180deg, + rgba(68, 79, 94, 0.96), + rgba(48, 57, 69, 0.98) + ); + color: rgba(220, 229, 242, 0.95); + } + } + .button-container { display: flex; justify-content: center; @@ -97,11 +188,27 @@ export class GameStartingModal extends LitElement { show() { this.isVisible = true; + window.dispatchEvent( + new CustomEvent<{ visible: boolean }>( + GAME_LOADING_VISIBILITY_CHANGE_EVENT, + { + detail: { visible: true }, + }, + ), + ); this.requestUpdate(); } hide() { this.isVisible = false; + window.dispatchEvent( + new CustomEvent<{ visible: boolean }>( + GAME_LOADING_VISIBILITY_CHANGE_EVENT, + { + detail: { visible: false }, + }, + ), + ); this.requestUpdate(); } } diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index b5c3a0a47..c704e5a6e 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -35,6 +35,11 @@ import { DifficultyDescription } from "./components/Difficulties"; import "./components/LobbyChatPanel"; import "./components/Maps"; import type { JoinLobbyEvent } from "./Main"; +import { MobileDetector } from "./mobile/MobileDetector"; +import { + getMobileViewportProfile, + type MobileViewportProfile, +} from "./mobile/MobileViewportProfile"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; import { formatStartingGold, translateText } from "./Utils"; @@ -81,15 +86,22 @@ export class HostLobbyModal extends LitElement { @state() private showUnitSettings = false; // Closed by default for Host @state() private chatEnabled: boolean = false; @state() private lobbyMessages: LobbyMessage[] = []; + @state() private activeMapCategory: string = "continental"; + @state() private viewportProfile: MobileViewportProfile | null = null; private playersInterval: NodeJS.Timeout | null = null; private botsUpdateTimer: number | null = null; private userSettings: UserSettings = new UserSettings(); private theme = new PastelTheme(); + private resizeHandler = (): void => { + this.updateViewportProfile(); + }; render() { // Calculate percentage for the CSS variable const sliderPercent = (this.bots / 400) * 100; + const useMobileMapCarousel = + typeof window !== "undefined" && MobileDetector.isMobile(); return html` + ` + : null}