Skip to content

Commit dbefaf1

Browse files
authored
rewrite
1 parent a3dbcbe commit dbefaf1

File tree

13 files changed

+405
-227
lines changed

13 files changed

+405
-227
lines changed

marble_webapp/app.js

Lines changed: 215 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,228 @@
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();
80.1 KB
Loading

marble_webapp/assets/goal.png

411 KB
Loading

marble_webapp/assets/hole.png

348 KB
Loading

marble_webapp/assets/hole.xcf

2.15 MB
Binary file not shown.

marble_webapp/assets/icon-192.png

1.9 KB
Loading

marble_webapp/assets/icon-512.png

63.2 KB
Loading

marble_webapp/assets/marble.png

60.9 KB
Loading

marble_webapp/assets/marble.xcf

128 KB
Binary file not shown.

0 commit comments

Comments
 (0)