#include "game/player.h" #include "game/sprites.h" #include "game/projectile.h" #include "game/stats.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 #include #include 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 — 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 */ 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 ─────────────────────── */ /* 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 = recharge_rate; } 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); /* Blue flame trail when boost is active */ if (pd->jetpack_boost_timer > 0) { particle_emit_jetpack_boost_trail(exhaust_pos, pd->dash_dir); } /* Skip normal movement during dash */ physics_update(body, dt, map); animation_update(&self->anim, dt); return; } /* ── Double-tap down for free downward jetpack ── */ if (!body->on_ground && input_pressed(ACTION_DOWN)) { if (pd->down_tap_timer > 0) { /* Second tap — trigger free downward dash */ pd->down_tap_timer = 0; pd->dash_timer = PLAYER_DASH_DURATION; pd->dash_dir = vec2(0.0f, 1.0f); /* Brief invincibility during dash */ pd->inv_timer = PLAYER_DASH_DURATION; self->flags |= ENTITY_INVINCIBLE; /* Jetpack burst downward */ 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; } pd->down_tap_timer = 0.3f; /* window for second tap */ } if (pd->down_tap_timer > 0) { pd->down_tap_timer -= dt; } if (input_pressed(ACTION_DASH) && pd->dash_charges > 0) { pd->dash_charges--; stats_record_dash(); /* Start recharge timer only if not already recharging */ if (pd->dash_recharge_timer <= 0) { 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 */ 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); /* Blue flame accents when boost powerup is active */ if (pd->jetpack_boost_timer > 0) { particle_emit_jetpack_boost_burst(exhaust_pos, pd->dash_dir); } audio_play_sound(s_sfx_dash, 96); return; } /* ── Jetpack boost idle glow ─────────────── */ /* Ambient blue flame from the player's back while boost is active * and not dashing. Emits from the rear center of the sprite. */ if (pd->jetpack_boost_timer > 0) { bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0; Vec2 back_pos = vec2( facing_left ? body->pos.x + body->size.x - 1.0f : body->pos.x + 1.0f, body->pos.y + body->size.y * 0.45f ); particle_emit_jetpack_boost_idle(back_pos, facing_left); } /* ── 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; stats_record_jump(); 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 (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; 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); stats_record_shot_fired(); /* 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) { pd->down_tap_timer = 0; /* reset double-tap on landing */ /* 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 && pd->has_gun) { 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; e->flags |= ENTITY_ALWAYS_UPDATE; 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; 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, 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 / 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; 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 (preserve has_gun across respawn) */ 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->jetpack_boost_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); }