fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / LITTLEJS_POC / src / littlejs-poc.ts

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

littlejs-poc.ts

11.4 KB · history · blame

1/**
2 * Proof of Concept: LittleJS + Three.js Integration for Nordic Skating
3 *
4 * This file demonstrates that LittleJS can work alongside Three.js
5 * for rendering while handling input, game loop, and state management.
6 *
7 * To test:
8 * 1. npm install littlejsengine
9 * 2. Place this file in your src directory
10 * 3. Update index.html to use this as entry point
11 * 4. npm run dev
12 */
13
14import * as THREE from 'three'
15import {
16 engineInit,
17 engine,
18 inputGetState,
19 inputGetPressed,
20 inputGetAxis,
21 vec2,
22 colorWhite,
23 drawTextScreen,
24 setThreeRenderer
25} from 'littlejsengine'
26import { createLake, type Lake, mulberry32 } from '../../src/world/lake.js'
27import { createSkater, stepSkater, PACES, type Skater, pushSeconds, sincePush } from '../../src/skater.js'
28import { BREAKING_THICKNESS, CRACKING_THICKNESS } from '../../src/ice/strength.js'
29
30// Make THREE available globally for LittleJS plugin
31if (typeof window !== 'undefined') {
32 (window as any).THREE = THREE
33}
34
35// Constants
36const EYE = 1.5
37const BASE_FOV = 65
38const PITCH = 0.12
39
40class NordicPOC {
41 private lake: Lake
42 private skater: Skater
43 private scene: THREE.Scene
44 private camera: THREE.PerspectiveCamera
45 private renderer: THREE.WebGLRenderer
46 private iceMesh: THREE.Mesh
47 private shoresGroup: THREE.Group
48 private skyMesh: THREE.Mesh
49
50 // Input state
51 private steerValue = 0
52 private muted = false
53 private seed = 12345
54 private lastPaceChange = 0
55
56 constructor() {
57 // Initialize LittleJS with Three.js plugin
58 this.initEngine()
59 }
60
61 private initEngine(): void {
62 // Set up canvas
63 const canvas = document.getElementById('scene') as HTMLCanvasElement
64 if (!canvas) {
65 console.error('Canvas element not found')
66 return
67 }
68
69 // Configure engine to use our canvas
70 engine.canvas = canvas
71 engine.canvasSize = vec2(canvas.width, canvas.height)
72
73 // Initialize Three.js renderer
74 this.renderer = new THREE.WebGLRenderer({
75 canvas,
76 antialias: true
77 })
78 this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
79
80 // Set Three.js renderer for LittleJS
81 setThreeRenderer(THREE)
82
83 // Create scene
84 this.scene = new THREE.Scene()
85 const haze = new THREE.Fog(0, 30, 600)
86 this.scene.fog = haze
87 this.scene.background = haze.color
88
89 // Create camera
90 this.camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1500)
91 this.camera.rotation.order = 'YXZ'
92
93 // Initialize world
94 this.lake = createLake(this.seed, { impossibleIce: false })
95 this.skater = createSkater(this.lake, 0)
96 this.skater.pace = 'cruise'
97
98 // Create simple ice plane for POC
99 this.createIcePlane()
100
101 // Create simple shores for POC
102 this.shoresGroup = new THREE.Group()
103 this.scene.add(this.shoresGroup)
104
105 // Create simple sky
106 this.createSimpleSky()
107
108 // Handle resize
109 window.addEventListener('resize', this.onResize.bind(this))
110 this.onResize()
111
112 // Initialize LittleJS
113 engineInit(
114 () => this.gameInit(),
115 () => this.gameUpdate(),
116 () => this.gameUpdatePost(),
117 () => this.gameRender(),
118 () => this.gameRenderPost()
119 )
120 }
121
122 private createIcePlane(): void {
123 // Simple ice plane for POC - larger than needed
124 const geometry = new THREE.PlaneGeometry(2000, 2000, 100, 100)
125 const material = new THREE.MeshStandardMaterial({
126 color: 0x0b1a24,
127 roughness: 0.95,
128 metalness: 0.05
129 })
130 this.iceMesh = new THREE.Mesh(geometry, material)
131 this.iceMesh.rotation.x = -Math.PI / 2
132 this.iceMesh.position.y = -0.1
133 this.scene.add(this.iceMesh)
134
135 // Add ambient light
136 const ambient = new THREE.AmbientLight(0x404040, 0.5)
137 this.scene.add(ambient)
138
139 // Add directional light
140 const sun = new THREE.DirectionalLight(0xffffff, 0.8)
141 sun.position.set(100, 200, 100)
142 this.scene.add(sun)
143 }
144
145 private createSimpleSky(): void {
146 // Simple skybox for POC
147 const geometry = new THREE.SphereGeometry(1000, 32, 32)
148 const material = new THREE.MeshBasicMaterial({
149 color: 0x87ceeb,
150 side: THREE.BackSide
151 })
152 this.skyMesh = new THREE.Mesh(geometry, material)
153 this.scene.add(this.skyMesh)
154 }
155
156 private onResize(): void {
157 const canvas = engine.canvas
158 this.renderer.setSize(canvas.width, canvas.height, false)
159 this.camera.aspect = canvas.width / canvas.height
160 this.camera.updateProjectionMatrix()
161 }
162
163 gameInit(): void {
164 console.log('✓ LittleJS + Nordic Skating POC initialized')
165 console.log(' Controls:')
166 console.log(' - WASD/Arrows: Steer')
167 console.log(' - W/S or Up/Down: Change pace')
168 console.log(' - M: Toggle mute')
169 console.log(' - R: Random lake')
170 }
171
172 gameUpdate(): void {
173 // Handle input using LittleJS
174 this.handleInput()
175
176 // Update skater using existing physics
177 const dt = engine.timing.lastTimeStep
178 stepSkater(this.skater, this.lake, this.steerValue, dt)
179 }
180
181 private handleInput(): void {
182 // Keyboard steering
183 let steer = 0
184 steer += inputGetState('arrowLeft') ? -1 : 0
185 steer += inputGetState('arrowRight') ? 1 : 0
186 steer += inputGetState('a') ? -1 : 0
187 steer += inputGetState('d') ? 1 : 0
188
189 // Mouse/touch steering
190 steer += inputGetAxis('mouseX') * 0.5
191
192 // Gamepad steering
193 steer += inputGetAxis('gamepadLeftX')
194
195 // Device orientation (tilt) - LittleJS provides this
196 if ('deviceOrientation' in window) {
197 const gamma = (window as any).DeviceOrientationEvent?.gamma ?? 0
198 const beta = (window as any).DeviceOrientationEvent?.beta ?? 0
199 // Simple tilt steering - would need proper calibration in production
200 steer += (gamma || 0) * 0.02
201 }
202
203 this.steerValue = Math.max(-1, Math.min(1, steer))
204
205 // Other inputs
206 if (inputGetPressed('m')) {
207 this.muted = !this.muted
208 console.log(`Sound ${this.muted ? 'OFF' : 'ON'}`)
209 }
210
211 if (inputGetPressed('r')) {
212 this.seed = Math.floor(Math.random() * 999999) + 1
213 this.lake = createLake(this.seed, { impossibleIce: false })
214 this.skater = createSkater(this.lake, 0)
215 this.skater.pace = 'cruise'
216 console.log(`New lake: ${this.seed}`)
217 }
218
219 // Pace control - use proper pace names from PACES object
220 const paceNames = Object.keys(PACES) as Array<keyof typeof PACES>
221 const currentIndex = paceNames.findIndex(p => p === this.skater.pace)
222
223 if ((inputGetPressed('arrowUp') || inputGetPressed('w')) &&
224 engine.timing.time > this.lastPaceChange + 0.2) {
225 const nextIndex = (currentIndex + 1) % paceNames.length
226 this.skater.pace = paceNames[nextIndex]
227 this.lastPaceChange = engine.timing.time
228 console.log(`Pace: ${this.skater.pace}`)
229 }
230 if ((inputGetPressed('arrowDown') || inputGetPressed('s')) &&
231 engine.timing.time > this.lastPaceChange + 0.2) {
232 const prevIndex = (currentIndex - 1 + paceNames.length) % paceNames.length
233 this.skater.pace = paceNames[prevIndex]
234 this.lastPaceChange = engine.timing.time
235 console.log(`Pace: ${this.skater.pace}`)
236 }
237
238 // Start sound on any input (browser requirement)
239 if (this.steerValue !== 0 ||
240 inputGetPressed('m') ||
241 inputGetPressed('r') ||
242 inputGetPressed('arrowUp') ||
243 inputGetPressed('arrowDown')) {
244 this.startSound()
245 }
246 }
247
248 private startSound(): void {
249 // POC: Just log for now
250 // In production, this would initialize the Web Audio context
251 console.log('Audio context would start here')
252 }
253
254 gameUpdatePost(): void {
255 // Post-update logic can go here
256 }
257
258 gameRender(): void {
259 // Update camera based on skater
260 this.updateCamera()
261
262 // Render Three.js scene
263 this.renderer.render(this.scene, this.camera)
264 }
265
266 private updateCamera(): void {
267 // Calculate push animation
268 const pushFor = pushSeconds(this.skater.pace)
269 const timeInStride = sincePush(this.skater)
270 const push = this.skater.under === 0 && timeInStride < pushFor
271 ? Math.sin((Math.PI * timeInStride) / pushFor)
272 : 0
273
274 // Calculate crouch based on speed
275 const hurry = (this.skater.speed - PACES.cruise.speed) /
276 (PACES.olympic.speed - PACES.cruise.speed)
277 const crouch = hurry > 0
278 ? PACES.olympic.crouch * hurry
279 : PACES.meander.crouch * Math.min(1, -hurry * 2.3)
280
281 // Calculate FOV
282 const fov = BASE_FOV + 12 * Math.max(-0.4, Math.min(1.1, hurry))
283
284 if (Math.abs(this.camera.fov - fov) > 0.05) {
285 this.camera.fov = fov
286 this.camera.updateProjectionMatrix()
287 }
288
289 // Calculate sinking animation
290 const sunk = this.skater.under > 0
291 ? Math.min(1, (3 - this.skater.under) / 0.3)
292 : 0
293
294 // Camera position: first person view
295 this.camera.position.set(
296 this.skater.x,
297 EYE - crouch + 0.03 * push - 1.3 * sunk,
298 this.skater.s
299 )
300
301 // Camera rotation
302 const along = Math.atan(this.skater.channel.slope(this.skater.s))
303 const look = this.skater.heading -
304 0.25 * Math.sin(this.skater.heading - along)
305
306 this.camera.rotation.set(-PITCH, -look, -0.02 * this.skater.turn * this.skater.speed)
307
308 // Update ice mesh position to follow skater (create illusion of endless lake)
309 this.iceMesh.position.set(this.skater.x, -0.1, this.skater.s)
310 this.shoresGroup.position.set(this.skater.x, 0, this.skater.s)
311 }
312
313 gameRenderPost(): void {
314 // Draw HUD using LittleJS
315 const x = 20
316 let y = 40
317 const lineHeight = 30
318
319 // Speed
320 drawTextScreen(
321 `Speed: ${Math.round(this.skater.speed * 3.6)} km/h`,
322 vec2(x, y),
323 24,
324 colorWhite
325 )
326 y += lineHeight
327
328 // Pace
329 const paceKmh = Math.round(PACES[this.skater.pace].speed * 3.6)
330 drawTextScreen(
331 `Pace: ${this.skater.pace} (${paceKmh} km/h)`,
332 vec2(x, y),
333 24,
334 colorWhite
335 )
336 y += lineHeight
337
338 // Lake seed
339 drawTextScreen(
340 `Lake: ${this.seed}`,
341 vec2(x, y),
342 24,
343 colorWhite
344 )
345 y += lineHeight
346
347 // Steering
348 drawTextScreen(
349 `Steer: ${this.steerValue.toFixed(2)}`,
350 vec2(x, y),
351 24,
352 colorWhite
353 )
354 y += lineHeight
355
356 // Skater info
357 drawTextScreen(
358 `Pos: (${this.skater.x.toFixed(1)}, ${this.skater.s.toFixed(1)})`,
359 vec2(x, y),
360 24,
361 colorWhite
362 )
363 y += lineHeight
364
365 // Thickness at current position
366 const thickness = this.lake.thickness(this.skater.x, this.skater.s)
367 drawTextScreen(
368 `Ice: ${(thickness * 100).toFixed(1)} cm`,
369 vec2(x, y),
370 24,
371 colorWhite
372 )
373 y += lineHeight
374
375 // FPS
376 drawTextScreen(
377 `FPS: ${Math.round(engine.timing.fps)}`,
378 vec2(x, y),
379 24,
380 colorWhite
381 )
382
383 // Instructions at bottom
384 drawTextScreen(
385 'WASD/Arrows: Steer | W/S or Up/Down: Change Pace | M: Mute | R: Random Lake',
386 vec2(engine.canvas.width / 2, engine.canvas.height - 30),
387 18,
388 colorWhite,
389 0.5
390 )
391
392 // POC status
393 drawTextScreen(
394 'LittleJS POC - Press any key to start audio context',
395 vec2(engine.canvas.width / 2, engine.canvas.height - 60),
396 16,
397 colorWhite,
398 0.5
399 )
400 }
401}
402
403// Initialize POC when DOM is ready
404if (document.readyState === 'loading') {
405 document.addEventListener('DOMContentLoaded', () => {
406 console.log('Starting Nordic Skating LittleJS POC...')
407 new NordicPOC()
408 })
409} else {
410 console.log('Starting Nordic Skating LittleJS POC...')
411 new NordicPOC()
412}