feat: original kitbashed hero assets built in Blender

First bespoke art authored for the project (tools/build_hero_assets.py,
headless Blender 5.1 -> assets/props/hero/):

- konbini.glb: 8m convenience-store front — fascia + teal sign band with
  ink trim, framed windows with sills and window ads, recessed double
  glass door, angled awning, drink machine, hedge planter. Placed on
  ~1/3 of wide buildings (glass-tagged collider)
- station.glb: viaduct station entrance — canopy on pillars, real stair
  steps with handrails and balusters, sign totem; placed at the central
  avenue crossing of the rail line
- clock.glb: 10m plaza clock tower — tiered base, ringed column, 4 clock
  faces with hands, teal crown; anchors the south-west plaza cell
- torii.glb: proper torii with segmented curved kasagi, shimaki, nuki
  beam and plaque — replaces the box torii in every shrine courtyard
  (invisible pillar colliders keep gameplay identical)

All hero pieces run through the toon pipeline with fine ink outlines.
Every part beveled; flat cel palette matches the city.

Verified: city renders clean with hero pieces placed; movement 11/11,
smoke 0 failures, acoustics ALL PASSED.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-19 03:00:17 -04:00
co-authored by Claude Fable 5
parent 6b3e363dfd
commit 7f9a579b63
6 changed files with 256 additions and 7 deletions
+39 -7
View File
@@ -96,6 +96,28 @@ func _kit_scene(key: String) -> PackedScene:
return _kit_cache[key]
# ── Hero assets (original kitbash, tools/build_hero_assets.py) ──────────────
## Hero piece with a box collider. Front faces -Z before yaw (glTF Y-up).
func _hero(key: String, pos: Vector3, yaw_deg: float, col_size: Vector3,
col_center_y: float, acoustic: String = "concrete") -> void:
var body := StaticBody3D.new()
body.name = "Hero_" + key + "_%d" % _rng.randi()
body.set_meta("acoustic_material", acoustic)
add_child(body)
body.global_position = pos
body.rotation_degrees.y = yaw_deg
var shape := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = col_size
shape.shape = bs
shape.position = Vector3(0, col_center_y, 0)
body.add_child(shape)
var inst: Node3D = _kit_scene_at("res://assets/props/hero/" + key + ".glb").instantiate()
body.add_child(inst)
LevelMaterials.apply_toon_recursive(inst, 0.015)
## Small visual-only kit prop (parasols, awnings): no collider needed.
func _kit_prop(key: String, pos: Vector3, yaw_deg: float, scale_f: float) -> void:
var inst: Node3D = _kit_scene(key).instantiate()
@@ -177,8 +199,12 @@ func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: Str
var h: float = KIT_BUILDINGS[key].y * KIT_SCALE
_kit_building(key, Vector3(front_x, 0, zc), yaw, "K%s_%d" % [seed_id, idx])
var face_yaw := -90.0 if dir > 0 else 90.0 # ad quads face the street
# Storefront awning on some buildings (kit detail piece)
if _rng.randi() % 5 < 2:
# Kitbashed konbini storefront module on wide buildings
if w >= 12.0 and _rng.randi() % 3 == 0:
_hero("konbini", Vector3(front_x - float(dir) * 0.75, 0.02, zc), 90.0 * float(dir),
Vector3(8.2, 4.2, 1.4), 2.1, "glass")
# Storefront awning on some other buildings (kit detail piece)
elif _rng.randi() % 5 < 2:
_kit_prop_path(KIT_DIR + "detail-awning-wide.glb",
Vector3(front_x, 3.1, zc), face_yaw, minf(w * 0.9, 11.0))
# Street-level posters plastered on the facade (graphic density)
@@ -501,12 +527,11 @@ func _shrine_block(c: Vector3, bx: int, bz: int) -> void:
_box_static(c + Vector3(0.0 if horiz else off, 0.7, off if horiz else 0.0),
Vector3(BLOCK - 6.0 if horiz else 0.6, 1.4, 0.6 if horiz else BLOCK - 6.0),
Color(0.55, 0.55, 0.52), n + "_Wall%d" % i, "brick")
# Torii gate at the south entry
# Kitbashed torii (curved kasagi) at the south entry; pillar colliders
var tz := c.z + (BLOCK - 6.0) * 0.5
_box_static(c + Vector3(-4.0, 3.5, tz), Vector3(0.8, 7.0, 0.8), VERMILION, n + "_PillarW", "wood")
_box_static(c + Vector3(4.0, 3.5, tz), Vector3(0.8, 7.0, 0.8), VERMILION, n + "_PillarE", "wood")
_box_static(c + Vector3(0, 7.0, tz), Vector3(11.0, 0.7, 1.1), VERMILION, n + "_BeamTop", "wood")
_box_static(c + Vector3(0, 5.8, tz), Vector3(9.4, 0.5, 0.9), VERMILION.darkened(0.15), n + "_BeamLow", "wood")
_hero("torii", c + Vector3(0, 0.1, tz), 0.0, Vector3(0.1, 0.1, 0.1), 0.05, "wood")
_box_static(c + Vector3(-4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarW", "wood").visible = false
_box_static(c + Vector3(4.0, 3.5, tz), Vector3(0.9, 7.0, 0.9), VERMILION, n + "_PillarE", "wood").visible = false
# 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")
@@ -571,6 +596,9 @@ func _plaza_block(c: Vector3, bx: int, bz: int) -> void:
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")
_kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 1.0, p.z), float(i) * 90.0, 4.5)
# Kitbashed clock tower on the south-west plaza cell
if bx == 4 and bz == 5:
_hero("clock", c + Vector3(0, 0.1, 0), 0.0, Vector3(3.2, 10.5, 3.2), 5.25, "concrete")
# 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")
@@ -637,6 +665,10 @@ func _build_rail_line() -> void:
_ad_quad(Vector3(x - 1.35, 2.0, z), Vector2(1.2, 1.8), -90.0, _poster_tex())
i += 1
x += PITCH
# Kitbashed station entrance beside the central avenue crossing
_hero("station", Vector3(ROAD_W * 0.5 + 8.0, 0.02, z + 7.5), 90.0,
Vector3(6.0, 3.6, 5.0), 1.8, "concrete")
# Slide ramps to the deck at the two main-avenue crossings
for sx in [-PITCH * 0.5 - 4.0, PITCH * 0.5 + 4.0]:
var ang := rad_to_deg(atan2(RAIL_TOP, 16.0))