level design etc
This commit is contained in:
parent
0151304d05
commit
4036b5f07e
13 changed files with 460 additions and 21 deletions
BIN
flower.fur
Normal file
BIN
flower.fur
Normal file
Binary file not shown.
BIN
res/human.png
BIN
res/human.png
Binary file not shown.
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
res/koakuma.png
Normal file
BIN
res/koakuma.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
|
|
@ -21,6 +21,7 @@ SPRITE butterflySprite "butterfly.png" 4 4 NONE 8
|
||||||
SPRITE fairySprite "fairy2.png" 4 4 NONE 8
|
SPRITE fairySprite "fairy2.png" 4 4 NONE 8
|
||||||
SPRITE aliceSprite "alice.png" 6 6 NONE 0
|
SPRITE aliceSprite "alice.png" 6 6 NONE 0
|
||||||
SPRITE humanSprite "human.png" 2 2 NONE 0
|
SPRITE humanSprite "human.png" 2 2 NONE 0
|
||||||
|
SPRITE koaSprite "koakuma.png" 4 4 NONE 0
|
||||||
|
|
||||||
// IMAGE mapPlayer "mapplayer.png" NONE NONE
|
// IMAGE mapPlayer "mapplayer.png" NONE NONE
|
||||||
IMAGE mapIndicator "mapindicator.png" NONE NONE
|
IMAGE mapIndicator "mapindicator.png" NONE NONE
|
||||||
|
|
|
||||||
13
src/chrome.h
13
src/chrome.h
|
|
@ -258,6 +258,19 @@ void updateChrome(){
|
||||||
SYS_hardReset();
|
SYS_hardReset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// level transition overlay
|
||||||
|
if(levelClearing){
|
||||||
|
if(levelClearClock == 2){
|
||||||
|
char lvlStr[4];
|
||||||
|
uintToStr(level + 2, lvlStr, 1);
|
||||||
|
VDP_drawText("LEVEL ", 15, 13);
|
||||||
|
VDP_drawText(lvlStr, 21, 13);
|
||||||
|
}
|
||||||
|
if(levelClearClock >= 110){
|
||||||
|
VDP_clearText(15, 13, 10);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(lastScore != score){
|
if(lastScore != score){
|
||||||
lastScore = score;
|
lastScore = score;
|
||||||
drawScore();
|
drawScore();
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,16 @@ void spawnEnemy(u8 type, u8 zone){
|
||||||
|
|
||||||
// Calculate zone bounds (each zone is 512px)
|
// Calculate zone bounds (each zone is 512px)
|
||||||
fix32 zoneStart = FIX32(zone * 512);
|
fix32 zoneStart = FIX32(zone * 512);
|
||||||
fix32 randX, randY;
|
fix32 randX, randY, playerDist;
|
||||||
u16 attempts = 0;
|
u16 attempts = 0;
|
||||||
do {
|
do {
|
||||||
// Random X within zone: zoneStart + random(0-511)
|
// Random X within zone: zoneStart + random(0-511)
|
||||||
randX = zoneStart + FIX32(random() % 512);
|
randX = zoneStart + FIX32(random() % 512);
|
||||||
randY = FIX32(16 + (random() % 128));
|
randY = FIX32(16 + (random() % 128));
|
||||||
attempts++;
|
attempts++;
|
||||||
} while(!isValidEnemyPosition(randX, randY) && attempts < 100);
|
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.x = randX;
|
||||||
enemies[i].pos.y = randY;
|
enemies[i].pos.y = randY;
|
||||||
|
|
@ -47,13 +49,29 @@ void spawnEnemy(u8 type, u8 zone){
|
||||||
enemies[i].active = FALSE;
|
enemies[i].active = FALSE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
enemies[i].hp = 1;
|
||||||
for(u8 j = 0; j < PROP_COUNT; j++){
|
for(u8 j = 0; j < PROP_COUNT; j++){
|
||||||
enemies[i].ints[j] = 0;
|
enemies[i].ints[j] = 0;
|
||||||
}
|
}
|
||||||
switch(enemies[i].type){
|
switch(enemies[i].type){
|
||||||
case 0:
|
case ENEMY_TYPE_TEST:
|
||||||
loadEnemyOne(i);
|
loadEnemyOne(i);
|
||||||
break;
|
break;
|
||||||
|
case ENEMY_TYPE_DRONE:
|
||||||
|
loadDrone(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_GUNNER:
|
||||||
|
loadGunner(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_HUNTER:
|
||||||
|
loadHunter(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_BUILDER:
|
||||||
|
loadBuilder(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_BOSS:
|
||||||
|
loadBoss(i);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(enemies[i].angle)), enemies[i].speed);
|
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);
|
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(enemies[i].angle)), enemies[i].speed);
|
||||||
|
|
@ -61,7 +79,7 @@ void spawnEnemy(u8 type, u8 zone){
|
||||||
}
|
}
|
||||||
|
|
||||||
static void boundsEnemy(u8 i){
|
static void boundsEnemy(u8 i){
|
||||||
if(enemies[i].ints[3] >= 0){
|
if((enemies[i].type == ENEMY_TYPE_TEST || enemies[i].type == ENEMY_TYPE_BUILDER) && enemies[i].ints[3] >= 0){
|
||||||
s16 h = enemies[i].ints[3];
|
s16 h = enemies[i].ints[3];
|
||||||
// if the human was collected by player or gone, kill this enemy
|
// if the human was collected by player or gone, kill this enemy
|
||||||
if(!humans[h].active || humans[h].state == HUMAN_COLLECTED){
|
if(!humans[h].active || humans[h].state == HUMAN_COLLECTED){
|
||||||
|
|
@ -75,7 +93,11 @@ static void boundsEnemy(u8 i){
|
||||||
if(humans[h].active) killHuman(h);
|
if(humans[h].active) killHuman(h);
|
||||||
enemies[i].ints[3] = -1;
|
enemies[i].ints[3] = -1;
|
||||||
humanBeingCarried = FALSE;
|
humanBeingCarried = FALSE;
|
||||||
// TODO: spawn mutant here
|
if(enemies[i].type == ENEMY_TYPE_BUILDER){
|
||||||
|
u8 zone = fix32ToInt(enemies[i].pos.x) / 512;
|
||||||
|
spawnEnemy(ENEMY_TYPE_GUNNER, zone);
|
||||||
|
}
|
||||||
|
enemies[i].hp = 0;
|
||||||
killEnemy(i);
|
killEnemy(i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -104,9 +126,24 @@ static void updateEnemy(u8 i){
|
||||||
enemies[i].onScreen = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
enemies[i].onScreen = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
||||||
|
|
||||||
switch(enemies[i].type){
|
switch(enemies[i].type){
|
||||||
case 0:
|
case ENEMY_TYPE_TEST:
|
||||||
updateEnemyOne(i);
|
updateEnemyOne(i);
|
||||||
break;
|
break;
|
||||||
|
case ENEMY_TYPE_DRONE:
|
||||||
|
updateDrone(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_GUNNER:
|
||||||
|
updateGunner(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_HUNTER:
|
||||||
|
updateHunter(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_BUILDER:
|
||||||
|
updateBuilder(i);
|
||||||
|
break;
|
||||||
|
case ENEMY_TYPE_BOSS:
|
||||||
|
updateBoss(i);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
|
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
|
||||||
|
|
|
||||||
246
src/enemytypes.h
246
src/enemytypes.h
|
|
@ -1,3 +1,4 @@
|
||||||
|
// test enemy -- for testing out bullet stress
|
||||||
void loadEnemyOne(u8 i){
|
void loadEnemyOne(u8 i){
|
||||||
enemies[i].ints[0] = random() % 60;
|
enemies[i].ints[0] = random() % 60;
|
||||||
enemies[i].ints[2] = -1; // target human index
|
enemies[i].ints[2] = -1; // target human index
|
||||||
|
|
@ -5,7 +6,6 @@ void loadEnemyOne(u8 i){
|
||||||
enemies[i].angle = ((random() % 4) * 256) + 128;
|
enemies[i].angle = ((random() % 4) * 256) + 128;
|
||||||
enemies[i].speed = FIX32(2);
|
enemies[i].speed = FIX32(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateEnemyOne(u8 i){
|
void updateEnemyOne(u8 i){
|
||||||
// carrying behavior: move upward, skip shooting
|
// carrying behavior: move upward, skip shooting
|
||||||
if(enemies[i].ints[3] >= 0){
|
if(enemies[i].ints[3] >= 0){
|
||||||
|
|
@ -89,3 +89,247 @@ void updateEnemyOne(u8 i){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Type 1: Drone ---
|
||||||
|
// Pressure enemy. Homes toward player, simple aimed shots.
|
||||||
|
// ints[0] = random shot offset, ints[1] = recalc timer
|
||||||
|
void loadDrone(u8 i){
|
||||||
|
enemies[i].ints[0] = random() % 60;
|
||||||
|
enemies[i].ints[1] = 0;
|
||||||
|
enemies[i].angle = random() % 1024;
|
||||||
|
enemies[i].speed = FIX32(2);
|
||||||
|
}
|
||||||
|
void updateDrone(u8 i){
|
||||||
|
// recalculate heading toward player every 30 frames
|
||||||
|
enemies[i].ints[1]++;
|
||||||
|
if(enemies[i].ints[1] >= 30){
|
||||||
|
enemies[i].ints[1] = 0;
|
||||||
|
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
|
||||||
|
fix32 dy = player.pos.y - enemies[i].pos.y;
|
||||||
|
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));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
// shooting: 1 aimed bullet every ~40 frames (only if level >= 1 i.e. L2+)
|
||||||
|
if(level >= 1 && enemies[i].onScreen &&
|
||||||
|
enemies[i].clock % 40 == (u32)(enemies[i].ints[0]) % 40){
|
||||||
|
sfxEnemyShotC();
|
||||||
|
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
|
||||||
|
fix32 dy = player.pos.y - enemies[i].pos.y;
|
||||||
|
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));
|
||||||
|
struct bulletSpawner spawner = {
|
||||||
|
.x = enemies[i].pos.x,
|
||||||
|
.y = enemies[i].pos.y,
|
||||||
|
.anim = 3 + (random() % 9),
|
||||||
|
.speed = FIX32(4),
|
||||||
|
.angle = aimAngle,
|
||||||
|
};
|
||||||
|
spawnBullet(spawner, EMPTY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Type 2: Gunner ---
|
||||||
|
// Bullet geometry. Slow drift, patterned danmaku.
|
||||||
|
// ints[0] = pattern type (0=radial, 1=aimed fan), ints[1] = shot timer offset, ints[2] = angle accumulator
|
||||||
|
void loadGunner(u8 i){
|
||||||
|
enemies[i].ints[0] = random() % 2;
|
||||||
|
enemies[i].ints[1] = random() % 60;
|
||||||
|
enemies[i].ints[2] = 0;
|
||||||
|
enemies[i].angle = random() % 1024;
|
||||||
|
enemies[i].speed = FIX32(0.5);
|
||||||
|
}
|
||||||
|
void updateGunner(u8 i){
|
||||||
|
if(!enemies[i].onScreen) return;
|
||||||
|
if(enemies[i].ints[0] == 0){
|
||||||
|
// Pattern 0: Radial Burst - 8 bullets every ~60 frames
|
||||||
|
if(enemies[i].clock % 60 == (u32)(enemies[i].ints[1]) % 60){
|
||||||
|
sfxEnemyShotB();
|
||||||
|
s16 baseAngle = enemies[i].ints[2];
|
||||||
|
struct bulletSpawner spawner = {
|
||||||
|
.x = enemies[i].pos.x,
|
||||||
|
.y = enemies[i].pos.y,
|
||||||
|
.anim = 3 + (random() % 3),
|
||||||
|
.speed = FIX32(3),
|
||||||
|
.angle = baseAngle,
|
||||||
|
};
|
||||||
|
for(u8 j = 0; j < 8; j++){
|
||||||
|
spawnBullet(spawner, EMPTY);
|
||||||
|
spawner.angle += 128;
|
||||||
|
}
|
||||||
|
enemies[i].ints[2] += 24;
|
||||||
|
if(enemies[i].ints[2] >= 1024) enemies[i].ints[2] -= 1024;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pattern 1: Aimed Fan - 5 bullets spread +-64 every ~45 frames
|
||||||
|
if(enemies[i].clock % 45 == (u32)(enemies[i].ints[1]) % 45){
|
||||||
|
sfxEnemyShotA();
|
||||||
|
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
|
||||||
|
fix32 dy = player.pos.y - enemies[i].pos.y;
|
||||||
|
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));
|
||||||
|
struct bulletSpawner spawner = {
|
||||||
|
.x = enemies[i].pos.x,
|
||||||
|
.y = enemies[i].pos.y,
|
||||||
|
.anim = 6 + (random() % 3),
|
||||||
|
.speed = FIX32(3),
|
||||||
|
.angle = aimAngle - 64,
|
||||||
|
};
|
||||||
|
for(u8 j = 0; j < 5; j++){
|
||||||
|
spawnBullet(spawner, EMPTY);
|
||||||
|
spawner.angle += 32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Type 3: Hunter ---
|
||||||
|
// Fast chaser. Homes toward player every frame. No shooting.
|
||||||
|
void loadHunter(u8 i){
|
||||||
|
enemies[i].angle = random() % 1024;
|
||||||
|
enemies[i].speed = FIX32(5);
|
||||||
|
}
|
||||||
|
void updateHunter(u8 i){
|
||||||
|
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
|
||||||
|
fix32 dy = player.pos.y - enemies[i].pos.y;
|
||||||
|
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));
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Type 4: Builder (Abductor) ---
|
||||||
|
// Seeks and abducts humans. On reaching top with human, spawns a Gunner.
|
||||||
|
// ints[0] = scan offset, ints[2] = target human, ints[3] = carried human
|
||||||
|
void loadBuilder(u8 i){
|
||||||
|
enemies[i].ints[0] = random() % 60;
|
||||||
|
enemies[i].ints[2] = -1;
|
||||||
|
enemies[i].ints[3] = -1;
|
||||||
|
enemies[i].angle = random() % 1024;
|
||||||
|
enemies[i].speed = FIX32(0.7);
|
||||||
|
}
|
||||||
|
void updateBuilder(u8 i){
|
||||||
|
// carrying: steer upward
|
||||||
|
if(enemies[i].ints[3] >= 0){
|
||||||
|
enemies[i].angle = 704 + (random() % 128);
|
||||||
|
enemies[i].speed = FIX32(1.4);
|
||||||
|
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 target if a human is already being carried
|
||||||
|
if(humanBeingCarried && 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;
|
||||||
|
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;
|
||||||
|
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));
|
||||||
|
enemies[i].vel.x = fix32Mul(fix16ToFix32(cosFix16(angle)), enemies[i].speed);
|
||||||
|
enemies[i].vel.y = fix32Mul(fix16ToFix32(sinFix16(angle)), enemies[i].speed);
|
||||||
|
|
||||||
|
// grab check
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Type 5: Boss ---
|
||||||
|
// High HP, alternates 2 patterns. hp set by level data via ints[0].
|
||||||
|
// ints[0] = initial hp (set by stage), ints[1] = pattern timer, ints[2] = current pattern
|
||||||
|
void loadBoss(u8 i){
|
||||||
|
enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 10;
|
||||||
|
pendingBossHp = 0;
|
||||||
|
enemies[i].ints[1] = 0;
|
||||||
|
enemies[i].ints[2] = 0;
|
||||||
|
enemies[i].angle = random() % 1024;
|
||||||
|
enemies[i].speed = FIX32(1);
|
||||||
|
}
|
||||||
|
void updateBoss(u8 i){
|
||||||
|
if(!enemies[i].onScreen) return;
|
||||||
|
enemies[i].ints[1]++;
|
||||||
|
// alternate patterns every 180 frames
|
||||||
|
if(enemies[i].ints[1] >= 180){
|
||||||
|
enemies[i].ints[1] = 0;
|
||||||
|
enemies[i].ints[2] = 1 - enemies[i].ints[2];
|
||||||
|
}
|
||||||
|
if(enemies[i].ints[2] == 0){
|
||||||
|
// Pattern A: Radial burst - 12 bullets every 50 frames
|
||||||
|
if(enemies[i].ints[1] % 50 == 0){
|
||||||
|
sfxEnemyShotB();
|
||||||
|
s16 baseAngle = random() % 1024;
|
||||||
|
struct bulletSpawner spawner = {
|
||||||
|
.x = enemies[i].pos.x,
|
||||||
|
.y = enemies[i].pos.y,
|
||||||
|
.anim = 3 + (random() % 3),
|
||||||
|
.speed = FIX32(3),
|
||||||
|
.angle = baseAngle,
|
||||||
|
};
|
||||||
|
for(u8 j = 0; j < 12; j++){
|
||||||
|
spawnBullet(spawner, EMPTY);
|
||||||
|
spawner.angle += 85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pattern B: Aimed wide fan - 8 bullets every 40 frames
|
||||||
|
if(enemies[i].ints[1] % 40 == 0){
|
||||||
|
sfxEnemyShotA();
|
||||||
|
fix32 dx = getWrappedDelta(player.pos.x, enemies[i].pos.x);
|
||||||
|
fix32 dy = player.pos.y - enemies[i].pos.y;
|
||||||
|
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));
|
||||||
|
struct bulletSpawner spawner = {
|
||||||
|
.x = enemies[i].pos.x,
|
||||||
|
.y = enemies[i].pos.y,
|
||||||
|
.anim = 9 + (random() % 3),
|
||||||
|
.speed = FIX32(3),
|
||||||
|
.angle = aimAngle - 112,
|
||||||
|
};
|
||||||
|
for(u8 j = 0; j < 8; j++){
|
||||||
|
spawnBullet(spawner, EMPTY);
|
||||||
|
spawner.angle += 32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/global.h
19
src/global.h
|
|
@ -1,6 +1,6 @@
|
||||||
u32 clock;
|
u32 clock;
|
||||||
#define CLOCK_LIMIT 32000
|
#define CLOCK_LIMIT 32000
|
||||||
#define PROP_COUNT 4
|
#define PROP_COUNT 8
|
||||||
|
|
||||||
#define GAME_H_F FIX32(224)
|
#define GAME_H_F FIX32(224)
|
||||||
|
|
||||||
|
|
@ -27,6 +27,11 @@ bool started;
|
||||||
bool gameOver;
|
bool gameOver;
|
||||||
bool paused, isPausing;
|
bool paused, isPausing;
|
||||||
s16 enemyCount, bulletCount;
|
s16 enemyCount, bulletCount;
|
||||||
|
u8 level;
|
||||||
|
s16 pendingBossHp;
|
||||||
|
bool waitForRelease;
|
||||||
|
bool levelClearing;
|
||||||
|
u32 levelClearClock;
|
||||||
|
|
||||||
// controls
|
// controls
|
||||||
struct controls {
|
struct controls {
|
||||||
|
|
@ -81,11 +86,19 @@ struct bullet bullets[BULLET_COUNT];
|
||||||
|
|
||||||
|
|
||||||
// enemies
|
// enemies
|
||||||
#define ENEMY_COUNT 16
|
#define ENEMY_COUNT 24
|
||||||
|
|
||||||
|
#define ENEMY_TYPE_TEST 0
|
||||||
|
#define ENEMY_TYPE_DRONE 1
|
||||||
|
#define ENEMY_TYPE_GUNNER 2
|
||||||
|
#define ENEMY_TYPE_HUNTER 3
|
||||||
|
#define ENEMY_TYPE_BUILDER 4
|
||||||
|
#define ENEMY_TYPE_BOSS 5
|
||||||
|
|
||||||
struct enemy {
|
struct enemy {
|
||||||
bool active, onScreen;
|
bool active, onScreen;
|
||||||
u8 type;
|
u8 type;
|
||||||
|
s16 hp;
|
||||||
s16 angle, off;
|
s16 angle, off;
|
||||||
u32 clock;
|
u32 clock;
|
||||||
fix32 speed;
|
fix32 speed;
|
||||||
|
|
@ -151,6 +164,8 @@ void killBullet(u8 i, bool explode){
|
||||||
}
|
}
|
||||||
|
|
||||||
void killEnemy(u8 i){
|
void killEnemy(u8 i){
|
||||||
|
enemies[i].hp--;
|
||||||
|
if(enemies[i].hp > 0) return;
|
||||||
if(enemies[i].ints[3] >= 0){
|
if(enemies[i].ints[3] >= 0){
|
||||||
s16 h = enemies[i].ints[3];
|
s16 h = enemies[i].ints[3];
|
||||||
if(humans[h].active){
|
if(humans[h].active){
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ static void updateHuman(u8 i){
|
||||||
fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x);
|
fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x);
|
||||||
if(humans[i].state != HUMAN_CARRIED && humans[i].state != HUMAN_COLLECTED){
|
if(humans[i].state != HUMAN_CARRIED && humans[i].state != HUMAN_COLLECTED){
|
||||||
fix32 dy = humans[i].pos.y - player.pos.y;
|
fix32 dy = humans[i].pos.y - player.pos.y;
|
||||||
if(dx >= FIX32(-24) && dx <= FIX32(24) && dy >= FIX32(-24) && dy <= FIX32(24)){
|
if(dx >= FIX32(-32) && dx <= FIX32(32) && dy >= FIX32(-32) && dy <= FIX32(32)){
|
||||||
score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000;
|
score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000;
|
||||||
sfxPickup();
|
sfxPickup();
|
||||||
humans[i].state = HUMAN_COLLECTED;
|
humans[i].state = HUMAN_COLLECTED;
|
||||||
|
|
|
||||||
40
src/main.c
40
src/main.c
|
|
@ -23,22 +23,58 @@ static void loadInternals(){
|
||||||
VDP_setTextPriority(1);
|
VDP_setTextPriority(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearLevel(){
|
||||||
|
for(s16 i = 0; i < BULLET_COUNT; i++)
|
||||||
|
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;
|
||||||
|
collectedCount = 0;
|
||||||
|
// black out everything
|
||||||
|
SPR_setVisibility(player.image, HIDDEN);
|
||||||
|
VDP_clearTileMapRect(BG_A, 0, 0, 128, 32);
|
||||||
|
VDP_clearTileMapRect(BG_B, 0, 0, 128, 32);
|
||||||
|
}
|
||||||
|
|
||||||
void loadGame(){
|
void loadGame(){
|
||||||
loadBackground();
|
loadBackground();
|
||||||
loadPlayer();
|
loadPlayer();
|
||||||
loadChrome();
|
loadChrome();
|
||||||
loadStage();
|
loadLevel(0);
|
||||||
started = TRUE;
|
started = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void updateGame(){
|
static void updateGame(){
|
||||||
updateChrome();
|
updateChrome();
|
||||||
updateSfx();
|
updateSfx();
|
||||||
|
if(levelClearing){
|
||||||
|
levelClearClock++;
|
||||||
|
if(levelClearClock == 1){
|
||||||
|
clearLevel();
|
||||||
|
}
|
||||||
|
if(levelClearClock >= 120){
|
||||||
|
levelClearing = FALSE;
|
||||||
|
loadBackground();
|
||||||
|
loadChrome();
|
||||||
|
loadLevel(level + 1);
|
||||||
|
SPR_setVisibility(player.image, VISIBLE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!paused){
|
if(!paused){
|
||||||
updatePlayer();
|
updatePlayer();
|
||||||
if(clock % 2 == 0){
|
if(clock % 2 == 0){
|
||||||
updateEnemies();
|
updateEnemies();
|
||||||
if(!gameOver && enemyCount == 0) gameOver = TRUE;
|
if(!gameOver && enemyCount == 0){
|
||||||
|
if(level >= LEVEL_COUNT - 1){
|
||||||
|
gameOver = TRUE;
|
||||||
|
} else {
|
||||||
|
levelClearing = TRUE;
|
||||||
|
levelClearClock = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
updateHumans();
|
updateHumans();
|
||||||
} else {
|
} else {
|
||||||
updateBackground();
|
updateBackground();
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,11 @@ void loadPlayer(){
|
||||||
}
|
}
|
||||||
|
|
||||||
void updatePlayer(){
|
void updatePlayer(){
|
||||||
|
if(waitForRelease){
|
||||||
|
if(!ctrl.a && !ctrl.b && !ctrl.c && !ctrl.start)
|
||||||
|
waitForRelease = FALSE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!gameOver){
|
if(!gameOver){
|
||||||
if(player.recoveringClock > 0){
|
if(player.recoveringClock > 0){
|
||||||
if(player.recoveringClock % 10 == 1)
|
if(player.recoveringClock % 10 == 1)
|
||||||
|
|
|
||||||
103
src/stage.h
103
src/stage.h
|
|
@ -1,15 +1,102 @@
|
||||||
void loadStage(){
|
struct LevelDef {
|
||||||
// Spawn 3 enemies per zone (4 zones = 12 total)
|
u8 drones, gunners, hunters, builders;
|
||||||
for(u8 zone = 0; zone < 4; zone++){
|
u8 bossHp;
|
||||||
for(u8 i = 0; i < 3; i++){
|
u8 humans;
|
||||||
spawnEnemy(0, zone);
|
u8 gunnerPattern; // 0=radial, 1=aimed fan, 2=mix
|
||||||
|
bool dronesShoot;
|
||||||
|
};
|
||||||
|
|
||||||
|
// dr gn hn bl boss hum pat shoot
|
||||||
|
const struct LevelDef levels[30] = {
|
||||||
|
// Phase 1: "Immediate danger" (L1-L6)
|
||||||
|
{ 8, 1, 0, 0, 0, 8, 0, FALSE }, // L1
|
||||||
|
{ 10, 2, 0, 0, 0, 8, 0, TRUE }, // L2
|
||||||
|
{ 12, 2, 0, 0, 0, 8, 0, TRUE }, // L3
|
||||||
|
{ 10, 3, 0, 0, 0, 8, 1, TRUE }, // L4
|
||||||
|
{ 14, 3, 0, 0, 0, 8, 1, TRUE }, // L5
|
||||||
|
{ 8, 0, 0, 0, 8, 8, 0, TRUE }, // L6 BOSS
|
||||||
|
|
||||||
|
// Phase 2: "You can't save everything" (L7-L12)
|
||||||
|
{ 10, 0, 0, 1, 0, 8, 0, TRUE }, // L7
|
||||||
|
{ 10, 2, 0, 1, 0, 8, 0, TRUE }, // L8
|
||||||
|
{ 12, 0, 0, 2, 0, 8, 0, TRUE }, // L9
|
||||||
|
{ 14, 3, 0, 1, 0, 8, 1, TRUE }, // L10 WALL
|
||||||
|
{ 10, 2, 0, 2, 0, 8, 2, TRUE }, // L11
|
||||||
|
{ 8, 0, 0, 1, 12, 8, 0, TRUE }, // L12 BOSS
|
||||||
|
|
||||||
|
// Phase 3: "Geometry matters" (L13-L18)
|
||||||
|
{ 8, 0, 4, 0, 0, 8, 0, TRUE }, // L13
|
||||||
|
{ 8, 3, 2, 0, 0, 8, 1, TRUE }, // L14
|
||||||
|
{ 16, 0, 0, 0, 0, 8, 0, TRUE }, // L15 FARM
|
||||||
|
{ 10, 2, 4, 0, 0, 8, 2, TRUE }, // L16
|
||||||
|
{ 12, 3, 3, 0, 0, 8, 1, TRUE }, // L17
|
||||||
|
{ 0, 2, 2, 0, 15, 8, 2, TRUE }, // L18 BOSS
|
||||||
|
|
||||||
|
// Phase 4: "Suffocation" (L19-L24)
|
||||||
|
{ 12, 4, 0, 0, 0, 8, 2, TRUE }, // L19
|
||||||
|
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L20 WALL
|
||||||
|
{ 10, 0, 6, 0, 0, 8, 0, TRUE }, // L21
|
||||||
|
{ 12, 4, 2, 0, 0, 8, 1, TRUE }, // L22
|
||||||
|
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L23
|
||||||
|
{ 0, 3, 0, 1, 20, 8, 2, TRUE }, // L24 BOSS
|
||||||
|
|
||||||
|
// Phase 5: "Arcade cruelty" (L25-L30)
|
||||||
|
{ 16, 0, 4, 0, 0, 8, 0, TRUE }, // L25
|
||||||
|
{ 12, 6, 0, 0, 0, 8, 2, TRUE }, // L26
|
||||||
|
{ 14, 2, 4, 0, 0, 8, 1, TRUE }, // L27
|
||||||
|
{ 16, 4, 0, 2, 0, 8, 2, TRUE }, // L28
|
||||||
|
{ 6, 2, 2, 1, 10, 8, 2, TRUE }, // L29 MINI-BOSS
|
||||||
|
{ 4, 2, 2, 1, 30, 8, 2, TRUE }, // L30 FINAL
|
||||||
|
};
|
||||||
|
|
||||||
|
#define LEVEL_COUNT 30
|
||||||
|
|
||||||
|
static void distributeEnemies(u8 type, u8 count){
|
||||||
|
for(u8 i = 0; i < count; i++){
|
||||||
|
u8 zone = i % 4;
|
||||||
|
spawnEnemy(type, zone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadLevel(u8 lvl){
|
||||||
|
if(lvl >= LEVEL_COUNT) lvl = LEVEL_COUNT - 1;
|
||||||
|
level = lvl;
|
||||||
|
const struct LevelDef* def = &levels[lvl];
|
||||||
|
|
||||||
|
distributeEnemies(ENEMY_TYPE_DRONE, def->drones);
|
||||||
|
distributeEnemies(ENEMY_TYPE_GUNNER, def->gunners);
|
||||||
|
distributeEnemies(ENEMY_TYPE_HUNTER, def->hunters);
|
||||||
|
distributeEnemies(ENEMY_TYPE_BUILDER, def->builders);
|
||||||
|
|
||||||
|
// set gunner pattern based on level def
|
||||||
|
for(s16 i = 0; i < ENEMY_COUNT; i++){
|
||||||
|
if(enemies[i].active && enemies[i].type == ENEMY_TYPE_GUNNER){
|
||||||
|
if(def->gunnerPattern == 2)
|
||||||
|
enemies[i].ints[0] = random() % 2;
|
||||||
|
else
|
||||||
|
enemies[i].ints[0] = def->gunnerPattern;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Spawn 2 humans per zone (4 zones = 8 total)
|
|
||||||
for(u8 zone = 0; zone < 4; zone++){
|
if(def->bossHp > 0){
|
||||||
for(u8 i = 0; i < 2; i++){
|
pendingBossHp = def->bossHp;
|
||||||
|
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);
|
spawnHuman(zone);
|
||||||
|
humansToSpawn--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMap();
|
loadMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy test stage
|
||||||
|
void loadStage(){
|
||||||
|
loadLevel(0);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ void loadStart(){
|
||||||
void updateStart(){
|
void updateStart(){
|
||||||
if(ctrl.a || ctrl.b || ctrl.c || ctrl.start){
|
if(ctrl.a || ctrl.b || ctrl.c || ctrl.start){
|
||||||
VDP_clearTileMapRect(BG_A, 0, 0, 40, 28);
|
VDP_clearTileMapRect(BG_A, 0, 0, 40, 28);
|
||||||
|
waitForRelease = TRUE;
|
||||||
loadGame();
|
loadGame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue