Add Opus 4.8 (Claude Code) v2 runs for both cases (#10)

Second-version artifacts for claude-opus-4-8-cc, generated one-shot:
- pelican-cycling-2.svg: hand-authored seaside SVG of a pelican on a bicycle
- mythos-craft-2.html: Three.js voxel world (Perlin terrain, WASD + mouse
  look, break/place, marble temple + crystal spires + floating isle)

Registers both as file-keyed array variants with bilingual provenance
notes; contributor OHMFOHS. Passes scripts/validate-data.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Junchao MaoandClaude Opus 4.8 authored and GitHub committed 2026-06-13 11:55:13 +01:00
1 parent 5f6a44aff7
commit 4f37651620
3 files changed
+788 -12

No files matched your search

+32 -12
View File
@@ -3,20 +3,40 @@
"vendor": "Anthropic",
"order": 15,
"runs": {
"mythos-craft": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.8(Max 思考配额)一次生成,本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.8 (Max effort) inside Claude Code; unmodified original output."
"mythos-craft": [
{
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.8(Max 思考配额)一次生成,本页为未经修改的原始产出。",
"en": "Generated in one shot by Claude Opus 4.8 (Max effort) inside Claude Code; unmodified original output."
},
"contributor": "Nagi-ovo"
},
"contributor": "Nagi-ovo"
},
"pelican-cycling": {
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.8(Max 思考配额)一次生成。",
"en": "Generated in one shot by Claude Opus 4.8 (Max effort) inside Claude Code."
{
"file": "mythos-craft-2.html",
"note": {
"zh": "第二版:在 Claude Code 中由 Claude Opus 4.8(Max 思考配额)一次生成。基于 Three.js 的体素世界,含柏林噪声地形、第一人称 WASD + 鼠标视角、DDA 体素射线拾取的破坏/放置、程序化贴图图集,并自带大理石神庙、水晶尖塔与悬浮岛。代码由模型直接写出,仅做了一次 JS 语法检查,未做其他修改。",
"en": "Second version: generated in one shot by Claude Opus 4.8 (Max effort) inside Claude Code. A Three.js voxel world with Perlin-noise terrain, first-person WASD + mouse-look, DDA voxel-raycast break/place, a procedural texture atlas, and a built-in marble temple, crystal spires and a floating sky-isle. Code written directly by the model; only a JS syntax check was run, otherwise unmodified."
},
"contributor": "OHMFOHS"
}
],
"pelican-cycling": [
{
"note": {
"zh": "在 Claude Code 中以 Claude Opus 4.8(Max 思考配额)一次生成。",
"en": "Generated in one shot by Claude Opus 4.8 (Max effort) inside Claude Code."
},
"contributor": "Nagi-ovo"
},
"contributor": "Nagi-ovo"
}
{
"file": "pelican-cycling-2.svg",
"note": {
"zh": "第二版:在 Claude Code 中由 Claude Opus 4.8(Max 思考配额)一次手写的 SVG,未经修改。海边日落场景:渐变天空与海面、沙滩、远处帆船与海鸥,一只鹈鹕骑着红色自行车,并以速度线增添动感。",
"en": "Second version: a hand-authored SVG written in one shot by Claude Opus 4.8 (Max effort) inside Claude Code; unmodified. A seaside sunset scene — gradient sky and sea, beach, a distant sailboat and gull, and a pelican riding a red bicycle, with motion lines for liveliness."
},
"contributor": "OHMFOHS"
}
]
},
"harness": "Claude Code",
"effort": "Max"
@@ -0,0 +1,557 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mythos Craft — Voxel Realm</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0b0f1a; font-family: 'Segoe UI', system-ui, sans-serif; }
#c { display: block; width: 100%; height: 100%; cursor: crosshair; }
#crosshair {
position: fixed; left: 50%; top: 50%; width: 22px; height: 22px;
transform: translate(-50%, -50%); pointer-events: none; mix-blend-mode: difference;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: #fff;
}
#crosshair::before { left: 50%; top: 0; width: 2px; height: 100%; transform: translateX(-50%); }
#crosshair::after { top: 50%; left: 0; height: 2px; width: 100%; transform: translateY(-50%); }
#hud {
position: fixed; left: 0; bottom: 18px; width: 100%; display: flex; justify-content: center;
gap: 8px; pointer-events: none; user-select: none;
}
.slot {
width: 54px; height: 54px; border-radius: 10px; background: rgba(15,20,34,.55);
border: 2px solid rgba(255,255,255,.18); display: flex; flex-direction: column;
align-items: center; justify-content: center; backdrop-filter: blur(6px); color: #cfe;
font-size: 10px; letter-spacing: .3px;
}
.slot .sw { width: 26px; height: 26px; border-radius: 5px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.3); }
.slot.active { border-color: #ffd76a; box-shadow: 0 0 16px rgba(255,215,106,.55); transform: translateY(-4px); }
.slot .k { position: absolute; }
#info {
position: fixed; top: 14px; left: 14px; color: #d7e6ff; font-size: 13px; line-height: 1.55;
background: rgba(10,14,26,.5); padding: 12px 16px; border-radius: 12px; backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,.1); max-width: 280px; pointer-events: none;
}
#info b { color: #ffd76a; }
#info .key { display: inline-block; min-width: 16px; text-align: center; padding: 1px 6px;
background: rgba(255,255,255,.12); border-radius: 5px; font-weight: 600; margin: 0 1px; }
#start {
position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
background: radial-gradient(circle at 50% 35%, #243b6b, #0b0f1a 70%); color: #eaf2ff; z-index: 10; cursor: pointer;
text-align: center;
}
#start h1 { font-size: 58px; letter-spacing: 2px; margin-bottom: 6px;
background: linear-gradient(90deg,#ffd76a,#7ee8fa,#ff8ad8); -webkit-background-clip: text; background-clip: text; color: transparent; }
#start p { font-size: 15px; opacity: .85; margin-bottom: 26px; }
#start .btn { padding: 14px 34px; border: 2px solid #ffd76a; border-radius: 40px; color: #ffd76a;
font-size: 17px; letter-spacing: 1px; transition: .25s; }
#start:hover .btn { background: #ffd76a; color: #16203a; box-shadow: 0 0 30px rgba(255,215,106,.6); }
#start small { margin-top: 22px; opacity: .55; font-size: 12px; }
#fade { position: fixed; inset: 0; background: #0b0f1a; opacity: 0; pointer-events: none; transition: opacity .4s; z-index: 5; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="crosshair"></div>
<div id="info">
<b>Mythos Craft</b><br>
<span class="key">W A S D</span> move &nbsp; <span class="key">Space</span> jump<br>
<span class="key">Shift</span> sprint &nbsp; <span class="key">F</span> toggle fly<br>
<b>Left-click</b> break &nbsp; <b>Right-click</b> place<br>
<span class="key">1</span>–<span class="key">6</span> / scroll: choose block<br>
<span class="key">Esc</span> release mouse
</div>
<div id="hud"></div>
<div id="fade"></div>
<div id="start">
<h1>MYTHOS&nbsp;CRAFT</h1>
<p>A hand-built voxel realm — temples of marble, crystal spires &amp; a floating sky-isle.</p>
<div class="btn">▶ ENTER THE REALM</div>
<small>Click to play · pointer-lock controls</small>
</div>
<script src="https://unpkg.com/three@0.152.2/build/three.min.js"></script>
<script>
(function () {
'use strict';
if (!window.THREE) { document.getElementById('start').innerHTML = '<h1>Three.js failed to load</h1><p>Check your network connection and reload.</p>'; return; }
// ---------------------------------------------------------------------------
// Block registry (top / side / bottom tile indices into the procedural atlas)
// ---------------------------------------------------------------------------
const T = { GRASS_T:0, GRASS_S:1, DIRT:2, STONE:3, WOOD_T:4, WOOD_S:5, LEAF:6, MARBLE:7, GLOW:8, CRYSTAL:9, SAND:10, WATER:11 };
const TILE_COUNT = 12;
// id -> faces. id 0 = air.
const BLOCKS = {
1: { name: 'Grass', top: T.GRASS_T, side: T.GRASS_S, bottom: T.DIRT, col: '#6abe4f' },
2: { name: 'Dirt', top: T.DIRT, side: T.DIRT, bottom: T.DIRT, col: '#7c5230' },
3: { name: 'Stone', top: T.STONE, side: T.STONE, bottom: T.STONE, col: '#8b8f96' },
4: { name: 'Log', top: T.WOOD_T, side: T.WOOD_S, bottom: T.WOOD_T, col: '#9c7038' },
5: { name: 'Leaves', top: T.LEAF, side: T.LEAF, bottom: T.LEAF, col: '#3f9d4a' },
6: { name: 'Marble', top: T.MARBLE, side: T.MARBLE, bottom: T.MARBLE, col: '#e9e6df' },
7: { name: 'Glowstone',top:T.GLOW, side: T.GLOW, bottom: T.GLOW, col: '#ffcf5c' },
8: { name: 'Crystal', top: T.CRYSTAL, side: T.CRYSTAL, bottom: T.CRYSTAL,col: '#74e6ff' },
9: { name: 'Sand', top: T.SAND, side: T.SAND, bottom: T.SAND, col: '#e6d29a' },
};
const HOTBAR = [1, 3, 6, 7, 8, 4]; // grass, stone, marble, glowstone, crystal, log
// ---------------------------------------------------------------------------
// Procedural texture atlas (one 16px tile per block face type)
// ---------------------------------------------------------------------------
function buildAtlas() {
const TS = 16, cnv = document.createElement('canvas');
cnv.width = TS * TILE_COUNT; cnv.height = TS;
const g = cnv.getContext('2d');
const rnd = mulberry32(1337);
function noiseFill(x0, base, lo, hi) {
for (let y = 0; y < TS; y++) for (let x = 0; x < TS; x++) {
const t = lo + (hi - lo) * rnd();
g.fillStyle = shade(base, t); g.fillRect(x0 + x, y, 1, 1);
}
}
// grass top
noiseFill(T.GRASS_T*TS, [106,190,79], -.12, .12);
// grass side (dirt with green lip)
noiseFill(T.GRASS_S*TS, [124,82,48], -.12, .12);
g.fillStyle = '#5fae46'; for (let x=0;x<TS;x++){ const h=2+Math.floor(rnd()*3); g.fillRect(T.GRASS_S*TS+x,0,1,h); }
// dirt
noiseFill(T.DIRT*TS, [124,82,48], -.14, .14);
// stone
noiseFill(T.STONE*TS, [139,143,150], -.12, .12);
// wood top (rings)
noiseFill(T.WOOD_T*TS, [156,112,56], -.05, .05);
g.strokeStyle='#6e4a22'; for(let r=2;r<8;r+=2){ g.beginPath(); g.arc(T.WOOD_T*TS+8,8,r,0,7); g.stroke(); }
// wood side (bark)
for (let x=0;x<TS;x++){ const b=shade([110,76,40], (rnd()-.5)*.4); for(let y=0;y<TS;y++){ g.fillStyle=b; g.fillRect(T.WOOD_S*TS+x,y,1,1);} }
// leaves
noiseFill(T.LEAF*TS, [63,157,74], -.22, .18);
// marble (veined)
noiseFill(T.MARBLE*TS, [233,230,223], -.04, .04);
g.strokeStyle='rgba(150,150,165,.5)'; g.beginPath(); g.moveTo(T.MARBLE*TS+2,3); g.bezierCurveTo(T.MARBLE*TS+6,8,T.MARBLE*TS+10,4,T.MARBLE*TS+14,12); g.stroke();
// glowstone
noiseFill(T.GLOW*TS, [255,207,92], -.08, .14);
for(let i=0;i<10;i++){ g.fillStyle='#fff3c4'; g.fillRect(T.GLOW*TS+Math.floor(rnd()*TS),Math.floor(rnd()*TS),2,2); }
// crystal
noiseFill(T.CRYSTAL*TS, [116,230,255], -.1, .18);
g.strokeStyle='rgba(255,255,255,.7)'; g.beginPath(); g.moveTo(T.CRYSTAL*TS+8,1); g.lineTo(T.CRYSTAL*TS+13,8); g.lineTo(T.CRYSTAL*TS+8,15); g.lineTo(T.CRYSTAL*TS+3,8); g.closePath(); g.stroke();
// sand
noiseFill(T.SAND*TS, [230,210,154], -.08, .08);
const tex = new THREE.CanvasTexture(cnv);
tex.magFilter = THREE.NearestFilter; tex.minFilter = THREE.NearestFilter;
tex.colorSpace = THREE.SRGBColorSpace || tex.colorSpace;
return tex;
function shade(rgb, t) {
const f = (v) => Math.max(0, Math.min(255, Math.round(v + v * t)));
return 'rgb(' + f(rgb[0]) + ',' + f(rgb[1]) + ',' + f(rgb[2]) + ')';
}
}
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};}
// ---------------------------------------------------------------------------
// Perlin noise (2D, seeded)
// ---------------------------------------------------------------------------
const Perlin = (function () {
const p = new Uint8Array(512); const perm = [];
for (let i = 0; i < 256; i++) perm[i] = i;
const r = mulberry32(20240611);
for (let i = 255; i > 0; i--) { const j = Math.floor(r() * (i + 1)); const t = perm[i]; perm[i] = perm[j]; perm[j] = t; }
for (let i = 0; i < 512; i++) p[i] = perm[i & 255];
const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
const lerp = (a, b, t) => a + t * (b - a);
function grad(h, x, y) { const u = (h & 1) ? x : -x, v = (h & 2) ? y : -y; return u + v; }
function noise(x, y) {
const X = Math.floor(x) & 255, Y = Math.floor(y) & 255;
x -= Math.floor(x); y -= Math.floor(y);
const u = fade(x), v = fade(y);
const aa = p[p[X] + Y], ab = p[p[X] + Y + 1], ba = p[p[X + 1] + Y], bb = p[p[X + 1] + Y + 1];
return lerp(lerp(grad(aa, x, y), grad(ba, x - 1, y), u), lerp(grad(ab, x, y - 1), grad(bb, x - 1, y - 1), u), v);
}
return {
fbm(x, y, oct, lac, gain) {
oct = oct || 4; lac = lac || 2; gain = gain || 0.5;
let f = 1, amp = 1, sum = 0, norm = 0;
for (let i = 0; i < oct; i++) { sum += amp * noise(x * f, y * f); norm += amp; f *= lac; amp *= gain; }
return sum / norm;
}
};
})();
// ---------------------------------------------------------------------------
// World data
// ---------------------------------------------------------------------------
const R = 28; // world half-extent (x,z in [-R, R])
const MAXY = 48;
const world = new Map(); // "x,y,z" -> blockId
const key = (x, y, z) => x + ',' + y + ',' + z;
function get(x, y, z) { return world.get(key(x, y, z)) || 0; }
function set(x, y, z, id) { if (id) world.set(key(x, y, z), id); else world.delete(key(x, y, z)); }
function generate() {
const SEA = 6;
for (let x = -R; x <= R; x++) {
for (let z = -R; z <= R; z++) {
let h = Math.round(8 + Perlin.fbm(x / 42, z / 42, 5) * 11);
// gentle mountains
const m = Perlin.fbm(x / 90 + 10, z / 90 + 10, 3);
if (m > 0.25) h += Math.round((m - 0.25) * 30);
h = Math.max(2, Math.min(MAXY - 8, h));
for (let y = 0; y <= h; y++) {
let id = 3; // stone
if (y === h) id = (h <= SEA + 1) ? 9 : 1; // sand near water, else grass
else if (y > h - 3) id = (h <= SEA + 1) ? 9 : 2; // dirt
set(x, y, z, id);
}
// water fill
for (let y = h + 1; y <= SEA; y++) set(x, y, z, /*water as crystal-ish? no*/ 0);
// trees
if (h > SEA + 1 && Perlin.fbm(x * 1.7, z * 1.7, 2) > 0.42 && Math.abs(x) < R - 2 && Math.abs(z) < R - 2) {
if (((x * 73856093) ^ (z * 19349663)) % 11 === 0) tree(x, h + 1, z);
}
}
}
buildTemple(0, 0);
buildFloatingIsle(-18, 30, 16);
buildCrystalField();
}
function tree(x, y, z) {
const th = 4 + (Math.abs((x * 31 + z * 17)) % 3);
for (let i = 0; i < th; i++) set(x, y + i, z, 4);
const top = y + th;
for (let dx = -2; dx <= 2; dx++) for (let dz = -2; dz <= 2; dz++) for (let dy = -1; dy <= 1; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy) <= 3 && get(x + dx, top + dy, z + dz) === 0)
set(x + dx, top + dy, z + dz, 5);
}
set(x, top + 1, z, 5);
}
// Central mythic temple: stepped marble pyramid, glowstone core, crystal spires
function buildTemple(cx, cz) {
// ground out a plateau
let base = 0;
for (let x = cx - 9; x <= cx + 9; x++) for (let z = cz - 9; z <= cz + 9; z++) {
let top = 0; for (let y = MAXY; y >= 0; y--) if (get(x, y, z)) { top = y; break; }
base = Math.max(base, top);
}
base += 1;
const steps = 6;
for (let s = 0; s < steps; s++) {
const r = 8 - s, y = base + s;
for (let x = cx - r; x <= cx + r; x++) for (let z = cz - r; z <= cz + r; z++) {
const edge = (x === cx - r || x === cx + r || z === cz - r || z === cz + r);
set(x, y, z, edge ? 6 : (s === 0 ? 3 : 6));
}
}
// glowing core chamber on top
const ty = base + steps;
for (let x = cx - 2; x <= cx + 2; x++) for (let z = cz - 2; z <= cz + 2; z++) {
set(x, ty, z, 6);
const wall = (x === cx - 2 || x === cx + 2 || z === cz - 2 || z === cz + 2);
if (wall) { set(x, ty + 1, z, 6); set(x, ty + 2, z, 6); }
}
set(cx, ty + 1, cz, 7); set(cx, ty + 2, cz, 7); // glowstone column
set(cx, ty + 3, cz, 8); // crystal capstone
// corner crystal spires
const sp = [[-7, -7], [7, -7], [-7, 7], [7, 7]];
for (const [dx, dz] of sp) for (let i = 0; i < 5; i++) set(cx + dx, base + 1 + i, cz + dz, i >= 3 ? 8 : 6);
// glowstone lanterns up the stairs
for (let s = 0; s < steps; s++) set(cx, base + s + 1, cz - (8 - s), 7);
}
function buildFloatingIsle(cx, cy, cz) {
for (let x = -7; x <= 7; x++) for (let z = -7; z <= 7; z++) {
const d = Math.sqrt(x * x + z * z); if (d > 7) continue;
const depth = Math.round(3 - d * 0.3);
for (let y = 0; y >= -depth; y--) {
let id = (y === 0) ? 1 : (y === -depth ? 8 : 2);
set(cx + x, cy + y, cz + z, id);
}
}
// a little glowing shrine + tree
set(cx, cy + 1, cz, 7); set(cx, cy + 2, cz, 8);
tree(cx + 3, cy + 1, cz + 2);
// dangling crystals
set(cx - 4, cy - 4, cz, 8); set(cx + 4, cy - 5, cz + 3, 8); set(cx, cy - 6, cz - 4, 8);
}
function buildCrystalField() {
for (let i = 0; i < 14; i++) {
const a = Perlin.fbm(i * 3.1, 0, 2), b = Perlin.fbm(0, i * 2.7, 2);
const x = Math.round(a * R), z = Math.round(b * R);
if (Math.abs(x) < 11 && Math.abs(z) < 11) continue; // keep clear of temple
let top = 0; for (let y = MAXY; y >= 0; y--) if (get(x, y, z)) { top = y; break; }
if (top === 0) continue;
const hgt = 2 + (Math.abs(x + z) % 4);
for (let k = 1; k <= hgt; k++) set(x, top + k, z, 8);
set(x, top + hgt + 1, z, 7);
}
}
// ---------------------------------------------------------------------------
// Meshing — cull hidden faces, single merged geometry, atlas UVs
// ---------------------------------------------------------------------------
const FACES = [
{ dir: [ 1, 0, 0], use: 'side', corners: [[1,1,0],[1,0,0],[1,1,1],[1,0,1]] },
{ dir: [-1, 0, 0], use: 'side', corners: [[0,1,1],[0,0,1],[0,1,0],[0,0,0]] },
{ dir: [ 0, 1, 0], use: 'top', corners: [[0,1,1],[1,1,1],[0,1,0],[1,1,0]] },
{ dir: [ 0,-1, 0], use: 'bottom',corners: [[0,0,0],[1,0,0],[0,0,1],[1,0,1]] },
{ dir: [ 0, 0, 1], use: 'side', corners: [[1,1,1],[1,0,1],[0,1,1],[0,0,1]] },
{ dir: [ 0, 0,-1], use: 'side', corners: [[0,1,0],[0,0,0],[1,1,0],[1,0,0]] },
];
const UVQ = [[0,1],[0,0],[1,1],[1,0]];
let mesh = null;
function buildMesh(scene, material) {
const pos = [], norm = [], uv = [], idx = [];
let v = 0;
world.forEach((id, k) => {
const parts = k.split(','); const x = +parts[0], y = +parts[1], z = +parts[2];
const b = BLOCKS[id]; if (!b) return;
for (const f of FACES) {
const nx = x + f.dir[0], ny = y + f.dir[1], nz = z + f.dir[2];
if (get(nx, ny, nz)) continue; // neighbour solid -> skip face
const tile = b[f.use];
const u0 = tile / TILE_COUNT, u1 = (tile + 1) / TILE_COUNT;
for (let i = 0; i < 4; i++) {
const c = f.corners[i];
pos.push(x + c[0], y + c[1], z + c[2]);
norm.push(f.dir[0], f.dir[1], f.dir[2]);
uv.push(u0 + (u1 - u0) * UVQ[i][0], UVQ[i][1]);
}
idx.push(v, v + 1, v + 2, v + 2, v + 1, v + 3); v += 4;
}
});
if (mesh) { scene.remove(mesh); mesh.geometry.dispose(); }
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(norm, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
geo.setIndex(idx);
mesh = new THREE.Mesh(geo, material);
scene.add(mesh);
}
// ---------------------------------------------------------------------------
// Three.js scene
// ---------------------------------------------------------------------------
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
if ('outputColorSpace' in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x8fc7ee);
scene.fog = new THREE.Fog(0x9fd0f0, 40, 110);
const camera = new THREE.PerspectiveCamera(72, 1, 0.1, 400);
camera.rotation.order = 'YXZ';
scene.add(new THREE.HemisphereLight(0xeaf4ff, 0x6b5b3a, 0.95));
const sun = new THREE.DirectionalLight(0xfff2d0, 1.05);
sun.position.set(40, 80, 25); scene.add(sun);
scene.add(new THREE.AmbientLight(0x404858, 0.4));
const atlas = buildAtlas();
const material = new THREE.MeshLambertMaterial({ map: atlas });
// wire-frame highlight of targeted block
const hl = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(1.001, 1.001, 1.001)),
new THREE.LineBasicMaterial({ color: 0x111111 }));
hl.visible = false; scene.add(hl);
generate();
buildMesh(scene, material);
// ---------------------------------------------------------------------------
// Player + controls
// ---------------------------------------------------------------------------
const player = {
pos: new THREE.Vector3(0, 0, 0),
vel: new THREE.Vector3(),
yaw: Math.PI, pitch: -0.15,
onGround: false, fly: false,
w: 0.6, h: 1.8, eye: 1.62,
};
// spawn above terrain centre-ish
(function spawn() {
const sx = 0, sz = 16; let top = 0;
for (let y = MAXY; y >= 0; y--) if (get(sx, y, sz)) { top = y; break; }
player.pos.set(sx + 0.5, top + 3, sz + 0.5);
})();
const keys = {};
addEventListener('keydown', (e) => {
keys[e.code] = true;
if (e.code === 'KeyF') player.fly = !player.fly;
if (/^Digit[1-6]$/.test(e.code)) selectSlot(+e.code.slice(5) - 1);
if (e.code === 'Space') e.preventDefault();
});
addEventListener('keyup', (e) => { keys[e.code] = false; });
// hotbar UI
const hud = document.getElementById('hud');
let slot = 0;
HOTBAR.forEach((id, i) => {
const el = document.createElement('div'); el.className = 'slot' + (i === 0 ? ' active' : '');
el.innerHTML = '<div class="sw" style="background:' + BLOCKS[id].col + '"></div>' + (i + 1) + ' ' + BLOCKS[id].name;
hud.appendChild(el);
});
function selectSlot(i) {
slot = (i + HOTBAR.length) % HOTBAR.length;
[].forEach.call(hud.children, (c, j) => c.classList.toggle('active', j === slot));
}
addEventListener('wheel', (e) => { if (locked) selectSlot(slot + (e.deltaY > 0 ? 1 : -1)); }, { passive: true });
// pointer lock
let locked = false;
const startScreen = document.getElementById('start');
startScreen.addEventListener('click', () => canvas.requestPointerLock());
document.addEventListener('pointerlockchange', () => {
locked = document.pointerLockElement === canvas;
startScreen.style.display = locked ? 'none' : 'flex';
});
document.addEventListener('mousemove', (e) => {
if (!locked) return;
const s = 0.0024;
player.yaw -= e.movementX * s;
player.pitch -= e.movementY * s;
player.pitch = Math.max(-1.55, Math.min(1.55, player.pitch));
});
// break / place
canvas.addEventListener('mousedown', (e) => {
if (!locked) return;
const hit = raycast();
if (!hit) return;
if (e.button === 0) { // break
set(hit.bx, hit.by, hit.bz, 0);
buildMesh(scene, material);
} else if (e.button === 2) { // place
const px = hit.bx + hit.nx, py = hit.by + hit.ny, pz = hit.bz + hit.nz;
if (!wouldHitPlayer(px, py, pz) && get(px, py, pz) === 0) {
set(px, py, pz, HOTBAR[slot]);
buildMesh(scene, material);
}
}
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// DDA voxel raycast from camera
const _ray = new THREE.Vector3();
function raycast() {
camera.getWorldDirection(_ray);
let x = player.pos.x, y = player.pos.y + player.eye, z = player.pos.z;
const dx = _ray.x, dy = _ray.y, dz = _ray.z;
let ix = Math.floor(x), iy = Math.floor(y), iz = Math.floor(z);
const stepX = Math.sign(dx), stepY = Math.sign(dy), stepZ = Math.sign(dz);
const tDX = Math.abs(1 / dx), tDY = Math.abs(1 / dy), tDZ = Math.abs(1 / dz);
let tMX = (stepX > 0 ? (ix + 1 - x) : (x - ix)) * tDX;
let tMY = (stepY > 0 ? (iy + 1 - y) : (y - iy)) * tDY;
let tMZ = (stepZ > 0 ? (iz + 1 - z) : (z - iz)) * tDZ;
let nx = 0, ny = 0, nz = 0;
for (let i = 0; i < 120; i++) {
if (get(ix, iy, iz)) return { bx: ix, by: iy, bz: iz, nx: nx, ny: ny, nz: nz };
if (tMX < tMY && tMX < tMZ) { ix += stepX; tMX += tDX; nx = -stepX; ny = 0; nz = 0; }
else if (tMY < tMZ) { iy += stepY; tMY += tDY; nx = 0; ny = -stepY; nz = 0; }
else { iz += stepZ; tMZ += tDZ; nx = 0; ny = 0; nz = -stepZ; }
}
return null;
}
function wouldHitPlayer(x, y, z) {
const pminx = player.pos.x - player.w / 2, pmaxx = player.pos.x + player.w / 2;
const pminy = player.pos.y, pmaxy = player.pos.y + player.h;
const pminz = player.pos.z - player.w / 2, pmaxz = player.pos.z + player.w / 2;
return (x + 1 > pminx && x < pmaxx && y + 1 > pminy && y < pmaxy && z + 1 > pminz && z < pmaxz);
}
// ---------------------------------------------------------------------------
// Physics + collision (axis-separated AABB vs voxels)
// ---------------------------------------------------------------------------
function solidAt(x, y, z) { return get(Math.floor(x), Math.floor(y), Math.floor(z)) !== 0; }
function collides(p) {
const hw = player.w / 2;
const x0 = Math.floor(p.x - hw), x1 = Math.floor(p.x + hw);
const y0 = Math.floor(p.y), y1 = Math.floor(p.y + player.h);
const z0 = Math.floor(p.z - hw), z1 = Math.floor(p.z + hw);
for (let x = x0; x <= x1; x++) for (let y = y0; y <= y1; y++) for (let z = z0; z <= z1; z++)
if (get(x, y, z)) return true;
return false;
}
function physics(dt) {
const speed = (keys['ShiftLeft'] || keys['ShiftRight'] ? 9.5 : 5.2);
const fwd = new THREE.Vector3(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
const right = new THREE.Vector3(Math.cos(player.yaw), 0, -Math.sin(player.yaw));
const wish = new THREE.Vector3();
if (keys['KeyW']) wish.add(fwd);
if (keys['KeyS']) wish.sub(fwd);
if (keys['KeyD']) wish.add(right);
if (keys['KeyA']) wish.sub(right);
if (wish.lengthSq() > 0) wish.normalize().multiplyScalar(speed);
player.vel.x = wish.x; player.vel.z = wish.z;
if (player.fly) {
player.vel.y = 0;
if (keys['Space']) player.vel.y = speed;
if (keys['ShiftLeft'] || keys['ShiftRight']) player.vel.y = -speed;
} else {
player.vel.y -= 26 * dt; // gravity
if (keys['Space'] && player.onGround) { player.vel.y = 8.6; player.onGround = false; }
}
// integrate per axis with collision
const p = player.pos;
p.x += player.vel.x * dt; if (collides(p)) { p.x -= player.vel.x * dt; player.vel.x = 0; }
p.z += player.vel.z * dt; if (collides(p)) { p.z -= player.vel.z * dt; player.vel.z = 0; }
player.onGround = false;
p.y += player.vel.y * dt;
if (collides(p)) {
p.y -= player.vel.y * dt;
if (player.vel.y < 0) player.onGround = true;
player.vel.y = 0;
}
if (p.y < -20) { p.set(0.5, 40, 16.5); player.vel.set(0, 0, 0); } // fell off -> respawn
}
// ---------------------------------------------------------------------------
// Loop
// ---------------------------------------------------------------------------
function resize() {
const w = innerWidth, h = innerHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h; camera.updateProjectionMatrix();
}
addEventListener('resize', resize); resize();
let last = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000); last = now;
if (locked) physics(dt);
camera.position.set(player.pos.x, player.pos.y + player.eye, player.pos.z);
camera.rotation.set(player.pitch, player.yaw, 0);
const hit = raycast();
if (hit) { hl.visible = true; hl.position.set(hit.bx + 0.5, hit.by + 0.5, hit.bz + 0.5); }
else hl.visible = false;
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
@@ -0,0 +1,199 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600" role="img" aria-label="A lively pelican riding a bicycle along the seaside">
<defs>
<!-- Sky -->
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fdeecb"/>
<stop offset="0.45" stop-color="#bfe5f5"/>
<stop offset="1" stop-color="#7fc6e8"/>
</linearGradient>
<!-- Sea -->
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2e9bd0"/>
<stop offset="0.5" stop-color="#1f7fb8"/>
<stop offset="1" stop-color="#13567f"/>
</linearGradient>
<linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f3dca6"/>
<stop offset="1" stop-color="#e2c282"/>
</linearGradient>
<radialGradient id="sun" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#fff7df"/>
<stop offset="0.6" stop-color="#ffe7a0"/>
<stop offset="1" stop-color="#ffe7a0" stop-opacity="0"/>
</radialGradient>
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#dbe3ea"/>
</linearGradient>
<linearGradient id="bill" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#ffd14d"/>
<stop offset="1" stop-color="#f7a93b"/>
</linearGradient>
<linearGradient id="pouch" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffd97a"/>
<stop offset="1" stop-color="#f08a3c"/>
</linearGradient>
<linearGradient id="frame" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ff5a5f"/>
<stop offset="1" stop-color="#c8313a"/>
</linearGradient>
<filter id="soft" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3"/>
</filter>
</defs>
<!-- Background -->
<rect width="800" height="600" fill="url(#sky)"/>
<circle cx="640" cy="130" r="120" fill="url(#sun)"/>
<circle cx="640" cy="130" r="46" fill="#fff2c4"/>
<!-- Distant clouds -->
<g fill="#ffffff" opacity="0.85">
<ellipse cx="180" cy="110" rx="70" ry="22"/>
<ellipse cx="230" cy="95" rx="48" ry="18"/>
<ellipse cx="470" cy="80" rx="58" ry="18"/>
</g>
<!-- Sea -->
<rect y="330" width="800" height="120" fill="url(#sea)"/>
<!-- gentle waves -->
<g stroke="#bfeaff" stroke-width="3" fill="none" opacity="0.7" stroke-linecap="round">
<path d="M40 360 q20 -10 40 0 t40 0 t40 0"/>
<path d="M300 380 q22 -10 44 0 t44 0 t44 0"/>
<path d="M560 365 q20 -10 40 0 t40 0 t40 0"/>
</g>
<!-- shimmer near sun -->
<g fill="#cdeaff" opacity="0.6">
<ellipse cx="620" cy="400" rx="60" ry="6"/>
<ellipse cx="640" cy="420" rx="40" ry="4"/>
</g>
<!-- Beach / sand -->
<path d="M0 430 Q400 400 800 440 L800 600 L0 600 Z" fill="url(#sand)"/>
<!-- a few seashells / pebbles -->
<g opacity="0.5" fill="#caa86a">
<circle cx="120" cy="540" r="6"/>
<circle cx="700" cy="560" r="7"/>
<circle cx="500" cy="575" r="5"/>
</g>
<!-- distant sailboat -->
<g transform="translate(140 300)">
<path d="M0 30 L30 30 L24 44 L6 44 Z" fill="#ffffff"/>
<path d="M15 30 L15 0 L32 28 Z" fill="#ffffff"/>
<path d="M15 30 L15 6 L2 28 Z" fill="#f2f2f2"/>
</g>
<!-- contact shadow -->
<ellipse cx="400" cy="512" rx="190" ry="20" fill="#000000" opacity="0.18" filter="url(#soft)"/>
<!-- ====================== BICYCLE ====================== -->
<g stroke-linecap="round">
<!-- wheels -->
<g>
<circle cx="260" cy="470" r="78" fill="none" stroke="#222" stroke-width="9"/>
<circle cx="260" cy="470" r="68" fill="none" stroke="#444" stroke-width="2"/>
<circle cx="540" cy="470" r="78" fill="none" stroke="#222" stroke-width="9"/>
<circle cx="540" cy="470" r="68" fill="none" stroke="#444" stroke-width="2"/>
<!-- spokes -->
<g stroke="#c9ccd1" stroke-width="2.5">
<g transform="translate(260 470)">
<line x1="0" y1="-66" x2="0" y2="66"/>
<line x1="-66" y1="0" x2="66" y2="0"/>
<line x1="-47" y1="-47" x2="47" y2="47"/>
<line x1="-47" y1="47" x2="47" y2="-47"/>
</g>
<g transform="translate(540 470)">
<line x1="0" y1="-66" x2="0" y2="66"/>
<line x1="-66" y1="0" x2="66" y2="0"/>
<line x1="-47" y1="-47" x2="47" y2="47"/>
<line x1="-47" y1="47" x2="47" y2="-47"/>
</g>
</g>
<circle cx="260" cy="470" r="7" fill="#555"/>
<circle cx="540" cy="470" r="7" fill="#555"/>
</g>
<!-- frame -->
<g stroke="url(#frame)" stroke-width="11" fill="none">
<line x1="260" y1="470" x2="430" y2="470"/> <!-- chain stay -->
<line x1="430" y1="470" x2="540" y2="470"/> <!-- rear lower -->
<line x1="430" y1="470" x2="370" y2="360"/> <!-- seat tube -->
<line x1="370" y1="360" x2="260" y2="470"/> <!-- down-ish to rear hub -->
<line x1="370" y1="360" x2="470" y2="350"/> <!-- top tube -->
<line x1="470" y1="350" x2="540" y2="470"/> <!-- fork -->
</g>
<!-- handlebar stem -->
<line x1="470" y1="350" x2="500" y2="318" stroke="#333" stroke-width="9"/>
<path d="M500 318 q26 -6 30 14" stroke="#333" stroke-width="9" fill="none"/>
<!-- seat -->
<path d="M345 356 q25 -8 50 2 q-8 12 -25 12 q-18 0 -25 -14 Z" fill="#333"/>
<!-- pedals + crank -->
<circle cx="430" cy="470" r="13" fill="#666"/>
<line x1="430" y1="470" x2="408" y2="500" stroke="#333" stroke-width="7"/>
<rect x="392" y="498" width="26" height="9" rx="3" fill="#222"/>
<line x1="430" y1="470" x2="452" y2="440" stroke="#333" stroke-width="7"/>
<rect x="442" y="432" width="26" height="9" rx="3" fill="#222"/>
</g>
<!-- ====================== PELICAN ====================== -->
<g>
<!-- tail -->
<path d="M300 360 q-55 -8 -78 22 q40 6 70 -2 Z" fill="#cdd6df"/>
<!-- body -->
<path d="M300 360
q-30 -85 50 -110
q70 -22 110 30
q26 38 -2 78
q-30 40 -88 38
q-58 -2 -70 -36 Z" fill="url(#body)"/>
<!-- wing -->
<path d="M330 320 q60 -16 110 8 q-6 44 -52 56 q-44 8 -66 -18 q-6 -30 8 -46 Z"
fill="#c2ccd6"/>
<path d="M348 330 q40 -8 78 8" fill="none" stroke="#aab6c2" stroke-width="3"/>
<path d="M344 350 q40 -6 74 10" fill="none" stroke="#aab6c2" stroke-width="3"/>
<!-- leg to pedal -->
<line x1="360" y1="430" x2="408" y2="498" stroke="#f2a93b" stroke-width="9" stroke-linecap="round"/>
<line x1="430" y1="408" x2="452" y2="438" stroke="#f6b94f" stroke-width="9" stroke-linecap="round"/>
<path d="M404 498 l18 4 l-2 8 l-20 -4 Z" fill="#e8932f"/>
<!-- neck + head -->
<path d="M392 268 q8 -52 60 -64 q40 -8 60 14 q-6 26 -40 30 q-30 4 -40 30 Z"
fill="url(#body)"/>
<!-- head -->
<circle cx="512" cy="236" r="40" fill="url(#body)"/>
<!-- little crest -->
<path d="M498 202 q14 -16 30 -6 q-8 10 -22 14 Z" fill="#e7edf2"/>
<!-- eye -->
<circle cx="528" cy="228" r="8.5" fill="#1d2a33"/>
<circle cx="531" cy="225" r="3" fill="#ffffff"/>
<path d="M512 214 q18 -8 32 2" fill="none" stroke="#9fb0bd" stroke-width="2"/>
<!-- upper bill -->
<path d="M544 230 q70 -6 116 16 q-6 12 -22 12 q-50 -10 -96 -8 Z" fill="url(#bill)"/>
<!-- pouch (lower bill) -->
<path d="M544 244 q60 24 112 24 q14 -18 6 -34 q-46 12 -96 -6 q-14 6 -22 16 Z"
fill="url(#pouch)"/>
<path d="M556 258 q40 14 84 12" fill="none" stroke="#d9772c" stroke-width="2.5"/>
<!-- bill tip hook -->
<path d="M658 244 q10 2 8 12 q-8 0 -12 -6 Z" fill="#e0892f"/>
<!-- wing in front feather hint -->
<path d="M300 360 q-8 -20 8 -34" fill="none" stroke="#b7c2cd" stroke-width="3"/>
</g>
<!-- a swooping gull in the distance -->
<g stroke="#ffffff" stroke-width="3" fill="none" opacity="0.9">
<path d="M120 160 q14 -12 28 0 q14 -12 28 0"/>
</g>
<!-- motion lines for liveliness -->
<g stroke="#ffffff" stroke-width="3" opacity="0.6" stroke-linecap="round">
<line x1="150" y1="430" x2="195" y2="430"/>
<line x1="135" y1="455" x2="185" y2="455"/>
<line x1="150" y1="480" x2="195" y2="480"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB