From 06648c2dc195622344c10fed05ab9330b60e592b Mon Sep 17 00:00:00 2001 From: Trevor Boddy Date: Wed, 18 Feb 2026 17:58:41 -0500 Subject: [PATCH] lots of work --- flower.fur | Bin 361 -> 1802 bytes res/fadebottom.png | Bin 1938 -> 1894 bytes res/fadetop.png | Bin 4561 -> 1910 bytes res/ground.png | Bin 5648 -> 5569 bytes res/koakuma.png | Bin 5266 -> 5615 bytes res/pbullet.png | Bin 2489 -> 2427 bytes res/resources.res | 4 +- res/stage.vgm | Bin 0 -> 32855 bytes src/background.h | 84 ++++++++-- src/bullets.h | 6 + src/chrome.h | 33 +++- src/enemies.h | 24 +++ src/enemytypes.h | 409 ++++++++++++++++++++++++++++++++++++++------- src/global.h | 4 + src/humans.h | 10 +- src/main.c | 12 +- src/player.h | 8 +- src/stage.h | 159 ++++++++++++++---- 18 files changed, 640 insertions(+), 113 deletions(-) create mode 100644 res/stage.vgm diff --git a/flower.fur b/flower.fur index 84c0ab4ed0fd32e64ba726a9c0138b9a109b681f..96d55e3cb44ebee9d872244f4a7c58fa845c14d4 100644 GIT binary patch literal 1802 zcmV+l2le=PoXwV9Y#h}U$IsdMb{yN}!;U~=mJ*Qrkk1Olmy~sELK3j!*oo~l&h9$N zlGt%}&8|bDBuHMY0fObgLt6Evt(1p8RDA#ysS>KHs=V+3sIQ=^-4~D_kWf`xg($)O z-!nVAb8W{lgoupqZ|0tR&&Pl6+?n;JU1y5B8|daSVX@7Ux7{=eQ+JT zf(807IN=ig3+(rZJ^)WQ5M2c~!SPi@e+F}4T_e$9a20$4)~_bo47x!9Tn6uhFTlno zqNCt4_y~Ls%x0oT!A>v=z6F^))HL-&FfGD{2H{cCwd%o zfj*E0d2kWD34R5B2i^mJ1Ahnq1Yd&dz-=XJ1slP3kODnm7>t2);5BdsTm|odzkrXx zXJ8Ke514I4Yd{ig2G4-KUAp%-9l44DB3@&nbB5CD3gAPzwqeKoft_zm&@l$ zqopm~%IGSNzMNZ-Wf02g?m0jvUzU^Qq0%>d7JouOPmH_zL1Hh_4{Ng7^yJD~PWkzJmA);wy-+ zAYR2rH)Ah2=ucR+kHM#)qiauJliV^6CwlH`)U3wG`+Iu&xA);X2mhXqb@c4!Wbf!m zzL4COZ0|c+IG!v0ly-GLLSLuw?MpxDuTT99@9bkfr}iUtoA0Hrs}jDO`iAI*F4Rj} zz1Cm(*&da-r;o_Tw+y=$nN^*LmH`ca=!{~_A;kZ-5n#jbwPZ%$oxX;nO~@7Zlc|zn|6;( zCOgLqrQGo_=Zna>gIb$Y_>Mi1qsG)eeCKFGYaenw+3&AX_M)77YdIUEocktn`6V(Y zs~Oj9Xm~z@jE~$RPQu3h`L>%o-Q(hLt>LT|@`o{4C~8>f)0 zL|o(&7m3f*4bJlf@mY-VS>b$W$@$bugD%v9=2hLX>L=r_*F9h6i}Uq6O`zg9Do!GV zCso+Xb%P8167e~V@j2l_Xvuker9l^KL9shE?ta}1Wqu`A5nmr8i;5?SPw+`TK|EGB z_!#FT^%#!}h0v07cEv$uekr!L*4?i-Q45l~8L4ZBJ18&9lb7v~k4B1eJ|7z|#%7oI zFHUnw%Jft8oxj5};R93Kb%x)I-GO_z@1R%8ct6w~lxC~)H&)+bGBO-+Cg39hA1?Ee zG7km(bMyn%&%}IrMZWUS1>ORvwUP$&N9&^6Ha|X1Rb5>v`pVG(`rAZ zFL9d0Vx8T@-2N2i0_d2 zJ}$oM^Lr>Ew>~IsK-xj+_enb-?Vz+FJ=*-v(T8F>ZHW$eFq9ewiAcyEjC{jN&iB+Q zk1e4d+u$q@%*wgG>It_#5y;+|;lcW2OQ?r3c!>u|JZ7Sfc&rl-OL@%163Coi{e{8) zG=6Jj&1ufQt{!Su4dClq-BYvT#^MId>h)HAxyG7Vj`@Wn3!~y~vFE~`hlt5vhlUaNQ$kb;NelsV$DiumZ4duW?7o$XqKy4cHFYH>b6$h)~efDbz7@$Yt?PN zf~LjRwAfzUax^WDrp3{;IGPqm)8c4a98HU(X>l|yj;6)cw78lUSJUEZT3k(wt7&mH sEv}};)wH;p7EjaSX<9r@i>GPvG%cQ{#nZHSnifyf;wdfv2l|;DMN%i5k^lez literal 361 zcmb=Jb9T~xze52c?dLPS7ygOAQ^}b0h?~R9fM-!)d_zWZ2UiQ5XNh59Y1s$P8m9RN zj-Iu!zH{;9k=W+=gHB#b7FD<2Kl^v9?)$xa+hXLuSg&>7UvkY>YR<2x=7O3h*GQfH zb#Aij@zT&G+XDqfms{+d-j*FL7MAL%yY0XG-n_2V%p1YI>sGG&@XWk`{rd-o{trxS z_Z}K=v$oQ$j`CLDA2mJW*@u!vt`Uh-qWe7G{yx6;(zN>@o69!rl5LCHU;FMnukrQx z?l3dCH6GC^4;I~c;J_5);lyR*;mo#aLbHvC${*Ev?p$IR;tjEf&3~}ne;{4cod1D$ z-$4Y&*}O`!eBF~~`5)Xi+jd7~zp=FU|99>FS~-*5mmm7MFkjrqc1v9LOjFqwb*m?{ y_>RZ;T-xz>bBX0tw})HaJt^)uGfC9DOviHLlnzz%X_Gci=~C7Ax$iq^@@fEC|FDk$ diff --git a/res/fadebottom.png b/res/fadebottom.png index a2a204f0ac6eb40b3c3dcd332ff2dc777c194090..b01a6607e371e93317c509c5225fdbd3b5a6ca1d 100644 GIT binary patch delta 250 zcmbQl|BO$uGr-TCmrII^fq{Y7)59eQNDF{42Q!e=(B%)_sQ8qL%Ob?k+{(zp%D`Z< z0JAbO*OoazB`rBQ)zBivRM*(R+(6ePCB;BD$->k`*VNcN&A`k!%_7ypZ1YCu3MQgd zPOf7WN48-;Ysln7Y?0m~qVKN*-K6j7;us<^H927cQ-Vvgf?!F{I;Qnq? z1(=nQxwgy!DwYqL>n1Aa8N6C;JjAPzvv=M m^xO1?2CIlq_Ob_9neFyf7rkN4=(-5BhQZU-&t;ucLK6U>fm*2m diff --git a/res/fadetop.png b/res/fadetop.png index c697e2a12165336b40cff3d75756bc17cab8ac8f..494b862a9ef58a7e0a305836f29d614e3383d132 100644 GIT binary patch delta 387 zcmcbp{EbhsGr-TCmrII^fq{Y7)59eQNGpIa2Q!d#S<-cEaynNC6#cPo9HH*o0;jFn53Clm|L1ACt4UQ zDOBVZ`1)G8yCL5+0nCTjr zn55_$8kr>PCMG4N0=da%7AXdXhGrJ2Fw^{tGSf3RyYR^}c@XD_;u6=g607oz)I3`y zBLgF#B}Tf2aKD-A8kkO&<+q+JDj+ubmq286#=Iv_fPrD}>Eak-(R+5nL0$#}4yOKh z`#0ZZm?a^t_~(m~m&sBWi!=Np4?Y#8M7pUo>@gScNaJi2V0HTLZ_jaL-sRts;>`O5 V&bwuo9{?J~;OXk;vd$@?2>^73asmJV delta 2959 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 diff --git a/res/ground.png b/res/ground.png index c69bc69b34e739604047fc148c29d7c480cd8cfe..ef08f9c3c98e3bfe3017e1c6cd2fd7fea5e4e580 100644 GIT binary patch delta 1211 zcmV;s1VsCgEWsP*Aais@c62&-Xk~6aA~G;CHZ3qREipJ$F*iChH99mn zEig5cD-S!9br2SlS`k@)r@X>S000B>NklQ`*?F@g?3uMggo*qd?B7S?6;$-hJ(Z@lkb_xjl`pCXVmrs?B<iu48*sW%1^(U$m?ZxVB!DjRO(eHRTdxt2j z119Q&-&uxri}kP$w(~UzH2Ykwn~Zm>u%p;HV?3|^AM$`hPgz)K!1eyRf_PY2k1PLV zdF1mBN7m#!b`S`EXq(YO$wAo!&zzAWVg42NFZZ7xlB@ z68>ba0RygB{XiLYZMt1y?Q<%OZLaIOSFu)Y|XFdPJs+_eD( zU(TXGvra4+7_h+O<=g88hPn>04=Rt#_TXx=WD!J6uT*1yvPHURr(aGcV-v|pjPa^_ z(#X(ygv0qBqLu)P$w873a?AMBb&_zx*Z|EhX*p0G%#5;=bLW^fzko4_X+atrPBAVg zCCD;$BB+7ZBXeERq9-DovI&EfaP_z<*Q}~RO$Y_X-WF{^cMkKaIvvF7#bkvA%_A%p zTO2Nr1@Se1Srgh)dg+~;No1HSl~@*XLXHcjuJ$H7Um|&HTVUP`R;wBmnFmz^Ih4XWA#edNj z1g-7Cna<~YlEj5Ux2~RdBv~3t__cQy;iR0#=Q{^~_>jSl7CF3SO=ouiXW&Y02M*00 zB=1Mu3%askEt4`7mLqx!Dd`Hkm4rFcx|T>Y=2jOB;)n^s&RRepeO<@NX^7AQqr(l2 za}q3F1V^-7q>!zgSoK_$44Oxwv=UD*X~Ys}G7>pZIkwkRNnD@q8p%}hD>sZf6g6T- z4-Qy==N#i+UF#ZdTBPSpWOrwZoiW$SPT6ud>ky~17-d&@Q)(`i6s}9o+>E8z%q^4^ z9UcNEec@JfSHA68Gtp*N*PfhbccpmAlZW%#=IUU%rS9c?FS*4nBcE1tc=<@%rJZaH zJh`xSi%nVeGuMB70rPSid)c~+al_%>Xw1uK&wCk# zhv?_RMjwwtx!+Hh=hxfsj&1sHTkC#@BJy?Xx<|S0eE;GX%k?+e{rmpgarT|R9X~}D Z@CVj;5n3)ac@h8s002ovPDHLkV1h?IHMsx) delta 1290 zcmV+l1@-#DE08R(ObrS)R53O>H8eUhGm};gBMLTDF*Z6iG&(XflYGc-6hH8nUQAais@c62&-Xk~6aA~G;CHZ3qREipD!F*Z6iG&(Xf zEig5cD-S!9br2SlS`k@)&)hRX000C*Nkl6?D+ zfyorBue=#yHRJ9X$SKWV_h7!Uep|ie{_4jb%pFtPXnfC#{piVm6w%4=tA77Ay5(2P z(sR1k-|x%6dhO4zTsnLHtp08NyY!M@VxZXBRG{~tI6#E zq){Gpw1_SJ3HT(51jbai-=&Z6OQ(uZ6)mH*=mX7 zp|J#O0v4dO%?Ic<~>0oL?a|*z_ua_?c(!@z^W1` zuDXq}`R*2UT?I@`W+m~e776~7{hg7!6j_Zz`huABJZrg>emk{-BuZLtpGS|NBwu0K za!@MJXm%aIf=_w~N=q`a_)$3qLY+ALj6gYe5+VqH#(>d*uFlZQ2iju~Iqd=Ed??9g zdfJzULHmurOORWcWndLnDW#Et_(34FZeSp&VN8`0L+{0yMIC4q+#cCM@_vL~V1a>; zEK2r{e8ZAR2^aMe-!5NJpfmw1;XRRo7G};^52=@(>v#Zlo;Fpgit5))Eo>MuqUJ(0 z7zs9imdxNPXdH!e6nU%?#}uF)bc^2j9XX)}4@_goq2Eb;kWq^}j2J-19!IaPywZ%J zq3jf}HJy-axa$M!Lu}U=YH5V0@&df+jAo1*RK}dJs|HfPxDz-cxdvWdt+N~HhMp_c zKt0$&nH)NSIA+t7-0X_IO80TTmnGgZ@-u3G4=x`Zqyw2p_Gp}G6!R9Fw6b06M%VPz z&pykTtN1d}ZjRtgH7Yb$MM(;IHuBUUDxDry&Q#XaCc@1MZ15o53F%^1P;h$ z+M4$@Pj?fUUBH3M2aACPXm|h4_t=4Oa73UxWNX^%6i0mVL@K^A=Xo#CWsC)uL6!`9 zAFp|PL04|Z-4~3$YUL4AQ_abm$!5iLtchdj51&Iw9)CWr0{{R307*qoM6N<$f;C7@ A^#A|> diff --git a/res/koakuma.png b/res/koakuma.png index c930e29f77394fc6115bce3bd2bf3d0265ae019d..aef39413f7b12dde824bce05ab30c9ee40ca6d29 100644 GIT binary patch delta 832 zcmbQF`CePGGr-TCmrII^fq{Y7)59eQNIQTq2Q!eIyS?$vMnwmHE{hODb1PE=D-*NH z+5C#g+-dw4DyGSn#)-+vrn*K(<|eu($wn!6{@e#n{oL&0|QgMr;B4qMC;M; zi+yVpcx+X>&NZL=|Nq$!14Xr}%S@~y-{T4=-#+=SM0fd|c}{1UE_&>})NgjRHfVW# zqpi*B@b^z8%o)5Jb|0)}*!AK~Ha|z-uT%db)YyG`lGSIf)cw|3x50(Ml}Yu&Wz#;+ z9V$-YYXoGj7bP}H*LMk>`2Q`s+DiJ`f;6UIYm;j@va*;S@g;Pe-?o_L65|@F^C}Fg zH+_8`KIr0c{HAj+zN5qc2V>`4lM6porg=HYm;GQceqgrWZko}-(8n=diRt&Z?u*o@ z+TpcP$63{`L*&?hLE|${1`%p5X9AA6a&SeR`yrL1zy9L4L)&e7CtKDZb=J0u{ap0m zxzj6cZSUP3Sgd}Q}~^2Z7;zw>hE@0y+3#uMv3t>4+jzq{pgr}(axaiQlfQR9m#DSu)-%|D+w zc>hp|aX;s+Gk@xsR&}o^Q&oDEzeOm*@X*V*r-V0MJN~2WDd$Z7Z&B-BdHSvW{Q1ZA zvs$c8xn~kY|4+XiVHy-8EhqVplX=p6GhU9xHBsNTZ~DAK^4()y9k)G))Aw|$pLM9< z7Or29`2Xuk?8$%ZSvco-SGoBAYdml6+j1}2@x!;@ z_>L*pa*KHyHYW_s&NY8jY~Fa{!ajT386GOPwx0_z>(5DLnA`f+_~Nhngn!n|zZw3o k;THV9q`&j5{cZUtOE)}Qt?IECm?;=MUHx3vIVCg!0BNCYMgRZ+ delta 481 zcmaE_JxNosGr-TCmrII^fq{Y7)59eQNGpIa2Q!d#S<-cEqoM;pmsyCRxs{Qjm9f#} zY<@*#?lgW272_m>6pIwgG~MJBBO_fC^E7kaq@+Z1U4vvGOiN8nHZ(Ha{E2@8lPfVQ zl@y9gT+2$V$}>{)Y?X`*jLdWmjC2i=oM0`eH~FhjeZ8n!dItjoBa^3#V~9oX(vXW> zhZT7CoN)Pd>Hq(8`#c2C7<5W?-#yKwEwq06X;s+^&Brn=+b4FrOjGX?{`itzPf$ZA z;JB&eG=T?f0alIL8;t@k2VR?E7a12?;(Bl~%T_)O$>@gN;%q@T89j^5--%D$@kTxA zideNv@{R`K9k1)(cuCA?{}k-?VrxmR>J5SAA{zu0@BS0ETp+hz-_6%d7*3FuLE&+gpzYqt|U1)%)Kn(x2b-u!T-1!TW+$?5;~i4 zU+v4jMY-NCywMXKa_(+__`Yn?@^#ziZrM9?^52U`>l@4`_Uq>@lT}!-;$g0{>`}4T b|G4(0S_ay9L_M$p#u|gCtDnm{r-UW|Duc4X diff --git a/res/pbullet.png b/res/pbullet.png index 7e26b48ae6394f91c216c13c0bb91976eb9bb0ba..d1b92a72a837b136d2c5bf2f9c5f9aa3acaa8e60 100644 GIT binary patch delta 764 zcmdlf{99-P8#AXxh@rWap`n$@WPWB9B%Td(po+1jS(;g*S+Z_gVrr7ENm6pMu7!oE znXYkavSq5Fd77C;s>$Y!%vnrCshnKED$WJB&UErb){x0t*dpuq97_Jez`zvb>EalY z(fD@8*}OFdJgw(LlxI$@{C_+rEYWqU#AJCM=h9MD_OCk24x4bQ%Ws>^Adyrdet7$h zoz85{5AMnz+-!f2Z*Nt~^M4z5&Yo-a-AUW$kzY*+U{6pY|`NT>+|;q#=_R$+RrXLUOVm7_T4J)|HZsy zRaau@{`mWM+;h$YTnaIKWqO*Nr(b^Ed9VEK(La{&@4t&yd2qpb-Ba$S98 zx`OPpZnWLedUwY3QCCaG7yl2X<*^fI96YzOQ&Q{Cl-Ssy7s-{g1pWUROF0@no57)1 ztv8Kl^|LclOItr0&NlU%-o0Bkxp0k+S}~`WWctc=EmK}$$Z^rD^`Pq`%o38B?-$N1gI<+Ei<{ z@$@~(Y|&jWZ+oyzxBWNkjMCF-i@aXnnV5QJ=jtydp%-JWe#~w?{BGI$Mu{E;zSS9G zM^|0y6w?058~QloT`bq-+#T=aKAkx;$x^{F!n$k0@7@1mJ+|H}s$aIlYJJF`@3N)) z*`~j~$Z@O4tJvrB)Tsv+IZJn!=I2;8MnJCvgCE~m!F+aG$a4~H=W5M`S3g2d;1%&?f$Or bGycn9c4(X5sxR_+3_#%N>gTe~DWM4f(hp<& delta 827 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 2 ? 64 : 0)), x * 8, y * 8, 8, 8); } } - // for(u8 x = 0; x < 5; x++){ - // VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, FADE_TOP_I), x * 8, 0, 8, 8); - // VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, FADE_BOTTOM_I), x * 8, 20, 8, 8); - // } + // place 64x64 ground block in sky area (zone 0 only) + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); + zoneBlockVisible = TRUE; + prevCamera = player.camera; + for(u8 i = 0; i < PARALLAX_COUNT; i++) + parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]); + // write initial scroll values so first frame has correct parallax + s16 initScroll = fix32ToInt(-player.camera); + for(u8 i = 0; i < 20; i++) + bgScroll[i] = initScroll; + for(u8 i = 0; i < 8; i++) + bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i])); + VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); + + // fade + // for(u8 x = 0; x < 20; x++) + // VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 + 64), x * 2, 26, 2, 2); + // for(u8 x = 0; x < 10; x++) + // VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 + 64 + 4), x * 4, 22, 4, 4); } void updateBackground(){ - VDP_setHorizontalScroll(BG_B, fix32ToInt(-player.camera)); - VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 3); -} \ No newline at end of file + s16 scrollVal = fix32ToInt(-player.camera); + + // accumulate parallax from camera delta (not absolute position) + // this avoids discontinuities at world wrap boundaries + // GAME_WRAP is already fix32, so use directly (no FIX32 wrapper) + fix32 delta = player.camera - prevCamera; + if(delta > GAME_WRAP / 2) delta -= GAME_WRAP; + else if(delta < -(GAME_WRAP / 2)) delta += GAME_WRAP; + prevCamera = player.camera; + + // update accumulators once, reuse for top and bottom + for(u8 i = 0; i < PARALLAX_COUNT; i++){ + parallaxAccum[i] += fix32Mul(delta, parallaxMul[i]); + if(parallaxAccum[i] > FIX32(1024)) parallaxAccum[i] -= FIX32(1024); + else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024); + } + + for(u8 i = 0; i < 20; i++) + bgScroll[i] = scrollVal; + for(u8 i = 0; i < 8; i++) + bgScroll[27 - i] = (scrollVal - fix32ToInt(parallaxAccum[i])); + + VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA); + + // show ground block only when zone 0 copy of these columns is on screen + fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160)); + bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212)); + if(shouldShow && !zoneBlockVisible){ + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); + zoneBlockVisible = TRUE; + } else if(!shouldShow && zoneBlockVisible){ + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8); + zoneBlockVisible = FALSE; + } +} diff --git a/src/bullets.h b/src/bullets.h index 4540bef..a23be4e 100644 --- a/src/bullets.h +++ b/src/bullets.h @@ -144,11 +144,17 @@ static void collideWithPlayer(u8 i){ fix32ToInt(deltaX), fix32ToInt(deltaY)); if(dist <= 4){ + // convert enemy bullet to player bullet explosion in-place + SPR_setDefinition(bullets[i].image, &pBulletSprite); + bullets[i].player = TRUE; + bullets[i].pos.x = player.pos.x; + bullets[i].pos.y = player.pos.y; killBullet(i, TRUE); sfxExplosion(); player.lives--; if(player.lives == 0){ gameOver = TRUE; + XGM2_stop(); } else { player.recoveringClock = 120; killBullets = TRUE; diff --git a/src/chrome.h b/src/chrome.h index d17a457..dc0ad08 100644 --- a/src/chrome.h +++ b/src/chrome.h @@ -174,6 +174,15 @@ static void updateMap(){ VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow); } +s16 lastLevel; +static void drawLevel(){ + char lvlStr[4]; + uintToStr(level + 1, lvlStr, 1); + VDP_drawText("L", 1, 26); + VDP_drawText(lvlStr, 2, 26); + lastLevel = level; +} + void loadChrome(){ VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA); VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA); @@ -183,6 +192,7 @@ void loadChrome(){ lastScore = 1; drawScore(); drawLives(); + drawLevel(); } bool didGameOver; @@ -191,7 +201,25 @@ static void doGameOver(){ didGameOver = TRUE; for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1); for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1); - for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1); + for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active){ + if(humans[i].state == HUMAN_COLLECTED){ + // spawn player bullet explosion at carried human position + struct bulletSpawner spawner = { + .x = humans[i].pos.x, .y = humans[i].pos.y, + .anim = 0, .speed = 0, .angle = 0, .player = TRUE + }; + void noop(s16 j){ (void)j; } + spawnBullet(spawner, noop); + for(s16 j = BULLET_COUNT - 1; j >= 0; j--){ + if(bullets[j].active && !bullets[j].explosion + && bullets[j].pos.x == humans[i].pos.x && bullets[j].pos.y == humans[i].pos.y){ + killBullet(j, TRUE); + break; + } + } + } + killHuman(i); + } SPR_releaseSprite(player.image); // clear minimap VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H); @@ -210,6 +238,7 @@ static void showPause(){ for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1); for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1); SPR_setPalette(player.image, PAL1); + XGM2_pause(); VDP_drawText("PAUSE", 17, 13); } @@ -218,6 +247,7 @@ static void clearPause(){ for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0); for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL0); SPR_setPalette(player.image, PAL0); + XGM2_resume(); VDP_clearText(17, 13, 5); } @@ -276,5 +306,6 @@ void updateChrome(){ drawScore(); } if(lastLives != player.lives) drawLives(); + if(lastLevel != level) drawLevel(); if(clock % 4 == 0) updateMap(); } \ No newline at end of file diff --git a/src/enemies.h b/src/enemies.h index d7b6936..7fc269d 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -49,10 +49,12 @@ void spawnEnemy(u8 type, u8 zone){ enemies[i].active = FALSE; return; } + SPR_setVisibility(enemies[i].image, HIDDEN); enemies[i].hp = 1; for(u8 j = 0; j < PROP_COUNT; j++){ enemies[i].ints[j] = 0; } + enemies[i].ints[3] = -1; switch(enemies[i].type){ case ENEMY_TYPE_TEST: loadEnemyOne(i); @@ -146,9 +148,31 @@ static void updateEnemy(u8 i){ break; } + // enemy->player collision + if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0){ + fix32 edx = getWrappedDelta(enemies[i].pos.x, player.pos.x); + fix32 edy = enemies[i].pos.y - player.pos.y; + if(edx >= FIX32(-16) && edx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){ + sfxExplosion(); + if(enemies[i].type != ENEMY_TYPE_BOSS){ + enemies[i].hp = 0; + killEnemy(i); + } + player.lives--; + if(player.lives == 0){ + gameOver = TRUE; + XGM2_stop(); + } else { + player.recoveringClock = 120; + killBullets = TRUE; + } + } + } + s16 sx = getScreenX(enemies[i].pos.x, player.camera); s16 sy = fix32ToInt(enemies[i].pos.y); SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN); + SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0); SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off); enemies[i].clock++; diff --git a/src/enemytypes.h b/src/enemytypes.h index 588d0b0..61f0ee4 100644 --- a/src/enemytypes.h +++ b/src/enemytypes.h @@ -1,4 +1,26 @@ -// test enemy -- for testing out bullet stress +// ============================================================================= +// --- Type 0: Test / Fairy (EnemyOne) --- +// ============================================================================= +// The original enemy type. A fairy that shoots 8-bullet circular bursts and +// also seeks/abducts humans (same carry behavior as Builder). +// +// Behavior: +// - When NOT carrying: drifts at speed 2, shoots 8 bullets in a circle +// every 20 frames (only when on screen). Also scans for nearby walking +// humans every 30 frames and steers toward the closest one within 256px. +// Grabs the human when within 16px. +// - When carrying: flies upward (angle 704-832, roughly up-left to up-right) +// at speed 2. Skips all shooting. boundsEnemy() handles reaching the top +// (kills human, self-destructs -- does NOT spawn a Gunner unlike Builder). +// +// ints[0] = random shot timer offset (0-59), desynchronizes shooting from +// other enemies so they don't all fire on the same frame +// ints[2] = target human index (-1 = no target) +// ints[3] = carried human index (-1 = not carrying) +// +// Speed: 2 HP: 1 Shoots: yes (8-bullet radial, every 20 frames) +// Abducts: yes (only 1 human globally at a time via humanBeingCarried flag) +// ============================================================================= void loadEnemyOne(u8 i){ enemies[i].ints[0] = random() % 60; enemies[i].ints[2] = -1; // target human index @@ -90,9 +112,28 @@ void updateEnemyOne(u8 i){ } } +// ============================================================================= // --- Type 1: Drone --- -// Pressure enemy. Homes toward player, simple aimed shots. -// ints[0] = random shot offset, ints[1] = recalc timer +// ============================================================================= +// Bread-and-butter pressure enemy. Periodically recalculates heading toward +// the player and fires single aimed bullets. The main "fodder" type -- use +// high counts (8-16) to create constant movement pressure without overwhelming +// bullet density. +// +// Behavior: +// - Recalculates heading toward player every 30 frames (ints[1] counter). +// Between recalcs, travels in a straight line at speed 2. +// - Fires 1 aimed bullet at player every 40 frames. Only shoots when: +// a) on screen, AND b) level index >= 1 (i.e. L2+, so L1 drones are +// harmless). This is hardcoded, separate from LevelDef.dronesShoot. +// - Bounces off top/bottom screen edges. +// +// ints[0] = random shot timer offset (0-59), prevents synchronized volleys +// ints[1] = heading recalculation timer (counts up to 30) +// +// Speed: 2 HP: 1 Shoots: yes (1 aimed bullet, every 40 frames, L2+ only) +// Abducts: no +// ============================================================================= void loadDrone(u8 i){ enemies[i].ints[0] = random() % 60; enemies[i].ints[1] = 0; @@ -132,9 +173,32 @@ void updateDrone(u8 i){ } } +// ============================================================================= // --- Type 2: Gunner --- -// Bullet geometry. Slow drift, patterned danmaku. -// ints[0] = pattern type (0=radial, 1=aimed fan), ints[1] = shot timer offset, ints[2] = angle accumulator +// ============================================================================= +// Danmaku / bullet geometry enemy. Drifts very slowly (speed 0.5), acting more +// like a turret than a chaser. Only fires when on screen. The bullet pattern is +// set by LevelDef.gunnerPattern (written to ints[0] by loadLevel). +// +// Behavior: +// - Drifts at speed 0.5 in a random initial direction. Bounces off top/bottom. +// - Does nothing when off screen (early return). +// - Pattern 0 (Radial Burst): fires 8 bullets in a circle every 60 frames. +// Start angle rotates by 24 each volley, creating a spiral-over-time effect. +// Bullet speed 3. Predictable, good for learning dodge patterns. +// - Pattern 1 (Aimed Fan): fires 5 bullets aimed at player, spread across +// +-64 angle units, every 45 frames. Bullet speed 3. More aggressive and +// targeted -- harder to dodge at close range. +// - Pattern is set per-gunner at level load. gunnerPattern=2 means each +// gunner randomly picks 0 or 1, creating mixed bullet fields. +// +// ints[0] = pattern type (0=radial, 1=aimed fan). Set by loadLevel, not random. +// ints[1] = random shot timer offset (0-59) +// ints[2] = angle accumulator (radial pattern only, rotates start angle) +// +// Speed: 0.5 HP: 1 Shoots: yes (pattern-dependent, see above) +// Abducts: no +// ============================================================================= void loadGunner(u8 i){ enemies[i].ints[0] = random() % 2; enemies[i].ints[1] = random() % 60; @@ -187,8 +251,30 @@ void updateGunner(u8 i){ } } +// ============================================================================= // --- Type 3: Hunter --- -// Fast chaser. Homes toward player every frame. No shooting. +// ============================================================================= +// Fast, relentless chaser. Homes toward the player EVERY frame with speed 5 +// (matching player's normal speed). Never shoots -- pure body-collision threat. +// Forces the player to keep moving and use focus mode / diagonal movement to +// outrun. Very dangerous in groups. +// +// Behavior: +// - Recalculates angle toward player every frame (no delay like Drone). +// - Moves at speed 5 (player normal speed = 6, focus = 3.5). +// - Bounces off top/bottom screen edges. +// - No shooting, no abduction. Just chases. +// +// No custom ints used. +// +// Speed: 5 HP: 1 Shoots: no Abducts: no +// +// Design notes: +// - 2-3 hunters alongside gunners is very hard (dodge bullets while fleeing) +// - 4+ hunters with no gunners is a pure movement/positioning challenge +// - 6 hunters is near-impossible -- only for late-game punishment +// - Hunters are the anti-camping enemy: you can't sit still +// ============================================================================= void loadHunter(u8 i){ enemies[i].angle = random() % 1024; enemies[i].speed = FIX32(5); @@ -203,9 +289,39 @@ void updateHunter(u8 i){ enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); } +// ============================================================================= // --- Type 4: Builder (Abductor) --- -// Seeks and abducts humans. On reaching top with human, spawns a Gunner. -// ints[0] = scan offset, ints[2] = target human, ints[3] = carried human +// ============================================================================= +// Human abductor. Drifts slowly, scans for walking humans, grabs one, and +// flies upward. If it reaches the top of the screen with a human, the human +// is killed and a Gunner spawns at that position -- punishing the player for +// not intercepting. Only 1 human can be globally carried at a time +// (humanBeingCarried flag). +// +// Behavior: +// - When NOT carrying: drifts at speed 0.7. Scans for nearest walking human +// within 256px every 30 frames. If a target is found, switches to seeking +// speed (1.4) and homes toward it. Grabs when within 16px. +// - When carrying: flies upward (angle 704-832) at speed 1.4. +// boundsEnemy() handles reaching the top: kills the human, spawns a Gunner +// at the builder's position, then self-destructs. +// - If the carried human gets collected by the player while being carried, +// boundsEnemy() detects this and kills the builder (enemy dies, human safe). +// - Cancels its target if another enemy is already carrying a human. +// - No shooting at all. +// +// ints[0] = random scan timer offset (0-59) +// ints[2] = target human index (-1 = no target) +// ints[3] = carried human index (-1 = not carrying) +// +// Speed: 0.7 (drift), 1.4 (seeking/carrying) HP: 1 Shoots: no +// Abducts: yes (kills human + spawns Gunner if it reaches the top) +// +// Design notes: +// - 1 builder adds light urgency. 2 builders is stressful. +// - Pairs well with drones to split the player's attention. +// - The Gunner spawn on success makes ignoring builders snowball badly. +// ============================================================================= void loadBuilder(u8 i){ enemies[i].ints[0] = random() % 60; enemies[i].ints[2] = -1; @@ -274,62 +390,243 @@ void updateBuilder(u8 i){ } } +// ============================================================================= // --- Type 5: Boss --- -// High HP, alternates 2 patterns. hp set by level data via ints[0]. -// ints[0] = initial hp (set by stage), ints[1] = pattern timer, ints[2] = current pattern +// ============================================================================= +// High-HP enemy with phase-based attack patterns. Boss number is set via +// pendingBossNum global before spawn (because spawnEnemy zeroes all ints[]). +// HP is set via pendingBossHp. Both are auto-configured by loadLevel() based +// on the level's bossHp field and level index. +// +// Behavior: +// - Speed 1, bounces off top/bottom. Only attacks when on screen. +// - Attack pattern changes based on remaining HP. HP range is divided into +// N equal phases. Phase 0 = full health, highest phase = near death. +// Later bosses have more phases and higher HP, so they cycle through +// more varied and increasingly aggressive patterns. +// - Each boss (1-5) has a unique updateBoss function with different pattern +// selections and timings per phase. +// +// ints[0] = boss number (0-4), selects which updateBoss variant runs +// ints[4] = max HP (stored at load for phase calculation) +// +// Boss 1 (L4): 25 HP, 2 phases -- radial, then aimed fan +// Boss 2 (L8): 50 HP, 3 phases -- radial, aimed fan, spiral +// Boss 3 (L12): 75 HP, 4 phases -- radial, spiral, aimed fan, double radial +// Boss 4 (L16): 100 HP, 5 phases -- adds wide spray +// Boss 5 (L20): 125 HP, 6 phases -- adds ring burst (all patterns used) +// +// Available boss attack patterns: +// Radial: N bullets in even circle, random start angle. Predictable. +// Aimed Fan: N bullets in arc aimed at player. Targeted. +// Spiral: N bullets in circle, start angle rotates with clock. Mesmerizing. +// Double Radial: Two offset rings (outer fast, inner slow). Dense. +// Wide Spray: N bullets in random spread toward player. Chaotic. +// Ring Burst: N bullets in even circle (like radial but different anim). Climactic. +// +// Speed: 1 HP: varies Shoots: yes (phase-dependent) Abducts: no +// ============================================================================= + +// shared pattern functions +static void bossPatternRadial(u8 i, u8 count, fix32 speed){ + sfxEnemyShotB(); + s16 baseAngle = random() % 1024; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, .y = enemies[i].pos.y, + .anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle, + }; + s16 step = 1024 / count; + for(u8 j = 0; j < count; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += step; + } +} + +static void bossPatternAimedFan(u8 i, u8 count, s16 spread, fix32 speed){ + sfxEnemyShotA(); + fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); + fix32 dy = player.pos.y - enemies[i].pos.y; + s16 aimAngle = honeAngle( + fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), + fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + s16 step = (count > 1) ? (spread * 2) / (count - 1) : 0; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, .y = enemies[i].pos.y, + .anim = 9 + (random() % 3), .speed = speed, .angle = aimAngle - spread, + }; + for(u8 j = 0; j < count; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += step; + } +} + +static void bossPatternSpiral(u8 i, u8 count, fix32 speed){ + sfxEnemyShotC(); + s16 baseAngle = (enemies[i].clock * 17) % 1024; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, .y = enemies[i].pos.y, + .anim = 6 + (random() % 3), .speed = speed, .angle = baseAngle, + }; + s16 step = 1024 / count; + for(u8 j = 0; j < count; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += step; + } +} + +static void bossPatternDoubleRadial(u8 i, u8 count, fix32 speed){ + sfxEnemyShotB(); + s16 baseAngle = random() % 1024; + s16 step = 1024 / count; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, .y = enemies[i].pos.y, + .anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle, + }; + for(u8 j = 0; j < count; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += step; + } + spawner.angle = baseAngle + step / 2; + spawner.speed = speed - FIX32(1); + spawner.anim = 6 + (random() % 3); + for(u8 j = 0; j < count; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += step; + } +} + +static void bossPatternWideSpray(u8 i, u8 count, fix32 speed){ + sfxEnemyShotA(); + fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); + fix32 dy = player.pos.y - enemies[i].pos.y; + s16 aimAngle = honeAngle( + fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), + fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, .y = enemies[i].pos.y, + .anim = 3 + (random() % 9), .speed = speed, .angle = aimAngle, + }; + for(u8 j = 0; j < count; j++){ + spawner.angle = aimAngle - 128 + (random() % 256); + spawner.speed = speed - FIX32(random() % 2); + spawnBullet(spawner, EMPTY); + } +} + +static void bossPatternRingBurst(u8 i, u8 count, fix32 speed){ + sfxEnemyShotB(); + sfxEnemyShotC(); + s16 baseAngle = random() % 1024; + s16 step = 1024 / count; + struct bulletSpawner spawner = { + .x = enemies[i].pos.x, .y = enemies[i].pos.y, + .anim = 9 + (random() % 3), .speed = speed, .angle = baseAngle, + }; + for(u8 j = 0; j < count; j++){ + spawnBullet(spawner, EMPTY); + spawner.angle += step; + } +} + +// get current phase based on HP. phase 0 = full health, higher = more damaged +static u8 getBossPhase(u8 i, u8 numPhases){ + s16 maxHp = enemies[i].ints[4]; + if(maxHp <= 0) return 0; + s16 lost = maxHp - enemies[i].hp; + if(lost < 0) lost = 0; + u8 phase = (lost * numPhases) / maxHp; + if(phase >= numPhases) phase = numPhases - 1; + return phase; +} + void loadBoss(u8 i){ - enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 10; + enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 25; pendingBossHp = 0; + enemies[i].ints[0] = pendingBossNum; + pendingBossNum = 0; enemies[i].ints[1] = 0; - enemies[i].ints[2] = 0; + enemies[i].ints[4] = enemies[i].hp; enemies[i].angle = random() % 1024; enemies[i].speed = FIX32(1); } + +// Boss 1 (L6): 2 patterns, 25 HP +static void updateBossOne(u8 i){ + u8 phase = getBossPhase(i, 2); + if(phase == 0){ + if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 10, FIX32(3)); + } else { + if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 6, 80, FIX32(3)); + } +} + +// Boss 2 (L12): 3 patterns, 50 HP +static void updateBossTwo(u8 i){ + u8 phase = getBossPhase(i, 3); + if(phase == 0){ + if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3)); + } else if(phase == 1){ + if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3)); + } else { + if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 6, FIX32(4)); + } +} + +// Boss 3 (L18): 4 patterns, 75 HP +static void updateBossThree(u8 i){ + u8 phase = getBossPhase(i, 4); + if(phase == 0){ + if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3)); + } else if(phase == 1){ + if(enemies[i].clock % 40 == 0) bossPatternSpiral(i, 8, FIX32(3)); + } else if(phase == 2){ + if(enemies[i].clock % 35 == 0) bossPatternAimedFan(i, 10, 112, FIX32(4)); + } else { + if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 8, FIX32(4)); + } +} + +// Boss 4 (L24): 5 patterns, 100 HP +static void updateBossFour(u8 i){ + u8 phase = getBossPhase(i, 5); + if(phase == 0){ + if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 14, FIX32(3)); + } else if(phase == 1){ + if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3)); + } else if(phase == 2){ + if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 8, FIX32(4)); + } else if(phase == 3){ + if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4)); + } else { + if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 12, FIX32(4)); + } +} + +// Boss 5 (L30): 6 patterns, 125 HP +static void updateBossFive(u8 i){ + u8 phase = getBossPhase(i, 6); + if(phase == 0){ + if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 16, FIX32(3)); + } else if(phase == 1){ + if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 10, 112, FIX32(3)); + } else if(phase == 2){ + if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 10, FIX32(4)); + } else if(phase == 3){ + if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4)); + } else if(phase == 4){ + if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 14, FIX32(5)); + } else { + if(enemies[i].clock % 20 == 0) bossPatternRingBurst(i, 16, FIX32(5)); + } +} + void updateBoss(u8 i){ if(!enemies[i].onScreen) return; - enemies[i].ints[1]++; - // alternate patterns every 180 frames - if(enemies[i].ints[1] >= 180){ - enemies[i].ints[1] = 0; - enemies[i].ints[2] = 1 - enemies[i].ints[2]; - } - if(enemies[i].ints[2] == 0){ - // Pattern A: Radial burst - 12 bullets every 50 frames - if(enemies[i].ints[1] % 50 == 0){ - sfxEnemyShotB(); - s16 baseAngle = random() % 1024; - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, - .y = enemies[i].pos.y, - .anim = 3 + (random() % 3), - .speed = FIX32(3), - .angle = baseAngle, - }; - for(u8 j = 0; j < 12; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += 85; - } - } - } else { - // Pattern B: Aimed wide fan - 8 bullets every 40 frames - if(enemies[i].ints[1] % 40 == 0){ - sfxEnemyShotA(); - fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x); - fix32 dy = player.pos.y - enemies[i].pos.y; - s16 aimAngle = honeAngle( - fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx), - fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy)); - struct bulletSpawner spawner = { - .x = enemies[i].pos.x, - .y = enemies[i].pos.y, - .anim = 9 + (random() % 3), - .speed = FIX32(3), - .angle = aimAngle - 112, - }; - for(u8 j = 0; j < 8; j++){ - spawnBullet(spawner, EMPTY); - spawner.angle += 32; - } - } + switch(enemies[i].ints[0]){ + case 0: updateBossOne(i); break; + case 1: updateBossTwo(i); break; + case 2: updateBossThree(i); break; + case 3: updateBossFour(i); break; + case 4: updateBossFive(i); break; } } \ No newline at end of file diff --git a/src/global.h b/src/global.h index f64d9a4..fed9c96 100644 --- a/src/global.h +++ b/src/global.h @@ -10,6 +10,9 @@ u32 clock; #define CULL_LIMIT FIX32(240) +// #define MUSIC_VOLUME 50 +#define MUSIC_VOLUME 0 + u32 score; #define SCORE_LENGTH 8 @@ -29,6 +32,7 @@ bool paused, isPausing; s16 enemyCount, bulletCount; u8 level; s16 pendingBossHp; +s16 pendingBossNum; bool waitForRelease; bool levelClearing; u32 levelClearClock; diff --git a/src/humans.h b/src/humans.h index db3d57a..acb136f 100644 --- a/src/humans.h +++ b/src/humans.h @@ -25,6 +25,7 @@ void spawnHuman(u8 zone){ humans[i].active = FALSE; return; } + SPR_setVisibility(humans[i].image, HIDDEN); } static void updateHuman(u8 i){ @@ -116,9 +117,16 @@ static void updateHuman(u8 i){ s16 sx = getScreenX(humans[i].pos.x, player.camera); s16 sy = fix32ToInt(humans[i].pos.y); - bool visible = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); + bool visible = (humans[i].state == HUMAN_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN); SPR_setPosition(humans[i].image, sx - HUMAN_OFF, sy - HUMAN_OFF); + // manually animate flash only when walking or falling + if(humans[i].state == HUMAN_WALKING || humans[i].state == HUMAN_FALLING) + SPR_setFrame(humans[i].image, (clock / 15) & 1); + else if(humans[i].state == HUMAN_COLLECTED) + SPR_setAnim(humans[i].image, 1); + else + SPR_setFrame(humans[i].image, 0); } void updateHumans(){ diff --git a/src/main.c b/src/main.c index 9d62b8d..03efb46 100644 --- a/src/main.c +++ b/src/main.c @@ -43,6 +43,11 @@ void loadGame(){ loadPlayer(); loadChrome(); loadLevel(0); + XGM2_play(stageMusic); + XGM2_setFMVolume(MUSIC_VOLUME); + XGM2_setPSGVolume(MUSIC_VOLUME); + player.recoveringClock = 120; + killBullets = TRUE; started = TRUE; } @@ -59,25 +64,30 @@ static void updateGame(){ loadBackground(); loadChrome(); loadLevel(level + 1); + XGM2_play(stageMusic); SPR_setVisibility(player.image, VISIBLE); + player.recoveringClock = 120; + killBullets = TRUE; } return; } if(!paused){ updatePlayer(); + updateBackground(); if(clock % 2 == 0){ updateEnemies(); if(!gameOver && enemyCount == 0){ if(level >= LEVEL_COUNT - 1){ gameOver = TRUE; + XGM2_stop(); } else { levelClearing = TRUE; levelClearClock = 0; + XGM2_stop(); } } updateHumans(); } else { - updateBackground(); updateBullets(); } } diff --git a/src/player.h b/src/player.h index e91efe1..511365e 100644 --- a/src/player.h +++ b/src/player.h @@ -1,9 +1,9 @@ -#define PLAYER_SPEED FIX32(5) +#define PLAYER_SPEED FIX32(6) -#define PLAYER_SPEED_FOCUS FIX32(3) +#define PLAYER_SPEED_FOCUS FIX32(3.5) -#define PLAYER_ACCEL PLAYER_SPEED >> 3 -#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 3 +#define PLAYER_ACCEL PLAYER_SPEED >> 4 +#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 4 #define PLAYER_OFF 24 #define PLAYER_BOUND_Y FIX32(PLAYER_OFF) diff --git a/src/stage.h b/src/stage.h index ce32a17..7ca21c9 100644 --- a/src/stage.h +++ b/src/stage.h @@ -1,55 +1,137 @@ +// ============================================================================= +// LEVEL DESIGN GUIDE +// ============================================================================= +// +// Each level is a single struct defining what spawns. The level ends when all +// enemies are killed (enemyCount == 0). Humans are bonus -- they don't affect +// level completion. +// +// --- STRUCT FIELDS --- +// +// drones Number of Drone enemies (type 1). Bulk pressure enemy. +// Speed 2, homes toward player every 30 frames. +// Fires 1 aimed bullet every 40 frames (only on L2+, i.e. index >= 1). +// Use dronesShoot=FALSE on L1 to introduce them without bullets. +// Good range: 6-16. Above 14 gets chaotic. +// +// gunners Number of Gunner enemies (type 2). Danmaku / bullet geometry. +// Speed 0.5 (slow drift), only shoots when on screen. +// Pattern controlled by gunnerPattern field (see below). +// Good range: 0-6. Even 2-3 gunners create significant bullet density. +// +// hunters Number of Hunter enemies (type 3). Fast chaser, no shooting. +// Speed 5 (matches player!). Homes every frame. Pure body pressure. +// Very dangerous -- 2-3 is oppressive, 6 is near-impossible. +// Good range: 0-6. Introduce after players learn movement. +// +// builders Number of Builder enemies (type 4). Human abductor. +// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking humans +// and flies upward. If it reaches the top, the human dies and a +// Gunner spawns in its place. Only 1 human can be carried at a time. +// Creates urgency -- player must choose between killing enemies +// and saving humans. Good range: 0-2. +// +// bossHp If > 0, spawns a Boss enemy (type 5) with this many HP. +// Boss number is auto-calculated from level index (lvl / 4). +// Set to 0 for non-boss levels. Boss speed is 1, bounces around. +// Boss has multiple attack phases based on remaining HP. +// Typical values: 25, 50, 75, 100, 125. +// Other enemies can coexist with the boss (adds pressure). +// +// humans Number of humans (koakuma) to spawn. Distributed across 4 zones +// (2 per zone if >= 4 humans, then 1 each for remainder). +// Walk along the ground, can be collected by player for 1000 pts +// (2000 if caught mid-fall after enemy drops them). +// Max 8 (HUMAN_COUNT). Usually just set to 8. +// +// gunnerPattern Controls what bullet pattern gunners use: +// 0 = Radial Burst: 8 bullets in a circle every 60 frames. +// Rotating start angle. Steady, predictable pressure. +// 1 = Aimed Fan: 5 bullets aimed at player, spread +-64, +// every 45 frames. More targeted/aggressive. +// 2 = Mix: each gunner randomly picks 0 or 1 at spawn. +// Creates varied, less predictable bullet fields. +// +// dronesShoot TRUE = drones fire aimed bullets (normal behavior on L2+). +// FALSE = drones still home toward player but never shoot. +// Only meaningful for the very first level as a gentle intro. +// (Drone shooting is also gated by level >= 1 in code, so +// L1 drones never shoot regardless of this flag.) +// +// --- LIMITS --- +// +// Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss +// must not exceed 24. If it does, excess enemies silently fail to spawn. +// +// Total humans: 8 slots (HUMAN_COUNT). +// +// Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets +// get priority and evict enemy bullets when full. +// +// --- SPAWNING --- +// +// Enemies are distributed across 4 zones (each 512px of the 2048px world). +// Enemy i spawns in zone (i % 4). They never spawn within 240px of the player +// and maintain 64px minimum spacing from each other. +// +// Boss always spawns in zone 1. +// +// --- DESIGN TIPS --- +// +// - Drone-heavy levels (12-16) create constant movement pressure +// - Gunner-heavy levels (4-6) create bullet reading / dodging challenges +// - Hunter levels force the player to keep moving (anti-camping) +// - Builder levels force tough choices: kill builders or save humans? +// - Combining hunters + gunners is very hard (dodge bullets while fleeing) +// - Boss levels with escort enemies (drones/gunners alongside boss) are +// harder than solo boss fights +// - A "farm" level (lots of drones, no gunners) gives score-building breathers +// - gunnerPattern 0 (radial) is easier to dodge than 1 (aimed fan) +// +// ============================================================================= + struct LevelDef { u8 drones, gunners, hunters, builders; u8 bossHp; u8 humans; - u8 gunnerPattern; // 0=radial, 1=aimed fan, 2=mix + u8 gunnerPattern; bool dronesShoot; }; -// dr gn hn bl boss hum pat shoot -const struct LevelDef levels[30] = { - // Phase 1: "Immediate danger" (L1-L6) +// dr gn hn bl boss hum pat shoot +const struct LevelDef levels[20] = { + // Phase 1: "Immediate danger" (L1-L4) { 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1 { 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2 - { 12, 2, 0, 0, 0, 8, 0, TRUE }, // L3 - { 10, 3, 0, 0, 0, 8, 1, TRUE }, // L4 - { 14, 3, 0, 0, 0, 8, 1, TRUE }, // L5 - { 8, 0, 0, 0, 8, 8, 0, TRUE }, // L6 BOSS + { 12, 3, 0, 0, 0, 8, 1, TRUE }, // L3 + { 8, 0, 0, 0, 25, 8, 0, TRUE }, // L4 BOSS 1 - // Phase 2: "You can't save everything" (L7-L12) - { 10, 0, 0, 1, 0, 8, 0, TRUE }, // L7 - { 10, 2, 0, 1, 0, 8, 0, TRUE }, // L8 - { 12, 0, 0, 2, 0, 8, 0, TRUE }, // L9 - { 14, 3, 0, 1, 0, 8, 1, TRUE }, // L10 WALL - { 10, 2, 0, 2, 0, 8, 2, TRUE }, // L11 - { 8, 0, 0, 1, 12, 8, 0, TRUE }, // L12 BOSS + // Phase 2: "You can't save everything" (L5-L8) + { 10, 2, 0, 1, 0, 8, 0, TRUE }, // L5 + { 14, 3, 0, 1, 0, 8, 1, TRUE }, // L6 + { 10, 2, 0, 2, 0, 8, 2, TRUE }, // L7 + { 8, 0, 0, 1, 50, 8, 0, TRUE }, // L8 BOSS 2 - // Phase 3: "Geometry matters" (L13-L18) - { 8, 0, 4, 0, 0, 8, 0, TRUE }, // L13 - { 8, 3, 2, 0, 0, 8, 1, TRUE }, // L14 - { 16, 0, 0, 0, 0, 8, 0, TRUE }, // L15 FARM - { 10, 2, 4, 0, 0, 8, 2, TRUE }, // L16 - { 12, 3, 3, 0, 0, 8, 1, TRUE }, // L17 - { 0, 2, 2, 0, 15, 8, 2, TRUE }, // L18 BOSS + // Phase 3: "Geometry matters" (L9-L12) + { 8, 3, 4, 0, 0, 8, 1, TRUE }, // L9 + { 10, 2, 4, 0, 0, 8, 2, TRUE }, // L10 + { 12, 3, 3, 0, 0, 8, 1, TRUE }, // L11 + { 0, 2, 2, 0, 75, 8, 2, TRUE }, // L12 BOSS 3 - // Phase 4: "Suffocation" (L19-L24) - { 12, 4, 0, 0, 0, 8, 2, TRUE }, // L19 - { 14, 4, 0, 2, 0, 8, 2, TRUE }, // L20 WALL - { 10, 0, 6, 0, 0, 8, 0, TRUE }, // L21 - { 12, 4, 2, 0, 0, 8, 1, TRUE }, // L22 - { 14, 4, 0, 2, 0, 8, 2, TRUE }, // L23 - { 0, 3, 0, 1, 20, 8, 2, TRUE }, // L24 BOSS + // Phase 4: "Suffocation" (L13-L16) + { 14, 4, 0, 2, 0, 8, 2, TRUE }, // L13 + { 10, 0, 6, 0, 0, 8, 0, TRUE }, // L14 + { 12, 4, 2, 0, 0, 8, 1, TRUE }, // L15 + { 0, 3, 0, 1, 100, 8, 2, TRUE }, // L16 BOSS 4 - // Phase 5: "Arcade cruelty" (L25-L30) - { 16, 0, 4, 0, 0, 8, 0, TRUE }, // L25 - { 12, 6, 0, 0, 0, 8, 2, TRUE }, // L26 - { 14, 2, 4, 0, 0, 8, 1, TRUE }, // L27 - { 16, 4, 0, 2, 0, 8, 2, TRUE }, // L28 - { 6, 2, 2, 1, 10, 8, 2, TRUE }, // L29 MINI-BOSS - { 4, 2, 2, 1, 30, 8, 2, TRUE }, // L30 FINAL + // Phase 5: "Arcade cruelty" (L17-L20) + { 16, 0, 4, 0, 0, 8, 0, TRUE }, // L17 + { 14, 4, 4, 2, 0, 8, 2, TRUE }, // L18 + { 6, 2, 2, 1, 50, 8, 2, TRUE }, // L19 MINI-BOSS + { 4, 2, 2, 1, 125, 8, 2, TRUE }, // L20 BOSS 5 FINAL }; -#define LEVEL_COUNT 30 +#define LEVEL_COUNT 20 static void distributeEnemies(u8 type, u8 count){ for(u8 i = 0; i < count; i++){ @@ -80,6 +162,9 @@ void loadLevel(u8 lvl){ if(def->bossHp > 0){ pendingBossHp = def->bossHp; + pendingBossNum = lvl / 4; // L3=0, L7=1, L11=2, L15=3, L18+=4 + if(pendingBossNum > 4) pendingBossNum = 4; + if(lvl == 18) pendingBossNum = 1; // L19 mini-boss reuses boss 2 spawnEnemy(ENEMY_TYPE_BOSS, 1); }