Files
nagi-bench/outputs/gemini-3-5-flash/antigravity-high/mythos-craft.html
T

2737 lines
111 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MythicCraft - 3D Minecraft Clone</title>
<!-- Google Fonts for Retro and Modern look -->
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
<!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
:root {
--primary: #ffd700;
--primary-glow: rgba(255, 215, 0, 0.4);
--bg-glass: rgba(15, 15, 25, 0.7);
--border-glass: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #050510;
font-family: 'Outfit', sans-serif;
color: #ffffff;
}
#canvas-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* HUD Overlay */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
}
/* Crosshair */
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
transform: translate(-50%, -50%);
pointer-events: none;
mix-blend-mode: difference;
z-index: 11;
}
#crosshair::before, #crosshair::after {
content: '';
position: absolute;
background: white;
}
#crosshair::before {
top: 7px;
left: 0;
width: 16px;
height: 2px;
}
#crosshair::after {
top: 0;
left: 7px;
width: 2px;
height: 16px;
}
/* Top Bar: Stats and Info */
.hud-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.info-panel {
background: var(--bg-glass);
backdrop-filter: blur(8px);
border: 1px solid var(--border-glass);
border-radius: 12px;
padding: 12px 18px;
font-size: 14px;
line-height: 1.6;
pointer-events: auto;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}
.info-panel h2 {
margin: 0 0 6px 0;
font-size: 16px;
font-weight: 600;
color: var(--primary);
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-row {
display: flex;
justify-content: space-between;
gap: 20px;
}
.stat-val {
font-weight: 600;
color: #00ffcc;
}
/* Bottom Bar: Hotbar & Selected Block Info */
.hud-bottom {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 100%;
}
#block-name {
font-family: 'Press Start 2P', monospace;
font-size: 12px;
color: var(--primary);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
letter-spacing: 1px;
background: rgba(0, 0, 0, 0.5);
padding: 4px 12px;
border-radius: 6px;
}
.hotbar-container {
display: flex;
background: rgba(10, 10, 20, 0.8);
border: 2px solid #555;
border-radius: 4px;
padding: 2px;
pointer-events: auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.hotbar-slot {
width: 48px;
height: 48px;
border: 2px solid transparent;
margin: 1px;
position: relative;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
transition: all 0.15s ease;
}
.hotbar-slot:hover {
background: rgba(255, 255, 255, 0.15);
}
.hotbar-slot.active {
border-color: var(--primary);
background: rgba(255, 215, 0, 0.15);
box-shadow: inset 0 0 10px var(--primary-glow), 0 0 12px var(--primary-glow);
transform: scale(1.08);
z-index: 2;
}
.hotbar-slot canvas {
width: 38px;
height: 38px;
image-rendering: pixelated;
}
.slot-key {
position: absolute;
top: 2px;
left: 4px;
font-size: 9px;
font-weight: 800;
color: #aaa;
font-family: monospace;
}
/* Screen Overlays (Pause, Creative Inventory, Main Menu) */
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(5, 5, 15, 0.85);
backdrop-filter: blur(12px);
transition: opacity 0.3s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.menu-card {
background: var(--bg-glass);
border: 1px solid var(--border-glass);
border-radius: 24px;
padding: 40px;
width: 90%;
max-width: 550px;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
animation: floatIn 0.5s ease-out;
}
@keyframes floatIn {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
h1.game-title {
font-family: 'Press Start 2P', monospace;
font-size: 28px;
color: var(--primary);
text-shadow: 0 4px 0 #b39700, 0 8px 20px rgba(255, 215, 0, 0.3);
margin: 0 0 25px 0;
letter-spacing: -1px;
line-height: 1.4;
}
.instructions {
text-align: left;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
font-size: 14px;
}
.instructions h3 {
margin-top: 0;
color: #00ffcc;
font-weight: 600;
}
.control-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px 15px;
}
.key-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 2px 6px;
font-family: monospace;
font-size: 11px;
font-weight: bold;
color: var(--primary);
text-align: center;
min-width: 28px;
display: inline-block;
}
.play-btn {
background: linear-gradient(135deg, #ffd700, #ffaa00);
color: #050510;
border: none;
border-radius: 12px;
padding: 16px 40px;
font-family: 'Press Start 2P', monospace;
font-size: 14px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 8px 24px rgba(255, 170, 0, 0.4);
transition: all 0.2s;
}
.play-btn:hover {
transform: translateY(-2px);
box-shadow: 0 12px 30px rgba(255, 170, 0, 0.6);
}
.play-btn:active {
transform: translateY(1px);
}
/* Creative Inventory Overlay */
#creative-inventory {
z-index: 110;
}
.inventory-card {
background: rgba(15, 15, 25, 0.95);
border: 2px solid var(--primary);
border-radius: 16px;
padding: 25px;
width: 90%;
max-width: 480px;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.8), 0 0 20px var(--primary-glow);
}
.inventory-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
margin: 20px 0;
max-height: 250px;
overflow-y: auto;
padding-right: 5px;
}
.inventory-grid::-webkit-scrollbar {
width: 6px;
}
.inventory-grid::-webkit-scrollbar-thumb {
background: #444;
border-radius: 3px;
}
.inventory-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 10px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.inventory-item:hover {
background: rgba(255, 255, 255, 0.15);
border-color: var(--primary);
transform: translateY(-2px);
}
.inventory-item canvas {
width: 44px;
height: 44px;
image-rendering: pixelated;
}
.inventory-item-name {
font-size: 10px;
text-align: center;
font-weight: 600;
color: #ddd;
}
.close-inv-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 8px 20px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
margin-top: 10px;
}
.close-inv-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Biome Selection in main menu */
.biome-select {
margin: 15px 0 25px 0;
display: flex;
justify-content: center;
gap: 10px;
}
.biome-option {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s;
}
.biome-option:hover {
background: rgba(255, 255, 255, 0.1);
}
.biome-option.active {
border-color: var(--primary);
background: rgba(255, 215, 0, 0.15);
color: var(--primary);
}
#toast-notification {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-50px);
background: rgba(0, 255, 204, 0.2);
border: 1px solid #00ffcc;
color: #00ffcc;
padding: 10px 24px;
border-radius: 30px;
font-weight: 600;
z-index: 200;
pointer-events: none;
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
opacity: 0;
box-shadow: 0 0 15px rgba(0, 255, 204, 0.3);
}
#toast-notification.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
</style>
</head>
<body>
<!-- Canvas Container for Three.js -->
<div id="canvas-container"></div>
<!-- Crosshair -->
<div id="crosshair"></div>
<!-- HUD Overlay -->
<div id="hud">
<div class="hud-top">
<div class="info-panel">
<h2>MythicCraft</h2>
<div class="stat-row">
<div>Pos: <span id="pos-val" class="stat-val">0, 0, 0</span></div>
<div>FPS: <span id="fps-val" class="stat-val">60</span></div>
</div>
<div class="stat-row" style="margin-top: 4px;">
<div>Biome: <span id="biome-val" class="stat-val" style="color: #ffd700;">Mythic Valleys</span></div>
<div>Fly: <span id="fly-val" class="stat-val" style="color: #ff3366;">OFF</span></div>
</div>
</div>
<div class="info-panel" style="pointer-events: auto;">
<h2>Legendary Structures</h2>
<div style="font-size: 12px; color: #bbb;">
Find structures generated in the seed:<br>
• <span style="color: #ffffff; font-weight: 600;">Floating Temple of Aether</span> (Y: ~95)<br>
• <span style="color: #8a2be2; font-weight: 600;">Ancient Portal Ruins</span> (On Surface)<br>
• <span style="color: #00ffcc; font-weight: 600;">Yggdrasil World Tree</span> (On Surface)
</div>
</div>
</div>
<div class="hud-bottom">
<div id="block-name">Grass Block</div>
<div class="hotbar-container" id="hotbar">
<!-- Slots populated dynamically by JS -->
</div>
</div>
</div>
<!-- Notification Toast -->
<div id="toast-notification">Fly Mode Enabled</div>
<!-- Main Menu Overlay -->
<div class="overlay" id="main-menu">
<div class="menu-card">
<h1 class="game-title">MYTHIC CRAFT</h1>
<div class="instructions">
<h3>CONTROLS</h3>
<div class="control-grid">
<div><span class="key-btn">W</span><span class="key-btn">A</span><span class="key-btn">S</span><span class="key-btn">D</span></div>
<div>Move player</div>
<div><span class="key-btn">SPACE</span></div>
<div>Jump</div>
<div><span class="key-btn">L-SHIFT</span></div>
<div>Sprint / Fly Down</div>
<div><span class="key-btn">F</span></div>
<div>Toggle Fly Mode</div>
<div><span class="key-btn">E</span></div>
<div>Open Creative Inventory</div>
<div><span class="key-btn">1</span> - <span class="key-btn">9</span> / Scroll</div>
<div>Select active block slot</div>
<div><span class="key-btn">L-Click</span></div>
<div>Destroy block</div>
<div><span class="key-btn">R-Click</span></div>
<div>Place block</div>
</div>
</div>
<div style="margin-bottom: 8px; font-weight: 600; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: #888;">Select World Seed</div>
<div class="biome-select">
<div class="biome-option active" data-seed="valleys">Mythic Valleys</div>
<div class="biome-option" data-seed="islands">Aether Islands</div>
<div class="biome-option" data-seed="abyss">Crystal Abyss</div>
</div>
<button class="play-btn" id="start-btn">ENTER WORLD</button>
<div style="margin-top: 15px; font-size: 11px; color: #666;">Requires mouse pointer lock. Press ESC to pause.</div>
</div>
</div>
<!-- Pause Menu Overlay -->
<div class="overlay hidden" id="pause-menu">
<div class="menu-card">
<h1 class="game-title" style="font-size: 20px;">GAME PAUSED</h1>
<p style="margin-bottom: 25px; color: #aaa;">Click below or press Escape to resume your adventure.</p>
<button class="play-btn" id="resume-btn">RESUME GAME</button>
</div>
</div>
<!-- Creative Inventory Overlay -->
<div class="overlay hidden" id="creative-inventory">
<div class="inventory-card">
<h1 class="game-title" style="font-size: 18px; margin-bottom: 10px;">CREATIVE INVENTORY</h1>
<p style="font-size: 12px; color: #888; margin: 0 0 15px 0;">Click a block to place it in your active hotbar slot.</p>
<div class="inventory-grid" id="inventory-grid">
<!-- Items populated dynamically by JS -->
</div>
<div style="text-align: center;">
<button class="close-inv-btn" id="close-inv-btn">Close Inventory (E)</button>
</div>
</div>
</div>
<!-- GAME LOGIC SCRIPT -->
<script>
// --- 1. GAME DATA & CONSTANTS ---
const BLOCK_TYPES = {
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
COBBLESTONE: 4,
MOSSY_COBBLESTONE: 5,
WOOD: 6,
LEAVES: 7,
QUARTZ: 8,
GOLD: 9,
CRYSTAL_CYAN: 10,
CRYSTAL_PURPLE: 11,
PORTAL: 12,
WATER: 13,
GLASS: 14
};
const BLOCK_DETAILS = {
[BLOCK_TYPES.AIR]: { name: "Air", solid: false, transparent: true },
[BLOCK_TYPES.GRASS]: { name: "Grass Block", solid: true, transparent: false, color: '#558a2f' },
[BLOCK_TYPES.DIRT]: { name: "Dirt", solid: true, transparent: false, color: '#866043' },
[BLOCK_TYPES.STONE]: { name: "Stone", solid: true, transparent: false, color: '#808080' },
[BLOCK_TYPES.COBBLESTONE]: { name: "Cobblestone", solid: true, transparent: false, color: '#8a8a8a' },
[BLOCK_TYPES.MOSSY_COBBLESTONE]: { name: "Mossy Cobblestone", solid: true, transparent: false, color: '#6a8d52' },
[BLOCK_TYPES.WOOD]: { name: "Wood Trunk", solid: true, transparent: false, color: '#5c4033' },
[BLOCK_TYPES.LEAVES]: { name: "Magical Leaves", solid: true, transparent: true, color: '#2e5c1e' },
[BLOCK_TYPES.QUARTZ]: { name: "Quartz Brick", solid: true, transparent: false, color: '#e6e6fa' },
[BLOCK_TYPES.GOLD]: { name: "Block of Gold", solid: true, transparent: false, color: '#ffd700' },
[BLOCK_TYPES.CRYSTAL_CYAN]: { name: "Glowing Cyan Crystal", solid: true, transparent: false, color: '#00bfff' },
[BLOCK_TYPES.CRYSTAL_PURPLE]: { name: "Glowing Purple Crystal", solid: true, transparent: false, color: '#ba55d3' },
[BLOCK_TYPES.PORTAL]: { name: "Mystic Portal Energy", solid: false, transparent: true, color: '#8a2be2' },
[BLOCK_TYPES.WATER]: { name: "Luminous Water", solid: false, transparent: true, color: '#1e90ff' },
[BLOCK_TYPES.GLASS]: { name: "Crystal Pane (Glass)", solid: true, transparent: true, color: '#ffffff' }
};
// Current active hotbar slots (holds block IDs)
const hotbarSlots = [
BLOCK_TYPES.GRASS,
BLOCK_TYPES.DIRT,
BLOCK_TYPES.STONE,
BLOCK_TYPES.COBBLESTONE,
BLOCK_TYPES.WOOD,
BLOCK_TYPES.LEAVES,
BLOCK_TYPES.QUARTZ,
BLOCK_TYPES.GOLD,
BLOCK_TYPES.CRYSTAL_CYAN
];
let activeHotbarIndex = 0; // 0..8
// World parameters
let worldSeed = "valleys"; // "valleys", "islands", "abyss"
const CHUNK_SIZE = 16;
const WORLD_HEIGHT = 128;
const RENDER_DISTANCE = 4; // chunks around player
// --- 2. RETRO SOUND SYNTHESIZER (WEB AUDIO) ---
class AudioManager {
constructor() {
this.ctx = null;
this.musicPlaying = false;
}
init() {
if (this.ctx) return;
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.startAmbientMusic();
}
playBreak() {
if (!this.ctx) return;
const ctx = this.ctx;
const bufferSize = ctx.sampleRate * 0.08;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(350, ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(10, ctx.currentTime + 0.08);
const gain = ctx.createGain();
gain.gain.setValueAtTime(0.4, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
noise.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
noise.start();
}
playPlace() {
if (!this.ctx) return;
const ctx = this.ctx;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(160, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(90, ctx.currentTime + 0.06);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.06);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.06);
}
playStep() {
if (!this.ctx) return;
const ctx = this.ctx;
const bufferSize = ctx.sampleRate * 0.04;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(150, ctx.currentTime);
const gain = ctx.createGain();
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.04);
noise.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
noise.start();
}
playJump() {
if (!this.ctx) return;
const ctx = this.ctx;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(180, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(320, ctx.currentTime + 0.12);
gain.gain.setValueAtTime(0.2, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.12);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.12);
}
startAmbientMusic() {
if (this.musicPlaying) return;
this.musicPlaying = true;
const ctx = this.ctx;
const playChord = () => {
if (!this.musicPlaying) return;
// Relaxing modal pentatonic pads (C major pentatonic / G Lydian)
const chords = [
[130.81, 164.81, 196.00, 246.94], // Cmaj7
[146.83, 196.00, 220.00, 293.66], // Gsus4 / D
[164.81, 196.00, 246.94, 329.63], // Em7
[174.61, 220.00, 261.63, 349.23] // Fmaj7
];
const chord = chords[Math.floor(Math.random() * chords.length)];
const gain = ctx.createGain();
gain.gain.setValueAtTime(0, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.02, ctx.currentTime + 4.0); // Slow attack
gain.gain.setValueAtTime(0.02, ctx.currentTime + 8.0);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 12.0); // Release
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(500, ctx.currentTime);
filter.frequency.linearRampToValueAtTime(700, ctx.currentTime + 6.0);
filter.frequency.linearRampToValueAtTime(400, ctx.currentTime + 12.0);
gain.connect(filter);
filter.connect(ctx.destination);
const oscillators = [];
chord.forEach(freq => {
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(freq, ctx.currentTime);
osc.detune.setValueAtTime(Math.random() * 8 - 4, ctx.currentTime); // Chorus effect
osc.connect(gain);
osc.start();
osc.stop(ctx.currentTime + 12.0);
oscillators.push(osc);
});
setTimeout(playChord, 14000); // Trigger next chord with small silence gap
};
playChord();
}
}
const audio = new AudioManager();
// --- 3. DYNAMIC TEXTURE ATLAS GENERATOR ---
// Generates a square 4x4 grid of 64x64 textures, total 256x256 pixels
function generateTextureAtlas() {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
const texSize = 64; // size of each block texture in the atlas
function drawNoise(tx, ty, r, g, b, varRange) {
const ox = tx * texSize;
const oy = ty * texSize;
for (let px = 0; px < texSize; px++) {
for (let py = 0; py < texSize; py++) {
const vr = (Math.random() - 0.5) * varRange;
ctx.fillStyle = `rgb(${Math.min(255, Math.max(0, r + vr))}, ${Math.min(255, Math.max(0, g + vr))}, ${Math.min(255, Math.max(0, b + vr))})`;
ctx.fillRect(ox + px, oy + py, 1, 1);
}
}
}
// --- 0: Grass Top (0, 3 in UV grid) ---
drawNoise(0, 3, 85, 138, 47, 30);
// Add darker green pixel patterns
ctx.fillStyle = 'rgba(51, 105, 30, 0.4)';
for (let i = 0; i < 200; i++) {
ctx.fillRect(0 * texSize + Math.random() * texSize, 3 * texSize + Math.random() * texSize, 2, 2);
}
// --- 1: Grass Side (1, 3) ---
// Base dirt
drawNoise(1, 3, 134, 96, 67, 25);
// Top grass cover
ctx.fillStyle = 'rgb(85, 138, 47)';
ctx.fillRect(1 * texSize, 3 * texSize, texSize, 16);
// Add pixelated grass drips
for (let col = 0; col < texSize; col++) {
const dripHeight = 16 + Math.floor(Math.sin(col * 0.8) * 6) + (Math.random() < 0.3 ? 4 : 0);
ctx.fillStyle = `rgb(${75 + Math.random()*20}, ${120 + Math.random()*20}, 40)`;
ctx.fillRect(1 * texSize + col, 3 * texSize, 1, dripHeight);
}
// Add some dirt spots under grass
ctx.fillStyle = 'rgba(87, 61, 38, 0.5)';
for (let i = 0; i < 150; i++) {
ctx.fillRect(1 * texSize + Math.random() * texSize, 3 * texSize + 20 + Math.random() * (texSize - 20), 2, 2);
}
// --- 2: Dirt (2, 3) ---
drawNoise(2, 3, 134, 96, 67, 25);
ctx.fillStyle = 'rgba(87, 61, 38, 0.6)';
for (let i = 0; i < 200; i++) {
ctx.fillRect(2 * texSize + Math.random() * texSize, 3 * texSize + Math.random() * texSize, 2, 2);
}
// --- 3: Stone (3, 3) ---
drawNoise(3, 3, 128, 128, 128, 20);
// Cracks and stone shading
ctx.fillStyle = 'rgba(80, 80, 80, 0.5)';
for (let i = 0; i < 150; i++) {
ctx.fillRect(3 * texSize + Math.random() * texSize, 3 * texSize + Math.random() * texSize, Math.random() * 4 + 1, 1);
}
ctx.fillStyle = 'rgba(200, 200, 200, 0.3)';
for (let i = 0; i < 100; i++) {
ctx.fillRect(3 * texSize + Math.random() * texSize, 3 * texSize + Math.random() * texSize, 2, 2);
}
// --- 4: Cobblestone (0, 2) ---
drawNoise(0, 2, 110, 110, 110, 15);
// Cobble outline drawing
ctx.fillStyle = '#555555';
for (let i = 0; i < 16; i++) {
const lx = Math.random() * texSize;
const ly = Math.random() * texSize;
ctx.fillRect(0 * texSize + lx, 2 * texSize + ly, 12, 2);
ctx.fillRect(0 * texSize + lx, 2 * texSize + ly, 2, 8);
}
ctx.fillStyle = '#dedede';
for (let i = 0; i < 20; i++) {
ctx.fillRect(0 * texSize + Math.random() * (texSize - 4), 2 * texSize + Math.random() * (texSize - 4), 4, 1);
}
// --- 5: Mossy Cobblestone (1, 2) ---
// Copy Cobble first
ctx.drawImage(canvas, 0 * texSize, 2 * texSize, texSize, texSize, 1 * texSize, 2 * texSize, texSize, texSize);
// Overlap green moss veins
ctx.fillStyle = 'rgba(61, 106, 32, 0.75)';
for (let i = 0; i < 12; i++) {
const mx = Math.random() * (texSize - 12);
const my = Math.random() * (texSize - 12);
ctx.fillRect(1 * texSize + mx, 2 * texSize + my, 12, 3);
ctx.fillRect(1 * texSize + mx + 4, 2 * texSize + my - 4, 3, 10);
}
// --- 6: Wood Trunk Side (2, 2) ---
drawNoise(2, 2, 92, 64, 51, 15);
// Bark vertical ridges
ctx.fillStyle = 'rgb(61, 43, 31)';
for (let i = 0; i < 10; i++) {
const rx = Math.floor(Math.random() * texSize);
ctx.fillRect(2 * texSize + rx, 2 * texSize, 3, texSize);
}
ctx.fillStyle = 'rgba(125, 95, 75, 0.4)';
for (let i = 0; i < 6; i++) {
const rx = Math.floor(Math.random() * texSize);
ctx.fillRect(2 * texSize + rx, 2 * texSize, 2, texSize);
}
// --- 7: Wood Trunk Top (3, 2) ---
drawNoise(3, 2, 215, 181, 135, 15);
// Concentric rings
ctx.fillStyle = 'rgba(145, 105, 75, 0.8)';
ctx.strokeStyle = 'rgba(145, 105, 75, 0.8)';
ctx.lineWidth = 4;
const ringCenter = 3 * texSize + texSize/2;
for (let r = 8; r < texSize/2; r += 10) {
ctx.beginPath();
ctx.arc(ringCenter, 2 * texSize + texSize/2, r, 0, Math.PI * 2);
ctx.stroke();
}
// --- 8: Leaves (0, 1) ---
// Fully transparent background
ctx.clearRect(0 * texSize, 1 * texSize, texSize, texSize);
ctx.fillStyle = 'rgb(46, 92, 30)';
ctx.fillRect(0 * texSize, 1 * texSize, texSize, texSize);
// Cutout pixels
ctx.fillStyle = '#224814';
for (let i = 0; i < 250; i++) {
ctx.fillRect(0 * texSize + Math.random() * texSize, 1 * texSize + Math.random() * texSize, 2, 2);
}
// Leaf transparency holes
for (let i = 0; i < 120; i++) {
ctx.clearRect(0 * texSize + Math.floor(Math.random() * (texSize - 4)), 1 * texSize + Math.floor(Math.random() * (texSize - 4)), 3, 3);
}
ctx.fillStyle = 'rgb(69, 140, 44)';
for (let i = 0; i < 180; i++) {
ctx.fillRect(0 * texSize + Math.random() * texSize, 1 * texSize + Math.random() * texSize, 2, 1);
}
// --- 9: Quartz Brick (1, 1) ---
drawNoise(1, 1, 235, 235, 245, 10);
// Brick tiles
ctx.fillStyle = '#b0c4de'; // slate shadows
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
// Draw grid lines
for (let y = 0; y <= texSize; y += 16) {
ctx.beginPath();
ctx.moveTo(1 * texSize, 1 * texSize + y);
ctx.lineTo(2 * texSize, 1 * texSize + y);
ctx.stroke();
}
for (let x = 0; x <= texSize; x += 16) {
ctx.beginPath();
ctx.moveTo(1 * texSize + x, 1 * texSize);
ctx.lineTo(1 * texSize + x, 2 * texSize);
ctx.stroke();
}
// --- 10: Gold Block (2, 1) ---
drawNoise(2, 1, 255, 215, 0, 15);
// Outer golden borders
ctx.strokeStyle = '#b8860b';
ctx.lineWidth = 3;
ctx.strokeRect(2 * texSize + 2, 1 * texSize + 2, texSize - 4, texSize - 4);
// Diagonal specular shines
ctx.fillStyle = 'rgba(255, 253, 208, 0.7)';
ctx.beginPath();
ctx.moveTo(2 * texSize + 8, 1 * texSize + 8);
ctx.lineTo(2 * texSize + 20, 1 * texSize + 8);
ctx.lineTo(2 * texSize + 8, 1 * texSize + 20);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(2 * texSize + texSize - 8, 1 * texSize + texSize - 8);
ctx.lineTo(2 * texSize + texSize - 20, 1 * texSize + texSize - 8);
ctx.lineTo(2 * texSize + texSize - 8, 1 * texSize + texSize - 20);
ctx.closePath();
ctx.fill();
// --- 11: Glowing Crystal Cyan (3, 1) ---
drawNoise(3, 1, 0, 191, 255, 20);
// Glowing magical pattern
ctx.fillStyle = 'rgba(224, 255, 255, 0.9)';
ctx.beginPath();
ctx.arc(3 * texSize + texSize/2, 1 * texSize + texSize/2, 14, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(3 * texSize + texSize/2, 1 * texSize + texSize/2, 7, 0, Math.PI * 2);
ctx.fill();
// Outer outline
ctx.strokeStyle = '#008b8b';
ctx.lineWidth = 4;
ctx.strokeRect(3 * texSize + 1, 1 * texSize + 1, texSize - 2, texSize - 2);
// --- 12: Glowing Crystal Purple (0, 0) ---
drawNoise(0, 0, 186, 85, 211, 20);
ctx.fillStyle = 'rgba(255, 182, 193, 0.9)';
ctx.beginPath();
ctx.arc(0 * texSize + texSize/2, 0 * texSize + texSize/2, 14, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(0 * texSize + texSize/2, 0 * texSize + texSize/2, 7, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#8b008b';
ctx.lineWidth = 4;
ctx.strokeRect(0 * texSize + 1, 0 * texSize + 1, texSize - 2, texSize - 2);
// --- 13: Portal Energy (1, 0) ---
drawNoise(1, 0, 25, 0, 50, 10);
// Purple space-swirls
ctx.fillStyle = '#8a2be2';
for (let i = 0; i < 40; i++) {
ctx.fillRect(1 * texSize + Math.random() * (texSize - 8), Math.random() * (texSize - 8), Math.random() * 8 + 4, Math.random() * 4 + 2);
}
ctx.fillStyle = '#da70d6';
for (let i = 0; i < 20; i++) {
ctx.fillRect(1 * texSize + Math.random() * (texSize - 4), Math.random() * (texSize - 4), 4, 2);
}
// --- 14: Luminous Water (2, 0) ---
ctx.clearRect(2 * texSize, 0 * texSize, texSize, texSize);
ctx.fillStyle = 'rgba(30, 144, 255, 0.6)';
ctx.fillRect(2 * texSize, 0 * texSize, texSize, texSize);
// Wave ripples
ctx.fillStyle = 'rgba(0, 191, 255, 0.8)';
for (let i = 0; i < 15; i++) {
const wx = Math.random() * texSize;
const wy = Math.random() * texSize;
ctx.fillRect(2 * texSize + wx, wy, 16, 2);
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
for (let i = 0; i < 10; i++) {
ctx.fillRect(2 * texSize + Math.random() * texSize, Math.random() * texSize, 4, 1);
}
// --- 15: Glass (3, 0) ---
ctx.clearRect(3 * texSize, 0 * texSize, texSize, texSize);
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
ctx.fillRect(3 * texSize, 0 * texSize, texSize, texSize);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
ctx.strokeRect(3 * texSize + 1, 0 * texSize + 1, texSize - 2, texSize - 2);
// White highlight streak
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.beginPath();
ctx.moveTo(3 * texSize + 10, 10);
ctx.lineTo(3 * texSize + texSize - 10, texSize - 10);
ctx.lineTo(3 * texSize + texSize - 14, texSize - 10);
ctx.lineTo(3 * texSize + 6, 10);
ctx.closePath();
ctx.fill();
// Return texture
const texture = new THREE.CanvasTexture(canvas);
texture.flipY = false; // fix: tiles are drawn with top-origin rows matching ATLAS_COORDS, but default flipY=true mirrored the rows (grass sampled the purple crystal/portal tiles, alpha holes made solid blocks see-through)
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
return texture;
}
// --- 4. 3D IMPROVED PERLIN NOISE CLASS ---
class ImprovedNoise {
constructor() {
const p = [151,160,137,91,90,15,
131,13,201,95,96,53,194,233, 7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,189,143,244,235,186,60,182,185,166,245,160,1,244,140,124,48,
115,224,141,184,229,29,11,248,187,47,152,22,12,184,81,34,190,130,22,238,206,
72,176,137,215,30,219,31,75,9,141,123,220,167,250,58,193,121,80,208,167,0,
139,230,166,188,189,87,244,151,200,97,180,252,115,181,124,212,150,9,249,30,
84,189,223,154,75,86,244,121,193,14,150,16,145,129,56,221,110,26,88,127,129,
162,120,240,37,135,30,0,188,143,142,217,80,52,59,22,252,220,90,250,113,16,
203,116,35,11,32,95,33,128,121,252,192,207,151,12,95,236,220,224,193,150,
16,38,175,70,239,171,85,97,229,70,210,21,242,78,203,127,136,144,47,85,190,
2,182,29,148,241,120,137,86,238,110,181,81,77,236,137,21,224,162,219,10,252,
188,242,120,247,228,89,9,201,223,16,250,211,10,30,229,70,12,166,68,12,24,
129,236,54,196,167,125,234,251,224,162,222,86,120,41,89,148,18,131,21,189,
250,42,91,52,59,0,26,252,252,121,248,40,111,224,9,112,111,229];
this.permutation = new Array(512);
for (let i = 0; i < 256; i++) {
this.permutation[i] = p[i];
this.permutation[256 + i] = p[i];
}
}
fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
lerp(t, a, b) {
return a + t * (b - a);
}
grad(hash, x, y, z) {
const h = hash & 15;
const u = h < 8 ? x : y;
const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise(x, y, z) {
const floorX = Math.floor(x);
const floorY = Math.floor(y);
const floorZ = Math.floor(z);
const X = floorX & 255;
const Y = floorY & 255;
const Z = floorZ & 255;
x -= floorX;
y -= floorY;
z -= floorZ;
const xMinus1 = x - 1;
const yMinus1 = y - 1;
const zMinus1 = z - 1;
const u = this.fade(x);
const v = this.fade(y);
const w = this.fade(z);
const A = this.permutation[X] + Y;
const AA = this.permutation[A] + Z;
const AB = this.permutation[A + 1] + Z;
const B = this.permutation[X + 1] + Y;
const BA = this.permutation[B] + Z;
const BB = this.permutation[B + 1] + Z;
return this.lerp(w,
this.lerp(v,
this.lerp(u, this.grad(this.permutation[AA], x, y, z),
this.grad(this.permutation[BA], xMinus1, y, z)),
this.lerp(u, this.grad(this.permutation[AB], x, yMinus1, z),
this.grad(this.permutation[BB], xMinus1, yMinus1, z))
),
this.lerp(v,
this.lerp(u, this.grad(this.permutation[AA + 1], x, y, zMinus1),
this.grad(this.permutation[BA + 1], xMinus1, y, zMinus1)),
this.lerp(u, this.grad(this.permutation[AB + 1], x, yMinus1, zMinus1),
this.grad(this.permutation[BB + 1], xMinus1, yMinus1, zMinus1))
)
);
}
}
const perlin = new ImprovedNoise();
// --- 5. GLOBAL VOXEL DATABASE & PROCEDURAL ENGINE ---
const chunkVoxels = new Map(); // key: "cx,cz", value: Uint8Array(16 * 128 * 16)
const terrainGenerated = new Set(); // key: "cx,cz"
// Helper to check if a block type is transparent
function isTransparent(type) {
if (type === BLOCK_TYPES.AIR) return true;
return BLOCK_DETAILS[type]?.transparent || false;
}
// Base terrain height formula using multi-layered noise
function getTerrainHeight(wx, wz) {
let height = 30; // base floor
if (worldSeed === "valleys") {
// Large sweeping hills
const n1 = perlin.noise(wx * 0.005, 0, wz * 0.005) * 25;
// Minor rocky detail
const n2 = perlin.noise(wx * 0.04, 0, wz * 0.04) * 5;
height += n1 + n2;
} else if (worldSeed === "islands") {
// Flying islands pattern - we use noise to generate huge floating gaps
// Base ground is very low, peaks are high
const baseNoise = perlin.noise(wx * 0.01, 0, wz * 0.01);
if (baseNoise > 0.15) {
height += 40 + perlin.noise(wx * 0.03, 0, wz * 0.03) * 15;
} else {
height += baseNoise * 10 - 10; // low ocean/void floor
}
} else if (worldSeed === "abyss") {
// Dark valleys and caverns
const canyon = Math.abs(perlin.noise(wx * 0.015, 0, wz * 0.015));
height += (canyon < 0.2 ? 5 : 45 + perlin.noise(wx * 0.05, 0, wz * 0.05) * 8);
}
return Math.floor(height);
}
function getVoxel(x, y, z) {
if (y < 0 || y >= WORLD_HEIGHT) return BLOCK_TYPES.AIR;
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const key = `${cx},${cz}`;
let arr = chunkVoxels.get(key);
if (!arr) {
// If chunk is not initialized in world, auto-generate it
arr = generateChunkVoxels(cx, cz);
}
const ox = (x % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE;
const oz = (z % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE;
return arr[(ox * WORLD_HEIGHT + y) * CHUNK_SIZE + oz];
}
// Set voxel raw (instantly populating and creating chunk if needed)
function setVoxelRaw(x, y, z, type, triggerRebuild = true) {
if (y < 0 || y >= WORLD_HEIGHT) return;
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const key = `${cx},${cz}`;
let arr = chunkVoxels.get(key);
if (!arr) {
arr = new Uint8Array(CHUNK_SIZE * WORLD_HEIGHT * CHUNK_SIZE);
chunkVoxels.set(key, arr);
}
const ox = (x % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE;
const oz = (z % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE;
arr[(ox * WORLD_HEIGHT + y) * CHUNK_SIZE + oz] = type;
if (triggerRebuild) {
dirtyChunks.add(key);
// Also trigger rebuild of adjacent chunks if block is on border (for face culling updates)
if (ox === 0) dirtyChunks.add(`${cx - 1},${cz}`);
if (ox === CHUNK_SIZE - 1) dirtyChunks.add(`${cx + 1},${cz}`);
if (oz === 0) dirtyChunks.add(`${cx},${cz - 1}`);
if (oz === CHUNK_SIZE - 1) dirtyChunks.add(`${cx},${cz + 1}`);
}
}
// Set voxel with player-facing interface (handles sound & visual updates)
function setVoxel(x, y, z, type) {
setVoxelRaw(x, y, z, type, true);
}
// --- 5B. MYTHOLOGICAL STRUCTURE GENERATION ---
function trySpawnStructure(cx, cz) {
// Use deterministic random based on chunk coords
const hash = Math.sin(cx * 12.9898 + cz * 78.233) * 43758.5453;
const rand = hash - Math.floor(hash);
const centerX = cx * CHUNK_SIZE + 8;
const centerZ = cz * CHUNK_SIZE + 8;
const surfY = getTerrainHeight(centerX, centerZ);
if (worldSeed === "islands") {
// High probability of floating temples in islands mode
if (rand < 0.02) {
spawnAetherTemple(centerX, 90, centerZ);
} else if (rand < 0.05 && surfY > 20) {
spawnWorldTree(centerX, surfY, centerZ);
}
} else if (worldSeed === "abyss") {
// Portal ruins and caverns spawn more frequently in Abyss
if (rand < 0.035 && surfY > 15) {
spawnAncientRuins(centerX, surfY, centerZ);
} else if (rand < 0.05 && surfY > 15) {
spawnWorldTree(centerX, surfY, centerZ);
}
} else {
// Normal Valleys Mode
if (rand < 0.008) {
spawnAetherTemple(centerX, 95, centerZ);
} else if (rand < 0.022 && surfY > 20) {
spawnAncientRuins(centerX, surfY, centerZ);
} else if (rand < 0.05 && surfY > 20) {
spawnWorldTree(centerX, surfY, centerZ);
}
}
}
function spawnAetherTemple(centerX, yBase, centerZ) {
const radius = 6;
// Platform
for (let dx = -radius; dx <= radius; dx++) {
for (let dz = -radius; dz <= radius; dz++) {
const dist = Math.sqrt(dx*dx + dz*dz);
if (dist <= radius) {
const x = centerX + dx;
const z = centerZ + dz;
setVoxelRaw(x, yBase, z, BLOCK_TYPES.QUARTZ, false);
// Gold edge
if (dist > radius - 1.2) {
setVoxelRaw(x, yBase + 1, z, BLOCK_TYPES.GOLD, false);
}
}
}
}
// Columns (4 corners of platform)
const offsets = [-4, 4];
for (let ox of offsets) {
for (let oz of offsets) {
const px = centerX + ox;
const pz = centerZ + oz;
for (let h = 1; h <= 6; h++) {
setVoxelRaw(px, yBase + h, pz, BLOCK_TYPES.QUARTZ, false);
}
setVoxelRaw(px, yBase + 7, pz, BLOCK_TYPES.GOLD, false);
}
}
// Roof
for (let dx = -5; dx <= 5; dx++) {
for (let dz = -5; dz <= 5; dz++) {
const x = centerX + dx;
const z = centerZ + dz;
setVoxelRaw(x, yBase + 8, z, BLOCK_TYPES.QUARTZ, false);
if (Math.abs(dx) === 5 || Math.abs(dz) === 5) {
setVoxelRaw(x, yBase + 8, z, BLOCK_TYPES.GOLD, false);
}
}
}
// Core in center
setVoxelRaw(centerX, yBase + 1, centerZ, BLOCK_TYPES.GOLD, false);
setVoxelRaw(centerX, yBase + 2, centerZ, BLOCK_TYPES.CRYSTAL_CYAN, false);
setVoxelRaw(centerX, yBase + 3, centerZ, BLOCK_TYPES.CRYSTAL_CYAN, false);
setVoxelRaw(centerX, yBase + 4, centerZ, BLOCK_TYPES.GOLD, false);
// Waterfalls falling into the void
setVoxelRaw(centerX + 3, yBase, centerZ, BLOCK_TYPES.WATER, false);
setVoxelRaw(centerX - 3, yBase, centerZ, BLOCK_TYPES.WATER, false);
setVoxelRaw(centerX, yBase, centerZ + 3, BLOCK_TYPES.WATER, false);
setVoxelRaw(centerX, yBase, centerZ - 3, BLOCK_TYPES.WATER, false);
}
function spawnAncientRuins(centerX, yBase, centerZ) {
const radius = 5;
// Dilapidated Floor
for (let dx = -radius; dx <= radius; dx++) {
for (let dz = -radius; dz <= radius; dz++) {
const x = centerX + dx;
const z = centerZ + dz;
if (Math.random() < 0.7) {
const type = Math.random() < 0.4 ? BLOCK_TYPES.MOSSY_COBBLESTONE : BLOCK_TYPES.COBBLESTONE;
setVoxelRaw(x, yBase - 1, z, type, false);
}
}
}
// Pillars
const pillarPos = [[-3, -3], [3, -3], [-3, 3], [3, 3]];
for (let pos of pillarPos) {
const px = centerX + pos[0];
const pz = centerZ + pos[1];
const height = Math.floor(Math.random() * 4) + 2;
for (let h = 0; h < height; h++) {
const type = Math.random() < 0.5 ? BLOCK_TYPES.MOSSY_COBBLESTONE : BLOCK_TYPES.COBBLESTONE;
setVoxelRaw(px, yBase + h, pz, type, false);
}
}
// Portal Frame (facing X-axis, aligned along Z-axis)
const px = centerX;
for (let dy = 0; dy < 5; dy++) {
for (let dz = -1; dz <= 2; dz++) {
const isBorder = (dy === 0 || dy === 4 || dz === -1 || dz === 2);
const wx = px;
const wy = yBase + dy;
const wz = centerZ + dz;
if (isBorder) {
setVoxelRaw(wx, wy, wz, BLOCK_TYPES.CRYSTAL_PURPLE, false);
} else {
setVoxelRaw(wx, wy, wz, BLOCK_TYPES.PORTAL, false);
}
}
}
}
function spawnWorldTree(centerX, yBase, centerZ) {
const trunkHeight = Math.floor(Math.random() * 8) + 14;
// 2x2 Trunk
for (let dy = 0; dy < trunkHeight; dy++) {
setVoxelRaw(centerX, yBase + dy, centerZ, BLOCK_TYPES.WOOD, false);
setVoxelRaw(centerX + 1, yBase + dy, centerZ, BLOCK_TYPES.WOOD, false);
setVoxelRaw(centerX, yBase + dy, centerZ + 1, BLOCK_TYPES.WOOD, false);
setVoxelRaw(centerX + 1, yBase + dy, centerZ + 1, BLOCK_TYPES.WOOD, false);
}
// Leaves canopy
const canopyRadius = 6;
const canopyCenterY = yBase + trunkHeight - 3;
for (let dx = -canopyRadius; dx <= canopyRadius + 1; dx++) {
for (let dy = -canopyRadius; dy <= canopyRadius; dy++) {
for (let dz = -canopyRadius; dz <= canopyRadius + 1; dz++) {
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist <= canopyRadius) {
const lx = centerX + dx;
const ly = canopyCenterY + dy;
const lz = centerZ + dz;
// Check if air
if (getVoxel(lx, ly, lz) === BLOCK_TYPES.AIR) {
// Spawn glowing cyan crystal fruits inside leaf canopy
if (Math.random() < 0.03) {
setVoxelRaw(lx, ly, lz, BLOCK_TYPES.CRYSTAL_CYAN, false);
} else {
setVoxelRaw(lx, ly, lz, BLOCK_TYPES.LEAVES, false);
}
}
}
}
}
}
}
// --- 5C. CHUNK VOXEL GENERATOR ---
function generateChunkVoxels(cx, cz) {
const key = `${cx},${cz}`;
let arr = chunkVoxels.get(key);
if (!arr) {
arr = new Uint8Array(CHUNK_SIZE * WORLD_HEIGHT * CHUNK_SIZE);
chunkVoxels.set(key, arr);
}
if (!terrainGenerated.has(key)) {
terrainGenerated.add(key);
// Populate terrain
for (let ox = 0; ox < CHUNK_SIZE; ox++) {
for (let oz = 0; oz < CHUNK_SIZE; oz++) {
const wx = cx * CHUNK_SIZE + ox;
const wz = cz * CHUNK_SIZE + oz;
const height = getTerrainHeight(wx, wz);
for (let y = 0; y < WORLD_HEIGHT; y++) {
const idx = (ox * WORLD_HEIGHT + y) * CHUNK_SIZE + oz;
// Skip if pre-populated by structure blocks
if (arr[idx] !== BLOCK_TYPES.AIR) continue;
let type = BLOCK_TYPES.AIR;
if (y <= height) {
if (y === height) {
type = BLOCK_TYPES.GRASS;
} else if (y > height - 4) {
type = BLOCK_TYPES.DIRT;
} else {
type = BLOCK_TYPES.STONE;
}
}
// Cavern Caves & Crystal cavern decoration using 3D Perlin Noise
if (type !== BLOCK_TYPES.AIR && y < 35) {
const caveNoise = perlin.noise(wx * 0.06, y * 0.12, wz * 0.06);
if (caveNoise > 0.42) {
type = BLOCK_TYPES.AIR;
}
}
// Deep cave crystal decoration on cave floors/ceilings
if (type === BLOCK_TYPES.AIR && y < 22 && y > 2) {
const floorBlock = (y > 0) ? arr[(ox * WORLD_HEIGHT + (y - 1)) * CHUNK_SIZE + oz] : BLOCK_TYPES.AIR;
if (floorBlock === BLOCK_TYPES.STONE && Math.random() < 0.015) {
type = Math.random() < 0.5 ? BLOCK_TYPES.CRYSTAL_CYAN : BLOCK_TYPES.CRYSTAL_PURPLE;
}
}
// Generate water level for sea (sea level = 20)
if (type === BLOCK_TYPES.AIR && y <= 20) {
type = BLOCK_TYPES.WATER;
}
arr[idx] = type;
}
}
}
// Trigger structure seeding centered in this chunk
trySpawnStructure(cx, cz);
}
return arr;
}
// --- 6. OPTIMIZED VOXEL MESH BUILDER (FACE CULLING) ---
// Voxel UV mapping in our 4x4 atlas grid
const UV_EPSILON = 0.0008;
const ATLAS_COORDS = {
[BLOCK_TYPES.GRASS]: {
top: [0, 3], bottom: [2, 3], side: [1, 3]
},
[BLOCK_TYPES.DIRT]: {
all: [2, 3]
},
[BLOCK_TYPES.STONE]: {
all: [3, 3]
},
[BLOCK_TYPES.COBBLESTONE]: {
all: [0, 2]
},
[BLOCK_TYPES.MOSSY_COBBLESTONE]: {
all: [1, 2]
},
[BLOCK_TYPES.WOOD]: {
top: [3, 2], bottom: [3, 2], side: [2, 2]
},
[BLOCK_TYPES.LEAVES]: {
all: [0, 1]
},
[BLOCK_TYPES.QUARTZ]: {
all: [1, 1]
},
[BLOCK_TYPES.GOLD]: {
all: [2, 1]
},
[BLOCK_TYPES.CRYSTAL_CYAN]: {
all: [3, 1]
},
[BLOCK_TYPES.CRYSTAL_PURPLE]: {
all: [0, 0]
},
[BLOCK_TYPES.PORTAL]: {
all: [1, 0]
},
[BLOCK_TYPES.WATER]: {
all: [2, 0]
},
[BLOCK_TYPES.GLASS]: {
all: [3, 0]
}
};
function getUVForFace(blockType, faceName) {
const atlasPos = ATLAS_COORDS[blockType];
let coords = atlasPos?.all || atlasPos?.[faceName] || [0, 0];
// Calculate UV min/max with epsilon padding
const uMin = coords[0] * 0.25 + UV_EPSILON;
const uMax = (coords[0] + 1) * 0.25 - UV_EPSILON;
const vMin = coords[1] * 0.25 + UV_EPSILON;
const vMax = (coords[1] + 1) * 0.25 - UV_EPSILON;
return { uMin, uMax, vMin, vMax };
}
// Compiles visual mesh data for a single chunk
function buildChunkGeometry(cx, cz) {
const arr = chunkVoxels.get(`${cx},${cz}`);
if (!arr) return null;
// Arrays for geometry buffers
const opaque = { positions: [], normals: [], uvs: [], indices: [], vertCount: 0 };
const transparent = { positions: [], normals: [], uvs: [], indices: [], vertCount: 0 };
// Face definition helper
function addFace(buffers, x, y, z, nx, ny, nz, uvs) {
const positions = buffers.positions;
const normals = buffers.normals;
const uvsArray = buffers.uvs;
const indices = buffers.indices;
const vc = buffers.vertCount;
// Push 4 vertices per face based on normals
if (nx === -1) { // Left Face (-X)
positions.push(x, y, z, x, y, z+1, x, y+1, z+1, x, y+1, z);
} else if (nx === 1) { // Right Face (+X)
positions.push(x+1, y, z+1, x+1, y, z, x+1, y+1, z, x+1, y+1, z+1);
} else if (ny === -1) { // Bottom Face (-Y)
positions.push(x, y, z, x+1, y, z, x+1, y, z+1, x, y, z+1);
} else if (ny === 1) { // Top Face (+Y)
positions.push(x, y+1, z+1, x+1, y+1, z+1, x+1, y+1, z, x, y+1, z);
} else if (nz === -1) { // Back Face (-Z)
positions.push(x+1, y, z, x, y, z, x, y+1, z, x+1, y+1, z);
} else if (nz === 1) { // Front Face (+Z)
positions.push(x, y, z+1, x+1, y, z+1, x+1, y+1, z+1, x, y+1, z+1);
}
// Push Normals 4 times
for (let i = 0; i < 4; i++) {
normals.push(nx, ny, nz);
}
// Push UVs
uvsArray.push(uvs.uMin, uvs.vMin, uvs.uMax, uvs.vMin, uvs.uMax, uvs.vMax, uvs.uMin, uvs.vMax);
// Push Indices (2 triangles)
indices.push(vc, vc+1, vc+2, vc, vc+2, vc+3);
buffers.vertCount += 4;
}
// Loop through chunk voxels
for (let ox = 0; ox < CHUNK_SIZE; ox++) {
for (let y = 0; y < WORLD_HEIGHT; y++) {
for (let oz = 0; oz < CHUNK_SIZE; oz++) {
const idx = (ox * WORLD_HEIGHT + y) * CHUNK_SIZE + oz;
const blockType = arr[idx];
if (blockType === BLOCK_TYPES.AIR) continue;
const wx = cx * CHUNK_SIZE + ox;
const wz = cz * CHUNK_SIZE + oz;
const isWater = (blockType === BLOCK_TYPES.WATER);
const destBuffers = isWater ? transparent : opaque;
// Check 6 directions
const neighbors = [
{ nx: -1, ny: 0, nz: 0, face: 'side' }, // Left
{ nx: 1, ny: 0, nz: 0, face: 'side' }, // Right
{ nx: 0, ny: -1, nz: 0, face: 'bottom' }, // Bottom
{ nx: 0, ny: 1, nz: 0, face: 'top' }, // Top
{ nx: 0, ny: 0, nz: -1, face: 'side' }, // Back
{ nx: 0, ny: 0, nz: 1, face: 'side' } // Front
];
for (let n of neighbors) {
const nType = getVoxel(wx + n.nx, y + n.ny, wz + n.nz);
// Face culling conditions:
// 1. Draw if neighbor is air
// 2. Draw solid next to transparent
// 3. Draw different transparents next to each other (e.g. water next to glass)
let draw = false;
if (nType === BLOCK_TYPES.AIR) {
draw = true;
} else if (isTransparent(nType) && !isTransparent(blockType)) {
draw = true;
} else if (isTransparent(nType) && blockType !== nType) {
draw = true;
}
if (draw) {
const uvs = getUVForFace(blockType, n.face);
addFace(destBuffers, wx, y, wz, n.nx, n.ny, n.nz, uvs);
}
}
}
}
}
return { opaque, transparent };
}
// --- 7. GAME ENGINE CLASS ---
class Game {
constructor() {
this.container = document.getElementById('canvas-container');
this.scene = null;
this.camera = null;
this.renderer = null;
// Lights
this.sunLight = null;
this.moonLight = null;
this.ambientLight = null;
this.hemisphereLight = null;
// Timing & Game State
this.clock = new THREE.Clock();
this.keys = {};
this.loadedChunks = new Map(); // key: "cx,cz", value: { opaqueMesh, transMesh }
this.timeOfDay = 0.35; // 0..1 (0.35 is bright afternoon)
this.isPaused = true;
// Player Physics & Spectator
this.player = {
x: 0, y: 40, z: 0,
vx: 0, vy: 0, vz: 0,
speed: 7.0,
sprintMultiplier: 1.6,
jumpForce: 10.0,
onGround: false,
creativeFly: false
};
this.gravity = -26.0;
this.friction = 8.0; // horizontal deceleration
// Visual FX & Particles
this.particles = [];
this.maxParticles = 500;
this.particlePoints = null;
this.particlePositions = new Float32Array(this.maxParticles * 3);
this.particleColors = new Float32Array(this.maxParticles * 3);
this.ambientDust = null;
// Selection Outline
this.selectionBox = null;
this.targetedVoxel = null; // {x, y, z, nx, ny, nz}
// FPS display
this.frameCount = 0;
this.fpsTimer = 0;
}
init() {
// Scene & Camera setup
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color('#7ec0ee');
this.scene.fog = new THREE.FogExp2('#d0e0f0', 0.012);
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.rotation.order = 'YXZ'; // Perfect FPS look
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.container.appendChild(this.renderer.domElement);
// Load Shared Texture Atlas
this.textureAtlas = generateTextureAtlas();
// Material Setup
this.opaqueMaterial = new THREE.MeshStandardMaterial({
map: this.textureAtlas,
roughness: 0.85,
metalness: 0.1,
alphaTest: 0.5,
side: THREE.DoubleSide
});
this.transparentMaterial = new THREE.MeshStandardMaterial({
map: this.textureAtlas,
roughness: 0.1,
metalness: 0.15,
transparent: true,
opacity: 0.65,
depthWrite: false,
side: THREE.DoubleSide
});
// Spawn selection highlight box
const geom = new THREE.BoxGeometry(1.002, 1.002, 1.002);
const edges = new THREE.EdgesGeometry(geom);
const lineMat = new THREE.LineBasicMaterial({ color: 0x00ffcc, linewidth: 2 });
this.selectionBox = new THREE.LineSegments(edges, lineMat);
this.selectionBox.visible = false;
this.scene.add(this.selectionBox);
// Setup Particle System
this.setupParticleSystem();
// Setup ambient magical glowing dust motes
this.setupAmbientDust();
// Lights
this.setupLights();
// Initialize Spawn Position
this.resetPlayerSpawn();
// Register event listeners
this.registerEvents();
// Start loop
this.clock.getDelta(); // flush clock
this.animate();
}
resetPlayerSpawn() {
// Generate spawn chunks
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
generateChunkVoxels(dx, dz);
}
}
// Find highest block at 0, 0
let spawnY = 30;
for (let y = WORLD_HEIGHT - 1; y >= 0; y--) {
const block = getVoxel(0, y, 0);
if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.WATER) {
spawnY = y + 1;
break;
}
}
this.player.x = 0.5;
this.player.y = spawnY + 2.0;
this.player.z = 0.5;
this.player.vx = 0;
this.player.vy = 0;
this.player.vz = 0;
this.camera.position.set(this.player.x, this.player.y + 1.6, this.player.z);
this.camera.rotation.set(0, 0, 0);
}
setupLights() {
// Hemisphere Sky light
this.hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444455, 0.4);
this.scene.add(this.hemisphereLight);
// Ambient glow
this.ambientLight = new THREE.AmbientLight(0x0a0a1a, 0.2);
this.scene.add(this.ambientLight);
// Main Sun Light (casting shadows)
this.sunLight = new THREE.DirectionalLight(0xfff1cc, 0.8);
this.sunLight.castShadow = true;
this.sunLight.shadow.mapSize.width = 1024;
this.sunLight.shadow.mapSize.height = 1024;
this.sunLight.shadow.camera.near = 0.5;
this.sunLight.shadow.camera.far = 200;
const d = 50;
this.sunLight.shadow.camera.left = -d;
this.sunLight.shadow.camera.right = d;
this.sunLight.shadow.camera.top = d;
this.sunLight.shadow.camera.bottom = -d;
this.scene.add(this.sunLight);
// Moon Light
this.moonLight = new THREE.DirectionalLight(0xaaccff, 0.3);
this.moonLight.castShadow = false;
this.scene.add(this.moonLight);
}
setupParticleSystem() {
const geom = new THREE.BufferGeometry();
geom.setAttribute('position', new THREE.BufferAttribute(this.particlePositions, 3));
geom.setAttribute('color', new THREE.BufferAttribute(this.particleColors, 3));
const mat = new THREE.PointsMaterial({
size: 0.18,
vertexColors: true,
transparent: true,
opacity: 0.85
});
this.particlePoints = new THREE.Points(geom, mat);
this.scene.add(this.particlePoints);
}
setupAmbientDust() {
// Add magical sparkles in the sky
const count = 300;
const geom = new THREE.BufferGeometry();
const pos = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
pos[i*3] = (Math.random() - 0.5) * 120;
pos[i*3+1] = 20 + Math.random() * 80;
pos[i*3+2] = (Math.random() - 0.5) * 120;
}
geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
const mat = new THREE.PointsMaterial({
size: 0.25,
color: 0xffe680,
transparent: true,
opacity: 0.5,
blending: THREE.AdditiveBlending
});
this.ambientDust = new THREE.Points(geom, mat);
this.scene.add(this.ambientDust);
}
registerEvents() {
// Key state mapping
window.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
this.handleKeyDownImmediate(e);
});
window.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
// Window Resize
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
// Pointer Lock listeners
const startBtn = document.getElementById('start-btn');
const resumeBtn = document.getElementById('resume-btn');
const mainMenu = document.getElementById('main-menu');
const pauseMenu = document.getElementById('pause-menu');
const requestLock = () => {
audio.init();
document.body.requestPointerLock();
};
startBtn.addEventListener('click', requestLock);
resumeBtn.addEventListener('click', requestLock);
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === document.body) {
this.isPaused = false;
mainMenu.classList.add('hidden');
pauseMenu.classList.add('hidden');
} else {
this.isPaused = true;
// Only show pause menu if creative inventory is not open
if (document.getElementById('creative-inventory').classList.contains('hidden')) {
pauseMenu.classList.remove('hidden');
}
}
});
// Mouse camera rotation (Only when locked)
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement === document.body && !this.isPaused) {
this.camera.rotation.y -= e.movementX * 0.0022;
this.camera.rotation.x -= e.movementY * 0.0022;
// Limit pitch to -85deg to +85deg
const limit = Math.PI / 2 - 0.06;
this.camera.rotation.x = Math.max(-limit, Math.min(limit, this.camera.rotation.x));
}
});
// Block Interaction (Click handlers)
window.addEventListener('mousedown', (e) => {
if (document.pointerLockElement !== document.body || this.isPaused) return;
if (e.button === 0) { // Left Click - Break
this.breakBlock();
} else if (e.button === 2) { // Right Click - Place
this.placeBlock();
}
});
// Scroll Wheel to select slots
window.addEventListener('wheel', (e) => {
if (this.isPaused) return;
if (e.deltaY > 0) {
activeHotbarIndex = (activeHotbarIndex + 1) % 9;
} else {
activeHotbarIndex = (activeHotbarIndex - 1 + 9) % 9;
}
this.updateHotbarUI();
});
}
handleKeyDownImmediate(e) {
// E - Creative Inventory Toggle
if (e.code === 'KeyE') {
this.toggleCreativeInventory();
}
// F - Creative Fly Mode
if (e.code === 'KeyF') {
if (document.pointerLockElement === document.body) {
this.player.creativeFly = !this.player.creativeFly;
this.player.vy = 0; // stop falling instantly
this.showToast(this.player.creativeFly ? "Creative Fly Mode: ON" : "Creative Fly Mode: OFF");
this.updateHUDStats();
}
}
// 1..9 slot selection
if (e.code.startsWith('Digit') && e.code.length === 6) {
const num = parseInt(e.code[5]);
if (num >= 1 && num <= 9) {
activeHotbarIndex = num - 1;
this.updateHotbarUI();
}
}
}
showToast(message) {
const toast = document.getElementById('toast-notification');
toast.textContent = message;
toast.classList.add('show');
clearTimeout(this.toastTimeout);
this.toastTimeout = setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
toggleCreativeInventory() {
const inv = document.getElementById('creative-inventory');
if (inv.classList.contains('hidden')) {
inv.classList.remove('hidden');
document.exitPointerLock();
} else {
inv.classList.add('hidden');
document.body.requestPointerLock();
}
}
// --- 8. BLOCK RAYCASTER ---
// Simulates Raymarching along the grid to check intersections
performRaycast() {
const dir = new THREE.Vector3();
this.camera.getWorldDirection(dir);
const start = this.camera.position.clone();
const step = 0.04;
const maxReach = 6.5;
const current = start.clone();
const prev = start.clone();
for (let dist = 0; dist < maxReach; dist += step) {
current.addScaledVector(dir, step);
const bx = Math.floor(current.x);
const by = Math.floor(current.y);
const bz = Math.floor(current.z);
const voxel = getVoxel(bx, by, bz);
if (voxel !== BLOCK_TYPES.AIR) {
// We hit a block!
const pbx = Math.floor(prev.x);
const pby = Math.floor(prev.y);
const pbz = Math.floor(prev.z);
// Calculate normal
let nx = pbx - bx;
let ny = pby - by;
let nz = pbz - bz;
// Clamp to face unit normal bounds
nx = Math.max(-1, Math.min(1, nx));
ny = Math.max(-1, Math.min(1, ny));
nz = Math.max(-1, Math.min(1, nz));
return {
hit: true,
x: bx, y: by, z: bz,
nx, ny, nz,
voxel
};
}
prev.copy(current);
}
return { hit: false };
}
breakBlock() {
const hitInfo = this.performRaycast();
if (hitInfo.hit) {
// Get voxel type for audio / particle feedback
const blockType = hitInfo.voxel;
// Don't break if it is Aether barrier or portal in some cases (unless we want to)
setVoxel(hitInfo.x, hitInfo.y, hitInfo.z, BLOCK_TYPES.AIR);
// Trigger sound synth
audio.playBreak();
// Spawn breaking particles
this.spawnBreakParticles(hitInfo.x, hitInfo.y, hitInfo.z, blockType);
}
}
placeBlock() {
const hitInfo = this.performRaycast();
if (hitInfo.hit) {
const targetX = hitInfo.x + hitInfo.nx;
const targetY = hitInfo.y + hitInfo.ny;
const targetZ = hitInfo.z + hitInfo.nz;
if (targetY < 0 || targetY >= WORLD_HEIGHT) return;
// Check if block overlaps player AABB
const blockMinX = targetX;
const blockMaxX = targetX + 1;
const blockMinY = targetY;
const blockMaxY = targetY + 1;
const blockMinZ = targetZ;
const blockMaxZ = targetZ + 1;
const pMinX = this.player.x - 0.3;
const pMaxX = this.player.x + 0.3;
const pMinY = this.player.y;
const pMaxY = this.player.y + 1.8;
const pMinZ = this.player.z - 0.3;
const pMaxZ = this.player.z + 0.3;
const blockType = hotbarSlots[activeHotbarIndex];
const isVoxelSolid = BLOCK_DETAILS[blockType].solid;
// If placing a solid block, verify it doesn't clip the player's body
if (isVoxelSolid) {
const overlapX = pMaxX > blockMinX && pMinX < blockMaxX;
const overlapY = pMaxY > blockMinY && pMinY < blockMaxY;
const overlapZ = pMaxZ > blockMinZ && pMinZ < blockMaxZ;
if (overlapX && overlapY && overlapZ) {
return; // collides, cancel placing
}
}
// Place the block
setVoxel(targetX, targetY, targetZ, blockType);
// Trigger sound
audio.playPlace();
}
}
spawnBreakParticles(bx, by, bz, blockType) {
const center = new THREE.Vector3(bx + 0.5, by + 0.5, bz + 0.5);
const blockColorHex = BLOCK_DETAILS[blockType]?.color || '#ffffff';
const col = new THREE.Color(blockColorHex);
const particleCount = 18;
for (let i = 0; i < particleCount; i++) {
this.particles.push({
pos: center.clone().add(new THREE.Vector3(
(Math.random() - 0.5) * 0.6,
(Math.random() - 0.5) * 0.6,
(Math.random() - 0.5) * 0.6
)),
vel: new THREE.Vector3(
(Math.random() - 0.5) * 3.5,
(Math.random() * 4.0) + 1.5, // blow upwards
(Math.random() - 0.5) * 3.5
),
color: col.clone().multiplyScalar(0.7 + Math.random() * 0.4), // slight shading noise
age: 0.0,
maxAge: 0.4 + Math.random() * 0.4
});
}
}
updateParticles(dt) {
let activeCount = 0;
const posAttr = this.particlePoints.geometry.getAttribute('position');
const colorAttr = this.particlePoints.geometry.getAttribute('color');
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.age += dt;
if (p.age >= p.maxAge) {
this.particles.splice(i, 1);
continue;
}
// Physics: apply gravity and velocity
p.vel.y -= 18.0 * dt;
p.pos.addScaledVector(p.vel, dt);
if (activeCount < this.maxParticles) {
const idx = activeCount * 3;
this.particlePositions[idx] = p.pos.x;
this.particlePositions[idx+1] = p.pos.y;
this.particlePositions[idx+2] = p.pos.z;
this.particleColors[idx] = p.color.r;
this.particleColors[idx+1] = p.color.g;
this.particleColors[idx+2] = p.color.b;
activeCount++;
}
}
posAttr.needsUpdate = true;
colorAttr.needsUpdate = true;
this.particlePoints.geometry.setDrawRange(0, activeCount);
}
// --- 9. PLAYER MOVEMENT & COLLISION RESOLUTION ---
checkAABBCollisions(px, py, pz) {
const xMin = px - 0.3;
const xMax = px + 0.3;
const yMin = py;
const yMax = py + 1.8;
const zMin = pz - 0.3;
const zMax = pz + 0.3;
const bxMin = Math.floor(xMin);
const bxMax = Math.floor(xMax);
const byMin = Math.floor(yMin);
const byMax = Math.floor(yMax);
const bzMin = Math.floor(zMin);
const bzMax = Math.floor(zMax);
for (let x = bxMin; x <= bxMax; x++) {
for (let y = byMin; y <= byMax; y++) {
for (let z = bzMin; z <= bzMax; z++) {
const voxel = getVoxel(x, y, z);
const details = BLOCK_DETAILS[voxel];
if (voxel !== BLOCK_TYPES.AIR && details.solid) {
// Intersection check
const blockMinX = x;
const blockMaxX = x + 1;
const blockMinY = y;
const blockMaxY = y + 1;
const blockMinZ = z;
const blockMaxZ = z + 1;
if (xMax > blockMinX && xMin < blockMaxX &&
yMax > blockMinY && yMin < blockMaxY &&
zMax > blockMinZ && zMin < blockMaxZ) {
return true;
}
}
}
}
}
return false;
}
updatePlayerPhysics(dt) {
// If spectator / fly mode, bypass gravity & standard collisions
if (this.player.creativeFly) {
this.updateSpectatorFly(dt);
return;
}
// Apply Gravity
this.player.vy += this.gravity * dt;
// Terminal velocity
if (this.player.vy < -40.0) this.player.vy = -40.0;
// Move direction relative to camera looking angle
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion);
forward.y = 0;
forward.normalize();
right.y = 0;
right.normalize();
const moveDir = new THREE.Vector3();
if (this.keys['KeyW']) moveDir.add(forward);
if (this.keys['KeyS']) moveDir.sub(forward);
if (this.keys['KeyD']) moveDir.add(right);
if (this.keys['KeyA']) moveDir.sub(right);
moveDir.normalize();
// Sprinting check
const isSprinting = this.keys['ShiftLeft'] || this.keys['ShiftRight'];
const speedMultiplier = isSprinting ? this.player.sprintMultiplier : 1.0;
const targetSpeed = this.player.speed * speedMultiplier;
if (moveDir.lengthSq() > 0) {
this.player.vx = moveDir.x * targetSpeed;
this.player.vz = moveDir.z * targetSpeed;
// Play rhythmic step sounds when moving on ground
if (this.player.onGround && !this.isPaused) {
if (!this.stepTimer) this.stepTimer = 0;
this.stepTimer += dt;
const stepInterval = isSprinting ? 0.28 : 0.38;
if (this.stepTimer >= stepInterval) {
audio.playStep();
this.stepTimer = 0;
}
}
} else {
// Decelerate smoothly using friction
this.player.vx -= this.player.vx * this.friction * dt;
this.player.vz -= this.player.vz * this.friction * dt;
if (Math.abs(this.player.vx) < 0.1) this.player.vx = 0;
if (Math.abs(this.player.vz) < 0.1) this.player.vz = 0;
}
// Jumping
if (this.keys['Space'] && this.player.onGround) {
this.player.vy = this.player.jumpForce;
this.player.onGround = false;
audio.playJump();
}
// Displacement vectors
const dx = this.player.vx * dt;
const dy = this.player.vy * dt;
const dz = this.player.vz * dt;
// --- Axis-by-Axis Collision Resolution ---
// 1. Y Axis
this.player.y += dy;
this.player.onGround = false;
if (this.checkAABBCollisions(this.player.x, this.player.y, this.player.z)) {
if (dy < 0) {
// Landed on block top
this.player.y = Math.floor(this.player.y) + 1.0;
// Stepping out
while (this.checkAABBCollisions(this.player.x, this.player.y, this.player.z)) {
this.player.y += 1.0;
}
this.player.onGround = true;
} else if (dy > 0) {
// Hit ceiling
this.player.y = Math.floor(this.player.y + 1.8) - 1.801;
}
this.player.vy = 0;
}
// 2. X Axis
this.player.x += dx;
if (this.checkAABBCollisions(this.player.x, this.player.y, this.player.z)) {
if (dx < 0) {
this.player.x = Math.floor(this.player.x - 0.3) + 1.301;
} else {
this.player.x = Math.floor(this.player.x + 0.3) - 0.301;
}
this.player.vx = 0;
}
// 3. Z Axis
this.player.z += dz;
if (this.checkAABBCollisions(this.player.x, this.player.y, this.player.z)) {
if (dz < 0) {
this.player.z = Math.floor(this.player.z - 0.3) + 1.301;
} else {
this.player.z = Math.floor(this.player.z + 0.3) - 0.301;
}
this.player.vz = 0;
}
// Fall into the void fallback (re-teleport player to highest peak)
if (this.player.y < -20) {
this.resetPlayerSpawn();
this.showToast("Returned to Spawn Peak");
}
// Update Camera Position to Head Level
this.camera.position.set(this.player.x, this.player.y + 1.6, this.player.z);
}
updateSpectatorFly(dt) {
// Free flying spectrum controls (no gravity, no collisions)
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion);
const moveDir = new THREE.Vector3();
if (this.keys['KeyW']) moveDir.add(forward);
if (this.keys['KeyS']) moveDir.sub(forward);
if (this.keys['KeyD']) moveDir.add(right);
if (this.keys['KeyA']) moveDir.sub(right);
// Fly Up/Down using Space/Shift
if (this.keys['Space']) moveDir.y += 1.0;
if (this.keys['ShiftLeft'] || this.keys['ShiftRight']) moveDir.y -= 1.0;
moveDir.normalize();
const flySpeed = 22.0; // fly faster than walking
this.player.x += moveDir.x * flySpeed * dt;
this.player.y += moveDir.y * flySpeed * dt;
this.player.z += moveDir.z * flySpeed * dt;
this.camera.position.set(this.player.x, this.player.y + 1.6, this.player.z);
}
// --- 10. DYNAMIC CHUNK MANAGER (INFINITE WORLD GENERATION) ---
updateChunksAroundPlayer() {
const pcx = Math.floor(this.player.x / CHUNK_SIZE);
const pcz = Math.floor(this.player.z / CHUNK_SIZE);
const activeKeys = new Set();
// Load chunks in render distance circle
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
const cx = pcx + dx;
const cz = pcz + dz;
const key = `${cx},${cz}`;
activeKeys.add(key);
if (!this.loadedChunks.has(key)) {
this.loadChunk(cx, cz);
}
}
}
// Cull chunks outside render distance + 1
for (let [key, chunkData] of this.loadedChunks.entries()) {
if (!activeKeys.has(key)) {
// Extract cx, cz
const [cx, cz] = key.split(',').map(Number);
const dist = Math.max(Math.abs(cx - pcx), Math.abs(cz - pcz));
if (dist > RENDER_DISTANCE + 1) {
this.unloadChunk(key, chunkData);
}
}
}
}
loadChunk(cx, cz) {
// Generates voxels if not present
generateChunkVoxels(cx, cz);
// Build geometry and meshes
const key = `${cx},${cz}`;
const geoms = buildChunkGeometry(cx, cz);
if (!geoms) return;
const chunkMeshes = {};
// 1. Opaque Mesh
if (geoms.opaque.positions.length > 0) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(geoms.opaque.positions, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(geoms.opaque.normals, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(geoms.opaque.uvs, 2));
geometry.setIndex(geoms.opaque.indices);
geometry.computeBoundingBox();
const mesh = new THREE.Mesh(geometry, this.opaqueMaterial);
mesh.receiveShadow = true;
mesh.castShadow = true;
this.scene.add(mesh);
chunkMeshes.opaqueMesh = mesh;
}
// 2. Transparent Mesh (Water)
if (geoms.transparent.positions.length > 0) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(geoms.transparent.positions, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(geoms.transparent.normals, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(geoms.transparent.uvs, 2));
geometry.setIndex(geoms.transparent.indices);
const mesh = new THREE.Mesh(geometry, this.transparentMaterial);
this.scene.add(mesh);
chunkMeshes.transMesh = mesh;
}
this.loadedChunks.set(key, chunkMeshes);
}
rebuildChunk(cx, cz) {
const key = `${cx},${cz}`;
const chunkData = this.loadedChunks.get(key);
if (!chunkData) return; // not loaded, skip
// Unload first (dispose graphics memory)
this.unloadChunk(key, chunkData);
// Reload compiled mesh
this.loadChunk(cx, cz);
}
unloadChunk(key, chunkData) {
if (chunkData.opaqueMesh) {
this.scene.remove(chunkData.opaqueMesh);
chunkData.opaqueMesh.geometry.dispose();
}
if (chunkData.transMesh) {
this.scene.remove(chunkData.transMesh);
chunkData.transMesh.geometry.dispose();
}
this.loadedChunks.delete(key);
}
// --- 11. ATMOSPHERIC TIME-OF-DAY CYCLE ---
updateDayNightCycle(dt) {
// Increment time of day (Full cycle takes 180 seconds)
this.timeOfDay += dt * (1 / 180);
if (this.timeOfDay > 1.0) this.timeOfDay -= 1.0;
const angle = this.timeOfDay * Math.PI * 2;
const sunHeight = Math.sin(angle);
// Define colors
const skyDay = new THREE.Color('#7ec0ee');
const skySunset = new THREE.Color('#fd5e53');
const skyNight = new THREE.Color('#030308');
const fogDay = new THREE.Color('#d0e0f0');
const fogSunset = new THREE.Color('#e07060');
const fogNight = new THREE.Color('#030308');
let currentSky, currentFog, intensity;
// Interpolate colors based on sun height
if (sunHeight > 0.15) { // Day
currentSky = skyDay;
currentFog = fogDay;
intensity = sunHeight * 0.8;
} else if (sunHeight > -0.15) { // Dawn / Dusk Transition
const factor = (sunHeight + 0.15) / 0.3;
currentSky = skySunset.clone().lerp(skyDay, factor);
currentFog = fogSunset.clone().lerp(fogDay, factor);
intensity = factor * 0.4;
} else { // Night
const factor = Math.min(1, Math.abs(sunHeight + 0.15) / 0.35);
currentSky = skySunset.clone().lerp(skyNight, factor);
currentFog = fogSunset.clone().lerp(fogNight, factor);
intensity = 0.05;
}
// Update scene visual params
this.scene.background = currentSky;
this.scene.fog.color = currentFog;
// Rotate Sun position
const distance = 120;
const sunX = Math.cos(angle) * distance;
const sunY = Math.sin(angle) * distance;
this.sunLight.position.set(this.player.x + sunX, sunY, this.player.z + 10);
this.sunLight.intensity = intensity;
// Position Moon opposite to Sun
this.moonLight.position.set(this.player.x - sunX, -sunY, this.player.z - 10);
this.moonLight.intensity = sunHeight < 0 ? 0.3 : 0.0;
}
// --- 12. RUNTIME INTERACTIVE UI CONTROLS ---
setupUI() {
// Populate hotbar slots with isometric canvas renderings
const hotbarContainer = document.getElementById('hotbar');
hotbarContainer.innerHTML = '';
hotbarSlots.forEach((type, index) => {
const slot = document.createElement('div');
slot.className = `hotbar-slot ${index === activeHotbarIndex ? 'active' : ''}`;
slot.innerHTML = `<span class="slot-key">${index + 1}</span>`;
const canvas = document.createElement('canvas');
canvas.width = 44;
canvas.height = 44;
slot.appendChild(canvas);
// Render isometric preview
drawIsometricBlock(canvas, type);
slot.addEventListener('click', () => {
activeHotbarIndex = index;
this.updateHotbarUI();
});
hotbarContainer.appendChild(slot);
});
// Populate Creative Inventory grid
const grid = document.getElementById('inventory-grid');
grid.innerHTML = '';
Object.keys(BLOCK_TYPES).forEach(key => {
const type = BLOCK_TYPES[key];
if (type === BLOCK_TYPES.AIR) return;
const item = document.createElement('div');
item.className = 'inventory-item';
const canvas = document.createElement('canvas');
canvas.width = 44;
canvas.height = 44;
item.appendChild(canvas);
drawIsometricBlock(canvas, type);
const name = document.createElement('div');
item.className = 'inventory-item';
name.className = 'inventory-item-name';
name.textContent = BLOCK_DETAILS[type].name;
item.appendChild(name);
// Add click handler to replace currently active hotbar slot
item.addEventListener('click', () => {
hotbarSlots[activeHotbarIndex] = type;
this.updateHotbarUI();
this.toggleCreativeInventory();
audio.playPlace();
});
grid.appendChild(item);
});
// Set close inventory click handler
document.getElementById('close-inv-btn').addEventListener('click', () => {
this.toggleCreativeInventory();
});
// Biome selection handler
const options = document.querySelectorAll('.biome-option');
options.forEach(opt => {
opt.addEventListener('click', () => {
options.forEach(o => o.classList.remove('active'));
opt.classList.add('active');
worldSeed = opt.dataset.seed;
// Set text on HUD
let displaySeed = "Mythic Valleys";
if (worldSeed === "islands") displaySeed = "Aether Islands";
if (worldSeed === "abyss") displaySeed = "Crystal Abyss";
document.getElementById('biome-val').textContent = displaySeed;
});
});
}
updateHotbarUI() {
const slots = document.querySelectorAll('.hotbar-slot');
slots.forEach((slot, index) => {
if (index === activeHotbarIndex) {
slot.classList.add('active');
} else {
slot.classList.remove('active');
}
// Redraw in case values changed
const canvas = slot.querySelector('canvas');
drawIsometricBlock(canvas, hotbarSlots[index]);
});
// Update active name text
const activeType = hotbarSlots[activeHotbarIndex];
document.getElementById('block-name').textContent = BLOCK_DETAILS[activeType].name;
}
updateHUDStats() {
// Update position stats
const px = Math.floor(this.player.x);
const py = Math.floor(this.player.y);
const pz = Math.floor(this.player.z);
document.getElementById('pos-val').textContent = `${px}, ${py}, ${pz}`;
document.getElementById('fly-val').textContent = this.player.creativeFly ? "ON" : "OFF";
document.getElementById('fly-val').style.color = this.player.creativeFly ? "#00ffcc" : "#ff3366";
}
// --- 13. MAIN ANIMATION & RENDERING LOOP ---
animate() {
requestAnimationFrame(() => this.animate());
const dt = Math.min(this.clock.getDelta(), 0.1); // clamp delta to avoid insane spikes on tab sleep
// FPS counter calculation
this.frameCount++;
this.fpsTimer += dt;
if (this.fpsTimer >= 0.5) {
const fps = Math.round(this.frameCount / this.fpsTimer);
document.getElementById('fps-val').textContent = fps;
this.frameCount = 0;
this.fpsTimer = 0;
}
if (this.isPaused) {
this.renderer.render(this.scene, this.camera);
return;
}
// 1. Update Day/Night Cycle
this.updateDayNightCycle(dt);
// 2. Physics & Spectator camera movement
this.updatePlayerPhysics(dt);
// 3. Highlight targeted voxel
const hit = this.performRaycast();
if (hit.hit) {
this.selectionBox.position.set(hit.x + 0.5, hit.y + 0.5, hit.z + 0.5);
this.selectionBox.visible = true;
this.targetedVoxel = hit;
} else {
this.selectionBox.visible = false;
this.targetedVoxel = null;
}
// 4. Update breaking particles
this.updateParticles(dt);
// 5. Update Ambient dust motes slowly rotating
if (this.ambientDust) {
this.ambientDust.rotation.y += 0.02 * dt;
}
// 6. Infinite World: load chunks around player
this.updateChunksAroundPlayer();
// 7. Flush dirty chunks rebuilt mesh updates
if (dirtyChunks.size > 0) {
for (let key of dirtyChunks) {
const [cx, cz] = key.split(',').map(Number);
this.rebuildChunk(cx, cz);
}
dirtyChunks.clear();
}
// 8. Update HUD coords text
this.updateHUDStats();
// 9. Render Frame
this.renderer.render(this.scene, this.camera);
}
}
// --- 14. GLOBAL DIRTY CHUNK SET ---
const dirtyChunks = new Set(); // holds chunk keys e.g. "cx,cz" to rebuild
// --- 15. ISOMETRIC CUBE RENDER HELPER (FOR HUD AND INVENTORY CARDS) ---
function drawIsometricBlock(canvas, blockType) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Shading colors for top, left, right faces
let topColor, leftColor, rightColor;
switch(blockType) {
case BLOCK_TYPES.GRASS:
topColor = '#558a2f'; leftColor = '#6d4c41'; rightColor = '#5d4037';
break;
case BLOCK_TYPES.DIRT:
topColor = '#866043'; leftColor = '#6d4c41'; rightColor = '#5c3e2b';
break;
case BLOCK_TYPES.STONE:
topColor = '#a9a9a9'; leftColor = '#808080'; rightColor = '#696969';
break;
case BLOCK_TYPES.COBBLESTONE:
topColor = '#8a8a8a'; leftColor = '#6e6e6e'; rightColor = '#5c5c5c';
break;
case BLOCK_TYPES.MOSSY_COBBLESTONE:
topColor = '#6a8d52'; leftColor = '#50723a'; rightColor = '#415e2e';
break;
case BLOCK_TYPES.WOOD:
topColor = '#d7b587'; leftColor = '#5c4033'; rightColor = '#4d3327';
break;
case BLOCK_TYPES.LEAVES:
topColor = '#2e5c1e'; leftColor = '#224814'; rightColor = '#1b3a10';
break;
case BLOCK_TYPES.QUARTZ:
topColor = '#ffffff'; leftColor = '#f0f0f0'; rightColor = '#e0e0e0';
break;
case BLOCK_TYPES.GOLD:
topColor = '#ffd700'; leftColor = '#daa520'; rightColor = '#b8860b';
break;
case BLOCK_TYPES.CRYSTAL_CYAN:
topColor = '#e0ffff'; leftColor = '#00bfff'; rightColor = '#008b8b';
break;
case BLOCK_TYPES.CRYSTAL_PURPLE:
topColor = '#ffb6c1'; leftColor = '#ba55d3'; rightColor = '#8b008b';
break;
case BLOCK_TYPES.PORTAL:
topColor = '#8a2be2'; leftColor = '#4b0082'; rightColor = '#190033';
break;
case BLOCK_TYPES.WATER:
topColor = '#1e90ff'; leftColor = '#00bfff'; rightColor = '#0000ff';
break;
case BLOCK_TYPES.GLASS:
topColor = 'rgba(255,255,255,0.45)'; leftColor = 'rgba(255,255,255,0.2)'; rightColor = 'rgba(255,255,255,0.3)';
break;
default:
topColor = '#ffffff'; leftColor = '#cccccc'; rightColor = '#999999';
}
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const size = canvas.width * 0.28;
// 1. Draw Top Face
ctx.fillStyle = topColor;
ctx.beginPath();
ctx.moveTo(cx, cy - size);
ctx.lineTo(cx + size * 1.732 / 2, cy - size / 2);
ctx.lineTo(cx, cy);
ctx.lineTo(cx - size * 1.732 / 2, cy - size / 2);
ctx.closePath();
ctx.fill();
// 2. Draw Left Face
ctx.fillStyle = leftColor;
ctx.beginPath();
ctx.moveTo(cx - size * 1.732 / 2, cy - size / 2);
ctx.lineTo(cx, cy);
ctx.lineTo(cx, cy + size);
ctx.lineTo(cx - size * 1.732 / 2, cy + size / 2);
ctx.closePath();
ctx.fill();
// 3. Draw Right Face
ctx.fillStyle = rightColor;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + size * 1.732 / 2, cy - size / 2);
ctx.lineTo(cx + size * 1.732 / 2, cy + size / 2);
ctx.lineTo(cx, cy + size);
ctx.closePath();
ctx.fill();
}
// --- 16. INIT RUNTIME ---
window.addEventListener('DOMContentLoaded', () => {
const game = new Game();
game.setupUI();
game.init();
});
</script>
</body>
</html>