TomatitoToho's picture
v4: 250+ blocks, 13 biomes, villages/temples/shipwrecks/igloos/dungeons, caves+ravines, deepslate, sword combat
67fb849 verified
Raw
History Blame Contribute Delete
13.9 kB
// 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);
}