nes-contra-us/src/bank7.asm

10653 lines
527 KiB
NASM

; Contra US Disassembly - v1.2
; https://github.com/vermiceli/nes-contra-us
; Bank 7 is the core of the game's programming. Reset, NMI, and IRQ vectors are
; in this bank and is the entry point to the game. Bank 7 is always loaded in
; memory unlike other banks, which are memory-mapped and can be swapped out.
; Bank 7 contains the code for drawing of nametables and sprites, bank
; switching, routines for the intro sequence, controller input, score
; calculation, graphics decompression routines, palette codes, collision
; detection, pointer table for enemy routines, shared enemy logic, score table,
; enemy attributes, and bullet angles and speeds, and the NES undocumented
; footer, among other things.
.segment "BANK_7"
.include "constants.asm"
; import labels needed from other banks
; this allows bank 7 to call into other banks
; bank 0 imports - enemy routines
; bank 0 level 1 enemies
.import bomb_turret_routine_ptr_tbl
.import boss_wall_plated_door_routine_ptr_tbl
.import exploding_bridge_routine_ptr_tbl
; bank 0 level 2 and 4 enemies
.import boss_eye_routine_ptr_tbl
.import roller_routine_ptr_tbl
.import grenade_routine_ptr_tbl
.import wall_turret_routine_ptr_tbl
.import wall_core_routine_ptr_tbl
.import indoor_soldier_routine_ptr_tbl
.import jumping_soldier_routine_ptr_tbl
.import grenade_launcher_routine_ptr_tbl
.import four_soldiers_routine_ptr_tbl
.import indoor_soldier_gen_routine_ptr_tbl
.import indoor_roller_gen_routine_ptr_tbl
.import eye_projectile_routine_ptr_tbl
.import boss_gemini_routine_ptr_tbl
.import spinning_bubbles_routine_ptr_tbl
.import blue_soldier_routine_ptr_tbl
.import red_soldier_routine_ptr_tbl
.import red_blue_soldier_gen_routine_ptr_tbl
; bank 0 level 3 enemies
.import floating_rock_routine_ptr_tbl
.import moving_flame_routine_ptr_tbl
.import rock_cave_routine_ptr_tbl
.import falling_rock_routine_ptr_tbl
.import boss_mouth_routine_ptr_tbl
.import dragon_arm_orb_routine_ptr_tbl
; bank 0 level 5 enemies
.import ice_grenade_generator_routine_ptr_tbl
.import ice_grenade_routine_ptr_tbl
.import tank_routine_ptr_tbl
.import ice_separator_routine_ptr_tbl
.import boss_ufo_routine_ptr_tbl
.import mini_ufo_routine_ptr_tbl
.import boss_ufo_bomb_routine_ptr_tbl
; bank 0 level 6 enemies
.import fire_beam_down_routine_ptr_tbl
.import fire_beam_left_routine_ptr_tbl
.import fire_beam_right_routine_ptr_tbl
.import boss_giant_soldier_routine_ptr_tbl
.import boss_giant_projectile_routine_ptr_tbl
; bank 0 level 7 enemies
.import claw_routine_ptr_tbl
.import rising_spiked_wall_routine_ptr_tbl
.import spiked_wall_routine_ptr_tbl
.import mine_cart_generator_routine_ptr_tbl
.import moving_cart_routine_ptr_tbl
.import immobile_cart_generator_routine_ptr_tbl
.import boss_door_routine_ptr_tbl
.import boss_mortar_routine_ptr_tbl
.import boss_soldier_generator_routine_ptr_tbl
; bank 0 level 8 enemies
.import alien_guardian_routine_ptr_tbl
.import alien_fetus_routine_ptr_tbl
.import alien_mouth_routine_ptr_tbl
.import white_blob_routine_ptr_tbl
.import alien_spider_routine_ptr_tbl
.import alien_spider_spawn_routine_ptr_tbl
.import boss_heart_routine_ptr_tbl
; bank 0 enemies that exist on multiple levels
.import enemy_bullet_routine_ptr_tbl
.import rotating_gun_routine_ptr_tbl
.import red_turret_routine_ptr_tbl
.import sniper_routine_ptr_tbl
.import soldier_routine_ptr_tbl
.import weapon_box_routine_ptr_tbl
.import weapon_item_routine_ptr_tbl
.import flying_capsule_routine_ptr_tbl
; bank 1 imports
.import draw_sprites, init_pulse_and_noise_channels
.import init_sound_code_vars, handle_sound_slots
; bank 2 imports
.import level_headers, graphic_data_02
.import alt_graphic_data_00, alt_graphic_data_01
.import alt_graphic_data_02, alt_graphic_data_03
.import alt_graphic_data_04
.import level_2_4_boss_supertiles_screen_ptr_table
.import load_screen_enemy_data
.import set_players_paused_sprite_attr
.import set_player_sprite_and_attrs
.import exe_soldier_generation
; bank 3 imports
.import level_1_nametable_update_supertile_data, level_1_nametable_update_palette_data
.import level_2_nametable_update_supertile_data, level_2_nametable_update_palette_data
.import level_3_nametable_update_supertile_data, level_3_nametable_update_palette_data
.import level_4_nametable_update_supertile_data, level_4_nametable_update_palette_data
.import level_5_nametable_update_supertile_data, level_5_nametable_update_palette_data
.import level_6_nametable_update_supertile_data, level_6_nametable_update_palette_data
.import level_7_nametable_update_supertile_data, level_7_nametable_update_palette_data
.import level_8_nametable_update_supertile_data, level_8_nametable_update_palette_data
.import level_2_4_nametable_update_supertile_data, level_2_4_boss_nametable_update_palette_data
.import level_2_4_boss_palette_data, run_end_level_sequence_routine
.import level_1_supertile_data, level_2_supertile_data, level_3_supertile_data
.import level_5_supertile_data, level_8_supertile_data
.import level_2_4_boss_supertile_data, level_2_4_tile_animation
.import level_6_tile_animation, level_7_tile_animation
; bank 4 imports
.import graphic_data_01, graphic_data_03
.import graphic_data_04, graphic_data_06
.import graphic_data_08, graphic_data_09
.import graphic_data_0a, graphic_data_0f
.import graphic_data_10, graphic_data_11
.import graphic_data_12, graphic_data_13
.import run_game_end_routine
; bank 5 imports
.import load_demo_input_table
.import graphic_data_05, graphic_data_07
.import graphic_data_0b, graphic_data_14
.import graphic_data_17, graphic_data_18
.import graphic_data_19, graphic_data_1a
; bank 6 imports
.import short_text_pointer_table
.import graphic_data_0c, graphic_data_0d
.import graphic_data_0e, graphic_data_15
.import graphic_data_16
.import run_player_bullet_routines, check_player_fire
; export labels for use by other banks
; labels need by bank 0
; - needed for enemy routines
.export load_bank_3_update_nametable_supertile
.export load_bank_3_update_nametable_tiles, load_palettes_color_to_cpu
.export get_cart_bg_collision, wall_core_routine_05, boss_defeated_routine
.export enemy_routine_init_explosion, mortar_shot_routine_03
.export set_enemy_delay_adv_routine, advance_enemy_routine, roller_routine_04
.export shared_enemy_routine_03, enemy_routine_explosion
.export enemy_routine_remove_enemy, shared_enemy_routine_clear_sprite
.export set_enemy_routine_to_a, update_enemy_pos
.export update_enemy_x_pos_rem_off_screen, set_enemy_y_vel_rem_off_screen
.export set_outdoor_weapon_item_vel, add_scroll_to_enemy_pos
.export set_enemy_velocity_to_0, set_enemy_y_velocity_to_0
.export set_enemy_x_velocity_to_0, reverse_enemy_x_direction
.export set_destroyed_enemy_routine, destroy_all_enemies
.export clear_supertile_bg_collision, set_supertile_bg_collision
.export set_supertile_bg_collisions, create_explosion_89
.export create_two_explosion_89, create_enemy_for_explosion, level_boss_defeated
.export set_delay_remove_enemy, disable_bullet_enemy_collision
.export disable_enemy_collision, enable_enemy_player_collision_check
.export enable_bullet_enemy_collision, enable_enemy_collision
.export add_a_to_enemy_y_pos, add_a_to_enemy_x_pos, set_08_09_to_enemy_pos
.export add_with_enemy_pos, add_10_to_enemy_y_fract_vel
.export add_a_to_enemy_y_fract_vel, generate_enemy_a, generate_enemy_at_pos
.export add_4_to_enemy_y_pos, add_a_with_vert_scroll_to_enemy_y_pos
.export update_nametable_tiles_set_delay, draw_enemy_supertile_a_set_delay
.export draw_enemy_supertile_a, update_2_enemy_supertiles
.export update_enemy_nametable_tiles_no_palette, update_enemy_nametable_tiles
.export check_enemy_collision_solid_bg, init_vars_get_enemy_bg_collision
.export add_y_to_y_pos_get_bg_collision, add_a_y_to_enemy_pos_get_bg_collision
.export set_flying_capsule_y_vel, set_flying_capsule_x_vel
.export red_turret_find_target_player, player_enemy_x_dist
.export find_far_segment_for_x_pos, find_far_segment_for_a
.export set_enemy_falling_arc_pos, set_weapon_item_indoor_velocity
.export find_next_enemy_slot, clear_sprite_clear_enemy_pt_3
.export clear_enemy_custom_vars, initialize_enemy, aim_and_create_enemy_bullet
.export bullet_generation, create_enemy_bullet_angle_a, set_bullet_velocities
.export aim_var_1_for_quadrant_aim_dir_01, aim_var_1_for_quadrant_aim_dir_00
.export get_rotate_00, get_rotate_01
.export get_rotate_dir, dragon_arm_orb_seek_should_move
.export get_quadrant_aim_dir_for_player, remove_enemy
; labels needed by bank 2
.export get_bg_collision, remove_all_enemies, find_next_enemy_slot_6_to_0
.export find_bullet_slot
; labels needed by bank 3
.export set_graphics_zero_mode, set_a_as_current_level_routine
; labels needed by bank 4
.export advance_graphic_read_addr, decrement_delay_timer
.export init_APU_channels, init_game_routine_reset_timer_low_byte
.export load_A_offset_graphic_data, load_alternate_graphics
.export load_palette_indexes, play_sound
.export reset_delay_timer, run_routine_from_tbl_below
.export zero_out_nametables
; labels needed by bank 6
.export set_vel_for_speed_vars, set_bullet_routine_to_2
; labels needed by multiple banks
.export run_routine_from_tbl_below ; bank 2, 3, 4, 6
.export init_APU_channels ; bank 0, 4
.export get_bg_collision_far ; bank 0, 6
.export play_sound ; bank 0, 1, 2, 4, 6
; Every PRG ROM bank starts with a single byte specifying which number it is
.byte $07 ; The PRG ROM bank number (7)
; interrupt called when the NES starts up, or the reset button is pressed
reset_vector:
cld ; disable decimal mode (NES chip 2A03 doesn't use decimal mode)
sei ; disable interrupts
wait_til_vblank:
lda PPUSTATUS ; read PPU status with the following bit layout -> VSO- ----
bpl wait_til_vblank ; Wait until V is 1 (accumulator is negative), i.e. in vertical blank VBLANK
vertical_blank_entry:
lda PPUSTATUS ; read PPU status with bit layout -> VSO- ----
bpl vertical_blank_entry ; ensure still in VBLANK
lda #$00 ; clear accumulator
sta GAME_MODE ; set the game mode to normal #$00 (not demo)
jsr clear_ppu ; initialize PPU
ldx #$ff
txs ; initialize stack pointer location to $01ff, stack range is from $01ff down to $0100 (descending stack)
; clear (zero out) memory range $0000-$01bf and $0200-$07df
lda #$01 ; high byte of max memory address to clear
ldx #$01 ; number of #$ff-sized blocks of memory to clear
ldy #$bf ; low byte of max memory address to clear
jsr clear_memory ; clear memory range $0000-$01bf
lda #$07 ; high byte of max memory address to clear
ldx #$05 ; number of #$ff-sized blocks of memory to clear
ldy #$df ; low byte of max memory address to clear
jsr clear_memory ; clear memory range $0200-$07df
ldx #$f0
@loop:
txa
cmp CPU_GRAPHICS_BUFFER,x ; compare A (#$f0) to cpu memory $07f0
bne init_07f0_through_high_score ; branch if is A #$f0 not equal to memory address $07f0
inx ; !(OBS) not sure if this line is ever executed
bne @loop ; !(OBS) not sure if this line is ever executed
beq init_APU_and_PPU ; !(OBS) not sure if this line is ever executed
; initialize memory $07f0-$07ff to be #$f0 to #$ff respectively
init_07f0_through_high_score:
ldx #$f0
@loop:
txa
sta CPU_GRAPHICS_BUFFER,x ; initialize $07f0 to $f0, $07f1 to $f1, etc. until $07ff !(WHY?)
; don't think this range is used ever $07f0-$07ff
inx
bne @loop
lda #$c8 ; default high score is 20,000
sta HIGH_SCORE_LOW ; store low byte of high score in HIGH_SCORE_LOW
lda #$00
sta HIGH_SCORE_HIGH ; store high byte of high score (#$00)
init_APU_and_PPU:
lda #$00
sta CPU_GRAPHICS_BUFFER
jsr init_APU
jsr init_APU_channels
jsr configure_PPU
; run between NMI interrupts after nmi_start code finishes
; loop forever updating RANDOM_NUM before NMI
forever_loop:
lda FRAME_COUNTER ; load frame counter
adc RANDOM_NUM ; add the frame number to RANDOM_NUM
sta RANDOM_NUM ; update RANDOM_NUM to new result
jmp forever_loop
; NMI entry point, beginning of vertical blanking interval. This happens once per video frame and is triggered by PPU.
; The PPU is available for graphics updates
; The NES will automatically clear the screen so you do not have to worry about trying to clear it with code.
; The end of all the game code will end at RTI (return from interrupt).
nmi_start:
php ; push processor status to stack #$02 bytes (NV--DIZC)
pha ; push A on to the stack
txa ; transfer X to A
pha ; push A on to the stack
tya ; transfer Y to A
pha ; push A on to the stack
lda PPUSTATUS ; reset PPU latch
ldy NMI_CHECK ; see if nmi interrupted previous frame's game loop
bne handle_sounds_set_ppu_scroll_rti ; branch if nmi occurred before game loop was completed
; to skip game loop and instead just check for sounds to play and play them
; set ppu scroll, and rti
jsr clear_ppu ; first frame, so clear/init PPU
sta OAMADDR ; set OAM address to #00 (DMA is used instead)
ldy #>OAMDMA_CPU_BUFFER ; setting OAMDMA to #$02 tells PPU to load sprite data from $0200-$02ff
sty OAMDMA ; write #$100 (256 decimal) bytes ($0200 to $02ff) of data to PPU OAM (the entire screen)
jsr write_palette_colors_to_ppu ; writes the colors for all the palettes defined in CPU memory to the PPU $3f00 to $3f1f
jsr write_cpu_graphics_buffer_to_ppu ; draw the graphic data in memory at CPU_GRAPHICS_BUFFER to the PPU
lda PPUMASK_SETTINGS ; load settings for PPUMASK (#$1e)
ldx PPU_READY ; every time configure_PPU is called, PPU_READY is set to #$05
; this confirms that at least 5 nmi interrupts have happened
; since the last configure_PPU was called
beq @continue ; PPU_READY is #$00, so continue setup
dec PPU_READY ; decrement PPU load loop (starts at #$05)
beq @continue ; PPU_READY is now #$00, so continue setup
lda #$00 ; set PPUMASK to #$00 since PPU isn't ready
@continue:
sta PPUMASK ; set PPU mask, either #$00 to clear or #$1e (from PPUMASK_SETTINGS) when PPU ready
jsr set_ppu_addr_to_nametables ; set the PPU write address to $2000 to write the pattern table tile data
inc NMI_CHECK ; entering important part of game loop, set to #$01
; if next NMI occurs before set back to #$00, then game engine knows logic was
; interrupted before completing. This is not good but does regularly occur
; at beginning of levels when clearing memory
ldy #$01
jsr load_bank_number ; load bank 1
jsr handle_sound_slots ; loop through sound slots and execute appropriate sound codes
jsr load_controller_state ; read controller for p1 and p2, stores results into memory
jsr exe_game_routine ; go into game routine loop
ldy #$01
jsr load_bank_number ; load bank 1
jsr draw_sprites ; bank 1
jsr write_0_to_cpu_graphics_buffer
lda #$00
sta NMI_CHECK
remove_registers_from_stack_and_rti:
pla ; remove byte from stack
tay ; store in y
pla ; remove byte from stack
tax ; story in x
pla ; remove byte stack
plp ; set cpu flags from stack
; end of CPU code execution for the frame
irq:
rti ; return to forever_loop until nmi is triggered again
; rti pops the processor flags and then the program counter
; then starts executing at that location
; NMI_CHECK is non-zero, meaning the previous frame's game loop was interrupted
handle_sounds_set_ppu_scroll_rti:
lda PPUMASK_SETTINGS ; load settings for PPUMASK
ldx PPU_READY ; load PPU status (#$00 is ready)
beq @handle_sound ; if ready keep PPU mask setting of #$1e
lda #$00 ; clear PPUMASK
@handle_sound:
sta PPUMASK
lda BANK_NUMBER ; get currently loaded switchable bank number
pha ; backup bank number
lda NMI_CHECK ; see if nmi interrupted while loading sound variables (play_sound)
bmi @continue ; jump if NMI_CHECK is negative to skip handling sound entry
ldy #$01
jsr set_rom_bank_to_y ; swap PRG ROM bank to 01
jsr handle_sound_slots ; loop through sound slots and execute appropriate sound codes
@continue:
pla ; pull previous bank from stack
tay ; store in y
jsr set_rom_bank_to_y
jsr set_ppu_scroll
jmp remove_registers_from_stack_and_rti
; initialize the audio processing unit
; NES APU has 5 channels, Contra uses 4 of them
; * disable DMC (data modulation channel)
; * enable noise channel (static sound)
; * enable triangle channel (triangle wave)
; * enable pulse 1 channel (pulse wave)
; * enable pulse 2 channel (pulse wave)
init_APU:
lda #$0f ; 0000 1111 in binary
sta APU_STATUS
lda #$c0 ; 1100 0000 in binary
sta APU_FRAME_COUNT ; set frame sequencer for frame counter to 5 step sequence (~192 Hz)
; clear frame interrupt flag
rts
; configures the PPU to the following settings
; base nametable address: $2000
; VRAM address increment: add 1 going across
; 8x8 sprite pattern table address (ignored since using 8x16 sprites)
; background pattern table address: $1000 (right pattern table)
; sprite size: 8x16 pixels
; generate NMI at start of VBLANK
; actually fills nametable with sprites and palettes
configure_PPU:
lda #$05 ; set a to #$05
sta PPU_READY ; store into PPU_READY, prevents writes to PPUMASK until 5 nmi_start executions
lda #$b0 ; set a to %1011 0000
sta PPUCTRL_SETTINGS ; store a into $ff
sta PPUCTRL
lda #$05
sta PPU_READY ; set PPU_READY to #$05
rts
; set PPU write address to $2000. This is the start of the nametables in the PPU
set_ppu_addr_to_nametables:
lda PPUSTATUS ; read PPUSTATUS to reset PPU latch
; setting the PPUADDR takes two writes, one for high byte and one for low
lda #$20 ; load high byte of address
sta PPUADDR
lda #$00 ; load low byte of address
sta PPUADDR
set_ppu_scroll:
lda PPUSTATUS ; clear bit 7 and address latch used by PPUSCROLL and PPUADDR
lda HORIZONTAL_SCROLL ; load horizontal component of the PPUSCROLL [#$0 - #$ff]
sta PPUSCROLL ; write X position
lda VERTICAL_SCROLL
sta PPUSCROLL ; write Y position
lda PPUCTRL_SETTINGS ; saved PPUCTRL settings (see configure_PPU)
sta PPUCTRL
rts
clear_ppu:
lda #$00 ; setting the PPUADDR takes two writes, one for high byte and one for low
sta PPUADDR ; write $00 high byte
sta PPUADDR ; write $00 low byte
sta PPUCTRL ; clear PPUCTRL
sta PPUMASK ; clear PPUMASK
rts
; clears blocks of CPU memory
; starts with a Y byte sized block of memory starting at A
; then clears #$ff * X byte-sized blocks of memory preceding A
clear_memory:
sta $01 ; set pointer to beginning of addresses to clear
lda #$00 ; set $00 to use to clear
sta $00 ; store #$00 to $00
@loop:
sta ($00),y ; clear memory address Y-bytes away from address stored in $00 and $01
dey
cpy #$ff ; #FF is equivalent to -1 here, loop until Y is -1
bne @loop
dec $01 ; decrement to next #$ff-sized block of memory to clear
dex ; decrement number of blocks to clear counter
bpl @loop ; clear the next #$ff block of bytes
rts
; ROM address $c139
; backs up the currently-loaded bank number into PREVIOUS_ROM_BANK ($07ec)
; updates the currently-loaded bank to Y
load_bank_number:
lda BANK_NUMBER ; load the currently-loaded bank number (address $8000)
; first byte of every bank is the bank number
sta PREVIOUS_ROM_BANK ; save bank number to $07ec
; updates the active PRG ROM bank to bank specified by y
; CPU address $c139
; swaps out the ROM available to CPU for addresses $8000 to $bfff
; this is because contra is a UxROM mapper, which swaps the active
; bank when detecting a write to CPU address between $8000 and $ffff inclusively
; the bank swapped in is the bank of the lowest 4 bits
set_rom_bank_to_y:
lda prg_rom_banks,y ; grab the bank number (should match y)
sta prg_rom_banks,y ; set the active bank number
rts
; loads the previously-loaded ROM bank specified in $07ec (PREVIOUS_ROM_BANK)
load_previous_bank:
ldy PREVIOUS_ROM_BANK
jmp set_rom_bank_to_y
; loads bank 1 into switchable memory without losing values of A and Y
load_bank_1:
pha ; save a to stack
tya ; transfer y to a
pha ; save a (y) to stack
lda BANK_NUMBER ; load currently loaded switchable rom number
sta PREVIOUS_ROM_BANK_1 ; set PREVIOUS_ROM_BANK_1 to match loaded bank
ldy #$01
jsr set_rom_bank_to_y ; load ROM BANK 1
pla ; restore a (y) from stack
tay ; move a to y
pla ; restore a from stack
rts
; loads the bank specified in PREVIOUS_ROM_BANK_1 without losing values of A and Y
; !(WHY?) not sure why there are 2 variables to store the previous bank, both used differently
local_previous_1_bank:
pha ; save a to stack
tya
pha ; save y to stack
ldy PREVIOUS_ROM_BANK_1
jsr set_rom_bank_to_y
pla ; pull y from stack
tay
pla ; pull a from stack
rts
; CPU address $c16b
; input
; * a - the sound code to play
play_sound:
pha ; push sound code to stack
lda NMI_CHECK ; load NMI_CHECK flag, should always be #$01 here
ora #$80 ; ensure most significant bit is set (1)
sta NMI_CHECK ; while bank 1 is loaded and inside init_sound_code_vars
pla ; pop sound code back from stack
jsr load_bank_1
jsr init_sound_code_vars ; bank 1
jsr local_previous_1_bank ; load PREVIOUS_ROM_BANK_1
lda NMI_CHECK ; load NMI_CHECK flag, should always be #$81 here
and #$7f ; finished with init_sound_code_vars, clear bit 7
sta NMI_CHECK ; reset NMI_CHECK flag back to #$01
rts
init_APU_channels:
sty $f7 ; backup Y in $f7
ldy #$01
jsr load_bank_1
jsr init_pulse_and_noise_channels ; bank 1, sets pulse channel duty cycle, volume, and sweep data, mute noise channel
jsr local_previous_1_bank ; load PREVIOUS_ROM_BANK_1
ldy $f7 ; restore Y back from $f7
rts
; draw super-tile $10 at position (a,y)
; redraws parts of the nametable for things like bridge explosions,
; nametable enemy explosions, animation (pill box sensor), etc.
; also used to draw palette colors for super-tiles
; input
; * a is x position of nametable super-tile in pixels
; * y is y position of nametable super-tile in pixels
; * $10 is the super-tile or palette index to draw (level_x_nametable_update_supertile_data/level_x_nametable_update_palette_data offset)
; If bit 7 clear, then update palette, if bit 7 set do not update palette
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
load_bank_3_update_nametable_supertile:
sta $f3 ; save a value before swapping banks
sty $f7 ; save y value before swapping banks
ldy #$03 ; y = #$03
jsr load_bank_number ; load bank y (03)
lda $f3 ; restore a value
ldy $f7 ; restore y value
jsr update_nametable_supertile ; draw super-tile $10 at position (a,y)
jmp load_previous_bank ; load previous bank
; load bank 3 and update_nametable_tiles
; indoor/base levels for drawing wall turrets, and changing to explosion when destroyed
; input
; * a is x position
; * y is y position
; * $10 (multiplied by #$05) is the index into the tile animation table to start drawing
; if bit 7 clear, then update palette, if bit 7 set do not update palette
; output
; * carry - clear when successful, set when CPU_GRAPHICS_BUFFER is full
load_bank_3_update_nametable_tiles:
sta $f3 ; backup a in $f3
sty $f7 ; backup y in $f7
ldy #$03 ; y = #$03
jsr load_bank_number ; switch to bank y (03)
lda $f3 ; restore a from $f3
ldy $f7 ; restore y from $f7
jsr update_nametable_tiles
jmp load_previous_bank
; various tasks with different banks
load_bank_6_run_player_bullet_routines:
ldy #$06 ; y = #$06
jsr load_bank_number ; switch bank to 06
jmp run_player_bullet_routines ; CPU address $b94a
; switch to bank 6 and see if player is trying to shoot
; if so and player should be able to fire, then generate bullet
load_bank_6_check_player_fire:
ldy #$06 ; y = #$06
jsr load_bank_number ; switch bank to 06
jmp check_player_fire ; generate bullet if player is shooting and allowed to shoot
load_bank_0_exe_all_enemy_routine:
ldy #$00 ; y = #$00
jsr load_bank_number ; switch bank to 00
jmp exe_all_enemy_routine
; enemy generation
load_bank_2_load_screen_enemy_data:
ldy #$02 ; y = #$02
jsr load_bank_number ; switch bank to 02
jmp load_screen_enemy_data ; CPU address $b419
; get table pointer for level-specific enemy routines
; ensure enemy routines bank (bank 00) is loaded
load_bank_0_load_level_enemies_to_mem:
ldy #$00 ; y = #$00
jsr load_bank_number ; switch bank to 00
jmp load_level_enemies_to_mem ; load the pointer to the level-specific enemy routines to $80
; soldier generation
load_bank_2_exe_soldier_generation:
ldy #$02 ; y = #$02
jsr load_bank_number ; switch bank to 02
jmp exe_soldier_generation ; CPU address $b523
; handles scrolling for the level if currently scrolling
; including writing tiles to nametable, writing to the attribute table, and loading alternate graphics
; includes handling auto scroll from boss reveal or tank
load_bank_3_handle_scroll:
ldy #$03 ; y = #$03
jsr load_bank_number ; switch bank to 03
jmp handle_scroll ; handles scroll for level if currently scrolling
; load bank 3 and execute init_lvl_nametable_animation
; output
; * zero flag - set when LEVEL_TRANSITION_TIMER has elapsed, clear otherwise
load_bank_3_init_lvl_nametable_animation:
ldy #$03 ; y = #$03
jsr load_bank_number ; switch bank to 03
jmp init_lvl_nametable_animation ; animate initial level nametable drawing
; load alternate tiles if necessary
load_bank_2_alternate_tile_loading:
ldy #$02 ; y = #$02
jsr load_bank_number ; switch bank to 02
jmp alternate_tile_loading ; alternate tiles loading code
load_level_graphics:
jmp load_current_level_graphic_data
; loads the graphic data for the level specified by offset into level_graphic_data_tbl (A register)
load_A_offset_graphic_data:
jmp load_level_graphic_data
load_bank_2_set_player_sprite:
ldy #$02
jsr load_bank_number
jmp set_player_sprite_and_attrs ; set player sprite based on player state, level, and animation sequence
; game paused, jump set_players_paused_sprite_attr
load_bank_2_set_players_paused_sprite_attr:
ldy #$02
jsr load_bank_number ; load bank 2
jmp set_players_paused_sprite_attr ; ensure player sprite attributes continue while paused, e.g. flashing while invincible, electrocuted, etc.
; loaded from bank #6
; write pattern tile (text) or palette information (color) to CPU offset CPU_GRAPHICS_BUFFER
; this is used when GRAPHICS_BUFFER_MODE is #$00, which defines the CPU_GRAPHICS_BUFFER format for text and palette data
; input
; * a - first six bits are index into the short_text_pointer_table
; when bit 7 set, write all blank characters instead of actual characters. Used for flashing effect
load_bank_6_write_text_palette_to_mem:
sta $f3 ; store the specific text/palette to load into $f3
ldy #$06
jsr load_bank_number ; load bank 6
lda $f3 ; load_bank_number sets A to the bank number, so reset it back to the item to load
jmp write_text_palette_to_mem
; game routines - pointer 6
; runs at the end of the game after defeating the alien
; runs end of game routines, and end of game sequence routines
; melts screen, ending helicopter animation and credits
game_routine_06:
ldy #$04 ; y = #$04
jsr load_bank_number ; switch bank to y (04)
jmp run_game_end_routine ; CPU address $b8b9
; loads bank five and execute procedure to simulate player input for demo
simulate_input_for_demo:
ldy #$05 ; y = #$05
jsr load_bank_number ; switch bank to y (05)
jmp load_demo_input_table ; label from bank 5
load_bank_3_run_end_lvl_sequence_routine:
ldy #$03 ; y = #$03
jsr load_bank_number ; switch bank to y (03)
jmp run_end_level_sequence_routine ; CPU address $bdfa
; determine which game routine to run and run it
; checks if player presses start to early exit animation
exe_game_routine:
inc FRAME_COUNTER ; increment frame counter
lda GAME_ROUTINE_INDEX ; index into game_routine_pointer_table
beq run_game_routine ; run game_routine_00 if GAME_ROUTINE_INDEX is 0, run only once to initialize intro
cmp #$03
bcs run_game_routine ; skip decrementing timer if GAME_ROUTINE_INDEX >= 3
; timer is used for only intro animation and when showing demos
jsr dec_theme_delay_check_user_input ; decrements animation timers, checks for early exit, player mode change etc, if not fall through
; run game routine for specified GAME_ROUTINE_INDEX
run_game_routine:
lda GAME_ROUTINE_INDEX ; offset into game_routine_pointer_table to execute
jsr run_routine_from_tbl_below ; run routine a in the following table (game_routine_pointer_table)
; pointer table to code to run for game routines ($0E bytes total)
; CPU address $c24d
game_routine_pointer_table:
.addr game_routine_00 ; CPU address $c25b (initial intro scrolling)
.addr game_routine_01 ; CPU address $c274 (play sound and load Bill and Lance, show menu, konami check)
.addr game_routine_02 ; CPU address $c2b1 (wait for input until timer expires, then start demo)
.addr game_routine_03 ; CPU address $c2c7 (player pressed start to start game (1p or 2p))
.addr game_routine_04 ; CPU address $c2f4 (clears player state and sprite data)
.addr game_routine_05 ; CPU address $ce30 (run level_routine execution, for actual playing of level, or demo)
.addr game_routine_06 ; CPU address $c223 (runs at the end of the game after defeating the alien)
; The 1st game routine game_routine_pointer_table
; this label initializes the intro scrolling effect
game_routine_00:
jsr zero_out_nametables ; initialize nametables 0 and 1 to zeroes
jsr load_intro_graphics ; load the graphic data (pattern, nametable, and palette) to ppu, as well as palette data to cpu
ldy #$00 ; y = #$00
sty KONAMI_CODE_NUM_CORRECT ; initialize konami check to #$0 (see konami_input_check)
.ifdef Probotector
sty HORIZONTAL_SCROLL ; initialize the horizontal scroll offset to #$00
ldy #$02
sty DELAY_TIME_HIGH_BYTE ; initialize delay high byte to #$02 (used for various delays)
lda #$b0 ; %1011 0000 (set nametable to $2000)
sta PPUCTRL_SETTINGS ; store PPUCTRL settings for next update of PPUCTRL
jmp inc_routine_index_set_timer ; move to game_routine_01
.else
iny
sty HORIZONTAL_SCROLL ; initialize the horizontal scroll offset to #$01
iny
sty DELAY_TIME_HIGH_BYTE ; initialize delay high byte to #$02 (used for various delays)
lda #$b1 ; %1011 0001 (set nametable to $2400)
sta PPUCTRL_SETTINGS ; store PPUCTRL settings for next update of PPUCTRL
jmp inc_routine_index_set_timer ; move to game_routine_01
.endif
; table for y positions of intro screen cursor
; same table is used for "CONTINUE"/"END" screen during game over
player_select_cursor_pos:
.ifdef Probotector
.byte $9a,$aa
.else
.byte $a2,$b2
.endif
; The 2nd game routine game_routine_pointer_table
; run once per frame for multiple frames while scrolling to right until intro screen is shown
; when scrolling complete, plays intro "explosion" sound and loads player select menu
game_routine_01:
jsr konami_input_check ; check if current input is part of Konami code (30-lives code)
; if completed input successfully, sets KONAMI_CODE_STATUS to #$01
.ifdef Probotector
ldx GAME_ROUTINE_INIT_FLAG ; see if current game_routine has initialized
bne game_routine_01_scroll_complete ; no scrolling animation is done for Probotector
; skip to complete if sound has played and routine is 'initialized'
jsr load_intro_palette2_play_intro_sound
lda #$26 ; a = #$26 (game intro tune)
jsr play_sound ; play sound_26 (game intro tune)
inc GAME_ROUTINE_INIT_FLAG ; mark game routine as initialized
rts ; exit
.else
lda HORIZONTAL_SCROLL ; load horizontal component of the PPUSCROLL [#$0 - #$ff]
beq game_routine_01_scroll_complete ; if scroll complete, show Bill and Lance and play sound
inc HORIZONTAL_SCROLL ; add 1 to the horizontal scroll offset
bne game_routine_01_exit ; if scrolling animation isn't complete, continue scrolling next frame
jsr load_intro_palette2_play_intro_sound ; scrolling complete, load 2nd intro background palette and play explosion sound
.endif
; write the text "PLAY SELECT", "1 PLAYER", player select cursor, etc
; move to next game_routine once timer elapses
game_routine_01_scroll_complete:
.ifdef Probotector
lda #$58 ; a = #$58 (x position of cursor in intro)
.else
lda #$2c ; a = #$2c (x position of cursor in intro)
.endif
sta SPRITE_X_POS ; store x position of cursor for player select (first sprite)
lda #$aa ; sprite_aa: player selector cursor (yellow falcon)
sta CPU_SPRITE_BUFFER ; store sprite number in CPU buffer
ldx PLAYER_MODE ; number of players (0 = 1 player)
lda player_select_cursor_pos,x ; load y position of cursor for player select
sta SPRITE_Y_POS ; store y position of cursor for player select
lda #$00 ; a = #$00
sta SPRITE_ATTR ; reset sprite effect for player
lda #$ab ; sprite_ab: Bill and Lance's hair and shirt
sta CPU_SPRITE_BUFFER+1 ; store next sprite to load
.ifdef Probotector
lda #$80 ; a = #$80 (x position for sprite_ab)
sta SPRITE_X_POS+1 ; store x position for sprite_ab
lda #$5f ; a = #$5f (y position for sprite_ab)
sta SPRITE_Y_POS+1 ; store y position for sprite_ab
.else
lda #$b3 ; a = #$b3 (x position for sprite_ab)
sta SPRITE_X_POS+1 ; store x position for sprite_ab
lda #$77 ; a = #$77 (y position for sprite_ab)
sta SPRITE_Y_POS+1 ; store y position for sprite_ab
.endif
jsr decrement_delay_timer ; decrease delay and check if it reaches 0
bne game_routine_01_exit ; timer not complete, wait
jmp increment_game_routine ; timer delay complete, increase game_routine to game_routine_02
game_routine_01_exit:
rts
; The 3rd game routine (see game_routine_pointer_table)
; * loads demo level and plays the level
; * stops level when demo timer elapsed and loads next level to demo (only levels 0-2)
; * resets GAME_ROUTINE_INDEX to #$0 between demo levels to reshow intro scroll and player select
game_routine_02:
ldx GAME_ROUTINE_INIT_FLAG ; determine if game routine has been "initialized"
bne @continue ; if GAME_ROUTINE_INIT_FLAG is already 1, no need to load level, continue with demo level
inc GAME_ROUTINE_INIT_FLAG ; set GAME_ROUTINE_INIT_FLAG to indicate that demo level is loaded
jmp set_next_demo_level ; set memory addresses in preparation for next demo level
@continue:
jsr run_level_routine_for_demo ; execute level routine with offset of current value of LEVEL_ROUTINE_INDEX
lda DEMO_LEVEL_END_FLAG ; whether or not the demo for the level is complete
beq game_routine_02_exit ; demo not complete, continue showing demo
lda #$00 ; demo of level complete, move to next level to demo
sta GRAPHICS_BUFFER_MODE ; start to read from beginning of CPU_GRAPHICS_BUFFER (see write_cpu_graphics_buffer_to_ppu)
beq set_game_routine_index_to_a ; reset GAME_ROUTINE_INDEX 0 to replay scrolling effect and player selection
; The 4th game routine (see game_routine_pointer_table)
; player pressed start to start game (1p or 2p)
game_routine_03:
ldx GAME_ROUTINE_INIT_FLAG ; load whether game_routine_03 has been initialized
bne @continue ; level 1 data already set, continue
lda #$00
sta DEMO_MODE ; set DEMO_MODE to off
lda #$40
bne set_game_routine_init_flag ; (always jump due to lda in previous line) set timer low byte to #$40
@continue:
jsr dec_intro_theme_delay
lda DELAY_TIME_LOW_BYTE ; various delays (low byte)
beq @intro_timer_elapsed ; if the timer low byte is complete, jump
dec DELAY_TIME_LOW_BYTE ; various delays (low byte)
@intro_timer_elapsed:
ora INTRO_THEME_DELAY ; combine DELAY_TIME_LOW_BYTE with the intro theme (with explosion) sound delay
beq increment_game_routine ; if both the intro theme and the delay timer are #$00 (elapsed), increment to next game routine #$04
lda #$01 ; a = #$01 (text_1_player)
clc ; clear carry in preparation for addition
adc PLAYER_MODE ; number of players (0 = 1 player)
; if 2 player mode, index is updated to text_2_players
sta $00 ; store player mode in $00
lda #$08 ; a = #$08
and FRAME_COUNTER ; flash #$08 frames at a time
asl
asl
asl
asl ; if FRAME_COUNTER bit 3 was set, then bit 7 is set here
; indicating to blank the text (flashing animation)
ora $00 ; merge with short_text_pointer_table offset (#$01 or #$02)
jmp load_bank_6_write_text_palette_to_mem ; flash "1 PLAYER" or "2 PLAYER" depending on PLAYER_MODE
; The 5th game routine (see game_routine_pointer_table)
game_routine_04:
jsr init_score_player_lives ; clear memory addresses $0028 to $00f0 then CPU_SPRITE_BUFFER to CPU_GRAPHICS_BUFFER
jmp increment_game_routine
init_game_routine_reset_timer_low_byte:
lda #$80 ; a = #$80
set_game_routine_init_flag:
sta DELAY_TIME_LOW_BYTE ; various delays (low byte)
inc GAME_ROUTINE_INIT_FLAG ; set that the routine has been initialized
; for game over routines, increments GAME_END_ROUTINE_INDEX (same memory address)
; also fall through from game_routine_03
game_routine_02_exit:
rts
; sets low byte of delay timer to #$80 and increments game routine
inc_routine_index_set_timer:
lda #$80 ; set default delay (low byte) for next routine (game_routine_01 or level_routine_06)
sta DELAY_TIME_LOW_BYTE ; various delays (low byte)
increment_game_routine:
inc GAME_ROUTINE_INDEX ; move to the next game_routine
; called every time the game_routine index is incremented
init_game_routine_flags:
lda #$00
sta DEMO_LEVEL_END_FLAG ; clear DEMO_LEVEL_END_FLAG
sta GAME_ROUTINE_INIT_FLAG ; reset GAME_ROUTINE_INIT_FLAG to #$00
rts
; update GAME_ROUTINE_INDEX to A
; reset delay timer to #$0240
set_game_routine_index_to_a:
sta GAME_ROUTINE_INDEX
jsr reset_delay_timer ; reset 2-byte delay timer to #$0240
bne init_game_routine_flags
; decrement delay timer
; zero flag set (checked) when the timer has elapsed, otherwise zero flag will not be set
decrement_delay_timer:
lda DELAY_TIME_LOW_BYTE ; load the low byte of the delay timer
ora DELAY_TIME_HIGH_BYTE ; OR it together with high byte
beq @exit ; all bits both high and low byte are #$0, exit with #$0 in a register, zero flag set
dec DELAY_TIME_LOW_BYTE ; decrease delay (loops below #$00 to #$ff)
bne @exit ; low byte isn't #$0, exit with zero flag clear
lda DELAY_TIME_HIGH_BYTE ; low byte was #$0, check high byte
beq @exit_z_flag_clear ; high byte is #$0 as well, exit with #$01 in a register, zero flag clear
dec DELAY_TIME_HIGH_BYTE ; high byte wasn't #$0, subtract 1 from it
@exit_z_flag_clear:
lda #$01 ; ensures the zero flag is clear
@exit:
rts
; set or reset delay before demo begins
; only if the intro screen is forced by pressing start/select
; NTSC is about #3c frames per second
; PAL is close to #$32 frames per second
; delay timer is strange in that once the high byte goes from #$01 to #$00, the low byte isn't rest to #$ff
; this means that although the timer is set to #$0240, the delay is only ~5 seconds (#$140 frames) and not ~9 seconds
reset_delay_timer:
ldx #$40
stx DELAY_TIME_LOW_BYTE ; set low byte of timer to #$40
ldx #$02
stx DELAY_TIME_HIGH_BYTE ;set high byte of timer to #$02
rts
; checks if current input is part of Kazuhisa Hashimoto's famous Konami code (30-lives code)
; if completed input successfully, set KONAMI_CODE_STATUS to #$01
konami_input_check:
ldy KONAMI_CODE_NUM_CORRECT ; load the number of successful inputs of Konami code
bmi konami_code_exit ; if #$ff (invalid Konami input), exit
lda CONTROLLER_STATE_DIFF ; buttons pressed (only care on input change so held button doesn't affect code)
and #$cf ; only care about input from d-pad and A/B buttons (not select nor start)
beq konami_code_exit ; if no input detected, exit
cmp konami_code_lookup_table,y ; compare with Konami code sequence at index y
beq konami_input_index_correct ; on success, goto konami_input_index_correct
lda #$ff ; incorrect sequence for Konami code
sta KONAMI_CODE_NUM_CORRECT ; since incorrect set KONAMI_CODE_NUM_CORRECT (number of successful inputs) to $ff
rts
konami_input_index_correct:
iny ; add to number of successfully entered Konami code inputs
sty KONAMI_CODE_NUM_CORRECT ; store in KONAMI_CODE_NUM_CORRECT
cpy #$0a ; Konami code is 10 inputs, compare against how many successfully entered
bcc konami_code_exit ; Konami code not yet fully entered, exit
lda #$01 ; Konami code successfully entered, set flag to $01
sta KONAMI_CODE_STATUS ; store success flag in memory
konami_code_exit:
rts
; table for Konami code (30-lives code) - up up down down ...
konami_code_lookup_table:
.byte $08,$08,$04,$04,$02,$01,$02,$01,$40,$80
; reads the controller for p1 and p2
; ultimately stores results into CONTROLLER_STATE,x and CONTROLLER_STATE_DIFF,x
; due to DMC channel DPCM (Delta Pulse Coded Modulation) bug in the APU, input is read twice to confirm
; if the inputs match then it is assumed to be a valid read, otherwise, uses last known good read
load_controller_state:
jsr read_controller_state
lda $04
sta $00 ; store p1 input in $00 in CPU memory
lda $05
sta $01 ; store p2 input in $01 in CPU memory
jsr read_controller_state ; re-read input to confirm it was not affected by DMC channel DPCM bug
ldx #$01 ; start with player 2
ensure_input_valid:
lda $04,x ; read player's input from second attempt to read
cmp $00,x ; compare to 1st attempt to read input
beq @continue ; if input matches continue, the read input is valid
lda CTRL_KNOWN_GOOD,x ; read input is invalid, load last good value into a register
sta $04,x ; store previous good input into $04, can't trust just-read player input
@continue:
dex
bpl ensure_input_valid ; move from p1 to p1 and ensure p1's input is valid
lda PLAYER_MODE_1D ; see player_mode_1d_table (#$01 or #$07)
and #$04 ; see if 2 player or single player (#$07 will have #$04 bit set)
bne write_input ; jump to store input to cpu memory if 2 player (PLAYER_MODE = #$01)
lda $04 ; single player mode, merge both controller inputs into single input
; this allows the player to play 1 player with the 2nd controller port
; or even have both players play as the same character!
ora $05 ; combine with p2 input
sta $04 ; store result into p1 input
; store controller input for both players
write_input:
ldx #$01
; set the new input to memory for use in code
; also sets the differences between last input and new input for use in code
set_player_input:
lda $04,x ; read player input
tay ; move input into Y
eor CTRL_KNOWN_GOOD,x ; find the differences between previous known-good input and new input
and $04,x ; set a to only have differences in input between last known-good and new input
sta CONTROLLER_STATE_DIFF,x ; store input differences value into memory
sty CONTROLLER_STATE,x ; store new known-good input into $f1
sty CTRL_KNOWN_GOOD,x ; store new known-good input into CTRL_KNOWN_GOOD (used only for controller input code)
dex ; move from player 2 to player 1
bpl set_player_input ; read player 1 input
rts
; reads the p1 and p2 controllers
; stores the inputs in a bit field in $04 and $05 respectively
; from msb to lsb: A, B, select, start, up, down, left, right
; sets and immediately clears strobe bit to read from controllers
read_controller_state:
ldx #$01
stx CONTROLLER_1 ; set the strobe bit so controller input for both controllers can be read
dex
stx CONTROLLER_1 ; clear strobe bit before starting controller read
ldy #$08 ; loop counter to go through #$08 inputs
; A, B, select, start, up, down, left, right
; read controller input for individual button press
; pushing each entry into the resulting byte for each controller input
; this looks at both bit 0 and bit 1 from the NES to determine input
; this means the game supports both the standard controller as well as a Famicom expansion port controller
read_controller_button:
lda CONTROLLER_1 ; read controller input to determine if button is pressed
; Contra is concerned with the 2 least significant bits (NES and Famicom inputs)
sta $07 ; store input value in $07
lsr ; move lsb specifying if button is pressed for a standard controller into carry flag
ora $07 ; or the original value with the shifted value
; this is essentially also checking if bit 1 (Famicom expansion port controller) is set
lsr ; move bit representing whether the button is pressed to the carry flag
rol $04 ; shift carry flag (button input flag) onto player 1 controller input bit-field
; $04 by pushing the button state bit to the next bit
lda CONTROLLER_2 ; do the same thing for player 2 controller
sta $07 ; store input value in $07
lsr ; move lsb specifying if button is pressed for a standard controller into carry flag
ora $07 ; or the original value with the shifted value
; this is essentially also checking if bit 1 (Famicom expansion port controller) is set
lsr ; move bit representing whether the button is pressed to the carry flag
rol $05 ; shift carry flag (button input flag) onto player 1 controller input bit-field
; $05 by pushing the button state bit to the next bit
dey ; decrement button loop counter
bne read_controller_button ; loop to see if next button is pressed
rts ; finished reading controller inputs. $04 and $05 contain button state
; decrements intro theme delay timer, and checks if player pressed start or select
; if so, stop demo and show player select UI
dec_theme_delay_check_user_input:
jsr dec_intro_theme_delay ; decrement intro theme delay timer
lda CONTROLLER_STATE_DIFF ; controller 1 buttons pressed
and #$30 ; select and start buttons
beq timer_exit ; if neither start nor select pressed, exit to continue animation
jsr reset_delay_timer ; reset 2-byte delay timer to #$0240
ldx GAME_ROUTINE_INDEX
cpx #$01 ; check if displaying a demo (in game_routine_02)
bne stop_demo_load_player_select_UI ; not in player select UI, stop demo and show player select UI
ldx HORIZONTAL_SCROLL ; load horizontal component of the PPUSCROLL [#$0 - #$ff]
bne load_intro_palette2_play_intro_sound ; if intro animation scroll wasn't complete, load graphics palette and play intro theme
and #$20 ; see if select button is pressed
bne player_mode_change ; if select was pressed, update the cursor to point to either 1 PLAYER or 2 PLAYERS
lda #$03 ; start button was pressed, set GAME_ROUTINE_INDEX to #$03 (start new game)
jmp set_game_routine_index_to_a ; go to next game routine
; swaps player mode from 1 PLAYER (#$00) to 2 PLAYER (#$01) or vice versa
player_mode_change:
inc PLAYER_MODE ; add one to number of players (will correct if more than #$02 players below)
lda #$02
sec ; set the carry flag for the next subtract statement
sbc PLAYER_MODE ; subtract player mode from #$02
bne timer_exit ; player mode was #$00 and now is #$01, simply exit
sta PLAYER_MODE ; player mode was #$02, set PLAYER_MODE so it is #$00 (#$01 player)
timer_exit:
rts
; user has pressed start or select while intro scrolling
; skip scrolling animation and load player select UI
stop_demo_load_player_select_UI:
lda #$00
sta GRAPHICS_BUFFER_MODE
jsr zero_out_nametables
jsr load_intro_graphics
jsr load_intro_palette2_play_intro_sound
lda #$01
jmp set_game_routine_index_to_a ; set GAME_ROUTINE_INDEX to #$01
; loads the 2nd intro palette for when Bill and Lance are on screen
; also plays the intro explosion sound
load_intro_palette2_play_intro_sound:
lda #$00
sta HORIZONTAL_SCROLL ; set the scroll to #$00 (completed) for next frame so player select UI is shown
lda #$b0
sta PPUCTRL_SETTINGS ; set nametable to $2000 next update of PPUCTRL
lda #$a4
sta INTRO_THEME_DELAY ; set intro theme delay timer to #$a4 (~5 seconds)
lda #$04 ; set background palettes for when Bill and Lance on screen (intro_background_palette2)
.ifdef Probotector
jmp load_bank_6_write_text_palette_to_mem ; write the palette data to CPU_GRAPHICS_BUFFER in CPU memory
; sound already played in game_routine_01 for Probotector
; so no play_sound call
.else
jsr load_bank_6_write_text_palette_to_mem ; write the palette data to CPU_GRAPHICS_BUFFER in CPU memory
lda #$26 ; a = #$26 (game intro tune)
jmp play_sound ; play sound_26 (game intro tune)
.endif
; delay INTRO_THEME_DELAY on odd frames
dec_intro_theme_delay:
lda FRAME_COUNTER ; load frame counter
and #$01 ; only care about least significant bit
bne timer_exit ; if last bit is not 0 (even frame), jump to timer_exit
lda INTRO_THEME_DELAY
beq timer_exit ; if INTRO_THEME_DELAY is #$0 then jump to timer_exit
dec INTRO_THEME_DELAY ; decrement from delay
rts ; exit
set_next_demo_level:
jsr clear_memory_3 ; clear $0028 to $00f0 then CPU_SPRITE_BUFFER up to CPU_GRAPHICS_BUFFER
lda #$07 ; see player_mode_1d_table
sta PLAYER_MODE_1D ; set to #$07 for 2 player
lda #$00 ; a = #$00
sta FRAME_COUNTER ; reset frame counter
sta RANDOM_NUM ; set randomizer to #$00
lda DEMO_LEVEL ; load level of demo to first level
cmp #$03 ; compare against 4th demo level
bcc @continue ; branch if less than 3
lda #$00 ; reset DEMO_LEVEL back to 0 if DEMO_LEVEL >= 3
; only levels 0 to 2 are demoed
@continue:
sta DEMO_LEVEL ; level of demo mode
sta CURRENT_LEVEL ; current level (0 = level 1)
inc DEMO_LEVEL ; increment level of demo mode
lda #$62 ; a = #$62
sta P1_NUM_LIVES ; player 1 lives
sta P2_NUM_LIVES ; player 2 lives
rts
; clears certain level and player data
; initializes player score and number of lives
init_score_player_lives:
jsr clear_memory_3 ; clear $0028 to $00f0 then CPU_SPRITE_BUFFER up to CPU_GRAPHICS_BUFFER
sta DEMO_LEVEL ; level of demo mode
lda #$03 ; a = #$03
sta NUM_CONTINUES ; continues remaining
reset_players_score:
ldx #$03 ; x = #$03
lda #$00 ; a = #$00
clear_score_byte:
sta PLAYER_1_SCORE_LOW,x ; clear player 1 and player 2 scores
dex
bpl clear_score_byte
sta DEMO_MODE ; ensure demo mode is #$00 (not in demo mode)
sta P1_GAME_OVER_STATUS ; set game over status for p1 to #$0 (not in game over state)
ldx PLAYER_MODE ; load number of players #$00 is 1 player, #$01 is 2 player
lda player_mode_1d_table,x ; #$01 for 1 player, #$07 for 2 player
sta PLAYER_MODE_1D
lda p2_game_over_status_tbl,x ; load initial player 2 game over status
sta P2_GAME_OVER_STATUS ; set to 1 when 1 player game; set to 0 if 2 players are playing
init_player_lives:
lda #$02 ; start of with #$02 lives
ldy KONAMI_CODE_STATUS ; 30-lives code switch ($01 = code activated)
beq init_player_num_lives ; if KONAMI_CODE_STATUS is not set, then just set 2 lives
lda #$1d ; KONAMI_CODE_STATUS active so set lives to #$1d (29 decimal)
; set the player number of remaining lives to either #$02 or #$1d depending if Konami code used
; sets default score required for extra lives as well
init_player_num_lives:
sta P1_NUM_LIVES,x ; if X is 1, then set P2_NUM_LIVES to accumulator (a is either #$02 or #$1d)
dex ; decrement player number
bpl init_player_lives ; if more another player to set score, jump
lda #$c8 ; set default high score. #$c8 is 200 decimal
sta EXTRA_LIFE_SCORE_LOW ; starting score for extra life (20,000)
sta $3e ; player 2 default score for extra life
lda #$00
sta EXTRA_LIFE_SCORE_HIGH ; clear high byte of score for extra life
sta KONAMI_CODE_NUM_CORRECT ; clear number of successful inputs to Konami code
rts
; a lookup of whether 1 player game or 2 player game
; first byte #$00 is when PLAYER_MODE = #$00 (1 player)
; second byte #$07 is when PLAYER_MODE = #$01 (2 player)
player_mode_1d_table:
.byte $01,$07
; initial value for P2_GAME_OVER_STATUS
; set to #$01 for 1 player game
; set to #$00 for 2 player game
p2_game_over_status_tbl:
.byte $01,$00
; clear memory addresses $0028 to $00f0 then CPU_SPRITE_BUFFER up to CPU_GRAPHICS_BUFFER
clear_memory_3:
ldx #$28
; clear x to #$f0 bytes
; then clear CPU_SPRITE_BUFFER ($300) up to CPU_GRAPHICS_BUFFER ($700)
clear_memory_starting_a_x:
lda #$00
@loop:
sta $00,x
inx
cpx #$f0
bne @loop
; clear memory from CPU_SPRITE_BUFFER ($300) up to CPU_GRAPHICS_BUFFER ($700)
ldx #$07 ; ending high byte of CPU memory to clear (exclusive)
ldy #$03 ; starting high byte of CPU memory to clear
sty $01
sta $00
ldy #$00
; clear blocks of memory specified by the 2-byte $00 address
; clears until the memory address $X00, specified by the x register
; in this case clear memory from CPU_SPRITE_BUFFER to $06FF
clear_memory_block:
sta ($00),y
iny
bne clear_memory_block
inc $01
cpx $01
bne clear_memory_block
add_player_score_exit:
rts
; add enemy points to player score in memory
; determines if extra life is awarded and awards if necessary
; determines if high score is met and updates if necessary
; y is player number: either $00 (player 1) or $01 (player 2)
; $00 (low byte) and $01 (high byte) contain the score to add
; $01 is always #$00
add_player_low_score:
lda DEMO_MODE ; #$00 not in demo mode, #$01 demo mode on
bne add_player_score_exit ; exit when in demo mode
lda #$00 ; a = #$00
sta $01 ; set high byte of score to #$00
; add enemy points to player score in memory
; determines if extra life is awarded and awards if necessary
; determines if high score is met and updates if necessary
; y is player number: either $00 (player 1) or $01 (player 2)
; $00 (low byte) and $01 (high byte) contain the score to add
add_player_score:
tya ; transfer player number to a
sta $02 ; store player number in $02
asl ; double since each player has 2 bytes representing score
tay ; transfer offset to y
lda $00 ; load low byte of score to add to player score
adc PLAYER_1_SCORE_LOW,y ; add low byte of score to add to player score (low byte)
sta PLAYER_1_SCORE_LOW,y ; store updated player score (low byte)
lda $01 ; load high byte of score to add to player score (always #$00 or #$50)
adc PLAYER_1_SCORE_HIGH,y ; add high byte of score to add to player score (high byte)
bcc @continue ; continue if no overflow occurred
lda #$ff ; overflow occurred in high byte, player maxed out score set low byte to #$ff
sta PLAYER_1_SCORE_LOW,y ; player score (low byte)
@continue:
sta PLAYER_1_SCORE_HIGH,y ; store updated player score (high byte)
ldx $01 ; load high byte of player score to add
beq @set_if_extra_life ; branch if not special #$a0 score (all other scores have #$00 for high byte)
lda #$88 ; #$88 will cause a subtraction of #$50 as the extra life score low add byte
clc ; clear carry so always jump
bcc @set_extra_life_inc_num_lives ; always jump
@set_if_extra_life:
cmp EXTRA_LIFE_SCORE_HIGH,y ; player score for extra life - high byte
bcc @set_if_new_high_score ; player did not get extra life, skip to check if player got new high score
bne @extra_life_logic ; score greater than necessary to get extra life
lda PLAYER_1_SCORE_LOW,y ; load player score low byte
cmp EXTRA_LIFE_SCORE_LOW,y ; player score for extra life - low byte
bcc @set_if_new_high_score ; player score less than EXTRA_LIFE_SCORE_LOW, no need to check if extra life
; every time an extra life is obtained, #$12c is added to points required to get next extra life (300 decimal, 30,000 in game score)
; once score reaches #$7500 (2,995,200 in game score), no more extra lives are awarded
; if adding special score code #$0a, then the next extra life is given and number of points needed for next extra life is unchanged
; because #$1388 points are added to the extra life score, so distance until next 30,000 points is kept the same
@extra_life_logic:
lda EXTRA_LIFE_SCORE_HIGH,y ; player score for extra life - high byte
cmp #$75 ; compare EXTRA_LIFE_SCORE_HIGH to #$75
bcs @set_if_new_high_score ; EXTRA_LIFE_SCORE_HIGH > #$75, score too high, don't give any more extra lives
lda #$2c ; a = #$2c (12c = 300 decimal) (high byte will be set to #$01 a few lines down)
@set_extra_life_inc_num_lives:
adc EXTRA_LIFE_SCORE_LOW,y ; add 300 decimal to low byte of score required to get extra life
sta EXTRA_LIFE_SCORE_LOW,y ; player score for extra life - low byte
lda #$01 ; a = #$01
ldx $01 ; determine if high byte is set
beq @continue_inc_num_lives ; branch if not special #$a0 score (all other scores have #$00 for high byte)
lda #$13 ; a = #$13
@continue_inc_num_lives:
adc EXTRA_LIFE_SCORE_HIGH,y ; player score for extra life - high byte
bcc @inc_num_lives
lda #$ff ; a = #$ff
sta EXTRA_LIFE_SCORE_LOW,y ; maxed out extra life high score, max out low byte
@inc_num_lives:
sta EXTRA_LIFE_SCORE_HIGH,y ; player score for extra life - high byte
ldx $02 ; load the player number
inc P1_NUM_LIVES,x ; if X is $01, then set P2 number of lives
lda P1_NUM_LIVES,x ; load incremented number into memory
cmp #$63 ; compare to #$99 lives
bcc @set_num_lives ; can't have more than 99 lives
lda #$63 ; a = #$63 (63 = 99 decimal)
@set_num_lives:
sta P1_NUM_LIVES,x ; number of lives
lda $01
bne @set_if_new_high_score ; don't play sound for special #$a0 score code
lda #$20 ; a = #$20 (sound_20)
jsr play_sound ; play extra life sound sound
@set_if_new_high_score:
lda PLAYER_1_SCORE_HIGH,y ; player score (high byte)
cmp HIGH_SCORE_HIGH ; high score (high byte)
bcc score_exit ; exit if no need to update high score score
bne @set_high_score ; high byte high score is greater, update high score score
lda PLAYER_1_SCORE_LOW,y ; player score (low byte)
cmp HIGH_SCORE_LOW ; high score (low byte)
bcc score_exit ; don't update high score score if player score isn't bigger than high score
@set_high_score:
lda PLAYER_1_SCORE_LOW,y ; load player score low byte
sta HIGH_SCORE_LOW ; set new high score low byte to player score low byte
lda PLAYER_1_SCORE_HIGH,y ; load player score high byte
sta HIGH_SCORE_HIGH ; set new high score high byte to player score high byte
score_exit:
rts
; redraws parts of the nametable for things like bridge explosions,
; nametable enemy explosions, animation (pill box sensor), etc.
; also used to draw palette colors for super-tiles
; input
; * a is x position of nametable super-tile in pixels
; * y is y position of nametable super-tile in pixels
; * $10 is the super-tile or palette index to draw (level_x_nametable_update_supertile_data/level_x_nametable_update_palette_data offset)
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
update_nametable_supertile:
sta $11 ; store the x position (in pixels) of the location to redraw the nametable in $11 (except bit 7)
lda GRAPHICS_BUFFER_OFFSET ; read current offset
cmp #$40 ; see if graphics buffer is already full
bcs score_exit ; exit if offset is greater than or equal to #$40
jsr set_ppu_addresses_in_mem ; determines attribute table PPU address, $14 (low) and $15 (high)
; determines PPU nametable write address, $0c (low) and $0d (high)
; for x ($11), y (y) coordinates
; $00 is set to non zero if should update palette, #$00 for nametable update only
ldx GRAPHICS_BUFFER_OFFSET
lda CURRENT_LEVEL ; current level
ldy LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bpl @continue ; outdoor levels use level-specific super-tiles
lda #$08 ; indoor (base) levels use a shared super-tile set (level 2 and 4)
@continue:
asl ; each address is 2 bytes, so double
asl ; each level has both super tile data and pattern data, so double
tay ; transfer offset into y
lda nametable_update_data_ptr_tbl,y ; load low byte of super-tile data address
sta $16 ; store in $16
lda nametable_update_data_ptr_tbl+1,y ; load high byte of super-tile data address
sta $17 ; store in $17
lda $0f ; determines if need to update the palette (#$00 meaning palette update is required, and needs to be added to CPU_GRAPHICS_BUFFER)
bne write_update_supertile_to_cpu ; go ahead and write the entire new super-tile bytes to the CPU_GRAPHICS_BUFFER, with no palette update instructions
lda nametable_update_data_ptr_tbl+2,y ; need to update palette, prep to write to CPU_GRAPHICS_BUFFER. load low byte of palette data address
sta $0e ; store low byte in $0e
lda nametable_update_data_ptr_tbl+3,y ; load high byte of palette data address
sta $0f ; store high byte in $0f
ldy $10 ; load level_X_nametable_update_palette_data read offset
lda $00
bne update_supertile_palette ; if $00 is not #$00, then branch, updates palette based on super-tile data for level instead of from nametable_update_data_ptr_tbl
jsr set_graphics_buffer_header ; set CPU_GRAPHICS_BUFFER to write one tile to PPU at PPU address specified in $14 and $15
lda ($0e),y ; read palette data byte for the super-tile
sta CPU_GRAPHICS_BUFFER,x ; write palette data byte to CPU_GRAPHICS_BUFFER (palette for entire super-tile)
inx
; updates/overwrites a single super-tile on the nametable
write_update_supertile_to_cpu:
lda #$00 ; a = #$00
sta $11 ; clear out address high byte overflow counter
lda $10 ; load level_X_nametable_update_supertile_data read offset
asl ; each entry is #$10 bytes, multiply by #$10
asl ; keeping track of overflow
rol $11
asl
rol $11
asl
rol $11
adc $16 ; add to nametable_update_data_ptr_tbl high byte
sta $16 ; PPU write address low byte
lda $11 ; load any overflow
adc $17 ; add to high byte of level_x_nametable_update_supertile_data offset
sta $17 ; PPU write address high byte
lda #$01
sta CPU_GRAPHICS_BUFFER,x ; set VRAM address increment to 0, meaning to add #$1 every write to PPU (write horizontally)
inx
lda #$04 ; a super-tile is 4 rows of 4 pattern table tiles, set pattern table tile size to #$04
sta $14 ; CPU_GRAPHICS_BUFFER graphic data group size
sta CPU_GRAPHICS_BUFFER,x ; each group of graphic data is #$04 bytes (4 rows in super-tile)
inx
sta CPU_GRAPHICS_BUFFER,x ; #$04 groups of #$04-byte-sized entries
inx
ldy #$00 ; initialize level_x_nametable_update_supertile_data entry offset to #$00 (beginning of data)
; write the PPU write address and then the graphics data
@write_graphic_group:
lda $0d ; PPU write address high byte
sta CPU_GRAPHICS_BUFFER,x ; set PPU write address high byte
inx
lda $0c ; PPU write address low byte
sta CPU_GRAPHICS_BUFFER,x ; set PPU write address low byte
inx
; write the #$04 bytes of the graphic group
@write_graphic_group_bytes:
lda ($16),y ; load tile from the level_X_nametable_update_supertile_data
sta CPU_GRAPHICS_BUFFER,x ; store in CPU_GRAPHICS_BUFFER
inx ; increment CPU_GRAPHICS_BUFFER write offset
iny ; increment level_x_nametable_update_supertile_data read offset
tya
and #$03 ; keep bits .... ..xx
bne @write_graphic_group_bytes
lda $0c ; load PPU write address low byte
clc ; clear carry in preparation for addition
adc #$20 ; move down a row on the nametable to prep for drawing the next 4 tiles
sta $0c ; update PPU write address low byte
lda $0d ; load PPU write address high byte
adc #$00 ; add any carry from previous adc
sta $0d ; update PPU write address high byte
dec $14 ; decrement from group size counter
bne @write_graphic_group
stx GRAPHICS_BUFFER_OFFSET ; update GRAPHICS_BUFFER_OFFSET
clc ; clear any leftover carry
rts
; level 2/4 boss screen, waterfall (red turret)
; input
; * $00 - palette update mode (#$01 = 2 horizontally, #$02 = 2 vertically, #$03 = 4 (2x2))
update_supertile_palette:
tax ; transfer palette update mode to x
lda ($0e),y ; load palette byte for super-tile
dex ; decrement palette update mode
stx $01 ; save in $01
bne @update_palette_01 ; branch if palette command code was not #$01
tax ; update 2 super-tiles' palettes horizontally, move palette byte to x
and #$33 ; keep bits ..xx ..xx
asl
asl
sta $08
txa
and #$cc ; keep bits xx.. xx..
lsr
lsr
sta $09
ldx $02 ; load current screen super-tile offset (set in set_ppu_addresses_in_mem)
; e.g. level_2_4_boss_supertiles_screen_00
ldy LEVEL_SCREEN_SUPERTILES,x ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load the palette data for the current super-tile
and #$33 ; keep left half of super-tile palette data
ora $08 ; merge with right half of super-tile palette
sta $08 ; set super-tile palette data
ldy LEVEL_SCREEN_SUPERTILES+1,x ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load the palette data for the current super-tile
and #$cc ; keep right half of super-tile palette data
ora $09 ; merge with left half of super-tile palette
sta $09 ; set super-tile palette data
jmp @update_palette_continue
; vertical two super-tiles
@update_palette_01:
dex ; decrement $00 value
bne @update_palette_02
ldx #$00 ; x = #$00
stx $09
asl
rol $09
asl
rol $09
asl
rol $09
asl
rol $09
sta $08
ldx $02
ldy LEVEL_SCREEN_SUPERTILES,x ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; level_x_palette_data
and #$0f ; keep bits .... xxxx
ora $08
sta $08 ; store
ldy LEVEL_SCREEN_SUPERTILES+8,x
lda (LEVEL_SUPERTILE_PALETTE_DATA),y
and #$f0 ; keep bits xxxx ....
ora $09
sta $09
jmp @update_palette_continue
; boss screen 2/4
; loads 4 super-tile palette, 2x2
@update_palette_02:
ldx #$00 ; x = #$00
stx $08
stx $0b
lsr
ror $08
lsr
ror $08
and #$0c ; keep bits .... xx..
sta $0a
lda ($0e),y
asl
rol $0b
asl
rol $0b
and #$30 ; keep bits ..xx ....
sta $09
ldx $02
ldy LEVEL_SCREEN_SUPERTILES,x ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
lda (LEVEL_SUPERTILE_PALETTE_DATA),y
and #$3f ; keep bits ..xx xxxx
ora $08
sta $08
ldy LEVEL_SCREEN_SUPERTILES+1,x
lda (LEVEL_SUPERTILE_PALETTE_DATA),y
and #$cf ; keep bits xx.. xxxx
ora $09
sta $09
ldy LEVEL_SCREEN_SUPERTILES+8,x
lda (LEVEL_SUPERTILE_PALETTE_DATA),y
and #$f3 ; keep bits xxxx ..xx
ora $0a
sta $0a
ldy LEVEL_SCREEN_SUPERTILES+9,x
lda (LEVEL_SUPERTILE_PALETTE_DATA),y
and #$fc ; keep bits xxxx xx..
ora $0b
sta $0b
@update_palette_continue:
lda $01 ; load updated palette update mode (#$00 = 2 horizontally, #$01 = 2 vertically, #$02 = 4 (2x2))
asl ; double since to determine how many super-tile palettes are being updated
tay ; transfer number of super-tiles to update to offset register
ldx GRAPHICS_BUFFER_OFFSET ; load current offset into graphics buffer
lda #$01 ; a = #$01
sta CPU_GRAPHICS_BUFFER,x ; set VRAM address increment to 0, meaning to add #$1 every write to PPU (write across)
inx ; increment graphics buffer offset
lda update_palette_cfg_tbl,y ; load how many tiles to draw per graphics group
sta CPU_GRAPHICS_BUFFER,x ; set how many pattern table tiles to draw per group
inx ; increment graphics buffer offset
lda update_palette_cfg_tbl+1,y ; load how many graphics groups there are to draw
sta CPU_GRAPHICS_BUFFER,x ; set how many graphics groups to draw
inx ; increment graphics buffer offset
ldy #$00 ; y = #$00
@write_palette_data:
lda $15 ; load PPU address (attribute table) high byte
sta CPU_GRAPHICS_BUFFER,x ; set PPU address (attribute table) high byte
inx ; increment graphics buffer offset
lda $14 ; load PPU address (attribute table) low byte
sta CPU_GRAPHICS_BUFFER,x ; set PPU address (attribute table) low byte
inx ; increment graphics buffer offset
lda $08,y ; load super-tile palette data
sta CPU_GRAPHICS_BUFFER,x ; set super-tile palette data
inx ; increment graphics buffer offset
lda $00 ; load original palette update mode (#$01 = 2 horizontally, #$02 = 2 vertically, #$03 = 4 (2x2))
cmp #$02 ; compare to mode 2
beq @mv_down_row ; branch if mode #$02 (2 vertical)
iny ; either mode #$01 or #$03 (mode contains 2 horizontal super-tiles)
; increment number of super-tiles updated
lda $08,y ; load 2nd super-tile palette data
sta CPU_GRAPHICS_BUFFER,x ; set 2nd super-tile palette data
inx ; increment graphics buffer offset
@mv_down_row:
lda $01 ; load updated palette update mode (#$00 = 2 horizontally, #$01 = 2 vertically, #$02 = 4 (2x2))
beq @write_update_supertile ; finished writing super-tile palette data, move to update super-tile pattern tiles
iny ; increment number of super-tiles updated
lda $14 ; load super-tile palette PPU address (attribute table) low byte
clc ; clear carry in preparation for addition
adc #$08 ; move one super-tile row down
sta $14 ; set new super-tile palette PPU address (attribute table) low byte
lda #$00 ; a = #$00
sta $01 ; change palette update mode from #$02 to #$01, or from #$01 to #$00
beq @write_palette_data ; loop to write the next row (only for mode #$02 which has 4 super-tiles to update total)
@write_update_supertile:
jmp write_update_supertile_to_cpu ; finished writing super-tile palette data, move to super-tile pattern tile data
; configuration based on palette update mode
; #$00 = 2 horizontally, #$02 = 2 vertically, #$04 = 4 (2x2)
; byte 0 - number of pattern table tiles per graphics group
; byte 1 - number of graphics groups
update_palette_cfg_tbl:
.byte $02,$01
.byte $01,$02
.byte $02,$02
; bank 3 offsets
; pointer table for nametable super-tile nametable and palettes ($12 * $02 = $24 bytes)
; 2 pointers per level.
; * pointer 1: super-tile tile definitions
; * pointer 2: palette codes for the super-tile, values ultimately end up in attribute table
; each byte from level_X_nametable_update_palette_data is a quarter of a super-tile
nametable_update_data_ptr_tbl:
.addr level_1_nametable_update_supertile_data ; bank 3 label - CPU address $83b1
.addr level_1_nametable_update_palette_data ; bank 3 label - CPU address $86ac
.addr level_2_nametable_update_supertile_data ; bank 3 label - CPU address $88a8
.addr level_2_nametable_update_palette_data ; bank 3 label - CPU address $8e91
.addr level_3_nametable_update_supertile_data ; bank 3 label - CPU address $9368
.addr level_3_nametable_update_palette_data ; bank 3 label - CPU address $965f
.addr level_4_nametable_update_supertile_data ; bank 3 label - CPU address $88a8
.addr level_4_nametable_update_palette_data ; bank 3 label - CPU address $8e91
.addr level_5_nametable_update_supertile_data ; bank 3 label - CPU address $9ba8
.addr level_5_nametable_update_palette_data ; bank 3 label - CPU address $9db9
.addr level_6_nametable_update_supertile_data ; bank 3 label - CPU address $a4ae
.addr level_6_nametable_update_palette_data ; bank 3 label - CPU address $a567
.addr level_7_nametable_update_supertile_data ; bank 3 label - CPU address $abea
.addr level_7_nametable_update_palette_data ; bank 3 label - CPU address $adae
.addr level_8_nametable_update_supertile_data ; bank 3 label - CPU address $b25a
.addr level_8_nametable_update_palette_data ; bank 3 label - CPU address $b543
.addr level_2_4_nametable_update_supertile_data ; bank 3 label - CPU address $ba1a
.addr level_2_4_boss_nametable_update_palette_data ; bank 3 label - CPU address $bdc4
update_nametable_tiles_exit:
rts
; updates #$02 columns of n rows (default #$02) of a nametable at position (a,y) with desired pattern table tiles
; bank 3 should be loaded
; input
; * a is ENEMY_X_POS
; * y is ENEMY_Y_POS
; * $10 (multiplied by #$05) is the index into the tile animation table to start drawing
; if bit 7 clear, then update palette, if bit 7 set do not update palette
; output
; * carry - clear when successful, set when CPU_GRAPHICS_BUFFER is full
; for example, indoor/base levels for drawing wall turrets, and changing to explosion when destroyed
; claw animations, etc.
update_nametable_tiles:
sta $11 ; store enemy x position in $11
lda GRAPHICS_BUFFER_OFFSET ; load current GRAPHICS_BUFFER_OFFSET
cmp #$50 ; GRAPHICS_BUFFER_OFFSET goes from $700 to $750
bcs update_nametable_tiles_exit ; graphics buffer full, exit
jsr set_ppu_addresses_in_mem ; determines attribute table PPU address, $14 (low) and $15 (high)
; determines PPU nametable write address, $0c (low) and $0d (high)
; for x ($11), y (y) coordinates
lda $10 ; load the current level's super-tile read offset ($16 read offset)
asl
asl
adc $10 ; multiply by 5
tay
lda CURRENT_LEVEL ; load current level
asl ; each entry in level_tile_animation_ptr_tbl is a 2-byte address, so double
tax
lda level_tile_animation_ptr_tbl,x ; read the address low byte
sta $16 ; store the address low byte
lda level_tile_animation_ptr_tbl+1,x ; load the address high byte
sta $17 ; store the address high byte
ldx #$02 ; default to two tiles per read
lda ($16),y ; read first byte of tile animation table
; #$00 means to update #$02 rows of #$02 pattern table tiles each row
bpl @continue ; if the msb is not set draw default number of rows (#$02), jump
and #$07 ; first bit set, mask its least significant 3 bits to see how many rows to draw
tax
@continue:
stx $14 ; $14 (total number of tile groups)
; store masked first byte of ($16) if its msb was set, otherwise store #$02
ldx GRAPHICS_BUFFER_OFFSET ; load current GRAPHICS_BUFFER_OFFSET
lda $0f ; loads whether palette needs to be updated (#$00 = yes, #$80 = no)
bne @update_nametable_tiles ; branch if $0f is #$80
lda ($16),y ; palette needs to be updated, re-read first byte of tile animation table
sty $0e ; store update tile offset in $0e
ldy $00 ; load palette update mode (#$01 = 2 horizontally, #$02 = 2 vertically, #$03 = 4 (2x2))
beq @set_supertile_palette
dey
beq @shift_2 ; branch if palette update mode is #$01 (2 horizontally)
dey
beq @shift_4 ; branch if palette update mode is #$02 (2 vertically)
asl
asl
@shift_4:
asl
asl
@shift_2:
asl
asl
@set_supertile_palette:
sta $08
ldx $02 ; load the super tile to draw LEVEL_SCREEN_SUPERTILES offset
ldy LEVEL_SCREEN_SUPERTILES,x ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load palette data for super-tile
ldy $00
and palette_mask_tbl,y ; strip out the 2 bits that need to change
ora $08 ; merge with new palette entry for quadrant of super-tile
sta $08 ; set updated palette data for super-tile
ldx GRAPHICS_BUFFER_OFFSET ; load graphics buffer offset
jsr set_graphics_buffer_header ; create entry saying to write one byte to PPU at PPU address specified by $14 (low PPU write address) and $15 (high PPU write address)
lda $08 ; load super-tile palette
sta CPU_GRAPHICS_BUFFER,x ; set super-tile palette to graphics buffer
inx ; increment graphics buffer read offset
ldy $0e ; restore tile animation offset
; specifies VRAM address increment, # of tiles groups and size of tile group
@update_nametable_tiles:
iny
lda #$01 ; set VRAM address increment to 1 (write across)
sta CPU_GRAPHICS_BUFFER,x
inx
lda #$02 ; set number of tiles per group to #$02
sta CPU_GRAPHICS_BUFFER,x
inx
lda $14 ; set total number of tile groups to $14
sta CPU_GRAPHICS_BUFFER,x
inx
; specifies PPU write address in CPU_GRAPHICS_BUFFER
; specifies #$02 pattern table tiles per row
prep_overwrite_nametable_tiles:
lda #$02 ; a = #$02
sta $15 ; set number of tiles in each group in CPU_GRAPHICS_BUFFER
lda $0d ; load low byte of PPU write address
sta CPU_GRAPHICS_BUFFER,x ; store low byte of PPU write address
inx ; increment PPU write offset
lda $0c ; load PPU tile write address high byte
sta CPU_GRAPHICS_BUFFER,x ; store high byte of PPU write address
inx ; increment CPU_GRAPHICS_BUFFER write offset
write_overwrite_tile_to_cpu_buffer:
lda ($16),y ; read tile from tile_animation_ptr data
sta CPU_GRAPHICS_BUFFER,x ; write tile to CPU_GRAPHICS_BUFFER
inx ; increment CPU_GRAPHICS_BUFFER offset
iny ; increment tile_animation_ptr read offset
dec $15 ; decrement tile read total
bne write_overwrite_tile_to_cpu_buffer ; if we aren't done writing tiles, loop
lda $0c ; load the low byte PPU write address
clc ; clear any previous carry
adc #$20 ; move next write address down by a row on the nametable (same column)
sta $0c ; store updated PPU tile write address for next loop
lda $0d ; load current PPU tile write address high byte
adc #$00 ; if there was a carry adding the #$20 to the low byte, add it to the high byte
sta $0d ; store updated PPU tile write address for next loop
dec $14 ; finished writing tile group to CPU_GRAPHICS_BUFFER, move to next group of overwrite tiles
bne prep_overwrite_nametable_tiles ; loop to next set of tiles to write to CPU_GRAPHICS_BUFFER
stx GRAPHICS_BUFFER_OFFSET ; ensure graphics buffer offset is up to date
clc ; clear any carry
rts
; bank 3 labels
; pointer table to pattern table tiles that are used to modify nametable after fully drawn for animations ($09 * $02 = $12 bytes)
level_tile_animation_ptr_tbl:
.addr level_1_supertile_data ; level 1 - CPU address $8001 (unused)
.addr level_2_4_tile_animation ; level 2 - CPU address $86e1
.addr level_3_supertile_data ; level 3 - CPU address $8ef8 (unused)
.addr level_2_4_tile_animation ; level 4 - CPU address $86e1
.addr level_5_supertile_data ; level 5 - CPU address $9698 (unused)
.addr level_6_tile_animation ; level 6 - CPU address $9dd8
.addr level_7_tile_animation ; level 7 - CPU address $a56e
.addr level_8_supertile_data ; level 8 - CPU address $adca (unused)
.addr level_2_4_boss_supertile_data ; level 2/4 boss rooms - CPU address $b57a
; the following 3 tables are ppu addresses for the tile_animation tables entries
; used on all levels for animations
nametable_base_high_byte:
.byte $20,$24
attribute_base_high_byte:
.byte $23,$27
; the base offset into cpu graphics buffer where super-tile indexes are loaded (LEVEL_SCREEN_SUPERTILES)
; $0600 or $0640
level_screen_mem_offset_tbl_00:
.byte $00,$40
palette_mask_tbl:
.byte $fc ; 1111 1100
.byte $f3 ; 1111 0011
.byte $cf ; 1100 1111
.byte $3f ; 0011 1111
; determines ppu nametable and attribute table addresses for given x, y coordinate
; input
; * $10 - super-tile to draw, bit 7 used to load $0f (palette update marker)
; (level_x_nametable_update_supertile_data/level_x_nametable_update_palette_data offset)
; if bit 7 clear, then update palette, if bit 7 set do not update palette
; * $11 - x offset
; * y - y offset
; output
; * PPU nametable write address: $0c (low) and $0d (high)
; * PPU nametable collision address: $12 (low) and $13 (high)
; * used for nametable collision removal
; * always same as $0c and $0d
; * PPU attribute table write address: $14 (low) and $15 (high)
; * $00 - if set, then branch update_supertile_palette is executed
; palette update modes (#$01 = 2 horizontally, #$02 = 2 vertically, #$03 = 4 (2x2))
; * $10 - same as input but with bit 7 stripped
; * $02 - super-tile index at location (level_x_supertiles_screen_xx offset)
; * $0f - #$00 if palette needs to be updated, #$80 otherwise
set_ppu_addresses_in_mem:
lda $10 ; load super-tile to draw
; (offset into level_x_nametable_update_supertile_data/level_x_nametable_update_palette_data offset)
and #$80 ; keep most significant bit (palette update flag)
sta $0f ; if #$00, then mark palette to updated as well as super-tile, #$80 means do not update palette
lda $10 ; re-load super-tile to draw
and #$7f ; trim off most significant bit (palette update flag)
sta $10 ; save updated super-tile to draw
tya ; transfer y offset to a
clc ; clear carry in preparation for addition
adc VERTICAL_SCROLL ; add vertical scroll offset to y pixel offset, e.g. #$e0 (224 pixels/28 tiles) for outdoor levels
bcs @round_up ; branch if an overflow occurred to continue
cmp #$f0 ; no overflow
bcc @nametable
@round_up:
adc #$0f
@nametable:
and #$f8 ; keep bits xxxx x...
sta $12 ; set PPU nametable address low byte
lsr
lsr
tay
lsr
and #$02 ; keep bits .... ..x.
sta $00
tya
and #$38 ; keep bits ..xx x...
sta $14 ; set PPU attribute table write address low byte
lda #$00 ; not used, next line overrides a
asl $12 ; PPU nametable address low byte
rol ; moves any carry from previous asl to bit 0
asl $12 ; PPU nametable address low byte
rol ; moves any carry from previous asl to bit 0
sta $13 ; set PPU nametable address high byte
lda $11 ; load x offset in pixels
clc ; clear carry in preparation for addition
adc HORIZONTAL_SCROLL ; add the horizontal scroll to the x position
sta $11 ; update x offset to include horizontal scroll
lda PPUCTRL_SETTINGS ; pull part of base nametable address, used to determine high byte for nametable and attribute table
and #$01 ; keep least significant bit (nametable base address 0 = $2000; 1 = $2400)
bcc @continue ; branch if no carry occurred on adc
eor #$01 ; carry occurred flip bits .... ...x
@continue:
tay ; transfer nametable index offset to y (#$00 or #$01)
lda attribute_base_high_byte,y ; grab byte used to determine attribute table PPU address
ora #$03 ; ensure smallest 2 bits always set (.... ..xx)
sta $15 ; set PPU attribute table write address high byte
lda nametable_base_high_byte,y ; grab byte used to determine nametable PPU address
ora $13 ; merge with value already in $13
sta $13 ; set PPU nametable collision address high byte
sta $0d ; store PPU write address high byte
lda level_screen_mem_offset_tbl_00,y ; load the base offset from LEVEL_SCREEN_SUPERTILES ($0600 or $0640)
sta $02 ; set base level screen supertile offset $0600 for nametable 0, $0640 for nametable 1
lda $11 ; load x offset in pixels including horizontal scroll
and #$f8 ; keep bits xxxx x...
lsr
lsr
lsr
tay
lsr
tax
and #$01 ; keep bits .... ...x
ora $00
sta $00
txa
lsr
ora $14 ; merge with PPU attribute table low write address high byte
sta $03 ; set LEVEL_SCREEN_SUPERTILES offset
clc ; clear carry in preparation for addition
adc #$c0
sta $14 ; set PPU attribute table low write address high byte
tya
ora $12 ; merge with PPU nametable collision address low byte
sta $12 ; set PPU nametable collision address low byte
sta $0c ; PPU write address low byte
lda $02 ; load base level screen supertile offset $0600 for nametable 0, $0640 for nametable 1
ora $03 ; merge with offset into structure
sta $02 ; set super-tile index for current screen (level_x_supertiles_screen_xx offset)
rts
; creates CPU_GRAPHICS_BUFFER entry to specify writing one byte to PPU
; at PPU address specified by $14 (low PPU write address) and $15 (high PPU write address)
set_graphics_buffer_header:
lda #$01 ; set VRAM address increment to 0, meaning to add #$1 every write to PPU (write horizontally)
sta CPU_GRAPHICS_BUFFER,x
inx
sta CPU_GRAPHICS_BUFFER,x ; writing #$01-byte long groups of tiles
inx
sta CPU_GRAPHICS_BUFFER,x ; writing #$01 group of #$01 byte tiles, i.e. writing #$01 tile total
inx
lda $15
sta CPU_GRAPHICS_BUFFER,x ; set PPU write address low byte to $15
inx
lda $14
sta CPU_GRAPHICS_BUFFER,x ; set PPU write address high byte to $14
inx
rts
; execute the code at offset A from the pointer table underneath the jsr opcode that called this method
; this is done by reading with offset from the value of the stack before this call and adding 1
; which effectively allows this method to read from the pointer table below the calling code
; CPU address $c857
run_routine_from_tbl_below:
asl ; double A since each entry is a 2-byte label address
sty $03 ; store y into $03 temporarily since this method overrides y
tay ; store offset into y
iny ; add one since the stack pointer points to one byte before table to offset into
pla ; pull the low byte of the stack pointer into a
sta $00 ; store low byte of stack pointer address into $00
pla ; pull the high byte of the stack pointer memory address into a
sta $01 ; store high byte into $01
lda ($00),y ; read low byte of address of code to execute (offset into table)
sta $02 ; store the low byte into $02
iny ; increment offset so high byte can be read
lda ($00),y ; read the high byte of the address of the code to execute (offset into table)
ldy $03 ; restore y register to what it was before the call to run_routine_from_tbl_below
sta $03 ; store high byte into $03
jmp ($0002) ; jump to the code specified by the address at offset A into the pointer table table
; dead code, never called !(UNUSED)
bank_7_unused_label_00:
stx $00
sty $01
; Calculate next digit of the score and put $02 and A register
;
; This logic converts from a binary to decimal, decimal digit by
; decimal digit by left shifting and keeping track of when the right-most digit
; is greater than 10 decimal.
;
; For example, for a score of 255, the first call to calculate the next digit to
; display will return 5, and store 25 into memory for the next call. On the
; second call to calculate the next digit to display, the subroutine will return
; 5, and store 2 into memory for the last call. On the final call, 2 is returned
; and 0 is stored, letting the calling code know the score is finished.
;
; CPU Memory used
; 0x00 - low byte of the current score being calculated
; 0x01 - high byte of the current score being calculated
; 0x02 - the next decimal digit to display
; 0x03 - hard-coded 10 in decimal
calculate_score_digit:
lda #$00 ; set the accumulator register A to zero (#$00)
sta $02 ; zero out any previously calculated digit
ldy #$10 ; set the left-shift loop counter back to #$10 (16 decimal)
rol $00 ; shift the score low byte to the left by one bit
; push the most significant bit (msb) to the carry flag
rol $01 ; shift the score high byte to the left by one byte
; push the msb to the carry flag
; pull in carry flag to least significant bit (lsb)
shift_and_check_digit_carry:
rol $02 ; shift score high byte to the left by one bit
; if the msb of the score high byte was 1, then carry into lsb
lda $02 ; load current digit into the accumulator register A
cmp $03 ; compare #$0a (10 decimal) to the current digit
; branch if current digit is less than #$0a (10 decimal)
; - this means no subtraction and carry is needed
; if greater than #$0a (10 decimal), don't jump
; - subtract 10 and carry
bcc continue_shift_score
sbc $03 ; the current digit is greater than 10, subtract 10
; this also sets the carry flag, which will be moved to the
; low byte of the score, which is the "Rest" of the number
; this carry represents adding 10 to the "Rest"
sta $02 ; store the difference (new current digit) back in $02
; $02 (current digit) is less than #$0a, or has just been subtracted
; continue algorithm by shifting score left
continue_shift_score:
rol $00 ; if just set $02 to digit by subtraction, this will put 1
; in $00's lsb, signifying adding 10 to "Rest"
rol $01 ; if $00's msb is 1, then it'll carry to the lsb of $01
dey ; Y goes from $10 to $00, once Y is $00, the algorithm is done
bne shift_and_check_digit_carry
rts
; advance read address ($00,x - $01,x) by a bytes
; increment 2-byte read address in $00,$01 by 1
; uses x register as offset from $00
; a is the amount to add to the base address
advance_graphic_read_addr:
clc ; clear the carry bit
adc $00,x ; set a to the value at $00,x plus a
sta $00,x ; store result back into $00,x
bcc @exit ; if a carry is required (>255), then increment the byte at $01,x
inc $01,x ; increment high byte
@exit:
rts
; dead code, never called !(UNUSED)
@handle_overflow:
eor #$ff ; flip all bits
sec ; set carry flag
adc $00,x ; flip all bits add 1 to handle overflow
sta $00,x ; store result back in $00,x
bcs @handle_overflow_exit
dec $01,x ; decrement high bight
@handle_overflow_exit:
rts
; loads the pattern table (graphic_data_01), nametable (graphic_data_02), and palette data (transition_screen_palettes)
load_intro_graphics:
jsr init_APU_channels
jsr clear_memory_3 ; clear $0028 to $00f0 then CPU_SPRITE_BUFFER up to CPU_GRAPHICS_BUFFER
lda #$1e ; %0001 1110 no RGB emphasis, show sprites, show background
; show sprites and background in leftmost 8 pixels of screen
; no grayscale
sta PPUMASK_SETTINGS ; set PPUMASK setting to #$1e
ldy #$00
sty SPRITE_LOAD_TYPE ; if 0, load regular sprites to cpu, else load hud sprites
iny
sty DEMO_MODE ; set DEMO_MODE to true
lda #$0b ; offset into level_graphic_data_tbl pointing to intro_graphic_data_01 (intro graphics)
jsr load_A_offset_graphic_data ; load all of intro graphics specified: $01 (graphic_data_01), $02 (graphic_data_02)
lda #$06 ; offset into short_text_pointer_table, which is transition_screen_palettes (bank 6 $b302)
jsr load_bank_6_write_text_palette_to_mem ; load palette data to CPU memory CPU_GRAPHICS_BUFFER
exit_load_graphics_group:
rts
; loads the graphic data for the current level
load_current_level_graphic_data:
lda CURRENT_LEVEL
; loads the graphic data for the level specified by offset into level_graphic_data_tbl (A register) into the PPU
; most often the pattern table data, but can be nametable data (blank_nametables, graphic_data_02, graphic_data_18)
load_level_graphic_data:
asl ; each entry is 2 bytes, so multiply offset by 2
tay ; store offset into y
lda level_graphic_data_tbl,y ; load low byte of graphic data address
sta $06 ; store low byte in $06
lda level_graphic_data_tbl+1,y ; load high byte of graphic data address
sta $07 ; store high byte in $07
ldy #$00 ; set graphic data index to 0
sty $05 ; store graphic data index in $05
; loop through each graphic data and load it in PPU memory
@loop:
lda ($06),y ; read 2-byte memory address where graphics are located in memory for the current index (Y)
bmi exit_load_graphics_group ; if read #ff, then we are done with the graphic data
jsr write_graphic_data_to_ppu ; load the graphics into the PPU
inc $05 ; increment graphics offset
ldy $05
bne @loop ; load next graphics
; pointer table for graphic data codes (#$0d * #$02 = $1a bytes) CPU address $c8e3
; each entry in this table points to a list of graphic data to load
level_graphic_data_tbl:
.addr level_1_graphic_data ; level 1 - CPU address $c8fd
.addr level_2_graphic_data ; level 2 - CPU address $c905
.addr level_3_graphic_data ; level 4 - CPU address $c916
.addr level_4_graphic_data ; level 3 - CPU address $c90d
.addr level_5_graphic_data ; level 5 - CPU address $c91e
.addr level_6_graphic_data ; level 6 - CPU address $c926
.addr level_7_graphic_data ; level 7 - CPU address $c92e
.addr level_8_graphic_data ; level 8 - CPU address $c936
.addr level_2_boss_graphic_data ; level 2 boss room - CPU address $c93b
.addr level_4_boss_graphic_data ; level 4 boss room - CPU address $c940
.addr intro_graphic_data_00 ; intro graphics - CPU address $c946
.addr intro_graphic_data_01 ; intro graphics and intro nametable - CPU address $c948
.addr ending_graphic_data ; ending scene - CPU address $c94b
; the following labels contains a list of bytes
; each byte is an offset into the graphic_data_ptr_tbl table
; each label ends in #$ff
; CPU memory $c8fd
level_1_graphic_data:
.byte $03,$13,$19,$1a,$14,$16,$05,$ff ; level 1
; CPU memory $c905
level_2_graphic_data:
.byte $03,$04,$06,$0a,$0f,$10,$11,$ff ; level 2
; CPU memory $c90d
level_4_graphic_data:
.byte $03,$04,$06,$0a,$0f,$10,$11,$12,$ff ; level 4
level_3_graphic_data:
.byte $03,$13,$19,$1a,$14,$16,$07,$ff ; level 3
level_5_graphic_data:
.byte $03,$13,$19,$1a,$15,$16,$0b,$ff ; level 5
level_6_graphic_data:
.byte $03,$13,$19,$1a,$15,$16,$0c,$ff ; level 6
level_7_graphic_data:
.byte $03,$13,$19,$1a,$15,$16,$0d,$ff ; level 7
level_8_graphic_data:
.byte $03,$13,$19,$0e,$ff ; level 8
level_2_boss_graphic_data:
.byte $03,$04,$13,$08,$ff ; level 2 boss room
level_4_boss_graphic_data:
.byte $03,$04,$13,$08,$09,$ff ; level 4 boss room
intro_graphic_data_00:
.byte $01,$ff ; intro palette
; graphic_data_01, graphic_data_02
; #$01 - intro screen, level title screens, and game over screens pattern table tiles
; #$02 - game and level intro screen nametable data
intro_graphic_data_01:
.byte $01,$02,$ff ; intro pattern table tiles, and intro nametable
ending_graphic_data:
.byte $01,$03,$17,$18,$ff ; ending scene
; tables for graphic data data pointers (#$1b * #$03 = $51 bytes)
; contains nametable and pattern table data
; CPU address $c950
; first 2 bytes are the memory address to load
; last byte specifies 2 things
; * bits 0-3 specify the rom bank to have loaded
; the exception is 0 means bank 7 instead of bank 0)
; * bit 7 is stored in $04, when set it means all tiles from
; the graphic data must be flipped horizontally
graphic_data_ptr_tbl:
; reset both PPU nametables to zeros
; bank 7 (not bank 0)
.addr blank_nametables ; CPU address $cb36
.byte $00 ; bank 7 not bank 0
; bank 4 - intro, level title, game over screen pattern table
.addr graphic_data_01 ; CPU address $c953
.byte $04 ; bank where data located
; bank 2 - intro screen nametable
.addr graphic_data_02
.byte $02 ; bank where data located
; bank 4 - character, medals, power-ups, explosions
.addr graphic_data_03
.byte $04 ; bank where data located
; bank 4 - character facing up
.addr graphic_data_04
.byte $04 ; bank where data located
; bank 5 - level 1 bridge, mountain, and water tiles
.addr graphic_data_05 ; CPU address $8001
.byte $05 ; bank where data located
; bank 4 - most Base graphics
.addr graphic_data_06
.byte $04 ; bank where data located
; bank 5
.addr graphic_data_07
.byte $05 ; bank where data located
; bank 4
.addr graphic_data_08
.byte $04 ; bank where data located
; bank 4
.addr graphic_data_09
.byte $04 ; bank where data located
; bank 4
.addr graphic_data_0a
.byte $04 ; bank where data located
; bank 5
.addr graphic_data_0b
.byte $05 ; bank where data located
; bank 6
.addr graphic_data_0c
.byte $06 ; bank where data located
; bank 6
.addr graphic_data_0d
.byte $06 ; bank where data located
; bank 6
.addr graphic_data_0e
.byte $06 ; bank where data located
; bank 4
.addr graphic_data_0f
.byte $04 ; bank where data located
; bank 4
; horizontal flip
.addr graphic_data_10
.byte $84 ; bank where data located (with horizontal flip)
; bank 4
.addr graphic_data_11
.byte $04 ; bank where data located
; bank 4 - Base 2 Graphics
.addr graphic_data_12
.byte $04 ; bank where data located
; bank 4 - player top-half aiming up and aiming straight, also contains the laser sprites
.addr graphic_data_13 ; CPU address $87a1
.byte $04 ; bank where data located
; bank 5 - rotating gun and red turret
.addr graphic_data_14 ; CPU address $a814
.byte $05 ; bank where data located
; bank 6
.addr graphic_data_15
.byte $06 ; bank where data located
; bank 6 - weapon box
.addr graphic_data_16 ; CPU address $b15c
.byte $06 ; bank where data located
; bank 5
.addr graphic_data_17
.byte $05 ; bank where data located
; bank 5
.addr graphic_data_18
.byte $05 ; bank where data located
; bank 5 - player killed sprite tiles: recoil from hit and lying on ground
.addr graphic_data_19 ; CPU address $a31b
.byte $05 ; bank where data located
; bank 5 - soldier pattern table tiles
.addr graphic_data_1a ; CPU address $a500
.byte $05 ; bank where data located
; CPU address $c9a2
zero_out_nametables:
lda #$00
; loads and decompresses the entire graphic data specified by the A register as offset into graphic_data_ptr_tbl
write_graphic_data_to_ppu:
sta $04 ; use $04 as a temp location so we can triple a
asl ; double a data
adc $04 ; add $04 data to a (the previous 3 lines are simply multiplying by 3)
; lookup table is 3 bytes each, so multiply by 3 to get offset
tax
lda graphic_data_ptr_tbl,x
sta $00 ; store graphic address low byte in $00
lda graphic_data_ptr_tbl+1,x
sta $01 ; store graphic address high byte in $01
lda graphic_data_ptr_tbl+2,x ; load byte that stores bank number as well as whether or not to horizontally flip
and #$80 ; %1000 0000 (check the msb), if msb is 1, then block will be flipped horizontally
sta $04 ; store whether or not flip the entire graphic data horizontally into $04
lda graphic_data_ptr_tbl+2,x ; re-read the bank number to load the graphics from
and #$07 ; clear out the flip horizontal bit (only care about the last 3 bits which specify the bank number)
tay ; store the bank number where data is in Y
jsr load_bank_number ; store current bank in $07ec and load new bank
jsr clear_ppu ; resets A to #$00 as well
sta GRAPHICS_BUFFER_OFFSET ; set cpu graphics buffer offset
sta VERTICAL_SCROLL ; set vertical scroll offset to 0
sta HORIZONTAL_SCROLL ; set horizontal scroll offset to 0
; reads 2-bytes of memory starting at address $00,$01, which is a specific graphic_data
; and sets the PPUADDR to that address
; then begins decompressing the graphic data and starts writing to PPU
begin_ppu_graphics_block_write:
lda PPUSTATUS ; reset PPU latch to prep for writing
ldy #$01
lda ($00),y ; read high byte from ROM location specified by $00 and $01
sta PPUADDR ; set high byte of PPU address location to write to
dey ; move to low byte
lda ($00),y ; read low byte from ROM location specified by $00 and $01
sta PPUADDR ; set low byte of PPU address location to write to
lda #$02 ; a = #$02
ldx $04 ; load horizontal flip bit
bpl init_graphic_data_index ; if set, then skip doubling
asl ; double a to be #$04 to skip 4 bytes
; increment the next address to use for the PPU
init_graphic_data_index:
ldx #$00
jsr advance_graphic_read_addr ; advance past the PPU address and start reading graphic data data
; reads the next graphics byte compression sequence and writes it to the PPU
; multiple times depending on the number of repetitions specified
write_graphic_data_sequences_to_ppu:
ldy #$00 ; set offset so next line reads number of repetitions
lda ($00),y ; read the byte of the graphic data
cmp #$ff ; see if we are at the end of the graphic data
beq end_graphic_code ; loaded entire graphics data, restore previously loaded bank, re-init PPU
cmp #$7f ; used to specify the PPU write address should change to the address specified in the next 2 bytes)
beq change_ppu_write_address
tay ; store number of repetitions in Y
bpl write_graphic_byte_a_times ; if msb of graphic byte is 1 (> #$7f), write the following byte multiple times
and #$7f ; clear bit 7
sta $02 ; store positive portion in $02
ldy #$01 ; prepare to read next n graphic bytes
; writes the next n bytes of the graphic compression sequence to the PPU, starting at offset Y
; where n is the value in $02
write_next_n_sequence_bytes:
lda ($00),y ; read graphic byte
ldx $04 ; load graphic data horizontal flip bit value
bpl write_repetition_sequence_byte ; if not flipping graphic byte, go to write_repetition_sequence_byte
jsr horizontal_flip_graphic_byte
; writes value of a to the PPU
; then determines if done with n bytes of graphic data
write_repetition_sequence_byte:
sta PPUDATA ; write graphic byte to PPU
cpy $02 ; see if Y has incremented to the positive portion of repetition byte
beq advance_graphic_read_addr_n_bytes ; advance offset past already-written bytes
iny ; have not written $02 times, repeat
bne write_next_n_sequence_bytes ; loop $02 times
; advances the address of the current graphic byte offset by n bytes
; where n is the value in $02 + #$1
advance_graphic_read_addr_n_bytes:
lda #$01
clc ; clear carry in preparation for addition
adc $02 ; set A to the number of bytes to skip (they've already been written to the PPU)
advance_ppu_write_addr:
ldx #$00
jsr advance_graphic_read_addr
jmp write_graphic_data_sequences_to_ppu
; write the next graphic byte to the PPU A times ($02)
write_graphic_byte_a_times:
ldy #$01 ; offset to read graphic data byte
sta $02 ; store number of repetitions of graphic byte to $02
lda ($00),y ; load the graphic byte into a
ldy $02 ; load the number of repetitions into y
ldx $04 ; load whether or not to flip the graphic horizontally into x
bpl write_a_to_ppu_y_times ; write graphic byte to PPU Y times no horizontal flip needed
jsr horizontal_flip_graphic_byte ; flip graphics byte horizontally
; writes the value of a to PPU repeatedly y times
write_a_to_ppu_y_times:
sta PPUDATA ; write PPU value address to PPU
dey ; decrement counter
bne write_a_to_ppu_y_times ; RLE loop Y times
lda #$02 ; prepare to read and write next graphic data byte
bne advance_ppu_write_addr ; continue updating PPU with data
; horizontal flip routine
; swap bit 0 with 7, bit 1 with 6, bit 2 with 5, and bit 3 with 4
horizontal_flip_graphic_byte:
sta $03
asl $03
ror a
asl $03
ror a
asl $03
ror a
asl $03
ror a
asl $03
ror a
asl $03
ror a
asl $03
ror a
asl $03
ror a
rts
; changes the PPU address where the graphic data bytes are written to
change_ppu_write_address:
lda #$01 ; load the low byte of second nametable address (#$00)
ldx #$00 ; load the high byte of the second nametable address (#$24)
jsr advance_graphic_read_addr
jmp begin_ppu_graphics_block_write ; start populating nametable with zeros
; handle when entire graphic data code has been read
; restore previously-loaded bank and re-init the PPU
end_graphic_code:
jsr load_previous_bank
jmp configure_PPU
; input
; * $1e - player index, #$00 = 1 player, #$01 = 2 player
; specifies REST text location, and which scores to load
draw_player_num_lives:
lda #$1e ; a = #$1e
sta $02 ; !(HUH) unused variable $02 set to #$1e
lda #$07 ; a = #$07 (text_rest)
clc ; clear carry in preparation for addition
adc $1e ; add #$00 or #$01 to use either text_rest (higher location) or text_rest2 (lower location)
jsr load_bank_6_write_text_palette_to_mem ; draw text string (REST)
ldx $1e ; #$00 = player 1, #$01 = player 2
lda P1_GAME_OVER_STATUS,x ; load game over state for player x (1 = game over)
eor #$01 ; flip bit 0 (adds #$01 to remaining lives when not in game over)
clc ; clear carry in preparation for addition
adc P1_NUM_LIVES,x ; add the remaining player lives
beq draw_game_over_tex ; draw game over if no lives remaining
; (for 2 player game when one of 2 players is game over)
bpl @continue ; continue if more than #$00 lives remaining
lda #$00 ; a = #$00 (remaining lives is negative, not sure if possible) !(WHY?)
@continue:
ldx #$00 ; initialize 10s calculation 'multiplier' to #$00
; draws number of lives remaining left to right
; 10s digit is calculated in x by repeatedly subtracting 10
; max number of lives able to printed is #$63 (99)
@draw_num_lives:
sta $00 ; store updated number of lives in $00
cmp #$0a ; see if new number of lives is greater than 10 (more than one digit)
bcc @convert_digits ; branch if new amount is less than 10 lives to just draw the digit
sbc #$0a ; subtract 10 from remaining lives to draw 10s digit
inx ; increment 10s digit
cpx #$0a ; see if 10 loops have happened, i.e. 10s digit more than 9
bcc @draw_num_lives ; continue subtracting 10 if number is larger than 10
ldx #$09 ; score 10s digit was larger than 9, reset it to 9, i.e. largest number to draw is 99
txa ; transfer 10s score to a
; input
; * a - ones digit
; * x - 10s digit
@convert_digits:
ldy GRAPHICS_BUFFER_OFFSET ; load the cpu graphics buffer offset
ora #$30 ; converting the ones digit to pattern tile offset
; #$31 = 1, #$32 = 2, etc.
cpx #$00 ; see if 10s digit is 0
bne @write_num_lives ; branch to write 0 and exit if num lives is 0
cmp #$30 ; 10s digit non-zero, see if
beq @exit ; exit if just drew a 0, meaning no more digits to draw
@write_num_lives:
sta $06fe,y ; set ones digit
txa ; transfer 10s digit to a
beq @exit ; exit without drawing 10s digit if #$00
ora #$30 ; converting number to pattern tile offset
; #$31 = 1, #$32 = 2, etc.
sta $06fd,y ; set 10s digit of num lives
@exit:
rts
draw_game_over_tex:
txa
clc ; clear carry in preparation for addition
adc #$0f
jmp load_bank_6_write_text_palette_to_mem ; draw text string
; draw text "stage " and the level's name
draw_stage_and_level_name:
lda #$0c ; a = $0c (text string "stage ")
jsr load_bank_6_write_text_palette_to_mem ; draw text string
lda CURRENT_LEVEL ; current level number
clc ; clear carry in preparation for addition
adc #$31
sta $06fe,x ; draw current level
lda CURRENT_LEVEL ; current level
adc #$11 ; string id = level number + 11
jmp load_bank_6_write_text_palette_to_mem ; draw text string
; draw the scores
draw_the_scores:
lda #$09 ; a = #$09 (text string "hi")
jsr load_bank_6_write_text_palette_to_mem ; draw text string
lda HIGH_SCORE_LOW ; high score (low byte)
sta $00
lda HIGH_SCORE_HIGH ; high score (high byte)
sta $01
jsr @draw_score
lda #$0a ; a = $0a (text string "1p")
jsr load_bank_6_write_text_palette_to_mem ; draw text string
lda PLAYER_1_SCORE_LOW ; player 1 score (low byte)
sta $00
lda PLAYER_1_SCORE_HIGH ; player 1 score (high byte)
sta $01
jsr @draw_score
lda PLAYER_MODE ; single player vs two player ($00 = 1 player)
beq @exit ; don't draw player 2 if no player 2
lda #$0b ; a = #$0b (text string "2p")
jsr load_bank_6_write_text_palette_to_mem ; draw text string
lda PLAYER_2_SCORE_LOW ; player 2 score (low byte)
sta $00
lda PLAYER_2_SCORE_HIGH ; player 2 score (high byte)
sta $01
; prints the #$02-byte score stored in $01 and $02 on the screen
; $caf8
@draw_score:
lda FRAME_COUNTER ; load frame counter
and #$10 ; only interested in 5th bit
bne @exit ; don't print score if frame counter low (used for flashing effect)
lda #$05 ; high score has a maximum of #$05 digits to print
sta $04 ; store maximum number of digits into $04
; digits are calculated from right to left, then two 0s are tacked to end (unless the score is 0)
@draw_score_digit:
lda #$0a ; ensuring the high score digits is below decimal 10
sta $03 ; store $0a into $03
jsr calculate_score_digit ; calculate next digit and store into $02
lda $02 ; load the digit of the score to display from $02
ora #$30 ; convert from number to character to display. In Contra 30 = 0, 31 = 1, etc.
sta $06fc,x ; draw digit
dex ; move to the previous digit (score drawn right to left)
lda $00 ; load current low byte of score
ora $01 ; combine low byte with high byte
beq @draw_score_end_zeros ; if both high and low bytes are 0, printing of the score is finished, add two 0s to end
dec $04 ; decrement the digit counter, only #$05 decimal digits are used
bne @draw_score_digit ; draw the next digit if less than #05 digits have been drawn
@draw_score_end_zeros:
ldx GRAPHICS_BUFFER_OFFSET ; index int PPU character map
lda $04 ; load the number of digits remaining (starts at $05)
sec ; set carry flag in preparation for subtraction
sbc #$05 ; subtract 5 from total digits used, used to know if no digits have been printed
ora $02 ; see if score is #$00
beq @draw_zero_score ; handle when the score is #$00 (don't tack on ending 00s)
lda #$30 ; set the character to display to 0
sta $06fd,x ; display the character '0'
@draw_final_0_exit:
sta $06fe,x ; display the character '0'
@exit:
rts
; handle the case when player score is $00, i.e. no points were scored
@draw_zero_score:
sta $06fc,x ; this is the first decimal digit of the score, set it to '0'
lda #$30 ; display the character '0'
bne @draw_final_0_exit ; always branch, to exit out of @draw_score_digit
; graphic data to reset nametables (#$2a bytes)
; set nametable 0 ($2000) and nametable 1 ($2400) all to #$00
; #$400 bytes (1 KiB) each
; first #$2 bytes are the nametable address ($2000)
; then repeatedly write #$78 zeros, then finally #$40 zeros.
; #$7f signifies keep reading to clear the $2400 nametable
; nametable data - writes addresses [$2000-$2800)
; CPU address $cb36
graphic_data_00:
blank_nametables:
.byte $00,$20,$78,$00,$78,$00,$78,$00,$78,$00,$78,$00,$78,$00,$78,$00
.byte $78,$00,$40,$00,$7f
; first #$2 bytes are the nametable address (#$2400)
blank_nametable_2:
.byte $00,$24,$78,$00,$78,$00,$78,$00,$78,$00,$78,$00,$78,$00,$78,$00
.byte $78,$00,$40,$00,$ff
; write pattern tile (text) or palette information (color) to CPU offset CPU_GRAPHICS_BUFFER
; this is used when GRAPHICS_BUFFER_MODE is #$00, which defines the CPU_GRAPHICS_BUFFER format for text and palette data
; input
; * a - first six bits are index into the short_text_pointer_table
; when bit 7 set, write all blank characters instead of actual characters. Used for flashing effect
write_text_palette_to_mem:
pha ; push a to stack (index into short_text_pointer_table)
lda #$02 ; a = #$02
sta $03 ; used when bit 7 of a is set, indicating to clear text
; since the first #$02 bytes are PPU address,
; $03 is used to prevent overwriting PPU address with #$00
lda #$01 ; a = #$01 (next #$02 bytes are PPU address)
jsr write_a_to_cpu_graphics_buffer ; write #$01 to CPU_GRAPHICS_BUFFER,x
pla ; restore the index into short_text_pointer_table to print
sta $02 ; store index into short_text_pointer_table $02
asl ; double index, since short_text_pointer_table is 2 bytes per label address
tax ; transfer index to x register
lda short_text_pointer_table,x ; read the low-byte memory address from pointer table
sta $00 ; store low byte of short_text_pointer_table address
lda short_text_pointer_table+1,x ; high byte of pointer table
sta $01 ; store high byte of short_text_pointer_table address
ldx GRAPHICS_BUFFER_OFFSET ; set X to store the next offset of CPU_GRAPHICS_BUFFER
ldy #$00 ; initialize character offset into string to 0
; read until #fe, #$fd, or #$ff and store CPU memory starting at CPU_GRAPHICS_BUFFER
@write_char_to_cpu_mem:
lda ($00),y ; read character from string
iny ; increment character offset
cmp #$ff ; #$ff signifies end of string, the #$ff isn't stored in CPU memory
beq set_x_to_offset_exit ; if #ff, the string has been completely loaded, restore x to GRAPHICS_BUFFER_OFFSET and exit
cmp #$fe ; like #$ff, #$fe is end of string, but #fe causes #$ff to be stored in CPU memory at end of string
beq @write_ff_to_cpu_memory ; store #$ff in CPU memory at the end of the string
cmp #$fd ; #fd specifies next two bytes are the PPU address, i.e. changing location on screen
beq @handle_fd ; branch if next two bytes are a new PPU address
sta CPU_GRAPHICS_BUFFER,x ; store character in CPU graphics buffer
lda $02 ; load the index into text string table into A, i.e. which string to print
bpl @next_char ; branch if text ins't being blanked (part of flashing animation)
lda $03 ; not writing characters, writing blanks to hide text
; see if already written PPU address (#$02 bytes)
bne @dec_blank_delay ; branch if writing PPU address to CPU graphics buffer
sta CPU_GRAPHICS_BUFFER,x ; write #$00 character to cpu memory to blank the text for flashing animation
beq @next_char ; continue to next character to read
@dec_blank_delay:
dec $03 ; decrement delay to allow writing PPU address before writing all #$00s for text
@next_char:
inx ; move to next cpu graphics buffer write offset
bne @write_char_to_cpu_mem ; loop to read next character
; #$fe encountered (end of string)
@write_ff_to_cpu_memory:
lda #$ff ; set the zero flag so next line jumps and #$ff is stored in CPU memory
bne write_to_700_offset ; always branch to write #$ff to CPU graphics buffer and exit
; read next two bytes for PPU address
; used to write both CONTINUE and END together
@handle_fd:
lda #$ff ; #$ff tells CPU graphics buffer reading logic to prepare to read next segment of text
jsr write_to_700_offset ; write #$ff to cpu buffer
lda #$02 ; set to #$02 to skip blanking (zeroing) the PPU address when writing to CPU memory
sta $03 ; reset delay before blanking text (flashing animation)
lda #$01 ; vram_address_increment offset (#$01 = write across)
jsr write_to_700_offset ; write vram_address_increment offset
bne @write_char_to_cpu_mem ; branch to write next PPU address and string of text
lda #$ff ; not sure when this would execute !(WHY?)
bne write_a_to_cpu_graphics_buffer
write_0_to_cpu_graphics_buffer:
lda #$00
beq write_a_to_cpu_graphics_buffer ; always jumps since lda loads #$0, also it's the next line of code !(HUH)
write_a_to_cpu_graphics_buffer:
ldx GRAPHICS_BUFFER_OFFSET ; update X to next location to write to in CPU_GRAPHICS_BUFFER offset
; store A into string memory
write_to_700_offset:
sta CPU_GRAPHICS_BUFFER,x
inx
set_x_to_offset_exit:
stx GRAPHICS_BUFFER_OFFSET
vram_address_increment:
rts
; table for PPU VRAM address increment (#$03 bytes)
; used to set whether the the VRAM address increments by 1 (across) or #$20 (down)
.byte $00,$04,$00
; writes the graphics data loaded in CPU_GRAPHICS_BUFFER to the PPU for drawing
write_cpu_graphics_buffer_to_ppu:
lda GRAPHICS_BUFFER_MODE ; load flag to determine if we should write the CPU_GRAPHICS_BUFFER to the PPU
bne @flush_cpu_graphics_buffer ; branch when non-zero. write CPU_GRAPHICS_BUFFER to the PPU
ldy #$00 ; GRAPHICS_BUFFER_MODE is #$00, zero out $08
sty $08 ; clear $08 address override flag so graphics buffer can be read
@read_cpu_mem_to_ppu:
lda $08 ; read previous high byte of PPU write address
cmp #$3f ; compare $08 to #$3f
bne @continue ; skip ahead if $08 is not equal to #$3f
sta PPUADDR ; !(WHY?) the following code doesn't make sense to me
lda #$00 ; it sets the PPUADDR to #$3f00, then #$0000
sta PPUADDR ; but this is overwritten in the next few lines
sta PPUADDR ; that would have been executed regardless of what $08 was
sta PPUADDR ; this code seems to be able to be removed without issue !(WHY?)
; CPU address $cbe5
@continue:
ldx CPU_GRAPHICS_BUFFER,y ; read byte #$00 (used to set PPUCTRL)
beq @reset_graphics_buffer ; nothing to draw, exit
lda PPUCTRL_SETTINGS ; load PPUCTRL settings
and #$18 ; %0001 1000
ora vram_address_increment,x ; include the VRAM increment offset
sta PPUCTRL ; set background and sprite pattern table addresses
iny
lda PPUSTATUS ; clear bit 7 and address latch used by PPUSCROLL and PPUADDR
lda CPU_GRAPHICS_BUFFER,y
sta $08 ; store high byte of PPU data write address into $08
sta PPUADDR ; store high byte of PPU address
iny ; increment offset into CPU memory
lda CPU_GRAPHICS_BUFFER,y ; read next byte from CPU_GRAPHICS_BUFFER CPU memory address
sta PPUADDR ; store low byte of PPU data write address
iny ; increment CPU_GRAPHICS_BUFFER CPU read offset
cpx #$03 ; if vram_address_increment offset is 3
bne @flush_graphics_buffer ; write graphics data until #$ff
lda CPU_GRAPHICS_BUFFER,y ; 0.3 mode - read total number of tiles/bytes to write to PPU
sta $09 ; store value in $09
; writes a block of bytes to PPU
@loop:
iny ; increment CPU_GRAPHICS_BUFFER read offset
lda CPU_GRAPHICS_BUFFER,y ; read graphic byte
sta PPUDATA ; store in PPU
dec $09 ; decrement total number of tiles/bytes to write
bne @loop ; loop if more tiles to write
; sets first byte of CPU_GRAPHICS_BUFFER to #$00 so no drawing takes place for frame
@reset_graphics_buffer:
lda #$00
sta CPU_GRAPHICS_BUFFER ; set initial byte to 0
sta GRAPHICS_BUFFER_OFFSET ; set PPU pattern table offset to 0
lda PPUCTRL_SETTINGS ; saved PPUCTRL settings (see configure_PPU)
sta PPUCTRL ; configure pattern table address, and sprite size
rts
@write_ff_to_ppu:
lda #$ff ; a = $ff
@write_byte_to_ppu:
sta PPUDATA
; write to PPU until #$ff is encountered
@flush_graphics_buffer:
lda CPU_GRAPHICS_BUFFER,y ; read next byte to write to PPU
iny ; increment CPU_GRAPHICS_BUFFER read offset
cmp #$ff ; compare to end of data byte #$ff
bne @write_byte_to_ppu ; if not #$ff, then write to PPU
lda CPU_GRAPHICS_BUFFER,y ; byte was #$ff, see what next graphic byte is
cmp #$04 ; compare graphic byte to #$04
bcs @write_ff_to_ppu ; branch if byte is greater than or equal to #$04
bcc @read_cpu_mem_to_ppu ; continue writing CPU_GRAPHICS_BUFFER (0 mode or nonzero mode)
; writes the entire graphics buffer to the PPU
; GRAPHICS_BUFFER_MODE nonzero mode
@flush_cpu_graphics_buffer:
ldx #$00 ; reset CPU_GRAPHICS_BUFFER read offset back to beginning
lda PPUCTRL_SETTINGS
and #$18 ; only care about nametable address and sprite pattern table address
sta $02 ; store in $02 for future use in PPUCTRL
; every loop of this prints a few more columns of nametable
; every loop CPU_GRAPHICS_BUFFER is different
@write_to_ppu:
ldy CPU_GRAPHICS_BUFFER,x ; read first byte $700
beq @reset_graphics_buffer ; if #$00, done - clear CPU_GRAPHICS_BUFFER
lda $02
ora vram_address_increment,y
sta PPUCTRL
inx
lda CPU_GRAPHICS_BUFFER,x ; read first byte - length of data to write
sta $00 ; store length of data to write in group in $00
inx ; increment CPU_GRAPHICS_BUFFER read offset
lda CPU_GRAPHICS_BUFFER,x ; read second byte - number of byte blocks to write
sta $01 ; set the number of blocks of memory to write
; set the PPU write address based on the CPU_GRAPHICS_BUFFER
@set_PPU_write_address:
ldy $00
inx
lda CPU_GRAPHICS_BUFFER,x
sta PPUADDR
inx
lda CPU_GRAPHICS_BUFFER,x
sta PPUADDR
; loop through CPU_GRAPHICS_BUFFER block and write all bytes to PPU
@write_loop:
inx ; increment cpu read offset
lda CPU_GRAPHICS_BUFFER,x ; read graphics buffer byte
sta PPUDATA ; write byte to PPU
dey ; decrement PPU write byte count
bne @write_loop ; loop if not yet written entire block of bytes
dec $01 ; decrement number of byte blocks counter
bne @set_PPU_write_address ; if more byte blocks to write, loop to read PPU write address
inx ; no more data to write, increment cpu read offset
bne @write_to_ppu ; jump to see if another set of data to write, never fall through to line of code below
; writes the palette data in cpu memory PALETTE_CPU_BUFFER to the ppu
; in the $3f00 ppu address range (palette data)
write_palette_colors_to_ppu:
ldx NUM_PALETTES_TO_LOAD ; load the number of palettes to write to PPU memory
beq graphics_loading_exit ; exit if #$00
lda GRAPHICS_BUFFER_OFFSET
cmp #$30
bcs graphics_loading_exit ; exit if the offset is >= #$30
lda PPUCTRL_SETTINGS
and #$18 ; keep ...x x...
sta PPUCTRL ; ensure sprite and background pattern tables are configured correctly
lda #$3f ; set PPU write address to $3f00 (palette address range)
sta PPUADDR
lda #$00
sta PPUADDR
tay
; now write the palette data in the cpu buffer PALETTE_CPU_BUFFER
@loop:
lda PALETTE_CPU_BUFFER,y ; load the current palette
sta PPUDATA
iny
dex
bne @loop
lda #$3f
sta PPUADDR ; set ppu write address to $3f00
stx PPUADDR ; store low byte of $3f00
stx PPUADDR ; be sure it wrote correctly
stx PPUADDR ; be sure it wrote correctly
stx NUM_PALETTES_TO_LOAD ; set number of palettes to load to #$3f
; don't think this value is ever read when it's #$3f, overwritten later
graphics_loading_exit:
rts
; load alternate tiles if necessary
alternate_tile_loading:
lda ALT_GRAPHIC_DATA_LOADING_FLAG ; whether or not to start loading alternate graphics
beq graphics_loading_exit ; if not loading alternate graphics, then exit
bmi set_alt_graphics_cpu_buffer ; already initialized alt graphics loading, continue loading. ALT_GRAPHIC_DATA_LOADING_FLAG is $80
lda CURRENT_LEVEL ; load current level
asl
asl
adc CURRENT_LEVEL ; level = level * #$05 (each entry is #$05 bytes)
tay
lda alt_graphic_data_ptr_tbl,y ; load PPU write address low byte
sta $6c
lda alt_graphic_data_ptr_tbl+1,y ; load PPU write address high byte
sta $6d
lda alt_graphic_data_ptr_tbl+2,y ; load graphics data read location low byte
sta $6e
lda alt_graphic_data_ptr_tbl+3,y ; load graphics data read location high byte
sta $6f
lda alt_graphic_data_ptr_tbl+4,y ; load the number of tiles to change
beq alt_graphics_loading_complete ; if #$00 no data to change, so mark ALT_GRAPHIC_DATA_LOADING_FLAG as #$00 and exit
sta $70
lda #$80
sta ALT_GRAPHIC_DATA_LOADING_FLAG ; set flag to specify alternate graphics are currently loading
set_alt_graphics_cpu_buffer:
ldx GRAPHICS_BUFFER_OFFSET
cpx #$10
bcs graphics_loading_exit
lda #$01 ; a = #$01
sta CPU_GRAPHICS_BUFFER,x
sta CPU_GRAPHICS_BUFFER+2,x
inx
lda #$20 ; a = #$20
sta CPU_GRAPHICS_BUFFER,x
inx
inx
lda $6d ; PPU write address high byte
sta CPU_GRAPHICS_BUFFER,x
inx
lda $6c ; PPU write address low byte
sta CPU_GRAPHICS_BUFFER,x
inx
ldy #$00 ; y = #$00
@loop:
lda ($6e),y
sta CPU_GRAPHICS_BUFFER,x
iny
inx
cpy #$20
bne @loop
stx GRAPHICS_BUFFER_OFFSET
dec $70
beq alt_graphics_loading_complete
lda #$20 ; a = #$20
ldx #$6e ; x = #$6e
jsr advance_graphic_read_addr ; advance address 6e-6f by 20 bytes
lda #$20 ; a = #$20
ldx #$6c ; x = #$6c
jmp advance_graphic_read_addr ; advance address 6c-6d by 20 bytes
alt_graphics_loading_complete:
lda #$00 ; a = #$00
sta ALT_GRAPHIC_DATA_LOADING_FLAG ; mark flag as #$00 to specify no more alternate graphics data to load
rts
; alternate graphics table, $08 entries each entry is $05 bytes long
; bank 2
; Bytes 0-1: PPU address
; Bytes 2-3: CPU address
; Byte 4 : Number of Tiles to Change (x2)
alt_graphic_data_ptr_tbl:
.byte $80,$1a
.addr alt_graphic_data_00
.byte $2c
.byte $00,$10
.addr alt_graphic_data_00
.byte $00
.byte $60,$14
.addr alt_graphic_data_01
.byte $5d
.byte $00,$10
.addr alt_graphic_data_00
.byte $00
.byte $a0,$16
.addr alt_graphic_data_02
.byte $1d
.byte $80,$0a
.addr alt_graphic_data_03
.byte $22
.byte $00,$10
.addr alt_graphic_data_00
.byte $00
.byte $60,$1b
.addr alt_graphic_data_04
.byte $25
; create #$04 new pattern table tiles starting at PPU address $1fc0
; it takes #$0f bytes per pattern table tile or #$40 bytes total
; it does this by taking a 'background' drawing of the tile, and then drawing electricity on top
animate_indoor_fence:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bmi @exit ; exit for indoor boss screen
lda INDOOR_SCREEN_CLEARED ; indoor screen cleared flag (0 = not cleared; 1 = cleared)
beq @draw_random_fence ; wall cores haven't been destroyed, draw random fence based on FRAME_COUNTER
lda #$80 ; screen cleared, don't draw fence, only floor, a = #$80
sta INDOOR_SCREEN_CLEARED ; flag (0 = not cleared; 1 = cleared, #$80 = cleared, fence removed)
lda #$20 ; a = #$20 (pattern_tile_fence_tbl offset for no fence)
bne @continue ; always branch
@draw_random_fence:
lda FRAME_COUNTER ; load frame counter
and #$03 ; random number [#$00-#$03]
bne @exit ; exit if not the 4th frame
lda FRAME_COUNTER ; change tiles every 4 frames
and #$0c ; grab either #$00, #$04, #$08, or #$0c
asl ; double to get #$00, #$08, #$10, or #$18 (offset into pattern_tile_fence_tbl)
@continue:
sta $14 ; set pattern_tile_fence_tbl offset
ldx GRAPHICS_BUFFER_OFFSET ; load graphics buffer offset
lda #$01 ; a = #$01
sta CPU_GRAPHICS_BUFFER,x ; set VRAM address increment to 0 (write across)
sta CPU_GRAPHICS_BUFFER+2,x ; set number of groups to #$01 group
inx ; increment graphics buffer write offset
lda #$40 ; a = #$40
sta CPU_GRAPHICS_BUFFER,x ; set to group size to #$40
inx ; increment graphics buffer write offset
inx ; increment graphics buffer write offset
lda #$1f ; a = #$1f
sta CPU_GRAPHICS_BUFFER,x ; set ppu address high byte to #$1f
inx ; increment graphics buffer write offset
lda #$c0 ; a = #$c0
sta CPU_GRAPHICS_BUFFER,x ; set ppu address low byte to #$c0
; last #$04 bytes of pattern table 1 and then #$3c pattern tiles (PPU address $1fc0)
inx ; increment graphics buffer write offset
lda pattern_tile_bg_tbl ; load low byte of pattern_tile_bg_00 address
sta $10 ; store low byte of pattern_tile_bg_00 address
lda pattern_tile_bg_tbl+1 ; load high byte of pattern_tile_bg_00 address
sta $11 ; store high byte of pattern_tile_bg_00 address
lda #$07 ; a = #$07
sta $13 ; set base address to CPU_GRAPHICS_BUFFER ($0700)
stx $12 ; store graphics buffer write offset
; use ($12),y in @write_to_graphics_buffer instead of CPU_GRAPHICS_BUFFER,x
; because x will be used for something else
ldx $14 ; load pattern_tile_fence_tbl offset
ldy #$00 ; y = #$00
@write_to_graphics_buffer:
lda ($10),y ; load pattern_tile_bg_00 byte
; this is the 'background' portion of the pattern tile with no electricity
ora pattern_tile_fence_tbl,x ; merge with the electricity part of the drawing
sta ($12),y ; set graphics buffer byte
inx ; increment pattern_tile_fence_tbl offset
txa
and #$07 ; keep bits .... .xxx
ora $14
tax
iny ; increment graphics buffer write offset used in this loop ($12),y
cpy #$40 ; see if written all #$40 pattern tile bytes (#$04 pattern tiles) to the graphics buffer
bcc @write_to_graphics_buffer ; branch if more tiles to write
tya
clc ; clear carry in preparation for addition
adc $12
sta GRAPHICS_BUFFER_OFFSET
@exit:
rts
; pointer for the table directly below
; !(OBS) no need for a single entry table
pattern_tile_bg_tbl:
.addr pattern_tile_bg_00
; table for pattern tile backgrounds that will then have the electric fence drawn on top of (#$40 bytes)
; each #$f bytes is a single pattern table tile
pattern_tile_bg_00:
.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$00,$00,$00,$00,$00,$00,$00,$00 ; solid square color 1
.byte $00,$00,$00,$00,$00,$00,$00,$00,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff ; solid square color 2
.byte $fe,$fc,$f8,$f0,$e0,$c0,$80,$00,$00,$01,$03,$07,$0f,$1f,$3f,$7f ; rising diagonal background (left side of fence)
.byte $7f,$3f,$1f,$0f,$07,$03,$01,$00,$00,$80,$c0,$e0,$f0,$f8,$fc,$fe ; falling diagonal background (right side of fence)
; table for electric fence line that is drawn on top of the backgrounds
; in pattern_tile_bg_00 to create the pattern table tiles for the electric fence (#$28 bytes)
; first #$04 entries are squiggly electric fence, last is blank (no fence)
pattern_tile_fence_tbl:
.byte $00,$00,$04,$44,$eb,$32,$20,$00
.byte $00,$00,$10,$30,$eb,$6a,$44,$00
.byte $00,$00,$08,$0c,$d7,$56,$22,$00
.byte $00,$00,$20,$22,$d7,$4c,$04,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00 ; no electric fence, just draw the ground/background part of the tile
run_level_routine_for_demo:
lda LEVEL_ROUTINE_INDEX ; load the offset into the instruction pointer table
cmp #$04
bne run_level_routine ; 4th subroutine is different, if not #$04, immediately go to run_level_routine
jsr simulate_input_for_demo ; if 4th subroutine (5th counting from 0), load bank 5 and run input simulation code
; game routines - pointer 5
; inside a level or in the demo. this routine runs the appropriate level_routine
game_routine_05:
lda LEVEL_ROUTINE_INDEX ; intro finished loading, start showing demo or actual game
; runs the code specified at offset A in the level_routine_ptr_tbl
; run for all levels, but not for intro. Intro loads from game_routine_pointer_table
run_level_routine:
jsr run_routine_from_tbl_below ; run routine a in the following table (level_routine_ptr_tbl)
; pointer table for main game (#$0b * #$02 = #$16 bytes)
; CPU address $ce35
level_routine_ptr_tbl:
.addr level_routine_00 ; CPU address $ce4b - init APU, zero nametables, load default palette, and load level headers
.addr level_routine_01 ; CPU address $ce7e - display number of lives text for player(s) - REST xx
.addr level_routine_02 ; CPU address $ce9b - flashes score until timer expires, loads the pattern data, sets the sprite palettes, starts level music
.addr level_routine_03 ; CPU address $ced8 - animate nametable drawing, set sprite load type
.addr level_routine_04 ; CPU address $cee3 - routine run repeatedly while playing level
.addr level_routine_05 ; CPU address $cf2e - initialize level after finishing level, or game over
.addr level_routine_06 ; CPU address $cf9d - no more lives screen - shows score and "continue"/"end" option
.addr level_routine_07 ; CPU address $cfe1 - show game over screen until player presses start
.addr level_routine_08 ; CPU address $cfea - check for game over, otherwise wait for delay and play end of level tune
.addr level_routine_09 ; CPU address $d01f - run end of level sequence routines
.addr level_routine_0a ; CPU address $d02e - show game over score until GAME_OVER_DELAY_TIMER elapses, then move to level_routine_05
; main game - pointer 0
; init APU, zero nametables, load default palette, and load level headers
level_routine_00:
jsr init_APU_channels
jsr zero_out_nametables ; reset both nametables to zeroes
lda #$06 ; load transition_screen_palettes (#$06th entry in short_text_pointer_table table bank 6)
jsr load_bank_6_write_text_palette_to_mem ; load palette for level name and hi score screen into PPU
ldy #$02
jsr load_bank_number ; switch to bank 2
lda CURRENT_LEVEL ; load current level
asl ; each level header is #$20 bytes so multiply by #20
asl
asl
asl
asl
tay ; y is CPU memory read offset
ldx #$00 ; x is CPU memory write offset
stx BOSS_DEFEATED_FLAG ; set flag to false
stx LEVEL_END_PLAYERS_ALIVE ; clear players alive after defeating boss heart flag
; ROM address $ce6a
; stores the $20 byte level header data in CPU memory in addresses $40 to $60
; the level header is a $20 byte data structure containing information about the
; level. All 8 levels have their headers stored consecutively starting at
; $b319 in bank 2
;
; For example, Level 1 contains the following information
; - $40 Location Type: Indoor ($00)
; - $41 Scrolling Type: Horizontal ($00)
; - $42 Level Screen Super-Tile Data Location : $8001 (Bank 2)
; - $44 Level Super-Tile Data Location: $8001 (Bank 3)
; - $46 Palette Data Location: $8671 (Bank 3)
; - $48 Alternate Graphics Loading Section: $0b (how far into level before loading alternate graphic data)
; - $49 Tile Collision Limits: $06 $f9 $ff
; - $4c Cycling Background Tile Palette Codes: $05 $08 $05 $08
; - $50 Background Tile Palette Codes: $02 $03 $04 $05
; - $54 Sprite Palette Codes: $00 $01 $22 $07
; - $58 Stop Scrolling Section: $0b
; - $59 Mystery Byte: $00
; - $5a-$5f Unused Bytes: $00 $00 $00 $00 $00 $00
load_level_header:
lda level_headers,y ; load level header offset y from bank 2
sta LEVEL_LOCATION_TYPE,x ; store into cpu addresses $40-$60
iny ; increment CPU memory read offset
inx ; increment CPU memory write offset
cpx #$20 ; level header is #$20 bytes, check if all bytes been read
bne load_level_header ; read next byte if not complete
jsr init_ppu_write_screen_supertiles ; initialize PPU write addresses (nametable, attribute table), scroll,
; and load super-tile indexes for current screen into LEVEL_SCREEN_SUPERTILES
jsr load_bank_0_load_level_enemies_to_mem ; load enemy routines bank (bank 0), and load level-specific enemies into $80
inc_level_routine_index:
inc LEVEL_ROUTINE_INDEX ; increment current level routine index
rts
; display number of lives text for player(s) - REST xx
; executed once so it doesn't flash like the score
level_routine_01:
lda DEMO_MODE ; #$00 not in demo mode, #$01 demo mode on
bne skip_level_routine_01 ; jump when in demo mode
jsr draw_stage_and_level_name ; draw "STAGE" string
lda #$00 ; a = #$00
sta $1e ; set PLAYER_MODE for use in draw_player_num_lives
jsr draw_player_num_lives ; draw player 1 number of lives (REST XX)
lda PLAYER_MODE ; number of players (0 = 1 player)
beq @continue ; branch if only one player number of lives to draw
sta $1e ; set player index to #$01, to draw number of lives for player 2
jsr draw_player_num_lives ; draw player 2 number of lives (REST XX)
@continue:
lda #$c0 ; set delay for score display screen for non-demo mode
skip_level_routine_01:
sta DELAY_TIME_LOW_BYTE ; setup score display delays (only low byte used)
bne inc_level_routine_index ; set $2a to 02 to skip score display screen
; flashes score until timer expires
; loads the pattern data for the level
; sets the sprite palettes
; starts level music
level_routine_02:
jsr decrement_delay_timer ; decrement timer (sets/clears zero flag)
bne draw_the_scores_1 ; jump if the timer has elapsed, i.e. finished flashing score, move on to load level
jsr zero_out_nametables ; the timer has elapsed, reset nametables (blank screen)
jsr load_level_graphics ; load level graphics
lda #$20 ; a = #$20
jsr load_palettes_color_to_cpu ; load #$20 palette colors into PALETTE_CPU_BUFFER based on LEVEL_PALETTE_INDEX
lda CURRENT_LEVEL ; current level
asl
tay
lda level_vert_scroll_and_song,y
sta VERTICAL_SCROLL ; set initial vertical scroll
lda DEMO_MODE ; #$00 not in demo mode, #$01 demo mode on
bne @continue ; don't play level music/song when in demo mode
lda level_vert_scroll_and_song+1,y
jsr play_sound ; play level background music
@continue:
lda #$ff ; a = #$ff
sta GRAPHICS_BUFFER_MODE ; non-zero graphics mode
inc LEVEL_ROUTINE_INDEX ; increment level routine
level_routine_03_exit:
rts
draw_the_scores_1:
jmp draw_the_scores
; table for vertical adjustment and music theme ($08 * $02 = $10 bytes)
; first byte is the vertical adjustment
; second byte is the music theme code
level_vert_scroll_and_song:
.byte $e0,$2a ; level 1 - show bottom nametables (#$2800 and #$2c00) - sound_2a
.byte $e8,$3e ; level 2 - show bottom nametables (offset top #$8 pixels) (#$2800 and #$2c00) - sound_3e
.byte $00,$2e ; level 3 - vertical scroll is variable throughout level - sound_2e
.byte $e8,$3e ; level 4 - show bottom nametables (offset top #$8 pixels) (#$2800 and #$2c00) - sound_3e
.byte $e0,$32 ; level 5 - show bottom nametables (#$2800 and #$2c00) - sound_32
.byte $e0,$36 ; level 6 - show bottom nametables (#$2800 and #$2c00) - sound_36
.byte $e0,$2a ; level 7 - show bottom nametables (#$2800 and #$2c00) - sound_2a
.byte $e0,$3a ; level 8 - show bottom nametables (#$2800 and #$2c00) - sound_3a
; animate nametable drawing, set sprite load type
level_routine_03:
jsr load_bank_3_init_lvl_nametable_animation ; load bank three and execute initial level nametable drawing animation
bne level_routine_03_exit ; exit if LEVEL_TRANSITION_TIMER has elapsed
lda #$ff ; a = #$ff
sta SPRITE_LOAD_TYPE ; set to load hud sprites
bne inc_level_routine_index ; increase level routine to level_routine_04
; CPU address $cee3
; routine run repeatedly while playing level
level_routine_04:
jsr check_for_pause ; see if player is pausing or un-pausing
lda PAUSE_STATE ; #$00 for un-paused #$01 for paused
bne level_routine_04_exit ; exit level routine if the game is paused
lda BOSS_DEFEATED_FLAG ; 0 = boss not defeated, 1 = boss defeated
bne set_to_level_routine_08 ; if boss defeated, skip to level_routine_08 to begin level over sequence
; level_routine_04 and level_routine_08 use this label
; checks to see if player(s) have game overed, if so, sets next level routine to
; be #$0a to begin game over sequence
; if not, then executes the various enemy logic
check_game_over_run_enemy_logic:
jsr set_frame_scroll_draw_player_bullets ; draw sprites, handle input
lda P1_GAME_OVER_STATUS ; player 1 game over state (1 = game over)
and P2_GAME_OVER_STATUS ; player 2 game over state (1 = game over)
bne init_game_over ; if both players have game over, show ending sequence
; run various enemy logic that exists in bank 0 and bank 7
; random enemies, all currently shown enemy logic, palette updates, etc.
; generate random soldiers if appropriate
; run in level_routine_04 and level_routine_0a
run_level_enemy_logic:
jsr load_bank_3_handle_scroll ; handles scrolling for the level if currently scrolling
; handles updating nametable, attribute table, and loading alternate graphics as appropriate
jsr load_bank_0_exe_all_enemy_routine ; execute all enemy routine logic
jsr load_bank_2_load_screen_enemy_data
jsr load_bank_2_exe_soldier_generation ; run soldier generation routine
jsr load_palette_indexes
jsr load_bank_2_alternate_tile_loading ; load alternate tiles if necessary
; also executed from level_routine_08
level_routine_04_exit:
rts
; initializes the game over timer (delay before showing score)
; goes to level routine #$0a
init_game_over:
lda #$60 ; set timer to delay showing scores
sta GAME_OVER_DELAY_TIMER ; initialize timer to #$60
lda #$0a
bne set_a_as_current_level_routine ; set next level routine to #$0a to wait for delay
set_to_level_routine_05:
lda #$00 ; a = #$00
sta BOSS_DEFEATED_FLAG ; reset flag to false now that GAME_OVER_DELAY_TIMER has elapsed
lda #$05 ; a = #05 (level_routine_05)
jsr set_a_as_current_level_routine ; change level routine to show high score
set_graphics_zero_mode:
lda #$00 ; a = #$00
sta GRAPHICS_BUFFER_MODE ;
sta CPU_GRAPHICS_BUFFER
jmp init_APU_channels
set_to_level_routine_08:
lda #$08 ; a = #$08
set_a_as_current_level_routine:
sta LEVEL_ROUTINE_INDEX ; set the next level routine to run
lda #$00 ; a = #$00
sta END_LEVEL_ROUTINE_INDEX ; clear end of level routine index
rts
; initialize level after finishing level, or game over
level_routine_05:
lda #$00 ; a = #$00
sta SPRITE_LOAD_TYPE ; set to load normal sprites
sta INDOOR_SCREEN_CLEARED ; indoor screen cleared flag (0 = not cleared; 1 = cleared)
lda P1_CURRENT_WEAPON
sta $10 ; temporarily store current weapon in $10
lda P2_CURRENT_WEAPON ; current weapon code (player 2)
sta $11 ; temporarily store current weapon in $11
ldx #$40 ; x = #$40 (set to 30 for game over after lvl 1)
jsr clear_memory_starting_a_x ; clear level header data, player data, sprite buffer, and super-tile buffer
lda BOSS_DEFEATED_FLAG ; 0 = boss not defeated, 1 = boss defeated
beq show_game_over_screen ; in level_routine_05 and boss wasn't defeated, game over
; unless demo mode (shouldn't happen because demos don't reach end of level), then just set DEMO_LEVEL_END_FLAG and exit
lda $10 ; restore current weapon from $10 for player 1
sta P1_CURRENT_WEAPON
lda $11 ; restore current weapon from $11 for player 2
sta P2_CURRENT_WEAPON ; current weapon code (player 2)
inc CURRENT_LEVEL ; increment current level
lda CURRENT_LEVEL ; current level
cmp #$08 ; if greater than last level, start game ending sequence
bcc load_level_intro ; jump if level is less than the last level
jsr inc_routine_index_set_timer ; completed last level, start ending sequence
inc GAME_COMPLETION_COUNT ; increment game completion count, used mainly to increase enemy difficulty every play-through
lda #$09
sta CURRENT_LEVEL ; set current level to #$09, this is interpreted as the ending sequence
bne level_routine_05_exit ; always jump since lda #$09 will set the Z flag #$00
; loads the pattern table tiles to the level intro screen
; shows player scores, number of lives, high score, stage number, and level name
load_level_intro:
lda #$0a ; set offset to point to intro_graphic_data_00 -> graphic_data_01 -> (pattern table for screen)
jsr load_A_offset_graphic_data ; load graphic data 0a
level_routine_05_exit:
lda #$00
sta LEVEL_ROUTINE_INDEX ; go back to level_routine_00
sta END_LEVEL_ROUTINE_INDEX ; clear end level routine
sta SPRITE_LOAD_TYPE ; set to load normal sprites
rts
; game over, unless demo mode, then set DEMO_LEVEL_END_FLAG
show_game_over_screen:
lda DEMO_MODE ; #$00 not in demo mode, #$01 demo mode on
bne @set_demo_end_exit ; skip to end when in demo mode
jsr zero_out_nametables ; reset nametables 0-1 to zeroes
lda #$0a ; set offset to point to intro_graphic_data_00 -> graphic_data_01 -> (pattern table for screen)
jsr load_A_offset_graphic_data ; load graphic_data_01
lda #$06 ; a = #$06 transition_screen_palettes (palettes for intro screen)
jsr load_bank_6_write_text_palette_to_mem ; draw text string
lda #$0d ; a = #$0d text_game_over (game over)
jsr load_bank_6_write_text_palette_to_mem ; draw text string
lda #$4e ; a = #$4e (sound_4e)
jsr play_sound ; play game over sound
dec NUM_CONTINUES ; subtract from number of lives remaining
bmi @no_continues_remaining ; if run out of continues, jump
lda #$0e ; a = #$0e (continue end)
jsr load_bank_6_write_text_palette_to_mem ; draw text string
jmp inc_level_routine_index
@no_continues_remaining:
lda #$07 ; a = #$07
jmp set_a_as_current_level_routine
; in demo mode and
@set_demo_end_exit:
inc DEMO_LEVEL_END_FLAG ; set value indicating demo for level is complete
rts
; no more lives screen - shows score and "continue"/"end" option
; resets player score and goes back to level_routine_00 if player selects continue
; resets game routine back to #$00 if player selects end
level_routine_06:
lda CONTROLLER_STATE_DIFF ; controller 1 buttons pressed
and #$10 ; keep bits ...x .... (start button)
beq @check_select_button ; if start button isn't pressed, jump
jsr init_APU_channels
lda CONT_END_SELECTION ; determine cursor position between "CONTINUE" and "END"
bne reset_game_routine ; exit game if end was selected
jsr reset_players_score ; continue was selected, reset the player scores back to #$00
lda #$00
sta LEVEL_ROUTINE_INDEX ; set to level_routine_00
sta SPRITE_X_POS
sta SPRITE_Y_POS
sta CPU_SPRITE_BUFFER
rts
@check_select_button:
lda CONTROLLER_STATE_DIFF ; controller 1 buttons pressed
and #$20 ; select button
beq @set_cursor_sprite_and_scores ; jump if select isn't pressed
lda CONT_END_SELECTION ; load which option is current selected
eor #$01 ; swap selection between "CONTINUE"/"END", i.e. flip bits .... ...x
sta CONT_END_SELECTION ; save swapped setting
@set_cursor_sprite_and_scores:
lda #$52 ; hard-code the horizontal position of the cursor sprite
sta SPRITE_X_POS
lda #$aa ; sprite_aa: player selector cursor (yellow falcon)
sta CPU_SPRITE_BUFFER ; set cursor as first (and only) sprite to draw
ldx CONT_END_SELECTION ; load whether "CONTINUE" or "END" is selected
lda player_select_cursor_pos,x ; load the vertical position on the screen for the cursor
sta SPRITE_Y_POS ; set Y position in CPU memory
jmp draw_the_scores ; draw the scores
; resets GAME_ROUTINE_INDEX to #$00 and resets delay timer
reset_game_routine:
lda #$00
jmp set_game_routine_index_to_a ; set GAME_ROUTINE_INDEX to #$00 and reset delay timer to #$0240
; show game over screen until player presses start
; once player presses start, game is reset
level_routine_07:
lda CONTROLLER_STATE_DIFF ; controller 1 buttons pressed
and #$10 ; check for start button
bne reset_game_routine ; reset the game if start button was pressed
jmp draw_the_scores ; start button not pressed, continue to show score
; check for game over, otherwise wait for delay and play end of level tune
level_routine_08:
jsr check_game_over_run_enemy_logic ; check to see if player(s) have game overed
; if so, sets next level routine to be #$0a to begin game over sequence
; otherwise, execute various enemy logic
lda LEVEL_ROUTINE_INDEX
cmp #$0a ; check_game_over_run_enemy_logic sets LEVEL_ROUTINE_INDEX to #$0a
; when both players are in game over state
beq level_routine_exit ; if game over, simply exit, next routine will be level_routine_0a
ldy DELAY_TIME_LOW_BYTE ; not game over, load delay
beq @continue ; branch to continue if delay has elapsed
iny ; delay timer has not elapsed, increment
beq level_routine_exit ; exit if delay was #$ff
jsr decrement_delay_timer ; delay wasn't #$ff, decrement full #$02 byte delay
bne level_routine_exit ; exit if timer hasn't elapsed
@continue:
ldx #$01 ; x = #$01
ldy #$00 ; initialize number of alive players to #$00
@player_loop:
lda P1_GAME_OVER_STATUS,x ; load game over state for player x (1 = game over)
bne @next_player_adv_lvl_index ; branch if game over
lda PLAYER_STATE,x ; game not over, load player state (0 = dropping into level, 1 = normal, 2 = dead, 3 = can't move)
cmp #$01 ; compare to normal state
bne @next_player_adv_lvl_index ; branch if player not in normal state
iny
@next_player_adv_lvl_index:
dex ; decrement player index (0 = p1, 1 = p2)
bpl @player_loop ; branch if still need to evaluate player 1
tya ; transfer number of alive players to a
sta LEVEL_END_PLAYERS_ALIVE ; set total number of alive players at level end
beq level_routine_exit
lda #$46 ; a = #$46 (sound_46)
jsr play_sound ; play end of level sound
inc LEVEL_ROUTINE_INDEX ; move to next level routine
level_routine_exit:
rts
; run end of level sequence routines
level_routine_09:
jsr load_bank_3_run_end_lvl_sequence_routine
jsr set_frame_scroll_draw_player_bullets
jsr load_bank_3_handle_scroll ; handles scrolling for the level if currently scrolling
; handles updating nametable, attribute table, and loading alternate graphics as appropriate
jsr load_bank_0_exe_all_enemy_routine
jmp load_palette_indexes
; waits for GAME_OVER_DELAY_TIMER to elapse and then move to level_routine_05
; to show game over score
level_routine_0a:
jsr run_level_enemy_logic
dec GAME_OVER_DELAY_TIMER ; decrement timer (initialized to #$60)
bne level_routine_exit ; wait for GAME_OVER_DELAY_TIMER to elapse
jmp set_to_level_routine_05 ; go to level_routine_05 to show game over high score
; checks for start button and sets pause status as appropriate
; plays sound if entering pause
check_for_pause:
lda DEMO_MODE ; #$00 not in demo mode, #$01 demo mode on
ora $26
ora PPU_READY ; #$00 when PPU is ready, > #$00 otherwise
bne pause_exit_00 ; if in demo, PPU isn't ready, or $26 > 0, then exit
lda CONTROLLER_STATE_DIFF ; controller 1 buttons pressed
ldy PAUSE_STATE ; #$01 for paused, #$00 for not paused
bne @game_paused ; if game paused, jump
and #$10 ; keep bits ...x .... (check for start button)
beq pause_exit_00 ; exit if start button isn't pressed
lda #$01 ; a = #$01
sta PAUSE_STATE ; #$01 for paused, #$00 for not paused
lda #$54 ; a = #$54 (54 = game pausing jingle sound)
jmp play_sound ; play game pausing jingle sound
; handle game paused state
; un-pauses if necessary
@game_paused:
jsr draw_player_bullet_sprites ; draw half of the bullets in alternating frames
jsr load_bank_2_set_players_paused_sprite_attr ; continue animating player sprite attributes while paused (electrocuted, invincible, etc.)
.ifdef Probotector
jsr pause_exit ; !(HUH) probably cut out code from the Japanese version
.endif
lda CONTROLLER_STATE_DIFF ; controller 1 buttons pressed
and #$10 ; keep bits ...x .... (check for start button)
beq pause_exit_00 ; exit if start button isn't pressed
lda #$00 ; a = #$00
sta PAUSE_STATE ; set game state to not paused
pause_exit_00:
.ifdef Probotector
rts
.endif
pause_exit:
rts
; load the alternate graphics
; CPU address $d064
load_alternate_graphics:
lda #$ff ; a = #$ff
sta LEVEL_ALT_GRAPHICS_POS ; prevent any further attempt to load alternate graphics
lda CURRENT_LEVEL ; prepare to determine index into lvl_alt_collision_and_palette_tbl, each level has #$f bytes
asl
asl
asl
asl
sec ; set the carry flag in preparation for subtraction
sbc CURRENT_LEVEL ; multiply by 16 and subtract level to get 15 * level number, i.e. 16n - n == 15n
tay
ldx #$00 ; set cpu mem write offset to #$00
; loop to overwrite level palette and collision info [$49-$57]
@loop:
lda lvl_alt_collision_and_palette_tbl,y
sta COLLISION_CODE_1_TILE_INDEX,x
iny ; increment read offset
inx ; increment write offset
cpx #$0f ; see if all #$f bytes have been written to cpu buffer
bne @loop ; if not yet finished, loop
lda #$20 ; set to reload #$20 palettes
; loads palette colors into PALETTE_CPU_BUFFER based on cpu memory LEVEL_PALETTE_INDEX
; a - the number of palette colors to load (including hard-coded black per palette)
; #$10 (4 palettes) or #$20 (8 palettes) depending on loading nametable colors, or both nametable and sprite colors
load_palettes_color_to_cpu:
sta $02 ; store number of palette colors to load to CPU memory
sta NUM_PALETTES_TO_LOAD ; set number of palette colors to load
lda FRAME_COUNTER ; load frame counter
and #$30 ; keep bits ..xx ....
sta $03 ; store the masked frame number in $03 (I don't believe this is used) !(WHY?)
ldx #$00 ; initialize colors written counter
stx $00 ; set LEVEL_PALETTE_INDEX read offset to #$00
; read $02 palette colors starting level palette index $00
; store actual palette colors from table game_palettes into cpu memory
; starting at PALETTE_CPU_BUFFER
; input
; * x - number of colors already written
; * $00 - current palette index to load (indexes into LEVEL_PALETTE_INDEX)
; * $02 - total number of palette colors to load
; game_palette_ptr_tbl only has one entry, not sure why it was used in the first place !(HUH)
; it complicates loading the index from the game_palettes table
load_palette_colors_to_cpu:
lda #$00 ; a = #$00
sta $07 ; reset $07 to #$00
ldy $00 ; load LEVEL_PALETTE_INDEX read offset [#$00-#$08)
lda LEVEL_PALETTE_INDEX,y ; load background palette index into game_palette_ptr_tbl
asl
adc LEVEL_PALETTE_INDEX,y ; double and add one (multiply by 3) - palettes are 3 (1-byte) colors
; at this point, the relative offset is known, but now the address must be computed
rol $07 ; if there was a carry (relative offset >= #$80), push into high byte
adc game_palette_ptr_tbl ; add relative offset to low byte of game_palettes address (#$27)
sta $06 ; store low byte of offset into game_palettes into $06
lda $07 ; reload high byte (could be #$01 if relative palette was >= #$80)
adc game_palette_ptr_tbl+1 ; add high byte of game_palettes pointer address to high byte of game_palettes address
sta $07 ; store offset into game_palettes into $07, know the exact address is stored in $(06)
ldy #$00 ; y = #$00
lda #$0f ; a = #$0f (basic black for all palettes)
sta PALETTE_CPU_BUFFER,x ; store the universal background color in cpu buffer
; every palette has black as its first color
inx ; increment cpu buffer write offset
read_palette_loop:
lda BG_PALETTE_ADJ_TIMER ; see if a palette color shift timer was used
bne shift_bg_palette_color ; adjust palette color if BG_PALETTE_ADJ_TIMER is non-zero
; reads the palette color from the game_palettes table
read_palette_color:
lda ($06),y ; read the palette color
; store the palette color in A register to CPU memory
write_palette_color_a_to_cpu_mem:
sta PALETTE_CPU_BUFFER,x ; store palette color into CPU memory
iny ; increment read offset
inx ; increment cpu memory buffer write offset
cpy #$03 ; see if read all #$03 colors of the palette
bne read_palette_loop ; branch if haven't yet loaded entire palette (palettes are 3 colors)
inc $00 ; finished reading palette, increment level palette index read offset
cpx $02 ; see if written all the colors to the cpu buffer
bne load_palette_colors_to_cpu ; if more palette colors to load, loop back and load them
lda BG_PALETTE_ADJ_TIMER ; load palette color shift timer
beq @exit ; exit if #$00
bmi @exit ; exit if < #$00
dec BG_PALETTE_ADJ_TIMER ; decrement palette color shift timer
@exit:
rts
; adjust nametable palette color based on BG_PALETTE_ADJ_TIMER to create fading effect
; while timer is out of range [#$01-#$09] only black is drawn, but once in range colors will be adjusted
; * for non-indoor boss screens, the first palette is not modified
; * only nametable palettes are modified and not sprite palettes
; * used on indoor levels between sections, dragon and boss ufo fade-in effect, and on boss mouth
; input
; * y - LEVEL_PALETTE_INDEX read offset
; * x - number of palette colors written (including hard-coded black)
shift_bg_palette_color:
sty $03 ; backup the read offset into y
ldy LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bmi @continue ; branch if indoor (base) level boss screen shown (LEVEL_LOCATION_TYPE set to #$80)
ldy $03 ; for non-indoor boss screens, the first palette is not modified, restore the read offset from $03 to check
cpx #$04 ; compare palette color offset to #$04
bcc read_palette_color ; branch if still reading the first palette, as that palette isn't modified for non-indoor boss screens
@continue:
ldy $03 ; load palette color read offset
cpx #$10 ; see if all of the nametable sprites have been written
bcs read_palette_color ; branch if loading a sprite palette, those aren't modified
lda BG_PALETTE_ADJ_TIMER ; load nametable palette modification timer for palette change
bmi @write_black_to_cpu_mem ; just write black if BG_PALETTE_ADJ_TIMER is negative
cmp #$09 ; see if BG_PALETTE_ADJ_TIMER is in valid range to start modifying palette color
bcs @write_black_to_cpu_mem ; just write black if BG_PALETTE_ADJ_TIMER isn't yet in range
tay ; transfer BG_PALETTE_ADJ_TIMER to offset register
lda palette_shift_amount_tbl-1,y ; load the amount to shift the palette color by (1-indexed)
sta $04 ; store palette color shift amount in $04
ldy $03 ; load the palette color read offset
lda ($06),y ; read palette color
sec ; set carry flag in preparation for subtraction
sbc $04 ; subtract the amount specified in palette_shift_amount_tbl from palette color
bcs write_palette_color_a_to_cpu_mem ; write modified palette color if result wasn't negative
; otherwise continue to just write black
@write_black_to_cpu_mem:
lda #$0f ; a = #$0f
bne write_palette_color_a_to_cpu_mem ; always jump
; amount to subtract from palette color when BG_PALETTE_ADJ_TIMER is between #$01 and #$09 (1-indexed)
palette_shift_amount_tbl:
.byte $00,$00,$10,$10,$20,$20,$30,$30
; load the appropriate palette colors based on level and LEVEL_PALETTE_CYCLE
; stores appropriate game_palettes indexes into LEVEL_PALETTE_INDEX+2 and LEVEL_PALETTE_INDEX+3
load_palette_indexes:
lda NUM_PALETTES_TO_LOAD ; load the number of palette indexes to update
cmp GAME_ROUTINE_INDEX ; current game routine index
bcs palette_mod_exit ; exit if NUM_PALETTES_TO_LOAD >= GAME_ROUTINE_INDEX
lda FRAME_COUNTER ; load frame counter
and #$07 ; keep bits .... .xxx
cmp #$05 ; see if the last 3 bits are #$05 (every #$8 frames)
bne falcon_weapon_flash ; branch if not equal to #$07 (do not increment LEVEL_PALETTE_CYCLE)
lda PAUSE_PALETTE_CYCLE ; see if palette cycling has been paused (ice field tanks pause palette cycling)
bne falcon_weapon_flash
inc LEVEL_PALETTE_CYCLE ; move to next set of palette colors for the 4th background palette
lda LEVEL_PALETTE_CYCLE
ldy CURRENT_LEVEL ; current level
cmp lvl_palette_animation_count,y ; see how many palette cycles there are for the level (level 3 only has 3, every other level has 4)
bcc @continue ; branch if current cycle less than max (LEVEL_PALETTE_CYCLE < lvl_palette_animation_count,y)
lda #$00 ; a = #$00
sta LEVEL_PALETTE_CYCLE ; exceeded number of palettes in level animation, set back to #$00 for next loop
@continue:
tay
lda LEVEL_PALETTE_CYCLE_INDEXES,y ; load current palette colors for cycling background tiles
sta LEVEL_PALETTE_INDEX+3 ; store index into palette code 3 for background tiles
lda LEVEL_LOCATION_TYPE ; see if have gotten to the indoor (base) level boss screen
bmi set_indoor_boss_palette_2_animation ; player has gotten to indoor boss and value has been set to #$80, jump
lda CURRENT_LEVEL ; current level
beq load_palettes_color_to_cpu_2_index ; skip ahead to load level 1 palette indexes for enemy flashing red
cmp #$07 ; check if level 8
beq load_10_sprite_palettes
cmp #$08 ; check if ending
beq set_ending_palette_animation
lda LEVEL_ALT_GRAPHICS_POS ; see status of loading alternate graphics
bmi load_10_sprite_palettes ; branch if alternate graphics are still being loaded (not yet done)
; update the 3rd nametable palette colors (flashing red lights effect)
load_palettes_color_to_cpu_2_index:
lda level_palette_2_index,y
set_a_to_palette_2:
sta LEVEL_PALETTE_INDEX+2
load_10_sprite_palettes:
lda #$10 ; a = #$10
jsr load_palettes_color_to_cpu ; load #$10 palette colors into PALETTE_CPU_BUFFER based on LEVEL_PALETTE_INDEX
falcon_weapon_flash:
lda FALCON_FLASH_TIMER ; falcon weapon flash timer
beq palette_mod_exit ; exit if timer has elapsed
dec FALCON_FLASH_TIMER ; falcon weapon flash timer
lda FALCON_FLASH_TIMER
lsr
bcs palette_mod_exit
and #$03 ; keep bits .... ..xx
tay
lda falcon_weapon_flash_tbl,y
sta PALETTE_CPU_BUFFER+16
sta PALETTE_CPU_BUFFER+20
sta PALETTE_CPU_BUFFER+24
sta PALETTE_CPU_BUFFER+28
lda #$20 ; offset #$20 into palette buffer
sta NUM_PALETTES_TO_LOAD ; number of palettes to write to cpu buffer
palette_mod_exit:
rts
; updates the 3rd palette code based on LEVEL_PALETTE_CYCLE for indoor (base) level boss screen
set_indoor_boss_palette_2_animation:
lda CURRENT_LEVEL ; current level (going to be either #$01 or #$03)
and #$02 ; differentiate which indoor level (#$00 for first base and #$02 for second base level)
asl ; double since each level has #$4 entries allowing second base to start at #$04
adc LEVEL_PALETTE_CYCLE ; add to current palette cycle iteration
tay
lda indoor_boss_palette_2_index,y ;
sta LEVEL_PALETTE_INDEX+2 ; palette code 2 for background tiles
jmp load_10_sprite_palettes
set_ending_palette_animation:
lda ending_palette_2_index,y
bne set_a_to_palette_2
; number of palettes to cycle through per level (LEVEL_PALETTE_CYCLE)
lvl_palette_animation_count:
.byte $04,$04,$03,$04,$04,$04,$04,$04,$04
; the palette indexes for the indoor boss screens
indoor_boss_palette_2_index:
.byte $13,$14,$15,$14 ; first indoor (base) palette code 2 animation cycle
.byte $1b,$1c,$1d,$1c ; second indoor (base) palette code 2 animation cycle
; flashing effect color codes for falcon weapon ($04 bytes)
falcon_weapon_flash_tbl:
.byte $0f,$30,$16,$11
; palette code 2 palette indexes into game_palettes shared among all levels
; animation for flashing red colors on enemies
level_palette_2_index:
.byte $04,$5c,$04,$5d
ending_palette_2_index:
.byte $66,$6a,$6b,$6a ; table for ending scene ($04 bytes)
; tables for alternate collision limits and palettes ($08 * $0f = $78 bytes)
; corresponds to [$49-$57]
lvl_alt_collision_and_palette_tbl:
.byte $06,$a8,$a8,$23,$23,$23,$23,$02,$03,$04,$23,$00,$01,$22,$07 ; level 1
.byte $00,$ff,$ff,$16,$17,$18,$17,$11,$12,$13,$16,$00,$01,$22,$21 ; level 2
.byte $07,$ff,$ff,$27,$54,$55,$54,$0b,$25,$26,$27,$00,$01,$22,$07 ; level 3
.byte $00,$ff,$ff,$1e,$1f,$20,$1f,$19,$1a,$1c,$1e,$00,$01,$22,$2b ; level 4
.ifdef Probotector
.byte $20,$f0,$f0,$42,$42,$42,$42,$3d,$3e,$40,$42,$00,$01,$22,$07 ; level 5
.byte $0c,$de,$de,$3a,$3b,$3a,$3c,$39,$39,$04,$3a,$00,$01,$22,$07 ; level 6
.else
.byte $20,$f0,$f0,$42,$42,$42,$42,$3d,$3e,$40,$42,$00,$01,$22,$06 ; level 5
.byte $0c,$de,$de,$3a,$3b,$3a,$3c,$39,$39,$04,$3a,$00,$01,$22,$56 ; level 6
.endif
.byte $0e,$f1,$f1,$5a,$5f,$5a,$5b,$45,$46,$59,$5f,$00,$01,$22,$07 ; level 7
.byte $05,$b6,$b6,$4b,$50,$4b,$50,$48,$49,$4a,$4b,$00,$01,$43,$44 ; level 8
.byte $00,$00,$00,$67,$68,$69,$68,$25,$65,$66,$67,$6d,$6c,$22,$64 ; ending animation
; pointer for palettes table ($02 bytes)
; CPU address $d225
game_palette_ptr_tbl:
.addr game_palettes ; CPU address $d227
; palettes ($6e * $03 = $14a bytes)
; CPU Address $d227
game_palettes:
.ifdef Probotector
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_BLACK_0f
.byte COLOR_PALE_VIOLET_32 ,COLOR_MED_VIOLET_12 ,COLOR_BLACK_0f
.else
.byte COLOR_PALE_ORANGE_37 ,COLOR_MED_VIOLET_12 ,COLOR_BLACK_0f
.byte COLOR_PALE_RED_36 ,COLOR_MED_RED_16 ,COLOR_BLACK_0f
.endif
.byte COLOR_MED_FOREST_GREEN_19 ,COLOR_LT_FOREST_GREEN_29 ,COLOR_DARK_OLIVE_08
.byte COLOR_LT_OLIVE_28 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_OLIVE_08
.byte COLOR_MED_RED_16 ,COLOR_WHITE_30 ,COLOR_LT_GRAY_10
.byte COLOR_MED_BLUE_11 ,COLOR_LT_BLUE_21 ,COLOR_WHITE_30
.ifdef Probotector
.byte COLOR_PALE_GREEN_3a ,COLOR_MED_BLUE_GREEN_1b ,COLOR_BLACK_0f
.byte COLOR_PALE_RED_36 ,COLOR_MED_RED_16 ,COLOR_BLACK_0f
.else
.byte COLOR_MED_RED_16 ,COLOR_WHITE_20 ,COLOR_DARK_GRAY_00
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_BLACK_0f
.endif
.byte COLOR_MED_BLUE_11 ,COLOR_WHITE_30 ,COLOR_LT_BLUE_21
.byte COLOR_LT_GRAY_10 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_FOREST_GREEN_09
.byte COLOR_DARK_GRAY_00 ,COLOR_DARK_TEAL_0c ,COLOR_WHITE_20
.byte COLOR_DARK_GREEN_0a ,COLOR_MED_GREEN_1a ,COLOR_DARK_GRAY_00
.byte COLOR_LT_OLIVE_28 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_ORANGE_07
.byte COLOR_MED_TEAL_1c ,COLOR_LT_TEAL_2c ,COLOR_DARK_TEAL_0c
.byte COLOR_DARK_TEAL_0c ,COLOR_MED_TEAL_1c ,COLOR_LT_TEAL_2c
.byte COLOR_LT_TEAL_2c ,COLOR_DARK_TEAL_0c ,COLOR_MED_TEAL_1c
.byte COLOR_LT_GRAY_10 ,COLOR_LT_BLUE_GREEN_2b ,COLOR_BLACK_0f
.byte COLOR_DARK_RED_06 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_OLIVE_08
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_OLIVE_08
.byte COLOR_DARK_RED_06 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_OLIVE_08
.byte COLOR_MED_RED_16 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_OLIVE_08
.byte COLOR_LT_RED_26 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_OLIVE_08
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_RED_06
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_MED_RED_16
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_LT_RED_26
.byte COLOR_DARK_TEAL_0c ,COLOR_MED_OLIVE_18 ,COLOR_DARK_FOREST_GREEN_09
.byte COLOR_WHITE_20 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_FOREST_GREEN_09
.byte COLOR_DARK_RED_06 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_FOREST_GREEN_09
.byte COLOR_MED_RED_16 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_FOREST_GREEN_09
.byte COLOR_LT_RED_26 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_FOREST_GREEN_09
.byte COLOR_WHITE_20 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_RED_06
.byte COLOR_WHITE_20 ,COLOR_MED_OLIVE_18 ,COLOR_MED_RED_16
.byte COLOR_WHITE_20 ,COLOR_MED_OLIVE_18 ,COLOR_LT_RED_26
.ifdef Probotector
.byte COLOR_WHITE_20 ,COLOR_LT_VIOLET_22 ,COLOR_DARK_TEAL_0c
.else
.byte COLOR_WHITE_20 ,COLOR_LT_VIOLET_22 ,COLOR_DARK_VIOLET_02
.endif
.byte COLOR_WHITE_20 ,COLOR_LT_RED_26 ,COLOR_MED_RED_16
.byte COLOR_DARK_BLUE_01 ,COLOR_WHITE_30 ,COLOR_LT_GRAY_10
.byte COLOR_PALE_RED_36 ,COLOR_DARK_RED_06 ,COLOR_DARK_VIOLET_02
.byte COLOR_WHITE_30 ,COLOR_LT_GRAY_10 ,COLOR_DARK_GRAY_00
.byte COLOR_LT_OLIVE_28 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_OLIVE_08
.byte COLOR_DARK_PINK_05 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_OLIVE_08
.byte COLOR_PALE_RED_36 ,COLOR_DARK_RED_06 ,COLOR_LT_VIOLET_22
.byte COLOR_PALE_RED_36 ,COLOR_DARK_RED_06 ,COLOR_PALE_VIOLET_32
.ifdef Probotector
.byte COLOR_PALE_RED_36 ,COLOR_MED_RED_16 ,COLOR_BLACK_0f
.byte COLOR_WHITE_20 ,COLOR_LT_VIOLET_22 ,COLOR_DARK_TEAL_0c
.else
.byte COLOR_WHITE_20 ,COLOR_MED_BLUE_GREEN_1b ,COLOR_BLACK_0f
.byte COLOR_WHITE_20 ,COLOR_LT_VIOLET_22 ,COLOR_MED_TEAL_1c
.endif
.byte COLOR_LT_GRAY_10 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_TEAL_0c
.byte COLOR_DARK_GRAY_00 ,COLOR_DARK_RED_06 ,COLOR_WHITE_20
.byte COLOR_PALE_OLIVE_38 ,COLOR_DARK_FOREST_GREEN_09 ,COLOR_DARK_RED_06
.byte COLOR_PALE_OLIVE_38 ,COLOR_DARK_FOREST_GREEN_09 ,COLOR_MED_RED_16
.byte COLOR_PALE_OLIVE_38 ,COLOR_DARK_FOREST_GREEN_09 ,COLOR_LT_RED_26
.byte COLOR_BLACK_0f ,COLOR_WHITE_20 ,COLOR_LT_TEAL_2c
.byte COLOR_BLACK_0f ,COLOR_WHITE_20 ,COLOR_LT_RED_26
.byte COLOR_DARK_GRAY_00 ,COLOR_WHITE_20 ,COLOR_MED_GREEN_1a
.byte COLOR_DARK_BLUE_01 ,COLOR_WHITE_20 ,COLOR_DARK_GRAY_00
.byte COLOR_WHITE_20 ,COLOR_LT_ORANGE_27 ,COLOR_MED_ORANGE_17
.byte COLOR_WHITE_20 ,COLOR_LT_RED_26 ,COLOR_DARK_ORANGE_07
.byte COLOR_WHITE_20 ,COLOR_LT_ORANGE_27 ,COLOR_MED_RED_16
.byte COLOR_WHITE_20 ,COLOR_LT_RED_26 ,COLOR_DARK_RED_06
.byte COLOR_DARK_GRAY_00 ,COLOR_LT_GRAY_10 ,COLOR_MED_PURPLE_13
.byte COLOR_MED_RED_16 ,COLOR_WHITE_20 ,COLOR_DARK_GRAY_00
.byte COLOR_DARK_RED_06 ,COLOR_WHITE_20 ,COLOR_DARK_GRAY_00
.byte COLOR_LT_RED_26 ,COLOR_WHITE_20 ,COLOR_DARK_GRAY_00
.byte COLOR_WHITE_20 ,COLOR_LT_GRAY_10 ,COLOR_MED_TEAL_1c
.byte COLOR_WHITE_20 ,COLOR_LT_GRAY_10 ,COLOR_DARK_GREEN_0a
.byte COLOR_WHITE_20 ,COLOR_DARK_GRAY_00 ,COLOR_MED_ORANGE_17
.byte COLOR_WHITE_20 ,COLOR_MED_VIOLET_12 ,COLOR_DARK_GRAY_00
.byte COLOR_DARK_ORANGE_07 ,COLOR_DARK_GRAY_00 ,COLOR_MED_ORANGE_17
.byte COLOR_WHITE_20 ,COLOR_MED_RED_16 ,COLOR_DARK_GRAY_00
.byte COLOR_WHITE_30 ,COLOR_LT_OLIVE_28 ,COLOR_MED_RED_16
.byte COLOR_WHITE_30 ,COLOR_LT_PINK_25 ,COLOR_MED_MAGENTA_14
.byte COLOR_LT_ORANGE_27 ,COLOR_MED_ORANGE_17 ,COLOR_DARK_RED_06
.byte COLOR_WHITE_20 ,COLOR_LT_GRAY_10 ,COLOR_DARK_GRAY_00
.byte COLOR_DARK_RED_06 ,COLOR_MED_BLUE_GREEN_1b ,COLOR_DARK_BLUE_GREEN_0b
.byte COLOR_LT_GRAY_10 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_ORANGE_07
.byte COLOR_PALE_RED_36 ,COLOR_MED_PINK_15 ,COLOR_DARK_RED_06
.byte COLOR_PALE_PINK_35 ,COLOR_MED_PINK_15 ,COLOR_DARK_MAGENTA_04
.byte COLOR_PALE_PINK_35 ,COLOR_MED_RED_16 ,COLOR_MED_TEAL_1c
.byte COLOR_MED_OLIVE_18 ,COLOR_MED_PURPLE_13 ,COLOR_DARK_PURPLE_03
.byte COLOR_LT_RED_26 ,COLOR_MED_PURPLE_13 ,COLOR_DARK_PURPLE_03
.byte COLOR_MED_MAGENTA_14 ,COLOR_MED_PURPLE_13 ,COLOR_DARK_PURPLE_03
.byte COLOR_LT_BLUE_GREEN_2b ,COLOR_MED_PURPLE_13 ,COLOR_DARK_PURPLE_03
.byte COLOR_LT_RED_26 ,COLOR_MED_RED_16 ,COLOR_DARK_PURPLE_03
.byte COLOR_WHITE_20 ,COLOR_MED_PURPLE_13 ,COLOR_LT_ORANGE_27
.byte COLOR_WHITE_20 ,COLOR_MED_RED_16 ,COLOR_LT_ORANGE_27
.byte COLOR_DARK_GRAY_00 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_GRAY_00
.byte COLOR_MED_PINK_15 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_OLIVE_08
.byte COLOR_PALE_PINK_35 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_OLIVE_08
.ifdef Probotector
.byte COLOR_PALE_GREEN_3a ,COLOR_MED_BLUE_GREEN_1b ,COLOR_BLACK_0f
.else
.byte COLOR_WHITE_20 ,COLOR_MED_VIOLET_12 ,COLOR_MED_ORANGE_17
.endif
.byte COLOR_MED_PINK_15 ,COLOR_MED_BLUE_GREEN_1b ,COLOR_DARK_BLUE_GREEN_0b
.byte COLOR_BLACK_0f ,COLOR_MED_BLUE_GREEN_1b ,COLOR_DARK_BLUE_GREEN_0b
.byte COLOR_LT_MAGENTA_24 ,COLOR_MED_PURPLE_13 ,COLOR_DARK_PURPLE_03
.byte COLOR_LT_ORANGE_27 ,COLOR_MED_ORANGE_17 ,COLOR_DARK_RED_06
.byte COLOR_MED_ORANGE_17 ,COLOR_DARK_RED_06 ,COLOR_BLACK_0f
.byte COLOR_DARK_RED_06 ,COLOR_WHITE_30 ,COLOR_LT_GRAY_10
.byte COLOR_LT_RED_26 ,COLOR_WHITE_30 ,COLOR_LT_GRAY_10
.ifdef Probotector
.byte COLOR_WHITE_20 ,COLOR_MED_VIOLET_12 ,COLOR_MED_ORANGE_17
.else
.byte COLOR_DARK_GRAY_00 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_GRAY_00
.endif
.byte COLOR_WHITE_20 ,COLOR_LT_ORANGE_27 ,COLOR_MED_ORANGE_17
.byte COLOR_LT_GRAY_10 ,COLOR_LT_RED_26 ,COLOR_DARK_RED_06
.byte COLOR_LT_GRAY_10 ,COLOR_MED_RED_16 ,COLOR_DARK_ORANGE_07
.byte COLOR_WHITE_20 ,COLOR_LT_VIOLET_22 ,COLOR_DARK_VIOLET_02
.byte COLOR_LT_VIOLET_22 ,COLOR_WHITE_20 ,COLOR_DARK_VIOLET_02
.byte COLOR_WHITE_20 ,COLOR_LT_GRAY_10 ,COLOR_DARK_GRAY_00
.byte COLOR_MED_OLIVE_18 ,COLOR_DARK_ORANGE_07 ,COLOR_MED_GREEN_1a
.byte COLOR_MED_RED_16 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_ORANGE_07
.byte COLOR_WHITE_20 ,COLOR_LT_GRAY_10 ,COLOR_MED_BLUE_11
.byte COLOR_WHITE_20 ,COLOR_MED_BLUE_11 ,COLOR_MED_BLUE_11
.byte COLOR_WHITE_20 ,COLOR_LT_TEAL_2c ,COLOR_MED_BLUE_11
.byte COLOR_LT_RED_26 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_ORANGE_07
.byte COLOR_MED_RED_16 ,COLOR_MED_OLIVE_18 ,COLOR_DARK_ORANGE_07
.byte COLOR_MED_RED_16 ,COLOR_DARK_GRAY_00 ,COLOR_DARK_GRAY_00
.ifdef Probotector
.byte COLOR_PALE_BLUE_GREEN_3b ,COLOR_MED_BLUE_GREEN_1b ,COLOR_DARK_BLUE_GREEN_0b
.else
.byte COLOR_DARK_BLUE_GREEN_0b ,COLOR_MED_BLUE_GREEN_1b ,COLOR_PALE_BLUE_GREEN_3b
.endif
; executed for indoor and outdoor levels
set_frame_scroll_draw_player_bullets:
jsr set_frame_scroll_weapon_strength ; set frame scroll, set weapon strength, update invincibility
lda INDOOR_SCROLL ; see if scrolling (0 = not scrolling; 1 = scrolling, 2 = finished scrolling)
cmp #$02 ; see if finished advancing and new screen has been shown
bcc @continue ; branch if still scrolling, or haven't started, or not indoor level
lda #$00 ; a = #$00
@continue:
sta INDOOR_SCROLL ; reset indoor scroll if completed, otherwise, keep its current value
lda AUTO_SCROLL_TIMER_00
ora AUTO_SCROLL_TIMER_01 ; merge two auto-scrolling values
beq @run_player_bullet_routines ; branch if no auto-scrolling
lda #$01 ; a = #$01
sta FRAME_SCROLL ; set scroll amount for frame
@run_player_bullet_routines:
jsr load_bank_6_run_player_bullet_routines ; run player bullet routines
; draw half the bullets, every other frame
; if only one bullet drawn every other frame
draw_player_bullet_sprites:
ldy #$07 ; maximum of #$08 player bullets
; loads bullets to sprite buffer
@player_bullet_loop:
tya ; transfer player bullet offset to a
asl ; shift bullet offset to the left by one bit
sta $08 ; store shifted value into $08
lda FRAME_COUNTER ; load the frame counter
and #$01 ; only care about bit 0 (odd/even)
ora $08 ; merge shifted bullet offset with frame counter odd/even flag
tax ; move specified bullet in memory into CPU_SPRITE_BUFFER so it can be drawn
lda PLAYER_BULLET_SPRITE_CODE,x ; load sprite code for specified bullet
sta PLAYER_SPRITES+2,y ; update bullet sprite in PLAYER_SPRITES
lda PLAYER_BULLET_SPRITE_ATTR,x ; load any bullet sprite attributes
sta SPRITE_ATTR+2,y ; set bullet sprite attributes
lda PLAYER_BULLET_Y_POS,x ; load player bullet y position
sta SPRITE_Y_POS+2,y ; set sprite bullet y position
lda PLAYER_BULLET_X_POS,x ; load player bullet x position
sta SPRITE_X_POS+2,y ; set sprite bullet x position
dey ; decrement to next bullet offset
bpl @player_bullet_loop ; loop if y still greater than or equal 0
rts
; initializes frame scroll, runs logic to set weapon strength, update invincibility
set_frame_scroll_weapon_strength:
lda #$00 ; a = #$00
sta FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
sta PLAYER_FRAME_SCROLL ; clear player 1 FRAME_SCROLL amount
sta PLAYER_FRAME_SCROLL+1 ; clear player 2 FRAME_SCROLL amount
sta ENEMY_ATTACK_FLAG ; stop enemies from attacking
sta PLAYER_WEAPON_STRENGTH ; clear player weapon strength
ldy P1_GAME_OVER_STATUS ; game over state of player 1 (1 = game over)
bne @p2_game_over_status ; skip assignment of a to #01 player 1 is in game over
ora #$01 ; set a to #$01 when player 1 is not in game over
@p2_game_over_status:
ldy P2_GAME_OVER_STATUS ; player 2 game over state (1 = game over)
bne run_player_invincibility_and_weapon_strength ; branch if player 2 is in game over, or if this is a single player game
ora #$02 ; set bit 2 to #$01 when player 2 is not in game over
; a will be #$00 when both players are in game over
; a will be #$01 when player 1 not game over, player 2 game over (or not playing)
; a will be #$02 when player 1 game over, player 2 not game over
; a will be #$03 when neither player 1 nor player 2 are in game over
run_player_invincibility_and_weapon_strength:
tax ; transfer game over statuses to x
beq player_state_routine_03 ; branch when both players are in game over
dex ; prep for setting in PLAYER_GAME_OVER_BIT_FIELD
stx PLAYER_GAME_OVER_BIT_FIELD ; #$00 = p1 not game over, p2 game over (or not playing)
; #$01 = p1 game over, p2 not game over, #$02 = p1 nor p2 are in game over
txa ; transfer PLAYER_GAME_OVER_BIT_FIELD to a
and #$01 ; used to determine if only one player is active, if so run logic on that player
; otherwise run logic on p1 first
tax ; transfer whether p2 is game over to x, set as current player
jsr handle_invincibility_and_weapon_strength ; run player state routine, checks invincibility, set weapon strength
lda PLAYER_GAME_OVER_BIT_FIELD ; #$00 = p1 not game over, p2 game over (or not playing)
; #$01 = p1 game over, p2 not game over, #$02 = p1 nor p2 are in game over
cmp #$02 ; see if both players are active
bne player_state_routine_03 ; if one of the players is game over (or not playing)
; already ran logic on the only active player, branch
inx ; both players active, already handled player 1, run logic for player 2
jsr handle_invincibility_and_weapon_strength ; run player state routine, checks invincibility, set weapon strength
jsr scroll_player ; scroll player that isn't causing the screen to scroll if necessary
player_state_routine_03:
lda LEVEL_ROUTINE_INDEX ; load current level routine
cmp #$04
bne @exit ; jump if not level routine 4 (this code is call from level_routine_04 and level_routine_09)
lda PLAYER_MODE ; number of players (#$00 = 1 player)
beq @exit
ldx #$01 ; x = #$01
@player_loop:
lda P1_GAME_OVER_STATUS,x ; load game over state for player x (1 = game over)
bne @check_transfer_life
dex
bpl @player_loop
bmi @exit
; related to lives transfer between players
; if game over player presses 'A', then a life is taken from the other player if possible
@check_transfer_life:
lda CONTROLLER_STATE_DIFF,x ; controller x buttons pressed
and #$80 ; keep bits x... .... (check for a button)
beq @exit ; if a isn't pressed, exit
txa
eor #$01 ; swap to other player by flipping bit 0
tay
lda P1_NUM_LIVES,y ; load other player's number of lives
beq @exit ; if other player doesn't have any additional remaining lives, exit
cmp #$01
bne @subtract_life
lda PLAYER_STATE,y ; player has load other player's player state
cmp #$01 ; make sure other player isn't dying and about to be on their last life
bne @exit ; if other player's player state isn't normal, exit
@subtract_life:
lda P1_NUM_LIVES,y ; player x lives
sec ; set carry flag in preparation for subtraction
sbc #$01 ; lose a life from player Y
cmp #$ff ; check if out of lives
bne @revive_player
lda #$00 ; a = #$00
@revive_player:
sta P1_NUM_LIVES,y ; set new, lowered number of lives for other player
jsr init_player_and_weapon ; reset revived player's attributes and player weapon
sta PLAYER_STATE,x ; reset revived player's state to #$00 (normal)
sta P1_GAME_OVER_STATUS,x ; clear game over status
@exit:
rts
; find if a player needs to be scrolled back if they aren't causing the scroll
; for horizontal levels, this means the player is scrolled to the left
; for the vertical level, this means the player is scrolled down
scroll_player:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bne @exit2 ; exit for indoor level, or indoor boss screen
lda LEVEL_SCROLLING_TYPE ; outdoor level, load scrolling type 0 = horizontal, indoor/base; 1 = vertical
bne @vertical_scroll_player ; exit on vertical level
jsr find_scrolled_player ; find the player that isn't causing scroll and needs to be scrolled
beq @exit ; exit if both players are causing scroll, don't cause any player to be scrolled
dec SPRITE_X_POS,x ; move player that isn't causing scroll back
; other player will remain at same relative position on screen
@exit:
rts
@vertical_scroll_player:
lda AUTO_SCROLL_TIMER_00 ; see if auto scroll is enabled
bne @exit2 ; exit if auto scroll is enabled, both players scroll down for auto scroll
jsr find_scrolled_player ; find player that should be scrolled down the screen,
; since they aren't causing the screen to scroll
beq @exit2 ; exit if both players are causing the screen to scroll
lda SPRITE_Y_POS,x ; load the player to scroll's y position
clc ; clear carry in preparation for addition
adc FRAME_SCROLL ; add the amount the screen is about to scroll
sta SPRITE_Y_POS,x ; adjust player position by FRAME_SCROLL so they are scrolled down the screen
bcc @exit2 ; exit if no overflow occurred adding to player position
inc PLAYER_HIDDEN,x ; overflow occurred, set player as hidden (off screen)
; doesn't ever seem to happen as off screen player dies before they can be hidden
@exit2:
rts
; determine player that is not causing scroll that should be moved in the opposite direction of the scroll
; output
; * x - 0 for p1, 1 for p2
; * zero flag - clear when both players are causing scroll, set when only one
find_scrolled_player:
ldx #$00 ; x = #$00
lda PLAYER_FRAME_SCROLL ; load player 1's frame scroll amount
cmp PLAYER_FRAME_SCROLL+1 ; compare to player 2's frame scroll amount
beq @exit ; exit if both are identical (default player 1 cause scroll, x = #$00)
bcc @exit ; exit if player 2 is causing scroll (mark player 1 cause scroll, x = #$00)
inx ; mark player 2 causing scroll (x = #$01)
@exit:
rts
; runs player state routine, checks new life invincibility timer,
; sets enemies to attack, sets weapon strength
; input
; * x - player offset
handle_invincibility_and_weapon_strength:
jsr run_player_state_routine ; run logic based on player's state (see PLAYER_STATE)
lda NEW_LIFE_INVINCIBILITY_TIMER,x ; timer for invincibility (after dying or start of level)
beq set_enemies_to_attack ; if invincibility timer is #$00, set enemies to attack
dec NEW_LIFE_INVINCIBILITY_TIMER,x ; decrement value
jmp decrement_invincibility_effect ; handle B (barrier) weapon (invincibility)
; new life invincibility elapsed. Set enemies to attack
set_enemies_to_attack:
lda PLAYER_STATE,x ; load player state (0 = dropping into level, 1 = normal, 2 = dead, 3 = can't move)
cmp #$01 ; see if player is in normal state
bne decrement_invincibility_effect ; if not in normal state, jump
lda #$01 ; new life invincibility timer elapsed, set enemies to attack
sta ENEMY_ATTACK_FLAG ; set enemies to attack
; decrement b weapon effect (invincibility) every 8th frame if active
decrement_invincibility_effect:
lda INVINCIBILITY_TIMER,x
beq @continue ; if no invincibility, jump
lda FRAME_COUNTER ; load frame counter
and #$07 ; clear all but last 3 bits
bne @continue ; only decrement every #$8 frames
dec INVINCIBILITY_TIMER,x ; decrement invincibility (b weapon effect) timer for current player
@continue:
lda PLAYER_RECOIL_TIMER,x ; see if how many frames player will have recoil
beq set_player_weapon_strength
dec PLAYER_RECOIL_TIMER,x ; decrement player recoil timer
; set the player weapon strength memory value based on current weapon
set_player_weapon_strength:
lda #$00 ; a = #$00
sta PLAYER_FAST_X_VEL_BOOST,x ; clear x fast velocity boost from being on a non-dangerous moving enemy
lda P1_CURRENT_WEAPON,x ; get current player's weapon
and #$07 ; keep bits .... .xxx
tay
lda weapon_strength,y ; load how strong the weapon is
cmp PLAYER_WEAPON_STRENGTH ; compare against current weapon strength
bcc @exit ; exit and do not lower player's current weapon strength
sta PLAYER_WEAPON_STRENGTH ; store current weapon strength code (#$00-#$03)
@exit:
rts
; table for weapon strength code (#$05 bytes)
weapon_strength:
.byte $00 ; Regular = Weak
.byte $02 ; M = Strong
.byte $01 ; F = Medium
.byte $03 ; S = Very Strong
.byte $02 ; L = Strong
; run logic based on players current state
; #$00 falling into level
; #$01 normal state
; #$02 dead
; #$03 can't move
run_player_state_routine:
ldy CURRENT_LEVEL ; current level
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
asl ; double the location type, shifting msb to carry
lda level_spawn_position_index,y ; load the spawn position offset into a
bcc @continue ; jump if not indoor boss screen
lda #$03 ; indoor boss screen, set $08 to #$03
@continue:
sta $08 ; store the offset into the spawn location into $08
; for player_state_routine_01 used to calculate offset into d_pad_player_aim_tbl
lda PLAYER_STATE,x ; load player state (0 = dropping into level, 1 = normal, 2 = dead, 3 = can't move)
jsr run_routine_from_tbl_below ; run routine a in the following table (player_state_routine)
; pointer table for unknown ($04 * $02 = $08 bytes)
player_state_routine:
.addr player_state_routine_00 ; CPU address $d4c3 - finds location to 'drop in' the player, executed once
.addr player_state_routine_01 ; CPU address $d534
.addr player_state_routine_02 ; CPU address $d593 - player has died, animate player falling backwards
.addr player_state_routine_03 ; CPU address $d3e6
; the offset into vertical_spawn_position and horizontal_spawn_position
; for player_state_routine_01 used to calculate offset into d_pad_player_aim_tbl
; based on the level
level_spawn_position_index:
.byte $00,$01,$02,$01,$00,$00,$00,$00
; player falling into level logic
; only run once to set player position
player_state_routine_00:
jsr init_player_attributes
txa ; set a to be current player
asl
asl
clc ; clear carry in preparation for addition
adc $08 ; offset index into spawn position tables
tay
lda vertical_spawn_position,y
sta SPRITE_Y_POS,x ; set player y position on screen
lda horizontal_spawn_position,y
sta SPRITE_X_POS,x ; set player x position on screen
lda #$01 ; a = #$01
sta PLAYER_JUMP_STATUS,x
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bne @set_frame_invincible_timer_exit ; branch if indoor level
jsr @check_if_floor_exit ; outdoor level, check if something to land on
bcc @set_frame_invincible_timer_exit ; branch if there is something to land on
lda #$10 ; nothing to land on at current position
; move from left of screen to right to find a spot (increments of #$10 pixels at a time)
sta SPRITE_X_POS,x ; set x position to #$10
@find_landing:
jsr @check_if_floor_exit ; check if something to land on
bcc @set_frame_invincible_timer_exit ; branch if nothing to land on at current position
lda SPRITE_X_POS,x ; nothing to land on at current position, move to next position
clc ; clear carry in preparation for addition
adc #$10 ; add #$10 to player x position
sta SPRITE_X_POS,x ; set new x position
cmp #$e0 ; see if at end of screen
bcs @set_x_pos ; couldn't find a position with a place to land, just use #$30
jmp @find_landing ; loop to see if next position to the right is appropriate for landing
; couldn't find a position with a place to land, just use x position #$30
@set_x_pos:
lda #$30 ; a = #$30
sta SPRITE_X_POS,x ; set initial x position when dropping in level
; sets PLAYER_ANIMATION_FRAME_INDEX to #$02, NEW_LIFE_INVINCIBILITY_TIMER to #$80, PLAYER_Y_FAST_VELOCITY to #$00
; and moves to next player state before exiting
@set_frame_invincible_timer_exit:
lda #$02 ; a = #$02
sta PLAYER_ANIMATION_FRAME_INDEX,x ; set frame index to #$02 sprite_08 (offset into player_curled_sprite_code_tbl)
; see set_player_jump_sprite
lda #$00 ; a = #$00
sta PLAYER_Y_FAST_VELOCITY,x ; set fast velocity to #$00 (fractional velocity still #$23 (.137))
lda #$80 ; invincibility time in number of frames, 2 seconds for NTSC
sta NEW_LIFE_INVINCIBILITY_TIMER,x ; set timer for invincibility (after dying)
inc PLAYER_STATE,x ; finished initializing player, move to state #$01 (normal state)
rts
; see if there is a place for the player
; output
; * carry flag - set when only empty collision codes below player; clear when solid, water, or ground beneath player
@check_if_floor_exit:
jsr get_player_bg_collision_code ; get player background collision code
asl ; push msb to carry flag (whether or not solid collision)
bcs @exit ; exit with carry set when collision code #$80 (solid)
; this means there is a solid object at the top of the screen
lda SPRITE_Y_POS,x ; still falling, load sprite y position
clc ; clear carry in preparation for addition
adc #$20 ; add #$20 to sprite y position
jmp check_collision_below ; jump to check if bg collision below player
@exit:
rts
; player spawn positions, according to level and player index
; table for spawn y positions ($08 bytes)
vertical_spawn_position:
.byte $20,$60,$50,$60 ; player 1
.byte $20,$60,$50,$60 ; player 2
; table for spawn x positions ($08 bytes)
horizontal_spawn_position:
.byte $30,$70,$30,$70 ; player 1
.byte $20,$90,$20,$90 ; player 2
; normal player state
player_state_routine_01:
jsr player_state_routine_01_logic
jsr load_bank_2_set_player_sprite ; set player sprite based on player state, level, and animation sequence
lda PLAYER_AIM_DIR,x
sta PLAYER_AIM_PREV_FRAME,x
lda PLAYER_HIDDEN,x ; 0 - visible; #$01/#$ff = invisible (any non-zero)
bne @exit
lda SPRITE_Y_POS,x
cmp #$e8 ; check if falling at the bottom of the screen
bcs kill_player
@exit:
rts
; x is the current player
kill_player:
lda #$52 ; a = #$52 (sound_52)
jsr play_sound ; play player death sound
jsr init_player_data ; reset player data and set a to #$00
sta PLAYER_WATER_STATE,x
sta ELECTROCUTED_TIMER,x
sta PLAYER_SPECIAL_SPRITE_TIMER,x
sta PLAYER_ANIMATION_FRAME_INDEX,x
sta PLAYER_ANIM_FRAME_TIMER,x
lda #$01 ; a = #$01
sta PLAYER_DEATH_FLAG,x
lda #$fd ; initiate jump by setting y velocity to #$fd80
sta PLAYER_Y_FAST_VELOCITY,x ; player y velocity
lda #$80 ; a = #$80
sta PLAYER_Y_FRACT_VELOCITY,x
inc PLAYER_STATE,x
rts
; set player aim direction based on d-pad input
; check if player is on an edge and should fall
; check if player is firing and generate bullet if so
; calculate player x velocity
; auto scroll player
player_state_routine_01_logic:
jsr set_player_aim_for_input ; set PLAYER_AIM_DIR based on d-pad input, facing direction, and jump status
jsr check_player_ledge ; see if player should check for walking off ledge and if so, walk off it
jsr load_bank_6_check_player_fire ; generate bullet if player is shooting and allowed to shoot
jsr handle_player_state_calc_x_vel
; auto scroll the player position if auto-scroll enabled
auto_scroll_player:
lda AUTO_SCROLL_TIMER_00 ; load auto scroll timer
ora AUTO_SCROLL_TIMER_01 ; merge with auto scroll timer 01
beq @exit ; branch if no auto scroll happening
lda LEVEL_SCROLLING_TYPE ; auto scroll happening, load scrolling type (0 = horizontal, indoor/base; 1 = vertical)
bne @inc_y_pos_exit ; increment y position and exit if vertical level with auto scroll (boss reveal)
lda SPRITE_X_POS,x ; horizontal, indoor/base level, load sprite x position
ldy #$00 ; y = #$00
cmp level_left_edge_x_pos_tbl,y ; compare player position to left edge
bcc @exit ; exit if already at farthest left edge to keep the player there (push them)
dec SPRITE_X_POS,x ; otherwise decrement from x position to make scroll effect
rts
@inc_y_pos_exit:
inc SPRITE_Y_POS,x
@exit:
rts
; player has died, animate player falling backwards
player_state_routine_02:
jsr auto_scroll_player ; auto scroll the player position if auto-scroll enabled
jsr sty_level_screen_type ; get screen type #$00 = outdoor, #$01 = indoor/base boss, #$02 = indoor/base
lda player_sprite_sequence_tbl,y ; load which animation to show for the player, .e.g. (4 = dead animation, 6 = indoor dead animation)
sta PLAYER_SPRITE_SEQUENCE,x ; set sprite sequence
lda PLAYER_ANIM_FRAME_TIMER,x ; see if animation timer has elapsed for sprite sequence
beq @animation_timer_elapsed ; branch if timer has elapsed
dec PLAYER_ANIM_FRAME_TIMER,x ; timer hasn't elapsed, decrement animation timer
bne @set_player_sprite_exit
jmp init_player_dec_num_lives ; init player variables to #$00, decrement number of lives, set game over if needed
@animation_timer_elapsed:
lda player_died_x_velocity_tbl,y ; which direction the player flies when killed
sta PLAYER_X_VELOCITY,x ; set player x velocity
lda PLAYER_AIM_DIR,x
cmp #$05 ; see if facing left or right
bcc @continue ; branch if facing right
lda PLAYER_DEATH_FLAG,x ; facing left, set bit 1 of PLAYER_DEATH_FLAG
ora #$02 ; set bit 1, so that player dies with head towards right
sta PLAYER_DEATH_FLAG,x ; update PLAYER_DEATH_FLAG
lda #$00 ; a = #$00
sec ; set carry flag in preparation for subtraction
sbc PLAYER_X_VELOCITY,x
sta PLAYER_X_VELOCITY,x
@continue:
lda PLAYER_Y_FAST_VELOCITY,x ; load player's fast y velocity
bmi @set_pos_and_sprite ; branch if negative y velocity (ascending/falling back)
cmp #$02 ; compare y fast velocity to #$02
bcc @set_pos_and_sprite ; branch if y fast velocity is less than #$02
lda PLAYER_HIDDEN,x ; 0 - visible; #$01/#$ff = invisible (any non-zero)
cmp #$01 ; see if player hidden
beq @set_animation_timer ; branch if player is hidden
jsr get_player_bg_collision_code ; player not hidden, get player background collision code
beq @set_pos_and_sprite ; branch if collision code #$00 (empty)
cmp #$02 ; see if collision code is #$02 (water)
beq @set_pos_and_sprite ; branch if in water
jsr set_player_landing_y_offset ; set SPRITE_Y_POS,x
@set_animation_timer:
lda #$40 ; a = #$40
sta PLAYER_ANIM_FRAME_TIMER,x ; set animation timer to #$40 frames
@set_pos_and_sprite:
jsr apply_gravity_set_y_pos ; increments y fractional velocity by #$23 (applying gravity) and sets y position
jsr calc_player_x_vel ; runs a series of checks to see if player's x velocity can be applied, and applies if possible
@set_player_sprite_exit:
jmp load_bank_2_set_player_sprite ; set player sprite based on player state, level, and animation sequence
; table for unknown ($03 bytes)
player_sprite_sequence_tbl:
.byte $04,$04,$06
; table for x velocities when dying (#$03 bytes)
; get screen type #$00 = outdoor, #$01 = indoor/base boss, #$02 = indoor/base
player_died_x_velocity_tbl:
.byte $ff ; outdoor
.byte $00 ; indoor/base boss
.byte $00 ; indoor/base
handle_player_state_calc_x_vel:
lda INDOOR_PLAYER_ADV_FLAG,x ; load whether player is walking between screens for indoor level
bne @continue ; branch if player is advancing between screens for indoor level
lda #$00 ; a = #$00
sta PLAYER_X_VELOCITY,x ; clear player x velocity to recalculate
sta INDOOR_TRANSITION_X_FRACT_VEL,x ; clear fractional x velocity for when walking between screens on indoor levels
@continue:
jsr handle_player_state
; runs a series of checks to see if player's x velocity can be applied, and applies if it possible
; e.g. checks if colliding with solid object, checks if in exiting water animation, checks if stuck due to boss screen limit
calc_player_x_vel:
lda PLAYER_X_VELOCITY,x ; load player X velocity
clc ; clear carry in preparation for addition
adc PLAYER_FAST_X_VEL_BOOST,x ; add any additional boost to velocity by being on a non-dangerous moving enemy
sta PLAYER_X_VELOCITY,x ; update player x velocity
lda PLAYER_WATER_STATE,x ; load player in water state flags
bmi @exit ; exit if player is walking out of water, don't want to stop animation
lda PLAYER_X_VELOCITY,x ; load player X velocity
ora INDOOR_TRANSITION_X_FRACT_VEL,x ; merge with fractional x velocity for when walking between screens on indoor levels
beq @exit ; exit if player isn't moving
lda PLAYER_X_VELOCITY,x ; player is moving, load player X velocity
bmi @player_negative_x_vel ; branch if player going left
lda BOSS_DEFEATED_FLAG ; player has positive velocity, see if currently executing post-boss defeated walking animation
bmi @set_scroll_apply_x_vel ; branch to apply velocity if part of end of level walk off screen animation
lda #$08 ; checking bg collision #$08 pixels to right of player
jsr check_player_solid_bg_collision ; see if player is about to collide with solid background object
bcs @exit ; exit if collided with solid background object, like lvl 1 boss screen plated door
jsr sty_level_screen_type ; get screen type #$00 = outdoor, #$01 = indoor/base boss, #$02 = indoor/base
lda SPRITE_X_POS,x ; load player's x position
cmp level_right_edge_x_pos_tbl,y ; compare player position to right edge of screen
bcs @exit ; don't apply velocity if player at the right edge
ldy BOSS_AUTO_SCROLL_COMPLETE ; see if boss reveal auto-scroll has completed (0 = not-complete, 1 = complete)
beq @set_scroll_apply_x_vel ; branch to apply velocity if auto scroll hasn't completed (or started)
ldy CURRENT_LEVEL ; auto-scroll has completed, load current level
cmp @lvl_boss_max_x_scroll_tbl,y ; compare player x position to the maximum x position for boss screen
bcs @exit ; exit if can't move past x position
@set_scroll_apply_x_vel:
jsr set_frame_scroll_if_appropriate ; set FRAME_SCROLL and PLAYER_FRAME_SCROLL if player is causing screen to scroll
jmp @apply_vel_to_player_x_pos
; table for maximum x position on boss screen to allow player to walk ($08 bytes)
; each byte is for each level
; for lvl 1, the x position isn't possible due to a solid bg object at #$88
; if you remove the collision code, the player won't walk past #$90
@lvl_boss_max_x_scroll_tbl:
.byte $90,$ff,$ff,$ff,$a0,$d0,$b0,$b0
; player is going left
@player_negative_x_vel:
lda #$f8 ; a = #$f8 (#$08 pixels to left of player)
jsr check_player_solid_bg_collision ; see if player is about to collide with solid background object
bcs @exit ; exit if collided with solid background object
lda BOSS_DEFEATED_FLAG ; 0 = boss not defeated, 1 = boss defeated
bmi @apply_vel_to_player_x_pos ; branch to apply velocity if part of end of level walk off screen animation
jsr sty_level_screen_type ; get screen type #$00 = outdoor, #$01 = indoor/base boss, #$02 = indoor/base
lda SPRITE_X_POS,x ; load player's x position
cmp level_left_edge_x_pos_tbl,y ; compare player position to left edge
bcc @exit ; exit if player too far to the left
@apply_vel_to_player_x_pos:
lda INDOOR_TRANSITION_X_ACCUM,x ; load the amount of x distance to move for single animation when moving between screens on indoor/base levels
clc ; clear carry in preparation for addition
adc INDOOR_TRANSITION_X_FRACT_VEL,x ; add to the fractional x velocity
sta INDOOR_TRANSITION_X_ACCUM,x ; store result back in accumulator
lda SPRITE_X_POS,x ; load player x position
adc PLAYER_X_VELOCITY,x ; add the player velocity (including any indoor transition velocity adjustment)
sta SPRITE_X_POS,x ; set new player x position
@exit:
rts
; table for x position of right edge of screen ($03 bytes)
; #$00 (outdoor) = #$e6
; #$01 (indoor/base boss) = #$e0
; #$02 (indoor/base) = #$d0
level_right_edge_x_pos_tbl:
.byte $e6,$e0,$d0
; table for x position of left edge of screen ($03 bytes)
; #$00 (outdoor) = #$1a
; #$01 (indoor/base boss) = #$20
; #$02 (indoor/base) = #$30
level_left_edge_x_pos_tbl:
.byte $1a,$20,$30
handle_player_state:
lda #$03 ; a = #$03
sta PLAYER_SPRITE_SEQUENCE,x
lda INDOOR_PLAYER_JUMP_FLAG,x ; see if engine has commanded the player to jump (set when entering new indoor screen)
beq @player_electrocution_check ; branch if no jump command specified
lda #$00 ; reset player jump command and set player jumping velocities
sta INDOOR_PLAYER_JUMP_FLAG,x ; reset player jump command
sta INDOOR_PLAYER_ADV_FLAG,x ; player is no longer walking between screens for indoor level, clear flag
jsr indoor_transition_end ; end player transition animation restore player position
jmp set_jump_status_and_y_velocity ; initialize PLAYER_JUMP_STATUS, animation frame index, and negative y velocity
@player_electrocution_check:
lda ELECTROCUTED_TIMER,x
beq @player_edge_fall_check
jmp update_indoor_electrocution ; decrement electrocution; if screen is cleared, stops electrocution
@player_edge_fall_check:
lda EDGE_FALL_CODE,x
beq @player_jump_check
jmp set_x_velocity_for_edge_fall_code
@player_jump_check:
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
beq @indoor_transition_anim_check ; branch if the player isn't jumping
jmp handle_jump ; player is jumping, jump
@indoor_transition_anim_check:
lda INDOOR_PLAYER_ADV_FLAG,x ; load whether player is walking between screens for indoor level
beq @handle_player_input
jmp indoor_transition_set_pos ; player is advancing between indoor screens, update player position for animation
@handle_player_input:
lda PLAYER_WATER_STATE,x ; see if player in water
bne handle_d_pad ; player not in water, read the controller input for d-pad input
lda CONTROLLER_STATE_DIFF,x ; load controller input
and #$80 ; check for A button pressed
beq handle_d_pad ; branch if a button not pressed to read the controller input
lda CONTROLLER_STATE,x ; load controller input
and #$07 ; keep bits .... .xxx (d-pad down, left, right)
cmp #$04 ; see if pressing the down d-pad button
bne set_jump_status_and_y_velocity ; branch if not only down is pressed
; to initialize PLAYER_JUMP_STATUS, animation frame index, and negative y velocity
lda #$02 ; only down button pressed,a = #$02 (sprite sequence to crouching)
sta PLAYER_SPRITE_SEQUENCE,x ; set sprite sequence to crouching
jsr can_player_drop_down ; determines if player can drop down (d-pad down and A)
bcs player_sprite_animation_exit ; branch if player cannot drop down (nothing below player to land on and not vertical level)
lda #$81 ; a = #$81
bne set_edge_fall_code ; always branch
; called when walked off ledge (not jumped)
; determine collision code and leave result in A
; collision code 0 - Empty
; collision code 1 - Floor
; collision code 2 - Water
; collision code 3 - Solid
walk_off_ledge:
lda PLAYER_AIM_DIR,x
cmp #$05 ; compare to crouched facing right
lda #$21 ; player is falling right off a ledge
bcc set_edge_fall_code ; branch if PLAYER_AIM_DIR,x is less than #$05, i.e. facing right
lda #$41 ; player is falling left off ledge
set_edge_fall_code:
sta EDGE_FALL_CODE,x
lda SPRITE_Y_POS,x
clc ; clear carry in preparation for addition
adc #$14 ; add #$14 to Y position
bcc @continue ; branch if no overflow
lda #$ff ; if overflow, just set #$ff
@continue:
sta PLAYER_FALL_X_FREEZE,x ; store updated Y position in $b8
player_sprite_animation_exit:
rts
; sets PLAYER_JUMP_STATUS based on facing direction, initializes animation frame index, and y velocity
; input
; * x - player index
set_jump_status_and_y_velocity:
lda PLAYER_AIM_DIR,x ; player animation frame
cmp #$05
lda #$91 ; a = #$91 (jumping left)
bcs @continue ; branch if facing left
lda #$11 ; a = #$11 (jumping right)
@continue:
sta PLAYER_JUMP_STATUS,x
lda #$00 ; a = #$00
sta PLAYER_ANIM_FRAME_TIMER,x ; reset player sprite index
sta PLAYER_ANIMATION_FRAME_INDEX,x
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
lsr
lda #$fb ; a = #$fb
ldy #$f0 ; y = #$f0
bcc @set_y_velocity ; branch if outdoor level (use y velocity -5.94)
lda #$fc ; indoor level, (y velocity -4.56), set fast velocity to #$fc (-4)
ldy #$90 ; set fractional y velocity to #$90 (.56)
@set_y_velocity:
sta PLAYER_Y_FAST_VELOCITY,x ; set y fast velocity to a
tya ; transfer fractional velocity to a
sta PLAYER_Y_FRACT_VELOCITY,x ; set y fractional velocity to a
rts
; reads the d-pad controller input and updates velocity appropriately
handle_d_pad:
lda CONTROLLER_STATE,x
lsr
bcs set_player_positive_x_velocity ; branch if d-pad right is pressed
lsr
bcs set_player_negative_x_velocity ; branch if d-pad left is pressed
ldy #$02 ; y = #$02 (crouching sprite sequence)
lsr
bcs set_sprite_sequence_to_y ; branch if down button is pressed to set sprite sequence to #$02
dey ; gun pointing up sprite sequence
lsr
bcs d_pad_up_pressed ; branch if up button is pressed
dey
set_sprite_sequence_to_y:
tya
; sets animation to show for the player
; * #$00 standing (no animation)
; * #$01 gun pointing up
; * #$02 crouching
; * #$03 walking or curled jump animation
; * #$04 dead animation
set_sprite_sequence_to_a:
sta PLAYER_SPRITE_SEQUENCE,x
rts
; d pad up button pressed by itself while not jumping
; on outdoor levels
; on indoor levels, check for electrocution (depends on if screen is cleared)
d_pad_up_pressed:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
lsr
bcc set_sprite_sequence_to_y ; branch for outdoor level
jsr set_sprite_sequence_to_y ; indoor level, set sprite sequence to y
lda INDOOR_SCREEN_CLEARED ; indoor level, check indoor screen cleared flag (0 = not cleared; 1 = cleared)
bne @indoor_screen_cleared ; branch if indoor screen is cleared
lda #$30 ; indoor screen not clear (has electric fence), set electrocution timer to #$30 frames
sta ELECTROCUTED_TIMER,x ; set timer for being electrocuted to #$30
lda #$1c ; a = #$1c (sound_1c - sound of electrocution)
jmp play_sound ; play sound
@indoor_screen_cleared:
stx $10 ; backup player index to $10
txa ; transfer player index to a
eor #$01 ; move to other player
tax ; transfer other player index to x
lda P1_GAME_OVER_STATUS,x ; load game over state for player x (1 = game over)
bne @check_players_advancing ; branch if other player is in game over to skip player state check
lda PLAYER_STATE,x ; both players alive, load player state (0 = dropping into level, 1 = normal, 2 = dead, 3 = can't move)
cmp #$01 ; compare to normal state
bne set_player_standing_sprite_sequence ; branch if not in normal state
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
bne set_player_standing_sprite_sequence ; branch if jumping
@check_players_advancing:
ldx #$01 ; start loop with player 2
@check_player_x_advancing:
lda #$05 ; a = #$05
sta PLAYER_SPRITE_SEQUENCE,x ; player animation frame
lda INDOOR_PLAYER_ADV_FLAG,x ; load whether player is walking between screens for indoor level
bne @check_next_player ; branch to move to next player if player x isn't walking between screens
lda #$01 ; player is walking between screens, a = #$01
sta INDOOR_SCROLL ; set indoor scroll to #$01
sta INDOOR_PLAYER_ADV_FLAG,x ; set flag indicating player is walking between screens for indoor level
lda #$00 ; a = #$00
sta PLAYER_ANIMATION_FRAME_INDEX,x ; initialize animation for player walking into screen
sta PLAYER_ANIM_FRAME_TIMER,x ; initialize timer delay between frames of animation of walking into screen
jsr set_player_advancing_vel ; set the x and y velocities and other variables to initiate the player advancing into screen
@check_next_player:
dex ; move to player 1
bpl @check_player_x_advancing
ldx $10 ; restore current player index to x
rts
set_player_standing_sprite_sequence:
ldx $10 ; load player index
lda #$00 ; a = #$00, standing (no animation)
beq set_sprite_sequence_to_a ; sets player animation sequence to standing
; sets the player's X velocity to a
set_player_positive_x_velocity:
lda #$01 ; a = #$01
bne set_player_x_vel_to_a ; always jump, set X velocity to #$01
; facing left
set_player_negative_x_velocity:
lda #$ff ; a = #$ff
set_player_x_vel_to_a:
ldy PLAYER_WATER_STATE,x ; see if player is in water
beq @continue ; branch if animation is #$00 (not in water)
sta $08 ; player in water, set player X velocity in $08 temporarily
lda CONTROLLER_STATE,x ; see what buttons are being pressed
and #$04 ; see if d-pad has down direction (among others) pressed (down, down right, down left)
bne @exit ; don't set x velocity if down button is pressed (don't allow moving in water while looking down)
lda $08 ; restore desired player X velocity
@continue:
sta PLAYER_X_VELOCITY,x
@exit:
rts
; set y to level screen type
; output
; y - screen type: #$00 = outdoor level, #$01 = indoor boss level screen, #$02 = indoor level
sty_level_screen_type:
ldy #$00 ; y = #$00
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor/base
beq @exit ; y = #$0 for outdoor level, exit
iny ; indoor level set y = #$01
asl
bcs @exit ; exit if indoor boss screen with y = #$01
iny ; indoor non-boss screen set y = #$02
@exit:
rts
set_x_velocity_for_edge_fall_code:
lda SPRITE_Y_POS,x
cmp PLAYER_FALL_X_FREEZE,x ; load #$14 from where player starting falling
bcc @off_ledge_start ; branch if beginning of fall off/through ledge
jsr get_player_bg_collision_code ; get player background collision code
beq @off_ledge_start ; if collision code set was #$00 (empty), branch
jsr set_player_landing_y_offset ; set SPRITE_Y_POS,x
jmp player_land_on_ground
; can't adjust x velocity for small amount of time after walking off ledge
@off_ledge_start:
jsr apply_gravity_set_y_pos ; increments y fractional velocity by #$23 (applying gravity) and sets y position
lda SPRITE_Y_POS,x ; load player y position
cmp PLAYER_FALL_X_FREEZE,x
bcc @set_x_velocity
jsr get_x_velocity_d_pad_code ; see if left or right d-pad button is pressed
beq @set_x_velocity ; branch if neither were pressed
sta $08 ; store #$20 for left d-pad, #$40 for right d-pad in $08
lda EDGE_FALL_CODE,x
and #$9f ; keep bits x..x xxxx
ora $08 ; update EDGE_FALL_CODE based on d-pad input
sta EDGE_FALL_CODE,x
@set_x_velocity:
lda EDGE_FALL_CODE,x
jmp set_x_velocity_from_a_code
handle_jump:
lda PLAYER_Y_FAST_VELOCITY,x ; load player y fast velocity
bmi @check_collision_above ; branch if player is still ascending
ldy LEVEL_LOCATION_TYPE ; player descending, load location type (0 = outdoor; 1 = indoor)
bne @check_collision ; branch for indoor level
cmp #$01 ; outdoor level, see if fast velocity is #$01
bcc @apply_gravity ; branch if either at apex of jump, or just beginning descent
lda PLAYER_Y_FAST_VELOCITY,x ; player y fast velocity >= #$01, reload y fast velocity value
; !(HUH) already in a register, lda instruction not needed
cmp #$04 ; related to ground collision test
bcs @check_collision ; branch if velocity is greater than or equal to #$04 (fast falling)
lda SPRITE_Y_POS,x ; y velocity less than #$04, load player y position on screen
clc ; clear carry in preparation for addition
adc VERTICAL_SCROLL ; add vertical scroll offset
and #$0f ; keep bits .... xxxx
cmp #$08 ; see if result ends in #$08
bcs @apply_gravity ; branch to check collision every #$08 pixels
@check_collision:
jsr get_player_bg_collision_code ; get player background collision code
beq @apply_gravity ; if collision code set was #$00 (empty), branch
jsr set_player_landing_y_offset ; set SPRITE_Y_POS,x
jsr @set_jump_status_from_input
jmp player_land_on_ground
@check_collision_above:
lda BOSS_DEFEATED_FLAG ; 0 = boss not defeated, 1 = boss defeated
bmi @apply_gravity ; branch if boss already defeated
lda SPRITE_Y_POS,x
clc ; clear carry in preparation for addition
adc #$f6 ; subtract #$0a from y position
tay ; set y position for bg collision check
lda SPRITE_X_POS,x ; load x position for bg collision check
jsr get_bg_collision ; determine player background collision code at position (a,y)
bpl @apply_gravity ; branch if not empty collision code (floor, water, solid)
lda #$00 ; a = #$00
sta PLAYER_Y_FAST_VELOCITY,x
sta PLAYER_Y_FRACT_VELOCITY,x
@apply_gravity:
jsr apply_gravity ; increments y fractional velocity by #$23 (applying gravity)
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
beq @set_y_pos ; branch if horizontal level to to set the y position based on velocity
jsr set_frame_scroll_if_appropriate ; vertical level, set FRAME_SCROLL and PLAYER_FRAME_SCROLL if player is causing screen to scroll
bcs @set_jump_status_from_input ; branch if vertical FRAME_SCROLL was set
@set_y_pos:
jsr player_jumping_set_y_pos ; set player y position based on PLAYER_JUMP_COEFFICIENT and velocity
@set_jump_status_from_input:
jsr get_x_velocity_d_pad_code ; see if left or right d-pad button is pressed
beq @set_x_velocity ; branch if neither were pressed
sta $08 ; store #$20 for right d-pad, #$40 for left d-pad in $08
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
and #$9f ; keep bits x..x xxxx
ora $08 ; update EDGE_FALL_CODE based on d-pad input
sta PLAYER_JUMP_STATUS,x
@set_x_velocity:
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
; a is EDGE_FALL_CODE, or PLAYER_JUMP_STATUS
; bit 7 set specifies negative velocity
; bit 6 set specifies positive velocity
set_x_velocity_from_a_code:
asl ; shift a left
bpl @continue ; branch if a is not negative
jmp set_player_negative_x_velocity ; result was negative, player facing left, jump
@continue:
asl
bpl x_velocity_exit ; branch if a is not negative
jmp set_player_positive_x_velocity ; set player X velocity to modified EDGE_FALL_CODE
; set a to #$20 (right), #$40 (left), or #$00 (neither) based on d-pad
get_x_velocity_d_pad_code:
ldy #$00 ; y = #$00
lda CONTROLLER_STATE,x ; load controller state
lsr
bcc @test_left_d_pad ; branch if not pressing right d-pad button
ldy #$20 ; y = #$20
@test_left_d_pad:
lsr
bcc @continue
ldy #$40 ; y = #$40
@continue:
tya
x_velocity_exit:
rts
; decrements player electrocution
; if screen is cleared, stops electrocution
update_indoor_electrocution:
lda #$01 ; a = #$01
sta PLAYER_SPRITE_SEQUENCE,x ; player animation frame
dec ELECTROCUTED_TIMER,x ; counter for electrocution
lda INDOOR_SCREEN_CLEARED ; indoor screen cleared flag (0 = not cleared; 1 = cleared)
beq @exit ; exit if indoor screen is not cleared
lda #$00 ; screen cleared; stop electrocution
sta ELECTROCUTED_TIMER,x ; counter for electrocution
@exit:
rts
; indoor transition, update player position
indoor_transition_set_pos:
lda #$05 ; a = #$05
sta PLAYER_SPRITE_SEQUENCE,x ; player animation frame
lda PLAYER_JUMP_COEFFICIENT,x ; load player's jump modifier (alters height of jump)
; used when animating walking into screen for indoor levels to keep track of overflows
; to adjust y position
clc ; clear carry in preparation for addition
adc INDOOR_TRANSITION_Y_FRACT_VEL,x ; add fractional y velocity for animation to 'accumulator'
sta PLAYER_JUMP_COEFFICIENT,x ; set new 'accumulator' value
lda SPRITE_Y_POS,x ; player y position on screen
adc INDOOR_TRANSITION_Y_FAST_VEL,x ; add (or subtract) y position fast velocity to y position for advancing animation
; including any overflow from the fractional velocity
sta SPRITE_Y_POS,x ; set new y position
lda INDOOR_SCROLL ; see if scrolling (0 = not scrolling; 1 = scrolling, 2 = finished scrolling)
cmp #$02
bcc indoor_transition_exit
; end player transition animation restore player position
indoor_transition_end:
lda #$00 ; a = #$00
sta INDOOR_PLAYER_ADV_FLAG,x ; player is no longer walking between screens for indoor level, clear flag
lda PLAYER_INDOOR_ANIM_Y,x ; load y position when player started advancing into screen (#$a8)
sta SPRITE_Y_POS,x ; restore y position from beginning of advancing animation
lda PLAYER_INDOOR_ANIM_X,x ; load x position when player started advancing into screen
sta SPRITE_X_POS,x ; restore x position from beginning of advancing animation
indoor_transition_exit:
rts
player_land_on_ground:
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
ora EDGE_FALL_CODE,x
beq @check_aim_dir ; brach if both are #$00
lda #$00 ; a = #$00
sta PLAYER_ANIMATION_FRAME_INDEX,x
sta PLAYER_ANIM_FRAME_TIMER,x
sta PLAYER_FALL_X_FREEZE,x
lda #$03 ; a = #$03 (sound_03)
jsr play_sound ; play player landing sound
@check_aim_dir:
lda PLAYER_AIM_DIR,x
cmp #$05 ; see if player is facing right
lda PLAYER_SPRITE_FLIP,x ; load player sprite horizontal and vertical flip flags
and #$3f ; reset sprite flip data (clear bits 6 and 7)
bcc @continue ; branch if player is facing right
ora #$40 ; player facing left, flip sprite horizontally
@continue:
sta PLAYER_SPRITE_FLIP,x ; store whether sprite is flipped horizontally and/or vertically
init_player_data:
lda #$00 ; a = #$00
sta PLAYER_JUMP_STATUS,x
sta EDGE_FALL_CODE,x
sta PLAYER_SPRITE_SEQUENCE,x
lda #$00 ; a = #$00
sta PLAYER_Y_FRACT_VELOCITY,x
sta PLAYER_Y_FAST_VELOCITY,x
sta INDOOR_TRANSITION_Y_FRACT_VEL,x
sta INDOOR_TRANSITION_Y_FAST_VEL,x
rts
; set the x and y velocities and a few other variables to initiate the player advancing into screen
set_player_advancing_vel:
lda #$00 ; a = #$00
sta $12 ; negate the resulting velocities
lda #$58 ; a = #$58 (speed code - affects speed when walking up)
jsr set_vel_for_speed_code_a ; set fast ($0f) and fractional ($0e) y velocities for speed code a
lda $0f ; load fast y velocity
sta INDOOR_TRANSITION_Y_FAST_VEL,x ; set indoor transition y fast velocity
lda $0e ; load fractional y velocity
sta INDOOR_TRANSITION_Y_FRACT_VEL,x ; set indoor transition y fractional velocity
lda SPRITE_Y_POS,x ; load player y position
sta PLAYER_INDOOR_ANIM_Y,x ; set y position the player was at when started walking into screen
; pretty sure always #$a8 since y pos is hard-coded for indoor levels
lda SPRITE_X_POS,x ; load player x position
sta PLAYER_INDOOR_ANIM_X,x ; set x position the player was at when started walking into screen
sec ; set carry flag in preparation for subtraction
sbc #$80 ; subtract #$80 from PLAYER_INDOOR_ANIM_X,x
sta $12 ; store whether to have negative velocity (walk left) based on x position
; when player on right half of screen, player will advance inward towards left
; when player on left half of screen, player will advance inward towards right
bcs @continue ; branch if on right half of the screen
eor #$ff ; player on left half of the screen, take negative x position and make positive
adc #$01 ; flip all bits and add #$01
@continue:
jsr set_vel_for_speed_code_a ; set fast ($0f) and fractional ($0e) x velocities for speed code a
lda $0f ; load fast x velocity
sta PLAYER_X_VELOCITY,x ; set x fast velocity
lda $0e ; load fractional x velocity
sta INDOOR_TRANSITION_X_FRACT_VEL,x ; set indoor transition x fractional velocity
rts
; for a given value a, set fast ($0f) and fractional ($0e) velocities
; (a is rotated #$07 times into $0e)
; negate final results if $12 is non-negative
; * a - sort-of speed code, this value is split into fast and fractional velocity
; * $12 - when greater than or equal to #$00, specifies to negate the resulting velocities
; output
; * $0e - x or y fractional velocity
; * $0f - x or y fast velocity
set_vel_for_speed_code_a:
sta $0f ; store speed code in $0f
lda #$00 ; a = #$00
sta $0e ; set $0e to #$00
ldy #$07 ; set number of bits to rotate speed code to #$07
; for a given value $0f, set fast ($0f) and fractional ($0e) velocities based on y
; negate final results if $12 is greater than or equal to #$00
; also used directly for indoor bullets
; * $0f - a sort-of speed code, this value is split into fast and fractional velocity based on y
; * y - number of bits to rotate $0f into fractional velocity $0e (#$05, #$06, or #$07)
; * $12 - when greater than or equal to #$00, specifies to negate the resulting velocities
; output
; * $0e - x or y fractional velocity
; * $0f - x or y fast velocity
set_vel_for_speed_vars:
lsr $0f ; shift bit 0 to carry
ror $0e ; push bit 0 of $0f into bit 7
dey ; decrement y
bne set_vel_for_speed_vars ; continue to shift the next bit into the fractional velocity
lda $12 ; load whether to negate calculated velocity
bpl @negate_bullet_velocities ; branch when $12 is greater than or equal to #$00 to negative velocity
rts
@negate_bullet_velocities:
lda #$00 ; a = #$00
sec ; set carry flag in preparation for subtraction
sbc $0e ; subtract the fractional velocity from #$00 (negate)
sta $0e ; set negated fractional x or y bullet velocity
lda #$00 ; a = #$00
sbc $0f ; subtract the fast velocity from #$00 (negate)
sta $0f ; set negated fast x or y bullet velocity
rts
; when landing, sets the player sprite Y position
set_player_landing_y_offset:
jsr sty_level_screen_type ; determine if screen needs to account for vertical offset
lda landing_y_position_tbl,y ; load vertical scroll offset
bne @set_sprite_y ; branch if hard-coded y landing position (indoor/base level, indoor/base boss screen)
lda VERTICAL_SCROLL ; load vertical scroll offset
and #$0f ; only care about low nibble (vertical offset within the nametable)
ora #$f0 ; set bits xxxx ....
sta $00
clc ; clear carry in preparation for addition
adc SPRITE_Y_POS,x ; add offset to sprite position
and #$f0 ; keep bits xxxx ....
sec ; set carry flag in preparation for subtraction
sbc $00
clc ; clear carry in preparation for addition
adc #$04 ; move player position down by #$04 since landing from fall/jump
@set_sprite_y:
sta SPRITE_Y_POS,x ; set sprite position
rts
; table for landing y position (#$03 bytes)
; when #$00 vertical offset and current position are taken into consideration
landing_y_position_tbl:
.byte $00,$c9,$a8
; calculates the player aim direction based on d-pad input, facing direction, and jump status
set_player_aim_for_input:
ldy #$20 ; y = #$20 (row 2 of d_pad_player_aim_tbl)
lda PLAYER_JUMP_STATUS ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
beq @continue ; branch if not jumping
ldy #$30 ; jumping, set y = #$30 (row 3 of d_pad_player_aim_tbl)
@continue:
lda $08 ; load level_spawn_position_index value for level
cmp #$03 ; see if indoor boss screen
beq @set_player_aim ; branch if indoor boss screen
ldy #$00 ; assume player facing facing right
lda PLAYER_AIM_PREV_FRAME,x ; see which direction was the last frame
cmp #$05 ; compare to player crouching facing left
bcc @set_player_aim ; branch if facing left
ldy #$10 ; y = #$10 (row 1 of d_pad_player_aim_tbl)
@set_player_aim:
sty $08 ; set $08 to #$00, #$10, #$20, or #$30 depending on facing direction and jump status
lda CONTROLLER_STATE,x ; read controller state
and #$0f ; keep bits .... xxxx (d pad input)
clc ; clear carry in preparation for addition
adc $08 ; add d-pad input to $08
tay ; transfer
lda d_pad_player_aim_tbl,y ; load the aim direction based on the d-pad input
sta PLAYER_AIM_DIR,x ; set new player aim direction
rts
; table for player aim direction code (#$40 bytes)
; each row is a type of aiming depending on player state and level type
; each byte offset represents a d-pad direction
; below is an example for 0th row (facing right)
; * d-pad value #$00 - no input - #$02 - facing right aiming up
; * d-pad value #$01 - r - #$02 - facing right
; * d-pad value #$02 - l - #$07 - facing left
; * d-pad value #$03 - l and r - #$02 - facing right
; * d-pad value #$04 - d - #$05 - crouch facing right
; * d-pad value #$05 - d and r - #$03 - aim down right
; * d-pad value #$06 - d and l - #$06 - aim down left
; * d-pad value #$07 - d l and r - #$02 - facing right
; * d-pad value #$08 - u - #$00 - facing right aiming up
; * d-pad value #$09 - u and r - #$01 - aiming up and right
; * d-pad value #$0a - u and l - #$08 - aiming up and left
; * d-pad value #$0b - l r and u - #$02 - facing right
; * d-pad value #$0c - u and d - #$07 - facing left
; * d-pad value #$0d - u d and r - #$02 - facing right
; * d-pad value #$0e - u d and l - #$07 - facing left
; * d-pad value #$0f - u d l and r - #$02 - facing right
d_pad_player_aim_tbl:
.byte $02,$02,$07,$02,$04,$03,$06,$02,$00,$01,$08,$02,$07,$02,$07,$02 ; standing facing right
.byte $07,$02,$07,$02,$05,$03,$06,$02,$09,$01,$08,$02,$07,$02,$07,$02 ; facing left
.byte $00,$02,$07,$00,$00,$02,$07,$02,$00,$01,$08,$02,$07,$02,$07,$02
.byte $00,$02,$07,$00,$0a,$03,$06,$02,$00,$01,$08,$02,$07,$02,$07,$02 ; jumping and indoor boss
; see if player should check for walking off ledge and if so, walk off it
check_player_ledge:
lda PLAYER_ON_ENEMY,x ; see if player is on non-dangerous enemy, e.g. (#$14, #$15 - mining cart, #$10 - floating rock platform)
bne @clear_edge_code_exit ; branch if player is on top non-dangerous enemy
lda PLAYER_WATER_STATE,x ; player not on enemy, load player water state
ora PLAYER_JUMP_STATUS,x ; merge with player jump status
ora EDGE_FALL_CODE,x ; merge with edge fall code
ora INDOOR_PLAYER_ADV_FLAG,x ; merge with whether or not the player is walking between screens for indoor level
bne @exit ; exit if any of the previous variables were non-zero
lda PLAYER_BG_FLAG_EDGE_DETECT,x ; see if should detect falling off ledge or skip
lsr
bcs @clear_edge_code_exit
jsr get_player_bg_collision_code ; get player background collision code
beq jmp_walk_off_ledge ; collision code is #$00 (empty), player is not on ground, fall
cmp #$02 ; see if player is in water (collision code #$02)
beq init_PLAYER_WATER_STATE ; if in water, initialize player in water animation
@clear_edge_code_exit:
lda #$00
sta EDGE_FALL_CODE,x
@exit:
rts
init_PLAYER_WATER_STATE:
lda #$01
sta PLAYER_WATER_STATE,x ; initialize player in water animation
rts
jmp_walk_off_ledge:
jmp walk_off_ledge
; retrieves the collision code of the player and sets it in register a
; output
; * carry flag set when on floor
; #$00 - empty
; #$01 - floor
; #$02 - water
; #$80 - solid (not 3 like normal)
get_player_bg_collision_code:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor; #$80 if indoor boss screen
bmi @indoor_boss_level ; branch if indoor boss level
bne @indoor_floor ; non-indoor boss level, determine max Y position
lda SPRITE_Y_POS,x ; outdoor level, load player y position on screen
clc ; clear carry in preparation for addition
adc #$10 ; add #$10 to player Y position
bcs @exit_code_0 ; branch if overflow
tay ; transfer player y position to the y register
lda SPRITE_X_POS,x ; player x position on screen
jmp get_bg_collision ; not indoor nor indoor boss level, use get_bg_collision to get collision code
@indoor_floor:
lda #$a0 ; indoor non-boss levels have a hard-coded max Y value of #$a0
bne @indoor_continue ; always branch
@indoor_boss_level:
lda #$c8 ; lowest Y value for indoor boss level is hard-coded #$c8
@indoor_continue:
cmp SPRITE_Y_POS,x ; compare max Y to y position on screen
lda #$01 ; prep collision code to #$01 (floor) if object is not at bottom of screen (walking to next screen)
bcc @exit ; branch if not at bottom of screen
@exit_code_0:
lda #$00 ; collision code #$00 (not colliding with anything, or bottom of screen)
@exit:
rts
; increments y fractional velocity by #$23 (applying gravity) and then sets y position
; based on y velocity and PLAYER_JUMP_COEFFICIENT
apply_gravity_set_y_pos:
jsr apply_gravity ; increments y fractional velocity by #$23 (applying gravity)
; player is jumping, set player y position based on PLAYER_JUMP_COEFFICIENT and velocity
player_jumping_set_y_pos:
lda PLAYER_Y_FAST_VELOCITY,x ; load player's fast y velocity
asl ; shift negative bit to carry flag
lda #$00 ; player falling down, or not falling (0 velocity)
bcc @continue ; branch if player is moving down (down or #$00 y velocity)
lda #$ff ; player moving up
@continue:
sta $08 ; store either #$00 or #$ff in $08
lda PLAYER_JUMP_COEFFICIENT,x ; load player's jump modifier (alters height of jump)
clc ; clear carry in preparation for addition
adc PLAYER_Y_FRACT_VELOCITY,x ; add to player's fractional y velocity
sta PLAYER_JUMP_COEFFICIENT,x ; update player's jump modifier (alters height of jump)
lda SPRITE_Y_POS,x ; load player's y position
adc PLAYER_Y_FAST_VELOCITY,x ; add (subtract) the fast y velocity (with jump coefficient overflow)
sta SPRITE_Y_POS,x ; set player's new y position
lda PLAYER_HIDDEN,x ; 0 - visible; #$01/#$ff = invisible (any non-zero)
adc $08 ; add or subtract #$01 to specify whether player is visible (with any overflow)
sta PLAYER_HIDDEN,x ; set whether to draw player sprite, not sure how this is really supposed to be used
rts
; increments y fractional velocity by #$23 (applying gravity)
apply_gravity:
clc
lda PLAYER_Y_FRACT_VELOCITY,x ; load player's y fractional velocity
adc #$23 ; add #$23 to y fractional velocity
sta PLAYER_Y_FRACT_VELOCITY,x ; update player's y fractional velocity
lda PLAYER_Y_FAST_VELOCITY,x ; load player's fast y velocity
adc #$00 ; add carry into high byte (any overflow when adding to fractional y velocity)
sta PLAYER_Y_FAST_VELOCITY,x ; update player's fast y velocity
rts
; player death
init_player_dec_num_lives:
jsr init_player_and_weapon
sta PLAYER_STATE,x ; set player state to #$00 (falling into level)
lda P1_NUM_LIVES,x ; load player number of lives
beq @set_game_over ; branch if no more lives
dec P1_NUM_LIVES,x ; decrement player number of lives
rts
@set_game_over:
lda #$01 ; a = #$01
sta P1_GAME_OVER_STATUS,x ; game over state of player (1 = game over)
rts
; sets FRAME_SCROLL and PLAYER_FRAME_SCROLL if player is causing screen to scroll
set_frame_scroll_if_appropriate:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
ora AUTO_SCROLL_TIMER_00 ; merge with AUTO_SCROLL_TIMER_00
ora AUTO_SCROLL_TIMER_01 ; merge with AUTO_SCROLL_TIMER_01
bne @exit ; if indoor, or any auto scroll timer running, exit
lda PLAYER_STATE,x ; load player state (0 = dropping into level, 1 = normal, 2 = dead, 3 = can't move)
cmp #$01 ; see if player in normal state
bne @exit ; exit if in normal state
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
bne set_vertical_level_frame_scroll ; branch if vertical scrolling
lda LEVEL_STOP_SCROLL ; horizontal, indoor/base level, load screen to stop scrolling at
bmi @exit ; exit if boss auto scroll set (LEVEL_STOP_SCROLL is #$ff when boss auto scroll started)
ldy PLAYER_GAME_OVER_BIT_FIELD ; #$00 = p1 not game over, p2 game over (or not playing)
; #$01 = p1 game over, p2 not game over, #$02 = p1 nor p2 are in game over
lda SPRITE_X_POS,x ; load player x position
cmp horizontal_scroll_point_tbl,y ; load horizontal scroll point position
bcc @exit ; if not yet reached point to initiate horizontal scroll, exit
cpy #$02 ; see if both players are active
bne @set_horizontal_level_frame_scroll ; if both players aren't active, begin setting scroll
txa ; both players active, see if other player is preventing screen scroll, transfer player index to a
eor #$01 ; swap to other player
tay ; transfer player index to y
lda SPRITE_X_POS,y ; load other player's x position
cmp #$21 ; compare other player to left edge of screen
bcc @stop_player_x_velocity ; don't scroll if other player is preventing it by being too far to the left
; sets FRAME_SCROLL if needed on horizontal levels
; sees if player is causing scroll
; if so sets FRAME_SCROLL to the right value based on Y velocity and PLAYER_JUMP_COEFFICIENT
; output
; * carry flag - #$01 set when scroll initiated
@set_horizontal_level_frame_scroll:
jsr set_boss_auto_scroll ; starts the auto scroll to reveal boss if at right screen, otherwise do nothing
beq @exit ; branch if boss auto scroll started
lda INDOOR_TRANSITION_X_FRACT_VEL,x ; load player's indoor x velocity (#$00 unless on indoor/base level)
clc ; clear carry in preparation for addition
adc INDOOR_TRANSITION_X_ACCUM,x
sta INDOOR_TRANSITION_X_ACCUM,x ; update INDOOR_TRANSITION_X_ACCUM with increased velocity
lda PLAYER_X_VELOCITY,x ; load player X velocity
adc #$00 ; incorporate any velocity overflow from INDOOR_TRANSITION_X_ACCUM
sta FRAME_SCROLL ; set screen scroll amount
lda #$01 ; a = #$01
sta PLAYER_FRAME_SCROLL,x ; set player scroll amount for player causing scroll
lda #$00 ; a = #$00
sta INDOOR_TRANSITION_X_FRACT_VEL,x ; clear player indoor velocity
sta PLAYER_X_VELOCITY,x ; clear player x velocity
sec ; set carry flag
rts
@stop_player_x_velocity:
lda #$00 ; a = #$00
sta INDOOR_TRANSITION_X_FRACT_VEL,x
sta PLAYER_X_VELOCITY,x
@exit:
clc
rts
; load the x point on horizontal levels to start scrolling the screen
; when 1 player mode, or only 1 player alive, it's the middle of the screen (#$80)
; for 2 active players, it's 70% of the screen (#$b0)
; byte 0 = p1 not game over, p2 game over (or not playing)
; byte 1 = p1 game over, p2 not game over
; byte 2 = p1 nor p2 are in game over
horizontal_scroll_point_tbl:
.byte $80,$80,$b0
; sets FRAME_SCROLL if needed on vertical level
; sees if player is jumping up and high enough to cause scrolling
; if so sets FRAME_SCROLL to the right value based on Y velocity and PLAYER_JUMP_COEFFICIENT
; output
; * carry flag - #$01 set when scroll initiated, #$00 when scroll not initiated
set_vertical_level_frame_scroll:
lda SPRITE_Y_POS,x ; load player y position
cmp #$50 ; compare to #$50
bcs @exit ; exit if player y position > #$50, i.e. far down the screen
; once player is above #$50, scrolling up is initiated
lda PLAYER_Y_FAST_VELOCITY,x ; load player's y fast velocity byte
bpl @exit ; exit if player falling down
jsr set_boss_auto_scroll ; starts the auto scroll to reveal boss if at right screen, otherwise do nothing
beq @exit ; branch if boss auto scroll started
lda PLAYER_JUMP_COEFFICIENT,x ; player is jumping up, load player's jump modifier (alters height of jump)
clc ; clear carry in preparation for addition
adc PLAYER_Y_FRACT_VELOCITY,x ; add the player's fractional velocity to the jump modifier
sta PLAYER_JUMP_COEFFICIENT,x ; update player's jump modifier (alters height of jump)
lda SPRITE_Y_POS,x ; re-load player y position
adc PLAYER_Y_FAST_VELOCITY,x ; add player y fast velocity
sta $08 ; store sum in $08
lda SPRITE_Y_POS,x ; re-load player y position
sec ; set carry flag in preparation for subtraction
sbc $08 ; subtract one frame of distance of the jump
sta FRAME_SCROLL ; store scroll amount
lda #$01 ; a = #$01
sta PLAYER_FRAME_SCROLL,x ; set player scroll amount for player causing scroll
sec ; set carry flag
rts
@exit:
clc
rts
; determines if player can drop down (d-pad down and A)
; player can drop down when solid, water, or floor bg collision below the current player
; vertical levels always allow drop down regardless if collision below player
; input
; * x - player index
; output
; * carry flag - clear when player can "drop down" (d-pad down + A)
; i.e. there is a solid, water, or floor collision below the player
; set when player cannot "drop down" (d-pad down + A)
can_player_drop_down:
lda LEVEL_STOP_SCROLL ; load the screen to stop scrolling on, set to #$ff when boss auto scroll starts
cmp #$ff ; see if boss auto scroll has started
beq @continue ; branch if auto scroll has started
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
bne collision_below_player ; clear carry and exit for vertical level, always can drop down
@continue:
lda SPRITE_Y_POS,x ; load player y position
clc ; clear carry in preparation for addition
adc #$10 ; prepare to move player down by #$10 pixels
; determines if solid, water, or floor bg collision below the player (all the way to the bottom of screen)
; input
; * a - y position to test
; * x - player offset
; output
; * a - collision code
; * carry flag - set when only empty collision codes below player, i.e. there is something to land on
; set when solid, water, or ground beneath player
check_collision_below:
tay ; transfer y position to y register
lsr
lsr
lsr
lsr
sta $08 ; store high nibble of player y position in $08
lda SPRITE_X_POS,x ; load player x position
jsr get_bg_collision_far ; determine player background collision code for (a, y) position
bmi set_carry_exit ; exit if collision with solid bg element, can't drop down
; loops down to bottom of screen looking for a bg collision
@loop:
inc $08 ; increment y position high byte, i.e. bg collision row
lda $08 ; load y position high byte
cmp #$0e ; compare to last bg collision row
bcs can_player_drop_down_exit ; exit if checked all rows below player
lda $13 ; load BG_COLLISION_DATA offset
and #$c0 ; keep bits xx.. ....
sta $17
lda $13 ; re-load BG_COLLISION_DATA offset
clc ; clear carry in preparation for addition
adc #$04 ; move down one row
and #$3f ; keep bits ..xx xxxx
ora $17 ; determine final BG_COLLISION_DATA
tay ; move to BG_COLLISION_DATA
jsr read_bg_collision_byte_unsafe ; get collision code from BG_COLLISION_DATA byte
beq @loop ; loop if no collision, collision code #$00 (empty)
collision_below_player:
clc ; clear when found non-empty collision
can_player_drop_down_exit:
rts
set_carry_exit:
sec ; set carry flag
rts
init_player_and_weapon:
lda #$00 ; a = #$00
sta P1_CURRENT_WEAPON,x ; reset current player's weapon
init_player_attributes:
lda #$00 ; a = #$00
sta CPU_SPRITE_BUFFER,x
sta SPRITE_Y_POS,x
sta SPRITE_X_POS,x
sta SPRITE_ATTR,x
sta INDOOR_TRANSITION_X_ACCUM,x
sta PLAYER_JUMP_COEFFICIENT,x
sta INDOOR_TRANSITION_X_FRACT_VEL,x
sta PLAYER_X_VELOCITY,x
sta INDOOR_TRANSITION_Y_FRACT_VEL,x
sta INDOOR_TRANSITION_Y_FAST_VEL,x
sta PLAYER_ANIM_FRAME_TIMER,x
sta PLAYER_JUMP_STATUS,x
sta PLAYER_FRAME_SCROLL,x
sta EDGE_FALL_CODE,x
sta PLAYER_ANIMATION_FRAME_INDEX,x
sta PLAYER_INDOOR_ANIM_Y,x
sta PLAYER_M_WEAPON_FIRE_TIME,x
sta NEW_LIFE_INVINCIBILITY_TIMER,x
sta INVINCIBILITY_TIMER,x
sta PLAYER_WATER_STATE,x
sta PLAYER_DEATH_FLAG,x
sta PLAYER_ON_ENEMY,x
sta PLAYER_FALL_X_FREEZE,x
sta PLAYER_HIDDEN,x
sta PLAYER_SPRITE_SEQUENCE,x
sta PLAYER_INDOOR_ANIM_X,x
sta PLAYER_AIM_PREV_FRAME,x
sta PLAYER_AIM_DIR,x
sta PLAYER_Y_FRACT_VELOCITY,x
sta PLAYER_Y_FAST_VELOCITY,x
sta ELECTROCUTED_TIMER,x
sta INDOOR_PLAYER_JUMP_FLAG,x
sta PLAYER_WATER_TIMER,x
sta PLAYER_RECOIL_TIMER,x
sta INDOOR_PLAYER_ADV_FLAG,x
sta PLAYER_SPRITE_CODE,x
sta PLAYER_SPRITE_FLIP,x
sta PLAYER_BG_FLAG_EDGE_DETECT,x
rts
; on player x position change
; input
; * a - amount to add to player x position
; * x - player offset
; output
; * carry flag - set when collided with solid object
check_player_solid_bg_collision:
clc ; clear carry in preparation for addition
adc SPRITE_X_POS,x ; add a to player x position
sta $0a ; store x position in $0a
ldy #$0b ; default amount to subtract from player y position
lda PLAYER_SPRITE_CODE,x ; load player sprite
cmp #$17 ; compare to player prone sprite
bne @continue ; branch if player isn't prone
ldy #$00 ; player is prone, don't subtract from player y position
@continue:
sty $08 ; set amount to subtract from player y position
lda PLAYER_HIDDEN,x ; 0 = visible; #$01/#$ff = invisible (any non-zero)
bne @continue_2 ; branch if invisible
lda SPRITE_Y_POS,x ; load player y position
sec ; set carry flag in preparation for subtraction
sbc $08 ; subtract either #$0b or #$00 from player y position
bcc @continue_2 ; branch if underflow
cmp #$10 ; compare calculated y position to #$10
bcs @continue_3 ; branch if result is grater than #$10
@continue_2:
lda #$0a ; a = #$0a
@continue_3:
sta $09 ; set calculated y position (position above player)
jsr get_player_bg_collision_code ; get player background collision code, set result in a
bne @player_bg_collision ; branch if collision code is not #$00 (empty)
lda PLAYER_JUMP_STATUS,x ; collision code #$00 (empty) load jump status
; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
lsr ; shift jump bit to carry flag
lda #$1b ; a = #$1b
bcs @find_bg_collision ; branch if jumping
@player_bg_collision:
lda #$0a ; a = #$0a
clc ; clear carry in preparation for addition
adc $08 ; add #$0a to the amount subtracted from player y position
@find_bg_collision:
sta $08 ; store new y position distance
lda $09 ; load calculated y position (position above player)
clc ; clear carry in preparation for addition
adc VERTICAL_SCROLL ; vertical scroll offset
and #$0f ; keep bits .... xxxx
sta $0b
lda #$10 ; a = #$10
sec ; set carry flag in preparation for subtraction
sbc $0b
sta $0b
lda $0a ; load x position
ldy $09 ; load y position
jsr get_bg_collision_far ; determine player background collision code at position (a,y)
bmi @set_carry_exit ; branch if collision with solid bg element
; look at background collision tiles in front of player
@loop:
lda $0b
cmp $08 ; compare to y position
bcs @clear_carry_exit
adc #$10
sta $0b
lda $13
and #$c0 ; keep bits xx.. ....
sta $17
lda $13
clc ; clear carry in preparation for addition
adc #$04
and #$3f ; keep bits ..xx xxxx
ora $17
tay ; set BG_COLLISION_DATA offset
jsr find_floor_collision ; get collision code at BG_COLLISION_DATA,y and if not floor
; look down one collision row and get that collision code
bpl @loop ; loop if collision code is non-empty
@set_carry_exit:
sec ; set carry flag
rts
@clear_carry_exit:
clc
rts
; initializes PPU scroll offset, PPU write offsets
; then calls load_current_supertiles_screen_indexes to decompress super-tiles
; of the current level's current screen to load into CPU memory at LEVEL_SCREEN_SUPERTILES
init_ppu_write_screen_supertiles:
lda LEVEL_SCROLLING_TYPE ; load level scroll (horizontal or vertical)
bne config_vertical_scrolling ; set-up level for vertical scrolling
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bne continue_init_level ; skip scrolling setup for indoor levels
lda #$30
; initializes scrolling offsets and sets PPU write address to top left of nametable
; sets up the attribute table write address to the first attribute table
config_horizontal_scrolling:
sta LEVEL_TRANSITION_TIMER ; set to a (either #$30 for indoor level or #$20 for outdoor level)
lda #$00
sta LEVEL_SCREEN_NUMBER
sta LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
sta SUPERTILE_NAMETABLE_OFFSET ; set to nametable 0 (#$00)
sta PPU_WRITE_TILE_OFFSET
sta PPU_WRITE_ADDRESS_LOW_BYTE
lda #$20
sta PPU_WRITE_ADDRESS_HIGH_BYTE ; set PPU write address to $2000
lda #$c0
sta ATTRIBUTE_TBL_WRITE_LOW_BYTE ; since all attribute tables have #$c0 in their low byte, this is actually never read, just stored
lda #$23
sta ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; $23c0 is the first nametable attribute table
jmp load_current_supertiles_screen_indexes ; load the super tile indexes for the screen into memory at LEVEL_SCREEN_SUPERTILES
; initializes scrolling offsets and sets PPU write address to bottom left of nametable
config_vertical_scrolling:
lda #$00
sta LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
sta LEVEL_SCREEN_NUMBER
sta SUPERTILE_NAMETABLE_OFFSET ; set to nametable 0 (#$00)
lda #$1d
sta PPU_WRITE_TILE_OFFSET ; vertical levels start with #$1d and are decremented as level scrolls up
lda #$a0
sta PPU_WRITE_ADDRESS_LOW_BYTE
lda #$23
sta PPU_WRITE_ADDRESS_HIGH_BYTE ; sets write address begin to #$23a0, which is the bottom left of the vertical level's nametable
jmp load_current_supertiles_screen_indexes ; load the super tile indexes for the screen into memory at LEVEL_SCREEN_SUPERTILES
continue_init_level:
lda #$10
sta BG_PALETTE_ADJ_TIMER
jsr load_palettes_color_to_cpu ; load #$10 palette colors into PALETTE_CPU_BUFFER based on LEVEL_PALETTE_INDEX
lda #$20
bne config_horizontal_scrolling ; always jump since lda #$20 clears zero flag
; animate initial level nametable drawing
; output
; * zero flag - set when LEVEL_TRANSITION_TIMER has elapsed, clear otherwise
init_lvl_nametable_animation:
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
bne @vertical_level ; branch for vertical level
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
bne @indoor_level ; branch for indoor level
jsr load_column_of_tiles_to_cpu_buffer ; outdoor horizontal level, load the next column of tiles to CPU for drawing
; set bg collision data in CPU memory
jsr write_col_attribute_to_cpu_memory ; write a column of attribute palette data (#$07 bytes) to the CPU graphics buffer
inc PPU_WRITE_ADDRESS_LOW_BYTE ; increment PPU write address low byte (move to next nametable column)
inc PPU_WRITE_TILE_OFFSET
dec LEVEL_TRANSITION_TIMER
beq @exit ; exit with zero flag set when LEVEL_TRANSITION_TIMER is #$00
lda PPU_WRITE_TILE_OFFSET ; transition timer hasn't elapsed
cmp #$20 ; see if finished writing entire nametable with pattern table tiles
bcc @set_a_exit ; branch if not finished with nametable
lda #$00 ; finished writing entire nametable, reset PPU write address low byte
sta PPU_WRITE_ADDRESS_LOW_BYTE ; set PPU write address low byte to #$00
sta PPU_WRITE_TILE_OFFSET ; reset ppu write tile column offset
lda #$24 ; set to top-right nametable
sta PPU_WRITE_ADDRESS_HIGH_BYTE ; move PPU write address to top right nametable
lda #$27 ; load attribute table to write to (top-right [$27c0-$27f8])
sta ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; set attribute table write address to $27c0 (2nd attribute table)
lda #$40 ; load 2nd nametable offset into $0600 (LEVEL_SCREEN_SUPERTILES) for super-tile indexes
sta SUPERTILE_NAMETABLE_OFFSET ; set to nametable 1 (#$40)
jsr load_next_supertiles_screen_indexes ; load the super tile indexes for the upcoming screen into memory at LEVEL_SCREEN_SUPERTILES
@set_a_exit:
lda #$ff ; exit with zero flag clear
@exit:
rts
@vertical_level:
jsr set_vert_lvl_super_tiles
jsr write_row_attribute_to_cpu_memory ; write a row of attribute palette data (#$08 bytes) to the CPU graphics buffer
lda PPU_WRITE_ADDRESS_LOW_BYTE
sec ; set carry flag in preparation for subtraction
sbc #$20
sta PPU_WRITE_ADDRESS_LOW_BYTE
lda PPU_WRITE_ADDRESS_HIGH_BYTE
sbc #$00
sta PPU_WRITE_ADDRESS_HIGH_BYTE
dec PPU_WRITE_TILE_OFFSET
bpl @set_a_exit
jsr config_vertical_scrolling
lda #$40 ; a = #$40
sta SUPERTILE_NAMETABLE_OFFSET ; set to nametable 1 (#$40)
jsr load_next_supertiles_screen_indexes ; load the super tile indexes for the upcoming screen into memory at LEVEL_SCREEN_SUPERTILES
lda #$00 ; a = #$00
rts
; init_lvl_nametable_animation - indoor level
@indoor_level:
jsr load_column_of_tiles_to_cpu_buffer
jsr write_col_attribute_to_cpu_memory ; write a column of attribute palette data (#$07 bytes) to the CPU graphics buffer
inc PPU_WRITE_ADDRESS_LOW_BYTE
inc PPU_WRITE_TILE_OFFSET
dec LEVEL_TRANSITION_TIMER
rts
; handles scrolling for the level if currently scrolling
; including writing tiles to nametable, writing to the attribute table, and loading alternate graphics
; includes handling auto scroll from boss reveal or tank
handle_scroll:
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
beq @handle_horizontal_level ; handle horizontal level
jmp handle_vertical_scroll ; vertical level, jump
@handle_horizontal_level:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
beq @handle_outdoor_level ; branch for outdoor level
jmp handle_indoor_scroll ; indoor level, handle scrolling while advancing to next screen
; when not scrolling, this method animates the electric fence
@handle_outdoor_level:
lda AUTO_SCROLL_TIMER_01 ; see if auto scrolling enabled to reveal boss
beq @check_scroll_timer_00 ; branch if auto scrolling 01 not set, check boss reveal auto scroller
dec AUTO_SCROLL_TIMER_01 ; decrement auto scrolling timer
bne @set_scroll_frame ; branch if timer still hasn't elapsed
@check_scroll_timer_00:
lda AUTO_SCROLL_TIMER_00 ; see if auto scrolling enabled to reveal boss
beq @include_tank_auto_scroll ; branch if auto scrolling not set
dec AUTO_SCROLL_TIMER_00 ; decrement auto scrolling timer
bne @set_scroll_frame ; branch if timer still hasn't elapsed, to continue screen scroll
inc BOSS_AUTO_SCROLL_COMPLETE ; set boss reveal auto-scroll completed
; scroll screen for this video frame
@set_scroll_frame:
lda #$01 ; a = #$01
sta FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
@include_tank_auto_scroll:
lda FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
clc ; clear carry in preparation for addition
adc TANK_AUTO_SCROLL ; auto scroll additional amount, always add this value to scroll
beq @exit ; exit if no scrolling is required
sta $17 ; set in memory frame scroll value (including tank auto scroll)
; sets alternative graphics loading flag if on correct screen
; loads alternate graphics if necessary
; then checks if need to move to next screen
@set_scroll_graphics_data:
inc LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
bne @inc_nametable_data ; branch if camera has scrolled scrolled within frame
inc LEVEL_SCREEN_NUMBER ; new screen, increment screen number
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
cmp LEVEL_ALT_GRAPHICS_POS ; compare to screen where alternate graphics should start loading
bne @change_screen ; skip loading alternate graphics if not at location to load them
; start preparing the new nametable data
lda #$01 ; at position to load alternate graphics
sta ALT_GRAPHIC_DATA_LOADING_FLAG ; set the alternate graphics loading flag so alternate graphics will be loaded
jsr load_alternate_graphics ; load alternate graphics
; initialize the new nametable data: enemy screen read offset, PPUCTRL, PPU write address, attribute write address
@change_screen:
lda #$00 ; a = #$00
sta ENEMY_SCREEN_READ_OFFSET ; set offset into level_xx_enemy_screen_xx table
lda PPUCTRL_SETTINGS ; load current PPU controller settings
eor #$01 ; swap to other nametable ($2000 or $2400)
sta PPUCTRL_SETTINGS ; update base nametable address
; handle increment graphics data write offsets and if necessary load new data to cpu buffer
@inc_nametable_data:
lda LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset within screen in pixels
and #$07 ; keep bits .... .xxx
bne @check_attr_update ; branch if not a new nametable tile column
; to check if need to update attribute table or just exit
jsr load_column_of_tiles_to_cpu_buffer ; new nametable column
inc PPU_WRITE_ADDRESS_LOW_BYTE
inc PPU_WRITE_TILE_OFFSET
lda PPU_WRITE_TILE_OFFSET
cmp #$20 ; super-tiles are #$20 (32 decimal) pixels wide
bcc @inc_scroll_exit ; branch if PPU_WRITE_TILE_OFFSET < 20
lda SUPERTILE_NAMETABLE_OFFSET ; load offset into CPU graphics buffer where super-tile indexes are stored for current screen
eor #$40 ; flip bits .x.. ....
sta SUPERTILE_NAMETABLE_OFFSET ; move to other nametable (#$00 = nametable 0, #$40 = nametable 1)
jsr load_next_next_supertiles_screen_indexes ; load the super tile indexes for the screen 2 screens ahead into memory at LEVEL_SCREEN_SUPERTILES
lda #$00 ; a = #$00
sta PPU_WRITE_ADDRESS_LOW_BYTE
sta PPU_WRITE_TILE_OFFSET ; $60 was #$1f, set back to #$00
lda PPU_WRITE_ADDRESS_HIGH_BYTE
eor #$04 ; flip bits .... .x..
sta PPU_WRITE_ADDRESS_HIGH_BYTE
lda ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; load current attribute table write address
eor #$04 ; flip bits .... .x..
sta ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; switch attribute table write address between $27c0 and $23c0, or $2bc0 and $2fc0
@inc_scroll_exit:
inc HORIZONTAL_SCROLL ; increment horizontal component of the PPUSCROLL [#$0 - #$ff]
dec $17 ; decrement in memory frame scroll value (including tank auto scroll)
bne @set_scroll_graphics_data ; branch if still has scroll, this will only happen when for tank auto scroll
; otherwise done handling scroll, exit
@exit:
rts
@check_attr_update:
lda LEVEL_SCREEN_SCROLL_OFFSET ; load the number of pixels into LEVEL_SCREEN_NUMBER the level has scrolled
and #$0f ; keep low nibble
cmp #$03
bne @inc_scroll_exit ; branch if scroll offset doesn't end in #$03, only update attribute table every #$f pixels
jsr write_col_attribute_to_cpu_memory ; 16 frames have scrolled, write next column of attribute palette data (#$08 bytes) to the CPU graphics buffer
jmp @inc_scroll_exit
; vertical level
handle_vertical_scroll:
lda AUTO_SCROLL_TIMER_00 ; load scroll to reveal boss timer
beq @init_loop ; branch if scroll complete
lda #$10 ; a = #$10
sta BG_PALETTE_ADJ_TIMER
lda #$01 ; a = #$01
sta FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
dec AUTO_SCROLL_TIMER_00 ; decrement boss reveal scroll
bne @init_loop
inc BOSS_AUTO_SCROLL_COMPLETE ; set boss reveal auto-scroll completed
@init_loop:
lda FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
beq @exit ; exit
sta $17 ; store frame scroll in $17
@frame_scroll_loop:
inc LEVEL_SCREEN_SCROLL_OFFSET ; increment scrolling offset in pixels within screen
lda LEVEL_SCREEN_SCROLL_OFFSET
cmp #$f0
bcc @continue ; branch if a < #$f0
lda #$00 ; a = #$00, scroll is >= #$f0, move to next screen
sta LEVEL_SCREEN_SCROLL_OFFSET ; reset scrolling offset in pixels within screen
sta ENEMY_SCREEN_READ_OFFSET ; offset for enemy data
inc LEVEL_SCREEN_NUMBER
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
cmp LEVEL_ALT_GRAPHICS_POS ; compare to screen where alternate graphics should start loading
bne @continue ; branch if not on the screen where alternate graphics should load
lda #$01
sta ALT_GRAPHIC_DATA_LOADING_FLAG ; reached screen where alternate graphics should start loading, set flag
lda #$80 ; a = #$80
sta BG_PALETTE_ADJ_TIMER
jsr load_alternate_graphics
@continue:
lda LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
and #$07 ; keep bits .... .xxx
bne @write_attribute_continue
jsr set_vert_lvl_super_tiles
lda PPU_WRITE_ADDRESS_LOW_BYTE
sec ; set carry flag in preparation for subtraction
sbc #$20
sta PPU_WRITE_ADDRESS_LOW_BYTE
lda PPU_WRITE_ADDRESS_HIGH_BYTE
sbc #$00
sta PPU_WRITE_ADDRESS_HIGH_BYTE
dec PPU_WRITE_TILE_OFFSET
bpl @dec_scroll_continue ; decrement vertical scroll and continue loop
lda SUPERTILE_NAMETABLE_OFFSET ; load offset into CPU graphics buffer where super-tile indexes are stored for current screen
eor #$40 ; move to other nametable of super-tile index data
sta SUPERTILE_NAMETABLE_OFFSET ; move to other nametable (#$00 = nametable 0, #$40 = nametable 1)
jsr load_next_supertiles_screen_indexes ; load the super tile indexes for the upcoming screen into memory at LEVEL_SCREEN_SUPERTILES
lda #$1d ; a = #$1d
sta PPU_WRITE_TILE_OFFSET ; $60 decremented to #$00, reset back to #$1d
lda #$a0 ; a = #$a0
sta PPU_WRITE_ADDRESS_LOW_BYTE
lda #$23 ; a = #$23
sta PPU_WRITE_ADDRESS_HIGH_BYTE
@dec_scroll_continue:
dec VERTICAL_SCROLL ; vertical scroll offset
lda VERTICAL_SCROLL ; vertical scroll offset
cmp #$ff
bne @continue_loop
lda #$ef ; a = #$ef
sta VERTICAL_SCROLL ; vertical scroll offset
@continue_loop:
dec $17 ; decrement FRAME_SCROLL
bne @frame_scroll_loop
@exit:
rts
@write_attribute_continue:
lda LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
and #$0f ; keep bits .... xxxx
cmp #$07
bne @dec_scroll_continue
jsr write_row_attribute_to_cpu_memory ; write a row of attribute palette data (#$08 bytes) to the CPU graphics buffer
jmp @dec_scroll_continue
; handle_scroll - indoor level
; handle scrolling, including animating the electric fence
handle_indoor_scroll:
lda INDOOR_SCREEN_CLEARED ; indoor screen cleared flag (0 = not cleared; 1 = cleared, #$80 = cleared and fence removed)
bpl @animate_indoor_fence ; branch if flag is #$80, indicating that the electric fence needs to be animated/removed
lda LEVEL_TRANSITION_TIMER ; load remaining animation timer for player advancing to next screen
bmi @indoor_screen_transition ; jump if LEVEL_TRANSITION_TIMER is negative (couldn't get this to happen)
bne @write_column_tiles_exit ; branch if the player is advancing and the background needs to update
lda INDOOR_SCROLL ; player isn't advancing, see if scrolling (0 = not scrolling; 1 = scrolling, 2 = finished scrolling)
beq @exit ; exit if not scrolling
lda #$00 ; player has pressed up and screen is now 'scrolling' as player advances
; begin advancing background animation
sta PPU_WRITE_TILE_OFFSET ; initialize PPU_WRITE_TILE_OFFSET to #$00
sta PPU_WRITE_ADDRESS_LOW_BYTE ; initialize PPU_WRITE_ADDRESS_LOW_BYTE to #$00
sta $66 ; !(UNUSED) not sure of use, only ever set to #$00 or #$c0, never read
lda #$20
sta LEVEL_TRANSITION_TIMER ; set initial advancing animation timer to #$20
lda PPU_WRITE_ADDRESS_HIGH_BYTE
eor #$04 ; flip bits .... .x..
sta PPU_WRITE_ADDRESS_HIGH_BYTE
lda ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; load current attribute table write address
eor #$04 ; flip bits .... .x..
sta ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; switch attribute table write address between $27c0 and $23c0, or $2bc0 and $2fc0
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
cmp LEVEL_ALT_GRAPHICS_POS ; compare to screen where alternate graphics should start loading
bne @load_supertiles_screen_indexes_indoor
lda LEVEL_SCREEN_SCROLL_OFFSET ; on screen where alternate graphics should load, load scrolling offset in pixels within screen
cmp #$03
bne @load_supertiles_screen_indexes_indoor
ldy #$00 ; y = #$00
; load the graphics data for the indoor boss screen
@loop:
lda level_2_4_boss_graphics_data,y
sta LEVEL_SCREEN_SUPERTILES_PTR,y ; depending on Y offset actually offsets one of the 3
; LEVEL_SCREEN_SUPERTILES_PTR, LEVEL_SUPERTILE_DATA_PTR, LEVEL_SUPERTILE_PALETTE_DATA
iny
cpy #$06
bne @loop
lda CURRENT_LEVEL ; current level
lsr
bcs @load_screen_a_supertile_indexes ; branch if odd level, i.e. indoor/base, energy zone, or alien's lair
; I believe this is only called in indoor/base boss context, so this always branches
@load_supertiles_screen_indexes_indoor:
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
asl
asl
sec ; set carry flag
adc LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
; input
; * a - index into the screen_supertile_ptr_table table
@load_screen_a_supertile_indexes:
jsr load_supertiles_screen_indexes ; decompress and load super-tile indexes into LEVEL_SCREEN_SUPERTILES
@write_column_tiles_exit:
jsr load_column_of_tiles_to_cpu_buffer
jsr write_col_attribute_to_cpu_memory ; write a column of attribute palette data (#$07 bytes) to the CPU graphics buffer
inc PPU_WRITE_TILE_OFFSET
inc PPU_WRITE_ADDRESS_LOW_BYTE
dec LEVEL_TRANSITION_TIMER ; subtract 1
bne @exit ; exit if not #$00
lda #$80
sta LEVEL_TRANSITION_TIMER ; reset LEVEL_TRANSITION_TIMER to #$80
@exit:
rts
; update pattern table tiles to animate electric fence
@animate_indoor_fence:
jmp animate_indoor_fence
; player has cleared the screen and finished advancing, swap active nametable
@indoor_screen_transition:
lda #$00 ; a = #$00
sta LEVEL_TRANSITION_TIMER ; init to #$00
inc INDOOR_SCROLL ; set INDOOR_SCROLL to #$02
inc LEVEL_SCREEN_SCROLL_OFFSET ; increment which of the #$04 screens are showing while advancing
lda LEVEL_SCREEN_SCROLL_OFFSET ; load which animation screen is being shown [#$00-#$03]
cmp #$04 ; see if have shown all #$04 of the backgrounds while advancing to next indoor screen
bne @swap_base_nametable ; swap to next screen for advancing animation
inc INDOOR_PLAYER_JUMP_FLAG ; finished advancing into next indoor screen, set player to jump
inc INDOOR_PLAYER_JUMP_FLAG+1 ; set player 2 to jump (if no player 2, this isn't used)
lda #$00 ; a = #$00
sta INDOOR_SCREEN_CLEARED ; indoor screen cleared flag (0 = not cleared; 1 = cleared)
sta LEVEL_SCREEN_SCROLL_OFFSET ; scrolling offset in pixels within screen
sta ENEMY_SCREEN_READ_OFFSET
inc LEVEL_SCREEN_NUMBER
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
cmp LEVEL_STOP_SCROLL ; compare to the screen to stop scrolling on
bne @continue
lda #$80 ; a = #$80
sta LEVEL_LOCATION_TYPE ; overwrite level location type with #$80 (no longer specifies indoor vs outdoor)
jsr load_alternate_graphics
jsr init_APU_channels
lda CURRENT_LEVEL ; current level
lsr
ora #$08 ; set bits .... x...
jsr load_A_offset_graphic_data ; load graphic data code 08 or 09
lda #$42 ; a = #$42 (sound_42)
jsr play_sound ; play indoor/base boss screen music
lda #$b1 ; a = #$b1
sta PPUCTRL_SETTINGS
lda #$e0 ; a = #$e0
sta VERTICAL_SCROLL ; set vertical scroll offset to match outdoor levels (#$e0)
@continue:
lda #$0c ; a = #$0c
sta BG_PALETTE_ADJ_TIMER ; set fade-in effect timer for boss screen
lda #$20 ; a = #$20
jsr load_palettes_color_to_cpu ; load #$20 palette colors into PALETTE_CPU_BUFFER based on LEVEL_PALETTE_INDEX
@swap_base_nametable:
lda PPUCTRL_SETTINGS
eor #$01 ; flip bits .... ...x
sta PPUCTRL_SETTINGS
rts
; pointer table for indoor level boss header changes ($03 * $02 = $06 bytes)
; bank 2 and 3 labels
level_2_4_boss_graphics_data:
.addr level_2_4_boss_supertiles_screen_ptr_table ; bank 2 - super-tiles per screen (LEVEL_SCREEN_SUPERTILES_PTR) - CPU address $9013
.addr level_2_4_boss_supertile_data ; bank 3 - super-tile pattern table tiles (LEVEL_SUPERTILE_DATA_PTR) - CPU address $b57a
.addr level_2_4_boss_palette_data ; bank 3 - super-tile palette data (LEVEL_SUPERTILE_PALETTE_DATA) - CPU address $bd7a
; populates the CPU_GRAPHICS_BUFFER with #$1c pattern table tiles (one column) from the super-tiles
; since vram_address_increment is 1, this represents a single column of the nametable
; it takes multiple frames to write the entire nametable to the GPU
load_column_of_tiles_to_cpu_buffer:
ldx GRAPHICS_BUFFER_OFFSET ; load the offset into CPU_GRAPHICS_BUFFER
lda #$02 ;
sta CPU_GRAPHICS_BUFFER,x ; set vram_address_increment offset to #$02 (value #$04) (increment by #$20 (write nametable tiles in a column fashion top to bottom))
lda #$1c ; #$1c (28 decimal) pattern table tiles (a full column of tiles for the screen)
inx
sta CPU_GRAPHICS_BUFFER,x ; set the size of graphics that will be written to PPU to #$1c (28 in decimal, one column of tiles)
lda #$01 ; a = #$01
inx
sta CPU_GRAPHICS_BUFFER,x ; set the number of #1c-sized blocks that will be written to #$01 (only writing one column)
lda PPU_WRITE_ADDRESS_HIGH_BYTE ; load high byte of PPU write address
inx
sta CPU_GRAPHICS_BUFFER,x ; set high byte of PPU write address
sta $12 ; store high byte into $12
lda PPU_WRITE_ADDRESS_LOW_BYTE ; load low byte of PPU write address
inx
sta CPU_GRAPHICS_BUFFER,x ; set low byte of PPU write address
inx
ldy #$ff ; y = #$ff
lsr ; shift right the low byte of the PPU write address moving lsb to carry flag
bcs @odd_nametable_column ; branch if low byte of PPU write address is odd (collision is only set on every other column)
; setting up $12 and $13 to for later when configuring collision, which only looks at every other pattern tile column
sta $13 ; low byte was even, store shifted (halved) byte address in $13 directly
lsr $12 ; shift high byte right
lsr $12 ; shift high byte right
lsr $12 ; shift high byte right
ror ; shift a register high byte right, pulling in carry if set
lsr ; shift a register high byte right
sta $12 ; store new high byte back into $12, this is now the BG_COLLISION_DATA write offset
lda $13 ; load PPU write address low byte
and #$03 ; keep bits .... ..xx (0 to 3)
sta $13 ; numbering every other column (0 to 3 in a loop) since PPU write address was shifted to the right
ldy #$00 ; y = #$00
@odd_nametable_column:
sty $11 ; #$ff for odd nametable columns, #$00 for even, used to determine if collision setting is necessary
lda PPU_WRITE_TILE_OFFSET
and #$03 ; keep bits .... ..xx
sta $02
lda PPU_WRITE_TILE_OFFSET
lsr
lsr
ora SUPERTILE_NAMETABLE_OFFSET ; merge with nametable offset (#$00 = nametable 0, #$40 = nametable 1) where super-tile indexes are stored for current screen
sta $10 ; score LEVEL_SCREEN_SUPERTILES offset into $10
tay ; y is the LEVEL_SCREEN_SUPERTILES offset
; * loads level super-tiles from PRG ROM bank 2 by CPU address LEVEL_SCREEN_SUPERTILES
; - looped through until all tiles for a single column have been written
; - ultimately is called 4 times for each super-tile because each time renders only 1 column of pattern table tiles for the super-tile
; * updates the in-memory background collision (BG_COLLISION_DATA) information (set_tile_collision)
; Y is the LEVEL_SCREEN_SUPERTILES offset (level_X_supertiles_screen_XX)
; $03 is the column offset of the super-tile to write to CPU_GRAPHICS_BUFFER block (currently drawing column)
; #$37 super-tiles per screen for horizontal levels
; #$40 super-tiles per screen for vertical levels
; CPU address #$df48
load_level_supertile_data:
lda #$00 ; initialize LEVEL_SUPERTILE_DATA_PTR read offset
sta $08 ; reset offset into level super-tile pointer table
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
asl ; each super-tile is #$10 in size so need to double offset 4 times
asl
rol $08 ; keep track of how many times A overflows
asl
rol $08
asl
rol $08
adc LEVEL_SUPERTILE_DATA_PTR ; add read address into level_X_SUPERTILE_data to get to correct super-tile
sta $00 ; store the low byte to level super-tile data
lda $08 ; load read offset
adc LEVEL_SUPERTILE_DATA_PTR+1 ; add number of overflows to the high byte of the pointer table address
sta $01 ; store high byte to level super-tile data
ldy $02 ; level the super-tile data byte offset of the tiles that will be drawn (which column of super-tile will be drawn)
lda ($00),y ; load the level's super-tile y-th pattern table tile byte (level_X_SUPERTILE_data)
sta CPU_GRAPHICS_BUFFER,x ; write first pattern table tile from super-tile data to CPU memory
jsr set_tile_collision ; set the tile collision for the top portion of the entire super-tile in BG_COLLISION_DATA
inx ; increment CPU write offset
iny
iny
iny
iny ; increment CPU read offset by 4 total
lda ($00),y ; read next pattern table tile of the super-tile one row down (level_X_SUPERTILE_data)
sta CPU_GRAPHICS_BUFFER,x ; write first byte of second quadrant of super-tile to CPU memory
inx ; increment CPU write offset
iny
iny
iny
iny
lda ($00),y ; read next pattern table tile of the super-tile one row down (level_X_SUPERTILE_data)
sta CPU_GRAPHICS_BUFFER,x ; write to CPU memory
jsr set_tile_collision ; set tile collision data for middle row of super-tile
inx ; increment CPU write offset
iny
iny
iny
iny
lda ($00),y ; read one tile of last row of the super-tile (level_X_SUPERTILE_data)
sta CPU_GRAPHICS_BUFFER,x ; write first byte of last quadrant of super-tile to CPU memory
inx ; increment CPU write offset
lda $10 ; load the number of horizontal pixels for the row that have been loaded
clc ; clear carry in preparation for addition
adc #$08 ; add #$08 (each pattern table entry is #$08 pixels wide and tall)
sta $10 ; add updated
tay ; A is nth super-tile to load for the column for the screen
and #$3f ; keep bits ..xx xxxx
cmp #$38
bcc load_level_supertile_data ; load next tile to CPU_GRAPHICS_BUFFER if < #$38 (horizontal levels have #$37 super-tiles)
stx GRAPHICS_BUFFER_OFFSET ; keep track of where in CPU_GRAPHICS_BUFFER buffer to write pattern table tiles
rts ; go back to load_column_of_tiles_to_cpu_buffer
; vertical level - writing pattern tiles for super-tiles
set_vert_lvl_super_tiles:
ldx GRAPHICS_BUFFER_OFFSET ; index for PPU background tile
lda #$01 ; a = #$01
sta CPU_GRAPHICS_BUFFER,x ; set vram_address_increment (write across)
sta CPU_GRAPHICS_BUFFER+2,x ; set length of data to #$01 byte
lda #$20 ; a = #$20
inx ; increment graphics buffer write offset
sta CPU_GRAPHICS_BUFFER,x ; store #$20 #$01-byte groups
inx ; increment graphics buffer write offset
lda PPU_WRITE_ADDRESS_HIGH_BYTE ; load write address high byte
inx ; increment graphics buffer write offset
sta CPU_GRAPHICS_BUFFER,x ; write the PPU write address high byte
lda PPU_WRITE_ADDRESS_LOW_BYTE ; load write address low byte
inx ; increment graphics buffer write offset
sta CPU_GRAPHICS_BUFFER,x ; write the next byte of the PPU write low address
inx ; increment graphics buffer write offset
lda #$00 ; a = #$00
sta $11
sta $13
lda PPU_WRITE_TILE_OFFSET ; load current super-tile data write offset
; starts with #$1d goes down to #$00 before looping
and #$03 ; keep bits .... ..xx
asl
asl
sta $02
lda PPU_WRITE_TILE_OFFSET
and #$1c ; keep bits ...x xx..
asl
ora SUPERTILE_NAMETABLE_OFFSET ; merge with nametable offset (#$00 = nametable 0, #$40 = nametable 1) where super-tile indexes are stored for current screen
sta $10
lda PPU_WRITE_TILE_OFFSET
lsr
ror $11
asl
asl
sta $12 ; store PPU write address high byte into $12
ldy $10
@set_supertile_tiles:
lda #$00 ; a = #$00
sta $08
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
asl
asl
rol $08
asl
rol $08
asl
rol $08
adc LEVEL_SUPERTILE_DATA_PTR ; (bank 3 pointer)
sta $00
lda $08
adc LEVEL_SUPERTILE_DATA_PTR+1 ; add the high byte of the pointer address
sta $01
ldy $02
lda ($00),y
sta CPU_GRAPHICS_BUFFER,x
jsr set_tile_collision ; set BG_COLLISION_DATA to tile collision code (0-3) for the pattern table tile
inx ; increment graphics buffer write offset
iny ; increment graphics data read offset
lda ($00),y
sta CPU_GRAPHICS_BUFFER,x
inx ; increment graphics buffer write offset
iny ; increment graphics data read offset
lda ($00),y
sta CPU_GRAPHICS_BUFFER,x
jsr set_tile_collision ; set BG_COLLISION_DATA to tile collision code (0-3) for the pattern table tile
inx ; increment graphics buffer write offset
iny ; increment graphics data read offset
lda ($00),y
sta CPU_GRAPHICS_BUFFER,x
inx ; increment graphics buffer write offset
inc $10
lda $10
tay
and #$07 ; keep bits .... .xxx
bne @set_supertile_tiles
stx GRAPHICS_BUFFER_OFFSET
rts
; write a column of attribute palette data (#$07 bytes) to the CPU graphics buffer
write_col_attribute_to_cpu_memory:
ldx GRAPHICS_BUFFER_OFFSET ; load the current tile to draw to the PPU
lda #$01 ; a = #$01
sta CPU_GRAPHICS_BUFFER,x ; set vram_address_increment (write across)
inx ; increment CPU_GRAPHICS_BUFFER write offset
sta CPU_GRAPHICS_BUFFER,x ; set length of data to #$01 byte
inx ; increment CPU_GRAPHICS_BUFFER write offset
lda #$07 ; a = #$07
sta CPU_GRAPHICS_BUFFER,x ; specifying to store #$07 #$01-byte groups (the column of super-tile palette data)
inx ; increment CPU_GRAPHICS_BUFFER write offset
lda PPU_WRITE_TILE_OFFSET ; load the current PPU tile offset being written to CPU memory
lsr
lsr ; only care about which column of super-tile is active
ora #$c0 ; set bits xx.. ....
; e.g. #$08 -> #$c2, which means the 3rd attribute table column
sta $00 ; store current attribute table low byte to $00 in range from #$c0 up to #$ff inclusively
and #$0f ; keep low nibble
ora SUPERTILE_NAMETABLE_OFFSET ; merge with nametable offset (#$00 = nametable 0, #$40 = nametable 1) where super-tile indexes are stored for current screen
; e.g. #$c2 -> #$42
sta $10 ; set super-tile address into cpu graphics buffer that contains the attribute data to write (offset from $0600 (LEVEL_SCREEN_SUPERTILES))
tay ; transfer super-tile index to render to y (level_X_supertiles_screen_XX)
; write palette data to CPU_GRAPHICS_BUFFER for use to write in attribute table
; writes an entire columned of super-tiles' palette attribute data (#$08 bytes)
@set_supertile_attribute_byte:
lda ATTRIBUTE_TBL_WRITE_HIGH_BYTE ; load current attribute table write address high byte
sta CPU_GRAPHICS_BUFFER,x ; set PPU high write address to correct attribute table
inx ; increment CPU_GRAPHICS_BUFFER write offset
lda $00 ; load the current attribute table low write byte (#$c0 up to #$ff inclusively)
sta CPU_GRAPHICS_BUFFER,x ; store low byte of PPU address
inx ; increment CPU_GRAPHICS_BUFFER write offset
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
tay ; transfer super-tile index into y register
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load the palette for the super-tile (one entry in attribute table)
sta CPU_GRAPHICS_BUFFER,x ; write the palette byte to graphics buffer, this will be written to the attribute table
inx ; increment CPU_GRAPHICS_BUFFER write offset
lda $00 ; load the current attribute table low write byte (#$c0 up to #$ff inclusively)
clc ; clear carry in preparation for addition
adc #$08 ; add #$08 to the current attribute table write low byte
; this moves to the next super-tile in the column (move down one row)
sta $00 ; set new attribute table write address low byte
cmp #$f8 ; see if on last attribute table entry
bcs @exit ; branch if attribute entry >= #$f8
; contra doesn't write palette data for the last half super-tile attribute row (for non-vertical levels)
; this area isn't usually rendered on CRTs and when emulated
lda $10 ; load current super-tile to render
adc #$08 ; add #$08 to render the next supertile down
sta $10 ; store next super-tile to render
tay
bcc @set_supertile_attribute_byte ; loop if more palette data in column to write to attribute table
@exit:
stx GRAPHICS_BUFFER_OFFSET ; restore x to the graphics buffer write offset
rts
; write a row of attribute palette data (#$08 bytes) to the CPU graphics buffer
; for vertical level (waterfall) only
write_row_attribute_to_cpu_memory:
ldx GRAPHICS_BUFFER_OFFSET ; load the current tile to draw to the PPU
lda #$01 ; a = #$01
sta CPU_GRAPHICS_BUFFER,x ; set vram_address_increment to #$01
sta CPU_GRAPHICS_BUFFER+2,x ; set number of length of graphics blocks to #$01
lda #$08 ; a = #$08
inx ; increment CPU_GRAPHICS_BUFFER write offset
sta CPU_GRAPHICS_BUFFER,x ; set number of #01-byte-long groups to #$08
inx ; increment CPU_GRAPHICS_BUFFER write offset
lda #$23 ; a = #$23
inx ; increment CPU_GRAPHICS_BUFFER write offset
sta CPU_GRAPHICS_BUFFER,x ; set high byte of PPU write address to #$23
lda PPU_WRITE_TILE_OFFSET
asl
and #$38 ; keep bits ..xx x...
ora SUPERTILE_NAMETABLE_OFFSET ; merge with nametable offset (#$00 = nametable 0, #$40 = nametable 1) where super-tile indexes are stored for current screen
sta $10 ; set super-tile address into cpu graphics buffer that contains the attribute data to write (offset from $0600 (LEVEL_SCREEN_SUPERTILES))
and #$bf ; strip bit 6
clc ; clear carry in preparation for addition
adc #$c0 ; attribute table low byte starts at #$c0, add #$c0 to get actual initial attribute address for row
inx ; increment CPU_GRAPHICS_BUFFER write offset
sta CPU_GRAPHICS_BUFFER,x ; set low byte of PPU write address
inx ; increment CPU_GRAPHICS_BUFFER write offset
@set_supertile_attribute_byte:
lda PPU_WRITE_TILE_OFFSET ; load the current row being written
and #$03 ; keep bits .... ..xx
cmp #$03 ; every #$04 rows take the palette from the bottom super-tile and
; merge with top palette from other nametable super-tile
beq @merge_supertiles_across_nametable
lda $10 ; load super-tile address into cpu graphics buffer that contains the attribute data to write (offset from $0600 (LEVEL_SCREEN_SUPERTILES))
tay ; transfer to offset register
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
tay ; transfer super-tile index to offset register
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load the palette for the super-tile
@write_supertile_palette_to_cpu:
sta CPU_GRAPHICS_BUFFER,x ; set palette in attribute table for nametable
inx ; increment CPU_GRAPHICS_BUFFER write offset
inc $10 ; move to next super-tile index
lda $10
and #$07 ; strip to low nibble to see how many super-tile palette attribute bytes have been written
bne @set_supertile_attribute_byte ; loop until all palette attribute table entries are written for the row
stx GRAPHICS_BUFFER_OFFSET ; finished writing attributes, restore x to the graphics buffer write offset
rts
; used to get correct palette for supertile when it spreads across a nametable
@merge_supertiles_across_nametable:
lda $10 ; load super-tile address into cpu graphics buffer that contains the attribute data to write (offset from $0600 (LEVEL_SCREEN_SUPERTILES))
tay ; transfer to offset register
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
tay ; transfer super-tile index to offset register
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load the palette data for the super-tile
and #$f0 ; keep lower half palette data for the super-tile
sta $11 ; store lower half palette data of the super-tile
lda $10 ; load super-tile address into cpu graphics buffer that contains the attribute data to write (offset from $0600 (LEVEL_SCREEN_SUPERTILES))
eor #$40 ; move to other nametable
tay ; transfer super-tile index to offset register
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
tay ; transfer super-tile index to offset register
lda (LEVEL_SUPERTILE_PALETTE_DATA),y ; load the palette data for the super-tile
and #$0f ; keep upper half palette data for the super-tile
ora $11 ; merge with lower half of super-tile palette data to get full super-tile palette data
jmp @write_supertile_palette_to_cpu
; determine tile collision code (0-3) for the pattern table tile updates BG_COLLISION_DATA
; input
; * a - namespace tile code from the super-tile
; * y - level namespace tile offset (level_X_SUPERTILE_data offset)
; tile code 0 is always set to collision code 0
set_tile_collision:
sty $14 ; store super-tile tile data read offset in $14 (LEVEL_SUPERTILE_DATA_PTR offset)
ldy $11 ; if $11 is set, then odd number nametable column, and collision isn't considered
bne tile_collision_exit ; exit if $11 is set
tay ; move the pattern table tile code to Y
beq set_collision_code_0 ; tile index is #$00, set to collision code 0 (empty)
cmp COLLISION_CODE_1_TILE_INDEX ; compare against collision code 1 tile index
bcs collision_code_0_check ; pattern table tile is not collision code 1, check to see if collision code 0
lda #$01 ; set collision code to 1 (floor)
bne collision_continue ; continue
; check if empty collision code
collision_code_0_check:
cmp COLLISION_CODE_0_TILE_INDEX
bcs collision_code_2_check ; tile index is greater than collision code 2 limit, check if collision code 03
set_collision_code_0:
lda #$00 ; set collision code to 0 (empty)
beq collision_continue
; check if water collision code
collision_code_2_check:
cmp COLLISION_CODE_2_TILE_INDEX
bcs set_collision_code_03 ; tile index offset is greater than collision code 2 limit
lda #$02 ; set collision code to 2 (water)
bne collision_continue
; check if solid collision code
set_collision_code_03:
lda #$03 ; set collision code to 03 (solid)
; register a contains the collision code
; handles storing the collision 2-bits for the 1/4 of the super-tile in the correct memory address
collision_continue:
ldy $13 ; load low byte of PPU write address (masked to 0 to 3)
bne set_collision_tile_col_2 ; jump if odd column of screen write address
asl
asl
asl
asl
asl
asl ; shift the tile code (2 bits) all the way to the left 2 bits
sta $15 ; store modified collision code for super-tile into $15
ldy $12 ; load BG_COLLISION_DATA write offset
lda BG_COLLISION_DATA,y ; load existing collision byte (each byte contains collision data for 2 super-tiles)
and #$3f ; keep bits ..xx xxxx, which will be merged with the modified collision code in $15
jmp set_collision_tile ; save the updated collision information in CPU memory
set_collision_tile_col_2:
dey ; see if second column by subtracting stored write offset
bne set_collision_tile_col_3 ; if not the second column, branch to see if 3rd or 4th
asl
asl
asl
asl ; shift the tile code (2 bits) all the way to the bits 5 and 4 (..xx ....)
sta $15 ; store shifted collision code for super-tile into $15
ldy $12 ; load BG_COLLISION_DATA write offset
lda BG_COLLISION_DATA,y ; load existing collision byte (each byte contains collision data for 2 super-tiles)
and #$cf ; keep bits xx.. xxxx, which will be merged with the modified collision code in $15
jmp set_collision_tile ; save the updated collision information in CPU memory
set_collision_tile_col_3:
dey ; see if second column by subtracting stored write offset
bne set_collision_tile_col_4 ; if not the second column, branch to see if 4th
asl
asl ; shift the tile code (2 bits) to the bits 2 and 2 (.... xx..)
sta $15 ; store collision code for super-tile into $15
ldy $12 ; load BG_COLLISION_DATA write offset
lda BG_COLLISION_DATA,y ; load existing collision byte (each byte contains collision data for 2 super-tiles)
and #$f3 ; keep bits xxxx ..xx, which will be merged with the modified collision code in $15
jmp set_collision_tile ; save the updated collision information in CPU memory
set_collision_tile_col_4:
sta $15 ; store collision code for super-tile into $15
ldy $12 ; load BG_COLLISION_DATA write offset
lda BG_COLLISION_DATA,y ; load collision code for super-tile
and #$fc ; keep bits xxxx xx.., which will be merged with the modified collision code in $15
; sets the already-shifted collision code in a with the masked collision code in $15 and updates the CPU memory accordingly
set_collision_tile:
ora $15 ; combine the BG_COLLISION_DATA,y with the shifted collision code for the super-tile
sta BG_COLLISION_DATA,y ; save back into CPU memory
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
bne vert_lvl_tile_collision_exit ; jump if vertical level
tya
clc ; clear carry in preparation for addition
adc #$04 ; move forward 4 bytes for next super-tile
sta $12 ; update BG_COLLISION_DATA write offset to next tile down
tile_collision_exit:
ldy $14
rts ; go back to load_level_supertile_data
vert_lvl_tile_collision_exit:
inc $13 ; increment low byte of PPU write address
lda $13
cmp #$04 ; see if finished writing all four pattern table tiles in super-tile
bcc tile_collision_exit
lda #$00 ; a = #$00
sta $13
inc $12 ; update BG_COLLISION_DATA write offset
bne tile_collision_exit ; should always jump since inc $12 is non-zero
; gets the collision code for (a,y) and if collision code is the floor,
; look one row (half supertile) see if collision code one row below (half supertile) is solid,
; if so, use that collision code.
; input
; * a - x pos
; * y - y pos
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
get_bg_collision_far:
jsr get_bg_collision ; determine player background collision code at position (a,y)
; determines the next half supertile row's collision code below the $13 offset
; if current collision code is a floor collision, otherwise, do nothing
; input
; * a - collision code
; * $13 - BG_COLLISION_DATA offset
; output
; * a - collision code of half row down
; * carry flag - set when collision code #$80 (solid)
floor_get_next_row_bg_collision:
pha ; push collision code on to stack
bcc @exit ; exit if current collision code is not a floor collision
lda $13 ; load current BG_COLLISION_DATA offset
sta $16 ; store in $16
and #$c0 ; keep bits 6 and 7
sta $17 ; save result in $17
lda $16 ; re-load BG_COLLISION_DATA offset
clc ; clear carry in preparation for addition
adc #$04 ; move down to next supertile half-row
and #$3f ; keep bits ..xx xxxx
ora $17 ; merge original bits 6 and 7 back
tay ; transfer BG_COLLISION_DATA offset to y
jsr read_bg_collision_byte_unsafe ; get collision code from BG_COLLISION_DATA byte
asl
lda $16 ; load previous BG_COLLISION_DATA offset
sta $13 ; store value in $13
bcc @exit ; exit if not a solid collision
pla ; solid collision, set collision code to #$80
; pop old collision code from stack
lda #$80 ; a = #$80
rts
@exit:
pla ; pop collision code from stack
rts
; get collision code at BG_COLLISION_DATA,y and if not floor, look down one collision row and get that collision code
; input
; * y - BG_COLLISION_DATA offset
; output
; * a - collision code of half row down
; * carry flag - set when collision code #$80 (solid)
find_floor_collision:
jsr read_bg_collision_byte_unsafe ; get collision code from BG_COLLISION_DATA byte
jmp floor_get_next_row_bg_collision ; if floor collision, get next half supertile row's collision code
; otherwise exit
; reads the specific bits of the BG_COLLISION_DATA byte and determines the collision code
; unsafe because it hard-codes a bypass of the bg collision row check, y must be correct here
; example usage is when checking below ground player is standing on to see if player can fall through (drop down)
; input
; * y - BG_COLLISION_DATA offset
; * $12 - specifies which 2 bits of the BG_COLLISION_DATA byte interested in (0, 1, 2, or 3)
; each super-tile has 4 bg collision points, 2 per bg collision row
; one byte contains 4 bg collision points on a single row of 2 super-tiles
; output
; * $13 - BG_COLLISION_DATA offset
; * $14 - collision code
; * a - collision code
; * zero flag - set when collision code #$00 (empty)
; * negative flag - set when collision code #$03 (solid)
; * carry flag - set when collision code #$01 (floor)
read_bg_collision_byte_unsafe:
lda #$00 ; a = #$00
sta $15 ; used by read_bg_collision_byte as a quick way to ensure not reading past last bg collision row
; this method is confident y offset (BG_COLLISION_DATA offset) is correct
beq read_bg_collision_byte ; always branch, get collision code from BG_COLLISION_DATA byte
; input
; * a is x pos
; * y is y pos
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
get_bg_collision:
sta $13 ; store x position in $13
; input
; * $13 - x position
; * y - y position
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
get_enemy_bg_collision:
lda #$00 ; a = #$00
sta $10
beq bg_collision_logic ; always jump because a is #$00
; used for the hangar mine cart
get_cart_bg_collision:
sta $13 ; store sprite x position in $13
; input
; * $13 - x position
; * y - y pos
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
bg_collision_logic:
tya ; transfer y pos to a
sta $15 ; store y pos in $15
clc ; clear carry in preparation for addition
adc VERTICAL_SCROLL ; add vertical scroll offset
bcs @vert_overflow ; branch if overflow
cmp #$f0 ; didn't overflow, compare VERTICAL_SCROLL + Y to #$f0
bcc @continue ; branch if VERTICAL_SCROLL + Y < #$f0
@vert_overflow:
adc #$0f ; add #$0f to vertical scroll
@continue:
sta $11 ; store VERTICAL_SCROLL + Y in $11
lda $13 ; load sprite x position
clc ; clear carry in preparation for addition
adc HORIZONTAL_SCROLL ; horizontal scroll offset
sta $12 ; store HORIZONTAL_SCROLL + X in $12
lda PPUCTRL_SETTINGS ; load PPUCTRL, used to get nametable value
eor $10 ; A XOR $10 (almost always #$00)
; hangar moving carts (enemy type #$14) will use $10
; when moving right and checking for bg collision in opposite nametable
and #$01 ; see if base nametable address is $2400
bcc @bg_collision_data ; jump if no carry when adding HORIZONTAL_SCROLL to X
eor #$01 ; if carry occurred, flip bit 0 of a
; does math to determine correct offset into BG_COLLISION_DATA
@bg_collision_data:
tay ; set level_screen_mem_offset_tbl_01 index
lda $11 ; load VERTICAL_SCROLL + Y
lsr
lsr
and #$3c ; keep bits ..xx xx..
sta $11 ; update VERTICAL_SCROLL + Y to include nametable offset
lda $12 ; load HORIZONTAL_SCROLL + X
lsr
lsr
lsr
lsr
sta $12 ; update HORIZONTAL_SCROLL + X to include nametable offset
lsr
lsr
ora $11 ; merge with adjusted VERTICAL_SCROLL + Y
ora level_screen_mem_offset_tbl_01,y
tay
lda $12 ; load HORIZONTAL_SCROLL + X nametable offset
and #$03 ; keep bits .... ..xx
sta $12 ; set HORIZONTAL_SCROLL + X nametable offset
; reads the specific bits of the BG_COLLISION_DATA byte and determines the collision code
; input
; * y - BG_COLLISION_DATA offset
; * $15 - bg collision row
; * $12 - specifies which 2 bits of the BG_COLLISION_DATA byte interested in (0, 1, 2, or 3)
; each super-tile has 4 bg collision points, 2 per bg collision row
; one byte contains 4 bg collision points on a single row of 2 super-tiles
; output
; * $13 - BG_COLLISION_DATA offset
; * $14 - collision code
; * a - collision code
; * negative flag - set when solid collision code
; * carry flag - set when collision code #$01 (floor)
read_bg_collision_byte:
sty $13 ; store BG_COLLISION_DATA offset in $13
lda $15 ; load bg collision row
cmp #$e0 ; see if past last row
lda #$00 ; a = #$00
bcs @set_code_exit ; set collision code to #$00 (empty) and exit if past last bg collision row
lda BG_COLLISION_DATA,y ; load background collision code
ldy $12 ; load column offset (0, 1, 2, or 3)
beq @shift_6_bits ; collision code stored in bits 6 and 7, shift right to 2 least significant bits
dey
beq @shift_4_bits ; collision code stored in bits 4 and 5, shift right to 2 least significant bits
dey
beq @shift_2_bits ; collision code stored in bits 2 and 3, shift right to 2 least significant bits
bne @no_shift ; collision code already in least 2 significant bits, no shift required
@shift_6_bits:
lsr
lsr
@shift_4_bits:
lsr
lsr
@shift_2_bits:
lsr
lsr
@no_shift:
and #$03 ; store the collision code in a (it's been shifted to right most bits)
tay ; transfer code offset to y
lda collision_code_lookup_tbl,y ; load collision code
@set_code_exit:
sta $14 ; store collision code in $14
lsr ; set carry if collision code is floor (#$01)
lda $14 ; set a register to collision code
rts
; the base offset into cpu graphics buffer where super-tile indexes are loaded (LEVEL_SCREEN_SUPERTILES)
; $0600 or $0640
level_screen_mem_offset_tbl_01:
.byte $00,$40
collision_code_lookup_tbl:
.byte $00,$01,$02,$80
; starts the auto scroll to reveal heart if at right screen, otherwise do nothing
; output
; * a - LEVEL_SCREEN_NUMBER when boss auto scroll not set
; #$00 when boss auto scroll already set, or just set
; #$01 when boss already defeated
set_boss_auto_scroll:
lda BOSS_DEFEATED_FLAG ; 0 = boss not defeated, 1 = boss defeated
bne @exit ; exit if boss is already defeated
lda LEVEL_STOP_SCROLL ; load the screen to stop scrolling on, set to #$ff when boss auto scroll starts
bmi @exit_mark_scroll_enabled ; exit if auto scroll has already started
cmp LEVEL_SCREEN_NUMBER ; screen number
bne @exit ; exit if not on the appropriate screen to start auto scroll
ldy LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
lda LEVEL_SCREEN_SCROLL_OFFSET ; load the number of pixels into LEVEL_SCREEN_NUMBER the level has scrolled
cmp scroll_trigger_tbl,y ; compare to scroll trigger point
bcc @exit ; exit if not yet at spot to trigger auto scroll
lda auto_scroll_timer_tbl,y ; load the appropriate auto scroll timer for the level
sta AUTO_SCROLL_TIMER_00 ; start auto scroll to reveal the boss
lda #$ff ; LEVEL_STOP_SCROLL is #$ff when auto scroll has started
sta LEVEL_STOP_SCROLL ; mark that boss auto scroll has started (LEVEL_STOP_SCROLL = #$ff)
@exit_mark_scroll_enabled:
lda #$00 ; a = #$00
@exit:
rts
; table for offset into screen before initiating auto scroll to show boss ($04 bytes)
; byte 0 - horizontal level
; byte 1 - vertical level
scroll_trigger_tbl:
.byte $a0,$c0
; the amount of time to auto scroll for the end of level
; byte 0 - horizontal level
; byte 1 - vertical level
auto_scroll_timer_tbl:
.byte $60,$40
; screen load
load_next_next_supertiles_screen_indexes:
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
clc ; clear carry bit
adc #$02 ; add #$02 to load screen in the future
bne load_supertiles_screen_indexes ; decompress and load super-tile indexes into LEVEL_SCREEN_SUPERTILES
load_next_supertiles_screen_indexes:
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
clc ; clear carry in preparation for addition
adc #$01 ; add #$01 to load screen in the future
bne load_supertiles_screen_indexes ; decompress and load super-tile indexes into LEVEL_SCREEN_SUPERTILES
; decompresses super-tiles of the current level's current screen to load into CPU memory at LEVEL_SCREEN_SUPERTILES
load_current_supertiles_screen_indexes:
lda LEVEL_SCREEN_NUMBER ; load current screen number within the level
; to know which super-tiles to load
; CPU address $e16b
; decompresses and loads super-tile indexes into LEVEL_SCREEN_SUPERTILES (level_x_supertiles_screen_ptr_table)
; read
; input
; * a - index into the screen_supertile_ptr_table table, i.e. the screen number to load
load_supertiles_screen_indexes:
asl ; double since each entry is a 2-byte address
tax ; save the index before jump to load_bank_number subroutine
ldy #$02 ; tell load_bank_number to load bank 2
jsr load_bank_number ; load bank 2
txa ; restore screen_supertile_ptr_table offset
tay ; move screen_supertile_ptr_table offset to Y
lda (LEVEL_SCREEN_SUPERTILES_PTR),y ; grab low-byte of pointer to level screen super-tiles (from bank 2)
; level_x_supertiles_screen_ptr_table
sta $00 ; store in $00
iny ; increment LEVEL_SCREEN_SUPERTILES_PTR read offset
lda (LEVEL_SCREEN_SUPERTILES_PTR),y ; grab high-byte of level graphic data location
sta $01 ; store in $01
ldy #$00 ; clear y
ldx SUPERTILE_NAMETABLE_OFFSET ; CPU graphic data write offset (LEVEL_SCREEN_SUPERTILES offset)
; decompresses encoded data specifying the super-tiles to display for nametable
read_supertiles_screen_ptr_table:
lda ($00),y ; grab next graphic byte (encoded) (level_x_supertiles_screen_xx)
iny ; increment level super tile graphic data read offset
cmp #$80 ; checking if most significant bit is a 1
bcs load_rle_repeat_command ; if A has msb set, then RLE command
sta LEVEL_SCREEN_SUPERTILES,x ; bit 7 is not set, regular super-tile index. store index in CPU memory
inx ; increment CPU write address offset
; input
; * ($00) should point to the correct level_x_supertiles_screen_xx
; * x - total number of tiles written to LEVEL_SCREEN_SUPERTILES memory location
; * y - offset into specific level_x_supertiles_screen_xx to read
load_supertile_indexes_starting_at_y:
lda LEVEL_SCROLLING_TYPE ; load the current level scrolling type (horizontal or vertical)
bne @vertical_level_section_end ; if level is a vertical level, then jump
cpx #$38 ; screen_supertile_ptr_table data is #$38 bytes for horizontal levels
beq @exit ; exit if read all #$38 super-tiles (horizontal level)
cpx #$78 ; 2 screens worth of super-tiles are loaded, so if started with second screen at offset #$40, stop at #$78
bcc read_supertiles_screen_ptr_table ; jump if read less than #$78 tiles
jmp load_previous_bank ; read #$78 super-tiles, exit
@vertical_level_section_end:
cpx #$40 ; exit if read all #$40 super-tiles (horizontal level)
beq @exit
cpx #$80 ; 2 screens worth of super-tiles are loaded, so if started with second screen at offset #$40, stop at #$80
bcc read_supertiles_screen_ptr_table ; if not finished reading all super-tiles, move to next super-tile
@exit:
jmp load_previous_bank
; read
load_rle_repeat_command:
cmp #$f0
bcs @set_nametable_supertile_indexes ; branch if >= #$f0
and #$7f ; clear first bit
sta $02 ; store number of times to repeat next byte
lda ($00),y ; load the byte that will be repeated
iny ; increment read offset
@repeat_level_data_byte:
sta LEVEL_SCREEN_SUPERTILES,x ; write level graphic byte to CPU memory
inx ; increment CPU memory write offset
dec $02 ; decrement counter for number of times to repeat byte
bne @repeat_level_data_byte ; repeat while $02 > 0
beq load_supertile_indexes_starting_at_y
@set_nametable_supertile_indexes:
and #$0f ; grab least significant 4 bits
asl
asl
asl
ora SUPERTILE_NAMETABLE_OFFSET ; merge with nametable offset (#$00 = nametable 0, #$40 = nametable 1) where super-tile indexes are stored for current screen
sty $03
tay
lda #$08 ; load #$08 super-tiles indexes
sta $02 ; set super-tile index counter to $02
@supertile_index_loop:
lda LEVEL_SCREEN_SUPERTILES,y ; read byte specifying which super-tile to load (level_X_supertiles_screen_XX)
sta LEVEL_SCREEN_SUPERTILES,x
inx ; increment write offset
iny ; increment read offset
dec $02 ; decrement number of remaining super-tiles indexes to load
bne @supertile_index_loop
ldy $03
bne load_supertile_indexes_starting_at_y
; checks if a player is colliding with the current enemy
check_players_collision:
ldx #$01 ; x = #$01
; input
; * x - current player to test
; loop through players and see if colliding with any enemy (including enemy bullets)
@check_player_x_collision:
ldy ENEMY_CURRENT_SLOT ; set y = #$enemy slot
lda PLAYER_STATE,x ; load player state (0 = dropping into level, 1 = normal, 2 = dead, 3 = can't move)
cmp #$01 ; compare to normal state
bne @next_player ; move to next player if current player not in normal state (falling, dead, stuck)
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor; #$80 = indoor/base boss screen
lsr ; shift least significant bit into carry flag
bcc @handle_outdoor_level ; branch for outdoor level or indoor/base boss screen
lda ENEMY_TYPE,y ; indoor level, load current enemy type
cmp #$01 ; see if the enemy is a bullet
bne @check_in_water_crouched ; branch if not a bullet
lda PLAYER_SPRITE_SEQUENCE,x ; enemy is a bullet, load player animation frame
cmp #$02 ; see if player is crouching
bne @check_in_water_crouched ; branch if player isn't crouched on indoor level
beq @next_player ; player crouching go to next player
; can't get hit by bullet when crouching on indoor level
@handle_outdoor_level:
lda ENEMY_STATE_WIDTH,y ; load enemy state width
asl ; shift bit 7 to carry flag
bpl @check_in_water_crouched ; branch if ENEMY_STATE_WIDTH,x bit 6 is clear (player can't land on enemy)
asl ; shift bit 6 to carry flag
bpl @check_player_jumping ; branch if ENEMY_STATE_WIDTH,x bit 5 is clear (collision box code bit)
lda ENEMY_Y_POS,y ; ENEMY_STATE_WIDTH,x bit 5 set, load enemy y position on screen
sec ; set carry flag in preparation for subtracting
sbc SPRITE_Y_POS,x ; player sprite y position on screen
bcc @set_collision_box_code ; branch if player below enemy
cmp #$08 ; player above enemy, see how close to enemy
lda ENEMY_STATE_WIDTH,y ; load enemy state width
and #$ef ; clear bit 4 (collision box type)
bcs @set_state_width ; branch if farther than #$08 from enemy, to use cleared bit 4 collision code
@set_collision_box_code:
lda ENEMY_STATE_WIDTH,y
ora #$10 ; set bit 4 (collision box type)
@set_state_width:
sta ENEMY_STATE_WIDTH,y
bne @check_in_water_crouched
@check_player_jumping:
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
beq @check_in_water_crouched ; branch if player is not jumping
lda PLAYER_Y_FAST_VELOCITY,x ; player is jumping, load y fast velocity
bmi @next_player ; move to next player if player ascending in jump for enemies with bit 5 clear of ENEMY_STATE_WIDTH
cmp #$01 ; player not ascending, compare y fast velocity to #$01
bcc @next_player ; move to next player if player y fast velocity #$00 for enemies with bit 5 clear of ENEMY_STATE_WIDTH
; !(OBS) not sure why didn't use beq here
bcs @check_in_water_crouched ; !(OBS) conditionally branches to next line
; no matter value of condition @check_in_water_crouched will execute
@check_in_water_crouched:
ldy #$00 ; default collision box offset collision_box_codes_tbl (in water)
lda PLAYER_WATER_STATE,x ; load player in water state
beq @set_collision_code_offset ; branch if player not in water
lda CONTROLLER_STATE,x ; player in water, load controller state to see if crouching
and #$04 ; bits .... .x.. (down button pressed)
bne @next_player ; player is invisible when crouching in water, don't test for collision
beq @check_if_enemy_collision ; player is in water, but not crouching, test collision
; player not in water
@set_collision_code_offset:
iny ; y = #$1, player is jumping collision_box_codes_tbl
lda PLAYER_JUMP_STATUS,x ; low nibble 1 = jumping, 0 not jumping; high nibble = facing direction
bne @check_if_enemy_collision ; branch if player is jumping
iny ; player not jumping, increment collision box offset
lda PLAYER_SPRITE_CODE,x ; load player sprite
cmp #$17 ; compare to crouching
beq @check_if_enemy_collision ; player animation frame is #$17 (crouching)
iny ; increment if player animation frame isn't #$17
; test against correct enemy collision box depending on player state (register y)
@check_if_enemy_collision:
jsr set_enemy_collision_box ; set collision box in [$08-$0b] based off y register
lda SPRITE_Y_POS,x ; load current sprite Y position
sec ; set the carry flag in preparation for subtraction
sbc $08 ; subtract from collision box top left y coordinate
cmp $0a ; compare to height of collision box
bcs @next_player ; branch if player sprite is vertically outside of collision box [(SPRITE_Y_POX,x - $08) < $0a]
; this is a neat trick
; * y is above collision box - subtraction result is negative and the carry is set since cmp thinks the value is positive
; * y is below collision box - subtraction result is greater than height $0a and the carry is set
lda SPRITE_X_POS,x ; player sprite is in collision box vertically, now check horizontally
sec ; set the carry flag in preparation for subtraction
sbc $09 ; subtract the top-left x coordinate of the collision box from the x position
cmp $0b ; compare to the width of the collision box
; same neat trick applies here
; * x is to the left of the collision box - subtraction result is negative and carry is set since cmp thinks value is positive
; * x is to the right of the collision box - subtraction result is greater than width ($08) so carry is set
bcc @inside_enemy_collision_box ; branch if inside of collision box (both horizontally and vertically) [(SPRITE_X_POX,x - $09) < $0b]
@next_player:
dex ; decrement player index
bmi @exit_00 ; branch if finished looping through sprites
jmp @check_player_x_collision ; loop to next player
@exit_00:
ldx ENEMY_CURRENT_SLOT
rts
; player landed on non-dangerous enemy, e.g. moving cart or floating rock in vertical level
; #$14 - mining cart, #$15 - stationary mining cart, #$10 - floating rock platform
; move the player as the enemy moves
@land_on_enemy:
lda SPRITE_Y_POS,x ; load the player's Y position
cmp PLAYER_FALL_X_FREEZE,x
bcc @next_player
lda #$01 ; a = #$01
sta ENEMY_FRAME,y ; enemy animation frame number to #$01, lets mining cart know to start moving
lda ENEMY_X_VELOCITY_FRACT,y
clc ; clear carry in preparation for addition
adc ENEMY_X_VEL_ACCUM,y
lda ENEMY_X_VELOCITY_FAST,y ; load enemy fast velocity
adc #$00 ; add any fractional overflow
clc ; clear carry in preparation for addition
adc PLAYER_FAST_X_VEL_BOOST,x ; add any existing boost
; can support being on a moving enemy that is on a moving enemy
sta PLAYER_FAST_X_VEL_BOOST,x ; set any boost to player's x velocity by being on a moving enemy
lda ENEMY_STATE_WIDTH,y
asl
asl
asl
lda #$e4 ; a = #$e4 (-28)
bcc @set_landing_pos ; branch if ENEMY_STATE_WIDTH,y bit 5 is 0
lda #$e8 ; a = #$e8 (-24)
@set_landing_pos:
clc ; clear carry in preparation for addition
adc ENEMY_Y_POS,y ; subtract 24 or 28 from enemy y position on screen
sta SPRITE_Y_POS,x ; set player y position
lda #$01 ; a = #$01
sta PLAYER_ON_ENEMY,x ; set player on enemy flag
jsr player_land_on_ground
jmp @next_player
; player sprite collides with enemy, or enemy bullet
@inside_enemy_collision_box:
ldy ENEMY_CURRENT_SLOT ; load slot index of enemy that is being collided with
lda ENEMY_X_POS,y ; load enemy x position on screen
sec ; set the carry flag in preparation for subtraction
sbc SPRITE_X_POS,x ; subtract the x position of player sprite
bcs @check_can_land_on ; branch if positive (player to left of enemy)
eor #$ff ; player to right of enemy - flip all bits
adc #$01 ; add 1 for correction
@check_can_land_on:
cmp #$80
bcs @next_player
lda ENEMY_STATE_WIDTH,y ; load ENEMY_STATE_WIDTH
asl ; shift bit 7 to carry
bpl @collide_with_enemy ; branch to collide with enemy if can't land on them
and #$20 ; player can land on enemy, keep bit 5 (collision box code)
beq @land_on_enemy ; player can land on enemy (floating rock and moving cart)
@collide_with_enemy:
lda ENEMY_TYPE,y ; load current enemy type
beq pick_up_weapon_item ; branch if weapon item enemy
lda NEW_LIFE_INVINCIBILITY_TIMER,x ; timer for invincibility (after dying)
bne @next_player ; when still invincibility after dying, player walks through enemy, skip
lda INVINCIBILITY_TIMER,x
bne @invincible_collision ; branch if player is invincible (barrier weapon) to set enemy HP to #$00
jsr kill_player ; player collided with enemy sprite, kill player
lda ENEMY_TYPE,y ; load current enemy type
cmp #$01 ; compare to bullet
beq remove_current_enemy ; remove bullet after collision
ldx ENEMY_CURRENT_SLOT
rts
@invincible_collision:
stx $17 ; store current player number in $17
ldx ENEMY_CURRENT_SLOT ; load current enemy slot number
lda ENEMY_HP,x ; load enemy hp
beq @exit_01
cmp #$f0
bcs @exit_01
lda #$00 ; a = #$00
sta ENEMY_HP,x ; set enemy hp
jsr add_enemy_score_set_enemy_routine ; adds score amount to player score, sets enemy destroyed routine
@exit_01:
rts
; player has collided with a weapon item, pick it up
pick_up_weapon_item:
stx $10
lda #$0a ; a = #$0a
sta $00 ; set score to add to player as #$0a (1,000 points)
txa
tay
jsr add_player_low_score ; add points to player score, check if new high score and extra life
ldx $10
ldy ENEMY_CURRENT_SLOT
lda #$1f ; a = #$1f (sound_1f)
jsr play_sound ; play weapon item taken sound
lda ENEMY_ATTRIBUTES,y ; get weapon item attributes
and #$07 ; keep bits 0-3 (attributes)
beq @set_rapid_flag ; set rapid flag for weapon if attribute is #$00
cmp #$05 ; check for b weapon (barrier). Gives invincibility
bcc @compare_and_set_weapon ; branch if less than #$05 (MFSL)
beq @set_invincibility_timer ; branch for barrier weapon
jsr destroy_all_enemies ; falcon weapon effect - destroy all enemies
lda #$20 ; a = #$20
sta FALCON_FLASH_TIMER ; set falcon weapon flash timer to #$20 frames
bne remove_current_enemy
; b weapon effect (barrier). Gives invincibility
; decreases every 8 frames
; NTSC is about #3c frames per second
; PAL is close to #$32 frames per second
; NTSC: #$80 * #$8 = #$400 / #$3c = 17.06667 (decimal) seconds
; NTSC: #$90 * #$8 = #$480 / #$3c = 19.2 (decimal) seconds
@set_invincibility_timer:
lda #$80 ; set duration of the b weapon effect
ldy CURRENT_LEVEL ; current level
cpy #$06 ; check if level 7 (hangar)
bne @continue
lda #$90 ; set duration for level 7
@continue:
sta INVINCIBILITY_TIMER,x
jmp remove_current_enemy
; r weapon
@set_rapid_flag:
lda #$10 ; a = #$10
sta $08
ldy #$ff ; y = #$ff
bne @set_player_weapon
; default for MFSL Weapons
; compare weapon being picked up with current weapon
; if the same, rapid fire flag is kept; otherwise it is dropped
@compare_and_set_weapon:
ldy #$f0 ; y = #$f0 (keep rapid fire flag)
sta $08 ; store weapon item attributes in $08
eor P1_CURRENT_WEAPON,x ; test to see if current weapon matches weapon item (XOR)
and #$0f ; compare to weapon regardless of rapid fire flag
beq @set_player_weapon ; keep rapid fire flag (if set) when picking up same weapon
ldy #$e0 ; remove rapid fire flag since picking up different weapon
@set_player_weapon:
tya ; y = #$f0 or #$e0 depending if same weapon was picked up
and P1_CURRENT_WEAPON,x ; strip/set rapid fire flag
ora $08 ; merge in rapid fire flag with weapon being picked up
sta P1_CURRENT_WEAPON,x ; set current player's weapon
remove_current_enemy:
ldx ENEMY_CURRENT_SLOT
jmp remove_enemy ; remove enemy
; loop through player bullets and see if they collide with current enemy
bullet_enemy_collision_test:
lda ENEMY_STATE_WIDTH,x ; load current enemy ENEMY_STATE_WIDTH
and #$30 ; keep bits 4 and 5 (collision box code)
asl
asl
sta $10 ; store enemy's original collision box code in $10
ldy #$04 ; override enemy collision box to box code #$04
jsr set_enemy_collision_box ; set collision box in [$08-$0b] for box code #$04 (bullet collision box code)
ldx #$0f ; loop through player bullets
@loop:
lda PLAYER_BULLET_SPRITE_CODE,x ; load current player bullet sprite
beq @next_bullet ; move to next bullet if no bullet at current position
lda PLAYER_BULLET_ROUTINE,x ; load player bullet routine
cmp #$01 ; compare to #$01
bne @next_bullet ; move to next bullet if not routine 01
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
lsr
bcc @test_collision ; branch for outdoor level
ldy $10 ; indoor level, load collision box code
bpl @bullet_delay ; branch if collision box is positive, i.e. non-zero
lda PLAYER_BULLET_SLOT,x ; load bullet type + 1
bmi @test_collision
bpl @next_bullet ; branch if bullet exists (PLAYER_BULLET_SLOT is non-zero)
@bullet_delay:
lda PLAYER_BULLET_TIMER,x
cmp #$02
bcs @next_bullet
@test_collision:
lda PLAYER_BULLET_Y_POS,x
sbc $08
cmp $0a
bcs @next_bullet
lda PLAYER_BULLET_X_POS,x
sbc $09
cmp $0b
bcc @bullet_enemy_collision
@next_bullet:
dex
bpl @loop
ldx ENEMY_CURRENT_SLOT
rts
@bullet_enemy_collision:
ldy ENEMY_CURRENT_SLOT ; load the current enemy slot index
lda ENEMY_X_POS,y ; load enemy x position on screen
sec ; set carry flag in preparation for subtraction
sbc PLAYER_BULLET_X_POS,x ; ENEMY_X_POS - PLAYER_BULLET_X_POS
bcs @continue ; branch if bullet hits enemy from left
eor #$ff ; flip all bits
adc #$01 ; add #$01 (convert to positive)
@continue:
cmp #$80
bcs @next_bullet
lda PLAYER_BULLET_OWNER,x
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 ; see if bullet slot number to #$0a
bcc @continue_2
ldx #$0a ; x = #$0a
@continue_2:
lda PLAYER_BULLET_ROUTINE,x
cmp #$01
beq @set_routine_exit
inx
cpx $08
bne @continue_2
@set_routine_exit:
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)
; set bullet delay to #$06
set_bullet_routine_to_2:
lda #$02 ; used to specify the bullet routine, see player_bullet_routine_0X_ptr_tbl and player_bullet_routine_indoor_0X_ptr_tbl
sta PLAYER_BULLET_ROUTINE,x ; move to last bullet routine for bullet (player_bullet_collision_routine), this handles destroying the bullet
lda #$06 ; a = #$06
sta PLAYER_BULLET_TIMER,x
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
ldy ENEMY_CURRENT_SLOT ; load current enemy slot
lda ENEMY_HP,y ; load enemy hp
beq @exit ; exit if enemy HP already #$00
cmp #$f0
bcs @exit ; exit if enemy HP is negative
sbc #$00 ; subtract #$01 from enemy HP (carry is clear)
bcs @continue
lda #$00 ; a = #$00
@continue:
sta ENEMY_HP,y ; update enemy hp
bne @play_collision_sound ; play collision sound if appropriate
ldx ENEMY_CURRENT_SLOT
jsr add_enemy_score_set_enemy_routine ; adds score amount to player score, sets enemy destroyed routine
ldx $11 ; restore bullet index back in x
rts
@play_collision_sound:
lda ENEMY_STATE_WIDTH,y
and #$04 ; keep bit 2 (play bullet collision sound bit)
beq @exit ; exit if shouldn't play sound
lda ENEMY_VAR_A,y ; load appropriate sound index for current enemy
tay
lda bullet_hit_sound_tbl,y ; load sound code
jsr play_sound ; play bullet collision sound
@exit:
rts
; table for bullet hit sound codes ($05 bytes)
; #$16 = Normal Hit (sound_16)
; #$18 = Heart Hit (sound_18)
; #$14 = Core Plating Hit (sound_14)
bullet_hit_sound_tbl:
.byte $16,$16,$16,$18,$14
; determine enemy (including enemy bullets) collision box
; input
; * y - the collision box table to use, depends on player state (is player crouching, etc)
; * 0 - player is in water
; * 1 - player is jumping
; * 2 - player is crouching
; * 3 - normal
; * 4 - bullet collision box code
; stores collision corners for enemy in $08 (top left Y), $09 (top left X), $0a (width), $0b (height)
set_enemy_collision_box:
stx $11 ; temporarily save x into $11 for duration of method
ldx ENEMY_CURRENT_SLOT ; load current enemy slot index
tya ; move y (desired collision box table) to a so it can be doubled with asl
asl ; double since table is 2 bytes each
tay ; move value back to y
lda collision_box_codes_tbl,y ; load low byte of desired collision box table
sta $0e ; store low byte in $0e
lda collision_box_codes_tbl+1,y ; load high byte of desired collision box table
sta $0f ; store high byte in $0f
lda ENEMY_SCORE_COLLISION,x ; load specified score and collision codes for enemy
and #$0f ; filter to only the collision code part of byte (low 4 bits)
cmp #$0f ; compare to all ones
beq @collision_code_f ; branch if collision code f (fire beams and rising spiked walls)
asl ; double collision code
asl ; double again, since each collision box is 4 bytes
tay ; transfer to offset register
lda ENEMY_Y_POS,x ; load enemy y position on screen
adc ($0e),y ; add collision_box_codes_XX,y to y position (frequently this is actually subtraction)
sta $08 ; store y position on screen of top-left of collision box
iny ; move to next byte to read
lda ENEMY_X_POS,x ; load enemy x position on screen
clc ; clear carry in preparation for addition
adc ($0e),y ; add (collision_box_codes_XX,y) from x position (frequently this is actually subtraction)
sta $09 ; store x position on screen of top-left of collision box
iny ; move to next byte to read
lda ($0e),y ; read height of collision box
sta $0a ; store height in $0a
iny ; move to next byte to read
lda ($0e),y ; read width of collision box
sta $0b ; store width in $0b
ldx $11 ; restore x back to value before set_enemy_collision_box was called
rts
; used for fire beams and rising spiked walls, i.e. things with variable sized collision boxes
@collision_code_f:
lda ENEMY_ATTRIBUTES,x ; load the enemy's attributes
asl
asl ; shift bit 6 of ENEMY_ATTRIBUTES into carry
lda ENEMY_VAR_1,x ; load collision_code_f_base_tbl offset stored in ENEMY_VAR_1
bcc @continue ; branch if bit 6 of ENEMY_ATTRIBUTES,x is 0
eor #$ff ; bit 6 set, negate the , flip all bits
adc #$00 ; add 1
@continue:
clc ; clear carry in preparation for addition
adc #$08 ; add #$08 to ENEMY_VAR_1,x
sta $0c ; store result in $0c
tya ; transfer which collision box table to use to y [#$00-#$04]
asl ; double (already doubled y) since each entry is #$04 bytes
tay ; transfer back to offset register
lda collision_code_f_base_tbl,y ; load initial y coordinate of top left of collision box
sta $08 ; store y coordinate of top left of collision box in $08
lda collision_code_f_base_tbl+1,y ; load initial x coordinate of top left of collision box
sta $09 ; store x coordinate of top left of collision box in $09
lda collision_code_f_base_tbl+2,y ; load initial height of top left of collision box
sta $0a ; store height of collision box in $0a
lda collision_code_f_base_tbl+3,y ; load initial width of top left of collision box
sta $0b ; store width of collision box in $0b
lda ENEMY_ATTRIBUTES,x ; load enemy attributes
lsr
lsr
lsr
lsr ; move high nibble to low nibble
and #$0c ; keep bits .... xx.. (0, 4, or 8, or 12)
tay ; transfer to offset register
lda collision_code_f_adj_tbl,y ; load top-left y coordinate offset
jsr @replace_placeholder ; if placeholder value, replace it with $c0 (or its negative)
clc ; clear carry in preparation for addition
adc $08 ; add to top-left y coordinate
clc ; clear carry in preparation for addition
adc ENEMY_Y_POS,x ; add to enemy y position on screen
sta $08 ; set top-left y coordinate of collision box based
lda collision_code_f_adj_tbl+1,y ; load top-left x coordinate offset
jsr @replace_placeholder ; if placeholder value, replace it with $c0 (or its negative)
clc ; clear carry in preparation for addition
adc $09 ; add to top-left x coordinate
clc ; clear carry in preparation for addition
adc ENEMY_X_POS,x ; add to enemy x position on screen
sta $09 ; set top-left x coordinate of collision box based
lda collision_code_f_adj_tbl+2,y ; load collision box height adjustment
jsr @replace_placeholder ; if placeholder value, replace it with $c0 (or its negative)
clc ; clear carry in preparation for addition
adc $0a ; add to base height
sta $0a ; set new collision box height
lda collision_code_f_adj_tbl+3,y ; load collision box width
jsr @replace_placeholder ; if placeholder value, replace it with $c0 (or its negative)
clc ; clear carry in preparation for addition
adc $0b ; add to base collision box width
sta $0b ; set new collision box width
ldx $11 ; restore x back to value before set_enemy_collision_box was called
rts
; replaces the variable placeholder (#$fe, #$ff) with adjustment value $0c (or its negative)
; input
; * a - value from collision_code_f_adj_tbl
; output
; * a
; * if input a < #$fe, a (not a placeholder)
; * if input a == #$fe, negative value of $0c
; * if input a == #$ff, $0c
; note: $0c is ENEMY_VAR_1 or negative ENEMY_VAR_1 depending on bit 6 of ENEMY_ATTRIBUTES
@replace_placeholder:
cmp #$fe ; compare collision box adjustment value to to #$fe
bcc @exit ; exit if value less than #$fe. it is not a placeholder
lsr ; shift bit 0 of collision_code_f_adj_tbl value to the carry flag
lda $0c ; load adjustment control variable
bcs @exit ; exit to $0c if placeholder is #$ff (odd)
eor #$ff ; placeholder is #$fe (event), return negative $0c, flip all bits
adc #$01 ; add one
@exit:
rts
; table for f collision code initial collision box rectangles depending on collision code ($14 bytes)
; byte 0 - initial top left y coordinate
; byte 1 - initial top left x coordinate
; byte 2 - initial collision box height
; byte 3 - initial collision box width
collision_code_f_base_tbl:
.byte $00,$fb,$0a,$0a ; (-5, 0 ) - player is in water
.byte $f8,$fc,$10,$08 ; (-4, -8 ) - player is jumping
.byte $f4,$f5,$04,$16 ; (-11, -12) - player is crouching
.byte $f1,$fc,$1d,$08 ; (-14, -15) - normal
.byte $fe,$fe,$04,$04 ; (-2 , -2 ) - bullet collision box code
; table for adjustment of initial f code collision box based (indirectly) on ENEMY_VAR_1 ($10 bytes)
; these values will be overwritten if value is #$fe or #$ff
; byte 0 - top left y coordinate adjustment
; byte 1 - top left x coordinate adjustment
; byte 2 - collision box height adjustment
; byte 3 - collision box width adjustment
collision_code_f_adj_tbl:
.byte $fa,$f8,$0c,$ff ; variable width fixed x (growing right)
.byte $fa,$fe,$0c,$ff ; variable width variable x (growing left)
.byte $f8,$fa,$ff,$0c ; variable height fixed y (growing downward)
.byte $fe,$f6,$ff,$14 ; variable height variable y (growing upward)
; pointer table to list of collision box codes for each player state ($05 * $02 = $0a bytes)
; CPU address $e4e8
collision_box_codes_tbl:
.addr collision_box_codes_00 ; CPU address $e4f2 - player in water
.addr collision_box_codes_01 ; CPU address $e52e - player jumping
.addr collision_box_codes_02 ; CPU address $e56a - player standing
.addr collision_box_codes_03 ; CPU address $e5a6 - player crouching
.addr collision_box_codes_04 ; CPU address $e5e2
; each 4 bytes is a a collision box code
; #$e different collision box codes
; player is in water
collision_box_codes_00:
.byte $f1,$f7,$28,$12
.byte $fe,$f9,$14,$14
.byte $f8,$f3,$1a,$1a
.byte $f2,$ed,$26,$26
.byte $e0,$f0,$08,$20
.byte $fd,$f8,$10,$10
.byte $f5,$f7,$20,$12
.byte $e7,$e2,$3c,$3c
.byte $f1,$e2,$28,$3c
.byte $e4,$d3,$48,$5a
.byte $00,$f6,$16,$16
.byte $08,$f1,$12,$1e
.byte $f5,$f1,$20,$1e
.byte $ea,$ec,$37,$28
.byte $f3,$f3,$11,$1a
; each 4 bytes is a a collision box code
; player is jumping
collision_box_codes_01:
.byte $ea,$f8,$1e,$10
.byte $f6,$fa,$0c,$0c
.byte $f1,$f5,$12,$16
.byte $ea,$ee,$24,$24
.byte $e0,$f0,$08,$20
.byte $f5,$f9,$0e,$0e
.byte $ed,$f8,$1e,$10
.byte $df,$e3,$3a,$3a
.byte $e9,$e3,$26,$3a
.byte $dc,$d4,$46,$58
.byte $f8,$f7,$14,$14
.byte $00,$f2,$10,$1c
.byte $ed,$f2,$1e,$1c
.byte $e2,$ed,$35,$26
.byte $f3,$f1,$12,$1e
; each 4 bytes is a a collision box code
; player is crouching
collision_box_codes_02:
.byte $f4,$f1,$0d,$1e
.byte $f3,$f4,$04,$18
.byte $ec,$ed,$10,$26
.byte $e6,$e7,$1c,$32
.byte $e0,$f0,$08,$20
.byte $f1,$f2,$06,$1c
.byte $e9,$f1,$16,$1e
.byte $db,$dc,$32,$48
.byte $e5,$dc,$1e,$48
.byte $d8,$cd,$3e,$66
.byte $f4,$f0,$19,$22
.byte $fc,$eb,$08,$2a
.byte $e3,$f2,$1a,$1c
.byte $de,$e6,$2d,$34
.byte $e9,$ea,$0d,$2c
; each 4 bytes is a a collision box code
; player is standing on ground
collision_box_codes_03:
.byte $f3,$f8,$24,$10
.byte $f0,$fb,$1f,$0a
.byte $ea,$f5,$2b,$16
.byte $e3,$ee,$39,$24
.byte $e0,$f0,$08,$20
.byte $ee,$f9,$23,$0e
.byte $e6,$f8,$33,$10 ; #$33, #$10 is the start of sound dpcm sound sample (length 385) !(BUG?)
.byte $d8,$e3,$4f,$3a ; used on level 5 (snow field) after defeating the boss
.byte $e2,$e3,$3b,$3a ; this DPCM sample does not occur in the Japanese version
.byte $d5,$d4,$5b,$58
.byte $f1,$f7,$29,$14
.byte $f9,$f2,$25,$1c
.byte $e4,$f2,$35,$1c
.byte $da,$ed,$4a,$26
.byte $e6,$f1,$2a,$1e
; each 4 bytes is a a collision box code
; bullet collision code
collision_box_codes_04:
.byte $ee,$f5,$24,$16
.byte $fc,$fc,$08,$08
.byte $f5,$f5,$16,$16
.byte $ef,$ef,$22,$22
.byte $e0,$f0,$08,$20
.byte $fa,$fa,$0c,$0c
.byte $f3,$fa,$16,$0c
.byte $e4,$e4,$38,$38
.byte $ee,$e4,$24,$38
.byte $e1,$d5,$44,$56
.byte $fd,$f8,$12,$12
.byte $05,$f3,$0e,$1a
.byte $f3,$f3,$1a,$1a
.byte $e7,$ee,$33,$24
.byte $f2,$f2,$13,$1c
; execute all enemy routines
exe_all_enemy_routine:
lda #$00 ; a = #$00
sta PLAYER_ON_ENEMY ; clear player on non-dangerous enemy flag
sta $b7
ldx #$0f ; x = #$0f
exe_enemy_routine_loop:
lda ENEMY_ROUTINE,x ; enemy routine index
beq advance_enemy ; no enemy loaded in slot, continue to next enemy
stx ENEMY_CURRENT_SLOT ; store current enemy slot offset into $83
asl ; double enemy routine index since each entry in enemy routine table is 2 bytes
sta $04 ; save enemy routine index (enemy_routine_ptr_tbl or level_enemy_routine_ptr_tbl)
jsr exe_enemy_routine ; execute the xth (current) enemy routine's current sub-routine
lda ENEMY_SPRITES,x ; enemy sprite data: sprite code, attribute, etc.
beq advance_enemy ; no enemy tiles specified, continue to next enemy
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
lsr
bcc @handle_outdoor ; branch for outdoor level
lda ENEMY_Y_POS,x ; indoor level, load enemy y position on screen
cmp #$9c
bcc @continue
@handle_outdoor:
lda ENEMY_STATE_WIDTH,x
lsr ; shift bit 0 into the carry flag
bcs @continue
jsr check_players_collision ; check if players are colliding with current enemy
@continue:
lda ENEMY_STATE_WIDTH,x ; see if enemy is active by checking bit 7
bmi advance_enemy ; branch if bit 7 is set
jsr bullet_enemy_collision_test ; enemy is active, check collision
; decrement x and if it's greater than #$00 jump back to execute enemy routine
advance_enemy:
dex
bpl exe_enemy_routine_loop
rts
; executes the xth enemy routine
; if the enemy type (ENEMY_TYPE,x) is >= #$10, then it is a level-specific enemy routine, i.e. an enemy unique for the level
; each level uses different enemies for these values, they executed from this label as well
; input
; * $04 - the enemy routine index (which sub-routine inside the enemy routine to execute)
; x has the current enemy slot number
exe_enemy_routine:
lda ENEMY_TYPE,x ; load current enemy type
asl ; double since enemy type pointer is 2 bytes
tay ; store offset into pointer table into y
lsr ; undo the doubling to see the offset number
cmp #$10 ; compare offset to #$10
bcs exec_level_enemy_routine ; offset is greater than or equal to #$10 (level-specific enemy)
lda enemy_routine_ptr_tbl,y ; load low byte of the routine pointer
sta $02 ; store in $02
lda enemy_routine_ptr_tbl+1,y ; load high byte of the enemy pointer
; $04 stores the enemy routine index (which sub-routine inside the enemy routine to execute)
exe_enemy_routine_subroutine:
sta $03 ; store enemy routine high byte into $03
ldy $04 ; load which sub-routine to execute
lda ($02),y ; load the low byte of the enemy sub-routine to execute
sta $04 ; store in $04
iny ; increment read offset
lda ($02),y ; load the high byte of the enemy sub-routine to execute
sta $05 ; store in $05
jmp ($04) ; jump to that sub-routine
; enemy type >= 10 (level specific)
exec_level_enemy_routine:
tya ; move current (doubled) enemy type into a
sbc #$20 ; subtract #$20 (since we are indexing from level specific enemy types)
tay ; store offset into y
lda (ENEMY_LEVEL_ROUTINES),y ; load current level-specific enemy routines low byte into a
sta $02 ; store into $02
iny ; increment read offset
lda (ENEMY_LEVEL_ROUTINES),y ; load current level-specific enemy routines high byte into a
jmp exe_enemy_routine_subroutine ; loads the current sub-routine for the current enemy
; stores the enemy routines table 2-byte address for the current level into ENEMY_LEVEL_ROUTINES ($80)
load_level_enemies_to_mem:
lda CURRENT_LEVEL ; load the current level
asl ; double the number since each address is 2 bytes
tay ; transfer offset to y
lda level_enemy_routine_ptr_tbl,y ; load low byte of level-specific enemy table to y
sta ENEMY_LEVEL_ROUTINES ; store low byte into $80
lda level_enemy_routine_ptr_tbl+1,y ; load high byte of level-specific enemy table to y
sta ENEMY_LEVEL_ROUTINES+1 ; store high byte into $81
rts
; pointer table for enemy routines by level ($08 * $02 = $10 bytes)
; each entry is an address pointing to another table listing the tables of
; routines for each enemy specific to the level
level_enemy_routine_ptr_tbl:
.addr enemy_routine_level_1 ; CPU address $e6c8
.addr enemy_routine_level_2_4 ; CPU address $e6ce
.addr enemy_routine_level_3 ; CPU address $e6f0
.addr enemy_routine_level_2_4 ; CPU address $e6ce
.addr enemy_routine_level_5 ; CPU address $e6fc
.addr enemy_routine_level_6 ; CPU address $e70a
.addr enemy_routine_level_7 ; CPU address $e714
.addr enemy_routine_level_8 ; CPU address $e726
; pointer table for common enemies routines - codes 00 to 0f
; bank 0 and bank 7 labels
; every entry is 2 bytes less than the actual label
enemy_routine_ptr_tbl:
.addr weapon_item_routine_ptr_tbl-2 ; weapon item (00) - CPU address $8001 bank 0
.addr enemy_bullet_routine_ptr_tbl-2 ; enemy bullet (01) - CPU address $8147 bank 0
.addr weapon_box_routine_ptr_tbl-2 ; pill box sensor (02) - CPU address $8205 bank 0
.addr flying_capsule_routine_ptr_tbl-2 ; flying capsule (03) - CPU address $8305 bank 0
.addr rotating_gun_routine_ptr_tbl-2 ; rotating gun (04) - CPU address $8379 bank 0
.addr soldier_routine_ptr_tbl-2 ; soldier (05) - CPU address $8608 bank 0
.addr sniper_routine_ptr_tbl-2 ; sniper (06) - CPU address $8946 bank 0
.addr red_turret_routine_ptr_tbl-2 ; red turret (07) - CPU address $84b8 bank 0
.addr wall_cannon_routine_ptr_tbl-2 ; wall cannon (08) - CPU address $efb7
.addr enemy_routine_do_nothing_ptr_tbl-2 ; unused (09) - CPU address $e734
.addr wall_plating_routine_ptr_tbl-2 ; wall plating (0a) - CPU address $f077
.addr mortar_shot_routine_ptr_tbl-2 ; mortar shot (0b) - CPU address $f1c4
.addr scuba_soldier_routine_ptr_tbl-2 ; scuba diver (0c) - CPU address $f13b
.addr enemy_routine_do_nothing_ptr_tbl-2 ; unused (0d) - CPU address $e734
.addr turret_man_routine_ptr_tbl-2 ; turret man (0e) - CPU address $f0bd
.addr turret_man_bullet_routine_ptr_tbl-2 ; turret man bullet (0f) - CPU address $f119
; pointer table for level 1 enemy routines (#$2 * #$3 = #$6 bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_1:
.addr bomb_turret_routine_ptr_tbl-2 ; boss bomb turret (10) - CPU address $8b47
.addr boss_wall_plated_door_routine_ptr_tbl-2 ; door plate with siren (11) - CPU address $8bc7
.addr exploding_bridge_routine_ptr_tbl-2 ; exploding bridge (12) - CPU address $8c50
; pointer table for level 2/4 enemy routines (#$11 * #$2 = #$22 bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_2_4:
.addr boss_eye_routine_ptr_tbl-2 ; Boss Eye (10) - CPU address $8e65
.addr roller_routine_ptr_tbl-2 ; Rollers (11) - CPU address $8f80
.addr grenade_routine_ptr_tbl-2 ; Grenades (12) - CPU address $8fc7
.addr wall_turret_routine_ptr_tbl-2 ; Wall Turret (13) - CPU address $908a
.addr wall_core_routine_ptr_tbl-2 ; Core (14) - CPU address $910e
.addr indoor_soldier_routine_ptr_tbl-2 ; Running Guy (15) - CPU address $92b8
.addr jumping_soldier_routine_ptr_tbl-2 ; Jumping Guy (16) - CPU address $936e
.addr grenade_launcher_routine_ptr_tbl-2 ; Seeking Guy (17) - CPU address $9458
.addr four_soldiers_routine_ptr_tbl-2 ; Group of 4 (18) - CPU address $952f
.addr indoor_soldier_gen_routine_ptr_tbl-2 ; Indoor Soldier Generator (19) - CPU address $8d17
.addr indoor_roller_gen_routine_ptr_tbl-2 ; Rollers Generator (1A) - CPU address $95c0
.addr eye_projectile_routine_ptr_tbl-2 ; Sphere Projectile (1B) - CPU address $8f33
.addr boss_gemini_routine_ptr_tbl-2 ; Boss Gemini (1C) - CPU address $9ef3
.addr spinning_bubbles_routine_ptr_tbl-2 ; Spinning Bubbles (1D) - CPU address $a04f
.addr blue_soldier_routine_ptr_tbl-2 ; Blue Jumping Guy (1E) - CPU address $a147
.addr red_soldier_routine_ptr_tbl-2 ; Red Shooting Guy (1F) - CPU address $a258
.addr red_blue_soldier_gen_routine_ptr_tbl-2 ; Red/Blue Guys Generator (20) - CPU address $a2fc
; pointer table for level 3 enemy routines (#$6 * #$2 = #$c bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_3:
.addr floating_rock_routine_ptr_tbl-2 ; Rock Platform (10) - CPU address $97e3
.addr moving_flame_routine_ptr_tbl-2 ; Moving Flame (11) - CPU address $983a
.addr rock_cave_routine_ptr_tbl-2 ; Falling Rock Generator (12) - CPU address $9855
.addr falling_rock_routine_ptr_tbl-2 ; Falling Rock (13) - CPU address $987b
.addr boss_mouth_routine_ptr_tbl-2 ; Level 3 Boss Mouth (14) - CPU address $9916
.addr dragon_arm_orb_routine_ptr_tbl-2 ; Level 3 Dragon Arm Orb (15) - CPU address $9a8a
; pointer table for level 5 enemy routines (#$7 * #$2 = #$e bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_5:
.addr ice_grenade_generator_routine_ptr_tbl-2 ; Grenade Generator (10) - CPU address $a382
.addr ice_grenade_routine_ptr_tbl-2 ; Grenade (11) - CPU address $a3a9
.addr tank_routine_ptr_tbl-2 ; Tank (12) - CPU address $a40c
.addr ice_separator_routine_ptr_tbl-2 ; Pipe Joint (13) - CPU address $a981
.addr boss_ufo_routine_ptr_tbl-2 ; Alien Carrier (Guldaf) (14) - CPU address $a698
.addr mini_ufo_routine_ptr_tbl-2 ; Flying Saucer (15) - CPU address $a8ea
.addr boss_ufo_bomb_routine_ptr_tbl-2 ; Drop Bomb (16) - CPU address $a96a
; pointer table for level 6 enemy routines (#$5 * #$2 = #$a bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_6:
.addr fire_beam_down_routine_ptr_tbl-2 ; Energy Beam - Down (10) - CPU address $a997
.addr fire_beam_left_routine_ptr_tbl-2 ; Energy Beam - Left (11) - CPU address $aa4b
.addr fire_beam_right_routine_ptr_tbl-2 ; Energy Beam - Right (12) - CPU address $aa9a
.addr boss_giant_soldier_routine_ptr_tbl-2 ; Giant Boss Robot (13) - CPU address $ab7d
.addr boss_giant_projectile_routine_ptr_tbl-2 ; Spiked Disk Projectile (14) - CPU address $ae40
; pointer table for level 7 enemy routines (#$9 * #$2 = #$12 bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_7:
.addr claw_routine_ptr_tbl-2 ; Mechanical Claw (10) - CPU address $aeb9
.addr rising_spiked_wall_routine_ptr_tbl-2 ; Raising Spiked Wall (11) - CPU address $afc8
.addr spiked_wall_routine_ptr_tbl-2 ; Spiked Wall (12) - CPU address $b0f9
.addr mine_cart_generator_routine_ptr_tbl-2 ; Cart Generator (13) - CPU address $b11c
.addr moving_cart_routine_ptr_tbl-2 ; Cart - Moving (14) - CPU address $b178
.addr immobile_cart_generator_routine_ptr_tbl-2 ; Cart - Immobile (15) - CPU address $b1d7
.addr boss_door_routine_ptr_tbl-2 ; Armored Door with Siren (16) - CPU address $b201
.addr boss_mortar_routine_ptr_tbl-2 ; Mortar Launcher (17) - CPU address $b272
.addr boss_soldier_generator_routine_ptr_tbl-2 ; Boss Screen Soldier Generator (18) - CPU address $b32c
; pointer table for level 8 enemy routines (#$6 * #$2 = #$c bytes)
; every entry is 2 bytes less than the actual label
enemy_routine_level_8:
.addr alien_guardian_routine_ptr_tbl-2 ; Alien Guardian (10) - CPU address $b422
.addr alien_fetus_routine_ptr_tbl-2 ; Alien Fetus (11) - CPU address $b6e0
.addr alien_mouth_routine_ptr_tbl-2 ; Alien Mouth (12) - CPU address $b7f4
.addr white_blob_routine_ptr_tbl-2 ; White Sentient Blob (13) - CPU address $b866
.addr alien_spider_routine_ptr_tbl-2 ; Alien Spider (14) - CPU address $ba29
.addr alien_spider_spawn_routine_ptr_tbl-2 ; Spider Spawn (15) - CPU address $bbb5
.addr boss_heart_routine_ptr_tbl-2 ; Heart (16) - CPU address $bc80
; enemy #$09 and #$0d, unused
enemy_routine_do_nothing_ptr_tbl:
.addr enemy_routine_do_nothing_00 ; Do Nothing - CPU address $e736
enemy_routine_do_nothing_00:
rts
wall_core_routine_05:
jsr enemy_routine_init_explosion ; initialize explosion and play sound if specified in ENEMY_STATE_WIDTH
lda #$00 ; a = #$00
sta ENEMY_FRAME,x ; set enemy animation frame number
rts
; door plate defeated
; various enemies use this routine, advanced to by enemy_destroyed_routine_ptr_tbl
; plays boss destroyed sound, destroys all enemies, destroys boss and advances enemy routine
boss_defeated_routine:
jsr init_APU_channels
lda #$57 ; a = #$57 (sound_57) - boss destroyed
jsr level_boss_defeated ; play sound and initiate auto-move
jsr destroy_all_enemies ; boss defeated, destroy all enemies
; initialize explosion and play sound if specified in ENEMY_STATE_WIDTH
; hide enemy and advance enemy routine
enemy_routine_init_explosion:
lda ENEMY_STATE_WIDTH,x
ora #$81 ; set boss destroyed bits x... ...x
bne explosion_sound_hide_enemy ; always branch
; also used by ice grenades (11)
; split mortar collide with ground routine
; play explosion sound, update collision, hide sprite
mortar_shot_routine_03:
lda #$0d ; a = #$0d (score code 0, collision code d)
sta ENEMY_SCORE_COLLISION,x ; set collision code for enemy
lda ENEMY_STATE_WIDTH,x ; load enemy state width
and #$be ; strip bits 0 and 6 (player-enemy collision)
ora #$80 ; set bit 7 (allow bullets to travel through enemy)
explosion_sound_hide_enemy:
sta ENEMY_STATE_WIDTH,x ; set updated ENEMY_STATE_WIDTH to specify explosion triggered
and #$02 ; keep bit 1 .... ..x.
beq @skip_explosion_sound ; skip explosion sound if bit 1 is set
lda #$19 ; a = #$19 (sound_19)
jsr play_sound ; play enemy destroyed sound
@skip_explosion_sound:
lda ENEMY_SPRITE_ATTR,x ; load enemy sprite attributes
and #$fc ; strip sprite palette
ora #$06 ; override sprite code palette with palette 2
sta ENEMY_SPRITE_ATTR,x ; update sprite attribute with new palette
lda ENEMY_SPRITES,x ; read enemy sprite code from CPU buffer
bne @continue ; if enemy sprite present, don't remove enemy
jmp remove_enemy ; remove enemy
; hide enemy (show invisible sprite) before advancing to next routine
@continue:
lda #$ff ; a = #$ff
sta ENEMY_FRAME,x ; set enemy animation frame number to #$ff
lda #$01 ; a = #$01
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer with invisible sprite (hide enemy)
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
lda #$01 ; a = #$01
; set ENEMY_ANIMATION_DELAY counter and advance to next routine
set_enemy_delay_adv_routine:
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
; advance to next routine
; input
; * x - enemy slot index of the enemy routine to advance
advance_enemy_routine:
lda ENEMY_ROUTINE,x ; enemy routine index
beq set_sprite_0 ; if routine not set, exit
inc ENEMY_ROUTINE,x ; increment enemy routine index
rts
; dead code, never called !(UNUSED)
bank_7_unused_label_01:
lda ENEMY_ROUTINE,x ; enemy routine index
beq set_sprite_0
inc ENEMY_ROUTINE,x ; enemy routine index
lda #$24 ; a = #$24 (sound of explosion)
jmp play_sound ; play sound
roller_routine_04:
lda #$03 ; explosion_type_03
ldy #$02 ; show #$02 of the sprites of the explosion_type_03 sequence
bne show_explosion_a
; generated indoor soldiers: indoor soldier, jumping soldier, grenade launcher, four soldiers
shared_enemy_routine_03:
lda #$02 ; explosion_type_02
ldy #$03 ; show #$02 of the sprites of the explosion_type_02 sequence
bne show_explosion_a
enemy_routine_explosion:
lda ENEMY_STATE_WIDTH,x ; load enemy state and width
ldy #$03 ; y = #$03
and #$08 ; kit bit 3
beq @continue ; branch if bit 3 wasn't set
iny ; increment y to #$04
@continue:
lda #$00 ; a = #$00
; enemy explosion
; input
; * a - explosion type, if not specified #$00, grab explosion type from ENEMY_STATE_WIDTH bit 3
; * y - the number of animations in the explosion to animate, e.g. number of sprites to draw in sequence
show_explosion_a:
sty $08 ; y is either #$03 or #$04
sta $09 ; a is #$00 for the first time through
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
lda ENEMY_ROUTINE,x ; load current enemy routine index
beq enemy_routine_explosion_exit ; exit if still on first enemy routine is #$00
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne enemy_routine_explosion_exit ; timer hasn't elapsed, wait another frame
inc ENEMY_FRAME,x ; increment explosion animation sprite
ldy ENEMY_FRAME,x ; load explosion animation sprite
cpy $08 ; compare ENEMY_FRAME,x to $08 (max number of sprites)
bcs advance_enemy_routine ; advance to next enemy-specific routine if shown all sprites
iny ; haven't shown all sprites, increment to next sprite animation
cpy $08 ; re-compare ENEMY_FRAME,x to $08 (max number of sprites)
bcc @continue ; branch if not on last sprite
jsr disable_enemy_collision ; showing last sprite, prevent player enemy collision
@continue:
lda #$0a ; a = #$0a (delay between explosion frames)
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
ldy $09 ; load explosion type, if not specified #$00, grab explosion type from ENEMY_STATE_WIDTH bit 3
bne @continue2
lda ENEMY_STATE_WIDTH,x ; load ENEMY_STATE_WIDTH to determine explosion type (bit 3)
and #$08 ; keep bit 3 (explosion type)
beq @continue2 ; set ENEMY_FRAME if explosion type is #$00
iny ; if explosion type is #$01 increment y to match
@continue2:
tya
asl
tay
lda explosion_type_ptr_tbl,y
sta $0a
lda explosion_type_ptr_tbl+1,y
sta $0b
ldy ENEMY_FRAME,x ; enemy animation frame number
lda ($0a),y
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
enemy_routine_explosion_exit:
rts
enemy_routine_remove_enemy:
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
; remove enemy
; CPU memory address $e809
remove_enemy:
lda #$00 ; a = #$00
sta ENEMY_ROUTINE,x ; enemy routine index
set_sprite_0:
lda #$00 ; a = #$00
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
rts
; set tile sprite code to #$00 and advance routine
; level 1 boss wall plated door
; level 2 boss eye
; level 3 boss mouth
; level 4 boss gemini
shared_enemy_routine_clear_sprite:
jsr set_sprite_0 ; set tile sprite code to 0
jmp advance_enemy_routine ; advance to next routine
; set enemy routine index to a, unless index is #$0
; remember enemy routines are off by one, so setting ENEMY_ROUTINE to #$03, results in the 2nd routine being run
; ex: for exploding bridge, setting ENEMY_ROUTINE to #$02 causes exploding_bridge_routine_01 to run the next frame
set_enemy_routine_to_a:
ldy ENEMY_ROUTINE,x ; enemy routine index
beq set_sprite_0
sta ENEMY_ROUTINE,x ; enemy routine index
rts
; pointer table for explosion type sprites (#$4 * #$2 = #$8 bytes)
explosion_type_ptr_tbl:
.addr explosion_type_00 ; CPU address $e82b
.addr explosion_type_01 ; CPU address $e82e
.addr explosion_type_02 ; CPU address $e832
.addr explosion_type_03 ; CPU address $e835
; tables for explosion sprite codes (#$4 * #$3 = #$c bytes)
; larger circular ring explosion
; sprite_38, sprite_39, sprite_3a
explosion_type_00:
.byte $38,$39,$3a
; cloudy explosion
; sprite_37, sprite_35, sprite_36, sprite_37
explosion_type_01:
.byte $37,$35,$36,$37
; small ring explosion -
; used for generated indoor soldiers: indoor soldier, jumping soldier, grenade launcher, four soldiers
; sprite_9d, sprite_9e, sprite_9f
explosion_type_02:
.byte $9d,$9e,$9f
; short cloudy explosion (used for rollers)
; sprite_36, sprite_37
explosion_type_03:
.byte $36,$37
; apply velocities and scrolling adjust
; update enemy position
; remove if off screen
update_enemy_pos:
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
beq update_enemy_x_pos_rem_if_off_screen ; branch for non-vertical levels
jsr update_enemy_y_pos_with_scroll ; vertical level; apply y velocity and update y position
cmp #$e8 ; compare enemy Y position to #$e8
bcs jmp_remove_enemy ; remove enemy if Y position is >= #$e8 (fallen off bottom of screen)
; apply X velocity to enemy X velocity
; remove enemy if enemy's resulting X position is less than 8 (off screen to left)
update_enemy_x_pos_rem_off_screen:
jsr update_enemy_x_pos ; apply velocity to X position
cmp #$08
bcc jmp_remove_enemy ; remove enemy if resulting X position is less than #$08
apply_vel_exit:
rts
; horizontal level, or indoor/base level
update_enemy_x_pos_rem_if_off_screen:
jsr update_enemy_x_pos_with_scroll
cmp #$08 ; compare enemy X position to #$08
bcc jmp_remove_enemy ; if X position < #$08, remove enemy (fallen off from left of screen)
; apply Y velocity to enemy Y position
; remove enemy if enemy's resulting Y position is greater than or equal to #$e8 (off screen to bottom)
set_enemy_y_vel_rem_off_screen:
jsr update_enemy_y_pos ; apply velocity to Y position
cmp #$e8
bcc apply_vel_exit ; remove enemy if resulting X position is greater than or equal to #$e8 (off screen to bottom)
jmp_remove_enemy:
jmp remove_enemy ; remove enemy
; sets the weapon item velocity for outdoor levels
set_outdoor_weapon_item_vel:
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
beq @set_weapon_item_velocity ; branch if horizontal or indoor/base level
ldy #$00 ; vertical level
lda ENEMY_Y_VELOCITY_FAST,x ; load Y velocity fast byte
clc ; clear carry in preparation for addition
adc FRAME_SCROLL ; add FRAME_SCROLL to ENEMY_Y_VELOCITY_FAST
jsr set_weapon_item_y_vel_enemy_frame ; apply y velocity to y position,
beq jmp_remove_enemy ; remove weapon item if ENEMY_FRAME is #$01
jmp update_enemy_x_pos_rem_off_screen ; add velocity to enemy X pos; remove enemy if X position < #$08 (off screen to left)
; horizontal levels
@set_weapon_item_velocity:
jsr update_enemy_x_pos_with_scroll ; update x position accounting for whether frame is scrolling
cmp #$08 ; compare the x position to the left side of the screen
bcc remove_enemy_far ; remove weapon item if too far to the left (scrolled off screen)
ldy #$00 ; y = #$00
lda ENEMY_Y_VELOCITY_FAST,x ; load fast velocity so it is applied in next line
jsr set_weapon_item_y_vel_enemy_frame ; apply y velocity to y position, don't adjust ENEMY_FRAME
bne scroll_enemy_pos_exit ; branch if weapon item, isn't off screen to bottom, otherwise remove
remove_enemy_far:
jmp remove_enemy ; remove enemy
; adds y to ENEMY_FRAME and adjusts Y position for weapon item by a plus
; ENEMY_FRAME: #$ff is explosion, #$00 is weapon item
; input
; * x - current enemy offset
; * a - how much to move current enemy's Y position
; * y - number of frames to advance ENEMY_FRAME (set to -1 when picked up by player)
; output
; * compare ENEMY_FRAME to #$01, determines if y position overflow (off screen)
set_weapon_item_y_vel_enemy_frame:
bpl @set_y_vel_enemy_frame
dey ; subtract 1 from ENEMY_FRAME advance since enemy is moving in positive Y (accommodate overflow)
@set_y_vel_enemy_frame:
sty $01
sta $00
lda ENEMY_Y_VEL_ACCUM,x ; load current accumulated ENEMY_Y_VELOCITY_FRACT total
clc ; clear carry in preparation for addition
adc ENEMY_Y_VELOCITY_FRACT,x ; a = ENEMY_Y_VELOCITY_FRACT + ENEMY_Y_VEL_ACCUM
sta ENEMY_Y_VEL_ACCUM,x ; add another ENEMY_X_VELOCITY_FRACT to accumulator
lda ENEMY_Y_POS,x ; load enemy y position on screen
adc $00 ; add $00 units to Y position
; along with an additional 1 unit if ENEMY_Y_VEL_ACCUM rolled over
sta ENEMY_Y_POS,x ; set new Y position
lda ENEMY_FRAME,x ; load enemy animation frame number
adc $01 ; add ENEMY_FRAME to $01, carry could be set from adc $00 above
sta ENEMY_FRAME,x ; set new value
cmp #$01 ; if any carry from previous addition, then y position is > #$ff, so remove weapon item, it's off screen
rts
; if the screen is scrolling add that amount to the enemy position
add_scroll_to_enemy_pos:
lda LEVEL_SCROLLING_TYPE ; 0 = horizontal, indoor/base; 1 = vertical
beq add_horizontal_scroll
lda ENEMY_Y_POS,x ; vertical level, load enemy y position on screen
clc ; clear carry in preparation for addition
adc FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
sta ENEMY_Y_POS,x ; enemy y position on screen
cmp #$e8
bcs remove_enemy_far ; remove enemy if far above screen for vertical level
scroll_enemy_pos_exit:
rts
; add X scrolling to enemy X position
; horizontal scrolling level
add_horizontal_scroll:
lda ENEMY_X_POS,x ; load enemy x position on screen
sec ; set carry flag in preparation for subtraction
sbc FRAME_SCROLL ; subtract the frame scroll amount
sta ENEMY_X_POS,x ; set new enemy x position
cmp #$08 ; remove enemy if too far to the left
bcc remove_enemy_far ; remove enemy if leaving the left of the screen
rts
; dead code, never called !(UNUSED)
bank_7_unused_label_02:
jsr update_enemy_x_pos_rem_off_screen ; add velocity to enemy X position; remove enemy if X position < #$08 (off screen to left)
jmp set_enemy_y_vel_rem_off_screen ; add velocity to enemy Y position; remove enemy if Y position >= #$e8 (off screen to bottom)
; set x/y velocities to zero
set_enemy_velocity_to_0:
jsr set_enemy_x_velocity_to_0
; set y velocity to zero
set_enemy_y_velocity_to_0:
lda #$00 ; a = #$00
sta ENEMY_Y_VELOCITY_FRACT,x
sta ENEMY_Y_VELOCITY_FAST,x
rts
; set x velocity to zero
set_enemy_x_velocity_to_0:
lda #$00 ; a = #$00
sta ENEMY_X_VELOCITY_FRACT,x
sta ENEMY_X_VELOCITY_FAST,x
rts
update_enemy_y_pos_with_scroll:
jsr update_enemy_y_pos ; apply velocity to y position
clc ; clear carry in preparation for addition
adc FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
sta ENEMY_Y_POS,x ; enemy y position on screen
rts
; apply Y velocity and update enemy's Y position
; output
; * a - ENEMY_Y_POS
update_enemy_y_pos:
lda ENEMY_Y_VEL_ACCUM,x ; load current accumulated ENEMY_Y_VELOCITY_FRACT total
clc ; clear carry in preparation for addition
adc ENEMY_Y_VELOCITY_FRACT,x ; a = ENEMY_Y_VELOCITY_FRACT + ENEMY_Y_VEL_ACCUM
sta ENEMY_Y_VEL_ACCUM,x ; add another ENEMY_X_VELOCITY_FRACT to accumulator
lda ENEMY_Y_POS,x
adc ENEMY_Y_VELOCITY_FAST,x ; add ENEMY_Y_VELOCITY_FAST units to Y position
; along with an additional 1 unit if ENEMY_Y_VEL_ACCUM rolled over
sta ENEMY_Y_POS,x ; set new Y position
rts
; updates enemy position based on velocity and adjusts when frame is scrolling
; input
; * x - enemy to adjust x position
; output
; * a - updated enemy x position
update_enemy_x_pos_with_scroll:
jsr update_enemy_x_pos ; apply velocity to x position
sec ; set carry flag in preparation for subtraction
sbc FRAME_SCROLL ; how much to scroll the screen (#00 - no scroll)
sta ENEMY_X_POS,x ; set enemy x position on screen
rts
; apply X velocity and update enemy's X position
; output
; * a - ENEMY_X_POS
update_enemy_x_pos:
lda ENEMY_X_VEL_ACCUM,x ; load current accumulated ENEMY_X_VELOCITY_FRACT total
clc ; clear carry in preparation for addition
adc ENEMY_X_VELOCITY_FRACT,x ; a = ENEMY_X_VELOCITY_FRACT + ENEMY_X_VEL_ACCUM
sta ENEMY_X_VEL_ACCUM,x ; add another ENEMY_X_VELOCITY_FRACT to accumulator
lda ENEMY_X_POS,x ; load current enemy x position
adc ENEMY_X_VELOCITY_FAST,x ; add ENEMY_X_VELOCITY_FAST units to X position
; along with an additional 1 unit if ENEMY_X_VEL_ACCUM rolled over
sta ENEMY_X_POS,x ; set new X position
rts
; reverse x direction
reverse_enemy_x_direction:
lda #$00 ; a = #$00
sec ; set carry flag in preparation for subtraction
sbc ENEMY_X_VELOCITY_FRACT,x ; #$00 - ENEMY_X_VELOCITY_FRACT,x
sta ENEMY_X_VELOCITY_FRACT,x
lda #$00 ; a = #$00
sbc ENEMY_X_VELOCITY_FAST,x ; #$00 - ENEMY_X_VELOCITY_FAST,x
sta ENEMY_X_VELOCITY_FAST,x
rts
; reverse y direction
; dead code, never called !(UNUSED)
bank_7_unused_label_03:
lda #$00 ; a = #$00
sec ; set carry flag in preparation for subtraction
sbc ENEMY_Y_VELOCITY_FRACT,x ; #$00 - ENEMY_Y_VELOCITY_FRACT,x
sta ENEMY_Y_VELOCITY_FRACT,x
lda #$00 ; a = #$00
sbc ENEMY_Y_VELOCITY_FAST,x ; #$00 - ENEMY_Y_VELOCITY_FAST,x
sta ENEMY_Y_VELOCITY_FAST,x
rts
; get score of current enemy according to score code
; adds score amount to player score
; sets enemy destroyed routine
add_enemy_score_set_enemy_routine:
lda ENEMY_SCORE_COLLISION,x ; pull score bits from byte
lsr
lsr
lsr
lsr
tay
cpy #$0a ; score type a (500,000 points), not in score_codes_tbl since 2 bytes
bne @add_y_code_to_player_score
lda #$88 ; custom score code #$0a - set a = #$88
sta $00 ; store in score to add low byte
lda #$13 ; a = #$13 (#$1388 = 5000 decimal = 500,000 points)
sta $01 ; store in score to add high byte
ldy $17 ; load current player number (0 or 1)
jsr add_player_score ; add $00 and $01 to player score, get extra life, check if high score
jmp @continue
@add_y_code_to_player_score:
lda score_codes_tbl,y
beq @continue
sta $00
ldy $17 ; load current player number (0 or 1)
jsr add_player_low_score ; add enemy points ($00) to player score in memory, see if extra life and high score
@continue:
ldx ENEMY_CURRENT_SLOT
lda ENEMY_SCORE_COLLISION,x ; load to remove score code
and #$0f ; keep bits .... xxxx (collision code)
sta ENEMY_SCORE_COLLISION,x ; set score component to #$0
; set enemy routine to their appropriate destroyed routine
set_destroyed_enemy_routine:
lda ENEMY_TYPE,x ; load current enemy type
cmp #$10 ; see if common enemy type
ldy #$10 ; y = #$10 (common enemies)
bcc @set_destroyed_routine ; if current enemy type is < #$10, use common enemy enemy logic
lda CURRENT_LEVEL ; level-specific enemy type, load current level
asl ; double since each entry in enemy_destroyed_routine_ptr_tbl is #$02 bytes
tay ; transfer offset to y
@set_destroyed_routine:
lda enemy_destroyed_routine_ptr_tbl,y ; load low byte of the routine pointer
sta $08 ; store in $08
lda enemy_destroyed_routine_ptr_tbl+1,y ; load high byte of the routine pointer
sta $09 ; store in $09
lda ENEMY_TYPE,x ; load current enemy type
lsr ; push enemy lsb to the carry flag (odd or even)
; and half the value since each byte is #$02 enemy types
tay ; transfer enemy type to y
lda ($08),y ; load the byte specified by the table (enemy_destroyed_routine_XX)
bcs @set_routine ; if enemy type loaded is odd, bits 0-3 is the routine number to set
lsr ; enemy type loaded was even, look at high 4 nibble
lsr
lsr
lsr
@set_routine:
and #$0f ; keep bits .... xxxx
cmp ENEMY_ROUTINE,x ; compare against current enemy routine being executed for the enemy
bcc @exit ; enemy destroyed routine is less than current enemy routine index, exit
sta ENEMY_ROUTINE,x ; update enemy routine to new destroyed index
@exit:
rts
; table for score codes (#$a bytes)
; type #$0a is hard-coded and gives 500,000 points
score_codes_tbl:
.byte $00 ; type 0: 0 points
.byte $01 ; type 1: 100 points
.byte $03 ; type 2: 300 points
.byte $05 ; type 3: 500 points
.byte $0a ; type 4: 1,000 points
.byte $14 ; type 5: 2,000 points
.byte $1e ; type 6: 3,000 points
.byte $32 ; type 7: 5,000 points
.byte $64 ; type 8: 10,000 points
.byte $96 ; type 9: 15,000 points
; pointer table for which enemy routine to execute when destroyed (#$9 * #$2 = #$12 bytes)
enemy_destroyed_routine_ptr_tbl:
.addr enemy_destroyed_routine_00 ; CPU address $e9bf - Level 1
.addr enemy_destroyed_routine_01 ; CPU address $e9c1 - Level 2
.addr enemy_destroyed_routine_02 ; CPU address $e9ca - Level 3
.addr enemy_destroyed_routine_01 ; CPU address $e9c1 - Level 4
.addr enemy_destroyed_routine_03 ; CPU address $e9cd - Level 5
.addr enemy_destroyed_routine_04 ; CPU address $e9d1 - Level 6
.addr enemy_destroyed_routine_05 ; CPU address $e9d4 - Level 7
.addr enemy_destroyed_routine_06 ; CPU address $e9d9 - Level 8
.addr enemy_destroyed_routine_00 ; CPU address $e9bf - common enemies (enemy type < #$10)
; table for enemy routine index when destroyed
; also used for falcon item or when boss is destroyed to destroy all enemies
; #$4 bits per enemy, 2 enemies per byte
; if enemy type is odd, then smaller nibble is used
; if enemy type byte is even, then high nibble is used
; keep in mind that all routines are offset by -2
; * e.g. #$03 for the boss bomb turret would be routine boss_bomb_turret_routine_02
enemy_destroyed_routine_00:
.byte $04 ; weapon item (00) / enemy bullet (01)
.byte $53 ; pill box sensor (02) / weapon zeppelin (03)
enemy_destroyed_routine_01:
.byte $75 ; rotating gun (04) / running man (05)
.byte $56 ; rifle man (06) / red turret (07)
.byte $50 ; wall cannon (08) / unused
.byte $44 ; wall plating (0a) / mortar shot (0b)
.byte $44 ; scuba diver (0c) / unused
.byte $43 ; turret man (0e) / turret man bullet
.byte $33 ; boss bomb turret (10) / door plate with siren (11)
.byte $20 ; exploding bridge (12)
.byte $43 ; boss eye (10) / rollers (11)
enemy_destroyed_routine_02:
.byte $45 ; grenades (12) / wall turret (13)
.byte $53 ; core (14) / indoor soldier (15)
.byte $33 ; jumping guy (16) / seeking guy (17)
enemy_destroyed_routine_03:
.byte $43 ; group of 4 (18) / indoor soldier generator (19)
.byte $33 ; rollers generator (1a) / sphere projectile (1b)
.byte $43 ; boss gemini (1c) / spinning bubbles projectile (1d)
.byte $54 ; blue jumping guy (1e) / red shooting guy (1f)
enemy_destroyed_routine_04:
.byte $30 ; red/blue guys generator (20)
.byte $22 ; rock platform (10) / moving flame (11)
.byte $24 ; falling rock generator (rock cave) (12) / falling rock (13)
enemy_destroyed_routine_05:
.byte $65 ; level 3 boss mouth (14) / level 3 dragon arm orb (15)
.byte $33 ; ice grenade generator (10) / ice grenade (11)
.byte $50 ; tank (12) / pipe joint (13)
.byte $a5 ; boss ufo (14) / flying saucer (15)
.byte $20 ; bomb drop (16)
enemy_destroyed_routine_06:
.byte $00 ; fire beam down (10) / fire beam left (11)
.byte $07 ; fire beam right (12) / giant boss robot (13)
.byte $30 ; spiked disk projectile (14)
.byte $05 ; mechanical claw (10) / raising spiked wall (11)
.byte $30 ; spiked wall (12) / cart generator (13)
.byte $44 ; cart moving (14) / cart immobile (15)
.byte $35 ; armored door (16) / mortar launcher (17)
.byte $50 ; enemy generator (18)
.byte $43 ; alien guardian (10) / alien fetus (11)
.byte $34 ; alien mouth (12) / white sentient blob (13)
.byte $63 ; alien spider (14) / spider spawn (15)
.byte $40 ; heart (16)
; falcon weapon - Destroy All Enemies (with exceptions like for pill box sensor, weapon zeppelin)
; also used when boss is defeated to remove all enemies
destroy_all_enemies:
stx $10 ; store value of x register in $10 temporarily
ldx #$0f ; x = #$0f
@enemy_loop:
lda ENEMY_ROUTINE,x ; load the current enemy routine pointer
beq @continue ; skip to next enemy when no routine set for enemy
lda ENEMY_SPRITES,x ; load enemy tile sprite code
beq @continue ; skip to next enemy when no sprite set for enemy
lda ENEMY_TYPE,x ; load current enemy type
cmp #$02 ; see if pill box sensor
beq @continue ; skip to next enemy when enemy is pill box sensor
cmp #$03 ; see if flying capsule (weapon zeppelin)
beq @continue ; skip to next enemy when enemy is flying capsule
lda ENEMY_HP,x ; load enemy hp
.ifdef Probotector
beq @continue ; !(WHY?) exit if enemy HP is already #$00, not sure of game play changes
; to have such a change between versions
.endif
cmp #$f0 ; f0 = no hit
beq @continue ; skip to next enemy when enemy hp is #$f0
jsr set_destroyed_enemy_routine ; regular enemy, set it to use its destroy routine
lda ENEMY_ATTRIBUTES,x ; load enemy attributes
ora #$80 ; set bits bit 7 to flag enemy as destroyed
sta ENEMY_ATTRIBUTES,x
@continue:
dex ; go to next enemy (enemy logic starts high and goes to #$00)
bpl @enemy_loop
ldx $10 ; restore x attribute from before destroy_all_enemies call
rts
remove_all_enemies:
stx $10 ; store value of x register in $10 temporarily
ldx #$0f ; x = #$0f (total number of possible enemies)
@loop:
jsr remove_enemy ; remove enemy
dex
bpl @loop
ldx $10 ; restore x attribute from before remove_all_enemies call
rts
; sets background collision code to #$00 (empty) for a single super-tile, or #$10 pattern table tiles (4 2x2 tiles)
; at PPU address $12 (low) $13 (high)
; input
; * PPU nametable collision address: $12 (low) and $13 (high)
clear_supertile_bg_collision:
lda #$00 ; a = #$00
; updates background collision code for a single super-tile, or #$10 pattern table tiles (4 2x2 tiles)
; at PPU address $12 (low) $13 (high)
; input
; * a - the bg collision code for the entire super-tile (4 2x2 tiles) [#$00-#$0f]
; * PPU nametable collision address: $12 (low) and $13 (high)
set_supertile_bg_collision:
tay
; updates background collision codes for a single super-tile, or #$10 pattern table tiles (4 2x2 tiles)
; at PPU address $12 (low) $13 (high)
; input
; * a - left two collision tiles [#$00-#$0f]
; * bits 0 and 1 - the top-left collision code (1 2x2 nametable tile)
; * bits 2 and 3 - the bottom-left collision code (1 2x2 nametable tile)
; * a - right two collision tiles [#$00-#$0f]
; * bits 0 and 1 - the top-right collision code (1 2x2 nametable tile)
; * bits 2 and 3 - the bottom-right collision code (1 2x2 nametable tile)
; * PPU nametable collision address: $12 (low) and $13 (high)
set_supertile_bg_collisions:
sta $11 ; save first 2x2 bg collision code to $11
sty $14 ; save second 2x2 bg collision code to $11
; start calculation BG_COLLISION_DATA offset from PPU address, e.g. $2190 goes to #$1a
lda $12 ; load low byte of PPU nametable address
lsr ; shift value to the right
and #$03 ; get bits 1 and 2 of $12 before shifting
sta $00 ; store bitmask offset in $00 [#$00-#$03]
lda $13 ; load high byte of PPU nametable address
and #$07 ; strip leading #$02 from nametable address high byte
; not used when calculating BG_COLLISION_DATA offset
asl $12
rol
asl $12
rol
asl $12 ; ignore bit 5 of address low byte
; this ensures each every 2nd nametable row has same bg collision tile offset as nametable row above
asl $12
rol
asl $12
rol
sta $04 ; set the in memory bg collision byte offset (BG_COLLISION_DATA)
lda #$02 ; set number of times to call @set_half_supertile_bg_collisions to #$02
; each call @set_half_supertile_bg_collisions will update 2 bg collision tiles (2 2x2 nametable tiles)
sta $01 ; store @set_half_supertile_bg_collisions call counter
@loop:
lda $00 ; load calculated bitmask index
sta $02 ; set bitmask index (0 = 0011 1111, 1 = 1100 1111, 2 = 1111 0011, 3 = 1111 1100)
jsr @set_half_supertile_bg_collisions ; set the bg collision for the #$08 tiles on one (horizontal) half of the super-tile
lsr $11
lsr $11 ; shift the bottom-left collision code into the lower bits for use
lsr $14
lsr $14 ; shift the bottom right collision code into the lower bits for use
lda $04 ; load in memory bg collision byte offset (BG_COLLISION_DATA)
clc ; clear carry in preparation for addition
adc #$04 ; add #$04 to the current calculated bg collision byte offset (BG_COLLISION_DATA)
sta $04 ; move one bg collision row down to next BG_COLLISION_DATA collision byte offset
dec $01 ; decrement @set_half_supertile_bg_collisions counter
bne @loop
rts
; sets #$08 nametable tile collision code (#$02 bg collision tiles)
; for either the top or bottom horizontal half of the super-tile
@set_half_supertile_bg_collisions:
lda $04 ; load in memory bg collision byte offset (BG_COLLISION_DATA)
sta $07 ; set in memory bg collision byte offset (BG_COLLISION_DATA)
lda #$01 ; a = #$01
sta $06 ; set to update #$02 bg collision tiles (2 2x2 nametable tiles)
; input
; * $02 - bitmask index (0 = 0011 1111, 1 = 1100 1111, 2 = 1111 0011, 3 = 1111 1100)
@set_quadrant_bg_collision:
ldy $02 ; load bitmask index (0 = 0011 1111, 1 = 1100 1111, 2 = 1111 0011, 3 = 1111 1100)
lda bg_collision_bit_mask_tbl,y ; load bitmask value, these are the bits to remain unchanged
sta $05 ; store background collision mask in $05
lda $06 ; load one less than the number of bg collision tiles to update, #$01 or #$00
lsr ; shift bit
lda $11 ; updating
bcs @continue
lda $14
@continue:
and #$03 ; keep bits .... ..xx
; determine location of the #$02 collision bits within the collision byte
; by counting up to #$04 from the bitmask index shifting a each time twice
@find_bit_offset:
iny ; increment bitmask index
cpy #$04 ; see if last index
bcs @set_collision ; branch if last index to continue
asl
asl ; shift #$02 collision bits to the next portion of the bg collision byte
jmp @find_bit_offset ; jump to see if new bit position is correct
@set_collision:
sta $03 ; store the background section's collision code, all other bits are 0
ldy $07 ; load in memory bg collision byte offset (BG_COLLISION_DATA)
lda BG_COLLISION_DATA,y ; load the current byte (a byte specifies bg collision for #$08 pattern table tiles)
and $05 ; keep all other background collision values except the #$02 bits to update
ora $03 ; merge in the new background collision value
sta BG_COLLISION_DATA,y ; update background collision for the #$02 pattern table tiles
inc $02 ; increment bitmask index
lda $02 ; load new bitmask index (0 = 0011 1111, 1 = 1100 1111, 2 = 1111 0011, 3 = 1111 1100)
and #$03 ; keep bits .... ..xx
sta $02 ; set new value
bne @check_next_loop ; branch if still have a bg collision code to update
inc $07 ; increment bg collision byte offset (BG_COLLISION_DATA)
@check_next_loop:
dec $06 ; decrement number of bg collision tiles to update
bpl @set_quadrant_bg_collision ; branch if more bg collision tiles to update the next quadrant of the super-tile
rts
; table for bit masks (#$4 bytes)
; each 2 bits encode 2 pattern table tiles (1/4 of a super-tile's collision information)
; 0011 1111
; 1100 1111
; 1111 0011
; 1111 1100
bg_collision_bit_mask_tbl:
.byte $3f,$cf,$f3,$fc
; create explosion #$89 at location ($09, $08)
create_explosion_89:
lda #$89 ; a = #$89
sta $0a ; set explosion type to #$89
lda #$09 ; set ENEMY_ROUTINE to #$09 (enemy_routine_init_explosion)
bne create_explosion_sequence ; always branch to create explosion animation, by using the weapon box's enemy routines
; creates 2 sets of explosion #$89 at location ($09, $08)
; input
; * $09 - x location
; * $08 - y location
create_two_explosion_89:
lda #$89 ; set ENEMY_STATE_WIDTH to #$89 (explosion type)
bne create_explosion_a ; always jump
; create new pill box sensor set to routine enemy_routine_init_explosion
; pill box sensor isn't important, it's just an enemy that has the enemy_routine_init_explosion routine sequence
; input
; * $09 - x position to create enemy at
; * $08 - y position to create enemy at
create_enemy_for_explosion:
lda #$08 ; set ENEMY_STATE_WIDTH to #$08 (explosion type)
; input
; * $09 - x position to create enemy at
; * $08 - y position to create enemy at
create_explosion_a:
sta $0a ; set explosion type
lda #$06 ; a = #$06 (enemy_routine_init_explosion, 2 rounds of explosions)
; input
; * $09 - x position to create enemy at
; * $08 - y position to create enemy at
create_explosion_sequence:
sta $0b ; set weapon box enemy routine
stx $10 ; save x to be restored after function call
jsr find_next_enemy_slot ; find next available enemy slot, put result in x register
bne @exit ; branch if no enemy slot was found
lda #$02 ; a = #$02, pill box sensor (weapon box)
; used for enemy_routine_init_explosion, enemy_routine_explosion, enemy_routine_remove_enemy sequence
sta ENEMY_TYPE,x ; set current enemy type to pill box sensor (weapon box)
jsr initialize_enemy ; initialize enemy attributes
lda $0b ; load enemy routine (#$06 for 2 explosions or #$09 for one explosion)
sta ENEMY_ROUTINE,x ; enemy routine index
lda #$01 ; a = #$01 (blank sprite)
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
lda $0a ; load explosion type
sta ENEMY_STATE_WIDTH,x ; set explosion type
lda $08 ; load y position of explosions
sta ENEMY_Y_POS,x ; set explosion y position on screen
lda $09 ; load x position of explosions
sta ENEMY_X_POS,x ; set explosion x position on screen
@exit:
ldx $10 ; restore x from before create_explosion_sequence call
rts
; level-boss defeated
; * play sound code a
; * set auto-move delay to #$ff
; * set BOSS_DEFEATED_FLAG level boss defeated flag
; input
; * a - play sound code
level_boss_defeated:
jsr play_sound ; play sound a
lda #$ff
sta DELAY_TIME_LOW_BYTE ; set auto-move delay to #$ff
lda #$01
sta BOSS_DEFEATED_FLAG ; set BOSS_DEFEATED_FLAG to true
rts
; set delay to a and remove enemy
set_delay_remove_enemy:
sta DELAY_TIME_LOW_BYTE ; various delays (low byte)
lda #$00 ; a = #$00
sta DELAY_TIME_HIGH_BYTE ; various delays (high byte)
jmp remove_enemy ; remove enemy
lda #$01 ; dead code !(UNUSED)
bne enemy_state_width_or_a ; dead code !(UNUSED)
; set bit 7 of ENEMY_STATE_WIDTH,x
; bit 7 set to allow bullets to travel through enemy, e.g. boss mouth
disable_bullet_enemy_collision:
lda #$80 ; a = #$80
bne enemy_state_width_or_a
; prevent player enemy collision check and allow bullets to pass through enemy
; set bits 0 and 7 of ENEMY_STATE_WIDTH,x
; bit 7 set to allow bullets to travel through enemy, e.g. weapon item
; bit 0 - #$00 test player-enemy collision, #$01 means to skip player-enemy collision test
disable_enemy_collision:
lda #$81 ; x... ...x (msb and lsb set)
enemy_state_width_or_a:
ora ENEMY_STATE_WIDTH,x ; set msb and lsb bits to 1
bne set_enemy_state_width_to_a
; enable enemy-player collision checking, e.g. fire beam collision with player
; bit 0 - #$00 test player-enemy collision, #$01 means to skip player-enemy collision test
enable_enemy_player_collision_check:
lda #$fe ; a = #$fe
bne enemy_state_width_and_a ; always branch
; allow bullets to collide with enemy, some enemies have bullets pass through, e.g. weapon item
enable_bullet_enemy_collision:
lda #$7f ; a = #$7f
bne enemy_state_width_and_a
; enable bullet-enemy collision and player-enemy collision checks
enable_enemy_collision:
lda #$7e ; a = #$7e
enemy_state_width_and_a:
and ENEMY_STATE_WIDTH,x
set_enemy_state_width_to_a:
sta ENEMY_STATE_WIDTH,x
rts
; add a to enemy y position on screen
add_a_to_enemy_y_pos:
clc ; clear carry in preparation for addition
adc ENEMY_Y_POS,x
sta ENEMY_Y_POS,x ; enemy y position on screen
rts
; add a to enemy x position on screen
add_a_to_enemy_x_pos:
clc ; clear carry in preparation for addition
adc ENEMY_X_POS,x ; add to enemy x position on screen
sta ENEMY_X_POS,x ; set enemy x position on screen
rts
; set memory $08 and $09 to enemy X's Y and X position respectively
; output
; * $08 - enemy y position
; * $09 - enemy x position
; * a - enemy y position
; * y - enemy y position
set_08_09_to_enemy_pos:
lda #$00 ; a = #$00
tay
; adds register a to the enemy x position, stores result in $09
; adds register y to the enemy y position, stores result in $08
; input
; * a - distance to add to x position
; * y - distance to add to y position
; output
; * $08 - y to the enemy y position
; * $09 - a to the enemy x position
add_with_enemy_pos:
clc ; clear carry in preparation for addition
adc ENEMY_X_POS,x ; add a to enemy x position on screen
sta $09 ; store result in $09
tya
clc ; clear carry in preparation for addition
adc ENEMY_Y_POS,x ; add a to enemy y position on screen
sta $08 ; store value in $08
rts
; add .06 to y velocity
add_10_to_enemy_y_fract_vel:
lda #$10 ; a = #$10
; add a to enemy y fractional velocity, incorporating carry into fast y velocity
add_a_to_enemy_y_fract_vel:
clc ; clear carry in preparation for addition
adc ENEMY_Y_VELOCITY_FRACT,x ; add a to enemy y fractional velocity
sta ENEMY_Y_VELOCITY_FRACT,x ; store updated result in enemy y fractional velocity
lda ENEMY_Y_VELOCITY_FAST,x ; load the y fast velocity
adc #$00 ; add any carry from adding to fractional velocity
sta ENEMY_Y_VELOCITY_FAST,x ; store updated fast velocity if any carry occurred
rts
; generate enemy at relative position 0,0 to current enemy
; input
; * a - enemy type
; output
; * a - #$01 when no enemy created, #$00 when enemy created
; * y - created enemy slot number
generate_enemy_a:
sta $0a
lda #$00 ; a = #$00
tay
; generate enemy type $0a at relative position a,y
; input
; * $0a - enemy type
; * a - x position
; * y - y position
; output
; * a - #$01 when no enemy created, #$00 when enemy created
; * y - created enemy slot number
generate_enemy_at_pos:
sty $08
sta $09
txa
tay
jsr find_next_enemy_slot ; find next available enemy slot, put result in x register
bne @exit ; no enemy slot, exit (find_next_enemy_slot sets zero flag when found, clears when not found)
lda $0a ; load enemy type
sta ENEMY_TYPE,x ; set enemy type
jsr initialize_enemy
lda ENEMY_Y_POS,y ; enemy y position on screen
clc ; clear carry in preparation for addition
adc $08
sta ENEMY_Y_POS,x
lda ENEMY_X_POS,y ; load enemy x position on screen
clc ; clear carry in preparation for addition
adc $09
sta ENEMY_X_POS,x ; set enemy x position on screen
txa
tay
ldx ENEMY_CURRENT_SLOT
lda #$00 ; a = #$00
rts
@exit:
ldx ENEMY_CURRENT_SLOT
lda #$01 ; a = #$01
rts
; add #$04 to the enemy y position accounting for VERTICAL_SCROLL overflow on vertical levels
add_4_to_enemy_y_pos:
lda #$04
; add a to the enemy y position accounting for VERTICAL_SCROLL overflow on vertical levels
add_a_with_vert_scroll_to_enemy_y_pos:
sta $01
lda VERTICAL_SCROLL ; vertical scroll offset
and #$0f ; keep bits .... xxxx
ora #$f0 ; set bits xxxx ....
sta $00
clc ; clear carry in preparation for addition
adc ENEMY_Y_POS,x ; add vertical scroll to enemy Y position
and #$f0 ; keep bits xxxx ....
sec ; set carry flag in preparation for subtraction
sbc $00
clc ; clear carry in preparation for addition
adc $01
sta ENEMY_Y_POS,x ; enemy y position on screen
rts
; draw the nametable tiles from level_xx_tile_animation (a) at the enemy position
; sets animation delay for enemy to #$01 if successful
; input
; * a - offset into level-specific tile_animation table, e.g. level_2_4_tile_animation
; does not update palette, i.e. leave existing palette
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
update_nametable_tiles_set_delay:
jsr update_enemy_nametable_tiles_no_palette ; draw the nametable tiles from level_2_4_tile_animation (a) at the enemy position
jmp update_nametable_set_anim_delay_exit
; draw super-tile a at position (ENEMY_X_POS, ENEMY_Y_POS)
; input
; * a - super-tile code (offset into level_xx_nametable_update_supertile_data)
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
draw_enemy_supertile_a_set_delay:
jsr draw_enemy_supertile_a ; draw super-tile a at position (ENEMY_X_POS, ENEMY_Y_POS)
update_nametable_set_anim_delay_exit:
bcc @exit ; exit if updated nametable tiles
lda #$01 ; a = #$01, animation delay
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
@exit:
rts
; draw super-tile a (offset into level nametable update table) at position (ENEMY_X_POS, ENEMY_Y_POS)
; input
; * a - nametable update super-tile code (offset into level_xx_nametable_update_supertile_data)
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
draw_enemy_supertile_a:
sta $10
; draw super-tile $10 (offset into level nametable update table) at position (ENEMY_X_POS, ENEMY_Y_POS)
; input
; * $10 - is the super-tile or palette index to draw (level_x_nametable_update_supertile_data/level_x_nametable_update_palette_data offset)
; If bit 7 clear, then update palette, if bit 7 set do not update palette
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
draw_enemy_supertile_10:
lda ENEMY_Y_POS,x ; load enemy y position on screen
sec ; set carry flag in preparation for subtraction
sbc #$0c ; subtract #$0c from enemy y position
bcc nametable_update_exit ; exit if negative (off screen to the top)
tay ; transfer y position to y
lda ENEMY_X_POS,x ; load enemy x position on screen
sec ; set carry flag in preparation for subtraction
sbc #$0c ; subtract #$0c from enemy x position
bcc nametable_update_exit ; exit if negative (off screen to the left)
jsr load_bank_3_update_nametable_supertile ; draw super-tile $10 at position (a,y)
ldx ENEMY_CURRENT_SLOT
rts
; draw two super-tiles, one on top of the other
; bit 7 of $10 and a control whether ot update palette:
; * if bit 7 clear, then update palette, if bit 7 set do not update palette
; input
; * $10 - first nametable update super-tile code (offset into level_xx_nametable_update_supertile_data)
; * a - second nametable update super-tile code (offset into level_xx_nametable_update_supertile_data)
; * y - whether or not to assign collision (0 - yes, 1 - skip)
; output
; * carry flag - clear when successful, set when CPU_GRAPHICS_BUFFER is full
update_2_enemy_supertiles:
sty $07 ; store whether or not to assign collision in $07
pha ; save a copy of a on the stack
jsr draw_enemy_supertile_10 ; draw nametable update super-tile index specified
pla ; restore a from before the draw_enemy_supertile_10 call
bcs @exit ; exit if CPU_GRAPHICS_BUFFER is full
sta $10
lda $07 ; load whether or not to set collision
bne @continue ; skip collision setting if $07 is set
lda #$00 ; left side of super-tile bg collision (#$00 = empty collision codes)
ldy #$0f ; right side of super-tile bg collision (#$0f = solid collision codes)
jsr set_supertile_bg_collisions ; update bg collision codes for a single super-tile at PPU address $12 (low) $13 (high)
@continue:
lda ENEMY_Y_POS,x ; load enemy y position on screen
clc ; clear carry in preparation for addition
adc #$14 ; add #$14 to enemy y position
bcs nametable_update_exit ; exit if off screen to the bottom
tay ; set update super-tile y position
lda ENEMY_X_POS,x ; load enemy x position on screen
sec ; set carry flag in preparation for subtraction
sbc #$0c ; subtract #$0c from enemy x position
bcc nametable_update_exit ; exit if off screen to the left
jsr load_bank_3_update_nametable_supertile ; draw super-tile $10 at position (a,y)
bcs @set_slot_exit ; exit if CPU_GRAPHICS_BUFFER is full
lda $07 ; load whether or not to set collision
bne @set_slot_exit ; exit if collision shouldn't be set
lda #$01 ; left side of super-tile bg collision (#$01 = empty then solid collision codes)
ldy #$01 ; right side of super-tile bg collision (#$01 = empty then solid collision codes)
; sets a horizontal ground collision area to walk on under a row of empty collision tiles
jsr set_supertile_bg_collisions ; update bg collision codes for a single super-tile at PPU address $12 (low) $13 (high)
@set_slot_exit:
ldx ENEMY_CURRENT_SLOT
@exit:
rts
; draws the nametable pattern table tiles specified by a at the enemy position
; ultimately, a (multiplied by #$05) is an offset into the level-specific tile_animation table
; input
; * a - offset into level-specific tile_animation table, e.g. level_2_4_tile_animation
; does not update palette, i.e. leave existing palette
; output
; * carry - clear when successful, set when CPU_GRAPHICS_BUFFER is full
update_enemy_nametable_tiles_no_palette:
ora #$80 ; do not update palette on nametable tile update
; draws the nametable pattern table tiles specified by a at the enemy position
; only called by bank 0 for wall turret and wall core
; ultimately, a (multiplied by #$05) is an offset into the level-specific tile_animation table
; input
; a - offset into level-specific tile_animation table, e.g. level_2_4_tile_animation
; if bit 7 clear, then update palette, if bit 7 set do not update palette
; output
; * carry - clear when successful, set when CPU_GRAPHICS_BUFFER is full
update_enemy_nametable_tiles:
sta $10 ; store offset in $10
lda ENEMY_Y_POS,x ; load enemy y position on screen
sec ; set carry flag in preparation for subtraction
sbc #$04 ; subtract #$04 from y position (top)
bcc nametable_update_exit
tay ; set enemy y position for load_bank_3_update_nametable_tiles
lda ENEMY_X_POS,x ; load enemy x position on screen
sec ; set carry flag in preparation for subtraction
sbc #$04 ; subtract #$04 from x position (left)
bcc nametable_update_exit
jsr load_bank_3_update_nametable_tiles ; draw tile code $10 to nametable at (a, y)
ldx ENEMY_CURRENT_SLOT ; restore x to point to current enemy
rts
nametable_update_exit:
clc
ldx ENEMY_CURRENT_SLOT
rts
; gets enemy's bg collision code and look for solid collision
; if collision with floor, load collision code one row down (half supertile)
; to see if it's a floor collision on top of a solid object
; output
; * a - collision code of half row down if floor collision, otherwise, current collision code
; * carry flag - set when collision code #$80 (solid)
check_enemy_collision_solid_bg:
ldy #$00 ; y = #$00
lda #$00 ; a = #$00
jsr add_a_y_to_enemy_pos_get_bg_collision ; add a to X position and y to Y position; get bg collision code
jmp floor_get_next_row_bg_collision ; if floor collision, get next half supertile row's collision code
; below the $13 BG_COLLISION_DATA offset
; initializes $13 to the enemy X position, and y to enemy Y position, then calls get_enemy_bg_collision
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
init_vars_get_enemy_bg_collision:
ldy #$00 ; y = #$00
; adds y to enemy y position and gets bg collision code
; input
; * y - added to enemy Y position
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
add_y_to_y_pos_get_bg_collision:
lda #$00 ; a = #$00
; adds a to X position and y to Y position for use in determining background collision
; ENEMY_X_POS and ENEMY_Y_POS are unaffected
; returns the bg collision code for current enemy
; input
; * a - added to enemy X position
; * y - added to enemy Y position
; output
; * a collision code #$00 (empty), #$01 (floor), #$02 (water), or #$80 (solid)
; * carry set when collision is with floor (#$01)
; * negative flag set when solid collision (#$80)
add_a_y_to_enemy_pos_get_bg_collision:
clc ; clear carry in preparation for addition
adc ENEMY_X_POS,x ; add to enemy x position on screen
sta $13 ; store enemy x position in $13 for get_enemy_bg_collision
tya ; transfer amount to add to y enemy y position into a
clc ; clear carry in preparation for addition
adc ENEMY_Y_POS,x ; add to enemy y position on screen
bcs @exit ; exit if overflow, i.e. enemy y position is off screen towards bottom
tay ; set enemy y position in y for bg collision detection
jmp get_enemy_bg_collision ; get bg collision code for position ($13, y)
@exit:
lda #$00 ; a = #$00
rts
; dead code, never called !(UNUSED)
bank_7_unused_label_04:
ldy #$00 ; y = #$00
; flying_capsule_routine_01 horizontal level
set_flying_capsule_y_vel:
lda ENEMY_Y_POS,x ; load enemy y position on screen
sta $01
lda ENEMY_VAR_1,x
sta $03
lda ENEMY_Y_VELOCITY_FAST,x
sta $04
lda ENEMY_Y_VELOCITY_FRACT,x
sta $05
jsr set_flying_capsule_path
lda $00
sta ENEMY_Y_VELOCITY_FAST,x
lda $01
sta ENEMY_Y_VELOCITY_FRACT,x
rts
; dead code, never called !(UNUSED)
bank_7_unused_label_05:
ldy #$00 ; y = #$00
; flying_capsule_routine_01 vertical level
; set various local variables to x velocity
; output
; * $01 - ENEMY_X_POS
; * $03 - ENEMY_VAR_2
; * $04 - ENEMY_X_VELOCITY_FAST
; * $05 - ENEMY_X_VELOCITY_FRACT
set_flying_capsule_x_vel:
lda ENEMY_X_POS,x ; load enemy x position on screen
sta $01
lda ENEMY_VAR_2,x
sta $03
lda ENEMY_X_VELOCITY_FAST,x
sta $04
lda ENEMY_X_VELOCITY_FRACT,x
sta $05
jsr set_flying_capsule_path
lda $00
sta ENEMY_X_VELOCITY_FAST,x
lda $01 ; load new fractional velocity
sta ENEMY_X_VELOCITY_FRACT,x ; save new fractional velocity
rts
; dead code, never called !(UNUSED)
bank_7_unused_label_06:
ldy #$00 ; y = #$00
; creates flight pattern for flying capsule
; input
; * $01 - ENEMY_X_POS or ENEMY_Y_POS
; * $03 - ENEMY_VAR_2 or ENEMY_VAR_1 (amount to subtract from $01)
; * $04 - ENEMY_X_VELOCITY_FAST or ENEMY_Y_VELOCITY_FAST
; * $05 - ENEMY_X_VELOCITY_FRACT or ENEMY_Y_VELOCITY_FRACT
set_flying_capsule_path:
lda $01 ; load position point (x or y position)
sec ; set carry flag in preparation for subtraction
sbc $03 ; subtract ENEMY_VAR_1 or ENEMY_VAR_2 from x or y position
sta $01 ; store new x position back in $01
lda #$00 ; a = #$00
sbc #$00 ; subtract #$00 and any overflow
sta $00 ; new fractional velocity in $00
sta $07 ; store overflow in $07
tya ; move overflow to a
beq @set_vars_exit
bmi @loop2
@loop:
asl $01 ; shift left the x or y position
rol $00 ; rotate fractional velocity left, bringing in any bit 7 from $01
dey
bne @loop
beq @set_vars_exit
@loop2:
lsr $07
ror $00
ror $01
iny
bne @loop2
@set_vars_exit:
lda $05
sec ; set carry flag in preparation for subtraction
sbc $01
sta $01
lda $04
sbc $00
sta $00
rts
; red turret
red_turret_find_target_player:
jsr player_enemy_x_dist ; a = closest x distance to enemy from players, y = closest player (#$00 or #$01)
tya ; set the closest player to a #$00 for p1, #$01 for p2
eor #$01 ; flip to other player
sta $0c ; store farther player in $0c
lda $08 ; load player 1 x distance to enemy
sta $0a ; store player 1 x distance to enemy in $0a
lda $09 ; load player 2 x distance to enemy
sta $0b ; store player 2 x distance to enemy in $0b
jsr player_enemy_y_dist ; a = closest y distance to enemy from players, y = closest player (#$00 or #$01)
tya ; set the closest player to a #$00 for p1, #$01 for p2
eor #$01 ; flip to other player
cmp $0c ; compare farther y player to farther x player
bne @continue ; branch if not the same player
tya ; one player is farther in both x and y axises, transfer farther player to a
sta $0c ; transfer farther player to $0c
@continue:
tax ; transfer farther y player to x
tay ; transfer farther y player to y
lda $08,x ; load the farther y player's distance
ldx $0c ; load the farther player's y distance
cmp $0a,x ; compare farther player's x distance
bcc @exit ; branch if
ldy $0c ; set y to the player to target
lda $0a,x ; load
@exit:
ldx ENEMY_CURRENT_SLOT
rts
; calculates x distance between p1 and the enemy and p2 and the enemy
; stores shortest distance in a, player number in y
; input
; * x - the current enemy offset
; output
; * a - the shortest x distance to current enemy (either p1 or p2)
; * y - the player closest, #$00 for p1, #$01 for p2
; * $08 - p1 x distance
; * $09 - p2 x distance
; when player state is not #$01, #$fe is stored in $08 or #$ff in $09
player_enemy_x_dist:
lda SPRITE_X_POS ; load player 1 x position
sec ; prepare for subtraction
sbc ENEMY_X_POS,x ; enemy x position on screen
bcs @continue_to_p2 ; branch if no overflow occurred
eor #$ff ; overflow occurred, flip bits and add one (two's compliment)
adc #$01
@continue_to_p2:
sta $08 ; store distance between player and enemy in $08
lda SPRITE_X_POS+1 ; load player 2 x position
sec ; prepare for subtraction
sbc ENEMY_X_POS,x ; enemy x position on screen
jmp lda_closer_distance ; jump to determine smallest of $08 (p1) and $09 (p2) store in a
; calculates y distance between p1 and p2 with the current enemy (x register)
; input
; * x - the current enemy offset
; output
; * a - the shortest y distance to current enemy (either p1 or p2)
; if both players are in non-normal state, a is set to #$fe
; * y - the player closest, #$00 for p1, #$01 for p2
; * $08 - p1 y distance
; * $09 - p2 y distance
; when player state is not #$01, #$fe is stored in $08 or #$ff in $09
player_enemy_y_dist:
lda SPRITE_Y_POS ; load player 1 y position
sec ; prepare for subtraction
sbc ENEMY_Y_POS,x ; enemy y position on screen
bcs @continue_to_p2 ; branch if no overflow occurred
eor #$ff ; overflow occurred, flip bits and add one (two's compliment)
adc #$01
@continue_to_p2:
sta $08 ; store distance between player and enemy in $08
lda SPRITE_Y_POS+1 ; load player 2 y position
sec ; prepare for subtraction
sbc ENEMY_Y_POS,x ; enemy y position on screen
; take the smallest of $08 (p1) and $09 (p2) and store in a accounting for overflow
; ignoring non-normal player state
lda_closer_distance:
bcs @continue
eor #$ff ; overflow occurred, flip bits and add one (two's compliment)
adc #$01
@continue:
sta $09 ; store player 2 x or y distance from current enemy in $09
ldy #$fe ; y = #$fe
lda PLAYER_STATE ; load player state (#$00 dropping into level, #$01 normal, #$02 dead, #$03 can't move)
cmp #$01
beq @continue_p1 ; branch if player state is #$01 (normal)
sty $08 ; player 1 state not normal, store #$fe in $08
@continue_p1:
ldy #$ff ; y = #$ff
lda PLAYER_STATE+1 ; load 2nd player state (#$00 dropping into level, #$01 normal, #$02 dead, #$03 can't move)
cmp #$01
beq @set_closest ; branch if p2 state is normal
sty $09 ; player 2 state not normal, set to #$ff
@set_closest:
lda $09 ; load player 2 distance (or #$ff if not normal)
ldy #$01
cmp $08 ; compare player 1 distance to player 2 distance
bcc @exit ; branch if $09 < $08 (p2 is closer)
dey ; p1 is closer, ensure player specified is p1
lda $08 ; load the closest distance
@exit:
rts
; gets a number from #$06 to #$00 indicating how far the enemy at position $09 is from the left of the screen
; starting from #$06 for farthest left, down to #$00 for farthest right
; very similar to find_close_segment in bank 0
; usually used together to compare player and enemy x positions on indoor levels
; input
; * $09 - current enemy X position
; output
; * a velocity code
find_far_segment_for_x_pos:
lda $09 ; load enemy X position
; gets a number from #$06 to #$00 indicating how far the enemy is from the left of the screen
; starting from #$06 for farthest left, down to #$00 for farthest right
; very similar to find_close_segment in bank 0
; usually used together to compare player and enemy x positions on indoor levels
find_far_segment_for_a:
ldy #$06 ; y = #$06
@loop:
cmp far_segment_code_tbl,y ; compare a to far_segment_code_tbl,y
bcc @exit ; branch if a < far_segment_code_tbl,y
dey ; a >= far_segment_code_tbl,y move to next larger velocity value
bmi @use_code_0 ; if y became negative (shouldn't happen), use largest velocity code
bcs @loop
@use_code_0:
lda #$00 ; safety code, use #$00 velocity code
rts
@exit:
tya ; move far_segment_code_tbl into a
rts
; table for ?? (#$7 bytes)
far_segment_code_tbl:
.byte $ff,$94,$8c,$84,$7c,$74,$6c
; grenade_routine_01
; weapon_item_routine_01 (indoor/base level only)
; creates a falling arc pattern by using X and Y velocities to update X and Y positions
; used for grenades and for weapon items in indoor/base levels
set_enemy_falling_arc_pos:
lda ENEMY_VAR_2,x ; load ENEMY_VAR_2,x
clc ; clear carry in preparation for addition
adc ENEMY_VAR_4,x ; add ENEMY_VAR_2,x and ENEMY_VAR_4,x
sta ENEMY_VAR_2,x ; store result back in ENEMY_VAR_2,x
lda ENEMY_VAR_3,x ; load ENEMY_VAR_3,x
adc ENEMY_VAR_B,x ; add ENEMY_VAR_3,x and ENEMY_VAR_B,x along with any overflow carry
sta ENEMY_VAR_3,x ; store result back in ENEMY_VAR_3,x
lda ENEMY_Y_VEL_ACCUM,x ; load ENEMY_Y_VEL_ACCUM,x
clc ; clear carry in preparation for addition
adc ENEMY_Y_VELOCITY_FRACT,x ; ENEMY_Y_VEL_ACCUM,x + ENEMY_Y_VELOCITY_FRACT,x
sta ENEMY_Y_VEL_ACCUM,x ; update ENEMY_Y_VEL_ACCUM,x to sum
lda ENEMY_VAR_1,x ; load ENEMY_VAR_1,x
adc ENEMY_Y_VELOCITY_FAST,x ; ENEMY_Y_VELOCITY_FAST,x + ENEMY_VAR_1,x along with any overflow carry
sta ENEMY_VAR_1,x ; update ENEMY_VAR_1,x with the result
cmp #$f0 ; if the enemy has fallen below the screen, remove it
bcs @remove_enemy
clc ; clear carry in preparation for addition
adc ENEMY_VAR_3,x
sta ENEMY_Y_POS,x ; enemy y position on screen
jmp update_enemy_x_pos_rem_off_screen ; add velocity to enemy X position; remove enemy if X position < #$08 (off screen to left)
@remove_enemy:
jmp remove_enemy ; remove enemy
; weapon_item_routine_00 indoor level
; sets initial velocities for indoor level based on X position
; sets Y velocity to #$01 for the high byte and #$00 for the low byte
; doesn't use weapon_item_init_vel_tbl like outdoor levels
set_weapon_item_indoor_velocity:
lda ENEMY_X_POS,x ; load enemy x position on screen
jsr find_far_segment_for_a ; find the appropriate velocity code, given the X position
asl ; double since each entry is #$02 bytes
tay
lda weapon_item_indoor_vel_tbl,y ; load X fractional velocity byte for velocity code
sta ENEMY_X_VELOCITY_FRACT,x ; set X fractional velocity byte
lda weapon_item_indoor_vel_tbl+1,y ; load X velocity fast value for velocity code
sta ENEMY_X_VELOCITY_FAST,x ; set X velocity fast value (number of units to move in the X direction per frame)
lda #$00 ; a = #$00
sta ENEMY_Y_VELOCITY_FRACT,x ; set Y fractional velocity byte to #$00
lda #$01 ; a = #$01
sta ENEMY_Y_VELOCITY_FAST,x ; set Y velocity fast value to #$01
rts
; two-byte values for weapon item X velocity in indoor levels (#$e bytes)
; byte 0 - X fractional velocity value
; byte 1 - X velocity fast value
weapon_item_indoor_vel_tbl:
.byte $aa,$00 ; aa 170 170 / 256 = 0.66
.byte $71,$00 ; 71 113 113 / 256 = 0.44
.byte $38,$00 ; 38 56 56 / 256 = 0.22
.byte $00,$00 ; 00 0
.byte $c8,$ff ; -38 -56 -56 / 256 = -0.22
.byte $8f,$ff ; -71 -113 -113 / 256 = -0.44
.byte $56,$ff ; -aa -170 -170 / 256 = -0.66
; find next available enemy slot (between slots 0 and 6)
; slots 0 to 6 are reserved for soldiers
; slot number is stored in x register
; zero flag set when found, not set when no slots available
find_next_enemy_slot_6_to_0:
ldx #$06 ; x = #$06
bne find_next_enemy_slot_x_to_0
; find next available enemy slot (all slots 0-f)
; slot number is stored in x register
; zero flag set when found, not set when no slots available
find_next_enemy_slot:
ldx #$0f ; x = #$0f
; slot number is stored in x register
find_next_enemy_slot_x_to_0:
lda ENEMY_ROUTINE,x
beq find_enemy_routine_slot_exit
dex ; decrement offset
bpl find_next_enemy_slot_x_to_0 ; if not zero loop to see if a lower index is available
find_enemy_routine_slot_exit:
rts
find_bullet_slot:
ldx #$0f ; x = #$0f
@loop:
lda ENEMY_TYPE,x ; load current enemy type
cmp #$01 ; is enemy type a bullet
beq find_enemy_routine_slot_exit
dex
bpl @loop
ldx #$00 ; no bullet found, return slot 0
rts
; dead code, never called !(UNUSED)
; runs clear_enemy on all enemies
bank_7_unused_label_07:
ldx #$0f ; set loop counter for enemies
@clear_next_enemy:
jsr clear_enemy ; clear current enemy vars
dex ; decrement enemy offset
bpl @clear_next_enemy ; branch if more enemies to clear
rts
clear_sprite_clear_enemy_pt_3:
lda #$00 ; a = #$00
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
beq clear_enemy_pt_3
clear_enemy_custom_vars:
lda #$00 ; a = #$00
beq clear_enemy_pt_4 ; always jump
; clear many of the enemy variables
clear_enemy:
lda #$00 ; a = #$00
sta ENEMY_ROUTINE,x
sta ENEMY_HP,x
sta ENEMY_TYPE,x
sta ENEMY_SPRITES,x
clear_enemy_pt_2:
sta ENEMY_ATTRIBUTES,x
sta ENEMY_Y_POS,x
sta ENEMY_X_POS,x
sta ENEMY_Y_VEL_ACCUM,x
sta ENEMY_X_VEL_ACCUM,x
clear_enemy_pt_3:
sta ENEMY_SPRITE_ATTR,x
sta ENEMY_Y_VELOCITY_FRACT,x
sta ENEMY_X_VELOCITY_FRACT,x
sta ENEMY_Y_VELOCITY_FAST,x
sta ENEMY_X_VELOCITY_FAST,x
sta ENEMY_ANIMATION_DELAY,x
sta ENEMY_VAR_A,x
sta ENEMY_ATTACK_DELAY,x
sta ENEMY_FRAME,x
sta ENEMY_STATE_WIDTH,x
sta ENEMY_SCORE_COLLISION,x
clear_enemy_pt_4:
sta ENEMY_VAR_1,x
sta ENEMY_VAR_2,x
sta ENEMY_VAR_3,x
sta ENEMY_VAR_4,x
rts
; initialize enemy attributes
; initialize enemy_routine index
; initialize enemy tiles
; initialize enemy hp
; initialize enemy position
initialize_enemy:
lda #$01 ; a = #$01
sta ENEMY_ROUTINE,x ; initialize enemy routine index to ..._routine_00 (always off by one)
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
lda #$00 ; a = #$00
jsr clear_enemy_pt_2 ; set many variables to a
tya ; save existing value of y to stack so it isn't overwritten
pha ; push on stack
lda ENEMY_TYPE,x ; load current enemy type
cmp #$10 ; see if shared enemy (used across levels)
ldy #$10
bcc @continue ; set to use enemy_prop_00 when for common/shared enemies (ENEMY_TYPE < #$10)
lda CURRENT_LEVEL ; not shared enemy type, load current level to figure out offset
asl ; double since each entry in enemy_prop_ptr_tbl is #$02 bytes
tay ; set offset based on current level
; y is offset enemy_prop_ptr_tbl
@continue:
lda enemy_prop_ptr_tbl,y ; load low byte of enemy_prop_xx address
sta $8c ; store in $8c
lda enemy_prop_ptr_tbl+1,y ; load high byte of enemy_prop_xx address
sta $8d ; store in $8d
lda ENEMY_TYPE,x ; load current enemy type
asl
asl ; quadruple since each entry is #$04 bytes
tay
lda ($8c),y ; load first byte of enemy_prop_XX
sta ENEMY_STATE_WIDTH,x ; set ENEMY_STATE_WIDTH
iny
lda ($8c),y ; read next byte of enemy_prop_XX
sta ENEMY_SCORE_COLLISION,x ; set ENEMY_SCORE_COLLISION
iny
lda ($8c),y ; read next byte of enemy_prop_XX
sta ENEMY_HP,x ; set ENEMY_HP
iny
lda ($8c),y ; read next byte of enemy_prop_XX
sta ENEMY_VAR_A,x ; set ENEMY_VAR_A
pla ; pull saved value of y from stack
tay ; restore previous value of y from stack
rts
; pointer table for enemy properties (#$9 * #$2 = #$12 bytes)
; enemy width, enemy score code, enemy collision box code, enemy HP, and enemy hit sound
enemy_prop_ptr_tbl:
.addr enemy_prop_00 ; Level 1 - CPU address $ee9f
.addr enemy_prop_01 ; Level 2 - CPU address $eeab
.addr enemy_prop_02 ; Level 3 - CPU address $eeef
.addr enemy_prop_01 ; Level 4 - CPU address $eeab
.addr enemy_prop_04 ; Level 5 - CPU address $ef07
.addr enemy_prop_05 ; Level 6 - CPU address $ef23
.addr enemy_prop_06 ; Level 7 - CPU address $ef37
.addr enemy_prop_07 ; Level 8 - CPU address $ef5b
.addr enemy_prop_00 ; shared enemies (ENEMY_TYPE < #$10)
; (#$46 * #$4 = #$118 bytes)
; byte 0: ENEMY_STATE_WIDTH - related to facing direction and/or enemy width
; byte 1: ENEMY_SCORE_COLLISION - score code (bits 4-7), explosion type (bit 3), collision box code
; byte 2: ENEMY_HP - enemy hp
; byte 3: ENEMY_VAR_A
; shared enemies and level 1 enemies
enemy_prop_00:
.byte $82,$22,$01,$00 ; weapon item (00)
.byte $80,$00,$01,$00 ; enemy bullet (01)
.byte $0f,$32,$f0,$00 ; weapon box (02)
; indoor/base level enemies
enemy_prop_01:
.byte $0b,$32,$01,$00 ; weapon zeppelin (03)
.byte $8f,$22,$08,$00 ; rotating gun (04)
.byte $83,$10,$01,$00 ; running man (05)
.byte $83,$30,$01,$00 ; rifle man (06)
.byte $8f,$30,$08,$00 ; red turret (07)
.byte $0f,$52,$f1,$00 ; triple cannon (08)
.byte $00,$00,$01,$00 ; ? (09)
.byte $0f,$42,$f0,$00 ; wall plating (0a)
.byte $8a,$05,$01,$00 ; mortar shot (0b)
.byte $83,$42,$01,$00 ; scuba diver (0c)
.byte $00,$00,$01,$00 ; ? (0d)
.byte $0e,$33,$0a,$00 ; turret man (0e)
.byte $80,$01,$01,$00 ; turret man bullet (0f)
; level 1 specific enemies
.byte $0f,$42,$10,$00 ; boss bomb turret (10)
.byte $0c,$82,$20,$00 ; door plate with siren (11)
.byte $89,$00,$01,$00 ; exploding bridge (12)
; level 2/4 enemies
.byte $8d,$02,$01,$00 ; boss eye (10)
enemy_prop_02:
.byte $2f,$22,$05,$00 ; rollers (11)
.byte $81,$03,$01,$00 ; grenades (12)
.byte $9f,$35,$04,$00 ; wall cannon (13)
.byte $9f,$05,$01,$00 ; core (14)
.byte $13,$16,$01,$00 ; running guy (15)
.byte $13,$16,$01,$00 ; jumping guy (16)
enemy_prop_04:
.byte $13,$36,$01,$00 ; seeking guy (17)
.byte $13,$16,$01,$00 ; group of 4 (18)
.byte $89,$00,$f1,$00 ; green guys generator (19)
.byte $81,$00,$f1,$00 ; rollers generator (1a)
.byte $8f,$13,$02,$01 ; sphere projectile (1b)
.byte $8f,$02,$01,$00 ; boss gemini (1c)
.byte $0a,$15,$01,$00 ; spinning bubbles projectile (1d)
enemy_prop_05:
.byte $03,$30,$01,$00 ; blue jumping guy (1e)
.byte $03,$30,$01,$00 ; red shooting guy (1f)
.byte $81,$00,$f1,$00 ; red/blue guys generator (20)
; level 3 enemies
.byte $c0,$04,$f0,$00 ; floating rock platform (10)
.byte $80,$02,$f0,$00 ; moving flame (11)
enemy_prop_06:
.byte $81,$00,$f0,$00 ; falling rock generator (12)
.byte $8f,$31,$05,$00 ; falling rock (13)
.byte $8d,$83,$f1,$02 ; level 3 boss mouth (14)
.byte $0e,$52,$f1,$00 ; level 3 dragon arm orb (15)
; level 5 enemies
.byte $81,$00,$f0,$00 ; grenade generator (10)
.byte $81,$02,$f1,$00 ; grenade (11)
.byte $85,$79,$f0,$00 ; tank (12)
.byte $81,$00,$f0,$00 ; pipe joint (13)
.byte $8d,$93,$20,$00 ; alien carrier (14)
enemy_prop_07:
.byte $02,$20,$01,$00 ; flying saucer (15)
.byte $0a,$12,$01,$00 ; bomb drop (16)
; level 6 enemies
.byte $81,$0f,$f0,$00 ; fire beam - down (10)
.byte $81,$0f,$f0,$00 ; fire beam - left (11)
.byte $81,$0f,$f0,$00 ; fire beam - right (12)
.byte $04,$9d,$01,$02 ; boss robot (13)
.byte $80,$05,$01,$00 ; spiked disk projectile (14)
; level 7 enemies
.byte $80,$0a,$f0,$00 ; mechanical claw (10)
.byte $8d,$0f,$10,$00 ; raising spiked wall (11)
.byte $0c,$0f,$10,$00 ; spiked wall (12)
.byte $81,$00,$f0,$00 ; cart generator (13)
.byte $6e,$0c,$03,$00 ; cart - moving (14)
.byte $6e,$0c,$03,$00 ; cart - immobile (15)
.byte $0c,$93,$20,$00 ; armored door (16)
.byte $8f,$72,$08,$00 ; mortar launcher (17)
.byte $89,$00,$01,$00 ; enemy generator (18)
; level 8 enemies
.byte $04,$78,$01,$02 ; alien guardian (10)
.byte $06,$22,$01,$01 ; alien fetus (11)
.byte $06,$42,$01,$01 ; alien mouth (12)
.byte $02,$22,$01,$00 ; white sentient blob (13)
.byte $06,$33,$01,$01 ; alien spider (14)
.byte $06,$62,$10,$01 ; spider spawn (15)
.byte $04,$a7,$01,$03 ; heart (16)
; pointer table for triple cannon (#$9 * #$2 = #$12 bytes)
wall_cannon_routine_ptr_tbl:
.addr wall_cannon_routine_00 ; CPU address $efc7 - set hp to #$08, animation delay to #$50, advance routine
.addr wall_cannon_routine_01 ; CPU address $efd4
.addr wall_cannon_routine_02 ; CPU address $f007
.addr wall_cannon_routine_03 ; CPU address $f048
.addr wall_cannon_routine_04 ; CPU address $f06d
.addr enemy_routine_init_explosion ; CPU address $e74b
.addr enemy_routine_explosion ; CPU address $e7b0
.addr enemy_routine_remove_enemy ; CPU address $e806
; set hp to #$08, animation delay to #$50, advance routine
wall_cannon_routine_00:
lda #$08 ; a = #$08 (hp for triple cannon)
sta ENEMY_VAR_1,x ; set hp
lda #$50 ; a = #$50
; set the animation delay to a and advanced the ENEMY_ROUTINE
; input
; * a - the ENEMY_ANIMATION_DELAY
; this label is identical to two other labels
; * bank 0 - set_anim_delay_adv_enemy_routine_00
; * bank 0 - set_anim_delay_adv_enemy_routine_01
set_anim_delay_adv_enemy_routine:
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
jmp advance_enemy_routine ; advance to next routine
wall_cannon_routine_01:
lda ENEMY_ATTACK_FLAG ; see if enemies should attack
beq wall_cannon_exit ; exit if enemies shouldn't attack
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne wall_cannon_exit ; exit if animation delay hasn't elapsed
jsr animate_wall_cannon ; animation delay elapsed, animate wall cannon
bcs wall_cannon_exit ; exit if unable to update the nametable to try again next frame
lda ENEMY_FRAME,x ; load enemy animation frame number
cmp #$02
bcs wall_cannon_set_delays
inc ENEMY_FRAME,x ; increment enemy animation frame number
wall_cannon_exit:
rts
; used only by wall_cannon_routine_01
wall_cannon_set_delays:
lda ENEMY_VAR_1,x ; load enemy hp
sta ENEMY_HP,x ; store hp temporarily
lda #$04 ; a = #$04
sta ENEMY_ATTACK_DELAY,x ; set delay between attacks
lda #$40 ; a = #$40 (delay between attack and closing)
bne set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY to #$40 and advance enemy routine
animate_wall_cannon:
lda #$06 ; a = #$06 (delay between frames when open/close)
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
lda ENEMY_FRAME,x ; load enemy animation frame number
jmp draw_enemy_supertile_a_set_delay ; draw pattern table tile specified in a at enemy position
wall_cannon_routine_02:
lda ENEMY_ATTACK_DELAY,x ; load delay between attacks
beq @continue ; skip creating bullet if delay is #$00
dec ENEMY_ATTACK_DELAY,x ; decrement delay between attacks
bne @continue ; skip creating bullet if delay not #$00
lda #$02 ; delay has elapsed, a = #$02 (number of bullets to fire)
sta $16 ; set number of bullets to fire to #$02 (#$03 bullets)
@create_bullet_loop:
ldy $16 ; load remaining number of bullets to fire
lda wall_cannon_bullet_x_offset,y ; set horizontal offset from enemy position (param for add_with_enemy_pos)
ldy #$08 ; set vertical offset from enemy position (param for add_with_enemy_pos)
jsr add_with_enemy_pos ; stores absolute screen x position in $09, and y position in $08
ldy $16 ; load remaining number of bullets to fire
lda wall_cannon_bullet_type_and_angle,y ; load bullet type (xxx. ....) and angle index (...x xxxx)
ldy #$07 ; set bullet speed to #$07
jsr create_enemy_bullet_angle_a ; create a bullet with speed y, bullet type a, angle a at position ($09, $08)
dec $16 ; decrement number of bullets to fire
bpl @create_bullet_loop
@continue:
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne wall_cannon_exit
lda ENEMY_HP,x ; load enemy hp
sta ENEMY_VAR_1,x ; store enemy hp while invulnerable
lda #$f1 ; a = #$f1 (f1 = hittable, no damage)
sta ENEMY_HP,x ; set enemy hp
lda #$06 ; a = #$06
jmp set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY to #$06 and advance enemy routine
; table for bullets starting x positions (#$3 bytes)
wall_cannon_bullet_x_offset:
.byte $f8,$00,$08
; table for bullets type and angle (#$3 bytes)
wall_cannon_bullet_type_and_angle:
.byte $48,$46,$44
wall_cannon_routine_03:
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne wall_cannon_exit
jsr animate_wall_cannon
bcs wall_cannon_exit_01
lda ENEMY_FRAME,x ; load enemy animation frame number
beq wall_cannon_calc_delay_set_routine_01
dec ENEMY_FRAME,x ; decrement enemy animation frame number
wall_cannon_exit_01:
rts
wall_cannon_calc_delay_set_routine_01:
lda PLAYER_WEAPON_STRENGTH
cmp #$02 ; if weapon strength < 2
lda #$c0 ; a = #$c0 (delay for weapon strength 0-1)
bcc @set_delay_set_routine_01
lda #$60 ; a = #$60 (delay for weapon strength 2-3)
@set_delay_set_routine_01:
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
lda #$02 ; a = #$02
jmp set_enemy_routine_to_a ; set enemy routine index to wall_cannon_routine_01
wall_cannon_routine_04:
lda #$05 ; a = #$05 (tile code after destruction)
jsr draw_enemy_supertile_a ; draw super-tile a at position (ENEMY_X_POS, ENEMY_Y_POS)
bcs wall_cannon_exit_01
jmp advance_enemy_routine ; advance to next routine
; pointer table for wall plating (#$7 * #$2 = #$e bytes)
; level 2/4 boss screen targets
; * 4 exist on level 2 boss screen
; * 3 exist on level 4 boss screen
wall_plating_routine_ptr_tbl:
.addr wall_plating_routine_00 ; CPU address $f085
.addr wall_plating_routine_01 ; CPU address $f08a
.addr wall_plating_routine_02 ; CPU address $f0b0
.addr wall_plating_routine_03 ; CPU address $f0b1
.addr enemy_routine_init_explosion ; CPU address $e74b
.addr enemy_routine_explosion ; CPU address $e7b0
.addr enemy_routine_remove_enemy ; CPU address $e806
; wall plating - pointer 0
wall_plating_routine_00:
lda #$80 ; a = #$80 (delay before deployment)
jmp set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY to #$80 and advance enemy routine
; wall plating - pointer 1
wall_plating_routine_01:
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne wall_plating_routine_02
lda #$04 ; a = #$04 (delay between frames when deploying)
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
lda ENEMY_FRAME,x ; load enemy animation frame number (#$00 to #$03)
clc ; clear carry in preparation for addition
adc #$03 ; draw the appropriate frame of the animation (level_2_4_nametable_update_supertile_data offset)
jsr draw_enemy_supertile_a_set_delay ; draw pattern table tile specified in a at enemy position
bcs wall_plating_routine_02
inc ENEMY_FRAME,x ; increment enemy animation frame number
lda ENEMY_FRAME,x ; load enemy animation frame number
cmp #$02 ; see if wall plating is now open
bcc wall_plating_routine_02 ; exit if wall cannon is open
lda #$0a ; a = #$0a (hp for wall plating)
sta ENEMY_HP,x ; set enemy hp
bcs wall_plating_adv_enemy_routine
; wall plating - pointer 2
wall_plating_routine_02:
rts
; wall plating - pointer 3
; called when enemy is destroyed
wall_plating_routine_03:
lda #$05 ; a = #$05 (level_2_4_nametable_update_supertile_data offset)
jsr draw_enemy_supertile_a ; draw destroyed wall plating super-tile
bcs wall_plating_routine_02 ; exit
inc WALL_PLATING_DESTROYED_COUNT ; increment number of boss platings destroyed
wall_plating_adv_enemy_routine:
jmp advance_enemy_routine ; advance to next routine
; pointer table for turret man (#$6 * #$2 = #$c bytes)
turret_man_routine_ptr_tbl:
.addr turret_man_routine_00 ; CPU address $f0c9
.addr turret_man_routine_01 ; CPU address $f0db
.addr turret_man_routine_02 ; CPU address $f0ec
.addr enemy_routine_init_explosion ; CPU address $e74b
.addr enemy_routine_explosion ; CPU address $e7b0
.addr enemy_routine_remove_enemy ; CPU address $e806
turret_man_routine_00:
lda #$bd ; a = #$bd
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
lda ENEMY_ATTRIBUTES,x ; load enemy attributes
asl
asl
asl
asl ; shift low nibble to high nibble
clc ; clear carry in preparation for addition
adc #$01 ; add one
jmp set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY to a and advance enemy routine
turret_man_routine_01:
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne turret_man_exit
inc ENEMY_SPRITES,x ; increment enemy sprite code to CPU buffer
lda #$05 ; a = #$05 (recoil delay)
jmp set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY to #$05 and advance enemy routine
turret_man_exit:
rts
turret_man_routine_02:
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne turret_man_exit
lda #$0c ; a = #$0c (sound_0c)
jsr play_sound ; play machine gun (M weapon) sound
lda #$0f ; a = #$0f (enemy code of projectile)
sta $0a
lda #$f0 ; a = #$f0
ldy #$fc ; y = #$fc
jsr generate_enemy_at_pos ; generate enemy type $0a at relative position a,y
lda ENEMY_ATTRIBUTES,x ; load enemy attributes
asl
asl
asl
asl
clc ; clear carry in preparation for addition
adc #$30 ; delay between shots (attr. * 10 + 30)
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
dec ENEMY_SPRITES,x ; decrement enemy sprite code to CPU buffer
lda #$02 ; a = #$02
jmp set_enemy_routine_to_a ; set enemy routine index to a
; pointer table for turret man bullet (#$3 * #$2 = #$6 bytes)
turret_man_bullet_routine_ptr_tbl:
.addr turret_man_bullet_routine_00 ; CPU address $f11f
.addr turret_man_bullet_routine_01 ; CPU address $f131
.addr remove_enemy ; Remove Enemy - CPU address $e809
; turret man bullet - pointer 1
turret_man_bullet_routine_00:
lda #$fd ; a = #$fd
sta ENEMY_X_VELOCITY_FAST,x
lda #$80 ; a = #$80
sta ENEMY_X_VELOCITY_FRACT,x
lda #$1f ; a = #$1f (sprite_1f)
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
turret_man_adv_routine:
jmp advance_enemy_routine ; advance to next routine
; turret man bullet - pointer 2
turret_man_bullet_routine_01:
lda ENEMY_X_POS,x ; load enemy x position on screen
cmp #$f0
bcs turret_man_adv_routine
jmp update_enemy_pos ; apply velocities and scrolling adjust
; pointer table for scuba diver (#$6 * #$2 = #$c bytes)
scuba_soldier_routine_ptr_tbl:
.addr scuba_soldier_routine_00 ; CPU address $f147
.addr scuba_soldier_routine_01 ; CPU address $f14c
.addr scuba_soldier_routine_02 ; CPU address $f183
.addr enemy_routine_init_explosion ; CPU address $e74b
.addr enemy_routine_explosion ; CPU address $e7b0
.addr enemy_routine_remove_enemy ; CPU address $e806
scuba_soldier_routine_00:
lda #$80 ; a = #$80 (delay before first attack)
jmp set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY counter to #$80; advance enemy routine
; hides in water, bumping up every #$08 frames until activated
; * for vertical levels, scuba soldiers aren't activated until towards bottom 72% of screen
; * snow field level has scuba soldiers at bottom of screen already so no scroll activation delay.
; only timer delay
; if already activated from scuba_soldier_routine_02, simply wait for delay timer to elapse
scuba_soldier_routine_01:
lda #$4b ; a = #$4b (sprite_4b scuba soldier hiding)
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
lda ENEMY_ANIMATION_DELAY,x ; load enemy animation frame delay counter
asl ; first execution is #$00, but can be set from
asl ; scuba_soldier_routine_02 after firing mortar
asl
asl
lda #$08 ; a = #$08
bcc @continue ; #$07 out of every #$08 frames use #$08, otherwise use #$00
lda #$00 ; a = #$00
@continue:
sta ENEMY_SPRITE_ATTR,x ; set gun recoil flag value
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
bne @exit ; exit if animation delay hasn't elapsed
lda ENEMY_Y_POS,x ; load enemy y position on screen
cmp #$b8 ; if vertical, don't shoot until this height
bcs @activate_scuba_soldier ; scuba soldier is at heigh #$b8 or higher (lower on screen), 'activate' enemy
lda #$10 ; wait another #$10 frames before checking again
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
@exit:
rts
; enable enemy collisions, set attack delay to #$10, set animation delay to #$30,
; advance routine to scuba_soldier_routine_02
@activate_scuba_soldier:
jsr enable_enemy_collision ; enable bullet-enemy collision and player-enemy collision checks
lda #$10 ; a = #$10 (delay between aim and fire)
sta ENEMY_ATTACK_DELAY,x ; set delay between attacks
lda #$30 ; a = #$30 (total delay of vulnerability)
jmp set_anim_delay_adv_enemy_routine ; set ENEMY_ANIMATION_DELAY to #$30 and advance enemy routine
; scuba soldier activated, set sprite, and fire mortar shot, then go back to scuba_soldier_routine_01
scuba_soldier_routine_02:
lda #$4c ; a = #$4c (sprite_4c scuba soldier out of water shooting up)
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
lda #$00 ; a = #$00
ldy ENEMY_VAR_1,x ; load gun recoil delay timer
beq @continue ; continue to firing logic, if elapsed
dec ENEMY_VAR_1,x ; recoil delay timer elapsed, set gun recoil
lda #$08 ; a = #$08 (gun recoil flag)
; create mortar shot (#$07) if timers have elapsed
@continue:
sta ENEMY_SPRITE_ATTR,x ; set/clear gun recoil flag
dec ENEMY_ANIMATION_DELAY,x ; decrement enemy animation frame delay counter
beq @disable_and_dec_enemy_routine ; branch if animation delay has elapsed
dec ENEMY_ATTACK_DELAY,x ; decrement delay between attacks
bne @add_scroll_exit ; exit if attack delay hasn't elapsed
lda #$07 ; a = #$07 (recoil delay)
sta ENEMY_VAR_1,x ; store recoil timer
lda #$0b ; a = #$0b (mortar shot)
sta $0a ; set enemy type to #$0b (mortar shot)
ldy #$e8 ; y = #$e8 (mortar initial relative y position)
lda #$05 ; a = #$05 (mortar initial relative x position)
jsr generate_enemy_at_pos ; generate enemy type #$0b at relative position (#$05,#$e8)
@add_scroll_exit:
jmp add_scroll_to_enemy_pos ; add scrolling to enemy position
@disable_and_dec_enemy_routine:
jsr add_scroll_to_enemy_pos ; add scrolling to enemy position
lda #$c0 ; a = #$c0 (delay hidden in water)
sta ENEMY_ANIMATION_DELAY,x ; set enemy animation frame delay counter
jsr disable_enemy_collision ; prevent player enemy collision check and allow bullets to pass through enemy
lda #$02 ; a = #$02
jmp set_enemy_routine_to_a ; set enemy routine index to scuba_soldier_routine_01
; pointer table for mortar shot (#$9 * #$2 = #$12 bytes)
mortar_shot_routine_ptr_tbl:
.addr mortar_shot_routine_00 ; CPU address $f1d6 - set explosion sound, sprite, palette, and velocities
.addr mortar_shot_routine_01 ; CPU address $f237
.addr mortar_shot_routine_02 ; CPU address $f26e - mortar shot falling down, create 3 split mortar rounds
.addr enemy_routine_init_explosion ; CPU address $e74b - mortar collide with player (enemy destroyed routine)
.addr enemy_routine_explosion ; CPU address $e7b0
.addr enemy_routine_remove_enemy ; CPU address $e806
.addr mortar_shot_routine_03 ; CPU address $e752 - split mortar collide with ground routine, play explosion sound, update collision, hide sprite
.addr enemy_routine_explosion ; CPU address $e7b0
.addr enemy_routine_remove_enemy ; CPU address $e806
; set explosion sound, sprite, palette, and velocities
mortar_shot_routine_00:
lda #$8a ; a = #$8a (type of explosion for main shot)
; explosion_type_01, with explosion noise 1
ldy ENEMY_ATTRIBUTES,x ; load mortar shot enemy attributes
beq @continue ; branch if no attributes, using custom explosion
lda #$80 ; default 0 explosion
@continue:
sta ENEMY_STATE_WIDTH,x ; set explosion and specify player bullets travel through enemy
lda #$20 ; a = #$20 (sprite_20) S bullet, mortar
sta ENEMY_SPRITES,x ; write enemy sprite code to CPU buffer
lda #$06 ; a = #$06 (use palette #$02)
sta ENEMY_SPRITE_ATTR,x ; set palette
lda ENEMY_ATTRIBUTES,x ; load enemy attributes
bne @set_mortar_velocities ; branch if enemy attributes specified (hangar zone boss screen)
lda ENEMY_VAR_1,x ; load mortar_shot_velocity_tbl offset
beq @set_mortar_velocities ; skip offset if using default mortar shot
clc ; hangar zone boss screen specified initial aim direction,
; use ENEMY_VAR_1 to get velocities
; clear carry in preparation for addition
adc #$03 ; aimed enemy mortar shots start at offset 4
; e.g. ENEMY_VAR_1 would be $00,$fb,$c0,$ff
@set_mortar_velocities:
asl
asl ; quadruple since each entry is #$04 bytes
tay ; transfer to offset register
lda mortar_shot_velocity_tbl,y ; load mortar shot fractional y velocity
sta ENEMY_Y_VELOCITY_FRACT,x ; set mortar shot fractional y velocity
lda mortar_shot_velocity_tbl+1,y ; load mortar shot fast y velocity
sta ENEMY_Y_VELOCITY_FAST,x ; set mortar shot fast y velocity
lda mortar_shot_velocity_tbl+2,y ; load mortar shot fractional x velocity
sta ENEMY_X_VELOCITY_FRACT,x ; set mortar shot fractional x velocity
lda mortar_shot_velocity_tbl+3,y ; load mortar shot fast x velocity
sta ENEMY_X_VELOCITY_FAST,x ; set mortar shot fast x velocity
mortar_shot_adv_routine:
jmp advance_enemy_routine ; advance to next routine
; table for mortar velocities (#$20 bytes)
; byte 0 - mortar shot fractional y velocity
; byte 1 - mortar shot fast y velocity
; byte 2 - mortar shot fractional x velocity
; byte 3 - mortar shot fast x velocity
mortar_shot_velocity_tbl:
.byte $00,$fb,$00,$00 ; ( -5 , 0 ) default initial mortar shot (straight up fast)
.byte $00,$fe,$00,$00 ; ( -2 , 0 ) one of 3 split mortar shot (straight up slow)
.byte $40,$fe,$90,$00 ; ( -1.75 , 0.5625 ) right of 3 split mortar shot
.byte $40,$fe,$70,$ff ; ( -1.75 , -0.5625 ) left of 3 split mortar shot
; values for initial launch velocity on hangar zone
.byte $00,$fb,$c0,$ff ; ( -5 , -0.25 ) - ENEMY_VAR_1 - 1 (aim slight left)
.byte $00,$fb,$80,$ff ; ( -5 , -0.5 ) - ENEMY_VAR_1 - 2 (aim farther left)
.byte $00,$fb,$40,$ff ; ( -5 , -0.75 ) - ENEMY_VAR_1 - 3 (aim even farther left)
.byte $00,$fb,$00,$ff ; ( -5 , -1 ) - ENEMY_VAR_1 - 4 (aim farthest left)
; apply gravity, apply velocity, advance routine if reached apex of initial mortar shot
mortar_shot_routine_01:
jsr add_10_to_enemy_y_fract_vel ; add #$10 (gravity) to y fractional velocity (.06 faster)
jsr update_enemy_pos ; apply velocities and scrolling adjust
lda ENEMY_ATTRIBUTES,x ; load enemy attributes
bne @split_mortar ; branch if not initial mortar shot
lda ENEMY_Y_VELOCITY_FAST,x ; initial mortar shot, load y fast velocity
bpl mortar_shot_adv_routine ; advance to next routine if mortar falling down
lda ENEMY_Y_POS,x ; load enemy y position on screen
cmp #$30 ; height for mortar to divide
bcc mortar_shot_adv_routine ; branch if mortar shot has reached its apex to advance the routine
@mortar_shot_routine_01_exit:
rts
; split mortar shot, e.g. ENEMY_ATTRIBUTES > 0, check collision if necessary if so
@split_mortar:
lda ENEMY_Y_VELOCITY_FAST,x ; load fast y velocity
bmi @mortar_shot_routine_01_exit ; exit if y split mortar is still shooting up
jsr player_enemy_x_dist ; split mortar falling, a = closest x distance to enemy from players, y = closest player (#$00 or #$01)
lda ENEMY_Y_POS,x ; load enemy y position on screen
cmp SPRITE_Y_POS,y ; compare closest player by x distance's y position to current enemy y position
bcc @mortar_shot_routine_01_exit ; exit if mortar shot y position is higher than closest player is
jsr init_vars_get_enemy_bg_collision ; initialize required memory and call get_enemy_bg_collision to determine bg collision
beq @mortar_shot_routine_01_exit ; exit if no bg collision
lda #$24 ; bg collision, a = #$24 (sound_24)
jsr play_sound ; play explosion sound
lda #$07 ; a = #$07
jmp set_enemy_routine_to_a ; set enemy routine index to mortar_shot_routine_03
; mortar shot falling down, create 3 split mortar rounds
mortar_shot_routine_02:
jsr update_enemy_pos ; apply velocities and scrolling adjust
lda #$03 ; a = #$03 (number of projectiles generated)
sta $08 ; store enemy attributes for mortars to create
txa ; transfer enemy slot offset to a
tay ; transfer enemy slot offset to y
@generate_mortar_shot:
jsr find_next_enemy_slot ; find next available enemy slot, put result in x register
bne @advance_enemy_routine ; branch if no enemy slot was found
lda #$0b ; a = #$0b (#$0b = mortar)
sta ENEMY_TYPE,x ; set current enemy type to mortar
jsr initialize_enemy ; initialize enemy attributes
lda ENEMY_X_POS,y ; load created enemy x position on screen
sta ENEMY_X_POS,x ; set current mortar x position on screen to match
lda ENEMY_Y_POS,y ; load created enemy y position on screen
sta ENEMY_Y_POS,x ; set current mortar y position on screen to match
lda $08 ; load enemy attributes
sta ENEMY_ATTRIBUTES,x ; load appropriate enemy attribute (mortar velocities)
; (see mortar_shot_velocity_tbl starting at 3rd entry)
dec $08 ; decrement mortar shot creation count
bne @generate_mortar_shot ; if haven't created 3 mortar shots loop to create next one
@advance_enemy_routine:
ldx ENEMY_CURRENT_SLOT ; restore enemy slot
jmp advance_enemy_routine ; advance to next routine
; determines firing direction based on enemy position ($08, $09) and player position ($0b, $0a)
; and creates bullet if appropriate
; input
; * a - bullet type
; * y - bullet speed code
; * $08 - enemy x position
; * $09 - enemy y position
; * $0b - player x position
; * $0a - player y position
aim_and_create_enemy_bullet:
sty $06 ; store bullet speed code in $06
sta $00 ; store bullet type temporarily
lda #$01 ; a = #$01, use quadrant_aim_dir_01
sta $0f ; quadrant_aim_dir_lookup_tbl offset (quadrant_aim_dir_01)
lda $0a ; load player y position
bpl @continue ; branch if >= #$00
lda $0c ; load player y position
sta $0a ; set target y position
jsr get_quadrant_aim_dir ; get aim direction code for target ($0b, $0a) from location ($08, $09) using table code $0f
jmp @create_bullet_if_appropriate
@continue:
jsr get_quadrant_aim_dir_for_player ; set a to the aim direction within a quadrant
; based on source position ($09, $08) targeting player index $0a
@create_bullet_if_appropriate:
ora $00 ; merge enemy bullet quadrant aim dir with bullet type code
sta $0a ; store bullet type and bullet velocity in $0a
jmp create_enemy_bullet_if_attack_enabled ; create enemy bullet (type $0a) at ($09, $08) in quadrant $07 and speed $06
; if ENEMY_ATTACK_FLAG is set or if level 1 boss cannonball
; create enemy bullet (ENEMY_TYPE #$02) of type a (and angle) with speed y at ($09, $08)
; input
; * a = bullet type and angle
; * y = bullet speed (enemy attributes)
; * $08 = y position
; * $09 = x position
bullet_generation:
asl
; creates a bullet if attack enabled with speed y, bullet type a, angle a at position ($09, $08)
; input
; * y - enemy bullet speed
; * a - (xxx. ....) specifies enemy bullet type:
; * #$00 - regular bullet
; * #$01 - level 1 boss large cannonball
; * #$02 - indoor large cannonball (boss screen)
; * #$03 - indoor regular bullet
; * #$04 - level 3 dragon boss fire ball (dragon arm orb projectile)
; * a - (...x xxxx) specifies bullet angle value (see bullet_fract_vel_dir_lookup_tbl)
; [#$00-#$17] is pointing right as value increments direction goes clockwise
; * $08 - y position
; * $09 - x position
; output
; zero flag - set when bullet created, clear when unable to create
create_enemy_bullet_angle_a:
sty $06 ; store enemy bullet speed
sta $0a ; store bullet type and angle: regular, cannonball, indoor bullet, etc. (see create_enemy_bullet)
and #$1f ; keep bits ...x xxxx (keep angle value)
ldy #$00 ; y = #$00
cmp #$07 ; compare to left of straight down
bcc @continue ; branch if creating bullet that is firing right or down and to the right
cmp #$12 ; compare to firing straight up
bcs @continue ; branch if firing up and to the right
ldy #$02 ; firing left, set y to #$02
@continue:
cmp #$0d ; compare to firing straight left
bcc @set_dir_create_bullet ; branch if firing down (between 3 o'clock and 9 o'clock)
iny ; #$01 or #$03
@set_dir_create_bullet:
sty $07 ; set aim quadrant
; create enemy bullet (type $0a) at ($09, $08) in quadrant $07 with quadrant aim dir in $0a and speed $06
; if ENEMY_ATTACK_FLAG is set or if level 1 boss cannonball
; input
; * $08 - y position
; * $09 - x position
; * $0a - (xxx. ....) specifies enemy bullet type:
; * #$00 - regular bullet
; * #$01 - level 1 boss large cannonball
; * #$02 - indoor large cannonball (boss screen)
; * #$03 - indoor regular bullet
; * #$04 - level 3 dragon boss fire ball (dragon arm orb projectile)
; * $0a - (...x xxxx) specifies bullet quadrant aim value (see bullet_fract_vel_dir_lookup_tbl)
; [#$00-#$17] is pointing right as value increments direction goes clockwise
; * $06 - enemy bullet speed code (see bullet_velocity_adjust_ptr_tbl)
; * $07 - specifies quadrant to aim in (0 = quadrant IV, 1 = quadrant I, 2 = quadrant III, 3 = quadrant II)
; * bit 0 - 0 = bottom half of plane (quadrants III and IV), 1 = top half of plane (quadrants I and II)
; * bit 1 - 0 = right half of the plan (quadrants I and IV), 1 = left half of plane (quadrants II and III)
create_enemy_bullet_if_attack_enabled:
lda $0a ; load bullet type and bullet angle
and #$e0 ; keep bits xxx. .... (keep type value)
cmp #$20 ; compare to level 1 large cannonball (boss screen) bullet type
beq create_enemy_bullet ; always create bullet if bullet type #$01 (level 1 boss large cannonball)
lda ENEMY_ATTACK_FLAG ; see if enemies should attack
beq bullet_gen_exit ; don't shoot/create enemy bullet if ENEMY_ATTACK_FLAG is set
; create enemy bullet
; input
; * $08 - y position
; * $09 - x position
; * $0a - (xxx. ....) specifies enemy bullet type:
; * #$00 - regular bullet
; * #$01 - level 1 boss large cannonball
; * #$02 - indoor large cannonball (boss screen)
; * #$03 - indoor regular bullet
; * #$04 - level 3 dragon boss fire ball (dragon arm orb projectile)
; * $0a - (...x xxxx) specifies bullet angle value (see bullet_fract_vel_dir_lookup_tbl)
; * $06 - enemy bullet speed code (see bullet_velocity_adjust_ptr_tbl)
; * $07 - specifies quadrant to aim in (0 = quadrant IV, 1 = quadrant I, 2 = quadrant III, 3 = quadrant II)
; * bit 0 - 0 = bottom half of plane (quadrants III and IV), 1 = top half of plane (quadrants I and II)
; * bit 1 - 0 = right half of the plan (quadrants I and IV), 1 = left half of plane (quadrants II and III)
; output
; zero flag - set when bullet created, clear when unable to create
create_enemy_bullet:
jsr find_next_enemy_slot ; find next available enemy slot for bullet, put result in x register
bne bullet_gen_exit ; branch if no enemy slot was found
lda #$01 ; a = #$01 (bullet)
sta ENEMY_TYPE,x ; set current enemy type to bullet
jsr initialize_enemy ; initialize enemy attributes
lda $0a ; load enemy bullet type
lsr
lsr
lsr
lsr
lsr
sta ENEMY_VAR_1,x ; store enemy bullet type in ENEMY_VAR_1
lda $06 ; load enemy bullet speed code (see bullet_velocity_adjust_ptr_tbl)
cmp #$07 ; see if speed code is >= #$07
bcc @continue ; continue if not too high
lda #$07 ; can't have more than speed code #$07, set to #$07
@continue:
sta $06 ; store speed code in $06
lda $08 ; load enemy bullet y position
sta ENEMY_Y_POS,x ; store enemy bullet y position on screen
lda $09 ; load enemy bullet x position
sta ENEMY_X_POS,x ; store enemy bullet x position on screen
lda $0a
and #$1f ; keep bits ...x xxxx (quadrant aim dir)
; sets the bullet/projectile X and Y velocities (both high and low) based on register a and $07
; used by bullets, eye projectile, and spinning bubbles
; input
; * a - bullet angle value (bullet_fract_vel_dir_lookup_tbl offset)
; * $07 - specifies quadrant to aim in (0 = quadrant IV, 1 = quadrant I, 2 = quadrant III, 3 = quadrant II)
; * bit 0 - 0 = bottom half of plane (quadrants III and IV), 1 = top half of plane (quadrants I and II)
; * bit 1 - 0 = right half of the plan (quadrants I and IV), 1 = left half of plane (quadrants II and III)
; output
; * a - #$00
set_bullet_velocities:
jsr calc_bullet_velocities ; determine the bullet velocities based on quadrant aim dir (a) and quadrant ($07)
lda $05 ; load enemy bullet y velocity fast
sta ENEMY_Y_VELOCITY_FAST,x ; set enemy bullet y velocity fast
lda $04 ; load enemy bullet y fractional velocity
sta ENEMY_Y_VELOCITY_FRACT,x ; set enemy bullet y fractional velocity
lda $0b ; load enemy bullet x velocity fast
sta ENEMY_X_VELOCITY_FAST,x ; set enemy bullet x velocity fast
lda $0a ; load enemy bullet x fractional velocity
sta ENEMY_X_VELOCITY_FRACT,x ; set enemy bullet x fractional velocity
ldx ENEMY_CURRENT_SLOT ; load enemy current slot (doesn't seem necessary)
lda #$00 ; clear a
rts
; no enemy slot available
bullet_gen_exit:
ldx ENEMY_CURRENT_SLOT
lda #$01 ; a = #$01
rts
; determine the bullet velocities based on quadrant aim dir (a) and quadrant ($07)
; input
; * a - quadrant aim dir (see bullet_fract_vel_dir_lookup_tbl)
; * $07 - specifies quadrant to aim in (0 = quadrant IV, 1 = quadrant I, 2 = quadrant III, 3 = quadrant II)
; * bit 0 - 0 = bottom half of plane (quadrants III and IV), 1 = top half of plane (quadrants I and II)
; * bit 1 - 0 = right half of the plan (quadrants I and IV), 1 = left half of plane (quadrants II and III)
; output
; * $04 - bullet y fractional velocity value
; * $05 - bullet y velocity fast value
; * $0a - bullet x fractional velocity value
; * $0b - bullet x velocity fast value
calc_bullet_velocities:
tay ; store quadrant aim direction in y
lda bullet_fract_vel_dir_lookup_tbl,y
tay
lda bullet_fract_vel_tbl+1,y ; load the bullet fractional x velocity
sta $04 ; store bullet x fractional velocity byte
lda #$00 ; set x velocity fast value to #$00
sta $05 ; store bullet x velocity fast value
jsr adjust_bullet_velocity ; adjust bullet x velocity based on speed code ($06)
lda $04 ; load bullet x fractional velocity byte
sta $0a ; set bullet x fractional velocity byte
lda $05 ; load bullet x velocity fast value
sta $0b ; set bullet x velocity fast value
lda bullet_fract_vel_tbl,y ; load bullet y fractional velocity
sta $04 ; set bullet y fractional velocity
lda #$00 ; load bullet y fast velocity
sta $05 ; set bullet y fast velocity
jsr adjust_bullet_velocity ; adjust bullet y velocity based on speed code ($06)
lda $07 ; load bullet direction
lsr ; puts bit 0 to carry (up down bit)
bcc @set_x_vel ; branch if firing down
lda #$00 ; bullet firing up, flip y velocity so bullet travels up instead of down
sec ; set carry flag in preparation for subtraction
sbc $04 ; #$00 - $04
sta $04 ; update y fractional velocity to be negative
lda #$00 ; a = #$00
sbc $05 ; #$00 - $05
sta $05 ; update y velocity fast to be negative
@set_x_vel:
lda $07 ; load bullet direction
lsr
lsr
bcc @exit ; exit if firing right
lda #$00 ; firing left, a = #$00
sec ; set carry flag in preparation for subtraction
sbc $0a ; negate bullet x fractional velocity
sta $0a ; set bullet x fractional velocity value
lda #$00 ; a = #$00
sbc $0b ; negate bullet x fast velocity
sta $0b ; set bullet x velocity fast value
@exit:
rts
; table of enemy bullet fractional velocity indexes based on quadrant aim direction (#$18 bytes)
; offsets into bullet_fract_vel_tbl
; [#$00-#$17] #$00 is pointing right as value increments direction goes clockwise
; #$00 right, #$03 is down, #$06 left, #$09 is straight up
bullet_fract_vel_dir_lookup_tbl:
.byte $00,$02,$04,$06,$08,$0a ; quadrant IV
.byte $0c,$0a,$08,$06,$04,$02 ; quadrant III
.byte $00,$02,$04,$06,$08,$0a ; quadrant II
.byte $0c,$0a,$08,$06,$04,$02 ; quadrant I
; table for bullet x and y fractional velocities, based on index specified from bullet_fract_vel_dir_lookup_tbl (#$d bytes)
; byte 0 - y fractional velocity
; byte 1 - x fractional velocity
bullet_fract_vel_tbl:
.byte $00,$ff ; x velocity
.byte $42,$f7
.byte $80,$dd
.byte $b5,$b5
.byte $dd,$80
.byte $f7,$42
.byte $ff,$00 ; shooting horizontally
; adjusts bullet x or y velocity based on speed code (#$0-#$07)
; e.g. bullet speed code #$01 is .75 speed
; assumes fast velocity will always be #$00, otherwise, math won't work in all cases
; input
; * $04 - bullet fractional velocity value (either x dir or y dir)
; * $05 - bullet velocity fast value (either x dir or y dir)
; * $06 - bullet speed code
; output
; * $04 - bullet fractional velocity value (either x dir or y dir)
; * $05 - bullet velocity fast value (either x dir or y dir)
adjust_bullet_velocity:
lda $06 ; bullet speed (0-7)
and #$07 ; keep bits .... .xxx
jsr run_routine_from_tbl_below ; run routine a in the following table (bullet_velocity_adjust_ptr_tbl)
; pointer table for bullet speeds (#$9 * #$2 = #$12 bytes)
bullet_velocity_adjust_ptr_tbl:
.addr bullet_velocity_adjust_00 ; CPU address $f3be (.5x speed)
.addr bullet_velocity_adjust_01 ; CPU address $f3c3 (.75x speed)
.addr bullet_velocity_adjust_02 ; CPU address $f3e4 (normal speed)
.addr bullet_velocity_adjust_03 ; CPU address $f3ca (1.25x speed)
.addr bullet_velocity_adjust_04 ; CPU address $f3d3 (1.5x speed)
.addr bullet_velocity_adjust_05 ; CPU address $f3e5 (1.62x speed)
.addr bullet_velocity_adjust_06 ; CPU address $f3f0 (1.75x speed)
.addr bullet_velocity_adjust_07 ; CPU address $f3ff (1.87x speed)
.addr bullet_velocity_adjust_08 ; CPU address $f415 (2x speed) impossible? !(HUH)
; bullet speed 0 (.5x speed)
; halves fast and slow velocity, e.g. #$03 #80 (3.5) becomes #$01 #$c0 (1.75)
bullet_velocity_adjust_00:
lsr $05 ; half fast velocity
ror $04 ; half fractional value, including carry from fast velocity
rts
; bullet speed 1 (.75x speed)
; first half value, then half that again to add to the originally halved value
bullet_velocity_adjust_01:
lsr $05 ; half fast velocity
ror $04 ; half fractional value, including carry from fast velocity
jmp bullet_velocity_adjust_04
; bullet speed 3 (1.25x speed)
; halves fast and fractional velocity, halves fractional again and adds it to original velocity
bullet_velocity_adjust_03:
lda $05 ; load fast velocity
lsr ; half fast velocity
lda $04 ; load fractional velocity
ror ; half fractional velocity, including carry from fast velocity
lsr ; half fractional velocity again
bpl bullet_velocity_adjust_add_a ; add .25 * to original velocity
; bullet speed 4 (1.5x speed)
bullet_velocity_adjust_04:
lda $05 ; load fast velocity
lsr ; half fast velocity
lda $04 ; load original fractional velocity
ror ; half fractional velocity, including carry from fast velocity
bullet_velocity_adjust_add_a:
clc ; clear carry in preparation for addition
adc $04 ; add to original value of $04
sta $04 ; store value back in $04
lda $05 ; re-load $05
adc #$00 ; add any carry
sta $05 ; update $05
; bullet speed 2 (normal speed)
bullet_velocity_adjust_02:
rts
; bullet speed 5 (1.62x speed)
bullet_velocity_adjust_05:
lda $05
lsr
lda $04
ror
sta $00
lsr
bpl bullet_dir_half_a_add_to_vel
; bullet speed 6 (1.75x speed) (for any value less than 1.14)
; doesn't work correctly when carry from fractional velocity, e.g. 1.5 becomes 1.62 and not 2.62
; however, 1.1 (#$01 #1a) correctly goes to (#$01 #$ed) (1.92)
bullet_velocity_adjust_06:
lda $05
lsr
lda $04
ror
sta $00
bullet_dir_half_a_add_to_vel:
lsr
clc ; clear carry in preparation for addition
adc $00
jmp bullet_velocity_adjust_add_a
; bullet speed 7 (1.87x speed)
; doesn't work correctly when carry from fractional velocity, e.g. 1.5 becomes 1.81 and not 2.81
; however, 1.05 (#$01 #0d) correctly goes to (#$01 #$f7) (1.96)
bullet_velocity_adjust_07:
lda $05
lsr
lda $04
ror
sta $00
lsr
sta $01
clc ; clear carry in preparation for addition
adc $00
lsr $01
clc ; clear carry in preparation for addition
adc $01
jmp bullet_velocity_adjust_add_a
; bullet speed 8 (2x speed) (impossible ?)
bullet_velocity_adjust_08:
asl $04 ; double fast velocity
rol $05
rts
; either increments or decrements ENEMY_VAR_1 by 1 to aim towards the player using quadrant_aim_dir_01
; used by spinning bubbles (enemy type = #$1d), tank (enemy type = #$12), and white blob (enemy type = #$13)
; input
; * $0a - player index
; output
; * carry flag - set when enemy already aiming at player, clear when rotation happened
; * minus flag
; * zero flag - clear when clockwise direction, set when counterclockwise
aim_var_1_for_quadrant_aim_dir_01:
jsr get_rotate_01 ; get enemy aim direction and rotation direction using quadrant_aim_dir_01
jmp rotate_enemy_var_1 ; rotate the enemy's aim by one in a clockwise or counterclockwise direction if needed
; either increments or decrements ENEMY_VAR_1 by 1 to aim towards the player using quadrant_aim_dir_00
; used by rotating gun (enemy type = #$04) and alien fetus (enemy type = #$11)
; output
; * ENEMY_VAR_1,x - enemy aim direction [#$00-#$0b] #$00 when facing right incrementing clockwise
; * carry flag - set when enemy already aiming at player, clear when rotation happened
aim_var_1_for_quadrant_aim_dir_00:
jsr get_rotate_00 ; get enemy aim direction and rotation direction using quadrant_aim_dir_00
; rotate the enemy's aim by one in a clockwise or counterclockwise direction
; input
; * minus flag - set when enemy is already aiming at player and no rotation is required
; * carry flag - set when enemy already aiming at player, clear when rotation happened
; * zero flag - clear when clockwise direction, set when counterclockwise
rotate_enemy_var_1:
bmi @set_carry_exit ; exit if enemy is already aiming at the player
bne @rotate_1_counterclockwise ; if a = #$01, then a counterclockwise rotation
inc ENEMY_VAR_1,x ; move enemy aim direction clockwise
lda ENEMY_VAR_1,x ; load enemy aim direction
cmp $06 ; compare maximum supported enemy aim dir, e.g. rotating gun is #$0b
bcc @continue ; continue if not past last position
lda #$00 ; wrapped around, set to first aim direction #$00 (horizontal left)
beq @set_var_1_continue
; rotate counter-clockwise
@rotate_1_counterclockwise:
dec ENEMY_VAR_1,x ; update enemy aim direction
; moves direction counter clockwise
bpl @continue ; branch if counter-clockwise doesn't cause underflow
lda $06 ; load maximum number of the supported enemy aim dir, e.g. rotating gun is #$0c (left and slightly down)
sec ; set carry flag in preparation for subtraction
sbc #$01 ; subtract one
@set_var_1_continue:
sta ENEMY_VAR_1,x ; update enemy aim direction
@continue:
lda ENEMY_VAR_1,x ; load enemy aim direction
cmp $0c ; compare to new enemy position as determined by get_rotate_00
beq @set_carry_exit ; set carry and exit
clc ; not yet at desired direction, clear carry and exit
rts
; rotating gun is at desired position, set carry and exit
@set_carry_exit:
sec ; set carry flag
rts
; determines which direction to rotate based on quadrant_aim_dir_00
; targetting player index ($0a)
; input
; * $0a - player index to target, 0 = player 1, 1 = player 2
; * $08 - source y position
; * $09 - source x position
; output
; * negative flag - set when enemy is already aiming at player and no rotation is needed
; * a - rotation direction, #$00 clockwise, #$01 counterclockwise, #$80 no rotation needed
; * $0c - new enemy aim direction
get_rotate_00:
lda #$00 ; a = #$00 (use quadrant_aim_dir_00)
beq get_rotate_dir_for_index ; always jump, get enemy aim direction and rotation direction using quadrant_aim_dir_00
; determines which direction to rotate based on quadrant_aim_dir_01
; targetting player index ($0a)
; input
; * $0a - player index to target, 0 = player 1, 1 = player 2
; * $08 - source y position
; * $09 - source x position
; output
; * negative flag - set when enemy is already aiming at player and no rotation is needed
; * a - rotation direction, #$00 clockwise, #$01 counterclockwise, #$80 no rotation needed
; * $0c - new enemy aim direction
get_rotate_01:
lda #$01 ; a = #$01 (use quadrant_aim_dir_01)
; determines which direction to rotate based on quadrant_aim_dir_lookup_tbl index offset (a)
; targetting player index ($0a)
; input
; * a - quadrant_aim_dir_lookup_tbl offset table
; * $0a - player index to target, 0 = player 1, 1 = player 2
; * $08 - source y position
; * $09 - source x position
; output
; * negative flag - set when enemy is already aiming at player and no rotation is needed
; * a - rotation direction, #$00 clockwise, #$01 counterclockwise, #$80 no rotation needed
; * $0c - new enemy aim direction
get_rotate_dir_for_index:
sta $0f ; set quadrant_aim_dir_lookup_tbl index offset
lda $0a ; load player index
bpl @get_quadrant_aim_dir ; branch if closest player has been determined
lda $0c ; no player to target, not sure when this happens (see player_enemy_x_dist)
; even when both player states are not normal, still targets player 1
sta $0a ; set $0c as player index
jsr get_quadrant_aim_dir ; get aim direction code for target ($0b, $0a) from location ($08, $09) using table code $0f
; depending on quadrant_aim_dir_xx, dir code will be [#$00-#$03], [#$00-#$06], or [#$00-#$0f]
jmp get_rotate_dir ; determine which direction to rotate
; based on a (quadrant aim dir) and quadrant ($07)
@get_quadrant_aim_dir:
jsr get_quadrant_aim_dir_for_player ; set a to the aim direction within a quadrant
; based on source position ($09, $08) targeting player index $0a
; determines which direction to rotate
; based on a (quadrant aim dir) and quadrant ($07)
; input
; * a - quadrant aim direction (quadrant_aim_dir_xx value)
; * $07 - specifies quadrant to aim in (0 = quadrant IV, 1 = quadrant I, 2 = quadrant III, 3 = quadrant II)
; * bit 0 - 0 = bottom half of plane (quadrants III and IV), 1 = top half of plane (quadrants I and II)
; * bit 1 - 0 = right half of the plan (quadrants I and IV), 1 = left half of plane (quadrants II and III)
; * $0f - quadrant_aim_dir_lookup_tbl offset
; output
; * negative flag - set when enemy is already aiming at player and no rotation is needed
; * a - rotation direction, #$00 clockwise, #$01 counterclockwise, #$80 no rotation needed
; * $0c - new enemy aim direction
get_rotate_dir:
sta $0c ; store quadrant aim direction code in $0c
lda $0f ; load quadrant_aim_dir_lookup_tbl offset (which quadrant_aim_dir_xx to use)
lsr ; move bit 0 to the carry
lda #$06 ; using either quadrant_aim_dir_00, or quadrant_aim_dir_02
; midway direction, i.e. 9 o'clock
ldy #$0c ; maximum aim direction (same as #$00 aim dir), i.e. 3 o'clock
bcc @continue ; branch if aim type quadrant_aim_dir_00 or quadrant_aim_dir_02
lda #$0c ; using quadrant_aim_dir_01
; midway direction, i.e. 9 o'clock
ldy #$18 ; maximum aim direction (same as #$00 aim dir), i.e. 3 o'clock
@continue:
sta $05 ; store either #$06 or #$0c into $06, which is the midway aim direction, i.e. 9 o'clock
sty $06 ; store either #$0c or #$18 into $06, which is the maximum aim direction, i.e. 3 o'clock
lda $07 ; load player position relative to enemy (left/right and above/below)
and #$02 ; keep bit 1 (set when player to the left)
beq @check_player_pos ; branch if player to the right of the enemy (#$00 or #$01)
lda $05 ; player to left of enemy enemy, load mid way direction (either #$06 or #$0c)
sec ; set carry flag in preparation for subtraction
sbc $0c ; subtract quadrant aim direction code from from midway direction point ($05 - $0c)
sta $0c ; store result back into $0c
@check_player_pos:
lsr $07 ; shift right the position relative to enemy (left/right and above/below)
bcc @calc_reflected_dir ; branch if player below enemy
lda $06 ; player is above enemy, need to reflect aim dir ($0c) across x-axis
; load max direction value (#$0c or #$18)
sec ; set carry flag in preparation for subtraction
sbc $0c ; subtract either the quadrant aim direction code directly from max aim direction (when player to right)
; or subtract the offset amount from half-way (when player to left) from max aim direction (reflect across x-axis)
; ($06 - $0c)
cmp $06 ; compare to max direction value
bcc @continue2 ; branch if aim direction wasn't #$00
lda #$00 ; direction result was #$00, set value to #$00 (right 3 o'clock)
@continue2:
sta $0c ; set new aim direction
; calculates the aim direction reflected along the vertical axis
; e.g. if original aim direction #$00 is right and increment clockwise
; reflected aim direction #$00 is left increment clockwise
@calc_reflected_dir:
lda #$00 ; a = #$00
sta $0e ; initialize value of $0e to #$00
lda ENEMY_VAR_1,x ; load current enemy aim direction (#$00 facing right)
clc ; clear carry in preparation for addition
adc $05 ; add either #$06 or #$0c to current enemy aim dir (halfway point)
cmp $06 ; see if current enemy aim direction is in first half or second half of aim directions
; used for calculating 'reflected' aim direction
bcc @determine_rotation ; branch if no wraparound, i.e. first half of aim directions
inc $0e ; current aim direction is greater than half way point
; set $0e to #$01
sbc $06 ; subtract max aim direction value to get correct reflected dir (modulus)
; check to see if need to rotate, and if so, which direction
; all examples assume quadrant_aim_dir_00 or quadrant_aim_dir_02
; $0c - new aim direction. Example: right is #$00, increment clockwise
; $0d - reflected new aim direction. Example: left is #$00, increment clockwise
; $0e - whether or not new direction has gone through the maximum direction, e.g. old value #$0a, new value #$03
; ENEMY_VAR_1 - previous enemy aim direction - (right is #$00, increment clockwise)
@determine_rotation:
sta $0d ; store 'reflected' aim direction in $0d
lda $0c ; load new enemy aim direction
cmp ENEMY_VAR_1,x ; compare to current enemy aim dir (#$00 is right)
beq @no_dir_change ; if the calculated enemy aim dir is the same, no need to move, set minus flag and exit
ldy $0e ; need to rotate to new enemy aim dir, load prefered direction
bne @wrapped_rotation_check_dir ; branch if new direction caused a 'wrap around'
bcc @rotate_counterclockwise ; no wrap occurred and new direction is less than current, rotate counterclockwise
cmp $0d ; compare 'reflected' dir to current enemy aim dir, to find shortest direction
bcs @rotate_counterclockwise ; rotate counterclockwise if reflected enemy aim dir is > new aim dir
; e.g. no wrap occurred, $0c is #$09 (up) (this means $0d is #$03)
; and suppose ENEMY_VAR_1 is #$0a (up-right), rotate counter clockwise
; clockwise rotation
@rotate_clockwise:
lda #$00 ; a = #$00
beq @exit
; when determining the new aim direction for the enemy, the direction 'wrapped around' the max dir, e.g. #$0b
; so while the enemy aim direction may have gotten smaller, it doesn't necessarily imply a counterclockwise rotation
@wrapped_rotation_check_dir:
bcs @rotate_clockwise ; rotate clockwise if new aim direction is greater than old aim direction
cmp $0d ; compare 'reflected' dir to current enemy aim dir, to find shortest direction
bcc @rotate_clockwise ; rotate clockwise if reflected enemy aim dir is < new aim dir
; e.g. wrap occurred, $0c is #$03 (down) (this means $0d is #$09)
; and suppose ENEMY_VAR_1 is #$0a (up-right), rotate clockwise
@rotate_counterclockwise:
lda #$01 ; a = #$01
@exit:
rts
@no_dir_change:
lda #$80 ; a = #$80
bne @exit ; always exit
; determines whether dragon arm orb should move, and if so, in which direction
; dragon_arm_orb_routine_03 - related to dragon arm orb seeking/following the players (ENEMY_FRAME #$04)
; input
; * x - current enemy slot index for for the dragon arm orb
; output
; * minus flag - set when orb doesn't need to move, clear otherwise
; * a - #$00, #$01 or #$80
dragon_arm_orb_seek_should_move:
jsr set_08_09_to_enemy_pos ; set $08 and $09 to enemy x's X and Y position
lda #$02 ; dragon arm orb is only enemy that uses quadrant_aim_dir_02
sta $0f ; set quadrant_aim_dir_lookup_tbl offset to use quadrant_aim_dir_02
jsr get_quadrant_aim_dir_for_player ; set a to the aim direction within a quadrant
; based on source position ($09, $08) targeting player index $0a
sta $0c ; store enemy aim direction in $0c
ldy ENEMY_VAR_3,x ; load next dragon arm orb (farther from body) enemy index
lda $07 ; load player position relative to enemy
lsr
lsr
bcc @check_player_vert_pos ; branch if player on right side enemy (and below)
lda #$20 ; player to the left, load a = #$20
sec ; set carry flag in preparation for subtraction
sbc $0c ; subtract enemy aim direction from #$20 (#$20 - $0c)
sta $0c ; update enemy aim direction to now point
@check_player_vert_pos:
lsr $07 ; move bit 0 to carry flag
bcc @player_below_enemy ; branch if player below enemy
lda #$40 ; player is above enemy, set a = #$40
; this doesn't seem possible for dragon arm orb enemy
sec ; set carry flag in preparation for subtraction
sbc $0c ; subtract enemy aim direction from #$40 (#$40 - $0c)
and #$3f ; keep bits ..xx xxxx
sta $0c ; update enemy aim direction
@player_below_enemy:
lda #$00 ; a = #$00
sta $0e
lda ENEMY_X_VELOCITY_FAST,y ; load next orb's enemy position index (see dragon_arm_orb_pos_tbl)
clc ; clear carry in preparation for addition
adc #$20 ; add #$20 to next orb's position index (2 rows)
cmp #$40 ; see if position index is on the last row
bcc @b3 ; branch if not on last row of dragon_arm_orb_pos_tbl
inc $0e ; on last row of dragon_arm_orb_pos_tbl, increment $0e
sbc #$40 ; set index into 0th row of dragon_arm_orb_pos_tbl
@b3:
sta $0d ; store adjusted enemy position index in $0d
lda $0c ; load enemy aim direction
cmp ENEMY_X_VELOCITY_FAST,y ; compare enemy aim direction to next dragon arm orb's fast x velocity
beq @set_negative_exit
ldy $0e ; load whether or not the position index was on the last row
bne @continue
bcc @clear_zero_exit
cmp $0d
bcs @clear_zero_exit
@loop:
lda #$00 ; a = #$00
beq @exit ; always exit with zero flag set
@continue:
bcs @loop
cmp $0d
bcc @loop
; exit with the zero flag clear
@clear_zero_exit:
lda #$01 ; a = #$01
@exit:
rts
; set negative flag, clear zero flag, exit
@set_negative_exit:
lda #$80 ; a = #$80
bne @exit ; always branch
; determines the aim direction within a quadrant based on source position ($09, $08) targeting player index $0a
; input
; * $0f - quadrant_aim_dir_lookup_tbl offset [#$00-#$02]
; * $0a - player index of player to target (#$00 for p1 or #$01 for p2)
; * $08 - source y position
; * $09 - source x position
; output
; * a - player aim direction (for most things this is an offset into bullet_fract_vel_dir_lookup_tbl)
; when called for dragon boss arm orbs, it is a reference to dragon_arm_orb_pos_tbl)
; * $07 - player position relative to enemy (left/right and above/below)
; * #$00 = player below enemy (or equal) and to the right
; * #$01 = player above enemy and to the right
; * #$02 = player to left of enemy and player below enemy (or equal)
; * #$03 = player to left of enemy and player above enemy
get_quadrant_aim_dir_for_player:
lda $0a ; load the player player index
and #$01 ; should only be #$00 or #$01 (p1 or p2)
tay ; transfer to y
lda PLAYER_STATE,y ; load the closest player's PLAYER_STATE
cmp #$01 ; see if normal state
beq @get_y_pos ; branch if normal state
tya ; not normal state, either falling, dead or can't move
eor #$01 ; flip to other player
tay
lda PLAYER_STATE,y ; load other player's PLAYER_STATE
cmp #$01 ; see if normal state
beq @get_y_pos ; branch if normal state
lda #$ff ; other player also not in normal state
sta $0a ; set player y position to #$ff (bottom of screen)
lda #$80 ; a = #$80
sta $0b ; set player x position to #$80 (center of screen)
bne get_quadrant_aim_dir ; always branch, get aim direction code for target ($0b, $0a) from location ($08, $09) using table code $0f
@get_y_pos:
lda LEVEL_LOCATION_TYPE ; 0 = outdoor; 1 = indoor
lsr
lda #$b0 ; player y position is set at #$a8 on indoor levels
; note this means that enemies won't aim correctly at a player who is jumping on an indoor level
bcs @get_x_pos ; branch for indoor level
lda SPRITE_Y_POS,y ; outdoor level, load y position from memory
@get_x_pos:
sta $0a ; store player Y location in $0a
lda SPRITE_X_POS,y ; load x position from memory
sta $0b ; store player X position in $0b
; determines the aim direction within a quadrant based on source position ($09, $08) targeting player location ($0b, $0a)
; input
; * $08 - source y position
; * $09 - source x position
; * $0a - closest player y position
; * $0b - closest player x position
; * $0f - which of the #$03 tables from quadrant_aim_dir_lookup_tbl to use
; output
; * a - quadrant aim direction (quadrant_aim_dir_xx value)
; * $07 - specifies quadrant to aim in (0 = quadrant IV, 1 = quadrant I, 2 = quadrant III, 3 = quadrant II)
; * bit 0 - 0 = bottom half of plane (quadrants III and IV), 1 = top half of plane (quadrants I and II)
; * bit 1 - 0 = right half of the plan (quadrants I and IV), 1 = left half of plane (quadrants II and III)
get_quadrant_aim_dir:
ldy #$00 ; default assume player is to the right and equal to or below enemy
lda $0a ; load closest player y position
sec ; set carry flag in preparation for subtraction
sbc $08 ; subract enemy y position from player y position
bcs @shift_get_x_diff ; branch if no overflow occurred (enemy above player or same vertical position)
eor #$ff ; enemy below player, handle overflow, flip all bits and add one
adc #$01
iny ; y used to keep track of where player is in relation to enemy, e.g. $07
; mark that enemy was below player
@shift_get_x_diff:
lsr ; shift the difference between player and enemy y difference 5 bits
lsr ; (every #$20 pixels difference is a new horizontal direction)
lsr
lsr
lsr
sta $0a ; store result in $0a (row offset for quadrant_aim_dir_xx)
lda $0b ; load player x position
sec ; set carry flag in preparation for subtraction
sbc $09 ; subtract enemy x position from player x position
bcs @continue ; branch if no overflow (player to right of enemy)
eor #$ff ; enemy to left of player, handle overflow, flip all bits and add one
adc #$01
iny ; player to left of enemy, increment y by two to set correct relative position
iny ; if y was 0, now is 2, if y was 1, now is 3
@continue:
lsr ; shift the difference between player and enemy x difference 6 bits
lsr ; (every #$40 pixels difference is a new horizontal direction)
lsr
lsr
lsr
sty $07 ; store position of player relative to enemy in $07 (above/below, left/right)
lsr ; push bit 5 to the carry flag for use after plp instruction below
sta $0b ; overwrite player x position with shifted bits 6 and 7
; (values [#$00-#$03]) of horizontal distance
php ; backup CPU status flags on stack
lda $0f ; load which of the #$03 tables from quadrant_aim_dir_lookup_tbl to use
asl ; double since each entry is #$2 bytes
tay ; transfer to offset register
lda quadrant_aim_dir_lookup_tbl,y ; get low byte of quadrant_aim_dir_xx address
sta $0c ; store low byte of pointer address in $0c
lda quadrant_aim_dir_lookup_tbl+1,y ; get high byte of quadrant_aim_dir_xx address
sta $0d ; store high byte of pointer address in $0d
lda $0a ; load y difference to determine row offset
asl
asl ; quadruple since each entry is #$04 bytes to get correct row
adc $0b ; add the x distance between player and enemy as offset into the entry to load
; this gets the column of the aim direction
tay ; transfer to offset register
lda ($0c),y ; load specific byte
plp ; restore CPU status flags from stack
bcs @set_and_exit ; branch if bit 5 of difference between player and enemy was set
lsr ; this segments screen into bands for which nibble to use
lsr
lsr
lsr
@set_and_exit:
and #$0f ; keep low nibble
rts
; pointer table for set of quadran aim directions (#$3 * #$2 = #$6 bytes)
quadrant_aim_dir_lookup_tbl:
.addr quadrant_aim_dir_00 ; CPU address $f5b2 (soldiers, weapon boxes, red turrets, wall core)
.addr quadrant_aim_dir_01 ; CPU address $f5d2 (rotating gun, wall turrets, sniper, eye projectile, spinning bubbles, jumping soldier, white blob)
.addr quadrant_aim_dir_02 ; CPU address $f5f2 (dragon arm seeking)
; table for where to aim within a quadrant that is split into 3 parts [#$00-#$03] (#$20 bytes)
; * used by soldiers, weapon boxes, red turrets, wall turrets,
; wall core, and dragon arm orb projectiles
; (not arm seeking, that's #$02)
; * which nibble used from byte depends on bit 5 of difference between player and enemy distance
; * each subsequent row is player farther away from enemy with respect to y (height)
; * each subsequent column is player farther away from enemy with respect to x (distance)
quadrant_aim_dir_00:
.byte $00,$00,$00,$00 ; player at same height
.byte $32,$11,$00,$00
.byte $32,$11,$11,$11
.byte $32,$22,$11,$11
.byte $33,$22,$11,$11
.byte $33,$22,$22,$11
.byte $33,$22,$22,$11
.byte $33,$22,$22,$22
; table for where to aim within a quadrant that is split into 6 parts [#$00-#$06] (#$20 bytes)
; when used indoors only one 'quadrant' so the quadrant aim dir is the same as the aim dir
; * indoor levels exclusively use this table: wall turret, eye projectile, spinning bubbles
; jumping soldier, and wall core
; * also used by rotating gun, sniper, white blob, spinning bubbles, and tank
; * which nibble used from byte depends on bit 5 of difference between player and enemy distance
; * each subsequent row is player farther away from enemy with respect to y (height)
; * each subsequent column is player farther away from enemy with respect to x (distance)
quadrant_aim_dir_01:
.byte $00,$00,$00,$00 ; player at same height
.byte $63,$21,$11,$11
.byte $64,$32,$21,$11
.byte $65,$43,$22,$22
.byte $65,$44,$33,$22
.byte $65,$54,$33,$32
.byte $65,$54,$43,$33
.byte $65,$54,$44,$33
; table for where to aim within a quadrant that is split into #$0f parts [#$00-#$0f] (#$20 bytes)
; * used by dragon arm seeking (not projectile firing that's #$00)
; * which nibble used from byte depends on bit 5 of difference between player and enemy distance
; * each subsequent row is player farther away from enemy with respect to y (height)
; * each subsequent column is player farther away from enemy with respect to x (distance)
quadrant_aim_dir_02:
.byte $80,$00,$00,$00
.byte $f8,$53,$32,$21
.byte $fb,$86,$54,$33
.byte $fd,$a8,$75,$54
.byte $fe,$b9,$87,$65
.byte $fe,$cb,$98,$76
.byte $fe,$db,$a9,$87
.byte $ff,$dc,$ba,$98
; unused #$5ee bytes out of #$4,000 bytes total (90.73% full)
; unused 1,518 bytes out of 16,384 bytes total (90.73% full)
; filled with 1,518 #$ff bytes by contra.cfg configuration
bank_7_unused_space:
.segment "DPCM_SAMPLES"
; DPCM (differential pulse code modulation) audio sample used by DMC (delta modulation channel)
; CPU address $fc00 (#$51 bytes) See dpcm_sample_data_tbl
dpcm_sample_00:
.incbin "assets/audio_data/dpcm_sample_00.bin"
; possibly unused DPCM sample, #$5f bytes (excluding #$ff)
; CPU address $fc51
; exists in Japanese version of game as well (I don't think it's used there either)
unknown_00:
.byte $6b,$56,$ce,$b5,$5b,$5d,$59,$b6,$d5,$ab,$d6,$b5,$d7,$6b,$6d,$ad
.byte $ae,$b6,$d6,$b5,$b5,$aa,$d6,$aa,$ac,$aa,$a9,$54,$a9,$94,$a4,$a9
.byte $4a,$4a,$8a,$92,$54,$a5,$49,$29,$4a,$54,$a5,$29,$52,$a5,$4a,$a5
.byte $54,$a9,$54,$aa,$95,$2a,$aa,$94,$95,$2a,$aa,$55,$55,$2a,$56,$66
.byte $aa,$9a,$aa,$b5,$5a,$ad,$ab,$5a,$b5,$6b,$6b,$6b,$5a,$b5,$aa,$b5
.byte $56,$aa,$d5,$55,$56,$aa,$aa,$aa,$aa,$aa,$aa,$aa,$95,$55,$55,$ff
.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff
; DPCM (differential pulse code modulation) audio sample used by DMC (delta modulation channel)
; CPU address $fcc0 (#$251 bytes). See dpcm_sample_data_tbl
dpcm_sample_01:
.incbin "assets/audio_data/dpcm_sample_01.bin"
; possibly unused DPCM sample, #$a8 bytes (excluding #$ff)
; CPU address $ff0b
; exists in Japanese version of game as well (I don't think it's used there either)
unknown_01:
.byte $4c,$aa,$ca,$aa,$a5,$a6,$56,$55,$54,$d3,$2a,$c6,$aa,$6a,$96,$a6
.byte $66,$aa,$aa,$b2,$b4,$d5,$55,$66,$9a,$aa,$aa,$aa,$aa,$aa,$aa,$aa
.byte $aa,$aa,$aa,$9a,$a6,$56,$55,$52,$b2,$aa,$aa,$aa,$96,$aa,$65,$aa
.byte $aa,$aa,$b5,$55,$56,$6a,$6a,$aa,$aa,$aa,$6a,$72,$9a,$aa,$9a,$9a
.byte $a9,$96,$59,$55,$55,$55,$52,$d2,$b2,$aa,$a9,$aa,$aa,$aa,$ac,$b2
.byte $cc,$d5,$55,$55,$65,$96,$59,$aa,$aa,$9a,$9a,$9a,$aa,$6a,$56,$a5
.byte $65,$95,$55,$55,$55,$54,$b5,$32,$aa,$aa,$b2,$aa,$b2,$aa,$ab,$55
.byte $55,$55,$55,$55,$69,$aa,$aa,$96,$9a,$99,$5a,$59,$5a,$55,$65,$96
.byte $59,$55,$55,$55,$55,$55,$52,$b4,$aa,$aa,$aa,$aa,$b2,$d3,$55,$55
.byte $55,$55,$55,$56,$55,$66,$aa,$aa,$aa,$aa,$a6,$9a,$aa,$65,$55,$55
.byte $55,$55,$55,$32,$cc,$aa,$aa,$aa,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff
; NES footer area
; contains Konami production code, catalog number, game name, checksum,
; coumpany code, and country code.
; see https://forums.nesdev.org/viewtopic.php?p=56921 for more details
.segment "NES_FOOTER"
; a byte for each bank written when switching PRG ROM banks
; CPU address $ffd0
prg_rom_banks:
.byte $00,$01,$02,$03,$04,$05,$06,$07
; Konami production code RD008
; RD008 would be later used as the name for Bill's replacement in Probotector (European Contra release)
; https://tcrf.net/User:Revenant/Konami_catalog_numbers
; > Incidentally, the NES version of Contra had the ID RD008 within its code.
; > RD008 would later be used as the codename for one of the robot protagonists
; > (alongside RC011) in the European version of the game titled Probotector,
; > released in 1990. 4/4
; -- https://twitter.com/Arc_Hound/status/1161732318740041729
konami_catalog_number:
.byte $52,$44,$30,$30,$38,$ff,$ff,$ff ; RD008 in ASCII
; NES undocumented footer
; https://forums.nesdev.org/viewtopic.php?p=56921
; game name left-padded with zeros
nes_footer_rom_name:
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $43,$4f,$4e,$54,$52,$41 ; CONTRA in ASCII
; checksum of game bytes
; * add all bytes together (setting checksum bytes to #$00)
; * take smallest 2 bytes bytes of result
; actual sum of all bytes excluding checksum bytes is 14,256,747 (#$d98a6b)
nes_footer_checksum:
.byte $8a,$6b
; CHR ROM checksum - the sum of all bytes in the CHR ROM
; no CHR ROM for game so #$00 #$00
nes_footer_chr_checksum:
.byte $00,$00
; PRG and CHR size
; #$03 PRG ROM size - 128 KiB (8 banks each bank 16 KiB)
; #$08 CHR RAM size - 8 KiB
nes_footer_size:
.byte $38
; #$02 vertical mirroring
; * #$02 - vertical
; * #$81 or #$82 - horizontal
; * #$04 - mapper controlled
nes_footer_mirroring:
.byte $02
; byte 0 - country code - #$01 - North America
; byte 1 - unknown, almost always #$01
; byte 2 - company code - #$a4 - Konami
; byte 3 - unknown (matches Japanese Contra ROM)
nes_footer_maker_code:
.byte $01,$05,$a4,$1c
; locations of all 'vectors'. These are the 3 handles for NES interrupts
; stored in the .nes ROM as the last $06 bytes (CPU addresses $fffa-$ffff)
; these are stored at known locations so the NES can point the instruction
; pointer at known locations for triggering interrupts.
.segment "VECTORS"
.addr nmi_start, reset_vector, irq