1145 lines
43 KiB
HTML
1145 lines
43 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Mythos Craft</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
|
|
#blocker {
|
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
background: rgba(0,0,0,0.7); display: flex; align-items: center;
|
|
justify-content: center; z-index: 100; cursor: pointer;
|
|
}
|
|
#instructions {
|
|
color: #fff; text-align: center; padding: 40px;
|
|
background: rgba(0,0,0,0.8); border: 2px solid #4a9eff;
|
|
border-radius: 10px; max-width: 500px;
|
|
}
|
|
#instructions h1 { font-size: 36px; color: #4a9eff; margin-bottom: 20px; }
|
|
#instructions p { margin: 10px 0; line-height: 1.6; }
|
|
#instructions .key {
|
|
display: inline-block; background: #333; padding: 2px 8px;
|
|
border-radius: 4px; border: 1px solid #666; margin: 0 2px;
|
|
}
|
|
#crosshair {
|
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
width: 20px; height: 20px; z-index: 10; pointer-events: none;
|
|
}
|
|
#crosshair::before, #crosshair::after {
|
|
content: ''; position: absolute; background: rgba(255,255,255,0.8);
|
|
}
|
|
#crosshair::before { width: 2px; height: 20px; left: 9px; top: 0; }
|
|
#crosshair::after { width: 20px; height: 2px; top: 9px; left: 0; }
|
|
#hud {
|
|
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
|
display: flex; gap: 4px; z-index: 10; background: rgba(0,0,0,0.5);
|
|
padding: 8px; border-radius: 8px;
|
|
}
|
|
.slot {
|
|
width: 50px; height: 50px; border: 2px solid #555;
|
|
display: flex; align-items: center; justify-content: center;
|
|
cursor: pointer; position: relative; background: rgba(0,0,0,0.3);
|
|
}
|
|
.slot.active { border-color: #4a9eff; background: rgba(74,158,255,0.2); }
|
|
.slot .block-preview { width: 36px; height: 36px; border-radius: 4px; }
|
|
.slot .slot-num {
|
|
position: absolute; top: 2px; left: 4px; color: #aaa;
|
|
font-size: 10px;
|
|
}
|
|
#info {
|
|
position: fixed; top: 10px; left: 10px; color: #fff;
|
|
font-size: 14px; z-index: 10; text-shadow: 1px 1px 2px #000;
|
|
}
|
|
#info div { margin: 4px 0; }
|
|
#minimap {
|
|
position: fixed; top: 10px; right: 10px; width: 150px; height: 150px;
|
|
border: 2px solid #4a9eff; z-index: 10; background: #1a1a2e;
|
|
}
|
|
#landmark-indicator {
|
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -80px);
|
|
color: #ffd700; font-size: 16px; z-index: 10; text-shadow: 1px 1px 2px #000;
|
|
pointer-events: none; text-align: center; display: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="blocker">
|
|
<div id="instructions">
|
|
<h1>🏛️ Mythos Craft</h1>
|
|
<p>A Minecraft-style voxel world with mythic structures</p>
|
|
<p style="margin-top: 20px;">
|
|
<span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> Move
|
|
<span class="key">Space</span> Jump
|
|
</p>
|
|
<p>
|
|
<span class="key">Mouse</span> Look
|
|
<span class="key">Left Click</span> Break
|
|
<span class="key">Right Click</span> Place
|
|
</p>
|
|
<p>
|
|
<span class="key">1-9</span> Select Block
|
|
<span class="key">G</span> Teleport to Landmark
|
|
</p>
|
|
<p style="color: #ffd700; margin-top: 20px;">Click to start</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="crosshair"></div>
|
|
<div id="hud"></div>
|
|
<div id="info">
|
|
<div>FPS: <span id="fps">0</span></div>
|
|
<div>Position: <span id="pos">0, 0, 0</span></div>
|
|
<div>Block: <span id="block-name">Grass</span></div>
|
|
</div>
|
|
<canvas id="minimap"></canvas>
|
|
<div id="landmark-indicator"></div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
// ============================================================
|
|
// PERLIN NOISE IMPLEMENTATION
|
|
// ============================================================
|
|
class PerlinNoise {
|
|
constructor(seed = 0) {
|
|
this.p = new Uint8Array(512);
|
|
const perm = new Uint8Array(256);
|
|
for (let i = 0; i < 256; i++) perm[i] = i;
|
|
|
|
// Fisher-Yates shuffle with seed
|
|
let s = seed;
|
|
for (let i = 255; i > 0; i--) {
|
|
s = (s * 16807 + 0) % 2147483647;
|
|
const j = s % (i + 1);
|
|
[perm[i], perm[j]] = [perm[j], perm[i]];
|
|
}
|
|
|
|
for (let i = 0; i < 512; i++) {
|
|
this.p[i] = perm[i & 255];
|
|
}
|
|
}
|
|
|
|
fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
|
|
lerp(t, a, b) { return a + t * (b - a); }
|
|
grad(hash, x, y, z) {
|
|
const h = hash & 15;
|
|
const u = h < 8 ? x : y;
|
|
const v = h < 4 ? y : (h === 12 || h === 14 ? x : z);
|
|
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
|
|
}
|
|
|
|
noise(x, y, z) {
|
|
const X = Math.floor(x) & 255;
|
|
const Y = Math.floor(y) & 255;
|
|
const Z = Math.floor(z) & 255;
|
|
|
|
x -= Math.floor(x);
|
|
y -= Math.floor(y);
|
|
z -= Math.floor(z);
|
|
|
|
const u = this.fade(x);
|
|
const v = this.fade(y);
|
|
const w = this.fade(z);
|
|
|
|
const A = this.p[X] + Y;
|
|
const AA = this.p[A] + Z;
|
|
const AB = this.p[A + 1] + Z;
|
|
const B = this.p[X + 1] + Y;
|
|
const BA = this.p[B] + Z;
|
|
const BB = this.p[B + 1] + Z;
|
|
|
|
return this.lerp(w,
|
|
this.lerp(v,
|
|
this.lerp(u, this.grad(this.p[AA], x, y, z), this.grad(this.p[BA], x-1, y, z)),
|
|
this.lerp(u, this.grad(this.p[AB], x, y-1, z), this.grad(this.p[BB], x-1, y-1, z))
|
|
),
|
|
this.lerp(v,
|
|
this.lerp(u, this.grad(this.p[AA+1], x, y, z-1), this.grad(this.p[BA+1], x-1, y, z-1)),
|
|
this.lerp(u, this.grad(this.p[AB+1], x, y-1, z-1), this.grad(this.p[BB+1], x-1, y-1, z-1))
|
|
)
|
|
);
|
|
}
|
|
|
|
octave(x, y, z, octaves = 4, persistence = 0.5) {
|
|
let total = 0, frequency = 1, amplitude = 1, maxValue = 0;
|
|
for (let i = 0; i < octaves; i++) {
|
|
total += this.noise(x * frequency, y * frequency, z * frequency) * amplitude;
|
|
maxValue += amplitude;
|
|
amplitude *= persistence;
|
|
frequency *= 2;
|
|
}
|
|
return total / maxValue;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// BLOCK TYPES
|
|
// ============================================================
|
|
const BLOCKS = {
|
|
AIR: 0,
|
|
GRASS: 1,
|
|
DIRT: 2,
|
|
STONE: 3,
|
|
SAND: 4,
|
|
WATER: 5,
|
|
WOOD: 6,
|
|
LEAVES: 7,
|
|
COBBLESTONE: 8,
|
|
SNOW: 9,
|
|
CRYSTAL: 10,
|
|
GOLD: 11,
|
|
OBSIDIAN: 12
|
|
};
|
|
|
|
const BLOCK_COLORS = {
|
|
[BLOCKS.GRASS]: { top: 0x4CAF50, side: 0x8B6914, bottom: 0x8B6914 },
|
|
[BLOCKS.DIRT]: { top: 0x8B6914, side: 0x8B6914, bottom: 0x8B6914 },
|
|
[BLOCKS.STONE]: { top: 0x808080, side: 0x808080, bottom: 0x808080 },
|
|
[BLOCKS.SAND]: { top: 0xF4D03F, side: 0xF4D03F, bottom: 0xF4D03F },
|
|
[BLOCKS.WATER]: { top: 0x3498DB, side: 0x2980B9, bottom: 0x2471A3 },
|
|
[BLOCKS.WOOD]: { top: 0x6D4C41, side: 0x5D4037, bottom: 0x6D4C41 },
|
|
[BLOCKS.LEAVES]: { top: 0x2E7D32, side: 0x2E7D32, bottom: 0x2E7D32 },
|
|
[BLOCKS.COBBLESTONE]: { top: 0x757575, side: 0x757575, bottom: 0x757575 },
|
|
[BLOCKS.SNOW]: { top: 0xFFFFFF, side: 0xE8E8E8, bottom: 0xE8E8E8 },
|
|
[BLOCKS.CRYSTAL]: { top: 0x00BCD4, side: 0x00ACC1, bottom: 0x0097A7 },
|
|
[BLOCKS.GOLD]: { top: 0xFFD700, side: 0xFFC107, bottom: 0xFFB300 },
|
|
[BLOCKS.OBSIDIAN]: { top: 0x1A1A2E, side: 0x16213E, bottom: 0x0F3460 }
|
|
};
|
|
|
|
const BLOCK_NAMES = {
|
|
[BLOCKS.GRASS]: 'Grass', [BLOCKS.DIRT]: 'Dirt', [BLOCKS.STONE]: 'Stone',
|
|
[BLOCKS.SAND]: 'Sand', [BLOCKS.WATER]: 'Water', [BLOCKS.WOOD]: 'Wood',
|
|
[BLOCKS.LEAVES]: 'Leaves', [BLOCKS.COBBLESTONE]: 'Cobblestone',
|
|
[BLOCKS.SNOW]: 'Snow', [BLOCKS.CRYSTAL]: 'Crystal', [BLOCKS.GOLD]: 'Gold',
|
|
[BLOCKS.OBSIDIAN]: 'Obsidian'
|
|
};
|
|
|
|
// ============================================================
|
|
// GAME STATE
|
|
// ============================================================
|
|
const CHUNK_SIZE = 16;
|
|
const RENDER_DISTANCE = 4;
|
|
const WORLD_SEED = 42;
|
|
|
|
const noise = new PerlinNoise(WORLD_SEED);
|
|
const chunks = new Map();
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x87CEEB);
|
|
scene.fog = new THREE.Fog(0x87CEEB, 50, 120);
|
|
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 200);
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// Player state
|
|
const player = {
|
|
position: new THREE.Vector3(0, 40, 0),
|
|
velocity: new THREE.Vector3(0, 0, 0),
|
|
rotation: { x: 0, y: 0 },
|
|
onGround: false,
|
|
selectedSlot: 0,
|
|
speed: 5,
|
|
jumpForce: 8,
|
|
gravity: -20
|
|
};
|
|
|
|
const inventory = [
|
|
BLOCKS.GRASS, BLOCKS.DIRT, BLOCKS.STONE, BLOCKS.SAND,
|
|
BLOCKS.WOOD, BLOCKS.LEAVES, BLOCKS.COBBLESTONE, BLOCKS.SNOW, BLOCKS.CRYSTAL
|
|
];
|
|
|
|
const keys = {};
|
|
let isLocked = false;
|
|
let lastTime = performance.now();
|
|
let frameCount = 0;
|
|
let fps = 0;
|
|
|
|
// ============================================================
|
|
// MYTHIC LANDMARKS
|
|
// ============================================================
|
|
const landmarks = [
|
|
{ name: 'Crystal Tower', x: 50, z: 50, teleportY: 45 },
|
|
{ name: 'Stone Circle', x: -60, z: 40, teleportY: 35 },
|
|
{ name: 'Dragon Skeleton', x: 40, z: -70, teleportY: 40 },
|
|
{ name: 'Sky Ruins', x: -50, z: -60, teleportY: 60 }
|
|
];
|
|
let currentLandmark = -1;
|
|
|
|
// ============================================================
|
|
// TEXTURE GENERATION
|
|
// ============================================================
|
|
function createBlockTexture(blockType, face) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 16;
|
|
canvas.height = 16;
|
|
const ctx = canvas.getContext('2d');
|
|
const colors = BLOCK_COLORS[blockType];
|
|
const color = face === 'top' ? colors.top : (face === 'bottom' ? colors.bottom : colors.side);
|
|
|
|
// Base color
|
|
ctx.fillStyle = `#${color.toString(16).padStart(6, '0')}`;
|
|
ctx.fillRect(0, 0, 16, 16);
|
|
|
|
// Add texture details
|
|
if (blockType === BLOCKS.GRASS && face === 'top') {
|
|
// Grass blades
|
|
ctx.fillStyle = '#388E3C';
|
|
for (let i = 0; i < 20; i++) {
|
|
const x = Math.random() * 16;
|
|
const y = Math.random() * 16;
|
|
ctx.fillRect(x, y, 1, 2);
|
|
}
|
|
} else if (blockType === BLOCKS.GRASS && face === 'side') {
|
|
// Dirt with grass top
|
|
ctx.fillStyle = '#4CAF50';
|
|
ctx.fillRect(0, 0, 16, 3);
|
|
} else if (blockType === BLOCKS.STONE || blockType === BLOCKS.COBBLESTONE) {
|
|
// Stone cracks
|
|
ctx.fillStyle = '#616161';
|
|
for (let i = 0; i < 5; i++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(Math.random() * 16, Math.random() * 16);
|
|
ctx.lineTo(Math.random() * 16, Math.random() * 16);
|
|
ctx.stroke();
|
|
}
|
|
} else if (blockType === BLOCKS.WOOD && face === 'side') {
|
|
// Wood grain
|
|
ctx.fillStyle = '#4E342E';
|
|
for (let y = 0; y < 16; y += 4) {
|
|
ctx.fillRect(0, y, 16, 1);
|
|
}
|
|
} else if (blockType === BLOCKS.LEAVES) {
|
|
// Leaf holes
|
|
ctx.fillStyle = '#1B5E20';
|
|
for (let i = 0; i < 8; i++) {
|
|
ctx.fillRect(Math.random() * 14, Math.random() * 14, 2, 2);
|
|
}
|
|
} else if (blockType === BLOCKS.CRYSTAL) {
|
|
// Crystal shine
|
|
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
|
ctx.fillRect(2, 2, 4, 4);
|
|
ctx.fillRect(10, 8, 3, 3);
|
|
} else if (blockType === BLOCKS.GOLD) {
|
|
// Gold sparkle
|
|
ctx.fillStyle = 'rgba(255,255,255,0.4)';
|
|
ctx.fillRect(4, 4, 2, 2);
|
|
ctx.fillRect(10, 10, 2, 2);
|
|
}
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
texture.magFilter = THREE.NearestFilter;
|
|
texture.minFilter = THREE.NearestFilter;
|
|
return texture;
|
|
}
|
|
|
|
function createBlockMaterial(blockType) {
|
|
return {
|
|
top: new THREE.MeshLambertMaterial({ map: createBlockTexture(blockType, 'top') }),
|
|
side: new THREE.MeshLambertMaterial({ map: createBlockTexture(blockType, 'side') }),
|
|
bottom: new THREE.MeshLambertMaterial({ map: createBlockTexture(blockType, 'bottom') })
|
|
};
|
|
}
|
|
|
|
const blockMaterials = {};
|
|
for (const type of Object.values(BLOCKS)) {
|
|
if (type !== BLOCKS.AIR) {
|
|
blockMaterials[type] = createBlockMaterial(type);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// CHUNK MANAGEMENT
|
|
// ============================================================
|
|
function getChunkKey(cx, cz) {
|
|
return `${cx},${cz}`;
|
|
}
|
|
|
|
function getHeight(x, z) {
|
|
// Multi-octave terrain
|
|
const base = noise.octave(x * 0.01, 0, z * 0.01, 4, 0.5) * 20 + 25;
|
|
const mountains = Math.max(0, noise.octave(x * 0.005, 0, z * 0.005, 3, 0.6)) * 30;
|
|
return Math.floor(base + mountains);
|
|
}
|
|
|
|
function getBiome(x, z) {
|
|
const temp = noise.octave(x * 0.003, 0, z * 0.003, 2, 0.5);
|
|
const moisture = noise.octave(x * 0.004, 100, z * 0.004, 2, 0.5);
|
|
|
|
if (temp > 0.3) return 'desert';
|
|
if (temp < -0.3) return 'snow';
|
|
if (moisture > 0.2) return 'forest';
|
|
return 'plains';
|
|
}
|
|
|
|
function generateChunk(cx, cz) {
|
|
const key = getChunkKey(cx, cz);
|
|
if (chunks.has(key)) return chunks.get(key);
|
|
|
|
const chunk = {
|
|
blocks: new Uint8Array(CHUNK_SIZE * 256 * CHUNK_SIZE),
|
|
mesh: null,
|
|
dirty: true
|
|
};
|
|
|
|
// Generate terrain
|
|
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
const worldX = cx * CHUNK_SIZE + x;
|
|
const worldZ = cz * CHUNK_SIZE + z;
|
|
const height = getHeight(worldX, worldZ);
|
|
const biome = getBiome(worldX, worldZ);
|
|
|
|
for (let y = 0; y < 256; y++) {
|
|
const idx = x + y * CHUNK_SIZE + z * CHUNK_SIZE * 256;
|
|
|
|
if (y === 0) {
|
|
chunk.blocks[idx] = BLOCKS.STONE;
|
|
} else if (y < height - 4) {
|
|
chunk.blocks[idx] = BLOCKS.STONE;
|
|
} else if (y < height) {
|
|
chunk.blocks[idx] = biome === 'desert' ? BLOCKS.SAND : BLOCKS.DIRT;
|
|
} else if (y === height) {
|
|
if (biome === 'desert') chunk.blocks[idx] = BLOCKS.SAND;
|
|
else if (biome === 'snow') chunk.blocks[idx] = BLOCKS.SNOW;
|
|
else chunk.blocks[idx] = BLOCKS.GRASS;
|
|
} else if (y <= 20 && y > height) {
|
|
chunk.blocks[idx] = BLOCKS.WATER;
|
|
} else {
|
|
chunk.blocks[idx] = BLOCKS.AIR;
|
|
}
|
|
}
|
|
|
|
// Trees
|
|
if (height > 20 && biome === 'forest' && Math.random() < 0.03) {
|
|
const treeHeight = 4 + Math.floor(Math.random() * 3);
|
|
for (let ty = 1; ty <= treeHeight; ty++) {
|
|
const idx = x + (height + ty) * CHUNK_SIZE + z * CHUNK_SIZE * 256;
|
|
chunk.blocks[idx] = BLOCKS.WOOD;
|
|
}
|
|
// Leaves
|
|
for (let lx = -2; lx <= 2; lx++) {
|
|
for (let lz = -2; lz <= 2; lz++) {
|
|
for (let ly = treeHeight - 1; ly <= treeHeight + 1; ly++) {
|
|
const nx = x + lx, nz = z + lz;
|
|
if (nx >= 0 && nx < CHUNK_SIZE && nz >= 0 && nz < CHUNK_SIZE) {
|
|
const idx = nx + (height + ly) * CHUNK_SIZE + nz * CHUNK_SIZE * 256;
|
|
if (chunk.blocks[idx] === BLOCKS.AIR) {
|
|
chunk.blocks[idx] = BLOCKS.LEAVES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
chunks.set(key, chunk);
|
|
return chunk;
|
|
}
|
|
|
|
function getBlock(x, y, z) {
|
|
if (y < 0 || y >= 256) return BLOCKS.AIR;
|
|
const cx = Math.floor(x / CHUNK_SIZE);
|
|
const cz = Math.floor(z / CHUNK_SIZE);
|
|
const key = getChunkKey(cx, cz);
|
|
const chunk = chunks.get(key);
|
|
if (!chunk) return BLOCKS.AIR;
|
|
|
|
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
return chunk.blocks[lx + y * CHUNK_SIZE + lz * CHUNK_SIZE * 256];
|
|
}
|
|
|
|
function setBlock(x, y, z, type) {
|
|
if (y < 0 || y >= 256) return;
|
|
const cx = Math.floor(x / CHUNK_SIZE);
|
|
const cz = Math.floor(z / CHUNK_SIZE);
|
|
const key = getChunkKey(cx, cz);
|
|
let chunk = chunks.get(key);
|
|
|
|
if (!chunk) {
|
|
chunk = generateChunk(cx, cz);
|
|
}
|
|
|
|
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
chunk.blocks[lx + y * CHUNK_SIZE + lz * CHUNK_SIZE * 256] = type;
|
|
chunk.dirty = true;
|
|
|
|
// Mark adjacent chunks dirty if on border
|
|
if (lx === 0) markChunkDirty(cx - 1, cz);
|
|
if (lx === CHUNK_SIZE - 1) markChunkDirty(cx + 1, cz);
|
|
if (lz === 0) markChunkDirty(cx, cz - 1);
|
|
if (lz === CHUNK_SIZE - 1) markChunkDirty(cx, cz + 1);
|
|
}
|
|
|
|
function markChunkDirty(cx, cz) {
|
|
const key = getChunkKey(cx, cz);
|
|
const chunk = chunks.get(key);
|
|
if (chunk) chunk.dirty = true;
|
|
}
|
|
|
|
function buildChunkMesh(chunk, cx, cz) {
|
|
if (chunk.mesh) {
|
|
scene.remove(chunk.mesh);
|
|
chunk.mesh.geometry.dispose();
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
const vertices = [];
|
|
const normals = [];
|
|
const uvs = [];
|
|
const indices = [];
|
|
const colors = [];
|
|
|
|
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
for (let y = 0; y < 256; y++) {
|
|
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
const block = chunk.blocks[x + y * CHUNK_SIZE + z * CHUNK_SIZE * 256];
|
|
if (block === BLOCKS.AIR) continue;
|
|
|
|
const worldX = cx * CHUNK_SIZE + x;
|
|
const worldZ = cz * CHUNK_SIZE + z;
|
|
const blockColors = BLOCK_COLORS[block];
|
|
|
|
// Check each face
|
|
const faces = [
|
|
{ dir: [0, 1, 0], color: blockColors.top, normal: [0, 1, 0] },
|
|
{ dir: [0, -1, 0], color: blockColors.bottom, normal: [0, -1, 0] },
|
|
{ dir: [1, 0, 0], color: blockColors.side, normal: [1, 0, 0] },
|
|
{ dir: [-1, 0, 0], color: blockColors.side, normal: [-1, 0, 0] },
|
|
{ dir: [0, 0, 1], color: blockColors.side, normal: [0, 0, 1] },
|
|
{ dir: [0, 0, -1], color: blockColors.side, normal: [0, 0, -1] }
|
|
];
|
|
|
|
for (const face of faces) {
|
|
const nx = worldX + face.dir[0];
|
|
const ny = y + face.dir[1];
|
|
const nz = worldZ + face.dir[2];
|
|
const neighbor = getBlock(nx, ny, nz);
|
|
|
|
if (neighbor === BLOCKS.AIR || (neighbor === BLOCKS.WATER && block !== BLOCKS.WATER)) {
|
|
const vertexIndex = vertices.length / 3;
|
|
|
|
// Add face vertices
|
|
if (face.dir[1] === 1) { // Top
|
|
vertices.push(worldX, y + 1, worldZ);
|
|
vertices.push(worldX + 1, y + 1, worldZ);
|
|
vertices.push(worldX + 1, y + 1, worldZ + 1);
|
|
vertices.push(worldX, y + 1, worldZ + 1);
|
|
} else if (face.dir[1] === -1) { // Bottom
|
|
vertices.push(worldX, y, worldZ + 1);
|
|
vertices.push(worldX + 1, y, worldZ + 1);
|
|
vertices.push(worldX + 1, y, worldZ);
|
|
vertices.push(worldX, y, worldZ);
|
|
} else if (face.dir[0] === 1) { // Right
|
|
vertices.push(worldX + 1, y, worldZ);
|
|
vertices.push(worldX + 1, y + 1, worldZ);
|
|
vertices.push(worldX + 1, y + 1, worldZ + 1);
|
|
vertices.push(worldX + 1, y, worldZ + 1);
|
|
} else if (face.dir[0] === -1) { // Left
|
|
vertices.push(worldX, y, worldZ + 1);
|
|
vertices.push(worldX, y + 1, worldZ + 1);
|
|
vertices.push(worldX, y + 1, worldZ);
|
|
vertices.push(worldX, y, worldZ);
|
|
} else if (face.dir[2] === 1) { // Front
|
|
vertices.push(worldX + 1, y, worldZ + 1);
|
|
vertices.push(worldX + 1, y + 1, worldZ + 1);
|
|
vertices.push(worldX, y + 1, worldZ + 1);
|
|
vertices.push(worldX, y, worldZ + 1);
|
|
} else { // Back
|
|
vertices.push(worldX, y, worldZ);
|
|
vertices.push(worldX, y + 1, worldZ);
|
|
vertices.push(worldX + 1, y + 1, worldZ);
|
|
vertices.push(worldX + 1, y, worldZ);
|
|
}
|
|
|
|
// Add normals
|
|
for (let i = 0; i < 4; i++) {
|
|
normals.push(...face.normal);
|
|
}
|
|
|
|
// Add colors
|
|
const r = ((face.color >> 16) & 0xFF) / 255;
|
|
const g = ((face.color >> 8) & 0xFF) / 255;
|
|
const b = (face.color & 0xFF) / 255;
|
|
for (let i = 0; i < 4; i++) {
|
|
colors.push(r, g, b);
|
|
}
|
|
|
|
// Add UVs
|
|
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
|
|
|
// Add indices
|
|
indices.push(
|
|
vertexIndex, vertexIndex + 1, vertexIndex + 2,
|
|
vertexIndex, vertexIndex + 2, vertexIndex + 3
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (vertices.length === 0) return;
|
|
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
geometry.setIndex(indices);
|
|
|
|
const material = new THREE.MeshLambertMaterial({ vertexColors: true });
|
|
chunk.mesh = new THREE.Mesh(geometry, material);
|
|
scene.add(chunk.mesh);
|
|
chunk.dirty = false;
|
|
}
|
|
|
|
// ============================================================
|
|
// MYTHIC STRUCTURES
|
|
// ============================================================
|
|
function buildCrystalTower(cx, cz) {
|
|
const baseX = cx * CHUNK_SIZE + 7;
|
|
const baseZ = cz * CHUNK_SIZE + 7;
|
|
const baseY = getHeight(baseX, baseZ);
|
|
|
|
// Crystal tower
|
|
for (let y = 0; y < 20; y++) {
|
|
const radius = Math.max(1, 4 - Math.floor(y / 5));
|
|
for (let dx = -radius; dx <= radius; dx++) {
|
|
for (let dz = -radius; dz <= radius; dz++) {
|
|
if (dx * dx + dz * dz <= radius * radius) {
|
|
setBlock(baseX + dx, baseY + y, baseZ + dz, BLOCKS.CRYSTAL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Top beacon
|
|
for (let y = 20; y < 25; y++) {
|
|
setBlock(baseX, baseY + y, baseZ, BLOCKS.GOLD);
|
|
}
|
|
}
|
|
|
|
function buildStoneCircle(cx, cz) {
|
|
const baseX = cx * CHUNK_SIZE + 8;
|
|
const baseZ = cz * CHUNK_SIZE + 8;
|
|
const baseY = getHeight(baseX, baseZ);
|
|
|
|
// Stone circle
|
|
for (let i = 0; i < 8; i++) {
|
|
const angle = (i / 8) * Math.PI * 2;
|
|
const x = Math.round(baseX + Math.cos(angle) * 6);
|
|
const z = Math.round(baseZ + Math.sin(angle) * 6);
|
|
|
|
// Standing stones
|
|
for (let y = 0; y < 5; y++) {
|
|
setBlock(x, baseY + y, z, BLOCKS.COBBLESTONE);
|
|
}
|
|
// Cap stone
|
|
setBlock(x, baseY + 5, z, BLOCKS.COBBLESTONE);
|
|
}
|
|
|
|
// Central altar
|
|
for (let dx = -1; dx <= 1; dx++) {
|
|
for (let dz = -1; dz <= 1; dz++) {
|
|
setBlock(baseX + dx, baseY, baseZ + dz, BLOCKS.OBSIDIAN);
|
|
setBlock(baseX + dx, baseY + 1, baseZ + dz, BLOCKS.GOLD);
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildDragonSkeleton(cx, cz) {
|
|
const baseX = cx * CHUNK_SIZE + 4;
|
|
const baseZ = cz * CHUNK_SIZE + 4;
|
|
const baseY = getHeight(baseX, baseZ);
|
|
|
|
// Spine
|
|
for (let i = 0; i < 12; i++) {
|
|
const x = baseX + i;
|
|
const y = baseY + Math.floor(Math.sin(i * 0.3) * 2) + 2;
|
|
setBlock(x, y, baseZ, BLOCKS.SNOW);
|
|
setBlock(x, y + 1, baseZ, BLOCKS.SNOW);
|
|
}
|
|
|
|
// Ribs
|
|
for (let i = 2; i < 10; i += 2) {
|
|
const x = baseX + i;
|
|
const y = baseY + Math.floor(Math.sin(i * 0.3) * 2) + 2;
|
|
for (let dz = -3; dz <= 3; dz++) {
|
|
if (Math.abs(dz) === 3) {
|
|
setBlock(x, y - 1, baseZ + dz, BLOCKS.SNOW);
|
|
setBlock(x, y - 2, baseZ + dz, BLOCKS.SNOW);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skull
|
|
for (let dx = 0; dx < 3; dx++) {
|
|
for (let dy = 0; dy < 3; dy++) {
|
|
for (let dz = -1; dz <= 1; dz++) {
|
|
setBlock(baseX - 3 + dx, baseY + 3 + dy, baseZ + dz, BLOCKS.SNOW);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Eye sockets
|
|
setBlock(baseX - 2, baseY + 4, baseZ - 1, BLOCKS.OBSIDIAN);
|
|
setBlock(baseX - 2, baseY + 4, baseZ + 1, BLOCKS.OBSIDIAN);
|
|
|
|
// Wings
|
|
for (let i = 0; i < 6; i++) {
|
|
const x = baseX + 4 + i;
|
|
const y = baseY + 5 + i;
|
|
setBlock(x, y, baseZ - 4, BLOCKS.SNOW);
|
|
setBlock(x, y, baseZ + 4, BLOCKS.SNOW);
|
|
}
|
|
}
|
|
|
|
function buildSkyRuins(cx, cz) {
|
|
const baseX = cx * CHUNK_SIZE + 6;
|
|
const baseZ = cz * CHUNK_SIZE + 6;
|
|
const baseY = getHeight(baseX, baseZ) + 15;
|
|
|
|
// Floating platform
|
|
for (let dx = -5; dx <= 5; dx++) {
|
|
for (let dz = -5; dz <= 5; dz++) {
|
|
if (Math.abs(dx) === 5 || Math.abs(dz) === 5) {
|
|
setBlock(baseX + dx, baseY, baseZ + dz, BLOCKS.COBBLESTONE);
|
|
setBlock(baseX + dx, baseY + 1, baseZ + dz, BLOCKS.COBBLESTONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Corner pillars
|
|
const corners = [[-4, -4], [-4, 4], [4, -4], [4, 4]];
|
|
for (const [dx, dz] of corners) {
|
|
for (let y = -5; y <= 3; y++) {
|
|
setBlock(baseX + dx, baseY + y, baseZ + dz, BLOCKS.COBBLESTONE);
|
|
}
|
|
}
|
|
|
|
// Central structure
|
|
for (let dx = -2; dx <= 2; dx++) {
|
|
for (let dz = -2; dz <= 2; dz++) {
|
|
for (let y = 2; y <= 6; y++) {
|
|
if (Math.abs(dx) === 2 || Math.abs(dz) === 2) {
|
|
setBlock(baseX + dx, baseY + y, baseZ + dz, BLOCKS.COBBLESTONE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Crystal on top
|
|
setBlock(baseX, baseY + 7, baseZ, BLOCKS.CRYSTAL);
|
|
setBlock(baseX, baseY + 8, baseZ, BLOCKS.GOLD);
|
|
}
|
|
|
|
// ============================================================
|
|
// LIGHTING
|
|
// ============================================================
|
|
const ambientLight = new THREE.AmbientLight(0x606060);
|
|
scene.add(ambientLight);
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
directionalLight.position.set(50, 100, 50);
|
|
directionalLight.castShadow = true;
|
|
scene.add(directionalLight);
|
|
|
|
const hemisphereLight = new THREE.HemisphereLight(0x87CEEB, 0x8B6914, 0.3);
|
|
scene.add(hemisphereLight);
|
|
|
|
// ============================================================
|
|
// CONTROLS
|
|
// ============================================================
|
|
document.addEventListener('click', () => {
|
|
if (!isLocked) {
|
|
renderer.domElement.requestPointerLock();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('pointerlockchange', () => {
|
|
isLocked = document.pointerLockElement === renderer.domElement;
|
|
document.getElementById('blocker').style.display = isLocked ? 'none' : 'flex';
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isLocked) return;
|
|
player.rotation.y -= e.movementX * 0.002;
|
|
player.rotation.x -= e.movementY * 0.002;
|
|
player.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, player.rotation.x));
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
keys[e.code] = true;
|
|
|
|
// Number keys for inventory
|
|
if (e.code >= 'Digit1' && e.code <= 'Digit9') {
|
|
player.selectedSlot = parseInt(e.code.slice(5)) - 1;
|
|
updateHUD();
|
|
}
|
|
|
|
// G key for teleport
|
|
if (e.code === 'KeyG') {
|
|
currentLandmark = (currentLandmark + 1) % landmarks.length;
|
|
const lm = landmarks[currentLandmark];
|
|
player.position.set(lm.x, lm.teleportY, lm.z);
|
|
player.velocity.set(0, 0, 0);
|
|
showLandmarkIndicator(lm.name);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', (e) => keys[e.code] = false);
|
|
|
|
// Mouse click for breaking/placing
|
|
document.addEventListener('mousedown', (e) => {
|
|
if (!isLocked) return;
|
|
|
|
const direction = new THREE.Vector3(0, 0, -1);
|
|
direction.applyQuaternion(camera.quaternion);
|
|
|
|
const raycaster = new THREE.Raycaster(camera.position, direction, 0, 8);
|
|
|
|
// Simple raycast
|
|
let hitPos = null;
|
|
let hitNormal = null;
|
|
|
|
for (let t = 0; t < 8; t += 0.1) {
|
|
const x = Math.floor(camera.position.x + direction.x * t);
|
|
const y = Math.floor(camera.position.y + direction.y * t);
|
|
const z = Math.floor(camera.position.z + direction.z * t);
|
|
|
|
if (getBlock(x, y, z) !== BLOCKS.AIR) {
|
|
hitPos = { x, y, z };
|
|
hitNormal = {
|
|
x: Math.floor(camera.position.x + direction.x * (t - 0.1)) - x,
|
|
y: Math.floor(camera.position.y + direction.y * (t - 0.1)) - y,
|
|
z: Math.floor(camera.position.z + direction.z * (t - 0.1)) - z
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hitPos) {
|
|
if (e.button === 0) { // Left click - break
|
|
setBlock(hitPos.x, hitPos.y, hitPos.z, BLOCKS.AIR);
|
|
} else if (e.button === 2) { // Right click - place
|
|
const px = hitPos.x + hitNormal.x;
|
|
const py = hitPos.y + hitNormal.y;
|
|
const pz = hitPos.z + hitNormal.z;
|
|
|
|
// Don't place inside player
|
|
const playerBlockX = Math.floor(player.position.x);
|
|
const playerBlockY = Math.floor(player.position.y);
|
|
const playerBlockZ = Math.floor(player.position.z);
|
|
|
|
if (!(px === playerBlockX && (py === playerBlockY || py === playerBlockY - 1) && pz === playerBlockZ)) {
|
|
setBlock(px, py, pz, inventory[player.selectedSlot]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Prevent context menu
|
|
document.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
|
// ============================================================
|
|
// HUD
|
|
// ============================================================
|
|
function createHUD() {
|
|
const hud = document.getElementById('hud');
|
|
hud.innerHTML = '';
|
|
|
|
for (let i = 0; i < 9; i++) {
|
|
const slot = document.createElement('div');
|
|
slot.className = `slot ${i === player.selectedSlot ? 'active' : ''}`;
|
|
|
|
const num = document.createElement('span');
|
|
num.className = 'slot-num';
|
|
num.textContent = i + 1;
|
|
slot.appendChild(num);
|
|
|
|
const preview = document.createElement('div');
|
|
preview.className = 'block-preview';
|
|
const color = BLOCK_COLORS[inventory[i]];
|
|
preview.style.background = `#${color.top.toString(16).padStart(6, '0')}`;
|
|
slot.appendChild(preview);
|
|
|
|
slot.addEventListener('click', () => {
|
|
player.selectedSlot = i;
|
|
updateHUD();
|
|
});
|
|
|
|
hud.appendChild(slot);
|
|
}
|
|
}
|
|
|
|
function updateHUD() {
|
|
const slots = document.querySelectorAll('.slot');
|
|
slots.forEach((slot, i) => {
|
|
slot.className = `slot ${i === player.selectedSlot ? 'active' : ''}`;
|
|
});
|
|
document.getElementById('block-name').textContent = BLOCK_NAMES[inventory[player.selectedSlot]];
|
|
}
|
|
|
|
// ============================================================
|
|
// MINIMAP
|
|
// ============================================================
|
|
function updateMinimap() {
|
|
const canvas = document.getElementById('minimap');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = 150;
|
|
canvas.height = 150;
|
|
|
|
const scale = 2;
|
|
const centerX = player.position.x;
|
|
const centerZ = player.position.z;
|
|
|
|
// Draw terrain
|
|
for (let px = 0; px < 150; px++) {
|
|
for (let pz = 0; pz < 150; pz++) {
|
|
const worldX = centerX + (px - 75) * scale;
|
|
const worldZ = centerZ + (pz - 75) * scale;
|
|
const height = getHeight(worldX, worldZ);
|
|
const biome = getBiome(worldX, worldZ);
|
|
|
|
let color;
|
|
if (height <= 20) color = '#3498DB';
|
|
else if (biome === 'desert') color = '#F4D03F';
|
|
else if (biome === 'snow') color = '#ECF0F1';
|
|
else if (biome === 'forest') color = '#27AE60';
|
|
else color = '#2ECC71';
|
|
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(px, pz, 1, 1);
|
|
}
|
|
}
|
|
|
|
// Draw landmarks
|
|
ctx.fillStyle = '#FFD700';
|
|
for (const lm of landmarks) {
|
|
const px = 75 + (lm.x - centerX) / scale;
|
|
const pz = 75 + (lm.z - centerZ) / scale;
|
|
if (px >= 0 && px < 150 && pz >= 0 && pz < 150) {
|
|
ctx.beginPath();
|
|
ctx.arc(px, pz, 3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Draw player
|
|
ctx.fillStyle = '#FF0000';
|
|
ctx.beginPath();
|
|
ctx.arc(75, 75, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Draw direction
|
|
ctx.strokeStyle = '#FF0000';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(75, 75);
|
|
ctx.lineTo(75 + Math.sin(player.rotation.y) * 10, 75 - Math.cos(player.rotation.y) * 10);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// ============================================================
|
|
// LANDMARK INDICATOR
|
|
// ============================================================
|
|
let landmarkTimeout = null;
|
|
function showLandmarkIndicator(name) {
|
|
const indicator = document.getElementById('landmark-indicator');
|
|
indicator.textContent = `📍 ${name}`;
|
|
indicator.style.display = 'block';
|
|
|
|
if (landmarkTimeout) clearTimeout(landmarkTimeout);
|
|
landmarkTimeout = setTimeout(() => {
|
|
indicator.style.display = 'none';
|
|
}, 2000);
|
|
}
|
|
|
|
// ============================================================
|
|
// PHYSICS
|
|
// ============================================================
|
|
function updatePhysics(dt) {
|
|
// Movement
|
|
const moveSpeed = player.speed * dt;
|
|
const forward = new THREE.Vector3(
|
|
-Math.sin(player.rotation.y) * moveSpeed,
|
|
0,
|
|
-Math.cos(player.rotation.y) * moveSpeed
|
|
);
|
|
const right = new THREE.Vector3(
|
|
Math.cos(player.rotation.y) * moveSpeed,
|
|
0,
|
|
-Math.sin(player.rotation.y) * moveSpeed
|
|
);
|
|
|
|
if (keys['KeyW']) player.position.add(forward);
|
|
if (keys['KeyS']) player.position.sub(forward);
|
|
if (keys['KeyA']) player.position.sub(right);
|
|
if (keys['KeyD']) player.position.add(right);
|
|
|
|
// Gravity
|
|
player.velocity.y += player.gravity * dt;
|
|
player.position.y += player.velocity.y * dt;
|
|
|
|
// Collision detection
|
|
const playerHeight = 1.8;
|
|
const playerWidth = 0.3;
|
|
|
|
// Check ground
|
|
const feetY = player.position.y - playerHeight;
|
|
const blockBelow = getBlock(
|
|
Math.floor(player.position.x),
|
|
Math.floor(feetY),
|
|
Math.floor(player.position.z)
|
|
);
|
|
|
|
if (blockBelow !== BLOCKS.AIR && blockBelow !== BLOCKS.WATER) {
|
|
player.position.y = Math.floor(feetY) + 1 + playerHeight;
|
|
player.velocity.y = 0;
|
|
player.onGround = true;
|
|
} else {
|
|
player.onGround = false;
|
|
}
|
|
|
|
// Jump
|
|
if (keys['Space'] && player.onGround) {
|
|
player.velocity.y = player.jumpForce;
|
|
player.onGround = false;
|
|
}
|
|
|
|
// Water physics
|
|
if (getBlock(Math.floor(player.position.x), Math.floor(player.position.y - 1), Math.floor(player.position.z)) === BLOCKS.WATER) {
|
|
player.velocity.y *= 0.9;
|
|
player.velocity.y += 2 * dt;
|
|
}
|
|
|
|
// Prevent falling through world
|
|
if (player.position.y < -10) {
|
|
player.position.set(0, 40, 0);
|
|
player.velocity.set(0, 0, 0);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// GAME LOOP
|
|
// ============================================================
|
|
function updateChunks() {
|
|
const playerCX = Math.floor(player.position.x / CHUNK_SIZE);
|
|
const playerCZ = Math.floor(player.position.z / CHUNK_SIZE);
|
|
|
|
// Generate/load chunks
|
|
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
|
|
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
|
|
const cx = playerCX + dx;
|
|
const cz = playerCZ + dz;
|
|
const chunk = generateChunk(cx, cz);
|
|
|
|
// Build landmarks on first generation
|
|
if (chunk.dirty) {
|
|
// Check if this chunk should have a landmark
|
|
for (const lm of landmarks) {
|
|
const lmCX = Math.floor(lm.x / CHUNK_SIZE);
|
|
const lmCZ = Math.floor(lm.z / CHUNK_SIZE);
|
|
if (lmCX === cx && lmCZ === cz) {
|
|
if (lm.name === 'Crystal Tower') buildCrystalTower(cx, cz);
|
|
else if (lm.name === 'Stone Circle') buildStoneCircle(cx, cz);
|
|
else if (lm.name === 'Dragon Skeleton') buildDragonSkeleton(cx, cz);
|
|
else if (lm.name === 'Sky Ruins') buildSkyRuins(cx, cz);
|
|
}
|
|
}
|
|
buildChunkMesh(chunk, cx, cz);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unload distant chunks
|
|
for (const [key, chunk] of chunks) {
|
|
const [cx, cz] = key.split(',').map(Number);
|
|
if (Math.abs(cx - playerCX) > RENDER_DISTANCE + 2 || Math.abs(cz - playerCZ) > RENDER_DISTANCE + 2) {
|
|
if (chunk.mesh) {
|
|
scene.remove(chunk.mesh);
|
|
chunk.mesh.geometry.dispose();
|
|
}
|
|
chunks.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const now = performance.now();
|
|
const dt = Math.min((now - lastTime) / 1000, 0.1);
|
|
lastTime = now;
|
|
|
|
// FPS counter
|
|
frameCount++;
|
|
if (frameCount % 30 === 0) {
|
|
fps = Math.round(1 / dt);
|
|
document.getElementById('fps').textContent = fps;
|
|
}
|
|
|
|
if (isLocked) {
|
|
updatePhysics(dt);
|
|
updateChunks();
|
|
|
|
// Update camera
|
|
camera.position.copy(player.position);
|
|
camera.rotation.order = 'YXZ';
|
|
camera.rotation.y = player.rotation.y;
|
|
camera.rotation.x = player.rotation.x;
|
|
|
|
// Update info
|
|
document.getElementById('pos').textContent =
|
|
`${Math.floor(player.position.x)}, ${Math.floor(player.position.y)}, ${Math.floor(player.position.z)}`;
|
|
|
|
// Update minimap every 10 frames
|
|
if (frameCount % 10 === 0) {
|
|
updateMinimap();
|
|
}
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// ============================================================
|
|
// INITIALIZATION
|
|
// ============================================================
|
|
function init() {
|
|
createHUD();
|
|
updateHUD();
|
|
|
|
// Find safe spawn point
|
|
const spawnX = 0, spawnZ = 0;
|
|
const spawnY = getHeight(spawnX, spawnZ) + 2;
|
|
player.position.set(spawnX, spawnY, spawnZ);
|
|
|
|
// Generate initial chunks
|
|
updateChunks();
|
|
|
|
// Start game loop
|
|
animate();
|
|
|
|
console.log('Mythos Craft initialized');
|
|
console.log('Landmarks:', landmarks.map(l => l.name).join(', '));
|
|
}
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
// Start
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|