LIST OFF ; *** G A M M A - A T T A C K *** ; Copyright 1983, Gammation ; Designer: Robert L. Esken, Jr. ; ; Analyzed, labeled and commented ; by Dennis Debro ; Last Update: December 28, 2023 ; ; *** 125 BYTES OF RAM USED 3 BYTES FREE ; *** 0 BYTES OF ROM FREE ; ; ============================================================================== ; = THIS REVERSE-ENGINEERING PROJECT IS BEING SUPPLIED TO THE PUBLIC DOMAIN = ; = FOR EDUCATIONAL PURPOSES ONLY. THOUGH THE CODE WILL ASSEMBLE INTO THE = ; = EXACT GAME ROM, THE LABELS AND COMMENTS ARE THE INTERPRETATION OF MY OWN = ; = AND MAY NOT REPRESENT THE ORIGINAL VISION OF THE AUTHOR. = ; = = ; = THE ASSEMBLED CODE IS © 1983, GAMMATION = ; = = ; ============================================================================== processor 6502 ; ; NOTE: You must compile this with vcs.h version 105 or greater. ; TIA_BASE_READ_ADDRESS = $00 ; set the read address base so this runs on ; the real VCS and compiles to the exact ; ROM image ; ; NOTE: You must compile this with vcs.h version 105 or greater. ; include "tia_constants.h" include "vcs.h" include "macro.h" ; ; Make sure we are using vcs.h version 1.05 or greater. ; IF VERSION_VCS < 105 echo "" echo "*** ERROR: vcs.h file *must* be version 1.05 or higher!" echo "*** Updates to this file, DASM, and associated tools are" echo "*** available at https://dasm-assembler.github.io/" echo "" err ENDIF ; ; Make sure we are using macro.h version 1.01 or greater. ; IF VERSION_MACRO < 101 echo "" echo "*** ERROR: macro.h file *must* be version 1.01 or higher!" echo "*** Updates to this file, DASM, and associated tools are" echo "*** available at https://dasm-assembler.github.io/" echo "" err ENDIF LIST ON ;=============================================================================== ; A S S E M B L E R - S W I T C H E S ;=============================================================================== NTSC = 0 PAL50 = 1 PAL60 = 2 TRUE = 1 FALSE = 0 ;=============================================================================== ; F R A M E - T I M I N G S ;=============================================================================== FPS = 60 ; ~60 frames per second VBLANK_TIME = 79 H_OVERSCAN = 20 ;=============================================================================== ; C O L O R - C O N S T A N T S ;=============================================================================== COLOR_MOUNTAIN = RED COLOR_TANK = BLACK COLOR_GROUND = RED + 8 COLOR_SKY = COBALT_BLUE + 2 COLOR_SAUCER = RED_ORANGE + 12 COLOR_SAUCER_LASER = WHITE + 1 COLOR_HIT_SAUCER = RED + 14 ;=============================================================================== ; U S E R - C O N S T A N T S ;=============================================================================== ROM_BASE = $F000 H_KERNEL = 74 H_COPYRIGHT_FONT = 7 H_SCORE_BOARD_FONT = 5 H_MOUNTAIN_TERRAIN = 7 XMIN = 1 XMAX = 72 INIT_SAUCER_HORIZ_POS = 56 INIT_SAUCER_VERT_POS = 16 INIT_OBJECT_HIT_TIMER = 31 MAX_GAME_SELECTION = 4 MOUNTAIN_ARRAY_SIZE = 24 SELECT_DELAY = 62 ; ; Mountain scrolling constants ; MOUNTAIN_SCROLL_DIR_LEFT = $80 MOUNTAIN_SCROLL_DIR_NONE = 0 MOUNTAIN_SCROLL_DIR_RIGHT = $7F ; ; Tank speed constants ; TANK_SLOW_VELOCITY = 1 TANK_MEDIUM_VELOCITY = 2 TANK_FAST_VELOCITY = 3 TANK_SCROLL_SPEED = 2 ; ; Tank position constants ; VERT_POSITION_TANK_00 = 48 VERT_POSITION_TANK_01 = 54 VERT_POSITION_TANK_02 = 60 VERT_POSITION_TANK_03 = 67 ; ; object ids ; ID_SAUCER_LASER = 0 ID_SAUCER = 1 ID_TANK_MISSILE = 2 ID_TANK_00 = 3 ID_TANK_01 = 4 ID_TANK_02 = 5 ID_TANK_03 = 6 ;=============================================================================== ; M A C R O S ;=============================================================================== MAC SLEEP_3 lda frameCount + 1 ENDM ;------------------------------------------------------- ; FILL_BOUNDARY byte# ; Original author: Dennis Debro (borrowed from Bob Smith / Thomas Jentzsch) ; ; Push data to a certain position inside a page and keep count of how ; many free bytes the programmer will have. ; ; eg: FILL_BOUNDARY 5, 234 ; position at byte #5 in page with $EA is byte filler FREE_BYTES SET 0 .BYTES_TO_SKIP SET 0 MAC FILL_BOUNDARY IF <. > {1} .BYTES_TO_SKIP SET (256 - <.) + {1} ELSE .BYTES_TO_SKIP SET (256 - <.) - (256 - {1}) ENDIF REPEAT .BYTES_TO_SKIP FREE_BYTES SET FREE_BYTES + 1 .byte {2} REPEND ENDM ;=============================================================================== ; Z P - V A R I A B L E S ;=============================================================================== SEG.U variables .org $80 gameSelection ds 1 currentRack ds 1 actionButtonDebounce ds 1 mountainScrollDirection ds 1 remainingTanks ds 1 saucerHoveringLimit ds 1 currentGameSpeed ds 1 frameCount ds 2 tankVelocity ds 1 tmpObjectIndex ds 1 ;-------------------------------------- tmpTankIndex = tmpObjectIndex objectScanlineValues ds 14 kernelZoneVectorLSBValues ds 11 zp_unused_00 ds 3 tmpScoreBoardIndex ds 1 ;-------------------------------------- tmpKernelZoneIndex = tmpScoreBoardIndex tmpMountainGraphicIndex ds 1 ;-------------------------------------- tmpScoreBoardDigitIdx = tmpMountainGraphicIndex ;-------------------------------------- tmpSavedObjectIndex = tmpScoreBoardDigitIdx scoreBoardPF1Graphics ds 5 scoreBoardPF2Graphics ds 5 playerScore ds 2 ;-------------------------------------- playerScoreHundredsValue = playerScore playerScoreOnesValue = playerScoreHundredsValue + 1 objectFineMotionValues ds 7 ;-------------------------------------- saucerLaserFineMotionValue = objectFineMotionValues saucerFineMotionValue = objectFineMotionValues + 1 tankMissileFineMotionValue = objectFineMotionValues + 2 tankFineMotionValues = objectFineMotionValues + 3 ;-------------------------------------- tankFineMotionValue_00 = tankFineMotionValues tankFineMotionValue_01 = tankFineMotionValue_00 + 1 tankFineMotionValue_02 = tankFineMotionValue_01 + 1 tankFineMotionValue_03 = tankFineMotionValue_02 + 1 objectCoarsePositionValues ds 7 ;-------------------------------------- saucerLaserCoarsePosValue = objectCoarsePositionValues saucerCoarsePosValue = objectCoarsePositionValues + 1 tankMissileCoarsePosValue = objectCoarsePositionValues + 2 tankCoarsePositionValues = objectCoarsePositionValues + 3 ;-------------------------------------- tankCoarsePosition_00 = tankCoarsePositionValues tankCoarsePosition_01 = tankCoarsePosition_00 + 1 tankCoarsePosition_02 = tankCoarsePosition_01 + 1 tankCoarsePosition_03 = tankCoarsePosition_02 + 1 objectHorizontalPositions ds 7 ;-------------------------------------- saucerLaserHorizPosition = objectHorizontalPositions saucerHorizPosition = objectHorizontalPositions + 1 tankMissileHorizPosition = objectHorizontalPositions + 2 tankHorizPositions = objectHorizontalPositions + 3 ;-------------------------------------- tankHorizPosition_00 = tankHorizPositions tankHorizPosition_01 = tankHorizPosition_00 + 1 tankHorizPosition_02 = tankHorizPosition_01 + 1 tankHorizPosition_03 = tankHorizPosition_02 + 1 gameState ds 1 objectVerticalPositions ds 7 ;-------------------------------------- saucerLaserIndex = objectVerticalPositions saucerVerticalPosition = objectVerticalPositions + 1 tankMissileVerticalPosition = objectVerticalPositions + 2 tankVerticalPositions = objectVerticalPositions + 3 ;-------------------------------------- tankVertPosition_00 = tankVerticalPositions tankVertPosition_01 = tankVertPosition_00 + 1 tankVertPosition_02 = tankVertPosition_01 + 1 tankVertPosition_03 = tankVertPosition_02 + 1 tankMissileSoundVolume ds 1 objectSpriteLSBValues ds 7 ;-------------------------------------- saucerLaserSpriteLSBValue = objectSpriteLSBValues saucerSpriteLSBValue = objectSpriteLSBValues + 1 tankMissileLSBValue = objectSpriteLSBValues + 2 tankSpriteLSBValues = objectSpriteLSBValues + 3 ;-------------------------------------- tankSpriteLSBValue_00 = tankSpriteLSBValues tankSpriteLSBValue_01 = tankSpriteLSBValue_00 + 1 tankSpriteLSBValue_02 = tankSpriteLSBValue_01 + 1 tankSpriteLSBValue_03 = tankSpriteLSBValue_02 + 1 activateSaucerLaserDisplay ds 1 activeTankIndex ds 1 objectHitTimer ds 1 objectColorValues ds 5 ;-------------------------------------- colorTank = objectColorValues ;-------------------------------------- colorCopyrightBackground = colorTank colorGround = objectColorValues + 1 colorSky = objectColorValues + 2 colorSaucer = objectColorValues + 3 colorSaucerLaser = objectColorValues + 4 ;-------------------------------------- colorCopyright = colorSaucerLaser mountainPlayfieldGraphics ds MOUNTAIN_ARRAY_SIZE ;-------------------------------------- mountainPF0Graphics = mountainPlayfieldGraphics mountainPF1Graphics = mountainPF0Graphics + 8 mountainPF2Graphics = mountainPF1Graphics + 8 tmpScoreBoardGraphics ds 4 ;-------------------------------------- tmpScoreBoardPF1Graphics = tmpScoreBoardGraphics tmpScoreBoardPF2Graphics = tmpScoreBoardPF1Graphics + 2 ;-------------------------------------- tmpKernelJmpVectors = tmpScoreBoardGraphics ;-------------------------------------- tmpKernelZoneVector = tmpKernelJmpVectors tmpCoarsePositionVector = tmpKernelZoneVector + 2 ;-------------------------------------- tmpAdjustedHorizPos = tmpScoreBoardGraphics ;-------------------------------------- tankMissileGraphicIdx ds 1 scanline ds 1 echo "***",(* - $80 - 3)d, "BYTES OF RAM USED", ($100 - * + 3)d, "BYTES FREE" ;=============================================================================== ; R O M - C O D E ;=============================================================================== SEG code .org ROM_BASE Start sei cld lda #0 tax .clearLoop sta VSYNC,x inx bne .clearLoop lda #$10 sta SWBCNT ; set D4 of SWCHB as output lda #128 sta frameCount lda #1 sta gameSelection ; set initial game selection sta playerScoreOnesValue ; set to display initial game selection ResetGame lda #INIT_SAUCER_HORIZ_POS sta saucerSpriteLSBValue sta saucerHorizPosition ; set Saucer horizontal position adc #4 sta saucerLaserHorizPosition ; set Saucer laser horizontal position ldx #MOUNTAIN_ARRAY_SIZE - 1 .setInitialMountainGraphicValues lda InitMountainGraphics,x sta mountainPlayfieldGraphics,x dex bpl .setInitialMountainGraphicValues ldx #4 stx remainingTanks .setInitialObjectColorValues lda InitObjectColors,x sta objectColorValues,x dex bpl .setInitialObjectColorValues ldy #<[OffScreenSprite - SpriteGraphics] ldx #<[tankMissileHorizPosition - objectHorizontalPositions] .setInitialTankValues sty objectSpriteLSBValues,x adc #8 sta objectHorizontalPositions,x inx cpx #8 bne .setInitialTankValues lda #INIT_SAUCER_VERT_POS sta saucerVerticalPosition sta saucerHoveringLimit lda #0 sta playerScoreHundredsValue ; initialize score hundreds value NewFrame lda #DISABLE_TIA sta WSYNC ; wait for next scan line sta VBLANK ; disable TIA (i.e. D1 = 1) sta WSYNC sta WSYNC sta WSYNC sta VSYNC ; start vertical sync (i.e. D1 = 1) sta CTRLPF ; set to PF_SCORE lda #0 inc frameCount + 1 ; increment frame count bpl .verticalSync inc frameCount sta frameCount + 1 .verticalSync sta WSYNC sta WSYNC sta WSYNC sta VSYNC ; end vertical sync (i.e. D1 = 0) sta activateSaucerLaserDisplay ; clear Saucer laser display value lda #VBLANK_TIME sta TIM64T ; set timer for vertical blanking period ldx #$FF txs ; set stack to the beginning jsr PerformGameCalculations lda colorSaucer sta COLUP1 sta CXCLR sta $2E ; not needed DisplayKernel .waitTime lda INTIM bne .waitTime sta WSYNC ;-------------------------------------- sta VBLANK ; 3 = @03 enable TIA (i.e. D1 = 0) sta WSYNC ;-------------------------------------- sta scanline ; 3 sta WSYNC ;-------------------------------------- sta PF0 ; 3 = @03 sta PF1 ; 3 = @06 sta PF2 ; 3 = @09 tax ; 2 lda objectHitTimer ; 3 get object hit timer value and #2 ; 2 beq .setNormalSkyColor ; 2³ lda colorSaucerLaser ; 3 bne .setSkyBackgroundColor ; 3 unconditional branch .setNormalSkyColor lda colorSky ; 3 .setSkyBackgroundColor sta COLUP0 ; 3 = @27 set to hide left side score values sta COLUBK ; 3 = @30 .drawScoreBoardKernel lda scoreBoardPF1Graphics,x; 4 sta PF1 ; 3 = @37 lda scoreBoardPF2Graphics,x; 4 sta PF2 ; 3 = @44 inx ; 2 sta WSYNC ;-------------------------------------- sta WSYNC ;-------------------------------------- lda scanline ; 3 get current scan line count clc ; 2 adc #2 ; 2 sta scanline ; 3 increment by 2 (i.e. 2LK) cpx #H_SCORE_BOARD_FONT ; 2 bne .drawScoreBoardKernel ; 2³ lda #0 ; 2 sta PF1 ; 3 = @19 sta PF2 ; 3 = @22 sta CTRLPF ; 3 = @25 sta WSYNC ;-------------------------------------- sta WSYNC ;-------------------------------------- tay ; 2 sta tmpKernelZoneIndex ; 3 sta tankMissileGraphicIdx ; 3 sta WSYNC ;-------------------------------------- sta HMCLR ; 3 = @03 lda colorSaucerLaser ; 3 sta COLUP1 ; 3 = @09 lda #$EE ; 2 sta tmpMountainGraphicIndex; 3 any value greater than 127 lda #>CoarsePositionObject ; 2 sta tmpCoarsePositionVector + 1;3 lda #>KernelSetupRoutines ; 2 sta tmpKernelZoneVector + 1; 3 bne .checkKernelZoneDone ; 3 + 1 crosses page boundary NextKernelZone sta WSYNC ;-------------------------------------- sta HMOVE ; 3 inc tmpKernelZoneIndex ; 5 .nextKernelZone lda SpriteGraphics,y ; 4 get GRP0 graphic data sta GRP0 ; 3 = @07 beq .drawMountainTerrain ; 2³ branch if done drawing GRP0 iny ; 2 increment GRP0 graphic index .drawMountainTerrain lda scanline ; 3 get scan line count and #3 ; 2 0 <= a <= 3 bne .incrementScanline ; 2³ ldx tmpMountainGraphicIndex; 3 bmi .incrementScanline ; 2³ branch if done drawing mountains lda mountainPF0Graphics,x ; 4 sta PF0 ; 3 = @30 lda mountainPF1Graphics,x ; 4 sta PF1 ; 3 = @37 lda mountainPF2Graphics,x ; 4 sta PF2 ; 3 = @44 lda MountainColorValues,x ; 4 clc ; 2 adc colorTank ; 3 sta COLUPF ; 3 = @56 dec tmpMountainGraphicIndex; 5 .incrementScanline inc scanline ; 5 increment scan line count ldx tankMissileGraphicIdx ; 3 get Tank missile graphic index sta HMCLR ; 3 = @72 lda activateSaucerLaserDisplay;3 get Saucer laser activation value beq .tankMissileScanline ; 2³ branch if not drawing Saucer laser ;-------------------------------------- lda #HMOVE_L2 ; 2 sta HMM1 ; 3 = @06 shift Saucer laser left 2 pixels .tankMissileScanline sta WSYNC ;-------------------------------------- lda SpriteGraphics,y ; 4 get GRP0 graphic data sta GRP0 ; 3 = @07 beq .checkToDrawTankMissile; 2³ branch if done drawing GRP0 iny ; 2 increment GRP0 graphic index .checkToDrawTankMissile lda TankMissileSprite,x ; 4 get Tank missile graphic data sta GRP1 ; 3 = @18 beq .setTankMissileGraphicIndex;2³ branch if done drawing Tank missile inx ; 2 increment Tank missile graphic index .setTankMissileGraphicIndex stx tankMissileGraphicIdx ; 3 .checkKernelZoneDone ldx tmpKernelZoneIndex ; 3 lda objectScanlineValues,x ; 4 get zone scan line value beq .checkToDrawCopyrightKernel;2³ cmp scanline ; 3 bpl .nextScanline ; 2³ lda kernelZoneVectorLSBValues,x;4 sta tmpKernelZoneVector ; 3 jmp (tmpKernelZoneVector) ; 5 = @51 .checkToDrawCopyrightKernel lda #H_KERNEL ; 2 cmp scanline ; 3 bne .nextScanline ; 2³ lda colorCopyrightBackground;3 sta COLUBK ; 3 sta WSYNC ;-------------------------------------- lda #MSBL_SIZE1 | TWO_COPIES;2 sta NUSIZ0 ; 3 = @05 sta NUSIZ1 ; 3 = @08 lda colorCopyright ; 3 sta COLUP0 ; 3 = @14 ldx #DISABLE_BM ; 2 stx ENAM1 ; 3 = @19 ldy #8 ; 2 sta WSYNC ;-------------------------------------- .coarsePositionCopyrightSprites dey ; 2 bne .coarsePositionCopyrightSprites;2³ sta RESP0 ; 3 = @47 sta RESP1 ; 3 = @50 .drawCopyrightKernel ldy #4 ; 2 sta WSYNC ;-------------------------------------- .copyrightDelay dey ; 2 bne .copyrightDelay ; 2³ lda CopyrightFont_00,x ; 4 sta GRP0 ; 3 = @26 lda CopyrightFont_01,x ; 4 sta GRP1 ; 3 = @33 lda CopyrightFont_02,x ; 4 ldy CopyrightFont_03,x ; 4 SLEEP 2 ; 2 sta GRP0 ; 3 = @46 sty GRP1 ; 3 = @49 inx ; 2 cpx #H_COPYRIGHT_FONT ; 2 bne .drawCopyrightKernel ; 2³ lda #0 ; 2 sta GRP0 ; 3 = @60 sta GRP1 ; 3 = @63 ldx #H_OVERSCAN ; 2 .overscan sta WSYNC dex bne .overscan jmp NewFrame .nextScanline sta WSYNC ;-------------------------------------- sta HMOVE ; 3 jmp .nextKernelZone ; 3 PerformGameCalculations lda frameCount + 1 ; get current frame count and #1 beq ProcessUserInputs ; branch on even frames jmp CheckSaucerLaserCollisionWithTank ProcessUserInputs lda SWCHB ; read console switches ror ; shift RESET value to carry bcs .checkForSelectPressed ; branch if RESET not pressed lda gameState ; get current game state bne .checkToCycleColors ; branch if game in progress lda #1 sta frameCount sta gameState ; set to show game in progress lda #0 sta currentRack ; reset current rack sta playerScoreOnesValue ; initialize score ones value lda gameSelection ; get current game selection sta currentGameSpeed jmp ResetGame .checkForSelectPressed ldx #0 stx gameState ; clear current game state ror ; shift SELECT value to carry bcs .checkToCycleColors ; branch if SELECT not pressed lda frameCount + 1 ; get current frame count and #SELECT_DELAY cmp #SELECT_DELAY bne .checkToCycleColors lda #128 sta frameCount lda #0 sta playerScoreHundredsValue ; initialize score hundreds value inc gameSelection ; increment game selection value lda gameSelection ; get current game selection cmp #MAX_GAME_SELECTION + 1 bne .setToDisplayCurrentGameSelection lda #1 sta gameSelection ; wrap to initial game selection value .setToDisplayCurrentGameSelection sta playerScoreOnesValue .checkToCycleColors lda frameCount and #$80 ; a == 0 || a == 128 beq .checkToAnimateSaucer lda #128 sta frameCount lda frameCount + 1 ; get current frame count bne .checkToAnimateSaucer ldx #4 .cycleObjectColors inc objectColorValues,x ; increment object color value dex bpl .cycleObjectColors .checkToAnimateSaucer lda saucerSpriteLSBValue ; get Saucer sprite LSB value cmp #<[GammySprite - SpriteGraphics] beq ReadJoystickValues ; branch if showing GAMMY and #1 ; 0 <= a <= 1 tax lda SaucerAnimation,x ; animate Saucer sprite sta saucerSpriteLSBValue ReadJoystickValues lda SWCHA ; read joystick values and #P1_JOYSTICK_MASK ; keep right joystick values ror ; shift MOVE_UP to carry bcs .checkJoystickMovedDown ldx saucerVerticalPosition ; get Saucer vertical position cpx saucerHoveringLimit bmi .checkJoystickMovedDown ; branch if reached hovering limit dec saucerVerticalPosition ; decrement Saucer vertical position .checkJoystickMovedDown ror ; shift MOVE_DOWN to carry bcs .checkJoystickMovedLeft ldx saucerVerticalPosition ; get Saucer vertical position cpx #INIT_SAUCER_VERT_POS + 24 bpl .checkJoystickMovedLeft inc saucerVerticalPosition ; increment Saucer vertical position .checkJoystickMovedLeft ror ; shift MOVE_LEFT to carry bcs .checkJoystickMovedRight ldx #MOUNTAIN_SCROLL_DIR_RIGHT stx mountainScrollDirection .checkJoystickMovedRight ror ; shift MOVE_RIGHT to carry bcs ResetZoneScanlineValues ; branch if not MOVE_RIGHT ldx #MOUNTAIN_SCROLL_DIR_LEFT stx mountainScrollDirection ResetZoneScanlineValues lda #0 ldx #14 .clearZoneScanlineValues sta objectScanlineValues,x ; clear zone scan line value dex bne .clearZoneScanlineValues ldx #0 stx tmpKernelZoneIndex ; reset kernel zone index for processing ldy #0 sty tmpSavedObjectIndex lda saucerSpriteLSBValue ; get Saucer sprite LSB value cmp #<[GammySprite - SpriteGraphics] bne ReadControllerActionButton ; branch if not showing GAMMY iny bne .setSaucerScanlineValues ; unconditional branch ReadControllerActionButton lda objectHitTimer ; get object hit timer value beq .readControllerActionButton ; branch if object not doing hit animation lda #4 sta saucerLaserIndex bne .activateSaucerLaser ; unconditional branch .readControllerActionButton lda INPT5 + 48 ; read right controller action button bmi .clearActionButtonDebounceValue;branch if action button not pressed lda actionButtonDebounce ; get action button debounce bne .activateSaucerLaser ; branch if action button held lda #1 sta actionButtonDebounce stx saucerLaserIndex ; reset Saucer laser index (i.e. x = 0) .activateSaucerLaser lda saucerLaserIndex cmp #4 beq .turnOffSaucerLaserDisplay inc saucerLaserIndex ldy saucerLaserIndex lda SaucerLaserScanlineValues - 1,y; y never 0 bne .setSaucerLaserScanlineValue ; unconditional branch .clearActionButtonDebounceValue stx actionButtonDebounce ; clear action button debounce (i.e. x = 0) jmp .activateSaucerLaser .turnOffSaucerLaserDisplay lda saucerVerticalPosition ; get Saucer vertical position .setSaucerLaserScanlineValue sta objectScanlineValues,x ldy #ID_SAUCER_LASER lda #