🎮ArcadeLab

MC网页沙盒|可破坏方块火把修复版

by DriftTurtle56
766 lines26.8 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>MC网页沙盒|可破坏方块火把修复版</title>
<style>
*{margin:0;padding:0}
body{overflow:hidden;background:#000;font-family:Arial}
.ui-info{
    position:absolute;
    top:15px;left:15px;
    background:rgba(0,0,0,0.65);
    color:#fff;padding:12px;border-radius:8px;
    z-index:10;line-height:1.6;font-size:14px;
}
.crosshair{
    position:absolute;
    top:50%;left:50%;
    transform:translate(-50%,-50%);
    color:#fff;font-size:26px;
    pointer-events:none;z-index:10;
}
</style>
</head>
<body>
<div class="ui-info">
左键拖拽转视角|左键打怪|中键放方块|右键拆方块<br>
WASD移动 空格跳跃|1草地 2泥土 3石头 4原木 5木板 6树叶 7水 8岩浆 9火把<br>
生命值:<span id="hpText">100</span>
<span id="dayTip">时段:白天</span>
</div>
<div class="crosshair">+</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script>
let scene, camera, renderer, sunLight, ambientLight;
const blockWorld = new Map();
const BLOCK_SIZE = 1;
let selectBlock = 0;
const keyMap = {};

let yaw = 0;
let pitch = 0;
let isDrag = false;
let lastX = 0, lastY = 0;
const SENSITIVITY = 0.0022;

const GRAVITY = -0.022;
let velocityY = 0;
const JUMP_FORCE = 0.35;
const MOVE_SPEED = 0.18;
const PLAYER_HALF_WIDTH = 0.35;
const PLAYER_HEIGHT = 1.8;

let hp = 100;
const hpText = document.getElementById("hpText");
const dayTip = document.getElementById("dayTip");

let dayTime = 0.45;
const DAY_SPEED = 0.00012;
let isNight = false;
let wasNightLastFrame = false;

const MONSTER_TYPE = {ZOMBIE:0,SKELETON:1,SPIDER:2};
const monsterList = [];
const monsterAnimations = [];
const arrowList = [];
let lastDamageTime = 0;
let lastAttackTime = 0;
const ATTACK_COOLDOWN = 350;
const ATTACK_RANGE = 4.5;

const villagerGeoBody = new THREE.BoxGeometry(0.45, 0.7, 0.35);
const villagerGeoHead = new THREE.BoxGeometry(0.35, 0.35, 0.35);
const villagerGeoLeg = new THREE.BoxGeometry(0.18, 0.55, 0.18);
const matVillager = new THREE.MeshLambertMaterial({color:0xc89979});
const matVillagerCloth = new THREE.MeshLambertMaterial({color:0x347eb8});

const blockGeo = new THREE.BoxGeometry(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
// 方形火把几何体
const torchStickBox = new THREE.BoxGeometry(0.12, 0.65, 0.12);
const torchFireBox = new THREE.BoxGeometry(0.22, 0.22, 0.22);

const boxSmall = new THREE.BoxGeometry(0.35, 0.45, 0.35);
const boxBody = new THREE.BoxGeometry(0.55, 0.75, 0.32);
const boxLeg = new THREE.BoxGeometry(0.22, 0.62, 0.22);
const spiderBodyGeo = new THREE.BoxGeometry(1.1, 0.45, 0.75);
const spiderHeadGeo = new THREE.BoxGeometry(0.42, 0.38, 0.42);
const spiderLegGeo = new THREE.BoxGeometry(0.72, 0.14, 0.14);
const arrowGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.6, 4);
const arrowMat = new THREE.MeshLambertMaterial({ color: 0x604020 });

const lavaLights = [];
const torchLights = [];

function createTex(canvasFn){
    const canvas = document.createElement("canvas");
    canvas.width = 64;
    canvas.height = 64;
    const ctx = canvas.getContext("2d");
    canvasFn(ctx);
    const tex = new THREE.CanvasTexture(canvas);
    tex.magFilter = THREE.NearestFilter;
    tex.minFilter = THREE.NearestMipmapNearestFilter;
    tex.wrapS = THREE.RepeatWrapping;
    tex.wrapT = THREE.RepeatWrapping;
    return tex;
}

const texGrassTop = createTex(ctx=>{
    ctx.fillStyle="#3a7d30";ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#2d6b24";for(let i=0;i<100;i++)ctx.fillRect(Math.random()*64,Math.random()*64,3,3);
});
const texGrassSide = createTex(ctx=>{
    ctx.fillStyle="#8b5a2b";ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#3a7d30";ctx.fillRect(0,0,64,22);
    ctx.fillStyle="#7a4c22";for(let i=0;i<80;i++)ctx.fillRect(Math.random()*64,22+Math.random()*42,3,3);
});
const texDirt = createTex(ctx=>{
    ctx.fillStyle="#8b5a2b";ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#7a4c22";for(let i=0;i<90;i++)ctx.fillRect(Math.random()*64,Math.random()*64,3,3);
});
const texStone = createTex(ctx=>{
    ctx.fillStyle="#777777";ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#666666";for(let y=0;y<64;y+=8)for(let x=0;x<64;x+=8)if((x+y)%16===0)ctx.fillRect(x,y,5,5);
});
const texLog = createTex(ctx=>{
    ctx.fillStyle="#795537";ctx.fillRect(0,0,64,64);
    ctx.strokeStyle="#5c4028";ctx.lineWidth=3;
    for(let x=8;x<64;x+=12){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,64);ctx.stroke();}
});
const texPlank = createTex(ctx=>{
    ctx.fillStyle="#b88c60";ctx.fillRect(0,0,64,64);
    ctx.strokeStyle="#8c6640";ctx.lineWidth=2;
    for(let y=0;y<64;y+=16){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(64,y);ctx.stroke();}
});
const texLeaves = createTex(ctx=>{
    ctx.fillStyle="#3a7d30";ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#2d6b24";for(let i=0;i<90;i++)ctx.fillRect(Math.random()*64,Math.random()*64,4,4);
});
const texWater = createTex(ctx=>{
    ctx.fillStyle="#4286f4";
    ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#63a1ff";
    for(let i=0;i<70;i++) ctx.fillRect(Math.random()*64,Math.random()*64,5,2);
});
const texLava = createTex(ctx=>{
    ctx.fillStyle="#ff4500";
    ctx.fillRect(0,0,64,64);
    ctx.fillStyle="#ffaa00";
    for(let i=0;i<80;i++) ctx.fillRect(Math.random()*64,Math.random()*64,6,6);
});

