🎮ArcadeLab

Space Mystery 3D - Expanded

by AtomicBolt46
929 lines21.4 KB🛠️ Three.js (3D graphics)
▶ Play

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Mystery 3D - Expanded</title>
<style>
* { box-sizing: border-box; }
body {
  margin: 0;
  overflow: hidden;
  background: #080d1c;
  color: white;
  font-family: Arial, sans-serif;
}
canvas { display: block; }

#hud {
  position: absolute;
  top: 12px;
  left: 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  z-index: 2;
}
.stat {
  background: #101b30ee;
  border: 2px solid #38d9ff;
  padding: 9px 12px;
  border-radius: 10px;
  font-weight: bold;
}
#taskBox {
  position: absolute;
  top: 65px;
  left: 12px;
  width: 220px;
  background: #101b30ee;
  border: 2px solid #38d9ff;
  padding: 12px;
  border-radius: 10px;
  z-index: 2;
  font-size: 14px;
  line-height: 1.7;
}
#prompt {
  position: absolute;
  bottom: 100px;
  left: 50%;
  transform: translateX(-50%);
  background: #101b30ee;
  border: 2px solid #ffe36b;
  border-radius: 10px;
  padding: 10px 16px;
  font-weight: bold;
  z-index: 3;
  display: none;
  text-align: center;
}
#controls {
  position: absolute;
  bottom: 12px;
  left: 12px;
  background: #101b30cc;
  border-radius: 10px;
  padding: 10px;
  font-size: 12px;
  z-index: 2;
  line-height: 1.5;
}
#mobile {
  position: absolute;
  right: 12px;
  bottom: 12px;
  z-index: 3;
  display: grid;
  grid-template-columns: repeat(3, 46px);
  grid-template-rows: repeat(2, 46px);
  gap: 4px;
}
.move {
  background: #142b48ee;
  color: white;
  border: 2px solid #38d9ff;
  border-radius: 10px;
  padding: 0;
  margin: 0;
  font-size: 20px;
  touch-action: none;
}
#up { grid-column: 2; grid-row: 1; }
#left { grid-column: 1; grid-row: 2; }
#down { grid-column: 2; grid-row: 2; }
#right { grid-column: 3; grid-row: 2; }

#actions {
  position: absolute;
  right: 12px;
  bottom: 120px;
  display: flex;
  flex-direction: column;
  z-index: 3;
}
.action {
  background: #b52d42;
  color: white;
  border: 2px solid white;
  border-radius: 50%;
  width: 66px;
  height: 66px;
  font-size: 12px;
  margin: 5px;
  padding: 3px;
}
#overlay {
  position: absolute;
  inset: 0;
  background: #050a17dd;
  z-index: 5;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 12px;
}
.panel {
  background: #14233b;
  border: 3px solid #38d9ff;
  border-radius: 20px;
  padding: 22px;
  width: min(94%, 470px);
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 0 30px #00cfff44;
}
h1 { color: #38d9ff; margin: 0 0 12px; }
button {
  background: #38d9ff;
  border: 0;
  border-radius: 10px;
  padding: 12px 18px;
  margin: 6px;
  color: #071321;
  font-weight: bold;
  font-size: 15px;
  cursor: pointer;
}
button:hover { background: white; }
.voteButton {
  display: block;
  width: 100%;
  margin: 7px 0;
  color: white;
  background: #263b5d;
  border: 2px solid #38d9ff;
}
.small {
  color: #c8d7ea;
  font-size: 13px;
  line-height: 1.6;
}
</style>
</head>
<body>

<div id="hud">
  <div class="stat">🚀 SPACE MYSTERY</div>
  <div class="stat">Tasks: <span id="taskCount">0/4</span></div>
  <div class="stat">Crew: <span id="crewCount">9</span></div>
</div>

<div id="taskBox">
  <b>YOUR MISSIONS</b>
  <div id="taskList"></div>
  <hr style="border-color:#40516c">
  <span id="status">Explore the spaceship!</span>
</div>

<div id="prompt"></div>

<div id="controls">
  WASD / ARROWS = Move<br>
  E = Complete task<br>
  R = Report nearby incident<br>
  V = Emergency meeting
</div>

<div id="actions">
  <button class="action" id="reportBtn">REPORT</button>
  <button class="action" id="meetingBtn" style="background:#d28c27">MEETING</button>
</div>

<div id="mobile">
  <button class="move" id="up">▲</button>
  <button class="move" id="left">◀</button>
  <button class="move" id="down">▼</button>
  <button class="move" id="right">▶</button>
</div>

<div id="overlay">
  <div class="panel" id="panel">
    <h1>SPACE MYSTERY 3D</h1>
    <p>Explore a huge spaceship with 8 other crewmates!</p>
    <p class="small">
      Some crewmates are secretly impostors. They may eliminate other crew
      members and try to avoid being discovered.
      Find clues, complete your tasks and vote out the impostors!
    </p>
    <p class="small">
      WASD = Move · E = Task · R = Report · V = Meeting
    </p>
    <button id="start">START MISSION</button>
  </div>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"
  }
}
</script>

