extends RefCounted
## Crownhaven / คราวน์เฮเวน (นครหลวง) - the central capital (owner 2026-09-30, docs/PAUSE_NOTES.md "New towns"): the
## biggest town, a trade hub and gathering place, reached by the warpers of every town. Our own design (generic
## fantasy capital, nothing copied): a walled city (4 gatehouses) with two avenues crossing at the Grand Plaza (the
## Crown Star fountain, banners, flower beds, benches), Market Row (striped-awning stalls, bunting), the merchant
## quarter, the Guild Hall (clock tower, banners), the Grand Bank (columns, dome), the Warp Hub (rune dais, six
## crystal pillars), the Party Square (gazebo), the Merchant Plots (space kept for player shops), tree planters,
## hedges, lamps and building signs.
## TOWN is merged into env_towns4.gd TOWNS (theme "capital"): houses / lamps / pieces go through env_village.gd +
## env_towns4.gd like the other towns; this file draws the capital's own piece kinds (piece()), its walls + bunting
## (build_extra()) and makes their colliders (colliders(): client + the server's collider-only build).
## Performance: everything static is merged into one vertex-coloured mesh (one draw call), glows are MultiMeshes,
## flowers / tree canopies thin with the scenery density (config env_density), about 20 night lights.

const EnvMesh = preload("res://scripts/env_mesh.gd")
const EnvNature = preload("res://scripts/env_nature.gd")
const L = preload("res://scripts/env_layout.gd")
const Kanit = preload("res://scripts/ui/kanit.gd")
const WATER = preload("res://shaders/env_water.gdshader")
const EnvTime = preload("res://scripts/env_time.gd")
const EnvLife = preload("res://scripts/env_life.gd")

const CREAM := Color(1.0, 0.95, 0.86)
const STONE := Color(0.86, 0.84, 0.8)
const STONE_DARK := Color(0.66, 0.64, 0.66)
const WALL_STONE := Color(0.84, 0.8, 0.74)
const ROYAL := Color(0.22, 0.36, 0.74)
const ROYAL_DARK := Color(0.16, 0.26, 0.56)
const GOLD := Color(0.98, 0.8, 0.32)
const WOOD := Color(0.56, 0.4, 0.3)
const WOOD_DARK := Color(0.4, 0.28, 0.22)
const LEAF := [Color(0.3, 0.6, 0.28), Color(0.38, 0.66, 0.3), Color(0.26, 0.54, 0.3), Color(0.44, 0.7, 0.34)]
const FLOWERS := [Color(1.0, 0.56, 0.72), Color(1.0, 0.86, 0.34), Color(0.76, 0.6, 1.0), Color(1.0, 0.44, 0.36), Color(1.0, 0.98, 0.96), Color(0.5, 0.74, 1.0)]
const AWNINGS := [Color(0.9, 0.3, 0.3), Color(0.24, 0.5, 0.86), Color(0.26, 0.66, 0.44), Color(0.96, 0.66, 0.2), Color(0.66, 0.4, 0.84)]
const TEAL := Color(0.22, 0.6, 0.64)
const IRON := Color(0.2, 0.2, 0.24)

## Half size of the walled square (m) and the gate opening width.
const WALL := 56.0
const GATE_W := 10.0

