diff --git a/README.md b/README.md index 69e84f5..6656f82 100644 --- a/README.md +++ b/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 diff --git a/levels/albert_parklands/level.glb b/levels/albert_parklands/level.glb index 71ea6bc..dce32ba 100644 Binary files a/levels/albert_parklands/level.glb and b/levels/albert_parklands/level.glb differ diff --git a/levels/albert_parklands/level.glb.import b/levels/albert_parklands/level.glb.import index 8993373..f8f759b 100644 --- a/levels/albert_parklands/level.glb.import +++ b/levels/albert_parklands/level.glb.import @@ -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 diff --git a/levels/kangaroo_point/level.glb b/levels/kangaroo_point/level.glb index 77a4606..eef9030 100644 Binary files a/levels/kangaroo_point/level.glb and b/levels/kangaroo_point/level.glb differ diff --git a/levels/kangaroo_point/level.glb.import b/levels/kangaroo_point/level.glb.import index 5c7cea3..789ebee 100644 --- a/levels/kangaroo_point/level.glb.import +++ b/levels/kangaroo_point/level.glb.import @@ -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 diff --git a/levels/queen_st_mall/level.glb b/levels/queen_st_mall/level.glb index c88a645..1ae809e 100644 Binary files a/levels/queen_st_mall/level.glb and b/levels/queen_st_mall/level.glb differ diff --git a/levels/queen_st_mall/level.glb.import b/levels/queen_st_mall/level.glb.import index dbf5793..517edae 100644 --- a/levels/queen_st_mall/level.glb.import +++ b/levels/queen_st_mall/level.glb.import @@ -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 diff --git a/levels/southbank/level.glb b/levels/southbank/level.glb index b261965..2158e2e 100644 Binary files a/levels/southbank/level.glb and b/levels/southbank/level.glb differ diff --git a/levels/southbank/level.glb.import b/levels/southbank/level.glb.import index d263f5a..8da64ad 100644 --- a/levels/southbank/level.glb.import +++ b/levels/southbank/level.glb.import @@ -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 diff --git a/levels/story_bridge/level.glb b/levels/story_bridge/level.glb index 2617eb4..677d61e 100644 Binary files a/levels/story_bridge/level.glb and b/levels/story_bridge/level.glb differ diff --git a/levels/story_bridge/level.glb.import b/levels/story_bridge/level.glb.import index ae6a87a..895b6fe 100644 --- a/levels/story_bridge/level.glb.import +++ b/levels/story_bridge/level.glb.import @@ -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 diff --git a/shot_sb_bridge.png.import b/shot_sb_bridge.png.import new file mode 100644 index 0000000..27d98a7 --- /dev/null +++ b/shot_sb_bridge.png.import @@ -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 diff --git a/shot_smash.png.import b/shot_smash.png.import new file mode 100644 index 0000000..5db5b46 --- /dev/null +++ b/shot_smash.png.import @@ -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 diff --git a/src/game.gd b/src/game.gd index d7b787d..d88b41e 100644 --- a/src/game.gd +++ b/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. diff --git a/tools/build_all_levels.sh b/tools/build_all_levels.sh index 35e2598..cabf28c 100755 --- a/tools/build_all_levels.sh +++ b/tools/build_all_levels.sh @@ -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' diff --git a/tools/build_level_osm.py b/tools/build_level_osm.py index 336ddb4..f7fc29c 100644 --- a/tools/build_level_osm.py +++ b/tools/build_level_osm.py @@ -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(",")]