const matTorchStick = new THREE.MeshLambertMaterial({color:0x6B4423});
const matTorchFire = new THREE.MeshLambertMaterial({
    color:0xFFE870,
    emissive:0xFF7711,
    emissiveIntensity:0.6
});

function createGrassMaterial(){
    return [
        new THREE.MeshLambertMaterial({map:texGrassSide}),
        new THREE.MeshLambertMaterial({map:texGrassSide}),
        new THREE.MeshLambertMaterial({map:texGrassTop}),
        new THREE.MeshLambertMaterial({map:texDirt}),
        new THREE.MeshLambertMaterial({map:texGrassSide}),
        new THREE.MeshLambertMaterial({map:texGrassSide})
    ]
}
const matDirt = new THREE.MeshLambertMaterial({map:texDirt});
const matStone = new THREE.MeshLambertMaterial({map:texStone});
const matLog = new THREE.MeshLambertMaterial({map:texLog});
const matPlank = new THREE.MeshLambertMaterial({map:texPlank});
const matLeaves = new THREE.MeshLambertMaterial({map:texLeaves});
const matWater = new THREE.MeshLambertMaterial({
    map: texWater,
    transparent: true,
    opacity: 0.7
});
const matLava = new THREE.MeshLambertMaterial({
    map: texLava,
    emissive: 0xff5500,
    emissiveIntensity: 0.6
});

