Skip to content

Commit 41b13ab

Browse files
authored
Merge pull request #59 from sam-mfb/mobile-scaling
Mobile scaling and fullscreen support
2 parents 0a37f59 + 5bb1d03 commit 41b13ab

21 files changed

+1594
-633
lines changed

MOBILE_PLAN.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# Mobile Adaptation Plan for Continuum
2+
3+
Based on analysis of the codebase, here's a comprehensive plan for adapting the game to mobile browsers:
4+
5+
## **A. Eliminate Hardcoded Scale-Sensitive Values (PHASE 0)****COMPLETED**
6+
7+
**Status**: All components have been refactored to use dynamic scale-based values.
8+
9+
**Completed work:**
10+
11+
1.**Created `src/game/constants/dimensions.ts`**:
12+
13+
- Exports `BASE_GAME_WIDTH`, `BASE_GAME_HEIGHT`, `BASE_CONTROLS_HEIGHT`, `BASE_TOTAL_HEIGHT`
14+
- Exports `DEFAULT_SCALE = 2` (currently used throughout)
15+
- Exports `getScaledDimensions(scale)` helper function
16+
17+
2.**Refactored all screen components** to accept and use `scale` prop:
18+
19+
- **App.tsx**: Uses `getScaledDimensions(scale)` and passes scale to all child components
20+
- **StartScreen.tsx**: All positions, dimensions, and font sizes multiply by scale
21+
- **GameOverScreen.tsx**: Container dimensions and all UI elements scaled dynamically
22+
- **HighScoreEntry.tsx**: Container dimensions and all UI elements scaled dynamically
23+
- **SettingsModal.tsx**: All dimensions, sprite icons, and nested components scaled
24+
- **VolumeControls.tsx**: All UI elements scaled (16px base at 1x)
25+
- **VolumeButton.tsx**: Icon, slider, and text scaled (16px icon base at 1x)
26+
- **Map.tsx**: Already correctly implemented ✓
27+
28+
3.**Current state**:
29+
- All components work correctly at any scale value (tested at 1x, 2x, 3x)
30+
- Default scale is 2x (set in `dimensions.ts`)
31+
- Sprites render with crisp pixels using canvas fillRect pattern
32+
- Ready for Phase 1: making scale dynamic based on viewport
33+
34+
---
35+
36+
## **B. Responsive Canvas Scaling (PHASE 1)****COMPLETED**
37+
38+
**Status**: Responsive scaling with user control implemented and working.
39+
40+
**Completed work:**
41+
42+
1.**Created `useResponsiveScale` hook** (`src/game/hooks/useResponsiveScale.ts`):
43+
44+
- Calculates optimal integer scale factor based on viewport dimensions
45+
- Supports both auto-responsive mode and fixed scale modes (1x, 2x, 3x)
46+
- Listens to window resize and orientation change events with 500ms debounce
47+
- Returns `{ scale, dimensions: { gameWidth, gameHeight, controlsHeight, totalHeight } }`
48+
- Immediately recalculates when switching from fixed to auto mode
49+
50+
2.**Updated App.tsx to use responsive scale**:
51+
52+
- Uses `useResponsiveScale(scaleMode)` hook with scale mode from Redux state
53+
- Passes scale to all child components (GameRenderer, StartScreen, SettingsModal, etc.)
54+
- Container dimensions come from hook's `dimensions` object
55+
56+
3.**Updated `index.html`**:
57+
58+
- Added mobile-optimized viewport meta tags:
59+
- `width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover`
60+
- `mobile-web-app-capable`, `apple-mobile-web-app-capable`
61+
- `apple-mobile-web-app-status-bar-style: black-fullscreen`
62+
63+
4.**Added Scale Mode setting**:
64+
65+
- New `ScaleMode` type: `'auto' | 1 | 2 | 3`
66+
- Added to `appSlice.ts` with default value 'auto'
67+
- Persisted to localStorage via `appMiddleware.ts`
68+
- Loaded from localStorage in `store.ts`
69+
- Custom dropdown in SettingsModal (replaced native `<select>` for better control over styling)
70+
71+
5.**Fixed dimensions calculation**:
72+
- Set `BASE_CONTROLS_HEIGHT = 0` (InGameControlsPanel is absolutely positioned)
73+
- Updated `BASE_TOTAL_HEIGHT = 342` (just game height, no controls height)
74+
- Allows 2x scale at smaller viewport sizes
75+
76+
---
77+
78+
## **C. Fullscreen Support****COMPLETED**
79+
80+
**Status**: Fullscreen support with toggle button and auto-hide controls implemented.
81+
82+
**Completed work:**
83+
84+
1.**Added fullscreen state to `appSlice.ts`**:
85+
86+
- New state field: `isFullscreen: boolean`
87+
- New action: `setFullscreen(value: boolean)`
88+
89+
2.**Created fullscreen utility** (`src/game/mobile/fullscreen.ts`):
90+
91+
- `enterFullscreen()` function with vendor prefix support
92+
- `exitFullscreen()` function with vendor prefix support
93+
- `toggleFullscreen()` helper that checks current state
94+
- `isFullscreen()` helper with vendor prefix checks
95+
96+
3.**Created FullscreenButton component** (`src/game/components/FullscreenButton.tsx`):
97+
98+
- Positioned at top-left corner (20px from edges)
99+
- Light gray color (rgba(192, 192, 192, 0.8)) with white hover state
100+
- Square fullscreen icon (⛶)
101+
- Auto-hides after 3 seconds of inactivity (mouse/touch)
102+
- Click-only control (no ESC key handling)
103+
- Updates Redux state after toggling
104+
105+
4.**Created inactivity detection hook** (`src/game/hooks/useInactivityDetection.ts`):
106+
107+
- Detects mouse movement and touch events
108+
- 3-second timeout before hiding controls
109+
- Used by both FullscreenButton and VolumeButton
110+
111+
5.**Updated VolumeButton** (`src/game/components/VolumeButton.tsx`):
112+
113+
- Repositioned to top-right corner
114+
- Horizontal slider extending left (direction: rtl)
115+
- Auto-hides after 3 seconds of inactivity
116+
- Left side = 100% volume
117+
118+
6.**Updated high score indicator** (`src/game/App.tsx`):
119+
120+
- Moved warning triangle (⚠) to bottom-right corner
121+
122+
7.**Responsive scaling integration**:
123+
- `useResponsiveScale` automatically recalculates when entering/exiting fullscreen
124+
- Game scales to fill entire screen in fullscreen mode
125+
- Maximum playable area on mobile devices
126+
127+
---
128+
129+
## **D. Touch Controls with nipplejs**
130+
131+
**1. Create `TouchJoystick` component** (`src/game/components/TouchJoystick.tsx`)
132+
133+
- Use nipplejs library (add to package.json)
134+
- Position in bottom-left corner with semi-transparent background
135+
- Map joystick vector to left/right/thrust controls:
136+
- Horizontal movement → left/right
137+
- Vertical upward → thrust
138+
- Return control states compatible with `ControlMatrix` type
139+
140+
**2. Create `TouchButtons` component** (`src/game/components/TouchButtons.tsx`)
141+
142+
- Two primary buttons: Fire (bottom-right) and Shield (above Fire)
143+
- Touch-optimized sizing (minimum 60px tap targets)
144+
- Visual feedback on press (opacity/scale changes)
145+
- Handle `touchstart`/`touchend` events
146+
- Return control states compatible with `ControlMatrix` type
147+
148+
**3. Create `TouchControlsOverlay` component** (`src/game/components/TouchControlsOverlay.tsx`)
149+
150+
- Wrapper that combines TouchJoystick + TouchButtons
151+
- **Positioning for ergonomic thumb access:**
152+
- `position: fixed` (relative to viewport, not canvas)
153+
- `bottom: 0`, `left: 0`, `right: 0`
154+
- `z-index: 1000` (above all game elements)
155+
- Joystick: `position: absolute; bottom: 20px; left: 20px` within overlay
156+
- Buttons: `position: absolute; bottom: 20px; right: 20px` within overlay
157+
- **Visual design:**
158+
- Semi-transparent controls (`opacity: 0.7-0.8`) to minimize obstruction
159+
- Don't block view of critical game elements
160+
- Manages combined control state from both components
161+
- Provides unified `ControlMatrix` output
162+
163+
---
164+
165+
## **E. Device Detection & Control Mode Management**
166+
167+
**1. Create device detection utility** (`src/game/mobile/deviceDetection.ts`)
168+
169+
- Export `isTouchDevice()` function (primary detection)
170+
- Check for touch capability: `'ontouchstart' in window || navigator.maxTouchPoints > 0`
171+
- **Returns true for phones AND tablets (including iPads)**
172+
- This is the key function for determining default touch control state
173+
- Optional: Export `isSmallScreen()` for UI layout decisions
174+
- Check screen width (e.g., `window.innerWidth < 768`)
175+
- Used for layout optimizations, NOT for enabling/disabling touch controls
176+
- **Default behavior**: Touch controls enabled for ANY touch-capable device, regardless of screen size
177+
178+
**2. Add touch controls state to `appSlice.ts`**
179+
180+
- New state fields:
181+
```typescript
182+
touchControlsEnabled: boolean
183+
touchControlsOverride: boolean | null // null = auto-detect
184+
```
185+
- New actions:
186+
```typescript
187+
enableTouchControls()
188+
disableTouchControls()
189+
setTouchControlsOverride(value: boolean | null)
190+
```
191+
192+
**3. Update `GameRenderer.tsx` to merge control inputs**
193+
194+
- Current keyboard controls from `getControls(keyInfo, bindings)`
195+
- Touch controls from `TouchControlsOverlay` (when enabled)
196+
- Merge both into single `ControlMatrix` (OR logic for each control)
197+
- Pass merged controls to renderer
198+
199+
**4. Initialize touch controls on app load** (`main.tsx` or `App.tsx`)
200+
201+
- Check `touchControlsOverride` value:
202+
- If `null`: use `isTouchDevice()` to auto-detect (enables for phones AND tablets)
203+
- If `true/false`: use that value
204+
- Dispatch `enableTouchControls()` or `disableTouchControls()`
205+
206+
---
207+
208+
## **F. Settings Integration**
209+
210+
**1. Update `SettingsModal.tsx`**
211+
212+
- Add "Touch Controls" section with toggle switch
213+
- Three-state control:
214+
- "Auto-detect" (default)
215+
- "Always On"
216+
- "Always Off"
217+
- Show current device detection status
218+
- Dispatches `setTouchControlsOverride()` action
219+
220+
**2. Conditional rendering in `GameRenderer.tsx`**
221+
222+
- Only render `TouchControlsOverlay` when `touchControlsEnabled === true`
223+
- Ensure touch controls don't interfere when disabled
224+
225+
---
226+
227+
## **G. Additional Mobile Optimizations**
228+
229+
**1. Update `InGameControlsPanel.tsx`**
230+
231+
- **Completely hide** (return null) when touch controls are active
232+
- Check `touchControlsEnabled` from app state
233+
- No repositioning or resizing - full removal to save screen space
234+
- Implementation:
235+
```typescript
236+
const touchControlsEnabled = useAppSelector(
237+
state => state.app.touchControlsEnabled
238+
)
239+
if (touchControlsEnabled) return null
240+
```
241+
242+
**2. Add CSS for mobile** (`src/game/styles/mobile.css`)
243+
244+
- Prevent text selection during touch interactions
245+
- Disable pull-to-refresh on game canvas
246+
- Prevent zoom on double-tap
247+
- Viewport lock styles
248+
249+
**3. Update `index.html`**
250+
251+
- Add mobile-optimized meta tags:
252+
```html
253+
<meta
254+
name="viewport"
255+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
256+
/>
257+
<meta name="mobile-web-app-capable" content="yes" />
258+
```
259+
260+
---
261+
262+
## **H. Package Dependencies**
263+
264+
Add to `package.json`:
265+
266+
```json
267+
{
268+
"dependencies": {
269+
"nipplejs": "^0.10.2"
270+
},
271+
"devDependencies": {
272+
"@types/nipplejs": "^0.0.3"
273+
}
274+
}
275+
```
276+
277+
---
278+
279+
## **Implementation Order**
280+
281+
1.**Phase 0 - Refactor Hardcoded Values**: Eliminate scale-dependent hardcoded values (A) - **COMPLETED**
282+
2.**Phase 1 - Responsive Scaling**: Dynamic scale based on viewport (B) - **COMPLETED**
283+
3.**Phase 2 - Fullscreen Support**: Maximize usable screen area on mobile (C) - **COMPLETED**
284+
4. **Phase 3 - Touch Input**: Joystick & buttons (D) - **NEXT**
285+
5. **Phase 4 - Intelligence**: Device detection & mode management (E)
286+
6. **Phase 5 - Polish**: Settings UI & mobile optimizations (F, G)
287+
288+
---
289+
290+
## **Key Files to Create/Modify**
291+
292+
**New Files:**
293+
294+
-`src/game/constants/dimensions.ts` (Phase 0) - **COMPLETED**
295+
-`src/game/hooks/useResponsiveScale.ts` (Phase 1) - **COMPLETED**
296+
-`src/game/hooks/useInactivityDetection.ts` (Phase 2) - **COMPLETED**
297+
-`src/game/mobile/fullscreen.ts` (Phase 2) - **COMPLETED**
298+
-`src/game/components/FullscreenButton.tsx` (Phase 2) - **COMPLETED**
299+
- `src/game/mobile/TouchJoystick.tsx` (Phase 3)
300+
- `src/game/mobile/TouchButtons.tsx` (Phase 3)
301+
- `src/game/mobile/TouchControlsOverlay.tsx` (Phase 3)
302+
- `src/game/mobile/deviceDetection.ts` (Phase 4)
303+
- `src/game/mobile/mobile.css` (Phase 5)
304+
305+
**Modified Files:**
306+
307+
**Phase 0:****COMPLETED**
308+
309+
-`src/game/App.tsx` - Added scale prop to all screen components
310+
-`src/game/components/StartScreen.tsx` - Accepts scale prop, uses dynamic positioning
311+
-`src/game/components/GameOverScreen.tsx` - Accepts scale prop, uses dynamic dimensions
312+
-`src/game/components/HighScoreEntry.tsx` - Accepts scale prop, uses dynamic dimensions
313+
-`src/game/components/SettingsModal.tsx` - Accepts scale prop, all elements scaled
314+
-`src/game/components/VolumeControls.tsx` - Accepts scale prop, all elements scaled
315+
-`src/game/components/VolumeButton.tsx` - Accepts scale prop, all elements scaled
316+
-`src/game/constants/dimensions.ts` - Created with base dimensions and helpers
317+
318+
**Phase 1:****COMPLETED**
319+
320+
-`src/game/App.tsx` - Uses `useResponsiveScale(scaleMode)` hook, passes scale to all components
321+
-`src/game/components/InGameControlsPanel.tsx` - Accepts and uses scale prop
322+
-`src/game/index.html` - Added mobile viewport meta tags
323+
-`src/game/appSlice.ts` - Added `scaleMode` state and `setScaleMode` action
324+
-`src/game/appMiddleware.ts` - Persists `scaleMode` to localStorage
325+
-`src/game/store.ts` - Loads `scaleMode` from localStorage on init
326+
-`src/game/components/SettingsModal.tsx` - Added Display Scale custom dropdown control
327+
-`src/game/constants/dimensions.ts` - Fixed BASE_CONTROLS_HEIGHT and BASE_TOTAL_HEIGHT
328+
329+
**Phase 2:****COMPLETED**
330+
331+
-`src/game/appSlice.ts` - Added fullscreen state and setFullscreen action
332+
-`src/game/App.tsx` - Added FullscreenButton component and repositioned high score indicator
333+
-`src/game/components/VolumeButton.tsx` - Repositioned to top-right, added inactivity detection, horizontal slider
334+
-`src/game/components/FullscreenButton.tsx` - Created with auto-hide functionality
335+
336+
**Phase 4:**
337+
338+
- `src/game/appSlice.ts` - Add touch control state
339+
340+
**Phase 5:**
341+
342+
- `src/game/components/SettingsModal.tsx` - Add touch controls toggle
343+
- `src/game/components/InGameControlsPanel.tsx` - Hide when touch controls active
344+
- `package.json` - Add nipplejs dependency
345+
346+
This approach maintains the existing keyboard control system while seamlessly adding mobile support with user override capability.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "continuum",
3-
"version": "0.1.0",
3+
"version": "1.0.2",
44
"type": "module",
55
"description": "Recreation of the 68000 Mac game Continuum for the web",
66
"scripts": {

0 commit comments

Comments
 (0)