1141 lines
48 KiB
HTML
1141 lines
48 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>神话幻想世界 - Minecraft 克隆版</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
overflow: hidden;
|
|
background: #000;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
canvas { display: block; }
|
|
#info {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
color: #fff;
|
|
background: rgba(0,0,0,0.6);
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
z-index: 100;
|
|
font-weight: bold;
|
|
text-shadow: 1px 1px 2px #000;
|
|
}
|
|
#info span { color: #ffcc00; }
|
|
#loading {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
color: #fff;
|
|
z-index: 1000;
|
|
transition: opacity 0.5s;
|
|
}
|
|
#loading.hidden { opacity: 0; pointer-events: none; }
|
|
.loader {
|
|
width: 60px; height: 60px;
|
|
border: 4px solid rgba(255,255,255,0.3);
|
|
border-top-color: #ffcc00;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin-bottom: 20px;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
#fps { position: absolute; bottom: 10px; left: 10px; color: #fff; background: rgba(0,0,0,0.6); padding: 5px 10px; border-radius: 5px; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="loading">
|
|
<div class="loader"></div>
|
|
<div>正在生成神话世界...</div>
|
|
</div>
|
|
<div id="info">
|
|
<div>神话幻想世界 | WASD 移动 | 鼠标环视</div>
|
|
<div>左键破坏 | 右键放置 | 1/2/3 选择方块</div>
|
|
<div>当前方块: <span id="current-block">草地</span> | 位置: <span id="position">(0, 0, 0)</span></div>
|
|
</div>
|
|
<div id="fps">FPS: 60</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.js"></script>
|
|
<script>
|
|
// ==================== 配置 ====================
|
|
const WORLD_SIZE = 128;
|
|
const CHUNK_SIZE = 16;
|
|
const BLOCK_SIZE = 1;
|
|
const RENDER_DISTANCE = 1;
|
|
const GRAVITY = -0.08;
|
|
const JUMP_FORCE = 0.12;
|
|
const MOVEMENT_SPEED = 0.1;
|
|
|
|
// 方块类型
|
|
const BLOCK_TYPES = {
|
|
AIR: 0,
|
|
GRASS: 1,
|
|
DIRT: 2,
|
|
STONE: 3,
|
|
WOOD: 4,
|
|
LEAVES: 5,
|
|
SAND: 6,
|
|
WATER: 7,
|
|
GOLD: 8,
|
|
MYTH_STONE: 9,
|
|
ANCIENT_BRICK: 10,
|
|
MAGIC_GLOW: 11,
|
|
OBSIDIAN: 12,
|
|
CRYSTAL: 13
|
|
};
|
|
|
|
const BLOCK_COLORS = {
|
|
[BLOCK_TYPES.GRASS]: { top: 0x2e8b57, side: 0x3cb371, bottom: 0x1a5930 },
|
|
[BLOCK_TYPES.DIRT]: { all: 0x8b4513 },
|
|
[BLOCK_TYPES.STONE]: { all: 0x808080 },
|
|
[BLOCK_TYPES.WOOD]: { all: 0x8B4513 },
|
|
[BLOCK_TYPES.LEAVES]: { all: 0x228B22 },
|
|
[BLOCK_TYPES.SAND]: { all: 0xF4A460 },
|
|
[BLOCK_TYPES.WATER]: { all: 0x1E90FF },
|
|
[BLOCK_TYPES.GOLD]: { all: 0xFFD700 },
|
|
[BLOCK_TYPES.MYTH_STONE]: { all: 0x4B0082 },
|
|
[BLOCK_TYPES.ANCIENT_BRICK]: { all: 0x696969 },
|
|
[BLOCK_TYPES.MAGIC_GLOW]: { all: 0x9400D3, emissive: 0x9400D3 },
|
|
[BLOCK_TYPES.OBSIDIAN]: { all: 0x000000 },
|
|
[BLOCK_TYPES.CRYSTAL]: { all: 0x00FFFF, transparent: true, opacity: 0.7 }
|
|
};
|
|
|
|
const BLOCK_NAMES = {
|
|
[BLOCK_TYPES.GRASS]: '草地',
|
|
[BLOCK_TYPES.DIRT]: '泥土',
|
|
[BLOCK_TYPES.STONE]: '石头',
|
|
[BLOCK_TYPES.WOOD]: '木头',
|
|
[BLOCK_TYPES.LEAVES]: '树叶',
|
|
[BLOCK_TYPES.SAND]: '沙子',
|
|
[BLOCK_TYPES.WATER]: '水',
|
|
[BLOCK_TYPES.GOLD]: '黄金',
|
|
[BLOCK_TYPES.MYTH_STONE]: '神话石',
|
|
[BLOCK_TYPES.ANCIENT_BRICK]: '古代砖',
|
|
[BLOCK_TYPES.MAGIC_GLOW]: '魔法光',
|
|
[BLOCK_TYPES.OBSIDIAN]: '黑曜石',
|
|
[BLOCK_TYPES.CRYSTAL]: '水晶'
|
|
};
|
|
|
|
// ==================== 场景设置 ====================
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x87CEEB); // 天空蓝
|
|
scene.fog = new THREE.FogExp2(0x87CEEB, 0.002);
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 10, 0);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// ==================== 光照 ====================
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
directionalLight.position.set(100, 200, 50);
|
|
directionalLight.castShadow = true;
|
|
directionalLight.shadow.mapSize.width = 2048;
|
|
directionalLight.shadow.mapSize.height = 2048;
|
|
directionalLight.shadow.camera.near = 0.5;
|
|
directionalLight.shadow.camera.far = 500;
|
|
directionalLight.shadow.camera.left = -200;
|
|
directionalLight.shadow.camera.right = 200;
|
|
directionalLight.shadow.camera.top = 200;
|
|
directionalLight.shadow.camera.bottom = -200;
|
|
scene.add(directionalLight);
|
|
|
|
// 太阳
|
|
const sunGeometry = new THREE.SphereGeometry(30, 32, 32);
|
|
const sunMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xFFD700,
|
|
emissive: 0xFFD700,
|
|
emissiveIntensity: 0.8
|
|
});
|
|
const sun = new THREE.Mesh(sunGeometry, sunMaterial);
|
|
sun.position.set(300, 300, -200);
|
|
scene.add(sun);
|
|
|
|
// 月亮
|
|
const moonGeometry = new THREE.SphereGeometry(15, 32, 32);
|
|
const moonMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xE6E6FA,
|
|
emissive: 0xADD8E6,
|
|
emissiveIntensity: 0.5
|
|
});
|
|
const moon = new THREE.Mesh(moonGeometry, moonMaterial);
|
|
moon.position.set(-300, 200, -100);
|
|
scene.add(moon);
|
|
|
|
// 星星
|
|
const starsGeometry = new THREE.BufferGeometry();
|
|
const starsMaterial = new THREE.PointsMaterial({
|
|
color: 0xFFFFFF,
|
|
size: 0.5,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
const starsVertices = [];
|
|
for (let i = 0; i < 5000; i++) {
|
|
const x = (Math.random() - 0.5) * 2000;
|
|
const y = (Math.random() - 0.5) * 2000;
|
|
const z = (Math.random() - 0.5) * 2000;
|
|
starsVertices.push(x, y, z);
|
|
}
|
|
starsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starsVertices, 3));
|
|
const stars = new THREE.Points(starsGeometry, starsMaterial);
|
|
scene.add(stars);
|
|
|
|
// ==================== 控制器 ====================
|
|
const controls = new THREE.PointerLockControls(camera, document.body);
|
|
|
|
// 鼠标锁定
|
|
const blocker = document.createElement('div');
|
|
blocker.style.position = 'absolute';
|
|
blocker.style.top = '0';
|
|
blocker.style.left = '0';
|
|
blocker.style.width = '100%';
|
|
blocker.style.height = '100%';
|
|
blocker.style.background = 'rgba(0,0,0,0.5)';
|
|
blocker.style.display = 'none';
|
|
document.body.appendChild(blocker);
|
|
|
|
const instructions = document.createElement('div');
|
|
instructions.style.position = 'absolute';
|
|
instructions.style.top = '50%';
|
|
instructions.style.left = '50%';
|
|
instructions.style.transform = 'translate(-50%, -50%)';
|
|
instructions.style.color = '#fff';
|
|
instructions.style.fontSize = '24px';
|
|
instructions.style.textAlign = 'center';
|
|
instructions.style.background = 'rgba(0,0,0,0.8)';
|
|
instructions.style.padding = '20px';
|
|
instructions.style.borderRadius = '10px';
|
|
instructions.innerHTML = '点击屏幕开始游戏<br><small>WASD 移动 | 鼠标环视 | 左键破坏 | 右键放置</small>';
|
|
document.body.appendChild(instructions);
|
|
|
|
instructions.addEventListener('click', () => {
|
|
controls.lock();
|
|
instructions.style.display = 'none';
|
|
blocker.style.display = 'none';
|
|
});
|
|
|
|
controls.addEventListener('lock', () => {
|
|
instructions.style.display = 'none';
|
|
blocker.style.display = 'none';
|
|
});
|
|
|
|
controls.addEventListener('unlock', () => {
|
|
blocker.style.display = 'block';
|
|
instructions.style.display = '';
|
|
});
|
|
|
|
// ==================== 玩家状态 ====================
|
|
const player = {
|
|
velocity: new THREE.Vector3(),
|
|
isOnGround: false,
|
|
canJump: false,
|
|
selectedBlock: BLOCK_TYPES.GRASS
|
|
};
|
|
|
|
// ==================== 地形生成 ====================
|
|
// 改进的柏林噪声
|
|
class ImprovedPerlinNoise {
|
|
constructor(seed = Math.random()) {
|
|
this.seed = seed;
|
|
this.permutation = this.generatePermutation();
|
|
this.gradients = this.generateGradients();
|
|
}
|
|
|
|
generatePermutation() {
|
|
const p = [];
|
|
for (let i = 0; i < 256; i++) p[i] = i;
|
|
|
|
// 使用种子进行洗牌
|
|
for (let i = 255; i > 0; i--) {
|
|
const j = Math.floor(this.random(i + 1));
|
|
[p[i], p[j]] = [p[j], p[i]];
|
|
}
|
|
|
|
return [...p, ...p];
|
|
}
|
|
|
|
generateGradients() {
|
|
const grad = [];
|
|
for (let i = 0; i < 256; i++) {
|
|
const theta = Math.random() * 2 * Math.PI;
|
|
grad[i] = { x: Math.cos(theta), y: Math.sin(theta) };
|
|
}
|
|
return grad;
|
|
}
|
|
|
|
random(max) {
|
|
let x = this.seed;
|
|
x = (x * 9301 + 49297) % 233280;
|
|
this.seed = x;
|
|
return x / 233280 * max;
|
|
}
|
|
|
|
fade(t) {
|
|
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
}
|
|
|
|
lerp(a, b, t) {
|
|
return a + t * (b - a);
|
|
}
|
|
|
|
grad(hash, x, y) {
|
|
const h = hash & 15;
|
|
const u = h < 8 ? x : y;
|
|
const v = h < 4 ? y : h === 12 || h === 14 ? x : 0;
|
|
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
|
|
}
|
|
|
|
noise2D(x, y) {
|
|
const X = Math.floor(x) & 255;
|
|
const Y = Math.floor(y) & 255;
|
|
|
|
x -= Math.floor(x);
|
|
y -= Math.floor(y);
|
|
|
|
const u = this.fade(x);
|
|
const v = this.fade(y);
|
|
|
|
const p = this.permutation;
|
|
const A = p[X] + Y;
|
|
const AA = p[A];
|
|
const AB = p[A + 1];
|
|
const B = p[X + 1] + Y;
|
|
const BA = p[B];
|
|
const BB = p[B + 1];
|
|
|
|
return this.lerp(
|
|
this.lerp(this.grad(p[AA], x, y), this.grad(p[BA], x - 1, y), u),
|
|
this.lerp(this.grad(p[AB], x, y - 1), this.grad(p[BB], x - 1, y - 1), u),
|
|
v
|
|
);
|
|
}
|
|
|
|
octaveNoise(x, y, octaves = 4, persistence = 0.5) {
|
|
let total = 0;
|
|
let frequency = 1;
|
|
let amplitude = 1;
|
|
let maxValue = 0;
|
|
|
|
for (let i = 0; i < octaves; i++) {
|
|
total += this.noise2D(x * frequency, y * frequency) * amplitude;
|
|
maxValue += amplitude;
|
|
amplitude *= persistence;
|
|
frequency *= 2;
|
|
}
|
|
|
|
return total / maxValue;
|
|
}
|
|
}
|
|
|
|
const noise = new ImprovedPerlinNoise(12345);
|
|
|
|
// 3D 噪声
|
|
function noise3D(x, y, z) {
|
|
return (noise.octaveNoise(x, y, 4) + noise.octaveNoise(x, z, 4) + noise.octaveNoise(y, z, 4)) / 3;
|
|
}
|
|
|
|
// 生成高度
|
|
function getHeight(x, z) {
|
|
// 基础地形
|
|
let height = noise3D(x * 0.05, z * 0.05, 0) * 20;
|
|
|
|
// 添加山脉
|
|
height += noise3D(x * 0.1, z * 0.1, 100) * 15;
|
|
|
|
// 添加细节
|
|
height += noise3D(x * 0.5, z * 0.5, 200) * 3;
|
|
|
|
// 确保最低高度
|
|
height = Math.max(height, 5);
|
|
|
|
return Math.floor(height);
|
|
}
|
|
|
|
// 生成温度(用于生物群系)
|
|
function getTemperature(x, z) {
|
|
return noise.octaveNoise(x * 0.001, z * 0.001, 2) * 0.5 + 0.5;
|
|
}
|
|
|
|
// 生成湿度
|
|
function getMoisture(x, z) {
|
|
return noise.octaveNoise(x * 0.001, z * 0.001, 2, 0.3) * 0.5 + 0.5;
|
|
}
|
|
|
|
// 根据生物群系确定方块
|
|
function getBlockType(x, y, z) {
|
|
const height = getHeight(x, z);
|
|
const temp = getTemperature(x, z);
|
|
const moisture = getMoisture(x, z);
|
|
|
|
// 地下层
|
|
if (y < height - 4) {
|
|
if (y < height - 8) {
|
|
return BLOCK_TYPES.STONE;
|
|
} else {
|
|
return BLOCK_TYPES.DIRT;
|
|
}
|
|
}
|
|
|
|
// 地表
|
|
if (y === height) {
|
|
// 神话区域 - 中心区域
|
|
const centerX = 0, centerZ = 0;
|
|
const distToCenter = Math.sqrt(x * x + z * z);
|
|
|
|
if (distToCenter < 30) {
|
|
// 神话建筑区域
|
|
if (y >= 15 && y <= 25 && distToCenter < 20) {
|
|
if (Math.random() > 0.7) return BLOCK_TYPES.MYTH_STONE;
|
|
if (Math.random() > 0.5) return BLOCK_TYPES.ANCIENT_BRICK;
|
|
return BLOCK_TYPES.MAGIC_GLOW;
|
|
}
|
|
if (distToCenter < 10 && y >= 10) {
|
|
if (Math.random() > 0.8) return BLOCK_TYPES.CRYSTAL;
|
|
if (Math.random() > 0.6) return BLOCK_TYPES.OBSIDIAN;
|
|
return BLOCK_TYPES.MYTH_STONE;
|
|
}
|
|
}
|
|
|
|
// 沙漠
|
|
if (temp > 0.7 && moisture < 0.3) {
|
|
return BLOCK_TYPES.SAND;
|
|
}
|
|
|
|
// 森林
|
|
if (temp > 0.3 && moisture > 0.6) {
|
|
return BLOCK_TYPES.GRASS;
|
|
}
|
|
|
|
// 普通草地
|
|
return BLOCK_TYPES.GRASS;
|
|
}
|
|
|
|
// 地表下方
|
|
if (y > height - 4 && y < height) {
|
|
return BLOCK_TYPES.DIRT;
|
|
}
|
|
|
|
// 水
|
|
if (y <= 8 && height <= 8) {
|
|
return BLOCK_TYPES.WATER;
|
|
}
|
|
|
|
// 空气
|
|
return BLOCK_TYPES.AIR;
|
|
}
|
|
|
|
// 生成神话建筑
|
|
function generateMythicalStructure(centerX, centerZ) {
|
|
const structures = [];
|
|
|
|
// 神殿
|
|
if (Math.random() > 0.3) {
|
|
const templeSize = 10 + Math.floor(Math.random() * 10);
|
|
for (let x = -templeSize; x <= templeSize; x++) {
|
|
for (let z = -templeSize; z <= templeSize; z++) {
|
|
for (let y = 0; y <= 15; y++) {
|
|
const dist = Math.sqrt(x * x + z * z);
|
|
if (dist <= templeSize && (y === 0 || dist <= templeSize - 2 || y >= 10)) {
|
|
if (y === 0) {
|
|
structures.push({x: centerX + x, y: getHeight(centerX + x, centerZ + z) + y, z: centerZ + z, type: BLOCK_TYPES.MYTH_STONE});
|
|
} else if (y < 10) {
|
|
structures.push({x: centerX + x, y: getHeight(centerX + x, centerZ + z) + y, z: centerZ + z, type: BLOCK_TYPES.ANCIENT_BRICK});
|
|
} else {
|
|
if (Math.random() > 0.7) {
|
|
structures.push({x: centerX + x, y: getHeight(centerX + x, centerZ + z) + y, z: centerZ + z, type: BLOCK_TYPES.MAGIC_GLOW});
|
|
} else {
|
|
structures.push({x: centerX + x, y: getHeight(centerX + x, centerZ + z) + y, z: centerZ + z, type: BLOCK_TYPES.CRYSTAL});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 方尖碑
|
|
if (Math.random() > 0.5) {
|
|
const obeliskHeight = 20 + Math.floor(Math.random() * 15);
|
|
for (let y = 0; y < obeliskHeight; y++) {
|
|
const size = Math.max(2, obeliskHeight - y - 10);
|
|
for (let x = -size; x <= size; x++) {
|
|
for (let z = -size; z <= size; z++) {
|
|
if (Math.abs(x) <= 1 && Math.abs(z) <= 1) {
|
|
structures.push({x: centerX + x, y: getHeight(centerX + x, centerZ + z) + y, z: centerZ + z, type: BLOCK_TYPES.OBSIDIAN});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 顶部水晶
|
|
structures.push({x: centerX, y: getHeight(centerX, centerZ) + obeliskHeight, z: centerZ, type: BLOCK_TYPES.CRYSTAL});
|
|
}
|
|
|
|
// 神树
|
|
if (Math.random() > 0.4) {
|
|
const treeHeight = 15 + Math.floor(Math.random() * 10);
|
|
// 树干
|
|
for (let y = 0; y < treeHeight; y++) {
|
|
structures.push({x: centerX, y: getHeight(centerX, centerZ) + y, z: centerZ, type: BLOCK_TYPES.WOOD});
|
|
if (y > 2 && y < treeHeight - 3) {
|
|
structures.push({x: centerX + 1, y: getHeight(centerX + 1, centerZ) + y, z: centerZ, type: BLOCK_TYPES.WOOD});
|
|
structures.push({x: centerX - 1, y: getHeight(centerX - 1, centerZ) + y, z: centerZ, type: BLOCK_TYPES.WOOD});
|
|
structures.push({x: centerX, y: getHeight(centerX, centerZ + 1) + y, z: centerZ + 1, type: BLOCK_TYPES.WOOD});
|
|
structures.push({x: centerX, y: getHeight(centerX, centerZ - 1) + y, z: centerZ - 1, type: BLOCK_TYPES.WOOD});
|
|
}
|
|
}
|
|
// 树冠
|
|
for (let y = treeHeight - 3; y < treeHeight + 3; y++) {
|
|
for (let x = -3; x <= 3; x++) {
|
|
for (let z = -3; z <= 3; z++) {
|
|
const dist = Math.sqrt(x * x + z * z);
|
|
if (dist <= 3 && Math.random() > 0.3) {
|
|
structures.push({x: centerX + x, y: getHeight(centerX + x, centerZ + z) + y, z: centerZ + z, type: BLOCK_TYPES.LEAVES});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 顶部水晶
|
|
structures.push({x: centerX, y: getHeight(centerX, centerZ) + treeHeight + 1, z: centerZ, type: BLOCK_TYPES.MAGIC_GLOW});
|
|
}
|
|
|
|
return structures;
|
|
}
|
|
|
|
// ==================== 区块系统 ====================
|
|
class Chunk {
|
|
constructor(x, z) {
|
|
this.x = x;
|
|
this.z = z;
|
|
this.mesh = new THREE.Group();
|
|
this.blocks = new Map(); // key: "x,y,z", value: blockType
|
|
this.needsUpdate = true;
|
|
}
|
|
|
|
generate() {
|
|
this.mesh.clear();
|
|
this.blocks.clear();
|
|
|
|
const startX = this.x * CHUNK_SIZE;
|
|
const startZ = this.z * CHUNK_SIZE;
|
|
|
|
// 生成神话建筑
|
|
const mythStructures = generateMythicalStructure(
|
|
startX + CHUNK_SIZE / 2,
|
|
startZ + CHUNK_SIZE / 2
|
|
);
|
|
|
|
// 创建方块几何体
|
|
const geometry = new THREE.BufferGeometry();
|
|
const materials = [];
|
|
const position = [];
|
|
const normal = [];
|
|
const color = [];
|
|
const uv = [];
|
|
const index = [];
|
|
|
|
let vertexIndex = 0;
|
|
|
|
// 方块的6个面
|
|
const faces = [
|
|
{ dir: [0, 0, -1], corners: [0, 1, 2, 3] }, // 前
|
|
{ dir: [0, 0, 1], corners: [4, 5, 6, 7] }, // 后
|
|
{ dir: [-1, 0, 0], corners: [0, 4, 7, 3] }, // 左
|
|
{ dir: [1, 0, 0], corners: [1, 2, 6, 5] }, // 右
|
|
{ dir: [0, 1, 0], corners: [0, 4, 5, 1] }, // 上
|
|
{ dir: [0, -1, 0], corners: [3, 7, 6, 2] } // 下
|
|
];
|
|
|
|
// 方块顶点(相对坐标)
|
|
const vertices = [
|
|
[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], // 前面
|
|
[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1] // 后面
|
|
];
|
|
|
|
// 处理每个方块
|
|
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
for (let y = 0; y < WORLD_SIZE; y++) {
|
|
const worldX = startX + x;
|
|
const worldZ = startZ + z;
|
|
const worldY = y;
|
|
|
|
// 检查是否有神话建筑
|
|
let blockType = BLOCK_TYPES.AIR;
|
|
const mythBlock = mythStructures.find(b =>
|
|
b.x === worldX && b.y === worldY && b.z === worldZ
|
|
);
|
|
if (mythBlock) {
|
|
blockType = mythBlock.type;
|
|
} else {
|
|
blockType = getBlockType(worldX, worldY, worldZ);
|
|
}
|
|
|
|
// 空气方块不渲染
|
|
if (blockType === BLOCK_TYPES.AIR) continue;
|
|
|
|
// 存储方块
|
|
this.blocks.set(`${worldX},${worldY},${worldZ}`, blockType);
|
|
|
|
// 检查每个面是否可见
|
|
for (const face of faces) {
|
|
const [dx, dy, dz] = face.dir;
|
|
const neighborX = worldX + dx;
|
|
const neighborY = worldY + dy;
|
|
const neighborZ = worldZ + dz;
|
|
|
|
// 检查邻居是否存在或是否为空气
|
|
const neighborType = mythStructures.find(b =>
|
|
b.x === neighborX && b.y === neighborY && b.z === neighborZ
|
|
)?.type || getBlockType(neighborX, neighborY, neighborZ);
|
|
|
|
if (neighborType === BLOCK_TYPES.AIR) {
|
|
// 这个面可见
|
|
for (const cornerIdx of face.corners) {
|
|
const [cx, cy, cz] = vertices[cornerIdx];
|
|
position.push(
|
|
(startX + x + cx) * BLOCK_SIZE,
|
|
worldY + cy * BLOCK_SIZE,
|
|
(startZ + z + cz) * BLOCK_SIZE
|
|
);
|
|
|
|
// 法线
|
|
normal.push(...face.dir);
|
|
|
|
// UV
|
|
const u = cornerIdx % 2 === 0 ? 0 : 1;
|
|
const v = Math.floor(cornerIdx / 2) % 2 === 0 ? 0 : 1;
|
|
uv.push(u, v);
|
|
}
|
|
|
|
// 颜色
|
|
const blockInfo = BLOCK_COLORS[blockType];
|
|
if (blockInfo) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (face.dir[1] === 1 && blockInfo.top) {
|
|
color.push(blockInfo.top >> 16 & 0xFF, blockInfo.top >> 8 & 0xFF, blockInfo.top & 0xFF);
|
|
} else if (face.dir[1] === -1 && blockInfo.bottom) {
|
|
color.push(blockInfo.bottom >> 16 & 0xFF, blockInfo.bottom >> 8 & 0xFF, blockInfo.bottom & 0xFF);
|
|
} else if (blockInfo.all) {
|
|
color.push(blockInfo.all >> 16 & 0xFF, blockInfo.all >> 8 & 0xFF, blockInfo.all & 0xFF);
|
|
} else if (blockInfo.side) {
|
|
color.push(blockInfo.side >> 16 & 0xFF, blockInfo.side >> 8 & 0xFF, blockInfo.side & 0xFF);
|
|
} else {
|
|
color.push(255, 255, 255);
|
|
}
|
|
}
|
|
} else {
|
|
color.push(255, 255, 255, 255, 255, 255, 255, 255, 255);
|
|
}
|
|
|
|
// 索引
|
|
const baseIdx = vertexIndex;
|
|
index.push(baseIdx, baseIdx + 1, baseIdx + 2);
|
|
index.push(baseIdx, baseIdx + 2, baseIdx + 3);
|
|
vertexIndex += 4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 创建几何体
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(position, 3));
|
|
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normal, 3));
|
|
geometry.setAttribute('color', new THREE.Float32BufferAttribute(color, 3));
|
|
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
|
|
geometry.setIndex(index);
|
|
|
|
// 材质
|
|
const material = new THREE.MeshStandardMaterial({
|
|
vertexColors: true,
|
|
roughness: 0.8,
|
|
metalness: 0.2
|
|
});
|
|
|
|
// 处理发光材质
|
|
const glowGeometry = new THREE.BufferGeometry();
|
|
const glowMaterials = [];
|
|
const glowPosition = [];
|
|
const glowColor = [];
|
|
const glowIndex = [];
|
|
|
|
let glowVertexIndex = 0;
|
|
|
|
// 重新处理发光方块
|
|
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
for (let y = 0; y < WORLD_SIZE; y++) {
|
|
const worldX = startX + x;
|
|
const worldZ = startZ + z;
|
|
const worldY = y;
|
|
|
|
const blockType = mythStructures.find(b =>
|
|
b.x === worldX && b.y === worldY && b.z === worldZ
|
|
)?.type || getBlockType(worldX, worldY, worldZ);
|
|
|
|
if (blockType === BLOCK_TYPES.AIR) continue;
|
|
if (!BLOCK_COLORS[blockType]?.emissive) continue;
|
|
|
|
// 处理每个面
|
|
for (const face of faces) {
|
|
const [dx, dy, dz] = face.dir;
|
|
const neighborX = worldX + dx;
|
|
const neighborY = worldY + dy;
|
|
const neighborZ = worldZ + dz;
|
|
|
|
const neighborType = mythStructures.find(b =>
|
|
b.x === neighborX && b.y === neighborY && b.z === neighborZ
|
|
)?.type || getBlockType(neighborX, neighborY, neighborZ);
|
|
|
|
if (neighborType === BLOCK_TYPES.AIR) {
|
|
for (const cornerIdx of face.corners) {
|
|
const [cx, cy, cz] = vertices[cornerIdx];
|
|
glowPosition.push(
|
|
(startX + x + cx) * BLOCK_SIZE,
|
|
worldY + cy * BLOCK_SIZE,
|
|
(startZ + z + cz) * BLOCK_SIZE
|
|
);
|
|
|
|
const blockInfo = BLOCK_COLORS[blockType];
|
|
if (blockInfo.emissive) {
|
|
glowColor.push(
|
|
(blockInfo.emissive >> 16 & 0xFF) / 255,
|
|
(blockInfo.emissive >> 8 & 0xFF) / 255,
|
|
(blockInfo.emissive & 0xFF) / 255
|
|
);
|
|
} else {
|
|
glowColor.push(1, 1, 1);
|
|
}
|
|
}
|
|
|
|
const baseIdx = glowVertexIndex;
|
|
glowIndex.push(baseIdx, baseIdx + 1, baseIdx + 2);
|
|
glowIndex.push(baseIdx, baseIdx + 2, baseIdx + 3);
|
|
glowVertexIndex += 4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
glowGeometry.setAttribute('position', new THREE.Float32BufferAttribute(glowPosition, 3));
|
|
glowGeometry.setAttribute('color', new THREE.Float32BufferAttribute(glowColor, 3));
|
|
glowGeometry.setIndex(glowIndex);
|
|
|
|
const glowMaterial = new THREE.MeshBasicMaterial({
|
|
vertexColors: true,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
side: THREE.FrontSide
|
|
});
|
|
|
|
const mainMesh = new THREE.Mesh(geometry, material);
|
|
mainMesh.castShadow = true;
|
|
mainMesh.receiveShadow = true;
|
|
this.mesh.add(mainMesh);
|
|
|
|
if (glowPosition.length > 0) {
|
|
const glowMesh = new THREE.Mesh(glowGeometry, glowMaterial);
|
|
this.mesh.add(glowMesh);
|
|
}
|
|
|
|
this.mesh.position.set(
|
|
startX * BLOCK_SIZE,
|
|
0,
|
|
startZ * BLOCK_SIZE
|
|
);
|
|
|
|
this.needsUpdate = false;
|
|
}
|
|
}
|
|
|
|
// ==================== 区块管理器 ====================
|
|
class ChunkManager {
|
|
constructor() {
|
|
this.chunks = new Map(); // key: "x,z"
|
|
this.loadedChunks = new Set();
|
|
}
|
|
|
|
getChunk(x, z) {
|
|
const key = `${Math.floor(x / CHUNK_SIZE)},${Math.floor(z / CHUNK_SIZE)}`;
|
|
return this.chunks.get(key);
|
|
}
|
|
|
|
createChunk(x, z) {
|
|
const key = `${x},${z}`;
|
|
if (this.chunks.has(key)) return this.chunks.get(key);
|
|
|
|
const chunk = new Chunk(x, z);
|
|
this.chunks.set(key, chunk);
|
|
scene.add(chunk.mesh);
|
|
return chunk;
|
|
}
|
|
|
|
update(playerX, playerZ) {
|
|
const playerChunkX = Math.floor(playerX / CHUNK_SIZE);
|
|
const playerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
|
|
|
|
// 卸载远距离区块
|
|
for (const [key, chunk] of this.chunks) {
|
|
const [x, z] = key.split(',').map(Number);
|
|
const dist = Math.sqrt(
|
|
Math.pow(x - playerChunkX, 2) +
|
|
Math.pow(z - playerChunkZ, 2)
|
|
);
|
|
|
|
if (dist > RENDER_DISTANCE) {
|
|
scene.remove(chunk.mesh);
|
|
this.chunks.delete(key);
|
|
this.loadedChunks.delete(key);
|
|
}
|
|
}
|
|
|
|
// 加载附近区块
|
|
for (let x = playerChunkX - RENDER_DISTANCE; x <= playerChunkX + RENDER_DISTANCE; x++) {
|
|
for (let z = playerChunkZ - RENDER_DISTANCE; z <= playerChunkZ + RENDER_DISTANCE; z++) {
|
|
const key = `${x},${z}`;
|
|
if (!this.chunks.has(key)) {
|
|
const chunk = this.createChunk(x, z);
|
|
chunk.generate();
|
|
this.loadedChunks.add(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const chunkManager = new ChunkManager();
|
|
|
|
// ==================== 方块交互 ====================
|
|
const raycaster = new THREE.Raycaster();
|
|
const mouse = new THREE.Vector2();
|
|
|
|
function getIntersectedBlock() {
|
|
raycaster.setFromCamera(mouse, camera);
|
|
|
|
const intersects = [];
|
|
|
|
// 检查所有加载的区块
|
|
for (const chunk of chunkManager.chunks.values()) {
|
|
const chunkIntersects = raycaster.intersectObject(chunk.mesh, true);
|
|
intersects.push(...chunkIntersects);
|
|
}
|
|
|
|
if (intersects.length === 0) return null;
|
|
|
|
// 找到最近的交点
|
|
intersects.sort((a, b) => a.distance - b.distance);
|
|
const intersect = intersects[0];
|
|
|
|
// 计算方块位置
|
|
const blockPos = new THREE.Vector3().copy(intersect.point).add(intersect.face.normal);
|
|
blockPos.x = Math.floor(blockPos.x / BLOCK_SIZE) * BLOCK_SIZE;
|
|
blockPos.y = Math.floor(blockPos.y / BLOCK_SIZE) * BLOCK_SIZE;
|
|
blockPos.z = Math.floor(blockPos.z / BLOCK_SIZE) * BLOCK_SIZE;
|
|
|
|
// 检查是否在区块中
|
|
const chunkX = Math.floor(blockPos.x / (CHUNK_SIZE * BLOCK_SIZE)) * CHUNK_SIZE * BLOCK_SIZE;
|
|
const chunkZ = Math.floor(blockPos.z / (CHUNK_SIZE * BLOCK_SIZE)) * CHUNK_SIZE * BLOCK_SIZE;
|
|
const chunk = chunkManager.getChunk(chunkX / BLOCK_SIZE, chunkZ / BLOCK_SIZE);
|
|
|
|
if (!chunk) return null;
|
|
|
|
return {
|
|
position: new THREE.Vector3(blockPos.x, blockPos.y, blockPos.z),
|
|
chunk: chunk,
|
|
face: intersect.face
|
|
};
|
|
}
|
|
|
|
function destroyBlock(position) {
|
|
const x = Math.floor(position.x / BLOCK_SIZE);
|
|
const y = Math.floor(position.y / BLOCK_SIZE);
|
|
const z = Math.floor(position.z / BLOCK_SIZE);
|
|
|
|
const chunk = chunkManager.getChunk(x, z);
|
|
if (!chunk) return false;
|
|
|
|
const key = `${x},${y},${z}`;
|
|
if (chunk.blocks.has(key)) {
|
|
chunk.blocks.delete(key);
|
|
chunk.needsUpdate = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function placeBlock(position, blockType) {
|
|
const x = Math.floor(position.x / BLOCK_SIZE);
|
|
const y = Math.floor(position.y / BLOCK_SIZE);
|
|
const z = Math.floor(position.z / BLOCK_SIZE);
|
|
|
|
const chunk = chunkManager.getChunk(x, z);
|
|
if (!chunk) return false;
|
|
|
|
const key = `${x},${y},${z}`;
|
|
chunk.blocks.set(key, blockType);
|
|
chunk.needsUpdate = true;
|
|
return true;
|
|
}
|
|
|
|
// ==================== 输入处理 ====================
|
|
const keys = { w: false, a: false, s: false, d: false, space: false, shift: false };
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
switch (e.code) {
|
|
case 'KeyW': keys.w = true; break;
|
|
case 'KeyA': keys.a = true; break;
|
|
case 'KeyS': keys.s = true; break;
|
|
case 'KeyD': keys.d = true; break;
|
|
case 'Space': keys.space = true; break;
|
|
case 'ShiftLeft': keys.shift = true; break;
|
|
case 'Digit1': player.selectedBlock = BLOCK_TYPES.GRASS; updateInfo(); break;
|
|
case 'Digit2': player.selectedBlock = BLOCK_TYPES.DIRT; updateInfo(); break;
|
|
case 'Digit3': player.selectedBlock = BLOCK_TYPES.STONE; updateInfo(); break;
|
|
case 'Digit4': player.selectedBlock = BLOCK_TYPES.WOOD; updateInfo(); break;
|
|
case 'Digit5': player.selectedBlock = BLOCK_TYPES.MYTH_STONE; updateInfo(); break;
|
|
case 'Digit6': player.selectedBlock = BLOCK_TYPES.ANCIENT_BRICK; updateInfo(); break;
|
|
case 'Digit7': player.selectedBlock = BLOCK_TYPES.MAGIC_GLOW; updateInfo(); break;
|
|
case 'Digit8': player.selectedBlock = BLOCK_TYPES.CRYSTAL; updateInfo(); break;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keyup', (e) => {
|
|
switch (e.code) {
|
|
case 'KeyW': keys.w = false; break;
|
|
case 'KeyA': keys.a = false; break;
|
|
case 'KeyS': keys.s = false; break;
|
|
case 'KeyD': keys.d = false; break;
|
|
case 'Space': keys.space = false; break;
|
|
case 'ShiftLeft': keys.shift = false; break;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
|
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
|
});
|
|
|
|
window.addEventListener('mousedown', (e) => {
|
|
if (!controls.isLocked) return;
|
|
|
|
const intersected = getIntersectedBlock();
|
|
if (!intersected) return;
|
|
|
|
if (e.button === 0) { // 左键破坏
|
|
destroyBlock(intersected.position);
|
|
} else if (e.button === 2) { // 右键放置
|
|
const placePos = new THREE.Vector3().copy(intersected.position).add(intersected.face.normal);
|
|
placeBlock(placePos, player.selectedBlock);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
function updateInfo() {
|
|
document.getElementById('current-block').textContent = BLOCK_NAMES[player.selectedBlock] || '未知';
|
|
document.getElementById('position').textContent =
|
|
`(${Math.floor(camera.position.x)}, ${Math.floor(camera.position.y)}, ${Math.floor(camera.position.z)})`;
|
|
}
|
|
|
|
// ==================== 玩家移动 ====================
|
|
function updatePlayer(deltaTime) {
|
|
const direction = new THREE.Vector3();
|
|
|
|
if (keys.w) direction.z -= 1;
|
|
if (keys.s) direction.z += 1;
|
|
if (keys.a) direction.x -= 1;
|
|
if (keys.d) direction.x += 1;
|
|
|
|
if (direction.length() > 0) {
|
|
direction.normalize();
|
|
|
|
// 根据相机方向调整移动方向
|
|
direction.applyQuaternion(camera.quaternion);
|
|
direction.y = 0;
|
|
direction.normalize();
|
|
|
|
player.velocity.x = direction.x * MOVEMENT_SPEED;
|
|
player.velocity.z = direction.z * MOVEMENT_SPEED;
|
|
} else {
|
|
player.velocity.x *= 0.8;
|
|
player.velocity.z *= 0.8;
|
|
}
|
|
|
|
// 跳跃
|
|
if (keys.space && player.isOnGround) {
|
|
player.velocity.y = JUMP_FORCE;
|
|
player.isOnGround = false;
|
|
}
|
|
|
|
// 下蹲
|
|
if (keys.shift) {
|
|
player.velocity.y = -MOVEMENT_SPEED / 2;
|
|
}
|
|
|
|
// 重力
|
|
player.velocity.y += GRAVITY;
|
|
|
|
// 检查地面碰撞
|
|
const playerPos = new THREE.Vector3(
|
|
camera.position.x,
|
|
camera.position.y - 1.8,
|
|
camera.position.z
|
|
);
|
|
|
|
const blockBelow = getBlockType(
|
|
Math.floor(playerPos.x),
|
|
Math.floor(playerPos.y - 0.1),
|
|
Math.floor(playerPos.z)
|
|
);
|
|
|
|
if (blockBelow !== BLOCK_TYPES.AIR && player.velocity.y < 0) {
|
|
player.velocity.y = 0;
|
|
player.isOnGround = true;
|
|
} else {
|
|
player.isOnGround = false;
|
|
}
|
|
|
|
// 移动玩家
|
|
camera.position.x += player.velocity.x;
|
|
camera.position.y += player.velocity.y;
|
|
camera.position.z += player.velocity.z;
|
|
|
|
// 碰撞检测
|
|
const playerBox = new THREE.Box3().setFromCenterAndSize(
|
|
new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z),
|
|
new THREE.Vector3(0.6, 1.8, 0.6)
|
|
);
|
|
|
|
// 简单的碰撞检测
|
|
const blockSize = new THREE.Vector3(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
for (let dy = -1; dy <= 1; dy++) {
|
|
for (let dz = -1; dz <= 1; dz++) {
|
|
const blockX = Math.floor(camera.position.x + dx * 0.5);
|
|
const blockY = Math.floor(camera.position.y + dy * 0.5);
|
|
const blockZ = Math.floor(camera.position.z + dz * 0.5);
|
|
|
|
const blockType = getBlockType(blockX, blockY, blockZ);
|
|
if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
|
|
const blockBox = new THREE.Box3().setFromCenterAndSize(
|
|
new THREE.Vector3(blockX + 0.5, blockY + 0.5, blockZ + 0.5),
|
|
blockSize
|
|
);
|
|
|
|
if (playerBox.intersectsBox(blockBox)) {
|
|
// 简单的碰撞响应
|
|
if (dx !== 0) {
|
|
camera.position.x -= player.velocity.x * 0.5;
|
|
player.velocity.x *= 0.5;
|
|
}
|
|
if (dy !== 0) {
|
|
camera.position.y -= player.velocity.y * 0.5;
|
|
player.velocity.y *= 0.5;
|
|
}
|
|
if (dz !== 0) {
|
|
camera.position.z -= player.velocity.z * 0.5;
|
|
player.velocity.z *= 0.5;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== 渲染循环 ====================
|
|
let lastTime = 0;
|
|
let frameCount = 0;
|
|
let fps = 60;
|
|
|
|
function animate(currentTime) {
|
|
requestAnimationFrame(animate);
|
|
|
|
const deltaTime = (currentTime - lastTime) / 1000;
|
|
lastTime = currentTime;
|
|
|
|
// 计算FPS
|
|
frameCount++;
|
|
if (currentTime - lastTime > 1000) {
|
|
fps = Math.round(frameCount * 1000 / (currentTime - lastTime));
|
|
document.getElementById('fps').textContent = `FPS: ${fps}`;
|
|
frameCount = 0;
|
|
lastTime = currentTime;
|
|
}
|
|
|
|
// 更新区块
|
|
chunkManager.update(camera.position.x, camera.position.z);
|
|
|
|
// 更新需要重新生成的区块
|
|
for (const chunk of chunkManager.chunks.values()) {
|
|
if (chunk.needsUpdate) {
|
|
chunk.generate();
|
|
}
|
|
}
|
|
|
|
// 更新玩家
|
|
if (controls.isLocked) {
|
|
updatePlayer(deltaTime);
|
|
updateInfo();
|
|
}
|
|
|
|
// 旋转太阳和月亮
|
|
sun.rotation.y += 0.0001;
|
|
moon.rotation.y += 0.00005;
|
|
|
|
// 渲染
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// ==================== 初始化 ====================
|
|
function init() {
|
|
// 生成初始区块
|
|
for (let x = -RENDER_DISTANCE; x <= RENDER_DISTANCE; x++) {
|
|
for (let z = -RENDER_DISTANCE; z <= RENDER_DISTANCE; z++) {
|
|
const chunk = chunkManager.createChunk(x, z);
|
|
chunk.generate();
|
|
}
|
|
}
|
|
|
|
// 设置初始相机位置
|
|
camera.position.set(0, 20, 0);
|
|
|
|
// 隐藏加载界面
|
|
setTimeout(() => {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
}, 1000);
|
|
|
|
// 开始动画
|
|
animate(0);
|
|
}
|
|
|
|
// 等待 Three.js 加载完成
|
|
window.addEventListener('load', init);
|
|
</script>
|
|
</body>
|
|
</html> |