// === BUILD PLAN ===
// Style: Hakka tulou, CONCENTRIC (Chengqi-lou variant): 3-storey outer ring + 1-storey inner ring + centre shrine
// Footprint: outer wall BATTERED (base R24.5 -> top R22.5, inward slope ~1 per 8 height, per architecture-guide battered walls)
// rooms band R14.0-19.5 | inner alley R11.5-13.5 | inner ring building R7.5-11.5 | shrine 5x5 at centre
// Height: walk1 Y1 / walk2 Y6 / walk3 Y11, inner wall top Y15, battered outer parapet Y17, ridge Y18
//
// PLAN: ############
// ###ooooooooooo### # outer rammed-earth wall (battered, 5 thick at base)
// ##ooo...........ooo## o 3-storey ring rooms + timber gallery
// #oo.. ####### ..oo# . inner alley (paved)
// #oo.. # S # ..oo# S centre shrine (5x5, pyramid tile roof)
// #oo.. ####### ..oo# inner 1-storey ring = kitchens/ancestral hall
// ###ooooooooooo###
// #####G###### G single gate, south (+Z, faces the camera)
//
// SECTION (courtyard LEFT / outside RIGHT):
// eave Y12 __/‾‾‾‾‾‾‾‾‾\__ Y15 eave + upside-down stair BRACKETS (roof overhang support)
// rail | 3F Y11 | |#### battered: each storey insets the outer face 1 block
// rail | 2F Y6 | |#####
// | 1F Y1 | |######
//
// PROCESS: LAYERED. (1) form in placeholders blue_wool (outer wall) / light_blue_wool (inner ring)
// (2) carve gate + recessed windows (3) TEXTURE LAST with replaceMasked + gradientPalette/noisePalette.
// Roof is built DIRECTLY with roofStair(mat(x,0,z)) - never replaceMasked over stairs (kills facing).
// MATERIALS: outer gradient mud_bricks/packed_mud (wet base) -> smooth_sandstone/sandstone (bleached top)
// roof noisePalette deepslate_tile/cobbled_deepslate/tuff_brick stairs = grey clay tile
// galleries spruce/oak timber; shrine dark oak + stone bricks
// STAIR PLAN: placeStairwell() U-turn wells (5x5) inside the ring: north walk1->f2, west walk2->f3.
// === END PLAN ===
var padY = 0, floorY = 0, walk1 = 1;
var f2Y = 5, walk2 = 6, f3Y = 10, walk3 = 11;
var innerTopY = 15, outerTopY = 17, ridgeY = 18;
var R_ROOM_IN = 14.0, R_ROOM_OUT = 19.5, R_ALLEY_IN = 11.5, R_INNER_OUT = 11.5, R_INNER_IN = 7.5;
function rr(x, z) { return Math.sqrt(x * x + z * z); }
// battered outer face: 24.5 at the base, 22.5 at the parapet
function outerFace(y) { return 24.5 - (y / outerTopY) * 2.0; }
var x, z, y, k, d;
// ============ 1. TERRAIN (hill terrace pad) ============
var ground = terrain({
cx: 0, cz: 0, radius: 36, baseY: padY, amp: 5, scale: 0.05, seed: 41,
surface: [{ block: 'grass_block', w: 60 }, { block: 'podzol', w: 22 }, { block: 'coarse_dirt', w: 18 }],
sub: 'dirt', deep: 'stone', depth: 4,
pads: [{ cx: 0, cz: 0, r: 28, y: padY, falloff: 10 }]
});
// ============ 2. FORM IN PLACEHOLDERS ============
// outer battered wall
for (x = -27; x <= 27; x++) {
for (z = -27; z <= 27; z++) {
d = rr(x, z);
if (d <= R_ROOM_OUT) continue;
for (y = 1; y <= outerTopY; y++) {
if (d <= outerFace(y)) pb(x, y, z, 'blue_wool');
}
}
}
// inner ring building (1 storey) + its own inner face
for (x = -13; x <= 13; x++) {
for (z = -13; z <= 13; z++) {
d = rr(x, z);
if (d > R_INNER_IN && d <= R_INNER_OUT) {
if (d > 10.4 || d <= 8.5) { for (y = 1; y <= 5; y++) pb(x, y, z, 'light_blue_wool'); }
pb(x, floorY, z, 'light_gray_wool');
}
}
}
// centre shrine
for (x = -2; x <= 2; x++) {
for (z = -2; z <= 2; z++) {
pb(x, floorY, z, 'polished_andesite');
if ((Math.abs(x) === 2 || Math.abs(z) === 2) && !(z === 2 && Math.abs(x) <= 1)) {
for (y = 1; y <= 4; y++) pb(x, y, z, 'light_blue_wool');
}
}
}
// ============ 3. CARVE: gate, windows, doorways ============
// gate tunnel through the outer wall, south (+Z)
for (x = -1; x <= 1; x++) {
for (z = 13; z <= 27; z++) {
d = rr(x, z);
if (d > 26) continue;
for (y = 1; y <= 4; y++) b(x, y, z, 'air');
}
}
// recessed windows: upper storeys only (a tulou has NO ground-floor openings)
for (k = 0; k < 26; k++) {
var aw = (k * 13.85 + 6) * Math.PI / 180;
var ux = Math.cos(aw), uz = Math.sin(aw);
if (uz > 0.93) continue;
var lv, ra;
for (lv = 0; lv < 2; lv++) {
var wy = (lv === 0 ? walk2 : walk3) + 1;
var inner = null;
for (ra = 19.2; ra <= 25.2; ra += 0.4) {
var vx = Math.round(ux * ra), vz = Math.round(uz * ra);
if (rr(vx, vz) > outerFace(wy) + 0.5) continue;
b(vx, wy, vz, 'air'); b(vx, wy + 1, vz, 'air');
if (inner === null) inner = [vx, vz];
}
if (inner) {
pb(inner[0], wy, inner[1], 'brown_stained_glass_pane');
pb(inner[0], wy + 1, inner[1], 'brown_stained_glass_pane');
ps(inner[0], wy + 2, inner[1], 'dark_oak_slab[type=top]');
}
}
}
// ============ 4. TEXTURE LAST (masks + gradient/noise palettes) ============
var earthGrad = gradientPalette(1, outerTopY, [
{ block: 'mud_bricks', from: 0 }, { block: 'packed_mud', from: 0.18 },
{ block: 'cut_sandstone', from: 0.42 }, { block: 'smooth_sandstone', from: 0.58 },
{ block: 'sandstone', from: 0.8 }, { block: 'yellow_terracotta', from: 0.93 }
], { noise: 0.28 });
replaceMasked(-28, 0, -28, 28, outerTopY + 1, 28, earthGrad, onlyMaterial('blue_wool'));
var timberNoise = noisePalette([
{ block: 'spruce_planks', weight: 45 }, { block: 'oak_planks', weight: 25 },
{ block: 'stripped_spruce_log', weight: 18 }, { block: 'dark_oak_planks', weight: 12 }
], { freq: 0.18, seed: 5 });
replaceMasked(-14, 0, -14, 14, 6, 14, timberNoise, onlyMaterial('light_blue_wool'));
replaceMasked(-14, 0, -14, 14, 1, 14, noisePalette([
{ block: 'stone_bricks', weight: 40 }, { block: 'andesite', weight: 30 }, { block: 'cobblestone', weight: 30 }
], { freq: 0.2, seed: 9 }), onlyMaterial('light_gray_wool'));
// stone plinth course + banding on the battered wall (reads the batter)
for (x = -27; x <= 27; x++) {
for (z = -27; z <= 27; z++) {
d = rr(x, z);
if (d <= R_ROOM_OUT) continue;
if (x >= -1 && x <= 1 && z > 13) continue;
if (d <= outerFace(1) + 0.5 && d > outerFace(2)) { pb(x, 1, z, 'cobblestone'); pb(x, 2, z, 'mossy_cobblestone'); }
if (d <= outerFace(f2Y) + 0.5 && d > outerFace(f2Y) - 1.0) pb(x, f2Y, z, 'cut_sandstone');
if (d <= outerFace(f3Y) + 0.5 && d > outerFace(f3Y) - 1.0) pb(x, f3Y, z, 'cut_sandstone');
}
}
// ============ 5. RING FLOORS, GALLERIES, PARTITIONS ============
for (x = -21; x <= 21; x++) {
for (z = -21; z <= 21; z++) {
d = rr(x, z);
if (d > R_ROOM_IN && d <= R_ROOM_OUT) {
pb(x, floorY, z, timberNoise(x, 0, z));
pb(x, f2Y, z, timberNoise(x, f2Y, z));
pb(x, f3Y, z, timberNoise(x, f3Y, z));
if (d > 18.1) pb(x, innerTopY, z, 'spruce_planks');
}
// cantilevered gallery deck over the alley
if (d > 12.2 && d <= R_ROOM_IN) {
pb(x, f2Y, z, timberNoise(x, f2Y, z));
pb(x, f3Y, z, timberNoise(x, f3Y, z));
}
// paved alley between the rings
if (d > R_INNER_OUT && d <= 13.6) pb(x, floorY, z, noisePalette([
{ block: 'stone_bricks', weight: 35 }, { block: 'andesite', weight: 25 },
{ block: 'cobblestone', weight: 25 }, { block: 'gravel', weight: 15 }], { freq: 0.25, seed: 3 })(x, 0, z));
}
}
// ground-floor inner wall of the ring rooms, WITH a doorway per room
for (k = 0; k < 24; k++) {
var ad = k * 15 * Math.PI / 180;
var dx = Math.round(Math.cos(ad) * 14.2), dz = Math.round(Math.sin(ad) * 14.2);
var doorx = Math.round(Math.cos((k * 15 + 7.5) * Math.PI / 180) * 14.2);
var doorz = Math.round(Math.sin((k * 15 + 7.5) * Math.PI / 180) * 14.2);
var rw;
for (rw = 13.6; rw <= 14.6; rw += 0.5) {
var sx = Math.round(Math.cos(ad) * rw), sz = Math.round(Math.sin(ad) * rw);
for (y = 1; y <= 4; y++) pb(sx, y, sz, timberNoise(sx, y, sz));
}
// 2-high doorway from the alley into the room
for (rw = 13.4; rw <= 14.8; rw += 0.4) {
var ex = Math.round(Math.cos((k * 15 + 7.5) * Math.PI / 180) * rw);
var ez = Math.round(Math.sin((k * 15 + 7.5) * Math.PI / 180) * rw);
b(ex, walk1, ez, 'air'); b(ex, walk1 + 1, ez, 'air');
}
ps(doorx, walk1 + 2, doorz, 'dark_oak_slab[type=top]');
}
// veranda posts + rails on the gallery (ROOF OVERHANG SUPPORT — no floating eaves)
for (k = 0; k < 24; k++) {
var ag = k * 15 * Math.PI / 180;
var px = Math.round(Math.cos(ag) * 13.0), pz = Math.round(Math.sin(ag) * 13.0);
for (y = walk1; y <= 4; y++) pb(px, y, pz, 'stripped_dark_oak_log');
for (y = walk2; y <= walk2 + 3; y++) pb(px, y, pz, 'stripped_dark_oak_log');
for (y = walk3; y <= walk3 + 3; y++) pb(px, y, pz, 'stripped_dark_oak_log');
var qx = Math.round(Math.cos(ag) * 12.3), qz = Math.round(Math.sin(ag) * 12.3);
pb(qx, walk2, qz, 'spruce_fence'); pb(qx, walk3, qz, 'spruce_fence');
if (k % 2 === 0) { ps(px, walk2 + 2, pz, 'lantern[hanging=false]'); ps(px, walk3 + 2, pz, 'lantern[hanging=false]'); }
}
for (x = -14; x <= 14; x++) {
for (z = -14; z <= 14; z++) {
d = rr(x, z);
if (d > 12.0 && d <= 12.9) { pb(x, walk2, z, 'spruce_fence'); pb(x, walk3, z, 'spruce_fence'); }
}
}
// radial partitions on all three storeys
for (k = 0; k < 24; k++) {
var ap = k * 15 * Math.PI / 180;
var rp;
for (rp = 14.6; rp <= 19.4; rp += 0.45) {
var wx = Math.round(Math.cos(ap) * rp), wz = Math.round(Math.sin(ap) * rp);
if (wx >= -3 && wx <= 3 && wz <= -14 && wz >= -21) continue; // stairwell N
if (wz >= -3 && wz <= 3 && wx <= -14 && wx >= -21) continue; // stairwell W
if (wx >= -1 && wx <= 1 && wz > 13) continue; // gate
for (y = 1; y <= 4; y++) pb(wx, y, wz, timberNoise(wx, y, wz));
for (y = walk2; y <= walk2 + 3; y++) pb(wx, y, wz, timberNoise(wx, y, wz));
for (y = walk3; y <= walk3 + 3; y++) pb(wx, y, wz, timberNoise(wx, y, wz));
}
}
// ============ 6. STAIRWELLS (U-turn, 5x5) ============
placeStairwell(0, -17, walk1, f2Y, 'spruce_stairs', 'spruce_fence', 'spruce_planks', 'east');
placeStairwell(-17, 0, walk2, f3Y, 'spruce_stairs', 'spruce_fence', 'spruce_planks', 'south');
// ============ 7. RING ROOF (grey tile, slopes inward; braced eaves) ============
var tileMat = noisePalette([
{ block: 'deepslate_tile_stairs', weight: 40 }, { block: 'cobbled_deepslate_stairs', weight: 28 },
{ block: 'tuff_brick_stairs', weight: 18 }, { block: 'stone_brick_stairs', weight: 14 }
], { freq: 0.16, seed: 13 });
var tileSolid = noisePalette([
{ block: 'deepslate_tiles', weight: 40 }, { block: 'cobbled_deepslate', weight: 30 },
{ block: 'tuff_bricks', weight: 18 }, { block: 'gray_terracotta', weight: 12 }
], { freq: 0.16, seed: 13 });
function roofYFor(dd) {
if (dd > 26.2) return -1;
if (dd > 25.0) return 15;
if (dd > 24.0) return 16;
if (dd > 23.0) return 17;
if (dd > 21.0) return ridgeY;
if (dd > 19.5) return 17;
if (dd > 18.0) return 16;
if (dd > 16.5) return 15;
if (dd > 15.0) return 14;
if (dd > 13.4) return 13;
if (dd > 12.0) return 12;
return -1;
}
for (x = -27; x <= 27; x++) {
for (z = -27; z <= 27; z++) {
d = rr(x, z);
var ry = roofYFor(d);
if (ry < 0) continue;
if (d > 21.0 && d <= 23.0) { pb(x, ry, z, tileSolid(x, ry, z)); continue; }
var edge;
if (d > 23.0) edge = (Math.abs(x) > Math.abs(z)) ? (x > 0 ? 'east' : 'west') : (z > 0 ? 'south' : 'north');
else edge = (Math.abs(x) > Math.abs(z)) ? (x > 0 ? 'west' : 'east') : (z > 0 ? 'north' : 'south');
roofStair(x, ry, z, tileMat(x, ry, z), edge);
if (ry - 1 >= 16) pb(x, ry - 1, z, tileSolid(x, ry - 1, z));
}
}
// upside-down stair BRACKETS under the outer eave (roof overhang support, build-rules #22)
for (k = 0; k < 48; k++) {
var ab = k * 7.5 * Math.PI / 180;
var bx = Math.round(Math.cos(ab) * 24.2), bz = Math.round(Math.sin(ab) * 24.2);
var fdir = (Math.abs(bx) > Math.abs(bz)) ? (bx > 0 ? 'west' : 'east') : (bz > 0 ? 'north' : 'south');
ps(bx, 15, bz, 'dark_oak_stairs[facing=' + fdir + ',half=top]');
var cx2 = Math.round(Math.cos(ab) * 23.2), cz2 = Math.round(Math.sin(ab) * 23.2);
ps(cx2, 14, cz2, 'dark_oak_stairs[facing=' + fdir + ',half=top]');
}
// ============ 8. INNER RING ROOF + SHRINE ROOF ============
for (x = -13; x <= 13; x++) {
for (z = -13; z <= 13; z++) {
d = rr(x, z);
if (d > 11.9 || d <= 6.4) continue;
var iy;
if (d > 10.6) iy = 6; else if (d > 9.2) iy = 7; else if (d > 7.8) iy = 7; else iy = 6;
if (d > 9.2 && d <= 10.6) { pb(x, 7, z, tileSolid(x, 7, z)); continue; }
var iedge;
if (d > 10.0) iedge = (Math.abs(x) > Math.abs(z)) ? (x > 0 ? 'east' : 'west') : (z > 0 ? 'south' : 'north');
else iedge = (Math.abs(x) > Math.abs(z)) ? (x > 0 ? 'west' : 'east') : (z > 0 ? 'north' : 'south');
roofStair(x, iy, z, tileMat(x, iy, z), iedge);
}
}
makePyramid(0, 5, 0, tileSolid, 3, true);
pb(0, 8, 0, 'gold_block');
// ============ 9. INTERIORS ============
// -- inner ring: ancestral hall (south segment) + communal kitchen (north segment)
rug(-2, walk1, 9, 5, 3, 'red', 'orange');
pb(0, walk1, 10, 'smooth_stone'); ps(0, walk1 + 1, 10, 'lectern[facing=north]');
pb(-1, walk1, 10, 'stone_brick_wall'); pb(-1, walk1 + 1, 10, 'lantern');
pb(1, walk1, 10, 'stone_brick_wall'); pb(1, walk1 + 1, 10, 'lantern');
ps(-2, 4, 11, 'red_wall_banner[facing=north]'); ps(2, 4, 11, 'red_wall_banner[facing=north]');
chairStair(-2, walk1, 8, 'dark_oak_stairs', 'north'); chairStair(2, walk1, 8, 'dark_oak_stairs', 'north');
ps(-3, walk1, 9, 'flower_pot'); ps(3, walk1, 9, 'flower_pot');
// kitchen (north segment of the inner ring)
ps(0, walk1, -10, 'smoker[facing=south]'); ps(-1, walk1, -10, 'furnace[facing=south,lit=true]');
pb(1, walk1, -10, 'stripped_spruce_log'); ps(1, walk1 + 1, -10, 'spruce_stairs[facing=south,half=top]');
ps(2, walk1, -10, 'water_cauldron[level=3]'); pb(-2, walk1, -10, 'barrel');
pb(0, walk1, -8, 'spruce_fence'); ps(0, walk1 + 1, -8, 'spruce_pressure_plate');
chairStair(-1, walk1, -8, 'spruce_stairs', 'east'); chairStair(1, walk1, -8, 'spruce_stairs', 'west');
pb(0, 5, -9, 'spruce_planks'); ps(0, 4, -9, 'lantern[hanging=true]');
rug(-2, walk1, -7, 5, 2, 'brown', 'yellow');
// -- outer ring, ground floor: workshop (east) + granary (west)
ps(18, walk1, 0, 'loom[facing=west]'); ps(18, walk1, -1, 'crafting_table');
ps(18, walk1, 1, 'smithing_table'); pb(17, walk1, 0, 'barrel'); pb(16, walk1, 1, 'chest');
pb(16, walk1 + 3, 0, 'spruce_planks'); ps(16, walk1 + 2, 0, 'lantern[hanging=true]');
rug(15, walk1, -1, 2, 3, 'green', 'lime');
for (z = -2; z <= 2; z++) { pb(-18, walk1, z, 'hay_block'); pb(-17, walk1, z, 'barrel'); }
pb(-18, walk1 + 1, -1, 'hay_block'); pb(-18, walk1 + 1, 1, 'hay_block');
pb(-16, walk1 + 3, 0, 'spruce_planks'); ps(-16, walk1 + 2, 0, 'lantern[hanging=true]');
// -- upper storeys: bedrooms
placeBed(17, walk2, -1, 'red', 'north'); pb(19, walk2, -1, 'spruce_fence'); pb(19, walk2 + 1, -1, 'lantern');
pb(17, walk2, 2, 'chest'); rug(16, walk2, 0, 3, 2, 'blue', 'light_blue'); shelfWall(19, walk2, 1, 'south', 3, 2);
placeBed(-17, walk2, 1, 'blue', 'south'); pb(-19, walk2, 1, 'spruce_fence'); pb(-19, walk2 + 1, 1, 'lantern');
pb(-17, walk2, -2, 'barrel'); rug(-18, walk2, -1, 2, 3, 'yellow', 'orange');
placeBed(1, walk3, 17, 'brown', 'north'); pb(1, walk3, 19, 'spruce_fence'); pb(1, walk3 + 1, 19, 'lantern');
rug(-1, walk3, 16, 3, 2, 'orange', 'red');
placeBed(17, walk3, -1, 'white', 'north'); pb(19, walk3, -1, 'spruce_fence'); pb(19, walk3 + 1, -1, 'lantern');
pb(17, walk3, 2, 'chest'); ps(16, walk3, 1, 'loom[facing=west]');
// ============ 10. EXTERIOR: terraces, path, bamboo ============
for (z = 27; z <= 35; z++) {
for (x = -2; x <= 2; x++) {
var gy = Math.round(ground(x, z));
pb(x, gy, z, noisePalette([{ block: 'dirt_path', weight: 55 }, { block: 'gravel', weight: 25 }, { block: 'cobblestone', weight: 20 }], { freq: 0.3, seed: 8 })(x, gy, z));
}
}
lampPost(-4, Math.round(ground(-4, 28)) + 1, 28, 4, 'dark_oak_fence');
lampPost(4, Math.round(ground(4, 28)) + 1, 28, 4, 'dark_oak_fence');
lampPost(-4, Math.round(ground(-4, 34)) + 1, 34, 4, 'dark_oak_fence');
lampPost(4, Math.round(ground(4, 34)) + 1, 34, 4, 'dark_oak_fence');
// tea terraces to the east, retained by low stone walls
for (k = 0; k < 4; k++) {
for (z = -12; z <= 12; z++) {
var tx = 29 + k * 2;
var ty = Math.round(ground(tx, z));
pb(tx, ty, z, 'farmland'); ps(tx, ty + 1, z, 'sweet_berry_bush[age=3]');
pb(tx + 1, ty, z, 'stone_bricks');
}
}
// bamboo + trees ring
for (k = 0; k < 30; k++) {
var at = k * 12 * Math.PI / 180;
var bxx = Math.round(Math.cos(at) * (29 + (k % 4) * 1.8));
var bzz = Math.round(Math.sin(at) * (29 + (k % 3) * 2.2));
if (bzz > 24 && Math.abs(bxx) < 6) continue;
if (bxx > 26) continue;
var byy = Math.round(ground(bxx, bzz)) + 1;
if (k % 3 === 0) placeTree(bxx, byy, bzz, 'oak');
else if (k % 3 === 1) { for (y = 0; y < 5; y++) pb(bxx, byy + y, bzz, 'bamboo'); ps(bxx + 1, byy, bzz, 'fern'); }
else { ps(bxx, byy, bzz, 'short_grass'); ps(bxx - 1, byy, bzz, 'poppy'); ps(bxx + 1, byy, bzz, 'azure_bluet'); }
}
// ============ 11. FIX PASS: sellar el faldon (se veia el cielo desde dentro) ============
for (x = -27; x <= 27; x++) {
for (z = -27; z <= 27; z++) {
d = rr(x, z);
var r0 = roofYFor(d);
if (r0 < 0) continue;
var rOut = roofYFor(d + 1.0);
var rIn = roofYFor(d - 1.0);
if (rOut > r0) pb(x, r0 + 1, z, tileSolid(x, r0 + 1, z));
if (rIn > r0 && d < 20.0) pb(x, r0 + 1, z, tileSolid(x, r0 + 1, z));
}
}
// techo sobre los huecos de escalera (los pozos quedaban abiertos al cielo)
for (x = -21; x <= 21; x++) {
for (z = -21; z <= 21; z++) {
d = rr(x, z);
if (d <= 14.6 || d > 18.2) continue;
var inN = (x >= -3 && x <= 3 && z <= -13 && z >= -22);
var inW = (z >= -3 && z <= 3 && x <= -13 && x >= -22);
if (inN || inW) pb(x, innerTopY, z, 'spruce_planks');
}
}