Files
gamercomp/games/samples/bridge-builder.html
T
2026-04-30 02:14:25 +00:00

488 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Bridge Builder</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #87ceeb; touch-action: none; font-family: Arial, sans-serif; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<script>
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
let musicEnabled = true;
let sfxEnabled = true;
let musicPlaying = false;
function initAudio() {
if (!audioCtx) audioCtx = new AudioContext();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
// Construction melody
const melody = [
{n: 392, d: 0.3}, {n: 440, d: 0.3}, {n: 494, d: 0.3}, {n: 523, d: 0.6},
{n: 494, d: 0.3}, {n: 440, d: 0.3}, {n: 392, d: 0.6},
{n: 349, d: 0.3}, {n: 392, d: 0.3}, {n: 440, d: 0.3}, {n: 494, d: 0.6},
{n: 523, d: 0.3}, {n: 587, d: 0.3}, {n: 523, d: 0.6}, {n: 0, d: 0.3}
];
const bass = [
{n: 98, d: 0.6}, {n: 110, d: 0.6}, {n: 131, d: 0.6}, {n: 110, d: 0.6},
{n: 87, d: 0.6}, {n: 98, d: 0.6}, {n: 110, d: 0.6}, {n: 131, d: 0.6}
];
let melodyIdx = 0, bassIdx = 0, nextMelody = 0, nextBass = 0;
function playTone(freq, dur, type = 'square', vol = 0.08) {
if (!audioCtx || !musicEnabled || freq === 0) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.value = freq;
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur * 0.9);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + dur);
}
function playSfx(type) {
if (!audioCtx || !sfxEnabled) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
if (type === 'build') {
osc.frequency.value = 500;
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
} else if (type === 'release') {
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(200, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
} else if (type === 'success') {
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.15);
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.45);
} else if (type === 'fail') {
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
osc.type = 'sawtooth';
}
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.5);
}
function updateMusic() {
if (!audioCtx || !musicEnabled || !musicPlaying) return;
const now = audioCtx.currentTime;
if (now >= nextMelody) {
const note = melody[melodyIdx];
playTone(note.n, note.d, 'triangle', 0.06);
nextMelody = now + note.d;
melodyIdx = (melodyIdx + 1) % melody.length;
}
if (now >= nextBass) {
const note = bass[bassIdx];
playTone(note.n, note.d, 'triangle', 0.1);
nextBass = now + note.d;
bassIdx = (bassIdx + 1) % bass.length;
}
}
function startMusic() {
if (!musicPlaying && musicEnabled) {
initAudio();
musicPlaying = true;
nextMelody = audioCtx.currentTime;
nextBass = audioCtx.currentTime;
}
}
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
let W, H, safeTop, safeBottom;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
safeTop = H * 0.1;
safeBottom = H * 0.9;
}
resize();
window.addEventListener('resize', resize);
let gameRunning = false;
let gameOver = false;
let levelComplete = false;
let score = 0;
let level = 1;
// Game state
let building = false;
let bridgeAngle = 0;
let bridgeLength = 0;
let testingBridge = false;
let car = null;
let leftCliff = { x: 0, y: 0, w: 0, h: 0 };
let rightCliff = { x: 0, y: 0, w: 0, h: 0 };
let gap = 0;
function generateLevel() {
const availH = safeBottom - safeTop;
const cliffY = safeTop + availH * 0.6;
const cliffH = safeBottom - cliffY + 50;
// Gap increases with level
gap = 100 + level * 30 + Math.random() * 50;
const leftW = 80 + Math.random() * 60;
const rightW = 80 + Math.random() * 60;
leftCliff = { x: 0, y: cliffY, w: W / 2 - gap / 2, h: cliffH };
rightCliff = { x: W / 2 + gap / 2, y: cliffY, w: W - (W / 2 + gap / 2), h: cliffH };
bridgeAngle = 0;
bridgeLength = 0;
building = false;
testingBridge = false;
car = null;
levelComplete = false;
}
function init() {
score = 0;
level = 1;
gameOver = false;
gameRunning = true;
generateLevel();
startMusic();
}
function startBuilding() {
if (testingBridge || levelComplete || gameOver) return;
building = true;
bridgeLength = 0;
playSfx('build');
}
function stopBuilding() {
if (!building) return;
building = false;
testingBridge = true;
playSfx('release');
// Start the car
car = {
x: leftCliff.w - 40,
y: leftCliff.y - 20,
w: 40,
h: 20,
vx: 2,
vy: 0,
onBridge: false,
finished: false
};
}
document.addEventListener('keydown', e => {
initAudio();
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (gameOver) init();
else if (levelComplete) {
level++;
generateLevel();
} else if (!building && !testingBridge) {
startBuilding();
}
}
});
document.addEventListener('keyup', e => {
if (e.key === ' ' || e.key === 'Enter') {
if (building) stopBuilding();
}
});
canvas.addEventListener('mousedown', e => {
initAudio();
if (gameOver) init();
else if (levelComplete) { level++; generateLevel(); }
else if (!building && !testingBridge) startBuilding();
});
canvas.addEventListener('mouseup', () => {
if (building) stopBuilding();
});
canvas.addEventListener('touchstart', e => {
e.preventDefault();
initAudio();
if (gameOver) init();
else if (levelComplete) { level++; generateLevel(); }
else if (!building && !testingBridge) startBuilding();
});
canvas.addEventListener('touchend', e => {
e.preventDefault();
if (building) stopBuilding();
});
function update() {
if (!gameRunning || gameOver) return;
// Build bridge
if (building) {
bridgeLength += 2;
// Slight random wobble while building
bridgeAngle = Math.sin(bridgeLength * 0.1) * 0.02;
}
// Test bridge with car
if (testingBridge && car) {
const bridgeStartX = leftCliff.w;
const bridgeStartY = leftCliff.y;
const bridgeEndX = bridgeStartX + bridgeLength;
const bridgeEndY = bridgeStartY + Math.tan(bridgeAngle) * bridgeLength;
// Car physics
car.vy += 0.3; // Gravity
car.x += car.vx;
car.y += car.vy;
// Check if car is on cliff
if (car.x + car.w > 0 && car.x < leftCliff.w && car.y + car.h >= leftCliff.y) {
car.y = leftCliff.y - car.h;
car.vy = 0;
}
// Check if car is on bridge
if (car.x + car.w > bridgeStartX && car.x < bridgeEndX) {
const t = (car.x + car.w / 2 - bridgeStartX) / bridgeLength;
const bridgeY = bridgeStartY + t * (bridgeEndY - bridgeStartY);
if (car.y + car.h >= bridgeY - 5) {
car.y = bridgeY - car.h;
car.vy = 0;
car.onBridge = true;
}
}
// Check if car reached other side
if (car.x >= rightCliff.x && car.y + car.h >= rightCliff.y - 10) {
car.y = rightCliff.y - car.h;
car.vy = 0;
car.vx = 0;
car.finished = true;
// Success!
levelComplete = true;
const bonus = Math.max(0, 100 - Math.abs(bridgeLength - gap));
score += 100 + bonus + level * 10;
playSfx('success');
}
// Check if car fell
if (car.y > H) {
// Failed!
gameOver = true;
gameRunning = false;
musicPlaying = false;
playSfx('fail');
}
}
updateMusic();
}
function draw() {
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#87ceeb');
skyGrad.addColorStop(1, '#e0f7fa');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Safe zones
ctx.fillStyle = '#6bb9f0';
ctx.fillRect(0, 0, W, safeTop);
ctx.fillStyle = '#5d4037';
ctx.fillRect(0, safeBottom, W, H - safeBottom);
// Clouds
ctx.fillStyle = '#fff';
for (let i = 0; i < 3; i++) {
const x = 100 + i * 200;
const y = safeTop + 40 + i * 20;
ctx.beginPath();
ctx.arc(x, y, 25, 0, Math.PI * 2);
ctx.arc(x + 30, y - 10, 30, 0, Math.PI * 2);
ctx.arc(x + 60, y, 25, 0, Math.PI * 2);
ctx.fill();
}
// Header
ctx.fillStyle = '#333';
ctx.font = 'bold 22px Arial';
ctx.textAlign = 'left';
ctx.fillText(`Score: ${score}`, 20, 35);
ctx.textAlign = 'center';
ctx.fillText(`Level ${level}`, W / 2, 35);
ctx.textAlign = 'right';
ctx.fillText(`Gap: ${Math.round(gap)}px`, W - 20, 35);
// Water/void
ctx.fillStyle = '#1e88e5';
ctx.fillRect(leftCliff.w, leftCliff.y + 30, rightCliff.x - leftCliff.w, safeBottom - leftCliff.y);
// Left cliff
ctx.fillStyle = '#5d4037';
ctx.fillRect(leftCliff.x, leftCliff.y, leftCliff.w, leftCliff.h);
ctx.fillStyle = '#8bc34a';
ctx.fillRect(leftCliff.x, leftCliff.y - 15, leftCliff.w, 20);
// Right cliff
ctx.fillStyle = '#5d4037';
ctx.fillRect(rightCliff.x, rightCliff.y, rightCliff.w, rightCliff.h);
ctx.fillStyle = '#8bc34a';
ctx.fillRect(rightCliff.x, rightCliff.y - 15, rightCliff.w, 20);
// Bridge
if (bridgeLength > 0) {
const bridgeStartX = leftCliff.w;
const bridgeStartY = leftCliff.y;
ctx.save();
ctx.translate(bridgeStartX, bridgeStartY);
ctx.rotate(bridgeAngle);
ctx.fillStyle = '#8B4513';
ctx.fillRect(0, -8, bridgeLength, 8);
// Bridge planks
ctx.fillStyle = '#654321';
for (let i = 0; i < bridgeLength; i += 15) {
ctx.fillRect(i, -8, 3, 8);
}
ctx.restore();
}
// Building indicator
if (!testingBridge && !levelComplete && !gameOver) {
ctx.fillStyle = building ? '#4caf50' : '#ff9800';
ctx.font = 'bold 18px Arial';
ctx.textAlign = 'center';
ctx.fillText(
building ? `Building: ${bridgeLength}px (Release to test!)` : 'Hold to build bridge!',
W / 2, leftCliff.y - 50
);
// Target marker
ctx.strokeStyle = '#4caf50';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(leftCliff.w, leftCliff.y - 20);
ctx.lineTo(rightCliff.x, rightCliff.y - 20);
ctx.stroke();
ctx.setLineDash([]);
}
// Car
if (car) {
ctx.fillStyle = '#e74c3c';
ctx.fillRect(car.x, car.y, car.w, car.h);
// Wheels
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(car.x + 8, car.y + car.h, 6, 0, Math.PI * 2);
ctx.arc(car.x + car.w - 8, car.y + car.h, 6, 0, Math.PI * 2);
ctx.fill();
// Window
ctx.fillStyle = '#87ceeb';
ctx.fillRect(car.x + 10, car.y + 3, car.w - 15, car.h - 8);
}
// Controls hint
ctx.fillStyle = '#666';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('Hold Space/Tap to build bridge, release to test!', W / 2, safeBottom + 25);
if (levelComplete) {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#4caf50';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.fillText('LEVEL COMPLETE!', W / 2, H / 2 - 50);
ctx.fillStyle = '#fff';
ctx.font = '24px Arial';
ctx.fillText(`Bridge: ${bridgeLength}px | Gap: ${Math.round(gap)}px`, W / 2, H / 2);
ctx.fillStyle = '#4caf50';
ctx.beginPath();
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 18px Arial';
ctx.fillText('NEXT LEVEL', W / 2, H / 2 + 62);
}
if (gameOver) {
ctx.fillStyle = 'rgba(0,0,0,0.85)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#e74c3c';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.fillText('BRIDGE TOO SHORT!', W / 2, H / 2 - 50);
ctx.fillStyle = '#fff';
ctx.font = '24px Arial';
ctx.fillText(`Final Score: ${score}`, W / 2, H / 2);
ctx.fillStyle = '#2196f3';
ctx.beginPath();
ctx.roundRect(W / 2 - 100, H / 2 + 30, 200, 50, 10);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 18px Arial';
ctx.fillText('TAP TO RESTART', W / 2, H / 2 + 62);
}
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
window.gameScore = 0;
Object.defineProperty(window, 'gameScore', { get: () => score });
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
window.resumeGame = () => { if (!gameOver && !levelComplete) { gameRunning = true; startMusic(); } };
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
window.setSfxEnabled = (v) => { sfxEnabled = v; };
init();
gameLoop();
</script>
</body>
</html>