const matZombie = new THREE.MeshLambertMaterial({ color: 0x669944 });
const matSkeleton = new THREE.MeshLambertMaterial({ color: 0xdddddd });
const matSpider = new THREE.MeshLambertMaterial({ color: 0x222222 });

function getGroundHeight(worldX, worldZ){
    let checkY = 20;
    while(checkY > -20){
        if(blockWorld.has(`${Math.round(worldX)},${checkY},${Math.round(worldZ)}`)){
            return checkY + 1;
        }
        checkY--;
    }
    return Math.round(noise(worldX, worldZ));
}

function createTorchMesh(){
    const group = new THREE.Group();
    const stick = new THREE.Mesh(torchStickBox, matTorchStick);
    stick.position.y = -0.25;
    const fire = new THREE.Mesh(torchFireBox, matTorchFire);
    fire.position.y = 0.18;
    group.add(stick);
    group.add(fire);
    return group;
}

function createVillager(){
    const root = new THREE.Group();
    const head = new THREE.Mesh(villagerGeoHead, matVillager);
    head.position.y = 1.35;
    const body = new THREE.Mesh(villagerGeoBody, matVillagerCloth);
    body.position.y = 0.9;
    const legL = new THREE.Mesh(villagerGeoLeg, matVillager);
    legL.position.set(-0.16, 0.28, 0);
    const legR = new THREE.Mesh(villagerGeoLeg, matVillager);
    legR.position.set(0.16, 0.28, 0);

    [head,body,legL,legR].forEach(m=>{
        m.castShadow = false;
        m.receiveShadow = false;
        root.add(m);
    })
    root.legL = legL;
    root.legR = legR;
    root.walkTimer = Math.random()*100;
    root.targetPos = new THREE.Vector3();
    root.wanderRange = 12;
    return root;
}

const villagerList = [];

function spawnVillager(count, spawnX, spawnZ){
    for(let i=0;i<count;i++){
        const villager = createVillager();
        const ox = spawnX + (Math.random()-0.5)*10;
        const oz = spawnZ + (Math.random()-0.5)*10;
        const groundY = getGroundHeight(ox, oz);
        villager.position.set(ox, groundY, oz);
        villager.targetPos.set(ox, groundY, oz);
        villagerList.push(villager);
        scene.add(villager);
    }
}

function updateVillagers(){
    for(let v of villagerList){
        v.walkTimer += 1;
        const dist = v.position.distanceTo(v.targetPos);
        if(dist<0.8 || v.walkTimer>300){
            v.walkTimer = 0;
            const rx = v.position.x + (Math.random()-0.5)*v.wanderRange;
            const rz = v.position.z + (Math.random()-0.5)*v.wanderRange;
            const groundY = getGroundHeight(rx, rz);
            v.targetPos.set(rx, groundY, rz);
        }
        const currentGround = getGroundHeight(v.position.x, v.position.z);
        if(Math.abs(v.position.y - currentGround) > 0.1){
            v.position.y = currentGround;
        }

        if(!isNight){
            const dir = new THREE.Vector3().subVectors(v.targetPos, v.position);
            dir.y=0;
            if(dir.length()>0.1){
                dir.normalize();
                v.position.x += dir.x*0.025;
                v.position.z += dir.z*0.025;
                const swing = Math.sin(v.walkTimer*0.12)*0.18;
                v.legL.rotation.x = swing;
                v.legR.rotation.x = -swing;
            }
            v.lookAt(v.targetPos.x, v.position.y, v.targetPos.z);
        }else{
            v.legL.rotation.x = 0;
            v.legR.rotation.x = 0;
        }
    }
}

function createMonsterState(mob, type){
    return {mob,type,walkTime:0,lastPos:new THREE.Vector3(),legL:null,legR:null,head:null,legs:[]};
}

