big
This commit is contained in:
@@ -96,26 +96,106 @@ func _kit_scene(key: String) -> PackedScene:
|
||||
return _kit_cache[key]
|
||||
|
||||
|
||||
## Combined rendered bounds expressed in an ancestor's local space. Imported
|
||||
## kit dimensions are useful for layout, but collision must follow the actual
|
||||
## imported geometry rather than a rounded catalog number.
|
||||
func _visual_bounds_in(node: Node, relative_to: Node3D) -> AABB:
|
||||
var result := AABB()
|
||||
var has_bounds := false
|
||||
var to_local := relative_to.global_transform.affine_inverse()
|
||||
for child in node.find_children("*", "MeshInstance3D", true, false):
|
||||
var mesh_instance := child as MeshInstance3D
|
||||
if mesh_instance.mesh == null:
|
||||
continue
|
||||
var transformed := _transformed_aabb(
|
||||
mesh_instance.get_aabb(), to_local * mesh_instance.global_transform)
|
||||
if has_bounds:
|
||||
result = result.merge(transformed)
|
||||
else:
|
||||
result = transformed
|
||||
has_bounds = true
|
||||
return result if has_bounds else AABB()
|
||||
|
||||
|
||||
func _transformed_aabb(bounds: AABB, xform: Transform3D) -> AABB:
|
||||
var result := AABB(xform * bounds.position, Vector3.ZERO)
|
||||
for x in 2:
|
||||
for y in 2:
|
||||
for z in 2:
|
||||
var corner := bounds.position + Vector3(
|
||||
bounds.size.x * float(x),
|
||||
bounds.size.y * float(y),
|
||||
bounds.size.z * float(z))
|
||||
result = result.expand(xform * corner)
|
||||
return result
|
||||
|
||||
|
||||
func _fit_box_collider(body: StaticBody3D, visual: Node,
|
||||
horizontal_inset: float = 0.04) -> CollisionShape3D:
|
||||
var bounds := _visual_bounds_in(visual, body)
|
||||
var shape := CollisionShape3D.new()
|
||||
shape.name = "CollisionShape3D"
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(
|
||||
maxf(0.05, bounds.size.x - horizontal_inset),
|
||||
maxf(0.05, bounds.size.y - 0.02),
|
||||
maxf(0.05, bounds.size.z - horizontal_inset))
|
||||
shape.shape = box
|
||||
shape.position = bounds.get_center()
|
||||
body.add_child(shape)
|
||||
body.set_meta("mesh_fitted_collision", true)
|
||||
return shape
|
||||
|
||||
|
||||
## Exact collision for imported art. One enclosing AABB is not mesh fitting:
|
||||
## a window sill or roof overhang moves its face away from the playable wall
|
||||
## and creates an invisible wallrun/bullet plane. Static concave shapes
|
||||
## preserve every recess, opening and overhang in the rendered mesh.
|
||||
func _fit_mesh_colliders(body: StaticBody3D, visual: Node) -> int:
|
||||
var count := 0
|
||||
var to_body := body.global_transform.affine_inverse()
|
||||
for child in visual.find_children("*", "MeshInstance3D", true, false):
|
||||
var mesh_instance := child as MeshInstance3D
|
||||
if mesh_instance.mesh == null:
|
||||
continue
|
||||
var triangle_shape := mesh_instance.mesh.create_trimesh_shape()
|
||||
if triangle_shape == null:
|
||||
continue
|
||||
var collision := CollisionShape3D.new()
|
||||
collision.name = "ExactMeshCollision_%03d" % count
|
||||
collision.shape = triangle_shape
|
||||
body.add_child(collision)
|
||||
collision.transform = to_body * mesh_instance.global_transform
|
||||
collision.set_meta("source_mesh_path", str(mesh_instance.get_path()))
|
||||
count += 1
|
||||
if count == 0:
|
||||
_fit_box_collider(body, visual, 0.06)
|
||||
body.set_meta("exact_collision_fallback", true)
|
||||
else:
|
||||
body.set_meta("mesh_exact_collision", true)
|
||||
body.set_meta("exact_collision_count", count)
|
||||
return count
|
||||
|
||||
|
||||
# ── 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:
|
||||
## Hero piece with exact imported-mesh collision. Front faces -Z before yaw.
|
||||
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.005)
|
||||
# Authored origins differ between hero pieces. Seat the rendered bottom on
|
||||
# the requested support plane before deriving collision from that geometry.
|
||||
var bounds := _visual_bounds_in(inst, body)
|
||||
inst.position.y -= bounds.position.y
|
||||
_fit_mesh_colliders(body, inst)
|
||||
|
||||
|
||||
## Small visual-only kit prop (parasols, awnings): no collider needed.
|
||||
@@ -125,16 +205,19 @@ func _kit_prop(key: String, pos: Vector3, yaw_deg: float, scale_f: float) -> voi
|
||||
add_child(inst)
|
||||
inst.global_position = pos
|
||||
inst.rotation_degrees.y = yaw_deg
|
||||
inst.set_meta("source_asset_path", KIT_DIR + key + ".glb")
|
||||
LevelMaterials.apply_toon_recursive(inst, 0.007)
|
||||
var bounds := _visual_bounds_in(inst, self)
|
||||
inst.global_position.y += pos.y - bounds.position.y
|
||||
inst.set_meta("ground_fitted", true)
|
||||
|
||||
|
||||
## Place a kit model with a box collider matching its scaled bounds.
|
||||
## Place a kit model with exact per-mesh collision.
|
||||
## front_pos: centre of the facade at ground level. yaw_deg: 0 faces +Z.
|
||||
func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: String,
|
||||
scale_f: float = KIT_SCALE) -> float:
|
||||
var dims: Vector3 = KIT_BUILDINGS.get(key, KIT_SKYSCRAPERS.get(key, Vector3.ONE))
|
||||
var w := dims.x * scale_f
|
||||
var h := dims.y * scale_f
|
||||
var d := dims.z * scale_f
|
||||
var fwd := Vector3(sin(deg_to_rad(yaw_deg)), 0, cos(deg_to_rad(yaw_deg)))
|
||||
var center := front_pos - fwd * d * 0.5
|
||||
@@ -145,19 +228,13 @@ func _kit_building(key: String, front_pos: Vector3, yaw_deg: float, node_name: S
|
||||
add_child(body)
|
||||
body.global_position = center
|
||||
body.rotation_degrees.y = yaw_deg
|
||||
var shape := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = Vector3(w, h, d)
|
||||
shape.shape = bs
|
||||
shape.position = Vector3(0, h * 0.5, 0)
|
||||
body.add_child(shape)
|
||||
|
||||
var inst: Node3D = _kit_scene(key).instantiate()
|
||||
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))
|
||||
_fit_mesh_colliders(body, inst)
|
||||
return w
|
||||
|
||||
|
||||
@@ -246,14 +323,36 @@ func _kit_fill_side(front_x: float, dir: int, z0: float, z1: float, seed_id: Str
|
||||
func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String = "",
|
||||
acoustic: String = "") -> StaticBody3D:
|
||||
var body := super._box_static(pos, size, color, node_name, acoustic)
|
||||
var law := _surface_law(size, node_name, acoustic)
|
||||
for mi in body.find_children("*", "MeshInstance3D", false, false):
|
||||
mi.mesh.surface_set_material(0, LevelMaterials.flat(color))
|
||||
mi.mesh.surface_set_material(0, LevelMaterials.flat(color, law))
|
||||
if maxf(size.x, maxf(size.y, size.z)) < 10.0:
|
||||
var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.003, 0.007, 0.018)
|
||||
mi.material_overlay = LevelMaterials.outline(w)
|
||||
return body
|
||||
|
||||
|
||||
## Choose a construction law from what the object is, not from its colour.
|
||||
## This is the map-side half of LevelMaterials.SURFACE_LAW: it finally applies
|
||||
## the shared surfacing kit to the actual city instead of only to the fidelity
|
||||
## probe. Thin horizontal slabs read as paving, structural masonry gets the
|
||||
## storey-scale wall grid, and machined metal gets tighter panels.
|
||||
func _surface_law(size: Vector3, node_name: String, acoustic: String) -> String:
|
||||
var footprint := minf(absf(size.x), absf(size.z))
|
||||
if node_name == "Ground" or (absf(size.y) <= 0.5 and footprint >= 2.0):
|
||||
return "ground"
|
||||
match acoustic:
|
||||
"concrete", "brick":
|
||||
if absf(size.y) >= 3.0 and maxf(absf(size.x), absf(size.z)) >= 4.0:
|
||||
return "wall"
|
||||
return "trim"
|
||||
"metal":
|
||||
return "panel"
|
||||
"wood":
|
||||
return "trim"
|
||||
return ""
|
||||
|
||||
|
||||
# ── Ad graphics (generated library: tools/generate_ads.py) ──────────────────
|
||||
|
||||
var _ad_mats: Dictionary = {}
|
||||
@@ -324,14 +423,15 @@ func _build_environment() -> void:
|
||||
# Under the filmic shoulder the ambient's lavender tint actually shows
|
||||
# (LINEAR used to clip it to white) — keep it lower so the sun shapes
|
||||
# surfaces and lit faces stay bright.
|
||||
env.environment.ambient_light_energy = 1.35
|
||||
env.environment.ambient_light_energy = 0.92
|
||||
env.environment.fog_density = 0.0007 # light city haze for depth over 600m
|
||||
env.environment.fog_light_color = Color(0.58, 0.43, 0.66)
|
||||
# Ambient occlusion grounds the modeled facade detail (ZZZ-style depth)
|
||||
env.environment.ssao_enabled = true
|
||||
env.environment.ssao_intensity = 2.0
|
||||
var sun := get_node_or_null("Sun")
|
||||
if sun:
|
||||
sun.light_energy = 2.5
|
||||
sun.light_energy = 1.85
|
||||
sun.rotation_degrees = Vector3(-38, 55, 0)
|
||||
sun.directional_shadow_max_distance = 220.0
|
||||
|
||||
@@ -339,6 +439,12 @@ func _build_environment() -> void:
|
||||
func _build_geometry() -> void:
|
||||
_build_environment()
|
||||
_build_lighting()
|
||||
# The play volume: just outside the perimeter wall, and tall enough to clear
|
||||
# the skyscrapers. Without one a player who grappled over that wall fell out
|
||||
# of the world with no warning and no death.
|
||||
CombatArea.add_to(self,
|
||||
Vector3(CITY_HALF * 2.0 + 26.0, 220.0, CITY_HALF * 2.0 + 26.0),
|
||||
Vector3(0, 96.0, 0))
|
||||
# Ground: one asphalt slab for the whole district
|
||||
_box_static(Vector3(0, -0.5, 0), Vector3(CITY_HALF * 2.0 + 16.0, 1.0, CITY_HALF * 2.0 + 16.0), ASPHALT, "Ground")
|
||||
# Perimeter wall
|
||||
@@ -465,12 +571,28 @@ func _standard_block(c: Vector3, bx: int, bz: int) -> void:
|
||||
# Detailed kit buildings fill both street-facing edges
|
||||
_kit_fill_side(c.x - half, 1, c.z - half, c.z + half, "%d_%dW" % [bx, bz])
|
||||
_kit_fill_side(c.x + half, -1, c.z - half, c.z + half, "%d_%dE" % [bx, bz])
|
||||
# Fire escapes into the mid-block gap on some blocks
|
||||
# A supported mid-block fire-escape scaffold on some blocks. The old
|
||||
# version was two unrelated slabs suspended in open air.
|
||||
if _rng.randi() % 2 == 0:
|
||||
var escape_h := FLOOR_H * 2.0 - 0.4
|
||||
for ex in [-1.1, 1.1]:
|
||||
for ez in [2.9, 5.1]:
|
||||
_box_static(c + Vector3(ex, escape_h * 0.5, ez),
|
||||
Vector3(0.12, escape_h, 0.12), METAL.darkened(0.18),
|
||||
"EscPost_%d_%d_%d_%d" % [bx, bz, int(ex), int(ez)], "metal")
|
||||
for f in range(1, 3):
|
||||
var y := float(f) * FLOOR_H - 0.4
|
||||
_box_static(c + Vector3(-2.0 + float(f) * 1.3, y, 4.0), Vector3(2.2, 0.16, 2.2), METAL,
|
||||
"Esc_%d_%d_%d" % [bx, bz, f], "metal")
|
||||
_box_static(c + Vector3(0, y, 4.0), Vector3(2.6, 0.16, 2.4),
|
||||
METAL, "EscLanding_%d_%d_%d" % [bx, bz, f], "metal")
|
||||
var stair_angle := rad_to_deg(atan2(FLOOR_H, 4.0))
|
||||
_ramp_static(c + Vector3(0, FLOOR_H * 0.45, 4.0),
|
||||
Vector3(1.2, 0.16, sqrt(FLOOR_H * FLOOR_H + 16.0)),
|
||||
Vector3(stair_angle, 0, 0), METAL.darkened(0.08),
|
||||
"EscStairGround_%d_%d" % [bx, bz], "metal")
|
||||
_ramp_static(c + Vector3(0, FLOOR_H + 1.6, 4.0),
|
||||
Vector3(1.2, 0.16, sqrt(FLOOR_H * FLOOR_H + 16.0)),
|
||||
Vector3(-stair_angle, 0, 0), METAL.darkened(0.08),
|
||||
"EscStair_%d_%d" % [bx, bz], "metal")
|
||||
# Sidewalk life: phone booth on some corners, bike rows by the fronts
|
||||
if _rng.randi() % 4 == 0:
|
||||
_hero("phonebooth", c + Vector3(-half - 0.6, 0.1, -half - 0.6), _rng.randf_range(0.0, 360.0),
|
||||
@@ -506,16 +628,11 @@ func _tower_block(c: Vector3, bx: int, bz: int) -> void:
|
||||
add_child(body)
|
||||
body.global_position = c + Vector3(0, FLOOR_H, 0)
|
||||
body.rotation_degrees.y = yaw
|
||||
var shape := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = Vector3(dims.x * s, dims.y * s, dims.z * s)
|
||||
shape.shape = bs
|
||||
shape.position = Vector3(0, dims.y * s * 0.5, 0)
|
||||
body.add_child(shape)
|
||||
var inst: Node3D = _kit_scene(key).instantiate()
|
||||
inst.scale = Vector3.ONE * s
|
||||
body.add_child(inst)
|
||||
LevelMaterials.apply_toon_recursive(inst, 0.0)
|
||||
_fit_mesh_colliders(body, inst)
|
||||
# Rooftop crown billboard both directions (skyline wayfinding)
|
||||
var tw := dims.x * s
|
||||
_box_static(c + Vector3(0, h + 2.4, 0), Vector3(0.6, 4.0, tw - 4.0), METAL, n + "_Crown", "metal")
|
||||
@@ -542,7 +659,9 @@ func _shrine_block(c: Vector3, bx: int, bz: int) -> void:
|
||||
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")
|
||||
# Kitbashed torii (curved kasagi) at the south entry; pillar colliders
|
||||
var tz := c.z + (BLOCK - 6.0) * 0.5
|
||||
# Local south-edge offset. This used to include c.z and was then added to c
|
||||
# a second time, launching shrine gates/fences far outside their blocks.
|
||||
var tz := (BLOCK - 6.0) * 0.5
|
||||
_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
|
||||
@@ -610,7 +729,7 @@ func _plaza_block(c: Vector3, bx: int, bz: int) -> void:
|
||||
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")
|
||||
_kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 1.0, p.z), float(i) * 90.0, 4.5)
|
||||
_kit_prop_path("res://assets/props/nature/tree_oak.glb", Vector3(p.x, 0.16, 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")
|
||||
@@ -699,20 +818,10 @@ func _build_rail_line() -> void:
|
||||
fb.set_meta("acoustic_material", "metal")
|
||||
add_child(fb)
|
||||
fb.global_position = Vector3(0, 0, fb_z) # span runs along x, across the avenue
|
||||
var fb_shape := CollisionShape3D.new()
|
||||
var fb_box := BoxShape3D.new()
|
||||
fb_box.size = Vector3(18.0, 0.4, 3.0)
|
||||
fb_shape.shape = fb_box
|
||||
fb_shape.position = Vector3(0, 5.5, 0)
|
||||
fb.add_child(fb_shape)
|
||||
var fb_inst: Node3D = _kit_scene_at("res://assets/props/hero/footbridge.glb").instantiate()
|
||||
fb.add_child(fb_inst)
|
||||
LevelMaterials.apply_toon_recursive(fb_inst, 0.005)
|
||||
# Walkable stair ramps at both ends (over the stair visuals)
|
||||
for e in [-1.0, 1.0]:
|
||||
var ang := rad_to_deg(atan2(5.5, 6.4))
|
||||
_ramp_static(Vector3(e * (9.0 + 3.2), 2.65, fb_z), Vector3(2.8, 0.3, 8.6),
|
||||
Vector3(0, 0, -ang * e), PAVING, "FootbridgeStair_%d" % int(e), "metal").visible = false
|
||||
_fit_mesh_colliders(fb, fb_inst)
|
||||
|
||||
# Slide ramps to the deck at the two main-avenue crossings
|
||||
for sx in [-PITCH * 0.5 - 4.0, PITCH * 0.5 + 4.0]:
|
||||
@@ -746,15 +855,140 @@ func _road_piece(key: String, pos: Vector3, yaw_deg: float, sc: Vector3) -> void
|
||||
|
||||
|
||||
var _path_cache: Dictionary = {}
|
||||
var _power_cable_index := 0
|
||||
var _power_curve_index := 0
|
||||
var _utility_terminal_index := 0
|
||||
func _kit_scene_at(path: String) -> PackedScene:
|
||||
if not _path_cache.has(path):
|
||||
_path_cache[path] = load(path)
|
||||
return _path_cache[path]
|
||||
|
||||
|
||||
func _cable_segment(from: Vector3, to: Vector3, curve_id: int,
|
||||
segment_index: int, segment_count: int) -> void:
|
||||
var direction := to - from
|
||||
var cable := MeshInstance3D.new()
|
||||
cable.name = "PowerCable_%d" % _power_cable_index
|
||||
_power_cable_index += 1
|
||||
var mesh := BoxMesh.new()
|
||||
mesh.size = Vector3(0.055, 0.055, direction.length() + 0.02)
|
||||
mesh.material = LevelMaterials.flat(INK_COLOR)
|
||||
cable.mesh = mesh
|
||||
add_child(cable)
|
||||
cable.look_at_from_position((from + to) * 0.5, to, Vector3.UP)
|
||||
cable.set_meta("cable_from", from)
|
||||
cable.set_meta("cable_to", to)
|
||||
cable.set_meta("curve_id", curve_id)
|
||||
cable.set_meta("segment_index", segment_index)
|
||||
cable.set_meta("segment_count", segment_count)
|
||||
|
||||
|
||||
func _utility_terminal(pos: Vector3, terminal_kind: String) -> void:
|
||||
var marker := Node3D.new()
|
||||
marker.name = "UtilityTerminal_%04d" % _utility_terminal_index
|
||||
_utility_terminal_index += 1
|
||||
marker.position = pos
|
||||
marker.set_meta("utility_terminal", true)
|
||||
marker.set_meta("terminal_kind", terminal_kind)
|
||||
add_child(marker)
|
||||
|
||||
|
||||
func _cable_curve(from: Vector3, to: Vector3, sag: float,
|
||||
segments: int = 8) -> void:
|
||||
var curve_id := _power_curve_index
|
||||
_power_curve_index += 1
|
||||
for segment in segments:
|
||||
var t0 := float(segment) / float(segments)
|
||||
var t1 := float(segment + 1) / float(segments)
|
||||
var p0 := from.lerp(to, t0) - Vector3.UP * sin(t0 * PI) * sag
|
||||
var p1 := from.lerp(to, t1) - Vector3.UP * sin(t1 * PI) * sag
|
||||
_cable_segment(p0, p1, curve_id, segment, segments)
|
||||
|
||||
|
||||
func _utility_pole(pos: Vector3, height: float, pole_id: String) -> void:
|
||||
var pole := _box_static(
|
||||
pos + Vector3(0, height * 0.5, 0),
|
||||
Vector3(0.24, height, 0.24), METAL.darkened(0.35),
|
||||
"UtilityPole_" + pole_id, "wood")
|
||||
for mesh in pole.find_children("*", "MeshInstance3D", false, false):
|
||||
mesh.material_override = LevelMaterials.flat(METAL.darkened(0.35))
|
||||
_deco_box(pos + Vector3(0, height, 0),
|
||||
Vector3(1.35, 0.12, 0.16), METAL.darkened(0.2))
|
||||
for lane in 3:
|
||||
var lane_x := -0.42 + float(lane) * 0.42
|
||||
var terminal := pos + Vector3(lane_x, height + 0.12, 0)
|
||||
_deco_box(terminal,
|
||||
Vector3(0.10, 0.16, 0.10), Color(0.32, 0.28, 0.34))
|
||||
_utility_terminal(terminal, "pole")
|
||||
# A transformer and grounded service cabinet make these read as utility
|
||||
# poles rather than unexplained posts holding decorative strings.
|
||||
_deco_box(pos + Vector3(0, height - 1.25, 0),
|
||||
Vector3(0.56, 0.82, 0.42), METAL.darkened(0.08))
|
||||
_box_static(pos + Vector3(0.38, 0.55, 0), Vector3(0.62, 1.1, 0.48),
|
||||
METAL.darkened(0.16), "UtilityCabinet_" + pole_id, "metal")
|
||||
|
||||
|
||||
func _storefront_service_drop(pole_pos: Vector3, height: float,
|
||||
front_x: float, z: float, side: float, _service_id: String) -> void:
|
||||
# Terminate on a facade service mast one metre outside the decorative
|
||||
# sign/sill layer, then bracket that mast back to the wall. Driving the wire
|
||||
# directly into the facade let it pass through three protruding sign columns.
|
||||
var wall_x := front_x - side * 1.05
|
||||
var service_height := 7.25
|
||||
var pole_terminal := pole_pos + Vector3(side * 0.42, height + 0.12, 0)
|
||||
var wall_terminal := Vector3(wall_x, service_height, z)
|
||||
_utility_terminal(wall_terminal, "building_service")
|
||||
_cable_curve(pole_terminal, wall_terminal, 0.16, 5)
|
||||
# Junction box and conduit sit on the facade plane and carry the line down
|
||||
# to street level: a visually meaningful endpoint.
|
||||
_deco_box(wall_terminal, Vector3(0.16, 0.40, 0.46),
|
||||
METAL.darkened(0.12))
|
||||
_deco_box(Vector3(wall_x, service_height * 0.5, z),
|
||||
Vector3(0.08, service_height, 0.10), METAL.darkened(0.25))
|
||||
_deco_box(Vector3((wall_x + front_x) * 0.5, service_height, z),
|
||||
Vector3(absf(front_x - wall_x), 0.08, 0.10), METAL.darkened(0.2))
|
||||
|
||||
|
||||
func _build_utility_network() -> void:
|
||||
var lines := _street_lines()
|
||||
var pole_zs: Array[float] = []
|
||||
for row in GRID:
|
||||
pole_zs.append(_cell_center(0, row).z)
|
||||
for line_index in range(0, lines.size(), 2):
|
||||
var street_x: float = lines[line_index]
|
||||
if absf(street_x) < 1.0:
|
||||
continue
|
||||
var side := 1.0 if line_index % 4 == 0 else -1.0
|
||||
# Stay between the road edge and the furthest storefront sign/canopy.
|
||||
# The previous 1.15 m offset grazed sign columns that project 0.8 m
|
||||
# from the facade; 0.55 m is the clear sidewalk utility corridor.
|
||||
var pole_x := street_x + side * (ROAD_W * 0.5 + 0.55)
|
||||
var height := 11.35
|
||||
for row in GRID:
|
||||
var pole_pos := Vector3(pole_x, 0, pole_zs[row])
|
||||
_utility_pole(pole_pos, height, "%d_%d" % [line_index, row])
|
||||
if row < GRID - 1:
|
||||
var next_pos := Vector3(pole_x, 0, pole_zs[row + 1])
|
||||
for lane in 3:
|
||||
var lane_x := -0.42 + float(lane) * 0.42
|
||||
_cable_curve(
|
||||
pole_pos + Vector3(lane_x, height + 0.12, 0),
|
||||
next_pos + Vector3(lane_x, height + 0.12, 0),
|
||||
0.58, 9)
|
||||
var adjacent_bx := line_index + 1 if side > 0.0 else line_index
|
||||
if adjacent_bx < 0 or adjacent_bx >= GRID:
|
||||
continue
|
||||
var block_type := LAYOUT[row][adjacent_bx]
|
||||
if block_type != "S" and block_type != "M":
|
||||
continue
|
||||
var front_x := street_x + side * (STREET * 0.5)
|
||||
_storefront_service_drop(
|
||||
pole_pos, height, front_x, pole_zs[row], side,
|
||||
"%d_%d" % [line_index, row])
|
||||
|
||||
|
||||
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:
|
||||
@@ -776,24 +1010,7 @@ func _build_streets() -> void:
|
||||
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
|
||||
_build_utility_network()
|
||||
|
||||
|
||||
# ── Main avenues: kit streetlights ───────────────────────────────────────────
|
||||
@@ -801,16 +1018,21 @@ func _build_streets() -> void:
|
||||
func _build_avenues() -> void:
|
||||
var lines := _street_lines()
|
||||
var lamp_scale := 10.0
|
||||
# Keep pole bases a full character radius behind the 12 m road mesh. The
|
||||
# old +0.4 m curb offset let the imported curved base lean into traffic.
|
||||
var sidewalk_pole_offset := ROAD_W * 0.5 + 1.45
|
||||
for z in lines:
|
||||
for side: float in [-1.0, 1.0]:
|
||||
# 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")
|
||||
var x := side * sidewalk_pole_offset
|
||||
var lamp_z := z + side * sidewalk_pole_offset
|
||||
_box_static(Vector3(x, 3.4, lamp_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)
|
||||
Vector3(x, 0, lamp_z), 90.0 if side > 0 else -90.0, lamp_scale)
|
||||
# Traffic signal on the opposite diagonal corner of each crossing
|
||||
_kit_prop_path("res://assets/props/roads/light-square-cross.glb",
|
||||
Vector3(x, 0, z - side * (ROAD_W * 0.5 + 0.8)), 90.0 if side > 0 else -90.0, lamp_scale * 0.8)
|
||||
Vector3(x, 0, z - side * sidewalk_pole_offset),
|
||||
90.0 if side > 0 else -90.0, lamp_scale * 0.8)
|
||||
# Bus stop shelters on alternating blocks along the avenue
|
||||
if int(roundf(z / PITCH)) % 2 == 0:
|
||||
_hero("busstop", Vector3(ROAD_W * 0.5 + 2.0, 0.02, z + 18.0), -90.0,
|
||||
@@ -822,9 +1044,10 @@ func _build_avenues() -> void:
|
||||
continue
|
||||
# Single lights down the central E-W avenue
|
||||
for side: float in [-1.0, 1.0]:
|
||||
var z2 := side * (ROAD_W * 0.5 + 0.4)
|
||||
var z2 := side * sidewalk_pole_offset
|
||||
_kit_prop_path("res://assets/props/roads/light-curved.glb",
|
||||
Vector3(x, 0, z2), 0.0 if side > 0 else 180.0, lamp_scale)
|
||||
Vector3(x + side * sidewalk_pole_offset, 0, z2),
|
||||
0.0 if side > 0 else 180.0, lamp_scale)
|
||||
|
||||
|
||||
# ── Parked cars (Kenney Car Kit) ─────────────────────────────────────────────
|
||||
@@ -843,16 +1066,9 @@ func _parked_car(pos: Vector3, yaw_deg: float) -> void:
|
||||
add_child(body)
|
||||
body.global_position = pos
|
||||
body.rotation_degrees.y = yaw_deg + _rng.randf_range(-3.0, 3.0)
|
||||
var shape := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = Vector3(2.8, 2.6, 5.0)
|
||||
shape.shape = bs
|
||||
shape.position = Vector3(0, 1.3, 0)
|
||||
body.add_child(shape)
|
||||
|
||||
var inst: Node3D = _kit_scene_at("res://assets/props/cars/" + key + ".glb").instantiate()
|
||||
inst.scale = Vector3.ONE * CAR_SCALE
|
||||
inst.position.y = 0.55 # wheel contact at ground after scaling
|
||||
body.add_child(inst)
|
||||
var wheel_scene := _kit_scene_at("res://assets/props/cars/wheel-default.glb")
|
||||
for n in inst.find_children("wheel*", "Node3D", true, false):
|
||||
@@ -860,8 +1076,16 @@ func _parked_car(pos: Vector3, yaw_deg: float) -> void:
|
||||
var wheel: Node3D = wheel_scene.instantiate()
|
||||
n.add_child(wheel)
|
||||
LevelMaterials.apply_toon_recursive(inst, 0.0)
|
||||
# Darken under the map lighting (kit albedo clips) + per-car hue
|
||||
_tint_recursive(inst, Color(0.62, 0.62, 0.66) * Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.6, 1.0), 0.35))
|
||||
# Kenney cars carry their paint/window/wheel palette as vertex colors.
|
||||
# Toon conversion now preserves it; keep the seeded tint subtle enough that
|
||||
# those authored regions remain legible.
|
||||
_tint_recursive(inst, Color(0.82, 0.82, 0.86) \
|
||||
* Color(1, 1, 1).lerp(Color.from_hsv(_rng.randf(), 0.55, 1.0), 0.22))
|
||||
# Imported origins vary by car. Seat every variant by its actual lowest
|
||||
# rendered point, then derive collision from the grounded visual.
|
||||
var bounds := _visual_bounds_in(inst, body)
|
||||
inst.position.y -= bounds.position.y
|
||||
_fit_mesh_colliders(body, inst)
|
||||
|
||||
|
||||
## Seeded parking lanes hugging the curbs of the N-S streets.
|
||||
@@ -886,8 +1110,17 @@ func _kit_prop_path(path: String, pos: Vector3, yaw_deg: float, scale_f: float)
|
||||
add_child(inst)
|
||||
inst.global_position = pos
|
||||
inst.rotation_degrees.y = yaw_deg
|
||||
inst.set_meta("source_asset_path", path)
|
||||
# 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)
|
||||
# `pos.y` is always the intended support plane (ground, pedestal, or facade
|
||||
# attachment). Imported origins vary, so align the actual rendered bottom
|
||||
# to that plane for every kit prop. The old lantern/awning exceptions were
|
||||
# the source of many inexplicable floating pieces.
|
||||
var bounds := _visual_bounds_in(inst, self)
|
||||
inst.global_position.y += pos.y - bounds.position.y
|
||||
inst.set_meta("support_fitted", true)
|
||||
inst.set_meta("support_plane_y", pos.y)
|
||||
|
||||
Reference in New Issue
Block a user