🎮ArcadeLab

Battle Bus x Simpsons 3D

by LunarGalaxy87
514 lines15.7 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Battle Bus x Simpsons 3D</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#4aa8e0;font-family:sans-serif;}
  #info{position:fixed;bottom:70px;left:0;right:0;text-align:center;color:#fff;opacity:.7;font-size:13px;pointer-events:none;}
  canvas{display:block;touch-action:none;}
  #playBtn{
    position:fixed;bottom:16px;left:50%;transform:translateX(-50%);
    z-index:50;padding:14px 28px;font-size:16px;font-weight:bold;
    background:#f7d117;color:#3a1a06;border:3px solid #3a1a06;border-radius:10px;
    box-shadow:0 4px 0 #3a1a06; -webkit-tap-highlight-color: transparent;
  }
  #playBtn:active{transform:translateX(-50%) translateY(3px);box-shadow:0 1px 0 #3a1a06;}
</style>
</head>
<body>
<div id="info">tap the button for sound + replay 🔊</div>
<button id="playBtn">▶ Play with Sound</button>
<div id="err" style="position:fixed;top:0;left:0;right:0;background:#b00020;color:#fff;font-size:12px;padding:8px;display:none;white-space:pre-wrap;z-index:99;"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" crossorigin="anonymous"></script>
<script>
window.addEventListener('error', function(e){
  const box = document.getElementById('err');
  box.style.display = 'block';
  box.textContent = 'Error: ' + (e.message || 'unknown') +
    (e.filename ? ('\n' + e.filename + ':' + e.lineno + ':' + e.colno) : '');
});
</script>
<script>
let scene, camera, renderer, clock;
let bus, balloons = [], busGroup;
let logoGroup, oMesh;
let logoReady = false;
let startTime = null;
const duration = 5.2; // seconds
let playing = false;
let clouds = [];

// ---- audio ----
let actx = null;
let engineNodes = null;
let audioUnlocked = false;

function unlockAudio(){
  if (!actx){
    actx = new (window.AudioContext || window.webkitAudioContext)();
  }
  if (actx.state === 'suspended'){
    actx.resume();
  }
  audioUnlocked = true;
}

function makeNoiseBuffer(duration){
  const rate = actx.sampleRate;
  const buf = actx.createBuffer(1, rate*duration, rate);
  const data = buf.getChannelData(0);
  for (let i=0;i<data.length;i++) data[i] = Math.random()*2-1;
  return buf;
}

function playEngine(){
  if (!actx) return;
  stopEngine();
  const o1 = actx.createOscillator();
  const o2 = actx.createOscillator();
  const gain = actx.createGain();
  o1.type = 'sawtooth';
  o2.type = 'square';
  o1.frequency.value = 55;
  o2.frequency.value = 58;
  const filter = actx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.value = 400;
  gain.gain.value = 0.0001;
  o1.connect(filter); o2.connect(filter);
  filter.connect(gain);
  gain.connect(actx.destination);
  o1.start(); o2.start();
  gain.gain.linearRampToValueAtTime(0.05, actx.currentTime + 0.3);
  engineNodes = {o1,o2,gain,filter};
}

function stopEngine(){
  if (engineNodes){
    const {o1,o2,gain} = engineNodes;
    try{
      gain.gain.linearRampToValueAtTime(0.0001, actx.currentTime + 0.4);
      o1.stop(actx.currentTime + 0.45);
      o2.stop(actx.currentTime + 0.45);
    }catch(e){}
    engineNodes = null;
  }
}