function buildZombie(){
    const root = new THREE.Group();
    const head = new THREE.Mesh(boxSmall, matZombie);head.position.y=1.42;
    const body = new THREE.Mesh(boxBody, matZombie);body.position.y=0.9;
    const legL = new THREE.Mesh(boxLeg, matZombie);legL.position.set(-0.22,0.31,0);
    const legR = new THREE.Mesh(boxLeg, matZombie);legR.position.set(0.22,0.31,0);
    [head,body,legL,legR].forEach(m=>{m.castShadow=false;m.receiveShadow=false;root.add(m)});
    root.monsterType=MONSTER_TYPE.ZOMBIE;root.lastShoot=0;
    const state=createMonsterState(root,MONSTER_TYPE.ZOMBIE);
    state.legL=legL;state.legR=legR;state.head=head;monsterAnimations.push(state);
    return root;
}

function buildSkeleton(){
    const root = new THREE.Group();
    const head = new THREE.Mesh(boxSmall, matSkeleton);head.position.y=1.42;
    const body = new THREE.Mesh(boxBody, matSkeleton);body.position.y=0.9;
    const legL = new THREE.Mesh(boxLeg, matSkeleton);legL.position.set(-0.22,0.31,0);
    const legR = new THREE.Mesh(boxLeg, matSkeleton);legR.position.set(0.22,0.31,0);
    [head,body,legL,legR].forEach(m=>{m.castShadow=false;m.receiveShadow=false;root.add(m)});
    root.monsterType=MONSTER_TYPE.SKELETON;root.lastShoot=0;
    const state=createMonsterState(root,MONSTER_TYPE.SKELETON);
    state.legL=legL;state.legR=legR;state.head=head;monsterAnimations.push(state);
    return root;
}

function buildSpider(){
    const root = new THREE.Group();
    const torso = new THREE.Mesh(spiderBodyGeo, matSpider);torso.position.y=0.26;
    const head = new THREE.Mesh(spiderHeadGeo, matSpider);head.position.set(0,0.32,0.52);
    const legPos=[[-0.6,0,0.25],[0.6,0,0.25],[-0.6,0,-0.25],[0.6,0,-0.25]];
    const legList=[];
    legPos.forEach(p=>{
        const leg=new THREE.Mesh(spiderLegGeo,matSpider);leg.position.set(p[0],0.12,p[2]);
        leg.castShadow=false;root.add(leg);legList.push(leg);
    });
    [torso,head].forEach(m=>{m.castShadow=false;m.receiveShadow=false;root.add(m)});
    root.monsterType=MONSTER_TYPE.SPIDER;root.lastShoot=0;
    const state=createMonsterState(root,MONSTER_TYPE.SPIDER);state.legs=legList;monsterAnimations.push(state);
    return root;
}

function updateMonsterAnimations(){
    for(const anim of monsterAnimations){
        const mob=anim.mob;
        const moveDelta=mob.position.clone().sub(anim.lastPos);
        const isMoving=moveDelta.length()>0.0008;
        anim.lastPos.copy(mob.position);
        if(isMoving)anim.walkTime+=0.12;
        if(anim.type===MONSTER_TYPE.ZOMBIE||anim.type===MONSTER_TYPE.SKELETON){
            const swing=Math.sin(anim.walkTime)*0.2;
            anim.legL.rotation.x=swing;anim.legR.rotation.x=-swing;
        }
        if(anim.type===MONSTER_TYPE.SPIDER){
            anim.legs.forEach((leg,i)=>{
                const phase=i%2===0?anim.walkTime:anim.walkTime+Math.PI;
                leg.rotation.x=Math.sin(phase)*0.2;
            })
        }
    }
}

function buildAdvHouse(hx,hz){
    const w=7,d=8,wallH=3;
    const groundY = getGroundHeight(hx, hz);
    const baseY = groundY + 1;

    for(let x=-1;x<=w+1;x++){
        for(let z=-1;z<=d+1;z++){
            createBlock(hx+x, groundY, hz+z, 2);
        }
    }
    for(let y=0;y<=wallH;y++){
        for(let x=0;x<=w;x++){
            createBlock(hx+x, baseY+y, hz, 4);
            createBlock(hx+x, baseY+y, hz+d, 4);
        }
        for(let z=1;z<d;z++){
            createBlock(hx, baseY+y, hz+z, 4);
            createBlock(hx+w, baseY+y, hz+z, 4);
        }
    }
    createBlock(hx+3, baseY+1, hz, -1);
    createBlock(hx+3, baseY+2, hz, -1);
    createBlock(hx+1, baseY+2, hz, -1);
    createBlock(hx+5, baseY+2, hz, -1);
    createBlock(hx+1, baseY+2, hz+d, -1);
    createBlock(hx+5, baseY+2, hz+d, -1);

    for(let lay=0;lay<=3;lay++){
        const shrink = lay;
        for(let x=shrink;x<=w-shrink;x++){
            for(let z=shrink;z<=d-shrink;z++){
                createBlock(hx+x, baseY+wallH+1+lay, hz+z, 5);
            }
        }
    }
}

