fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / LITTLEJS_MIGRATION_STEPS.md

Branch mistral-medium-35-littlejs · default branch · all branches

LITTLEJS_MIGRATION_STEPS.md

14.8 KB · history · blame

LittleJS Migration: Step-by-Step Implementation Guide

This is a practical, actionable guide to migrate Nordic Skating to LittleJS. Follow these steps in order.


Phase 0: Preparation (1-2 hours)

Step 0.1: Install LittleJS

cd /Users/pavel/code/nordic-skating
npm install littlejsengine

Step 0.2: Back up current code

cp src/main.ts src/main.ts.backup
cp index.html index.html.backup

Step 0.3: Test POC (Optional but recommended)

cd LITTLEJS_POC
npm install
npm run dev
# Test in browser, then Ctrl+C
cd ..

Phase 1: Minimal Integration - Input + Game Loop (1-2 days)

Step 1.1: Create new entry point

Create src/main-littlejs.ts:

import * as THREE from 'three'
import { 
  engineInit, 
  engine, 
  inputGetState, 
  inputGetPressed,
  inputGetAxis,
  setThreeRenderer,
  vec2,
  colorWhite,
  drawTextScreen
} from 'littlejsengine'

// Import all existing modules
import skateIcon from '../assets/skate-shoes.png'
import soundOffIcon from '../assets/sound-off.png'
import soundOnIcon from '../assets/sound-on.png'
import tiltOffIcon from '../assets/tilt-off.png'
import tiltOnIcon from '../assets/tilt-on.png'
import { IceSound } from './audio/iceSound.js'
import { Input as OldInput } from './input.js'
import { Drifts } from './render/drifts.js'
import { Ice } from './render/ice.js'
import { Marks } from './render/marks.js'
import { Shores } from './render/shores.js'
import { LOOKS, Sky, TIME_NAMES, type TimeName } from './render/sky.js'
import { Snow } from './render/snow.js'
import { WEATHER, WEATHERS, type Weather, anyLake, loadSettings, saveSettings, timeOf, weatherOf, windOf } from './settings.js'
import { BREAKING_THICKNESS, CRACKING_THICKNESS } from './ice/strength.js'
import { PACES, PACE_NAMES, type Pace, UNDER_SECONDS, createSkater, pushSeconds, sincePush, stepSkater } from './skater.js'
import { createLake } from './world/lake.js'

// Make THREE available globally
if (typeof window !== 'undefined') {
  (window as any).THREE = THREE
}

class NordicSkating {
  // All existing fields from main.ts
  private lake: any
  private skater: any
  private scene: THREE.Scene
  private camera: THREE.PerspectiveCamera
  private renderer: THREE.WebGLRenderer
  // ... etc (copy all from main.ts)

  constructor() {
    // Get canvas
    const canvas = document.querySelector<HTMLCanvasElement>('#scene')!

    // Configure LittleJS engine
    engine.canvas = canvas
    engine.canvasSize = vec2(canvas.width, canvas.height)

    // Initialize Three.js renderer
    this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

    // Set Three.js renderer for LittleJS
    setThreeRenderer(THREE)

    // Now run existing initialization code
    this.initializeGame()

    // Initialize LittleJS
    engineInit(
      () => this.gameInit(),
      () => this.gameUpdate(),
      () => this.gameUpdatePost(),
      () => this.gameRender(),
      () => this.gameRenderPost()
    )
  }

  private initializeGame(): void {
    // Copy all initialization code from main.ts
    // But replace custom input with LittleJS input

    // ... (all existing init code)

    // REPLACE: Remove old Input initialization
    // OLD: const input = new Input(canvas)
    // NEW: Use LittleJS input system directly
  }

  gameInit(): void {
    // Initialization that needs to happen in LittleJS context
  }