function playWhoosh(startAt, dur){
  if (!actx) return;
  const t0 = actx.currentTime + startAt;
  const noise = actx.createBufferSource();
  noise.buffer = makeNoiseBuffer(dur);
  const filter = actx.createBiquadFilter();
  filter.type = 'bandpass';
  filter.Q.value = 0.7;
  filter.frequency.setValueAtTime(200, t0);
  filter.frequency.linearRampToValueAtTime(2200, t0 + dur*0.6);
  filter.frequency.linearRampToValueAtTime(600, t0 + dur);
  const gain = actx.createGain();
  gain.gain.setValueAtTime(0.0001, t0);
  gain.gain.linearRampToValueAtTime(0.35, t0 + dur*0.4);
  gain.gain.linearRampToValueAtTime(0.0001, t0 + dur);
  noise.connect(filter); filter.connect(gain); gain.connect(actx.destination);
  noise.start(t0);
  noise.stop(t0 + dur + 0.05);
}

function playImpact(startAt){
  if (!actx) return;
  const t0 = actx.currentTime + startAt;

  // low thud
  const osc = actx.createOscillator();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(140, t0);
  osc.frequency.exponentialRampToValueAtTime(35, t0 + 0.35);
  const oGain = actx.createGain();
  oGain.gain.setValueAtTime(0.6, t0);
  oGain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.4);
  osc.connect(oGain); oGain.connect(actx.destination);
  osc.start(t0); osc.stop(t0 + 0.45);

  // crash noise burst
  const noise = actx.createBufferSource();
  noise.buffer = makeNoiseBuffer(0.4);
  const nFilter = actx.createBiquadFilter();
  nFilter.type = 'highpass';
  nFilter.frequency.value = 800;
  const nGain = actx.createGain();
  nGain.gain.setValueAtTime(0.5, t0);
  nGain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.35);
  noise.connect(nFilter); nFilter.connect(nGain); nGain.connect(actx.destination);
  noise.start(t0); noise.stop(t0 + 0.4);
}

function playFanfare(startAt){
  if (!actx) return;
  const t0 = actx.currentTime + startAt;
  // original upbeat brass-y cartoon fanfare (not any existing theme) - bouncy staccato notes
  const notes = [
    {f:392.00, t:0.00, d:0.14},
    {f:392.00, t:0.16, d:0.14},
    {f:523.25, t:0.32, d:0.20},
    {f:466.16, t:0.54, d:0.12},
    {f:523.25, t:0.68, d:0.28},
    {f:659.25, t:1.00, d:0.30},
  ];
  notes.forEach(n=>{
    const o = actx.createOscillator();
    o.type = 'square';
    o.frequency.value = n.f;
    const g = actx.createGain();
    const nt0 = t0 + n.t;
    g.gain.setValueAtTime(0.0001, nt0);
    g.gain.linearRampToValueAtTime(0.12, nt0 + 0.02);
    g.gain.exponentialRampToValueAtTime(0.0001, nt0 + n.d);
    o.connect(g); g.connect(actx.destination);
    o.start(nt0); o.stop(nt0 + n.d + 0.02);
  });
}

function playHonk(startAt){
  if (!actx) return;
  const t0 = actx.currentTime + startAt;
  [220, 277].forEach((freq, i)=>{
    const o = actx.createOscillator();
    o.type = 'square';
    o.frequency.value = freq;
    const g = actx.createGain();
    g.gain.setValueAtTime(0.0001, t0);
    g.gain.linearRampToValueAtTime(0.08, t0+0.05);
    g.gain.linearRampToValueAtTime(0.0001, t0+0.35);
    o.connect(g); g.connect(actx.destination);
    o.start(t0); o.stop(t0+0.4);
  });
}
function init(){
  scene = new THREE.Scene();

  // sky gradient via fog + clear color
  scene.background = new THREE.Color(0x4aa8e0);
  scene.fog = new THREE.Fog(0x9fd6f0, 200, 1400);

  camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 1, 3000);
  camera.position.set(0, 40, 340);
  camera.lookAt(0,40,0);

  renderer = new THREE.WebGLRenderer({antialias:true});
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(Math.min(devicePixelRatio,2));
  renderer.shadowMap.enabled = true;
  document.body.appendChild(renderer.domElement);

  // lights
  const hemi = new THREE.HemisphereLight(0xffffff, 0x6699cc, 0.9);
  scene.add(hemi);
  const sun = new THREE.DirectionalLight(0xfff2d0, 1.1);
  sun.position.set(150, 300, 200);
  sun.castShadow = true;
  scene.add(sun);

  buildClouds();
  buildBus();
  buildLogo();
  logoReady = true;
  play();

  window.addEventListener('resize', onResize);

  const btn = document.getElementById('playBtn');
  const trigger = (e)=>{
    if (e) e.preventDefault();
    unlockAudio();
    if (logoReady) play();
    const dbg = document.getElementById('err');
    if (actx){
      dbg.style.display = 'block';
      dbg.style.background = '#1a5';
      dbg.textContent = 'Audio context state: ' + actx.state + ' (sampleRate: ' + actx.sampleRate + ')';
      setTimeout(()=>{ dbg.style.display='none'; }, 2500);
    }
  };
  btn.addEventListener('click', trigger);
  btn.addEventListener('touchend', trigger);
  renderer.domElement.addEventListener('click', trigger);

  animate();
}

