🎮ArcadeLab

Super Bear Adventure 3D

by NovaNinja74
712 lines24.4 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#1a1a2e">
<title>Super Bear Adventure 3D</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent;
      user-select:none; -webkit-user-select:none; }
  html, body { width:100%; height:100%; overflow:hidden; background:#1a1a2e;
    font-family:-apple-system,'Arial',sans-serif; touch-action:none;
    position:fixed; overscroll-behavior:none; }
  canvas { display:block; touch-action:none; }

  /* HUD */
  #hud {
    position:fixed; top:calc(8px + env(safe-area-inset-top,0px)); left:10px;
    color:#fff; font-size:14px; font-weight:bold; text-shadow:2px 2px 4px #000;
    pointer-events:none; z-index:100; background:rgba(0,0,0,0.35);
    padding:6px 12px; border-radius:12px;
    backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);
  }
  #hud div { margin-bottom:3px; line-height:1.3; }

  #zoneName {
    position:fixed; top:45%; left:50%; transform:translate(-50%,-50%);
    color:#fff; font-size:28px; font-weight:bold; text-shadow:3px 3px 8px #000;
    pointer-events:none; opacity:0; transition:opacity 0.5s; z-index:200;
    text-align:center; padding:0 20px;
  }

  /* Джойстик */
  #joystick {
    position:fixed; bottom:calc(25px + env(safe-area-inset-bottom,0px)); left:20px;
    width:130px; height:130px; border-radius:50%;
    background:rgba(255,255,255,0.15);
    border:2px solid rgba(255,255,255,0.35);
    z-index:150; touch-action:none;
    backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
  }
  #stick {
    position:absolute; width:60px; height:60px; border-radius:50%;
    background:radial-gradient(circle at 30% 30%,#fff,#999);
    top:50%; left:50%; transform:translate(-50%,-50%);
    box-shadow:0 4px 12px rgba(0,0,0,0.5); pointer-events:none;
  }

  .btn {
    position:fixed; border-radius:50%; background:rgba(255,255,255,0.2);
    border:2px solid rgba(255,255,255,0.5); color:#fff;
    display:flex; justify-content:center; align-items:center;
    z-index:150; touch-action:none;
    backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
    font-size:30px; transition:transform 0.1s, background 0.1s;
  }
  .btn.pressed { transform:scale(0.9); background:rgba(255,255,255,0.45); }
  #jumpBtn {
    bottom:calc(115px + env(safe-area-inset-bottom,0px)); right:20px;
    width:85px; height:85px;
    background:rgba(100,200,100,0.35); border-color:rgba(100,255,100,0.6);
  }
  #attackBtn {
    bottom:calc(25px + env(safe-area-inset-bottom,0px)); right:20px;
    width:85px; height:85px;
    background:rgba(200,100,100,0.35); border-color:rgba(255,100,100,0.6);
  }

  #hint {
    position:fixed; bottom:calc(160px + env(safe-area-inset-bottom,0px));
    left:50%; transform:translateX(-50%);
    color:#fff; background:rgba(0,0,0,0.55); padding:6px 14px;
    border-radius:20px; font-size:12px; z-index:100; white-space:nowrap;
    opacity:1; transition:opacity 1s; pointer-events:none;
  }
  @media (max-height:450px) { #hint { display:none; } }

  /* Победа / старт */
  .overlay {
    position:fixed; inset:0; display:none;
    justify-content:center; align-items:center; flex-direction:column;
    z-index:500; text-align:center; padding:20px;
  }
  #startScreen {
    background:linear-gradient(180deg,#87CEEB,#4a90e2); display:flex;
  }
  #startScreen h1 { color:#fff; font-size:30px; text-shadow:3px 3px 6px #000; margin-bottom:10px; }
  #startScreen p { color:#fff; font-size:15px; text-shadow:1px 1px 3px #000; margin-bottom:25px; line-height:1.5; }
  #startScreen button, #win button {
    padding:18px 50px; font-size:22px; font-weight:bold;
    background:linear-gradient(135deg,#FFD700,#FFA500); color:#fff;
    border:none; border-radius:50px; cursor:pointer;
    box-shadow:0 8px 20px rgba(0,0,0,0.4);
    text-shadow:2px 2px 4px rgba(0,0,0,0.3);
  }
  #startScreen button:active, #win button:active { transform:scale(0.95); }

  #win { background:rgba(0,0,0,0.85); color:#FFD700; font-size:34px; font-weight:bold;
         text-shadow:3px 3px 6px #000; }
  #win small { color:#fff; font-size:16px; margin-top:15px; }
</style>
</head>
<body>

<div id="hud">
  <div>🍯 <span id="coins">0</span> / 125</div>
  <div>🐻 Спасено: <span id="bearsSaved">0</span> / 3</div>
  <div>📍 <span id="zoneLabel">Turtletown</span></div>
</div>

<div id="zoneName"></div>
<div id="hint">Джойстик — движение • 🦘 прыжок • ⚡ удар</div>

<div id="joystick"><div id="stick"></div></div>
<div class="btn" id="jumpBtn">🦘</div>
<div class="btn" id="attackBtn">⚡</div>

<div id="win" class="overlay">
  🐻 ПОБЕДА!
  <small>Королевство спасено от фиолетового мёда!</small>
  <button id="winBtn">🔄 Играть снова</button>
</div>

<div id="startScreen" class="overlay">
  <h1>🐻 Super Bear Adventure</h1>
  <p>Спаси 3 медведей — откроется босс!<br>Победи всех 6 боссов и спаси королевство.</p>
  <button id="startBtn">▶ ИГРАТЬ</button>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ============================================================
// SUPER BEAR ADVENTURE 3D — Mobile (full working version)
// ============================================================

const ZONES = [
  { name:'Turtletown',        boss:'Turtle Golem',         bg:0x87CEEB, ground:0x7cb342 },
  { name:'Snow Valley',       boss:'Yeti',                 bg:0xB0D4F1, ground:0xE8F4F8 },
  { name:'Beemothep Desert',  boss:'The Guardian',         bg:0xFFD59E, ground:0xE8C170 },
  { name:'Giant House',       boss:'Test Subject No.1706', bg:0x8B7355, ground:0xA0522D },
  { name:'The Hive',          boss:'Queen Beeatrice',      bg:0x4B0082, ground:0x8B4513 },
  { name:'Bear Village',      boss:"Baaren's Brother",     bg:0x87CEEB, ground:0x7cb342 }
];

let scene, camera, renderer, clock;
let player, playerMesh;
let platforms = [], enemies = [], coinList = [], bearList = [], boss = null, particles = [], trees = [];
let ground = null;

const joy = { x:0, y:0, active:false };
let jumpBtnDown = false, attackBtnDown = false, attackCooldown = 0;
let keys = {};

const state = {
  zone: 0, coins: 0, bearsSaved: 0, bearsTotal: 3,
  playing: false, won: false, bossActive: false,
  camAngle: 0, camDist: 11, camHeight: 5.5,
  bossHp: 3, bossHpMax: 3, transition: false
};

const $ = id => document.getElementById(id);

// ============================================================
// ИНИЦИАЛИЗАЦИЯ СЦЕНЫ
// ============================================================
function initScene() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(ZONES[0].bg);
  scene.fog = new THREE.Fog(ZONES[0].bg, 35, 110);

  camera = new THREE.PerspectiveCamera(65, innerWidth/innerHeight, 0.1, 400);
  camera.position.set(0, 6, 12);

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

  clock = new THREE.Clock();

  scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.9));
  const sun = new THREE.DirectionalLight(0xffffff, 1.0);
  sun.position.set(20, 40, 20);
  sun.castShadow = true;
  sun.shadow.mapSize.set(1024, 1024);
  sun.shadow.camera.left = -45;
  sun.shadow.camera.right = 45;
  sun.shadow.camera.top = 45;
  sun.shadow.camera.bottom = -45;
  scene.add(sun);

  // Стартовая земля — постоянная (не пересоздаётся)
  ground = new THREE.Mesh(
    new THREE.PlaneGeometry(200, 200),
    new THREE.MeshStandardMaterial({ color: ZONES[0].ground, roughness: 0.9 })
  );
  ground.rotation.x = -Math.PI / 2;
  ground.receiveShadow = true;
  scene.add(ground);

  createPlayer();
}

