News:

Don't forget to visit the main site! There's lots of helpful docs, patches, and more!

Main Menu

Random Metroid 2 Facts

Started by RT-55J, August 13, 2022, 03:05:59 PM

Previous topic - Next topic

RT-55J

Welcome to this thread where I post random facts about Metroid 2 as I run into them.




Apparently there's code to hurt the queen for 10 damage!

func_03_7785: ; 03:7785 - State $0F
    ...

    ld a, [$c3d3]
    sub $0a ; Hurt for 10 damage
    ld [$c3d3], a


I did some testing, and confirmed this is indeed used.

If you let the queen eat you, but just stay in her mouth (without rolling in), the bombs to 10 damage to her (though she spits you out immediately). In comparison, normal missiles do 1 damage each, and the stomach special does a full 30 damage.

I do wonder how the math shakes out for low% (probably not worth it).




Feel free to share any random facts you come across!

RT-55J

How about some extremely pointless missing contentâ„¢ ?



This enemy, the "Gullugg", only moves clockwise in-game. Apparently there are unused/unreferenced tables that would have made it move anti-clockwise, though no code paths exist to activate it in the vanilla game.

You could, however, apply the following hex tweak to force it to go counter-clockwise.

0x9CEA: 85 -> 0C
0x9CFF: C2 -> 49


(would anybody want this as a game genie code?)

RT-55J

Missile doors in this game are interesting. They have code that allows to to tell if the door was hit from the right or the left, but there is no code that would allow them to control the direction they face.

By poking at the x position in RAM, I was able to discover the horrifying truth of how they worked (make a guess before you click the spoiler):

[spoiler]

It turns out that the left and right door caps are a single object, with a single shared hitbox, conjoined at all times.

lmao[/spoiler]

RT-55J

Immediately after the table that defines the transverse (sideways) motion of the wave beam, there's another table that is unused, but appears to work with the wave beam as well:

.waveSpeedTable: ; 01:5183 - Wave speed table (transverse velocity)
    db $00, $07, $05, $02, $00, $fe, $fb, $f9, $00, $f9, $fb, $fe, $00, $02, $05, $07
    db $80

.waveSpeedTable_alt: ; 01:5194 - Unused (alternate wave table)
    db $0A, $F6, $F6, $0A, $0A, $F6, $F6, $0A, $0A, $F6, $F6, $0A, $80


Here's what it looks like in motion:



I blame my gif recorder/your browser for making it look jankier than it is, but in person it just looks like a more blurry/flickery version of the Spazer.

This is just speculation on my part, but perhaps this was a earlier version of the spazer. Alternatively, this might have been an early version of the Wave Beam, but the the visual similarity between it and the Spazer made them upgrade the Wave Beam's pattern to something more sophisticated.

Whatever the case may be, you can experience this ~scintillating~ change with the following hex tweak:

0x50D5: 83 to 94

Enjoy!

RT-55J

Ever wondered how much damage each weapon does? Well, here's what the weapon damage table says:

weaponDamageTable: ; 02:43C8
    db $01 ; Weapon 0 - Power Beam
    db $02 ; Weapon 1 - Ice Beam
    db $04 ; Weapon 2 - Wave Beam
    db $08 ; Weapon 3 - Spazer Beam
    db $1E ; Weapon 4 - Plasma Beam (30!?)
    db $00 ; Weapon 5 - [unused]
    db $00 ; Weapon 6 - [unused]
    db $02 ; Weapon 7 - Bomb Beam [unused]
    db $14 ; Weapon 8 - Missiles (20!)
    db $0A ; Weapon 9 - Bomb Explosion


Plasma sends out 3 projectiles in a row, while Spazer stacks them on top of each other. Both are strong enough to make quick work of nearly any enemy.

However, the Wave Beam does have one advantage over every other beam: It is able to pierce the armor of enemies with directional-invulnerabilities (such as the halzyns, ramulkens, and motos).

