像素世界 · 55 方块
by ElectricShark88941 lines34.5 KB
<!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>像素世界 · 55 方块</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none;touch-action:none}
html,body{width:100%;height:100%;overflow:hidden;background:#87c5ff;font-family:system-ui,-apple-system,sans-serif}
canvas{display:block;width:100%;height:100%}
#ui{position:fixed;inset:0;pointer-events:none;z-index:10}
#joyBase{position:absolute;left:20px;bottom:30px;width:130px;height:130px;border-radius:50%;background:rgba(255,255,255,.12);border:2px solid rgba(255,255,255,.3);pointer-events:auto;touch-action:none}
#joyKnob{position:absolute;left:50%;top:50%;width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,.6);transform:translate(-50%,-50%);box-shadow:0 2px 10px rgba(0,0,0,.3)}
.rBtn{position:absolute;border-radius:50%;background:rgba(255,255,255,.15);border:2px solid rgba(255,255,255,.45);color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;pointer-events:auto;text-shadow:0 1px 4px rgba(0,0,0,.6);touch-action:none;font-size:15px}
.rBtn:active{background:rgba(255,255,255,.45)}
#btnJump{right:24px;bottom:120px;width:80px;height:80px;font-size:18px}
#btnDig{right:120px;bottom:30px;width:70px;height:70px}
#btnPlace{right:30px;bottom:30px;width:70px;height:70px}
#btnBag{right:24px;top:60px;width:60px;height:60px;font-size:13px;border-radius:12px}
#hotbar{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;gap:4px;pointer-events:auto;background:rgba(0,0,0,.35);padding:4px;border-radius:8px}
.slot{width:40px;height:40px;border-radius:4px;border:2px solid rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.slot.active{border-color:#fff;box-shadow:0 0 0 2px rgba(255,255,255,.5)}
.slot canvas{width:32px;height:32px;image-rendering:pixelated}
#bagPanel{position:absolute;inset:0;background:rgba(0,0,0,.75);display:none;align-items:center;justify-content:center;pointer-events:auto;z-index:20}
#bagPanel.open{display:flex}
#bagBox{width:92%;max-width:520px;max-height:80%;background:#2a2a35;border:2px solid #555;border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
#bagTitle{padding:14px 16px;font-size:15px;font-weight:700;color:#eee;border-bottom:1px solid #444;display:flex;justify-content:space-between;align-items:center}
#bagClose{background:#444;border:none;color:#fff;border-radius:6px;padding:6px 14px;font-size:13px}
#bagGrid{flex:1;overflow-y:auto;padding:10px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.bagItem{display:flex;flex-direction:column;align-items:center;gap:3px;padding:6px 3px;background:#1e1e28;border-radius:8px;border:2px solid transparent}
.bagItem:active{background:#333;border-color:#7dff5a}
.bagItem canvas{width:36px;height:36px;image-rendering:pixelated;border-radius:3px}
.bagItem span{font-size:9px;color:#ccc;text-align:center;line-height:1.1}
#hint{position:absolute;top:calc(12px + env(safe-area-inset-top));left:50%;transform:translateX(-50%);background:rgba(0,0,0,.45);color:#fff;padding:6px 16px;border-radius:16px;font-size:12px;white-space:nowrap;pointer-events:none}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
<div id="hint">摇杆移动 · 滑动转视角 · 挖/放/背包</div>
<div id="joyBase"><div id="joyKnob"></div></div>
<div class="rBtn" id="btnJump">跳</div>
<div class="rBtn" id="btnDig">挖</div>
<div class="rBtn" id="btnPlace">放</div>
<div class="rBtn" id="btnBag">背包</div>
<div id="hotbar"></div>
</div>
<div id="bagPanel">
<div id="bagBox">
<div id="bagTitle"><span>方块背包 · 55 种</span><button id="bagClose">关闭</button></div>
<div id="bagGrid"></div>
</div>
</div>
<script>
(function(){
'use strict';
/* ========== 画布 ========== */
const canvas = document.getElementById('game');
const gl = canvas.getContext('webgl', { antialias: true });
function resize(){
const dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = innerWidth * dpr;
canvas.height = innerHeight * dpr;
canvas.style.width = innerWidth + 'px';
canvas.style.height = innerHeight + 'px';
gl.viewport(0, 0, canvas.width, canvas.height);
}
addEventListener('resize', resize);
resize();
/* ========== 纹理图集 ========== */
const TILE = 16, COLS = 8, ROWS = 8, ATLAS_SIZE = TILE * COLS;
function rnd(x, y, s){
let h = (x * 374761393 + y * 668265263 + s * 1442695041) | 0;
h = (h ^ (h >> 13)) * 1274126177;
return ((h ^ (h >> 16)) >>> 0) / 4294967295;
}
function clamp(v){ return v < 0 ? 0 : v > 255 ? 255 : Math.round(v); }
function buildAtlas(){
const c = document.createElement('canvas');
c.width = c.height = ATLAS_SIZE;
const ctx = c.getContext('2d');
const img = ctx.createImageData(ATLAS_SIZE, ATLAS_SIZE);
const d = img.data;
function put(tile, lx, ly, r, g, b, a){
if(a === undefined) a = 255;
const cx = (tile % COLS) * TILE + lx;
const cy = Math.floor(tile / COLS) * TILE + ly;
if(cx < 0 || cx >= ATLAS_SIZE || cy < 0 || cy >= ATLAS_SIZE) return;
const i = (cy * ATLAS_SIZE + cx) * 4;
d[i] = clamp(r); d[i+1] = clamp(g); d[i+2] = clamp(b); d[i+3] = a;
}
function noise(tile, r, g, b, v, seed, extra){
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,seed);
let k = 1 - v/2 + n*v;
if(extra) k *= extra(x,y);
put(tile,x,y, r*k, g*k, b*k);
}
}
function ore(tile, seed, r, g, b){
noise(tile, 128, 128, 128, 0.5, seed);
for(let k=0;k<7;k++){
const cx = Math.floor(rnd(k,0,seed+1)*TILE);
const cy = Math.floor(rnd(k,1,seed+1)*TILE);
for(let dx=-1;dx<=1;dx++) for(let dy=-1;dy<=1;dy++){
if(Math.abs(dx)+Math.abs(dy) > 1) continue;
const n = rnd(cx+dx,cy+dy,seed+2);
put(tile, cx+dx, cy+dy, r*(0.8+n*0.4), g*(0.8+n*0.4), b*(0.8+n*0.4));
}
}
}
function metalBlock(tile, r, g, b){
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,tile*7+1), k = 0.85+n*0.2;
const edge = (x<2||x>13||y<2||y>13) ? 0.78 : 1;
put(tile,x,y, r*k*edge, g*k*edge, b*k*edge);
}
}
function wool(tile, r, g, b, seed){
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,seed), k = 0.85+n*0.2;
const fiber = (y%2===0) ? 1.05 : 0.95;
put(tile,x,y, r*k*fiber, g*k*fiber, b*k*fiber);
}
}
// 0 草顶 / 1 草侧 / 2 泥土
noise(0, 95, 159, 53, 0.35, 1);
for(let x=0;x<TILE;x++){
const gh = 4 + Math.floor(rnd(x,0,2)*3);
for(let y=0;y<TILE;y++){
const n = rnd(x,y,3);
if(y < gh){ const k=0.8+n*0.3; put(1,x,y, 95*k,159*k,53*k); }
else { const k=0.8+n*0.4; put(1,x,y, 139*k,90*k,43*k); }
}
}
noise(2, 139, 90, 43, 0.4, 4);
// 3 石头
noise(3, 128, 128, 128, 0.5, 5);
// 4 沙子
noise(4, 219, 207, 163, 0.24, 6);
// 5 原木侧
noise(5, 102, 74, 48, 0.2, 7, (x)=>1+Math.sin(x*1.4)*0.12);
// 6 原木顶
noise(6, 102, 74, 48, 0.2, 8, (x,y)=>{
const dx=x-7.5, dy=y-7.5;
return 1+Math.sin(Math.sqrt(dx*dx+dy*dy)*1.8)*0.14;
});
// 7 树叶(带镂空)
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,9);
if(n < 0.12) put(7,x,y, 0,0,0, 0);
else { const k=0.6+n*0.5; put(7,x,y, 45*k,110*k,30*k); }
}
// 8 玻璃
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const edge = (x===0||x===15||y===0||y===15);
if(edge) put(8,x,y, 200,230,245, 255);
else if(rnd(x,y,10) > 0.88) put(8,x,y, 220,240,255, 110);
else put(8,x,y, 0,0,0, 0);
}
// 9 砖块
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const row = Math.floor(y/4);
const offset = row%2 ? 4 : 0;
const mortar = (y%4===0) || ((x+offset)%8===0);
const n = rnd(x,y,11);
if(mortar) put(9,x,y, 200,200,200);
else { const k=0.8+n*0.35; put(9,x,y, 165*k,75*k,60*k); }
}
// 10 木板
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,12);
const k = y%4===0 ? 0.6 : 0.85+n*0.3;
put(10,x,y, 190*k, 148*k, 90*k);
}
// 11 水
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,13), k = 0.9+n*0.15;
put(11,x,y, 45*k, 100*k, 200*k, 210);
}
// 12 雪
noise(12, 240, 248, 255, 0.12, 14);
// 13-16 矿石
ore(13, 20, 30, 30, 30);
ore(14, 22, 200, 163, 126);
ore(15, 24, 232, 192, 74);
ore(16, 26, 74, 217, 217);
// 17 圆石
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,28);
let k = 0.72+n*0.5;
if(rnd(Math.floor(x/3),Math.floor(y/3),29) > 0.62) k *= 0.7;
put(17,x,y, 130*k,130*k,130*k);
}
// 18 基岩
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,30), k = 0.5 + n*0.8;
put(18,x,y, 60*k, 60*k, 60*k);
}
// 19 砂砾
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,31), g = 100 + n*60;
put(19,x,y, g, g*0.9, g*0.8);
}
// 20 黏土
noise(20, 160, 180, 200, 0.2, 32);
// 21 冰
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,33), k = 0.9 + n*0.15;
put(21,x,y, 168*k, 216*k, 240*k, 220);
}
// 22 黑曜石
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,34), k = 0.6 + n*0.4;
put(22,x,y, 25*k, 20*k, 35*k);
}
// 23 地狱岩
noise(23, 110, 50, 50, 0.4, 35);
// 24 萤石
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,36);
const glow = rnd(Math.floor(x/2), Math.floor(y/2), 37);
if(glow > 0.5){ const k=0.9+n*0.15; put(24,x,y, 255*k, 220*k, 100*k); }
else { const k=0.6+n*0.3; put(24,x,y, 130*k, 90*k, 50*k); }
}
// 25-27 更多矿石
ore(25, 40, 30, 60, 180);
ore(26, 42, 200, 30, 30);
ore(27, 44, 40, 200, 80);
// 28 石英
noise(28, 235, 230, 225, 0.1, 45);
// 29-34 羊毛
wool(29, 240, 240, 240, 50);
wool(30, 200, 40, 40, 51);
wool(31, 50, 80, 200, 52);
wool(32, 60, 160, 60, 53);
wool(33, 230, 210, 60, 54);
wool(34, 40, 40, 40, 55);
// 35 干草块
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,60), k = 0.8 + n*0.3;
const stripe = (x%4===0) ? 0.8 : 1;
put(35,x,y, 200*k*stripe, 170*k*stripe, 50*k*stripe);
}
// 36 书架
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,61);
if(y < 2 || y > 13){
const k = 0.8 + n*0.2;
put(36,x,y, 160*k, 110*k, 60*k);
} else {
const bc = rnd(Math.floor(x/3), Math.floor(y/6), 62);
let r=150, g=60, b=60;
if(bc > 0.7){ r=60; g=100; b=160; }
else if(bc > 0.4){ r=60; g=140; b=80; }
const k = 0.8 + n*0.3;
put(36,x,y, r*k, g*k, b*k);
}
}
// 37 工作台
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,63), k = 0.8 + n*0.25;
if(y < 3) put(37,x,y, 110*k, 70*k, 40*k);
else if(y > 12) put(37,x,y, 90*k, 55*k, 30*k);
else {
const grid = (x%5===0 || y%5===0) ? 0.7 : 1;
put(37,x,y, 190*k*grid, 150*k*grid, 100*k*grid);
}
}
// 38 熔炉
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,64), k = 0.7 + n*0.3;
if(x > 4 && x < 11 && y > 6 && y < 13){
const fire = rnd(x,y,65);
if(fire > 0.5) put(38,x,y, 255, 150, 50);
else put(38,x,y, 50*k, 50*k, 50*k);
} else put(38,x,y, 110*k, 110*k, 110*k);
}
// 39 南瓜
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,66), k = 0.85 + n*0.2;
const stripe = (x%4<2) ? 1 : 0.85;
const eyeL = (x>3 && x<6 && y>4 && y<7);
const eyeR = (x>9 && x<12 && y>4 && y<7);
const mouth = (y>9 && y<12 && x>4 && x<11);
if(eyeL || eyeR || mouth) put(39,x,y, 40, 25, 10);
else put(39,x,y, 230*k*stripe, 130*k*stripe, 30*k*stripe);
}
// 40 西瓜
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,67), k = 0.85 + n*0.2;
const stripe = (x%4<2) ? 1.1 : 0.75;
put(40,x,y, 40*k*stripe, 140*k*stripe, 40*k*stripe);
}
// 41 苔石
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,68);
let k = 0.72+n*0.5;
if(rnd(Math.floor(x/3),Math.floor(y/3),69) > 0.62) k *= 0.7;
if(rnd(Math.floor(x/2), Math.floor(y/2), 70) > 0.7) put(41,x,y, 60*k, 120*k, 50*k);
else put(41,x,y, 130*k,130*k,130*k);
}
// 42 石砖
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,71), k = 0.75+n*0.3;
const mortar = (y%8===0) || ((x+(Math.floor(y/8)*8))%8===0);
put(42,x,y, (mortar?100:140)*k, (mortar?100:140)*k, (mortar?100:140)*k);
}
// 43 地狱砖
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,72), k = 0.8+n*0.2;
const mortar = (y%4===0) || (x%8===0);
put(43,x,y, (mortar?60:100)*k, (mortar?25:35)*k, (mortar?25:35)*k);
}
// 44 末地石
noise(44, 220, 220, 180, 0.2, 73);
// 45 紫珀块
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,74), k = 0.85+n*0.2;
const pattern = ((x+y)%8 < 2) ? 0.85 : 1;
put(45,x,y, 170*k*pattern, 120*k*pattern, 180*k*pattern);
}
// 46 海晶石
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,75), k = 0.8+n*0.3;
const pattern = ((x*2+y*3)%7 < 2) ? 1.15 : 0.9;
put(46,x,y, 60*k*pattern, 180*k*pattern, 160*k*pattern);
}
// 47 砂岩
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,76), k = 0.85+n*0.2;
const band = (y%5===0) ? 0.85 : 1;
put(47,x,y, 220*k*band, 200*k*band, 140*k*band);
}
// 48-54 金属/矿物块
metalBlock(48, 200, 200, 200);
metalBlock(49, 240, 210, 60);
metalBlock(50, 80, 220, 220);
metalBlock(51, 50, 200, 100);
metalBlock(52, 40, 60, 180);
metalBlock(53, 200, 40, 40);
metalBlock(54, 40, 40, 40);
// 55 骨块
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,84), k = 0.9+n*0.15;
const bone = (x%4===0 && y%4===0) ? 0.8 : 1;
put(55,x,y, 235*k*bone, 230*k*bone, 200*k*bone);
}
// 56 海绵
for(let y=0;y<TILE;y++) for(let x=0;x<TILE;x++){
const n = rnd(x,y,85), k = 0.8+n*0.3;
if(rnd(Math.floor(x/2), Math.floor(y/2), 86) > 0.65)
put(56,x,y, 120*k, 140*k, 60*k);
else put(56,x,y, 200*k, 210*k, 80*k);
}
ctx.putImageData(img, 0, 0);
return c;
}
const atlasCanvas = buildAtlas();
/* ========== 纹理上传 ========== */
const glTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, glTex);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlasCanvas);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
/* ========== 着色器 ========== */
const vsSrc = `
attribute vec3 aPos;
attribute vec2 aUV;
attribute float aLight;
uniform mat4 uMVP;
varying vec2 vUV;
varying float vLight;
void main(){
gl_Position = uMVP * vec4(aPos, 1.0);
vUV = aUV;
vLight = aLight;
}`;
const fsSrc = `
precision mediump float;
varying vec2 vUV;
varying float vLight;
uniform sampler2D uTex;
void main(){
vec4 c = texture2D(uTex, vUV);
if(c.a < 0.5) discard;
gl_FragColor = vec4(c.rgb * vLight, 1.0);
}`;
function compile(type, src){
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.error(gl.getShaderInfoLog(s));
return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(prog);
gl.useProgram(prog);
const aPos = gl.getAttribLocation(prog, 'aPos');
const aUV = gl.getAttribLocation(prog, 'aUV');
const aLight = gl.getAttribLocation(prog, 'aLight');
const uMVP = gl.getUniformLocation(prog, 'uMVP');
gl.uniform1i(gl.getUniformLocation(prog, 'uTex'), 0);
gl.clearColor(0.53, 0.77, 1.0, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
/* ========== 矩阵 ========== */
function perspective(fovy, aspect, near, far){
const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
const o = new Float32Array(16);
o[0] = f/aspect; o[5] = f;
o[10] = (far+near)*nf; o[11] = -1;
o[14] = 2*far*near*nf;
return o;
}
function lookAt(eye, target, up){
let zx = eye[0]-target[0], zy = eye[1]-target[1], zz = eye[2]-target[2];
let l = Math.hypot(zx,zy,zz); if(l<1e-6){zx=0;zy=0;zz=1;l=1;}
zx/=l; zy/=l; zz/=l;
let xx = up[1]*zz-up[2]*zy, xy = up[2]*zx-up[0]*zz, xz = up[0]*zy-up[1]*zx;
l = Math.hypot(xx,xy,xz); if(l<1e-6){xx=1;xy=0;xz=0;l=1;}
xx/=l; xy/=l; xz/=l;
const yx = zy*xz-zz*xy, yy = zz*xx-zx*xz, yz = zx*xy-zy*xx;
const o = new Float32Array(16);
o[0]=xx;o[1]=yx;o[2]=zx;o[3]=0;
o[4]=xy;o[5]=yy;o[6]=zy;o[7]=0;
o[8]=xz;o[9]=yz;o[10]=zz;o[11]=0;
o[12]=-(xx*eye[0]+xy*eye[1]+xz*eye[2]);
o[13]=-(yx*eye[0]+yy*eye[1]+yz*eye[2]);
o[14]=-(zx*eye[0]+zy*eye[1]+zz*eye[2]);
o[15]=1;
return o;
}
function mul(a,b){
const o = new Float32Array(16);
for(let c=0;c<4;c++) for(let r=0;r<4;r++)
o[c*4+r] = a[r]*b[c*4] + a[4+r]*b[c*4+1] + a[8+r]*b[c*4+2] + a[12+r]*b[c*4+3];
return o;
}
/* ========== 方块定义(55 种) ========== */
const W = 24, D = 24, H = 16;
const world = new Uint8Array(W*D*H);
const B = {
AIR:0, GRASS:1, DIRT:2, STONE:3, SAND:4, LOG:5, LEAVES:6, GLASS:7,
BRICK:8, PLANK:9, WATER:10, SNOW:11, COAL:12, IRON:13, GOLD:14,
DIAMOND:15, COBBLE:16, BEDROCK:17, GRAVEL:18, CLAY:19, ICE:20,
OBSIDIAN:21, NETHERRACK:22, GLOWSTONE:23, LAPIS:24, REDSTONE:25,
EMERALD:26, QUARTZ:27, WOOL_WHITE:28, WOOL_RED:29, WOOL_BLUE:30,
WOOL_GREEN:31, WOOL_YELLOW:32, WOOL_BLACK:33, HAY:34, BOOKSHELF:35,
CRAFTING:36, FURNACE:37, PUMPKIN:38, MELON:39, MOSSY_COBBLE:40,
STONE_BRICK:41, NETHER_BRICK:42, END_STONE:43, PURPUR:44,
PRISMARINE:45, SANDSTONE:46, IRON_BLOCK:47, GOLD_BLOCK:48,
DIAMOND_BLOCK:49, EMERALD_BLOCK:50, LAPIS_BLOCK:51, REDSTONE_BLOCK:52,
COAL_BLOCK:53, BONE:54, SPONGE:55
};
const T = (all)=>({ tex:[all,all,all,all,all,all] });
const BLOCK_INFO = {
1: { name:'草方块', tex:[1,1,0,2,1,1] },
2: { name:'泥土', tex:[2,2,2,2,2,2] },
3: { name:'石头', tex:[3,3,3,3,3,3] },
4: { name:'沙子', tex:[4,4,4,4,4,4] },
5: { name:'原木', tex:[5,5,6,6,5,5] },
6: { name:'树叶', tex:[7,7,7,7,7,7] },
7: { name:'玻璃', tex:[8,8,8,8,8,8] },
8: { name:'砖块', tex:[9,9,9,9,9,9] },
9: { name:'木板', tex:[10,10,10,10,10,10] },
10:{ name:'水', tex:[11,11,11,11,11,11] },
11:{ name:'雪块', tex:[12,12,12,12,12,12] },
12:{ name:'煤矿石', tex:[13,13,13,13,13,13] },
13:{ name:'铁矿石', tex:[14,14,14,14,14,14] },
14:{ name:'金矿石', tex:[15,15,15,15,15,15] },
15:{ name:'钻石矿', tex:[16,16,16,16,16,16] },
16:{ name:'圆石', tex:[17,17,17,17,17,17] },
17:{ name:'基岩', tex:[18,18,18,18,18,18] },
18:{ name:'砂砾', tex:[19,19,19,19,19,19] },
19:{ name:'黏土', tex:[20,20,20,20,20,20] },
20:{ name:'冰', tex:[21,21,21,21,21,21] },
21:{ name:'黑曜石', tex:[22,22,22,22,22,22] },
22:{ name:'地狱岩', tex:[23,23,23,23,23,23] },
23:{ name:'萤石', tex:[24,24,24,24,24,24] },
24:{ name:'青金石矿', tex:[25,25,25,25,25,25] },
25:{ name:'红石矿', tex:[26,26,26,26,26,26] },
26:{ name:'绿宝石矿', tex:[27,27,27,27,27,27] },
27:{ name:'石英块', tex:[28,28,28,28,28,28] },
28:{ name:'白色羊毛', tex:[29,29,29,29,29,29] },
29:{ name:'红色羊毛', tex:[30,30,30,30,30,30] },
30:{ name:'蓝色羊毛', tex:[31,31,31,31,31,31] },
31:{ name:'绿色羊毛', tex:[32,32,32,32,32,32] },
32:{ name:'黄色羊毛', tex:[33,33,33,33,33,33] },
33:{ name:'黑色羊毛', tex:[34,34,34,34,34,34] },
34:{ name:'干草块', tex:[35,35,35,35,35,35] },
35:{ name:'书架', tex:[36,36,36,36,36,36] },
36:{ name:'工作台', tex:[37,37,37,37,37,37] },
37:{ name:'熔炉', tex:[38,38,38,38,38,38] },
38:{ name:'南瓜', tex:[39,39,39,39,39,39] },
39:{ name:'西瓜', tex:[40,40,40,40,40,40] },
40:{ name:'苔石', tex:[41,41,41,41,41,41] },
41:{ name:'石砖', tex:[42,42,42,42,42,42] },
42:{ name:'地狱砖', tex:[43,43,43,43,43,43] },
43:{ name:'末地石', tex:[44,44,44,44,44,44] },
44:{ name:'紫珀块', tex:[45,45,45,45,45,45] },
45:{ name:'海晶石', tex:[46,46,46,46,46,46] },
46:{ name:'砂岩', tex:[47,47,47,47,47,47] },
47:{ name:'铁块', tex:[48,48,48,48,48,48] },
48:{ name:'金块', tex:[49,49,49,49,49,49] },
49:{ name:'钻石块', tex:[50,50,50,50,50,50] },
50:{ name:'绿宝石块', tex:[51,51,51,51,51,51] },
51:{ name:'青金石块', tex:[52,52,52,52,52,52] },
52:{ name:'红石块', tex:[53,53,53,53,53,53] },
53:{ name:'煤矿块', tex:[54,54,54,54,54,54] },
54:{ name:'骨块', tex:[55,55,55,55,55,55] },
55:{ name:'海绵', tex:[56,56,56,56,56,56] }
};
const TRANSPARENT = new Set([0, 6, 7, 10, 20]);
function idx(x,y,z){ return (y*D + z)*W + x; }
function getBlock(x,y,z){
if(x<0||x>=W||y<0||y>=H||z<0||z>=D) return 0;
return world[idx(x,y,z)];
}
function setBlock(x,y,z,id){
if(x<0||x>=W||y<0||y>=H||z<0||z>=D) return;
world[idx(x,y,z)] = id;
}
function isTransparent(id){ return TRANSPARENT.has(id); }
/* ========== 世界生成 ========== */
function generateWorld(){
for(let x=0;x<W;x++) for(let z=0;z<D;z++){
const h = Math.max(2, Math.min(H-2, Math.floor(
6 + Math.sin(x*0.3)*2 + Math.cos(z*0.35)*2 + Math.sin((x+z)*0.2)*1.5
)));
for(let y=0;y<=h;y++){
let id = B.STONE;
if(y === h) id = h > 10 ? B.SNOW : (h < 5 ? B.SAND : B.GRASS);
else if(y >= h-2) id = B.DIRT;
else {
const r = Math.random();
if(r > 0.98) id = B.DIAMOND;
else if(r > 0.965) id = B.EMERALD;
else if(r > 0.95) id = B.GOLD;
else if(r > 0.92) id = B.LAPIS;
else if(r > 0.89) id = B.REDSTONE;
else if(r > 0.86) id = B.IRON;
else if(r > 0.82) id = B.COAL;
}
setBlock(x,y,z,id);
}
if(h < 5) for(let y=h+1;y<=5;y++) setBlock(x,y,z,B.WATER);
}
// 树
for(let i=0;i<18;i++){
const tx = 2+Math.floor(Math.random()*(W-4));
const tz = 2+Math.floor(Math.random()*(D-4));
let ty = H-1;
while(ty > 0 && getBlock(tx,ty,tz) === 0) ty--;
if(getBlock(tx,ty,tz) !== B.GRASS) continue;
const th = 3 + Math.floor(Math.random()*2);
for(let j=1;j<=th;j++) setBlock(tx,ty+j,tz,B.LOG);
const top = ty + th;
for(let dx=-2;dx<=2;dx++) for(let dz=-2;dz<=2;dz++) for(let dy=-1;dy<=2;dy++){
if(Math.abs(dx)+Math.abs(dz)+Math.abs(dy) > 3) continue;
const bx=tx+dx, by=top+dy, bz=tz+dz;
if(bx>=0&&bx<W&&by>=0&&by<H&&bz>=0&&bz<D && getBlock(bx,by,bz)===0){
setBlock(bx,by,bz,B.LEAVES);
}
}
}
// 地表装饰:随机散布各种方块,让世界更有细节
const decor = [B.HAY, B.BOOKSHELF, B.CRAFTING, B.FURNACE, B.PUMPKIN, B.MELON,
B.WOOL_WHITE, B.WOOL_RED, B.WOOL_BLUE, B.WOOL_GREEN, B.WOOL_YELLOW,
B.BONE, B.SPONGE, B.GLOWSTONE, B.QUARTZ, B.PURPUR, B.PRISMARINE,
B.NETHERRACK, B.OBSIDIAN, B.ICE, B.CLAY, B.GRAVEL, B.MOSSY_COBBLE,
B.STONE_BRICK, B.NETHER_BRICK, B.END_STONE, B.SANDSTONE,
B.IRON_BLOCK, B.GOLD_BLOCK, B.DIAMOND_BLOCK, B.EMERALD_BLOCK,
B.LAPIS_BLOCK, B.REDSTONE_BLOCK, B.COAL_BLOCK];
for(let i=0;i<40;i++){
const x = Math.floor(Math.random()*W);
const z = Math.floor(Math.random()*D);
let y = H-1;
while(y > 0 && getBlock(x,y,z) === 0) y--;
if(y < 1) continue;
const id = decor[Math.floor(Math.random()*decor.length)];
setBlock(x, y+1, z, id);
}
}
/* ========== 网格构建 ========== */
const FACES = {
px: { v:[[1,0,0],[1,0,1],[1,1,1],[1,1,0]], tc:[[0,0],[1,0],[1,1],[0,1]], light:0.85, idx:0 },
nx: { v:[[0,0,0],[0,1,0],[0,1,1],[0,0,1]], tc:[[0,0],[0,1],[1,1],[1,0]], light:0.75, idx:1 },
py: { v:[[0,1,0],[0,1,1],[1,1,1],[1,1,0]], tc:[[0,0],[1,0],[1,1],[0,1]], light:1.0, idx:2 },
ny: { v:[[0,0,0],[1,0,0],[1,0,1],[0,0,1]], tc:[[0,0],[1,0],[1,1],[0,1]], light:0.55, idx:3 },
pz: { v:[[0,0,1],[0,1,1],[1,1,1],[1,0,1]], tc:[[0,0],[0,1],[1,1],[1,0]], light:0.9, idx:4 },
nz: { v:[[0,0,0],[1,0,0],[1,1,0],[0,1,0]], tc:[[0,0],[1,0],[1,1],[0,1]], light:0.7, idx:5 },
};
const FACE_KEYS = ['px','nx','py','ny','pz','nz'];
const FACE_OFFSETS = {
px:[1,0,0], nx:[-1,0,0], py:[0,1,0], ny:[0,-1,0], pz:[0,0,1], nz:[0,0,-1]
};
function tileUV(tile){
const col = tile % COLS, row = Math.floor(tile / COLS);
return {
u0: (col * TILE) / ATLAS_SIZE,
u1: ((col + 1) * TILE) / ATLAS_SIZE,
v0: 1 - ((row + 1) * TILE) / ATLAS_SIZE,
v1: 1 - (row * TILE) / ATLAS_SIZE
};
}
const MAX_VERTS = 300000;
let vBuf = new Float32Array(MAX_VERTS * 6);
let vCount = 0;
function addFace(x, y, z, faceKey, tile){
const face = FACES[faceKey];
const tu = tileUV(tile);
const order = [0,1,2, 0,2,3];
for(const oi of order){
if(vCount >= MAX_VERTS) return;
const c = face.v[oi];
const tc = face.tc[oi];
const off = vCount * 6;
vBuf[off] = x + c[0];
vBuf[off+1] = y + c[1];
vBuf[off+2] = z + c[2];
vBuf[off+3] = tu.u0 + tc[0]*(tu.u1-tu.u0);
vBuf[off+4] = tu.v0 + tc[1]*(tu.v1-tu.v0);
vBuf[off+5] = face.light;
vCount++;
}
}
function buildMesh(){
vCount = 0;
for(let y=0;y<H;y++) for(let z=0;z<D;z++) for(let x=0;x<W;x++){
const id = getBlock(x,y,z);
if(id === 0) continue;
const info = BLOCK_INFO[id];
if(!info) continue;
for(let fi=0; fi<6; fi++){
const fk = FACE_KEYS[fi];
const off = FACE_OFFSETS[fk];
const nb = getBlock(x+off[0], y+off[1], z+off[2]);
if(!isTransparent(nb)) continue;
if(nb === id) continue;
addFace(x, y, z, fk, info.tex[fi]);
}
}
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, vBuf.subarray(0, vCount*6));
}
/* ========== VBO ========== */
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vBuf.byteLength, gl.DYNAMIC_DRAW);
/* ========== 玩家 ========== */
let px = W/2, py = 10, pz = D/2;
let vy = 0, yaw = 0, pitch = -0.15;
let onGround = false, jumpHeld = false;
function isSolidAt(x,y,z){
const id = getBlock(Math.floor(x), Math.floor(y), Math.floor(z));
return id !== 0 && id !== B.GLASS && id !== B.WATER && id !== B.LEAVES && id !== B.ICE;
}
function collides(x,y,z){
const r = 0.3;
for(let bx=Math.floor(x-r); bx<=Math.floor(x+r); bx++)
for(let by=Math.floor(y); by<=Math.floor(y+1.7); by++)
for(let bz=Math.floor(z-r); bz<=Math.floor(z+r); bz++)
if(isSolidAt(bx,by,bz)) return true;
return false;
}
function updatePlayer(dt){
const speed = 4.5;
const sy = Math.sin(yaw), cy = Math.cos(yaw);
let mx = 0, mz = 0;
if(joy.active){
mx = -sy * (-joy.y) + cy * joy.x;
mz = -cy * (-joy.y) + (-sy) * joy.x;
}
const mag = Math.hypot(mx, mz);
if(mag > 1){ mx /= mag; mz /= mag; }
const dx = mx * speed * dt, dz = mz * speed * dt;
if(dx !== 0 && !collides(px+dx, py, pz)) px += dx;
if(dz !== 0 && !collides(px, py, pz+dz)) pz += dz;
vy -= 25 * dt;
if(vy < -30) vy = -30;
const prevY = py;
if(!collides(px, py + vy*dt, pz)){
py += vy * dt;
onGround = false;
} else {
if(vy < 0){ py = prevY; onGround = true; }
vy = 0;
}
if(jumpHeld && onGround){ vy = 8; onGround = false; jumpHeld = false; }
px = Math.max(0.5, Math.min(W-0.5, px));
pz = Math.max(0.5, Math.min(D-0.5, pz));
if(py < -10){ px = W/2; py = 15; pz = D/2; vy = 0; }
}
/* ========== 射线 ========== */
function raycast(){
const eye = [px, py+1.62, pz];
const cp = Math.cos(pitch), sp = Math.sin(pitch);
const dir = [-Math.sin(yaw)*cp, sp, -Math.cos(yaw)*cp];
let last = null;
for(let t=0; t<6; t+=0.05){
const bx = Math.floor(eye[0]+dir[0]*t);
const by = Math.floor(eye[1]+dir[1]*t);
const bz = Math.floor(eye[2]+dir[2]*t);
if(bx<0||bx>=W||by<0||by>=H||bz<0||bz>=D) break;
const id = getBlock(bx,by,bz);
if(id !== 0 && id !== B.WATER) return { x:bx, y:by, z:bz, prev:last };
last = { x:bx, y:by, z:bz };
}
return null;
}
/* ========== 挖 / 放 ========== */
let selectedBlock = B.GRASS;
function dig(){
const hit = raycast();
if(!hit) return;
setBlock(hit.x, hit.y, hit.z, 0);
buildMesh();
}
function place(){
const hit = raycast();
if(!hit || !hit.prev) return;
const p = hit.prev;
if(collides(p.x+0.5, p.y, p.z+0.5)) return;
setBlock(p.x, p.y, p.z, selectedBlock);
buildMesh();
}
/* ========== 渲染 ========== */
function render(){
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const proj = perspective(Math.PI/3, canvas.width/canvas.height, 0.1, 100);
const cp = Math.cos(pitch), sp = Math.sin(pitch);
const sy = Math.sin(yaw), cy = Math.cos(yaw);
const eye = [px, py+1.62, pz];
const fwd = [-sy*cp, sp, -cy*cp];
const view = lookAt(eye, [eye[0]+fwd[0], eye[1]+fwd[1], eye[2]+fwd[2]], [0,1,0]);
const mvp = mul(proj, view);
gl.useProgram(prog);
gl.uniformMatrix4fv(uMVP, false, mvp);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, glTex);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 24, 0);
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 24, 12);
gl.enableVertexAttribArray(aUV);
gl.vertexAttribPointer(aLight, 1, gl.FLOAT, false, 24, 20);
gl.enableVertexAttribArray(aLight);
gl.drawArrays(gl.TRIANGLES, 0, vCount);
}
/* ========== 主循环 ========== */
let last = performance.now();
function loop(now){
requestAnimationFrame(loop);
let dt = (now-last)/1000; last = now;
if(dt > 0.1) dt = 0.1;
updatePlayer(dt);
render();
}
/* ========== 摇杆 ========== */
const joy = { x:0, y:0, active:false, 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(); const t=e.changedTouches[0]; joy.id=t.identifier; joy.active=true; joyMove(t); }, {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(e){
for(const t of e.changedTouches) if(t.identifier === joy.id){
joy.active = false; 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, lookX = 0, lookY = 0;
canvas.addEventListener('touchstart', e=>{
for(const t of e.changedTouches){
if(lookId === null){ lookId = t.identifier; lookX = t.clientX; lookY = t.clientY; break; }
}
}, {passive:true});
canvas.addEventListener('touchmove', e=>{
for(const t of e.changedTouches){
if(t.identifier === lookId){
yaw -= (t.clientX - lookX) * 0.005;
pitch -= (t.clientY - lookY) * 0.005;
pitch = Math.max(-1.4, Math.min(1.4, pitch));
lookX = t.clientX; lookY = 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 btnDig = document.getElementById('btnDig');
const btnPlace = document.getElementById('btnPlace');
btnJump.addEventListener('touchstart', e=>{ e.preventDefault(); jumpHeld = true; }, {passive:false});
btnJump.addEventListener('touchend', e=>{ e.preventDefault(); jumpHeld = false; }, {passive:false});
btnJump.addEventListener('touchcancel', ()=>{ jumpHeld = false; });
btnDig.addEventListener('touchstart', e=>{ e.preventDefault(); dig(); }, {passive:false});
btnPlace.addEventListener('touchstart', e=>{ e.preventDefault(); place(); }, {passive:false});
/* ========== 快捷栏 ========== */
const HOTBAR = [B.GRASS, B.DIRT, B.STONE, B.SAND, B.LOG, B.LEAVES, B.GLASS, B.BRICK, B.PLANK];
const hotbarEl = document.getElementById('hotbar');
function makeIcon(tile){
const c = document.createElement('canvas');
c.width = c.height = TILE;
const ctx = c.getContext('2d');
const col = tile % COLS, row = Math.floor(tile / COLS);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(atlasCanvas, col*TILE, row*TILE, TILE, TILE, 0, 0, TILE, TILE);
return c;
}
function buildHotbar(){
hotbarEl.innerHTML = '';
HOTBAR.forEach(id=>{
const info = BLOCK_INFO[id];
if(!info) return;
const slot = document.createElement('div');
slot.className = 'slot' + (id === selectedBlock ? ' active' : '');
slot.appendChild(makeIcon(info.tex[0]));
slot.addEventListener('touchstart', e=>{ e.preventDefault(); selectBlock(id); }, {passive:false});
hotbarEl.appendChild(slot);
});
}
function selectBlock(id){
selectedBlock = id;
[...hotbarEl.children].forEach((c,i)=>c.classList.toggle('active', HOTBAR[i]===id));
}
buildHotbar();
/* ========== 背包 ========== */
const PLACEABLE = Object.keys(BLOCK_INFO).map(Number);
const bagPanel = document.getElementById('bagPanel');
const bagGrid = document.getElementById('bagGrid');
const bagClose = document.getElementById('bagClose');
const btnBag = document.getElementById('btnBag');
function buildBag(){
bagGrid.innerHTML = '';
PLACEABLE.forEach(id=>{
const info = BLOCK_INFO[id];
if(!info) return;
const item = document.createElement('div');
item.className = 'bagItem';
item.appendChild(makeIcon(info.tex[0]));
const span = document.createElement('span');
span.textContent = info.name;
item.appendChild(span);
item.addEventListener('touchstart', e=>{
e.preventDefault();
selectBlock(id);
if(!HOTBAR.includes(id)){
HOTBAR[HOTBAR.length-1] = id;
buildHotbar();
selectBlock(id);
}
bagPanel.classList.remove('open');
}, {passive:false});
bagGrid.appendChild(item);
});
}
btnBag.addEventListener('touchstart', e=>{ e.preventDefault(); buildBag(); bagPanel.classList.add('open'); }, {passive:false});
bagClose.addEventListener('click', ()=> bagPanel.classList.remove('open'));
bagPanel.addEventListener('touchstart', e=>{ if(e.target === bagPanel) bagPanel.classList.remove('open'); }, {passive:false});
/* ========== 初始化 ========== */
generateWorld();
buildMesh();
px = W/2; pz = D/2;
for(let y=H-1; y>=0; y--){
if(getBlock(Math.floor(px), y, Math.floor(pz)) !== 0){ py = y+1; break; }
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 像素世界 · 55 方块
Creator: ElectricShark88
Libraries: none
Complexity: complex (941 lines, 34.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: 55-electricshark88" to link back to the original. Then publish at arcadelab.ai/publish.