function buildFarm(fx,fz){
    const farmW=6,farmD=6;
    const groundY = getGroundHeight(fx, fz);
    for(let x=0;x<farmW;x++){
        for(let z=0;z<farmD;z++){
            createBlock(fx+x, groundY, fz+z, 1);
        }
    }
}

function buildWell(wx,wz){
    const groundY = getGroundHeight(wx, wz);
    createBlock(wx,groundY,wz,2);
    createBlock(wx+1,groundY,wz,2);
    createBlock(wx,groundY,wz+1,2);
    createBlock(wx+1,groundY,wz+1,2);
    createBlock(wx+0.5,groundY-1,wz+0.5,7);
}

function generateVillage(){
    const villageX = 22;
    const villageZ = -18;
    buildAdvHouse(villageX, villageZ);
    buildAdvHouse(villageX+9, villageZ+3);
    buildAdvHouse(villageX-8, villageZ+6);
    buildAdvHouse(villageX+5, villageZ-9);

    buildFarm(villageX+12, villageZ-4);
    buildFarm(villageX-9, villageZ-6);
    buildWell(villageX+4, villageZ-8);

    spawnVillager(6, villageX, villageZ);
}

function startGame(){
    scene=new THREE.Scene();
    scene.background=new THREE.Color(0x87CEEB);
    scene.fog=new THREE.Fog(0x87CEEB,40,120);

    camera=new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,500);
    camera.position.set(0,PLAYER_HEIGHT,12);

    renderer=new THREE.WebGLRenderer({antialias:false});
    renderer.setSize(window.innerWidth,window.innerHeight);
    renderer.shadowMap.enabled=false;
    document.body.appendChild(renderer.domElement);

    sunLight=new THREE.DirectionalLight(0xffffff,1);
    sunLight.position.set(40,60,30);
    scene.add(sunLight);

    ambientLight=new THREE.AmbientLight(0xffffff,0.4);
    scene.add(ambientLight);

    generateTerrain();
    spawnManyTrees(15);
    generateVillage();

    window.addEventListener("mousedown",e=>{
        if(e.button===0){attackMonster(performance.now());isDrag=true;lastX=e.clientX;lastY=e.clientY;}
    });
    document.addEventListener("mouseup",()=>isDrag=false);
    document.addEventListener("mousemove",e=>{
        if(!isDrag)return;
        const dx=e.clientX-lastX,dy=e.clientY-lastY;
        yaw+=dx*SENSITIVITY;pitch+=dy*SENSITIVITY;
        pitch=Math.max(-Math.PI/2+0.1,Math.min(Math.PI/2-0.1,pitch));
        lastX=e.clientX;lastY=e.clientY;
    });

    window.addEventListener("keydown",e=>{
        keyMap[e.code]=true;
        if(e.code==="Digit1")selectBlock=0;
        if(e.code==="Digit2")selectBlock=1;
        if(e.code==="Digit3")selectBlock=2;
        if(e.code==="Digit4")selectBlock=3;
        if(e.code==="Digit5")selectBlock=4;
        if(e.code==="Digit6")selectBlock=5;
        if(e.code==="Digit7")selectBlock=7;
        if(e.code==="Digit8")selectBlock=8;
        if(e.code==="Digit9")selectBlock=9;
    });
    window.addEventListener("keyup",e=>keyMap[e.code]=false);

    const raycaster=new THREE.Raycaster();
    renderer.domElement.addEventListener("mousedown",e=>{
        raycaster.setFromCamera(new THREE.Vector2(0,0),camera);
        const hits=raycaster.intersectObjects(scene.children,true);
        if(hits.length<=0)return;
        const hit=hits[0];
        // 向上追溯找到最外层父物体(火把Group)
        let targetObj = hit.object;
        while(targetObj.parent !== scene){
            targetObj = targetObj.parent;
        }

        const pos=hit.point.clone().add(hit.face.normal.clone().multiplyScalar(0.5));
        if(e.button===1)createBlock(Math.round(pos.x),Math.round(pos.y),Math.round(pos.z),selectBlock);
        if(e.button===2){
            const bx = Math.round(targetObj.position.x);
            const by = Math.round(targetObj.position.y);
            const bz = Math.round(targetObj.position.z);
            removeBlock(bx,by,bz);
        }
    });
    renderer.domElement.addEventListener("contextmenu",e=>e.preventDefault());
    loop();
}

