diff --git a/game/assets/store/cabinet-carcass.glb b/game/assets/store/cabinet-carcass.glb new file mode 100644 index 0000000..fdc4137 Binary files /dev/null and b/game/assets/store/cabinet-carcass.glb differ diff --git a/game/assets/store/cabinet-carcass.glb.import b/game/assets/store/cabinet-carcass.glb.import new file mode 100644 index 0000000..f2660f5 --- /dev/null +++ b/game/assets/store/cabinet-carcass.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://si6xdny7yehl" +path="res://.godot/imported/cabinet-carcass.glb-238befe94afa5291839887691432da2b.scn" + +[deps] + +source_file="res://assets/store/cabinet-carcass.glb" +dest_files=["res://.godot/imported/cabinet-carcass.glb-238befe94afa5291839887691432da2b.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/cabinet-drawer.glb b/game/assets/store/cabinet-drawer.glb new file mode 100644 index 0000000..38c9d18 Binary files /dev/null and b/game/assets/store/cabinet-drawer.glb differ diff --git a/game/assets/store/cabinet-drawer.glb.import b/game/assets/store/cabinet-drawer.glb.import new file mode 100644 index 0000000..a6b77bf --- /dev/null +++ b/game/assets/store/cabinet-drawer.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cey5a314gtnuy" +path="res://.godot/imported/cabinet-drawer.glb-b3f72509ee62d2d19c1a0038604be29c.scn" + +[deps] + +source_file="res://assets/store/cabinet-drawer.glb" +dest_files=["res://.godot/imported/cabinet-drawer.glb-b3f72509ee62d2d19c1a0038604be29c.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/desk-phone.glb b/game/assets/store/desk-phone.glb new file mode 100644 index 0000000..746a093 Binary files /dev/null and b/game/assets/store/desk-phone.glb differ diff --git a/game/assets/store/desk-phone.glb.import b/game/assets/store/desk-phone.glb.import new file mode 100644 index 0000000..a5f2068 --- /dev/null +++ b/game/assets/store/desk-phone.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b0ryrh1b87oad" +path="res://.godot/imported/desk-phone.glb-0b4e710e838e5ab39f12cd38794928db.scn" + +[deps] + +source_file="res://assets/store/desk-phone.glb" +dest_files=["res://.godot/imported/desk-phone.glb-0b4e710e838e5ab39f12cd38794928db.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/file-folder.glb b/game/assets/store/file-folder.glb new file mode 100644 index 0000000..007a80e Binary files /dev/null and b/game/assets/store/file-folder.glb differ diff --git a/game/assets/store/file-folder.glb.import b/game/assets/store/file-folder.glb.import new file mode 100644 index 0000000..58e548a --- /dev/null +++ b/game/assets/store/file-folder.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b6wn6ivjipo38" +path="res://.godot/imported/file-folder.glb-996d4a433542c9c1abe7cf8adf443e60.scn" + +[deps] + +source_file="res://assets/store/file-folder.glb" +dest_files=["res://.godot/imported/file-folder.glb-996d4a433542c9c1abe7cf8adf443e60.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/guillotine.glb b/game/assets/store/guillotine.glb new file mode 100644 index 0000000..423560d Binary files /dev/null and b/game/assets/store/guillotine.glb differ diff --git a/game/assets/store/guillotine.glb.import b/game/assets/store/guillotine.glb.import new file mode 100644 index 0000000..8268878 --- /dev/null +++ b/game/assets/store/guillotine.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://me4h4r87qi8a" +path="res://.godot/imported/guillotine.glb-0850d7d61b8c7893d2ad4b765c14a621.scn" + +[deps] + +source_file="res://assets/store/guillotine.glb" +dest_files=["res://.godot/imported/guillotine.glb-0850d7d61b8c7893d2ad4b765c14a621.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/server-rack.glb b/game/assets/store/server-rack.glb new file mode 100644 index 0000000..e748aa4 Binary files /dev/null and b/game/assets/store/server-rack.glb differ diff --git a/game/assets/store/server-rack.glb.import b/game/assets/store/server-rack.glb.import new file mode 100644 index 0000000..8ff1081 --- /dev/null +++ b/game/assets/store/server-rack.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dfj8ngisb8pji" +path="res://.godot/imported/server-rack.glb-e13aeaf7bf0cdf2cd49217c33d8e2448.scn" + +[deps] + +source_file="res://assets/store/server-rack.glb" +dest_files=["res://.godot/imported/server-rack.glb-e13aeaf7bf0cdf2cd49217c33d8e2448.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/shredder.glb b/game/assets/store/shredder.glb new file mode 100644 index 0000000..a8b5dbb Binary files /dev/null and b/game/assets/store/shredder.glb differ diff --git a/game/assets/store/shredder.glb.import b/game/assets/store/shredder.glb.import new file mode 100644 index 0000000..ad53114 --- /dev/null +++ b/game/assets/store/shredder.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dtjftptqfqw2j" +path="res://.godot/imported/shredder.glb-e7be012212295ed44f5a7a52ad82e1b0.scn" + +[deps] + +source_file="res://assets/store/shredder.glb" +dest_files=["res://.godot/imported/shredder.glb-e7be012212295ed44f5a7a52ad82e1b0.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/sofa.glb b/game/assets/store/sofa.glb new file mode 100644 index 0000000..f7ba1fe Binary files /dev/null and b/game/assets/store/sofa.glb differ diff --git a/game/assets/store/sofa.glb.import b/game/assets/store/sofa.glb.import new file mode 100644 index 0000000..0e0846e --- /dev/null +++ b/game/assets/store/sofa.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dinkdfb8jin4m" +path="res://.godot/imported/sofa.glb-c2ee6c7bd1e23613e6084f217cc88f31.scn" + +[deps] + +source_file="res://assets/store/sofa.glb" +dest_files=["res://.godot/imported/sofa.glb-c2ee6c7bd1e23613e6084f217cc88f31.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/stapler.glb b/game/assets/store/stapler.glb new file mode 100644 index 0000000..b8324c1 Binary files /dev/null and b/game/assets/store/stapler.glb differ diff --git a/game/assets/store/stapler.glb.import b/game/assets/store/stapler.glb.import new file mode 100644 index 0000000..33168b4 --- /dev/null +++ b/game/assets/store/stapler.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://02eubr818s2p" +path="res://.godot/imported/stapler.glb-9abfdda7570f61d9ca4808557b7ea9c6.scn" + +[deps] + +source_file="res://assets/store/stapler.glb" +dest_files=["res://.godot/imported/stapler.glb-9abfdda7570f61d9ca4808557b7ea9c6.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/wastebin.glb b/game/assets/store/wastebin.glb new file mode 100644 index 0000000..3c21106 Binary files /dev/null and b/game/assets/store/wastebin.glb differ diff --git a/game/assets/store/wastebin.glb.import b/game/assets/store/wastebin.glb.import new file mode 100644 index 0000000..971ec8b --- /dev/null +++ b/game/assets/store/wastebin.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://baxe2k4e2nkxf" +path="res://.godot/imported/wastebin.glb-fbd43ede5b2ede38a8e605f1a922673f.scn" + +[deps] + +source_file="res://assets/store/wastebin.glb" +dest_files=["res://.godot/imported/wastebin.glb-fbd43ede5b2ede38a8e605f1a922673f.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/dev/DemoDriver.gd b/game/dev/DemoDriver.gd index 7b6ecc6..d7dae01 100644 --- a/game/dev/DemoDriver.gd +++ b/game/dev/DemoDriver.gd @@ -48,85 +48,33 @@ func _find_player(n: Node) -> Player: return null func _build_script() -> void: - # Office floor plan (see Office.gd): spawn at reception (-8.6, 6.6) looking into the - # bullpen; desk clusters at (-4.6,-1.4) (-4.6,2.2) (-0.4,-1.4) (-0.4,2.2); copier - # alcove at (3.0,-0.3); break room x>5.4 z>-1.2; conference and manager along z<-4.4; - # warehouse corner around (7,-6). + # A lap of all four sites. Each gets a settle beat (nothing must move on its own) + # and two vantage points, so the palette, lighting and layout can all be read. _script = [] + var tours := { + "scranton": [[Vector3(-8.6, 0, 6.4), Vector3(-4.0, 1.1, 0.0)], + [Vector3(-2.6, 0, 3.4), Vector3(-0.4, 1.0, -1.0)], + [Vector3(1.6, 0, -2.0), Vector3(8.0, 1.3, -6.5)]], + "pawnee": [[Vector3(-8.0, 0, 6.6), Vector3(-2.0, 1.1, 1.0)], + [Vector3(-1.0, 0, 2.6), Vector3(-2.6, 1.2, -5.2)], + [Vector3(4.0, 0, 0.6), Vector3(6.5, 1.2, -3.2)]], + "house": [[Vector3(-1.0, 0, 7.0), Vector3(-4.4, 1.2, 0.6)], + [Vector3(0.5, 0, 2.0), Vector3(6.0, 1.3, 6.0)], + [Vector3(-6.0, 0, -2.0), Vector3(4.0, 1.4, -6.0)]], + "basement": [[Vector3(1.0, 0, -5.2), Vector3(-8.0, 1.3, -1.0)], + [Vector3(-4.0, 0, -1.0), Vector3(-10.6, 1.4, -1.0)], + [Vector3(4.0, 0, 1.0), Vector3(9.0, 1.3, 5.4)]], + } + for id in ["scranton", "pawnee", "house", "basement"]: + var shots: Array = tours[id] + _script.append({"dur": 0.4, "move_to": shots[0][0], "look_at": shots[0][1], + "act": "level:" + id}) + _script.append({"dur": 2.2, "move_to": null, "look_at": null, "act": ""}) + for i in range(1, shots.size()): + _script.append({"dur": 2.4, "move_to": shots[i][0], "look_at": shots[i][1], + "act": ""}) + _script.append({"dur": 1.2, "move_to": null, "look_at": null, "act": ""}) - # ---- ACT 0: SETTLE CHECK. Stand at the entrance and touch nothing. If anything - # ---- moves in these five seconds, the level is falling over by itself again. - _script += [ - {"dur": 2.5, "move_to": Vector3(-8.6, 0, 6.4), "look_at": Vector3(-4.0, 1.1, 0.0), "act": ""}, - {"dur": 2.5, "move_to": null, "look_at": null, "act": ""}, - ] - - # ---- ACT 0b: CUBICLE HELL. Sit at a desk, do the spreadsheet, get wound up until - # ---- the meter snaps, then wreck the place until it drains. - _script += [ - {"dur": 2.4, "move_to": Vector3(-4.6, 0, -0.55), "look_at": Vector3(-4.6, 1.0, -1.4), "act": ""}, - {"dur": 1.2, "move_to": null, "look_at": null, "act": "task"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "type"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "commit"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "rage:0.35"}, - # SNAP — the vignette closes in, veins crawl, heartbeat goes. Now wreck things. - {"dur": 1.0, "move_to": Vector3(-4.6, 0, 0.3), "look_at": Vector3(-4.6, 0.9, -0.7), "act": "weapon:2"}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.6, "move_to": Vector3(-2.6, 0, 4.8), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"}, - ] - - # ---- ACT 1: a walk through the floor plan, so the rooms read. - _script += [ - {"dur": 2.2, "move_to": Vector3(-8.4, 0, 2.0), "look_at": Vector3(-4.6, 1.1, 1.0), "act": ""}, - {"dur": 2.2, "move_to": Vector3(-2.6, 0, 3.4), "look_at": Vector3(-0.4, 1.0, 0.5), "act": ""}, - {"dur": 2.2, "move_to": Vector3(-3.0, 0, -3.0), "look_at": Vector3(-3.4, 1.4, -6.4), "act": ""}, - {"dur": 2.2, "move_to": Vector3(1.6, 0, -2.0), "look_at": Vector3(8.0, 1.3, -6.5), "act": ""}, - {"dur": 2.2, "move_to": Vector3(3.4, 0, 3.0), "look_at": Vector3(8.0, 1.2, 4.0), "act": ""}, - ] - - # ---- ACT 2: the loadout, held still so each grip can be inspected. - for slot in range(6): - _script.append({"dur": 0.5, "move_to": Vector3(-2.2, 0, 4.6), - "look_at": Vector3(-2.0, 1.2, 0.0), "act": "weapon:%d" % slot}) - _script.append({"dur": 0.8, "move_to": null, "look_at": null, "act": ""}) - _script.append({"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"}) - - # ---- ACT 3: the matrix. Cutter shreds a carton, clanks off a filing cabinet, - # ---- then the sledge takes the cabinet out. - _script += [ - {"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:1"}, - {"dur": 2.0, "move_to": Vector3(-1.6, 0, 5.3), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""}, - {"dur": 0.5, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 2.0, "move_to": Vector3(3.1, 0, -0.9), "look_at": Vector3(4.5, 0.6, -0.9), "act": ""}, - {"dur": 0.4, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:4"}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.3, "move_to": null, "look_at": null, "act": "swing"}, - ] - - # ---- ACT 4: the printer boss, then a lap of the HUD styles. - _script += [ - {"dur": 2.0, "move_to": Vector3(1.5, 0, -0.3), "look_at": Vector3(3.0, 0.95, -0.3), "act": ""}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.5, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 2.0, "move_to": Vector3(-1.0, 0, 5.6), "look_at": Vector3(-1.0, 1.2, -1.0), "act": "hud"}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"}, - ] - -## Total speed across every physics body in the level. During the settle act nobody has -## touched anything, so this MUST fall to ~0 — that is the objective version of "is the -## level quietly falling over on its own before the player does anything". func _motion(verbose := false) -> float: var total := 0.0 var seen := {} @@ -215,6 +163,10 @@ func _do(act: String) -> void: if act.begins_with("weapon:"): _player.select(int(act.substr(7))) return + if act.begins_with("level:"): + if _main.has_method("_load_level"): + _main._load_level(act.substr(6)) + return if act.begins_with("rage:"): var r = _main.get("_rage") if r != null: diff --git a/game/dev/probe_levels.gd b/game/dev/probe_levels.gd new file mode 100644 index 0000000..511e50e --- /dev/null +++ b/game/dev/probe_levels.gd @@ -0,0 +1,49 @@ +extends SceneTree + +## Build every level in Levels.ORDER and report whether it's structurally sound: +## prop count, missing assets, and — the one that matters — whether anything is spawned +## inside anything else. A level that fires furniture across the room on frame one is +## broken whatever it looks like in a screenshot. +## +## Godot --headless --path game --script dev/probe_levels.gd + +func _initialize() -> void: + var main: Node = (load("res://main.tscn") as PackedScene).instantiate() + get_root().add_child(main) + await process_frame + + for id in Levels.ORDER: + main._load_level(id) + for i in 8: + await physics_frame + var plan = main.get("_plan") + var bodies := get_nodes_in_group("smashable").size() + var statics := _count_static(plan) + # let it run a full second and see if anything is still moving + for i in 60: + await physics_frame + var motion := 0.0 + var worst := "" + var worst_v := 0.0 + for n in get_nodes_in_group("smashable"): + var b := n as RigidBody3D + if b == null or b.freeze: + continue + var v := b.linear_velocity.length() + motion += v + if v > worst_v: + worst_v = v + worst = "%s at %s" % [b.name, b.global_position.snappedf(0.1)] + print("\n=== %-14s %s" % [id, plan.level_name()]) + print(" smashables=%-4d static bodies=%-4d" % [bodies, statics]) + print(" residual motion after 1s = %.2f m/s worst: %s" % [ + motion, worst if worst != "" else "-"]) + quit() + +func _count_static(n: Node) -> int: + var c := 0 + if n is StaticBody3D: + c += 1 + for ch in n.get_children(): + c += _count_static(ch) + return c diff --git a/game/scripts/Floorplan.gd b/game/scripts/Floorplan.gd new file mode 100644 index 0000000..fd85653 --- /dev/null +++ b/game/scripts/Floorplan.gd @@ -0,0 +1,458 @@ +extends Node3D +class_name Floorplan + +## A workplace, built from a data spec. +## +## This started as Office.gd with the Dunder Mifflin numbers hard-coded. Lifting those +## numbers into a Dictionary buys three things: each new workplace is a data file rather +## than a new script, levels can be diffed and tuned without touching build code, and a +## real *generator* becomes possible later — a BSP split of the footprint emitting rooms, +## doors on shared walls and fittings by room type produces the same spec this already +## consumes. +## +## (For the record: `doratracyer/floor_plan` can't do that job. It's OpenSCAD turning +## hand-drawn SVG wall paths into 3D-printable STLs — no generation, no plan parsing, no +## room polygons, and GPL-3.0. Levels.gd is the better path.) +## +## SPEC — every key optional unless marked. See Levels.gd for worked examples. +## name, subtitle ...... strings for the HUD +## bounds * ............ {x0, x1, z0, z1, h} +## palette * ........... {floor, wall, ceiling, trim, partition, wood, accent} Colors +## floor_style ......... "carpet" | "lino" | "concrete" | "timber" +## ceiling ............. "tile" (T-bar grid) | "flat" | "open" +## walls ............... [{a:"x"|"z", at, from, to, door, style:"solid"|"glass_top"|"half"}] +## windows ............. [{a, at, from, to, y0, y1, blinds}] +## lights .............. {xs:[], zs:[], color, energy, style:"troffer"|"pendant"|"strip"} +## sun ................. {color, energy, rot:Vector3} +## env ................. {bg, ambient, ambient_energy} +## slabs ............... [{size:Vector3, at:Vector3, mat:String, collide, shadow}] +## props ............... [{glb, at:Vector3, yaw}] static, gets a box collider +## clusters ............ [Vector2] facing desk-pair centres +## cluster_gap ......... metres from a cluster centre to each of its two desks +## chairs .............. [{at:Vector3, yaw}] non-bullpen chairs +## smashables .......... [{glb, kind, at:Vector2, yaw, frozen}] level-specific breakables +## copier .............. {at:Vector3, yaw} +## spawn * ............. {at:Vector3, yaw} +## +## This class owns the SHELL, the FITTINGS and the LIGHTING — everything static. Main +## owns everything smashable and asks here where things go. +## +## Godot 4.7 GDScript 2.0. + +const P := "res://assets/store/" + +var spec: Dictionary = {} +var _mats: Dictionary = {} +var _spots_desk: Array[Transform3D] = [] +var _spots_chair: Array[Transform3D] = [] +var _clusters: Array = [] + +func build(s: Dictionary) -> void: + spec = s + _materials() + _shell() + _walls() + _windows() + _ceiling() + _lights() + _slabs() + _props() + _bullpen() + _chairs() + +# ---------------------------------------------------------------- convenience +func _b(k: String) -> float: + return float((spec["bounds"] as Dictionary)[k]) + +func _pal(k: String, dflt := Color(0.7, 0.7, 0.7)) -> Color: + var p: Dictionary = spec.get("palette", {}) + return p.get(k, dflt) + +func _materials() -> void: + var rough := {"carpet": 1.0, "lino": 0.55, "concrete": 0.92, "timber": 0.5} + _mats["floor"] = _mk(_pal("floor", Color(0.30, 0.30, 0.32)), + float(rough.get(String(spec.get("floor_style", "carpet")), 1.0))) + _mats["wall"] = _mk(_pal("wall", Color(0.80, 0.78, 0.72)), 0.96) + _mats["ceiling"] = _mk(_pal("ceiling", Color(0.88, 0.88, 0.86)), 0.98) + _mats["trim"] = _mk(_pal("trim", Color(0.13, 0.12, 0.13)), 0.7) + _mats["partition"] = _mk(_pal("partition", Color(0.44, 0.44, 0.40)), 1.0) + _mats["wood"] = _mk(_pal("wood", Color(0.42, 0.29, 0.17)), 0.55) + _mats["accent"] = _mk(_pal("accent", Color(0.55, 0.20, 0.22)), 0.6) + var g := StandardMaterial3D.new() + g.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + g.albedo_color = Color(0.66, 0.74, 0.78, 0.15) + g.roughness = 0.05 + g.cull_mode = BaseMaterial3D.CULL_DISABLED + _mats["glass"] = g + +func _mk(c: Color, rough: float) -> StandardMaterial3D: + var m := StandardMaterial3D.new() + m.albedo_color = c + m.roughness = rough + return m + +func mat(name: String) -> Material: + return _mats.get(name, _mats["wall"]) + +# ---------------------------------------------------------------- primitives +func slab(size: Vector3, centre: Vector3, m: Material, collide := true, + shadow := true) -> StaticBody3D: + var body := StaticBody3D.new() + var mi := MeshInstance3D.new() + var bm := BoxMesh.new() + bm.size = size + mi.mesh = bm + mi.material_override = m + if not shadow: + mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + body.add_child(mi) + if collide: + var col := CollisionShape3D.new() + var sh := BoxShape3D.new() + sh.size = size + col.shape = sh + body.add_child(col) + add_child(body) + body.position = centre + return body + +## Static dressing from a GLB, with a box collider sized to its own mesh. +func glb(path: String, pos: Vector3, yaw := 0.0) -> Node3D: + if not ResourceLoader.exists(path): + push_warning("[floorplan] missing prop %s" % path) + return null + var packed := load(path) as PackedScene + if packed == null: + return null + var body := StaticBody3D.new() + add_child(body) + body.position = pos + body.rotation.y = yaw + var n: Node3D = packed.instantiate() + body.add_child(n) + var ab := mesh_aabb(n) + if ab.size != Vector3.ZERO: + var col := CollisionShape3D.new() + var sh := BoxShape3D.new() + sh.size = ab.size + col.shape = sh + col.position = ab.get_center() + body.add_child(col) + return body + +static func mesh_aabb(root: Node3D) -> AABB: + var acc := AABB() + var started := false + for m in meshes(root): + var mi := m as MeshInstance3D + var a := mi.transform * mi.get_aabb() + if not started: + acc = a + started = true + else: + acc = acc.merge(a) + return acc + +static func meshes(n: Node, acc: Array = []) -> Array: + if n is MeshInstance3D: + acc.append(n) + for c in n.get_children(): + meshes(c, acc) + return acc + +# ---------------------------------------------------------------- shell +func _shell() -> void: + var w := _b("x1") - _b("x0") + var d := _b("z1") - _b("z0") + var cx := (_b("x0") + _b("x1")) * 0.5 + var cz := (_b("z0") + _b("z1")) * 0.5 + slab(Vector3(w + 1.0, 0.30, d + 1.0), Vector3(cx, -0.15, cz), mat("floor")) + +## One wall segment, optionally with a doorway punched in it. `style` picks how the +## upper half is treated: solid, glazed (the manager watching you), or absent (a +## counter-height divider you can see over but not walk through). +func _wall_seg(a: String, at: float, from: float, to: float, door: float, + style: String) -> void: + var h := _b("h") + var T := 0.16 + var lo := 0.0 + var hi := h + if style == "half": + hi = 1.15 + var solid_h := hi - lo + var pieces: Array = [] + if door == INF: + pieces.append([from, to, lo, solid_h]) + else: + var d0 := door - 0.55 + var d1 := door + 0.55 + if d0 > from: + pieces.append([from, d0, lo, solid_h]) + if to > d1: + pieces.append([d1, to, lo, solid_h]) + if hi > 2.05: + pieces.append([d0, d1, 2.05, hi - 2.05]) + for p in pieces: + var c := (float(p[0]) + float(p[1])) * 0.5 + var l: float = float(p[1]) - float(p[0]) + var y0: float = float(p[2]) + var hh: float = float(p[3]) + if hh <= 0.0 or l <= 0.0: + continue + if a == "x": + slab(Vector3(l, hh, T), Vector3(c, y0 + hh * 0.5, at), mat("wall")) + else: + slab(Vector3(T, hh, l), Vector3(at, y0 + hh * 0.5, c), mat("wall")) + # glazing above a half-height base + if style == "glass_top" and h > 1.05: + var gc := (from + to) * 0.5 + var gl := to - from + if a == "x": + slab(Vector3(gl, h - 1.05, 0.06), Vector3(gc, (h + 1.0) * 0.5, at), + mat("glass"), true, false) + else: + slab(Vector3(0.06, h - 1.05, gl), Vector3(at, (h + 1.0) * 0.5, gc), + mat("glass"), true, false) + +func _walls() -> void: + var h := _b("h") + var T := 0.16 + var cx := (_b("x0") + _b("x1")) * 0.5 + var cz := (_b("z0") + _b("z1")) * 0.5 + # outer shell, minus any side declared as a window wall + var win_sides := {} + for wnd in spec.get("windows", []): + win_sides[_side_key(wnd)] = true + if not win_sides.has("z:" + str(_b("z0"))): + _wall_seg("x", _b("z0") - T * 0.5, _b("x0"), _b("x1"), INF, "solid") + if not win_sides.has("z:" + str(_b("z1"))): + _wall_seg("x", _b("z1") + T * 0.5, _b("x0"), _b("x1"), INF, "solid") + if not win_sides.has("x:" + str(_b("x0"))): + _wall_seg("z", _b("x0") - T * 0.5, _b("z0"), _b("z1"), INF, "solid") + if not win_sides.has("x:" + str(_b("x1"))): + _wall_seg("z", _b("x1") + T * 0.5, _b("z0"), _b("z1"), INF, "solid") + for wl in spec.get("walls", []): + var d: Dictionary = wl + _wall_seg(String(d.get("a", "x")), float(d["at"]), float(d["from"]), + float(d["to"]), float(d.get("door", INF)), String(d.get("style", "solid"))) + +func _side_key(w: Dictionary) -> String: + return "%s:%s" % [String(w.get("a", "x")), str(float(w["at"]))] + +## A glazed run with mullions every 2.4 m and, optionally, half-raised blinds. It's what +## stops an interior reading as a sealed box. +func _windows() -> void: + var h := _b("h") + var T := 0.16 + for wnd in spec.get("windows", []): + var d: Dictionary = wnd + var a := String(d.get("a", "x")) + var at := float(d["at"]) + var f := float(d["from"]) + var t := float(d["to"]) + var y0 := float(d.get("y0", 0.85)) + var y1 := float(d.get("y1", 2.35)) + var blinds := bool(d.get("blinds", true)) + var out := at + (T * 0.5 * signf(at)) + var span := t - f + var c := (f + t) * 0.5 + if a == "x": + slab(Vector3(span, y0, T), Vector3(c, y0 * 0.5, out), mat("wall")) + slab(Vector3(span, h - y1, T), Vector3(c, (h + y1) * 0.5, out), mat("wall")) + else: + slab(Vector3(T, y0, span), Vector3(out, y0 * 0.5, c), mat("wall")) + slab(Vector3(T, h - y1, span), Vector3(out, (h + y1) * 0.5, c), mat("wall")) + var p := f + while p < t - 0.1: + var p1: float = minf(p + 2.4, t) + var mid := (p + p1) * 0.5 + var seg := p1 - p - 0.12 + if a == "x": + slab(Vector3(seg, y1 - y0, 0.05), Vector3(mid, (y0 + y1) * 0.5, at), + mat("glass"), false, false) + slab(Vector3(0.12, y1 - y0, T), Vector3(p1, (y0 + y1) * 0.5, out), mat("trim")) + if blinds: + slab(Vector3(seg, 0.55, 0.06), Vector3(mid, y1 - 0.28, + at - 0.10 * signf(at)), mat("ceiling"), false, false) + else: + slab(Vector3(0.05, y1 - y0, seg), Vector3(at, (y0 + y1) * 0.5, mid), + mat("glass"), false, false) + slab(Vector3(T, y1 - y0, 0.12), Vector3(out, (y0 + y1) * 0.5, p1), mat("trim")) + if blinds: + slab(Vector3(0.06, 0.55, seg), Vector3(at - 0.10 * signf(at), + y1 - 0.28, mid), mat("ceiling"), false, false) + p = p1 + +func _ceiling() -> void: + var style := String(spec.get("ceiling", "tile")) + if style == "open": + return + var w := _b("x1") - _b("x0") + var d := _b("z1") - _b("z0") + var cx := (_b("x0") + _b("x1")) * 0.5 + var cz := (_b("z0") + _b("z1")) * 0.5 + var h := _b("h") + slab(Vector3(w, 0.16, d), Vector3(cx, h + 0.08, cz), mat("ceiling"), true, false) + if style != "tile": + return + # suspended-tile T-bar. Purely visual, and it IS the texture of an office ceiling. + var gx := _b("x0") + while gx <= _b("x1") + 0.01: + slab(Vector3(0.05, 0.03, d), Vector3(gx, h - 0.02, cz), mat("trim"), false, false) + gx += 1.22 + var gz := _b("z0") + while gz <= _b("z1") + 0.01: + slab(Vector3(w, 0.03, 0.05), Vector3(cx, h - 0.02, gz), mat("trim"), false, false) + gz += 1.22 + +# ---------------------------------------------------------------- lighting +func _lights() -> void: + var L: Dictionary = spec.get("lights", {}) + var xs: Array = L.get("xs", []) + var zs: Array = L.get("zs", []) + var col: Color = L.get("color", Color(1.0, 0.98, 0.93)) + var energy := float(L.get("energy", 1.9)) + var style := String(L.get("style", "troffer")) + var h := _b("h") + + var glow := StandardMaterial3D.new() + glow.albedo_color = Color(0.97, 0.97, 0.93) + glow.emission_enabled = true + glow.emission = col + glow.emission_energy_multiplier = 1.5 + + for xc in xs: + for zc in zs: + var y := h - 0.06 + var size := Vector3(1.14, 0.05, 0.58) + match style: + "pendant": + y = h - 0.55 + size = Vector3(0.30, 0.16, 0.30) + slab(Vector3(0.02, 0.45, 0.02), Vector3(xc, h - 0.24, zc), + mat("trim"), false, false) + "strip": + size = Vector3(1.30, 0.10, 0.14) + var mi := MeshInstance3D.new() + var bm := BoxMesh.new() + bm.size = size + mi.mesh = bm + mi.material_override = glow + mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(mi) + mi.position = Vector3(xc, y, zc) + + var l := OmniLight3D.new() + l.light_color = col + l.light_energy = energy + l.omni_range = 6.5 + l.omni_attenuation = 1.2 + l.shadow_enabled = false # a grid of shadow-casting omnis isn't worth it + add_child(l) + l.position = Vector3(xc, y - 0.14, zc) + + # a couple of shadow-casters so props still sit in contact shadows + var cx := (_b("x0") + _b("x1")) * 0.5 + for zc in [_b("z0") * 0.45, _b("z1") * 0.45]: + var key := OmniLight3D.new() + key.light_color = col + key.light_energy = float(L.get("key_energy", 1.5)) + key.omni_range = 13.0 + key.shadow_enabled = true + add_child(key) + key.position = Vector3(cx, h - 0.30, zc) + + var S: Dictionary = spec.get("sun", {}) + if not S.is_empty(): + var sun := DirectionalLight3D.new() + sun.light_color = S.get("color", Color(0.82, 0.88, 1.0)) + sun.light_energy = float(S.get("energy", 0.7)) + sun.rotation_degrees = S.get("rot", Vector3(-30, -104, 0)) + sun.shadow_enabled = bool(S.get("shadow", true)) + add_child(sun) + +func configure_environment(env: Environment) -> void: + var E: Dictionary = spec.get("env", {}) + env.background_mode = Environment.BG_COLOR + env.background_color = E.get("bg", Color(0.55, 0.62, 0.72)) + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = E.get("ambient", Color(0.50, 0.50, 0.50)) + env.ambient_light_energy = float(E.get("ambient_energy", 0.5)) + env.ssao_enabled = true + env.ssao_radius = 0.8 + env.ssao_intensity = 1.5 + env.tonemap_mode = Environment.TONE_MAPPER_FILMIC + env.tonemap_exposure = float(E.get("exposure", 1.0)) + +# ---------------------------------------------------------------- contents +func _slabs() -> void: + for s in spec.get("slabs", []): + var d: Dictionary = s + slab(d["size"], d["at"], mat(String(d.get("mat", "wall"))), + bool(d.get("collide", true)), bool(d.get("shadow", true))) + +func _props() -> void: + for p in spec.get("props", []): + var d: Dictionary = p + glb(P + String(d["glb"]) + ".glb", d["at"], float(d.get("yaw", 0.0))) + +## Facing desk pairs behind a cubicle divider — the Jim-and-Dwight arrangement. +func _bullpen() -> void: + _clusters = spec.get("clusters", []) + var gap := float(spec.get("cluster_gap", 0.78)) + for c in _clusters: + var v: Vector2 = c + _partition(Vector3(v.x, 0.0, v.y), 1.9) + for s in [-1.0, 1.0]: + _spots_desk.append(Transform3D(Basis(Vector3.UP, 0.0 if s > 0.0 else PI), + Vector3(v.x, 0.0, v.y + s * gap))) + +func _partition(at: Vector3, length: float) -> void: + slab(Vector3(length, 1.18, 0.07), at + Vector3(0, 0.62, 0), mat("partition")) + slab(Vector3(length + 0.06, 0.09, 0.11), at + Vector3(0, 1.24, 0), mat("trim")) + for s in [-1.0, 1.0]: + slab(Vector3(0.09, 1.24, 0.30), at + Vector3(s * length * 0.5, 0.62, 0), mat("trim")) + +func _chairs() -> void: + for c in spec.get("chairs", []): + var d: Dictionary = c + _spots_chair.append(Transform3D(Basis(Vector3.UP, float(d.get("yaw", 0.0))), + d["at"])) + +# ---------------------------------------------------------------- reads +func desk_spots() -> Array[Transform3D]: + return _spots_desk + +func chair_spots() -> Array[Transform3D]: + return _spots_chair + +## Cluster centre for desk index `i`, so Main can work out which side the chair goes. +func desk_cluster(i: int) -> Vector2: + var c: int = i / 2 + if c < 0 or c >= _clusters.size(): + return Vector2.ZERO + return _clusters[c] + +func copier_spot() -> Transform3D: + var c: Dictionary = spec.get("copier", {}) + if c.is_empty(): + return Transform3D.IDENTITY + return Transform3D(Basis(Vector3.UP, float(c.get("yaw", 0.0))), c["at"]) + +func has_copier() -> bool: + return spec.has("copier") + +func smashables() -> Array: + return spec.get("smashables", []) + +func spawn_point() -> Vector3: + return (spec["spawn"] as Dictionary)["at"] + +func spawn_yaw() -> float: + return float((spec["spawn"] as Dictionary).get("yaw", 0.0)) + +func level_name() -> String: + return String(spec.get("name", "UNNAMED SITE")) + +func subtitle() -> String: + return String(spec.get("subtitle", "")) diff --git a/game/scripts/Floorplan.gd.uid b/game/scripts/Floorplan.gd.uid new file mode 100644 index 0000000..ecb2218 --- /dev/null +++ b/game/scripts/Floorplan.gd.uid @@ -0,0 +1 @@ +uid://d0oexxkrw0hln diff --git a/game/scripts/Levels.gd b/game/scripts/Levels.gd new file mode 100644 index 0000000..e313b6b --- /dev/null +++ b/game/scripts/Levels.gd @@ -0,0 +1,428 @@ +extends RefCounted +class_name Levels + +## The workplaces, as data. Each is a Floorplan spec (see Floorplan.gd for the schema). +## +## The point of these being data and not four subclasses: a new site is authorable in +## minutes, they can be diffed, and a generator can later emit the same structure. +## +## They are deliberately NOT reskins. Each differs in the three things a player actually +## reads — the palette, the light, and what the walls are made of: +## +## SCRANTON ..... magnolia, grey carpet, drop ceiling, fluorescent troffers, daylight +## down one glazed wall. The baseline office. +## PAWNEE ....... civic beige and blue-grey, low partitions everywhere, a public +## counter, pinboards. Municipal, over-partitioned, no privacy. +## THE HOUSE .... timber floor, white walls, 3 m ceiling, pendant lights, a glass wall +## onto a pool. Nobody here has an office; they work at a dining table. +## THE BASEMENT . concrete, no windows at all, exposed services, bare strip lights, a +## wall of server racks. The light is green and everything is junk. +## +## Godot 4.7 GDScript 2.0. + +const ORDER := ["scranton", "pawnee", "house", "basement"] + +static func get_level(id: String) -> Dictionary: + match id: + "pawnee": return pawnee() + "house": return house() + "basement": return basement() + return scranton() + +static func next_id(id: String) -> String: + var i := ORDER.find(id) + return ORDER[(i + 1) % ORDER.size()] if i >= 0 else ORDER[0] + +# ================================================================ LEVEL 01 +static func scranton() -> Dictionary: + return { + "name": "SCRANTON BRANCH", + "subtitle": "paper company · 2nd floor", + "bounds": {"x0": -11.0, "x1": 11.0, "z0": -8.0, "z1": 8.0, "h": 2.75}, + "palette": { + "floor": Color(0.30, 0.30, 0.32), "wall": Color(0.80, 0.78, 0.72), + "ceiling": Color(0.88, 0.88, 0.86), "trim": Color(0.13, 0.12, 0.13), + "partition": Color(0.44, 0.44, 0.40), "wood": Color(0.42, 0.29, 0.17), + "accent": Color(0.36, 0.42, 0.50), + }, + "floor_style": "carpet", + "ceiling": "tile", + "walls": [ + # manager's office — half solid, half glass, so you can be watched through it + {"a": "x", "at": -4.4, "from": -11.0, "to": -6.6, "door": -9.2}, + {"a": "z", "at": -6.6, "from": -8.0, "to": -4.4, "style": "glass_top"}, + # conference room + {"a": "x", "at": -4.4, "from": -6.6, "to": -0.4, "door": -3.4}, + {"a": "z", "at": -0.4, "from": -8.0, "to": -4.4}, + # break room + {"a": "z", "at": 5.4, "from": -1.2, "to": 8.0, "door": 2.2}, + {"a": "x", "at": -1.2, "from": 5.4, "to": 11.0}, + # copier alcove + {"a": "z", "at": 3.6, "from": -1.6, "to": 1.0}, + # reception screen + {"a": "z", "at": -6.9, "from": 4.6, "to": 8.0, "door": 6.8}, + ], + "windows": [{"a": "z", "at": 11.0, "from": -8.0, "to": 8.0, + "y0": 0.85, "y1": 2.35, "blinds": true}], + "lights": {"xs": [-8.5, -4.6, -0.7, 3.2, 7.1], "zs": [-6.2, -2.6, 1.0, 4.6], + "color": Color(1.0, 0.98, 0.93), "energy": 1.9, "style": "troffer"}, + "sun": {"color": Color(0.82, 0.88, 1.0), "energy": 0.7, + "rot": Vector3(-30, -104, 0)}, + "env": {"bg": Color(0.55, 0.62, 0.72), "ambient": Color(0.50, 0.50, 0.50), + "ambient_energy": 0.5}, + "clusters": [Vector2(-4.6, -2.0), Vector2(-4.6, 2.8), + Vector2(-0.4, -2.0), Vector2(-0.4, 2.8)], + "cluster_gap": 0.78, + "slabs": [ + # reception counter + {"size": Vector3(0.70, 1.10, 3.0), "at": Vector3(-9.9, 0.55, 5.6), "mat": "wood"}, + {"size": Vector3(0.92, 0.08, 3.2), "at": Vector3(-9.9, 1.14, 5.6), "mat": "trim"}, + {"size": Vector3(1.8, 1.10, 0.70), "at": Vector3(-9.6, 0.55, 4.45), "mat": "wood"}, + # conference table + whiteboard + {"size": Vector3(3.9, 0.09, 1.35), "at": Vector3(-3.5, 0.74, -6.2), "mat": "wood"}, + {"size": Vector3(0.16, 0.72, 0.90), "at": Vector3(-5.1, 0.36, -6.2), "mat": "trim"}, + {"size": Vector3(0.16, 0.72, 0.90), "at": Vector3(-1.9, 0.36, -6.2), "mat": "trim"}, + {"size": Vector3(2.5, 1.25, 0.03), "at": Vector3(-3.5, 1.6, -7.93), "mat": "trim"}, + {"size": Vector3(2.4, 1.15, 0.05), "at": Vector3(-3.5, 1.6, -7.90), "mat": "ceiling"}, + # break room kitchen + tables + {"size": Vector3(0.62, 0.88, 3.4), "at": Vector3(10.58, 0.44, 4.0), "mat": "wood"}, + {"size": Vector3(0.70, 0.06, 3.5), "at": Vector3(10.58, 0.90, 4.0), "mat": "trim"}, + {"size": Vector3(1.05, 0.07, 1.05), "at": Vector3(7.4, 0.73, 0.4), "mat": "ceiling"}, + {"size": Vector3(0.12, 0.70, 0.12), "at": Vector3(7.4, 0.36, 0.4), "mat": "trim"}, + {"size": Vector3(1.05, 0.07, 1.05), "at": Vector3(7.4, 0.73, 3.0), "mat": "ceiling"}, + {"size": Vector3(0.12, 0.70, 0.12), "at": Vector3(7.4, 0.36, 3.0), "mat": "trim"}, + # warehouse roller door + {"size": Vector3(2.6, 2.4, 0.12), "at": Vector3(7.8, 1.2, -7.94), "mat": "trim"}, + ], + "props": [ + {"glb": "store-column-white", "at": Vector3(-2.2, 0.0, -3.4)}, + {"glb": "store-column-white", "at": Vector3(2.2, 0.0, -3.4)}, + {"glb": "store-aircon", "at": Vector3(2.6, 2.35, -7.9)}, + {"glb": "office-plant", "at": Vector3(-6.4, 0.0, 6.6)}, + {"glb": "office-plant", "at": Vector3(2.9, 0.0, 6.4)}, + {"glb": "vending-machine", "at": Vector3(6.3, 0.0, 7.4), "yaw": PI}, + {"glb": "vending-machine", "at": Vector3(7.3, 0.0, 7.4), "yaw": PI}, + {"glb": "microwave", "at": Vector3(10.55, 0.93, 5.1), "yaw": -PI / 2}, + ], + "chairs": [ + {"at": Vector3(-7.6, 0.0, 5.5), "yaw": -PI / 2}, + {"at": Vector3(-7.6, 0.0, 6.35), "yaw": -PI / 2}, + {"at": Vector3(-7.6, 0.0, 7.2), "yaw": -PI / 2}, + {"at": Vector3(-4.85, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-3.95, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-3.05, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-2.15, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-4.85, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-3.95, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-3.05, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-2.15, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-8.8, 0.0, -7.1), "yaw": 0.0}, + {"at": Vector3(-9.6, 0.0, -5.1), "yaw": PI}, + {"at": Vector3(8.0, 0.0, 1.13), "yaw": 2.0}, + {"at": Vector3(7.7, 0.0, -0.49), "yaw": 4.1}, + {"at": Vector3(6.5, 0.0, 0.56), "yaw": 0.2}, + {"at": Vector3(8.0, 0.0, 3.73), "yaw": 2.0}, + {"at": Vector3(7.7, 0.0, 2.11), "yaw": 4.1}, + {"at": Vector3(6.5, 0.0, 3.16), "yaw": 0.2}, + ], + "copier": {"at": Vector3(3.0, 0.0, -0.3), "yaw": -PI / 2}, + "smashables": [ + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -1.6)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -0.9)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -0.2)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(-6.2, 3.4)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(5.9, -0.6)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.3, 5.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(1.6, 5.4)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(2.1, 4.7)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(6.4, -3.3)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(7.3, -3.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-9.7, 1.2)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(-6.0, -1.0)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(1.0, 1.0)}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(-9.8, 5.0), "sit_on": 1.18}, + {"glb": "rack", "kind": "wood", "at": Vector2(9.4, -6.6), "frozen": true}, + {"glb": "rack", "kind": "wood", "at": Vector2(9.4, -4.3), "frozen": true}, + {"glb": "turntable", "kind": "wood", "at": Vector2(6.9, -5.9)}, + ], + "records": [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)], + "spawn": {"at": Vector3(-8.6, 0.0, 7.2), "yaw": deg_to_rad(196.0)}, + } + +# ================================================================ LEVEL 02 +static func pawnee() -> Dictionary: + return { + "name": "PAWNEE CITY HALL", + "subtitle": "parks & recreation · 4th floor", + "bounds": {"x0": -10.0, "x1": 10.0, "z0": -7.0, "z1": 7.5, "h": 2.70}, + "palette": { + "floor": Color(0.34, 0.36, 0.40), "wall": Color(0.84, 0.80, 0.68), + "ceiling": Color(0.90, 0.89, 0.85), "trim": Color(0.30, 0.22, 0.14), + "partition": Color(0.52, 0.50, 0.42), "wood": Color(0.50, 0.34, 0.19), + "accent": Color(0.16, 0.36, 0.28), + }, + "floor_style": "carpet", + "ceiling": "tile", + "walls": [ + # the department head's office, and the meeting room beside it + {"a": "x", "at": -3.4, "from": -10.0, "to": -5.6, "door": -7.0}, + {"a": "z", "at": -5.6, "from": -7.0, "to": -3.4, "style": "glass_top"}, + {"a": "x", "at": -3.4, "from": -5.6, "to": 0.4, "door": -1.6}, + {"a": "z", "at": 0.4, "from": -7.0, "to": -3.4}, + # records room, and the corridor wall that makes this place a warren + {"a": "z", "at": 4.6, "from": -7.0, "to": -2.0, "door": -4.4}, + {"a": "x", "at": -2.0, "from": 4.6, "to": 10.0}, + # the public counter divides the front third from everything else + {"a": "x", "at": 3.6, "from": -10.0, "to": 3.0, "door": -8.0, "style": "half"}, + ], + "windows": [{"a": "x", "at": 7.5, "from": -10.0, "to": 10.0, + "y0": 0.95, "y1": 2.30, "blinds": true}], + "lights": {"xs": [-7.5, -3.4, 0.7, 4.8, 8.4], "zs": [-5.2, -1.6, 2.0, 5.6], + "color": Color(1.0, 0.96, 0.86), "energy": 1.8, "style": "troffer", + "key_energy": 1.4}, + "sun": {"color": Color(0.90, 0.88, 0.78), "energy": 0.55, + "rot": Vector3(-34, 20, 0)}, + "env": {"bg": Color(0.62, 0.66, 0.62), "ambient": Color(0.50, 0.48, 0.44), + "ambient_energy": 0.52, "exposure": 1.02}, + "clusters": [Vector2(-6.0, 0.4), Vector2(-1.4, 0.4), Vector2(2.6, 0.4)], + "cluster_gap": 0.78, + "slabs": [ + # the public counter itself — the thing between you and the citizens + {"size": Vector3(6.6, 1.06, 0.66), "at": Vector3(-1.2, 0.53, 5.4), "mat": "wood"}, + {"size": Vector3(6.9, 0.07, 0.86), "at": Vector3(-1.2, 1.10, 5.4), "mat": "trim"}, + {"size": Vector3(0.66, 1.06, 2.2), "at": Vector3(2.1, 0.53, 6.4), "mat": "wood"}, + # meeting room table + {"size": Vector3(2.9, 0.09, 1.25), "at": Vector3(-2.6, 0.74, -5.2), "mat": "wood"}, + {"size": Vector3(0.14, 0.72, 0.84), "at": Vector3(-3.7, 0.36, -5.2), "mat": "trim"}, + {"size": Vector3(0.14, 0.72, 0.84), "at": Vector3(-1.5, 0.36, -5.2), "mat": "trim"}, + # pinboards — municipal offices are 40% pinboard by surface area + {"size": Vector3(2.2, 1.20, 0.05), "at": Vector3(5.0, 1.65, -6.93), "mat": "accent"}, + {"size": Vector3(2.3, 1.30, 0.03), "at": Vector3(5.0, 1.65, -6.90), "mat": "trim"}, + {"size": Vector3(0.05, 1.20, 2.6), "at": Vector3(-9.93, 1.62, 2.0), "mat": "accent"}, + {"size": Vector3(0.03, 1.30, 2.7), "at": Vector3(-9.90, 1.62, 2.0), "mat": "trim"}, + # department head's desk + {"size": Vector3(1.7, 0.08, 0.85), "at": Vector3(-7.4, 0.74, -5.4), "mat": "wood"}, + {"size": Vector3(1.5, 0.66, 0.72), "at": Vector3(-7.4, 0.35, -5.4), "mat": "wood"}, + ], + "props": [ + {"glb": "office-plant", "at": Vector3(-9.3, 0.0, 6.6)}, + {"glb": "office-plant", "at": Vector3(3.2, 0.0, -2.6)}, + {"glb": "office-plant", "at": Vector3(-9.3, 0.0, -1.0)}, + {"glb": "sofa", "at": Vector3(-6.6, 0.0, 6.5), "yaw": PI}, + {"glb": "vending-machine", "at": Vector3(9.2, 0.0, 6.6), "yaw": -PI / 2}, + {"glb": "microwave", "at": Vector3(9.3, 0.90, 1.2), "yaw": -PI / 2}, + {"glb": "store-aircon", "at": Vector3(-6.0, 2.30, -6.9)}, + ], + "chairs": [ + {"at": Vector3(-7.4, 0.0, -4.2), "yaw": PI}, + {"at": Vector3(-8.6, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-3.5, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-2.6, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-1.7, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-3.5, 0.0, -4.1), "yaw": PI}, + {"at": Vector3(-2.6, 0.0, -4.1), "yaw": PI}, + {"at": Vector3(-1.7, 0.0, -4.1), "yaw": PI}, + {"at": Vector3(-4.0, 0.0, 6.4), "yaw": PI}, + {"at": Vector3(0.4, 0.0, 6.5), "yaw": PI}, + {"at": Vector3(1.2, 0.0, 6.5), "yaw": PI}, + ], + "copier": {"at": Vector3(6.4, 0.0, 2.4), "yaw": PI}, + "smashables": [ + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(5.4, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(6.1, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(6.8, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(7.5, -3.2)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(3.9, 4.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.6, -5.4)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(9.2, -4.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-4.8, 3.0)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(-8.8, -3.9)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(4.4, 0.9)}, + {"glb": "guillotine", "kind": "steel", "at": Vector2(6.4, 3.6), "sit_on": 0.0}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(-7.4, -5.4), "sit_on": 0.78}, + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-7.6, -5.2), "sit_on": 0.78}, + ], + "records": [], + "spawn": {"at": Vector3(-8.0, 0.0, 6.6), "yaw": deg_to_rad(178.0)}, + } + +# ================================================================ LEVEL 03 +static func house() -> Dictionary: + return { + "name": "THE INCUBATOR", + "subtitle": "seed round · someone's house", + "bounds": {"x0": -10.0, "x1": 10.0, "z0": -8.0, "z1": 8.0, "h": 3.05}, + "palette": { + "floor": Color(0.46, 0.33, 0.20), "wall": Color(0.92, 0.91, 0.88), + "ceiling": Color(0.95, 0.95, 0.94), "trim": Color(0.18, 0.17, 0.17), + "partition": Color(0.72, 0.72, 0.70), "wood": Color(0.40, 0.27, 0.16), + "accent": Color(0.22, 0.44, 0.62), + }, + "floor_style": "timber", + "ceiling": "flat", + "walls": [ + # only two real walls: a bedroom-turned-office and the bathroom nobody cleans + {"a": "z", "at": -4.6, "from": -8.0, "to": -2.4, "door": -4.0}, + {"a": "x", "at": -2.4, "from": -10.0, "to": -4.6, "door": -7.4}, + {"a": "z", "at": 4.0, "from": -8.0, "to": -5.2, "door": -6.6}, + # the kitchen island's back wall + {"a": "x", "at": -1.0, "from": 4.0, "to": 10.0, "style": "half"}, + ], + "windows": [{"a": "x", "at": 8.0, "from": -10.0, "to": 10.0, + "y0": 0.35, "y1": 2.70, "blinds": false}], + "lights": {"xs": [-6.5, -1.5, 3.5, 7.5], "zs": [-5.0, 0.0, 5.0], + "color": Color(1.0, 0.90, 0.76), "energy": 2.4, "style": "pendant", + "key_energy": 1.7}, + "sun": {"color": Color(1.0, 0.94, 0.80), "energy": 1.5, + "rot": Vector3(-46, -96, 0)}, + "env": {"bg": Color(0.70, 0.80, 0.90), "ambient": Color(0.56, 0.55, 0.52), + "ambient_energy": 0.62, "exposure": 1.05}, + # nobody has a desk; they work at the big table + "clusters": [Vector2(1.6, -1.4), Vector2(1.6, 2.6)], + "cluster_gap": 0.80, + "slabs": [ + # kitchen island + counter + {"size": Vector3(2.6, 0.92, 1.05), "at": Vector3(4.2, 0.46, 5.6), "mat": "wood"}, + {"size": Vector3(2.8, 0.08, 1.20), "at": Vector3(4.2, 0.95, 5.6), "mat": "ceiling"}, + {"size": Vector3(6.0, 0.90, 0.65), "at": Vector3(5.0, 0.45, 7.5), "mat": "ceiling"}, + {"size": Vector3(6.2, 0.06, 0.75), "at": Vector3(5.0, 0.92, 7.5), "mat": "trim"}, + # the long table everyone actually works at + {"size": Vector3(1.5, 0.09, 4.6), "at": Vector3(-4.4, 0.75, 0.6), "mat": "wood"}, + {"size": Vector3(0.12, 0.72, 0.12), "at": Vector3(-4.9, 0.36, -1.4), "mat": "trim"}, + {"size": Vector3(0.12, 0.72, 0.12), "at": Vector3(-3.9, 0.36, 2.6), "mat": "trim"}, + # whiteboard, obviously + {"size": Vector3(0.05, 1.40, 3.0), "at": Vector3(-9.93, 1.75, 1.0), "mat": "ceiling"}, + {"size": Vector3(0.03, 1.50, 3.1), "at": Vector3(-9.90, 1.75, 1.0), "mat": "trim"}, + # a low bookshelf run + {"size": Vector3(0.42, 0.90, 3.2), "at": Vector3(-9.7, 0.45, -5.0), "mat": "wood"}, + # pool decking visible through the glass + {"size": Vector3(7.0, 0.10, 16.0), "at": Vector3(12.6, -0.05, 0.0), + "mat": "ceiling", "collide": false}, + ], + "props": [ + {"glb": "sofa", "at": Vector3(-6.6, 0.0, 5.4), "yaw": -PI / 2}, + {"glb": "sofa", "at": Vector3(-2.0, 0.0, 6.6), "yaw": PI}, + {"glb": "office-plant", "at": Vector3(-0.6, 0.0, 3.2)}, + {"glb": "office-plant", "at": Vector3(7.2, 0.0, -6.4)}, + {"glb": "office-plant", "at": Vector3(-9.2, 0.0, 7.2)}, + {"glb": "microwave", "at": Vector3(5.4, 0.95, 7.4), "yaw": PI}, + {"glb": "server-rack", "at": Vector3(9.0, 0.0, -6.8), "yaw": -PI / 2}, + ], + "chairs": [ + {"at": Vector3(-6.0, 0.0, -1.4), "yaw": -PI / 2}, + {"at": Vector3(-6.0, 0.0, -0.2), "yaw": -PI / 2}, + {"at": Vector3(-6.0, 0.0, 1.4), "yaw": -PI / 2}, + {"at": Vector3(-6.0, 0.0, 2.6), "yaw": -PI / 2}, + {"at": Vector3(-2.8, 0.0, -1.0), "yaw": PI / 2}, + {"at": Vector3(-2.8, 0.0, 1.8), "yaw": PI / 2}, + {"at": Vector3(4.2, 0.0, 4.2), "yaw": 0.0}, + {"at": Vector3(5.2, 0.0, 4.2), "yaw": 0.0}, + {"at": Vector3(-7.6, 0.0, -6.4), "yaw": 0.2}, + ], + "copier": {"at": Vector3(-8.6, 0.0, -3.4), "yaw": PI / 2}, + "smashables": [ + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-4.4, -2.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.4, 2.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.9, 1.5)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-8.2, 6.4)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(-2.6, -3.2)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(6.2, 3.0)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(1.0, 6.8)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(-9.3, -1.6)}, + {"glb": "turntable", "kind": "wood", "at": Vector2(-9.4, -6.6)}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(4.0, 5.6), "sit_on": 0.99}, + ], + "records": [Vector2(-8.4, -7.0), Vector2(-7.6, -7.0)], + "spawn": {"at": Vector3(-1.0, 0.0, 7.0), "yaw": deg_to_rad(186.0)}, + } + +# ================================================================ LEVEL 04 +static func basement() -> Dictionary: + return { + "name": "SUB-LEVEL 4", + "subtitle": "i.t. department · no windows", + "bounds": {"x0": -12.0, "x1": 12.0, "z0": -6.0, "z1": 6.0, "h": 2.90}, + "palette": { + "floor": Color(0.22, 0.22, 0.23), "wall": Color(0.50, 0.53, 0.46), + "ceiling": Color(0.20, 0.20, 0.21), "trim": Color(0.10, 0.10, 0.10), + "partition": Color(0.34, 0.36, 0.32), "wood": Color(0.30, 0.20, 0.12), + "accent": Color(0.52, 0.10, 0.09), + }, + "floor_style": "concrete", + "ceiling": "flat", + "walls": [ + # the boss's glass box, bolted into a room that never wanted one + {"a": "z", "at": 4.6, "from": -6.0, "to": 1.2, "door": -1.0, + "style": "glass_top"}, + {"a": "x", "at": 1.2, "from": 4.6, "to": 12.0}, + # server room behind a security door + {"a": "z", "at": -6.4, "from": -6.0, "to": 6.0, "door": 1.0}, + # a stub wall someone put up and nobody took down + {"a": "x", "at": -1.4, "from": -4.0, "to": -1.6}, + ], + "windows": [], # it is a basement + "lights": {"xs": [-9.0, -4.5, 0.0, 4.5, 9.0], "zs": [-3.6, 0.4, 4.0], + "color": Color(0.86, 0.96, 0.84), "energy": 2.0, "style": "strip", + "key_energy": 1.1}, + "env": {"bg": Color(0.06, 0.07, 0.06), "ambient": Color(0.32, 0.36, 0.32), + "ambient_energy": 0.40, "exposure": 1.08}, + "clusters": [Vector2(3.6, 0.0)], + "cluster_gap": 0.82, + "slabs": [ + # exposed services, because there's no ceiling to hide them + {"size": Vector3(24.0, 0.22, 0.22), "at": Vector3(0.0, 2.70, -3.0), + "mat": "trim", "collide": false}, + {"size": Vector3(24.0, 0.16, 0.16), "at": Vector3(0.0, 2.62, 1.4), + "mat": "trim", "collide": false}, + {"size": Vector3(24.0, 0.30, 0.10), "at": Vector3(0.0, 2.78, 4.2), + "mat": "trim", "collide": false}, + # boss's desk in the glass box + {"size": Vector3(1.8, 0.08, 0.86), "at": Vector3(8.0, 0.74, 5.4), "mat": "wood"}, + {"size": Vector3(1.6, 0.66, 0.74), "at": Vector3(8.0, 0.35, 5.4), "mat": "wood"}, + # workbench along the back + {"size": Vector3(5.0, 0.08, 0.80), "at": Vector3(-6.0, 0.90, -5.5), "mat": "trim"}, + {"size": Vector3(0.10, 0.88, 0.70), "at": Vector3(-8.4, 0.44, -5.5), "mat": "trim"}, + {"size": Vector3(0.10, 0.88, 0.70), "at": Vector3(-3.6, 0.44, -5.5), "mat": "trim"}, + # the famous red door + {"size": Vector3(1.10, 2.10, 0.10), "at": Vector3(1.0, 1.05, -6.35), + "mat": "accent"}, + ], + "props": [ + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, -4.4), "yaw": PI / 2}, + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, -2.4), "yaw": PI / 2}, + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, -0.4), "yaw": PI / 2}, + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, 1.6), "yaw": PI / 2}, + {"glb": "sofa", "at": Vector3(-4.0, 0.0, 3.4), "yaw": 0.15}, + {"glb": "office-plant", "at": Vector3(11.2, 0.0, 5.4)}, + {"glb": "vending-machine", "at": Vector3(-0.4, 0.0, 5.4), "yaw": PI}, + {"glb": "microwave", "at": Vector3(-5.0, 0.95, -5.5), "yaw": PI}, + ], + "chairs": [ + {"at": Vector3(8.0, 0.0, 4.1), "yaw": PI}, + {"at": Vector3(6.6, 0.0, 5.9), "yaw": 0.3}, + {"at": Vector3(-6.6, 0.0, -4.2), "yaw": PI}, + {"at": Vector3(-1.0, 0.0, 2.2), "yaw": 1.2}, + ], + "copier": {"at": Vector3(0.4, 0.0, -4.4), "yaw": 0.0}, + "smashables": [ + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-7.0, -5.5), "sit_on": 0.94}, + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-5.0, -4.6)}, + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-8.4, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(10.8, 3.0)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(10.8, 2.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.6, -4.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.0, -5.3)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-1.4, -4.6)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(5.6, -4.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(6.3, -5.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(7.0, -4.6)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(2.0, 2.6)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(9.0, 4.4)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(0.6, 2.2)}, + {"glb": "shredder", "kind": "plastic", "at": Vector2(-2.2, -2.0)}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(8.3, 5.4), "sit_on": 0.78}, + {"glb": "turntable", "kind": "wood", "at": Vector2(-9.0, 4.4)}, + ], + "records": [Vector2(-8.2, 5.0), Vector2(-7.5, 5.0)], + "spawn": {"at": Vector3(1.0, 0.0, -5.2), "yaw": deg_to_rad(0.0)}, + } diff --git a/game/scripts/Levels.gd.uid b/game/scripts/Levels.gd.uid new file mode 100644 index 0000000..f9b2553 --- /dev/null +++ b/game/scripts/Levels.gd.uid @@ -0,0 +1 @@ +uid://c0ic0hj3e0do0 diff --git a/game/scripts/Main.gd b/game/scripts/Main.gd index a2ca70b..465c995 100644 --- a/game/scripts/Main.gd +++ b/game/scripts/Main.gd @@ -30,11 +30,13 @@ var _juice: Juice var _player: Player # kept so GameMode can reach player.grab var _records: Array[Record] = [] # the live records, handed to GameMode each round var _game_mode: GameMode -var _office: Office # the level shell: walls, fittings, lighting +var _plan: Floorplan # the level shell: walls, fittings, lighting — see Levels.gd +var _level_id := "scranton" var _rage: Rage # the tilt meter — see Rage.gd var _rage_hud: RageOverlay var _tasks: Tasks var _shift: CubicleHell +var _env_node: WorldEnvironment func _ready() -> void: randomize() @@ -74,34 +76,35 @@ func _ready() -> void: ## Every desk is a spreadsheet you have to fill in; the copier is the copier. func _register_stations() -> void: var i := 1 - for t in _office.desk_spots(): + for t in _plan.desk_spots(): _tasks.register(Tasks.Kind.SPREADSHEET, (t as Transform3D).origin, "work at desk %d" % i) i += 1 - _tasks.register(Tasks.Kind.COPIER, _office.copier_spot().origin, - "use the photocopier") + if _plan.has_copier(): + _tasks.register(Tasks.Kind.COPIER, _plan.copier_spot().origin, + "use the photocopier") # ---------------------------------------------------------------- world / camera func _setup_world() -> void: # The level shell: floor, walls, ceiling, windows, fittings and ALL the lighting. # It owns the environment too, because interior lighting and the ambient/tonemap # settings are one decision, not two. - _office = Office.new() - add_child(_office) - _office.build() + _plan = Floorplan.new() + add_child(_plan) + _plan.build(Levels.get_level(_level_id)) - var we := WorldEnvironment.new() + _env_node = WorldEnvironment.new() var env := Environment.new() - _office.configure_environment(env) - we.environment = env - add_child(we) + _plan.configure_environment(env) + _env_node.environment = env + add_child(_env_node) # First-person player. add_child() runs its _ready() synchronously, so player.camera # and player.grab exist on the next line; we reuse them. _player = Player.new() add_child(_player) - _player.global_position = _office.spawn_point() - _player.rotation.y = _office.spawn_yaw() + _player.global_position = _plan.spawn_point() + _player.rotation.y = _plan.spawn_yaw() _cam = _player.camera _world = Node3D.new() @@ -125,6 +128,7 @@ const COOLER_GLB := "res://assets/store/water-cooler.glb" const BOX_GLB := "res://assets/store/cardboard-box.glb" const TURNTABLE_GLB := "res://assets/store/turntable.glb" const CHAIR_GLB := "res://assets/store/office-chair.glb" +const P := "res://assets/store/" ## Mass by material, so a thrown record can't tip a filing cabinet but a sledgehammer ## still can. Everything defaulted to 1 kg before, which is why the level fell over. @@ -149,7 +153,7 @@ func _populate() -> void: _records.clear() # stale refs from the freed world; repopulated by _place_record # --- the bullpen: a desk at every cluster spot, each with a monitor on it --- - var spots := _office.desk_spots() + var spots := _plan.desk_spots() for i in spots.size(): var t: Transform3D = spots[i] var xz := Vector2(t.origin.x, t.origin.z) @@ -161,53 +165,45 @@ func _populate() -> void: # used to guess a constant here and kept putting chairs inside desks (or, once # pushed out far enough to clear them, inside each other across the aisle). var depth: float = (desk["aabb"] as AABB).size.z - var back := signf(t.origin.z - _office.CLUSTERS[i / 2].y) + var back := signf(t.origin.z - _plan.desk_cluster(i).y) if is_zero_approx(back): back = 1.0 _glb_piece(CHAIR_GLB, "plastic", xz + Vector2(0.0, back * (depth * 0.5 + 0.42)), false, 0.0, t.basis.get_euler().y) # --- the copier alcove: the PRINTER is the level boss, on its own desk --- - var cs := _office.copier_spot() - var cxz := Vector2(cs.origin.x, cs.origin.z) - var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y) - var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"]) - var boss := printer["piece"] as Smashable - boss.is_boss = true - boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge - boss.mass = 8.0 # heavy, so hits rock it but don't shove it off the desk - (cdesk["piece"] as Smashable).supports.append(printer["piece"]) + if _plan.has_copier(): + var cs := _plan.copier_spot() + var cxz := Vector2(cs.origin.x, cs.origin.z) + var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y) + var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"]) + var boss := printer["piece"] as Smashable + boss.is_boss = true + boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge + boss.mass = 8.0 # heavy: hits rock it but don't shove it off the desk + (cdesk["piece"] as Smashable).supports.append(printer["piece"]) - # --- filing cabinets down the wall by the copier, water coolers, cartons --- - for z in [-1.6, -0.9, -0.2]: - _glb_piece(CABINET_GLB, "steel", Vector2(4.5, z), false) - _glb_piece(COOLER_GLB, "steel", Vector2(-6.2, 3.4), false) - _glb_piece(COOLER_GLB, "steel", Vector2(5.9, -0.6), false) - # Cartons go on OPEN floor only. Tucking them under desks or against the conference - # table spawned them inside those colliders, and Jolt resolved the overlap by firing - # them across the room on frame one. - for c in [Vector2(-2.3, 5.2), Vector2(1.6, 5.4), Vector2(2.1, 4.7), - Vector2(6.4, -3.3), Vector2(7.3, -3.8), Vector2(-9.7, 1.2)]: - _glb_piece(BOX_GLB, "cardboard", c, false) + # --- everything else this site contains, straight from its spec --- + for e in _plan.smashables(): + var d: Dictionary = e + _glb_piece(P + String(d["glb"]) + ".glb", String(d.get("kind", "wood")), + d["at"], bool(d.get("frozen", false)), float(d.get("sit_on", 0.0)), + float(d.get("yaw", 0.0))) - # --- the warehouse corner: shelving, crates, and somebody's record collection. - # --- This is where the grab/slide/throw ritual still lives. - _glb_piece(RACK_GLB, "wood", Vector2(9.4, -6.6), true) - _glb_piece(RACK_GLB, "wood", Vector2(9.4, -4.3), true) - _glb_piece(TURNTABLE_GLB, "wood", Vector2(6.9, -5.9), false) - for spot in [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)]: + # --- crates of records, where the site has any. Records stand side by side ON the + # --- crate lid, offset by more than a sleeve is thick: they used to be stacked 3 cm + # --- apart vertically while being 30 cm TALL, so Jolt resolved 27 cm of + # --- interpenetration explosively on frame one. + for spot in _plan.spec.get("records", []): var crate := _glb_piece(CRATE_GLB, "wood", spot, false) var crate_top: float = crate["top"] - # Records stand side by side ON the crate lid, offset along Z by more than a - # sleeve is thick. They used to be stacked 3 cm apart vertically while being - # 30 cm TALL, so Jolt resolved 27 cm of interpenetration explosively on frame - # one and shoved the surrounding furniture over before you touched anything. for i in range(3): var rec := _place_record(spot + Vector2(randf_range(-0.02, 0.02), (float(i) - 1.0) * 0.045), crate_top) (crate["piece"] as Smashable).supports.append(rec) + # --- chairs: the best thing in an office to send across the room --- - for t in _office.chair_spots(): + for t in _plan.chair_spots(): _glb_piece(CHAIR_GLB, "plastic", Vector2(t.origin.x, t.origin.z), false, 0.0, t.basis.get_euler().y) @@ -404,8 +400,9 @@ func _physics_process(dt: float) -> void: # report where it was PLACED, not where it is now — at 500 m/s a body is # already 8 m away by the time this runs, which points at nothing useful var at = b.get_meta("spawn_pos", b.global_position) - push_warning("[spawn] %s placed overlapping something at %s (%.0f m/s) — clamped" % [ - b.name, (at as Vector3).snappedf(0.01), v]) + push_warning("[spawn] %s (%s) placed overlapping something at %s (%.0f m/s) — clamped" % [ + b.name, (b as Smashable).kind if b is Smashable else "?", + (at as Vector3).snappedf(0.01), v]) b.linear_velocity = b.linear_velocity.normalized() * SPAWN_MAX_SPEED if b.angular_velocity.length() > SPAWN_MAX_SPIN: b.angular_velocity = b.angular_velocity.normalized() * SPAWN_MAX_SPIN @@ -464,10 +461,53 @@ func _unhandled_input(event: InputEvent) -> void: KEY_H: if _hud != null: _hud.cycle() + KEY_L: + _load_level(Levels.next_id(_level_id)) KEY_F: if _game_mode != null: _game_mode.report() # report the disc you're holding (FindMisfiled) +## Swap sites without restarting: tear down the shell and everything in it, build the +## new spec, and re-arm the shift. The player is moved to the new spawn because the old +## one is very likely now inside a wall. +func _load_level(id: String) -> void: + _level_id = id + for c in _world.get_children(): + c.queue_free() + _plan.queue_free() + if _env_node != null: + _env_node.queue_free() + await get_tree().process_frame + + _plan = Floorplan.new() + add_child(_plan) + _plan.build(Levels.get_level(_level_id)) + _env_node = WorldEnvironment.new() + var env := Environment.new() + _plan.configure_environment(env) + _env_node.environment = env + add_child(_env_node) + + _player.global_position = _plan.spawn_point() + _player.rotation.y = _plan.spawn_yaw() + _player.velocity = Vector3.ZERO + + _smash_count = 0 + _score = 0 + _tally.clear() + _populate() + _spawn_guard = SPAWN_GUARD_TIME + _guard_warned.clear() + + _tasks.clear_stations() + _register_stations() + if _game_mode != null: + _game_mode.begin_free_play(_records) + if _shift != null: + _shift.begin() + if _hud != null: + _hud.toast("%s — %s" % [_plan.level_name(), _plan.subtitle()]) + # The Day-0 stress gate: rain N bodies, watch the FPS counter hold. func _rain(n: int) -> void: for i in n: @@ -494,6 +534,10 @@ func _reset() -> void: _tally.clear() await get_tree().process_frame _populate() + _spawn_guard = SPAWN_GUARD_TIME + _guard_warned.clear() + if _shift != null: + _shift.begin() # re-arm whatever mode we're in against the fresh set of records if _game_mode != null: if _game_mode.mode == GameMode.Mode.FIND_MISFILED: @@ -519,7 +563,7 @@ func _on_weapon_changed(w: Weapon) -> void: if _hud != null and w != null: _hud.toast(w.display_name) -const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · B rain · R reset" +const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · L site · B rain · R reset" func _process(_dt: float) -> void: if _hud == null: diff --git a/game/scripts/Office.gd b/game/scripts/Office.gd deleted file mode 100644 index 5921101..0000000 --- a/game/scripts/Office.gd +++ /dev/null @@ -1,454 +0,0 @@ -extends Node3D -class_name Office - -## LEVEL 01 — the open-plan office. -## -## The prototype ran on a grey disc in a black void, which quietly broke the premise: a -## game about wrecking your workplace needs a workplace. This is that workplace, laid out -## after the Dunder Mifflin floor plan — the shape everyone already knows. -## -## z- (back) -## +--------------------------------------------------+ -## | MANAGER | CONFERENCE | | WAREHOUSE | -## | (glass) | | | door | -## |----------+---------------+ +-----------| -## | | | -## | B U L L P E N | BREAK | x+ -## | (4 facing desk pairs) | ROOM | -## | [copier] | | -## |----------+ +-----------| -## | RECEPTION| entrance | -## +--------------------------------------------------+ -## z+ (front / street) -## -## Walls, desks, partitions and counters are boxes built in code — they ARE boxes, so -## modelling them would be wasted effort. Anything with a silhouette a box can't fake -## (chairs, vending machines, the plant) comes from tools/gen_office_props.py. -## -## This script owns the SHELL and the FITTINGS: everything static and non-smashable, plus -## the lighting. Smashable furniture stays Main's business, so the two can be tuned -## independently — Main asks this class where things go via the `spot_*` helpers. -## -## Godot 4.7 GDScript 2.0. - -# ---------------------------------------------------------------- floor plan -const X_MIN := -11.0 -const X_MAX := 11.0 -const Z_BACK := -8.0 -const Z_FRONT := 8.0 -const H := 2.75 # drop ceiling -const WALL_T := 0.16 - -# interior room boundaries -const MGR_X := -6.6 # manager's office: X_MIN..MGR_X, Z_BACK..MGR_Z -const MGR_Z := -4.4 -const CONF_X0 := -6.6 # conference room -const CONF_X1 := -0.4 -const CONF_Z := -4.4 -const BREAK_X := 5.4 # break room: BREAK_X..X_MAX, BREAK_Z..Z_FRONT -const BREAK_Z := -1.2 -const RECEP_Z := 4.6 # reception partition - -const DOOR_W := 1.1 - -const P := "res://assets/store/" - -var _spots_desk: Array[Transform3D] = [] # where Main should put smashable desks -var _spots_chair: Array[Transform3D] = [] -var _spot_copier := Transform3D.IDENTITY - -# ---------------------------------------------------------------- materials -var _m_carpet: StandardMaterial3D -var _m_wall: StandardMaterial3D -var _m_ceil: StandardMaterial3D -var _m_trim: StandardMaterial3D -var _m_glass: StandardMaterial3D -var _m_part: StandardMaterial3D -var _m_wood: StandardMaterial3D - -func build() -> void: - _materials() - _shell() - _interior_walls() - _ceiling_grid() - _lights_rig() - _bullpen() - _reception() - _conference() - _manager() - _break_room() - _dressing() - -func _materials() -> void: - _m_carpet = StandardMaterial3D.new() - _m_carpet.albedo_color = Color(0.30, 0.30, 0.32) # the grey office carpet - _m_carpet.roughness = 1.0 - - _m_wall = StandardMaterial3D.new() - _m_wall.albedo_color = Color(0.80, 0.78, 0.72) # magnolia, of course - _m_wall.roughness = 0.96 - - _m_ceil = StandardMaterial3D.new() - _m_ceil.albedo_color = Color(0.88, 0.88, 0.86) - _m_ceil.roughness = 0.98 - - _m_trim = StandardMaterial3D.new() - _m_trim.albedo_color = Color(0.13, 0.12, 0.13) - _m_trim.roughness = 0.7 - - _m_part = StandardMaterial3D.new() - _m_part.albedo_color = Color(0.44, 0.44, 0.40) # cubicle partition fabric - _m_part.roughness = 1.0 - - _m_wood = StandardMaterial3D.new() - _m_wood.albedo_color = Color(0.42, 0.29, 0.17) - _m_wood.roughness = 0.55 - - _m_glass = StandardMaterial3D.new() - _m_glass.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - _m_glass.albedo_color = Color(0.66, 0.74, 0.78, 0.15) - _m_glass.roughness = 0.05 - _m_glass.cull_mode = BaseMaterial3D.CULL_DISABLED - -# ---------------------------------------------------------------- primitives -func _slab(size: Vector3, centre: Vector3, mat: Material, collide := true, - shadow := true) -> StaticBody3D: - var body := StaticBody3D.new() - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = size - mi.mesh = bm - mi.material_override = mat - if not shadow: - mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - body.add_child(mi) - if collide: - var col := CollisionShape3D.new() - var sh := BoxShape3D.new() - sh.size = size - col.shape = sh - body.add_child(col) - add_child(body) - body.position = centre - return body - -## A wall running along X (or Z) with a doorway punched in it. -func _wall_x(z: float, x0: float, x1: float, mat: Material, door_at := INF) -> void: - if door_at == INF: - _slab(Vector3(x1 - x0, H, WALL_T), Vector3((x0 + x1) * 0.5, H * 0.5, z), mat) - return - var d0 := door_at - DOOR_W * 0.5 - var d1 := door_at + DOOR_W * 0.5 - if d0 > x0: - _slab(Vector3(d0 - x0, H, WALL_T), Vector3((x0 + d0) * 0.5, H * 0.5, z), mat) - if x1 > d1: - _slab(Vector3(x1 - d1, H, WALL_T), Vector3((d1 + x1) * 0.5, H * 0.5, z), mat) - _slab(Vector3(DOOR_W, H - 2.05, WALL_T), Vector3(door_at, (H + 2.05) * 0.5, z), mat) - -func _wall_z(x: float, z0: float, z1: float, mat: Material, door_at := INF) -> void: - if door_at == INF: - _slab(Vector3(WALL_T, H, z1 - z0), Vector3(x, H * 0.5, (z0 + z1) * 0.5), mat) - return - var d0 := door_at - DOOR_W * 0.5 - var d1 := door_at + DOOR_W * 0.5 - if d0 > z0: - _slab(Vector3(WALL_T, H, d0 - z0), Vector3(x, H * 0.5, (z0 + d0) * 0.5), mat) - if z1 > d1: - _slab(Vector3(WALL_T, H, z1 - d1), Vector3(x, H * 0.5, (d1 + z1) * 0.5), mat) - _slab(Vector3(WALL_T, H - 2.05, DOOR_W), Vector3(x, (H + 2.05) * 0.5, door_at), mat) - -## Static set dressing from a GLB, with a box collider sized to its own mesh so you can't -## walk through the vending machine. -func _glb(path: String, pos: Vector3, yaw := 0.0) -> Node3D: - if not ResourceLoader.exists(path): - return null - var packed := load(path) as PackedScene - if packed == null: - return null - var body := StaticBody3D.new() - add_child(body) - body.position = pos - body.rotation.y = yaw - var n: Node3D = packed.instantiate() - body.add_child(n) - var ab := _mesh_aabb(n) - if ab.size != Vector3.ZERO: - var col := CollisionShape3D.new() - var sh := BoxShape3D.new() - sh.size = ab.size - col.shape = sh - col.position = ab.get_center() - body.add_child(col) - return body - -func _mesh_aabb(root: Node3D) -> AABB: - var acc := AABB() - var started := false - for m in _meshes(root): - var mi := m as MeshInstance3D - var a := mi.get_aabb() - a = mi.transform * a - if not started: - acc = a - started = true - else: - acc = acc.merge(a) - return acc - -func _meshes(n: Node, acc: Array = []) -> Array: - if n is MeshInstance3D: - acc.append(n) - for c in n.get_children(): - _meshes(c, acc) - return acc - -## Chairs are NOT placed here. They're the most satisfying thing in an office to send -## across the room, so they're spawned by Main as smashable rigid bodies; Office only -## says where they start. -func _chair(pos: Vector3, yaw: float) -> void: - _spots_chair.append(Transform3D(Basis(Vector3.UP, yaw), pos)) - -# ---------------------------------------------------------------- shell -func _shell() -> void: - var w := X_MAX - X_MIN - var d := Z_FRONT - Z_BACK - var cx := (X_MIN + X_MAX) * 0.5 - var cz := (Z_BACK + Z_FRONT) * 0.5 - _slab(Vector3(w + 1.0, 0.30, d + 1.0), Vector3(cx, -0.15, cz), _m_carpet) - # outer walls; the +X wall is the window wall, so it's built in _windows() - _wall_x(Z_BACK - WALL_T * 0.5, X_MIN, X_MAX, _m_wall) - _wall_x(Z_FRONT + WALL_T * 0.5, X_MIN, X_MAX, _m_wall, -8.6) # entrance - _wall_z(X_MIN - WALL_T * 0.5, Z_BACK, Z_FRONT, _m_wall) - _windows() - -## The window wall. Dunder Mifflin's bullpen has that whole run of blinded windows down -## one side, and it's what stops an interior reading as a sealed box. -func _windows() -> void: - var x := X_MAX + WALL_T * 0.5 - var y0 := 0.85 - var y1 := 2.35 - _slab(Vector3(WALL_T, y0, Z_FRONT - Z_BACK), Vector3(x, y0 * 0.5, - (Z_BACK + Z_FRONT) * 0.5), _m_wall) - _slab(Vector3(WALL_T, H - y1, Z_FRONT - Z_BACK), Vector3(x, (H + y1) * 0.5, - (Z_BACK + Z_FRONT) * 0.5), _m_wall) - # mullions every 2.4 m, glazing between - var z := Z_BACK - while z < Z_FRONT - 0.1: - var z1: float = minf(z + 2.4, Z_FRONT) - _slab(Vector3(0.05, y1 - y0, z1 - z - 0.12), Vector3(X_MAX - 0.02, - (y0 + y1) * 0.5, (z + z1) * 0.5), _m_glass, false, false) - _slab(Vector3(WALL_T, y1 - y0, 0.12), Vector3(x, (y0 + y1) * 0.5, z1), _m_trim) - # venetian blind, half-raised - _slab(Vector3(0.06, 0.55, z1 - z - 0.14), Vector3(X_MAX - 0.10, y1 - 0.28, - (z + z1) * 0.5), _m_ceil, false, false) - z = z1 - -func _ceiling_grid() -> void: - var w := X_MAX - X_MIN - var d := Z_FRONT - Z_BACK - var cx := (X_MIN + X_MAX) * 0.5 - var cz := (Z_BACK + Z_FRONT) * 0.5 - _slab(Vector3(w, 0.16, d), Vector3(cx, H + 0.08, cz), _m_ceil, true, false) - # suspended-tile T-bar, purely visual — it's the texture of an office ceiling - var gx := X_MIN - while gx <= X_MAX + 0.01: - _slab(Vector3(0.05, 0.03, d), Vector3(gx, H - 0.02, cz), _m_trim, false, false) - gx += 1.22 - var gz := Z_BACK - while gz <= Z_FRONT + 0.01: - _slab(Vector3(w, 0.03, 0.05), Vector3(cx, H - 0.02, gz), _m_trim, false, false) - gz += 1.22 - -# ---------------------------------------------------------------- interior walls -func _interior_walls() -> void: - # manager's office — half solid, half glass so you can be watched through it - _wall_x(MGR_Z, X_MIN, MGR_X, _m_wall, -9.2) - _slab(Vector3(WALL_T, 1.0, MGR_Z - Z_BACK), Vector3(MGR_X, 0.5, - (Z_BACK + MGR_Z) * 0.5), _m_wall) - _slab(Vector3(0.06, H - 1.05, MGR_Z - Z_BACK), Vector3(MGR_X, (H + 1.0) * 0.5, - (Z_BACK + MGR_Z) * 0.5), _m_glass, true, false) - - # conference room - _wall_x(CONF_Z, CONF_X0, CONF_X1, _m_wall, -3.4) - _wall_z(CONF_X1, Z_BACK, CONF_Z, _m_wall) - - # break room - _wall_z(BREAK_X, BREAK_Z, Z_FRONT, _m_wall, 2.2) - _wall_x(BREAK_Z, BREAK_X, X_MAX, _m_wall) - - # warehouse roller door on the back wall - _slab(Vector3(2.6, 2.4, 0.12), Vector3(7.8, 1.2, Z_BACK + 0.06), _m_trim) - for i in range(9): - _slab(Vector3(2.5, 0.06, 0.05), Vector3(7.8, 0.18 + i * 0.26, - Z_BACK + 0.14), _m_ceil, false, false) - -# ---------------------------------------------------------------- lighting -## Recessed fluorescent troffers in the ceiling grid. These ARE the light: the old scene -## lit everything with one outdoor directional lamp, which is exactly why it read as -## "props on a plane" rather than "inside somewhere". -func _lights_rig() -> void: - var panel := StandardMaterial3D.new() - panel.albedo_color = Color(0.97, 0.97, 0.93) - panel.emission_enabled = true - panel.emission = Color(1.0, 0.98, 0.92) - panel.emission_energy_multiplier = 1.5 - - var xs := [-8.5, -4.6, -0.7, 3.2, 7.1] - var zs := [-6.2, -2.6, 1.0, 4.6] - for xc in xs: - for zc in zs: - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = Vector3(1.14, 0.05, 0.58) - mi.mesh = bm - mi.material_override = panel - mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - add_child(mi) - mi.position = Vector3(xc, H - 0.06, zc) - - var l := OmniLight3D.new() - l.light_color = Color(1.0, 0.98, 0.93) - l.light_energy = 1.9 - l.omni_range = 6.5 - l.omni_attenuation = 1.2 - l.shadow_enabled = false # 20 shadow-casting omnis is not worth the cost - add_child(l) - l.position = Vector3(xc, H - 0.20, zc) - - # two shadow-casters so props still sit in contact shadows - for zc in [-2.0, 3.0]: - var key := OmniLight3D.new() - key.light_color = Color(1.0, 0.97, 0.92) - key.light_energy = 1.5 - key.omni_range = 13.0 - key.shadow_enabled = true - add_child(key) - key.position = Vector3(-1.0, H - 0.30, zc) - - # daylight through the window wall - var sun := DirectionalLight3D.new() - sun.light_color = Color(0.82, 0.88, 1.0) - sun.light_energy = 0.7 - sun.rotation_degrees = Vector3(-30, -104, 0) - sun.shadow_enabled = true - add_child(sun) - -func configure_environment(env: Environment) -> void: - env.background_mode = Environment.BG_COLOR - env.background_color = Color(0.55, 0.62, 0.72) # daylight past the glass - env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR - env.ambient_light_color = Color(0.50, 0.50, 0.50) - env.ambient_light_energy = 0.5 - env.ssao_enabled = true - env.ssao_radius = 0.8 - env.ssao_intensity = 1.5 - env.tonemap_mode = Environment.TONE_MAPPER_FILMIC - env.tonemap_exposure = 1.0 - -# ---------------------------------------------------------------- bullpen -## Four pairs of desks facing each other across a low partition — the Jim-and-Dwight -## arrangement, which is the single most recognisable thing about the floor plan. -## Rows are 4.8 m apart, not 3.6. At 3.6 the two rows' chairs met back-to-back in the -## middle of the aisle with 3 cm to spare, which Jolt resolved by firing them out of the -## building — and it left no room to walk between the rows either. -const CLUSTERS := [ - Vector2(-4.6, -2.0), Vector2(-4.6, 2.8), - Vector2(-0.4, -2.0), Vector2(-0.4, 2.8), -] - -func _bullpen() -> void: - for c in CLUSTERS: - _partition(Vector3(c.x, 0.0, c.y), 1.9) - for s in [-1.0, 1.0]: - var t := Transform3D(Basis(Vector3.UP, 0.0 if s > 0.0 else PI), - Vector3(c.x, 0.0, c.y + s * 0.78)) - _spots_desk.append(t) - # NOTE: the bullpen chairs are NOT listed here. Their offset depends on how - # deep office-desk.glb actually is, which only Main knows once it has measured - # the mesh — guessing a constant here put chairs inside desks. - # the copier alcove, between the bullpen and the break room - _slab(Vector3(WALL_T, H, 2.6), Vector3(3.6, H * 0.5, -0.3), _m_wall) - _spot_copier = Transform3D(Basis(Vector3.UP, deg_to_rad(-90.0)), - Vector3(3.0, 0.0, -0.3)) - -## A cubicle divider: fabric panel on a dark base rail. -func _partition(at: Vector3, length: float) -> void: - _slab(Vector3(length, 1.18, 0.07), at + Vector3(0, 0.62, 0), _m_part) - _slab(Vector3(length + 0.06, 0.09, 0.11), at + Vector3(0, 1.24, 0), _m_trim) - for s in [-1.0, 1.0]: - _slab(Vector3(0.09, 1.24, 0.30), at + Vector3(s * length * 0.5, 0.62, 0), _m_trim) - -func desk_spots() -> Array[Transform3D]: - return _spots_desk - -func chair_spots() -> Array[Transform3D]: - return _spots_chair - -func copier_spot() -> Transform3D: - return _spot_copier - -# ---------------------------------------------------------------- rooms -func _reception() -> void: - # the counter you meet on the way in, with the waiting-area chairs - _slab(Vector3(0.70, 1.10, 3.0), Vector3(-9.9, 0.55, RECEP_Z + 1.0), _m_wood) - _slab(Vector3(0.92, 0.08, 3.2), Vector3(-9.9, 1.14, RECEP_Z + 1.0), _m_trim) - _slab(Vector3(1.8, 1.10, 0.70), Vector3(-9.6, 0.55, RECEP_Z - 0.15), _m_wood) - _wall_z(-6.9, RECEP_Z, Z_FRONT, _m_wall, 6.8) - for i in range(3): - _chair(Vector3(-7.6, 0.0, 5.5 + i * 0.85), deg_to_rad(-90)) - _glb(P + "office-plant.glb", Vector3(-6.4, 0.0, 6.6)) - -func _conference() -> void: - var cx := (CONF_X0 + CONF_X1) * 0.5 - var cz := (Z_BACK + CONF_Z) * 0.5 - # the long table everyone dreads - _slab(Vector3(3.9, 0.09, 1.35), Vector3(cx, 0.74, cz), _m_wood) - for s in [-1.0, 1.0]: - _slab(Vector3(0.16, 0.72, 0.90), Vector3(cx + s * 1.6, 0.36, cz), _m_trim) - for i in range(4): - var x := cx - 1.35 + i * 0.9 - _chair(Vector3(x, 0.0, cz - 1.15), 0.0) - _chair(Vector3(x, 0.0, cz + 1.15), PI) - # whiteboard - _slab(Vector3(2.4, 1.15, 0.05), Vector3(cx, 1.6, Z_BACK + 0.10), _m_ceil) - _slab(Vector3(2.5, 1.25, 0.03), Vector3(cx, 1.6, Z_BACK + 0.07), _m_trim) - -func _manager() -> void: - var cx := (X_MIN + MGR_X) * 0.5 - var cz := (Z_BACK + MGR_Z) * 0.5 - _chair(Vector3(cx, 0.0, cz - 0.9), 0.0) - _chair(Vector3(cx - 0.8, 0.0, cz + 1.1), PI) - _glb(P + "office-plant.glb", Vector3(X_MIN + 0.7, 0.0, MGR_Z - 0.8)) - -func _break_room() -> void: - var cz := (BREAK_Z + Z_FRONT) * 0.5 - # kitchen counter along the window wall - _slab(Vector3(0.62, 0.88, 3.4), Vector3(X_MAX - 0.42, 0.44, cz + 0.6), _m_wood) - _slab(Vector3(0.70, 0.06, 3.5), Vector3(X_MAX - 0.42, 0.90, cz + 0.6), _m_trim) - _glb(P + "microwave.glb", Vector3(X_MAX - 0.45, 0.93, cz + 1.7), deg_to_rad(-90)) - _glb(P + "vending-machine.glb", Vector3(BREAK_X + 0.9, 0.0, Z_FRONT - 0.6), PI) - _glb(P + "vending-machine.glb", Vector3(BREAK_X + 1.9, 0.0, Z_FRONT - 0.6), PI) - # two round tables with chairs - for i in range(2): - var tx := 7.4 - var tz := BREAK_Z + 1.6 + i * 2.6 - _slab(Vector3(1.05, 0.07, 1.05), Vector3(tx, 0.73, tz), _m_ceil) - _slab(Vector3(0.12, 0.70, 0.12), Vector3(tx, 0.36, tz), _m_trim) - _slab(Vector3(0.62, 0.05, 0.62), Vector3(tx, 0.03, tz), _m_trim) - for a in range(3): - var ang := deg_to_rad(a * 120.0 + 40.0) - _chair(Vector3(tx + sin(ang) * 0.95, 0.0, tz + cos(ang) * 0.95), ang + PI) - -func _dressing() -> void: - _glb(P + "office-plant.glb", Vector3(2.9, 0.0, 6.4)) - _glb(P + "office-plant.glb", Vector3(-0.9, 0.0, -3.6)) - # wall clock, because every office has one you stare at - _slab(Vector3(0.42, 0.42, 0.05), Vector3(-3.4, 2.15, Z_FRONT - 0.02), _m_ceil) - _slab(Vector3(0.48, 0.48, 0.03), Vector3(-3.4, 2.15, Z_FRONT - 0.01), _m_trim) - -## Where the player should start: just inside the entrance, looking into the bullpen. -func spawn_point() -> Vector3: - return Vector3(-8.6, 0.0, 7.2) - -func spawn_yaw() -> float: - return deg_to_rad(196.0) diff --git a/game/scripts/Office.gd.uid b/game/scripts/Office.gd.uid deleted file mode 100644 index 5794565..0000000 --- a/game/scripts/Office.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://btc0q0j2wj0bs diff --git a/game/scripts/Tasks.gd b/game/scripts/Tasks.gd index 588368c..f56d6b0 100644 --- a/game/scripts/Tasks.gd +++ b/game/scripts/Tasks.gd @@ -68,6 +68,12 @@ func setup(player: Node3D, r: Rage) -> void: func register(kind: int, pos: Vector3, label: String) -> void: _stations.append({"kind": kind, "pos": pos, "label": label}) +## Swapping sites invalidates every station position. +func clear_stations() -> void: + _stations.clear() + _near = {} + phase = Phase.IDLE + func is_working() -> bool: return phase != Phase.IDLE and phase != Phase.DONE diff --git a/tools/gen_level_props.py b/tools/gen_level_props.py new file mode 100644 index 0000000..2ee0ce5 --- /dev/null +++ b/tools/gen_level_props.py @@ -0,0 +1,404 @@ +"""Second batch of procedural props: the ones the Gauntlet and the three new workplaces +need that a box can't fake. + +Same rule as tools/gen_office_props.py — if it IS a box (counters, partitions, tables, +whiteboards) it gets built in code by the level builder. This file is only for silhouettes. + + /Applications/Blender.app/Contents/MacOS/Blender --background \ + --python tools/gen_level_props.py -- [--render] + +Convention: glTF Y-up, 1 unit = 1 m, ORIGIN AT FLOOR-CENTRE, so a level can drop a prop +at a floor position with no offset maths. In Blender that means authoring Z-up with the +base on z = 0. + +The cabinet is the exception and the important one: it exports as a CARCASS plus four +separate DRAWER meshes, each with its own origin at its own floor-centre, so the Gauntlet +can pull a drawer out as its own rigid body and spill the files inside it. +""" + +import bpy +import math +import os +import sys +from mathutils import Vector, Matrix + +OUT_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "game", "assets", "store") +PREVIEW_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "previews") +RENDER = "--render" in sys.argv + + +def reset(): + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def mat(name, color, rough=0.6, metal=0.0, alpha=1.0): + m = bpy.data.materials.new(name) + m.use_nodes = True + b = m.node_tree.nodes["Principled BSDF"] + b.inputs["Base Color"].default_value = (color[0], color[1], color[2], alpha) + b.inputs["Roughness"].default_value = rough + b.inputs["Metallic"].default_value = metal + if alpha < 1.0: + b.inputs["Alpha"].default_value = alpha + m.blend_method = 'BLEND' + return m + + +def assign(o, m): + o.data.materials.clear() + o.data.materials.append(m) + return o + + +def box(size, loc=(0, 0, 0), rot=(0, 0, 0)): + bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot) + o = bpy.context.active_object + o.scale = (size[0], size[1], size[2]) + bpy.ops.object.transform_apply(scale=True) + return o + + +def cyl(r, depth, loc=(0, 0, 0), rot=(0, 0, 0), verts=16, r2=None): + if r2 is None: + bpy.ops.mesh.primitive_cylinder_add(radius=r, depth=depth, vertices=verts, + location=loc, rotation=rot) + else: + bpy.ops.mesh.primitive_cone_add(radius1=r, radius2=r2, depth=depth, + vertices=verts, location=loc, rotation=rot) + return bpy.context.active_object + + +def bevel(o, width=0.008, segments=2): + m = o.modifiers.new("bevel", "BEVEL") + m.width = width + m.segments = segments + m.limit_method = 'ANGLE' + m.angle_limit = math.radians(40) + bpy.context.view_layer.objects.active = o + try: + bpy.ops.object.modifier_apply(modifier=m.name) + except Exception: + o.modifiers.remove(m) + return o + + +def shade_smooth(o, angle=34.0): + bpy.context.view_layer.objects.active = o + o.select_set(True) + try: + bpy.ops.object.shade_auto_smooth(angle=math.radians(angle)) + except Exception: + bpy.ops.object.shade_smooth() + o.select_set(False) + + +# ---------------------------------------------------------------- props + +def p_cabinet_carcass(M): + """Four-drawer cabinet with the drawer OPENINGS empty — drawers ship separately.""" + parts = [] + W, D, H = 0.46, 0.62, 1.32 + T = 0.022 + for s in (-1, 1): # sides + parts.append(assign(box((T, D, H), loc=(s * (W / 2 - T / 2), 0, H / 2)), M["cab"])) + parts.append(assign(box((W, T, H), loc=(0, D / 2 - T / 2, H / 2)), M["cab"])) # back + parts.append(assign(box((W, D, T), loc=(0, 0, H - T / 2)), M["cab_top"])) # top + parts.append(assign(box((W, D, T), loc=(0, 0, T / 2)), M["cab"])) # base + for i in range(5): # shelf rails between drawers + z = 0.045 + i * 0.312 + parts.append(assign(box((W - 2 * T, D - T, 0.012), loc=(0, 0, z)), M["cab_dark"])) + plinth = box((W - 0.04, D - 0.04, 0.045), loc=(0, 0, 0.0225)) + parts.append(assign(plinth, M["cab_dark"])) + return parts + + +def p_cabinet_drawer(M): + """One drawer, origin at ITS OWN floor-centre so it can become its own rigid body.""" + parts = [] + W, D, H = 0.40, 0.58, 0.27 + T = 0.014 + parts.append(assign(box((W, T, H), loc=(0, -D / 2 + T / 2, H / 2)), M["cab"])) # front + parts.append(assign(box((W, T, H * 0.7), loc=(0, D / 2 - T / 2, H * 0.35)), M["cab"])) + for s in (-1, 1): + parts.append(assign(box((T, D, H * 0.8), loc=(s * (W / 2 - T / 2), 0, H * 0.4)), + M["cab"])) + parts.append(assign(box((W, D, T), loc=(0, 0, T / 2)), M["cab"])) + handle = box((0.17, 0.030, 0.032), loc=(0, -D / 2 - 0.012, H * 0.60)) + parts.append(assign(bevel(handle, 0.008), M["cab_dark"])) + label = box((0.10, 0.006, 0.036), loc=(0, -D / 2 - 0.004, H * 0.26)) + parts.append(assign(label, M["paper"])) + return parts + + +def p_file_folder(M): + """A manila folder. Lies flat; the Gauntlet stands them up inside a drawer.""" + parts = [] + body = box((0.24, 0.32, 0.014), loc=(0, 0, 0.007)) + parts.append(assign(bevel(body, 0.004), M["manila"])) + tab = box((0.075, 0.030, 0.012), loc=(-0.06, 0.172, 0.006)) + parts.append(assign(tab, M["manila"])) + for i in range(3): # paper peeking out + sheet = box((0.225, 0.30, 0.003), loc=(0.002, -0.004 + i * 0.003, 0.012 + i * 0.003)) + parts.append(assign(sheet, M["paper"])) + return parts + + +def p_desk_phone(M): + parts = [] + base = box((0.21, 0.24, 0.055), loc=(0, 0, 0.028), rot=(math.radians(-7), 0, 0)) + parts.append(assign(bevel(base, 0.010, 2), M["plastic_dark"])) + for r in range(4): # keypad + for c in range(3): + k = box((0.030, 0.024, 0.008), + loc=(-0.038 + c * 0.038, -0.052 + r * 0.032, 0.062 + r * 0.004)) + parts.append(assign(k, M["plastic_pale"])) + cradle = box((0.20, 0.075, 0.045), loc=(0, 0.086, 0.085)) + parts.append(assign(bevel(cradle, 0.012, 2), M["plastic_dark"])) + handset = box((0.055, 0.235, 0.048), loc=(0, 0.086, 0.128)) + parts.append(assign(bevel(handset, 0.020, 3), M["plastic_dark"])) + for s in (-1, 1): + ear = box((0.070, 0.062, 0.036), loc=(0, s * 0.088 + 0.086, 0.142)) + parts.append(assign(bevel(ear, 0.016, 3), M["plastic_dark"])) + # coiled cord + pos = Vector((-0.10, 0.02, 0.10)) + for i in range(9): + a = i * 1.35 + nxt = pos + Vector((math.cos(a) * 0.018 - 0.006, math.sin(a) * 0.018, -0.010)) + s = _span(0.006, pos, nxt) + if s: + parts.append(assign(s, M["plastic_dark"])) + pos = nxt + return parts + + +def p_guillotine(M): + """Paper trimmer. The alignment guide is 2 mm off and always has been.""" + parts = [] + base = box((0.42, 0.46, 0.030), loc=(0, 0, 0.015)) + parts.append(assign(bevel(base, 0.006), M["board"])) + grid = box((0.34, 0.38, 0.002), loc=(-0.02, 0, 0.031)) + parts.append(assign(grid, M["paper"])) + rail = box((0.030, 0.46, 0.028), loc=(0.196, 0, 0.044)) + parts.append(assign(bevel(rail, 0.005), M["steel"])) + guide = box((0.30, 0.020, 0.026), loc=(-0.05, -0.16, 0.043)) + parts.append(assign(guide, M["steel"])) + arm = box((0.022, 0.44, 0.055), loc=(0.185, 0.02, 0.10), rot=(math.radians(16), 0, 0)) + parts.append(assign(bevel(arm, 0.005), M["steel"])) + blade = box((0.016, 0.40, 0.075), loc=(0.170, 0.02, 0.085), rot=(math.radians(16), 0, 0)) + parts.append(assign(blade, M["blade"])) + grip = cyl(0.018, 0.10, loc=(0.185, 0.235, 0.175), rot=(math.radians(16), 0, 0), verts=12) + parts.append(assign(grip, M["plastic_dark"])) + return parts + + +def p_shredder(M): + """The Gauntlet's perfect tool, if you work out that's what it is.""" + parts = [] + bin_ = box((0.36, 0.30, 0.44), loc=(0, 0, 0.22)) + parts.append(assign(bevel(bin_, 0.012, 2), M["bin_grey"])) + win = box((0.22, 0.006, 0.26), loc=(0, -0.153, 0.24)) + parts.append(assign(win, M["glass_dark"])) + head = box((0.40, 0.34, 0.13), loc=(0, 0, 0.505)) + parts.append(assign(bevel(head, 0.014, 2), M["plastic_dark"])) + slot = box((0.30, 0.020, 0.012), loc=(0, 0, 0.572)) + parts.append(assign(slot, M["black"])) + for i in range(3): + led = box((0.016, 0.014, 0.010), loc=(0.13, -0.15 + i * 0.02, 0.545)) + parts.append(assign(led, M["led"] if i == 0 else M["black"])) + return parts + + +def p_server_rack(M): + parts = [] + W, D, H = 0.62, 0.90, 1.90 + for s in (-1, 1): + parts.append(assign(box((0.030, D, H), loc=(s * (W / 2), 0, H / 2)), M["black"])) + parts.append(assign(box((W, 0.030, H), loc=(0, D / 2, H / 2)), M["black"])) + parts.append(assign(box((W, D, 0.030), loc=(0, 0, H)), M["black"])) + parts.append(assign(box((W, D, 0.030), loc=(0, 0, 0.015)), M["black"])) + for i in range(9): # blade servers + z = 0.14 + i * 0.19 + u = box((W - 0.07, D - 0.10, 0.155), loc=(0, 0.01, z)) + parts.append(assign(bevel(u, 0.005), M["server"])) + for j in range(4): + led = box((0.014, 0.010, 0.012), loc=(-0.20 + j * 0.05, -D / 2 + 0.06, z + 0.05)) + parts.append(assign(led, M["led"] if (i + j) % 3 else M["led_amber"])) + vent = box((0.22, 0.008, 0.10), loc=(0.09, -D / 2 + 0.055, z)) + parts.append(assign(vent, M["black"])) + return parts + + +def p_sofa(M): + parts = [] + seat = box((1.85, 0.86, 0.30), loc=(0, 0, 0.34)) + parts.append(assign(bevel(seat, 0.045, 3), M["fabric"])) + back = box((1.85, 0.24, 0.52), loc=(0, 0.31, 0.62), rot=(math.radians(-8), 0, 0)) + parts.append(assign(bevel(back, 0.050, 3), M["fabric"])) + for s in (-1, 1): + arm = box((0.20, 0.86, 0.28), loc=(s * 0.825, 0, 0.56)) + parts.append(assign(bevel(arm, 0.055, 3), M["fabric"])) + for i in range(2): + cush = box((0.84, 0.76, 0.14), loc=(-0.44 + i * 0.88, -0.02, 0.545)) + parts.append(assign(bevel(cush, 0.050, 3), M["fabric_light"])) + for sx in (-1, 1): + for sy in (-1, 1): + parts.append(assign(cyl(0.030, 0.19, loc=(sx * 0.78, sy * 0.34, 0.095), verts=10), + M["wood"])) + return parts + + +def p_bin(M): + parts = [] + body = cyl(0.15, 0.36, loc=(0, 0, 0.18), r2=0.175, verts=18) + parts.append(assign(body, M["bin_grey"])) + rim = cyl(0.182, 0.028, loc=(0, 0, 0.352), verts=18) + parts.append(assign(rim, M["plastic_dark"])) + for i in range(5): # crumpled paper spilling out + a = i * 1.3 + ball = box((0.075, 0.075, 0.065), + loc=(math.cos(a) * 0.06, math.sin(a) * 0.06, 0.36 + (i % 2) * 0.05), + rot=(a, a * 0.7, a * 0.3)) + parts.append(assign(bevel(ball, 0.022, 2), M["paper"])) + return parts + + +def p_stapler(M): + parts = [] + base = box((0.052, 0.175, 0.022), loc=(0, 0, 0.011)) + parts.append(assign(bevel(base, 0.008, 2), M["stapler_red"])) + anvil = box((0.040, 0.040, 0.006), loc=(0, -0.058, 0.024)) + parts.append(assign(anvil, M["steel"])) + top = box((0.048, 0.168, 0.030), loc=(0, 0.006, 0.041), rot=(math.radians(4), 0, 0)) + parts.append(assign(bevel(top, 0.010, 2), M["stapler_red"])) + hinge = cyl(0.012, 0.052, loc=(0, 0.082, 0.030), rot=(0, math.radians(90), 0), verts=10) + parts.append(assign(hinge, M["steel"])) + return parts + + +def _span(r, p0, p1, verts=8): + p0, p1 = Vector(p0), Vector(p1) + d = p1 - p0 + n = d.length + if n < 1e-6: + return None + o = cyl(r, n, verts=verts) + quat = Vector((0, 0, 1)).rotation_difference(d.normalized()) + o.matrix_world = Matrix.Translation(p0 + d * 0.5) @ quat.to_matrix().to_4x4() + bpy.ops.object.transform_apply(location=True, rotation=True) + return o + + +PROPS = { + "cabinet-carcass": p_cabinet_carcass, + "cabinet-drawer": p_cabinet_drawer, + "file-folder": p_file_folder, + "desk-phone": p_desk_phone, + "guillotine": p_guillotine, + "shredder": p_shredder, + "server-rack": p_server_rack, + "sofa": p_sofa, + "wastebin": p_bin, + "stapler": p_stapler, +} + + +def materials(): + return { + "cab": mat("cab", (0.62, 0.63, 0.64), rough=0.45, metal=0.6), + "cab_top": mat("cab_top", (0.58, 0.59, 0.60), rough=0.42, metal=0.6), + "cab_dark": mat("cab_dark", (0.22, 0.23, 0.25), rough=0.5), + "manila": mat("manila", (0.78, 0.66, 0.40), rough=0.95), + "paper": mat("paper", (0.93, 0.92, 0.88), rough=1.0), + "plastic_dark": mat("plastic_dark", (0.10, 0.10, 0.11), rough=0.55), + "plastic_pale": mat("plastic_pale", (0.80, 0.79, 0.74), rough=0.6), + "black": mat("black", (0.05, 0.05, 0.06), rough=0.6), + "steel": mat("steel", (0.66, 0.67, 0.70), rough=0.32, metal=1.0), + "blade": mat("blade", (0.86, 0.87, 0.90), rough=0.14, metal=1.0), + "board": mat("board", (0.34, 0.24, 0.16), rough=0.75), + "bin_grey": mat("bin_grey", (0.30, 0.31, 0.33), rough=0.7), + "glass_dark": mat("glass_dark", (0.12, 0.14, 0.15), rough=0.2), + "led": mat("led", (0.15, 0.85, 0.30), rough=0.3), + "led_amber": mat("led_amber", (0.90, 0.55, 0.10), rough=0.3), + "server": mat("server", (0.24, 0.25, 0.28), rough=0.5, metal=0.4), + "fabric": mat("fabric", (0.30, 0.33, 0.38), rough=0.98), + "fabric_light": mat("fabric_light", (0.38, 0.41, 0.46), rough=0.98), + "wood": mat("wood", (0.36, 0.24, 0.14), rough=0.6), + "stapler_red": mat("stapler_red", (0.52, 0.08, 0.10), rough=0.4), + } + + +def export(objs, name): + objs = [o for o in objs if o is not None] + bpy.ops.object.select_all(action='DESELECT') + for o in objs: + o.select_set(True) + bpy.context.view_layer.objects.active = objs[0] + path = os.path.join(OUT_DIR, name + ".glb") + bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', + use_selection=True, export_apply=True, export_yup=True) + print("[props] %-20s -> %s" % (name, os.path.basename(path))) + bpy.ops.object.select_all(action='DESELECT') + + +def render_preview(name): + scene = bpy.context.scene + engines = scene.render.bl_rna.properties['engine'].enum_items.keys() + for want in ('BLENDER_EEVEE_NEXT', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'): + if want in engines: + scene.render.engine = want + break + scene.render.resolution_x = 520 + scene.render.resolution_y = 520 + if scene.world is None: + scene.world = bpy.data.worlds.new("preview") + scene.world.use_nodes = True + bg = scene.world.node_tree.nodes.get("Background") + if bg: + bg.inputs[0].default_value = (0.07, 0.07, 0.08, 1.0) + lo = Vector((1e9,) * 3) + hi = Vector((-1e9,) * 3) + for o in scene.objects: + if o.type != 'MESH': + continue + for c in o.bound_box: + w = o.matrix_world @ Vector(c) + lo = Vector((min(lo.x, w.x), min(lo.y, w.y), min(lo.z, w.z))) + hi = Vector((max(hi.x, w.x), max(hi.y, w.y), max(hi.z, w.z))) + centre = (lo + hi) * 0.5 + radius = max((hi - lo).length * 0.5, 0.05) + dist = radius * 2.9 + d = Vector((0.66, -0.72, 0.30)).normalized() + bpy.ops.object.camera_add(location=tuple(centre + d * dist)) + cam = bpy.context.active_object + cam.data.lens = 58 + cam.rotation_mode = 'QUATERNION' + cam.rotation_quaternion = (-d).to_track_quat('-Z', 'Y') + scene.camera = cam + for off, e, s in ((Vector((1.0, -1.0, 1.1)), 58.0, 1.2), + (Vector((-1.1, -0.5, 0.3)), 17.0, 1.8)): + bpy.ops.object.light_add(type='AREA', location=tuple(centre + off * dist)) + L = bpy.context.active_object + L.data.energy = e * (dist ** 2) + L.data.size = s * radius + scene.render.filepath = os.path.join(PREVIEW_DIR, name + ".png") + bpy.ops.render.render(write_still=True) + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + os.makedirs(PREVIEW_DIR, exist_ok=True) + for name, builder in PROPS.items(): + reset() + M = materials() + parts = builder(M) + for p in parts: + shade_smooth(p, 34.0) + export(parts, name) + if RENDER: + render_preview(name) + print("[props] done -> %s" % OUT_DIR) + + +main()