const TOWN := {
	"theme": "capital", "house_extras": {"lantern": true, "chimney": true, "balcony": true},
	"houses": [
		# merchant quarter (north-east): behind Market Row, then a second row
		{"at": [14, -18], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 0, "walls": 0, "seed": 71},
		{"at": [26, -18], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 4, "walls": 2, "seed": 72},
		{"at": [38, -18], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 5, "walls": 1, "seed": 73},
		{"at": [49, -18], "face": "s", "w": 5, "d": 3.4, "floors": 1, "roof": 17, "walls": 3, "seed": 74},
		{"at": [18, -32], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 17, "walls": 13, "seed": 75},
		{"at": [31, -32], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 3, "walls": 0, "seed": 76},
		{"at": [44, -32], "face": "s", "w": 5, "d": 3.4, "floors": 1, "roof": 0, "walls": 2, "seed": 77},
		{"at": [22, -45], "face": "s", "w": 5, "d": 3.4, "floors": 1, "roof": 4, "walls": 1, "seed": 78},
		{"at": [38, -45], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 5, "walls": 14, "seed": 79},
		# north-west (by the bank)
		{"at": [-14, -14], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 5, "walls": 3, "seed": 80},
		{"at": [-28, -15], "face": "s", "w": 5, "d": 3.4, "floors": 1, "roof": 0, "walls": 0, "seed": 81},
		{"at": [-44, -15], "face": "s", "w": 5, "d": 3.4, "floors": 2, "roof": 17, "walls": 2, "seed": 82},
		{"at": [-40, -30], "face": "e", "w": 5, "d": 3.4, "floors": 2, "roof": 4, "walls": 13, "seed": 83},
		{"at": [-40, -44], "face": "e", "w": 5, "d": 3.4, "floors": 1, "roof": 3, "walls": 1, "seed": 84},
		# south-west
		{"at": [-14, 15], "face": "n", "w": 5, "d": 3.4, "floors": 2, "roof": 3, "walls": 1, "seed": 85},
		{"at": [-27, 16], "face": "n", "w": 5, "d": 3.4, "floors": 1, "roof": 17, "walls": 0, "seed": 86},
		{"at": [-44, 16], "face": "n", "w": 5, "d": 3.4, "floors": 2, "roof": 0, "walls": 14, "seed": 87},
		{"at": [-18, 31], "face": "n", "w": 5, "d": 3.4, "floors": 2, "roof": 4, "walls": 2, "seed": 88},
		{"at": [-32, 31], "face": "n", "w": 5, "d": 3.4, "floors": 1, "roof": 5, "walls": 3, "seed": 89},
		{"at": [-46, 31], "face": "n", "w": 5, "d": 3.4, "floors": 2, "roof": 17, "walls": 0, "seed": 90},
		# south-east: the inn + a row
		{"at": [14, 17], "face": "n", "w": 5, "d": 3.4, "floors": 2, "roof": 5, "walls": 13, "seed": 91},
		{"at": [27, 17], "face": "n", "w": 5, "d": 3.4, "floors": 1, "roof": 0, "walls": 1, "seed": 92},
		{"at": [40, 17], "face": "n", "w": 5, "d": 3.4, "floors": 2, "roof": 4, "walls": 2, "seed": 93},
		{"at": [50, 38], "face": "w", "w": 5, "d": 3.4, "floors": 2, "roof": 3, "walls": 0, "seed": 94},
	],
	"npc_spots": [[-27, -5], [-27, 5], [-12, -26], [4, -31], [10.5, 8.5], [7, -12], [21, -5.5], [28, 5.5], [35, 5.5], [42, -5.5],
		[9.5, 11.5], [-6, 33], [18, 21], [14, -5.5], [42, 5.5], [14, 5.5]],
	"sign": [-6, -10],
	"lamps4": [[15, 4.6], [15, -4.6], [31.5, 4.6], [31.5, -4.6], [45.5, 4.6], [45.5, -4.6], [-20, 5], [-20, -5], [-46, 5], [-46, -5],
		[5, -20], [-5, -20], [5, -27], [5, 20], [-5, 20], [5, 28], [-5, 28], [-11, 11], [11, -11], [-11, -11]],
	"pieces": [
		{"kind": "monument", "at": [0, 0]},
		{"kind": "guild_hall", "at": [0, -40]},
		{"kind": "bank", "at": [-20, -30], "rot": PI / 2.0},
		{"kind": "warp_hub", "at": [-34, 0]},
		{"kind": "gazebo", "at": [0, 42]},
		{"kind": "notice_board", "at": [12.2, 10.2], "rot": -PI / 4.0},
		{"kind": "plots", "at": [32, 29]},
		# Market Row: stalls both sides of the east avenue (NPC vendors stand in front of some)
		{"kind": "stall", "at": [14, -8], "rot": 0.0, "s": 0.0}, {"kind": "stall", "at": [21, -8], "rot": 0.0, "s": 1.0},
		{"kind": "stall", "at": [28, -8], "rot": 0.0, "s": 2.0}, {"kind": "stall", "at": [35, -8], "rot": 0.0, "s": 3.0},
		{"kind": "stall", "at": [42, -8], "rot": 0.0, "s": 4.0},
		{"kind": "stall", "at": [14, 8], "rot": PI, "s": 4.0}, {"kind": "stall", "at": [21, 8], "rot": PI, "s": 3.0},
		{"kind": "stall", "at": [28, 8], "rot": PI, "s": 2.0}, {"kind": "stall", "at": [35, 8], "rot": PI, "s": 1.0},
		{"kind": "stall", "at": [42, 8], "rot": PI, "s": 0.0},
		# plaza: banners, flower beds, benches
		{"kind": "banner", "at": [12.9, 5.4]}, {"kind": "banner", "at": [5.4, 12.9]}, {"kind": "banner", "at": [-5.4, 12.9]},
		{"kind": "banner", "at": [-12.9, 5.4]}, {"kind": "banner", "at": [-12.9, -5.4]}, {"kind": "banner", "at": [-5.4, -12.9]},
		{"kind": "banner", "at": [5.4, -12.9]}, {"kind": "banner", "at": [12.9, -5.4]},
		{"kind": "flowerbed", "at": [7.6, 7.6], "rot": -PI / 4.0}, {"kind": "flowerbed", "at": [-7.6, 7.6], "rot": PI / 4.0},
		{"kind": "flowerbed", "at": [-7.6, -7.6], "rot": -PI / 4.0}, {"kind": "flowerbed", "at": [7.6, -7.6], "rot": PI / 4.0},
		{"kind": "bench", "at": [9.4, 3.6], "rot": -PI / 2.0}, {"kind": "bench", "at": [-9.4, 3.6], "rot": PI / 2.0},
		{"kind": "bench", "at": [-9.4, -3.6], "rot": PI / 2.0}, {"kind": "bench", "at": [3.6, 9.4], "rot": PI},
		{"kind": "bench", "at": [-3.6, 9.4], "rot": PI}, {"kind": "bench", "at": [3.6, -9.4], "rot": 0.0},
		# avenues: tree planters
		{"kind": "tree", "at": [7, 18]}, {"kind": "tree", "at": [-7, 18]}, {"kind": "tree", "at": [7, 26]}, {"kind": "tree", "at": [-7, 26]},
		{"kind": "tree", "at": [7, -18]}, {"kind": "tree", "at": [-7, -17]}, {"kind": "tree", "at": [7, -26]},
		{"kind": "tree", "at": [-18, 7]}, {"kind": "tree", "at": [-18, -7]}, {"kind": "tree", "at": [-48, 7]}, {"kind": "tree", "at": [-48, -7]},
		{"kind": "tree", "at": [50, 5]}, {"kind": "tree", "at": [50, -5]},
		# parks
		{"kind": "tree", "at": [-26, 42]}, {"kind": "tree", "at": [-38, 44]}, {"kind": "tree", "at": [-16, 46]}, {"kind": "tree", "at": [-46, 46]},
		{"kind": "flowerbed", "at": [-30, 40], "rot": 0.0}, {"kind": "flowerbed", "at": [-22, 46], "rot": 0.4},
		{"kind": "tree", "at": [18, 44]}, {"kind": "tree", "at": [48, 48]}, {"kind": "flowerbed", "at": [12, 38], "rot": 1.2},
		{"kind": "tree", "at": [-48, -48]}, {"kind": "tree", "at": [-28, -48]}, {"kind": "tree", "at": [48, -48]}, {"kind": "tree", "at": [30, -50]},
		{"kind": "hedge", "at": [-11, -40], "rot": PI / 2.0}, {"kind": "hedge", "at": [11, -40], "rot": PI / 2.0},
		{"kind": "hedge", "at": [-8, 48], "rot": 0.0}, {"kind": "hedge", "at": [8, 48], "rot": 0.0},
		# party square benches
		{"kind": "bench", "at": [-6, 40], "rot": PI / 2.0}, {"kind": "bench", "at": [6, 40], "rot": -PI / 2.0},
		{"kind": "bench", "at": [0, 48.5], "rot": PI},
		# signs (building names, EN + TH)
		{"kind": "sign", "at": [9.5, -3.2], "rot": 0.0, "text": "Market Row\nถนนตลาด"},
		{"kind": "sign", "at": [-26, 8.6], "rot": PI / 2.0, "text": "Warp Hub\nลานวาร์ป"},
		{"kind": "sign", "at": [-6.5, 36], "rot": 0.0, "text": "Party Square\nลานนัดพบปาร์ตี้"},
		{"kind": "sign", "at": [21, 22.5], "rot": 0.0, "text": "Merchant Plots\nแผงร้านผู้เล่น (เร็ว ๆ นี้)"},
	],
	# bunting strings across Market Row + the plaza avenues: [x0, z0, x1, z1, height]
	"bunting": [[17.5, -10, 17.5, 10, 5.0], [24.5, -10, 24.5, 10, 5.2], [31.5, -10, 31.5, 10, 5.0], [38.5, -10, 38.5, 10, 5.2],
		[45.5, -10, 45.5, 10, 5.0], [-6, -22, 6, -22, 4.8], [-6, 22, 6, 22, 4.8]],
	# lawns (walkable, visual): [x, z, w, d]
	"lawns": [[30, -38.5, 10, 5], [-30, -46, 8, 6], [-32, 43, 20, 9], [44, 44, 12, 10], [-47, -24, 8, 8], [47, -24, 7, 6]],
	# avenue paving bands: [x0, z0, x1, z1]
	"avenues": [[16, 0, 54, 0], [-16, 0, -25, 0], [-43, 0, -54, 0], [0, -16, 0, -33], [0, 16, 0, 32]],
}


static func _v(a) -> Vector3:
	return Vector3(float(a[0]), 0, float(a[1]))


static func _box() -> BoxMesh:
	return BoxMesh.new()


static func _cyl(top: float, bottom: float, h: float, segs: int = 10) -> CylinderMesh:
	return EnvNature._cyl(top, bottom, h, segs)


static func _sph(r: float, segs: int = 12, rings: int = 7) -> SphereMesh:
	return EnvNature._sphere(r, segs, rings)


static func _torus(inner: float, outer: float, rings: int = 24, segs: int = 8) -> TorusMesh:
	var t := TorusMesh.new()
	t.inner_radius = inner
	t.outer_radius = outer
	t.rings = rings
	t.ring_segments = segs
	return t


## A gabled roof (ridge along local x): a 45-degree-turned box whose lower half hides in the walls.
static func _gable(em, xf: Transform3D, length: float, width: float, rise: float, c: Color) -> void:
	var s := width / sqrt(2.0)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.0, rise / (width / 2.0), 1.0)) * Basis(Vector3(1, 0, 0), PI / 4.0) * Basis.from_scale(Vector3(length, s, s)), Vector3.ZERO), c)


static func _label(root: Node3D, text: String, xf: Transform3D, size: int = 64) -> void:
	if not root.is_inside_tree():
		return
	var l := Label3D.new()
	l.text = text
	l.font = Kanit.font("SemiBold")
	l.font_size = size
	l.outline_size = 12
	l.pixel_size = 0.006
	l.modulate = Color(0.36, 0.22, 0.1)
	l.outline_modulate = Color(1.0, 0.94, 0.78)
	l.double_sided = false
	l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	root.add_child(l)
	l.transform = xf
	l.add_to_group("capital_signs")


# ---------------------------------------------------------------- colliders (client + server)

static func _box_body(root: Node3D, size: Vector3, pos: Vector3, yaw: float) -> StaticBody3D:
	var sb := StaticBody3D.new()
	sb.collision_layer = 1
	sb.collision_mask = 0
	var cs := CollisionShape3D.new()
	var bs := BoxShape3D.new()
	bs.size = size
	cs.shape = bs
	sb.add_child(cs)
	root.add_child(sb)
	sb.position = pos
	sb.rotation.y = yaw
	return sb


static func _cyl_body(root: Node3D, r: float, h: float, pos: Vector3) -> StaticBody3D:
	var sb := StaticBody3D.new()
	sb.collision_layer = 1
	sb.collision_mask = 0
	var cs := CollisionShape3D.new()
	var c := CylinderShape3D.new()
	c.radius = r
	c.height = h
	cs.shape = c
	cs.position.y = h / 2.0
	sb.add_child(cs)
	root.add_child(sb)
	sb.position = Vector3(pos.x, 0, pos.z)
	return sb