<script type="module">
import * as THREE from "three";

// ---------------- SCENE ----------------
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x080f24);
scene.fog = new THREE.Fog(0x080f24, 40, 95);

const camera = new THREE.PerspectiveCamera(
  55, innerWidth / innerHeight, 0.1, 150
);
camera.position.set(0, 28, 30);
camera.lookAt(0, 0, 0);

const 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);

scene.add(new THREE.HemisphereLight(0x9bbfff, 0x29304b, 2));

const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(-12, 25, 15);
light.castShadow = true;
scene.add(light);

// ---------------- MATERIALS ----------------
const mat = (color, emissive = 0x000000) =>
  new THREE.MeshStandardMaterial({
    color, emissive, roughness: 0.65
  });

const floorMat = mat(0x26354d);
const wallMat = mat(0x182943);
const cyanMat = mat(0x38d9ff, 0x075b77);
const screenMat = mat(0x0b203b, 0x063b65);

// ---------------- BIG MAP ----------------
const MAP_W = 50;
const MAP_H = 38;

const floor = new THREE.Mesh(
  new THREE.BoxGeometry(MAP_W, 0.5, MAP_H),
  floorMat
);
floor.position.y = -0.35;
floor.receiveShadow = true;
scene.add(floor);

const grid = new THREE.GridHelper(50, 25, 0x42658c, 0x344b6b);
grid.position.y = -0.08;
grid.scale.z = 0.76;
scene.add(grid);

function wall(x, z, width, depth) {
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(width, 3.5, depth),
    wallMat
  );
  mesh.position.set(x, 1.3, z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  scene.add(mesh);
}

// Outer walls
wall(0, -19, 50, 0.8);
wall(0, 19, 50, 0.8);
wall(-25, 0, 0.8, 38);
wall(25, 0, 0.8, 38);

// Interior walls create different spaceship rooms.
// Gaps are left for doorways.
wall(-12, -10, 0.7, 14);
wall(-12, 9, 0.7, 16);
wall(12, -10, 0.7, 14);
wall(12, 9, 0.7, 16);
wall(0, -7, 18, 0.7);
wall(0, 8, 18, 0.7);
wall(-19, 0, 0.7, 15);
wall(19, 0, 0.7, 15);

// Glowing wall strips
for (const z of [-18.5, 18.5]) {
  const strip = new THREE.Mesh(
    new THREE.BoxGeometry(48, 0.12, 0.12),
    cyanMat
  );
  strip.position.set(0, 1.3, z);
  scene.add(strip);
}

// Room decorations
function makeCrate(x, z) {
  const crate = new THREE.Mesh(
    new THREE.BoxGeometry(1.7, 1.5, 1.7),
    mat(0x455b78)
  );
  crate.position.set(x, 0.45, z);
  crate.castShadow = true;
  scene.add(crate);
}
[
  [-21,-15], [21,-15], [-21,15], [21,15],
  [-6,-15], [7,-15], [-6,15], [7,15],
  [-17,4], [17,-4]
].forEach(p => makeCrate(p[0], p[1]));

