lots of work
BIN
flower.fur
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
res/fadetop.png
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
res/ground.png
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
res/koakuma.png
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
res/pbullet.png
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
|
@ -30,4 +30,6 @@ IMAGE mapIndicator "mapindicator.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
|
||||
IMAGE imageChromeLife2 "life2.png" NONE NONE
|
||||
|
||||
XGM2 stageMusic "stage.vgm"
|
||||
BIN
res/stage.vgm
Normal file
|
|
@ -1,26 +1,86 @@
|
|||
#define BG_I 8
|
||||
// #define FADE_TOP_I BG_I + 64
|
||||
// #define FADE_BOTTOM_I FADE_TOP_I + 64
|
||||
|
||||
#define BG_OFF 24
|
||||
// zone-unique block: 64x64px ground block in sky area, only visible in zone 0
|
||||
// world X=256 = tile col 32, placed in sky row block 1 (tile row 8)
|
||||
#define ZONE_BLOCK_WORLD_X 256
|
||||
#define ZONE_BLOCK_COL ((ZONE_BLOCK_WORLD_X / 8) % 128)
|
||||
#define ZONE_BLOCK_ROW 16
|
||||
bool zoneBlockVisible;
|
||||
fix32 prevCamera;
|
||||
#define PARALLAX_COUNT 8
|
||||
fix32 parallaxAccum[PARALLAX_COUNT];
|
||||
static const fix32 parallaxMul[PARALLAX_COUNT] = {
|
||||
FIX32(0.8), FIX32(0.7), FIX32(0.6), FIX32(0.5), FIX32(0.4), FIX32(0.3), FIX32(0.2), FIX32(0.1)
|
||||
};
|
||||
|
||||
s16 bgScroll[28];
|
||||
|
||||
void loadBackground(){
|
||||
VDP_setScrollingMode(HSCROLL_TILE, VSCROLL_PLANE);
|
||||
VDP_setVerticalScroll(BG_B, 32);
|
||||
VDP_loadTileSet(sky.tileset, BG_I, DMA);
|
||||
VDP_loadTileSet(ground.tileset, BG_I + 64, DMA);
|
||||
// VDP_loadTileSet(fadeTop.tileset, FADE_TOP_I, DMA);
|
||||
// VDP_loadTileSet(fadeBottom.tileset, FADE_BOTTOM_I, DMA);
|
||||
VDP_loadTileSet(fadeBottom.tileset, BG_I + 64 + 64, DMA);
|
||||
VDP_loadTileSet(fadeTop.tileset, BG_I + 64 + 64 + 4, 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 + (y > 2 ? 64 : 0)), x * 8, y * 8, 8, 8);
|
||||
}
|
||||
}
|
||||
// for(u8 x = 0; x < 5; x++){
|
||||
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, FADE_TOP_I), x * 8, 0, 8, 8);
|
||||
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, FADE_BOTTOM_I), x * 8, 20, 8, 8);
|
||||
// }
|
||||
// place 64x64 ground block in sky area (zone 0 only)
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
zoneBlockVisible = TRUE;
|
||||
prevCamera = player.camera;
|
||||
for(u8 i = 0; i < PARALLAX_COUNT; i++)
|
||||
parallaxAccum[i] = fix32Mul(player.camera + FIX32(256), parallaxMul[i]);
|
||||
// write initial scroll values so first frame has correct parallax
|
||||
s16 initScroll = fix32ToInt(-player.camera);
|
||||
for(u8 i = 0; i < 20; i++)
|
||||
bgScroll[i] = initScroll;
|
||||
for(u8 i = 0; i < 8; i++)
|
||||
bgScroll[27 - i] = (initScroll - fix32ToInt(parallaxAccum[i]));
|
||||
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
|
||||
|
||||
// fade
|
||||
// for(u8 x = 0; x < 20; x++)
|
||||
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 + 64), x * 2, 26, 2, 2);
|
||||
// for(u8 x = 0; x < 10; x++)
|
||||
// VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64 + 64 + 4), x * 4, 22, 4, 4);
|
||||
}
|
||||
|
||||
void updateBackground(){
|
||||
VDP_setHorizontalScroll(BG_B, fix32ToInt(-player.camera));
|
||||
VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 3);
|
||||
}
|
||||
s16 scrollVal = fix32ToInt(-player.camera);
|
||||
|
||||
// accumulate parallax from camera delta (not absolute position)
|
||||
// this avoids discontinuities at world wrap boundaries
|
||||
// GAME_WRAP is already fix32, so use directly (no FIX32 wrapper)
|
||||
fix32 delta = player.camera - prevCamera;
|
||||
if(delta > GAME_WRAP / 2) delta -= GAME_WRAP;
|
||||
else if(delta < -(GAME_WRAP / 2)) delta += GAME_WRAP;
|
||||
prevCamera = player.camera;
|
||||
|
||||
// update accumulators once, reuse for top and bottom
|
||||
for(u8 i = 0; i < PARALLAX_COUNT; i++){
|
||||
parallaxAccum[i] += fix32Mul(delta, parallaxMul[i]);
|
||||
if(parallaxAccum[i] > FIX32(1024)) parallaxAccum[i] -= FIX32(1024);
|
||||
else if(parallaxAccum[i] < FIX32(-1024)) parallaxAccum[i] += FIX32(1024);
|
||||
}
|
||||
|
||||
for(u8 i = 0; i < 20; i++)
|
||||
bgScroll[i] = scrollVal;
|
||||
for(u8 i = 0; i < 8; i++)
|
||||
bgScroll[27 - i] = (scrollVal - fix32ToInt(parallaxAccum[i]));
|
||||
|
||||
VDP_setHorizontalScrollTile(BG_B, 0, bgScroll, 28, DMA);
|
||||
|
||||
// show ground block only when zone 0 copy of these columns is on screen
|
||||
fix32 dx = getWrappedDelta(FIX32(ZONE_BLOCK_WORLD_X + 32), player.camera + FIX32(160));
|
||||
bool shouldShow = (dx > FIX32(-212) && dx < FIX32(212));
|
||||
if(shouldShow && !zoneBlockVisible){
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I + 64), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
zoneBlockVisible = TRUE;
|
||||
} else if(!shouldShow && zoneBlockVisible){
|
||||
VDP_fillTileMapRectInc(BG_B, TILE_ATTR_FULL(PAL1, 0, 0, 0, BG_I), ZONE_BLOCK_COL, ZONE_BLOCK_ROW, 8, 8);
|
||||
zoneBlockVisible = FALSE;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,11 +144,17 @@ static void collideWithPlayer(u8 i){
|
|||
fix32ToInt(deltaX),
|
||||
fix32ToInt(deltaY));
|
||||
if(dist <= 4){
|
||||
// convert enemy bullet to player bullet explosion in-place
|
||||
SPR_setDefinition(bullets[i].image, &pBulletSprite);
|
||||
bullets[i].player = TRUE;
|
||||
bullets[i].pos.x = player.pos.x;
|
||||
bullets[i].pos.y = player.pos.y;
|
||||
killBullet(i, TRUE);
|
||||
sfxExplosion();
|
||||
player.lives--;
|
||||
if(player.lives == 0){
|
||||
gameOver = TRUE;
|
||||
XGM2_stop();
|
||||
} else {
|
||||
player.recoveringClock = 120;
|
||||
killBullets = TRUE;
|
||||
|
|
|
|||
33
src/chrome.h
|
|
@ -174,6 +174,15 @@ static void updateMap(){
|
|||
VDP_setTileMapXY(BG_A, MAP_PLAYER_TILE, MAP_X + MAP_W / 2, MAP_Y + pRow);
|
||||
}
|
||||
|
||||
s16 lastLevel;
|
||||
static void drawLevel(){
|
||||
char lvlStr[4];
|
||||
uintToStr(level + 1, lvlStr, 1);
|
||||
VDP_drawText("L", 1, 26);
|
||||
VDP_drawText(lvlStr, 2, 26);
|
||||
lastLevel = level;
|
||||
}
|
||||
|
||||
void loadChrome(){
|
||||
VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA);
|
||||
VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA);
|
||||
|
|
@ -183,6 +192,7 @@ void loadChrome(){
|
|||
lastScore = 1;
|
||||
drawScore();
|
||||
drawLives();
|
||||
drawLevel();
|
||||
}
|
||||
|
||||
bool didGameOver;
|
||||
|
|
@ -191,7 +201,25 @@ 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);
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active){
|
||||
if(humans[i].state == HUMAN_COLLECTED){
|
||||
// spawn player bullet explosion at carried human position
|
||||
struct bulletSpawner spawner = {
|
||||
.x = humans[i].pos.x, .y = humans[i].pos.y,
|
||||
.anim = 0, .speed = 0, .angle = 0, .player = TRUE
|
||||
};
|
||||
void noop(s16 j){ (void)j; }
|
||||
spawnBullet(spawner, noop);
|
||||
for(s16 j = BULLET_COUNT - 1; j >= 0; j--){
|
||||
if(bullets[j].active && !bullets[j].explosion
|
||||
&& bullets[j].pos.x == humans[i].pos.x && bullets[j].pos.y == humans[i].pos.y){
|
||||
killBullet(j, TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
killHuman(i);
|
||||
}
|
||||
SPR_releaseSprite(player.image);
|
||||
// clear minimap
|
||||
VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H);
|
||||
|
|
@ -210,6 +238,7 @@ static void showPause(){
|
|||
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_setPalette(player.image, PAL1);
|
||||
XGM2_pause();
|
||||
VDP_drawText("PAUSE", 17, 13);
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +247,7 @@ static void clearPause(){
|
|||
for(s16 i = 0; i < ENEMY_COUNT; i++) if(enemies[i].active) SPR_setPalette(enemies[i].image, PAL0);
|
||||
for(s16 i = 0; i < HUMAN_COUNT; i++) if(humans[i].active) SPR_setPalette(humans[i].image, PAL0);
|
||||
SPR_setPalette(player.image, PAL0);
|
||||
XGM2_resume();
|
||||
VDP_clearText(17, 13, 5);
|
||||
}
|
||||
|
||||
|
|
@ -276,5 +306,6 @@ void updateChrome(){
|
|||
drawScore();
|
||||
}
|
||||
if(lastLives != player.lives) drawLives();
|
||||
if(lastLevel != level) drawLevel();
|
||||
if(clock % 4 == 0) updateMap();
|
||||
}
|
||||
|
|
@ -49,10 +49,12 @@ void spawnEnemy(u8 type, u8 zone){
|
|||
enemies[i].active = FALSE;
|
||||
return;
|
||||
}
|
||||
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].ints[3] = -1;
|
||||
switch(enemies[i].type){
|
||||
case ENEMY_TYPE_TEST:
|
||||
loadEnemyOne(i);
|
||||
|
|
@ -146,9 +148,31 @@ static void updateEnemy(u8 i){
|
|||
break;
|
||||
}
|
||||
|
||||
// enemy->player collision
|
||||
if(enemies[i].onScreen && !gameOver && player.recoveringClock == 0){
|
||||
fix32 edx = getWrappedDelta(enemies[i].pos.x, player.pos.x);
|
||||
fix32 edy = enemies[i].pos.y - player.pos.y;
|
||||
if(edx >= FIX32(-16) && edx <= FIX32(16) && edy >= FIX32(-16) && edy <= FIX32(16)){
|
||||
sfxExplosion();
|
||||
if(enemies[i].type != ENEMY_TYPE_BOSS){
|
||||
enemies[i].hp = 0;
|
||||
killEnemy(i);
|
||||
}
|
||||
player.lives--;
|
||||
if(player.lives == 0){
|
||||
gameOver = TRUE;
|
||||
XGM2_stop();
|
||||
} else {
|
||||
player.recoveringClock = 120;
|
||||
killBullets = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
|
||||
s16 sy = fix32ToInt(enemies[i].pos.y);
|
||||
SPR_setVisibility(enemies[i].image, enemies[i].onScreen ? VISIBLE : HIDDEN);
|
||||
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++;
|
||||
|
|
|
|||
409
src/enemytypes.h
|
|
@ -1,4 +1,26 @@
|
|||
// test enemy -- for testing out bullet stress
|
||||
// =============================================================================
|
||||
// --- Type 0: Test / Fairy (EnemyOne) ---
|
||||
// =============================================================================
|
||||
// The original enemy type. A fairy that shoots 8-bullet circular bursts and
|
||||
// also seeks/abducts humans (same carry behavior as Builder).
|
||||
//
|
||||
// Behavior:
|
||||
// - When NOT carrying: drifts at speed 2, shoots 8 bullets in a circle
|
||||
// every 20 frames (only when on screen). Also scans for nearby walking
|
||||
// humans every 30 frames and steers toward the closest one within 256px.
|
||||
// Grabs the human when within 16px.
|
||||
// - When carrying: flies upward (angle 704-832, roughly up-left to up-right)
|
||||
// at speed 2. Skips all shooting. boundsEnemy() handles reaching the top
|
||||
// (kills human, self-destructs -- does NOT spawn a Gunner unlike Builder).
|
||||
//
|
||||
// ints[0] = random shot timer offset (0-59), desynchronizes shooting from
|
||||
// other enemies so they don't all fire on the same frame
|
||||
// ints[2] = target human index (-1 = no target)
|
||||
// ints[3] = carried human index (-1 = not carrying)
|
||||
//
|
||||
// Speed: 2 HP: 1 Shoots: yes (8-bullet radial, every 20 frames)
|
||||
// Abducts: yes (only 1 human globally at a time via humanBeingCarried flag)
|
||||
// =============================================================================
|
||||
void loadEnemyOne(u8 i){
|
||||
enemies[i].ints[0] = random() % 60;
|
||||
enemies[i].ints[2] = -1; // target human index
|
||||
|
|
@ -90,9 +112,28 @@ void updateEnemyOne(u8 i){
|
|||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// --- Type 1: Drone ---
|
||||
// Pressure enemy. Homes toward player, simple aimed shots.
|
||||
// ints[0] = random shot offset, ints[1] = recalc timer
|
||||
// =============================================================================
|
||||
// Bread-and-butter pressure enemy. Periodically recalculates heading toward
|
||||
// the player and fires single aimed bullets. The main "fodder" type -- use
|
||||
// high counts (8-16) to create constant movement pressure without overwhelming
|
||||
// bullet density.
|
||||
//
|
||||
// Behavior:
|
||||
// - Recalculates heading toward player every 30 frames (ints[1] counter).
|
||||
// Between recalcs, travels in a straight line at speed 2.
|
||||
// - Fires 1 aimed bullet at player every 40 frames. Only shoots when:
|
||||
// a) on screen, AND b) level index >= 1 (i.e. L2+, so L1 drones are
|
||||
// harmless). This is hardcoded, separate from LevelDef.dronesShoot.
|
||||
// - Bounces off top/bottom screen edges.
|
||||
//
|
||||
// ints[0] = random shot timer offset (0-59), prevents synchronized volleys
|
||||
// ints[1] = heading recalculation timer (counts up to 30)
|
||||
//
|
||||
// Speed: 2 HP: 1 Shoots: yes (1 aimed bullet, every 40 frames, L2+ only)
|
||||
// Abducts: no
|
||||
// =============================================================================
|
||||
void loadDrone(u8 i){
|
||||
enemies[i].ints[0] = random() % 60;
|
||||
enemies[i].ints[1] = 0;
|
||||
|
|
@ -132,9 +173,32 @@ void updateDrone(u8 i){
|
|||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// --- 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
|
||||
// =============================================================================
|
||||
// Danmaku / bullet geometry enemy. Drifts very slowly (speed 0.5), acting more
|
||||
// like a turret than a chaser. Only fires when on screen. The bullet pattern is
|
||||
// set by LevelDef.gunnerPattern (written to ints[0] by loadLevel).
|
||||
//
|
||||
// Behavior:
|
||||
// - Drifts at speed 0.5 in a random initial direction. Bounces off top/bottom.
|
||||
// - Does nothing when off screen (early return).
|
||||
// - Pattern 0 (Radial Burst): fires 8 bullets in a circle every 60 frames.
|
||||
// Start angle rotates by 24 each volley, creating a spiral-over-time effect.
|
||||
// Bullet speed 3. Predictable, good for learning dodge patterns.
|
||||
// - Pattern 1 (Aimed Fan): fires 5 bullets aimed at player, spread across
|
||||
// +-64 angle units, every 45 frames. Bullet speed 3. More aggressive and
|
||||
// targeted -- harder to dodge at close range.
|
||||
// - Pattern is set per-gunner at level load. gunnerPattern=2 means each
|
||||
// gunner randomly picks 0 or 1, creating mixed bullet fields.
|
||||
//
|
||||
// ints[0] = pattern type (0=radial, 1=aimed fan). Set by loadLevel, not random.
|
||||
// ints[1] = random shot timer offset (0-59)
|
||||
// ints[2] = angle accumulator (radial pattern only, rotates start angle)
|
||||
//
|
||||
// Speed: 0.5 HP: 1 Shoots: yes (pattern-dependent, see above)
|
||||
// Abducts: no
|
||||
// =============================================================================
|
||||
void loadGunner(u8 i){
|
||||
enemies[i].ints[0] = random() % 2;
|
||||
enemies[i].ints[1] = random() % 60;
|
||||
|
|
@ -187,8 +251,30 @@ void updateGunner(u8 i){
|
|||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// --- Type 3: Hunter ---
|
||||
// Fast chaser. Homes toward player every frame. No shooting.
|
||||
// =============================================================================
|
||||
// Fast, relentless chaser. Homes toward the player EVERY frame with speed 5
|
||||
// (matching player's normal speed). Never shoots -- pure body-collision threat.
|
||||
// Forces the player to keep moving and use focus mode / diagonal movement to
|
||||
// outrun. Very dangerous in groups.
|
||||
//
|
||||
// Behavior:
|
||||
// - Recalculates angle toward player every frame (no delay like Drone).
|
||||
// - Moves at speed 5 (player normal speed = 6, focus = 3.5).
|
||||
// - Bounces off top/bottom screen edges.
|
||||
// - No shooting, no abduction. Just chases.
|
||||
//
|
||||
// No custom ints used.
|
||||
//
|
||||
// Speed: 5 HP: 1 Shoots: no Abducts: no
|
||||
//
|
||||
// Design notes:
|
||||
// - 2-3 hunters alongside gunners is very hard (dodge bullets while fleeing)
|
||||
// - 4+ hunters with no gunners is a pure movement/positioning challenge
|
||||
// - 6 hunters is near-impossible -- only for late-game punishment
|
||||
// - Hunters are the anti-camping enemy: you can't sit still
|
||||
// =============================================================================
|
||||
void loadHunter(u8 i){
|
||||
enemies[i].angle = random() % 1024;
|
||||
enemies[i].speed = FIX32(5);
|
||||
|
|
@ -203,9 +289,39 @@ void updateHunter(u8 i){
|
|||
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
|
||||
// =============================================================================
|
||||
// Human abductor. Drifts slowly, scans for walking humans, grabs one, and
|
||||
// flies upward. If it reaches the top of the screen with a human, the human
|
||||
// is killed and a Gunner spawns at that position -- punishing the player for
|
||||
// not intercepting. Only 1 human can be globally carried at a time
|
||||
// (humanBeingCarried flag).
|
||||
//
|
||||
// Behavior:
|
||||
// - When NOT carrying: drifts at speed 0.7. Scans for nearest walking human
|
||||
// within 256px every 30 frames. If a target is found, switches to seeking
|
||||
// speed (1.4) and homes toward it. Grabs when within 16px.
|
||||
// - When carrying: flies upward (angle 704-832) at speed 1.4.
|
||||
// boundsEnemy() handles reaching the top: kills the human, spawns a Gunner
|
||||
// at the builder's position, then self-destructs.
|
||||
// - If the carried human gets collected by the player while being carried,
|
||||
// boundsEnemy() detects this and kills the builder (enemy dies, human safe).
|
||||
// - Cancels its target if another enemy is already carrying a human.
|
||||
// - No shooting at all.
|
||||
//
|
||||
// ints[0] = random scan timer offset (0-59)
|
||||
// ints[2] = target human index (-1 = no target)
|
||||
// ints[3] = carried human index (-1 = not carrying)
|
||||
//
|
||||
// Speed: 0.7 (drift), 1.4 (seeking/carrying) HP: 1 Shoots: no
|
||||
// Abducts: yes (kills human + spawns Gunner if it reaches the top)
|
||||
//
|
||||
// Design notes:
|
||||
// - 1 builder adds light urgency. 2 builders is stressful.
|
||||
// - Pairs well with drones to split the player's attention.
|
||||
// - The Gunner spawn on success makes ignoring builders snowball badly.
|
||||
// =============================================================================
|
||||
void loadBuilder(u8 i){
|
||||
enemies[i].ints[0] = random() % 60;
|
||||
enemies[i].ints[2] = -1;
|
||||
|
|
@ -274,62 +390,243 @@ void updateBuilder(u8 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
|
||||
// =============================================================================
|
||||
// High-HP enemy with phase-based attack patterns. Boss number is set via
|
||||
// pendingBossNum global before spawn (because spawnEnemy zeroes all ints[]).
|
||||
// HP is set via pendingBossHp. Both are auto-configured by loadLevel() based
|
||||
// on the level's bossHp field and level index.
|
||||
//
|
||||
// Behavior:
|
||||
// - Speed 1, bounces off top/bottom. Only attacks when on screen.
|
||||
// - Attack pattern changes based on remaining HP. HP range is divided into
|
||||
// N equal phases. Phase 0 = full health, highest phase = near death.
|
||||
// Later bosses have more phases and higher HP, so they cycle through
|
||||
// more varied and increasingly aggressive patterns.
|
||||
// - Each boss (1-5) has a unique updateBoss function with different pattern
|
||||
// selections and timings per phase.
|
||||
//
|
||||
// ints[0] = boss number (0-4), selects which updateBoss variant runs
|
||||
// ints[4] = max HP (stored at load for phase calculation)
|
||||
//
|
||||
// Boss 1 (L4): 25 HP, 2 phases -- radial, then aimed fan
|
||||
// Boss 2 (L8): 50 HP, 3 phases -- radial, aimed fan, spiral
|
||||
// Boss 3 (L12): 75 HP, 4 phases -- radial, spiral, aimed fan, double radial
|
||||
// Boss 4 (L16): 100 HP, 5 phases -- adds wide spray
|
||||
// Boss 5 (L20): 125 HP, 6 phases -- adds ring burst (all patterns used)
|
||||
//
|
||||
// Available boss attack patterns:
|
||||
// Radial: N bullets in even circle, random start angle. Predictable.
|
||||
// Aimed Fan: N bullets in arc aimed at player. Targeted.
|
||||
// Spiral: N bullets in circle, start angle rotates with clock. Mesmerizing.
|
||||
// Double Radial: Two offset rings (outer fast, inner slow). Dense.
|
||||
// Wide Spray: N bullets in random spread toward player. Chaotic.
|
||||
// Ring Burst: N bullets in even circle (like radial but different anim). Climactic.
|
||||
//
|
||||
// Speed: 1 HP: varies Shoots: yes (phase-dependent) Abducts: no
|
||||
// =============================================================================
|
||||
|
||||
// shared pattern functions
|
||||
static void bossPatternRadial(u8 i, u8 count, fix32 speed){
|
||||
sfxEnemyShotB();
|
||||
s16 baseAngle = random() % 1024;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
.anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle,
|
||||
};
|
||||
s16 step = 1024 / count;
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawnBullet(spawner, EMPTY);
|
||||
spawner.angle += step;
|
||||
}
|
||||
}
|
||||
|
||||
static void bossPatternAimedFan(u8 i, u8 count, s16 spread, fix32 speed){
|
||||
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));
|
||||
s16 step = (count > 1) ? (spread * 2) / (count - 1) : 0;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
.anim = 9 + (random() % 3), .speed = speed, .angle = aimAngle - spread,
|
||||
};
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawnBullet(spawner, EMPTY);
|
||||
spawner.angle += step;
|
||||
}
|
||||
}
|
||||
|
||||
static void bossPatternSpiral(u8 i, u8 count, fix32 speed){
|
||||
sfxEnemyShotC();
|
||||
s16 baseAngle = (enemies[i].clock * 17) % 1024;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
.anim = 6 + (random() % 3), .speed = speed, .angle = baseAngle,
|
||||
};
|
||||
s16 step = 1024 / count;
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawnBullet(spawner, EMPTY);
|
||||
spawner.angle += step;
|
||||
}
|
||||
}
|
||||
|
||||
static void bossPatternDoubleRadial(u8 i, u8 count, fix32 speed){
|
||||
sfxEnemyShotB();
|
||||
s16 baseAngle = random() % 1024;
|
||||
s16 step = 1024 / count;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
.anim = 3 + (random() % 3), .speed = speed, .angle = baseAngle,
|
||||
};
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawnBullet(spawner, EMPTY);
|
||||
spawner.angle += step;
|
||||
}
|
||||
spawner.angle = baseAngle + step / 2;
|
||||
spawner.speed = speed - FIX32(1);
|
||||
spawner.anim = 6 + (random() % 3);
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawnBullet(spawner, EMPTY);
|
||||
spawner.angle += step;
|
||||
}
|
||||
}
|
||||
|
||||
static void bossPatternWideSpray(u8 i, u8 count, fix32 speed){
|
||||
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 = 3 + (random() % 9), .speed = speed, .angle = aimAngle,
|
||||
};
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawner.angle = aimAngle - 128 + (random() % 256);
|
||||
spawner.speed = speed - FIX32(random() % 2);
|
||||
spawnBullet(spawner, EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
static void bossPatternRingBurst(u8 i, u8 count, fix32 speed){
|
||||
sfxEnemyShotB();
|
||||
sfxEnemyShotC();
|
||||
s16 baseAngle = random() % 1024;
|
||||
s16 step = 1024 / count;
|
||||
struct bulletSpawner spawner = {
|
||||
.x = enemies[i].pos.x, .y = enemies[i].pos.y,
|
||||
.anim = 9 + (random() % 3), .speed = speed, .angle = baseAngle,
|
||||
};
|
||||
for(u8 j = 0; j < count; j++){
|
||||
spawnBullet(spawner, EMPTY);
|
||||
spawner.angle += step;
|
||||
}
|
||||
}
|
||||
|
||||
// get current phase based on HP. phase 0 = full health, higher = more damaged
|
||||
static u8 getBossPhase(u8 i, u8 numPhases){
|
||||
s16 maxHp = enemies[i].ints[4];
|
||||
if(maxHp <= 0) return 0;
|
||||
s16 lost = maxHp - enemies[i].hp;
|
||||
if(lost < 0) lost = 0;
|
||||
u8 phase = (lost * numPhases) / maxHp;
|
||||
if(phase >= numPhases) phase = numPhases - 1;
|
||||
return phase;
|
||||
}
|
||||
|
||||
void loadBoss(u8 i){
|
||||
enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 10;
|
||||
enemies[i].hp = pendingBossHp > 0 ? pendingBossHp : 25;
|
||||
pendingBossHp = 0;
|
||||
enemies[i].ints[0] = pendingBossNum;
|
||||
pendingBossNum = 0;
|
||||
enemies[i].ints[1] = 0;
|
||||
enemies[i].ints[2] = 0;
|
||||
enemies[i].ints[4] = enemies[i].hp;
|
||||
enemies[i].angle = random() % 1024;
|
||||
enemies[i].speed = FIX32(1);
|
||||
}
|
||||
|
||||
// Boss 1 (L6): 2 patterns, 25 HP
|
||||
static void updateBossOne(u8 i){
|
||||
u8 phase = getBossPhase(i, 2);
|
||||
if(phase == 0){
|
||||
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 10, FIX32(3));
|
||||
} else {
|
||||
if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 6, 80, FIX32(3));
|
||||
}
|
||||
}
|
||||
|
||||
// Boss 2 (L12): 3 patterns, 50 HP
|
||||
static void updateBossTwo(u8 i){
|
||||
u8 phase = getBossPhase(i, 3);
|
||||
if(phase == 0){
|
||||
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3));
|
||||
} else if(phase == 1){
|
||||
if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3));
|
||||
} else {
|
||||
if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 6, FIX32(4));
|
||||
}
|
||||
}
|
||||
|
||||
// Boss 3 (L18): 4 patterns, 75 HP
|
||||
static void updateBossThree(u8 i){
|
||||
u8 phase = getBossPhase(i, 4);
|
||||
if(phase == 0){
|
||||
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 12, FIX32(3));
|
||||
} else if(phase == 1){
|
||||
if(enemies[i].clock % 40 == 0) bossPatternSpiral(i, 8, FIX32(3));
|
||||
} else if(phase == 2){
|
||||
if(enemies[i].clock % 35 == 0) bossPatternAimedFan(i, 10, 112, FIX32(4));
|
||||
} else {
|
||||
if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 8, FIX32(4));
|
||||
}
|
||||
}
|
||||
|
||||
// Boss 4 (L24): 5 patterns, 100 HP
|
||||
static void updateBossFour(u8 i){
|
||||
u8 phase = getBossPhase(i, 5);
|
||||
if(phase == 0){
|
||||
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 14, FIX32(3));
|
||||
} else if(phase == 1){
|
||||
if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 8, 96, FIX32(3));
|
||||
} else if(phase == 2){
|
||||
if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 8, FIX32(4));
|
||||
} else if(phase == 3){
|
||||
if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4));
|
||||
} else {
|
||||
if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 12, FIX32(4));
|
||||
}
|
||||
}
|
||||
|
||||
// Boss 5 (L30): 6 patterns, 125 HP
|
||||
static void updateBossFive(u8 i){
|
||||
u8 phase = getBossPhase(i, 6);
|
||||
if(phase == 0){
|
||||
if(enemies[i].clock % 50 == 0) bossPatternRadial(i, 16, FIX32(3));
|
||||
} else if(phase == 1){
|
||||
if(enemies[i].clock % 40 == 0) bossPatternAimedFan(i, 10, 112, FIX32(3));
|
||||
} else if(phase == 2){
|
||||
if(enemies[i].clock % 35 == 0) bossPatternSpiral(i, 10, FIX32(4));
|
||||
} else if(phase == 3){
|
||||
if(enemies[i].clock % 30 == 0) bossPatternDoubleRadial(i, 10, FIX32(4));
|
||||
} else if(phase == 4){
|
||||
if(enemies[i].clock % 25 == 0) bossPatternWideSpray(i, 14, FIX32(5));
|
||||
} else {
|
||||
if(enemies[i].clock % 20 == 0) bossPatternRingBurst(i, 16, FIX32(5));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
switch(enemies[i].ints[0]){
|
||||
case 0: updateBossOne(i); break;
|
||||
case 1: updateBossTwo(i); break;
|
||||
case 2: updateBossThree(i); break;
|
||||
case 3: updateBossFour(i); break;
|
||||
case 4: updateBossFive(i); break;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ u32 clock;
|
|||
|
||||
#define CULL_LIMIT FIX32(240)
|
||||
|
||||
// #define MUSIC_VOLUME 50
|
||||
#define MUSIC_VOLUME 0
|
||||
|
||||
u32 score;
|
||||
#define SCORE_LENGTH 8
|
||||
|
||||
|
|
@ -29,6 +32,7 @@ bool paused, isPausing;
|
|||
s16 enemyCount, bulletCount;
|
||||
u8 level;
|
||||
s16 pendingBossHp;
|
||||
s16 pendingBossNum;
|
||||
bool waitForRelease;
|
||||
bool levelClearing;
|
||||
u32 levelClearClock;
|
||||
|
|
|
|||
10
src/humans.h
|
|
@ -25,6 +25,7 @@ void spawnHuman(u8 zone){
|
|||
humans[i].active = FALSE;
|
||||
return;
|
||||
}
|
||||
SPR_setVisibility(humans[i].image, HIDDEN);
|
||||
}
|
||||
|
||||
static void updateHuman(u8 i){
|
||||
|
|
@ -116,9 +117,16 @@ static void updateHuman(u8 i){
|
|||
|
||||
s16 sx = getScreenX(humans[i].pos.x, player.camera);
|
||||
s16 sy = fix32ToInt(humans[i].pos.y);
|
||||
bool visible = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
||||
bool visible = (humans[i].state == HUMAN_COLLECTED) || (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
|
||||
SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN);
|
||||
SPR_setPosition(humans[i].image, sx - HUMAN_OFF, sy - HUMAN_OFF);
|
||||
// manually animate flash only when walking or falling
|
||||
if(humans[i].state == HUMAN_WALKING || humans[i].state == HUMAN_FALLING)
|
||||
SPR_setFrame(humans[i].image, (clock / 15) & 1);
|
||||
else if(humans[i].state == HUMAN_COLLECTED)
|
||||
SPR_setAnim(humans[i].image, 1);
|
||||
else
|
||||
SPR_setFrame(humans[i].image, 0);
|
||||
}
|
||||
|
||||
void updateHumans(){
|
||||
|
|
|
|||
12
src/main.c
|
|
@ -43,6 +43,11 @@ void loadGame(){
|
|||
loadPlayer();
|
||||
loadChrome();
|
||||
loadLevel(0);
|
||||
XGM2_play(stageMusic);
|
||||
XGM2_setFMVolume(MUSIC_VOLUME);
|
||||
XGM2_setPSGVolume(MUSIC_VOLUME);
|
||||
player.recoveringClock = 120;
|
||||
killBullets = TRUE;
|
||||
started = TRUE;
|
||||
}
|
||||
|
||||
|
|
@ -59,25 +64,30 @@ static void updateGame(){
|
|||
loadBackground();
|
||||
loadChrome();
|
||||
loadLevel(level + 1);
|
||||
XGM2_play(stageMusic);
|
||||
SPR_setVisibility(player.image, VISIBLE);
|
||||
player.recoveringClock = 120;
|
||||
killBullets = TRUE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!paused){
|
||||
updatePlayer();
|
||||
updateBackground();
|
||||
if(clock % 2 == 0){
|
||||
updateEnemies();
|
||||
if(!gameOver && enemyCount == 0){
|
||||
if(level >= LEVEL_COUNT - 1){
|
||||
gameOver = TRUE;
|
||||
XGM2_stop();
|
||||
} else {
|
||||
levelClearing = TRUE;
|
||||
levelClearClock = 0;
|
||||
XGM2_stop();
|
||||
}
|
||||
}
|
||||
updateHumans();
|
||||
} else {
|
||||
updateBackground();
|
||||
updateBullets();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#define PLAYER_SPEED FIX32(5)
|
||||
#define PLAYER_SPEED FIX32(6)
|
||||
|
||||
#define PLAYER_SPEED_FOCUS FIX32(3)
|
||||
#define PLAYER_SPEED_FOCUS FIX32(3.5)
|
||||
|
||||
#define PLAYER_ACCEL PLAYER_SPEED >> 3
|
||||
#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 3
|
||||
#define PLAYER_ACCEL PLAYER_SPEED >> 4
|
||||
#define PLAYER_ACCEL_FOCUS PLAYER_SPEED_FOCUS >> 4
|
||||
|
||||
#define PLAYER_OFF 24
|
||||
#define PLAYER_BOUND_Y FIX32(PLAYER_OFF)
|
||||
|
|
|
|||
159
src/stage.h
|
|
@ -1,55 +1,137 @@
|
|||
// =============================================================================
|
||||
// LEVEL DESIGN GUIDE
|
||||
// =============================================================================
|
||||
//
|
||||
// Each level is a single struct defining what spawns. The level ends when all
|
||||
// enemies are killed (enemyCount == 0). Humans are bonus -- they don't affect
|
||||
// level completion.
|
||||
//
|
||||
// --- STRUCT FIELDS ---
|
||||
//
|
||||
// drones Number of Drone enemies (type 1). Bulk pressure enemy.
|
||||
// Speed 2, homes toward player every 30 frames.
|
||||
// Fires 1 aimed bullet every 40 frames (only on L2+, i.e. index >= 1).
|
||||
// Use dronesShoot=FALSE on L1 to introduce them without bullets.
|
||||
// Good range: 6-16. Above 14 gets chaotic.
|
||||
//
|
||||
// gunners Number of Gunner enemies (type 2). Danmaku / bullet geometry.
|
||||
// Speed 0.5 (slow drift), only shoots when on screen.
|
||||
// Pattern controlled by gunnerPattern field (see below).
|
||||
// Good range: 0-6. Even 2-3 gunners create significant bullet density.
|
||||
//
|
||||
// hunters Number of Hunter enemies (type 3). Fast chaser, no shooting.
|
||||
// Speed 5 (matches player!). Homes every frame. Pure body pressure.
|
||||
// Very dangerous -- 2-3 is oppressive, 6 is near-impossible.
|
||||
// Good range: 0-6. Introduce after players learn movement.
|
||||
//
|
||||
// builders Number of Builder enemies (type 4). Human abductor.
|
||||
// Speed 0.7 (drift), 1.4 (seeking/carrying). Grabs walking humans
|
||||
// and flies upward. If it reaches the top, the human dies and a
|
||||
// Gunner spawns in its place. Only 1 human can be carried at a time.
|
||||
// Creates urgency -- player must choose between killing enemies
|
||||
// and saving humans. Good range: 0-2.
|
||||
//
|
||||
// bossHp If > 0, spawns a Boss enemy (type 5) with this many HP.
|
||||
// Boss number is auto-calculated from level index (lvl / 4).
|
||||
// Set to 0 for non-boss levels. Boss speed is 1, bounces around.
|
||||
// Boss has multiple attack phases based on remaining HP.
|
||||
// Typical values: 25, 50, 75, 100, 125.
|
||||
// Other enemies can coexist with the boss (adds pressure).
|
||||
//
|
||||
// humans Number of humans (koakuma) to spawn. Distributed across 4 zones
|
||||
// (2 per zone if >= 4 humans, then 1 each for remainder).
|
||||
// Walk along the ground, can be collected by player for 1000 pts
|
||||
// (2000 if caught mid-fall after enemy drops them).
|
||||
// Max 8 (HUMAN_COUNT). Usually just set to 8.
|
||||
//
|
||||
// gunnerPattern Controls what bullet pattern gunners use:
|
||||
// 0 = Radial Burst: 8 bullets in a circle every 60 frames.
|
||||
// Rotating start angle. Steady, predictable pressure.
|
||||
// 1 = Aimed Fan: 5 bullets aimed at player, spread +-64,
|
||||
// every 45 frames. More targeted/aggressive.
|
||||
// 2 = Mix: each gunner randomly picks 0 or 1 at spawn.
|
||||
// Creates varied, less predictable bullet fields.
|
||||
//
|
||||
// dronesShoot TRUE = drones fire aimed bullets (normal behavior on L2+).
|
||||
// FALSE = drones still home toward player but never shoot.
|
||||
// Only meaningful for the very first level as a gentle intro.
|
||||
// (Drone shooting is also gated by level >= 1 in code, so
|
||||
// L1 drones never shoot regardless of this flag.)
|
||||
//
|
||||
// --- LIMITS ---
|
||||
//
|
||||
// Total enemies: 24 slots (ENEMY_COUNT). drones+gunners+hunters+builders+boss
|
||||
// must not exceed 24. If it does, excess enemies silently fail to spawn.
|
||||
//
|
||||
// Total humans: 8 slots (HUMAN_COUNT).
|
||||
//
|
||||
// Bullet slots: 70. Heavy gunner/boss levels can fill this up. Player bullets
|
||||
// get priority and evict enemy bullets when full.
|
||||
//
|
||||
// --- SPAWNING ---
|
||||
//
|
||||
// Enemies are distributed across 4 zones (each 512px of the 2048px world).
|
||||
// Enemy i spawns in zone (i % 4). They never spawn within 240px of the player
|
||||
// and maintain 64px minimum spacing from each other.
|
||||
//
|
||||
// Boss always spawns in zone 1.
|
||||
//
|
||||
// --- DESIGN TIPS ---
|
||||
//
|
||||
// - Drone-heavy levels (12-16) create constant movement pressure
|
||||
// - Gunner-heavy levels (4-6) create bullet reading / dodging challenges
|
||||
// - Hunter levels force the player to keep moving (anti-camping)
|
||||
// - Builder levels force tough choices: kill builders or save humans?
|
||||
// - Combining hunters + gunners is very hard (dodge bullets while fleeing)
|
||||
// - Boss levels with escort enemies (drones/gunners alongside boss) are
|
||||
// harder than solo boss fights
|
||||
// - A "farm" level (lots of drones, no gunners) gives score-building breathers
|
||||
// - gunnerPattern 0 (radial) is easier to dodge than 1 (aimed fan)
|
||||
//
|
||||
// =============================================================================
|
||||
|
||||
struct LevelDef {
|
||||
u8 drones, gunners, hunters, builders;
|
||||
u8 bossHp;
|
||||
u8 humans;
|
||||
u8 gunnerPattern; // 0=radial, 1=aimed fan, 2=mix
|
||||
u8 gunnerPattern;
|
||||
bool dronesShoot;
|
||||
};
|
||||
|
||||
// dr gn hn bl boss hum pat shoot
|
||||
const struct LevelDef levels[30] = {
|
||||
// Phase 1: "Immediate danger" (L1-L6)
|
||||
// dr gn hn bl boss hum pat shoot
|
||||
const struct LevelDef levels[20] = {
|
||||
// Phase 1: "Immediate danger" (L1-L4)
|
||||
{ 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
|
||||
{ 12, 3, 0, 0, 0, 8, 1, TRUE }, // L3
|
||||
{ 8, 0, 0, 0, 25, 8, 0, TRUE }, // L4 BOSS 1
|
||||
|
||||
// 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 2: "You can't save everything" (L5-L8)
|
||||
{ 10, 2, 0, 1, 0, 8, 0, TRUE }, // L5
|
||||
{ 14, 3, 0, 1, 0, 8, 1, TRUE }, // L6
|
||||
{ 10, 2, 0, 2, 0, 8, 2, TRUE }, // L7
|
||||
{ 8, 0, 0, 1, 50, 8, 0, TRUE }, // L8 BOSS 2
|
||||
|
||||
// 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 3: "Geometry matters" (L9-L12)
|
||||
{ 8, 3, 4, 0, 0, 8, 1, TRUE }, // L9
|
||||
{ 10, 2, 4, 0, 0, 8, 2, TRUE }, // L10
|
||||
{ 12, 3, 3, 0, 0, 8, 1, TRUE }, // L11
|
||||
{ 0, 2, 2, 0, 75, 8, 2, TRUE }, // L12 BOSS 3
|
||||
|
||||
// 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 4: "Suffocation" (L13-L16)
|
||||
{ 14, 4, 0, 2, 0, 8, 2, TRUE }, // L13
|
||||
{ 10, 0, 6, 0, 0, 8, 0, TRUE }, // L14
|
||||
{ 12, 4, 2, 0, 0, 8, 1, TRUE }, // L15
|
||||
{ 0, 3, 0, 1, 100, 8, 2, TRUE }, // L16 BOSS 4
|
||||
|
||||
// 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
|
||||
// Phase 5: "Arcade cruelty" (L17-L20)
|
||||
{ 16, 0, 4, 0, 0, 8, 0, TRUE }, // L17
|
||||
{ 14, 4, 4, 2, 0, 8, 2, TRUE }, // L18
|
||||
{ 6, 2, 2, 1, 50, 8, 2, TRUE }, // L19 MINI-BOSS
|
||||
{ 4, 2, 2, 1, 125, 8, 2, TRUE }, // L20 BOSS 5 FINAL
|
||||
};
|
||||
|
||||
#define LEVEL_COUNT 30
|
||||
#define LEVEL_COUNT 20
|
||||
|
||||
static void distributeEnemies(u8 type, u8 count){
|
||||
for(u8 i = 0; i < count; i++){
|
||||
|
|
@ -80,6 +162,9 @@ void loadLevel(u8 lvl){
|
|||
|
||||
if(def->bossHp > 0){
|
||||
pendingBossHp = def->bossHp;
|
||||
pendingBossNum = lvl / 4; // L3=0, L7=1, L11=2, L15=3, L18+=4
|
||||
if(pendingBossNum > 4) pendingBossNum = 4;
|
||||
if(lvl == 18) pendingBossNum = 1; // L19 mini-boss reuses boss 2
|
||||
spawnEnemy(ENEMY_TYPE_BOSS, 1);
|
||||
}
|
||||
|
||||
|
|
|
|||