Initial commit

This commit is contained in:
Thomas
2026-02-28 18:00:58 +00:00
commit c66c12ae68
587 changed files with 239570 additions and 0 deletions

265
src/game/enemy.c Normal file
View File

@@ -0,0 +1,265 @@
#include "game/enemy.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/physics.h"
#include "engine/renderer.h"
#include <stdlib.h>
#include <math.h>
/* ════════════════════════════════════════════════════
* GRUNT - ground patrol enemy
* ════════════════════════════════════════════════════ */
static EntityManager *s_grunt_em = NULL;
static void grunt_update(Entity *self, float dt, const Tilemap *map) {
GruntData *gd = (GruntData *)self->data;
if (!gd) return;
Body *body = &self->body;
/* Death sequence */
if (self->flags & ENTITY_DEAD) {
animation_set(&self->anim, &anim_grunt_death);
animation_update(&self->anim, dt);
gd->death_timer -= dt;
body->vel.x = 0;
if (gd->death_timer <= 0) {
entity_destroy(s_grunt_em, self);
}
return;
}
/* Patrol: walk in one direction, reverse when hitting a wall
or about to walk off a ledge */
body->vel.x = gd->patrol_dir * GRUNT_SPEED;
/* Set facing direction */
if (gd->patrol_dir < 0) self->flags |= ENTITY_FACING_LEFT;
else self->flags &= ~ENTITY_FACING_LEFT;
/* Physics */
physics_update(body, dt, map);
/* Turn around on wall collision */
if (body->on_wall_left || body->on_wall_right) {
gd->patrol_dir = -gd->patrol_dir;
}
/* Turn around at ledge: check if there's ground ahead */
if (body->on_ground) {
float check_x = (gd->patrol_dir > 0) ?
body->pos.x + body->size.x + 2.0f :
body->pos.x - 2.0f;
float check_y = body->pos.y + body->size.y + 4.0f;
int tx = world_to_tile(check_x);
int ty = world_to_tile(check_y);
if (!tilemap_is_solid(map, tx, ty)) {
gd->patrol_dir = -gd->patrol_dir;
}
}
/* Animation */
if (fabsf(body->vel.x) > 1.0f) {
animation_set(&self->anim, &anim_grunt_walk);
} else {
animation_set(&self->anim, &anim_grunt_idle);
}
animation_update(&self->anim, dt);
}
static void grunt_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Vec2 render_pos = vec2(
body->pos.x - 2.0f,
body->pos.y
);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = render_pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = (self->flags & ENTITY_FACING_LEFT) != 0,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
SDL_Color color = {200, 60, 60, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void grunt_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void grunt_register(EntityManager *em) {
entity_register(em, ENT_ENEMY_GRUNT, grunt_update, grunt_render, grunt_destroy);
s_grunt_em = em;
}
Entity *grunt_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_ENEMY_GRUNT, pos);
if (!e) return NULL;
e->body.size = vec2(GRUNT_WIDTH, GRUNT_HEIGHT);
e->body.gravity_scale = 1.0f;
e->health = GRUNT_HEALTH;
e->max_health = GRUNT_HEALTH;
e->damage = 1;
GruntData *gd = calloc(1, sizeof(GruntData));
gd->patrol_dir = 1.0f;
gd->death_timer = 0.3f;
e->data = gd;
return e;
}
/* ════════════════════════════════════════════════════
* FLYER - flying enemy
* ════════════════════════════════════════════════════ */
static EntityManager *s_flyer_em = NULL;
/* Helper: find the player entity in the manager */
static Entity *find_player(EntityManager *em) {
for (int i = 0; i < em->count; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
static void flyer_update(Entity *self, float dt, const Tilemap *map) {
(void)map; /* flyers don't collide with tiles */
FlyerData *fd = (FlyerData *)self->data;
if (!fd) return;
Body *body = &self->body;
/* Death sequence */
if (self->flags & ENTITY_DEAD) {
animation_set(&self->anim, &anim_flyer_death);
animation_update(&self->anim, dt);
/* Fall when dead */
body->vel.y += physics_get_gravity() * dt;
if (body->vel.y > MAX_FALL_SPEED) body->vel.y = MAX_FALL_SPEED;
body->pos.y += body->vel.y * dt;
fd->death_timer -= dt;
if (fd->death_timer <= 0) {
entity_destroy(s_flyer_em, self);
}
return;
}
/* Bob up and down */
fd->bob_timer += dt;
float bob_offset = sinf(fd->bob_timer * FLYER_BOB_SPD) * FLYER_BOB_AMP;
body->pos.y = fd->base_y + bob_offset;
/* Chase player if in range */
Entity *player = find_player(s_flyer_em);
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
float px = player->body.pos.x + player->body.size.x * 0.5f;
float fx = body->pos.x + body->size.x * 0.5f;
float dist = px - fx;
if (fabsf(dist) < FLYER_DETECT) {
/* Move toward player */
if (dist < -2.0f) {
body->pos.x -= FLYER_SPEED * dt;
self->flags |= ENTITY_FACING_LEFT;
} else if (dist > 2.0f) {
body->pos.x += FLYER_SPEED * dt;
self->flags &= ~ENTITY_FACING_LEFT;
}
/* Shoot at player */
fd->shoot_timer -= dt;
if (fd->shoot_timer <= 0 && s_flyer_em) {
fd->shoot_timer = FLYER_SHOOT_CD;
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
float bx = facing_left ?
body->pos.x - 4.0f :
body->pos.x + body->size.x;
float by = body->pos.y + body->size.y * 0.4f;
projectile_spawn(s_flyer_em, vec2(bx, by), facing_left, false);
}
}
}
/* Animation */
animation_set(&self->anim, &anim_flyer_fly);
animation_update(&self->anim, dt);
}
static void flyer_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
Vec2 render_pos = vec2(
body->pos.x - 1.0f,
body->pos.y - 2.0f
);
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = render_pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = (self->flags & ENTITY_FACING_LEFT) != 0,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
SDL_Color color = {150, 50, 180, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void flyer_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
void flyer_register(EntityManager *em) {
entity_register(em, ENT_ENEMY_FLYER, flyer_update, flyer_render, flyer_destroy);
s_flyer_em = em;
}
Entity *flyer_spawn(EntityManager *em, Vec2 pos) {
Entity *e = entity_spawn(em, ENT_ENEMY_FLYER, pos);
if (!e) return NULL;
e->body.size = vec2(FLYER_WIDTH, FLYER_HEIGHT);
e->body.gravity_scale = 0.0f; /* flyers don't fall */
e->health = FLYER_HEALTH;
e->max_health = FLYER_HEALTH;
e->damage = 1;
FlyerData *fd = calloc(1, sizeof(FlyerData));
fd->base_y = pos.y;
fd->death_timer = 0.5f;
fd->shoot_timer = FLYER_SHOOT_CD;
e->data = fd;
return e;
}

47
src/game/enemy.h Normal file
View File

@@ -0,0 +1,47 @@
#ifndef JNR_ENEMY_H
#define JNR_ENEMY_H
#include "engine/entity.h"
#include "engine/camera.h"
#include "engine/tilemap.h"
/* ── Grunt enemy ───────────────────────────────────── */
/* A ground-patrolling enemy that walks back and forth */
#define GRUNT_WIDTH 12
#define GRUNT_HEIGHT 16
#define GRUNT_SPEED 40.0f /* horizontal walk speed (px/s) */
#define GRUNT_HEALTH 2
typedef struct GruntData {
float patrol_dir; /* 1.0 or -1.0 */
float death_timer; /* countdown after dying */
} GruntData;
void grunt_register(EntityManager *em);
Entity *grunt_spawn(EntityManager *em, Vec2 pos);
/* ── Flyer enemy ───────────────────────────────────── */
/* A flying enemy that bobs up and down, moves toward */
/* the player when in range */
#define FLYER_WIDTH 14
#define FLYER_HEIGHT 12
#define FLYER_SPEED 50.0f /* horizontal chase speed (px/s) */
#define FLYER_BOB_AMP 20.0f /* vertical bob amplitude */
#define FLYER_BOB_SPD 2.5f /* bob frequency (radians/s) */
#define FLYER_HEALTH 1
#define FLYER_DETECT 120.0f /* detection range (px) */
#define FLYER_SHOOT_CD 2.0f /* seconds between shots */
typedef struct FlyerData {
float bob_timer; /* for sine wave bobbing */
float base_y; /* original y position */
float death_timer;
float shoot_timer; /* cooldown for shooting */
} FlyerData;
void flyer_register(EntityManager *em);
Entity *flyer_spawn(EntityManager *em, Vec2 pos);
#endif /* JNR_ENEMY_H */

405
src/game/level.c Normal file
View File

@@ -0,0 +1,405 @@
#include "game/level.h"
#include "game/player.h"
#include "game/enemy.h"
#include "game/projectile.h"
#include "game/sprites.h"
#include "engine/core.h"
#include "engine/renderer.h"
#include "engine/physics.h"
#include "engine/particle.h"
#include "engine/audio.h"
#include "engine/input.h"
#include "engine/camera.h"
#include "engine/assets.h"
#include <stdio.h>
#include <string.h>
/* ── Sound effects ───────────────────────────────── */
static Sound s_sfx_hit;
static Sound s_sfx_enemy_death;
static bool s_sfx_loaded = false;
bool level_load(Level *level, const char *path) {
memset(level, 0, sizeof(Level));
/* Initialize subsystems */
entity_manager_init(&level->entities);
camera_init(&level->camera, SCREEN_WIDTH, SCREEN_HEIGHT);
particle_init();
/* Load combat sound effects */
if (!s_sfx_loaded) {
s_sfx_hit = audio_load_sound("assets/sounds/hitHurt.wav");
s_sfx_enemy_death = audio_load_sound("assets/sounds/teleport.wav");
s_sfx_loaded = true;
}
/* Generate spritesheet */
sprites_init_anims();
if (!sprites_generate(g_engine.renderer)) {
fprintf(stderr, "Warning: failed to generate spritesheet\n");
}
/* Register entity types */
player_register(&level->entities);
player_set_entity_manager(&level->entities);
grunt_register(&level->entities);
flyer_register(&level->entities);
projectile_register(&level->entities);
/* Load tilemap */
if (!tilemap_load(&level->map, path, g_engine.renderer)) {
return false;
}
/* Apply level gravity (0 = use default) */
if (level->map.gravity > 0) {
physics_set_gravity(level->map.gravity);
} else {
physics_set_gravity(DEFAULT_GRAVITY);
}
/* Apply level background color */
if (level->map.has_bg_color) {
renderer_set_clear_color(level->map.bg_color);
}
/* Initialize parallax backgrounds */
parallax_init(&level->parallax);
if (level->map.parallax_far_path[0]) {
SDL_Texture *far_tex = assets_get_texture(level->map.parallax_far_path);
if (far_tex) parallax_set_far(&level->parallax, far_tex, 0.05f, 0.05f);
}
if (level->map.parallax_near_path[0]) {
SDL_Texture *near_tex = assets_get_texture(level->map.parallax_near_path);
if (near_tex) parallax_set_near(&level->parallax, near_tex, 0.15f, 0.10f);
}
/* Generate procedural backgrounds for any layers not loaded from file */
if (!level->parallax.far_layer.active) {
parallax_generate_stars(&level->parallax, g_engine.renderer);
}
if (!level->parallax.near_layer.active) {
parallax_generate_nebula(&level->parallax, g_engine.renderer);
}
/* Set camera bounds to level size */
camera_set_bounds(&level->camera,
(float)(level->map.width * TILE_SIZE),
(float)(level->map.height * TILE_SIZE));
/* Spawn player at map spawn point */
Entity *player = player_spawn(&level->entities, level->map.player_spawn);
if (!player) {
fprintf(stderr, "Failed to spawn player!\n");
return false;
}
/* Spawn entities from level data */
for (int i = 0; i < level->map.entity_spawn_count; i++) {
EntitySpawn *es = &level->map.entity_spawns[i];
Vec2 pos = vec2(es->x, es->y);
if (strcmp(es->type_name, "grunt") == 0) {
grunt_spawn(&level->entities, pos);
} else if (strcmp(es->type_name, "flyer") == 0) {
flyer_spawn(&level->entities, pos);
} else {
fprintf(stderr, "Unknown entity type: %s\n", es->type_name);
}
}
/* Load level music (playback deferred to first update —
* browsers require user interaction before playing audio) */
if (level->map.music_path[0]) {
level->music = audio_load_music(level->map.music_path);
}
level->music_started = false;
printf("Level loaded successfully.\n");
return true;
}
/* ── Collision handling ──────────────────────────── */
/* Forward declaration for shake access */
static Camera *s_active_camera = NULL;
static void damage_entity(Entity *target, int damage) {
target->health -= damage;
if (target->health <= 0) {
target->flags |= ENTITY_DEAD;
/* Death particles — centered on entity */
Vec2 center = vec2(
target->body.pos.x + target->body.size.x * 0.5f,
target->body.pos.y + target->body.size.y * 0.5f
);
SDL_Color death_color;
if (target->type == ENT_ENEMY_GRUNT) {
death_color = (SDL_Color){200, 60, 60, 255}; /* red debris */
} else if (target->type == ENT_ENEMY_FLYER) {
death_color = (SDL_Color){140, 80, 200, 255}; /* purple puff */
} else {
death_color = (SDL_Color){200, 200, 200, 255}; /* grey */
}
particle_emit_death_puff(center, death_color);
/* Screen shake on kill */
if (s_active_camera) {
camera_shake(s_active_camera, 2.0f, 0.15f);
}
audio_play_sound(s_sfx_enemy_death, 80);
}
}
static void damage_player(Entity *player, int damage, Entity *source) {
PlayerData *ppd = (PlayerData *)player->data;
damage_entity(player, damage);
/* Screen shake on player hit (stronger) */
if (s_active_camera) {
camera_shake(s_active_camera, 4.0f, 0.2f);
}
audio_play_sound(s_sfx_hit, 100);
if (player->health > 0 && ppd) {
ppd->inv_timer = PLAYER_INV_TIME;
player->flags |= ENTITY_INVINCIBLE;
/* Knockback away from source */
if (source) {
float knock_dir = (player->body.pos.x < source->body.pos.x) ?
-1.0f : 1.0f;
player->body.vel.x = knock_dir * 150.0f;
player->body.vel.y = -150.0f;
}
}
}
static bool is_enemy(const Entity *e) {
return e->type == ENT_ENEMY_GRUNT || e->type == ENT_ENEMY_FLYER;
}
static void handle_collisions(EntityManager *em) {
/* Find the player */
Entity *player = NULL;
for (int i = 0; i < em->count; i++) {
Entity *e = &em->entities[i];
if (e->active && e->type == ENT_PLAYER && !(e->flags & ENTITY_DEAD)) {
player = e;
break;
}
}
for (int i = 0; i < em->count; i++) {
Entity *a = &em->entities[i];
if (!a->active) continue;
/* ── Projectile vs entities ──────────── */
if (a->type == ENT_PROJECTILE) {
if (projectile_is_impacting(a)) continue;
bool from_player = projectile_is_from_player(a);
for (int j = 0; j < em->count; j++) {
Entity *b = &em->entities[j];
if (!b->active || b == a) continue;
if (b->flags & ENTITY_DEAD) continue;
bool hit = false;
/* Player bullet hits enemies */
if (from_player && is_enemy(b)) {
if (physics_overlap(&a->body, &b->body)) {
damage_entity(b, a->damage);
hit = true;
}
}
/* Enemy bullet hits player */
if (!from_player && b->type == ENT_PLAYER &&
!(b->flags & ENTITY_INVINCIBLE)) {
if (physics_overlap(&a->body, &b->body)) {
damage_player(b, a->damage, NULL);
hit = true;
}
}
if (hit) {
projectile_hit(a);
/* If projectile was destroyed or is impacting, stop checking */
if (!a->active || projectile_is_impacting(a)) break;
}
}
}
/* ── Enemy contact damage to player ──── */
if (player && !(player->flags & ENTITY_INVINCIBLE) &&
is_enemy(a) && !(a->flags & ENTITY_DEAD)) {
if (physics_overlap(&a->body, &player->body)) {
/* Check if player is stomping (falling onto enemy from above) */
bool stomping = (player->body.vel.y > 0) &&
(player->body.pos.y + player->body.size.y <
a->body.pos.y + a->body.size.y * 0.5f);
if (stomping) {
damage_entity(a, 2);
player->body.vel.y = -PLAYER_JUMP_FORCE * 0.7f;
} else {
damage_player(player, a->damage, a);
}
}
}
}
}
void level_update(Level *level, float dt) {
/* Start music on first update (deferred so browser audio context
* is unlocked by the first user interaction / keypress) */
if (!level->music_started && level->music.music) {
audio_set_music_volume(64);
audio_play_music(level->music, true);
level->music_started = true;
}
/* Set camera pointer for collision shake triggers */
s_active_camera = &level->camera;
/* Update all entities */
entity_update_all(&level->entities, dt, &level->map);
/* Handle collisions */
handle_collisions(&level->entities);
/* Check for player respawn */
for (int i = 0; i < level->entities.count; i++) {
Entity *e = &level->entities.entities[i];
if (e->active && e->type == ENT_PLAYER && player_wants_respawn(e)) {
player_respawn(e, level->map.player_spawn);
/* Re-snap camera to player immediately */
Vec2 center = vec2(
e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f
);
camera_follow(&level->camera, center, vec2_zero(), dt);
break;
}
}
/* Update particles */
particle_update(dt);
/* Find player for camera tracking */
for (int i = 0; i < level->entities.count; i++) {
Entity *e = &level->entities.entities[i];
if (e->active && e->type == ENT_PLAYER) {
float look_offset = player_get_look_up_offset(e);
Vec2 center = vec2(
e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f + look_offset
);
camera_follow(&level->camera, center, e->body.vel, dt);
break;
}
}
/* Update screen shake */
camera_update_shake(&level->camera, dt);
}
void level_render(Level *level, float interpolation) {
(void)interpolation; /* TODO: use for render interpolation */
Camera *cam = &level->camera;
/* Render parallax backgrounds (behind everything) */
parallax_render(&level->parallax, cam, g_engine.renderer);
/* Render tile layers */
tilemap_render_layer(&level->map, level->map.bg_layer,
cam, g_engine.renderer);
tilemap_render_layer(&level->map, level->map.collision_layer,
cam, g_engine.renderer);
/* Render entities */
entity_render_all(&level->entities, cam);
/* Render particles (between entities and foreground) */
particle_render(cam);
/* Render foreground tiles */
tilemap_render_layer(&level->map, level->map.fg_layer,
cam, g_engine.renderer);
/* Render HUD - health display */
Entity *player = NULL;
for (int i = 0; i < level->entities.count; i++) {
Entity *e = &level->entities.entities[i];
if (e->active && e->type == ENT_PLAYER) {
player = e;
break;
}
}
if (player) {
/* Draw health hearts */
for (int i = 0; i < player->max_health; i++) {
SDL_Color heart_color;
if (i < player->health) {
heart_color = (SDL_Color){220, 50, 50, 255}; /* red = full */
} else {
heart_color = (SDL_Color){80, 80, 80, 255}; /* grey = empty */
}
Vec2 pos = vec2(8.0f + i * 14.0f, 8.0f);
Vec2 size = vec2(10.0f, 10.0f);
renderer_draw_rect(pos, size, heart_color, LAYER_HUD, cam);
}
/* Draw jetpack charge indicators */
int charges, max_charges;
float recharge_pct;
if (player_get_dash_charges(player, &charges, &max_charges, &recharge_pct)) {
for (int i = 0; i < max_charges; i++) {
float bx = 8.0f + i * 10.0f;
float by = 22.0f;
float bw = 7.0f;
float bh = 5.0f;
/* Background (empty slot) */
renderer_draw_rect(vec2(bx, by), vec2(bw, bh),
(SDL_Color){50, 50, 60, 255}, LAYER_HUD, cam);
if (i < charges) {
/* Full charge — bright orange */
renderer_draw_rect(vec2(bx, by), vec2(bw, bh),
(SDL_Color){255, 180, 50, 255}, LAYER_HUD, cam);
} else if (i == charges) {
/* Currently recharging — partial fill */
float fill = recharge_pct * bw;
if (fill > 0.5f) {
renderer_draw_rect(vec2(bx, by), vec2(fill, bh),
(SDL_Color){200, 140, 40, 180}, LAYER_HUD, cam);
}
}
}
}
}
/* Flush the renderer */
renderer_flush(cam);
}
void level_free(Level *level) {
audio_stop_music();
entity_manager_clear(&level->entities);
particle_clear();
parallax_free(&level->parallax);
tilemap_free(&level->map);
/* Free spritesheet */
if (g_spritesheet) {
SDL_DestroyTexture(g_spritesheet);
g_spritesheet = NULL;
}
}

24
src/game/level.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef JNR_LEVEL_H
#define JNR_LEVEL_H
#include "engine/tilemap.h"
#include "engine/entity.h"
#include "engine/camera.h"
#include "engine/audio.h"
#include "engine/parallax.h"
typedef struct Level {
Tilemap map;
EntityManager entities;
Camera camera;
Parallax parallax;
Music music;
bool music_started;
} Level;
bool level_load(Level *level, const char *path);
void level_update(Level *level, float dt);
void level_render(Level *level, float interpolation);
void level_free(Level *level);
#endif /* JNR_LEVEL_H */

650
src/game/player.c Normal file
View File

@@ -0,0 +1,650 @@
#include "game/player.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/input.h"
#include "engine/physics.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include "engine/audio.h"
#include "engine/assets.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>
static EntityManager *s_em = NULL;
/* ── Sound effects ───────────────────────────────── */
static Sound s_sfx_jump;
static Sound s_sfx_shoot;
static Sound s_sfx_dash;
static bool s_sfx_loaded = false;
/* ── Weapon sprite ───────────────────────────────── */
static SDL_Texture *s_weapon_tex = NULL;
static SDL_Rect s_weapon_src = {0}; /* cropped source rect */
static float s_weapon_render_w = 0; /* display width in game px */
static float s_weapon_render_h = 0; /* display height in game px */
/* Max camera offset when looking up (pixels) */
#define LOOK_UP_OFFSET 80.0f
#define LOOK_UP_DELAY 0.3f /* seconds of holding up before camera pans */
#define LOOK_UP_SPEED 3.0f /* camera pan speed multiplier */
/* Respawn timing */
#define RESPAWN_DELAY 1.0f /* seconds after death anim before respawn */
/* ── Astronaut sprite animations ─────────────────── */
/* Built from the PNG spritesheets at load time */
static bool s_anims_loaded = false;
/* Idle: 144x24, 6 frames of 24x24 */
static AnimFrame s_idle_frames[6];
static AnimDef s_anim_idle;
/* Run: 144x24, 6 frames of 24x24 */
static AnimFrame s_run_frames[6];
static AnimDef s_anim_run;
/* Jump: 120x24, 5 frames of 24x24 */
/* We split this: first 3 = ascending, last 2 = falling */
static AnimFrame s_jump_frames[3];
static AnimDef s_anim_jump;
static AnimFrame s_fall_frames[2];
static AnimDef s_anim_fall;
/* Death: 128x32, 4 frames of 32x32 */
static AnimFrame s_death_frames[4];
static AnimDef s_anim_death;
static void build_strip_frames(AnimFrame *out, int count, int frame_w, int frame_h,
float duration) {
for (int i = 0; i < count; i++) {
out[i].src = (SDL_Rect){ i * frame_w, 0, frame_w, frame_h };
out[i].duration = duration;
}
}
static void load_astronaut_anims(void) {
if (s_anims_loaded) return;
SDL_Texture *tex_idle = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Idle.png");
SDL_Texture *tex_run = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Run.png");
SDL_Texture *tex_jump = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Jump.png");
SDL_Texture *tex_death = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Death.png");
/* Idle: 6 frames, 24x24 */
build_strip_frames(s_idle_frames, 6, PLAYER_SPRITE_W, PLAYER_SPRITE_H, 0.15f);
s_anim_idle = (AnimDef){
.frames = s_idle_frames,
.frame_count = 6,
.looping = true,
.texture = tex_idle,
};
/* Run: 6 frames, 24x24 */
build_strip_frames(s_run_frames, 6, PLAYER_SPRITE_W, PLAYER_SPRITE_H, 0.1f);
s_anim_run = (AnimDef){
.frames = s_run_frames,
.frame_count = 6,
.looping = true,
.texture = tex_run,
};
/* Jump (ascending): first 3 frames from jump sheet */
for (int i = 0; i < 3; i++) {
s_jump_frames[i].src = (SDL_Rect){ i * PLAYER_SPRITE_W, 0,
PLAYER_SPRITE_W, PLAYER_SPRITE_H };
s_jump_frames[i].duration = 0.12f;
}
s_anim_jump = (AnimDef){
.frames = s_jump_frames,
.frame_count = 3,
.looping = false,
.texture = tex_jump,
};
/* Fall (descending): last 2 frames from jump sheet */
for (int i = 0; i < 2; i++) {
s_fall_frames[i].src = (SDL_Rect){ (3 + i) * PLAYER_SPRITE_W, 0,
PLAYER_SPRITE_W, PLAYER_SPRITE_H };
s_fall_frames[i].duration = 0.15f;
}
s_anim_fall = (AnimDef){
.frames = s_fall_frames,
.frame_count = 2,
.looping = false,
.texture = tex_jump, /* same texture, different frames */
};
/* Death: 4 frames, 32x32 */
build_strip_frames(s_death_frames, 4, PLAYER_DEATH_SPRITE_W, PLAYER_DEATH_SPRITE_H, 0.2f);
s_anim_death = (AnimDef){
.frames = s_death_frames,
.frame_count = 4,
.looping = false,
.texture = tex_death,
};
s_anims_loaded = true;
/* Load weapon sprite (mac) */
if (!s_weapon_tex) {
s_weapon_tex = assets_get_texture("assets/sprites/weapons/mac.png");
if (s_weapon_tex) {
/* Crop to actual gun pixels within the 128x128 canvas */
s_weapon_src = (SDL_Rect){ 52, 51, 30, 17 };
/* Scale to fit the 24x24 character — gun ~12x7 game pixels */
s_weapon_render_w = 12.0f;
s_weapon_render_h = 7.0f;
}
}
/* Load player sound effects */
if (!s_sfx_loaded) {
s_sfx_jump = audio_load_sound("assets/sounds/jump.wav");
s_sfx_shoot = audio_load_sound("assets/sounds/laserShoot.wav");
s_sfx_dash = audio_load_sound("assets/sounds/dash.wav");
s_sfx_loaded = true;
}
}
/* ── Entity manager ref ──────────────────────────── */
void player_set_entity_manager(EntityManager *em) {
s_em = em;
}
void player_register(EntityManager *em) {
entity_register(em, ENT_PLAYER, player_update, player_render, player_destroy);
}
/* ── Update ──────────────────────────────────────── */
void player_update(Entity *self, float dt, const Tilemap *map) {
PlayerData *pd = (PlayerData *)self->data;
if (!pd) return;
/* Handle death */
if (self->flags & ENTITY_DEAD) {
animation_set(&self->anim, &s_anim_death);
animation_update(&self->anim, dt);
/* After death animation finishes, count down to respawn */
if (self->anim.finished) {
pd->respawn_timer -= dt;
}
return;
}
/* Fall off bottom of level = instant death */
float level_bottom = (float)(map->height * TILE_SIZE) + 64.0f;
if (self->body.pos.y > level_bottom) {
self->health = 0;
self->flags |= ENTITY_DEAD;
pd->respawn_timer = 0.3f; /* shorter delay for pit death */
return;
}
/* Update invincibility */
if (pd->inv_timer > 0) {
pd->inv_timer -= dt;
if (pd->inv_timer <= 0) {
pd->inv_timer = 0;
self->flags &= ~ENTITY_INVINCIBLE;
}
}
Body *body = &self->body;
/* ── Read directional input ──────────────── */
bool hold_left = input_held(ACTION_LEFT);
bool hold_right = input_held(ACTION_RIGHT);
bool hold_up = input_held(ACTION_UP);
bool hold_down = input_held(ACTION_DOWN);
/* ── Determine aim direction ─────────────── */
if (hold_up && (hold_left || hold_right)) {
pd->aim_dir = AIM_DIAG_UP;
} else if (hold_up) {
pd->aim_dir = AIM_UP;
} else {
pd->aim_dir = AIM_FORWARD;
}
/* ── Look up tracking ────────────────────── */
bool standing_still = body->on_ground && fabsf(body->vel.x) < 10.0f;
if (hold_up && standing_still && !hold_left && !hold_right) {
pd->look_up_timer += dt;
pd->looking_up = (pd->look_up_timer > LOOK_UP_DELAY);
} else {
pd->look_up_timer = 0;
pd->looking_up = false;
}
/* ── Dash / Jetpack ─────────────────────── */
/* Recharge jetpack charges over time */
if (pd->dash_charges < pd->dash_max_charges) {
pd->dash_recharge_timer -= dt;
if (pd->dash_recharge_timer <= 0) {
pd->dash_charges++;
/* Reset timer for next charge (if still not full) */
if (pd->dash_charges < pd->dash_max_charges) {
pd->dash_recharge_timer = PLAYER_DASH_RECHARGE;
} else {
pd->dash_recharge_timer = 0;
}
}
}
if (pd->dash_timer > 0) {
/* Currently dashing */
pd->dash_timer -= dt;
body->vel.x = pd->dash_dir.x * PLAYER_DASH_SPEED;
body->vel.y = pd->dash_dir.y * PLAYER_DASH_SPEED;
/* Jetpack trail particles every frame */
Vec2 exhaust_pos = vec2(
body->pos.x + body->size.x * 0.5f,
body->pos.y + body->size.y * 0.5f
);
particle_emit_jetpack_trail(exhaust_pos, pd->dash_dir);
/* Skip normal movement during dash */
physics_update(body, dt, map);
animation_update(&self->anim, dt);
return;
}
if (input_pressed(ACTION_DASH) && pd->dash_charges > 0) {
pd->dash_charges--;
pd->dash_recharge_timer = PLAYER_DASH_RECHARGE;
pd->dash_timer = PLAYER_DASH_DURATION;
/* Determine dash direction from input */
Vec2 dash = vec2_zero();
if (hold_left) dash.x = -1.0f;
if (hold_right) dash.x = 1.0f;
if (hold_up) dash.y = -1.0f;
if (hold_down && !body->on_ground) dash.y = 1.0f;
/* Default: dash in facing direction */
if (dash.x == 0.0f && dash.y == 0.0f) {
dash.x = (self->flags & ENTITY_FACING_LEFT) ? -1.0f : 1.0f;
}
pd->dash_dir = vec2_norm(dash);
/* Grant brief invincibility during dash */
pd->inv_timer = PLAYER_DASH_DURATION;
self->flags |= ENTITY_INVINCIBLE;
/* Cancel vertical velocity for upward/horizontal dashes */
if (pd->dash_dir.y <= 0) {
body->vel.y = 0;
}
/* Jetpack burst at dash start */
Vec2 exhaust_pos = vec2(
body->pos.x + body->size.x * 0.5f,
body->pos.y + body->size.y * 0.5f
);
particle_emit_jetpack_burst(exhaust_pos, pd->dash_dir);
audio_play_sound(s_sfx_dash, 96);
return;
}
/* ── Horizontal movement ─────────────────── */
float target_vx = 0.0f;
if (hold_left) target_vx -= PLAYER_SPEED;
if (hold_right) target_vx += PLAYER_SPEED;
/* Set facing direction */
if (target_vx < 0) self->flags |= ENTITY_FACING_LEFT;
if (target_vx > 0) self->flags &= ~ENTITY_FACING_LEFT;
/* Acceleration / deceleration */
float accel = body->on_ground ? PLAYER_ACCEL : PLAYER_AIR_ACCEL;
if (target_vx != 0.0f) {
if (body->vel.x < target_vx) {
body->vel.x += accel * dt;
if (body->vel.x > target_vx) body->vel.x = target_vx;
} else if (body->vel.x > target_vx) {
body->vel.x -= accel * dt;
if (body->vel.x < target_vx) body->vel.x = target_vx;
}
} else {
float decel = body->on_ground ? PLAYER_DECEL : PLAYER_AIR_ACCEL;
if (body->vel.x > 0) {
body->vel.x -= decel * dt;
if (body->vel.x < 0) body->vel.x = 0;
} else if (body->vel.x < 0) {
body->vel.x += decel * dt;
if (body->vel.x > 0) body->vel.x = 0;
}
}
/* ── Coyote time ─────────────────────────── */
if (body->on_ground) {
pd->coyote_timer = PLAYER_COYOTE_TIME;
} else {
pd->coyote_timer -= dt;
}
/* ── Jump buffer ─────────────────────────── */
if (input_pressed(ACTION_JUMP)) {
pd->jump_buffer_timer = PLAYER_JUMP_BUFFER;
} else {
pd->jump_buffer_timer -= dt;
}
/* ── Jump execution ──────────────────────── */
if (pd->jump_buffer_timer > 0 && pd->coyote_timer > 0) {
body->vel.y = -PLAYER_JUMP_FORCE;
pd->jumping = true;
pd->jump_buffer_timer = 0;
pd->coyote_timer = 0;
audio_play_sound(s_sfx_jump, 96);
}
/* Variable jump height: cut velocity on release */
if (pd->jumping && input_released(ACTION_JUMP) && body->vel.y < 0) {
body->vel.y *= PLAYER_JUMP_CUT;
pd->jumping = false;
}
if (body->on_ground && body->vel.y >= 0) {
pd->jumping = false;
}
/* ── Shooting ────────────────────────────── */
pd->shoot_cooldown -= dt;
if (input_pressed(ACTION_SHOOT) && pd->shoot_cooldown <= 0 && s_em) {
pd->shoot_cooldown = PLAYER_SHOOT_COOLDOWN;
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
float forward = facing_left ? -1.0f : 1.0f;
Vec2 shoot_dir;
Vec2 bullet_pos;
switch (pd->aim_dir) {
case AIM_UP:
shoot_dir = vec2(0, -1.0f);
bullet_pos = vec2(
body->pos.x + body->size.x * 0.5f - 4.0f,
body->pos.y - 8.0f
);
break;
case AIM_DIAG_UP:
shoot_dir = vec2(forward, -1.0f);
bullet_pos = vec2(
facing_left ? body->pos.x - 4.0f : body->pos.x + body->size.x - 4.0f,
body->pos.y - 4.0f
);
break;
case AIM_FORWARD:
default:
shoot_dir = vec2(forward, 0);
bullet_pos = vec2(
facing_left ? body->pos.x - 8.0f : body->pos.x + body->size.x,
body->pos.y + body->size.y * 0.15f
);
break;
}
projectile_spawn_dir(s_em, bullet_pos, shoot_dir, true);
/* Muzzle flash slightly ahead of bullet origin (at barrel tip) */
Vec2 flash_pos = vec2(
bullet_pos.x + shoot_dir.x * 4.0f,
bullet_pos.y + shoot_dir.y * 4.0f + 3.0f
);
particle_emit_muzzle_flash(flash_pos, shoot_dir);
audio_play_sound(s_sfx_shoot, 80);
}
/* ── Physics ─────────────────────────────── */
physics_update(body, dt, map);
/* ── Landing detection ───────────────────── */
if (body->on_ground && !pd->was_on_ground) {
/* Just landed — emit dust at feet */
Vec2 feet = vec2(
body->pos.x + body->size.x * 0.5f,
body->pos.y + body->size.y
);
particle_emit_landing_dust(feet);
}
pd->was_on_ground = body->on_ground;
/* ── Animation ───────────────────────────── */
if (!body->on_ground) {
if (body->vel.y < 0) {
animation_set(&self->anim, &s_anim_jump);
} else {
animation_set(&self->anim, &s_anim_fall);
}
} else if (fabsf(body->vel.x) > 10.0f) {
animation_set(&self->anim, &s_anim_run);
} else {
animation_set(&self->anim, &s_anim_idle);
}
animation_update(&self->anim, dt);
}
/* ── Render ──────────────────────────────────────── */
void player_render(Entity *self, const Camera *cam) {
if (self->flags & ENTITY_DEAD) {
/* Render death animation if available */
if (!self->anim.def) return;
if (self->anim.finished) return;
}
PlayerData *pd = (PlayerData *)self->data;
Body *body = &self->body;
/* Flash during invincibility (but not during dash) */
if (pd && pd->inv_timer > 0 && pd->dash_timer <= 0) {
int blink = (int)(pd->inv_timer / 0.1f);
if (blink % 2) return;
}
/* Get texture from animation (per-anim texture) or fall back to global */
SDL_Texture *tex = animation_texture(&self->anim);
if (!tex) tex = g_spritesheet;
if (tex && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
/* Determine sprite size for this frame */
float spr_w = (float)src.w;
float spr_h = (float)src.h;
/* Center the sprite on the hitbox, compensating for bottom padding
* in the astronaut spritesheets (4px transparent below feet) */
Vec2 render_pos = vec2(
body->pos.x + body->size.x * 0.5f - spr_w * 0.5f,
body->pos.y + body->size.y - spr_h + 4.0f
);
Sprite spr = {
.texture = tex,
.src = src,
.pos = render_pos,
.size = vec2(spr_w, spr_h),
.flip_x = (self->flags & ENTITY_FACING_LEFT) != 0,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
.rotation = 0.0,
};
renderer_submit(&spr);
/* ── Weapon overlay ─────────────────── */
if (s_weapon_tex && !(self->flags & ENTITY_DEAD) && pd) {
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
/* Anchor gun to the player sprite position (not body pos)
* so it moves in exact lockstep — no sub-pixel jitter. */
float anchor_x = (float)(int)(render_pos.x + spr_w * 0.5f);
float anchor_y = (float)(int)(render_pos.y + spr_h * 0.5f);
/* Offset from sprite center to grip point */
float gun_offset_x = 6.0f; /* pixels in front of center */
float gun_offset_y = 1.0f; /* slightly below center */
/* Rotation angle based on aim direction */
double gun_rotation = 0.0;
switch (pd->aim_dir) {
case AIM_UP:
gun_rotation = -90.0;
gun_offset_x = 0.0f;
gun_offset_y = -4.0f;
break;
case AIM_DIAG_UP:
gun_rotation = -45.0;
gun_offset_x = 1.0f;
gun_offset_y = -3.0f;
break;
case AIM_FORWARD:
default:
gun_rotation = 0.0;
break;
}
/* Flip adjustments for facing left */
if (facing_left) {
gun_offset_x = -gun_offset_x;
gun_rotation = -gun_rotation;
}
/* Position gun relative to anchor, snapped to integer pixels */
Vec2 gun_pos = vec2(
(float)(int)(anchor_x + gun_offset_x - s_weapon_render_w * 0.5f),
(float)(int)(anchor_y + gun_offset_y - s_weapon_render_h * 0.5f)
);
Sprite gun = {
.texture = s_weapon_tex,
.src = s_weapon_src,
.pos = gun_pos,
.size = vec2(s_weapon_render_w, s_weapon_render_h),
.flip_x = facing_left,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
.rotation = gun_rotation,
};
renderer_submit(&gun);
}
} else {
/* Fallback: colored rectangle */
SDL_Color color;
if (body->on_ground) {
color = (SDL_Color){100, 200, 100, 255};
} else {
color = (SDL_Color){100, 150, 255, 255};
}
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
/* ── Lifecycle ───────────────────────────────────── */
void player_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
Entity *player_spawn(EntityManager *em, Vec2 pos) {
/* Ensure astronaut animations are loaded */
load_astronaut_anims();
Entity *e = entity_spawn(em, ENT_PLAYER, pos);
if (!e) return NULL;
e->body.size = vec2(PLAYER_WIDTH, PLAYER_HEIGHT);
e->body.gravity_scale = 1.0f;
e->health = 3;
e->max_health = 3;
PlayerData *pd = calloc(1, sizeof(PlayerData));
pd->dash_charges = PLAYER_DASH_MAX_CHARGES;
pd->dash_max_charges = PLAYER_DASH_MAX_CHARGES;
pd->respawn_timer = RESPAWN_DELAY;
pd->spawn_point = pos;
e->data = pd;
return e;
}
float player_get_look_up_offset(const Entity *self) {
if (!self || !self->data) return 0.0f;
const PlayerData *pd = (const PlayerData *)self->data;
if (!pd->looking_up) return 0.0f;
/* Smoothly ramp up the offset */
float t = (pd->look_up_timer - LOOK_UP_DELAY) * LOOK_UP_SPEED;
if (t > 1.0f) t = 1.0f;
return -LOOK_UP_OFFSET * t;
}
bool player_get_dash_charges(const Entity *self, int *charges, int *max_charges,
float *recharge_pct) {
if (!self || !self->data || self->type != ENT_PLAYER) return false;
const PlayerData *pd = (const PlayerData *)self->data;
if (charges) *charges = pd->dash_charges;
if (max_charges) *max_charges = pd->dash_max_charges;
if (recharge_pct) {
if (pd->dash_charges >= pd->dash_max_charges) {
*recharge_pct = 1.0f;
} else {
*recharge_pct = 1.0f - (pd->dash_recharge_timer / PLAYER_DASH_RECHARGE);
}
}
return true;
}
bool player_wants_respawn(const Entity *self) {
if (!self || !self->data || self->type != ENT_PLAYER) return false;
if (!(self->flags & ENTITY_DEAD)) return false;
const PlayerData *pd = (const PlayerData *)self->data;
return pd->respawn_timer <= 0;
}
void player_respawn(Entity *self, Vec2 pos) {
if (!self || !self->data) return;
PlayerData *pd = (PlayerData *)self->data;
/* Reset entity state */
self->health = self->max_health;
self->flags = 0; /* clear DEAD, INVINCIBLE, etc. */
self->body.pos = pos;
self->body.vel = vec2_zero();
/* Grant brief invincibility on respawn */
pd->inv_timer = PLAYER_INV_TIME;
self->flags |= ENTITY_INVINCIBLE;
/* Reset player-specific state */
pd->coyote_timer = 0;
pd->jump_buffer_timer = 0;
pd->jumping = false;
pd->was_on_ground = false;
pd->shoot_cooldown = 0;
pd->dash_timer = 0;
pd->dash_charges = pd->dash_max_charges;
pd->dash_recharge_timer = 0;
pd->aim_dir = AIM_FORWARD;
pd->looking_up = false;
pd->look_up_timer = 0;
pd->respawn_timer = RESPAWN_DELAY;
/* Reset animation */
animation_set(&self->anim, &s_anim_idle);
}

96
src/game/player.h Normal file
View File

@@ -0,0 +1,96 @@
#ifndef JNR_PLAYER_H
#define JNR_PLAYER_H
#include "engine/entity.h"
#include "engine/camera.h"
#include "engine/tilemap.h"
/* Player physics tuning */
#define PLAYER_SPEED 150.0f /* horizontal speed (px/s) */
#define PLAYER_ACCEL 1200.0f /* ground acceleration */
#define PLAYER_DECEL 1600.0f /* ground deceleration (friction)*/
#define PLAYER_AIR_ACCEL 600.0f /* air acceleration */
#define PLAYER_JUMP_FORCE 320.0f /* initial jump velocity */
#define PLAYER_JUMP_CUT 0.4f /* multiplier when releasing jump*/
#define PLAYER_COYOTE_TIME 0.08f /* seconds after leaving edge */
#define PLAYER_JUMP_BUFFER 0.1f /* seconds before landing */
#define PLAYER_WIDTH 12
#define PLAYER_HEIGHT 16
/* Sprite dimensions (art is larger than hitbox) */
#define PLAYER_SPRITE_W 24
#define PLAYER_SPRITE_H 24
#define PLAYER_DEATH_SPRITE_W 32
#define PLAYER_DEATH_SPRITE_H 32
/* Shooting */
#define PLAYER_SHOOT_COOLDOWN 0.12f /* seconds between shots */
/* Dash / Jetpack */
#define PLAYER_DASH_SPEED 350.0f /* dash velocity (px/s) */
#define PLAYER_DASH_DURATION 0.15f /* seconds the dash lasts */
#define PLAYER_DASH_MAX_CHARGES 3 /* max jetpack charges */
#define PLAYER_DASH_RECHARGE 3.0f /* seconds to recharge one charge*/
/* Invincibility after taking damage */
#define PLAYER_INV_TIME 1.5f /* seconds of invincibility */
/* Aim direction (for shooting) */
typedef enum AimDir {
AIM_FORWARD, /* horizontal, based on facing */
AIM_UP, /* straight up */
AIM_DIAG_UP, /* 45 degrees up + forward */
} AimDir;
typedef struct PlayerData {
float coyote_timer;
float jump_buffer_timer;
bool jumping;
bool was_on_ground; /* on_ground last frame (landing detect) */
float shoot_cooldown;
float inv_timer; /* invincibility timer */
/* Dash / Jetpack */
float dash_timer; /* remaining dash time (0=not dashing) */
int dash_charges; /* available jetpack charges */
int dash_max_charges; /* max charges (for HUD) */
float dash_recharge_timer; /* time until next charge restored*/
Vec2 dash_dir; /* direction of current dash */
/* Aiming */
AimDir aim_dir; /* current aim direction */
bool looking_up; /* holding up without moving */
float look_up_timer; /* how long up has been held */
/* Death / Respawn */
float respawn_timer; /* countdown after death anim finishes */
Vec2 spawn_point; /* where to respawn */
} PlayerData;
/* Register player entity type with the entity manager */
void player_register(EntityManager *em);
/* Entity callbacks */
void player_update(Entity *self, float dt, const Tilemap *map);
void player_render(Entity *self, const Camera *cam);
void player_destroy(Entity *self);
/* Spawn a fully configured player entity */
Entity *player_spawn(EntityManager *em, Vec2 pos);
/* Set the entity manager the player lives in (for spawning projectiles) */
void player_set_entity_manager(EntityManager *em);
/* Get the player's current look-up offset for the camera */
float player_get_look_up_offset(const Entity *self);
/* Get jetpack dash charge info for HUD (returns false if entity is not player) */
bool player_get_dash_charges(const Entity *self, int *charges, int *max_charges,
float *recharge_pct);
/* Check if the player is requesting a respawn (death anim finished + timer expired).
* Returns true when respawn should occur. */
bool player_wants_respawn(const Entity *self);
/* Reset the player to alive state at the given position */
void player_respawn(Entity *self, Vec2 pos);
#endif /* JNR_PLAYER_H */

430
src/game/projectile.c Normal file
View File

@@ -0,0 +1,430 @@
#include "game/projectile.h"
#include "game/sprites.h"
#include "engine/physics.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include <stdlib.h>
#include <math.h>
static EntityManager *s_proj_em = NULL;
/* ════════════════════════════════════════════════════
* Built-in weapon definitions
* ════════════════════════════════════════════════════ */
const ProjectileDef WEAPON_PLASMA = {
.name = "plasma",
.speed = 400.0f,
.damage = 1,
.lifetime = 2.0f,
.gravity_scale = 0.0f,
.pierce_count = 0,
.bounce_count = 0,
.homing_strength = 0.0f,
.hitbox_w = 8.0f,
.hitbox_h = 8.0f,
.flags = 0,
.anim_fly = NULL, /* set in projectile_register after sprites_init_anims */
.anim_impact = NULL,
};
const ProjectileDef WEAPON_SPREAD = {
.name = "spread",
.speed = 350.0f,
.damage = 1,
.lifetime = 0.8f, /* short range */
.gravity_scale = 0.0f,
.pierce_count = 0,
.bounce_count = 0,
.homing_strength = 0.0f,
.hitbox_w = 6.0f,
.hitbox_h = 6.0f,
.flags = 0,
.anim_fly = NULL,
.anim_impact = NULL,
};
const ProjectileDef WEAPON_LASER = {
.name = "laser",
.speed = 600.0f,
.damage = 1,
.lifetime = 1.5f,
.gravity_scale = 0.0f,
.pierce_count = 3, /* passes through 3 enemies */
.bounce_count = 0,
.homing_strength = 0.0f,
.hitbox_w = 10.0f,
.hitbox_h = 4.0f,
.flags = PROJ_PIERCING,
.anim_fly = NULL,
.anim_impact = NULL,
};
const ProjectileDef WEAPON_ROCKET = {
.name = "rocket",
.speed = 200.0f,
.damage = 3,
.lifetime = 3.0f,
.gravity_scale = 0.1f, /* slight drop */
.pierce_count = 0,
.bounce_count = 0,
.homing_strength = 0.0f,
.hitbox_w = 10.0f,
.hitbox_h = 6.0f,
.flags = PROJ_GRAVITY,
.anim_fly = NULL,
.anim_impact = NULL,
};
const ProjectileDef WEAPON_BOUNCE = {
.name = "bounce",
.speed = 300.0f,
.damage = 1,
.lifetime = 4.0f,
.gravity_scale = 0.5f,
.pierce_count = 0,
.bounce_count = 3, /* bounces off 3 walls */
.homing_strength = 0.0f,
.hitbox_w = 6.0f,
.hitbox_h = 6.0f,
.flags = PROJ_BOUNCY | PROJ_GRAVITY,
.anim_fly = NULL,
.anim_impact = NULL,
};
const ProjectileDef WEAPON_ENEMY_FIRE = {
.name = "enemy_fire",
.speed = 180.0f,
.damage = 1,
.lifetime = 3.0f,
.gravity_scale = 0.0f,
.pierce_count = 0,
.bounce_count = 0,
.homing_strength = 0.0f,
.hitbox_w = 8.0f,
.hitbox_h = 8.0f,
.flags = 0,
.anim_fly = NULL,
.anim_impact = NULL,
};
/* ── Mutable copies with animation pointers ────────── */
/* We need mutable copies because the AnimDef pointers */
/* aren't available at compile time (set after init). */
static ProjectileDef s_weapon_plasma;
static ProjectileDef s_weapon_spread;
static ProjectileDef s_weapon_laser;
static ProjectileDef s_weapon_rocket;
static ProjectileDef s_weapon_bounce;
static ProjectileDef s_weapon_enemy_fire;
static void init_weapon_defs(void) {
s_weapon_plasma = WEAPON_PLASMA;
s_weapon_plasma.anim_fly = &anim_bullet;
s_weapon_plasma.anim_impact = &anim_bullet_impact;
s_weapon_spread = WEAPON_SPREAD;
s_weapon_spread.anim_fly = &anim_bullet;
s_weapon_spread.anim_impact = &anim_bullet_impact;
s_weapon_laser = WEAPON_LASER;
s_weapon_laser.anim_fly = &anim_bullet;
s_weapon_laser.anim_impact = &anim_bullet_impact;
s_weapon_rocket = WEAPON_ROCKET;
s_weapon_rocket.anim_fly = &anim_bullet;
s_weapon_rocket.anim_impact = &anim_bullet_impact;
s_weapon_bounce = WEAPON_BOUNCE;
s_weapon_bounce.anim_fly = &anim_bullet;
s_weapon_bounce.anim_impact = &anim_bullet_impact;
s_weapon_enemy_fire = WEAPON_ENEMY_FIRE;
s_weapon_enemy_fire.anim_fly = &anim_enemy_bullet;
s_weapon_enemy_fire.anim_impact = &anim_bullet_impact;
}
/* ════════════════════════════════════════════════════
* Projectile entity callbacks
* ════════════════════════════════════════════════════ */
static void resolve_wall_bounce(Body *body, const Tilemap *map, int *bounces_left) {
/* Check if the center of the projectile is inside a solid tile */
int cx = world_to_tile(body->pos.x + body->size.x * 0.5f);
int cy = world_to_tile(body->pos.y + body->size.y * 0.5f);
if (!tilemap_is_solid(map, cx, cy)) return;
if (*bounces_left <= 0) return;
(*bounces_left)--;
/* Determine bounce axis by checking neighboring tiles */
int left_tile = world_to_tile(body->pos.x);
int right_tile = world_to_tile(body->pos.x + body->size.x);
int top_tile = world_to_tile(body->pos.y);
int bot_tile = world_to_tile(body->pos.y + body->size.y);
bool hit_h = tilemap_is_solid(map, cx, top_tile - 1) == false &&
tilemap_is_solid(map, cx, bot_tile + 1) == false;
bool hit_v = tilemap_is_solid(map, left_tile - 1, cy) == false &&
tilemap_is_solid(map, right_tile + 1, cy) == false;
if (hit_h || (!hit_h && !hit_v)) {
body->vel.y = -body->vel.y;
}
if (hit_v || (!hit_h && !hit_v)) {
body->vel.x = -body->vel.x;
}
/* Push out of solid tile */
body->pos.x += body->vel.x * DT;
body->pos.y += body->vel.y * DT;
}
static void projectile_update(Entity *self, float dt, const Tilemap *map) {
ProjectileData *pd = (ProjectileData *)self->data;
if (!pd || !pd->def) return;
const ProjectileDef *def = pd->def;
Body *body = &self->body;
/* ── Impact animation phase ──────────────── */
if (pd->proj_flags & PROJ_IMPACT) {
animation_update(&self->anim, dt);
if (self->anim.finished) {
entity_destroy(s_proj_em, self);
}
return;
}
/* ── Apply gravity if flagged ────────────── */
if (def->flags & PROJ_GRAVITY) {
body->vel.y += physics_get_gravity() * def->gravity_scale * dt;
if (body->vel.y > MAX_FALL_SPEED) body->vel.y = MAX_FALL_SPEED;
}
/* ── Move ────────────────────────────────── */
body->pos.x += body->vel.x * dt;
body->pos.y += body->vel.y * dt;
/* ── Tilemap collision ───────────────────── */
int cx = world_to_tile(body->pos.x + body->size.x * 0.5f);
int cy = world_to_tile(body->pos.y + body->size.y * 0.5f);
if (tilemap_is_solid(map, cx, cy)) {
if ((def->flags & PROJ_BOUNCY) && pd->bounces_left > 0) {
resolve_wall_bounce(body, map, &pd->bounces_left);
} else {
projectile_hit(self);
return;
}
}
/* ── Lifetime ────────────────────────────── */
pd->lifetime -= dt;
if (pd->lifetime <= 0) {
entity_destroy(s_proj_em, self);
return;
}
/* ── Homing ──────────────────────────────── */
if ((def->flags & PROJ_HOMING) && def->homing_strength > 0 && s_proj_em) {
/* Find nearest valid target */
bool is_player_proj = (pd->proj_flags & PROJ_FROM_PLAYER) != 0;
Entity *best = NULL;
float best_dist = 99999.0f;
Vec2 proj_center = vec2(
body->pos.x + body->size.x * 0.5f,
body->pos.y + body->size.y * 0.5f
);
for (int i = 0; i < s_proj_em->count; i++) {
Entity *e = &s_proj_em->entities[i];
if (!e->active || (e->flags & ENTITY_DEAD)) continue;
/* Player bullets target enemies, enemy bullets target player */
if (is_player_proj) {
if (e->type != ENT_ENEMY_GRUNT && e->type != ENT_ENEMY_FLYER) continue;
} else {
if (e->type != ENT_PLAYER) continue;
}
Vec2 target_center = vec2(
e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f
);
float d = vec2_dist(proj_center, target_center);
if (d < best_dist) {
best_dist = d;
best = e;
}
}
if (best) {
Vec2 target_center = vec2(
best->body.pos.x + best->body.size.x * 0.5f,
best->body.pos.y + best->body.size.y * 0.5f
);
Vec2 to_target = vec2_norm(vec2_sub(target_center, proj_center));
Vec2 cur_dir = vec2_norm(body->vel);
float spd = vec2_len(body->vel);
/* Steer toward target */
Vec2 new_dir = vec2_norm(vec2_lerp(cur_dir, to_target,
def->homing_strength * dt));
body->vel = vec2_scale(new_dir, spd);
}
}
/* ── Animation ───────────────────────────── */
animation_update(&self->anim, dt);
}
static void projectile_render(Entity *self, const Camera *cam) {
(void)cam;
Body *body = &self->body;
ProjectileData *pd = (ProjectileData *)self->data;
if (g_spritesheet && self->anim.def) {
SDL_Rect src = animation_current_rect(&self->anim);
/* Center the 16x16 sprite on the smaller hitbox */
Vec2 render_pos = vec2(
body->pos.x + body->size.x * 0.5f - SPRITE_CELL * 0.5f,
body->pos.y + body->size.y * 0.5f - SPRITE_CELL * 0.5f
);
bool flip_x = false;
bool flip_y = false;
if (pd && !(pd->proj_flags & PROJ_IMPACT)) {
flip_x = (body->vel.x < 0);
/* Flip vertically for downward-only projectiles */
flip_y = (body->vel.y > 0 && fabsf(body->vel.x) < 1.0f);
}
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = render_pos,
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = flip_x,
.flip_y = flip_y,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
} else {
/* Fallback colored rectangle */
SDL_Color color = (pd && (pd->proj_flags & PROJ_FROM_PLAYER)) ?
(SDL_Color){100, 220, 255, 255} :
(SDL_Color){255, 180, 50, 255};
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
}
}
static void projectile_destroy_fn(Entity *self) {
free(self->data);
self->data = NULL;
}
/* ════════════════════════════════════════════════════
* Public API
* ════════════════════════════════════════════════════ */
void projectile_register(EntityManager *em) {
entity_register(em, ENT_PROJECTILE, projectile_update,
projectile_render, projectile_destroy_fn);
s_proj_em = em;
init_weapon_defs();
}
Entity *projectile_spawn_def(EntityManager *em, const ProjectileDef *def,
Vec2 pos, Vec2 dir, bool from_player) {
Entity *e = entity_spawn(em, ENT_PROJECTILE, pos);
if (!e) return NULL;
e->body.size = vec2(def->hitbox_w, def->hitbox_h);
e->body.gravity_scale = 0.0f; /* gravity handled manually via def */
/* Normalize direction and scale to projectile speed */
Vec2 norm_dir = vec2_norm(dir);
e->body.vel = vec2_scale(norm_dir, def->speed);
e->health = 1;
e->damage = def->damage;
ProjectileData *pd = calloc(1, sizeof(ProjectileData));
pd->def = def;
pd->lifetime = def->lifetime;
pd->pierces_left = def->pierce_count;
pd->bounces_left = def->bounce_count;
if (from_player) {
pd->proj_flags |= PROJ_FROM_PLAYER;
}
if (def->anim_fly) {
animation_set(&e->anim, def->anim_fly);
}
e->data = pd;
return e;
}
Entity *projectile_spawn_dir(EntityManager *em, Vec2 pos, Vec2 dir, bool from_player) {
const ProjectileDef *def = from_player ? &s_weapon_plasma : &s_weapon_enemy_fire;
return projectile_spawn_def(em, def, pos, dir, from_player);
}
Entity *projectile_spawn(EntityManager *em, Vec2 pos, bool facing_left, bool from_player) {
Vec2 dir = facing_left ? vec2(-1, 0) : vec2(1, 0);
return projectile_spawn_dir(em, pos, dir, from_player);
}
void projectile_hit(Entity *proj) {
if (!proj || !proj->data) return;
ProjectileData *pd = (ProjectileData *)proj->data;
/* If piercing and has pierces left, don't destroy */
if ((pd->def->flags & PROJ_PIERCING) && pd->pierces_left > 0) {
pd->pierces_left--;
return;
}
/* Emit impact sparks */
Vec2 hit_center = vec2(
proj->body.pos.x + proj->body.size.x * 0.5f,
proj->body.pos.y + proj->body.size.y * 0.5f
);
SDL_Color spark_color = (pd->proj_flags & PROJ_FROM_PLAYER) ?
(SDL_Color){100, 220, 255, 255} : /* cyan for player bullets */
(SDL_Color){255, 180, 50, 255}; /* orange for enemy bullets */
particle_emit_spark(hit_center, spark_color);
/* Switch to impact animation */
pd->proj_flags |= PROJ_IMPACT;
proj->body.vel = vec2(0, 0);
if (pd->def->anim_impact) {
animation_set(&proj->anim, pd->def->anim_impact);
proj->anim.current_frame = 0;
proj->anim.timer = 0;
proj->anim.finished = false;
} else {
/* No impact animation, destroy immediately */
entity_destroy(s_proj_em, proj);
}
}
bool projectile_is_impacting(const Entity *proj) {
if (!proj || !proj->data) return true;
const ProjectileData *pd = (const ProjectileData *)proj->data;
return (pd->proj_flags & PROJ_IMPACT) != 0;
}
bool projectile_is_from_player(const Entity *proj) {
if (!proj || !proj->data) return false;
const ProjectileData *pd = (const ProjectileData *)proj->data;
return (pd->proj_flags & PROJ_FROM_PLAYER) != 0;
}

75
src/game/projectile.h Normal file
View File

@@ -0,0 +1,75 @@
#ifndef JNR_PROJECTILE_H
#define JNR_PROJECTILE_H
#include "engine/entity.h"
#include "engine/camera.h"
#include "engine/tilemap.h"
#include "engine/animation.h"
/* ── Projectile behavior flags ─────────────────────── */
#define PROJ_FROM_PLAYER (1 << 0) /* owned by player (vs enemy) */
#define PROJ_IMPACT (1 << 1) /* currently playing impact anim */
#define PROJ_PIERCING (1 << 2) /* passes through enemies */
#define PROJ_BOUNCY (1 << 3) /* bounces off walls */
#define PROJ_GRAVITY (1 << 4) /* affected by gravity */
#define PROJ_HOMING (1 << 5) /* tracks nearest target */
/* ── Projectile definition (weapon type) ───────────── */
/* Describes how a class of projectile behaves. */
/* These are static data - shared by all instances. */
typedef struct ProjectileDef {
const char *name; /* e.g. "plasma", "rocket" */
float speed; /* travel speed (px/s) */
int damage; /* damage per hit */
float lifetime; /* seconds before auto-destroy */
float gravity_scale; /* 0 = no gravity, 1 = full */
int pierce_count; /* 0 = destroy on first hit */
int bounce_count; /* 0 = destroy on wall hit */
float homing_strength;/* 0 = none, higher = tighter turn */
float hitbox_w; /* collision box width */
float hitbox_h; /* collision box height */
uint32_t flags; /* PROJ_* behavior flags */
const AnimDef *anim_fly; /* animation while flying */
const AnimDef *anim_impact; /* animation on hit (NULL = instant) */
} ProjectileDef;
/* ── Per-instance data ─────────────────────────────── */
typedef struct ProjectileData {
const ProjectileDef *def; /* shared definition */
uint32_t proj_flags; /* runtime flags (PROJ_FROM_PLAYER, PROJ_IMPACT) */
float lifetime; /* remaining lifetime */
int pierces_left; /* remaining pierces */
int bounces_left; /* remaining bounces */
} ProjectileData;
/* ── Built-in weapon definitions ───────────────────── */
extern const ProjectileDef WEAPON_PLASMA; /* player default */
extern const ProjectileDef WEAPON_SPREAD; /* 3-way fan */
extern const ProjectileDef WEAPON_LASER; /* fast, piercing */
extern const ProjectileDef WEAPON_ROCKET; /* slow, high damage */
extern const ProjectileDef WEAPON_BOUNCE; /* ricochets off walls */
extern const ProjectileDef WEAPON_ENEMY_FIRE; /* enemy fireball */
/* ── API ───────────────────────────────────────────── */
void projectile_register(EntityManager *em);
/* Spawn a projectile from a definition + direction vector */
Entity *projectile_spawn_def(EntityManager *em, const ProjectileDef *def,
Vec2 pos, Vec2 dir, bool from_player);
/* Convenience: spawn with default player/enemy weapon */
Entity *projectile_spawn_dir(EntityManager *em, Vec2 pos, Vec2 dir, bool from_player);
/* Legacy convenience: horizontal shot */
Entity *projectile_spawn(EntityManager *em, Vec2 pos, bool facing_left, bool from_player);
/* Trigger impact on a projectile (used by collision system) */
void projectile_hit(Entity *proj);
/* Check if a projectile is currently impacting (not collidable) */
bool projectile_is_impacting(const Entity *proj);
/* Check if a projectile belongs to the player */
bool projectile_is_from_player(const Entity *proj);
#endif /* JNR_PROJECTILE_H */

728
src/game/sprites.c Normal file
View File

@@ -0,0 +1,728 @@
#include "game/sprites.h"
#include <string.h>
#include <stdio.h>
SDL_Texture *g_spritesheet = NULL;
/* ── Pixel art data ─────────────────────────────────
* Each sprite is defined as a 16x16 grid of hex color values.
* 0x00000000 = transparent
* Colors are RGBA packed as 0xRRGGBBAA
*/
#define T 0x00000000 /* transparent */
/* Color palette */
#define BLK 0x1a1a2eFF /* black/dark */
#define WHT 0xeeeeeaFF /* white */
#define SKN 0xe8b796FF /* skin tone */
#define SKD 0xc48e6aFF /* skin dark */
#define BLU 0x4a7cbdFF /* blue */
#define BLD 0x365e8fFF /* blue dark */
#define BLL 0x6fa8dcFF /* blue light */
#define RED 0xd94444FF /* red */
#define RDD 0xa83232FF /* red dark */
#define RDL 0xff6666FF /* red light */
#define GRN 0x4caf50FF /* green */
#define GRD 0x388e3cFF /* green dark */
#define YLW 0xffd54fFF /* yellow */
#define YLD 0xd4a017FF /* yellow dark */
#define ORG 0xff9800FF /* orange */
#define ORD 0xcc7a00FF /* orange dark */
#define PRP 0x9c27b0FF /* purple */
#define PRD 0x7b1fa2FF /* purple dark */
#define PRL 0xce93d8FF /* purple light */
#define GRY 0x888888FF /* grey */
#define GYD 0x555555FF /* grey dark */
#define GYL 0xbbbbbbFF /* grey light */
#define BRN 0x8d6e63FF /* brown */
#define BRD 0x5d4037FF /* brown dark */
#define CYN 0x4dd0e1FF /* cyan */
#define CYD 0x00acc1FF /* cyan dark */
/* ── Player sprites ────────────────────────────────── */
/* Player idle frame 1 - heroic adventurer */
static const uint32_t player_idle1[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, SKN, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, SKN, T, BLU, BLU, BLU, BLU, BLU, BLU, T, SKN, T, T, T, T,
T, T, T, T, BLD, BLD, BLU, BLU, BLD, BLD, T, T, T, T, T, T,
T, T, T, T, BLD, BLD, T, T, BLD, BLD, T, T, T, T, T, T,
T, T, T, T, BRN, BRN, T, T, BRN, BRN, T, T, T, T, T, T,
T, T, T, T, BRD, BRD, T, T, BRD, BRD, T, T, T, T, T, T,
};
/* Player idle frame 2 - slight bob */
static const uint32_t player_idle2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, SKN, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, T, T, BLD, BLD, BLU, BLU, BLD, BLD, T, T, T, T, T, T,
T, T, T, T, BLD, BLD, T, T, BLD, BLD, T, T, T, T, T, T,
T, T, T, T, BRN, BRN, T, T, BRN, BRN, T, T, T, T, T, T,
T, T, T, T, BRD, BRD, T, T, BRD, BRD, T, T, T, T, T, T,
};
/* Player run frame 1 */
static const uint32_t player_run1[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T,
T, T, SKN, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, SKN, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, T, SKN, T, T, T, T,
T, T, T, T, T, BLD, BLU, BLU, BLD, T, T, T, T, T, T, T,
T, T, T, T, T, BLD, T, T, T, BLD, T, T, T, T, T, T,
T, T, T, T, BRN, BRN, T, T, T, BRN, BRN, T, T, T, T, T,
T, T, T, BRD, BRD, T, T, T, T, T, BRD, T, T, T, T, T,
};
/* Player run frame 2 */
static const uint32_t player_run2[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, SKN, T, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLD, T, BLU, BLU, T, BLD, T, T, T, T, T, T,
T, T, T, T, BLD, T, T, T, BLD, T, T, T, T, T, T, T,
T, T, T, T, T, BRN, T, BRN, BRN, T, T, T, T, T, T, T,
T, T, T, T, T, BRD, T, BRD, T, T, T, T, T, T, T, T,
};
/* Player run frame 3 */
static const uint32_t player_run3[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, SKN, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, SKN, T, BLU, BLU, BLU, BLU, BLU, BLU, T, SKN, T, T, T, T,
T, T, T, T, BLD, BLD, BLU, BLU, BLD, BLD, T, T, T, T, T, T,
T, T, T, BLD, T, T, T, T, T, T, BLD, T, T, T, T, T,
T, T, BRN, BRN, T, T, T, T, T, BRN, BRN, T, T, T, T, T,
T, T, T, BRD, T, T, T, T, BRD, BRD, T, T, T, T, T, T,
};
/* Player run frame 4 - same as run2 mirrored leg positions */
static const uint32_t player_run4[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T,
T, T, SKN, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, SKN, T, T, T, T,
T, T, SKN, T, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, T, BLD, BLU, BLU, BLD, T, T, T, T, T, T, T,
T, T, T, BLD, T, T, T, T, T, BLD, T, T, T, T, T, T,
T, T, BRN, BRN, T, T, T, T, BRN, BRN, T, T, T, T, T, T,
T, T, BRD, T, T, T, T, T, T, BRD, T, T, T, T, T, T,
};
/* Player jump */
static const uint32_t player_jump[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, SKN, T, SKN, SKN, SKD, SKD, SKN, SKN, T, SKN, T, T, T, T,
T, T, SKN, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, SKN, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLD, BLD, T, T, BLD, BLD, T, T, T, T, T, T,
T, T, T, BRN, BRN, T, T, T, BRN, BRN, T, T, T, T, T, T,
T, T, T, BRD, BRD, T, T, T, T, BRN, BRN, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, BRD, BRD, T, T, T, T, T,
};
/* Player fall */
static const uint32_t player_fall[16*16] = {
T, T, T, T, T, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLU, BLU, T, T, T, T, T,
T, T, T, T, BLU, BLL, BLL, BLL, BLL, BLL, BLU, T, T, T, T, T,
T, T, T, T, BLK, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T, T,
T, T, T, BLK, SKN, SKN, SKN, SKN, SKN, SKN, BLK, T, T, T, T, T,
T, T, T, BLK, SKN, BLK, SKN, SKN, BLK, SKN, BLK, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKN, SKD, SKN, SKN, T, T, T, T, T, T,
T, T, T, T, SKN, SKN, SKD, SKD, SKN, SKN, T, T, T, T, T, T,
T, SKN, SKN, BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, SKN, SKN, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, T, BLD, BLU, BLU, BLU, BLU, BLU, BLU, BLD, T, T, T, T, T,
T, T, T, T, BLU, BLU, BLU, BLU, BLU, BLU, T, T, T, T, T, T,
T, T, T, T, BLD, BLD, T, T, BLD, BLD, T, T, T, T, T, T,
T, T, T, T, BRN, BRN, T, T, T, BRN, T, T, T, T, T, T,
T, T, T, BRN, BRD, T, T, T, T, BRN, T, T, T, T, T, T,
T, T, T, BRD, T, T, T, T, T, BRD, T, T, T, T, T, T,
};
/* ── Grunt enemy sprites ───────────────────────────── */
/* Grunt: Red spiky enemy that patrols back and forth */
static const uint32_t grunt_idle1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, RED, T, T, T, T, T, RED, T, T, T, T, T,
T, T, T, T, RED, RED, T, T, T, RED, RED, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, WHT, BLK, RED, WHT, BLK, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, BLK, BLK, BLK, RED, RED, RDD, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, T, RDD, RED, RED, RDD, T, T, T, T, T, T, T,
T, T, T, T, RDD, RDD, T, T, RDD, RDD, T, T, T, T, T, T,
T, T, T, T, RDD, RDD, T, T, RDD, RDD, T, T, T, T, T, T,
T, T, T, T, BLK, BLK, T, T, BLK, BLK, T, T, T, T, T, T,
};
static const uint32_t grunt_idle2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, RED, T, T, T, T, T, RED, T, T, T, T, T,
T, T, T, T, RED, RED, T, T, T, RED, RED, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, WHT, BLK, RED, WHT, BLK, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, BLK, BLK, BLK, RED, RED, RDD, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, T, RDD, RED, RED, RDD, T, T, T, T, T, T, T,
T, T, T, T, RDD, RDD, T, T, RDD, RDD, T, T, T, T, T, T,
T, T, T, T, RDD, RDD, T, T, RDD, RDD, T, T, T, T, T, T,
T, T, T, T, BLK, BLK, T, T, BLK, BLK, T, T, T, T, T, T,
};
/* Grunt walk frames (same body, different leg positions) */
static const uint32_t grunt_walk1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, RED, T, T, T, T, T, RED, T, T, T, T, T,
T, T, T, T, RED, RED, T, T, T, RED, RED, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, WHT, BLK, RED, WHT, BLK, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, BLK, BLK, BLK, RED, RED, RDD, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, T, RDD, RED, RED, RDD, T, T, T, T, T, T, T,
T, T, T, T, T, RDD, T, T, RDD, T, T, T, T, T, T, T,
T, T, T, T, RDD, T, T, T, T, RDD, T, T, T, T, T, T,
T, T, T, T, BLK, T, T, T, T, BLK, T, T, T, T, T, T,
};
static const uint32_t grunt_walk2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, RED, T, T, T, T, T, RED, T, T, T, T, T,
T, T, T, T, RED, RED, T, T, T, RED, RED, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, WHT, BLK, RED, WHT, BLK, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, RDD, RED, RED, BLK, BLK, BLK, RED, RED, RDD, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, RDD, RED, RED, RED, RED, RED, RDD, T, T, T, T, T,
T, T, T, T, T, RDD, RED, RED, RDD, T, T, T, T, T, T, T,
T, T, T, T, RDD, T, T, T, T, RDD, T, T, T, T, T, T,
T, T, T, T, T, RDD, T, T, RDD, T, T, T, T, T, T, T,
T, T, T, T, T, BLK, T, T, BLK, T, T, T, T, T, T, T,
};
/* Grunt death frame */
static const uint32_t grunt_death[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T,
T, T, RDD, RED, RED, BLK, BLK, RED, BLK, BLK, RED, RED, RDD, T, T, T,
T, T, RDD, RED, RED, RED, RED, BLK, RED, RED, RED, RED, RDD, T, T, T,
T, T, T, RDD, RED, RED, RED, RED, RED, RED, RED, RDD, T, T, T, T,
T, T, T, T, RDD, RDD, RDD, RDD, RDD, RDD, RDD, T, T, T, T, T,
};
/* ── Flyer enemy sprites ───────────────────────────── */
/* Flying enemy: Purple bat-like creature */
static const uint32_t flyer_idle1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, PRP, PRP, PRP, PRP, PRP, T, T, T, T, T, T,
T, T, T, T, PRP, PRP, PRP, PRP, PRP, PRP, PRP, T, T, T, T, T,
T, T, T, PRP, PRP, YLW, BLK, PRP, YLW, BLK, PRP, PRP, T, T, T, T,
T, T, T, PRD, PRP, PRP, PRP, PRP, PRP, PRP, PRP, PRD, T, T, T, T,
T, T, T, PRD, PRP, PRP, BLK, BLK, PRP, PRP, PRP, PRD, T, T, T, T,
T, T, PRL, PRD, PRP, PRP, PRP, PRP, PRP, PRP, PRP, PRD, PRL, T, T, T,
T, PRL, PRL, T, PRD, PRP, PRP, PRP, PRP, PRP, PRD, T, PRL, PRL, T, T,
PRL, PRL, T, T, T, PRD, PRP, PRP, PRD, T, T, T, T, PRL, PRL, T,
PRL, T, T, T, T, T, PRD, PRD, T, T, T, T, T, T, PRL, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
static const uint32_t flyer_idle2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, PRP, PRP, PRP, PRP, PRP, T, T, T, T, T, T,
T, T, T, T, PRP, PRP, PRP, PRP, PRP, PRP, PRP, T, T, T, T, T,
T, T, T, PRP, PRP, YLW, BLK, PRP, YLW, BLK, PRP, PRP, T, T, T, T,
T, T, T, PRD, PRP, PRP, PRP, PRP, PRP, PRP, PRP, PRD, T, T, T, T,
T, T, T, PRD, PRP, PRP, BLK, BLK, PRP, PRP, PRP, PRD, T, T, T, T,
T, T, T, PRD, PRP, PRP, PRP, PRP, PRP, PRP, PRP, PRD, T, T, T, T,
T, PRL, T, T, PRD, PRP, PRP, PRP, PRP, PRP, PRD, T, T, PRL, T, T,
PRL, PRL, PRL, T, T, PRD, PRP, PRP, PRD, T, T, T, PRL, PRL, PRL, T,
T, T, PRL, PRL, T, T, PRD, PRD, T, T, T, PRL, PRL, T, T, T,
T, T, T, PRL, PRL, T, T, T, T, T, PRL, PRL, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Flyer death */
static const uint32_t flyer_death[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, PRP, PRP, PRP, PRP, PRP, T, T, T, T, T, T,
T, T, T, T, PRP, PRP, PRP, PRP, PRP, PRP, PRP, T, T, T, T, T,
T, T, T, PRP, PRP, BLK, BLK, PRP, BLK, BLK, PRP, PRP, T, T, T, T,
T, T, T, PRD, PRP, PRP, PRP, PRP, PRP, PRP, PRP, PRD, T, T, T, T,
T, T, T, PRD, PRP, PRP, PRP, BLK, PRP, PRP, PRP, PRD, T, T, T, T,
T, T, T, T, PRD, PRP, PRP, PRP, PRP, PRP, PRD, T, T, T, T, T,
T, T, T, PRL, T, PRD, PRP, PRP, PRD, T, PRL, T, T, T, T, T,
T, T, PRL, T, T, T, PRD, PRD, T, T, T, PRL, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Projectile sprites ────────────────────────────── */
/* Player bullet frame 1 */
static const uint32_t bullet1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, CYD, CYN, CYN, CYD, T, T, T, T, T, T,
T, T, T, T, T, CYD, CYN, WHT, WHT, CYN, CYD, T, T, T, T, T,
T, T, T, T, T, CYN, WHT, WHT, WHT, WHT, CYN, T, T, T, T, T,
T, T, T, T, T, CYN, WHT, WHT, WHT, WHT, CYN, T, T, T, T, T,
T, T, T, T, T, CYD, CYN, WHT, WHT, CYN, CYD, T, T, T, T, T,
T, T, T, T, T, T, CYD, CYN, CYN, CYD, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Player bullet frame 2 - glow variation */
static const uint32_t bullet2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, T, CYN, WHT, WHT, CYN, T, T, T, T, T, T,
T, T, T, T, T, CYN, WHT, WHT, WHT, WHT, CYN, T, T, T, T, T,
T, T, T, T, CYD, WHT, WHT, WHT, WHT, WHT, WHT, CYD, T, T, T, T,
T, T, T, T, CYD, WHT, WHT, WHT, WHT, WHT, WHT, CYD, T, T, T, T,
T, T, T, T, T, CYN, WHT, WHT, WHT, WHT, CYN, T, T, T, T, T,
T, T, T, T, T, T, CYN, WHT, WHT, CYN, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Impact effect frame 1 */
static const uint32_t impact1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, WHT, T, T, T, T, T, T, T, T,
T, T, T, T, T, WHT, T, T, T, WHT, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYN, T, T, T, T, T, T, T, T,
T, T, T, T, T, CYN, CYN, WHT, CYN, CYN, T, T, T, T, T, T,
T, T, T, T, T, CYN, WHT, WHT, WHT, CYN, T, T, T, T, T, T,
T, T, WHT, T, CYN, WHT, WHT, WHT, WHT, WHT, CYN, T, WHT, T, T, T,
T, T, T, T, T, CYN, WHT, WHT, WHT, CYN, T, T, T, T, T, T,
T, T, T, T, T, CYN, CYN, WHT, CYN, CYN, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYN, T, T, T, T, T, T, T, T,
T, T, T, T, T, WHT, T, T, T, WHT, T, T, T, T, T, T,
T, T, T, T, T, T, T, WHT, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Impact effect frame 2 - expanding */
static const uint32_t impact2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, CYD, T, T, T, T, T, CYD, T, T, T, T, T,
T, T, T, T, T, T, CYN, T, CYN, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYN, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, CYN, WHT, CYN, T, T, T, T, T, T, T,
T, T, T, CYN, T, CYN, T, WHT, T, CYN, T, CYN, T, T, T, T,
T, T, T, T, CYN, WHT, WHT, WHT, WHT, WHT, CYN, T, T, T, T, T,
T, T, T, CYN, T, CYN, T, WHT, T, CYN, T, CYN, T, T, T, T,
T, T, T, T, T, T, CYN, WHT, CYN, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYN, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, CYN, T, CYN, T, T, T, T, T, T, T,
T, T, T, T, CYD, T, T, T, T, T, CYD, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Impact effect frame 3 - fading */
static const uint32_t impact3[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, CYD, T, T, T, T, T, T, T, CYD, T, T, T, T,
T, T, T, T, T, T, T, CYD, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, CYD, T, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, CYD, T, T, T, CYD, T, T, T, T, T, T,
T, T, T, T, T, T, CYD, T, CYD, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, CYD, T, T, T, T, T, T, T, T,
T, T, T, CYD, T, T, T, T, T, T, T, CYD, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* Enemy bullet */
static const uint32_t enemy_bullet1[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, ORD, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T, T,
T, T, T, T, T, ORD, YLW, WHT, YLW, ORD, T, T, T, T, T, T,
T, T, T, T, T, ORD, YLW, WHT, YLW, ORD, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, ORD, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
static const uint32_t enemy_bullet2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, ORD, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T, T,
T, T, T, T, T, ORG, YLW, WHT, YLW, ORG, T, T, T, T, T, T,
T, T, T, T, ORD, YLW, WHT, WHT, WHT, YLW, ORD, T, T, T, T, T,
T, T, T, T, ORD, YLW, WHT, WHT, WHT, YLW, ORD, T, T, T, T, T,
T, T, T, T, T, ORG, YLW, WHT, YLW, ORG, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, YLW, ORG, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, ORD, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Spritesheet generation ────────────────────────── */
/* All sprite definitions for the sheet - row, column, pixel data */
typedef struct SpriteDef {
int row;
int col;
const uint32_t *pixels;
} SpriteDef;
static const SpriteDef s_sprite_defs[] = {
/* Row 0: Player */
{0, 0, player_idle1},
{0, 1, player_idle2},
{0, 2, player_run1},
{0, 3, player_run2},
{0, 4, player_run3},
{0, 5, player_run4},
{0, 6, player_jump},
{0, 7, player_fall},
/* Row 1: Grunt */
{1, 0, grunt_idle1},
{1, 1, grunt_idle2},
{1, 2, grunt_walk1},
{1, 3, grunt_walk2},
{1, 4, grunt_walk1}, /* reuse frame */
{1, 5, grunt_walk2}, /* reuse frame */
{1, 6, grunt_death},
/* Row 2: Flyer */
{2, 0, flyer_idle1},
{2, 1, flyer_idle2},
{2, 2, flyer_idle1}, /* reuse for fly anim */
{2, 3, flyer_idle2},
{2, 4, flyer_idle1},
{2, 5, flyer_idle2},
{2, 6, flyer_death},
/* Row 3: Projectiles */
{3, 0, bullet1},
{3, 1, bullet2},
{3, 2, impact1},
{3, 3, impact2},
{3, 4, impact3},
{3, 5, enemy_bullet1},
{3, 6, enemy_bullet2},
};
#define SHEET_COLS 8
#define SHEET_ROWS 4
SDL_Texture *sprites_generate(SDL_Renderer *renderer) {
int w = SHEET_COLS * SPRITE_CELL;
int h = SHEET_ROWS * SPRITE_CELL;
SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(
0, w, h, 32, SDL_PIXELFORMAT_RGBA8888);
if (!surface) {
fprintf(stderr, "Failed to create sprite surface: %s\n", SDL_GetError());
return NULL;
}
/* Clear to transparent */
SDL_FillRect(surface, NULL, 0);
/* Blit each sprite definition */
int count = sizeof(s_sprite_defs) / sizeof(s_sprite_defs[0]);
for (int i = 0; i < count; i++) {
const SpriteDef *def = &s_sprite_defs[i];
int ox = def->col * SPRITE_CELL;
int oy = def->row * SPRITE_CELL;
uint32_t *pixels = (uint32_t *)surface->pixels;
int pitch = surface->pitch / 4; /* pitch in uint32_t */
for (int y = 0; y < SPRITE_CELL; y++) {
for (int x = 0; x < SPRITE_CELL; x++) {
uint32_t rgba = def->pixels[y * SPRITE_CELL + x];
if (rgba == 0) continue; /* transparent */
/* Convert from our RRGGBBAA to surface format */
uint8_t r = (rgba >> 24) & 0xFF;
uint8_t g = (rgba >> 16) & 0xFF;
uint8_t b = (rgba >> 8) & 0xFF;
uint8_t a = (rgba) & 0xFF;
pixels[(oy + y) * pitch + (ox + x)] =
SDL_MapRGBA(surface->format, r, g, b, a);
}
}
}
SDL_Texture *tex = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface);
if (!tex) {
fprintf(stderr, "Failed to create sprite texture: %s\n", SDL_GetError());
return NULL;
}
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
g_spritesheet = tex;
printf("Generated spritesheet: %dx%d (%d sprites)\n", w, h, count);
return tex;
}
/* ── Animation definitions ─────────────────────────── */
#define FRAME(col, row, dur) \
{ .src = {(col)*SPRITE_CELL, (row)*SPRITE_CELL, SPRITE_CELL, SPRITE_CELL}, .duration = (dur) }
/* Player */
static AnimFrame s_player_idle_frames[] = {
FRAME(0, 0, 0.6f),
FRAME(1, 0, 0.6f),
};
static AnimFrame s_player_run_frames[] = {
FRAME(2, 0, 0.1f),
FRAME(3, 0, 0.1f),
FRAME(4, 0, 0.1f),
FRAME(5, 0, 0.1f),
};
static AnimFrame s_player_jump_frames[] = {
FRAME(6, 0, 1.0f),
};
static AnimFrame s_player_fall_frames[] = {
FRAME(7, 0, 1.0f),
};
/* Grunt */
static AnimFrame s_grunt_idle_frames[] = {
FRAME(0, 1, 0.5f),
FRAME(1, 1, 0.5f),
};
static AnimFrame s_grunt_walk_frames[] = {
FRAME(2, 1, 0.15f),
FRAME(3, 1, 0.15f),
FRAME(4, 1, 0.15f),
FRAME(5, 1, 0.15f),
};
static AnimFrame s_grunt_death_frames[] = {
FRAME(6, 1, 0.3f),
};
/* Flyer */
static AnimFrame s_flyer_idle_frames[] = {
FRAME(0, 2, 0.3f),
FRAME(1, 2, 0.3f),
};
static AnimFrame s_flyer_fly_frames[] = {
FRAME(2, 2, 0.12f),
FRAME(3, 2, 0.12f),
FRAME(4, 2, 0.12f),
FRAME(5, 2, 0.12f),
};
static AnimFrame s_flyer_death_frames[] = {
FRAME(6, 2, 0.3f),
};
/* Projectiles */
static AnimFrame s_bullet_frames[] = {
FRAME(0, 3, 0.08f),
FRAME(1, 3, 0.08f),
};
static AnimFrame s_impact_frames[] = {
FRAME(2, 3, 0.05f),
FRAME(3, 3, 0.05f),
FRAME(4, 3, 0.08f),
};
static AnimFrame s_enemy_bullet_frames[] = {
FRAME(5, 3, 0.1f),
FRAME(6, 3, 0.1f),
};
/* Exported animation definitions */
AnimDef anim_player_idle;
AnimDef anim_player_run;
AnimDef anim_player_jump;
AnimDef anim_player_fall;
AnimDef anim_grunt_idle;
AnimDef anim_grunt_walk;
AnimDef anim_grunt_death;
AnimDef anim_flyer_idle;
AnimDef anim_flyer_fly;
AnimDef anim_flyer_death;
AnimDef anim_bullet;
AnimDef anim_bullet_impact;
AnimDef anim_enemy_bullet;
void sprites_init_anims(void) {
anim_player_idle = (AnimDef){s_player_idle_frames, 2, true, NULL};
anim_player_run = (AnimDef){s_player_run_frames, 4, true, NULL};
anim_player_jump = (AnimDef){s_player_jump_frames, 1, false, NULL};
anim_player_fall = (AnimDef){s_player_fall_frames, 1, false, NULL};
anim_grunt_idle = (AnimDef){s_grunt_idle_frames, 2, true, NULL};
anim_grunt_walk = (AnimDef){s_grunt_walk_frames, 4, true, NULL};
anim_grunt_death = (AnimDef){s_grunt_death_frames, 1, false, NULL};
anim_flyer_idle = (AnimDef){s_flyer_idle_frames, 2, true, NULL};
anim_flyer_fly = (AnimDef){s_flyer_fly_frames, 4, true, NULL};
anim_flyer_death = (AnimDef){s_flyer_death_frames, 1, false, NULL};
anim_bullet = (AnimDef){s_bullet_frames, 2, true, NULL};
anim_bullet_impact = (AnimDef){s_impact_frames, 3, false, NULL};
anim_enemy_bullet = (AnimDef){s_enemy_bullet_frames, 2, true, NULL};
}

49
src/game/sprites.h Normal file
View File

@@ -0,0 +1,49 @@
#ifndef JNR_SPRITES_H
#define JNR_SPRITES_H
#include <SDL2/SDL.h>
#include "engine/animation.h"
/* Sprite sheet layout:
* Each sprite cell is 16x16 pixels.
* The sheet is organized in rows:
*
* Row 0: Player idle (2 frames), run (4 frames), jump (1), fall (1)
* Row 1: Grunt idle (2 frames), walk (4 frames), death (2 frames)
* Row 2: Flyer idle (2 frames), fly (4 frames), death (2 frames)
* Row 3: Projectiles: bullet (2 frames), impact (3 frames), enemy bullet (2)
*/
#define SPRITE_CELL 16
/* Generate the spritesheet texture (call after renderer is ready) */
SDL_Texture *sprites_generate(SDL_Renderer *renderer);
/* ── Player animations ─────────────────────────── */
extern AnimDef anim_player_idle;
extern AnimDef anim_player_run;
extern AnimDef anim_player_jump;
extern AnimDef anim_player_fall;
/* ── Grunt animations ──────────────────────────── */
extern AnimDef anim_grunt_idle;
extern AnimDef anim_grunt_walk;
extern AnimDef anim_grunt_death;
/* ── Flyer animations ──────────────────────────── */
extern AnimDef anim_flyer_idle;
extern AnimDef anim_flyer_fly;
extern AnimDef anim_flyer_death;
/* ── Projectile animations ─────────────────────── */
extern AnimDef anim_bullet;
extern AnimDef anim_bullet_impact;
extern AnimDef anim_enemy_bullet;
/* Initialize all animation definitions */
void sprites_init_anims(void);
/* The global spritesheet texture */
extern SDL_Texture *g_spritesheet;
#endif /* JNR_SPRITES_H */