'use client' import { create } from 'zustand' import type { ChunkData, World } from '@/types' import { CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_DEPTH, BLOCK_AIR, MIN_BUILD_HEIGHT, } from '@/engine/constants' import { TerrainGenerator } from '@/engine/terrain/generator' import { NetherGenerator } from '@/engine/terrain/dimensionGenerator' import { EndGenerator } from '@/engine/terrain/dimensionGenerator' // ═══════════════════════════════════════════════════════════════ // DIMENSION CONSTANTS // ═══════════════════════════════════════════════════════════════ const NETHER_CHUNK_HEIGHT = 128 const END_CHUNK_HEIGHT = 128 type DimensionType = 'overworld' | 'nether' | 'end' function chunkKey(cx: number, cz: number): string { return `${cx},${cz}` } // ═══════════════════════════════════════════════════════════════ // STORE INTERFACE // ═══════════════════════════════════════════════════════════════ interface WorldStore { world: World | null chunks: Map dimension: DimensionType netherSeed: number endSeed: number // Generators (lazy-initialized) overworldGenerator: TerrainGenerator | null netherGenerator: NetherGenerator | null endGenerator: EndGenerator | null initWorld: (seed: number) => void setChunk: (key: string, chunk: ChunkData) => void removeChunk: (key: string) => void getBlock: (x: number, y: number, z: number) => number setBlock: (x: number, y: number, z: number, blockId: number) => void updateWorldTime: () => void switchDimension: (dimension: string) => void generateChunk: (cx: number, cz: number) => ChunkData getCurrentChunkHeight: () => number } // ═══════════════════════════════════════════════════════════════ // STORE IMPLEMENTATION // ═══════════════════════════════════════════════════════════════ export const useWorldStore = create((set, get) => ({ world: null, chunks: new Map(), dimension: 'overworld', netherSeed: 0, endSeed: 0, overworldGenerator: null, netherGenerator: null, endGenerator: null, initWorld: (seed: number) => { // Derive dimension seeds from main seed const netherSeed = (seed * 0x5bd1e995 + 0xDEAD) | 0 const endSeed = (seed * 0x27d4eb2d + 0xBEEF) | 0 const world: World = { seed, time: 6000, // noon gameTime: 0, spawnPoint: [8, 80, 8], weather: { raining: false, thundering: false, rainTime: 0, thunderTime: 0 }, dimension: 'overworld', } set({ world, chunks: new Map(), dimension: 'overworld', netherSeed, endSeed, overworldGenerator: new TerrainGenerator(seed), netherGenerator: new NetherGenerator(netherSeed), endGenerator: new EndGenerator(endSeed), }) }, setChunk: (key, chunk) => { const chunks = new Map(get().chunks) chunks.set(key, chunk) set({ chunks }) }, removeChunk: (key) => { const chunks = new Map(get().chunks) chunks.delete(key) set({ chunks }) }, getBlock: (x, y, z) => { const { dimension } = get() // Different height ranges per dimension if (dimension === 'overworld') { if (y < MIN_BUILD_HEIGHT || y > 319) return BLOCK_AIR } else { // Nether and End: 0-127 if (y < 0 || y > 127) return BLOCK_AIR } const cx = Math.floor(x / CHUNK_WIDTH) const cz = Math.floor(z / CHUNK_DEPTH) const chunk = get().chunks.get(chunkKey(cx, cz)) if (!chunk) return BLOCK_AIR const lx = ((x % CHUNK_WIDTH) + CHUNK_WIDTH) % CHUNK_WIDTH const lz = ((z % CHUNK_DEPTH) + CHUNK_DEPTH) % CHUNK_DEPTH // Y offset depends on dimension let ly: number if (dimension === 'overworld') { ly = y - MIN_BUILD_HEIGHT // offset for negative Y } else { ly = y // nether/end start at 0 } const chunkH = get().getCurrentChunkHeight() if (ly < 0 || ly >= chunkH) return BLOCK_AIR const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_DEPTH return chunk.blocks[idx] || BLOCK_AIR }, setBlock: (x, y, z, blockId) => { const { dimension } = get() if (dimension === 'overworld') { if (y < MIN_BUILD_HEIGHT || y > 319) return } else { if (y < 0 || y > 127) return } const cx = Math.floor(x / CHUNK_WIDTH) const cz = Math.floor(z / CHUNK_DEPTH) const key = chunkKey(cx, cz) const chunk = get().chunks.get(key) if (!chunk) return const lx = ((x % CHUNK_WIDTH) + CHUNK_WIDTH) % CHUNK_WIDTH const lz = ((z % CHUNK_DEPTH) + CHUNK_DEPTH) % CHUNK_DEPTH let ly: number if (dimension === 'overworld') { ly = y - MIN_BUILD_HEIGHT } else { ly = y } const chunkH = get().getCurrentChunkHeight() if (ly < 0 || ly >= chunkH) return const idx = lx + lz * CHUNK_WIDTH + ly * CHUNK_WIDTH * CHUNK_DEPTH chunk.blocks[idx] = blockId chunk.dirty = true chunk.meshVersion++ set({ chunks: new Map(get().chunks) }) }, updateWorldTime: () => { const { world } = get() if (!world) return world.time = (world.time + 1) % 24000 world.gameTime++ set({ world }) }, switchDimension: (dimension: string) => { const { world } = get() if (!world) return const dim = dimension as DimensionType if (dim === get().dimension) return // Update the world's dimension world.dimension = dim // Set spawn point based on dimension let spawnPoint: [number, number, number] switch (dim) { case 'nether': spawnPoint = [8, 64, 8] break case 'end': spawnPoint = [0, 65, 0] break default: spawnPoint = [8, 80, 8] } // Clear all chunks — they will be regenerated by the renderer // using the appropriate dimension generator set({ world, chunks: new Map(), dimension: dim, }) // Update spawn point world.spawnPoint = spawnPoint set({ world }) }, generateChunk: (cx: number, cz: number): ChunkData => { const { dimension, overworldGenerator, netherGenerator, endGenerator } = get() switch (dimension) { case 'nether': if (!netherGenerator) { // Lazy init if needed const { netherSeed } = get() const gen = new NetherGenerator(netherSeed) set({ netherGenerator: gen }) return gen.generateChunk(cx, cz) } return netherGenerator.generateChunk(cx, cz) case 'end': if (!endGenerator) { const { endSeed } = get() const gen = new EndGenerator(endSeed) set({ endGenerator: gen }) return gen.generateChunk(cx, cz) } return endGenerator.generateChunk(cx, cz) case 'overworld': default: if (!overworldGenerator) { const { world } = get() const seed = world?.seed ?? 12345 const gen = new TerrainGenerator(seed) set({ overworldGenerator: gen }) return gen.generateChunk(cx, cz) } return overworldGenerator.generateChunk(cx, cz) } }, getCurrentChunkHeight: (): number => { const { dimension } = get() switch (dimension) { case 'nether': return NETHER_CHUNK_HEIGHT case 'end': return END_CHUNK_HEIGHT default: return CHUNK_HEIGHT } }, }))