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
|
||||
|
||||
func _build_script() -> void:
|
||||
# Office floor plan (see Office.gd): spawn at reception (-8.6, 6.6) looking into the
|
||||
# bullpen; desk clusters at (-4.6,-1.4) (-4.6,2.2) (-0.4,-1.4) (-0.4,2.2); copier
|
||||
# alcove at (3.0,-0.3); break room x>5.4 z>-1.2; conference and manager along z<-4.4;
|
||||
# warehouse corner around (7,-6).
|
||||
# A lap of all four sites. Each gets a settle beat (nothing must move on its own)
|
||||
# and two vantage points, so the palette, lighting and layout can all be read.
|
||||
_script = []
|
||||
var tours := {
|
||||
"scranton": [[Vector3(-8.6, 0, 6.4), Vector3(-4.0, 1.1, 0.0)],
|
||||
[Vector3(-2.6, 0, 3.4), Vector3(-0.4, 1.0, -1.0)],
|
||||
[Vector3(1.6, 0, -2.0), Vector3(8.0, 1.3, -6.5)]],
|
||||
"pawnee": [[Vector3(-8.0, 0, 6.6), Vector3(-2.0, 1.1, 1.0)],
|
||||
[Vector3(-1.0, 0, 2.6), Vector3(-2.6, 1.2, -5.2)],
|
||||
[Vector3(4.0, 0, 0.6), Vector3(6.5, 1.2, -3.2)]],
|
||||
"house": [[Vector3(-1.0, 0, 7.0), Vector3(-4.4, 1.2, 0.6)],
|
||||
[Vector3(0.5, 0, 2.0), Vector3(6.0, 1.3, 6.0)],
|
||||
[Vector3(-6.0, 0, -2.0), Vector3(4.0, 1.4, -6.0)]],
|
||||
"basement": [[Vector3(1.0, 0, -5.2), Vector3(-8.0, 1.3, -1.0)],
|
||||
[Vector3(-4.0, 0, -1.0), Vector3(-10.6, 1.4, -1.0)],
|
||||
[Vector3(4.0, 0, 1.0), Vector3(9.0, 1.3, 5.4)]],
|
||||
}
|
||||
for id in ["scranton", "pawnee", "house", "basement"]:
|
||||
var shots: Array = tours[id]
|
||||
_script.append({"dur": 0.4, "move_to": shots[0][0], "look_at": shots[0][1],
|
||||
"act": "level:" + id})
|
||||
_script.append({"dur": 2.2, "move_to": null, "look_at": null, "act": ""})
|
||||
for i in range(1, shots.size()):
|
||||
_script.append({"dur": 2.4, "move_to": shots[i][0], "look_at": shots[i][1],
|
||||
"act": ""})
|
||||
_script.append({"dur": 1.2, "move_to": null, "look_at": null, "act": ""})
|
||||
|
||||
# ---- ACT 0: SETTLE CHECK. Stand at the entrance and touch nothing. If anything
|
||||
# ---- moves in these five seconds, the level is falling over by itself again.
|
||||
_script += [
|
||||
{"dur": 2.5, "move_to": Vector3(-8.6, 0, 6.4), "look_at": Vector3(-4.0, 1.1, 0.0), "act": ""},
|
||||
{"dur": 2.5, "move_to": null, "look_at": null, "act": ""},
|
||||
]
|
||||
|
||||
# ---- ACT 0b: CUBICLE HELL. Sit at a desk, do the spreadsheet, get wound up until
|
||||
# ---- the meter snaps, then wreck the place until it drains.
|
||||
_script += [
|
||||
{"dur": 2.4, "move_to": Vector3(-4.6, 0, -0.55), "look_at": Vector3(-4.6, 1.0, -1.4), "act": ""},
|
||||
{"dur": 1.2, "move_to": null, "look_at": null, "act": "task"},
|
||||
{"dur": 1.6, "move_to": null, "look_at": null, "act": "type"},
|
||||
{"dur": 1.6, "move_to": null, "look_at": null, "act": "commit"},
|
||||
{"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"},
|
||||
{"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"},
|
||||
{"dur": 1.6, "move_to": null, "look_at": null, "act": "rage:0.35"},
|
||||
# SNAP — the vignette closes in, veins crawl, heartbeat goes. Now wreck things.
|
||||
{"dur": 1.0, "move_to": Vector3(-4.6, 0, 0.3), "look_at": Vector3(-4.6, 0.9, -0.7), "act": "weapon:2"},
|
||||
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.6, "move_to": Vector3(-2.6, 0, 4.8), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""},
|
||||
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"},
|
||||
]
|
||||
|
||||
# ---- ACT 1: a walk through the floor plan, so the rooms read.
|
||||
_script += [
|
||||
{"dur": 2.2, "move_to": Vector3(-8.4, 0, 2.0), "look_at": Vector3(-4.6, 1.1, 1.0), "act": ""},
|
||||
{"dur": 2.2, "move_to": Vector3(-2.6, 0, 3.4), "look_at": Vector3(-0.4, 1.0, 0.5), "act": ""},
|
||||
{"dur": 2.2, "move_to": Vector3(-3.0, 0, -3.0), "look_at": Vector3(-3.4, 1.4, -6.4), "act": ""},
|
||||
{"dur": 2.2, "move_to": Vector3(1.6, 0, -2.0), "look_at": Vector3(8.0, 1.3, -6.5), "act": ""},
|
||||
{"dur": 2.2, "move_to": Vector3(3.4, 0, 3.0), "look_at": Vector3(8.0, 1.2, 4.0), "act": ""},
|
||||
]
|
||||
|
||||
# ---- ACT 2: the loadout, held still so each grip can be inspected.
|
||||
for slot in range(6):
|
||||
_script.append({"dur": 0.5, "move_to": Vector3(-2.2, 0, 4.6),
|
||||
"look_at": Vector3(-2.0, 1.2, 0.0), "act": "weapon:%d" % slot})
|
||||
_script.append({"dur": 0.8, "move_to": null, "look_at": null, "act": ""})
|
||||
_script.append({"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"})
|
||||
|
||||
# ---- ACT 3: the matrix. Cutter shreds a carton, clanks off a filing cabinet,
|
||||
# ---- then the sledge takes the cabinet out.
|
||||
_script += [
|
||||
{"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:1"},
|
||||
{"dur": 2.0, "move_to": Vector3(-1.6, 0, 5.3), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""},
|
||||
{"dur": 0.5, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 2.0, "move_to": Vector3(3.1, 0, -0.9), "look_at": Vector3(4.5, 0.6, -0.9), "act": ""},
|
||||
{"dur": 0.4, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:4"},
|
||||
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.3, "move_to": null, "look_at": null, "act": "swing"},
|
||||
]
|
||||
|
||||
# ---- ACT 4: the printer boss, then a lap of the HUD styles.
|
||||
_script += [
|
||||
{"dur": 2.0, "move_to": Vector3(1.5, 0, -0.3), "look_at": Vector3(3.0, 0.95, -0.3), "act": ""},
|
||||
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 1.5, "move_to": null, "look_at": null, "act": "swing"},
|
||||
{"dur": 2.0, "move_to": Vector3(-1.0, 0, 5.6), "look_at": Vector3(-1.0, 1.2, -1.0), "act": "hud"},
|
||||
{"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"},
|
||||
{"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"},
|
||||
{"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"},
|
||||
]
|
||||
|
||||
## Total speed across every physics body in the level. During the settle act nobody has
|
||||
## touched anything, so this MUST fall to ~0 — that is the objective version of "is the
|
||||
## level quietly falling over on its own before the player does anything".
|
||||
func _motion(verbose := false) -> float:
|
||||
var total := 0.0
|
||||
var seen := {}
|
||||
@ -215,6 +163,10 @@ func _do(act: String) -> void:
|
||||
if act.begins_with("weapon:"):
|
||||
_player.select(int(act.substr(7)))
|
||||
return
|
||||
if act.begins_with("level:"):
|
||||
if _main.has_method("_load_level"):
|
||||
_main._load_level(act.substr(6))
|
||||
return
|
||||
if act.begins_with("rage:"):
|
||||
var r = _main.get("_rage")
|
||||
if r != null:
|
||||
|
||||
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 _records: Array[Record] = [] # the live records, handed to GameMode each round
|
||||
var _game_mode: GameMode
|
||||
var _office: Office # the level shell: walls, fittings, lighting
|
||||
var _plan: Floorplan # the level shell: walls, fittings, lighting — see Levels.gd
|
||||
var _level_id := "scranton"
|
||||
var _rage: Rage # the tilt meter — see Rage.gd
|
||||
var _rage_hud: RageOverlay
|
||||
var _tasks: Tasks
|
||||
var _shift: CubicleHell
|
||||
var _env_node: WorldEnvironment
|
||||
|
||||
func _ready() -> void:
|
||||
randomize()
|
||||
@ -74,34 +76,35 @@ func _ready() -> void:
|
||||
## Every desk is a spreadsheet you have to fill in; the copier is the copier.
|
||||
func _register_stations() -> void:
|
||||
var i := 1
|
||||
for t in _office.desk_spots():
|
||||
for t in _plan.desk_spots():
|
||||
_tasks.register(Tasks.Kind.SPREADSHEET, (t as Transform3D).origin,
|
||||
"work at desk %d" % i)
|
||||
i += 1
|
||||
_tasks.register(Tasks.Kind.COPIER, _office.copier_spot().origin,
|
||||
"use the photocopier")
|
||||
if _plan.has_copier():
|
||||
_tasks.register(Tasks.Kind.COPIER, _plan.copier_spot().origin,
|
||||
"use the photocopier")
|
||||
|
||||
# ---------------------------------------------------------------- world / camera
|
||||
func _setup_world() -> void:
|
||||
# The level shell: floor, walls, ceiling, windows, fittings and ALL the lighting.
|
||||
# It owns the environment too, because interior lighting and the ambient/tonemap
|
||||
# settings are one decision, not two.
|
||||
_office = Office.new()
|
||||
add_child(_office)
|
||||
_office.build()
|
||||
_plan = Floorplan.new()
|
||||
add_child(_plan)
|
||||
_plan.build(Levels.get_level(_level_id))
|
||||
|
||||
var we := WorldEnvironment.new()
|
||||
_env_node = WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
_office.configure_environment(env)
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
_plan.configure_environment(env)
|
||||
_env_node.environment = env
|
||||
add_child(_env_node)
|
||||
|
||||
# First-person player. add_child() runs its _ready() synchronously, so player.camera
|
||||
# and player.grab exist on the next line; we reuse them.
|
||||
_player = Player.new()
|
||||
add_child(_player)
|
||||
_player.global_position = _office.spawn_point()
|
||||
_player.rotation.y = _office.spawn_yaw()
|
||||
_player.global_position = _plan.spawn_point()
|
||||
_player.rotation.y = _plan.spawn_yaw()
|
||||
_cam = _player.camera
|
||||
|
||||
_world = Node3D.new()
|
||||
@ -125,6 +128,7 @@ const COOLER_GLB := "res://assets/store/water-cooler.glb"
|
||||
const BOX_GLB := "res://assets/store/cardboard-box.glb"
|
||||
const TURNTABLE_GLB := "res://assets/store/turntable.glb"
|
||||
const CHAIR_GLB := "res://assets/store/office-chair.glb"
|
||||
const P := "res://assets/store/"
|
||||
|
||||
## Mass by material, so a thrown record can't tip a filing cabinet but a sledgehammer
|
||||
## still can. Everything defaulted to 1 kg before, which is why the level fell over.
|
||||
@ -149,7 +153,7 @@ func _populate() -> void:
|
||||
_records.clear() # stale refs from the freed world; repopulated by _place_record
|
||||
|
||||
# --- the bullpen: a desk at every cluster spot, each with a monitor on it ---
|
||||
var spots := _office.desk_spots()
|
||||
var spots := _plan.desk_spots()
|
||||
for i in spots.size():
|
||||
var t: Transform3D = spots[i]
|
||||
var xz := Vector2(t.origin.x, t.origin.z)
|
||||
@ -161,53 +165,45 @@ func _populate() -> void:
|
||||
# used to guess a constant here and kept putting chairs inside desks (or, once
|
||||
# pushed out far enough to clear them, inside each other across the aisle).
|
||||
var depth: float = (desk["aabb"] as AABB).size.z
|
||||
var back := signf(t.origin.z - _office.CLUSTERS[i / 2].y)
|
||||
var back := signf(t.origin.z - _plan.desk_cluster(i).y)
|
||||
if is_zero_approx(back):
|
||||
back = 1.0
|
||||
_glb_piece(CHAIR_GLB, "plastic", xz + Vector2(0.0, back * (depth * 0.5 + 0.42)),
|
||||
false, 0.0, t.basis.get_euler().y)
|
||||
|
||||
# --- the copier alcove: the PRINTER is the level boss, on its own desk ---
|
||||
var cs := _office.copier_spot()
|
||||
var cxz := Vector2(cs.origin.x, cs.origin.z)
|
||||
var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y)
|
||||
var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"])
|
||||
var boss := printer["piece"] as Smashable
|
||||
boss.is_boss = true
|
||||
boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge
|
||||
boss.mass = 8.0 # heavy, so hits rock it but don't shove it off the desk
|
||||
(cdesk["piece"] as Smashable).supports.append(printer["piece"])
|
||||
if _plan.has_copier():
|
||||
var cs := _plan.copier_spot()
|
||||
var cxz := Vector2(cs.origin.x, cs.origin.z)
|
||||
var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y)
|
||||
var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"])
|
||||
var boss := printer["piece"] as Smashable
|
||||
boss.is_boss = true
|
||||
boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge
|
||||
boss.mass = 8.0 # heavy: hits rock it but don't shove it off the desk
|
||||
(cdesk["piece"] as Smashable).supports.append(printer["piece"])
|
||||
|
||||
# --- filing cabinets down the wall by the copier, water coolers, cartons ---
|
||||
for z in [-1.6, -0.9, -0.2]:
|
||||
_glb_piece(CABINET_GLB, "steel", Vector2(4.5, z), false)
|
||||
_glb_piece(COOLER_GLB, "steel", Vector2(-6.2, 3.4), false)
|
||||
_glb_piece(COOLER_GLB, "steel", Vector2(5.9, -0.6), false)
|
||||
# Cartons go on OPEN floor only. Tucking them under desks or against the conference
|
||||
# table spawned them inside those colliders, and Jolt resolved the overlap by firing
|
||||
# them across the room on frame one.
|
||||
for c in [Vector2(-2.3, 5.2), Vector2(1.6, 5.4), Vector2(2.1, 4.7),
|
||||
Vector2(6.4, -3.3), Vector2(7.3, -3.8), Vector2(-9.7, 1.2)]:
|
||||
_glb_piece(BOX_GLB, "cardboard", c, false)
|
||||
# --- everything else this site contains, straight from its spec ---
|
||||
for e in _plan.smashables():
|
||||
var d: Dictionary = e
|
||||
_glb_piece(P + String(d["glb"]) + ".glb", String(d.get("kind", "wood")),
|
||||
d["at"], bool(d.get("frozen", false)), float(d.get("sit_on", 0.0)),
|
||||
float(d.get("yaw", 0.0)))
|
||||
|
||||
# --- the warehouse corner: shelving, crates, and somebody's record collection.
|
||||
# --- This is where the grab/slide/throw ritual still lives.
|
||||
_glb_piece(RACK_GLB, "wood", Vector2(9.4, -6.6), true)
|
||||
_glb_piece(RACK_GLB, "wood", Vector2(9.4, -4.3), true)
|
||||
_glb_piece(TURNTABLE_GLB, "wood", Vector2(6.9, -5.9), false)
|
||||
for spot in [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)]:
|
||||
# --- crates of records, where the site has any. Records stand side by side ON the
|
||||
# --- crate lid, offset by more than a sleeve is thick: they used to be stacked 3 cm
|
||||
# --- apart vertically while being 30 cm TALL, so Jolt resolved 27 cm of
|
||||
# --- interpenetration explosively on frame one.
|
||||
for spot in _plan.spec.get("records", []):
|
||||
var crate := _glb_piece(CRATE_GLB, "wood", spot, false)
|
||||
var crate_top: float = crate["top"]
|
||||
# Records stand side by side ON the crate lid, offset along Z by more than a
|
||||
# sleeve is thick. They used to be stacked 3 cm apart vertically while being
|
||||
# 30 cm TALL, so Jolt resolved 27 cm of interpenetration explosively on frame
|
||||
# one and shoved the surrounding furniture over before you touched anything.
|
||||
for i in range(3):
|
||||
var rec := _place_record(spot + Vector2(randf_range(-0.02, 0.02),
|
||||
(float(i) - 1.0) * 0.045), crate_top)
|
||||
(crate["piece"] as Smashable).supports.append(rec)
|
||||
|
||||
# --- chairs: the best thing in an office to send across the room ---
|
||||
for t in _office.chair_spots():
|
||||
for t in _plan.chair_spots():
|
||||
_glb_piece(CHAIR_GLB, "plastic", Vector2(t.origin.x, t.origin.z), false, 0.0,
|
||||
t.basis.get_euler().y)
|
||||
|
||||
@ -404,8 +400,9 @@ func _physics_process(dt: float) -> void:
|
||||
# report where it was PLACED, not where it is now — at 500 m/s a body is
|
||||
# already 8 m away by the time this runs, which points at nothing useful
|
||||
var at = b.get_meta("spawn_pos", b.global_position)
|
||||
push_warning("[spawn] %s placed overlapping something at %s (%.0f m/s) — clamped" % [
|
||||
b.name, (at as Vector3).snappedf(0.01), v])
|
||||
push_warning("[spawn] %s (%s) placed overlapping something at %s (%.0f m/s) — clamped" % [
|
||||
b.name, (b as Smashable).kind if b is Smashable else "?",
|
||||
(at as Vector3).snappedf(0.01), v])
|
||||
b.linear_velocity = b.linear_velocity.normalized() * SPAWN_MAX_SPEED
|
||||
if b.angular_velocity.length() > SPAWN_MAX_SPIN:
|
||||
b.angular_velocity = b.angular_velocity.normalized() * SPAWN_MAX_SPIN
|
||||
@ -464,10 +461,53 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
KEY_H:
|
||||
if _hud != null:
|
||||
_hud.cycle()
|
||||
KEY_L:
|
||||
_load_level(Levels.next_id(_level_id))
|
||||
KEY_F:
|
||||
if _game_mode != null:
|
||||
_game_mode.report() # report the disc you're holding (FindMisfiled)
|
||||
|
||||
## Swap sites without restarting: tear down the shell and everything in it, build the
|
||||
## new spec, and re-arm the shift. The player is moved to the new spawn because the old
|
||||
## one is very likely now inside a wall.
|
||||
func _load_level(id: String) -> void:
|
||||
_level_id = id
|
||||
for c in _world.get_children():
|
||||
c.queue_free()
|
||||
_plan.queue_free()
|
||||
if _env_node != null:
|
||||
_env_node.queue_free()
|
||||
await get_tree().process_frame
|
||||
|
||||
_plan = Floorplan.new()
|
||||
add_child(_plan)
|
||||
_plan.build(Levels.get_level(_level_id))
|
||||
_env_node = WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
_plan.configure_environment(env)
|
||||
_env_node.environment = env
|
||||
add_child(_env_node)
|
||||
|
||||
_player.global_position = _plan.spawn_point()
|
||||
_player.rotation.y = _plan.spawn_yaw()
|
||||
_player.velocity = Vector3.ZERO
|
||||
|
||||
_smash_count = 0
|
||||
_score = 0
|
||||
_tally.clear()
|
||||
_populate()
|
||||
_spawn_guard = SPAWN_GUARD_TIME
|
||||
_guard_warned.clear()
|
||||
|
||||
_tasks.clear_stations()
|
||||
_register_stations()
|
||||
if _game_mode != null:
|
||||
_game_mode.begin_free_play(_records)
|
||||
if _shift != null:
|
||||
_shift.begin()
|
||||
if _hud != null:
|
||||
_hud.toast("%s — %s" % [_plan.level_name(), _plan.subtitle()])
|
||||
|
||||
# The Day-0 stress gate: rain N bodies, watch the FPS counter hold.
|
||||
func _rain(n: int) -> void:
|
||||
for i in n:
|
||||
@ -494,6 +534,10 @@ func _reset() -> void:
|
||||
_tally.clear()
|
||||
await get_tree().process_frame
|
||||
_populate()
|
||||
_spawn_guard = SPAWN_GUARD_TIME
|
||||
_guard_warned.clear()
|
||||
if _shift != null:
|
||||
_shift.begin()
|
||||
# re-arm whatever mode we're in against the fresh set of records
|
||||
if _game_mode != null:
|
||||
if _game_mode.mode == GameMode.Mode.FIND_MISFILED:
|
||||
@ -519,7 +563,7 @@ func _on_weapon_changed(w: Weapon) -> void:
|
||||
if _hud != null and w != null:
|
||||
_hud.toast(w.display_name)
|
||||
|
||||
const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · B rain · R reset"
|
||||
const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · L site · B rain · R reset"
|
||||
|
||||
func _process(_dt: float) -> void:
|
||||
if _hud == null:
|
||||
|
||||
@ -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:
|
||||
_stations.append({"kind": kind, "pos": pos, "label": label})
|
||||
|
||||
## Swapping sites invalidates every station position.
|
||||
func clear_stations() -> void:
|
||||
_stations.clear()
|
||||
_near = {}
|
||||
phase = Phase.IDLE
|
||||
|
||||
func is_working() -> bool:
|
||||
return phase != Phase.IDLE and phase != Phase.DONE
|
||||
|
||||
|
||||
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