// ============================================================
// ИГРОК (Баарен)
// ============================================================
function createPlayer() {
  playerMesh = new THREE.Group();

  const body = new THREE.Mesh(
    new THREE.SphereGeometry(0.8, 14, 14),
    new THREE.MeshStandardMaterial({ color:0x8B4513, roughness:0.8 })
  );
  body.castShadow = true; body.position.y = 0.8;
  playerMesh.add(body);

  const head = new THREE.Mesh(
    new THREE.SphereGeometry(0.55, 14, 14),
    new THREE.MeshStandardMaterial({ color:0xA0522D })
  );
  head.castShadow = true; head.position.set(0, 1.7, 0.1);
  playerMesh.add(head);

  const earGeo = new THREE.SphereGeometry(0.2, 8, 8);
  const earMat = new THREE.MeshStandardMaterial({ color:0x6B3E1E });
  const earL = new THREE.Mesh(earGeo, earMat); earL.position.set(-0.35, 2.15, 0);
  const earR = new THREE.Mesh(earGeo, earMat); earR.position.set( 0.35, 2.15, 0);
  playerMesh.add(earL, earR);

  const eyeMat = new THREE.MeshStandardMaterial({ color:0x000000 });
  const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 8), eyeMat);
  eyeL.position.set(-0.2, 1.75, 0.5);
  const eyeR = new THREE.Mesh(new THREE.SphereGeometry(0.12, 8, 8), eyeMat);
  eyeR.position.set( 0.2, 1.75, 0.5);
  playerMesh.add(eyeL, eyeR);

  const nose = new THREE.Mesh(
    new THREE.SphereGeometry(0.1, 8, 8),
    new THREE.MeshStandardMaterial({ color:0x000000 })
  );
  nose.position.set(0, 1.6, 0.55);
  playerMesh.add(nose);

  const pawGeo = new THREE.SphereGeometry(0.25, 8, 8);
  const pawMat = new THREE.MeshStandardMaterial({ color:0x6B3E1E });
  const pawL = new THREE.Mesh(pawGeo, pawMat); pawL.position.set(-0.5, 0.3, 0.4);
  const pawR = new THREE.Mesh(pawGeo, pawMat); pawR.position.set( 0.5, 0.3, 0.4);
  playerMesh.add(pawL, pawR);

  scene.add(playerMesh);

  player = {
    mesh: playerMesh,
    vx:0, vy:0, vz:0,
    speed:7,
    jumpPower:11,
    gravity:26,
    grounded:false,
    canDouble:true,
    jumpCooldown:0
  };
}