  gameUpdate(): void {
    // NEW: Handle input using LittleJS
    this.handleLittleJSInput()

    // EXISTING: All update logic from main.ts animation loop
    const passed = engine.timing.lastTimeStep
    const dt = this.waiting() ? 0 : passed

    // ... rest of update logic
  }

  private handleLittleJSInput(): void {
    // Map LittleJS input to existing input system

    // Steering
    let steer = 0
    steer += inputGetState('arrowLeft') ? -1 : 0
    steer += inputGetState('arrowRight') ? 1 : 0
    steer += inputGetState('a') ? -1 : 0
    steer += inputGetState('d') ? 1 : 0
    steer += inputGetAxis('mouseX') * 0.5

    // Store for use in existing code
    this.currentSteer = steer

    // Button presses
    if (inputGetPressed('m')) {
      this.toggleMute()
    }
    if (inputGetPressed('r')) {
      this.randomize()
    }
    if (inputGetPressed('arrowUp') || inputGetPressed('w')) {
      this.stepPace(1, true)
    }
    if (inputGetPressed('arrowDown') || inputGetPressed('s')) {
      this.stepPace(-1, true)
    }

    // Gamepad
    for (const button of this.pollLittleJSPads()) {
      // ... handle gamepad buttons
    }

    // Start sound on any input
    if (Math.abs(steer) > 0 || 
        inputGetPressed('m') || 
        inputGetPressed('r') ||
        inputGetPressed('arrowUp') ||
        inputGetPressed('arrowDown')) {
      this.startSound()
    }
  }

  private pollLittleJSPads(): number[] {
    // Implement gamepad polling using LittleJS
    const pressed: number[] = []
    const down = new Set<number>()

    for (const pad of navigator.getGamepads?.() ?? []) {
      if (!pad) continue

      // Check buttons
      pad.buttons.forEach((button, n) => {
        if (button.pressed) {
          down.add(n)
          if (!this.padDown.has(n)) pressed.push(n)
        }
      })
    }

    this.padDown = down
    return pressed
  }

  gameUpdatePost(): void {
    // Post-update logic
  }

  gameRender(): void {
    // Render the Three.js scene
    this.renderer.render(this.scene, this.camera)
  }

  gameRenderPost(): void {
    // Draw HUD using LittleJS (optional)
    // Can keep existing DOM-based UI or use LittleJS
  }
}

// Initialize when ready
new NordicSkating()

Step 1.2: Update index.html

Replace the script tag:

<!-- OLD -->
<script type="module" src="/src/main.ts"></script>

<!-- NEW -->
<script type="module" src="/src/main-littlejs.ts"></script>

Step 1.3: Update vite.config.ts (if needed)

Ensure LittleJS can be resolved:

import { defineConfig } from 'vite'

export default defineConfig({
  resolve: {
    alias: {
      littlejsengine: 'littlejsengine/dist/littlejs.esm.js'
    }
  }
})

Step 1.4: Test basic functionality

npm run dev

Test checklist: - [ ] Page loads without errors - [ ] Keyboard steering works - [ ] Mouse/touch steering works - [ ] Gamepad works (if available) - [ ] Pace changes work - [ ] Random lake works - [ ] UI still functions - [ ] Audio works


Phase 2: Input System Migration (1 day)

Step 2.1: Update tilt steering

Replace custom device orientation with LittleJS:

// OLD: Custom tilt handling in Input class
// NEW: Use LittleJS device orientation

// In gameUpdate():
const tiltX = inputGetAxis('deviceOrientationGamma')
const tiltY = inputGetAxis('deviceOrientationBeta')
// Process tilt...

Step 2.2: Remove old Input class usage

Gradually replace all calls to the old Input class with LittleJS input functions.

Step 2.3: Test on mobile


Phase 3: Game Loop Migration (1 day)

Step 3.1: Remove custom animation loop

Remove the renderer.setAnimationLoop() call from main.ts and replace with LittleJS game loop.

Step 3.2: Convert timing to use delta time

