Without analyzing the code
I'd risk to say that the state of Mario consists of 2 different variables:
- big or small mario state
- big or small mario image
When you're hit and you end a level, the game only updates one of the variables: mario state, and doesn't have time to update the other one, resulting in an inconsistency. This should not happen but it did.
Now when you pick the mushroom, since the state is known as "small mario", the game probably toggles the sprite (by EORing it), so it swaps it, which can explain why mario doesn't remain big, but swaps to small.
And note that there is no firing frame as small. In this video https://www.youtube.com/watch?v=v2lVgHaRDf8 we can witness that firing mario is small unless when he fires. The frame where he fires is the one from a big mario.
Now why are there 2 variables/states for the same thing? Because there's a transition from big to small and reverse. When the level is completed, the transition isn't done / is aborted before it can happen, and Mario remains big.
With analysis of the code
Now you can find a comprehensive SMB 6502 disassembly on github. It seems to confirm what I suspected:
PlayerSize = $0754
PlayerStatus = $0756
as you see, there is one variable for size and another one for status.
The status variable is changed here:
Shroom_Flower_PUp:
lda PlayerStatus ;if player status = small, branch
beq UpToSuper
cmp #$01 ;if player status not super, leave
bne NoPUp
ldx ObjectOffset ;get enemy offset, not necessary
lda #$02 ;set player status to fiery
sta PlayerStatus
jsr GetPlayerColors ;run sub to change colors of player
ldx ObjectOffset ;get enemy offset again, and again not necessary
lda #$0c ;set value to be used by subroutine tree (fiery)
jmp UpToFiery ;jump to set values accordingly
The size isn't changed there. It's done somewhere else.
The transition code uses a third variable PlayerChangeSizeFlag
, and as predicted, when the flag reaches a given value, an inversion of the size variable is done (using eor #$01
on it), which explains that if the size is big, it becomes small:
InitChangeSize:
ldy PlayerChangeSizeFlag ;if growing/shrinking flag already set
bne ExitBoth ;then branch to leave
sty PlayerAnimCtrl ;otherwise initialize player's animation frame control
inc PlayerChangeSizeFlag ;set growing/shrinking flag
lda PlayerSize
eor #$01 ;invert player's size
sta PlayerSize
ExitBoth: rts ;leave
Note that InitChangeSize
is the same routine used to make Mario bigger when he gets a mushroom, or smaller when he's hit.
When Mario is hit just when the level ends, PlayerState
is updated immediately to "small", but InitChangeSize
is probably not called or not called enough times to reach the state where it changes PlayerSize
The fix is rather easy if one wants to patch the game: at the start of the level, propagate PlayerState
to PlayerSize
to correct the desynchronization.