// ============================================================
// УРОВЕНЬ
// ============================================================
function clearLevel() {
  platforms.forEach(p => scene.remove(p.mesh));
  enemies.forEach(e => scene.remove(e.mesh));
  coinList.forEach(c => scene.remove(c.mesh));
  bearList.forEach(b => scene.remove(b.mesh));
  trees.forEach(t => scene.remove(t));
  if (boss) { scene.remove(boss.mesh); boss = null; }
  platforms = []; enemies = []; coinList = []; bearList = []; trees = [];
  particles.forEach(p => scene.remove(p.mesh)); particles = [];
}

function buildLevel(idx) {
  clearLevel();
  state.zone = idx;
  state.coins = 0;
  state.bearsSaved = 0;
  state.bossActive = false;
  state.won = false;
  state.transition = false;
  state.bossHp = 3;

  const z = ZONES[idx];
  scene.background = new THREE.Color(z.bg);
  scene.fog = new THREE.Fog(z.bg, 35, 110);
  ground.material.color.setHex(z.ground);

  // Стартовая площадка
  addPlatform(0, -0.5, 0, 16, 1, 16, z.ground);

  // Платформы
  const layout = [
    [0, 0, -14, 6, 1, 6],
    [6, 2, -21, 5, 1, 5],
    [-2, 4, -29, 5, 1, 5],
    [8, 6, -37, 5, 1, 5],
    [0, 8, -46, 7, 1, 7],
    [-10, 10, -54, 5, 1, 5],
    [-4, 12, -62, 5, 1, 5],
    [10, 14, -72, 12, 1, 12]  // Арена босса
  ];
  layout.forEach(p => addPlatform(p[0], p[1], p[2], p[3], p[4], p[5], 0x9C6A3A));

  // Монеты — по платформам и в воздухе
  for (let i = 0; i < 125; i++) {
    const px = (Math.random() - 0.5) * 60;
    const pz = -10 - Math.random() * 65;
    const py = 1.5 + Math.random() * 13;
    addCoin(px, py, pz);
  }

  // Медведи (3 для спасения)
  const bearZ = [-18, -40, -60];
  for (let i = 0; i < 3; i++) {
    addBear((Math.random() - 0.5) * 40, 1, bearZ[i]);
  }

  // Враги
  for (let i = 0; i < 4; i++) {
    addEnemy((Math.random() - 0.5) * 40, 1, -20 - i * 14);
  }

  // Босс
  createBoss(z.boss, 10, 15.2, -72);

  // Деревья
  for (let i = 0; i < 12; i++) {
    const tx = (Math.random() - 0.5) * 70;
    const tz = -10 - Math.random() * 65;
    if (Math.abs(tx) > 8) addTree(tx, tz, z.name === 'Snow Valley');
  }

  // Респавн игрока
  player.mesh.position.set(0, 1.5, 0);
  player.vx = player.vy = player.vz = 0;
  player.grounded = false;
  player.canDouble = true;
  state.camAngle = 0;

  updateHUD();
  showZoneName(z.name);
}