// ---------------- CREWMATE ----------------
function makeCrew(color, x, z) {
  const group = new THREE.Group();
  const suit = mat(color);

  const body = new THREE.Mesh(
    new THREE.CylinderGeometry(0.55, 0.62, 1.25, 20),
    suit
  );
  body.position.y = 0.9;
  body.castShadow = true;
  group.add(body);

  const helmet = new THREE.Mesh(
    new THREE.SphereGeometry(0.57, 24, 18),
    suit
  );
  helmet.position.y = 1.55;
  helmet.scale.set(1, 0.9, 0.9);
  helmet.castShadow = true;
  group.add(helmet);

  const visor = new THREE.Mesh(
    new THREE.SphereGeometry(0.35, 20, 14),
    new THREE.MeshStandardMaterial({
      color: 0x8ceaff,
      metalness: 0.7,
      roughness: 0.18
    })
  );
  visor.position.set(0, 1.62, 0.46);
  visor.scale.set(1.25, 0.65, 0.5);
  group.add(visor);

  const pack = new THREE.Mesh(
    new THREE.BoxGeometry(0.65, 0.8, 0.35),
    suit
  );
  pack.position.set(0, 0.9, -0.58);
  group.add(pack);

  group.position.set(x, 0, z);
  scene.add(group);
  return group;
}

// ---------------- PLAYER ----------------
const player = makeCrew(0x21d5e9, 0, 0);
let playerX = 0;
let playerZ = 0;

// ---------------- BOTS ----------------
const colors = [
  0xff5757, 0x9b70ff, 0xffc83d, 0x24cf77,
  0xff8c42, 0xff70bb, 0x9aa9bd, 0x7cdeea
];
const names = [
  "RED", "PURPLE", "YELLOW", "GREEN",
  "ORANGE", "PINK", "WHITE", "CYAN"
];

let bots = [];
let impostors = [];
let evidence = [];
let tasks = [];
let consoles = [];

function createBots() {
  bots = [];
  impostors = [];

  // Two randomly selected impostors.
  const shuffled = [...Array(8).keys()].sort(() => Math.random() - 0.5);
  const impSet = new Set(shuffled.slice(0, 2));

  const positions = [
    [-18,-12], [-6,-12], [6,-12], [18,-12],
    [-18,12], [-6,12], [6,12], [18,12]
  ];

  for (let i = 0; i < 8; i++) {
    const [x,z] = positions[i];
    const bot = {
      mesh: makeCrew(colors[i], x, z),
      name: names[i],
      color: colors[i],
      x, z,
      targetX: x,
      targetZ: z,
      timer: Math.random() * 3,
      alive: true,
      isImpostor: impSet.has(i),
      cooldown: 3 + Math.random() * 5,
      phase: Math.random() * 5
    };

    bots.push(bot);
    if (bot.isImpostor) impostors.push(bot);
  }
}

// ---------------- TASKS ----------------
function createTasks() {
  tasks = [
    { name: "Communications", x: -20, z: 0, done: false },
    { name: "Download data", x: 19, z: -12, done: false },
    { name: "Engine calibration", x: 0, z: 14, done: false },
    { name: "Repair navigation", x: 0, z: -14, done: false }
  ];

  consoles = [];

  tasks.forEach(task => {
    const group = new THREE.Group();

    const stand = new THREE.Mesh(
      new THREE.BoxGeometry(1.6, 1.1, 1),
      mat(0x40536c)
    );
    stand.position.y = 0.55;
    group.add(stand);

    const screen = new THREE.Mesh(
      new THREE.BoxGeometry(1.4, 0.9, 0.12),
      screenMat
    );
    screen.position.set(0, 1.5, 0.45);
    group.add(screen);

    group.position.set(task.x, 0, task.z);
    scene.add(group);

    const marker = new THREE.Mesh(
      new THREE.TorusGeometry(0.8, 0.06, 8, 28),
      mat(0xffe16b, 0x805000)
    );
    marker.rotation.x = Math.PI / 2;
    marker.position.set(task.x, 0.05, task.z);
    scene.add(marker);

    consoles.push({ task, marker, group });
  });
}

