๐ฎ Marble Run Builder!
by StealthWolf75680 lines32.4 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๐ฎ Marble Run Builder!</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Fredoka+One&family=Nunito:wght@400;700;900&display=swap');
* { margin:0; padding:0; box-sizing:border-box; }
:root {
--bg:#1a1a2e; --panel:#16213e; --gold:#f5a623;
--green:#2ecc71; --blue:#3498db; --red:#e94560; --purple:#9b59b6;
}
body { font-family:'Nunito',sans-serif; background:var(--bg); color:white; overflow:hidden; height:100vh; display:flex; flex-direction:column; }
#header {
background:linear-gradient(135deg,#e94560,#9b59b6);
padding:7px 14px; display:flex; align-items:center; justify-content:space-between;
box-shadow:0 4px 20px rgba(0,0,0,.5); z-index:100; flex-shrink:0; flex-wrap:wrap; gap:6px;
}
#title { font-family:โFredoka Oneโ,cursive; font-size:1.5em; text-shadow:3px 3px 0 rgba(0,0,0,.3); }
#level-info { font-family:โFredoka Oneโ,cursive; font-size:.95em; background:rgba(255,255,255,.2); padding:4px 12px; border-radius:20px; }
#controls-bar { display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.btn { font-family:โFredoka Oneโ,cursive; font-size:.85em; padding:5px 13px; border:none; border-radius:20px; cursor:pointer; transition:transform .1s,box-shadow .1s; box-shadow:0 4px 0 rgba(0,0,0,.3); white-space:nowrap; }
.btn:active { transform:translateY(2px); box-shadow:0 2px 0 rgba(0,0,0,.3); }
.btn-run { background:var(โgreen); color:#fff; }
.btn-clear { background:var(โred); color:#fff; }
.btn-undo { background:var(โblue); color:#fff; }
.btn-next { background:var(โgold); color:#333; }
.btn-rotate { background:#8e44ad; color:#fff; }
#main { display:flex; flex:1; overflow:hidden; }
#piece-panel {
width:122px; background:var(โpanel); border-right:3px solid rgba(255,255,255,.1);
display:flex; flex-direction:column; padding:5px 5px; gap:4px;
overflow-y:auto; flex-shrink:0;
}
#piece-panel::-webkit-scrollbar { width:4px; }
#piece-panel::-webkit-scrollbar-thumb { background:rgba(255,255,255,.15); border-radius:4px; }
.panel-section { font-family:โFredoka Oneโ,cursive; font-size:.65em; color:rgba(255,255,255,.45);
text-align:center; text-transform:uppercase; letter-spacing:1px;
margin-top:5px; border-top:1px solid rgba(255,255,255,.1); padding-top:3px; }
.piece-btn {
width:110px; height:62px; border:2.5px solid rgba(255,255,255,.18); border-radius:10px;
background:rgba(255,255,255,.04); cursor:pointer; display:flex; flex-direction:column;
align-items:center; justify-content:center; transition:all .15s; font-size:.63em;
font-family:โFredoka Oneโ,cursive; gap:2px; flex-shrink:0;
}
.piece-btn:hover { transform:scale(1.04); border-color:rgba(255,255,255,.5); background:rgba(255,255,255,.09); }
.piece-btn.selected { border-color:var(โgold); background:rgba(245,166,35,.2); box-shadow:0 0 12px rgba(245,166,35,.5); }
.piece-btn canvas { width:66px; height:36px; }
.piece-label { color:rgba(255,255,255,.88); }
#canvas-wrap { flex:1; position:relative; overflow:hidden; }
#game-canvas { display:block; cursor:crosshair; }
#overlay {
position:absolute; inset:0; display:none; align-items:center; justify-content:center;
background:rgba(0,0,0,.72); z-index:50; flex-direction:column;
}
#overlay.show { display:flex; }
#overlay-box {
background:linear-gradient(135deg,#9b59b6,#e94560);
border-radius:24px; padding:26px 42px; text-align:center;
box-shadow:0 20px 60px rgba(0,0,0,.5); max-width:420px;
}
#overlay h2 { font-family:โFredoka Oneโ,cursive; font-size:2.2em; margin-bottom:6px; text-shadow:3px 3px 0 rgba(0,0,0,.2); }
#overlay p { font-size:1.05em; margin-bottom:14px; opacity:.92; }
.stars { font-size:2em; }
#score-bar {
background:rgba(0,0,0,.3); padding:4px 12px; border-top:2px solid rgba(255,255,255,.1);
display:flex; gap:16px; align-items:center; font-family:โFredoka Oneโ,cursive; font-size:.82em; flex-shrink:0;
}
.score-item { display:flex; align-items:center; gap:5px; }
.score-dot { width:11px; height:11px; border-radius:50%; }
#hint-text { margin-left:auto; font-size:.72em; opacity:.55; font-family:โNunitoโ,sans-serif; }
</style>
</head>
<body>
<div id="header">
<div id="title">๐ฎ Marble Run Builder!</div>
<div id="level-info">Level 1</div>
<div id="controls-bar">
<button class="btn btn-rotate" onclick="rotatePiece()">๐ Flip</button>
<button class="btn btn-undo" onclick="undoLast()">โฉ Undo</button>
<button class="btn btn-clear" onclick="clearPieces()">๐ Clear</button>
<button class="btn btn-run" onclick="launchMarble()">โถ Launch!</button>
<button class="btn btn-next" onclick="nextLevel()" id="next-btn" style="display:none">Next โก</button>
</div>
</div>
<div id="main">
<div id="piece-panel"></div>
<div id="canvas-wrap">
<canvas id="game-canvas"></canvas>
<div id="overlay">
<div id="overlay-box">
<h2 id="overlay-title">๐ You Win!</h2>
<p id="overlay-msg">The marble reached the goal!</p>
<div class="stars" id="overlay-stars">โญโญโญ</div><br>
<button class="btn btn-next" onclick="nextLevel()" style="font-size:1em;padding:9px 24px;">Next Level โก</button>
<button class="btn btn-clear" onclick="retryLevel()" style="font-size:1em;padding:9px 24px;margin-left:10px;">๐ Retry</button>
</div>
</div>
</div>
</div>
<div id="score-bar">
<div class="score-item"><div class="score-dot" style="background:#e94560"></div><span id="stat-pieces">Pieces: 0</span></div>
<div class="score-item"><div class="score-dot" style="background:#f5a623"></div><span id="stat-time">Time: 0s</span></div>
<div class="score-item"><div class="score-dot" style="background:#2ecc71"></div><span id="stat-marbles">Marbles in: 0</span></div>
<div id="hint-text">Click a piece, click the board to place. Right-click to erase a piece.</div>
</div>
<script>
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// MARBLE RUN BUILDER โ 22 PIECES EDITION
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const wrap = document.getElementById('canvas-wrap');
function resize(){ canvas.width=wrap.clientWidth; canvas.height=wrap.clientHeight; }
resize();
window.addEventListener('resize',()=>{ resize(); drawAll(); });
const PW=80, PH=44, GRID=40;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 22 PIECES
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const PIECES = {
// RAMPS (6)
ramp_r: { label:'Ramp โ', color:'#3498db', cat:'Ramps', hint:'Gentle slope going right' },
ramp_l: { label:'Ramp โ', color:'#2980b9', cat:'Ramps', hint:'Gentle slope going left' },
steep_r: { label:'Steep โ', color:'#1abc9c', cat:'Ramps', hint:'Steep fast slope right' },
steep_l: { label:'Steep โ', color:'#16a085', cat:'Ramps', hint:'Steep fast slope left' },
curve_r: { label:'Curve โ', color:'#5dade2', cat:'Ramps', hint:'Curved arc sweeping right' },
curve_l: { label:'Curve โ', color:'#2e86c1', cat:'Ramps', hint:'Curved arc sweeping left' },
// PLATFORMS (3)
flat: { label:'Flat', color:'#9b59b6', cat:'Platforms', hint:'Flat shelf' },
wide_flat: { label:'Wide Flat', color:'#7d3c98', cat:'Platforms', hint:'Double-wide flat shelf' },
zig: { label:'Zig-Zag', color:'#884ea0', cat:'Platforms', hint:'W-shape ramp: right then left' },
// LAUNCHERS (3)
spring: { label:'Spring โ', color:'#8e44ad', cat:'Launchers', hint:'Launches marble upward!' },
cannon: { label:'Cannon โ', color:'#d35400', cat:'Launchers', hint:'Blasts marble sideways at speed!' },
fan: { label:'Fan โ', color:'#e67e22', cat:'Launchers', hint:'Pushes marble upward continuously' },
// REDIRECTORS (4)
funnel: { label:'Funnel โ', color:'#e67e22', cat:'Redirectors', hint:'Funnels marble down to center' },
splitter: { label:'Splitter', color:'#c0392b', cat:'Redirectors', hint:'Splits marble left AND right' },
flip_l: { label:'Flip โ', color:'#e74c3c', cat:'Redirectors', hint:'Sharp redirect to the left' },
flip_r: { label:'Flip โ', color:'#c0392b', cat:'Redirectors', hint:'Sharp redirect to the right' },
// SPECIAL (4)
bounce: { label:'Bounce', color:'#e94560', cat:'Special', hint:'Elastic super-bounce pad!' },
loop: { label:'Loop', color:'#f5a623', cat:'Special', hint:'Marble does a full loop-the-loop!' },
teleport: { label:'Teleport', color:'#ff00ff', cat:'Special', hint:'Place 2 to teleport marble between them!' },
blackhole: { label:'Black Hole', color:'#6c3483', cat:'Special', hint:'Sucks nearby marbles in!' },
// PIPES (2)
pipe_h: { label:'Pipe H', color:'#27ae60', cat:'Pipes', hint:'Horizontal enclosed tunnel' },
pipe_v: { label:'Pipe V', color:'#1e8449', cat:'Pipes', hint:'Vertical enclosed pipe' },
};
const CATEGORIES = ['Ramps','Platforms','Launchers','Redirectors','Special','Pipes'];
// FLIP PAIRS for Rotate button
const FLIPS = {ramp_r:'ramp_l',ramp_l:'ramp_r',steep_r:'steep_l',steep_l:'steep_r',
curve_r:'curve_l',curve_l:'curve_r',flip_r:'flip_l',flip_l:'flip_r'};
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// LEVELS (12)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const ALL = Object.keys(PIECES);
const LEVELS = [
{ name:'Level 1 โ First Drop', goal:{x:.85,y:.85}, start:{x:.1,y:.08}, hint:'Place ramps to guide the marble to ๐!', allowed:['ramp_r','ramp_l','flat'], marbles:1, par:4 },
{ name:'Level 2 โ Zigzag', goal:{x:.15,y:.88}, start:{x:.82,y:.08}, hint:'Zigzag the marble down to the left!', allowed:['ramp_r','ramp_l','flat','funnel','zig'], marbles:2, par:6 },
{ name:'Level 3 โ Bounce House', goal:{x:.5,y:.88}, start:{x:.1,y:.08}, hint:'Bounce pads redirect marbles!', allowed:['ramp_r','ramp_l','flat','bounce','funnel'], marbles:2, par:6 },
{ name:'Level 4 โ Speed Racer', goal:{x:.88,y:.88}, start:{x:.1,y:.08}, hint:'Steep ramps = FAST marbles!', allowed:['ramp_r','ramp_l','steep_r','steep_l','flat','bounce'], marbles:3, par:8 },
{ name:'Level 5 โ Curves Ahead', goal:{x:.5,y:.88}, start:{x:.1,y:.08}, hint:'Curved ramps guide marbles smoothly!', allowed:['ramp_r','ramp_l','curve_r','curve_l','flat','funnel'], marbles:2, par:7 },
{ name:'Level 6 โ Pipe Dream', goal:{x:.85,y:.88}, start:{x:.5,y:.05}, hint:'Pipes carry marbles through tunnels!', allowed:['flat','pipe_h','pipe_v','ramp_r','ramp_l','funnel'], marbles:2, par:7 },
{ name:'Level 7 โ Splitsville', goal:{x:.88,y:.88}, start:{x:.5,y:.05}, hint:'Split the marble to reach BOTH goals!', allowed:['ramp_r','ramp_l','flat','splitter','funnel','bounce'], marbles:2, par:7, twoGoals:true, goal2:{x:.12,y:.88} },
{ name:'Level 8 โ Spring Up!', goal:{x:.85,y:.12}, start:{x:.1,y:.88}, hint:'Springs launch marbles UPWARD!', allowed:['ramp_r','ramp_l','spring','flat','funnel','bounce'], marbles:2, par:8 },
{ name:'Level 9 โ Cannon Run', goal:{x:.88,y:.88}, start:{x:.08,y:.5}, hint:'The cannon blasts marble sideways at speed!', allowed:['ramp_r','ramp_l','flat','cannon','bounce','funnel','wide_flat'], marbles:1, par:5 },
{ name:'Level 10 โ Loop de Loop', goal:{x:.85,y:.88}, start:{x:.08,y:.08}, hint:'Loop = the ultimate marble trick!', allowed:['ramp_r','ramp_l','steep_r','flat','loop','bounce','funnel','zig'], marbles:2, par:8 },
{ name:'Level 11 โ Warp Zone', goal:{x:.85,y:.85}, start:{x:.1,y:.08}, hint:'Teleporters warp the marble instantly!', allowed:['ramp_r','ramp_l','flat','teleport','bounce','funnel','pipe_h','fan'], marbles:2, par:9 },
{ name:'Level 12 โ Marble Madness!',goal:{x:.5,y:.88}, start:{x:.1,y:.08}, hint:'ALL pieces unlocked. Epic final challenge!', allowed:ALL, marbles:3, par:12 },
];
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// STATE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let currentLevel=0, placedPieces=[], selectedPiece=null;
let simRunning=false, simMarbles=[], simFrame=null;
let timeStart=0, elapsedTime=0, marblesIn=0, victoryShown=false;
let ghostPos=null, timerInterval=null;
const GRAVITY=0.34, FRICTION=0.994;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// PANEL
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildPanel(){
const panel=document.getElementById('piece-panel');
panel.innerHTML='';
const allowed=new Set(LEVELS[currentLevel].allowed);
CATEGORIES.forEach(cat=>{
const items=Object.entries(PIECES).filter(([k,v])=>v.cat===cat&&allowed.has(k));
if(!items.length) return;
const sec=document.createElement('div'); sec.className='panel-section'; sec.textContent=cat;
panel.appendChild(sec);
items.forEach(([type,p])=>{
const btn=document.createElement('div'); btn.className='piece-btn'; btn.id='pbtn_'+type; btn.title=p.hint;
const mc=document.createElement('canvas'); mc.width=72; mc.height=40;
drawPieceShape(mc.getContext('2d'),type,3,2,66,36,p.color,1,false);
btn.appendChild(mc);
const lbl=document.createElement('div'); lbl.className='piece-label'; lbl.textContent=p.label;
btn.appendChild(lbl);
btn.addEventListener('click',()=>selectPiece(type));
panel.appendChild(btn);
});
});
}
function selectPiece(t){
selectedPiece=t;
document.querySelectorAll('.piece-btn').forEach(b=>b.classList.remove('selected'));
const btn=document.getElementById('pbtn_'+t); if(btn) btn.classList.add('selected');
canvas.style.cursor='crosshair';
}
function rotatePiece(){
if(!selectedPiece) return;
if(FLIPS[selectedPiece]) selectPiece(FLIPS[selectedPiece]);
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DRAW PIECES
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawPieceShape(c,type,x,y,w,h,col,alpha,shadow){
c.save();
c.globalAlpha=alpha;
if(shadow){ c.shadowColor='rgba(0,0,0,.4)'; c.shadowBlur=7; c.shadowOffsetY=3; }
c.strokeStyle=col; c.fillStyle=col; c.lineWidth=4; c.lineCap='round'; c.lineJoin='round';
const dl=(x1,y1,x2,y2,cl,lw)=>{
c.strokeStyle=cl||col; c.lineWidth=lw||4;
c.beginPath(); c.moveTo(x1,y1); c.lineTo(x2,y2); c.stroke();
};
switch(type){
case 'ramp_r':
dl(x,y+h,x+w,y); dl(x,y+h,x+w,y,'rgba(255,255,255,.3)',1.5); break;
case 'ramp_l':
dl(x,y,x+w,y+h); dl(x,y,x+w,y+h,'rgba(255,255,255,.3)',1.5); break;
case 'steep_r':
dl(x,y+h,x+w*.55,y);
c.strokeStyle=col+'99'; c.lineWidth=2;
c.beginPath(); c.moveTo(x+w*.55,y); c.lineTo(x+w*.55,y+h); c.stroke(); break;
case 'steep_l':
dl(x+w*.45,y,x+w,y+h);
c.strokeStyle=col+'99'; c.lineWidth=2;
c.beginPath(); c.moveTo(x+w*.45,y); c.lineTo(x+w*.45,y+h); c.stroke(); break;
case 'curve_r':
c.strokeStyle=col; c.lineWidth=4; c.beginPath();
c.moveTo(x,y+h); c.quadraticCurveTo(x,y,x+w,y); c.stroke(); break;
case 'curve_l':
c.strokeStyle=col; c.lineWidth=4; c.beginPath();
c.moveTo(x,y); c.quadraticCurveTo(x+w,y,x+w,y+h); c.stroke(); break;
case 'flat':
dl(x,y+h*.5,x+w,y+h*.5); dl(x,y+h*.5,x+w,y+h*.5,'rgba(255,255,255,.3)',1.5); break;
case 'wide_flat':
c.fillStyle=col+'44'; c.fillRect(x,y+h*.38,w,h*.24);
dl(x,y+h*.5,x+w,y+h*.5); dl(x,y+h*.5,x+w,y+h*.5,'rgba(255,255,255,.3)',1.5); break;
case 'zig':
dl(x,y+h,x+w*.5,y); dl(x+w*.5,y,x+w,y+h); break;
case 'spring': {
const sx2=x+w*.25,sw2=w*.5;
c.strokeStyle=col; c.lineWidth=3;
for(let i=0;i<5;i++){
const ax=(i%2===0)?sx2:sx2+sw2, bx=(i%2===0)?sx2+sw2:sx2;
c.beginPath(); c.moveTo(ax,y+i*(h/5)); c.lineTo(bx,y+(i+1)*(h/5)); c.stroke();
}
break;
}
case 'cannon': {
c.fillStyle=col;
rrect(c,x+w*.28,y+h*.28,w*.62,h*.44,5); c.fill();
c.beginPath(); c.arc(x+w*.22,y+h*.65,h*.28,0,Math.PI*2); c.fill();
c.fillStyle='#f39c12'; c.beginPath(); c.arc(x+w*.9,y+h*.5,6,0,Math.PI*2); c.fill();
break;
}
case 'fan': {
const fx=x+w*.5,fy=y+h*.68;
c.fillStyle=col+'99';
for(let i=0;i<3;i++){
const a=i*(Math.PI*2/3);
c.beginPath(); c.moveTo(fx,fy); c.arc(fx,fy,h*.38,a,a+1); c.closePath(); c.fill();
}
c.fillStyle=col; c.beginPath(); c.arc(fx,fy,5,0,Math.PI*2); c.fill();
dl(x,y+h,x+w,y+h);
break;
}
case 'funnel':
dl(x,y,x+w*.5,y+h); dl(x+w,y,x+w*.5,y+h); break;
case 'splitter':
dl(x+w*.5,y,x+w*.5,y+h*.5);
dl(x+w*.5,y+h*.5,x,y+h); dl(x+w*.5,y+h*.5,x+w,y+h); break;
case 'flip_l':
dl(x+w,y,x,y+h*.5); dl(x,y+h*.5,x+w*.28,y+h); break;
case 'flip_r':
dl(x,y,x+w,y+h*.5); dl(x+w,y+h*.5,x+w*.72,y+h); break;
case 'bounce': {
const br=Math.min(w,h)*.36;
c.fillStyle=col; c.beginPath(); c.arc(x+w*.5,y+h*.65,br,0,Math.PI*2); c.fill();
c.strokeStyle='white'; c.lineWidth=2;
c.beginPath(); c.arc(x+w*.5,y+h*.45,br*.38,0,Math.PI*2); c.stroke(); break;
}
case 'loop': {
const lr=Math.min(w,h)*.36;
c.strokeStyle=col; c.lineWidth=4;
c.beginPath(); c.arc(x+w*.5,y+h*.5,lr,0,Math.PI*2); c.stroke();
dl(x,y+h*.5,x+w*.5-lr,y+h*.5); dl(x+w*.5+lr,y+h*.5,x+w,y+h*.5); break;
}
case 'teleport': {
const tr=Math.min(w,h)*.38;
const gr=c.createRadialGradient(x+w*.5,y+h*.5,1,x+w*.5,y+h*.5,tr);
gr.addColorStop(0,'#fff'); gr.addColorStop(.5,col); gr.addColorStop(1,'transparent');
c.fillStyle=gr; c.beginPath(); c.arc(x+w*.5,y+h*.5,tr,0,Math.PI*2); c.fill();
c.strokeStyle='white'; c.lineWidth=2;
c.beginPath(); c.arc(x+w*.5,y+h*.5,tr,0,Math.PI*2); c.stroke();
c.fillStyle='white'; c.font=`${h*.5}px serif`;
c.textAlign='center'; c.textBaseline='middle'; c.fillText('โฆ',x+w*.5,y+h*.5); break;
}
case 'blackhole': {
const bhr=Math.min(w,h)*.38, bhx=x+w*.5, bhy=y+h*.5;
for(let i=4;i>=1;i--){
c.strokeStyle=`rgba(108,52,131,${i*.22})`; c.lineWidth=i*2.5;
c.beginPath(); c.arc(bhx,bhy,bhr*(i/4),0,Math.PI*2); c.stroke();
}
c.fillStyle='#0d001a'; c.beginPath(); c.arc(bhx,bhy,bhr*.28,0,Math.PI*2); c.fill(); break;
}
case 'pipe_h': {
c.fillStyle=col; rrect(c,x,y+h*.27,w,h*.46,8); c.fill();
c.fillStyle='rgba(255,255,255,.22)'; rrect(c,x+2,y+h*.27,w-4,h*.18,6); c.fill(); break;
}
case 'pipe_v': {
c.fillStyle=col; rrect(c,x+w*.3,y,w*.4,h,8); c.fill();
c.fillStyle='rgba(255,255,255,.22)'; rrect(c,x+w*.3+2,y+2,w*.14,h-4,6); c.fill(); break;
}
}
if(shadow){ c.shadowColor='transparent'; c.shadowBlur=0; c.shadowOffsetY=0; }
c.globalAlpha=1; c.restore();
}
function rrect(c,x,y,w,h,r){
c.beginPath();
c.moveTo(x+r,y); c.lineTo(x+w-r,y); c.arcTo(x+w,y,x+w,y+r,r);
c.lineTo(x+w,y+h-r); c.arcTo(x+w,y+h,x+w-r,y+h,r);
c.lineTo(x+r,y+h); c.arcTo(x,y+h,x,y+h-r,r);
c.lineTo(x,y+r); c.arcTo(x,y,x+r,y,r); c.closePath();
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DRAW ALL
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawAll(){
const W=canvas.width, H=canvas.height;
ctx.clearRect(0,0,W,H);
const bg=ctx.createLinearGradient(0,0,0,H);
bg.addColorStop(0,'#1a1a3e'); bg.addColorStop(1,'#0d0d1f');
ctx.fillStyle=bg; ctx.fillRect(0,0,W,H);
ctx.fillStyle='rgba(255,255,255,.035)';
for(let x=GRID;x<W;x+=GRID) for(let y=GRID;y<H;y+=GRID) ctx.fillRect(x-1,y-1,2,2);
const lv=LEVELS[currentLevel];
placedPieces.forEach(p=>drawPieceShape(ctx,p.type,p.x-PW/2,p.y-PH/2,PW,PH,PIECES[p.type].color,1,true));
if(ghostPos&&selectedPiece&&!simRunning)
drawPieceShape(ctx,selectedPiece,ghostPos.x-PW/2,ghostPos.y-PH/2,PW,PH,PIECES[selectedPiece].color,.38,false);
drawMarker(lv.start.x*W,lv.start.y*H,'#2ecc71','START');
drawGoal(lv.goal.x*W,lv.goal.y*H,'#f5a623');
if(lv.twoGoals) drawGoal(lv.goal2.x*W,lv.goal2.y*H,'#2ecc71');
simMarbles.forEach(m=>{
if(!m.active) return;
const gr=ctx.createRadialGradient(m.x-4,m.y-4,2,m.x,m.y,m.r);
gr.addColorStop(0,'#fff'); gr.addColorStop(.4,m.color); gr.addColorStop(1,'#000');
ctx.fillStyle=gr; ctx.shadowColor=m.color; ctx.shadowBlur=16;
ctx.beginPath(); ctx.arc(m.x,m.y,m.r,0,Math.PI*2); ctx.fill();
ctx.shadowBlur=0;
});
}
function drawMarker(x,y,color,label){
ctx.save(); ctx.shadowColor=color; ctx.shadowBlur=18;
ctx.fillStyle=color; ctx.beginPath();
ctx.moveTo(x-16,y-12); ctx.lineTo(x+16,y-12); ctx.lineTo(x+16,y+10);
ctx.lineTo(x,y+22); ctx.lineTo(x-16,y+10); ctx.closePath(); ctx.fill();
ctx.fillStyle='white'; ctx.font='bold 10px Nunito';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(label,x,y+1);
ctx.restore();
}
function drawGoal(x,y,color){
ctx.save(); ctx.shadowColor=color; ctx.shadowBlur=20;
ctx.strokeStyle=color; ctx.lineWidth=3; ctx.strokeRect(x-22,y-18,44,36);
ctx.fillStyle=color+'33'; ctx.fillRect(x-22,y-18,44,36);
ctx.fillStyle=color; ctx.font='18px serif';
ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('๐',x,y);
ctx.restore();
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// PHYSICS
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function launchMarble(){
if(simRunning){ stopSim(); return; }
simRunning=true; marblesIn=0; victoryShown=false;
buildTeleportPairs();
const lv=LEVELS[currentLevel];
const W=canvas.width, H=canvas.height;
const colors=['#e94560','#3498db','#f5a623','#2ecc71','#9b59b6','#1abc9c'];
simMarbles=[];
for(let i=0;i<lv.marbles;i++){
simMarbles.push({
x:lv.start.x*W+(i-Math.floor(lv.marbles/2))*16,
y:lv.start.y*H+8, vx:(Math.random()-.5)*.5, vy:0,
r:10, color:colors[i%colors.length], active:true, atGoal:false, lastTeleport:-1
});
}
timeStart=Date.now();
timerInterval=setInterval(()=>{
elapsedTime=((Date.now()-timeStart)/1000).toFixed(1);
document.getElementById('stat-time').textContent=`Time: ${elapsedTime}s`;
},100);
document.querySelector('.btn-run').textContent='โ Stop';
simLoop();
}
let teleportPairs=[];
function buildTeleportPairs(){
teleportPairs=[];
const tps=placedPieces.filter(p=>p.type==='teleport');
for(let i=0;i<tps.length-1;i+=2) teleportPairs.push([tps[i],tps[i+1]]);
}
function stopSim(){
simRunning=false; cancelAnimationFrame(simFrame); clearInterval(timerInterval);
document.querySelector('.btn-run').textContent='โถ Launch!';
}
function simLoop(){ if(!simRunning) return; updatePhysics(); drawAll(); updateStats(); simFrame=requestAnimationFrame(simLoop); }
function updatePhysics(){
const W=canvas.width, H=canvas.height;
const lv=LEVELS[currentLevel];
simMarbles.forEach(m=>{
if(!m.active) return;
m.vy+=GRAVITY; m.vx*=FRICTION; m.vy*=FRICTION;
// Special field forces
placedPieces.forEach(p=>{
if(p.type==='blackhole'){
const dx=p.x-m.x,dy=p.y-m.y,d=Math.sqrt(dx*dx+dy*dy);
if(d<160&&d>1){ m.vx+=(dx/d)*0.9; m.vy+=(dy/d)*0.9; if(d<20) m.active=false; }
}
if(p.type==='fan'){
const px2=p.x-PW/2;
if(m.x>px2&&m.x<px2+PW&&m.y>p.y-80&&m.y<p.y+PH) m.vy-=1.3;
}
});
m.x+=m.vx; m.y+=m.vy;
placedPieces.forEach(p=>collideWith(m,p));
// Teleport
teleportPairs.forEach((pair,pi)=>{
pair.forEach((tp,ti)=>{
if(m.lastTeleport===pi) return;
if(Math.abs(m.x-tp.x)<18&&Math.abs(m.y-tp.y)<18){
const dest=pair[1-ti]; m.x=dest.x; m.y=dest.y;
m.vy=Math.abs(m.vy)+2; m.vx*=.5; m.lastTeleport=pi;
setTimeout(()=>{ m.lastTeleport=-1; },700);
}
});
});
// Goals
const gx=lv.goal.x*W,gy=lv.goal.y*H;
if(!m.atGoal&&Math.abs(m.x-gx)<26&&Math.abs(m.y-gy)<26){ m.atGoal=true; m.active=false; marblesIn++; checkVictory(); }
if(lv.twoGoals){
const gx2=lv.goal2.x*W,gy2=lv.goal2.y*H;
if(!m.atGoal&&Math.abs(m.x-gx2)<26&&Math.abs(m.y-gy2)<26){ m.atGoal=true; m.active=false; marblesIn++; checkVictory(); }
}
if(m.y>H+40||m.x<-60||m.x>W+60){ m.active=false; checkVictory(); }
if(m.y>H-m.r){ m.y=H-m.r; m.vy*=-0.35; }
});
}
function collideWith(m,p){
const px=p.x-PW/2,py=p.y-PH/2,pw=PW,ph=PH;
switch(p.type){
case 'ramp_r': lc(m,px,py+ph,px+pw,py); break;
case 'ramp_l': lc(m,px,py,px+pw,py+ph); break;
case 'steep_r': lc(m,px,py+ph,px+pw*.55,py); break;
case 'steep_l': lc(m,px+pw*.45,py,px+pw,py+ph); break;
case 'curve_r': bezCollide(m,px,py+ph,px,py,px+pw,py); break;
case 'curve_l': bezCollide(m,px,py,px+pw,py,px+pw,py+ph); break;
case 'flat': lc(m,px,py+ph*.5,px+pw,py+ph*.5); break;
case 'wide_flat': lc(m,px,py+ph*.5,px+pw,py+ph*.5); break;
case 'zig': lc(m,px,py+ph,px+pw*.5,py); lc(m,px+pw*.5,py,px+pw,py+ph); break;
case 'funnel': lc(m,px,py,px+pw*.5,py+ph); lc(m,px+pw,py,px+pw*.5,py+ph); break;
case 'splitter':
lc(m,px+pw*.5,py,px+pw*.5,py+ph*.5);
lc(m,px+pw*.5,py+ph*.5,px,py+ph);
lc(m,px+pw*.5,py+ph*.5,px+pw,py+ph); break;
case 'flip_l': lc(m,px+pw,py,px,py+ph*.5); lc(m,px,py+ph*.5,px+pw*.28,py+ph); break;
case 'flip_r': lc(m,px,py,px+pw,py+ph*.5); lc(m,px+pw,py+ph*.5,px+pw*.72,py+ph); break;
case 'bounce': bc(m,p.x,p.y+ph*.15,Math.min(pw,ph)*.36); break;
case 'loop': loopC(m,p.x,p.y,Math.min(pw,ph)*.36); break;
case 'spring': springC(m,px,py,pw,ph); break;
case 'cannon': cannonC(m,p,px,py,pw,ph); break;
case 'pipe_h': pipeH(m,px,py+ph*.27,pw,ph*.46); break;
case 'pipe_v': pipeV(m,px+pw*.3,py,pw*.4,ph); break;
}
}
function lc(m,x1,y1,x2,y2){
const dx=x2-x1,dy=y2-y1,len=Math.sqrt(dx*dx+dy*dy); if(len<1)return;
const nx=-dy/len,ny=dx/len;
const t=((m.x-x1)*dx+(m.y-y1)*dy)/(len*len);
if(t<-0.05||t>1.05)return;
const dist=(m.x-(x1+t*dx))*nx+(m.y-(y1+t*dy))*ny;
if(dist>0&&dist<m.r+2){
m.x+=nx*(m.r+2-dist); m.y+=ny*(m.r+2-dist);
const dot=m.vx*nx+m.vy*ny;
if(dot<0){ m.vx-=2*dot*nx*.62; m.vy-=2*dot*ny*.62; }
}
}
function bc(m,cx,cy,r){
const dx=m.x-cx,dy=m.y-cy,d=Math.sqrt(dx*dx+dy*dy);
if(d<r+m.r&&d>0){
const nx=dx/d,ny=dy/d;
m.x=cx+nx*(r+m.r+1); m.y=cy+ny*(r+m.r+1);
const dot=m.vx*nx+m.vy*ny;
if(dot<0){ m.vx-=2*dot*nx*1.4; m.vy-=2*dot*ny*1.4; }
}
}
function loopC(m,cx,cy,r){
const dx=m.x-cx,dy=m.y-cy,d=Math.sqrt(dx*dx+dy*dy);
if(d<r+m.r+10&&d>r-m.r-10&&d>1){
const nx=dx/d,ny=dy/d;
if(d>r){ m.x=cx+nx*(r-m.r-1); m.y=cy+ny*(r-m.r-1); }
else { m.x=cx+nx*(r+m.r+1); m.y=cy+ny*(r+m.r+1); }
const spd=Math.sqrt(m.vx*m.vx+m.vy*m.vy);
m.vx=-ny*Math.max(spd,5); m.vy=nx*Math.max(spd,5);
}
}
function springC(m,px,py,pw,ph){
if(m.x>px&&m.x<px+pw&&m.y>py+ph*.4&&m.y<py+ph+m.r){
m.y=py+ph*.35-m.r; m.vy=-15; m.vx+=(Math.random()-.5)*2;
}
}
function cannonC(m,p,px,py,pw,ph){
if(m.x>px-6&&m.x<px+pw*.38&&m.y>py+ph*.15&&m.y<py+ph*.85){
m.vx=17; m.vy=-2; m.x=px+pw+14; m.y=py+ph*.5;
}
}
function pipeH(m,px,py,pw,ph){
if(m.x>px&&m.x<px+pw){
if(m.y<py+m.r&&m.y>py-m.r*2){ m.y=py+m.r; m.vy=Math.abs(m.vy)*.22; }
if(m.y>py+ph-m.r&&m.y<py+ph+m.r*2){ m.y=py+ph-m.r; m.vy=-Math.abs(m.vy)*.22; }
}
}
function pipeV(m,px,py,pw,ph){
if(m.y>py&&m.y<py+ph){
if(m.x<px+m.r&&m.x>px-m.r*2){ m.x=px+m.r; m.vx=Math.abs(m.vx)*.22; }
if(m.x>px+pw-m.r&&m.x<px+pw+m.r*2){ m.x=px+pw-m.r; m.vx=-Math.abs(m.vx)*.22; }
}
}
function bezCollide(m,sx,sy,cpx,cpy,ex,ey){
const N=14;
function bz(a,b,c,t){ return (1-t)*(1-t)*a+2*(1-t)*t*b+t*t*c; }
for(let i=0;i<N;i++){
lc(m, bz(sx,cpx,ex,i/N),bz(sy,cpy,ey,i/N), bz(sx,cpx,ex,(i+1)/N),bz(sy,cpy,ey,(i+1)/N));
}
}
function checkVictory(){
if(!simMarbles.every(m=>!m.active)) return;
stopSim();
if(!victoryShown){
victoryShown=true;
const needed=LEVELS[currentLevel].twoGoals?2:LEVELS[currentLevel].marbles;
marblesIn>=needed ? showVictory() : showDefeat();
}
}
function showVictory(){
const n=placedPieces.length,par=LEVELS[currentLevel].par;
const stars=n<=par?'โญโญโญ':n<=par*1.4?'โญโญ':'โญ';
document.getElementById('overlay-title').textContent='๐ Level Complete!';
document.getElementById('overlay-msg').textContent=`${marblesIn} marble${marblesIn>1?'s':''} reached the goal in ${elapsedTime}s using ${n} pieces!`;
document.getElementById('overlay-stars').textContent=stars;
document.getElementById('overlay').classList.add('show');
if(currentLevel<LEVELS.length-1) document.getElementById('next-btn').style.display='';
}
function showDefeat(){
document.getElementById('overlay-title').textContent='๐ฎ Try Again!';
document.getElementById('overlay-msg').textContent=`Only ${marblesIn} marble${marblesIn>1?'s':''} reached the goal!`;
document.getElementById('overlay-stars').textContent='';
document.getElementById('overlay').classList.add('show');
}
function nextLevel(){
if(currentLevel<LEVELS.length-1){ currentLevel++; resetLevel(); }
else{
document.getElementById('overlay-title').textContent='๐ ALL DONE!';
document.getElementById('overlay-msg').textContent='You are a marble engineering LEGEND!';
document.getElementById('overlay-stars').textContent='๐๐๐';
}
}
function retryLevel(){ resetLevel(); }
function resetLevel(){
stopSim(); placedPieces=[]; simMarbles=[]; marblesIn=0; elapsedTime=0; victoryShown=false;
document.getElementById('overlay').classList.remove('show');
document.getElementById('next-btn').style.display='none';
document.querySelector('.btn-run').textContent='โถ Launch!';
document.getElementById('level-info').textContent=LEVELS[currentLevel].name;
document.getElementById('hint-text').textContent=LEVELS[currentLevel].hint;
buildPanel(); updateStats(); drawAll();
}
function clearPieces(){ if(simRunning)return; placedPieces=[]; drawAll(); updateStats(); }
function undoLast() { if(simRunning)return; placedPieces.pop(); drawAll(); updateStats(); }
function updateStats(){
document.getElementById('stat-pieces').textContent=`Pieces: ${placedPieces.length}`;
document.getElementById('stat-marbles').textContent=`Marbles in: ${marblesIn}`;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// INPUT
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('mousemove',e=>{
const r=canvas.getBoundingClientRect();
ghostPos={x:snap(e.clientX-r.left),y:snap(e.clientY-r.top)};
if(!simRunning) drawAll();
});
canvas.addEventListener('mouseleave',()=>{ ghostPos=null; drawAll(); });
canvas.addEventListener('click',e=>{
if(!selectedPiece||simRunning) return;
const r=canvas.getBoundingClientRect();
placedPieces.push({type:selectedPiece,x:snap(e.clientX-r.left),y:snap(e.clientY-r.top)});
updateStats(); drawAll();
});
canvas.addEventListener('contextmenu',e=>{
e.preventDefault(); if(simRunning)return;
const r=canvas.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top;
const idx=placedPieces.findLastIndex(p=>Math.abs(p.x-mx)<PW/2&&Math.abs(p.y-my)<PH/2);
if(idx>=0){ placedPieces.splice(idx,1); drawAll(); updateStats(); }
else{
selectedPiece=null;
document.querySelectorAll('.piece-btn').forEach(b=>b.classList.remove('selected'));
canvas.style.cursor='default';
}
});
canvas.addEventListener('touchstart',e=>{
e.preventDefault(); if(!selectedPiece||simRunning)return;
const r=canvas.getBoundingClientRect(),t=e.touches[0];
placedPieces.push({type:selectedPiece,x:snap(t.clientX-r.left),y:snap(t.clientY-r.top)});
updateStats(); drawAll();
},{passive:false});
function snap(v){ return Math.round(v/GRID)*GRID; }
resetLevel();
</script>
</body>
</html>Game Source: ๐ฎ Marble Run Builder!
Creator: StealthWolf75
Libraries: none
Complexity: complex (680 lines, 32.4 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: marble-run-builder-stealthwolf75" to link back to the original. Then publish at arcadelab.ai/publish.