๐ŸŽฎArcadeLab

๐Ÿ”ฎ Marble Run Builder!

by StealthWolf75
680 lines32.4 KB
โ–ถ Play
<!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.