Created
August 9, 2025 13:35
-
-
Save ji4ko0/be0efc8542a12b1abcdbc1535541444c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>Crystal Tower (HTML5)</title> | |
| <style> | |
| :root { | |
| --bg: #07102a; | |
| --paper: #e6f7ff; | |
| --accent: #70d6ff; | |
| --lava-bg: #2a0707; | |
| --lava-accent: #ff7070; | |
| } | |
| html, | |
| body { | |
| height: 100%; | |
| margin: 0; | |
| background: var(--bg); | |
| font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial; | |
| color: var(--paper); | |
| } | |
| body.theme-lava { | |
| background: var(--lava-bg); | |
| color: #ffc9c9; | |
| } | |
| body.theme-lava .gameOverlay { | |
| background: linear-gradient(180deg, rgba(20, 4, 4, 0.6), rgba(30, 8, 8, 0.5)); | |
| } | |
| body.theme-lava .btn { | |
| background: linear-gradient(180deg, #ff7070, #ff3e3e); | |
| } | |
| body.theme-lava .uiCoins svg { | |
| fill: #ff9900; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| .ui { | |
| position: fixed; | |
| left: 16px; | |
| bottom: 16px; | |
| z-index: 20; | |
| backdrop-filter: blur(6px); | |
| padding: 10px; | |
| border-radius: 12px; | |
| background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01)); | |
| border: 1px solid rgba(255, 255, 255, 0.04); | |
| } | |
| .centerOverlay { | |
| position: fixed; | |
| left: 50%; | |
| top: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 25; | |
| text-align: center; | |
| } | |
| .gameOverlay { | |
| position: fixed; | |
| left: 50%; | |
| top: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 25; | |
| text-align: center; | |
| background: linear-gradient(180deg, rgba(4, 8, 20, 0.6), rgba(8, 16, 30, 0.5)); | |
| padding: 22px; | |
| border-radius: 14px; | |
| border: 1px solid rgba(255, 255, 255, 0.04); | |
| } | |
| .btn { | |
| background: linear-gradient(180deg, #70d6ff, #3e8cff); | |
| color: #001; | |
| padding: 10px 16px; | |
| border-radius: 12px; | |
| border: none; | |
| font-weight: 700; | |
| cursor: pointer; | |
| box-shadow: 0 6px 18px rgba(62, 140, 255, 0.18); | |
| transition: transform 0.2s; | |
| } | |
| .btn:hover { | |
| transform: translateY(-2px); | |
| } | |
| .btn.btn-secondary { | |
| background: linear-gradient(180deg, #5c6275, #3c404d); | |
| color: #fff; | |
| box-shadow: none; | |
| margin-top: 10px; | |
| } | |
| .btn.disabled { | |
| opacity: 0.5; | |
| pointer-events: none; | |
| } | |
| .muted { | |
| opacity: 0.85; | |
| font-size: 13px; | |
| } | |
| .touch { | |
| position: fixed; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| height: 24vh; | |
| display: none; | |
| z-index: 15; | |
| } | |
| #uiCoins { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| margin-left: 12px; | |
| font-weight: 700; | |
| } | |
| #uiCoins svg { | |
| width: 14px; | |
| height: 14px; | |
| fill: #ffd700; | |
| } | |
| body.theme-lava #uiCoins svg { | |
| fill: #ff9900; | |
| } | |
| .stage-message { | |
| position: fixed; | |
| top: 20%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| color: white; | |
| font-size: 48px; | |
| font-weight: bold; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.5); | |
| animation: fadeOut 2s forwards; | |
| } | |
| @keyframes fadeOut { | |
| 0% { opacity: 1; transform: translate(-50%, 0) scale(1); } | |
| 100% { opacity: 0; transform: translate(-50%, -50px) scale(1.2); } | |
| } | |
| @media (max-width: 900px) { | |
| .touch { | |
| display: block; | |
| } | |
| .ui { | |
| left: 8px; | |
| bottom: 8px; | |
| } | |
| .centerOverlay, .gameOverlay { | |
| width: 86%; | |
| } | |
| } | |
| @keyframes comboPulse { | |
| 0%, 100% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.1); opacity: 0.8; } | |
| } | |
| .combo-pulse { | |
| animation: comboPulse 0.5s ease-in-out; | |
| color: #ffd700; | |
| } | |
| body.theme-lava .combo-pulse { | |
| color: #ff9900; | |
| } | |
| .powerup-info { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 20px; | |
| margin-top: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .powerup-item { | |
| display: flex; | |
| align-items: center; | |
| flex-direction: column; | |
| text-align: center; | |
| max-width: 120px; | |
| } | |
| .powerup-item svg { | |
| width: 40px; | |
| height: 40px; | |
| fill: var(--paper); | |
| stroke: var(--paper); | |
| stroke-width: 0; | |
| } | |
| .powerup-item p { | |
| font-size: 14px; | |
| margin: 5px 0 0 0; | |
| opacity: 0.9; | |
| } | |
| #shop-menu { | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .shop-item { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| width: 100%; | |
| max-width: 300px; | |
| padding: 10px; | |
| border-radius: 12px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .shop-item-preview { | |
| width: 40px; | |
| height: 40px; | |
| background-size: cover; | |
| background-position: center; | |
| border-radius: 8px; | |
| } | |
| .powerup-timer { | |
| position: fixed; | |
| top: 16px; | |
| right: 16px; | |
| z-index: 20; | |
| backdrop-filter: blur(6px); | |
| padding: 12px; | |
| border-radius: 12px; | |
| background: linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| font-size: 16px; | |
| line-height: 1.5; | |
| min-width: 140px; | |
| } | |
| .powerup-timer div { | |
| font-weight: bold; | |
| color: var(--accent); | |
| } | |
| body.theme-lava .powerup-timer div { | |
| color: var(--lava-accent); | |
| } | |
| </style> | |
| </head> | |
| <body class="theme-ice"> | |
| <audio id="bgMusic" src="https://assets.mixkit.co/sfx/download/mixkit-small-bell-ding-1090.wav" loop></audio> | |
| <audio id="sfx-coin" src="https://assets.mixkit.co/sfx/download/mixkit-game-coin-2051.wav"></audio> | |
| <audio id="sfx-powerup" src="https://assets.mixkit.co/sfx/download/mixkit-arcade-bonus-alert-315.wav"></audio> | |
| <audio id="sfx-gameover" src="https://assets.mixkit.co/sfx/download/mixkit-arcade-retro-game-over-213.wav"></audio> | |
| <div class="ui"> | |
| <div style="font-weight:700">Crystal Tower</div> | |
| <div style="font-size:13px;margin-top:6px">Speed: <span id="uiSpeed">0</span> • Score: <span id="uiScore">0</span></div> | |
| <div style="font-size:13px">Combo: <span id="uiCombo">0</span></div> | |
| <div id="uiCoins"> | |
| <svg viewBox="0 0 24 24"> | |
| <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-.5-14H11c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5h.5c.83 0 1.5.67 1.5 1.5v.5c0 .83-.67 1.5-1.5 1.5h-1c-.55 0-1 .45-1 1s.45 1 1 1h1.5c1.38 0 2.5-1.12 2.5-2.5s-1.12-2.5-2.5-2.5h-.5c-.83 0-1.5-.67-1.5-1.5v-.5c0-.83.67-1.5 1.5-1.5h1c.55 0 1-.45 1-1s-.45-1-1-1z" stroke="none"/> | |
| </svg> | |
| <span id="uiCoinCount">0</span> | |
| </div> | |
| </div> | |
| <div class="powerup-timer" id="powerup-timer" style="display:none;"> | |
| <div id="shield-timer" style="display:none;">Shield: <span id="shield-timer-value"></span>s</div> | |
| <div id="magnet-timer" style="display:none;">Magnet: <span id="magnet-timer-value"></span>s</div> | |
| <div id="score-multiplier-timer" style="display:none;">2x Score: <span id="score-multiplier-timer-value"></span>s</div> | |
| </div> | |
| <div class="centerOverlay" id="menu"> | |
| <div class="gameOverlay"> | |
| <h2 style="margin:0 0 6px 0">Crystal Tower</h2> | |
| <div class="muted">Smooth physics • Parallax • Shiny ice • Modern look</div> | |
| <div style="font-size:13px;margin-top:10px">High Score: <span id="highScore">0</span></div> | |
| <div style="margin-top:12px"> | |
| <button id="startBtn" class="btn">Start Game</button> | |
| </div> | |
| <div style="margin-top:10px"> | |
| <button id="shopBtn" class="btn btn-secondary">Skin Shop</button> | |
| </div> | |
| <div style="margin-top:10px"> | |
| <button id="toggleMusicBtn" class="btn btn-secondary">Music: Off</button> | |
| </div> | |
| <div id="reviveContainer"></div> | |
| <div style="margin-top:10px" class="muted">Controls: ← → / A D — move, ↑ / Space — jump</div> | |
| </div> | |
| </div> | |
| <div class="centerOverlay" id="pregame-menu" style="display:none;"> | |
| <div class="gameOverlay"> | |
| <h2 style="margin:0 0 6px 0">In-game Power-ups</h2> | |
| <div class="powerup-info"> | |
| <div class="powerup-item"> | |
| <svg viewBox="0 0 24 24"><path d="M12 2L2 22h20L12 2zm0 4l7.6 15.2H4.4L12 6z" stroke="none"/><path d="M12 8l4 8h-8l4-8z" stroke="none"/></svg> | |
| <p>Super Jump</p> | |
| </div> | |
| <div class="powerup-item"> | |
| <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-.5-14H11c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5h.5c.83 0 1.5.67 1.5 1.5v.5c0 .83-.67 1.5-1.5 1.5h-1c-.55 0-1 .45-1 1s.45 1 1 1h1.5c1.38 0 2.5-1.12 2.5-2.5s-1.12-2.5-2.5-2.5h-.5c-.83 0-1.5-.67-1.5-1.5v-.5c0-.83.67-1.5 1.5-1.5h1c.55 0 1-.45 1-1s-.45-1-1-1z" stroke="none"/></svg> | |
| <p>Slow Motion</p> | |
| </div> | |
| <div class="powerup-item"> | |
| <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-4.5-8c0-.83.67-1.5 1.5-1.5h1c.83 0 1.5-.67 1.5-1.5v-.5c0-.83-.67-1.5-1.5-1.5h-1c-.55 0-1-.45-1-1s.45-1 1-1h1.5c1.38 0 2.5-1.12 2.5-2.5S13.38 5 12 5h-.5c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5h.5c.83 0 1.5.67 1.5 1.5v.5c0 .83-.67 1.5-1.5 1.5h-1c-.55 0-1 .45-1 1s.45 1 1 1h1.5c1.38 0 2.5-1.12 2.5-2.5s-1.12-2.5-2.5-2.5z" stroke="none"/></svg> | |
| <p>Magnet</p> | |
| </div> | |
| <div class="powerup-item"> | |
| <svg viewBox="0 0 24 24"><path d="M12 2L4 5v6.09c0 5.05 3.41 9.76 8 10.91 4.59-1.15 8-5.86 8-10.91V5l-8-3zm0 2.91L18.42 6.5l-6.42 2.4-6.42-2.4L12 4.91zM12 20.09c-3.8-1.07-6.9-4.5-6.9-9.09V7.12l6.9-2.58 6.9 2.58v3.88c0 4.59-3.1 8.02-6.9 9.09z" stroke="none" fill="var(--paper)"/></svg> | |
| <p>Shield</p> | |
| </div> | |
| <div class="powerup-item"> | |
| <svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-4.5-8c0-.83.67-1.5 1.5-1.5h1c.83 0 1.5-.67 1.5-1.5v-.5c0-.83-.67-1.5-1.5-1.5h-1c-.55 0-1-.45-1-1s.45-1 1-1h1.5c1.38 0 2.5-1.12 2.5-2.5S13.38 5 12 5h-.5c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5h.5c.83 0 1.5.67 1.5 1.5v.5c0 .83-.67 1.5-1.5 1.5h-1c-.55 0-1 .45-1 1s.45 1 1 1h1.5c1.38 0 2.5-1.12 2.5-2.5s-1.12-2.5-2.5-2.5z" stroke="none"/></svg> | |
| <p>2x Score</p> | |
| </div> | |
| </div> | |
| <div style="margin-top:20px"><button id="pregame-startBtn" class="btn">Continue</button></div> | |
| </div> | |
| </div> | |
| <div class="centerOverlay" id="shop-menu"> | |
| <div class="gameOverlay"> | |
| <h2 style="margin:0 0 6px 0">Skin Shop</h2> | |
| <div id="skin-list" style="margin-top: 20px; text-align: left; max-height: 400px; overflow-y: auto;"></div> | |
| <div style="margin-top:20px"><button id="backBtn" class="btn">Back</button></div> | |
| </div> | |
| </div> | |
| <div id="stageMessageContainer"></div> | |
| <canvas id="c"></canvas> | |
| <div class="touch" id="touchAreas"> | |
| <div id="leftArea" style="position:absolute;left:0;top:0;bottom:0;width:50%"></div> | |
| <div id="rightArea" style="position:absolute;right:0;top:0;bottom:0;width:50%"></div> | |
| </div> | |
| <script> | |
| /* =========================== | |
| Crystal Tower | |
| Single-file, procedural graphics | |
| =========================== */ | |
| const canvas = document.getElementById('c'); | |
| const ctx = canvas.getContext('2d', { alpha:true }); | |
| let DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); | |
| function resize(){ | |
| DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); | |
| canvas.width = Math.floor(innerWidth * DPR); | |
| canvas.height = Math.floor(innerHeight * DPR); | |
| canvas.style.width = innerWidth + 'px'; | |
| canvas.style.height = innerHeight + 'px'; | |
| ctx.setTransform(DPR,0,0,DPR,0,0); | |
| } | |
| addEventListener('resize', resize); | |
| resize(); | |
| /* Utilities */ | |
| const rand=(a,b)=>Math.random()*(b-a)+a; | |
| const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); | |
| const lerp=(a,b,t)=>a+(b-a)*t; | |
| /* Game state */ | |
| let running=false, lastTime=0; | |
| let score=0, combo=0, best=0; | |
| let gravity = 2300; | |
| let baseAscend = 190; | |
| let difficulty = 1.0; | |
| let currentStage = 1; | |
| const stagePoints = [0, 500, 1500, 3000, 5000]; | |
| let screenShake = 0; | |
| let lastPlayerPosition = {x: 0, y: 0}; | |
| let highScore = parseInt(localStorage.getItem('highScore')) || 0; | |
| let currentTheme = localStorage.getItem('theme') || 'ice'; | |
| /* Currency and Skins */ | |
| let coins = parseInt(localStorage.getItem('coins')) || 0; | |
| let playerSkin = localStorage.getItem('playerSkin') || 'default'; | |
| let ownedSkins = JSON.parse(localStorage.getItem('ownedSkins')) || ['default']; | |
| const skins = { | |
| default: { name: 'Default', price: 0, color: 'linear-gradient(180deg, #ffffff, #7ad0ff)' }, | |
| red: { name: 'Red', price: 5, color: 'linear-gradient(180deg, #ffaaaa, #ff5555)' }, | |
| green: { name: 'Green', price: 10, color: 'linear-gradient(180deg, #aaffaa, #55ff55)' }, | |
| blue: { name: 'Blue', price: 20, color: 'linear-gradient(180deg, #aaddff, #5588ff)' }, | |
| purple: { name: 'Purple', price: 30, color: 'linear-gradient(180deg, #e6b3ff, #a555ff)' }, | |
| orange: { name: 'Orange', price: 50, color: 'linear-gradient(180deg, #ffb366, #ff8c00)' }, | |
| gray: { name: 'Gray', price: 100, color: 'linear-gradient(180deg, #cccccc, #777777)' }, | |
| gold: { name: 'Gold', price: 200, color: 'linear-gradient(180deg, #f0e68c, #daa520)' } | |
| }; | |
| /* Camera */ | |
| let camY = 0; | |
| /* Player */ | |
| const player = { | |
| x: innerWidth*0.5, | |
| y: innerHeight*0.7, | |
| w: 56, h:66, | |
| vx:0, vy:0, | |
| grounded:false, | |
| moveSpeed:480, | |
| jump:1200, | |
| jumpsMade: 0, | |
| canJump: true, | |
| lastPlatform: null, | |
| hasShield: false, | |
| shieldTimer: 0, | |
| magnetTimer: 0, | |
| scoreMultiplierTimer: 0 | |
| }; | |
| /* Power-ups */ | |
| let powerups = []; | |
| const powerupTypes = ['shield', 'superJump', 'slowMotion', 'magnet', 'scoreMultiplier']; | |
| const powerupProps = { | |
| shield: { color: 'rgba(100, 200, 255, 0.7)' }, | |
| superJump: { color: 'rgba(255, 255, 100, 0.7)' }, | |
| slowMotion: { color: 'rgba(100, 255, 100, 0.7)' }, | |
| magnet: { color: 'rgba(255, 150, 100, 0.7)'}, | |
| scoreMultiplier: { color: 'rgba(150, 255, 150, 0.7)'} | |
| }; | |
| function spawnPowerup(x, y, type){ | |
| powerups.push({ x, y, type, w: 24, h: 24, active: true }); | |
| } | |
| /* Coins */ | |
| let coinsInGame = []; | |
| function spawnCoin(x, y){ | |
| coinsInGame.push({ x, y, w: 14, h: 14, active: true }); | |
| } | |
| /* Enemies */ | |
| let enemies = []; | |
| const enemyProps = { | |
| fly: { color: 'red', size: 30, speed: 200, behavior: 'horizontal' } | |
| }; | |
| function spawnEnemy(x, y, type) { | |
| enemies.push({ x, y, type, w: 30, h: 30, vx: rand(-1, 1) * enemyProps[type].speed, vy: 0, active: true }); | |
| } | |
| /* Obstacles */ | |
| let obstacles = []; | |
| function spawnObstacle(x, y, type) { | |
| if (type === 'fireball') { | |
| obstacles.push({ x, y, w: 20, h: 20, type, vx: 0, vy: rand(150, 250) }); | |
| } | |
| } | |
| /* Platforms */ | |
| let platforms = []; | |
| function makePlatform(x,y,w=180,theme='ice'){ | |
| const plat = { | |
| x,y,w,h:22, | |
| baseW:w,theme, | |
| angle:rand(-0.06,0.06), | |
| id:Math.random().toString(36).slice(2), | |
| lifetime: 2.5, time: 0, breaking: false, | |
| isMoving: false, | |
| isRotating: false, | |
| isDisappearing: false, | |
| isHazardous: false, | |
| moveX: x, | |
| moveDir: Math.random() < 0.5 ? 1 : -1, | |
| moveSpeed: 0, | |
| disappearTimer: 0 | |
| }; | |
| if (currentStage >= 2 && Math.random() < 0.3 + (currentStage - 2) * 0.1) { | |
| plat.isMoving = true; | |
| plat.moveSpeed = rand(60, 150) * (currentStage > 2 ? Math.min(3, currentStage-1) : 1); | |
| } | |
| if (currentStage >= 3 && Math.random() < 0.2 + (currentStage - 3) * 0.08) { | |
| plat.isRotating = true; | |
| plat.angle = rand(-0.1, 0.1); | |
| } | |
| if (currentStage >= 4 && Math.random() < 0.15 + (currentStage - 4) * 0.07) { | |
| plat.isDisappearing = true; | |
| plat.disappearTimer = rand(2, 4); | |
| } | |
| if (currentStage >= 5 && Math.random() < 0.1 + (currentStage - 5) * 0.05) { | |
| plat.isHazardous = true; | |
| } | |
| if (Math.random() < 0.08) { | |
| spawnPowerup(plat.x, plat.y - 40, powerupTypes[Math.floor(Math.random() * powerupTypes.length)]); | |
| } | |
| if (Math.random() < 0.15) { | |
| spawnCoin(plat.x, plat.y - 40); | |
| } | |
| return plat; | |
| } | |
| /* Particles */ | |
| let particles = []; | |
| function spawnParticle(x,y,opts={}){ | |
| particles.push({ | |
| x,y, | |
| vx: rand(-220,220), | |
| vy: rand(-260,-40), | |
| ax: 0, ay: 600, | |
| life: rand(0.45,1.2), | |
| t:0, | |
| sz: rand(2,7), | |
| col: opts.col || `hsla(${rand(180,210)},85%,${rand(60,90)}%,1)` | |
| }); | |
| } | |
| function spawnCoinParticle(x, y) { | |
| for(let i=0; i<8; i++){ | |
| particles.push({ | |
| x,y, | |
| vx: rand(-100,100), | |
| vy: rand(-140,-60), | |
| ax: 0, ay: 600, | |
| life: rand(0.3,0.8), | |
| t:0, | |
| sz: rand(1.5,4), | |
| col: '#ffd700' | |
| }); | |
| } | |
| } | |
| function spawnFireballParticle(x,y){ | |
| for(let i=0; i<5; i++) { | |
| particles.push({ | |
| x,y, | |
| vx: rand(-30, 30), | |
| vy: rand(20, 60), | |
| ax: 0, ay: 100, | |
| life: rand(0.3, 0.6), | |
| t: 0, | |
| sz: rand(2, 5), | |
| col: `hsla(${rand(10,40)}, 100%, 50%, 1)` | |
| }); | |
| } | |
| } | |
| /* Parallax layers (procedural shapes) */ | |
| const layers = { | |
| stars: Array.from({length:140},()=>({x:rand(0,innerWidth),y:rand(0,innerHeight/1.2),r:rand(0.3,2.2)})), | |
| mountains: Array.from({length:8},(_,i)=>({x:(i-2)*innerWidth*0.6 + rand(-200,200),h:rand(120,420)})), | |
| clouds: Array.from({length:14},()=>({x:rand(0,innerWidth),y:rand(0,innerHeight),sz:rand(120,360)})) | |
| }; | |
| /* Seed initial platforms */ | |
| function initPlatforms(){ | |
| platforms = []; | |
| powerups = []; | |
| coinsInGame = []; | |
| enemies = []; | |
| obstacles = []; | |
| particles = []; | |
| const startY = camY + innerHeight*0.7; | |
| platforms.push(makePlatform(innerWidth*0.5, startY, 260)); | |
| for(let i=1;i<12;i++){ | |
| const py = startY - rand(80,140); | |
| const px = rand(60, innerWidth-220); | |
| platforms.push(makePlatform(px + 100, py, rand(160,320))); | |
| } | |
| } | |
| initPlatforms(); | |
| /* Input */ | |
| const keys = {}; | |
| addEventListener('keydown', e=>{ keys[e.code]=true; if(e.code==='Space') e.preventDefault(); }); | |
| addEventListener('keyup', e=>{ keys[e.code]=false; }); | |
| /* Touch */ | |
| let touchLeft=false, touchRight=false; | |
| document.getElementById('leftArea').addEventListener('touchstart', e=>{ touchLeft=true; e.preventDefault(); }); | |
| document.getElementById('leftArea').addEventListener('touchend', e=>{ touchLeft=false; e.preventDefault(); }); | |
| document.getElementById('rightArea').addEventListener('touchstart', e=>{ touchRight=true; e.preventDefault(); }); | |
| document.getElementById('rightArea').addEventListener('touchend', e=>{ touchRight=false; e.preventDefault(); }); | |
| /* UI & Menus */ | |
| const uiSpeed = document.getElementById('uiSpeed'); | |
| const uiScore = document.getElementById('uiScore'); | |
| const uiCombo = document.getElementById('uiCombo'); | |
| const uiCoinCount = document.getElementById('uiCoinCount'); | |
| const menu = document.getElementById('menu'); | |
| const pregameMenu = document.getElementById('pregame-menu'); | |
| const shopMenu = document.getElementById('shop-menu'); | |
| const reviveContainer = document.getElementById('reviveContainer'); | |
| const stageMessageContainer = document.getElementById('stageMessageContainer'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const pregameStartBtn = document.getElementById('pregame-startBtn'); | |
| const shopBtn = document.getElementById('shopBtn'); | |
| const backBtn = document.getElementById('backBtn'); | |
| const skinList = document.getElementById('skin-list'); | |
| const highScoreEl = document.getElementById('highScore'); | |
| const bgMusic = document.getElementById('bgMusic'); | |
| const toggleMusicBtn = document.getElementById('toggleMusicBtn'); | |
| const powerupTimerEl = document.getElementById('powerup-timer'); | |
| const shieldTimerEl = document.getElementById('shield-timer'); | |
| const shieldTimerValueEl = document.getElementById('shield-timer-value'); | |
| const magnetTimerEl = document.getElementById('magnet-timer'); | |
| const magnetTimerValueEl = document.getElementById('magnet-timer-value'); | |
| const scoreMultiplierTimerEl = document.getElementById('score-multiplier-timer'); | |
| const scoreMultiplierTimerValueEl = document.getElementById('score-multiplier-timer-value'); | |
| startBtn.addEventListener('click', ()=>{ | |
| menu.style.display = 'none'; | |
| pregameMenu.style.display = 'block'; | |
| highScoreEl.textContent = highScore; | |
| }); | |
| pregameStartBtn.addEventListener('click', ()=>{ startGame(); }); | |
| shopBtn.addEventListener('click', ()=>{ | |
| menu.style.display = 'none'; | |
| shopMenu.style.display = 'flex'; | |
| renderSkins(); | |
| }); | |
| backBtn.addEventListener('click', ()=>{ | |
| shopMenu.style.display = 'none'; | |
| menu.style.display = 'block'; | |
| highScoreEl.textContent = highScore; | |
| }); | |
| toggleMusicBtn.addEventListener('click', () => { | |
| if (bgMusic.paused) { | |
| bgMusic.play(); | |
| toggleMusicBtn.textContent = 'Music: On'; | |
| } else { | |
| bgMusic.pause(); | |
| toggleMusicBtn.textContent = 'Music: Off'; | |
| } | |
| }); | |
| function renderSkins(){ | |
| skinList.innerHTML = ''; | |
| for(const skinId in skins){ | |
| const skin = skins[skinId]; | |
| const item = document.createElement('div'); | |
| item.className = 'shop-item'; | |
| const preview = document.createElement('div'); | |
| preview.className = 'shop-item-preview'; | |
| preview.style.background = skin.color; | |
| item.appendChild(preview); | |
| const info = document.createElement('span'); | |
| info.textContent = skin.name; | |
| item.appendChild(info); | |
| const btn = document.createElement('button'); | |
| if (ownedSkins.includes(skinId)){ | |
| btn.textContent = 'Equip'; | |
| btn.className = 'btn btn-secondary'; | |
| if (playerSkin === skinId) { | |
| btn.textContent = 'Equipped'; | |
| btn.classList.add('disabled'); | |
| } | |
| btn.onclick = () => { | |
| playerSkin = skinId; | |
| localStorage.setItem('playerSkin', playerSkin); | |
| renderSkins(); | |
| }; | |
| } else { | |
| btn.textContent = `Buy (${skin.price})`; | |
| btn.className = 'btn'; | |
| if (coins < skin.price) { | |
| btn.classList.add('disabled'); | |
| } | |
| btn.onclick = () => { | |
| if(coins >= skin.price){ | |
| coins -= skin.price; | |
| ownedSkins.push(skinId); | |
| playerSkin = skinId; | |
| localStorage.setItem('coins', coins); | |
| localStorage.setItem('ownedSkins', JSON.stringify(ownedSkins)); | |
| localStorage.setItem('playerSkin', playerSkin); | |
| updateUI(); | |
| renderSkins(); | |
| } | |
| }; | |
| } | |
| item.appendChild(btn); | |
| skinList.appendChild(item); | |
| } | |
| } | |
| /* Audio (tiny tones) */ | |
| let audioCtx = null; | |
| function beep(freq,dur=0.06){ | |
| if(!audioCtx) try{ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){return;} | |
| const o = audioCtx.createOscillator(); | |
| const g = audioCtx.createGain(); | |
| o.type = 'triangle'; | |
| o.frequency.value = freq; | |
| g.gain.value = 0.03; | |
| o.connect(g); g.connect(audioCtx.destination); | |
| o.start(); | |
| g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + dur); | |
| o.stop(audioCtx.currentTime + dur + 0.01); | |
| } | |
| const sfxCoin = document.getElementById('sfx-coin'); | |
| const sfxPowerup = document.getElementById('sfx-powerup'); | |
| const sfxGameover = document.getElementById('sfx-gameover'); | |
| function playSfx(sfx) { | |
| if (sfx) { | |
| sfx.currentTime = 0; | |
| sfx.play(); | |
| } | |
| } | |
| /* Start / Reset */ | |
| function reset(){ | |
| score = 0; combo = 0; difficulty = 1; camY = 0; | |
| player.x = innerWidth*0.5; player.y = innerHeight*0.7; player.vx=0; player.vy=0; player.grounded=true; | |
| player.jumpsMade = 0; | |
| player.canJump = true; | |
| player.lastPlatform = null; | |
| player.hasShield = false; | |
| player.shieldTimer = 0; | |
| player.magnetTimer = 0; | |
| player.scoreMultiplierTimer = 0; | |
| baseAscend = 190; | |
| currentStage = 1; | |
| initPlatforms(); | |
| updateUI(); | |
| reviveContainer.innerHTML = ''; | |
| } | |
| function startGame(){ | |
| reset(); | |
| running = true; | |
| pregameMenu.style.display = 'none'; | |
| powerupTimerEl.style.display = 'block'; | |
| lastTime = performance.now(); | |
| try{ audioCtx && audioCtx.resume(); }catch(e){} | |
| bgMusic.play(); | |
| beep(620,0.05); | |
| requestAnimationFrame(loop); | |
| } | |
| function revive() { | |
| if (coins >= 15) { | |
| coins -= 15; | |
| localStorage.setItem('coins', coins); | |
| updateUI(); | |
| running = true; | |
| menu.style.display = 'none'; | |
| player.x = lastPlayerPosition.x; | |
| player.y = lastPlayerPosition.y; | |
| player.vy = 0; | |
| player.grounded = true; | |
| player.jumpsMade = 0; | |
| camY = player.y - innerHeight*0.45; | |
| reviveContainer.innerHTML = ''; | |
| lastTime = performance.now(); | |
| bgMusic.play(); | |
| beep(620,0.05); | |
| requestAnimationFrame(loop); | |
| } | |
| } | |
| /* Stage Messages */ | |
| function showStageMessage(stage){ | |
| const msg = document.createElement('div'); | |
| msg.className = 'stage-message'; | |
| msg.textContent = `Stage ${stage}`; | |
| stageMessageContainer.appendChild(msg); | |
| setTimeout(() => msg.remove(), 2000); | |
| } | |
| /* Physics & world update */ | |
| function update(dt){ | |
| difficulty = 1 + Math.log1p(score/1500); | |
| const newStage = (score >= stagePoints[4]) ? 5 : | |
| (score >= stagePoints[3]) ? 4 : | |
| (score >= stagePoints[2]) ? 3 : | |
| (score >= stagePoints[1]) ? 2 : 1; | |
| if (newStage > currentStage) { | |
| currentStage = newStage; | |
| showStageMessage(currentStage); | |
| beep(1020, 0.1); | |
| if (currentStage >= 4) { | |
| document.body.className = 'theme-lava'; | |
| } | |
| } | |
| baseAscend = 190 + difficulty*70; | |
| let ax=0; | |
| if(keys['ArrowLeft'] || keys['KeyA'] || touchLeft) ax = -1; | |
| if(keys['ArrowRight'] || keys['KeyD'] || touchRight) ax = 1; | |
| player.vx = player.moveSpeed * ax; | |
| const wantJump = keys['ArrowUp'] || keys['Space'] || keys['KeyW']; | |
| if (wantJump && player.canJump) { | |
| player.canJump = false; | |
| if (player.grounded) { | |
| player.vy = -player.jump; | |
| player.grounded = false; | |
| player.jumpsMade = 1; | |
| combo = 1; | |
| spawnJumpFX(player.x, player.y + player.h * 0.45); | |
| beep(880, 0.06); | |
| } else if (player.jumpsMade === 1) { | |
| player.vy = -player.jump * 0.8; | |
| player.jumpsMade = 2; | |
| combo++; | |
| spawnJumpFX(player.x, player.y + player.h * 0.45); | |
| beep(960, 0.06); | |
| } else if (player.jumpsMade === 2 && player.scoreMultiplierTimer > 0) { | |
| player.vy = -player.jump * 0.6; | |
| player.jumpsMade = 3; | |
| combo++; | |
| spawnJumpFX(player.x, player.y + player.h * 0.45); | |
| beep(1000, 0.06); | |
| } | |
| } | |
| if (!wantJump) { | |
| player.canJump = true; | |
| } | |
| if (player.shieldTimer > 0) { | |
| player.shieldTimer -= dt; | |
| if (player.shieldTimer <= 0) player.hasShield = false; | |
| shieldTimerEl.style.display = 'block'; | |
| shieldTimerValueEl.textContent = Math.ceil(player.shieldTimer); | |
| if (player.shieldTimer <= 0) shieldTimerEl.style.display = 'none'; | |
| } | |
| if (player.magnetTimer > 0) { | |
| player.magnetTimer -= dt; | |
| if (player.magnetTimer <= 0) player.magnetTimer = 0; | |
| magnetTimerEl.style.display = 'block'; | |
| magnetTimerValueEl.textContent = Math.ceil(player.magnetTimer); | |
| if (player.magnetTimer <= 0) magnetTimerEl.style.display = 'none'; | |
| for (const coin of coinsInGame) { | |
| if (coin.active) { | |
| const dist = Math.sqrt(Math.pow(player.x - coin.x, 2) + Math.pow(player.y - coin.y, 2)); | |
| if (dist < 150) { | |
| const dx = player.x - coin.x; | |
| const dy = player.y - coin.y; | |
| coin.x += dx * dt * 3; | |
| coin.y += dy * dt * 3; | |
| } | |
| } | |
| } | |
| } | |
| if (player.scoreMultiplierTimer > 0) { | |
| player.scoreMultiplierTimer -= dt; | |
| if (player.scoreMultiplierTimer <= 0) player.scoreMultiplierTimer = 0; | |
| scoreMultiplierTimerEl.style.display = 'block'; | |
| scoreMultiplierTimerValueEl.textContent = Math.ceil(player.scoreMultiplierTimer); | |
| if (player.scoreMultiplierTimer <= 0) scoreMultiplierTimerEl.style.display = 'none'; | |
| } | |
| if (player.magnetTimer <= 0 && player.scoreMultiplierTimer <= 0 && player.shieldTimer <= 0) { | |
| powerupTimerEl.style.display = 'none'; | |
| } else { | |
| powerupTimerEl.style.display = 'block'; | |
| } | |
| player.vy += gravity * dt; | |
| player.x += player.vx * dt; | |
| player.y += player.vy * dt; | |
| screenShake = lerp(screenShake, 0, dt*4); | |
| player.x = clamp(player.x, player.w*0.5 + 8, innerWidth - player.w*0.5 - 8); | |
| const targetCam = player.y - innerHeight*0.45; | |
| camY += (targetCam - camY) * clamp(6*dt, 0, 1); | |
| let highest = Math.min(...platforms.map(p=>p.y)); | |
| while(highest > camY - innerHeight){ | |
| const platformWidth = clamp(rand(120,320) - (currentStage-1)*20, 100, 320); | |
| const py = highest - rand(80, 150) + difficulty * 4; | |
| const px = rand(48, innerWidth - platformWidth - 48); | |
| platforms.push(makePlatform(px + platformWidth/2, py, platformWidth)); | |
| // Increased chance for enemies and fireballs | |
| if (currentStage >= 2 && Math.random() < 0.15 + (currentStage-2) * 0.05) { | |
| spawnEnemy(rand(50, innerWidth-50), py - rand(50, 100), 'fly'); | |
| } | |
| if (currentStage >= 3 && Math.random() < 0.08 + (currentStage-3) * 0.05) { | |
| spawnObstacle(rand(50, innerWidth-50), camY, 'fireball'); | |
| } | |
| highest = py; | |
| } | |
| platforms = platforms.filter(p => { | |
| if (p.breaking && p.time > p.lifetime) { | |
| return false; | |
| } | |
| return (p.y - camY) < innerHeight + 240; | |
| }); | |
| powerups = powerups.filter(pu => (pu.y - camY) < innerHeight + 240); | |
| coinsInGame = coinsInGame.filter(c => (c.y - camY) < innerHeight + 240); | |
| enemies = enemies.filter(e => (e.y - camY) < innerHeight + 240); | |
| obstacles = obstacles.filter(o => (o.y - camY) < innerHeight + 240); | |
| for (let p of platforms) { | |
| if (p.isMoving) { | |
| p.x += p.moveDir * p.moveSpeed * dt; | |
| if (p.x < p.w/2 + 20 || p.x > innerWidth - p.w/2 - 20) { | |
| p.moveDir *= -1; | |
| p.x = clamp(p.x, p.w/2 + 20, innerWidth - p.w/2 - 20); | |
| } | |
| if (player.grounded && player.lastPlatform === p.id) { | |
| player.x += p.moveDir * p.moveSpeed * dt; | |
| } | |
| } | |
| if (p.isDisappearing) { | |
| p.disappearTimer -= dt; | |
| if (p.disappearTimer < 0) p.breaking = true; | |
| } | |
| } | |
| powerups = powerups.filter(pu => { | |
| const dist = Math.sqrt(Math.pow(player.x - pu.x, 2) + Math.pow(player.y - pu.y, 2)); | |
| if (dist < player.w/2 + pu.w/2 && pu.active) { | |
| playSfx(sfxPowerup); | |
| pu.active = false; | |
| if (pu.type === 'shield') { | |
| player.hasShield = true; | |
| player.shieldTimer = 10; | |
| } else if (pu.type === 'superJump') { | |
| player.vy = -player.jump * 1.5; | |
| } else if (pu.type === 'slowMotion') { | |
| difficulty = Math.max(1, difficulty - 1.5); | |
| } else if (pu.type === 'magnet') { | |
| player.magnetTimer = 10; | |
| } else if (pu.type === 'scoreMultiplier') { | |
| player.scoreMultiplierTimer = 10; | |
| } | |
| return false; | |
| } | |
| return true; | |
| }); | |
| coinsInGame = coinsInGame.filter(c => { | |
| const dist = Math.sqrt(Math.pow(player.x - c.x, 2) + Math.pow(player.y - c.y, 2)); | |
| if (dist < player.w/2 + c.w/2 && c.active) { | |
| coins++; | |
| localStorage.setItem('coins', coins); | |
| updateUI(); | |
| spawnCoinParticle(c.x, c.y); | |
| playSfx(sfxCoin); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| enemies = enemies.filter(e => { | |
| if (e.active) { | |
| e.x += e.vx * dt; | |
| e.y += e.vy * dt; | |
| if (e.x < 15 || e.x > innerWidth-15) e.vx *= -1; | |
| const dist = Math.sqrt(Math.pow(player.x - e.x, 2) + Math.pow(player.y - e.y, 2)); | |
| if (dist < player.w / 2 + e.w / 2) { | |
| if (player.hasShield) { | |
| player.hasShield = false; | |
| player.shieldTimer = 0; | |
| beep(300, 0.1); | |
| return false; | |
| } else { | |
| running = false; | |
| endGame(); | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| }); | |
| obstacles = obstacles.filter(o => { | |
| o.y += o.vy * dt; | |
| spawnFireballParticle(o.x, o.y); | |
| const dist = Math.sqrt(Math.pow(player.x - o.x, 2) + Math.pow(player.y - o.y, 2)); | |
| if (dist < player.w / 2 + o.w / 2) { | |
| if (player.hasShield) { | |
| player.hasShield = false; | |
| player.shieldTimer = 0; | |
| beep(300, 0.1); | |
| return false; | |
| } else { | |
| running = false; | |
| endGame(); | |
| return false; | |
| } | |
| } | |
| return true; | |
| }); | |
| for(let p of platforms){ | |
| const platTop = p.y; | |
| const playerBottom = player.y + player.h/2; | |
| if(player.vy > 0 && playerBottom > platTop - 20 && playerBottom < platTop + 20){ | |
| const left = p.x - p.w/2; | |
| const right = p.x + p.w/2; | |
| if((player.x + player.w/2) > left && (player.x - player.w/2) < right){ | |
| if(p.isHazardous) { | |
| if (player.hasShield) { | |
| player.hasShield = false; | |
| player.shieldTimer = 0; | |
| beep(300, 0.1); | |
| } else { | |
| running = false; | |
| endGame(); | |
| return; | |
| } | |
| } | |
| player.y = platTop - player.h/2; | |
| player.vy = 0; | |
| player.grounded = true; | |
| player.jumpsMade = 0; | |
| if (player.lastPlatform !== p.id) { | |
| let pointsGained = Math.floor(12 + difficulty*4 + Math.max(0, combo-1)*3); | |
| if (player.scoreMultiplierTimer > 0) pointsGained *= 2; | |
| score += pointsGained; | |
| if (combo > 10) screenShake = 0.5; | |
| lastPlayerPosition = {x: player.x, y: player.y}; | |
| } | |
| player.lastPlatform = p.id; | |
| if(!p.isHazardous) { | |
| if(Math.random() < 0.55) spawnParticle(player.x + rand(-18,18), player.y + player.h*0.5, {col:`hsla(${rand(190,210)},90%,75%,1)`}); | |
| p.angle = rand(-0.06,0.06); | |
| p.breaking = true; | |
| p.lifetime = 2.5 / difficulty; | |
| } | |
| } | |
| } | |
| } | |
| for (let p of platforms) { | |
| if (p.breaking) { | |
| p.time += dt; | |
| } | |
| } | |
| if(player.y - camY > innerHeight + 260){ | |
| running = false; | |
| endGame(); | |
| } | |
| for(let i = particles.length-1; i>=0; i--){ | |
| const P = particles[i]; | |
| P.t += dt; | |
| P.vy += P.ay * dt; | |
| P.vx += (P.ax||0) * dt; | |
| P.x += P.vx * dt; | |
| P.y += P.vy * dt; | |
| if(P.t > P.life) particles.splice(i,1); | |
| } | |
| for(let p of platforms) p.w = p.baseW + Math.sin((performance.now()/1000) + (p.id.charCodeAt(0)||0)) * 6; | |
| for(let c of coinsInGame) c.w = 14 + Math.sin(performance.now()/200) * 2; | |
| updateUI(); | |
| } | |
| function endGame(){ | |
| menu.style.display = 'block'; | |
| menu.querySelector('h2').textContent = 'Game Over — Score: ' + Math.floor(score); | |
| if(score > highScore) { | |
| highScore = score; | |
| localStorage.setItem('highScore', highScore); | |
| highScoreEl.textContent = highScore; | |
| } | |
| bgMusic.pause(); | |
| bgMusic.currentTime = 0; | |
| playSfx(sfxGameover); | |
| document.body.className = 'theme-ice'; | |
| if (coins >= 15) { | |
| reviveContainer.innerHTML = '<button id="reviveBtn" class="btn">Revive (15 coins)</button>'; | |
| document.getElementById('reviveBtn').addEventListener('click', revive); | |
| } | |
| } | |
| function updateUI(){ | |
| document.getElementById('uiSpeed').textContent = Math.round(baseAscend + difficulty*30); | |
| document.getElementById('uiScore').textContent = Math.floor(score); | |
| document.getElementById('uiCombo').textContent = combo; | |
| document.getElementById('uiCoinCount').textContent = coins; | |
| const comboUI = document.getElementById('uiCombo'); | |
| if (combo >= 5 && !comboUI.classList.contains('combo-pulse')) { | |
| comboUI.classList.add('combo-pulse'); | |
| } else if (combo < 5) { | |
| comboUI.classList.remove('combo-pulse'); | |
| } | |
| } | |
| /* FX */ | |
| function spawnJumpFX(x,y){ | |
| for(let i=0;i<16;i++) spawnParticle(x + rand(-18,18), y + rand(-6,10), {col:`hsla(${rand(190,210)},90%,75%,1)`}); | |
| } | |
| /* Rendering helpers */ | |
| function roundRect(ctx,x,y,w,h,r){ | |
| ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); | |
| } | |
| /* Draw background & parallax */ | |
| function drawBackground(){ | |
| const g = ctx.createLinearGradient(0,0,0,innerHeight); | |
| const bg = document.body.className === 'theme-lava' ? ['#2a0707', '#301205'] : ['#07102a', '#051225']; | |
| g.addColorStop(0,bg[0]); g.addColorStop(1,bg[1]); | |
| ctx.fillStyle = g; ctx.fillRect(0,0,innerWidth,innerHeight); | |
| ctx.save(); | |
| ctx.globalAlpha = 0.9; | |
| for(let s of layers.stars){ | |
| const x = (s.x - camY*0.12) % (innerWidth+200); | |
| ctx.beginPath(); ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.arc((x+innerWidth)%innerWidth, s.y, s.r, 0, Math.PI*2); ctx.fill(); | |
| } | |
| ctx.restore(); | |
| ctx.save(); | |
| ctx.translate(innerWidth/2, innerHeight*0.88); | |
| const mountainColor = document.body.className === 'theme-lava' ? 'rgba(36,18,8,0.3)' : 'rgba(8,18,36,0.3)'; | |
| for(let i=0;i<layers.mountains.length;i++){ | |
| const m = layers.mountains[i]; | |
| const offset = (m.x - camY*0.35) % (innerWidth*2) - innerWidth; | |
| ctx.beginPath(); | |
| ctx.moveTo(offset-600,0); | |
| ctx.lineTo(offset + 120, -m.h); | |
| ctx.lineTo(offset + 560, 0); | |
| ctx.closePath(); | |
| ctx.fillStyle = mountainColor; | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| ctx.save(); | |
| for(let c of layers.clouds){ | |
| const x = (c.x - camY*0.7) % (innerWidth+400); | |
| ctx.globalAlpha = 0.06; | |
| ctx.beginPath(); ctx.ellipse((x+innerWidth)%innerWidth, c.y, c.sz, c.sz*0.6, 0, 0, Math.PI*2); ctx.fillStyle = '#bfe6ff'; ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| /* Render platforms with icy shader (procedural) */ | |
| function drawPlatforms(){ | |
| for(let p of platforms){ | |
| const sx = p.x; | |
| const sy = p.y - camY; | |
| if(sy < -200 || sy > innerHeight + 200) continue; | |
| ctx.save(); | |
| ctx.translate(sx, sy); | |
| if (p.isRotating) ctx.rotate(p.angle * Math.sin(performance.now()/1000 * p.id.charCodeAt(0) * 0.005)); | |
| const w = p.w, h = p.h; | |
| ctx.globalAlpha = 1; | |
| if (p.isDisappearing) ctx.globalAlpha = 0.2 + Math.abs(Math.sin(p.disappearTimer * 4)); | |
| if (p.breaking) { | |
| const shake = Math.sin(p.time * 25) * (1 - p.time / p.lifetime) * 6; | |
| ctx.translate(shake, 0); | |
| ctx.globalAlpha = 1 - p.time / p.lifetime; | |
| } | |
| ctx.globalAlpha *= 0.28; | |
| ctx.beginPath(); roundRect(ctx, -w/2 + 6, -h/2 + 8, w-12, h+12, 14); ctx.fillStyle = 'rgba(2,6,18,0.7)'; ctx.fill(); | |
| ctx.globalAlpha /= 0.28; | |
| let g; | |
| if (document.body.className === 'theme-lava') { | |
| g = ctx.createLinearGradient(-w/2, -h/2, w/2, h/2); | |
| g.addColorStop(0,'#ffb8b8'); g.addColorStop(0.5,'#ff8888'); g.addColorStop(1,'#ff5555'); | |
| } else { | |
| g = ctx.createLinearGradient(-w/2, -h/2, w/2, h/2); | |
| g.addColorStop(0,'#e8fbff'); g.addColorStop(0.5,'#9fe8ff'); g.addColorStop(1,'#70d6ff'); | |
| } | |
| roundRect(ctx, -w/2, -h/2, w, h, 14); | |
| ctx.fillStyle = g; ctx.fill(); | |
| ctx.beginPath(); | |
| ctx.moveTo(-w/2 + 8, -h/2 + 4); | |
| ctx.quadraticCurveTo(0, -h/2 - 18, w/2 - 8, -h/2 + 6); | |
| ctx.lineWidth = 2; | |
| ctx.strokeStyle = document.body.className === 'theme-lava' ? 'rgba(255,200,200,0.82)' : 'rgba(255,255,255,0.82)'; | |
| ctx.stroke(); | |
| ctx.beginPath(); | |
| for(let i=-Math.floor(w/2)+12;i<w/2-6;i+=24){ | |
| const ix = i; | |
| const ih = 6 + Math.abs(Math.sin((performance.now()/400) + i))*8; | |
| ctx.moveTo(ix, h/2 - 2); | |
| ctx.lineTo(ix + 6, h/2 + ih); | |
| ctx.lineTo(ix + 12, h/2 - 2); | |
| } | |
| ctx.fillStyle = document.body.className === 'theme-lava' ? 'rgba(255,220,220,0.95)' : 'rgba(220,250,255,0.95)'; | |
| ctx.fill(); | |
| if (p.isHazardous) { | |
| ctx.beginPath(); | |
| ctx.moveTo(-w/2, -h/2); | |
| for(let i = -w/2; i <= w/2; i += 15) { | |
| ctx.lineTo(i + 7, -h/2 - 10); | |
| ctx.lineTo(i + 14, -h/2); | |
| } | |
| ctx.fillStyle = document.body.className === 'theme-lava' ? 'rgba(200,50,50,0.8)' : 'rgba(10,20,40,0.8)'; | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| } | |
| /* Draw power-ups */ | |
| function drawPowerups() { | |
| for (const pu of powerups) { | |
| if (!pu.active) continue; | |
| const sx = pu.x; | |
| const sy = pu.y - camY; | |
| ctx.save(); | |
| ctx.translate(sx, sy); | |
| ctx.globalAlpha = 0.8 + Math.sin(performance.now() / 200) * 0.2; | |
| ctx.strokeStyle = '#fff'; | |
| ctx.fillStyle = powerupProps[pu.type].color; | |
| if (pu.type === 'shield') { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -18); | |
| ctx.lineTo(16, -6); | |
| ctx.lineTo(16, 12); | |
| ctx.quadraticCurveTo(0, 18, -16, 12); | |
| ctx.lineTo(-16, -6); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| } else if (pu.type === 'superJump') { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -16); | |
| ctx.lineTo(10, 0); | |
| ctx.lineTo(4, 0); | |
| ctx.lineTo(4, 16); | |
| ctx.lineTo(-4, 16); | |
| ctx.lineTo(-4, 0); | |
| ctx.lineTo(-10, 0); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| } else if (pu.type === 'slowMotion') { | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 14, 0, Math.PI * 2); | |
| ctx.moveTo(0, 0); | |
| ctx.lineTo(0, -10); | |
| ctx.moveTo(0, 0); | |
| ctx.lineTo(8, 0); | |
| ctx.stroke(); | |
| ctx.fill(); | |
| } else if (pu.type === 'magnet') { | |
| ctx.beginPath(); | |
| ctx.moveTo(-10, 0); ctx.lineTo(-10, 10); | |
| ctx.arc(0, 10, 10, Math.PI, 0, false); | |
| ctx.lineTo(10, 0); ctx.lineTo(10, -10); | |
| ctx.arc(0, -10, 10, 0, Math.PI, true); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| } else if (pu.type === 'scoreMultiplier') { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -16); | |
| ctx.lineTo(8, -8); | |
| ctx.lineTo(8, 8); | |
| ctx.lineTo(0, 16); | |
| ctx.lineTo(-8, 8); | |
| ctx.lineTo(-8, -8); | |
| ctx.closePath(); | |
| ctx.font = "20px Arial"; | |
| ctx.fillStyle = "#000"; | |
| ctx.textAlign = "center"; | |
| ctx.textBaseline = "middle"; | |
| ctx.fillText("2x", 0, 0); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| } | |
| /* Draw coins */ | |
| function drawCoins() { | |
| for (const coin of coinsInGame) { | |
| if (!coin.active) continue; | |
| const sx = coin.x; | |
| const sy = coin.y - camY; | |
| ctx.save(); | |
| ctx.translate(sx, sy); | |
| ctx.rotate(performance.now() / 200); | |
| ctx.globalAlpha = 0.9; | |
| ctx.fillStyle = '#ffd700'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 0, 10, 14, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.strokeStyle = '#fff'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| } | |
| /* Draw enemies */ | |
| function drawEnemies() { | |
| for (const enemy of enemies) { | |
| if (!enemy.active) continue; | |
| const sx = enemy.x; | |
| const sy = enemy.y - camY; | |
| ctx.save(); | |
| ctx.translate(sx, sy); | |
| ctx.rotate(performance.now() / 300 * enemy.vx > 0 ? 1 : -1); | |
| ctx.globalAlpha = 0.8 + Math.sin(performance.now() / 150) * 0.2; | |
| const size = enemy.w * (1 + Math.sin(performance.now() / 200) * 0.1); | |
| ctx.fillStyle = 'red'; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -size); | |
| ctx.lineTo(size, size); | |
| ctx.lineTo(-size, size); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.strokeStyle = 'rgba(255,255,255,0.8)'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| } | |
| /* Draw obstacles */ | |
| function drawObstacles() { | |
| for (const obs of obstacles) { | |
| const sx = obs.x; | |
| const sy = obs.y - camY; | |
| ctx.save(); | |
| ctx.translate(sx, sy); | |
| ctx.rotate(performance.now() / 100); | |
| ctx.globalAlpha = 0.8; | |
| ctx.shadowColor = 'orange'; | |
| ctx.shadowBlur = 15; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 16, 0, Math.PI * 2); | |
| ctx.fillStyle = 'red'; | |
| ctx.fill(); | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 10, 0, Math.PI * 2); | |
| ctx.fillStyle = 'orange'; | |
| ctx.fill(); | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 5, 0, Math.PI * 2); | |
| ctx.fillStyle = 'yellow'; | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| /* Draw player (cute block with glow & soft rim) */ | |
| function drawPlayer(p, skinId){ | |
| const sx = p.x; | |
| const sy = p.y - camY; | |
| const currentSkin = skins[skinId]; | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'lighter'; | |
| ctx.globalAlpha = 0.16; | |
| ctx.beginPath(); ctx.ellipse(sx, sy, p.w*1.6, p.h*1.4, 0,0,Math.PI*2); ctx.fillStyle = '#7fe6ff'; ctx.fill(); | |
| ctx.restore(); | |
| ctx.save(); | |
| ctx.translate(sx, sy); | |
| let g; | |
| if (currentSkin && currentSkin.color.includes('linear-gradient')) { | |
| const colors = currentSkin.color.match(/#[a-f0-9]{6}/g); | |
| g = ctx.createLinearGradient(-p.w/2, -p.h/2, p.w/2, p.h/2); | |
| g.addColorStop(0,colors[0]); g.addColorStop(1,colors[1]); | |
| } else { | |
| g = ctx.createLinearGradient(-p.w/2, -p.h/2, p.w/2, p.h/2); | |
| g.addColorStop(0,'#ffffff'); g.addColorStop(1,'#7ad0ff'); | |
| } | |
| roundRect(ctx, -p.w/2, -p.h/2, p.w, p.h, 14); | |
| ctx.fillStyle = g; ctx.fill(); | |
| ctx.fillStyle = '#031022'; | |
| ctx.beginPath(); ctx.ellipse(-10, -8, 5, 7, 0,0,Math.PI*2); ctx.fill(); | |
| ctx.beginPath(); ctx.ellipse(10, -8, 5, 7, 0,0,Math.PI*2); ctx.fill(); | |
| ctx.strokeStyle = 'rgba(2,8,20,0.9)'; ctx.lineWidth = 2; | |
| ctx.beginPath(); ctx.moveTo(-12, 10); ctx.quadraticCurveTo(0, 16, 12, 10); ctx.stroke(); | |
| ctx.restore(); | |
| if (p.hasShield) { | |
| ctx.save(); | |
| const shieldAlpha = 0.5 + Math.sin(p.shieldTimer * 10) * 0.3; | |
| ctx.globalAlpha = shieldAlpha; | |
| ctx.beginPath(); | |
| ctx.ellipse(sx, sy, p.w, p.h*1.1, 0, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(100, 200, 255, 0.7)'; | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| const timerWidth = p.w * (p.shieldTimer / 10); | |
| ctx.fillStyle = `rgba(255, 255, 255, ${shieldAlpha})`; | |
| ctx.fillRect(sx - timerWidth/2, sy + p.h/2 + 8, timerWidth, 4); | |
| ctx.restore(); | |
| } | |
| } | |
| /* Particles rendering */ | |
| function drawParticles(){ | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'lighter'; | |
| for(let p of particles){ | |
| const alpha = 1 - p.t / p.life; | |
| ctx.globalAlpha = alpha; | |
| ctx.beginPath(); ctx.arc(p.x, p.y - camY, p.sz, 0, Math.PI*2); | |
| ctx.fillStyle = p.col; | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| /* Pseudo-bloom: draw bright layer tiny-scaled and overlay translucent */ | |
| const bloomTmp = document.createElement('canvas'); | |
| const bctx = bloomTmp.getContext('2d'); | |
| function applyBloom(){ | |
| bloomTmp.width = innerWidth * 0.22; | |
| bloomTmp.height = innerHeight * 0.22; | |
| bctx.clearRect(0,0,bloomTmp.width,bloomTmp.height); | |
| bctx.drawImage(canvas, 0, 0, bloomTmp.width, bloomTmp.height); | |
| ctx.save(); | |
| ctx.globalAlpha = 0.06; | |
| ctx.drawImage(bloomTmp, 0, 0, bloomTmp.width, bloomTmp.height, 0, 0, innerWidth, innerHeight); | |
| ctx.restore(); | |
| } | |
| /* Main draw */ | |
| function render(){ | |
| ctx.save(); | |
| ctx.translate(rand(-screenShake*10, screenShake*10), rand(-screenShake*10, screenShake*10)); | |
| ctx.clearRect(0,0,innerWidth,innerHeight); | |
| drawBackground(); | |
| drawPlatforms(); | |
| drawPowerups(); | |
| drawCoins(); | |
| drawEnemies(); | |
| drawObstacles(); | |
| drawPlayer(player, playerSkin); | |
| drawParticles(); | |
| ctx.save(); | |
| const vg = ctx.createRadialGradient(innerWidth/2, innerHeight/2, innerHeight*0.2, innerWidth/2, innerHeight/2, innerHeight*0.9); | |
| vg.addColorStop(0,'rgba(0,0,0,0)'); | |
| vg.addColorStop(1,'rgba(0,0,0,0.28)'); | |
| ctx.fillStyle = vg; ctx.fillRect(0,0,innerWidth,innerHeight); | |
| ctx.restore(); | |
| applyBloom(); | |
| ctx.restore(); | |
| } | |
| /* Game loop */ | |
| function loop(ts){ | |
| if(!lastTime) lastTime = ts; | |
| const dt = Math.min(0.035, (ts - lastTime)/1000); | |
| lastTime = ts; | |
| if(running){ | |
| update(dt); | |
| render(); | |
| requestAnimationFrame(loop); | |
| } | |
| } | |
| document.getElementById('restart')?.addEventListener?.('click', ()=>{ reset(); startGame(); }); | |
| render(); | |
| window.addEventListener('keydown', e=>{ | |
| if(!running && (e.code === 'Enter')) startGame(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment