From cb3f363cfe5eed06ec91c2dba5e1fe7fe38f4598 Mon Sep 17 00:00:00 2001 From: Lephenixnoir Date: Sat, 25 Dec 2021 11:47:22 +0100 Subject: [PATCH] switch engine to ECS and rewrite just about everything --- CMakeLists.txt | 9 +- assets-cg/enemies/fxconv-metadata.txt | 6 +- assets-cg/hud.png | Bin 1342 -> 1375 bytes assets-cg/levels/1.txt | 12 +- assets-cg/player/fxconv-metadata.txt | 4 +- assets-cg/player/player_left.aseprite | Bin 2958 -> 2666 bytes assets-cg/skillicons.png | Bin 540 -> 553 bytes assets-cg/skills/fxconv-metadata.txt | 2 +- src/anim.c | 98 +++++-- src/anim.h | 81 +++--- src/aoe.c | 256 ++++++++++++++++ src/aoe.h | 77 +++++ src/comp/entity.c | 69 +++++ src/comp/entity.h | 87 ++++++ src/comp/fighter.c | 33 +++ src/comp/fighter.h | 38 +++ src/comp/mechanical.c | 132 +++++++++ src/comp/mechanical.h | 67 +++++ src/comp/physical.c | 14 + src/comp/physical.h | 29 ++ src/comp/visible.c | 37 +++ src/comp/visible.h | 55 ++++ src/enemies.c | 80 +++-- src/enemies.h | 21 +- src/entities.c | 404 -------------------------- src/entities.h | 204 ------------- src/game.c | 250 +++++++++------- src/game.h | 45 ++- src/main.c | 311 ++++++++++---------- src/map.c | 6 +- src/map.h | 16 +- src/object.c | 0 src/pathfinding.c | 8 +- src/render.c | 195 ++++++++----- src/render.h | 5 - 35 files changed, 1548 insertions(+), 1103 deletions(-) create mode 100644 src/aoe.c create mode 100644 src/aoe.h create mode 100644 src/comp/entity.c create mode 100644 src/comp/entity.h create mode 100644 src/comp/fighter.c create mode 100644 src/comp/fighter.h create mode 100644 src/comp/mechanical.c create mode 100644 src/comp/mechanical.h create mode 100644 src/comp/physical.c create mode 100644 src/comp/physical.h create mode 100644 src/comp/visible.c create mode 100644 src/comp/visible.h delete mode 100644 src/entities.c delete mode 100644 src/entities.h delete mode 100644 src/object.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 05bdc29..871954e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,8 +16,8 @@ endif() set(SOURCES src/anim.c + src/aoe.c src/enemies.c - src/entities.c src/game.c src/geometry.c src/level.c @@ -27,6 +27,12 @@ set(SOURCES src/pathfinding.c src/render.c src/util.c + + src/comp/entity.c + src/comp/fighter.c + src/comp/mechanical.c + src/comp/physical.c + src/comp/visible.c ) set(ASSETS # Tilesets @@ -80,6 +86,7 @@ fxconv_declare_converters(assets-cg/converters.py) add_executable(addin ${SOURCES} ${ASSETS}) target_compile_options(addin PRIVATE -Wall -Wextra -Os) +target_include_directories(addin PRIVATE src) target_link_libraries(addin LibProf::LibProf Gint::Gint) target_link_options(addin PRIVATE -Wl,-Map=map) diff --git a/assets-cg/enemies/fxconv-metadata.txt b/assets-cg/enemies/fxconv-metadata.txt index f157198..4aafc40 100644 --- a/assets-cg/enemies/fxconv-metadata.txt +++ b/assets-cg/enemies/fxconv-metadata.txt @@ -1,13 +1,13 @@ *.aseprite: custom-type: aseprite-anim - name_regex: (.*)\.aseprite anims_\1 + name_regex: (.*)\.aseprite frames_\1 profile: p8 next: Idle=Idle, Walking=Walking, Hit=Idle slime_left.aseprite: - center: 12, 17 + center: 11, 18 slime_right.aseprite: - center: 13, 17 + center: 11, 18 bat_left.aseprite: center: 10, 12 diff --git a/assets-cg/hud.png b/assets-cg/hud.png index 3b896296a189dcbadeacd3cad7a6a5f44a1a2be0..3b51076fc5fdf309cc180097f84be962dd54a47e 100644 GIT binary patch delta 1344 zcmV-G1;6^f3f~HlFn%60 z4=IEz><-C4GYEGO{ut~%ngK>a5gT@bP$snVM0^1LtN;UVtQt_FKHA}l8i%}Y510Z z=nc`|ZJjgDd;}htFa$ z@7=!XB#A`OOXOBBkj1zBL$BWcrNS`A#;*mHGc|Reh=@9!{or{T%=0^){UELjpx3Ql zAd7GLhhDw?ONC)X;mYHRO9P*uB}r01FYpQ9@(;awP=8?<=G4ok(xORnJq0C#-m1{+ zoeX^c%9Zhgl*r>-{-O7LTrH?DjOdgui!(D^#w=gd6cl>>WSB`Lo$H%<_?Ca@)!V;R z7)BA!kGTHCbxjtd7*yUDmn20*a4~2= zZ%A2#vSzf^iEsIb-ViCh6BBnt;)pFt*<9G)Uw^-)WFT$@;sRNG%Rl1E$CWZ3kr~F~ zQnP=2d_>*uVXpFbuU-zG$L-pfhNLoiH4OrKLtN;UqS2TnqVxaEmb#lMDW0}E@h$(* z8zQB*(U_#grDorDSxR(CD*sjqe*!nP*|pK!C6eR0*}egK-RhNtO{3dA6f=&v^87jG z`+s`yE&tG~wSS2ihV;5%o=PusYhdWrNw<3#4CE5VG0Z49zP3&t#kc%JuM867wDqs% z=5kctj&f`C4-pX^eE(@HCxEAkAOBqxi^)K*TfLI-;2t-yGsBD{tx=fgYQnetL$AEv zUhjk&jY*oAxMOv9T?6yYW3V(Nj%Ld{C=ygLD<47tysJup_V0_Cz^oB_4 z8S0apvAR%~~ah&N!CW+O)f~9Xt{d&CM?m5z+3> zwwFec1vA&2dvl=EMqJy}++Nuzh|-&dG!kuB#gbIo3t3)k)7Iuj=5o@c`P``1$IU0S zwYgD><~(|Nz%Y+hkzsYSyR%Jm^M4Co*F&ECvsw7iUd=CLW#lQ1-qzFAO7ySX|MWJO zql!ZFH;5>v*2m2!XC5^Y)m!x$MjSk&=ynf-c0D)0P>Xsnud)4X^~^V7jYMZe+S{v^ z89cXZ9>24a`!@`UyQ8wu>z$l&^5|<##A#(TYEIItQL8I>2FQ>PuSt zm~ZVht{%&*TbsM3yfF*_T*4jL?8sUNOaN@KF^vD&)AwPjcWo~K0000pMM3cd=EFnu}d656vm$+fixPySe*p4PLW0|{09Q*#S{t_ zc7lc7am5v4XCc^G2&PC#AILvoAvP&ON)5rQf{@%12pDoH%-bEu-MihLH@k0kzh85A z>+SQ~_h#n18TZBs0surTE-equ*XL)a6IE#eKpW@`08nfc{eOP{&he?KDI$V#06+x8 z01YR}DA**Dj=?xUlR@JLjbxNj*uXddxCMWV<3KPsY0JsH>9W;U>MqvZv08Iyt z9Ml>Kqp*Q-fTp6Nald}`ayb57#d!PSqxdyKZ;TJUQY!hu#=% z{p(@X0+=7y-+#qpPS4DW$7G<_uU<){je_BLzT!&~TK%C{Xa7=R7@X`+SJz@OU(C&T zDIyW{68Y5&WC^YQ(5tt9sW6P`nb{~gbFCH;5nc6q(eo^r@4xExqPRYQUcY*QETPpO zdiC}%6^3EM$>YRjf#08}NK!yA2#L_@550O&VHoDr%YP+lc9DEfL5ZNZF7yY3kw2fB zoGeL+JfYPedhf^8f(pa1r*wIonc;KH^UIooLT{LiGl{hKhGrh2)gOBG_AeENQHJv) zZa8s$lgB7Voga#EgjRpV^&8g(6@~#o8MqonTD-J9r~s%N52_XbKpFV_j)dm#UjQHp z3e|qCSxR(Cs`yq3H<6pV?AmDV63OxWY~KLAe)Y;>c0?)TNIKulr@owoR)6T#+P_2$ zLw|Z*ut?IY+!`2qbz*9kFphCX!SS_qa+}cV54|!-jML74t}&ORiguJ|4{nHv==0Ct zu5u!t|DPs)`};^NCIh{G^-98nd(yzpj5Cg`Mq$2J6QR`~dgb-@2LqaGwP=2R!Rc-( zXTw-rS{@R)ZH>SQ_kqs_;sfaQLmuNuJAZf7`5KLa39bIn8zZTgH_1FdJ1so!W4Bt+ z2zWp@|L=hLIBuybM56#e8mAcs&n{foZL_Q)w>#bF`QC1q+MO=#?RH0;af^=*Jx*_u zR(?JXO?vtDC_)`Dv&=X~{XC@W&Rc);SQYgTt-XelUtX3XDn82)nPHUGd9n<+T7NLL znvF#3HQ0JJYn8yBakM*KIypXy9*Kz7H#Uig=;Zh)NTbMuS!=GnInZe%scmX*uXcXz zV`U)Gc2z7%WxbGgr%Q(i`?=4PCd==&wQgL;^nW&&qsl__CkPu;>(=(pokxw>dh0&JNP=e+SG``; zuGcp<8<7WlhN=CU?N995+pCrtJhy8ezw=V~Zx|AHN9CbE7~FAk`?V&Lv@#ksr|H$G z)fYSi;e+C9O84)1)9|#y2ZmFmM$u3VdAh|8W5J=ve9tb3V?`!(6 z3e?5|@-5UzkiP%_fjWT7fog#YfvQwMicmrWsGEU@A-JF@vm{j^I3qQ+qyQ))p}+zp zVF1bL;X6M8ZShRWNo8VSg~>B8d_EV%z|Ih!n3J8Em(B)r0!aQ}bR+{agGXiw7tk6w z$T}O%z{cQMQj(aQy$&eKssO}5k^f9EB|HrL3>9-~gEsOuIB=wJcxP8nH`rLh{d_|S zqxr@X5%G-|9EzUIYnUG+x0CyPqf*(u2Ce5mSH4%8|4ih!&5zo3iyMm>UjMXy#{Ay) z?(s_tKDF8fx^ImMuKafBV{tCqk<&fBr5mvvb(~v~(}ffxkX7f+H6cE^C1p;(AsT*9!n$ zpPazZ{69ltxq%}R6__yiIzrE`@dlGYWUP(^4dgzR6S~6!aTh2e`M3dscE6qmvooq~UT*m;n#h28P7;h&cM7Yy2~ zgN-@Q=JMOj|FAXd$o`zd!~fze${3$c_<3m?nopL&eS(>Yl4%?bi;gn3Ri*G0bDzld zczCqo4LCabCRF9r9e662Bhg>Kk&$OJ;}iSGjN8#%y&UdpdMBQO6-QUfNglq_ArhwX z%27fr+@M@Is9(9SqsV?*#Zd!!*~gsnIShww8BX1Nq`DK$*(>4Brhi~R3gF)+uf~74 z;tun7X`OWmh3u1^x#FjkH0t&htZ?t-mXSDozT(LNVA`Jgi0Kg2;YL6jl-@T1F=kQ2 z3odGs6C4=-Pc*PI6e%>9ocKTE)rQW-uO*eM8V!5)>K+#8nRqlIC&l)~v%>~HwzK4R z{C?-lf8@Z9Kb*zJCv;{+m3*G~=V-&R|NpDwg(Z}4_qy`O$b0CX$w@KWXW>xB&y#E{ zpJIMSSD`XxYs8i07mi`vm;3c)4?Ge|ufvP%7`{+BE6tK`_280JSYd~Xds z9tAb_1ds*=j28i8qqVgDCrW@#o`KusRT}nbupoz-JoJKmJv3}mpkXr~9yaVKB|IN( zOZZ0&vHW7?3wE%IsTq_;91>hzl~dSq+V({zL;LT*FALhGH&roM-@GIPD#Jk)5-g3c zf;*SqnR7#{yxC^QZ-?)28VjqmiTFvpax^K5c^Jt5-FV_V$B)dLtZe?JCLA!~KU~AW zb^FFM+(`wwG=$Vf$*?kEuALkVY}(3LZ!<7g)bN%-v2%8fW)z53)>kZZO_yIGpB+3cVmvj zXV_;pK5-YRU$|O*3A?4<#jlTxq&G!w_;GZx+-fK(Gdcdn&;_lJi;ATJ>g2i LS8j%$?#~MWa@MJn literal 2958 zcmds3Yfw{16y5+y1OrhF%(Wt1L|@tN0NP7T0RkTs-~a%qAvEubF~DJsU=n=(i82WP{<@Fw7L@=2Ea6K;-$IUo^M~J4 zQRC5;31fQ3eChyTSy7NuJ>dEi9(jFaQUQ~kGQBEwW5JN)fcO05*CUfF=z|7rV!t`6&Ebq^IUWZT^PhYE zR-*{{vfe!7JKPo8v?NV<|1(YPxi>*k>NlMq-w-KC7gejsz39)Z7hRrW?IlhnEhb}d z#*Ra?l@AcJ7OvH=^`*EeWJkz7}#v^II>o46c41Fxn_!(^Yla*#K-Ey=0Zlv;E%tjp1DF9ps{YxH< z(#nT5M(fspgjVPGdqTT-fz`=zD^oiuJZsWJ-wRwJnIrADq@Jyo`viZsYdS5*+3aKl zv&H&l;_{4MZ&w?iL##4!oTiN5=9p>#gelsl#`ow+#-Uf7%4=Wn(KUNkr7G5KMwgmx zVOKxwvi!V`wk0gn7O~q|O|w_qJbRyaXDcx^vDO>;zAnHvngSE8WnD5~ko`1k&yixN4k$A2v~PZoj&vM4A6Ong zdrfr*j!w=jLzul6w>@@V8NRi_wj@N<9U;j(agxhanps7e-RTr$^KuQ~?)!Osr@=3y zl-yvAEvRebQc|}sFu&%&suaVFlc_t*?Nl)1+-aHR7ow1KR}qrqcShUQlgSpSD1uvn z{=Ds_7fJ4wWF?Uzk5~e6R}U+j_%7d6%PEJ+7Yw;Ba)pdfFp*~AmaeX$mGls_HkQ_C z#U4dTBcG(nQ)H;>yh4>U8k%8BU0$suH4d_toZ#M1vtr2lmN;u}yINeI&gq_prx*Yo z|Ji)rd#r-TWmO4TNgP-^#I>n;Jf_p8#wPxT?!xoh<4V`P@>zAm9UP*w!r?A|;Xpuh zOP&d6s9jOd9YvAB8jD@f-$2RFzMg`>@Ij#VQJ`OM_fB2K+s7G+E8m*0-WS8L9Xc7^ z`sgwKANpX&EVeIT5yT=V9xrkcvUJ^Ap|+!>K} zt)H41kwYa&_X~C(x7n4aD3v4=1WB(-iN_)mhh*;4EN{}jNtd%W(S5C*Tx1NJZ7DBe z3?dPsiJFcIO%vw23DZQqY4_8t3(#dnRRJx#V~G|u_*jOcM?^G%=VH-hAFL@qo^&dM zyycKrNv}`2VS~pyaZ0Trn7yLON`K7im(O^|><8Tu*o)@`#eWKkbdVD0z9gsV0et3m z*3Gb>PYTj-OOJ92!v`hZUUL#oB3DTyK~G8LkPj8!-^5C_a#SCE)?xL(c6-R04DSN) z_8_S89A9*thW4xC{j+DS48vwL*!S)r;$eUh|UQM zF}*-egOd1 z^|4)Rz~$HV5tZ}ad*@tpl3UsJ-fxA`!_OFIx*$@B5DiFPl7CO6qzI89`55^Wpk5@> zZ;gFUVhei~On|)-IVAbkO60JT-vXiF4|Sjs-06Jr)=HG%`n6<`e$O#dlEteg^~`G5 zs-Lr!vNwlr?hU=#YRDeldqrhx-WiL<)XS+>;2+mAxVfYlIZ@&7l!(d7SnR$copY{b zMOu?#)P$uZNq@O-yC)K8R~^=bpd=bWrnGM*_7Fr8YSw7AZFvNlH9qcsj3m@96KY8q z(Oz@oVhV?<$P_fyYHgK0Hz7jrsli(iY1vWXlP3+dB*tuNk|y>%=O@=YOly@Nn_KVY zf>8UM%V`ovyGGU8s5v!}6Buj2NWUb|xv{#$pAxvwKLp+bPaRaDR`;)KkpKVy07*qoM6N<$ Ef@kdhF8}}l delta 501 zcmVSs+eA6?Jl{Gkps!rodfl}I2Ru3t(9=}7u5Gdm=U+xEZcv91MD zLP`G2v6E**ueKVqN9T^HOwF27ET&#gwF1Anmch*>#SxY0B9LUmSe(8hopY{bMOu?# z)C)^Vl5*cpPk$uPt~#s&esCfygZObFbtnu;m&qzY;XF@FrBid{3 zxtPMGDl!F4wOU(c&mlx?of^Cak(P-He|geCOQK{`lQh!xoS$6pGOabd+1&SDE(o>P zxtu0(wBLvvDcNwnOI2j+78h%smTJ|uXr6Q_d5s^*$!<<;7ej-Xj&>I#M2Su*QX{$W zy47y0@t#$80?KEtz~6y;&EUIa;GSw}CHj^C#@c3VDl_6+bZ@QFmQ)Iod~QoBhMHZI r+Dah(l0fI4)g}Iv!2SP2;63mH4b+?Bg}7oF00000NkvXXu0mjf -/* List of animations (and directional animations). */ +/* Definition of simple and direction animations. */ -#define DIRECTIONAL_ANIM(name) \ - extern anim_frame_t name ## _up[]; \ - extern anim_frame_t name ## _right[]; \ - extern anim_frame_t name ## _down[]; \ - extern anim_frame_t name ## _left[]; \ - anim_frame_t *name[4] = { \ - name ## _up, name ## _right, name ## _down, name ## _left }; +#define ANIM1(name) \ + extern anim_frame_t frames_ ## name[]; \ + anim_t anims_ ## name = { 1, { frames_ ## name }}; + +#define ANIM_2DIRECTIONAL(prefix, suffix) \ + extern anim_frame_t frames_ ## prefix ## _left ## suffix[]; \ + extern anim_frame_t frames_ ## prefix ## _right ## suffix[]; \ + anim_t anims_ ## prefix ## suffix = { 2, { \ + frames_ ## prefix ## _left ## suffix, \ + frames_ ## prefix ## _right ## suffix, \ + }}; \ #define ANIM_4DIRECTIONAL(prefix, suffix) \ - extern anim_frame_t prefix ## _up_ ## suffix[]; \ - extern anim_frame_t prefix ## _right_ ## suffix[]; \ - extern anim_frame_t prefix ## _down_ ## suffix[]; \ - extern anim_frame_t prefix ## _left_ ## suffix[]; \ - anim_frame_t *prefix ## _ ## suffix[4] = { \ - prefix ## _up_ ## suffix, \ - prefix ## _right_ ## suffix, \ - prefix ## _down_ ## suffix, \ - prefix ## _left_ ## suffix, \ - }; + extern anim_frame_t frames_ ## prefix ## _up ## suffix[]; \ + extern anim_frame_t frames_ ## prefix ## _right ## suffix[]; \ + extern anim_frame_t frames_ ## prefix ## _down ## suffix[]; \ + extern anim_frame_t frames_ ## prefix ## _left ## suffix[]; \ + anim_t anims_ ## prefix ## suffix = { 4, { \ + frames_ ## prefix ## _up ## suffix, \ + frames_ ## prefix ## _right ## suffix, \ + frames_ ## prefix ## _down ## suffix, \ + frames_ ## prefix ## _left ## suffix, \ + }}; -DIRECTIONAL_ANIM(anims_skill_swing); -DIRECTIONAL_ANIM(anims_skill_impale); -DIRECTIONAL_ANIM(anims_skill_bullet); +/* Make suffix optional */ +#define ANIM2(prefix, ...) \ + ANIM_2DIRECTIONAL(prefix, __VA_OPT__(_ ## __VA_ARGS__)) +#define ANIM4(prefix, ...) \ + ANIM_4DIRECTIONAL(prefix, __VA_OPT__(_ ## __VA_ARGS__)) -ANIM_4DIRECTIONAL(anims_player, Idle); -ANIM_4DIRECTIONAL(anims_player, Walking); -ANIM_4DIRECTIONAL(anims_player, Attack); -ANIM_4DIRECTIONAL(anims_player, Hit); +/* Basic skills */ +ANIM1(skill_hit); +ANIM1(skill_teleport); +ANIM1(skill_shock); +ANIM1(skill_judgement); +/* Directional skills */ +ANIM4(skill_swing); +ANIM4(skill_impale); +ANIM4(skill_bullet); + +/* Enemies */ +ANIM2(slime, Idle); +ANIM2(slime, Walking); +ANIM2(slime, Hit); +ANIM2(slime, Death); +ANIM2(bat, Idle); +ANIM2(bat, Hit); +ANIM2(bat, Death); + +/* Player */ +ANIM4(player, Idle); +ANIM4(player, Walking); +ANIM4(player, Attack); +ANIM4(player, Hit); /* Animation functions. */ -fixed_t (anim_duration)(anim_frame_t const *first_frame) +fixed_t (anim_duration)(anim_t const *anim) { - anim_frame_t const *frame = first_frame; + anim_frame_t const *frame = anim->start[0]; int ms = 0; int i = 0; - while(frame == first_frame + i) { + while(frame == anim->start[0] + i) { ms += frame->duration; frame = frame->next; i++; @@ -49,6 +75,22 @@ fixed_t (anim_duration)(anim_frame_t const *first_frame) return fix(ms) / 1000; } +bool anim_in(anim_frame_t const *frame, anim_t const *anim, int index) +{ + if(index >= anim->directions) + return false; + + anim_frame_t *f = anim->start[index]; + + for(int i = 0; f == anim->start[index] + i; i++) { + if(f == frame) + return true; + f = f->next; + } + + return false; +} + void anim_frame_render(int x, int y, anim_frame_t const *frame) { if(!frame) return; diff --git a/src/anim.h b/src/anim.h index ce9d460..ec0ca0e 100644 --- a/src/anim.h +++ b/src/anim.h @@ -12,7 +12,8 @@ #include -typedef struct anim_frame { +typedef struct anim_frame +{ /* Sheet */ bopti_image_t const *sheet; /* Box for the frame within the sheet */ @@ -23,21 +24,35 @@ typedef struct anim_frame { uint16_t duration; /* Next frame */ struct anim_frame *next; + } anim_frame_t; -typedef struct { +typedef struct +{ + /* Number of directions + 1: Basic sprite + 2: LEFT and RIGHT (enemies) + 4: UP, RIGHT, DOWN and LEFT (player, some attacks) */ + int directions; + /* First frame for each direction */ + anim_frame_t *start[]; + +} anim_t; + +typedef struct +{ /* Current frame */ anim_frame_t const *frame; /* Time elapsed */ uint16_t elapsed; + } anim_state_t; -/* Duration of an animation, in seconds (assuming linear order until end). */ -fixed_t anim_duration(anim_frame_t const *first_frame); +/* Duration of an animation, in seconds; assumed unique for all directions */ +fixed_t anim_duration(anim_t const *anim); -#define anim_duration(ff) _Generic((ff), \ - anim_frame_t *: anim_duration((void *)ff), \ - anim_frame_t **: anim_duration(*(void **)ff)) +/* Check if a frame is in an animation. */ +bool anim_in(anim_frame_t const *frame, anim_t const *anim, int index); /* Render a frame at the center position (x,y). */ void anim_frame_render(int x, int y, anim_frame_t const *frame); @@ -47,35 +62,27 @@ void anim_state_update(anim_state_t *state, fixed_t dt); /* List of animations. */ -/* Basic animations. */ -extern anim_frame_t anims_skill_hit[]; -extern anim_frame_t anims_skill_teleport[]; -extern anim_frame_t anims_skill_shock[]; -extern anim_frame_t anims_skill_judgement[]; +/* Basic skills */ +extern anim_t anims_skill_hit; +extern anim_t anims_skill_teleport; +extern anim_t anims_skill_shock; +extern anim_t anims_skill_judgement; +/* Directional skills */ +extern anim_t anims_skill_swing; +extern anim_t anims_skill_impale; +extern anim_t anims_skill_bullet; -/* Enemy animations (bidirectional). */ +/* Enemies */ +extern anim_t anims_slime_Idle; +extern anim_t anims_slime_Walking; +extern anim_t anims_slime_Hit; +extern anim_t anims_slime_Death; +extern anim_t anims_bat_Idle; +extern anim_t anims_bat_Hit; +extern anim_t anims_bat_Death; -extern anim_frame_t anims_slime_left_Idle[]; -extern anim_frame_t anims_slime_right_Idle[]; -extern anim_frame_t anims_slime_left_Walking[]; -extern anim_frame_t anims_slime_right_Walking[]; -extern anim_frame_t anims_slime_left_Hit[]; -extern anim_frame_t anims_slime_right_Hit[]; -extern anim_frame_t anims_slime_left_Death[]; -extern anim_frame_t anims_slime_right_Death[]; - -extern anim_frame_t anims_bat_left_Idle[]; -extern anim_frame_t anims_bat_right_Idle[]; -extern anim_frame_t anims_bat_left_Hit[]; -extern anim_frame_t anims_bat_right_Hit[]; -extern anim_frame_t anims_bat_left_Death[]; -extern anim_frame_t anims_bat_right_Death[]; - -/* Quadri-directional animations. */ -extern anim_frame_t *anims_player_Idle[4]; -extern anim_frame_t *anims_player_Walking[4]; -extern anim_frame_t *anims_player_Attack[4]; -extern anim_frame_t *anims_player_Hit[4]; -extern anim_frame_t *anims_skill_swing[4]; -extern anim_frame_t *anims_skill_impale[4]; -extern anim_frame_t *anims_skill_bullet[4]; +/* Player */ +extern anim_t anims_player_Idle; +extern anim_t anims_player_Walking; +extern anim_t anims_player_Attack; +extern anim_t anims_player_Hit; diff --git a/src/aoe.c b/src/aoe.c new file mode 100644 index 0000000..39f8e3c --- /dev/null +++ b/src/aoe.c @@ -0,0 +1,256 @@ +#include "comp/entity.h" +#include "comp/physical.h" +#include "comp/visible.h" +#include "comp/fighter.h" +#include "aoe.h" +#include "game.h" +#include "enemies.h" + +entity_t *aoe_make(uint16_t type, vec2 position, fixed_t lifetime) +{ + entity_t *e = entity_make(physical, visible, aoe); + + physical_t *p = getcomp(e, physical); + p->x = position.x; + p->y = position.y; + + aoe_t *aoe = getcomp(e, aoe); + aoe->type = type; + aoe->lifetime = lifetime; + aoe->hits = NULL; + aoe->hit_count = 0; + + return e; +} + +void aoe_destroy(entity_t *e) +{ + aoe_t *aoe = getcomp(e, aoe); + free(aoe->hits); +} + +entity_t *aoe_make_attack(uint16_t type, entity_t *origin, int facing) +{ + entity_t *e = aoe_make(type, physical_pos(origin), fix(0)); + if(e == NULL) + return NULL; + + fighter_t *f = getcomp(origin, fighter); + + physical_t *p = getcomp(e, physical); + visible_t *v = getcomp(e, visible); + aoe_t *aoe = getcomp(e, aoe); + + rect hitbox = { 0 }; + fixed_t distance = fix(0.625); + vec2 dir = fdir(facing); + anim_t const *anim = NULL; + bool rotate = true; + fixed_t lifetime = fix(0); + int plane = VERTICAL; + + if(type == AOE_HIT) { + anim = &anims_skill_hit; + hitbox = (rect){ -fix(4)/16, fix(3)/16, -fix(4)/16, fix(3)/16 }; + } + else if(type == AOE_SLASH) { + anim = &anims_skill_swing; + hitbox = (rect){ -fix(10)/16, fix(10)/16, -fix(8)/16, 0 }; + } + else if(type == AOE_IMPALE) { + anim = &anims_skill_impale; + hitbox = (rect){ -fix(4)/16, fix(4)/16, -fix(10)/16, 0 }; + } + else if(type == AOE_SHOCK) { + anim = &anims_skill_shock; + hitbox = (rect){ -fix(17)/16, fix(18)/16, -fix(17)/16, fix(18)/16 }; + distance = fix(0); + rotate = false; + plane = HORIZONTAL; + } + else if(type == AOE_JUDGEMENT) { + anim = &anims_skill_judgement; + hitbox = (rect){ -fix(10)/16, fix(11)/16, -fix(6)/16, fix(6)/16 }; + distance = fix(1.5); + rotate = false; + } + else if(type == AOE_BULLET) { + anim = &anims_skill_bullet; + hitbox = (rect){ -fix(8)/16, fix(8)/16, -fix(7)/16, fix(7)/16 }; + distance = fix(0.375); + lifetime = fix(999.0); + } + + p->x += fmul(distance, dir.x); + p->y += fmul(distance, dir.y); + p->facing = facing; + p->hitbox = rotate ? rect_rotate(hitbox, UP, facing) : hitbox; + + v->sprite_plane = plane; + + aoe->lifetime = lifetime ? lifetime : anim_duration(anim); + aoe->repeat_delay = 0; + aoe->origin = origin; + + if(type == AOE_HIT + || type == AOE_SLASH + || type == AOE_IMPALE + || type == AOE_JUDGEMENT) { + aoe->data.generic.strength = f->ATK; + aoe->data.generic.dir = facing; + } + else if(type == AOE_SHOCK) { + aoe->data.shock.strength = f->ATK; + aoe->data.shock.origin = physical_pos(origin); + aoe->repeat_delay = fix(0.1); + } + else if(type == AOE_BULLET) { + aoe->data.bullet.strength = f->ATK; + aoe->data.bullet.dir = facing; + aoe->data.bullet.v = fix(10.0); + aoe->data.bullet.final_v = fix(4.5); + aoe->repeat_delay = fix(0.05); + } + + visible_set_anim(e, anim, 1); + return e; +} + +/* Existing record of the area hitting this entity */ +static aoe_record_t *aoe_record(aoe_t *aoe, entity_t *target) +{ + for(int i = 0; i < aoe->hit_count; i++) { + aoe_record_t *r = &aoe->hits[i]; + if(r->entity == target) return r; + } + return NULL; +} + +static bool attack_apply(game_t *game, aoe_t *aoe, entity_t *target) +{ + fighter_t *origin_f = getcomp(aoe->origin, fighter); + fighter_t *target_f = getcomp(target, fighter); + mechanical_t *target_m = getcomp(target, mechanical); + physical_t *target_p = getcomp(target, physical); + + if(!target_f) + return false; + + /* No friendly fire */ + bool origin_is_monster = (origin_f && origin_f->identity != 0); + bool target_is_monster = (target_f->identity != 0); + if(origin_is_monster == target_is_monster || target_f->HP == 0) + return false; + + /* Dash invincibility */ + if(target_m && mechanical_dashing(target)) + return false; + + /* Knockback */ + fixed_t r = (rand() & (fix(0.125)-1)) + fix(0.375); + /* Half knockback against players */ + if(target_f->identity == 0) r /= 2; + + vec2 dir = { 0, 0 }; + int damage = 0; + + if(aoe->type == AOE_HIT + || aoe->type == AOE_SLASH + || aoe->type == AOE_IMPALE) { + dir = fdir(aoe->data.generic.dir); + damage = aoe->data.generic.strength; + } + else if(aoe->type == AOE_SHOCK) { + dir.x = target_p->x - aoe->data.shock.origin.x; + dir.y = target_p->y - aoe->data.shock.origin.y; + dir = fnormalize(dir); + damage = aoe->data.shock.strength * 3 / 2; + r /= 2; + } + else if(aoe->type == AOE_JUDGEMENT) { + r = fix(0); + damage = aoe->data.generic.strength * 5; + } + else if(aoe->type == AOE_BULLET) { + /* TODO: Sideways knockback */ + damage = aoe->data.bullet.strength * 2; + } + + /* Inflict damage */ + damage = fighter_damage(target, damage); + + /* Apply knockback */ + if(target_m) { + target_m->vdx += fmul(dir.x, fmul(r, KNOCKBACK_SPEED)); + target_m->vdy += fmul(dir.y, fmul(r, KNOCKBACK_SPEED)); + } + + /* Spawn damage particle */ + particle_damage_t *p = malloc(sizeof *p); + p->particle.type = PARTICLE_DAMAGE; + p->particle.age = 0; + p->particle.pos = (vec2){ target_p->x, target_p->y - fix(0.5) }; + p->damage = damage; + p->color = (target_f->identity == 0) ? C_RED : C_WHITE; + game_add_particle(game, &p->particle); + + return true; +} + +void aoe_apply(game_t *game, entity_t *entity, entity_t *e) +{ + bool was_hit = false; + aoe_t *aoe = getcomp(entity, aoe); + aoe_record_t *rec = aoe_record(aoe, e); + + /* Don't hit entities that have been recently hit */ + if(rec && aoe->repeat_delay == 0) return; + if(rec && aoe->lifetime > rec->lifetime - aoe->repeat_delay) return; + + if(aoe->type == AOE_HIT + || aoe->type == AOE_SLASH + || aoe->type == AOE_IMPALE + || aoe->type == AOE_SHOCK + || aoe->type == AOE_JUDGEMENT + || aoe->type == AOE_BULLET) + was_hit = attack_apply(game, aoe, e); + + if(!was_hit) return; + + /* Update record */ + if(!rec) { + size_t new_size = (aoe->hit_count + 1) * sizeof *aoe->hits; + aoe_record_t *new_hits = realloc(aoe->hits, new_size); + if(!new_hits) return; + + aoe->hits = new_hits; + rec = &new_hits[aoe->hit_count]; + rec->entity = e; + aoe->hit_count++; + } + + rec->lifetime = aoe->lifetime; +} + +void aoe_update(game_t *game, entity_t *entity, fixed_t dt) +{ + physical_t *p = getcomp(entity, physical); + aoe_t *aoe = getcomp(entity, aoe); + + if(aoe->type == AOE_BULLET) { + vec2 dir = fdir(aoe->data.bullet.dir); + p->x += fmul(fmul(aoe->data.bullet.v, dir.x), dt); + p->y += fmul(fmul(aoe->data.bullet.v, dir.y), dt); + + /* Speed update - exponential decline from initial value to final_v */ + aoe->data.bullet.v += fmul( + aoe->data.bullet.final_v - aoe->data.bullet.v, fix(0.001)); + + /* Collision of anchor with map destroys the bullet */ + rect small_box = { 0, 0, 0, 0 }; + small_box = rect_translate(small_box, physical_pos(entity)); + + if(map_collides(&game->map, small_box)) + aoe->lifetime = 0; + } +} diff --git a/src/aoe.h b/src/aoe.h new file mode 100644 index 0000000..13c8c8c --- /dev/null +++ b/src/aoe.h @@ -0,0 +1,77 @@ +//--- +// aoe: Physical entities tracking areas of effect of skills +//--- + +#pragma once + +#include "geometry.h" +#include "anim.h" + + +enum { + /* Bare-hand fist/close-contact attack */ + AOE_HIT, + /* Normal attack by a slashing weapon */ + AOE_SLASH, + /* Impaling attack using a slashing weapon */ + AOE_IMPALE, + /* Skills */ + AOE_SHOCK, + AOE_JUDGEMENT, + AOE_BULLET, + /* Spawn effect */ + EFFECT_SPAWN, +}; + +/* Record of when an area hit an entity */ +typedef struct +{ + /* Entity hit */ + entity_t *entity; + /* Lifetime of the area at the time of the hit */ + fixed_t lifetime; + +} aoe_record_t; + +/* Extra component for areas of effect */ +typedef struct +{ + /* Lifetime (s) */ + fixed_t lifetime; + /* Effect repeat delay (seconds; 0 for no repeat) */ + fixed_t repeat_delay; + /* List of entities that were hit */ + aoe_record_t *hits; + int hit_count; + /* Entity that caused the area */ + entity_t *origin; + /* Type of effect */ + uint16_t type; + /* Effect data (type-dependent) */ + union { + /* Generic attacks: source and ATK strength */ + struct { int strength; int dir; } generic; + /* EFFECT_ATTACK_SHOCK: origin and ATK strength */ + struct { int strength; vec2 origin; } shock; + /* EFFECT_ATTACK_BULLET: Magic strengh, speed parameters */ + struct { int strength; int dir; fixed_t v; fixed_t final_v; } bullet; + } data; + +} aoe_t; + +/* Create a new area of the specified type. The animation, repeat delay, origin + and type-specific data should be set manually after creation. */ +entity_t *aoe_make(uint16_t type, vec2 position, fixed_t lifetime); + +/* Free resources for an area. */ +void aoe_destroy(entity_t *aoe); + +/* Create an area for a particular attack; all attributes are initialized. */ +entity_t *aoe_make_attack(uint16_t type, entity_t *origin, int facing); + +/* Apply effect of area on entity */ +struct game; +void aoe_apply(struct game *g, entity_t *aoe, entity_t *e); + +/* Regular update, for moving areas (eg. bullet) */ +void aoe_update(struct game *g, entity_t *aoe, fixed_t dt); diff --git a/src/comp/entity.c b/src/comp/entity.c new file mode 100644 index 0000000..1144a0f --- /dev/null +++ b/src/comp/entity.c @@ -0,0 +1,69 @@ +#include "comp/entity.h" +#include "comp/physical.h" +#include "comp/visible.h" +#include "comp/mechanical.h" +#include "comp/fighter.h" +#include "aoe.h" + +/* Align up to native pointer size */ +#define ALIGN(expr) ((((expr) - 1) | (sizeof(void *) - 1)) + 1) + +entity_t *(entity_make)(uint32_t comps) +{ + /* Determine full size of entity; also store offsets of components (we + explicitly 4-align them so we don't want to compute it several times) */ + int offsets[ENTITY_COMP_CAPACITY] = { 0 }; + size_t size = 0; + int n = 0; + + if(comps & ENTITY_COMP_physical) { + offsets[n++] = size; + size = ALIGN(size + sizeof(physical_t)); + } + if(comps & ENTITY_COMP_visible) { + offsets[n++] = size; + size = ALIGN(size + sizeof(visible_t)); + } + if(comps & ENTITY_COMP_mechanical) { + offsets[n++] = size; + size = ALIGN(size + sizeof(mechanical_t)); + } + if(comps & ENTITY_COMP_fighter) { + offsets[n++] = size; + size = ALIGN(size + sizeof(fighter_t)); + } + if(comps & ENTITY_COMP_aoe) { + offsets[n++] = size; + size = ALIGN(size + sizeof(aoe_t)); + } + + /* Allocate all components together */ + entity_t *e; + size_t base_size = sizeof(*e) + n * sizeof(e->data[0]); + + e = calloc(1, base_size + size); + if(e == NULL) + return NULL; + + /* Initialize component addresses */ + e->comps = comps; + for(int i = 0; i < n; i++) + e->data[i] = (void *)e + base_size + offsets[i]; + + return e; +} + +void entity_mark_to_delete(entity_t *e) +{ + e->deleted = 1; +} + +void entity_destroy(entity_t *e) +{ + if(e->comps & ENTITY_COMP_aoe) { + aoe_destroy(e); + } + + /* Since there's only one allocation for all components, this is easy */ + free(e); +} diff --git a/src/comp/entity.h b/src/comp/entity.h new file mode 100644 index 0000000..5142af7 --- /dev/null +++ b/src/comp/entity.h @@ -0,0 +1,87 @@ +//--- +// entity: Varied objects in the game, implemented with components +// +// This game runs on an Entity-Component system. Entities uniquely identify a +// variety of game objects, ranging from players and enemies to skills' areas +// of effect, item drops, spawn regions... +// +// Entities themselves are fairly empty, and most of their properties are +// defined through components. For instance, the [physical] component assigns a +// in space and a hitbox; the [visible] component gives sprites and rendering +// properties; the [mechanical] component provides controlled motion. +//--- + +#pragma once + +#include +#include +#include + +typedef struct +{ + /* Deletion flag - deletions are delayed until end of frame to ease + management of dangling references */ + uint32_t deleted: 1; + /* Bitmask of components in that entity (see ENTITY_COMP_*) */ + uint32_t comps: 31; + /* Pointer to components; one pointer per non-zero entry in [comps], + starting from LSB to MSB */ + void *data[]; + +} entity_t; + +/* Bit field values for each type of component */ +#define ENTITY_COMP_physical 0x00000001 +#define ENTITY_COMP_visible 0x00000002 +#define ENTITY_COMP_mechanical 0x00000004 +#define ENTITY_COMP_fighter 0x00000008 +#define ENTITY_COMP_aoe 0x00000010 +/* Maximum number of components */ +#define ENTITY_COMP_CAPACITY 31 + +/* Get a component from an entity */ +static inline void *getcomp(entity_t const *e, uint32_t comp) +{ + if(__builtin_expect(!(e->comps & comp), 0)) + return NULL; + + int i = 0; + while(comp) i += (e->comps & (comp >>= 1)) != 0; + + return e->data[i]; +} +/* Write "getcomp(e, physical)" */ +#define getcomp(e, comp) ((comp ## _t *)getcomp(e, ENTITY_COMP_ ## comp)) + +/* Create an entity with a fixed set of components */ +entity_t *entity_make(uint32_t comp); + +/* Variation of entity_make() with up to 6 variable arguments */ +#define ENTITY_MAKE_ARGS1(type, ...) \ + ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS(__VA_ARGS__)) +#define ENTITY_MAKE_ARGS2(type, ...) \ + ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS1(__VA_ARGS__)) +#define ENTITY_MAKE_ARGS3(type, ...) \ + ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS2(__VA_ARGS__)) +#define ENTITY_MAKE_ARGS4(type, ...) \ + ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS3(__VA_ARGS__)) +#define ENTITY_MAKE_ARGS5(type, ...) \ + ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS4(__VA_ARGS__)) +#define ENTITY_MAKE_ARGS6(type, ...) \ + ENTITY_COMP_##type __VA_OPT__(| ENTITY_MAKE_ARGS5(__VA_ARGS__)) +#define entity_make(...) \ + entity_make(ENTITY_MAKE_ARGS6(__VA_ARGS__)) + +/* Mark an entity for deletion at the next collection step */ +void entity_mark_to_delete(entity_t *e); + +/* Destroy an entity */ +void entity_destroy(entity_t *e); + +// Useful types when finding/filtering/sorting entities + +/* Propositional predicate on entities; used for counting and filtering */ +typedef bool entity_predicate_t(entity_t const *e); + +/* Measure; used for filtering-sorting */ +typedef int entity_measure_t(entity_t const *e); diff --git a/src/comp/fighter.c b/src/comp/fighter.c new file mode 100644 index 0000000..9625f22 --- /dev/null +++ b/src/comp/fighter.c @@ -0,0 +1,33 @@ +#include "comp/entity.h" +#include "comp/fighter.h" +#include "comp/visible.h" +#include "enemies.h" +#include + +int fighter_damage(entity_t *e, int base_damage) +{ + fighter_t *f = getcomp(e, fighter); + + if(f->HP == 0) return 0; + + base_damage -= f->DEF; + if(base_damage <= 0) return 0; + + int variation = (base_damage >= 4) ? (rand() % (base_damage / 4)) : 0; + int damage = (base_damage * 7) / 8 + variation; + + if(f->HP < damage) f->HP = 0; + else f->HP -= damage; + + if(f->identity > 0) { + if(f->HP == 0) + visible_set_anim(e, enemies[f->identity]->anim_death, 4); + else + visible_set_anim(e, enemies[f->identity]->anim_hit, 3); + } + else { + visible_set_anim(e, &anims_player_Hit, 3); + } + + return damage; +} diff --git a/src/comp/fighter.h b/src/comp/fighter.h new file mode 100644 index 0000000..82c285c --- /dev/null +++ b/src/comp/fighter.h @@ -0,0 +1,38 @@ +//--- +// fighter: Component for entities that participate in fights +// +// This component gives combat statistics and leveling properties to entities. +//--- + +#pragma once + +#include "comp/entity.h" +#include "fixed.h" + +typedef struct +{ + /* Current attack's effect area */ + entity_t *current_attack; + /* Whether attack follows movement */ + uint8_t attack_follows_movement; + /* Fighter ID (0 for player) */ + uint8_t identity; + /* Combat statistics */ + uint16_t HP, ATK, DEF, HP_max; + + /* Number of hits in main attack's combo (TODO: Delegate to weapon?) */ + uint8_t combo_length; + /* Next hit to be dealt */ + uint8_t combo_next; + /* Delay until ideal time to start next hit */ + fixed_t combo_delay; + /* Skill and item cooldowns (F2..F5).The "total" field is updated when + switching skills/items. The base field is the dynamic value. */ + fixed_t actions_cooldown_total[5]; + fixed_t actions_cooldown[5]; + +} fighter_t; + +/* Damage entity for that amount of raw strength. Returns actual damage after + DES is subtracted, and randomization. */ +int fighter_damage(entity_t *e, int base_damage); diff --git a/src/comp/mechanical.c b/src/comp/mechanical.c new file mode 100644 index 0000000..6d478af --- /dev/null +++ b/src/comp/mechanical.c @@ -0,0 +1,132 @@ +#include "comp/entity.h" +#include "comp/mechanical.h" +#include "comp/physical.h" +#include "comp/fighter.h" + +// Movement functions + +void mechanical_move(entity_t *e, vec2 direction, fixed_t dt, map_t const *map) +{ + physical_t *p = getcomp(e, physical); + mechanical_t *m = getcomp(e, mechanical); + mechanical_limits_t const *limits = m->limits; + + direction = fnormalize(direction); + + /* Determine facing */ + int facing = -1; + bool horz = abs(direction.x) >= abs(direction.y); + if(direction.x == 0 && direction.y == 0) facing = -1; + else if( horz && direction.x >= 0) facing = RIGHT; + else if( horz && direction.x <= 0) facing = LEFT; + else if(!horz && direction.y <= 0) facing = UP; + else if(!horz && direction.y >= 0) facing = DOWN; + + /* Targeted speed, with two components: dash motion and walk motion */ + vec2 target = { 0, 0 }; + + if(m->dash > 0) { + /* The dash speed is set to one direction and cannot changed */ + vec2 dir = fdir(m->dash_facing); + target.x += fmul(limits->dash_speed, dir.x); + target.y += fmul(limits->dash_speed, dir.y); + } + if(facing >= 0) { + /* Walking speed can be directed anywhere, anytime */ + target.x += fmul(limits->max_speed, direction.x); + target.y += fmul(limits->max_speed, direction.y); + } + + /* Friction from environment */ + fixed_t friction_x = fmul(fmul(-target.x, limits->friction), dt); + fixed_t friction_y = fmul(fmul(-target.y, limits->friction), dt); + + /* Get there exponentially fast */ + m->vx = target.x + friction_x + m->vdx; + m->vy = target.y + friction_y + m->vdy; + + /* Round very small speeds (smaller than 1/256 tiles/s) to 0 */ + if(m->vx >= -256 && m->vx <= 255) m->vx = 0; + if(m->vy >= -256 && m->vy <= 255) m->vy = 0; + + /* Decrement dash duration or dash cooldown */ + if(m->dash > 0) { + m->dash -= dt; + if(m->dash <= 0) m->dash = -limits->dash_cooldown; + } + else if(m->dash < 0) { + m->dash += dt; + if(m->dash >= 0) m->dash = 0; + } + + if(facing >= 0) + p->facing = facing; + + fixed_t new_x = p->x + fmul(m->vx, dt); + fixed_t new_y = p->y + fmul(m->vy, dt); + rect new_hitbox = rect_translate(p->hitbox, (vec2){ new_x, new_y }); + + // TODO ECS: New collision/ejection system based on teleports + if(!map_collides(map, new_hitbox)) { + fighter_t *f = getcomp(e, fighter); + if(f && f->current_attack && f->attack_follows_movement) { + physical_t *attack = getcomp(f->current_attack, physical); + attack->x += (new_x - p->x); + attack->y += (new_y - p->y); + } + + p->x = new_x; + p->y = new_y; + } + + /* TODO: Without acceleration, the movement model is broken */ + m->vdx = fmul(m->vdx, fix(0.8)); + m->vdy = fmul(m->vdy, fix(0.8)); +} + +void mechanical_move4(entity_t *e, int direction, fixed_t dt, map_t const *map) +{ + return mechanical_move(e, fdir(direction), dt, map); +} + +void mechanical_dash(entity_t *e, int direction) +{ + mechanical_t *m = getcomp(e, mechanical); + if(m->dash != 0) + return; + + m->dash = m->limits->dash_duration; + m->dash_facing = direction; +} + +// Information functions + +bool mechanical_moving(entity_t const *e) +{ + mechanical_t *m = getcomp(e, mechanical); + return (m->vx != 0) || (m->vy != 0); +} + +bool mechanical_dashing(entity_t const *e) +{ + physical_t *p = getcomp(e, physical); + mechanical_t *m = getcomp(e, mechanical); + mechanical_limits_t const *limits = m->limits; + + /* True only if the direction of the dash movement is preserved */ + if(p->facing != m->dash_facing) + return false; + + /* True during initial propulsion */ + if(m->dash > 0) + return true; + + /* Also true as long as 1.5x over-speed is maintained */ + fixed_t cur_v2 = fmul(m->vx, m->vx) + fmul(m->vy, m->vy); + fixed_t max_v2 = fmul(limits->max_speed, limits->max_speed); + + if(m->dash < 0 && 2 * cur_v2 > 3 * max_v2) + return true; + + return false; +} diff --git a/src/comp/mechanical.h b/src/comp/mechanical.h new file mode 100644 index 0000000..884b0fd --- /dev/null +++ b/src/comp/mechanical.h @@ -0,0 +1,67 @@ +//--- +// mechanical: Component for objects with continuous motion +// +// Mechanical entities (which must also be physical) have kinematic properties +// that carry over from frame to frame, such as speed. The mechanical component +// itself only holds simulation data, and is controlled by another component +// (either player input or an AI). +//--- + +#pragma once + +#include "comp/entity.h" +#include "geometry.h" +#include "map.h" + +typedef struct +{ + /* Maximum walking speed (m/s) */ + fixed_t max_speed; + /* Friction coefficient */ + fixed_t friction; + + /* Peak dash speed (m/s) */ + fixed_t dash_speed; + /* Dash duration (s) */ + fixed_t dash_duration; + /* Dash cooldown (s) */ + fixed_t dash_cooldown; + +} mechanical_limits_t; + +typedef struct +{ + mechanical_limits_t const *limits; + + /* Position of the mechanical entity is specified in physical.x/y! */ + + /* Current speed */ + fixed_t vx, vy; + /* Disruption speed to be integrated next frame (eg. knockback) */ + fixed_t vdx, vdy; + /* Dash time remaining (if positive) or cooldown remaining (if negative) */ + fixed_t dash; + /* Dash direction */ + uint8_t dash_facing; + +} mechanical_t; + +// Movement functions + +/* Update movement for a 4-directional control (stand still if direction = -1). + Both the [mechanical] and [physical] components are affected. */ +void mechanical_move4(entity_t *e, int direction, fixed_t dt,map_t const *map); + +/* Update movement for a full-directional control (for AIs). */ +void mechanical_move(entity_t *e, vec2 direction, fixed_t dt,map_t const *map); + +/* Start dashing in the specified direction */ +void mechanical_dash(entity_t *e, int direction); + +// Information functions + +/* Check if the entity is currently moving */ +bool mechanical_moving(entity_t const *e); + +/* Check if the entity is currently dashing */ +bool mechanical_dashing(entity_t const *e); diff --git a/src/comp/physical.c b/src/comp/physical.c new file mode 100644 index 0000000..e172b26 --- /dev/null +++ b/src/comp/physical.c @@ -0,0 +1,14 @@ +#include "comp/entity.h" +#include "comp/physical.h" + +vec2 physical_pos(entity_t const *e) +{ + physical_t *p = getcomp(e, physical); + return (vec2){ p->x, p->y }; +} + +rect physical_abs_hitbox(entity_t const *e) +{ + physical_t *p = getcomp(e, physical); + return rect_translate(p->hitbox, (vec2){ p->x, p->y }); +} diff --git a/src/comp/physical.h b/src/comp/physical.h new file mode 100644 index 0000000..5167a3a --- /dev/null +++ b/src/comp/physical.h @@ -0,0 +1,29 @@ +//--- +// physical: Component for entities that exist on the map +// +// This basic component gives a position and size (hitbox) to every object on +// the map. It's a requirement for several others, including [visible] and +// [mechanical], since the position is very commonly used. +//--- + +#pragma once + +#include "comp/entity.h" +#include "geometry.h" + +typedef struct +{ + /* Current position, in absolute map coordinates */ + fixed_t x, y; + /* Hitbox on the floor, relative to (x,y) */ + rect hitbox; + /* Direction facing */ + uint8_t facing; + +} physical_t; + +/* Entity position as a vector */ +vec2 physical_pos(entity_t const *e); + +/* Hitbox offset by position */ +rect physical_abs_hitbox(entity_t const *e); diff --git a/src/comp/visible.c b/src/comp/visible.c new file mode 100644 index 0000000..0d70fa4 --- /dev/null +++ b/src/comp/visible.c @@ -0,0 +1,37 @@ +#include "comp/entity.h" +#include "comp/physical.h" +#include "comp/visible.h" + +void visible_set_anim(entity_t *e, anim_t const *anim, int priority) +{ + visible_t *v = getcomp(e, visible); + + if(priority < v->anim_priority) + return; + + int anim_index = 0; + physical_t *p = getcomp(e, physical); + + if(p && anim->directions == 2) { + anim_index = (p->facing == RIGHT); + } + else if(p && anim->directions == 4) { + anim_index = p->facing; + } + + if(anim_in(v->anim.frame, anim, anim_index)) + return; + + v->anim.frame = anim->start[anim_index]; + v->anim.elapsed = 0; + v->anim_priority = priority; +} + +void visible_update(entity_t *e, fixed_t dt) +{ + visible_t *v = getcomp(e, visible); + anim_state_update(&v->anim, dt); + + if(v->anim.frame == NULL) + v->anim_priority = 0; +} diff --git a/src/comp/visible.h b/src/comp/visible.h new file mode 100644 index 0000000..7711ba8 --- /dev/null +++ b/src/comp/visible.h @@ -0,0 +1,55 @@ +//--- +// visible: Component for visible entities with a sprite +// +// Visible entities (which must also be physical in order to have a position) +// mostly consist of 2D sprites that either lie flat on the ground or stand +// vertically. They also have a visual elevation (z) which only affects the +// display, and can drop shadows. +// +// Below is a schematic diagram of visible objects' geometry. +// +// |-_ +// | `-_ z +// | `. | y +// ^--- :-_ | < Wall |_.-` +// z | : `-_ | sprite `-_ +// | : `: x +// v--- :_.-``-: +// _.-``-_ :`-_ +// `-_ `x_ :_.-` < Floor sprite +// `-_ _.-`` +// ` +// +// The "x" marks the entity's anchor, which is the point of the object tied to +// its position (which is used for depth ordering among other things). The +// anchor is aligned with the sprite's anchor, before optionally adding a z +// displacement for elevated visible entities. +//--- + +#pragma once + +#include "comp/entity.h" +#include "geometry.h" +#include "anim.h" + +typedef struct +{ + /* Display elevation; has no influence on mechanics */ + fixed_t z; + /* Current sprite */ + anim_state_t anim; + /* Priority of current sprite (used to avoid animation conflicts) */ + uint8_t anim_priority; + /* Sprite plane (either VERTICAL or HORIZONTAL) */ + uint8_t sprite_plane; + /* Size of shadow cast (0 for no shadow; not all values are valid) */ + uint8_t shadow_size; + +} visible_t; + +/* Set the entity's animation. If the priority is strictly lower than the + current animation's priority, this operation is ignored. */ +void visible_set_anim(entity_t *e, anim_t const *anim, int priority); + +/* Regular update function */ +void visible_update(entity_t *e, fixed_t dt); diff --git a/src/enemies.c b/src/enemies.c index fae1cc0..a3f72bf 100644 --- a/src/enemies.c +++ b/src/enemies.c @@ -1,21 +1,26 @@ +#include "comp/entity.h" +#include "comp/physical.h" +#include "comp/visible.h" +#include "comp/mechanical.h" +#include "comp/fighter.h" #include "enemies.h" -#include +#include /* Declare animations for an enemy */ #define ANIMS(name, I, W, H, D) \ - .anim_idle = { anims_##name##_left_##I, anims_##name##_right_##I }, \ - .anim_walking = { anims_##name##_left_##W, anims_##name##_right_##W }, \ - .anim_hit = { anims_##name##_left_##H, anims_##name##_right_##H }, \ - .anim_death = { anims_##name##_left_##D, anims_##name##_right_##D } + .anim_idle = &anims_ ## name ## _ ## I, \ + .anim_walking = &anims_ ## name ## _ ## W, \ + .anim_hit = &anims_ ## name ## _ ## H, \ + .anim_death = &anims_ ## name ## _ ## D static enemy_t const slime = { .name = "Slime", ANIMS(slime, Idle, Walking, Hit, Death), .hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 }, .sprite = (rect){ -fix(5)/16, fix(5)/16, -fix(4)/16, fix(3)/16 }, - .movement_params = { + .limits = { .max_speed = fix(1), - .propulsion = fix(12), + .friction = fix(0.6), }, .HP = { .base = fix(10), @@ -32,6 +37,8 @@ static enemy_t const slime = { .growth = fix(1), .affinity = fix(0.5), }, + .shadow_size = 4, + .z = 0, }; static enemy_t const bat = { @@ -39,9 +46,9 @@ static enemy_t const bat = { ANIMS(bat, Idle, Idle, Hit, Death), .hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 }, .sprite = (rect){ -fix(5)/16, fix(5)/16, -fix(4)/16, fix(3)/16 }, - .movement_params = { + .limits = { .max_speed = fix(1.8), - .propulsion = fix(8), + .friction = fix(0.8), }, .HP = { .base = fix(8), @@ -58,6 +65,8 @@ static enemy_t const bat = { .growth = fix(2), .affinity = fix(1), }, + .shadow_size = 4, + .z = fix(0.75), }; enemy_t const * const enemies[] = { @@ -76,42 +85,49 @@ static int instantiate_stat(enemy_stat_t const *stat, int level) return ffloor(value); } -entity_t *enemy_spawn(int enemy_id, int level) +entity_t *enemy_make(int enemy_id, int level) { if(enemy_id < 0 || (size_t)enemy_id >= sizeof enemies / sizeof *enemies) return NULL; - entity_t *e = malloc(sizeof *e); - if(!e) return NULL; + entity_t *e = entity_make(physical, visible, mechanical, fighter); + if(e == NULL) + return NULL; enemy_t const *data = enemies[enemy_id]; /* These will probably be overridden by the caller */ - e->movement.x = 0; - e->movement.y = 0; - e->movement.vx = 0; - e->movement.vy = 0; - e->movement.facing = LEFT; - e->movement.dash = 0; - e->movement.dash_facing = LEFT; - e->movement_params = &data->movement_params; - e->anim_priority = 0; - entity_set_anim(e, data->anim_idle[0], 1); + physical_t *p = getcomp(e, physical); + p->x = fix(0); + p->y = fix(0); + p->hitbox = data->hitbox; + p->facing = LEFT; - e->hitbox = data->hitbox; - e->sprite = data->sprite; + visible_t *v = getcomp(e, visible); + v->z = data->z; + v->sprite_plane = VERTICAL; + v->shadow_size = data->shadow_size; + v->anim_priority = 0; + visible_set_anim(e, data->anim_idle, 1); - e->current_attack = NULL; - e->attack_follows_movement = false; + mechanical_t *m = getcomp(e, mechanical); + m->limits = &data->limits; + m->vx = fix(0); + m->vy = fix(0); + m->dash = fix(0); + m->dash_facing = LEFT; - e->identity = enemy_id; + /* Instantiate fighter statistics */ + fighter_t *f = getcomp(e, fighter); + memset(f, 0, sizeof *f); - e->HP_max = instantiate_stat(&data->HP, level); - e->ATK = instantiate_stat(&data->ATK, level); - e->DEF = instantiate_stat(&data->DEF, level); - - e->HP = e->HP_max; + f->identity = enemy_id; + f->HP_max = instantiate_stat(&data->HP, level); + f->ATK = instantiate_stat(&data->ATK, level); + f->DEF = instantiate_stat(&data->DEF, level); + f->HP = f->HP_max; + f->combo_length = 1; return e; } diff --git a/src/enemies.h b/src/enemies.h index 75ed82a..2d6dd9c 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -4,12 +4,14 @@ #pragma once +#include "comp/entity.h" +#include "comp/mechanical.h" #include "geometry.h" #include "anim.h" -#include "entities.h" /* enemy_stat_t: Statistic base and growth */ -typedef struct { +typedef struct +{ /* Base value at level 1 */ fixed_t base; /* Initial per-level growth */ @@ -20,19 +22,24 @@ typedef struct { } enemy_stat_t; /* enemy_t: Static enemy information */ -typedef struct { - /* Enemy name (showed in wave information) */ +typedef struct +{ + /* Enemy name (shown in wave information) */ char const *name; /* Map hitbox (represents ground size, collides with walls) */ rect hitbox; /* Sprite hitbox (interacts with attacks and effect areas) */ rect sprite; /* Idle animation, death animation, damage animation */ - anim_frame_t *anim_idle[2], *anim_walking[2], *anim_hit[2], *anim_death[2]; + anim_t *anim_idle, *anim_walking, *anim_hit, *anim_death; /* Movement parameters */ - entity_movement_params_t movement_params; + mechanical_limits_t limits; /* Statistics model */ enemy_stat_t HP, ATK, DEF; + /* Shadow size */ + uint8_t shadow_size; + /* Rendering elevation */ + fixed_t z; } enemy_t; @@ -47,4 +54,4 @@ enum { extern enemy_t const * const enemies[]; /* Create a new enemy of the given type. */ -entity_t *enemy_spawn(int enemy_id, int level); +entity_t *enemy_make(int enemy_id, int level); diff --git a/src/entities.c b/src/entities.c deleted file mode 100644 index 8e67a3c..0000000 --- a/src/entities.c +++ /dev/null @@ -1,404 +0,0 @@ -#include "entities.h" -#include "game.h" -#include "enemies.h" -#include -#include - -//--- -// Entities -//--- - -vec2 entity_pos(entity_t const *e) -{ - return (vec2){ e->movement.x, e->movement.y }; -} - -rect entity_hitbox(entity_t const *e) -{ - return rect_translate(e->hitbox, entity_pos(e)); -} - -rect entity_sprite(entity_t const *e) -{ - return rect_translate(e->sprite, entity_pos(e)); -} - -static void update_pos(entity_movement_t *m, fixed_t dt) -{ - m->x += fmul(m->vx, dt); - m->y += fmul(m->vy, dt); -} - -void entity_dash(entity_t *e, int direction) -{ - entity_movement_params_t const *params = e->movement_params; - entity_movement_t *move = &e->movement; - if(move->dash != 0) return; - - move->dash = params->dash_duration; - move->dash_facing = direction; -} - -bool entity_dashing(entity_t const *e) -{ - entity_movement_t const *move = &e->movement; - entity_movement_params_t const *params = e->movement_params; - - /* True only if the direction of the dash movement is preserved */ - if(move->facing != move->dash_facing) - return false; - - /* True during initial propulsion */ - if(move->dash > 0) - return true; - - /* Also true as long as 1.5x over-speed is maintained */ - fixed_t cur_v2 = fmul(move->vx, move->vx) + fmul(move->vy, move->vy); - fixed_t max_v2 = fmul(params->max_speed, params->max_speed); - - if(move->dash < 0 && 2 * cur_v2 > 3 * max_v2) - return true; - - return false; -} - -entity_movement_t entity_move(entity_t *e, vec2 direction, fixed_t dt) -{ - entity_movement_params_t const *params = e->movement_params; - entity_movement_t next = e->movement; - - direction = fnormalize(direction); - - /* Determine facing */ - int facing = -1; - bool horz = abs(direction.x) >= abs(direction.y); - if(direction.x == 0 && direction.y == 0) facing = -1; - else if( horz && direction.x >= 0) facing = RIGHT; - else if( horz && direction.x <= 0) facing = LEFT; - else if(!horz && direction.y <= 0) facing = UP; - else if(!horz && direction.y >= 0) facing = DOWN; - - /* Targeted speed */ - vec2 target = { 0, 0 }; - - /* The dash speed is set to one direction and cannot changed */ - if(next.dash > 0) { - vec2 dir = fdir(next.dash_facing); - target.x += fmul(params->dash_speed, dir.x); - target.y += fmul(params->dash_speed, dir.y); - } - - /* Walking speed can be directed anywhere, anytime */ - if(facing >= 0) { - next.facing = facing; - target.x += fmul(params->max_speed, direction.x); - target.y += fmul(params->max_speed, direction.y); - } - - /* Get there exponentially fast */ - next.vx += fmul(target.x - next.vx, fmul(params->propulsion, dt)); - next.vy += fmul(target.y - next.vy, fmul(params->propulsion, dt)); - - /* Round very small speeds (smaller than 1/256 tiles/s) to 0 */ - if(next.vx >= -256 && next.vx <= 255) next.vx = 0; - if(next.vy >= -256 && next.vy <= 255) next.vy = 0; - - /* Decrement dash duration or dash cooldown */ - if(next.dash > 0) { - next.dash -= dt; - if(next.dash <= 0) next.dash = -params->dash_cooldown; - } - else if(next.dash < 0) { - next.dash += dt; - if(next.dash >= 0) next.dash = 0; - } - - update_pos(&next, dt); - return next; -} - -entity_movement_t entity_move4(entity_t *e, int direction, fixed_t dt) -{ - return entity_move(e, fdir(direction), dt); -} - -bool entity_moving(entity_t const *e) -{ - return (e->movement.vx != 0) || (e->movement.vy != 0); -} - -void entity_set_normal_anim(entity_t *e, anim_frame_t *frame, int priority) -{ - if(priority < e->anim_priority) return; - - e->anim.frame = frame; - e->anim.elapsed = 0; - e->anim_priority = priority; -} - -void entity_set_directional_anim(entity_t *e, anim_frame_t *frame_dirs[], - int priority) -{ - if(e->movement.facing >= 4) return; - entity_set_normal_anim(e, frame_dirs[e->movement.facing], priority); -} - -int entity_damage(entity_t *e, int base_damage) -{ - if(e->HP == 0) return 0; - - base_damage -= e->DEF; - if(base_damage <= 0) return 0; - - int variation = (base_damage >= 4) ? (rand() % (base_damage / 4)) : 0; - int damage = (base_damage * 7) / 8 + variation; - - if(e->HP < damage) e->HP = 0; - else e->HP -= damage; - - if(e->identity > 0) { - int index = (e->movement.facing == RIGHT); - if(e->HP == 0) - entity_set_anim(e, enemies[e->identity]->anim_death[index], 4); - else - entity_set_anim(e, enemies[e->identity]->anim_hit[index], 3); - } - else { - entity_set_anim(e, anims_player_Hit[e->movement.facing], 3); - } - - return damage; -} - -//--- -// Effect areas -//--- - -effect_area_t *effect_area_new(uint16_t type) -{ - effect_area_t *ea = malloc(sizeof *ea); - if(!ea) return NULL; - - ea->type = type; - ea->hits = NULL; - ea->hit_count = 0; - - return ea; -} - -effect_area_t *effect_area_new_attack(uint16_t type, entity_t *e, int facing) -{ - effect_area_t *area = effect_area_new(type); - if(!area) return NULL; - - rect hitbox = { 0 }; - fixed_t distance = fix(0.625); - vec2 dir = fdir(facing); - vec2 anchor = rect_center(entity_sprite(e)); - anim_frame_t *anim = NULL; - bool rotate = true; - fixed_t lifetime = fix(0); - - if(type == EFFECT_ATTACK_HIT) { - anim = anims_skill_hit; - hitbox = (rect){ -fix(4)/16, fix(3)/16, -fix(4)/16, fix(3)/16 }; - } - else if(type == EFFECT_ATTACK_SLASH) { - anim = anims_skill_swing[facing]; - hitbox = (rect){ -fix(10)/16, fix(10)/16, -fix(8)/16, 0 }; - if(facing == UP || facing == DOWN) - anchor = entity_pos(e); - } - else if(type == EFFECT_ATTACK_IMPALE) { - anim = anims_skill_impale[facing]; - hitbox = (rect){ -fix(4)/16, fix(4)/16, -fix(10)/16, 0 }; - if(facing == UP || facing == DOWN) - anchor = entity_pos(e); - } - else if(type == EFFECT_ATTACK_SHOCK) { - anim = anims_skill_shock; - hitbox = (rect){ -fix(17)/16, fix(18)/16, -fix(17)/16, fix(18)/16 }; - anchor = entity_pos(e); - distance = fix(0); - rotate = false; - } - else if(type == EFFECT_ATTACK_JUDGEMENT) { - anim = anims_skill_judgement; - hitbox = (rect){ -fix(10)/16, fix(11)/16, -fix(6)/16, fix(6)/16 }; - anchor = entity_pos(e); - distance = fix(1.5); - rotate = false; - } - else if(type == EFFECT_ATTACK_BULLET) { - anim = anims_skill_bullet[facing]; - hitbox = (rect){ -fix(8)/16, fix(8)/16, -fix(7)/16, fix(7)/16 }; - distance = fix(0.375); - anchor = entity_pos(e); - lifetime = fix(999.0); - } - - area->sprite = rotate ? rect_rotate(hitbox, UP, facing) : hitbox; - area->anchor = anchor; - area->anchor.x += fmul(distance, dir.x); - area->anchor.y += fmul(distance, dir.y); - area->lifetime = lifetime ? lifetime : anim_duration(anim); - area->repeat_delay = 0; - area->origin = e; - - if(type == EFFECT_ATTACK_HIT - || type == EFFECT_ATTACK_SLASH - || type == EFFECT_ATTACK_IMPALE - || type == EFFECT_ATTACK_JUDGEMENT) { - area->data.generic.strength = e->ATK; - area->data.generic.dir = facing; - } - else if(type == EFFECT_ATTACK_SHOCK) { - area->data.shock.strength = e->ATK; - area->data.shock.origin = entity_pos(e); - area->repeat_delay = fix(0.1); - } - else if(type == EFFECT_ATTACK_BULLET) { - area->data.bullet.strength = e->ATK; - area->data.bullet.dir = facing; - area->data.bullet.v = fix(10.0); - area->data.bullet.final_v = fix(4.5); - area->repeat_delay = fix(0.05); - } - - effect_area_set_anim(area, anim); - return area; -} - -void effect_area_set_anim(effect_area_t *ea, anim_frame_t *frame) -{ - ea->anim.frame = frame; - ea->anim.elapsed = 0; -} - -/* Existing record of the area hitting this entity */ -static effect_area_record_t *effect_record(effect_area_t *ea, entity_t *e) -{ - for(int i = 0; i < ea->hit_count; i++) { - effect_area_record_t *rec = &ea->hits[i]; - if(rec->entity == e) return rec; - } - return NULL; -} - -static bool attack_apply(game_t *game, effect_area_t *ea, entity_t *e) -{ - /* No friendly fire */ - bool origin_is_monster = (ea->origin->identity != 0); - bool target_is_monster = (e->identity != 0); - if(origin_is_monster == target_is_monster || e->HP == 0) return false; - - /* Dash invincibility */ - if(entity_dashing(e)) return false; - - /* Knockback */ - fixed_t r = (rand() & (fix(0.25)-1)) + fix(0.875); - /* Half knockback against players */ - if(e->identity == 0) r /= 2; - - vec2 dir = { 0, 0 }; - int damage = 0; - - if(ea->type == EFFECT_ATTACK_HIT - || ea->type == EFFECT_ATTACK_SLASH - || ea->type == EFFECT_ATTACK_IMPALE) { - dir = fdir(ea->data.generic.dir); - damage = ea->data.generic.strength; - } - else if(ea->type == EFFECT_ATTACK_SHOCK) { - dir.x = e->movement.x - ea->data.shock.origin.x; - dir.y = e->movement.y - ea->data.shock.origin.y; - dir = fnormalize(dir); - damage = ea->data.shock.strength * 3 / 2; - r /= 2; - } - else if(ea->type == EFFECT_ATTACK_JUDGEMENT) { - r = fix(0); - damage = ea->data.generic.strength * 5; - } - else if(ea->type == EFFECT_ATTACK_BULLET) { - /* TODO: Sideways knockback */ - damage = ea->data.bullet.strength * 2; - } - - /* Inflict damage */ - damage = entity_damage(e, damage); - - /* Apply knockback */ - e->movement.vx += fmul(dir.x, fmul(r, KNOCKBACK_SPEED)); - e->movement.vy += fmul(dir.y, fmul(r, KNOCKBACK_SPEED)); - - /* Spawn damage particle */ - particle_damage_t *p = malloc(sizeof *p); - p->particle.type = PARTICLE_DAMAGE; - p->particle.age = 0; - p->particle.pos = (vec2){ e->movement.x, e->movement.y - fix(0.5) }; - p->damage = damage; - p->color = (e->identity == 0) ? C_RED : C_WHITE; - game_add_particle(game, &p->particle); - - return true; -} - -void effect_area_apply(game_t *game, effect_area_t *ea, entity_t *e) -{ - bool was_hit = false; - effect_area_record_t *rec = effect_record(ea, e); - - /* Don't hit entities that have been recently hit */ - if(rec && ea->repeat_delay == 0) return; - if(rec && ea->lifetime > rec->lifetime - ea->repeat_delay) return; - - if(ea->type == EFFECT_ATTACK_HIT - || ea->type == EFFECT_ATTACK_SLASH - || ea->type == EFFECT_ATTACK_IMPALE - || ea->type == EFFECT_ATTACK_SHOCK - || ea->type == EFFECT_ATTACK_JUDGEMENT - || ea->type == EFFECT_ATTACK_BULLET) - was_hit = attack_apply(game, ea, e); - - if(!was_hit) return; - - /* Update record */ - if(!rec) { - size_t new_size = (ea->hit_count + 1) * sizeof *ea->hits; - effect_area_record_t *new_hits = realloc(ea->hits, new_size); - if(!new_hits) return; - - ea->hits = new_hits; - rec = &new_hits[ea->hit_count]; - rec->entity = e; - ea->hit_count++; - } - - rec->lifetime = ea->lifetime; -} - -bool effect_area_is_background(effect_area_t const *ea) -{ - return ea->type == EFFECT_ATTACK_SHOCK; -} - -void effect_area_update(game_t *game, effect_area_t *ea, fixed_t dt) -{ - if(ea->type == EFFECT_ATTACK_BULLET) { - vec2 dir = fdir(ea->data.bullet.dir); - ea->anchor.x += fmul(fmul(ea->data.bullet.v, dir.x), dt); - ea->anchor.y += fmul(fmul(ea->data.bullet.v, dir.y), dt); - - /* Speed update - exponential decline from initial value to final_v */ - ea->data.bullet.v += fmul(ea->data.bullet.final_v - ea->data.bullet.v, - fix(0.001)); - - /* Collision of anchor with map destroys the bullet */ - rect small_box = { 0, 0, 0, 0 }; - small_box = rect_translate(small_box, ea->anchor); - - if(map_collides(&game->map, small_box)) - ea->lifetime = 0; - } -} diff --git a/src/entities.h b/src/entities.h deleted file mode 100644 index 65bb383..0000000 --- a/src/entities.h +++ /dev/null @@ -1,204 +0,0 @@ -//--- -// entities: Objects that move around the map -//--- - -#pragma once - -#include "geometry.h" -#include "anim.h" - -struct effect_area; - -//--- -// Agents -//--- - -typedef struct { - /* Maximum walking speed (m/s) */ - fixed_t max_speed; - /* Rate a which target speed is approached (1/s) */ - fixed_t propulsion; - - /* Peak dash speed (m/s) */ - fixed_t dash_speed; - /* Dash duration (s) */ - fixed_t dash_duration; - /* Dash cooldown (s) */ - fixed_t dash_cooldown; - -} entity_movement_params_t; - -typedef struct { - /* Current position */ - fixed_t x, y; - /* Current speed */ - fixed_t vx, vy; - /* Dash time remaining (if positive) or cooldown remaining (if negative) */ - fixed_t dash; - /* Direction currently facing */ - uint8_t facing; - /* Dash direction */ - uint8_t dash_facing; - -} entity_movement_t; - -typedef struct { - /* Number of hits in the main combo (TODO: delegate to weapon?) */ - uint8_t combo_length; - /* Next hit to be dealt */ - uint8_t combo_next; - /* Delay until ideal time to start next hit */ - fixed_t combo_delay; - /* Skill and item cooldowns (F2..F5).The "total" field is updated when - switching skills/items. The base field is the dynamic value. */ - fixed_t actions_cooldown_total[5]; - fixed_t actions_cooldown[5]; - -} entity_player_t; - -/* Alive agents with hitboxes and movement control */ -typedef struct { - /* Map hitbox, centered around (movement.x, movement.y) */ - rect hitbox; - /* Sprite hitbox, centered similarly */ - rect sprite; - /* Current cinematic situation */ - entity_movement_t movement; - /* Cinematic parameters */ - entity_movement_params_t const *movement_params; - /* Player-only parameters */ - entity_player_t const *player; - /* Animated image and state */ - anim_state_t anim; - /* Current attack's effect area */ - struct effect_area *current_attack; - /* Animation priority (to improve legibility) */ - uint8_t anim_priority; - /* Whether attack follows movement */ - uint8_t attack_follows_movement; - /* Enemy ID (0 for player) */ - uint8_t identity; - /* Combat statistics */ - uint16_t HP, ATK, DEF, HP_max; - /* Time left until nexth pathfinding run (ms) */ -// uint8_t pathfind_delay; - -} entity_t; - -/* Entity position */ -vec2 entity_pos(entity_t const *e); - -/* Entity hitbox, accounting for position */ -rect entity_hitbox(entity_t const *e); - -/* Entity sprite, accounting for position */ -rect entity_sprite(entity_t const *e); - -/* Update walk/sprint movement (stand still if direction == -1); this cinematic - state can be submitted for collisions and may or may not be accpeted */ -entity_movement_t entity_move4(entity_t *e, int direction, fixed_t dt); - -/* Update walk/sprint movement in any direction (for IAs) */ -entity_movement_t entity_move(entity_t *e, vec2 direction, fixed_t dt); - -/* Check if the entity is currently moving */ -bool entity_moving(entity_t const *e); - -/* Start dashing in the set direction */ -void entity_dash(entity_t *e, int direction); - -/* Check if the entity is currently dashing */ -bool entity_dashing(entity_t const *e); - -/* Set entity animation */ -void entity_set_normal_anim(entity_t *e, anim_frame_t *frame, int priority); -void entity_set_directional_anim(entity_t *e, anim_frame_t *frame_dirs[], - int priority); - -#define entity_set_anim(e, frame, priority) _Generic((frame), \ - anim_frame_t *: entity_set_normal_anim(e, (void *)frame, priority), \ - anim_frame_t **: entity_set_directional_anim(e, (void *)frame, priority)) - -/* Damage entity for that amount of raw strength. Returns actual damage after - DES is subtracted, and randomization. */ -int entity_damage(entity_t *e, int base_damage); - -//--- -// Effect areas -//--- - -enum { - /* Bare-hand fist/close-contact attack */ - EFFECT_ATTACK_HIT, - /* Normal attack by a slashing weapon */ - EFFECT_ATTACK_SLASH, - /* Impaling attack using a slashing weapon */ - EFFECT_ATTACK_IMPALE, - /* Skills */ - EFFECT_ATTACK_SHOCK, - EFFECT_ATTACK_JUDGEMENT, - EFFECT_ATTACK_BULLET, - /* Monster spawning */ - EFFECT_SPAWN, -}; - -/* Record of when an effect area hit an entity */ -typedef struct { - /* Entity hit */ - entity_t *entity; - /* Lifetime of the area at that time */ - fixed_t lifetime; - -} effect_area_record_t; - -/* Effect area */ -typedef struct effect_area { - /* Area sprite (used as a hitbox) */ - rect sprite; - /* Position of center */ - vec2 anchor; - /* Lifetime (s) */ - fixed_t lifetime; - /* Animated image and state */ - anim_state_t anim; - /* Effect repeat delay (s; 0 for no repeat) */ - fixed_t repeat_delay; - /* List of entities that were hit */ - effect_area_record_t *hits; - int hit_count; - /* Entity that caused the area */ - entity_t *origin; - /* Type of effect */ - uint16_t type; - /* Effect data (type-dependent) */ - union { - /* Generic attacks: source and ATK strength */ - struct { int strength; int dir; } generic; - /* EFFECT_ATTACK_SHOCK: origin and ATK strength */ - struct { int strength; vec2 origin; } shock; - /* EFFECT_ATTACK_BULLET: Magic strengh, speed parameters */ - struct { int strength; int dir; fixed_t v; fixed_t final_v; } bullet; - } data; - -} effect_area_t; - -/* Create a new effect area of the specified type. - * sprite, anchor, lifetime, anim, repeat_delay, and data should be set. */ -effect_area_t *effect_area_new(uint16_t type); - -/* Create an effect area for a particular attack - All additional parameters are set by this function. */ -effect_area_t *effect_area_new_attack(uint16_t type, entity_t *e, int facing); - -/* Set area animation */ -void effect_area_set_anim(effect_area_t *ea, anim_frame_t *frame); - -/* Apply effect of area on entity */ -struct game; -void effect_area_apply(struct game *g, effect_area_t *ea, entity_t *e); - -/* Whether the area should be rendered in background (floor level) */ -bool effect_area_is_background(effect_area_t const *ea); - -/* Regular update, for moving areas (eg. bullet) */ -void effect_area_update(struct game *g, effect_area_t *ea, fixed_t dt); diff --git a/src/game.c b/src/game.c index 074be18..eb10dda 100644 --- a/src/game.c +++ b/src/game.c @@ -2,6 +2,11 @@ #include "util.h" #include "enemies.h" +#include "comp/fighter.h" +#include "comp/physical.h" +#include "comp/visible.h" +#include "aoe.h" + #include bool game_load(game_t *g, level_t *level) @@ -19,7 +24,7 @@ bool game_load(game_t *g, level_t *level) for(int y = 0; y < m->height; y++) for(int x = 0; x < m->width; x++) { - struct tile *t = map_tile(m, x, y); + tile_t *t = map_tile(m, x, y); t->base = level->map->tiles[level->map->width * y + x].base; t->decor = level->map->tiles[level->map->width * y + x].decor; t->solid = (t->base == 0) || (t->base >= 16); @@ -33,8 +38,6 @@ bool game_load(game_t *g, level_t *level) g->entities = NULL; g->entity_count = 0; - g->effect_areas = NULL; - g->effect_area_count = 0; g->particles = NULL; g->particle_count = 0; @@ -53,16 +56,19 @@ void game_unload(game_t *g) free(g->map.tiles); for(int i = 0; i < g->entity_count; i++) - free(g->entities[i]); + entity_destroy(g->entities[i]); free(g->entities); - for(int i = 0; i < g->effect_area_count; i++) - free(g->effect_areas[i]); - free(g->effect_areas); + g->entities = NULL; + g->entity_count = 0; + // TODo ECS: Remove particles for(int i = 0; i < g->particle_count; i++) free(g->particles[i]); free(g->particles); + + g->particles = NULL; + g->particle_count = 0; } level_wave_t const *game_current_wave(game_t const *g) @@ -97,97 +103,79 @@ void game_add_entity(game_t *g, entity_t *entity) g->entity_count++; } -void game_spawn_entity(game_t *g, entity_t *e, vec2 pos) +void game_spawn_entity(game_t *g, entity_t *e) { game_add_entity(g, e); - e->movement.x = pos.x; - e->movement.y = pos.y; - if(e->current_attack) return; + fighter_t *f = getcomp(e, fighter); + if(!f || f->current_attack) return; /* Teleport hitbox */ rect hitbox = { -fix(8)/16, fix(7)/16, -fix(20)/16, fix(4)/16, }; - effect_area_t *area = effect_area_new(EFFECT_SPAWN); - area->sprite = hitbox; - area->anchor = entity_pos(e); - area->lifetime = anim_duration(anims_skill_teleport); - area->repeat_delay = 0; - area->origin = e; - effect_area_set_anim(area, anims_skill_teleport); - game_add_effect_area(g, area); + entity_t *spawn = aoe_make(EFFECT_SPAWN, physical_pos(e), fix(0)); - e->current_attack = area; - e->attack_follows_movement = true; + getcomp(spawn, physical)->hitbox = hitbox; + + aoe_t *aoe = getcomp(spawn, aoe); + aoe->lifetime = anim_duration(&anims_skill_teleport); + aoe->repeat_delay = 0; + aoe->origin = e; + + visible_set_anim(spawn, &anims_skill_teleport, 2); + game_add_entity(g, spawn); + + f->current_attack = spawn; + f->attack_follows_movement = true; } /* Remove an entity and rearrange the array. */ static void game_remove_entity(game_t *g, int i) { if(i < 0 || i >= g->entity_count) return; - - entity_t *e = g->entities[i]; - if(e->current_attack) { - /* Kill the effect area */ - e->current_attack->lifetime = fix(0); - } - - free(g->entities[i]); + entity_destroy(g->entities[i]); g->entities[i] = g->entities[--g->entity_count]; } void game_remove_dead_entities(game_t *g) { - int i = 0; - while(i < g->entity_count) { + for(int i = 0; i < g->entity_count; i++) { entity_t *e = g->entities[i]; - if(e->HP == 0 && !e->anim.frame && e->identity != 0) - game_remove_entity(g, i); - else i++; - } -} + /* First remove dead fighters */ + fighter_t *f = getcomp(e, fighter); + visible_t *v = getcomp(e, visible); + bool anim_finished = !v || (v->anim.frame == NULL); -void game_add_effect_area(game_t *g, effect_area_t *ea) -{ - size_t new_size = (g->effect_area_count + 1) * sizeof *g->effect_areas; - effect_area_t **new_effect_areas = realloc(g->effect_areas, new_size); - if(!new_effect_areas) return; + if(f && f->HP == 0 && f->identity != 0 && anim_finished) { + /* Disown remaining areas of effect */ + fighter_t *f = getcomp(e, fighter); + if(f->current_attack) { + aoe_t *aoe = getcomp(f->current_attack, aoe); + aoe->origin = NULL; + } + entity_mark_to_delete(e); + } - g->effect_areas = new_effect_areas; - g->effect_areas[g->effect_area_count] = ea; - g->effect_area_count++; -} - -/* Remove an effect area and rearrange the array. */ -static void game_remove_effect_area(game_t *g, int i) -{ - if(i < 0 || i >= g->effect_area_count) return; - - effect_area_t *ea = g->effect_areas[i]; - if(ea->origin && ea->origin->current_attack == ea) { - ea->origin->current_attack = NULL; - ea->origin->attack_follows_movement = false; + /* Then remove areas of effect with expired lifetime */ + aoe_t *aoe = getcomp(e, aoe); + if(aoe && aoe->lifetime <= 0) { + /* Notify origin of area removal */ + if(aoe->origin) { + fighter_t *f = getcomp(aoe->origin, fighter); + f->current_attack = NULL; + f->attack_follows_movement = false; + } + entity_mark_to_delete(e); + } } - free(g->effect_areas[i]->hits); - free(g->effect_areas[i]); - g->effect_areas[i] = g->effect_areas[--g->effect_area_count]; - - /* Don't realloc, we'll likely add new areas soon enough and space will be - reclaimed as needed at that time. Don't need to add heap work now. */ -} - -/* Remove all dead effect areas */ -static void game_remove_dead_effect_areas(game_t *g) -{ int i = 0; - while(i < g->effect_area_count) { - effect_area_t *ea = g->effect_areas[i]; - - if(ea->lifetime <= 0) game_remove_effect_area(g, i); + while(i < g->entity_count) { + if(g->entities[i]->deleted) + game_remove_entity(g, i); else i++; } } @@ -215,9 +203,61 @@ static void game_remove_particle(game_t *g, int i) } //--- -// Interacting with game elements +// Generic entity functions //--- +int game_count_entities(game_t const *g, entity_predicate_t *predicate) +{ + int total = 0; + for(int i = 0; i < g->entity_count; i++) + total += (predicate(g->entities[i]) != 0); + return total; +} + +/* Not re-entrant, but we can deal with that */ +static entity_measure_t *gse_measure = NULL; +static entity_t **gse_entities = NULL; + +static int gse_compare(const void *ptr1, const void *ptr2) +{ + int i1 = *(uint16_t *)ptr1; + int i2 = *(uint16_t *)ptr2; + return gse_measure(gse_entities[i1]) - gse_measure(gse_entities[i2]); +} + +int game_sort_entities(game_t const *g, entity_measure_t *measure, + uint16_t **result) +{ + int count = 0; + *result = NULL; + + for(int i = 0; i < g->entity_count; i++) + count += (measure(g->entities[i]) >= 0); + if(count == 0) + return 0; + + /* Initialize array with matching entities in storage order */ + uint16_t *array = malloc(count * sizeof *array); + *result = array; + if(array == NULL) + return -1; + + for(int i=0, j=0; i < g->entity_count; i++) { + if(measure(g->entities[i]) >= 0) + array[j++] = i; + } + + /* Sort and return */ + gse_measure = measure; + gse_entities = g->entities; + qsort(array, count, sizeof *array, gse_compare); + + return count; +} + +#if 0 +// TODO ECS: New collision/ejection system based on teleports + void game_try_move_entity(game_t *g, entity_t *e, entity_movement_t const * next) { @@ -249,6 +289,7 @@ void game_try_move_entity(game_t *g, entity_t *e, m->dash = next->dash; } } +#endif //--- // Per-frame update functions @@ -263,9 +304,12 @@ void game_spawn_enemies(game_t *g) level_wave_spawn_t *s = &wave->enemies[i]; if(s->time > g->time_wave) break; - entity_t *e = enemy_spawn(s->identity, s->level); - vec2 pos = { fix(s->x) + fix(0.5), fix(s->y) + fix(0.5) }; - game_spawn_entity(g, e, pos); + entity_t *e = enemy_make(s->identity, s->level); + physical_t *p = getcomp(e, physical); + p->x = fix(s->x) + fix(0.5); + p->y = fix(s->y) + fix(0.5); + + game_spawn_entity(g, e); g->wave_spawned++; } } @@ -274,37 +318,37 @@ void game_update_animations(game_t *g, fixed_t dt) { for(int i = 0; i < g->entity_count; i++) { entity_t *e = g->entities[i]; - anim_state_update(&e->anim, dt); - if(e->anim.frame == NULL) - e->anim_priority = 0; - } - for(int i = 0; i < g->effect_area_count; i++) { - effect_area_t *ea = g->effect_areas[i]; - anim_state_update(&ea->anim, dt); + if(getcomp(e, visible)) + visible_update(e, dt); } } -void game_update_effect_areas(game_t *g, fixed_t dt) +void game_update_aoes(game_t *g, fixed_t dt) { - for(int i = 0; i < g->effect_area_count; i++) { - effect_area_t *ea = g->effect_areas[i]; + for(int i = 0; i < g->entity_count; i++) { + entity_t *e = g->entities[i]; + aoe_t *aoe = getcomp(e, aoe); + if(aoe == NULL) + continue; /* Movement and collisions, when relevant */ - effect_area_update(g, ea, dt); + aoe_update(g, e, dt); - rect hitbox = rect_translate(ea->sprite, ea->anchor); + rect hitbox = physical_abs_hitbox(e); + // TODO ECS: Move collisions in a proper system + // TODO ECS: Quadratic collision check is a no-no for high performance for(int i = 0; i < g->entity_count; i++) { - entity_t *e = g->entities[i]; - if(rect_collide(hitbox, entity_sprite(e))) - effect_area_apply(g, ea, e); + entity_t *target = g->entities[i]; + physical_t *p = getcomp(target, physical); + + if(p && rect_collide(hitbox, rect_translate(p->hitbox, + (vec2){ p->x, p->y }))) + aoe_apply(g, e, target); } - - ea->lifetime -= dt; + aoe->lifetime -= dt; } - - game_remove_dead_effect_areas(g); } void game_update_particles(game_t *g, fixed_t dt) @@ -320,32 +364,18 @@ void game_update_particles(game_t *g, fixed_t dt) /* Spawn dash particles */ for(int i = 0; i < g->entity_count; i++) { entity_t *e = g->entities[i]; + mechanical_t *m = getcomp(e, mechanical); - if(entity_dashing(e)) { + if(m && mechanical_dashing(e)) { particle_dash_t *p = malloc(sizeof *p); p->particle.type = PARTICLE_DASH; p->particle.age = 0; - p->particle.pos = entity_pos(e); + p->particle.pos = physical_pos(e); game_add_particle(g, &p->particle); } } } -/* Compare the y values of two entities. */ -static int game_sort_entities_compare(void const *p1, void const *p2) -{ - entity_t const *e1 = *(entity_t const **)p1; - entity_t const *e2 = *(entity_t const **)p2; - - return e1->movement.y - e2->movement.y; -} - -void game_sort_entities(game_t *g) -{ - heap_sort(g->entities, g->entity_count, sizeof *g->entities, - game_sort_entities_compare); -} - /* Compare the y values of two particles. */ static int game_sort_particles_compare(void const *v1, void const *v2) { diff --git a/src/game.h b/src/game.h index 95f841b..93c9561 100644 --- a/src/game.h +++ b/src/game.h @@ -5,12 +5,13 @@ #pragma once #include "map.h" -#include "entities.h" #include "render.h" #include "level.h" #include "pathfinding.h" #include "particles.h" +#include "comp/entity.h" + typedef struct game { /* The map's coordinate system is the primary coordinate system in all of this game's code */ @@ -27,16 +28,13 @@ typedef struct game { int entity_count; /* Player; this must be one of the entities loaded in the game */ entity_t *player; - /* List of effect areas */ - effect_area_t **effect_areas; - int effect_area_count; /* List of particles */ particle_t **particles; int particle_count; /* Field of movement to reach the player (used by most enemy AIs) */ pfg_all2one_t paths_to_player; - /* Level begin played */ + /* Level being played */ level_t *level; /* Current wave, number of enemies spawned in wave, time spent in wave */ int wave; @@ -58,31 +56,34 @@ level_wave_t const *game_current_wave(game_t const *g); void game_next_wave(game_t *g); //--- -// Adding dynamic game elements +// Managing dynamic game elements //--- /* Add an entity to the game (takes ownership; e will be freed). */ void game_add_entity(game_t *g, entity_t *e); -/* Add an effect area to the game (takes ownership; ea will be freed). */ -void game_add_effect_area(game_t *g, effect_area_t *ea); - /* Add a particle to the game (takes ownership; p will be freed) */ void game_add_particle(game_t *g, particle_t *p); -/* Like game_add_entity() at a specific position, but with a visual effect */ -void game_spawn_entity(game_t *g, entity_t *e, vec2 pos); +/* Like game_add_entity(), but with a visual effect */ +void game_spawn_entity(game_t *g, entity_t *e); + +/* Remove dead entities. */ +void game_remove_dead_entities(game_t *g); //--- -// Interacting with game elements +// Generic entity functions //--- -/* Try to move an entity at the specified next-frame movement data. The data is - applied if valid (no collisions). Otherwise the entity does not move and - only some data is updated. The data is obtained by entity_move() or related - functions. */ -void game_try_move_entity(game_t *g, entity_t *e, - entity_movement_t const *next_movement); +/* Count entities satisfying the provided predicate. */ +int game_count_entities(game_t const *g, entity_predicate_t *predicate); + +/* Filter and sort entities. The entities are sorted by increasing measure, + with negative measures filtering entities out. Returns an array of entity + indices (within [g->entities]) to be freed with free() in [*result], while + returning the number of matching entities. If 0, *result is NULL. */ +int game_sort_entities(game_t const *g, entity_measure_t *measure, + uint16_t **result); //--- // Per-frame update functions @@ -95,16 +96,10 @@ void game_spawn_enemies(game_t *g); void game_update_animations(game_t *g, fixed_t dt); /* Update all effect areas and apply their effects. */ -void game_update_effect_areas(game_t *g, fixed_t dt); +void game_update_aoes(game_t *g, fixed_t dt); /* Update all particles and remove the oldest ones. */ void game_update_particles(game_t *g, fixed_t dt); -/* Sort entities by increasing y position (which is rendering order). */ -void game_sort_entities(game_t *g); - /* Sort particles by increasing y position */ void game_sort_particles(game_t *g); - -/* Remove dead entities. */ -void game_remove_dead_entities(game_t *g); diff --git a/src/main.c b/src/main.c index f9caf41..c73afed 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,12 @@ +#include "comp/entity.h" +#include "comp/physical.h" +#include "comp/visible.h" +#include "comp/mechanical.h" +#include "comp/fighter.h" + #include "anim.h" +#include "aoe.h" #include "enemies.h" -#include "entities.h" #include "game.h" #include "geometry.h" #include "level.h" @@ -15,6 +21,7 @@ #include #include #include +#include #include #include @@ -38,7 +45,7 @@ int main(void) usb_open(interfaces, GINT_CALL_NULL); game_t game = { 0 }; - map_t *m = &game.map; + map_t *map = &game.map; camera_t *c = &game.camera; game_load(&game, &lv_1); @@ -65,48 +72,41 @@ int main(void) // Spawn player //--- - entity_movement_params_t emp_player = { + mechanical_limits_t ml_player = { .max_speed = fix(4.5), - .propulsion = fix(12), - .dash_speed = fix(55), - .dash_duration = fix(1) / 32, + .friction = fix(0.7), + .dash_speed = fix(20), + .dash_duration = fix(1) / 8, .dash_cooldown = fix(1), }; - entity_player_t player_data = { - .combo_length = 2, - .combo_next = 0, - .combo_delay = fix(0), - }; - entity_t *player = enemy_spawn(ENEMY_BAT, 1); - entity_player_t *playerd = &player_data; - player->player = playerd; + entity_t *player = enemy_make(ENEMY_BAT, 2); + physical_t *player_p = getcomp(player, physical); + visible_t *player_v = getcomp(player, visible); + mechanical_t *player_m = getcomp(player, mechanical); + fighter_t *player_f = getcomp(player, fighter); - int x=1, y=1; - for(int i = 0; i < 1000; i++) { - x = rand() % m->width; - y = rand() % m->height; + player_f->combo_length = 2; + player_f->combo_next = 0; + player_f->combo_delay = fix(0); + player_f->identity = 0; + player_f->HP_max += 100; + player_f->HP += 100; - struct tile *t = map_tile(m, x, y); - if(t && !t->solid) break; - } - player->movement.x = fix(x) + fix(0.5); - player->movement.y = fix(y) + fix(0.5); + player_p->x = fix(9.5); + player_p->y = fix(4.5); + player_p->hitbox = (rect){ -fix(5)/16, fix(5)/16, -fix(2)/16, fix(4)/16 }; + + player_m->limits = &ml_player; + + player_v->z = 0; + player_v->shadow_size = 4; + + visible_set_anim(player, &anims_player_Idle, 1); game_add_entity(&game, player); game.player = player; - player->HP_max += 100; - player->HP += 100; - player->movement_params = &emp_player; - player->identity = 0; - player->hitbox = (rect){ - -fix(5)/16, fix(5)/16, -fix(2)/16, fix(4)/16 }; - player->sprite = (rect){ - -fix(6)/16, fix(5)/16, -fix(12)/16, fix(4)/16 }; - - entity_set_anim(player, anims_player_Idle, 1); - //--- // Main loop //--- @@ -137,6 +137,10 @@ int main(void) render_game(&game, debug.show_hitboxes); + /* kmalloc_arena_t *_uram = kmalloc_get_arena("_uram"); + kmalloc_gint_stats_t *_uram_stats = kmalloc_get_gint_stats(_uram); + dprint(1, 1, C_WHITE, "Memory: %d", _uram_stats->used_memory); */ + /* Developer/tweaking menu */ if(debug.show_vars) { uint32_t *vram = (void *)gint_vram; @@ -149,25 +153,23 @@ int main(void) uint16_t gray = C_RGB(16, 16, 16); dprint(3, 40, C_WHITE, "Player speed: %g tiles/s", - f2double(emp_player.max_speed)); + f2double(ml_player.max_speed)); dprint(15, 55, gray, "[frac] -/+ [X,0,T]"); - dprint(3, 70, C_WHITE, "Propulsion: %g s^-1", - f2double(emp_player.propulsion)); + dprint(3, 70, C_WHITE, "Friction: %g", + f2double(ml_player.friction)); dprint(15, 85, gray, "[F<>D] -/+ [log]"); - dprint(15, 100, C_WHITE, "(Friction: %g)", - f2double(fix(1) - emp_player.propulsion / FRAME_RATE)); dprint(3, 115, C_WHITE, "Dash speed: %g tiles/s", - f2double(emp_player.dash_speed)); + f2double(ml_player.dash_speed)); dprint(15, 130, gray, "[(] -/+ [ln]"); dprint(3, 145, C_WHITE, "Dash duration: %g s", - f2double(emp_player.dash_duration)); + f2double(ml_player.dash_duration)); dprint(15, 160, gray, "[)] -/+ [sin]"); dprint(3, 175, C_WHITE, "Dash cooldown: %g s", - f2double(emp_player.dash_cooldown)); + f2double(ml_player.dash_cooldown)); dprint(15, 190, gray, "[,] -/+ [cos]"); } @@ -189,9 +191,9 @@ int main(void) dline(j.x, j.y, k.x, k.y, C_RGB(0, 31, 31)); } - vec2 p = entity_pos(player); + vec2 p = physical_pos(player); vec2 q = vec_i2f_center((ivec2){ 6, 9 }); - bool clear = raycast_clear_hitbox(m, p, q, player->hitbox); + bool clear = raycast_clear_hitbox(map, p, q, player_p->hitbox); ivec2 j = camera_map2screen(c, p); ivec2 k = camera_map2screen(c, q); @@ -254,10 +256,6 @@ int main(void) prof_enter(perf_simul); game_spawn_enemies(&game); - - game_update_animations(&game, dt); - - game_sort_entities(&game); game_sort_particles(&game); key_event_t ev; @@ -282,46 +280,46 @@ int main(void) debug.show_perf ^= 1; if(ev.key == KEY_XOT) - emp_player.max_speed += fix(1)/8; + ml_player.max_speed += fix(1)/8; if(ev.key == KEY_FRAC) { - emp_player.max_speed -= fix(1)/8; - if(emp_player.max_speed < 0) - emp_player.max_speed = 0; + ml_player.max_speed -= fix(1)/8; + if(ml_player.max_speed < 0) + ml_player.max_speed = 0; } if(ev.key == KEY_LOG) { - emp_player.propulsion += fix(1) / 8; - if(emp_player.propulsion > fix(FRAME_RATE)) - emp_player.propulsion = fix(FRAME_RATE); + ml_player.friction += fix(1) / 32; + if(ml_player.friction > 1) + ml_player.friction = 1; } if(ev.key == KEY_FD) { - emp_player.propulsion -= fix(1) / 8; - if(emp_player.propulsion <= 0) - emp_player.propulsion = 0; + ml_player.friction -= fix(1) / 32; + if(ml_player.friction <= 0) + ml_player.friction = 0; } if(ev.key == KEY_LN) - emp_player.dash_speed += fix(0.5); + ml_player.dash_speed += fix(0.5); if(ev.key == KEY_LEFTP) { - emp_player.dash_speed -= fix(0.5); - if(emp_player.dash_speed <= 0) - emp_player.dash_speed = 0; + ml_player.dash_speed -= fix(0.5); + if(ml_player.dash_speed <= 0) + ml_player.dash_speed = 0; } if(ev.key == KEY_SIN) - emp_player.dash_duration += fix(1) / 64; + ml_player.dash_duration += fix(1) / 64; if(ev.key == KEY_RIGHTP) { - emp_player.dash_duration -= fix(1) / 64; - if(emp_player.dash_duration <= 0) - emp_player.dash_duration = 0; + ml_player.dash_duration -= fix(1) / 64; + if(ml_player.dash_duration <= 0) + ml_player.dash_duration = 0; } if(ev.key == KEY_COS) - emp_player.dash_cooldown += fix(1) / 8; + ml_player.dash_cooldown += fix(1) / 8; if(ev.key == KEY_COMMA) { - emp_player.dash_cooldown -= fix(1) / 8; - if(emp_player.dash_cooldown <= 0) - emp_player.dash_cooldown = 0; + ml_player.dash_cooldown -= fix(1) / 8; + if(ml_player.dash_cooldown <= 0) + ml_player.dash_cooldown = 0; } if(ev.key == KEY_PLUS) @@ -348,7 +346,7 @@ int main(void) #endif /* Player movement */ - if(player->HP > 0) { + if(player_f->HP > 0) { int dir = -1; if(keydown(KEY_UP)) dir = UP; if(keydown(KEY_DOWN)) dir = DOWN; @@ -356,43 +354,43 @@ int main(void) if(keydown(KEY_RIGHT)) dir = RIGHT; if(keydown(KEY_F1) && !keydown(KEY_VARS)) { - int dash_dir = (dir >= 0) ? dir : player->movement.facing; - entity_dash(player, dash_dir); + int dash_dir = (dir >= 0) ? dir : player_p->facing; + mechanical_dash(player, dash_dir); } - entity_movement_t next = entity_move4(player, dir, dt); + mechanical_move4(player, dir, dt, map); - bool set_anim = (player->movement.facing != next.facing) || - ((entity_moving(player) == 0) != (dir == -1)) || - (player->anim.frame == NULL); - - game_try_move_entity(&game, player, &next); - if(set_anim && dir >= 0) - entity_set_anim(player, anims_player_Walking, 1); - else if(set_anim && dir < 0) - entity_set_anim(player, anims_player_Idle, 1); + if(dir >= 0) + visible_set_anim(player, &anims_player_Walking, 1); + else + visible_set_anim(player, &anims_player_Idle, 1); } /* Directions to reach the player from anywhere on the grid */ pfg_all2one_free(&game.paths_to_player); - game.paths_to_player = pfg_bfs(m, vec_f2i(entity_pos(player))); + game.paths_to_player = pfg_bfs(map, vec_f2i(physical_pos(player))); /* Enemy AI */ - if(player->HP > 0) for(int i = 0; i < game.entity_count; i++) { + if(player_f->HP > 0) + for(int i = 0; i < game.entity_count; i++) { entity_t *e = game.entities[i]; - if(e == player || e->HP == 0) continue; + fighter_t *f = getcomp(e, fighter); + mechanical_t *m = getcomp(e, mechanical); + physical_t *p = getcomp(e, physical); + if(!f || !m || f->identity == 0 || f->HP == 0) + continue; /* Go within 1 block of the player */ vec2 direction = { 0, 0 }; - vec2 pos = entity_pos(e); + vec2 pos = physical_pos(e); - bool in_range = dist2(pos, entity_pos(player)) <= fix(1); + bool in_range = dist2(pos, physical_pos(player)) <= fix(1); if(!in_range) { pfg_path_t path = pfg_bfs_inwards(&game.paths_to_player, vec_f2i(pos)); if(path.points) { direction = pfc_shortcut_one(&path, pos, - entity_pos(player), e->hitbox); + physical_pos(player), getcomp(e, physical)->hitbox); pfg_path_free(&path); } @@ -402,88 +400,83 @@ int main(void) } } - entity_movement_t next = entity_move(e, direction, dt); - if(direction.x > 0) next.facing = RIGHT; - else if(direction.x < 0) next.facing = LEFT; - else if(e->movement.x < player->movement.x) next.facing = RIGHT; - else next.facing = LEFT; + mechanical_move(e, direction, dt, map); + if(direction.x > 0) p->facing = RIGHT; + else if(direction.x < 0) p->facing = LEFT; + else if(p->x < player_p->x) p->facing = RIGHT; + else p->facing = LEFT; - bool will_move = !in_range; - bool set_anim = (e->movement.facing != next.facing) || - ((entity_moving(e) == 0) != (will_move == false)) || - (e->anim.frame == NULL); + if(mechanical_moving(e)) + visible_set_anim(e, enemies[f->identity]->anim_walking, 1); + else + visible_set_anim(e, enemies[f->identity]->anim_idle, 1); - game_try_move_entity(&game, e, &next); - int id = e->movement.facing == RIGHT; - if(set_anim && will_move) - entity_set_anim(e, enemies[e->identity]->anim_walking[id], 1); - else if(set_anim && !will_move) - entity_set_anim(e, enemies[e->identity]->anim_idle[id], 1); - - if(in_range && !e->current_attack) { + if(in_range && !f->current_attack) { /* Enemy attack */ int facing = frdir((vec2){ - player->movement.x - e->movement.x, - player->movement.y - e->movement.y }); + player_p->x - p->x, + player_p->y - p->y }); - effect_area_t *area = effect_area_new_attack(EFFECT_ATTACK_HIT, - e, facing); - game_add_effect_area(&game, area); + entity_t *aoe = aoe_make_attack(AOE_HIT, e, facing); + game_add_entity(&game, aoe); - e->current_attack = area; - e->attack_follows_movement = true; + f->current_attack = aoe; + f->attack_follows_movement = true; } } /* Player attack */ - if(player->HP > 0 && attack && !player->current_attack) { - int hit_number=0, effect=EFFECT_ATTACK_SLASH; + if(player_f->HP > 0 && attack && !player_f->current_attack) { + int hit_number=0, effect=AOE_SLASH; /* If hitting within .25s of the previous hit ending, combo! */ - if(abs(playerd->combo_delay) < fix(0.25)) - hit_number = playerd->combo_next; - playerd->combo_next = (hit_number + 1) % playerd->combo_length; + if(abs(player_f->combo_delay) < fix(0.25)) + hit_number = player_f->combo_next; + player_f->combo_next = (hit_number + 1) % player_f->combo_length; - if(hit_number == 0) effect = EFFECT_ATTACK_SLASH; - if(hit_number == 1) effect = EFFECT_ATTACK_IMPALE; + if(hit_number == 0) effect = AOE_SLASH; + if(hit_number == 1) effect = AOE_IMPALE; + entity_t *aoe = aoe_make_attack(effect, player, player_p->facing); + game_add_entity(&game, aoe); - effect_area_t *area = effect_area_new_attack(effect, player, - player->movement.facing); - game_add_effect_area(&game, area); - - entity_set_anim(player, anims_player_Attack, 2); - player->current_attack = area; - player->attack_follows_movement = true; - playerd->combo_delay = area->lifetime; + visible_set_anim(player, &anims_player_Attack, 2); + player_f->current_attack = aoe; + player_f->attack_follows_movement = true; + player_f->combo_delay = getcomp(aoe, aoe)->lifetime; } + /* Player skills */ - if(player->HP > 0 && keydown(KEY_F2) && !player->current_attack) { - effect_area_t *area = effect_area_new_attack(EFFECT_ATTACK_SHOCK, - player, player->movement.facing); - game_add_effect_area(&game, area); + bool can_attack = player_f->HP > 0 && !player_f->current_attack && + !keydown(KEY_VARS); - entity_set_anim(player, anims_player_Attack, 2); - player->current_attack = area; - player->attack_follows_movement = true; - } - if(player->HP > 0 && keydown(KEY_F3) && !player->current_attack) { - effect_area_t *area = effect_area_new_attack( - EFFECT_ATTACK_JUDGEMENT, player, player->movement.facing); - game_add_effect_area(&game, area); + if(can_attack && keydown(KEY_F2)) { + entity_t *aoe = aoe_make_attack(AOE_SHOCK, player, + player_p->facing); + game_add_entity(&game, aoe); - entity_set_anim(player, anims_player_Attack, 2); - player->current_attack = area; - player->attack_follows_movement = false; + visible_set_anim(player, &anims_player_Attack, 2); + player_f->current_attack = aoe; + player_f->attack_follows_movement = true; } - if(player->HP > 0 && keydown(KEY_F4) && !player->current_attack) { - effect_area_t *area = effect_area_new_attack( - EFFECT_ATTACK_BULLET, player, player->movement.facing); - game_add_effect_area(&game, area); + if(can_attack && keydown(KEY_F3)) { + entity_t *aoe = aoe_make_attack( AOE_JUDGEMENT, player, + player_p->facing); + game_add_entity(&game, aoe); - entity_set_anim(player, anims_player_Attack, 2); - player->current_attack = area; - player->attack_follows_movement = false; + visible_set_anim(player, &anims_player_Attack, 2); + player_f->current_attack = aoe; + player_f->attack_follows_movement = false; } + if(can_attack && keydown(KEY_F4)) { + entity_t *aoe = aoe_make_attack(AOE_BULLET, player, + player_p->facing); + game_add_entity(&game, aoe); + + visible_set_anim(player, &anims_player_Attack, 2); + player_f->current_attack = aoe; + player_f->attack_follows_movement = false; + } + /* Ideas for additional skills: - Freeze enemies - Barrier around player @@ -496,18 +489,22 @@ int main(void) - XP boosts - Weaker but longer-lasting buffs */ - /* Remove dead entities first as it will kill their attack areas */ - game_remove_dead_entities(&game); - game_update_effect_areas(&game, dt); + game_update_animations(&game, dt); + game_update_aoes(&game, dt); game_update_particles(&game, dt); - playerd->combo_delay -= dt; + game_remove_dead_entities(&game); + player_f->combo_delay -= dt; + + /* Reset default anims */ + if(!player_v->anim.frame) + visible_set_anim(player, &anims_player_Idle, 1); game.time_total += dt; game.time_wave += dt; - if(game.time_defeat == 0 && player->HP == 0) + if(game.time_defeat == 0 && player_f->HP == 0) game.time_defeat = game.time_total; - if(game.time_victory == 0 && player->HP > 0 && game.entity_count == 1) + if(game.time_victory == 0 && player_f->HP > 0 && game.entity_count==1) game.time_victory = game.time_total; /* Next wave */ @@ -524,7 +521,7 @@ int main(void) debug.grid_path = pfg_bfs_outwards(&game.paths_to_player, vec_f2i(target)); debug.continuous_path = pfc_shortcut_full(&debug.grid_path, - entity_pos(player), target, player->hitbox); + physical_pos(player), target, player_p->hitbox); } prof_leave(perf_simul); diff --git a/src/map.c b/src/map.c index 4eb3eb4..b495ab6 100644 --- a/src/map.c +++ b/src/map.c @@ -1,7 +1,7 @@ #include "map.h" #include -rect tile_shape(struct tile const *tile) +rect tile_shape(tile_t const *tile) { if(!tile->solid) return (rect){ 0 }; @@ -13,7 +13,7 @@ rect tile_shape(struct tile const *tile) }; } -struct tile *map_tile(map_t const *m, int x, int y) +tile_t *map_tile(map_t const *m, int x, int y) { if((unsigned)x >= (unsigned)m->width || (unsigned)y >= (unsigned)m->height) return NULL; @@ -31,7 +31,7 @@ bool map_collides(map_t const *m, rect hitbox) /* Collisions against walls and static objects */ for(int y = y_min; y < y_max; y++) for(int x = x_min; x < x_max; x++) { - struct tile *t = map_tile(m, x, y); + tile_t *t = map_tile(m, x, y); if(!t || !t->solid) continue; vec2 center = { fix(x) + fix(0.5), fix(y) + fix(0.5) }; diff --git a/src/map.h b/src/map.h index fb0b252..c924e82 100644 --- a/src/map.h +++ b/src/map.h @@ -11,7 +11,6 @@ #include "fixed.h" #include "map.h" #include "geometry.h" -#include "entities.h" #include #include @@ -21,7 +20,8 @@ // Tiles //--- -struct tile { +typedef struct +{ /* TODO: Layers of objects, stuff, dynamic elements, etc? */ /* TODO: Allow any collision shape for the tile! */ bool solid; @@ -29,27 +29,29 @@ struct tile { uint8_t base; /* Decoration layer */ uint8_t decor; -}; + +} tile_t; /* Shape for a tile; this lives in a coordinate system whose (0,0) ends up at the middle of the tile in the map space (which is a point with half-integer coordinates) */ -rect tile_shape(struct tile const *tile); +rect tile_shape(tile_t const *tile); //--- // Map grid, tiles location in space, and entities //--- -typedef struct { +typedef struct +{ /* Dimensions, columns are 0 to width-1, rows are 0 to height-1 */ int width, height; /* All tiles */ - struct tile *tiles; + tile_t *tiles; } map_t; /* Get a pointer to the tile at (x,y) in map; NULL if out of bounds. */ -struct tile *map_tile(map_t const *m, int x, int y); +tile_t *map_tile(map_t const *m, int x, int y); /* Check whether a hitbox collides with the map. */ bool map_collides(map_t const *m, rect hitbox); diff --git a/src/object.c b/src/object.c deleted file mode 100644 index e69de29..0000000 diff --git a/src/pathfinding.c b/src/pathfinding.c index 904c270..60ea108 100644 --- a/src/pathfinding.c +++ b/src/pathfinding.c @@ -66,7 +66,7 @@ pfg_all2one_t pfg_bfs(map_t const *map, ivec2 center) int dx = dx_array[dir]; int dy = dy_array[dir]; - struct tile *tile = map_tile(map, point.x+dx, point.y+dy); + tile_t *tile = map_tile(map, point.x+dx, point.y+dy); if(!tile || tile->solid) continue; int next_i = idx(point.x+dx, point.y+dy); @@ -159,7 +159,7 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end) perfectly */ int current_x = ffloor(x-(u.x < 0)); int current_y = ffloor(y-(u.y < 0)); - struct tile *tile = map_tile(map, current_x, current_y); + tile_t *tile = map_tile(map, current_x, current_y); if(tile && tile->solid) return false; if(raycast_clear_points < 64) { @@ -185,7 +185,7 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end) int next_x = ffloor(x-(u.x < 0)); int next_y = ffloor(y-(u.y < 0)) + (u.y >= 0 ? 1 : -1); - struct tile *tile = map_tile(map, next_x, next_y); + tile_t *tile = map_tile(map, next_x, next_y); if(tile && tile->solid) return false; } else { @@ -195,7 +195,7 @@ bool raycast_clear(map_t const *map, vec2 start, vec2 end) int next_x = ffloor(x-(u.x < 0)) + (u.x >= 0 ? 1 : -1); int next_y = ffloor(y-(u.y < 0)); - struct tile *tile = map_tile(map, next_x, next_y); + tile_t *tile = map_tile(map, next_x, next_y); if(tile && tile->solid) return false; } } diff --git a/src/render.c b/src/render.c index 8b6d3fb..b58aa80 100644 --- a/src/render.c +++ b/src/render.c @@ -1,8 +1,14 @@ +#include "comp/entity.h" +#include "comp/physical.h" +#include "comp/visible.h" +#include "comp/mechanical.h" +#include "comp/fighter.h" #include "render.h" #include "game.h" #include "anim.h" #include +#include //--- // Camera management @@ -111,7 +117,7 @@ fixed_t camera_ppu(camera_t const *c) // Rendering //--- -void render_map(map_t const *m, camera_t const *c) +static void render_map_layer(map_t const *m, camera_t const *c, int layer) { extern bopti_image_t img_tileset_base; extern bopti_image_t img_tileset_decor; @@ -119,7 +125,7 @@ void render_map(map_t const *m, camera_t const *c) /* Render floor and walls */ for(int row = -2; row < m->height + 2; row++) for(int col = -1; col < m->width + 1; col++) { - struct tile *t = map_tile(m, col, row); + tile_t *t = map_tile(m, col, row); vec2 tile_pos = { fix(col), fix(row) }; ivec2 p = camera_map2screen(c, tile_pos); @@ -128,7 +134,12 @@ void render_map(map_t const *m, camera_t const *c) continue; } - /* Floor/wall layer */ + bool will_draw = false; + if(layer == 0) will_draw = (t->base < 16); // Floor + if(layer == 1) will_draw = (t->base >= 16); // Walls + if(!will_draw) + continue; + dsubimage(p.x, p.y, &img_tileset_base, TILE_WIDTH * (t->base % 16), TILE_HEIGHT * (t->base / 16), TILE_WIDTH, TILE_HEIGHT, DIMAGE_NOCLIP); @@ -139,82 +150,128 @@ void render_map(map_t const *m, camera_t const *c) } } -static void render_effect_area(effect_area_t const *ea, camera_t const *c, - bool show_hitboxes) +static int depth_measure(entity_t const *e, int direction) { - ivec2 anchor = camera_map2screen(c, ea->anchor); + visible_t *v = getcomp(e, visible); + if(v == NULL || v->sprite_plane != direction) + return -1; + return getcomp(e, physical)->y; +} +static int floor_depth_measure(entity_t const *e) +{ + return depth_measure(e, HORIZONTAL); +} +static int wall_depth_measure(entity_t const *e) +{ + return depth_measure(e, VERTICAL); +} - if(ea->anim.frame) { - ivec2 p = camera_map2screen(c, ea->anchor); - anim_frame_render(p.x, p.y, ea->anim.frame); +static void render_shadow(int cx, int cy, int shadow_size) +{ + if(shadow_size < 1 || shadow_size > 4) + return; + + static const int hw_array[] = { 2, 3, 5, 7 }; + static const int hh_array[] = { 1, 1, 2, 3 }; + + /* TODO: render_shadow(): Encode shadow shapes properly x) */ + static const uint8_t profile_1[] = { 1, 0, 1 }; + static const uint8_t profile_2[] = { 1, 0, 1 }; + static const uint8_t profile_3[] = { 3, 1, 0, 1, 3 }; + static const uint8_t profile_4[] = { 4, 2, 1, 0, 1, 2, 4 }; + static const uint8_t *profile_array[] = { + profile_1, profile_2, profile_3, profile_4, + }; + + int hw = hw_array[shadow_size - 1]; + int hh = hh_array[shadow_size - 1]; + uint8_t const *profile = profile_array[shadow_size - 1] + hh; + + int xmin = max(-hw, -cx); + int xmax = min(hw, DWIDTH-1 - cx); + int ymin = max(-hh, -cy); + int ymax = min(hh, DHEIGHT-1 - cy); + + for(int y = ymin; y <= ymax; y++) + for(int x = xmin; x <= xmax; x++) { + if(hw - abs(x) < profile[y]) + continue; + int i = DWIDTH * (cy+y) + (cx+x); + gint_vram[i] = (gint_vram[i] & 0xf7de) >> 1; } - if(!ea->anim.frame || show_hitboxes) { - rect r = ea->sprite; - r = rect_scale(r, camera_ppu(c)); - r = rect_translate(r, vec_i2f(anchor)); - rect_draw(r, C_RGB(31, 31, 0)); +} + +static void render_entities(game_t const *g, camera_t const *camera, + entity_measure_t *measure, bool show_hitboxes) +{ + uint16_t *rendering_order; + int count = game_sort_entities(g, measure, &rendering_order); + + for(int i = 0; i < count; i++) { + entity_t *e = g->entities[rendering_order[i]]; + physical_t *p = getcomp(e, physical); + visible_t *v = getcomp(e, visible); + + ivec2 scr = camera_map2screen(camera, physical_pos(e)); + int elevated_y = scr.y - fround(16 * v->z); + + /* Show shadow */ + if(v->shadow_size) { + render_shadow(scr.x, scr.y, v->shadow_size); + } + + /* Show entity sprite */ + if(v->anim.frame) { + anim_frame_render(scr.x, elevated_y, v->anim.frame); + } + /* Show entity hitbox in the map coordinate system */ + if(!v->anim.frame || show_hitboxes) { + rect r = p->hitbox; + r = rect_scale(r, camera_ppu(camera)); + r = rect_translate(r, vec_i2f(scr)); + rect_draw(r, C_BLUE); + + /* Show entity center */ + dline(scr.x-1, scr.y, scr.x+1, scr.y, C_BLUE); + dline(scr.x, scr.y-1, scr.x, scr.y+1, C_BLUE); + } } + + free(rendering_order); } void render_game(game_t const *g, bool show_hitboxes) { - camera_t const *c = &g->camera; + camera_t const *camera = &g->camera; - render_map(&g->map, &g->camera); + /* Render map floor */ + render_map_layer(&g->map, camera, 0); - /* Render background particles */ + /* Render background particles + TODO ECS: Use ECS entities for particles */ for(int i = 0; i < g->particle_count; i++) { particle_t *p = g->particles[i]; if(!particle_is_background(p)) continue; - ivec2 center = camera_map2screen(c, p->pos); + ivec2 center = camera_map2screen(camera, p->pos); particle_render(center.x, center.y, p); } - /* Render background effect areas */ - for(int i = 0; i < g->effect_area_count; i++) { - if(!effect_area_is_background(g->effect_areas[i])) continue; - render_effect_area(g->effect_areas[i], c, show_hitboxes); - } + /* Render floor entities */ + render_entities(g, camera, floor_depth_measure, show_hitboxes); - /* Render entities */ - for(int i = 0; i < g->entity_count; i++) { - entity_t *e = g->entities[i]; + /* Render map walls */ + render_map_layer(&g->map, camera, 1); - ivec2 p = camera_map2screen(c, entity_pos(e)); + /* Render wall entities + TODO ECS: Sort walls along wall entities! */ + render_entities(g, camera, wall_depth_measure, show_hitboxes); - /* Show entity sprite */ - if(e->anim.frame) { - anim_frame_render(p.x, p.y, e->anim.frame); - } - /* Show entity hitbox in the map coordinate system */ - if(!e->anim.frame || show_hitboxes) { - rect r = e->hitbox; - r = rect_scale(r, camera_ppu(c)); - r = rect_translate(r, vec_i2f(p)); - rect_draw(r, C_BLUE); - - r = e->sprite; - r = rect_scale(r, camera_ppu(c)); - r = rect_translate(r, vec_i2f(p)); - rect_draw(r, C_RGB(12, 12, 12)); - - /* Show entity center */ - dline(p.x-1, p.y, p.x+1, p.y, C_BLUE); - dline(p.x, p.y-1, p.x, p.y+1, C_BLUE); - } - } - - /* Render background effect areas */ - for(int i = 0; i < g->effect_area_count; i++) { - if(effect_area_is_background(g->effect_areas[i])) continue; - render_effect_area(g->effect_areas[i], c, show_hitboxes); - } - - /* Render foreground particles */ + /* Render foreground particles + TODO ECS: Use ECS entities for particles */ for(int i = 0; i < g->particle_count; i++) { particle_t *p = g->particles[i]; if(particle_is_background(p)) continue; - ivec2 center = camera_map2screen(c, p->pos); + ivec2 center = camera_map2screen(camera, p->pos); particle_render(center.x, center.y, p); } @@ -223,8 +280,10 @@ void render_game(game_t const *g, bool show_hitboxes) /* Render wave progress bar */ int enemies_left = 0; - for(int i = 0; i < g->entity_count; i++) - enemies_left += (g->entities[i]->identity != 0); + for(int i = 0; i < g->entity_count; i++) { + fighter_t *f = getcomp(g->entities[i], fighter); + enemies_left += (f && f->identity != 0); + } if(g->wave_spawned < game_current_wave(g)->enemy_count) dprint_opt(DWIDTH/2, 2, C_WHITE, C_NONE, DTEXT_CENTER, DTEXT_TOP, @@ -241,10 +300,12 @@ void render_game(game_t const *g, bool show_hitboxes) extern bopti_image_t img_hud; dimage(0, DHEIGHT - img_hud.height, &img_hud); + fighter_t *player_f = getcomp(g->player, fighter); + mechanical_t *player_m = getcomp(g->player, mechanical); + /* Render life bar */ extern bopti_image_t img_hud_life; - entity_t const *player = g->player; - int fill_height = (img_hud_life.height * player->HP) / player->HP_max; + int fill_height = (img_hud_life.height * player_f->HP) / player_f->HP_max; dsubimage(184, DHEIGHT - 5 - fill_height, &img_hud_life, 0, img_hud_life.height - fill_height, img_hud_life.width, fill_height, DIMAGE_NONE); @@ -254,13 +315,13 @@ void render_game(game_t const *g, bool show_hitboxes) for(int i = 0; i < 6; i++) { /* Activity and cooldown */ fixed_t cooldown_total=0, cooldown_remaining=0; - if(i == 0 && player->movement.dash < 0) { - cooldown_total = player->movement_params->dash_cooldown; - cooldown_remaining = -player->movement.dash; + if(i == 0 && player_m->dash < 0) { + cooldown_total = player_m->limits->dash_cooldown; + cooldown_remaining = -player_m->dash; } else if(i > 0) { - cooldown_total = player->player->actions_cooldown_total[i-1]; - cooldown_remaining = player->player->actions_cooldown[i-1]; + cooldown_total = player_f->actions_cooldown_total[i-1]; + cooldown_remaining = player_f->actions_cooldown[i-1]; } int x = 33 + 44*i + 84*(i>=3); @@ -288,7 +349,7 @@ void render_pfg_all2one(pfg_all2one_t const *paths, camera_t const *c) { for(int row = 0; row < paths->map->height; row++) for(int col = 0; col < paths->map->width; col++) { - struct tile *tile = map_tile(paths->map, col, row); + tile_t *tile = map_tile(paths->map, col, row); if(!tile || tile->solid) continue; vec2 fp = vec_i2f_center((ivec2){ col, row }); diff --git a/src/render.h b/src/render.h index a041d93..2e27227 100644 --- a/src/render.h +++ b/src/render.h @@ -56,11 +56,6 @@ fixed_t camera_ppu(camera_t const *c); // Rendering //--- -/* Render map for the specified camera. - TODO: render_map: Honor zoom - TODO: render_map: Clip around viewport */ -void render_map(map_t const *m, camera_t const *c); - /* Render game full-screen. */ struct game; void render_game(struct game const *g, bool show_hitboxes);