// ============================================================
// ХЕЛПЕРЫ УРОВНЯ
// ============================================================
function addPlatform(x, y, z, w, h, d, color) {
  const mesh = new THREE.Mesh(
    new THREE.BoxGeometry(w, h, d),
    new THREE.MeshStandardMaterial({ color, roughness: 0.85 })
  );
  mesh.position.set(x, y, z);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  scene.add(mesh);
  platforms.push({ mesh, x, y, z, w, h, d });
}

function addCoin(x, y, z) {
  const mesh = new THREE.Mesh(
    new THREE.SphereGeometry(0.32, 8, 8),
    new THREE.MeshStandardMaterial({ color:0xFFD700, emissive:0xFFD700, emissiveIntensity:0.5 })
  );
  mesh.position.set(x, y, z);
  mesh.castShadow = true;
  scene.add(mesh);
  coinList.push({ mesh, taken:false, baseY:y, bob: Math.random() * 6.28 });
}

function addBear(x, y, z) {
  const g = new THREE.Group();

  const body = new THREE.Mesh(
    new THREE.BoxGeometry(1.2, 1.4, 1.2),
    new THREE.MeshStandardMaterial({ color:0x8B4513 })
  );
  body.position.y = 0.7; body.castShadow = true;
  g.add(body);

  const head = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 8, 8),
    new THREE.MeshStandardMaterial({ color:0xA0522D })
  );
  head.position.y = 1.7; head.castShadow = true;
  g.add(head);

  // Клетка — прутья
  const barMat = new THREE.MeshStandardMaterial({ color:0x654321, metalness:0.6 });
  for (let i = -1; i <= 1; i++) {
    const bar = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.5, 0.1), barMat);
    bar.position.set(i * 0.7, 1.25, 0.7);
    g.add(bar);
  }

  g.position.set(x, y, z);
  scene.add(g);
  bearList.push({ mesh: g, saved:false, bob: Math.random() * 6.28 });
}

