Pixel Sandbox - 2D Sandbox Game
by SonicLegend13907 lines36.9 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Pixel Sandbox - 2D Sandbox Game</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
overflow: hidden;
background: #000;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
#game {
display: block;
width: 100vw;
height: 100vh;
image-rendering: pixelated;
image-rendering: crisp-edges;
cursor: crosshair;
}
#ui {
position: fixed;
top: 10px;
left: 10px;
right: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
z-index: 10;
pointer-events: none;
}
#ui > * { pointer-events: all; }
#ui button {
background: rgba(0,0,0,0.75);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
backdrop-filter: blur(8px);
cursor: pointer;
transition: all 0.2s;
}
#ui button:hover { background: rgba(30,30,30,0.9); transform: translateY(-1px); }
#ui button:active { transform: translateY(0); }
#modeLabel {
color: #fff;
background: rgba(0,0,0,0.75);
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.2);
}
#hotbar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
background: rgba(0,0,0,0.75);
padding: 6px;
border-radius: 12px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.15);
z-index: 10;
}
.slot {
width: 52px;
height: 52px;
border: 2px solid #333;
border-radius: 8px;
cursor: pointer;
position: relative;
background: #1a1a1a;
transition: all 0.15s;
overflow: hidden;
}
.slot:hover { border-color: #666; }
.slot.selected {
border-color: #fff;
transform: translateY(-6px);
box-shadow: 0 6px 20px rgba(255,255,255,0.25), inset 0 0 0 1px rgba(255,255,255,0.5);
background: #2a2a2a;
}
.slot .icon {
width: 100%;
height: 100%;
image-rendering: pixelated;
}
.slot span {
position: absolute;
bottom: 2px;
right: 5px;
color: #fff;
font-size: 11px;
font-weight: bold;
text-shadow: 1px 1px 2px #000, -1px -1px 2px #000;
pointer-events: none;
}
.bars {
position: fixed;
bottom: 86px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 6px;
z-index: 10;
pointer-events: none;
}
.bar {
width: 208px;
height: 18px;
background: rgba(0,0,0,0.8);
border-radius: 9px;
overflow: hidden;
border: 2px solid rgba(255,255,255,0.2);
display: none;
backdrop-filter: blur(4px);
}
.bar .fill {
height: 100%;
width: 100%;
transition: width 0.3s ease;
position: relative;
}
#healthBar .fill {
background: linear-gradient(90deg, #e74c3c, #c0392b);
box-shadow: inset 0 2px 4px rgba(255,255,255,0.2);
}
#hungerBar .fill {
background: linear-gradient(90deg, #d4a574, #b97a56);
box-shadow: inset 0 2px 4px rgba(255,255,255,0.2);
}
.bar.active { display: block; }
#tutorial {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(10,10,20,0.95);
color: #fff;
padding: 32px 40px;
border-radius: 20px;
text-align: center;
max-width: 90%;
width: 420px;
z-index: 100;
backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 20px 60px rgba(0,0,0,0.8);
}
#tutorial h2 {
margin: 0 0 16px;
font-size: 28px;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
#tutorial p {
margin: 10px 0;
opacity: 0.9;
font-size: 15px;
line-height: 1.5;
}
#tutorial .controls {
background: rgba(255,255,255,0.05);
padding: 14px;
border-radius: 10px;
margin: 16px 0;
border: 1px solid rgba(255,255,255,0.1);
}
#tutorial button {
margin-top: 20px;
padding: 12px 32px;
background: linear-gradient(90deg, #4CAF50, #45a049);
border: none;
color: #fff;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 15px rgba(76,175,80,0.3);
}
#tutorial button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(76,175,80,0.4); }
#toast {
position: fixed;
top: 70px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0,0,0,0.9);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
opacity: 0;
transition: all 0.3s;
z-index: 200;
pointer-events: none;
border: 1px solid rgba(255,255,255,0.2);
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#mobileControls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 200px;
pointer-events: none;
z-index: 20;
display: none;
}
.joystick {
position: absolute;
left: 25px;
bottom: 25px;
width: 120px;
height: 120px;
background: rgba(255,255,255,0.08);
border: 2px solid rgba(255,255,255,0.25);
border-radius: 50%;
pointer-events: all;
backdrop-filter: blur(4px);
}
.stick {
position: absolute;
width: 54px;
height: 54px;
background: rgba(255,255,255,0.4);
border: 2px solid rgba(255,255,255,0.6);
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
transition: transform 0.05s;
}
.mbtn {
position: absolute;
width: 72px;
height: 72px;
background: rgba(255,255,255,0.12);
border: 2px solid rgba(255,255,255,0.35);
border-radius: 50%;
pointer-events: all;
color: #fff;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
backdrop-filter: blur(6px);
text-align: center;
line-height: 1.2;
user-select: none;
}
.mbtn:active { background: rgba(255,255,255,0.25); transform: scale(0.95); }
#jumpBtn { right: 25px; bottom: 110px; }
#actionBtn { right: 105px; bottom: 25px; width: 84px; height: 84px; font-size: 13px; }
#modeBtnM { right: 25px; bottom: 25px; }
@media (max-width: 768px) {
#mobileControls { display: block; }
#hotbar { bottom: 210px; transform: translateX(-50%) scale(0.9); }
.bars { bottom: 280px; }
#ui { top: 8px; left: 8px; }
#ui button, #modeLabel { padding: 7px 11px; font-size: 12px; }
.slot { width: 46px; height: 46px; }
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
<button id="saveBtn">Save World</button>
<button id="loadBtn">Load World</button>
<button id="resetBtn">Reset</button>
<button id="modeBtn">Creative Mode</button>
<div id="modeLabel">Creative</div>
</div>
<div class="bars">
<div class="bar" id="hungerBar"><div class="fill"></div></div>
<div class="bar" id="healthBar"><div class="fill"></div></div>
</div>
<div id="hotbar"></div>
<div id="tutorial">
<h2>PIXEL SANDBOX</h2>
<p>An infinite 2D world ready to explore!</p>
<div class="controls">
<p><strong>Left Click</strong> = break • <strong>Right Click</strong> = place</p>
<p><strong>WASD / Arrows</strong> = move • <strong>Space</strong> = jump</p>
<p><strong>1-9</strong> = select block • <strong>Scroll</strong> = switch</p>
<p><strong>S</strong> = crouch</p>
</div>
<p style="opacity:0.7; font-size:13px">Dig, build, and survive!</p>
<button id="startBtn">Start Playing</button>
</div>
<div id="toast"></div>
<div id="mobileControls">
<div class="joystick" id="joystick"><div class="stick" id="stick"></div></div>
<div class="mbtn" id="jumpBtn">JUMP</div>
<div class="mbtn" id="actionBtn">BREAK</div>
<div class="mbtn" id="modeBtnM">PLACE</div>
</div>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
let width = window.innerWidth, height = window.innerHeight;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.scale(dpr, dpr);
const BLOCK = 16;
const SCALE = width < 768 ? 1.75 : 2.2;
const TS = BLOCK * SCALE;
const WORLD_H = 140;
const VIEW_W = Math.ceil(width / TS) + 4;
const VIEW_H = Math.ceil(height / TS) + 4;
const BLOCKS = { AIR:0, GRASS:1, DIRT:2, STONE:3, PLANK:4, SAND:5, GLASS:6, LEAVES:7, WATER:8, LAVA:9, LOG:10, COAL:11, IRON:12, GOLD:13, FLOWER:14, TALLGRASS:15, BEDROCK:16, GRAVEL:17 };
const HARDNESS = {0:0,1:0.5,2:0.4,3:1.8,4:1,5:0.4,6:0.3,7:0.2,8:0,9:0,10:1.2,11:2,12:2.5,13:3,14:0.1,15:0.1,16:999,17:0.5};
const isSolid = id => id!==0 && id!==8 && id!==9 && id!==14 && id!==15;
const isFalling = id => id===5 || id===17;
const seed = 1337;
function rand2d(x,y){ const s = Math.sin(x*127.1 + y*311.7 + seed)*43758.5453; return s-Math.floor(s); }
function smoothNoise(x,y){ const ix=Math.floor(x),iy=Math.floor(y),fx=x-ix,fy=y-iy; const a=rand2d(ix,iy),b=rand2d(ix+1,iy),c=rand2d(ix,iy+1),d=rand2d(ix+1,iy+1); const ux=fx*fx*(3-2*fx),uy=fy*fy*(3-2*fy); return a*(1-ux)*(1-uy)+b*ux*(1-uy)+c*(1-ux)*uy+d*ux*uy; }
function perlin(x,y,o=3){ let v=0,amp=1,freq=1,max=0; for(let i=0;i<o;i++){ v+=smoothNoise(x*freq,y*freq)*amp; max+=amp; amp*=0.5; freq*=2; } return v/max; }
const worldMod = new Map();
const worldGen = new Map();
function getGen(x,y){
const key = x+','+y;
if(worldGen.has(key)) return worldGen.get(key);
let id = BLOCKS.AIR;
const surface = 70 + Math.floor((perlin(x*0.012,0,4)-0.5)*22);
const waterLevel = 68;
if(y > WORLD_H-3) id = BLOCKS.BEDROCK;
else if(y === surface){
if(y > waterLevel-2) id = BLOCKS.SAND;
else id = BLOCKS.GRASS;
}
else if(y > surface && y < surface+4){
id = (y > waterLevel-1) ? BLOCKS.SAND : BLOCKS.DIRT;
}
else if(y >= surface+4 && y < WORLD_H-3){
const cave = perlin(x*0.07, y*0.07, 3);
if(cave > 0.58) id = BLOCKS.AIR;
else {
id = BLOCKS.STONE;
const r = rand2d(x,y);
if(y > 85 && r < 0.07) id = BLOCKS.COAL;
else if(y > 95 && r < 0.045) id = BLOCKS.IRON;
else if(y > 110 && r < 0.025) id = BLOCKS.GOLD;
else if(y > 100 && rand2d(x*2,y*3) < 0.03) id = BLOCKS.GRAVEL;
}
}
if(y < surface && y >= waterLevel && surface < waterLevel){
id = BLOCKS.WATER;
}
// trees
if(id===BLOCKS.GRASS){
const treeChance = rand2d(x, 999);
if(treeChance < 0.018 && y < waterLevel){
// tree will be handled in getBlock for structure
}
else if(rand2d(x*1.3, y) < 0.06) id = BLOCKS.FLOWER;
else if(rand2d(x*0.7, y+5) < 0.12) id = BLOCKS.TALLGRASS;
}
// tree structure check
const tx = Math.floor(x/1)*1;
if(rand2d(tx,999) < 0.018){
const th = 4 + Math.floor(rand2d(tx,555)*2);
const treeSurf = 70 + Math.floor((perlin(tx*0.012,0,4)-0.5)*22);
if(x===tx && y>=treeSurf-th && y<treeSurf) id = BLOCKS.LOG;
const dx = Math.abs(x-tx), dy = y-(treeSurf-th);
if(dx<=2 && dy>=-1 && dy<=1 && !(x===tx && y<treeSurf)){
if(dx+Math.abs(dy) < 3.5) id = BLOCKS.LEAVES;
}
}
worldGen.set(key,id);
return id;
}
function getBlock(x,y){
if(y<0 || y>=WORLD_H) return BLOCKS.AIR;
const key = x+','+y;
if(worldMod.has(key)) return worldMod.get(key);
return getGen(x,y);
}
function setBlock(x,y,id){
const key = x+','+y;
const gen = getGen(x,y);
if(id===gen) worldMod.delete(key);
else worldMod.set(key, id);
}
const player = { x:0, y:0, vx:0, vy:0, w:12, h:26, onGround:false, facing:1 };
function spawnPlayer(){
const sx=0;
const sy = 70 + Math.floor((perlin(sx*0.012,0,4)-0.5)*22);
player.x = sx*BLOCK - 6;
player.y = (sy-5)*BLOCK;
player.vx=0; player.vy=0;
}
spawnPlayer();
let camX=0, camY=0;
let time=8000;
const hotbar = [1,2,3,4,5,6,7,8,9];
let selected=0;
let gameMode='creative';
let health=20, hunger=20;
let hungerTimer=0;
const inputs = { left:false, right:false, up:false, down:false };
const mouse = { x:0, y:0, downL:false, downR:false, worldX:0, worldY:0 };
let breaking=null, breakProgress=0;
const particles=[];
const clouds=[];
for(let i=0;i<12;i++) clouds.push({x:Math.random()*4000-2000, y:20+Math.random()*60, s:2+Math.random()*3, spd:0.2+Math.random()*0.3});
const stars=[];
for(let i=0;i<180;i++) stars.push({x:Math.random(), y:Math.random()*0.6, b:0.3+Math.random()*0.7, t:Math.random()*6.28});
let audioCtx=null;
function initAudio(){ if(!audioCtx) audioCtx = new (window.AudioContext||window.webkitAudioContext)(); }
function playSound(f,type='sine',t=0.08,v=0.12){
if(!audioCtx) return;
const o=audioCtx.createOscillator(), g=audioCtx.createGain();
o.type=type; o.frequency.value=f+Math.random()*20;
g.gain.setValueAtTime(v,audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime+t);
o.connect(g); g.connect(audioCtx.destination);
o.start(); o.stop(audioCtx.currentTime+t);
}
function showToast(msg){
const t=document.getElementById('toast');
t.textContent=msg; t.classList.add('show');
setTimeout(()=>t.classList.remove('show'),2000);
}
function addParticle(x,y,color,blockId){
for(let i=0;i<8;i++){
particles.push({x:x+TS/2,y:y+TS/2,vx:(Math.random()-0.5)*4,vx0:(Math.random()-0.5)*4,vy:Math.random()*-3-1,life:30+Math.random()*20,color,sz:2+Math.random()*2});
}
}
function collide(px,py,w,h){
const left=Math.floor(px/BLOCK), right=Math.floor((px+w-0.1)/BLOCK);
const top=Math.floor(py/BLOCK), bot=Math.floor((py+h-0.1)/BLOCK);
for(let y=top;y<=bot;y++) for(let x=left;x<=right;x++) if(isSolid(getBlock(x,y))) return true;
return false;
}
const hotbarEl=document.getElementById('hotbar');
function buildHotbar(){
hotbarEl.innerHTML='';
const colors={1:'linear-gradient(to bottom,#4e9c42 0% 32%,#8B5A2B 32%)',2:'#8B5A2B',3:'#9b9b9b',4:'#c49a6c',5:'#e2c98a',6:'rgba(160,216,239,0.7)',7:'#3a8c3e',8:'#2e6bcc',9:'#d04525',10:'#6d4c41',11:'#555',12:'#b5855c',13:'#d4af37'};
hotbar.forEach((id,i)=>{
const s=document.createElement('div'); s.className='slot'+(i===selected?' selected':''); s.dataset.i=i;
const ic=document.createElement('div'); ic.className='icon'; ic.style.background=colors[id]||'#333';
if(id===4){ ic.style.backgroundImage='repeating-linear-gradient(#c49a6c,#c49a6c 8px,#b3895f 8px,#b3895f 9px)'; }
if(id===6){ ic.style.border='2px solid #7fc0d9'; ic.style.boxSizing='border-box'; }
if(id===10){ ic.style.background='linear-gradient(90deg,#5d4037 0 30%,#6d4c41 30% 70%,#5d4037 70%)'; }
const num=document.createElement('span'); num.textContent=i+1;
s.appendChild(ic); s.appendChild(num);
s.onclick=()=>{selected=i; updateHotbar(); initAudio();};
hotbarEl.appendChild(s);
});
}
function updateHotbar(){ document.querySelectorAll('.slot').forEach((s,i)=>s.classList.toggle('selected',i===selected)); }
buildHotbar();
function update(dt){
time=(time+0.4)%24000;
// input
let tx=0;
if(inputs.left) tx-=1;
if(inputs.right) tx+=1;
player.facing = tx!==0 ? (tx>0?1:-1) : player.facing;
const speed = inputs.down ? 1.8 : 3.2;
player.vx += (tx*speed - player.vx)*0.3;
const inWater = getBlock(Math.floor((player.x+player.w/2)/BLOCK), Math.floor((player.y+player.h*0.6)/BLOCK))===BLOCKS.WATER;
const inLava = getBlock(Math.floor((player.x+player.w/2)/BLOCK), Math.floor((player.y+player.h*0.6)/BLOCK))===BLOCKS.LAVA;
if(inputs.up){
if(inWater){ player.vy = -2.2; }
else if(player.onGround){ player.vy = -8.8; player.onGround=false; playSound(520,'square',0.06,0.08); }
}
player.vy += inWater ? 0.18 : 0.46;
if(inWater) player.vy *= 0.85;
player.vy = Math.max(-12, Math.min(12, player.vy));
const prevVy=player.vy;
// horizontal
let nx = player.x + player.vx;
if(!collide(nx, player.y, player.w, player.h)) player.x=nx;
else player.vx=0;
// vertical
let ny = player.y + player.vy;
player.onGround=false;
if(!collide(player.x, ny, player.w, player.h)) player.y=ny;
else {
if(player.vy>0){ player.onGround=true; if(gameMode==='survival' && prevVy>11){ const dmg=Math.floor((prevVy-10)/2.5); health=Math.max(0,health-dmg); updateBars(); playSound(180,'sawtooth',0.15,0.2); if(health<=0) respawn(); } }
player.vy=0;
}
// clamp fall void
if(player.y > WORLD_H*BLOCK) { if(gameMode==='survival'){health=0;respawn();} else {player.y=0; player.vy=0;} }
if(inLava && gameMode==='survival'){ health=Math.max(0,health-0.15); updateBars(); if(health<=0) respawn(); }
// camera
const vw = width/SCALE, vh=height/SCALE;
const targetCamX = player.x + player.w/2 - vw/2;
const targetCamY = player.y + player.h/2 - vh/2;
camX += (targetCamX - camX)*0.12;
camY += (targetCamY - camY)*0.12;
camY = Math.max(-20, Math.min(camY, WORLD_H*BLOCK - vh + 20));
// mouse world
mouse.worldX = camX + mouse.x/SCALE;
mouse.worldY = camY + mouse.y/SCALE;
const bx=Math.floor(mouse.worldX/BLOCK), by=Math.floor(mouse.worldY/BLOCK);
const dist = Math.hypot((bx*BLOCK+BLOCK/2)-(player.x+player.w/2), (by*BLOCK+BLOCK/2)-(player.y+player.h/2));
const inReach = dist < 90;
// breaking
if(mouse.downL && inReach){
const bid=getBlock(bx,by);
if(bid!==0 && bid!==BLOCKS.BEDROCK){
if(!breaking || breaking.x!==bx || breaking.y!==by){ breaking={x:bx,y:by}; breakProgress=0; }
const hard=HARDNESS[bid]||1;
breakProgress += gameMode==='creative' ? 1 : dt/(hard*0.8);
if(breakProgress>=1){
setBlock(bx,by,BLOCKS.AIR);
addParticle(bx*BLOCK, by*BLOCK, '#aaa', bid);
playSound(240+Math.random()*80,'square',0.07,0.1);
breaking=null; breakProgress=0;
}
}
} else { breaking=null; breakProgress=0; }
if(mouse.downR && inReach && !mouse.downL){
const bid=getBlock(bx,by);
const sel = hotbar[selected];
if(bid===0 || bid===BLOCKS.WATER || bid===BLOCKS.LAVA || bid===BLOCKS.TALLGRASS || bid===BLOCKS.FLOWER){
const px=player.x, py=player.y;
const blockCollide = bx*BLOCK < px+player.w && bx*BLOCK+BLOCK > px && by*BLOCK < py+player.h && by*BLOCK+BLOCK > py;
if(!blockCollide || sel===BLOCKS.WATER || sel===BLOCKS.LAVA){
let placeId=sel;
if(sel===8) placeId=BLOCKS.WATER;
else if(sel===9) placeId=BLOCKS.LAVA;
setBlock(bx,by,placeId);
playSound(320,'sine',0.05,0.07);
mouse.downR=false;
}
}
}
// physics sand & water
const startX=Math.floor(camX/BLOCK)-2, endX=startX+VIEW_W+2;
const startY=Math.max(0,Math.floor(camY/BLOCK)-2), endY=Math.min(WORLD_H-1, startY+VIEW_H+4);
if(frame%2===0){
for(let x=startX;x<endX;x++){
for(let y=endY;y>=startY;y--){
const id=getBlock(x,y);
if(isFalling(id)){
const below=getBlock(x,y+1);
if(below===0 || below===8 || below===9 || below===14 || below===15){
setBlock(x,y,0); setBlock(x,y+1,id);
} else if(below!==id){
const left=getBlock(x-1,y+1), right=getBlock(x+1,y+1);
if(left===0 && getBlock(x-1,y)===0 && Math.random()<0.5){ setBlock(x,y,0); setBlock(x-1,y+1,id); }
else if(right===0 && getBlock(x+1,y)===0){ setBlock(x,y,0); setBlock(x+1,y+1,id); }
}
} else if(id===BLOCKS.WATER || id===BLOCKS.LAVA){
const isLava=id===BLOCKS.LAVA;
if(Math.random() < (isLava?0.15:0.6)){
const below=getBlock(x,y+1);
if(below===0 || below===BLOCKS.TALLGRASS || below===BLOCKS.FLOWER){ setBlock(x,y+1,id); }
else {
const dirs=[-1,1]; if(Math.random()<0.5) dirs.reverse();
for(const d of dirs){
const nx=x+d, ny=y;
const nb=getBlock(nx,ny), nbelow=getBlock(nx,ny+1);
if(nb===0 && (nbelow!==0 || isLava)){ setBlock(nx,ny,id); break; }
}
}
}
}
}
}
}
// particles
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.vy+=0.22; p.x+=p.vx0*0.3; p.y+=p.vy; p.life--;
if(p.life<=0) particles.splice(i,1);
}
// survival
if(gameMode==='survival'){
hungerTimer+=dt;
if(hungerTimer>560){ hungerTimer=0; hunger=Math.max(0,hunger-1); if(hunger===0) health=Math.max(0,health-1); updateBars(); }
if(hunger>17 && health<20 && frame%90===0){ health=Math.min(20,health+0.5); updateBars(); }
if(health<=0) respawn();
}
// clouds
clouds.forEach(c=>{ c.x+=c.spd*dt; if(c.x>camX/5+3000) c.x = camX/5-3000; });
}
let frame=0;
function respawn(){
spawnPlayer();
health=20; hunger=20;
updateBars();
showToast('You respawned!');
playSound(150,'sawtooth',0.3,0.15);
}
function updateBars(){
document.getElementById('healthBar').querySelector('.fill').style.width=(health/20*100)+'%';
document.getElementById('hungerBar').querySelector('.fill').style.width=(hunger/20*100)+'%';
}
function drawBlock(id, x,y){
ctx.save();
ctx.translate(x,y);
const s=TS;
switch(id){
case BLOCKS.GRASS:
ctx.fillStyle='#8B5A2B'; ctx.fillRect(0,0,s,s);
ctx.fillStyle='#5e9c4f'; ctx.fillRect(0,0,s,s*0.28);
ctx.fillStyle='#4a7d3f'; ctx.fillRect(0,s*0.28,s,2);
break;
case BLOCKS.DIRT: ctx.fillStyle='#8B5A2B'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#6b4423'; for(let i=0;i<3;i++) ctx.fillRect((i*5)%s,((i*7)%s),2,2); break;
case BLOCKS.STONE: ctx.fillStyle='#9b9b9b'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#868686'; ctx.fillRect(0,0,s,2); ctx.fillRect(0,s-2,s,2); break;
case BLOCKS.PLANK: ctx.fillStyle='#c49a6c'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#a67c52'; for(let i=1;i<4;i++) ctx.fillRect(0,i*s/4,s,1); break;
case BLOCKS.SAND: ctx.fillStyle='#e2c98a'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#d4b875'; ctx.fillRect(2,2,s-4,s-4); break;
case BLOCKS.GLASS: ctx.fillStyle='rgba(160,216,239,0.35)'; ctx.fillRect(0,0,s,s); ctx.strokeStyle='rgba(255,255,255,0.5)'; ctx.lineWidth=2; ctx.strokeRect(1,1,s-2,s-2); break;
case BLOCKS.LEAVES: ctx.fillStyle='#3a8c3e'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#2d6f2f'; for(let i=0;i<4;i++) ctx.fillRect(Math.random()*s,Math.random()*s,3,3); break;
case BLOCKS.WATER: ctx.fillStyle='rgba(46,107,204,0.7)'; ctx.fillRect(0,0,s,s); ctx.fillStyle='rgba(80,140,230,0.5)'; ctx.fillRect(0,0,s,3); break;
case BLOCKS.LAVA: ctx.fillStyle='#d04525'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#ff6b35'; ctx.fillRect(2,2,s-4,s-4); break;
case BLOCKS.LOG: ctx.fillStyle='#6d4c41'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#5d4037'; ctx.fillRect(3,0,s-6,s); ctx.fillStyle='#4e342e'; ctx.fillRect(0,0,s,2); ctx.fillRect(0,s-2,s,2); break;
case BLOCKS.COAL: ctx.fillStyle='#7a7a7a'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#222'; ctx.fillRect(4,4,5,5); ctx.fillRect(10,9,4,4); break;
case BLOCKS.IRON: ctx.fillStyle='#9b9b9b'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#d4a574'; ctx.fillRect(3,5,6,3); ctx.fillRect(8,10,5,4); break;
case BLOCKS.GOLD: ctx.fillStyle='#9b9b9b'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#f1c40f'; ctx.fillRect(4,4,5,5); ctx.fillRect(9,8,4,4); break;
case BLOCKS.FLOWER: ctx.fillStyle='#8B5A2B'; ctx.fillRect(0,s*0.6,s,s*0.4); ctx.fillStyle='#4caf50'; ctx.fillRect(s*0.45,s*0.3,3,s*0.5); ctx.fillStyle='#e91e63'; ctx.fillRect(s*0.3,2,8,8); break;
case BLOCKS.TALLGRASS: ctx.strokeStyle='#5e9c4f'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(s*0.3,s); ctx.lineTo(s*0.4,s*0.3); ctx.moveTo(s*0.6,s); ctx.lineTo(s*0.5,s*0.2); ctx.stroke(); break;
case BLOCKS.BEDROCK: ctx.fillStyle='#2a2a2a'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#1a1a1a'; ctx.fillRect(2,2,s-4,s-4); break;
case BLOCKS.GRAVEL: ctx.fillStyle='#a0a0a0'; ctx.fillRect(0,0,s,s); ctx.fillStyle='#8a8a8a'; for(let i=0;i<5;i++) ctx.fillRect((i*3)%s,(i*5)%s,2,2); break;
}
ctx.restore();
}
function draw(){
// sky
const tNorm = (time%24000)/24000;
const day = Math.sin(tNorm*Math.PI*2 - Math.PI/2)*0.5+0.5;
const skyTop = `rgb(${ Math.floor(11+90*day) },${ Math.floor(16+120*day) },${ Math.floor(38+100*day) })`;
const skyBot = `rgb(${ Math.floor(30+120*day) },${ Math.floor(40+150*day) },${ Math.floor(70+140*day) })`;
const grad=ctx.createLinearGradient(0,0,0,height);
grad.addColorStop(0,skyTop); grad.addColorStop(1,skyBot);
ctx.fillStyle=grad; ctx.fillRect(0,0,width,height);
// stars
if(day<0.3){
ctx.save(); ctx.globalAlpha=(0.3-day)*3;
stars.forEach(s=>{ const tw=0.7+0.3*Math.sin(frame*0.02+s.t); ctx.globalAlpha*=tw*s.b; ctx.fillStyle='#fff'; ctx.fillRect(s.x*width, s.y*height*0.7, 2,2); });
ctx.restore();
}
// sun/moon
const sunAng = tNorm*Math.PI*2 - Math.PI/2;
const sx = width*0.5 + Math.cos(sunAng)*width*0.4;
const sy = height*0.5 + Math.sin(sunAng)*height*0.35;
ctx.save(); ctx.globalAlpha=0.9;
if(day>0.2){ ctx.fillStyle='#ffdb70'; ctx.beginPath(); ctx.arc(sx,sy,28,0,Math.PI*2); ctx.fill(); ctx.fillStyle='rgba(255,219,112,0.2)'; ctx.beginPath(); ctx.arc(sx,sy,45,0,Math.PI*2); ctx.fill(); }
else { ctx.fillStyle='#e0e7ff'; ctx.beginPath(); ctx.arc(sx,sy,22,0,Math.PI*2); ctx.fill(); ctx.fillStyle='rgba(180,190,220,0.15)'; ctx.beginPath(); ctx.arc(sx-8,sy-6,6,0,Math.PI*2); ctx.fill(); }
ctx.restore();
// clouds parallax
ctx.save();
ctx.translate(-(camX*0.15)% (TS*40),0);
clouds.forEach(c=>{
const cx = (c.x - camX*0.1) % (width+400) - 200;
const cy = c.y;
ctx.globalAlpha=0.85;
ctx.fillStyle='#fff';
const sz = c.s*TS*0.6;
ctx.beginPath(); ctx.arc(cx,cy,sz*0.5,0,7); ctx.arc(cx+sz*0.4,cy,sz*0.4,0,7); ctx.arc(cx-sz*0.35,cy+5,sz*0.35,0,7); ctx.fill();
});
ctx.restore();
// world
const startX=Math.floor(camX/BLOCK)-1, endX=startX+VIEW_W+2;
const startY=Math.max(0,Math.floor(camY/BLOCK)-1), endY=Math.min(WORLD_H-1, startY+VIEW_H+2);
ctx.save(); ctx.scale(SCALE,SCALE); ctx.translate(-camX,-camY);
for(let y=startY;y<=endY;y++){
for(let x=startX;x<=endX;x++){
const id=getBlock(x,y);
if(id!==0){
drawBlock(id, x*BLOCK, y*BLOCK);
}
}
}
// particles
particles.forEach(p=>{ ctx.fillStyle=p.color; ctx.globalAlpha=Math.min(1,p.life/30); ctx.fillRect((p.x-camX)/SCALE, (p.y-camY)/SCALE, p.sz,p.sz); });
ctx.globalAlpha=1;
// player
ctx.save();
const px=player.x, py=player.y;
// shadow
ctx.globalAlpha=0.2; ctx.fillStyle='#000'; ctx.beginPath(); ctx.ellipse(px+player.w/2, py+player.h+2, player.w*0.4, 4,0,0,7); ctx.fill(); ctx.globalAlpha=1;
// body
ctx.translate(px,py);
const crouch = inputs.down ? 0.7 : 1;
// legs
ctx.fillStyle='#3b5fcc'; ctx.fillRect(2,14*crouch,4,10*crouch); ctx.fillRect(6,14*crouch,4,10*crouch);
// torso
ctx.fillStyle='#4a7bd1'; ctx.fillRect(1,6*crouch,10,9*crouch);
// arms
ctx.fillStyle='#e8b888'; ctx.fillRect(0,7*crouch,2,7*crouch); ctx.fillRect(10,7*crouch,2,7*crouch);
// head
ctx.fillStyle='#e8b888'; ctx.fillRect(2,0,8,8);
ctx.fillStyle='#2a1f14'; ctx.fillRect(2,0,8,3);
// eyes
ctx.fillStyle='#000'; ctx.fillRect(player.facing>0?6:4,3,1,1); ctx.fillRect(player.facing>0?7:3,3,1,1);
ctx.restore();
// breaking overlay
if(breaking){
const bx=breaking.x*BLOCK, by=breaking.y*BLOCK;
ctx.strokeStyle='rgba(0,0,0,0.6)'; ctx.lineWidth=2; ctx.globalAlpha=0.7;
const prog=Math.min(1,breakProgress);
ctx.strokeRect(bx+1,by+1,BLOCK-2,BLOCK-2);
for(let i=0;i<prog*8;i++){ ctx.beginPath(); ctx.moveTo(bx+Math.random()*BLOCK, by+Math.random()*BLOCK); ctx.lineTo(bx+Math.random()*BLOCK, by+Math.random()*BLOCK); ctx.stroke(); }
}
// hover
const hx=Math.floor(mouse.worldX/BLOCK), hy=Math.floor(mouse.worldY/BLOCK);
const hd=Math.hypot((hx*BLOCK+BLOCK/2)-(player.x+player.w/2), (hy*BLOCK+BLOCK/2)-(player.y+player.h/2));
if(hd<90 && getBlock(hx,hy)!==0){
ctx.strokeStyle='rgba(255,255,255,0.9)'; ctx.lineWidth=2; ctx.setLineDash([4,3]); ctx.strokeRect(hx*BLOCK+0.5, hy*BLOCK+0.5, BLOCK-1, BLOCK-1); ctx.setLineDash([]);
}
ctx.restore();
// lighting
const underground = player.y/BLOCK > 75;
let dark = (1-day)*0.65 + (underground?0.3:0);
if(dark>0){ ctx.fillStyle=`rgba(5,8,25,${Math.min(0.82,dark)})`; ctx.fillRect(0,0,width,height); }
// crosshair
ctx.strokeStyle='rgba(255,255,255,0.85)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(width/2-10, height/2); ctx.lineTo(width/2+10, height/2); ctx.moveTo(width/2, height/2-10); ctx.lineTo(width/2, height/2+10); ctx.stroke();
}
function loop(ts){
frame++;
update(1);
draw();
requestAnimationFrame(loop);
}
// events
window.addEventListener('resize',()=>{ width=innerWidth; height=innerHeight; canvas.width=width*dpr; canvas.height=height*dpr; ctx.setTransform(1,0,0,1); ctx.scale(dpr,dpr); });
canvas.addEventListener('mousemove',e=>{ const r=canvas.getBoundingClientRect(); mouse.x=e.clientX-r.left; mouse.y=e.clientY-r.top; });
canvas.addEventListener('mousedown',e=>{ initAudio(); if(e.button===0) mouse.downL=true; if(e.button===2) mouse.downR=true; });
window.addEventListener('mouseup',e=>{ if(e.button===0) mouse.downL=false; if(e.button===2) mouse.downR=false; });
canvas.addEventListener('contextmenu',e=>e.preventDefault());
canvas.addEventListener('wheel',e=>{ e.preventDefault(); selected=(selected+(e.deltaY>0?1:-1)+9)%9; updateHotbar(); },{passive:false});
window.addEventListener('keydown',e=>{
if(['1','2','3','4','5','6','7','8','9'].includes(e.key)){ selected=parseInt(e.key)-1; updateHotbar(); }
if(e.key==='a'||e.key==='ArrowLeft') inputs.left=true;
if(e.key==='d'||e.key==='ArrowRight') inputs.right=true;
if(e.key==='w'||e.key===' '||e.key==='ArrowUp') inputs.up=true;
if(e.key==='s'||e.key==='ArrowDown') inputs.down=true;
});
window.addEventListener('keyup',e=>{
if(e.key==='a'||e.key==='ArrowLeft') inputs.left=false;
if(e.key==='d'||e.key==='ArrowRight') inputs.right=false;
if(e.key==='w'||e.key===' '||e.key==='ArrowUp') inputs.up=false;
if(e.key==='s'||e.key==='ArrowDown') inputs.down=false;
});
// mobile
const isTouch='ontouchstart' in window;
if(isTouch) document.getElementById('mobileControls').style.display='block';
const joy=document.getElementById('joystick'), stick=document.getElementById('stick');
let joyActive=false, joyId=null;
joy.addEventListener('touchstart',e=>{ joyActive=true; joyId=e.touches[0].identifier; e.preventDefault(); });
window.addEventListener('touchmove',e=>{
for(const t of e.touches){
if(t.identifier===joyId){
const r=joy.getBoundingClientRect(), cx=r.left+r.width/2, cy=r.top+r.height/2;
let dx=t.clientX-cx, dy=t.clientY-cy; const d=Math.hypot(dx,dy), m=40; if(d>m){dx=dx/d*m; dy=dy/d*m;}
stick.style.transform=`translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
inputs.left=dx<-15; inputs.right=dx>15;
}
}
},{passive:false});
window.addEventListener('touchend',e=>{
for(const t of e.changedTouches){ if(t.identifier===joyId){ joyActive=false; joyId=null; stick.style.transform='translate(-50%,-50%)'; inputs.left=false; inputs.right=false; } }
});
document.getElementById('jumpBtn').addEventListener('touchstart',e=>{e.preventDefault(); inputs.up=true; setTimeout(()=>inputs.up=false,150); initAudio();});
let mobileMode='break';
const actionBtn=document.getElementById('actionBtn'), modeBtnM=document.getElementById('modeBtnM');
function updateMobileBtns(){ actionBtn.textContent=mobileMode==='break'?'BREAK':'PLACE'; }
updateMobileBtns();
modeBtnM.addEventListener('touchstart',e=>{e.preventDefault(); mobileMode=mobileMode==='break'?'place':'break'; updateMobileBtns();});
actionBtn.addEventListener('touchstart',e=>{
e.preventDefault(); initAudio();
const bx=Math.floor((player.x+player.w/2 + player.facing*BLOCK*2.5)/BLOCK);
const by=Math.floor((player.y+player.h/2)/BLOCK);
if(mobileMode==='break'){ if(getBlock(bx,by)!==0){ setBlock(bx,by,0); playSound(240,'square',0.07,0.1); addParticle(bx*BLOCK,by*BLOCK,'#aaa'); } }
else { const id=hotbar[selected]; if(getBlock(bx,by)===0){ setBlock(bx,by, id===8?8: id===9?9: id); playSound(320,'sine',0.05,0.07); } }
});
canvas.addEventListener('touchstart',e=>{
if(e.target.closest('#mobileControls')) return;
e.preventDefault(); initAudio();
const t=e.touches[0], r=canvas.getBoundingClientRect();
mouse.x=t.clientX-r.left; mouse.y=t.clientY-r.top;
mouse.worldX=camX+mouse.x/SCALE; mouse.worldY=camY+mouse.y/SCALE;
const bx=Math.floor(mouse.worldX/BLOCK), by=Math.floor(mouse.worldY/BLOCK);
if(mobileMode==='break') mouse.downL=true; else mouse.downR=true;
setTimeout(()=>{mouse.downL=false; mouse.downR=false;},120);
},{passive:false});
// UI buttons
document.getElementById('saveBtn').onclick=()=>{
const data={mod:Array.from(worldMod.entries()),player:{x:player.x,y:player.y},time,mode:gameMode,health,hunger};
localStorage.setItem('pixelSandbox',JSON.stringify(data));
showToast('World saved!');
playSound(600,'sine',0.12,0.1);
};
document.getElementById('loadBtn').onclick=()=>{
const raw=localStorage.getItem('pixelSandbox');
if(!raw){ showToast('No save found'); return; }
try{
const data=JSON.parse(raw);
worldMod.clear(); data.mod.forEach(([k,v])=>worldMod.set(k,v));
player.x=data.player.x; player.y=data.player.y; time=data.time||8000; gameMode=data.mode||'creative';
health=data.health||20; hunger=data.hunger||20;
document.getElementById('modeBtn').textContent=gameMode==='creative'?'Creative Mode':'Survival Mode';
document.getElementById('modeLabel').textContent=gameMode==='creative'?'Creative':'Survival';
document.getElementById('healthBar').classList.toggle('active',gameMode==='survival');
document.getElementById('hungerBar').classList.toggle('active',gameMode==='survival');
updateBars(); showToast('World loaded!'); playSound(700,'sine',0.15,0.1);
}catch{ showToast('Failed to load'); }
};
document.getElementById('resetBtn').onclick=()=>{
if(confirm('Reset world? All buildings will be lost.')){ worldMod.clear(); worldGen.clear(); spawnPlayer(); time=8000; showToast('World reset'); }
};
document.getElementById('modeBtn').onclick=()=>{
gameMode = gameMode==='creative' ? 'survival' : 'creative';
const isSur = gameMode==='survival';
document.getElementById('modeBtn').textContent=isSur?'Survival Mode':'Creative Mode';
document.getElementById('modeLabel').textContent=isSur?'Survival':'Creative';
document.getElementById('healthBar').classList.toggle('active',isSur);
document.getElementById('hungerBar').classList.toggle('active',isSur);
if(isSur){ health=20; hunger=20; updateBars(); }
showToast(isSur?'Survival Mode enabled':'Creative Mode enabled');
playSound(500,'triangle',0.1,0.08);
};
document.getElementById('startBtn').onclick=()=>{ document.getElementById('tutorial').style.display='none'; initAudio(); playSound(650,'sine',0.2,0.12); };
updateBars();
requestAnimationFrame(loop);
// prevent scroll
document.body.addEventListener('touchmove',e=>{ if(e.target===canvas) e.preventDefault(); },{passive:false});
})();
</script>
</body>
</html>
Game Source: Pixel Sandbox - 2D Sandbox Game
Creator: SonicLegend13
Libraries: none
Complexity: complex (907 lines, 36.9 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: pixel-sandbox-2d-sandbox-game-soniclegend13" to link back to the original. Then publish at arcadelab.ai/publish.