Eiffel Tower Parachute - Day 15
Canvas + PhysicsThe game uses a world coordinate system measured in meters, which is then transformed to screen coordinates. This makes physics calculations intuitive (the Eiffel Tower is 330m tall).
// World constants (in meters)
const TOWER_HEIGHT = 330; // Eiffel Tower height
const GROUND_LEVEL = 0;
// Scale calculation
worldScale = canvasHeight / (TOWER_HEIGHT + 50);
// Transform world coordinates to screen pixels
function worldToScreen(worldX, worldY) {
return {
// Center horizontally
x: canvasWidth / 2 + worldX * worldScale,
// Y from bottom (world Y=0 is ground)
y: canvasHeight - (worldY * worldScale) - 30
};
}
Using real-world units makes the game logic more intuitive:
y = 330 (top of tower)y = 0Coordinate Transformation
The player follows basic physics with gravity, velocity, and air resistance. The simulation runs every frame.
let player = {
x: 0, // Horizontal position (world units)
y: 330, // Vertical position (starts at tower top)
vx: 0, // Horizontal velocity
vy: 0, // Vertical velocity (negative = falling)
parachuteDeployed: false,
landed: false
};
// Physics constants
const GRAVITY = 0.15;
const AIR_RESISTANCE = 0.01;
const MAX_FALL_SPEED = 8;
const HORIZONTAL_SPEED = 0.8;
function update() {
// Apply gravity (accelerates downward)
player.vy -= GRAVITY;
// Player input for horizontal movement
if (keys.left) {
player.vx -= HORIZONTAL_SPEED * 0.1;
}
if (keys.right) {
player.vx += HORIZONTAL_SPEED * 0.1;
}
// Air resistance slows horizontal movement
player.vx *= (1 - AIR_RESISTANCE);
// Terminal velocity - can't fall faster than this
if (player.vy < -MAX_FALL_SPEED) {
player.vy = -MAX_FALL_SPEED;
}
// Update position
player.x += player.vx;
player.y += player.vy;
// Check for landing
if (player.y <= GROUND_LEVEL) {
player.y = GROUND_LEVEL;
player.landed = true;
}
}
Negative velocity means falling (y decreases). Gravity subtracts from vy each frame, making the player accelerate downward.
Wind adds challenge by pushing the player horizontally. It's randomized each jump with varying strength and direction.
let wind = {
strength: 0, // 1-5 scale
direction: 1 // 1 = right, -1 = left
};
function generateWind() {
// Random strength between 1 and 5
wind.strength = 1 + Math.random() * 4;
// Random direction
wind.direction = Math.random() > 0.5 ? 1 : -1;
updateWindDisplay();
}
function update() {
// Wind pushes player horizontally
player.vx += wind.direction * wind.strength * 0.01;
// Wind is constant force, player must compensate
// Strong wind (5) = significant drift
// Weak wind (1) = minor adjustment needed
}
function updateWindDisplay() {
const arrow = document.getElementById('windArrow');
// Arrow direction
arrow.textContent = wind.direction > 0 ? '→' : '←';
// Scale arrow based on wind strength
arrow.style.transform = `scaleX(${wind.strength / 3})`;
}
// Wind particles for visual feedback
function drawWindParticles() {
// Spawn dashes moving in wind direction
if (Math.random() < 0.3) {
windParticles.push({
x: wind.direction > 0 ? -10 : canvasWidth + 10,
y: Math.random() * canvasHeight * 0.8,
speed: wind.strength * 2
});
}
}
Deploying the parachute dramatically changes the physics - increasing drag and slowing the fall.
const PARACHUTE_DRAG = 0.85; // Multiplier each frame
const MAX_PARACHUTE_FALL = 2; // Much slower terminal velocity
function update() {
if (player.parachuteDeployed) {
// High drag - velocity decays quickly
player.vy *= PARACHUTE_DRAG;
player.vx *= 0.95;
// New, much lower terminal velocity
if (player.vy < -MAX_PARACHUTE_FALL) {
player.vy = -MAX_PARACHUTE_FALL;
}
} else {
// Normal freefall physics
player.vx *= (1 - AIR_RESISTANCE);
if (player.vy < -MAX_FALL_SPEED) {
player.vy = -MAX_FALL_SPEED;
}
}
}
function onKeyDown(e) {
if (e.key === ' ') {
// Can only deploy once, while falling, not landed
if (!player.parachuteDeployed && gameRunning && !player.landed) {
player.parachuteDeployed = true;
}
e.preventDefault();
}
}
Velocity Comparison
Players who deploy the parachute get a 20% score bonus for controlled landings, encouraging skillful timing.
Targets are circles on the ground. We check if the player lands within a target's radius and award points based on accuracy.
function generateTargets() {
targets = [];
// 3-5 targets per jump
const numTargets = 3 + Math.floor(Math.random() * 3);
const spacing = 300 / numTargets;
for (let i = 0; i < numTargets; i++) {
// Spread horizontally with some randomness
const baseX = (i - numTargets/2 + 0.5) * spacing;
const x = baseX + (Math.random() - 0.5) * 50;
// Target type determines size and points
const type = Math.random();
let radius, points, color;
if (type < 0.2) {
// Bullseye: small, 100 points
radius = 15;
points = 100;
color = '#ffd700';
} else if (type < 0.5) {
// Medium: 50 points
radius = 30;
points = 50;
color = '#44ff44';
} else {
// Large: 25 points
radius = 50;
points = 25;
color = '#44aaff';
}
targets.push({ x, y: GROUND_LEVEL, radius, points, color });
}
}
function handleLanding() {
let landedOnTarget = null;
let minDist = Infinity;
// Check each target
for (const target of targets) {
// Distance from player to target center
const dist = Math.abs(player.x - target.x);
// Within radius? And closer than previous matches?
if (dist < target.radius && dist < minDist) {
minDist = dist;
landedOnTarget = target;
}
}
if (landedOnTarget) {
// Accuracy bonus: closer to center = more points
const accuracy = 1 - (minDist / landedOnTarget.radius);
let points = Math.floor(landedOnTarget.points * (1 + accuracy));
// Parachute bonus
if (player.parachuteDeployed) {
points = Math.floor(points * 1.2);
}
score += points;
} else {
// Missed all targets - consolation points
score += 5;
}
}
We use 1D collision (horizontal distance only) since targets are on the ground. A full 2D check would compare distance to radius in both dimensions.
The Eiffel Tower is drawn using Canvas path operations, creating its iconic silhouette with layered detail.
function drawEiffelTower() {
const baseY = canvasHeight - 30;
const topPos = worldToScreen(0, TOWER_HEIGHT);
const towerWidth = 80;
ctx.fillStyle = '#5D4837';
ctx.strokeStyle = '#4A3728';
// Draw iconic tapered shape
ctx.beginPath();
// Left leg - curves inward as we go up
ctx.moveTo(canvasWidth/2 - towerWidth, baseY);
ctx.lineTo(canvasWidth/2 - towerWidth * 0.7, baseY - canvasHeight * 0.3);
ctx.lineTo(canvasWidth/2 - towerWidth * 0.4, baseY - canvasHeight * 0.55);
ctx.lineTo(canvasWidth/2 - towerWidth * 0.15, baseY - canvasHeight * 0.75);
ctx.lineTo(canvasWidth/2, topPos.y);
// Right leg - mirror of left
ctx.lineTo(canvasWidth/2 + towerWidth * 0.15, baseY - canvasHeight * 0.75);
ctx.lineTo(canvasWidth/2 + towerWidth * 0.4, baseY - canvasHeight * 0.55);
ctx.lineTo(canvasWidth/2 + towerWidth * 0.7, baseY - canvasHeight * 0.3);
ctx.lineTo(canvasWidth/2 + towerWidth, baseY);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// Horizontal viewing platforms
ctx.fillStyle = '#6B5344';
// First platform (bottom observation deck)
const p1Y = baseY - canvasHeight * 0.18;
ctx.fillRect(canvasWidth/2 - towerWidth * 0.85, p1Y, towerWidth * 1.7, 8);
// Second platform (middle)
const p2Y = baseY - canvasHeight * 0.45;
ctx.fillRect(canvasWidth/2 - towerWidth * 0.5, p2Y, towerWidth, 6);
// Top platform
const p3Y = baseY - canvasHeight * 0.72;
ctx.fillRect(canvasWidth/2 - towerWidth * 0.2, p3Y, towerWidth * 0.4, 4);
// Lattice effect with horizontal lines
ctx.strokeStyle = '#3D2817';
ctx.lineWidth = 1;
for (let i = 0; i < 6; i++) {
const y = baseY - canvasHeight * (0.1 + i * 0.1);
const spread = towerWidth * (1 - i * 0.12);
ctx.beginPath();
ctx.moveTo(canvasWidth/2 - spread, y);
ctx.lineTo(canvasWidth/2 + spread, y);
ctx.stroke();
}
The tower uses percentages of canvas height rather than world coordinates, ensuring it always fits the screen properly regardless of scale.