Replace all clock.getDelta() with engine.timing.lastTimeStep:

// OLD
const passed = clock.getDelta()

// NEW
const passed = engine.timing.lastTimeStep

Step 3.3: Test frame rate independence


Phase 4: Camera and Rendering (2-3 days)

Step 4.1: Keep existing rendering code

The Three.js rendering code can remain largely unchanged.

Step 4.2: Integrate with LittleJS render loop

Ensure the Three.js renderer is properly integrated with LittleJS.

Step 4.3: Add LittleJS HUD (optional)

Replace DOM-based HUD with LittleJS text rendering:

gameRenderPost(): void {
  const x = 20
  let y = 40

  drawTextScreen(
    `Speed: ${Math.round(this.skater.speed * 3.6)} km/h`,
    vec2(x, y),
    24,
    colorWhite
  )
  // ... more HUD elements
}

Phase 5: Audio Integration (1-2 days)

Step 5.1: Keep existing audio system

The custom Web Audio code is excellent and should be kept.

Step 5.2: Trigger audio from game loop

// In gameUpdate():
if (this.sounding() && this.skater.under === 0) {
  const h = this.lake.thickness(this.skater.x, this.skater.s)
  const lying = this.lake.snow(this.skater.x, this.skater.s)

  if (Math.floor(this.skater.stride) !== this.strides) {
    this.sound?.strike(h, undefined, (0.75 + 0.5 * Math.random()) * (1 - 0.9 * lying))
  }

  this.sound?.glide(h, Math.exp(-sincePush(this.skater) / 0.5), undefined, lying)
}

Step 5.3: Handle audio context

Use LittleJS to manage audio context state if needed, or keep existing approach.


Phase 6: UI Migration (2-3 days)

Step 6.1: Keep DOM-based UI (easiest)

The existing dialog-based UI works well. Just trigger it from LittleJS input.

Step 6.2: Or migrate to LittleJS UI (optional)

Use LittleJS UI system for buttons and dialogs:

// Create buttons
const soundButton = new Button(
  vec2(100, 50),
  vec2(200, 60),
  'Sound',
  () => this.toggleMute()
)

Phase 7: Polish and Testing (3-5 days)

Step 7.1: Full regression testing

Test every feature against the current implementation.

Step 7.2: Performance testing

Step 7.3: User testing

Step 7.4: Bug fixing

Fix any issues discovered during testing.


Phase 8: Cleanup (1-2 days)

Step 8.1: Remove deprecated code

Step 8.2: Update documentation

Step 8.3: Final testing

One more round of full testing.


Migration Checklist

Use this checklist to track progress:

Phase 1: Setup

Phase 2: Input

Phase 3: Game Loop

Phase 4: Rendering

Phase 5: Audio

Phase 6: UI

Phase 7: Testing

Phase 8: Cleanup


Rollback Plan

If you need to rollback at any point:

# Restore original files
cp src/main.ts.backup src/main.ts
cp index.html.backup index.html

# Remove LittleJS
npm uninstall littlejsengine

# Reinstall original dependencies
npm install

Tips and Tricks

1. Debugging Input

If input isn't working:

// Add debug logging
console.log('Keyboard states:', {
  left: inputGetState('arrowLeft'),
  right: inputGetState('arrowRight'),
  a: inputGetState('a'),
  d: inputGetState('d')
})

2. Checking Delta Time

console.log('Delta time:', engine.timing.lastTimeStep)
console.log('FPS:', engine.timing.fps)

3. Device Orientation Debug

console.log('Orientation:', {
  gamma: inputGetAxis('deviceOrientationGamma'),
  beta: inputGetAxis('deviceOrientationBeta')
})

4. Canvas Issues

If canvas isn't rendering: - Check that engine.canvas is set correctly - Verify Three.js renderer is using the same canvas - Ensure setThreeRenderer(THREE) is called

5. Performance Issues


Common Pitfalls

1. Input Names Don't Match

