Initial commit
This commit is contained in:
@@ -0,0 +1,702 @@
|
||||
<!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>Night Racer - Racing Example</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; overflow: hidden; background: #0a0a15; touch-action: none; }
|
||||
canvas { display: block; }
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 0 10px #0ff;
|
||||
z-index: 10;
|
||||
}
|
||||
#ui div { margin-bottom: 5px; }
|
||||
#speedometer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
color: #0ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 20px #0ff;
|
||||
}
|
||||
#loading {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(135deg, #0a0a15, #1a1a2e);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #0ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
z-index: 100;
|
||||
}
|
||||
#loading h1 {
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 0 20px #0ff;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.loading-bar {
|
||||
width: 250px;
|
||||
height: 4px;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #0ff;
|
||||
}
|
||||
.loading-bar-fill {
|
||||
height: 100%;
|
||||
background: #0ff;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
box-shadow: 0 0 10px #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">
|
||||
<h1>NIGHT RACER</h1>
|
||||
<div class="loading-bar"><div class="loading-bar-fill" id="loadingFill"></div></div>
|
||||
<p id="loadingText">Initializing...</p>
|
||||
</div>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div id="ui">
|
||||
<div>SCORE: <span id="score">0</span></div>
|
||||
<div>DISTANCE: <span id="distance">0</span>m</div>
|
||||
</div>
|
||||
<div id="speedometer"><span id="speed">0</span> km/h</div>
|
||||
|
||||
<!-- Asset Libraries -->
|
||||
<script src="/assets/backgrounds/backgroundEngine.js"></script>
|
||||
<script src="/assets/backgrounds/effects.js"></script>
|
||||
<script src="/assets/backgrounds/themes/urban.js"></script>
|
||||
<script src="/assets/backgrounds/backgroundCatalog.js"></script>
|
||||
<script src="/assets/music/moodCategories.js"></script>
|
||||
<script src="/assets/music/musicEngine.js"></script>
|
||||
<script src="/assets/music/library/songs1-80.js"></script>
|
||||
<script src="/assets/avatar/avatarData.js"></script>
|
||||
<script src="/assets/avatar/accessories.js"></script>
|
||||
<script src="/assets/avatar/animations.js"></script>
|
||||
<script src="/assets/avatar/avatarRenderer.js"></script>
|
||||
<script src="/assets/avatar/accessoryRenderer.js"></script>
|
||||
<script src="/assets/avatar/avatarSystem.js"></script>
|
||||
<script src="/assets/avatar/contexts/vehicles.js"></script>
|
||||
<script src="/assets/avatar/contextRenderer.js"></script>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// RACER EXAMPLE - Vehicle Context Demo
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Demonstrates: HEAD_AND_SHOULDERS avatar, race-car context,
|
||||
// urban/neonDistrict background, Intense music mood
|
||||
|
||||
const GAME_CONFIG = {
|
||||
title: "Night Racer",
|
||||
assets: {
|
||||
music: { mood: "Intense", energy: 9, enabled: true },
|
||||
background: { theme: "urban", variant: "neonDistrict", animated: true },
|
||||
avatarContext: { mode: "HEAD_AND_SHOULDERS", context: "race-car", showAccessories: true }
|
||||
},
|
||||
gameplay: {
|
||||
baseSpeed: 300,
|
||||
maxSpeed: 600,
|
||||
acceleration: 150,
|
||||
deceleration: 200,
|
||||
laneWidth: 100,
|
||||
lanes: 3,
|
||||
obstacleSpawnRate: 1200,
|
||||
coinSpawnRate: 800
|
||||
}
|
||||
};
|
||||
|
||||
const playerAvatar = {
|
||||
faceShape: 'square',
|
||||
skinTone: 'tan',
|
||||
hairStyle: 'short',
|
||||
hairColor: '#1a1a1a',
|
||||
eyeShape: 'sharp',
|
||||
eyeColor: '#2ecc71',
|
||||
noseShape: 'pointed',
|
||||
mouthShape: 'serious'
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// SETUP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
let state = {
|
||||
playerLane: 1,
|
||||
playerX: 0,
|
||||
targetX: 0,
|
||||
speed: 0,
|
||||
distance: 0,
|
||||
animTime: 0,
|
||||
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
paused: false,
|
||||
lastTime: 0,
|
||||
|
||||
obstacles: [],
|
||||
coins: [],
|
||||
roadLines: [],
|
||||
buildings: [],
|
||||
lastObstacle: 0,
|
||||
lastCoin: 0
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// LOADING
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateLoading(progress, text) {
|
||||
document.getElementById('loadingFill').style.width = progress + '%';
|
||||
document.getElementById('loadingText').textContent = text;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadAssets() {
|
||||
updateLoading(20, 'Loading city...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(40, 'Loading music...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(60, 'Loading vehicle...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(80, 'Starting engine...');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
updateLoading(100, 'GO!');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
hideLoading();
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
|
||||
if (window.MusicEngine && GAME_CONFIG.assets.music.enabled) {
|
||||
try {
|
||||
window.MusicEngine.playByMood(GAME_CONFIG.assets.music.mood, GAME_CONFIG.assets.music.energy);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initGame() {
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
|
||||
state.playerLane = 1;
|
||||
state.playerX = roadX + laneWidth * state.playerLane + laneWidth / 2;
|
||||
state.targetX = state.playerX;
|
||||
state.speed = GAME_CONFIG.gameplay.baseSpeed;
|
||||
state.distance = 0;
|
||||
state.score = 0;
|
||||
state.obstacles = [];
|
||||
state.coins = [];
|
||||
state.lastObstacle = 0;
|
||||
state.lastCoin = 0;
|
||||
|
||||
// Initialize road lines
|
||||
state.roadLines = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
state.roadLines.push({
|
||||
y: i * 80,
|
||||
lane: 0
|
||||
});
|
||||
state.roadLines.push({
|
||||
y: i * 80,
|
||||
lane: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize buildings
|
||||
state.buildings = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
state.buildings.push({
|
||||
x: Math.random() > 0.5 ? -50 - Math.random() * 100 : canvas.width + 50 + Math.random() * 100,
|
||||
y: i * 100 - 500,
|
||||
width: 60 + Math.random() * 100,
|
||||
height: 150 + Math.random() * 300,
|
||||
color: `hsl(${260 + Math.random() * 40}, 50%, ${15 + Math.random() * 15}%)`
|
||||
});
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function getLaneX(lane) {
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
return roadX + laneWidth * lane + laneWidth / 2;
|
||||
}
|
||||
|
||||
function updateGame(dt) {
|
||||
const { baseSpeed, maxSpeed, acceleration, deceleration, laneWidth, lanes, obstacleSpawnRate, coinSpawnRate } = GAME_CONFIG.gameplay;
|
||||
|
||||
// Speed control
|
||||
if (keys['ArrowUp'] || keys['KeyW']) {
|
||||
state.speed = Math.min(state.speed + acceleration * dt, maxSpeed);
|
||||
} else {
|
||||
state.speed = Math.max(state.speed - deceleration * dt * 0.3, baseSpeed);
|
||||
}
|
||||
|
||||
// Lane switching
|
||||
if (keys['ArrowLeft'] || keys['KeyA']) {
|
||||
if (!keys._leftPressed) {
|
||||
keys._leftPressed = true;
|
||||
state.playerLane = Math.max(0, state.playerLane - 1);
|
||||
state.targetX = getLaneX(state.playerLane);
|
||||
}
|
||||
} else {
|
||||
keys._leftPressed = false;
|
||||
}
|
||||
|
||||
if (keys['ArrowRight'] || keys['KeyD']) {
|
||||
if (!keys._rightPressed) {
|
||||
keys._rightPressed = true;
|
||||
state.playerLane = Math.min(lanes - 1, state.playerLane + 1);
|
||||
state.targetX = getLaneX(state.playerLane);
|
||||
}
|
||||
} else {
|
||||
keys._rightPressed = false;
|
||||
}
|
||||
|
||||
// Smooth lane movement
|
||||
state.playerX += (state.targetX - state.playerX) * 10 * dt;
|
||||
|
||||
// Update distance
|
||||
state.distance += state.speed * dt * 0.01;
|
||||
|
||||
// Spawn obstacles
|
||||
state.lastObstacle += dt * 1000;
|
||||
if (state.lastObstacle > obstacleSpawnRate * (baseSpeed / state.speed)) {
|
||||
state.lastObstacle = 0;
|
||||
const lane = Math.floor(Math.random() * lanes);
|
||||
state.obstacles.push({
|
||||
x: getLaneX(lane),
|
||||
y: -100,
|
||||
lane: lane,
|
||||
type: Math.random() > 0.7 ? 'truck' : 'car',
|
||||
color: `hsl(${Math.random() * 360}, 70%, 50%)`
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn coins
|
||||
state.lastCoin += dt * 1000;
|
||||
if (state.lastCoin > coinSpawnRate) {
|
||||
state.lastCoin = 0;
|
||||
const lane = Math.floor(Math.random() * lanes);
|
||||
// Don't spawn on obstacles
|
||||
const blocked = state.obstacles.some(o => o.lane === lane && o.y < 200);
|
||||
if (!blocked) {
|
||||
state.coins.push({
|
||||
x: getLaneX(lane),
|
||||
y: -50,
|
||||
lane: lane,
|
||||
rotation: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
state.obstacles = state.obstacles.filter(obs => {
|
||||
obs.y += state.speed * dt;
|
||||
|
||||
// Collision check
|
||||
const playerY = canvas.height - 150;
|
||||
if (Math.abs(obs.x - state.playerX) < 40 &&
|
||||
Math.abs(obs.y - playerY) < 60) {
|
||||
endGame();
|
||||
}
|
||||
|
||||
return obs.y < canvas.height + 150;
|
||||
});
|
||||
|
||||
// Update coins
|
||||
state.coins = state.coins.filter(coin => {
|
||||
coin.y += state.speed * dt;
|
||||
coin.rotation += dt * 5;
|
||||
|
||||
// Collection check
|
||||
const playerY = canvas.height - 150;
|
||||
if (Math.abs(coin.x - state.playerX) < 50 &&
|
||||
Math.abs(coin.y - playerY) < 50) {
|
||||
state.score += 10;
|
||||
return false;
|
||||
}
|
||||
|
||||
return coin.y < canvas.height + 50;
|
||||
});
|
||||
|
||||
// Update road lines
|
||||
state.roadLines.forEach(line => {
|
||||
line.y += state.speed * dt;
|
||||
if (line.y > canvas.height) {
|
||||
line.y -= 15 * 80;
|
||||
}
|
||||
});
|
||||
|
||||
// Update buildings
|
||||
state.buildings.forEach(b => {
|
||||
b.y += state.speed * dt * 0.3;
|
||||
if (b.y > canvas.height + 200) {
|
||||
b.y -= 20 * 100 + 500;
|
||||
b.height = 150 + Math.random() * 300;
|
||||
}
|
||||
});
|
||||
|
||||
// Score over time
|
||||
state.score += Math.floor(state.speed * dt * 0.05);
|
||||
|
||||
state.animTime += dt * 1000;
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function renderGame() {
|
||||
// Dark background
|
||||
ctx.fillStyle = '#0a0a15';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// City skyline (buildings)
|
||||
state.buildings.forEach(b => {
|
||||
// Building
|
||||
ctx.fillStyle = b.color;
|
||||
const bx = b.x < 0 ? b.x : b.x;
|
||||
ctx.fillRect(bx, b.y, b.width, b.height);
|
||||
|
||||
// Windows
|
||||
ctx.fillStyle = Math.random() > 0.3 ? '#ffff88' : '#333';
|
||||
for (let wy = b.y + 20; wy < b.y + b.height - 20; wy += 30) {
|
||||
for (let wx = bx + 10; wx < bx + b.width - 10; wx += 20) {
|
||||
if (Math.random() > 0.4) {
|
||||
ctx.fillRect(wx, wy, 10, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { laneWidth, lanes } = GAME_CONFIG.gameplay;
|
||||
const roadWidth = laneWidth * lanes + 40;
|
||||
const roadX = canvas.width / 2 - roadWidth / 2;
|
||||
|
||||
// Road
|
||||
ctx.fillStyle = '#1a1a2a';
|
||||
ctx.fillRect(roadX, 0, roadWidth, canvas.height);
|
||||
|
||||
// Road edges (neon)
|
||||
ctx.strokeStyle = '#ff00ff';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.shadowColor = '#ff00ff';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(roadX, 0);
|
||||
ctx.lineTo(roadX, canvas.height);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(roadX + roadWidth, 0);
|
||||
ctx.lineTo(roadX + roadWidth, canvas.height);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Lane dividers
|
||||
ctx.strokeStyle = '#0ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.shadowColor = '#0ff';
|
||||
ctx.shadowBlur = 10;
|
||||
state.roadLines.forEach(line => {
|
||||
const x1 = roadX + 20 + laneWidth * (line.lane + 1);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, line.y);
|
||||
ctx.lineTo(x1, line.y + 40);
|
||||
ctx.stroke();
|
||||
});
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Coins
|
||||
state.coins.forEach(coin => {
|
||||
ctx.save();
|
||||
ctx.translate(coin.x, coin.y);
|
||||
ctx.rotate(coin.rotation);
|
||||
ctx.fillStyle = '#FFD700';
|
||||
ctx.shadowColor = '#FFD700';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#FFA500';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-4, -4, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Obstacles (other cars)
|
||||
state.obstacles.forEach(obs => {
|
||||
ctx.save();
|
||||
ctx.translate(obs.x, obs.y);
|
||||
|
||||
if (obs.type === 'truck') {
|
||||
// Truck
|
||||
ctx.fillStyle = obs.color;
|
||||
ctx.fillRect(-25, -60, 50, 100);
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-20, -55, 40, 25);
|
||||
// Wheels
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(-28, -40, 8, 20);
|
||||
ctx.fillRect(20, -40, 8, 20);
|
||||
ctx.fillRect(-28, 20, 8, 20);
|
||||
ctx.fillRect(20, 20, 8, 20);
|
||||
} else {
|
||||
// Car
|
||||
ctx.fillStyle = obs.color;
|
||||
ctx.fillRect(-20, -40, 40, 70);
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-15, -35, 30, 20);
|
||||
// Tail lights
|
||||
ctx.fillStyle = '#ff0000';
|
||||
ctx.shadowColor = '#ff0000';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fillRect(-18, 25, 8, 5);
|
||||
ctx.fillRect(10, 25, 8, 5);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Player car with avatar
|
||||
const playerY = canvas.height - 150;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(state.playerX, playerY);
|
||||
|
||||
// Car body
|
||||
ctx.fillStyle = '#e74c3c';
|
||||
ctx.shadowColor = '#e74c3c';
|
||||
ctx.shadowBlur = 20;
|
||||
|
||||
// Main body
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-30, 50);
|
||||
ctx.lineTo(-35, 20);
|
||||
ctx.lineTo(-30, -30);
|
||||
ctx.lineTo(-20, -50);
|
||||
ctx.lineTo(20, -50);
|
||||
ctx.lineTo(30, -30);
|
||||
ctx.lineTo(35, 20);
|
||||
ctx.lineTo(30, 50);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Windshield
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -45);
|
||||
ctx.lineTo(-25, -15);
|
||||
ctx.lineTo(25, -15);
|
||||
ctx.lineTo(20, -45);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Avatar in car (head and shoulders visible)
|
||||
if (window.ContextRenderer) {
|
||||
ctx.save();
|
||||
ctx.translate(-32, -65);
|
||||
window.ContextRenderer.render(
|
||||
ctx,
|
||||
GAME_CONFIG.assets.avatarContext.context,
|
||||
playerAvatar,
|
||||
32, 32,
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
ctx.restore();
|
||||
} else if (window.AvatarSystem) {
|
||||
ctx.save();
|
||||
ctx.translate(-32, -65);
|
||||
window.AvatarSystem.render(
|
||||
ctx,
|
||||
playerAvatar,
|
||||
'HEAD_AND_SHOULDERS',
|
||||
'idle',
|
||||
state.animTime
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Headlights
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.shadowColor = '#fff';
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-18, -45, 5, 0, Math.PI * 2);
|
||||
ctx.arc(18, -45, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Light beams
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -50);
|
||||
ctx.lineTo(-40, -200);
|
||||
ctx.lineTo(0, -200);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, -50);
|
||||
ctx.lineTo(0, -200);
|
||||
ctx.lineTo(40, -200);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Speed lines effect at high speed
|
||||
if (state.speed > 400) {
|
||||
ctx.strokeStyle = `rgba(0, 255, 255, ${(state.speed - 400) / 400})`;
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x, y + 50 + state.speed * 0.2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function updateUI() {
|
||||
document.getElementById('score').textContent = state.score;
|
||||
document.getElementById('distance').textContent = Math.floor(state.distance);
|
||||
document.getElementById('speed').textContent = Math.floor(state.speed * 0.6);
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
state.gameOver = true;
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'gameOver', score: state.score }, '*');
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (confirm(`GAME OVER\n\nScore: ${state.score}\nDistance: ${Math.floor(state.distance)}m\n\nRace again?`)) {
|
||||
state.gameOver = false;
|
||||
initGame();
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// INPUT
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
keys[e.code] = true;
|
||||
if (e.code === 'Escape') {
|
||||
state.paused = !state.paused;
|
||||
if (!state.paused) {
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
keys[e.code] = false;
|
||||
});
|
||||
|
||||
// Touch controls
|
||||
let touchStartX = 0;
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault();
|
||||
touchStartX = e.touches[0].clientX;
|
||||
keys['ArrowUp'] = true;
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
const x = e.touches[0].clientX;
|
||||
const dx = x - touchStartX;
|
||||
|
||||
if (dx < -50) {
|
||||
keys['ArrowLeft'] = true;
|
||||
keys['ArrowRight'] = false;
|
||||
touchStartX = x;
|
||||
} else if (dx > 50) {
|
||||
keys['ArrowRight'] = true;
|
||||
keys['ArrowLeft'] = false;
|
||||
touchStartX = x;
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', e => {
|
||||
keys['ArrowUp'] = false;
|
||||
keys['ArrowLeft'] = false;
|
||||
keys['ArrowRight'] = false;
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// GAME LOOP
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
function gameLoop(time) {
|
||||
if (state.gameOver || state.paused) return;
|
||||
|
||||
const dt = Math.min((time - state.lastTime) / 1000, 0.05);
|
||||
state.lastTime = time;
|
||||
|
||||
updateGame(dt);
|
||||
renderGame();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
window.pauseGame = () => { state.paused = true; };
|
||||
window.resumeGame = () => {
|
||||
state.paused = false;
|
||||
state.lastTime = performance.now();
|
||||
requestAnimationFrame(gameLoop);
|
||||
};
|
||||
|
||||
loadAssets();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user