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