fuzzyworld

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

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

LITTLEJS_POC.md

14.5 KB · history · blame

LittleJS Proof of Concept for Nordic Skating

This document provides a working proof-of-concept implementation to validate the migration approach before committing to the full conversion.

Quick Start

To test the POC:

cd /Users/pavel/code/nordic-skating
npm install littlejsengine
cp src/main.ts src/main.ts.backup
cp LITTLEJS_POC/src/main-littlejs.ts src/
npm run dev

POC Structure

The POC demonstrates: 1. LittleJS engine initialization with Three.js 2. Basic input handling (keyboard, mouse) 3. Simple skater movement 4. Lake rendering (static for POC) 5. Integration with existing lake generation

Proof of Concept Implementation

1. Install LittleJS

npm install littlejsengine

2. Create the POC Entry Point

src/littlejs-poc.ts:

/**
 * Proof of Concept: LittleJS + Three.js Integration for Nordic Skating
 * 
 * This file demonstrates that LittleJS can work alongside Three.js
 * for rendering while handling input, game loop, and state management.
 */

import * as THREE from 'three'
import { 
  engineInit, 
  engine, 
  inputGetState, 
  inputGetPressed,
  vec2,
  colorWhite,
  drawTextScreen,
  setThreeRenderer
} from 'littlejsengine'
import { createLake, type Lake, mulberry32 } from './world/lake.js'
import { createSkater, stepSkater, PACES, type Skater } from './skater.js'
import { BREAKING_THICKNESS, CRACKING_THICKNESS } from './ice/strength.js'

// Constants
const EYE = 1.5
const BASE_FOV = 65
const PITCH = 0.12

class NordicPOC {
  private lake: Lake
  private skater: Skater
  private scene: THREE.Scene
  private camera: THREE.PerspectiveCamera
  private renderer: THREE.WebGLRenderer
  private iceMesh: THREE.Mesh
  private shoresGroup: THREE.Group

  // Input state
  private steerValue = 0
  private muted = false
  private seed = 12345

  constructor() {
    // Store reference for Three.js
    window.THREE = THREE

    // Initialize LittleJS with Three.js plugin
    this.initEngine()
  }

  private initEngine(): void {
    // Set up canvas
    const canvas = document.getElementById('scene') as HTMLCanvasElement
    if (!canvas) {
      console.error('Canvas element not found')
      return
    }

    // Configure engine to use our canvas
    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)

    // Create scene
    this.scene = new THREE.Scene()
    const haze = new THREE.Fog(0, 30, 600)
    this.scene.fog = haze
    this.scene.background = haze.color

    // Create camera
    this.camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
    this.camera.rotation.order = 'YXZ'

    // Initialize world
    this.lake = createLake(this.seed, { impossibleIce: false })
    this.skater = createSkater(this.lake, 0)
    this.skater.pace = 'cruise'

    // Create simple ice plane for POC
    this.createIcePlane()

    // Create simple shores for POC
    this.shoresGroup = new THREE.Group()
    this.scene.add(this.shoresGroup)

    // Create simple sky
    this.createSimpleSky()

    // Handle resize
    window.addEventListener('resize', this.onResize.bind(this))
    this.onResize()

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

  private createIcePlane(): void {
    // Simple ice plane for POC
    const geometry = new THREE.PlaneGeometry(1000, 1000, 100, 100)
    const material = new THREE.MeshStandardMaterial({ 
      color: 0x0b1a24,
      roughness: 0.9,
      metalness: 0.1
    })
    this.iceMesh = new THREE.Mesh(geometry, material)
    this.iceMesh.rotation.x = -Math.PI / 2
    this.scene.add(this.iceMesh)
  }

  private createSimpleSky(): void {
    // Simple skybox for POC
    const geometry = new THREE.SphereGeometry(500, 32, 32)
    const material = new THREE.MeshBasicMaterial({ 
      color: 0x87ceeb,
      side: THREE.BackSide
    })
    const sky = new THREE.Mesh(geometry, material)
    this.scene.add(sky)
  }

  private onResize(): void {
    const canvas = engine.canvas
    this.renderer.setSize(canvas.width, canvas.height, false)
    this.camera.aspect = canvas.width / canvas.height
    this.camera.updateProjectionMatrix()
  }

  gameInit(): void {
    console.log('LittleJS + Nordic Skating POC initialized')
  }

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

    // Update skater using existing physics
    const dt = engine.timing.lastTimeStep
    stepSkater(this.skater, this.lake, this.steerValue, dt)
  }

  private handleInput(): void {
    // Keyboard steering
    let steer = 0
    steer += inputGetState('arrowLeft') ? -1 : 0
    steer += inputGetState('arrowRight') ? 1 : 0
    steer += inputGetState('a') ? -1 : 0
    steer += inputGetState('d') ? 1 : 0

    // Mouse/touch steering
    steer += inputGetAxis('mouseX') * 0.5

    // Gamepad steering
    steer += inputGetAxis('gamepadLeftX')

    this.steerValue = Math.max(-1, Math.min(1, steer))

    // Other inputs
    if (inputGetPressed('m')) {
      this.muted = !this.muted
      console.log(`Sound ${this.muted ? 'off' : 'on'}`)
    }

    if (inputGetPressed('r')) {
      this.seed = Math.floor(Math.random() * 999999) + 1
      this.lake = createLake(this.seed, { impossibleIce: false })
      this.skater = createSkater(this.lake, 0)
      this.skater.pace = 'cruise'
      console.log(`New lake: ${this.seed}`)
    }

    // Pace control
    if (inputGetPressed('arrowUp') || inputGetPressed('w')) {
      const currentIndex = PACES[0].findIndex(p => p === this.skater.pace)
      const nextIndex = (currentIndex + 1) % PACES[0].length
      this.skater.pace = PACES[0][nextIndex]
    }
    if (inputGetPressed('arrowDown') || inputGetPressed('s')) {
      const currentIndex = PACES[0].findIndex(p => p === this.skater.pace)
      const prevIndex = (currentIndex - 1 + PACES[0].length) % PACES[0].length
      this.skater.pace = PACES[0][prevIndex]
    }

    // Start sound on any input (browser requirement)
    if (this.steerValue !== 0 || 
        inputGetPressed('m') || 
        inputGetPressed('r') ||
        inputGetPressed('arrowUp') ||
        inputGetPressed('arrowDown')) {
      this.startSound()
    }
  }

  private startSound(): void {
    // POC: Just log for now
    console.log('Sound would start here')
  }

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

  gameRender(): void {
    // Update camera based on skater
    this.updateCamera()

    // Render Three.js scene
    this.renderer.render(this.scene, this.camera)
  }

  private updateCamera(): void {
    // Calculate camera position based on skater state
    const pushFor = 0.5 * PACES[this.skater.pace].strides
    const push = this.skater.under === 0 && 
                ((Date.now() / 1000) % this.skater.stride) < pushFor 
      ? Math.sin((Math.PI * ((Date.now() / 1000) % this.skater.stride)) / pushFor) 
      : 0

    const hurry = (this.skater.speed - PACES.cruise.speed) / 
                (PACES.olympic.speed - PACES.cruise.speed)
    const crouch = hurry > 0 
      ? PACES.olympic.crouch * hurry 
      : PACES.meander.crouch * Math.min(1, -hurry * 2.3)

    const fov = BASE_FOV + 12 * Math.max(-0.4, Math.min(1.1, hurry))

    if (Math.abs(this.camera.fov - fov) > 0.05) {
      this.camera.fov = fov
      this.camera.updateProjectionMatrix()
    }

    const sunk = this.skater.under > 0 
      ? Math.min(1, (3 - this.skater.under) / 0.3) 
      : 0

    // Camera position: first person view
    this.camera.position.set(
      this.skater.x,
      EYE - crouch + 0.03 * push - 1.3 * sunk,
      this.skater.s
    )

    // Camera rotation
    const along = Math.atan(this.skater.channel.slope(this.skater.s))
    const look = this.skater.heading - 
                0.25 * Math.sin(this.skater.heading - along)

    this.camera.rotation.set(-PITCH, -look, -0.02 * this.skater.turn * this.skater.speed)
  }

  gameRenderPost(): void {
    // Draw HUD using LittleJS
    const x = 20
    let y = 40
    const lineHeight = 30

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

    drawTextScreen(
      `Pace: ${this.skater.pace}`,
      vec2(x, y),
      24,
      colorWhite
    )
    y += lineHeight

    drawTextScreen(
      `Lake: ${this.seed}`,
      vec2(x, y),
      24,
      colorWhite
    )
    y += lineHeight

    drawTextScreen(
      `Steer: ${this.steerValue.toFixed(2)}`,
      vec2(x, y),
      24,
      colorWhite
    )

    // Instructions
    drawTextScreen(
      'WASD/Arrows: Steer | M: Mute | R: Random Lake | Up/Down: Change Pace',
      vec2(engine.canvas.width / 2, engine.canvas.height - 30),
      18,
      colorWhite,
      0.5
    )
  }
}