## Grass-free radius of a capital piece (env_towns4.shapes).
static func shape_r(kind: String, def: float) -> float:
	return {"monument": 7.5, "guild_hall": 12.0, "bank": 9.0, "warp_hub": 8.5, "gazebo": 5.0, "plots": 10.0, "stall": 2.4,
		"banner": 0.8, "flowerbed": 2.2, "bench": 1.2, "tree": 1.6, "hedge": 2.6, "sign": 1.0, "notice_board": 1.6}.get(kind, def)


## Static bodies of the capital's own pieces + the city wall (group "town4_colliders" is added by env_towns4).
static func colliders(root: Node3D, town: Dictionary) -> Array:
	var made: Array = []
	for pc in town.get("pieces", []):
		var k := str(pc["kind"])
		var at := _v(pc["at"])
		var yaw := float(pc.get("rot", 0.0))
		var b := Basis(Vector3.UP, yaw)
		match k:
			"monument":
				made.append(_cyl_body(root, 6.2, 1.4, at))
			"guild_hall":
				made.append(_box_body(root, Vector3(18.4, 3.0, 12.4), at + Vector3(0, 1.5, 0), yaw))
			"bank":
				made.append(_box_body(root, Vector3(14.4, 3.0, 11.0), at + b * Vector3(0, 1.5, -0.6), yaw))
			"warp_hub":
				for i in 6:
					var a := TAU * i / 6.0 + PI / 6.0
					made.append(_cyl_body(root, 0.55, 3.0, at + Vector3(cos(a), 0, sin(a)) * 6.4))
				made.append(_cyl_body(root, 1.0, 1.2, at))
			"gazebo":
				for i in 8:
					var a := TAU * i / 8.0
					made.append(_cyl_body(root, 0.22, 2.6, at + Vector3(cos(a), 0, sin(a)) * 3.6))
			"notice_board":
				made.append(_box_body(root, Vector3(2.4, 2.0, 0.5), at + Vector3(0, 1.0, 0), yaw))
			"stall":
				made.append(_box_body(root, Vector3(3.2, 2.4, 1.9), at + b * Vector3(0, 1.2, -0.2), yaw))
			"banner":
				made.append(_cyl_body(root, 0.25, 3.0, at))
			"flowerbed":
				made.append(_box_body(root, Vector3(3.2, 0.8, 1.6), at + Vector3(0, 0.4, 0), yaw))
			"tree":
				made.append(_cyl_body(root, 0.95, 1.2, at))
			"hedge":
				made.append(_box_body(root, Vector3(4.4, 1.2, 1.2), at + Vector3(0, 0.6, 0), yaw))
	for st in town.get("bunting", []):
		for e in [Vector3(float(st[0]), 0, float(st[1])), Vector3(float(st[2]), 0, float(st[3]))]:
			made.append(_cyl_body(root, 0.16, 3.0, e))
	if str(town.get("theme", "")) == "capital":
		made.append_array(_wall_colliders(root))
	return made


## Wall runs (each side split by its gate) as [{c, size, yaw}] + gate tower centres.
static func wall_runs() -> Array:
	var out: Array = []
	var half_run := (WALL - GATE_W / 2.0) / 2.0
	var mid := GATE_W / 2.0 + half_run
	for side in 4:
		var b := Basis(Vector3.UP, side * PI / 2.0)
		for sgn: float in [-1.0, 1.0]:
			out.append({"c": b * Vector3(sgn * mid, 0, -WALL), "len": half_run * 2.0, "yaw": side * PI / 2.0})
	return out


static func gate_towers() -> Array:
	var out: Array = []
	for side in 4:
		var b := Basis(Vector3.UP, side * PI / 2.0)
		for sgn: float in [-1.0, 1.0]:
			out.append(b * Vector3(sgn * (GATE_W / 2.0 + 1.6), 0, -WALL))
	return out


static func _wall_colliders(root: Node3D) -> Array:
	var made: Array = []
	for w in wall_runs():
		made.append(_box_body(root, Vector3(float(w["len"]), 4.0, 1.8), (w["c"] as Vector3) + Vector3(0, 2.0, 0), float(w["yaw"])))
	for t in gate_towers():
		made.append(_cyl_body(root, 2.5, 4.0, t))
	return made


# ---------------------------------------------------------------- visuals

## Draws a capital piece kind into `em`. False = not a capital kind (env_towns4 handles it).
static func piece(em, kind: String, at: Vector3, yaw: float, s: float, add_glow: Callable, lights: Array, root: Node3D, pc: Dictionary = {}) -> bool:
	var xf := Transform3D(Basis(Vector3.UP, yaw), at)
	match kind:
		"monument":
			_monument(root, em, xf, add_glow)
			lights.append(at + Vector3(0, 4.0, 0))
		"guild_hall":
			_guild_hall(root, em, xf, add_glow)
			lights.append(at + Vector3(0, 3.0, 7.5))
		"bank":
			_bank(root, em, xf, add_glow)
			lights.append(at + Basis(Vector3.UP, yaw) * Vector3(0, 3.0, 6.5))
		"warp_hub":
			_warp_hub(root, em, xf, add_glow)
			lights.append(at + Vector3(0, 3.0, 0))
		"gazebo":
			_gazebo(em, xf, add_glow)
			lights.append(at + Vector3(0, 2.6, 0))
		"notice_board":
			_notice_board(em, xf)
		"plots":
			_plots(em, xf)
		"stall":
			_stall(em, xf, int(s))
		"banner":
			_banner(em, xf)
		"flowerbed":
			_flowerbed(em, xf, int(absf(at.x * 3.0 + at.z)))
		"bench":
			_bench(em, xf)
		"tree":
			_tree(em, xf, int(absf(at.x * 7.0 + at.z * 3.0)))
		"hedge":
			_hedge(em, xf)
		"sign":
			_signpost(root, em, xf, str(pc.get("text", "")))
		_:
			return false
	return true


## The walls, gatehouses and bunting of the town (called once by env_towns4.build_town).
static func build_extra(root: Node3D, em, town: Dictionary, add_glow: Callable) -> void:
	for lw in town.get("lawns", []):
		_lawn(em, Vector3(float(lw[0]), 0, float(lw[1])), float(lw[2]), float(lw[3]), int(absf(float(lw[0]) + float(lw[1]))))
	for av in town.get("avenues", []):
		_avenue(em, Vector3(float(av[0]), 0, float(av[1])), Vector3(float(av[2]), 0, float(av[3])))
	for st in town.get("bunting", []):
		_bunting(em, Vector3(float(st[0]), 0, float(st[1])), Vector3(float(st[2]), 0, float(st[3])), float(st[4]))
	if str(town.get("theme", "")) != "capital":
		return
	# crenellated wall runs
	for w in wall_runs():
		var xf := Transform3D(Basis(Vector3.UP, float(w["yaw"])), w["c"])
		var length := float(w["len"])
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(length, 3.6, 1.8)), Vector3(0, 1.8, 0)), WALL_STONE)
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(length, 0.3, 2.1)), Vector3(0, 3.7, 0)), STONE)
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(length, 0.5, 2.0)), Vector3(0, 0.25, 0)), STONE_DARK)
		var n := int(length / 1.6)
		for i in n:
			if i % 2 == 0:
				var x := -length / 2.0 + (i + 0.5) * length / n
				em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(length / n * 0.9, 0.7, 0.5)), Vector3(x, 4.2, 0.75)), WALL_STONE.lightened(0.04))
				em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(length / n * 0.9, 0.7, 0.5)), Vector3(x, 4.2, -0.75)), WALL_STONE.lightened(0.04))
		# stone courses (a darker band every metre) on the city side
		for y in [1.0, 2.2]:
			em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(length, 0.08, 0.04)), Vector3(0, y, 0.92)), WALL_STONE.darkened(0.1))
	# gatehouses: two round towers with blue cone roofs + an arch beam with the royal banner
	for side in 4:
		var b := Basis(Vector3.UP, side * PI / 2.0)
		for sgn: float in [-1.0, 1.0]:
			var t := b * Vector3(sgn * (GATE_W / 2.0 + 1.6), 0, -WALL)
			em.add(_cyl(2.3, 2.5, 6.4, 16), Transform3D(Basis(), t + Vector3(0, 3.2, 0)), WALL_STONE)
			em.add(_cyl(2.6, 2.6, 0.4, 16), Transform3D(Basis(), t + Vector3(0, 6.6, 0)), STONE)
			em.add(_cyl(0.0, 2.8, 3.2, 16), Transform3D(Basis(), t + Vector3(0, 8.4, 0)), ROYAL)
			em.add(_sph(0.22, 8, 4), Transform3D(Basis(), t + Vector3(0, 10.1, 0)), GOLD)
			em.add(_box(), Transform3D((b * Basis.from_scale(Vector3(0.7, 1.0, 0.1))), t + b * Vector3(0, 4.6, 2.5)), IRON)
			add_glow.call(Color(1.0, 0.9, 0.6), Transform3D(Basis.from_scale(Vector3(0.22, 0.28, 0.22)), t + b * Vector3(-sgn * 2.2, 3.6, 2.2)))
		var arch := Transform3D(b, b * Vector3(0, 0, -WALL))
		em.add(_box(), arch * Transform3D(Basis.from_scale(Vector3(GATE_W + 1.2, 1.2, 2.2)), Vector3(0, 6.0, 0)), WALL_STONE)
		em.add(_box(), arch * Transform3D(Basis.from_scale(Vector3(GATE_W + 1.6, 0.3, 2.5)), Vector3(0, 6.75, 0)), STONE)
		em.add(_box(), arch * Transform3D(Basis.from_scale(Vector3(2.0, 2.4, 0.08)), Vector3(0, 4.5, 1.15)), ROYAL)
		_crest(em, arch * Transform3D(Basis(), Vector3(0, 4.7, 1.2)), 0.5)


