diff --git a/res/fontbig.png b/res/fontbig.png new file mode 100644 index 0000000..d394a91 Binary files /dev/null and b/res/fontbig.png differ diff --git a/res/fontbigshadow.png b/res/fontbigshadow.png new file mode 100644 index 0000000..56ae8d0 Binary files /dev/null and b/res/fontbigshadow.png differ diff --git a/res/ground.png b/res/ground.png index 1d66e9e..c69bc69 100644 Binary files a/res/ground.png and b/res/ground.png differ diff --git a/res/human.png b/res/human.png new file mode 100644 index 0000000..10d026d Binary files /dev/null and b/res/human.png differ diff --git a/res/life.png b/res/life.png new file mode 100644 index 0000000..6a7c981 Binary files /dev/null and b/res/life.png differ diff --git a/res/life2.png b/res/life2.png new file mode 100644 index 0000000..2b01299 Binary files /dev/null and b/res/life2.png differ diff --git a/res/mapindicator.png b/res/mapindicator.png index 93ae0b5..bff5d3a 100644 Binary files a/res/mapindicator.png and b/res/mapindicator.png differ diff --git a/res/resources.res b/res/resources.res index 000a46b..588ee61 100644 --- a/res/resources.res +++ b/res/resources.res @@ -20,7 +20,13 @@ SPRITE butterflySprite "butterfly.png" 4 4 NONE 8 // SPRITE fairySprite "fairy.png" 4 4 NONE 8 SPRITE fairySprite "fairy2.png" 4 4 NONE 8 SPRITE aliceSprite "alice.png" 6 6 NONE 0 +SPRITE humanSprite "human.png" 2 2 NONE 0 // IMAGE mapPlayer "mapplayer.png" NONE NONE IMAGE mapIndicator "mapindicator.png" NONE NONE -// IMAGE mapFrame "mapframe.png" NONE NONE \ No newline at end of file +// 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 \ No newline at end of file diff --git a/res/sky.png b/res/sky.png index bad2f06..e119cf2 100644 Binary files a/res/sky.png and b/res/sky.png differ diff --git a/src/background.h b/src/background.h index 303132b..d1df8a3 100644 --- a/src/background.h +++ b/src/background.h @@ -2,7 +2,7 @@ // #define FADE_TOP_I BG_I + 64 // #define FADE_BOTTOM_I FADE_TOP_I + 64 -#define BG_OFF 112 +#define BG_OFF 24 void loadBackground(){ VDP_loadTileSet(sky.tileset, BG_I, DMA); @@ -11,7 +11,7 @@ void loadBackground(){ // 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); + VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + (y > 2 ? 64 : 0)), x * 8, y * 8, 8, 8); } } // for(u8 x = 0; x < 5; x++){ @@ -22,5 +22,5 @@ void loadBackground(){ void updateBackground(){ VDP_setHorizontalScroll(BG_B, fix32ToInt(-player.camera)); - VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 2); + VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 3); } \ No newline at end of file diff --git a/src/bullets.h b/src/bullets.h index 5c34ff1..4540bef 100644 --- a/src/bullets.h +++ b/src/bullets.h @@ -57,6 +57,7 @@ static void doBulletRotation(u8 i){ } void spawnBullet(struct bulletSpawner spawner, void(*updater)){ + if(player.recoveringClock > 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); @@ -124,6 +125,7 @@ static void collideWithEnemy(u8 i){ deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){ bulletDist = getApproximatedDistance(fix32ToInt(deltaX), fix32ToInt(deltaY)); if(bulletDist <= bullets[i].dist){ + score += (enemies[j].ints[3] >= 0) ? 200 : 100; killBullet(i, TRUE); killEnemy(j); sfxExplosion(); @@ -134,6 +136,7 @@ static void collideWithEnemy(u8 i){ } static void collideWithPlayer(u8 i){ + if(player.recoveringClock > 0) return; fix32 deltaX = getWrappedDelta(bullets[i].pos.x, player.pos.x); fix32 deltaY = bullets[i].pos.y - player.pos.y; @@ -143,8 +146,13 @@ static void collideWithPlayer(u8 i){ if(dist <= 4){ killBullet(i, TRUE); sfxExplosion(); - // player.lives--; - // if(player.lives <= 0) gameOver = TRUE; + player.lives--; + if(player.lives == 0){ + gameOver = TRUE; + } else { + player.recoveringClock = 120; + killBullets = TRUE; + } } } @@ -195,7 +203,7 @@ static void updateBullet(u8 i){ } if(bullets[i].clock > 0) bullets[i].updater(i); if(bullets[i].player) collideWithEnemy(i); - else collideWithPlayer(i); + else if(!gameOver) collideWithPlayer(i); if(bullets[i].active){ s16 sx = getScreenX(bullets[i].pos.x, player.camera); s16 sy = fix32ToInt(bullets[i].pos.y); @@ -209,6 +217,11 @@ static void updateBullet(u8 i){ void updateBullets(){ bulletCount = 0; + if(killBullets){ + killBullets = FALSE; + for(s16 i = 0; i < BULLET_COUNT; i++) + if(bullets[i].active && !bullets[i].player) killBullet(i, TRUE); + } for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active) updateBullet(i); // intToStr(bulletCount, debugStr, 1); diff --git a/src/chrome.h b/src/chrome.h index 1aa8d6e..a82bc22 100644 --- a/src/chrome.h +++ b/src/chrome.h @@ -2,18 +2,55 @@ #define MAP_TILE TILE_ATTR_FULL(PAL1, 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_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3) +#define MAP_HUMAN_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 + +void bigText(char* str, u16 x, u16 y, bool shadow){ + for(u8 i = 0; i < strlen(str); i++){ + if(str[i] >= 48){ + VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + str[i] - 48), x + i, y); + VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + 16 + str[i] - 48), x + i, y + 1); + } + } +} char scoreStr[SCORE_LENGTH]; u32 lastScore; +s16 scoreLength; + +#define SCORE_X 1 +#define SCORE_Y 5 + +#define LIFE_I (FONT_BIG_I + 64) +#define LIVES_X 38 +#define LIVES_Y 5 +s16 lastLives; + +static void drawLives(){ + VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16); + for(u8 i = 0; i < (player.lives - 1); i++) + VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, LIFE_I + (i > 0 ? 2 : 0)), LIVES_X, LIVES_Y + i, 1, 2); + lastLives = player.lives; +} // previous map positions: -1 means not drawn s16 mapEnemyCol[ENEMY_COUNT], mapEnemyRow[ENEMY_COUNT]; +s16 mapHumanCol[HUMAN_COUNT], mapHumanRow[HUMAN_COUNT]; s16 mapPlayerRow; static void drawScore(){ - uintToStr(score, scoreStr, 1); - VDP_drawText(scoreStr, 1, 5); + if(lastScore < 10) scoreLength = 1; + else if(lastScore < 100) scoreLength = 2; + else if(lastScore < 1000) scoreLength = 3; + else if(lastScore < 10000) scoreLength = 4; + else if(lastScore < 100000) scoreLength = 5; + else if(lastScore < 1000000) scoreLength = 6; + else if(lastScore < 10000000) scoreLength = 7; + else scoreLength = 8; + uintToStr(lastScore, scoreStr, scoreLength); + bigText(scoreStr, SCORE_X, SCORE_Y, FALSE); } // load map when stage does so we have all enemies + player @@ -29,17 +66,24 @@ void loadMap(){ mapEnemyCol[i] = -1; mapEnemyRow[i] = -1; } + for(s16 i = 0; i < HUMAN_COUNT; i++){ + mapHumanCol[i] = -1; + mapHumanRow[i] = -1; + } mapPlayerRow = -1; } // temp arrays for new positions s16 mapNewCol[ENEMY_COUNT], mapNewRow[ENEMY_COUNT]; +s16 mapNewHumanCol[HUMAN_COUNT], mapNewHumanRow[HUMAN_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; return FALSE; } @@ -67,6 +111,24 @@ 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){ + mapNewHumanCol[i] = -1; + mapNewHumanRow[i] = -1; + continue; + } + fix32 dx = getWrappedDelta(humans[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; + if(row < 0) row = 0; + if(row >= MAP_H) row = MAP_H - 1; + mapNewHumanCol[i] = col; + mapNewHumanRow[i] = row; + } + // clear old player tile if it moved and nothing new occupies it if(mapPlayerRow >= 0 && mapPlayerRow != pRow) if(!mapTileOccupied(MAP_W / 2, mapPlayerRow, pRow)) @@ -80,6 +142,23 @@ 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]); + } + + // 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 enemy dots (skip if player occupies same tile) for(s16 i = 0; i < ENEMY_COUNT; i++){ mapEnemyCol[i] = mapNewCol[i]; @@ -95,18 +174,42 @@ static void updateMap(){ } void loadChrome(){ + VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA); + VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA); + VDP_loadTileSet(imageChromeLife.tileset, LIFE_I, DMA); + VDP_loadTileSet(imageChromeLife2.tileset, LIFE_I + 2, DMA); VDP_loadTileSet(mapIndicator.tileset, MAP_I, DMA); + lastScore = 1; drawScore(); + drawLives(); +} + +bool didGameOver; +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) SPR_setPalette(humans[i].image, PAL1); + 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); + + VDP_drawText("GAME OVER", 15, 13); + VDP_drawText("PRESS ANY BUTTON", 12, 14); } void updateChrome(){ - score++; - if(score > 99999999) score = 0; + if(gameOver && !didGameOver) doGameOver(); + if(didGameOver) return; if(lastScore != score){ lastScore = score; drawScore(); } + if(lastLives != player.lives) drawLives(); if(clock % 4 == 0) updateMap(); - // VDP_clearText(1, 26, 4); - // VDP_drawText(debugStr, 1, 26); } \ No newline at end of file diff --git a/src/enemies.h b/src/enemies.h index 381a349..1bc4039 100644 --- a/src/enemies.h +++ b/src/enemies.h @@ -34,7 +34,7 @@ void spawnEnemy(u8 type, u8 zone){ do { // Random X within zone: zoneStart + random(0-511) randX = zoneStart + FIX32(random() % 512); - randY = FIX32(16 + (random() % 192)); + randY = FIX32(16 + (random() % 128)); attempts++; } while(!isValidEnemyPosition(randX, randY) && attempts < 100); @@ -61,8 +61,22 @@ void spawnEnemy(u8 type, u8 zone){ } 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].ints[3] >= 0){ + // carrying: only check for reaching the top + if(enemies[i].pos.y <= FIX32(0)){ + s16 h = enemies[i].ints[3]; + if(humans[h].active) killHuman(h); + enemies[i].ints[3] = -1; + humanBeingCarried = FALSE; + // TODO: spawn mutant here + killEnemy(i); + return; + } + } else { + // not carrying: bounce off top and bottom + 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; @@ -73,7 +87,6 @@ static void boundsEnemy(u8 i){ } static void updateEnemy(u8 i){ - boundsEnemy(i); enemies[i].pos.x += enemies[i].vel.x; enemies[i].pos.y += enemies[i].vel.y; @@ -83,6 +96,9 @@ static void updateEnemy(u8 i){ break; } + boundsEnemy(i); + if(!enemies[i].active) return; + 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); diff --git a/src/enemytypes.h b/src/enemytypes.h index f538b02..7d83041 100644 --- a/src/enemytypes.h +++ b/src/enemytypes.h @@ -1,18 +1,81 @@ void loadEnemyOne(u8 i){ - enemies[i].ints[0] = random() % 40; + enemies[i].ints[0] = random() % 60; + enemies[i].ints[2] = -1; // target human index + enemies[i].ints[3] = -1; // carried human index enemies[i].angle = ((random() % 4) * 256) + 128; enemies[i].speed = FIX32(2); } void updateEnemyOne(u8 i){ - if(enemies[i].clock % 40 == enemies[i].ints[0] && enemies[i].onScreen){ - enemies[i].clock % 80 == enemies[i].ints[0] ? sfxEnemyShotB() : sfxEnemyShotA(); + // carrying behavior: move upward, skip shooting + if(enemies[i].ints[3] >= 0){ + // enemies[i].vel.x = (enemies[i].vel.x > 0) ? FIX32(0.3) : FIX32(-0.3); + // enemies[i].vel.y = FIX32(-1.5); + enemies[i].angle = 704 + (random() % 128); + 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); + return; + } + + // cancel any target if a human is already being carried + if(humanBeingCarried && 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; + 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; + fix32 dist = (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy); + if(dist < bestDist && dist < FIX32(256)){ + bestDist = dist; + bestHuman = j; + } + } + enemies[i].ints[2] = bestHuman; + } + + // steer toward target human + if(enemies[i].ints[2] >= 0){ + s16 t = enemies[i].ints[2]; + if(!humans[t].active || humans[t].state != HUMAN_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; + + // hone toward human'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)); + enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed); + enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed); + + // grab check: within 16px + fix32 adx = dx < 0 ? -dx : dx; + fix32 ady = dy < 0 ? -dy : dy; + 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; + return; + } + } + } + + // normal shooting + if(enemies[i].clock % 60 == enemies[i].ints[0] && enemies[i].onScreen){ + enemies[i].clock % 120 == enemies[i].ints[0] ? sfxEnemyShotB() : sfxEnemyShotA(); struct bulletSpawner spawner = { .x = enemies[i].pos.x, .y = enemies[i].pos.y, .anim = 3 + (random() % 3), - // .anim = 6, - // .frame = 1, .speed = FIX32(2) + FIX16(random() % 4), .angle = random() % 128, }; @@ -26,16 +89,3 @@ void updateEnemyOne(u8 i){ } } } - -void updateLander(u8 i){} - -void updateMutant(u8 i){} - -void updateSwarmer(u8 i){} - -void updatePod(u8 i){} - -void updateBomber(u8 i){} - -void updateBaiter(u8 i){} - diff --git a/src/global.h b/src/global.h index 77b84cc..ba83d82 100644 --- a/src/global.h +++ b/src/global.h @@ -50,15 +50,16 @@ void updateControls(u16 joy, u16 changed, u16 state){ struct playerStruct { Vect2D_f32 pos, vel; s16 shotAngle; - u8 lives; + u8 lives, recoveringClock; fix32 camera, yCamera; Sprite* image; }; struct playerStruct player; +bool killBullets; // bullets -#define BULLET_COUNT 64 +#define BULLET_COUNT 70 struct bulletSpawner { fix32 x, y, speed; @@ -92,6 +93,31 @@ struct enemy { }; struct enemy enemies[ENEMY_COUNT]; +// humans +#define HUMAN_COUNT 8 +#define HUMAN_WALKING 0 +#define HUMAN_CARRIED 1 +#define HUMAN_FALLING 2 + +struct human { + bool active; + u8 state; + s16 carriedBy; + Vect2D_f32 pos, vel; + Sprite* image; +}; +struct human humans[HUMAN_COUNT]; +bool humanBeingCarried; + +void killHuman(u8 i){ + if(humans[i].state == HUMAN_CARRIED && humans[i].carriedBy >= 0){ + enemies[humans[i].carriedBy].ints[3] = -1; + humanBeingCarried = FALSE; + } + humans[i].active = FALSE; + SPR_releaseSprite(humans[i].image); +} + void killBullet(u8 i, bool explode){ if(explode){ if(bullets[i].player){ @@ -120,6 +146,16 @@ void killBullet(u8 i, bool explode){ } void killEnemy(u8 i){ + 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(2); + } + humanBeingCarried = FALSE; + } enemies[i].active = FALSE; SPR_releaseSprite(enemies[i].image); } @@ -142,4 +178,31 @@ static s16 getScreenX(fix32 worldX, fix32 camera) { screenX -= GAME_WRAP; } return fix32ToInt(screenX); +} + + +// homing +#define PI_MOD 2.84444444444 +#define PI_F FIX16(3.14159265358 * PI_MOD) +#define PI_F_2 FIX16(1.57079632679 * PI_MOD) +#define PI_F_4 FIX16(0.78539816339 * PI_MOD) +fix16 arctan(fix16 x) { + return fix16Mul(PI_F_4, x) - fix16Mul(fix16Mul(x, (abs(x) - 1)), (FIX16(0.245) + fix16Mul(FIX16(0.066), abs(x)))); +} +fix16 arctan2(fix16 y, fix16 x) { + return x >= 0 ? + (y >= 0 ? (y < x ? arctan(fix16Div(y, x)) : PI_F_2 - arctan(fix16Div(x, y))) : (-y < x ? arctan(fix16Div(y, x)) : -PI_F_2 - arctan(fix16Div(x, y)))) : + (y >= 0 ? (y < -x ? arctan(fix16Div(y, x)) + PI_F : PI_F_2 - arctan(fix16Div(x, y))) : (-y < -x ? arctan(fix16Div(y, x)) - PI_F : -PI_F_2 - arctan(fix16Div(x, y)))); +} +s16 arcAngle; +s16 honeAngle(fix16 x1, fix16 x2, fix16 y1, fix16 y2){ + arcAngle = arctan2(y2 - y1, x2 - x1); + if(arcAngle >= 128) arcAngle -= 32; + if(arcAngle >= 384) arcAngle -= 32; + if(arcAngle < 0){ + arcAngle = 1024 + arcAngle; + if(arcAngle < 896) arcAngle += 32; + if(arcAngle < 640) arcAngle += 32; + } + return arcAngle; } \ No newline at end of file diff --git a/src/humans.h b/src/humans.h new file mode 100644 index 0000000..633bb84 --- /dev/null +++ b/src/humans.h @@ -0,0 +1,96 @@ + +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(&humanSprite, + getScreenX(humans[i].pos.x, player.camera), fix32ToInt(humans[i].pos.y), + TILE_ATTR(PAL0, 0, 0, 0)); + if(!humans[i].image){ + humans[i].active = FALSE; + return; + } +} + +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(2); + 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; + } + + // 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){ + fix32 dy = humans[i].pos.y - player.pos.y; + if(dx >= FIX32(-24) && dx <= FIX32(24) && dy >= FIX32(-24) && dy <= FIX32(24)){ + score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000; + sfxPickup(); + killHuman(i); + return; + } + } + + s16 sx = getScreenX(humans[i].pos.x, player.camera); + s16 sy = fix32ToInt(humans[i].pos.y); + bool visible = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT); + SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN); + SPR_setPosition(humans[i].image, sx, sy); +} + +void updateHumans(){ + for(s16 i = 0; i < HUMAN_COUNT; i++) + if(humans[i].active) + updateHuman(i); +} diff --git a/src/main.c b/src/main.c index 73be5eb..896b72c 100644 --- a/src/main.c +++ b/src/main.c @@ -5,6 +5,7 @@ #include "background.h" #include "bullets.h" #include "enemies.h" +#include "humans.h" #include "player.h" #include "stage.h" #include "chrome.h" @@ -35,6 +36,8 @@ static void updateGame(){ updateSfx(); if(clock % 2 == 0){ updateEnemies(); + if(!gameOver && enemyCount == 0) gameOver = TRUE; + updateHumans(); } else { updateBackground(); updateBullets(); diff --git a/src/player.h b/src/player.h index 75ff7bb..607dff1 100644 --- a/src/player.h +++ b/src/player.h @@ -9,8 +9,8 @@ #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 CAMERA_X FIX32(96) +#define CAMERA_W FIX32(224) #define SHOT_INTERVAL 15 @@ -106,7 +106,10 @@ static void shootPlayer(){ .angle = player.shotAngle, .player = TRUE }; - spawnBullet(spawner, EMPTY); + void updater(s16 i){ + if(bullets[i].clock == 5) killBullet(i, TRUE); + } + spawnBullet(spawner, updater); sfxPlayerShot(); shotClock = SHOT_INTERVAL; } else if(shotClock > 0) shotClock--; @@ -127,16 +130,21 @@ void loadPlayer(){ } 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); + if(!gameOver){ + if(player.recoveringClock > 0){ + if(player.recoveringClock % 10 == 1) + SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN); + player.recoveringClock--; + if(player.recoveringClock == 0) + SPR_setVisibility(player.image, VISIBLE); + } + movePlayer(); + boundsPlayer(); + cameraPlayer(); + shootPlayer(); + s16 sx = getScreenX(player.pos.x, player.camera); + s16 sy = fix32ToInt(player.pos.y); + SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF); + intToStr(fix32ToInt(player.pos.x), debugStr, 1); + } } \ No newline at end of file diff --git a/src/sfx.h b/src/sfx.h index 2bb4574..47209c4 100644 --- a/src/sfx.h +++ b/src/sfx.h @@ -1,10 +1,12 @@ static s16 sfxShotClock; static u16 sfxShotFreq; +#define SFX_VOL 2 + void sfxPlayerShot(){ sfxShotClock = 4; sfxShotFreq = 150; - PSG_setEnvelope(2, 2); + PSG_setEnvelope(2, SFX_VOL); PSG_setFrequency(2, sfxShotFreq); } @@ -14,43 +16,56 @@ static u8 sfxEnemyShotType; // high sharp zap - quick descending chirp void sfxEnemyShotA(){ + if(player.recoveringClock > 0) return; sfxEnemyShotClock = 3; sfxEnemyShotFreq = 1200; sfxEnemyShotType = 0; - PSG_setEnvelope(1, 3); + PSG_setEnvelope(1, SFX_VOL); PSG_setFrequency(1, sfxEnemyShotFreq); } // mid buzzy pulse - sits in the midrange void sfxEnemyShotB(){ + if(player.recoveringClock > 0) return; sfxEnemyShotClock = 5; sfxEnemyShotFreq = 400; sfxEnemyShotType = 1; - PSG_setEnvelope(1, 3); + PSG_setEnvelope(1, SFX_VOL); PSG_setFrequency(1, sfxEnemyShotFreq); } // quick rising ping - sweeps upward void sfxEnemyShotC(){ + if(player.recoveringClock > 0) return; sfxEnemyShotClock = 4; sfxEnemyShotFreq = 300; sfxEnemyShotType = 2; - PSG_setEnvelope(1, 3); + PSG_setEnvelope(1, SFX_VOL); PSG_setFrequency(1, sfxEnemyShotFreq); } +static s16 sfxPickupClock; +static u16 sfxPickupFreq; + +void sfxPickup(){ + sfxPickupClock = 12; + sfxPickupFreq = 800; + PSG_setEnvelope(0, SFX_VOL); + PSG_setFrequency(0, sfxPickupFreq); +} + static s16 sfxExpClock; void sfxExplosion(){ sfxExpClock = 18; PSG_setNoise(PSG_NOISE_TYPE_WHITE, PSG_NOISE_FREQ_CLOCK2); - PSG_setEnvelope(3, 0); + PSG_setEnvelope(3, SFX_VOL); } void updateSfx(){ if(sfxExpClock > 0){ sfxExpClock--; - PSG_setEnvelope(3, (18 - sfxExpClock) * 15 / 18); + PSG_setEnvelope(3, SFX_VOL + (18 - sfxExpClock) * (15 - SFX_VOL) / 18); if(sfxExpClock == 0){ PSG_setEnvelope(3, 15); } @@ -61,18 +76,28 @@ void updateSfx(){ else if(sfxEnemyShotType == 1) sfxEnemyShotFreq -= 50; else sfxEnemyShotFreq += 150; PSG_setFrequency(1, sfxEnemyShotFreq); - PSG_setEnvelope(1, 3 + (sfxEnemyShotType == 0 ? (3 - sfxEnemyShotClock) * 4 : - sfxEnemyShotType == 1 ? (5 - sfxEnemyShotClock) * 2 : - (4 - sfxEnemyShotClock) * 3)); + PSG_setEnvelope(1, SFX_VOL + (sfxEnemyShotType == 0 ? (3 - sfxEnemyShotClock) * (15 - SFX_VOL) / 3 : + sfxEnemyShotType == 1 ? (5 - sfxEnemyShotClock) * (15 - SFX_VOL) / 5 : + (4 - sfxEnemyShotClock) * (15 - SFX_VOL) / 4)); if(sfxEnemyShotClock == 0){ PSG_setEnvelope(1, 15); } } + if(sfxPickupClock > 0){ + sfxPickupClock--; + // rising staircase: jump up every 3 frames + if(sfxPickupClock % 3 == 0) sfxPickupFreq += 200; + PSG_setFrequency(0, sfxPickupFreq); + PSG_setEnvelope(0, SFX_VOL); + if(sfxPickupClock == 0){ + PSG_setEnvelope(0, 15); + } + } if(sfxShotClock > 0){ sfxShotClock--; sfxShotFreq -= 30; PSG_setFrequency(2, sfxShotFreq); - PSG_setEnvelope(2, 2 + (4 - sfxShotClock) * 3); + PSG_setEnvelope(2, SFX_VOL + (4 - sfxShotClock) * (15 - SFX_VOL) / 4); if(sfxShotClock == 0){ PSG_setEnvelope(2, 15); } diff --git a/src/stage.h b/src/stage.h index 723bcd3..f808115 100644 --- a/src/stage.h +++ b/src/stage.h @@ -1,9 +1,15 @@ void loadStage(){ - // Spawn 2 enemies per zone (4 zones = 8 total) + // Spawn 3 enemies per zone (4 zones = 12 total) for(u8 zone = 0; zone < 4; zone++){ for(u8 i = 0; i < 3; i++){ spawnEnemy(0, zone); } } + // Spawn 2 humans per zone (4 zones = 8 total) + for(u8 zone = 0; zone < 4; zone++){ + for(u8 i = 0; i < 2; i++){ + spawnHuman(zone); + } + } loadMap(); } \ No newline at end of file