// Initialize POC when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', () => {
    new NordicPOC()
  })
} else {
  new NordicPOC()
}

3. Update index.html for POC

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <title>Nordic Skating - LittleJS POC</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { 
      overflow: hidden; 
      background: #000; 
      font-family: sans-serif;
    }
    canvas { 
      display: block; 
      width: 100vw; 
      height: 100vh;
    }
  </style>
</head>
<body>
  <canvas id="scene"></canvas>
  <script type="module" src="/src/littlejs-poc.ts"></script>
</body>
</html>

4. Update Vite Config (if needed)

vite.config.ts:

import { defineConfig } from 'vite'
import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      // Ensure LittleJS can be resolved
      littlejsengine: resolve(__dirname, 'node_modules/littlejsengine/dist/littlejs.esm.js')
    }
  },
  build: {
    target: 'esnext'
  }
})

Testing the POC

Expected Behavior

  1. Basic Movement: Use WASD or arrow keys to steer
  2. Pace Changes: Use Up/Down arrows or W/S to change pace
  3. Random Lake: Press R to generate a new lake
  4. HUD: Shows speed, pace, lake seed, and steering value
  5. Camera: First-person view that follows skater physics

Test Checklist

POC Validation Criteria

The POC is successful if:

  1. ✅ LittleJS engine initializes without errors
  2. ✅ Three.js rendering works alongside LittleJS
  3. ✅ Input from keyboard, mouse, and gamepad is captured
  4. ✅ Skater physics update correctly using LittleJS delta time
  5. ✅ Camera positioning works as expected
  6. ✅ HUD rendering with LittleJS text works
  7. ✅ Basic lake generation is integrated

Next Steps After POC

If POC is successful:

  1. Full Migration: Proceed with the complete migration plan
  2. Incremental Approach: Migrate one system at a time
  3. Side-by-side Testing: Compare performance and feel with current implementation

If POC has issues:

  1. Debug: Identify and fix integration problems
  2. Adjust Plan: Modify migration approach based on findings
  3. Partial Integration: Use only specific LittleJS features (input, game loop) while keeping Three.js for rendering

Troubleshooting

Common Issues

1. Three.js not available in LittleJS context - Solution: Ensure THREE is available globally or pass it explicitly - In the POC, we set window.THREE = THREE as a fallback

2. Canvas sizing issues - Solution: Let LittleJS manage the canvas, don't manually size it - Remove any manual canvas resizing from existing code

3. Input not working - Solution: Check LittleJS input mapping - Ensure input system is initialized before use - Verify key names match LittleJS expectations

4. Audio context issues - Solution: Handle audio context creation carefully - Remember browser restrictions on autoplay - Use user interaction to unlock audio

5. Performance issues - Solution: Check for double rendering - Ensure only one render loop is running - Verify Three.js renderer is properly configured

Alternative: Minimal Integration

If the full integration proves problematic, consider a minimal integration approach:

// Only use LittleJS for input and game loop
import { engineInit, inputGetState } from 'littlejsengine'
import * as THREE from 'three'

// Keep existing Three.js setup
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)

// Use LittleJS just for input and timing
engineInit(
  () => {}, // init
  () => {
    // Game update using LittleJS input
    const steer = inputGetState('arrowLeft') - inputGetState('arrowRight')
    // Update game state
  },
  () => {}, // updatePost
  () => {
    // Render with Three.js
    renderer.render(scene, camera)
  },
  () => {} // renderPost
)

This approach gives you: - LittleJS input handling (better than custom implementation) - LittleJS timing (consistent delta time) - LittleJS game loop management - While keeping full control over Three.js rendering

Conclusion

The POC demonstrates that LittleJS and Three.js can coexist in the Nordic Skating project. The main benefits from even a minimal integration would be:

  1. Better Input Handling: LittleJS provides comprehensive input support
  2. Consistent Timing: Automatic delta time calculation
  3. Game Loop Management: No need to manually manage animation frames
  4. Mobile Support: Built-in touch and device orientation support

