# Compile the lodge plan into sealed shells, circulation, furnishings, and landscape.
from __future__ import annotations
import json
import math
import random
from pathlib import Path
from functools import lru_cache
from .plan import Plan, Volume
STATE_FILE = Path('/work/generator/MCRender/resources/1.21.1-blocks.json')
STATES = json.loads(STATE_FILE.read_text())
STONE = ('stone_bricks',) * 8 + ('andesite', 'cobblestone', 'mossy_stone_bricks')
ROCK = ('stone',) * 9 + ('andesite', 'tuff', 'cobblestone')
PAVING = ('stone_bricks',) * 9 + ('andesite', 'mossy_stone_bricks', 'cobblestone')
ROOF = ('deepslate_tiles',) * 12 + ('deepslate_bricks', 'cobbled_deepslate')
PALETTES = {
'honey': ('smooth_sandstone', 'stripped_spruce_log', 'spruce_planks', 'dark_oak_log', 'yellow_stained_glass', 'orange'),
'ivory': ('calcite', 'stripped_dark_oak_log', 'spruce_planks', 'dark_oak_log', 'white_stained_glass', 'green'),
'ochre': ('smooth_sandstone', 'stripped_oak_log', 'oak_planks', 'spruce_log', 'yellow_stained_glass', 'red'),
}
DIRECTIONS = {'north': (0, -1), 'south': (0, 1), 'east': (1, 0), 'west': (-1, 0)}
@lru_cache(None)
def blockstate(name, properties=()):
"""Validate and canonicalize a vanilla state with explicit default properties."""
name = name.removeprefix('minecraft:')
if name not in STATES:
raise ValueError(f'not a Java 1.21.1 block: {name}')
domains, defaults = STATES[name]
values = dict(defaults)
for key, value in properties:
if key not in domains or str(value) not in domains[key]:
raise ValueError(f'invalid {name}[{key}={value}]')
values[key] = str(value)
return 'minecraft:' + name + ('[' + ','.join(f'{k}={v}' for k, v in sorted(values.items())) + ']' if values else '')
def name_of(state):
return (state or 'minecraft:air').split('[')[0].removeprefix('minecraft:')
class Scene:
"""Sparse local-coordinate scene with shared spatial reservations and evidence."""
def __init__(self, plan: Plan):
self.plan = plan
self.rng = random.Random(plan.seed)
self.blocks = {}
self.reserved = set()
self.walks = set()
self.windows = []
self.doors = []
self.beds = []
self.stairs = []
self.ladders = []
self.rooms = []
self.lights = []
self.attachments = []
self.enclosed = []
self.ground = {}
self.furniture = []
self.plaster, self.timber, self.floor, self.beam, self.glass, self.color = PALETTES[plan.parameters['palette']]
def p(self, x, y, z, name, **props):
self.blocks[x, y, z] = blockstate(name, tuple(sorted(props.items())))
def get(self, x, y, z):
return name_of(self.blocks.get((x, y, z)))
def box(self, bounds, name, **props):
x0, y0, z0, x1, y1, z1 = bounds
for x in range(x0, x1 + 1):
for y in range(y0, y1 + 1):
for z in range(z0, z1 + 1):
self.p(x, y, z, name, **props)
def mix(self, bounds, palette):
x0, y0, z0, x1, y1, z1 = bounds
for x in range(x0, x1 + 1):
for y in range(y0, y1 + 1):
for z in range(z0, z1 + 1):
self.p(x, y, z, self.rng.choice(palette))
def clear(self, bounds):
self.box(bounds, 'air')
def reserve(self, point, headroom=2):
x, y, z = point
self.walks.add(point)
for dy in range(headroom):
self.reserved.add((x, y + dy, z))
def lamp(self, x, y, z, hanging=False):
self.p(x, y, z, 'lantern', hanging=str(hanging).lower())
self.lights.append((x, y, z))
self.attachments.append(((x, y, z), (x, y + (1 if hanging else -1), z)))
def door(self, x, y, z, facing):
for half, dy in (('lower', 0), ('upper', 1)):
self.p(x, y + dy, z, 'spruce_door', half=half, facing=facing)
if self.get(x, y + 2, z) == 'air':
self.p(x, y + 2, z, self.glass)
self.doors.append((x, y, z))
dx, dz = DIRECTIONS[facing]
for side in (-1, 1):
self.reserve((x + side * dx, y, z + side * dz))
def stair(self, x, y, z, facing, material='spruce', walking=False):
self.p(x, y, z, material + '_stairs', facing=facing)
if walking:
self.stairs.append((x, y, z, facing))
self.reserve((x, y + 1, z), 3)
def occupied_elsewhere(self, volume, x, y, z):
return any(v is not volume and v.interior(x, y, z) for v in self.plan.volumes)
def roof_covered(self, volume, x, y, z):
return any(v is not volume and v.contains(x, z) and v.base <= y < v.roof(x, z) for v in self.plan.volumes)
def route_reservations(self):
"""Reserve main axes, portals, furniture access, stairs, and ladder landings."""
p = self.plan
for v in p.volumes:
if v.kind == 'dormer':
for vv in range(v.v0 + 1, v.v1):
self.reserve(v.xyz(v.uc, v.base + 1, vv), 3)
continue
for floor in v.levels:
for u in range(v.uc - 1, v.uc + 2):
for vv in range(v.v0 + 1, v.v1):
x, y, z = v.xyz(u, floor + 1, vv)
if y + 1 < v.roof(x, z):
self.reserve((x, y, z))
# A crossing axis keeps side-wing interfaces connected.
vv = (v.v0 + v.v1) // 2
for u in range(v.u0 + 1, v.u1):
self.reserve(v.xyz(u, floor + 1, vv))
if v.name == 'main':
for floor in v.levels:
for u in range(v.u0 + 2, v.u1 - 1):
for vv in (v.v0 + 1, v.v0 + 2, v.v1 - 2, v.v1 - 1):
self.reserve(v.xyz(u, floor + 1, vv))
sign = -1 if p.parameters['stair_side'] == 'left' else 1
us = (v.uc + sign * 2, v.uc + sign * 3)
for floor in v.levels:
for vv in range(v.v1 - 9, v.v1):
self.reserve(v.xyz(v.uc + sign * 5, floor + 1, vv))
for floor in v.levels[:-1]:
for i in range(6):
vv = v.v1 - 8 + i
for u in us:
x, y, z = v.xyz(u, floor + i + 1, vv)
for yy in range(floor + 1, floor + 10):
self.reserved.add((x, yy, z))
self.reserve((x, y + 1, z), 3)
for u in range(min(us) - 1, max(us) + 2):
for vv in (v.v1 - 9, v.v1 - 2, v.v1 - 1):
self.reserve(v.xyz(u, floor + 1, vv))
self.reserve(v.xyz(u, floor + 7, vv))
elif len(v.levels) > 1:
x, _, z = v.xyz(v.uc, 0, v.v1 - 1)
for y in range(v.base + 1, v.levels[-1] + 3):
self.reserved.add((x, y, z))
px0, pz0, px1, _ = p.porch
post_xs = {px0, px1}
if px1 - px0 > 9:
post_xs.update((p.entrance[0] - 3, p.entrance[0] + 3))
for x in post_xs:
for z in (pz0 - 1, pz0):
for y in range(p.volumes[0].base + 1, p.volumes[0].base + 5):
self.reserved.add((x, y, z))
for v in p.volumes:
if v.kind == 'stone':
f = v.levels[-1]
zz = (v.z0 + v.z1) // 2
for x in range(v.x1 - 1, v.x1 + 3):
for z in range(zz - 2, zz + 3):
self.reserve((x, f + 1, z), 3)
for link in p.links:
x, z, f = link['x'], link['z'], link['floor']
for dx in range(-2, 3):
for dz in range(-2, 3):
self.reserve((x + dx, f + 1, z + dz), 3)
ex, ey, ez = p.entrance
for x in range(ex - 1, ex + 2):
for z in range(ez - 4, ez + 4):
self.reserve((x, ey, z), 3)
if p.parameters['balcony']:
for x in range(ex - 3, ex + 4):
for z in range(-3, 4):
self.reserve((x, ey + 6, z), 3)
def terrain(self):
"""Grow solid crag columns and an irregular garden island around the plan."""
plan = self.plan
base = plan.volumes[0].base
ox, _, oz = plan.origin
xmin, xmax = 2 - ox, plan.size_xyz[0] - ox - 3
zmin, zmax = 2 - oz, plan.size_xyz[2] - oz - 3
masses = [v for v in plan.volumes if v.kind != 'dormer']
cx = (xmin + xmax) / 2
cz = (zmin + zmax) / 2
for x in range(xmin, xmax + 1):
for z in range(zmin, zmax + 1):
corner = max(0, xmin + 4 - x, x - xmax + 4) + max(0, zmin + 4 - z, z - zmax + 4)
if corner > 4 + self.rng.randrange(2):
continue
dist = min(max(v.x0 - x, 0, x - v.x1) + max(v.z0 - z, 0, z - v.z1) for v in masses)
h = 1
if dist < 6 and z >= plan.terrace_front:
h = max(1, base - 1 - max(0, dist - 2) * (3 if base > 10 else 1))
if dist <= 2:
h = base
if plan.porch[0] - 2 <= x <= plan.porch[2] + 2 and plan.terrace_front <= z < 1:
h = base
if x in (xmin, xmax) or z in (zmin, zmax):
h = 1
seam = self.rng.choice(('stone', 'stone', 'andesite', 'tuff'))
for y in range(h):
self.p(x, y, z, self.rng.choice((seam,) * 15 + ROCK) if base > 10 or y < h - 2 else 'dirt')
self.p(x, h, z, self.rng.choice(('grass_block',) * 6 + ('moss_block', 'coarse_dirt')))
self.ground[x, z] = h
# Foundations, walks, retaining edges: all connected to terrain at y=0.
for v in masses:
self.mix((v.x0, 0, v.z0, v.x1, base - 1, v.z1), STONE)
for x in range(plan.porch[0] - 1, plan.porch[2] + 2):
for z in range(plan.terrace_front, 0):
self.mix((x, 0, z, x, base, z), STONE)
self.p(x, base, z, self.rng.choice(PAVING))
self.ground[x, z] = base
for x, z in list(self.ground):
h = self.ground[x, z]
if h == base and not any(v.contains(x, z) for v in masses):
self.p(x, h, z, self.rng.choice(PAVING))
edge = any(self.ground.get((x + dx, z + dz), -1) < base - 1 for dx, dz in DIRECTIONS.values())
if edge and abs(x - plan.entrance[0]) > 3:
self.p(x, h + 1, z, 'cobblestone_wall')
if (x + z) % 6 == 0:
self.p(x, h + 1, z, 'stone_bricks')
self.p(x, h + 2, z, 'stone_brick_slab')
for patch in plan.approach:
x, z, y = patch['x'], patch['z'], patch['y']
for xx in range(x - 2, x + 3):
self.mix((xx, 0, z, xx, y - 1, z), STONE)
self.clear((xx, y, z, xx, max(base + 2, y + 3), z))
if patch['kind'] == 'step':
self.stair(xx, y, z, 'south', 'stone_brick', True)
else:
self.p(xx, y, z, self.rng.choice(PAVING))
self.reserve((xx, y + 1, z), 3)
self.ground[xx, z] = y
if z < plan.terrace_front:
for xx in (x - 3, x + 3):
self.mix((xx, 0, z, xx, y, z), STONE)
self.p(xx, y + 1, z, 'stone_brick_slab')
if (z - plan.approach[0]['z']) % 5 == 0:
self.p(xx, y + 1, z, 'stone_bricks')
self.lamp(xx, y + 2, z)
def shells(self):
"""Union room envelopes before placing roof planes and real timber frames."""
for v in self.plan.volumes:
for f in v.levels:
self.box((v.x0, f, v.z0, v.x1, f, v.z1), self.floor if v.kind != 'glass' else 'polished_andesite')
for x in range(v.x0, v.x1 + 1):
for z in range(v.z0, v.z1 + 1):
if x not in (v.x0, v.x1) and z not in (v.z0, v.z1):
continue
u, vv = v.uv(x, z)
top = v.roof(x, z)
for y in range(v.base + 1, top):
if self.occupied_elsewhere(v, x, y, z):
continue
frame = x in (v.x0, v.x1) and z in (v.z0, v.z1)
frame |= ((x - v.x0) % 4 == 0 and z in (v.z0, v.z1)) or ((z - v.z0) % 4 == 0 and x in (v.x0, v.x1))
belt = y in v.levels or y == v.levels[-1] + (1 if v.attic else 4)
name = self.timber if frame else self.beam if belt else 'glass' if v.kind == 'glass' else self.rng.choice(STONE) if v.kind == 'stone' else self.plaster
self.p(x, y, z, name, **({'axis': 'y' if frame else 'x' if z in (v.z0, v.z1) else 'z'} if name.endswith('log') else {}))
if v.kind not in ('glass', 'stone', 'timber'):
# Timber follows both gable verges below the slate.
if vv in (v.v0, v.v1) and not self.occupied_elsewhere(v, x, top - 1, z):
self.p(x, top - 1, z, self.timber, axis=v.axis)
for f in v.levels:
for x in range(v.x0, v.x1 + 1):
for z in (v.z0, v.z1):
if not self.occupied_elsewhere(v, x, f, z):
self.p(x, f, z, self.beam, axis='x')
for z in range(v.z0, v.z1 + 1):
for x in (v.x0, v.x1):
if not self.occupied_elsewhere(v, x, f, z):
self.p(x, f, z, self.beam, axis='z')
for v in self.plan.volumes:
overhang = 1 if v.kind == 'glass' else 2
for x in range(v.x0 - overhang, v.x1 + overhang + 1):
for z in range(v.z0 - overhang, v.z1 + overhang + 1):
h = v.roof(x, z)
u, vv = v.uv(x, z)
toward = 'east' if u < v.uc else 'west'
if v.axis == 'x':
toward = 'south' if u < v.uc else 'north'
if v.kind in ('stone', 'timber', 'glass'):
ds = [(x - v.x0, 'east'), (v.x1 - x, 'west'), (z - v.z0, 'south'), (v.z1 - z, 'north')]
toward = min(ds)[1]
dx, dz = DIRECTIONS[toward]
low = min(h, v.roof(x - dx, z - dz))
for y in range(low, h + 1):
if not self.roof_covered(v, x, y, z):
self.p(x, y, z, 'glass' if v.kind == 'glass' else self.rng.choice(ROOF))
if v.kind != 'glass' and not self.roof_covered(v, x, h + 1, z):
if u == v.uc and v.kind not in ('stone', 'timber'):
self.p(x, h + 1, z, 'deepslate_tile_slab')
else:
edge = x in (v.x0 - overhang, v.x1 + overhang) or z in (v.z0 - overhang, v.z1 + overhang)
self.stair(x, h + 1, z, toward, 'cobbled_deepslate' if edge else 'deepslate_tile')
# Carved ridge ends, anchored into the roof cap.
if v.kind not in ('glass', 'stone', 'timber'):
for vv in (v.v0 - 2, v.v1 + 2):
x, _, z = v.xyz(v.uc, 0, vv)
h = v.roof(x, z)
if not self.roof_covered(v, x, h + 2, z):
self.p(x, h + 1, z, self.timber, axis=v.axis)
self.p(x, h + 2, z, 'oak_fence')
if v.name == 'main' and self.plan.parameters['profile'] != 'chalet':
self.p(x, h + 3, z, 'lightning_rod')
def opening(self, x, f, z, face, width=2):
"""Glaze an exterior bay only when it has clear indoor and outdoor space."""
dx, dz = DIRECTIONS[face]
cells = [(x + (i if dz else 0), y, z + (i if dx else 0)) for i in range(width) for y in (f + 2, f + 3)]
inside = [(a - dx, b, c - dz) for a, b, c in cells]
outside = [(a + dx, b, c + dz) for a, b, c in cells]
if any(c in self.reserved for c in cells):
return
if any(any(v.interior(*q) for v in self.plan.volumes) for q in outside):
return
if any(self.get(*q) not in ('air',) for q in inside + outside):
return
if any(self.get(*q) in ('air', 'glass') for q in cells):
return
for q in cells:
self.p(*q, self.glass)
self.windows.append({'cells': cells, 'inside': inside, 'outside': outside})
self.reserved.update(inside + outside + cells)
for i in range(width):
xx, zz = x + (i if dz else 0), z + (i if dx else 0)
self.p(xx + dx, f + 1, zz + dz, 'spruce_trapdoor', facing=face, half='top')
self.attachments.append(((xx + dx, f + 1, zz + dz), (xx, f + 1, zz)))
# Compact shutters anchor to wall blocks, never cover the glazing.
for i in (-1, width):
xx, zz = x + (i if dz else 0), z + (i if dx else 0)
for y in (f + 2, f + 3):
q = (xx + dx, y, zz + dz)
if self.get(xx, y, zz) != 'air' and self.get(*q) == 'air' and q not in self.reserved:
self.p(*q, 'spruce_trapdoor', facing=face, open='true')
self.attachments.append((q, (xx, y, zz)))
def gable_details(self):
"""Frame vaulted attic gables with tall paired lights and collar beams."""
v = self.plan.volumes[0]
f = v.levels[-1]
for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')):
dx, dz = DIRECTIONS[face]
peak = v.roof(*v.xyz(v.uc, 0, vv)[::2])
for y in (f + 6, f + 12):
for u in range(v.u0 + 2, v.u1 - 1):
x, _, z = v.xyz(u, 0, vv)
if y < v.roof(x, z) - 2 and not self.occupied_elsewhere(v, x, y, z):
self.p(x, y, z, self.beam, axis='x' if v.axis == 'z' else 'z')
for u in (v.uc - 1, v.uc + 1):
x, _, z = v.xyz(u, 0, vv)
top = min(f + 9, v.roof(x, z) - 3)
cells = [(x, y, z) for y in range(f + 3, top + 1)]
inside = [(x - dx, y, z - dz) for _, y, _ in cells]
outside = [(x + dx, y, z + dz) for _, y, _ in cells]
if cells and all(q not in self.reserved and self.get(*q) == 'air' for q in inside + outside) and not any(any(other.interior(*q) for other in self.plan.volumes) for q in outside):
for q in cells:
self.p(*q, self.glass)
self.windows.append({'cells': cells, 'inside': inside, 'outside': outside})
self.reserved.update(cells + inside + outside)
x, _, z = v.xyz(v.uc, 0, vv)
for y in range(f + 2, peak):
if not self.occupied_elsewhere(v, x, y, z):
self.p(x, y, z, self.timber, axis='y')
# Corbels and small low lamps make the timber frame read in relief.
for v in self.plan.volumes:
if v.kind in ('glass', 'dormer'):
continue
for f in v.levels[1:]:
for x in range(v.x0, v.x1 + 1, 4):
for z, face in ((v.z0 - 1, 'south'), (v.z1 + 1, 'north')):
dx, dz = DIRECTIONS[face]
if self.get(x + dx, f - 1, z + dz) != 'air':
self.exterior_p(x, f - 1, z, 'spruce_stairs', facing=face, half='top')
for x in (v.x0, v.x1):
z = v.z0 - 1
y = v.base + 3
if all(self.get(x, yy, z) == 'air' and (x, yy, z) not in self.reserved for yy in (y, y + 1)) and not any(other.contains(x, z) for other in self.plan.volumes):
self.p(x, y + 1, z, self.beam, axis='z')
self.lamp(x, y, z, True)
def glazing(self):
for v in self.plan.volumes:
if v.kind == 'glass':
continue
for f in v.levels:
for face, z in (('north', v.z0), ('south', v.z1)):
for x in range(v.x0 + 2, v.x1 - 1, 4):
self.opening(x, f, z, face)
for face, x in (('west', v.x0), ('east', v.x1)):
for z in range(v.z0 + 2, v.z1 - 1, 4):
self.opening(x, f, z, face)
if v.attic:
for vv, face in ((v.v0, 'north' if v.axis == 'z' else 'west'), (v.v1, 'south' if v.axis == 'z' else 'east')):
x, f, z = v.xyz(v.uc - 1, v.levels[-1], vv)
self.opening(x, f + 1, z, face, 3)
def openings(self):
for link in self.plan.links:
x, z, f = link['x'], link['z'], link['floor']
if link['axis'] == 'x':
self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1))
self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor)
else:
self.clear((x - 1, f + 1, z - 1, x + 1, f + 3, z + 1))
self.box((x - 1, f, z - 1, x + 1, f, z + 1), self.floor)
x, y, z = self.plan.entrance
self.clear((x, y, z - 1, x, y + 2, z + 1))
self.door(x, y, z, 'north')
for xx in (x - 1, x + 1):
self.box((xx, y, z - 1, xx, y + 2, z - 1), self.timber, axis='y')
self.box((x - 1, y + 3, z - 1, x + 1, y + 3, z - 1), self.beam, axis='x')
def circulation(self):
"""Cut stacked stairs and supported ladders after all intersecting roofs."""
for v in self.plan.volumes:
if v.name == 'main':
sign = -1 if self.plan.parameters['stair_side'] == 'left' else 1
for f in v.levels[:-1]:
for i in range(6):
for u in (v.uc + 2 * sign, v.uc + 3 * sign):
x, y, z = v.xyz(u, f + i + 1, v.v1 - 8 + i)
self.clear((x, y + 1, z, x, y + 3, z))
self.p(x, y - 1, z, self.floor)
self.stair(x, y, z, 'south' if v.axis == 'z' else 'east', walking=True)
for u in (v.uc + 2 * sign, v.uc + 3 * sign):
for vv in (v.v1 - 2, v.v1 - 1):
x, _, z = v.xyz(u, 0, vv)
self.p(x, f + 6, z, self.floor)
self.clear((x, f + 7, z, x, f + 9, z))
# Outer guard on each stair opening, leaving return aisles clear.
for f in v.levels[1:]:
for vv in range(v.v1 - 7, v.v1 - 2):
u = v.uc + 4 * sign
x, y, z = v.xyz(u, f + 1, vv)
if self.get(x, f, z) != 'air' and (x, y, z) not in self.reserved and y + 2 < v.roof(x, z):
self.p(x, y, z, 'spruce_fence')
elif len(v.levels) > 1:
x, _, z = v.xyz(v.uc, 0, v.v1 - 1)
bx, _, bz = v.xyz(v.uc, 0, v.v1)
self.box((bx, v.base + 1, bz, bx, v.levels[-1] + 3, bz), self.beam, axis='y')
for y in range(v.base + 1, v.levels[-1] + 3):
self.p(x, y, z, 'ladder', facing='north' if v.axis == 'z' else 'west')
self.ladders.append((x, y, z))
for f in v.levels:
u = v.uc - 1
xx, yy, zz = v.xyz(u, f + 1, v.v1 - 1)
self.clear((xx, yy, zz, xx, yy + 1, zz))
self.reserve((xx, yy, zz))
def room(self, v, f, purpose, bounds, target):
self.rooms.append({'volume': v.name, 'floor': f, 'purpose': purpose, 'bounds': bounds, 'target': target, 'furniture': [], 'lights': []})
self.reserve(target)
return self.rooms[-1]
def furnish_cell(self, room, x, y, z, name, **props):
if (x, y, z) in self.reserved or self.get(x, y, z) != 'air' or self.get(x, y - 1, z) == 'air':
return False
self.p(x, y, z, name, **props)
room['furniture'].append((x, y, z))
self.furniture.append((x, y, z))
return True
def bed(self, room, x, y, z, facing):
dx, dz = DIRECTIONS[facing]
pts = [(x, y, z), (x + dx, y, z + dz)]
if any(q in self.reserved or self.get(*q) != 'air' or not self.get(q[0], y - 1, q[2]) or self.get(q[0], y - 1, q[2]) == 'air' or self.get(q[0], y + 1, q[2]) != 'air' for q in pts):
return False
for q, part in zip(pts, ('foot', 'head')):
self.p(*q, self.color + '_bed', facing=facing, part=part)
room['furniture'].append(q)
self.beds.append(pts[0])
for xx, yy, zz in pts:
for a, b in DIRECTIONS.values():
if all(self.get(xx + a, yy + dy, zz + b) == 'air' for dy in (0, 1)):
self.reserve((xx + a, yy, zz + b))
return True
def interiors(self):
"""Allocate functions to actual rooms, with layouts responding to the plan."""
for v in self.plan.volumes:
if v.kind == 'dormer':
# Its floor continues the furnished attic, with a useful window bench.
continue
for level, f in enumerate(v.levels):
layout = self.plan.parameters['layout']
vc = (v.v0 + v.v1) // 2 + (2 if layout == 'suites' else 0)
segments = [(v.v0 + 1, v.v1 - 1)]
if v.name == 'main' and level < v.floors and layout != 'open':
segments = [(v.v0 + 1, vc - 1), (vc + 1, v.v1 - 1)]
for u in range(v.u0 + 1, v.u1):
for y in range(f + 1, f + 6):
x, _, z = v.xyz(u, y, vc)
if abs(u - v.uc) <= 1 or (x, y, z) in self.reserved:
continue
self.p(x, y, z, self.timber if u % 4 == 0 else self.plaster, **({'axis': 'y'} if u % 4 == 0 else {}))
# A framed three-wide arch leaves the central hall continuous.
for u in range(v.uc - 1, v.uc + 2):
x, _, z = v.xyz(u, 0, vc)
self.p(x, f + 4, z, self.beam, axis='x' if v.axis == 'z' else 'z')
if layout == 'suites' and level > 0:
for u in (v.uc - 1, v.uc + 1):
for yy in range(f + 1, f + 6):
self.p(*v.xyz(u, yy, vc), self.timber, axis='y')
self.door(*v.xyz(v.uc, f + 1, vc), 'south' if v.axis == 'z' else 'east')
for index, (va, vb) in enumerate(segments):
if v.kind == 'glass':
purpose = 'winter garden and herbal workroom'
elif v.name == 'main':
purpose = ('hearth kitchen and dining', 'parlour and pantry')[index] if level == 0 else 'bedchamber and study' if level == len(v.levels) - 1 else ('guest suite' if layout == 'suites' else 'library and workroom')
elif v.name == 'tower':
purpose = ('pantry', 'map room', 'watchkeeper bedroom', 'archive', 'lookout study')[min(level, 4)]
else:
purpose = 'guest bedroom' if level else self.rng.choice(('craft workshop', 'dining parlour', 'library'))
ua, ub = v.u0 + 1, v.u1 - 1
target = v.xyz(v.uc, f + 1, (va + vb) // 2)
x0, _, z0 = v.xyz(ua, 0, va)
x1, _, z1 = v.xyz(ub, 0, vb)
room = self.room(v, f, purpose, (x0, f + 1, z0, x1, f + 4, z1), target)
# Furnishing banks on both sides of the protected longitudinal hall.
spots = []
for u in (ua + 1, ub - 1):
for vv in range(va + 1, vb, 2):
x, y, z = v.xyz(u, f + 1, vv)
if y + 3 < v.roof(x, z) and all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)):
spots.append((x, y, z))
# The minimum room width always admits perimeter furniture, even at ladders.
for x in range(x0, x1 + 1):
for z in range(z0, z1 + 1):
y = f + 1
if all((x, y + dy, z) not in self.reserved and self.get(x, y + dy, z) == 'air' for dy in (0, 1)) and self.get(x, f, z) != 'air':
spots.append((x, y, z))
# Bound furniture density and retain walkable gaps, including low attics.
spaced = []
for q in spots:
if all(abs(q[0] - r[0]) + abs(q[2] - r[2]) >= 3 for r in spaced):
spaced.append(q)
spots = spaced[:max(4, min(12, ((ub - ua + 1) * (vb - va + 1)) // 16))]
if not spots:
raise AssertionError(f'no furnishing positions in {v.name} {f}')
# Every room has a low supported lamp and a second task light when large.
for lamp_index in sorted(set((0, len(spots) - 1))):
x, y, z = spots[lamp_index]
self.p(x, y, z, 'barrel', facing='up')
room['furniture'].append((x, y, z))
self.lamp(x, y + 1, z)
room['lights'].append((x, y + 1, z))
candidates = spots[1:-1] or spots[1:]
wants_bed = 'bed' in purpose or 'suite' in purpose
if wants_bed:
found = False
for x, y, z in candidates:
if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'):
found = True
break
if not found:
for u in range(ua, ub + 1):
for vv in range(va, vb):
x, y, z = v.xyz(u, f + 1, vv)
if self.bed(room, x, y, z, 'south' if v.axis == 'z' else 'east'):
found = True
break
if found:
break
assert found, f'bed has no valid location in {v.name} {f}'
furnishings = ('smoker', 'crafting_table', 'barrel', 'furnace', 'bookshelf') if 'kitchen' in purpose else ('composter', 'moss_block', 'barrel', 'crafting_table') if v.kind == 'glass' else ('bookshelf', 'cartography_table', 'barrel', 'loom', 'chest')
for i, (x, y, z) in enumerate(candidates):
name = furnishings[i % len(furnishings)]
props = {'facing': 'south'} if name in ('smoker', 'furnace', 'chest', 'loom', 'barrel') else {}
if name in ('smoker', 'furnace'):
props['lit'] = 'true'
if self.furnish_cell(room, x, y, z, name, **props):
if i % 2 == 0 and name not in ('composter', 'chest') and (x, y + 1, z) not in self.reserved:
if name == 'bookshelf' and self.get(x, y + 2, z) == 'air':
self.p(x, y + 1, z, 'bookshelf')
else:
self.p(x, y + 1, z, 'potted_fern' if v.kind != 'glass' else self.rng.choice(('potted_azalea_bush', 'potted_poppy', 'potted_dandelion')))
self.attachments.append(((x, y + 1, z), (x, y, z)))
# A table and facing seat fill a front-side bay without covering the hall.
for offset in (() if wants_bed else (-3, 3)):
x, y, z = v.xyz(v.uc + offset, f + 1, va + 1)
if self.furnish_cell(room, x, y, z, self.floor):
if (x, y + 1, z) not in self.reserved and self.get(x, y + 1, z) == 'air':
self.p(x, y + 1, z, 'potted_spruce_sapling')
self.attachments.append(((x, y + 1, z), (x, y, z)))
sx, sy, sz = v.xyz(v.uc + offset, f + 1, va + 2)
self.furnish_cell(room, sx, sy, sz, 'spruce_stairs', facing='north' if v.axis == 'z' else 'west')
# Wool inlays provide colour without reducing headroom.
for u in range(v.uc - 1, v.uc + 2):
for vv in range(va + 2, min(vb, va + 5)):
x, _, z = v.xyz(u, 0, vv)
if self.get(x, f, z).endswith('planks'):
self.p(x, f, z, self.color + '_wool')
self.enclosed.append(target)
def dormer_nooks(self):
"""Furnish and verify the accessible reading bays extending the attic."""
main = self.plan.volumes[0]
for v in self.plan.volumes:
if v.kind != 'dormer':
continue
exterior_v = v.v0 + 1 if v.v0 < main.uc else v.v1 - 1
target = v.xyz(v.uc, v.base + 1, exterior_v)
room = self.room(v, v.base, 'dormer reading nook', (v.x0 + 1, v.base + 1, v.z0 + 1, v.x1 - 1, v.base + 4, v.z1 - 1), target)
# The parent attic remains open behind the bay. Its own small lamp is near the floor.
for vv in range(v.v0 + 1, v.v1):
for u in (v.uc - 1, v.uc + 1):
x, y, z = v.xyz(u, v.base + 1, vv)
if all(self.get(x, y + dy, z) == 'air' and (x, y + dy, z) not in self.reserved for dy in (0, 1)):
self.p(x, y, z, 'bookshelf')
self.lamp(x, y + 1, z)
room['furniture'].append((x, y, z))
room['lights'].append((x, y + 1, z))
break
if room['lights']:
break
assert room['lights'], f'no task-light position for {v.name}'
self.enclosed.append(target)
def chimney(self):
"""Run a masonry chimney from a supported hearth through the roof shoulder."""
v = self.plan.volumes[0]
x, z, f = v.x0 + 1, v.z1 - 2, v.base
h = max(v.roof(x, z), v.roof(x + 1, z)) + 5
self.mix((x, f + 1, z, x + 1, h, z + 1), STONE)
for yy in (h - 3, h):
self.box((x - 1, yy, z - 1, x + 2, yy, z + 2), 'stone_bricks')
self.p(x, h + 1, z, 'campfire', lit='true')
for xx, zz in ((x - 1, z - 1), (x + 2, z - 1), (x - 1, z + 2), (x + 2, z + 2)):
self.p(xx, h + 1, zz, 'stone_brick_wall')
self.box((x - 1, h + 2, z - 1, x + 2, h + 2, z + 2), 'stone_bricks')
# A contained hearth opens to the room, with a proper masonry hood.
for xx in (x, x + 1):
self.p(xx, f + 1, z, 'campfire', lit='true')
self.p(xx, f + 2, z, 'air')
self.p(xx, f + 1, z - 1, 'iron_bars', east='true', west='true')
self.p(xx, f + 2, z - 1, 'iron_bars', east='true', west='true')
self.box((x, f + 3, z - 1, x + 1, f + 3, z - 1), 'stone_bricks')
def exterior_p(self, x, y, z, name, **props):
if any(v.contains(x, z) and v.base < y <= v.roof(x, z) + 1 for v in self.plan.volumes):
return
if (x, y, z) in self.reserved or self.get(x, y, z) != 'air':
return
self.p(x, y, z, name, **props)
def porches(self):
"""Build post-supported canopies, optional balcony, and an open side gallery."""
p = self.plan
f = p.volumes[0].base
ex = p.entrance[0]
x0, z0, x1, z1 = p.porch
self.box((x0, f, z0, x1, f, z1), self.floor)
for x in range(x0 - 1, x1 + 2):
for z in range(z0 - 1, 0):
balcony = p.parameters['balcony'] and ex - 3 <= x <= ex + 3 and -3 <= z <= -1
if not balcony:
h = f + 5 + (z - z0) // 2
self.exterior_p(x, h, z, self.rng.choice(ROOF))
self.exterior_p(x, h + 1, z, 'deepslate_tile_slab')
posts = {x0, x1}
if x1 - x0 > 9:
posts.update((ex - 3, ex + 3))
for x in sorted(posts):
if abs(x - ex) <= 1 or any(v.contains(x, z0) for v in p.volumes):
continue
self.mix((x, 0, z0, x, f, z0), STONE)
self.box((x, f + 1, z0, x, f + 4, z0), self.timber, axis='y')
self.p(x, f + 4, z0 - 1, self.floor)
self.lamp(x, f + 3, z0 - 1, True)
for dx in (-1, 1):
if x0 <= x + dx <= x1:
self.p(x + dx, f + 4, z0, 'spruce_stairs', facing='east' if dx < 0 else 'west', half='top')
for x in range(x0, x1 + 1):
self.exterior_p(x, f + 4, z0, self.beam, axis='x')
for x in range(x0 + 1, x1):
if abs(x - ex) > 2:
self.p(x, f + 1, z0, 'spruce_fence')
if p.parameters['balcony']:
self.box((ex - 3, f + 6, -3, ex + 3, f + 6, -1), self.floor)
for x in range(ex - 3, ex + 4):
self.p(x, f + 7, -3, 'spruce_fence')
for x in (ex - 3, ex + 3):
self.box((x, f + 1, -3, x, f + 6, -3), self.timber, axis='y')
for z in (-2, -1):
self.p(x, f + 7, z, 'spruce_fence')
self.clear((ex, f + 7, -2, ex, f + 9, 1))
self.door(ex, f + 7, 0, 'north')
self.rooms.append({'volume': 'balcony', 'floor': f + 6, 'purpose': 'open balcony', 'bounds': (ex - 2, f + 7, -2, ex + 2, f + 9, -1), 'target': (ex, f + 7, -2), 'furniture': [], 'lights': []})
self.p(ex - 2, f + 7, -1, 'barrel', facing='up')
self.lamp(ex - 2, f + 8, -1)
self.rooms[-1]['furniture'].append((ex - 2, f + 7, -1))
self.rooms[-1]['lights'].append((ex - 2, f + 8, -1))
if p.parameters['porch'] == 'wrap':
main = p.volumes[0]
# Choose a free facade interface at planning time.
side = p.parameters['wrap_side']
xa, xb = (-4, -1) if side == 'left' else (main.x1 + 1, main.x1 + 4)
for x in range(xa, xb + 1):
for z in range(-3, main.z1 + 1):
self.mix((x, 0, z, x, f - 1, z), STONE)
self.p(x, f, z, self.floor)
h = f + 5 + (x - xa) // 2 if side == 'left' else f + 5 + (xb - x) // 2
self.exterior_p(x, h, z, self.rng.choice(ROOF))
self.exterior_p(x, h + 1, z, 'deepslate_tile_slab')
outer = xa if side == 'left' else xb
for z in range(-2, main.z1 + 1, 5):
self.box((outer, f + 1, z, outer, f + 4, z), self.timber, axis='y')
self.p(outer, f + 5, z, self.beam, axis='z')
self.lamp(outer, f + 3, z + 1, True)
self.p(outer, f + 4, z + 1, self.floor)
for z in range(0, main.z1):
if self.get(outer, f + 1, z) == 'air':
self.p(outer, f + 1, z, 'spruce_fence')
# Stone tower lookout: a glazed room with an east balcony and railed ledges.
for v in p.volumes:
if v.kind != 'stone':
continue
top = v.levels[-1]
for x in range(v.x0 - 1, v.x1 + 2):
for z in range(v.z0 - 1, v.z1 + 2):
if v.contains(x, z) or any(other is not v and other.contains(x, z, 2) and top <= other.roof(x, z) + 2 for other in p.volumes):
continue
self.p(x, top, z, self.floor)
self.p(x, top + 1, z, 'oak_fence')
if x in (v.x0 - 1, v.x1 + 1) and z in (v.z0, v.z1):
self.p(x, top - 1, z, 'spruce_stairs', facing='east' if x < v.x0 else 'west', half='top')
# East gallery has a two-wide deck so the rail does not block the door.
zz = (v.z0 + v.z1) // 2
for z in range(zz - 2, zz + 3):
self.p(v.x1 + 2, top, z, self.floor)
self.p(v.x1 + 2, top + 1, z, 'oak_fence')
self.p(v.x1 + 1, top + 1, z, 'air')
self.door(v.x1, top + 1, zz, 'east')
self.reserve((v.x1 - 1, top + 1, zz))
def tree(self, x, z, height):
base = self.ground[x, z]
self.box((x, base + 1, z, x, base + height - 2, z), 'spruce_log', axis='y')
for rise in range(4, height, 3):
radius = max(1, min(3, (height - rise + 3) // 5))
for dx, dz in DIRECTIONS.values():
for i in range(1, radius):
xx, yy, zz = x + dx * i, base + rise, z + dz * i
if self.get(xx, yy, zz) == 'air':
self.p(xx, yy, zz, 'spruce_log', axis='x' if dx else 'z')
for dy in (0, 1):
r = max(1, radius - dy)
for dx in range(-r, r + 1):
for dz in range(-r, r + 1):
if abs(dx) + abs(dz) > r + 1 or (abs(dx) == abs(dz) == r and r > 1):
continue
xx, yy, zz = x + dx, base + rise + dy, z + dz
if self.get(xx, yy, zz) == 'air' and (xx, yy, zz) not in self.reserved:
self.p(xx, yy, zz, 'spruce_leaves', persistent='true')
for yy in range(base + height - 1, base + height + 2):
self.p(x, yy, z, 'spruce_leaves', persistent='true')
def landscape(self):
"""Plant grounded conifers and garden borders while preserving every opening."""
for x, z, height in self.plan.tree_sites:
self.tree(x, z, height)
for (x, z), y in sorted(self.ground.items()):
if self.get(x, y, z) not in ('grass_block', 'moss_block', 'coarse_dirt') or self.get(x, y + 1, z) != 'air' or (x, y + 1, z) in self.reserved:
continue
n = self.rng.random()
if n < .14:
self.p(x, y + 1, z, self.rng.choice(('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet')))
self.attachments.append(((x, y + 1, z), (x, y, z)))
elif n < .20:
self.p(x, y + 1, z, self.rng.choice(('azalea_leaves', 'flowering_azalea_leaves')), persistent='true')
elif n < .23:
self.p(x, y + 1, z, 'mossy_cobblestone')
# Vertical ivy grows only against an actual solid corner post.
for v in self.plan.volumes:
if v.kind in ('dormer', 'glass'):
continue
for x, z, face in ((v.x0, v.z0 - 1, 'south'), (v.x1 + 1, v.z1, 'west')):
dx, dz = DIRECTIONS[face]
for y in range(v.base + 1, min(v.levels[-1] + 4, v.base + 13)):
if self.get(x, y, z) == 'air' and (x, y, z) not in self.reserved and self.get(x + dx, y, z + dz).endswith('log'):
self.p(x, y, z, 'vine', **{face: 'true'})
self.attachments.append(((x, y, z), (x + dx, y, z + dz)))
# Entrance heraldry has physical textile blocks, visible without block entity NBT.
if self.plan.parameters['floors'] >= 2:
v = self.plan.volumes[0]
for x in (v.x0 + 1, v.x1 - 1):
for y in range(v.base + 8, v.base + 12):
z = -1
if (x, y, z) not in self.reserved and self.get(x, y, z) == 'air' and self.get(x, y, 0) != 'air':
self.p(x, y, z, self.color + '_wool')
self.p(x, v.base + 12, -1, self.beam, axis='z')
def connections(self):
"""Resolve fence arms from final neighboring geometry."""
for (x, y, z), state in list(self.blocks.items()):
name = name_of(state)
if name.endswith('_fence'):
props = {}
for face, (dx, dz) in DIRECTIONS.items():
neighbor = self.get(x + dx, y, z + dz)
props[face] = 'true' if neighbor.endswith(('_fence', '_log', '_planks')) or neighbor in ('stone_bricks', 'cobblestone') else 'false'
self.p(x, y, z, name, **props)
def build(self):
self.route_reservations()
v = self.plan.volumes[0]
for x in range(v.x0, v.x0 + 4):
for z in range(v.z1 - 3, v.z1 + 2):
for y in range(v.base + 1, self.plan.size_xyz[1]):
self.reserved.add((x, y, z))
self.terrain()
self.shells()
self.openings()
self.circulation()
self.chimney()
self.porches()
self.gable_details()
self.glazing()
self.interiors()
self.dormer_nooks()
self.landscape()
self.connections()
return self