This commit is contained in:
t. boddy 2026-02-16 17:00:35 -05:00
parent 1702a06d9f
commit 364a34ce33
20 changed files with 453 additions and 64 deletions

BIN
res/fontbig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
res/fontbigshadow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

BIN
res/human.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

BIN
res/life.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
res/life2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -20,7 +20,13 @@ SPRITE butterflySprite "butterfly.png" 4 4 NONE 8
// SPRITE fairySprite "fairy.png" 4 4 NONE 8
SPRITE fairySprite "fairy2.png" 4 4 NONE 8
SPRITE aliceSprite "alice.png" 6 6 NONE 0
SPRITE humanSprite "human.png" 2 2 NONE 0
// IMAGE mapPlayer "mapplayer.png" NONE NONE
IMAGE mapIndicator "mapindicator.png" NONE NONE
// IMAGE mapFrame "mapframe.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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

View file

@ -2,7 +2,7 @@
// #define FADE_TOP_I BG_I + 64
// #define FADE_BOTTOM_I FADE_TOP_I + 64
#define BG_OFF 112
#define BG_OFF 24
void loadBackground(){
VDP_loadTileSet(sky.tileset, BG_I, DMA);
@ -11,7 +11,7 @@ void loadBackground(){
// VDP_loadTileSet(fadeBottom.tileset, FADE_BOTTOM_I, 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), x * 8, y * 8, 8, 8);
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++){
@ -22,5 +22,5 @@ void loadBackground(){
void updateBackground(){
VDP_setHorizontalScroll(BG_B, fix32ToInt(-player.camera));
VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 2);
VDP_setVerticalScroll(BG_B, (fix32ToInt(player.pos.y) - BG_OFF) >> 3);
}

View file

@ -57,6 +57,7 @@ static void doBulletRotation(u8 i){
}
void spawnBullet(struct bulletSpawner spawner, void(*updater)){
if(player.recoveringClock > 0 && !spawner.player) return;
// Don't spawn if offscreen
fix32 dx = getWrappedDelta(spawner.x, player.pos.x);
bool offScreenX = (dx < -CULL_LIMIT || dx > CULL_LIMIT);
@ -124,6 +125,7 @@ static void collideWithEnemy(u8 i){
deltaX >= -BULLET_CHECK && deltaX <= BULLET_CHECK){
bulletDist = getApproximatedDistance(fix32ToInt(deltaX), fix32ToInt(deltaY));
if(bulletDist <= bullets[i].dist){
score += (enemies[j].ints[3] >= 0) ? 200 : 100;
killBullet(i, TRUE);
killEnemy(j);
sfxExplosion();
@ -134,6 +136,7 @@ static void collideWithEnemy(u8 i){
}
static void collideWithPlayer(u8 i){
if(player.recoveringClock > 0) return;
fix32 deltaX = getWrappedDelta(bullets[i].pos.x, player.pos.x);
fix32 deltaY = bullets[i].pos.y - player.pos.y;
@ -143,8 +146,13 @@ static void collideWithPlayer(u8 i){
if(dist <= 4){
killBullet(i, TRUE);
sfxExplosion();
// player.lives--;
// if(player.lives <= 0) gameOver = TRUE;
player.lives--;
if(player.lives == 0){
gameOver = TRUE;
} else {
player.recoveringClock = 120;
killBullets = TRUE;
}
}
}
@ -195,7 +203,7 @@ static void updateBullet(u8 i){
}
if(bullets[i].clock > 0) bullets[i].updater(i);
if(bullets[i].player) collideWithEnemy(i);
else collideWithPlayer(i);
else if(!gameOver) collideWithPlayer(i);
if(bullets[i].active){
s16 sx = getScreenX(bullets[i].pos.x, player.camera);
s16 sy = fix32ToInt(bullets[i].pos.y);
@ -209,6 +217,11 @@ static void updateBullet(u8 i){
void updateBullets(){
bulletCount = 0;
if(killBullets){
killBullets = FALSE;
for(s16 i = 0; i < BULLET_COUNT; i++)
if(bullets[i].active && !bullets[i].player) killBullet(i, TRUE);
}
for(s16 i = 0; i < BULLET_COUNT; i++) if(bullets[i].active)
updateBullet(i);
// intToStr(bulletCount, debugStr, 1);

View file

@ -2,18 +2,55 @@
#define MAP_TILE TILE_ATTR_FULL(PAL1, 1, 0, 0, MAP_I)
#define MAP_PLAYER_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 1)
#define MAP_ENEMY_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 2)
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3)
#define MAP_HUMAN_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 3)
#define MAP_BORDER_X_TILE TILE_ATTR_FULL(PAL0, 1, 0, 0, MAP_I + 4)
#define FONT_BIG_I 256
void bigText(char* str, u16 x, u16 y, bool shadow){
for(u8 i = 0; i < strlen(str); i++){
if(str[i] >= 48){
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + str[i] - 48), x + i, y);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, (shadow ? 32 : 0) + FONT_BIG_I + 16 + str[i] - 48), x + i, y + 1);
}
}
}
char scoreStr[SCORE_LENGTH];
u32 lastScore;
s16 scoreLength;
#define SCORE_X 1
#define SCORE_Y 5
#define LIFE_I (FONT_BIG_I + 64)
#define LIVES_X 38
#define LIVES_Y 5
s16 lastLives;
static void drawLives(){
VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16);
for(u8 i = 0; i < (player.lives - 1); i++)
VDP_fillTileMapRectInc(BG_A, TILE_ATTR_FULL(PAL0, 1, 0, 0, LIFE_I + (i > 0 ? 2 : 0)), LIVES_X, LIVES_Y + i, 1, 2);
lastLives = player.lives;
}
// previous map positions: -1 means not drawn
s16 mapEnemyCol[ENEMY_COUNT], mapEnemyRow[ENEMY_COUNT];
s16 mapHumanCol[HUMAN_COUNT], mapHumanRow[HUMAN_COUNT];
s16 mapPlayerRow;
static void drawScore(){
uintToStr(score, scoreStr, 1);
VDP_drawText(scoreStr, 1, 5);
if(lastScore < 10) scoreLength = 1;
else if(lastScore < 100) scoreLength = 2;
else if(lastScore < 1000) scoreLength = 3;
else if(lastScore < 10000) scoreLength = 4;
else if(lastScore < 100000) scoreLength = 5;
else if(lastScore < 1000000) scoreLength = 6;
else if(lastScore < 10000000) scoreLength = 7;
else scoreLength = 8;
uintToStr(lastScore, scoreStr, scoreLength);
bigText(scoreStr, SCORE_X, SCORE_Y, FALSE);
}
// load map when stage does so we have all enemies + player
@ -29,17 +66,24 @@ void loadMap(){
mapEnemyCol[i] = -1;
mapEnemyRow[i] = -1;
}
for(s16 i = 0; i < HUMAN_COUNT; i++){
mapHumanCol[i] = -1;
mapHumanRow[i] = -1;
}
mapPlayerRow = -1;
}
// temp arrays for new positions
s16 mapNewCol[ENEMY_COUNT], mapNewRow[ENEMY_COUNT];
s16 mapNewHumanCol[HUMAN_COUNT], mapNewHumanRow[HUMAN_COUNT];
static bool mapTileOccupied(s16 col, s16 row, s16 pRow){
// player always at center column
if(col == MAP_W / 2 && row == pRow) return TRUE;
for(s16 i = 0; i < ENEMY_COUNT; i++)
if(mapNewCol[i] == col && mapNewRow[i] == row) return TRUE;
for(s16 i = 0; i < HUMAN_COUNT; i++)
if(mapNewHumanCol[i] == col && mapNewHumanRow[i] == row) return TRUE;
return FALSE;
}
@ -67,6 +111,24 @@ static void updateMap(){
mapNewRow[i] = row;
}
// compute new human positions
for(s16 i = 0; i < HUMAN_COUNT; i++){
if(!humans[i].active || humans[i].image == NULL){
mapNewHumanCol[i] = -1;
mapNewHumanRow[i] = -1;
continue;
}
fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x);
s16 col = fix32ToInt(dx) / 54 + MAP_W / 2;
if(col < 0) col = 0;
if(col >= MAP_W) col = MAP_W - 1;
s16 row = fix32ToInt(humans[i].pos.y) / 75;
if(row < 0) row = 0;
if(row >= MAP_H) row = MAP_H - 1;
mapNewHumanCol[i] = col;
mapNewHumanRow[i] = row;
}
// clear old player tile if it moved and nothing new occupies it
if(mapPlayerRow >= 0 && mapPlayerRow != pRow)
if(!mapTileOccupied(MAP_W / 2, mapPlayerRow, pRow))
@ -80,6 +142,23 @@ static void updateMap(){
VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapEnemyCol[i], MAP_Y + mapEnemyRow[i]);
}
// clear old human tiles that moved or disappeared
for(s16 i = 0; i < HUMAN_COUNT; i++){
if(mapHumanCol[i] < 0) continue;
if(mapHumanCol[i] == mapNewHumanCol[i] && mapHumanRow[i] == mapNewHumanRow[i]) continue;
if(!mapTileOccupied(mapHumanCol[i], mapHumanRow[i], pRow))
VDP_setTileMapXY(BG_A, MAP_TILE, MAP_X + mapHumanCol[i], MAP_Y + mapHumanRow[i]);
}
// draw human dots (skip if player occupies same tile)
for(s16 i = 0; i < HUMAN_COUNT; i++){
mapHumanCol[i] = mapNewHumanCol[i];
mapHumanRow[i] = mapNewHumanRow[i];
if(mapNewHumanCol[i] < 0) continue;
if(mapNewHumanCol[i] == MAP_W / 2 && mapNewHumanRow[i] == pRow) continue;
VDP_setTileMapXY(BG_A, MAP_HUMAN_TILE, MAP_X + mapNewHumanCol[i], MAP_Y + mapNewHumanRow[i]);
}
// draw enemy dots (skip if player occupies same tile)
for(s16 i = 0; i < ENEMY_COUNT; i++){
mapEnemyCol[i] = mapNewCol[i];
@ -95,18 +174,42 @@ static void updateMap(){
}
void loadChrome(){
VDP_loadTileSet(imageFontBig.tileset, FONT_BIG_I, DMA);
VDP_loadTileSet(imageFontBigShadow.tileset, FONT_BIG_I + 32, DMA);
VDP_loadTileSet(imageChromeLife.tileset, LIFE_I, DMA);
VDP_loadTileSet(imageChromeLife2.tileset, LIFE_I + 2, DMA);
VDP_loadTileSet(mapIndicator.tileset, MAP_I, DMA);
lastScore = 1;
drawScore();
drawLives();
}
bool didGameOver;
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);
SPR_releaseSprite(player.image);
// clear minimap
VDP_clearTileMapRect(BG_A, MAP_X, MAP_Y, MAP_W, MAP_H);
// clear score
VDP_clearTileMapRect(BG_A, SCORE_X, SCORE_Y, SCORE_LENGTH, 2);
// clear lives
VDP_clearTileMapRect(BG_A, LIVES_X, LIVES_Y, 1, 16);
VDP_drawText("GAME OVER", 15, 13);
VDP_drawText("PRESS ANY BUTTON", 12, 14);
}
void updateChrome(){
score++;
if(score > 99999999) score = 0;
if(gameOver && !didGameOver) doGameOver();
if(didGameOver) return;
if(lastScore != score){
lastScore = score;
drawScore();
}
if(lastLives != player.lives) drawLives();
if(clock % 4 == 0) updateMap();
// VDP_clearText(1, 26, 4);
// VDP_drawText(debugStr, 1, 26);
}