function addEnemy(x, y, z) {
  const mesh = new THREE.Mesh(
    new THREE.SphereGeometry(0.6, 10, 10),
    new THREE.MeshStandardMaterial({ color:0x8B008B, emissive:0x4B0055, emissiveIntensity:0.5 })
  );
  mesh.position.set(x, y + 0.6, z);
  mesh.castShadow = true;
  scene.add(mesh);
  enemies.push({ mesh, baseX:x, range:4, speed:1.6, dir:1 });
}

function addTree(x, z, snowy) {
  const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.3, 0.4, 3, 6),
    new THREE.MeshStandardMaterial({ color:0x5D4037 })
  );
  trunk.position.set(x, 1.5, z);
  trunk.castShadow = true;
  scene.add(trunk);

  const leaves = new THREE.Mesh(
    new THREE.SphereGeometry(1.5, 8, 8),
    new THREE.MeshStandardMaterial({ color: snowy ? 0xFFFFFF : 0x2E7D32 })
  );
  leaves.position.set(x, 4, z);
  leaves.castShadow = true;
  scene.add(leaves);

  trees.push(trunk, leaves);
}

function createBoss(name, x, y, z) {
  const g = new THREE.Group();

  const body = new THREE.Mesh(
    new THREE.BoxGeometry(4, 4, 4),
    new THREE.MeshStandardMaterial({ color:0x800080, emissive:0x400040, emissiveIntensity:0.3 })
  );
  body.position.y = 2; body.castShadow = true;
  g.add(body);

  const eyeMat = new THREE.MeshStandardMaterial({ color:0xFF0000, emissive:0xFF0000, emissiveIntensity:1 });
  const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 8), eyeMat);
  eyeL.position.set(-0.8, 3, 2);
  const eyeR = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 8), eyeMat);
  eyeR.position.set( 0.8, 3, 2);
  g.add(eyeL, eyeR);

  g.position.set(x, y, z);
  g.visible = false;
  scene.add(g);
  boss = { mesh: g, name, active:false, baseY: y };
}

// ============================================================
// UI
// ============================================================
function updateHUD() {
  $('coins').textContent = state.coins;
  $('bearsSaved').textContent = state.bearsSaved;
  $('zoneLabel').textContent = ZONES[state.zone].name;
}

function showZoneName(name) {
  const el = $('zoneName');
  el.textContent = name;
  el.style.opacity = '1';
  clearTimeout(showZoneName._t);
  showZoneName._t = setTimeout(() => { el.style.opacity = '0'; }, 1800);
}

function showBossName(name) {
  const el = $('zoneName');
  el.innerHTML = '⚔️ БОСС<br><span style="font-size:22px">' + name + '</span>';
  el.style.opacity = '1';
  clearTimeout(showZoneName._t);
  showZoneName._t = setTimeout(() => { el.style.opacity = '0'; }, 2200);
}

