1- const canvas = document . getElementById ( "gameCanvas" ) ;
2- const ctx = canvas . getContext ( "2d" ) ;
3- canvas . width = window . innerWidth ;
4- canvas . height = window . innerHeight ;
5-
6- let angleX = 0 ;
7- let angleY = 0 ;
8- let velocity = { x : 0 , y : 0 } ;
9- let marble = { x : 50 , y : 50 , r : 10 } ;
10- const startPoint = { x : 50 , y : 50 } ;
11- const goal = { x : canvas . width - 60 , y : canvas . height - 60 , r : 20 } ;
12- const holes = [
13- { x : 150 , y : 150 , r : 15 } ,
14- { x : 300 , y : 200 , r : 15 } ,
15- { x : 400 , y : 400 , r : 15 }
16- ] ;
17- const friction = 0.98 ;
18- let sensitivity = 0.5 ;
19-
20- // Für Reset-Button: Offset für Neutralstellung
21- let angleXOffset = 0 ;
22- let angleYOffset = 0 ;
23-
24- const keys = { } ;
25- document . addEventListener ( 'keydown' , e => keys [ e . key . toLowerCase ( ) ] = true ) ;
26- document . addEventListener ( 'keyup' , e => keys [ e . key . toLowerCase ( ) ] = false ) ;
27-
28- // Mobile tilt support
29- if ( window . DeviceMotionEvent ) {
30- window . addEventListener ( 'devicemotion' , e => {
31- if ( e . accelerationIncludingGravity ) {
32- // Offset berücksichtigen
33- angleX = e . accelerationIncludingGravity . x * - 0.1 - angleXOffset ;
34- angleY = e . accelerationIncludingGravity . y * 0.1 - angleYOffset ;
35- }
36- } ) ;
37- }
1+ class Game {
2+ constructor ( canvasId ) {
3+ this . canvas = document . getElementById ( canvasId ) ;
4+ this . ctx = this . canvas . getContext ( "2d" ) ;
5+
6+ // Game state
7+ this . gameState = 'loading' ; // 'loading', 'playing', 'won'
8+ this . assets = { } ;
9+ this . friction = 0.98 ;
10+ this . sensitivity = 0.5 ;
11+
12+ // Input state
13+ this . keys = { } ;
14+ this . tilt = { x : 0 , y : 0 } ;
15+ this . tiltOffset = { x : 0 , y : 0 } ;
16+
17+ // Game objects (will be defined in resize)
18+ this . marble = { } ;
19+ this . goal = { } ;
20+ this . holes = [ ] ;
21+ this . level = {
22+ start : { x : 0.1 , y : 0.1 } ,
23+ goal : { x : 0.9 , y : 0.9 , r : 0.05 } ,
24+ holes : [
25+ { x : 0.3 , y : 0.4 , r : 0.04 } ,
26+ { x : 0.6 , y : 0.2 , r : 0.04 } ,
27+ { x : 0.7 , y : 0.7 , r : 0.04 } ,
28+ { x : 0.2 , y : 0.8 , r : 0.04 } ,
29+ ]
30+ } ;
31+
32+ // UI Elements
33+ this . resetTiltBtn = document . getElementById ( 'resetTiltBtn' ) ;
34+ this . sensitivitySlider = document . getElementById ( 'sensitivitySlider' ) ;
35+ this . winMessage = document . getElementById ( 'win-message' ) ;
36+ this . playAgainBtn = document . getElementById ( 'play-again-btn' ) ;
37+ }
3838
39- function restartGame ( ) {
40- marble . x = startPoint . x ;
41- marble . y = startPoint . y ;
42- velocity = { x : 0 , y : 0 } ;
43- }
39+ async init ( ) {
40+ await this . loadAssets ( ) ;
41+ this . setupEventListeners ( ) ;
42+ this . resize ( ) ; // Initial size calculation
43+ this . restart ( ) ;
44+ this . gameState = 'playing' ;
45+ this . gameLoop ( ) ;
46+ }
4447
45- // --- NEU: Reset-Button und Sensitivitäts-Slider ---
46- const resetBtn = document . getElementById ( "resetBtn" ) ;
47- const sensitivitySlider = document . getElementById ( "sensitivitySlider" ) ;
48- const sensitivityValue = document . getElementById ( "sensitivityValue" ) ;
49-
50- resetBtn . addEventListener ( "click" , ( ) => {
51- // Aktuelle Werte als Offset speichern
52- angleXOffset = angleX + angleXOffset ;
53- angleYOffset = angleY + angleYOffset ;
54- } ) ;
55-
56- sensitivitySlider . addEventListener ( "input" , ( ) => {
57- sensitivity = Number ( sensitivitySlider . value ) ;
58- sensitivityValue . textContent = sensitivity . toFixed ( 2 ) ;
59- } ) ;
60- // Initialwert anzeigen
61- sensitivityValue . textContent = sensitivitySlider . value ;
62-
63- // ---------------------------------------------------
64-
65- function draw ( ) {
66- ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
67-
68- // Goal
69- ctx . fillStyle = "green" ;
70- ctx . beginPath ( ) ;
71- ctx . arc ( goal . x , goal . y , goal . r , 0 , Math . PI * 2 ) ;
72- ctx . fill ( ) ;
73-
74- // Holes
75- ctx . fillStyle = "black" ;
76- for ( const hole of holes ) {
77- ctx . beginPath ( ) ;
78- ctx . arc ( hole . x , hole . y , hole . r , 0 , Math . PI * 2 ) ;
79- ctx . fill ( ) ;
48+ loadAssets ( ) {
49+ const assetPromises = [
50+ this . loadImage ( 'marble' , 'assets/marble.png' ) ,
51+ this . loadImage ( 'hole' , 'assets/hole.png' ) ,
52+ this . loadImage ( 'goal' , 'assets/goal.png' ) ,
53+ this . loadImage ( 'background' , 'assets/background.jpg' ) ,
54+ ] ;
55+ return Promise . all ( assetPromises ) ;
8056 }
8157
82- // Marble
83- ctx . fillStyle = "blue" ;
84- ctx . beginPath ( ) ;
85- ctx . arc ( marble . x , marble . y , marble . r , 0 , Math . PI * 2 ) ;
86- ctx . fill ( ) ;
58+ loadImage ( key , src ) {
59+ return new Promise ( ( resolve , reject ) => {
60+ const img = new Image ( ) ;
61+ img . src = src ;
62+ img . onload = ( ) => {
63+ this . assets [ key ] = img ;
64+ resolve ( ) ;
65+ } ;
66+ img . onerror = reject ;
67+ } ) ;
68+ }
8769
88- // Debug
89- ctx . fillStyle = "black" ;
90- ctx . font = "14px sans-serif" ;
91- ctx . fillText ( `angleX: ${ ( angleX + angleXOffset ) . toFixed ( 2 ) } angleY: ${ ( angleY + angleYOffset ) . toFixed ( 2 ) } sensitivity: ${ sensitivity . toFixed ( 2 ) } ` , 10 , 40 ) ;
92- }
70+ setupEventListeners ( ) {
71+ // Keyboard
72+ document . addEventListener ( 'keydown' , e => this . keys [ e . key . toLowerCase ( ) ] = true ) ;
73+ document . addEventListener ( 'keyup' , e => this . keys [ e . key . toLowerCase ( ) ] = false ) ;
74+
75+ // Device Tilt
76+ if ( window . DeviceMotionEvent ) {
77+ window . addEventListener ( 'devicemotion' , e => {
78+ if ( e . accelerationIncludingGravity ) {
79+ // Adjust axis for intuitive control
80+ this . tilt . x = e . accelerationIncludingGravity . x * 2 ;
81+ this . tilt . y = e . accelerationIncludingGravity . y * - 2 ;
82+ }
83+ } ) ;
84+ }
9385
94- function update ( ) {
95- // WASD simulation
96- let keyboardAngleX = 0 ;
97- let keyboardAngleY = 0 ;
98- let keyboard_sensitivity = 5 ;
99- if ( keys [ 'w' ] ) keyboardAngleY -= keyboard_sensitivity ;
100- if ( keys [ 's' ] ) keyboardAngleY += keyboard_sensitivity ;
101- if ( keys [ 'a' ] ) keyboardAngleX -= keyboard_sensitivity ;
102- if ( keys [ 'd' ] ) keyboardAngleX += keyboard_sensitivity ;
103-
104- // Kombiniere Tastatur und DeviceMotion
105- const totalAngleX = ( angleX + angleXOffset ) + keyboardAngleX ;
106- const totalAngleY = ( angleY + angleYOffset ) + keyboardAngleY ;
107-
108- velocity . x += totalAngleX * sensitivity ;
109- velocity . y += totalAngleY * sensitivity ;
110- velocity . x *= friction ;
111- velocity . y *= friction ;
112-
113- marble . x += velocity . x ;
114- marble . y += velocity . y ;
115-
116- // Wall clamp
117- marble . x = Math . max ( marble . r , Math . min ( canvas . width - marble . r , marble . x ) ) ;
118- marble . y = Math . max ( marble . r , Math . min ( canvas . height - marble . r , marble . y ) ) ;
119-
120- // Hole check
121- for ( const hole of holes ) {
122- const dx = marble . x - hole . x ;
123- const dy = marble . y - hole . y ;
124- const dist = Math . sqrt ( dx * dx + dy * dy ) ;
125- if ( dist < marble . r + hole . r ) {
126- restartGame ( ) ;
127- return ;
86+ // Window Resize/Orientation Change
87+ window . addEventListener ( 'resize' , ( ) => this . resize ( ) ) ;
88+
89+ // UI Controls
90+ this . resetTiltBtn . addEventListener ( 'click' , ( ) => {
91+ this . tiltOffset . x = this . tilt . x ;
92+ this . tiltOffset . y = this . tilt . y ;
93+ } ) ;
94+ this . sensitivitySlider . addEventListener ( 'input' , e => this . sensitivity = Number ( e . target . value ) ) ;
95+ this . playAgainBtn . addEventListener ( 'click' , ( ) => this . restart ( ) ) ;
96+ }
97+
98+ resize ( ) {
99+ const size = this . canvas . parentElement . getBoundingClientRect ( ) . width ;
100+ this . canvas . width = size ;
101+ this . canvas . height = size ;
102+
103+ // Recalculate all game object sizes and positions based on the new canvas size
104+ const scale = ( obj , relativePos ) => {
105+ obj . x = relativePos . x * size ;
106+ obj . y = relativePos . y * size ;
107+ obj . r = ( relativePos . r || 0.025 ) * size ; // Default marble radius
128108 }
109+
110+ scale ( this . marble , this . level . start ) ;
111+ scale ( this . goal , this . level . goal ) ;
112+ this . holes = this . level . holes . map ( h => {
113+ const hole = { } ;
114+ scale ( hole , h ) ;
115+ return hole ;
116+ } ) ;
117+
118+ // Force a redraw
119+ this . draw ( ) ;
129120 }
130121
131- // Goal check
132- const dx = marble . x - goal . x ;
133- const dy = marble . y - goal . y ;
134- const dist = Math . sqrt ( dx * dx + dy * dy ) ;
135- if ( dist < marble . r + goal . r ) {
136- alert ( "You reached the goal!" ) ;
137- restartGame ( ) ;
122+ restart ( ) {
123+ const size = this . canvas . width ;
124+ this . marble . x = this . level . start . x * size ;
125+ this . marble . y = this . level . start . y * size ;
126+ this . marble . vx = 0 ;
127+ this . marble . vy = 0 ;
128+
129+ this . winMessage . classList . add ( 'hidden' ) ;
130+ this . gameState = 'playing' ;
138131 }
139- }
140132
141- function gameLoop ( ) {
142- update ( ) ;
143- draw ( ) ;
144- requestAnimationFrame ( gameLoop ) ;
133+ update ( ) {
134+ if ( this . gameState !== 'playing' ) return ;
135+
136+ // 1. Calculate input force
137+ let forceX = 0 ;
138+ let forceY = 0 ;
139+
140+ // Keyboard input (for desktop)
141+ const keySensitivity = 5 ;
142+ if ( this . keys [ 'w' ] || this . keys [ 'arrowup' ] ) forceY -= keySensitivity ;
143+ if ( this . keys [ 's' ] || this . keys [ 'arrowdown' ] ) forceY += keySensitivity ;
144+ if ( this . keys [ 'a' ] || this . keys [ 'arrowleft' ] ) forceX -= keySensitivity ;
145+ if ( this . keys [ 'd' ] || this . keys [ 'arrowright' ] ) forceX += keySensitivity ;
146+
147+ // Tilt input (for mobile)
148+ const finalTiltX = this . tilt . x - this . tiltOffset . x ;
149+ const finalTiltY = this . tilt . y - this . tiltOffset . y ;
150+ forceX += finalTiltX ;
151+ forceY -= finalTiltY ; // Invert Y-axis for natural feel
152+
153+ // 2. Apply forces to velocity
154+ this . marble . vx += forceX * this . sensitivity * 0.1 ;
155+ this . marble . vy += forceY * this . sensitivity * 0.1 ;
156+
157+ // 3. Apply friction
158+ this . marble . vx *= this . friction ;
159+ this . marble . vy *= this . friction ;
160+
161+ // 4. Update position
162+ this . marble . x += this . marble . vx ;
163+ this . marble . y += this . marble . vy ;
164+
165+ // 5. Collision detection
166+ // Walls
167+ if ( this . marble . x < this . marble . r ) { this . marble . x = this . marble . r ; this . marble . vx *= - 0.5 ; }
168+ if ( this . marble . x > this . canvas . width - this . marble . r ) { this . marble . x = this . canvas . width - this . marble . r ; this . marble . vx *= - 0.5 ; }
169+ if ( this . marble . y < this . marble . r ) { this . marble . y = this . marble . r ; this . marble . vy *= - 0.5 ; }
170+ if ( this . marble . y > this . canvas . height - this . marble . r ) { this . marble . y = this . canvas . height - this . marble . r ; this . marble . vy *= - 0.5 ; }
171+
172+ // Holes
173+ for ( const hole of this . holes ) {
174+ if ( this . checkCollision ( this . marble , hole ) ) {
175+ this . restart ( ) ;
176+ return ;
177+ }
178+ }
179+
180+ // Goal
181+ if ( this . checkCollision ( this . marble , this . goal ) ) {
182+ this . gameState = 'won' ;
183+ this . winMessage . classList . remove ( 'hidden' ) ;
184+ }
185+ }
186+
187+ checkCollision ( obj1 , obj2 ) {
188+ const dx = obj1 . x - obj2 . x ;
189+ const dy = obj1 . y - obj2 . y ;
190+ const distance = Math . sqrt ( dx * dx + dy * dy ) ;
191+ // Fall in if center is over the hole radius
192+ return distance < obj2 . r ;
193+ }
194+
195+ draw ( ) {
196+ this . ctx . clearRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ;
197+
198+ // Background
199+ if ( this . assets . background ) {
200+ this . ctx . drawImage ( this . assets . background , 0 , 0 , this . canvas . width , this . canvas . height ) ;
201+ } else {
202+ this . ctx . fillStyle = '#d2b48c' ; // Fallback color
203+ this . ctx . fillRect ( 0 , 0 , this . canvas . width , this . canvas . height ) ;
204+ }
205+
206+ // Draw helper function for sprites
207+ const drawSprite = ( asset , obj ) => {
208+ if ( this . assets [ asset ] ) {
209+ this . ctx . drawImage ( this . assets [ asset ] , obj . x - obj . r , obj . y - obj . r , obj . r * 2 , obj . r * 2 ) ;
210+ }
211+ } ;
212+
213+ // Draw Goal, Holes, and Marble
214+ drawSprite ( 'goal' , this . goal ) ;
215+ this . holes . forEach ( hole => drawSprite ( 'hole' , hole ) ) ;
216+ drawSprite ( 'marble' , this . marble ) ;
217+ }
218+
219+ gameLoop ( ) {
220+ this . update ( ) ;
221+ this . draw ( ) ;
222+ requestAnimationFrame ( ( ) => this . gameLoop ( ) ) ;
223+ }
145224}
146225
147- gameLoop ( ) ;
226+ // Kickstart the game
227+ const game = new Game ( 'gameCanvas' ) ;
228+ game . init ( ) ;
0 commit comments