View file

@ -34,7 +34,7 @@ void spawnEnemy(u8 type, u8 zone){
do {
// Random X within zone: zoneStart + random(0-511)
randX = zoneStart + FIX32(random() % 512);
randY = FIX32(16 + (random() % 192));
randY = FIX32(16 + (random() % 128));
attempts++;
} while(!isValidEnemyPosition(randX, randY) && attempts < 100);
@ -61,8 +61,22 @@ void spawnEnemy(u8 type, u8 zone){
}
static void boundsEnemy(u8 i){
if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off) || enemies[i].pos.y <= FIX32(enemies[i].off))
enemies[i].vel.y *= -1;
if(enemies[i].ints[3] >= 0){
// carrying: only check for reaching the top
if(enemies[i].pos.y <= FIX32(0)){
s16 h = enemies[i].ints[3];
if(humans[h].active) killHuman(h);
enemies[i].ints[3] = -1;
humanBeingCarried = FALSE;
// TODO: spawn mutant here
killEnemy(i);
return;
}
} else {
// not carrying: bounce off top and bottom
if(enemies[i].pos.y >= GAME_H_F - FIX32(enemies[i].off) || enemies[i].pos.y <= FIX32(enemies[i].off))
enemies[i].vel.y *= -1;
}
if(enemies[i].pos.x >= GAME_WRAP){
enemies[i].pos.x -= GAME_WRAP;
@ -73,7 +87,6 @@ static void boundsEnemy(u8 i){
}
static void updateEnemy(u8 i){
boundsEnemy(i);
enemies[i].pos.x += enemies[i].vel.x;
enemies[i].pos.y += enemies[i].vel.y;
@ -83,6 +96,9 @@ static void updateEnemy(u8 i){
break;
}
boundsEnemy(i);
if(!enemies[i].active) return;
s16 sx = getScreenX(enemies[i].pos.x, player.camera);
s16 sy = fix32ToInt(enemies[i].pos.y);
fix32 dx = getWrappedDelta(enemies[i].pos.x, player.pos.x);

View file

@ -1,18 +1,81 @@
void loadEnemyOne(u8 i){
enemies[i].ints[0] = random() % 40;
enemies[i].ints[0] = random() % 60;
enemies[i].ints[2] = -1; // target human index
enemies[i].ints[3] = -1; // carried human index
enemies[i].angle = ((random() % 4) * 256) + 128;
enemies[i].speed = FIX32(2);
}
void updateEnemyOne(u8 i){
if(enemies[i].clock % 40 == enemies[i].ints[0] && enemies[i].onScreen){
enemies[i].clock % 80 == enemies[i].ints[0] ? sfxEnemyShotB() : sfxEnemyShotA();
// carrying behavior: move upward, skip shooting
if(enemies[i].ints[3] >= 0){
// enemies[i].vel.x = (enemies[i].vel.x > 0) ? FIX32(0.3) : FIX32(-0.3);
// enemies[i].vel.y = FIX32(-1.5);
enemies[i].angle = 704 + (random() % 128);
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 any target if a human is already being carried
if(humanBeingCarried && enemies[i].ints[2] >= 0){
enemies[i].ints[2] = -1;
}
// seeking behavior: periodically look for a human to grab
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;
// hone toward human's current position at base speed
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: within 16px
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;
return;
}
}
}
// normal shooting
if(enemies[i].clock % 60 == enemies[i].ints[0] && enemies[i].onScreen){
enemies[i].clock % 120 == enemies[i].ints[0] ? sfxEnemyShotB() : sfxEnemyShotA();
struct bulletSpawner spawner = {
.x = enemies[i].pos.x,
.y = enemies[i].pos.y,
.anim = 3 + (random() % 3),
// .anim = 6,
// .frame = 1,
.speed = FIX32(2) + FIX16(random() % 4),
.angle = random() % 128,
};
@ -26,16 +89,3 @@ void updateEnemyOne(u8 i){
}
}
}
void updateLander(u8 i){}
void updateMutant(u8 i){}
void updateSwarmer(u8 i){}
void updatePod(u8 i){}
void updateBomber(u8 i){}
void updateBaiter(u8 i){}

View file

@ -50,15 +50,16 @@ void updateControls(u16 joy, u16 changed, u16 state){
struct playerStruct {
Vect2D_f32 pos, vel;
s16 shotAngle;
u8 lives;
u8 lives, recoveringClock;
fix32 camera, yCamera;
Sprite* image;
};
struct playerStruct player;
bool killBullets;
// bullets
#define BULLET_COUNT 64
#define BULLET_COUNT 70
struct bulletSpawner {
fix32 x, y, speed;
@ -92,6 +93,31 @@ struct enemy {
};
struct enemy enemies[ENEMY_COUNT];
// humans
#define HUMAN_COUNT 8
#define HUMAN_WALKING 0
#define HUMAN_CARRIED 1
#define HUMAN_FALLING 2
struct human {
bool active;
u8 state;
s16 carriedBy;
Vect2D_f32 pos, vel;
Sprite* image;
};
struct human humans[HUMAN_COUNT];
bool humanBeingCarried;
void killHuman(u8 i){
if(humans[i].state == HUMAN_CARRIED && humans[i].carriedBy >= 0){
enemies[humans[i].carriedBy].ints[3] = -1;
humanBeingCarried = FALSE;
}
humans[i].active = FALSE;
SPR_releaseSprite(humans[i].image);
}
void killBullet(u8 i, bool explode){
if(explode){
if(bullets[i].player){
@ -120,6 +146,16 @@ void killBullet(u8 i, bool explode){
}
void killEnemy(u8 i){
if(enemies[i].ints[3] >= 0){
s16 h = enemies[i].ints[3];
if(humans[h].active){
humans[h].state = HUMAN_FALLING;
humans[h].carriedBy = -1;
humans[h].vel.x = 0;
humans[h].vel.y = FIX32(2);
}
humanBeingCarried = FALSE;
}
enemies[i].active = FALSE;
SPR_releaseSprite(enemies[i].image);
}
@ -143,3 +179,30 @@ static s16 getScreenX(fix32 worldX, fix32 camera) {
}
return fix32ToInt(screenX);
}
// homing
#define PI_MOD 2.84444444444
#define PI_F FIX16(3.14159265358 * PI_MOD)
#define PI_F_2 FIX16(1.57079632679 * PI_MOD)
#define PI_F_4 FIX16(0.78539816339 * PI_MOD)
fix16 arctan(fix16 x) {
return fix16Mul(PI_F_4, x) - fix16Mul(fix16Mul(x, (abs(x) - 1)), (FIX16(0.245) + fix16Mul(FIX16(0.066), abs(x))));
}
fix16 arctan2(fix16 y, fix16 x) {
return x >= 0 ?
(y >= 0 ? (y < x ? arctan(fix16Div(y, x)) : PI_F_2 - arctan(fix16Div(x, y))) : (-y < x ? arctan(fix16Div(y, x)) : -PI_F_2 - arctan(fix16Div(x, y)))) :
(y >= 0 ? (y < -x ? arctan(fix16Div(y, x)) + PI_F : PI_F_2 - arctan(fix16Div(x, y))) : (-y < -x ? arctan(fix16Div(y, x)) - PI_F : -PI_F_2 - arctan(fix16Div(x, y))));
}
s16 arcAngle;
s16 honeAngle(fix16 x1, fix16 x2, fix16 y1, fix16 y2){
arcAngle = arctan2(y2 - y1, x2 - x1);
if(arcAngle >= 128) arcAngle -= 32;
if(arcAngle >= 384) arcAngle -= 32;
if(arcAngle < 0){
arcAngle = 1024 + arcAngle;
if(arcAngle < 896) arcAngle += 32;
if(arcAngle < 640) arcAngle += 32;
}
return arcAngle;
}

96
src/humans.h Normal file
View file

@ -0,0 +1,96 @@
void spawnHuman(u8 zone){
s16 i = -1;
for(s16 j = 0; j < HUMAN_COUNT; j++) if(!humans[j].active) { i = j; break; }
if(i == -1) return;
humans[i].active = TRUE;
humans[i].state = HUMAN_WALKING;
humans[i].carriedBy = -1;
fix32 zoneStart = FIX32(zone * 512);
humans[i].pos.x = zoneStart + FIX32(random() % 512);
humans[i].pos.y = GAME_H_F - FIX32(24);
fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) };
humans[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3];
humans[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
humans[i].image = SPR_addSprite(&humanSprite,
getScreenX(humans[i].pos.x, player.camera), fix32ToInt(humans[i].pos.y),
TILE_ATTR(PAL0, 0, 0, 0));
if(!humans[i].image){
humans[i].active = FALSE;
return;
}
}
static void updateHuman(u8 i){
switch(humans[i].state){
case HUMAN_WALKING:
// Y bounce: bob 4px around ground level
if(humans[i].pos.y >= GAME_H_F - FIX32(20) || humans[i].pos.y <= GAME_H_F - FIX32(28))
humans[i].vel.y *= -1;
// X wrap
if(humans[i].pos.x >= GAME_WRAP)
humans[i].pos.x -= GAME_WRAP;
if(humans[i].pos.x < 0)
humans[i].pos.x += GAME_WRAP;
humans[i].pos.x += humans[i].vel.x;
humans[i].pos.y += humans[i].vel.y;
break;
case HUMAN_CARRIED:
// follow carrier enemy position
if(humans[i].carriedBy >= 0 && enemies[humans[i].carriedBy].active){
humans[i].pos.x = enemies[humans[i].carriedBy].pos.x;
humans[i].pos.y = enemies[humans[i].carriedBy].pos.y + FIX32(16);
} else {
// carrier died (shouldn't normally reach here, killEnemy handles it)
humans[i].state = HUMAN_FALLING;
humans[i].carriedBy = -1;
humans[i].vel.x = 0;
humans[i].vel.y = FIX32(2);
humanBeingCarried = FALSE;
}
break;
case HUMAN_FALLING:
humans[i].pos.y += humans[i].vel.y;
// land on ground
if(humans[i].pos.y >= GAME_H_F - FIX32(24)){
humans[i].pos.y = GAME_H_F - FIX32(24);
humans[i].state = HUMAN_WALKING;
fix32 speeds[] = { FIX32(0.3), FIX32(0.4), FIX32(0.5) };
humans[i].vel.x = (random() % 2 == 0) ? speeds[random() % 3] : -speeds[random() % 3];
humans[i].vel.y = (random() % 2 == 0) ? FIX32(0.1) : FIX32(-0.1);
}
break;
}
// collect: check overlap with player (walking or falling only)
fix32 dx = getWrappedDelta(humans[i].pos.x, player.pos.x);
if(humans[i].state != HUMAN_CARRIED){
fix32 dy = humans[i].pos.y - player.pos.y;
if(dx >= FIX32(-24) && dx <= FIX32(24) && dy >= FIX32(-24) && dy <= FIX32(24)){
score += (humans[i].state == HUMAN_FALLING) ? 2000 : 1000;
sfxPickup();
killHuman(i);
return;
}
}
s16 sx = getScreenX(humans[i].pos.x, player.camera);
s16 sy = fix32ToInt(humans[i].pos.y);
bool visible = (dx >= -CULL_LIMIT && dx <= CULL_LIMIT);
SPR_setVisibility(humans[i].image, visible ? VISIBLE : HIDDEN);
SPR_setPosition(humans[i].image, sx, sy);
}
void updateHumans(){
for(s16 i = 0; i < HUMAN_COUNT; i++)
if(humans[i].active)
updateHuman(i);
}

View file

@ -5,6 +5,7 @@
#include "background.h"
#include "bullets.h"
#include "enemies.h"
#include "humans.h"
#include "player.h"
#include "stage.h"
#include "chrome.h"
@ -35,6 +36,8 @@ static void updateGame(){
updateSfx();
if(clock % 2 == 0){
updateEnemies();
if(!gameOver && enemyCount == 0) gameOver = TRUE;
updateHumans();
} else {
updateBackground();
updateBullets();

View file

@ -9,8 +9,8 @@
#define PLAYER_BOUND_Y FIX32(PLAYER_OFF)
#define PLAYER_BOUND_H FIX32(224 - PLAYER_OFF)
#define CAMERA_X FIX32(80)
#define CAMERA_W FIX32(240)
#define CAMERA_X FIX32(96)
#define CAMERA_W FIX32(224)
#define SHOT_INTERVAL 15
@ -106,7 +106,10 @@ static void shootPlayer(){
.angle = player.shotAngle,
.player = TRUE
};
spawnBullet(spawner, EMPTY);
void updater(s16 i){
if(bullets[i].clock == 5) killBullet(i, TRUE);
}
spawnBullet(spawner, updater);
sfxPlayerShot();
shotClock = SHOT_INTERVAL;
} else if(shotClock > 0) shotClock--;
@ -127,16 +130,21 @@ void loadPlayer(){
}
void updatePlayer(){
movePlayer();
boundsPlayer();
cameraPlayer();
shootPlayer();
s16 sx = getScreenX(player.pos.x, player.camera);
s16 sy = fix32ToInt(player.pos.y);
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
intToStr(fix32ToInt(player.pos.x), debugStr, 1);
if(!gameOver){
if(player.recoveringClock > 0){
if(player.recoveringClock % 10 == 1)
SPR_setVisibility(player.image, player.recoveringClock % 20 == 1 ? VISIBLE : HIDDEN);
player.recoveringClock--;
if(player.recoveringClock == 0)
SPR_setVisibility(player.image, VISIBLE);
}
movePlayer();
boundsPlayer();
cameraPlayer();
shootPlayer();
s16 sx = getScreenX(player.pos.x, player.camera);
s16 sy = fix32ToInt(player.pos.y);
SPR_setPosition(player.image, sx - PLAYER_OFF, sy - PLAYER_OFF);
intToStr(fix32ToInt(player.pos.x), debugStr, 1);
}
}

View file

@ -1,10 +1,12 @@
static s16 sfxShotClock;
static u16 sfxShotFreq;
#define SFX_VOL 2
void sfxPlayerShot(){
sfxShotClock = 4;
sfxShotFreq = 150;
PSG_setEnvelope(2, 2);
PSG_setEnvelope(2, SFX_VOL);
PSG_setFrequency(2, sfxShotFreq);
}
@ -14,43 +16,56 @@ static u8 sfxEnemyShotType;
// high sharp zap - quick descending chirp
void sfxEnemyShotA(){
if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 3;
sfxEnemyShotFreq = 1200;
sfxEnemyShotType = 0;
PSG_setEnvelope(1, 3);
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
}
// mid buzzy pulse - sits in the midrange
void sfxEnemyShotB(){
if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 5;
sfxEnemyShotFreq = 400;
sfxEnemyShotType = 1;
PSG_setEnvelope(1, 3);
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
}
// quick rising ping - sweeps upward
void sfxEnemyShotC(){
if(player.recoveringClock > 0) return;
sfxEnemyShotClock = 4;
sfxEnemyShotFreq = 300;
sfxEnemyShotType = 2;
PSG_setEnvelope(1, 3);
PSG_setEnvelope(1, SFX_VOL);
PSG_setFrequency(1, sfxEnemyShotFreq);
}
static s16 sfxPickupClock;
static u16 sfxPickupFreq;
void sfxPickup(){
sfxPickupClock = 12;
sfxPickupFreq = 800;
PSG_setEnvelope(0, SFX_VOL);
PSG_setFrequency(0, sfxPickupFreq);
}
static s16 sfxExpClock;
void sfxExplosion(){
sfxExpClock = 18;
PSG_setNoise(PSG_NOISE_TYPE_WHITE, PSG_NOISE_FREQ_CLOCK2);
PSG_setEnvelope(3, 0);
PSG_setEnvelope(3, SFX_VOL);
}
void updateSfx(){
if(sfxExpClock > 0){
sfxExpClock--;
PSG_setEnvelope(3, (18 - sfxExpClock) * 15 / 18);
PSG_setEnvelope(3, SFX_VOL + (18 - sfxExpClock) * (15 - SFX_VOL) / 18);
if(sfxExpClock == 0){
PSG_setEnvelope(3, 15);
}
@ -61,18 +76,28 @@ void updateSfx(){
else if(sfxEnemyShotType == 1) sfxEnemyShotFreq -= 50;
else sfxEnemyShotFreq += 150;
PSG_setFrequency(1, sfxEnemyShotFreq);
PSG_setEnvelope(1, 3 + (sfxEnemyShotType == 0 ? (3 - sfxEnemyShotClock) * 4 :
sfxEnemyShotType == 1 ? (5 - sfxEnemyShotClock) * 2 :
(4 - sfxEnemyShotClock) * 3));
PSG_setEnvelope(1, SFX_VOL + (sfxEnemyShotType == 0 ? (3 - sfxEnemyShotClock) * (15 - SFX_VOL) / 3 :
sfxEnemyShotType == 1 ? (5 - sfxEnemyShotClock) * (15 - SFX_VOL) / 5 :
(4 - sfxEnemyShotClock) * (15 - SFX_VOL) / 4));
if(sfxEnemyShotClock == 0){
PSG_setEnvelope(1, 15);
}
}
if(sfxPickupClock > 0){
sfxPickupClock--;
// rising staircase: jump up every 3 frames
if(sfxPickupClock % 3 == 0) sfxPickupFreq += 200;
PSG_setFrequency(0, sfxPickupFreq);
PSG_setEnvelope(0, SFX_VOL);
if(sfxPickupClock == 0){
PSG_setEnvelope(0, 15);
}
}
if(sfxShotClock > 0){
sfxShotClock--;
sfxShotFreq -= 30;
PSG_setFrequency(2, sfxShotFreq);
PSG_setEnvelope(2, 2 + (4 - sfxShotClock) * 3);
PSG_setEnvelope(2, SFX_VOL + (4 - sfxShotClock) * (15 - SFX_VOL) / 4);
if(sfxShotClock == 0){
PSG_setEnvelope(2, 15);
}

View file

@ -1,9 +1,15 @@
void loadStage(){
// Spawn 2 enemies per zone (4 zones = 8 total)
// Spawn 3 enemies per zone (4 zones = 12 total)
for(u8 zone = 0; zone < 4; zone++){
for(u8 i = 0; i < 3; i++){
spawnEnemy(0, zone);
}
}
// Spawn 2 humans per zone (4 zones = 8 total)
for(u8 zone = 0; zone < 4; zone++){
for(u8 i = 0; i < 2; i++){
spawnHuman(zone);
}
}
loadMap();
}