shit lmao
BIN
res/boss.vgm
Normal file
BIN
res/fairy.png
|
Before Width: | Height: | Size: 5.4 KiB |
BIN
res/font.png
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
res/ground.png
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
res/koakuma.png
|
Before Width: | Height: | Size: 5.5 KiB |
BIN
res/level.vgm
Normal file
BIN
res/logo.png
|
Before Width: | Height: | Size: 2.2 KiB |
BIN
res/mapenemy.png
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4 KiB |
BIN
res/momoyo.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
res/pbullet.png
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
|
@ -1,28 +1,33 @@
|
|||
IMAGE font "font.png" NONE NONE
|
||||
IMAGE shadow "shadow.png" NONE NONE
|
||||
|
||||
IMAGE logo "logo.png" NONE NONE
|
||||
IMAGE startFade1 "start/fade1.png" FAST
|
||||
IMAGE startFade2 "start/fade2.png" FAST
|
||||
IMAGE startFade3 "start/fade3.png" FAST
|
||||
IMAGE startFade4 "start/fade4.png" FAST
|
||||
IMAGE startSplash1 "start/splash1.png" FAST
|
||||
IMAGE startLogo "start/logo.png" FAST
|
||||
IMAGE startBigBg "start/bigbg.png" FAST
|
||||
XGM2 bgmStart "start.vgm"
|
||||
|
||||
IMAGE sky "sky.png" NONE NONE
|
||||
IMAGE skyTop "skytop.png" NONE NONE
|
||||
IMAGE skyRed "skyred.png" NONE NONE
|
||||
IMAGE ground "ground.png" NONE NONE
|
||||
|
||||
// SPRITE sakuyaSprite "sakuya.png" 4 4 NONE 0
|
||||
SPRITE sakuyaSprite "sakuya2.png" 6 6 NONE 0
|
||||
SPRITE momoyoSprite "momoyo.png" 6 6 NONE 0
|
||||
|
||||
SPRITE bulletsSprite "bullets.png" 2 2 NONE 0
|
||||
SPRITE pBulletSprite "pbullet.png" 4 4 NONE 0
|
||||
|
||||
// SPRITE fairySprite "fairy.png" 4 4 NONE 8
|
||||
SPRITE fairySprite "fairy2.png" 4 4 NONE 8
|
||||
SPRITE koaSprite "koakuma.png" 4 4 NONE 0
|
||||
SPRITE treasureSprite "treasure.png" 4 4 NONE 0
|
||||
|
||||
// IMAGE mapPlayer "mapplayer.png" NONE NONE
|
||||
IMAGE mapIndicator "mapindicator.png" NONE NONE
|
||||
// IMAGE mapFrame "mapframe.png" NONE NONE
|
||||
|
||||
IMAGE imageFontBig "fontbig.png" NONE NONE
|
||||
IMAGE imageFontBigShadow "fontbigshadow.png" NONE NONE
|
||||
IMAGE imageChromeLife "life.png" NONE NONE
|
||||
IMAGE imageChromeLife2 "life2.png" NONE NONE
|
||||
|
||||
XGM2 stageMusic "stage.vgm"
|
||||
XGM2 stageMusic "level.vgm"
|
||||
BIN
res/sakuya.png
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
res/sakuya2.png
|
Before Width: | Height: | Size: 2 KiB |
BIN
res/sky.png
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
res/skyred.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
res/start.vgm
Normal file
BIN
res/start/bigbg.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
res/start/fade1.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
res/start/fade2.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
res/start/fade3.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
res/start/fade4.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
res/start/logo.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
res/start/splash1.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
res/treasure.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
res/treasure.vgm
Normal file
|
|
@ -14,27 +14,49 @@ static const fix32 parallaxMul[PARALLAX_COUNT] = {
|
|||
};
|
||||
|
||||
s16 bgScroll[28];
|
||||
u8 bgOff;
|
||||
|
||||
void loadBackground(){
|
||||
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
|
||||
VDP_setVerticalScroll(BG_B, 32);
|
||||
VDP_loadTileSet(sky.tileset, BG_I, DMA);
|
||||
VDP_loadTileSet(ground.tileset, BG_I + 64, DMA);
|
||||
for(u8 y = 0; y < 4; y++){
|
||||
VDP_loadTileSet(skyTop.tileset, BG_I, DMA);
|
||||
VDP_loadTileSet(sky.tileset, BG_I + 64, DMA);
|
||||
VDP_loadTileSet(ground.tileset, BG_I + 128, DMA);
|
||||
VDP_loadTileSet(skyRed.tileset, BG_I + 192, DMA);
|
||||
|
||||
// for(u8 y = 0; y < 14; y++){
|
||||
// for(u8 x = 0; x < 64; x++){
|
||||
// if(y < 11) bgOff = 0;
|
||||
// // else if(y == 13) bgOff = 2;
|
||||
// else bgOff = 1;
|
||||
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + bgOff * 2), x * 2, y * 2 + 0, 2, 1);
|
||||
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 28 + bgOff * 2), x * 2, y * 2 + 1, 2, 1);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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 + 2), x * 2, y * 2, 2, 1);
|
||||
// // VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 2 + 28), x * 2, y * 2 + 1, 2, 1);
|
||||
// }
|
||||
// }
|
||||
|
||||
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 192), 0, 0, 128, 8);
|
||||
|
||||
for(u8 y = 0; y < 3; y++){
|
||||
for(u8 x = 0; x < 16; x++){
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + (y > 2 ? 64 : 0)), x * 8, y * 8, 8, 8);
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 * y), x * 8, y * 8 + 8, 8, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// place 64x64 ground block in sky area (zone 0 only)
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
zoneBlockVisible = TRUE;
|
||||
prevCamera = player.camera;
|
||||
for(u8 i = 0; i < PARALLAX_COUNT; i++)
|
||||
parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]);
|
||||
// write initial scroll values so first frame has correct parallax
|
||||
s16 initScroll = fix32ToInt(-player.camera);
|
||||
for(u8 i = 0; i < 20; i++)
|
||||
bgScroll[i] = initScroll;
|
||||
for(u8 i = 0; i < 8; i++)
|
||||
bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i]));
|
||||
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
|
||||
|
|
@ -68,11 +90,11 @@ void updateBackground(){
|
|||
// show ground block only when zone 0 copy of these columns is on screen
|
||||
fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160));
|
||||
bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212));
|
||||
if(shouldShow && !zoneBlockVisible){
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
zoneBlockVisible = TRUE;
|
||||
} else if(!shouldShow && zoneBlockVisible){
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
zoneBlockVisible = FALSE;
|
||||
}
|
||||
// if(shouldShow && !zoneBlockVisible){
|
||||
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
// zoneBlockVisible = TRUE;
|
||||
// } else if(!shouldShow && zoneBlockVisible){
|
||||
// VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
// zoneBlockVisible = FALSE;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ static void doBulletRotation(u8 i){
|
|||
}
|
||||
|
||||
void spawnBullet(struct bulletSpawner spawner, void(*updater)){
|
||||
if(player.recoveringClock > 0 && !spawner.player) return;
|
||||
if((player.recoveringClock > 0 || player.respawnClock > 0) && !spawner.player) return;
|
||||
// Don't spawn if offscreen
|
||||
fix32 dx = getWrappedDelta(spawner.x, player.pos.x);
|
||||
bool offScreenX = (dx < -CULL_LIMIT || dx > CULL_LIMIT);
|
||||
|
|
@ -114,7 +114,7 @@ void spawnBullet(struct bulletSpawner spawner, void(*updater)){
|
|||
s32 bulletDist;
|
||||
static void collideWithEnemy(u8 i){
|
||||
for(s16 j = 0; j < ENEMY_COUNT; j++) {
|
||||
if(enemies[j].active && enemies[j].onScreen && bullets[i].active){
|
||||
if(enemies[j].active && bullets[i].active){
|
||||
fix32 deltaX = getWrappedDelta(bullets[i].pos.x, enemies[j].pos.x);
|
||||
fix32 deltaY = bullets[i].pos.y - enemies[j].pos.y;
|
||||
if(deltaY >= -BULLET_CHECK && deltaY <= BULLET_CHECK &&
|
||||
|
|
@ -132,7 +132,7 @@ static void collideWithEnemy(u8 i){
|
|||
}
|
||||
|
||||
static void collideWithPlayer(u8 i){
|
||||
if(player.recoveringClock > 0) return;
|
||||
if(player.recoveringClock > 0 || player.respawnClock > 0) return;
|
||||
fix32 deltaX = getWrappedDelta(bullets[i].pos.x, player.pos.x);
|
||||
fix32 deltaY = bullets[i].pos.y - player.pos.y;
|
||||
|
||||
|
|
@ -152,8 +152,11 @@ static void collideWithPlayer(u8 i){
|
|||
gameOver = TRUE;
|
||||
XGM2_stop();
|
||||
} else {
|
||||
player.recoveringClock = 120;
|
||||
player.respawnClock = 120;
|
||||
SPR_setVisibility(player.image, HIDDEN);
|
||||
killBullets = TRUE;
|
||||
hitMessageClock = 120;
|
||||
hitMessageBullet = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
154
src/chrome.h
|
|
@ -1,8 +1,8 @@
|
|||
#define MAP_I 512
|
||||
#define MAP_TILE TILE_ATTR_FULL(PAL1, 1, 0, 0, MAP_I)
|
||||
#define MAP_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I)
|
||||
#define MAP_PLAYER_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 1)
|
||||
#define MAP_ENEMY_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 2)
|
||||
#define MAP_HUMAN_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3)
|
||||
#define MAP_TREASURE_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3)
|
||||
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4)
|
||||
|
||||
#define FONT_BIG_I 256
|
||||
|
|
@ -37,7 +37,7 @@ static void drawLives(){
|
|||
|
||||
// previous map positions: -1 means not drawn
|
||||
s16 mapEnemyCol[ENEMY_COUNT], mapEnemyRow[ENEMY_COUNT];
|
||||
s16 mapHumanCol[HUMAN_COUNT], mapHumanRow[HUMAN_COUNT];
|
||||
s16 mapTreasureCol[TREASURE_COUNT], mapTreasureRow[TREASURE_COUNT];
|
||||
s16 mapPlayerRow;
|
||||
|
||||
static void drawScore(){
|
||||
|
|
@ -57,33 +57,40 @@ static void drawScore(){
|
|||
void loadMap(){
|
||||
VDP_fillTileMapRect(BG_A, MAP_TILE, MAP_X, MAP_Y, MAP_W, MAP_H);
|
||||
|
||||
// VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3), MAP_X, MAP_Y - 1, MAP_W, 1);
|
||||
// VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 1, 0, MAP_I + 3), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4), MAP_X, MAP_Y - 1, MAP_W, 1);
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 5), MAP_X, MAP_Y + MAP_H, MAP_W, 1);
|
||||
|
||||
// VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4), MAP_X - 1, MAP_Y, 1, MAP_H);
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 6), MAP_X - 1, MAP_Y, 1, MAP_H);
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 6), MAP_X + MAP_W, MAP_Y, 1, MAP_H);
|
||||
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 7), MAP_X - 1, MAP_Y + MAP_H, 1, 1);
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 7), MAP_X + MAP_W, MAP_Y + MAP_H, 1, 1);
|
||||
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 8), MAP_X - 1, MAP_Y - 1, 1, 1);
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 1, MAP_I + 8), MAP_X + MAP_W, MAP_Y - 1, 1, 1);
|
||||
|
||||
for(s16 i = 0; i < ENEMY_COUNT; i++){
|
||||
mapEnemyCol[i] = -1;
|
||||
mapEnemyRow[i] = -1;
|
||||
}
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++){
|
||||
mapHumanCol[i] = -1;
|
||||
mapHumanRow[i] = -1;
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++){
|
||||
mapTreasureCol[i] = -1;
|
||||
mapTreasureRow[i] = -1;
|
||||
}
|
||||
mapPlayerRow = -1;
|
||||
}
|
||||
|
||||
// temp arrays for new positions
|
||||
s16 mapNewCol[ENEMY_COUNT], mapNewRow[ENEMY_COUNT];
|
||||
s16 mapNewHumanCol[HUMAN_COUNT], mapNewHumanRow[HUMAN_COUNT];
|
||||
s16 mapNewTreasureCol[TREASURE_COUNT], mapNewTreasureRow[TREASURE_COUNT];
|
||||
|
||||
static bool mapTileOccupied(s16 col, s16 row, s16 pRow){
|
||||
// player always at center column
|
||||
if(col == MAP_W / 2 && row == pRow) return TRUE;
|
||||
for(s16 i = 0; i < ENEMY_COUNT; i++)
|
||||
if(mapNewCol[i] == col && mapNewRow[i] == row) return TRUE;
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++)
|
||||
if(mapNewHumanCol[i] == col && mapNewHumanRow[i] == row) return TRUE;
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++)
|
||||
if(mapNewTreasureCol[i] == col && mapNewTreasureRow[i] == row) return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
|
@ -112,22 +119,22 @@ static void updateMap(){
|
|||
mapNewRow[i] = row;
|
||||
}
|
||||
|
||||
// compute new human positions
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++){
|
||||
if(!humans[i].active || humans[i].image == NULL || humans[i].state == HUMAN_COLLECTED){
|
||||
mapNewHumanCol[i] = -1;
|
||||
mapNewHumanRow[i] = -1;
|
||||
// compute new treasure positions
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++){
|
||||
if(!treasures[i].active || treasures[i].image == NULL || treasures[i].state == TREASURE_COLLECTED){
|
||||
mapNewTreasureCol[i] = -1;
|
||||
mapNewTreasureRow[i] = -1;
|
||||
continue;
|
||||
}
|
||||
fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x);
|
||||
fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x);
|
||||
s16 col = fix32ToInt(dx) / 54 + MAP_W / 2;
|
||||
if(col < 0) col = 0;
|
||||
if(col >= MAP_W) col = MAP_W - 1;
|
||||
s16 row = fix32ToInt(humans[i].pos.y) / 75;
|
||||
s16 row = fix32ToInt(treasures[i].pos.y) / 75;
|
||||
if(row < 0) row = 0;
|
||||
if(row >= MAP_H) row = MAP_H - 1;
|
||||
mapNewHumanCol[i] = col;
|
||||
mapNewHumanRow[i] = row;
|
||||
mapNewTreasureCol[i] = col;
|
||||
mapNewTreasureRow[i] = row;
|
||||
}
|
||||
|
||||
// clear old player tile if it moved and nothing new occupies it
|
||||
|
|
@ -143,21 +150,21 @@ static void updateMap(){
|
|||
VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapEnemyCol[i], MAP_Y + mapEnemyRow[i]);
|
||||
}
|
||||
|
||||
// clear old human tiles that moved or disappeared
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++){
|
||||
if(mapHumanCol[i] < 0) continue;
|
||||
if(mapHumanCol[i] == mapNewHumanCol[i] && mapHumanRow[i] == mapNewHumanRow[i]) continue;
|
||||
if(!mapTileOccupied(mapHumanCol[i], mapHumanRow[i], pRow))
|
||||
VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapHumanCol[i], MAP_Y + mapHumanRow[i]);
|
||||
// clear old treasure tiles that moved or disappeared
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++){
|
||||
if(mapTreasureCol[i] < 0) continue;
|
||||
if(mapTreasureCol[i] == mapNewTreasureCol[i] && mapTreasureRow[i] == mapNewTreasureRow[i]) continue;
|
||||
if(!mapTileOccupied(mapTreasureCol[i], mapTreasureRow[i], pRow))
|
||||
VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapTreasureCol[i], MAP_Y + mapTreasureRow[i]);
|
||||
}
|
||||
|
||||
// draw human dots (skip if player occupies same tile)
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++){
|
||||
mapHumanCol[i] = mapNewHumanCol[i];
|
||||
mapHumanRow[i] = mapNewHumanRow[i];
|
||||
if(mapNewHumanCol[i] < 0) continue;
|
||||
if(mapNewHumanCol[i] == MAP_W / 2 && mapNewHumanRow[i] == pRow) continue;
|
||||
VDP_setTileMapXY(BG_A, MAP_HUMAN_TILE, MAP_X + mapNewHumanCol[i], MAP_Y + mapNewHumanRow[i]);
|
||||
// draw treasure dots (skip if player occupies same tile)
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++){
|
||||
mapTreasureCol[i] = mapNewTreasureCol[i];
|
||||
mapTreasureRow[i] = mapNewTreasureRow[i];
|
||||
if(mapNewTreasureCol[i] < 0) continue;
|
||||
if(mapNewTreasureCol[i] == MAP_W / 2 && mapNewTreasureRow[i] == pRow) continue;
|
||||
VDP_setTileMapXY(BG_A, MAP_TREASURE_TILE, MAP_X + mapNewTreasureCol[i], MAP_Y + mapNewTreasureRow[i]);
|
||||
}
|
||||
|
||||
// draw enemy dots (skip if player occupies same tile)
|
||||
|
|
@ -174,12 +181,14 @@ static void updateMap(){
|
|||
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow);
|
||||
}
|
||||
|
||||
u8 phraseIndex[4];
|
||||
|
||||
s16 lastLevel;
|
||||
static void drawLevel(){
|
||||
char lvlStr[4];
|
||||
uintToStr(level + 1, lvlStr, 1);
|
||||
VDP_drawText("L", 1, 26);
|
||||
VDP_drawText(lvlStr, 2, 26);
|
||||
VDP_drawText("LVL", 1, 8);
|
||||
VDP_drawText(lvlStr, 4, 8);
|
||||
lastLevel = level;
|
||||
}
|
||||
|
||||
|
|
@ -201,34 +210,36 @@ static void doGameOver(){
|
|||
didGameOver = TRUE;
|
||||
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
|
||||
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active){
|
||||
if(humans[i].state == HUMAN_COLLECTED){
|
||||
// spawn player bullet explosion at carried human position
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active){
|
||||
if(treasures[i].state == TREASURE_COLLECTED){
|
||||
// spawn player bullet explosion at carried treasure position
|
||||
struct bulletSpawner spawner = {
|
||||
.x = humans[i].pos.x, .y = humans[i].pos.y,
|
||||
.x = treasures[i].pos.x, .y = treasures[i].pos.y,
|
||||
.anim = 0, .speed = 0, .angle = 0, .player = TRUE
|
||||
};
|
||||
void noop(s16 j){ (void)j; }
|
||||
spawnBullet(spawner, noop);
|
||||
for(s16 j = BULLET_COUNT - 1; j >= 0; j--){
|
||||
if(bullets[j].active && !bullets[j].explosion
|
||||
&& bullets[j].pos.x == humans[i].pos.x && bullets[j].pos.y == humans[i].pos.y){
|
||||
&& bullets[j].pos.x == treasures[i].pos.x && bullets[j].pos.y == treasures[i].pos.y){
|
||||
killBullet(j, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
killHuman(i);
|
||||
killTreasure(i);
|
||||
}
|
||||
SPR_releaseSprite(player.image);
|
||||
// clear minimap
|
||||
VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H);
|
||||
// clear score
|
||||
VDP_clearTileMapRect(BG_A, SCORE_X, SCORE_Y, SCORE_LENGTH, 2);
|
||||
|
||||
// clear lives
|
||||
VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16);
|
||||
|
||||
// clear messages
|
||||
treasureCollectedClock = 0;
|
||||
allTreasureCollected = FALSE;
|
||||
hitMessageClock = 0;
|
||||
VDP_clearText(9, 5, 22);
|
||||
|
||||
VDP_drawText("GAME OVER", 15, 13);
|
||||
VDP_drawText("PRESS ANY BUTTON", 12, 14);
|
||||
}
|
||||
|
|
@ -236,7 +247,7 @@ static void doGameOver(){
|
|||
static void showPause(){
|
||||
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL1);
|
||||
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL1);
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL1);
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL1);
|
||||
SPR_setPalette(player.image, PAL1);
|
||||
XGM2_pause();
|
||||
VDP_drawText("PAUSE", 17, 13);
|
||||
|
|
@ -245,7 +256,7 @@ static void showPause(){
|
|||
static void clearPause(){
|
||||
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) SPR_setPalette(bullets[i].image, PAL0);
|
||||
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0);
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL0);
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++) if(treasures[i].active) SPR_setPalette(treasures[i].image, PAL0);
|
||||
SPR_setPalette(player.image, PAL0);
|
||||
XGM2_resume();
|
||||
VDP_clearText(17, 13, 5);
|
||||
|
|
@ -307,5 +318,52 @@ void updateChrome(){
|
|||
}
|
||||
if(lastLives != player.lives) drawLives();
|
||||
if(lastLevel != level) drawLevel();
|
||||
if(treasureCollectedClock > 0 && levelWaitClock == 0){
|
||||
if(treasureCollectedClock == 120){
|
||||
VDP_clearText(10, 5, 22);
|
||||
const char* mirrorPhrases[] = {"REFLECT THE DEPTHS", "DIG DEEPER WITHIN", "SEE WHAT SHINES BELOW", "MIRROR OF THE MINE", "LOOK BACK STRIKE BACK"};
|
||||
const char* lampPhrases[] = {"STRIKE LIGHT", "LET THERE BE LODE", "BRIGHT IDEA DEEP DOWN", "ILLUMINATE THE VEIN", "GLOW FROM BELOW"};
|
||||
const char* scarfPhrases[] = {"COZY IN THE CAVES", "WRAP THE UNDERWORLD", "SNUG AS BEDROCK", "STYLE FROM THE STRATA", "WARM THE DEPTHS"};
|
||||
const char* swordPhrases[] = {"ORE YOU READY", "MINED YOUR STEP", "CUTTING EDGE GEOLOGY", "STRIKE THE VEIN", "SPIRIT STEEL"};
|
||||
const char** sets[] = {mirrorPhrases, lampPhrases, scarfPhrases, swordPhrases};
|
||||
const char* phrase = sets[treasureCollectedType][phraseIndex[treasureCollectedType]];
|
||||
phraseIndex[treasureCollectedType] = (phraseIndex[treasureCollectedType] + 1) % 5;
|
||||
u8 len = strlen(phrase);
|
||||
VDP_drawText(phrase, 20 - len / 2, 5);
|
||||
}
|
||||
treasureCollectedClock--;
|
||||
if(treasureCollectedClock == 0){
|
||||
VDP_clearText(10, 5, 22);
|
||||
// check if all treasures are collected or gone
|
||||
bool allDone = TRUE;
|
||||
for(s16 j = 0; j < TREASURE_COUNT; j++){
|
||||
if(treasures[j].active && treasures[j].state != TREASURE_COLLECTED){
|
||||
allDone = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(allDone && collectedCount > 0){
|
||||
allTreasureCollected = TRUE;
|
||||
VDP_drawText("ALL TREASURE COLLECTED", 9, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(hitMessageClock > 0){
|
||||
if(hitMessageClock == 120){
|
||||
VDP_clearText(9, 5, 22);
|
||||
treasureCollectedClock = 0;
|
||||
allTreasureCollected = FALSE;
|
||||
VDP_drawText(hitMessageBullet ? "BLASTED" : "SMASHED", hitMessageBullet ? 16 : 16, 5);
|
||||
}
|
||||
hitMessageClock--;
|
||||
if(hitMessageClock == 0)
|
||||
VDP_clearText(9, 5, 22);
|
||||
}
|
||||
if(levelWaitClock == 240){
|
||||
VDP_clearText(9, 5, 22);
|
||||
treasureCollectedClock = 0;
|
||||
allTreasureCollected = FALSE;
|
||||
VDP_drawText("ALL ENEMIES DESTROYED", 9, 5);
|
||||
}
|
||||
if(clock % 4 == 0) updateMap();
|
||||
}
|
||||
|
|
@ -83,18 +83,18 @@ void spawnEnemy(u8 type, u8 zone){
|
|||
static void boundsEnemy(u8 i){
|
||||
if((enemies[i].type == ENEMY_TYPE_TEST || enemies[i].type == ENEMY_TYPE_BUILDER) && enemies[i].ints[3] >= 0){
|
||||
s16 h = enemies[i].ints[3];
|
||||
// if the human was collected by player or gone, kill this enemy
|
||||
if(!humans[h].active || humans[h].state == HUMAN_COLLECTED){
|
||||
// if the treasure was collected by player or gone, kill this enemy
|
||||
if(!treasures[h].active || treasures[h].state == TREASURE_COLLECTED){
|
||||
enemies[i].ints[3] = -1;
|
||||
humanBeingCarried = FALSE;
|
||||
treasureBeingCarried = FALSE;
|
||||
killEnemy(i);
|
||||
return;
|
||||
}
|
||||
// carrying: only check for reaching the top
|
||||
else if(enemies[i].pos.y <= FIX32(0)){
|
||||
if(humans[h].active) killHuman(h);
|
||||
if(treasures[h].active) killTreasure(h);
|
||||
enemies[i].ints[3] = -1;
|
||||
humanBeingCarried = FALSE;
|
||||
treasureBeingCarried = FALSE;
|
||||
if(enemies[i].type == ENEMY_TYPE_BUILDER){
|
||||
u8 zone = fix32ToInt(enemies[i].pos.x) / 512;
|
||||
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
|
||||
|
|
@ -149,11 +149,33 @@ static void updateEnemy(u8 i){
|
|||
}
|
||||
|
||||
// enemy->player collision
|
||||
if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0){
|
||||
if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0 && player.respawnClock == 0){
|
||||
fix32 edx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
|
||||
fix32 edy = enemies[i].pos.y - player.pos.y;
|
||||
if(edx >= FIX32(-16) && edx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){
|
||||
sfxExplosion();
|
||||
// spawn explosion at player position
|
||||
s16 expSlot = -1;
|
||||
for(s16 j = 0; j < BULLET_COUNT; j++) if(!bullets[j].active){ expSlot = j; break; }
|
||||
if(expSlot >= 0){
|
||||
bullets[expSlot].active = TRUE;
|
||||
bullets[expSlot].player = TRUE;
|
||||
bullets[expSlot].explosion = TRUE;
|
||||
bullets[expSlot].pos.x = player.pos.x;
|
||||
bullets[expSlot].pos.y = player.pos.y;
|
||||
bullets[expSlot].vel.x = 0;
|
||||
bullets[expSlot].vel.y = 0;
|
||||
bullets[expSlot].clock = 0;
|
||||
bullets[expSlot].frame = 0;
|
||||
bullets[expSlot].image = SPR_addSprite(&pBulletSprite, -32, -32, TILE_ATTR(PAL0, 0, 0, 0));
|
||||
if(bullets[expSlot].image){
|
||||
SPR_setAnim(bullets[expSlot].image, 1);
|
||||
SPR_setFrame(bullets[expSlot].image, 0);
|
||||
SPR_setHFlip(bullets[expSlot].image, random() & 1);
|
||||
} else {
|
||||
bullets[expSlot].active = FALSE;
|
||||
}
|
||||
}
|
||||
if(enemies[i].type != ENEMY_TYPE_BOSS){
|
||||
enemies[i].hp = 0;
|
||||
killEnemy(i);
|
||||
|
|
@ -163,8 +185,11 @@ static void updateEnemy(u8 i){
|
|||
gameOver = TRUE;
|
||||
XGM2_stop();
|
||||
} else {
|
||||
player.recoveringClock = 120;
|
||||
player.respawnClock = 120;
|
||||
SPR_setVisibility(player.image, HIDDEN);
|
||||
killBullets = TRUE;
|
||||
hitMessageClock = 120;
|
||||
hitMessageBullet = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
134
src/enemytypes.h
|
|
@ -2,29 +2,29 @@
|
|||
// --- Type 0: Test / Fairy (EnemyOne) ---
|
||||
// =============================================================================
|
||||
// The original enemy type. A fairy that shoots 8-bullet circular bursts and
|
||||
// also seeks/abducts humans (same carry behavior as Builder).
|
||||
// also seeks/abducts treasures (same carry behavior as Builder).
|
||||
//
|
||||
// Behavior:
|
||||
// - When NOT carrying: drifts at speed 2, shoots 8 bullets in a circle
|
||||
// every 20 frames (only when on screen). Also scans for nearby walking
|
||||
// humans every 30 frames and steers toward the closest one within 256px.
|
||||
// Grabs the human when within 16px.
|
||||
// treasures every 30 frames and steers toward the closest one within 256px.
|
||||
// Grabs the treasure when within 16px.
|
||||
// - When carrying: flies upward (angle 704-832, roughly up-left to up-right)
|
||||
// at speed 2. Skips all shooting. boundsEnemy() handles reaching the top
|
||||
// (kills human, self-destructs -- does NOT spawn a Gunner unlike Builder).
|
||||
// (kills treasure, self-destructs -- does NOT spawn a Gunner unlike Builder).
|
||||
//
|
||||
// ints[0] = random shot timer offset (0-59), desynchronizes shooting from
|
||||
// other enemies so they don't all fire on the same frame
|
||||
// ints[2] = target human index (-1 = no target)
|
||||
// ints[3] = carried human index (-1 = not carrying)
|
||||
// ints[2] = target treasure index (-1 = no target)
|
||||
// ints[3] = carried treasure index (-1 = not carrying)
|
||||
//
|
||||
// Speed: 2 HP: 1 Shoots: yes (8-bullet radial, every 20 frames)
|
||||
// Abducts: yes (only 1 human globally at a time via humanBeingCarried flag)
|
||||
// Abducts: yes (only 1 treasure globally at a time via treasureBeingCarried flag)
|
||||
// =============================================================================
|
||||
void loadEnemyOne(u8 i){
|
||||
enemies[i].ints[0] = random() % 60;
|
||||
enemies[i].ints[2] = -1; // target human index
|
||||
enemies[i].ints[3] = -1; // carried human index
|
||||
enemies[i].ints[2] = -1; // target treasure index
|
||||
enemies[i].ints[3] = -1; // carried treasure index
|
||||
enemies[i].angle = ((random() % 4) * 256) + 128;
|
||||
enemies[i].speed = FIX32(2);
|
||||
}
|
||||
|
|
@ -39,41 +39,41 @@ void updateEnemyOne(u8 i){
|
|||
return;
|
||||
}
|
||||
|
||||
// cancel any target if a human is already being carried
|
||||
if(humanBeingCarried && enemies[i].ints[2] >= 0){
|
||||
// cancel any target if a treasure is already being carried
|
||||
if(treasureBeingCarried && enemies[i].ints[2] >= 0){
|
||||
enemies[i].ints[2] = -1;
|
||||
}
|
||||
|
||||
// seeking behavior: periodically look for a human to grab
|
||||
if(!humanBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){
|
||||
s16 bestHuman = -1;
|
||||
// seeking behavior: periodically look for a treasure to grab
|
||||
if(!treasureBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){
|
||||
s16 bestTreasure = -1;
|
||||
fix32 bestDist = FIX32(9999);
|
||||
for(s16 j = 0; j < HUMAN_COUNT; j++){
|
||||
if(!humans[j].active || humans[j].state != HUMAN_WALKING) continue;
|
||||
fix32 dx = getWrappedDelta(enemies[i].pos.x, humans[j].pos.x);
|
||||
fix32 dy = enemies[i].pos.y - humans[j].pos.y;
|
||||
for(s16 j = 0; j < TREASURE_COUNT; j++){
|
||||
if(!treasures[j].active || treasures[j].state != TREASURE_WALKING) continue;
|
||||
fix32 dx = getWrappedDelta(enemies[i].pos.x, treasures[j].pos.x);
|
||||
fix32 dy = enemies[i].pos.y - treasures[j].pos.y;
|
||||
fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
|
||||
if(dist < bestDist && dist < FIX32(256)){
|
||||
bestDist = dist;
|
||||
bestHuman = j;
|
||||
bestTreasure = j;
|
||||
}
|
||||
}
|
||||
enemies[i].ints[2] = bestHuman;
|
||||
enemies[i].ints[2] = bestTreasure;
|
||||
}
|
||||
|
||||
// steer toward target human
|
||||
// steer toward target treasure
|
||||
if(enemies[i].ints[2] >= 0){
|
||||
s16 t = enemies[i].ints[2];
|
||||
if(!humans[t].active || humans[t].state != HUMAN_WALKING){
|
||||
if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){
|
||||
enemies[i].ints[2] = -1;
|
||||
} else {
|
||||
fix32 dx = getWrappedDelta(humans[t].pos.x, enemies[i].pos.x);
|
||||
fix32 dy = humans[t].pos.y - enemies[i].pos.y;
|
||||
fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x);
|
||||
fix32 dy = treasures[t].pos.y - enemies[i].pos.y;
|
||||
|
||||
// hone toward human's current position at base speed
|
||||
// hone toward treasure's current position at base speed
|
||||
s16 angle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(humans[t].pos.x),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(humans[t].pos.y));
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(treasures[t].pos.x),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(treasures[t].pos.y));
|
||||
enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed);
|
||||
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed);
|
||||
|
||||
|
|
@ -83,9 +83,9 @@ void updateEnemyOne(u8 i){
|
|||
if(adx < FIX32(16) && ady < FIX32(16)){
|
||||
enemies[i].ints[3] = t;
|
||||
enemies[i].ints[2] = -1;
|
||||
humanBeingCarried = TRUE;
|
||||
humans[t].state = HUMAN_CARRIED;
|
||||
humans[t].carriedBy = i;
|
||||
treasureBeingCarried = TRUE;
|
||||
treasures[t].state = TREASURE_CARRIED;
|
||||
treasures[t].carriedBy = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -150,6 +150,7 @@ void updateDrone(u8 i){
|
|||
enemies[i].angle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
|
||||
if(player.respawnClock > 0) enemies[i].angle = (enemies[i].angle + 512) % 1024;
|
||||
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);
|
||||
}
|
||||
|
|
@ -162,6 +163,7 @@ void updateDrone(u8 i){
|
|||
s16 aimAngle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
|
||||
if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x,
|
||||
.y = enemies[i].pos.y,
|
||||
|
|
@ -236,6 +238,7 @@ void updateGunner(u8 i){
|
|||
s16 aimAngle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
|
||||
if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x,
|
||||
.y = enemies[i].pos.y,
|
||||
|
|
@ -256,12 +259,12 @@ void updateGunner(u8 i){
|
|||
// =============================================================================
|
||||
// Fast, relentless chaser. Homes toward the player EVERY frame with speed 5
|
||||
// (matching player's normal speed). Never shoots -- pure body-collision threat.
|
||||
// Forces the player to keep moving and use focus mode / diagonal movement to
|
||||
// Forces the player to keep moving and use diagonal movement to
|
||||
// outrun. Very dangerous in groups.
|
||||
//
|
||||
// Behavior:
|
||||
// - Recalculates angle toward player every frame (no delay like Drone).
|
||||
// - Moves at speed 5 (player normal speed = 6, focus = 3.5).
|
||||
// - Moves at speed 5 (player normal speed = 6).
|
||||
// - Bounces off top/bottom screen edges.
|
||||
// - No shooting, no abduction. Just chases.
|
||||
//
|
||||
|
|
@ -285,6 +288,7 @@ void updateHunter(u8 i){
|
|||
enemies[i].angle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
|
||||
if(player.respawnClock > 0) enemies[i].angle = (enemies[i].angle + 512) % 1024;
|
||||
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);
|
||||
}
|
||||
|
|
@ -292,30 +296,30 @@ void updateHunter(u8 i){
|
|||
// =============================================================================
|
||||
// --- Type 4: Builder (Abductor) ---
|
||||
// =============================================================================
|
||||
// Human abductor. Drifts slowly, scans for walking humans, grabs one, and
|
||||
// flies upward. If it reaches the top of the screen with a human, the human
|
||||
// Treasure abductor. Drifts slowly, scans for walking treasures, grabs one, and
|
||||
// flies upward. If it reaches the top of the screen with a treasure, the treasure
|
||||
// is killed and a Gunner spawns at that position -- punishing the player for
|
||||
// not intercepting. Only 1 human can be globally carried at a time
|
||||
// (humanBeingCarried flag).
|
||||
// not intercepting. Only 1 treasure can be globally carried at a time
|
||||
// (treasureBeingCarried flag).
|
||||
//
|
||||
// Behavior:
|
||||
// - When NOT carrying: drifts at speed 0.7. Scans for nearest walking human
|
||||
// - When NOT carrying: drifts at speed 0.7. Scans for nearest walking treasure
|
||||
// within 256px every 30 frames. If a target is found, switches to seeking
|
||||
// speed (1.4) and homes toward it. Grabs when within 16px.
|
||||
// - When carrying: flies upward (angle 704-832) at speed 1.4.
|
||||
// boundsEnemy() handles reaching the top: kills the human, spawns a Gunner
|
||||
// boundsEnemy() handles reaching the top: kills the treasure, spawns a Gunner
|
||||
// at the builder's position, then self-destructs.
|
||||
// - If the carried human gets collected by the player while being carried,
|
||||
// boundsEnemy() detects this and kills the builder (enemy dies, human safe).
|
||||
// - Cancels its target if another enemy is already carrying a human.
|
||||
// - If the carried treasure gets collected by the player while being carried,
|
||||
// boundsEnemy() detects this and kills the builder (enemy dies, treasure safe).
|
||||
// - Cancels its target if another enemy is already carrying a treasure.
|
||||
// - No shooting at all.
|
||||
//
|
||||
// ints[0] = random scan timer offset (0-59)
|
||||
// ints[2] = target human index (-1 = no target)
|
||||
// ints[3] = carried human index (-1 = not carrying)
|
||||
// ints[2] = target treasure index (-1 = no target)
|
||||
// ints[3] = carried treasure index (-1 = not carrying)
|
||||
//
|
||||
// Speed: 0.7 (drift), 1.4 (seeking/carrying) HP: 1 Shoots: no
|
||||
// Abducts: yes (kills human + spawns Gunner if it reaches the top)
|
||||
// Abducts: yes (kills treasure + spawns Gunner if it reaches the top)
|
||||
//
|
||||
// Design notes:
|
||||
// - 1 builder adds light urgency. 2 builders is stressful.
|
||||
|
|
@ -339,40 +343,40 @@ void updateBuilder(u8 i){
|
|||
return;
|
||||
}
|
||||
|
||||
// cancel target if a human is already being carried
|
||||
if(humanBeingCarried && enemies[i].ints[2] >= 0){
|
||||
// cancel target if a treasure is already being carried
|
||||
if(treasureBeingCarried && enemies[i].ints[2] >= 0){
|
||||
enemies[i].ints[2] = -1;
|
||||
}
|
||||
|
||||
// scan for nearest walking human every 30 frames
|
||||
if(!humanBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){
|
||||
s16 bestHuman = -1;
|
||||
// scan for nearest walking treasure every 30 frames
|
||||
if(!treasureBeingCarried && enemies[i].clock % 30 == (u32)(enemies[i].ints[0]) % 30){
|
||||
s16 bestTreasure = -1;
|
||||
fix32 bestDist = FIX32(9999);
|
||||
for(s16 j = 0; j < HUMAN_COUNT; j++){
|
||||
if(!humans[j].active || humans[j].state != HUMAN_WALKING) continue;
|
||||
fix32 dx = getWrappedDelta(enemies[i].pos.x, humans[j].pos.x);
|
||||
fix32 dy = enemies[i].pos.y - humans[j].pos.y;
|
||||
for(s16 j = 0; j < TREASURE_COUNT; j++){
|
||||
if(!treasures[j].active || treasures[j].state != TREASURE_WALKING) continue;
|
||||
fix32 dx = getWrappedDelta(enemies[i].pos.x, treasures[j].pos.x);
|
||||
fix32 dy = enemies[i].pos.y - treasures[j].pos.y;
|
||||
fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
|
||||
if(dist < bestDist && dist < FIX32(256)){
|
||||
bestDist = dist;
|
||||
bestHuman = j;
|
||||
bestTreasure = j;
|
||||
}
|
||||
}
|
||||
enemies[i].ints[2] = bestHuman;
|
||||
enemies[i].ints[2] = bestTreasure;
|
||||
}
|
||||
|
||||
// steer toward target human
|
||||
// steer toward target treasure
|
||||
if(enemies[i].ints[2] >= 0){
|
||||
s16 t = enemies[i].ints[2];
|
||||
if(!humans[t].active || humans[t].state != HUMAN_WALKING){
|
||||
if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){
|
||||
enemies[i].ints[2] = -1;
|
||||
} else {
|
||||
fix32 dx = getWrappedDelta(humans[t].pos.x, enemies[i].pos.x);
|
||||
fix32 dy = humans[t].pos.y - enemies[i].pos.y;
|
||||
fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x);
|
||||
fix32 dy = treasures[t].pos.y - enemies[i].pos.y;
|
||||
enemies[i].speed = FIX32(1.4);
|
||||
s16 angle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(humans[t].pos.x),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(humans[t].pos.y));
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(treasures[t].pos.x),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(treasures[t].pos.y));
|
||||
enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed);
|
||||
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed);
|
||||
|
||||
|
|
@ -382,9 +386,9 @@ void updateBuilder(u8 i){
|
|||
if(adx < FIX32(16) && ady < FIX32(16)){
|
||||
enemies[i].ints[3] = t;
|
||||
enemies[i].ints[2] = -1;
|
||||
humanBeingCarried = TRUE;
|
||||
humans[t].state = HUMAN_CARRIED;
|
||||
humans[t].carriedBy = i;
|
||||
treasureBeingCarried = TRUE;
|
||||
treasures[t].state = TREASURE_CARRIED;
|
||||
treasures[t].carriedBy = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -449,6 +453,7 @@ static void bossPatternAimedFan(u8 i, u8 count, s16 spread, fix32 speed){
|
|||
s16 aimAngle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
|
||||
if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024;
|
||||
s16 step = (count > 1) ? (spread * 2) / (count - 1) : 0;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
|
|
@ -502,6 +507,7 @@ static void bossPatternWideSpray(u8 i, u8 count, fix32 speed){
|
|||
s16 aimAngle = honeAngle(
|
||||
fix32ToFix16(enemies[i].pos.x), fix32ToFix16(enemies[i].pos.x) + fix32ToFix16(dx),
|
||||
fix32ToFix16(enemies[i].pos.y), fix32ToFix16(enemies[i].pos.y) + fix32ToFix16(dy));
|
||||
if(player.respawnClock > 0) aimAngle = (aimAngle + 512) % 1024;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
.anim = 3 + (random() % 9), .speed = speed, .angle = aimAngle,
|
||||
|
|
|
|||
57
src/global.h
|
|
@ -8,6 +8,8 @@ void sfxPickup();
|
|||
void loadMap();
|
||||
void loadGame();
|
||||
|
||||
#define SKIP_START 1
|
||||
|
||||
u32 clock;
|
||||
#define CLOCK_LIMIT 32000
|
||||
#define PROP_COUNT 8
|
||||
|
|
@ -20,8 +22,8 @@ u32 clock;
|
|||
|
||||
#define CULL_LIMIT FIX32(240)
|
||||
|
||||
// #define MUSIC_VOLUME 50
|
||||
#define MUSIC_VOLUME 0
|
||||
#define MUSIC_VOLUME 50
|
||||
// #define MUSIC_VOLUME 0
|
||||
|
||||
u32 score;
|
||||
#define SCORE_LENGTH 8
|
||||
|
|
@ -43,8 +45,14 @@ u8 level;
|
|||
s16 pendingBossHp;
|
||||
s16 pendingBossNum;
|
||||
bool waitForRelease;
|
||||
s16 treasureCollectedClock;
|
||||
u8 treasureCollectedType;
|
||||
bool allTreasureCollected;
|
||||
u8 hitMessageClock;
|
||||
bool hitMessageBullet; // TRUE = blasted, FALSE = smashed
|
||||
bool levelClearing;
|
||||
u32 levelClearClock;
|
||||
u8 levelWaitClock;
|
||||
|
||||
// controls
|
||||
struct controls {
|
||||
|
|
@ -70,7 +78,7 @@ void updateControls(u16 joy, u16 changed, u16 state){
|
|||
struct playerStruct {
|
||||
Vect2D_f32 pos, vel;
|
||||
s16 shotAngle;
|
||||
u8 lives, recoveringClock;
|
||||
u8 lives, recoveringClock, respawnClock;
|
||||
fix32 camera;
|
||||
Sprite* image;
|
||||
};
|
||||
|
|
@ -121,32 +129,33 @@ struct enemy {
|
|||
};
|
||||
struct enemy enemies[ENEMY_COUNT];
|
||||
|
||||
// humans
|
||||
#define HUMAN_COUNT 8
|
||||
#define HUMAN_WALKING 0
|
||||
#define HUMAN_CARRIED 1
|
||||
#define HUMAN_FALLING 2
|
||||
#define HUMAN_COLLECTED 3
|
||||
// treasure
|
||||
#define TREASURE_COUNT 8
|
||||
#define TREASURE_WALKING 0
|
||||
#define TREASURE_CARRIED 1
|
||||
#define TREASURE_FALLING 2
|
||||
#define TREASURE_COLLECTED 3
|
||||
|
||||
struct human {
|
||||
struct treasure {
|
||||
bool active;
|
||||
u8 state;
|
||||
u8 type;
|
||||
s16 carriedBy;
|
||||
s16 trailIndex;
|
||||
Vect2D_f32 pos, vel;
|
||||
Sprite* image;
|
||||
};
|
||||
struct human humans[HUMAN_COUNT];
|
||||
bool humanBeingCarried;
|
||||
struct treasure treasures[TREASURE_COUNT];
|
||||
bool treasureBeingCarried;
|
||||
s16 collectedCount;
|
||||
|
||||
void killHuman(u8 i){
|
||||
if(humans[i].state == HUMAN_CARRIED && humans[i].carriedBy >= 0){
|
||||
enemies[humans[i].carriedBy].ints[3] = -1;
|
||||
humanBeingCarried = FALSE;
|
||||
void killTreasure(u8 i){
|
||||
if(treasures[i].state == TREASURE_CARRIED && treasures[i].carriedBy >= 0){
|
||||
enemies[treasures[i].carriedBy].ints[3] = -1;
|
||||
treasureBeingCarried = FALSE;
|
||||
}
|
||||
humans[i].active = FALSE;
|
||||
SPR_releaseSprite(humans[i].image);
|
||||
treasures[i].active = FALSE;
|
||||
SPR_releaseSprite(treasures[i].image);
|
||||
}
|
||||
|
||||
void killBullet(u8 i, bool explode){
|
||||
|
|
@ -181,13 +190,13 @@ void killEnemy(u8 i){
|
|||
if(enemies[i].hp > 0) return;
|
||||
if(enemies[i].ints[3] >= 0){
|
||||
s16 h = enemies[i].ints[3];
|
||||
if(humans[h].active){
|
||||
humans[h].state = HUMAN_FALLING;
|
||||
humans[h].carriedBy = -1;
|
||||
humans[h].vel.x = 0;
|
||||
humans[h].vel.y = FIX32(3);
|
||||
if(treasures[h].active){
|
||||
treasures[h].state = TREASURE_FALLING;
|
||||
treasures[h].carriedBy = -1;
|
||||
treasures[h].vel.x = 0;
|
||||
treasures[h].vel.y = FIX32(3);
|
||||
}
|
||||
humanBeingCarried = FALSE;
|
||||
treasureBeingCarried = FALSE;
|
||||
}
|
||||
enemies[i].active = FALSE;
|
||||
SPR_releaseSprite(enemies[i].image);
|
||||
|
|
|
|||
136
src/humans.h
|
|
@ -1,136 +0,0 @@
|
|||
#define HUMAN_OFF 16
|
||||
|
||||
|
||||
void spawnHuman(u8 zone){
|
||||
s16 i = -1;
|
||||
for(s16 j = 0; j < HUMAN_COUNT; j++) if(!humans[j].active) { i = j; break; }
|
||||
if(i == -1) return;
|
||||
|
||||
humans[i].active = TRUE;
|
||||
humans[i].state = HUMAN_WALKING;
|
||||
humans[i].carriedBy = -1;
|
||||
|
||||
fix32 zoneStart = FIX32(zone * 512);
|
||||
humans[i].pos.x = zoneStart + FIX32(random() % 512);
|
||||
humans[i].pos.y = GAME_H_F - FIX32(24);
|
||||
|
||||
fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) };
|
||||
humans[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3];
|
||||
humans[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
|
||||
|
||||
humans[i].image = SPR_addSprite(&koaSprite,
|
||||
getScreenX(humans[i].pos.x, player.camera) - HUMAN_OFF, fix32ToInt(humans[i].pos.y) - HUMAN_OFF,
|
||||
TILE_ATTR(PAL0, 0, 0, 0));
|
||||
if(!humans[i].image){
|
||||
humans[i].active = FALSE;
|
||||
return;
|
||||
}
|
||||
SPR_setVisibility(humans[i].image, HIDDEN);
|
||||
}
|
||||
|
||||
static void updateHuman(u8 i){
|
||||
switch(humans[i].state){
|
||||
case HUMAN_WALKING:
|
||||
// Y bounce: bob 4px around ground level
|
||||
if(humans[i].pos.y >= GAME_H_F - FIX32(20) || humans[i].pos.y <= GAME_H_F - FIX32(28))
|
||||
humans[i].vel.y *= -1;
|
||||
|
||||
// X wrap
|
||||
if(humans[i].pos.x >= GAME_WRAP)
|
||||
humans[i].pos.x -= GAME_WRAP;
|
||||
if(humans[i].pos.x < 0)
|
||||
humans[i].pos.x += GAME_WRAP;
|
||||
|
||||
humans[i].pos.x += humans[i].vel.x;
|
||||
humans[i].pos.y += humans[i].vel.y;
|
||||
break;
|
||||
|
||||
case HUMAN_CARRIED:
|
||||
// follow carrier enemy position
|
||||
if(humans[i].carriedBy >= 0 && enemies[humans[i].carriedBy].active){
|
||||
humans[i].pos.x = enemies[humans[i].carriedBy].pos.x;
|
||||
humans[i].pos.y = enemies[humans[i].carriedBy].pos.y + FIX32(16);
|
||||
} else {
|
||||
// carrier died (shouldn't normally reach here, killEnemy handles it)
|
||||
humans[i].state = HUMAN_FALLING;
|
||||
humans[i].carriedBy = -1;
|
||||
humans[i].vel.x = 0;
|
||||
humans[i].vel.y = FIX32(3);
|
||||
humanBeingCarried = FALSE;
|
||||
}
|
||||
break;
|
||||
|
||||
case HUMAN_FALLING:
|
||||
humans[i].pos.y += humans[i].vel.y;
|
||||
// land on ground
|
||||
if(humans[i].pos.y >= GAME_H_F - FIX32(24)){
|
||||
humans[i].pos.y = GAME_H_F - FIX32(24);
|
||||
humans[i].state = HUMAN_WALKING;
|
||||
fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) };
|
||||
humans[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3];
|
||||
humans[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
|
||||
}
|
||||
break;
|
||||
|
||||
case HUMAN_COLLECTED: {
|
||||
fix32 targetX, targetY;
|
||||
if(humans[i].trailIndex == 0){
|
||||
targetX = player.pos.x;
|
||||
targetY = player.pos.y + FIX32(24);
|
||||
} else {
|
||||
// find the human ahead in the chain
|
||||
targetX = player.pos.x;
|
||||
targetY = player.pos.y + FIX32(24);
|
||||
for(s16 j = 0; j < HUMAN_COUNT; j++){
|
||||
if(humans[j].active && humans[j].state == HUMAN_COLLECTED
|
||||
&& humans[j].trailIndex == humans[i].trailIndex - 1){
|
||||
targetX = humans[j].pos.x;
|
||||
targetY = humans[j].pos.y + FIX32(8);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fix32 deltaX = getWrappedDelta(targetX, humans[i].pos.x);
|
||||
humans[i].pos.x += deltaX >> 2;
|
||||
humans[i].pos.y += (targetY - humans[i].pos.y) >> 2;
|
||||
// X wrap
|
||||
if(humans[i].pos.x >= GAME_WRAP)
|
||||
humans[i].pos.x -= GAME_WRAP;
|
||||
if(humans[i].pos.x < 0)
|
||||
humans[i].pos.x += GAME_WRAP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// collect: check overlap with player (walking or falling only)
|
||||
fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x);
|
||||
if(humans[i].state != HUMAN_CARRIED && humans[i].state != HUMAN_COLLECTED){
|
||||
fix32 dy = humans[i].pos.y - player.pos.y;
|
||||
if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){
|
||||
score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000;
|
||||
sfxPickup();
|
||||
humans[i].state = HUMAN_COLLECTED;
|
||||
humans[i].trailIndex = collectedCount++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
s16 sx = getScreenX(humans[i].pos.x, player.camera);
|
||||
s16 sy = fix32ToInt(humans[i].pos.y);
|
||||
bool visible = (humans[i].state == HUMAN_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
||||
SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN);
|
||||
SPR_setPosition(humans[i].image, sx - HUMAN_OFF, sy - HUMAN_OFF);
|
||||
// manually animate flash only when walking or falling
|
||||
if(humans[i].state == HUMAN_WALKING || humans[i].state == HUMAN_FALLING)
|
||||
SPR_setFrame(humans[i].image, (clock / 15) & 1);
|
||||
else if(humans[i].state == HUMAN_COLLECTED)
|
||||
SPR_setAnim(humans[i].image, 1);
|
||||
else
|
||||
SPR_setFrame(humans[i].image, 0);
|
||||
}
|
||||
|
||||
void updateHumans(){
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++)
|
||||
if(humans[i].active)
|
||||
updateHuman(i);
|
||||
}
|
||||
38
src/main.c
|
|
@ -5,7 +5,7 @@
|
|||
#include "background.h"
|
||||
#include "bullets.h"
|
||||
#include "enemies.h"
|
||||
#include "humans.h"
|
||||
#include "treasure.h"
|
||||
#include "player.h"
|
||||
#include "stage.h"
|
||||
#include "chrome.h"
|
||||
|
|
@ -28,10 +28,12 @@ void clearLevel(){
|
|||
if(bullets[i].active) killBullet(i, FALSE);
|
||||
for(s16 i = 0; i < ENEMY_COUNT; i++)
|
||||
if(enemies[i].active){ enemies[i].hp = 0; killEnemy(i); }
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++)
|
||||
if(humans[i].active) killHuman(i);
|
||||
humanBeingCarried = FALSE;
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++)
|
||||
if(treasures[i].active) killTreasure(i);
|
||||
treasureBeingCarried = FALSE;
|
||||
collectedCount = 0;
|
||||
allTreasureCollected = FALSE;
|
||||
treasureCollectedClock = 0;
|
||||
// black out everything
|
||||
SPR_setVisibility(player.image, HIDDEN);
|
||||
VDP_clearTileMapRect(BG_A, 0, 0, 128, 32);
|
||||
|
|
@ -46,7 +48,7 @@ void loadGame(){
|
|||
XGM2_play(stageMusic);
|
||||
XGM2_setFMVolume(MUSIC_VOLUME);
|
||||
XGM2_setPSGVolume(MUSIC_VOLUME);
|
||||
player.recoveringClock = 120;
|
||||
player.recoveringClock = 240;
|
||||
killBullets = TRUE;
|
||||
started = TRUE;
|
||||
}
|
||||
|
|
@ -61,32 +63,41 @@ static void updateGame(){
|
|||
}
|
||||
if(levelClearClock >= 120){
|
||||
levelClearing = FALSE;
|
||||
player.pos.y = FIX32(112);
|
||||
player.camera = player.pos.x - FIX32(160);
|
||||
playerVelX = 0;
|
||||
loadBackground();
|
||||
loadChrome();
|
||||
loadLevel(level + 1);
|
||||
XGM2_play(stageMusic);
|
||||
SPR_setVisibility(player.image, VISIBLE);
|
||||
player.recoveringClock = 120;
|
||||
player.recoveringClock = 240;
|
||||
killBullets = TRUE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(levelWaitClock > 0){
|
||||
levelWaitClock--;
|
||||
if(levelWaitClock == 0){
|
||||
levelClearing = TRUE;
|
||||
levelClearClock = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!paused){
|
||||
updatePlayer();
|
||||
updateBackground();
|
||||
if(clock % 2 == 0){
|
||||
updateEnemies();
|
||||
if(!gameOver && enemyCount == 0){
|
||||
if(!gameOver && enemyCount == 0 && levelWaitClock == 0){
|
||||
if(level >= LEVEL_COUNT - 1){
|
||||
gameOver = TRUE;
|
||||
XGM2_stop();
|
||||
} else {
|
||||
levelClearing = TRUE;
|
||||
levelClearClock = 0;
|
||||
XGM2_stop();
|
||||
levelWaitClock = 240;
|
||||
killBullets = TRUE;
|
||||
}
|
||||
}
|
||||
updateHumans();
|
||||
updateTreasures();
|
||||
} else {
|
||||
updateBullets();
|
||||
}
|
||||
|
|
@ -95,7 +106,8 @@ static void updateGame(){
|
|||
|
||||
int main(bool hardReset){
|
||||
loadInternals();
|
||||
loadStart();
|
||||
if(SKIP_START) loadGame();
|
||||
else loadStart();
|
||||
while(1){
|
||||
if(started) updateGame();
|
||||
else updateStart();
|
||||
|
|
|
|||
41
src/player.h
|
|
@ -1,9 +1,6 @@
|
|||
#define PLAYER_SPEED FIX32(6)
|
||||
|
||||
#define PLAYER_SPEED_FOCUS FIX32(3.5)
|
||||
|
||||
#define PLAYER_ACCEL PLAYER_SPEED >> 4
|
||||
#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 4
|
||||
|
||||
#define PLAYER_OFF 24
|
||||
#define PLAYER_BOUND_Y FIX32(PLAYER_OFF)
|
||||
|
|
@ -25,11 +22,7 @@ static void movePlayer(){
|
|||
|
||||
fix32 targetVelX = 0;
|
||||
if(ctrl.left || ctrl.right || ctrl.up || ctrl.down){
|
||||
if(ctrl.b){
|
||||
playerSpeed = PLAYER_SPEED_FOCUS;
|
||||
} else {
|
||||
playerSpeed = PLAYER_SPEED;
|
||||
}
|
||||
if(ctrl.left || ctrl.right){
|
||||
if(!ctrl.a) player.shotAngle = ctrl.left ? 512 : 0;
|
||||
targetVelX = ctrl.left ? -playerSpeed : playerSpeed;
|
||||
|
|
@ -40,10 +33,10 @@ static void movePlayer(){
|
|||
}
|
||||
|
||||
if(playerVelX < targetVelX){
|
||||
playerVelX += ctrl.b ? PLAYER_ACCEL_FOCUS : PLAYER_ACCEL;
|
||||
playerVelX += PLAYER_ACCEL;
|
||||
if(playerVelX > targetVelX) playerVelX = targetVelX;
|
||||
} else if(playerVelX > targetVelX){
|
||||
playerVelX -= ctrl.b ? PLAYER_ACCEL_FOCUS : PLAYER_ACCEL;
|
||||
playerVelX -= PLAYER_ACCEL;
|
||||
if(playerVelX < targetVelX) playerVelX = targetVelX;
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +98,7 @@ static void shootPlayer(){
|
|||
.player = TRUE
|
||||
};
|
||||
void updater(s16 i){
|
||||
if(bullets[i].clock == 5) killBullet(i, TRUE);
|
||||
if(bullets[i].clock == 4) killBullet(i, TRUE);
|
||||
}
|
||||
spawnBullet(spawner, updater);
|
||||
sfxPlayerShot();
|
||||
|
|
@ -116,11 +109,11 @@ static void shootPlayer(){
|
|||
void loadPlayer(){
|
||||
player.shotAngle = 0;
|
||||
player.camera = 0;
|
||||
player.pos.x = FIX32(128);
|
||||
player.pos.x = FIX32(160);
|
||||
player.pos.y = FIX32(112);
|
||||
playerVelX = 0;
|
||||
player.lives = 3;
|
||||
player.image = SPR_addSprite(&sakuyaSprite,
|
||||
player.image = SPR_addSprite(&momoyoSprite,
|
||||
fix32ToInt(player.pos.x) - PLAYER_OFF,
|
||||
fix32ToInt(player.pos.y) - PLAYER_OFF,
|
||||
TILE_ATTR(PAL0, 0, 0, 0));
|
||||
|
|
@ -133,6 +126,30 @@ void updatePlayer(){
|
|||
return;
|
||||
}
|
||||
if(!gameOver){
|
||||
if(player.respawnClock > 0){
|
||||
// kill momentum
|
||||
playerVelX = 0;
|
||||
player.vel.x = 0;
|
||||
player.vel.y = 0;
|
||||
// lerp camera to center on player
|
||||
fix32 targetCamera = player.pos.x - FIX32(160);
|
||||
fix32 cameraDelta = getWrappedDelta(targetCamera, player.camera);
|
||||
player.camera += cameraDelta >> 3;
|
||||
// lerp player Y to center of screen
|
||||
fix32 targetY = FIX32(112);
|
||||
player.pos.y += (targetY - player.pos.y) >> 3;
|
||||
// keep sprite position in sync so it doesn't pop on reappear
|
||||
s16 sx = getScreenX(player.pos.x, player.camera);
|
||||
s16 sy = fix32ToInt(player.pos.y);
|
||||
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
|
||||
player.respawnClock--;
|
||||
if(player.respawnClock == 0){
|
||||
SPR_setVisibility(player.image, VISIBLE);
|
||||
player.recoveringClock = 240;
|
||||
killBullets = TRUE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(player.recoveringClock > 0){
|
||||
if(player.recoveringClock % 10 == 1)
|
||||
SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN);
|
||||
|
|
|
|||
40
src/stage.h
|
|
@ -3,7 +3,7 @@
|
|||
// =============================================================================
|
||||
//
|
||||
// Each level is a single struct defining what spawns. The level ends when all
|
||||
// enemies are killed (enemyCount == 0). Humans are bonus -- they don't affect
|
||||
// enemies are killed (enemyCount == 0). Treasures are bonus -- they don't affect
|
||||
// level completion.
|
||||
//
|
||||
// --- STRUCT FIELDS ---
|
||||
|
|
@ -24,12 +24,12 @@
|
|||
// Very dangerous -- 2-3 is oppressive, 6 is near-impossible.
|
||||
// Good range: 0-6. Introduce after players learn movement.
|
||||
//
|
||||
// builders Number of Builder enemies (type 4). Human abductor.
|
||||
// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking humans
|
||||
// and flies upward. If it reaches the top, the human dies and a
|
||||
// Gunner spawns in its place. Only 1 human can be carried at a time.
|
||||
// builders Number of Builder enemies (type 4). Treasure abductor.
|
||||
// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking treasures
|
||||
// and flies upward. If it reaches the top, the treasure dies and a
|
||||
// Gunner spawns in its place. Only 1 treasure can be carried at a time.
|
||||
// Creates urgency -- player must choose between killing enemies
|
||||
// and saving humans. Good range: 0-2.
|
||||
// and saving treasures. Good range: 0-2.
|
||||
//
|
||||
// bossHp If > 0, spawns a Boss enemy (type 5) with this many HP.
|
||||
// Boss number is auto-calculated from level index (lvl / 4).
|
||||
|
|
@ -38,11 +38,11 @@
|
|||
// Typical values: 25, 50, 75, 100, 125.
|
||||
// Other enemies can coexist with the boss (adds pressure).
|
||||
//
|
||||
// humans Number of humans (koakuma) to spawn. Distributed across 4 zones
|
||||
// (2 per zone if >= 4 humans, then 1 each for remainder).
|
||||
// treasures Number of treasures to spawn. Distributed across 4 zones
|
||||
// (2 per zone if >= 4 treasures, then 1 each for remainder).
|
||||
// Walk along the ground, can be collected by player for 1000 pts
|
||||
// (2000 if caught mid-fall after enemy drops them).
|
||||
// Max 8 (HUMAN_COUNT). Usually just set to 8.
|
||||
// Max 8 (TREASURE_COUNT). Usually just set to 8.
|
||||
//
|
||||
// gunnerPattern Controls what bullet pattern gunners use:
|
||||
// 0 = Radial Burst: 8 bullets in a circle every 60 frames.
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
// Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss
|
||||
// must not exceed 24. If it does, excess enemies silently fail to spawn.
|
||||
//
|
||||
// Total humans: 8 slots (HUMAN_COUNT).
|
||||
// Total treasures: 8 slots (TREASURE_COUNT).
|
||||
//
|
||||
// Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets
|
||||
// get priority and evict enemy bullets when full.
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
// - Drone-heavy levels (12-16) create constant movement pressure
|
||||
// - Gunner-heavy levels (4-6) create bullet reading / dodging challenges
|
||||
// - Hunter levels force the player to keep moving (anti-camping)
|
||||
// - Builder levels force tough choices: kill builders or save humans?
|
||||
// - Builder levels force tough choices: kill builders or save treasures?
|
||||
// - Combining hunters + gunners is very hard (dodge bullets while fleeing)
|
||||
// - Boss levels with escort enemies (drones/gunners alongside boss) are
|
||||
// harder than solo boss fights
|
||||
|
|
@ -93,12 +93,12 @@
|
|||
struct LevelDef {
|
||||
u8 drones, gunners, hunters, builders;
|
||||
u8 bossHp;
|
||||
u8 humans;
|
||||
u8 treasures;
|
||||
u8 gunnerPattern;
|
||||
bool dronesShoot;
|
||||
};
|
||||
|
||||
// dr gn hn bl boss hum pat shoot
|
||||
// dr gn hn bl boss tre pat shoot
|
||||
const struct LevelDef levels[20] = {
|
||||
// Phase 1: "Immediate danger" (L1-L4)
|
||||
{ 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1
|
||||
|
|
@ -168,13 +168,13 @@ void loadLevel(u8 lvl){
|
|||
spawnEnemy(ENEMY_TYPE_BOSS, 1);
|
||||
}
|
||||
|
||||
// spawn humans
|
||||
u8 humansToSpawn = def->humans;
|
||||
for(u8 zone = 0; zone < 4 && humansToSpawn > 0; zone++){
|
||||
u8 perZone = humansToSpawn >= 4 ? 2 : 1;
|
||||
for(u8 h = 0; h < perZone && humansToSpawn > 0; h++){
|
||||
spawnHuman(zone);
|
||||
humansToSpawn--;
|
||||
// spawn treasures
|
||||
u8 treasureToSpawn = def->treasures;
|
||||
for(u8 zone = 0; zone < 4 && treasureToSpawn > 0; zone++){
|
||||
u8 perZone = treasureToSpawn >= 4 ? 2 : 1;
|
||||
for(u8 h = 0; h < perZone && treasureToSpawn > 0; h++){
|
||||
spawnTreasure(zone);
|
||||
treasureToSpawn--;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
85
src/start.h
|
|
@ -1,15 +1,82 @@
|
|||
#define START_I 8
|
||||
#define START_W 40
|
||||
#define START_H 28
|
||||
#define START_I 1
|
||||
#define TRANS_TIME 200
|
||||
|
||||
s16 startClock;
|
||||
|
||||
static void updateTransition(s16 startTime, bool last){
|
||||
if(startClock >= startTime && startClock < startTime + TRANS_TIME){
|
||||
switch(startClock - startTime){
|
||||
case 0: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break;
|
||||
case 5: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break;
|
||||
case 10: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break;
|
||||
case 15: VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H); break;
|
||||
case 20: VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H); break;
|
||||
case TRANS_TIME - 20: if(!last){
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 3), 0, 0, START_W, START_H);
|
||||
} break;
|
||||
case TRANS_TIME - 15: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 2), 0, 0, START_W, START_H); break;
|
||||
case TRANS_TIME - 10: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I + 1), 0, 0, START_W, START_H); break;
|
||||
case TRANS_TIME - 5: if(!last) VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 1, 0, 0, START_I), 0, 0, START_W, START_H); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void drawStartSplash(){
|
||||
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I), 0, 0, START_W, START_H);
|
||||
VDP_drawImageEx(BG_B, &startSplash1, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 256), 13, 7, 0, DMA);
|
||||
}
|
||||
|
||||
static void drawStartBg(){
|
||||
VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H);
|
||||
VDP_drawImageEx(BG_B, &startBigBg, TILE_ATTR_FULL(PAL1, 0, 0, 0, START_I + 16), 0, 0, 0, DMA);
|
||||
}
|
||||
|
||||
static void drawStartMenu(){
|
||||
// VDP_drawImageEx(BG_A, &startLogo, TILE_ATTR_FULL(PAL0, 0, 0, 0, START_I + 16 + 256), 26, 2, 0, DMA);
|
||||
VDP_drawText(" PRESS ANY", 19, 18);
|
||||
VDP_drawText(" BUTTON", 19, 19);
|
||||
VDP_drawText(" T. BODDY", 19, 24);
|
||||
VDP_drawText(" 02.2026", 19, 25);
|
||||
}
|
||||
|
||||
static void loadGameFromStart(){
|
||||
XGM2_stop();
|
||||
setRandomSeed(startClock);
|
||||
VDP_clearTileMapRect(BG_A, 0, 0, START_W, START_H);
|
||||
VDP_clearTileMapRect(BG_B, 0, 0, START_W, START_H);
|
||||
loadGame();
|
||||
}
|
||||
|
||||
s16 startTime;
|
||||
static void updateStartMenu(){
|
||||
if(startTime == 0 && (ctrl.start || ctrl.a || ctrl.b || ctrl.c)){
|
||||
XGM2_stop();
|
||||
startTime = 30;
|
||||
}
|
||||
if(startTime > 0){
|
||||
startTime--;
|
||||
if(startTime <= 0){
|
||||
loadGameFromStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
VDP_loadTileSet(startFade1.tileset, START_I, DMA);
|
||||
VDP_loadTileSet(startFade2.tileset, START_I + 1, DMA);
|
||||
VDP_loadTileSet(startFade3.tileset, START_I + 2, DMA);
|
||||
VDP_loadTileSet(startFade4.tileset, START_I + 3, DMA);
|
||||
drawStartSplash();
|
||||
XGM2_play(bgmStart);
|
||||
}
|
||||
|
||||
void updateStart(){
|
||||
if(ctrl.a || ctrl.b || ctrl.c || ctrl.start){
|
||||
VDP_clearTileMapRect(BG_A, 0, 0, 40, 28);
|
||||
waitForRelease = TRUE;
|
||||
loadGame();
|
||||
}
|
||||
updateTransition(0, FALSE);
|
||||
updateTransition(TRANS_TIME, TRUE);
|
||||
if(startClock == TRANS_TIME) drawStartBg();
|
||||
else if(startClock == TRANS_TIME + 40) drawStartMenu();
|
||||
else if(startClock > TRANS_TIME + 40) updateStartMenu();
|
||||
if(startClock < CLOCK_LIMIT) startClock++;
|
||||
}
|
||||
159
src/treasure.h
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
#define TREASURE_OFF 16
|
||||
|
||||
|
||||
void spawnTreasure(u8 zone){
|
||||
s16 i = -1;
|
||||
for(s16 j = 0; j < TREASURE_COUNT; j++) if(!treasures[j].active) { i = j; break; }
|
||||
if(i == -1) return;
|
||||
|
||||
treasures[i].active = TRUE;
|
||||
treasures[i].state = TREASURE_WALKING;
|
||||
treasures[i].carriedBy = -1;
|
||||
|
||||
fix32 zoneStart = FIX32(zone * 512);
|
||||
treasures[i].pos.x = zoneStart + FIX32(random() % 512);
|
||||
treasures[i].pos.y = GAME_H_F - FIX32(24);
|
||||
|
||||
fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) };
|
||||
treasures[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3];
|
||||
treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
|
||||
|
||||
treasures[i].image = SPR_addSprite(&treasureSprite,
|
||||
getScreenX(treasures[i].pos.x, player.camera) - TREASURE_OFF, fix32ToInt(treasures[i].pos.y) - TREASURE_OFF,
|
||||
TILE_ATTR(PAL0, 0, 0, 0));
|
||||
if(!treasures[i].image){
|
||||
treasures[i].active = FALSE;
|
||||
return;
|
||||
}
|
||||
SPR_setVisibility(treasures[i].image, HIDDEN);
|
||||
treasures[i].type = random() % 4;
|
||||
SPR_setAnim(treasures[i].image, treasures[i].type);
|
||||
}
|
||||
|
||||
static void updateTreasure(u8 i){
|
||||
switch(treasures[i].state){
|
||||
case TREASURE_WALKING:
|
||||
// Y bounce: bob 4px around ground level
|
||||
if(treasures[i].pos.y >= GAME_H_F - FIX32(20) || treasures[i].pos.y <= GAME_H_F - FIX32(28))
|
||||
treasures[i].vel.y *= -1;
|
||||
|
||||
// X wrap
|
||||
if(treasures[i].pos.x >= GAME_WRAP)
|
||||
treasures[i].pos.x -= GAME_WRAP;
|
||||
if(treasures[i].pos.x < 0)
|
||||
treasures[i].pos.x += GAME_WRAP;
|
||||
|
||||
treasures[i].pos.x += treasures[i].vel.x;
|
||||
treasures[i].pos.y += treasures[i].vel.y;
|
||||
break;
|
||||
|
||||
case TREASURE_CARRIED:
|
||||
// follow carrier enemy position
|
||||
if(treasures[i].carriedBy >= 0 && enemies[treasures[i].carriedBy].active){
|
||||
treasures[i].pos.x = enemies[treasures[i].carriedBy].pos.x;
|
||||
treasures[i].pos.y = enemies[treasures[i].carriedBy].pos.y + FIX32(16);
|
||||
} else {
|
||||
// carrier died (shouldn't normally reach here, killEnemy handles it)
|
||||
treasures[i].state = TREASURE_FALLING;
|
||||
treasures[i].carriedBy = -1;
|
||||
treasures[i].vel.x = 0;
|
||||
treasures[i].vel.y = FIX32(3);
|
||||
treasureBeingCarried = FALSE;
|
||||
}
|
||||
break;
|
||||
|
||||
case TREASURE_FALLING:
|
||||
treasures[i].pos.y += treasures[i].vel.y;
|
||||
// land on ground
|
||||
if(treasures[i].pos.y >= GAME_H_F - FIX32(24)){
|
||||
treasures[i].pos.y = GAME_H_F - FIX32(24);
|
||||
treasures[i].state = TREASURE_WALKING;
|
||||
fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) };
|
||||
treasures[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3];
|
||||
treasures[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
|
||||
}
|
||||
break;
|
||||
|
||||
case TREASURE_COLLECTED: {
|
||||
fix32 targetX, targetY;
|
||||
if(treasures[i].trailIndex == 0){
|
||||
targetX = player.pos.x;
|
||||
targetY = player.pos.y + FIX32(24);
|
||||
} else {
|
||||
// find the treasure ahead in the chain
|
||||
targetX = player.pos.x;
|
||||
targetY = player.pos.y + FIX32(24);
|
||||
for(s16 j = 0; j < TREASURE_COUNT; j++){
|
||||
if(treasures[j].active && treasures[j].state == TREASURE_COLLECTED
|
||||
&& treasures[j].trailIndex == treasures[i].trailIndex - 1){
|
||||
targetX = treasures[j].pos.x;
|
||||
targetY = treasures[j].pos.y + FIX32(8);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fix32 deltaX = getWrappedDelta(targetX, treasures[i].pos.x);
|
||||
treasures[i].pos.x += deltaX >> 2;
|
||||
treasures[i].pos.y += (targetY - treasures[i].pos.y) >> 2;
|
||||
// X wrap
|
||||
if(treasures[i].pos.x >= GAME_WRAP)
|
||||
treasures[i].pos.x -= GAME_WRAP;
|
||||
if(treasures[i].pos.x < 0)
|
||||
treasures[i].pos.x += GAME_WRAP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// collect: check overlap with player (walking or falling only)
|
||||
fix32 dx = getWrappedDelta(treasures[i].pos.x, player.pos.x);
|
||||
if(treasures[i].state != TREASURE_CARRIED && treasures[i].state != TREASURE_COLLECTED){
|
||||
fix32 dy = treasures[i].pos.y - player.pos.y;
|
||||
if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){
|
||||
score += (treasures[i].state == TREASURE_FALLING) ? 2000 : 1000;
|
||||
sfxPickup();
|
||||
treasureCollectedType = treasures[i].type;
|
||||
treasureCollectedClock = 120;
|
||||
// only add to trail if this type isn't already collected
|
||||
bool duplicate = FALSE;
|
||||
for(s16 j = 0; j < TREASURE_COUNT; j++){
|
||||
if(treasures[j].active && treasures[j].state == TREASURE_COLLECTED
|
||||
&& treasures[j].type == treasures[i].type){
|
||||
duplicate = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(duplicate){
|
||||
killTreasure(i);
|
||||
} else {
|
||||
treasures[i].state = TREASURE_COLLECTED;
|
||||
treasures[i].trailIndex = collectedCount++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
s16 sx = getScreenX(treasures[i].pos.x, player.camera);
|
||||
s16 sy = fix32ToInt(treasures[i].pos.y);
|
||||
bool visible = (treasures[i].state == TREASURE_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
||||
if(visible && treasures[i].state == TREASURE_COLLECTED){
|
||||
if(player.respawnClock > 0)
|
||||
visible = FALSE;
|
||||
else if(player.recoveringClock > 0)
|
||||
visible = (player.recoveringClock % 20 > 10);
|
||||
}
|
||||
SPR_setVisibility(treasures[i].image, visible ? VISIBLE : HIDDEN);
|
||||
SPR_setPosition(treasures[i].image, sx - TREASURE_OFF, sy - TREASURE_OFF);
|
||||
// frame 0 = normal, frame 1 = flash, frame 2 = collected/faded
|
||||
if(treasures[i].state == TREASURE_WALKING || treasures[i].state == TREASURE_FALLING)
|
||||
SPR_setFrame(treasures[i].image, (clock / 15) & 1);
|
||||
else if(treasures[i].state == TREASURE_COLLECTED)
|
||||
SPR_setFrame(treasures[i].image, 2);
|
||||
else
|
||||
SPR_setFrame(treasures[i].image, 0);
|
||||
}
|
||||
|
||||
void updateTreasures(){
|
||||
for(s16 i = 0; i < TREASURE_COUNT; i++)
|
||||
if(treasures[i].active)
|
||||
updateTreasure(i);
|
||||
}
|
||||