// ---------------- ELIMINATION MARKERS ----------------
// These are harmless hologram beacons, not graphic character models.
function makeEvidence(bot) {
  const group = new THREE.Group();

  const ring = new THREE.Mesh(
    new THREE.TorusGeometry(0.7, 0.12, 8, 24),
    mat(bot.color, bot.color)
  );
  ring.rotation.x = Math.PI / 2;
  ring.position.y = 0.15;
  group.add(ring);

  const beam = new THREE.Mesh(
    new THREE.CylinderGeometry(0.08, 0.08, 1.1, 12),
    new THREE.MeshBasicMaterial({
      color: bot.color,
      transparent: true,
      opacity: 0.6
    })
  );
  beam.position.y = 0.7;
  group.add(beam);

  group.position.set(bot.x, 0, bot.z);
  scene.add(group);

  evidence.push({
    mesh: group,
    x: bot.x,
    z: bot.z,
    reported: false,
    victim: bot.name
  });
}

function eliminateBot(bot) {
  if (!bot.alive) return;

  bot.alive = false;
  scene.remove(bot.mesh);
  makeEvidence(bot);
  updateCrewCount();
}

function updateCrewCount() {
  const alive = bots.filter(b => b.alive).length + 1;
  document.getElementById("crewCount").textContent = alive;
}

// ---------------- TASK UI ----------------
let tasksDone = 0;

function updateTasks() {
  tasksDone = tasks.filter(t => t.done).length;
  document.getElementById("taskCount").textContent =
    tasksDone + "/4";

  document.getElementById("taskList").innerHTML =
    tasks.map(t => (t.done ? "✅ " : "☐ ") + t.name).join("<br>");

  document.getElementById("status").textContent =
    tasksDone === 4
      ? "All tasks complete! Find the impostors."
      : "Explore the ship and finish your tasks.";
}

let nearestTask = null;
let nearestEvidence = null;
const prompt = document.getElementById("prompt");

function checkNearby() {
  nearestTask = null;
  nearestEvidence = null;
  let closestTask = 2.8;
  let closestEvidence = 2.8;

  for (const task of tasks) {
    if (task.done) continue;
    const d = Math.hypot(playerX - task.x, playerZ - task.z);
    if (d < closestTask) {
      closestTask = d;
      nearestTask = task;
    }
  }

  for (const item of evidence) {
    if (item.reported) continue;
    const d = Math.hypot(playerX - item.x, playerZ - item.z);
    if (d < closestEvidence) {
      closestEvidence = d;
      nearestEvidence = item;
    }
  }

  if (nearestEvidence) {
    prompt.textContent = "Press R to report " + nearestEvidence.victim;
    prompt.style.display = "block";
  } else if (nearestTask) {
    prompt.textContent = "Press E to use " + nearestTask.name;
    prompt.style.display = "block";
  } else {
    prompt.style.display = "none";
  }
}

function interact() {
  if (!playing || !nearestTask) return;
  nearestTask.done = true;

  const c = consoles.find(c => c.task === nearestTask);
  if (c) c.marker.material = mat(0x26ff9a, 0x087c43);

  updateTasks();
  checkNearby();
}

function reportEvidence(item) {
  if (!playing || !item || item.reported) return;
  item.reported = true;
  showMeeting("You reported " + item.victim + "!");
}

// ---------------- BOT AI ----------------
let playing = false;
let meetingOpen = false;
let gameFinished = false;
let meetingTimer = 0;
let meetingMessage = "";

