🎮ArcadeLab

MC 仿制 · 真实贴图 + 设置界面

by ElectricShark88
693 lines30.5 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>MC 仿制 · 真实贴图 + 设置界面</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:#000;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;touch-action:none}
canvas{display:block}

/* 顶部菜单 */
#topBar{position:fixed;top:calc(6px + env(safe-area-inset-top));left:8px;right:8px;display:flex;justify-content:space-between;z-index:40;pointer-events:none}
#topLeft,#topRight{display:flex;gap:6px;pointer-events:auto}
.topBtn{background:rgba(0,0,0,.45);color:#fff;border:1px solid rgba(255,255,255,.25);border-radius:6px;padding:7px 11px;font-size:12px;text-shadow:0 1px 2px #000}
.topBtn:active{background:rgba(255,255,255,.22)}

/* 摇杆 */
#joyBase{position:fixed;left:calc(18px + env(safe-area-inset-left));bottom:calc(22px + env(safe-area-inset-bottom));width:126px;height:126px;border-radius:50%;background:rgba(255,255,255,.08);border:2px solid rgba(255,255,255,.25);z-index:30;pointer-events:auto;touch-action:none}
#joyKnob{position:absolute;left:50%;top:50%;width:52px;height:52px;border-radius:50%;background:rgba(255,255,255,.55);transform:translate(-50%,-50%);box-shadow:0 2px 8px rgba(0,0,0,.35)}

/* 右侧按钮 */
#rightBtns{position:fixed;right:calc(14px + env(safe-area-inset-right));bottom:calc(22px + env(safe-area-inset-bottom));width:166px;height:186px;z-index:30;pointer-events:none}
.tBtn{position:absolute;pointer-events:auto;border-radius:50%;background:rgba(255,255,255,.13);border:2px solid rgba(255,255,255,.4);color:#fff;font-weight:600;display:flex;align-items:center;justify-content:center;text-shadow:0 1px 4px rgba(0,0,0,.6);touch-action:none}
.tBtn:active{background:rgba(255,255,255,.4)}
#btnJump{right:0;bottom:58px;width:74px;height:74px;font-size:15px}
#btnSneak{right:86px;bottom:8px;width:56px;height:56px;font-size:12px}
#btnJump.fly{background:rgba(100,180,255,.3);border-color:#9cf}
#btnSneak.fly{background:rgba(100,180,255,.2);border-color:#9cf}

/* 生存 HUD */
#survivalHUD{position:fixed;left:50%;bottom:calc(76px + env(safe-area-inset-bottom));transform:translateX(-50%);z-index:20;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:3px}
#statsRow{display:flex;gap:12px;align-items:center}
.hearts,.hunger{display:flex;gap:1px;line-height:1}
.heart,.food{width:9px;height:9px;image-rendering:pixelated}
#xpBarOuter{width:200px;height:6px;background:rgba(0,0,0,.55);border-radius:3px;overflow:hidden;position:relative}
#xpBarInner{height:100%;width:0%;background:#7dff5a;transition:width .1s}
#xpLevel{position:absolute;left:50%;bottom:7px;transform:translateX(-50%);color:#7dff5a;font-size:10px;font-weight:700;text-shadow:0 1px 2px #000}
body.creative #survivalHUD{display:none}

/* 快捷栏 */
#hotbar{position:fixed;left:50%;bottom:calc(8px + env(safe-area-inset-bottom));transform:translateX(-50%);display:flex;gap:3px;z-index:35;pointer-events:auto;background:rgba(0,0,0,.28);padding:3px;border-radius:5px}
.slot{width:38px;height:38px;border-radius:3px;border:2px solid rgba(255,255,255,.16);display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.slot.active{border-color:#fff;box-shadow:0 0 0 1px #fff}
.slot img{width:32px;height:32px;image-rendering:pixelated}
.slot .count{position:absolute;right:1px;bottom:-1px;font-size:8px;color:#fff;text-shadow:0 1px 1px #000}

/* 准星 */
#cross{position:fixed;top:50%;left:50%;width:18px;height:18px;transform:translate(-50%,-50%);z-index:15;pointer-events:none;display:none}
body.split #cross{display:block}
#cross::before,#cross::after{content:'';position:absolute;background:rgba(255,255,255,.85);box-shadow:0 0 2px #000}
#cross::before{left:50%;top:0;width:2px;height:100%;transform:translateX(-50%)}
#cross::after{top:50%;left:0;height:2px;width:100%;transform:translateY(-50%)}

/* 设置面板 */
#settingsPanel{position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:100;display:none;align-items:center;justify-content:center;padding:16px}
#settingsPanel.open{display:flex}
#settingsBox{width:100%;max-width:520px;max-height:82vh;background:#2b2b2b;border:2px solid #555;border-radius:8px;display:flex;flex-direction:column;overflow:hidden;color:#ddd}
#settingsTitle{background:#1e1e1e;padding:12px 16px;font-size:15px;font-weight:600;border-bottom:1px solid #444;display:flex;justify-content:space-between;align-items:center}
#settingsClose{background:#444;border:none;color:#fff;border-radius:4px;padding:4px 10px;font-size:13px}
#settingsTabs{display:flex;background:#222;border-bottom:1px solid #444}
.stab{flex:1;padding:10px 4px;text-align:center;font-size:12px;color:#999;border:none;background:transparent;border-bottom:2px solid transparent}
.stab.active{color:#fff;border-bottom-color:#7dff5a;background:#2e2e2e}
#settingsBody{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:14px}
.setRow{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px}
.setRow label{color:#ccc;flex-shrink:0}
.setRow input[type=range]{flex:1;accent-color:#7dff5a;max-width:180px}
.setRow select,.setRow input[type=text]{background:#1a1a1a;border:1px solid #555;color:#fff;padding:6px 8px;border-radius:4px;font-size:12px;max-width:170px}
.setRow .val{color:#7dff5a;font-size:12px;min-width:32px;text-align:right}
.setGroup{font-size:11px;color:#777;text-transform:uppercase;letter-spacing:1px;margin-top:4px;border-bottom:1px solid #3a3a3a;padding-bottom:4px}
#hint{position:fixed;top:calc(48px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);color:#fff;background:rgba(0,0,0,.5);padding:5px 12px;border-radius:12px;font-size:11px;z-index:45;pointer-events:none;white-space:nowrap}
</style>
</head>
<body class="creative">

<canvas id="game"></canvas>

<div id="topBar">
  <div id="topLeft">
    <div class="topBtn" id="btnPause">‖ 暂停</div>
    <div class="topBtn" id="btnMode">创造</div>
  </div>
  <div id="topRight">
    <div class="topBtn" id="btnSettings">设置</div>
    <div class="topBtn" id="btnPersp">视角</div>
    <div class="topBtn" id="btnSplit">分离</div>
  </div>
</div>

<div id="survivalHUD">
  <div id="statsRow">
    <div class="hearts" id="hearts"></div>
    <div class="hunger" id="hunger"></div>
  </div>
  <div style="position:relative">
    <div id="xpBarOuter"><div id="xpBarInner"></div></div>
    <div id="xpLevel">0</div>
  </div>
</div>

<div id="cross"></div>
<div id="joyBase"><div id="joyKnob"></div></div>

<div id="rightBtns">
  <div class="tBtn" id="btnJump">跳跃</div>
  <div class="tBtn" id="btnSneak">潜行</div>
</div>

<div id="hotbar"></div>
<div id="hint">创造模式 · 双击跳跃飞行</div>

<!-- 设置面板 -->
<div id="settingsPanel">
  <div id="settingsBox">
    <div id="settingsTitle">
      <span>设置</span>
      <button id="settingsClose">关闭</button>
    </div>
    <div id="settingsTabs">
      <button class="stab active" data-tab="video">视频</button>
      <button class="stab" data-tab="audio">音频</button>
      <button class="stab" data-tab="control">控制</button>
      <button class="stab" data-tab="touch">触控</button>
    </div>
    <div id="settingsBody"></div>
  </div>
</div>

<script type="module">
import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';

/* ============================================================
   0. 贴图加载(ProgrammerArt CC0)
   ============================================================ */
const TEX_BASE = 'https://cdn.jsdelivr.net/npm/programmerart-textures@2.1.0/textures/';
const loader = new THREE.TextureLoader();

function loadTex(path){
  const t = loader.load(TEX_BASE + path + '.png');
  t.magFilter = THREE.NearestFilter;
  t.minFilter = THREE.NearestFilter;
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}

const TEX = {
  grass_top: loadTex('blocks/grass_top'),
  grass_side: loadTex('blocks/grass_side'),
  dirt: loadTex('blocks/dirt'),
  stone: loadTex('blocks/stone'),
  sand: loadTex('blocks/sand'),
  log: loadTex('blocks/log_oak'),
  leaf: loadTex('blocks/leaves'),
  glass: loadTex('blocks/glass'),
  brick: loadTex('blocks/brick'),
  plank: loadTex('blocks/wood'),
};

const BLOCK_TEX = {
  grass:   { top: TEX.grass_top, side: TEX.grass_side, bottom: TEX.dirt },
  dirt:    { all: TEX.dirt },
  stone:   { all: TEX.stone },
  sand:    { all: TEX.sand },
  log:     { all: TEX.log },
  leaf:    { all: TEX.leaf, transparent: true },
  glass:   { all: TEX.glass, transparent: true },
  brick:   { all: TEX.brick },
  plank:   { all: TEX.plank },
};

/* ============================================================
   1. 渲染器
   ============================================================ */
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87c5ff);
scene.fog = new THREE.Fog(0x87c5ff, 22, 55);

const camera = new THREE.PerspectiveCamera(72, innerWidth/innerHeight, 0.08, 120);
camera.rotation.order = 'YXZ';

scene.add(new THREE.HemisphereLight(0xffffff, 0x556677, 1.05));
const sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(30, 60, 20);
scene.add(sun);

/* ============================================================
   2. 体素世界
   ============================================================ */
const WORLD_W = 32, WORLD_H = 12, WORLD_D = 32;

const world = {};
const key = (x,y,z) => `${x},${y},${z}`;

function generateWorld(){
  for(let x=0;x<WORLD_W;x++){
    for(let z=0;z<WORLD_D;z++){
      const h = 2 + Math.floor(Math.sin(x*0.4)*1.2 + Math.cos(z*0.35)*1.0);
      for(let y=0;y<=h;y++){
        let type = 'stone';
        if(y === h) type = 'grass';
        else if(y >= h-1) type = 'dirt';
        world[key(x,y,z)] = type;
      }
    }
  }
  // 几棵树
  for(let i=0;i<6;i++){
    const tx = 4 + Math.floor(Math.random()*(WORLD_W-8));
    const tz = 4 + Math.floor(Math.random()*(WORLD_D-8));
    let gy = 0;
    for(let y=WORLD_H-1;y>=0;y--){ if(world[key(tx,y,tz)]){ gy=y; break; } }
    for(let j=1;j<=4;j++) world[key(tx, gy+j, tz)] = 'log';
    for(let dx=-2;dx<=2;dx++) for(let dz=-2;dz<=2;dz++) for(let dy=-1;dy<=1;dy++){
      if(Math.abs(dx)+Math.abs(dz)+Math.abs(dy) > 3) continue;
      const bx=tx+dx, by=gy+4+dy+1, bz=tz+dz;
      if(!world[key(bx,by,bz)]) world[key(bx,by,bz)] = 'leaf';
    }
  }
}
generateWorld();

/* 每种方块一个 InstancedMesh,用对应纹理 */
const boxGeo = new THREE.BoxGeometry(1,1,1);
const instanced = {};

function buildMeshes(){
  Object.values(instanced).forEach(m => scene.remove(m));
  for(const k in instanced) delete instanced[k];

  const groups = {};
  for(const k in world){
    const [x,y,z] = k.split(',').map(Number);
    const t = world[k];
    if(!groups[t]) groups[t] = [];
    groups[t].push([x,y,z]);
  }

  const dummy = new THREE.Object3D();

  for(const type in groups){
    const list = groups[type];
    const def = BLOCK_TEX[type];
    if(!def) continue;

    let mat;
    if(def.all){
      mat = new THREE.MeshLambertMaterial({
        map: def.all,
        transparent: !!def.transparent,
        side: def.transparent ? THREE.DoubleSide : THREE.FrontSide,
        alphaTest: def.transparent ? 0.5 : 0
      });
    } else {
      // 多面纹理(草方块):用材质数组
      const mTop = new THREE.MeshLambertMaterial({ map: def.top });
      const mSide = new THREE.MeshLambertMaterial({ map: def.side });
      const mBottom = new THREE.MeshLambertMaterial({ map: def.bottom });
      mat = [mSide, mSide, mTop, mBottom, mSide, mSide];
    }

    const mesh = new THREE.InstancedMesh(boxGeo, mat, list.length);
    list.forEach((p,i)=>{
      dummy.position.set(p[0], p[1], p[2]);
      dummy.updateMatrix();
      mesh.setMatrixAt(i, dummy.matrix);
    });
    mesh.instanceMatrix.needsUpdate = true;
    scene.add(mesh);
    instanced[type] = mesh;
  }
}
buildMeshes();

/* ============================================================
   3. 玩家
   ============================================================ */
const player = {
  pos: new THREE.Vector3(WORLD_W/2, 6, WORLD_D/2),
  yaw: 0, pitch: -0.12,
  vy: 0, onGround: false,
  flying: false, sneaking: false,
  thirdPerson: false,
};

/* 史蒂夫贴图皮肤(ProgrammerArt 没有玩家皮肤,用肤色+衣服色块拼) */
function createSteve(){
  const g = new THREE.Group();
  const skin = new THREE.MeshLambertMaterial({ color: 0xe6b98a });
  const shirt = new THREE.MeshLambertMaterial({ color: 0x18a5a5 });
  const pants = new THREE.MeshLambertMaterial({ color: 0x3a3a8c });

  const head = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.5,0.5), skin);
  head.position.y = 1.75; g.add(head);
  const body = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.75,0.26), shirt);
  body.position.y = 1.125; g.add(body);

  const armGeo = new THREE.BoxGeometry(0.24,0.72,0.24);
  const armL = new THREE.Group(); armL.position.set(-0.37,1.47,0);
  const amL = new THREE.Mesh(armGeo, shirt); amL.position.y=-0.36; armL.add(amL); g.add(armL);
  const armR = new THREE.Group(); armR.position.set(0.37,1.47,0);
  const amR = new THREE.Mesh(armGeo, skin); amR.position.y=-0.36; armR.add(amR); g.add(armR);

  const legGeo = new THREE.BoxGeometry(0.24,0.75,0.24);
  const legL = new THREE.Group(); legL.position.set(-0.13,0.75,0);
  const lmL = new THREE.Mesh(legGeo, pants); lmL.position.y=-0.375; legL.add(lmL); g.add(legL);
  const legR = new THREE.Group(); legR.position.set(0.13,0.75,0);
  const lmR = new THREE.Mesh(legGeo, pants); lmR.position.y=-0.375; legR.add(lmR); g.add(legR);

  g.userData = { armL, armR, legL, legR };
  return g;
}
const steve = createSteve();
steve.visible = false;
scene.add(steve);

/* ============================================================
   4. 物理
   ============================================================ */
function solidAt(x,y,z){
  const t = world[key(Math.floor(x), Math.floor(y), Math.floor(z))];
  return t && t !== 'glass' && t !== 'leaf';
}

function moveAxis(axis, delta){
  const p = player.pos;
  const nx = axis==='x' ? p.x+delta : p.x;
  const ny = axis==='y' ? p.y+delta : p.y;
  const nz = axis==='z' ? p.z+delta : p.z;
  const r = 0.3;
  for(let dx=-r;dx<=r;dx+=r) for(let dz=-r;dz<=r;dz+=r){
    if(solidAt(nx+dx, ny, nz+dz) || solidAt(nx+dx, ny+1.7, nz+dz)) return false;
  }
  p[axis] += delta;
  return true;
}

/* ============================================================
   5. 触控
   ============================================================ */
const joy = { x:0, y:0, id:null };
const joyBase = document.getElementById('joyBase');
const joyKnob = document.getElementById('joyKnob');

function joyMove(t){
  const r = joyBase.getBoundingClientRect();
  let dx = t.clientX - (r.left+r.width/2);
  let dy = t.clientY - (r.top+r.height/2);
  const max = r.width/2 - 14;
  const d = Math.hypot(dx,dy);
  if(d>max){ dx=dx/d*max; dy=dy/d*max; }
  joy.x = dx/max; joy.y = dy/max;
  joyKnob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
}
joyBase.addEventListener('touchstart', e=>{ e.preventDefault(); joy.id=e.changedTouches[0].identifier; joyMove(e.changedTouches[0]); }, {passive:false});
joyBase.addEventListener('touchmove', e=>{ e.preventDefault(); for(const t of e.changedTouches) if(t.identifier===joy.id) joyMove(t); }, {passive:false});
function joyEnd(){ joy.x=0; joy.y=0; joy.id=null; joyKnob.style.transform='translate(-50%,-50%)'; }
joyBase.addEventListener('touchend', joyEnd, {passive:false});
joyBase.addEventListener('touchcancel', joyEnd, {passive:false});

/* 右侧视角 */
let lookId=null, lx=0, ly=0;
canvas.addEventListener('touchstart', e=>{
  for(const t of e.changedTouches){
    if(t.clientX > innerWidth*0.3 && lookId===null){ lookId=t.identifier; lx=t.clientX; ly=t.clientY; break; }
  }
}, {passive:true});
canvas.addEventListener('touchmove', e=>{
  for(const t of e.changedTouches){
    if(t.identifier===lookId){
      player.yaw -= (t.clientX-lx)*0.005;
      player.pitch -= (t.clientY-ly)*0.005;
      player.pitch = Math.max(-1.45, Math.min(1.45, player.pitch));
      lx=t.clientX; ly=t.clientY;
    }
  }
}, {passive:true});
canvas.addEventListener('touchend', e=>{ for(const t of e.changedTouches) if(t.identifier===lookId) lookId=null; }, {passive:true});

/* 跳跃 / 双击飞行 */
const btnJump = document.getElementById('btnJump');
const btnSneak = document.getElementById('btnSneak');
let jumpHeld=false, sneakHeld=false, lastTap=0;

btnJump.addEventListener('touchstart', e=>{
  e.preventDefault(); jumpHeld=true;
  const now = performance.now();
  if(state.mode==='creative'){
    if(now-lastTap<320){ player.flying=!player.flying; player.vy=0; updateFlyUI(); lastTap=0; return; }
    lastTap=now;
  }
  if(!player.flying && player.onGround){ player.vy=8.4; player.onGround=false; }
}, {passive:false});
btnJump.addEventListener('touchend', ()=>{ jumpHeld=false; });
btnJump.addEventListener('touchcancel', ()=>{ jumpHeld=false; });

btnSneak.addEventListener('touchstart', e=>{
  e.preventDefault(); sneakHeld=true;
  player.sneaking = !player.sneaking;
}, {passive:false});
btnSneak.addEventListener('touchend', ()=>{ sneakHeld=false; });

function updateFlyUI(){
  btnJump.textContent = player.flying ? '上升' : '跳跃';
  btnSneak.textContent = player.flying ? '下降' : '潜行';
  btnJump.classList.toggle('fly', player.flying);
  btnSneak.classList.toggle('fly', player.flying);
}

/* ============================================================
   6. 状态 & 生存 HUD
   ============================================================ */
const state = { mode:'creative', hp:20, maxHp:20, food:20, maxFood:20, xp:0, xpLevel:0, selected:0 };

/* 心形 SVG(像素风) */
const HEART_FULL = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9"><path d="M1 1h2v1h1V1h2v1h1v3H6v1H5v1H4V6H3V5H2V4H1z" fill="#e03030"/></svg>');
const HEART_EMPTY = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9"><path d="M1 1h2v1h1V1h2v1h1v3H6v1H5v1H4V6H3V5H2V4H1z" fill="none" stroke="#555" stroke-width="1"/></svg>');
const FOOD_FULL = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9"><rect x="1" y="2" width="7" height="5" rx="1" fill="#c98a3a"/><rect x="2" y="1" width="2" height="2" fill="#c98a3a"/></svg>');

function renderHUD(){
  const hearts = document.getElementById('hearts');
  const hunger = document.getElementById('hunger');
  hearts.innerHTML = '';
  for(let i=0;i<10;i++){
    const img = new Image(); img.src = (state.hp >= (i+1)*2) ? HEART_FULL : HEART_EMPTY;
    img.className = 'heart'; hearts.appendChild(img);
  }
  hunger.innerHTML = '';
  for(let i=0;i<10;i++){
    const img = new Image(); img.src = FOOD_FULL;
    img.style.opacity = state.food >= (i+1)*2 ? 1 : 0.2;
    img.className = 'food'; hunger.appendChild(img);
  }
  const need = 10 + state.xpLevel*5;
  document.getElementById('xpBarInner').style.width = Math.min(100, state.xp/need*100) + '%';
  document.getElementById('xpLevel').textContent = state.xpLevel;
}

/* ============================================================
   7. 快捷栏(带贴图预览)
   ============================================================ */
const HOTBAR = [
  { type:'grass', name:'草方块' },
  { type:'dirt', name:'泥土' },
  { type:'stone', name:'石头' },
  { type:'sand', name:'沙子' },
  { type:'log', name:'原木' },
  { type:'leaf', name:'树叶' },
  { type:'glass', name:'玻璃' },
  { type:'brick', name:'砖块' },
  { type:'plank', name:'木板' },
];

const hotbarEl = document.getElementById('hotbar');
function buildHotbar(){
  hotbarEl.innerHTML = '';
  HOTBAR.forEach((item,i)=>{
    const d = document.createElement('div');
    d.className = 'slot' + (i===0?' active':'');
    const img = new Image();
    img.src = TEX_BASE + 'blocks/' + (item.type==='grass'?'grass_side':item.type==='log'?'log_oak':item.type==='leaf'?'leaves':item.type==='plank'?'wood':item.type) + '.png';
    img.onerror = ()=>{ img.style.display='none'; };
    d.appendChild(img);
    d.addEventListener('touchstart', e=>{
      e.preventDefault();
      state.selected = i;
      [...hotbarEl.children].forEach((c,idx)=>c.classList.toggle('active', idx===i));
    }, {passive:false});
    hotbarEl.appendChild(d);
  });
}
buildHotbar();

/* ============================================================
   8. 设置面板
   ============================================================ */
const settingsPanel = document.getElementById('settingsPanel');
const settingsBody = document.getElementById('settingsBody');
const settingsTabs = document.querySelectorAll('.stab');

const settings = {
  renderDistance: 8, fov: 72, uiScale: 1, brightness: 1,
  masterVol: 80, musicVol: 60, sfxVol: 80,
  sensitivity: 100, invertY: false,
  controlMode: 'joystick', splitControls: false,
  autoJump: false, flyDoubleTap: true,
};

function buildSettingsBody(tab){
  settingsBody.innerHTML = '';
  const add = (html)=>{ const div=document.createElement('div'); div.innerHTML=html; settingsBody.appendChild(div); };

  if(tab==='video'){
    add('<div class="setGroup">显示</div>');
    add(`<div class="setRow"><label>视野 FOV</label><input type="range" min="50" max="110" value="${settings.fov}" data-k="fov"><span class="val">${settings.fov}</span></div>`);
    add(`<div class="setRow"><label>亮度</label><input type="range" min="30" max="150" value="${settings.brightness*100}" data-k="brightness"><span class="val">${Math.round(settings.brightness*100)}%</span></div>`);
    add(`<div class="setRow"><label>渲染距离</label><input type="range" min="4" max="16" value="${settings.renderDistance}" data-k="renderDistance"><span class="val">${settings.renderDistance}</span></div>`);
    add(`<div class="setRow"><label>UI 缩放</label><input type="range" min="70" max="130" value="${settings.uiScale*100}" data-k="uiScale"><span class="val">${Math.round(settings.uiScale*100)}%</span></div>`);
  }
  if(tab==='audio'){
    add('<div class="setGroup">音量</div>');
    add(`<div class="setRow"><label>主音量</label><input type="range" min="0" max="100" value="${settings.masterVol}" data-k="masterVol"><span class="val">${settings.masterVol}%</span></div>`);
    add(`<div class="setRow"><label>音乐</label><input type="range" min="0" max="100" value="${settings.musicVol}" data-k="musicVol"><span class="val">${settings.musicVol}%</span></div>`);
    add(`<div class="setRow"><label>音效</label><input type="range" min="0" max="100" value="${settings.sfxVol}" data-k="sfxVol"><span class="val">${settings.sfxVol}%</span></div>`);
  }
  if(tab==='control'){
    add('<div class="setGroup">视角</div>');
    add(`<div class="setRow"><label>灵敏度</label><input type="range" min="20" max="200" value="${settings.sensitivity}" data-k="sensitivity"><span class="val">${settings.sensitivity}%</span></div>`);
    add(`<div class="setRow"><label>反转 Y 轴</label><select data-k="invertY"><option value="false"${!settings.invertY?' selected':''}>关</option><option value="true"${settings.invertY?' selected':''}>开</option></select></div>`);
    add('<div class="setGroup">移动</div>');
    add(`<div class="setRow"><label>自动跳跃</label><select data-k="autoJump"><option value="false"${!settings.autoJump?' selected':''}>关</option><option value="true"${settings.autoJump?' selected':''}>开</option></select></div>`);
  }
  if(tab==='touch'){
    add('<div class="setGroup">触控模式</div>');
    add(`<div class="setRow"><label>控制方式</label><select data-k="controlMode"><option value="joystick"${settings.controlMode==='joystick'?' selected':''}>摇杆</option><option value="dpad"${settings.controlMode==='dpad'?' selected':''}>十字键</option><option value="crosshair"${settings.controlMode==='crosshair'?' selected':''}>摇杆+准星</option></select></div>`);
    add(`<div class="setRow"><label>分离控制</label><select data-k="splitControls"><option value="false"${!settings.splitControls?' selected':''}>关</option><option value="true"${settings.splitControls?' selected':''}>开</option></select></div>`);
    add(`<div class="setRow"><label>双击飞行</label><select data-k="flyDoubleTap"><option value="true"${settings.flyDoubleTap?' selected':''}>开</option><option value="false"${!settings.flyDoubleTap?' selected':''}>关</option></select></div>`);
  }

  // 绑定事件
  settingsBody.querySelectorAll('input[type=range]').forEach(el=>{
    el.addEventListener('input', ()=>{
      const k = el.dataset.k;
      const v = Number(el.value);
      const span = el.parentElement.querySelector('.val');
      if(k==='fov'){ settings.fov=v; camera.fov=v; camera.updateProjectionMatrix(); span.textContent=v; }
      if(k==='brightness'){ settings.brightness=v/100; scene.children.forEach(c=>{ if(c.isHemisphereLight) c.intensity=settings.brightness*1.05; }); span.textContent=v+'%'; }
      if(k==='renderDistance'){ settings.renderDistance=v; span.textContent=v; }
      if(k==='uiScale'){ settings.uiScale=v/100; document.documentElement.style.fontSize=(14*settings.uiScale)+'px'; span.textContent=v+'%'; }
      if(k==='masterVol'||k==='musicVol'||k==='sfxVol'){ settings[k]=v; span.textContent=v+'%'; }
      if(k==='sensitivity'){ settings.sensitivity=v; span.textContent=v+'%'; }
    });
  });
  settingsBody.querySelectorAll('select').forEach(el=>{
    el.addEventListener('change', ()=>{
      const k = el.dataset.k;
      const v = el.value;
      if(k==='invertY') settings.invertY = v==='true';
      if(k==='autoJump') settings.autoJump = v==='true';
      if(k==='controlMode') settings.controlMode = v;
      if(k==='splitControls'){ settings.splitControls = v==='true'; document.body.classList.toggle('split', settings.splitControls); }
      if(k==='flyDoubleTap') settings.flyDoubleTap = v==='true';
    });
  });
}

function openSettings(){
  settingsPanel.classList.add('open');
  buildSettingsBody('video');
  settingsTabs.forEach(t=>t.classList.toggle('active', t.dataset.tab==='video'));
}
function closeSettings(){ settingsPanel.classList.remove('open'); }

settingsTabs.forEach(tab=>{
  tab.addEventListener('click', ()=>{
    settingsTabs.forEach(t=>t.classList.remove('active'));
    tab.classList.add('active');
    buildSettingsBody(tab.dataset.tab);
  });
});
document.getElementById('settingsClose').addEventListener('click', closeSettings);
document.getElementById('btnSettings').addEventListener('touchstart', e=>{ e.preventDefault(); openSettings(); }, {passive:false});

/* 顶部按钮 */
const btnMode = document.getElementById('btnMode');
const btnPersp = document.getElementById('btnPersp');
const btnSplit = document.getElementById('btnSplit');
const hint = document.getElementById('hint');

btnMode.addEventListener('touchstart', e=>{
  e.preventDefault();
  state.mode = state.mode==='creative'?'survival':'creative';
  document.body.classList.toggle('creative', state.mode==='creative');
  btnMode.textContent = state.mode==='creative'?'创造':'生存';
  if(state.mode==='survival') player.flying=false;
  updateFlyUI();
  hint.textContent = state.mode==='creative'?'创造模式 · 双击跳跃飞行':'生存模式';
  renderHUD();
}, {passive:false});

btnPersp.addEventListener('touchstart', e=>{ e.preventDefault(); player.thirdPerson=!player.thirdPerson; hint.textContent = player.thirdPerson?'第三人称':'第一人称'; }, {passive:false});

btnSplit.addEventListener('touchstart', e=>{
  e.preventDefault();
  settings.splitControls = !settings.splitControls;
  document.body.classList.toggle('split', settings.splitControls);
  hint.textContent = settings.splitControls?'分离控制 · 准星瞄准':'触控模式';
}, {passive:false});

/* ============================================================
   9. 主循环
   ============================================================ */
let last = performance.now();

function animate(){
  requestAnimationFrame(animate);
  const now = performance.now();
  let dt = (now-last)/1000; last = now;
  if(dt>0.1) dt=0.1;

  // 移动
  const speed = player.flying ? 7.5 : (player.sneaking ? 1.8 : 4.4);
  const sinY = Math.sin(player.yaw), cosY = Math.cos(player.yaw);
  const mvx = (-sinY)*(-joy.y) + cosY*joy.x;
  const mvz = (-cosY)*(-joy.y) + (-sinY)*joy.x;

  if(Math.abs(mvx)>0.05 || Math.abs(mvz)>0.05){
    moveAxis('x', mvx*speed*dt);
    moveAxis('z', mvz*speed*dt);
  }

  // 垂直
  if(player.flying){
    let vy = 0;
    if(jumpHeld) vy += speed;
    if(sneakHeld) vy -= speed;
    moveAxis('y', vy*dt);
    player.vy = 0;
  } else {
    if(jumpHeld && player.onGround){ player.vy=8.4; player.onGround=false; }
    player.vy -= 26*dt;
    if(player.vy < -40) player.vy = -40;
    const before = player.pos.y;
    const ok = moveAxis('y', player.vy*dt);
    if(!ok && player.vy <= 0){ player.onGround = true; player.vy = 0; }
    else player.onGround = false;
  }

  // 边界
  player.pos.x = Math.max(1, Math.min(WORLD_W-1, player.pos.x));
  player.pos.z = Math.max(1, Math.min(WORLD_D-1, player.pos.z));

  // 史蒂夫
  steve.position.copy(player.pos);
  steve.rotation.y = player.yaw + Math.PI;
  steve.visible = player.thirdPerson;
  if(player.thirdPerson){
    const moving = Math.abs(joy.x)>0.15 || Math.abs(joy.y)>0.15;
    if(moving && player.onGround) steve.userData.phase = (steve.userData.phase||0) + dt*9;
    const sw = Math.sin(steve.userData.phase||0)*0.6;
    steve.userData.armL.rotation.x = sw;
    steve.userData.armR.rotation.x = -sw;
    steve.userData.legL.rotation.x = -sw;
    steve.userData.legR.rotation.x = sw;
  }

  // 相机
  const eye = new THREE.Vector3(player.pos.x, player.pos.y+0.1, player.pos.z);
  if(player.thirdPerson){
    const dist = 4.5;
    const cp = Math.cos(player.pitch);
    const target = new THREE.Vector3(
      eye.x + Math.sin(player.yaw)*cp*dist,
      eye.y - Math.sin(player.pitch)*dist + 1.2,
      eye.z + Math.cos(player.yaw)*cp*dist
    );
    camera.position.lerp(target, Math.min(1, dt*12));
    camera.lookAt(eye);
  } else {
    camera.position.copy(eye);
    camera.rotation.set(player.pitch, player.yaw, 0, 'YXZ');
  }

  // 生存 HUD 缓慢更新
  if(state.mode==='survival'){
    state.xp += dt*1.5;
    const need = 10 + state.xpLevel*5;
    if(state.xp >= need){ state.xp -= need; state.xpLevel++; }
    renderHUD();
  }

  renderer.render(scene, camera);
}

/* 初始化 */
player.pos.y = 6;
updateFlyUI();
renderHUD();
animate();

addEventListener('resize', ()=>{
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
document.addEventListener('gesturestart', e=>e.preventDefault());
</script>
</body>
</html>

Game Source: MC 仿制 · 真实贴图 + 设置界面

Creator: ElectricShark88

Libraries: three

Complexity: complex (693 lines, 30.5 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: mc-electricshark88" to link back to the original. Then publish at arcadelab.ai/publish.