Smashable street furniture; fix levels losing every texture on rebuild
Street lamps, bollards and bins are no longer baked scenery. The builder emits SL_/BO_/WB_ markers off the OSM nodes and game.gd spawns them as RigidBodies with a low centre of mass, so clipping a lamp at 100 topples it instead of driving through a painted pole. ~206 physics bodies per level (58 parked cars, 60 lamps, 88 bollards/bins), all asleep until hit. Verified by ramming one: 23.6 deg tilt, launched at 18 m/s. Fixes a self-inflicted trap from the last commit: level_*.jpg are Godot's extraction of the GLB's embedded textures, NOT Blender output. build_all cleared them to avoid orphans but left level.glb.import, so Godot's cache decided nothing had changed and never re-extracted -- queen_st_mall shipped with 0 of its 31 textures, loading fine but rendering untextured. The .import now goes with them. Also confirmed the Wheel of Brisbane pivot really does turn (0.086 rad over 120 frames, 0.000 m centre drift) -- it was previously untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1db71420a3
commit
e9717326f4
13
README.md
13
README.md
@ -104,11 +104,14 @@ whole footway network — so most of the detail is real Brisbane, not invented:
|
||||
- **Trees** are a trunk plus three offset welded spheres (figs) or radiating
|
||||
fronds (palms), scattered along kerbs on top of the ones OSM maps — Brisbane
|
||||
has far more street trees than are tagged.
|
||||
- **Smashables**: the builder drops `PP_`/`WB_` marker empties (kerbside
|
||||
parking + carpark rows from `amenity=parking`, wheelie bins on footpaths);
|
||||
game.gd fills them at load with shuntable RigidBody fleet cars and council
|
||||
bins (asleep until rammed, skipped in junction mode to keep the puzzles
|
||||
deterministic).
|
||||
- **Smashables**: the builder drops marker empties and game.gd fills them at
|
||||
load with RigidBodies — `PP_` kerbside/carpark parking spots become shuntable
|
||||
fleet cars, and `SL_`/`BO_`/`WB_` (OSM street lamps, bollards, bins) become
|
||||
topple-able street furniture with a low centre of mass so a clipped lamp post
|
||||
goes over rather than helicoptering. All asleep until rammed, and skipped
|
||||
entirely in junction mode to keep those puzzles deterministic. Lamps and
|
||||
bollards are deliberately *not* baked into the level mesh — a static pole you
|
||||
drive through is worse than no pole.
|
||||
- **Brisbane colour**: red busway asphalt, jacaranda-purple canopy on ~1 in 5
|
||||
street trees, Queenslander weatherboard + corrugated iron on `building=house`
|
||||
footprints, fictional shop-fascia signs above awnings and rooftop billboards
|
||||
|
||||
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://2sfe3g28na0f"
|
||||
uid="uid://lekue6r6vs5u"
|
||||
path="res://.godot/imported/level.glb-4aa597fe23a02438c7c91aac8329adb8.scn"
|
||||
|
||||
[deps]
|
||||
@ -42,4 +42,4 @@ materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=0
|
||||
gltf/texture_map_mode=1
|
||||
|
||||
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://dgc141hgvu6r0"
|
||||
uid="uid://cik0c7knt33ep"
|
||||
path="res://.godot/imported/level.glb-30461b4810ef38297351fb89cbe9aa18.scn"
|
||||
|
||||
[deps]
|
||||
@ -42,4 +42,4 @@ materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=0
|
||||
gltf/texture_map_mode=1
|
||||
|
||||
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://c674y4fs837pi"
|
||||
uid="uid://bo4jjuhgers8e"
|
||||
path="res://.godot/imported/level.glb-f093d0a503a60c4e982eefdc88ad879e.scn"
|
||||
|
||||
[deps]
|
||||
@ -42,4 +42,4 @@ materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=0
|
||||
gltf/texture_map_mode=1
|
||||
|
||||
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bbivnp8wb7dw6"
|
||||
uid="uid://ce6dmcne00017"
|
||||
path="res://.godot/imported/level.glb-17af9cdeb05fd15a816097b7ae0fb96c.scn"
|
||||
|
||||
[deps]
|
||||
@ -42,4 +42,4 @@ materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=0
|
||||
gltf/texture_map_mode=1
|
||||
|
||||
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bggawagdgkqmy"
|
||||
uid="uid://dah3x674j2af"
|
||||
path="res://.godot/imported/level.glb-045fc5cf8c0e529ecdc2678ee7acde2a.scn"
|
||||
|
||||
[deps]
|
||||
@ -42,4 +42,4 @@ materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=0
|
||||
gltf/texture_map_mode=1
|
||||
|
||||
40
shot_sb_bridge.png.import
Normal file
40
shot_sb_bridge.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dpv2rx6laf2w3"
|
||||
path="res://.godot/imported/shot_sb_bridge.png-3a49e069a8658d202658083a9b3802d5.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://shot_sb_bridge.png"
|
||||
dest_files=["res://.godot/imported/shot_sb_bridge.png-3a49e069a8658d202658083a9b3802d5.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
shot_smash.png.import
Normal file
40
shot_smash.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cfi0qm6la5und"
|
||||
path="res://.godot/imported/shot_smash.png-74c714eba18dc662dd518b4cb8cf6335.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://shot_smash.png"
|
||||
dest_files=["res://.godot/imported/shot_smash.png-74c714eba18dc662dd518b4cb8cf6335.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
91
src/game.gd
91
src/game.gd
@ -17,7 +17,14 @@ const CRASH_TIME := 60.0
|
||||
const RIVAL_COUNT := 3
|
||||
const TRAFFIC_COUNT := {"cruise": 8, "crash": 12, "race": 4}
|
||||
const PARKED_MAX := 50 # PP_ markers filled with shuntable fleet cars
|
||||
const BINS_MAX := 40 # WB_ markers -> wheelie bins (light, they FLY)
|
||||
## Street furniture that topples. Marker prefix -> size, mass, colour.
|
||||
## Deliberately physics, not scenery: clipping a lamp post at 120 and watching
|
||||
## it go over is the whole point of the genre.
|
||||
const SMASHABLE := {
|
||||
"WB": {"size": Vector3(0.55, 1.0, 0.6), "mass": 35.0, "col": Color(0.14, 0.30, 0.13), "max": 45},
|
||||
"BO": {"size": Vector3(0.22, 0.95, 0.22), "mass": 45.0, "col": Color(0.20, 0.21, 0.23), "max": 60},
|
||||
"SL": {"size": Vector3(0.26, 7.4, 0.26), "mass": 190.0, "col": Color(0.24, 0.25, 0.27), "max": 60},
|
||||
}
|
||||
const JCT_RUN_TIME := 30.0 # crash into something before this or the run ends
|
||||
const JCT_SETTLE := 9.0 # aftertouch/crashbreaker window after the crash
|
||||
const ROAM_RIVALS := 2 # challengers circling the RacePath in cruise
|
||||
@ -242,32 +249,62 @@ func _spawn_smashables(level: Node) -> void:
|
||||
Basis(Vector3.UP, t.basis.get_euler().y), Vector3(t.origin.x, 0.75, t.origin.z))
|
||||
b.sleeping = true
|
||||
n += 1
|
||||
var bin_mesh := BoxMesh.new()
|
||||
bin_mesh.size = Vector3(0.55, 1.0, 0.6)
|
||||
var bin_mat := StandardMaterial3D.new()
|
||||
bin_mat.albedo_color = Color(0.14, 0.30, 0.13) # council green
|
||||
bin_mat.roughness = 0.85
|
||||
bin_mesh.material = bin_mat
|
||||
n = 0
|
||||
for m in level.find_children("WB_*", "Node3D", true, false):
|
||||
if n >= BINS_MAX:
|
||||
break
|
||||
var b := RigidBody3D.new()
|
||||
b.mass = 35.0
|
||||
var cs := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = bin_mesh.size
|
||||
cs.shape = bs
|
||||
b.add_child(cs)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = bin_mesh
|
||||
b.add_child(mi)
|
||||
add_child(b)
|
||||
var t: Transform3D = (m as Node3D).global_transform
|
||||
b.global_transform = Transform3D(
|
||||
Basis(Vector3.UP, t.basis.get_euler().y), Vector3(t.origin.x, 0.55, t.origin.z))
|
||||
b.sleeping = true
|
||||
n += 1
|
||||
for prefix in SMASHABLE:
|
||||
var spec: Dictionary = SMASHABLE[prefix]
|
||||
var size: Vector3 = spec["size"]
|
||||
var mesh := BoxMesh.new()
|
||||
mesh.size = size
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = spec["col"]
|
||||
mat.roughness = 0.8
|
||||
mat.metallic = 0.3
|
||||
mesh.material = mat
|
||||
n = 0
|
||||
for m in level.find_children("%s_*" % prefix, "Node3D", true, false):
|
||||
if n >= int(spec["max"]):
|
||||
break
|
||||
var b := RigidBody3D.new()
|
||||
b.mass = spec["mass"]
|
||||
# low centre of mass so a clipped lamp post topples rather than
|
||||
# helicoptering off into the skybox
|
||||
b.center_of_mass_mode = RigidBody3D.CENTER_OF_MASS_MODE_CUSTOM
|
||||
b.center_of_mass = Vector3(0, -size.y * 0.3, 0)
|
||||
var cs := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = size
|
||||
cs.shape = bs
|
||||
b.add_child(cs)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
b.add_child(mi)
|
||||
if prefix == "SL":
|
||||
# arm + lantern, visual only -- collision stays the plain pole box
|
||||
var arm := MeshInstance3D.new()
|
||||
var am := BoxMesh.new()
|
||||
am.size = Vector3(1.5, 0.12, 0.12)
|
||||
am.material = mat
|
||||
arm.mesh = am
|
||||
arm.position = Vector3(0.75, size.y * 0.5 - 0.25, 0)
|
||||
b.add_child(arm)
|
||||
var head := MeshInstance3D.new()
|
||||
var hm := BoxMesh.new()
|
||||
hm.size = Vector3(0.8, 0.14, 0.4)
|
||||
var hmat := StandardMaterial3D.new()
|
||||
hmat.albedo_color = Color(0.86, 0.84, 0.72)
|
||||
hmat.emission_enabled = true
|
||||
hmat.emission = Color(0.9, 0.85, 0.6)
|
||||
hmat.emission_energy_multiplier = 0.35
|
||||
hm.material = hmat
|
||||
head.mesh = hm
|
||||
head.position = Vector3(1.45, size.y * 0.5 - 0.36, 0)
|
||||
b.add_child(head)
|
||||
add_child(b)
|
||||
var t: Transform3D = (m as Node3D).global_transform
|
||||
b.global_transform = Transform3D(
|
||||
Basis(Vector3.UP, t.basis.get_euler().y),
|
||||
Vector3(t.origin.x, size.y * 0.5 + 0.02, t.origin.z))
|
||||
b.sleeping = true
|
||||
n += 1
|
||||
|
||||
func _build_environment() -> void:
|
||||
## Brisbane midday: hard high sun, deep blue zenith, everything a bit blown out.
|
||||
|
||||
@ -10,9 +10,12 @@ while IFS='|' read -r id json lat lon sway loop bridge canopy; do
|
||||
ARGS=$(python3 tools/osm_level_args.py "$C/$json" "$sway" "$loop") || { echo "ARGS FAILED $id"; continue; }
|
||||
SPAWN=$(echo "$ARGS" | head -1)
|
||||
RP=$(echo "$ARGS" | tail -1)
|
||||
# the glTF export drops textures next to the .glb; clear them first or
|
||||
# renamed/dropped materials leave orphans behind forever
|
||||
rm -f "levels/$id"/level_*.jpg "levels/$id"/level_*.jpg.import
|
||||
# level_*.jpg are Godot's extraction of the GLB's embedded textures, not
|
||||
# Blender's output. Clear them so renamed/dropped materials don't leave
|
||||
# orphans -- but the .glb.import MUST go too, or Godot's cache decides
|
||||
# nothing changed and never re-extracts, leaving a level with no textures
|
||||
# at all (loads, renders untextured, spams "Resource file not found").
|
||||
rm -f "levels/$id"/level_*.jpg "levels/$id"/level_*.jpg.import "levels/$id"/level.glb.import
|
||||
"$B" -b -P tools/build_level_osm.py -- "$C/$json" "$id" "$lat" "$lon" "$SPAWN" "$RP" "$bridge" "$canopy" 2>&1 \
|
||||
| grep -E '^(level built|bridge truss|canopies built)'
|
||||
done <<'EOF'
|
||||
|
||||
@ -289,14 +289,6 @@ def add_tree(fol, jac, frond, trunk, x, y, rng):
|
||||
(r * rng.uniform(0.75, 1.05), r * rng.uniform(0.75, 1.05), r * rng.uniform(0.55, 0.75)),
|
||||
subdiv=2)
|
||||
|
||||
def add_lamp(bm, x, y, rng):
|
||||
h = 7.5
|
||||
cone(bm, (x, y, h / 2), 0.13, 0.09, h, 6)
|
||||
arm = Vector((math.cos(rng.random() * math.tau), math.sin(rng.random() * math.tau), 0)) * 1.5
|
||||
beam(bm, Vector((x, y, h - 0.2)), Vector((x, y, h - 0.2)) + arm, 0.11, 0.11)
|
||||
slab(bm, [(x + arm.x - 0.4, y + arm.y - 0.25, h - 0.35), (x + arm.x + 0.4, y + arm.y - 0.25, h - 0.35),
|
||||
(x + arm.x + 0.4, y + arm.y + 0.25, h - 0.35), (x + arm.x - 0.4, y + arm.y + 0.25, h - 0.35)], 0.12)
|
||||
|
||||
def add_bench(bm, x, y, rng):
|
||||
a = rng.random() * math.tau
|
||||
d = Vector((math.cos(a), math.sin(a), 0))
|
||||
@ -308,12 +300,6 @@ def add_bench(bm, x, y, rng):
|
||||
slab(bm, [(a0 + p * 0.28).to_tuple(), (a0 - p * 0.28).to_tuple(),
|
||||
(a0 - p * 0.28 + d * 1.9).to_tuple(), (a0 + p * 0.28 + d * 1.9).to_tuple()], 0.08)
|
||||
|
||||
def add_bin(bm, x, y, rng):
|
||||
cone(bm, (x, y, 0.45), 0.30, 0.27, 0.9, 8)
|
||||
|
||||
def add_bollard(bm, x, y, rng):
|
||||
cone(bm, (x, y, 0.45), 0.10, 0.09, 0.9, 6)
|
||||
|
||||
def add_signal(bm, lens, x, y, rng):
|
||||
"""Aussie traffic signal: pole, lantern head, mast arm with a second head.
|
||||
Lenses go in their own bmesh so they can be emissive."""
|
||||
@ -387,7 +373,7 @@ def add_canopy(bm, at, fwd):
|
||||
d = json.load(open(src))
|
||||
counts = dict.fromkeys(
|
||||
("bld", "road", "path", "park", "planter", "water", "cliff", "tree", "prop",
|
||||
"cross", "signal", "pole"), 0)
|
||||
"cross", "signal", "pole", "pp", "sl", "bo", "wb"), 0)
|
||||
GREEN_LEISURE = ("park", "recreation_ground", "pitch", "playground", "golf_course", "nature_reserve")
|
||||
GREEN_LANDUSE = ("grass", "recreation_ground", "village_green", "meadow", "forest")
|
||||
PLANTER_LEISURE = ("garden",)
|
||||
@ -395,8 +381,11 @@ SKIP_HW = ("steps", "corridor", "platform", "elevator", "proposed", "constructio
|
||||
ROAD_W = {"motorway": 11.0, "trunk": 10.0, "primary": 9.0, "secondary": 8.0,
|
||||
"tertiary": 7.0, "pedestrian": 16.0, "residential": 6.5, "busway": 7.0}
|
||||
PATH_W = {"footway": 2.6, "path": 2.2, "cycleway": 2.4, "pedestrian": 4.0}
|
||||
PROPS = {"tree": None, "street_lamp": add_lamp, "bench": add_bench,
|
||||
"waste_basket": add_bin, "bollard": add_bollard, "bus_stop": None}
|
||||
# Baked decorative props. Lamps, bollards and bins are deliberately absent:
|
||||
# they're the things you clip at speed, so they become markers that game.gd
|
||||
# fills with topple-able RigidBodies instead of static scenery.
|
||||
PROPS = {"bench": add_bench}
|
||||
SMASHABLE = {"street_lamp": "SL", "bollard": "BO", "waste_basket": "WB"}
|
||||
|
||||
fol_bm, frond_bm, trunk_bm = bmesh.new(), bmesh.new(), bmesh.new()
|
||||
prop_bm, glass_bm = bmesh.new(), bmesh.new()
|
||||
@ -407,7 +396,9 @@ bill_bm = [bmesh.new(), bmesh.new(), bmesh.new()] # one per ad design
|
||||
sig_bm, lens_bm = bmesh.new(), bmesh.new()
|
||||
pole_bm, wire_bm = bmesh.new(), bmesh.new()
|
||||
pp_spots, wb_spots = [], [] # (x, y, yaw): parked-car / wheelie-bin markers for game.gd to fill
|
||||
smash_spots = {"SL": [], "BO": [], "WB": []} # street lamps / bollards / bins off OSM
|
||||
PP_CAP, WB_CAP = 70, 40
|
||||
SMASH_CAP = {"SL": 90, "BO": 90, "WB": 60}
|
||||
PARK_STREETS = ("residential", "tertiary", "secondary", "unclassified")
|
||||
tree_sites = [] # (x, y, seed) -- mapped trees first, then scattered
|
||||
road_ways = [] # (pts, width) for street-tree scattering
|
||||
@ -428,7 +419,9 @@ for e in d["elements"]:
|
||||
elif kind == "traffic_signals":
|
||||
add_signal(sig_bm, lens_bm, px, py, rng)
|
||||
counts["signal"] += 1
|
||||
elif kind in PROPS and PROPS[kind]:
|
||||
elif kind in SMASHABLE:
|
||||
smash_spots[SMASHABLE[kind]].append((px, py, rng.random() * math.tau))
|
||||
elif kind in PROPS:
|
||||
PROPS[kind](prop_bm, px, py, rng)
|
||||
counts["prop"] += 1
|
||||
continue
|
||||
@ -876,14 +869,15 @@ add_obj("ground-convcol", bm, [M_GROUND], 18.0)
|
||||
# parked-car / wheelie-bin markers -> empties; game.gd fills them with
|
||||
# shuntable RigidBodies at load (a static GLB can't carry physics bodies)
|
||||
random.Random(1982).shuffle(pp_spots)
|
||||
for prefix, spots, cap in (("PP", pp_spots, PP_CAP), ("WB", wb_spots, WB_CAP)):
|
||||
smash_spots["WB"] += wb_spots # OSM street bins join the procedural wheelie bins
|
||||
batches = [("PP", pp_spots, PP_CAP)] + [(k, v, SMASH_CAP[k]) for k, v in smash_spots.items()]
|
||||
for prefix, spots, cap in batches:
|
||||
for i, (mx, my, yaw) in enumerate(spots[:cap]):
|
||||
emp = bpy.data.objects.new("%s_%03d" % (prefix, i), None)
|
||||
emp.location = (mx, my, 0.05)
|
||||
emp.rotation_euler = (0, 0, yaw)
|
||||
bpy.context.collection.objects.link(emp)
|
||||
counts["pp"] = min(len(pp_spots), PP_CAP)
|
||||
counts["wb"] = min(len(wb_spots), WB_CAP)
|
||||
counts[prefix.lower()] = min(len(spots), cap)
|
||||
|
||||
if spawn_arg:
|
||||
slat, slon, sbear = [float(v) for v in spawn_arg.split(",")]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user