Skip to content

Commit d68844f

Browse files
authored
Merge pull request #1 from nmilosevic/adapt-for-mobile
Adapt for mobile
2 parents 3d4e950 + b7d08e5 commit d68844f

4 files changed

Lines changed: 479 additions & 20 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Nenad Milosevic
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,9 @@ green-wave-game/
6666

6767
## License
6868

69-
Feel free to use, modify, and share.
69+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
70+
71+
## Credits
72+
73+
- **Game design & development** - Original concept and implementation
74+
- **Mobile adaptations** - Responsive design, touch controls, landscape optimization, and UI refinement with assistance from Claude Haiku

game.js

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@
44
const canvas = document.getElementById('gameCanvas');
55
const ctx = canvas.getContext('2d');
66

7+
// Canvas scaling for responsive design
8+
function resizeCanvas() {
9+
const wrapper = document.getElementById('canvasWrapper');
10+
const displayWidth = wrapper.clientWidth;
11+
const displayHeight = wrapper.clientHeight;
12+
13+
// Always maintain internal resolution of 1000x400
14+
canvas.width = 1000;
15+
canvas.height = 400;
16+
17+
// Let CSS scale the display
18+
canvas.style.width = displayWidth + 'px';
19+
canvas.style.height = displayHeight + 'px';
20+
}
21+
22+
// Initialize canvas size on load
23+
resizeCanvas();
24+
25+
// Resize canvas on window resize
26+
window.addEventListener('resize', resizeCanvas);
27+
28+
// Handle orientation change on mobile
29+
window.addEventListener('orientationchange', () => {
30+
setTimeout(resizeCanvas, 100);
31+
});
32+
733
// Polyfill for roundRect (Safari < 16, older browsers)
834
if (!ctx.roundRect) {
935
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, radii) {
@@ -132,7 +158,7 @@ const ENDING_DURATION = 12; // seconds for full animation
132158
const levels = [
133159
{
134160
// Tutorial: Just one light with long green, teaches basic controls
135-
name: "First Light",
161+
name: "First light",
136162
startSpeed: 40,
137163
lights: [
138164
{ x: 600, greenDuration: 4, redDuration: 2, offset: 0 },
@@ -141,7 +167,7 @@ const levels = [
141167
},
142168
{
143169
// Two lights, introduces timing between lights
144-
name: "Easy Start",
170+
name: "Easy start",
145171
startSpeed: 40,
146172
lights: [
147173
{ x: 500, greenDuration: 3.5, redDuration: 2, offset: 0 },
@@ -151,7 +177,7 @@ const levels = [
151177
},
152178
{
153179
// Three lights, first real challenge
154-
name: "Finding the Rhythm",
180+
name: "Finding the rhythm",
155181
startSpeed: 45,
156182
lights: [
157183
{ x: 500, greenDuration: 3, redDuration: 2, offset: 0 },
@@ -162,7 +188,7 @@ const levels = [
162188
},
163189
{
164190
// Four lights with tighter timing
165-
name: "Keep the Pace",
191+
name: "Keep the pace",
166192
startSpeed: 50,
167193
lights: [
168194
{ x: 400, greenDuration: 2.5, redDuration: 2.5, offset: 0 },
@@ -174,7 +200,7 @@ const levels = [
174200
},
175201
{
176202
// Mixed timing requires speed adjustment
177-
name: "Speed Adjustment",
203+
name: "Speed adjustment",
178204
startSpeed: 60,
179205
lights: [
180206
{ x: 400, greenDuration: 2, redDuration: 3, offset: 0 },
@@ -187,7 +213,7 @@ const levels = [
187213
},
188214
{
189215
// Long level with many lights
190-
name: "The Long Road",
216+
name: "The long road",
191217
startSpeed: 55,
192218
lights: [
193219
{ x: 350, greenDuration: 2, redDuration: 2, offset: 0 },
@@ -202,7 +228,7 @@ const levels = [
202228
},
203229
{
204230
// Short greens, requires patience and precise timing
205-
name: "Patience Required",
231+
name: "Patience required",
206232
startSpeed: 70,
207233
lights: [
208234
{ x: 400, greenDuration: 1.5, redDuration: 3, offset: 0 },
@@ -311,7 +337,13 @@ function getCurrentPhaseDuration(light, time) {
311337
}
312338
}
313339

314-
// Input handling
340+
// Detect device type
341+
const isMobileDevice = () => {
342+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
343+
|| window.matchMedia('(max-width: 768px)').matches;
344+
};
345+
346+
// Input handling - Keyboard
315347
document.addEventListener('keydown', (e) => {
316348
if (e.key === 'w' || e.key === 'W' || e.key === 'ArrowUp') {
317349
keys.gas = true;
@@ -343,6 +375,60 @@ document.addEventListener('keydown', (e) => {
343375
}
344376
});
345377

378+
// Mobile touch controls
379+
const gasButton = document.getElementById('gasButton');
380+
const brakeButton = document.getElementById('brakeButton');
381+
const restartButton = document.getElementById('restartButton');
382+
383+
// Prevent double-tap zoom on mobile buttons
384+
gasButton.addEventListener('touchstart', (e) => {
385+
e.preventDefault();
386+
keys.gas = true;
387+
}, { passive: false });
388+
389+
gasButton.addEventListener('touchend', (e) => {
390+
e.preventDefault();
391+
keys.gas = false;
392+
}, { passive: false });
393+
394+
gasButton.addEventListener('mousedown', () => {
395+
keys.gas = true;
396+
});
397+
398+
gasButton.addEventListener('mouseup', () => {
399+
keys.gas = false;
400+
});
401+
402+
gasButton.addEventListener('mouseleave', () => {
403+
keys.gas = false;
404+
});
405+
406+
brakeButton.addEventListener('touchstart', (e) => {
407+
e.preventDefault();
408+
keys.brake = true;
409+
}, { passive: false });
410+
411+
brakeButton.addEventListener('touchend', (e) => {
412+
e.preventDefault();
413+
keys.brake = false;
414+
}, { passive: false });
415+
416+
brakeButton.addEventListener('mousedown', () => {
417+
keys.brake = true;
418+
});
419+
420+
brakeButton.addEventListener('mouseup', () => {
421+
keys.brake = false;
422+
});
423+
424+
brakeButton.addEventListener('mouseleave', () => {
425+
keys.brake = false;
426+
});
427+
428+
restartButton.addEventListener('click', () => {
429+
initLevel(currentLevel);
430+
});
431+
346432
// Message display
347433
function showMessage(title, text, buttonText, buttonAction) {
348434
messageTitle.textContent = title;
@@ -375,11 +461,11 @@ function winLevel() {
375461
const stars = calculateStars(totalSpeedChange, level.finishX);
376462
const starDisplay = getStarDisplay(stars);
377463

378-
let timeText = `Time: ${formatTime(finishTime)}s`;
464+
let timeText = `Time: ${formatTime(finishTime)} s`;
379465
if (isNewRecord) {
380-
timeText += ' - New Record!';
466+
timeText += ' - New record!';
381467
} else if (bestTime) {
382-
timeText += ` (Best: ${formatTime(bestTime)}s)`;
468+
timeText += ` (Best: ${formatTime(bestTime)} s)`;
383469
}
384470

385471
if (currentLevel >= levels.length) {
@@ -1528,5 +1614,25 @@ function gameLoop(timestamp) {
15281614
}
15291615

15301616
// Start the game
1531-
initLevel(1);
1532-
requestAnimationFrame(gameLoop);
1617+
let gameStarted = false;
1618+
1619+
const startScreen = document.getElementById('startScreen');
1620+
const startButton = document.getElementById('startButton');
1621+
1622+
// Check if we should skip to ending animation for testing
1623+
const urlParams = new URLSearchParams(window.location.search);
1624+
const skipToEnding = urlParams.has('ending');
1625+
1626+
startButton.addEventListener('click', () => {
1627+
gameStarted = true;
1628+
startScreen.classList.add('hidden');
1629+
1630+
if (skipToEnding) {
1631+
gameState = 'ending';
1632+
endingTime = 0;
1633+
} else {
1634+
initLevel(1);
1635+
}
1636+
1637+
requestAnimationFrame(gameLoop);
1638+
});

0 commit comments

Comments
 (0)