v4: 250+ blocks, 13 biomes, villages/temples/shipwrecks/igloos/dungeons, caves+ravines, deepslate, sword combat
67fb849 verified | // Structure generator: villages, desert temples, shipwrecks, jungle temples, igloos, etc. | |
| import { CHUNK_SIZE, WORLD_HEIGHT, BlockId } from './blocks'; | |
| import { WorldGenerator, ChunkData, blockIdToNum } from './worldgen'; | |
| export type StructureType = | |
| | 'village' | 'desert_temple' | 'jungle_temple' | 'shipwreck' | |
| | 'igloo' | 'witch_hut' | 'ocean_monument' | 'woodland_mansion' | |
| | 'stronghold' | 'pillager_outpost' | 'ruined_portal' | 'end_portal' | |
| | 'mineshaft' | 'dungeon' | 'well' | 'boulder' | 'cactus_field' | |
| | 'village_house' | 'blacksmith'; | |
| export interface Structure { | |
| type: StructureType; | |
| cx: number; | |
| cz: number; | |
| baseX: number; | |
| baseY: number; | |
| baseZ: number; | |
| rotation: number; | |
| } | |
| // Try to place a structure at chunk coords (rare, controlled by seed) | |
| export function tryPlaceStructures( | |
| chunk: ChunkData, | |
| generator: WorldGenerator, | |
| structuresEnabled: boolean = true | |
| ): void { | |
| if (!structuresEnabled) return; | |
| // Use chunk coords to determine structure presence (deterministic) | |
| const cx = chunk.cx; | |
| const cz = chunk.cz; | |
| // Village: rare, in plains/savanna/desert | |
| if ((Math.abs(cx) % 6 === 0) && (Math.abs(cz) % 6 === 0) && (cx !== 0 || cz !== 0)) { | |
| const biome = generator.getBiome(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (biome === 'plains' || biome === 'savanna' || biome === 'desert' || biome === 'forest') { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (height > 24) { | |
| placeVillage(chunk, height, biome); | |
| return; | |
| } | |
| } | |
| } | |
| // Desert temple: rare, only in desert | |
| if ((Math.abs(cx) % 8 === 3) && (Math.abs(cz) % 8 === 3)) { | |
| const biome = generator.getBiome(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (biome === 'desert') { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| placeDesertTemple(chunk, height); | |
| return; | |
| } | |
| } | |
| // Shipwreck: only in ocean chunks | |
| if ((Math.abs(cx) % 5 === 2) && (Math.abs(cz) % 5 === 2)) { | |
| const biome = generator.getBiome(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (biome === 'ocean') { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (height < 20) { | |
| placeShipwreck(chunk, Math.max(height, 18)); | |
| return; | |
| } | |
| } | |
| } | |
| // Witch hut: in swamp | |
| if ((Math.abs(cx) % 7 === 1) && (Math.abs(cz) % 7 === 1)) { | |
| const biome = generator.getBiome(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (biome === 'swamp' || biome === 'mangrove_swamp') { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| placeWitchHut(chunk, height); | |
| return; | |
| } | |
| } | |
| // Igloo: in snowy | |
| if ((Math.abs(cx) % 9 === 4) && (Math.abs(cz) % 9 === 4)) { | |
| const biome = generator.getBiome(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (biome === 'snowy') { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| placeIgloo(chunk, height); | |
| return; | |
| } | |
| } | |
| // Ruined portal: random small structure | |
| if ((Math.abs(cx * 7 + cz * 13) % 17 === 0) && (cx + cz) % 3 === 0) { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 4, cz * CHUNK_SIZE + 4); | |
| if (height > 25) { | |
| placeRuinedPortal(chunk, height); | |
| } | |
| } | |
| // Boulder (mossy cobblestone) - small decoration in forests | |
| if ((Math.abs(cx * 11 + cz * 17) % 23 === 5)) { | |
| const biome = generator.getBiome(cx * CHUNK_SIZE + 8, cz * CHUNK_SIZE + 8); | |
| if (biome === 'forest' || biome === 'jungle' || biome === 'taiga') { | |
| const height = generator.getHeight(cx * CHUNK_SIZE + 4, cz * CHUNK_SIZE + 4); | |
| placeBoulder(chunk, 4, height, 4); | |
| } | |
| } | |
| // Dungeon (underground) - small room with spawner | |
| if ((Math.abs(cx * 19 + cz * 23) % 11 === 7)) { | |
| const baseY = 10 + Math.floor(Math.random() * 15); | |
| if (baseY < WORLD_HEIGHT - 8) { | |
| placeDungeon(chunk, 4, baseY, 4); | |
| } | |
| } | |
| } | |
| // === Village === | |
| function placeVillage(chunk: ChunkData, baseY: number, biome: string) { | |
| const isDesert = biome === 'desert'; | |
| const wallBlock: BlockId = isDesert ? 'sandstone' : 'cobblestone'; | |
| const floorBlock: BlockId = isDesert ? 'red_sand' : 'cobblestone'; | |
| const roofBlock: BlockId = isDesert ? 'sandstone' : 'planks_oak'; | |
| const logBlock: BlockId = isDesert ? 'log_oak' : 'log_oak'; | |
| // Several houses in a grid | |
| for (let hx = 2; hx < 14; hx += 5) { | |
| for (let hz = 2; hz < 14; hz += 5) { | |
| placeHouse(chunk, hx, baseY, hz, wallBlock, floorBlock, roofBlock, logBlock); | |
| } | |
| } | |
| // Central well | |
| placeWell(chunk, 7, baseY, 7); | |
| // Path blocks | |
| for (let i = 0; i < CHUNK_SIZE; i++) { | |
| setBlock(chunk, i, baseY - 1, 7, 'dirt_path'); | |
| setBlock(chunk, 7, baseY - 1, i, 'dirt_path'); | |
| } | |
| } | |
| function placeHouse(chunk: ChunkData, x: number, y: number, z: number, wall: BlockId, floor: BlockId, roof: BlockId, log: BlockId) { | |
| // 4x4 house, 4 tall | |
| const w = 4, h = 4, d = 4; | |
| // Floor | |
| for (let dx = 0; dx < w; dx++) { | |
| for (let dz = 0; dz < d; dz++) { | |
| setBlock(chunk, x + dx, y - 1, z + dz, floor); | |
| } | |
| } | |
| // Walls | |
| for (let dy = 0; dy < h; dy++) { | |
| for (let dx = 0; dx < w; dx++) { | |
| setBlock(chunk, x + dx, y + dy, z, wall); | |
| setBlock(chunk, x + dx, y + dy, z + d - 1, wall); | |
| } | |
| for (let dz = 0; dz < d; dz++) { | |
| setBlock(chunk, x, y + dy, z + dz, wall); | |
| setBlock(chunk, x + w - 1, y + dy, z + dz, wall); | |
| } | |
| } | |
| // Door (front, mid) | |
| setBlock(chunk, x + 1, y, z, 'air'); | |
| setBlock(chunk, x + 1, y + 1, z, 'oak_door'); | |
| setBlock(chunk, x + 2, y, z, 'air'); | |
| setBlock(chunk, x + 2, y + 1, z, 'oak_door'); | |
| // Windows | |
| setBlock(chunk, x + 1, y + 1, z + d - 1, 'glass'); | |
| setBlock(chunk, x + w - 2, y + 1, z + d - 1, 'glass'); | |
| setBlock(chunk, x, y + 1, z + 1, 'glass'); | |
| setBlock(chunk, x, y + 1, z + d - 2, 'glass'); | |
| setBlock(chunk, x + w - 1, y + 1, z + 1, 'glass'); | |
| setBlock(chunk, x + w - 1, y + 1, z + d - 2, 'glass'); | |
| // Roof (sloped) | |
| for (let dx = 0; dx < w; dx++) { | |
| for (let dz = 0; dz < d; dz++) { | |
| setBlock(chunk, x + dx, y + h, z + dz, roof); | |
| } | |
| } | |
| // Interior: bed and crafting table | |
| setBlock(chunk, x + 1, y, z + 1, 'crafting_table'); | |
| setBlock(chunk, x + w - 2, y, z + d - 2, 'furnace'); | |
| setBlock(chunk, x + 1, y, z + d - 2, 'chest'); | |
| // Torch | |
| setBlock(chunk, x + 1, y + 2, z + 1, 'torch'); | |
| } | |
| function placeWell(chunk: ChunkData, x: number, y: number, z: number) { | |
| // 3x3 well with water | |
| for (let dx = 0; dx < 3; dx++) { | |
| for (let dz = 0; dz < 3; dz++) { | |
| setBlock(chunk, x + dx, y - 1, z + dz, 'cobblestone'); | |
| if (dx === 1 && dz === 1) { | |
| // Water in center | |
| for (let dy = 0; dy < 3; dy++) setBlock(chunk, x + dx, y + dy, z + dz, 'water'); | |
| } else { | |
| setBlock(chunk, x + dx, y, z + dz, 'cobblestone'); | |
| } | |
| } | |
| } | |
| // Corner posts | |
| for (let dy = 0; dy < 3; dy++) { | |
| setBlock(chunk, x, y + dy, z, 'cobblestone_wall'); | |
| setBlock(chunk, x + 2, y + dy, z, 'cobblestone_wall'); | |
| setBlock(chunk, x, y + dy, z + 2, 'cobblestone_wall'); | |
| setBlock(chunk, x + 2, y + dy, z + 2, 'cobblestone_wall'); | |
| } | |
| // Roof | |
| for (let dx = -1; dx <= 3; dx++) { | |
| for (let dz = -1; dz <= 3; dz++) { | |
| setBlock(chunk, x + dx, y + 3, z + dz, 'planks_oak'); | |
| } | |
| } | |
| } | |
| // === Desert Temple === | |
| function placeDesertTemple(chunk: ChunkData, baseY: number) { | |
| // Pyramid shape: 21x21 base tapering to 1x1 top | |
| const size = 11; | |
| for (let layer = 0; layer < size; layer++) { | |
| const w = size - layer; | |
| for (let dx = 0; dx < w; dx++) { | |
| for (let dz = 0; dz < w; dz++) { | |
| const lx = 3 + layer + dx; | |
| const lz = 3 + layer + dz; | |
| const ly = baseY + layer; | |
| if (lx >= 0 && lx < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE) { | |
| setBlock(chunk, lx, ly, lz, 'sandstone'); | |
| } | |
| } | |
| } | |
| } | |
| // Entrance at base | |
| for (let dy = 0; dy < 3; dy++) { | |
| setBlock(chunk, 8, baseY + dy, 3, 'air'); | |
| } | |
| // Hidden chamber with chests | |
| for (let dy = 0; dy < 4; dy++) { | |
| setBlock(chunk, 7, baseY - 1 - dy, 7, 'air'); | |
| setBlock(chunk, 8, baseY - 1 - dy, 7, 'air'); | |
| setBlock(chunk, 9, baseY - 1 - dy, 7, 'air'); | |
| } | |
| // TNT trap | |
| setBlock(chunk, 8, baseY - 5, 7, 'tnt'); | |
| setBlock(chunk, 7, baseY - 5, 7, 'tnt'); | |
| setBlock(chunk, 9, baseY - 5, 7, 'tnt'); | |
| // Chests with loot | |
| setBlock(chunk, 7, baseY - 4, 6, 'chest'); | |
| setBlock(chunk, 8, baseY - 4, 6, 'chest'); | |
| setBlock(chunk, 9, baseY - 4, 6, 'chest'); | |
| setBlock(chunk, 7, baseY - 4, 8, 'chest'); | |
| setBlock(chunk, 9, baseY - 4, 8, 'chest'); | |
| } | |
| // === Shipwreck === | |
| function placeShipwreck(chunk: ChunkData, baseY: number) { | |
| // Boat shape: pointed at front | |
| const length = 12; | |
| for (let dx = 0; dx < length; dx++) { | |
| const width = dx < 2 ? dx + 1 : (dx > length - 3 ? length - dx : 3); | |
| for (let dz = -width; dz <= width; dz++) { | |
| const lz = 8 + dz; | |
| if (lz < 0 || lz >= CHUNK_SIZE) continue; | |
| // Hull | |
| setBlock(chunk, dx + 2, baseY, lz, 'planks_oak'); | |
| // Damaged parts | |
| if (Math.random() > 0.6) setBlock(chunk, dx + 2, baseY, lz, 'air'); | |
| // Mast | |
| if (dx === 6 && dz === 0) { | |
| for (let dy = 1; dy < 6; dy++) { | |
| setBlock(chunk, dx + 2, baseY + dy, lz, 'log_oak'); | |
| } | |
| } | |
| } | |
| } | |
| // Chests | |
| setBlock(chunk, 6, baseY + 1, 8, 'chest'); | |
| setBlock(chunk, 10, baseY + 1, 8, 'chest'); | |
| } | |
| // === Witch Hut === | |
| function placeWitchHut(chunk: ChunkData, baseY: number) { | |
| // Hut on stilts | |
| const floorY = baseY + 3; | |
| // Floor | |
| for (let dx = 0; dx < 7; dx++) { | |
| for (let dz = 0; dz < 7; dz++) { | |
| setBlock(chunk, 4 + dx, floorY, 4 + dz, 'planks_spruce'); | |
| } | |
| } | |
| // Stilts | |
| for (const [sx, sz] of [[4, 4], [10, 4], [4, 10], [10, 10]]) { | |
| for (let dy = 0; dy < 3; dy++) { | |
| setBlock(chunk, sx, baseY + dy, sz, 'log_oak'); | |
| } | |
| } | |
| // Walls | |
| for (let dy = 1; dy <= 3; dy++) { | |
| for (let i = 0; i < 7; i++) { | |
| setBlock(chunk, 4 + i, floorY + dy, 4, 'planks_spruce'); | |
| setBlock(chunk, 4 + i, floorY + dy, 10, 'planks_spruce'); | |
| setBlock(chunk, 4, floorY + dy, 4 + i, 'planks_spruce'); | |
| setBlock(chunk, 10, floorY + dy, 4 + i, 'planks_spruce'); | |
| } | |
| } | |
| // Door | |
| setBlock(chunk, 7, floorY + 1, 4, 'air'); | |
| setBlock(chunk, 7, floorY + 2, 4, 'air'); | |
| // Roof | |
| for (let dx = 0; dx < 9; dx++) { | |
| for (let dz = 0; dz < 9; dz++) { | |
| setBlock(chunk, 3 + dx, floorY + 4, 3 + dz, 'leaves_spruce'); | |
| } | |
| } | |
| // Cauldron and brewing stand inside | |
| setBlock(chunk, 6, floorY + 1, 6, 'cauldron'); | |
| setBlock(chunk, 8, floorY + 1, 8, 'brewing_stand'); | |
| setBlock(chunk, 7, floorY + 1, 7, 'flower_pot'); | |
| } | |
| // === Igloo === | |
| function placeIgloo(chunk: ChunkData, baseY: number) { | |
| // Snow dome | |
| for (let dy = 0; dy < 4; dy++) { | |
| const r = 4 - dy; | |
| for (let dx = -r; dx <= r; dx++) { | |
| for (let dz = -r; dz <= r; dz++) { | |
| if (dx * dx + dz * dz <= r * r) { | |
| const lx = 8 + dx; | |
| const lz = 8 + dz; | |
| if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) continue; | |
| setBlock(chunk, lx, baseY + dy, lz, 'snow'); | |
| } | |
| } | |
| } | |
| } | |
| // Carve interior | |
| for (let dy = 0; dy < 3; dy++) { | |
| for (let dx = -2; dx <= 2; dx++) { | |
| for (let dz = -2; dz <= 2; dz++) { | |
| const lx = 8 + dx; | |
| const lz = 8 + dz; | |
| setBlock(chunk, lx, baseY + dy, lz, 'air'); | |
| } | |
| } | |
| } | |
| // Bed and crafting table inside | |
| setBlock(chunk, 6, baseY, 8, 'crafting_table'); | |
| setBlock(chunk, 10, baseY, 8, 'furnace'); | |
| // Entrance tunnel | |
| for (let dy = 0; dy < 2; dy++) { | |
| setBlock(chunk, 8, baseY + dy, 12, 'air'); | |
| setBlock(chunk, 8, baseY + dy, 13, 'air'); | |
| } | |
| } | |
| // === Ruined Portal === | |
| function placeRuinedPortal(chunk: ChunkData, baseY: number) { | |
| // Vertical obsidian portal frame (broken) | |
| const positions = [ | |
| [4, 0], [4, 1], [4, 2], [4, 3], [4, 4], | |
| [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], | |
| [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], | |
| ]; | |
| for (const [dx, dy] of positions) { | |
| if (Math.random() > 0.2) { // some missing | |
| setBlock(chunk, 6 + dx, baseY + dy, 8, 'obsidian'); | |
| } | |
| } | |
| // Some crying obsidian | |
| if (Math.random() > 0.5) setBlock(chunk, 6 + 4, baseY + 1, 8, 'crying_obsidian'); | |
| // Chest nearby | |
| setBlock(chunk, 5, baseY, 6, 'chest'); | |
| } | |
| // === Boulder (mossy cobblestone) === | |
| function placeBoulder(chunk: ChunkData, x: number, y: number, z: number) { | |
| // Random cluster of mossy cobblestone | |
| for (let dy = 0; dy < 2; dy++) { | |
| for (let dx = -1; dx <= 1; dx++) { | |
| for (let dz = -1; dz <= 1; dz++) { | |
| if (Math.abs(dx) + Math.abs(dz) + dy <= 2 && Math.random() > 0.3) { | |
| setBlock(chunk, x + dx, y + dy, z + dz, 'mossy_cobblestone'); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // === Dungeon === | |
| function placeDungeon(chunk: ChunkData, x: number, y: number, z: number) { | |
| // 5x5 room with cobblestone walls | |
| for (let dx = -2; dx <= 2; dx++) { | |
| for (let dz = -2; dz <= 2; dz++) { | |
| for (let dy = -1; dy <= 3; dy++) { | |
| const onEdge = Math.abs(dx) === 2 || Math.abs(dz) === 2 || dy === -1 || dy === 3; | |
| if (onEdge) { | |
| setBlock(chunk, x + dx, y + dy, z + dz, 'cobblestone'); | |
| } else { | |
| setBlock(chunk, x + dx, y + dy, z + dz, 'air'); | |
| } | |
| } | |
| } | |
| } | |
| // Spawner in center | |
| const mobTypes: BlockId[] = ['spawner_zombie', 'spawner_skeleton', 'spawner_spider']; | |
| setBlock(chunk, x, y, z, mobTypes[Math.floor(Math.random() * 3)]); | |
| // Chests in corners | |
| setBlock(chunk, x - 1, y, z - 1, 'chest'); | |
| setBlock(chunk, x + 1, y, z + 1, 'chest'); | |
| } | |
| // Helper: set block safely | |
| function setBlock(chunk: ChunkData, x: number, y: number, z: number, block: BlockId) { | |
| if (x < 0 || x >= CHUNK_SIZE || z < 0 || z >= CHUNK_SIZE || y < 0 || y >= WORLD_HEIGHT) return; | |
| chunk.blocks[(y * CHUNK_SIZE + z) * CHUNK_SIZE + x] = blockIdToNum(block); | |
| } | |