function noise(x,z){
    return Math.sin(x*0.08)*6+Math.cos(z*0.08)*6+Math.sin((x+z)*0.05)*4;
}

function generateTerrain(){
    const MAP_SIZE=40;
    for(let x=-MAP_SIZE;x<=MAP_SIZE;x++){
        for(let z=-MAP_SIZE;z<=MAP_SIZE;z++){
            const h=Math.round(noise(x,z));
            for(let y=h;y>=h-3;y--)createBlock(x,y,z,1);
            createBlock(x,h,z,0);
            if(Math.random()<0.004)createBlock(x,h+1,z,2);
        }
    }
    const spawnH=getGroundHeight(0,0);
    camera.position.set(0,spawnH+PLAYER_HEIGHT,12);
}

function createBlock(x,y,z,matIndex){
    if(matIndex===-1) return;
    const key=`${x},${y},${z}`;
    if(blockWorld.has(key))return;

    let mesh;
    if(matIndex === 9){
        mesh = createTorchMesh();
        const torchLight = new THREE.PointLight(0xffbb55, 0.75, 14);
        torchLight.position.set(0,0.3,0);
        mesh.add(torchLight);
        torchLights.push(torchLight);
        mesh.torchLight = torchLight;
    }else{
        mesh=new THREE.Mesh(blockGeo,getBlockMaterial(matIndex));
    }

    mesh.position.set(x,y,z);
    mesh.castShadow=false;mesh.receiveShadow=false;
    mesh.blockType = matIndex;
    blockWorld.set(key,mesh);
    scene.add(mesh);

    if(matIndex === 8){
        const pointLight = new THREE.PointLight(0xff5500, 0.8, 12);
        pointLight.position.set(x,y,z);
        scene.add(pointLight);
        lavaLights.push(pointLight);
        mesh.lightRef = pointLight;
    }
}

function getBlockMaterial(index){
    switch(index){
        case 0:return createGrassMaterial();
        case 1:return matDirt;
        case 2:return matStone;
        case 3:return matLog;
        case 4:return matPlank;
        case 5:return matLeaves;
        case 7:return matWater;
        case 8:return matLava;
    }
}

function removeBlock(x,y,z){
    const key=`${x},${y},${z}`;
    if(blockWorld.has(key)){
        const mesh = blockWorld.get(key);
        if(mesh.torchLight){
            mesh.remove(mesh.torchLight);
            const idx = torchLights.indexOf(mesh.torchLight);
            if(idx>-1) torchLights.splice(idx,1);
        }
        if(mesh.lightRef){
            scene.remove(mesh.lightRef);
            const idx = lavaLights.indexOf(mesh.lightRef);
            if(idx>-1) lavaLights.splice(idx,1);
        }
        scene.remove(mesh);
        blockWorld.delete(key);
    }
}