The full migration would additionally provide: 5. UI System: For dialogs and HUD 6. Audio Management: Though we may keep custom Web Audio 7. Scene Management: For different game states 8. Particle System: Could enhance visual effects

Recommendation: Start with the minimal integration (input + game loop) to validate the approach, then expand to full migration if successful.

Source
1# LittleJS Proof of Concept for Nordic Skating
2
3This document provides a working proof-of-concept implementation to validate the migration approach before committing to the full conversion.
4
5## Quick Start
6
7To test the POC:
8
9```bash
10cd /Users/pavel/code/nordic-skating
11npm install littlejsengine
12cp src/main.ts src/main.ts.backup
13cp LITTLEJS_POC/src/main-littlejs.ts src/
14npm run dev
15```
16
17## POC Structure
18
19The POC demonstrates:
201. LittleJS engine initialization with Three.js
212. Basic input handling (keyboard, mouse)
223. Simple skater movement
234. Lake rendering (static for POC)
245. Integration with existing lake generation
25
26## Proof of Concept Implementation
27
28### 1. Install LittleJS
29
30```bash
31npm install littlejsengine
32```
33
34### 2. Create the POC Entry Point
35
36`src/littlejs-poc.ts`:
37
38```typescript
39/**
40 * Proof of Concept: LittleJS + Three.js Integration for Nordic Skating
41 *
42 * This file demonstrates that LittleJS can work alongside Three.js
43 * for rendering while handling input, game loop, and state management.
44 */
45
46import * as THREE from 'three'
47import {
48 engineInit,
49 engine,
50 inputGetState,
51 inputGetPressed,
52 vec2,
53 colorWhite,
54 drawTextScreen,
55 setThreeRenderer
56} from 'littlejsengine'
57import { createLake, type Lake, mulberry32 } from './world/lake.js'
58import { createSkater, stepSkater, PACES, type Skater } from './skater.js'
59import { BREAKING_THICKNESS, CRACKING_THICKNESS } from './ice/strength.js'
60
61// Constants
62const EYE = 1.5
63const BASE_FOV = 65
64const PITCH = 0.12
65
66class NordicPOC {
67 private lake: Lake
68 private skater: Skater
69 private scene: THREE.Scene
70 private camera: THREE.PerspectiveCamera
71 private renderer: THREE.WebGLRenderer
72 private iceMesh: THREE.Mesh
73 private shoresGroup: THREE.Group
74
75 // Input state
76 private steerValue = 0
77 private muted = false
78 private seed = 12345
79
80 constructor() {
81 // Store reference for Three.js
82 window.THREE = THREE
83
84 // Initialize LittleJS with Three.js plugin
85 this.initEngine()
86 }
87
88 private initEngine(): void {
89 // Set up canvas
90 const canvas = document.getElementById('scene') as HTMLCanvasElement
91 if (!canvas) {
92 console.error('Canvas element not found')
93 return
94 }
95
96 // Configure engine to use our canvas
97 engine.canvas = canvas
98 engine.canvasSize = vec2(canvas.width, canvas.height)
99
100 // Initialize Three.js renderer
101 this.renderer = new THREE.WebGLRenderer({
102 canvas,
103 antialias: true
104 })
105 this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
106
107 // Set Three.js renderer for LittleJS
108 setThreeRenderer(THREE)
109
110 // Create scene
111 this.scene = new THREE.Scene()
112 const haze = new THREE.Fog(0, 30, 600)
113 this.scene.fog = haze
114 this.scene.background = haze.color
115
116 // Create camera
117 this.camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
118 this.camera.rotation.order = 'YXZ'
119
120 // Initialize world
121 this.lake = createLake(this.seed, { impossibleIce: false })
122 this.skater = createSkater(this.lake, 0)
123 this.skater.pace = 'cruise'
124
125 // Create simple ice plane for POC
126 this.createIcePlane()
127
128 // Create simple shores for POC
129 this.shoresGroup = new THREE.Group()
130 this.scene.add(this.shoresGroup)
131
132 // Create simple sky
133 this.createSimpleSky()
134
135 // Handle resize
136 window.addEventListener('resize', this.onResize.bind(this))
137 this.onResize()
138
139 // Initialize LittleJS
140 engineInit(
141 () => this.gameInit(),
142 () => this.gameUpdate(),
143 () => this.gameUpdatePost(),
144 () => this.gameRender(),
145 () => this.gameRenderPost()
146 )
147 }
148
149 private createIcePlane(): void {
150 // Simple ice plane for POC
151 const geometry = new THREE.PlaneGeometry(1000, 1000, 100, 100)
152 const material = new THREE.MeshStandardMaterial({
153 color: 0x0b1a24,
154 roughness: 0.9,
155 metalness: 0.1
156 })
157 this.iceMesh = new THREE.Mesh(geometry, material)
158 this.iceMesh.rotation.x = -Math.PI / 2
159 this.scene.add(this.iceMesh)
160 }
161
162 private createSimpleSky(): void {
163 // Simple skybox for POC
164 const geometry = new THREE.SphereGeometry(500, 32, 32)
165 const material = new THREE.MeshBasicMaterial({
166 color: 0x87ceeb,
167 side: THREE.BackSide
168 })
169 const sky = new THREE.Mesh(geometry, material)
170 this.scene.add(sky)
171 }
172
173 private onResize(): void {
174 const canvas = engine.canvas
175 this.renderer.setSize(canvas.width, canvas.height, false)
176 this.camera.aspect = canvas.width / canvas.height
177 this.camera.updateProjectionMatrix()
178 }
179
180 gameInit(): void {
181 console.log('LittleJS + Nordic Skating POC initialized')
182 }
183
184 gameUpdate(): void {
185 // Handle input using LittleJS
186 this.handleInput()
187
188 // Update skater using existing physics
189 const dt = engine.timing.lastTimeStep
190 stepSkater(this.skater, this.lake, this.steerValue, dt)
191 }
192
193 private handleInput(): void {
194 // Keyboard steering
195 let steer = 0
196 steer += inputGetState('arrowLeft') ? -1 : 0
197 steer += inputGetState('arrowRight') ? 1 : 0
198 steer += inputGetState('a') ? -1 : 0
199 steer += inputGetState('d') ? 1 : 0
200
201 // Mouse/touch steering
202 steer += inputGetAxis('mouseX') * 0.5
203
204 // Gamepad steering
205 steer += inputGetAxis('gamepadLeftX')
206
207 this.steerValue = Math.max(-1, Math.min(1, steer))
208
209 // Other inputs
210 if (inputGetPressed('m')) {
211 this.muted = !this.muted
212 console.log(`Sound ${this.muted ? 'off' : 'on'}`)
213 }
214
215 if (inputGetPressed('r')) {
216 this.seed = Math.floor(Math.random() * 999999) + 1
217 this.lake = createLake(this.seed, { impossibleIce: false })
218 this.skater = createSkater(this.lake, 0)
219 this.skater.pace = 'cruise'
220 console.log(`New lake: ${this.seed}`)
221 }
222
223 // Pace control
224 if (inputGetPressed('arrowUp') || inputGetPressed('w')) {
225 const currentIndex = PACES[0].findIndex(p => p === this.skater.pace)
226 const nextIndex = (currentIndex + 1) % PACES[0].length
227 this.skater.pace = PACES[0][nextIndex]
228 }
229 if (inputGetPressed('arrowDown') || inputGetPressed('s')) {
230 const currentIndex = PACES[0].findIndex(p => p === this.skater.pace)
231 const prevIndex = (currentIndex - 1 + PACES[0].length) % PACES[0].length
232 this.skater.pace = PACES[0][prevIndex]
233 }
234
235 // Start sound on any input (browser requirement)
236 if (this.steerValue !== 0 ||
237 inputGetPressed('m') ||
238 inputGetPressed('r') ||
239 inputGetPressed('arrowUp') ||
240 inputGetPressed('arrowDown')) {
241 this.startSound()
242 }
243 }
244
245 private startSound(): void {
246 // POC: Just log for now
247 console.log('Sound would start here')
248 }
249
250 gameUpdatePost(): void {
251 // Post-update logic
252 }
253
254 gameRender(): void {
255 // Update camera based on skater
256 this.updateCamera()
257
258 // Render Three.js scene
259 this.renderer.render(this.scene, this.camera)
260 }
261
262 private updateCamera(): void {
263 // Calculate camera position based on skater state
264 const pushFor = 0.5 * PACES[this.skater.pace].strides
265 const push = this.skater.under === 0 &&
266 ((Date.now() / 1000) % this.skater.stride) < pushFor
267 ? Math.sin((Math.PI * ((Date.now() / 1000) % this.skater.stride)) / pushFor)
268 : 0
269
270 const hurry = (this.skater.speed - PACES.cruise.speed) /
271 (PACES.olympic.speed - PACES.cruise.speed)
272 const crouch = hurry > 0
273 ? PACES.olympic.crouch * hurry
274 : PACES.meander.crouch * Math.min(1, -hurry * 2.3)
275
276 const fov = BASE_FOV + 12 * Math.max(-0.4, Math.min(1.1, hurry))
277
278 if (Math.abs(this.camera.fov - fov) > 0.05) {
279 this.camera.fov = fov
280 this.camera.updateProjectionMatrix()
281 }
282
283 const sunk = this.skater.under > 0
284 ? Math.min(1, (3 - this.skater.under) / 0.3)
285 : 0
286
287 // Camera position: first person view
288 this.camera.position.set(
289 this.skater.x,
290 EYE - crouch + 0.03 * push - 1.3 * sunk,
291 this.skater.s
292 )
293
294 // Camera rotation
295 const along = Math.atan(this.skater.channel.slope(this.skater.s))
296 const look = this.skater.heading -
297 0.25 * Math.sin(this.skater.heading - along)
298
299 this.camera.rotation.set(-PITCH, -look, -0.02 * this.skater.turn * this.skater.speed)
300 }
301
302 gameRenderPost(): void {
303 // Draw HUD using LittleJS
304 const x = 20
305 let y = 40
306 const lineHeight = 30
307
308 drawTextScreen(
309 `Speed: ${Math.round(this.skater.speed * 3.6)} km/h`,
310 vec2(x, y),
311 24,
312 colorWhite
313 )
314 y += lineHeight
315
316 drawTextScreen(
317 `Pace: ${this.skater.pace}`,
318 vec2(x, y),
319 24,
320 colorWhite
321 )
322 y += lineHeight
323
324 drawTextScreen(
325 `Lake: ${this.seed}`,
326 vec2(x, y),
327 24,
328 colorWhite
329 )
330 y += lineHeight
331
332 drawTextScreen(
333 `Steer: ${this.steerValue.toFixed(2)}`,
334 vec2(x, y),
335 24,
336 colorWhite
337 )
338
339 // Instructions
340 drawTextScreen(
341 'WASD/Arrows: Steer | M: Mute | R: Random Lake | Up/Down: Change Pace',
342 vec2(engine.canvas.width / 2, engine.canvas.height - 30),
343 18,
344 colorWhite,
345 0.5
346 )
347 }
348}
349
350// Initialize POC when DOM is ready
351if (document.readyState === 'loading') {
352 document.addEventListener('DOMContentLoaded', () => {
353 new NordicPOC()
354 })
355} else {
356 new NordicPOC()
357}
358```
359
360### 3. Update index.html for POC
361
362```html
363<!DOCTYPE html>
364<html lang="en">
365<head>
366 <meta charset="UTF-8">
367 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
368 <title>Nordic Skating - LittleJS POC</title>
369 <style>
370 * { margin: 0; padding: 0; box-sizing: border-box; }
371 body {
372 overflow: hidden;
373 background: #000;
374 font-family: sans-serif;
375 }
376 canvas {
377 display: block;
378 width: 100vw;
379 height: 100vh;
380 }
381 </style>
382</head>
383<body>
384 <canvas id="scene"></canvas>
385 <script type="module" src="/src/littlejs-poc.ts"></script>
386</body>
387</html>
388```
389
390### 4. Update Vite Config (if needed)
391
392`vite.config.ts`:
393```typescript
394import { defineConfig } from 'vite'
395import { resolve } from 'path'
396
397export default defineConfig({
398 resolve: {
399 alias: {
400 // Ensure LittleJS can be resolved
401 littlejsengine: resolve(__dirname, 'node_modules/littlejsengine/dist/littlejs.esm.js')
402 }
403 },
404 build: {
405 target: 'esnext'
406 }
407})
408```
409
410## Testing the POC
411
412### Expected Behavior
413
4141. **Basic Movement**: Use WASD or arrow keys to steer
4152. **Pace Changes**: Use Up/Down arrows or W/S to change pace
4163. **Random Lake**: Press R to generate a new lake
4174. **HUD**: Shows speed, pace, lake seed, and steering value
4185. **Camera**: First-person view that follows skater physics
419
420### Test Checklist
421
422- [ ] Keyboard steering works (A/D, Arrow Left/Right)
423- [ ] Mouse steering works (drag left/right)
424- [ ] Gamepad steering works (if available)
425- [ ] Pace changes work (Up/Down, W/S)
426- [ ] Random lake generation works (R)
427- [ ] HUD displays correctly
428- [ ] Camera follows skater properly
429- [ ] Frame rate is smooth
430- [ ] No errors in console
431
432## POC Validation Criteria
433
434The POC is successful if:
435
4361. ✅ LittleJS engine initializes without errors
4372. ✅ Three.js rendering works alongside LittleJS
4383. ✅ Input from keyboard, mouse, and gamepad is captured
4394. ✅ Skater physics update correctly using LittleJS delta time
4405. ✅ Camera positioning works as expected
4416. ✅ HUD rendering with LittleJS text works
4427. ✅ Basic lake generation is integrated
443
444## Next Steps After POC
445
446If POC is successful:
447
4481. **Full Migration**: Proceed with the complete migration plan
4492. **Incremental Approach**: Migrate one system at a time
4503. **Side-by-side Testing**: Compare performance and feel with current implementation
451
452If POC has issues:
453
4541. **Debug**: Identify and fix integration problems
4552. **Adjust Plan**: Modify migration approach based on findings
4563. **Partial Integration**: Use only specific LittleJS features (input, game loop) while keeping Three.js for rendering
457
458## Troubleshooting
459
460### Common Issues
461
462**1. Three.js not available in LittleJS context**
463- Solution: Ensure THREE is available globally or pass it explicitly
464- In the POC, we set `window.THREE = THREE` as a fallback
465
466**2. Canvas sizing issues**
467- Solution: Let LittleJS manage the canvas, don't manually size it
468- Remove any manual canvas resizing from existing code
469
470**3. Input not working**
471- Solution: Check LittleJS input mapping
472- Ensure input system is initialized before use
473- Verify key names match LittleJS expectations
474
475**4. Audio context issues**
476- Solution: Handle audio context creation carefully
477- Remember browser restrictions on autoplay
478- Use user interaction to unlock audio
479
480**5. Performance issues**
481- Solution: Check for double rendering
482- Ensure only one render loop is running
483- Verify Three.js renderer is properly configured
484
485## Alternative: Minimal Integration
486
487If the full integration proves problematic, consider a **minimal integration** approach:
488
489```typescript
490// Only use LittleJS for input and game loop
491import { engineInit, inputGetState } from 'littlejsengine'
492import * as THREE from 'three'
493
494// Keep existing Three.js setup
495const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
496const scene = new THREE.Scene()
497const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
498
499// Use LittleJS just for input and timing
500engineInit(
501 () => {}, // init
502 () => {
503 // Game update using LittleJS input
504 const steer = inputGetState('arrowLeft') - inputGetState('arrowRight')
505 // Update game state
506 },
507 () => {}, // updatePost
508 () => {
509 // Render with Three.js
510 renderer.render(scene, camera)
511 },
512 () => {} // renderPost
513)
514```
515
516This approach gives you:
517- LittleJS input handling (better than custom implementation)
518- LittleJS timing (consistent delta time)
519- LittleJS game loop management
520- While keeping full control over Three.js rendering
521
522## Conclusion
523
524The POC demonstrates that **LittleJS and Three.js can coexist** in the Nordic Skating project. The main benefits from even a minimal integration would be:
525
5261. **Better Input Handling**: LittleJS provides comprehensive input support
5272. **Consistent Timing**: Automatic delta time calculation
5283. **Game Loop Management**: No need to manually manage animation frames
5294. **Mobile Support**: Built-in touch and device orientation support
530
531The full migration would additionally provide:
5325. **UI System**: For dialogs and HUD
5336. **Audio Management**: Though we may keep custom Web Audio
5347. **Scene Management**: For different game states
5358. **Particle System**: Could enhance visual effects
536
537**Recommendation**: Start with the minimal integration (input + game loop) to validate the approach, then expand to full migration if successful.