From 0b905f269029aedc4022173b61cbff3a19b4525f Mon Sep 17 00:00:00 2001 From: Trevor Boddy Date: Sat, 14 Feb 2026 19:31:58 -0500 Subject: [PATCH] init --- .gitignore | 20 ++ build.sh | 6 + compile.sh | 2 + res/bullets.png | Bin 0 -> 8340 bytes res/butterfly.png | Bin 0 -> 2413 bytes res/fadebottom.png | Bin 0 -> 1938 bytes res/fadetop.png | Bin 0 -> 4561 bytes res/font.png | Bin 0 -> 3213 bytes res/logo.png | Bin 0 -> 2232 bytes res/pbullet.png | Bin 0 -> 1994 bytes res/resources.res | 15 ++ res/sakuya.png | Bin 0 -> 1754 bytes res/shadow.png | Bin 0 -> 1678 bytes res/sky.png | Bin 0 -> 5258 bytes run.sh | 1 + src/background.h | 27 +++ src/boot/rom_head.c | 33 +++ src/boot/sega.s | 487 ++++++++++++++++++++++++++++++++++++++++++++ src/bullets.h | 171 ++++++++++++++++ src/chrome.h | 22 ++ src/enemies.h | 70 +++++++ src/global.h | 142 +++++++++++++ src/main.c | 51 +++++ src/player.h | 153 ++++++++++++++ src/stage.h | 3 + src/start.h | 7 + 26 files changed, 1210 insertions(+) create mode 100644 .gitignore create mode 100755 build.sh create mode 100755 compile.sh create mode 100644 res/bullets.png create mode 100644 res/butterfly.png create mode 100644 res/fadebottom.png create mode 100644 res/fadetop.png create mode 100644 res/font.png create mode 100644 res/logo.png create mode 100644 res/pbullet.png create mode 100644 res/resources.res create mode 100644 res/sakuya.png create mode 100644 res/shadow.png create mode 100644 res/sky.png create mode 100755 run.sh create mode 100644 src/background.h create mode 100644 src/boot/rom_head.c create mode 100644 src/boot/sega.s create mode 100644 src/bullets.h create mode 100644 src/chrome.h create mode 100644 src/enemies.h create mode 100644 src/global.h create mode 100644 src/main.c create mode 100644 src/player.h create mode 100644 src/stage.h create mode 100644 src/start.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01131ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +CLAUDE.md +.DS_Store +**/.DS_Store +Thumbs.db +**/Thumbs.db +out.bin +out.elf +out/* +symbol.txt +blastem/ +boot/rom_head.bin +boot/sega.o +res/resources.o +res/resources.h +dist/ +*.psd +**/**.psd +*.pdf +**/**.pdf +docs/ \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..41b0d7a --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +rm -rf res/resources.o res/resources.h out/* +# make +# ./blastem/blastem out.bin +#dgen out.bin +docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00 +/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin \ No newline at end of file diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..86704ed --- /dev/null +++ b/compile.sh @@ -0,0 +1,2 @@ +rm -rf res/resources.o res/resources.h out/* +docker run --rm -v $PWD:/m68k -t registry.gitlab.com/doragasu/docker-sgdk:v2.00 \ No newline at end of file diff --git a/res/bullets.png b/res/bullets.png new file mode 100644 index 0000000000000000000000000000000000000000..b234322fc278f82ead07bc1319f491df9a3d40e4 GIT binary patch literal 8340 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 literal 0 HcmV?d00001 diff --git a/res/fadebottom.png b/res/fadebottom.png new file mode 100644 index 0000000000000000000000000000000000000000..a2a204f0ac6eb40b3c3dcd332ff2dc777c194090 GIT binary patch literal 1938 zcmb_dONbmr7;fE-nvFpr7Zt?bj>v|!rmMPprl*U{KHQnOL(=QIJCKpRSY2H`)9g%l z+uifn4U()O2Zey*MMTB*=)rgkK~d3*LM}^S@!&zhgJMGP1%fD6&!e-EO3y!HHuRN}Jo~ zN>kmo{r6dP&@}T@hj}h3-4e0{kNa)o@^v!`-A!CB0)HC zD7o$u-hAsJLAYnrtyJTxc>+_vnIX1su}rraaI_#Co$dyNuCiFPSk3hc()Eu%l|MUNk5bFPhvXfs8URNvj96X^IP4VWSt^rZQ`=CiCKmV^N9?>VE7; zb^jLB>F1jQ@ba2w8e>;2&1M=Ric4*t#-Jd(qNB=Mz~m(s`K^$$Qk%;>oMaQ=xsZ|A z4=cXk7&dEhC|OkWj4Do;gt}gm#E~6SSdql6Ao0FI0CPamDkz7uI&U3-8iueBHGSK4 z)>3E=B>hz@NW*Fls|tYG8&GZ>o5bWl!8XN?A2tb3*=>>C8>_sv0C@U8W63;s0x;8fQ1UWj?fSyBHpyezFSdLDCW2reqb)aUeXgI&<(^k^k zWPac3wof_7ji?T?vbwQS5yKVP1jVN5jC&$Yr@q$6S{o0k;YEeM3v1M z`FcRm;uAGhwSblsAf~0I*Ffq7Qypw#GAJ^SRu>og~46J6??A0geGn; zNHl3Pv!%gncb9tXDe_=`r_MO(ZJOVY9{En(At9Ts@!IXdUh=KvN2Gmw@i_vsR6#0W zOjCe{ECMXeaDbE3k&)*eHuU^3`BeN4J0b&TvqL-HnfO6zXq|X9#>$h5S^x_{JY6`H8er zF3wh}=dQi}?AXls{a-9yKV5t5<4OD7`N_ZH0~@nfgx6l&7;nvd|La#Hqg&_Oub%yL z`QFQyCuSE8J^rEj^XQ5KznyuaHM4*F^aJm0jjmj)ojHT6zf3>+*}jpfPfmqb_tjt7 QN>09VX`%SRQ_sKj4=c-Q`v3p{ literal 0 HcmV?d00001 diff --git a/res/fadetop.png b/res/fadetop.png new file mode 100644 index 0000000000000000000000000000000000000000..c697e2a12165336b40cff3d75756bc17cab8ac8f GIT binary patch literal 4561 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 UH>N}-&$m1q3wuJLnfJl}0pN|2;s5{u literal 0 HcmV?d00001 diff --git a/res/font.png b/res/font.png new file mode 100644 index 0000000000000000000000000000000000000000..ba87fa7838705cd1ff1a71b576df9350741e68bd GIT binary patch literal 3213 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 u`5Gv-Z+5z_HXx}BZ&RuAy`s@)g(KyW0-IIC-0> 2); +} \ No newline at end of file diff --git a/src/boot/rom_head.c b/src/boot/rom_head.c new file mode 100644 index 0000000..50f0b0b --- /dev/null +++ b/src/boot/rom_head.c @@ -0,0 +1,33 @@ +#include "genesis.h" + +__attribute__((externally_visible)) +const ROMHeader rom_header = { +#if (ENABLE_BANK_SWITCH != 0) + "SEGA SSF ", +#elif (MODULE_MEGAWIFI != 0) + "SEGA MEGAWIFI ", +#else + "SEGA MEGA DRIVE ", +#endif + "(C)SGDK 2024 ", + "SAMPLE PROGRAM ", + "SAMPLE PROGRAM ", + "GM 00000000-00", + 0x000, + "JD ", + 0x00000000, +#if (ENABLE_BANK_SWITCH != 0) + 0x003FFFFF, +#else + 0x000FFFFF, +#endif + 0xE0FF0000, + 0xE0FFFFFF, + "RA", + 0xF820, + 0x00200000, + 0x0020FFFF, + " ", + "DEMONSTRATION PROGRAM ", + "JUE " +}; diff --git a/src/boot/sega.s b/src/boot/sega.s new file mode 100644 index 0000000..6dcde0b --- /dev/null +++ b/src/boot/sega.s @@ -0,0 +1,487 @@ +#include "task_cst.h" + +.section .text.keepboot + +*------------------------------------------------------- +* +* Sega startup code for the GNU Assembler +* Translated from: +* Sega startup code for the Sozobon C compiler +* Written by Paul W. Lee +* Modified by Charles Coty +* Modified by Stephane Dallongeville +* +*------------------------------------------------------- + + .globl rom_header + + .org 0x00000000 + +_Start_Of_Rom: +_Vecteurs_68K: + dc.l __stack /* Stack address */ + dc.l _Entry_Point /* Program start address */ + dc.l _Bus_Error + dc.l _Address_Error + dc.l _Illegal_Instruction + dc.l _Zero_Divide + dc.l _Chk_Instruction + dc.l _Trapv_Instruction + dc.l _Privilege_Violation + dc.l _Trace + dc.l _Line_1010_Emulation + dc.l _Line_1111_Emulation + dc.l _Error_Exception, _Error_Exception, _Error_Exception, _Error_Exception + dc.l _Error_Exception, _Error_Exception, _Error_Exception, _Error_Exception + dc.l _Error_Exception, _Error_Exception, _Error_Exception, _Error_Exception + dc.l _Error_Exception + dc.l _INT + dc.l _EXTINT + dc.l _INT + dc.l hintCaller + dc.l _INT + dc.l _VINT + dc.l _INT + dc.l _trap_0 /* Resume supervisor task */ + dc.l _INT,_INT,_INT,_INT,_INT,_INT,_INT + dc.l _INT,_INT,_INT,_INT,_INT,_INT,_INT,_INT + dc.l _INT,_INT,_INT,_INT,_INT,_INT,_INT,_INT + dc.l _INT,_INT,_INT,_INT,_INT,_INT,_INT,_INT + +rom_header: + .incbin "out/rom_head.bin", 0, 0x100 + +_Entry_Point: +* disable interrupts + move #0x2700,%sr + +* Configure a USER_STACK_LENGTH bytes user stack at bottom, and system stack on top of it + move %sp, %usp + sub #USER_STACK_LENGTH, %sp + +* Halt Z80 (need to be done as soon as possible on reset) + move.l #0xA11100,%a0 /* Z80_HALT_PORT */ + move.w #0x0100,%d0 + move.w %d0,(%a0) /* HALT Z80 */ + move.w %d0,0x0100(%a0) /* END RESET Z80 */ + + tst.l 0xa10008 + bne.s SkipInit + + tst.w 0xa1000c + bne.s SkipInit + +* Check Version Number + move.b -0x10ff(%a0),%d0 + andi.b #0x0f,%d0 + beq.s NoTMSS + +* Sega Security Code (SEGA) + move.l #0x53454741,0x2f00(%a0) + +NoTMSS: + jmp _start_entry + +SkipInit: + jmp _reset_entry + + +*------------------------------------------------ +* +* interrupt functions +* +*------------------------------------------------ + +registersDump: + move.l %d0,registerState+0 + move.l %d1,registerState+4 + move.l %d2,registerState+8 + move.l %d3,registerState+12 + move.l %d4,registerState+16 + move.l %d5,registerState+20 + move.l %d6,registerState+24 + move.l %d7,registerState+28 + move.l %a0,registerState+32 + move.l %a1,registerState+36 + move.l %a2,registerState+40 + move.l %a3,registerState+44 + move.l %a4,registerState+48 + move.l %a5,registerState+52 + move.l %a6,registerState+56 + move.l %a7,registerState+60 + rts + +busAddressErrorDump: + move.w 4(%sp),ext1State + move.l 6(%sp),addrState + move.w 10(%sp),ext2State + move.w 12(%sp),srState + move.l 14(%sp),pcState + jmp registersDump + +exception4WDump: + move.w 4(%sp),srState + move.l 6(%sp),pcState + move.w 10(%sp),ext1State + jmp registersDump + +exceptionDump: + move.w 4(%sp),srState + move.l 6(%sp),pcState + jmp registersDump + + +_Bus_Error: + jsr busAddressErrorDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l busErrorCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Address_Error: + jsr busAddressErrorDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l addressErrorCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Illegal_Instruction: + jsr exception4WDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l illegalInstCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Zero_Divide: + jsr exceptionDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l zeroDivideCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Chk_Instruction: + jsr exception4WDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l chkInstCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Trapv_Instruction: + jsr exception4WDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l trapvInstCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Privilege_Violation: + jsr exceptionDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l privilegeViolationCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Trace: + jsr exceptionDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l traceCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Line_1010_Emulation: +_Line_1111_Emulation: + jsr exceptionDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l line1x1xCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_Error_Exception: + jsr exceptionDump + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l errorExceptionCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_INT: + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l intCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_EXTINT: + movem.l %d0-%d1/%a0-%a1,-(%sp) + move.l eintCB, %a0 + jsr (%a0) + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +_VINT: + btst #5, (%sp) /* Skip context switch if not in user task */ + bne.s no_user_task + + tst.w task_lock + bne.s 1f + move.w #0, -(%sp) /* TSK_superPend() will return 0 */ + bra.s unlock /* If lock == 0, supervisor task is not locked */ + +1: + bcs.s no_user_task /* If lock < 0, super is locked with infinite wait */ + subq.w #1, task_lock /* Locked with wait, subtract 1 to the frame count */ + bne.s no_user_task /* And do not unlock if we did not reach 0 */ + move.w #1, -(%sp) /* TSK_superPend() will return 1 */ + +unlock: + /* Save bg task registers (excepting a7, that is stored in usp) */ + move.l %a0, task_regs + lea (task_regs + UTSK_REGS_LEN), %a0 + movem.l %d0-%d7/%a1-%a6, -(%a0) + + move.w (%sp)+, %d0 /* Load return value previously pushed to stack */ + + move.w (%sp)+, task_sr /* Pop user task sr and pc, and save them, */ + move.l (%sp)+, task_pc /* so they can be restored later. */ + movem.l (%sp)+, %d2-%d7/%a2-%a6 /* Restore non clobberable registers */ + +no_user_task: + /* At this point, we always have in the stack the SR and PC of the task */ + /* we want to jump after processing the interrupt, that might be the */ + /* point where we came from (if there is no context switch) or the */ + /* supervisor task (if we unlocked it). */ + + movem.l %d0-%d1/%a0-%a1,-(%sp) + ori.w #0x0001, intTrace /* in V-Int */ + addq.l #1, vtimer /* increment frame counter (more a vint counter) */ + btst #3, VBlankProcess+1 /* PROCESS_XGM_TASK ? (use VBlankProcess+1 as btst is a byte operation) */ + beq.s no_xgm_task + + jsr XGM_doVBlankProcess /* do XGM vblank task */ + +no_xgm_task: + btst #1, VBlankProcess+1 /* PROCESS_BITMAP_TASK ? (use VBlankProcess+1 as btst is a byte operation) */ + beq.s no_bmp_task + + jsr BMP_doVBlankProcess /* do BMP vblank task */ + +no_bmp_task: + move.l vintCB, %a0 /* load user callback */ + jsr (%a0) /* call user callback */ + andi.w #0xFFFE, intTrace /* out V-Int */ + movem.l (%sp)+,%d0-%d1/%a0-%a1 + rte + +*------------------------------------------------ +* +* Copyright (c) 1988 by Sozobon, Limited. Author: Johann Ruegg +* +* Permission is granted to anyone to use this software for any purpose +* on any computer system, and to redistribute it freely, with the +* following restrictions: +* 1) No charge may be made other than reasonable charges for reproduction. +* 2) Modified versions must be clearly marked as such. +* 3) The authors are not responsible for any harmful consequences +* of using this software, even if they result from defects in it. +* +*------------------------------------------------ + +ldiv: + move.l 4(%a7),%d0 + bpl ld1 + neg.l %d0 +ld1: + move.l 8(%a7),%d1 + bpl ld2 + neg.l %d1 + eor.b #0x80,4(%a7) +ld2: + bsr i_ldiv /* d0 = d0/d1 */ + tst.b 4(%a7) + bpl ld3 + neg.l %d0 +ld3: + rts + +lmul: + move.l 4(%a7),%d0 + bpl lm1 + neg.l %d0 +lm1: + move.l 8(%a7),%d1 + bpl lm2 + neg.l %d1 + eor.b #0x80,4(%a7) +lm2: + bsr i_lmul /* d0 = d0*d1 */ + tst.b 4(%a7) + bpl lm3 + neg.l %d0 +lm3: + rts + +lrem: + move.l 4(%a7),%d0 + bpl lr1 + neg.l %d0 +lr1: + move.l 8(%a7),%d1 + bpl lr2 + neg.l %d1 +lr2: + bsr i_ldiv /* d1 = d0%d1 */ + move.l %d1,%d0 + tst.b 4(%a7) + bpl lr3 + neg.l %d0 +lr3: + rts + +ldivu: + move.l 4(%a7),%d0 + move.l 8(%a7),%d1 + bsr i_ldiv + rts + +lmulu: + move.l 4(%a7),%d0 + move.l 8(%a7),%d1 + bsr i_lmul + rts + +lremu: + move.l 4(%a7),%d0 + move.l 8(%a7),%d1 + bsr i_ldiv + move.l %d1,%d0 + rts +* +* A in d0, B in d1, return A*B in d0 +* +i_lmul: + move.l %d3,%a2 /* save d3 */ + move.w %d1,%d2 + mulu %d0,%d2 /* d2 = Al * Bl */ + + move.l %d1,%d3 + swap %d3 + mulu %d0,%d3 /* d3 = Al * Bh */ + + swap %d0 + mulu %d1,%d0 /* d0 = Ah * Bl */ + + add.l %d3,%d0 /* d0 = (Ah*Bl + Al*Bh) */ + swap %d0 + clr.w %d0 /* d0 = (Ah*Bl + Al*Bh) << 16 */ + + add.l %d2,%d0 /* d0 = A*B */ + move.l %a2,%d3 /* restore d3 */ + rts +* +*A in d0, B in d1, return A/B in d0, A%B in d1 +* +i_ldiv: + tst.l %d1 + bne nz1 + +* divide by zero +* divu #0,%d0 /* cause trap */ + move.l #0x80000000,%d0 + move.l %d0,%d1 + rts +nz1: + move.l %d3,%a2 /* save d3 */ + cmp.l %d1,%d0 + bhi norm + beq is1 +* AB and B is not 0 +norm: + cmp.l #1,%d1 + bne not1 +* B==1, so ret A, rem 0 + clr.l %d1 + move.l %a2,%d3 /* restore d3 */ + rts +* check for A short (implies B short also) +not1: + cmp.l #0xffff,%d0 + bhi slow +* A short and B short -- use 'divu' + divu %d1,%d0 /* d0 = REM:ANS */ + swap %d0 /* d0 = ANS:REM */ + clr.l %d1 + move.w %d0,%d1 /* d1 = REM */ + clr.w %d0 + swap %d0 + move.l %a2,%d3 /* restore d3 */ + rts +* check for B short +slow: + cmp.l #0xffff,%d1 + bhi slower +* A long and B short -- use special stuff from gnu + move.l %d0,%d2 + clr.w %d2 + swap %d2 + divu %d1,%d2 /* d2 = REM:ANS of Ahi/B */ + clr.l %d3 + move.w %d2,%d3 /* d3 = Ahi/B */ + swap %d3 + + move.w %d0,%d2 /* d2 = REM << 16 + Alo */ + divu %d1,%d2 /* d2 = REM:ANS of stuff/B */ + + move.l %d2,%d1 + clr.w %d1 + swap %d1 /* d1 = REM */ + + clr.l %d0 + move.w %d2,%d0 + add.l %d3,%d0 /* d0 = ANS */ + move.l %a2,%d3 /* restore d3 */ + rts +* A>B, B > 1 +slower: + move.l #1,%d2 + clr.l %d3 +moreadj: + cmp.l %d0,%d1 + bhs adj + add.l %d2,%d2 + add.l %d1,%d1 + bpl moreadj +* we shifted B until its >A or sign bit set +* we shifted #1 (d2) along with it +adj: + cmp.l %d0,%d1 + bhi ltuns + or.l %d2,%d3 + sub.l %d1,%d0 +ltuns: + lsr.l #1,%d1 + lsr.l #1,%d2 + bne adj +* d3=answer, d0=rem + move.l %d0,%d1 + move.l %d3,%d0 + move.l %a2,%d3 /* restore d3 */ + rts diff --git a/src/bullets.h b/src/bullets.h new file mode 100644 index 0000000..482ecc3 --- /dev/null +++ b/src/bullets.h @@ -0,0 +1,171 @@ +#define BULLET_OFF 8 +#define P_BULLET_OFF 16 + +static void doBulletRotation(u8 i){ + if(bullets[i].anim >= FIRST_ROTATING_BULLET && !bullets[i].player){ + bullets[i].vFlip = FALSE; + bullets[i].hFlip = FALSE; + + if(bullets[i].angle < 0) bullets[i].angle += 1024; + else if(bullets[i].angle >= 1024) bullets[i].angle -= 1024; + + + // 0 - 256 + if(bullets[i].angle >= 1008 || bullets[i].angle < 16) bullets[i].frame = 0; + else if(bullets[i].angle >= 16 && bullets[i].angle < 48) bullets[i].frame = 1; + else if(bullets[i].angle >= 48 && bullets[i].angle < 80) bullets[i].frame = 2; + else if(bullets[i].angle >= 80 && bullets[i].angle < 112) bullets[i].frame = 3; + else if(bullets[i].angle >= 112 && bullets[i].angle < 144) bullets[i].frame = 4; + else if(bullets[i].angle >= 112 && bullets[i].angle < 176) bullets[i].frame = 5; + else if(bullets[i].angle >= 176 && bullets[i].angle < 208) bullets[i].frame = 6; + else if(bullets[i].angle >= 208 && bullets[i].angle < 240) bullets[i].frame = 7; + else if(bullets[i].angle >= 240 && bullets[i].angle < 272) bullets[i].frame = 8; + + // 256 - 512 + else if(bullets[i].angle >= 272 && bullets[i].angle < 304) { bullets[i].frame = 7; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 304 && bullets[i].angle < 336) { bullets[i].frame = 6; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 336 && bullets[i].angle < 368) { bullets[i].frame = 5; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 368 && bullets[i].angle < 400) { bullets[i].frame = 4; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 400 && bullets[i].angle < 432) { bullets[i].frame = 3; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 432 && bullets[i].angle < 464) { bullets[i].frame = 2; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 464 && bullets[i].angle < 496) { bullets[i].frame = 1; bullets[i].hFlip = TRUE; } + else if(bullets[i].angle >= 496 && bullets[i].angle < 528) { bullets[i].frame = 0; bullets[i].hFlip = TRUE; } + + // 512 - 768 + else if(bullets[i].angle >= 528 && bullets[i].angle < 560) { bullets[i].frame = 1; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 560 && bullets[i].angle < 592) { bullets[i].frame = 2; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 592 && bullets[i].angle < 624) { bullets[i].frame = 3; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 624 && bullets[i].angle < 656) { bullets[i].frame = 4; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 656 && bullets[i].angle < 688) { bullets[i].frame = 5; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 688 && bullets[i].angle < 720) { bullets[i].frame = 6; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 720 && bullets[i].angle < 752) { bullets[i].frame = 7; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 752 && bullets[i].angle < 784) { bullets[i].frame = 8; bullets[i].hFlip = TRUE; bullets[i].vFlip = TRUE; } + + // 768 - 1024 + else if(bullets[i].angle >= 784 && bullets[i].angle < 816) { bullets[i].frame = 7; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 816 && bullets[i].angle < 848) { bullets[i].frame = 6; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 848 && bullets[i].angle < 880) { bullets[i].frame = 5; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 880 && bullets[i].angle < 912) { bullets[i].frame = 4; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 912 && bullets[i].angle < 944) { bullets[i].frame = 3; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 944 && bullets[i].angle < 976) { bullets[i].frame = 2; bullets[i].vFlip = TRUE; } + else if(bullets[i].angle >= 976 && bullets[i].angle < 1008) { bullets[i].frame = 1; bullets[i].vFlip = TRUE; } + + SPR_setFrame(bullets[i].image, bullets[i].frame); + SPR_setHFlip(bullets[i].image, bullets[i].hFlip); + SPR_setVFlip(bullets[i].image, bullets[i].vFlip); + } +} + +void spawnBullet(struct bulletSpawner spawner, void(*updater)){ + s16 i = -1; + for(s16 j = 0; j < BULLET_COUNT; j++) if(!bullets[j].active && i == -1) i = j; + if(i > -1){ + bullets[i].active = TRUE; + bullets[i].pos.x = spawner.x; + bullets[i].pos.y = spawner.y; + bullets[i].speed = spawner.speed; + bullets[i].angle = spawner.angle; + bullets[i].player = spawner.player; + bullets[i].explosion = FALSE; + bullets[i].clock = 0; + for(u8 j = 0; j < COUNT_INT; j++){ + bullets[i].bools[j] = spawner.bools[j]; + bullets[i].ints[j] = spawner.ints[j]; + bullets[i].fixes[j] = spawner.fixes[j]; + } + if(spawner.vel.x || spawner.vel.y){ + bullets[i].vel.x = spawner.vel.x; + bullets[i].vel.y = spawner.vel.y; + } else { + bullets[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(spawner.angle)), spawner.speed); + bullets[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(spawner.angle)), spawner.speed); + } + bullets[i].updater = updater; + bullets[i].dist = bullets[i].player ? 16 : (spawner.anim == 0 ? 4 : 7); + bullets[i].image = SPR_addSprite(spawner.player ? &pBulletSprite : &bulletsSprite, + getScreenX(bullets[i].pos.x, player.camera) - (spawner.player ? P_BULLET_OFF : BULLET_OFF), + fix32ToInt(bullets[i].pos.y) - (spawner.player ? P_BULLET_OFF : BULLET_OFF), + TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, spawner.player && spawner.angle == 512 ? 1 : 0)); + if(spawner.anim) SPR_setAnim(bullets[i].image, spawner.anim); + bullets[i].anim = spawner.anim; + SPR_setDepth(bullets[i].image, spawner.player ? 7 : (spawner.top ? 3 : 4)); + doBulletRotation(i); + } +} + +s32 bulletDist; +#define BULLET_CHECK FIX32(32) +static void collideWithEnemy(u8 i){ + for(s16 j = 0; j < ENEMY_COUNT; j++) { + if(enemies[j].active){ + // Calculate wrapped distances + fix32 deltaX = getWrappedDelta(bullets[i].pos.x, enemies[j].pos.x); + fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y; + + // Quick bounding box check using wrapped distance + if(deltaY >= -BULLET_CHECK && deltaY <= BULLET_CHECK && + deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){ + // Precise distance check + bulletDist = getApproximatedDistance( + fix32ToInt(deltaX), + fix32ToInt(deltaY)); + if(bulletDist <= bullets[i].dist){ + killBullet(i); + killEnemy(j); + } + } + } + } +} + +static void collideWithPlayer(u8 i){ + if(!bullets[i].player && bullets[i].active){ + fix32 deltaX = getWrappedDelta(bullets[i].pos.x, player.pos.x); + fix32 deltaY = bullets[i].pos.y - player.pos.y; + + s32 dist = getApproximatedDistance( + fix32ToInt(deltaX), + fix32ToInt(deltaY)); + if(dist <= 16){ // Player hit radius + killBullet(i); + player.lives--; + if(player.lives <= 0) gameOver = TRUE; + } + } +} + +static void updateBullet(u8 i){ + bullets[i].pos.x += bullets[i].vel.x; + bullets[i].pos.y += bullets[i].vel.y; + + // Wrap bullet position + if(bullets[i].pos.x >= GAME_WRAP){ + bullets[i].pos.x -= GAME_WRAP; + } + if(bullets[i].pos.x < 0){ + bullets[i].pos.x += GAME_WRAP; + } + + if(bullets[i].clock > 0) bullets[i].updater(i); + if(bullets[i].player) collideWithEnemy(i); + else collideWithPlayer(i); + if(bullets[i].active){ + s16 sx = getScreenX(bullets[i].pos.x, player.camera); + s16 sy = fix32ToInt(bullets[i].pos.y); + u8 off = bullets[i].player ? P_BULLET_OFF : BULLET_OFF; + + // Set visibility to prevent VDP 512px wrap ghosting + // bool onScreen = (sx >= VISIBLE_X_MIN && sx <= VISIBLE_X_MAX && + // sy >= VISIBLE_Y_MIN && sy <= VISIBLE_Y_MAX); + // SPR_setVisibility(bullets[i].image, onScreen ? VISIBLE : HIDDEN); + + SPR_setPosition(bullets[i].image, sx - off, sy - off); + bullets[i].clock++; + } +} + + +void updateBullets(){ + for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) + updateBullet(i); +} \ No newline at end of file diff --git a/src/chrome.h b/src/chrome.h new file mode 100644 index 0000000..ff2216a --- /dev/null +++ b/src/chrome.h @@ -0,0 +1,22 @@ +char scoreStr[SCORE_LENGTH]; +u32 lastScore; + +static void drawScore(){ + uintToStr(score, scoreStr, 1); + VDP_drawText(scoreStr, 1, 1); +} + +void loadChrome(){ + drawScore(); +} + +void updateChrome(){ + score++; + if(score > 99999999) score = 0; + if(lastScore != score){ + lastScore = score; + drawScore(); + } + VDP_clearText(1, 26, 4); + VDP_drawText(debugStr, 1, 26); +} \ No newline at end of file diff --git a/src/enemies.h b/src/enemies.h new file mode 100644 index 0000000..59c0183 --- /dev/null +++ b/src/enemies.h @@ -0,0 +1,70 @@ +void spawnEnemy(u8 type){ + s16 i = -1; + for(s16 j = 0; j < ENEMY_COUNT; j++) if(!enemies[j].active && i == -1) i = j; + if(i > -1){ + enemies[i].active = TRUE; + enemies[i].type = type; + enemies[i].pos.x = FIX32(64); + enemies[i].pos.y = FIX32(64); + enemies[i].off = 16; + enemies[i].image = SPR_addSprite(&butterflySprite, + getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, fix32ToInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, 0)); + enemies[i].angle = 128; + enemies[i].speed = FIX32(0.25); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + } +} + +static void boundsEnemy(u8 i){ + if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off) || enemies[i].pos.y <= FIX32(enemies[i].off)) + enemies[i].vel.y *= -1; + + if(enemies[i].pos.x >= GAME_WRAP){ + enemies[i].pos.x -= GAME_WRAP; + } + if(enemies[i].pos.x < 0){ + enemies[i].pos.x += GAME_WRAP; + } +} + +static void updateEnemy(u8 i){ + boundsEnemy(i); + enemies[i].pos.x += enemies[i].vel.x; + enemies[i].pos.y += enemies[i].vel.y; + + s16 sx = getScreenX(enemies[i].pos.x, player.camera); + s16 sy = fix32ToInt(enemies[i].pos.y); + + fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x); + bool onScreen = (dx >= FIX32(-256) && dx <= FIX32(256)); + SPR_setVisibility(enemies[i].image, onScreen ? VISIBLE : HIDDEN); + + SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off); + enemyCount++; +} + +void updateEnemies(){ + enemyCount = 0; + for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) + updateEnemy(i); + intToStr(enemyCount, debugStr, 1); +} + +void spawnEnemyAt(u8 type, fix32 x, fix32 y){ + s16 i = -1; + for(s16 j = 0; j < ENEMY_COUNT; j++) if(!enemies[j].active && i == -1) i = j; + if(i > -1){ + enemies[i].active = TRUE; + enemies[i].type = type; + enemies[i].pos.x = x; + enemies[i].pos.y = y; + enemies[i].off = 16; + enemies[i].image = SPR_addSprite(&butterflySprite, + getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, fix32ToInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(gameOver ? PAL1 : PAL0, 0, 0, 0)); + enemies[i].angle = 128; + enemies[i].speed = FIX32(2); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed); + } +} \ No newline at end of file diff --git a/src/global.h b/src/global.h new file mode 100644 index 0000000..f56143b --- /dev/null +++ b/src/global.h @@ -0,0 +1,142 @@ +u32 clock; +#define CLOCK_LIMIT 32000 +#define COUNT_INT 4 + +#define GAME_H_F FIX32(224) + +// Section-based world size +#define SECTION_SIZE FIX32(512) // Size of one section (512px) +#define SECTION_COUNT 4 // Number of sections (N = 2, 4, 8, etc.) +#define GAME_WRAP (SECTION_SIZE * SECTION_COUNT) // Total world width + +u32 score, highScore; +#define SCORE_LENGTH 8 + +#define FIRST_ROTATING_BULLET 3 +#define CAMERA_Y_MOD FIX32(112) + +// Screen bounds for visibility (screen is 320x224) +// VDP wraps sprites every 512px, so we must hide sprites outside screen +#define VISIBLE_X_MIN 0 +#define VISIBLE_X_MAX 512 +#define VISIBLE_Y_MIN -32 +#define VISIBLE_Y_MAX 256 + +char debugStr[8]; +s16 emptyI; +void EMPTY(s16 i){emptyI = i;} + +bool gameOver, paused, started; +s16 enemyCount; + +// controls +struct controls { + bool left, right, up, down, a, b, c, start; +}; +struct controls ctrl; +void updateControls(u16 joy, u16 changed, u16 state){ + if(changed){} + if(joy == JOY_1){ + ctrl.left = (state & BUTTON_LEFT); + ctrl.right = (state & BUTTON_RIGHT); + ctrl.up = (state & BUTTON_UP); + ctrl.down = (state & BUTTON_DOWN); + ctrl.a = (state & BUTTON_A); + ctrl.b = (state & BUTTON_B); + ctrl.c = (state & BUTTON_C); + ctrl.start = (state & BUTTON_START); + } +} + + +// player +struct playerStruct { + Vect2D_f32 pos, vel, last; + s16 shotAngle; + u8 lives; + fix32 camera; + Sprite* image; +}; +struct playerStruct player; + + +// bullets +#define BULLET_COUNT 64 + +struct bulletSpawner { + fix32 x, y, speed; + Vect2D_f32 vel; + s16 angle, anim; + bool top, player; + bool bools[COUNT_INT]; + s16 ints[COUNT_INT]; + fix32 fixes[COUNT_INT]; +}; +struct bullet { + bool active, player, explosion, top, vFlip, hFlip; + fix32 speed; + Vect2D_f32 pos, vel; + Sprite* image; + s16 clock, angle, anim, frame; + s16 dist; + void (*updater)(s16); + bool bools[COUNT_INT]; + s16 ints[COUNT_INT]; + fix32 fixes[COUNT_INT]; +}; +struct bullet bullets[BULLET_COUNT]; + + +// enemies +#define ENEMY_COUNT 16 + +struct enemy { + bool active; + s16 clock, angle, anim, frame, off; + fix32 speed; + u8 type; + Vect2D_f32 vel, pos; + s16 dist; + Sprite* image; + void (*updater)(s16); + bool bools[COUNT_INT]; + s16 ints[COUNT_INT]; + fix32 fixes[COUNT_INT]; +}; +struct enemy enemies[ENEMY_COUNT]; + +void killBullet(u8 i){ + bullets[i].active = FALSE; + SPR_releaseSprite(bullets[i].image); +} + +void killEnemy(u8 i){ + enemies[i].active = FALSE; + SPR_releaseSprite(enemies[i].image); +} + +// Calculate shortest X distance accounting for world wrap +// Returns distance in the range [-GAME_WRAP/2, GAME_WRAP/2] +static fix32 getWrappedDelta(fix32 a, fix32 b) { + fix32 delta = a - b; + // If distance is more than half the world, go the other way + if (delta > GAME_WRAP / 2) { + delta -= GAME_WRAP; + } else if (delta < -GAME_WRAP / 2) { + delta += GAME_WRAP; + } + return delta; +} + +// Safe screen X calculation handling wrap edge cases +// Returns screen coordinate for an entity, accounting for entities that just wrapped +static s16 getScreenX(fix32 worldX, fix32 camera) { + fix32 screenX = worldX - camera; + // Handle entity that just wrapped (temporarily far off-screen) + if (screenX < FIX32(-256)) { + screenX += GAME_WRAP; + } else if (screenX > FIX32(256)) { + screenX -= GAME_WRAP; + } + return fix32ToInt(screenX); +} \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..3c2c606 --- /dev/null +++ b/src/main.c @@ -0,0 +1,51 @@ +#include +#include + +#include "global.h" +#include "background.h" +#include "bullets.h" +#include "enemies.h" +#include "player.h" +#include "stage.h" +#include "chrome.h" +#include "start.h" + +static void loadInternals(){ + JOY_init(); + JOY_setEventHandler(&updateControls); + SPR_init(); + VDP_setPlaneSize(128, 32, TRUE); + VDP_loadFont(font.tileset, DMA); + PAL_setPalette(PAL0, font.palette->data, DMA); + PAL_setPalette(PAL1, shadow.palette->data, CPU); + VDP_setTextPriority(1); +} + +void loadGame(){ + loadBackground(); + loadPlayer(); + loadChrome(); + loadStage(); +} + +static void updateGame(){ + updateChrome(); + updateBackground(); + updateEnemies(); + updatePlayer(); + updateBullets(); +} + +int main(bool hardReset){ + loadInternals(); + loadGame(); + // loadStart(); + while(1){ + updateGame(); + clock++; + if(clock >= CLOCK_LIMIT) clock = 600; + SPR_update(); + SYS_doVBlankProcess(); + } + return(0); +} \ No newline at end of file diff --git a/src/player.h b/src/player.h new file mode 100644 index 0000000..a0c40cd --- /dev/null +++ b/src/player.h @@ -0,0 +1,153 @@ +#define PLAYER_SPEED FIX32(5) +#define PLAYER_SPEED_NORM fix32Mul(PLAYER_SPEED, FIX32(0.707)) + +#define PLAYER_SPEED_FOCUS FIX32(3) +#define PLAYER_SPEED_FOCUS_NORM fix32Mul(PLAYER_SPEED_FOCUS, FIX32(0.707)) + +#define PLAYER_ACCEL PLAYER_SPEED >> 3 +#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 3 + +#define PLAYER_OFF 16 +#define PLAYER_BOUND_Y FIX32(PLAYER_OFF) +#define PLAYER_BOUND_H FIX32(224 - PLAYER_OFF) + +#define CAMERA_X FIX32(80) +#define CAMERA_W FIX32(240) + +#define SHOT_INTERVAL 15 + +s16 shotClock; +fix32 screenX; + +fix32 playerSpeed, playerSpeedNorm; +fix32 playerVelX; // Track actual X velocity for momentum + +static void movePlayer(){ + // Y-axis stays instant + player.vel.y = 0; + + // Determine target X speed based on input + fix32 targetVelX = 0; + if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){ + if(ctrl.b){ + playerSpeed = PLAYER_SPEED_FOCUS; + playerSpeedNorm = PLAYER_SPEED_FOCUS_NORM; + } else { + playerSpeed = PLAYER_SPEED; + playerSpeedNorm = PLAYER_SPEED_NORM; + } + player.last.x = player.pos.x; + if(ctrl.left || ctrl.right){ + if(!ctrl.a) player.shotAngle = ctrl.left ? 512 : 0; + targetVelX = ctrl.left ? -playerSpeed : playerSpeed; + } + + // Y velocity (instant) + if(ctrl.up) player.vel.y = -playerSpeed; + else if(ctrl.down) player.vel.y = playerSpeed; + } + + // Apply acceleration toward target X velocity + if(playerVelX < targetVelX){ + playerVelX += ctrl.b ? PLAYER_ACCEL_FOCUS : PLAYER_ACCEL; + if(playerVelX > targetVelX) playerVelX = targetVelX; + } else if(playerVelX > targetVelX){ + playerVelX -= ctrl.b ? PLAYER_ACCEL_FOCUS : PLAYER_ACCEL; + if(playerVelX < targetVelX) playerVelX = targetVelX; + } + + player.vel.x = playerVelX; + + // Normalize if diagonal + if(player.vel.x != 0 && player.vel.y != 0){ + player.vel.x = fix32Mul(player.vel.x, FIX32(0.707)); + player.vel.y = fix32Mul(player.vel.y, FIX32(0.707)); + } + + // Apply movement (always, for momentum to work during deceleration) + player.pos.x += player.vel.x; + player.pos.y += player.vel.y; + + // Update facing direction when moving horizontally + if(ctrl.a){ + SPR_setHFlip(player.image, player.shotAngle != 0); + } else { + if(player.vel.x < 0){ + SPR_setHFlip(player.image, TRUE); + } else if(player.vel.x > 0){ + SPR_setHFlip(player.image, FALSE); + } + } +} + +static void boundsPlayer(){ + if(player.pos.y < PLAYER_BOUND_Y) + player.pos.y = PLAYER_BOUND_Y; + else if(player.pos.y > PLAYER_BOUND_H) + player.pos.y = PLAYER_BOUND_H; + + if(player.pos.x >= GAME_WRAP){ + player.pos.x -= GAME_WRAP; + player.camera -= GAME_WRAP; + } + + if(player.pos.x <= 0){ + player.pos.x += GAME_WRAP; + player.camera += GAME_WRAP; + } + +} + +static void cameraPlayer(){ + screenX = player.pos.x - player.camera; + if(screenX < CAMERA_X && player.vel.x < 0) + player.camera += player.vel.x; + else if(screenX > CAMERA_W && player.vel.x > 0) + player.camera += player.vel.x; +} + +static void shootPlayer(){ + if(ctrl.a && shotClock == 0){ + struct bulletSpawner spawner = { + .x = player.pos.x, + .y = player.pos.y, + .anim = 0, + .speed = FIX32(12), + .angle = player.shotAngle, + .player = TRUE + }; + void updater(u8 i){ + if(bullets[i].clock >= 16) killBullet(i); + } + spawnBullet(spawner, updater); + shotClock = SHOT_INTERVAL; + } else if(shotClock > 0) shotClock--; +} + +void loadPlayer(){ + player.shotAngle = 0; + player.camera = 0; + player.pos.x = FIX32(128); + player.pos.y = FIX32(112); + playerVelX = 0; + player.lives = 3; + player.image = SPR_addSprite(&sakuyaSprite, + fix32ToInt(player.pos.x) - PLAYER_OFF, + fix32ToInt(player.pos.y) - PLAYER_OFF, + TILE_ATTR(PAL0, 0, 0, 0)); +} + +void updatePlayer(){ + movePlayer(); + boundsPlayer(); + cameraPlayer(); + shootPlayer(); + + s16 sx = getScreenX(player.pos.x, player.camera); + s16 sy = fix32ToInt(player.pos.y); + + + SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); + + intToStr(fix32ToInt(player.pos.x), debugStr, 1); +} \ No newline at end of file diff --git a/src/stage.h b/src/stage.h new file mode 100644 index 0000000..4ce724a --- /dev/null +++ b/src/stage.h @@ -0,0 +1,3 @@ +void loadStage(){ + spawnEnemy(0); +} \ No newline at end of file diff --git a/src/start.h b/src/start.h new file mode 100644 index 0000000..1842d2e --- /dev/null +++ b/src/start.h @@ -0,0 +1,7 @@ +#define START_I 8 + +void loadStart(){ + VDP_drawImageEx(BG_A, &logo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I), 6, 10, FALSE, FALSE); + VDP_drawText("press any button", 12, 16); + VDP_drawText(" 2026 T.BODDY ", 12, 18); +} \ No newline at end of file