Super Mango manualPlay

Player Module


The player module is split across focused files under src/player/. player.h is the public API; player_internal.h holds private sprite/hitbox constants shared by implementation files.

File Role
player.h Player struct, AnimState, public function declarations
player.c High-level player_update orchestration
player_lifecycle.c init, render, hitbox, reset, cleanup, default physics
player_input.c apply sampled physical/replay intent, run state and climb input
player_motion.c acceleration, friction, counter-accel, air control
player_jump.c jump buffer, coyote time, jump cut
player_climb.c vine/ladder/rope grab detection and climbing helpers
player_surfaces.c floor/platform/float-platform/bridge/bouncepad surface collision helpers
player_animation.c animation state selection and source frame updates

Lifecycle at a Glance

player_init
  ├── asset_cache_texture_acquire(cache, "assets/sprites/player/player.png")
  ├── 48x48 frame setup from 192x288 sheet (4 cols x 6 rows)
  ├── default spawn (overridden by LevelDef)
  └── player_apply_default_physics

per fixed simulation step (dt = 1/60 s, see game_player_step)
  ├── player_handle_input        -> move_dir, run, jump/climb intent
  └── player_update              -> climbing, gravity, motion, collisions, jump timers, animation

per rendered frame
  └── player_render              -> draw current frame with camera offset

player_get_hitbox                -> inset AABB for damage/collision checks

player_reset                     -> restore spawn/state; keep texture + tunable physics
player_cleanup                   -> asset_cache_texture_release

Public API

int player_init(Player *player, AssetCache *cache);
void player_apply_default_physics(Player *player);
void player_handle_input(Player *player, SoundEffect *snd_jump,
                         unsigned int replay_input_mask,
                         unsigned int physical_input_mask,
                         const VineDecor *vines, int vine_count,
                         const LadderDecor *ladders, int ladder_count,
                         const RopeDecor *ropes, int rope_count);
void player_update(Player *player, float dt, SoundEffect *snd_jump,
                   const Platform *platforms, int platform_count,
                   const FloatPlatform *float_platforms, int float_platform_count,
                   const BouncepadList *bouncepad_lists, int bouncepad_list_count,
                   const VineDecor *vines, int vine_count,
                   const LadderDecor *ladders, int ladder_count,
                   const RopeDecor *ropes, int rope_count,
                   const Bridge *bridges, int bridge_count,
                   const SpikePlatform *spike_platforms, int spike_platform_count,
                   const int *floor_gaps, int floor_gap_count,
                   int *out_bounce_idx,
                   int *out_fp_landed_idx,
                   int *out_bridge_landed_idx,
                   int prev_fp_landed_idx,
                   int world_w);
void player_render(const Player *player, int cam_x, int reduced_motion);
int player_hurt_tint(const Player *player, int reduced_motion, Color *tint);
IntRect player_get_hitbox(const Player *player);
void player_reset(Player *player);
void player_cleanup(Player *player, AssetCache *cache);

player_update receives the three bouncepad arrays as BouncepadList views ({ const Bouncepad *pads; int count; }) built by game_bouncepads_lists; no pads are copied each step. *out_bounce_idx is a flat index across those lists in order (list 0’s pads first). Its dt is always GAME_FIXED_STEP, so jump arcs come out the same at any display rate.

Inside player_update the order is: climbing (returns early while on a climbable), gravity, horizontal motion, position integration, platform and float-platform landing, bridge, spike-platform top and ceiling, floor and bouncepad collision, world bounds, jump buffer/coyote timers, then animation. The floor check holds a player whose feet were already below the floor top inside a floor gap between that gap’s edges, so they keep falling instead of being lifted back onto the grass.


Initialization

Action Current code
Texture assets/sprites/player/player.png
Sheet 192×288 px, 4 columns × 6 rows
Frame 48×48 px (PLAYER_FRAME_W/H)
Default spawn spawn_x = 80, spawn_y = FLOOR_Y - 2*TILE_SIZE + 16
Runtime spawn level_load may override from LevelDef.player_start_x/y
Physics defaults player_apply_default_physics, then optional LevelDef overrides

player_init returns 0 after getting the required player texture and -1 if the texture cannot be loaded, allowing game_init to clean up the partially initialized runtime.

