cenotaph/src/enemies.h

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