-
Notifications
You must be signed in to change notification settings - Fork 34
/
Copy pathcombat.asm
1987 lines (1916 loc) · 67.1 KB
/
combat.asm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
; 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 BMisDec ; 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