## The city crest: a gold crown over a star, facing local +z.
static func _crest(em, xf: Transform3D, k: float) -> void:
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(0.9, 0.22, 0.05) * k), Vector3(0, -0.1 * k, 0)), GOLD)
	for i in 3:
		var x := (i - 1) * 0.36 * k
		em.add(_cyl(0.0, 0.13 * k, 0.42 * k, 4), xf * Transform3D(Basis(), Vector3(x, 0.2 * k, 0)), GOLD)
		em.add(_sph(0.06 * k, 6, 3), xf * Transform3D(Basis(), Vector3(x, 0.44 * k, 0.02)), Color(0.9, 0.2, 0.3))
	for i in 5:
		var a := TAU * i / 5.0
		em.add(_cyl(0.0, 0.1 * k, 0.34 * k, 4), xf * Transform3D(Basis(Vector3(0, 0, 1), -a), Vector3(sin(a) * 0.16 * k, -0.62 * k + cos(a) * 0.16 * k, 0)), CREAM)


static func _monument(root: Node3D, em, xf: Transform3D, add_glow: Callable) -> void:
	# grand basin, a water pool, a second bowl, a fluted column and the gold Crown Star glowing on top
	em.add(_cyl(6.0, 6.2, 0.7, 36), xf * Transform3D(Basis(), Vector3(0, 0.35, 0)), STONE)
	em.add(_torus(5.7, 6.35, 40, 6), xf * Transform3D(Basis.from_scale(Vector3(1, 0.55, 1)), Vector3(0, 0.74, 0)), CREAM)
	for i in 12:
		var a := TAU * i / 12.0
		em.add(_box(), xf * Transform3D((Basis(Vector3.UP, -a) * Basis.from_scale(Vector3(0.5, 0.5, 0.7))), Vector3(cos(a) * 6.1, 0.3, sin(a) * 6.1)), STONE_DARK)
	_disc_water(root, xf * Vector3(0, 0.73, 0), 5.75)
	em.add(_cyl(1.2, 1.5, 1.4, 16), xf * Transform3D(Basis(), Vector3(0, 1.3, 0)), STONE)
	em.add(_cyl(3.2, 1.3, 0.6, 28), xf * Transform3D(Basis(), Vector3(0, 2.2, 0)), CREAM)
	em.add(_torus(3.0, 3.35, 32, 6), xf * Transform3D(Basis.from_scale(Vector3(1, 0.5, 1)), Vector3(0, 2.5, 0)), GOLD)
	_disc_water(root, xf * Vector3(0, 2.53, 0), 3.0)
	EnvLife.spray(root, xf.origin, 0.0, 2.95, 2.53, 0.73)
	for i in 10:
		var a := TAU * i / 10.0
		em.add(_cyl(0.05, 0.09, 1.9, 5), xf * Transform3D(Basis(), Vector3(cos(a) * 3.2, 1.4, sin(a) * 3.2)), Color(0.82, 0.95, 1.0))
	# column with flutes + gold rings
	em.add(_cyl(0.6, 0.7, 3.2, 12), xf * Transform3D(Basis(), Vector3(0, 4.0, 0)), CREAM)
	for i in 12:
		var a := TAU * i / 12.0
		em.add(_box(), xf * Transform3D((Basis(Vector3.UP, -a) * Basis.from_scale(Vector3(0.08, 3.0, 0.12))), Vector3(cos(a) * 0.64, 4.0, sin(a) * 0.64)), CREAM.darkened(0.06))
	em.add(_torus(0.62, 0.8, 18, 5), xf * Transform3D(Basis(), Vector3(0, 2.55, 0)), GOLD)
	em.add(_torus(0.58, 0.76, 18, 5), xf * Transform3D(Basis(), Vector3(0, 5.6, 0)), GOLD)
	em.add(_cyl(1.0, 0.7, 0.4, 14), xf * Transform3D(Basis(), Vector3(0, 5.8, 0)), CREAM)
	# the Crown Star: an eight-point gold star standing up, a crown above, a glowing heart
	var star := xf * Transform3D(Basis(), Vector3(0, 7.3, 0))
	for i in 8:
		var a := TAU * i / 8.0
		var ln := 1.25 if i % 2 == 0 else 0.8
		em.add(_cyl(0.0, 0.26, ln, 4), star * Transform3D(Basis(Vector3(0, 0, 1), -a), Vector3(sin(a) * ln / 2.0, cos(a) * ln / 2.0, 0)), GOLD)
		em.add(_cyl(0.0, 0.26, ln, 4), star * Transform3D(Basis(Vector3.UP, PI / 2.0) * Basis(Vector3(0, 0, 1), -a), Basis(Vector3.UP, PI / 2.0) * Vector3(sin(a) * ln / 2.0, cos(a) * ln / 2.0, 0)), GOLD)
	add_glow.call(Color(1.0, 0.94, 0.62), star * Transform3D(Basis.from_scale(Vector3(0.42, 0.42, 0.42)), Vector3.ZERO))
	for i in 5:
		var a := TAU * i / 5.0
		em.add(_cyl(0.0, 0.16, 0.5, 4), star * Transform3D(Basis(), Vector3(cos(a) * 0.4, 1.55, sin(a) * 0.4)), GOLD)
	em.add(_cyl(0.52, 0.48, 0.22, 16), star * Transform3D(Basis(), Vector3(0, 1.25, 0)), GOLD.darkened(0.08))
	# four little spouting fish-mouths on the rim (blue-green tiles)
	for i in 4:
		var a := TAU * i / 4.0 + PI / 4.0
		em.add(_sph(0.4, 10, 5), xf * Transform3D(Basis.from_scale(Vector3(1, 0.8, 1.3)), Vector3(cos(a) * 5.9, 1.05, sin(a) * 5.9)), TEAL)
		em.add(_cyl(0.04, 0.07, 1.2, 5), xf * Transform3D(Basis(Vector3(cos(a + PI / 2.0), 0, sin(a + PI / 2.0)), 0.9), Vector3(cos(a) * 5.4, 1.0, sin(a) * 5.4)), Color(0.8, 0.95, 1.0))


static func _disc_water(root: Node3D, at: Vector3, r: float) -> void:
	var pm := PlaneMesh.new()
	pm.size = Vector2(r * 2.1, r * 2.1)
	pm.subdivide_width = 8
	pm.subdivide_depth = 8
	var m := ShaderMaterial.new()
	m.shader = WATER
	L.set_shader(m)
	m.set_shader_parameter("shape", 1)
	m.set_shader_parameter("disc_radius", r)
	m.set_shader_parameter("shallow", Color(0.42, 0.82, 0.9))
	m.set_shader_parameter("deep", Color(0.18, 0.56, 0.78))
	m.set_shader_parameter("depth_span", 1.8)
	var mi := MeshInstance3D.new()
	mi.mesh = pm
	mi.material_override = m
	mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
	root.add_child(mi)
	mi.position = at
	mi.add_to_group("town4_water")


