This commit is contained in:
t. boddy 2026-02-14 19:31:58 -05:00
parent ab73e04b32
commit 0b905f2690
26 changed files with 1210 additions and 0 deletions

20
.gitignore vendored Normal file
View file

@ -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/

6
build.sh Executable file
View file

@ -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

2
compile.sh Executable file
View file

@ -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

BIN
res/bullets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
res/butterfly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
res/fadebottom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
res/fadetop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
res/font.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
res/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
res/pbullet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

15
res/resources.res Normal file
View file

@ -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

BIN
res/sakuya.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
res/shadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
res/sky.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

1
run.sh Executable file
View file

@ -0,0 +1 @@
/Applications/Genesis\ Plus.app/Contents/MacOS/Genesis\ Plus out/rom.bin

27
src/background.h Normal file
View file

@ -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);
}

33
src/boot/rom_head.c Normal file
View file

@ -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 "
};

487
src/boot/sega.s Normal file
View file

@ -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
* A<B, so ret 0, rem A
move.l %d0,%d1
clr.l %d0
move.l %a2,%d3 /* restore d3 */
rts
* A==B, so ret 1, rem 0
is1:
moveq.l #1,%d0
clr.l %d1
move.l %a2,%d3 /* restore d3 */
rts
* A>B 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

171
src/bullets.h Normal file
View file

@ -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);
}

22
src/chrome.h Normal file
View file

@ -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);
}

70
src/enemies.h Normal file
View file

@ -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);
}
}

142
src/global.h Normal file
View file

@ -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);
}

51
src/main.c Normal file
View file

@ -0,0 +1,51 @@
#include <genesis.h>
#include <resources.h>
#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);
}

153
src/player.h Normal file
View file

@ -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);
}

3
src/stage.h Normal file
View file

@ -0,0 +1,3 @@
void loadStage(){
spawnEnemy(0);
}

7
src/start.h Normal file
View file

@ -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);
}