From 4bcd7b49d9c58417b6b08a9916a74aa992eda382 Mon Sep 17 00:00:00 2001 From: Monster Robot Party Date: Fri, 31 Jul 2026 17:58:41 +1000 Subject: [PATCH] LANE7: four workplaces, built from data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- game/assets/store/cabinet-carcass.glb | Bin 0 -> 16712 bytes game/assets/store/cabinet-carcass.glb.import | 45 ++ game/assets/store/cabinet-drawer.glb | Bin 0 -> 14000 bytes game/assets/store/cabinet-drawer.glb.import | 45 ++ game/assets/store/desk-phone.glb | Bin 0 -> 65360 bytes game/assets/store/desk-phone.glb.import | 45 ++ game/assets/store/file-folder.glb | Bin 0 -> 11016 bytes game/assets/store/file-folder.glb.import | 45 ++ game/assets/store/guillotine.glb | Bin 0 -> 21408 bytes game/assets/store/guillotine.glb.import | 45 ++ game/assets/store/server-rack.glb | Bin 0 -> 111588 bytes game/assets/store/server-rack.glb.import | 45 ++ game/assets/store/shredder.glb | Bin 0 -> 16836 bytes game/assets/store/shredder.glb.import | 45 ++ game/assets/store/sofa.glb | Bin 0 -> 41752 bytes game/assets/store/sofa.glb.import | 45 ++ game/assets/store/stapler.glb | Bin 0 -> 14328 bytes game/assets/store/stapler.glb.import | 45 ++ game/assets/store/wastebin.glb | Bin 0 -> 30640 bytes game/assets/store/wastebin.glb.import | 45 ++ game/dev/DemoDriver.gd | 106 ++--- game/dev/probe_levels.gd | 49 ++ game/scripts/Floorplan.gd | 458 +++++++++++++++++++ game/scripts/Floorplan.gd.uid | 1 + game/scripts/Levels.gd | 428 +++++++++++++++++ game/scripts/Levels.gd.uid | 1 + game/scripts/Main.gd | 142 ++++-- game/scripts/Office.gd | 454 ------------------ game/scripts/Office.gd.uid | 1 - game/scripts/Tasks.gd | 6 + tools/gen_level_props.py | 404 ++++++++++++++++ 31 files changed, 1919 insertions(+), 581 deletions(-) create mode 100644 game/assets/store/cabinet-carcass.glb create mode 100644 game/assets/store/cabinet-carcass.glb.import create mode 100644 game/assets/store/cabinet-drawer.glb create mode 100644 game/assets/store/cabinet-drawer.glb.import create mode 100644 game/assets/store/desk-phone.glb create mode 100644 game/assets/store/desk-phone.glb.import create mode 100644 game/assets/store/file-folder.glb create mode 100644 game/assets/store/file-folder.glb.import create mode 100644 game/assets/store/guillotine.glb create mode 100644 game/assets/store/guillotine.glb.import create mode 100644 game/assets/store/server-rack.glb create mode 100644 game/assets/store/server-rack.glb.import create mode 100644 game/assets/store/shredder.glb create mode 100644 game/assets/store/shredder.glb.import create mode 100644 game/assets/store/sofa.glb create mode 100644 game/assets/store/sofa.glb.import create mode 100644 game/assets/store/stapler.glb create mode 100644 game/assets/store/stapler.glb.import create mode 100644 game/assets/store/wastebin.glb create mode 100644 game/assets/store/wastebin.glb.import create mode 100644 game/dev/probe_levels.gd create mode 100644 game/scripts/Floorplan.gd create mode 100644 game/scripts/Floorplan.gd.uid create mode 100644 game/scripts/Levels.gd create mode 100644 game/scripts/Levels.gd.uid delete mode 100644 game/scripts/Office.gd delete mode 100644 game/scripts/Office.gd.uid create mode 100644 tools/gen_level_props.py diff --git a/game/assets/store/cabinet-carcass.glb b/game/assets/store/cabinet-carcass.glb new file mode 100644 index 0000000000000000000000000000000000000000..fdc41372541743f0cde30beaa8ea1634035f4e4b GIT binary patch literal 16712 zcmbtZOOG5^6)uthArK%y!j3cz8>Gf`y{jzJ8OJz^A@Ky;3Xvi!-7{|6L+lx=r=1Xt zrP?5V0Bpj71ws)Eb|3^OnvTR4egR^E1Ur_nMB@8y-BZ5z)?=z>N~%7u?>y?%x%KLs zx39kVi064Po%6hZKI?hkzOsGk{&;bCxLl9V-XGsw?k`u1^}%XVk#`_1?mo!W#{v?=$lV}o8l1VzrCi%plUVCuuA>{t}&hqe8|zqhkQ zA~E>DmeFZAGoAW|F>sB6VGLbkXc!~c7#YUcHO7W9agB*#OkHDY7&F(H8OGc-=7!Nn zyR9zxI2Y*RoyB^&+FRT%&%*VCdpoz6SN5(iuaD2JSNE2c)9zvi=ko5(>Xqesar^e( z?&X7fH*f7P58-%!yt6o5o6s*$sx3;BEKL1mLRGG|I0*eD$Resj zrODzXkFqQdCq9ePIV2ijWvn2~B0q~$9O_E)KLkZl87rHn`84&X(E|=a=P|vxKDayZ zxR9M&7KZ*b$@3To5ob!3hG`n+aT4;l+=5SYTz;7`$+9F*Ge7QeT-O(?w+0?qP^QT< z-9H>f5bAU2r$HFRK94Mjo6k|>DM6{~d>-CX%eh+TYKdZ=R(p5$)_ZSj<6^yD?d{xK zQ@lTZb^FT2s~5K~sil2s`|>O2ULK#tV!nFe_4C`?m%sJqbbK~=Fd6UdU*FrM3zn;l zGkhLgdTkyo27J(2jR&N~wYN%(c3Ww)*0e#>(Ix}Z;({e<12o)B8~DT0rv1|f zxVu$p(QqqmG%Rg4AT1V5l@<-R(x$`G<^$4V%~Wa8a5HT<9h4R~$KDr;5Q|3AhG@8z zHtbA$Z8Bc$;?i|^uqyArJNItfSgu~%TfU<%db#i-fVHlQk%YwX2^eM4^L() zM0sz{@*wqn+?9c!AaaAtS`{;X?ZWx;amXwhy*RGuE_bxAzkB(lW@g8T*WHy;wJb#U zboMLf&%J!^vU)x;1E8n%08Kqd%WM4_*7By4c9M=`tsPf#SI5=Cbx+wt_j<>12|X@* zIixt;+OtGM!#K;CdcexkWEuovm|H_yi*b^c;W}|#(GQQgKI6Ew^mnhfj5pll{0H@{ zagjb;*R_`6y)0KPPP1teM|e~|$wEKnO7~B>)bS{KiY`viOdFnd8?JR7Z9K{3`8_T$ zU8Cm@TI~(9*JHU?vsb&Pplfk?2MJQV(D`wm@qHjK-+u7&gY zNfM;fi1XJJrn412E;#SE^XPHcqpn5OwyR+Tq;m~;u zdR&k?+c@ab(9p&nmxiu3=J>XN4c}nmIG<+0N&1+Vr{dvPnv;xUk4sc%9|v6*8v5Ad zy3pQ7-dokxEr`-Qj7S&JJ+4n(eGCU(7V3TEYf=AYtv+z& zcykG(j1MHZ)kJDpPyIB@{17HKvb@veeuCCUc^cZcnpzOmw+5fihvFU9YEi3 z3=dyH%-2Bm3b^y`dinBl|K=KBxPuI{NyYY!8|qs;PSJX?y2;P-Xh(vtHTV>(ULlY7 zRl)jtV+ifcaf)Ty*E@Bb@j=*j##?g#!n5c!hwb};lf0{Iwd3|9e8}#7;I$LxC({Ah z>+V@TAbYL&z{e-|lNTR^Lr!|F_#jCJWUm*W#~7o&BUj5F`q6;wb@zy$cKT+o6(8cs zqQ84Rd(PLTzS(Q;5xxZVcdr#6;g6aA?)B_RHXwW5J$J7bgBENY!~>R0EqjcEKgF@f zsru&B^BP_2JidUOW@wY(R2Qox#+zC1sjC;C@}_a(p-u26c?$P5;9aM0_FD02oSbH8 z)AUpqE4&yF$X+Wx3+wl|>M27%VHl4U8NLtW|Bcb;r7wHl3l}eWU;6P|NBH~SyZnb= z%)0nL|M%C=3#N_NagK~QHD3EFeG~tm|6Q%>>EfjZV%m7A$B1*}@=}8lM`1+I80A(x zuj$67+N?;0aj+J~@h`O8i0O*s*d(_VM=%bxFq=}6IvV2$rXkLUGaU`Iz)qPlMjAT@ zrJgeNv~SS{>KQWa)X!xy$J%-=Myw7@=ALdo- zsEapdb=Jn~IGjU?S>t6siD}}o2E~`_x|Nq2h-u@c9wQFduf$Zm)L_K1^o&t%18PzO(|9NXlx^kSJM*DTf5lmMc z$0oV0T!L|^h1rx+RgcCtVjAM8IU^WzS74{iT1FZ>2c@1e^|Wu%2I?6y?bOd{!!ELl z2dqnJFC?x(iKD+uiKpJCoM-6O^G_H5dj7j_@;!o>HeSc!{7B3iuYHxig=hUzPb+WK zFl*zDdL$0>TD(z%#8DX0Ge)@;>>svjPBx`%{#FgTj!7g@yv)}^!;64#)_(ch)SQ*Tqw z{Dt5Db|&-R#((8+f2=TVy!KVI-NtKQg=yhuMh&gJQNyf_H)@bLYPL)$7}29= zdMnsJY}K4>O4wjXur-lg6WFm*d(`=OE3<#Fq=}U>e1LnOhX(sX9Q#J3hb0w z%SdDApwv^Qp7t%;Ks`gIo%%U#*hN``-Om7AIhpn2EO(~nd6~@6@812^?M=)J+9Gm2}atX$v7G_gQ zRXrNph-rwU=8Ry>U4fl4YZ+)YHC28>nZuv|$%n#RJx*v=pWO|7&Kj;OWC zU3zvYTiFm8$w7bwNPq|e5+I0^5J6rrg22v45=1*|*a;xOhJqjg^5cm4%3mVqf!GOU z=bY-g&FY?>mz)8r@2y++o_p$6Rd>(K`3t9>*s7HJ(@?4J>{jaOlQYNH%k!(N&9(Bx zdii{Fx!IauTWOUi%Fn#oT3KFMEuBY5>EMOt^0{WKbmXy_(rdwfcfaS9E9KXkt<|NK zWjK5Lo${4Rd36y0*eM7i?EeX>aj|BdF5P_15U-Qcon}=tpt^D_R8#y zk@fP$=IW~&#>5`FywC)`47l)3Ksm7Aaa?QA%MW_ipr0S~t-)%3uxbqk`N6;%4D*9w znG?D=zt(Im&0k3BaBk)D!iDC^rE|@5<%zY{$Fp6`ZURHez+mM948 z)hG)5ic3-^hoHeN!Sej5>PA7WUMaUE{|%>VHNlFUTHUF+PW1}cqwkzvJ~!X`;)oOT z++-G!$%9#VzL~3A^L#IGecFQJxfD_l22i~;f2r9Tk!RhIs=0v|1T|K>o~(%*h7m^t zn4(%Tn{-VI7vPHCE7_ zxIQe$XHFbDaI`#u<8kWnnL{%(Cmw&7>9P95&9inRXOzuZA1$Xpd`=fPG?}QItFAj@d8;F&#bTvtQER)vZZO(x zgAvl=ZkDD+xie`aJFSab#f{zM7VV~R_`IG~WiMA{uXmL_q@Asl=NGZRS65oey>{X9 zE3Y(L&o4ElAnTI(uc-;y(=V43<=4L7kttow!Q`SssuH^RG%j-xgxd$Lb4GdD^e z+SMxdW!0~7gSm+*2ptdm^doK4k5Dt*ga@&{G3oAojyuTk;oUXkoq14v-@`ph(Td8h zcjk!OjE|5(r2Ey({V7j3-k=8N?^iU(t(rG7-*bIGtnpQ+=&b#JpHFv)+e+WUjdZ{Z zcH4jxUdOl;4!8NFNKw7&g!Pa|)U8F@<~#K|PA*=^o&9)O=X0Tk6CH+5Ev(s(l0uMb zav!WkK^4!aNY_4jMmh3zV9>*5z^#V!=v&7Dhdvu$P{+bkZ#`f5Og031JYe{;Fz5`W zOJP<5cgnyCBK-t+qDVjNy}A?CoKP>1SJz8Xcd%k;nT05V2s{l@tp-um!(EswQFrwU z;~DQeA-;1BSP8!844Ov2mGDNr6@br;=d}H+lXHrOA9cvSsq zrtP{opF=b6nzviUH~XQ(yA}-3wh^*-he!Gx4_Wpu@}7&|AU-;I&mU#cyU2TCI70Sr z^7R1QXxJ^+mECu%BV_Lmulgfo?;`Klcy}C@yo+Bjvd7J@^9|#p#|O?Rwb$pL&FwYAf2u)XnPSYB#RC)$Qs|b(h+O>n^oZZCAIcF%n-QuHh zOx}!canLy8BX(jWpElP4pLD-USeR94T9~0LXM|}F>? zx6r2mNAFiUqpoc<_Q|DWeCHr(XP1#ar<|W75f@?|oF+MNp zPkUC%p2xb!{q%bun@yi1y>#FY-2Wz4pOGT{-Phs`gA@1Dg#&$h>B7gt*~sg{!NSo# zVkbuOX|sO#r2C|k^zCcvlk?5^n7F<;;?oZ&Hzv6=xx^bg+2v1dY{o5 z`>u_~KDm^P?;IrU?9i+m$bDWoSUB28?8Hbu zZPpK;bf0vRzI{!7a=sZK6W13%j*Ld*FOOCe%*OK&Od|uL@_N>IZZ@@nAd&I%H z=A=9$dg%s7-`jiXCRY3O(8*u;^wcd5aWCECAUIw7-{K%R+DGifNIq@W51(|ObdtV( zO?`5{86OkZ7e{>h;pE07cP5wk}*BH0f(R_~s?Y+k$IJ`Y}E)=}&uB?)%1Xy>s&$yW>XiwV3+8Z`>dM z-Lb3j`kjBGb=oViKmIRw#oxT=cQg989{z)O7XEur{CjWxPTWiX@sod~aTuG|iAj9g zMJI>#JNIi2)UO^6wGa8wE_}|t@27l3Cx<8A^HM%VI&;xq^4dD_g{SCJpQ&4aJ*^3K zsns9<;H{KD?J^hYQj>rD`rAEqsmZ&0-%jg8U22}!r6$6GJjp@oLpx(RFNqah>LYwu zpS(^!#1uZZP8^w|=-@LY`v)ARxPMq%+GQ^Rb4u3N*1>1W+CPOl{fT4NCDk!UjcN8| zsxz?@5gbzptWdTIV!a7ost0*Voyd>%5k5NB@X@s~T%_K5{fP|@>DtJe(PkQJrq@QWgbQmf zISr3=orD+1IS-iwW2qaP=4EV}FYSg8>mwYtkfPZW!NSUc_uL#_*v{+i5dnoS)to zR($1-@E24}pXzBN7h z;q@0AWu0fdk>KM!4EF1=%X?h=->!a^?|+Y6{MGm$Chw*lZLX2vR>5gAtt03Ez?KXCpx&dY?qLVdUnl$+_g5qBH-C-~zH;*!TUPgys6MM)RaZ4!YpSX|HK77r0~M)BHLVWfdQd%~ zrqtu=0Imlx#aKP54&i!89ac}Nqv|-W$JGgSL_MpH;d)Fxqn=hL)eNpP>N#~vy`Vmi z>*v+W>a?0wXK_8N&Zy_rOX_pDeonoJ87Wj|58Az$4gYbu5AAMLX*;UH|584N3f+Y& zd>m~nDzg`}+kti`s`40SwHs|GW_k}Q@DN%BvwRQ;`_MiGhI>%4r_hdqT^U}_pdA6q zDX`j$_Gwh;C9ruI?IAFH8q80l&4S&t@O%;Nc~tBv%+x{iQLU5ks-n3-nM9RNp}l~a zJ&Ed^K|76#d8p(R+T*BJ9iE5LxCI?lUZHKnE|`GV7}^$~?7)goqD`aHw_)}Tv?;KE z3K4tI6lT8@Gun!FH)5tR?xAf#_PvNcg*J(~Jp{J*pdA6f)5!QVn#=#8-ue1_r>7Ri zeeEXn&VPQWf6v#LMLJ`}-b0_V`1I5*4pY5!i;u=Jc{94jLF0&z*il~d zY4aT8lm1@ON&5CR^~w2Wd`w(l9P#OglN*!VnOx$NlNp~*(o6Xm-$I`PoK0?CejwXu w?2}8$_|8Gn&OYrNpXL~(vicmSeU4*j(${$K%}b8A1=o`FV|-rHpZ2W$KRl}NX#fBK literal 0 HcmV?d00001 diff --git a/game/assets/store/cabinet-drawer.glb.import b/game/assets/store/cabinet-drawer.glb.import new file mode 100644 index 0000000..a6b77bf --- /dev/null +++ b/game/assets/store/cabinet-drawer.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cey5a314gtnuy" +path="res://.godot/imported/cabinet-drawer.glb-b3f72509ee62d2d19c1a0038604be29c.scn" + +[deps] + +source_file="res://assets/store/cabinet-drawer.glb" +dest_files=["res://.godot/imported/cabinet-drawer.glb-b3f72509ee62d2d19c1a0038604be29c.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/desk-phone.glb b/game/assets/store/desk-phone.glb new file mode 100644 index 0000000000000000000000000000000000000000..746a0934205dff48a9a320a8589ea4fa613aa329 GIT binary patch literal 65360 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 z zEW(%nSK1vgX>;LUE#I>?>`uE+PdL@Fn|9a-Uw2wz;d?{f7w$RIdf=#f_p{Y23hy8K zuJzuWdUtdG9q!S;+h*@QeLs=k`NqHy>WU}&*=L5*nZp{k&ACPArWZBezhedfP3uLk=4`qQ&_C?37 zDa_vTQ)~6>yM#QH1u~hGhq6E>b1~$hEHi$%)}}m^1u_YpK!yVKB4u!?7b&OE9mfcs z1&&klbU98bPk}m;@(3MCd0fcj%6?t+Taf*_)P)?Qi!m1D7z-Gq=$E>Ij$COklk!P> zSkJR(>Vr5&k3P5k4d35xxXlQyyh& z$|Gz|{m@p@e<+XgAIhWrhw>=@p*+HWq&&(eDUa|;X)obF(q6)6q`ibMNqZ?<(_X^X z(q77c#N47x@MoKIi!!la&MoObw3qN7X)ooIw3qP72P0jr#f#3fo|*ZaWptgMdkXoY zMLX?>fj5j6q`g)n@V%aW46tE*I%>Zn{`42WYKwNjdwGiuQ1D|r@)@wak4?V6et((G ze)^+KykXSYe=93XB#iwIOU&x1`?0dWG_KG;@Per`Qbx8tf&09-S%(m zX~=#1ZPRSZ|J3_A(g-}|AfDxS-A}9o-hSAsTWZ@XC%s`bigwIN|0LQ!B*t&pPeQ&s zz@OtFp8b*^@!*M6XS2UEFB)vm*wtuH-MvEKRoRoXQM4ofb{G%kWjpe@=8c~UeNhgM zlX}cOxfS($t%YLTUcU6uhphb{JIe~@QtzJqV6Ug%&s*2M@s(m8={j8Ko$`|}Ds}j9aw%6att`X9rWboUqAUl z>qh%CE0}xwH>Ccl&%H&vQBP6G&z$|Moc*wWrAOuqmJP9;ZiXGqJ-H3f4`DBkmvX7x zFgh5WV8~8J7o(lAzp)>#`xzaL_QrumXIwiQ2N+$A9!4)*dl~(VZbl#DAY2bJdK%r0 z{zh+HdmDX?0meXMFs_4*;l{znFk>jLLybYkAx6D11lJ)(oiV}~ZH&WpoH5ZDX*3vP zaUE-nF-94ajPbaRHzpVnBW|Q{O&ODosF5|&xTcMS5i_P3NnDdg#>g3s##CIV8nX=3 zm|;xAbsAJ>8HXCvah-0=G!8S4G>*dcC}X~HxG~R|gXd3H z9InS1D~v_Pa$_m3ON}MQVq>LoEUw2I%P=AXni+sJ5TiK|8X1IiFtpSa+GvNgKQz=I zS~viyJv1`{qw9v$2U?kgQT0XYj*%V$4U9x;z$k}-!vv&JkZ=Grb{Nu;kggt7M<5*z zE(;;m2&B=_&`L-%25BTDoDInrAuWe=^FX-_X)!c*7)BaFibGoqK$Sp>f=dosT8MNE zMs_H)vjk}oG!}yJ+gF&Wp%@QaSnc?{PW#+k#Idg9s>y0_sEU2*M- z_5;vw1lI`Kw8uC{;W`Q+lPYc&qd|C?r+$%|(qEWe|_0a^{D}H5ZD)9-_DfxVS6ip!a(G)#X5}C!MgC^u@I=R@^>VTMiOyZgX0a_o8Y*&$wqN&6uP^bF#^`&S6xsRscm5TC6+Ojld_y~U` zG5og$iDk-CCuy6I6VEd7tZ!r;aziV#p7f6_sq6y?anu1p<$|GsZ(v;yN{EWo# zyA~vtDNCKCZ9+~w%fz$3k#)!ot;~Ajn^=dW%7PzggE{M46kjbT9oq+U(i88`VRvWu z`#a5zy*h~dpD=jrvdfAzVX$nQ(|DG^vaP_EgE1HKN`^|Xl7Tc~u#!jPlMBL1291xP zQF!P~ep;wMe2RWj@XGqNNE4v-(a3fc`6!x7d;)ctVCYPKTBtvK zihfe?%KEiP6QK3c$aWR^D4I%q0(GiyUtfwQko#x~Ua2ULq%BKRhL7+w62tFWkXWWH zb&|FTIq@tL&-zBzAvd%#>xpk-9g->wexMEJtZz|#wVZTpAIwQlyg$dfr~H9u%urbQ z7-_;_4vuBD2nfR&GFe0+G`hbaHA1S=Ux69y}JG(OyS6n_CLc{DygJai^MEz}=A zML#KcW&K*D3DEjzWV?!d6ip>QfjZTguT+#r(w3zu!$`)CSYsVI-6ElX2|kMJ`R!|z&< zSf(s>lC}vs@hlV1`bO3vH?%VAiEm;Zk}3;+pbh4%Z&7@;oOEm-%t=qYKj(dj^8ZS( z@-fnc!OGthANo=dR(_@UC>jkf&exE&1vH_sCPOX^*5na`opK_ zCk3ypUyC#WS|5#USCNmRsl+Exr~3BwrDy`VkEY<2itvNUD*2tOk+{H_IwWy(?~ zX`7G}&oc3>Z)6>ELo2hM_$JmNsj}b)+F;K57R6V~NyqlVob<%|bF6zsAFBi_A0tf| zto%*!k!xwu-&9+LSN^8sc^{(YVQ}mO9 zSJtmZngFejMz*WSN6}Q`6R1;t`}$Hef!s$^@JdB_ByCxmGJJ%ekr;m0g2Xaqsgtx# z$cbl}c-A+v4!NO~SxyT7g@B?iyXMKy}tL3C)`(RFb;{7@7?#O#!rxJXcdoANX zf+h?mpJ`3aMLy+VwpI9YFy?|fUptjxB?D=~U?q>@!+wLXl0orNGzt%$$xjRQhfmQ@ z3SL>i7HI;sJ{sAsA|FLliBF(T_3i6R(FAfIO~ES_<&m^yY0B^senw*WT?-P+l%-D6 zHX$dTW#U=i$U5YPR%Si%O{_ywWx)@$!JPFiim#TFj_rdv>52E}ybn?SUkO$|Mw&2K z`J3WHUkbv?uM{6eqv6&0nr%a2O@>?;tjQzz$i95A-fuY=I+LFk>JOiypA@{Zel5}j zXni!YT}3{MrV^h(o$A}ym!b*eKAM78D#{~i%hHtLBm9iS@VgcymMKe}q-{b@Jj=wh zzL9mv4Xw<2;+t59q{@OHXoETHTNGa{Cmq`dbJ7#<&#~?meXJ6!e2g?N3Nws ze^YH0Uiq7j=Y5EpkCk9018KrwC6D66euJ=*LGe*E3J;yhPYd;jPti{bURl2uX#%u9 z8riNQA4OA%PoPfq?dwa?1acou!7CNzk+fxL%J30>Mq>C~3lhterB2c|At#2{W|!4I^-ob@e=ua=XJ?Snb#iTCGAviCXQV?O`u(;ULvzx93d-CJfk zzxw4b1+4J*$G&c^>9$DFDER($`2Ne9i_AkdE_K4-K36{CYy(fW4Z`F@{=_SoGJNej z4+$9ve>mm^K|^_nSMuaO{B2Pl1yhEXA9=bcPdS)V&Y; z)i?k4aZ!Ha)mR8Co$S2my---`CXaIvQd83R>71<^O>?RZTF*+%4Q%u)x`kI+XjXT73jd&NWH%JG$b3f)K< zgl;tb2z{fi7$5ZwzSNJfm8K8MrSwGks2?SdXe;Frx{LY=EVr5r^&DY;nBai}q{En$fha!H(!mv|`;^`m5YpeW6$fU`m z{72-PJff{?PhREtu^%aq@E=Vl1x;79ml{ifF!)lxw3o06OC@3(lMM$Z_rz_fIUdp7&YfHvF3RRmR_Ub`oXYUv<4>hg}DH;=R0u_gI1- z>vw_A-{0wOlh21^mf7rwWsiTIz<&UIdvzb@J~@T=e}BH^0_)_*27nj&HQBGA9q-%N z?p?HJe_q~VkA-}L&U@RU+^3kcZI0u^G2ZuC>VT&l#D6FCb8A|}7V-^P@=r^ZJ-Hj@ zI2uvESG4!WZP=812>7r+@_h$6`B5I0DL=%q|7-0a``=@y+2=NS-)B)}j+ghfD(84S zx$5lA;PY45ntGx994F=f-3^So;h<*JlXIsJCle<)?lqcbDfn*c=DP z_W|TM@9lQbkC*q_^N_V{@F!L<_vAK|9|`>{yD_Kyv@hjgnK}ER93GvQf93mw?c-vf zTEX0t-;nw@?DH^g%B6C{=xCIFpQVEl{65P;c#rxZyfe-3v-HBXSDEj#48ivrhLrg} z%V2zeVX*i<%a}6XXBmy}Ka~4E%M^UeLH|C>WZ(B$rs2C0)8zMArsBI3Q;pK^v&_MF zE#}DYvmAx*PaGBceU_E@W`+KJmKFb3e4k|{z6Y`r-(wk#Z?!DO^?&R8ELeo_O_q*E z2Yjm~hU@>v_gQv5_`w7FKKZSA=1I96;oa|lVvgw4&b;f;m-B=b{;5-+H|_oI&fU~_ zS)MTQvmd`Wzw75Ony+1YhrpBm`7wX>!Gh;mZwkB)v+YsK-!h+n{aL}M9Nf6}iTrM~ zWnU#Q`l39I!*&ZkltG8tSBLSPEso_IfM2_5phZ3$7x8BRz6AJ=%?DYO`9_SZBk)1^ zFvywxu%|^ib(r+0y*WVe)ZvT4lX%Le!))7sYCnr|hQT(*OBvW!z>tkHTNoo{U|Wub z_S*2q6j#}Zu(I8p)$?4+LmLrK83a6Rmdk!=uj}uf>5@O~wE=hmL(inwVfIVAkY0zW zH`?W;n~rjInEjF`@rO(MwtO<%2UAYkLdi^9P-bNpg{KZF8|f8H-xDz8)M5G(bxS#Q zm^9QU^U_r zpz|q&X-DWhr{)NBE80r?=9F(kw>j#W_JuyD5HI~u&=ZC}1r7a>{VIPZto)p^5vI=& zulx{t$Pp$UW6vo+BwqPE>D62yjq-WuBu6~k3K;w;KlCGLC^OqCpCnA3NSg}zrA>uS zs9W}{{F8ktpCn%SkKiMHj5O4bw3pDEw3o-Codl? z$6{d|wi*-1qK#FIi(>+BB{Tc+!BRHzR&okh8Yiev&_B6?KYE_9f^vIec`A2Wh1|!nT@D3YxBHN2L>2_CsCMcES$G z1)ezc$vUM6+Dna{_Trd|enEPcH63yu2z?+Iwox+4oOJ~?R<>tfim#MM*n>Dtj-oxZ zv5Rr3@i3RXl+0pmv-&T$9)9g2{1(Fp)~<~qx zFYz13?8I*`>}OMsiF*#SDHqG6kKWu%;EzF>{VowX;v0Q?k%!C~f2b4mJh!tw%Zi^T zXPuxYp8b)I{gW@{iK8F(N4~759&!Uez;AtgV6EOV)8@D*?HOYq3cV12DC!@-_}|v6 z829t@{w?$~aF6$U9IWTK*pB0T0rf0@^SNnKZ#V>^JQ4iJf8wzTn|cPVz#sO9xIGDa zARp?1{WACX)!A%EJ$dwX65k}~Iliv1Peomm&3-w)N#IXCk`Ko>qcLsAP$oa>lXAZU zJmp)y?ed~uDF4X+rQTX`@2vvQ`V{=*vfZ}`zo7gHl&|~o^VWmV`(?j=+8P7BPTX^= z)wJzsp}+jvqpidK@MCM&?i28PCh4Mm$#)U@{{`B;dHr~s^iSVBUeqt^HOeME<-Z^8 zDgXVTr~HF9%(W>0qiDx^%0v89v9DQ_k9woMo|)NS*o|f4e*t@~#5g9SO!<}Fp1%1& z=<`;{aG-rCcI$v|j2q&&Iy&Gx<6Thhf>Jx96TU;<4(R}VJG=*C9G#K6 zBL2}Gd3&TDh<0=YWjCarNL_I4h3}dVz;yt=ZQd8Za4`f|Mn8JvS1;;ttwX$nzvM9- z*WvhGk3RSXi{ZEq2W}XmDE!5bgHhsdehkMce>8sAV}$r^kI~{+Jx1f#F8FI7-Y)Ex z1>bB>n~oe~Dx%;v!ABHX%x~!dPvr z!F3Hv%ZydXmm#eN)NCA&>+vWpH`XFwj?@fjB`DV-9gnmI*AtABjP0J}*N(95Xk5GD+6}gC z5WjlT0M8l;uj>W>X@}GaUe^yk(HW@+{)WLFM`&qk(QyS6EKbh zQU*SAGRBievhcSVxGy^@&g*{I|IthOZVAqq7HsWsy?7b1`MEJu@c+othMfhU`ZxPb5_&XdN zjzu~eTAmIsGmtvq?@)MK2PFQ+%#mn)08%^j)Eo10F4BDb4Z>WVkF*$n{KbgHNb{hb zrQkFIiNDLz2O4XSG!pISp?)S(J4l-VrxTFoLSIRYbQIDujORp1vK(oK@#;-i<$r>6 z=Xa-HoO=Z@R?lO{HTf6efx#a=g*qm!Mg7s zkLCIUKL+%(e)F*pX4?lRbQX9WX4|#rceY-9>TSWN9Q^o|ogB7hUqP6CQJ#tMj|Csf zpu_Adzpb|z%WHt~J$W$}j*ECTuHiQfv}Qo&evqH|Alwdew#*)A{RwjF@Bq-?d-E{s z+mKC%*)Q?GfNVO2;X>(k}f$ufyz%cDb$l3!+~I zQ-|b9Jo{qXX_cDlH;$JwE4wI}sYA-9bV!)KM?Bl=Fnx)-<+ya1K1u#c zPU@2|X{gWXkWJ~CJPDJYebJXlL!Quijyk8$Ku*pLkTlexoU=K~Ea$9ASo(I3x{`BN zz;e#!s4F>VMO*SGPwIy-^poS*IX5XM@p29d{&L<48YMIJBW)^lB5f*kC2cBnBJC(( z(z7q+ldzWU*CA&IH%Uqmc zQ76vciU(!W`tiY(QSw%DvL8Vs$E42N;>@e*KKslUp)kkG@sVDRU7U-Rj@ee) zN7zQQk+7Sp<5(oGFXLD;exljBpxN}xIF^jzXnv=BRQNf4oc_pq!itV_K>D(nOPVhh zeN@do`lW1Npl>Sw2JZhCajaXhAxwL~#KK)i6ZBwo+%A{ZX;6VbUQe@SlCQ(%uVtj_(PV@v|oT8uTmLgFp31J{;fgmrW66 z@?$;a-U>YBYjCd@_TqSH2j)x8oM|;2caJDfKt28A^#|@Q`UT7Lcf4Rx@8cH#R^a=s zyvK^Tt1XUi;eD&Eear8+UVC7(_11M0g?-uYLhxnIcH8HTwn_ii{Lwc3uG7=QZPK%T z3EESB(o_CloVH4oDIe?EKk*}si!I7ez0t4Vx~{vhTOQ@{z|&q6Fb7I9mhp_%1{3EPh}tW^CskGnd2ZI%1=I&kL{0v{NzVD zU-)dFke}^X|2Fz3zWeo^VYje2R_9_Is}o`e2N|7eievS|S-93XR{t;KScqJF8OLHA z%lmEL&hWbah+cL2GLH5CkvP`tPu06Bj`h8byhXK03rzajbW)>EnahmgjQfb(n2=E}v59C-{_u8CPIi_7#MAzNb7Yc10OHu7f-iT$!Ic`{~@zHn;MMudrHOy#_{j~4RKUKxoeeC2;WW(Jm7om@E2KF!ey5#4G#0eC1I-m~Ckb%B^R&gvPC-tdhqdqB{(z6;n_0N7)9E&_5gNS2E{}Hhn=|3W7Lm$J~ zMQn!lBE6a~jAy7hg1$uDMcP-yW2AjW3`W{l#BZb@intD8j7!ka56NHoGju3oCvwh; zxQmia`5|;AVm9oHe5fn-#ki63dGb_qfi%j`DYNoJ!pc9PA3>vJRz692lpi7&x{@{( zI-zbUkMd9UrF>Gwv826(9;Cg**rmOM-lV;RKBc_`jk1^WG0H%hl?;UC91%JpAC6u5 z811EeNsX8Ml^#g1;#jILen|A5*ap+yrCUKyn@GK~jff9w zcKkAq6^KvKccCA~TEC2AwZ`p)-_d{QkNP~Oe<{C`^}^pY9TtLdtV?iSXB>xk700p= zC-@@bSd2?BZu63iV~v+_EXI+DfBFlGW061OH(r0?aV++y$FV36;|y%icn<4T9E*Ah z#<3VLV*L`tC#aw65#M0EL&dSYc+y_0PR6lV&v8yF6USn^YT{Uon}o%&yndRBajeH> z9E<%jPD1{{I2OmtcC4q|jH6IK702SZ8DCOyEXIGRSH^=>9BYM)Z?e1)aUc2%<2}oO zS8*(kZ&`&n7RN=tDvtFw;z8uoRve4+vmNDEajg0>aV(C5@g>SldvP4J593(WPo+2( z=_vnVv>V?>9Era6=7UO6tj>X?A_$9u7j`#KRVem2d4Fr6@ zPSAwGzh3r?^Xbq>&6e*!nh%4QJoZy(@J;s!nlSjL+kWdzTJ{kB$IT7-FgVgb@637m zE4f+&_?Nk#J6C<>HzxZEgBSnsE@#iD zuAm8n_r3G5bCUV27<(9e*3vti3m^QMpb3MO4767mtYjcf7<|=>XINv;|E@Lcob?63 zE?}2%?;p-6oc!8wtb2x`(*SP+Uy%4Bz20{7`u>GS?n)Mh{No{4=M&aer`KRhgG2df zG(LJw9$TlWLLMKzHg;R*sq@ikLdM9pO3&nPLq9h3V}An2M&3rey(LU^DzOzi6z$-fBb&}r8$)Ab*nag@bhuo0#DsQyCK13PS zEtbE`=WVR>a?0%S6=hU;`RY?{G(CKar1x@97ma8y%7Q;GhVk5RJj)~Akn~9{$9dPXc zS@;&_{`l*T_x<|dy~;ZLjRLj-IAZv}=N>-fUgzk0?=+9U;T!od`0k^3JDby2m@iE2 z?u5a!QnxwryYDgY-*81f432HM%bB0N+U#^oA14fsPP*0k&aR)D#~yu0J`5gw{2k7H zeQz?K9WcZRgTJ4=)0wk-uX%&HCm#l%yZ#<${ggY*kGhX^!r;Fiv)j4g9}k*$b$K8k z2LJBTJI*rZ;V<|vCF7#vRG&-L!8l8`B zN1a~h6Gp?eRojQS&ho=`Si)+b@+!D?d+Tl!>ydmpP@-(8eM{|2at#N4qiiQ~(0jSs zx5(eyAL=BXF9*FS3;rIRZ(ova43&f4zfVziuiakXe$~1!2|U_)d0<}x-xjZ3abFT; z(GM;L`Fr{?eEXH!mp~%+DV4XwPH_PCDSkVI-x}$OEB}*gH|#6?HH?1PC;0zd2jV&q z`xJlag8!AY9{W=r_M@@5j>WEW5cW9kQxmXjjm9pCEbOrTNjD)15H5`z+UaxBXq_!gG2#Vep-=zU}n5;0p8k{bEiSJZ87!G+%Iw z`J=yfbi&}DFL>KIx7)YO+gKSiLblJe0<<=Ck(!J z`+qs_jJ(2pV{Q*847Pf|>U6vPTyw)S6P+-4$CdXwBX7UL+&?kQ34;&GyyVP?oM&FT zyU_`Q2kpGds&83s?mBC0;j26Fy{h?x3Yn9K7S7qb!j69Wb8E&6S6CHj3ZQX81?a@odFp%uG(|n@_%ORy zpegdfx~r|}T;F~B7FICzQTtVspyyg&X1@ZlsQMKQTx?iv44c@SD+L1;=JSS zXDxES@$$gFCHRv2e|nzcdtnEl`2pAyxKH)KmET@q@8j z)#F-^{b>mHqtUqXJjH!#Km4il)L`sq-afUTI8P0h`_u&NQxmZ7aG&akYe(#nJWqAT zpW3H}Vh{ArQ=P>=H53$Qe(N3Q?eo5Cx{pkE!r)6D-rwzV;MwMHuRp*EgHOESEoaub zSDGvCNH}5eIlt`W9)H03=9|4f$cMpy?(mwk>tEk9J1!XGguxG<*}?tMSI;pMO)uob z;PK!6lk?%~UFKyUjC8`_*B>$5aZjCQ9)8nv`7rp%3;yVw6uZE@?Zb&q7>sX0yS+D^ zV7A=yW=8fHRk^AC--eN|9(R+Ck)=TBW{21&}Xc% z*}jFZJlMOCzif`Zd$-~4Iy6zZy7?mO`_cX% zSUS%zJlYm{e!S{g>zEUMZ#{9=QFa(lokr(b4IjvCV_YuAW$S!&yv_&m+t5h?I@ro78iPRp|^rgqvLgX!rJOK4Q;FWMqpo( zYdY8Vmg4&Et<7RJHbV9%(0com%GG&_{Lxaz;s|?XnWIXS4RxN9=OuCG@%D*2(O#XS zRNfi~G-Q3*a}|YVJwE<@O72Huul4%z_NTCYDzHC+r`WdyU%_6Uufon#pb_V&;JJ$D zDKGA~KX!@(usd}Y=PCZL_P*FTy#L|ex=%6gSC2Cf&r`vDisz|`VpkfDT}tg!UfhrS z)F_;%Cg3^&`%?pUwEb|^&r{v7C-%l3ScgA;*PiDoV(I|(oY~HuyY^i3isaw&Ves&c z|8#!;%t~|Pd8g*X;K>(!;vD+Vo#ru|yZqJ9oTinj> zh56Oyqkrh^gu&Y)?cIkT&6>AAe04qyZnnF)FJ7|2?0($;8~MaNA`Tnn&iE^I`Co+xK(7HThigYwy394}-T2Hr&-` ztu(E6%kp9HiG8P7>uz0WkNZIR{}DjQfqppZ9!f2%z6R*1$ zt@$wc&X+%Q?)dR+bN4R~%ZI`5+OIfoIm66-58j*)gO^R&=VU)xVqX8=wtN_T?G-ON zr|lOp-}>)-J`Dbb{ZFS`U6c94!B^$O;HR#B#W`YJ+MIUAAM;`G8)H9po_}wtxis