static func _windows(em, xf: Transform3D, count: int, width: float, y: float, z: float, h: float = 1.5) -> void:
	for i in count:
		var x := -width / 2.0 + (i + 0.5) * width / count
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.0, h, 0.12)), Vector3(x, y, z)), Color(0.22, 0.2, 0.28))
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.2, 0.14, 0.2)), Vector3(x, y - h / 2.0 - 0.05, z + 0.04)), CREAM)
		em.add(_sph(0.5, 10, 4), xf * Transform3D(Basis.from_scale(Vector3(1.0, 0.6, 0.12)), Vector3(x, y + h / 2.0, z)), Color(0.22, 0.2, 0.28))
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(0.06, h, 0.14)), Vector3(x, y, z + 0.02)), CREAM)


static func _guild_hall(root: Node3D, em, xf: Transform3D, add_glow: Callable) -> void:
	# front = local +z. Plinth, a long hall with windows, blue gabled roof, a central clock tower, banners
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(18.6, 0.6, 12.6)), Vector3(0, 0.3, 0)), STONE_DARK)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(17.0, 6.6, 10.4)), Vector3(0, 3.9, 0)), CREAM)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(17.4, 0.4, 10.8)), Vector3(0, 7.2, 0)), STONE)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(17.2, 0.25, 10.6)), Vector3(0, 3.8, 0)), STONE)
	for sx: float in [-1.0, 1.0]:
		for i in 4:
			em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(0.5, 6.6, 0.5)), Vector3(sx * (8.5 - i * 0.02), 3.9, (i - 1.5) * 3.4)), STONE)
	_windows(em, xf, 6, 15.0, 2.2, 5.22)
	_windows(em, xf, 6, 15.0, 5.4, 5.22, 1.3)
	_windows(em, xf * Transform3D(Basis(Vector3.UP, PI), Vector3.ZERO), 6, 15.0, 2.2, 5.22)
	_gable(em, xf * Transform3D(Basis(), Vector3(0, 7.4, 0)), 18.2, 11.6, 3.6, ROYAL)
	_gable(em, xf * Transform3D(Basis(), Vector3(0, 7.42, 0)), 18.3, 1.0, 3.62, GOLD)
	# grand door + steps + banners
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(3.2, 3.6, 0.3)), Vector3(0, 2.4, 5.25)), WOOD_DARK)
	em.add(_sph(1.6, 16, 6), xf * Transform3D(Basis.from_scale(Vector3(1.0, 0.7, 0.18)), Vector3(0, 4.2, 5.25)), WOOD_DARK)
	em.add(_torus(1.5, 1.8, 20, 5), xf * Transform3D(Basis(Vector3(1, 0, 0), PI / 2.0), Vector3(0, 3.9, 5.3)), GOLD)
	for i in 3:
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(6.0 - i * 0.6, 0.2, 0.7)), Vector3(0, 0.1 + 0.2 * i, 6.7 - i * 0.5)), STONE)
	for x: float in [-6.0, -3.2, 3.2, 6.0]:
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.3, 3.4, 0.06)), Vector3(x, 4.2, 5.3)), ROYAL)
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.4, 0.18, 0.1)), Vector3(x, 5.95, 5.32)), GOLD)
		_crest(em, xf * Transform3D(Basis(), Vector3(x, 4.6, 5.36)), 0.9)
	# the clock tower
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(4.2, 7.0, 4.2)), Vector3(0, 10.0, 1.4)), CREAM)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(4.6, 0.4, 4.6)), Vector3(0, 13.6, 1.4)), STONE)
	for sd in 4:
		var bb := Basis(Vector3.UP, sd * PI / 2.0)
		var c := xf * Transform3D(bb, bb * Vector3(0, 11.2, 2.13) + Vector3(0, 0, 1.4) - bb * Vector3(0, 0, 0))
		em.add(_cyl(1.25, 1.25, 0.12, 24), c * Transform3D(Basis(Vector3(1, 0, 0), PI / 2.0), Vector3.ZERO), CREAM.lightened(0.05))
		em.add(_torus(1.2, 1.42, 24, 5), c * Transform3D(Basis(Vector3(1, 0, 0), PI / 2.0), Vector3.ZERO), GOLD)
		em.add(_box(), c * Transform3D(Basis.from_scale(Vector3(0.1, 0.9, 0.06)), Vector3(0, 0.4, 0.08)), IRON)
		em.add(_box(), c * Transform3D((Basis(Vector3(0, 0, 1), 1.0) * Basis.from_scale(Vector3(0.1, 0.65, 0.06))), Vector3(0.26, 0.15, 0.08)), IRON)
	em.add(_cyl(0.0, 3.4, 4.2, 4), xf * Transform3D(Basis(Vector3.UP, PI / 4.0), Vector3(0, 15.9, 1.4)), ROYAL_DARK)
	em.add(_cyl(0.05, 0.05, 1.6, 5), xf * Transform3D(Basis(), Vector3(0, 18.6, 1.4)), GOLD)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.2, 0.7, 0.05)), Vector3(0.62, 19.0, 1.4)), Color(0.9, 0.28, 0.3))
	add_glow.call(Color(1.0, 0.9, 0.55), xf * Transform3D(Basis.from_scale(Vector3(0.18, 0.18, 0.18)), Vector3(0, 19.5, 1.4)))
	for sx: float in [-1.0, 1.0]:
		add_glow.call(Color(1.0, 0.86, 0.5), xf * Transform3D(Basis.from_scale(Vector3(0.24, 0.3, 0.24)), Vector3(sx * 2.4, 3.0, 5.7)))
	_label(root, "Guild Hall\nหอกิลด์", xf * Transform3D(Basis(), Vector3(0, 6.4, 5.45)), 96)


static func _bank(root: Node3D, em, xf: Transform3D, add_glow: Callable) -> void:
	# front = local +z. Three-step podium, a cella, six columns, a pediment with the gold coin, a teal dome
	for i in 3:
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(14.0 - i * 0.8, 0.3, 11.0 - i * 0.8)), Vector3(0, 0.15 + i * 0.3, -0.6 + i * 0.3)), STONE.darkened(0.03 * i))
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(11.0, 6.0, 7.0)), Vector3(0, 3.9, -1.6)), CREAM)
	_windows(em, xf * Transform3D(Basis(), Vector3(0, 0, -1.6)), 4, 9.0, 3.6, 3.52)
	for i in 6:
		var x := -5.5 + i * 2.2
		em.add(_cyl(0.42, 0.48, 5.6, 14), xf * Transform3D(Basis(), Vector3(x, 3.7, 3.6)), Color(0.98, 0.97, 0.94))
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.1, 0.3, 1.1)), Vector3(x, 1.05, 3.6)), STONE)
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.1, 0.3, 1.1)), Vector3(x, 6.6, 3.6)), STONE)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(13.0, 0.9, 9.4)), Vector3(0, 7.2, 0.2)), STONE)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(13.2, 0.18, 9.6)), Vector3(0, 7.72, 0.2)), GOLD.darkened(0.1))
	_gable(em, xf * Transform3D(Basis(Vector3.UP, PI / 2.0), Vector3(0, 7.8, 0.2)), 9.4, 13.0, 2.2, STONE.lightened(0.04))
	em.add(_cyl(0.9, 0.9, 0.14, 22), xf * Transform3D(Basis(Vector3(1, 0, 0), PI / 2.0), Vector3(0, 8.6, 4.95)), GOLD)
	em.add(_cyl(0.6, 0.6, 0.16, 22), xf * Transform3D(Basis(Vector3(1, 0, 0), PI / 2.0), Vector3(0, 8.6, 5.0)), GOLD.lightened(0.12))
	# dome on a drum
	em.add(_cyl(2.4, 2.4, 1.0, 20), xf * Transform3D(Basis(), Vector3(0, 10.2, -1.2)), CREAM)
	em.add(_sph(2.4, 20, 10), xf * Transform3D(Basis.from_scale(Vector3(1, 0.85, 1)), Vector3(0, 10.7, -1.2)), TEAL)
	em.add(_sph(0.3, 10, 5), xf * Transform3D(Basis(), Vector3(0, 12.9, -1.2)), GOLD)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.2, 3.4, 0.2)), Vector3(0, 2.6, 1.95)), WOOD_DARK)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.5, 0.2, 0.3)), Vector3(0, 4.35, 1.98)), GOLD)
	for sx: float in [-1.0, 1.0]:
		add_glow.call(Color(1.0, 0.88, 0.56), xf * Transform3D(Basis.from_scale(Vector3(0.22, 0.3, 0.22)), Vector3(sx * 1.8, 3.2, 2.2)))
		# stone lions? no - two gold coin urns
		em.add(_cyl(0.5, 0.6, 0.9, 12), xf * Transform3D(Basis(), Vector3(sx * 6.4, 1.35, 4.4)), STONE)
		em.add(_sph(0.5, 12, 6), xf * Transform3D(Basis.from_scale(Vector3(1, 0.6, 1)), Vector3(sx * 6.4, 1.9, 4.4)), GOLD)
	_label(root, "Grand Bank\nธนาคารหลวง", xf * Transform3D(Basis(), Vector3(0, 7.2, 4.95)), 96)