// ============================================================
// УПРАВЛЕНИЕ (мульти-тач)
// ============================================================
function setupControls() {
  // ----- Кнопка старт -----
  $('startBtn').addEventListener('click', () => {
    $('startScreen').style.display = 'none';
    state.playing = true;
    clock.start();
  });

  $('winBtn').addEventListener('click', () => {
    $('win').style.display = 'none';
    buildLevel(0);
    state.playing = true;
  });

  // ----- ДЖОЙСТИК (мульти-тач) -----
  const joyEl = $('joystick');
  const stickEl = $('stick');
  const JOY_MAX = 60;
  let joyTouchId = null;

  function setStick(dx, dy) {
    const dist = Math.min(Math.hypot(dx, dy), JOY_MAX);
    const ang = Math.atan2(dy, dx);
    const px = Math.cos(ang) * dist;
    const py = Math.sin(ang) * dist;
    stickEl.style.transform = `translate(calc(-50% + ${px}px), calc(-50% + ${py}px))`;
    joy.x = px / JOY_MAX;
    joy.y = py / JOY_MAX;
  }

  function resetStick() {
    stickEl.style.transform = 'translate(-50%, -50%)';
    joy.x = joy.y = 0;
    joyTouchId = null;
  }

  joyEl.addEventListener('touchstart', e => {
    e.preventDefault();
    const t = e.changedTouches[0];
    joyTouchId = t.identifier;
    const r = joyEl.getBoundingClientRect();
    const cx = r.left + r.width / 2;
    const cy = r.top + r.height / 2;
    setStick(t.clientX - cx, t.clientY - cy);
  }, { passive:false });

  joyEl.addEventListener('touchmove', e => {
    e.preventDefault();
    if (joyTouchId === null) return;
    for (const t of e.changedTouches) {
      if (t.identifier !== joyTouchId) continue;
      const r = joyEl.getBoundingClientRect();
      const cx = r.left + r.width / 2;
      const cy = r.top + r.height / 2;
      setStick(t.clientX - cx, t.clientY - cy);
    }
  }, { passive:false });

  joyEl.addEventListener('touchend', e => {
    for (const t of e.changedTouches) {
      if (t.identifier === joyTouchId) resetStick();
    }
  }, { passive:false });
  joyEl.addEventListener('touchcancel', resetStick, { passive:false });

  // Мышь для теста на ПК
  let joyMouse = false;
  joyEl.addEventListener('mousedown', e => {
    joyMouse = true;
    const r = joyEl.getBoundingClientRect();
    setStick(e.clientX - (r.left + r.width/2), e.clientY - (r.top + r.height/2));
  });
  document.addEventListener('mousemove', e => {
    if (!joyMouse) return;
    const r = joyEl.getBoundingClientRect();
    setStick(e.clientX - (r.left + r.width/2), e.clientY - (r.top + r.height/2));
  });
  document.addEventListener('mouseup', () => { if (joyMouse) { joyMouse = false; resetStick(); } });

  // ----- КНОПКИ ПРЫЖКА И АТАКИ (мульти-тач) -----
  function bindBtn(el, setter) {
    let id = null;
    el.addEventListener('touchstart', e => {
      e.preventDefault();
      const t = e.changedTouches[0];
      id = t.identifier;
      setter(true);
      el.classList.add('pressed');
    }, { passive:false });
    el.addEventListener('touchend', e => {
      for (const t of e.changedTouches) {
        if (t.identifier === id) {
          setter(false);
          el.classList.remove('pressed');
          id = null;
        }
      }
    }, { passive:false });
    el.addEventListener('touchcancel', () => { setter(false); el.classList.remove('pressed'); id = null; }, { passive:false });
    // Мышь
    el.addEventListener('mousedown', e => { e.preventDefault(); setter(true); el.classList.add('pressed'); });
    el.addEventListener('mouseup',   e => { e.preventDefault(); setter(false); el.classList.remove('pressed'); });
    el.addEventListener('mouseleave',() => { setter(false); el.classList.remove('pressed'); });
  }

  bindBtn($('jumpBtn'),   v => { jumpBtnDown = v; });
  bindBtn($('attackBtn'), v => { attackBtnDown = v; });

  // ----- КАМЕРА СВАЙПОМ (по середине/правой части, кроме UI) -----
  let camTouchId = null, camLastX = 0;

  function isOverUI(x, y) {
    const els = [joyEl, $('jumpBtn'), $('attackBtn')];
    for (const el of els) {
      const r = el.getBoundingClientRect();
      if (x >= r.left - 15 && x <= r.right + 15 && y >= r.top - 15 && y <= r.bottom + 15) return true;
    }
    return false;
  }

  document.addEventListener('touchstart', e => {
    if (!state.playing) return;
    for (const t of e.changedTouches) {
      if (camTouchId !== null) continue;
      if (isOverUI(t.clientX, t.clientY)) continue;
      camTouchId = t.identifier;
      camLastX = t.clientX;
    }
  }, { passive:true });

  document.addEventListener('touchmove', e => {
    if (camTouchId === null) return;
    for (const t of e.changedTouches) {
      if (t.identifier !== camTouchId) continue;
      const dx = t.clientX - camLastX;
      state.camAngle -= dx * 0.008;
      camLastX = t.clientX;
    }
  }, { passive:true });

  document.addEventListener('touchend', e => {
    for (const t of e.changedTouches) {
      if (t.identifier === camTouchId) camTouchId = null;
    }
  }, { passive:true });

  // ----- КЛАВИАТУРА (для ПК) -----
  document.addEventListener('keydown', e => {
    keys[e.code] = true;
    if (e.code === 'Space') { jumpBtnDown = true; e.preventDefault(); }
    if (e.code === 'KeyE') attackBtnDown = true;
  });
  document.addEventListener('keyup', e => {
    keys[e.code] = false;
    if (e.code === 'Space') jumpBtnDown = false;
    if (e.code === 'KeyE') attackBtnDown = false;
  });

  // Колесо мыши — зум (ПК)
  document.addEventListener('wheel', e => {
    state.camDist = Math.max(7, Math.min(18, state.camDist + e.deltaY * 0.01));
  }, { passive:true });

  // Скрыть подсказку
  setTimeout(() => { $('hint').style.opacity = '0'; }, 6000);
}

