From 47476b43fbed190988db74bdc55efa90e003b7fb Mon Sep 17 00:00:00 2001 From: numex-admin Date: Wed, 9 Sep 2026 22:39:37 +0000 Subject: [PATCH] =?UTF-8?q?Numex=20yay=C4=B1n:=20game.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game.js | 1174 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1174 insertions(+) create mode 100644 game.js diff --git a/game.js b/game.js new file mode 100644 index 0000000..8376ab8 --- /dev/null +++ b/game.js @@ -0,0 +1,1174 @@ +/** + * Süper Racing - Game Engine v2.0 Turbo Edition (Keyboard Enter Restart Added) + * Deep Upgrade Shop (Engine, Nitro, Armor Shield), Bulletproof Canvas Drawing. + */ + +(function () { + 'use strict'; + + // --- SAFE CANVAS ROUNDRECT POLYFILL --- + function drawRoundRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + } + + // --- AUDIO SYNTHESIZER --- + class SoundManager { + constructor() { + this.ctx = null; + this.enabled = true; + this.engineOsc = null; + this.engineGain = null; + } + + init() { + try { + if (!this.ctx) { + const AudioContext = window.AudioContext || window.webkitAudioContext; + if (AudioContext) { + this.ctx = new AudioContext(); + } + } + if (this.ctx && this.ctx.state === 'suspended') { + this.ctx.resume().catch(() => {}); + } + } catch (e) {} + } + + toggleSound() { + this.enabled = !this.enabled; + if (!this.enabled && this.engineGain && this.ctx) { + try { this.engineGain.gain.setValueAtTime(0, this.ctx.currentTime); } catch (e) {} + } + return this.enabled; + } + + startEngine() { + if (!this.enabled) return; + this.init(); + if (!this.ctx || this.engineOsc) return; + + try { + this.engineOsc = this.ctx.createOscillator(); + this.engineGain = this.ctx.createGain(); + + this.engineOsc.type = 'sawtooth'; + this.engineOsc.frequency.setValueAtTime(45, this.ctx.currentTime); + this.engineGain.gain.setValueAtTime(0.04, this.ctx.currentTime); + + const filter = this.ctx.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.setValueAtTime(150, this.ctx.currentTime); + + this.engineOsc.connect(filter); + filter.connect(this.engineGain); + this.engineGain.connect(this.ctx.destination); + + this.engineOsc.start(); + } catch (e) {} + } + + updateEngine(speedRatio) { + if (!this.enabled || !this.engineOsc || !this.ctx) return; + try { + const targetFreq = 40 + speedRatio * 110; + this.engineOsc.frequency.setTargetAtTime(targetFreq, this.ctx.currentTime, 0.1); + this.engineGain.gain.setTargetAtTime(0.03 + speedRatio * 0.04, this.ctx.currentTime, 0.1); + } catch (e) {} + } + + stopEngine() { + if (this.engineOsc) { + try { + this.engineOsc.stop(); + this.engineOsc.disconnect(); + } catch (e) {} + this.engineOsc = null; + this.engineGain = null; + } + } + + playCoin() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(987.77, now); + osc.frequency.setValueAtTime(1318.51, now + 0.08); + + gain.gain.setValueAtTime(0.12, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.2); + } catch (e) {} + } + + playNearMiss() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'triangle'; + osc.frequency.setValueAtTime(320, now); + osc.frequency.exponentialRampToValueAtTime(750, now + 0.12); + + gain.gain.setValueAtTime(0.08, now); + gain.gain.linearRampToValueAtTime(0.001, now + 0.12); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.12); + } catch (e) {} + } + + playNitro() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(140, now); + osc.frequency.linearRampToValueAtTime(400, now + 0.25); + + gain.gain.setValueAtTime(0.1, now); + gain.gain.linearRampToValueAtTime(0.001, now + 0.25); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.25); + } catch (e) {} + } + + playCrash() { + if (!this.enabled) return; + this.init(); + this.stopEngine(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'square'; + osc.frequency.setValueAtTime(100, now); + osc.frequency.exponentialRampToValueAtTime(25, now + 0.35); + + gain.gain.setValueAtTime(0.25, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.4); + } catch (e) {} + } + + playShield() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(400, now); + osc.frequency.linearRampToValueAtTime(800, now + 0.2); + + gain.gain.setValueAtTime(0.2, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.25); + } catch (e) {} + } + + playNudge() { + if (!this.enabled) return; + this.init(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(180, now); + osc.frequency.setValueAtTime(120, now + 0.07); + osc.frequency.setValueAtTime(240, now + 0.14); + + gain.gain.setValueAtTime(0.2, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.3); + } catch (e) {} + } + + playWin() { + if (!this.enabled) return; + this.init(); + this.stopEngine(); + if (!this.ctx) return; + try { + const now = this.ctx.currentTime; + const notes = [523.25, 659.25, 783.99]; + + notes.forEach((freq, i) => { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now + i * 0.1); + gain.gain.setValueAtTime(0.12, now + i * 0.1); + gain.gain.exponentialRampToValueAtTime(0.001, now + i * 0.1 + 0.25); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now + i * 0.1); + osc.stop(now + i * 0.1 + 0.25); + }); + } catch (e) {} + } + } + + // --- VEHICLES CONFIGURATION --- + const VEHICLES = [ + { + id: 'car_red', + name: 'Süper Kırmızı Spor', + desc: 'Standart başlangıç yarış aracı.', + color: '#ef4444', + price: 0, + speedBase: 140, + accelBase: 50 + }, + { + id: 'car_blue', + name: 'Mavi Fırtına Turbo', + desc: 'Hızlı ivmelenen şehir yarışçısı.', + color: '#0284c7', + price: 150, + speedBase: 165, + accelBase: 65 + }, + { + id: 'car_neon', + name: 'Neon Siber Roadster', + desc: 'Yüksek maksimum hız ve özel görünüm.', + color: '#38bdf8', + price: 350, + speedBase: 190, + accelBase: 80 + }, + { + id: 'car_gold', + name: 'Efsanevi Altın Phantom', + desc: 'Pistlerin en güçlü ve prestijli efsanesi.', + color: '#fbbf24', + price: 700, + speedBase: 220, + accelBase: 100 + } + ]; + + // --- LEVELS --- + const LEVELS = [ + { + id: 1, + name: 'Sahil Yolu (Başlangıç)', + desc: 'Güneşli otobanda 800m sürüş yap.', + goalDistance: 800, + trafficDensity: 0.015, + bgTheme: 'day', + asphaltColor: '#334155', + starScores: [1000, 2000, 3500] + }, + { + id: 2, + name: 'Gece Yağmuru (Orta)', + desc: 'Gece yağmurlu yolda 1200m ilerle.', + goalDistance: 1200, + trafficDensity: 0.02, + bgTheme: 'rain', + asphaltColor: '#1e293b', + starScores: [2000, 3500, 5500] + }, + { + id: 3, + name: 'Sonsuz Otoban Şampiyonluğu (Zor)', + desc: 'Sınırsız sürüş! En yüksek skoru kır.', + goalDistance: 999999, + trafficDensity: 0.026, + bgTheme: 'cyber', + asphaltColor: '#0f172a', + starScores: [4000, 10000, 20000] + } + ]; + + // --- GAME ENGINE --- + class Game { + constructor() { + this.canvas = document.getElementById('gameCanvas'); + this.ctx = this.canvas.getContext('2d'); + this.sound = new SoundManager(); + + this.userData = this.loadUserData(); + + this.currentLevel = LEVELS[0]; + this.selectedCar = VEHICLES[0]; + + this.state = 'MENU'; + this.distance = 0; + this.score = 0; + this.coinsCollected = 0; + this.speed = 0; + this.maxSpeed = 150; + this.nitro = 100; + this.hasShield = false; + + this.lanes = [240, 340, 440, 540]; + + this.player = { + currentLane: 1, + x: 340, + targetX: 340, + y: 480, + width: 42, + height: 76 + }; + + this.traffic = []; + this.coins = []; + this.particles = []; + this.roadScroll = 0; + + this.keys = { + left: false, + right: false, + up: false, + down: false, + nitro: false + }; + + this.laneCooldown = false; + + this.initEvents(); + this.initGarageView(); + this.renderMenuStats(); + this.startLoop(); + } + + loadUserData() { + const defaultData = { + totalCoins: 0, + unlockedCars: ['car_red'], + selectedCarId: 'car_red', + unlockedLevels: [1], + levelStars: { 1: 0, 2: 0, 3: 0 }, + upgrades: { + engineLevel: 1, + nitroLevel: 1, + armorLevel: 0 + } + }; + + try { + const stored = localStorage.getItem('super_racing_save'); + return stored ? Object.assign(defaultData, JSON.parse(stored)) : defaultData; + } catch (e) { + return defaultData; + } + } + + saveUserData() { + try { + localStorage.setItem('super_racing_save', JSON.stringify(this.userData)); + } catch (e) {} + } + + initEvents() { + const unlockAudio = () => this.sound.init(); + window.addEventListener('click', unlockAudio, { once: true }); + window.addEventListener('keydown', unlockAudio, { once: true }); + + // Keyboard Controls (INCLUDING ENTER / RESTART SHORTCUTS!) + window.addEventListener('keydown', (e) => { + // ENTER or SPACE or 'R' Key on Game Over / Win / Menu screens to instantly restart/play! + if (e.key === 'Enter' || e.key === 'r' || e.key === 'R') { + if (this.state === 'GAMEOVER') { + this.startLevel(this.currentLevel.id); + e.preventDefault(); + return; + } else if (this.state === 'WIN') { + const nextId = Math.min(3, this.currentLevel.id + 1); + this.startLevel(nextId); + e.preventDefault(); + return; + } else if (this.state === 'MENU' && !document.getElementById('menuModal').classList.contains('hidden')) { + this.startLevel(1); + e.preventDefault(); + return; + } + } + + if (this.state === 'GAMEOVER' && (e.key === ' ' || e.key === 'Spacebar')) { + this.startLevel(this.currentLevel.id); + e.preventDefault(); + return; + } + + if (this.state === 'PLAYING') { + if ((e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') && !this.laneCooldown) { + this.changeLane(-1); + this.laneCooldown = true; + } + if ((e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') && !this.laneCooldown) { + this.changeLane(1); + this.laneCooldown = true; + } + } + + if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') this.keys.up = true; + if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') this.keys.down = true; + if (e.key === ' ' || e.key === 'Spacebar') { + this.keys.nitro = true; + e.preventDefault(); + } + }); + + window.addEventListener('keyup', (e) => { + if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A' || e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { + this.laneCooldown = false; + } + if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') this.keys.up = false; + if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') this.keys.down = false; + if (e.key === ' ' || e.key === 'Spacebar') this.keys.nitro = false; + }); + + // Touch Controls + const setupTouchClick = (id, action) => { + const btn = document.getElementById(id); + if (!btn) return; + btn.onclick = (e) => { + e.preventDefault(); + this.sound.init(); + if (action === 'left') this.changeLane(-1); + if (action === 'right') this.changeLane(1); + }; + }; + + setupTouchClick('btnTouchLeft', 'left'); + setupTouchClick('btnTouchRight', 'right'); + + const setupHoldBtn = (id, keyName) => { + const btn = document.getElementById(id); + if (!btn) return; + btn.onmousedown = () => { this.keys[keyName] = true; }; + btn.onmouseup = () => { this.keys[keyName] = false; }; + btn.ontouchstart = (e) => { e.preventDefault(); this.keys[keyName] = true; }; + btn.ontouchend = (e) => { e.preventDefault(); this.keys[keyName] = false; }; + }; + + setupHoldBtn('btnTouchBrake', 'down'); + setupHoldBtn('btnTouchNitro', 'nitro'); + + // Menu Buttons + document.getElementById('btnQuickPlay').onclick = () => this.startLevel(1); + document.getElementById('btnSelectLevel').onclick = () => this.showModal('levelModal'); + document.getElementById('btnCloseLevel').onclick = () => this.hideModal('levelModal'); + document.getElementById('btnGarage').onclick = () => this.showGarage(); + document.getElementById('btnCloseGarage').onclick = () => this.hideModal('garageModal'); + document.getElementById('btnHowToPlay').onclick = () => this.showModal('howToModal'); + document.getElementById('btnCloseHowTo').onclick = () => this.hideModal('howToModal'); + + document.getElementById('btnRetryLevel').onclick = () => this.startLevel(this.currentLevel.id); + document.getElementById('btnBackToMenuFromGO').onclick = () => this.showMenu(); + document.getElementById('btnNextLevel').onclick = () => { + const nextId = Math.min(3, this.currentLevel.id + 1); + this.startLevel(nextId); + }; + document.getElementById('btnBackToMenuFromWin').onclick = () => this.showMenu(); + + // Nudge Button + document.getElementById('btnNudge').onclick = () => { + this.sound.playNudge(); + const wrapper = document.getElementById('canvasWrapper'); + wrapper.classList.remove('nudge-shake'); + void wrapper.offsetWidth; + wrapper.classList.add('nudge-shake'); + }; + + // Sound Toggle + document.getElementById('btnSound').onclick = (e) => { + const isEnabled = this.sound.toggleSound(); + e.target.innerText = isEnabled ? '🔊 Ses' : '🔇 Mute'; + }; + + // Fullscreen Toggle + document.getElementById('btnFullscreen').onclick = () => { + if (!document.fullscreenElement) { + document.querySelector('.app-container').requestFullscreen().catch(() => {}); + } else { + document.exitFullscreen().catch(() => {}); + } + }; + + // Shop Upgrades + document.getElementById('btnUpgradeEngine').onclick = () => this.buyUpgrade('engine'); + document.getElementById('btnUpgradeNitro').onclick = () => this.buyUpgrade('nitro'); + document.getElementById('btnUpgradeArmor').onclick = () => this.buyUpgrade('armor'); + } + + changeLane(dir) { + const nextLane = Math.max(0, Math.min(3, this.player.currentLane + dir)); + this.player.currentLane = nextLane; + this.player.targetX = this.lanes[nextLane]; + } + + renderMenuStats() { + let totalStars = 0; + Object.values(this.userData.levelStars).forEach(s => totalStars += s); + + document.getElementById('totalStarsText').innerText = `⭐ ${totalStars}/9`; + document.getElementById('totalCoinsText').innerText = `🪙 ${this.userData.totalCoins}`; + } + + showModal(id) { + document.querySelectorAll('.modal-overlay').forEach(m => m.classList.add('hidden')); + document.getElementById(id).classList.remove('hidden'); + + if (id === 'levelModal') this.renderLevelsList(); + } + + hideModal(id) { + document.getElementById(id).classList.add('hidden'); + } + + showMenu() { + this.state = 'MENU'; + this.sound.stopEngine(); + document.getElementById('hudOverlay').classList.add('hidden'); + document.getElementById('touchControls').classList.add('hidden'); + this.renderMenuStats(); + this.showModal('menuModal'); + } + + renderLevelsList() { + const container = document.getElementById('levelsGrid'); + container.innerHTML = ''; + + LEVELS.forEach(lvl => { + const isUnlocked = this.userData.unlockedLevels.includes(lvl.id); + const stars = this.userData.levelStars[lvl.id] || 0; + const starStr = '⭐'.repeat(stars) + '☆'.repeat(3 - stars); + + const item = document.createElement('div'); + item.className = `level-item ${isUnlocked ? '' : 'locked'}`; + item.innerHTML = ` +
+
${lvl.id}. ${lvl.name}
+

${lvl.desc}

+
+
${isUnlocked ? starStr : '🔒 KİLİTLİ'}
+ `; + + if (isUnlocked) { + item.onclick = () => { + this.hideModal('levelModal'); + this.startLevel(lvl.id); + }; + } + container.appendChild(item); + }); + } + + showGarage() { + this.showModal('garageModal'); + this.renderGarageCar(VEHICLES.find(c => c.id === this.userData.selectedCarId) || VEHICLES[0]); + } + + renderGarageCar(car) { + document.getElementById('garageCarName').innerText = car.name; + document.getElementById('garageCarDesc').innerText = car.desc; + + const engineLvl = this.userData.upgrades.engineLevel || 1; + const nitroLvl = this.userData.upgrades.nitroLevel || 1; + const armorLvl = this.userData.upgrades.armorLevel || 0; + + const effectiveSpeed = car.speedBase + (engineLvl - 1) * 15; + document.getElementById('specSpeed').style.width = `${Math.min(100, (effectiveSpeed / 250) * 100)}%`; + document.getElementById('specAccel').style.width = `${Math.min(100, ((car.accelBase + engineLvl * 10) / 150) * 100)}%`; + document.getElementById('garageCoinDisplay').innerText = `Altınınız: 🪙 ${this.userData.totalCoins}`; + + document.getElementById('lblEngineLvl').innerText = `Lvl ${engineLvl}/5`; + document.getElementById('lblNitroLvl').innerText = `Lvl ${nitroLvl}/5`; + document.getElementById('lblArmorLvl').innerText = armorLvl > 0 ? `Aktif (${armorLvl} Kalkan)` : 'Devre Dışı'; + + const engineCost = engineLvl < 5 ? engineLvl * 60 : 'MAX'; + const nitroCost = nitroLvl < 5 ? nitroLvl * 50 : 'MAX'; + const armorCost = armorLvl < 3 ? (armorLvl + 1) * 100 : 'MAX'; + + document.getElementById('btnUpgradeEngine').innerText = engineLvl < 5 ? `Yükselt (🪙 ${engineCost})` : 'MAX LEVEL'; + document.getElementById('btnUpgradeNitro').innerText = nitroLvl < 5 ? `Yükselt (🪙 ${nitroCost})` : 'MAX LEVEL'; + document.getElementById('btnUpgradeArmor').innerText = armorLvl < 3 ? `Kalkan Al (🪙 ${armorCost})` : 'MAX KALKAN'; + + const btn = document.getElementById('btnSelectOrBuyCar'); + const isUnlocked = this.userData.unlockedCars.includes(car.id); + const isSelected = this.userData.selectedCarId === car.id; + + if (isSelected) { + btn.innerText = 'SEÇİLDİ ✓'; + btn.className = 'btn btn-secondary'; + btn.onclick = null; + } else if (isUnlocked) { + btn.innerText = 'ARACI SEÇ'; + btn.className = 'btn btn-primary'; + btn.onclick = () => { + this.userData.selectedCarId = car.id; + this.selectedCar = car; + this.saveUserData(); + this.renderGarageCar(car); + }; + } else { + btn.innerText = `SATIN AL (🪙 ${car.price})`; + btn.className = 'btn btn-primary'; + btn.onclick = () => { + if (this.userData.totalCoins >= car.price) { + this.userData.totalCoins -= car.price; + this.userData.unlockedCars.push(car.id); + this.userData.selectedCarId = car.id; + this.selectedCar = car; + this.saveUserData(); + this.sound.playCoin(); + this.renderGarageCar(car); + } else { + alert('Yeterli altınınız yok! Bölümleri oynayarak altın kazanın.'); + } + }; + } + + const strip = document.getElementById('carSelectorStrip'); + strip.innerHTML = ''; + VEHICLES.forEach(v => { + const thumb = document.createElement('div'); + const unlocked = this.userData.unlockedCars.includes(v.id); + const sel = this.userData.selectedCarId === v.id; + thumb.className = `car-thumb ${sel ? 'active' : ''}`; + thumb.innerHTML = `${v.name.split(' ')[0]}
${unlocked ? 'Açık' : `🪙 ${v.price}`}`; + thumb.onclick = () => this.renderGarageCar(v); + strip.appendChild(thumb); + }); + + const gCanvas = document.getElementById('garageCarCanvas'); + const gCtx = gCanvas.getContext('2d'); + gCtx.clearRect(0, 0, gCanvas.width, gCanvas.height); + this.drawCarBody(gCtx, gCanvas.width / 2, gCanvas.height / 2, 46, 84, car.color); + } + + buyUpgrade(type) { + const u = this.userData.upgrades; + let cost = 0; + + if (type === 'engine' && u.engineLevel < 5) { + cost = u.engineLevel * 60; + if (this.userData.totalCoins >= cost) { + this.userData.totalCoins -= cost; + u.engineLevel++; + this.sound.playCoin(); + } else { alert('Yeterli altınınız yok!'); } + } else if (type === 'nitro' && u.nitroLevel < 5) { + cost = u.nitroLevel * 50; + if (this.userData.totalCoins >= cost) { + this.userData.totalCoins -= cost; + u.nitroLevel++; + this.sound.playCoin(); + } else { alert('Yeterli altınınız yok!'); } + } else if (type === 'armor' && u.armorLevel < 3) { + cost = (u.armorLevel + 1) * 100; + if (this.userData.totalCoins >= cost) { + this.userData.totalCoins -= cost; + u.armorLevel++; + this.sound.playCoin(); + } else { alert('Yeterli altınınız yok!'); } + } + + this.saveUserData(); + this.renderGarageCar(this.selectedCar); + } + + initGarageView() { + const car = VEHICLES.find(c => c.id === this.userData.selectedCarId) || VEHICLES[0]; + this.selectedCar = car; + } + + startLevel(levelId) { + try { + this.currentLevel = LEVELS.find(l => l.id === levelId) || LEVELS[0]; + this.selectedCar = VEHICLES.find(c => c.id === this.userData.selectedCarId) || VEHICLES[0]; + + const engineLvl = this.userData.upgrades.engineLevel || 1; + const nitroLvl = this.userData.upgrades.nitroLevel || 1; + + this.distance = 0; + this.score = 0; + this.coinsCollected = 0; + this.speed = 50; + this.maxSpeed = this.selectedCar.speedBase + (engineLvl - 1) * 15; + this.nitro = 100; + this.hasShield = (this.userData.upgrades.armorLevel || 0) > 0; + + this.player.currentLane = 1; + this.player.x = this.lanes[1]; + this.player.targetX = this.lanes[1]; + + this.traffic = []; + this.coins = []; + this.particles = []; + + this.hideModal('menuModal'); + this.hideModal('levelModal'); + this.hideModal('gameOverModal'); + this.hideModal('levelWinModal'); + + document.getElementById('hudOverlay').classList.remove('hidden'); + document.getElementById('touchControls').classList.remove('hidden'); + + document.getElementById('hudLevelName').innerText = `${this.currentLevel.id}: ${this.currentLevel.name}`; + document.getElementById('hudProgressText').innerText = `0 / ${this.currentLevel.goalDistance === 999999 ? '∞' : this.currentLevel.goalDistance + 'm'}`; + + this.state = 'PLAYING'; + this.sound.startEngine(); + } catch (err) { + console.error('Start level error:', err); + } + } + + startLoop() { + let lastTime = performance.now(); + const loop = (now) => { + try { + const dt = Math.min((now - lastTime) / 1000, 0.1); + lastTime = now; + + this.update(dt); + this.render(); + } catch (err) { + console.error('Loop error:', err); + } + requestAnimationFrame(loop); + }; + requestAnimationFrame(loop); + } + + update(dt) { + if (this.state !== 'PLAYING') return; + + this.player.x += (this.player.targetX - this.player.x) * 18 * dt; + + const engineLvl = this.userData.upgrades.engineLevel || 1; + const accel = this.selectedCar.accelBase + engineLvl * 10; + + if (this.keys.up) { + this.speed = Math.min(this.speed + accel * dt, this.maxSpeed); + } else if (this.keys.down) { + this.speed = Math.max(this.speed - accel * 1.5 * dt, 20); + } else { + if (this.speed > 70) this.speed -= 15 * dt; + if (this.speed < 70) this.speed += 15 * dt; + } + + const nitroLvl = this.userData.upgrades.nitroLevel || 1; + if (this.keys.nitro && this.nitro > 5) { + this.speed = Math.min(this.speed + accel * 2 * dt, this.maxSpeed * (1.25 + nitroLvl * 0.05)); + this.nitro -= (35 - nitroLvl * 3) * dt; + this.sound.playNitro(); + this.createNitroParticles(); + } else { + if (this.nitro < 100) this.nitro += (8 + nitroLvl * 2) * dt; + } + + const distDelta = (this.speed * dt); + this.distance += distDelta * 0.2; + this.score += Math.floor(distDelta * 0.5); + this.roadScroll += this.speed * dt * 8; + + this.sound.updateEngine(this.speed / this.maxSpeed); + + if (Math.random() < this.currentLevel.trafficDensity) { + this.spawnTraffic(); + } + + if (Math.random() < 0.018) { + this.spawnCoin(); + } + + for (let i = this.traffic.length - 1; i >= 0; i--) { + const car = this.traffic[i]; + car.y += (this.speed - car.speed) * dt * 6; + + if (!car.nearMissed && Math.abs(car.x - this.player.x) < 48 && Math.abs(car.y - this.player.y) < 65 && car.y < this.player.y) { + car.nearMissed = true; + this.score += 150; + this.sound.playNearMiss(); + this.showNearMissBanner(); + } + + if (Math.abs(car.x - this.player.x) < 30 && Math.abs(car.y - this.player.y) < 58) { + if (this.hasShield) { + this.hasShield = false; + this.userData.upgrades.armorLevel = Math.max(0, (this.userData.upgrades.armorLevel || 1) - 1); + this.saveUserData(); + this.sound.playShield(); + this.traffic.splice(i, 1); + this.createSparkles(this.player.x, this.player.y); + continue; + } else { + this.handleCrash(); + return; + } + } + + if (car.y > 700 || car.y < -300) { + this.traffic.splice(i, 1); + } + } + + for (let i = this.coins.length - 1; i >= 0; i--) { + const coin = this.coins[i]; + coin.y += this.speed * dt * 6; + + if (Math.abs(coin.x - this.player.x) < 30 && Math.abs(coin.y - this.player.y) < 38) { + this.coinsCollected++; + this.score += 100; + this.sound.playCoin(); + this.createSparkles(coin.x, coin.y); + this.coins.splice(i, 1); + continue; + } + + if (coin.y > 650) { + this.coins.splice(i, 1); + } + } + + for (let i = this.particles.length - 1; i >= 0; i--) { + const p = this.particles[i]; + p.x += p.vx; + p.y += p.vy; + p.life -= dt; + if (p.life <= 0) this.particles.splice(i, 1); + } + + document.getElementById('hudScore').innerText = this.score; + document.getElementById('hudCoins').innerText = `🪙 ${this.coinsCollected}`; + document.getElementById('hudSpeed').innerText = Math.floor(this.speed); + document.getElementById('hudNitroBar').style.width = `${this.nitro}%`; + + const distMeter = Math.floor(this.distance); + const goal = this.currentLevel.goalDistance; + document.getElementById('hudProgressText').innerText = `${distMeter} / ${goal === 999999 ? '∞' : goal + 'm'}`; + document.getElementById('hudProgressBar').style.width = goal === 999999 ? '100%' : `${Math.min(100, (distMeter / goal) * 100)}%`; + + if (distMeter >= goal && goal !== 999999) { + this.handleLevelWin(); + } + } + + spawnTraffic() { + const laneIndex = Math.floor(Math.random() * 4); + const laneX = this.lanes[laneIndex]; + + const existingInLane = this.traffic.some(c => Math.abs(c.x - laneX) < 20 && c.y < 0 && c.y > -220); + if (existingInLane) return; + + const carTypes = [ + { color: '#e11d48', speed: 55 }, + { color: '#f59e0b', speed: 45 }, + { color: '#16a34a', speed: 65 } + ]; + const type = carTypes[Math.floor(Math.random() * carTypes.length)]; + + this.traffic.push({ + x: laneX, + y: -100, + width: 40, + height: 72, + color: type.color, + speed: type.speed, + nearMissed: false + }); + } + + spawnCoin() { + const laneIndex = Math.floor(Math.random() * 4); + this.coins.push({ x: this.lanes[laneIndex], y: -40 }); + } + + createNitroParticles() { + for (let i = 0; i < 2; i++) { + this.particles.push({ + x: this.player.x + (Math.random() - 0.5) * 14, + y: this.player.y + 38, + vx: (Math.random() - 0.5) * 2, + vy: Math.random() * 3 + 3, + color: Math.random() > 0.5 ? '#f59e0b' : '#38bdf8', + size: Math.random() * 5 + 3, + life: 0.25 + }); + } + } + + createSparkles(x, y) { + for (let i = 0; i < 6; i++) { + this.particles.push({ + x: x, + y: y, + vx: (Math.random() - 0.5) * 5, + vy: (Math.random() - 0.5) * 5, + color: '#fbbf24', + size: 4, + life: 0.35 + }); + } + } + + showNearMissBanner() { + const banner = document.getElementById('hudNearMiss'); + if (banner) { + banner.classList.remove('hidden'); + clearTimeout(this.nearMissTimeout); + this.nearMissTimeout = setTimeout(() => banner.classList.add('hidden'), 700); + } + } + + handleCrash() { + this.state = 'GAMEOVER'; + this.sound.playCrash(); + + for (let i = 0; i < 30; i++) { + this.particles.push({ + x: this.player.x, + y: this.player.y, + vx: (Math.random() - 0.5) * 8, + vy: (Math.random() - 0.5) * 8, + color: Math.random() > 0.5 ? '#ef4444' : '#f59e0b', + size: Math.random() * 6 + 4, + life: 0.6 + }); + } + + this.userData.totalCoins += this.coinsCollected; + this.saveUserData(); + + document.getElementById('goDistance').innerText = `${Math.floor(this.distance)}m`; + document.getElementById('goCoins').innerText = `🪙 ${this.coinsCollected}`; + document.getElementById('goScore').innerText = this.score; + + setTimeout(() => { + document.getElementById('hudOverlay').classList.add('hidden'); + document.getElementById('touchControls').classList.add('hidden'); + this.showModal('gameOverModal'); + }, 400); + } + + handleLevelWin() { + this.state = 'WIN'; + this.sound.playWin(); + + let stars = 1; + if (this.score >= this.currentLevel.starScores[1]) stars = 2; + if (this.score >= this.currentLevel.starScores[2]) stars = 3; + + const currentStars = this.userData.levelStars[this.currentLevel.id] || 0; + if (stars > currentStars) { + this.userData.levelStars[this.currentLevel.id] = stars; + } + + const rewardCoins = 100 + stars * 50; + this.userData.totalCoins += this.coinsCollected + rewardCoins; + + const nextLevelId = this.currentLevel.id + 1; + if (nextLevelId <= 3 && !this.userData.unlockedLevels.includes(nextLevelId)) { + this.userData.unlockedLevels.push(nextLevelId); + } + + this.saveUserData(); + + const starsDisplay = document.getElementById('winStarsDisplay'); + starsDisplay.innerHTML = '⭐'.repeat(stars) + '☆'.repeat(3 - stars); + document.getElementById('winRewardCoins').innerText = `🪙 +${rewardCoins}`; + document.getElementById('winScore').innerText = this.score; + + document.getElementById('hudOverlay').classList.add('hidden'); + document.getElementById('touchControls').classList.add('hidden'); + this.showModal('levelWinModal'); + } + + render() { + this.ctx.clearRect(0, 0, 800, 600); + + this.drawEnvironment(); + this.drawRoad(); + this.drawCoins(); + this.drawTraffic(); + this.drawPlayer(); + this.drawParticles(); + } + + drawEnvironment() { + const theme = this.currentLevel.bgTheme; + if (theme === 'day') { + this.ctx.fillStyle = '#15803d'; + } else if (theme === 'rain') { + this.ctx.fillStyle = '#0f172a'; + } else { + this.ctx.fillStyle = '#2e1065'; + } + this.ctx.fillRect(0, 0, 800, 600); + } + + drawRoad() { + this.ctx.fillStyle = this.currentLevel.asphaltColor; + this.ctx.fillRect(180, 0, 440, 600); + + this.ctx.fillStyle = '#f59e0b'; + this.ctx.fillRect(185, 0, 6, 600); + this.ctx.fillRect(609, 0, 6, 600); + + this.ctx.fillStyle = '#ffffff'; + const offset = this.roadScroll % 50; + const dividers = [290, 390, 490]; + + for (let x of dividers) { + for (let y = -50 + offset; y < 600; y += 50) { + this.ctx.fillRect(x - 2, y, 4, 26); + } + } + } + + drawPlayer() { + this.drawCarBody(this.ctx, this.player.x, this.player.y, this.player.width, this.player.height, this.selectedCar.color); + + if (this.hasShield) { + this.ctx.save(); + this.ctx.strokeStyle = '#38bdf8'; + this.ctx.lineWidth = 3; + this.ctx.beginPath(); + this.ctx.arc(this.player.x, this.player.y, 48, 0, Math.PI * 2); + this.ctx.stroke(); + this.ctx.restore(); + } + } + + drawTraffic() { + this.traffic.forEach(car => { + this.drawCarBody(this.ctx, car.x, car.y, car.width, car.height, car.color); + }); + } + + drawCoins() { + this.ctx.save(); + this.coins.forEach(c => { + this.ctx.fillStyle = '#fbbf24'; + this.ctx.strokeStyle = '#d97706'; + this.ctx.lineWidth = 3; + this.ctx.beginPath(); + this.ctx.arc(c.x, c.y, 13, 0, Math.PI * 2); + this.ctx.fill(); + this.ctx.stroke(); + + this.ctx.fillStyle = '#92400e'; + this.ctx.font = 'bold 12px sans-serif'; + this.ctx.textAlign = 'center'; + this.ctx.textBaseline = 'middle'; + this.ctx.fillText('$', c.x, c.y); + }); + this.ctx.restore(); + } + + drawParticles() { + this.particles.forEach(p => { + this.ctx.fillStyle = p.color; + this.ctx.beginPath(); + this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + this.ctx.fill(); + }); + } + + drawCarBody(ctx, x, y, width, height, color) { + ctx.save(); + ctx.translate(x, y); + + ctx.fillStyle = '#0f172a'; + ctx.fillRect(-width / 2 - 4, -height / 2 + 10, 6, 16); + ctx.fillRect(width / 2 - 2, -height / 2 + 10, 6, 16); + ctx.fillRect(-width / 2 - 4, height / 2 - 24, 6, 16); + ctx.fillRect(width / 2 - 2, height / 2 - 24, 6, 16); + + ctx.fillStyle = color; + drawRoundRect(ctx, -width / 2, -height / 2, width, height, 8); + ctx.fill(); + + ctx.fillStyle = '#0f172a'; + drawRoundRect(ctx, -width / 2 + 4, -height / 2 + 16, width - 8, height - 34, 5); + ctx.fill(); + + ctx.fillStyle = '#38bdf8'; + drawRoundRect(ctx, -width / 2 + 6, -height / 2 + 18, width - 12, 14, 3); + ctx.fill(); + + ctx.fillStyle = '#fef08a'; + ctx.fillRect(-width / 2 + 4, -height / 2 + 2, 8, 4); + ctx.fillRect(width / 2 - 12, -height / 2 + 2, 8, 4); + + ctx.fillStyle = '#ef4444'; + ctx.fillRect(-width / 2 + 4, height / 2 - 6, 8, 4); + ctx.fillRect(width / 2 - 12, height / 2 - 6, 8, 4); + + ctx.restore(); + } + } + + window.addEventListener('DOMContentLoaded', () => { + window.gameInstance = new Game(); + }); +})();