static func _warp_hub(root: Node3D, em, xf: Transform3D, add_glow: Callable) -> void:
	# low round dais in three steps, an inlaid glowing rune ring, six crystal pillars, a floating crystal
	for i in 3:
		em.add(_cyl(8.0 - i * 0.8, 8.2 - i * 0.8, 0.1, 40), xf * Transform3D(Basis(), Vector3(0, 0.05 + i * 0.08, 0)), STONE.lightened(0.03 * i) if i != 1 else Color(0.78, 0.82, 0.9))
	var ring := MeshInstance3D.new()
	ring.mesh = _torus(4.6, 4.9, 48, 4)
	ring.material_override = _glow(Color(0.6, 0.9, 1.0))
	ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
	root.add_child(ring)
	ring.transform = xf * Transform3D(Basis.from_scale(Vector3(1, 0.2, 1)), Vector3(0, 0.26, 0))
	ring.add_to_group("capital_glow")
	for i in 12:
		var a := TAU * i / 12.0
		add_glow.call(Color(0.7, 0.92, 1.0), xf * Transform3D((Basis(Vector3.UP, -a) * Basis.from_scale(Vector3(0.12, 0.03, 0.3))), Vector3(cos(a) * 3.8, 0.27, sin(a) * 3.8)))
	var cols := [Color(0.6, 0.9, 1.0), Color(1.0, 0.7, 0.86), Color(1.0, 0.9, 0.5), Color(0.7, 1.0, 0.76), Color(0.8, 0.7, 1.0), Color(1.0, 0.76, 0.6)]
	for i in 6:
		var a := TAU * i / 6.0 + PI / 6.0
		var p := Vector3(cos(a), 0, sin(a)) * 6.4
		em.add(_cyl(0.5, 0.62, 0.5, 6), xf * Transform3D(Basis(), p + Vector3(0, 0.25, 0)), STONE_DARK)
		em.add(_cyl(0.28, 0.42, 2.6, 6), xf * Transform3D(Basis(), p + Vector3(0, 1.8, 0)), CREAM)
		em.add(_torus(0.3, 0.44, 12, 4), xf * Transform3D(Basis(), p + Vector3(0, 3.1, 0)), GOLD)
		add_glow.call(cols[i], xf * Transform3D(Basis.from_scale(Vector3(0.32, 0.62, 0.32)), p + Vector3(0, 3.7, 0)))
	em.add(_cyl(0.8, 1.0, 1.0, 12), xf * Transform3D(Basis(), Vector3(0, 0.6, 0)), CREAM)
	em.add(_torus(0.7, 0.95, 16, 5), xf * Transform3D(Basis(), Vector3(0, 1.1, 0)), GOLD)
	add_glow.call(Color(0.62, 0.92, 1.0), xf * Transform3D(Basis.from_scale(Vector3(0.5, 0.95, 0.5)), Vector3(0, 2.4, 0)))


static func _glow(c: Color) -> StandardMaterial3D:
	var m := StandardMaterial3D.new()
	m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	m.albedo_color = c
	return EnvTime.night_glow(m)


static func _gazebo(em, xf: Transform3D, add_glow: Callable) -> void:
	em.add(_cyl(4.2, 4.4, 0.3, 8), xf * Transform3D(Basis(Vector3.UP, PI / 8.0), Vector3(0, 0.15, 0)), STONE)
	for i in 8:
		var a := TAU * i / 8.0
		em.add(_cyl(0.16, 0.2, 2.6, 8), xf * Transform3D(Basis(), Vector3(cos(a) * 3.6, 1.6, sin(a) * 3.6)), CREAM)
		em.add(_box(), xf * Transform3D((Basis(Vector3.UP, -a + PI / 2.0) * Basis.from_scale(Vector3(2.7, 0.12, 0.12))), Vector3(cos(a + PI / 8.0) * 3.4, 2.8, sin(a + PI / 8.0) * 3.4)), GOLD)
	em.add(_cyl(4.3, 4.3, 0.3, 8), xf * Transform3D(Basis(Vector3.UP, PI / 8.0), Vector3(0, 3.05, 0)), CREAM)
	# a sweeping eight-sided pink roof, a gold rim, a small lantern dome on top
	em.add(_cyl(0.7, 4.8, 1.9, 8), xf * Transform3D(Basis(Vector3.UP, PI / 8.0), Vector3(0, 4.15, 0)), Color(0.92, 0.5, 0.56))
	em.add(_cyl(4.85, 4.85, 0.14, 8), xf * Transform3D(Basis(Vector3.UP, PI / 8.0), Vector3(0, 3.22, 0)), GOLD)
	em.add(_cyl(0.7, 0.7, 0.5, 8), xf * Transform3D(Basis(Vector3.UP, PI / 8.0), Vector3(0, 5.35, 0)), CREAM)
	em.add(_sph(0.75, 12, 6), xf * Transform3D(Basis.from_scale(Vector3(1, 0.8, 1)), Vector3(0, 5.6, 0)), Color(0.92, 0.5, 0.56))
	em.add(_cyl(0.06, 0.06, 0.8, 6), xf * Transform3D(Basis(), Vector3(0, 6.5, 0)), GOLD)
	em.add(_sph(0.2, 10, 5), xf * Transform3D(Basis(), Vector3(0, 6.95, 0)), GOLD)
	add_glow.call(Color(1.0, 0.86, 0.58), xf * Transform3D(Basis.from_scale(Vector3(0.3, 0.36, 0.3)), Vector3(0, 2.5, 0)))
	em.add(_cyl(0.01, 0.01, 0.5, 4), xf * Transform3D(Basis(), Vector3(0, 2.85, 0)), IRON)


static func _notice_board(em, xf: Transform3D) -> void:
	for sx: float in [-1.0, 1.0]:
		em.add(_cyl(0.1, 0.12, 2.6, 6), xf * Transform3D(Basis(), Vector3(sx * 1.1, 1.3, 0)), WOOD_DARK)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.4, 1.5, 0.12)), Vector3(0, 1.55, 0)), WOOD)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.6, 0.12, 0.18)), Vector3(0, 2.34, 0)), WOOD_DARK)
	_gable(em, xf * Transform3D(Basis(), Vector3(0, 2.5, 0)), 2.9, 0.9, 0.4, ROYAL)
	var rng := RandomNumberGenerator.new()
	rng.seed = 313
	for i in 9:
		var p := Vector3(rng.randf_range(-0.9, 0.9), rng.randf_range(1.05, 2.05), 0.08)
		em.add(_box(), xf * Transform3D((Basis(Vector3(0, 0, 1), rng.randf_range(-0.2, 0.2)) * Basis.from_scale(Vector3(0.42, 0.52, 0.02))), p), [CREAM, Color(1.0, 0.9, 0.7), Color(0.9, 0.96, 1.0)][i % 3])
		em.add(_sph(0.04, 6, 3), xf * Transform3D(Basis(), p + Vector3(0, 0.2, 0.02)), Color(0.9, 0.2, 0.2))


