RANK: 1/12
RACING...
STUMBLE DASH
CHOOSE YOUR LOOK
ROOM: XXXX
QUALIFIED!
RANK #1
// Helpers function getConfig(category, id) { return window.sekaiEditable[category]?.find(i => i.id === id)?.value; } function setNestedValue(obj, path, value) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { if (!current[parts[i]]) current[parts[i]] = {}; current = current[parts[i]]; } current[parts[parts.length - 1]] = value; } 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.src = img.value; else if (img.property === 'backgroundImage') el.style.backgroundImage = img.value ? `url('${img.value}')` : 'none'; } } }); 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; } } }); } // Preview Editing API (function setupPreviewEditingAPI() { window.addEventListener('message', (event) => { const msg = event.data; if (msg?.origin !== 'sekai_gaming_iframe_api') return; switch (msg.type) { case 'get_editable_metadata': window.parent.postMessage({ origin: 'sekai_gaming_iframe_api', type: 'receive_editable_metadata', taskId: msg.taskId, data: window.sekaiEditable || {} }, '*'); break; case '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 } }, '*'); break; case 'get_current_html': window.parent.postMessage({ origin: 'sekai_gaming_iframe_api', type: 'receive_current_html', taskId: msg.taskId, data: { html: document.documentElement.outerHTML } }, '*'); break; case 'reset_changes': location.reload(); break; } }); 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') { const wasPlaying = !el.paused; el.src = value; el.load(); if (wasPlaying) el.play().catch(() => {}); } else if (item.property === 'src') { el.src = value; } else if (item.property === 'backgroundImage') { el.style.backgroundImage = value ? `url('${value}')` : 'none'; } else { el.textContent = value; } } return true; } return false; } })(); // ========================================== // 2. IFRAME API HELPERS // ========================================== const IframeAPI = { sendMessage(type, data, awaitResponse = true, sessionCode = null) { return new Promise((resolve, reject) => { const taskId = crypto.randomUUID(); const message = { origin: 'sekai_gaming_iframe_api', type, taskId, data }; if (sessionCode) message.sessionCode = sessionCode; if (awaitResponse) { const expectedType = 'receive_' + (type === 'upload_image' ? 'upload_image' : type); const listener = (event) => { const msg = event.data; if (msg?.origin === 'sekai_gaming_iframe_api' && msg.type === expectedType && msg.taskId === taskId) { window.removeEventListener('message', listener); if (msg.error) reject(new Error(msg.error.message)); else resolve(msg.data); } }; window.addEventListener('message', listener); setTimeout(() => { window.removeEventListener('message', listener); reject(new Error('Timeout')); }, 5000); } window.parent.postMessage(message, '*'); if (!awaitResponse) resolve(); }); }, vibrate(duration) { this.sendMessage('gen_vibration', { duration }, false).catch(()=>{}); } }; // ========================================== // 3. GAME LOGIC & STATE // ========================================== // App State accessible by paths window.appState = { config: { speed: 450, gravity: 2800, jumpForce: 950 }, stats: { level: 1, attempts: 1 } }; const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d', { alpha: false }); // UI Elements Cache const elements = { screenStart: document.getElementById('screen-start'), screenDeath: document.getElementById('screen-death'), screenWin: document.getElementById('screen-win'), screenEnd: document.getElementById('screen-end'), btnRetry: document.getElementById('btn-retry'), btnNext: document.getElementById('btn-next'), btnShare: document.getElementById('btn-share'), btnRestart: document.getElementById('btn-restart-game'), progressBar: document.getElementById('progress-bar'), attemptCounter: document.getElementById('attempt-counter'), deathStats: document.getElementById('death-stats'), winStats: document.getElementById('win-stats') }; // Game Constants & Architecture const TILE_SIZE = 50; const PLAYER_SIZE = 36; // Slightly smaller than tile for leniency const GROUND_HEIGHT = 150; let GAME_STATE = 'IDLE'; // IDLE, PLAYING, DEAD, WIN let lastTime = 0; let animationId; // Viewport scaling let vWidth = 1000; let vHeight = 600; let scale = 1; // Entities let player = { x: 200, y: 0, w: PLAYER_SIZE, h: PLAYER_SIZE, vy: 0, isGrounded: true, rotation: 0 }; let obstacles = []; let particles = []; let levelLength = 0; let cameraX = 0; // Level Designs (Map strings where bottom row is y=1 above ground) // # = Block, ^ = Spike, > = Trigger Win const levels = [ // Level 1: Introduction [ " ", " ", " >", " ### ### #", " # # ", " ^ ## ^ ## ^ ", " # ### # ### ##### ### ### ^ ^ ^ " ], // Level 2: Staircases and Gaps [ " ", " >", " # # # #", " ### # # # ", " # ##### # # # ", " # ### ####### # # # ", " # ### ##### ^ ######### ^ # # # ^ ^ " ], // Level 3: Tight timings [ " ", " >", " # # # # #", " ### ### # # # # ", " # # # # # # ### # # # # ", " # # # # # # # # # # # # # ### ### # ", " ^ ^ ### # ^ # ### ### # ^ # ### ^^^^^ # # # ^ # # ##### ##### # ^^^^ " ] ]; // Audio Management const audioContext = new (window.AudioContext || window.webkitAudioContext)(); function playProceduralSound(type) { if (audioContext.state === 'suspended') audioContext.resume(); const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.connect(gain); gain.connect(audioContext.destination); const now = audioContext.currentTime; if (type === 'jump') { osc.type = 'square'; osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(300, now + 0.1); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); osc.start(now); osc.stop(now + 0.1); } else if (type === 'crash') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, now); osc.frequency.exponentialRampToValueAtTime(20, now + 0.3); gain.gain.setValueAtTime(0.2, now); gain.gain.linearRampToValueAtTime(0.01, now + 0.3); osc.start(now); osc.stop(now + 0.3); } else if (type === 'win') { osc.type = 'sine'; osc.frequency.setValueAtTime(440, now); osc.frequency.setValueAtTime(554.37, now + 0.1); // C# osc.frequency.setValueAtTime(659.25, now + 0.2); // E gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.2, now + 0.05); gain.gain.linearRampToValueAtTime(0, now + 0.5); osc.start(now); osc.stop(now + 0.5); } } function playSound(id, fallbackType) { const el = document.getElementById('sfx-' + id); if (el && el.src && el.src !== window.location.href) { el.currentTime = 0; el.play().catch(()=>{}); } else { playProceduralSound(fallbackType); } } 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(); } // Parse Level String Array into Entities function loadLevel(levelIndex) { const map = levels[levelIndex]; obstacles = []; let maxCol = 0; // Map height determines ground offset internally, but we render bottom up. const mapHeight = map.length; for (let r = 0; r < map.length; r++) { const row = map[r]; for (let c = 0; c < row.length; c++) { const char = row[c]; // Calculate logical Y from bottom up (0 is ground) const yDistFromBottom = mapHeight - 1 - r; const yPos = yDistFromBottom * TILE_SIZE; const xPos = c * TILE_SIZE + 800; // Offset start by 800 units if (char === '#') { obstacles.push({ type: 'block', x: xPos, y: yPos, w: TILE_SIZE, h: TILE_SIZE }); } else if (char === '^') { // Spikes are slightly smaller than tile for fair hitboxes const hitW = TILE_SIZE * 0.6; const hitH = TILE_SIZE * 0.7; obstacles.push({ type: 'spike', x: xPos + (TILE_SIZE - hitW)/2, y: yPos, w: hitW, h: hitH, drawX: xPos, drawW: TILE_SIZE }); } else if (char === '>') { obstacles.push({ type: 'trigger', x: xPos, y: yPos, w: TILE_SIZE, h: TILE_SIZE * 10 }); } if (char !== ' ') { maxCol = Math.max(maxCol, c); } } } levelLength = maxCol * TILE_SIZE + 800 + 400; // Extra padding at end resetPlayer(); updateUI(); } function resetPlayer() { player.x = 200; player.y = 0; player.vy = 0; player.isGrounded = true; player.rotation = 0; cameraX = 0; particles = []; elements.progressBar.style.width = '0%'; } function spawnParticles(x, y, color) { for (let i = 0; i < 20; i++) { particles.push({ x: x, y: y, vx: (Math.random() - 0.5) * 600, vy: (Math.random() - 0.5) * 600 - 200, life: 1.0, color: color }); } } // ========================================== // 4. GAME LOOP & PHYSICS // ========================================== function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Calculate scale to guarantee at least 'vWidth' units horizontally scale = canvas.width / vWidth; vHeight = canvas.height / scale; } window.addEventListener('resize', resize); function jump() { if (GAME_STATE === 'IDLE') { // Start game GAME_STATE = 'PLAYING'; elements.screenStart.style.opacity = '0'; setTimeout(() => { if (GAME_STATE === 'PLAYING') elements.screenStart.classList.add('hidden'); }, 300); } if (GAME_STATE === 'PLAYING' && player.isGrounded) { player.vy = -appState.config.jumpForce; player.isGrounded = false; playSound('jump', 'jump'); IframeAPI.vibrate(20); } } // Input handling - unified pointer events document.addEventListener('pointerdown', (e) => { // Prevent jump if clicking UI buttons if (e.target.closest('button')) return; if (GAME_STATE === 'IDLE' || GAME_STATE === 'PLAYING') { jump(); } }); function AABB(r1, r2) { return r1.x < r2.x + r2.w && r1.x + r1.w > r2.x && r1.y < r2.y + r2.h && r1.y + r1.h > r2.y; } function update(dt) { if (dt > 0.1) dt = 0.1; // Cap delta time to prevent physics glitches if (GAME_STATE === 'PLAYING') { // Apply Gravity player.vy += appState.config.gravity * dt; // Keep track of old position for collision resolution const oldY = player.y; const oldX = player.x; // Move Player player.y -= player.vy * dt; // Subtract because Y is up in logic player.x += appState.config.speed * dt; // Rotation if (!player.isGrounded) { player.rotation += 400 * dt * (Math.PI / 180); } // Floor Collision player.isGrounded = false; if (player.y <= 0) { player.y = 0; player.vy = 0; player.isGrounded = true; // Snap rotation to 90 degrees player.rotation = Math.round(player.rotation / (Math.PI/2)) * (Math.PI/2); } // Obstacle Collision const pRect = { x: player.x, y: player.y, w: player.w, h: player.h }; for (let obs of obstacles) { // Only check nearby obstacles if (obs.x > player.x + 400 || obs.x + obs.w < player.x - 100) continue; if (AABB(pRect, obs)) { if (obs.type === 'trigger') { levelComplete(); continue; } if (obs.type === 'spike') { die(); break; } if (obs.type === 'block') { // Check if hit from top if (oldY >= obs.y + obs.h - 5 && player.vy < 0) { // Landed on top player.y = obs.y + obs.h; player.vy = 0; player.isGrounded = true; player.rotation = Math.round(player.rotation / (Math.PI/2)) * (Math.PI/2); } else { // Hit side or bottom die(); break; } } } } // Camera follow cameraX = player.x - 200; // Update Progress UI const progress = Math.min(100, Math.max(0, (player.x / levelLength) * 100)); elements.progressBar.style.width = `${progress}%`; // Check fall out of world (failsafe) if (player.y < -100) die(); } // Update Particles for (let i = particles.length - 1; i >= 0; i--) { let p = particles[i]; p.x += p.vx * dt; p.y -= p.vy * dt; p.vy += appState.config.gravity * dt; // gravity applies p.life -= dt * 2; if (p.life <= 0) particles.splice(i, 1); } } function die() { GAME_STATE = 'DEAD'; playSound('crash', 'crash'); IframeAPI.vibrate(100); // Visuals spawnParticles(player.x + player.w/2, player.y + player.h/2, getComputedStyle(document.documentElement).getPropertyValue('--player-color').trim()); // UI const progress = Math.min(100, Math.max(0, Math.floor((player.x / levelLength) * 100))); elements.deathStats.textContent = `PROGRESS: ${progress}%`; showScreen(elements.screenDeath); } function levelComplete() { GAME_STATE = 'WIN'; playSound('win', 'win'); elements.winStats.textContent = `ATTEMPTS: ${appState.stats.attempts}`; if (appState.stats.level >= levels.length) { showScreen(elements.screenEnd); } else { showScreen(elements.screenWin); } } // Rendering helpers function getWorldToScreen(wx, wy) { const sx = (wx - cameraX) * scale; // Invert Y: virtual 0 is GROUND_HEIGHT from bottom const groundPixelY = canvas.height - GROUND_HEIGHT; const sy = groundPixelY - (wy * scale); return { x: sx, y: sy }; } function draw(time) { // Background ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--bg-color').trim(); ctx.fillRect(0, 0, canvas.width, canvas.height); // Ground Line const groundY = canvas.height - GROUND_HEIGHT; ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(canvas.width, groundY); ctx.stroke(); // Grid Lines (Parallax) ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; const bgOffsetX = -(cameraX * 0.5) % (TILE_SIZE * scale); for (let x = bgOffsetX; x < canvas.width; x += TILE_SIZE * scale) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } // Glow settings ctx.globalCompositeOperation = 'source-over'; // Changed from lighter for better performance, shadowBlur handles glow const playerCol = getComputedStyle(document.documentElement).getPropertyValue('--player-color').trim(); const obsCol = getComputedStyle(document.documentElement).getPropertyValue('--obstacle-color').trim(); const spikeCol = getComputedStyle(document.documentElement).getPropertyValue('--spike-color').trim(); // Draw Obstacles ctx.shadowBlur = 10; for (let obs of obstacles) { if (obs.x > cameraX + vWidth + 100 || obs.x + obs.w < cameraX - 100) continue; // Culling if (obs.type === 'trigger') continue; if (obs.type === 'block') { const pos = getWorldToScreen(obs.x, obs.y + obs.h); // Top-left corner const w = obs.w * scale; const h = obs.h * scale; ctx.shadowColor = obsCol; ctx.strokeStyle = obsCol; ctx.lineWidth = 3; ctx.strokeRect(pos.x, pos.y, w, h); ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(pos.x, pos.y, w, h); } else if (obs.type === 'spike') { const pos = getWorldToScreen(obs.drawX, obs.y); // Bottom-left const w = obs.drawW * scale; const h = obs.h * scale; // Visual height ctx.shadowColor = spikeCol; ctx.fillStyle = spikeCol; ctx.beginPath(); ctx.moveTo(pos.x, pos.y); // Bottom left ctx.lineTo(pos.x + w/2, pos.y - h); // Top center ctx.lineTo(pos.x + w, pos.y); // Bottom right ctx.closePath(); ctx.fill(); } } // Draw Player if (GAME_STATE !== 'DEAD') { const pCenter = getWorldToScreen(player.x + player.w/2, player.y + player.h/2); const sW = player.w * scale; const sH = player.h * scale; ctx.shadowColor = playerCol; ctx.fillStyle = playerCol; ctx.save(); ctx.translate(pCenter.x, pCenter.y); ctx.rotate(player.rotation); // Hollow square with thick border ctx.lineWidth = 4; ctx.strokeStyle = playerCol; ctx.strokeRect(-sW/2, -sH/2, sW, sH); // Small inner core ctx.fillStyle = 'white'; ctx.shadowBlur = 0; ctx.fillRect(-sW/4, -sH/4, sW/2, sH/2); ctx.restore(); // Trail (simple) if (GAME_STATE === 'PLAYING') { ctx.fillStyle = playerCol; ctx.globalAlpha = 0.3; const trailPos = getWorldToScreen(player.x - 20 + player.w/2, player.y + player.h/2); ctx.fillRect(trailPos.x - sW/2, trailPos.y - sH/2, sW, sH); ctx.globalAlpha = 1.0; } } // Draw Particles ctx.shadowBlur = 5; for (let p of particles) { const pos = getWorldToScreen(p.x, p.y); ctx.shadowColor = p.color; ctx.fillStyle = p.color; ctx.globalAlpha = p.life; const size = 8 * scale * p.life; ctx.fillRect(pos.x - size/2, pos.y - size/2, size, size); } ctx.globalAlpha = 1.0; ctx.shadowBlur = 0; } function gameLoop(timestamp) { const dt = (timestamp - lastTime) / 1000; lastTime = timestamp; update(dt || 0); draw(timestamp); animationId = requestAnimationFrame(gameLoop); } // ========================================== // 5. UI & FLOW MANAGEMENT // ========================================== function showScreen(screenEl) { // Hide all first document.querySelectorAll('.screen').forEach(s => s.classList.remove('visible')); screenEl.classList.add('visible'); } function hideAllScreens() { document.querySelectorAll('.screen').forEach(s => s.classList.remove('visible')); } function updateUI() { elements.attemptCounter.textContent = `LEVEL ${appState.stats.level} - ATTEMPT ${appState.stats.attempts}`; } // Button Listeners elements.btnRetry.addEventListener('click', () => { appState.stats.attempts++; GAME_STATE = 'IDLE'; hideAllScreens(); resetPlayer(); updateUI(); elements.screenStart.classList.remove('hidden'); elements.screenStart.style.opacity = '1'; }); elements.btnNext.addEventListener('click', () => { appState.stats.level++; appState.stats.attempts = 1; loadLevel(appState.stats.level - 1); GAME_STATE = 'IDLE'; hideAllScreens(); elements.screenStart.classList.remove('hidden'); elements.screenStart.style.opacity = '1'; }); elements.btnRestart.addEventListener('click', () => { appState.stats.level = 1; appState.stats.attempts = 1; loadLevel(0); GAME_STATE = 'IDLE'; hideAllScreens(); elements.screenStart.classList.remove('hidden'); elements.screenStart.style.opacity = '1'; }); elements.btnShare.addEventListener('click', async (e) => { const btn = e.currentTarget; if (btn.dataset.loading === 'true') return; btn.dataset.loading = 'true'; btn.disabled = true; btn.textContent = '...'; try { const element = document.getElementById('share-zone'); if (window.snapdom) { const jpgImg = await window.snapdom.toJpg(element, { quality: 0.8, width: element.offsetWidth * 2, height: element.offsetHeight * 2 }); await IframeAPI.sendMessage('invoke_share', { title: `I crashed at ${elements.deathStats.textContent.replace('PROGRESS: ', '')} on Level ${appState.stats.level} in Neon Dash!`, coverImageBase64: jpgImg.src }, false); } } catch (err) { console.error(err); } finally { btn.dataset.loading = 'false'; btn.disabled = false; btn.textContent = 'SHARE'; } }); // Initialize function initApp() { applyAllEditableValues(); initBGM(); resize(); // Load initial state appState.stats.level = 1; appState.stats.attempts = 1; loadLevel(0); elements.screenStart.style.opacity = '1'; lastTime = performance.now(); requestAnimationFrame(gameLoop); } document.addEventListener('DOMContentLoaded', initApp);