function onResize(){
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
}

function buildClouds(){
  const cloudMat = new THREE.MeshStandardMaterial({color:0xffffff, roughness:1});
  for (let i=0;i<26;i++){
    const g = new THREE.Group();
    const n = 3+Math.floor(Math.random()*3);
    for (let j=0;j<n;j++){
      const s = 14+Math.random()*16;
      const sph = new THREE.Mesh(new THREE.SphereGeometry(s,8,8), cloudMat);
      sph.position.set((Math.random()-0.5)*40, (Math.random()-0.5)*10, (Math.random()-0.5)*20);
      g.add(sph);
    }
    g.position.set((Math.random()-0.5)*1400, 60+Math.random()*220, -Math.random()*1200+100);
    g.scale.setScalar(0.6+Math.random()*0.8);
    scene.add(g);
    clouds.push(g);
  }
}

function buildBus(){
  busGroup = new THREE.Group();

  const bodyMat = new THREE.MeshStandardMaterial({color:0x2f6fb0, roughness:0.5, metalness:0.1});
  const stripeMat = new THREE.MeshStandardMaterial({color:0xf2c53d, roughness:0.6});
  const windowMat = new THREE.MeshStandardMaterial({color:0xcfeeff, roughness:0.2, metalness:0.3});
  const darkMat = new THREE.MeshStandardMaterial({color:0x16324f, roughness:0.8});

  // main body
  const body = new THREE.Mesh(new THREE.BoxGeometry(60,34,110,2,2,2), bodyMat);
  body.castShadow = true;
  busGroup.add(body);

  // rounded front/back caps using spheres squashed
  const cap1 = new THREE.Mesh(new THREE.SphereGeometry(17,12,12), bodyMat);
  cap1.scale.set(1,1,0.3);
  cap1.position.set(0,0,55);
  busGroup.add(cap1);
  const cap2 = cap1.clone();
  cap2.position.z = -55;
  busGroup.add(cap2);

  // stripe
  const stripe = new THREE.Mesh(new THREE.BoxGeometry(61,8,111), stripeMat);
  stripe.position.y = 4;
  busGroup.add(stripe);

  // windows (row along both sides)
  for (let s=-1;s<=1;s+=2){
    for (let i=-3;i<=3;i++){
      const win = new THREE.Mesh(new THREE.BoxGeometry(2,10,12), windowMat);
      win.position.set(s*30.5, 10, i*14);
      busGroup.add(win);
    }
  }
  // windshield front/back
  const wf = new THREE.Mesh(new THREE.BoxGeometry(50,14,2), windowMat);
  wf.position.set(0,10,60);
  busGroup.add(wf);
  const wb = wf.clone(); wb.position.z=-60; busGroup.add(wb);

  // underside dark
  const belly = new THREE.Mesh(new THREE.BoxGeometry(58,10,108), darkMat);
  belly.position.y = -20;
  busGroup.add(belly);

  // ropes + balloons
  const ropeMat = new THREE.MeshStandardMaterial({color:0x555555});
  const balloonMat = new THREE.MeshStandardMaterial({color:0x3aa1e0, roughness:0.3, metalness:0.1});
  const positions = [
    [-24,60,40],[24,60,40],
    [-24,66,0],[24,66,0],
    [-24,60,-40],[24,60,-40],
  ];
  positions.forEach(p=>{
    const rope = new THREE.Mesh(new THREE.CylinderGeometry(0.6,0.6,60,6), ropeMat);
    rope.position.set(p[0], (p[1]+17)/2, p[2]);
    busGroup.add(rope);

    const balloon = new THREE.Mesh(new THREE.SphereGeometry(16,16,16), balloonMat);
    balloon.position.set(p[0], p[1], p[2]);
    balloon.castShadow = true;
    busGroup.add(balloon);
    balloons.push(balloon);
  });

  bus = busGroup;
  bus.rotation.y = Math.PI; // face forward towards camera
  scene.add(bus);
}

