boss gemini helmet glitch documentation

This commit is contained in:
Michael Miceli 2023-04-28 23:46:55 -04:00
parent eb94c2a0cd
commit a6cb97d08d
5 changed files with 134 additions and 27 deletions

View File

@ -65,4 +65,89 @@ Extracted sound sample: [sound_ff.mp3](attachments/sound_ff.mp3?raw=true)
Note that the boss defeated audio (`sound_55`) is still played because the enemy
defeated routine is set to `boss_ufo_routine_09` (see
`enemy_destroyed_routine_05`). `boss_ufo_routine_09` plays `sound_55`.
`enemy_destroyed_routine_05`). `boss_ufo_routine_09` plays `sound_55`.
# 3. Level 4 Boss Gemini Vulnerability
Under normal circumstances, when the level 4 indoor/base boss gemini helmet
(enemy type = #$1c) splits into 'phantoms', then they don't take damage. Only
when the helmet re-combines is it vulnerable to damage. However, `Mr. K`
[researched](http://www.youtube.com/watch?v=hL1BMFRt6aA) a glitch to find that
if a player's bullet collides with the helmet at just the right frame, then when
the helmet separates into two, it can still take damage.
The reason this happens is because the boss gemini uses `ENEMY_ANIMATION_DELAY`
to know when to be vulnerable or when to be invulnerable.
`ENEMY_ANIMATION_DELAY` specifies how long for the helmet to stay still when
merged, or when at the farthest distance apart. The timer is set to #$20 when
farthest apart, and #$30 when merged. If the helmets are moving (either toward
each other or away), the value will be #$00.
When merged and `ENEMY_ANIMATION_DELAY` is 2, the helmet is not moving, but
about to start separating. The value will be decremented to 1. This logic
happens in `boss_gemini_routine_02`. After `boss_gemini_routine_02` runs,
`bullet_enemy_collision_test` is executed to check for a bullet to enemy
collision. If during this frame, a bullet hits the gemini, then the
`ENEMY_ROUTINE` is updated to `boss_gemini_routine_03`. This is known as the
enemy destroyed routine and it will be executed in the next frame. However,
boss gemini is special, in that it isn't automatically destroyed in the
destroyed routine. Instead, unless boss gemini doesn't have any more health
(`ENEMY_VAR_4`), the routine will decrement `ENEMY_ANIMATION_DELAY` to 0 and
set back to `boss_gemini_routine_02` to be called the next frame.
Now when the next frame executes and the `boss_gemini_routine_02` routine is
run, `ENEMY_ANIMATION_DELAY` is 0 and game thinks that the code that makes the
helmet invulnerable has already been executed when it hasn't!
In short, the bug happens because for the special time when
`ENEMY_ANIMATION_DELAY` is decremented from 1 to 0, the code should make the
helmet invincible by calling `disable_enemy_collision`. However, if you time
the bullet collision to happen on the frame when `ENEMY_ANIMATION_DELAY` is 2,
then the regular game code will decrement the timer to 1, and then next frame
will have a different routine (`boss_gemini_routine_03`) set the timer to
0, but that routine doesn't call `disable_enemy_collision`, leaving the helmet
in a vulnerable state.
Interestingly, if you take advantage of this bug, then you can exploit the same
logic mistake when the helmet is not moving at the edge of the screen, before it
starts merging. If a bullet collides with the helmet right when
`ENEMY_ANIMATION_DELAY` is 2, then the helmets will remain vulnerable while
merging.
```
; ----- Frame 1 -----
boss_gemini_routine_02:
...
lda ENEMY_ANIMATION_DELAY,x ; ENEMY_ANIMATION_DELAY = 2
beq @calc_offset_set_pos ; branch doesn't occur
dec ENEMY_ANIMATION_DELAY,x ; ENEMY_ANIMATION_DELAY = 1
bne @set_x_pos ; helmet still not moving, branch
...
rts
...
bullet_enemy_collision_test:
...
jsr bullet_collision_logic ; set boss gemini routine `boss_gemini_routine_03`
; ----- Frame 2 -----
boss_gemini_routine_03:
lda ENEMY_ANIMATION_DELAY,x ; ENEMY_ANIMATION_DELAY = 1
beq @continue ; branch doesn't occur
dec ENEMY_ANIMATION_DELAY,x ; ENEMY_ANIMATION_DELAY = 0
...
lda #$03 ; a = #$03
jmp set_enemy_routine_to_a ; set enemy routine index to boss_gemini_routine_02
; ----- Frame 3 -----
boss_gemini_routine_02:
lda ENEMY_ANIMATION_DELAY,x ; ENEMY_ANIMATION_DELAY = 0
beq @calc_offset_set_pos ; branch skipping disabling of collision!!
dec ENEMY_ANIMATION_DELAY,x ; skipped!
bne @set_x_pos ; skipped!
jsr disable_enemy_collision ; skipped!
@calc_offset_set_pos:
...
```

View File

@ -523,25 +523,33 @@ The health of the boss gemini helmets are #$01 and each hit 'destroys' them.
However, the destroyed routine `boss_gemini_routine_03` will check `ENEMY_VAR_4`
for the boss gemini helmet's actual HP.
Note that this enemy uses `ENEMY_Y_VELOCITY_FRACT` and `ENEMY_Y_VELOCITY_FAST`
not for anything with the y velocity, but rather to control speed of x movement
and keep track of x distance from initial position respectively.
* `ENEMY_FRAME` - offset into `boss_gemini_sprite_tbl`, which contains the exact
sprite code: `sprite_68`, `sprite_69`, `sprite_6b`, `sprite_6c`.
* `ENEMY_VAR_1` - initial x position
* `ENEMY_VAR_2` - timer after being hit - #$00 down to #$00
* `ENEMY_VAR_2` - timer after being hit - #$10 down to #$00
* `ENEMY_VAR_3` - whether or not the boss gemini's health is low (less than
#$07). Used to show a red brain instead of a green one.
* `ENEMY_VAR_4` - actual representation of ENEMY_HP
* `ENEMY_X_VELOCITY_FRACT` - always #$80 (.50). Used with
`ENEMY_Y_VELOCITY_FRACT` to move gemini by 1 every #$02 frames
* `ENEMY_X_VELOCITY_FAST` - x direction of boss gemini
* #$00 - boss gemini are travelling away from center
* #$ff - boss gemini are travelling towards center
* #$00 - boss gemini are traveling away from center
* #$ff - boss gemini are traveling towards center
* `ENEMY_Y_VELOCITY_FRACT` - alternates every frame between #$00 and #$80. Used
with `ENEMY_Y_VELOCITY_FRACT` to move gemini by 1 every #$02 frames
* `ENEMY_Y_VELOCITY_FAST` - offset from initial x position. Either added to or
subtracted `ENEMY_VAR_1` based on whether the frame is even or odd. Always
either #$00 or #$80
subtracted `ENEMY_VAR_1` based on whether the frame is even or odd. Goes from
#$00 to #$30
* `ENEMY_HP` - always #$01 until hit by bullet. The 'enemy destroyed' routine
will reset `ENEMY_HP` back to #$01 until `ENEMY_VAR_4` is #$00.
* `ENEMY_ANIMATION_DELAY` - how long for the helmet to stay still when merged,
or when farthest distance apart. The value is set to #$20 in game for when
farthest apart, and #$30 when merged. If the helmets are moving (either
toward each other or away), the value will be #$00.
### 1D - Gardegura
@ -718,7 +726,7 @@ simplify defining the enemy vars.
* all other orbs - the running total of index into `dragon_arm_orb_pos_tbl`.
Each farther orb has the next value. Then the orb's `ENEMY_VAR_1` is added to
get position.
* `ENEMY_VAR_1` - used in correlation wiht shoulder's `ENEMY_X_VELOCITY_FRACT`
* `ENEMY_VAR_1` - used in correlation with shoulder's `ENEMY_X_VELOCITY_FRACT`
to set position. `ENEMY_VAR_1` is the distance from the previous orb's
position index.
* `ENEMY_VAR_2` - duration timer for rotation direction. positive = clockwise,
@ -797,7 +805,7 @@ No attributes exist for this enemy.
Appears randomly and creates flying saucers (enemy type - #$15) and drop bombs
(enemy type - #$16). One of the few enemies that use BG_PALETTE_ADJ_TIMER to
create a fadeing in effect.
create a fading in effect.
#### Logic

View File

@ -5514,8 +5514,16 @@ boss_gemini_routine_02:
@wait_delay_update_pos:
lda ENEMY_ANIMATION_DELAY,x ; load enemy animation frame delay counter
beq @calc_offset_set_pos ; branch to calculate new position if animation delay has elapsed
dec ENEMY_ANIMATION_DELAY,x ; animation delay hasn't elapsed, decrement enemy animation frame delay counter
; this specifies how long the helmets should freeze when merged or when really far apart
; always #$00 when moving
; !(BUG?) if a bullet collision with the boss gemini occurs in a frame when ENEMY_ANIMATION_DELAY is #$02
; then the disable_enemy_collision method will not be called
; and the boss gemini will be vulnerable until the next time it starts moving again
beq @calc_offset_set_pos ; branch if moving, i.e. animation delay is #$00,
; or animation delay has elapsed and helmets should start moving
; to calculate new position
dec ENEMY_ANIMATION_DELAY,x ; helmets are staying still, either merged, or really far apart
; decrement enemy animation frame delay counter
bne @set_x_pos ; if animation delay still hasn't elapsed, set position based on FRAME_COUNTER and ENEMY_VAR_1
jsr disable_enemy_collision ; animation delay has elapsed and boss gemini are about to separate
; prevent player enemy collision check and allow bullets to pass through enemy
@ -5525,25 +5533,29 @@ boss_gemini_routine_02:
clc ; clear carry in preparation for addition
adc ENEMY_X_VELOCITY_FRACT,x ; load x fractional velocity. Always #$80 (.5)
sta ENEMY_Y_VELOCITY_FRACT,x ; store result back into x position offset
; this overflows every #$02 frames, causing ENEMY_Y_VELOCITY_FAST to increment
; this overflows every #$02 frames, causing ENEMY_Y_VELOCITY_FAST (x position) to increment
; every other frame
lda ENEMY_Y_VELOCITY_FAST,x ; load x position offset (#$00 or #$80)
lda ENEMY_Y_VELOCITY_FAST,x ; load x position offset from merge point (#$00 to #$30)
adc ENEMY_X_VELOCITY_FAST,x ; add x direction (#$00 = away from center, #$ff = towards center)
; this includes any overflow from previous addition
sta ENEMY_Y_VELOCITY_FAST,x ; set x position offset
sta ENEMY_Y_VELOCITY_FAST,x ; set new x position offset (#$00 to #$30)
ldy ENEMY_X_VELOCITY_FAST,x ; load x direction (#$00 = away from center, #$ff = towards center)
bmi @check_combined_set_x ; branch if boss gemini helmets are going to the center (combining), or have combined
cmp #$30 ; see if x position offset is at maximum (#$30)
bcc @set_x_pos ; branch if x position offset is less than max (#$30)
lda #$20 ; x position offset is max, reset to #$20
lda #$20 ; x position offset is max, set animation delay to #$20
bne @set_delay_reverse_dir ; always branch to reverse direction
; phantom helmets moving toward center (merging)
@check_combined_set_x:
tay ; transfer x position offset to y
bpl @set_x_pos ; branch if boss gemini haven't yet combined
tay ; transfer x position away from merge point (#$00 to #$30) offset to y
; x position will temporarily underflow to #$ff (-1)
; this is when helmet freeze for ENEMY_ANIMATION_DELAY amount of time
bpl @set_x_pos ; branch if boss gemini haven't yet combined, i.e. their offset isn't #$ff
jsr set_enemy_y_velocity_to_0 ; boss gemini have combined to become solid, pause motion
jsr enable_bullet_enemy_collision ; allow bullets to collide (and stop) upon colliding with boss gemini
lda #$30 ; a = #$30 (delay when gemini is fused)
lda #$30 ; a = #$30 (delay when gemini is not moving)
; either merged or really far apart
@set_delay_reverse_dir:
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter

View File

@ -125,7 +125,7 @@ game_end_routine_tbl:
.addr game_end_routine_04 ; CPU address $bae3 (music change and presented by Konami)
.addr game_end_routine_05 ; CPU address $bb87
; set level to #$08 (ending routing)
; set level to #$08 (ending routine)
game_end_routine_00:
lda #$08 ; a = #$08
sta CURRENT_LEVEL ; set current level to 'level 9' (special ending level)

View File

@ -6859,14 +6859,14 @@ bullet_enemy_collision_test:
cmp #$80
bcs @next_bullet
lda PLAYER_BULLET_OWNER,x
jsr bullet_collision_logic
lda PLAYER_BULLET_SLOT,x
cmp #$05
bne @set_routine_exit
stx $08
txa
jsr bullet_collision_logic ; subtract enemy HP, play collision sound (if appropriate), award points
lda PLAYER_BULLET_SLOT,x ; load bullet type + 1
cmp #$05 ; see if laser
bne @set_routine_exit ; branch if not laser
stx $08 ; laser, store bullet slot number in $08
txa ; move bullet slot number to a
ldx #$00 ; x = #$00
cmp #$0a
cmp #$0a ; see if bullet slot number to #$0a
bcc @continue_2
ldx #$0a ; x = #$0a
@ -6879,8 +6879,9 @@ bullet_enemy_collision_test:
bne @continue_2
@set_routine_exit:
jsr set_bullet_routine_to_2 ; move to bullet routine 2 and reset PLAYER_BULLET_TIMER to #$06
ldx ENEMY_CURRENT_SLOT
jsr set_bullet_routine_to_2 ; move to bullet routine 2, which destroys the bullet (player_bullet_collision_routine)
; reset PLAYER_BULLET_TIMER to #$06
ldx ENEMY_CURRENT_SLOT ; restore x to the current enemy slot
rts
; set PLAYER_BULLET_ROUTINE,x to #$02, which destroys the bullet (player_bullet_collision_routine)
@ -6893,6 +6894,7 @@ set_bullet_routine_to_2:
rts
; subtract enemy HP, play collision sound (if appropriate), award points
; set enemy destroyed routine if HP is #$00
bullet_collision_logic:
sta $17 ; store PLAYER_BULLET_OWNER in $17 0 = p1, 1 = p2
stx $11 ; backup bullet index in $11 for logic