LIST OFF ; *** S P A C E J O C K E Y *** ; Copyright 1982 US Games Corporation ; Designer: Garry Kitchen ; Analyzed, labeled and commented ; by Dennis Debro ; Last Update: June 14, 2004 ; ; This was Garry's first Atari VCS game. Garry learned to program the VCS by ; reverse engineering the hardware and various games using his Apple II. This ; game was started by him reverse engineering Outlaw which was written by ; David Crane while at Atari. ; ; After writing Space Jockey he approached his boss and suggested selling the ; game to Activision. Instead the company decided to sell the game to US Games. ; So Space Jockey became US Games' first VCS game. ; ; Garry uses a horizontal position routine that *seems* to first appear here. ; This routine was modified over the years and has been seen in a number of ; games. ; ; To produce the PAL listing I used the Carrere Video version. The PAL version ; adjusts the vertical blank time to make the game produce 314 scan lines. ; The colors were also adjusted but it seems they missed the place in the ; kernel where Garry colors some objects directly. The speeds and the sound ; frequencies were not adjusted for the PAL timing. processor 6502 ; ; NOTE: You must compile this with vcs.h version 105 or greater. ; TIA_BASE_READ_ADDRESS = $30 ; set the read address base so this runs on ; the real VCS and compiles to the exact ; ROM image include vcs.h LIST ON ;=============================================================================== ; A S S E M B L E R - S W I T C H E S ;=============================================================================== NTSC = 0 PAL = 1 COMPILE_VERSION = NTSC ; change this to compile for different ; regions ;============================================================================ ; T I A - C O N S T A N T S ;============================================================================ HMOVE_L7 = $70 HMOVE_L6 = $60 HMOVE_L5 = $50 HMOVE_L4 = $40 HMOVE_L3 = $30 HMOVE_L2 = $20 HMOVE_L1 = $10 HMOVE_0 = $00 HMOVE_R1 = $F0 HMOVE_R2 = $E0 HMOVE_R3 = $D0 HMOVE_R4 = $C0 HMOVE_R5 = $B0 HMOVE_R6 = $A0 HMOVE_R7 = $90 HMOVE_R8 = $80 ; values for NUSIZx: ONE_COPY = %000 TWO_COPIES = %001 TWO_WIDE_COPIES = %010 THREE_COPIES = %011 DOUBLE_SIZE = %101 THREE_MED_COPIES = %110 QUAD_SIZE = %111 ; values for REFPx: NO_REFLECT = %0000 REFLECT = %1000 ; SWCHA joystick bits: MOVE_RIGHT = %01111111 MOVE_LEFT = %10111111 MOVE_DOWN = %00100000 MOVE_UP = %00010000 NO_MOVE = %11111111 ; mask for SWCHB BW_MASK = %1000 ; black and white bit SELECT_MASK = %10 RESET_MASK = %01 ;============================================================================ ; U S E R - C O N S T A N T S ;============================================================================ ROM_BASE_ADDRESS = $F000 ; frame time values VSYNC_TIME = $28 OVERSCAN_LINES = 28 ; number of scanlines in overscan IF COMPILE_VERSION = NTSC VBLANK_TIME = $2D ELSE VBLANK_TIME = $6A ENDIF ; color constants BLACK = $00 WHITE = $0F IF COMPILE_VERSION = NTSC BLUE = $80 YELLOW = $10 GREEN_BROWN = $E0 BRIGHT_GREEN = $C0 GREEN = $B0 RED = $40 PURPLE = $50 BRICK_RED = $30 BROWN = $F0 LT_BROWN = BROWN+2 SIREN_LIGHT = BRICK_RED ELSE BLUE = $D0 YELLOW = $40 GREEN_BROWN = YELLOW BRIGHT_GREEN = $50 GREEN = $30 RED = $80 PURPLE = $A0 BRICK_RED = $60 BROWN = YELLOW LT_BROWN = BROWN+4 SIREN_LIGHT = RED ENDIF ; objectType ids: ID_BALLOON = 0 ID_JET_PLANE = 1 ID_HELICOPTER = 2 ID_PROP_PLANE = 3 ID_HOUSE = 4 ID_TREE = 5 ID_TANK_0 = 6 ID_TANK_1 = 7 ; game selection values OPTION_PLAYER_MOVE_MISSILE = %01 OPTION_ENEMY_MOVE_VERT = %10 OPTION_PLAYER_MOVE_HORIZ = %100 OPTION_PLAYER_COLLISIONS = %1000 ; object score values (BCD) BALLOON_SCORE = $25 JET_PLANE_SCORE = $99 HELICOPTER_SCORE = $50 PROP_PLANE_SCORE = $99 HOUSE_SCORE = $20 TREE_SCORE = $20 TANK_SCORE = $99 ; kernel boundaries KERNEL_HEIGHT = 153 KERNEL_ZONE_HEIGHT = 52 SPACE_JOCKEY_XMIN = 16 SPACE_JOCKEY_XMAX = 131 SPACE_JOCKEY_YMIN = 21 SPACE_JOCKEY_YMAX = 153 ENEMY_XMAX = 152 MISSILE_XMIN = 1 MISSILE_XMAX = 162 SPACE_JOCKEY_MISSILE_OFFSET = 3 ENEMY_MISSILE_OFFSET = 8 INITIAL_START_COUNT = 32 NUMBER_OF_GROUPS = 3 ; number of enemy groups STARTING_LIVES = 2 ; number of lives at the start of a game MAX_LIVES = 6 ; maximum number of reserved lives MOUNTAIN_ROLL_RATE = 3 SELECT_DELAY = 30 ; number of frames to wait to check select ; switch LOGO_HEIGHT = 5 NUMBER_HEIGHT = 8 SPACE_JOCKEY_HEIGHT = 7 MISSILE_PIXEL_MOVEMENT = 3 ;============================================================================ ; Z P - V A R I A B L E S ;============================================================================ spaceJockeyColors = $80 ; $80 - $86 scoreColor = $87 playfieldColor = $88 backgroundColor = $89 livesColor = $8A spaceJockeyGraphics = $8B ; $8B - $91 spaceJockeyVertPos = $92 spaceJockeyHorzPos = $93 pf0Data = $94 ; $94 - $96 pf1Data = $97 ; $97 - $99 pf2Data = $9A ; $9A - $9C enemyVerticalPos = $9D ; $9D - $9F enemyLSB = $A0 ; $A0 - $A2 enemyGraphicPointer = $A3 ; $A3 - $A4 enemyColorPointer = $A5 ; $A5 - $A6 numberOfLives = $A7 playerScore = $A8 ; $A8 - $AA enemyAttributes = $AB ; $AB - $AD digitPointer = $AE ; $AE - $B9 deathAnimPointer = $BA ; $BA - $BB ;-------------------------------------- playerColors = deathAnimPointer ;-------------------------------------- tempCharHolder = playerColors ;-------------------------------------- allowedMotion = playerColors ;-------------------------------------- enemyVertDistance = playerColors ; used to calculate new enemy vertical pos loopCount = playerColors+1 ;-------------------------------------- tempGRP0Graphic = loopCount enemyMotion = $BC ; $BC - $BE startCount = enemyMotion objectIds = $BF ; $BF - $C1 enemyHorizPos = $C2 ; $C2 - $C4 enemyVelocity = $C5 ; $C5 - $C7 enemyDeathAnimRate = $C8 ; $C8 - $CA enemyDeathSound = $CB ; $CB - $CD enemyColorsLSB = $CE ; $CE - $CF enemyScanline = $D1 enemyMissileVert = $D3 enemyMissileHoriz = $D4 spaceJockeyMissileVert = $D5 spaceJockeyMissileHoriz = $D6 kernelZone = $D7 kernelZoneEnd = $D8 scanline = $D9 selectDebounce = $DA mountainRollingRate = $DB randomSeed = $DC ; $DC - $DE frameCount = $DF ; updated each frame enemyShotVolume = $E0 spaceJockeyShotFreq = $E1 ; sound frequency spaceJockDeathAnimRate = $E2 spaceJockeyDeathSound = $E3 attractModeTimer = $E4 showHighScore = $E5 ; show high score when set high (D7 = 1) spaceJockeyHit = $E6 enemyFiring = $E7 ; $FF = firing $00 = not firing spaceJockeyFiring = $E8 ; $FF = firing $00 = not firing gameState = $E9 ; D7 = 0 game over attractMode = $EA clearGameRAM = $EB ; set high to clear RAM highScore = $EC ; $EC - $EE gameSelection = $EF ;============================================================================ ; R O M - C O D E (Part 1) ;============================================================================ SEG Bank0 org ROM_BASE_ADDRESS Start ; ; Set up everything so the power up state is known. ; sei cld ; clear decimal mode ldx #$FF txs ; point the stack to the beginning inx ; x = 0 txa .clearLoop sta VSYNC,x inx bne .clearLoop jsr InitializeGame inc playerScore+2 dec attractMode ; reduces on game cart power up so it's ; value is negative MainLoop lda backgroundColor sta COLUBK inc randomSeed dec randomSeed+1 inc randomSeed+2 VerticalSync SUBROUTINE ldy #$FF sty VSYNC ; start vertical sync (D1 = 1) sty VBLANK ; turn off TIA lda #VSYNC_TIME sta TIM8T ; set timer for VSYNC wait period inc frameCount ; increment frameCount each new frame bne .vsyncWaitTime lda attractMode ; cycle colors when value goes negative bpl .skipColorCycling ldx #spaceJockeyGraphics-spaceJockeyColors-1 .cycleGameColors inc spaceJockeyColors,x dex bpl .cycleGameColors .skipColorCycling inc attractModeTimer bne .vsyncWaitTime sty attractMode ; attractMode now negative .vsyncWaitTime ldy INTIM bne .vsyncWaitTime sty WSYNC sty VSYNC ; end vertical sync VerticalBlank SUBROUTINE lda #VBLANK_TIME sta TIM64T ; set timer for VBLANK time lda SWCHB ; read the console switches and #SELECT_MASK | RESET_MASK ; mask the SELECT and RESET values cmp #SELECT_MASK | RESET_MASK ; see if SELECT or RESET is pressed bne .consoleSwitchDown ; one of the console switches is down lda INPT4 ; read the fire button bmi .checkForGameReset ; if not pressed then check game reset .consoleSwitchDown sty attractMode ; reset the attractMode sty attractModeTimer ; zero out ldx #10 jsr InitLoop .checkForGameReset lda SWCHB ; read the console switches lsr ; shift game reset to carry bcs .skipReset ; skip game reset lda clearGameRAM ; if negative then clear game RAM bmi ClearGameRAM .setToClearGameRAM dec clearGameRAM ; show to clear RAM ; ; This routine clears RAM starting at (attractMode) and goes back to PF1 : ClearGameRAM ldx #156 lda #0 .clearRAM sta PF1+64,x ; clear RAM starting at attractMode dex ; back to PF1 bne .clearRAM jsr InitializeGame bmi .convertDigits ; unconditional branch .skipReset ldx #0 lsr ; shift game select to carry bcs .skipSelect lda clearGameRAM bpl .setToClearGameRAM lda selectDebounce beq .incrementGameSelection dec selectDebounce bpl .skipIncrementGameSelection .incrementGameSelection stx playerScore+1 ; clear the player's score (x = 0) stx playerScore inc gameSelection ; increase game selection lda gameSelection ; get the game selection and #$0F ; mask the upper nibble sta gameSelection ; store the value in the gameSelection clc adc #1 ; add 1 to the value cmp #10 ; if the value is greater than 10 then ; show the tens position value (i.e. BCD) bcc .setTensPosition sbc #10 ; subtract 10 from the value (carry set) ora #16 ; set the upper nibble to show tens value .setTensPosition sta playerScore+2 ; store value to show the game selection ldx #SELECT_DELAY ; reset select debounce value .skipSelect stx selectDebounce .skipIncrementGameSelection lda gameState ; get the current game state bmi .skipAttactModeProcessing ; branch if game in progress lda INPT4 ; read the fire button bmi .checkToShowHighScore ; skip game start if not pressed lda clearGameRAM bpl .setToClearGameRAM lda #$FF sta gameState ; show game is in progress inc clearGameRAM ; now positive sty playerScore+2 sty showHighScore ; show the score beq .convertDigits ; unconditional branch .checkToShowHighScore lda SWCHA ; read the joystick values and #$F0 ; only concerned with left joystick port cmp #$F0 ; if the stick wasn't moved then beq .dontShowHighScore ; don't show the high score ldy #$FF ; joystick was moved so show high score .dontShowHighScore sty showHighScore ; set flag to trigger showing high score .convertDigits jmp BCD2DigitPtrs .skipAttactModeProcessing lda spaceJockeyHit ; check if the space jocky was hit bmi CheckGameCollisions ; skip joystick routine if true lda SWCHA ; read the joystick values sta allowedMotion ; save the value for later lda gameSelection ; get the current game selection and #OPTION_PLAYER_MOVE_HORIZ ; if the selection is divisible by 4 beq .checkVerticalMotion ; then skip horizontal movement lda allowedMotion ; get the player joystick value asl bmi .checkRightMotion ; ; player moving left ; inc randomSeed+2 dec spaceJockeyHorzPos dec spaceJockeyHorzPos lda #SPACE_JOCKEY_XMIN ; make sure Space Jockey doesn't move cmp spaceJockeyHorzPos ; too far to the left bcc .checkRightMotion sta spaceJockeyHorzPos .checkRightMotion lda allowedMotion ; get the player joystick value bmi .checkVerticalMotion ; ; player moving right ; inc randomSeed inc spaceJockeyHorzPos inc spaceJockeyHorzPos lda #SPACE_JOCKEY_XMAX ; make sure Space Jockey doesn't move cmp spaceJockeyHorzPos ; too far to the right bcs .checkVerticalMotion sta spaceJockeyHorzPos .checkVerticalMotion lda allowedMotion ; get the player joystick value and #MOVE_DOWN bne .checkUpMotion ; ; player moving down ; inc randomSeed dec spaceJockeyVertPos dec spaceJockeyVertPos lda #SPACE_JOCKEY_YMIN ; make sure Space Jockey doesn't move cmp spaceJockeyVertPos ; too far down the screen bcc .checkUpMotion sta spaceJockeyVertPos .checkUpMotion lda allowedMotion ; get the player joystick value and #MOVE_UP bne CheckGameCollisions ; ; player moving up ; dec randomSeed+1 inc spaceJockeyVertPos inc spaceJockeyVertPos lda #SPACE_JOCKEY_YMAX ; make sure Space Jockey doesn't move cmp spaceJockeyVertPos ; too far up the screen bcs CheckGameCollisions sta spaceJockeyVertPos CheckGameCollisions lda gameSelection and #OPTION_PLAYER_COLLISIONS ; game #8-16 check player/player collision beq .noPlayerCollisions lda CXPPMM ; read the player/missle collision bpl .noPlayerCollisions ; if the players hadn't collided then skip lda spaceJockeyVertPos sec sbc #SPACE_JOCKEY_HEIGHT-2 bne LF189 .noPlayerCollisions lda spaceJockeyFiring ; get Space Jockey firing state bmi .moveSpaceJockeyMissile ; if firing then move the missile sta AUDV0 ldx spaceJockeyVertPos dex dex stx spaceJockeyMissileVert lda spaceJockeyHorzPos clc adc #16 sta spaceJockeyMissileHoriz lda INPT4 ; read the fire button ora spaceJockeyHit ; or the value with space jockey hit state bmi RollingMountainAnimation ; skip the missile fire routine dec spaceJockeyFiring ; value now negative to show firing state dec spaceJockeyMissileVert .moveSpaceJockeyMissile inc randomSeed lda #8 sta AUDV0 sta AUDC0 lda spaceJockeyMissileHoriz ; get the Space Jockey missile's horizontal ; position clc adc #MISSILE_PIXEL_MOVEMENT ; Space Jockey missile pixel movement cmp #MISSILE_XMAX ; make sure the missile doesn't go out of bcc .setMissileHorizontalValue ; range lda #MISSILE_XMIN sta spaceJockeyMissileVert .setMissileHorizontalValue sta spaceJockeyMissileHoriz lda spaceJockeyShotFreq ; get the shot sound frequency clc adc #MISSILE_PIXEL_MOVEMENT ; increment it by the pixel movement cmp #SPACE_JOCKEY_XMAX-1 bcc .setSpaceJockeyShotFrequency inc spaceJockeyFiring ; value positive to show not firing state lda #0 ; used to reset the shot sound frequency .setSpaceJockeyShotFrequency sta spaceJockeyShotFreq sta AUDF0 lda CXM1P bpl .moveSpaceJockeyMissileVert ; Space Jockey didn't shoot an enemy lda spaceJockeyMissileVert LF189 jsr FindKernelZone ; find the "zone" the space jockey is in lda enemyAttributes,x ; get the attributes value bmi .moveSpaceJockeyMissileVert ; branch if the object has been shot ora #%10000000 ; or the value to show that the object sta enemyAttributes,x ; has been shot lda #$15 sta AUDF0 ; set the object shot sound frequency sta spaceJockeyMissileVert lda #2 sta enemyDeathAnimRate,x ; death animation updated every 3 frames lda #0 sta enemyDeathSound,x sta spaceJockeyFiring ; clear the space jockey missile data sta spaceJockeyMissileHoriz sta spaceJockeyShotFreq jsr IncrementScore lda #$0F sta AUDC0 .moveSpaceJockeyMissileVert lda spaceJockeyFiring ; check to see if Space Jockey is firing bpl RollingMountainAnimation ; if not then skip to moutain rolling lda spaceJockeyMissileVert ; get the vertical position of the missile lsr ; if odd skip to mountain rolling (2LK) bcs RollingMountainAnimation lda gameSelection ; get the game selection lsr ; if even number skip to mountain rolling bcc RollingMountainAnimation lda spaceJockeyVertPos ; get Space Jockey vertical position sbc #SPACE_JOCKEY_MISSILE_OFFSET ; reduce the value by 3 for the new sta spaceJockeyMissileVert ; vertical position of the missile RollingMountainAnimation dec mountainRollingRate bpl .skipMountainRolling lda #MOUNTAIN_ROLL_RATE sta mountainRollingRate ldx #2 .rollMoutainLoop lda pf0Data,x and #$10 adc #$FE ror pf2Data,x rol pf1Data,x ror pf0Data,x dex bpl .rollMoutainLoop .skipMountainRolling lda spaceJockeyHit ; check to see if the space jockey was hit bpl .spaceJockeyAnimation ; branch if not dec spaceJockDeathAnimRate ; reduce the death animation frame count bpl .moveEnemies ; branch if not negative ldx #$02 stx spaceJockDeathAnimRate ; reset the death animation frame rate ldx spaceJockeyDeathSound inx ; increment the death sound frequency cpx #16 bcc .determineDeathAnimation clc ldy #0 sty AUDC1 ; clear the channel (turn off death sound) ldx #NUMBER_OF_GROUPS-1 .clearEnemiesLoop sty enemyVelocity,x ; set enemy velocity to 0 to move faster lda enemyAttributes,x ; get the enemy attribute value bcs .clearNextEnemy ; clear next enemy if not reached left lsr ; moves D0 to carry .clearNextEnemy dex bpl .clearEnemiesLoop bcs .moveEnemies dec numberOfLives ; reduce the number of lives bpl .skipGameOver ; if still positive then game not over sty gameState ; show that game is over (y = 0) .skipGameOver sty spaceJockeyHit ldx #19 jsr InitLoop sty AUDC1 bmi .moveEnemies .determineDeathAnimation cpx #5 bcs .playSpaceJockeyDeathSound ldy #SPACE_JOCKEY_HEIGHT-2 lda #>SpaceJockeyDeathSprites sta deathAnimPointer+1 lda SpaceJockeyDeathAnimTable-1,x sta deathAnimPointer .storeDeathAnimationGraphics lda (deathAnimPointer),y sta spaceJockeyGraphics,y dey bpl .storeDeathAnimationGraphics .playSpaceJockeyDeathSound stx spaceJockeyDeathSound ; save the current death sound value txa sta AUDF1 ; set the death sound frequency eor #$FF sta AUDV1 ; set the death sound volume lda #8 sta AUDC1 ; set the death sound channel .moveEnemies jmp MoveEnemies .spaceJockeyAnimation ldy #%11010101 lda #$08 and frameCount beq .setSpaceJockeyWindowGraphics ldy #%10101011 .setSpaceJockeyWindowGraphics sty spaceJockeyGraphics+2 ldy #SIREN_LIGHT+14 lda #$10 and frameCount beq .setSirenLightColor ldy #SIREN_LIGHT+6 .setSirenLightColor sty spaceJockeyColors+5 MoveEnemies SUBROUTINE ldx #NUMBER_OF_GROUPS-1 .moveEnemyLoop lda enemyAttributes,x ; get the enemy's attributes lsr ; shift the "moving" flag to carry bcs .determineEnemyMovement ; check to move enemy (present on screen) jmp CheckToLaunchNewAttack .determineEnemyMovement dec enemyMotion,x bpl .checkForEnemyShot lda enemyVelocity,x sta enemyMotion,x lda objectIds,x ; get the object id cmp #ID_HOUSE ; check to see if it can move vertically bcs .moveEnemyLeft ; if not then move the object toward the ; space jockey lda gameSelection ; get the game selection and #OPTION_ENEMY_MOVE_VERT ; check if the objects can move vertically beq .moveEnemyLeft ; ; enemy can move randomly up or down (game options 3,4,7,8,11,12,15,16) ; lda randomSeed,x cmp #208 bcs .moveEnemyLeft bpl .moveEnemyUp .moveEnemyDown dec enemyVerticalPos,x lda EnemyVerticalFloor,x cmp enemyVerticalPos,x bcc .moveEnemyLeft bcs .setEnemyVerticalPosition .moveEnemyUp inc enemyVerticalPos,x lda EnemyVerticalCeiling,x cmp enemyVerticalPos,x bcs .moveEnemyLeft .setEnemyVerticalPosition sta enemyVerticalPos,x .moveEnemyLeft dec enemyHorizPos,x bne .checkForEnemyShot .removeEnemy lda #NumberFonts sta digitPointer+1,x dex dex lda playerScore,y ; get the value to display and #$0F ; mask the upper nybbles asl ; multiply the value by 8 asl asl adc #NumberFonts sta digitPointer+1,x iny ; next digit dex dex bpl .bcd2DigitLoop ldx #8 ldy #