new level! :)

This commit is contained in:
liquidraver
2026-05-22 09:11:32 +02:00
parent 4f388c0b98
commit 072b4edadb
+384 -139
View File
@@ -7,7 +7,13 @@
* Runs on k_work_delayable at 20 FPS on the system work queue.
* Writes directly to display via display_write() (bypasses CFB).
*
* ~1.7KB RAM, ~5KB flash when enabled.
* Two levels:
* L1 — kill 2 monsters (imp + demon); a portal spawns at a random open tile.
* L2 — open arena; a slow, 300-HP boss waits in a random corner. Kite and
* shoot it to win. Boss is ~4× slower than the player so you can run,
* turn, fire, and sprint away before it reaches you.
*
* ~1.9 KB RAM, ~6 KB flash when enabled.
*/
#include "doom_game.h"
@@ -42,11 +48,15 @@ LOG_MODULE_REGISTER(doom_game, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL);
#define TARGET_FPS 20
#define FRAME_MS (1000 / TARGET_FPS)
#define MOVE_SPEED (FP_ONE / 8)
#define MOVE_SPEED (FP_ONE / 8) /* player */
#define ROT_SPEED (FP_ONE / 12)
#define COLLISION_R (FP_ONE / 4)
#define MAX_ENEMIES 8
#define ENEMY_SPEED (FP_ONE / 16) /* imp / demon */
#define BOSS_SPEED (FP_ONE / 32) /* boss — half of normal, ~4× slower than player */
#define BOSS_HP 300
/* Input bitflags */
#define DINPUT_FWD BIT(0)
#define DINPUT_BACK BIT(1)
@@ -90,6 +100,7 @@ enum enemy_type {
ENEMY_NONE = 0,
ENEMY_IMP,
ENEMY_DEMON,
ENEMY_BOSS,
};
enum enemy_state {
@@ -124,6 +135,10 @@ struct doom_state {
int level;
int score;
bool game_over;
bool won;
bool portal_active;
int portal_map_x, portal_map_y;
int portal_anim;
uint32_t frame_count;
};
@@ -136,7 +151,6 @@ static atomic_t input_state;
static fixed_t zbuffer[SCREEN_W];
static int fire_cooldown;
/* Game tick work */
static struct k_work_delayable game_tick_work;
/* ========== SOUND ========== */
@@ -162,6 +176,10 @@ static const struct tone snd_hit[] = {
static const struct tone snd_death[] = {
{500, 80}, {400, 80}, {300, 100}, {200, 120}, {100, 150}, {0, 0}
};
/* Rising arpeggio — plays when portal opens and again on victory */
static const struct tone snd_portal[] = {
{400, 40}, {600, 40}, {800, 40}, {1100, 80}, {0, 0}
};
static const struct tone *current_sound;
static struct k_work_delayable sound_work;
@@ -220,8 +238,13 @@ static void doom_sound_off(void)
#endif
}
/* ========== MAP ========== */
/* ========== MAPS ========== */
/*
* Level 1: corridors and alcoves.
* Player starts south-centre facing north.
* Two monsters spawn NW and NE.
*/
static const uint8_t level_1[MAP_H][MAP_W] = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
@@ -241,11 +264,37 @@ static const uint8_t level_1[MAP_H][MAP_W] = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
};
/*
* Level 2: open boss arena with corner pillars and a hollow centre block.
* Long sight-lines for kiting. Player enters south-centre; boss spawns in one
* of four far corners chosen pseudo-randomly.
*/
static const uint8_t level_2[MAP_H][MAP_W] = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,2,0,0,0,0,0,0,0,0,2,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,3,3,3,3,0,0,0,0,0,1},
{1,0,0,0,0,0,3,0,0,3,0,0,0,0,0,1},
{1,0,0,0,0,0,3,0,0,3,0,0,0,0,0,1},
{1,0,0,0,0,0,3,3,3,3,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,2,0,0,0,0,0,0,0,0,2,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
};
static const uint8_t *current_map;
static void doom_map_init(void)
{
current_map = &level_1[0][0];
current_map = (game.level == 2)
? &level_2[0][0]
: &level_1[0][0];
}
static uint8_t doom_map_get(int x, int y)
@@ -340,7 +389,7 @@ static bool ceiling_pixel(int x, int y)
return brightness > threshold;
}
/* ========== ENEMY SPRITES (16x16 1-bit) ========== */
/* ========== SPRITES (16x16 1-bit) ========== */
static const uint16_t spr_imp_idle[16] = {
0x07C0, 0x0FE0, 0x1BB0, 0x1FF0, 0x0FE0, 0x07C0, 0x27C8, 0x77DC,
@@ -358,6 +407,24 @@ static const uint16_t spr_demon_attack[16] = {
0x4004, 0x600C, 0x783C, 0x7FFC, 0xFFFE, 0xF03E, 0xFBBE, 0x787C,
0x7FFC, 0x7FFC, 0xFFFE, 0xFFFE, 0xF83E, 0xE82E, 0xC826, 0xC826,
};
/*
* Boss: hulking demon — very wide, almost fully filled, wide-open jaw on attack.
* Distinct from the regular demon by the solid torso and broader silhouette.
*/
static const uint16_t spr_boss_idle[16] = {
0x2004, 0x300C, 0x7FFE, 0xFFFF,
0xE3C7, 0xFFFF, 0xDBBD, 0xFFFF,
0xFFFF, 0xFFFF, 0x7FFE, 0x3FFC,
0x1FF8, 0x0FF0, 0x0660, 0x0FF0,
};
static const uint16_t spr_boss_attack[16] = {
0x2004, 0x300C, 0x7FFE, 0xFFFF,
0xC003, 0xE7E7, 0x8001, 0xFFFF,
0xFFFF, 0xFFFF, 0x7FFE, 0x3FFC,
0x1FF8, 0x0FF0, 0x0660, 0x0FF0,
};
static const uint16_t spr_dying[16] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x0200, 0x1290, 0x0840, 0x27C8,
0x1FF0, 0x3FF8, 0x7FFC, 0x7FFC, 0xFFFE, 0xFFFE, 0xFFFE, 0xFFFE,
@@ -367,6 +434,21 @@ static const uint16_t spr_dead[16] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x4488, 0x2B54, 0x7FFC, 0xFFFE,
};
/*
* Portal sprites — two frames that alternate every 8 ticks (~0.4 s each):
* frame A: diamond outline (converging tips at top/bottom)
* frame B: X pattern (diverging to corners)
* The visual swap reads like a spinning / pulsing energy field.
*/
static const uint16_t spr_portal_a[16] = {
0x0180, 0x0240, 0x0420, 0x0810, 0x1008, 0x2004, 0x4002, 0x8001,
0x8001, 0x4002, 0x2004, 0x1008, 0x0810, 0x0420, 0x0240, 0x0180,
};
static const uint16_t spr_portal_b[16] = {
0x8001, 0x4002, 0x2004, 0x1008, 0x0810, 0x0420, 0x0240, 0x0180,
0x0180, 0x0240, 0x0420, 0x0810, 0x1008, 0x2004, 0x4002, 0x8001,
};
static const uint16_t *get_sprite_16(enum enemy_type type,
enum enemy_state state,
int anim_tick)
@@ -381,6 +463,9 @@ static const uint16_t *get_sprite_16(enum enemy_type type,
case ENEMY_DEMON:
return (state == ESTATE_ATTACK && (anim_tick & 4))
? spr_demon_attack : spr_demon_idle;
case ENEMY_BOSS:
return (state == ESTATE_ATTACK && (anim_tick & 4))
? spr_boss_attack : spr_boss_idle;
default:
return spr_imp_idle;
}
@@ -388,10 +473,88 @@ static const uint16_t *get_sprite_16(enum enemy_type type,
/* ========== RAYCASTER ========== */
/*
* Render one 16×16 sprite at world position (wx, wy).
* Shared by both the portal and all enemy sprites.
*/
static void render_one_sprite(const uint16_t *sprite,
fixed_t wx, fixed_t wy,
fixed_t inv_det,
struct doom_player *p)
{
fixed_t sx = wx - p->x;
fixed_t sy = wy - p->y;
fixed_t transform_x = fp_mul(inv_det,
fp_mul(p->dir_y, sx) - fp_mul(p->dir_x, sy));
fixed_t transform_y = fp_mul(inv_det,
fp_mul(-p->plane_y, sx) + fp_mul(p->plane_x, sy));
if (transform_y <= 0) return;
int sprite_screen_x = (SCREEN_W / 2) +
fp_to_int(fp_div(fp_mul(transform_x,
fp_from_int(SCREEN_W)), transform_y));
int sprite_h = fp_to_int(fp_abs(fp_div(
fp_from_int(SCREEN_H), transform_y)));
if (sprite_h < 3) return;
if (sprite_h > SCREEN_H * 2) sprite_h = SCREEN_H * 2;
int draw_start_y = (SCREEN_H - sprite_h) / 2;
int draw_end_y = draw_start_y + sprite_h;
int draw_start_x = sprite_screen_x - sprite_h / 2;
int draw_end_x = sprite_screen_x + sprite_h / 2;
int brightness = dist_to_brightness(transform_y);
for (int sc = draw_start_x; sc < draw_end_x; sc++) {
if (sc < 0 || sc >= SCREEN_W) continue;
if (transform_y >= zbuffer[sc]) continue;
int tex_x = (sc - draw_start_x) * 16 / sprite_h;
if (tex_x < 0) tex_x = 0;
if (tex_x > 15) tex_x = 15;
for (int sr = draw_start_y; sr < draw_end_y; sr++) {
if (sr < 0 || sr >= SCREEN_H) continue;
int tex_y = (sr - draw_start_y) * 16 / sprite_h;
if (tex_y < 0) tex_y = 0;
if (tex_y > 15) tex_y = 15;
if (sprite[tex_y] & (0x8000 >> tex_x)) {
uint8_t threshold =
bayer4x4[sr & 3][sc & 3];
if (brightness > threshold) {
render_fb[(sr / 8) * SCREEN_W + sc] |=
(1U << (sr & 7));
}
}
}
}
}
static void render_sprites(void)
{
struct doom_player *p = &game.player;
fixed_t det = fp_mul(p->plane_x, p->dir_y) -
fp_mul(p->dir_x, p->plane_y);
if (det == 0) return;
fixed_t inv_det = fp_div(FP_ONE, det);
/* Portal renders behind enemies (drawn first, no depth-sort needed) */
if (game.portal_active) {
const uint16_t *pspr = (game.portal_anim & 8)
? spr_portal_b : spr_portal_a;
render_one_sprite(pspr,
fp_from_int(game.portal_map_x) + FP_HALF,
fp_from_int(game.portal_map_y) + FP_HALF,
inv_det, p);
}
/* Collect visible enemies and sort farthest-first */
struct sprite_order {
int idx;
fixed_t dist;
@@ -404,14 +567,11 @@ static void render_sprites(void)
fixed_t dx = e->x - p->x;
fixed_t dy = e->y - p->y;
order[visible].idx = i;
order[visible].idx = i;
order[visible].dist = fp_mul(dx, dx) + fp_mul(dy, dy);
visible++;
}
if (visible == 0) return;
/* Insertion sort — farthest first */
for (int i = 1; i < visible; i++) {
struct sprite_order tmp = order[i];
int j = i - 1;
@@ -422,67 +582,11 @@ static void render_sprites(void)
order[j + 1] = tmp;
}
fixed_t det = fp_mul(p->plane_x, p->dir_y) -
fp_mul(p->dir_x, p->plane_y);
if (det == 0) return;
fixed_t inv_det = fp_div(FP_ONE, det);
for (int i = 0; i < visible; i++) {
struct doom_enemy *e = &game.enemies[order[i].idx];
const uint16_t *sprite = get_sprite_16(e->type, e->state,
e->anim_tick);
fixed_t sx = e->x - p->x;
fixed_t sy = e->y - p->y;
fixed_t transform_x = fp_mul(inv_det,
fp_mul(p->dir_y, sx) - fp_mul(p->dir_x, sy));
fixed_t transform_y = fp_mul(inv_det,
fp_mul(-p->plane_y, sx) + fp_mul(p->plane_x, sy));
if (transform_y <= 0) continue;
int sprite_screen_x = (SCREEN_W / 2) +
fp_to_int(fp_div(fp_mul(transform_x,
fp_from_int(SCREEN_W)), transform_y));
int sprite_h = fp_to_int(fp_abs(fp_div(
fp_from_int(SCREEN_H), transform_y)));
if (sprite_h < 3) continue;
if (sprite_h > SCREEN_H * 2) sprite_h = SCREEN_H * 2;
int draw_start_y = (SCREEN_H - sprite_h) / 2;
int draw_end_y = draw_start_y + sprite_h;
int draw_start_x = sprite_screen_x - sprite_h / 2;
int draw_end_x = sprite_screen_x + sprite_h / 2;
int brightness = dist_to_brightness(transform_y);
for (int sc = draw_start_x; sc < draw_end_x; sc++) {
if (sc < 0 || sc >= SCREEN_W) continue;
if (transform_y >= zbuffer[sc]) continue;
int tex_x = (sc - draw_start_x) * 16 / sprite_h;
if (tex_x < 0) tex_x = 0;
if (tex_x > 15) tex_x = 15;
for (int sr = draw_start_y; sr < draw_end_y; sr++) {
if (sr < 0 || sr >= SCREEN_H) continue;
int tex_y = (sr - draw_start_y) * 16 / sprite_h;
if (tex_y < 0) tex_y = 0;
if (tex_y > 15) tex_y = 15;
if (sprite[tex_y] & (0x8000 >> tex_x)) {
uint8_t threshold =
bayer4x4[sr & 3][sc & 3];
if (brightness > threshold) {
render_fb[(sr / 8) * SCREEN_W + sc] |=
(1U << (sr & 7));
}
}
}
}
render_one_sprite(
get_sprite_16(e->type, e->state, e->anim_tick),
e->x, e->y, inv_det, p);
}
}
@@ -585,7 +689,7 @@ static void raycaster_render(void)
perp_dist));
int draw_start = (SCREEN_H - line_height) / 2;
int draw_end = draw_start + line_height;
int draw_end = draw_start + line_height;
fixed_t wall_x;
if (!side) {
@@ -604,7 +708,7 @@ static void raycaster_render(void)
}
int actual_start = draw_start < 0 ? 0 : draw_start;
int actual_end = draw_end >= SCREEN_H ? SCREEN_H - 1 : draw_end;
int actual_end = draw_end >= SCREEN_H ? SCREEN_H - 1 : draw_end;
for (int y = actual_start; y <= actual_end; y++) {
int tex_y = ((y - draw_start) * 8) / line_height;
@@ -669,6 +773,7 @@ static const uint8_t glyph_E[5] = {0xF, 0x8, 0xE, 0x8, 0xF};
static const uint8_t glyph_O[5] = {0x6, 0x9, 0x9, 0x9, 0x6};
static const uint8_t glyph_V[5] = {0x9, 0x9, 0x9, 0x6, 0x4};
static const uint8_t glyph_R[5] = {0xE, 0x9, 0xE, 0xC, 0x9};
static const uint8_t glyph_L[5] = {0x8, 0x8, 0x8, 0x8, 0xF};
static void draw_glyph(int dx, int dy, const uint8_t *glyph)
{
@@ -731,18 +836,27 @@ static void draw_hud(void)
{
memset(&render_fb[7 * SCREEN_W], 0, SCREEN_W);
/* Separator line at y=57 */
for (int x = 0; x < SCREEN_W; x++) {
render_fb[7 * SCREEN_W + x] |= 0x01;
}
/* HP bar */
draw_glyph(1, 58, glyph_H);
draw_glyph(6, 58, glyph_P);
draw_health_bar(12, 58, game.player.health);
/* Ammo */
draw_glyph(48, 58, glyph_A);
draw_glyph(53, 58, glyph_M);
draw_number(59, 58, game.player.ammo);
/* Level indicator: "LV1" / "LV2" */
draw_glyph(76, 58, glyph_L);
draw_glyph(81, 58, glyph_V);
draw_number(87, 58, game.level);
/* Score */
draw_number(100, 58, game.score);
}
@@ -785,7 +899,7 @@ static void draw_gun(bool firing)
set_px(ch_x + 2, ch_y);
}
/* ========== GAME OVER SCREEN ========== */
/* ========== END SCREENS ========== */
static const uint8_t letter_D[8] = {
0xFC, 0xFE, 0x86, 0x86, 0x86, 0x86, 0xFE, 0xFC
@@ -796,6 +910,15 @@ static const uint8_t letter_E[8] = {
static const uint8_t letter_A[8] = {
0x7C, 0xFE, 0x86, 0x86, 0xFE, 0xFE, 0x86, 0x86
};
static const uint8_t letter_W[8] = {
0x82, 0x82, 0x82, 0x92, 0xAA, 0xC6, 0x82, 0x00
};
static const uint8_t letter_I[8] = {
0xFE, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFE
};
static const uint8_t letter_N[8] = {
0x82, 0xC2, 0xA2, 0x92, 0x8A, 0x86, 0x82, 0x00
};
static void blit_letter(int lx, int ly, const uint8_t *data)
{
@@ -818,25 +941,36 @@ static void draw_game_over(void)
{
memset(render_fb, 0, FB_SIZE);
int start_x = (SCREEN_W - 38) / 2;
int start_y = 20;
if (game.won) {
/* "WIN" + score */
int sx = (SCREEN_W - 29) / 2;
int sy = 18;
blit_letter(sx, sy, letter_W);
blit_letter(sx + 10, sy, letter_I);
blit_letter(sx + 20, sy, letter_N);
blit_letter(start_x, start_y, letter_D);
blit_letter(start_x + 10, start_y, letter_E);
blit_letter(start_x + 20, start_y, letter_A);
blit_letter(start_x + 30, start_y, letter_D);
/* Sub-line: score */
draw_number((SCREEN_W - 15) / 2, sy + 14, game.score);
} else {
/* "DEAD" */
int sx = (SCREEN_W - 38) / 2;
int sy = 20;
blit_letter(sx, sy, letter_D);
blit_letter(sx + 10, sy, letter_E);
blit_letter(sx + 20, sy, letter_A);
blit_letter(sx + 30, sy, letter_D);
/* "game over" on one line: GAME(4 chars) + 7px gap + OVER(4 chars) = 40px */
int go_x = (SCREEN_W - 40) / 2;
int go_y = start_y + 14;
draw_glyph(go_x, go_y, glyph_G);
draw_glyph(go_x + 5, go_y, glyph_A);
draw_glyph(go_x + 10, go_y, glyph_M);
draw_glyph(go_x + 15, go_y, glyph_E);
draw_glyph(go_x + 22, go_y, glyph_O);
draw_glyph(go_x + 27, go_y, glyph_V);
draw_glyph(go_x + 32, go_y, glyph_E);
draw_glyph(go_x + 37, go_y, glyph_R);
int go_x = (SCREEN_W - 40) / 2;
int go_y = sy + 14;
draw_glyph(go_x, go_y, glyph_G);
draw_glyph(go_x + 5, go_y, glyph_A);
draw_glyph(go_x + 10, go_y, glyph_M);
draw_glyph(go_x + 15, go_y, glyph_E);
draw_glyph(go_x + 22, go_y, glyph_O);
draw_glyph(go_x + 27, go_y, glyph_V);
draw_glyph(go_x + 32, go_y, glyph_E);
draw_glyph(go_x + 37, go_y, glyph_R);
}
}
/* ========== DISPLAY ========== */
@@ -862,43 +996,36 @@ static void game_init(void)
{
memset(&game, 0, sizeof(game));
game.player.x = fp_from_int(8) + FP_HALF;
game.player.y = fp_from_int(13) + FP_HALF;
game.player.x = fp_from_int(8) + FP_HALF;
game.player.y = fp_from_int(13) + FP_HALF;
game.player.health = 100;
game.player.ammo = 50;
game.player.dir_x = 0;
game.player.dir_y = -FP_ONE;
game.player.ammo = 50;
game.player.dir_x = 0;
game.player.dir_y = -FP_ONE;
game.player.plane_x = (FP_ONE * 2) / 3;
game.player.plane_y = 0;
/* Level 1: exactly two monsters — one imp (NW), one demon (NE) */
game.enemies[0] = (struct doom_enemy){
.type = ENEMY_IMP, .state = ESTATE_IDLE,
.x = fp_from_int(4) + FP_HALF,
.y = fp_from_int(4) + FP_HALF,
.type = ENEMY_IMP,
.state = ESTATE_IDLE,
.x = fp_from_int(4) + FP_HALF,
.y = fp_from_int(4) + FP_HALF,
.health = 30,
};
game.enemies[1] = (struct doom_enemy){
.type = ENEMY_DEMON, .state = ESTATE_IDLE,
.x = fp_from_int(12) + FP_HALF,
.y = fp_from_int(4) + FP_HALF,
.type = ENEMY_DEMON,
.state = ESTATE_IDLE,
.x = fp_from_int(12) + FP_HALF,
.y = fp_from_int(4) + FP_HALF,
.health = 60,
};
game.enemies[2] = (struct doom_enemy){
.type = ENEMY_IMP, .state = ESTATE_IDLE,
.x = fp_from_int(8) + FP_HALF,
.y = fp_from_int(8) + FP_HALF,
.health = 30,
};
game.enemies[3] = (struct doom_enemy){
.type = ENEMY_DEMON, .state = ESTATE_IDLE,
.x = fp_from_int(4) + FP_HALF,
.y = fp_from_int(10) + FP_HALF,
.health = 60,
};
game.num_enemies = 4;
game.level = 1;
game.score = 0;
game.game_over = false;
game.num_enemies = 2;
game.level = 1;
game.score = 0;
game.game_over = false;
game.won = false;
game.portal_active = false;
fire_cooldown = 0;
atomic_set(&input_state, 0);
@@ -946,8 +1073,8 @@ static void handle_movement(uint32_t input)
fixed_t cos_r = FP_ONE - (ROT_SPEED / 8);
fixed_t sin_r = ROT_SPEED;
p->dir_x = fp_mul(old_dx, cos_r) - fp_mul(p->dir_y, sin_r);
p->dir_y = fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r);
p->dir_x = fp_mul(old_dx, cos_r) - fp_mul(p->dir_y, sin_r);
p->dir_y = fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r);
p->plane_x = fp_mul(old_px, cos_r) - fp_mul(p->plane_y, sin_r);
p->plane_y = fp_mul(old_px, sin_r) + fp_mul(p->plane_y, cos_r);
}
@@ -958,8 +1085,8 @@ static void handle_movement(uint32_t input)
fixed_t cos_r = FP_ONE - (ROT_SPEED / 8);
fixed_t sin_r = ROT_SPEED;
p->dir_x = fp_mul(old_dx, cos_r) + fp_mul(p->dir_y, sin_r);
p->dir_y = -fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r);
p->dir_x = fp_mul(old_dx, cos_r) + fp_mul(p->dir_y, sin_r);
p->dir_y = -fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r);
p->plane_x = fp_mul(old_px, cos_r) + fp_mul(p->plane_y, sin_r);
p->plane_y = -fp_mul(old_px, sin_r) + fp_mul(p->plane_y, cos_r);
}
@@ -1001,9 +1128,10 @@ static void handle_shooting(uint32_t input)
doom_play_sound(snd_hit);
if (e->health <= 0) {
e->state = ESTATE_DYING;
e->state = ESTATE_DYING;
e->anim_tick = 0;
game.score += (e->type == ENEMY_DEMON) ? 200 : 100;
game.score += (e->type == ENEMY_BOSS) ? 1000 :
(e->type == ENEMY_DEMON) ? 200 : 100;
} else {
e->state = ESTATE_CHASE;
}
@@ -1012,6 +1140,102 @@ static void handle_shooting(uint32_t input)
}
}
/*
* Scan the current map for open floor tiles that have open neighbours on all
* four cardinal sides and are at least 4 tiles (squared) from the player.
* Pick pseudo-randomly using frame_count as a seed.
*/
static void find_portal_spawn(int *out_x, int *out_y)
{
int cand_x[32], cand_y[32];
int count = 0;
for (int y = 1; y < MAP_H - 1 && count < 32; y++) {
for (int x = 1; x < MAP_W - 1 && count < 32; x++) {
if (doom_map_get(x, y ) != 0) continue;
if (doom_map_get(x - 1, y ) != 0) continue;
if (doom_map_get(x + 1, y ) != 0) continue;
if (doom_map_get(x, y - 1) != 0) continue;
if (doom_map_get(x, y + 1) != 0) continue;
int pdx = x - fp_to_int(game.player.x);
int pdy = y - fp_to_int(game.player.y);
if (pdx * pdx + pdy * pdy < 16) continue;
cand_x[count] = x;
cand_y[count] = y;
count++;
}
}
if (count == 0) {
/* Fallback — should never trigger on the defined maps */
*out_x = 8;
*out_y = 2;
return;
}
int idx = (int)(game.frame_count % (uint32_t)count);
*out_x = cand_x[idx];
*out_y = cand_y[idx];
}
/* If all L1 enemies are dead, open a portal at a random floor tile. */
static void check_level_complete(void)
{
if (game.portal_active || game.level != 1) return;
for (int i = 0; i < game.num_enemies; i++) {
if (game.enemies[i].type != ENEMY_NONE &&
game.enemies[i].state != ESTATE_DEAD) {
return;
}
}
int px, py;
find_portal_spawn(&px, &py);
game.portal_active = true;
game.portal_map_x = px;
game.portal_map_y = py;
game.portal_anim = 0;
doom_play_sound(snd_portal);
}
/*
* Transition to level 2.
* Player keeps current health and ammo.
* Boss spawns in one of four far corners, chosen pseudo-randomly.
*/
static void advance_to_level2(void)
{
game.level = 2;
game.portal_active = false;
current_map = &level_2[0][0];
/* Player respawns south-centre, facing north */
game.player.x = fp_from_int(8) + FP_HALF;
game.player.y = fp_from_int(13) + FP_HALF;
game.player.dir_x = 0;
game.player.dir_y = -FP_ONE;
game.player.plane_x = (FP_ONE * 2) / 3;
game.player.plane_y = 0;
static const int8_t boss_spots[4][2] = {
{2, 2}, {13, 2}, {2, 7}, {13, 7}
};
int bi = (int)(game.frame_count % 4);
memset(game.enemies, 0, sizeof(game.enemies));
game.enemies[0] = (struct doom_enemy){
.type = ENEMY_BOSS,
.state = ESTATE_CHASE, /* immediately aggressive */
.x = fp_from_int(boss_spots[bi][0]) + FP_HALF,
.y = fp_from_int(boss_spots[bi][1]) + FP_HALF,
.health = BOSS_HP,
};
game.num_enemies = 1;
}
static void update_enemies(void)
{
struct doom_player *p = &game.player;
@@ -1020,10 +1244,12 @@ static void update_enemies(void)
struct doom_enemy *e = &game.enemies[i];
if (e->type == ENEMY_NONE) continue;
fixed_t spd = (e->type == ENEMY_BOSS) ? BOSS_SPEED : ENEMY_SPEED;
switch (e->state) {
case ESTATE_IDLE: {
fixed_t dx = e->x - p->x;
fixed_t dy = e->y - p->y;
fixed_t dx = e->x - p->x;
fixed_t dy = e->y - p->y;
fixed_t dist = fp_mul(dx, dx) + fp_mul(dy, dy);
if (dist < fp_from_int(6 * 6)) {
e->state = ESTATE_CHASE;
@@ -1031,24 +1257,24 @@ static void update_enemies(void)
break;
}
case ESTATE_CHASE: {
fixed_t dx = p->x - e->x;
fixed_t dy = p->y - e->y;
fixed_t dx = p->x - e->x;
fixed_t dy = p->y - e->y;
fixed_t dist = fp_mul(dx, dx) + fp_mul(dy, dy);
if (dist < fp_from_int(2)) {
e->state = ESTATE_ATTACK;
e->state = ESTATE_ATTACK;
e->anim_tick = 0;
} else {
fixed_t speed = FP_ONE / 16;
fixed_t move_x = fp_mul(dx, speed) /
fixed_t move_x = fp_mul(dx, spd) /
(fp_to_int(dist) + 1);
fixed_t move_y = fp_mul(dy, speed) /
fixed_t move_y = fp_mul(dy, spd) /
(fp_to_int(dist) + 1);
fixed_t nx = e->x + move_x;
fixed_t ny = e->y + move_y;
if (doom_map_get(fp_to_int(nx), fp_to_int(ny)) == 0) {
if (doom_map_get(fp_to_int(nx),
fp_to_int(ny)) == 0) {
e->x = nx;
e->y = ny;
}
@@ -1058,7 +1284,9 @@ static void update_enemies(void)
case ESTATE_ATTACK:
e->anim_tick++;
if (e->anim_tick >= 20) {
int dmg = (e->type == ENEMY_DEMON) ? 15 : 8;
int dmg = (e->type == ENEMY_BOSS) ? 25 :
(e->type == ENEMY_DEMON) ? 15 : 8;
p->health -= dmg;
doom_play_sound(snd_hit);
@@ -1067,13 +1295,18 @@ static void update_enemies(void)
doom_play_sound(snd_death);
}
e->anim_tick = 0;
e->state = ESTATE_CHASE;
e->state = ESTATE_CHASE;
}
break;
case ESTATE_DYING:
e->anim_tick++;
if (e->anim_tick >= 10) {
e->state = ESTATE_DEAD;
if (e->type == ENEMY_BOSS) {
game.game_over = true;
game.won = true;
doom_play_sound(snd_portal);
}
}
break;
case ESTATE_DEAD:
@@ -1101,7 +1334,6 @@ static void game_tick_handler(struct k_work *work)
doom_sound_off();
}
/* Reschedule at slower rate for game over screen */
k_work_reschedule(&game_tick_work, K_MSEC(100));
return;
}
@@ -1120,6 +1352,24 @@ static void game_tick_handler(struct k_work *work)
update_enemies();
/* L1: spawn portal once both monsters are dead */
check_level_complete();
/* Portal: flicker and check if player stepped into it */
if (game.portal_active) {
game.portal_anim++;
fixed_t pdx = fp_from_int(game.portal_map_x) + FP_HALF
- game.player.x;
fixed_t pdy = fp_from_int(game.portal_map_y) + FP_HALF
- game.player.y;
/* Trigger within ~0.7 tiles (squared distance < 0.5) */
if (fp_mul(pdx, pdx) + fp_mul(pdy, pdy) < (FP_ONE / 2)) {
advance_to_level2();
}
}
memset(render_fb, 0, FB_SIZE);
raycaster_render();
draw_gun(game.player.firing && fire_cooldown > 2);
@@ -1128,7 +1378,6 @@ static void game_tick_handler(struct k_work *work)
game.frame_count++;
/* Reschedule next tick */
k_work_reschedule(&game_tick_work, K_MSEC(FRAME_MS));
}
@@ -1140,14 +1389,12 @@ void doom_game_start(void)
LOG_INF("Doom easter egg starting!");
/* Ensure display is unblanked */
const struct device *dev = mc_display_get_device();
if (dev) {
display_blanking_off(dev);
}
#ifdef CONFIG_ZEPHCORE_UI_BUZZER
/* Stop any firmware buzzer activity */
buzzer_stop();
#endif
@@ -1157,7 +1404,6 @@ void doom_game_start(void)
k_work_init_delayable(&game_tick_work, game_tick_handler);
k_work_init_delayable(&sound_work, doom_sound_work_handler);
/* Start game loop */
k_work_reschedule(&game_tick_work, K_NO_WAIT);
}
@@ -1171,7 +1417,6 @@ void doom_game_stop(void)
k_work_cancel_delayable(&game_tick_work);
doom_sound_off();
/* Clear the direct framebuffer to black */
const struct device *dev = mc_display_get_device();
if (dev) {
memset(render_fb, 0, FB_SIZE);