Entities & Hazards
Super Mango has six enemy types and seven hazard types. All are stored as fixed-size arrays inside GameState, filled from the level by src/levels/level_loader.c, then updated and rendered every frame. Updates run in fixed 1/60 s simulation steps, so every speed below (px/s, °/s) means the same on any display. Positions are in logical pixels (400×300 space).
Losing a life resets every enemy and hazard to its authored placement.
All enemies patrol between patrol_x0 and patrol_x1. The TOML vx is only the starting velocity: its sign picks the first direction, and after the first turn the enemy moves at its type’s speed constant below.
Enemies
Spider
File: src/entities/spider.c / spider.h
Sprite: assets/sprites/entities/spider.png — 192×48 px, 3 frames of 64×48 px
Behaviour: Horizontal ground patrol. Walks back and forth between patrol_x0 and patrol_x1. No gravity — stays on the ground floor. Reverses direction and flips sprite when it hits a patrol boundary, or when its art centre would move over a floor gap.
| Constant | Value | Description |
|---|---|---|
MAX_SPIDERS |
16 | Slots in the GameState array |
SPIDER_FRAMES |
3 | Animation frames |
SPIDER_FRAME_W |
64 | Width of one frame slot in px |
SPIDER_ART_W |
25 | Width of visible art (cols 20–44) |
SPIDER_ART_H |
10 | Height of visible art (rows 22–31) |
SPIDER_SPEED |
50.0 | Walk speed in logical px/s |
SPIDER_FRAME_MS |
150 | ms per animation frame |
TOML placement:
[[spiders]]
x = 600.0
vx = 50.0 # positive = starts moving right
patrol_x0 = 592.0
patrol_x1 = 750.0
frame_index = 0 # starting animation frame (0–2)
Jumping Spider
File: src/entities/jumping_spider.c / jumping_spider.h
Sprite: assets/sprites/entities/jumping_spider.png
Behaviour: Like the spider (55 px/s, JSPIDER_SPEED) but leaps when its art centre reaches a floor gap: an upward impulse of −200 px/s under its own 600 px/s² gravity, with the attack sound when on screen. Normal spiders reverse at gaps; jumping spiders continue across them. Neither variant follows the player.
TOML placement:
[[jumping_spiders]]
x = 130.0
vx = 55.0
patrol_x0 = 46.0
patrol_x1 = 310.0
Bird
File: src/entities/bird.c / bird.h; movement, sound, hitbox and render are shared with the faster bird in src/entities/bird_variant.c, tuned by a BirdVariantSpec
Sprite: assets/sprites/entities/bird.png — 144×48 px, 3 frames of 48×48 px
Behaviour: Slow sine-wave sky patrol. Flies horizontally while oscillating vertically around base_y using a sine curve. The wing-flap sound effect plays once per animation cycle with distance-based volume.
| Constant | Value | Description |
|---|---|---|
MAX_BIRDS |
16 | Slots in GameState |
BIRD_FRAMES |
3 | Animation frames |
BIRD_FRAME_W |
48 | Frame slot width in px |
BIRD_ART_W |
15 | Visible art width (cols 17–31) |
BIRD_ART_H |
14 | Visible art height (rows 17–30) |
BIRD_SPEED |
45.0 | Horizontal speed in px/s |
BIRD_WAVE_AMP |
20.0 | Sine-wave vertical amplitude in px |
BIRD_WAVE_FREQ |
0.015 | Sine phase in radians per horizontal px |
BIRD_FRAME_MS |
140 | ms per animation frame |
TOML placement:
[[birds]]
x = 100.0
base_y = 60.0 # vertical centre of the sine wave
vx = 45.0
patrol_x0 = 0.0
patrol_x1 = 700.0
frame_index = 0
Faster Bird
File: src/entities/faster_bird.c / faster_bird.h
Sprite: assets/sprites/entities/faster_bird.png
Behaviour: Faster sky patrol with a tighter wave, through the same bird_variant.c code. Same schema as Bird but uses [[faster_birds]] in TOML.
| Constant | Value | Description |
|---|---|---|
MAX_FASTER_BIRDS |
16 | Slots in GameState |
FBIRD_SPEED |
80.0 | Horizontal speed in px/s (bird: 45) |
FBIRD_WAVE_AMP |
15.0 | Sine-wave vertical amplitude in px |
FBIRD_WAVE_FREQ |
0.025 | Sine phase in radians per horizontal px |
FBIRD_FRAME_MS |
90 | ms per animation frame |
[[faster_birds]]
x = 600.0
base_y = 50.0
vx = -80.0
patrol_x0 = 300.0
patrol_x1 = 1100.0
frame_index = 0
Fish
File: src/entities/fish.c / fish.h
Sprite: assets/sprites/entities/fish.png — 96×48 px, 2 frames of 48×48 px
Behaviour: Patrols horizontally in the water lane at the bottom of the screen. Periodically performs a random upward jump (impulse −280 px/s) that can reach the player on the ground floor. Jump interval is randomised between 1.4 s and 3.0 s.
| Constant | Value | Description |
|---|---|---|
MAX_FISH |
16 | Slots in GameState |
FISH_FRAMES |
2 | Animation frames |
FISH_FRAME_W |
48 | Frame slot width in px |
FISH_SPEED |
70.0 | Horizontal patrol speed in px/s |
FISH_JUMP_VY |
−280.0 | Upward jump impulse in px/s |
FISH_JUMP_MIN |
1.4 | Minimum seconds between jumps |
FISH_JUMP_MAX |
3.0 | Maximum seconds between jumps |
FISH_HITBOX_PAD_X |
16 | Left/right inset for collision |
FISH_HITBOX_PAD_Y |
13 | Top inset for collision |
FISH_FRAME_MS |
120 | ms per animation frame |
[[fish]]
x = 700.0
vx = 70.0
patrol_x0 = 500.0
patrol_x1 = 950.0
Faster Fish
File: src/entities/faster_fish.c / faster_fish.h
Sprite: assets/sprites/entities/faster_fish.png
Behaviour: Same as fish (one shared implementation in fish.c, tuned by a FishSpec) but faster and jumpier: 120 px/s (FFISH_SPEED), a −420 px/s jump (FFISH_JUMP_VY) every 1.0–2.2 s, and 100 ms animation frames. Uses [[faster_fish]] in TOML.
[[faster_fish]]
x = 1100.0
vx = 120.0
patrol_x0 = 900.0
patrol_x1 = 1400.0
Hazards
Enemy and active hazard hitboxes deal 1 heart of damage on contact with knockback, subject to hurt immunity. game_collide() in src/collision/game_collision.c routes hits through apply_damage() in src/collision/collision_damage.c; waiting flames have no active damage hitbox. Collision uses each hazard’s position after this step’s update.
When a level has no authored [[checkpoints]], automatic screen-edge checkpoints avoid floor gaps and the static hazards on this page (spike rows, spike platforms, blue and fire flames). Moving hazards and enemies are not considered; see Authored Checkpoints.
Spike Row
File: src/hazards/spike.c / spike.h
Sprite: assets/sprites/hazards/spike.png — 16×16 px per tile
Behaviour: Static horizontal strip of spike tiles sitting on the ground floor. No movement or animation. Damages the player on any overlap.
| Constant | Value | Description |
|---|---|---|
MAX_SPIKE_ROWS |
16 | Rows in GameState |
MAX_SPIKE_TILES |
16 | Max tiles per row |
SPIKE_TILE_W |
16 | Width of one spike tile in px |
SPIKE_TILE_H |
16 | Height of one spike tile in px |
[[spike_rows]]
x = 780.0 # left edge of the strip
count = 4 # number of tiles
Spike Block
File: src/hazards/spike_block.c / spike_block.h
Sprite: assets/sprites/hazards/spike_block.png
Behaviour: A rotating hazard (24×24 px, 360°/s spin) that travels along a Rail path. References a rail by index and can be given an initial offset and speed; the presets are SPIKE_SPEED_SLOW 1.5, SPIKE_SPEED_NORMAL 3.0 and SPIKE_SPEED_FAST 6.0 tiles/s. On a closed loop it circulates; on an open rail it bounces at a capped end. On an open rail without an end cap it waits at the start until the camera reaches it, then flies off the far end and falls. The player is pushed on contact.
[[spike_blocks]]
rail_index = 0 # 0-based index into the [[rails]] list
t_offset = 0.0 # starting position on the rail (0.0 = first tile)
speed = 1.5 # traversal speed in tiles/s
Spike Platform
File: src/hazards/spike_platform.c / spike_platform.h
Sprite: assets/sprites/hazards/spike_platform.png
Behaviour: Elevated static surface tiled across tile_count 16 px pieces (1–16). The player can land on the spiked top, which damages them (the hitbox reaches 2 px above the surface so a standing player always overlaps it), and touching the sides also hurts. The smooth underside is a solid ceiling that deals no damage.
[[spike_platforms]]
x = 370.0
y = 200.0 # top edge in logical pixels
tile_count = 3
Circular Saw
File: src/hazards/circular_saw.c / circular_saw.h
Sprite: assets/sprites/hazards/circular_saw.png — 32×32 px
Behaviour: Spins continuously and patrols a horizontal line. With y = 0 it uses the default height, rolling on top of a 2-tile pillar (y 140); a non-zero y is its top edge. Does not ride a rail — it bounces between patrol_x0 and patrol_x1. Faster than the player’s walk speed. Pushes the player on contact.
| Constant | Value | Description |
|---|---|---|
SAW_FRAME_W/H |
32 | Sprite dimensions in px |
SAW_SPIN_DEG_PER_SEC |
720.0 | Rotation speed (2 full rotations/s) |
SAW_PATROL_SPEED |
180.0 | Horizontal patrol speed in px/s |
SAW_PUSH_SPEED |
220.0 | Push impulse magnitude on contact |
SAW_PUSH_VY |
−150.0 | Upward component of push |
[[circular_saws]]
x = 1350.0
y = 0.0 # 0 = default height (y 140)
patrol_x0 = 1350.0
patrol_x1 = 1446.0
direction = 1 # 1 = starts right, -1 = starts left
Axe Trap
File: src/hazards/axe_trap.c / axe_trap.h
Sprite: assets/sprites/hazards/axe_trap.png — 48×64 px
Behaviour: Swinging or spinning axe. The pivot sits at the horizontal centre of the 48 px column starting at pillar_x. With y = 0 the pivot uses a fixed default height (y 124, the top of a 3-tile pillar); it is not measured from a pillar, so set y for other pillar heights. Two modes:
- PENDULUM — sinusoidal swing from −60° to +60° over a 2 s cycle. SFX plays at each extreme.
- SPIN — continuous 360° clockwise rotation at 180°/s. SFX plays each full rotation.
Collision uses a 28×28 px box centred on the blade, whose centre is rotated around the pivot with the current angle (axe_trap_get_hitbox()).
| Constant | Value | Description |
|---|---|---|
AXE_FRAME_W/H |
48 / 64 | Sprite dimensions |
AXE_SWING_AMPLITUDE |
60.0° | Max angle from vertical |
AXE_SWING_PERIOD |
2.0 s | Full pendulum cycle duration |
AXE_SPIN_SPEED |
180.0°/s | Full-rotation variant speed |
[[axe_traps]]
pillar_x = 256.0 # left x of the pillar column
y = 0.0 # pivot y; 0 = default (y 124)
mode = "PENDULUM" # or "SPIN"
Blue Flame
File: src/hazards/blue_flame.c / blue_flame.h
Sprite: assets/sprites/hazards/blue_flame.png — 96×48 px, 2 frames of 48×48 px
Behaviour: Erupts from a manually placed floor-gap position in four phases:
| Phase | Duration | Description |
|---|---|---|
WAITING |
1.5 s | Hidden below the floor, counting down |
RISING |
Until apex (y = 60) | Launches at −550 px/s, decelerates at 800 px/s² |
FLIPPING |
0.12 s | Rotates 180° at the apex |
FALLING |
Until below floor | Descends upside-down, accelerating with gravity |
Blue flames are placed explicitly with [[blue_flames]]. x is the gap’s left edge and normally matches a floor_gaps entry; the flame is centred in the 32 px opening. An x of 0 is skipped at load.
[[blue_flames]]
x = 192.0 # left edge of the sea gap
| Constant | Value | Description |
|---|---|---|
BLUE_FLAME_LAUNCH_VY |
−550.0 | Initial upward impulse in px/s |
BLUE_FLAME_APEX_Y |
60.0 | Highest point in logical pixels |
BLUE_FLAME_FLIP_DURATION |
0.12 s | Time to rotate 180° at apex |
BLUE_FLAME_WAIT_DURATION |
1.5 s | Idle time between eruptions |
MAX_BLUE_FLAMES |
16 | Maximum blue flame instances per level |
MAX_FIRE_FLAMES |
16 | Maximum fire flame instances per level |
Fire Flame
File: src/hazards/blue_flame.c / blue_flame.h (fire variant uses the shared blue-flame runtime type)
Sprite: assets/sprites/hazards/fire_flame.png
Behaviour: Same eruption cycle as the blue flame but with a fire-colored sprite. Used in volcanic or lava-themed levels. Shares the same phase constants, but stores placement in fire_flames[MAX_FIRE_FLAMES].
Collision Architecture
All entity–player collision uses AABB (axis-aligned bounding box) overlap tests on integer IntRects. Entity hitboxes are inset from the sprite frame to match visible art bounds only — transparent padding is excluded.
/* src/shared/geometry.h — half-open boxes: [x, x+w) */
static inline int rect_intersects(const IntRect *a, const IntRect *b)
{
return a && b && a->w > 0 && a->h > 0 && b->w > 0 && b->h > 0 &&
(int64_t)a->x < (int64_t)b->x + b->w &&
(int64_t)b->x < (int64_t)a->x + a->w &&
(int64_t)a->y < (int64_t)b->y + b->h &&
(int64_t)b->y < (int64_t)a->y + a->h;
}
Boxes that merely touch an edge do not overlap, and the 64-bit widening keeps the additions from overflowing. Each module exposes a hitbox function, such as bird_get_hitbox(), fish_get_hitbox() or circular_saw_get_hitbox(); game_collision.c builds spider hitboxes with spider_build_hitbox(). See each entity’s header for ART_X, ART_W, ART_Y, ART_H, and HITBOX_PAD_* constants.
Adding a New Enemy or Hazard
See the Developer Guide for the full entity template and step-by-step checklist.