Reorganize artifacts by model and harness

This commit is contained in:
Jesse Zhang committed 2026-06-13 12:59:01 +01:00
1 parent 4027ba94c2
commit a12017f31e
71 files changed
+95 -48

No files matched your search

@@ -0,0 +1,759 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mythical Minecraft Clone</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #87CEEB;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
color: rgba(255, 255, 255, 0.9);
font-size: 24px;
font-weight: bold;
text-align: center;
pointer-events: none;
z-index: 10;
text-shadow: 1px 1px 2px black;
}
#instructions {
position: absolute;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: rgba(10, 15, 30, 0.85);
backdrop-filter: blur(10px);
color: white;
z-index: 20;
transition: opacity 0.4s;
}
#instructions h1 {
font-size: 5rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #00FFFF, #FF00FF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: glow 2s infinite alternate;
}
@keyframes glow {
from { text-shadow: 0 0 10px rgba(0,255,255,0.2); }
to { text-shadow: 0 0 20px rgba(0,255,255,0.6), 0 0 30px rgba(255,0,255,0.4); }
}
#instructions p {
font-size: 1.2rem;
color: #AAA;
margin-bottom: 20px;
text-align: center;
line-height: 1.5;
}
.play-btn {
padding: 15px 50px;
font-size: 24px;
font-weight: bold;
background: rgba(255,255,255,0.05);
border: 2px solid #00FFFF;
border-radius: 40px;
color: white;
margin-top: 30px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 0 15px rgba(0,255,255,0.3);
}
.play-btn:hover {
background: #00FFFF;
color: #0B0B1A;
box-shadow: 0 0 30px rgba(0,255,255,0.8);
transform: scale(1.05);
}
#inventory {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 15px;
z-index: 10;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
padding: 15px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.1);
}
.slot {
width: 60px;
height: 60px;
border: 3px solid #444;
background-color: rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
color: white;
font-size: 12px;
font-weight: bold;
cursor: pointer;
border-radius: 10px;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
padding-bottom: 5px;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.slot::before {
content: '';
position: absolute;
top: 10px;
width: 25px;
height: 25px;
background-size: cover;
border-radius: 3px;
}
#slot-1::before { background-color: #557A2B; border-bottom: 4px solid #5A3A22; }
#slot-2::before { background-color: #5A3A22; }
#slot-3::before { background-color: #7D7D7D; }
#slot-4::before { background-color: #4A0080; box-shadow: 0 0 10px #00FFFF; }
#slot-5::before { background-color: #FFD700; }
.slot.active {
border-color: #00FFFF;
box-shadow: 0 0 20px rgba(0,255,255,0.5);
transform: translateY(-10px) scale(1.1);
background-color: rgba(0,255,255,0.1);
}
.hotkey {
position: absolute;
top: 2px;
left: 5px;
font-size: 10px;
color: rgba(255,255,255,0.5);
}
</style>
</head>
<body>
<div id="instructions">
<h1>MYTHIC REALMS</h1>
<p>A voxel adventure built in a single HTML file.<br>Explore the infinite terrain, find the floating temple, and build your own legend.</p>
<p style="color: #00FFFF; margin-top: 10px;">WASD to Move &bull; Mouse to Look &bull; Space to Jump<br>Left Click to Break &bull; Right Click to Place &bull; 1-5 to Select Block</p>
<button class="play-btn" id="play-btn">ENTER REALM</button>
</div>
<div id="crosshair">+</div>
<div id="inventory">
<div class="slot active" id="slot-1"><span class="hotkey">1</span>Grass</div>
<div class="slot" id="slot-2"><span class="hotkey">2</span>Dirt</div>
<div class="slot" id="slot-3"><span class="hotkey">3</span>Stone</div>
<div class="slot" id="slot-4"><span class="hotkey">4</span>Magic</div>
<div class="slot" id="slot-5"><span class="hotkey">5</span>Gold</div>
</div>
<!-- Dependencies -->
<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 src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script>
// --- 1. PROCEDURAL TEXTURE GENERATOR ---
function createBlockTexture(type) {
const canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
const ctx = canvas.getContext('2d');
function drawNoise(base, c1, c2, c3) {
ctx.fillStyle = base;
ctx.fillRect(0, 0, 16, 16);
for(let i=0; i<256; i++) {
const r = Math.random();
if (r < 0.2) ctx.fillStyle = c1;
else if (r < 0.4) ctx.fillStyle = c2;
else if (r < 0.6) ctx.fillStyle = c3;
else continue;
ctx.fillRect(Math.floor(Math.random()*16), Math.floor(Math.random()*16), 1, 1);
}
}
if (type === 'dirt') {
drawNoise('#5A3A22', '#4A2A12', '#6A4A32', '#3A1A02');
} else if (type === 'grass_top') {
drawNoise('#557A2B', '#456A1B', '#658A3B', '#355A0B');
} else if (type === 'grass_side') {
drawNoise('#5A3A22', '#4A2A12', '#6A4A32', '#3A1A02');
ctx.fillStyle = '#557A2B';
for(let x=0; x<16; x++) {
ctx.fillRect(x, 0, 1, 3 + Math.floor(Math.random()*4));
}
} else if (type === 'stone') {
drawNoise('#7D7D7D', '#6D6D6D', '#8D8D8D', '#5D5D5D');
} else if (type === 'magic') {
drawNoise('#4A0080', '#6A00A0', '#2A0060', '#8A00C0');
ctx.fillStyle = '#00FFFF';
for(let i=0; i<15; i++) {
ctx.fillRect(Math.floor(Math.random()*16), Math.floor(Math.random()*16), 1, 1);
}
} else if (type === 'gold') {
drawNoise('#FFD700', '#FFA500', '#FFFF00', '#DAA520');
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(1, 1, 2, 2);
ctx.fillRect(13, 13, 2, 2);
}
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
return tex;
}
// --- 2. GAME SETUP & THREE.JS ---
const scene = new THREE.Scene();
scene.background = new THREE.Color('#87CEEB');
scene.fog = new THREE.Fog('#87CEEB', 20, 60);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: false }); // False for retro look
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Lighting
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
scene.add(hemiLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(50, 100, 50);
dirLight.castShadow = true;
dirLight.shadow.camera.left = -40;
dirLight.shadow.camera.right = 40;
dirLight.shadow.camera.top = 40;
dirLight.shadow.camera.bottom = -40;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
scene.add(dirLight);
// Materials & Geometries
const chunkGeometries = {
1: new THREE.BoxGeometry(1, 1, 1),
2: new THREE.BoxGeometry(1, 1, 1),
3: new THREE.BoxGeometry(1, 1, 1),
4: new THREE.BoxGeometry(1, 1, 1),
5: new THREE.BoxGeometry(1, 1, 1)
};
const materials = {
1: [
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') }),
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') }),
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_top') }),
new THREE.MeshLambertMaterial({ map: createBlockTexture('dirt') }),
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') }),
new THREE.MeshLambertMaterial({ map: createBlockTexture('grass_side') })
],
2: new THREE.MeshLambertMaterial({ map: createBlockTexture('dirt') }),
3: new THREE.MeshLambertMaterial({ map: createBlockTexture('stone') }),
4: new THREE.MeshLambertMaterial({ map: createBlockTexture('magic'), emissive: 0x4A0080, emissiveIntensity: 0.6 }),
5: new THREE.MeshStandardMaterial({ map: createBlockTexture('gold'), metalness: 0.6, roughness: 0.3 })
};
// --- 3. WORLD GENERATION ---
const simplex = new SimplexNoise();
const chunks = new Map();
let currentChunkX = -999;
let currentChunkZ = -999;
const renderDistance = 3;
function getElevation(wx, wz) {
let e = 1 * simplex.noise2D(wx / 100, wz / 100) +
0.5 * simplex.noise2D(wx / 50, wz / 50) +
0.25 * simplex.noise2D(wx / 25, wz / 25);
return Math.floor(Math.pow((e / 1.75) * 0.5 + 0.5, 2) * 30 + 10);
}
function generateMythicalStructure(wx, wy, wz) {
const dist = Math.sqrt(wx*wx + wz*wz);
// 1. Floating Island Base
if (dist < 40 && wy >= 45 && wy < 60) {
const islandBottom = 60 - Math.pow((40 - dist) / 40, 0.7) * 15;
if (wy >= islandBottom) {
if (wy === 59) return 1;
else if (wy > 55) return 2;
else return 3;
}
}
// 2. Temple Structure
if (wy >= 60 && wy < 110) {
if (dist < 25 && wy === 60) return 3; // Floor
// Circular stairs
if (dist < 20 && wy === 61) return 3;
if (dist < 15 && wy === 62) return 3;
if (dist < 10 && wy === 63) return 3;
// Pillars
const angle = Math.atan2(wz, wx);
const numPillars = 8;
const segment = (Math.PI * 2) / numPillars;
const nearestPillarAngle = Math.round(angle / segment) * segment;
const px = Math.cos(nearestPillarAngle) * 18;
const pz = Math.sin(nearestPillarAngle) * 18;
if (Math.abs(wx - px) <= 1 && Math.abs(wz - pz) <= 1 && wy >= 60 && wy <= 80) {
return (wy % 6 === 0) ? 5 : 3;
}
// Roof
if (wy >= 81 && wy < 95) {
const roofRadius = 22 - (wy - 81) * 1.5;
if (dist < roofRadius && dist > roofRadius - 2) {
return 4;
}
}
// Central Altar
if (dist < 3 && wy >= 64 && wy <= 65) return 5;
if (dist < 1 && wy === 66) return 4;
// Floating magic halo
if (wy === 100 && Math.abs(dist - 10) < 1) return 5;
}
// 3. Floating crystals
const hash = Math.abs(Math.sin(wx * 12.9898 + wz * 78.233) * 43758.5453);
if (hash > 0.999 && dist > 45 && dist < 100 && wy > 50 && wy < 90) {
return 4;
}
return null;
}
class Chunk {
constructor(cx, cz) {
this.cx = cx;
this.cz = cz;
this.blocks = new Uint8Array(16 * 128 * 16);
this.meshes = {};
this.generate();
this.updateMesh();
}
generate() {
for(let x=0; x<16; x++) {
for(let z=0; z<16; z++) {
const wx = this.cx * 16 + x;
const wz = this.cz * 16 + z;
const elevation = getElevation(wx, wz);
const distToCenter = Math.sqrt(wx*wx + wz*wz);
for(let y=0; y<128; y++) {
let type = 0;
const mythicType = generateMythicalStructure(wx, y, wz);
if (mythicType !== null) {
type = mythicType;
} else if (y <= elevation) {
// Clear a space beneath the floating island
if (distToCenter < 45 && y > 30) {
type = 0;
} else {
if (y === elevation) type = 1;
else if (y > elevation - 4) type = 2;
else type = 3;
}
}
this.blocks[x + z*16 + y*256] = type;
}
}
}
}
isSolid(bx, y, bz) {
if (bx < 0 || bx >= 16 || bz < 0 || bz >= 16 || y < 0 || y >= 128) {
return getBlock(this.cx * 16 + bx, y, this.cz * 16 + bz) !== 0;
}
return this.blocks[bx + bz * 16 + y * 256] !== 0;
}
updateMesh() {
for (let type in this.meshes) {
scene.remove(this.meshes[type]);
this.meshes[type].dispose();
}
this.meshes = {};
const counts = {};
for(let i=0; i<this.blocks.length; i++) {
const type = this.blocks[i];
if (type !== 0) {
const x = i % 16;
const z = Math.floor(i / 16) % 16;
const y = Math.floor(i / 256);
if (this.isSolid(x+1, y, z) && this.isSolid(x-1, y, z) &&
this.isSolid(x, y+1, z) && this.isSolid(x, y-1, z) &&
this.isSolid(x, y, z+1) && this.isSolid(x, y, z-1)) {
continue;
}
counts[type] = (counts[type] || 0) + 1;
}
}
for (let type in counts) {
if (counts[type] > 0) {
this.meshes[type] = new THREE.InstancedMesh(chunkGeometries[type], materials[type], counts[type]);
this.meshes[type].frustumCulled = false;
// Only let some chunks cast shadows to save performance
if (Math.abs(this.cx) <= 1 && Math.abs(this.cz) <= 1) {
this.meshes[type].castShadow = true;
this.meshes[type].receiveShadow = true;
}
scene.add(this.meshes[type]);
}
}
const dummy = new THREE.Object3D();
const counters = {};
for (let type in counts) counters[type] = 0;
for(let i=0; i<this.blocks.length; i++) {
const type = this.blocks[i];
if (type !== 0) {
const x = i % 16;
const z = Math.floor(i / 16) % 16;
const y = Math.floor(i / 256);
if (this.isSolid(x+1, y, z) && this.isSolid(x-1, y, z) &&
this.isSolid(x, y+1, z) && this.isSolid(x, y-1, z) &&
this.isSolid(x, y, z+1) && this.isSolid(x, y, z-1)) {
continue;
}
if (this.meshes[type]) {
dummy.position.set(this.cx*16 + x, y, this.cz*16 + z);
dummy.updateMatrix();
this.meshes[type].setMatrixAt(counters[type]++, dummy.matrix);
}
}
}
for (let type in this.meshes) {
this.meshes[type].instanceMatrix.needsUpdate = true;
}
}
}
function getBlock(x, y, z) {
const cx = Math.floor(x / 16);
const cz = Math.floor(z / 16);
const chunk = chunks.get(`${cx},${cz}`);
if (!chunk || y < 0 || y >= 128) return 0;
const bx = x - cx * 16;
const bz = z - cz * 16;
return chunk.blocks[bx + bz * 16 + y * 256];
}
function setBlock(x, y, z, type) {
const cx = Math.floor(x / 16);
const cz = Math.floor(z / 16);
const chunk = chunks.get(`${cx},${cz}`);
if (!chunk || y < 0 || y >= 128) return;
const bx = x - cx * 16;
const bz = z - cz * 16;
chunk.blocks[bx + bz * 16 + y * 256] = type;
chunk.updateMesh();
// Update neighbor chunks if on edge
if (bx === 0) chunks.get(`${cx-1},${cz}`)?.updateMesh();
if (bx === 15) chunks.get(`${cx+1},${cz}`)?.updateMesh();
if (bz === 0) chunks.get(`${cx},${cz-1}`)?.updateMesh();
if (bz === 15) chunks.get(`${cx},${cz+1}`)?.updateMesh();
}
function updateChunks(px, pz) {
const cx = Math.floor(Math.round(px) / 16);
const cz = Math.floor(Math.round(pz) / 16);
if (cx !== currentChunkX || cz !== currentChunkZ || chunks.size === 0) {
currentChunkX = cx;
currentChunkZ = cz;
for(let x = cx - renderDistance; x <= cx + renderDistance; x++) {
for(let z = cz - renderDistance; z <= cz + renderDistance; z++) {
const key = `${x},${z}`;
if (!chunks.has(key)) {
chunks.set(key, new Chunk(x, z));
}
}
}
for (const [key, chunk] of chunks.entries()) {
if (Math.abs(chunk.cx - cx) > renderDistance + 1 ||
Math.abs(chunk.cz - cz) > renderDistance + 1) {
for (let type in chunk.meshes) {
scene.remove(chunk.meshes[type]);
chunk.meshes[type].dispose();
}
chunks.delete(key);
}
}
}
}
// --- 4. PLAYER & PHYSICS ---
const player = {
position: new THREE.Vector3(0, 70, 20), // Spawn looking at the temple
velocity: new THREE.Vector3(0, 0, 0),
onGround: false,
speed: 8,
jumpForce: 10,
gravity: 25
};
camera.position.copy(player.position);
const controls = new THREE.PointerLockControls(camera, document.body);
const instructions = document.getElementById('instructions');
const playBtn = document.getElementById('play-btn');
playBtn.addEventListener('click', () => controls.lock());
controls.addEventListener('lock', () => instructions.style.opacity = '0');
controls.addEventListener('unlock', () => instructions.style.opacity = '1');
// Actually hide it after transition
instructions.addEventListener('transitionend', (e) => {
if (instructions.style.opacity === '0') instructions.style.display = 'none';
});
// Show immediately before transition
controls.addEventListener('unlock', () => {
instructions.style.display = 'flex';
setTimeout(() => instructions.style.opacity = '1', 10);
});
const keys = {};
document.addEventListener('keydown', e => keys[e.code] = true);
document.addEventListener('keyup', e => keys[e.code] = false);
function checkCollision(pos) {
const pr = 0.3;
const pMinX = pos.x - pr;
const pMaxX = pos.x + pr;
const pMinY = pos.y;
const pMaxY = pos.y + 1.6; // Player height
const pMinZ = pos.z - pr;
const pMaxZ = pos.z + pr;
const bMinX = Math.floor(pMinX + 0.5);
const bMaxX = Math.floor(pMaxX + 0.5);
const bMinY = Math.floor(pMinY + 0.5);
const bMaxY = Math.floor(pMaxY + 0.5);
const bMinZ = Math.floor(pMinZ + 0.5);
const bMaxZ = Math.floor(pMaxZ + 0.5);
for (let x = bMinX; x <= bMaxX; x++) {
for (let y = bMinY; y <= bMaxY; y++) {
for (let z = bMinZ; z <= bMaxZ; z++) {
if (getBlock(x, y, z) !== 0) {
const blockMinX = x - 0.5;
const blockMaxX = x + 0.5;
const blockMinY = y - 0.5;
const blockMaxY = y + 0.5;
const blockMinZ = z - 0.5;
const blockMaxZ = z + 0.5;
if (pMinX < blockMaxX && pMaxX > blockMinX &&
pMinY < blockMaxY && pMaxY > blockMinY &&
pMinZ < blockMaxZ && pMaxZ > blockMinZ) {
return true;
}
}
}
}
}
return false;
}
function updatePlayer(delta) {
player.velocity.y -= player.gravity * delta;
const moveDir = new THREE.Vector3();
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3();
right.crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
if (keys['KeyW']) moveDir.add(forward);
if (keys['KeyS']) moveDir.sub(forward);
if (keys['KeyD']) moveDir.add(right);
if (keys['KeyA']) moveDir.sub(right);
if (moveDir.lengthSq() > 0) moveDir.normalize();
const moveStep = moveDir.multiplyScalar(player.speed * delta);
player.position.x += moveStep.x;
if (checkCollision(player.position)) player.position.x -= moveStep.x;
player.position.z += moveStep.z;
if (checkCollision(player.position)) player.position.z -= moveStep.z;
player.position.y += player.velocity.y * delta;
player.onGround = false;
if (checkCollision(player.position)) {
if (player.velocity.y < 0) {
player.onGround = true;
player.position.y = Math.floor(player.position.y) + 0.5;
} else {
player.position.y = Math.floor(player.position.y + 1.6) - 1.6 + 0.5;
}
player.velocity.y = 0;
}
if (keys['Space'] && player.onGround) {
player.velocity.y = player.jumpForce;
}
if (player.position.y < -20) {
player.position.set(0, 80, 20); // Reset if fall off
player.velocity.y = 0;
}
camera.position.copy(player.position);
camera.position.y += 1.4; // Eye height relative to feet
// Move light to follow player for dynamic shadows
dirLight.position.set(player.position.x + 50, player.position.y + 100, player.position.z + 50);
dirLight.target.position.copy(player.position);
dirLight.target.updateMatrixWorld();
}
// --- 5. INTERACTION & INVENTORY ---
let selectedBlock = 1;
function setSelectedBlock(type) {
selectedBlock = type;
document.querySelectorAll('.slot').forEach(el => el.classList.remove('active'));
document.getElementById(`slot-${type}`).classList.add('active');
}
document.addEventListener('keydown', (e) => {
if (e.code === 'Digit1') setSelectedBlock(1);
if (e.code === 'Digit2') setSelectedBlock(2);
if (e.code === 'Digit3') setSelectedBlock(3);
if (e.code === 'Digit4') setSelectedBlock(4);
if (e.code === 'Digit5') setSelectedBlock(5);
});
document.querySelectorAll('.slot').forEach(el => {
el.addEventListener('click', (e) => {
const type = parseInt(e.currentTarget.id.split('-')[1]);
setSelectedBlock(type);
});
});
function raycastVoxel(origin, direction, maxDistance) {
let t = 0;
const step = 0.05;
const pos = new THREE.Vector3();
let lastBlockPos = null;
while (t < maxDistance) {
pos.copy(origin).addScaledVector(direction, t);
const bx = Math.floor(pos.x + 0.5);
const by = Math.floor(pos.y + 0.5);
const bz = Math.floor(pos.z + 0.5);
const type = getBlock(bx, by, bz);
if (type !== 0) {
return { hit: new THREE.Vector3(bx, by, bz), normalPos: lastBlockPos };
}
lastBlockPos = new THREE.Vector3(bx, by, bz);
t += step;
}
return null;
}
document.addEventListener('mousedown', (e) => {
if (!controls.isLocked) return;
const dir = new THREE.Vector3();
camera.getWorldDirection(dir);
const hitInfo = raycastVoxel(camera.position, dir, 8);
if (hitInfo) {
if (e.button === 0) {
setBlock(hitInfo.hit.x, hitInfo.hit.y, hitInfo.hit.z, 0);
} else if (e.button === 2) {
if (hitInfo.normalPos) {
const p = hitInfo.normalPos;
const oldType = getBlock(p.x, p.y, p.z);
setBlock(p.x, p.y, p.z, selectedBlock);
if (checkCollision(player.position)) {
setBlock(p.x, p.y, p.z, oldType);
}
}
}
}
});
document.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- 6. MAIN LOOP ---
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const time = performance.now();
const delta = Math.min((time - lastTime) / 1000, 0.1);
lastTime = time;
if (controls.isLocked) {
updatePlayer(delta);
updateChunks(player.position.x, player.position.z);
}
renderer.render(scene, camera);
}
// Initial chunk generation around spawn
updateChunks(player.position.x, player.position.z);
animate();
</script>
</body>
</html>
@@ -0,0 +1,349 @@
<svg viewBox="0 0 1000 700" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 环境光影渐变 -->
<linearGradient id="skyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#4a90e2" />
<stop offset="40%" stop-color="#87ceeb" />
<stop offset="80%" stop-color="#ffdfa0" />
<stop offset="100%" stop-color="#ffb347" />
</linearGradient>
<radialGradient id="sunGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="20%" stop-color="#ffeeaa" />
<stop offset="100%" stop-color="#ffb347" stop-opacity="0" />
</radialGradient>
<linearGradient id="seaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#005c8a" />
<stop offset="40%" stop-color="#0083b0" />
<stop offset="100%" stop-color="#00b4db" />
</linearGradient>
<linearGradient id="sandGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e6d3a8" />
<stop offset="100%" stop-color="#c2a77a" />
</linearGradient>
<!-- 自行车材质渐变 -->
<linearGradient id="frameGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#00e5ff" />
<stop offset="50%" stop-color="#00b4db" />
<stop offset="100%" stop-color="#0083b0" />
</linearGradient>
<linearGradient id="chromeGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="50%" stop-color="#cccccc" />
<stop offset="100%" stop-color="#999999" />
</linearGradient>
<linearGradient id="tireGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#444444" />
<stop offset="100%" stop-color="#111111" />
</linearGradient>
<!-- 鹈鹕材质渐变 -->
<radialGradient id="bodyGrad" cx="50%" cy="50%" r="50%" fx="70%" fy="30%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="70%" stop-color="#e6ecef" />
<stop offset="100%" stop-color="#a0b4c0" />
</radialGradient>
<linearGradient id="beakTopGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#ffe066" />
<stop offset="60%" stop-color="#ffb300" />
<stop offset="100%" stop-color="#f57c00" />
</linearGradient>
<linearGradient id="beakPouchGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffcc80" />
<stop offset="40%" stop-color="#ff8a65" />
<stop offset="80%" stop-color="#e64a19" />
<stop offset="100%" stop-color="#bf360c" />
</linearGradient>
<!-- 滤镜 -->
<filter id="blur-shadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" />
</filter>
<filter id="blur-cloud" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="6" />
</filter>
<!-- 复用组件:车轮 -->
<g id="wheel">
<!-- 外胎 -->
<circle cx="0" cy="0" r="90" fill="none" stroke="url(#tireGrad)" stroke-width="14" />
<!-- 白边复古轮胎圈 -->
<circle cx="0" cy="0" r="82" fill="none" stroke="#f4f4f4" stroke-width="5" />
<!-- 金属轮毂 -->
<circle cx="0" cy="0" r="77" fill="none" stroke="url(#chromeGrad)" stroke-width="6" />
<!-- 交叉辐条 -->
<g stroke="#cfcfcf" stroke-width="1.5">
<line x1="-75" y1="0" x2="75" y2="0" />
<line x1="0" y1="-75" x2="0" y2="75" />
<line x1="-53" y1="-53" x2="53" y2="53" />
<line x1="-53" y1="53" x2="53" y2="-53" />
<line x1="-69" y1="-29" x2="69" y2="29" />
<line x1="-69" y1="29" x2="69" y2="-29" />
<line x1="-29" y1="-69" x2="29" y2="69" />
<line x1="-29" y1="69" x2="29" y2="-69" />
</g>
<!-- 轴心 -->
<circle cx="0" cy="0" r="8" fill="url(#chromeGrad)" stroke="#666" stroke-width="2"/>
</g>
</defs>
<!-- 背景:天空与太阳 -->
<rect x="0" y="0" width="1000" height="400" fill="url(#skyGrad)" />
<!-- 太阳光晕 -->
<circle cx="800" cy="220" r="150" fill="url(#sunGrad)" />
<circle cx="800" cy="220" r="40" fill="#ffffff" filter="url(#blur-cloud)" />
<!-- 远处的飞鸟 -->
<g fill="#ffffff" opacity="0.6">
<path d="M 150,150 Q 160,140 170,150 Q 180,140 190,150 Q 180,155 170,145 Q 160,155 150,150 Z" />
<path d="M 220,120 Q 225,115 230,120 Q 235,115 240,120 Q 235,123 230,118 Q 225,123 220,120 Z" />
</g>
<!-- 云彩 -->
<g fill="#ffffff" opacity="0.85" filter="url(#blur-cloud)">
<circle cx="200" cy="180" r="30" />
<circle cx="240" cy="160" r="45" />
<circle cx="290" cy="180" r="35" />
<rect x="190" y="170" width="110" height="40" rx="20" />
<circle cx="850" cy="120" r="25" />
<circle cx="890" cy="110" r="35" />
<circle cx="930" cy="130" r="20" />
</g>
<!-- 背景:海面 -->
<rect x="0" y="400" width="1000" height="120" fill="url(#seaGrad)" />
<!-- 水波纹与太阳倒影 -->
<g stroke="#ffffff" stroke-width="2" fill="none" opacity="0.4" stroke-linecap="round">
<path d="M 150,430 Q 180,430 210,430" />
<path d="M 350,460 Q 380,460 410,460" />
<path d="M 100,480 Q 140,480 180,480" />
<!-- 倒影区域 -->
<g stroke="#ffcc80" stroke-width="3" opacity="0.8">
<path d="M 750,415 L 850,415" />
<path d="M 770,435 L 830,435" />
<path d="M 740,455 L 860,455" />
<path d="M 780,475 L 820,475" />
<path d="M 760,495 L 840,495" />
</g>
<path d="M 680,440 Q 700,440 720,440" />
<path d="M 880,460 Q 900,460 920,460" />
</g>
<!-- 背景:沙滩 -->
<path d="M 0,520 Q 250,510 500,520 T 1000,520 L 1000,700 L 0,700 Z" fill="url(#sandGrad)" />
<path d="M 0,560 Q 300,530 600,580 T 1000,550 L 1000,700 L 0,700 Z" fill="#b99e71" opacity="0.3" />
<!-- 投影 -->
<path d="M 180,630 L 700,610 L 780,630 L 250,650 Z" fill="rgba(0,0,0,0.25)" filter="url(#blur-shadow)" />
<ellipse cx="280" cy="625" rx="50" ry="10" fill="rgba(0,0,0,0.3)" filter="url(#blur-shadow)" />
<ellipse cx="720" cy="625" rx="50" ry="10" fill="rgba(0,0,0,0.3)" filter="url(#blur-shadow)" />
<!-- 左侧(背景层)踏板与鹈鹕左腿 -->
<g id="left-leg-and-pedal">
<!-- 左侧曲柄 -->
<line x1="500" y1="540" x2="475" y2="515" stroke="#888888" stroke-width="7" stroke-linecap="round" />
<!-- 左腿大腿 -->
<path d="M 370,330 C 380,360 400,390 420,390 C 410,360 390,340 370,330 Z" fill="#a0b4c0" />
<!-- 左腿小腿 -->
<line x1="415" y1="385" x2="475" y2="515" stroke="#cc7700" stroke-width="8" stroke-linecap="round" />
<!-- 左侧脚蹼 -->
<path d="M 475,515 L 495,515 L 505,530 L 485,525 Z" fill="#cc7700" />
<!-- 左踏板 -->
<rect x="463" y="511" width="24" height="8" rx="2" fill="#555555" />
</g>
<!-- 挡泥板(后层) -->
<path d="M 190,550 A 100 100 0 0 1 350,450" fill="none" stroke="url(#frameGrad)" stroke-width="12" stroke-linecap="round" />
<!-- 车轮 -->
<use href="#wheel" x="280" y="520" />
<use href="#wheel" x="720" y="520" />
<!-- 自行车车架 -->
<g id="bicycle-frame">
<!-- 后下叉 (Chainstay) -->
<line x1="280" y1="520" x2="500" y2="540" stroke="url(#frameGrad)" stroke-width="12" stroke-linecap="round" />
<!-- 后上叉 (Seatstay) -->
<line x1="280" y1="520" x2="420" y2="360" stroke="url(#frameGrad)" stroke-width="10" stroke-linecap="round" />
<!-- 下管 (Down tube) -->
<line x1="500" y1="540" x2="680" y2="400" stroke="url(#frameGrad)" stroke-width="15" stroke-linecap="round" />
<!-- 上管 (Top tube) -->
<line x1="420" y1="360" x2="660" y2="340" stroke="url(#frameGrad)" stroke-width="14" stroke-linecap="round" />
<!-- 立管 (Seat tube) -->
<line x1="500" y1="540" x2="420" y2="360" stroke="url(#frameGrad)" stroke-width="14" stroke-linecap="round" />
<!-- 头管 (Head tube) -->
<line x1="660" y1="340" x2="680" y2="400" stroke="url(#frameGrad)" stroke-width="18" stroke-linecap="round" />
<!-- 前叉 (Fork) -->
<line x1="680" y1="400" x2="720" y2="520" stroke="url(#chromeGrad)" stroke-width="12" stroke-linecap="round" />
<!-- 挡泥板(前层) -->
<path d="M 630,550 A 100 100 0 0 1 790,450" fill="none" stroke="url(#frameGrad)" stroke-width="12" stroke-linecap="round" />
</g>
<!-- 座管与座椅 -->
<line x1="420" y1="360" x2="410" y2="320" stroke="url(#chromeGrad)" stroke-width="8" />
<path d="M 380,320 C 370,310 440,310 450,330 L 380,330 Z" fill="#2a2a2a" />
<!-- 座椅弹簧细节 -->
<path d="M 395,330 L 390,345 M 435,330 L 430,345" stroke="#999" stroke-width="3" />
<!-- 传动系统:牙盘与链条 -->
<g id="drivetrain">
<!-- 链条 -->
<path d="M 280,510 L 500,518 M 280,530 L 500,562" stroke="#444444" stroke-width="4" stroke-dasharray="5 3" />
<circle cx="280" cy="520" r="10" fill="#999" stroke="#555" stroke-width="2" />
<!-- 牙盘 -->
<circle cx="500" cy="540" r="24" fill="none" stroke="#555" stroke-width="4" stroke-dasharray="4 2" />
<circle cx="500" cy="540" r="18" fill="url(#chromeGrad)" />
<line x1="482" y1="540" x2="518" y2="540" stroke="#777" stroke-width="4" />
<line x1="500" y1="522" x2="500" y2="558" stroke="#777" stroke-width="4" />
</g>
<!-- 右侧曲柄与踏板 (前景) -->
<g id="right-crank">
<line x1="500" y1="540" x2="525" y2="565" stroke="url(#chromeGrad)" stroke-width="7" stroke-linecap="round" />
<rect x="513" y="561" width="24" height="8" rx="2" fill="#222222" />
</g>
<!-- 鹈鹕主体 (尾巴、身体、脖子、头部) -->
<g id="pelican">
<!-- 尾羽 -->
<path d="M 260,330 L 150,360 L 140,340 L 240,310 Z" fill="#333333" />
<path d="M 250,350 L 140,380 L 130,360 L 220,330 Z" fill="#1a1a1a" />
<!-- 身体 -->
<path d="M 380,360
C 280,380 200,340 180,280
C 160,220 280,200 350,230
C 410,250 460,290 450,340
C 445,355 420,360 380,360 Z"
fill="url(#bodyGrad)" />
<!-- 脖子 -->
<path d="M 350,240
C 340,160 430,150 460,110
C 470,90 500,80 520,90
C 530,110 500,130 480,130
C 440,160 410,210 400,260 Z"
fill="url(#bodyGrad)" />
<!-- 喉囊 (下颌) -->
<!-- 增加半透明度和阴影表现饱满感 -->
<path d="M 505,105 C 570,180 720,210 800,130 C 680,120 580,115 505,105 Z" fill="url(#beakPouchGrad)" />
<path d="M 505,105 C 550,150 630,170 730,125" fill="none" stroke="#d84315" stroke-width="2" opacity="0.4" />
<!-- 上喙 (上颌) -->
<path d="M 515,85 Q 680,80 800,120 Q 815,125 808,135 Q 803,135 800,130 Q 680,110 505,105 Z" fill="url(#beakTopGrad)" />
<!-- 眼睛与眼斑 -->
<path d="M 495,90 C 495,80 515,80 515,90 C 515,100 495,100 495,90 Z" fill="#ffe082" stroke="#ffb300" stroke-width="1.5" />
<circle cx="505" cy="90" r="5" fill="#111111" />
<circle cx="507" cy="88" r="1.5" fill="#ffffff" />
</g>
<!-- 配饰:车篓与捕到的鱼 -->
<g id="basket-and-fish">
<!-- 车把立管 -->
<line x1="660" y1="340" x2="650" y2="290" stroke="url(#chromeGrad)" stroke-width="10" />
<!-- 车篓底色与轮廓 -->
<path d="M 670,290 L 730,290 L 715,360 L 665,360 Z" fill="#d4a373" stroke="#8b5a2b" stroke-width="3" stroke-linejoin="round" />
<!-- 鱼 -->
<g id="fish">
<path d="M 685,300 L 665,320 L 685,325 Z" fill="#90a4ae" />
<path d="M 685,300 C 680,270 690,250 710,240 C 725,230 745,240 745,240 C 745,240 735,260 715,270 C 700,280 685,300 685,300 Z" fill="url(#chromeGrad)" stroke="#78909c" stroke-width="1" />
<circle cx="730" cy="245" r="2.5" fill="#111" />
<circle cx="731" cy="244" r="0.8" fill="#fff" />
<path d="M 718,245 Q 713,255 718,265" fill="none" stroke="#546e7a" stroke-width="1.5" />
</g>
<!-- 车篓藤条纹理 -->
<line x1="675" y1="305" x2="725" y2="305" stroke="#b5835a" stroke-width="4" />
<line x1="673" y1="322" x2="721" y2="322" stroke="#b5835a" stroke-width="4" />
<line x1="670" y1="340" x2="717" y2="340" stroke="#b5835a" stroke-width="4" />
<line x1="685" y1="290" x2="680" y2="360" stroke="#b5835a" stroke-width="4" />
<line x1="700" y1="290" x2="695" y2="360" stroke="#b5835a" stroke-width="4" />
<line x1="715" y1="290" x2="710" y2="360" stroke="#b5835a" stroke-width="4" />
</g>
<!-- 车把与握把 -->
<path d="M 600,270 C 630,260 660,260 680,290" fill="none" stroke="url(#chromeGrad)" stroke-width="10" stroke-linecap="round" />
<line x1="585" y1="275" x2="615" y2="265" stroke="#8b4513" stroke-width="16" stroke-linecap="round" />
<!-- 鹈鹕生动的配饰:红底白点波浪头巾 -->
<g id="bandana">
<path d="M 400,260 C 410,275 435,275 440,250 L 445,265 C 420,285 395,280 400,260 Z" fill="#d32f2f" />
<!-- 飘扬的头巾尾巴 -->
<path d="M 408,265 Q 360,280 320,265 Q 360,295 415,275 Z" fill="#b71c1c" />
<path d="M 418,268 Q 370,305 340,315 Q 380,325 425,278 Z" fill="#d32f2f" />
<!-- 波点 -->
<circle cx="410" cy="263" r="2" fill="#ffffff" />
<circle cx="425" cy="265" r="2" fill="#ffffff" />
<circle cx="435" cy="255" r="2" fill="#ffffff" />
<circle cx="390" cy="275" r="1.5" fill="#ffffff" />
<circle cx="360" cy="273" r="1.5" fill="#ffffff" />
<circle cx="380" cy="295" r="1.5" fill="#ffffff" />
<circle cx="350" cy="305" r="1.5" fill="#ffffff" />
</g>
<!-- 鹈鹕右腿 (前景层) -->
<g id="right-leg">
<path d="M 390,340 C 410,380 440,430 470,440 C 460,400 420,360 390,340 Z" fill="url(#bodyGrad)" />
<line x1="465" y1="435" x2="525" y2="565" stroke="#ff9900" stroke-width="9" stroke-linecap="round" />
<path d="M 525,565 L 560,575 L 575,595 L 540,585 Z" fill="#ffaa00" />
</g>
<!-- 鹈鹕右翼 (握住车把的翅膀,最前层) -->
<g id="pelican-wing">
<!-- 基础翅膀形态 -->
<path d="M 350,240 C 440,250 560,260 600,270 C 590,290 500,310 400,320 C 300,330 200,320 220,290 Z" fill="url(#bodyGrad)" />
<!-- 初级飞羽 (深色) -->
<path d="M 220,290 C 180,290 120,300 90,320 C 120,330 180,320 220,310 Z" fill="#222222" />
<path d="M 230,300 C 180,310 110,330 80,350 C 120,355 180,340 230,320 Z" fill="#3a3a3a" />
<path d="M 250,310 C 200,320 130,350 100,380 C 140,380 200,360 250,330 Z" fill="#4d4d4d" />
<!-- 次级飞羽 -->
<path d="M 300,310 C 260,330 200,360 180,380 C 220,370 270,340 320,320 Z" fill="#888888" />
<path d="M 350,315 C 310,340 250,370 230,390 C 270,380 320,350 370,325 Z" fill="#aaaaaa" />
<!-- 肩羽覆羽 (亮白) -->
<path d="M 400,260 C 450,270 520,270 580,280 C 560,290 480,300 400,290 C 350,280 320,270 350,260 Z" fill="#ffffff" />
<!-- 翅膀轮廓与羽毛纹理 -->
<path d="M 350,240 C 440,250 560,260 600,270" fill="none" stroke="#e0e0e0" stroke-width="2" />
<path d="M 420,285 Q 470,285 520,290" fill="none" stroke="#cccccc" stroke-width="2" />
<path d="M 380,300 Q 430,300 480,305" fill="none" stroke="#bbbbbb" stroke-width="2" />
</g>
<!-- 动感特效:后轮扬起的沙粒与速度线 -->
<g id="action-effects">
<!-- 速度线 -->
<line x1="120" y1="520" x2="220" y2="520" stroke="#ffffff" stroke-width="3" opacity="0.5" stroke-dasharray="15 8" />
<line x1="80" y1="560" x2="200" y2="560" stroke="#b99e71" stroke-width="4" opacity="0.6" stroke-dasharray="25 15" />
<!-- 扬起的沙子 -->
<g fill="#c2a77a">
<circle cx="180" cy="580" r="4" />
<circle cx="160" cy="595" r="5" />
<circle cx="140" cy="575" r="2.5" />
<circle cx="195" cy="605" r="3" />
<circle cx="150" cy="615" r="4.5" />
<circle cx="130" cy="590" r="2" />
<circle cx="170" cy="610" r="3.5" />
</g>
</g>
<!-- 整体氛围光 (屏幕混合叠加,增加温馨感) -->
<polygon points="800,220 -200,800 200,800 800,220" fill="url(#sunGrad)" opacity="0.25" style="mix-blend-mode: screen;" />
</svg>

After

Width:  |  Height:  |  Size: 17 KiB