Rotate for Best Experience

Grand Getaway is designed for landscape mode.

SUNSETCITY

Safehouse

$0

Vault

Thomas

Master Getaway Driver

Career Progress

1 / 100

Heat Level

Incoming Job

The Setup

Loading mission intel...

Choose Approach

0 MPH
Target

Mission Cleared

Clean getaway. The cops lost your tail in the industrial district.

Payout +$5,000
// Caching DOM elements const elements = { screens: { home: document.getElementById('screen-home'), briefing: document.getElementById('screen-briefing'), drive: document.getElementById('screen-drive'), result: document.getElementById('screen-result') }, ui: { cash: document.getElementById('ui-cash'), missionCount: document.getElementById('ui-mission-count'), heatStars: document.getElementById('ui-heat-stars'), tiltBtn: document.getElementById('btn-toggle-tilt'), tiltStatus: document.getElementById('status-tilt'), tiltIcon: document.getElementById('icon-tilt'), shareBtn: document.getElementById('btn-share'), avatarImg: document.getElementById('avatar-img'), avatarIcon: document.getElementById('avatar-icon') }, briefing: { title: document.getElementById('briefing-title'), desc: document.getElementById('briefing-desc'), choice1Title: document.getElementById('choice-1-title'), choice2Title: document.getElementById('choice-2-title') }, result: { container: document.getElementById('result-icon-container'), icon: document.getElementById('result-icon'), title: document.getElementById('result-title'), desc: document.getElementById('result-desc'), cash: document.getElementById('result-cash') }, drive: { speed: document.getElementById('hud-speed'), heat: document.getElementById('hud-heat'), progress: document.getElementById('hud-progress'), health: document.getElementById('hud-health'), touchControls: document.getElementById('touch-steering-controls'), tiltIndicator: document.getElementById('tilt-indicator'), damageOverlay: document.getElementById('damage-overlay') }, canvas: document.getElementById('game-canvas') }; // --- INIT --- function applyAllEditableValues() { window.sekaiEditable.tune?.forEach(t => { if (t.path) setNestedValue(window, t.path, t.value); }); window.sekaiEditable.colors?.forEach(c => { if (c.cssVar) document.documentElement.style.setProperty(c.cssVar, c.value); }); window.sekaiEditable.images?.forEach(img => { if (img.selector) { const el = document.querySelector(img.selector); if (el) { if (img.property === 'src' || el.tagName === 'IMG') { el.src = img.value; if (img.value && img.id === 'avatar') { el.classList.remove('hidden'); elements.ui.avatarIcon.classList.add('hidden'); } } else if (img.property === 'backgroundImage' || el.tagName === 'DIV') { el.style.backgroundImage = img.value ? `url('${img.value}')` : ''; } } } }); window.sekaiEditable.music?.forEach(m => { if (m.selector) { const el = document.querySelector(m.selector); if (el && el.src !== m.value) { el.src = m.value; el.load(); } } }); window.sekaiEditable.sfx?.forEach(s => { if (s.selector) { const el = document.querySelector(s.selector); if (el && el.src !== s.value) { el.src = s.value; el.load(); } } }); window.sekaiEditable.text?.forEach(t => { if (t.selector) { const el = document.querySelector(t.selector); if (el) el.textContent = t.value; } }); } function initBGM() { const el = document.getElementById('music-bgm'); if (!el?.src || el.src === window.location.href) return; el.loop = true; el.volume = 0.5; const playBgm = () => { el.play().catch(() => { document.addEventListener('touchstart', playBgm, { once: true }); document.addEventListener('click', playBgm, { once: true }); }); }; playBgm(); } // Procedural Audio Fallbacks const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); let engineOscillator = null; let engineGain = null; function playSound(type) { if (audioCtx.state === 'suspended') audioCtx.resume(); if (type === 'crash') { const el = document.getElementById('sfx-crash'); if (el && el.src && el.src !== window.location.href) { el.currentTime = 0; el.play().catch(()=>{}); return; } // Procedural Crash const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 0.5); gain.gain.setValueAtTime(1, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.5); sendMessage('gen_vibration', { duration: 300 }); // Haptic } else if (type === 'success') { const el = document.getElementById('sfx-success'); if (el && el.src && el.src !== window.location.href) { el.currentTime = 0; el.play().catch(()=>{}); return; } // Procedural Success const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(440, audioCtx.currentTime); osc.frequency.setValueAtTime(659.25, audioCtx.currentTime + 0.1); // E5 osc.frequency.setValueAtTime(880, audioCtx.currentTime + 0.2); // A5 gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.5, audioCtx.currentTime + 0.05); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.5); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.6); } } function startEngineSound() { const el = document.getElementById('sfx-engine'); if (el && el.src && el.src !== window.location.href) { el.volume = 0.5; el.play().catch(()=>{}); return; } // Procedural Engine Drone if (audioCtx.state === 'suspended') audioCtx.resume(); if (!engineOscillator) { engineOscillator = audioCtx.createOscillator(); engineGain = audioCtx.createGain(); const filter = audioCtx.createBiquadFilter(); engineOscillator.type = 'sawtooth'; engineOscillator.frequency.value = 50; filter.type = 'lowpass'; filter.frequency.value = 400; engineGain.gain.value = 0.2; engineOscillator.connect(filter); filter.connect(engineGain); engineGain.connect(audioCtx.destination); engineOscillator.start(); } else { engineGain.gain.setTargetAtTime(0.2, audioCtx.currentTime, 0.1); } } function updateEnginePitch(speedRatio) { if (engineOscillator) { const baseFreq = 40; const maxFreq = 150; engineOscillator.frequency.setTargetAtTime(baseFreq + (maxFreq - baseFreq) * speedRatio, audioCtx.currentTime, 0.1); } } function stopEngineSound() { const el = document.getElementById('sfx-engine'); if (el && !el.paused) { el.pause(); } if (engineGain) { engineGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1); } } // --- IFRAME COMMUNICATION --- function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } const pendingRequests = new Map(); function sendMessage(type, data, expectsResponse = false, responseType = null) { return new Promise((resolve, reject) => { const taskId = generateUUID(); const msg = { origin: 'sekai_gaming_iframe_api', type, taskId, data }; if (expectsResponse) { const timeout = setTimeout(() => { pendingRequests.delete(taskId); reject(new Error(`Timeout waiting for ${responseType}`)); }, 10000); pendingRequests.set(taskId, { resolve, reject, responseType, timeout }); } window.parent.postMessage(msg, '*'); if (!expectsResponse) resolve(); }); } // Use dummy data instead of waiting for external state to ensure fast load async function getAppStates() { try { const res = await sendMessage('get_app_states', null, true, 'receive_app_states'); if (res && res.pageSettings && res.pageSettings.mission) { window.appState.player = { ...window.appState.player, ...res.pageSettings }; } } catch (e) { console.warn("Using local state", e); } updateHomeUI(); } function saveAppStates() { sendMessage('save_app_states', { pageSettings: window.appState.player }); } window.addEventListener('message', (event) => { const msg = event.data; if (msg?.origin !== 'sekai_gaming_iframe_api') return; // Handle Preview API if (msg.type === 'get_editable_metadata') { window.parent.postMessage({ origin: 'sekai_gaming_iframe_api', type: 'receive_editable_metadata', taskId: msg.taskId, data: window.sekaiEditable || {} }, '*'); return; } if (msg.type === 'apply_change') { const success = applySingleChange(msg.data.id, msg.data.value, msg.data.name, msg.data.description); window.parent.postMessage({ origin: 'sekai_gaming_iframe_api', type: 'receive_apply_change', taskId: msg.taskId, data: { success } }, '*'); return; } if (msg.type === 'get_current_html') { window.parent.postMessage({ origin: 'sekai_gaming_iframe_api', type: 'receive_current_html', taskId: msg.taskId, data: { html: document.documentElement.outerHTML } }, '*'); return; } if (msg.type === 'reset_changes') { location.reload(); return; } if (msg.type === 'receive_audio_unlock') { if (audioCtx.state === 'suspended') audioCtx.resume(); return; } // Handle Promise resolutions const req = pendingRequests.get(msg.taskId); if (req && msg.type === req.responseType) { clearTimeout(req.timeout); pendingRequests.delete(msg.taskId); if (msg.error) req.reject(msg.error); else req.resolve(msg.data); } }); function applySingleChange(id, value, name, description) { const categories = ['tune', 'images', 'videos', 'music', 'sfx', 'colors', 'text', 'prompts', 'voices']; for (const cat of categories) { const item = window.sekaiEditable[cat]?.find(i => i.id === id); if (!item) continue; item.value = value; if (name) item.name = name; if (description) item.description = description; if (item.cssVar) document.documentElement.style.setProperty(item.cssVar, value); else if (item.path) setNestedValue(window, item.path, value); else if (item.selector) { const el = document.querySelector(item.selector); if (!el) return false; if (el.tagName === 'AUDIO' || el.tagName === 'VIDEO') { const wasPlaying = !el.paused; el.src = value; el.load(); if (wasPlaying) el.play().catch(() => {}); } else if (item.property === 'src' || el.tagName === 'IMG') { el.src = value; if (id === 'avatar' && value) { el.classList.remove('hidden'); elements.ui.avatarIcon.classList.add('hidden'); } } else if (item.property === 'backgroundImage' || el.tagName === 'DIV') { el.style.backgroundImage = value ? `url('${value}')` : ''; } else { el.textContent = value; } } return true; } return false; } // --- APP LOGIC --- function switchScreen(screenId) { Object.values(elements.screens).forEach(s => s.classList.remove('active')); elements.screens[screenId].classList.add('active'); } function updateHomeUI() { elements.ui.cash.textContent = `$${window.appState.player.cash.toLocaleString()}`; elements.ui.missionCount.textContent = window.appState.player.mission; // Heat Stars elements.ui.heatStars.innerHTML = ''; for(let i=0; i<5; i++) { const star = document.createElement('i'); star.className = `fas fa-star ${i < window.appState.player.heat ? 'text-red-500' : 'text-gray-700'}`; elements.ui.heatStars.appendChild(star); } // Tilt toggle UI if (window.appState.player.tiltEnabled) { elements.ui.tiltStatus.textContent = "ON"; elements.ui.tiltStatus.className = "text-green-400"; elements.ui.tiltIcon.className = "fas fa-mobile-screen text-2xl text-green-400"; } else { elements.ui.tiltStatus.textContent = "OFF"; elements.ui.tiltStatus.className = "text-red-400"; elements.ui.tiltIcon.className = "fas fa-mobile-screen-button text-2xl text-gray-400"; } } // Procedural Mission Generator const missionAdjectives = ["Neon", "Midnight", "Diamond", "Silent", "Crimson", "Golden", "Shadow", "Sunset", "Rogue"]; const missionNouns = ["Heist", "Job", "Run", "Delivery", "Extraction", "Payload", "Drop", "Interception"]; const missionLocations = ["Vinewood", "Downtown", "The Docks", "Financial District", "Industrial Zone", "The Hills"]; function generateMissionBriefing() { const adj = missionAdjectives[Math.floor(Math.random() * missionAdjectives.length)]; const noun = missionNouns[Math.floor(Math.random() * missionNouns.length)]; const loc = missionLocations[Math.floor(Math.random() * missionLocations.length)]; const title = `The ${adj} ${noun}`; const isMilestone = window.appState.player.mission % 10 === 0; const desc = isMilestone ? `High alert! This is a major operation at ${loc}. The cops are already mobilized. You need to pick up the crew and get out of the city limits fast.` : `Routine pickup at ${loc}. Get in, grab the package, and lose any tails before returning to the safehouse.`; window.appState.currentMission = { title, desc, isMilestone }; elements.briefing.title.textContent = title; elements.briefing.desc.textContent = desc; } // --- DRIVING MINIGAME ENGINE --- const canvas = elements.canvas; const ctx = canvas.getContext('2d'); let gameLoopId = null; const game = { active: false, width: 0, height: 0, roadWidth: 0, laneWidth: 0, speedY: 0, // forward speed distance: 0, targetDistance: 5000, baseSpeed: 10, maxSpeed: 25, linesOffset: 0, cameraY: 0, difficulty: 1, heatOffset: 0, player: { x: 0, y: 0, width: 40, height: 70, health: 100, vx: 0, steerAmount: 0, // -1 to 1 braking: false }, entities: [], particles: [], keys: { left: false, right: false, brake: false }, tilt: 0 }; function resizeCanvas() { const dpr = window.devicePixelRatio || 1; const rect = elements.screens.drive.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; game.width = canvas.width; game.height = canvas.height; game.roadWidth = Math.min(game.width * 0.8, 600 * dpr); game.laneWidth = game.roadWidth / 3; // Adjust player game.player.width = game.laneWidth * 0.5; game.player.height = game.player.width * 1.8; game.player.y = game.height - (game.player.height * 1.5) - (rect.height * 0.15 * dpr); // Keep above buttons if (game.player.x === 0) game.player.x = game.width / 2; // Center initially } function initDriveGame(choiceIndex) { resizeCanvas(); // Setup based on choice (0 = Stealth, 1 = Loud) const isLoud = choiceIndex === 1; const baseHeat = window.appState.player.heat; game.heatOffset = isLoud ? 2 : 0; const totalHeat = Math.min(5, baseHeat + game.heatOffset); game.difficulty = window.appState.config.difficulty * (1 + (window.appState.player.mission * 0.02)) * (isLoud ? 1.5 : 1.0); game.targetDistance = 3000 + (window.appState.player.mission * 100); game.active = true; game.distance = 0; game.speedY = 5; game.player.health = 100; game.player.x = game.width / 2; game.player.vx = 0; game.entities = []; game.particles = []; // UI Update elements.drive.health.style.width = '100%'; elements.drive.health.className = 'h-full bg-green-500 transition-all duration-200'; elements.drive.heat.innerHTML = ''; for(let i=0; i 0.05 * game.difficulty) return; // Spawn in one of 3 lanes const laneIndex = Math.floor(Math.random() * 3); const laneCenter = (game.width/2 - game.roadWidth/2) + (laneIndex * game.laneWidth) + (game.laneWidth/2); // Check if lane is clear const isOccupied = game.entities.some(e => Math.abs(e.x - laneCenter) < game.laneWidth/2 && e.y < 200); if (isOccupied) return; const isCop = Math.random() < (0.1 + (game.heatOffset * 0.1)); game.entities.push({ type: isCop ? 'cop' : 'traffic', x: laneCenter, y: -100, width: game.player.width * 0.9, height: game.player.height * 0.9, speedY: isCop ? (game.speedY + 2) : (game.speedY * 0.5), // Cops chase, traffic is slower color: isCop ? '#3b82f6' : '#eab308' // Blue cop, yellow traffic }); } function spawnParticles(x, y, color) { for(let i=0; i<15; i++) { game.particles.push({ x: x + (Math.random() - 0.5) * 20, y: y + (Math.random() - 0.5) * 20, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10, life: 1.0, color: color }); } } let lastTime = 0; function updateDriveGame(timestamp) { if (!game.active) return; const dt = (timestamp - lastTime) / 1000; lastTime = timestamp; // --- Input & Physics --- let targetSteer = 0; if (window.appState.player.tiltEnabled) { targetSteer = game.tilt; // -1 to 1 } else { if (game.keys.left) targetSteer = -1; if (game.keys.right) targetSteer = 1; } // Smoothing steering game.player.steerAmount += (targetSteer - game.player.steerAmount) * 0.2; // Movement const dpr = window.devicePixelRatio || 1; const moveSpeed = 600 * dpr * dt; game.player.x += game.player.steerAmount * moveSpeed; // Bounds const leftBound = (game.width/2 - game.roadWidth/2) + game.player.width/2; const rightBound = (game.width/2 + game.roadWidth/2) - game.player.width/2; if (game.player.x < leftBound) game.player.x = leftBound; if (game.player.x > rightBound) game.player.x = rightBound; // Acceleration / Braking const targetSpeed = game.keys.brake ? game.baseSpeed * 0.4 : game.maxSpeed; game.speedY += (targetSpeed - game.speedY) * (game.keys.brake ? 0.05 : 0.01); game.distance += game.speedY * dt * 60; // scale up for score game.linesOffset = (game.linesOffset + game.speedY * dt * 60) % (100 * dpr); // Update UI elements.drive.speed.textContent = Math.floor(game.speedY * 5); elements.drive.progress.style.width = `${Math.min(100, (game.distance / game.targetDistance) * 100)}%`; updateEnginePitch(game.speedY / game.maxSpeed); // --- Entities --- spawnEntity(); for (let i = game.entities.length - 1; i >= 0; i--) { let e = game.entities[i]; // Relative movement e.y += (game.speedY - e.speedY) * dt * 60; // Cops change lanes slowly towards player if (e.type === 'cop') { if (e.x < game.player.x - 10) e.x += 50 * dpr * dt; else if (e.x > game.player.x + 10) e.x -= 50 * dpr * dt; } // Collision AABB if (Math.abs(game.player.x - e.x) < (game.player.width/2 + e.width/2) * 0.8 && Math.abs(game.player.y - e.y) < (game.player.height/2 + e.height/2) * 0.8) { // Crash game.player.health -= 25; game.speedY *= 0.3; // lose speed playSound('crash'); // Flash overlay elements.drive.damageOverlay.classList.add('flash'); setTimeout(() => elements.drive.damageOverlay.classList.remove('flash'), 100); // Update Health UI elements.drive.health.style.width = `${Math.max(0, game.player.health)}%`; if (game.player.health < 50) elements.drive.health.className = 'h-full bg-yellow-500 transition-all duration-200'; if (game.player.health <= 25) elements.drive.health.className = 'h-full bg-red-500 transition-all duration-200'; spawnParticles(e.x, e.y, '#eab308'); game.entities.splice(i, 1); if (game.player.health <= 0) { endDriveGame(false); return; } continue; } // Remove off-screen if (e.y > game.height + 100 || e.y < -500) { game.entities.splice(i, 1); } } // Particles for (let i = game.particles.length - 1; i >= 0; i--) { let p = game.particles[i]; p.x += p.vx; p.y += p.vy + game.speedY * dt * 60; p.life -= dt * 2; if (p.life <= 0) game.particles.splice(i, 1); } // Win condition if (game.distance >= game.targetDistance) { endDriveGame(true); return; } drawDriveGame(); gameLoopId = requestAnimationFrame(updateDriveGame); } function drawDriveGame() { const dpr = window.devicePixelRatio || 1; // Clear ctx.fillStyle = '#1e293b'; // off-road color ctx.fillRect(0, 0, game.width, game.height); // Speed Lines (bg) ctx.strokeStyle = '#334155'; ctx.lineWidth = 2 * dpr; for(let i=0; i<10; i++) { let lx = (game.width/10) * i; let ly = (game.linesOffset * 2 + i * 50) % game.height; ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx, ly + 100 * dpr); ctx.stroke(); } // Road const roadLeft = game.width/2 - game.roadWidth/2; ctx.fillStyle = '#0f172a'; ctx.fillRect(roadLeft, 0, game.roadWidth, game.height); // Road lines ctx.fillStyle = '#ffffff'; const lineLen = 40 * dpr; const gap = 60 * dpr; for(let lane=1; lane<3; lane++) { let lx = roadLeft + lane * game.laneWidth; for(let y = game.linesOffset - 100; y < game.height; y += lineLen + gap) { ctx.globalAlpha = 0.5; ctx.fillRect(lx - 2*dpr, y, 4*dpr, lineLen); } } ctx.globalAlpha = 1.0; // Entities game.entities.forEach(e => { ctx.fillStyle = e.color; ctx.beginPath(); ctx.roundRect(e.x - e.width/2, e.y - e.height/2, e.width, e.height, 5*dpr); ctx.fill(); // lights if (e.type === 'cop') { ctx.fillStyle = (Date.now() % 300 < 150) ? '#ef4444' : '#3b82f6'; ctx.fillRect(e.x - e.width/2, e.y - e.height/2, e.width, 5*dpr); } else { ctx.fillStyle = '#f87171'; ctx.fillRect(e.x - e.width/2 + 2*dpr, e.y + e.height/2 - 5*dpr, e.width - 4*dpr, 5*dpr); } }); // Player ctx.save(); ctx.translate(game.player.x, game.player.y); ctx.rotate(game.player.steerAmount * 0.2); // slight lean // Draw Car body ctx.fillStyle = '#f97316'; // orange-500 ctx.beginPath(); ctx.roundRect(-game.player.width/2, -game.player.height/2, game.player.width, game.player.height, 8*dpr); ctx.fill(); // Windows ctx.fillStyle = '#0f172a'; ctx.fillRect(-game.player.width/2 + 4*dpr, -game.player.height/2 + 10*dpr, game.player.width - 8*dpr, 15*dpr); // Windshield ctx.fillRect(-game.player.width/2 + 4*dpr, game.player.height/2 - 20*dpr, game.player.width - 8*dpr, 10*dpr); // Rear // Brake lights if (game.keys.brake) { ctx.fillStyle = '#ef4444'; ctx.shadowColor = '#ef4444'; ctx.shadowBlur = 10; ctx.fillRect(-game.player.width/2 + 2*dpr, game.player.height/2 - 5*dpr, 10*dpr, 5*dpr); ctx.fillRect(game.player.width/2 - 12*dpr, game.player.height/2 - 5*dpr, 10*dpr, 5*dpr); ctx.shadowBlur = 0; } ctx.restore(); // Particles game.particles.forEach(p => { ctx.fillStyle = p.color; ctx.globalAlpha = p.life; ctx.fillRect(p.x, p.y, 4*dpr, 4*dpr); }); ctx.globalAlpha = 1.0; } function endDriveGame(success) { game.active = false; cancelAnimationFrame(gameLoopId); stopEngineSound(); const isLoud = game.heatOffset > 0; let cashReward = 0; if (success) { playSound('success'); cashReward = Math.floor(Math.random() * 1000) + 2000 + (isLoud ? 2000 : 0) + (window.appState.player.mission * 100); window.appState.player.cash += cashReward; window.appState.player.mission++; // Adjust heat if (isLoud) window.appState.player.heat = Math.min(5, window.appState.player.heat + 1); else window.appState.player.heat = Math.max(1, window.appState.player.heat - 1); elements.result.icon.className = "fas fa-check text-4xl text-green-500"; elements.result.container.className = "w-24 h-24 rounded-full flex items-center justify-center mb-6 border-4 border-green-500 shadow-[0_0_20px_rgba(34,197,94,0.4)]"; elements.result.title.textContent = "Mission Cleared"; elements.result.title.className = "text-4xl font-black italic mb-2 uppercase tracking-wide text-green-400"; elements.result.desc.textContent = "Clean getaway. The cops lost your tail."; elements.result.cash.textContent = `+$${cashReward.toLocaleString()}`; elements.result.cash.className = "text-3xl font-black text-green-400"; } else { cashReward = Math.floor(window.appState.player.cash * 0.1); // Lose 10% on bust window.appState.player.cash -= cashReward; elements.result.icon.className = "fas fa-times text-4xl text-red-500"; elements.result.container.className = "w-24 h-24 rounded-full flex items-center justify-center mb-6 border-4 border-red-500 shadow-[0_0_20px_rgba(239,68,68,0.4)]"; elements.result.title.textContent = "Busted"; elements.result.title.className = "text-4xl font-black italic mb-2 uppercase tracking-wide text-red-400"; elements.result.desc.textContent = "Car totaled. Had to pay off the cops to let you walk."; elements.result.cash.textContent = `-$${cashReward.toLocaleString()}`; elements.result.cash.className = "text-3xl font-black text-red-500"; } saveAppStates(); switchScreen('result'); } // --- INPUT HANDLING --- function setupInput() { // Touch Buttons const btnLeft = document.getElementById('btn-steer-left'); const btnRight = document.getElementById('btn-steer-right'); const btnBrake = document.getElementById('btn-brake'); const bindBtn = (btn, key) => { btn.addEventListener('pointerdown', (e) => { e.preventDefault(); game.keys[key] = true; btn.classList.add('active'); }); btn.addEventListener('pointerup', (e) => { e.preventDefault(); game.keys[key] = false; btn.classList.remove('active'); }); btn.addEventListener('pointerleave', (e) => { e.preventDefault(); game.keys[key] = false; btn.classList.remove('active'); }); }; bindBtn(btnLeft, 'left'); bindBtn(btnRight, 'right'); bindBtn(btnBrake, 'brake'); // Device Orientation (Tilt) window.addEventListener('deviceorientation', (e) => { if (!window.appState.player.tiltEnabled) return; // Gamma: left/right tilt. Normalize to roughly -1 to 1 based on 45 deg max. let gamma = e.gamma || 0; // Handle portrait orientation properly if (gamma > 45) gamma = 45; if (gamma < -45) gamma = -45; // Deadzone if (Math.abs(gamma) < 5) game.tilt = 0; else game.tilt = (gamma + (gamma < 0 ? 5 : -5)) / 40; }); // Tilt Request Flow elements.ui.tiltBtn.addEventListener('click', async () => { if (!window.appState.player.tiltEnabled) { if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') { try { const permission = await DeviceOrientationEvent.requestPermission(); if (permission === 'granted') { window.appState.player.tiltEnabled = true; } else { alert("Tilt permission denied."); } } catch (e) { console.error(e); alert("Tilt permission requires a secure context (HTTPS) and direct user action."); } } else { // Android or non-requesting browser window.appState.player.tiltEnabled = true; } } else { window.appState.player.tiltEnabled = false; } updateHomeUI(); saveAppStates(); }); // Navigation Flow document.getElementById('btn-start-mission').addEventListener('click', () => { generateMissionBriefing(); switchScreen('briefing'); }); document.getElementById('btn-cancel-mission').addEventListener('click', () => { switchScreen('home'); }); document.getElementById('btn-choice-1').addEventListener('click', () => initDriveGame(0)); document.getElementById('btn-choice-2').addEventListener('click', () => initDriveGame(1)); document.getElementById('btn-continue').addEventListener('click', () => { updateHomeUI(); switchScreen('home'); }); // Share Logic elements.ui.shareBtn.addEventListener('click', async () => { if (elements.ui.shareBtn.dataset.loading === 'true') return; elements.ui.shareBtn.dataset.loading = 'true'; try { const el = document.getElementById('share-zone'); const jpgImg = await window.snapdom.toJpg(el, { quality: 0.8, width: el.offsetWidth * 2, height: el.offsetHeight * 2 }); sendMessage('invoke_share', { title: `I'm on Mission ${window.appState.player.mission} in Sunset City!`, coverImageBase64: jpgImg.src, appStates: { pageSettings: window.appState.player } }, false); } catch(e) { console.error("Share fail", e); } finally { elements.ui.shareBtn.dataset.loading = 'false'; } }); window.addEventListener('resize', () => { if (game.active) resizeCanvas(); }); } // --- INIT APP --- async function initApp() { applyAllEditableValues(); initBGM(); setupInput(); await getAppStates(); // Loads user data if exists // Check if game was shared and we need to load shared state // (Handled implicitly by getAppStates returning the URL state if present) switchScreen('home'); }