LittleJS uses specific key names. Check the LittleJS Input Documentation.

Solution: Use the exact key names LittleJS expects: - arrowLeft, arrowRight, arrowUp, arrowDown - a, d, w, s - m, r

2. Canvas Not Found

Solution: Ensure the canvas element exists before initialization:

const canvas = document.getElementById('scene')
if (!canvas) {
  console.error('Canvas not found!')
  return
}

3. THREE Not Available

Solution: Make THREE globally available:

(window as any).THREE = THREE

4. Audio Context Issues

Solution: Audio must be triggered by user interaction. Use any input to start it:

if (anyInputDetected) {
  if (audio.state === 'suspended') {
    audio.resume()
  }
}

5. Mobile Input Not Working

Solution: - Check for touch event handling - Ensure canvas has proper touch action settings - Test on actual device (simulators may not work)


Success Criteria Checklist

Before declaring the migration complete, verify:

Functionality

Performance

Code Quality

User Experience


Final Notes

This migration is low-risk and high-reward due to:

  1. Three.js plugin means rendering code stays the same
  2. Incremental approach allows validating each step
  3. Minimal integration provides immediate benefits
  4. Well-documented API with good examples
  5. Active community for support

Estimated time for full migration: 17-26 days (part-time) or 8-13 days (full-time)

Estimated time for minimal integration: 3-5 days (part-time) or 1-2 days (full-time)

Recommendation: Start with minimal integration to gain immediate benefits, then proceed with full migration if successful.