B0bf;XLmKZ%+9c72c!gYQ1Iy}R%AIp+R9xGx_D-*CjcPVC*0<_|vkFdqhg|M9_v zdv6$Iw>A zqf{Q)x3qZQUUB}B=O*xO5&KWzT&2bPL_L*McApBKqa>Zt4rd_m_sjOj>7@%!J^bD$ z-=kN)15rpQn1_EXH3@nt(IY7@W4aPxZsetS4~93Pfq7`L(HkyzmgAwuej!Y=b}Hqm7DaNj1vaOu724$chDqrWBYgVVeoA1K@W}U zWX?Rta>C%IWp6reGHd>7Obw0LxnjiF;;)KBqy8pwO_jt-&eb6)cFgP`~ zgL~<3yP110ia23#@8~|K@6560{2>qI!{G1k>F6$)YnYjXMmk~e>UZ9CzIXWmbCmm| zd>CvV+`+v){bFv|s-aF8oS5{cv*Psb=A&2c&WFKAZvE8B?Ds@&%aip^7`*cAd#o-; zttq_yn=7qVzk17>IqDVbs^e$cH~(?I-F3?Wg`Yh-+0|)uJm_7}yPz-VZ7)0RX#1nP zJ6O?EdK7f}Fucy^gZpl?-tU;PSB)DK%2TJ&c~-**V{tKF8{>6#K003KqmN6c!MF<0 zj}84;I$m$9)98F4a{>Bsp&#nG$VYEmfu?99$nQc|1?bAw>2-V?s{9DM#t+s zxv=7cby%IFTGYDl)db>y!Tm|?Q!+=JCblm=pOX90|G~JQ+Lwa+l_*0J|9%zNmmsUy zk7_?pdHYt?`xFF``<|DpeXBk8sV+u)q{?x>f!Ht9dlcTUXVzh-s>jYV6v=y!!h1f& zT}gkBVn4A@dGAq-mirWcH?#G9zwy{D`5uKA_fzjtaG&zxemqajc;Qp$uxmcdU3~VT zP8i(J7~uZm-Wzil-#pU^gOBRg&izT|Kr{D(?S#S8u07a&=e&n=Im2?o;1NG)=iYnS zLFTrjCOTp8!SO@f*Wyp-ww==8gu&f@W4Oz&?qW_HI@k$=+czKVX6HVXn>c--69%7? z{?vKt&--$JT-DDBgLmKA-);Kn`rIGS>*IvM4gdPLv#0le=Nk4O;)KB$ckky;9&=8v zp<$2{27mSGe>o2~|01{VyOW$S_`Wmxy65j(pWC-*f)fVcvEd_U)9hz+lOJhx!r&(k z>*v0+;@sTiH!UX&o_+R5<};Vv;XeD}#=@u>>+OMWKWyz?c3Iw@JINi;*Rh`c?Ye?a zqvJte0KE(PTv*$`{P!Mr*e6TvUv2w|tGCthVSJ45-0d#Be2M+{%u|+5ABGR(1Nj|{ z(ZU#Qy=@p?=L4B7=)r{^Y<-M+Tb)M7hw;H!Ea;>Fome_golk(KsAtG;K_><1#0}6C zZC8P&$Om#3?XJ`7@>HNH^5LF>&;PM~O2zrqeig{kb)(qV)VV5fenMRx>s1a=l(qN$ zSjSX$ze1I$N2c~GZ(maTQ()f$ov7E&RW0It#P;M>a-J&LuMA1=<>b$VwxRpf0Y-bA zhAO}B=f(XF!AZ*hUiuj9Q)6&as^&ck{_gWcMD$AEOZWCE?>!3d`IPq_h4;Q6-%J1K z;(_kAZ#|xyGGLq&2LF2INcV-gr{tDh{7^m&e(L=G?w>yVRc^wWbxs(3W8*OQu70QI z8W-H14}*8z-P@gY=l!`?rw(z#;I$_Wc3aLlDtG&zZqJ9oS0C2P{b=I%avOg#!U=;< zX+Ow)>-;Xcy^r0W4}+f>*2i6)JR>);Ps9m>Z~DVv_qO~UQ>LBsWIhc3c+>!Q*T${6 z+}&9x41VgRVQ!~M*G^gSz$^JMIQPjw_eJBf-1%=!al+u9KN;z+UbJt@dz0SChryGY z2fO3Wx;yvrwxkmVcNjX_J#;i~J!U+g4}*Jm`Movt?ANTX|LpL>G5xP_|MK=4yZdi` zWOaRMUg3@xd)h;HzUSz8(7T{_L2v6cH_dp=dSrXlKJ(^7>@c1>O&Cv|M(6X<^T*k{ z&cEM0>+$xsPNU;>+4Q!M)5cf|7>lXXhv9WTkkf)r3ebrg##5)!c~-**V{xIk0`x}x z7yHujIv-sIod)A7KtB%jQ_%5xTb)MdquVh+Q`Xix4cB+=`4iVuevr3FSUsQeD%ANY z^}H&uFM+N_?n5FkIZp-lC5~OttMijA zBNKR>WLn>&=!D&>2hK1(5cT8xe*JJ&@1>X9r|NK?8jYyjXt_^$@1^rR<$sTY81Fp_ i{)Yknm&Vff(i!*Te}n9W_bB+jpZ8w6df#uH@&5uWK7>>N literal 0 HcmV?d00001 diff --git a/game/assets/store/desk-phone.glb.import b/game/assets/store/desk-phone.glb.import new file mode 100644 index 0000000..a5f2068 --- /dev/null +++ b/game/assets/store/desk-phone.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b0ryrh1b87oad" +path="res://.godot/imported/desk-phone.glb-0b4e710e838e5ab39f12cd38794928db.scn" + +[deps] + +source_file="res://assets/store/desk-phone.glb" +dest_files=["res://.godot/imported/desk-phone.glb-0b4e710e838e5ab39f12cd38794928db.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/file-folder.glb b/game/assets/store/file-folder.glb new file mode 100644 index 0000000000000000000000000000000000000000..007a80ec163bd60b73d540c51a8a1e776b093f19 GIT binary patch literal 11016 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 f=~K?>Pua({$ejL+k4yT~9+&iEd|c9x_GFz`@ z#I)!Lf)1eL1JNa7Kn;kfI14bjograVzvxPYuB!?zP(SKI_I7Zo0nWZ(in5^Vq-ot(U>b2FIu>>wxzqft*5qOXYGcz zj<&9rp3bh?hT3@>yE;2MyK6R}OU=y9Z5`{{x@zWLx~OJHsy?WX$7<_pceHhNw|92H zI9?yC-BnlHy%r8?8)8oCu5DPkv$msU3v{)MrLC^EqjOyw8)9`qUA!()m#jHBFx3_L?Tim{`ZC!0cPuKRgBJkGEmacU`y|uM#L0eDD=FRPEukPHw zVPi*IH@fbuZEfjpo7K6wv+MGfwUQv1R3a(BVu@5H9nWMlKo(C3uv{_?09l|T+Ek=V zC*z4sJV?|9nB^8Xh`K}0bm@3SU416Ci(&5{_STlILiTWA=MoX@OpuDFQkf*f5EoUD zPG{Mgf-0MdP^aPv#4DSuj(QMy-92q>n}^A2IxSe@>3BRHOD4!*I#U5J9ihu+W0_nm z69~F01SW#W4im^ElkxN*cw0BOtZN%4-fV<7NG4*bAV?B#Ho_ZAX42V2CP1>+XCoY$ zbbz!-#`^^vxSWcma+rNOIcNfGZt3o6UwhrUmaglE2|S3>AQmKZu^>nXalovui?A0j ztm>&yHGxynwHpM;uv|R1i|ZU$H(3IUYon}hxc0i*x3u@P?{JMRJw09Rt=oH8-C29p zqQ!HU%w4ol*2RU3u3j)>er*F*!X>kp&swzT>dUT+)i%U;)zxD8X|G31~Grol8MHv4VoDGpaCe= znIa=frWz#tVN2#(b=c%EhV=*z$`}qxC=Lc6H#LlLJ(7bmmg6NXKYFpMK#k)JZqPtq__Vw%Ax|X)Ly<0AhwVhkG zc6PLN^enk?D|Q~K0C(HkwVm5Lu*bi zJLnvG_h?7tkB$gbb{D0vmWU%g-SCTN&6q#qYT5kw9$--E;Rx?^pu7jsny4#$k~^B1 zyHRB_*gC{{#GzRJ}6Yf9zJQ|S%m0`f8T{*fJ z2Dl$M3b>jp9UO^5N%jX(;--?gS<78JhOH_8vWY|x!~SBB+$Xn@i)0*OOC^JTgHY`P z8i+DVfc+l-a+ypDn=#pz<8g95HWlR1tH>nU zGSTA)Xi8vR#-<%aa$}l^qfa7{jA!H7Y_vR!ml(r>%*_l&;6$yg50jS`HwxPgmAHl2v22Tkii)(4M_s`uLXAnQYE z|H^1^D^z(|4QHL^_NUUrkqavYgnGM-1@1`{EMe(LBamETBtWrPCYg$*vN5g>m4?Mj zgVa~Lp|-+LRntK(5ANUwo6bO&2MHf~|0)5gdU+lRNRR;E4uD=Bn9`B`i@%SQJ!I>R zJ#F*bIyUs+Jyd}A1TcnX(falBrVP>PY3bU)mu7hBlEz~fUXaKpbO@azcSG%uC+O0# zaW4g1L+zf%?y3ZOB8FR3Nq0Qd^oyRWWoh>eFP@=}UeO&7_i#AW?s#Y}9UU*oxX=!f zdQQ}jS~AJm_`L_3dS%p;xUHA=A70>7ST~2>zasGIY^)v1qU3`tqK84vA0NsLZh(KLwAI0`>PtTon{Q zaF)o*$u+;(Sm;k?Uvj?fv3BXJGX0Yi>dR%viRfWZt`r;j5xwK2ME3X*JdU3d*_*rI zaXg6ZxGIr>&1(LDA4~i$XRPDL!l&h2sP|>yvCJRv!~Cf#)826+eO2bTjvretYtB&b z_+kEttk@hs(pP0-@N%ZAOgx$!<+9^PaH{O_!&t~%T)aef@shb9pT&mrqO6=CpZk@S zAKI78^n*MXy~@O(`Qve-*d&+ehxm!jmz5uv&%#f+OndU<^0_E;T>5nRT$Bsn$P}LX z+`IcwTFR~-IvwkF>w?UR0I2MRpyTDCTHWgkk5I!m@$_H%tg z_H({O@9Nh1qn%=~7(Y}j#Nc66JnW}pW+msiiPmoFbFpuKf*Wq%KXST{E{DumB$Bh zDNn>leiV=Nb$Emuhei38_|rEq6l0??qs|@VOk$(H6bm`mF&&O#oD>(^IS-u!`%-o~ z!R2&eYHP*RrAMj93IIZkCQ%+E5=Lb z(nlG7$uHw2F##Tlk1LCDLwkYaqECT+93JXKPn~Py{3I_tek3-E$(0gI`cz*mbqt56 zB2LPehezh%e8}9~97XTuD43>yX_Vdg;QQ>|cmI<$<2OzJG|KtYc8A6sux9M0>1;Qq z*S`G1({i7C^!5=_XZu&~-yPDP_rIv@ML%OV!5`b{Cy_WMG~+f+e`~^ZA;(Fg9{H~k z{hQF!zcHx)*4RX?KR;qV5jAh#goHq zW3MeQ9nUgT#DGFj7Tn#@d`XPV1R-dtv8;5-AASaXG$h4U;k+srZZ%|e_P znybxRbCp?u^8z!^Txk}YMK~`q?=nlwGII^i*O==}vsq*A~~| zQ8@;;p+U(4l8-PVf}hb`J!vb<@qjU{PLX0s{i@RCZ9Qa zlh~*{`sV4j{zIekQ&zUy{^bAoaf7`OKH0Zirl0i~mwJ_n;g-`@2nNa<@7j_l2I8S! z@jQ3eh9Vx7iQ(qg*A?+plQ|acbzEP*75){c${s%jkCTGORl&nwB72+^`jb6Qq_5*j z{5zg2WXFl<9anG<-KoSUCf`>@ym-jL%rtG zO?NIW#@CnIUu`btPk%D)HGh2hLHO4^^kt5#IYU`@~jLTm*yWjK@!j zp7xqEzU=WVb9DR{@EphIIzDgm*;DI(eV^l=`8|G=C*jEBN}j#FXJ@JNIhxP6JiO;{ zK9?8k?)g`qv)NyL=(9IV)v;vV;o-Xp#Uan&ehfJ_pTWJp^6c#J=<_&n@I8g%k#`4f zY<(V=_Y2hVfIoSk;;_hj1mKYIU8%S%>%_?S79JM*6)b$F$JjEaz7KFb5ue~9oxCS-I>Af5heI(C-(TP7Ab#Z0<40nnadM@^(#1<-E%9St7cb49Vw^O7%q5Kt z;-c@DBuXU?3;zW=y$Xqwpx#HH@SHG2 zf73p)cazof!M)RjcaDD*{1QL;T+%!>q@9)@{KYw8UA)(pml5t>nZL$Knu4A`KMmX7uQY> zKf7T>SYAf>jpN^hoG)={`Rp_E3wPrAcju|~b|4wQ?c~*_Kbd~+#&fOusV1|p>Z{4m zJ@ZUn@eCv@2HNx|D<1X3am!`JpnlXw^}N=iKe@Ic7N0l!wB<-ywBa>?Kedf&(OGrn zWmTJj{FJrn*d>0{rmQTn=_|wKY^&N-@gvWxG)6A=%Jqs8b~Vm+4NB@+r=IprY(r_h zI@_sV!!{gRr#<_Zmu#oLyrdobmzT7ozO?+uc^Nxn`KHFNugzG>*Uz}C@wboV?bu7= z_L18^)A-IaS^E_9mu=hD*g0oNxw+f9WiclOO$V zjGeoqqd@)S6=Uu6*6sp*{<`OQ`{ee{5}A1Z`?b+_(d4c|e=_?n_}z&%`Q|q1t1>xn z=^1Sw`h8cympSg-3#{!~u|_c5yQjf^_2Kn`ZQ1mk-S=dR*gyWwybbKS5}6pTeWlTU z{JU!fPc@l%o^NQdU(B}&9$%jE_lZj{TKOVq1O44OZbHD`!NbBP!j>%EAzFB1dvX94o3Kbd1OXX418a+x`K z069s$%EX}gLkyZT)N3x$UgPV_#GpCTpG?bb{XP(?SpMCIIa6Sv1CpiIz{p96}?gt7_-!rT4-FwFNIXne&L>@41o?N(b_mc}Z z$3F5-&N0XRM>EULiQ@bMXp^&&A7vqk@Z< zMZEf4yev2poaBaBUA!zf+7CQd^M}uwnlr#=HJ30)>%}_MSObss^2d7l6MFe$z5EF^ ze=tYu<)qb|*Y_V`NO$*c@k=#AP20MSE1&W!z20Q z@JP-$Jd#VC3uEPCO*|57#E2L)f2@~3#H0B`JeohmBl+X-XigH3{xDveKa7{=596iz!+1&lxOiz!GG3CCx70V-YkJ;iU%T&ncKj0? z^DJMS9XkaT@8P1e*u0z zTt6wK-_COl>u00=T+QQ$8h71xHrsKm39l7@HPVm%INnF3 z>8_l4QQpYq7^;V=Ayy)}J9MH%7lmHy;&P0!ctq1RfA~wZrGFTwdG}myI1;Yyfhvx3&2l)h~x9Oj}2c($$ATLJU{zb@Dr8q zy=}j}?R$r9dFk>;^GJA)_&H!%-Us}QFY&V;m1Bi}<&ovI`g6inZ;c4cOP4Q_hl~s7 zOI%tG?R$pt_+@Pjj_7-alkh9rN%(E8`u7aA_%+-Ap5eUzhwm9ee813$Bl@0UCVo4g ziQm~vzh{_N<$H$t`1SlR?|X(?d=pTM?-=IetAKuJR5o6!67ks-bY!F?w`7Dv(ddT zK|i+hK1O>ab+KLpnff(s!=ZKYr+&SX?Y*d%m$Ya9@{)GcmzG2Oo?*N>3Rm4xa8Uj| z!zirHqi}Wd5APrM@9X*4zCq-?XHy~X>p7*7ex6vrdtVhf@99NGUF>1IuZj%+MZL%v zTkL_cw~CDYP*E?k=d-bjOrNS3*~8peMJ8s|i|lcQ-yn({stlhWE0H-L#}ECK%j%zc zkujdw5K9$V@ryle7#sR`Je14IkJ!`3^GW{2p17P$|0Oc(UVO`CV&3O$L?$QEpT7pE ztmS|<%zO72**`AVfiYXYv8pd>=oOnPdTEE9wwKsf(U-LAsGv#uA1SpZ!sGcVv8$pN z{~pc~`$|2>DUL0Db!1QPe2A_3mr}=9Tlg>6YhC+=k^g8+^e@*-9sXRYX!^w{cAg*Y zAF^i^y|jBcOYBSa_a@*1eq?NvP2sh7j6^AVG!-%7JI9gX)sL+X?|yo8_~716urcAD zt^2K4Hycvh!-uapxA?oVWV^w0iY z-B9Kj%bF(?$M@=HWR%k~2FG%B&cm`!EVN(I{F2-s9-Dl$_3DOm;TR9&cQuZG=a=j9 zULD_klAES*l-zSnIR3}yVO=WLr9a1bM1Hr8`g?UlnPYr&?_RDUA;+79;uO54j#8W&ZkYpKw1m{Y+5*IzWx?o{} zIgnsiY!|{dlq1D2VnUT;L%FbBksW7`P!O@PFm{!V6DVUl*r}xC*an9rRY?{3zJL0g z)6>&4yM6W`bZ?!l+Ua-%Cc8oaoMGJ zuN}Jc&cnyn&bxccb?MK(nTYL3ekKTUU?ROry2~`eUe9Ph6Za94Oz$M>* z*@1Ts59SAZ-L=lzI}abd^T_SDLAiIZyY`;W+MU0uy&eTAttDD{1XSvM&4eT4}*DD{1XxjHEIeT8W}DD{1X89pfW zeTB)tS~~Q7g>@k)^?ijUBPjKKh1Db|^?ikfCMflNg*7NB^?ilqDJb=Qg_SEP^?fxA zeKmxyu*%(f=-A<-M-JUmtlBr+{*LQzIef*D8xG&FcHXh0?>JnVyzZ7m*WZlQ|Bma9 zzV`63L$};=b%*Xe{F2*mx&7$N4_$AE5^w4r><+udXFM2o zhm-E8)08*0&tx>5Or}E&Tux-J85zWY9aW2E-oa7PLa#p=LX5w;aCV zEr)KsE*bo!z&;-3fIh(D(mgmV#-&8vJ(xR$`C!`3^AV55mL7qA#Rrq=XmLjp{hzxy za{j|^-#s`f^2%{XRt?B%oiGR7`Bpn@EstmI&<}gT(IdAWIdT$%d#+^&hewo4w~6^hZ6eV8Hb-e~_Bcw*^%FCX+Qc;Dw>eE~v%h?sJm6HL zIDM$JnkTw*=_L{xii#0n=Xx8&Gk6J6uHhZ}rYj%>*tmkDO(pH+Sc)7C6Mw^``G>ajVlX<-B zH`~ws!L2kKb284lmfmZ5cw9CM%avvajm^d;D4fnM-z=u3vROF1((Jg>EGCooP|g!3 zDIPnkT^=d>?GBo~92?v`VV>fFb=hp-t6& zV3f8P^b)42kvC0^gK28qJWa(yxtB0ajlF4V986Q=$}}}bptsM;y@Yvc?0I<{czN9H z<=8ycOPHs|o|h+qmnY3$j_pCcgn4S>d3h3ec~bE*3U{0wZ8l+^ns|%BBv=e4X^TNG zVVatFi@_vV45rPCK{S*Trm3m77)*o3U|LxWrkG83yc|1L#?{894{WBMm#2Z3r_ElD z4Q@YSo|<}Io(5jVd41#J7i~9TqRP9TpYv|u=)7C;bS0c)J8wT>uFAXKx|?@{g*We3 zR^Cd>(LN{4R(aQ3dGl_t^yb~>wKo>j{e%fCub%G)t8ZRCWq7^tN zr{W)9;d)V8j3rE5IZ2qEmP3E%&Hj$YSi;+aK`6t79cs86To(>OE<}v?#I47@EnM+#E zh@;+4nZ^85;+(uwwdLj&IvU+6vzUKYj7#i6%hmJZ#m6LrlxfUAGtR?P<9yh>NJm2~ zWg7EOj&l-Q6?a!pk1NYntYxrs%THte=`mQ3u;sKxW0*3H`De(WQG%9R&XJ>iPMO5~ zljJ-+OU_$PlVia>Oqs;|^W;1{QO>Jp%1a!W3{$2t|71B2&zAF+)8(kYQ>HQhd^rzK znDgoxb0xZC)7UU&8sqtQxoGh8xw2o4o2Rj8pHrr>v3I7C^9;Ju@+9q41N`&yd-TLV z)!<2VwdLknjM05?Np-Gw{Xz7KTo)JVsCG@497He!Oy4+iuqXvo2!AhRdk z@w7X}xeD69^$6~lqltdcN+)Gur`thxtj;k*vv$?XUQ*mY;G>2+7NKsd4hJq@y|}l* zZyoVmuWp&05AxoXQDX;j zJw!2AEJEB?9ka{w)$9J^+47?LEe@OFC=H(xZYk+^dzjwiyXekt9eNBtpA_=|?)d2U zhNH=@cJmeYbKY3F$O}7z5b!Ll@v)pI&kA$+X{_4hG{fj=59)xT{^2mn)X< zc0F`?g=6{p5w|Va*?DnL!PgrX`(b%+5GJg=$n);c=}+w@n;iE>jeFhksIsO+Oms)M zKZ$#Jw-|xZpw}Pw@aPTz-759B!g<8~pd1QnWdGo<&G#(;A z?Jl!qN9M!Ak;C3#*z3amd*#U$rvu&?y~qtYg@db<6*@ImUn z-E|?^#Zoru_Hf~GZyd=hoXb0+KGE{N_vP!?Ly}ie7u7FUs3Mf)0gN$yP^>Gus}xzd zN0y5DWP&5h(Re)c=g4ke=np5o@rW^Lw*nW7TGvgIIG3lD;xM8&!gPue5d^*v0NbZ$8m>B~M(*xI6@x@<=V zVZpRetsik>bXrJS_IW+KtqTj6{XXVNesFHnosNfYDAJbwJ}Tog>37FH+!}82oVe=q zQv9NRw8b+Y@>srpeJFT!eo_5$t%|Ms$$pHj`g`HX6(o|POKog9{5^OOA;Tl4qAk4wx@I{{UnWck3-Dt;spw5WdD zlHV_$b?FVeeO&&*tq;3d@@o&wq_6nouom^B)sIONLw(04t<~24f)#%_9dl6`jYc>f zpW?14dLV7Z9}J2Wf7t8iquyk&ixYj)af>?9>cj<;!lD(Q3|(9MmDw?NmOjK1jn5b- zii5!j#01@#wptHIc9M<@^waJDmt1HfakVD5xTqVgZcLgJ>aj?=aZ&rNt97ayV~h13 z_>oNMqJFgbag;PCMEtnKoKX5Pwpb_oF|tc-eoy%%m=Vs>f`$E0Z?;>IPWg|?-7IO5zmonmbmW3k}5cKX@*pfEDR;)I7@aTG@* zX(#6-E!FPYIiH+OU8+I-*rHU9MEc1&?=4u=iMC+G zWz>ZF5hq6H1OrvDz22nKa7~^!DYSK=GA+bbdt6m#Jdb-5Tw4bH&w)r=?8n36iu?q3 z2jj45Hy7mjbi$%uv<}3IvjT6pSk!*GNX1Ug6TKK)>-WHoeA;4BH`?6DTS1nuUyoBiVvpX$cQQok2&Tre$odu8-eJWd-Hx?yJ+B5sUM3(KFDCweiq((i>AmzbG$ zyih+W2eY@D7nhh5+E)7FEC^TLb(6tvR{COc4NghZR(d|&vuLMk^W!vWQm98GANgr( zf5A#W1y6?c#lPJnb)}#5ij}?`iS(78&nhkIM5_}QObLrl%6aE;z5S$>o{vD)O*G63 zv6X&0ELM7)9E`eCoY$o<^|;lIi*Zmz=9Q;WLMIua{R3w)$&QTyc@6FBY8TV>EE14nDX;WH=fQ!Skf9 z{V)M;3Lo4z((m{1bQ4V^UemYdTNm}Cb$;M$MVB`a8;zys2LxMv`A_#`efi%bS1xfJ z)9%VuCy1omLF?=PE?ik#|M$j~tKJTZCze-?%A^}YYwLfyC+q9~9(i(!&&dEwPQfB`4eBm=`yF42Zs+|wW3h|%f6?8b)#W?l$3?e;Rt-c}ocR5q zY3n~9qi-9Fh3kJGr~LR#aOeHFi&vHIV*Ll*LKEHYbcnn3@pSnv)_=bFc~L)F=LfzV zdU*q}(OBXDqUH2IKbY#t*!sUGu3UA3;M=fQbLA2T5Tz?)>wmHm=As5tq+64eC zT*VVR)pOs(3jlnH#-fh2yE18lh)k7yvql?&1qT3do?QUIqas6`z!sjg1!U<1fc~_& z0MMJ_d=qzD?B)UhUkS3P7p;C=baUv!1Ayi44&^Cl?XkK1;1G{%;Dh_#CzE`FCkoO} z-tlA|!@7@Ghz}+MdtfK|t_97}5%u>~$iX7mB#ab;YlkOCaEZ(V}j4j@K;mK9+ z6vYkGt9o*Y38HQB9u|+n;qf4QmUwrI_n>Es6do>~;!Y@c1t4+p=1YRw7M+Dbx$51b zc#lduf(45=-b7(%@9sAE;(Re}@y7FrG_f-nyEuF2E05azSY3M~J2&19kzgP)Px38D zZ3wDMPGs>O=f#r^SxgS+XLYTy)21)svCM zJJpl1#d}X&xx_5B*Ag4%E+1bp15+sELHVH%#{l!h^m2@ zD;G=+V07u`2n89bND-N@bX`yX0K4Y?p?b2H!RV<

Hi+3Gyc!vYRXxZwu*84dxF7CeifJDx+)oY%|E20vD7B@-+dkmfrN@>1UAMXuG z=)LWq{tk;LY7<5t*O;pN`)C@*UD6Ie#x=kGf1CU|+t^3`jtPw|r3RjLp8znZ%SkLS4)g0>ERYOl-O?fS^q#w0XfiyQoV zLdrf{gU>5w3GGMfgU49G`&=j!BDX$g@v46cfZC*myE#%upf+pec%NoM{MDu{ut{M6 zCe2$pE=VOfpgw|lb6&~_gvV$&172zO&H3iUu%byE?%X)Qv6Qv-o{n@KjE#ayI()>!nBlAg~$vx z?WP=g)D86T(zXN-)MhZe$YiBGpx3iM^jUTVu0>D}|IfV22;fa~DKl84{mDuZ$m`b$ zGgxg8?tyk*Wd`Z>@g|hzBe>pwxIdx&I)3~s^(r$+4-b4Lv|q=MR~;m@AHiQcb7-5v z>g^Bnlp}yhef+8T^5a*>kJoOb>_4?bpxzMAvL-p|RY=Q&oBZzxYQ$`>>hQd3_SK5Pd1dy_X)bR6#hY2%CEdu$<`h@YTw~v48 zN@zb)ANNrw1aQs&dD1a_r2Xki$H4s_-Y~xk{$A}IB54Ne;}!V{?brLy0A2-uzn^jz zTR#NqANP+Zc%U|e^@o#`J)ky&^+&a9-AOYTF8!s9Kz#(qN!JnTGr$Z}+AYlfHv_DQc2CUb|D+BLkUIWJ z68Bigk6X4=&SB~k&?MaXo#C%QvlSaz#(2+ zvC<5NXJ=Q606fc-(0)CF@Hbj14ycU){yQUK1ZwT$F6NcaV);iw3GLVRfDxX@TV)0r z;V(lI+OPK?_irb(U&oI_@Kxexln+-r2Il^g(0(01sHj!q7q5m*IRdErzc)%bhl=3G zY5ejlP<;j(^^-27M-1S<1rkP}b__M*dv=%aLEVFJU_$$m`bleOeFX5Now;YHCcCal*9QlI(N^rY;G*>`3q;rrz5DlA)`*;|5QubZhsrWrLdtCNUvZrKE!0!{Xr)G#Bwrl%Q zo9xBvtVwou7>BZ1lNF!TiOJXpRhRp9qbv;6X_AG%y-XS>)c@i^V6PQ;GIE0y8bEqu96EC-zK zCQ{~eoB1r?U>VZP+h#fQH(7?Sw#^=N!<6M)vg@VPWBo9tKJ%;T<@f)>9QoPh_kU&H zCNoZ?(|nQTO}T+8`X)}u{fhL{JMoNw~6I| z)7?bMd~P$JNLsDj>UhI?CP3MgFN%_G|C=^S(EHxkR7&Lkv$BuV>-%*&)jE={==t?lkH;L z|1EU_-HMK@*J?*PZFYh(wOwgTIzd{|*)Fz9JWe;N6R~4)MrFj^7QWmjmIF?A6Djk# z&3u+`uncMDZL^&Dn=Hdu+hz~CVaoC?+4WNDv3{6RpZV2Pb3V@z+T?p4{Kf^+X_8@| z$59sBO|q+NIt?=M+wmANhe4vI@`roiO1rVmaV+H<2=*+stSA z2Fs9U-ZsmbzsWLuwQcsG8>TGZl3g#Q9_xoG^_gEy-+tkb&td1n-#+J|8D)GgeEKK< zAFudl7e4de&(50UFQ4+0^E+UZbwimp ze+TxNFPRwbyZ7oDF;Kqz{x{Evfq0lNJhx1)oe7U*Vz~N)*UW?`CbKQ-t6g9Im#{CK zl8J%-dEqr*n9C=WnUC*kz6?Aw zFMgsj^}YBhW%SW(x)>;B;!(^{RxA;l7e6!NS6#`($LIY1)DK?2?$T|fvtw6Zzx@k`KR*ARqaUBKo~yIYdd`;RyX~2?M?GPfq5XAX zAqEel@UWintP79BGDF*QU?7%thlhD;m-W;x?FpyZ6fW9y80a7Q$$Z3Z<*|>hj-~3e z=O6loeyXI*lP>GahVo)|U7O|`hrxW~`N#Z?y4FAX8+Pd*8!L{FhodrX9v-Xf@R)BL z7Wv!8pSFRa92<=p^W2v{v$2saVWH1GEH*Zb88=?*Ui{d2vmU=}lYL`-50l|#zQaTR z2m|pkp36K}mvs~eiXUO3U+5=GiHWh~;>X(d@DPW6Lwjt;;j#Gfe6sENisPleY*U6E z`j_KHJc^szxUr0Fqt2XlT>H$keCD|__K)q%J^$F)2$M@~ENN4=n5qqjM>ymk;xj*a zc&rc3hV_m8a(2~6!}NPsoHYBxhqq?m@tn`hvVS=9e5Uu^``+UC>FZ~n{}9S~ob|x` z(@3-I^Zz%qZ606U|0nNVe=_UgQ>0rw#+p40dn~7&cmMsf*E85T?dG?vGoRaqeH@2< z?acE}M?UTIxbH((F-88vcc1r*2hL?azMEE$`K3KHVs=qZ{Db~C4gZZdZmbjkX*aJM z4(1aF^EdzEf!UA#Ws!f!uY7Z+?E?3P>&FJ_Juo{R^^1OI>!$)A?XW%CWlDR*!|eip zw*I;goxA?Ar*EvE`f2uOXS=|;QTV|f-xjzIn4Pn~I5&S0H~qtO@N4fe-0~ySM?U<( z>@$D!-dgJXK2!SCo402NzW5_ErCfpv~Aaa;hqOlj|~R~=YC8+uBA>~rU?f9dS4 zS(q01i~7Yl9Vo|xcAf?N^8eR9oSFZ7@YiX;@vc{8@KKTe2MoP}wDJ5wB) z|21w*iJvKPDE&Ixq5liLBK?WqI(PkpCui$nTHw!||FiXXq2G4_m(nbILUu9+>|}5P zj{}$RMEpK6dwfP_;Ys*?61WBO2xs8;3~&ag8nVH#ndo6xno4q=FRdz*o8GbLzUY}i={b2S+ z{Jt@JOZEfVwb`5S`=;y-*;Uz_v#arYb#@JUl)*D+;rlG~hK$SE_&yU}ItAV!m-1wI z=o#?Bci?*x{PSG&?o@owgjc>Fy?Q#nr=h3c4G%mI-wt~D99Z~1e4h`5XTf8y#P>Bo zw+5|O<9i9LTnSXq#rJ#Rp*I7~3-Em&5WWh?e*oWWf$nwCyawN^;IUVtr(Jye@YWU3 z8sMA5$_4P!mH7T3diDx<=MDJ&06f-%CojbJrSR4inlHl_yB%ztkKgkV7mtU}d-(04 zpBJD{PsQ(3;rn&O!zuVZ1@+HDyIuTtQRgJ|^ZEFFK6=(e*(vxv1$D1PKTgH(si^%y-XS>)c@i^V6PQ=aw3|@YaZrS>Q6nY1o?j};^bDR0p-(VTi%-d!;^EX+B zuePbr`eDj)=7%ZuSU*gu$NXygbDud-##@`L7^6;;tay_h#!@INu4G3#%KsO)ol)Xx zlZAmgO|tOF4%-T4zulM&pSjN#{fAE(C)>rg|6A$=x)mK)uhoup+Ux{nYP-^wbb_>^ zvt4YJc${ujCt}CqjLLqTLeJt1*_)8J6sM4>zd<`*oN}A`OgCBPZBw81!<72W4^!%~ zewb2^`PKA~*S`Gk87G|o+hoNUb(&9^ z3T40Dm<*q}&ldfMPZ=lM#kT)j>IAwK9apc_j&$1W1Z8Tw(w20Bw4$?JY?XMNZd4~? z$Ks62ew;$j;tbiFkhc`4kg2~xJ6@b}oBB*QS>|n1pY_9(`pgeg>al*9Qjhu7^iS_P zQpQ`GtQezCldO1?9mZQIE5>9;I?8|OK)1xxCJO^~nq=XT9kvz9e!DRlK69Tf`VXHn zPPU6}|F_f$bSpZpUaKAHwAl&D)OMvU=>%y-XS>)c@i^V6PQ;GI8I}Dwg`UM3vNs`b zDNZ3%e}i_sIOR6=nQpSo+onG2hbi@$AEwk}{V=5-^Q-BT|Lm84_itZ+|AqhVl80w) z@-_EdJd;k7Ogk^gf3LI?lUY~!G5PZkzh)*pZL%;>r%4tb*KAAQpM#aqoKgO{2A>NW2#dC!;(UdHT*-=s~+v?D%LGBLdS%s(u7*#Gp) ze^m0Y%om=&`RW%+Jd%m$%OCtgi6B4!%}8Gd06&KbtTI`!a)CoGV#!k@c1(AsE?9? z2lq7<@k3ruF~fYt5^T;izP=1RGcRVMGWETfDP{6{v?(rCGTT+mP$qtNUxCRkerCk4 zx{`^$4S5`on;}nR9FgORDtw;fJ?G**#RZ|h@j|TY?1_`1J>#Ps2J%7V%?t}tZVRLF zPPF0SF|O!wH+_7L@9gR~mAtTcH1fjY(bNy)ifG4iMAklSI-JHEISj_5c|I{esm$vO zZL2Tj(d_H-p4-k}mO9>w?OHw8uKC*Yxwtg5>-j_&%r~BY%%1a)>e4^trEN?&KCyB0 z@R)BrKSal$82z!)m@&_NZTvhev};)CbF^1-dW;p~Bj@ROJ^IeZ!1?D}&ciB3h{0jA zl-nv}Ux=Ok;FmJJgj`+;%85JSmVRMi!zPzun(@n!@}-j z9u~)C;9q(CM?N-{hb3hwJJM;h6O^g#N?Xzi(u&S@u~p)6x>22o9lPE^ zWz45r_;Q)hBWiGSyvj^QUW%-uudMWi-KTN65{Ax-*RPoj(E5@kPBrD!z zhw&E5iZR)dj`GQq3QwCX4Ag0og-3SSRw(=J#$@=+eYWU7e9Ac4F1G#OQYX-@=(u{V zcBIp0Cn!_fmA0f4q!pd*VyndCbfY>EI~HeDM%-=T%WYyg;B+^UGN0SbXZZ%pkY?UC z%bCB)GJLgd_MjW4EZ>q{FQp#qhbi@$Urot}D&E>;#Ta#(WW}58Fy2C0F(y0GQ9gN6 z;c1hFfjUjH@W>9^3T40Dm<*q}&ldfMPZ=lM#kT)j>IAwK9apc_j&$1W1Z8Tw(w20B zw4$?JY?XMNZd4~?$Ks62h`TL(xlJqwobD!4=5w3*EZ<-m(#+dtIrBGJhOf5G9(2Q$ zb9`^oo{zkmjoU(Y&%P#w>c&oW&@G|qOKlas<_l(KlWz3HF zP1=-9JK{qn6VI8Se8#$-hkfuHXRQ+hd12-Y&+mQYtaZnu%_S4hOK(1FUC+bDWa6Q| z+Vy4H7dFZA54kF0Ab!VNnJgZb_{C95mVX>?Wxfh!`N#2AX47$1=I2l*9{NNWh*LZ) zW#*HIWxrHcviu_q^iL=g5A6t#FVl|tC>eNgA4m~D--{~(Rh6?eynZQ zRejdwSKCa5(cv*3-(fm#d05Xs3LYNLr$%WEvM8IxI8`7xP1 zlrXf(!a$uSS$JfJ?S`^2$c}WBkNw4cws`)8PkElWU2OZmrB0w*(Q)-!?MSE1PEe+{ zD{V<9NGm$q#a4;O=|*)TcI-Tp%GhVOY`^8abiADZl7~{fwaJPx>NLrUH`!q9aoCJO^~nq=XT9kvz9e!DSwCkhl_Wt_OU+0iJ*o!g;qMaR`^wIdzG5kJx` zwu;}d4Q)qtWXGiy9kr#jO~;k_Z5`k7PaaC~)+Q^)sM91X-eiaI7Rrh-*^!R&$xjJS zn=A~}X_AFUcGy-Z`|ZZ$ok-D`D;AaRXn1k2IB_vm(Q$dIuQaODW+x~UzKR{`1ZhQQ zyVw$rs2%A<>>S_mPaaC~)+Q^)sM91X-eiaI7Rrh-*^!R&$xjJSn=A~}X_AFUcGy-Z z`|ZZ$ok&q!lyTx>Vn<^?{*S-oAM#&c{N;ZrxveJo1CKmyUC$vl$*e1Gt4U^E>BMC6 zP{PnA3j=kUWMPmUaa(@AFvyN{l)n?*`|)4jzv-Rhd;Z7u`S}MPd5-Y{l>7bf5no`k z_@=E_uZb_1OUCu^`4ir`R`N|T8P~(-1K1R=ZCLa1k80Zu6UzC|o{)_!mUCHu~Fwj4tOg!wn@c1(AsE?9?2lG!6 zKjc&BAC3|86-%%=)A;%_@XWlJiOSUXVy2YIr_iQ2EXizFF+*9gL~LIC%!prgB@>Sq zGf|m%6dR#TJL112%Rg?s%qQZ%6ie!h8!zh@W%|+yHm_%sb0F_g z;XlZo@GBn0@jT=U$T!(Df$rM3)kS??`=%|sChoSyUD!2pwMh(bzHdDLZ@=_I>mT{e z<)1_3Gl=9Wa6P_!AFOckn*A9szkmMLx7@t`_m{5Cj=bgO?eBW|{j-07tZVe_qi_+Y z=O6jWeB}Ac_%8L0sru~shkOD3WU1qu?72YcvYu=xkE!xx*Yk;Rkl*2RZ5%JdD<6Ax z$;WU!=FUGhZuAE+EyI6#eltw4XK}$c)i%e>`Gq(P3-hS!urM_&9Dqf9)0}=GCg#ER z-1U#`%su}QhkQaD<{M)3`l7nl2FF8V$>1ML~R%NH`bN%cy>3> zA%^dP)%#%!oEz&(d4HGQV@keG?}J^$fBmg{A8eQwxVL6X^&VKItG*A``FW<4*Yyit zu1NJh*hTzT`23-^3*4FWe}?Nzd&_?nxHISfo%g_gyWR);LcAOFh3|cjD5wF9L z4^_Oi$%--RG|7rL*eV z=A7gEIwbi}#ao-K7^6;;tay_h##<;W#$-o2$|p}MJZ-WtP^U>29@$}Aq3pLCllSAF z9w@mt94FhhODP}U@lQTf@zy3Q#;DUIE8b*>@fOO8G1-xh^2w75Pn#?Z)M=81M|Rj& zDEsZkWE*b#*+2gA*1!`c{>j5W_>J!^Io2i_??)|lnq<}$$J!*bu5@BD`A}hKlZAmg zO|me^jyP67Ul?RZI?CUVe|q4=o&Ukh;5o$0#huNc@@H3+JS=7Lo>%?-<>Ia8lEKT& zzx2#2O5QUjgO@Rz;x}niGVO>Dl}tPje&eo^hrQ#%A2tl+K$$N*@BH(3m3Sl*&(}Zo zt`bj7CLZdmU0~Q#lsT6I7-R#kK?V(SD`HbINr)^IN7ZE?Da${?K>vg?@z4(SLz#BeN6Em0`KO2<@_LFH<|~$9XQuJ> zW#E~4F%y-k@5M|hlgpz`ajBBou40BV`=aL%DZBWY5x?q6CLS+lqB8N&j*F9uOgrLX zCCfirXyBsg}Mc632csBot zhh<+V%Rj>4#gF-l`SOo2(5CuDJE1II$nh}73z458AHy={lfNOCLhi)zI^>D;{!V*_ zjT{WW=v`9i6~NZ(kQv&lN_CvDP}@nqs$J^zrmGCr1h(q%o@ZkZRe z>)KrRFwBU{^N+QSy4F93iT<(i;h3m>=c|(E^YB<*&ku$P+w`0IXmJ4?*1k)H%f?Aq zJuI|qSYU_oGZP2w^RU82tgNd(sC}2mGGeKWrH4Zph;Jz#mhCzJ7_aDX5aYMyeXyQ? zIChF}@t!uGo`1x9+E_6kzEMod=ZsnNhsK86lwE3L>FR2%S=YwE^AF>faYR4aJ}-Pu z*V@+DAdfL=W9cxJaj+~8tN-KF^N2j}|5pAl=UzXDh=bV5^M`sav3UONvGYFI{pS$( zmWN&De>tCs;CQ0@U(S=W$7LsHyZc|xXXF1FpB?#M&U5fTjOTnC{+IK$_|M4Ky8q?8 z6#oNx>Hhz6f~$V){x9eM`<1ts9BY&Ow%NLr$%WHk)VVh*ul}=11A1VxOvM^Am zNfrj#VY{I$46-8~_|uXVB)BfiGzR&;{;Q9IIUvlEnQ|E#nnogl5~ zY!_R?5w#qY`cnf94nCwVL`Q%B3r%e_H>NLs1BRgy> zl>K&N@=l~EF3LD@F|nhuAOBm&-TM&8hbrFMWW^YDnq@eO!SurL%(osHnQsHTn zg@HOvvhc_b+X`jB-I%-+-TU!h-@oad<9q%m4}0&uSFh_?*Cx5&zq8b7l37~n z(uv9BLxrJD76$4x$-*Ez;#mEBVUQi^D1Sfx>49?1JK>Ihz{||pRh3^+{ zWwLnBpZ)#!iMN_d1}`)J&tG|8$$Q3R@G@pc{3h*7rXBI2l8NE-4}ZAiVLx`qM@k-+ z`NH$@jgORgBoogKFaAi0CnggQ_0_H~KMecACRzS*yp{FE@m3~_hb4Y-l#=Bi$6J}N zLRtQCyp`E>T$TAbl!=Ev5eE81JS=7AlZR!$R9CY6BMkIUC=(C+Em+AYewpWNiu1B-t`!JAMB+cI(PkJPv2PAbFtbkaBi$C-R<+R zkGSVy!?fU$i~sY)ar#Wp!75$$dDtKQ+_~#tI(ustrq0hZrMrC|R_}ieQ@saPDdTE4 z?}PpE-#T~wgC}R}VOrqMod0+KJnW;|5Hy#U{r<4M?;;BMC6p~BE63j=kUWMPmUwj0XAAUo1g{{D5C6Z<+0`B24M zo2(e4PLr&7lO4uVC@ZdHM>@(UPbxfZvM^AmNfsX2VOyc>w;Pl9DWZ zMx7>E@g_Trw@_A$$&PfCPo7kG+GJs%PLnJ=vctAQ*>5){@5ld%9slG*6>n{_VvIUX zvf@p47;mAh7?U08D4#s3@U+RoK%FL8cw~odg|gpnOx}L#&UmT@m z`N#2A=BrSae;jXRHXT=Gehy{gp-+T?IK{(KWFPZ2-l^%OJAS1iHiOyldzz%%n=CMr|ki)vy_&G!S7qXU0qT#ds(ZZDaKC66dh$nl&rZhxzJSg$X0uP=|e=V3kn*t3-4K0W^! z-%9?F{ot1}$A({Q>lsOom+_;F8^=$VW5zu9opT)Bdu}z>B@auUQ5>*%JdT%mbmCJx zY+LQSJj3Pk6hqdoiydJwTz;PG6YErXQ-{YmMYiqyqxU3>$7jE-uIdvHzuIOhjGlkE zZxIuHvNR9t{A2eQ0FQC`o_{zlHV$aB!ozw#pF2FU2DVh zkMI~b%rVpWsox$R8z;}_7E{8s|2!<;tGxR2u;O3!K4PWC^Ozg!N*}8{?2kDfHcSiL zTQjAPRUY;y91k0&1@6rGKhDEGn$N>Nn(rlE=6Trt_Y%J}dl~rd9KSi9md(I{pO4@3 zvnSxG=<^;`9`;Y~`jwJnZIVwt=~qjgCYg14t&coxlgzr(iOJ+cg`rIr2I@4)!XP_r zHp{y8_9qA~aJgM-s$-+RLCRuo7hi!$j z-)>CakN*=p{>g_b-r8iv7%=)d){{Ir^H*$C4-ll|N8qsRq~!O8N7_y z5x+^Bl4(bLsAS^#*c}g*JnY?PezxRcnJ+v)^4AZQcq9|g1z&im#1oT=hx%&QmuX+v zB+EaJx3a!C-pXY0u*5HpQnLKxcq{W&D9b;Nw=$cKt1>@_GV#zS!a$tjVJS18JS_XA zx{~D|VW59PnRsYNczl_5)JMs{gJ&>{I3cg6m|?zR2{vaMUtb2EnHMurnfhMLlrni( z+T?ioGTT+mP$quyu$0Ba62Iz7CLS+lqB8N&j*F9uOgrLXCCfirXyBsg}Mc62dhb4YD)-%cSk1%-gW4>a({38ss zseaK;D2o^J_!#47$j7jZ`Q&fFuNWsonH&rG7RT$=-b?IwJw4B8{115-)^oOun{nGS zXODWuQ?X5QMc#j*8b9PP3J>dv7c}0>;W18#&kFMSMLs*|@r;k^|HFFz5$|a{nz%KV z)35TW=O4>+J~yAqM;sIJXq26=$m?;u%zyH+?9orO#rDXTv3<{1hRN}c!ee9N>f2a3 zu9^RzOWz6bG)bicX&_!AJ+4acuyNE&p#Y5`KQ9eGT-@}<7MMdKc@aaES^33 zKfW%0>>daBYc4-Arab=$kHwGt$f){zIwLB(pBBvB+jjW?kjSWb#nL&?XB5 zb(&=1ksY=h%EBN!(oz2Y`R|E+{!1Q8@zy3Q#;DUIE8b*>u@uURE7_5b^2tvLPn#?Z z)M=81M|Rj&DEsZk@(UKP5bEvM^AmNfsX2 zVOyc>w;Pl9a_<;tS@IaXozgbAR%= zl5dL1xE?+qz@~U5U#1=LP?Cw^o(KQD6re;5lC@@x)}} zp}yMnW!e``$?}im3(O~uFECmB7x9Z5k}Ur?zQBAH%JPrn3(QxJBQQUQGV#zS!a$tj zzbG@G{1^MBx{~D|VW59PnRsYNczl_5)JMs{gZZb3AMz=TAC3|86-%%=)A;%_@XWlJ ziOSUXVy2YIdC{ggEXizFF+-X7#eY#2|3&<&E17t_n2E~7Lpv@`Dl+Ye|B@{KxbZTd zi2tI0+;~}E+<29;{Nu*R{1eLZj~g%Zk7UK6d@lY=@x$?AUxbaa_%GrY|0P-e5e6@Q z%va2pe}sWH)i2r!WpEYbQ%df{Z7=m4BA<(L&ql5587cB8j&E9-{}LARDIP|<-tVsO zSNJb+CgcY^T*70|k$F7M%)N(p#`n`qwH;x0xi~b-FXR@~aJdcYp{>u5s__)Us|K<6|-jnP6Bfd%TBmanR;@C&W zpK&37AZ0wuPnPe>wmmLy-o$?qo1TY!H2AO2&OgnEbh7Y$uupvs`J*g5%~Jc%5WFwE z_+Oa&?tO3Z{MXmdJpZ(t-?Gkp@)6AE^K9AI&OHBg;K|`*^mC^I{(kW{-0CZryK5#b*1EAPQQ75{h=#PV!zf;1wOu?l(o1o&O)~598jEbkWY$%FOePN{3~jP7P^U>2 z9@$~Lp)3rtBOT>)-!4A3Z$m8q8)RE0J87xI{R|)J7F)%y^1?K#(`F|qQ`;3g(h1Uv z&US#y=|*)Tc8>4=10oNlcx#gtW7KJq6>qY`SPEsumF!4I`Q)dBr%e_H>NLs1BRgy> zl>K&N@=g>ezFOlph!5%RSlQVpRHihlBYWF^X(89}I9*?})RCQ-j9^3T40Dn7k7Oii(Yvjvn{MqJJN~RIlkkcJe1dlc^E{6G z1ATSC?JJu|na^$JvwVYPNHcGn<;>q?8NS*!d(aJ2mT$?fmr{@Q!<72WucqX`?mqJm zOKz)4e#KvXsnltbSy$Xvlgzr(iOJ-lgrQ9q2I@4)!XP{1w)}iykR9nLA3k%RE&30i z{rE59xbTLHGj1U67>|rQ`pV)A*_)7=&u!+jd;@8QH1oDu&iqZ5;j3-42i-7b`IhW@ PDfL)COsUWOYWn{HyI|5S literal 0 HcmV?d00001 diff --git a/game/assets/store/server-rack.glb.import b/game/assets/store/server-rack.glb.import new file mode 100644 index 0000000..8ff1081 --- /dev/null +++ b/game/assets/store/server-rack.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dfj8ngisb8pji" +path="res://.godot/imported/server-rack.glb-e13aeaf7bf0cdf2cd49217c33d8e2448.scn" + +[deps] + +source_file="res://assets/store/server-rack.glb" +dest_files=["res://.godot/imported/server-rack.glb-e13aeaf7bf0cdf2cd49217c33d8e2448.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/shredder.glb b/game/assets/store/shredder.glb new file mode 100644 index 0000000000000000000000000000000000000000..a8b5dbb5454119da352b9416da002f19911fe3af GIT binary patch literal 16836 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 literal 0 HcmV?d00001 diff --git a/game/assets/store/shredder.glb.import b/game/assets/store/shredder.glb.import new file mode 100644 index 0000000..ad53114 --- /dev/null +++ b/game/assets/store/shredder.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dtjftptqfqw2j" +path="res://.godot/imported/shredder.glb-e7be012212295ed44f5a7a52ad82e1b0.scn" + +[deps] + +source_file="res://assets/store/shredder.glb" +dest_files=["res://.godot/imported/shredder.glb-e7be012212295ed44f5a7a52ad82e1b0.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/sofa.glb b/game/assets/store/sofa.glb new file mode 100644 index 0000000000000000000000000000000000000000..f7ba1fe9d580d5bd5ec7a46d2721e6a12a47ee5c GIT binary patch literal 41752 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 X^Ztu>K>7Rb&igO&8aedMy#M|`D>T@W literal 0 HcmV?d00001 diff --git a/game/assets/store/sofa.glb.import b/game/assets/store/sofa.glb.import new file mode 100644 index 0000000..0e0846e --- /dev/null +++ b/game/assets/store/sofa.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dinkdfb8jin4m" +path="res://.godot/imported/sofa.glb-c2ee6c7bd1e23613e6084f217cc88f31.scn" + +[deps] + +source_file="res://assets/store/sofa.glb" +dest_files=["res://.godot/imported/sofa.glb-c2ee6c7bd1e23613e6084f217cc88f31.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/stapler.glb b/game/assets/store/stapler.glb new file mode 100644 index 0000000000000000000000000000000000000000..b8324c124b812634998dfe2677fbbf281a9671b1 GIT binary patch literal 14328 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 K{lD?|q5mHcEY>{$ literal 0 HcmV?d00001 diff --git a/game/assets/store/wastebin.glb.import b/game/assets/store/wastebin.glb.import new file mode 100644 index 0000000..971ec8b --- /dev/null +++ b/game/assets/store/wastebin.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://baxe2k4e2nkxf" +path="res://.godot/imported/wastebin.glb-fbd43ede5b2ede38a8e605f1a922673f.scn" + +[deps] + +source_file="res://assets/store/wastebin.glb" +dest_files=["res://.godot/imported/wastebin.glb-fbd43ede5b2ede38a8e605f1a922673f.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/dev/DemoDriver.gd b/game/dev/DemoDriver.gd index 7b6ecc6..d7dae01 100644 --- a/game/dev/DemoDriver.gd +++ b/game/dev/DemoDriver.gd @@ -48,85 +48,33 @@ func _find_player(n: Node) -> Player: return null func _build_script() -> void: - # Office floor plan (see Office.gd): spawn at reception (-8.6, 6.6) looking into the - # bullpen; desk clusters at (-4.6,-1.4) (-4.6,2.2) (-0.4,-1.4) (-0.4,2.2); copier - # alcove at (3.0,-0.3); break room x>5.4 z>-1.2; conference and manager along z<-4.4; - # warehouse corner around (7,-6). + # A lap of all four sites. Each gets a settle beat (nothing must move on its own) + # and two vantage points, so the palette, lighting and layout can all be read. _script = [] + var tours := { + "scranton": [[Vector3(-8.6, 0, 6.4), Vector3(-4.0, 1.1, 0.0)], + [Vector3(-2.6, 0, 3.4), Vector3(-0.4, 1.0, -1.0)], + [Vector3(1.6, 0, -2.0), Vector3(8.0, 1.3, -6.5)]], + "pawnee": [[Vector3(-8.0, 0, 6.6), Vector3(-2.0, 1.1, 1.0)], + [Vector3(-1.0, 0, 2.6), Vector3(-2.6, 1.2, -5.2)], + [Vector3(4.0, 0, 0.6), Vector3(6.5, 1.2, -3.2)]], + "house": [[Vector3(-1.0, 0, 7.0), Vector3(-4.4, 1.2, 0.6)], + [Vector3(0.5, 0, 2.0), Vector3(6.0, 1.3, 6.0)], + [Vector3(-6.0, 0, -2.0), Vector3(4.0, 1.4, -6.0)]], + "basement": [[Vector3(1.0, 0, -5.2), Vector3(-8.0, 1.3, -1.0)], + [Vector3(-4.0, 0, -1.0), Vector3(-10.6, 1.4, -1.0)], + [Vector3(4.0, 0, 1.0), Vector3(9.0, 1.3, 5.4)]], + } + for id in ["scranton", "pawnee", "house", "basement"]: + var shots: Array = tours[id] + _script.append({"dur": 0.4, "move_to": shots[0][0], "look_at": shots[0][1], + "act": "level:" + id}) + _script.append({"dur": 2.2, "move_to": null, "look_at": null, "act": ""}) + for i in range(1, shots.size()): + _script.append({"dur": 2.4, "move_to": shots[i][0], "look_at": shots[i][1], + "act": ""}) + _script.append({"dur": 1.2, "move_to": null, "look_at": null, "act": ""}) - # ---- ACT 0: SETTLE CHECK. Stand at the entrance and touch nothing. If anything - # ---- moves in these five seconds, the level is falling over by itself again. - _script += [ - {"dur": 2.5, "move_to": Vector3(-8.6, 0, 6.4), "look_at": Vector3(-4.0, 1.1, 0.0), "act": ""}, - {"dur": 2.5, "move_to": null, "look_at": null, "act": ""}, - ] - - # ---- ACT 0b: CUBICLE HELL. Sit at a desk, do the spreadsheet, get wound up until - # ---- the meter snaps, then wreck the place until it drains. - _script += [ - {"dur": 2.4, "move_to": Vector3(-4.6, 0, -0.55), "look_at": Vector3(-4.6, 1.0, -1.4), "act": ""}, - {"dur": 1.2, "move_to": null, "look_at": null, "act": "task"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "type"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "commit"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "rage:0.30"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "rage:0.35"}, - # SNAP — the vignette closes in, veins crawl, heartbeat goes. Now wreck things. - {"dur": 1.0, "move_to": Vector3(-4.6, 0, 0.3), "look_at": Vector3(-4.6, 0.9, -0.7), "act": "weapon:2"}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.6, "move_to": Vector3(-2.6, 0, 4.8), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"}, - ] - - # ---- ACT 1: a walk through the floor plan, so the rooms read. - _script += [ - {"dur": 2.2, "move_to": Vector3(-8.4, 0, 2.0), "look_at": Vector3(-4.6, 1.1, 1.0), "act": ""}, - {"dur": 2.2, "move_to": Vector3(-2.6, 0, 3.4), "look_at": Vector3(-0.4, 1.0, 0.5), "act": ""}, - {"dur": 2.2, "move_to": Vector3(-3.0, 0, -3.0), "look_at": Vector3(-3.4, 1.4, -6.4), "act": ""}, - {"dur": 2.2, "move_to": Vector3(1.6, 0, -2.0), "look_at": Vector3(8.0, 1.3, -6.5), "act": ""}, - {"dur": 2.2, "move_to": Vector3(3.4, 0, 3.0), "look_at": Vector3(8.0, 1.2, 4.0), "act": ""}, - ] - - # ---- ACT 2: the loadout, held still so each grip can be inspected. - for slot in range(6): - _script.append({"dur": 0.5, "move_to": Vector3(-2.2, 0, 4.6), - "look_at": Vector3(-2.0, 1.2, 0.0), "act": "weapon:%d" % slot}) - _script.append({"dur": 0.8, "move_to": null, "look_at": null, "act": ""}) - _script.append({"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"}) - - # ---- ACT 3: the matrix. Cutter shreds a carton, clanks off a filing cabinet, - # ---- then the sledge takes the cabinet out. - _script += [ - {"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:1"}, - {"dur": 2.0, "move_to": Vector3(-1.6, 0, 5.3), "look_at": Vector3(-2.3, 0.25, 5.2), "act": ""}, - {"dur": 0.5, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 2.0, "move_to": Vector3(3.1, 0, -0.9), "look_at": Vector3(4.5, 0.6, -0.9), "act": ""}, - {"dur": 0.4, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:4"}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.3, "move_to": null, "look_at": null, "act": "swing"}, - ] - - # ---- ACT 4: the printer boss, then a lap of the HUD styles. - _script += [ - {"dur": 2.0, "move_to": Vector3(1.5, 0, -0.3), "look_at": Vector3(3.0, 0.95, -0.3), "act": ""}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 1.5, "move_to": null, "look_at": null, "act": "swing"}, - {"dur": 2.0, "move_to": Vector3(-1.0, 0, 5.6), "look_at": Vector3(-1.0, 1.2, -1.0), "act": "hud"}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "hud"}, - ] - -## Total speed across every physics body in the level. During the settle act nobody has -## touched anything, so this MUST fall to ~0 — that is the objective version of "is the -## level quietly falling over on its own before the player does anything". func _motion(verbose := false) -> float: var total := 0.0 var seen := {} @@ -215,6 +163,10 @@ func _do(act: String) -> void: if act.begins_with("weapon:"): _player.select(int(act.substr(7))) return + if act.begins_with("level:"): + if _main.has_method("_load_level"): + _main._load_level(act.substr(6)) + return if act.begins_with("rage:"): var r = _main.get("_rage") if r != null: diff --git a/game/dev/probe_levels.gd b/game/dev/probe_levels.gd new file mode 100644 index 0000000..511e50e --- /dev/null +++ b/game/dev/probe_levels.gd @@ -0,0 +1,49 @@ +extends SceneTree + +## Build every level in Levels.ORDER and report whether it's structurally sound: +## prop count, missing assets, and — the one that matters — whether anything is spawned +## inside anything else. A level that fires furniture across the room on frame one is +## broken whatever it looks like in a screenshot. +## +## Godot --headless --path game --script dev/probe_levels.gd + +func _initialize() -> void: + var main: Node = (load("res://main.tscn") as PackedScene).instantiate() + get_root().add_child(main) + await process_frame + + for id in Levels.ORDER: + main._load_level(id) + for i in 8: + await physics_frame + var plan = main.get("_plan") + var bodies := get_nodes_in_group("smashable").size() + var statics := _count_static(plan) + # let it run a full second and see if anything is still moving + for i in 60: + await physics_frame + var motion := 0.0 + var worst := "" + var worst_v := 0.0 + for n in get_nodes_in_group("smashable"): + var b := n as RigidBody3D + if b == null or b.freeze: + continue + var v := b.linear_velocity.length() + motion += v + if v > worst_v: + worst_v = v + worst = "%s at %s" % [b.name, b.global_position.snappedf(0.1)] + print("\n=== %-14s %s" % [id, plan.level_name()]) + print(" smashables=%-4d static bodies=%-4d" % [bodies, statics]) + print(" residual motion after 1s = %.2f m/s worst: %s" % [ + motion, worst if worst != "" else "-"]) + quit() + +func _count_static(n: Node) -> int: + var c := 0 + if n is StaticBody3D: + c += 1 + for ch in n.get_children(): + c += _count_static(ch) + return c diff --git a/game/scripts/Floorplan.gd b/game/scripts/Floorplan.gd new file mode 100644 index 0000000..fd85653 --- /dev/null +++ b/game/scripts/Floorplan.gd @@ -0,0 +1,458 @@ +extends Node3D +class_name Floorplan + +## A workplace, built from a data spec. +## +## This started as Office.gd with the Dunder Mifflin numbers hard-coded. Lifting those +## numbers into a Dictionary buys three things: each new workplace is a data file rather +## than a new script, levels can be diffed and tuned without touching build code, and a +## real *generator* becomes possible later — a BSP split of the footprint emitting rooms, +## doors on shared walls and fittings by room type produces the same spec this already +## consumes. +## +## (For the record: `doratracyer/floor_plan` can't do that job. It's OpenSCAD turning +## hand-drawn SVG wall paths into 3D-printable STLs — no generation, no plan parsing, no +## room polygons, and GPL-3.0. Levels.gd is the better path.) +## +## SPEC — every key optional unless marked. See Levels.gd for worked examples. +## name, subtitle ...... strings for the HUD +## bounds * ............ {x0, x1, z0, z1, h} +## palette * ........... {floor, wall, ceiling, trim, partition, wood, accent} Colors +## floor_style ......... "carpet" | "lino" | "concrete" | "timber" +## ceiling ............. "tile" (T-bar grid) | "flat" | "open" +## walls ............... [{a:"x"|"z", at, from, to, door, style:"solid"|"glass_top"|"half"}] +## windows ............. [{a, at, from, to, y0, y1, blinds}] +## lights .............. {xs:[], zs:[], color, energy, style:"troffer"|"pendant"|"strip"} +## sun ................. {color, energy, rot:Vector3} +## env ................. {bg, ambient, ambient_energy} +## slabs ............... [{size:Vector3, at:Vector3, mat:String, collide, shadow}] +## props ............... [{glb, at:Vector3, yaw}] static, gets a box collider +## clusters ............ [Vector2] facing desk-pair centres +## cluster_gap ......... metres from a cluster centre to each of its two desks +## chairs .............. [{at:Vector3, yaw}] non-bullpen chairs +## smashables .......... [{glb, kind, at:Vector2, yaw, frozen}] level-specific breakables +## copier .............. {at:Vector3, yaw} +## spawn * ............. {at:Vector3, yaw} +## +## This class owns the SHELL, the FITTINGS and the LIGHTING — everything static. Main +## owns everything smashable and asks here where things go. +## +## Godot 4.7 GDScript 2.0. + +const P := "res://assets/store/" + +var spec: Dictionary = {} +var _mats: Dictionary = {} +var _spots_desk: Array[Transform3D] = [] +var _spots_chair: Array[Transform3D] = [] +var _clusters: Array = [] + +func build(s: Dictionary) -> void: + spec = s + _materials() + _shell() + _walls() + _windows() + _ceiling() + _lights() + _slabs() + _props() + _bullpen() + _chairs() + +# ---------------------------------------------------------------- convenience +func _b(k: String) -> float: + return float((spec["bounds"] as Dictionary)[k]) + +func _pal(k: String, dflt := Color(0.7, 0.7, 0.7)) -> Color: + var p: Dictionary = spec.get("palette", {}) + return p.get(k, dflt) + +func _materials() -> void: + var rough := {"carpet": 1.0, "lino": 0.55, "concrete": 0.92, "timber": 0.5} + _mats["floor"] = _mk(_pal("floor", Color(0.30, 0.30, 0.32)), + float(rough.get(String(spec.get("floor_style", "carpet")), 1.0))) + _mats["wall"] = _mk(_pal("wall", Color(0.80, 0.78, 0.72)), 0.96) + _mats["ceiling"] = _mk(_pal("ceiling", Color(0.88, 0.88, 0.86)), 0.98) + _mats["trim"] = _mk(_pal("trim", Color(0.13, 0.12, 0.13)), 0.7) + _mats["partition"] = _mk(_pal("partition", Color(0.44, 0.44, 0.40)), 1.0) + _mats["wood"] = _mk(_pal("wood", Color(0.42, 0.29, 0.17)), 0.55) + _mats["accent"] = _mk(_pal("accent", Color(0.55, 0.20, 0.22)), 0.6) + var g := StandardMaterial3D.new() + g.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + g.albedo_color = Color(0.66, 0.74, 0.78, 0.15) + g.roughness = 0.05 + g.cull_mode = BaseMaterial3D.CULL_DISABLED + _mats["glass"] = g + +func _mk(c: Color, rough: float) -> StandardMaterial3D: + var m := StandardMaterial3D.new() + m.albedo_color = c + m.roughness = rough + return m + +func mat(name: String) -> Material: + return _mats.get(name, _mats["wall"]) + +# ---------------------------------------------------------------- primitives +func slab(size: Vector3, centre: Vector3, m: Material, collide := true, + shadow := true) -> StaticBody3D: + var body := StaticBody3D.new() + var mi := MeshInstance3D.new() + var bm := BoxMesh.new() + bm.size = size + mi.mesh = bm + mi.material_override = m + if not shadow: + mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + body.add_child(mi) + if collide: + var col := CollisionShape3D.new() + var sh := BoxShape3D.new() + sh.size = size + col.shape = sh + body.add_child(col) + add_child(body) + body.position = centre + return body + +## Static dressing from a GLB, with a box collider sized to its own mesh. +func glb(path: String, pos: Vector3, yaw := 0.0) -> Node3D: + if not ResourceLoader.exists(path): + push_warning("[floorplan] missing prop %s" % path) + return null + var packed := load(path) as PackedScene + if packed == null: + return null + var body := StaticBody3D.new() + add_child(body) + body.position = pos + body.rotation.y = yaw + var n: Node3D = packed.instantiate() + body.add_child(n) + var ab := mesh_aabb(n) + if ab.size != Vector3.ZERO: + var col := CollisionShape3D.new() + var sh := BoxShape3D.new() + sh.size = ab.size + col.shape = sh + col.position = ab.get_center() + body.add_child(col) + return body + +static func mesh_aabb(root: Node3D) -> AABB: + var acc := AABB() + var started := false + for m in meshes(root): + var mi := m as MeshInstance3D + var a := mi.transform * mi.get_aabb() + if not started: + acc = a + started = true + else: + acc = acc.merge(a) + return acc + +static func meshes(n: Node, acc: Array = []) -> Array: + if n is MeshInstance3D: + acc.append(n) + for c in n.get_children(): + meshes(c, acc) + return acc + +# ---------------------------------------------------------------- shell +func _shell() -> void: + var w := _b("x1") - _b("x0") + var d := _b("z1") - _b("z0") + var cx := (_b("x0") + _b("x1")) * 0.5 + var cz := (_b("z0") + _b("z1")) * 0.5 + slab(Vector3(w + 1.0, 0.30, d + 1.0), Vector3(cx, -0.15, cz), mat("floor")) + +## One wall segment, optionally with a doorway punched in it. `style` picks how the +## upper half is treated: solid, glazed (the manager watching you), or absent (a +## counter-height divider you can see over but not walk through). +func _wall_seg(a: String, at: float, from: float, to: float, door: float, + style: String) -> void: + var h := _b("h") + var T := 0.16 + var lo := 0.0 + var hi := h + if style == "half": + hi = 1.15 + var solid_h := hi - lo + var pieces: Array = [] + if door == INF: + pieces.append([from, to, lo, solid_h]) + else: + var d0 := door - 0.55 + var d1 := door + 0.55 + if d0 > from: + pieces.append([from, d0, lo, solid_h]) + if to > d1: + pieces.append([d1, to, lo, solid_h]) + if hi > 2.05: + pieces.append([d0, d1, 2.05, hi - 2.05]) + for p in pieces: + var c := (float(p[0]) + float(p[1])) * 0.5 + var l: float = float(p[1]) - float(p[0]) + var y0: float = float(p[2]) + var hh: float = float(p[3]) + if hh <= 0.0 or l <= 0.0: + continue + if a == "x": + slab(Vector3(l, hh, T), Vector3(c, y0 + hh * 0.5, at), mat("wall")) + else: + slab(Vector3(T, hh, l), Vector3(at, y0 + hh * 0.5, c), mat("wall")) + # glazing above a half-height base + if style == "glass_top" and h > 1.05: + var gc := (from + to) * 0.5 + var gl := to - from + if a == "x": + slab(Vector3(gl, h - 1.05, 0.06), Vector3(gc, (h + 1.0) * 0.5, at), + mat("glass"), true, false) + else: + slab(Vector3(0.06, h - 1.05, gl), Vector3(at, (h + 1.0) * 0.5, gc), + mat("glass"), true, false) + +func _walls() -> void: + var h := _b("h") + var T := 0.16 + var cx := (_b("x0") + _b("x1")) * 0.5 + var cz := (_b("z0") + _b("z1")) * 0.5 + # outer shell, minus any side declared as a window wall + var win_sides := {} + for wnd in spec.get("windows", []): + win_sides[_side_key(wnd)] = true + if not win_sides.has("z:" + str(_b("z0"))): + _wall_seg("x", _b("z0") - T * 0.5, _b("x0"), _b("x1"), INF, "solid") + if not win_sides.has("z:" + str(_b("z1"))): + _wall_seg("x", _b("z1") + T * 0.5, _b("x0"), _b("x1"), INF, "solid") + if not win_sides.has("x:" + str(_b("x0"))): + _wall_seg("z", _b("x0") - T * 0.5, _b("z0"), _b("z1"), INF, "solid") + if not win_sides.has("x:" + str(_b("x1"))): + _wall_seg("z", _b("x1") + T * 0.5, _b("z0"), _b("z1"), INF, "solid") + for wl in spec.get("walls", []): + var d: Dictionary = wl + _wall_seg(String(d.get("a", "x")), float(d["at"]), float(d["from"]), + float(d["to"]), float(d.get("door", INF)), String(d.get("style", "solid"))) + +func _side_key(w: Dictionary) -> String: + return "%s:%s" % [String(w.get("a", "x")), str(float(w["at"]))] + +## A glazed run with mullions every 2.4 m and, optionally, half-raised blinds. It's what +## stops an interior reading as a sealed box. +func _windows() -> void: + var h := _b("h") + var T := 0.16 + for wnd in spec.get("windows", []): + var d: Dictionary = wnd + var a := String(d.get("a", "x")) + var at := float(d["at"]) + var f := float(d["from"]) + var t := float(d["to"]) + var y0 := float(d.get("y0", 0.85)) + var y1 := float(d.get("y1", 2.35)) + var blinds := bool(d.get("blinds", true)) + var out := at + (T * 0.5 * signf(at)) + var span := t - f + var c := (f + t) * 0.5 + if a == "x": + slab(Vector3(span, y0, T), Vector3(c, y0 * 0.5, out), mat("wall")) + slab(Vector3(span, h - y1, T), Vector3(c, (h + y1) * 0.5, out), mat("wall")) + else: + slab(Vector3(T, y0, span), Vector3(out, y0 * 0.5, c), mat("wall")) + slab(Vector3(T, h - y1, span), Vector3(out, (h + y1) * 0.5, c), mat("wall")) + var p := f + while p < t - 0.1: + var p1: float = minf(p + 2.4, t) + var mid := (p + p1) * 0.5 + var seg := p1 - p - 0.12 + if a == "x": + slab(Vector3(seg, y1 - y0, 0.05), Vector3(mid, (y0 + y1) * 0.5, at), + mat("glass"), false, false) + slab(Vector3(0.12, y1 - y0, T), Vector3(p1, (y0 + y1) * 0.5, out), mat("trim")) + if blinds: + slab(Vector3(seg, 0.55, 0.06), Vector3(mid, y1 - 0.28, + at - 0.10 * signf(at)), mat("ceiling"), false, false) + else: + slab(Vector3(0.05, y1 - y0, seg), Vector3(at, (y0 + y1) * 0.5, mid), + mat("glass"), false, false) + slab(Vector3(T, y1 - y0, 0.12), Vector3(out, (y0 + y1) * 0.5, p1), mat("trim")) + if blinds: + slab(Vector3(0.06, 0.55, seg), Vector3(at - 0.10 * signf(at), + y1 - 0.28, mid), mat("ceiling"), false, false) + p = p1 + +func _ceiling() -> void: + var style := String(spec.get("ceiling", "tile")) + if style == "open": + return + var w := _b("x1") - _b("x0") + var d := _b("z1") - _b("z0") + var cx := (_b("x0") + _b("x1")) * 0.5 + var cz := (_b("z0") + _b("z1")) * 0.5 + var h := _b("h") + slab(Vector3(w, 0.16, d), Vector3(cx, h + 0.08, cz), mat("ceiling"), true, false) + if style != "tile": + return + # suspended-tile T-bar. Purely visual, and it IS the texture of an office ceiling. + var gx := _b("x0") + while gx <= _b("x1") + 0.01: + slab(Vector3(0.05, 0.03, d), Vector3(gx, h - 0.02, cz), mat("trim"), false, false) + gx += 1.22 + var gz := _b("z0") + while gz <= _b("z1") + 0.01: + slab(Vector3(w, 0.03, 0.05), Vector3(cx, h - 0.02, gz), mat("trim"), false, false) + gz += 1.22 + +# ---------------------------------------------------------------- lighting +func _lights() -> void: + var L: Dictionary = spec.get("lights", {}) + var xs: Array = L.get("xs", []) + var zs: Array = L.get("zs", []) + var col: Color = L.get("color", Color(1.0, 0.98, 0.93)) + var energy := float(L.get("energy", 1.9)) + var style := String(L.get("style", "troffer")) + var h := _b("h") + + var glow := StandardMaterial3D.new() + glow.albedo_color = Color(0.97, 0.97, 0.93) + glow.emission_enabled = true + glow.emission = col + glow.emission_energy_multiplier = 1.5 + + for xc in xs: + for zc in zs: + var y := h - 0.06 + var size := Vector3(1.14, 0.05, 0.58) + match style: + "pendant": + y = h - 0.55 + size = Vector3(0.30, 0.16, 0.30) + slab(Vector3(0.02, 0.45, 0.02), Vector3(xc, h - 0.24, zc), + mat("trim"), false, false) + "strip": + size = Vector3(1.30, 0.10, 0.14) + var mi := MeshInstance3D.new() + var bm := BoxMesh.new() + bm.size = size + mi.mesh = bm + mi.material_override = glow + mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(mi) + mi.position = Vector3(xc, y, zc) + + var l := OmniLight3D.new() + l.light_color = col + l.light_energy = energy + l.omni_range = 6.5 + l.omni_attenuation = 1.2 + l.shadow_enabled = false # a grid of shadow-casting omnis isn't worth it + add_child(l) + l.position = Vector3(xc, y - 0.14, zc) + + # a couple of shadow-casters so props still sit in contact shadows + var cx := (_b("x0") + _b("x1")) * 0.5 + for zc in [_b("z0") * 0.45, _b("z1") * 0.45]: + var key := OmniLight3D.new() + key.light_color = col + key.light_energy = float(L.get("key_energy", 1.5)) + key.omni_range = 13.0 + key.shadow_enabled = true + add_child(key) + key.position = Vector3(cx, h - 0.30, zc) + + var S: Dictionary = spec.get("sun", {}) + if not S.is_empty(): + var sun := DirectionalLight3D.new() + sun.light_color = S.get("color", Color(0.82, 0.88, 1.0)) + sun.light_energy = float(S.get("energy", 0.7)) + sun.rotation_degrees = S.get("rot", Vector3(-30, -104, 0)) + sun.shadow_enabled = bool(S.get("shadow", true)) + add_child(sun) + +func configure_environment(env: Environment) -> void: + var E: Dictionary = spec.get("env", {}) + env.background_mode = Environment.BG_COLOR + env.background_color = E.get("bg", Color(0.55, 0.62, 0.72)) + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + env.ambient_light_color = E.get("ambient", Color(0.50, 0.50, 0.50)) + env.ambient_light_energy = float(E.get("ambient_energy", 0.5)) + env.ssao_enabled = true + env.ssao_radius = 0.8 + env.ssao_intensity = 1.5 + env.tonemap_mode = Environment.TONE_MAPPER_FILMIC + env.tonemap_exposure = float(E.get("exposure", 1.0)) + +# ---------------------------------------------------------------- contents +func _slabs() -> void: + for s in spec.get("slabs", []): + var d: Dictionary = s + slab(d["size"], d["at"], mat(String(d.get("mat", "wall"))), + bool(d.get("collide", true)), bool(d.get("shadow", true))) + +func _props() -> void: + for p in spec.get("props", []): + var d: Dictionary = p + glb(P + String(d["glb"]) + ".glb", d["at"], float(d.get("yaw", 0.0))) + +## Facing desk pairs behind a cubicle divider — the Jim-and-Dwight arrangement. +func _bullpen() -> void: + _clusters = spec.get("clusters", []) + var gap := float(spec.get("cluster_gap", 0.78)) + for c in _clusters: + var v: Vector2 = c + _partition(Vector3(v.x, 0.0, v.y), 1.9) + for s in [-1.0, 1.0]: + _spots_desk.append(Transform3D(Basis(Vector3.UP, 0.0 if s > 0.0 else PI), + Vector3(v.x, 0.0, v.y + s * gap))) + +func _partition(at: Vector3, length: float) -> void: + slab(Vector3(length, 1.18, 0.07), at + Vector3(0, 0.62, 0), mat("partition")) + slab(Vector3(length + 0.06, 0.09, 0.11), at + Vector3(0, 1.24, 0), mat("trim")) + for s in [-1.0, 1.0]: + slab(Vector3(0.09, 1.24, 0.30), at + Vector3(s * length * 0.5, 0.62, 0), mat("trim")) + +func _chairs() -> void: + for c in spec.get("chairs", []): + var d: Dictionary = c + _spots_chair.append(Transform3D(Basis(Vector3.UP, float(d.get("yaw", 0.0))), + d["at"])) + +# ---------------------------------------------------------------- reads +func desk_spots() -> Array[Transform3D]: + return _spots_desk + +func chair_spots() -> Array[Transform3D]: + return _spots_chair + +## Cluster centre for desk index `i`, so Main can work out which side the chair goes. +func desk_cluster(i: int) -> Vector2: + var c: int = i / 2 + if c < 0 or c >= _clusters.size(): + return Vector2.ZERO + return _clusters[c] + +func copier_spot() -> Transform3D: + var c: Dictionary = spec.get("copier", {}) + if c.is_empty(): + return Transform3D.IDENTITY + return Transform3D(Basis(Vector3.UP, float(c.get("yaw", 0.0))), c["at"]) + +func has_copier() -> bool: + return spec.has("copier") + +func smashables() -> Array: + return spec.get("smashables", []) + +func spawn_point() -> Vector3: + return (spec["spawn"] as Dictionary)["at"] + +func spawn_yaw() -> float: + return float((spec["spawn"] as Dictionary).get("yaw", 0.0)) + +func level_name() -> String: + return String(spec.get("name", "UNNAMED SITE")) + +func subtitle() -> String: + return String(spec.get("subtitle", "")) diff --git a/game/scripts/Floorplan.gd.uid b/game/scripts/Floorplan.gd.uid new file mode 100644 index 0000000..ecb2218 --- /dev/null +++ b/game/scripts/Floorplan.gd.uid @@ -0,0 +1 @@ +uid://d0oexxkrw0hln diff --git a/game/scripts/Levels.gd b/game/scripts/Levels.gd new file mode 100644 index 0000000..e313b6b --- /dev/null +++ b/game/scripts/Levels.gd @@ -0,0 +1,428 @@ +extends RefCounted +class_name Levels + +## The workplaces, as data. Each is a Floorplan spec (see Floorplan.gd for the schema). +## +## The point of these being data and not four subclasses: a new site is authorable in +## minutes, they can be diffed, and a generator can later emit the same structure. +## +## They are deliberately NOT reskins. Each differs in the three things a player actually +## reads — the palette, the light, and what the walls are made of: +## +## SCRANTON ..... magnolia, grey carpet, drop ceiling, fluorescent troffers, daylight +## down one glazed wall. The baseline office. +## PAWNEE ....... civic beige and blue-grey, low partitions everywhere, a public +## counter, pinboards. Municipal, over-partitioned, no privacy. +## THE HOUSE .... timber floor, white walls, 3 m ceiling, pendant lights, a glass wall +## onto a pool. Nobody here has an office; they work at a dining table. +## THE BASEMENT . concrete, no windows at all, exposed services, bare strip lights, a +## wall of server racks. The light is green and everything is junk. +## +## Godot 4.7 GDScript 2.0. + +const ORDER := ["scranton", "pawnee", "house", "basement"] + +static func get_level(id: String) -> Dictionary: + match id: + "pawnee": return pawnee() + "house": return house() + "basement": return basement() + return scranton() + +static func next_id(id: String) -> String: + var i := ORDER.find(id) + return ORDER[(i + 1) % ORDER.size()] if i >= 0 else ORDER[0] + +# ================================================================ LEVEL 01 +static func scranton() -> Dictionary: + return { + "name": "SCRANTON BRANCH", + "subtitle": "paper company · 2nd floor", + "bounds": {"x0": -11.0, "x1": 11.0, "z0": -8.0, "z1": 8.0, "h": 2.75}, + "palette": { + "floor": Color(0.30, 0.30, 0.32), "wall": Color(0.80, 0.78, 0.72), + "ceiling": Color(0.88, 0.88, 0.86), "trim": Color(0.13, 0.12, 0.13), + "partition": Color(0.44, 0.44, 0.40), "wood": Color(0.42, 0.29, 0.17), + "accent": Color(0.36, 0.42, 0.50), + }, + "floor_style": "carpet", + "ceiling": "tile", + "walls": [ + # manager's office — half solid, half glass, so you can be watched through it + {"a": "x", "at": -4.4, "from": -11.0, "to": -6.6, "door": -9.2}, + {"a": "z", "at": -6.6, "from": -8.0, "to": -4.4, "style": "glass_top"}, + # conference room + {"a": "x", "at": -4.4, "from": -6.6, "to": -0.4, "door": -3.4}, + {"a": "z", "at": -0.4, "from": -8.0, "to": -4.4}, + # break room + {"a": "z", "at": 5.4, "from": -1.2, "to": 8.0, "door": 2.2}, + {"a": "x", "at": -1.2, "from": 5.4, "to": 11.0}, + # copier alcove + {"a": "z", "at": 3.6, "from": -1.6, "to": 1.0}, + # reception screen + {"a": "z", "at": -6.9, "from": 4.6, "to": 8.0, "door": 6.8}, + ], + "windows": [{"a": "z", "at": 11.0, "from": -8.0, "to": 8.0, + "y0": 0.85, "y1": 2.35, "blinds": true}], + "lights": {"xs": [-8.5, -4.6, -0.7, 3.2, 7.1], "zs": [-6.2, -2.6, 1.0, 4.6], + "color": Color(1.0, 0.98, 0.93), "energy": 1.9, "style": "troffer"}, + "sun": {"color": Color(0.82, 0.88, 1.0), "energy": 0.7, + "rot": Vector3(-30, -104, 0)}, + "env": {"bg": Color(0.55, 0.62, 0.72), "ambient": Color(0.50, 0.50, 0.50), + "ambient_energy": 0.5}, + "clusters": [Vector2(-4.6, -2.0), Vector2(-4.6, 2.8), + Vector2(-0.4, -2.0), Vector2(-0.4, 2.8)], + "cluster_gap": 0.78, + "slabs": [ + # reception counter + {"size": Vector3(0.70, 1.10, 3.0), "at": Vector3(-9.9, 0.55, 5.6), "mat": "wood"}, + {"size": Vector3(0.92, 0.08, 3.2), "at": Vector3(-9.9, 1.14, 5.6), "mat": "trim"}, + {"size": Vector3(1.8, 1.10, 0.70), "at": Vector3(-9.6, 0.55, 4.45), "mat": "wood"}, + # conference table + whiteboard + {"size": Vector3(3.9, 0.09, 1.35), "at": Vector3(-3.5, 0.74, -6.2), "mat": "wood"}, + {"size": Vector3(0.16, 0.72, 0.90), "at": Vector3(-5.1, 0.36, -6.2), "mat": "trim"}, + {"size": Vector3(0.16, 0.72, 0.90), "at": Vector3(-1.9, 0.36, -6.2), "mat": "trim"}, + {"size": Vector3(2.5, 1.25, 0.03), "at": Vector3(-3.5, 1.6, -7.93), "mat": "trim"}, + {"size": Vector3(2.4, 1.15, 0.05), "at": Vector3(-3.5, 1.6, -7.90), "mat": "ceiling"}, + # break room kitchen + tables + {"size": Vector3(0.62, 0.88, 3.4), "at": Vector3(10.58, 0.44, 4.0), "mat": "wood"}, + {"size": Vector3(0.70, 0.06, 3.5), "at": Vector3(10.58, 0.90, 4.0), "mat": "trim"}, + {"size": Vector3(1.05, 0.07, 1.05), "at": Vector3(7.4, 0.73, 0.4), "mat": "ceiling"}, + {"size": Vector3(0.12, 0.70, 0.12), "at": Vector3(7.4, 0.36, 0.4), "mat": "trim"}, + {"size": Vector3(1.05, 0.07, 1.05), "at": Vector3(7.4, 0.73, 3.0), "mat": "ceiling"}, + {"size": Vector3(0.12, 0.70, 0.12), "at": Vector3(7.4, 0.36, 3.0), "mat": "trim"}, + # warehouse roller door + {"size": Vector3(2.6, 2.4, 0.12), "at": Vector3(7.8, 1.2, -7.94), "mat": "trim"}, + ], + "props": [ + {"glb": "store-column-white", "at": Vector3(-2.2, 0.0, -3.4)}, + {"glb": "store-column-white", "at": Vector3(2.2, 0.0, -3.4)}, + {"glb": "store-aircon", "at": Vector3(2.6, 2.35, -7.9)}, + {"glb": "office-plant", "at": Vector3(-6.4, 0.0, 6.6)}, + {"glb": "office-plant", "at": Vector3(2.9, 0.0, 6.4)}, + {"glb": "vending-machine", "at": Vector3(6.3, 0.0, 7.4), "yaw": PI}, + {"glb": "vending-machine", "at": Vector3(7.3, 0.0, 7.4), "yaw": PI}, + {"glb": "microwave", "at": Vector3(10.55, 0.93, 5.1), "yaw": -PI / 2}, + ], + "chairs": [ + {"at": Vector3(-7.6, 0.0, 5.5), "yaw": -PI / 2}, + {"at": Vector3(-7.6, 0.0, 6.35), "yaw": -PI / 2}, + {"at": Vector3(-7.6, 0.0, 7.2), "yaw": -PI / 2}, + {"at": Vector3(-4.85, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-3.95, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-3.05, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-2.15, 0.0, -7.35), "yaw": 0.0}, + {"at": Vector3(-4.85, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-3.95, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-3.05, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-2.15, 0.0, -5.05), "yaw": PI}, + {"at": Vector3(-8.8, 0.0, -7.1), "yaw": 0.0}, + {"at": Vector3(-9.6, 0.0, -5.1), "yaw": PI}, + {"at": Vector3(8.0, 0.0, 1.13), "yaw": 2.0}, + {"at": Vector3(7.7, 0.0, -0.49), "yaw": 4.1}, + {"at": Vector3(6.5, 0.0, 0.56), "yaw": 0.2}, + {"at": Vector3(8.0, 0.0, 3.73), "yaw": 2.0}, + {"at": Vector3(7.7, 0.0, 2.11), "yaw": 4.1}, + {"at": Vector3(6.5, 0.0, 3.16), "yaw": 0.2}, + ], + "copier": {"at": Vector3(3.0, 0.0, -0.3), "yaw": -PI / 2}, + "smashables": [ + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -1.6)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -0.9)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(4.5, -0.2)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(-6.2, 3.4)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(5.9, -0.6)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.3, 5.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(1.6, 5.4)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(2.1, 4.7)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(6.4, -3.3)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(7.3, -3.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-9.7, 1.2)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(-6.0, -1.0)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(1.0, 1.0)}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(-9.8, 5.0), "sit_on": 1.18}, + {"glb": "rack", "kind": "wood", "at": Vector2(9.4, -6.6), "frozen": true}, + {"glb": "rack", "kind": "wood", "at": Vector2(9.4, -4.3), "frozen": true}, + {"glb": "turntable", "kind": "wood", "at": Vector2(6.9, -5.9)}, + ], + "records": [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)], + "spawn": {"at": Vector3(-8.6, 0.0, 7.2), "yaw": deg_to_rad(196.0)}, + } + +# ================================================================ LEVEL 02 +static func pawnee() -> Dictionary: + return { + "name": "PAWNEE CITY HALL", + "subtitle": "parks & recreation · 4th floor", + "bounds": {"x0": -10.0, "x1": 10.0, "z0": -7.0, "z1": 7.5, "h": 2.70}, + "palette": { + "floor": Color(0.34, 0.36, 0.40), "wall": Color(0.84, 0.80, 0.68), + "ceiling": Color(0.90, 0.89, 0.85), "trim": Color(0.30, 0.22, 0.14), + "partition": Color(0.52, 0.50, 0.42), "wood": Color(0.50, 0.34, 0.19), + "accent": Color(0.16, 0.36, 0.28), + }, + "floor_style": "carpet", + "ceiling": "tile", + "walls": [ + # the department head's office, and the meeting room beside it + {"a": "x", "at": -3.4, "from": -10.0, "to": -5.6, "door": -7.0}, + {"a": "z", "at": -5.6, "from": -7.0, "to": -3.4, "style": "glass_top"}, + {"a": "x", "at": -3.4, "from": -5.6, "to": 0.4, "door": -1.6}, + {"a": "z", "at": 0.4, "from": -7.0, "to": -3.4}, + # records room, and the corridor wall that makes this place a warren + {"a": "z", "at": 4.6, "from": -7.0, "to": -2.0, "door": -4.4}, + {"a": "x", "at": -2.0, "from": 4.6, "to": 10.0}, + # the public counter divides the front third from everything else + {"a": "x", "at": 3.6, "from": -10.0, "to": 3.0, "door": -8.0, "style": "half"}, + ], + "windows": [{"a": "x", "at": 7.5, "from": -10.0, "to": 10.0, + "y0": 0.95, "y1": 2.30, "blinds": true}], + "lights": {"xs": [-7.5, -3.4, 0.7, 4.8, 8.4], "zs": [-5.2, -1.6, 2.0, 5.6], + "color": Color(1.0, 0.96, 0.86), "energy": 1.8, "style": "troffer", + "key_energy": 1.4}, + "sun": {"color": Color(0.90, 0.88, 0.78), "energy": 0.55, + "rot": Vector3(-34, 20, 0)}, + "env": {"bg": Color(0.62, 0.66, 0.62), "ambient": Color(0.50, 0.48, 0.44), + "ambient_energy": 0.52, "exposure": 1.02}, + "clusters": [Vector2(-6.0, 0.4), Vector2(-1.4, 0.4), Vector2(2.6, 0.4)], + "cluster_gap": 0.78, + "slabs": [ + # the public counter itself — the thing between you and the citizens + {"size": Vector3(6.6, 1.06, 0.66), "at": Vector3(-1.2, 0.53, 5.4), "mat": "wood"}, + {"size": Vector3(6.9, 0.07, 0.86), "at": Vector3(-1.2, 1.10, 5.4), "mat": "trim"}, + {"size": Vector3(0.66, 1.06, 2.2), "at": Vector3(2.1, 0.53, 6.4), "mat": "wood"}, + # meeting room table + {"size": Vector3(2.9, 0.09, 1.25), "at": Vector3(-2.6, 0.74, -5.2), "mat": "wood"}, + {"size": Vector3(0.14, 0.72, 0.84), "at": Vector3(-3.7, 0.36, -5.2), "mat": "trim"}, + {"size": Vector3(0.14, 0.72, 0.84), "at": Vector3(-1.5, 0.36, -5.2), "mat": "trim"}, + # pinboards — municipal offices are 40% pinboard by surface area + {"size": Vector3(2.2, 1.20, 0.05), "at": Vector3(5.0, 1.65, -6.93), "mat": "accent"}, + {"size": Vector3(2.3, 1.30, 0.03), "at": Vector3(5.0, 1.65, -6.90), "mat": "trim"}, + {"size": Vector3(0.05, 1.20, 2.6), "at": Vector3(-9.93, 1.62, 2.0), "mat": "accent"}, + {"size": Vector3(0.03, 1.30, 2.7), "at": Vector3(-9.90, 1.62, 2.0), "mat": "trim"}, + # department head's desk + {"size": Vector3(1.7, 0.08, 0.85), "at": Vector3(-7.4, 0.74, -5.4), "mat": "wood"}, + {"size": Vector3(1.5, 0.66, 0.72), "at": Vector3(-7.4, 0.35, -5.4), "mat": "wood"}, + ], + "props": [ + {"glb": "office-plant", "at": Vector3(-9.3, 0.0, 6.6)}, + {"glb": "office-plant", "at": Vector3(3.2, 0.0, -2.6)}, + {"glb": "office-plant", "at": Vector3(-9.3, 0.0, -1.0)}, + {"glb": "sofa", "at": Vector3(-6.6, 0.0, 6.5), "yaw": PI}, + {"glb": "vending-machine", "at": Vector3(9.2, 0.0, 6.6), "yaw": -PI / 2}, + {"glb": "microwave", "at": Vector3(9.3, 0.90, 1.2), "yaw": -PI / 2}, + {"glb": "store-aircon", "at": Vector3(-6.0, 2.30, -6.9)}, + ], + "chairs": [ + {"at": Vector3(-7.4, 0.0, -4.2), "yaw": PI}, + {"at": Vector3(-8.6, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-3.5, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-2.6, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-1.7, 0.0, -6.3), "yaw": 0.0}, + {"at": Vector3(-3.5, 0.0, -4.1), "yaw": PI}, + {"at": Vector3(-2.6, 0.0, -4.1), "yaw": PI}, + {"at": Vector3(-1.7, 0.0, -4.1), "yaw": PI}, + {"at": Vector3(-4.0, 0.0, 6.4), "yaw": PI}, + {"at": Vector3(0.4, 0.0, 6.5), "yaw": PI}, + {"at": Vector3(1.2, 0.0, 6.5), "yaw": PI}, + ], + "copier": {"at": Vector3(6.4, 0.0, 2.4), "yaw": PI}, + "smashables": [ + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(5.4, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(6.1, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(6.8, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(7.5, -3.2)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(3.9, 4.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.6, -5.4)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(9.2, -4.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-4.8, 3.0)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(-8.8, -3.9)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(4.4, 0.9)}, + {"glb": "guillotine", "kind": "steel", "at": Vector2(6.4, 3.6), "sit_on": 0.0}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(-7.4, -5.4), "sit_on": 0.78}, + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-7.6, -5.2), "sit_on": 0.78}, + ], + "records": [], + "spawn": {"at": Vector3(-8.0, 0.0, 6.6), "yaw": deg_to_rad(178.0)}, + } + +# ================================================================ LEVEL 03 +static func house() -> Dictionary: + return { + "name": "THE INCUBATOR", + "subtitle": "seed round · someone's house", + "bounds": {"x0": -10.0, "x1": 10.0, "z0": -8.0, "z1": 8.0, "h": 3.05}, + "palette": { + "floor": Color(0.46, 0.33, 0.20), "wall": Color(0.92, 0.91, 0.88), + "ceiling": Color(0.95, 0.95, 0.94), "trim": Color(0.18, 0.17, 0.17), + "partition": Color(0.72, 0.72, 0.70), "wood": Color(0.40, 0.27, 0.16), + "accent": Color(0.22, 0.44, 0.62), + }, + "floor_style": "timber", + "ceiling": "flat", + "walls": [ + # only two real walls: a bedroom-turned-office and the bathroom nobody cleans + {"a": "z", "at": -4.6, "from": -8.0, "to": -2.4, "door": -4.0}, + {"a": "x", "at": -2.4, "from": -10.0, "to": -4.6, "door": -7.4}, + {"a": "z", "at": 4.0, "from": -8.0, "to": -5.2, "door": -6.6}, + # the kitchen island's back wall + {"a": "x", "at": -1.0, "from": 4.0, "to": 10.0, "style": "half"}, + ], + "windows": [{"a": "x", "at": 8.0, "from": -10.0, "to": 10.0, + "y0": 0.35, "y1": 2.70, "blinds": false}], + "lights": {"xs": [-6.5, -1.5, 3.5, 7.5], "zs": [-5.0, 0.0, 5.0], + "color": Color(1.0, 0.90, 0.76), "energy": 2.4, "style": "pendant", + "key_energy": 1.7}, + "sun": {"color": Color(1.0, 0.94, 0.80), "energy": 1.5, + "rot": Vector3(-46, -96, 0)}, + "env": {"bg": Color(0.70, 0.80, 0.90), "ambient": Color(0.56, 0.55, 0.52), + "ambient_energy": 0.62, "exposure": 1.05}, + # nobody has a desk; they work at the big table + "clusters": [Vector2(1.6, -1.4), Vector2(1.6, 2.6)], + "cluster_gap": 0.80, + "slabs": [ + # kitchen island + counter + {"size": Vector3(2.6, 0.92, 1.05), "at": Vector3(4.2, 0.46, 5.6), "mat": "wood"}, + {"size": Vector3(2.8, 0.08, 1.20), "at": Vector3(4.2, 0.95, 5.6), "mat": "ceiling"}, + {"size": Vector3(6.0, 0.90, 0.65), "at": Vector3(5.0, 0.45, 7.5), "mat": "ceiling"}, + {"size": Vector3(6.2, 0.06, 0.75), "at": Vector3(5.0, 0.92, 7.5), "mat": "trim"}, + # the long table everyone actually works at + {"size": Vector3(1.5, 0.09, 4.6), "at": Vector3(-4.4, 0.75, 0.6), "mat": "wood"}, + {"size": Vector3(0.12, 0.72, 0.12), "at": Vector3(-4.9, 0.36, -1.4), "mat": "trim"}, + {"size": Vector3(0.12, 0.72, 0.12), "at": Vector3(-3.9, 0.36, 2.6), "mat": "trim"}, + # whiteboard, obviously + {"size": Vector3(0.05, 1.40, 3.0), "at": Vector3(-9.93, 1.75, 1.0), "mat": "ceiling"}, + {"size": Vector3(0.03, 1.50, 3.1), "at": Vector3(-9.90, 1.75, 1.0), "mat": "trim"}, + # a low bookshelf run + {"size": Vector3(0.42, 0.90, 3.2), "at": Vector3(-9.7, 0.45, -5.0), "mat": "wood"}, + # pool decking visible through the glass + {"size": Vector3(7.0, 0.10, 16.0), "at": Vector3(12.6, -0.05, 0.0), + "mat": "ceiling", "collide": false}, + ], + "props": [ + {"glb": "sofa", "at": Vector3(-6.6, 0.0, 5.4), "yaw": -PI / 2}, + {"glb": "sofa", "at": Vector3(-2.0, 0.0, 6.6), "yaw": PI}, + {"glb": "office-plant", "at": Vector3(-0.6, 0.0, 3.2)}, + {"glb": "office-plant", "at": Vector3(7.2, 0.0, -6.4)}, + {"glb": "office-plant", "at": Vector3(-9.2, 0.0, 7.2)}, + {"glb": "microwave", "at": Vector3(5.4, 0.95, 7.4), "yaw": PI}, + {"glb": "server-rack", "at": Vector3(9.0, 0.0, -6.8), "yaw": -PI / 2}, + ], + "chairs": [ + {"at": Vector3(-6.0, 0.0, -1.4), "yaw": -PI / 2}, + {"at": Vector3(-6.0, 0.0, -0.2), "yaw": -PI / 2}, + {"at": Vector3(-6.0, 0.0, 1.4), "yaw": -PI / 2}, + {"at": Vector3(-6.0, 0.0, 2.6), "yaw": -PI / 2}, + {"at": Vector3(-2.8, 0.0, -1.0), "yaw": PI / 2}, + {"at": Vector3(-2.8, 0.0, 1.8), "yaw": PI / 2}, + {"at": Vector3(4.2, 0.0, 4.2), "yaw": 0.0}, + {"at": Vector3(5.2, 0.0, 4.2), "yaw": 0.0}, + {"at": Vector3(-7.6, 0.0, -6.4), "yaw": 0.2}, + ], + "copier": {"at": Vector3(-8.6, 0.0, -3.4), "yaw": PI / 2}, + "smashables": [ + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-4.4, -2.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.4, 2.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(8.9, 1.5)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-8.2, 6.4)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(-2.6, -3.2)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(6.2, 3.0)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(1.0, 6.8)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(-9.3, -1.6)}, + {"glb": "turntable", "kind": "wood", "at": Vector2(-9.4, -6.6)}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(4.0, 5.6), "sit_on": 0.99}, + ], + "records": [Vector2(-8.4, -7.0), Vector2(-7.6, -7.0)], + "spawn": {"at": Vector3(-1.0, 0.0, 7.0), "yaw": deg_to_rad(186.0)}, + } + +# ================================================================ LEVEL 04 +static func basement() -> Dictionary: + return { + "name": "SUB-LEVEL 4", + "subtitle": "i.t. department · no windows", + "bounds": {"x0": -12.0, "x1": 12.0, "z0": -6.0, "z1": 6.0, "h": 2.90}, + "palette": { + "floor": Color(0.22, 0.22, 0.23), "wall": Color(0.50, 0.53, 0.46), + "ceiling": Color(0.20, 0.20, 0.21), "trim": Color(0.10, 0.10, 0.10), + "partition": Color(0.34, 0.36, 0.32), "wood": Color(0.30, 0.20, 0.12), + "accent": Color(0.52, 0.10, 0.09), + }, + "floor_style": "concrete", + "ceiling": "flat", + "walls": [ + # the boss's glass box, bolted into a room that never wanted one + {"a": "z", "at": 4.6, "from": -6.0, "to": 1.2, "door": -1.0, + "style": "glass_top"}, + {"a": "x", "at": 1.2, "from": 4.6, "to": 12.0}, + # server room behind a security door + {"a": "z", "at": -6.4, "from": -6.0, "to": 6.0, "door": 1.0}, + # a stub wall someone put up and nobody took down + {"a": "x", "at": -1.4, "from": -4.0, "to": -1.6}, + ], + "windows": [], # it is a basement + "lights": {"xs": [-9.0, -4.5, 0.0, 4.5, 9.0], "zs": [-3.6, 0.4, 4.0], + "color": Color(0.86, 0.96, 0.84), "energy": 2.0, "style": "strip", + "key_energy": 1.1}, + "env": {"bg": Color(0.06, 0.07, 0.06), "ambient": Color(0.32, 0.36, 0.32), + "ambient_energy": 0.40, "exposure": 1.08}, + "clusters": [Vector2(3.6, 0.0)], + "cluster_gap": 0.82, + "slabs": [ + # exposed services, because there's no ceiling to hide them + {"size": Vector3(24.0, 0.22, 0.22), "at": Vector3(0.0, 2.70, -3.0), + "mat": "trim", "collide": false}, + {"size": Vector3(24.0, 0.16, 0.16), "at": Vector3(0.0, 2.62, 1.4), + "mat": "trim", "collide": false}, + {"size": Vector3(24.0, 0.30, 0.10), "at": Vector3(0.0, 2.78, 4.2), + "mat": "trim", "collide": false}, + # boss's desk in the glass box + {"size": Vector3(1.8, 0.08, 0.86), "at": Vector3(8.0, 0.74, 5.4), "mat": "wood"}, + {"size": Vector3(1.6, 0.66, 0.74), "at": Vector3(8.0, 0.35, 5.4), "mat": "wood"}, + # workbench along the back + {"size": Vector3(5.0, 0.08, 0.80), "at": Vector3(-6.0, 0.90, -5.5), "mat": "trim"}, + {"size": Vector3(0.10, 0.88, 0.70), "at": Vector3(-8.4, 0.44, -5.5), "mat": "trim"}, + {"size": Vector3(0.10, 0.88, 0.70), "at": Vector3(-3.6, 0.44, -5.5), "mat": "trim"}, + # the famous red door + {"size": Vector3(1.10, 2.10, 0.10), "at": Vector3(1.0, 1.05, -6.35), + "mat": "accent"}, + ], + "props": [ + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, -4.4), "yaw": PI / 2}, + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, -2.4), "yaw": PI / 2}, + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, -0.4), "yaw": PI / 2}, + {"glb": "server-rack", "at": Vector3(-10.6, 0.0, 1.6), "yaw": PI / 2}, + {"glb": "sofa", "at": Vector3(-4.0, 0.0, 3.4), "yaw": 0.15}, + {"glb": "office-plant", "at": Vector3(11.2, 0.0, 5.4)}, + {"glb": "vending-machine", "at": Vector3(-0.4, 0.0, 5.4), "yaw": PI}, + {"glb": "microwave", "at": Vector3(-5.0, 0.95, -5.5), "yaw": PI}, + ], + "chairs": [ + {"at": Vector3(8.0, 0.0, 4.1), "yaw": PI}, + {"at": Vector3(6.6, 0.0, 5.9), "yaw": 0.3}, + {"at": Vector3(-6.6, 0.0, -4.2), "yaw": PI}, + {"at": Vector3(-1.0, 0.0, 2.2), "yaw": 1.2}, + ], + "copier": {"at": Vector3(0.4, 0.0, -4.4), "yaw": 0.0}, + "smashables": [ + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-7.0, -5.5), "sit_on": 0.94}, + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-5.0, -4.6)}, + {"glb": "crt-monitor", "kind": "glass", "at": Vector2(-8.4, -3.2)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(10.8, 3.0)}, + {"glb": "filing-cabinet", "kind": "steel", "at": Vector2(10.8, 2.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.6, -4.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-2.0, -5.3)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(-1.4, -4.6)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(5.6, -4.8)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(6.3, -5.2)}, + {"glb": "cardboard-box", "kind": "cardboard", "at": Vector2(7.0, -4.6)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(2.0, 2.6)}, + {"glb": "wastebin", "kind": "plastic", "at": Vector2(9.0, 4.4)}, + {"glb": "water-cooler", "kind": "steel", "at": Vector2(0.6, 2.2)}, + {"glb": "shredder", "kind": "plastic", "at": Vector2(-2.2, -2.0)}, + {"glb": "desk-phone", "kind": "plastic", "at": Vector2(8.3, 5.4), "sit_on": 0.78}, + {"glb": "turntable", "kind": "wood", "at": Vector2(-9.0, 4.4)}, + ], + "records": [Vector2(-8.2, 5.0), Vector2(-7.5, 5.0)], + "spawn": {"at": Vector3(1.0, 0.0, -5.2), "yaw": deg_to_rad(0.0)}, + } diff --git a/game/scripts/Levels.gd.uid b/game/scripts/Levels.gd.uid new file mode 100644 index 0000000..f9b2553 --- /dev/null +++ b/game/scripts/Levels.gd.uid @@ -0,0 +1 @@ +uid://c0ic0hj3e0do0 diff --git a/game/scripts/Main.gd b/game/scripts/Main.gd index a2ca70b..465c995 100644 --- a/game/scripts/Main.gd +++ b/game/scripts/Main.gd @@ -30,11 +30,13 @@ var _juice: Juice var _player: Player # kept so GameMode can reach player.grab var _records: Array[Record] = [] # the live records, handed to GameMode each round var _game_mode: GameMode -var _office: Office # the level shell: walls, fittings, lighting +var _plan: Floorplan # the level shell: walls, fittings, lighting — see Levels.gd +var _level_id := "scranton" var _rage: Rage # the tilt meter — see Rage.gd var _rage_hud: RageOverlay var _tasks: Tasks var _shift: CubicleHell +var _env_node: WorldEnvironment func _ready() -> void: randomize() @@ -74,34 +76,35 @@ func _ready() -> void: ## Every desk is a spreadsheet you have to fill in; the copier is the copier. func _register_stations() -> void: var i := 1 - for t in _office.desk_spots(): + for t in _plan.desk_spots(): _tasks.register(Tasks.Kind.SPREADSHEET, (t as Transform3D).origin, "work at desk %d" % i) i += 1 - _tasks.register(Tasks.Kind.COPIER, _office.copier_spot().origin, - "use the photocopier") + if _plan.has_copier(): + _tasks.register(Tasks.Kind.COPIER, _plan.copier_spot().origin, + "use the photocopier") # ---------------------------------------------------------------- world / camera func _setup_world() -> void: # The level shell: floor, walls, ceiling, windows, fittings and ALL the lighting. # It owns the environment too, because interior lighting and the ambient/tonemap # settings are one decision, not two. - _office = Office.new() - add_child(_office) - _office.build() + _plan = Floorplan.new() + add_child(_plan) + _plan.build(Levels.get_level(_level_id)) - var we := WorldEnvironment.new() + _env_node = WorldEnvironment.new() var env := Environment.new() - _office.configure_environment(env) - we.environment = env - add_child(we) + _plan.configure_environment(env) + _env_node.environment = env + add_child(_env_node) # First-person player. add_child() runs its _ready() synchronously, so player.camera # and player.grab exist on the next line; we reuse them. _player = Player.new() add_child(_player) - _player.global_position = _office.spawn_point() - _player.rotation.y = _office.spawn_yaw() + _player.global_position = _plan.spawn_point() + _player.rotation.y = _plan.spawn_yaw() _cam = _player.camera _world = Node3D.new() @@ -125,6 +128,7 @@ const COOLER_GLB := "res://assets/store/water-cooler.glb" const BOX_GLB := "res://assets/store/cardboard-box.glb" const TURNTABLE_GLB := "res://assets/store/turntable.glb" const CHAIR_GLB := "res://assets/store/office-chair.glb" +const P := "res://assets/store/" ## Mass by material, so a thrown record can't tip a filing cabinet but a sledgehammer ## still can. Everything defaulted to 1 kg before, which is why the level fell over. @@ -149,7 +153,7 @@ func _populate() -> void: _records.clear() # stale refs from the freed world; repopulated by _place_record # --- the bullpen: a desk at every cluster spot, each with a monitor on it --- - var spots := _office.desk_spots() + var spots := _plan.desk_spots() for i in spots.size(): var t: Transform3D = spots[i] var xz := Vector2(t.origin.x, t.origin.z) @@ -161,53 +165,45 @@ func _populate() -> void: # used to guess a constant here and kept putting chairs inside desks (or, once # pushed out far enough to clear them, inside each other across the aisle). var depth: float = (desk["aabb"] as AABB).size.z - var back := signf(t.origin.z - _office.CLUSTERS[i / 2].y) + var back := signf(t.origin.z - _plan.desk_cluster(i).y) if is_zero_approx(back): back = 1.0 _glb_piece(CHAIR_GLB, "plastic", xz + Vector2(0.0, back * (depth * 0.5 + 0.42)), false, 0.0, t.basis.get_euler().y) # --- the copier alcove: the PRINTER is the level boss, on its own desk --- - var cs := _office.copier_spot() - var cxz := Vector2(cs.origin.x, cs.origin.z) - var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y) - var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"]) - var boss := printer["piece"] as Smashable - boss.is_boss = true - boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge - boss.mass = 8.0 # heavy, so hits rock it but don't shove it off the desk - (cdesk["piece"] as Smashable).supports.append(printer["piece"]) + if _plan.has_copier(): + var cs := _plan.copier_spot() + var cxz := Vector2(cs.origin.x, cs.origin.z) + var cdesk := _glb_piece(DESK_GLB, "wood", cxz, true, 0.0, cs.basis.get_euler().y) + var printer := _glb_piece(PRINTER_GLB, "steel", cxz, false, cdesk["top"]) + var boss := printer["piece"] as Smashable + boss.is_boss = true + boss.toughness_scale = 5.0 # steel x5 — a real fight for anything but the sledge + boss.mass = 8.0 # heavy: hits rock it but don't shove it off the desk + (cdesk["piece"] as Smashable).supports.append(printer["piece"]) - # --- filing cabinets down the wall by the copier, water coolers, cartons --- - for z in [-1.6, -0.9, -0.2]: - _glb_piece(CABINET_GLB, "steel", Vector2(4.5, z), false) - _glb_piece(COOLER_GLB, "steel", Vector2(-6.2, 3.4), false) - _glb_piece(COOLER_GLB, "steel", Vector2(5.9, -0.6), false) - # Cartons go on OPEN floor only. Tucking them under desks or against the conference - # table spawned them inside those colliders, and Jolt resolved the overlap by firing - # them across the room on frame one. - for c in [Vector2(-2.3, 5.2), Vector2(1.6, 5.4), Vector2(2.1, 4.7), - Vector2(6.4, -3.3), Vector2(7.3, -3.8), Vector2(-9.7, 1.2)]: - _glb_piece(BOX_GLB, "cardboard", c, false) + # --- everything else this site contains, straight from its spec --- + for e in _plan.smashables(): + var d: Dictionary = e + _glb_piece(P + String(d["glb"]) + ".glb", String(d.get("kind", "wood")), + d["at"], bool(d.get("frozen", false)), float(d.get("sit_on", 0.0)), + float(d.get("yaw", 0.0))) - # --- the warehouse corner: shelving, crates, and somebody's record collection. - # --- This is where the grab/slide/throw ritual still lives. - _glb_piece(RACK_GLB, "wood", Vector2(9.4, -6.6), true) - _glb_piece(RACK_GLB, "wood", Vector2(9.4, -4.3), true) - _glb_piece(TURNTABLE_GLB, "wood", Vector2(6.9, -5.9), false) - for spot in [Vector2(6.2, -6.6), Vector2(7.0, -6.9), Vector2(7.8, -6.6)]: + # --- crates of records, where the site has any. Records stand side by side ON the + # --- crate lid, offset by more than a sleeve is thick: they used to be stacked 3 cm + # --- apart vertically while being 30 cm TALL, so Jolt resolved 27 cm of + # --- interpenetration explosively on frame one. + for spot in _plan.spec.get("records", []): var crate := _glb_piece(CRATE_GLB, "wood", spot, false) var crate_top: float = crate["top"] - # Records stand side by side ON the crate lid, offset along Z by more than a - # sleeve is thick. They used to be stacked 3 cm apart vertically while being - # 30 cm TALL, so Jolt resolved 27 cm of interpenetration explosively on frame - # one and shoved the surrounding furniture over before you touched anything. for i in range(3): var rec := _place_record(spot + Vector2(randf_range(-0.02, 0.02), (float(i) - 1.0) * 0.045), crate_top) (crate["piece"] as Smashable).supports.append(rec) + # --- chairs: the best thing in an office to send across the room --- - for t in _office.chair_spots(): + for t in _plan.chair_spots(): _glb_piece(CHAIR_GLB, "plastic", Vector2(t.origin.x, t.origin.z), false, 0.0, t.basis.get_euler().y) @@ -404,8 +400,9 @@ func _physics_process(dt: float) -> void: # report where it was PLACED, not where it is now — at 500 m/s a body is # already 8 m away by the time this runs, which points at nothing useful var at = b.get_meta("spawn_pos", b.global_position) - push_warning("[spawn] %s placed overlapping something at %s (%.0f m/s) — clamped" % [ - b.name, (at as Vector3).snappedf(0.01), v]) + push_warning("[spawn] %s (%s) placed overlapping something at %s (%.0f m/s) — clamped" % [ + b.name, (b as Smashable).kind if b is Smashable else "?", + (at as Vector3).snappedf(0.01), v]) b.linear_velocity = b.linear_velocity.normalized() * SPAWN_MAX_SPEED if b.angular_velocity.length() > SPAWN_MAX_SPIN: b.angular_velocity = b.angular_velocity.normalized() * SPAWN_MAX_SPIN @@ -464,10 +461,53 @@ func _unhandled_input(event: InputEvent) -> void: KEY_H: if _hud != null: _hud.cycle() + KEY_L: + _load_level(Levels.next_id(_level_id)) KEY_F: if _game_mode != null: _game_mode.report() # report the disc you're holding (FindMisfiled) +## Swap sites without restarting: tear down the shell and everything in it, build the +## new spec, and re-arm the shift. The player is moved to the new spawn because the old +## one is very likely now inside a wall. +func _load_level(id: String) -> void: + _level_id = id + for c in _world.get_children(): + c.queue_free() + _plan.queue_free() + if _env_node != null: + _env_node.queue_free() + await get_tree().process_frame + + _plan = Floorplan.new() + add_child(_plan) + _plan.build(Levels.get_level(_level_id)) + _env_node = WorldEnvironment.new() + var env := Environment.new() + _plan.configure_environment(env) + _env_node.environment = env + add_child(_env_node) + + _player.global_position = _plan.spawn_point() + _player.rotation.y = _plan.spawn_yaw() + _player.velocity = Vector3.ZERO + + _smash_count = 0 + _score = 0 + _tally.clear() + _populate() + _spawn_guard = SPAWN_GUARD_TIME + _guard_warned.clear() + + _tasks.clear_stations() + _register_stations() + if _game_mode != null: + _game_mode.begin_free_play(_records) + if _shift != null: + _shift.begin() + if _hud != null: + _hud.toast("%s — %s" % [_plan.level_name(), _plan.subtitle()]) + # The Day-0 stress gate: rain N bodies, watch the FPS counter hold. func _rain(n: int) -> void: for i in n: @@ -494,6 +534,10 @@ func _reset() -> void: _tally.clear() await get_tree().process_frame _populate() + _spawn_guard = SPAWN_GUARD_TIME + _guard_warned.clear() + if _shift != null: + _shift.begin() # re-arm whatever mode we're in against the fresh set of records if _game_mode != null: if _game_mode.mode == GameMode.Mode.FIND_MISFILED: @@ -519,7 +563,7 @@ func _on_weapon_changed(w: Weapon) -> void: if _hud != null and w != null: _hud.toast(w.display_name) -const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · B rain · R reset" +const KEYS_LINE := "WASD move · 1-6 / wheel weapon · Q swap · LMB swing · E grab · drag ←→ pull · G drop · F report · M mode · H hud · L site · B rain · R reset" func _process(_dt: float) -> void: if _hud == null: diff --git a/game/scripts/Office.gd b/game/scripts/Office.gd deleted file mode 100644 index 5921101..0000000 --- a/game/scripts/Office.gd +++ /dev/null @@ -1,454 +0,0 @@ -extends Node3D -class_name Office - -## LEVEL 01 — the open-plan office. -## -## The prototype ran on a grey disc in a black void, which quietly broke the premise: a -## game about wrecking your workplace needs a workplace. This is that workplace, laid out -## after the Dunder Mifflin floor plan — the shape everyone already knows. -## -## z- (back) -## +--------------------------------------------------+ -## | MANAGER | CONFERENCE | | WAREHOUSE | -## | (glass) | | | door | -## |----------+---------------+ +-----------| -## | | | -## | B U L L P E N | BREAK | x+ -## | (4 facing desk pairs) | ROOM | -## | [copier] | | -## |----------+ +-----------| -## | RECEPTION| entrance | -## +--------------------------------------------------+ -## z+ (front / street) -## -## Walls, desks, partitions and counters are boxes built in code — they ARE boxes, so -## modelling them would be wasted effort. Anything with a silhouette a box can't fake -## (chairs, vending machines, the plant) comes from tools/gen_office_props.py. -## -## This script owns the SHELL and the FITTINGS: everything static and non-smashable, plus -## the lighting. Smashable furniture stays Main's business, so the two can be tuned -## independently — Main asks this class where things go via the `spot_*` helpers. -## -## Godot 4.7 GDScript 2.0. - -# ---------------------------------------------------------------- floor plan -const X_MIN := -11.0 -const X_MAX := 11.0 -const Z_BACK := -8.0 -const Z_FRONT := 8.0 -const H := 2.75 # drop ceiling -const WALL_T := 0.16 - -# interior room boundaries -const MGR_X := -6.6 # manager's office: X_MIN..MGR_X, Z_BACK..MGR_Z -const MGR_Z := -4.4 -const CONF_X0 := -6.6 # conference room -const CONF_X1 := -0.4 -const CONF_Z := -4.4 -const BREAK_X := 5.4 # break room: BREAK_X..X_MAX, BREAK_Z..Z_FRONT -const BREAK_Z := -1.2 -const RECEP_Z := 4.6 # reception partition - -const DOOR_W := 1.1 - -const P := "res://assets/store/" - -var _spots_desk: Array[Transform3D] = [] # where Main should put smashable desks -var _spots_chair: Array[Transform3D] = [] -var _spot_copier := Transform3D.IDENTITY - -# ---------------------------------------------------------------- materials -var _m_carpet: StandardMaterial3D -var _m_wall: StandardMaterial3D -var _m_ceil: StandardMaterial3D -var _m_trim: StandardMaterial3D -var _m_glass: StandardMaterial3D -var _m_part: StandardMaterial3D -var _m_wood: StandardMaterial3D - -func build() -> void: - _materials() - _shell() - _interior_walls() - _ceiling_grid() - _lights_rig() - _bullpen() - _reception() - _conference() - _manager() - _break_room() - _dressing() - -func _materials() -> void: - _m_carpet = StandardMaterial3D.new() - _m_carpet.albedo_color = Color(0.30, 0.30, 0.32) # the grey office carpet - _m_carpet.roughness = 1.0 - - _m_wall = StandardMaterial3D.new() - _m_wall.albedo_color = Color(0.80, 0.78, 0.72) # magnolia, of course - _m_wall.roughness = 0.96 - - _m_ceil = StandardMaterial3D.new() - _m_ceil.albedo_color = Color(0.88, 0.88, 0.86) - _m_ceil.roughness = 0.98 - - _m_trim = StandardMaterial3D.new() - _m_trim.albedo_color = Color(0.13, 0.12, 0.13) - _m_trim.roughness = 0.7 - - _m_part = StandardMaterial3D.new() - _m_part.albedo_color = Color(0.44, 0.44, 0.40) # cubicle partition fabric - _m_part.roughness = 1.0 - - _m_wood = StandardMaterial3D.new() - _m_wood.albedo_color = Color(0.42, 0.29, 0.17) - _m_wood.roughness = 0.55 - - _m_glass = StandardMaterial3D.new() - _m_glass.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - _m_glass.albedo_color = Color(0.66, 0.74, 0.78, 0.15) - _m_glass.roughness = 0.05 - _m_glass.cull_mode = BaseMaterial3D.CULL_DISABLED - -# ---------------------------------------------------------------- primitives -func _slab(size: Vector3, centre: Vector3, mat: Material, collide := true, - shadow := true) -> StaticBody3D: - var body := StaticBody3D.new() - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = size - mi.mesh = bm - mi.material_override = mat - if not shadow: - mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - body.add_child(mi) - if collide: - var col := CollisionShape3D.new() - var sh := BoxShape3D.new() - sh.size = size - col.shape = sh - body.add_child(col) - add_child(body) - body.position = centre - return body - -## A wall running along X (or Z) with a doorway punched in it. -func _wall_x(z: float, x0: float, x1: float, mat: Material, door_at := INF) -> void: - if door_at == INF: - _slab(Vector3(x1 - x0, H, WALL_T), Vector3((x0 + x1) * 0.5, H * 0.5, z), mat) - return - var d0 := door_at - DOOR_W * 0.5 - var d1 := door_at + DOOR_W * 0.5 - if d0 > x0: - _slab(Vector3(d0 - x0, H, WALL_T), Vector3((x0 + d0) * 0.5, H * 0.5, z), mat) - if x1 > d1: - _slab(Vector3(x1 - d1, H, WALL_T), Vector3((d1 + x1) * 0.5, H * 0.5, z), mat) - _slab(Vector3(DOOR_W, H - 2.05, WALL_T), Vector3(door_at, (H + 2.05) * 0.5, z), mat) - -func _wall_z(x: float, z0: float, z1: float, mat: Material, door_at := INF) -> void: - if door_at == INF: - _slab(Vector3(WALL_T, H, z1 - z0), Vector3(x, H * 0.5, (z0 + z1) * 0.5), mat) - return - var d0 := door_at - DOOR_W * 0.5 - var d1 := door_at + DOOR_W * 0.5 - if d0 > z0: - _slab(Vector3(WALL_T, H, d0 - z0), Vector3(x, H * 0.5, (z0 + d0) * 0.5), mat) - if z1 > d1: - _slab(Vector3(WALL_T, H, z1 - d1), Vector3(x, H * 0.5, (d1 + z1) * 0.5), mat) - _slab(Vector3(WALL_T, H - 2.05, DOOR_W), Vector3(x, (H + 2.05) * 0.5, door_at), mat) - -## Static set dressing from a GLB, with a box collider sized to its own mesh so you can't -## walk through the vending machine. -func _glb(path: String, pos: Vector3, yaw := 0.0) -> Node3D: - if not ResourceLoader.exists(path): - return null - var packed := load(path) as PackedScene - if packed == null: - return null - var body := StaticBody3D.new() - add_child(body) - body.position = pos - body.rotation.y = yaw - var n: Node3D = packed.instantiate() - body.add_child(n) - var ab := _mesh_aabb(n) - if ab.size != Vector3.ZERO: - var col := CollisionShape3D.new() - var sh := BoxShape3D.new() - sh.size = ab.size - col.shape = sh - col.position = ab.get_center() - body.add_child(col) - return body - -func _mesh_aabb(root: Node3D) -> AABB: - var acc := AABB() - var started := false - for m in _meshes(root): - var mi := m as MeshInstance3D - var a := mi.get_aabb() - a = mi.transform * a - if not started: - acc = a - started = true - else: - acc = acc.merge(a) - return acc - -func _meshes(n: Node, acc: Array = []) -> Array: - if n is MeshInstance3D: - acc.append(n) - for c in n.get_children(): - _meshes(c, acc) - return acc - -## Chairs are NOT placed here. They're the most satisfying thing in an office to send -## across the room, so they're spawned by Main as smashable rigid bodies; Office only -## says where they start. -func _chair(pos: Vector3, yaw: float) -> void: - _spots_chair.append(Transform3D(Basis(Vector3.UP, yaw), pos)) - -# ---------------------------------------------------------------- shell -func _shell() -> void: - var w := X_MAX - X_MIN - var d := Z_FRONT - Z_BACK - var cx := (X_MIN + X_MAX) * 0.5 - var cz := (Z_BACK + Z_FRONT) * 0.5 - _slab(Vector3(w + 1.0, 0.30, d + 1.0), Vector3(cx, -0.15, cz), _m_carpet) - # outer walls; the +X wall is the window wall, so it's built in _windows() - _wall_x(Z_BACK - WALL_T * 0.5, X_MIN, X_MAX, _m_wall) - _wall_x(Z_FRONT + WALL_T * 0.5, X_MIN, X_MAX, _m_wall, -8.6) # entrance - _wall_z(X_MIN - WALL_T * 0.5, Z_BACK, Z_FRONT, _m_wall) - _windows() - -## The window wall. Dunder Mifflin's bullpen has that whole run of blinded windows down -## one side, and it's what stops an interior reading as a sealed box. -func _windows() -> void: - var x := X_MAX + WALL_T * 0.5 - var y0 := 0.85 - var y1 := 2.35 - _slab(Vector3(WALL_T, y0, Z_FRONT - Z_BACK), Vector3(x, y0 * 0.5, - (Z_BACK + Z_FRONT) * 0.5), _m_wall) - _slab(Vector3(WALL_T, H - y1, Z_FRONT - Z_BACK), Vector3(x, (H + y1) * 0.5, - (Z_BACK + Z_FRONT) * 0.5), _m_wall) - # mullions every 2.4 m, glazing between - var z := Z_BACK - while z < Z_FRONT - 0.1: - var z1: float = minf(z + 2.4, Z_FRONT) - _slab(Vector3(0.05, y1 - y0, z1 - z - 0.12), Vector3(X_MAX - 0.02, - (y0 + y1) * 0.5, (z + z1) * 0.5), _m_glass, false, false) - _slab(Vector3(WALL_T, y1 - y0, 0.12), Vector3(x, (y0 + y1) * 0.5, z1), _m_trim) - # venetian blind, half-raised - _slab(Vector3(0.06, 0.55, z1 - z - 0.14), Vector3(X_MAX - 0.10, y1 - 0.28, - (z + z1) * 0.5), _m_ceil, false, false) - z = z1 - -func _ceiling_grid() -> void: - var w := X_MAX - X_MIN - var d := Z_FRONT - Z_BACK - var cx := (X_MIN + X_MAX) * 0.5 - var cz := (Z_BACK + Z_FRONT) * 0.5 - _slab(Vector3(w, 0.16, d), Vector3(cx, H + 0.08, cz), _m_ceil, true, false) - # suspended-tile T-bar, purely visual — it's the texture of an office ceiling - var gx := X_MIN - while gx <= X_MAX + 0.01: - _slab(Vector3(0.05, 0.03, d), Vector3(gx, H - 0.02, cz), _m_trim, false, false) - gx += 1.22 - var gz := Z_BACK - while gz <= Z_FRONT + 0.01: - _slab(Vector3(w, 0.03, 0.05), Vector3(cx, H - 0.02, gz), _m_trim, false, false) - gz += 1.22 - -# ---------------------------------------------------------------- interior walls -func _interior_walls() -> void: - # manager's office — half solid, half glass so you can be watched through it - _wall_x(MGR_Z, X_MIN, MGR_X, _m_wall, -9.2) - _slab(Vector3(WALL_T, 1.0, MGR_Z - Z_BACK), Vector3(MGR_X, 0.5, - (Z_BACK + MGR_Z) * 0.5), _m_wall) - _slab(Vector3(0.06, H - 1.05, MGR_Z - Z_BACK), Vector3(MGR_X, (H + 1.0) * 0.5, - (Z_BACK + MGR_Z) * 0.5), _m_glass, true, false) - - # conference room - _wall_x(CONF_Z, CONF_X0, CONF_X1, _m_wall, -3.4) - _wall_z(CONF_X1, Z_BACK, CONF_Z, _m_wall) - - # break room - _wall_z(BREAK_X, BREAK_Z, Z_FRONT, _m_wall, 2.2) - _wall_x(BREAK_Z, BREAK_X, X_MAX, _m_wall) - - # warehouse roller door on the back wall - _slab(Vector3(2.6, 2.4, 0.12), Vector3(7.8, 1.2, Z_BACK + 0.06), _m_trim) - for i in range(9): - _slab(Vector3(2.5, 0.06, 0.05), Vector3(7.8, 0.18 + i * 0.26, - Z_BACK + 0.14), _m_ceil, false, false) - -# ---------------------------------------------------------------- lighting -## Recessed fluorescent troffers in the ceiling grid. These ARE the light: the old scene -## lit everything with one outdoor directional lamp, which is exactly why it read as -## "props on a plane" rather than "inside somewhere". -func _lights_rig() -> void: - var panel := StandardMaterial3D.new() - panel.albedo_color = Color(0.97, 0.97, 0.93) - panel.emission_enabled = true - panel.emission = Color(1.0, 0.98, 0.92) - panel.emission_energy_multiplier = 1.5 - - var xs := [-8.5, -4.6, -0.7, 3.2, 7.1] - var zs := [-6.2, -2.6, 1.0, 4.6] - for xc in xs: - for zc in zs: - var mi := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = Vector3(1.14, 0.05, 0.58) - mi.mesh = bm - mi.material_override = panel - mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - add_child(mi) - mi.position = Vector3(xc, H - 0.06, zc) - - var l := OmniLight3D.new() - l.light_color = Color(1.0, 0.98, 0.93) - l.light_energy = 1.9 - l.omni_range = 6.5 - l.omni_attenuation = 1.2 - l.shadow_enabled = false # 20 shadow-casting omnis is not worth the cost - add_child(l) - l.position = Vector3(xc, H - 0.20, zc) - - # two shadow-casters so props still sit in contact shadows - for zc in [-2.0, 3.0]: - var key := OmniLight3D.new() - key.light_color = Color(1.0, 0.97, 0.92) - key.light_energy = 1.5 - key.omni_range = 13.0 - key.shadow_enabled = true - add_child(key) - key.position = Vector3(-1.0, H - 0.30, zc) - - # daylight through the window wall - var sun := DirectionalLight3D.new() - sun.light_color = Color(0.82, 0.88, 1.0) - sun.light_energy = 0.7 - sun.rotation_degrees = Vector3(-30, -104, 0) - sun.shadow_enabled = true - add_child(sun) - -func configure_environment(env: Environment) -> void: - env.background_mode = Environment.BG_COLOR - env.background_color = Color(0.55, 0.62, 0.72) # daylight past the glass - env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR - env.ambient_light_color = Color(0.50, 0.50, 0.50) - env.ambient_light_energy = 0.5 - env.ssao_enabled = true - env.ssao_radius = 0.8 - env.ssao_intensity = 1.5 - env.tonemap_mode = Environment.TONE_MAPPER_FILMIC - env.tonemap_exposure = 1.0 - -# ---------------------------------------------------------------- bullpen -## Four pairs of desks facing each other across a low partition — the Jim-and-Dwight -## arrangement, which is the single most recognisable thing about the floor plan. -## Rows are 4.8 m apart, not 3.6. At 3.6 the two rows' chairs met back-to-back in the -## middle of the aisle with 3 cm to spare, which Jolt resolved by firing them out of the -## building — and it left no room to walk between the rows either. -const CLUSTERS := [ - Vector2(-4.6, -2.0), Vector2(-4.6, 2.8), - Vector2(-0.4, -2.0), Vector2(-0.4, 2.8), -] - -func _bullpen() -> void: - for c in CLUSTERS: - _partition(Vector3(c.x, 0.0, c.y), 1.9) - for s in [-1.0, 1.0]: - var t := Transform3D(Basis(Vector3.UP, 0.0 if s > 0.0 else PI), - Vector3(c.x, 0.0, c.y + s * 0.78)) - _spots_desk.append(t) - # NOTE: the bullpen chairs are NOT listed here. Their offset depends on how - # deep office-desk.glb actually is, which only Main knows once it has measured - # the mesh — guessing a constant here put chairs inside desks. - # the copier alcove, between the bullpen and the break room - _slab(Vector3(WALL_T, H, 2.6), Vector3(3.6, H * 0.5, -0.3), _m_wall) - _spot_copier = Transform3D(Basis(Vector3.UP, deg_to_rad(-90.0)), - Vector3(3.0, 0.0, -0.3)) - -## A cubicle divider: fabric panel on a dark base rail. -func _partition(at: Vector3, length: float) -> void: - _slab(Vector3(length, 1.18, 0.07), at + Vector3(0, 0.62, 0), _m_part) - _slab(Vector3(length + 0.06, 0.09, 0.11), at + Vector3(0, 1.24, 0), _m_trim) - for s in [-1.0, 1.0]: - _slab(Vector3(0.09, 1.24, 0.30), at + Vector3(s * length * 0.5, 0.62, 0), _m_trim) - -func desk_spots() -> Array[Transform3D]: - return _spots_desk - -func chair_spots() -> Array[Transform3D]: - return _spots_chair - -func copier_spot() -> Transform3D: - return _spot_copier - -# ---------------------------------------------------------------- rooms -func _reception() -> void: - # the counter you meet on the way in, with the waiting-area chairs - _slab(Vector3(0.70, 1.10, 3.0), Vector3(-9.9, 0.55, RECEP_Z + 1.0), _m_wood) - _slab(Vector3(0.92, 0.08, 3.2), Vector3(-9.9, 1.14, RECEP_Z + 1.0), _m_trim) - _slab(Vector3(1.8, 1.10, 0.70), Vector3(-9.6, 0.55, RECEP_Z - 0.15), _m_wood) - _wall_z(-6.9, RECEP_Z, Z_FRONT, _m_wall, 6.8) - for i in range(3): - _chair(Vector3(-7.6, 0.0, 5.5 + i * 0.85), deg_to_rad(-90)) - _glb(P + "office-plant.glb", Vector3(-6.4, 0.0, 6.6)) - -func _conference() -> void: - var cx := (CONF_X0 + CONF_X1) * 0.5 - var cz := (Z_BACK + CONF_Z) * 0.5 - # the long table everyone dreads - _slab(Vector3(3.9, 0.09, 1.35), Vector3(cx, 0.74, cz), _m_wood) - for s in [-1.0, 1.0]: - _slab(Vector3(0.16, 0.72, 0.90), Vector3(cx + s * 1.6, 0.36, cz), _m_trim) - for i in range(4): - var x := cx - 1.35 + i * 0.9 - _chair(Vector3(x, 0.0, cz - 1.15), 0.0) - _chair(Vector3(x, 0.0, cz + 1.15), PI) - # whiteboard - _slab(Vector3(2.4, 1.15, 0.05), Vector3(cx, 1.6, Z_BACK + 0.10), _m_ceil) - _slab(Vector3(2.5, 1.25, 0.03), Vector3(cx, 1.6, Z_BACK + 0.07), _m_trim) - -func _manager() -> void: - var cx := (X_MIN + MGR_X) * 0.5 - var cz := (Z_BACK + MGR_Z) * 0.5 - _chair(Vector3(cx, 0.0, cz - 0.9), 0.0) - _chair(Vector3(cx - 0.8, 0.0, cz + 1.1), PI) - _glb(P + "office-plant.glb", Vector3(X_MIN + 0.7, 0.0, MGR_Z - 0.8)) - -func _break_room() -> void: - var cz := (BREAK_Z + Z_FRONT) * 0.5 - # kitchen counter along the window wall - _slab(Vector3(0.62, 0.88, 3.4), Vector3(X_MAX - 0.42, 0.44, cz + 0.6), _m_wood) - _slab(Vector3(0.70, 0.06, 3.5), Vector3(X_MAX - 0.42, 0.90, cz + 0.6), _m_trim) - _glb(P + "microwave.glb", Vector3(X_MAX - 0.45, 0.93, cz + 1.7), deg_to_rad(-90)) - _glb(P + "vending-machine.glb", Vector3(BREAK_X + 0.9, 0.0, Z_FRONT - 0.6), PI) - _glb(P + "vending-machine.glb", Vector3(BREAK_X + 1.9, 0.0, Z_FRONT - 0.6), PI) - # two round tables with chairs - for i in range(2): - var tx := 7.4 - var tz := BREAK_Z + 1.6 + i * 2.6 - _slab(Vector3(1.05, 0.07, 1.05), Vector3(tx, 0.73, tz), _m_ceil) - _slab(Vector3(0.12, 0.70, 0.12), Vector3(tx, 0.36, tz), _m_trim) - _slab(Vector3(0.62, 0.05, 0.62), Vector3(tx, 0.03, tz), _m_trim) - for a in range(3): - var ang := deg_to_rad(a * 120.0 + 40.0) - _chair(Vector3(tx + sin(ang) * 0.95, 0.0, tz + cos(ang) * 0.95), ang + PI) - -func _dressing() -> void: - _glb(P + "office-plant.glb", Vector3(2.9, 0.0, 6.4)) - _glb(P + "office-plant.glb", Vector3(-0.9, 0.0, -3.6)) - # wall clock, because every office has one you stare at - _slab(Vector3(0.42, 0.42, 0.05), Vector3(-3.4, 2.15, Z_FRONT - 0.02), _m_ceil) - _slab(Vector3(0.48, 0.48, 0.03), Vector3(-3.4, 2.15, Z_FRONT - 0.01), _m_trim) - -## Where the player should start: just inside the entrance, looking into the bullpen. -func spawn_point() -> Vector3: - return Vector3(-8.6, 0.0, 7.2) - -func spawn_yaw() -> float: - return deg_to_rad(196.0) diff --git a/game/scripts/Office.gd.uid b/game/scripts/Office.gd.uid deleted file mode 100644 index 5794565..0000000 --- a/game/scripts/Office.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://btc0q0j2wj0bs diff --git a/game/scripts/Tasks.gd b/game/scripts/Tasks.gd index 588368c..f56d6b0 100644 --- a/game/scripts/Tasks.gd +++ b/game/scripts/Tasks.gd @@ -68,6 +68,12 @@ func setup(player: Node3D, r: Rage) -> void: func register(kind: int, pos: Vector3, label: String) -> void: _stations.append({"kind": kind, "pos": pos, "label": label}) +## Swapping sites invalidates every station position. +func clear_stations() -> void: + _stations.clear() + _near = {} + phase = Phase.IDLE + func is_working() -> bool: return phase != Phase.IDLE and phase != Phase.DONE diff --git a/tools/gen_level_props.py b/tools/gen_level_props.py new file mode 100644 index 0000000..2ee0ce5 --- /dev/null +++ b/tools/gen_level_props.py @@ -0,0 +1,404 @@ +"""Second batch of procedural props: the ones the Gauntlet and the three new workplaces +need that a box can't fake. + +Same rule as tools/gen_office_props.py — if it IS a box (counters, partitions, tables, +whiteboards) it gets built in code by the level builder. This file is only for silhouettes. + + /Applications/Blender.app/Contents/MacOS/Blender --background \ + --python tools/gen_level_props.py -- [--render] + +Convention: glTF Y-up, 1 unit = 1 m, ORIGIN AT FLOOR-CENTRE, so a level can drop a prop +at a floor position with no offset maths. In Blender that means authoring Z-up with the +base on z = 0. + +The cabinet is the exception and the important one: it exports as a CARCASS plus four +separate DRAWER meshes, each with its own origin at its own floor-centre, so the Gauntlet +can pull a drawer out as its own rigid body and spill the files inside it. +""" + +import bpy +import math +import os +import sys +from mathutils import Vector, Matrix + +OUT_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "game", "assets", "store") +PREVIEW_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "previews") +RENDER = "--render" in sys.argv + + +def reset(): + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def mat(name, color, rough=0.6, metal=0.0, alpha=1.0): + m = bpy.data.materials.new(name) + m.use_nodes = True + b = m.node_tree.nodes["Principled BSDF"] + b.inputs["Base Color"].default_value = (color[0], color[1], color[2], alpha) + b.inputs["Roughness"].default_value = rough + b.inputs["Metallic"].default_value = metal + if alpha < 1.0: + b.inputs["Alpha"].default_value = alpha + m.blend_method = 'BLEND' + return m + + +def assign(o, m): + o.data.materials.clear() + o.data.materials.append(m) + return o + + +def box(size, loc=(0, 0, 0), rot=(0, 0, 0)): + bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot) + o = bpy.context.active_object + o.scale = (size[0], size[1], size[2]) + bpy.ops.object.transform_apply(scale=True) + return o + + +def cyl(r, depth, loc=(0, 0, 0), rot=(0, 0, 0), verts=16, r2=None): + if r2 is None: + bpy.ops.mesh.primitive_cylinder_add(radius=r, depth=depth, vertices=verts, + location=loc, rotation=rot) + else: + bpy.ops.mesh.primitive_cone_add(radius1=r, radius2=r2, depth=depth, + vertices=verts, location=loc, rotation=rot) + return bpy.context.active_object + + +def bevel(o, width=0.008, segments=2): + m = o.modifiers.new("bevel", "BEVEL") + m.width = width + m.segments = segments + m.limit_method = 'ANGLE' + m.angle_limit = math.radians(40) + bpy.context.view_layer.objects.active = o + try: + bpy.ops.object.modifier_apply(modifier=m.name) + except Exception: + o.modifiers.remove(m) + return o + + +def shade_smooth(o, angle=34.0): + bpy.context.view_layer.objects.active = o + o.select_set(True) + try: + bpy.ops.object.shade_auto_smooth(angle=math.radians(angle)) + except Exception: + bpy.ops.object.shade_smooth() + o.select_set(False) + + +# ---------------------------------------------------------------- props + +def p_cabinet_carcass(M): + """Four-drawer cabinet with the drawer OPENINGS empty — drawers ship separately.""" + parts = [] + W, D, H = 0.46, 0.62, 1.32 + T = 0.022 + for s in (-1, 1): # sides + parts.append(assign(box((T, D, H), loc=(s * (W / 2 - T / 2), 0, H / 2)), M["cab"])) + parts.append(assign(box((W, T, H), loc=(0, D / 2 - T / 2, H / 2)), M["cab"])) # back + parts.append(assign(box((W, D, T), loc=(0, 0, H - T / 2)), M["cab_top"])) # top + parts.append(assign(box((W, D, T), loc=(0, 0, T / 2)), M["cab"])) # base + for i in range(5): # shelf rails between drawers + z = 0.045 + i * 0.312 + parts.append(assign(box((W - 2 * T, D - T, 0.012), loc=(0, 0, z)), M["cab_dark"])) + plinth = box((W - 0.04, D - 0.04, 0.045), loc=(0, 0, 0.0225)) + parts.append(assign(plinth, M["cab_dark"])) + return parts + + +def p_cabinet_drawer(M): + """One drawer, origin at ITS OWN floor-centre so it can become its own rigid body.""" + parts = [] + W, D, H = 0.40, 0.58, 0.27 + T = 0.014 + parts.append(assign(box((W, T, H), loc=(0, -D / 2 + T / 2, H / 2)), M["cab"])) # front + parts.append(assign(box((W, T, H * 0.7), loc=(0, D / 2 - T / 2, H * 0.35)), M["cab"])) + for s in (-1, 1): + parts.append(assign(box((T, D, H * 0.8), loc=(s * (W / 2 - T / 2), 0, H * 0.4)), + M["cab"])) + parts.append(assign(box((W, D, T), loc=(0, 0, T / 2)), M["cab"])) + handle = box((0.17, 0.030, 0.032), loc=(0, -D / 2 - 0.012, H * 0.60)) + parts.append(assign(bevel(handle, 0.008), M["cab_dark"])) + label = box((0.10, 0.006, 0.036), loc=(0, -D / 2 - 0.004, H * 0.26)) + parts.append(assign(label, M["paper"])) + return parts + + +def p_file_folder(M): + """A manila folder. Lies flat; the Gauntlet stands them up inside a drawer.""" + parts = [] + body = box((0.24, 0.32, 0.014), loc=(0, 0, 0.007)) + parts.append(assign(bevel(body, 0.004), M["manila"])) + tab = box((0.075, 0.030, 0.012), loc=(-0.06, 0.172, 0.006)) + parts.append(assign(tab, M["manila"])) + for i in range(3): # paper peeking out + sheet = box((0.225, 0.30, 0.003), loc=(0.002, -0.004 + i * 0.003, 0.012 + i * 0.003)) + parts.append(assign(sheet, M["paper"])) + return parts + + +def p_desk_phone(M): + parts = [] + base = box((0.21, 0.24, 0.055), loc=(0, 0, 0.028), rot=(math.radians(-7), 0, 0)) + parts.append(assign(bevel(base, 0.010, 2), M["plastic_dark"])) + for r in range(4): # keypad + for c in range(3): + k = box((0.030, 0.024, 0.008), + loc=(-0.038 + c * 0.038, -0.052 + r * 0.032, 0.062 + r * 0.004)) + parts.append(assign(k, M["plastic_pale"])) + cradle = box((0.20, 0.075, 0.045), loc=(0, 0.086, 0.085)) + parts.append(assign(bevel(cradle, 0.012, 2), M["plastic_dark"])) + handset = box((0.055, 0.235, 0.048), loc=(0, 0.086, 0.128)) + parts.append(assign(bevel(handset, 0.020, 3), M["plastic_dark"])) + for s in (-1, 1): + ear = box((0.070, 0.062, 0.036), loc=(0, s * 0.088 + 0.086, 0.142)) + parts.append(assign(bevel(ear, 0.016, 3), M["plastic_dark"])) + # coiled cord + pos = Vector((-0.10, 0.02, 0.10)) + for i in range(9): + a = i * 1.35 + nxt = pos + Vector((math.cos(a) * 0.018 - 0.006, math.sin(a) * 0.018, -0.010)) + s = _span(0.006, pos, nxt) + if s: + parts.append(assign(s, M["plastic_dark"])) + pos = nxt + return parts + + +def p_guillotine(M): + """Paper trimmer. The alignment guide is 2 mm off and always has been.""" + parts = [] + base = box((0.42, 0.46, 0.030), loc=(0, 0, 0.015)) + parts.append(assign(bevel(base, 0.006), M["board"])) + grid = box((0.34, 0.38, 0.002), loc=(-0.02, 0, 0.031)) + parts.append(assign(grid, M["paper"])) + rail = box((0.030, 0.46, 0.028), loc=(0.196, 0, 0.044)) + parts.append(assign(bevel(rail, 0.005), M["steel"])) + guide = box((0.30, 0.020, 0.026), loc=(-0.05, -0.16, 0.043)) + parts.append(assign(guide, M["steel"])) + arm = box((0.022, 0.44, 0.055), loc=(0.185, 0.02, 0.10), rot=(math.radians(16), 0, 0)) + parts.append(assign(bevel(arm, 0.005), M["steel"])) + blade = box((0.016, 0.40, 0.075), loc=(0.170, 0.02, 0.085), rot=(math.radians(16), 0, 0)) + parts.append(assign(blade, M["blade"])) + grip = cyl(0.018, 0.10, loc=(0.185, 0.235, 0.175), rot=(math.radians(16), 0, 0), verts=12) + parts.append(assign(grip, M["plastic_dark"])) + return parts + + +def p_shredder(M): + """The Gauntlet's perfect tool, if you work out that's what it is.""" + parts = [] + bin_ = box((0.36, 0.30, 0.44), loc=(0, 0, 0.22)) + parts.append(assign(bevel(bin_, 0.012, 2), M["bin_grey"])) + win = box((0.22, 0.006, 0.26), loc=(0, -0.153, 0.24)) + parts.append(assign(win, M["glass_dark"])) + head = box((0.40, 0.34, 0.13), loc=(0, 0, 0.505)) + parts.append(assign(bevel(head, 0.014, 2), M["plastic_dark"])) + slot = box((0.30, 0.020, 0.012), loc=(0, 0, 0.572)) + parts.append(assign(slot, M["black"])) + for i in range(3): + led = box((0.016, 0.014, 0.010), loc=(0.13, -0.15 + i * 0.02, 0.545)) + parts.append(assign(led, M["led"] if i == 0 else M["black"])) + return parts + + +def p_server_rack(M): + parts = [] + W, D, H = 0.62, 0.90, 1.90 + for s in (-1, 1): + parts.append(assign(box((0.030, D, H), loc=(s * (W / 2), 0, H / 2)), M["black"])) + parts.append(assign(box((W, 0.030, H), loc=(0, D / 2, H / 2)), M["black"])) + parts.append(assign(box((W, D, 0.030), loc=(0, 0, H)), M["black"])) + parts.append(assign(box((W, D, 0.030), loc=(0, 0, 0.015)), M["black"])) + for i in range(9): # blade servers + z = 0.14 + i * 0.19 + u = box((W - 0.07, D - 0.10, 0.155), loc=(0, 0.01, z)) + parts.append(assign(bevel(u, 0.005), M["server"])) + for j in range(4): + led = box((0.014, 0.010, 0.012), loc=(-0.20 + j * 0.05, -D / 2 + 0.06, z + 0.05)) + parts.append(assign(led, M["led"] if (i + j) % 3 else M["led_amber"])) + vent = box((0.22, 0.008, 0.10), loc=(0.09, -D / 2 + 0.055, z)) + parts.append(assign(vent, M["black"])) + return parts + + +def p_sofa(M): + parts = [] + seat = box((1.85, 0.86, 0.30), loc=(0, 0, 0.34)) + parts.append(assign(bevel(seat, 0.045, 3), M["fabric"])) + back = box((1.85, 0.24, 0.52), loc=(0, 0.31, 0.62), rot=(math.radians(-8), 0, 0)) + parts.append(assign(bevel(back, 0.050, 3), M["fabric"])) + for s in (-1, 1): + arm = box((0.20, 0.86, 0.28), loc=(s * 0.825, 0, 0.56)) + parts.append(assign(bevel(arm, 0.055, 3), M["fabric"])) + for i in range(2): + cush = box((0.84, 0.76, 0.14), loc=(-0.44 + i * 0.88, -0.02, 0.545)) + parts.append(assign(bevel(cush, 0.050, 3), M["fabric_light"])) + for sx in (-1, 1): + for sy in (-1, 1): + parts.append(assign(cyl(0.030, 0.19, loc=(sx * 0.78, sy * 0.34, 0.095), verts=10), + M["wood"])) + return parts + + +def p_bin(M): + parts = [] + body = cyl(0.15, 0.36, loc=(0, 0, 0.18), r2=0.175, verts=18) + parts.append(assign(body, M["bin_grey"])) + rim = cyl(0.182, 0.028, loc=(0, 0, 0.352), verts=18) + parts.append(assign(rim, M["plastic_dark"])) + for i in range(5): # crumpled paper spilling out + a = i * 1.3 + ball = box((0.075, 0.075, 0.065), + loc=(math.cos(a) * 0.06, math.sin(a) * 0.06, 0.36 + (i % 2) * 0.05), + rot=(a, a * 0.7, a * 0.3)) + parts.append(assign(bevel(ball, 0.022, 2), M["paper"])) + return parts + + +def p_stapler(M): + parts = [] + base = box((0.052, 0.175, 0.022), loc=(0, 0, 0.011)) + parts.append(assign(bevel(base, 0.008, 2), M["stapler_red"])) + anvil = box((0.040, 0.040, 0.006), loc=(0, -0.058, 0.024)) + parts.append(assign(anvil, M["steel"])) + top = box((0.048, 0.168, 0.030), loc=(0, 0.006, 0.041), rot=(math.radians(4), 0, 0)) + parts.append(assign(bevel(top, 0.010, 2), M["stapler_red"])) + hinge = cyl(0.012, 0.052, loc=(0, 0.082, 0.030), rot=(0, math.radians(90), 0), verts=10) + parts.append(assign(hinge, M["steel"])) + return parts + + +def _span(r, p0, p1, verts=8): + p0, p1 = Vector(p0), Vector(p1) + d = p1 - p0 + n = d.length + if n < 1e-6: + return None + o = cyl(r, n, verts=verts) + quat = Vector((0, 0, 1)).rotation_difference(d.normalized()) + o.matrix_world = Matrix.Translation(p0 + d * 0.5) @ quat.to_matrix().to_4x4() + bpy.ops.object.transform_apply(location=True, rotation=True) + return o + + +PROPS = { + "cabinet-carcass": p_cabinet_carcass, + "cabinet-drawer": p_cabinet_drawer, + "file-folder": p_file_folder, + "desk-phone": p_desk_phone, + "guillotine": p_guillotine, + "shredder": p_shredder, + "server-rack": p_server_rack, + "sofa": p_sofa, + "wastebin": p_bin, + "stapler": p_stapler, +} + + +def materials(): + return { + "cab": mat("cab", (0.62, 0.63, 0.64), rough=0.45, metal=0.6), + "cab_top": mat("cab_top", (0.58, 0.59, 0.60), rough=0.42, metal=0.6), + "cab_dark": mat("cab_dark", (0.22, 0.23, 0.25), rough=0.5), + "manila": mat("manila", (0.78, 0.66, 0.40), rough=0.95), + "paper": mat("paper", (0.93, 0.92, 0.88), rough=1.0), + "plastic_dark": mat("plastic_dark", (0.10, 0.10, 0.11), rough=0.55), + "plastic_pale": mat("plastic_pale", (0.80, 0.79, 0.74), rough=0.6), + "black": mat("black", (0.05, 0.05, 0.06), rough=0.6), + "steel": mat("steel", (0.66, 0.67, 0.70), rough=0.32, metal=1.0), + "blade": mat("blade", (0.86, 0.87, 0.90), rough=0.14, metal=1.0), + "board": mat("board", (0.34, 0.24, 0.16), rough=0.75), + "bin_grey": mat("bin_grey", (0.30, 0.31, 0.33), rough=0.7), + "glass_dark": mat("glass_dark", (0.12, 0.14, 0.15), rough=0.2), + "led": mat("led", (0.15, 0.85, 0.30), rough=0.3), + "led_amber": mat("led_amber", (0.90, 0.55, 0.10), rough=0.3), + "server": mat("server", (0.24, 0.25, 0.28), rough=0.5, metal=0.4), + "fabric": mat("fabric", (0.30, 0.33, 0.38), rough=0.98), + "fabric_light": mat("fabric_light", (0.38, 0.41, 0.46), rough=0.98), + "wood": mat("wood", (0.36, 0.24, 0.14), rough=0.6), + "stapler_red": mat("stapler_red", (0.52, 0.08, 0.10), rough=0.4), + } + + +def export(objs, name): + objs = [o for o in objs if o is not None] + bpy.ops.object.select_all(action='DESELECT') + for o in objs: + o.select_set(True) + bpy.context.view_layer.objects.active = objs[0] + path = os.path.join(OUT_DIR, name + ".glb") + bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', + use_selection=True, export_apply=True, export_yup=True) + print("[props] %-20s -> %s" % (name, os.path.basename(path))) + bpy.ops.object.select_all(action='DESELECT') + + +def render_preview(name): + scene = bpy.context.scene + engines = scene.render.bl_rna.properties['engine'].enum_items.keys() + for want in ('BLENDER_EEVEE_NEXT', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'): + if want in engines: + scene.render.engine = want + break + scene.render.resolution_x = 520 + scene.render.resolution_y = 520 + if scene.world is None: + scene.world = bpy.data.worlds.new("preview") + scene.world.use_nodes = True + bg = scene.world.node_tree.nodes.get("Background") + if bg: + bg.inputs[0].default_value = (0.07, 0.07, 0.08, 1.0) + lo = Vector((1e9,) * 3) + hi = Vector((-1e9,) * 3) + for o in scene.objects: + if o.type != 'MESH': + continue + for c in o.bound_box: + w = o.matrix_world @ Vector(c) + lo = Vector((min(lo.x, w.x), min(lo.y, w.y), min(lo.z, w.z))) + hi = Vector((max(hi.x, w.x), max(hi.y, w.y), max(hi.z, w.z))) + centre = (lo + hi) * 0.5 + radius = max((hi - lo).length * 0.5, 0.05) + dist = radius * 2.9 + d = Vector((0.66, -0.72, 0.30)).normalized() + bpy.ops.object.camera_add(location=tuple(centre + d * dist)) + cam = bpy.context.active_object + cam.data.lens = 58 + cam.rotation_mode = 'QUATERNION' + cam.rotation_quaternion = (-d).to_track_quat('-Z', 'Y') + scene.camera = cam + for off, e, s in ((Vector((1.0, -1.0, 1.1)), 58.0, 1.2), + (Vector((-1.1, -0.5, 0.3)), 17.0, 1.8)): + bpy.ops.object.light_add(type='AREA', location=tuple(centre + off * dist)) + L = bpy.context.active_object + L.data.energy = e * (dist ** 2) + L.data.size = s * radius + scene.render.filepath = os.path.join(PREVIEW_DIR, name + ".png") + bpy.ops.render.render(write_still=True) + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + os.makedirs(PREVIEW_DIR, exist_ok=True) + for name, builder in PROPS.items(): + reset() + M = materials() + parts = builder(M) + for p in parts: + shade_smooth(p, 34.0) + export(parts, name) + if RENDER: + render_preview(name) + print("[props] done -> %s" % OUT_DIR) + + +main()