Metroid Fusion
Nintendo R&D1's 2002 Game Boy Advance Metroid, read through metroidret's decompilation of the US release.
Metroid Fusion came out on the Game Boy Advance two years before Metroid: Zero Mission, and the same community decompiles both. The atlas reads it through metroidret’s C decompilation, which rebuilds the North American ROM1. Names and comments cited here are the decompilers’. Nothing here has been measured in the ROM yet.
#Energy and ammunition
A new game sets Samus’s energy to 99 energy2, as Zero Mission does. Her missile and power bomb counters already read 10, though she has no weapon to fire them2. An energy tank adds 100 per tank, up to 20993 and refills her, up to 2099. Zero Mission stops at 1299, or 699 on Hard3. A missile tank adds 5 missiles per tank, up to 2554, up to 255. Super, ice, and diffusion missiles all draw on that one count, where Zero Mission keeps super missiles apart4.
#The jump
A jump sets the vertical speed to 5.3 px/frame (6.9 with the High Jump), capped at 45, but movement caps it at 4 px/frame, so the first 8 frames of a jump rise 4 px each5. Gravity is 0.1875 px/frame²6, three-fifths of Zero Mission’s (computed)6. Held to the top, a jump rises about 71 px (104 with the High Jump)7, about 12 px more than Zero Mission’s, and the High Jump about 21 px more (computed)7.
Letting go of A while Samus rises stops the rise at once, at any height, as in Zero Mission8.
#X parasites
In Fusion a dying enemy turns into an X parasite, and absorbing it refills Samus by its color: 10 energy (yellow), 2 missiles (green), 500 energy and 100 missiles (red)9,9. Each enemy carries weights out of 1024 for red and green; yellow takes what’s left, if the enemy allows yellow at all. The draw also bends toward what Samus lacks: a green X turns yellow when her missiles are full and her energy isn’t, and the reverse10. Metroid’s enemies dropped energy balls and missiles from one table instead11.
Mechanics
| Mechanic | Kind | How |
|---|---|---|
| Health upgrade | Whole containers | energy tanks of 100 from 99 at the start, each refilling her energy, up to 2099 |
| Ammunition | Several pools | one missile count that every missile type shares, and power bombs; 5 and 2 per tank |
| Jump | Speed by state | 5.3 px/frame up, 6.9 with the High Jump, held to 4 px/frame by the speed cap |
| Gravity | One gravity | 0.1875 px/frame² in air, held or not, two-thirds of that in liquid without the Gravity Suit |
| Variable jump height | Release cap | letting go during the rise sets the vertical speed to 0 at any height |
| Item drop table | Weighted, by need | a killed enemy becomes an X parasite whose color, picked from 1024 by per-enemy weights, sets its refill |
Every claim about this game, by concept (10)
References
- ^The mf decompilation's README names
mf_us.gba, the North American release, as its default and only supported build and gives its SHA-1 as ca33f4348c2c05dd330d37b97e2c5a69531dfe87, which is the SHA-1 of the No-Intro file Metroid Fusion (USA).gba. The README counts 376 functions not yet decompiled. The atlas hasn't built it.Codemetroidret/mfrevisedHow we know
53231/3607 functions decompiled (89.57%, 376 left)README.md line 5 at a6d6187 · upstream ↗ 13This repository can be used to build multiple ROMs. The ROMs are named like this: `mf_{region}.gba` (or `mf_{region}_debug.gba`) ⋮ 16- us: USA, North America `sha1: ca33f4348c2c05dd330d37b97e2c5a69531dfe87` 17- eu: Europe **(Not yet supported)**
README.md lines 13–17 at a6d6187 · upstream ↗ 26The default built ROM is mf_us.gbaREADME.md line 26 at a6d6187 · upstream ↗ 1CA33F4348C2C05DD330D37B97E2C5A69531DFE87 mf_us.gbamf_us.sha1 line 1 at a6d6187 · upstream ↗ 4REGION ?= us ⋮ 7ifeq ($(REGION),us) 8 TARGET = mf_us ⋮ 11 CPPFLAGS += -DREGION_US
Makefile lines 4–11 at a6d6187 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abWhen
SamusInitruns with no file loading (gIsLoadingFileclear) and an unnamed flag,gUnk_3000be3, clear, it copiessEquipment_EmptyintogEquipment: 99 energy of 99, 10 missiles of 10, 10 power bombs of 10, and no weapon flags (MBF_NONE). The atlas reads this as a new game's start. Zero Mission starts with the same 99 energy, but with every ammunition count at 0.Codemetroidret/mf · metroidret/mzmHow we know
7523 if (gUnk_3000be3 || gIsLoadingFile) 7524 return; ⋮ 7526 gEquipment = sEquipment_Empty;
src/samus.c lines 7523–7526 at a6d6187 · upstream ↗ 47const struct Equipment sEquipment_Empty = { 48 .currentEnergy = 99, 49 .maxEnergy = 99, 50 .currentMissiles = 10, 51 .maxMissiles = 10, 52 .currentPowerBombs = 10, 53 .maxPowerBombs = 10, 54 .beamStatus = BF_NONE, 55 .weaponsStatus = MBF_NONE,
src/data/samus/samus_animation_pointers.c lines 47–55 at a6d6187 · upstream ↗ 236// --- Energy and ammo --- 237#define STARTING_ENERGY 99 238#define STARTING_MISSILES 10 239#define STARTING_POWER_BOMBS 10
include/constants/samus.h lines 236–239 at a6d6187 · upstream ↗ 8071 if (!gIsLoadingFile) ⋮ 8073 // Zero out most of samus's data 8074 DMA3_FILL_32(0, &gSamusData, sizeof(gSamusData)); 8075 DMA3_FILL_32(0, &gEquipment, sizeof(gEquipment)); ⋮ 8087 gEquipment.maxEnergy = 99; 8088 gEquipment.currentEnergy = 99;
src/samus.c lines 8071–8088 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abAn energy tank adds 100 to Samus's maximum energy and sets her energy to the new maximum, unless the new maximum would pass
MAX_ENERGY_AMOUNT, 20 tanks plus the starting 99, or 2099. The tank code reads no difficulty. Zero Mission's tank also adds 100 and refills, or 50 on Hard, and stops at 12 tanks: 1299 on Easy and Normal and 699 on Hard (computed).Codemetroidret/mf · metroidret/mzmrevisedHow we know
267 else if (i == TANK_TYPE_ENERGY) ⋮ 269 if (gEquipment.maxEnergy + ENERGY_INCREASE_AMOUNT <= MAX_ENERGY_AMOUNT) ⋮ 271 gEquipment.maxEnergy += ENERGY_INCREASE_AMOUNT; 272 gEquipment.currentEnergy = gEquipment.maxEnergy;
src/bg_clip.c lines 267–274 at a6d6187 · upstream ↗ 241#ifndef REGION_JP 242#define ENERGY_INCREASE_AMOUNT 100 ⋮ 246// Default 2099 247#define MAX_ENERGY_AMOUNT (20 * ENERGY_INCREASE_AMOUNT + STARTING_ENERGY)
include/constants/samus.h lines 241–247 at a6d6187 · upstream ↗ 531 else if (i == ITEM_TYPE_ENERGY) ⋮ 533 if (sNumberOfTanksPerArea[MAX_AMOUNT_OF_AREAS - 1].energy * sTankIncreaseAmount[gDifficulty].energy + sStartingHealthAmmo.energy >= gEquipment.maxEnergy + sTankIncreaseAmount[gDifficulty].energy) ⋮ 535 gEquipment.maxEnergy += sTankIncreaseAmount[gDifficulty].energy; 536 gEquipment.currentEnergy = gEquipment.maxEnergy;
src/bg_clip.c lines 531–537 at 2aae289 · upstream ↗ 52 [MAX_AMOUNT_OF_AREAS - 1] = { 53 .energy = 12,
src/data/block_data.c lines 52–53 at 2aae289 · upstream ↗ 60const struct TankList sStartingHealthAmmo = { 61 .energy = 99, ⋮ 67const struct TankList sTankIncreaseAmount[DIFF_COUNT] = { 68 [DIFF_EASY] = { 69 .energy = 100, ⋮ 74 [DIFF_NORMAL] = { 75 .energy = 100, ⋮ 80 [DIFF_HARD] = { 81 .energy = 50,
src/data/block_data.c lines 60–81 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abSamus has one missile count and one power bomb count, each with a maximum. A missile tank adds 5 to both the missile count and its maximum, unless the maximum would pass 255; a power bomb tank adds 2 to both, up to 254. The missile types the downloads add (super, ice, and diffusion missiles, flags in
weaponsStatus) draw on that one count. Zero Mission keeps separate counts for missiles, super missiles, and power bombs, and its missile tank also adds 5, or 2 on Hard.Codemetroidret/mf · metroidret/mzmHow we know
258 // Apply tank 259 if (i == TANK_TYPE_MISSILE) ⋮ 261 if (gEquipment.maxMissiles + MISSILE_INCREASE_AMOUNT <= MAX_MISSILE_AMOUNT) ⋮ 263 gEquipment.maxMissiles += MISSILE_INCREASE_AMOUNT; 264 gEquipment.currentMissiles += MISSILE_INCREASE_AMOUNT; ⋮ 275 else if (i == TANK_TYPE_POWER_BOMB) ⋮ 277 if (gEquipment.maxPowerBombs + POWER_BOMB_INCREASE_AMOUNT <= MAX_POWER_BOMB_AMOUNT) ⋮ 279 gEquipment.maxPowerBombs += POWER_BOMB_INCREASE_AMOUNT; 280 gEquipment.currentPowerBombs += POWER_BOMB_INCREASE_AMOUNT;
src/bg_clip.c lines 258–282 at a6d6187 · upstream ↗ 241#ifndef REGION_JP 242#define ENERGY_INCREASE_AMOUNT 100 243#define MISSILE_INCREASE_AMOUNT 5 244#define POWER_BOMB_INCREASE_AMOUNT 2 ⋮ 248// Default 255 249#define MAX_MISSILE_AMOUNT (51 * MISSILE_INCREASE_AMOUNT) 250// Default 254 251#define MAX_POWER_BOMB_AMOUNT (127 * POWER_BOMB_INCREASE_AMOUNT)
include/constants/samus.h lines 241–251 at a6d6187 · upstream ↗ 42struct Equipment { 43 u16 currentEnergy; 44 u16 maxEnergy; 45 u16 currentMissiles; 46 u16 maxMissiles; 47 u8 currentPowerBombs; 48 u8 maxPowerBombs; 49 u8 beamStatus; 50 u8 weaponsStatus;
include/structs/samus.h lines 42–50 at a6d6187 · upstream ↗ 273#define MBF_NONE 0 274#define MBF_MISSILES (1 << 0) 275#define MBF_SUPER_MISSILES (1 << 1) 276#define MBF_ICE_MISSILES (1 << 2) 277#define MBF_DIFFUSION_MISSILES (1 << 3) ⋮ 280#define MBF_ANY_MISSILE (MBF_MISSILES | MBF_SUPER_MISSILES | MBF_ICE_MISSILES | MBF_DIFFUSION_MISSILES)
include/constants/samus.h lines 273–280 at a6d6187 · upstream ↗ 16struct Equipment { 17 u16 maxEnergy; 18 u16 maxMissiles; 19 u8 maxSuperMissiles; 20 u8 maxPowerBombs; 21 u16 currentEnergy; 22 u16 currentMissiles; 23 u8 currentSuperMissiles; 24 u8 currentPowerBombs;
include/structs/samus.h lines 16–24 at 2aae289 · upstream ↗ 67const struct TankList sTankIncreaseAmount[DIFF_COUNT] = { 68 [DIFF_EASY] = { 69 .energy = 100, 70 .missile = 5, ⋮ 74 [DIFF_NORMAL] = { 75 .energy = 100, 76 .missile = 5, ⋮ 80 [DIFF_HARD] = { 81 .energy = 50, 82 .missile = 2,
src/data/block_data.c lines 67–84 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abA jump sets Samus's upward
yVelocityto 170 (SAMUS_LOW_JUMP_VELOCITY), or 220 with the High Jump, with no separate suitless speed. The velocity counts eighths of a subpixel and a pixel is 4 subpixels, so these are 5.3125 and 6.875 px/frame (computed). Movement caps the velocity at 128 (16 subpixels, 4 px), so a jump's first 8 frames each rise 4 px, and a High Jump's first 16 (computed). Zero Mission's jump takes off at 192, or 232 with the High Jump, under a cap of 192: 6 px/frame from the first frame.Codemetroidret/mf · metroidret/mzmHow we know
4784 case SPOSE_RUNNING: 4785 if (gSamusDataCopy.forcedMovement == FORCED_MOVEMENT_MID_AIR_JUMP) ⋮ 4787 gSamusData.pose = SPOSE_STARTING_SPIN_JUMP; ⋮ 4789 if (gEquipment.suitMiscStatus & SMF_HIGH_JUMP) 4790 gSamusData.yVelocity = SAMUS_HIGH_JUMP_VELOCITY; 4791 else 4792 gSamusData.yVelocity = SAMUS_LOW_JUMP_VELOCITY;
src/samus.c lines 4784–4793 at a6d6187 · upstream ↗ 4929 default: 4930 gSamusData.pose = SPOSE_MID_AIR; ⋮ 4932 if (gSamusDataCopy.forcedMovement == FORCED_MOVEMENT_MID_AIR_JUMP) ⋮ 4936 else 4937 gSamusData.pose = SPOSE_MID_AIR; ⋮ 4939 if (gEquipment.suitMiscStatus & SMF_HIGH_JUMP) 4940 gSamusData.yVelocity = SAMUS_HIGH_JUMP_VELOCITY; 4941 else 4942 gSamusData.yVelocity = SAMUS_LOW_JUMP_VELOCITY;
src/samus.c lines 4929–4942 at a6d6187 · upstream ↗ 321#define SAMUS_HIGH_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + EIGHTH_BLOCK_SIZE + PIXEL_SIZE - ONE_SUB_PIXEL * .5f)) 322#define SAMUS_LOW_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + PIXEL_SIZE + ONE_SUB_PIXEL + ONE_SUB_PIXEL * .25f)) ⋮ 327#define SAMUS_Y_VELOCITY_CAP (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE))
include/constants/samus.h lines 321–327 at a6d6187 · upstream ↗ 192#define VELOCITY_TO_SUB_PIXEL(velocity) (DIV_SHIFT((velocity), 8)) 193#define SUB_PIXEL_TO_VELOCITY(velocity) ((s32)((velocity) * 8))
include/macros.h lines 192–193 at a6d6187 · upstream ↗ 43// The amount of sub pixels a single visible pixel contains 44#define SUB_PIXEL_RATIO 4 45// The amount of visible pixels a block contains 46#define PIXEL_PER_BLOCK 16 ⋮ 49#define BLOCK_SIZE (PIXEL_PER_BLOCK * SUB_PIXEL_RATIO) ⋮ 53#define QUARTER_BLOCK_SIZE (BLOCK_SIZE / 4) ⋮ 55#define EIGHTH_BLOCK_SIZE (BLOCK_SIZE / 8) ⋮ 57#define PIXEL_SIZE (BLOCK_SIZE / PIXEL_PER_BLOCK) ⋮ 59#define ONE_SUB_PIXEL (PIXEL_SIZE / PIXEL_SIZE)
include/types.h lines 43–59 at a6d6187 · upstream ↗ 5526 // Check is within the bounds 5527 if (gSamusData.yVelocity > gSamusPhysics.yVelocityCap) 5528 velocity = VELOCITY_TO_SUB_PIXEL(gSamusPhysics.yVelocityCap); 5529 else if (gSamusData.yVelocity < -gSamusPhysics.yVelocityCap) 5530 velocity = VELOCITY_TO_SUB_PIXEL(-gSamusPhysics.yVelocityCap); 5531 else 5532 velocity = VELOCITY_TO_SUB_PIXEL(gSamusData.yVelocity); ⋮ 5538 gSamusData.yPosition -= velocity;
src/samus.c lines 5526–5538 at a6d6187 · upstream ↗ 336#define SAMUS_HIGH_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + PIXEL_SIZE * 3 + ONE_SUB_PIXEL)) 337#define SAMUS_LOW_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + EIGHTH_BLOCK_SIZE)) 338#define SAMUS_SUITLESS_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + EIGHTH_BLOCK_SIZE + PIXEL_SIZE / 2 + ONE_SUB_PIXEL / 2.f))
include/constants/samus.h lines 336–338 at 2aae289 · upstream ↗ 325#define SAMUS_Y_POSITIVE_VELOCITY_CAP 192include/constants/samus.h line 325 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abOut of liquid,
SamusUpdatePhysicssets the vertical acceleration to 6 and the vertical speed cap to 128, andSamusUpdateVelocityPositiontakes 6 fromyVelocityon every frame of the airborne poses while the velocity is above -220, whether A is held or not: 0.1875 px/frame², with rises and falls both capped at 4 px/frame (computed). In water, lava, or acid without the Gravity Suit or the flag the decompilers nameSMF_SA_X_SUIT, the acceleration drops to 4 and the cap to 48, 0.125 px/frame² and 1.5 px/frame. Zero Mission's gravity is 10, 0.3125 px/frame², with the same 4 px/frame fall cap (computed).Codemetroidret/mf · metroidret/mzmHow we know
326#define SAMUS_Y_ACCELERATION (SUB_PIXEL_TO_VELOCITY(ONE_SUB_PIXEL * .75f)) 327#define SAMUS_Y_VELOCITY_CAP (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE)) ⋮ 335#define SAMUS_TERMINAL_VELOCITY (SUB_PIXEL_TO_VELOCITY(HALF_BLOCK_SIZE - PIXEL_SIZE - ONE_SUB_PIXEL / 2.f))
include/constants/samus.h lines 326–335 at a6d6187 · upstream ↗ 137 switch (currentHazard) ⋮ 139 case HAZARD_WATER: 140 gSamusUnderwaterFlag++; ⋮ 142 case HAZARD_LAVA: 143 case HAZARD_ACID: 144 if (!(gEquipment.suitMiscStatus & (SMF_GRAVITY_SUIT | SMF_SA_X_SUIT))) 145 slowed++; 146 break; ⋮ 151 slowed++; ⋮ 160 gSamusPhysics.yAcceleration = SAMUS_Y_ACCELERATION * 2.f / 3; 161 gSamusPhysics.yVelocityCap = SAMUS_Y_VELOCITY_CAP * 0.375f; ⋮ 174 gSamusPhysics.yAcceleration = SAMUS_Y_ACCELERATION; ⋮
src/samus.c lines 137–175 at a6d6187 · upstream ↗ 5526 // Check is within the bounds 5527 if (gSamusData.yVelocity > gSamusPhysics.yVelocityCap) ⋮ 5529 else if (gSamusData.yVelocity < -gSamusPhysics.yVelocityCap) 5530 velocity = VELOCITY_TO_SUB_PIXEL(-gSamusPhysics.yVelocityCap); 5531 else ⋮ 5534 // Apply acceleration 5535 if (gSamusData.yVelocity > -SAMUS_TERMINAL_VELOCITY) 5536 gSamusData.yVelocity -= gSamusPhysics.yAcceleration; ⋮ 5538 gSamusData.yPosition -= velocity;
src/samus.c lines 5526–5538 at a6d6187 · upstream ↗ 324#define SAMUS_Y_ACCELERATION 10 325#define SAMUS_Y_POSITIVE_VELOCITY_CAP 192 326#define SAMUS_Y_NEGATIVE_VELOCITY_CAP 128 327#define SAMUS_Y_SLOWED_VELOCITY_CAP 40
include/constants/samus.h lines 324–327 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abStepping the code's arithmetic with A held, out of liquid, a jump from the ground peaks about 71 px up (283 subpixels) on its 28th frame, and a High Jump 104 px (416 subpixels) on its 36th (computed: each frame moves
yVelocitycapped at 128 and shifted right 3, then takes 6 from it). The same stepping gives Zero Mission about 59 px on the 19th frame and 83 px on the 23rd (mzm/jump-height), so Fusion's jump is higher and slower.Codemetroidret/mf · metroidret/mzmHow we know
321#define SAMUS_HIGH_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + EIGHTH_BLOCK_SIZE + PIXEL_SIZE - ONE_SUB_PIXEL * .5f)) 322#define SAMUS_LOW_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + PIXEL_SIZE + ONE_SUB_PIXEL + ONE_SUB_PIXEL * .25f)) ⋮ 326#define SAMUS_Y_ACCELERATION (SUB_PIXEL_TO_VELOCITY(ONE_SUB_PIXEL * .75f)) 327#define SAMUS_Y_VELOCITY_CAP (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE))
include/constants/samus.h lines 321–327 at a6d6187 · upstream ↗ 5526 // Check is within the bounds 5527 if (gSamusData.yVelocity > gSamusPhysics.yVelocityCap) 5528 velocity = VELOCITY_TO_SUB_PIXEL(gSamusPhysics.yVelocityCap); ⋮ 5531 else 5532 velocity = VELOCITY_TO_SUB_PIXEL(gSamusData.yVelocity); ⋮ 5535 if (gSamusData.yVelocity > -SAMUS_TERMINAL_VELOCITY) 5536 gSamusData.yVelocity -= gSamusPhysics.yAcceleration; ⋮ 5538 gSamusData.yPosition -= velocity;
src/samus.c lines 5526–5538 at a6d6187 · upstream ↗ 324#define SAMUS_Y_ACCELERATION 10 325#define SAMUS_Y_POSITIVE_VELOCITY_CAP 192 ⋮ 336#define SAMUS_HIGH_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + PIXEL_SIZE * 3 + ONE_SUB_PIXEL)) 337#define SAMUS_LOW_JUMP_VELOCITY (SUB_PIXEL_TO_VELOCITY(QUARTER_BLOCK_SIZE + EIGHTH_BLOCK_SIZE))
include/constants/samus.h lines 324–337 at 2aae289 · upstream ↗ 6758 if (pData->yVelocity > gSamusPhysics.positiveYVelocityCap) ⋮ 6760 velocity = DIV_SHIFT(gSamusPhysics.positiveYVelocityCap, 8); ⋮ 6766 else ⋮ 6768 velocity = DIV_SHIFT(pData->yVelocity, 8); ⋮ 6771 if (pData->yVelocity >= -231) ⋮ 6773 pData->yVelocity -= gSamusPhysics.yAcceleration; ⋮ 6776 pData->yPosition -= velocity;
src/samus.c lines 6758–6776 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^On any frame Samus rises (
yVelocityabove 0) without A held, the mid-air handlers setyVelocityto 0, at any height:SamusMidAir,SamusTurningAroundMidAir,SamusSpinning(which runs the spin jump, the Space Jump, and the Screw Attack), andSamusWallJumping, which also drops a wall jump into a spin. The test is each handler's last step but the wall jump's, so a frame whose earlier branch returns skips it. Zero Mission's first three handlers end with the same test.Codemetroidret/mf · metroidret/mzmHow we know
2124 // Check turning 2125 if (turning) 2126 return SPOSE_TURNING_AROUND_MID_AIR; ⋮ 2128 // Check terminate Y velocity if not pressing A and going up 2129 if (!(gButtonInput & KEY_A) && gSamusData.yVelocity > 0) 2130 gSamusData.yVelocity = 0;
src/samus.c lines 2124–2130 at a6d6187 · upstream ↗ 2195 if (!(gButtonInput & KEY_A) && gSamusData.yVelocity > 0) 2196 gSamusData.yVelocity = 0;
src/samus.c lines 2195–2196 at a6d6187 · upstream ↗ 2596 if (!(gButtonInput & KEY_A) && gSamusData.yVelocity > 0) 2597 gSamusData.yVelocity = 0;
src/samus.c lines 2596–2597 at a6d6187 · upstream ↗ 3025 if (!(gButtonInput & KEY_A)) ⋮ 3027 // Stop upwards Y velocity if released A 3028 if (gSamusData.yVelocity > 0) 3029 gSamusData.yVelocity = 0; ⋮ 3031 // Set spinning 3032 gSamusData.pose = SPOSE_SPINNING;
src/samus.c lines 3025–3032 at a6d6187 · upstream ↗ 163 [SPOSE_MID_AIR] = SamusMidAir, 164 [SPOSE_TURNING_AROUND_MID_AIR] = SamusTurningAroundMidAir, ⋮ 169 [SPOSE_STARTING_SPIN_JUMP] = SamusSpinning, 170 [SPOSE_SPINNING] = SamusSpinning, ⋮ 181 [SPOSE_WALL_JUMPING] = SamusWallJumping, ⋮ 188 [SPOSE_SPACE_JUMPING] = SamusSpinning, 189 [SPOSE_SCREW_ATTACKING] = SamusSpinning,
src/data/samus_data.c lines 162–189 at a6d6187 · upstream ↗ 4572 // Check turning 4573 if (turning) 4574 return SPOSE_TURNING_AROUND_MIDAIR; ⋮ 4576 // Check terminate Y velocity if not pressing A and going up 4577 if (!(gButtonInput & KEY_A) && pData->yVelocity > 0) 4578 pData->yVelocity = 0;
src/samus.c lines 4572–4578 at 2aae289 · upstream ↗ 4645 // Check terminate Y velocity if not pressing A and going up 4646 if (!(gButtonInput & KEY_A) && pData->yVelocity > 0) 4647 pData->yVelocity = 0;
src/samus.c lines 4645–4647 at 2aae289 · upstream ↗ 4774 // Check terminate Y velocity if not pressing A and going up 4775 if (!(gButtonInput & KEY_A) && pData->yVelocity > 0) 4776 pData->yVelocity = 0;
src/samus.c lines 4774–4776 at 2aae289 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abTouching an absorbable X parasite runs
SamusAbsorbX, which refills by its color: a red X (SSC_RED_X_PARASITE) 500 energy, 100 missiles, and 10 power bombs; a green X 2 missiles; any other (yellow) X 10 energy.SpriteUtilRefillSamusadds missiles only once a missile flag is set and power bombs only once the power bomb flag is, and stops each at its maximum.Codemetroidret/mfHow we know
482void XParasiteGettingAbsorbedInit(void) ⋮ 497 SamusAbsorbX();
src/x_parasite_util.c lines 482–497 at a6d6187 · upstream ↗ 433void SamusAbsorbX(void) ⋮ 444 case SSP_X_ABSORBABLE_BY_SAMUS: 445 if (gCurrentSprite.samusCollision == SSC_RED_X_PARASITE) ⋮ 447 energy = 500; 448 missiles = 100; 449 powerBombs = 10; ⋮ 453 else if (gCurrentSprite.samusCollision == SSC_GREEN_X_PARASITE) ⋮ 455 energy = 0; 456 missiles = 2; 457 powerBombs = 0; ⋮ 461 else ⋮ 463 energy = 10; ⋮
src/x_parasite_util.c lines 433–470 at a6d6187 · upstream ↗ 2686void SpriteUtilRefillSamus(u16 energy, u16 missiles, u16 powerBombs) ⋮ 2688 gEquipment.currentEnergy += energy; ⋮ 2690 if (gEquipment.currentEnergy > gEquipment.maxEnergy) 2691 gEquipment.currentEnergy = gEquipment.maxEnergy; ⋮ 2693 if (gEquipment.weaponsStatus & MBF_ANY_MISSILE) ⋮ 2695 gEquipment.currentMissiles += missiles; ⋮ 2697 if (gEquipment.currentMissiles > gEquipment.maxMissiles) 2698 gEquipment.currentMissiles = gEquipment.maxMissiles; ⋮ 2701 if (gEquipment.weaponsStatus & MBF_POWER_BOMBS) ⋮ 2703 gEquipment.currentPowerBombs += powerBombs;
src/sprite_util.c lines 2686–2707 at a6d6187 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^When a dying sprite's countdown ends,
SpriteDyingsets its pose to 0x5B (the decompilers'SPRITE_POSE_TURNING_INTO_X) unless it is a secondary sprite, and an enemy's AI (Choot's, for one) runsXParasiteInitin that pose. The X it becomes takes its color from a number from 1 to 1024, made from the low 2 bits ofgSpriteRandomNumberand the 8-bit frame counter, against three weights in that sprite's row ofsPrimarySpriteStats: the top red-weight values give red and the next green-weight values green, each only when its weight is above 0, and everything left below the green bound gives yellow, whatever the yellow weight is, unless it is 0, which deletes the X instead. A would-be green X turns yellow when missiles are full and energy isn't, or Samus has no missiles yet; a would-be yellow X turns green when energy is full and missiles aren't. An X from a core X is green whengSpriteRandomNumberis below 5 or that energy test holds, and otherwise yellow; an X placed in a room is yellow. All of this applies only to an X whose sprite property isSSP_X_ABSORBABLE_BY_SAMUS: one markedSSP_X_UNABSORBABLE_BY_SAMUS, or the unnamedSSP_UNKNOWN_40, is always yellow, and any other property deletes the X.Codemetroidret/mfrevisedHow we know
561 case SSP_X_ABSORBABLE_BY_SAMUS: 562 if (type == X_PARASITE_TYPE_FROM_CORE_X) ⋮ 564 if (gSpriteRandomNumber < 5 || SpriteUtilCheckEnergyFullAndMissilesNotFull()) ⋮ 566 gCurrentSprite.pOam = sXParasiteOam_Green; 567 gCurrentSprite.samusCollision = SSC_GREEN_X_PARASITE; ⋮ 569 else ⋮ 571 gCurrentSprite.pOam = sXParasiteOam_Yellow;
src/x_parasite_util.c lines 561–572 at a6d6187 · upstream ↗ 575 else if (type == X_PARASITE_TYPE_FROM_SPRITE) ⋮ 577 // Calculate random number between 1 and 1024 inclusive 578 randomNumber = gSpriteRandomNumber & 3; 579 randomNumber <<= 8; 580 randomNumber |= gFrameCounter8Bit; 581 randomNumber++; ⋮ 583 yellowXThreshold = sPrimarySpriteStats[spriteId][4]; 584 greenXThreshold = sPrimarySpriteStats[spriteId][5]; 585 redXThreshold = sPrimarySpriteStats[spriteId][6]; ⋮ 589 redXThreshold = 1024 - redXThreshold; 590 if (randomNumber <= 1024 && randomNumber > redXThreshold) ⋮ 598 else ⋮
src/x_parasite_util.c lines 574–655 at a6d6187 · upstream ↗ 657 case SSP_X_UNABSORBABLE_BY_SAMUS: 658 gCurrentSprite.invincibilityStunFlashTimer &= 0x7f; 659 gCurrentSprite.pOam = sXParasiteOam_Yellow; ⋮ 661 break; ⋮ 663 case SSP_UNKNOWN_40: 664 gCurrentSprite.pOam = sXParasiteOam_Yellow; 665 gCurrentSprite.samusCollision = SSC_YELLOW_X_PARASITE; 666 break; ⋮ 668 default: 669 gCurrentSprite.status = 0;
src/x_parasite_util.c lines 657–669 at a6d6187 · upstream ↗ 509void SpriteDying(void) ⋮ 519 if (--gCurrentSprite.work1 == 0) ⋮ 521 if (gCurrentSprite.properties & SP_SECONDARY_SPRITE) ⋮ 523 gCurrentSprite.status = 0; ⋮ 525 else ⋮ 527 gCurrentSprite.pose = 0x5b;
src/x_parasite_util.c lines 509–530 at a6d6187 · upstream ↗ 328 case 0x57: 329 SpriteDyingInit(); 330 case 0x58: 331 SpriteDying(); 332 break; ⋮ 338 break; ⋮ 340 case 0x5b: 341 XParasiteInit(); 342 break;
src/sprites_AI/choot.c lines 328–342 at a6d6187 · upstream ↗ 477 SPRITE_POSE_TURNING_INTO_X = 0x5B,include/constants/sprite.h line 477 at a6d6187 · upstream ↗ 2746u8 SpriteUtilCheckEnergyFullAndMissilesNotFull(void) ⋮ 2748 if (gEquipment.currentEnergy == gEquipment.maxEnergy) ⋮ 2750 if (gEquipment.weaponsStatus & MBF_ANY_MISSILE && gEquipment.currentMissiles < gEquipment.maxMissiles) 2751 return TRUE; ⋮ 2762u8 SpriteUtilCheckMissilesFullAndEnergyNotFull(void) ⋮ 2764 if (!(gEquipment.weaponsStatus & MBF_ANY_MISSILE)) 2765 return TRUE; ⋮ 2767 if (gEquipment.currentMissiles == gEquipment.maxMissiles && gEquipment.currentEnergy < gEquipment.maxEnergy) 2768 return TRUE;
src/sprite_util.c lines 2746–2770 at a6d6187 · upstream ↗ 697 case PSPRITE_X_PARASITE_IN_ROOM: 698 case PSPRITE_X_PARASITE_IN_ROOM_PROXIMITY: ⋮ 704 XParasiteDetermineColor(X_PARASITE_TYPE_IN_ROOM, 0); 705 break; ⋮ 720 XParasiteDetermineColor(X_PARASITE_TYPE_FROM_CORE_X, 0); ⋮ 726 break; ⋮ 728 case PSPRITE_X_PARASITE: 729 case PSPRITE_X_PARASITE_AQUA_ZEBESIAN: 730 XParasiteSpawningInit(); 731 XParasiteDetermineColor(X_PARASITE_TYPE_FROM_SPRITE, gCurrentSprite.roomSlot); 732 break; ⋮ 734 default: ⋮
src/x_parasite_util.c lines 697–737 at a6d6187 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^When an enemy dies, a
RandomNumber1below 16 drops nothing; otherwise its low 3 bits pick fromItemDropTbl, which holds a missile at 0 and 3, energy at 1, 4, and 6, and$89at 2, 5, and 7.Codenmikstas/metroid-disassembly · “Metroid (USA)”How we know
4575 LDA RandomNumber1 4576 CMP #$10 4577 BCC LDD5B 4578* AND #$07 4579 TAY 4580 LDA ItemDropTbl,y 4581 STA EnAnimFrame,x
Source_Files/Bank07.asm lines 4575–4581 at 4270d57 · upstream ↗ 4722;The following table determines what, if any, items an enemy will drop when it is killed. ⋮ 4724ItemDropTbl: 4725LDE35: .byte $80 ;Missile. 4726LDE36: .byte $81 ;Energy. 4727LDE37: .byte $89 ;No item. 4728LDE38: .byte $80 ;Missile. 4729LDE39: .byte $81 ;Energy. 4730LDE3A: .byte $89 ;No item. 4731LDE3B: .byte $81 ;Energy. 4732LDE3C: .byte $89 ;No item.
Source_Files/Bank07.asm lines 4722–4732 at 4270d57 · upstream ↗ In the cartridge (“Metroid (USA)”): bank 7 $DCFC, the drop code; bank 7 $DE35, ItemDropTbl. We don't reproduce the bytes.
Checked against the evidence by its author, an AI model, 2026-10-20.
Checked by a second AI model, 2026-10-21.
Sources
Code
- metroidret contributors. 2026. “mf: a decompilation of Metroid Fusion (metroidret/mf).” Commit a6d6187. metroidret/mf @ a6d6187 · licence, all uses
- metroidret contributors. 2026. “mzm: a decompilation of Metroid: Zero Mission (metroidret/mzm).” Commit 2aae289. metroidret/mzm @ 2aae289 · licence, all uses
- nmikstas and Kent Hansen. 2025. “metroid-disassembly: Metroid software disassembly.” Commit 4270d57. nmikstas/metroid-disassembly @ 4270d57 · licence, all uses
- n.d. “Metroid (USA).” · licence, all uses