// 5x7 dot-matrix patterns for a chunky blocky 3D font (no external font files needed)
const PIXEL_FONT = {
  S: ["01111","10000","10000","01110","00001","00001","11110"],
  I: ["11111","00100","00100","00100","00100","00100","11111"],
  M: ["10001","11011","10101","10101","10001","10001","10001"],
  P: ["11110","10001","10001","11110","10000","10000","10000"],
  O: ["01110","10001","10001","10001","10001","10001","01110"],
  N: ["10001","11001","10101","10101","10011","10001","10001"]
};

function buildLogo(){
  logoGroup = new THREE.Group();
  const word = "SIMPSONS";
  const cell = 13;       // pixel block size
  const depth = 26;      // extrusion depth (3D thickness)
  const letterGap = cell*1.4;

  const yellowMat = new THREE.MeshStandardMaterial({color:0xf7d117, roughness:0.4, metalness:0.1});
  const outlineMat = new THREE.MeshStandardMaterial({color:0x3a1a06, roughness:0.6});

  const blockGeo = new THREE.BoxGeometry(cell*0.92, cell*0.92, depth);
  const outlineGeo = new THREE.BoxGeometry(cell*1.04, cell*1.04, depth*1.04);

  const letterWidth = 5*cell;
  const totalWidth = word.length*letterWidth + (word.length-1)*letterGap;
  let cursorX = -totalWidth/2;

  word.split('').forEach(ch=>{
    const pattern = PIXEL_FONT[ch];
    const letterGroup = new THREE.Group();

    for (let row=0; row<7; row++){
      for (let col=0; col<5; col++){
        if (pattern[row][col] === '1'){
          const px = col*cell;
          const py = (6-row)*cell; // flip so row 0 is top

          const outline = new THREE.Mesh(outlineGeo, outlineMat);
          outline.position.set(px, py, 0);
          letterGroup.add(outline);

          const fill = new THREE.Mesh(blockGeo, yellowMat);
          fill.position.set(px, py, 0);
          fill.castShadow = true;
          letterGroup.add(fill);
        }
      }
    }

    letterGroup.position.x = cursorX;
    logoGroup.add(letterGroup);

    if (ch === 'O' && !oMesh){
      oMesh = letterGroup;
      oMesh.userData.centerX = cursorX + letterWidth/2;
    }

    cursorX += letterWidth + letterGap;
  });

  // center vertically around row height
  logoGroup.children.forEach(g=> g.position.y -= 3*cell);

  logoGroup.position.set(0, 50, -400);
  scene.add(logoGroup);
}

