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

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