function moveBots(dt, time) {
  for (const bot of bots) {
    if (!bot.alive) continue;

    bot.timer -= dt;
    bot.cooldown -= dt;

    if (bot.timer <= 0) {
      bot.timer = 2 + Math.random() * 3;

      // Bots wander around the larger ship.
      bot.targetX = -21 + Math.random() * 42;
      bot.targetZ = -15 + Math.random() * 30;
    }

    // Impostors sometimes approach a nearby crewmate.
    if (bot.isImpostor && bot.cooldown <= 0) {
      const targets = bots.filter(b =>
        b.alive && !b.isImpostor && b !== bot &&
        Math.hypot(b.x - bot.x, b.z - bot.z) < 8
      );

      if (targets.length && Math.random() < 0.75) {
        const victim = targets[Math.floor(Math.random() * targets.length)];

        // A short, non-graphic sci-fi tagging animation.
        bot.targetX = victim.x;
        bot.targetZ = victim.z;
        bot.cooldown = 8 + Math.random() * 8;

        setTimeout(() => {
          if (!playing || !bot.alive || !victim.alive) return;

          const d = Math.hypot(bot.x - victim.x, bot.z - victim.z);
          if (d < 2.2) {
            eliminateBot(victim);
          }
        }, 1200);
      } else {
        bot.cooldown = 5;
      }
    }

    const dx = bot.targetX - bot.x;
    const dz = bot.targetZ - bot.z;
    const length = Math.hypot(dx, dz);

    if (length > 0.2) {
      const pace = bot.isImpostor ? 2.2 : 1.5;
      bot.x += dx / length * pace * dt;
      bot.z += dz / length * pace * dt;
    }

    bot.x = THREE.MathUtils.clamp(bot.x, -23, 23);
    bot.z = THREE.MathUtils.clamp(bot.z, -17, 17);

    bot.mesh.position.set(bot.x, 0, bot.z);
    bot.mesh.rotation.y = Math.sin(time + bot.phase) * 0.12;
  }
}

// ---------------- AUTOMATIC REPORTING ----------------
function botReporting(dt) {
  for (const item of evidence) {
    if (item.reported) continue;

    for (const bot of bots) {
      if (!bot.alive || bot.isImpostor) continue;

      const d = Math.hypot(bot.x - item.x, bot.z - item.z);

      if (d < 1.7 && Math.random() < dt * 0.25) {
        item.reported = true;
        showMeeting(bot.name + " reported " + item.victim + "!");
        return;
      }
    }
  }
}

// ---------------- MEETINGS AND VOTING ----------------
function showMeeting(message) {
  if (!playing || meetingOpen) return;

  playing = false;
  meetingOpen = true;
  prompt.style.display = "none";
  meetingMessage = message;

  const panel = document.getElementById("panel");
  const overlay = document.getElementById("overlay");
  overlay.style.display = "flex";

  panel.innerHTML = `
    <h1>🚨 EMERGENCY MEETING</h1>
    <p>${message}</p>
    <p class="small">Who do you think is an impostor?</p>
    <div id="votes"></div>
  `;

  const votes = document.getElementById("votes");

  bots.filter(b => b.alive).forEach(bot => {
    const button = document.createElement("button");
    button.className = "voteButton";
    button.textContent = "Vote for " + bot.name;
    button.onclick = () => castVote(bot);
    votes.appendChild(button);
  });

  const skip = document.createElement("button");
  skip.className = "voteButton";
  skip.textContent = "Skip vote";
  skip.onclick = () => resumeGame("You skipped the vote.");
  votes.appendChild(skip);
}

function castVote(bot) {
  const caught = bot.isImpostor;

  if (caught) {
    impostors = impostors.filter(i => i !== bot);
    bot.alive = false;
    scene.remove(bot.mesh);
    updateCrewCount();

    if (impostors.length === 0) {
      finishGame(true, "You found every impostor! The spaceship is safe!");
      return;
    }

    resumeGame("Correct! " + bot.name + " was an impostor. One remains.");
  } else {
    resumeGame("Wrong vote! " + bot.name + " was not an impostor.");
  }
}

function resumeGame(message) {
  meetingOpen = false;
  playing = true;
  document.getElementById("overlay").style.display = "none";
  document.getElementById("status").textContent = message;
}

function finishGame(won, message) {
  playing = false;
  gameFinished = true;
  meetingOpen = false;
  prompt.style.display = "none";

  const panel = document.getElementById("panel");
  document.getElementById("overlay").style.display = "flex";

  panel.innerHTML = `
    <h1 style="color:${won ? "#5dffad" : "#ff7373"}">
      ${won ? "CREW VICTORY!" : "MISSION OVER!"}
    </h1>
    <p>${message}</p>
    <p class="small">Tasks completed: ${tasksDone}/4</p>
    <button id="again">PLAY AGAIN</button>
  `;
  document.getElementById("again").onclick = startGame;
}

function callMeeting() {
  if (!playing) return;
  showMeeting("You called an emergency meeting!");
}

// ---------------- PLAYER CONTROLS ----------------
const keys = {};

document.addEventListener("keydown", e => {
  const k = e.key.toLowerCase();
  keys[k] = true;

  if (k === "e") interact();
  if (k === "r" && nearestEvidence) reportEvidence(nearestEvidence);
  if (k === "v") callMeeting();

  if (["arrowup","arrowdown","arrowleft","arrowright"].includes(k)) {
    e.preventDefault();
  }
});

document.addEventListener("keyup", e => {
  keys[e.key.toLowerCase()] = false;
});

function bindMove(id, key) {
  const button = document.getElementById(id);
  button.addEventListener("pointerdown", e => {
    e.preventDefault();
    keys[key] = true;
  });
  for (const event of ["pointerup", "pointerleave", "pointercancel"]) {
    button.addEventListener(event, () => keys[key] = false);
  }
}
bindMove("up", "w");
bindMove("down", "s");
bindMove("left", "a");
bindMove("right", "d");

document.getElementById("reportBtn").onclick = () => {
  if (nearestEvidence) reportEvidence(nearestEvidence);
};
document.getElementById("meetingBtn").onclick = callMeeting;

// ---------------- START / RESET ----------------
function startGame() {
  // Remove old evidence and markers.
  evidence.forEach(item => scene.remove(item.mesh));
  evidence = [];

  // Remove old bots.
  bots.forEach(bot => scene.remove(bot.mesh));

  // Reset tasks and characters.
  createBots();
  createTasks();

  playerX = 0;
  playerZ = 0;
  player.position.set(0, 0, 0);

  tasksDone = 0;
  meetingOpen = false;
  gameFinished = false;
  playing = true;
  nearestTask = null;
  nearestEvidence = null;

  updateTasks();
  updateCrewCount();

  document.getElementById("overlay").style.display = "none";
  prompt.style.display = "none";
}

document.getElementById("start").onclick = startGame;

// ---------------- MAIN LOOP ----------------
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);

  const dt = Math.min(clock.getDelta(), 0.04);
  const time = clock.elapsedTime;

  if (playing) {
    let dx = 0;
    let dz = 0;

    if (keys["w"] || keys["arrowup"]) dz -= 1;
    if (keys["s"] || keys["arrowdown"]) dz += 1;
    if (keys["a"] || keys["arrowleft"]) dx -= 1;
    if (keys["d"] || keys["arrowright"]) dx += 1;

    const length = Math.hypot(dx, dz);
    if (length > 0) {
      dx /= length;
      dz /= length;
    }

    const moveSpeed = 7;

    playerX += dx * moveSpeed * dt;
    playerZ += dz * moveSpeed * dt;

    playerX = THREE.MathUtils.clamp(playerX, -23, 23);
    playerZ = THREE.MathUtils.clamp(playerZ, -17, 17);

    player.position.set(playerX, 0, playerZ);

    if (length > 0) {
      player.rotation.y = Math.atan2(dx, dz);
    }

    moveBots(dt, time);
    botReporting(dt);
    checkNearby();
  }

  // Smooth camera follow.
  camera.position.x += (playerX * 0.55 - camera.position.x) *
    Math.min(1, dt * 3);
  camera.position.z += (playerZ * 0.55 + 30 - camera.position.z) *
    Math.min(1, dt * 3);
  camera.lookAt(playerX * 0.45, 0, playerZ * 0.35);

  // Pulse task markers.
  for (const item of consoles) {
    if (!item.task.done) {
      const pulse = 1 + Math.sin(time * 3) * 0.08;
      item.marker.scale.set(pulse, pulse, pulse);
    }
  }

  // Pulse evidence beacons.
  for (const item of evidence) {
    item.mesh.rotation.y += dt;
    const pulse = 1 + Math.sin(time * 4) * 0.12;
    item.mesh.scale.set(pulse, pulse, pulse);
  }

  renderer.render(scene, camera);
}

animate();

// ---------------- RESIZE ----------------
window.addEventListener("resize", () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

Game Source: Space Mystery 3D - Expanded

Creator: AtomicBolt46

Libraries: three

Complexity: complex (929 lines, 21.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: space-mystery-3d-expanded-atomicbolt46" to link back to the original. Then publish at arcadelab.ai/publish.