feat: add Kenney road and nature assets and implement map builder script

This commit is contained in:
Nicholas Butzke
2026-07-19 02:32:06 -04:00
parent 67040ba12d
commit f62f4f2503
81 changed files with 194 additions and 24 deletions
+134 -24
View File
@@ -134,9 +134,25 @@ func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: S
inst.scale = Vector3.ONE * scale_f
body.add_child(inst)
LevelMaterials.apply_toon_recursive(inst, 0.0)
# Subtle per-building tint so repeated kit models read as different shops
_tint_recursive(inst, Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.3, 1.0), 0.16))
return w
func _tint_recursive(node: Node, tint: Color) -> void:
if node is MeshInstance3D:
var mi := node as MeshInstance3D
var sc: int = mi.mesh.get_surface_count() if mi.mesh else 0
for s in sc:
var m := mi.get_surface_override_material(s)
if m is ShaderMaterial:
var c = m.get_shader_parameter("albedo_color")
if c is Color:
m.set_shader_parameter("albedo_color", c * tint)
for child in node.get_children():
_tint_recursive(child, tint)
## Fill one street-facing edge of a lot with kit buildings + Akiba signage.
func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: String) -> void:
var yaw := -90.0 if dir > 0 else 90.0 # face the street (-x for east row)
@@ -252,6 +268,7 @@ func _build_geometry() -> void:
_build_block(bx, bz)
_build_rail_line()
_build_streets()
_build_avenues()
if _spawn_points.is_empty():
@@ -430,12 +447,14 @@ func _shrine_block(c: Vector3, bx: int, bz: int) -> void:
# Hall with slide roof
_box_static(c + Vector3(0, 2.0, -10.0), Vector3(12.0, 4.0, 8.0), WOOD.darkened(0.2), n + "_Hall", "wood")
_ramp_static(c + Vector3(0, 4.9, -8.2), Vector3(13.0, 0.3, 5.6), Vector3(-24, 0, 0), VERMILION.darkened(0.35), n + "_Roof", "wood")
# Stone lanterns + trees (trunk static, canopy deco)
# Stone lanterns + real modeled trees (trunk collider under each)
for i in 2:
var lx := -8.0 + 16.0 * float(i)
_box_static(c + Vector3(lx, 1.0, 8.0), Vector3(1.0, 2.0, 1.0), Color(0.55, 0.55, 0.52), n + "_Lant%d" % i, "brick")
_box_static(c + Vector3(lx, 1.6, -2.0), Vector3(0.7, 3.2, 0.7), TRUNK, n + "_Trunk%d" % i, "wood")
_deco_box(c + Vector3(lx, 4.6, -2.0), Vector3(4.2, 3.4, 4.2), LEAF)
_box_static(c + Vector3(lx, 1.6, -2.0), Vector3(0.8, 3.2, 0.8), TRUNK, n + "_Trunk%d" % i, "wood")
_kit_prop_path("res://assets/props/nature/" + ["tree_detailed.glb", "tree_default.glb"][i % 2],
c + Vector3(lx, 0, -2.0), _rng.randf_range(0.0, 360.0), 7.0)
_kit_prop_path("res://assets/props/nature/tree_oak.glb", c + Vector3(0, 0, 2.0), _rng.randf_range(0.0, 360.0), 6.0)
_spawn_points.append(c + Vector3(0, 1.5, 12))
@@ -470,12 +489,12 @@ func _market_block(c: Vector3, bx: int, bz: int) -> void:
## Plaza: open ground; the exact centre cell hosts the giant screen tower.
func _plaza_block(c: Vector3, bx: int, bz: int) -> void:
var n := "Plaza_%d_%d" % [bx, bz]
# Planter cover ring
# Planter cover ring with real trees
for i in 4:
var a := TAU * float(i) / 4.0 + PI / 4.0
var p := c + Vector3(cos(a), 0, sin(a)) * (BLOCK * 0.32)
_box_static(Vector3(p.x, 0.55, p.z), Vector3(4.0, 1.1, 4.0), Color(0.5, 0.48, 0.52), "%s_Pl%d" % [n, i], "brick")
_deco_box(Vector3(p.x, 1.5, p.z), Vector3(3.2, 1.0, 3.2), LEAF)
_kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 1.0, p.z), float(i) * 90.0, 4.5)
# The Crossing screen tower on the single centre-most plaza cell
if bx == 5 and bz == 4:
_box_static(c + Vector3(0, 9.0, 0), Vector3(6.0, 18.0, 6.0), METAL.darkened(0.2), n + "_ScreenTower", "metal")
@@ -511,6 +530,13 @@ func _construction_block(c: Vector3, bx: int, bz: int) -> void:
for i in 3:
_box_static(c + Vector3(10, 1.3 + 2.6 * float(mini(i, 1)), -12.0 + float(i) * 3.2), Vector3(6.2, 2.6, 2.6),
cc[i], "%s_Cont%d" % [n, i], "metal")
# Cones + barriers scattered around the site entrance
for i in 4:
_kit_prop_path("res://assets/props/roads/construction-cone.glb",
c + Vector3(-16.0 + float(i) * 4.0, 0.1, 18.0), 0.0, 8.0)
for i in 2:
_kit_prop_path("res://assets/props/roads/construction-barrier.glb",
c + Vector3(-14.0 + float(i) * 12.0, 0.1, 14.0), float(i * 30), 8.0)
_spawn_points.append(c + Vector3(14, 1.5, -14))
const INK_COLOR := Color(0.08, 0.07, 0.1)
@@ -543,26 +569,110 @@ func _build_rail_line() -> void:
_spawn_points.append(Vector3(0, RAIL_TOP + 1.5, z))
# ── Main avenues: lights + crosswalks ────────────────────────────────────────
# ── Streets: real road meshes with markings ──────────────────────────────────
const ROAD_W := 12.0 # visual road width between sidewalk aprons
## Street lines run between block columns/rows at multiples of PITCH.
func _street_lines() -> Array[float]:
var lines: Array[float] = []
for i in GRID - 1:
lines.append((float(i) - float(GRID) * 0.5 + 1.0) * PITCH)
return lines
func _road_piece(key: String, pos: Vector3, yaw_deg: float, sc: Vector3) -> void:
var inst: Node3D = _kit_scene_at("res://assets/props/roads/" + key + ".glb").instantiate()
inst.scale = sc
add_child(inst)
inst.global_position = pos
inst.rotation_degrees.y = yaw_deg
LevelMaterials.apply_toon_recursive(inst, 0.0)
# The kit's pale concrete clips to white under the map lighting
_tint_recursive(inst, Color(0.55, 0.55, 0.6))
var _path_cache: Dictionary = {}
func _kit_scene_at(path: String) -> PackedScene:
if not _path_cache.has(path):
_path_cache[path] = load(path)
return _path_cache[path]
func _build_streets() -> void:
var lines := _street_lines()
var seg := PITCH - ROAD_W # straight run between intersections
var y := 0.02
# Intersections with line markings
for x in lines:
for z in lines:
_road_piece("road-crossroad-line", Vector3(x, y, z), 0.0, Vector3(ROAD_W, 2.0, ROAD_W))
# Straight segments (stretched single meshes) + edge stubs to the wall
for x in lines:
for i in lines.size() + 1:
var z0 := -CITY_HALF if i == 0 else lines[i - 1] + ROAD_W * 0.5
var z1 := CITY_HALF if i == lines.size() else lines[i] - ROAD_W * 0.5
if z1 - z0 < 2.0:
continue
_road_piece("road-straight", Vector3(x, y, (z0 + z1) * 0.5), 0.0, Vector3(ROAD_W, 2.0, z1 - z0))
for z in lines:
for i in lines.size() + 1:
var x0 := -CITY_HALF if i == 0 else lines[i - 1] + ROAD_W * 0.5
var x1 := CITY_HALF if i == lines.size() else lines[i] - ROAD_W * 0.5
if x1 - x0 < 2.0:
continue
_road_piece("road-straight", Vector3((x0 + x1) * 0.5, y, z), 90.0, Vector3(ROAD_W, 2.0, x1 - x0))
# Overhead cable bundles across east-west streets (urban-Japan clutter)
for z in lines:
var x := -CITY_HALF + 24.0
while x < CITY_HALF - 24.0:
if absf(x) > 14.0: # keep the central avenue sky clear
var h := 9.5 + fmod(absf(x), 3.0) * 0.4
for s3 in 3:
var t0 := float(s3) / 3.0
var t1 := float(s3 + 1) / 3.0
var sag0 := sin(t0 * PI) * 0.7
var sag1 := sin(t1 * PI) * 0.7
var za := z - STREET * 0.5 + STREET * t0
var zb := z - STREET * 0.5 + STREET * t1
var mid := Vector3(x, h - (sag0 + sag1) * 0.5, (za + zb) * 0.5)
_deco_box(mid, Vector3(0.07, 0.07, zb - za + 0.1), INK_COLOR)
x += 48.0
else:
x += 48.0
# ── Main avenues: kit streetlights ───────────────────────────────────────────
func _build_avenues() -> void:
# The two central avenues (x=0 and z=0) get matched streetlights and
# zebra crossings at every block line.
var zline := -CITY_HALF + PITCH * 0.5
while zline <= CITY_HALF:
var lines := _street_lines()
var lamp_scale := 10.0
for z in lines:
for side: float in [-1.0, 1.0]:
var x := side * (STREET * 0.5 + 0.6)
_box_static(Vector3(x, 3.25, zline), Vector3(0.28, 6.5, 0.28), METAL, "LampA_%d_%d" % [int(zline), int(side)], "metal")
_box_static(Vector3(x - side * 1.5, 6.35, zline), Vector3(3.0, 0.22, 0.22), METAL, "LampArmA_%d_%d" % [int(zline), int(side)], "metal")
_deco_box(Vector3(x - side * 2.8, 6.2, zline), Vector3(0.9, 0.18, 0.4), Color(1.0, 0.95, 0.8), true)
# Crosswalk across the x=0 avenue at each block line
for s in 5:
var sxx := -6.0 + 3.0 * float(s)
_deco_box(Vector3(sxx, 0.03, zline + PITCH * 0.5 - 4.0), Vector3(0.9, 0.06, 3.2), STRIPE)
zline += PITCH
var xline := -CITY_HALF + PITCH * 0.5
while xline <= CITY_HALF:
# Double-arm lights along the central N-S avenue at every block line
var x := side * (ROAD_W * 0.5 + 0.4)
_box_static(Vector3(x, 3.4, z), Vector3(0.26, 6.8, 0.26), METAL, "LampP_%d_%d" % [int(z), int(side)], "metal")
_kit_prop_path("res://assets/props/roads/light-curved.glb",
Vector3(x, 0, z), 90.0 if side > 0 else -90.0, lamp_scale)
for x in lines:
if absf(x) < 1.0:
continue
# Single lights down the central E-W avenue
for side: float in [-1.0, 1.0]:
var z2 := side * (STREET * 0.5 + 0.6)
_box_static(Vector3(xline, 3.25, z2), Vector3(0.28, 6.5, 0.28), METAL, "LampB_%d_%d" % [int(xline), int(side)], "metal")
xline += PITCH
var z2 := side * (ROAD_W * 0.5 + 0.4)
_kit_prop_path("res://assets/props/roads/light-curved.glb",
Vector3(x, 0, z2), 0.0 if side > 0 else 180.0, lamp_scale)
func _kit_prop_path(path: String, pos: Vector3, yaw_deg: float, scale_f: float) -> void:
var inst: Node3D = _kit_scene_at(path).instantiate()
inst.scale = Vector3.ONE * scale_f
add_child(inst)
inst.global_position = pos
inst.rotation_degrees.y = yaw_deg
# Nature-kit foliage uses vertex-colored materials that the toon swap
# mangles (cyan trees) — keep original materials; the faceted low-poly
# look already reads cel under this lighting.
if not path.contains("/nature/"):
LevelMaterials.apply_toon_recursive(inst, 0.0)