Spaces:
Sleeping
Sleeping
| '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<string, ChunkData> | |
| 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<WorldStore>((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 | |
| } | |
| }, | |
| })) | |