// ============================================================
// ФИЗИКА И ЛОГИКА
// ============================================================
function updatePhysics(dt) {
  if (!state.playing || state.won || state.transition) return;

  // ----- Ввод -----
  const forward = new THREE.Vector3(Math.sin(state.camAngle), 0, Math.cos(state.camAngle));
  const right   = new THREE.Vector3(forward.z, 0, -forward.x);

  let mx = 0, mz = 0;
  if (Math.hypot(joy.x, joy.y) > 0.18) {
    mx = forward.x * (-joy.y) + right.x * joy.x;
    mz = forward.z * (-joy.y) + right.z * joy.x;
  }
  if (keys['KeyW'] || keys['ArrowUp'])    { mx += forward.x; mz += forward.z; }
  if (keys['KeyS'] || keys['ArrowDown'])  { mx -= forward.x; mz -= forward.z; }
  if (keys['KeyA'] || keys['ArrowLeft'])  { mx -= right.x;   mz -= right.z; }
  if (keys['KeyD'] || keys['ArrowRight']) { mx += right.x;   mz += right.z; }

  const len = Math.hypot(mx, mz);
  if (len > 0.01) {
    mx /= len; mz /= len;
    player.vx = mx * player.speed;
    player.vz = mz * player.speed;
    // Поворот морды в сторону движения
    const targetRot = Math.atan2(mx, mz);
    let diff = targetRot - player.mesh.rotation.y;
    while (diff > Math.PI) diff -= Math.PI * 2;
    while (diff < -Math.PI) diff += Math.PI * 2;
    player.mesh.rotation.y += diff * Math.min(1, dt * 12);
  } else {
    player.vx *= 0.75;
    player.vz *= 0.75;
  }

  // ----- Прыжок -----
  player.jumpCooldown = Math.max(0, player.jumpCooldown - dt);
  if (jumpBtnDown && player.jumpCooldown <= 0) {
    if (player.grounded) {
      player.vy = player.jumpPower;
      player.grounded = false;
      player.canDouble = true;
      player.jumpCooldown = 0.28;
      burst(player.mesh.position.x, player.mesh.position.y, player.mesh.position.z, 0xffffff, 6);
    } else if (player.canDouble) {
      player.vy = player.jumpPower * 0.85;
      player.canDouble = false;
      player.jumpCooldown = 0.28;
      burst(player.mesh.position.x, player.mesh.position.y, player.mesh.position.z, 0xffff00, 10);
    }
  }

  // ----- Атака (Belly Stomp) -----
  attackCooldown = Math.max(0, attack

Game Source: Super Bear Adventure 3D

Creator: NovaNinja74

Libraries: three

Complexity: complex (712 lines, 24.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: super-bear-adventure-3d-novaninja74" to link back to the original. Then publish at arcadelab.ai/publish.