Source
1# LittleJS Migration: Step-by-Step Implementation Guide
2
3This is a **practical, actionable guide** to migrate Nordic Skating to LittleJS. Follow these steps in order.
4
5---
6
7## Phase 0: Preparation (1-2 hours)
8
9### Step 0.1: Install LittleJS
10```bash
11cd /Users/pavel/code/nordic-skating
12npm install littlejsengine
13```
14
15### Step 0.2: Back up current code
16```bash
17cp src/main.ts src/main.ts.backup
18cp index.html index.html.backup
19```
20
21### Step 0.3: Test POC (Optional but recommended)
22```bash
23cd LITTLEJS_POC
24npm install
25npm run dev
26# Test in browser, then Ctrl+C
27cd ..
28```
29
30---
31
32## Phase 1: Minimal Integration - Input + Game Loop (1-2 days)
33
34### Step 1.1: Create new entry point
35Create `src/main-littlejs.ts`:
36
37```typescript
38import * as THREE from 'three'
39import {
40 engineInit,
41 engine,
42 inputGetState,
43 inputGetPressed,
44 inputGetAxis,
45 setThreeRenderer,
46 vec2,
47 colorWhite,
48 drawTextScreen
49} from 'littlejsengine'
50
51// Import all existing modules
52import skateIcon from '../assets/skate-shoes.png'
53import soundOffIcon from '../assets/sound-off.png'
54import soundOnIcon from '../assets/sound-on.png'
55import tiltOffIcon from '../assets/tilt-off.png'
56import tiltOnIcon from '../assets/tilt-on.png'
57import { IceSound } from './audio/iceSound.js'
58import { Input as OldInput } from './input.js'
59import { Drifts } from './render/drifts.js'
60import { Ice } from './render/ice.js'
61import { Marks } from './render/marks.js'
62import { Shores } from './render/shores.js'
63import { LOOKS, Sky, TIME_NAMES, type TimeName } from './render/sky.js'
64import { Snow } from './render/snow.js'
65import { WEATHER, WEATHERS, type Weather, anyLake, loadSettings, saveSettings, timeOf, weatherOf, windOf } from './settings.js'
66import { BREAKING_THICKNESS, CRACKING_THICKNESS } from './ice/strength.js'
67import { PACES, PACE_NAMES, type Pace, UNDER_SECONDS, createSkater, pushSeconds, sincePush, stepSkater } from './skater.js'
68import { createLake } from './world/lake.js'
69
70// Make THREE available globally
71if (typeof window !== 'undefined') {
72 (window as any).THREE = THREE
73}
74
75class NordicSkating {
76 // All existing fields from main.ts
77 private lake: any
78 private skater: any
79 private scene: THREE.Scene
80 private camera: THREE.PerspectiveCamera
81 private renderer: THREE.WebGLRenderer
82 // ... etc (copy all from main.ts)
83
84 constructor() {
85 // Get canvas
86 const canvas = document.querySelector<HTMLCanvasElement>('#scene')!
87
88 // Configure LittleJS engine
89 engine.canvas = canvas
90 engine.canvasSize = vec2(canvas.width, canvas.height)
91
92 // Initialize Three.js renderer
93 this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
94 this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
95
96 // Set Three.js renderer for LittleJS
97 setThreeRenderer(THREE)
98
99 // Now run existing initialization code
100 this.initializeGame()
101
102 // Initialize LittleJS
103 engineInit(
104 () => this.gameInit(),
105 () => this.gameUpdate(),
106 () => this.gameUpdatePost(),
107 () => this.gameRender(),
108 () => this.gameRenderPost()
109 )
110 }
111
112 private initializeGame(): void {
113 // Copy all initialization code from main.ts
114 // But replace custom input with LittleJS input
115
116 // ... (all existing init code)
117
118 // REPLACE: Remove old Input initialization
119 // OLD: const input = new Input(canvas)
120 // NEW: Use LittleJS input system directly
121 }
122
123 gameInit(): void {
124 // Initialization that needs to happen in LittleJS context
125 }
126
127 gameUpdate(): void {
128 // NEW: Handle input using LittleJS
129 this.handleLittleJSInput()
130
131 // EXISTING: All update logic from main.ts animation loop
132 const passed = engine.timing.lastTimeStep
133 const dt = this.waiting() ? 0 : passed
134
135 // ... rest of update logic
136 }
137
138 private handleLittleJSInput(): void {
139 // Map LittleJS input to existing input system
140
141 // Steering
142 let steer = 0
143 steer += inputGetState('arrowLeft') ? -1 : 0
144 steer += inputGetState('arrowRight') ? 1 : 0
145 steer += inputGetState('a') ? -1 : 0
146 steer += inputGetState('d') ? 1 : 0
147 steer += inputGetAxis('mouseX') * 0.5
148
149 // Store for use in existing code
150 this.currentSteer = steer
151
152 // Button presses
153 if (inputGetPressed('m')) {
154 this.toggleMute()
155 }
156 if (inputGetPressed('r')) {
157 this.randomize()
158 }
159 if (inputGetPressed('arrowUp') || inputGetPressed('w')) {
160 this.stepPace(1, true)
161 }
162 if (inputGetPressed('arrowDown') || inputGetPressed('s')) {
163 this.stepPace(-1, true)
164 }
165
166 // Gamepad
167 for (const button of this.pollLittleJSPads()) {
168 // ... handle gamepad buttons
169 }
170
171 // Start sound on any input
172 if (Math.abs(steer) > 0 ||
173 inputGetPressed('m') ||
174 inputGetPressed('r') ||
175 inputGetPressed('arrowUp') ||
176 inputGetPressed('arrowDown')) {
177 this.startSound()
178 }
179 }
180
181 private pollLittleJSPads(): number[] {
182 // Implement gamepad polling using LittleJS
183 const pressed: number[] = []
184 const down = new Set<number>()
185
186 for (const pad of navigator.getGamepads?.() ?? []) {
187 if (!pad) continue
188
189 // Check buttons
190 pad.buttons.forEach((button, n) => {
191 if (button.pressed) {
192 down.add(n)
193 if (!this.padDown.has(n)) pressed.push(n)
194 }
195 })
196 }
197
198 this.padDown = down
199 return pressed
200 }
201
202 gameUpdatePost(): void {
203 // Post-update logic
204 }
205
206 gameRender(): void {
207 // Render the Three.js scene
208 this.renderer.render(this.scene, this.camera)
209 }
210
211 gameRenderPost(): void {
212 // Draw HUD using LittleJS (optional)
213 // Can keep existing DOM-based UI or use LittleJS
214 }
215}
216
217// Initialize when ready
218new NordicSkating()
219```
220
221### Step 1.2: Update index.html
222Replace the script tag:
223```html
224<!-- OLD -->
225<script type="module" src="/src/main.ts"></script>
226
227<!-- NEW -->
228<script type="module" src="/src/main-littlejs.ts"></script>
229```
230
231### Step 1.3: Update vite.config.ts (if needed)
232Ensure LittleJS can be resolved:
233```typescript
234import { defineConfig } from 'vite'
235
236export default defineConfig({
237 resolve: {
238 alias: {
239 littlejsengine: 'littlejsengine/dist/littlejs.esm.js'
240 }
241 }
242})
243```
244
245### Step 1.4: Test basic functionality
246```bash
247npm run dev
248```
249
250**Test checklist:**
251- [ ] Page loads without errors
252- [ ] Keyboard steering works
253- [ ] Mouse/touch steering works
254- [ ] Gamepad works (if available)
255- [ ] Pace changes work
256- [ ] Random lake works
257- [ ] UI still functions
258- [ ] Audio works
259
260---
261
262## Phase 2: Input System Migration (1 day)
263
264### Step 2.1: Update tilt steering
265Replace custom device orientation with LittleJS:
266```typescript
267// OLD: Custom tilt handling in Input class
268// NEW: Use LittleJS device orientation
269
270// In gameUpdate():
271const tiltX = inputGetAxis('deviceOrientationGamma')
272const tiltY = inputGetAxis('deviceOrientationBeta')
273// Process tilt...
274```
275
276### Step 2.2: Remove old Input class usage
277Gradually replace all calls to the old Input class with LittleJS input functions.
278
279### Step 2.3: Test on mobile
280- Test on iOS and Android devices
281- Verify tilt steering works
282- Check touch input
283
284---
285
286## Phase 3: Game Loop Migration (1 day)
287
288### Step 3.1: Remove custom animation loop
289Remove the `renderer.setAnimationLoop()` call from main.ts and replace with LittleJS game loop.
290
291### Step 3.2: Convert timing to use delta time
292Replace all `clock.getDelta()` with `engine.timing.lastTimeStep`:
293```typescript
294// OLD
295const passed = clock.getDelta()
296
297// NEW
298const passed = engine.timing.lastTimeStep
299```
300
301### Step 3.3: Test frame rate independence
302- Verify physics work the same at different frame rates
303- Test on low-end devices
304
305---
306
307## Phase 4: Camera and Rendering (2-3 days)
308
309### Step 4.1: Keep existing rendering code
310The Three.js rendering code can remain largely unchanged.
311
312### Step 4.2: Integrate with LittleJS render loop
313Ensure the Three.js renderer is properly integrated with LittleJS.
314
315### Step 4.3: Add LittleJS HUD (optional)
316Replace DOM-based HUD with LittleJS text rendering:
317```typescript
318gameRenderPost(): void {
319 const x = 20
320 let y = 40
321
322 drawTextScreen(
323 `Speed: ${Math.round(this.skater.speed * 3.6)} km/h`,
324 vec2(x, y),
325 24,
326 colorWhite
327 )
328 // ... more HUD elements
329}
330```
331
332---
333
334## Phase 5: Audio Integration (1-2 days)
335
336### Step 5.1: Keep existing audio system
337The custom Web Audio code is excellent and should be kept.
338
339### Step 5.2: Trigger audio from game loop
340```typescript
341// In gameUpdate():
342if (this.sounding() && this.skater.under === 0) {
343 const h = this.lake.thickness(this.skater.x, this.skater.s)
344 const lying = this.lake.snow(this.skater.x, this.skater.s)
345
346 if (Math.floor(this.skater.stride) !== this.strides) {
347 this.sound?.strike(h, undefined, (0.75 + 0.5 * Math.random()) * (1 - 0.9 * lying))
348 }
349
350 this.sound?.glide(h, Math.exp(-sincePush(this.skater) / 0.5), undefined, lying)
351}
352```
353
354### Step 5.3: Handle audio context
355Use LittleJS to manage audio context state if needed, or keep existing approach.
356
357---
358
359## Phase 6: UI Migration (2-3 days)
360
361### Step 6.1: Keep DOM-based UI (easiest)
362The existing dialog-based UI works well. Just trigger it from LittleJS input.
363
364### Step 6.2: Or migrate to LittleJS UI (optional)
365Use LittleJS UI system for buttons and dialogs:
366```typescript
367// Create buttons
368const soundButton = new Button(
369 vec2(100, 50),
370 vec2(200, 60),
371 'Sound',
372 () => this.toggleMute()
373)
374```
375
376---
377
378## Phase 7: Polish and Testing (3-5 days)
379
380### Step 7.1: Full regression testing
381Test every feature against the current implementation.
382
383### Step 7.2: Performance testing
384- Compare FPS with current implementation
385- Check memory usage
386- Test on all target devices
387
388### Step 7.3: User testing
389- Get feedback on controls feel
390- Verify audio quality
391- Check visual quality
392
393### Step 7.4: Bug fixing
394Fix any issues discovered during testing.
395
396---
397
398## Phase 8: Cleanup (1-2 days)
399
400### Step 8.1: Remove deprecated code
401- Remove old Input class
402- Remove custom animation loop code
403- Remove any other replaced systems
404
405### Step 8.2: Update documentation
406- Update README
407- Update build instructions
408- Document any changes
409
410### Step 8.3: Final testing
411One more round of full testing.
412
413---
414
415## Migration Checklist
416
417Use this checklist to track progress:
418
419### Phase 1: Setup
420- [ ] LittleJS installed
421- [ ] New entry point created
422- [ ] index.html updated
423- [ ] Vite config updated (if needed)
424- [ ] Basic page loads
425
426### Phase 2: Input
427- [ ] Keyboard input works
428- [ ] Mouse/touch input works
429- [ ] Gamepad input works
430- [ ] Tilt input works
431- [ ] All button mappings correct
432
433### Phase 3: Game Loop
434- [ ] Custom animation loop removed
435- [ ] Delta time integrated
436- [ ] Physics work correctly
437- [ ] Frame rate independent
438
439### Phase 4: Rendering
440- [ ] Three.js rendering works
441- [ ] Camera follows correctly
442- [ ] All visuals correct
443- [ ] HUD works (DOM or LittleJS)
444
445### Phase 5: Audio
446- [ ] Audio triggers correctly
447- [ ] Audio context handled
448- [ ] All sounds work
449- [ ] Volume/mute works
450
451### Phase 6: UI
452- [ ] Settings dialog works
453- [ ] About dialog works
454- [ ] Buttons work
455- [ ] Responsive design maintained
456
457### Phase 7: Testing
458- [ ] All features tested
459- [ ] Performance acceptable
460- [ ] Mobile devices tested
461- [ ] No regressions
462
463### Phase 8: Cleanup
464- [ ] Deprecated code removed
465- [ ] Documentation updated
466- [ ] Final testing complete
467
468---
469
470## Rollback Plan
471
472If you need to rollback at any point:
473
474```bash
475# Restore original files
476cp src/main.ts.backup src/main.ts
477cp index.html.backup index.html
478
479# Remove LittleJS
480npm uninstall littlejsengine
481
482# Reinstall original dependencies
483npm install
484```
485
486---
487
488## Tips and Tricks
489
490### 1. Debugging Input
491If input isn't working:
492```typescript
493// Add debug logging
494console.log('Keyboard states:', {
495 left: inputGetState('arrowLeft'),
496 right: inputGetState('arrowRight'),
497 a: inputGetState('a'),
498 d: inputGetState('d')
499})
500```
501
502### 2. Checking Delta Time
503```typescript
504console.log('Delta time:', engine.timing.lastTimeStep)
505console.log('FPS:', engine.timing.fps)
506```
507
508### 3. Device Orientation Debug
509```typescript
510console.log('Orientation:', {
511 gamma: inputGetAxis('deviceOrientationGamma'),
512 beta: inputGetAxis('deviceOrientationBeta')
513})
514```
515
516### 4. Canvas Issues
517If canvas isn't rendering:
518- Check that `engine.canvas` is set correctly
519- Verify Three.js renderer is using the same canvas
520- Ensure `setThreeRenderer(THREE)` is called
521
522### 5. Performance Issues
523- Check for double rendering (Three.js + LittleJS)
524- Verify only one render loop is running
525- Use Chrome DevTools to profile
526
527---
528
529## Common Pitfalls
530
531### 1. Input Names Don't Match
532LittleJS uses specific key names. Check the [LittleJS Input Documentation](https://github.com/KilledByAPixel/LittleJS/blob/main/REFERENCE.md#input).
533
534**Solution**: Use the exact key names LittleJS expects:
535- `arrowLeft`, `arrowRight`, `arrowUp`, `arrowDown`
536- `a`, `d`, `w`, `s`
537- `m`, `r`
538
539### 2. Canvas Not Found
540**Solution**: Ensure the canvas element exists before initialization:
541```typescript
542const canvas = document.getElementById('scene')
543if (!canvas) {
544 console.error('Canvas not found!')
545 return
546}
547```
548
549### 3. THREE Not Available
550**Solution**: Make THREE globally available:
551```typescript
552(window as any).THREE = THREE
553```
554
555### 4. Audio Context Issues
556**Solution**: Audio must be triggered by user interaction. Use any input to start it:
557```typescript
558if (anyInputDetected) {
559 if (audio.state === 'suspended') {
560 audio.resume()
561 }
562}
563```
564
565### 5. Mobile Input Not Working
566**Solution**:
567- Check for touch event handling
568- Ensure canvas has proper touch action settings
569- Test on actual device (simulators may not work)
570
571---
572
573## Success Criteria Checklist
574
575Before declaring the migration complete, verify:
576
577### Functionality
578- [ ] All current features work exactly as before
579- [ ] No features are broken or missing
580- [ ] Controls feel the same or better
581- [ ] Audio quality unchanged
582- [ ] Visual quality unchanged
583
584### Performance
585- [ ] Frame rate >= original implementation
586- [ ] Memory usage <= original implementation
587- [ ] Load time acceptable
588- [ ] Works on all target devices
589
590### Code Quality
591- [ ] Code is cleaner and more maintainable
592- [ ] No unnecessary code left behind
593- [ ] Documentation updated
594- [ ] TypeScript types correct
595
596### User Experience
597- [ ] No noticeable difference for users
598- [ ] Mobile experience improved (if applicable)
599- [ ] All input methods work
600- [ ] No new bugs introduced
601
602---
603
604## Final Notes
605
606This migration is **low-risk and high-reward** due to:
607
6081. **Three.js plugin** means rendering code stays the same
6092. **Incremental approach** allows validating each step
6103. **Minimal integration** provides immediate benefits
6114. **Well-documented** API with good examples
6125. **Active community** for support
613
614**Estimated time for full migration**: 17-26 days (part-time) or 8-13 days (full-time)
615
616**Estimated time for minimal integration**: 3-5 days (part-time) or 1-2 days (full-time)
617
618**Recommendation**: Start with minimal integration to gain immediate benefits, then proceed with full migration if successful.