function play(){
  playing = true;
  startTime = null;
  if (oMesh) { oMesh.scale.set(1,1,1); oMesh.rotation.z = 0; }

  if (actx){
    playEngine();
    playFanfare(0);
    playHonk(1.3);
    playWhoosh(0.1, duration*0.55);
    playImpact(duration*0.58);
    playWhoosh(duration*0.66, duration*0.3);
    setTimeout(stopEngine, duration*1000 + 200);
  }
}

function lerp(a,b,t){return a+(b-a)*t;}
function easeIn(t){return t*t;}
function easeOut(t){return 1-(1-t)*(1-t);}
function easeInOut(t){return t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2;}

function animate(){
  requestAnimationFrame(animate);
  const dt = clock ? clock.getDelta() : 0.016;
  if (!clock) clock = new THREE.Clock();

  clouds.forEach(c=>{
    c.position.x += 6*dt;
    if (c.position.x > 800) c.position.x = -800;
  });

  balloons.forEach((b,i)=>{
    b.position.y += Math.sin(Date.now()*0.002 + i)*0.02;
  });

  if (playing && logoReady){
    if (startTime === null) startTime = performance.now()/1000;
    const t = performance.now()/1000 - startTime;
    const p = Math.min(t/duration, 1);

    const oX = oMesh.userData.centerX;
    const oWorldX = logoGroup.position.x + oX;
    const oWorldY = logoGroup.position.y;
    const oWorldZ = logoGroup.position.z;

    const startZ = 900, startY = 90, startX = -260;
    let bx,by,bz,brotY,bscale;

    if (p < 0.55){
      const lp = easeIn(p/0.55);
      bx = lerp(startX, oWorldX, lp);
      by = lerp(startY, oWorldY, lp);
      bz = lerp(startZ, oWorldZ+10, lp);
      brotY = Math.PI + lerp(-0.3,0,lp);
      bscale = lerp(1.4, 1.0, lp);
    } else if (p < 0.68){
      const lp = (p-0.55)/0.13;
      bx = oWorldX;
      by = oWorldY;
      bz = lerp(oWorldZ+10, oWorldZ-10, lp);
      brotY = Math.PI;
      bscale = 1.0;

      // impact wobble on O
      const bump = Math.sin(lp*Math.PI);
      oMesh.scale.set(1+bump*0.3, 1+bump*0.3, 1+bump*0.3);
      oMesh.rotation.z = Math.sin(lp*Math.PI*2)*0.15;

      // camera shake
      camera.position.x += (Math.random()-0.5)*bump*3;
      camera.position.y += (Math.random()-0.5)*bump*3;
    } else {
      const lp = easeOut((p-0.68)/0.32);
      bx = lerp(oWorldX, oWorldX*0.3, lp);
      by = lerp(oWorldY, oWorldY+60, lp);
      bz = lerp(oWorldZ-10, camera.position.z+120, lp);
      brotY = Math.PI + lerp(0,0.4,lp);
      bscale = lerp(1.0, 0.7, lp);

      if (oMesh.scale.x > 1){
        oMesh.scale.multiplyScalar(0.9);
        oMesh.rotation.z *= 0.85;
      }
    }

    bus.position.set(bx,by,bz);
    bus.rotation.y = brotY;
    bus.scale.setScalar(bscale);

    if (p >= 1){
      playing = false;
      bus.position.set(startX, startY, startZ);
      oMesh.scale.set(1,1,1);
      oMesh.rotation.z = 0;
    }
  }

  renderer.render(scene, camera);
}

try {
  init();
} catch(e){
  const box = document.getElementById('err');
  box.style.display = 'block';
  box.textContent = 'Init error: ' + e.message;
  console.error(e);
}
</script>
</body>
</html>

Game Source: Battle Bus x Simpsons 3D

Creator: LunarGalaxy87

Libraries: three

Complexity: complex (514 lines, 15.7 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: battle-bus-x-simpsons-3d-lunargalaxy87" to link back to the original. Then publish at arcadelab.ai/publish.