Register Composer 2.5 Fast × Cursor (Max) (#6)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
驱不散的雾andCursor authored and GitHub committed 2026-06-11 20:24:44 +01:00
1 parent 44ebf586aa
commit 6fbf660b49
3 files changed
+858

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
{
"label": "Composer 2.5 Fast",
"vendor": "Cursor",
"harness": "Cursor",
"effort": "Max",
"order": 71,
"runs": {
"mythos-craft": {
"note": {
"zh": "在 Cursor 中以 Composer 2.5 Fast(Max 思考配额)一次生成,本页为未经修改的原始产出。",
"en": "Generated in one shot by Composer 2.5 Fast (Max effort) inside Cursor; this page is the unmodified original output."
},
"contributor": "Mist-wu"
},
"pelican-cycling": {
"note": {
"zh": "在 Cursor 中以 Composer 2.5 Fast(Max 思考配额)一次生成。",
"en": "Generated in one shot by Composer 2.5 Fast (Max effort) inside Cursor."
},
"contributor": "Mist-wu"
}
}
}
+632
View File
@@ -0,0 +1,632 @@
<!DOCTYPE html>
<html lang="zh-CN">
<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: system-ui, sans-serif; }
canvas { display: block; }
#overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.82);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 100; cursor: pointer; text-align: center; padding: 24px;
}
#overlay h1 { color: #e8d5a3; font-size: 40px; letter-spacing: 4px; margin-bottom: 8px; }
#overlay h2 { color: #9ec5e8; font-size: 17px; font-weight: 400; margin-bottom: 28px; }
#overlay p { color: rgba(255,255,255,0.65); line-height: 1.9; font-size: 14px; max-width: 520px; }
#overlay .btn {
margin-top: 28px; padding: 12px 36px; font-size: 17px; letter-spacing: 2px;
background: linear-gradient(135deg, #4a6741, #2d4a2d); color: #e8d5a3;
border: 2px solid #6b8f5e; border-radius: 4px;
}
#crosshair {
position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
width: 20px; height: 20px; pointer-events: none; z-index: 10; display: none;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(255,255,255,0.85);
}
#crosshair::before { width: 2px; height: 20px; left: 9px; }
#crosshair::after { width: 20px; height: 2px; top: 9px; }
#hud {
position: fixed; bottom: 0; left: 0; right: 0; padding: 14px;
display: none; flex-direction: column; align-items: center; gap: 8px;
pointer-events: none; z-index: 10;
}
#hotbar {
display: flex; gap: 4px; background: rgba(0,0,0,0.55);
padding: 6px; border-radius: 4px; border: 2px solid rgba(255,255,255,0.28);
}
.slot {
width: 46px; height: 46px; border: 2px solid rgba(100,100,100,0.55);
border-radius: 2px; position: relative; background: rgba(0,0,0,0.35);
}
.slot.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.45); }
.slot canvas { width: 100%; height: 100%; image-rendering: pixelated; }
.slot .key { position: absolute; top: 2px; left: 4px; font-size: 10px; color: rgba(255,255,255,0.65); }
#info { color: rgba(255,255,255,0.7); font-size: 12px; text-shadow: 1px 1px 2px #000; }
#blockName {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, 36px);
color: rgba(255,255,255,0.85); font-size: 13px; display: none;
text-shadow: 1px 1px 3px #000; pointer-events: none; z-index: 10;
}
#minimap {
position: fixed; top: 14px; right: 14px; width: 110px; height: 110px;
border: 2px solid rgba(255,255,255,0.28); border-radius: 4px;
background: rgba(0,0,0,0.45); display: none; z-index: 10; pointer-events: none;
}
</style>
</head>
<body>
<div id="overlay">
<h1>神话之境</h1>
<h2>Mythos Craft — 体素世界</h2>
<p>
WASD 移动 · 空格跳跃 · Shift 潜行<br>
鼠标环视 · 左键破坏 · 右键放置 · 1-6 切换方块<br>
柏林噪声生成的无限地形:草原、沙漠、雪原、神话群系<br>
探索精灵螺旋塔、龙神殿、天空浮岛遗迹
</p>
<div class="btn">点击开始</div>
</div>
<div id="crosshair"></div>
<div id="blockName"></div>
<canvas id="minimap"></canvas>
<div id="hud">
<div id="hotbar"></div>
<div id="info">坐标 0, 0, 0</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(() => {
const BLOCK = { AIR: 0, GRASS: 1, DIRT: 2, STONE: 3, SAND: 4, SNOW: 5, WOOD: 6, LEAVES: 7, MYTH_STONE: 8, GOLD: 9 };
const BLOCK_NAMES = ['', '草地', '泥土', '石头', '沙子', '雪', '原木', '树叶', '神话石', '金块'];
const HOTBAR = [BLOCK.GRASS, BLOCK.DIRT, BLOCK.STONE, BLOCK.SAND, BLOCK.WOOD, BLOCK.MYTH_STONE];
const CHUNK = 16, WORLD_H = 64, RENDER = 4;
const GRAVITY = 24, JUMP = 9, WALK = 5.5, SNEAK = 2.5;
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 16;
const ctx = canvas.getContext('2d');
const atlasCanvas = document.createElement('canvas');
atlasCanvas.width = 160; atlasCanvas.height = 16;
const atlasCtx = atlasCanvas.getContext('2d');
function noise2D(x, z) {
const s = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
return s - Math.floor(s);
}
function smoothNoise(x, z) {
const ix = Math.floor(x), iz = Math.floor(z);
const fx = x - ix, fz = z - iz;
const u = fx * fx * (3 - 2 * fx), v = fz * fz * (3 - 2 * fz);
const a = noise2D(ix, iz), b = noise2D(ix + 1, iz);
const c = noise2D(ix, iz + 1), d = noise2D(ix + 1, iz + 1);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function fbm(x, z, oct = 5) {
let v = 0, a = 0.5, f = 1;
for (let i = 0; i < oct; i++) { v += smoothNoise(x * f, z * f) * a; a *= 0.5; f *= 2; }
return v;
}
function drawFace(color, detail) {
ctx.fillStyle = color; ctx.fillRect(0, 0, 16, 16);
if (detail === 'grass') {
ctx.fillStyle = '#5a8f3c'; ctx.fillRect(0, 0, 16, 4);
for (let i = 0; i < 20; i++) {
ctx.fillStyle = i % 2 ? '#6b9a45' : '#4a7530';
ctx.fillRect((i * 7) % 16, (i * 3) % 16, 1, 1);
}
} else if (detail === 'dirt') {
for (let i = 0; i < 30; i++) {
ctx.fillStyle = i % 2 ? '#6b5038' : '#5a422e';
ctx.fillRect((i * 5) % 16, (i * 7) % 16, 1, 1);
}
} else if (detail === 'stone') {
for (let i = 0; i < 24; i++) {
ctx.fillStyle = i % 3 ? '#7a7a7a' : '#686868';
ctx.fillRect((i * 4) % 16, (i * 6) % 16, 2, 1);
}
} else if (detail === 'sand') {
for (let i = 0; i < 18; i++) {
ctx.fillStyle = i % 2 ? '#d4b870' : '#c9a860';
ctx.fillRect((i * 6) % 16, (i * 4) % 16, 1, 1);
}
} else if (detail === 'snow') {
ctx.fillStyle = '#eef6ff'; ctx.fillRect(0, 0, 16, 16);
for (let i = 0; i < 8; i++) ctx.fillRect((i * 9) % 16, (i * 5) % 16, 1, 1);
} else if (detail === 'wood') {
ctx.fillStyle = '#6b4423';
for (let x = 0; x < 16; x += 2) ctx.fillRect(x, 0, 1, 16);
} else if (detail === 'leaves') {
for (let i = 0; i < 40; i++) {
ctx.fillStyle = i % 2 ? '#2d6b2d' : '#3a8040';
ctx.fillRect((i * 3) % 16, (i * 5) % 16, 2, 2);
}
} else if (detail === 'myth') {
ctx.fillStyle = '#4a3a7a';
ctx.fillRect(0, 0, 16, 16);
ctx.fillStyle = '#c8b4ff';
for (let i = 0; i < 12; i++) ctx.fillRect((i * 5) % 16, (i * 7) % 16, 1, 1);
} else if (detail === 'gold') {
ctx.fillStyle = '#d4af37';
ctx.fillRect(0, 0, 16, 16);
ctx.fillStyle = '#f0d060';
ctx.fillRect(2, 2, 12, 12);
}
}
const texData = {
[BLOCK.GRASS]: { top: ['#5a8f3c', 'grass'], side: ['#6b5038', 'dirt'], bottom: ['#6b5038', 'dirt'] },
[BLOCK.DIRT]: { top: ['#6b5038', 'dirt'], side: ['#6b5038', 'dirt'], bottom: ['#6b5038', 'dirt'] },
[BLOCK.STONE]: { top: ['#888', 'stone'], side: ['#888', 'stone'], bottom: ['#888', 'stone'] },
[BLOCK.SAND]: { top: ['#d4b870', 'sand'], side: ['#d4b870', 'sand'], bottom: ['#d4b870', 'sand'] },
[BLOCK.SNOW]: { top: ['#eef6ff', 'snow'], side: ['#eef6ff', 'snow'], bottom: ['#eef6ff', 'snow'] },
[BLOCK.WOOD]: { top: ['#8b6914', 'wood'], side: ['#6b4423', 'wood'], bottom: ['#8b6914', 'wood'] },
[BLOCK.LEAVES]: { top: ['#3a8040', 'leaves'], side: ['#3a8040', 'leaves'], bottom: ['#3a8040', 'leaves'] },
[BLOCK.MYTH_STONE]: { top: ['#4a3a7a', 'myth'], side: ['#4a3a7a', 'myth'], bottom: ['#4a3a7a', 'myth'] },
[BLOCK.GOLD]: { top: ['#d4af37', 'gold'], side: ['#d4af37', 'gold'], bottom: ['#d4af37', 'gold'] },
};
const faceUV = {};
let atlasIndex = 0;
for (const id of Object.keys(texData).map(Number)) {
faceUV[id] = {};
for (const face of ['top', 'side', 'bottom']) {
const [c, d] = texData[id][face];
drawFace(c, d);
atlasCtx.drawImage(canvas, atlasIndex * 16, 0);
faceUV[id][face] = atlasIndex / 10;
atlasIndex++;
}
}
const atlasTex = new THREE.CanvasTexture(atlasCanvas);
atlasTex.magFilter = THREE.NearestFilter;
atlasTex.minFilter = THREE.NearestFilter;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 40, 120);
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.05, 200);
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.55));
const sun = new THREE.DirectionalLight(0xfff4d6, 0.85);
sun.position.set(50, 80, 30);
scene.add(sun);
const chunks = new Map();
const chunkMeshes = new Map();
let worldSeed = 42;
let generating = false;
function chunkKey(cx, cz) { return cx + ',' + cz; }
function biome(x, z) {
const t = fbm(x * 0.008 + worldSeed, z * 0.008, 4);
const m = fbm(x * 0.015 + 100, z * 0.015, 3);
if (t < 0.28) return 'desert';
if (t > 0.72 && m > 0.55) return 'snow';
if (m > 0.62) return 'myth';
return 'plains';
}
function heightAt(x, z) {
const b = biome(x, z);
let h = fbm(x * 0.04 + worldSeed, z * 0.04, 5) * 18 + 22;
if (b === 'desert') h += fbm(x * 0.02, z * 0.02) * 6 - 2;
if (b === 'snow') h += 8 + fbm(x * 0.03, z * 0.03) * 10;
if (b === 'myth') h += 12 + Math.sin(x * 0.08) * 3;
return Math.floor(h);
}
function getBlock(x, y, z) {
if (y < 0 || y >= WORLD_H) return BLOCK.AIR;
const cx = Math.floor(x / CHUNK), cz = Math.floor(z / CHUNK);
const c = chunks.get(chunkKey(cx, cz));
if (!c) return BLOCK.AIR;
const lx = ((x % CHUNK) + CHUNK) % CHUNK;
const lz = ((z % CHUNK) + CHUNK) % CHUNK;
return c[lx + lz * CHUNK + y * CHUNK * CHUNK] || BLOCK.AIR;
}
function setBlock(x, y, z, v, silent) {
const cx = Math.floor(x / CHUNK), cz = Math.floor(z / CHUNK);
const key = chunkKey(cx, cz);
if (!chunks.has(key)) return;
const c = chunks.get(key);
const lx = ((x % CHUNK) + CHUNK) % CHUNK;
const lz = ((z % CHUNK) + CHUNK) % CHUNK;
c[lx + lz * CHUNK + y * CHUNK * CHUNK] = v;
if (silent || generating) return;
rebuildChunk(cx, cz);
if (lx === 0) rebuildChunk(cx - 1, cz);
if (lx === CHUNK - 1) rebuildChunk(cx + 1, cz);
if (lz === 0) rebuildChunk(cx, cz - 1);
if (lz === CHUNK - 1) rebuildChunk(cx, cz + 1);
}
function fillBox(x0, y0, z0, x1, y1, z1, id) {
for (let y = y0; y <= y1; y++)
for (let x = x0; x <= x1; x++)
for (let z = z0; z <= z1; z++) setBlock(x, y, z, id);
}
function buildTree(x, y, z) {
for (let dy = 0; dy < 5; dy++) setBlock(x, y + dy, z, BLOCK.WOOD);
for (let dy = 3; dy <= 7; dy++)
for (let dx = -2; dx <= 2; dx++)
for (let dz = -2; dz <= 2; dz++)
if (Math.abs(dx) + Math.abs(dz) < 4) setBlock(x + dx, y + dy, z + dz, BLOCK.LEAVES);
}
function buildSpiralTower(bx, by, bz) {
const r = 5, h = 22;
for (let y = 0; y < h; y++) {
const ang = y * 0.55, rad = r - (y % 6 === 0 ? 0 : 0);
for (let i = 0; i < 8; i++) {
const a = ang + (i / 8) * Math.PI * 2;
const x = bx + Math.round(Math.cos(a) * rad);
const z = bz + Math.round(Math.sin(a) * rad);
setBlock(x, by + y, z, BLOCK.MYTH_STONE);
if (y % 5 === 0) setBlock(x, by + y, z, BLOCK.GOLD);
}
}
fillBox(bx - 1, by, bz - 1, bx + 1, by, bz + 1, BLOCK.GOLD);
}
function buildDragonTemple(bx, by, bz) {
fillBox(bx - 6, by, bz - 8, bx + 6, by, bz + 8, BLOCK.STONE);
fillBox(bx - 5, by + 1, bz - 7, bx + 5, by + 8, bz + 7, BLOCK.AIR);
for (let x = bx - 6; x <= bx + 6; x++)
for (let z = bz - 8; z <= bz + 8; z++)
setBlock(x, by + 9, z, BLOCK.MYTH_STONE);
for (let y = by + 1; y <= by + 6; y++) {
setBlock(bx - 6, y, bz, BLOCK.MYTH_STONE);
setBlock(bx + 6, y, bz, BLOCK.MYTH_STONE);
}
fillBox(bx - 2, by + 1, bz - 8, bx + 2, by + 4, bz - 8, BLOCK.AIR);
for (let dz = -2; dz <= 2; dz++) setBlock(bx, by + 12, bz + dz, BLOCK.GOLD);
for (let dx = -4; dx <= 4; dx++) setBlock(bx + dx, by + 10, bz - 9, BLOCK.GOLD);
}
function buildSkyRuins(bx, by, bz) {
for (let i = 0; i < 4; i++) {
const ox = (i % 2) * 8 - 4, oz = Math.floor(i / 2) * 8 - 4;
for (let y = 0; y < 3 + (i % 2) * 2; y++)
fillBox(bx + ox, by + y, bz + oz, bx + ox + 2, by + y, bz + oz + 2, BLOCK.MYTH_STONE);
}
fillBox(bx - 5, by + 6, bz - 1, bx + 5, by + 6, bz + 1, BLOCK.STONE);
setBlock(bx, by + 7, bz, BLOCK.GOLD);
setBlock(bx - 3, by + 7, bz, BLOCK.GOLD);
setBlock(bx + 3, by + 7, bz, BLOCK.GOLD);
}
const landmarks = [
{ name: '精灵螺旋塔', x: 48, z: 32 },
{ name: '龙神殿', x: -64, z: 80 },
{ name: '天空遗迹', x: 96, z: -56 }
];
function generateChunk(cx, cz) {
generating = true;
const data = new Uint8Array(CHUNK * CHUNK * WORLD_H);
chunks.set(chunkKey(cx, cz), data);
const wx = cx * CHUNK, wz = cz * CHUNK;
for (let lx = 0; lx < CHUNK; lx++) {
for (let lz = 0; lz < CHUNK; lz++) {
const wxp = wx + lx, wzp = wz + lz;
const h = heightAt(wxp, wzp);
const b = biome(wxp, wzp);
for (let y = 0; y < WORLD_H; y++) {
let id = BLOCK.AIR;
if (y === 0) id = BLOCK.STONE;
else if (y < h - 4) id = BLOCK.STONE;
else if (y < h - 1) id = BLOCK.DIRT;
else if (y === h - 1) {
if (b === 'desert') id = BLOCK.SAND;
else if (b === 'snow') id = BLOCK.SNOW;
else if (b === 'myth') id = BLOCK.MYTH_STONE;
else id = BLOCK.GRASS;
}
data[lx + lz * CHUNK + y * CHUNK * CHUNK] = id;
}
if (b === 'plains' && noise2D(wxp * 0.17, wzp * 0.17) > 0.93) buildTree(wxp, h, wzp);
}
}
for (const lm of landmarks) {
if (wx <= lm.x && wx + CHUNK > lm.x && wz <= lm.z && wz + CHUNK > lm.z) {
const ground = heightAt(lm.x, lm.z);
if (lm.name === '精灵螺旋塔') buildSpiralTower(lm.x, ground, lm.z);
else if (lm.name === '龙神殿') buildDragonTemple(lm.x, ground, lm.z);
else buildSkyRuins(lm.x, ground + 4, lm.z);
}
}
generating = false;
}
function rebuildChunk(cx, cz) {
const key = chunkKey(cx, cz);
if (!chunks.has(key)) return;
if (chunkMeshes.has(key)) {
scene.remove(chunkMeshes.get(key));
chunkMeshes.get(key).geometry.dispose();
chunkMeshes.delete(key);
}
const data = chunks.get(key);
const positions = [], normals = [], uvs = [], indices = [];
const wx = cx * CHUNK, wz = cz * CHUNK;
let vi = 0;
const faces = [
[0,1,0, 'top'], [0,-1,0, 'bottom'], [1,0,0, 'side'], [-1,0,0, 'side'], [0,0,1, 'side'], [0,0,-1, 'side']
];
function neighbor(x, y, z, dx, dy, dz) {
return getBlock(x + dx, y + dy, z + dz) === BLOCK.AIR;
}
for (let lx = 0; lx < CHUNK; lx++) {
for (let lz = 0; lz < CHUNK; lz++) {
for (let y = 0; y < WORLD_H; y++) {
const id = data[lx + lz * CHUNK + y * CHUNK * CHUNK];
if (!id) continue;
const x = wx + lx, z = wz + lz;
for (const [dx, dy, dz, faceName] of faces) {
if (!neighbor(x, y, z, dx, dy, dz)) continue;
let uvFace = faceName;
if (faceName === 'side' && (dx !== 0 || dz !== 0)) uvFace = 'side';
if (dy === 1) uvFace = 'top';
if (dy === -1) uvFace = 'bottom';
const u0 = faceUV[id][uvFace], u1 = u0 + 0.1;
const corners = [];
if (dy === 1) corners.push([0,1,0],[1,1,0],[1,1,1],[0,1,1]);
else if (dy === -1) corners.push([0,0,1],[1,0,1],[1,0,0],[0,0,0]);
else if (dx === 1) corners.push([1,0,0],[1,1,0],[1,1,1],[1,0,1]);
else if (dx === -1) corners.push([0,0,1],[0,1,1],[0,1,0],[0,0,0]);
else if (dz === 1) corners.push([1,0,1],[1,1,1],[0,1,1],[0,0,1]);
else corners.push([0,0,0],[0,1,0],[1,1,0],[1,0,0]);
const base = vi;
const uvQuad = [[u0,1],[u1,1],[u1,0],[u0,0]];
corners.forEach((c, i) => {
positions.push(x + c[0], y + c[1], z + c[2]);
normals.push(dx, dy, dz);
uvs.push(uvQuad[i][0], uvQuad[i][1]);
vi++;
});
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
}
}
}
}
if (!positions.length) return;
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geo.setIndex(indices);
const mat = new THREE.MeshLambertMaterial({ map: atlasTex, vertexColors: false });
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);
chunkMeshes.set(key, mesh);
}
function ensureChunks(px, pz) {
const ccx = Math.floor(px / CHUNK), ccz = Math.floor(pz / CHUNK);
for (let dx = -RENDER; dx <= RENDER; dx++)
for (let dz = -RENDER; dz <= RENDER; dz++) {
const cx = ccx + dx, cz = ccz + dz, key = chunkKey(cx, cz);
if (!chunks.has(key)) { generateChunk(cx, cz); rebuildChunk(cx, cz); }
}
for (const [key, mesh] of chunkMeshes) {
const [cx, cz] = key.split(',').map(Number);
if (Math.abs(cx - ccx) > RENDER + 1 || Math.abs(cz - ccz) > RENDER + 1) {
scene.remove(mesh); mesh.geometry.dispose(); chunkMeshes.delete(key); chunks.delete(key);
}
}
}
const player = {
x: 8, y: 30, z: 8,
vx: 0, vy: 0, vz: 0,
yaw: 0, pitch: 0,
onGround: false,
height: 1.7,
radius: 0.35
};
function groundY(x, z) {
for (let y = WORLD_H - 1; y >= 0; y--)
if (getBlock(Math.floor(x), y, Math.floor(z)) !== BLOCK.AIR) return y + 1;
return 20;
}
player.y = groundY(player.x, player.z) + player.height;
const keys = {};
let pointerLocked = false, selected = 0, playing = false;
let lastTime = performance.now();
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code.startsWith('Digit') && +e.code.slice(5) >= 1 && +e.code.slice(5) <= 6) {
selected = +e.code.slice(5) - 1;
updateHotbar();
}
});
document.addEventListener('keyup', e => { keys[e.code] = false; });
const overlay = document.getElementById('overlay');
overlay.addEventListener('click', () => {
renderer.domElement.requestPointerLock();
});
document.addEventListener('pointerlockchange', () => {
pointerLocked = document.pointerLockElement === renderer.domElement;
if (pointerLocked) {
playing = true;
overlay.style.display = 'none';
document.getElementById('crosshair').style.display = 'block';
document.getElementById('hud').style.display = 'flex';
document.getElementById('blockName').style.display = 'block';
document.getElementById('minimap').style.display = 'block';
}
});
document.addEventListener('mousemove', e => {
if (!pointerLocked) return;
player.yaw -= e.movementX * 0.002;
player.pitch -= e.movementY * 0.002;
player.pitch = Math.max(-1.5, Math.min(1.5, player.pitch));
});
document.addEventListener('mousedown', e => {
if (!playing) return;
const hit = raycast();
if (!hit) return;
if (e.button === 0) setBlock(hit.x, hit.y, hit.z, BLOCK.AIR);
else if (e.button === 2) {
const nx = hit.x + hit.nx, ny = hit.y + hit.ny, nz = hit.z + hit.nz;
if (!collides(nx + 0.5, ny + 0.5, nz + 0.5)) setBlock(nx, ny, nz, HOTBAR[selected]);
}
});
document.addEventListener('contextmenu', e => e.preventDefault());
document.addEventListener('wheel', e => {
selected = (selected + (e.deltaY > 0 ? 1 : -1) + HOTBAR.length) % HOTBAR.length;
updateHotbar();
});
function collides(x, y, z) {
const r = player.radius, h = player.height;
for (let dy = 0; dy <= h; dy += h) {
for (let dx = -r; dx <= r; dx += r * 2)
for (let dz = -r; dz <= r; dz += r * 2)
if (getBlock(Math.floor(x + dx), Math.floor(y - h / 2 + dy), Math.floor(z + dz)) !== BLOCK.AIR) return true;
}
return false;
}
function raycast(max = 6) {
const dir = new THREE.Vector3(
-Math.sin(player.yaw) * Math.cos(player.pitch),
Math.sin(player.pitch),
-Math.cos(player.yaw) * Math.cos(player.pitch)
);
let x = player.x, y = player.y, z = player.z;
for (let i = 0; i < max * 10; i++) {
x += dir.x * 0.1; y += dir.y * 0.1; z += dir.z * 0.1;
const bx = Math.floor(x), by = Math.floor(y), bz = Math.floor(z);
if (getBlock(bx, by, bz) !== BLOCK.AIR) {
const px = x - dir.x * 0.1, py = y - dir.y * 0.1, pz = z - dir.z * 0.1;
return {
x: bx, y: by, z: bz,
nx: Math.floor(px) !== bx ? (bx > px ? -1 : 1) : 0,
ny: Math.floor(py) !== by ? (by > py ? -1 : 1) : 0,
nz: Math.floor(pz) !== bz ? (bz > pz ? -1 : 1) : 0
};
}
}
return null;
}
function moveAxis(val, axis) {
const next = { x: player.x, y: player.y, z: player.z };
next[axis] += val;
if (!collides(next.x, next.y, next.z)) player[axis] = next[axis];
else if (axis === 'y') player.vy = 0;
}
function updatePlayer(dt) {
const speed = keys['ShiftLeft'] || keys['ShiftRight'] ? SNEAK : WALK;
let mx = 0, mz = 0;
if (keys['KeyW']) { mx -= Math.sin(player.yaw); mz -= Math.cos(player.yaw); }
if (keys['KeyS']) { mx += Math.sin(player.yaw); mz += Math.cos(player.yaw); }
if (keys['KeyA']) { mx -= Math.cos(player.yaw); mz += Math.sin(player.yaw); }
if (keys['KeyD']) { mx += Math.cos(player.yaw); mz -= Math.sin(player.yaw); }
const len = Math.hypot(mx, mz) || 1;
mx = (mx / len) * speed * dt;
mz = (mz / len) * speed * dt;
moveAxis(mx, 'x');
moveAxis(mz, 'z');
player.vy -= GRAVITY * dt;
if (keys['Space'] && player.onGround) { player.vy = JUMP; player.onGround = false; }
moveAxis(player.vy * dt, 'y');
player.onGround = collides(player.x, player.y - 0.05, player.z);
ensureChunks(player.x, player.z);
camera.position.set(player.x, player.y, player.z);
camera.rotation.order = 'YXZ';
camera.rotation.y = player.yaw;
camera.rotation.x = player.pitch;
}
const hotbarEl = document.getElementById('hotbar');
function updateHotbar() {
hotbarEl.innerHTML = '';
HOTBAR.forEach((id, i) => {
const slot = document.createElement('div');
slot.className = 'slot' + (i === selected ? ' active' : '');
const ic = document.createElement('canvas');
ic.width = ic.height = 16;
const ict = ic.getContext('2d');
const [c, d] = texData[id].top;
drawFace(c, d);
ict.drawImage(canvas, 0, 0);
slot.appendChild(ic);
const k = document.createElement('span');
k.className = 'key'; k.textContent = i + 1;
slot.appendChild(k);
hotbarEl.appendChild(slot);
});
document.getElementById('blockName').textContent = BLOCK_NAMES[HOTBAR[selected]];
}
updateHotbar();
const minimap = document.getElementById('minimap');
const mm = minimap.getContext('2d');
function drawMinimap() {
const s = 110, r = 28;
mm.fillStyle = 'rgba(0,0,0,0.55)'; mm.fillRect(0, 0, s, s);
const px = Math.floor(player.x), pz = Math.floor(player.z);
for (let dx = -r; dx <= r; dx++) {
for (let dz = -r; dz <= r; dz++) {
const wx = px + dx, wz = pz + dz;
const b = biome(wx, wz);
mm.fillStyle = b === 'desert' ? '#d4b870' : b === 'snow' ? '#eef6ff' : b === 'myth' ? '#6a5acd' : '#5a8f3c';
mm.fillRect(r + dx, r + dz, 1, 1);
}
}
landmarks.forEach(lm => {
const dx = lm.x - px + r, dz = lm.z - pz + r;
if (dx >= 0 && dx < s && dz >= 0 && dz < s) {
mm.fillStyle = '#ffd700';
mm.fillRect(dx - 1, dz - 1, 3, 3);
}
});
mm.fillStyle = '#f00';
mm.fillRect(r - 2, r - 2, 4, 4);
}
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
function loop(now) {
requestAnimationFrame(loop);
const dt = Math.min(0.05, (now - lastTime) / 1000);
lastTime = now;
if (playing) {
updatePlayer(dt);
document.getElementById('info').textContent =
`坐标 ${player.x.toFixed(1)}, ${player.y.toFixed(1)}, ${player.z.toFixed(1)} | ${biome(Math.floor(player.x), Math.floor(player.z))}`;
drawMinimap();
}
renderer.render(scene, camera);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
@@ -0,0 +1,203 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" width="1200" height="800">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#5b9bd5"/>
<stop offset="45%" stop-color="#9ec5e8"/>
<stop offset="100%" stop-color="#ffd9a8"/>
</linearGradient>
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1565a8"/>
<stop offset="100%" stop-color="#2ec4c9"/>
</linearGradient>
<linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f0d9a8"/>
<stop offset="100%" stop-color="#c9a66b"/>
</linearGradient>
<linearGradient id="featherWhite" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="100%" stop-color="#ddd5c8"/>
</linearGradient>
<linearGradient id="featherDark" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3d4f5f"/>
<stop offset="100%" stop-color="#1f2a36"/>
</linearGradient>
<linearGradient id="beakTop" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#ffd54f"/>
<stop offset="100%" stop-color="#e6a020"/>
</linearGradient>
<linearGradient id="pouch" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f4a574"/>
<stop offset="100%" stop-color="#c96b3a"/>
</linearGradient>
<linearGradient id="chrome" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#b8c4d0"/>
<stop offset="50%" stop-color="#6b7d8f"/>
<stop offset="100%" stop-color="#3d4a56"/>
</linearGradient>
<radialGradient id="sunHalo" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff8dc"/>
<stop offset="100%" stop-color="#ffb347" stop-opacity="0"/>
</radialGradient>
<filter id="shadow" x="-30%" y="-30%" width="160%" height="160%">
<feDropShadow dx="4" dy="6" stdDeviation="5" flood-color="#000" flood-opacity="0.28"/>
</filter>
<pattern id="waves" width="100" height="16" patternUnits="userSpaceOnUse">
<path d="M0 8 Q25 0 50 8 T100 8" fill="none" stroke="#fff" stroke-width="2" opacity="0.22"/>
</pattern>
</defs>
<rect width="1200" height="800" fill="url(#sky)"/>
<g opacity="0.75">
<ellipse cx="220" cy="130" rx="95" ry="34" fill="#fff"/>
<ellipse cx="280" cy="118" rx="62" ry="26" fill="#fff"/>
<ellipse cx="860" cy="95" rx="110" ry="38" fill="#fff"/>
<ellipse cx="930" cy="82" rx="70" ry="28" fill="#fff"/>
</g>
<circle cx="1020" cy="120" r="110" fill="url(#sunHalo)"/>
<circle cx="1020" cy="120" r="48" fill="#ffe082"/>
<path d="M0 365 Q280 330 520 355 Q760 340 1200 360 L1200 420 L0 420 Z" fill="#5d8aa8" opacity="0.35"/>
<rect x="0" y="390" width="1200" height="170" fill="url(#sea)"/>
<rect x="0" y="390" width="1200" height="170" fill="url(#waves)"/>
<path d="M0 530 C180 510 360 545 600 525 C840 505 1020 540 1200 515 L1200 800 L0 800 Z" fill="url(#sand)"/>
<path d="M0 530 C300 518 600 538 1200 520 L1200 575 L0 575 Z" fill="#b89868" opacity="0.35"/>
<g opacity="0.55">
<circle cx="180" cy="660" r="4" fill="#8a7358"/>
<circle cx="420" cy="710" r="3" fill="#7a6548"/>
<circle cx="980" cy="690" r="4" fill="#9a8260"/>
<ellipse cx="760" cy="640" rx="10" ry="6" fill="#d4b890" transform="rotate(12 760 640)"/>
</g>
<g stroke="#4a7a52" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.55">
<path d="M70 545 Q75 520 80 545"/>
<path d="M1120 550 Q1125 525 1130 550"/>
<path d="M1140 560 Q1148 535 1155 560"/>
</g>
<ellipse cx="610" cy="598" rx="155" ry="20" fill="#000" opacity="0.14"/>
<g transform="translate(430 410)" filter="url(#shadow)">
<g transform="translate(-95 95)">
<circle r="54" fill="none" stroke="#222" stroke-width="11"/>
<circle r="48" fill="none" stroke="#666" stroke-width="1.5"/>
<g stroke="#999" stroke-width="1.4">
<line x1="0" y1="0" x2="0" y2="-44"/>
<line x1="0" y1="0" x2="38" y2="-22"/>
<line x1="0" y1="0" x2="38" y2="22"/>
<line x1="0" y1="0" x2="0" y2="44"/>
<line x1="0" y1="0" x2="-38" y2="22"/>
<line x1="0" y1="0" x2="-38" y2="-22"/>
</g>
<circle r="7" fill="url(#chrome)"/>
</g>
<g transform="translate(130 95)">
<circle r="54" fill="none" stroke="#222" stroke-width="11"/>
<circle r="48" fill="none" stroke="#666" stroke-width="1.5"/>
<g stroke="#999" stroke-width="1.4">
<line x1="0" y1="0" x2="0" y2="-44"/>
<line x1="0" y1="0" x2="38" y2="-22"/>
<line x1="0" y1="0" x2="38" y2="22"/>
<line x1="0" y1="0" x2="0" y2="44"/>
<line x1="0" y1="0" x2="-38" y2="22"/>
<line x1="0" y1="0" x2="-38" y2="-22"/>
</g>
<circle r="7" fill="url(#chrome)"/>
</g>
<g stroke="url(#chrome)" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M-95 95 L25 -35 L130 95"/>
<line x1="25" y1="-35" x2="25" y2="25"/>
<line x1="-40" y1="25" x2="25" y2="95"/>
<line x1="-95" y1="95" x2="25" y2="95"/>
<line x1="25" y1="-35" x2="-60" y2="55"/>
</g>
<line x1="130" y1="95" x2="130" y2="-8" stroke="url(#chrome)" stroke-width="4" stroke-linecap="round"/>
<path d="M115 -8 Q108 -28 92 -32" fill="none" stroke="url(#chrome)" stroke-width="4" stroke-linecap="round"/>
<path d="M145 -8 Q152 -28 168 -32" fill="none" stroke="url(#chrome)" stroke-width="4" stroke-linecap="round"/>
<rect x="108" y="-38" width="44" height="7" rx="3" fill="#253545"/>
<ellipse cx="-42" cy="18" rx="24" ry="9" fill="#7a4520" transform="rotate(-12 -42 18)"/>
<rect x="-46" y="14" width="9" height="16" fill="url(#chrome)"/>
<g transform="translate(25 95)">
<circle r="15" fill="none" stroke="url(#chrome)" stroke-width="3"/>
<line x1="-20" y1="0" x2="20" y2="0" stroke="#555" stroke-width="4" stroke-linecap="round"/>
<rect x="-32" y="-5" width="14" height="10" rx="2" fill="#333"/>
<rect x="18" y="-5" width="14" height="10" rx="2" fill="#333"/>
</g>
<g transform="translate(130 -2)">
<path d="M-28 0 L-28 28 L28 28 L28 0 Z" fill="none" stroke="#c49a55" stroke-width="3"/>
<line x1="-28" y1="10" x2="28" y2="10" stroke="#c49a55" stroke-width="2"/>
<line x1="-28" y1="20" x2="28" y2="20" stroke="#c49a55" stroke-width="2"/>
<ellipse cx="0" cy="18" rx="14" ry="6" fill="#6eb5c8"/>
<path d="M14 18 L22 14 L22 22 Z" fill="#6eb5c8"/>
</g>
</g>
<g transform="translate(455 255)" filter="url(#shadow)">
<path d="M-70 95 Q-105 70 -115 98 Q-95 108 -75 92 Q-88 112 -58 98 Z" fill="url(#featherDark)"/>
<g transform="translate(5 215)">
<path d="M0 0 Q-8 35 8 62" fill="none" stroke="#e89a45" stroke-width="9" stroke-linecap="round"/>
<path d="M8 62 L18 88" fill="none" stroke="#d08030" stroke-width="6" stroke-linecap="round"/>
<path d="M18 88 L8 98 L28 98 L22 90 L34 98 L28 90" fill="#e89a45"/>
</g>
<g transform="translate(45 215)">
<path d="M0 0 Q12 30 24 58" fill="none" stroke="#e89a45" stroke-width="9" stroke-linecap="round"/>
<path d="M24 58 L34 82" fill="none" stroke="#d08030" stroke-width="6" stroke-linecap="round"/>
<path d="M34 82 L24 94 L44 94 L38 86 L48 94 L42 86" fill="#e89a45"/>
</g>
<ellipse cx="35" cy="125" rx="58" ry="72" fill="url(#featherWhite)"/>
<ellipse cx="25" cy="135" rx="42" ry="58" fill="#ccc4b8" opacity="0.25"/>
<path d="M-5 95 Q-55 72 -78 102 Q-65 138 -28 145 Q0 128 -5 95 Z" fill="url(#featherDark)"/>
<path d="M55 88 Q98 58 108 96 Q92 125 62 118 Q48 102 55 88 Z" fill="url(#featherDark)" opacity="0.9"/>
<path d="M60 72 Q82 28 98 -2" fill="none" stroke="url(#featherWhite)" stroke-width="30" stroke-linecap="round"/>
<ellipse cx="102" cy="-8" rx="30" ry="26" fill="url(#featherWhite)"/>
<circle cx="116" cy="-14" r="9" fill="#fff"/>
<circle cx="118" cy="-14" r="5.5" fill="#1a2233"/>
<circle cx="120" cy="-16" r="2" fill="#fff"/>
<path d="M125 -10 Q175 8 215 0 Q225 18 208 32 Q168 44 128 28 Q118 18 125 -10 Z" fill="url(#pouch)"/>
<path d="M125 -18 Q178 -8 218 -12 Q224 -4 208 4 L125 -4 Z" fill="url(#beakTop)"/>
<path d="M132 -14 Q172 -8 206 -10" fill="none" stroke="#b87818" stroke-width="1.5"/>
<ellipse cx="182" cy="-10" rx="3" ry="2" fill="#9a6018"/>
<path d="M88 -28 Q84 -46 96 -50 Q100 -38 92 -30" fill="#2a3848"/>
<path d="M96 -30 Q92 -48 104 -52 Q108 -40 100 -32" fill="#1f2a36"/>
<g stroke="#fff" stroke-width="2" stroke-linecap="round" opacity="0.35">
<line x1="310" y1="340" x2="280" y2="340"/>
<line x1="318" y1="360" x2="285" y2="360"/>
</g>
</g>
<g fill="#fff" opacity="0.45">
<circle cx="400" cy="565" r="3"/>
<circle cx="415" cy="558" r="2"/>
<circle cx="820" cy="562" r="3"/>
<circle cx="838" cy="555" r="2"/>
</g>
<g transform="translate(250 310)" opacity="0.55">
<rect x="-7" y="0" width="14" height="58" fill="#ece4d4"/>
<rect x="-9" y="-6" width="18" height="8" fill="#c04848"/>
<polygon points="0,-16 -11,-6 11,-6" fill="#c04848"/>
</g>
<g fill="#fff" opacity="0.65">
<path d="M500 190 L508 186 L516 190 L508 194 Z"/>
<path d="M640 210 L646 207 L652 210 L646 213 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB