; Combat for Atari by Larry Wagner ; ; Original disassembly by Harry Dodgson ; Commented further by Nick Bensema (1997) ; Major overhaul by Roger Williams (2002) ; ; My intent in overhauling this classic disassembly is to finish it ; so that the purpose of every instruction, memory location, and ; table is made completely clear. ; ; For some reason the NBCOMBAT file ORG statements all point to ; the region $1000-$1FFF; this would play in a VCS but the cartridge ; .BIN I have is mapped from $F000-$FFFF. This file compiles with ; DASM to an image which differs from this ROM only in the few ; unwritten bytes between the end of data and the startup vectors. ; DASM sets these to zero, typical of unwritten RAM, but in the cart ; they are $FF, typical of unprogrammed PROM. ; ; Thanks to Brian Prescott for pointing me to Joe DeCuir's ; presentation notes, which revealed Atari's original names ; for the main loop toplevel routines and offered some guidance ; on their separation of function. ; ; I have removed some of the breathless intro-to-VCS and historical ; comments. This version assumes a basic familiarity with VCS ; programming, and is meant as a basis for hacking the COMBAT game ; itself. There are plenty of resources outside of this file if ; you don't know how the VCS works. ; ; For reference, as this is rather important when reading the, ; code, here is the game variation matrix (it is not trivially ; obvious how this corresponds to GAMVAR): ; ; Game No. Open Field ; | Straight Missiles | Easy Maze ; | | Guided Missiles | | Complex Maze ; | | | Machine Guns | | | Clouds ; | | | | Direct Hit | | | | ; | | | | | Billiard | | | | ; | | | | | | Hit | | | | ; | | | | | | | | | | ; | | | | | | | | | | ; ;TANK 1 - X - - - X - - - ; 2 - X - - - - X - - ; 3 X - - - - - X - - ; 4 - X - - - - - X - ; 5 X - - - - - - X - ;-------------------------------------------------- ;TANK-PONG 6 - - - X X - X - - ; 7 - - - X X - - X - ; 8 - - - - X X - - - ; 9 - - - - X - X - - ;-------------------------------------------------- ;INVISIBLE TANK 10 - X - - - X - - - ; 11 - X - - - - X - - ;-------------------------------------------------- ;INVISIBLE 12 - - - X X - X - - ;TANK-PONG 13 - - - - X X - - - ; 14 - - - - X - X - - ;-------------------------------------------------- ;BI-PLANE 15 - X - - - - - - X ; 16 X - - - - - - - X ; 17 - - X - - - - - X ; 18 - - X - - X - - - ; 2 vs. 2 19 - X - - - X - - - ; 1 vs. 3 20 X - - - - X - - - ;-------------------------------------------------- ;JET 21 - X - - - - - - X ; 22 X - - - - - - - X ; 23 - X - - - X - - - ; 24 X - - - - X - - - ; 2 vs. 2 25 - X - - - - - - X ; 1 vs. 3 26 - X - - - X - - - ; 2 vs. 2 27 X - - - - X - - - processor 6502 include vcs.h ; RAM is cleared in blocks beginning at various addresses and ; always ending at $A2 (though this isn't the highest address ; used). I have placed \\\/// comments to mark these points. BINvar = $80 ; Master Game Variation Control (binary) ; (When BINvar is reset or incremented, ; BCDvar is reset or BCD-imcremented and ; GAMVAR flag is read from VARMAP+BINvar) BCDvar = $81 ; Game Variation in BCD ; ;\\\/// ; ; $82 thru $85 contain flags built from GAMVAR for quick testing via BIT. ; PF_PONG = $82 ; bit 7 DIS-able playfield flag ; ; bit 6 Pong missiles (bounce off playfield) GUIDED = $83 ; bit 7 = guided missile game ; ; bit 6 = machine gun game BILLIARD = $84 ; Just bit 6 = billiard hit game (missiles can't ; ; hit tank until at least 1 bounce off playfield) GAMSHP = $85 ; Shape of player and game type ; ; 0 = Tank ; ; 1 = Biplane ; ; 2 = Jet Fighter ; CLOCK = $86 ; Master timer inc'd every frame during VSYNC ; ; in NBCOMBAT this was misleadingly labelled GTIMER SHOWSCR = $87 ; Show/hide RIGHT player score (left only is used ; ; to indicate game selection in attract mode) To ; ; inhibit both scores, KLskip is set to $0E vs. $02 GameOn = $88 ; $00=attract mode, $FF=game going on. Bits 7, 1, ; ; and "all" tested in various places. Incorrectly set ; ; to $10 at START, but must not be a problem :-) ;\\\/// ; SelDbnce = $89 ; Select Switch Debounce flag which prevents a ; ; hold-down from registering as 60 presses/second StirTimer = $8A ; Bit 0 = identity of loser during tank stir ; ; Bits 2-7 = countdown timer controlling stir after loss Vtemp = $8B ; Temp storage for current velocity FwdTimer = $8D ; FwdTimer must count $F0 to $00 between changes in ; thru $8E ; forward motion control; also used for momentum pacing ; $8F ; ... ; thru $90 ; seem to be reserved too (missiles?) but not used LastTurn = $91 ; Flag indicating direction of last turn, used ; thru $92 ; to inhibit whipsaw direction changes (may ; ; have been intended for rotational momentum) TurnTimer = $93 ; Countdown timer between 22.5-degree rotates ; thru $94 ; for P0 and P1 DIRECTN = $95 ; Players and missiles' current bearing. ; thru $98 ; (4 bytes P0,P1,M0,M1) MisLife = $99 ; Missile Lifetime down-counters ; thru $9A BounceCount = $9B ; (1) Billiard bounced-once flag, via any value other ; thru $9C ; than $1F init value; (2) Pong sound tone freq, which ; ; ascends in tone as BounceCount DECed with each bounce MxPFcount = $9D ; During Pong bounce, count of collision duration in ; thru $9E ; frames, used to try different heading adjustments ; ; until "desired" reflection achieved AltSnd = $9F ; Alt Player Sound flag/counter; 0=normal motor sound, ; thru $A0 ; else counts up to $04 to time Pong sound SCORE = $A1 ; Player scores in BCD. ; thru $A2 ; ; ;\\\/// Addresses beyond here aren't ever cleared by ClearMem. ; GAMVAR = $A3 ; Game Variation bitwise descriptor via VARMAP TankY0 = $A4 ; Tank 0's Y-position TankY1 = $A5 ; and tank 1 MissileY0 = $A6 ; Missile 0's Y-position MissileY1 = $A7 ; and missile 1 MVadjA = $A8 ; First-half FwdTimer-Velocity adjustments ; thru $A9 ; for each player. By an amazing coincidence ; ; in all games these seem to be the same as ; ; the *current* velocity. MVadjB = $AA ; Second-half FwdTimer-Velocity adjustments, ; thru $AB ; which seem to be the same as the *final* velocity. MPace = $AC ; Pacing counter; never initialized! INC'd and ; ; masked to pace certain actions slower than ; thru $AF ; once/frame, for each player & missile XOFFS = $B0 ; X-offset for pending Hmove. XoffBase = $B1 ; $0, $10, $20, or $30 offset into X-offset tbl OldMisDir = $B2 ; Missile bearing before a Pong-bounce began ; thru $B3 ; ScanLine = $B4 ; Current scanline on the playfield. LORES = $B5 ; lo-res indirect addresses. ; thru $BA ; 6 bytes / 3 16-bit pointers SHAPES = $BB ; Pointer to player sprites HIRES = $BD ; Hi-res (sprite) shape buffer. Left player's shape ; thru $CC ; stored in even bytes, right player's in odd. TEMP1 = $D1 ; Temp storage for several quick save/math operations TEMP = $D2 ; "score conversion temporary" TMPSTK = $D3 ; Temporary storage for stack. DIFSWCH = $D5 ; Hold & shift temp for console switches Color0 = $D6 ; Colors loaded from ColorTbl for player 0 and 1 Color1 = $D7 ; These may be changed e.g. invisible tanks XColor0 = $D8 ; Repeated P0 and P1 Colors for reference, used XColor1 = $D9 ; to restore ColorX after a change ColorPF = $DA ; BK and PF colors loaded in same block as XColorX. ColorBK = $DB ; Never changed, so no reference versions are kept. KLskip = $DC ; Kernal lines to skip before score, or main w/o score ; ; (Also used in Kernal as flag whether to show score) GameTimer = $DD ; Master game timer set to $80 when game starts, ; ; incremented until overflow at $FF-->$00 ends game ; ; Bit 7 indicates game in play, also used w/GameOn to ; ; flash score. During attract mode GameTimer is used ; ; to cycle colors; this is OK since it only assumes ; ; its game-timing function if GameOn != $00. NUMG0 = $DE ; Storage for current byte NUMG1 = $DF ; of score number graphics. SCROFF = $E0 ; Score pattern offsets (4 bytes) ; thru $E3 ; lo nibble 0, lo 1, hi 0, hi 1 COLcount = $E4 ; Counter keeps tank-tank and tank-PF collisions from ; thru $E5 ; affecting a stationary tank's bearing unless the ; ; collision lasts at least 4 cycles ; StkTop = $FF ; Top of stack (which IS used, at least 8 bytes) ; ; So much for the RAM. Here's the ROM: org $F000 START SEI ; Disable interrupts CLD ; Clear decimal bit LDX #StkTop TXS ; Init Stack LDX #$5D JSR ClearMem ; zero out RAM except address $A2 LDA #$10 ; STA SWCHB+1 ; Port B data direction register and STA GameOn ; GameOn (tho not quite a valid value)... JSR ClrGam ; clear game RAM $82-$A2 ; MLOOP JSR VCNTRL ; Generate a VSYNC and begin VBLANK ; ; VBLANK logic: ; JSR GSGRCK ; Parse console switches JSR LDSTEL ; Load Stella Registers JSR CHKSW ; Check Joystick Switches JSR COLIS ; Check Collision Registers JSR STPMPL ; Setup Player, Missile Motion JSR ROT ; Rotate Sprites JSR SCROT ; Calculate Score Offsets ; JSR VOUT ; do the Kernal (trashes the stack ptr, ; but then restores it because it IS JMP MLOOP ; used when we reiterate this loop) ; ; ------------------------------------------------------------ ; ; Vertical CoNTRoL ; ; Vertical sync, basic frame-start housekeeping ; VCNTRL INC CLOCK ; Master frame count timer STA HMCLR ; Clear horizontal move registers. LDA #2 ; Get this ready... STA WSYNC ; for start of next line... STA VBLANK ; Start vertical blank. STA WSYNC STA WSYNC ; and do three lines STA WSYNC STA VSYNC ; Now start vertical sync STA WSYNC STA WSYNC ; and do three lines LDA #0 ; get this ready STA WSYNC STA VSYNC ; End of vertical sync pulse LDA #43 ; And set VBLANK timer STA TIM64T ; with 64 clock interval. RTS ; ; ------------------------------------------------------------ ; ; Video OUT -- THE KERNAL ; ; We start with the score, then we render the playfield, players, ; and missiles simultaneously. All in all, an average day for a VCS. ; VOUT LDA #$20 STA ScanLine ; We're assuming scanline $20. STA WSYNC STA HMOVE ; Move sprites horizontally. VOUT_VB LDA INTIM BNE VOUT_VB ; Wait for INTIM to time-out. STA WSYNC STA CXCLR ; Clear collision latches STA VBLANK ; End vertical blank TSX STX TMPSTK ; Save stack pointer LDA #$02 STA CTRLPF ; Double, instead of reflect. LDX KLskip Vskip1 STA WSYNC ; Skip a few scanlines... DEX BNE Vskip1 LDA KLskip CMP #$0E ; "No Score" value of KLskip BEQ Vmain ; ; KLskip is set as such so that when the score is ; to be displayed, it waits for just the right time ; to start drawing the score, but if the score is ; not to be displayed, as when the score flashes ; signifying "time's almost up", it waits for just ; the right time to start drawing the rest of the ; screen. ; ; Draw the score: ; LDX #$05 ; Score is five bytes high. LDA #$00 ; Clear number graphics. STA NUMG0 ; They won't be calculated yet, STA NUMG1 ; but first time through the loop ; the game will try to draw with ; them anyway. VSCOR STA WSYNC ; Start with a fresh scanline. LDA NUMG0 ; Take last scanline's left score, STA PF1 ; and recycle it, ; ; Here, we begin drawing the next scanline's ; left score, as the electron beam moves towards ; the right score's position in this scanline. ; LDY SCROFF+2 LDA NUMBERS,Y ; Get left digit. AND #$F0 STA NUMG0 LDY SCROFF LDA NUMBERS,Y ; Get right digit. AND #$0F ORA NUMG0 STA NUMG0 ; Left score is ready to ship. LDA NUMG1 ; Take last scanline's right score, STA PF1 ; and recycle it. LDY SCROFF+3 LDA NUMBERS,Y ; Left digit... AND #$F0 STA NUMG1 LDY SCROFF+1 LDA NUMBERS,Y ; right digit... AND SHOWSCR ; ; Now, we use our fresh, new score graphics in this next scanline. ; STA WSYNC ; *COUNT* ORA NUMG1 ;Finish calculating (0) +3 STA NUMG1 ;right score. (3) +3 LDA NUMG0 ; (6) +3 STA PF1 ; *9* +3 ; ; We use this time to check whether we're at the end of our loop. ; DEX ; (12)+2 BMI Vmain ; (14)+2 No Branch ; ; If so, we're out of here. Don't worry, the score will be ; cleared immediately, so nobody will know that we've gone ; past five bytes and are displaying garbage. ; INC SCROFF ; (16)+5 INC SCROFF+2 ; Get ready to draw the next INC SCROFF+1 ; line of the byte. INC SCROFF+3 LDA NUMG1 STA PF1 ; Right score is in place. JMP VSCOR ; Go to next scanline, ; ; Main Kernal Display loop for the game itself ; Vmain LDA #$00 ; Inner Display Loop STA PF1 ; Clear the score. STA WSYNC LDA #$05 STA CTRLPF ; Reflecting playfield. LDA Color0 STA COLUP0 ; How often must THIS be done? LDA Color1 STA COLUP1 Vfield LDX #$1E ; Very Sneaky - TXS ; Set stack to missile registers SEC ; ; This yields which line of player 0 to draw. ; LDA TankY0 SBC ScanLine ; A=TankY0-ScanLine AND #$FE ; Force an even number TAX ; Only sixteen bytes of AND #$F0 ; sprite memory, so... BEQ VdoTank ; If not valid, LDA #$00 ; blank the tank. BEQ VnoTank ; (unconditional branch) VdoTank LDA HIRES,X ; Else, load the appropriate byte. VnoTank STA WSYNC ; ----END OF ONE LINE---- STA GRP0 ; Just for player 0. ; ; The infamous Combat Stack Trick: ; ; Keep in mind that at this point, the stack pointer ; is set to the missile registers, and the "zero-result" ; bit of the P register is the same at the bit ENAM0/1 ; looks at. ; LDA MissileY1 EOR ScanLine AND #$FE PHP ; This turns the missle 1 on/off LDA MissileY0 EOR ScanLine AND #$FE PHP ; This turns the missle 0 on/off ; ; We've got the missile taken care of. ; Now let's see which line of the playfield to draw. ; LDA ScanLine BPL VvRefl ; If on the bottom half of the screen, EOR #$F8 ; reverse direction so we can mirror. VvRefl CMP #$20 BCC VfDone ; Branch if at bottom. LSR LSR LSR ; Divide by eight, TAY ; and stow it in the Y-register. ; ; By now, the electron beam is already at the next ; scanline, so we don't have to do a STA WSYNC. ; ; This yields which line of Tank 1 to draw. ; VfDone LDA TankY1 ; TankY1 is other player's position. SEC SBC ScanLine ; A=TankY1 - ScanLine INC ScanLine ; Increment the loop. NOP ORA #$01 ; Add bit 0, force odd number. TAX ; AND #$F0 ; There are only sixteen bytes of BEQ VdoT1 ; sprite memory, so... LDA #$00 ; If tank is not ready, blank it. BEQ VnoT1 VdoT1 LDA HIRES,X ; Else, draw the tank VnoT1 BIT PF_PONG STA GRP1 BMI VnoPF ; If PF_PONG bit 7 set, don't write PF LDA (LORES),Y ; (this means game variation has blank STA PF0 ; background) LDA (LORES+2),Y STA PF1 LDA (LORES+4),Y STA PF2 VnoPF INC ScanLine ; One more up in the loop. LDA ScanLine EOR #$EC ; When we've reached the $ECth line, BNE Vfield ; we've had enough. LDX TMPSTK ; Restore stack pointer, which is TXS ; is used for calls in main game loop STA ENAM0 ; Clear a bunch of registers. STA ENAM1 STA GRP0 STA GRP1 STA GRP0 ; In case GRP0 isn't COMPLETELY zeroed. STA PF0 STA PF1 STA PF2 RTS ; ------------------------------------------------------------ ; ; Game Select Game Reset ChecK ; ; Executed immediately after VCNTRL, this subroutine parses all ; the console switches. ; GSGRCK ; LDA SWCHB ; Start/Reset button.... LSR ; Shove bit 0 into carry flag, BCS NoNewGM ; and if it's pushed... ; ; Start a new game. ; LDA #$0F STA SHOWSCR ; Show right score. LDA #$FF ; Set all bits STA GameOn ; in GameOn. LDA #$80 STA GameTimer ; and bit 7 of GameTimer (this is not too ; significant, as GameTimer rollover is ; only checked if GameOn<>$00) LDX #$E6 JSR ClearMem ; zero out $89 thru $A2 BEQ ResetField ; Unconditional branch ; NoNewGM LDY #$02 ; Assume score to be drawn LDA GameTimer ; If game in play (GameOn=$FF) AND AND GameOn ; GameTimer < 7/8 finished @ $F0, CMP #$F0 ; draw the score unconditionally. BCC SCdrawn LDA CLOCK ; CLOCK used to flash score near end AND #$30 ; of play, note the peripheral synchronization BNE SCdrawn ; with GameTimer's timing of the game, which ; always ends when CLOCK & $3F = 0. CLOCK ; is used here because the score blink ; off duty cycle is a too quick for ; GameTimer to handle, being about 1/3 sec. LDY #$0E ; Set this for no score SCdrawn STY KLskip ; where the Kernal will find it LDA CLOCK AND #$3F ; CLOCK also used to slow debounce reset BNE ChkSel ; ; GameTimer is incremented and SelDbnce reset when ; CLOCK & $3F = 0. This occurs 1 frame out of 64 or ; about once/second. Thus the game is 128*64 frames ; or about 2 minutes long. ; STA SelDbnce ; Reset Select Debounce Flag. This is ; what keeps incrementing the selection ; if you hold Select down for a long time. INC GameTimer ; increment the Main Game ~1-sec Timer. BNE ChkSel ; if GameTimer rolls over, STA GameOn ; zero GameOn -- game over ; ChkSel LDA SWCHB ; Select button??? AND #$02 BEQ SelDown STA SelDbnce ; Set flag: Sel has not been down BNE CS_RTS ; Unconditional branch ; SelDown BIT SelDbnce ; If Sel has been down, BMI CS_RTS ; don't select a new game. ; INC BINvar ; SELECT: Go to next game. ClrGam LDX #$DF ; Clear data from current game ($82-$A2) ClrGRST JSR ClearMem LDA #$FF STA SelDbnce ; Set flag: Sel has been down. LDY BINvar LDA VARMAP,Y ; Get feature bits for this variation. STA GAMVAR EOR #$FF ; #$FF signifies end of variations BNE SelGO ; Not at end yet, set up new game LDX #$DD ; Clear $80-$A2; resets BINvar, BCDvar BNE ClrGRST ; so we start over. BNE is unconditional. ; SelGO LDA BCDvar ; Since we have incremented BINvar, we SED ; must increment BCDvar in BCD to keep CLC ; it in sync. Note BCDvar is actually ADC #1 ; BinVar+1, since it's incremented when STA BCDvar ; we reset but don't increment BINvar. STA SCORE ; Display variation as score 0 CLD BIT GAMVAR ; GAMSHP was reset at ClrGam... BPL ResetField ; if this is a plane game, INC GAMSHP ; increase GAMSHP. BVC ResetField ; if this is a jet game, INC GAMSHP ; increase GAMSHP further still. ; ; Branches here when game is started, too. ; ResetField JSR InitPF ; ; Assuming plane game for now, we set the right player ; at a slightly higher position than the left player, ; and the position of the right player is irrelevant. ; LDA #50 STA TankY1 LDA #134 STA TankY0 BIT GAMVAR ; Check to see if it is a tank game. BMI CS_RTS ; Nope, bail. ; It is a tank game, so STA TankY1 ; Right tank has same Y value, STA RESP1 ; and tank is at opposite side. LDA #$08 STA DIRECTN+1 ; and right player faces left. LDA #$20 STA HMP0 STA HMP1 STA WSYNC STA HMOVE CS_RTS RTS ; ------------------------------------------------------------ ; ; SCoRe OffseT ; ; Convert BCD scores to score pattern offset. ; This involves the horrible, horrible implications ; involved in multiplying by five. ; ; If it weren't for the geniuses at NMOS using BCD, ; this routine would be a nightmare. ; ; This routine starts with Player 1, writes bytes 1 & 3 of ; the table, then decrements X to write bytes 0 & 2 for P0. ; SCROT LDX #$01 SCROT0 LDA SCORE,X AND #$0F ; Lo nibble STA TEMP ASL ; *2 ASL ; *4 CLC ADC TEMP ; + original * 1 = original * 5 STA SCROFF,X LDA SCORE,X AND #$F0 ; Repeat for hi nibble. Starts *16 LSR ; *8 LSR ; *4 STA TEMP LSR ; *2 LSR ; *1 CLC ADC TEMP ; + (*4) = original * 5 STA SCROFF+2,X DEX BPL SCROT0 ;Decrement & repeat once for P0 RTS ; ------------------------------------------------------------ ; ; SeTuP Motion for PLayers ; ; Apply horizontal and vertical motion ; STPMPL BIT GUIDED BVC STPnoMG ; Branch if not machine gun game. LDA #$30 ; (Machine gun bullets move faster) BPL STPMG ; Unconditional JMP. STPnoMG LDA #$20 STPMG STA XoffBase ; $30=machine gun, $20=normal LDX #$03 JSR STPM ; Do the honors for X=3, Missile 1 DEX JSR STPM ; Now X=2, M0 ; DEX ; Now X=1, P1; we will DEX and loop STPnext LDA FwdTimer,X ; back to run this code block again AND #$08 ; with X=0 for P0. LSR ; (to 4) This bit on means FwdTimer has LSR ; (to 2) run half of the FwdTimer period ; ($F0 to $FF and roll) ; This bit will index MVadjA or MVadjB STX TEMP1 ; Player # --> TEMP1 CLC ADC TEMP1 TAY ; Player # + FwdTimer half done*2 --> Y LDA MVadjA,Y ; And retrieve MVadjA or MVadjB via Y ; SEC ; assume bit 7 on BMI STP7set ; OK, it is CLC ; whoops, backtrack STP7set ROL ; carry=bit 7, now ROL; net effect is to ; ; rotate left inserting duplicate MSB STA MVadjA,Y ; instead of original Carry, and save it BCC STPnoV ; Skip next code block if bit wasn't 1 ; LDA MPace,X ; Tweak velocity by changing XoffBase AND #$01 ; but only every other time we get here ASL ASL ASL ASL STA XoffBase ; XoffBase=$0 or $10 via (MPace & 1) << 4 JSR STPM ; Note this is where we INC MPace STPnoV DEX ; Move to _previous_ player. BEQ STPnext ; Stop if about to do player -1. :) RTS ; ; This routine will move both tanks and missiles. ; Special cases are made for missiles, which are ; otherwise treated as players 2 and 3. ; ; It doesn't change the X register, but it does ; utilize it. ; STPM INC MPace,X LDA DIRECTN,X AND #$0F CLC ADC XoffBase ; Pick table offset by game condition TAY LDA Xoffsets,Y ; X-offset by orientation. STA XOFFS ; Store the default HMPV code. BIT PF_PONG BVS STPgo ; Branch if (fast) Pong missiles LDA DIRECTN,X SEC SBC #$02 ; If motion is near X or Y axis, AND #$03 BNE STPgo ; don't apply delay LDA MPace,X ; but if very diagonal, slow a bit by AND #$03 ; moving only 3 of every 4 frames BNE STPgo ; LDA #$08 ; HMPV for no motion X or Y STA XOFFS ; no motion this frame STPgo LDA XOFFS ; ; (This falls through, but PhMove is also called from elsewhere) ; ; Physically move a tank (0,1) or missile (2,3) ; according to the HMPV code in A ; PhMove STA HMP0,X ; Hi nibble sets HMPx horizontal motion AND #$0F ; Lo nibble... SEC SBC #$08 ; less 8 for 2's complement 4-bit... STA $D4 ; (save this offset) CLC ADC TankY0,X ; add to Y-coordinate BIT GAMVAR BMI PhNoTank ; Branch if a plane game. CPX #$02 BCS PhNoWrap ; Branch if moving a tank player PhNoTank CMP #$DB ; Perform vertical wrap-around BCS PhNoWrapTop ; branch if over top (wrap) CMP #$25 BCS PhNoWrap ; branch if over bottom (no wrap) PhNoWrapTop LDA #$D9 ; Assume we wrapped bottom to top BIT $D4 ; Meaning offset was negative BMI PhNoWrap LDA #$28 ; Otherwise, we wrapped top to bottom PhNoWrap STA TankY0,X ; The tank/missile is moved here. CPX #$02 BCS PhnoVD ; Skip if moving a missile. STA VDELP0,X ; Vertical Delay Player X... PhnoVD RTS ; ------------------------------------------------------------ ; ; ROTate player sprites ; ; This subroutine sets up the sprite data for each player by copying ; them into sixteen bytes of RAM. ; ; The X-register starts at $0E plus player number and goes down by two ; each time through the loop, until it hits zero. This way, after calling ; this subroutine twice, every even-numbered byte contains the left player ; shape, and every odd-numbered byte contains the right player shape. Since ; each player is updated every two scanlines, this saves us some math. ; ; Only the first 180 degrees of rotation has been drawn into ROM. In the ; case of the other 180 degrees, this subroutine renders a flipped version ; by doing the following: ; ; 1. It sets the TIA's reflection flag for that player, taking care of ; the horizontal aspect rather easily. ; ; 2. It copies the bytes into memory last-to-first instead of first-to- ; last, using the carry bit as a flag for which to do. ; ROT LDA #$01 ; The LO byte of CLOCK used to AND CLOCK ; select alternate players on TAX ; alternate frames LDA DIRECTN,X STA REFP0,X ; Step 1 taken care of. AND #$0F TAY ; Y = DIRECTN[X] & 0x0F. BIT GUIDED BPL ROTnoGM ; If it's a guided missile game, STY DIRECTN+2,X ; copy player bearings to missile ROTnoGM TXA ; X ^= $0E, EOR #$0E TAX TYA ASL ASL ASL CMP #$3F ; And so step 2 begins... CLC BMI ROTnoFlip ; Branch if <180 deg. SEC EOR #$47 ;The EOR sets bits 0-2, and clears bit 4 ; to subtract 180 degrees from the memory ; pointer, too. ROTnoFlip TAY ; ;Put all the shapes where they ought to be. ; ROTnext LDA (SHAPES),Y STA HIRES,X BCC ROTinc DEY ; Decrement instead of increment DEY ; plus cancel the upcoming INY. ROTinc INY ; More of step 2. DEX DEX ; X-=2. BPL ROTnext ; Do for both, 1 then 0 then stop. RTS ; ------------------------------------------------------------ ; ; CHecK joystick SWitches ; ; If we are in the interval while a loser's tank is stirring, ; he stirs and the winner freezes or goes forward. Otherwise, ; parse the joystick inputs and move the tanks appropriately. ; CHKSW LDA StirTimer ; We must dec StirTimer by 2 SEC ; since bit 0 is identity of SBC #$02 ; the stirree BCC NoStir ; If no tank is exploding, ; parse joystick instead. STA StirTimer CMP #$02 BCC StirRTS ; RTS if tank has ; just finished exploding. AND #$01 ; Stir the LOSER's tank. TAX ;One of these is the tank's bearings. INC DIRECTN,X LDA XColor0,X STA Color0,X LDA StirTimer CMP #$F7 ; We only rush the tank for a BCC NoStirRush ; small part of the stir interval JSR RushTank NoStirRush LDA StirTimer BPL StirRTS ; Don't start decrementing ; volume until halfway through. LSR LSR ; StirTimer scales audio volume LSR ; BoomSnd STA AUDV0,X ; Set explosion sound to volume in A LDA #$08 ; and pitch according to player X STA AUDC0,X LDA AudPitch,X STA AUDF0,X StirRTS RTS ; ; Process joysticks. ; NoStir LDX #$01 ; Start with P1 LDA SWCHB ; Console switches. STA DIFSWCH ; Store switches. Before we return ; via DEX to do P0, we will ASL this ; byte so difficulty bit for working ; player appears in bit 7. LDA SWCHA ; Joysticks. Before we return via ; DEX to do P0, we will reload and ; LSR this 4 times so controls for ; the working player appear in the NextPJS BIT GameOn ; LO nibble. BMI NoFreezeJS ; Branch if game on (via bit 7). LDA #$FF ; Freeze all joystick movement. NoFreezeJS EOR #$FF ; Reverse all bits AND #$0F ; Keep low four bits (working player) ; ; At this point, the joystick's switches are in ; the A-register, with a bit set wherever the ; joystick is pointed. ; ; Bit 0 = up Bit 1 = down ; Bit 2 = left Bit 3 = right ; STA TEMP LDY GAMSHP LDA CtrlBase,Y ; Account for two-dimensional array CLC ADC TEMP TAY LDA CTRLTBL,Y AND #$0F ; Get rotation from CTRLTBL. STA TEMP1 ; Stash it here BEQ NoTurn ; Branch if no turn. CMP LastTurn,X ; If new turn is different direction BNE TurnReset ; from last turn, reset the... NoTurn DEC TurnTimer,X ; ...turn pacing delay and... BNE DoFwdMotion ; ...inhibit turn this interval. TurnReset ; We do turn-wait counts even when STA LastTurn,X ; we aren't turning, for consistency LDA #$0F ; Initial countdown value to delay STA TurnTimer,X ; 22.5-degree turns ; LDA TEMP1 ; Retrieve rotation code CLC ; Turn +/- 22.5-degrees or zero, ADC DIRECTN,X ; per DIRECTN STA DIRECTN,X ; ; For reference, we do get here every frame (~60Hz) during game. ; COMBAT does not change player speed instantaneously; it has ; an elaborate momentum system, which is just barely noticeable ; in the course of game play. ; DoFwdMotion INC FwdTimer,X ; Inc FwdTImer and if it doesn't BMI SkipFwdCtrl ; roll over, don't acknowledge velocity LDA CTRLTBL,Y ; changes yet LSR LSR LSR LSR ; Get forward velocity from CTRLTBL ; ; This is the desired _final_ velocity of the player. If ; it is different from the player's _current_ velocity, we ; won't reach it until the end of the FwdTimer period. ; BIT DIFSWCH BMI FwdPro ; Branch if difficulty="Pro" ; (reduces A and branches back to FwdNorm) FwdNorm STA Vtemp,X ; Stash velocity in Vtemp ASL ; Multiply by two TAY ; Stash in Y. LDA MVtable,Y ; Indexed by velocity * 2, even STA MVadjA,X ; V+MVtable goes to MVadjA+X INY ; Why not LDA MVtable+1,Y? LDA MVtable,Y STA MVadjB,X ; odd V+MVtable goes to MVadjB+X LDA #$F0 ; Initialize FwdTimer STA FwdTimer,X ; (Counts up to $00 before fwd ; ; motion change is final) SkipFwdCtrl JSR ChkVM LDA SWCHA ; Joysticks.. LSR LSR LSR LSR ; Keep bottom four bits (Left Player) ASL DIFSWCH ; Use other difficulty switch. DEX BEQ NextPJS RTS ; FwdPro SEC ; Velocity is in A SBC GAMSHP ; subtract 0/tank, 1/biplane, 2/jet BPL FwdNorm ; Not obvious, but this is unconditional ; ------------------------------------------------------------ ; ; Check invisible tank visibility, missile lifetime expiration; ; read trigger if appropriate and launch a new missile ; ChkVM LDA GAMVAR BMI NoInvis ; Branch if plane game AND #$01 ; check also for bit 0 (invisible). BEQ NoInvis LDA ColorBK ; Make invisible tank invisible STA Color0,X NoInvis LDA MisLife,X BEQ RdTrig ; Branch if no missile in flight LDA XColor0,X ; Reset tank to normal color STA Color0,X LDA MisLife,X ; How long does missile have to go? CMP #$07 BCC MisKill ; Branch to go ahead and kill it BIT DIFSWCH ; Check difficulty BPL MisEZ ; If game is hard, CMP #$1C ; Compare mislife to this BCC MisKill ; and expire it early. MisEZ CMP #$30 ; If MisLife < 30 do motor BCC MotMis ; do motor, not shot sound CMP #$37 ; If MisLife >= 37 BCS MisFly ; do sliding boom sound (shot) BIT GUIDED BVC MisFly ; Branch if machine gun. MisKill LDA #$00 ; Reset missile's life, killing it STA MisLife,X LDA #$FF ; And reset its position ResRTS STA RESMP0,X ; to player. RTS ; ; If game in progress, Read the trigger ; RdTrig BIT GameOn ; Branch if no game on BPL RDnoGame ; (via bit 7 being clear) LDA INPT4,X ; Read Input (Trigger) X. BPL Launch ; unconditional branch -- Launch missile ; RDnoGame JSR MOTORS JMP MisKill MotMis JSR MOTORS JMP MisAge MisFly LDA AltSnd,X BEQ MisBoom JSR MOTORS LDA #$30 STA MisLife,X JMP MisAge ; MisBoom LDA MisLife,X JSR BoomSnd MisAge LDA CLOCK ; Missile aging rate depends on type AND #$03 BEQ MisDec ; Only do this test 3/4 of the time BIT BILLIARD BVS MisDSkp ; branch if Billiard (must bounce before hit) BIT PF_PONG BVC MisDec ; branch if not Pong game (PF_PONG bit 6) AND #$01 ; Upshot of this is, in non-billiard Pong BNE MisDSkp ; game, missiles last about twice as long MisDec DEC MisLife,X ; I'm getting older! MisDSkp LDA #$00 BEQ ResRTS ; Unconditional -- DO NOT Reset missile to tank ; ; (we'd need $02 on to do that) but RTS ; ; Launch a missile ; Launch LDA #$3F STA MisLife,X ; Init MisLife to $3F SEC LDA TankY0,X ; Copy Y-position... Tank Y-position points ; to top of sprite, but missile is launched SBC #$06 ; from its center 6 scanlines down. STA MissileY0,X LDA DIRECTN,X ; Copy player bearing to missile. STA DIRECTN+2,X LDA #$1F STA BounceCount,X ; Init BounceCount to $1F LDA #$00 STA MxPFcount,X ; Reset MxPFcount JMP MisFly ; Proceed w/missile in flight ; ------------------------------------------------------------ ; ; This routine generates engine or Pong sound as appropriate. ; MOTORS LDA AltSnd,X BEQ DOMOTOR ; Pong sound. LDA #$04 STA AUDC0,X LDA #$07 STA AUDV0,X LDA BounceCount,X STA AUDF0,X RTS ; Engine sound. DOMOTOR LDY GAMSHP LDA SNDV,Y AND GameOn ; Kills sound if no game on by ANDing STA AUDV0,X ; volume value w/$00 no-game value LDA SNDC,Y STA AUDC0,X CLC LDA #$00 MOPIT0 DEY ; This loop sets start value for sound BMI MOPIT1 ; pitch based on GAMSHP in Y (tank, ADC #$0C ; biplane, or jet) BPL MOPIT0 MOPIT1 ADC Vtemp,X ; Use saved velocity to adjust TAY ; sound pitch via SNDP table TXA ASL ADC SNDP,Y STA AUDF0,X RTS ; ------------------------------------------------------------ ; ; COLISion check ; ; 150 lines of angel-hair spaghetti code ; ; Check to see whether, during all that drawing, ; a missile hit one of the tanks, or a tank hit ; the wall or the other tank, and if so let ; the consequences fall. ; COLIS LDX #$01 ; Do first for P1, DEX, P0, etc. COLnext LDA CXM0P,X BPL COLnoHit ; No missile collision BIT BILLIARD BVC COLDET ; Not Billiard game, go ahead & do it LDA BounceCount,X CMP #$1F BEQ COLnoHit ; Billiard 1st bounce not satisfied ; ; A touch, a touch! I do confess. ; COLDET INC DIRECTN,X ; Turn both tanks 22.5 degrees. INC DIRECTN+2,X ; ; Increase player's score. A simple INC SCORE,X ; won't do because we're doing it in BCD. ; SED LDA SCORE,X CLC ADC #$01 STA SCORE,X CLD TXA CLC ADC #$FD STA StirTimer ; ; Now StirTimer contains loser's ID in bit 0, ; victor's ID in bit 1, and set bits 2-7. ; Bit 1 ID is never used, and just creates a ; slight, unnoticeable difference in stir time. ; LDA #$FF STA RESMP0 ; Reset both missiles. STA RESMP1 LDA #$00 STA AUDV0,X ; Turn off the victor's engine. STA MisLife ; clear MisLife (no missile) STA $9A ; and 9A. RTS ; ; We didn't just end the game, so we deal with some ; sound and bounce logic ; COLnoHit BIT GAMVAR BPL COLTNK ; Branch if a tank game. JMP COLPD ; Skip this code if NOT a tank game COLTNK LDA AltSnd,X BEQ COLnoAlt CMP #$04 ; See if alt sound has played out INC AltSnd,X ; Increment if it has not BCC COLnoAlt LDA #$00 ; if played out, reset to 0 "no alt sound" STA AltSnd,X COLnoAlt LDA CXM0FB,X ; Missile collision with playfield? BMI COLMPF ; If true, bounce or obliterate... LDA #$00 STA MxPFcount,X ; ...else clear MxPFcount JMP COLTCK ; COLMPF BIT PF_PONG BVC COLMISX ; Branch if not Pong (bit 6 clear) ; LDA MxPFcount,X ; It's Pong, so we bounce BNE COLMPFX ; Branch if collision is already ongoing INC AltSnd,X ; NEW COLLISION, set alt sound flag DEC BounceCount,X LDA DIRECTN+2,X ; First try at reflecting STA OldMisDir,X ; Stash current missile heading EOR #$FF ; reverse heading by complement, STA DIRECTN+2,X ; then increment=additive inverse INC DIRECTN+2,X ; same as subtracting from zero LDA DIRECTN+2,X ; check new heading AND #$03 ; See if it's moving exactly N,S,E, or W BNE COLXY0 INC DIRECTN+2,X ; and add 22.5 degrees if so COLXY0 JMP COLMPFdone ; ; I always wondered how this works. Stella does not know the ; orientation of the wall that was hit, so this is how it ; reflects: ; ; Immediately after a collision, it tries a vertical reflection, ; jiggering the result so that it won't be exactly vertical or ; exactly horizontal. ; ; If this is the next frame (MxPFcount=$01) that failed, so ; we reverse direction 180 degrees to turn it into a horizontal ; reflection. ; ; On MxPfcount=$02 we take no action, since the missile may need ; the cycle to re-emerge from a wall. ; ; On MxPFcount=$03 or higher, we retrieve the original heading and ; turn it 180 degrees, assuming a corner reflection. And we keep ; applying this same bearing until it's out of the #*%@ wall. ; COLMPFX CMP #$01 ; branch if BEQ Rev180 ; exactly 1 previous collision frame CMP #$03 ; branch if BCC COLMPFdone ; less than 3 collision frames BNE COLMPFdone ; or more than three LDA OldMisDir,X ; retrieve pre-bounce missile heading JMP Bump180 ; and reverse it 180 degrees ; ; Exactly 1 previous collision: Do a 180-degree reversal, meaning ; 90 degrees the *other* way from our initial course. ; Rev180 LDA DIRECTN+2,X ; Here to add 180 degrees Bump180 CLC ; Here to add A to missile dir ADC #$08 STA DIRECTN+2,X JMP COLMPFdone ; COLMISX LDA #$01 ; If it's not Pong, we come here and STA MisLife,X ; set the missile's life to 1 to kill it. ; COLMPFdone ; When we're done, increase collision INC MxPFcount,X ; frame count & move on. ; ; Check for tank collisions ; COLTCK LDA CXP0FB,X BMI COLTW ; check if tank collided with a wall. LDA CXPPMM ; check for a tank-tank collision. BPL COLTCLR ; branch if NO tank collisions at all COLTW LDA StirTimer ; See if we are stirring a tank CMP #$02 BCC COLTnk1 ; No, branch & block JSR RushTank ; We are stirring, send it scooting ; COLTCLR LDA #$03 ; No tank collision, reset counter STA COLcount,X BNE COLPD ; unconditional branch, player done ; COLTnk1 DEC COLcount,X ; Tank colliding BMI COLbonk ; COLcount rolled, ignore collision LDA Vtemp,X BEQ COLPD ; No boink if velocity=0, player done BNE COLreverse ; else skip INC, needed for elsewhere ; COLbonk INC DIRECTN,X ; Jigger direction 22.5 for disorientation COLreverse LDA DIRECTN,X CLC ADC #$08 ; Add 180 degrees to direction JSR BumpTank ; to bump tank back ; ; COLIS Player Done ; COLPD DEX BMI COLrts ;Return if X<0. JMP COLnext ;Else do the other player COLrts RTS ; ; Bump the tank in the direction ; the other player's missile is moving ; RushTank TXA EOR #$01 ; Get OTHER player # TAY ; in Y LDA DIRECTN+2,Y ; OTHER player Missile's Direction ; ; Bump the tank in the direction of a standard ; 22.5-degree bearing code ; BumpTank AND #$0F TAY LDA HDGTBL,Y ;Nove JSR PhMove ;Move object in that direction. LDA #$00 STA MVadjA,X STA MVadjB,X STA FwdTimer,X ;Stop it dead in its tracks.... LDA XColor0,X STA Color0,X RTS ; ------------------------------------------------------------ ; ; This was probably a toplevel routine early in development, ; but ended up getting called from GSGRCK. It sets everything ; up to draw the playfield based on the current game selection. ; InitPF LDX GAMSHP ; 0=tank, 1=biplane, 2=jet LDA SPRLO,X ; Set up base pointer to all STA SHAPES ; sprite shapes which will LDA SPRHI,X ; be used in this game. STA SHAPES+1 ; LDA GAMVAR ; Now set up PF_PONG and playfield type LSR LSR AND #$03 ; bits 0,1=maze (playfield) type. TAX ; send it to X. LDA GAMVAR BPL IFgo ; Branch not plane game, PF_PONG=GAMVAR AND #$08 ; Test for clouds BEQ IF80 ; Branch if no clouds LDX #$03 ; change "maze type" in X to 3 ("clouds") BPL IFskip ; Unconditional skip to next test, ; leaving PF_PONG set to 0. IF80 LDA #$80 ; Change PF_PONG to #$80 ; (enable playfield, no Pong) IFgo STA PF_PONG ; store GAMVAR or #$80 in PF_PONG. IFskip LDA GAMVAR ; Next test.. ASL ASL ; Do this again.... BIT GAMVAR BMI IFnoPlane ; Branch if a plane game. STA WSYNC ; This MUST be something that dropped ; through the cracks, there is NO reason! STA BILLIARD ; Store GAMVAR*4 in 84 (bit 6 = Billiard Hit) AND #$80 ; IF it's a tank game. IFnoPlane STA GUIDED ; set guided missile flag. ; ; GUIDED is ZERO if a tank game ; it is negative if a guided missile game, ; it is overflowed if a machine gun game. ; (Inapplicable in tank games, hence the ; previous branch trick) ; LDA #>PF0_0 ; Store page of first PF map STA LORES+1 ; as high order byte STA LORES+3 ; for all of these pointers, STA LORES+5 ; 'cause that's where it is. ; ; Store the proper offsets for each column of ; playfield from the vectors given ; LDA PLFPNT,X STA RESP0 ; Reset player 0 while we're at it. STA LORES LDA PLFPNT+4,X STA LORES+2 LDA PLFPNT+8,X STA LORES+4 RTS ; ------------------------------------------------------------ ; ; LoaD STELla ; ; Set the number and size of player sprites, color, and ; disable the joysticks if game is not in play ; LDSTEL LDA GAMVAR AND #$87 BMI LDmult ; ; If bit 7 is set, we are playing with one or more ; planes. If not, well, we can only have one tank, ; so... ; LDA #$00 LDmult ASL TAX LDA WIDTHS,X ; The TIA's NUSIZ registers make STA NUSIZ0 ; it as easy to play with two or LDA WIDTHS+1,X ; three planes as it is for one STA NUSIZ1 ; freakin' huge bomber. LDA GAMVAR AND #$C0 LSR LSR LSR LSR ; Our hardware is now in bits 3 and 2. TAY ; Of the Y-register. ; ; Render joysticks immobile if game not in play, and ; select player and field colors according to Y ; LDA GameOn ; Enable joysticks via bit 1 STA SWCHB ; of $FF game-on value EOR #$FF ; now $FF=no game, $00=game on AND GameTimer ; Cycle tank colors only when NO STA TEMP1 ; game on (attract mode) LDX #$FF LDA SWCHB AND #$08 ; Color/BW switch BNE LDcolor ; Branch if set to Color LDY #$10 ; Force B&W colors LDX #$0F LDcolor STX TEMP LDX #$03 ; We loop 3 times to get 4 values LDcol0 LDA ColorTbl,Y EOR TEMP1 ; Apply color-cycle if no game on AND TEMP ; Apply B&W massage STA COLUP0,X ; Color the real item. STA Color0,X ; Color the virtual item. This can ; be changd, e.g. invisible tanks STA XColor0,X ; Color the deep virtual item. This ; is used to restore ColorX. INY DEX BPL LDcol0 RTS ; ; ------------------------------------------------------------ ; ; Zero out zero-page memory starting with ($A3+X) MOD $100, ; through $A2 wrapping around at $100. ; ; Calling with: ; X=$5D will clear $00-$A2 ; X=$DD will clear $80-$A2 ; X=$DF will clear $82-$A2 ; X=$E6 will clear $89-$A2 ; ; Returns with zero bit set. ; ClearMem LDA #$00 ClrLoop INX STA $A2,X BNE ClrLoop ;Continue until X rolls over. RTS ; Patterns for numbers ; NUMBERS .byte $0E ; | XXX | $F5C5 Leading zero is not drawn .byte $0A ; | X X | $F5C6 because it's never used. .byte $0A ; | X X | $F5C7 .byte $0A ; | X X | $F5C8 .byte $0E ; | XXX | $F5C9 .byte $22 ; | X X | $F5CA .byte $22 ; | X X | $F5CB .byte $22 ; | X X | $F5CC .byte $22 ; | X X | $F5CD .byte $22 ; | X X | $F5CE .byte $EE ; |XXX XXX | $F5CF .byte $22 ; | X X | $F5D0 .byte $EE ; |XXX XXX | $F5D1 .byte $88 ; |X X | $F5D2 .byte $EE ; |XXX XXX | $F5D3 .byte $EE ; |XXX XXX | $F5D4 .byte $22 ; | X X | $F5D5 .byte $66 ; | XX XX | $F5D6 .byte $22 ; | X X | $F5D7 .byte $EE ; |XXX XXX | $F5D8 .byte $AA ; |X X X X | $F5D9 .byte $AA ; |X X X X | $F5DA .byte $EE ; |XXX XXX | $F5DB .byte $22 ; | X X | $F5DC .byte $22 ; | X X | $F5DD .byte $EE ; |XXX XXX | $F5DE .byte $88 ; |X X | $F5DF .byte $EE ; |XXX XXX | $F5E0 .byte $22 ; | X X | $F5E1 .byte $EE ; |XXX XXX | $F5E2 .byte $EE ; |XXX XXX | $F5E3 .byte $88 ; |X X | $F5E4 .byte $EE ; |XXX XXX | $F5E5 .byte $AA ; |X X X X | $F5E6 .byte $EE ; |XXX XXX | $F5E7 .byte $EE ; |XXX XXX | $F5E8 .byte $22 ; | X X | $F5E9 .byte $22 ; | X X | $F5EA .byte $22 ; | X X | $F5EB .byte $22 ; | X X | $F5EC .byte $EE ; |XXX XXX | $F5ED .byte $AA ; |X X X X | $F5EE .byte $EE ; |XXX XXX | $F5EF .byte $AA ; |X X X X | $F5F0 .byte $EE ; |XXX XXX | $F5F1 .byte $EE ; |XXX XXX | $F5F2 .byte $AA ; |X X X X | $F5F3 .byte $EE ; |XXX XXX | $F5F4 .byte $22 ; | X X | $F5F5 .byte $EE ; |XXX XXX | $F5F6 ; ; Horizontal and vertical offsets for movement by orientation. ; Basic table is $10 bytes long (22.5-degree increments), but ; XoffBase is added to it to alter for game options. High ; nibble is raw HMPx value for horizontal offset, low nibble ; is vertical offset in scan lines. ; Xoffsets .BYTE $F8 ,$F7 ,$F6 ,$06 ;XoffBase=0 .BYTE $06 ,$06 ,$16 ,$17 .BYTE $18 ,$19 ,$1A ,$0A .BYTE $0A ,$0A ,$FA ,$F9 .BYTE $F8 ,$F7 ,$F6 ,$F6 ;XoffBase=$10 .BYTE $06 ,$16 ,$16 ,$17 .BYTE $18 ,$19 ,$1A ,$1A .BYTE $0A ,$FA ,$FA ,$F9 .BYTE $E8 ,$E6 ,$E4 ,$F4 ;XoffBase=$20 .BYTE $04 ,$14 ,$24 ,$26 ;normal missiles .BYTE $28 ,$2A ,$2C ,$1C .BYTE $0C ,$FC ,$EC ,$EA ; This Xoffsets entry is also used directly for "bumping" ; a player after a hit or to back away from playfield collision ; HDGTBL .BYTE $C8 ,$C4 ,$C0 ,$E0 ;XoffBase=$30 .BYTE $00 ,$20 ,$40 ,$44 ;machine guns, "bump" .BYTE $48 ,$4C ,$4F ,$2F .BYTE $0F ,$EF ,$CF ,$CC ; ; Player velocity momentum adjustments. Table of two-byte ; entries, indexed by player's desired final velocity. Even ; locations go to MVadjA to be applied during the first half of ; the FwdTimer cycle, and odd locations goe to MVadjB to be ; applied during the second half. ; ; During each half, the byte is rotated left one bit; if ; the bit which emerges is 1, XoffBase is tweaked by $10 ; to adjust the velocity for that frame only. Since FwdTimer ; goes through 16 cycles or 2 8-bit halves in its course from, ; $F0 to $00, this gives us a bitwise "adjust this frame" flag ; for each frame in the course of FwdTimer's run. This is ; used to obscure the suddenness of transition from one ; velocity to another. ; ; The adjustment is only done once for each two ON bits ; since the MPace 1 bit is used for the adjustment, and ; MPace is INCed in the same code block that does the ; tweak. The tweak consists of replacing whatever XoffBase ; the final velocity calls for with $10, an intermediate value. ; MVtable .BYTE $00 ,$00 .BYTE $80 ,$80 .BYTE $84 ,$20 .BYTE $88 ,$88 .BYTE $92 ,$48 .BYTE $A4 ,$A4 .BYTE $A9 ,$52 .BYTE $AA ,$AA .BYTE $D5 ,$AA .BYTE $DA ,$DA .BYTE $DB ,$6D .BYTE $EE ,$EE ; ; These are all the sprite shapes. ; The most I suspect any of you will do is ; modify these. And/or the number shapes. ; TankShape .byte $00 ; | | $F64F .byte $FC ; |XXXXXX | $F650 .byte $FC ; |XXXXXX | $F651 .byte $38 ; | XXX | $F652 .byte $3F ; | XXXXXX| $F653 .byte $38 ; | XXX | $F654 .byte $FC ; |XXXXXX | $F655 .byte $FC ; |XXXXXX | $F656 .byte $1C ; | XXX | $F657 .byte $78 ; | XXXX | $F658 .byte $FB ; |XXXXX XX| $F659 .byte $7C ; | XXXXX | $F65A .byte $1C ; | XXX | $F65B .byte $1F ; | XXXXX| $F65C .byte $3E ; | XXXXX | $F65D .byte $18 ; | XX | $F65E .byte $19 ; | XX X| $F65F .byte $3A ; | XXX X | $F660 .byte $7C ; | XXXXX | $F661 .byte $FF ; |XXXXXXXX| $F662 .byte $DF ; |XX XXXXX| $F663 .byte $0E ; | XXX | $F664 .byte $1C ; | XXX | $F665 .byte $18 ; | XX | $F666 .byte $24 ; | X X | $F667 .byte $64 ; | XX X | $F668 .byte $79 ; | XXXX X| $F669 .byte $FF ; |XXXXXXXX| $F66A .byte $FF ; |XXXXXXXX| $F66B .byte $4E ; | X XXX | $F66C .byte $0E ; | XXX | $F66D .byte $04 ; | X | $F66E .byte $08 ; | X | $F66F .byte $08 ; | X | $F670 .byte $6B ; | XX X XX| $F671 .byte $7F ; | XXXXXXX| $F672 .byte $7F ; | XXXXXXX| $F673 .byte $7F ; | XXXXXXX| $F674 .byte $63 ; | XX XX| $F675 .byte $63 ; | XX XX| $F676 .byte $24 ; | X X | $F677 .byte $26 ; | X XX | $F678 .byte $9E ; |X XXXX | $F679 .byte $FF ; |XXXXXXXX| $F67A .byte $FF ; |XXXXXXXX| $F67B .byte $72 ; | XXX X | $F67C .byte $70 ; | XXX | $F67D .byte $20 ; | X | $F67E .byte $98 ; |X XX | $F67F .byte $5C ; | X XXX | $F680 .byte $3E ; | XXXXX | $F681 .byte $FF ; |XXXXXXXX| $F682 .byte $FB ; |XXXXX XX| $F683 .byte $70 ; | XXX | $F684 .byte $38 ; | XXX | $F685 .byte $18 ; | XX | $F686 .byte $38 ; | XXX | $F687 .byte $1E ; | XXXX | $F688 .byte $DF ; |XX XXXXX| $F689 .byte $3E ; | XXXXX | $F68A .byte $38 ; | XXX | $F68B .byte $F8 ; |XXXXX | $F68C .byte $7C ; | XXXXX | $F68D .byte $18 ; | XX | $F68E JetShape .byte $60 ; | XX | $F68F .byte $70 ; | XXX | $F690 .byte $78 ; | XXXX | $F691 .byte $FF ; |XXXXXXXX| $F692 .byte $78 ; | XXXX | $F693 .byte $70 ; | XXX | $F694 .byte $60 ; | XX | $F695 .byte $00 ; | | $F696 .byte $00 ; | | $F697 .byte $C1 ; |XX X| $F698 .byte $FE ; |XXXXXXX | $F699 .byte $7C ; | XXXXX | $F69A .byte $78 ; | XXXX | $F69B .byte $30 ; | XX | $F69C .byte $30 ; | XX | $F69D .byte $30 ; | XX | $F69E .byte $00 ; | | $F69F .byte $03 ; | XX| $F6A0 .byte $06 ; | XX | $F6A1 .byte $FC ; |XXXXXX | $F6A2 .byte $FC ; |XXXXXX | $F6A3 .byte $3C ; | XXXX | $F6A4 .byte $0C ; | XX | $F6A5 .byte $0C ; | XX | $F6A6 .byte $02 ; | X | $F6A7 .byte $04 ; | X | $F6A8 .byte $0C ; | XX | $F6A9 .byte $1C ; | XXX | $F6AA .byte $FC ; |XXXXXX | $F6AB .byte $FC ; |XXXXXX | $F6AC .byte $1E ; | XXXX | $F6AD .byte $06 ; | XX | $F6AE .byte $10 ; | X | $F6AF .byte $10 ; | X | $F6B0 .byte $10 ; | X | $F6B1 .byte $38 ; | XXX | $F6B2 .byte $7C ; | XXXXX | $F6B3 .byte $FE ; |XXXXXXX | $F6B4 .byte $FE ; |XXXXXXX | $F6B5 .byte $10 ; | X | $F6B6 .byte $40 ; | X | $F6B7 .byte $20 ; | X | $F6B8 .byte $30 ; | XX | $F6B9 .byte $38 ; | XXX | $F6BA .byte $3F ; | XXXXXX| $F6BB .byte $3F ; | XXXXXX| $F6BC .byte $78 ; | XXXX | $F6BD .byte $60 ; | XX | $F6BE .byte $40 ; | X | $F6BF .byte $60 ; | XX | $F6C0 .byte $3F ; | XXXXXX| $F6C1 .byte $1F ; | XXXXX| $F6C2 .byte $1E ; | XXXX | $F6C3 .byte $1E ; | XXXX | $F6C4 .byte $18 ; | XX | $F6C5 .byte $18 ; | XX | $F6C6 .byte $00 ; | | $F6C7 .byte $83 ; |X XX| $F6C8 .byte $7F ; | XXXXXXX| $F6C9 .byte $3E ; | XXXXX | $F6CA .byte $1E ; | XXXX | $F6CB .byte $0C ; | XX | $F6CC .byte $0C ; | XX | $F6CD .byte $0C ; | XX | $F6CE PlaneShape .byte $00 ; | | $F6CF .byte $8E ; |X XXX | $F6D0 .byte $84 ; |X X | $F6D1 .byte $FF ; |XXXXXXXX| $F6D2 .byte $FF ; |XXXXXXXX| $F6D3 .byte $04 ; | X | $F6D4 .byte $0E ; | XXX | $F6D5 .byte $00 ; | | $F6D6 .byte $00 ; | | $F6D7 .byte $0E ; | XXX | $F6D8 .byte $04 ; | X | $F6D9 .byte $8F ; |X XXXX| $F6DA .byte $7F ; | XXXXXXX| $F6DB .byte $72 ; | XXX X | $F6DC .byte $07 ; | XXX| $F6DD .byte $00 ; | | $F6DE .byte $10 ; | X | $F6DF .byte $36 ; | XX XX | $F6E0 .byte $2E ; | X XXX | $F6E1 .byte $0C ; | XX | $F6E2 .byte $1F ; | XXXXX| $F6E3 .byte $B2 ; |X XX X | $F6E4 .byte $E0 ; |XXX | $F6E5 .byte $40 ; | X | $F6E6 .byte $24 ; | X X | $F6E7 .byte $2C ; | X XX | $F6E8 .byte $5D ; | X XXX X| $F6E9 .byte $1A ; | XX X | $F6EA .byte $1A ; | XX X | $F6EB .byte $30 ; | XX | $F6EC .byte $F0 ; |XXXX | $F6ED .byte $60 ; | XX | $F6EE .byte $18 ; | XX | $F6EF .byte $5A ; | X XX X | $F6F0 .byte $7E ; | XXXXXX | $F6F1 .byte $5A ; | X XX X | $F6F2 .byte $18 ; | XX | $F6F3 .byte $18 ; | XX | $F6F4 .byte $18 ; | XX | $F6F5 .byte $78 ; | XXXX | $F6F6 .byte $34 ; | XX X | $F6F7 .byte $36 ; | XX XX | $F6F8 .byte $5A ; | X XX X | $F6F9 .byte $78 ; | XXXX | $F6FA .byte $2C ; | X XX | $F6FB .byte $0C ; | XX | $F6FC .byte $06 ; | XX | $F6FD .byte $0C ; | XX | $F6FE .byte $08 ; | X | $F6FF .byte $6C ; | XX XX | $F700 .byte $70 ; | XXX | $F701 .byte $B8 ; |X XXX | $F702 .byte $DC ; |XX XXX | $F703 .byte $4E ; | X XXX | $F704 .byte $07 ; | XXX| $F705 .byte $06 ; | XX | $F706 .byte $38 ; | XXX | $F707 .byte $10 ; | X | $F708 .byte $F0 ; |XXXX | $F709 .byte $7C ; | XXXXX | $F70A .byte $4F ; | X XXXX| $F70B .byte $E3 ; |XXX XX| $F70C .byte $02 ; | X | $F70D .byte $00 ; | | $F70E ; ; These are sub-pointers, used to set up the ; two-dimensional array at CTRLTBL. ; CtrlBase .BYTE $00 ,$0B ,$16 ; ; Two-dimensional array, 12x3. ; ; This array specifies what the joystick does ; in each game. Looking at it now the format looks ; like this: ; ; Low nybble = Amount to rotate object (signed) ; $00 = Not at all ; $01 = Clockwise (+1) ; $0F = Counter-clockwise (-1) ; High nybble = Speed to move object (unsigned) ; $00 = Not moving ; $F0 = Warp speed ; ; Observe the $FF's. Notice how indexing out of bounds with impossible ; joystick movements will cause strange behavior. ; ; Tank movement ; UP DOWN (No reverse) CTRLTBL .BYTE $00 ,$10 ,$00 ,$FF .BYTE $01 ,$11 ,$01 ,$FF ;LEFT .BYTE $0F ,$1F ,$0F ;RIGHT ; ; Biplane movement (This is why controls are sideways) ; UP DOWN .BYTE $50 ,$5F ,$51 ,$FF ; .BYTE $30 ,$3F ,$31 ,$FF ;LEFT .BYTE $70 ,$7F ,$71 ;RIGHT ; ; Jet fighter movement ; UP DOWN .BYTE $90 ,$B0 ,$70 ,$FF ; .BYTE $91 ,$B1 ,$71 ,$FF ;LEFT .BYTE $9F ,$BF ,$7F ;RIGHT ; ; ; Sound information for different game types. ; Different tools of destruction make different ; sound. ; ; There is some more data below which looks to ; be other information; different machines at ; different speeds. The pitch table is 3D, ; having 12-entry records for each GAMSHP. ; ; Tanks Biplane, Jet Fighter SNDV .BYTE $08 ,$02 ,$02 ; sound volumes SNDC .BYTE $02 ,$03 ,$08 ; sound types SNDP .BYTE $1D ,$05 ,$00 ; sound pitches indexed by velocity .BYTE $00 ,$00 ,$00 ; for TANKS .BYTE $00 ,$00 ,$00 .BYTE $00 ,$00 ,$00 .BYTE $00 ,$00 ,$1D ; for BIPLANES .BYTE $1D ,$16 ,$16 .BYTE $0F ,$0F ,$00 .BYTE $00 ,$00 ,$00 .BYTE $00 ,$00 ,$00 ; for JETS .BYTE $00 ,$00 ,$12 .BYTE $10 ,$10 ,$0C .BYTE $0C ,$07 ,$07 ; ; Player widths for various plane games. ; Through the miracle of the Atari 2600's NUSIZ ; register, the difference between a 1 vs. 1 game ; and a Bomber vs. 3 game is contained in just ; two bytes. ; WIDTHS .BYTE $00 ,$00 ;1 vs. 1 .BYTE $01 ,$01 ;2 vs. 2 .BYTE $00 ,$03 ;1 vs. 3 .BYTE $27 ,$03 ;Bomber vs. 3 ; Table of color combinations. Each 4 byte entry specifies ; Player 0, Player1, Playfield, and Background colors. ; (By a not-so-odd coincidence, these 4 color registers are ; addressed consecutively in the same order in the TIA.) ; Table is indexed by the high 2 bits of GAMVAR << 2, or ; forced to +$10 if B&W switch selected. ; ColorTbl byte $EA ,$3C ,$82 ,$44 ; 00 = Regular Tanks .byte $32 ,$2C ,$8A ,$DA ; 01 = Tank Pong .byte $80 ,$9C ,$DA ,$3A ; 10 = Jets .byte $64 ,$A8 ,$DA ,$4A ; 11 = Biplanes .byte $08 ,$04 ,$00 ,$0E ; special B&W PF0_0 .byte $F0 ; |XXXX | $F779 .byte $10 ; | X | $F77A .byte $10 ; | X | $F77B .byte $10 ; | X | $F77C .byte $10 ; | X | $F77D .byte $10 ; | X | $F77E .byte $10 ; | X | $F77F .byte $10 ; | X | $F780 .byte $10 ; | X | $F781 .byte $10 ; | X | $F782 .byte $10 ; | X | $F783 .byte $10 ; | X | $F784 PF1_0 .byte $FF ; |XXXXXXXX| $F785 .byte $00 ; | | $F786 .byte $00 ; | | $F787 .byte $00 ; | | $F788 .byte $38 ; | XXX | $F789 .byte $00 ; | | $F78A .byte $00 ; | | $F78B .byte $00 ; | | $F78C .byte $60 ; | XX | $F78D .byte $20 ; | X | $F78E .byte $20 ; | X | $F78F .byte $23 ; | X XX| $F790 PF2_0 .byte $FF ; |XXXXXXXX| $F791 .byte $80 ; |X | $F792 .byte $80 ; |X | $F793 .byte $00 ; | | $F794 .byte $00 ; | | $F795 .byte $00 ; | | $F796 .byte $1C ; | XXX | $F797 .byte $04 ; | X | $F798 .byte $00 ; | | $F799 .byte $00 ; | | $F79A .byte $00 ; | | $F79B .byte $00 ; | | $F79C PF1_1 .byte $FF ; |XXXXXXXX| $F79D PF0_3 .byte $00 ; | | $F79E .byte $00 ; | | $F79F .byte $00 ; | | $F7A0 PF1_3 .byte $00 ; | | $F7A1 .byte $00 ; | | $F7A2 .byte $00 ; | | $F7A3 .byte $00 ; | | $F7A4 .byte $00 ; | | $F7A5 .byte $00 ; | | $F7A6 .byte $00 ; | | $F7A7 .byte $00 ; | | $F7A8 .byte $00 ; | | $F7A9 .byte $07 ; | XXX| $F7AA .byte $1F ; | XXXXX| $F7AB .byte $3F ; | XXXXXX| $F7AC .byte $7F ; | XXXXXXX| $F7AD PF1_2 .byte $FF ; |XXXXXXXX| $F7AE .byte $00 ; | | $F7AF .byte $00 ; | | $F7B0 .byte $00 ; | | $F7B1 .byte $00 ; | | $F7B2 .byte $00 ; | | $F7B3 .byte $00 ; | | $F7B4 .byte $00 ; | | $F7B5 .byte $00 ; | | $F7B6 .byte $60 ; | XX | $F7B7 .byte $20 ; | X | $F7B8 .byte $21 ; | X X| $F7B9 PF2_2 .byte $FF ; |XXXXXXXX| $F7BA .byte $00 ; | | $F7BB .byte $00 ; | | $F7BC .byte $00 ; | | $F7BD .byte $80 ; |X | $F7BE .byte $80 ; |X | $F7BF .byte $80 ; |X | $F7C0 .byte $80 ; |X | $F7C1 .byte $00 ; | | $F7C2 .byte $00 ; | | $F7C3 .byte $00 ; | | $F7C4 .byte $07 ; | XXX| $F7C5 ; Addresses for Sprite Graphics SPRLO .BYTE #TankShape, #>PlaneShape, #>JetShape ; Playfield address data. Kernal timing requires that ; these addresses point 4 bytes before the real start ; of data. ; ; Complex , None ; Simple , Clouds PLFPNT .BYTE #<(PF0_0-4) ,#<(PF0_0-4) .BYTE #<(PF0_0-4) ,#<(PF0_3-4) ;PF0 .BYTE #<(PF1_0-4) ,#<(PF1_1-4) .BYTE #<(PF1_2-4) ,#<(PF1_3-4) ;PF1 .BYTE #<(PF2_0-4) ,#<(PF1_1-4) .BYTE #<(PF2_2-4) ,#<(PF1_3-4) ;PF2 ; Game features, indexed by game number-1. ; ; bits ; 1,0: TANKS PLANES ; X0 = Normal ; X1 = Invisible ; 00 = 1 vs. 1 ; 01 = 2 vs. 2 ; 10 = 3 vs. 1 ; 11 = 3 vs. Giant ; 3,2: 01 = No maze ; 10 = Simple maze ; 00 = Complex maze ; 1X = Clouds ; 0X = No clouds ; 4: 0 = Direct Hit Normal Gun ; 1 = Billiard Hit Machine Gun ; 5: 0 = Straight Missiles ; 1 = Guided Missiles ; 6: 0 = Tanks Jets ; 1 = Tank Pong Biplanes ; 7: 0 = Tank Game ; 1 = Plane Game ; VARMAP .BYTE $24 ;Game 1: 0010 0100 TANK .BYTE $28 ;Game 2: 0010 1000 .BYTE $08 ;Game 3: 0000 1000 .BYTE $20 ;Game 4: 0010 0000 .BYTE $00 ;Game 5: 0000 0000 .BYTE $48 ;Game 6: 0100 1000 TANK PONG .BYTE $40 ;Game 7: 0100 0000 .BYTE $54 ;Game 8: 0101 0100 .BYTE $58 ;Game 9: 0101 1000 .BYTE $25 ;Game 10: 0010 0101 INVISIBLE TANK .BYTE $29 ;Game 11: 0010 1001 .BYTE $49 ;Game 12: 0100 1001 INVISIBLE TANK-PONG .BYTE $55 ;Game 13: 0101 0101 .BYTE $59 ;Game 14: 0101 1001 .BYTE $A8 ;Game 15: 1010 1000 BIPLANE .BYTE $88 ;Game 16: 1000 1000 .BYTE $98 ;Game 17: 1001 1000 .BYTE $90 ;Game 18: 1001 0000 .BYTE $A1 ;Game 19: 1010 0001 .BYTE $83 ;Game 20: 1000 0011 .BYTE $E8 ;Game 21: 1110 1000 JET FIGHTER .BYTE $C8 ;Game 22: 1100 1000 .BYTE $E0 ;Game 23: 1110 0000 .BYTE $C0 ;Game 24: 1100 0000 .BYTE $E9 ;Game 25: 1110 1001 .BYTE $E2 ;Game 26: 1110 0010 .BYTE $C1 ;Game 27: 1100 0001 ; ; $FF to signify end of game variations. ; .BYTE $FF ; If you were changing this to a 4K cart, you'd ; want to change this ORG to $FFFC. You might also ; want to move AudPitch out of the interrupt vector... ; ORG $F7FC .word $f000 ; Reset IRQ ; AudPitch .BYTE $0F, $11 ; Motor sound pitch table by player