The player does not own its sprite sheet. player_init asks the session’s asset cache (src/shared/asset_cache.h) for it and holds one reference; player_cleanup gives that reference back and clears player->texture. The first game of a session decodes player.png; a Replay’s new Player is handed the same texture and reads no file. Pass the same cache to both functions. NULL is allowed (a GameState without a session): the sheet is then loaded for that Player alone and freed by player_cleanup.

Default display size is 48×48 logical pixels. PLAYER_FLOOR_SINK = 16 sinks the frame into the floor to compensate for transparent sprite padding.


Movement Physics

Current movement uses acceleration and friction, not instant velocity reset.

Field / constant Default Meaning
walk_max_speed 100.0 px/s walking cap
run_max_speed 250.0 px/s running cap
walk_ground_accel 750.0 px/s² ground walk acceleration
run_ground_accel 600.0 px/s² ground run acceleration
ground_friction 550.0 px/s² braking with no input on ground
ground_counter_accel 100.0 px/s² extra brake when reversing direction
air_accel_walk 350.0 px/s² air control for walk jumps
air_accel_run 180.0 px/s² reduced air control for run jumps
air_friction 80.0 px/s² passive air drag with no input

LevelDef.physics can override these values; -1.0 keeps engine defaults and 0.0 is a valid override. See Level Design — TOML Reference for the complete [physics] table syntax, including camera lookahead overrides.


Jumping

Constant Value Meaning
JUMP_VY −325.0 px/s jump impulse
JUMP_BUFFER_TIME 0.10 s pre-landing jump buffer
PLAYER_COYOTE_TIME 0.10 s post-edge jump grace
JUMP_CUT_FACTOR 0.45 shorten jump when button released early

player_update receives snd_jump so buffered/coyote jumps can play the same guarded jump sound as direct jumps.


Climbables

The same climb state supports vines, ladders, and ropes.

Constant Value Meaning
CLIMB_SPEED 80.0 px/s vertical climb speed
CLIMB_H_SPEED 80.0 px/s horizontal drift while climbing
PLAYER_CLIMB_GRAB_PAD 4 px extra grab width around climbable art
PLAYER_KNOCKBACK_GRAB_LOCK 0.25 s no grabbing this long after a hit

Player.climb_source identifies the active climbable type: 0 = vine, 1 = ladder, 2 = rope.

A hit with knockback (apply_damage) clears on_vine, so the push is not overwritten by the climbing controls on the next step. For PLAYER_KNOCKBACK_GRAB_LOCK after the hit (while hurt_timer, which starts at PLAYER_HURT_TIME, is above PLAYER_HURT_TIME - PLAYER_KNOCKBACK_GRAB_LOCK) Up does not grab a climbable, so holding it cannot cancel the knockback.

While hurt_timer is above zero player_render blinks the sprite on and off every 100 ms. With the Reduced motion setting it draws the sprite steadily at half alpha instead (player_hurt_tint); the renderer passes the setting in, and neither function writes to the player, so the simulation is unchanged.


Animation

player_animation.c selects an AnimState, then maps it to a linear frame index on the 4-column sheet. anim_timer_ms is a float, and a finished frame subtracts its duration instead of resetting to zero, so fractional milliseconds from 1/60 s steps carry over and animation speed stays exact.

State Frames ms/frame First frame Sheet row
ANIM_IDLE 4 150 0 0
ANIM_WALK 4 100 4 1
ANIM_JUMP 2 150 8 2
ANIM_FALL 1 200 12 3
ANIM_CLIMB 2 100 16 4

Walk animation starts only when horizontal velocity exceeds WALK_ANIM_MIN_VX = 8.0f; this avoids foot-shuffling during tiny acceleration/friction speeds.


Hitbox

Constant Value Effect
PLAYER_PHYS_PAD_X 15 px left/right inset; physics width = 18 px
PLAYER_PHYS_PAD_TOP 18 px top inset
PLAYER_FLOOR_SINK 16 px bottom inset/sink; physics height = 14 px
r.x = (int)player->x + PLAYER_PHYS_PAD_X;
r.y = (int)player->y + PLAYER_PHYS_PAD_TOP;
r.w = player->w - 2 * PLAYER_PHYS_PAD_X;
r.h = player->h - PLAYER_PHYS_PAD_TOP - PLAYER_FLOOR_SINK;

Sprite analysis confirms the art envelope fits this inset: visible art is roughly 18×16 px inside the 48×48 frame.