From d9fce4457db3af99693114e6f7e285968a0fcfc0 Mon Sep 17 00:00:00 2001 From: XProger Date: Wed, 15 Feb 2017 04:11:26 +0300 Subject: [PATCH] #3 underwater acceleration and tilt; #4 new collision detection system for walls (slide and rotate along walls); #8 first person view mode (V key); auto-rotate camera to the back while moving --- bin/OpenLara.exe | Bin 162816 -> 166400 bytes src/animation.h | 2 +- src/camera.h | 141 ++++++++++----- src/character.h | 37 ++-- src/collision.h | 104 +++++++++++ src/controller.h | 44 ++--- src/enemy.h | 2 +- src/format.h | 10 +- src/game.h | 12 +- src/lara.h | 290 ++++++++++++++++++++---------- src/level.h | 45 ++++- src/platform/web/index.html | 2 + src/platform/win/OpenLara.vcxproj | 1 + src/sound.h | 6 + src/utils.h | 38 +++- 15 files changed, 532 insertions(+), 202 deletions(-) create mode 100644 src/collision.h diff --git a/bin/OpenLara.exe b/bin/OpenLara.exe index 6e40e4b54252dd130294dc292eec8b9f5309f8e7..58f39d77703054f6d8494f327ecfbf8d5b039277 100644 GIT binary patch delta 36405 zcmb5X4P2B}_6I)qfq_B6XHZa7P}EULQ7E)9G(j1l1RZ>V8AY^GqtR`xW_&4hpa;zH zanu^?T5F}H<+k>Ug_bFx38tC7ZCdUwmF+$(wu|W+nK}ROxz7yh{(isz|MTw?^W4{S z?>+b2bI&>V+-p^5RMqBgo3uV_{`ivJ5d5#z5XQQ*XvCx^0^fFDc9j_nX3CTN?QDb; zF8TXN!`RxoJEY|iz5j868A{YnZQZYdfO6Q}9$i|heSV?tn2CiAwXK_E_(?X$w0OnW zcv5^FO+U%@#-HTW2ILt}`OkMx7}FZec`0re6}^_Ng)$QUE2G2fpAg-rUuP&k`M>fF z{s*EH`k6!z98E8!{8x6me_QkbPlDdTV8_ZEm7z0Hj{2OmR?mvp`|db|PNCnyK8r@n z8($hDpx&6N4+WW-|4XJm9Au8CO#g%!S;9c3#`Kpuz5c~9{i6(`*>Yowe`8F7KP@Iw zGN#nM8MB_Tgt`gxfN(b6U)F0Vejn>Kp~rX>z95%dFXG2&_J7=Ka6d{qZ9RJ`W*7P> z@=n{Y=-E+mDQuVjmbkl6dsW<=$b_@7)#%oSu8-wDk&pQOtV9;ZT2=dkDdp@n}X)IVq=ov@D(==!10_ z&0^5@CcO|Ma!Qk&_US`t#t~!o_gXMGK`&a?DyJMnVv=5iGK&@r^#E2p6_8U}5EPkm z>5p<++p^wr%6BOLy_|CR0!R=Akh=Ul;3M^roYt}|27%8n$YmcPvixUY?9eRrTyb%U z`qKqD)_SE>{RJR<$Z0Vypk&#K3-auS;^Lp=*l>#`zeGdrybyy?cFJkSwJ70m`2VnA z?QN*T=!b7oM6f=cl#*Cvm?NY|kjE0({^5zEr;Dw_HH2oF`$fJ$a zo~Uo~t-82#=g#(Y)QsQne_-KUw%>nzA;<56MHOjPD7SOxvWMl;26XPjQ0IE+^ME=u z16n86vR?n!MM?cX6EO!-S*(>N(~7BbSUa_Szxp%od%m}h?Q&xakXoXiFD^!I%r5^w z?i&ylw^aZ#?)Ueon8(a@4^%`&m><4KiLusB0ZJS1fBb=a%KSR1EQs|g^CTl{-gER6c4EmUFf$}vN?=tV$mvDxA6)tLKF(LhNG zh(g=eRr1l51p1wojcor*%O8=@y%m+Ogxz9W69{-C9FE!sclU}4&m@CpM2f>Hl?HCr zzCjS5Mtn*U_vV?jmNow3szK7uHGWssU{>wtRm1xnuGRT!FEuYZEHAv&7I2%i&5zf8 zSv7^pH0?5PYOB8*2sE=6f7)YN1DDjG3H3<%+@^5CjdF*(q2`&C`z!{VH$Pc( z`eW7%otRr42&kpWj*?<^TB0{XD>?<}4sX6$ZJvjgP1LeF)!ZCG)wP(fFw|vn+5=Ve zP1F|oA6#>@P5p-I)Hb*x!#c9u(Uqss={eWxYJd7O+9*l(<;B^G)v1Zv2Y`Y$^33J5 z>d=6;7~n2h!Jr~bw7+}&Kdo6Rz2osOczmVwjK}}C$A|S1L$ZgflcT*4#;Gn#oHr-B znVs|}t{owNx|$FhtxS%-;!M#7`)92k)OWj{R?&(bPrHB!b#GL&p~b&(t%V)-?^(N} z??sR~DBNflUpvUbfwg3g+3$bQI}9_w#mhjS{%6;%O`BPXX$iMf-DxmnTv9trO0_1B zXf)&K-j*1H!5KdL=kMfAjX$66-cqa$^B1d0Y_0$IYTv#FFux1fZEQ2NCos3_tSjmX zG?KKxZoB#ji+X=0x>l;~S?Ra<(wO3(>9eyM|7*VSl5eHI-S?1mdbxkzhBE9K`!@`b z`Xtm>59f5WmAb$q+)=KivNFo5o^L!=G3tJ>&Af@WjK+;_vljb+5x9gh2e3ZP&`C z`(JsosONVN)AS{1UyE;|_SwUA5&j0o(*4bwOU(%niowp$F&Ii_Xdk)#qn}Eajy~wO zJ#~}x{Db~SpPD4?d(eOIse#g#2mP&2Ju0od-#_>1$F3}3tGS)s;s2UXVzd28Th>Yo@AJR1WubJ}ef}QLj6lw~XDXyGf9HSc zncuNR{@7>du?YWz&vvkQ|2=Qn3@ZqwG@>)-j@2IddquEqT%a1*=7H69tzcs2tAr1nRd?v_j-;qu^W-+(E(VBDkA^`69TN zg5yPy=2^R01RE$gOau>8u)hc%r=VE`zo4K|1W!@$@_fQTD}om7-y*D0@>vn=px|i{ zyhyD>q-?wY_$&RT-`o9dulS|Q<#nrf9cFBdfAXtCSr7mHulASx zW&SmIm?+ZBn^W@4H#goo!hEjt(a3c zx$c#4w!W^tzMQcF|I~e{`1S1D%3}P9@71!${V%?EFB?)9@xC`qdL!Te#DOo^eE*_@ zi=?;n{U;74Bl7Qq@zU3M{)h%Gw!;c`7u?clqu%ciG{m#x{`f{k`n%QtY~u&4-fwR@ zf?5L)eJ-t^>`y+F~v z$|#iB-+#+d4!|EBozKqtM}G7t6#D9;2nn+CUyt`=+5TUSXEMA0mXC`DPkD-9IgG7h z8%U)s!1|_K->JgD9>QOpIwgHF%Kz5s$5~$8xUaiM-n8Em2-G%I>>ULuM}EpUW;^!MT}XqTmGKRP ziar&`DNEbz|J}Dq!uFLjZ7ZdHhO~;^6lxjbU-4by zgjbP}nKTy7@6(XHw-rBfZlkTDg9@z|xzxFR<}~D*@FQ{)5YQg*fBIcf_hbUOp5ndz zH+?@)dUv@0uJ7;YdkM+3A@_4KU(uD~88g88nA)QK?BD)sJ5kA&FJ_&v)#` z@AKz};`i|ROl)?)olix0#7~9zUH;Q6SXu)woc5TBb*jsg8~<($TW*r;^CHGsP437y zS=&Z~!GL(eEuUo^T{|n_E$Wz2+pu~ilKQEA7N3d;do~kGS*{PuLIs;#KUsR4DGM(e zP!{DTsgdfYY*8T39G0Kzh4xTCIoucBB-hW7O9&>t+S0^&I6;1e(rjseF1})0}zbP_rN)$_H~I^L6>LJfLD$fD2mqR=4@t^*zs&2%8Qdu&oSaQk{ zEQqnR_u23CfPW=wEhe1iR?peradYAqc!SpS8v2g92H!SF1xDi)U^$X7X$Q zy+4nQS_`=jDJ1Ot+=q3$x`ZkeC1`W}O`Ub@mAYlW?*^GrcNx@uuRq7?xMAWtC8*;lj!rHc~r6W<|r>GEdz$WamoJE3pNrBHHW ztvjv-_xkdgFZvfb#nb35N)G1lzn0%1#}@4x8fGw5tuz)WEmyLAxgYNETFuq}fEa?b zY04*8^D%Xguc%RS?eHi!dU9_?y(6I1oNUC*?Vx z0SasbpDr5^)}+pgLt=wG$J2n+NjmIi77j@5VS1+3qUMC6tcOq8HVW@r4r1^Cq7h}u8*p#0ch6KC-IdW-?hsZ@PHR@Dj zp7}v5236FdT02mp@Pku`Suc968W@DvdanI$j1O?36}Z#&W&-ZyP%En5iV@V%K0H`Z zx~`x(RPY|X;0)Dj_Mn-W)QmZLFNP@Ew-fYwf?Th9%;}h+j+1HHJxU8RCgVd&x zN3yI5l?iI|$fhi7qDLP2Ro1Lzk16YCPxvhj?UAb0;thL~%`2ay{@|J0M6{UNSaKi$ zI(lZvpMB9%<1gKi)P!kn1OXr|tf{fHTRHI&U5_kl$!c_m!yI#HHH=GUV<)%^=&11! zW>lZavKX>*kgX}B;ZRdF;eu2+=)cVot&dGIkn-Ex7zo@El8^+lqGt#cm6SGzxfJO2 zQ^HjiuwnH@s~z4c(dsep!zOKDcW5FdzA0wS`Y(UxZ|(tIdV%N#tT<>Hb5Ya@laf~L zKlWRH=}5R=`|W0_`vHIdtG^#Xu(efTSoZIp1!A>_D7b8i1_CDKy8KW{sWTax`sJ{? zJy&xW?2nfM$?~F}n1c?Vb%#wop+!CX)KGS_wCAU%?99dXKlL$7X0E6H#Y&lr=P@=G zpGwAVVXpc&7#jf9nxBx^NTgntSP4GGVJrooN5a@pe71$L1JciP`227-NZxSeDr|1F zLaZk3I|tts&JtoT{6>kwjuj1C*FFc|7tZo!8Y+-Xqm9;jIk;?ODFYhq#F>LuFTima zgBSt4kL`RG%1Dpe>pe!+%=))O0aiPJshzhp=!6?*P;*R+i=zi{S0o!W8vUqVOITW6 zDmIpwF6B%|v&h7DiB33-(e)-1OJxIMez}VMyYScWp#hJ^wCxgon|pi%1W@< z?E3hQLoq<1cpaxOTsS`3Yk)+7rSRYi8sHG{q|_&>fD3#(hn!TK70uwvDsUxx3S8y_ z_Y^FcBSOw7Yi=KaNK+_slW&ayytRvO+Oa3c=B#M%>wD~frdLx7DB6tL^Jvk*OzS|? zv2W2dv^|V2VX*|~NUb6TjweO4I4Px_XGAk!)a%prf$Zl;quC&kUVAj#E*;p#H^i`1 zY11ygKZb3InhF7h5POo(?E&hWKb&<%csKTFCXkKWW;VQ5Ub>a zQzYdO|6NbkH|`@qtUW5%?8H}%P<{c7P5jxOY@kP-nmumncvp8dqByW@ym}&FOQ>31 zVUTO~p{TFi202YVrY)En2w+5MJ-9k(AB(z#_x+fr}H`UooZa71WBk4YLxsP%fL-&}4%)gC_{FBHlI)xlYNKw*tn+6Wa7k zfk4}MBvA(fw$!%tu5fxAw4{J?D70)K(fPs{elDJsNI$&Hb9=MGQT*jVV7q+>_6}t;nh&mi6YW1_dmRnqcgU1j zcaDG1oAv3p{vUzBI;&ZEr4tnlYac&{UkG{4k>~iodb0^q@;N@L54$}pD4!GOAQqu^ zIs?V(7wRWq4J`pTEcgy#p$fRokb1x&8X@^C@=7I|?>YWrA2vqHJjYMz6tmKsK^v&Qnj6w=MZmaEj`4G*Q@DOG;_e{P|11pg)5e$lvPE%A`M? z<8mU{!jxzEghV!4dgC1bT_PL8cJn6^*~e@=pO?fEi-TJ%m}_WzEu&2b8_JJ4`jW@# z3hh_#im16{g`A>M$`v`~2UHq$60}4|Q}vU35{#4lcoMsrweqV;Y=qs&(e+O6sw)cg zk`2_`3KN#FcH)Pt0rixl_8V7uX<%76NUKMyAXJ5@u{kUq`}h$h97UPdJJ=7}EJU_; zCYx6cU=K)F|H(TBuv?`k+jz=AOv4Dy%Ljs)WjxIvAIJuHl(U`H=ZIEktea~Bk(Aba z5qh!t%py^1*z`riDKZ+~!)cyXRWQyGJ23CM)0Mwq!JbA=P|?13A7anHhV0 z^@(6*F-`;c%ESDnWR}+Lb1V#)Do$v(^E1g1Vy5z6lUd?T>1!t$!q!p;yhRrU5#Xn& zQ%kr*rvxFSbZR%>!LtXk-sAcrxl6#06>>f36PoiZ zpI~7Y>Hcr{y%tv5V|E2Ph$Z<6Ey-_qi-p}j(DB!+U8X+KwwadO5#6R4FIVBrEY^LZ;xrvPnly?GVg0Yoe9cc(>94-*^TRiy`wdz z9YJ=N2!o@0U4FA((OyFa=CSCCPo;HBxFonfcZWDyHn#HJ!&twY^a{FWfHJ`oZu1NX zS7k{$H>43bPVsO1L!60mapQ27BmL*!ykIyRdi&&m6UJ?CQ1M ztKl?%4#lQy{sP;)N_$oy1Icm+UmpWrh_ zuq?@=@=YVyQfcB}xHOVI>v0|?#suA2h&V1AiI62onv&W^x*;UOTF$ZPMn_mR>KAnJsXN6j_VgC8q@-l z5U;DtatwyWopE9N=4TTrdRJYvsI*C8mtkY3fw-?AH|(-+c{IgV znZUuaeu4EtE;^!mkZA}bTUu^oH2R|Ek!cOnlWR^u7}JM<&w4gU9@wd4t>?5y3ee%a zgnabIgbrnO%IlY5|5!XVA=nWdk}HiJVZTs=lj0WNm5}IL+TmMqHtf7eqXeYSD{mf~ z%fNuDCaO(j1Q!yma;{*8+6Z%WNolcaf{k6v{r-kY|6wwpk&1+QY=X3(n{S46b!8pT zyBXZJeiVQ1X2_G9)A+|XvrpJQ{@g8WP3*l8;I5;}@EkrSgAE({UIRMVqMe(BkGnGi zoxPY3H7Pv>=x&wgoW-gbx|Dk|*pgo4AtR@KC$D)N6H5*)B_sLO43-?bR4~@EQ((#X zGaL8vv@xto7w?Gu(NTxD(vs|hYy`0?*XFCSv;c-pD}}ERT6aN&R6TJ%mn9qIK@)<~ zI97O)vOm$*z-dX%Psq6Bby?EY6Zka~eXR!Q0z~2{ayKFXf#wTuOTnTqyp0bT3w^SX zBt{1|jE_JTGc5^-P^ioT_Ch{eZ;u|NRUAr@MqYmcse0I5m3!N;6?8{j*>zrf!c%eLfTG&#zVL@(fA90ge|^7@_F zQ2^HrN00o39ORfO^)aLZISoLL`iV{R{UWx;q5JuUTiNQcnU*--<2Kei45j(7+nBZI z7(pRrt>|wyH3(zPs@qsZdzdMH3oSDd&b8RJTT=v{p#_6H$A~^B zpwa@AHJ4dZkx)iG1u{We7-Kq8W|HV%c1r9`Koe1!j-xV51>pw;0Z*h-L0~_2rpK}b zpF+!0?Ze@s#X?IaTFe&Bj7N)h(GMviLlM<6T+33d^`)YTOq6IvL6PhdN#jL$1?mtK znoN|T&4^M|VhxyEb(%TsgqD`2FC)yd(ik1qh*TSrOQF>S*OAS)-f|Wow2|Wi{Mp;t zF-#WFTGpb1N&&tKc?xJWIR~xo(4$S5&DgD$pqjbaLIMs_)>}vh*onoO?nUKLXM!vG z?JNA%EH-#{Hz@zU_jjYa&0!imseBKwP+9^zGjN>iRkk77tL(sU&RYL^e0)l_L<76i zYxY&qw5wck@w*kj+64TBt_-Z`Mm&`|a@&slXcD#d>xQqs`au!YD z)P!tPJ7L$jZpX;Gl{+V}A)dfHq4D{an$<(VLYJji_yei+1fw~U#tt`+zU0aG@nJBw zWoGR|d`q;0_=UAU;~QW7e5~imzIT^mXNJ?^%gC7GSov@D6M$@sY!vdha;`$1Wy)xn z0pla~M;?6#GkXY+&B`wP0(*wAH6-!aO0+nW!G~t=Y>bOk(}#>XoxlnUEfA9#M@7LV zji@Dbkzz<#|29CDZ8r|4%+1PVnG1NwV%AMo+!RP*jI;eVecxT zch^B@wH}Pk9vVs6=HN)|*bmVIeZ-<1iU}#8COGI&qJE}P6Ra0XHM=`e9z*$`KqwJn zNvtSQlg>`@$$9KKkNW=qYD4MJ9{S%iG|i!BXc+-{&W}FpUQjZQ)^`DX@KQnfC^D=d zt-+?^4B5Pjns#gWiP2AM&9g2dG8bJn^LhEKXK@=i7)&Q455sGNex zv{LQTP5iw(p_^{|BmdW(FnVp{6YpZ9d%gfh00v~;O8ljp-KK5ep1atDz9F-dPDimy zAyzMNsO;xo-UY9z9Xxvqv*5FE3cEeHgzH-QD^u7Mu)7OWur1J1z&cGN9`Sy|PMiZ^ zllDzb(NebwQOBs;ckG0y;5H;gln8Om0s6tJgW8QoVOm`l0r?=Iy(g|lP>&FygNkH1 zk1!me74#G!2dKY-sEtt53_ku;Av4GB5i+|YX1D%L+REQ8ME5)SpduCzv;A#FY*cSr zreSVjXeMJ>=#=x3BKFT1;&y0aQUPuRxcGxp*=v}Ifz!a%xAMu;*kUR7MgHbA)<+uo zB0oBf4ZWEv`0V6n-2p;5luBFEW1%|EXe8QDQ$1GQOyZFMeeZVu z1VBr<+xeR|Hq1jp18S{9zVg1#nvS_2#^933faFtxEG2lkg(P+3HlWRhKoVIv;ZK5a ztW`r}`kj_|N~U~K(AgtUN7gTtnTu(G&W)d07q#Zsh21E;hobJ~j$)9Fov$utbBBm# zttZig|87$~<^&i=(q8`74A!f7?+wjF!rV)3V6niph}t-(eTRJ>=YMG-`9?q$x|gr0 zRV+x9ZPm?a+8UYPGo9Te&5`SU)8TCYe{_Tf7wX6^(UESX%!%=C;U{LW1SxGd*Jj`V ziu&Aps4V@=+DmzG+9D0ixQl->i}gvN=?psp6A^~L z1Am)s#rph!270|JrX*QeKVUEKQNjimx1zMt8KJKNT4L>qN~e*|(#$mjE{a7^UNfK* zfzUbR+zx_UeWXET;`WlS)`bl&?5{+c&hGWAOPG;K_jcp8r7Ss$5F^mSUoT~=Axn(1 zv)@ajBlz=nSoA-8n;*8bu{}P89|}qpfqcqhT6v^{B}dX)@G5)xXa~C`wx1{vJiF-B zGK~B|2kSk&Ka#II*&vd#Uex{)PSCmvp9tr#J6J;B*hroK&|G9R;5bM52*t3y^S?RR z+frTx_s@od$S!_xHcOYj3FGHyvrP1O#2hw}jpWWb?AEvuVFJc1_@Loj4H_(Uch4O5 zMwAPU(!Ipw`Xyz!1u4y7eC1sBdk<{3LFX;-E8zGuTmTOitvGzZfPKAS-Nwi zNay1C?D_0LY3(Tf@qBFVXZYp$ENxnM_!J4`Veixa46h@!uA@$DhJ-i`ssD|*bK0w- zELbg7d`_>3Bi$8uvwPV_e&%jAW*!|F>$DW~7b-V~+_ZGtlb`SltWIlnt1s=Y*-d4s8~xUYXht0MdS8-dMXmifN4_JSC!|ny5`k> zwqoqKnorLlb#I?q$2A9nbNP?`6wI9)ycZ zO#@C8$jnopdxx0_dnL?&l=v_bVJOB?dWjacjz6$~P3ZI8OK`*(LK(HI!HtHJE1g$4 z!h zi-n6+h~V{O?yr94!xljoaPgu=Y)Y?2$T{jebcm`7&ggspT>YOGv2JYIIlXi{{3uXTI36W>awJQ$d9PU1RcdruGIEFhh&x7ASE@t_NbeBepe)a=YBSR zNCk4CJ+{xdq$Hw9mCNW1v$tE}Zf|fVlIO$3$9SBR4eLSJBtsBD?$mnn0w+uLYy&F9 z;RXagTBt9R)y1G>c9&}pB7lCcu>dG43G;sYwv5pNV1dQa1dDrT$zP}ZblAR-oy+l>Z8%OpAlK#t>63fPfVW+@aZ z6fCoLxShg9%LHp^rB<$j_A4#6FjqH1q@nFwpwwv~H!QN7iZjXId1+5fEwp&Xt2M$P zJhJhbjHZ!IN@wyaI2q!o1uPM09zjH`mrzd_zW`2~*APjAWzfzCpX>&71)O~1vnWWe z3*Ft3&Teh@!FB~&KJT1Q8%945Ol9T4X$^A^2jIP*(fqmY%2&nAVk+0(f;viT!T19A zEzU7*!zdOQ;~GYUFh~hRxe`GQu0BejudAmL=D7J0PH56INIlYi5_=8ZDnB1wJV63;C2H$F6+P>PPVPT1f+pv1 zd)uQJ^$hiSB184iPy$&xeyvdUsbh{N5*fYH2B=s#S-wQaO-G=yERwy7Ma4 z4J0azY@AR9OnQ0&gXFX9yoCgoRxI@Pgb7tZt*37&2coRqcn&%91e)7>sH@W_RAG;B z_Or`VJF`}&yLxvG`GlrVD}0>U9Y5V^ zsCW`xrRiy_#D?$(>UW;YDXS(sT_tRB->iH;VC3IiL-3cNw1>Mgm3EUW(wW#6hcP(& zw#Cq=Ut0uy`nQqO_oYLgWY^G3hfG+xzl6EN)u#4>tdBqtVS)&Oq5GZ54j9|oZ@zRW z+m%5j!!sJ%hg>>j24r*F`vQoBaPPJ^5O&62Iuve~afaGFZ;lWVYQ98gc(exN}&0KQ(x6Z*sxJn%#CUwy#S~ zX3(P1Zfh$Mu?V?l3Ic@CIEM7g3w!lNtxGE$C9x??U3b$!zC-a9;&O32D$ir-Md@}I9D;yI$uq~c~Cb_0N zJLT>_G;Evwud$ofWpS5ZEEMQJ#>ZqJl*v1chR_v|;gx zfg>Bdmm6KIL_U*)X~AjFNSqW+xGZs}i;ecwpA+<9#6q8UNX>VZsL=G_VEaAcy)#j+ z`C0_}$~B*gK!3TWMFjfEHMHX@ol&}1u+rIGu6a$QjB($Oc8x-VU%o<}21!Dfn{=D) z$Hxorz`!3Te!=^=*{yfI4O<25R?Z>q<1Rf;bearui)~L;I(xc@5~v-RT>?UL3022P zvn{7RJ_bdXg*#(lGVW<_^AWrnH_NjQ20T>Duk=mB4hHvh6bF1XnqMh;76e0{K!S~e zM>&kSLYdP0rR6N=e{r*irQ^%^^kpo&=iaU8;x#u7$8T5pj%D!b=(UPBFJrg#EWR=S zrz=-^_vKJt|5(XyUe1zbfsDy3)F}cPTloCtEQ5Vfzj--ZEZm|XBT3Ee7UgpGs)38Vx@fIdQ?+J24;V58 zL_+x--5a5}1SS%ADLwTx$l-+cE%=FYfOfWUca-vSE6N+%i-Dyztp*-!ntvmJmr2H%+{;2QpM>+jKg#+GkG$pPjH9~v zLrz5U(~Wm#ed>Zh_gUL}h=-$M~S zN7k7Tr1E&dmOzr%Qi6SpKP=jCd@7HQhecexydtYa7APD;c2VB}2#aJXcYK7i->$nn z_K zNMfc13_fMSQaXi@ILnUplvz@Ac``agKI4OAkl@0$jyHC6Tm}8^z&8=!g?+TE!wxYT z(4c8INT|exN+C|XfuulHQW|hOAa(h4`Q54!esRU2A3ns~#(MuG1U!3^K`8sJshO7VLE=Xb+7KB{1WcUQhqVYwG%1nc2FZ75-GQjULB>} zezWdL;+qP8;QWWsZ2^XW5;JOhgG#PWU+(VKED{Wcr8&y&vF<|ZN)@^cK{Ohu6`Tx0 zs|iMM6C6_}AwMR><$|_v0YG;_LnS${5ngyAO}RbBWz1D>@9EAVu!1ClU_}!E3vu5B zJefC8uP}{dm?2^k6L}4m*oHiy3nArK{4Gc*M3zJ-9+3dG1_{^n7?e%W79i{Toe6vv zRT4Ez2nI$eVD!>q2v?6#cd#cBM65^@p}T#d^Cmh+f_R^dUnmLqooPvFdl(WUN`}yf zmV)SmmZhU_Dd8bziJ%Ba=;+`E^aWk`boBjVbmyUhAOmgdU#HQi9U==Y2KC2aZYey5 zLz&9Uq+<~Zg;v?t3;hy4-zBE7_sGwhZl=@@(f4)_77!4XsD%p%7>>VpyEth0IFp0FAiUOdpsqpAHlgDB3ix#ANRm=h-h(uCD82w`n0gnScF1O z%Uo+YLr6yN;{YI8S6SifVgm+sC!2JtPLxbZ!m9-tiQ4cO0l3T(Cm5KSt_|znNf;yf zX_3YqWLLz#1`)gz!9D4aC>iXlJq1;Qp2+0V`{83ilkZN$eH(HN8}YXie}BTCA*|~^ z5rmE%W?)iSn8dEjqA~`F0c7yZ0`j8ncML1d`O*-tD31@a`j{;`$;Qvuzk(ksS-*ti z#&^&;8(%TI*gT=sWa2d<@TYYjxuv%lE^+s@yh4{ zz6V#Ro2ZVjdN*cZkutuIVV<0OM4Y9II~jDQL|7Djs9HC_mrq)4??>zpmMr$q(k-OKq@h~ zjM%o_l6qCi>`mc#x6zqBue6*bt;;LP z2LdAtsJIY*fzhz=j3-o~fDm>`=-mWN&{ZBW4Q0aROm7w*eLxwOQdyF=PMo--`9jgn znfi{N3b!dxpg^o{D-=?Y@r+KU!F#IYa))kai@U$2C13%WWL;8QoCYFHor4lf5XES1 zf_)$yZV`qYOmC*mm*>G9IWpj^7v5!BIrdcF(_}foiDSDrKaxodk(E4>&H8G%T^Web)Rk@ZW#LcrY4_Yg{t8mZ^VkPu z;24x~M)yts@0GPk)Do>4)TfCFfv0$CKXWHg!@cCR4Z?^D(M@dhhK?L%946%NM2toA6fZGSN?3L~-WO?+pKd4sK0X&GN)5n7Jc-l#(T#8hM-utsYQu1_ z&{hKi0Hj=U^@NSGjojN{SQE!}8Dt14w3OwD%4HrB4bKABvUtoY^sF8k4VjoRTsc!3 z#4{Dua|IJ*btm8m%{`eK@}MD6 zzVv8t5`hh4s;!LJN$hT1UI;>7;BZDhCL$}qjxs#H;j24T&1g2t@JB(K_ zk66uS6#GPf3+zde+%(O50V!t%f5+n~yyZHFFf$P?z$?)otS&Hn6@Ha*mSI zl>Hd`@)!ijIZ9qL3eivt28AEn08f|w{O*k`kws*0%?2swxC_^aPh_ zG}{$H*#N@NZ(#lTDk}Sr@Azhvm7cqY@88HK^>`5KbLh78vVrCbHloDja=qxUCZsB~sZA ztq?Cf1zmlr51D{>f`AhjGUVu9Kh`hc@^To@KAqJ%Fvj*U+y_GD+)iNKXWe7dzCO-R z``J6VR=sgE%gCo*(zsBDURiQX#t%j*p6(UTQy@*eM&w-^TE%s$3Jn8@iV#~&P z5fH4K?vMs1ibL$pkifj++?t@92qNjRxH|iXXIWxzI>}nrpAyKWleX_3N0jya<7ZjF z;R2a*E$IxzqHU0pv15?a-3ZYE*Mn5=B=7MDHaK=EZl7IeZjSwm=lp>UA8`a{EJ}m$ zb6A9)0LfyISx4mD=JurE{ZeOC+oj+5lYd}&w>>)zsvP;2ob4ouJgAjIBrO*@FO<^a zVlaQTH-tQF`0~3AG~xnod!F^?L$@;Xs2?waAJK+wr2QexMK2RcZ^HYUUe`&v6bKX- z2i_&@BdYLwwz4^FJwLFO^^KW{>quC2i}g{R-O2{V{=s#_sK$Ruqw2+PdXDvszA&?E zXnvmm92-0NjZS@DhG6RczvktE%h$~dZjpy(<=}HHc>v9dyrv1qs@Khoyyh(=(Cqy7 z92+>MqzH|iRK>&J|9{Zb7dPgd&$9(dbAJYa6GTT(02?7>;|p()RNnqP>z7Xb0OO!7 z8Hp7KY0c5o3)f!Qew4KJsrTs}ZQx^HV8aKj0v_8VkE{{&;|y!>iAut@3QM*29Dn2m zHbnYz3V-1R*1OlsbTeZD6(|sG(PjCT7uanc@%ZX0T%u6V*9KfhH9xvtQsDrFyYZ$n zyoVTvb=(}0jP>R{K(ncByZdTj(DN&3!ELC-Z$Q~lMIXlu_@%F*C8XaF(4LIP+=D?( zgKa@Pa@x8TzXS~R6W0idIi>#mUF~k0#^;<%IJnIgL z`xW{Mtv*NkMAbe*6t+I-#|d)H8?ZqGVDP@XBdH4Ws;)HQf@C5z$!BPSfC$pWMOd!i z*pdzW93<2eQ+>wwVBqIp;9WgNZSu{u+&%`q^%bzj*6^@qw2%&yMRL{&QWe3J(&Zvj zS5K)X`Cwi|?hAodxYHSy6q;S`?Ar|)DQer6#vCFo#ctLY#L4JOhs zfrnInekV)1&5}dY>L?8d7)Z3{wJ9ROT$Qe82q}b)WkhaOoSquVb6;kIObMab123}) z9*H0WR+Sma>SX|;1xI=wb`OU-V^?Yr@sw8VJF0Eaj?x{cIg(cZy$+1uK56) zH2-D6DU&M1*BrAzkH;+0<51}wNo#enDn|d>pCO$c*e5+F7>@sA>q%1+w*==^XI zzg@)R5I2eV2*j(&&JAOO>;Wekj<)cRO0=M(BK@J>%67^(w8u1y1YqePotJuv6JzNm zRyPuAv`oCjkCmDLA2V#MH_2(wQ?+HuL7ck#pe14GxhB~M?LNWe(T6LphCfSCRhEJiN>MiSi1SF+ju}_c-liF0-$m`VJ-;9Fw|{ zK!dyHDjUV_tZWo}wM{#=U&s&wk=4zD(V-!=Ls%PujjDsS1Y53o2m6b1P*j#{cOlXR zbE!=m2}$JIMIqRjOSP6}&>+~k(ri|aq(gvZ&K`Pia+|Zobslu5(xyXE8qJzJU-V=N z8Jx{7k7cHu@-aei-P51uM7M)ml2sR^#KS5u*`L?uGEQFCGnv`(rkX|w6S}}U00eM74M||~TKFrAgK&JEAb?kBU)oU|y5~itv6eB7dVd^&I~a^B6ld*|%>2(2Rnhe?pijObE+Mz|R8~ z!NBpRfS0yXungZ@`m|_8?+KPNhGvLf9ENiTmX>ySUr=UazLhI{ZxL|R3fK~93#>ju zmD{q3d;Jp7kN!yI(U1OUad+{tT(nM7Mm=%CEcEL`8;QdrHCcPDUa%*u7Tj?J>oVK4 z?QiN0`?%nn9qMIbaBo%dr~b$saDD&xA6Y;47vAkvIKuDeBVWZUvbp^3SK)-%^9|JO z5WYkh$_>YaqB+b5$j74jb_;V$=!nmv?GOnd8c>pcEGV98z{#HGMb@?b932E?oMLNt z{x^)w-7use6F`C@UyEoi4)RqdjeySPgmQ;%zt;v`wO7vedSu=e1-QC0`kU7 z*c$+9re!>BE&^yKJG(h%?xxm%@#O_RfRa>qJ2_Nl|f{9kIUmh{`YQ^u0}Aq;T`= zY((ZRMDV;x%?p$%n^Q2|L>0nM0F#}Ja1wDH!P?AA!^H|U56!H9=yhmtDFlm9w!pTN zaIjOmghl~e?KoIP@-?L)v1^Ebj=K#;Hu5-iN(ZMjQ;H*nHeN(T?ol>`gXB z^6lnR-^62}!`|Qzyvfo%$3}#%s)U~Uv#bj}O{J-VQr4~^F+?07>$gS5-cGz>_64~x(A)JVOSHF-;(P#aogmelpQ`;chr9OT zLX{ui{MbP9CFun8U~IWBu)o*QI7^&*tdu zC5S^zG`5ePCV?@wMcWcA=$)LN@!65du?BU@7;V5ny2IMwE7}7q{T{7!0M4B7M2r?O zkni5de(QQHaVvlOJ+@K$qJ>X>pWQFr-%|hj`)m}GcY`o|&zsN&M2(>r`9JrwM{?gf zg3eFN%=k>3)L-m^Ruf1&A6zlyX>AX>dZZF{>$%A=Bwv@!cYVMHg%w`9%8!4*GNXy@ zDCY{$h;b`7A7Ev{o|$pE-hF@#ilUBC?nVB>0oHG58ICH{snmrZ`|4fj1o*rgyTJc? zfc1{jg&OpT{{}!|KVQ7c2OVT1LPL+)#%CU6Wn!f2)G_)&JC30a9t0&`fM4zY1gy#~^2;Xjd;h{!KDU95O8(nmER`O3TGd+#;?ydsQV+cFp+|pr zh`-vvrbOM-M;s+4^x;<<@Ss}LPkdG*9#ngxH?PI_t~(|78YXtoA-h5^gt4yFx z0Czcb{vbTAO|;6ClGmgg`W^<)(9ra5Nvqg+;=AVBU!Z+P>hS{q;{vyBQ2ulFI zOOLQQQ}6u%!| z@j3!PCvAJW8COADdGBU+XSa9HVat*n+G}Qhe=}PcxfhS!YSsq+Wiz`u_Y&??(Zh({ zO0k^Qq9ob6$!twLMvF876(_WE=;7)i?_ATk=l5ho6W;I`WXbo4m+d3&$qQPT&2tYR zshtj(%pW9vV4{D-?NVtVu=WMA#y3YUz^g^x7f2~@pFUk1(^Y!1GcH)V<+{?Rs==p6 zOxL2j%Jp|f1j}u^uH0_Cor*61s`5TZ;cfrY&7n@b-PMURd3aRPvFip^y(QYC(d^ZsxcD z^L;ZS2IVfl-{F4Wz2}~L?z#7#@4n_7Frvz&*$Jt+&X9;`987$}t|ZBCz|F$$rov$H z=HPG2 z>JC|7hkfkk@2{gqbRr^ZqWO@on7_#l^z(GpZcEW^#QV`|6Z_>cc&19v)92~Yxz}NL zjykthv4-BfoM@2cDKW#2-Wv{W6lTS9WU~vH#q$iy&PfR>r}C zc{q*C{fJ3_ltiqZijTaBa#5up{Ys^i+lt!2%qbBo_QHSRW=HlM2gY`tv7^nxsInGs zAcXUSxc`3kkmkcd+_<0Jt@{u=gbGW^?$q0@mF0vr*o&L?!@)rVuiB40636+){cM6} z!BC!Z05kG59Ax1pEv$+bbX2?2sE@0pL*F?M;ApFuJmc9?oPuGh-qi+6F0xOu<;(lx z6^vO8dAM2hXB6?2P1}iJsdj$6;0N2nQY3W7+A4`-blU{nU>2`Lp=-f!;j}2|^6cz9 zT~^6zQWT_{dm)pDS{C#h4Ec{nYynK_BqLL{O{o*;By#;hoB_w2j9YyPIp+kyb}5w5 zUoObql1N=7;^X(ZtJr(1TV1Fu2Xoo%L#^Okx{|RPLA)GRc8qsNSW{} zNoTPZdwvsCe=y{WMzvOO$)8#RbDV-!u{Dz4U(13tUzG6?wK$HP9myxwvK5-s5nQfi zfx!(M>^iuh)slJ%o!VNgd{IA+;17O{^bPj(Olz?jjEkml)O@;^I33O>e9hkJ_2>X- zd*g&maVVU79fE048qP~8_F5UwrP!VQDRzt+8~$mTS~!SLBI2PS zPRi0RcM=9xZXGY+s!#F3NAVHOJyo&j zC|koctKIm}V=RgNi5DJYKVv&#dL64`u{_~88-8!&3B?Fiiq5&DpdCk089MiEA+p#E}$%E_J)Re!{V#hl} znSY4!oi8q8Z0CzcjG`ACH3KFJ!|-BGAb)yNfw#>@-%p!|ICZq5uAWsgR#d@Hvr-nl z7dIXAd_Jd@p{*5nc40B9?VmKCIr=IkU@D>5vQkdpyvz9$jVwy@!3_RdBTGhI_cgKw ztd?hf2QM~-Gj6A}Or=zWt7^AX+QQ+TGD??KNJ(Eb*h~Kj3)s|U;^UdT`8$^A@hNyi z`>vnKho50vd#!B8LICT?dQ9?eoMAyuGcb<~KFcy5*^8h0+ZK@?z_c&`Xt+c4Hg|m0fPi$tU-$41f8N|wY$OSe$Ib1!ZwIBJ}ydEge3yim%70RHy@t9Uh+f-+{GWj?|enleE!A-*4Hm|KBaUZF>hox(uLoA zzUKl<3EFLizzWX7#z~F-a6`%Cv2&~7I9mi;`Jjtzq2|@zeCtJ6)*1IY*Ir^Ho$8%O z@w7`UH0b%>kSjjq9hGLZL;5&t!X$*E)|W551YbWsz7=D>heL#$;UEo)|LjIqYub;BlHx z4v>%?n?4z7#&L3hGhPs6ATW-jV_Z347M_LsHCYyiF*4$2yW3`a&2h1rUUtR``l@BN zyN2HIv5%2O=y%YtVbuMIWa8Ybn@gdHwMkWT<;oR!GHR2qiEXfOj5YB$z(^5gS8y1G ztMG7apYMs=>WSj02gH|q-UXGqo-PNpqWC;dJRBiDrr~Z9{Kv{At4snDVEZ~-Dk)OT zvKr-pv%o@^zX!FRh1U+e+8=B2*nEIt@Jyr1M1m437>-+3#hVeU_^wip`=bs(Tso!K zV_vfr1VyyJUfVIlvugVkvH;#n(-XkcSbHJdAQ==59uR5!+prPd2*NWFVC+qej2T zk}X?8%@E^VvD&#Un3s!mTq+ai+n~5{_fGAyC}p4=U<7?C50`Ia(T5uFn$DXpvk<=$ zHyYjUFbxaOc1EkKc;C!}+gOY)*`DyiK~JkI^sX;1#np(X;jyQU>G7Q{-MR?;5;J_|IGkD1;i$&u!TXMcbZ zX-2bVNt!0IV(~TRG{sbpYa``B2B{hl+Q%UE#f_9mxG)<5py!bYM*%3LcSqc1ynouy zAf+R}rGHl%YZ45S>pLoqiibLBq#`~qz6%YLV7s1$P(T%0yotM(s#ZjgnMB6yPAeG_b&K(iyfksr6D%oG7VY61;ydWh`blDBk2Flu zml1bH4-{p+IDnvGi7 zJt3EvJCjf<@JK?B?EfX5o}^^zW=wVw)3!1aQ>T<@|Dd~3@DA{9ACgULRJ|~1sj6Fu z2^s;<&o@XkB)?c;Fe%FA?H5(WupHq_lMT`sn>r+St~N;P@qPgBnXopneTG3=_=-V# z7w{&UF4ba?CIL2LHI!Ctkg~8fG9IuT(34*1It;@)wNrN*ZtY-2r2OC{&1a_Rab4A1 zS4zK5Xy)&`AJ09tOE=B(OI3OGU!CnjvVV5nFWcrz;N0FW-SyU$Lw~E2*;VDQ3}$KT zZxT_}t%Jrm89KZH$^}5fY3X9U6#lw8v|Jb6HndE)e4*}WY@w$)3R~s0Tpd>0nY#^=afx2~5gbM@!M4I=%@C*2jsM{_lQsSLg~^(aT;9eq zZv$UGMbr10K+K#%0D8b^z!ZQP@EV{Tum{izkRFz#KtL?O1TX{M0Bi^B0Z56U0~iL- z14aYPiBbr^F-6l~zoI~zH{$H?=6D|Kw*~Q)z~6=tAZ!lr6Ydi)oF*E=%+ delta 32718 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 diff --git a/src/animation.h b/src/animation.h index 19a53cc..488d543 100644 --- a/src/animation.h +++ b/src/animation.h @@ -145,7 +145,7 @@ struct Animation { TR::AnimRange &range = level->ranges[s.rangesOffset + j]; if (anim->frameStart + frameIndex >= range.low && anim->frameStart + frameIndex <= range.high) { setAnim(range.nextAnimation - model->animation, range.nextFrame); - break; + return true; } } } diff --git a/src/camera.h b/src/camera.h index c970abe..4ccf987 100644 --- a/src/camera.h +++ b/src/camera.h @@ -13,28 +13,21 @@ struct Camera : Controller { Frustum *frustum; float fov, znear, zfar; - vec3 target, destPos, lastDest, angleAdv; + vec3 target, destPos, lastDest, advAngle; + float advTimer; mat4 mViewInv; int room; float timer; int actTargetEntity, actCamera; bool cutscene; + bool firstPerson; + Basis prevBasis; vec4 *reflectPlane; Camera(IGame *game, Lara *owner) : Controller(game, owner ? owner->entity : 0), owner(owner), frustum(new Frustum()), timer(0.0f), actTargetEntity(-1), actCamera(-1), reflectPlane(NULL) { - fov = 65.0f; - znear = 16; - zfar = 40.0f * 1024.0f; - angleAdv = vec3(0.0f); - - if (owner) { - room = owner->getEntity().room; - pos = pos - owner->getDir() * 1024.0f; - target = owner->getViewPoint(); - } - + changeView(false); cutscene = owner->getEntity().type != TR::Entity::LARA && level->cameraFrames; } @@ -46,6 +39,30 @@ struct Camera : Controller { return actCamera > -1 ? level->cameras[actCamera].room : room; } + virtual void checkRoom() { + TR::Level::FloorInfo info; + level->getFloorInfo(room, (int)pos.x, (int)pos.y, (int)pos.z, info); + + if (info.roomNext != TR::NO_ROOM) + room = info.roomNext; + + if (pos.y < info.roomCeiling) { + if (info.roomAbove != TR::NO_ROOM) + room = info.roomAbove; + else + if (info.roomCeiling != 0xffff8100) + pos.y = (float)info.roomCeiling; + } + + if (pos.y > info.roomFloor) { + if (info.roomBelow != TR::NO_ROOM) + room = info.roomBelow; + else + if (info.roomFloor != 0xffff8100) + pos.y = (float)info.roomFloor; + } + } + virtual bool activate(ActionCommand *cmd) { Controller::activate(cmd); this->timer = max(max(1.0f, this->timer), cmd->timer); @@ -88,18 +105,43 @@ struct Camera : Controller { } else #endif { + vec3 advAngleOld = advAngle; + if (Input::down[ikMouseL]) { vec2 delta = Input::mouse.pos - Input::mouse.start.L; - angleAdv.x -= delta.y * 0.01f; - angleAdv.y += delta.x * 0.01f; + advAngle.x -= delta.y * 0.01f; + advAngle.y += delta.x * 0.01f; Input::mouse.start.L = Input::mouse.pos; } - angleAdv.x -= Input::joy.R.y * 2.0f * Core::deltaTime; - angleAdv.y += Input::joy.R.x * 2.0f * Core::deltaTime; - - angle = owner->angle + angleAdv; - angle.z = 0.0f; + advAngle.x -= Input::joy.R.y * 2.0f * Core::deltaTime; + advAngle.y += Input::joy.R.x * 2.0f * Core::deltaTime; + + if (advAngleOld == advAngle) { + if (advTimer > 0.0f) { + advTimer -= Core::deltaTime; + if (advTimer <= 0.0f) + advTimer = 0.0f; + } + } else + advTimer = -1.0f; + + if (owner->velocity != 0.0f && advTimer < 0.0f && !Input::down[ikMouseL]) + advTimer = -advTimer; + + if (advTimer == 0.0f && advAngle != 0.0f) { + float t = 10.0f * Core::deltaTime; + advAngle.x = lerp(clampAngle(advAngle.x), 0.0f, t); + advAngle.y = lerp(clampAngle(advAngle.y), 0.0f, t); + } + + angle = owner->angle + advAngle; + angle.z = 0.0f; + + if (owner->stand == Lara::STAND_ONWATER) + angle.x -= 22.0f * DEG2RAD; + if (owner->state == Lara::STATE_HANG || owner->state == Lara::STATE_HANG_LEFT || owner->state == Lara::STATE_HANG_RIGHT) + angle.x -= 60.0f * DEG2RAD; #ifdef LEVEL_EDITOR angle = angleAdv; @@ -124,7 +166,6 @@ struct Camera : Controller { return; #endif - int lookAt = -1; if (actTargetEntity > -1) lookAt = actTargetEntity; if (owner->target > -1) lookAt = owner->target; @@ -142,6 +183,25 @@ struct Camera : Controller { } } + if (firstPerson && actCamera == -1) { + Basis head = owner->animation.getJoints(owner->getMatrix(), 14, true); + Basis eye(quat(0.0f, 0.0f, 0.0f, 1.0f), vec3(0.0f, -40.0f, 10.0f)); + eye = head * eye; + mViewInv.identity(); + + //prevBasis = prevBasis.lerp(eye, 15.0f * Core::deltaTime); + + mViewInv.setRot(eye.rot); + mViewInv.setPos(eye.pos); + mViewInv.rotateY(advAngle.y); + mViewInv.rotateX(advAngle.x + PI); + + pos = mViewInv.getPos(); + checkRoom(); + Sound::listener.matrix = mViewInv; + return; + } + float lerpFactor = (lookAt == -1) ? 6.0f : 10.0f; vec3 dir; target = target.lerp(owner->getViewPoint(), lerpFactor * Core::deltaTime); @@ -170,34 +230,13 @@ struct Camera : Controller { } else { vec3 eye = lastDest + dir.cross(vec3(0, 1, 0)).normal() * 2048.0f - vec3(0.0f, 512.0f, 0.0f); destPos = trace(owner->getRoomIndex(), target, eye, destRoom, true); - } + } room = destRoom; } pos = pos.lerp(destPos, Core::deltaTime * lerpFactor); - if (actCamera <= -1) { - TR::Level::FloorInfo info; - level->getFloorInfo(room, (int)pos.x, (int)pos.y, (int)pos.z, info); - - if (info.roomNext != 255) - room = info.roomNext; - - if (pos.y < info.roomCeiling) { - if (info.roomAbove != 255) - room = info.roomAbove; - else - if (info.roomCeiling != 0xffff8100) - pos.y = (float)info.roomCeiling; - } - - if (pos.y > info.roomFloor) { - if (info.roomBelow != 255) - room = info.roomBelow; - else - if (info.roomFloor != 0xffff8100) - pos.y = (float)info.roomFloor; - } - } + if (actCamera <= -1) + checkRoom(); } mViewInv = mat4(pos, target, vec3(0, -1, 0)); @@ -226,6 +265,20 @@ struct Camera : Controller { frustum->pos = Core::viewPos; frustum->calcPlanes(Core::mViewProj); } + + void changeView(bool firstPerson) { + this->firstPerson = firstPerson; + + room = owner->getRoomIndex(); + pos = owner->pos - owner->getDir() * 1024.0f; + target = owner->getViewPoint(); + advAngle = vec3(0.0f); + advTimer = 0.0f; + + fov = firstPerson ? 90.0f : 65.0f; + znear = firstPerson ? 8.0f : 16.0f; + zfar = 40.0f * 1024.0f; + } }; #endif \ No newline at end of file diff --git a/src/character.h b/src/character.h index 1b6eef2..1080f46 100644 --- a/src/character.h +++ b/src/character.h @@ -29,12 +29,24 @@ struct Character : Controller { vec3 velocity; float angleExt; + float speed; + + Collision collision; Character(IGame *game, int entity, int health) : Controller(game, entity), target(-1), health(health), tilt(0.0f), stand(STAND_GROUND), lastInput(0), velocity(0.0f) { animation.initOverrides(); rotHead = rotChest = quat(0, 0, 0, 1); } + void rotateY(float delta) { + angle.y += delta; + velocity = velocity.rotateY(-delta); + } + + void rotateX(float delta) { + angle.x = clamp(angle.x + delta, -PI * 0.49f, PI * 0.49f); + } + virtual void hit(int damage, Controller *enemy = NULL) { health -= damage; }; @@ -44,13 +56,13 @@ struct Character : Controller { TR::Entity &e = getEntity(); level->getFloorInfo(e.room, e.x, e.y, e.z, info); - if (info.roomNext != 0xFF) + if (info.roomNext != TR::NO_ROOM) e.room = info.roomNext; - if (info.roomBelow != 0xFF && e.y > info.roomFloor) + if (info.roomBelow != TR::NO_ROOM && e.y > info.roomFloor) e.room = info.roomBelow; - if (info.roomAbove != 0xFF && e.y <= info.roomCeiling) { + if (info.roomAbove != TR::NO_ROOM && e.y <= info.roomCeiling) { if (stand == STAND_UNDERWATER && !level->rooms[info.roomAbove].flags.water) { stand = STAND_ONWATER; velocity.y = 0; @@ -106,18 +118,13 @@ struct Character : Controller { virtual void updateTilt(bool active, float tiltSpeed, float tiltMax) { // calculate turning tilt if (active && (input & (LEFT | RIGHT)) && (tilt == 0.0f || (tilt < 0.0f && (input & LEFT)) || (tilt > 0.0f && (input & RIGHT)))) { - if (input & LEFT) tilt -= tiltSpeed * Core::deltaTime; - if (input & RIGHT) tilt += tiltSpeed * Core::deltaTime; - } else - if (fabsf(tilt) > 0.01f) { - if (tilt > 0.0f) - tilt -= min(tilt, tiltSpeed * 4.0f * Core::deltaTime); - else - tilt -= max(tilt, -tiltSpeed * 4.0f * Core::deltaTime); - } else - tilt = 0.0f; - tilt = clamp(tilt, -tiltMax, tiltMax); - + if (input & LEFT) tilt -= tiltSpeed; + if (input & RIGHT) tilt += tiltSpeed; + tilt = clamp(tilt, -tiltMax, +tiltMax); + } else { + if (tilt > 0.0f) tilt = max(0.0f, tilt - tiltSpeed); + if (tilt < 0.0f) tilt = min(0.0f, tilt + tiltSpeed); + } angle.z = tilt; } diff --git a/src/collision.h b/src/collision.h new file mode 100644 index 0000000..1be6d49 --- /dev/null +++ b/src/collision.h @@ -0,0 +1,104 @@ +#ifndef H_COLLISION +#define H_COLLISION + +#include "core.h" +#include "utils.h" +#include "format.h" + +struct Collision { + enum Side { NONE, LEFT, RIGHT, FRONT, TOP, BOTTOM } side; + + struct Info { + int room, roomAbove, roomBelow, floor, ceiling; + } info[4]; + + Collision() : side(NONE) {} + + Collision(TR::Level *level, int room, vec3 &pos, const vec3 &offset, const vec3 &velocity, float radius, float angle, int minHeight, int maxHeight, int maxAscent, int maxDescent) { + if (velocity.x > 0.0f || velocity.z > 0.0f) + angle = normalizeAngle(PI2 + vec2(velocity.z, velocity.x).angle()); + pos += velocity; + + int q = angleQuadrant(angle); + + const vec2 v[] = { + { -radius, radius }, + { radius, radius }, + { radius, -radius }, + { -radius, -radius }, + }; + + const vec2 &l = v[q], &r = v[(q + 1) % 4]; + + vec2 f = (q %= 2) ? vec2(l.x, radius * cosf(angle)) : vec2(radius * sinf(angle), l.y), + p(pos.x, pos.z), + d(0.0F); + + vec3 hpos = pos + offset; + + int height = maxHeight - minHeight; + + if (checkHeight(level, room, hpos, vec2(0.0f), height, 0xFFFFFF, 0xFFFFFF, side = NONE)) { + pos -= velocity; + side = FRONT; + return; + } + + if (info[NONE].ceiling > hpos.y - maxHeight) { + pos.y = info[NONE].ceiling + maxHeight - offset.y; + side = TOP; + } + + if (info[NONE].floor < hpos.y + minHeight) { + pos.y = info[NONE].floor - minHeight - offset.y; + side = BOTTOM; + } + + if (checkHeight(level, room, hpos, f, height, maxAscent, maxDescent, FRONT)) { + d = vec2(-velocity.x, -velocity.z); + q ^= 1; + d[q] = getOffset(p[q] + f[q], p[q]); + } else if (checkHeight(level, room, hpos, l, height, maxAscent, maxDescent, LEFT)) { + d[q] = getOffset(p[q] + l[q], p[q] + f[q]); + } else if (checkHeight(level, room, hpos, r, height, maxAscent, maxDescent, RIGHT)) { + d[q] = getOffset(p[q] + r[q], p[q] + f[q]); + } else + return; + + pos += vec3(d.x, 0.0f, d.y); + } + + inline bool checkHeight(TR::Level *level, int room, const vec3 &pos, const vec2 &offset, int height, int maxAscent, int maxDescent, Side side) { + TR::Level::FloorInfo info; + int py = int(pos.y); + level->getFloorInfo(room, int(pos.x + offset.x), py, int(pos.z + offset.y), info); + + Info &inf = this->info[side]; + inf.room = info.roomNext != TR::NO_ROOM ? info.roomNext : room; + inf.roomAbove = info.roomAbove; + inf.roomBelow = info.roomBelow; + inf.floor = info.floor; + inf.ceiling = info.ceiling; + + if ((info.ceiling == info.floor) || (info.floor - info.ceiling < height) || (py - info.floor > maxAscent) || (info.floor - py > maxDescent) || (info.ceiling > py)) { + this->side = side; + return true; + } + return false; + } + + inline float getOffset(float from, float to) { + int a = int(from) / 1024; + int b = int(to) / 1024; + + from -= float(a * 1024.0f); + + if (b == a) + return 0.0f; + else if (b > a) + return -from + 1025.0f; + return -from - 1.0f; + } +}; + +#endif \ No newline at end of file diff --git a/src/controller.h b/src/controller.h index ba1f5e2..1ead1eb 100644 --- a/src/controller.h +++ b/src/controller.h @@ -5,6 +5,7 @@ #include "frustum.h" #include "mesh.h" #include "animation.h" +#include "collision.h" #define GRAVITY (6.0f * 30.0f) #define NO_OVERLAP 0x7FFFFFFF @@ -119,13 +120,14 @@ struct Controller { return false; } + + void updateEntity() { TR::Entity &e = getEntity(); e.x = int(pos.x); e.y = int(pos.y); e.z = int(pos.z); - while (angle.y < 0.0f) angle.y += 2 * PI; - while (angle.y > 2 * PI) angle.y -= 2 * PI; + angle.y = normalizeAngle(angle.y); e.rotation = angle.y; } @@ -220,26 +222,18 @@ struct Controller { } void alignToWall(float offset = 0.0f) { - float fx = pos.x / 1024.0f; - float fz = pos.z / 1024.0f; - fx -= (int)fx; - fz -= (int)fz; + int q = angleQuadrant(angle.y); + int x = int(pos.x) & ~1023; + int z = int(pos.z) & ~1023; - int k; - if (fx > 1.0f - fz) - k = fx < fz ? 0 : 1; - else - k = fx < fz ? 3 : 2; - - angle.y = k * PI * 0.5f; // clamp angle to n*PI/2 - - if (offset != 0.0f) { - vec3 dir = getDir() * (512.0f - offset); - if (k % 2) - pos.x = int(pos.x / 1024.0f) * 1024.0f + 512.0f + dir.x; - else - pos.z = int(pos.z / 1024.0f) * 1024.0f + 512.0f + dir.z; + switch (q) { + case 0 : pos.z = z + 1024 + offset; break; + case 1 : pos.x = x + 1024 + offset; break; + case 2 : pos.z = z - offset; break; + case 3 : pos.x = x - offset; break; } + + angle.y = q * (PI * 0.5f); updateEntity(); } @@ -264,7 +258,7 @@ struct Controller { if (lr != room || lx != sx || lz != sz) { level->getFloorInfo(room, sx, py, sz, info); - if (info.roomNext != 0xFF) { + if (info.roomNext != TR::NO_ROOM) { room = info.roomNext; level->getFloorInfo(room, sx, py, sz, info); } @@ -274,9 +268,9 @@ struct Controller { } if (isCamera) { - if (py > info.floor && info.roomBelow != 0xFF) + if (py > info.floor && info.roomBelow != TR::NO_ROOM) room = info.roomBelow; - else if (py < info.ceiling && info.roomAbove != 0xFF) + else if (py < info.ceiling && info.roomAbove != TR::NO_ROOM) room = info.roomAbove; else if (py > info.floor || py < info.ceiling) { int minX = px / 1024 * 1024; @@ -289,14 +283,14 @@ struct Controller { } } else { if (py > info.floor) { - if (info.roomBelow != 0xFF) + if (info.roomBelow != TR::NO_ROOM) room = info.roomBelow; else break; } if (py < info.ceiling) { - if (info.roomAbove != 0xFF) + if (info.roomAbove != TR::NO_ROOM) room = info.roomAbove; else break; diff --git a/src/enemy.h b/src/enemy.h index 93db718..4521e80 100644 --- a/src/enemy.h +++ b/src/enemy.h @@ -179,7 +179,7 @@ struct Wolf : Enemy { return (state == STATE_STOP || state == STATE_SLEEP) ? STATE_SLEEP : STATE_STOP; switch (state) { - case STATE_SLEEP : return target > -1 ? STATE_STOP : state; + case STATE_SLEEP : return (target > -1 && level->entities[target].room == getRoomIndex()) ? STATE_STOP : state; case STATE_STOP : return target > -1 ? STATE_HOWL : STATE_SLEEP; case STATE_HOWL : return state; case STATE_GROWL : return target > -1 ? (randf() > 0.5f ? STATE_STALKING : STATE_RUN) : STATE_STOP; diff --git a/src/format.h b/src/format.h index d012d20..a754a07 100644 --- a/src/format.h +++ b/src/format.h @@ -1551,8 +1551,8 @@ namespace TR { int sx = x - room.info.x; int sz = z - room.info.z; - sx = clamp(sx, 0, (room.xSectors - 1) * 1024); - sz = clamp(sz, 0, (room.zSectors - 1) * 1024); + sx = clamp(sx, 0, room.xSectors * 1024 - 1); + sz = clamp(sz, 0, room.zSectors * 1024 - 1); dx = sx % 1024; dz = sz % 1024; @@ -1584,13 +1584,13 @@ namespace TR { return; Room::Sector *sBelow = &s; - while (sBelow->roomBelow != 0xFF) sBelow = &getSector(sBelow->roomBelow, x, z, dx, dz); + while (sBelow->roomBelow != TR::NO_ROOM) sBelow = &getSector(sBelow->roomBelow, x, z, dx, dz); info.floor = 256 * sBelow->floor; parseFloorData(info, sBelow->floorIndex, dx, dz); - if (info.roomNext == 0xFF) { + if (info.roomNext == TR::NO_ROOM) { Room::Sector *sAbove = &s; - while (sAbove->roomAbove != 0xFF) sAbove = &getSector(sAbove->roomAbove, x, z, dx, dz); + while (sAbove->roomAbove != TR::NO_ROOM) sAbove = &getSector(sAbove->roomAbove, x, z, dx, dz); if (sAbove != sBelow) { info.ceiling = 256 * sAbove->ceiling; parseFloorData(info, sAbove->floorIndex, dx, dz); diff --git a/src/game.h b/src/game.h index 75cd8b2..4ab309b 100644 --- a/src/game.h +++ b/src/game.h @@ -39,13 +39,21 @@ namespace Game { void update() { float dt = Core::deltaTime; + if (Input::down[ikV]) { // third <-> first person view + level->camera->changeView(!level->camera->firstPerson); + Input::down[ikV] = false; + } + if (Input::down[ikR]) // slow motion (for animation debugging) Core::deltaTime /= 10.0f; - if (Input::down[ikT]) - Core::deltaTime *= 10.0f; + + if (Input::down[ikT]) // fast motion + for (int i = 0; i < 9; i++) + level->update(); level->update(); + Core::deltaTime = dt; } diff --git a/src/lara.h b/src/lara.h index 1dc696d..ef04abb 100644 --- a/src/lara.h +++ b/src/lara.h @@ -12,16 +12,25 @@ #define TURN_FAST_BACK PI * 3.0f / 4.0f #define TURN_NORMAL PI / 2.0f #define TURN_SLOW PI / 3.0f -#define TURN_WATER_FAST PI * 3.0f / 4.0f -#define TURN_WATER_SLOW PI * 2.0f / 3.0f +#define TURN_WATER_FAST (DEG2RAD * 150.0f) +#define TURN_WATER_SLOW (DEG2RAD * 60.0f) +#define TURN_WALL_Y (DEG2RAD * 150.0f) +#define TURN_WALL_X (DEG2RAD * 60.0f) +#define TURN_WALL_X_CLAMP (DEG2RAD * 35.0f) -#define TILT_MAX (PI / 18.0f) -#define TILT_SPEED TILT_MAX +#define LARA_TILT_SPEED (DEG2RAD * 37.5f) +#define LARA_TILT_MAX (DEG2RAD * 10.0f) -#define GLIDE_SPEED 35.0f -#define SWIM_SPEED 45.0f +#define LARA_HANG_OFFSET 724 +#define LARA_HEIGHT 762 +#define LARA_HEIGHT_WATER 400 +#define LARA_RADIUS 100.0f +#define LARA_RADIUS_WATER 300.0f -#define LARA_HANG_OFFSET 724.0f +#define LARA_WATER_ACCEL 2.0f +#define LARA_SURF_SPEED 15.0f +#define LARA_SWIM_SPEED 50.0f +#define LARA_SWIM_FRICTION 1.0f #define LARA_WET_SPECULAR 0.5f #define LARA_WET_TIMER (LARA_WET_SPECULAR / 16.0f) // 4 sec @@ -232,8 +241,8 @@ struct Lara : Character { for (int i = 0; i < jointsCount - 1; i++) { TR::Node &t = node[min(i, model->mCount - 2)]; joints[i].posPrev = joints[i].pos = basis.pos; - joints[i].length = t.z; - basis.translate(vec3(0.0f, 0.0f, -t.z)); + joints[i].length = float(t.z); + basis.translate(vec3(0.0f, 0.0f, -joints[i].length)); } joints[jointsCount - 1].posPrev = joints[jointsCount - 1].pos = basis.pos; joints[jointsCount - 1].length = 1.0f; @@ -397,7 +406,7 @@ struct Lara : Character { if (level->extra.braid > -1) braid = new Braid(this, vec3(-4.0f, 24.0f, -48.0f)); - /* + /* pos = vec3(40448, 3584, 60928); angle = vec3(0.0f, PI * 0.5f, 0.0f); getEntity().room = 14; @@ -434,12 +443,12 @@ struct Lara : Character { angle = vec3(0.0f, -PI * 0.25f, 0.0f); getEntity().room = 13; - // level 2 (room 43) + // level 2 (reach) pos = vec3(31400, -2560, 25200); angle = vec3(0.0f, PI, 0.0f); getEntity().room = 43; - // level 2 (room 16) + // level 2 (hang & climb) pos = vec3(60907, 0, 39642); angle = vec3(0.0f, PI * 3 / 2, 0.0f); getEntity().room = 16; @@ -1074,6 +1083,13 @@ struct Lara : Character { wpnHide(); } + virtual void cmdJump(const vec3 &vel) { + vec3 v = vel; + if (state == STATE_HANG_UP) + v.y = (3.0f - sqrtf(-2.0f * GRAVITY / 30.0f * (collision.info[Collision::FRONT].floor - pos.y + 800.0f))); + Character::cmdJump(v); + } + void drawGun(int right) { int mask = right ? BODY_ARM_R3 : BODY_ARM_L3; // unholster if (layers[1].mask & mask) @@ -1100,22 +1116,21 @@ struct Lara : Character { bool waterOut() { // TODO: playSound 36 - vec3 dst = pos + getDir() * 32.0f; + if (collision.side != Collision::FRONT || pos.y - collision.info[Collision::FRONT].floor > 256 + 128) + return false; - TR::Level::FloorInfo infoCur, infoDst; - level->getFloorInfo(getEntity().room, (int)pos.x, (int)pos.y, (int)pos.z, infoCur), - level->getFloorInfo(infoCur.roomAbove, (int)dst.x, (int)dst.y, (int)dst.z, infoDst); + vec3 dst = pos + getDir() * (LARA_RADIUS + 32.0f); - int h = int(pos.y - infoDst.floor); + TR::Level::FloorInfo info; + level->getFloorInfo(collision.info[Collision::NONE].roomAbove, int(dst.x), int(dst.y), int(dst.z), info); - if (h >= 0 && h <= 356 && (state == STATE_SURF_TREAD || animation.setState(STATE_SURF_TREAD)) && animation.setState(STATE_STOP)) { // possibility check - alignToWall(-96.0f); - pos.y = float(infoDst.floor); - //pos = dst; // set new position - + int h = int(pos.y - info.floor); + + if (h >= 0 && h <= (256 + 128) && (state == STATE_SURF_TREAD || animation.setState(STATE_SURF_TREAD)) && animation.setState(STATE_STOP)) { + alignToWall(LARA_RADIUS); + getEntity().room = collision.info[Collision::NONE].roomAbove; + pos.y = float(info.floor); specular = LARA_WET_SPECULAR; - - getEntity().room = infoCur.roomAbove; updateEntity(); return true; } @@ -1353,12 +1368,35 @@ struct Lara : Character { velocity.x = velocity.z = 0.0f; if ((state == STATE_REACH || state == STATE_UP_JUMP) && (input & ACTION) && emptyHands()) { - if (state == STATE_REACH && velocity.y < 0.0f) return state; + vec3 p = pos; + Collision c = Collision(level, getRoomIndex(), p, getDir() * 32.0f, vec3(0.0f), LARA_RADIUS, angleExt, 0, 0, 0, 0); + + if (c.side != Collision::FRONT) + return state; + Box bounds = animation.getBoundingBox(pos, 0); + int floor = c.info[Collision::FRONT].floor; + int hands = int(bounds.min.y); + + if (abs(floor - hands) < 32) { + alignToWall(-LARA_RADIUS); + pos.y = float(floor + LARA_HANG_OFFSET); + stand = STAND_HANG; + updateEntity(); + + if (state == STATE_REACH) { + return STATE_HANG; // TODO: ANIM_HANG_WALL / ANIM_HANG_NOWALL + } else + return animation.setAnim(ANIM_HANG, -15); + } + + /* + + vec3 p = pos + getDir() * 128.0f; TR::Level::FloorInfo info; // TODO: use brain @@ -1372,7 +1410,7 @@ struct Lara : Character { } while (info.ceiling > bounds.min.y && info.roomAbove != 0xFF); if (abs(info.floor - int(bounds.min.y)) < 16) { // reach fall - alignToWall(96.0f); + alignToWall(LARA_RADIUS); pos.y = info.floor + 724.0f; updateEntity(); @@ -1382,6 +1420,7 @@ struct Lara : Character { } else return animation.setAnim(ANIM_HANG, -15); } + */ } if (state == STATE_FORWARD_JUMP) { @@ -1409,7 +1448,7 @@ struct Lara : Character { if ((dx <= (512 + 128) && dz <= (512 - 128)) || (dx <= (512 - 128) && dz <= (512 + 128))) { - alignToWall(); + alignToWall(-LARA_RADIUS); Block *block = (Block*)e.controller; block->angle.y = angle.y; @@ -1427,26 +1466,22 @@ struct Lara : Character { if ((input & ACTION) && emptyHands() && doPickUp()) return STATE_PICK_UP; - if ((input & (FORTH | ACTION)) == (FORTH | ACTION) && (animation.index == ANIM_STAND || animation.index == ANIM_STAND_NORMAL) && emptyHands()) { // TODO: get rid of animation.index - vec3 p = pos + getDir() * 64.0f; - TR::Level::FloorInfo info; - level->getFloorInfo(getRoomIndex(), (int)p.x, (int)p.y, (int)p.z, info); - int h = (int)pos.y - info.floor; + if ((input & (FORTH | ACTION)) == (FORTH | ACTION) && (animation.index == ANIM_STAND || animation.index == ANIM_STAND_NORMAL) && emptyHands() && collision.side == Collision::FRONT) { // TODO: get rid of animation.index + int floor = collision.info[Collision::FRONT].floor; + int h = (int)pos.y - floor; int aIndex = animation.index; - if (info.floor == info.ceiling || h < 256 + 128) { - ; // do nothing - } else if (h <= 2 * 256 + 128) { + if (h <= 2 * 256 + 128) { aIndex = ANIM_CLIMB_2; - pos.y = info.floor + 512.0f; + pos.y = floor + 512.0f; } else if (h <= 3 * 256 + 128) { aIndex = ANIM_CLIMB_3; - pos.y = info.floor + 768.0f; + pos.y = floor + 768.0f; } else if (h <= 7 * 256 + 128) aIndex = ANIM_CLIMB_JUMP; if (aIndex != animation.index) { - alignToWall(96.0f); + alignToWall(-LARA_RADIUS); return animation.setAnim(aIndex); } } @@ -1490,7 +1525,7 @@ struct Lara : Character { int pushState = (input & FORTH) ? STATE_PUSH_BLOCK : STATE_PULL_BLOCK; Block *block = getBlock(); if (animation.canSetState(pushState) && block->doMove((input & FORTH) != 0)) { - alignToWall(128.0f); + alignToWall(-LARA_RADIUS); return pushState; } } @@ -1553,9 +1588,9 @@ struct Lara : Character { if (input & FORTH) { // possibility check TR::Level::FloorInfo info; - vec3 p = pos + getDir() * 128.0f; + vec3 p = pos + getDir() * (LARA_RADIUS + 2.0f); level->getFloorInfo(getRoomIndex(), (int)p.x, (int)p.y, (int)p.z, info); - if (info.floor - info.ceiling >= 768) + if (info.floor - info.ceiling >= LARA_HEIGHT) return (input & WALK) ? STATE_HANDSTAND : STATE_HANG_UP; } return STATE_HANG; @@ -1568,6 +1603,8 @@ struct Lara : Character { if (state == STATE_FORWARD_JUMP || state == STATE_UP_JUMP || state == STATE_BACK_JUMP || state == STATE_LEFT_JUMP || state == STATE_RIGHT_JUMP || state == STATE_FALL || state == STATE_REACH) { game->waterDrop(pos, 256.0f, 0.2f); Sprite::add(game, TR::Entity::WATER_SPLASH, getRoomIndex(), (int)pos.x, (int)pos.y, (int)pos.z); + pos.y += 100.0f; + angle.x = -45.0f * DEG2RAD; return animation.setAnim(ANIM_WATER_FALL); // TODO: wronng animation } @@ -1577,10 +1614,13 @@ struct Lara : Character { Sprite::add(game, TR::Entity::WATER_SPLASH, getRoomIndex(), (int)pos.x, (int)pos.y, (int)pos.z); return STATE_DIVE; } - + if (input & JUMP) return STATE_SWIM; + + if (state == STATE_GLIDE && speed < LARA_SWIM_SPEED * 2.0f / 3.0f) + return STATE_TREAD; - return (state == STATE_SWIM || velocity.y > GLIDE_SPEED) ? STATE_GLIDE : STATE_TREAD; + return STATE_GLIDE; } virtual int getStateOnwater() { @@ -1709,9 +1749,9 @@ struct Lara : Character { if (state == STATE_SWIM || state == STATE_GLIDE) w *= TURN_WATER_FAST; else if (state == STATE_TREAD || state == STATE_SURF_TREAD || state == STATE_SURF_SWIM || state == STATE_SURF_BACK) - w *= TURN_WATER_SLOW; + w *= TURN_WATER_FAST; else if (state == STATE_RUN) - w *= sign(w) != sign(tilt) ? 0.0f : w * TURN_FAST * tilt / TILT_MAX; + w *= sign(w) != sign(tilt) ? 0.0f : w * TURN_FAST * tilt / LARA_TILT_MAX; else if (state == STATE_FAST_TURN) w *= TURN_FAST; else if (state == STATE_FAST_BACK) @@ -1723,17 +1763,12 @@ struct Lara : Character { else w = 0.0f; - if (w != 0.0f) { - w *= Core::deltaTime; - angle.y += w; - velocity = velocity.rotateY(-w); // in-air velocity rotation control - } + if (w != 0.0f) + rotateY(w * Core::deltaTime); // pitch (underwater only) - if (stand == STAND_UNDERWATER && (input & (FORTH | BACK)) ) { - angle.x += ((input & FORTH) ? -TURN_WATER_SLOW : TURN_WATER_SLOW) * 0.5f * Core::deltaTime; - angle.x = clamp(angle.x, -PI * 0.5f, PI * 0.5f); - } + if (stand == STAND_UNDERWATER && (input & (FORTH | BACK))) + rotateX(((input & FORTH) ? -TURN_WATER_SLOW : TURN_WATER_SLOW) * Core::deltaTime); // get animation direction angleExt = angle.y; @@ -1743,6 +1778,7 @@ struct Lara : Character { case STATE_BACK_JUMP : case STATE_FAST_BACK : case STATE_SLIDE_BACK : + case STATE_ROLL_1 : angleExt += PI; break; case STATE_LEFT_JUMP : @@ -1768,18 +1804,15 @@ struct Lara : Character { case STAND_HANG : case STAND_ONWATER : { - float speed = 0.0f; switch (state) { case STATE_SURF_SWIM : case STATE_SURF_BACK : case STATE_SURF_LEFT : - case STATE_SURF_RIGHT : - speed = 15.0f; + case STATE_SURF_RIGHT : + speed = min(speed + 30.0f * LARA_WATER_ACCEL * Core::deltaTime, LARA_SURF_SPEED); break; default : speed = animation.getSpeed(); - if (animation.index == ANIM_STAND_ROLL_END) - speed = -speed; } if (stand == STAND_ONWATER) { @@ -1790,9 +1823,9 @@ struct Lara : Character { TR::Entity &e = getEntity(); TR::Level::FloorInfo info; if (stand == STAND_HANG) { - vec3 p = pos + getDir() * 128.0f; + vec3 p = pos + getDir() * (LARA_RADIUS + 2.0f); level->getFloorInfo(e.room, (int)p.x, (int)p.y, (int)p.z, info); - if (info.roomAbove != 0xFF && info.floor >= e.y - LARA_HANG_OFFSET) + if (info.roomAbove != TR::NO_ROOM && info.floor >= e.y - LARA_HANG_OFFSET) level->getFloorInfo(info.roomAbove, (int)p.x, (int)p.y, (int)p.z, info); } else level->getFloorInfo(e.room, e.x, e.y, e.z, info); @@ -1803,16 +1836,13 @@ struct Lara : Character { break; } case STAND_UNDERWATER : { - float speed = 0.0f; if (animation.index == ANIM_TO_UNDERWATER) speed = 15.0f; if (state == STATE_SWIM) - speed = SWIM_SPEED; - - if (speed != 0.0f) - velocity = vec3(angle.x, angle.y) * speed; - else - velocity = velocity - velocity * min(1.0f, Core::deltaTime * 2.0f); + speed = min(speed + 30.0f * LARA_WATER_ACCEL * Core::deltaTime, LARA_SWIM_SPEED); + if (state == STATE_TREAD || state == STATE_GLIDE) + speed = max(speed - 30.0f * LARA_SWIM_FRICTION * Core::deltaTime, 0.0f); + velocity = vec3(angle.x, angle.y) * speed; // TODO: apply flow velocity break; } @@ -1820,9 +1850,13 @@ struct Lara : Character { } virtual void updatePosition() { // TODO: sphere / bbox collision - updateTilt(state == STATE_RUN, TILT_SPEED, TILT_MAX); + // tilt control + vec2 vTilt(LARA_TILT_SPEED * Core::deltaTime, LARA_TILT_MAX); + if (stand == STAND_UNDERWATER) + vTilt *= 2.0f; + updateTilt(state == STATE_RUN || stand == STAND_UNDERWATER, vTilt.x, vTilt.y); - if (velocity.length() >= 0.001f) + if (velocity.length() >= 1.0f) move(); if (getEntity().type != TR::Entity::LARA) { @@ -1844,11 +1878,11 @@ struct Lara : Character { } void move() { - TR::Entity &e = getEntity(); - TR::Level::FloorInfo info; + //TR::Entity &e = getEntity(); + //TR::Level::FloorInfo info; - float f, c; - bool canPassGap = true; + //float f, c; + //bool canPassGap = true; /* if (velocity != 0.0f) { vec3 dir = velocity.normal() * 128.0f; @@ -1868,11 +1902,53 @@ struct Lara : Character { } } */ - vec3 offset = velocity * Core::deltaTime * 30.0f; + vec3 vel = velocity * Core::deltaTime * 30.0f; + vec3 opos(pos), offset(0.0f); - vec3 p = pos; - pos = pos + offset; + float radius = stand == STAND_UNDERWATER ? LARA_RADIUS_WATER : LARA_RADIUS; + int maxHeight = stand == STAND_UNDERWATER ? LARA_HEIGHT_WATER : LARA_HEIGHT; + int minHeight = 0; + int maxAscent = 256 + 128; + int maxDescent = 0xFFFFFF; + if (state == STATE_WALK || state == STATE_BACK) + maxDescent = maxAscent; + if (state == STATE_STEP_LEFT || state == STATE_STEP_RIGHT) + maxAscent = maxDescent = 64; + if (stand == STAND_ONWATER) + maxAscent = -1; + if (stand == STAND_HANG) { + maxHeight = 0; + maxAscent = maxDescent = 64; + offset = getDir() * (LARA_RADIUS + 32.0f); + offset.y -= LARA_HANG_OFFSET; + } + if (stand == STAND_UNDERWATER) { + offset.y += LARA_HEIGHT_WATER * 0.5f; + } + + int room = getRoomIndex(); + + collision = Collision(level, room, pos, offset, vel, radius, angleExt, minHeight, maxHeight, maxAscent, maxDescent); + + if (stand != STAND_HANG && (collision.side == Collision::LEFT || collision.side == Collision::RIGHT)) { + float rot = TURN_WALL_Y * Core::deltaTime; + rotateY((collision.side == Collision::LEFT) ? rot : -rot); + } + + if (stand == STAND_HANG && collision.side != Collision::FRONT) { + offset.x = offset.z = 0.0f; + minHeight = LARA_HANG_OFFSET; + maxDescent = 0xFFFFFF; + maxAscent = -LARA_HANG_OFFSET; + vec3 p = pos; + collision = Collision(level, room, p, offset, vec3(0.0f), radius, angleExt, minHeight, maxHeight, maxAscent, maxDescent); + if (collision.side == Collision::FRONT) + pos = opos; + } + + + /* if (canPassGap) { level->getFloorInfo(e.room, (int)pos.x, (int)pos.y, (int)pos.z, info); canPassGap = (info.floor - info.ceiling) >= (stand == STAND_GROUND ? 768 : 512); @@ -1880,6 +1956,8 @@ struct Lara : Character { f = info.floor - pos.y; c = pos.y - info.ceiling; + */ + /* TR::Animation *anim = animation; Box eBox = Box(pos - vec3(128.0f, 0.0f, 128.0f), pos + vec3(128.0, getHeight(), 128.0f)); // getBoundingBox(); @@ -1915,6 +1993,7 @@ struct Lara : Character { } } */ + /* if (canPassGap) switch (stand) { case STAND_AIR : { @@ -1959,38 +2038,46 @@ struct Lara : Character { break; default : ; } + */ + // get current leading foot in animation int rightStart = 0; if (state == STATE_RUN) rightStart = 6; if (state == STATE_WALK) rightStart = 13; if (state == STATE_BACK) rightStart = 28; bool isLeftFoot = animation.frameIndex < rightStart || animation.frameIndex > (rightStart + animation.framesCount / 2); + - if (!canPassGap) { - pos = p; // TODO: use smart ejection + if (stand == STAND_UNDERWATER) { + if (collision.side == Collision::TOP) + rotateX(-TURN_WALL_X * Core::deltaTime); + if (collision.side == Collision::BOTTOM) + rotateX( TURN_WALL_X * Core::deltaTime); + } + + if (stand == STAND_AIR && collision.side == Collision::TOP && velocity.y < 0.0f) + velocity.y = 30.0f; + + if (collision.side == Collision::FRONT) { + int floor = collision.info[Collision::FRONT].floor; // hit the wall switch (stand) { case STAND_AIR : - if (state == STATE_UP_JUMP || state == STATE_REACH) { + if (state == STATE_UP_JUMP || state == STATE_REACH) velocity.x = velocity.z = 0.0f; - if (c <= 0 && offset.y < 0.0f) offset.y = velocity.y = 0.0f; - } if (velocity.x != 0.0f || velocity.z != 0.0f) { animation.setAnim(ANIM_SMASH_JUMP); - velocity.x = -velocity.x * 0.5f; - velocity.z = -velocity.z * 0.5f; - if (offset.y < 0.0f) - offset.y = velocity.y = 0.0f; + velocity.x = -velocity.x * 0.25f; + velocity.z = -velocity.z * 0.25f; + if (velocity.y < 0.0f) + velocity.y = 30.0f; } - - pos.y = p.y + offset.y; - break; case STAND_GROUND : case STAND_HANG : - if (f <= -(256 * 3 - 128) && state == STATE_RUN) + if (opos.y - floor > (256 * 3 - 128) && state == STATE_RUN) animation.setAnim(isLeftFoot ? ANIM_SMASH_RUN_LEFT : ANIM_SMASH_RUN_RIGHT); else if (stand == STAND_HANG) animation.setAnim(ANIM_HANG, -21); @@ -1998,11 +2085,18 @@ struct Lara : Character { animation.setAnim((state == STATE_RUN || state == STATE_WALK) ? (isLeftFoot ? ANIM_STAND_LEFT : ANIM_STAND_RIGHT) : ANIM_STAND); velocity.x = velocity.z = 0.0f; break; + case STAND_UNDERWATER : + if (fabsf(angle.x) > TURN_WALL_X_CLAMP) + rotateX(TURN_WALL_X * Core::deltaTime * sign(angle.x)); + else + pos.y = opos.y; + break; default : ;// no smash animation } - } else + } else { if (stand == STAND_GROUND) { - int h = int(info.floor - pos.y); + int floor = collision.info[Collision::NONE].floor; + int h = int(floor - opos.y); if (h >= 256 && state == STATE_FAST_BACK) { stand = STAND_AIR; @@ -2010,18 +2104,20 @@ struct Lara : Character { } else if (h >= 128 && (state == STATE_WALK || state == STATE_BACK)) { // descend if (state == STATE_WALK) animation.setAnim(isLeftFoot ? ANIM_WALK_DESCEND_LEFT : ANIM_WALK_DESCEND_RIGHT); if (state == STATE_BACK) animation.setAnim(isLeftFoot ? ANIM_BACK_DESCEND_LEFT : ANIM_BACK_DESCEND_RIGHT); - pos.y = float(info.floor); + pos.y = float(floor); } else if (h > -1.0f) { - pos.y = min((float)info.floor, pos.y += DESCENT_SPEED * Core::deltaTime); + pos.y = min(float(floor), pos.y += DESCENT_SPEED * Core::deltaTime); } else if (h > -128) { - pos.y = float(info.floor); + pos.y = float(floor); } else if (h >= -(256 + 128) && (state == STATE_RUN || state == STATE_WALK)) { // ascend if (state == STATE_RUN) animation.setAnim(isLeftFoot ? ANIM_RUN_ASCEND_LEFT : ANIM_RUN_ASCEND_RIGHT); if (state == STATE_WALK) animation.setAnim(isLeftFoot ? ANIM_WALK_ASCEND_LEFT : ANIM_WALK_ASCEND_RIGHT); - pos.y = float(info.floor); + pos.y = float(floor); } else - pos.y = float(info.floor); + pos.y = float(floor); } + collision.side = Collision::NONE; + } updateEntity(); checkRoom(); diff --git a/src/level.h b/src/level.h index 3544342..c0020bc 100644 --- a/src/level.h +++ b/src/level.h @@ -767,7 +767,8 @@ struct Level : IGame { delete cube; delete mesh; - delete camera; + delete camera; + Sound::stopAll(); } void initTextures() { @@ -1148,7 +1149,7 @@ struct Level : IGame { } } } - } + } camera->update(); waterCache->update(); @@ -1375,6 +1376,40 @@ struct Level : IGame { // renderModel(level.models[modelIndex], level.entities[4]); */ Debug::begin(); + + lara->updateEntity(); // TODO clip angle while rotating + + int q = int(normalizeAngle(lara->angleExt + PI * 0.25f) / (PI * 0.5f)); + float radius = 256.0f; + + const vec2 v[] = { + { -radius, radius }, + { radius, radius }, + { radius, -radius }, + { -radius, -radius }, + }; + + const vec2 &l = v[q], + &r = v[(q + 1) % 4], + &f = (q %= 2) ? vec2(l.x, radius * cosf(lara->angleExt)) : vec2(radius * sinf(lara->angleExt), l.y); + + vec3 F = vec3(f.x, 0.0f, f.y); + vec3 L = vec3(l.x, 0.0f, l.y); + vec3 R = vec3(r.x, 0.0f, r.y); + + vec3 p, n = lara->pos + vec3(0.0f, -512.0f, 0.0f); + + Core::setDepthTest(false); + glBegin(GL_LINES); + glColor3f(0, 0, 1); p = n; glVertex3fv((GLfloat*)&p); p += F; glVertex3fv((GLfloat*)&p); + glColor3f(1, 0, 0); p = n; glVertex3fv((GLfloat*)&p); p += L; glVertex3fv((GLfloat*)&p); + glColor3f(0, 1, 0); p = n; glVertex3fv((GLfloat*)&p); p += R; glVertex3fv((GLfloat*)&p); + glColor3f(1, 1, 0); p = lara->pos; glVertex3fv((GLfloat*)&p); p -= vec3(0.0f, LARA_HANG_OFFSET, 0.0f); glVertex3fv((GLfloat*)&p); + glEnd(); + Core::setDepthTest(true); + + + /* glMatrixMode(GL_MODELVIEW); glPushMatrix(); @@ -1415,15 +1450,17 @@ struct Level : IGame { glPopMatrix(); */ - + Core::setBlending(bmAlpha); // Debug::Level::rooms(level, lara->pos, lara->getEntity().room); // Debug::Level::lights(level, lara->getRoomIndex()); - // Debug::Level::sectors(level, lara->getRoomIndex(), (int)lara->pos.y); + Debug::Level::sectors(level, lara->getRoomIndex(), (int)lara->pos.y); // Core::setDepthTest(false); // Debug::Level::portals(level); // Core::setDepthTest(true); // Debug::Level::meshes(level); // Debug::Level::entities(level); + Core::setBlending(bmNone); + /* static int dbg_ambient = 0; dbg_ambient = int(params.time * 2) % 4; diff --git a/src/platform/web/index.html b/src/platform/web/index.html index ce7bfad..a477a7c 100644 --- a/src/platform/web/index.html +++ b/src/platform/web/index.html @@ -124,6 +124,8 @@ controls:
keyboad: move - WASD / arrows, jump - Space, action - E/Ctrl, draw weapon - Q, change weapon - 1-4, walk - Shift, side steps - ZX/walk+direction, camera - MouseR)
gamepad: PSX controls on XBox controller
+ Change view: V
+ Time Control: R - slow motion, T - fast motion
FullScreen: Alt + Enter diff --git a/src/platform/win/OpenLara.vcxproj b/src/platform/win/OpenLara.vcxproj index 13197d7..e5a9775 100644 --- a/src/platform/win/OpenLara.vcxproj +++ b/src/platform/win/OpenLara.vcxproj @@ -189,6 +189,7 @@ + diff --git a/src/sound.h b/src/sound.h index 076d14e..26fc6c8 100644 --- a/src/sound.h +++ b/src/sound.h @@ -530,6 +530,12 @@ namespace Sound { delete stream; return NULL; } + + void stopAll() { + for (int i = 0; i < channelsCount; i++) + delete channels[i]; + channelsCount = 0; + } } #endif \ No newline at end of file diff --git a/src/utils.h b/src/utils.h index d87c5a8..94acac7 100644 --- a/src/utils.h +++ b/src/utils.h @@ -85,6 +85,16 @@ float shortAngle(float a, float b) { return clampAngle(n - int(n / PI2) * PI2); } +float normalizeAngle(float angle) { + while (angle < 0.0f) angle += PI2; + while (angle > PI2) angle -= PI2; + return angle; +} + +int angleQuadrant(float angle) { + return int(normalizeAngle(angle + PI * 0.25f) / (PI * 0.5f)); +} + float decrease(float delta, float &value, float &speed) { if (speed > 0.0f && fabsf(delta) > 0.01f) { if (delta > 0) speed = min(delta, speed); @@ -112,11 +122,13 @@ struct vec2 { vec2(float s) : x(s), y(s) {} vec2(float x, float y) : x(x), y(y) {} - float& operator [] (int index) const { ASSERT(index >= 0 && index <= 1); return ((float*)this)[index]; } + inline float& operator [] (int index) const { ASSERT(index >= 0 && index <= 1); return ((float*)this)[index]; } - bool operator == (float s) const { return x == s && y == s; } - bool operator != (float s) const { return !(*this == s); } - vec2 operator - () const { return vec2(-x, -y); } + inline bool operator == (const vec2 &v) const { return x == v.x && y == v.y; } + inline bool operator != (const vec2 &v) const { return !(*this == v); } + inline bool operator == (float s) const { return x == s && y == s; } + inline bool operator != (float s) const { return !(*this == s); } + inline vec2 operator - () const { return vec2(-x, -y); } vec2& operator += (const vec2 &v) { x += v.x; y += v.y; return *this; } vec2& operator -= (const vec2 &v) { x -= v.x; y -= v.y; return *this; } @@ -138,6 +150,7 @@ struct vec2 { float length2() const { return dot(*this); } float length() const { return sqrtf(length2()); } vec2 normal() const { float s = length(); return s == 0.0 ? (*this) : (*this)*(1.0f/s); } + float angle() const { return atan2(y, x); } }; struct vec3 { @@ -148,11 +161,13 @@ struct vec3 { vec3(const vec2 &xy, float z = 0.0f) : x(xy.x), y(xy.y), z(z) {} vec3(float lng, float lat) : x(sinf(lat) * cosf(lng)), y(-sinf(lng)), z(cosf(lat) * cosf(lng)) {} - float& operator [] (int index) const { ASSERT(index >= 0 && index <= 2); return ((float*)this)[index]; } + inline float& operator [] (int index) const { ASSERT(index >= 0 && index <= 2); return ((float*)this)[index]; } - bool operator == (float s) const { return x == s && y == s && z == s; } - bool operator != (float s) const { return !(*this == s); } - vec3 operator - () const { return vec3(-x, -y, -z); } + inline bool operator == (const vec3 &v) const { return x == v.x && y == v.y && z == v.z; } + inline bool operator != (const vec3 &v) const { return !(*this == v); } + inline bool operator == (float s) const { return x == s && y == s && z == s; } + inline bool operator != (float s) const { return !(*this == s); } + inline vec3 operator - () const { return vec3(-x, -y, -z); } vec3& operator += (const vec3 &v) { x += v.x; y += v.y; z += v.z; return *this; } vec3& operator -= (const vec3 &v) { x -= v.x; y -= v.y; z -= v.z; return *this; } @@ -598,6 +613,13 @@ struct Basis { void rotate(const quat &q) { rot = rot * q; } + + Basis lerp(const Basis &basis, float t) { + Basis b; + b.rot = rot.lerp(basis.rot, t); + b.pos = pos.lerp(basis.pos, t); + return b; + } }; struct ubyte2 {