Skip to content

Instantly share code, notes, and snippets.

@ji4ko0
Created August 9, 2025 13:35
Show Gist options
  • Save ji4ko0/be0efc8542a12b1abcdbc1535541444c to your computer and use it in GitHub Desktop.
Save ji4ko0/be0efc8542a12b1abcdbc1535541444c to your computer and use it in GitHub Desktop.
<!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