From 38672ec938499ce9e610c3e1848e74880b78a845 Mon Sep 17 00:00:00 2001 From: hgn Date: Thu, 26 Jan 2023 09:44:19 +0000 Subject: [PATCH] LPR - Skating --- maps_src/mp_gridmap.mdl | Bin 1311520 -> 1338480 bytes player_device_skate.h | 1458 +++++++++++++++++++++++++++++++++++++++ player_device_walk.h | 25 +- player_interface.h | 35 +- player_physics.h | 4 + rigidbody.h | 91 ++- skaterift.c | 5 +- 7 files changed, 1613 insertions(+), 5 deletions(-) create mode 100644 player_device_skate.h diff --git a/maps_src/mp_gridmap.mdl b/maps_src/mp_gridmap.mdl index 6bcb5c9581d17d4589b2bc514700f70296984ab8..810f659590d16edcf44b9483b4aa760d445df82c 100644 GIT binary patch delta 88543 zcmb512YeLO`u;aGDG5?SlPUtDqSS;=vKvrTL@X#MO+^Gmihu>KPNai?gasE6rK&Vh zN)joOL{Yh5fonrV#d;MRcCf(z`Och4W_M@(-(NmHJkOl>J?FgdIcLty?#`|&XY@I7 z_?kNcf!8-C1pH zBM^X3q$LGTluHQ|&MOso?xU*VACu49GUxm9$<_Z_9J*(8ZfMX~Zx`kiHnQlI{y`am zzuIc^iFq4BTYq{nw58ypBFcU#la?XMb~szwMIhj9!^uMNsSW;`r#YNkIS{z*g=#O2 z%Gn&M^8TYQ4jQmI)Zva})j0^!d5NQ9ZBUu&6lKenTRHq`g~k=vr7Ja!L&dYhl`A%` z*t1I@FhkZWs)Re~@Qtp4Ksi;quxfai4&T-xV{}Q?@IoD))M2vBmsSn$(P5eHfxxun zK;T$v__z*d^^xa0fxwzt;hL3n*fc57TP0U?3x{<0t`48m*o^KL9<9R}7bXP?Wt|-i zFVo?r7ZH6RP$6W;%>hNn&a#Vsl1H<)NrCbzU!N6jkleW9CGC;|_fesH_zE39r9%Sk z-#vVf4)^PDvku2~53knYeVRu84s@sWiy0M)sDD5?^b+@4Jg86d)s|XWc$2^qdyN-RmY6$*RBXjJ?-hn>OUCFAD~bcqHqL{+x2>-!u1)4{eiwo%i=e z`5!wqx}d|UxpDq4EV(JPMgD24UW@Wy(z#OR??>iZaTPWP2iq%j=?9adDzy6QK*qxB z;{*Eimr=I=f9s{Z=!BS^vGmlR?E>3=d7;hdIdSdZQSAmhfoDflmcK<6r$XlFmYKKr zkFRic=b+uev9fOzooID`+L<$!eacKr-I#gk&G?Kf&d#zkF8#{CqWqU1U6uJ&ub2er zf7p-R>?ye7^U5zCn{ZYh0WIG4W!om5w`cCYCO)9Mn=Y{Z%UrdqaDM)D&ZZ3UMf)y1 zR5mUm9KN9euD zMMZDD5Y8o_2D6W(joorjVdHl5;sRRo+(Y&hytk-g_2M~;oKCqYP`-2FwnxL=(iL5w7b0(N_%Qa`Jmh*8IL5@ujDa=Z*1SR`iJk&u!r=ssaNH0 zkYlGe=0*zUCDqRuyZIhFq&f57ZHM9DmGMKj;{6`QLs!4yXLdvBr{3A#YuS?UH|@{0 zyk(ozhTnYMdgiv`HlDuncDv6fAN#6ZASFA)&;PcJIriRfJvcj9bNut|$nyKz9n*UL zv8wQwha=wMx0idMXTPvrq3gux3S*0o7jB5I!08c~eWT<5UHdLk9o)C;c;T^Dk9s?> z{TqHXJ9MuuMmu^(`!_#fJyF=Xb$o@q-jCY>JvM%Ja9H!H+uc)aY^VA~kNX6)?H0#B zsaKaM|DiML7q#+f|LhG;0%0rtgUnrj4YwCUy_W{2J$LnYg`>)j}V0M&$qc)cn&J54%)Ue{gy?5CwrRD5!UZ-ZM z7r#7a8&R}fv^{rs_xeSa8jXxSFR5OcC)Qn^7Ov8{g(EUMw@4j3G|4_!4o`n(OO-b- zE&8v9FmmjYq#7Ima1{;}>VX z+kT)un6-y>%H8+&lZD@W7gfPtkFB20wmTR<=xAo&e?4m_F!;R%xnsv&Sop%p_}TpS zgyTP}f9oj!Rw-7?!kd1K@8HtlLv{y)vbzMA_8eztFu3AvX@%9=6_#ofzur&wn``@L z4mh>v$rcyd0g>^^SGE?u(hwXyJ6O+DFxmNg;$>`oK-Kr!qb;5u{*fquWJgu z{vCFfFDf&7Vce80ym!9sU+vQNQU3dOy;Qh!R*Ane{t9)+ug)BQaNQ?+%FIo-Grs@n zbJDZlJ+J89^Wp<~z3)oPi{^j-W#^xRt>RRUISsOL3Hqbf9-mQ*yzr-KWpF0ebeFFmL5rZr9N5t{V+ z6X~;Rzg-xvA0JRs|0%ZrhokrIX|#P?8Yl19H}1^Mp5LSJ?(@xvUgVCC_{O)V*%iK; zaB<;pduOzBH_c;NcNIRoaA8*DM1E4e>I0JI=VAGJ<@fdpd-#%Y`0CDeY9_B~Qs^|6 zov}LHKb@0#;A7#@84a?2p0{B;y~~`Fg--a>h1K-zhofwC z@#zKenV-LLRdHM6-nqg)8$Rh>l-^;}CBA*%z4|dbar$;sNUFbV^U&Efz5c~(HPH5@ zCB+?hD-2Jrlzra9Lwt|6e{^TLA4*xzavw626aTS)`G(9wF-cGv%Q^EGnd}~?eFKHv={L=@` z36);$1k~!r!EG}e>xN7xNuv`lX4al{cbxyyo2==fXNT>vJMdQMpbqveyFac18Kf;M zwEYjfI=jue_d0iSBrvw!Lvj90LESG3;{2I{`xaTjGc99YxvkM0XRmT941Ms|tqoSs zurmm}a&uwwj0c0^6YWzk<2{6u^NKSfJELBk9}7R<(Kjvv?sd7%i}i1mJ}!J9)HIXY zZv95x;uq0vy3dWRZ#J==U#UL=vKF;+#+;^py!H`0VgefQV*DcTo>uE-ez*0ndv`k- zUif3?F`op+>omOU+I%|!uYY;@7hXP)gsn4l?Edk>$c@-rA<%4GCK1P;CUkIX`i|le z%KNRVeQ!$#i{2mV>wnehl7x$2pHd<7)OgWzxSnPl)pxF zxcrC2c0dM8CKlNlcq@=``ZrVJI`F=j@+J_-4(y}@EwXy47<%=uyWL%_BOODkwI-(| zb+5ycOCOBu$@|jJ+n`Q|LVAkLBs^nS=q{Z;15VBkzI^MJ=jpsh$H(#m?OJBM^oaek zEqe70e`<8-ZdEwzV04AddF%H)G(YNfo}KAcpWGfAB>&Stc8Ths@Mu=XzP9mqrH8KR zAG$#PnJu~+f6Ixe%J>?YTiV4}m^k@9`$D{V)9m1wrKRl*&g)f}*J%5z1;ggW2XyCc z<3r=s!HDO(MEM8q7*+78&u#J{%M;NBV;|bV5Ve@I=J`Ycc{BFr|;o!{!k+^?>zR=DqEl#jg{yJw9K4NweaK5Co!+M7vs4u5vf;}Jo9RG^JJ9K;A9ca(}SMR+1ywA`%cN*RfycH64@Z<$Ih1O~=liq2U>GdDJ zy>jL|zr+XRo%`8urtJJ%ch%2~U9qF*3zq-ndneOd!ChUki^Us|*FRA}U4QK!%D+?l zf%SdcOmBs^FIt}Yz0Z)|d`_28pHaD?7q)zu+3BIXcK)Iaj;CI+b4a8hbEwbi^=9n# zk6rD>cY4JUDIjl!L;>AD@#0YV;kltU&00qJe}3T~nazCODQKO3Rp@g~*{j342HP#& zz2meJxc!skcI+A@ydE z{!)^?sz+7o{d}SX^6w6YGDqZwW?r^^PrGk6@BBs?*DrU+&TrE;WZrv$S)|3|uZX&( z_YeJ`IsWR=$D%5%yt-!QycY4Rx^nBALo4LJ|MT+;y&W{Vzg%X$CGjsUdQK09-nb(- zbbiB&i~TJkTKCQR%un>PGj>&b16q(&YUf|dVDrW`I}!!-@!~O|p4ulizPoyl*FW>l zFZMinQG5cAV+e%0Q3|1)mt4g#|LE|=cRvA05^%dz*o_xdL~?>z}yd%P7AW$f+X zi;g4g8<4xNuk?JP4!r*TUs?7%XU*fM$NKo2{nz^rp0`5OJ9wT&+imS7rT>s zPfM@=N-M!m=dG}J(gHifTz~)uB^9zeC(76xkk>y^K=yku?mKRS3435+AJh=EUn?a;c7aL&uw)w}Ll2 zZ*tzz^ZF+mJ@0j5*L&%^bfva3-u|Y|u0+=f@3w3z*#3#OWpBn_e^16|ya}8>$1|kf z3L4UFyZSCIQ^$S7ZTSyt_fT`#O?TVx01gP>byLGsZ|S1HE+|TUWsLnkU0wH`lEbFj z&8C{(DG9H*v3{zzoQl1!c))(|rNT0|pK#Ng&o4RJ*7xSl6|G>WgTcvTazh8+Y8hdtDPO$}asW^vrjX4Oy54waVw&Wo>!^6%cQLok0@{Og09XG@2Yb>nj9 zziLMNuj&*0Y3btl8;eno?hf9i4lY|(CaS_;+vf#GT>H-fr9a-eBX_?I4>#JGD4?l} z+lMwPppl#J4yHeDl~gF)a42(U#rT)%m6kLQZPd-fGl-EDeDdkPU>+h3-bLZjhV9d^s+VghVglP|?C+fiKzyI?& z9k{dXJ1TESB7g7OcV7QKCoTW?@4W9acq=4&m%+2-gI@ncE56v@(ynuTlNLKpDT8)f z@7bBCf;S+qf1-fA?^JpHW8YROzVq{*gWd{>&OxvLp5Wk}2bI9<6|3(`pzSNi8A&EISGwb(N<2!wzdEb8b$(@^S+ZOzHO8f=id;RwMC%S%nQ}Fuxq!8XUx@vW= zj~SNm*(2&(lRoX)k&DQ&&)@85E-rD4-n;pG{e3ondl7qc@%sDZ5}rJ!YE%q~#;4Zu zCxbh?=Z1!L?O#x<$+^4oe%1X={v*4awOJb6QXziXPCV;a@cS!t`|-%lQT{)iyf;`c z+TS@lw)Fipcv0)z(2=843cVd{-7qnDX{Y!XF@?8o4qo3#=h1*jl>dL;SQNahYP`QU zpqsZ&-Sx^TE5j<^Z}$%;t+H)=BF9K-nYgFRGhG-jJ*~9myBnm-?J;}C*9@W|J3e885eh;_PzeG?HBJ( z8%F#$cu9@iP~JPAMOE1G=-6Q2`{Ez=I{15DaKNeUp(U-(D)M%4#k2*%Zs*1OdjtCE z+V^%f_|3`~xNWMnJ2oO`D-Hy_L+YupJJFCn)Fvg=zmq;IKDe{c>woKGU4w)7#|Pvj zT*t}STft-;&KOhQ`t-KsP8Xik!|WX&*d4z1rC`5d@f(_VgYx?OY*2P?-m}T;?{hYV zAD&P(Du$hHw*|LWj9-7=7(Qvze`n`Etc+EE_uHK)2JgP__4nELofYCegS-`dPU*NL zz5YH)+Wy{vF8l1Ko$da%GA_C}b@y+pEdNWR_uA{N5OwW!*OJ(KMtl7eozcY!TiV3D z6%u9a4d}d;)pym_#(wLN$95$O$b0wW^^g5vulNl0zER?>km!99Z^mB#L>YViPdzYa z*KSS0Lko^uyAoCK2IQ%b@QoYqTUB2FL~m6&JAm%r*j2q15^VwAEn{t?FLrJCOYbCH z`u(m%{@#GR{!sxHkG~d?_q{D|g+%XddG{o*f1*9f>)-m}$-7Hw>*;*wfzF*1RqzJn zR>0o`6#k)oje738iC(+=ZeqAk|9Y0!(CV*_!Jk8kc1vfoe0F++-4E;fbM{9$yAxd= zyqS3YeG&=Z+`nq7*T;<^e4<}H%WHUi*vMe5JqzOQk-XQUFTP&BYsL>&M!&na?M`$p z@?Pt_{yxcs|Lj*a)$8NN5N_44Ud0ka_^C64pP^c)VG+{}9Pla`&D{cL5t-+1-z%Rbu}toV8Se0lGXy#9&q7raC0 z^>@Sae9#i!HK}TKua8MJY)zoZ9_j$fpf4}GX ze%7PSe*R@1IXfu-_y+)gQU#t!j0;E={GLc8AoqDb_lm~*Atd1UJRbp-_?!KU3Pj{r zfoGxJ4DRUBrri@}gvwMaLk4k=3dnDS z2*@OC6}tJl86?V>FCE7|C>&4+BYmEXBmurN8t1PBqMxKR^UnkL-U{Q__OQRESzc=* zx`GofQ|9$|N}Vb5^Jh)CBN~-(y5`t#NLi)5&4awXySHgK&df23wEL;?+sa%29Fg!LRwce69j)Z2^y}Hor@Wt4LS*;$= zwH@m2obztzk*~)_In=s(Vb+8i&qM~dO{!7vUsc}@o&M#TD2Gb={?EzA>zpoTj5`u~ z{^ogwn!W;)S@z{k7iJ}Yw%u{~WY627ev89V4)4}pnDycV>x1Dwom1;gy8X@2wwViT zhd-VimAm%3Uim*ZSeW(8{wJIk+TU?3G_KP$eVCM8(DauI?fUhuoOgfAaMtKI3T=mT z-q`bQD1324^>y>q5S8NbmBb~o_4cfIwW(DQA#*bV{bFMH{SIax=ZdeR>L zUVO2d?|3kQ<+A7Niy?t*K9n8ev*){J)+*+W(L+4m2G z<{a7@G1b)!;MpOvYK7I&Fu}VWF7l?i<3pW4-bbXSN$-`;lt@US;Kd3akBV) z$NQnxBb{;Rw=5&=?++%ozv#A{tbOCRISwE9c`NkQEGG-s;d^DVXVp`V!}~j44_#aT zce{&#{w}>@SNbd0~!cx9`70XG7$!)TFw#=O2CFHl4m{d%J(V{Y(C3b4yN?diqU=keh6; z+FQ~-t+CEdb}T2cwt>3I-ybPX$?H)2Xs&Y}u3YYnhiiOQ<57Gj9h>k@aqVyS9!q=X z>zD233%}mrOvIci`-}Sz=WzT{(Zwq0r?QT@mHb>+u;`^=jC?oMCX>Ke9+I z{eEb|gHBZ8FX!Y=+kSF;9Yxi=+*YT@vJ;lbWZvM{VXzb(i&9SMLuE zt2^5c+cht$Hzn(-6^kMt-=9>cN?__x+qmJ@v)VTKduUo@_$0MdYJF;6t!ql0Jl*5}`nl|``Sy@%qZn4*TjmQ+I z^%uI7xA*+_+Rrj)S*?8mXYO3nFOSUNG_bEWweH&C%tMFyXQ%gG+$w!Undv3QZCR6F z76D`QE-2ZCb8pyXTj0FYNfA_+Ou>)@*_8 z-*(jYBCr3!mWK*gERX-Ohfl-H?O&^Sy8WUk|K?YpQ0%*019O`e09v-NB<}ZYp~C#5K;Xt0uqpZyy$Z|6cr0G;A*I++DWr+FAZqNrmex zmoG}bF8)>Rf}@Yw0bS8BrRYHR5Y1?I#`OMOa*vLyRW!9y{L9le<5t)K^gAZ zO^e2?ifP}{`gv!;8oR=&SI#b)u)I*$>+Fm<8O_sAgwl(?2*h_V__{Ur?`@20*)Gbz z_El*`Sz}^4Q2QI(JZZOo?eU9?e^N!o***7nDY9;y6<^_zolXb&7mtXlaP;f+qAMPY zAJWFPpR`Bl?|X8Jg3G4cUpO9j)ib%xhjb~reEa;k_cXo?t+oTYcjAE_Q58B=YF*U# z^Z4IXS^R`Eq&Ln@e(~U_I`$Mid*jz_Pu}0GsPnyZvclO*YNxi3knYQulx|4E; zR9RRU*}Xlfk@dioQud&dbf4p26tW$7Ui7Qjc^T)vCP>-2-Tpdt%FcG15`AUX`o3~vS4eA|>+_vo<~R`GWmEL4pzMqj3tHsGd~MnG;~i%I{t~ig&hB?$+VxG z?*BFC=K9{X+d-2{=i1}8eC>hUF6MjJks)o9Qtj=OdssIFadxu#R(K>cm{hx`bHjn% z`OY}s^X8@_c7*xPxZ9t%HS?YEEW5SZ-1;m$>*0J_>u}l2_7~H+{Ry9l)UM4fNcPIZ zwvqn$dO9~hPSwEr)FQ8yKn8rbz*xBO^8$X_~HwT9;nweUps8Z>fGsex9b-# zaJnrVb3@Ug;8Vrj(s1z0M@-czhc-r8Ys)&}lX=Ga_LR&x>#tqipqkwIZPC2HKeboS z^<@@qr|#27AI>TndoE?KuYB0vmv>*Z^CJ3Y{e~MYsa=~T?=?30w4QCWbEvt0s71#q zo!w^pOzWB(%nWBGr>=4`s9EN<(4#qjsZXb09P*16X0<54#CAAmf8i^kZXK>E4y?tG z)6@6qweg~d@<;DTy8EzoWaQe=VDqK;_YPNYH|B}8_BTXVZn!)+%jZk+wL)t{Q_OeP zz5a{Nx;&Wb^PTmydsc?>&3EFx{>g_s2EQ6MF8-~ll+LR{@0xGjdn=6krb}?B`PMx{ z?EaB}WxnzK>NR!Rb-Qp~I}Jw$36$42zGF{N+IPQF{^~VWNd-Evzf-OnZsYLC8=olt z&ibM{o3=eQ{7buoV*^?T{l2q)*MY}Fo6UFPz5d^}Xc^oa`<-}p&1w6W7k}&iv&j!@ z`1#Au=y}0KLBDU^(}DY~ey=|r_5UtLd>Nh>J-w#C>HOjVLAegk$-(d2?%rR_@cL)Ia7EDmw!0dW^QR)l_Uv&CNVc3fGp9sZ-1wQ>EUzY)zP)$i-X4xZxzu))q@pnIAauQ%sDk3WQ{Uk#CFNZk(WZ?G$0d(O00)f*(N&Ds( z2%=87HkSL|?)&c-Rv4VjRiWTm&2sre^;0IjlXG5*J*9agvKMbe#`qu9FO&HB^Y%S& zT$aB5WK{e6q=msewoBCAl|Cw ztykKYzx|^gb_MRs{5l{5GM;i@CHLO7U|-F0?a83Q!uWvP1Y-S3Al_f|PX=xW?-%Uk zwyB^`f8-ISS#`3AaBC$v-9m{9_Vu`lkc? z?R71Romalb-ILCO8F^IjTNC^$1pzU~`30T0k8{?9{QaiPO<>BNbBZ*@p7yOnh2{Fj zFCuq{VkJfA7>^LN+8LzXI37;JyWSA5?4RdCk?88;a`*WM>I z*Y=|G%edHI?Z^6i+c)#?tZH`(@=XT&YR*V!Rr^f=8QiJgS|Q^(J+^Awoc9_ zo!?qHb}nNdCKh0wRGM&armc z5sm5IMmraOP6k7LemjK@xHu6|l)pOgyEw7akump(|1hifIzdmYBaP^~tJI5xV?XJJy9P_(e zu^r%FUTebd>Zefonqsx@e9Wi(-NxbK^D?$Vf{$9g=lwdZp*Q-?wG-fjN_{REcSr8sSLpAs zwWJH@vi;$*-v^K0or?<2Uv@^7fA|3*cagf@w2!~vBDLtiyGp6VoKeAV3h?)?32#7t zYl4iuYr^$sk!nrEzbK*uzX}B8w}?0`{rvf8*3X~zIcvCsQleJIu#q#aoJjR=26lv^%F(ruqzbXKfC=Ra37=8RjS*a|}u1jIh!2ILHBAe#i9 z^V#VL2!FSI*Pno_L>25cp^FUxO&EDrdsWKjLKByPNx0oBw zTulKTBnqhH2E=|>iX^hR`Do{JnvrqI>xgLo=n*yhFBR+#>^qi{3JhtY4j3Ws=@=oR z(VrcV-w4sZ`??^?pZmYM8BZ+r{|XuN94d z$@r@!cPnM*it2zDi&w{Yuw8pQ9k8)@{SWg}&c~m7KsTU9O(&%*b}Qpcy;vN=t6iTA z+#3oyFcoZne(2M$12<#OYd?F0$hbkZ_yo3=vCP#;t6Bq~zmg&KtHAw%dltC<_FXbB zE`2(1Pcu%-Lrt71+oii|`;`z0>;B`6(~$EocPwH0HeKK@qmqVraG zT>-lu|nm+WpV?i4(WKQGOE0`hKLuD>{&-)3!4&5B*GS3Z2u|H=_?9Qn9*Vd~F zo*RUy3Vs2NlfQclrML^eF&h^N_+9U5pWAj$W3Rsgva|_8)4L|xf2rVm z^-~4UEmuK-rvtyM869};po;xTAd$b`Z?N2}U%?kg=56}oNJ@KFso&Ml?ZDZ&vI{;M zF*EOnBf4kQnn=`v=W=Y1Xg6*^v`3WqyW?+$$bE6inqb)63W*k}djsP7Go*BoXbRjL z9Q-fPh<39~M2S{u$;Bp{A=NeCS)_^9M9Icg;!i+{{Oh_G4|mbg0aKuRK>HJix;@_d zcj)n9RqcmoWDE>H@b~sdFPM`x=c%ycP;h2nsQJ-P?DNQVXxMsg)`MrvlUlpe4~9Ok ze<;eKYX01;`|dIiAHGudP-x!${O?cPMFex6z!!`2yBXteWm zZ{1s7IUHJC%^3&RVO0CMSuf7r5_z+Oo-%s+a44l+6+6rAcc13Xi0pzcbFxNXy~TM* zkdJpM`}Mnxx%XD9-c}#&>KQ2e;X^)F#?Sk~@oeWa|14|!&>79VC~duZ-6 z{G5C?A7l^pJeau0$L>CuSewtfy_NZ#TMawI&ZCKL?Q1G;u@@h$^JpTEBiEg+r}*gJ zyLfP@=03%@T@UHeLaf8^-&Z&-EYxFuehy8{V}A8M){}%@hfa6ZNT-WEUvIF3tkYgk zGP*;+@_S`&%XT=Jry%REa1wM)XMHx$c}&rL$k9#Eb)c^MkYlaC-#cy(&$5Fl>1B2g z$j$uv;gScqsvhD0c9UID|C-CUPdr{FZ*VD1rg=iE9+|(RM+I;=Ib+tQ3Hy7s=P?0g zZh!DrkLMS8NWtsC0}6V6(OGajL;1CjgPx&urUwU}M)0;k7w$urb{9ND;f{X2ndS3& zej=(1KL@vqdMSDe^cxcc19W0!<1~Ui=csvNy^BQhV)WgBe;3k3QJVMF9SN@nIx7R&6Dw86NhZ31HXXWDJ!ryNyq<_H=Tsp39Q(R z-3mOpH0GWmdS5I%qw&F=c?&c`Jh>F>?~V|Uhv(iigpaB+HXZ7e#|ZiPyCdX0oNfRA z%(64iJJ>0Y4E!qaf9T_X;ASe=8HW{6>bP4H`o~V0>mNTtx<8@=DtPBCtSO)azeVTz z$DReX<)3KEXy4hyohg`arZ{M64q}JY%{aCK9WZCH{_YewJH68Z!<4Ei@bjnrb$X(o zHK+Wu3o>=%HRy@?xkTURnb~pYy<5TE&Ru^-$IpMDI#{7=tGl~<{ku(^=WSp8>yHke zPz84*aw`x}m*@(`{$${s1=$5xnL9`3*DoO2Cm?r|Eb+G%WW_{O+`%GUy4{S|>t6OX zjSv|(@0#PypxB=$`rS)~>rVoDqCd``5u*JwmVF*)7i?YA<6^axO$LcN;4JXo3EkOmrk4&9t%=2&Gq;00Ejrc&{ud^g za6L}o<)~vRg-|bhP?iM?Q6|h_IdAC z$kVHKWnkX``4suf@dz;v*##eJE1k>b`>y!M9Jm4D?#XilV*7KsUta@%EBfup zdb-}zf#(Lqu8^#))Tt2d&(>QqWhDXG_Xj=!adoBr;%kSc3`oPig?ix;MHJrw8F=mw zY=8FlUw!<$>i&QZycw6TpWJ={Hz0P#(e3x)dQ1nN8;Sxd*f$hD8Myw%87P5rhj!-K zH!KRsc@Vhd>rc*@^r)BIo`&C~vOgrscXPn`1{$(!RfSeMuRy3D>9k}}e39NH&-3r8;xsmxNf7X`cZ)F!( z@LODN2Q024(?N|lb+5;rlF#CD{n^kw{^|Jp&2v}ohGQpMKOvOP>SgJ~o*i^hvc)n+ z*#-F1fuFy7x;o!wbo_79{$by&s(tMS(WeJ<%u9HNwCZbha$g<#p#3;Qt9u6q{q}q| z9ByP{{oBbuII=M|Jw$uA4*%__&jr7f!{lmScc>UYS zzeO9L45&arWPD(Mo!mbv&9@&xp@IU6expW1lJ>^7)%G0mM_LEOMSFA}Z{>m(8x^m>MLVwvA+hteCysKp~A*!3U zul+0b|8R=Be!ne1FudR+eXl>k_&t-%{Bd7K-_L43?#ykh{VQQzXk&}w1RD9A z)Ah?+8|~LWu@%^e8~apncNzTCwy$onzrj4_xPS|TU&ijH5$n%gjh(pn@{6--u@Tu6=G;TAAF&cIVs}3^H#-_ta={Rq@*S=2N24;*^eUkLex| z{iXSw=e|taYX*~h8BLb6;W66u*SBk7TH8%6>{xM7thR&YUPeiVMNTp}RQ6f0OrM`W z0r~l}!0{)5Z}uz*L7_HxivO88QRx% zp9I)4+?QsKzvWcm)<Oq5y}eXgtLV5LWTUVaw=}Wy~5QuU*1-AGhWJn^7o3*mPzUs>HAm3 z!KF^`j^zDS@rDWyU;MHCy`DXh8h=;JPdX8K{qKq`DU;?oreC%ql{mC`Lka_u^YBTdif@S7Vhx1Cl*`kqk}*KWAY zKHL(?)nU>l(^D$trX>HIYxP>(ppfZVbM8;wVDjXjQhWdEc5Z4~M(lpwYw@5$y#BBL zEq^FSyZVc2-}33e2`E8@1pa$7dt?oD6Ud+!H-Q;5W}LD6HUBsX@68w##HE57znr(XvKLS-0k4{>#z6qPZUtko;^$aEj8y1>9GtlZr<;~f(8$N)s&lLFi%O%F&Nq~T2{VnD=Dk8gsl8j?3;2)ncQ+6!FSrhi?sFjig;@6Pl zuY`G6;eV~*PC>kX{FKpwyC&lOOV$MAuRmu|I8(rJ@-rj4c3*52xKj|H!JtAib{A=^ zfBcknlfRpQr+v$uX8uF!Q^EMhXY2&zo|Y+20vT=s$|+_|Fha*NtV9*!Gp2$&q=7(e z2PH$QuJqSyv6HcT7R36SHD|`(u0RIfjfJD^;tc!)`oH?Si`{&@|N|Np6iXVKa1dpd|;bWB;vB8~MY;ga!})!o~jfb4ymI>j52 z{|LEL@W1-IBWnBG{TFwj&SHktnSw+C`Da`lPzKTKuekmA1T2Qgor3s{BECX{O#I!m zhWXXuKXl;umrQYtzoi~%(EZaCWTF#RNZ|i}3#jBYwiGzgl)HZeJI(7=O#}YUWfhm&=$6epgGT%vsg( z9i%b3F;h&!<}5I8QOl=f3j7zHI|cElS^SjIfqU(U_xFyd=HI^hse-#v#8>bS$n`J2 z7W~6b=M5;z-?DEqy%9M9byGlY22mAk|CmL@#^p&M-ameyaQxStYdLqlu~#Pr5i@5_ zK>wes8L``c?xIfQUp!^@_^XOj!QC2*Gx(>AQzku?ocA1+T%6*k%+1)n&3F7Q_v)v^ zn2Z}_7Ozr#?Z1EO0RR6>K(@c-+3BeMPnSz4ppu>O|K~p_vt&`@%U?^C447*Z;sKFF9IQ<;ZFq0eiaA>4ZAj&cJYJh?>AmG{w_fdIv7=#pu)#EphUxGR;ZK2_(ePgaWq%8d2^w~7@b^5}gCiZ#;c0=Dz#yUFK$25N z=8OP#*tNklj2}#gNz$c+(gGSTBa{`&nPYTmOaI__mQbDoqhTB?2o(j6(J+3M1RCTR z4da(AqzH71hARt{oh{H88pdW}M9>%-t|C+wstH7ihN}ydr3wUxhFu#>!}!5;SVO2O zkRTeaB~Vs7N#jp89Kfy-rePdmI;97e81X2I>n91V*iX z(jAUyXbQ=k5x@b)2^&nqXqXNg3FiupjfT%NWlh9QjfNXX9(XyWu7wMS7gA*SB5@1h zVu2>n@FfCeEd{zk!`LWmCD08T#zxtDT7K^fXc!xi?j88j6u<-m(4U)QnBV~V3v&#& zR#|hQjnEbyX}uz)R#IBz+-_B?m+h>Qj*+UjRjrGr4-yX+h6uMfI`Zn?^wm~e+M+-P`&FjBZv7-ckk zmoQowBaB5y9$%SKtNz`R6NHJvJ;J@heMa9G`Eg}Rb!(Ci|0O&iJSbQ~w$boI0ycc6 z7!6Mqu;DY!Xn49XLzpSdG8&#O%n=?ICL1lb0t7Hum?zLV8YY=-kI8R|@cQfrLnGmC>sr zt1_!rw^r+Lt?;Cj8v&k0+MhOq(kR-<780qAW;!vqD;+l_{Eg>^}mV{{Ij!aNzsj!aw)=L^pZ z*jx?o5OxZT3^p{3FIkg4HZ+WVm#|xSftAC-3&I|O!UCbtXxO#EMFM{Civnf%yksUn z7NZVq4gXspJ8Z6o_X`IEY_5i16J8e>8Ej}6U$Q1aY-kw!8^W7O?E`^><^Vn<{6~08 zc-v_B9pPQ!J>f7q^5dbD>M2Jhj|lLGk%!i#)T;4`E*AM%tGM zX!v;KlQk*lC;bpfTC4VcRmsl+{97crHs$<|e@gx>oEFXqfl|6<5zufcp|ns&C~Gua zF7nLUlv=k`l&mBq3n@m!m4&l~DneDG;c7y4AyuehG+a}tCDaz`7!98j>H1_!tzZMm zhC(CZT%+N}!g)dyp{dbuGvR!pxo`nGvgOH?>U}SkyhwmA5n2kZgw{sGZG^T$J0Zf7jiB1^fem3LAX)qC-gTOzDc-Q7$DFU8m8-k!XTlK(PAq= z0E2}g!YxL_w;G^tGa4RhfWF;mc$fkD4x{1W0>KjW2&3VV!u6$eSt83j?E{BLN#7-m z7RI0><9bxB-ju^}rWiAclJMO|!{Z`P9!)vVnkad%08bF^7bclwH0AdRlZAhoV>IRX zJs{j;j?t9k_n=@2Uq3`cm1z_pECogmThRDm*fd4fa3t_`LG z{9rm{gPtZPDKz{%mnI6Q3uJ?a9iziE%w-0q!x`e40;7P2X9<+e7RVkAyEd41@q_7a zj_@$ci(%sco+~^okU1lOhFu#>!}!5;I8T@_Jc5q2zO-uf`eDf&0bV3579JHM=*TU& zJSMqRfR_u83r`3u(2*Qm)<~`r;I+b&!c)T2=tz-VuG%QMUVt|V&j_1^XN`uR6SfFj zg>6Q|+l5>qPsm3{wyf73`i@8u^g`s2$SRWIC1J1dvha%Vs?qR1;ori3;egTbYr^Zo z8^W7L!v{^-A@P5VhFu%{mVlqQTHtMC#OWP$UT>(BUyeGUb&>tGcM%fX8 z{?ITs%03W|8VzFu=nsvC=^CIva$efuYe!?l|UIjUmFc$ zJ1%^~)v6+m+m zqv0O}qUG4t@CkwJu(=xkQTR!~=4$w7;TM6C!G;#==QJe9nhY_bVH|!HeiME-8a^qU z68;c4M#K31Dg0%Q(Ujx&w{Y4VqbbMl4D%l-?G&6b1{{-AMn^cJVf;!7r3C^*!(~ia zSusIihg};?Tlm4{#AgZRg;UHw2V_yf6jl@yH4dn`j&+WX>k9P*G+bY3 zATY{|FdA+sZX}#5Fw$tau|OH4#i*fSZ08A0gr-Kr&4lxX=A{FXXE&vkO*%hP^}Xck z^)FW0MFM<@&{Ak6v_?mQa*3SSlv2iOtK&2wT_6VfZ*Mf*LFg!S5;_|VcM-Y@-GmII z;Y=YYgoG@k;qF2Y;Zor;qs65HJtO0u(RHGi%B~W63s)Nr_YtlUt`)8`8oplWE8HO5 zXf)hU=r7zP+-x*FKo}?t5(XO$4-sxDt@H6#35+_h8Ry%Cp~CG(!^2G39pd3e!>$b; zA>ap(6ez>zPNQLL1VwPSmDc&l0UeGK?h-}|V~mE!3gd*kh4Dti6NHJvJ;J?4!}kdU zdA~5pXqZ3%da}`zEeWCm6#mO-_yK`vvAG(4P#`;Ou7+tVTfpXO_#t76z{p@j!}#7s z)?|+n4dXCXm?lg&8lGXwW{PJS4ZAjYwtyczN1zO!htZMNALtfmwYIU1&GfM(~Hb}j+L11RKLfrfL$#Lls+;e`VEU~@Gb z78VKETn#T49u*i1Y-o5<=`l{$WQdUwfN_WjON6CH!^=$BW8&pT!>$c}T)+>0LZA$v z73j#8qZ)LYUMawSHpFBnpvxHz(=|Y!>uAds!~-a7 zWHkJwKE24>niBPYde=Y_5ja3mXK+0vj4$$9YcHWQY+BhmJUaw+lo}Hgt}Lu~A0$bdH9xQO0O>FdD`N z(78s#1Om|g1&ad&Q33P-qv1S(XtB8(&KJlIo2%jHg&hJmSHnAnT>>M64K228XR;nZh zvZr%2jE%AZ!YHF*YydsQXqZ3%I-B|DAX^|RfPTnm_!WU@vAG(4RUkWTu7>vs{}!;h z8s0A)5EvP3XbXWeS(70~G>pS*!t25tM#FCk2Zcid$7mS8{|IlHV>IRXy)C?Bj?t9M z&kDRNL4y>aVI1ER4hsZ;hTj(`J0j2-8g^|kZQ%!hAj}o!3j~RVj|!AM%KQ@@2e4~| zX&6VC4i^g_3S@4c5N{2;s-w}d@LLjK0#YXd@BA-_*@`+G|W(aVVH)| zFdcp={8wPq(C}9RWn|6>pkdbr(=L8+e>(hHI4*o+H2ke8`%e75(XeZSe-QA4PY9IZ z^CLP^^)p>wX!<7s{zdp%_*I}gG>nb1-vqit!`LWWDXcac#s<(EjE3nNpw~ItvIX$~ z3fCJA|1JYWS4!hk(u1@Snn80%L&<4gbONB5N|lh=y_aTR1J8F&Yk( zamtd!rHqDM8(dn)_`zk24WF_`!`KMwlw}&{fDX&aK*VH2=V%xkWn@q1Xc!x1jMjFe zVQc`s%N)Z50?<1Iivt8v0W=#e8a_)*v>dw{E-#QBHdn(Hgo*+-SHqQrWPy>vh8EYf zoynREF`@&JQ(q+CVNr*=DFPa zAx1Qeb3LKH(7Z{X zaDZJSOv5eE z@VUll7#qP682zB(Ynguv34qSgFhpm>ncuxo<{3;4l91j_Ka#b_8CK@lANpv8KN%M_9go#O!GL>bxBIU2@B8KYIs zXc!wnCmRhD2tZdf8YZY)n12o`nF9DWfoO4XH9S-xJ8Z6oZx@CM*jx?YAq*E78Ej}6 zUy>%l0hT$y35OBFNa0SS;ZdgSF7ariVb=za5%7b@3Y6h9&S)4LK@lANpe@AR0)=El z=V%xkWn@q1Xc!x1m4uo`!`J}2uF)`o0Q5OV!wSl>BjkWB0}V3NINA?-j7&^Rm(K zD*`rrUNsutClHi^)BV3WfcFbzK{j-bh6#qO$)3*9Fu^cNjMf38;nxh%uNw`&VSs+q z)|!9BK?C$5qv8JuL`&3f84bTJkR4gRV>JA(@Sbp3c;9IFi12}MRKSLY-)o_N$eQdi zqG2396h0C@Hu~ep(qQF=)-hA`g%}NgDSR$`Cj8fE_$z_FiQ#Lb;o}0a5;I+(VY(zY za{Sb2_!EJ#(3n_(Z_EMwtpWNwqv7uj&_5UrpD;lGXf*tjK(GY;v(fM`0?Co&ueR2V zA$}8n7fuSNjE4UZ{uKTau%Tgm$(ro3p<(QQ3#WxMM#F)!dT}q55=t8lm$CE@Mx4qT z4VM$j2soW(G+bUFD1xhCG+a?236i06G)ypLP4;w-h6#pIvKX#P<^WDMK&KcDS2jSO zZ8TiP09`}5a15sk1WVA>jfSfUBuA1})xUip5vU?mRbe$D#b`KLsIIb9V?*PEFIkg4 zHZ+XAhEP+eWi(vdl+_WRWAr&?HUF*=t}EaO*ApnixxUdbwgy5&fsW8{BZ0DW1v*E= z*eGi(&^a2$)|mM}PiVq{VT^#XrbfdA0?^Hjh6xIw&o>%wE)Xm>SHl+wB!|s%58w-h ziv*0WhFb_13ycglG>k71cJrpYWQZMH}j9tIdFJ@jtAOCy`nN29wZDF7#T(d4dYAJ zWQYw7V;>^iBHU^;e48m7D!!fh$KiItHNwLL9N{|z%J3O(G>mP8FjAl+G<>H(*(iaw z(J(g3?h@!6Z6PpHI9eEEG>i?P#~KY22tbcB8YU=!zT0Scyg;znTn$fP{>cub4r~oi z6z(y{u7>Xw?h_ap>Y-tL$(jtYp<(Rz3zLM&M#KLyWe+g_51IqmHNs2{j&Qa>89omg z4P%=kOcm$|4Nns&n=a5f8pcN14B-JZ4InU5I8&HqG>i?PXB!O@2tdy<8YU=!e%NSu zu0UVdTn*ncPydh|Mpwi0g+~Nzu7(!~IRYbt4GrT<)?|nc4d+B&=v}#O)5VgDge3wR zUMf%)5tgAN<4)`It%Z`03Gm~>a^VRfY&49GvK7LkM#I=BYcF&(8pa0Dml_SzH9&WF zv}Ft80TlKy8eS<7J2qFts|51F=4yDgutvbiq`+8UL&IxWkz`GV7|}2ePYF*8 z>x_oio3ah!jYh++4c;W+2R|cFhR-TyH5%S05G*!V z!~YgY4x6jt{lWnOo2%j1gx3WI1{)eaP*x9aku@1&M8i0|A-pLZG#Wl+%KjsM%V^lO z!EX!r!S4u^;qxv!@?$A|r_Y>@neSqhV|S%{h#Q z=^CKfAfzo@5D%b`T>=ds6^NZ6C|DdIhzg*8G8+C_AX;p$hJO*r4x6jtUxi!& zo2%hGVY|S{U_*;7+nKD%5F;AK;Wy!T;iS>myaFd8NffM$b`wroLA zfI@Z&G+aiF6D-HBhRX^hht1V+IpHj0b2VIEs30&f)I-B(mD5Q;)?|ng4dYNzs3asC z4X2p0%Hp$)hFu$6MZgcPDo}<`HKSo{)rC|`|IiT!a19|tfsM#BUG&~*ii0|Ze4bUmZt`U25nb2Z#RAUkZXh8qfv1Z=K`&lMUAj0`rk zxUTI?)?|ng4L2?q`KWx#IaU+t^M&RD8g42yGkk&3@P(!fpNou!v9V3G5T9o>%oah} z#R6@kVQiFLQceq$&N+ZFs?fgipQg%a7#l$6m}8hg0QwPg3=?BAs*w8TcHsZEII{^)+3F$(6bBv}OzYc=z zmR-OB1{})^9R(Ue!<_`mItx@r!>$dcGyLE#LRX=iK&NOpL!c~Eps#M6tq9i$(=d)O z9R`JvK#*uSOQ4L7=nxINHkfwtgXyrl&_lS?X!tS=6!sJ@HyXB$hOZD`CcsykGJJZW zBRQ3H<3Zb33GmfIZ=sJs(`XnQW!DIFhla6HR$ZuJG>i?PIk(U-T>~`drL<)W;sF$L za-!jD1!Cve)$nx!`CxN3e7(?Dz~*ZB2H{45vA~9g`!b7UO@Jd1pffb=+F*ji z557YP3M?anM8m@c$}$Clvp7JwMwo_igz2!WFhU?JG~7*~Y@|RoXxO#Ew2L21hj$92 z1V#ZZwgPvV!qH-~#{tF(8%)D!m=4DXV+BSH4UZEjBXdRo4ZAj&cJYJh?{4OQyu<|i zz~PC~l-(oTYcz}xHuyfH;rmS)K9h`wu}v2KCERT^{D3KYP(0o;2Xu&$LdxhI4P&E> z)072@hOq%O3lk7g9;sD0DPzVcjj)t*Own$iPG>i?PA2k{#5P*&t4HFbVFF`Yxh@}G2VstgUOdvaK zu7)2ImJ8Ti4L>eCAuux7&@jGaO@i3au!X}4VWqIjXn3_LTO(d;H0;{oCk6cArv%FI zdD>_g+d5(W|Lf^4z^$yhH4JYHX~pgY#qLH>?C!4L?(V=P3U=4F!0ztu?(XjH4)nbD z+T%Otx}N>KbBw_^=Un0W#R~S3`KpjwtK;efn;T$FwQGXSW!6*kmGS|SSr2kPQ0>eP z0{Mu_>`;(Lc>gU%glmJ-S~r=m3vQ>^WWGM!5cHbNH-?*nS4K}}vu^Ezdh$iB{4j8H zxFy_LnQsfXhdY9`%;xS4`qna=^ZwrzJWuAkgW5g8ua#M^r73ur%=d=-!u`RfGCvU1 z9t<{~( zzO~Hed;q=lo_B@!UwEd9-U`3i05?7MFmwED@IF5mY$~(&Snc^>Gntznd#*XY$@~98 zcriGo%r6Bs8`)6irpF#^j&1mIcqP1AnP02g>+u_vx#_ipH-kZbE2tTIyE5y&6W$Fr zlKH)$_I|Lr%zA1c1e?pOr{=Sex3|?^c%L;Pd%tCN5XkpeW`~0811j@}!C|eN%pV1p z(`)ia-v5tN_@tUm=1;?C!7Jkz%52uH-B3?v{m;V};mgYWRn@+Zzp2daUaP{lu|fW> zYKFeA%z8hBAA^l#{wb*a9BeMLp4u!&APQ4>dCDCU+Bo6sg4z79y^Q^#tqgo zcZ{F+f4l?(3Njn)7RC=AB6Ihu^@wev&rOd#%N+L%y+ZF`Q-Z4 zb(ki2w#?H8wdsO=AMTlP2>_VdkLMWbPjZ1h0&q%x2x%?e%2VpC!y1273Q328P*!;_P9L%G~sL z&R~w`3TkHNuFQJ#gn5H!%RFCDn?Kk`ZV8%-3j~|Xtf#hMSg11VL0-5rI|$@ODzigD z_Q92TQSZOgDrC{lyjXBMy(aVGVTqvEWF8cj3|<*Mna#Sj3+l{-iu|AiI9N@3+-Q<+x@YO4mD$=vkV zgUzuGR|~5Lhm?7Zpk^QY$=qt1?7;@vhHHkk!rGO2ovN)HuUDCy9S+KdxdTR58`6{y>Wbe1k4gz@z@4tl)ic>-Mfs%Ri z;I!6F<}HHT={1@E7q$#~P3En_*1;>ICvVWo55sQlhMF=P*d}ZnwyVtBS8a!Q$I9IF zc&A{FcMfW1cB#yItzGkDw_qcgcMocN1e?pOr?zLXxy*WMK5RZeGV4M1d6L;dklbp2 zqAh%~oeJ_wm3gn=w0ceEy@T87HJSGb`v$!x^L}Cf;FZza-}^5Zc564(l-ckB;lOZE zWj?rShs47wbJOEPgE{s#tC=~hG9RijJRBZuB=ZqLZA7rS%zA1ggUw~uQ`;!`{K%{a z+2=`S2SLli2hf6@3bIeE%tr<{v2HRS6~xSvSLXAB!|FAeF9>d@*JQphTom-0%om4Cf>)-c zX~Blw+6^^j*1t4d7A~*MSA;9WRl!#BHt>=zqgLwf9C zGT#tx3^xUv%6xNByCv96=BCGtXplRs-%B(jihkGElk<3e0?ZIT5>$9HPL&4@U>!}S7 zzF{p3K@;+EmDxccA5)nf3i7d)`QhNOdQIj>g3IYOIc{}4n!=FWLbJ)dRBD5RS4K}} zvu^E%dNS)j7C#=I2r@qz)Se1YSLUu>-hbhlV33~;YKESx%zDp<7lMb&{9;ghDcDG6 zJ++sE&1Ke8TQdh<+uwL%v39Hpd5c=JgFxP_*6dJ_H?K9nlA6QnHJM)xE~nSzmhf75 zJ!m$W-w1C8uZ*6|X5HEi^<>t6E4&@vsm$+&_rm+(gUbA&_y41WkHaTH=1;?C;q&lC zW&SdJ6}}GNROWBPcj5c+LuLLk{1ko;zf|U5!*Aht-~T^q!GDIo!r$Sa$~-3g8~zI& z-Mcc66~+$ZgmEkLc%fSuKXk9mJwngWEA*CI75c<|!vtZ%%G@tZ6ebRnROU&;WMT3! zMP;5cOckaM(^TeZ!*pT#FhgaYYR3GSDa;)DSLOlTJHH#)XS>$px$q)cFC2K$LSC#g z&sxZfSLT6*yhLRl6lSY+lX=N7xO&O0_Cj$;xFm6%x8o%LuWXv@>$*U zWji~IbHcgdyvjT(j1K3A3o7%4;i7PHxTG>)8ZHZ$hbt=cmEo#zb-1Q7UmLFT@?0No zuqfXc-xO{Rw^Zg^!)@XAa7SgnGu##K4);{%d&7O<{_sF$elR=~9uBQXYQc|&$HL>m zT4sY!gePk)tDAc&JY8#9-P|+b*;>o$=AH}B*E+e?ZWw$aIDkQ!4L+PN(?e)-{9;gh zFgTptcGHVJ%N)NH?#s-}u}jH(fA((|U&+F43~*Po%N~4xZD7M!vwkgj6=Z%rsJ#)~ zUgoC9o@`7o2%c)o+tAU;d|fz9~1my1Kc#(!wm9I;pgy6u&KE+CPG#jch1$(_;@d$2R;k{1yJL%>RTj;oo2_ zv$_9*zO~#E3_8SEd1*XQW(QE~5&UAAn;v_ZId<^wnOQv8ROa!kHYm22KBu=uyJ^US z4YCcrPhNVblzC{?Y-B@yZhGv&=6F!I&e03xZ;~9hNB;Lu7(2*3R_GSSskN+b&R;R| zSCBd?v%lP_=C3&UwK6w7_AGP!Z0s**jo%|5a+_MPzm%0?NB3YenVTMausOEj72%oS zkTUlOYBsW=%uSCyw?~hB$ORkr4820{%G@XP4HE=wnaxcY^sQw!=Q90b&y(3j)Fujk zt;{`J`JrjZ!(?`#iNhqprZP_&)Fulyley`!2b*IXdR4q44k`2GL9JyY8(MJFWDhpT zHk=|%8K$btQ&(-8c-qR`^mw{pj;9Z5W@hNou}*uD&~9Fn|G_?EvW;ZEwrVpa+gzXZ z)MgGgmwCz_oue1-Gg(W!~dgY%a5&+O9e9?m^zYm7od5{cFt*0(swBvqM4NuhzV1Y7VQ{ zWL_+|oL-Z8@zC-FeofpnFm*GNW4^KZhCR6W9bwO z^3ZA;TBb7Vb;}Kxjcp|J_*Gjj+2;DJr?z~sxy*WMD})u>Iq$!q33;W;>>!X=uFMVv zd6mk%YH(P+Ci7~+<@B1&tGDu_Pi~>PdS&jFT94qB(UaM%Tf3p2%=&A@YlgLg%xed= zb;7!px#_ip^@2fOKd2enpfc-u*)|LwF0)rmZKGf#nf25*4mOurPwk!j5%xWQtBgfL zyJ=bqmyHw^~!){^s zU~`!rW{6%x8o%LuatL%nox_I6K%}W`{W^oEy%o%%ci7vZwKrvBuji+!eG)zkGJjskUsUEV3)zR=ntk}4 z%lXc(%x4AnaG%b~d}jD6eC_o-trqOt_)YjWq}k4F*aclwPv&nr#|+8q`AgzYfqxA@ zh9AOjmHGFeXXcN}{Ac(p{2l(O%wxj8;lJ>GW&SR7^vs`ag`c}}s~sF*>=efd;|7_> zE97pKdHh1|UYUCo^22$iH9r&_)}ehlWPZT^XLCEZ^lg&ax2tDlY}WM^ORw#Fm;%wGEW<( z3)2VN$!z->!i-_!%53|Y!puGM|5N(cf(I1xER}iILLOL|XDj57b8l<@D9oNbN0>9n zJXdfzm-LOByyqHS!B=qZ6z2(VS7u+?c~hIOda@a_ZteDZGSAyNW@tXyizY4{c(Jfh zSTHPJnU@H9W(HN}CBxt_BrH{#mkvY2GGT$rJbzd=EEg8(%B^;AfaSvqVa3Y4QX#Kg znO7;~RV(vqh3s2m&AurP>(IVdGUqn~x^Cx|zK3qgzL%>fuMz&P%)ZBKrnXk~WHV;n z+U@mZ)?YiU6V|QF>xK2h2Ce+quok>g&@{AhW!@y{nc1{5Zx%KWTZI2r<}JfkVe4RX znH^@Eux;2b^so@N?;N#U{$Rax;!a_gAoH$;yjx}7y^v?F%rgb&a=sZW^9;c~+{gD% zX5Ya*!k#_z2kWV8!Bd63!rmdxc4osa=%RWu@6|bGxqPG#N<1L&!D0WfUpS;P4-0x` z4z0}IkHf<7aCl`tB8&(l!@iYypKxS2Dje9ATkYTgM~7p=v6cC_LO#ASpHRprR_2om z+1JCGeO(;Zp?#lZUe-t2?c8#iAoI|0ayTUnsmz1Jso}JsC$m|%c6&XU^-m9HgflC1 zXE-aI?Ih>ag3k?_hR&Zn<@3-YVP_ZVp>i=FP(` z;ntuhvst%xdp();Zwt4FJ1X;?;V$q0-3j-!7hS%$nuhMH%=ZU9vkz3}2g4EhL*3!w zq00PlXr*>&u(`|*Gc5U#aCfT~4slmFIK_j)BbE8lLVm0=KVHaBROTlO`KikMbZ}UQ zex`Cucs975TRvBrpARpD7sE@H`Q`9Rcs1zBY}T#aUQcHI*TU=JjmrF{_y4Vgx5GO@ z=66Gy?cw(-^ZP;1%mQq~x#f42`TOug_%ZxcnSTzygkOW6 z%x2x%?e%2V|1JC;{;15YKl9_S@OSv9GLH#*hW@S0{{=lW9lg3Tj}?3_d_Kmm%;N-~ zBcCUm%j_`u+_l=DM4S5%vcveK`oxZ#Uo7)@h1{(&k6*~$D|3%R?pc|81&4L$-o5hv zTl5Zng4?-e-^x5em@xDU6IJGk!z5wSpeM6gw|09yne`_NlZPoPPti)4vWip1Q&(m~ zdOS^Kp0;XcrmM_)(}x*?jbxrNsLd2?F0@Y zF#gcs4-rdNUeZTnaG^M)GA~ugOIPNhg}h8&K{8%}x z5@cRAtQJ-edNS*~wcG2-tiMKBGptpa*ADB1b;EjlA|pAlwE zZH8cTnH^^OgPzR#ZteDZGV5Ovt_)XI=BulAO?+)-ZhCR6Vc!%8i>iT`AZ`m7n zhPx{B-NBjkn#}hEchGAx-y7}=dQIm0!vn$n^<*}iyXC*2n4syRY~aE0Ppc~o4iEZrSlIBHDn1+A+yLvTJr`^)v!0rF%Hd_!gX}Ug zI|yXAm+$lCa4INzxnzDmIIVS)`Gw$idQIjR!%IQ0$^3G7C3t1@WH#G!Yd6%C*}$ve zweWgnexqt{#&1>TrpIpwbNo(FGxKg`)_X6!@BO!t1%J?0?D#OYc?R0nQ~M~`TxK(B zABRsWvmWG6E3<<@{;Zwz{tHe8`SZ&BMQ~cZCi9oU?ev<=UxlxOUX%Ho@NMwQ=*j$b zD?i-Y4K-yp@Ll*m{7{*HtlCfU&y~69@h`y~{~FZH{8pLueh;lb^20_J{AW=6E7)9S zJ+;4s&1Ke8^AM_J};Nd{{*MCZZeMvZl~8|{x|#= z^qS26;~8TGuZ*7jtM9*Iw{}BKnGK8`#tGwA=JBf5EgruzH$Cni%yEyPW~OIl*86zf zY}6~kMn1*tQ>)fH*j#2kwLZb-GV7`J4HL+n<878dvfB?6hJKaV-jFA%%o7J^(rYqL z65K(r$vkQBebH+&``+XW)@r{1n!asp*xlU2izBoCWbx!-iXiipL2arqb!BdPJWVji z(*`v&)Ai0fYcXBWye*qdAA7jWw^wb3WE<(Tp4yDT<}&N4%@k&?%zBXfSMJ}r(dPO6 zecNL|n58n$8XQZn$viN)hhCF;ws2>TtJh?{E44d<*FaBZv$B&~*j#2kwMD~X zm01t+;&SJ!TjbMjZ!8f8RpupwGwC&%2M2f1YcdZBO9j0q^U`5xaDP3S&E{&Y_B}M+ zlMO5rmJQ2Q=H;ulLcC&SZhE{@FvlwgH8ZPJX1)G-Ggpn5@j_eJa6lDTOSZWI)>B(O z*j#2kwKc+;m01t+T5{)lTjqtfH`Wg8ROWSqGwC&%*9-2T*JNHl%$lR>HJJydHcN1S zJ(Ma%(r#l-aJ#Zkyl#6>(ZPy)xSu@)?!c86clo znLC5C={1?p3htuUWIj8b<1?&Ti?+|r`n=$U(33gC?K`@Up8K-?s4zO5Uzslm7lw<% z#g+Mzpl9Y%pJ9tj!(~C!(B+l+if~MR&-UnWWo5o99F^LU!R9hM%*f;s!R9hM%<$yH z!qt_p_PM>LP`tJ>UsuT2SLPcE`Nqn8Q*c;^zPU2r5?szDZ>`L?h1Tu)aYwi_$b470 zJKPiWWY%|Ux7U+d|K4z4xW6(#P_+l+hbnW^i(4HJr(lpDsivVvE3@8X;qhQ2nV$%1 zPX?RItf%%=u(`~7YEOq}+BxsPpb7ce%IqMJpR3Fc1^M~P{6cV8y(aUE!R7Rt%rCX_ z|FQe}Q6yc)bRdNP}JYd6%BS^u^0dU&HUzge}n;-zwrJ4GKIr(3(Y3;@YMXtzgI?2X0vYXhI%sVe-(cnz6mma8@>zQhaW2Q zkKtSIzoDNj%0I`NhJLBczlJOG2ahYlZt=(Qv=H7ieH{Uh?JmCb%6NQO`%oB!woMEoenma#iYQggawRwZhWNv!w!RFY8C*=_H#SSU+{6Wn| zHk7&Pah}_158A+n3xoy3LX~;put-=mSj%i~v7m1)vpI)2C3v394xlzV__gwaee(V{ zP4+N@?Bt^|ba$|+%;#0@-q>dP-1OK6=GcbbCNI51%IsguJiC|OMm99SO_M#?Am5vR z1o-d1Q?&*s4+(>U%u9r&!jiR?)y++ieuy|#juxbm(eyu(?J@zbf+%sMgJq#m3Z08?96Jb2ESJ3rpKOTj@@;&u(}t+rWU+LSFvNw*k&1M zyXmn9n`0Y#0bJf8WnL?&*~o@6H$C?JwS0aB8?GJJ3F}to^}_mLgJ3PQxebH9wan&L z%ON(3Jx}J1gW4v+ua$Z2R(@z2@-Ue<4V#6{gH2`LBB=c@*i7c8#~y5sZ8&ufv1RO# zGH(^sS~jww1vgFhV1sPKt;059+seFM*gotKtYtR0W6-yj*_=a68}htXd*Q(jpg17- z#WFWN_Aqno;QceRe6Xp^GgocJ*k=0N^w@LFZR7p7q4&v4@02pHUNsxpP@kI~d$2jS z;ZE_+VV5BDu3@*Zd#z=4b9;FI4JhnUHt1jVHcRjjnQzSoY6F5_EOXOi&oak1#4~4R z&t#j*+`npjCAVy5fSV?JutBz=e?0xV;E*!!9n@@OLz$Z%d#*XQ;XYyCuwPfs-w%fl<7_4PBcu>%{mf4)!92|R~%r2sKNbqZAZhGul=6Emf|FDEZgH2`jwx}HzY$kKl zV-GgRHuS2vsYA*5V%WUrGa7?Xbb#upt z<7zFdn>#*uo^{JYFnB^ZF*tzCCk3^WgUw`adhB85_>^#JI4wA&%%=ynGlIj(asCS{ z37YJ|GTZR&vGXTu!L4LIGpM~8+(zc6$2Ks>Hhe8Jov~Lz=GWV`y#Hrq;r0f&X|e|! zWE-9xpA)=lGM^jN+}tZ5bJJtbHOKZlFN_MKz5f=Y!}&q+f^cDFZhCxCFvk}MH8Yn~ zX1z;`GT#_(3VKcEo5L-^E2Af~S+{mUJ(=}y4Y&FJ-(Cy8qg~8zO~rRr=BCGY z2XlN+P&0FHW!Aed+#hTt^8-Qc!El@0s-UU(P_VhohSVMok5pzo$d6WL2Z8)pWp*gY zk5}f_6Zzq^noZ^>gWKsfnV$+z2fZfqGvV3bmC=*gtXsRFp3M5sdHc)-I?gvwrLI z{P-e#8D#z{sC^y2smx7}zYXU2yP#&~`^v2MZ+`!#W2~-c%l7yosEx_~?TsvKsF`BN z-@)cG>#6+}eyq%TkbkPo4g&e-%Ir{(e`)8u|H7}qX*HY7zXi9`Ycl^H{s?+a=0C$< z!7HODvst%xK|PuO^8WuF{t06$^S@R5FYf5u)#s+iV`XiQ$F824aVoRkxM93tADO#V ztu=mv%?+@oTK8acnf26qgq}fWJ;=Rk%?<*&cV%`c$bI_e{kKTy=o_5Yy2(62a67#w z^Ms*a&}%YJ6ebQ{89kZJy0r`H$^ZKP8<-?a8YZjElZPq7l)+kNb5n(>Yb~prnZhXANotgWJg5^w@*Vu?=Snvj?w&%yR^_IfL6L=O4;QXqxQ72HA$A zbGuQ&%O>+&LCwv*0x~x}_FQvp!@0veVcyC-U*Ej{it~pBg3MjbE-#q%e1R9Lo|%O! zvz}LLkzga4y;5q62G5pRPi?VQe%RcCH5UuJ=l87k^c!8ttOt3wTC;;d-lf*;P>^@7 zH7}l;!|KT`3t@@icGgYiLBZRh*JSpddV9PwdNP}JYd6%BS%1keI1H&Aw>p-p;?nWZ z%4|rFm#NIlR?W{E-%-aNq-N^U9S@5>O?KGRr+lB3eUXyu;uw(Gb=*eu> zt=(QvX8oPQ&S4kX`!DPo6n6`|SLUY2djxa5XHYYF@TZ{}xR`IC#=*rym_?Tdhj}2;Oj;qXi$A=SwXUlwI zP&+BuTpr;Dnu;d}o6BrS?UZn8W!8gyT4i<+$fs9khk|@YWj-^soL199=FZ@D)=lQK z!r4Ku$$U;YH+W_AWH#&8E~qE7{&``P_kVOP`24P7#|80)mAUEhMZp|j9MsHQQknHG z4VMKQ$$WWGyCP)ss<}&N4T@|jb%zBWosmu-n`P$0tP>`>y%-4JWomTVu z%6vm`JH00Jjp3%C*JQpq+!DMpdNP}JYZug$S>OL1)qkHpp>LQVOn7_$efkX=tMi33 z`<&eQOlP0oo%<}(Z`lF)pUdrk9E|fcW3X3J7-v{--MH{oT2%{LP6$Lj@oSBeqo~9H`{mO HhfnxFW(qJ2 delta 61378 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 T&0er*U|8Yy1&daEV9x&mfs6!| diff --git a/player_device_skate.h b/player_device_skate.h new file mode 100644 index 0000000..af95a7f --- /dev/null +++ b/player_device_skate.h @@ -0,0 +1,1458 @@ +#ifndef PLAYER_DEVICE_SKATE_H +#define PLAYER_DEVICE_SKATE_H + +#include "player_interface.h" +#include "skeleton.h" +#include "player_model.h" + +struct player_device_skate +{ + struct + { + enum skate_activity + { + k_skate_activity_air, + k_skate_activity_ground, + k_skate_activity_grind + } + activity, + activity_prev; + + float steery, + steerx, + steery_s, + steerx_s, + reverse, + slip; + + m3x3f velocity_bias, + velocity_bias_pstep; + + int lift_frames; + + v3f throw_v; + v3f cog_v, cog; + + float grabbing; + v2f grab_mouse_delta; + + int charging_jump, jump_dir; + float jump_charge; + double jump_time; + + double start_push, + cur_push; + + v3f vl; + } + state, + state_gate_storage; + + struct land_prediction + { + v3f log[50]; + v3f n; + u32 log_length; + float score; + + enum prediction_type + { + k_prediction_none, + k_prediction_land, + k_prediction_grind + } + type; + + u32 colour; + } + predictions[22]; + u32 prediction_count; + + /* animation */ + struct skeleton_anim *anim_stand, *anim_highg, *anim_slide, + *anim_air, + *anim_push, *anim_push_reverse, + *anim_ollie, *anim_ollie_reverse, + *anim_grabs, *anim_stop; + rb_sphere sphere_front, sphere_back; + v3f board_offset; + v4f board_rotation; + + float blend_slide, + blend_z, + blend_x, + blend_fly, + blend_stand, + blend_push, + blend_jump, + blend_airdir; + + v2f wobble; + + float debug_normal_pressure; +}; + +VG_STATIC void player_skate_bind( player_interface *player, + player_attachment *at ) +{ + struct player_device_skate *s = at->storage; + struct player_avatar *av = player->playeravatar; + struct skeleton *sk = &av->sk; + + rb_update_transform( &player->rb ); + s->anim_stand = skeleton_get_anim( sk, "pose_stand" ); + s->anim_highg = skeleton_get_anim( sk, "pose_highg" ); + s->anim_air = skeleton_get_anim( sk, "pose_air" ); + s->anim_slide = skeleton_get_anim( sk, "pose_slide" ); + s->anim_push = skeleton_get_anim( sk, "push" ); + s->anim_push_reverse = skeleton_get_anim( sk, "push_reverse" ); + s->anim_ollie = skeleton_get_anim( sk, "ollie" ); + s->anim_ollie_reverse = skeleton_get_anim( sk, "ollie_reverse" ); + s->anim_grabs = skeleton_get_anim( sk, "grabs" ); +} + +VG_STATIC void player_skate_pre_update( player_interface *player, + player_attachment *at ) +{ +} + +/* + * Collision detection routines + * + * + */ + +/* + * Does collision detection on a sphere vs world, and applies some smoothing + * filters to the manifold afterwards + */ +VG_STATIC int skate_collide_smooth( player_interface *player, + m4x3f mtx, rb_sphere *sphere, + rb_ct *man ) +{ + debug_sphere( mtx, sphere->radius, VG__BLACK ); + + int len = 0; + len = rb_sphere__scene( mtx, sphere, NULL, &world.rb_geo.inf.scene, man ); + + for( int i=0; irb; + man[i].rbb = NULL; + } + + rb_manifold_filter_coplanar( man, len, 0.05f ); + + if( len > 1 ) + { + rb_manifold_filter_backface( man, len ); + rb_manifold_filter_joint_edges( man, len, 0.05f ); + rb_manifold_filter_pairs( man, len, 0.05f ); + } + int new_len = rb_manifold_apply_filtered( man, len ); + if( len && !new_len ) + len = 1; + else + len = new_len; + + return len; +} +/* + * Gets the closest grindable edge to the player within max_dist + */ +VG_STATIC struct grind_edge *skate_collect_grind_edge( v3f p0, v3f p1, + v3f c0, v3f c1, + float max_dist ) +{ + bh_iter it; + bh_iter_init( 0, &it ); + + boxf region; + + box_init_inf( region ); + box_addpt( region, p0 ); + box_addpt( region, p1 ); + + float k_r = max_dist; + v3_add( (v3f){ k_r, k_r, k_r}, region[1], region[1] ); + v3_add( (v3f){-k_r,-k_r,-k_r}, region[0], region[0] ); + + float closest = k_r*k_r; + struct grind_edge *closest_edge = NULL; + + int idx; + while( bh_next( world.grind_bh, &it, region, &idx ) ) + { + struct grind_edge *edge = &world.grind_edges[ idx ]; + + float s,t; + v3f pa, pb; + + float d2 = + closest_segment_segment( p0, p1, edge->p0, edge->p1, &s,&t, pa, pb ); + + if( d2 < closest ) + { + closest = d2; + closest_edge = edge; + v3_copy( pa, c0 ); + v3_copy( pb, c1 ); + } + } + + return closest_edge; +} + +VG_STATIC int skate_grind_collide( player_interface *player, + player_attachment *at, rb_ct *contact ) +{ + v3f p0, p1, c0, c1; + v3_muladds( player->rb.co, player->rb.to_world[2], 0.5f, p0 ); + v3_muladds( player->rb.co, player->rb.to_world[2], -0.5f, p1 ); + v3_muladds( p0, player->rb.to_world[1], 0.125f-0.15f, p0 ); + v3_muladds( p1, player->rb.to_world[1], 0.125f-0.15f, p1 ); + + float const k_r = 0.25f; + struct grind_edge *closest_edge = skate_collect_grind_edge( p0, p1, + c0, c1, k_r ); + + if( closest_edge ) + { + v3f delta; + v3_sub( c1, c0, delta ); + + if( v3_dot( delta, player->rb.to_world[1] ) > 0.0001f ) + { + contact->p = v3_length( delta ); + contact->type = k_contact_type_edge; + contact->element_id = 0; + v3_copy( c1, contact->co ); + contact->rba = NULL; + contact->rbb = NULL; + + v3f edge_dir, axis_dir; + v3_sub( closest_edge->p1, closest_edge->p0, edge_dir ); + v3_normalize( edge_dir ); + v3_cross( (v3f){0.0f,1.0f,0.0f}, edge_dir, axis_dir ); + v3_cross( edge_dir, axis_dir, contact->n ); + + return 1; + } + else + return 0; + } + + return 0; +} + +/* + * + * Prediction system + * + * + */ + +/* + * Trace a path given a velocity rotation. + * + * TODO: this MIGHT be worth doing RK4 on the gravity field. + */ +VG_STATIC void skate_score_biased_path( v3f co, v3f v, m3x3f vr, + struct land_prediction *prediction ) +{ + float pstep = VG_TIMESTEP_FIXED * 10.0f; + float k_bias = 0.96f; + + v3f pco, pco1, pv; + v3_copy( co, pco ); + v3_muls( v, k_bias, pv ); + + m3x3_mulv( vr, pv, pv ); + v3_muladds( pco, pv, pstep, pco ); + + struct grind_edge *best_grind = NULL; + float closest_grind = INFINITY; + + float grind_score = INFINITY, + air_score = INFINITY; + + prediction->log_length = 0; + + for( int i=0; ilog); i++ ) + { + v3_copy( pco, pco1 ); + + pv[1] += -k_gravity * pstep; + + m3x3_mulv( vr, pv, pv ); + v3_muladds( pco, pv, pstep, pco ); + + v3f vdir; + + v3_sub( pco, pco1, vdir ); + + float l = v3_length( vdir ); + v3_muls( vdir, 1.0f/l, vdir ); + + v3f c0, c1; + struct grind_edge *ge = skate_collect_grind_edge( pco, pco1, + c0, c1, 0.4f ); + + if( ge && (v3_dot((v3f){0.0f,1.0f,0.0f},vdir) < -0.2f ) ) + { + float d2 = v3_dist2( c0, c1 ); + if( d2 < closest_grind ) + { + closest_grind = d2; + best_grind = ge; + grind_score = closest_grind * 0.05f; + } + } + + v3f n1; + + float t1; + int idx = spherecast_world( pco1, pco, 0.4f, &t1, n1 ); + if( idx != -1 ) + { + v3_copy( n1, prediction->n ); + air_score = -v3_dot( pv, n1 ); + + u32 vert_index = world.scene_geo->arrindices[ idx*3 ]; + struct world_material *mat = world_tri_index_material( vert_index ); + + /* Bias prediction towords ramps */ + if( mat->info.flags & k_material_flag_skate_surface ) + air_score *= 0.1f; + + v3_lerp( pco1, pco, t1, prediction->log[ prediction->log_length ++ ] ); + break; + } + + v3_copy( pco, prediction->log[ prediction->log_length ++ ] ); + } + + if( grind_score < air_score ) + { + prediction->score = grind_score; + prediction->type = k_prediction_grind; + } + else if( air_score < INFINITY ) + { + prediction->score = air_score; + prediction->type = k_prediction_land; + } + else + { + prediction->score = INFINITY; + prediction->type = k_prediction_none; + } +} + +VG_STATIC +void player_approximate_best_trajectory( player_interface *player, + struct player_device_skate *s ) +{ + float pstep = VG_TIMESTEP_FIXED * 10.0f; + float best_velocity_delta = -9999.9f; + + v3f axis; + v3_cross( player->rb.to_world[1], player->rb.v, axis ); + v3_normalize( axis ); + + s->prediction_count = 0; + m3x3_identity( s->state.velocity_bias ); + + float best_vmod = 0.0f, + min_score = INFINITY, + max_score = -INFINITY; + + /* + * Search a broad selection of futures + */ + for( int m=-3;m<=12; m++ ) + { + struct land_prediction *p = &s->predictions[ s->prediction_count ++ ]; + + float vmod = ((float)m / 15.0f)*0.09f; + + m3x3f bias; + v4f bias_q; + + q_axis_angle( bias_q, axis, vmod ); + q_m3x3( bias_q, bias ); + + skate_score_biased_path( player->rb.co, player->rb.v, bias, p ); + + if( p->type != k_prediction_none ) + { + if( p->score < min_score ) + { + min_score = p->score; + best_vmod = vmod; + } + + if( p->score > max_score ) + max_score = p->score; + } + } + + v4f vr_q; + q_axis_angle( vr_q, axis, best_vmod*0.1f ); + q_m3x3( vr_q, s->state.velocity_bias ); + + q_axis_angle( vr_q, axis, best_vmod ); + q_m3x3( vr_q, s->state.velocity_bias_pstep ); + + /* + * Logging + */ + for( int i=0; iprediction_count; i ++ ) + { + struct land_prediction *p = &s->predictions[i]; + + float l = p->score; + + if( l < 0.0f ) + { + vg_error( "negative score! (%f)\n", l ); + } + + l -= min_score; + l /= (max_score-min_score); + l = 1.0f - l; + l *= 255.0f; + + p->colour = l; + p->colour <<= 8; + p->colour |= 0xff000000; + } +} + +/* + * + * Varius physics models + * ------------------------------------------------ + */ + +VG_STATIC void skate_apply_grind_model( player_interface *player, + struct player_device_skate *s, + rb_ct *manifold, int len ) +{ + /* FIXME: Queue audio events instead */ + if( len == 0 ) + { + if( s->state.activity == k_skate_activity_grind ) + { +#if 0 + audio_lock(); + audio_player_set_flags( &audio_player_extra, + AUDIO_FLAG_SPACIAL_3D ); + audio_player_set_position( &audio_player_extra, player.rb.co ); + audio_player_set_vol( &audio_player_extra, 20.0f ); + audio_player_playclip( &audio_player_extra, &audio_board[6] ); + audio_unlock(); +#endif + + s->state.activity = k_skate_activity_air; + } + return; + } + + v2f steer = { player->input_js1h->axis.value, + player->input_js1v->axis.value }; + v2_normalize_clamp( steer ); + + s->state.steery -= steer[0] * k_steer_air * k_rb_delta; + s->state.steerx += steer[1] * s->state.reverse * k_steer_air * k_rb_delta; + +#if 0 + v4f rotate; + q_axis_angle( rotate, player->rb.to_world[0], siX ); + q_mul( rotate, player.rb.q, player.rb.q ); +#endif + + s->state.slip = 0.0f; + s->state.activity = k_skate_activity_grind; + + /* TODO: Compression */ + v3f up = { 0.0f, 1.0f, 0.0f }; + float angle = v3_dot( player->rb.to_world[1], up ); + + if( fabsf(angle) < 0.99f ) + { + v3f axis; + v3_cross( player->rb.to_world[1], up, axis ); + + v4f correction; + q_axis_angle( correction, axis, k_rb_delta * 10.0f * acosf(angle) ); + q_mul( correction, player->rb.q, player->rb.q ); + } + + float const DOWNFORCE = -k_downforce*1.2f*VG_TIMESTEP_FIXED; + v3_muladds( player->rb.v, manifold->n, DOWNFORCE, player->rb.v ); + m3x3_identity( s->state.velocity_bias ); + m3x3_identity( s->state.velocity_bias_pstep ); + + if( s->state.activity_prev != k_skate_activity_grind ) + { + /* FIXME: Queue audio events instead */ +#if 0 + audio_lock(); + audio_player_set_flags( &audio_player_extra, + AUDIO_FLAG_SPACIAL_3D ); + audio_player_set_position( &audio_player_extra, player.rb.co ); + audio_player_set_vol( &audio_player_extra, 20.0f ); + audio_player_playclip( &audio_player_extra, &audio_board[5] ); + audio_unlock(); +#endif + } +} + +/* + * Air control, no real physics + */ +VG_STATIC void skate_apply_air_model( player_interface *player, + struct player_device_skate *s ) +{ + if( s->state.activity != k_skate_activity_air ) + return; + + if( s->state.activity_prev != k_skate_activity_air ) + player_approximate_best_trajectory( player, s ); + + m3x3_mulv( s->state.velocity_bias, player->rb.v, player->rb.v ); + ray_hit hit; + + /* + * Prediction + */ + float pstep = VG_TIMESTEP_FIXED * 1.0f; + float k_bias = 0.98f; + + v3f pco, pco1, pv; + v3_copy( player->rb.co, pco ); + v3_muls( player->rb.v, 1.0f, pv ); + + float time_to_impact = 0.0f; + float limiter = 1.0f; + + struct grind_edge *best_grind = NULL; + float closest_grind = INFINITY; + + v3f target_normal = { 0.0f, 1.0f, 0.0f }; + int has_target = 0; + + for( int i=0; i<250; i++ ) + { + v3_copy( pco, pco1 ); + m3x3_mulv( s->state.velocity_bias, pv, pv ); + + pv[1] += -k_gravity * pstep; + v3_muladds( pco, pv, pstep, pco ); + + ray_hit contact; + v3f vdir; + + v3_sub( pco, pco1, vdir ); + contact.dist = v3_length( vdir ); + v3_divs( vdir, contact.dist, vdir); + + v3f c0, c1; + struct grind_edge *ge = skate_collect_grind_edge( pco, pco1, + c0, c1, 0.4f ); + + if( ge && (v3_dot((v3f){0.0f,1.0f,0.0f},vdir) < -0.2f ) ) + { + vg_line( ge->p0, ge->p1, 0xff0000ff ); + vg_line_cross( pco, 0xff0000ff, 0.25f ); + has_target = 1; + break; + } + + float orig_dist = contact.dist; + if( ray_world( pco1, vdir, &contact ) ) + { + v3_copy( contact.normal, target_normal ); + has_target = 1; + time_to_impact += (contact.dist/orig_dist)*pstep; + vg_line_cross( contact.pos, 0xffff0000, 0.25f ); + break; + } + time_to_impact += pstep; + } + + if( has_target ) + { + float angle = v3_dot( player->rb.to_world[1], target_normal ); + v3f axis; + v3_cross( player->rb.to_world[1], target_normal, axis ); + + limiter = vg_minf( 5.0f, time_to_impact )/5.0f; + limiter = 1.0f-limiter; + limiter *= limiter; + limiter = 1.0f-limiter; + + if( fabsf(angle) < 0.99f ) + { + v4f correction; + q_axis_angle( correction, axis, + acosf(angle)*(1.0f-limiter)*3.0f*VG_TIMESTEP_FIXED ); + q_mul( correction, player->rb.q, player->rb.q ); + } + } + + v2f steer = { player->input_js1h->axis.value, + player->input_js1v->axis.value }; + v2_normalize_clamp( steer ); + + s->state.steery -= steer[0] * k_steer_air * VG_TIMESTEP_FIXED; + s->state.steerx += steer[1] * s->state.reverse * k_steer_air + * limiter * k_rb_delta; +} + +VG_STATIC void skate_get_board_points( player_interface *player, + struct player_device_skate *s, + v3f front, v3f back ) +{ + v3f pos_front = {0.0f,0.0f,-k_board_length}, + pos_back = {0.0f,0.0f, k_board_length}; + + m4x3_mulv( player->rb.to_world, pos_front, front ); + m4x3_mulv( player->rb.to_world, pos_back, back ); +} + +/* + * Casts and pushes a sphere-spring model into the world + */ +VG_STATIC int skate_simulate_spring( player_interface *player, + struct player_device_skate *s, + v3f pos ) +{ + float mod = 0.7f * player->input_grab->axis.value + 0.3f, + spring_k = mod * k_spring_force, + damp_k = mod * k_spring_dampener, + disp_k = 0.4f; + + v3f start, end; + v3_copy( pos, start ); + v3_muladds( pos, player->rb.to_world[1], -disp_k, end ); + + float t; + v3f n; + int hit_info = spherecast_world( start, end, 0.2f, &t, n ); + + if( hit_info != -1 ) + { + v3f F, delta; + v3_sub( start, player->rb.co, delta ); + + float displacement = vg_clampf( 1.0f-t, 0.0f, 1.0f ), + damp = + vg_maxf( 0.0f, v3_dot( player->rb.to_world[1], player->rb.v ) ); + + v3_muls( player->rb.to_world[1], displacement*spring_k*k_rb_delta - + damp*damp_k*k_rb_delta, F ); + + v3_muladds( player->rb.v, F, 1.0f, player->rb.v ); + + /* Angular velocity */ + v3f wa; + v3_cross( delta, F, wa ); + v3_muladds( player->rb.w, wa, k_spring_angular, player->rb.w ); + + v3_lerp( start, end, t, pos ); + return 1; + } + else + { + v3_copy( end, pos ); + return 0; + } +} + + +/* + * Handles connection between the player and the ground + */ +VG_STATIC void skate_apply_interface_model( player_interface *player, + struct player_device_skate *s, + rb_ct *manifold, int len ) +{ + if( !((s->state.activity == k_skate_activity_ground) || + (s->state.activity == k_skate_activity_air )) ) + return; + + if( s->state.activity == k_skate_activity_air ) + s->debug_normal_pressure = 0.0f; + else + s->debug_normal_pressure = v3_dot( player->rb.to_world[1], player->rb.v ); + + /* springs */ + v3f spring0, spring1; + + skate_get_board_points( player, s, spring1, spring0 ); + int spring_hit0 = skate_simulate_spring( player, s, spring0 ), + spring_hit1 = skate_simulate_spring( player, s, spring1 ); + + v3f animavg, animdelta; + v3_add( spring0, spring1, animavg ); + v3_muls( animavg, 0.5f, animavg ); + + v3_sub( spring1, spring0, animdelta ); + v3_normalize( animdelta ); + + m4x3_mulv( player->rb.to_local, animavg, s->board_offset ); + + float dx = -v3_dot( animdelta, player->rb.to_world[2] ), + dy = v3_dot( animdelta, player->rb.to_world[1] ); + + float angle = -atan2f( dy, dx ); + q_axis_angle( s->board_rotation, (v3f){1.0f,0.0f,0.0f}, angle ); + + /* Surface connection */ + if( len == 0 && !(spring_hit0 && spring_hit1) ) + { + s->state.lift_frames ++; + + if( s->state.lift_frames >= 8 ) + s->state.activity = k_skate_activity_air; + } + else + { + v3f surface_avg; + v3_zero( surface_avg ); + + for( int i=0; irb.v, surface_avg ) > 0.7f ) + { + s->state.lift_frames ++; + + if( s->state.lift_frames >= 8 ) + s->state.activity = k_skate_activity_air; + } + else + { + s->state.activity = k_skate_activity_ground; + s->state.lift_frames = 0; + v3f projected, axis; + + float const DOWNFORCE = -k_downforce*VG_TIMESTEP_FIXED; + v3_muladds( player->rb.v, player->rb.to_world[1], + DOWNFORCE, player->rb.v ); + + float d = v3_dot( player->rb.to_world[2], surface_avg ); + v3_muladds( surface_avg, player->rb.to_world[2], -d, projected ); + v3_normalize( projected ); + + float angle = v3_dot( player->rb.to_world[1], projected ); + v3_cross( player->rb.to_world[1], projected, axis ); + + if( fabsf(angle) < 0.9999f ) + { + v4f correction; + q_axis_angle( correction, axis, + acosf(angle)*4.0f*VG_TIMESTEP_FIXED ); + q_mul( correction, player->rb.q, player->rb.q ); + } + } + } +} + +VG_STATIC void skate_apply_grab_model( player_interface *player, + struct player_device_skate *s ) +{ + float grabt = player->input_grab->axis.value; + + if( grabt > 0.5f ) + { + v2_muladds( s->state.grab_mouse_delta, vg.mouse_delta, 0.02f, + s->state.grab_mouse_delta ); + + v2_normalize_clamp( s->state.grab_mouse_delta ); + } + else + v2_zero( s->state.grab_mouse_delta ); + + s->state.grabbing = vg_lerpf( s->state.grabbing, grabt, 8.4f*k_rb_delta ); +} + +/* + * Computes friction and surface interface model + */ +VG_STATIC void skate_apply_friction_model( player_interface *player, + struct player_device_skate *s ) +{ + if( s->state.activity != k_skate_activity_ground ) + return; + + /* + * Computing localized friction forces for controlling the character + * Friction across X is significantly more than Z + */ + + v3f vel; + m3x3_mulv( player->rb.to_local, player->rb.v, vel ); + float slip = 0.0f; + + if( fabsf(vel[2]) > 0.01f ) + slip = fabsf(-vel[0] / vel[2]) * vg_signf(vel[0]); + + if( fabsf( slip ) > 1.2f ) + slip = vg_signf( slip ) * 1.2f; + + s->state.slip = slip; + s->state.reverse = -vg_signf(vel[2]); + + vel[0] += vg_cfrictf( vel[0], k_friction_lat * k_rb_delta ); + vel[2] += vg_cfrictf( vel[2], k_friction_resistance * k_rb_delta ); + + /* Pushing additive force */ + + if( !player->input_jump->button.value ) + { + if( player->input_push->button.value ) + { + if( (vg.time - s->state.cur_push) > 0.25 ) + s->state.start_push = vg.time; + + s->state.cur_push = vg.time; + + double push_time = vg.time - s->state.start_push; + + float cycle_time = push_time*k_push_cycle_rate, + accel = k_push_accel * (sinf(cycle_time)*0.5f+0.5f), + amt = accel * VG_TIMESTEP_FIXED, + current = v3_length( vel ), + new_vel = vg_minf( current + amt, k_max_push_speed ), + delta = new_vel - vg_minf( current, k_max_push_speed ); + + vel[2] += delta * -s->state.reverse; + } + } + + /* Send back to velocity */ + m3x3_mulv( player->rb.to_world, vel, player->rb.v ); + + /* Steering */ + float input = player->input_js1h->axis.value, + grab = player->input_grab->axis.value, + steer = input * (1.0f-(s->state.jump_charge+grab)*0.4f), + steer_scaled = vg_signf(steer) * powf(steer,2.0f) * k_steer_ground; + + s->state.steery -= steer_scaled * k_rb_delta; +} + +VG_STATIC void skate_apply_jump_model( player_interface *player, + struct player_device_skate *s ) +{ + int charging_jump_prev = s->state.charging_jump; + s->state.charging_jump = player->input_jump->button.value; + + /* Cannot charge this in air */ + if( s->state.activity != k_skate_activity_ground ) + s->state.charging_jump = 0; + + if( s->state.charging_jump ) + { + s->state.jump_charge += k_rb_delta * k_jump_charge_speed; + + if( !charging_jump_prev ) + s->state.jump_dir = s->state.reverse>0.0f? 1: 0; + } + else + { + s->state.jump_charge -= k_jump_charge_speed * VG_TIMESTEP_FIXED; + } + + s->state.jump_charge = vg_clampf( s->state.jump_charge, 0.0f, 1.0f ); + + if( s->state.activity == k_skate_activity_air ) + return; + + /* player let go after charging past 0.2: trigger jump */ + if( (!s->state.charging_jump) && (s->state.jump_charge > 0.2f) ) + { + v3f jumpdir; + + /* Launch more up if alignment is up else improve velocity */ + float aup = v3_dot( (v3f){0.0f,1.0f,0.0f}, player->rb.to_world[1] ), + mod = 0.5f, + dir = mod + fabsf(aup)*(1.0f-mod); + + v3_copy( player->rb.v, jumpdir ); + v3_normalize( jumpdir ); + v3_muls( jumpdir, 1.0f-dir, jumpdir ); + v3_muladds( jumpdir, player->rb.to_world[1], dir, jumpdir ); + v3_normalize( jumpdir ); + + float force = k_jump_force*s->state.jump_charge; + v3_muladds( player->rb.v, jumpdir, force, player->rb.v ); + s->state.jump_charge = 0.0f; + + s->state.jump_time = vg.time; + + /* FIXME audio events */ +#if 0 + audio_lock(); + audio_player_set_flags( &audio_player_extra, AUDIO_FLAG_SPACIAL_3D ); + audio_player_set_position( &audio_player_extra, player.rb.co ); + audio_player_set_vol( &audio_player_extra, 20.0f ); + audio_player_playclip( &audio_player_extra, &audio_jumps[rand()%2] ); + audio_unlock(); +#endif + } +} + +VG_STATIC void skate_apply_pump_model( player_interface *player, + struct player_device_skate *s ) +{ + /* Throw / collect routine + * + * TODO: Max speed boost + */ + if( player->input_grab->axis.value > 0.5f ) + { + if( s->state.activity == k_skate_activity_ground ) + { + /* Throw */ + v3_muls( player->rb.to_world[1], k_mmthrow_scale, s->state.throw_v ); + } + } + else + { + /* Collect */ + float doty = v3_dot( player->rb.to_world[1], s->state.throw_v ); + + v3f Fl, Fv; + v3_muladds( s->state.throw_v, player->rb.to_world[1], -doty, Fl); + + if( s->state.activity == k_skate_activity_ground ) + { + v3_muladds( player->rb.v, Fl, k_mmcollect_lat, player->rb.v ); + v3_muladds( s->state.throw_v, Fl, -k_mmcollect_lat, s->state.throw_v ); + } + + v3_muls( player->rb.to_world[1], -doty, Fv ); + v3_muladds( player->rb.v, Fv, k_mmcollect_vert, player->rb.v ); + v3_muladds( s->state.throw_v, Fv, k_mmcollect_vert, s->state.throw_v ); + } + + /* Decay */ + if( v3_length2( s->state.throw_v ) > 0.0001f ) + { + v3f dir; + v3_copy( s->state.throw_v, dir ); + v3_normalize( dir ); + + float max = v3_dot( dir, s->state.throw_v ), + amt = vg_minf( k_mmdecay * k_rb_delta, max ); + v3_muladds( s->state.throw_v, dir, -amt, s->state.throw_v ); + } +} + +VG_STATIC void skate_apply_cog_model( player_interface *player, + struct player_device_skate *s ) +{ + v3f ideal_cog, ideal_diff; + v3_muladds( player->rb.co, player->rb.to_world[1], + 1.0f-player->input_grab->axis.value, ideal_cog ); + v3_sub( ideal_cog, s->state.cog, ideal_diff ); + + /* Apply velocities */ + v3f rv; + v3_sub( player->rb.v, s->state.cog_v, rv ); + + v3f F; + v3_muls( ideal_diff, -k_cog_spring * k_rb_rate, F ); + v3_muladds( F, rv, -k_cog_damp * k_rb_rate, F ); + + float ra = k_cog_mass_ratio, + rb = 1.0f-k_cog_mass_ratio; + + /* Apply forces & intergrate */ + v3_muladds( s->state.cog_v, F, -rb, s->state.cog_v ); + s->state.cog_v[1] += -9.8f * k_rb_delta; + v3_muladds( s->state.cog, s->state.cog_v, k_rb_delta, s->state.cog ); +} + +VG_STATIC void skate_collision_response( player_interface *player, + struct player_device_skate *s, + rb_ct *manifold, int len ) +{ + for( int j=0; j<10; j++ ) + { + for( int i=0; ico, player->rb.co, delta ); + v3_cross( player->rb.w, delta, dv ); + v3_add( player->rb.v, dv, dv ); + + float vn = -v3_dot( dv, ct->n ); + vn += ct->bias; + + float temp = ct->norm_impulse; + ct->norm_impulse = vg_maxf( temp + vn, 0.0f ); + vn = ct->norm_impulse - temp; + + v3f impulse; + v3_muls( ct->n, vn, impulse ); + + if( fabsf(v3_dot( impulse, player->rb.to_world[2] )) > 10.0f || + fabsf(v3_dot( impulse, player->rb.to_world[1] )) > 50.0f ) + { + /* FIXME */ +#if 0 + player_kill(); + return; +#endif + } + + v3_add( impulse, player->rb.v, player->rb.v ); + v3_cross( delta, impulse, impulse ); + + /* + * W Impulses are limited to the Y and X axises, we don't really want + * roll angular velocities being included. + * + * Can also tweak the resistance of each axis here by scaling the wx,wy + * components. + */ + + float wy = v3_dot( player->rb.to_world[1], impulse ) * 0.8f, + wx = v3_dot( player->rb.to_world[0], impulse ) * 1.0f; + + v3_muladds( player->rb.w, player->rb.to_world[1], wy, player->rb.w ); + v3_muladds( player->rb.w, player->rb.to_world[0], wx, player->rb.w ); + } + } +} + +VG_STATIC void skate_integrate( player_interface *player, + struct player_device_skate *s ) +{ + /* integrate rigidbody velocities */ + v3f gravity = { 0.0f, -9.6f, 0.0f }; + v3_muladds( player->rb.v, gravity, k_rb_delta, player->rb.v ); + v3_muladds( player->rb.co, player->rb.v, k_rb_delta, player->rb.co ); + + v3_lerp( player->rb.w, (v3f){0.0f,0.0f,0.0f}, 0.125f*0.5f, player->rb.w ); + if( v3_length2( player->rb.w ) > 0.0f ) + { + v4f rotation; + v3f axis; + v3_copy( player->rb.w, axis ); + + float mag = v3_length( axis ); + v3_divs( axis, mag, axis ); + q_axis_angle( rotation, axis, mag*k_rb_delta ); + q_mul( rotation, player->rb.q, player->rb.q ); + } + + /* integrate steering velocities */ + v4f rotate; + float l = (s->state.activity == k_skate_activity_air)? 0.04f: 0.3f; + + s->state.steery_s = vg_lerpf( s->state.steery_s, s->state.steery, l ); + s->state.steerx_s = vg_lerpf( s->state.steerx_s, s->state.steerx, l ); + + q_axis_angle( rotate, player->rb.to_world[1], s->state.steery_s ); + q_mul( rotate, player->rb.q, player->rb.q ); + + q_axis_angle( rotate, player->rb.to_world[0], s->state.steerx_s ); + q_mul( rotate, player->rb.q, player->rb.q ); + + s->state.steerx = 0.0f; + s->state.steery = 0.0f; + +#if 0 + v3_sub( player.rb.v, s->phys.v_prev, s->phys.a ); + v3_muls( s->phys.a, 1.0f/VG_TIMESTEP_FIXED, s->phys.a ); + v3_copy( player.rb.v, s->phys.v_prev ); +#endif + + rb_update_transform( &player->rb ); +} + +VG_STATIC void player_skate_update( player_interface *player, + player_attachment *at ) +{ + struct player_device_skate *s = at->storage; + s->state.activity_prev = s->state.activity; + + /* Setup colliders */ + m4x3f mtx_front, mtx_back; + m3x3_identity( mtx_front ); + m3x3_identity( mtx_back ); + + skate_get_board_points( player, s, mtx_front[3], mtx_back[3] ); + + s->sphere_back.radius = 0.3f; + s->sphere_front.radius = 0.3f; + + /* create manifold(s) */ + rb_ct manifold[72], + *interface_manifold = NULL, + *grind_manifold = NULL; + + int + len_front = skate_collide_smooth( player, mtx_front, + &s->sphere_front, manifold ), + len_back = skate_collide_smooth( player, mtx_back, + &s->sphere_back, &manifold[len_front] ), + + interface_len = len_front + len_back; + + interface_manifold = manifold; + grind_manifold = manifold + interface_len; + + int grind_len = skate_grind_collide( player, at, grind_manifold ); + + for( int i=0; istorage; + + /* FIXME: Compression */ + player_debugtext( 1, "V: %5.2f %5.2f %5.2f",player->rb.v[0], + player->rb.v[1], + player->rb.v[2] ); + player_debugtext( 1, "CO: %5.2f %5.2f %5.2f",player->rb.co[0], + player->rb.co[1], + player->rb.co[2] ); + player_debugtext( 1, "W: %5.2f %5.2f %5.2f",player->rb.w[0], + player->rb.w[1], + player->rb.w[2] ); + + player_debugtext( 1, "activity: %s\n", + (const char *[]){ "k_skate_activity_air", + "k_skate_activity_ground", + "k_skate_activity_grind }" } + [s->state.activity] ); +} + +VG_STATIC void player_skate_pose( player_interface *player, + player_attachment *at, + player_pose pose, m4x3f transform ) +{ + struct player_device_skate *s = at->storage; + struct player_avatar *av = player->playeravatar; + struct skeleton *sk = &av->sk; + + /* Camera position */ + /* TODO split up */ + /* FIXME */ + +#if 0 + v3_muladds( phys->m, phys->a, VG_TIMESTEP_FIXED, phys->m ); + v3_lerp( phys->m, (v3f){0.0f,0.0f,0.0f}, 0.1f, phys->m ); + + phys->m[0] = vg_clampf( phys->m[0], -2.0f, 2.0f ); + phys->m[1] = vg_clampf( phys->m[1], -2.0f, 2.0f ); + phys->m[2] = vg_clampf( phys->m[2], -2.0f, 2.0f ); + v3_lerp( phys->bob, phys->m, 0.2f, phys->bob ); +#endif + + /* Head */ + float kheight = 2.0f, + kleg = 0.6f; + + v3f offset; + v3_zero( offset ); + +#if 0 + m3x3_mulv( player.inv_visual_transform, phys->bob, offset ); +#endif + + static float speed_wobble = 0.0f, speed_wobble_2 = 0.0f; + + float curspeed = v3_length( player->rb.v ), + kickspeed = vg_clampf( curspeed*(1.0f/40.0f), 0.0f, 1.0f ), + kicks = (vg_randf()-0.5f)*2.0f*kickspeed, + sign = vg_signf( kicks ); + + s->wobble[0] = vg_lerpf( s->wobble[0], kicks*kicks*sign, 6.0f*vg.time_delta); + s->wobble[1] = vg_lerpf( s->wobble[1], speed_wobble, 2.4f*vg.time_delta); + + offset[0] *= 0.26f; + offset[0] += speed_wobble_2*3.0f; + + offset[1] *= -0.3f; + offset[2] *= 0.01f; + + offset[0]=vg_clampf(offset[0],-0.8f,0.8f)*(1.0f-fabsf(s->blend_slide)*0.9f); + offset[1]=vg_clampf(offset[1],-0.5f,0.0f); + + /* + * Animation blending + * =========================================== + */ + + /* sliding */ + { + float desired = vg_clampf( fabsf( s->state.slip ), 0.0f, 1.0f ); + s->blend_slide = vg_lerpf( s->blend_slide, desired, 2.4f*vg.time_delta); + } + + /* movement information */ + { + int iair = (s->state.activity == k_skate_activity_air) || + (s->state.activity == k_skate_activity_grind ); + + float dirz = s->state.reverse > 0.0f? 0.0f: 1.0f, + dirx = s->state.slip < 0.0f? 0.0f: 1.0f, + fly = iair? 1.0f: 0.0f; + + s->blend_z = vg_lerpf( s->blend_z, dirz, 2.4f*vg.time_delta ); + s->blend_x = vg_lerpf( s->blend_x, dirx, 0.6f*vg.time_delta ); + s->blend_fly = vg_lerpf( s->blend_fly, fly, 2.4f*vg.time_delta ); + } + + mdl_keyframe apose[32], bpose[32]; + mdl_keyframe ground_pose[32]; + { + /* when the player is moving fast he will crouch down a little bit */ + float stand = 1.0f - vg_clampf( curspeed * 0.03f, 0.0f, 1.0f ); + s->blend_stand = vg_lerpf( s->blend_stand, stand, 6.0f*vg.time_delta ); + + /* stand/crouch */ + float dir_frame = s->blend_z * (15.0f/30.0f), + stand_blend = offset[1]*-2.0f; + + v3f local_cog; + m4x3_mulv( player->rb.to_local, s->state.cog, local_cog ); + + stand_blend = vg_clampf( 1.0f-local_cog[1], 0, 1 ); + + skeleton_sample_anim( sk, s->anim_stand, dir_frame, apose ); + skeleton_sample_anim( sk, s->anim_highg, dir_frame, bpose ); + skeleton_lerp_pose( sk, apose, bpose, stand_blend, apose ); + + /* sliding */ + float slide_frame = s->blend_x * (15.0f/30.0f); + skeleton_sample_anim( sk, s->anim_slide, slide_frame, bpose ); + skeleton_lerp_pose( sk, apose, bpose, s->blend_slide, apose ); + + /* pushing */ + double push_time = vg.time - s->state.start_push; + s->blend_push = vg_lerpf( s->blend_push, + (vg.time - s->state.cur_push) < 0.125, + 6.0f*vg.time_delta ); + + float pt = push_time + vg.accumulator; + if( s->state.reverse > 0.0f ) + skeleton_sample_anim( sk, s->anim_push, pt, bpose ); + else + skeleton_sample_anim( sk, s->anim_push_reverse, pt, bpose ); + + skeleton_lerp_pose( sk, apose, bpose, s->blend_push, apose ); + + /* trick setup */ + float jump_start_frame = 14.0f/30.0f; + + float charge = s->state.jump_charge; + s->blend_jump = vg_lerpf( s->blend_jump, charge, 8.4f*vg.time_delta ); + + float setup_frame = charge * jump_start_frame, + setup_blend = vg_minf( s->blend_jump, 1.0f ); + + float jump_frame = (vg.time - s->state.jump_time) + jump_start_frame; + if( jump_frame >= jump_start_frame && jump_frame <= (40.0f/30.0f) ) + setup_frame = jump_frame; + + struct skeleton_anim *jump_anim = s->state.jump_dir? + s->anim_ollie: + s->anim_ollie_reverse; + + skeleton_sample_anim_clamped( sk, jump_anim, setup_frame, bpose ); + skeleton_lerp_pose( sk, apose, bpose, setup_blend, ground_pose ); + } + + mdl_keyframe air_pose[32]; + { + float target = -player->input_js1h->axis.value; + s->blend_airdir = vg_lerpf( s->blend_airdir, target, 2.4f*vg.time_delta ); + + float air_frame = (s->blend_airdir*0.5f+0.5f) * (15.0f/30.0f); + skeleton_sample_anim( sk, s->anim_air, air_frame, apose ); + + static v2f grab_choice; + + v2f grab_input = { player->input_js2h->axis.value, + player->input_js2v->axis.value }; + v2_add( s->state.grab_mouse_delta, grab_input, grab_input ); + if( v2_length2( grab_input ) <= 0.001f ) + grab_input[0] = -1.0f; + else + v2_normalize_clamp( grab_input ); + v2_lerp( grab_choice, grab_input, 2.4f*vg.time_delta, grab_choice ); + + float ang = atan2f( grab_choice[0], grab_choice[1] ), + ang_unit = (ang+VG_PIf) * (1.0f/VG_TAUf), + grab_frame = ang_unit * (15.0f/30.0f); + + skeleton_sample_anim( sk, s->anim_grabs, grab_frame, bpose ); + skeleton_lerp_pose( sk, apose, bpose, s->state.grabbing, air_pose ); + } + + skeleton_lerp_pose( sk, ground_pose, air_pose, s->blend_fly, pose ); + + float add_grab_mod = 1.0f - s->blend_fly; + + /* additive effects */ + { + u32 apply_to[] = { av->id_hip, + av->id_ik_hand_l, + av->id_ik_hand_r, + av->id_ik_elbow_l, + av->id_ik_elbow_r }; + + for( int i=0; iid_board-1], + *kf_foot_l = &pose[av->id_ik_foot_l-1], + *kf_foot_r = &pose[av->id_ik_foot_r-1]; + + v3f bo; + v3_muls( s->board_offset, add_grab_mod, bo ); + + v3_add( bo, kf_board->co, kf_board->co ); + v3_add( bo, kf_foot_l->co, kf_foot_l->co ); + v3_add( bo, kf_foot_r->co, kf_foot_r->co ); + + m3x3f c; + q_m3x3( s->board_rotation, c ); + + v3f d; + v3_sub( kf_foot_l->co, bo, d ); + m3x3_mulv( c, d, d ); + v3_add( bo, d, kf_foot_l->co ); + + v3_sub( kf_foot_r->co, bo, d ); + m3x3_mulv( c, d, d ); + v3_add( bo, d, kf_foot_r->co ); + + q_mul( s->board_rotation, kf_board->q, kf_board->q ); + q_normalize( kf_board->q ); + } + + /* transform */ + rb_extrapolate_transform( &player->rb, transform ); + +#if 0 + v3_muladds( player.visual_transform[3], phys->rb.up, -0.2f, + player.visual_transform[3] ); +#endif + + v4f qresy, qresx, qresidual; + m3x3f mtx_residual; + float substep = vg_clampf( vg.accumulator / VG_TIMESTEP_FIXED, 0.0f, 1.0f ); + q_axis_angle( qresy, player->rb.to_world[1], s->state.steery_s*substep ); + q_axis_angle( qresx, player->rb.to_world[0], s->state.steerx_s*substep ); + + q_mul( qresy, qresx, qresidual ); + q_m3x3( qresidual, mtx_residual ); + m3x3_mul( transform, mtx_residual, transform ); +} + +VG_STATIC void player_skate_get_camera( player_interface *player, + player_attachment *at, camera *cam ) +{ + struct player_device_skate *s = at->storage; + struct player_avatar *av = player->playeravatar; + + /* FIXME: viewpoint entity */ + v3f vp = {-0.1f,1.8f,0.0f}; + m4x3_mulv( av->sk.final_mtx[ av->id_head-1 ], vp, cam->pos ); + + v3_zero( cam->angles ); + cam->fov = 119.0f; + + /* TODO: smooth clamp lerp rate of change */ + + v3_lerp( s->state.vl, player->rb.v, 5.0f*vg.time_delta, s->state.vl ); + + float *v = s->state.vl, + yaw = atan2f( v[0], -v[2] ), + pitch = atan2f + ( + -v[1], + sqrtf + ( + v[0]*v[0] + v[2]*v[2] + ) + ) + * 0.7f + 0.5f; + + cam->angles[0] = yaw; + cam->angles[1] = pitch; +} + +VG_STATIC void player_skate_transport( player_interface *player, + player_attachment *at, + teleport_gate *gate ) +{ + struct player_device_skate *s = at->storage; + + m4x3_mulv( gate->transport, player->rb.co, player->rb.co ); + m4x3_mulv( gate->transport, s->state.cog, s->state.cog ); + m3x3_mulv( gate->transport, s->state.cog_v, s->state.cog_v ); + m3x3_mulv( gate->transport, player->rb.v, player->rb.v ); + m3x3_mulv( gate->transport, s->state.vl, s->state.vl ); + + v4f transport_rotation; + m3x3_q( gate->transport, transport_rotation ); + q_mul( transport_rotation, player->rb.q, player->rb.q ); + + s->state_gate_storage = s->state; +} + +VG_STATIC player_device player_device_skate = +{ + .pre_update = player_skate_pre_update, + .update = player_skate_update, + .post_update = player_skate_post_update, + .get_camera = player_skate_get_camera, + .debug_ui = player_skate_ui, + .bind = player_skate_bind, + .pose = player_skate_pose, + .gate_transport= player_skate_transport +}; + +#endif /* PLAYER_DEVICE_SKATE_H */ diff --git a/player_device_walk.h b/player_device_walk.h index a0ea96b..508feef 100644 --- a/player_device_walk.h +++ b/player_device_walk.h @@ -30,7 +30,8 @@ struct player_device_walk } activity; } - state; + state, + state_gate_storage; enum mdl_surface_prop surface; @@ -414,6 +415,25 @@ VG_STATIC void player_walk_get_camera( player_interface *player, cam->fov = 90.0f; } +VG_STATIC void player_walk_transport( player_interface *player, + player_attachment *at, + teleport_gate *gate ) +{ + struct player_device_walk *w = at->storage; + + m4x3_mulv( gate->transport, player->rb.co, player->rb.co ); + m3x3_mulv( gate->transport, player->rb.v, player->rb.v ); + + /* analytical rotation of yaw */ + v3f fwd_dir = { cosf(w->state.angles[0]), + 0.0f, + sinf(w->state.angles[0])}; + m3x3_mulv( gate->transport, fwd_dir, fwd_dir ); + w->state.angles[0] = atan2f( fwd_dir[2], fwd_dir[0] ); + + w->state_gate_storage = w->state; +} + VG_STATIC player_device player_device_walk = { .pre_update = player_walk_pre_update, @@ -422,7 +442,8 @@ VG_STATIC player_device player_device_walk = .get_camera = player_walk_get_camera, .debug_ui = player_walk_ui, .bind = player_walk_bind, - .pose = player_walk_pose + .pose = player_walk_pose, + .gate_transport= player_walk_transport }; #endif /* PLAYER_DEVICE_WALK_H */ diff --git a/player_interface.h b/player_interface.h index f28d7a8..1b9e16b 100644 --- a/player_interface.h +++ b/player_interface.h @@ -38,6 +38,8 @@ struct player_interface *input_reset, *input_grab; + v3f prev_position; + struct player_avatar *playeravatar; glmesh *playermesh; struct player_ragdoll ragdoll; @@ -67,6 +69,8 @@ struct player_device void (* store_state)( player_interface *player, player_attachment *at ); void (* load_state) ( player_interface *player, player_attachment *at ); void (* debug_ui) ( player_interface *player, player_attachment *at ); + void (* gate_transport)( player_interface *player, player_attachment *at, + teleport_gate *gate ); }; VG_STATIC void player_interface_create_player( player_interface *inst ) @@ -162,6 +166,8 @@ VG_STATIC void player_pre_update( player_interface *player ) { assert( player->dev.device ); + v3_copy( player->rb.co, player->prev_position ); + if( player->dev.device->pre_update ) player->dev.device->pre_update( player, &player->dev ); } @@ -174,12 +180,37 @@ VG_STATIC void player_update( player_interface *player ) player->dev.device->update( player, &player->dev ); } -VG_STATIC void player_post_update( player_interface *player ) +VG_STATIC void player_post_update( player_interface *player, + camera *main_camera ) { assert( player->dev.device ); if( player->dev.device->post_update ) player->dev.device->post_update( player, &player->dev ); + + /* FIXME: only need to test against the visible gate.... + * OR... bvh */ + + for( int i=0; igate; + + if( gate_intersect( gate, player->rb.co, player->prev_position ) ) + { + player->dev.device->gate_transport( player, &player->dev, gate ); + v3_copy( player->rb.co, player->prev_position ); + + /* Pre-emptively edit the camera matrices so that the motion vectors + * are correct */ + m4x3f transport_i; + m4x4f transport_4; + m4x3_invert_affine( gate->transport, transport_i ); + m4x3_expand( transport_i, transport_4 ); + m4x4_mul( main_camera->mtx.pv, transport_4, main_camera->mtx.pv ); + m4x4_mul( main_camera->mtx.v, transport_4, main_camera->mtx.v ); + } + } #if 0 camera_update_transform( &player->cam ); @@ -274,9 +305,11 @@ VG_STATIC void player_spawn( player_interface *player, struct respawn_point *rp ) { v3_copy( rp->co, player->rb.co ); + v3_copy( rp->co, player->prev_position ); v3_zero( player->rb.v ); v3_zero( player->rb.w ); q_identity( player->rb.q ); + rb_update_transform( &player->rb ); } /* diff --git a/player_physics.h b/player_physics.h index 56d793c..73bad63 100644 --- a/player_physics.h +++ b/player_physics.h @@ -13,6 +13,7 @@ VG_STATIC void player_integrate(void); VG_STATIC int player_collide_sphere( rigidbody *rb, rb_ct *manifold ) { +#if 0 int len = 0; len = rb_sphere_scene( rb, &world.rb_geo, manifold ); @@ -30,6 +31,9 @@ VG_STATIC int player_collide_sphere( rigidbody *rb, rb_ct *manifold ) len = new_len; return len; +#endif + + return 0; } #if 0 diff --git a/rigidbody.h b/rigidbody.h index 5c497f2..d2ce949 100644 --- a/rigidbody.h +++ b/rigidbody.h @@ -169,6 +169,11 @@ struct rb_constr_swingtwist float tangent_mass, axis_mass; }; +struct rb_constr_spring +{ + int nothing; +}; + /* * ----------------------------------------------------------------------------- * Math Utils @@ -213,7 +218,7 @@ VG_STATIC void rb_debug_contact( rb_ct *ct ) { v3f p1; v3_muladds( ct->co, ct->n, 0.05f, p1 ); - vg_line_pt3( ct->co, 0.0025f, 0xff0000ff ); + vg_line_pt3( ct->co, 0.0125f, 0xff0000ff ); vg_line( ct->co, p1, 0xffffffff ); } } @@ -1276,6 +1281,7 @@ VG_STATIC int rb_sphere_sphere( rigidbody *rba, rigidbody *rbb, rb_ct *buf ) //#define RIGIDBODY_DYNAMIC_MESH_EDGES +__attribute__ ((deprecated)) VG_STATIC int rb_sphere_triangle( rigidbody *rba, rigidbody *rbb, v3f tri[3], rb_ct *buf ) { @@ -1323,6 +1329,45 @@ VG_STATIC int rb_sphere_triangle( rigidbody *rba, rigidbody *rbb, return 0; } +VG_STATIC int rb_sphere__triangle( m4x3f mtxA, rb_sphere *b, + v3f tri[3], rb_ct *buf ) +{ + v3f delta, co; + enum contact_type type = closest_on_triangle_1( mtxA[3], tri, co ); + + v3_sub( mtxA[3], co, delta ); + + float d2 = v3_length2( delta ), + r = b->radius; + + if( d2 < r*r ) + { + rb_ct *ct = buf; + + v3f ab, ac, tn; + v3_sub( tri[2], tri[0], ab ); + v3_sub( tri[1], tri[0], ac ); + v3_cross( ac, ab, tn ); + v3_copy( tn, ct->n ); + + if( v3_length2( ct->n ) <= 0.00001f ) + { + vg_error( "Zero area triangle!\n" ); + return 0; + } + + v3_normalize( ct->n ); + + float d = sqrtf(d2); + + v3_copy( co, ct->co ); + ct->type = type; + ct->p = r-d; + return 1; + } + + return 0; +} VG_STATIC void rb_debug_sharp_scene_edges( rigidbody *rbb, float sharp_ang, boxf box, u32 colour ) @@ -1418,6 +1463,50 @@ VG_STATIC void rb_debug_sharp_scene_edges( rigidbody *rbb, float sharp_ang, } } +VG_STATIC int rb_sphere__scene( m4x3f mtxA, rb_sphere *b, + m4x3f mtxB, rb_scene *s, rb_ct *buf ) +{ + scene *sc = s->bh_scene->user; + + bh_iter it; + bh_iter_init( 0, &it ); + int idx; + + int count = 0; + + float r = b->radius; + boxf box; + v3_sub( mtxA[3], (v3f){ r,r,r }, box[0] ); + v3_add( mtxA[3], (v3f){ r,r,r }, box[1] ); + + while( bh_next( s->bh_scene, &it, box, &idx ) ) + { + u32 *ptri = &sc->arrindices[ idx*3 ]; + v3f tri[3]; + + for( int j=0; j<3; j++ ) + v3_copy( sc->arrvertices[ptri[j]].co, tri[j] ); + + buf[ count ].element_id = ptri[0]; + + vg_line( tri[0],tri[1],0x70ff6000 ); + vg_line( tri[1],tri[2],0x70ff6000 ); + vg_line( tri[2],tri[0],0x70ff6000 ); + + int contact = rb_sphere__triangle( mtxA, b, tri, &buf[count] ); + count += contact; + + if( count == 16 ) + { + vg_warn( "Exceeding sphere_vs_scene capacity. Geometry too dense!\n" ); + return count; + } + } + + return count; +} + +__attribute__ ((deprecated)) VG_STATIC int rb_sphere_scene( rigidbody *rba, rigidbody *rbb, rb_ct *buf ) { scene *sc = rbb->inf.scene.bh_scene->user; diff --git a/skaterift.c b/skaterift.c index be5d4d6..7420fbd 100644 --- a/skaterift.c +++ b/skaterift.c @@ -24,11 +24,13 @@ #else #include "player_interface.h" #include "player_device_walk.h" +#include "player_device_skate.h" #include "player_model.h" /* temp */ VG_STATIC player_interface localplayer; VG_STATIC struct player_device_walk localplayer_walk; +VG_STATIC struct player_device_skate localplayer_skate; VG_STATIC struct player_avatar localplayer_avatar; VG_STATIC glmesh localplayer_meshes[3]; @@ -233,6 +235,7 @@ VG_STATIC void vg_load(void) player_use_avatar( &localplayer, &localplayer_avatar ); player_use_mesh( &localplayer, &localplayer_meshes[0] ); player_use_device( &localplayer, &player_device_walk, &localplayer_walk ); + player_use_device( &localplayer, &player_device_skate, &localplayer_skate ); /* --------------------- */ @@ -308,7 +311,7 @@ VG_STATIC void vg_update_post(void) } #endif - player_post_update( &localplayer ); + player_post_update( &localplayer, &main_camera ); #if 0 menu_update(); -- 2.25.1