Ski Game - Day 7
Advanced LevelThe skier stays still; the world scrolls up. This creates the illusion of infinite downhill movement.
const skier = {
x: canvas.width / 2,
y: 100, // Fixed Y position on screen
direction: 0, // -2 to 2 (left to right)
speed: 3 // World scroll speed
};
function update() {
// Skier moves horizontally based on direction
skier.x += skier.direction * 3;
skier.x = Math.max(30, Math.min(canvas.width - 30, skier.x));
// Move ALL world objects UP (creates downhill effect)
for (const obs of obstacles) {
obs.y -= skier.speed * 1.5;
// Also shift horizontally based on player direction
obs.x += skier.direction * 2;
}
// Track distance traveled
distance += skier.speed * 0.3;
}
For infinite games, moving objects toward the player is simpler than managing a camera with infinite scroll.
Obstacles spawn below the screen and move up. Spawn rate increases with speed.
const obstacleTypes = ['tree', 'tree', 'tree', 'rock', 'rock', 'flag'];
function spawnObstacle() {
const type = obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
obstacles.push({
x: Math.random() * (canvas.width - 60) + 30,
y: canvas.height + 50, // Start below screen
type: type,
collected: false // For flags
});
}
function updateObstacles() {
// Remove obstacles that went off screen
obstacles = obstacles.filter(obs => obs.y > -50);
// Spawn probability increases with speed
const spawnChance = 0.03 + (skier.speed * 0.005);
if (Math.random() < spawnChance) {
spawnObstacle();
}
}
Ski tracks show where the player has been. They're stored as points and rendered as lines.
let tracks = [];
function updateTracks() {
if (!skier.crashed) {
// Add current position to tracks
tracks.push({
x: skier.x,
y: skier.y + 20, // Behind skier
direction: skier.direction
});
}
// Move tracks up with world
for (const track of tracks) {
track.y -= skier.speed * 1.5;
track.x += skier.direction * 2;
}
// Remove old tracks
tracks = tracks.filter(t => t.y > -10);
// Limit track count for performance
if (tracks.length > 100) tracks.shift();
}
function drawTracks() {
ctx.strokeStyle = 'rgba(180, 180, 200, 0.5)';
ctx.lineWidth = 2;
// Draw two parallel lines (left and right ski)
for (let i = 1; i < tracks.length; i++) {
const t1 = tracks[i - 1];
const t2 = tracks[i];
// Left ski track
ctx.beginPath();
ctx.moveTo(t1.x - 8, t1.y);
ctx.lineTo(t2.x - 8, t2.y);
ctx.stroke();
// Right ski track
ctx.beginPath();
ctx.moveTo(t1.x + 8, t1.y);
ctx.lineTo(t2.x + 8, t2.y);
ctx.stroke();
}
}
Different obstacle types have different hitboxes. Flags can be collected; trees and rocks cause crashes.
function checkCollisions() {
const skierBox = {
x: skier.x - 10,
y: skier.y - 10,
width: 20,
height: 30
};
for (const obs of obstacles) {
// Different hitboxes per type
let obsBox;
switch (obs.type) {
case 'tree':
obsBox = { x: obs.x - 15, y: obs.y - 20, width: 30, height: 50 };
break;
case 'rock':
obsBox = { x: obs.x - 12, y: obs.y - 8, width: 24, height: 18 };
break;
case 'flag':
obsBox = { x: obs.x - 5, y: obs.y - 15, width: 10, height: 30 };
break;
}
if (isColliding(skierBox, obsBox)) {
if (obs.type === 'flag' && !obs.collected) {
// Collect flag - bonus points!
obs.collected = true;
flags++;
distance += 50;
} else if (obs.type !== 'flag') {
// Crash!
crash();
}
}
}
}
Persist the high score across sessions using localStorage.
// Load high score on startup
let highScore = parseInt(localStorage.getItem('skiHighScore')) || 0;
document.getElementById('highScore').textContent = highScore + 'm';
// Update high score on crash
function crash() {
skier.crashed = true;
gameRunning = false;
const finalScore = Math.floor(distance);
// Check for new high score
if (finalScore > highScore) {
highScore = finalScore;
localStorage.setItem('skiHighScore', highScore);
document.getElementById('highScore').textContent = highScore + 'm';
}
showGameOverScreen(finalScore);
}
localStorage stores strings only. Use parseInt() when reading numbers. Falls back to 0 if not found (|| 0).
Speed increases over time, making the game progressively harder.
const skier = {
speed: 3, // Starting speed
maxSpeed: 8 // Cap to prevent impossible speed
};
function update() {
// Gradually increase speed
if (skier.speed < skier.maxSpeed) {
skier.speed += 0.001; // Very gradual increase
}
// Display current speed (for feedback)
const displaySpeed = Math.floor(skier.speed * 10);
document.getElementById('speed').textContent = displaySpeed;
// Obstacles spawn faster at higher speeds (see section 02)
// This creates natural difficulty progression
}
// Direction affects speed perception
if (keys.left) {
skier.direction = Math.max(skier.direction - 0.15, -2);
} else if (keys.right) {
skier.direction = Math.min(skier.direction + 0.15, 2);
} else {
// Return to center gradually
if (skier.direction > 0) skier.direction = Math.max(0, skier.direction - 0.1);
if (skier.direction < 0) skier.direction = Math.min(0, skier.direction + 0.1);
}