A few enemies, such as Arachnus and the Metroids, ignore this table because they're technically invincible! Instead, they handle the damage values themselves. Arachnus takes 1 damage from bombs, and the Metroids take 1 damage from missiles (and the Omega Metroids take 3 damage from the back).

Now, ignoring those exceptions, only two non-Metroid enemies take more than 1 missile to kill:
- The blob thrower (the plant above Arachnus)
- The Gunzoo (the robots inside the tower)

RT-55J

The Metroid Queen has a jump table (basically, a list of function pointers) in bank 3 for each of her states. At the end of it is this mysterious entry:

dw $5651

That pointer does not lead to any sort of sensible looking code. In fact, it points to the enemy map data for screen (3, 1) of bank B. By sheer coincidence this is the "ret nc" opcode, all because a common little bug enemy had an x position of $D0. Fortunately, because of that coincidence and the Queen's state transitions being well-controlled, this doesn't appear to be a major cause for concern re:causing a crash.

Still, one wonders why this is the case.

Well, if we look in bank 2, we see this function at address $5651:

enAI_NULL: ; 02:5651
    ret


That's just the default, "do nothing" stub for enemies with no proper AI (they're used in like one or two places for cosmetic purposes IIRC). From this we can surmise that the Queen's AI originally resided in bank 2 and was moved when they ran out of space, but the pointer to enAI_null wasn't updated to refer to something in the correct bank.

To me, that's a pretty funny mistake.

Some context that makes this even funnier to me is that Arachnus's AI follows a similar design pattern with a jump table that also ends with a dummy state that doesn't do anything:

        dw .state_0
        dw .state_1
        dw .state_2
        dw .state_3
        dw .state_4
        dw .state_5
        dw .state_6
        dw enAI_NULL ; $5651


Perhaps these two bosses were programmed by the same individual.

RT-55J

Alpha and Gamma Metroids do some very weird math:


someone who knows trigonometry, pls help these metroids. their family is dying :( :( :(

Perhaps a list of the left circle (the one labelled "Alpha") would make the oddness clearer:

; $00 - Right                 (3,0)
; $01 - Left                 (-3,0)
; $02 - Down                  (0,3)
; $03 - Up                    (0,-3)
; $04 - Bottom right quadrant (3,1)
; $05 -  ""                   (2,2)
; $06 -  ""                   (1,3)
; $07 - Bottom left quadrant (-3,1)
; $08 -  ""                  (-2,2)
; $09 -  ""                  (-1,3)
; $0A - Upper right quadrant (3,-1)
; $0B -  ""                  (2,-2)
; $0C -  ""                  (1,-3)
; $0D - Upper left quadrant (-3,-1)
; $0E -  ""                 (-2,-2)
; $0F -  ""                 (-1,-3)


You got the cardinal directions for entries 0-3, and then each quadrant separately, ordered from least steep to most steep.

These values are indexes into a jump table (!!) that provide the Y and X speed vectors that the Alpha Metroid uses to move (oh, and they're in signed-magnitude format rather than two's compliment (if that means anything to you)).

How an alpha metroid's AI works is that it picks one of the above directions to lunge towards, uses that direction value for a few frames as it moves, pauses for a short bit, and then picks another direction again. How exactly it picks the direction to move is the bit that interests me.

To start off, there's this table that is a list of indexes into the jump table referenced above:

alpha_angleTable: ; 01:7158
    ; $00 - Cardinal directions
    db $00, $01, $02, $03
    ; $04 - Bottom right quadrant
    db $00, $04, $05, $06, $02
    ; $09 - Bottom left quadrant
    db $01, $07, $08, $09, $02
    ; $0E - Top right quadrant
    db $00, $0A, $0B, $0C, $03
    ; $13 - Top left quadrant
    db $01, $0D, $0E, $0F, $03


In this table, you can see how the cardinal directions are paired with their appropriate quadrants, so it's a bit more sensible seeming at this point.

Now first, it checks if you are horizontally or vertically aligned, and if so just assigns the metroid a cardinal direction accordingly. If you are not aligned, it checks which quadrant you are in, and then puts the metroid's angle at the start of the corresponding row in that table.

Next, it performs an actual, legit-seeming trigonometric operation to produce a 16-bit angle (?) of your relative position in the quadrant. Then, based on that angle, it chooses whether to add 0 to 4 to it's position in the table above to get the final angle (4 being the steepest).

I am not familiar with how 8-bit games did or did not do trigonometry, but coming from the modern world where we can just call atan2(), this seems like a very labored and error-prone approach.

Anyhow, it's worth mentioning that the gamma metroid uses different functions and tables to implement basically the exact same algorithm (just with more angles in the end).




For those who are curious, the Actual Trigonometry that was performed in one step is was in these functions here. I don't know exactly what trig function they implement, but the algorithm at work seems very CORDIC-like:

; metroid_absSamusDistY and metroid_absSamusDistX are the absolute values of the distances between Samus and the metroid in question

trigFunction: ; 01:7170
    ld b, $64
    ld a, [metroid_absSamusDistY]
    ld e, a
    call function_A

    ld a, [metroid_absSamusDistX]
    ld c, a
    call function_B

    ld a, l
    ld [resultLowByte], a
    ld a, h
    ld [resultHighByte], a
ret

function_A: ; 01:73B9
    ; B is set to $64 before entry
    ; E is metroid_absSamusDistY
    ld hl, $0000
    ld c, l
    ld a, $08

    .loop:
        srl b
        rr c
        sla e
        jr nc, .endIf
            add hl, bc
        .endIf:
        dec a
    jr nz, .loop
ret

function_B: ; 01:73CC
    ; C is metroid_absSamusDistX
    ld a, h
    or l
        ret z
    ld de, $0000
    ld b, $10
    sla l
    rl h
    rl e
    rl d

    .loop:
        ld a, e
        sub c
        ld a, d
        sbc $00
        jr c, .endIf
            ld a, e
            sub c
            ld e, a
            ld a, d
            sbc $00
            ld d, a
        .endIf:
        ccf
        rl l
        rl h
        rl e
        rl d
        dec b
    jr nz, .loop
ret


Cookie for anybody who can figure out what exactly going on here, in actual math terms.

RT-55J

Random thing I noticed when messing with the Gunzoo enemy: Apparently if you're facing away from a solid sprite-object while damage boosting, you can go through it. For instance, look at Samus in this door:



I tested this when in morph ball as well, an you can actually get damaged boosted into the door transition this way.

As it is, the Samus in the screenshot is stuck in the door. The Gunzoo's projectiles do pass through the door, but they can't hurt Samus again when she's inside of it (odd).

I can aim up to fire missiles at the door, but if I didn't have any missiles this would be a softlock.

What a game (I love it)

RT-55J

Zeta and Omega Metroids are interesting. They have a lot of similarities, but also a lot of differences. For instance, both use the same movement routines and parameters to chase Samus, but the conditions they use are different.

The Zeta Metroid seeks Samus until she is within a certain range above Samus. It seeks toward the point at the top of Samus's head when she is standing. However, since crouching/morphing does not modify that point, you can do this silly party trick:



The Omega Metroid's chasing works on a time-based system rather than a position-based one. Basically, it has 5 different time periods it can choose to chase you, from $0C (12 frames) to $60 (96 frames):

chaseTimerState 0: $0C
chaseTimerState 1: $14
chaseTimerState 2: $28
chaseTimerState 3: $40
chaseTimerState 4: $60


Normally, it just loops through them in order, from 0 to 4, 0 to 4, etc. Each state is longer than the previous.

However, if you take enough damage between swoops it will go back to period number 0 (though it only checks the low byte of your health (lol)). Also, if you hit it with the screw attack you force it to use period number 3 next time it swoops.

Now, what's really interesting is that if you are crouching then the omega metroid will skip swooping and move on to its next states (where it waits for a second and stuff). However, if the omega is on period number 4, then it will swoop no matter what.

Maybe this is useful for low% sickos or something (or maybe they already know this).

P.Yoshi

I just wanna say I love all these cool facts. C:

Good luck with all the disassembly stuff, RT-55J!

RT-55J

Thanks! The means a lot coming from you.

Quote from: RT-55J on August 13, 2022, 03:39:18 PM
Ever wondered how much damage each weapon does? Well, here's what the weapon damage table says:

    db $02 ; Weapon 1 - Ice Beam

One interesting thing I found out yesterday is that while the Ice Beam does indeed do 2 damage, it does not read from this table!

Instead, it has a special case separate from the rest of the beams where it manually decrements the enemy's health twice.

Also, one interesting behavior I noticed regarding the ice beam is that it does not kill an enemy once it reaches zero health. Instead, it only kills an enemy if it is at zero health and you shoot it again. If an enemy is at zero health and you let it unfreeze, it will instantly die without making an explosion or a drop (but it still makes the sound effect).




Enemy invincibility is handled in a few ways:
- Enemies with $FF health are invincible and unable to be frozen.
- Enemies with $FE health are invincible but able to be frozen (this appears to be unused, from what I can tell). They can be killed by the screw attack (however, this might be an oversight).
- Enemy sprites within the range of $A0-$CF are excluded from the normal subroutine that applies damage (this is all of the metroid sprites).
- There is a byte in the enemy's working RAM that determines what directions it is invulnerable to beam fire from, which is how the Moto, Halzyn, and other enemies have those hard plates you can't shoot through. As mentioned previously, the wave beam ignores this.

There is also some sort of special case related to enemy health being $FD (not invincibility related), but I can't exactly make sense of it.




One very surprising thing I've discovered is that Metroid 2 does not have a random number generator!

Instead, any time where it would try reading from a pseudo-RNG, it reads this 8-bit hardware register instead:

FF04 - DIV - Divider Register (R/W)
This register is incremented at rate of 16384Hz [...] Writing any value to this register resets it to 00h.

Source

(It appears that the 16384 Hz value is just the 4.19MHz master clock rate of the system divided 256. That ends up being around 273 ticks/frame.)

Usually it just reads the lower bits to make a coinflip, which is convenient since those change the most often. While this probably isn't the ideal solution for something that needs a lot of big numbers fast like an RPG, it is remarkably effective for this game's purposes.

RT-55J

A while ago I watched a video of all the of this game's endings and saw something weird with the worst ending. Once the time appeared, all of the stars disappeared!!



I was like "Huh, I wonder what's causing that?" At most, all I noticed was that there appeared to be no code paths that would allow the star rendering to be disabled. I was very flummoxed, though I didn't care enough to actually look too deeply into it.

It turns out the answer is very silly:


(pretend that's my actual mouse cursor there at the bottom instead of a pretend one I reinserted with MS Paint)

The reason? It's because the standing pose and the timer use exactly the total number of sprites that GB can handle. The star rendering code does get called, but there aren't any free sprite slots left.

you lose, mr. slow person

:twisted: enjoy your darkness :twisted:




Bonus Fun Fact: When initializing the positions of the stars in the ending, it only ends up reading the positions of the first 8 stars by accident, when in fact that there 16 initial position defined and 16 stars rendered every frame. In practice, this results in there being 9 stars (at most) rendered, with the 9th actually being 8 stars superimposed on top of each other.

However, as we noticed with the previous fact, the amount of sprites the Game Boy has to work with is actually quite tight. If you attempt to fix this issue and have all 16 stars in separate positions, what you get in practice is a bunch of stars flickering in and out of existence. It's actually quite jarring to look at, so if this was indeed an accident then it sure was a lucky one.




Bonus Bonus Fun Fact: There is an unused routine in between all of the other routines for the credits that reboots the game. This seems to indicate that, at least at one point in development, they intended you be able to return to the title screen after the credits.
Though with the soft reset code (A+B+Start+Select) and the fact that the Game Boy's power switch is right there, it's only marginally useful. Besides, it would have put you at risk of losing your time before you could take a picture of it for Nintendo Power! (idk if they ever had a time attack competition for this tho)


RT-55J

If the earthquake noise is playing, then that will prevent any item fanfare from playing.