Add jetpack fuel pickup and slow base recharge to 30s

Jetpack charges now take 30s to passively recharge (up from 3s),
making fuel management a core gameplay loop. A new fuel canister
powerup (POWERUP_FUEL) restores exactly one charge on pickup.

The existing jetpack powerup remains as the rare full-refill + 15s
boost. Fuel pickups replace most procedural jetpack spawns at higher
spawn rates to compensate for the weaker per-pickup value. Fuel
canisters also appear in corridors and arenas.

Adds orange canister pixel art, editor icon, entity registry entry,
and places fuel pickups throughout moon01.
This commit is contained in:
Thomas
2026-03-01 18:06:32 +00:00
parent fdba6ef077
commit bb0b9ddce1
11 changed files with 135 additions and 27 deletions

View File

@@ -253,6 +253,8 @@ static const uint64_t s_icon_bitmaps[ICON_COUNT] = {
0x006C7E7E3E1C0800ULL,
/* ICON_BOLT: lightning bolt */
0x0C1C18387060C000ULL,
/* ICON_FUEL: fuel canister */
0x003C4242423C1800ULL,
/* ICON_DRONE: quad-rotor */
0x00427E3C3C7E4200ULL,
/* ICON_GUN: pistol shape */

View File

@@ -28,6 +28,10 @@ static Entity *spawn_powerup_jetpack(EntityManager *em, Vec2 pos) {
return powerup_spawn_jetpack(em, pos);
}
static Entity *spawn_powerup_fuel(EntityManager *em, Vec2 pos) {
return powerup_spawn_fuel(em, pos);
}
static Entity *spawn_powerup_drone(EntityManager *em, Vec2 pos) {
return powerup_spawn_drone(em, pos);
}
@@ -80,6 +84,7 @@ void entity_registry_populate(void) {
reg_add("force_field", "Force Field", force_field_spawn, (SDL_Color){60, 140, 255, 255}, FFIELD_WIDTH, FFIELD_HEIGHT, ICON_FORCEFIELD);
reg_add("powerup_hp", "Health Pickup", spawn_powerup_health, (SDL_Color){255, 80, 80, 255}, 12, 12, ICON_HEART);
reg_add("powerup_jet", "Jetpack Refill", spawn_powerup_jetpack,(SDL_Color){255, 200, 50, 255}, 12, 12, ICON_BOLT);
reg_add("powerup_fuel", "Jetpack Fuel", spawn_powerup_fuel, (SDL_Color){255, 150, 30, 255}, 12, 12, ICON_FUEL);
reg_add("powerup_drone", "Drone Pickup", spawn_powerup_drone, (SDL_Color){80, 200, 255, 255}, 12, 12, ICON_DRONE);
reg_add("powerup_gun", "Gun Pickup", spawn_powerup_gun, (SDL_Color){200, 200, 220, 255}, 12, 12, ICON_GUN);
reg_add("asteroid", "Asteroid", asteroid_spawn, (SDL_Color){140, 110, 80, 255}, ASTEROID_WIDTH, ASTEROID_HEIGHT, ICON_ASTEROID);

View File

@@ -36,10 +36,11 @@ typedef enum EditorIcon {
ICON_FORCEFIELD = 6, /* electric field */
ICON_HEART = 7, /* health pickup */
ICON_BOLT = 8, /* jetpack / lightning */
ICON_DRONE = 9, /* drone companion */
ICON_GUN = 10, /* weapon pickup */
ICON_ASTEROID = 11, /* rock */
ICON_SPACECRAFT = 12, /* ship */
ICON_FUEL = 9, /* fuel canister */
ICON_DRONE = 10, /* drone companion */
ICON_GUN = 11, /* weapon pickup */
ICON_ASTEROID = 12, /* rock */
ICON_SPACECRAFT = 13, /* ship */
ICON_COUNT,
ICON_NONE = -1 /* no icon (fallback) */
} EditorIcon;

View File

@@ -328,6 +328,24 @@ static void handle_collisions(EntityManager *em) {
break;
}
case POWERUP_FUEL: {
PlayerData *ppd = (PlayerData *)player->data;
if (ppd && ppd->dash_charges < ppd->dash_max_charges) {
ppd->dash_charges++;
/* Reset recharge timer for the next charge */
if (ppd->dash_charges < ppd->dash_max_charges) {
float rate = (ppd->jetpack_boost_timer > 0)
? PLAYER_JETPACK_BOOST_RECHARGE
: PLAYER_DASH_RECHARGE;
ppd->dash_recharge_timer = rate;
} else {
ppd->dash_recharge_timer = 0.0f;
}
picked_up = true;
}
break;
}
case POWERUP_DRONE:
drone_spawn(em, vec2(
player->body.pos.x + player->body.size.x * 0.5f,

View File

@@ -273,11 +273,11 @@ static void gen_platforms(Tilemap *map, int x0, int w, int ground_row,
}
}
/* Jetpack refill on a high platform — reward for climbing */
if (rng_float() < 0.3f) {
/* Fuel pickup on a high platform — reward for climbing */
if (rng_float() < 0.45f) {
int top_py = ground_row - 3 - (num_plats - 1) * 3;
if (top_py >= ceil_row) {
add_entity(map, "powerup_jet", x0 + rng_range(2, w - 3), top_py - 1);
add_entity(map, "powerup_fuel", x0 + rng_range(2, w - 3), top_py - 1);
}
}
}
@@ -340,6 +340,11 @@ static void gen_corridor(Tilemap *map, int x0, int w, int ground_row,
if (difficulty > 0.3f && rng_float() < 0.35f) {
add_entity(map, "powerup_hp", x0 + w - 3, ground_row - 1);
}
/* Fuel pickup hidden in the corridor */
if (rng_float() < 0.3f) {
add_entity(map, "powerup_fuel", x0 + rng_range(2, w - 3), ceil_row + 2);
}
}
/* SEG_ARENA: wide open area, multiple enemies */
@@ -405,6 +410,8 @@ static void gen_arena(Tilemap *map, int x0, int w, int ground_row,
if (rng_float() < 0.5f) {
if (difficulty > 0.6f && rng_float() < 0.25f) {
add_entity(map, "powerup_drone", cp_x + 2, cp_y - 1);
} else if (rng_float() < 0.35f) {
add_entity(map, "powerup_fuel", cp_x + 2, cp_y - 1);
} else {
add_entity(map, "powerup_hp", cp_x + 2, cp_y - 1);
}
@@ -482,9 +489,9 @@ static void gen_shaft(Tilemap *map, int x0, int w, int ground_row,
}
}
/* Jetpack refill near the top — reward for climbing */
if (rng_float() < 0.4f) {
add_entity(map, "powerup_jet", x0 + w / 2, ceil_row + 3);
/* Fuel pickup near the top — reward for climbing */
if (rng_float() < 0.5f) {
add_entity(map, "powerup_fuel", x0 + w / 2, ceil_row + 3);
}
}
@@ -724,10 +731,10 @@ static void gen_climb(Tilemap *map, int x0, int w,
}
}
/* Powerup midway through the climb */
if (rng_float() < 0.4f) {
/* Fuel pickup midway through the climb */
if (rng_float() < 0.5f) {
int mid_y = (top_ground + bot_ground) / 2;
add_entity(map, "powerup_jet", (shaft_left + shaft_right) / 2, mid_y - 2);
add_entity(map, "powerup_fuel", (shaft_left + shaft_right) / 2, mid_y - 2);
}
}
@@ -1381,9 +1388,9 @@ static void gen_station_vent(Tilemap *map, int x0, int w, float difficulty) {
add_entity(map, "grunt", x0 + rng_range(2, w - 3), STATION_FLOOR_ROW - 1);
}
/* Jetpack refill reward (useful in low gravity) */
if (rng_float() < 0.35f) {
add_entity(map, "powerup_jet", x0 + rng_range(2, w - 3), vent_ceil + 1);
/* Fuel pickup reward (useful in low gravity) */
if (rng_float() < 0.4f) {
add_entity(map, "powerup_fuel", x0 + rng_range(2, w - 3), vent_ceil + 1);
}
}

View File

@@ -31,7 +31,7 @@
#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*/
#define PLAYER_DASH_RECHARGE 30.0f /* seconds to recharge one charge*/
/* Jetpack boost (from powerup) */
#define PLAYER_JETPACK_BOOST_DURATION 15.0f /* seconds the boost lasts */

View File

@@ -41,6 +41,7 @@ static void powerup_update(Entity *self, float dt, const Tilemap *map) {
switch (pd->kind) {
case POWERUP_HEALTH: color = (SDL_Color){220, 50, 50, 200}; break;
case POWERUP_JETPACK: color = (SDL_Color){255, 180, 50, 200}; break;
case POWERUP_FUEL: color = (SDL_Color){255, 120, 30, 200}; break;
case POWERUP_DRONE: color = (SDL_Color){50, 200, 255, 200}; break;
case POWERUP_GUN: color = (SDL_Color){200, 200, 220, 200}; break;
default: color = (SDL_Color){255, 255, 255, 200}; break;
@@ -129,6 +130,7 @@ Entity *powerup_spawn(EntityManager *em, Vec2 pos, PowerupKind kind) {
switch (kind) {
case POWERUP_HEALTH: animation_set(&e->anim, &anim_powerup_health); break;
case POWERUP_JETPACK: animation_set(&e->anim, &anim_powerup_jetpack); break;
case POWERUP_FUEL: animation_set(&e->anim, &anim_powerup_fuel); break;
case POWERUP_DRONE: animation_set(&e->anim, &anim_powerup_drone); break;
case POWERUP_GUN: animation_set(&e->anim, &anim_powerup_gun); break;
default: animation_set(&e->anim, &anim_powerup_health); break;
@@ -145,6 +147,10 @@ Entity *powerup_spawn_jetpack(EntityManager *em, Vec2 pos) {
return powerup_spawn(em, pos, POWERUP_JETPACK);
}
Entity *powerup_spawn_fuel(EntityManager *em, Vec2 pos) {
return powerup_spawn(em, pos, POWERUP_FUEL);
}
Entity *powerup_spawn_drone(EntityManager *em, Vec2 pos) {
return powerup_spawn(em, pos, POWERUP_DRONE);
}

View File

@@ -9,13 +9,15 @@
* Small collectible items that grant the player
* an instant benefit on contact:
* - Health: restores 1 HP
* - Jetpack: instantly refills all dash charges
* - Jetpack: instantly refills all dash charges + boost
* - Fuel: instantly fills one jetpack charge
* - Drone: spawns an orbiting combat drone
* ═══════════════════════════════════════════════════ */
typedef enum PowerupKind {
POWERUP_HEALTH,
POWERUP_JETPACK,
POWERUP_FUEL,
POWERUP_DRONE,
POWERUP_GUN,
POWERUP_KIND_COUNT
@@ -36,6 +38,7 @@ Entity *powerup_spawn(EntityManager *em, Vec2 pos, PowerupKind kind);
/* Convenience spawners for level/levelgen */
Entity *powerup_spawn_health(EntityManager *em, Vec2 pos);
Entity *powerup_spawn_jetpack(EntityManager *em, Vec2 pos);
Entity *powerup_spawn_fuel(EntityManager *em, Vec2 pos);
Entity *powerup_spawn_drone(EntityManager *em, Vec2 pos);
Entity *powerup_spawn_gun(EntityManager *em, Vec2 pos);

View File

@@ -902,6 +902,48 @@ static const uint32_t powerup_gun2[16*16] = {
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Fuel powerup sprite ───────────────────────────── */
/* Fuel canister — orange/amber napalm canister icon */
static const uint32_t powerup_fuel1[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, GYD, GYD, GYD, T, T, T, T, T, T, T,
T, T, T, T, T, GYD, GYL, GYL, GYL, GYD, T, T, T, T, T, T,
T, T, T, T, T, ORD, ORG, ORG, ORG, ORD, T, T, T, T, T, T,
T, T, T, T, ORD, ORG, YLW, YLW, ORG, ORG, ORD, T, T, T, T, T,
T, T, T, T, ORD, ORG, YLW, YLW, ORG, ORG, ORD, T, T, T, T, T,
T, T, T, T, ORD, ORG, ORG, ORG, ORG, ORG, ORD, T, T, T, T, T,
T, T, T, T, ORD, ORG, YLD, YLD, YLD, ORG, ORD, T, T, T, T, T,
T, T, T, T, ORD, ORG, ORG, ORG, ORG, ORG, ORD, T, T, T, T, T,
T, T, T, T, ORD, ORG, YLW, YLW, ORG, ORG, ORD, T, T, T, T, T,
T, T, T, T, T, ORD, ORG, ORG, ORG, ORD, T, T, T, T, T, T,
T, T, T, T, T, T, ORD, ORD, 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,
};
/* Fuel canister frame 2 — brighter glow */
static const uint32_t powerup_fuel2[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, GYL, GYL, GYL, T, T, T, T, T, T, T,
T, T, T, T, T, GYL, WHT, WHT, WHT, GYL, T, T, T, T, T, T,
T, T, T, T, T, ORG, YLW, YLW, YLW, ORG, T, T, T, T, T, T,
T, T, T, T, ORG, YLW, WHT, WHT, YLW, YLW, ORG, T, T, T, T, T,
T, T, T, T, ORG, YLW, WHT, WHT, YLW, YLW, ORG, T, T, T, T, T,
T, T, T, T, ORG, YLW, YLW, YLW, YLW, YLW, ORG, T, T, T, T, T,
T, T, T, T, ORG, YLW, ORG, ORG, ORG, YLW, ORG, T, T, T, T, T,
T, T, T, T, ORG, YLW, YLW, YLW, YLW, YLW, ORG, T, T, T, T, T,
T, T, T, T, ORG, YLW, WHT, WHT, YLW, YLW, ORG, T, T, T, T, T,
T, T, T, T, T, ORG, YLW, YLW, YLW, ORG, T, T, T, T, T, T,
T, T, T, T, T, T, ORG, ORG, ORG, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T,
};
/* ── Asteroid sprite ────────────────────────────────── */
/* Asteroid-specific greys — cold, cratered moonrock */
@@ -1024,6 +1066,8 @@ static const SpriteDef s_sprite_defs[] = {
{6, 3, powerup_gun2},
{6, 4, asteroid1},
{6, 5, asteroid2},
{6, 6, powerup_fuel1},
{6, 7, powerup_fuel2},
};
#define SHEET_COLS 8
@@ -1210,6 +1254,12 @@ static AnimFrame s_powerup_drone_frames[] = {
FRAME(6, 5, 0.3f),
};
/* Fuel powerup */
static AnimFrame s_powerup_fuel_frames[] = {
FRAME(6, 6, 0.35f),
FRAME(7, 6, 0.35f),
};
/* Gun powerup */
static AnimFrame s_powerup_gun_frames[] = {
FRAME(2, 6, 0.4f),
@@ -1256,6 +1306,7 @@ AnimDef anim_force_field_off;
AnimDef anim_powerup_health;
AnimDef anim_powerup_jetpack;
AnimDef anim_powerup_fuel;
AnimDef anim_powerup_drone;
AnimDef anim_powerup_gun;
AnimDef anim_asteroid;
@@ -1290,6 +1341,7 @@ void sprites_init_anims(void) {
anim_powerup_health = (AnimDef){s_powerup_health_frames, 2, true, NULL};
anim_powerup_jetpack = (AnimDef){s_powerup_jetpack_frames, 2, true, NULL};
anim_powerup_fuel = (AnimDef){s_powerup_fuel_frames, 2, true, NULL};
anim_powerup_drone = (AnimDef){s_powerup_drone_frames, 2, true, NULL};
anim_powerup_gun = (AnimDef){s_powerup_gun_frames, 2, true, NULL};
anim_asteroid = (AnimDef){s_asteroid_frames, 2, true, NULL};

View File

@@ -52,6 +52,7 @@ extern AnimDef anim_force_field_off;
/* ── Powerup animations ────────────────────────── */
extern AnimDef anim_powerup_health;
extern AnimDef anim_powerup_jetpack;
extern AnimDef anim_powerup_fuel;
extern AnimDef anim_powerup_drone;
extern AnimDef anim_powerup_gun;