Add moon surface intro level with asteroid hazards and unarmed mechanics
Introduce moon01.lvl as the starting level — a pure jump-and-run intro with no gun and no enemies, just platforming over gaps and dodging falling asteroids. The player picks up their gun upon transitioning to level01. New features: - Moon tileset and PARALLAX_STYLE_MOON with crater terrain backgrounds - Asteroid entity (ENT_ASTEROID): falls from sky, damages on contact, explodes on ground with particles, respawns after delay - PLAYER_UNARMED directive disables gun for the level - Pit rescue mechanic: falling costs 1 HP and auto-dashes upward - Gun powerup entity type for future armed-pickup levels - Segment-based procedural level generator with themed rooms - Extended editor with entity palette and improved tile cycling - Web shell improvements for Emscripten builds
This commit is contained in:
@@ -178,13 +178,39 @@ void player_update(Entity *self, float dt, const Tilemap *map) {
|
||||
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;
|
||||
/* Fall off bottom of level — lose 1 HP and auto-dash upward */
|
||||
float level_bottom = (float)(map->height * TILE_SIZE);
|
||||
if (self->body.pos.y + self->body.size.y > level_bottom &&
|
||||
!(self->flags & ENTITY_INVINCIBLE)) {
|
||||
self->health--;
|
||||
if (self->health <= 0) {
|
||||
/* Out of HP — die as before */
|
||||
self->health = 0;
|
||||
self->flags |= ENTITY_DEAD;
|
||||
pd->respawn_timer = 0.3f;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Drain all jetpack charges */
|
||||
pd->dash_charges = 0;
|
||||
pd->dash_recharge_timer = PLAYER_DASH_RECHARGE;
|
||||
|
||||
/* Auto-trigger upward dash (ignores charge requirement) */
|
||||
pd->dash_timer = PLAYER_DASH_DURATION;
|
||||
pd->dash_dir = vec2(0.0f, -1.0f);
|
||||
self->body.vel.y = 0;
|
||||
|
||||
/* Grant invincibility so this doesn't re-trigger immediately */
|
||||
pd->inv_timer = PLAYER_INV_TIME;
|
||||
self->flags |= ENTITY_INVINCIBLE;
|
||||
|
||||
/* Effects */
|
||||
Vec2 burst_pos = vec2(
|
||||
self->body.pos.x + self->body.size.x * 0.5f,
|
||||
self->body.pos.y + self->body.size.y
|
||||
);
|
||||
particle_emit_jetpack_burst(burst_pos, vec2(0.0f, -1.0f));
|
||||
audio_play_sound(s_sfx_dash, 96);
|
||||
}
|
||||
|
||||
/* Update invincibility */
|
||||
@@ -225,14 +251,24 @@ void player_update(Entity *self, float dt, const Tilemap *map) {
|
||||
|
||||
/* ── Dash / Jetpack ─────────────────────── */
|
||||
|
||||
/* Count down jetpack boost timer */
|
||||
if (pd->jetpack_boost_timer > 0) {
|
||||
pd->jetpack_boost_timer -= dt;
|
||||
if (pd->jetpack_boost_timer < 0)
|
||||
pd->jetpack_boost_timer = 0;
|
||||
}
|
||||
|
||||
/* Recharge jetpack charges over time */
|
||||
float recharge_rate = (pd->jetpack_boost_timer > 0)
|
||||
? PLAYER_JETPACK_BOOST_RECHARGE
|
||||
: PLAYER_DASH_RECHARGE;
|
||||
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;
|
||||
pd->dash_recharge_timer = recharge_rate;
|
||||
} else {
|
||||
pd->dash_recharge_timer = 0;
|
||||
}
|
||||
@@ -260,7 +296,8 @@ void player_update(Entity *self, float dt, const Tilemap *map) {
|
||||
|
||||
if (input_pressed(ACTION_DASH) && pd->dash_charges > 0) {
|
||||
pd->dash_charges--;
|
||||
pd->dash_recharge_timer = PLAYER_DASH_RECHARGE;
|
||||
pd->dash_recharge_timer = (pd->jetpack_boost_timer > 0)
|
||||
? PLAYER_JETPACK_BOOST_RECHARGE : PLAYER_DASH_RECHARGE;
|
||||
pd->dash_timer = PLAYER_DASH_DURATION;
|
||||
|
||||
/* Determine dash direction from input */
|
||||
@@ -362,7 +399,7 @@ void player_update(Entity *self, float dt, const Tilemap *map) {
|
||||
|
||||
/* ── Shooting ────────────────────────────── */
|
||||
pd->shoot_cooldown -= dt;
|
||||
if (input_pressed(ACTION_SHOOT) && pd->shoot_cooldown <= 0 && s_em) {
|
||||
if (pd->has_gun && 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;
|
||||
@@ -486,7 +523,7 @@ void player_render(Entity *self, const Camera *cam) {
|
||||
renderer_submit(&spr);
|
||||
|
||||
/* ── Weapon overlay ─────────────────── */
|
||||
if (s_weapon_tex && !(self->flags & ENTITY_DEAD) && pd) {
|
||||
if (s_weapon_tex && !(self->flags & ENTITY_DEAD) && pd && pd->has_gun) {
|
||||
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
|
||||
|
||||
/* Anchor gun to the player sprite position (not body pos)
|
||||
@@ -574,6 +611,7 @@ Entity *player_spawn(EntityManager *em, Vec2 pos) {
|
||||
e->max_health = 3;
|
||||
|
||||
PlayerData *pd = calloc(1, sizeof(PlayerData));
|
||||
pd->has_gun = true; /* armed by default; moon level overrides */
|
||||
pd->dash_charges = PLAYER_DASH_MAX_CHARGES;
|
||||
pd->dash_max_charges = PLAYER_DASH_MAX_CHARGES;
|
||||
pd->respawn_timer = RESPAWN_DELAY;
|
||||
@@ -595,21 +633,36 @@ float player_get_look_up_offset(const Entity *self) {
|
||||
}
|
||||
|
||||
bool player_get_dash_charges(const Entity *self, int *charges, int *max_charges,
|
||||
float *recharge_pct) {
|
||||
float *recharge_pct, bool *boosted) {
|
||||
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 (boosted) *boosted = pd->jetpack_boost_timer > 0;
|
||||
if (recharge_pct) {
|
||||
float rate = (pd->jetpack_boost_timer > 0)
|
||||
? PLAYER_JETPACK_BOOST_RECHARGE : PLAYER_DASH_RECHARGE;
|
||||
if (pd->dash_charges >= pd->dash_max_charges) {
|
||||
*recharge_pct = 1.0f;
|
||||
} else {
|
||||
*recharge_pct = 1.0f - (pd->dash_recharge_timer / PLAYER_DASH_RECHARGE);
|
||||
*recharge_pct = 1.0f - (pd->dash_recharge_timer / rate);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void player_give_gun(Entity *self) {
|
||||
if (!self || !self->data || self->type != ENT_PLAYER) return;
|
||||
PlayerData *pd = (PlayerData *)self->data;
|
||||
pd->has_gun = true;
|
||||
}
|
||||
|
||||
bool player_has_gun(const Entity *self) {
|
||||
if (!self || !self->data || self->type != ENT_PLAYER) return false;
|
||||
const PlayerData *pd = (const PlayerData *)self->data;
|
||||
return pd->has_gun;
|
||||
}
|
||||
|
||||
bool player_wants_respawn(const Entity *self) {
|
||||
if (!self || !self->data || self->type != ENT_PLAYER) return false;
|
||||
if (!(self->flags & ENTITY_DEAD)) return false;
|
||||
@@ -631,7 +684,7 @@ void player_respawn(Entity *self, Vec2 pos) {
|
||||
pd->inv_timer = PLAYER_INV_TIME;
|
||||
self->flags |= ENTITY_INVINCIBLE;
|
||||
|
||||
/* Reset player-specific state */
|
||||
/* Reset player-specific state (preserve has_gun across respawn) */
|
||||
pd->coyote_timer = 0;
|
||||
pd->jump_buffer_timer = 0;
|
||||
pd->jumping = false;
|
||||
@@ -640,6 +693,7 @@ void player_respawn(Entity *self, Vec2 pos) {
|
||||
pd->dash_timer = 0;
|
||||
pd->dash_charges = pd->dash_max_charges;
|
||||
pd->dash_recharge_timer = 0;
|
||||
pd->jetpack_boost_timer = 0;
|
||||
pd->aim_dir = AIM_FORWARD;
|
||||
pd->looking_up = false;
|
||||
pd->look_up_timer = 0;
|
||||
|
||||
Reference in New Issue
Block a user