382 lines
No EOL
11 KiB
C
382 lines
No EOL
11 KiB
C
|
|
#include "enemytypes.h"
|
|
|
|
#define ENEMY_MIN_DIST FIX32(64)
|
|
|
|
static bool isValidEnemyPosition(fix32 x, fix32 y) {
|
|
for(s16 j = 0; j < ENEMY_COUNT; j++) {
|
|
if(enemies[j].active) {
|
|
fix32 dx = getWrappedDelta(x, enemies[j].pos.x);
|
|
fix32 dy = y - enemies[j].pos.y;
|
|
// Check if within 64px in both X and Y
|
|
if(dx >= -ENEMY_MIN_DIST && dx <= ENEMY_MIN_DIST &&
|
|
dy >= -ENEMY_MIN_DIST && dy <= ENEMY_MIN_DIST) {
|
|
return FALSE; // Too close to existing enemy
|
|
}
|
|
}
|
|
}
|
|
return TRUE; // Position is valid
|
|
}
|
|
|
|
void spawnEnemy(u8 type, u8 zone){
|
|
// Find available slot, return if none
|
|
s16 i = -1;
|
|
for(s16 j = 0; j < ENEMY_COUNT; j++) if(!enemies[j].active) { i = j; break; }
|
|
if(i == -1) return;
|
|
|
|
enemies[i].active = TRUE;
|
|
enemies[i].type = type;
|
|
|
|
// Calculate zone bounds (each zone is 512px)
|
|
fix32 zoneStart = FIX32(zone * 512);
|
|
fix32 randX, randY, playerDist;
|
|
u16 attempts = 0;
|
|
do {
|
|
// Random X within zone: zoneStart + random(0-511)
|
|
randX = zoneStart + FIX32(random() % 512);
|
|
randY = FIX32(16 + (random() % 128));
|
|
attempts++;
|
|
playerDist = getWrappedDelta(randX, player.pos.x);
|
|
if(playerDist < 0) playerDist = -playerDist;
|
|
} while((playerDist < CULL_LIMIT || !isValidEnemyPosition(randX, randY)) && attempts < 100);
|
|
|
|
enemies[i].pos.x = randX;
|
|
enemies[i].pos.y = randY;
|
|
|
|
// Default sprite — load functions can override via SPR_setDefinition()
|
|
static const SpriteDefinition* bossSpriteDefs[4] = { &boss1Sprite, &boss2Sprite, &boss3Sprite, &boss4Sprite };
|
|
SpriteDefinition const* spriteDef;
|
|
if(type == ENEMY_TYPE_BOSS) spriteDef = bossSpriteDefs[pendingBossNum % 4];
|
|
else spriteDef = &fairySprite;
|
|
|
|
enemies[i].off = (type == ENEMY_TYPE_BOSS) ? 24 : 16;
|
|
enemies[i].image = SPR_addSprite(spriteDef,
|
|
getScreenX(enemies[i].pos.x, player.camera) - enemies[i].off, F32_toInt(enemies[i].pos.y) - enemies[i].off, TILE_ATTR(PAL0, 0, 0, 0));
|
|
if(!enemies[i].image){
|
|
enemies[i].active = FALSE;
|
|
return;
|
|
}
|
|
SPR_setDepth(enemies[i].image, (type == ENEMY_TYPE_BOSS) ? 1 : 2);
|
|
SPR_setVisibility(enemies[i].image, HIDDEN);
|
|
enemies[i].hp = 1;
|
|
for(u8 j = 0; j < PROP_COUNT; j++){
|
|
enemies[i].ints[j] = 0;
|
|
enemies[i].fixes[j] = 0;
|
|
}
|
|
enemies[i].canGrabTreasure = FALSE;
|
|
enemies[i].homesOnPlayer = FALSE;
|
|
enemies[i].canShoot = FALSE;
|
|
enemies[i].canFlipH = FALSE;
|
|
enemies[i].useBigSprite = FALSE;
|
|
enemies[i].carriedTreasure = -1;
|
|
enemies[i].targetTreasure = -1;
|
|
enemies[i].anim = 0;
|
|
switch(enemies[i].type){
|
|
case ENEMY_TYPE_ONE:
|
|
loadEnemyOne(i);
|
|
break;
|
|
case ENEMY_TYPE_TWO:
|
|
loadEnemyTwo(i);
|
|
break;
|
|
case ENEMY_TYPE_THREE:
|
|
loadEnemyThree(i);
|
|
break;
|
|
case ENEMY_TYPE_FOUR:
|
|
loadEnemyFour(i);
|
|
break;
|
|
case ENEMY_TYPE_FIVE:
|
|
loadEnemyFive(i);
|
|
break;
|
|
case ENEMY_TYPE_SIX:
|
|
loadEnemySix(i);
|
|
break;
|
|
case ENEMY_TYPE_SEVEN:
|
|
loadEnemySeven(i);
|
|
break;
|
|
case ENEMY_TYPE_EIGHT:
|
|
loadEnemyEight(i);
|
|
break;
|
|
case ENEMY_TYPE_NINE:
|
|
loadEnemyNine(i);
|
|
break;
|
|
case ENEMY_TYPE_TEN:
|
|
loadEnemyTen(i);
|
|
break;
|
|
case ENEMY_TYPE_ELEVEN:
|
|
loadEnemyEleven(i);
|
|
break;
|
|
case ENEMY_TYPE_TWELVE:
|
|
loadEnemyTwelve(i);
|
|
break;
|
|
case ENEMY_TYPE_THIRTEEN:
|
|
loadEnemyThirteen(i);
|
|
break;
|
|
case ENEMY_TYPE_FOURTEEN:
|
|
loadEnemyFourteen(i);
|
|
break;
|
|
case ENEMY_TYPE_FIFTEEN:
|
|
loadEnemyFifteen(i);
|
|
break;
|
|
case ENEMY_TYPE_SIXTEEN:
|
|
loadEnemySixteen(i);
|
|
break;
|
|
case ENEMY_TYPE_BOSS:
|
|
loadBoss(i);
|
|
break;
|
|
}
|
|
enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
|
|
enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
|
|
SPR_setAnim(enemies[i].image, enemies[i].anim);
|
|
}
|
|
|
|
static void boundsEnemy(u8 i){
|
|
if(enemies[i].canGrabTreasure && enemies[i].carriedTreasure >= 0){
|
|
s16 h = enemies[i].carriedTreasure;
|
|
// if the treasure was collected by player or gone, kill this enemy
|
|
if(!treasures[h].active || treasures[h].state == TREASURE_COLLECTED){
|
|
enemies[i].carriedTreasure = -1;
|
|
treasureBeingCarried = FALSE;
|
|
killEnemy(i);
|
|
return;
|
|
}
|
|
// carrying: only check for reaching the top
|
|
else if(enemies[i].pos.y <= FIX32(0)){
|
|
if(isAttract){
|
|
// in attract mode enemies can't die -- drop treasure and head back down
|
|
if(treasures[h].active){
|
|
treasures[h].state = TREASURE_FALLING;
|
|
treasures[h].carriedBy = -1;
|
|
treasures[h].vel.x = 0;
|
|
treasures[h].vel.y = FIX32(3);
|
|
}
|
|
enemies[i].carriedTreasure = -1;
|
|
treasureBeingCarried = FALSE;
|
|
enemies[i].vel.y = FIX32(1);
|
|
} else {
|
|
if(treasures[h].active) killTreasure(h);
|
|
enemies[i].carriedTreasure = -1;
|
|
treasureBeingCarried = FALSE;
|
|
enemies[i].hp = 0;
|
|
killEnemy(i);
|
|
}
|
|
return;
|
|
}
|
|
} else {
|
|
// not carrying: bounce off top and bottom
|
|
if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off)){
|
|
if(enemies[i].vel.y > 0) enemies[i].vel.y = -enemies[i].vel.y;
|
|
enemies[i].pos.y = GAME_H_F - FIX32(enemies[i].off);
|
|
} else if(enemies[i].pos.y <= FIX32(enemies[i].off)){
|
|
if(enemies[i].vel.y < 0) enemies[i].vel.y = -enemies[i].vel.y;
|
|
enemies[i].pos.y = FIX32(enemies[i].off);
|
|
}
|
|
}
|
|
|
|
if(enemies[i].pos.x >= GAME_WRAP){
|
|
enemies[i].pos.x -= GAME_WRAP;
|
|
}
|
|
if(enemies[i].pos.x < 0){
|
|
enemies[i].pos.x += GAME_WRAP;
|
|
}
|
|
}
|
|
|
|
static void enemySeekTreasure(u8 i){
|
|
// carrying: steer upward
|
|
if(enemies[i].carriedTreasure >= 0){
|
|
enemies[i].angle = FIX16(248 + (random() % 45));
|
|
enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
|
|
enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
|
|
return;
|
|
}
|
|
|
|
// cancel target if a treasure is already being carried
|
|
if(treasureBeingCarried && enemies[i].targetTreasure >= 0){
|
|
enemies[i].targetTreasure = -1;
|
|
}
|
|
|
|
// scan for nearest walking treasure every 30 frames
|
|
if(!treasureBeingCarried && enemies[i].clock % 30 == 0){
|
|
s16 bestTreasure = -1;
|
|
fix32 bestDist = FIX32(9999);
|
|
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;
|
|
bestTreasure = j;
|
|
}
|
|
}
|
|
enemies[i].targetTreasure = bestTreasure;
|
|
}
|
|
|
|
// steer toward target treasure
|
|
if(enemies[i].targetTreasure >= 0){
|
|
s16 t = enemies[i].targetTreasure;
|
|
if(!treasures[t].active || treasures[t].state != TREASURE_WALKING){
|
|
enemies[i].targetTreasure = -1;
|
|
} else {
|
|
fix32 dx = getWrappedDelta(treasures[t].pos.x, enemies[i].pos.x);
|
|
fix32 dy = treasures[t].pos.y - enemies[i].pos.y;
|
|
|
|
fix16 angle = getAngle(dx, dy);
|
|
enemies[i].vel.x = F32_mul(F32_cos(angle), enemies[i].speed);
|
|
enemies[i].vel.y = F32_mul(F32_sin(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].carriedTreasure = t;
|
|
enemies[i].targetTreasure = -1;
|
|
treasureBeingCarried = TRUE;
|
|
treasures[t].state = TREASURE_CARRIED;
|
|
treasures[t].carriedBy = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void updateEnemy(u8 i){
|
|
enemies[i].pos.x += enemies[i].vel.x - (player.vel.x >> 3);
|
|
enemies[i].pos.y += enemies[i].vel.y - (playerScrollVelY >> 3);
|
|
|
|
boundsEnemy(i);
|
|
if(!enemies[i].active) return;
|
|
|
|
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
|
|
enemies[i].onScreen = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
|
|
|
// flag-based treasure seeking (before type-specific update)
|
|
if(enemies[i].canGrabTreasure){
|
|
enemySeekTreasure(i);
|
|
}
|
|
|
|
// flag-based homing
|
|
if(enemies[i].homesOnPlayer){
|
|
enemies[i].angle = enemyHoneAngle(i);
|
|
if(player.respawnClock > 0) enemies[i].angle = F16_normalizeAngle(enemies[i].angle + FIX16(180));
|
|
enemies[i].vel.x = F32_mul(F32_cos(enemies[i].angle), enemies[i].speed);
|
|
enemies[i].vel.y = F32_mul(F32_sin(enemies[i].angle), enemies[i].speed);
|
|
}
|
|
|
|
switch(enemies[i].type){
|
|
case ENEMY_TYPE_ONE:
|
|
updateEnemyOne(i);
|
|
break;
|
|
case ENEMY_TYPE_TWO:
|
|
updateEnemyTwo(i);
|
|
break;
|
|
case ENEMY_TYPE_THREE:
|
|
updateEnemyThree(i);
|
|
break;
|
|
case ENEMY_TYPE_FOUR:
|
|
updateEnemyFour(i);
|
|
break;
|
|
case ENEMY_TYPE_FIVE:
|
|
updateEnemyFive(i);
|
|
break;
|
|
case ENEMY_TYPE_SIX:
|
|
updateEnemySix(i);
|
|
break;
|
|
case ENEMY_TYPE_SEVEN:
|
|
updateEnemySeven(i);
|
|
break;
|
|
case ENEMY_TYPE_EIGHT:
|
|
updateEnemyEight(i);
|
|
break;
|
|
case ENEMY_TYPE_NINE:
|
|
updateEnemyNine(i);
|
|
break;
|
|
case ENEMY_TYPE_TEN:
|
|
updateEnemyTen(i);
|
|
break;
|
|
case ENEMY_TYPE_ELEVEN:
|
|
updateEnemyEleven(i);
|
|
break;
|
|
case ENEMY_TYPE_TWELVE:
|
|
updateEnemyTwelve(i);
|
|
break;
|
|
case ENEMY_TYPE_THIRTEEN:
|
|
updateEnemyThirteen(i);
|
|
break;
|
|
case ENEMY_TYPE_FOURTEEN:
|
|
updateEnemyFourteen(i);
|
|
break;
|
|
case ENEMY_TYPE_FIFTEEN:
|
|
updateEnemyFifteen(i);
|
|
break;
|
|
case ENEMY_TYPE_SIXTEEN:
|
|
updateEnemySixteen(i);
|
|
break;
|
|
case ENEMY_TYPE_BOSS:
|
|
updateBoss(i);
|
|
break;
|
|
}
|
|
|
|
// enemy->player collision (reuses dx from above — position unchanged this frame)
|
|
if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0 && player.respawnClock == 0){
|
|
fix32 edy = enemies[i].pos.y - player.pos.y;
|
|
if(dx >= FIX32(-16) && dx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){
|
|
sfxExplosion();
|
|
// spawn big explosion at player position
|
|
spawnExplosion(player.pos.x, player.pos.y, 3, TRUE); // yellow
|
|
if(enemies[i].type != ENEMY_TYPE_BOSS){
|
|
enemies[i].hp = 0;
|
|
killEnemy(i);
|
|
}
|
|
if(!isAttract){
|
|
if(player.hasShield){
|
|
// shield absorbs hit
|
|
player.hasShield = FALSE;
|
|
player.shieldClock = 0;
|
|
removeShieldVisual();
|
|
player.recoveringClock = 60;
|
|
player.recoverFlash = TRUE;
|
|
killBullets = TRUE;
|
|
hitMessageClock = 120;
|
|
hitMessageBullet = FALSE;
|
|
} else {
|
|
player.lives--;
|
|
if(player.lives == 0){
|
|
gameOver = TRUE;
|
|
XGM2_stop();
|
|
sfxGameOver();
|
|
} else {
|
|
sfxPlayerHit();
|
|
levelPerfect = FALSE;
|
|
player.respawnClock = 120;
|
|
player.activePowerup = 0;
|
|
player.powerupClock = 0;
|
|
player.hasShield = FALSE;
|
|
player.shieldClock = 0;
|
|
removeShieldVisual();
|
|
SPR_setVisibility(player.image, HIDDEN);
|
|
killBullets = TRUE;
|
|
hitMessageClock = 120;
|
|
hitMessageBullet = FALSE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
|
|
s16 sy = F32_toInt(enemies[i].pos.y);
|
|
SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN);
|
|
if(enemies[i].canFlipH)
|
|
SPR_setHFlip(enemies[i].image, enemies[i].vel.x > 0);
|
|
SPR_setPosition(enemies[i].image, sx - enemies[i].off, sy - enemies[i].off);
|
|
|
|
enemies[i].clock++;
|
|
if(enemies[i].clock >= CLOCK_LIMIT) enemies[i].clock = 0;
|
|
enemyCount++;
|
|
}
|
|
|
|
void updateEnemies(){
|
|
enemyCount = 0;
|
|
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active)
|
|
updateEnemy(i);
|
|
// intToStr(enemyCount, debugStr, 1);
|
|
} |