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 0000000..b234322 Binary files /dev/null and b/res/bullets.png differ diff --git a/res/butterfly.png b/res/butterfly.png new file mode 100644 index 0000000..511385a Binary files /dev/null and b/res/butterfly.png differ diff --git a/res/fadebottom.png b/res/fadebottom.png new file mode 100644 index 0000000..a2a204f Binary files /dev/null and b/res/fadebottom.png differ diff --git a/res/fadetop.png b/res/fadetop.png new file mode 100644 index 0000000..c697e2a Binary files /dev/null and b/res/fadetop.png differ diff --git a/res/font.png b/res/font.png new file mode 100644 index 0000000..ba87fa7 Binary files /dev/null and b/res/font.png differ diff --git a/res/logo.png b/res/logo.png new file mode 100644 index 0000000..e1d2f5a Binary files /dev/null and b/res/logo.png differ diff --git a/res/pbullet.png b/res/pbullet.png new file mode 100644 index 0000000..5037231 Binary files /dev/null and b/res/pbullet.png differ diff --git a/res/resources.res b/res/resources.res new file mode 100644 index 0000000..fbf8f94 --- /dev/null +++ b/res/resources.res @@ -0,0 +1,15 @@ +IMAGE font "font.png" NONE NONE +IMAGE shadow "shadow.png" NONE NONE + +IMAGE logo "logo.png" NONE NONE + +IMAGE sky "sky.png" NONE NONE +IMAGE fadeTop "fadetop.png" NONE NONE +IMAGE fadeBottom "fadebottom.png" NONE NONE + +SPRITE sakuyaSprite "sakuya.png" 4 4 NONE 0 + +SPRITE bulletsSprite "bullets.png" 2 2 NONE 0 +SPRITE pBulletSprite "pbullet.png" 4 4 NONE 0 + +SPRITE butterflySprite "butterfly.png" 4 4 NONE 8 \ No newline at end of file diff --git a/res/sakuya.png b/res/sakuya.png new file mode 100644 index 0000000..f02273d Binary files /dev/null and b/res/sakuya.png differ diff --git a/res/shadow.png b/res/shadow.png new file mode 100644 index 0000000..f1aa9cb Binary files /dev/null and b/res/shadow.png differ diff --git a/res/sky.png b/res/sky.png new file mode 100644 index 0000000..0e304e7 Binary files /dev/null and b/res/sky.png differ diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..f544178 --- /dev/null +++ b/run.sh @@ -0,0 +1 @@ +/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin \ No newline at end of file diff --git a/src/background.h b/src/background.h new file mode 100644 index 0000000..087411f --- /dev/null +++ b/src/background.h @@ -0,0 +1,27 @@ +#define BG_I 8 +#define FADE_TOP_I BG_I + 64 +#define FADE_BOTTOM_I FADE_TOP_I + 64 + +#define BG_COUNT 27 +#define BG_OFF 112 +s16 bgScrolls[BG_COUNT]; + +void loadBackground(){ + VDP_loadTileSet(sky.tileset, BG_I, DMA); + VDP_loadTileSet(fadeTop.tileset, FADE_TOP_I, DMA); + VDP_loadTileSet(fadeBottom.tileset, FADE_BOTTOM_I, DMA); + for(u8 y = 0; y < 4; y++){ + for(u8 x = 0; x < 16; x++){ + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), 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); + // } +} + +void updateBackground(){ + VDP_setHorizontalScroll(BG_B, fix32ToInt(-player.camera)); + VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 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