static func _plots(em, xf: Transform3D) -> void:
	# four marked 6 x 6 m plots for future player shops (gold-edged outlines + numbered posts)
	for i in 4:
		var c := Vector3(-12.0 + i * 8.0, 0, 0)
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(6.0, 0.04, 6.0)), c + Vector3(0, 0.02, 0)), Color(0.94, 0.9, 0.8))
		for sd in 4:
			var bb := Basis(Vector3.UP, sd * PI / 2.0)
			em.add(_box(), xf * Transform3D((bb * Basis.from_scale(Vector3(6.2, 0.06, 0.18))), c + bb * Vector3(0, 0.04, 3.0)), GOLD.darkened(0.1))
		em.add(_cyl(0.08, 0.1, 1.2, 6), xf * Transform3D(Basis(), c + Vector3(-2.8, 0.6, -2.8)), WOOD_DARK)
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(0.6, 0.4, 0.06)), c + Vector3(-2.8, 1.2, -2.76)), CREAM)
		for d in i + 1:
			em.add(_sph(0.05, 6, 3), xf * Transform3D(Basis(), c + Vector3(-3.0 + d * 0.13, 1.2, -2.72)), ROYAL)


static func _stall(em, xf: Transform3D, k: int) -> void:
	# counter, four posts, a striped sloping awning, goods by kind (fruit, cloth, pots, gems, bread)
	var aw: Color = AWNINGS[k % AWNINGS.size()]
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(3.0, 1.0, 1.1)), Vector3(0, 0.5, 0.2)), WOOD)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(3.2, 0.1, 1.3)), Vector3(0, 1.05, 0.2)), WOOD.lightened(0.1))
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.9, 0.5, 0.06)), Vector3(0, 0.5, 0.78)), aw)
	for sx: float in [-1.0, 1.0]:
		em.add(_cyl(0.06, 0.07, 2.6, 6), xf * Transform3D(Basis(), Vector3(sx * 1.5, 1.3, 0.75)), WOOD_DARK)
		em.add(_cyl(0.06, 0.07, 3.0, 6), xf * Transform3D(Basis(), Vector3(sx * 1.5, 1.5, -0.6)), WOOD_DARK)
	var n := 7
	for i in n:
		var x := -1.65 + (i + 0.5) * 3.3 / n
		em.add(_box(), xf * Transform3D((Basis(Vector3(1, 0, 0), -0.32) * Basis.from_scale(Vector3(3.3 / n + 0.01, 0.06, 1.7))), Vector3(x, 2.85, 0.1)), aw if i % 2 == 0 else CREAM)
	var sc: int = em.verts.size()
	for i in n:
		var x := -1.65 + (i + 0.5) * 3.3 / n
		em.add(_sph(3.3 / n / 2.0, 8, 4), xf * Transform3D(Basis.from_scale(Vector3(1, 0.6, 0.4)), Vector3(x, 2.55, 0.92)), aw if i % 2 == 0 else CREAM)
	# the scalloped awning edge flaps a little
	em.cloth(sc, (xf * Vector3(0, 2.75, 0)).y, 1.2)
	var rng := RandomNumberGenerator.new()
	rng.seed = 200 + k
	match k % 5:
		0:
			for i in 14:
				em.add(_sph(0.12, 8, 4), xf * Transform3D(Basis(), Vector3(rng.randf_range(-1.3, 1.3), 1.2, rng.randf_range(-0.1, 0.5))), [Color(0.92, 0.2, 0.2), Color(1.0, 0.6, 0.1), Color(0.5, 0.8, 0.2), Color(1.0, 0.86, 0.2)][i % 4])
		1:
			for i in 5:
				em.add(_cyl(0.13, 0.13, 0.9, 10), xf * Transform3D(Basis(Vector3(0, 0, 1), PI / 2.0), Vector3(-1.1 + i * 0.55, 1.23, 0.25)), [Color(0.9, 0.4, 0.5), Color(0.4, 0.6, 0.9), Color(0.96, 0.84, 0.4), Color(0.5, 0.8, 0.6), Color(0.8, 0.6, 1.0)][i])
		2:
			for i in 6:
				em.add(_cyl(0.12, 0.16, 0.34, 10), xf * Transform3D(Basis(), Vector3(-1.2 + i * 0.48, 1.27, 0.2)), [Color(0.8, 0.5, 0.3), Color(0.3, 0.6, 0.7), Color(0.9, 0.9, 0.86)][i % 3])
		3:
			for i in 10:
				add_gem(em, xf * Transform3D(Basis(), Vector3(rng.randf_range(-1.2, 1.2), 1.2, rng.randf_range(0.0, 0.5))), [Color(0.4, 0.8, 1.0), Color(1.0, 0.4, 0.6), Color(0.6, 1.0, 0.6), Color(0.9, 0.7, 1.0)][i % 4])
		4:
			for i in 8:
				em.add(_sph(0.18, 8, 4), xf * Transform3D(Basis.from_scale(Vector3(1.4, 0.7, 1.0)), Vector3(-1.2 + i * 0.34, 1.18, rng.randf_range(0.0, 0.4))), Color(0.9, 0.66, 0.36))
	em.add(_box(), xf * Transform3D((Basis(Vector3.UP, 0.3) * Basis.from_scale(Vector3(0.7, 0.6, 0.7))), Vector3(2.1, 0.3, -0.2)), WOOD.lightened(0.05))
	em.add(_cyl(0.3, 0.3, 0.8, 10), xf * Transform3D(Basis(), Vector3(-2.1, 0.4, -0.3)), WOOD_DARK)


static func add_gem(em, xf: Transform3D, c: Color) -> void:
	em.add(_sph(0.1, 4, 2), xf * Transform3D(Basis.from_scale(Vector3(1, 1.5, 1)), Vector3.ZERO), c)


static func _banner(em, xf: Transform3D) -> void:
	em.add(_cyl(0.2, 0.24, 0.4, 8), xf * Transform3D(Basis(), Vector3(0, 0.2, 0)), STONE)
	em.add(_cyl(0.07, 0.08, 6.2, 8), xf * Transform3D(Basis(), Vector3(0, 3.3, 0)), IRON)
	em.add(_sph(0.16, 8, 4), xf * Transform3D(Basis(), Vector3(0, 6.5, 0)), GOLD)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.4, 0.08, 0.08)), Vector3(0.62, 6.0, 0)), IRON)
	var s0: int = em.verts.size()
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.2, 2.8, 0.05)), Vector3(0.62, 4.55, 0)), ROYAL)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.24, 0.16, 0.07)), Vector3(0.62, 3.1, 0)), GOLD)
	for i in 3:
		em.add(_cyl(0.0, 0.2, 0.36, 3), xf * Transform3D(Basis(Vector3(1, 0, 0), PI), Vector3(0.26 + i * 0.36, 2.86, 0)), ROYAL_DARK)
	_crest(em, xf * Transform3D(Basis(), Vector3(0.62, 4.85, 0.04)), 1.1)
	_crest(em, xf * Transform3D(Basis(Vector3.UP, PI), Vector3(0.62, 4.85, -0.04)), 1.1)
	# the panel, its gold hem, tassels and crests flutter from the cross bar down (town graphics pass)
	em.cloth(s0, (xf * Vector3(0, 5.95, 0)).y, 3.3)


static func _flowerbed(em, xf: Transform3D, k: int) -> void:
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(3.2, 0.5, 1.6)), Vector3(0, 0.25, 0)), STONE)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.9, 0.1, 1.3)), Vector3(0, 0.52, 0)), Color(0.36, 0.26, 0.2))
	var rng := RandomNumberGenerator.new()
	rng.seed = 900 + k
	for i in L.dn(26, 8):
		var p := Vector3(rng.randf_range(-1.3, 1.3), 0.62, rng.randf_range(-0.55, 0.55))
		em.add(_sph(0.2, 6, 3), xf * Transform3D(Basis.from_scale(Vector3(1, 0.7, 1)), p), LEAF[i % LEAF.size()])
		em.add(_sph(0.09, 6, 3), xf * Transform3D(Basis(), p + Vector3(rng.randf_range(-0.06, 0.06), 0.18, 0)), FLOWERS[(k + i) % FLOWERS.size()])


