Add Claude Haiku 4.5 artifacts
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
1 parent
00226c3e70
commit
2b85907eb3
5 files changed
+860
No files matched your search
@@ -35,6 +35,7 @@ One-shot LLM eval cases by NAGI STUDIO: same prompt, different model x harness x
|
||||
| Claude Opus 4.6 | Anthropic | Cursor · Max | 02 |
|
||||
| Claude Opus 4.5 | Anthropic | Cursor · Thinking | 02 |
|
||||
| Claude Sonnet 4.6 | Anthropic | Claude Code · Max<br>Cursor · High | 03 |
|
||||
| Claude Haiku 4.5 | Anthropic | Claude Code · Default | 02 |
|
||||
| GPT-5.5 Pro | OpenAI | ChatGPT Web · Extended Pro | 02 |
|
||||
| GPT-5.5 | OpenAI | Codex CLI · xhigh | 02 |
|
||||
| GPT-5.4 | OpenAI | Cursor · xhigh<br>Codex CLI · xhigh | 05 |
|
||||
|
||||
@@ -35,6 +35,7 @@ NAGI STUDIO 的 LLM 测评案例集:同一段提示词,不同「模型 × Ha
|
||||
| Claude Opus 4.6 | Anthropic | Cursor · Max | 02 |
|
||||
| Claude Opus 4.5 | Anthropic | Cursor · Thinking | 02 |
|
||||
| Claude Sonnet 4.6 | Anthropic | Claude Code · Max<br>Cursor · High | 03 |
|
||||
| Claude Haiku 4.5 | Anthropic | Claude Code · Default | 02 |
|
||||
| GPT-5.5 Pro | OpenAI | ChatGPT Web · Extended Pro | 02 |
|
||||
| GPT-5.5 | OpenAI | Codex CLI · xhigh | 02 |
|
||||
| GPT-5.4 | OpenAI | Cursor · xhigh<br>Codex CLI · xhigh | 05 |
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"label": "Claude Haiku 4.5",
|
||||
"vendor": "Anthropic",
|
||||
"harness": "Claude Code",
|
||||
"effort": "Default",
|
||||
"artifactDir": "claude-haiku-4-5/claude-code-default",
|
||||
"order": 19.5,
|
||||
"runs": {
|
||||
"mythos-craft": {
|
||||
"file": "minecraft-clone.html",
|
||||
"note": {
|
||||
"zh": "在 Claude Code 中以 Claude Haiku 4.5(Default 思考配额)一次生成。原始 HTML 因 Three.js 异步加载、渲染 canvas 绑定和 world 变量接线问题无法正常打开;Codex 仅修复启动接线以让原始实现可运行,未改动玩法、地形、视觉设计或控制逻辑。",
|
||||
"en": "Generated in one shot by Claude Haiku 4.5 (Default effort) inside Claude Code. The original HTML could not open correctly due to Three.js async loading, render-canvas binding, and world variable wiring issues; Codex only fixed the startup wiring so the original implementation can run, without changing gameplay, terrain, visual design, or control logic."
|
||||
},
|
||||
"contributor": "Nagi-ovo"
|
||||
},
|
||||
"pelican-cycling": {
|
||||
"file": "pelican-bike.svg",
|
||||
"note": {
|
||||
"zh": "在 Claude Code 中以 Claude Haiku 4.5(Default 思考配额)一次生成;本页为未经修改的原始 SVG 产出。",
|
||||
"en": "Generated in one shot by Claude Haiku 4.5 (Default effort) inside Claude Code; unmodified original SVG output."
|
||||
},
|
||||
"contributor": "Nagi-ovo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>魔幻世界 - Minecraft克隆版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#info {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
z-index: 100;
|
||||
max-width: 300px;
|
||||
}
|
||||
#info p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
#crosshair {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
#crosshair::before,
|
||||
#crosshair::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
#crosshair::before {
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
left: 6px;
|
||||
top: 9px;
|
||||
}
|
||||
#crosshair::after {
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
left: 9px;
|
||||
top: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="crosshair"></div>
|
||||
<div id="info">
|
||||
<p><strong>魔幻世界</strong></p>
|
||||
<p>移动: WASD | 跳跃: 空格</p>
|
||||
<p>鼠标: 环视 | 左键: 破坏 | 右键: 放置</p>
|
||||
<p id="pos">位置: (0, 0, 0)</p>
|
||||
<p id="block">方块: 无</p>
|
||||
<p id="fps">FPS: 0</p>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script>
|
||||
// ==================== 柏林噪声实现 ====================
|
||||
class PerlinNoise {
|
||||
constructor(seed = 0) {
|
||||
this.seed = seed;
|
||||
this.permutation = this.buildPermutation(seed);
|
||||
this.p = [...this.permutation, ...this.permutation];
|
||||
}
|
||||
|
||||
buildPermutation(seed) {
|
||||
const p = Array.from({ length: 256 }, (_, i) => i);
|
||||
// Fisher-Yates shuffle with seed
|
||||
for (let i = 255; i > 0; i--) {
|
||||
const j = Math.floor((Math.sin(seed + i) * 10000 % 1) * (i + 1));
|
||||
[p[i], p[j]] = [p[j], p[i]];
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
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 < 8 ? y : z;
|
||||
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
|
||||
}
|
||||
|
||||
noise(x, y, z) {
|
||||
const xi = Math.floor(x) & 255;
|
||||
const yi = Math.floor(y) & 255;
|
||||
const zi = Math.floor(z) & 255;
|
||||
|
||||
const xf = x - Math.floor(x);
|
||||
const yf = y - Math.floor(y);
|
||||
const zf = z - Math.floor(z);
|
||||
|
||||
const u = this.fade(xf);
|
||||
const v = this.fade(yf);
|
||||
const w = this.fade(zf);
|
||||
|
||||
const aaa = this.p[this.p[this.p[xi] + yi] + zi];
|
||||
const aba = this.p[this.p[this.p[xi] + (yi + 1)] + zi];
|
||||
const aab = this.p[this.p[this.p[xi] + yi] + (zi + 1)];
|
||||
const abb = this.p[this.p[this.p[xi] + (yi + 1)] + (zi + 1)];
|
||||
const baa = this.p[this.p[this.p[(xi + 1)] + yi] + zi];
|
||||
const bba = this.p[this.p[this.p[(xi + 1)] + (yi + 1)] + zi];
|
||||
const bab = this.p[this.p[this.p[(xi + 1)] + yi] + (zi + 1)];
|
||||
const bbb = this.p[this.p[this.p[(xi + 1)] + (yi + 1)] + (zi + 1)];
|
||||
|
||||
const ga = this.grad(aaa, xf, yf, zf);
|
||||
const gb = this.grad(baa, xf - 1, yf, zf);
|
||||
const gc = this.grad(aba, xf, yf - 1, zf);
|
||||
const gd = this.grad(bba, xf - 1, yf - 1, zf);
|
||||
const ge = this.grad(aab, xf, yf, zf - 1);
|
||||
const gf = this.grad(bab, xf - 1, yf, zf - 1);
|
||||
const gg = this.grad(abb, xf, yf - 1, zf - 1);
|
||||
const gh = this.grad(bbb, xf - 1, yf - 1, zf - 1);
|
||||
|
||||
const p0 = this.lerp(u, ga, gb);
|
||||
const p1 = this.lerp(u, gc, gd);
|
||||
const p2 = this.lerp(u, ge, gf);
|
||||
const p3 = this.lerp(u, gg, gh);
|
||||
const p4 = this.lerp(v, p0, p1);
|
||||
const p5 = this.lerp(v, p2, p3);
|
||||
return this.lerp(w, p4, p5);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 块状世界管理 ====================
|
||||
const CHUNK_SIZE = 16;
|
||||
const CHUNK_HEIGHT = 256;
|
||||
const WORLD_HEIGHT = 64;
|
||||
const LOAD_DISTANCE = 3;
|
||||
|
||||
const BlockType = {
|
||||
AIR: 0,
|
||||
GRASS: 1,
|
||||
DIRT: 2,
|
||||
STONE: 3,
|
||||
OBSIDIAN: 4,
|
||||
CRYSTAL: 5
|
||||
};
|
||||
|
||||
const BlockColors = {
|
||||
[BlockType.GRASS]: new THREE.Color(0x7cb342),
|
||||
[BlockType.DIRT]: new THREE.Color(0x8b6f47),
|
||||
[BlockType.STONE]: new THREE.Color(0x757575),
|
||||
[BlockType.OBSIDIAN]: new THREE.Color(0x1a1a2e),
|
||||
[BlockType.CRYSTAL]: new THREE.Color(0x00bcd4)
|
||||
};
|
||||
|
||||
class World {
|
||||
constructor() {
|
||||
this.perlin = new PerlinNoise(42);
|
||||
this.chunks = new Map();
|
||||
this.scene = null;
|
||||
this.meshes = new Map();
|
||||
}
|
||||
|
||||
getBlockType(x, y, z) {
|
||||
const h = this.getHeight(x, z);
|
||||
if (y > h) return BlockType.AIR;
|
||||
if (y === h) return BlockType.GRASS;
|
||||
if (y > h - 3) return BlockType.DIRT;
|
||||
return BlockType.STONE;
|
||||
}
|
||||
|
||||
getHeight(x, z) {
|
||||
let height = 20;
|
||||
let scale = 1;
|
||||
let amplitude = 1;
|
||||
let maxAmplitude = 0;
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const n = this.perlin.noise(x * scale * 0.05, 0, z * scale * 0.05);
|
||||
height += (n * 0.5 + 0.5) * 15 * amplitude;
|
||||
maxAmplitude += amplitude;
|
||||
scale *= 2;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
|
||||
return Math.floor(height / maxAmplitude);
|
||||
}
|
||||
|
||||
addMythicStructures(x, z, chunk) {
|
||||
const chunkX = Math.floor(x / CHUNK_SIZE);
|
||||
const chunkZ = Math.floor(z / CHUNK_SIZE);
|
||||
|
||||
// 魔法塔
|
||||
if (chunkX === 2 && chunkZ === 2) {
|
||||
this.buildMagicTower(x + 5, z + 5, chunk);
|
||||
}
|
||||
// 古代神庙
|
||||
if (chunkX === -1 && chunkZ === 1) {
|
||||
this.buildAncientTemple(x + 8, z + 8, chunk);
|
||||
}
|
||||
// 水晶洞
|
||||
if (chunkX === 1 && chunkZ === -1) {
|
||||
this.buildCrystalCave(x + 7, z + 7, chunk);
|
||||
}
|
||||
// 悬浮岛
|
||||
if (chunkX === -2 && chunkZ === -2) {
|
||||
this.buildFloatingIsland(x + 10, z + 10, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
buildMagicTower(baseX, baseZ, chunk) {
|
||||
const baseHeight = this.getHeight(baseX, baseZ) + 1;
|
||||
// 塔底
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
for (let dz = -2; dz <= 2; dz++) {
|
||||
const x = Math.floor(baseX + dx);
|
||||
const z = Math.floor(baseZ + dz);
|
||||
if (this.isInChunk(x, z, chunk)) {
|
||||
const idx = this.getBlockIndex(x, z, baseHeight, chunk);
|
||||
if (idx >= 0) chunk[idx] = BlockType.OBSIDIAN;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 塔身
|
||||
for (let h = 1; h < 15; h++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dz = -1; dz <= 1; dz++) {
|
||||
if (Math.abs(dx) === 1 || Math.abs(dz) === 1) {
|
||||
const x = Math.floor(baseX + dx);
|
||||
const z = Math.floor(baseZ + dz);
|
||||
if (this.isInChunk(x, z, chunk)) {
|
||||
const idx = this.getBlockIndex(x, z, baseHeight + h, chunk);
|
||||
if (idx >= 0) chunk[idx] = h % 2 === 0 ? BlockType.OBSIDIAN : BlockType.CRYSTAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildAncientTemple(baseX, baseZ, chunk) {
|
||||
const baseHeight = this.getHeight(baseX, baseZ) + 1;
|
||||
// 主体
|
||||
for (let dx = -3; dx <= 3; dx++) {
|
||||
for (let dz = -3; dz <= 3; dz++) {
|
||||
const x = Math.floor(baseX + dx);
|
||||
const z = Math.floor(baseZ + dz);
|
||||
if (this.isInChunk(x, z, chunk)) {
|
||||
const idx = this.getBlockIndex(x, z, baseHeight, chunk);
|
||||
if (idx >= 0) chunk[idx] = BlockType.STONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 柱子
|
||||
for (let h = 1; h < 8; h++) {
|
||||
const positions = [[-3, -3], [3, -3], [-3, 3], [3, 3]];
|
||||
positions.forEach(([dx, dz]) => {
|
||||
const x = Math.floor(baseX + dx);
|
||||
const z = Math.floor(baseZ + dz);
|
||||
if (this.isInChunk(x, z, chunk)) {
|
||||
const idx = this.getBlockIndex(x, z, baseHeight + h, chunk);
|
||||
if (idx >= 0) chunk[idx] = BlockType.STONE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buildCrystalCave(baseX, baseZ, chunk) {
|
||||
const baseHeight = this.getHeight(baseX, baseZ) - 5;
|
||||
for (let dx = -4; dx <= 4; dx++) {
|
||||
for (let dz = -4; dz <= 4; dz++) {
|
||||
for (let h = 0; h < 8; h++) {
|
||||
if (Math.random() < 0.3) {
|
||||
const x = Math.floor(baseX + dx);
|
||||
const z = Math.floor(baseZ + dz);
|
||||
if (this.isInChunk(x, z, chunk)) {
|
||||
const idx = this.getBlockIndex(x, z, baseHeight + h, chunk);
|
||||
if (idx >= 0) chunk[idx] = BlockType.CRYSTAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFloatingIsland(baseX, baseZ, chunk) {
|
||||
const baseHeight = this.getHeight(baseX, baseZ) + 30;
|
||||
for (let dx = -5; dx <= 5; dx++) {
|
||||
for (let dz = -5; dz <= 5; dz++) {
|
||||
const dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist < 5) {
|
||||
const x = Math.floor(baseX + dx);
|
||||
const z = Math.floor(baseZ + dz);
|
||||
if (this.isInChunk(x, z, chunk)) {
|
||||
const idx = this.getBlockIndex(x, z, baseHeight, chunk);
|
||||
if (idx >= 0) chunk[idx] = BlockType.GRASS;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isInChunk(x, z, chunk) {
|
||||
return x >= 0 && x < CHUNK_SIZE && z >= 0 && z < CHUNK_SIZE;
|
||||
}
|
||||
|
||||
getBlockIndex(x, z, y, chunk) {
|
||||
if (y < 0 || y >= CHUNK_HEIGHT) return -1;
|
||||
return y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x;
|
||||
}
|
||||
|
||||
getChunk(cx, cz) {
|
||||
const key = `${cx},${cz}`;
|
||||
if (!this.chunks.has(key)) {
|
||||
const chunk = new Uint8Array(CHUNK_SIZE * CHUNK_SIZE * CHUNK_HEIGHT);
|
||||
for (let x = 0; x < CHUNK_SIZE; x++) {
|
||||
for (let z = 0; z < CHUNK_SIZE; z++) {
|
||||
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
||||
const wx = cx * CHUNK_SIZE + x;
|
||||
const wz = cz * CHUNK_SIZE + z;
|
||||
const blockType = this.getBlockType(wx, y, wz);
|
||||
const idx = y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x;
|
||||
chunk[idx] = blockType;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.addMythicStructures(cx * CHUNK_SIZE, cz * CHUNK_SIZE, chunk);
|
||||
this.chunks.set(key, chunk);
|
||||
}
|
||||
return this.chunks.get(key);
|
||||
}
|
||||
|
||||
getBlock(x, y, z) {
|
||||
const cx = Math.floor(x / CHUNK_SIZE);
|
||||
const cz = Math.floor(z / CHUNK_SIZE);
|
||||
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
|
||||
const chunk = this.getChunk(cx, cz);
|
||||
if (y < 0 || y >= CHUNK_HEIGHT) return BlockType.AIR;
|
||||
return chunk[y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx];
|
||||
}
|
||||
|
||||
setBlock(x, y, z, type) {
|
||||
const cx = Math.floor(x / CHUNK_SIZE);
|
||||
const cz = Math.floor(z / CHUNK_SIZE);
|
||||
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
||||
|
||||
if (y < 0 || y >= CHUNK_HEIGHT) return;
|
||||
const chunk = this.getChunk(cx, cz);
|
||||
chunk[y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx] = type;
|
||||
this.updateChunkMesh(cx, cz);
|
||||
// 更新相邻块
|
||||
if (lx === 0) this.updateChunkMesh(cx - 1, cz);
|
||||
if (lx === CHUNK_SIZE - 1) this.updateChunkMesh(cx + 1, cz);
|
||||
if (lz === 0) this.updateChunkMesh(cx, cz - 1);
|
||||
if (lz === CHUNK_SIZE - 1) this.updateChunkMesh(cx, cz + 1);
|
||||
}
|
||||
|
||||
updateChunkMesh(cx, cz) {
|
||||
const key = `${cx},${cz}`;
|
||||
if (this.meshes.has(key)) {
|
||||
this.scene.remove(this.meshes.get(key));
|
||||
}
|
||||
const mesh = this.createChunkMesh(cx, cz);
|
||||
if (mesh) {
|
||||
this.scene.add(mesh);
|
||||
this.meshes.set(key, mesh);
|
||||
}
|
||||
}
|
||||
|
||||
createChunkMesh(cx, cz) {
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const vertices = [];
|
||||
const colors = [];
|
||||
const indices = [];
|
||||
|
||||
let indexOffset = 0;
|
||||
|
||||
for (let x = 0; x < CHUNK_SIZE; x++) {
|
||||
for (let z = 0; z < CHUNK_SIZE; z++) {
|
||||
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
||||
const wx = cx * CHUNK_SIZE + x;
|
||||
const wz = cz * CHUNK_SIZE + z;
|
||||
const block = this.getBlock(wx, y, wz);
|
||||
|
||||
if (block === BlockType.AIR) continue;
|
||||
|
||||
const color = BlockColors[block];
|
||||
let facesAdded = 0;
|
||||
|
||||
// 检查六个面
|
||||
const faces = [
|
||||
{ dir: [0, 1, 0], norm: [0, 1, 0] },
|
||||
{ dir: [0, -1, 0], norm: [0, -1, 0] },
|
||||
{ dir: [1, 0, 0], norm: [1, 0, 0] },
|
||||
{ dir: [-1, 0, 0], norm: [-1, 0, 0] },
|
||||
{ dir: [0, 0, 1], norm: [0, 0, 1] },
|
||||
{ dir: [0, 0, -1], norm: [0, 0, -1] }
|
||||
];
|
||||
|
||||
faces.forEach(face => {
|
||||
if (this.getBlock(wx + face.dir[0], y + face.dir[1], wz + face.dir[2]) === BlockType.AIR) {
|
||||
const v = this.getFaceVertices(x, y, z, face.dir);
|
||||
const startIdx = indexOffset;
|
||||
v.forEach(vertex => {
|
||||
vertices.push(...vertex);
|
||||
colors.push(color.r, color.g, color.b);
|
||||
});
|
||||
indices.push(
|
||||
startIdx, startIdx + 1, startIdx + 2,
|
||||
startIdx + 2, startIdx + 3, startIdx
|
||||
);
|
||||
indexOffset += 4;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.length === 0) return null;
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));
|
||||
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
||||
|
||||
const material = new THREE.MeshLambertMaterial({
|
||||
vertexColors: true,
|
||||
side: THREE.DoubleSide,
|
||||
flatShading: true
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(cx * CHUNK_SIZE, 0, cz * CHUNK_SIZE);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
getFaceVertices(x, y, z, dir) {
|
||||
const vertices = [
|
||||
[x, y, z], [x + 1, y, z], [x + 1, y + 1, z], [x, y + 1, z]
|
||||
];
|
||||
|
||||
if (dir[0] === 1) return [[x + 1, y, z], [x + 1, y, z + 1], [x + 1, y + 1, z + 1], [x + 1, y + 1, z]];
|
||||
if (dir[0] === -1) return [[x, y, z + 1], [x, y, z], [x, y + 1, z], [x, y + 1, z + 1]];
|
||||
if (dir[2] === 1) return [[x, y, z + 1], [x + 1, y, z + 1], [x + 1, y + 1, z + 1], [x, y + 1, z + 1]];
|
||||
if (dir[2] === -1) return [[x + 1, y, z], [x, y, z], [x, y + 1, z], [x + 1, y + 1, z]];
|
||||
if (dir[1] === 1) return [[x, y + 1, z], [x + 1, y + 1, z], [x + 1, y + 1, z + 1], [x, y + 1, z + 1]];
|
||||
if (dir[1] === -1) return [[x, y, z + 1], [x + 1, y, z + 1], [x + 1, y, z], [x, y, z]];
|
||||
return vertices;
|
||||
}
|
||||
|
||||
updateChunks(playerX, playerZ) {
|
||||
const cx = Math.floor(playerX / CHUNK_SIZE);
|
||||
const cz = Math.floor(playerZ / CHUNK_SIZE);
|
||||
|
||||
for (let dx = -LOAD_DISTANCE; dx <= LOAD_DISTANCE; dx++) {
|
||||
for (let dz = -LOAD_DISTANCE; dz <= LOAD_DISTANCE; dz++) {
|
||||
const key = `${cx + dx},${cz + dz}`;
|
||||
if (!this.meshes.has(key)) {
|
||||
const mesh = this.createChunkMesh(cx + dx, cz + dz);
|
||||
if (mesh) {
|
||||
this.scene.add(mesh);
|
||||
this.meshes.set(key, mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载远处的块
|
||||
for (const [key, mesh] of this.meshes.entries()) {
|
||||
const [mcx, mcz] = key.split(',').map(Number);
|
||||
if (Math.abs(mcx - cx) > LOAD_DISTANCE + 1 || Math.abs(mcz - cz) > LOAD_DISTANCE + 1) {
|
||||
this.scene.remove(mesh);
|
||||
this.meshes.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 玩家控制 ====================
|
||||
class Player {
|
||||
constructor(camera) {
|
||||
this.camera = camera;
|
||||
this.velocity = new THREE.Vector3();
|
||||
this.keys = {};
|
||||
this.euler = new THREE.Euler(0, 0, 0, 'YXZ');
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.selectedBlock = null;
|
||||
this.selectedFace = null;
|
||||
|
||||
document.addEventListener('keydown', (e) => this.keys[e.key.toLowerCase()] = true);
|
||||
document.addEventListener('keyup', (e) => this.keys[e.key.toLowerCase()] = false);
|
||||
document.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
document.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||
document.addEventListener('mouseup', (e) => this.onMouseUp(e));
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
if (document.pointerLockElement === document.documentElement) return;
|
||||
document.documentElement.requestPointerLock();
|
||||
});
|
||||
}
|
||||
|
||||
onMouseMove(e) {
|
||||
if (document.pointerLockElement !== document.documentElement) return;
|
||||
this.euler.setFromQuaternion(this.camera.quaternion);
|
||||
this.euler.rotateY(-e.movementX * 0.005);
|
||||
this.euler.rotateX(-e.movementY * 0.005);
|
||||
this.euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.euler.x));
|
||||
this.camera.quaternion.setFromEuler(this.euler);
|
||||
}
|
||||
|
||||
onMouseDown(e) {
|
||||
if (!this.selectedBlock) return;
|
||||
if (e.button === 0) this.destroyBlock();
|
||||
if (e.button === 2) this.placeBlock();
|
||||
}
|
||||
|
||||
onMouseUp(e) {}
|
||||
|
||||
destroyBlock() {
|
||||
if (this.selectedBlock) {
|
||||
const pos = this.selectedBlock;
|
||||
window.world.setBlock(pos.x, pos.y, pos.z, BlockType.AIR);
|
||||
}
|
||||
}
|
||||
|
||||
placeBlock() {
|
||||
if (!this.selectedBlock || !this.selectedFace) return;
|
||||
const pos = this.selectedBlock;
|
||||
const face = this.selectedFace;
|
||||
const newX = pos.x + face[0];
|
||||
const newY = pos.y + face[1];
|
||||
const newZ = pos.z + face[2];
|
||||
window.world.setBlock(newX, newY, newZ, BlockType.DIRT);
|
||||
}
|
||||
|
||||
update(world, delta) {
|
||||
// 移动
|
||||
const speed = 6;
|
||||
const forward = new THREE.Vector3();
|
||||
const right = new THREE.Vector3();
|
||||
this.camera.getWorldDirection(forward);
|
||||
forward.y = 0;
|
||||
forward.normalize();
|
||||
right.crossVectors(forward, new THREE.Vector3(0, 1, 0));
|
||||
|
||||
this.velocity.x = 0;
|
||||
this.velocity.z = 0;
|
||||
|
||||
if (this.keys['w']) this.velocity.add(forward.multiplyScalar(speed));
|
||||
if (this.keys['s']) this.velocity.sub(forward.multiplyScalar(speed));
|
||||
if (this.keys['a']) this.velocity.sub(right.multiplyScalar(speed));
|
||||
if (this.keys['d']) this.velocity.add(right.multiplyScalar(speed));
|
||||
|
||||
// 重力
|
||||
this.velocity.y -= 9.8 * delta;
|
||||
|
||||
// 碰撞检测
|
||||
const nextPos = this.camera.position.clone().add(this.velocity.clone().multiplyScalar(delta));
|
||||
const below = Math.floor(nextPos.y - 1.6);
|
||||
const blockBelow = world.getBlock(Math.floor(nextPos.x), below, Math.floor(nextPos.z));
|
||||
|
||||
if (blockBelow !== BlockType.AIR && this.velocity.y < 0) {
|
||||
this.velocity.y = 0;
|
||||
nextPos.y = below + 1.6;
|
||||
if (this.keys[' ']) this.velocity.y = 8;
|
||||
}
|
||||
|
||||
this.camera.position.copy(nextPos);
|
||||
|
||||
// 射线检测方块
|
||||
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
|
||||
const intersects = this.raycaster.intersectObjects(world.scene.children);
|
||||
|
||||
this.selectedBlock = null;
|
||||
this.selectedFace = null;
|
||||
|
||||
for (let intersection of intersects) {
|
||||
const point = intersection.point;
|
||||
const normal = intersection.face.normal;
|
||||
|
||||
const blockPos = {
|
||||
x: Math.floor(point.x - normal.x * 0.1),
|
||||
y: Math.floor(point.y - normal.y * 0.1),
|
||||
z: Math.floor(point.z - normal.z * 0.1)
|
||||
};
|
||||
|
||||
if (world.getBlock(blockPos.x, blockPos.y, blockPos.z) !== BlockType.AIR) {
|
||||
this.selectedBlock = blockPos;
|
||||
this.selectedFace = [normal.x, normal.y, normal.z];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新块加载
|
||||
world.updateChunks(this.camera.position.x, this.camera.position.z);
|
||||
|
||||
// 更新UI
|
||||
const pos = this.camera.position;
|
||||
document.getElementById('pos').textContent = `位置: (${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)})`;
|
||||
if (this.selectedBlock) {
|
||||
const block = this.selectedBlock;
|
||||
const type = world.getBlock(block.x, block.y, block.z);
|
||||
const typeNames = { 1: '草地', 2: '泥土', 3: '石头', 4: '黑曜石', 5: '魔法水晶' };
|
||||
document.getElementById('block').textContent = `方块: ${typeNames[type] || '未知'}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
let camera, scene, renderer, world, player;
|
||||
let lastTime = Date.now();
|
||||
let frameCount = 0;
|
||||
let lastFpsUpdate = Date.now();
|
||||
|
||||
function init() {
|
||||
// 场景
|
||||
scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x87ceeb);
|
||||
scene.fog = new THREE.Fog(0x87ceeb, 200, 300);
|
||||
|
||||
// 相机
|
||||
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(0, 50, 0);
|
||||
|
||||
// 渲染器
|
||||
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.shadowMap.enabled = true;
|
||||
|
||||
// 光源
|
||||
const light = new THREE.DirectionalLight(0xffffff, 1);
|
||||
light.position.set(50, 100, 50);
|
||||
light.castShadow = true;
|
||||
light.shadow.camera.far = 500;
|
||||
light.shadow.camera.left = -200;
|
||||
light.shadow.camera.right = 200;
|
||||
light.shadow.camera.top = 200;
|
||||
light.shadow.camera.bottom = -200;
|
||||
scene.add(light);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
scene.add(ambientLight);
|
||||
|
||||
// 世界
|
||||
world = new World();
|
||||
window.world = world;
|
||||
world.scene = scene;
|
||||
|
||||
// 玩家
|
||||
player = new Player(camera);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
const now = Date.now();
|
||||
const delta = Math.min((now - lastTime) / 1000, 0.016);
|
||||
lastTime = now;
|
||||
|
||||
player.update(world, delta);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
|
||||
frameCount++;
|
||||
if (now - lastFpsUpdate >= 1000) {
|
||||
document.getElementById('fps').textContent = `FPS: ${frameCount}`;
|
||||
frameCount = 0;
|
||||
lastFpsUpdate = now;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
window.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,120 @@
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Sky gradient -->
|
||||
<defs>
|
||||
<linearGradient id="skyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#87CEEB;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#E0F6FF;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="oceanGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1E90FF;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0047AB;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="800" fill="url(#skyGrad)"/>
|
||||
|
||||
<!-- Ocean -->
|
||||
<rect y="450" width="1200" height="350" fill="url(#oceanGrad)"/>
|
||||
|
||||
<!-- Sand -->
|
||||
<ellipse cx="600" cy="480" rx="600" ry="80" fill="#F4A460"/>
|
||||
|
||||
<!-- Waves -->
|
||||
<path d="M 0 450 Q 150 430 300 450 T 600 450 T 900 450 T 1200 450" stroke="#ffffff" stroke-width="2" fill="none" opacity="0.6"/>
|
||||
<path d="M 0 500 Q 150 480 300 500 T 600 500 T 900 500 T 1200 500" stroke="#ffffff" stroke-width="2" fill="none" opacity="0.4"/>
|
||||
|
||||
<!-- Sun -->
|
||||
<circle cx="1000" cy="100" r="60" fill="#FFD700" opacity="0.8"/>
|
||||
|
||||
<!-- Clouds -->
|
||||
<ellipse cx="200" cy="120" rx="80" ry="40" fill="#ffffff" opacity="0.7"/>
|
||||
<ellipse cx="250" cy="100" rx="60" ry="35" fill="#ffffff" opacity="0.7"/>
|
||||
|
||||
<!-- Bicycle frame -->
|
||||
<g id="bike">
|
||||
<!-- Frame -->
|
||||
<line x1="450" y1="280" x2="550" y2="350" stroke="#FF4500" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="450" y1="280" x2="600" y2="280" stroke="#FF4500" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="600" y1="280" x2="550" y2="350" stroke="#FF4500" stroke-width="8" stroke-linecap="round"/>
|
||||
|
||||
<!-- Seat -->
|
||||
<ellipse cx="480" cy="260" rx="35" ry="15" fill="#333333"/>
|
||||
|
||||
<!-- Handlebars -->
|
||||
<line x1="450" y1="280" x2="420" y2="250" stroke="#FF4500" stroke-width="6"/>
|
||||
<circle cx="420" cy="250" r="8" fill="#333333"/>
|
||||
<circle cx="435" cy="260" r="6" fill="#333333"/>
|
||||
|
||||
<!-- Front wheel -->
|
||||
<circle cx="600" cy="350" r="60" fill="none" stroke="#333333" stroke-width="6"/>
|
||||
<circle cx="600" cy="350" r="55" fill="none" stroke="#FFD700" stroke-width="3"/>
|
||||
<line x1="600" y1="290" x2="600" y2="410" stroke="#666666" stroke-width="2"/>
|
||||
<line x1="540" y1="350" x2="660" y2="350" stroke="#666666" stroke-width="2"/>
|
||||
<line x1="550" y1="300" x2="650" y2="400" stroke="#666666" stroke-width="1"/>
|
||||
<line x1="650" y1="300" x2="550" y2="400" stroke="#666666" stroke-width="1"/>
|
||||
|
||||
<!-- Back wheel -->
|
||||
<circle cx="450" cy="350" r="60" fill="none" stroke="#333333" stroke-width="6"/>
|
||||
<circle cx="450" cy="350" r="55" fill="none" stroke="#FFD700" stroke-width="3"/>
|
||||
<line x1="450" y1="290" x2="450" y2="410" stroke="#666666" stroke-width="2"/>
|
||||
<line x1="390" y1="350" x2="510" y2="350" stroke="#666666" stroke-width="2"/>
|
||||
<line x1="400" y1="300" x2="500" y2="400" stroke="#666666" stroke-width="1"/>
|
||||
<line x1="500" y1="300" x2="400" y2="400" stroke="#666666" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Pelican body -->
|
||||
<g id="pelican">
|
||||
<!-- Body (large, round) -->
|
||||
<ellipse cx="520" cy="240" rx="85" ry="100" fill="#F5F5DC"/>
|
||||
<ellipse cx="520" cy="240" rx="85" ry="100" fill="none" stroke="#D3D3D3" stroke-width="2"/>
|
||||
|
||||
<!-- Neck -->
|
||||
<path d="M 480 170 Q 470 130 480 80" stroke="#F5F5DC" stroke-width="45" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Head -->
|
||||
<ellipse cx="490" cy="50" rx="40" ry="45" fill="#F5F5DC"/>
|
||||
<ellipse cx="490" cy="50" rx="40" ry="45" fill="none" stroke="#D3D3D3" stroke-width="2"/>
|
||||
|
||||
<!-- Eye -->
|
||||
<circle cx="510" cy="35" r="6" fill="#000000"/>
|
||||
<circle cx="512" cy="33" r="2" fill="#ffffff"/>
|
||||
|
||||
<!-- Beak pouch (iconic pelican feature) -->
|
||||
<path d="M 515 55 Q 540 65 545 50 Q 540 45 520 45" fill="#FFB6C1" opacity="0.8"/>
|
||||
<path d="M 515 55 Q 540 65 545 50 Q 540 45 520 45" fill="none" stroke="#FF69B4" stroke-width="1.5"/>
|
||||
|
||||
<!-- Beak -->
|
||||
<path d="M 520 45 L 560 50 L 520 60 Z" fill="#F08080"/>
|
||||
|
||||
<!-- Wing -->
|
||||
<ellipse cx="450" cy="200" rx="60" ry="90" fill="#E8E8E8" opacity="0.8" transform="rotate(-30 450 200)"/>
|
||||
|
||||
<!-- Legs -->
|
||||
<line x1="510" y1="340" x2="510" y2="360" stroke="#FFB347" stroke-width="8" stroke-linecap="round"/>
|
||||
<line x1="530" y1="340" x2="530" y2="360" stroke="#FFB347" stroke-width="8" stroke-linecap="round"/>
|
||||
|
||||
<!-- Feet (webbed) -->
|
||||
<ellipse cx="510" cy="368" rx="12" ry="8" fill="#FF8C00"/>
|
||||
<ellipse cx="530" cy="368" rx="12" ry="8" fill="#FF8C00"/>
|
||||
</g>
|
||||
|
||||
<!-- Pelican gripping handlebars -->
|
||||
<g id="pelican-wings-on-bars">
|
||||
<path d="M 440 240 Q 420 220 415 250" stroke="#D3D3D3" stroke-width="6" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M 600 240 Q 620 220 625 250" stroke="#D3D3D3" stroke-width="6" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
</g>
|
||||
|
||||
<!-- Smile/happy expression -->
|
||||
<path d="M 500 65 Q 510 75 520 65" stroke="#000000" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.6"/>
|
||||
|
||||
<!-- Sand beach details -->
|
||||
<circle cx="300" cy="460" r="15" fill="#D2B48C" opacity="0.6"/>
|
||||
<circle cx="950" cy="475" r="20" fill="#D2B48C" opacity="0.6"/>
|
||||
<circle cx="150" cy="485" r="12" fill="#D2B48C" opacity="0.5"/>
|
||||
|
||||
<!-- Sparkle on water -->
|
||||
<circle cx="750" cy="550" r="3" fill="#ffffff" opacity="0.8"/>
|
||||
<circle cx="850" cy="600" r="2" fill="#ffffff" opacity="0.6"/>
|
||||
<circle cx="400" cy="520" r="2.5" fill="#ffffff" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
Reference in new issue
Block a user