function spawnTree(originX,originZ){
    const trunkHeight=4;
    const h = getGroundHeight(originX, originZ);
    for(let y=h;y<=h+trunkHeight-1;y++)createBlock(originX,y,originZ,3);
    const leafY=h+trunkHeight;
    for(let dx=-2;dx<=2;dx++){
        for(let dz=-2;dz<=2;dz++){
            for(let dy=0;dy<=2;dy++){
                if(Math.abs(dx)+Math.abs(dz)<=2)createBlock(originX+dx,leafY+dy,originZ+dz,5);
            }
        }
    }
}

function spawnManyTrees(count){
    for(let i=0;i<count;i++){
        const tx=Math.floor(Math.random()*70-35);
        const tz=Math.floor(Math.random()*70-35);
        spawnTree(tx,tz);
    }
}

function spawnMonsterGroup(count,type){
    for(let i=0;i<count;i++){
        const mx=Math.floor(Math.random()*60-30);
        const mz=Math.floor(Math.random()*60-30);
        const h=getGroundHeight(mx,mz);
        let mobRoot;
        if(type===MONSTER_TYPE.ZOMBIE)mobRoot=buildZombie();
        else if(type===MONSTER_TYPE.SKELETON)mobRoot=buildSkeleton();
        else mobRoot=buildSpider();
        mobRoot.position.set(mx,h,mz);
        monsterList.push(mobRoot);scene.add(mobRoot);
    }
}

function despawnAllMonsters(){
    monsterList.forEach(m=>scene.remove(m));
    monsterList.length=0;monsterAnimations.length=0;arrowList.length=0;
}

function hasBlock(x,z,y){
    return blockWorld.has(`${Math.round(x)},${Math.round(y)},${Math.round(z)}`);
}

function canMove(newX,newZ){
    const r=PLAYER_HALF_WIDTH;
    const floorY=camera.position.y-0.2;
    return !(hasBlock(newX-r,newZ-r,floorY)||hasBlock(newX+r,newZ-r,floorY)||hasBlock(newX-r,newZ+r,floorY)||hasBlock(newX+r,newZ+r,floorY));
}

function isOnGround(){
    return hasBlock(camera.position.x,camera.position.z,camera.position.y-1.05);
}

function attackMonster(time){
    if(time-lastAttackTime<ATTACK_COOLDOWN)return;
    lastAttackTime=time;
    for(let i=monsterList.length-1;i>=0;i--){
        const mob=monsterList[i];
        if(camera.position.distanceTo(mob.position)<ATTACK_RANGE){
            const idx=monsterAnimations.findIndex(a=>a.mob===mob);
            if(idx>-1)monsterAnimations.splice(idx,1);
            scene.remove(mob);monsterList.splice(i,1);break;
        }
    }
}

function updateMonsters(time){
    if(!isNight){despawnAllMonsters();return;}
    if(isNight&&!wasNightLastFrame){
        spawnMonsterGroup(2,MONSTER_TYPE.ZOMBIE);
        spawnMonsterGroup(2,MONSTER_TYPE.SKELETON);
        spawnMonsterGroup(2,MONSTER_TYPE.SPIDER);
    }

    for(const mob of monsterList){
        const dir=new THREE.Vector3().subVectors(camera.position,mob.position);dir.y=0;
        const dist=dir.length();
        const speed=mob.monsterType===MONSTER_TYPE.SPIDER?0.052:0.035;
        if(dist>1.8){
            dir.normalize();
            if(!hasBlock(mob.position.x+dir.x*speed,mob.position.z,0))mob.position.x+=dir.x*speed;
            if(!hasBlock(mob.position.x,mob.position.z+dir.z*speed,0))mob.position.z+=dir.z*speed;
        }

        if(mob.monsterType===MONSTER_TYPE.SKELETON&&dist<14&&dist>3){
            if(time-mob.lastShoot>2800){
                mob.lastShoot=time;
                const arrow=new THREE.Mesh(arrowGeo,arrowMat);
                arrow.position.copy(mob.position);arrow.position.y+=1.1;
                arrow.vel=new THREE.Vector3().subVectors(camera.position,arrow.position).normalize().multiplyScalar(0.22);
                arrowList.push(arrow);scene.add(arrow);
            }
        }

        if(dist<1.6&&time-lastDamageTime>800){
            hp-=10;hpText.textContent=hp;lastDamageTime=time;
            if(hp<=0)alert("死亡,刷新页面重开");
        }
        mob.lookAt(camera.position.x,mob.position.y,camera.position.z);
    }

    for(let i=arrowList.length-1;i>=0;i--){
        const arr=arrowList[i];
        arr.position.add(arr.vel);
        if(arr.position.distanceTo(camera.position)<1.2){
            if(time-lastDamageTime>800){hp-=10;hpText.textContent=hp;lastDamageTime=time;if(hp<=0)alert("死亡");}
            scene.remove(arr);arrowList.splice(i,1);
        }else if(arr.position.distanceTo(camera.position)>25){
            scene.remove(arr);arrowList.splice(i,1);
        }
    }
    updateMonsterAnimations();
}

function updateDayNight(){
    dayTime+=DAY_SPEED;
    const angle=dayTime%(Math.PI*2);
    sunLight.position.set(Math.cos(angle)*60,Math.sin(angle)*60,30);
    wasNightLastFrame=isNight;
    isNight=Math.sin(angle)<=0;
    dayTip.textContent=isNight?"时段:夜晚(刷怪)":"时段:白天(怪物清空)";

    let skyColor,sunIntensity,ambIntensity;
    if(!isNight){
        const bright=Math.sin(angle);
        sunIntensity=1*bright;ambIntensity=0.25+bright*0.25;
        skyColor=new THREE.Color(0x87CEEB);
    }else{
        const dark=-Math.sin(angle);
        sunIntensity=0;ambIntensity=0.08+dark*0.04;
        skyColor=new THREE.Color(0x0a1028);
    }
    scene.background=skyColor;scene.fog.color.copy(skyColor);
    sunLight.intensity=sunIntensity;ambientLight.intensity=ambIntensity;

    const pulse = Math.sin(dayTime * 15) * 0.3 + 0.7;
    lavaLights.forEach(light=>{
        light.intensity = 0.8 * pulse;
    })
    torchLights.forEach(light=>{
        light.intensity = (0.65 + Math.sin(dayTime * 20 + light.parent.position.x) * 0.15);
    })
}

function loop(time=0){
    requestAnimationFrame(loop);
    updateDayNight();
    camera.rotation.order="YXZ";
    camera.rotation.y=yaw;camera.rotation.x=pitch;

    const forward=new THREE.Vector3(0,0,-1).applyQuaternion(camera.quaternion);forward.y=0;forward.normalize();
    const right=new THREE.Vector3(1,0,0).applyQuaternion(camera.quaternion);
    let moveVec=new THREE.Vector3();
    if(keyMap["KeyW"])moveVec.add(forward);
    if(keyMap["KeyS"])moveVec.add(forward.clone().multiplyScalar(-1));
    if(keyMap["KeyA"])moveVec.add(right.clone().multiplyScalar(-1));
    if(keyMap["KeyD"])moveVec.add(right);
    if(moveVec.length()>0)moveVec.normalize().multiplyScalar(MOVE_SPEED);

    const nx=camera.position.x+moveVec.x;
    const nz=camera.position.z+moveVec.z;
    if(canMove(nx,camera.position.z))camera.position.x=nx;
    if(canMove(camera.position.x,nz))camera.position.z=nz;

    const onGround=isOnGround();
    if(onGround){velocityY=0;if(keyMap["Space"])velocityY=JUMP_FORCE;}
    else velocityY+=GRAVITY;
    camera.position.y+=velocityY;

    updateMonsters(time);
    updateVillagers();
    renderer.render(scene,camera);
}

window.addEventListener("resize",()=>{
    camera.aspect=window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth,window.innerHeight, false);
});

startGame();
</script>
</body>
</html>

Game Source: MC网页沙盒|可破坏方块火把修复版

Creator: DriftTurtle56

Libraries: three

Complexity: complex (766 lines, 26.8 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-driftturtle56" to link back to the original. Then publish at arcadelab.ai/publish.