static func _bench(em, xf: Transform3D) -> void:
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.0, 0.1, 0.5)), Vector3(0, 0.5, 0)), WOOD)
	em.add(_box(), xf * Transform3D((Basis(Vector3(1, 0, 0), -0.2) * Basis.from_scale(Vector3(2.0, 0.45, 0.08))), Vector3(0, 0.85, -0.25)), WOOD)
	for sx: float in [-1.0, 1.0]:
		em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(0.1, 0.5, 0.5)), Vector3(sx * 0.85, 0.25, 0)), IRON)


static func _tree(em, xf: Transform3D, k: int) -> void:
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.9, 0.55, 1.9)), Vector3(0, 0.27, 0)), STONE)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(1.7, 0.08, 1.7)), Vector3(0, 0.56, 0)), Color(0.36, 0.26, 0.2))
	em.add(_cyl(0.16, 0.24, 2.6, 7), xf * Transform3D(Basis(), Vector3(0, 1.8, 0)), WOOD)
	var rng := RandomNumberGenerator.new()
	rng.seed = 700 + k
	var c: Color = LEAF[k % LEAF.size()]
	var blooms := k % 3 == 0
	for i in 5:
		var p := Vector3(rng.randf_range(-0.7, 0.7), 3.4 + rng.randf_range(-0.3, 0.8), rng.randf_range(-0.7, 0.7))
		em.add(_sph(rng.randf_range(0.9, 1.3), 10, 6), xf * Transform3D(Basis(), p), c.lightened(rng.randf() * 0.1))
	if blooms:
		for i in L.dn(12, 3):
			var a := rng.randf() * TAU
			var e := rng.randf_range(0.2, 1.1)
			em.add(_sph(0.12, 6, 3), xf * Transform3D(Basis(), Vector3(cos(a) * cos(e) * 1.5, 3.7 + sin(e) * 1.2, sin(a) * cos(e) * 1.5)), FLOWERS[k % FLOWERS.size()])
	for i in 6:
		var a := TAU * i / 6.0
		em.add(_sph(0.09, 6, 3), xf * Transform3D(Basis(), Vector3(cos(a) * 0.6, 0.66, sin(a) * 0.6)), FLOWERS[(k + i) % FLOWERS.size()])


static func _hedge(em, xf: Transform3D) -> void:
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(4.2, 1.0, 1.0)), Vector3(0, 0.5, 0)), Color(0.28, 0.54, 0.28))
	for i in 7:
		em.add(_sph(0.55, 8, 4), xf * Transform3D(Basis.from_scale(Vector3(1, 0.6, 1)), Vector3(-1.8 + i * 0.6, 1.0, 0)), Color(0.32, 0.6, 0.3))


static func _signpost(root: Node3D, em, xf: Transform3D, text: String) -> void:
	em.add(_cyl(0.08, 0.1, 2.4, 6), xf * Transform3D(Basis(), Vector3(0, 1.2, 0)), WOOD_DARK)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.4, 0.9, 0.1)), Vector3(0, 2.2, 0)), CREAM)
	em.add(_box(), xf * Transform3D(Basis.from_scale(Vector3(2.55, 1.0, 0.06)), Vector3(0, 2.2, -0.04)), WOOD)
	_label(root, text, xf * Transform3D(Basis(), Vector3(0, 2.2, 0.07)), 40)
	_label(root, text, xf * Transform3D(Basis(Vector3.UP, PI), Vector3(0, 2.2, -0.09)), 40)


static func _bunting(em, a: Vector3, b: Vector3, h: float) -> void:
	for e in [a, b]:
		em.add(_cyl(0.07, 0.09, h + 0.4, 6), Transform3D(Basis(), (e as Vector3) + Vector3(0, (h + 0.4) / 2.0, 0)), WOOD_DARK)
		em.add(_sph(0.12, 6, 3), Transform3D(Basis(), (e as Vector3) + Vector3(0, h + 0.45, 0)), GOLD)
	var d := b - a
	var n := int(d.length() / 0.6)
	var yaw := atan2(d.x, d.z)
	for i in n + 1:
		var t := float(i) / n
		var p := a.lerp(b, t) + Vector3(0, h - 0.9 * sin(t * PI), 0)
		if i < n:
			var p2 := a.lerp(b, float(i + 1) / n) + Vector3(0, h - 0.9 * sin(float(i + 1) / n * PI), 0)
			var dd := p2 - p
			em.add(_box(), Transform3D(Basis(Vector3.UP, yaw) * (Basis(Vector3(1, 0, 0), -atan2(dd.y, Vector2(dd.x, dd.z).length())) * Basis.from_scale(Vector3(0.02, 0.02, dd.length()))), (p + p2) / 2.0), IRON)
		if i > 0 and i < n:
			var sf: int = em.verts.size()
			em.add(_cyl(0.0, 0.2, 0.34, 3), Transform3D(Basis(Vector3.UP, yaw) * (Basis(Vector3(1, 0, 0), PI) * Basis.from_scale(Vector3(1, 1, 0.15))), p - Vector3(0, 0.18, 0)),
				[Color(0.9, 0.3, 0.34), Color(1.0, 0.84, 0.3), Color(0.3, 0.56, 0.9), Color(0.4, 0.78, 0.46), Color(0.96, 0.96, 0.96)][i % 5])
			em.cloth(sf, p.y, 0.45)


## A lawn patch in the paved city: grass slab, darker rim, flower clusters and a few round shrubs (walkable, visual).
static func _lawn(em, c: Vector3, w: float, d: float, k: int) -> void:
	em.add(_box(), Transform3D(Basis.from_scale(Vector3(w + 0.4, 0.1, d + 0.4)), c + Vector3(0, 0.05, 0)), STONE_DARK)
	em.add(_box(), Transform3D(Basis.from_scale(Vector3(w, 0.14, d)), c + Vector3(0, 0.07, 0)), Color(0.4, 0.66, 0.3))
	var rng := RandomNumberGenerator.new()
	rng.seed = 5000 + k
	var n := L.dn(int(w * d / 3.0), 4)
	for i in n:
		var p := c + Vector3(rng.randf_range(-w / 2.0 + 0.4, w / 2.0 - 0.4), 0.16, rng.randf_range(-d / 2.0 + 0.4, d / 2.0 - 0.4))
		if i % 4 == 0:
			em.add(_sph(rng.randf_range(0.35, 0.55), 8, 5), Transform3D(Basis.from_scale(Vector3(1, 0.75, 1)), p + Vector3(0, 0.2, 0)), LEAF[i % LEAF.size()].lightened(0.05))
		else:
			var fc: Color = FLOWERS[(k + i) % FLOWERS.size()]
			for j in 3:
				em.add(_sph(0.09, 6, 3), Transform3D(Basis(), p + Vector3(rng.randf_range(-0.2, 0.2), 0.1, rng.randf_range(-0.2, 0.2))), fc)
			em.add(_sph(0.14, 6, 3), Transform3D(Basis.from_scale(Vector3(1.4, 0.5, 1.4)), p), LEAF[(i + 1) % LEAF.size()])


## Avenue paving: a band of large light slabs with dark edge lines along a straight avenue (visual).
static func _avenue(em, a: Vector3, b: Vector3) -> void:
	var d := b - a
	var length := d.length()
	var along := d / length
	var side := Vector3(-along.z, 0, along.x)
	var yaw := atan2(along.x, along.z)
	var n := int(length / 1.25)
	for i in n:
		var p := a + along * ((i + 0.5) * length / n)
		for c in 3:
			var sc: Color = Color(0.94, 0.9, 0.82) if (i + c) % 2 == 0 else Color(0.88, 0.84, 0.78)
			em.add(_box(), Transform3D((Basis(Vector3.UP, yaw) * Basis.from_scale(Vector3(1.2, 0.05, length / n - 0.08))), p + side * (c - 1) * 1.28 + Vector3(0, 0.025, 0)), sc)
	for sd: float in [-1.0, 1.0]:
		em.add(_box(), Transform3D((Basis(Vector3.UP, yaw) * Basis.from_scale(Vector3(0.22, 0.06, length))), (a + b) / 2.0 + side * sd * 2.2 + Vector3(0, 0.03, 0)), Color(0.62, 0.52, 0.44))
