LANE7: four workplaces, built from data
Office.gd is gone. Floorplan.gd consumes a spec Dictionary — bounds, palette, walls
with doors and glazing styles, window runs with mullions and blinds, ceiling style,
light grid and style, slabs, static props, desk clusters, chairs, level-specific
smashables and spawn — and Levels.gd holds four of them.
Making this data rather than four subclasses means a site is authorable in minutes,
levels can be diffed, and a real generator can later emit the same structure (a BSP
split of the footprint -> rooms -> doors on shared walls -> fittings by room type).
That was the actual answer to "can we use this git to generate plans": the repo John
found is OpenSCAD SVG->STL for 3D printing, with no generation, no plan parsing, no
room polygons and GPL-3.0, so it can't help — but Office.gd was already most of a
parametric plan builder, and lifting its numbers out is the real path.
The four sites are deliberately not reskins; they differ in the three things a player
actually reads — palette, light, and what the walls are made of:
SCRANTON magnolia, grey carpet, drop ceiling, fluorescent troffers, daylight
down one glazed wall. The baseline.
PAWNEE civic beige and blue-grey, low partitions everywhere, a public
counter, pinboards. Municipal and over-partitioned.
THE INCUBATOR timber floor, white walls, 3 m ceiling, PENDANT lights, glass wall
onto a pool. Nobody has an office; they work at a dining table.
SUB-LEVEL 4 concrete, NO windows at all, exposed services, bare strip lights, a
wall of server racks. The light is green and everything is junk.
L cycles sites in-game; _load_level tears down the shell, rebuilds, re-registers task
stations and re-arms the shift.
tools/gen_level_props.py adds ten more procedural props. The filing cabinet is the
important one: it exports as a CARCASS plus a separate DRAWER, each with its own
floor-centre origin, so the Gauntlet can pull a drawer out as its own rigid body and
spill the files. Also file folder, desk phone, guillotine, shredder, server rack,
sofa, wastebin, stapler.
dev/probe_levels.gd builds every level and reports residual motion after a full
second. All four read 0.00 m/s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dd905d908d
commit
4bcd7b49d9
BIN
game/assets/store/cabinet-carcass.glb
Normal file
BIN
game/assets/store/cabinet-carcass.glb
Normal file
Binary file not shown.
45
game/assets/store/cabinet-carcass.glb.import
Normal file
45
game/assets/store/cabinet-carcass.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/cabinet-drawer.glb
Normal file
BIN
game/assets/store/cabinet-drawer.glb
Normal file
Binary file not shown.
45
game/assets/store/cabinet-drawer.glb.import
Normal file
45
game/assets/store/cabinet-drawer.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/desk-phone.glb
Normal file
BIN
game/assets/store/desk-phone.glb
Normal file
Binary file not shown.
45
game/assets/store/desk-phone.glb.import
Normal file
45
game/assets/store/desk-phone.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/file-folder.glb
Normal file
BIN
game/assets/store/file-folder.glb
Normal file
Binary file not shown.
45
game/assets/store/file-folder.glb.import
Normal file
45
game/assets/store/file-folder.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/guillotine.glb
Normal file
BIN
game/assets/store/guillotine.glb
Normal file
Binary file not shown.
45
game/assets/store/guillotine.glb.import
Normal file
45
game/assets/store/guillotine.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/server-rack.glb
Normal file
BIN
game/assets/store/server-rack.glb
Normal file
Binary file not shown.
45
game/assets/store/server-rack.glb.import
Normal file
45
game/assets/store/server-rack.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/shredder.glb
Normal file
BIN
game/assets/store/shredder.glb
Normal file
Binary file not shown.
45
game/assets/store/shredder.glb.import
Normal file
45
game/assets/store/shredder.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/sofa.glb
Normal file
BIN
game/assets/store/sofa.glb
Normal file
Binary file not shown.
45
game/assets/store/sofa.glb.import
Normal file
45
game/assets/store/sofa.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/stapler.glb
Normal file
BIN
game/assets/store/stapler.glb
Normal file
Binary file not shown.
45
game/assets/store/stapler.glb.import
Normal file
45
game/assets/store/stapler.glb.import
Normal file
@ -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
|
||||||
BIN
game/assets/store/wastebin.glb
Normal file
BIN
game/assets/store/wastebin.glb
Normal file
Binary file not shown.
45
game/assets/store/wastebin.glb.import
Normal file
45
game/assets/store/wastebin.glb.import
Normal file
@ -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
|
||||||
@ -48,85 +48,33 @@ func _find_player(n: Node) -> Player:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
func _build_script() -> void:
|
func _build_script() -> void:
|
||||||
# Office floor plan (see Office.gd): spawn at reception (-8.6, 6.6) looking into the
|
# A lap of all four sites. Each gets a settle beat (nothing must move on its own)
|
||||||
# bullpen; desk clusters at (-4.6,-1.4) (-4.6,2.2) (-0.4,-1.4) (-0.4,2.2); copier
|
# and two vantage points, so the palette, lighting and layout can all be read.
|
||||||
# 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).
|
|
||||||
_script = []
|
_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:
|
func _motion(verbose := false) -> float:
|
||||||
var total := 0.0
|
var total := 0.0
|
||||||
var seen := {}
|
var seen := {}
|
||||||
@ -215,6 +163,10 @@ func _do(act: String) -> void:
|
|||||||
if act.begins_with("weapon:"):
|
if act.begins_with("weapon:"):
|
||||||
_player.select(int(act.substr(7)))
|
_player.select(int(act.substr(7)))
|
||||||
return
|
return
|
||||||
|
if act.begins_with("level:"):
|
||||||
|
if _main.has_method("_load_level"):
|
||||||
|
_main._load_level(act.substr(6))
|
||||||
|
return
|
||||||
if act.begins_with("rage:"):
|
if act.begins_with("rage:"):
|
||||||
var r = _main.get("_rage")
|
var r = _main.get("_rage")
|
||||||
if r != null:
|
if r != null:
|
||||||
|
|||||||
49
game/dev/probe_levels.gd
Normal file
49
game/dev/probe_levels.gd
Normal file
@ -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
|
||||||
458
game/scripts/Floorplan.gd
Normal file
458
game/scripts/Floorplan.gd
Normal file
@ -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", ""))
|
||||||
1
game/scripts/Floorplan.gd.uid
Normal file
1
game/scripts/Floorplan.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://d0oexxkrw0hln
|
||||||
428
game/scripts/Levels.gd
Normal file
428
game/scripts/Levels.gd
Normal file
@ -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)},
|
||||||
|
}
|
||||||
1
game/scripts/Levels.gd.uid
Normal file
1
game/scripts/Levels.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://c0ic0hj3e0do0
|
||||||
@ -30,11 +30,13 @@ var _juice: Juice
|
|||||||
var _player: Player # kept so GameMode can reach player.grab
|
var _player: Player # kept so GameMode can reach player.grab
|
||||||
var _records: Array[Record] = [] # the live records, handed to GameMode each round
|
var _records: Array[Record] = [] # the live records, handed to GameMode each round
|
||||||
var _game_mode: GameMode
|
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: Rage # the tilt meter — see Rage.gd
|
||||||
var _rage_hud: RageOverlay
|
var _rage_hud: RageOverlay
|
||||||
var _tasks: Tasks
|
var _tasks: Tasks
|
||||||
var _shift: CubicleHell
|
var _shift: CubicleHell
|
||||||
|
var _env_node: WorldEnvironment
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
randomize()
|
randomize()
|
||||||
@ -74,34 +76,35 @@ func _ready() -> void:
|
|||||||
## Every desk is a spreadsheet you have to fill in; the copier is the copier.
|
## Every desk is a spreadsheet you have to fill in; the copier is the copier.
|
||||||
func _register_stations() -> void:
|
func _register_stations() -> void:
|
||||||
var i := 1
|
var i := 1
|
||||||
for t in _office.desk_spots():
|
for t in _plan.desk_spots():
|
||||||
_tasks.register(Tasks.Kind.SPREADSHEET, (t as Transform3D).origin,
|
_tasks.register(Tasks.Kind.SPREADSHEET, (t as Transform3D).origin,
|
||||||
"work at desk %d" % i)
|
"work at desk %d" % i)
|
||||||
i += 1
|
i += 1
|
||||||
_tasks.register(Tasks.Kind.COPIER, _office.copier_spot().origin,
|
if _plan.has_copier():
|
||||||
"use the photocopier")
|
_tasks.register(Tasks.Kind.COPIER, _plan.copier_spot().origin,
|
||||||
|
"use the photocopier")
|
||||||
|
|
||||||
# ---------------------------------------------------------------- world / camera
|
# ---------------------------------------------------------------- world / camera
|
||||||
func _setup_world() -> void:
|
func _setup_world() -> void:
|
||||||
# The level shell: floor, walls, ceiling, windows, fittings and ALL the lighting.
|
# The level shell: floor, walls, ceiling, windows, fittings and ALL the lighting.
|
||||||
# It owns the environment too, because interior lighting and the ambient/tonemap
|
# It owns the environment too, because interior lighting and the ambient/tonemap
|
||||||
# settings are one decision, not two.
|
# settings are one decision, not two.
|
||||||
_office = Office.new()
|
_plan = Floorplan.new()
|
||||||
add_child(_office)
|
add_child(_plan)
|
||||||
_office.build()
|
_plan.build(Levels.get_level(_level_id))
|
||||||
|
|
||||||
var we := WorldEnvironment.new()
|
_env_node = WorldEnvironment.new()
|
||||||
var env := Environment.new()
|
var env := Environment.new()
|
||||||
_office.configure_environment(env)
|
_plan.configure_environment(env)
|
||||||
we.environment = env
|
_env_node.environment = env
|
||||||
add_child(we)
|
add_child(_env_node)
|
||||||
|
|
||||||
# First-person player. add_child() runs its _ready() synchronously, so player.camera
|
# First-person player. add_child() runs its _ready() synchronously, so player.camera
|
||||||
# and player.grab exist on the next line; we reuse them.
|
# and player.grab exist on the next line; we reuse them.
|
||||||
_player = Player.new()
|
_player = Player.new()
|
||||||
add_child(_player)
|
add_child(_player)
|
||||||
_player.global_position = _office.spawn_point()
|
_player.global_position = _plan.spawn_point()
|
||||||
_player.rotation.y = _office.spawn_yaw()
|
_player.rotation.y = _plan.spawn_yaw()
|
||||||
_cam = _player.camera
|
_cam = _player.camera
|
||||||
|
|
||||||
_world = Node3D.new()
|
_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 BOX_GLB := "res://assets/store/cardboard-box.glb"
|
||||||
const TURNTABLE_GLB := "res://assets/store/turntable.glb"
|
const TURNTABLE_GLB := "res://assets/store/turntable.glb"
|
||||||
const CHAIR_GLB := "res://assets/store/office-chair.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
|
## 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.
|
## 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
|
_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 ---
|
# --- 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():
|
for i in spots.size():
|
||||||
var t: Transform3D = spots[i]
|
var t: Transform3D = spots[i]
|
||||||
var xz := Vector2(t.origin.x, t.origin.z)
|
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
|
# 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).
|
# pushed out far enough to clear them, inside each other across the aisle).
|
||||||
var depth: float = (desk["aabb"] as AABB).size.z
|
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):
|
if is_zero_approx(back):
|
||||||
back = 1.0
|
back = 1.0
|
||||||
_glb_piece(CHAIR_GLB, "plastic", xz + Vector2(0.0, back * (depth * 0.5 + 0.42)),
|
_glb_piece(CHAIR_GLB, "plastic", xz + Vector2(0.0, back * (depth * 0.5 + 0.42)),
|
||||||
false, 0.0, t.basis.get_euler().y)
|
false, 0.0, t.basis.get_euler().y)
|
||||||
|
|
||||||
# --- the copier alcove: the PRINTER is the level boss, on its own desk ---
|
# --- the copier alcove: the PRINTER is the level boss, on its own desk ---
|
||||||
var cs := _office.copier_spot()
|
if _plan.has_copier():
|
||||||
var cxz := Vector2(cs.origin.x, cs.origin.z)
|
var cs := _plan.copier_spot()
|
||||||
var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y)
|
var cxz := Vector2(cs.origin.x, cs.origin.z)
|
||||||
var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"])
|
var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y)
|
||||||
var boss := printer["piece"] as Smashable
|
var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"])
|
||||||
boss.is_boss = true
|
var boss := printer["piece"] as Smashable
|
||||||
boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge
|
boss.is_boss = true
|
||||||
boss.mass = 8.0 # heavy, so hits rock it but don't shove it off the desk
|
boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge
|
||||||
(cdesk["piece"] as Smashable).supports.append(printer["piece"])
|
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 ---
|
# --- everything else this site contains, straight from its spec ---
|
||||||
for z in [-1.6, -0.9, -0.2]:
|
for e in _plan.smashables():
|
||||||
_glb_piece(CABINET_GLB, "steel", Vector2(4.5, z), false)
|
var d: Dictionary = e
|
||||||
_glb_piece(COOLER_GLB, "steel", Vector2(-6.2, 3.4), false)
|
_glb_piece(P + String(d["glb"]) + ".glb", String(d.get("kind", "wood")),
|
||||||
_glb_piece(COOLER_GLB, "steel", Vector2(5.9, -0.6), false)
|
d["at"], bool(d.get("frozen", false)), float(d.get("sit_on", 0.0)),
|
||||||
# Cartons go on OPEN floor only. Tucking them under desks or against the conference
|
float(d.get("yaw", 0.0)))
|
||||||
# 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)
|
|
||||||
|
|
||||||
# --- the warehouse corner: shelving, crates, and somebody's record collection.
|
# --- crates of records, where the site has any. Records stand side by side ON the
|
||||||
# --- This is where the grab/slide/throw ritual still lives.
|
# --- crate lid, offset by more than a sleeve is thick: they used to be stacked 3 cm
|
||||||
_glb_piece(RACK_GLB, "wood", Vector2(9.4, -6.6), true)
|
# --- apart vertically while being 30 cm TALL, so Jolt resolved 27 cm of
|
||||||
_glb_piece(RACK_GLB, "wood", Vector2(9.4, -4.3), true)
|
# --- interpenetration explosively on frame one.
|
||||||
_glb_piece(TURNTABLE_GLB, "wood", Vector2(6.9, -5.9), false)
|
for spot in _plan.spec.get("records", []):
|
||||||
for spot in [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)]:
|
|
||||||
var crate := _glb_piece(CRATE_GLB, "wood", spot, false)
|
var crate := _glb_piece(CRATE_GLB, "wood", spot, false)
|
||||||
var crate_top: float = crate["top"]
|
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):
|
for i in range(3):
|
||||||
var rec := _place_record(spot + Vector2(randf_range(-0.02, 0.02),
|
var rec := _place_record(spot + Vector2(randf_range(-0.02, 0.02),
|
||||||
(float(i) - 1.0) * 0.045), crate_top)
|
(float(i) - 1.0) * 0.045), crate_top)
|
||||||
(crate["piece"] as Smashable).supports.append(rec)
|
(crate["piece"] as Smashable).supports.append(rec)
|
||||||
|
|
||||||
# --- chairs: the best thing in an office to send across the room ---
|
# --- 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,
|
_glb_piece(CHAIR_GLB, "plastic", Vector2(t.origin.x, t.origin.z), false, 0.0,
|
||||||
t.basis.get_euler().y)
|
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
|
# 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
|
# already 8 m away by the time this runs, which points at nothing useful
|
||||||
var at = b.get_meta("spawn_pos", b.global_position)
|
var at = b.get_meta("spawn_pos", b.global_position)
|
||||||
push_warning("[spawn] %s placed overlapping something at %s (%.0f m/s) — clamped" % [
|
push_warning("[spawn] %s (%s) placed overlapping something at %s (%.0f m/s) — clamped" % [
|
||||||
b.name, (at as Vector3).snappedf(0.01), v])
|
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
|
b.linear_velocity = b.linear_velocity.normalized() * SPAWN_MAX_SPEED
|
||||||
if b.angular_velocity.length() > SPAWN_MAX_SPIN:
|
if b.angular_velocity.length() > SPAWN_MAX_SPIN:
|
||||||
b.angular_velocity = b.angular_velocity.normalized() * 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:
|
KEY_H:
|
||||||
if _hud != null:
|
if _hud != null:
|
||||||
_hud.cycle()
|
_hud.cycle()
|
||||||
|
KEY_L:
|
||||||
|
_load_level(Levels.next_id(_level_id))
|
||||||
KEY_F:
|
KEY_F:
|
||||||
if _game_mode != null:
|
if _game_mode != null:
|
||||||
_game_mode.report() # report the disc you're holding (FindMisfiled)
|
_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.
|
# The Day-0 stress gate: rain N bodies, watch the FPS counter hold.
|
||||||
func _rain(n: int) -> void:
|
func _rain(n: int) -> void:
|
||||||
for i in n:
|
for i in n:
|
||||||
@ -494,6 +534,10 @@ func _reset() -> void:
|
|||||||
_tally.clear()
|
_tally.clear()
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
_populate()
|
_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
|
# re-arm whatever mode we're in against the fresh set of records
|
||||||
if _game_mode != null:
|
if _game_mode != null:
|
||||||
if _game_mode.mode == GameMode.Mode.FIND_MISFILED:
|
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:
|
if _hud != null and w != null:
|
||||||
_hud.toast(w.display_name)
|
_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:
|
func _process(_dt: float) -> void:
|
||||||
if _hud == null:
|
if _hud == 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)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
uid://btc0q0j2wj0bs
|
|
||||||
@ -68,6 +68,12 @@ func setup(player: Node3D, r: Rage) -> void:
|
|||||||
func register(kind: int, pos: Vector3, label: String) -> void:
|
func register(kind: int, pos: Vector3, label: String) -> void:
|
||||||
_stations.append({"kind": kind, "pos": pos, "label": label})
|
_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:
|
func is_working() -> bool:
|
||||||
return phase != Phase.IDLE and phase != Phase.DONE
|
return phase != Phase.IDLE and phase != Phase.DONE
|
||||||
|
|
||||||
|
|||||||
404
tools/gen_level_props.py
Normal file
404
tools/gen_level_props.py
Normal file
@ -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()
|
||||||
Loading…
Reference in New Issue
Block a user