├── bg ├── clouds.iscr ├── bg.gpx ├── bg.imap ├── clouds.gpx ├── clouds.imap └── bg.iscr ├── assets ├── grgo.png ├── credits.map ├── credits.scr ├── splash.kla ├── splash.png ├── activisionlogo2.png └── activisionlogo_bands.png ├── font └── digits.bin ├── sprites ├── hero.bin ├── charvan.png ├── enemy1.bin ├── enemy2.bin ├── explosion.bin ├── hud_chopper.bin └── bullet.bin ├── radar ├── mountains.gpx └── mountains.imap ├── src ├── ChopperCommand.prg ├── hud.asm ├── sfx.asm ├── splash.asm ├── hero.asm ├── camera.asm ├── misc.asm ├── weapons.asm ├── controls.asm ├── radar.asm ├── enemies.asm └── game.asm ├── sid ├── Chopper_Command_C64.sid └── Chopper_Command_C64.sng ├── README.md ├── LICENSE.md └── MANUAL.txt /bg/clouds.iscr: -------------------------------------------------------------------------------- 1 |  2 |   -------------------------------------------------------------------------------- /bg/bg.gpx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/bg/bg.gpx -------------------------------------------------------------------------------- /bg/bg.imap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/bg/bg.imap -------------------------------------------------------------------------------- /bg/clouds.gpx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/bg/clouds.gpx -------------------------------------------------------------------------------- /bg/clouds.imap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/bg/clouds.imap -------------------------------------------------------------------------------- /assets/grgo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/grgo.png -------------------------------------------------------------------------------- /font/digits.bin: -------------------------------------------------------------------------------- 1 | f< -------------------------------------------------------------------------------- /sprites/hero.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sprites/hero.bin -------------------------------------------------------------------------------- /assets/credits.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/credits.map -------------------------------------------------------------------------------- /assets/credits.scr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/credits.scr -------------------------------------------------------------------------------- /assets/splash.kla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/splash.kla -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/splash.png -------------------------------------------------------------------------------- /radar/mountains.gpx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/radar/mountains.gpx -------------------------------------------------------------------------------- /sprites/charvan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sprites/charvan.png -------------------------------------------------------------------------------- /sprites/enemy1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sprites/enemy1.bin -------------------------------------------------------------------------------- /sprites/enemy2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sprites/enemy2.bin -------------------------------------------------------------------------------- /radar/mountains.imap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/radar/mountains.imap -------------------------------------------------------------------------------- /sprites/explosion.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sprites/explosion.bin -------------------------------------------------------------------------------- /sprites/hud_chopper.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sprites/hud_chopper.bin -------------------------------------------------------------------------------- /src/ChopperCommand.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/src/ChopperCommand.prg -------------------------------------------------------------------------------- /assets/activisionlogo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/activisionlogo2.png -------------------------------------------------------------------------------- /sid/Chopper_Command_C64.sid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sid/Chopper_Command_C64.sid -------------------------------------------------------------------------------- /sid/Chopper_Command_C64.sng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/sid/Chopper_Command_C64.sng -------------------------------------------------------------------------------- /assets/activisionlogo_bands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonysavon/Chopper-Command-C64/HEAD/assets/activisionlogo_bands.png -------------------------------------------------------------------------------- /sprites/bullet.bin: -------------------------------------------------------------------------------- 1 | <<8 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A Commodore 64 port of Atari's Chopper Command 2 | https://csdb.dk/release/?id=180944 3 | 4 | Coded in 6510 Asm using KickAssembler. 5 | http://theweb.dk/KickAssembler/Main.html 6 | -------------------------------------------------------------------------------- /bg/bg.iscr: -------------------------------------------------------------------------------- 1 |  2 |   !"#$%&'%()***+,-./0000000000000000000000000000000000000000 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Antonio Savona 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/hud.asm: -------------------------------------------------------------------------------- 1 | //puts down score into sprites 2 | update_score: 3 | { 4 | .for (var s = 0; s < 2; s++) 5 | { 6 | ldx #0 7 | !: lda score + s * 3,x 8 | asl 9 | asl 10 | asl 11 | tay 12 | 13 | //ldy #0 14 | .for (var i = 0; i < 7; i++) 15 | { 16 | lda digits + i,y 17 | sta score_sprites + 2 * 3 + i * 3 + s * 64,x 18 | } 19 | inx 20 | cpx #3 21 | bne !- 22 | } 23 | rts 24 | } 25 | 26 | //add to score. Load A with the points x 100 to be added. 27 | //000x00 28 | add_score: 29 | { 30 | ldy #0 //flag for extralives 31 | 32 | ldx #3 //back_counter on digits 33 | !: clc 34 | adc score,x 35 | sta score,x 36 | cmp #$0a 37 | bcc !done+ 38 | sbc #$0a 39 | sta score,x 40 | lda #1 41 | cpx #2 42 | bne !skp+ 43 | 44 | iny 45 | 46 | !skp: 47 | dex 48 | bpl !- 49 | //overflow! 50 | lda #9 51 | sta score 52 | sta score + 1 53 | sta score + 2 54 | sta score + 3 55 | sta score + 4 56 | sta score + 5 57 | 58 | !done: 59 | 60 | cpy #0 61 | beq !skp+ 62 | 63 | lda lives 64 | cmp #6 65 | bcs !skp+ 66 | inc lives 67 | !skp: 68 | jmp update_score 69 | } 70 | digits: 71 | .import binary "../font/digits.bin" -------------------------------------------------------------------------------- /src/sfx.asm: -------------------------------------------------------------------------------- 1 | .macro sfx(wavetable,channel) 2 | { 3 | lda #wavetable 5 | ldx #channel * 7 6 | jsr sid.init + 6 7 | } 8 | 9 | 10 | sfx_fire: 11 | 12 | .byte $00,$F8,$08,$A4,$21,$AC,$81,$AB,$41,$AC,$80,$AA,$A9,$A8,$A7,$A6 13 | .byte $A5,$A4,$A3,$A2,$00 14 | 15 | sfx_bexplosion: 16 | 17 | .byte $00,$FA,$08,$A8,$41,$AC,$81,$AB,$80,$AA,$A9,$A8,$A7,$A6,$A5,$A4 18 | .byte $A3,$A2,$A2,$A2,$A2,$A2,$A2,$A2,$A2,$A2 19 | .fill 32,$a2 20 | .byte $00 21 | 22 | sfx_explosion: 23 | 24 | .byte $00,$FA,$08,$A0,$41,$97,$A2,$81,$A2,$A2,$A2,$80,$A2,$A2,$A1,$A0 25 | .byte $9F,$9E,$9D,$9D,$9D,$9D,$9D,$9D,$9D,$9C,$9C,$9C,$9C,$9C,$9C,$9C 26 | .byte $9C,$9C,$9C,$9B,$9B,$9B,$9B,$9B,$9A,$9A,$9A,$9A,$9A,$9A 27 | .byte $99,$99,$99,$99,$99,$99,$99,$00 28 | 29 | sfx_rotor: 30 | 31 | .byte $90,$F8,$00,$DF,$81,$D7,$80,$D0,$C7,$C7,$C0,$C0,$C0,$BF,$BE,$BD 32 | .byte $00 33 | 34 | 35 | sfx_bonus: 36 | 37 | .byte $00,$F6,$01,$DF,$81,$8C,$41,$8C,$8C,$8D,$8D,$8D,$8E,$8E,$8E,$8F 38 | .byte $8F,$8F,$90,$90,$90,$92,$92,$92,$93,$93,$93,$94,$94,$94,$96,$96 39 | .byte $96,$98,$98,$98,$9B,$9B,$9B,$A1,$A1,$A1,$A8,$A8,$A8,$AD,$AD,$AD 40 | .byte $B9,$B9,$B9,$40,$00 -------------------------------------------------------------------------------- /src/splash.asm: -------------------------------------------------------------------------------- 1 | showpic: 2 | { 3 | jsr vsync 4 | lda #$0b 5 | sta $d011 6 | 7 | lda #$00 8 | sta $d020 9 | sta $d021 10 | 11 | lda #$01 12 | sta $dd00 13 | lda #%00001000 14 | sta $d018 15 | lda #$d8 16 | sta $d016 17 | 18 | ldx #0 19 | !: 20 | .for (var i = 0; i < 4; i++) 21 | { 22 | lda $8400 + $100 * i,x 23 | sta $d800 + $100 * i,x 24 | } 25 | inx 26 | bne !- 27 | 28 | jsr vsync 29 | lda #$3b 30 | sta $d011 31 | 32 | !: jsr vsync 33 | 34 | lda $dc00 35 | and #%00010000 36 | bne !- 37 | 38 | 39 | rts 40 | } 41 | 42 | 43 | splash: 44 | { 45 | lda #0 46 | jsr sid.init 47 | nosid: 48 | sei 49 | 50 | lda #0 51 | sta $d021 52 | sta $d015 53 | sta button 54 | 55 | lda #$0f 56 | sta $d418 57 | 58 | ldx #39 59 | !: lda #0 60 | sta $d800 + 16 * 40,x 61 | lda #$0f 62 | .for (var i = 0; i < 8; i++) 63 | sta $d800 + (17 + i) * 40,x 64 | dex 65 | bpl !- 66 | 67 | lda #$c8 68 | sta $d016 69 | 70 | loop: 71 | jsr vsync 72 | jsr random_ 73 | jsr sid.play 74 | 75 | lda #2 76 | sta $dd00 77 | lda #%10000000 78 | sta $d018 79 | lda #$3b 80 | sta $d011 81 | 82 | lda #180 83 | !: cmp $d012 84 | bcs !- 85 | 86 | lda #3 87 | sta $dd00 88 | lda #$1b 89 | sta $d011 90 | lda #%11010110 91 | sta $d018 92 | // inc $d020 93 | 94 | lda button 95 | cmp #2 96 | bcs !exit+ 97 | cmp #0 98 | beq waitrelease 99 | cmp #1 100 | beq waitpush 101 | jmp loop 102 | 103 | 104 | !exit: 105 | lda #1 106 | jsr sid.init 107 | rts 108 | 109 | waitpush: 110 | lda #%00010000 111 | bit $dc00 112 | bne !skp+ 113 | inc button 114 | !skp: 115 | jmp loop 116 | 117 | waitrelease: 118 | lda #%00010000 119 | bit $dc00 120 | beq !skp+ 121 | inc button 122 | !skp: 123 | jmp loop 124 | 125 | button: 126 | .byte 0 127 | } 128 | 129 | -------------------------------------------------------------------------------- /src/hero.asm: -------------------------------------------------------------------------------- 1 | hero: 2 | { 3 | //inits all the member variables of the hero "class" 4 | //the chopper uses two sprites (0 and 1) 5 | //it also inits viewport variable because every time we init the chopper we must also init the viewport 6 | init: 7 | 8 | //chopper color 9 | lda #7 10 | sta sprc 11 | sta sprc + 1 12 | 13 | 14 | lda #0 15 | sta camera_x 16 | sta camera_x + 1 17 | 18 | sta xpos 19 | sta xpos + 1 20 | 21 | sta speed 22 | sta speed + 1 23 | 24 | sta facing 25 | 26 | sta flipclock 27 | 28 | sta shadowd01f 29 | sta shadowd01e 30 | 31 | sta beam.active 32 | sta beam.active + 1 33 | 34 | lda #$08 35 | sta testcollisions.dontcheck //we also init invincibility for the enemies here 36 | 37 | //grant 64 frames of invincibility wheneer the hero is initialized 38 | lda #$3f 39 | sta invincibility 40 | //and make it blink! 41 | lda #1 42 | sta blink 43 | 44 | //viewport is slightly ahead of the chopper, so while get ready is on display the camera gently catches up 45 | lda #$00 46 | sta xpos + 2 47 | lda #$01 48 | sta camera_x + 2 49 | 50 | lda #128 //vertically "kinda" centered 51 | sta ypos 52 | 53 | rts 54 | 55 | //translates controls into actions 56 | update: 57 | lda ypos 58 | sta spry 59 | sta spry + 1 60 | 61 | sec 62 | lda xpos + 1 63 | sbc camera_x + 1 64 | sta p0tmp 65 | lda xpos + 2 66 | sbc camera_x + 2 67 | sta p0tmp + 1 68 | 69 | lda p0tmp 70 | clc 71 | adc #24 72 | sta sprxl + 0 73 | lda p0tmp + 1 74 | adc #0 75 | sta sprxh + 0 76 | 77 | lda p0tmp 78 | clc 79 | adc #48 80 | sta sprxl + 1 81 | lda p0tmp + 1 82 | adc #0 83 | sta sprxh + 1 84 | !skp: 85 | 86 | lda flipclock 87 | beq !regular+ 88 | 89 | dec flipclock 90 | lda #[hero_sprite & $3fff] / 64 + 8 //flipping frame 91 | clc 92 | jmp !str+ 93 | 94 | 95 | !regular: 96 | lda clock 97 | //and #1 98 | //asl 99 | and #%00000010 100 | sta p0tmp 101 | lda facing 102 | asl 103 | asl 104 | adc p0tmp 105 | 106 | adc #[hero_sprite & $3fff] / 64 107 | 108 | !str: 109 | sta sprf 110 | adc #1 111 | sta sprf + 1 112 | 113 | !next: 114 | 115 | rts 116 | 117 | } 118 | 119 | //plays the rotor sound, whose speed changes according to chopper speed. Hence, it needs special cares 120 | rotor: 121 | { 122 | dec rotor_clock 123 | bpl !skp+ 124 | 125 | :sfx(sfx_rotor,0) 126 | 127 | lda speed + 1 128 | lsr 129 | 130 | sta p0tmp 131 | 132 | lda #8 133 | sec 134 | sbc p0tmp 135 | sta rotor_clock 136 | !skp: 137 | 138 | rts 139 | 140 | rotor_clock: 141 | .byte 0 142 | } -------------------------------------------------------------------------------- /src/camera.asm: -------------------------------------------------------------------------------- 1 | /* 2 | at each frame the camera procedure checks the Chopper X position in world coordinates and chases a target. 3 | the target is not exactly the chopper, which would cause player to be centered on screen, but a point in space that is in front of the chopper. 4 | this makes sure that the camera is aware of where the chopper is facing and puts him nicely at one end of the viewport. 5 | C = Chopper, T = Camera Target [ ] = Viewport 6 | Chopper facing right: [ C T ] 7 | Chopper facing left: [ T C ] 8 | Finally the speed at which the camera chases the target is proportional to the distance from the target, which makes the effect smoother and more natural looking. 9 | while doing all this we must take care of the wraparound at $a00: 10 | If target is at position $80 and camera is at position $980, we don't want to move left by $900 pixels, but rather right by $100 11 | */ 12 | camera: 13 | { 14 | chase: 15 | //first, let's compute the target 16 | //camera moves at a speed that is equal to the distance in pixel from the target / 256 17 | 18 | //p0tmp computes the target 19 | 20 | lda #24 21 | 22 | ldx facing 23 | beq !skp+ 24 | 25 | lda #320-72 26 | 27 | !skp: 28 | //now accumulator contains the desired target in the viewport distance 29 | //we don't just put it there, we slowly get there from the current target. 30 | 31 | //sta sub1 + 1 32 | cmp sub1 + 1 33 | beq !ok+ 34 | 35 | bcs !mustincrease+ 36 | 37 | lda sub1 + 1 38 | sec 39 | sbc #8 40 | sta sub1 + 1 41 | jmp !ok+ 42 | 43 | !mustincrease: 44 | lda #8 45 | clc 46 | adc sub1 + 1 47 | sta sub1 + 1 48 | 49 | !ok: 50 | 51 | sec 52 | lda xpos + 1 53 | sub1: sbc #24 54 | sta p0tmp + 1 55 | lda xpos + 2 56 | sbc #0 57 | sta p0tmp + 2 58 | 59 | 60 | //p0tmp is the target. 61 | //the first thing we need to do is to measure the distance between the camera and the target 62 | 63 | //compute the two distances. Left and right 64 | 65 | lda p0tmp + 1 66 | sec 67 | sbc camera_x + 1 68 | sta p0tmp + 3 69 | lda p0tmp + 2 70 | sbc camera_x + 2 71 | sta p0tmp + 4 72 | 73 | 74 | lda camera_x + 1 75 | sec 76 | sbc p0tmp + 1 77 | sta p0tmp + 5 78 | lda camera_x + 2 79 | sbc p0tmp + 2 80 | sta p0tmp + 6 81 | 82 | 83 | 84 | //where do we need to go? 85 | lda p0tmp + 4 86 | cmp p0tmp + 6 87 | bcs !left+ 88 | 89 | //right 90 | lda p0tmp + 1 91 | sec 92 | sbc camera_x + 1 93 | sta p0tmp + 3 94 | lda p0tmp + 2 95 | sbc camera_x + 2 96 | sta p0tmp + 4 97 | 98 | lsr p0tmp + 4 99 | ror p0tmp + 3 100 | 101 | lsr p0tmp + 4 102 | ror p0tmp + 3 103 | 104 | lda p0tmp + 3 105 | 106 | clc 107 | adc camera_x + 1 108 | sta camera_x + 1 109 | lda camera_x + 2 110 | adc p0tmp + 4 111 | sta camera_x + 2 112 | 113 | jmp !next+ 114 | 115 | 116 | !left: 117 | 118 | lda camera_x + 1 119 | sec 120 | sbc p0tmp + 1 121 | sta p0tmp + 3 122 | lda camera_x + 2 123 | sbc p0tmp + 2 124 | sta p0tmp + 4 125 | 126 | 127 | 128 | lsr p0tmp + 4 129 | ror p0tmp + 3 130 | 131 | lsr p0tmp + 4 132 | ror p0tmp + 3 133 | 134 | !skp: 135 | 136 | lda camera_x + 1 137 | sec 138 | sbc p0tmp + 3 139 | sta camera_x + 1 140 | lda camera_x + 2 141 | sbc p0tmp + 4 142 | sta camera_x + 2 143 | 144 | 145 | jmp !next+ 146 | 147 | !next: 148 | 149 | lda camera_x + 1 150 | and #7 151 | eor #7 152 | ora #$d0 153 | sta hwscroll 154 | 155 | lda camera_x + 1 156 | alr #7 * 2 157 | eor #7 158 | ora #$d0 159 | sta clouds_hwscroll 160 | 161 | lda camera_x + 1 162 | sec 163 | sbc #<[$0a00 / 2 - 160] 164 | sta radar.pixelviewport 165 | lda camera_x + 2 166 | sbc #>[$0a00 / 2 - 160] 167 | bpl !+ 168 | clc 169 | adc #$0a 170 | !: 171 | sta radar.pixelviewport + 1 172 | 173 | 174 | rts 175 | 176 | } -------------------------------------------------------------------------------- /src/misc.asm: -------------------------------------------------------------------------------- 1 | //this is the classic vblank. For all those cases where panelvsync is not needed 2 | vsync: 3 | { 4 | bit $d011 5 | bmi * -3 6 | bit $d011 7 | bpl * -3 8 | rts 9 | } 10 | 11 | 12 | //this erases the entire game area, except the vans row. It is 13 charline (520 chars) 13 | eraseplayarea: 14 | { 15 | ldx #103 16 | lda #32 17 | !: 18 | .for (var i=0; i < 5; i++) 19 | sta $0400 + 6 * 40 + 104 * i,x 20 | 21 | dex 22 | bpl !- 23 | rts 24 | } 25 | 26 | 27 | //32 bit random number generator 28 | random_: 29 | { 30 | asl random 31 | rol random+1 32 | rol random+2 33 | rol random+3 34 | bcc nofeedback 35 | lda random 36 | eor #$b7 37 | sta random 38 | lda random+1 39 | eor #$1d 40 | sta random+1 41 | lda random+2 42 | eor #$c1 43 | sta random+2 44 | lda random+3 45 | eor #$04 46 | sta random+3 47 | nofeedback: 48 | rts 49 | 50 | random: .byte $ff,$ff,$ff,$ff 51 | } 52 | 53 | .macro LoadSprites(fname,start,n) 54 | { 55 | .const sf = LoadBinary(fname) 56 | .fill 64 * n, sf.get(start * 64 + i) 57 | } 58 | 59 | 60 | flip_sprite: 61 | { 62 | init: 63 | ldx #0 64 | !: lda #0 65 | sta sprmir,x 66 | txa 67 | 68 | .for (var i = 0; i < 8; i++) 69 | { 70 | asl 71 | ror sprmir,x 72 | } 73 | 74 | inx 75 | bne !- 76 | rts 77 | 78 | //load x with the source sprite number, y with the destination sprite number 79 | 80 | flip: 81 | stx src1 + 2 82 | lda #0 83 | lsr src1 + 2 84 | ror 85 | lsr src1 + 2 86 | ror 87 | sta src1 + 1 88 | 89 | sty dst1 + 2 90 | lda #0 91 | lsr dst1 + 2 92 | ror 93 | lsr dst1 + 2 94 | ror 95 | sta dst1 + 1 96 | 97 | 98 | ldy src1 + 1 99 | iny 100 | sty src2 + 1 101 | iny 102 | sty src3 + 1 103 | 104 | ldy dst1 + 1 105 | iny 106 | sty dst2 + 1 107 | iny 108 | sty dst3 + 1 109 | 110 | ldy src1 + 2 111 | sty src2 + 2 112 | sty src3 + 2 113 | 114 | ldy dst1 + 2 115 | sty dst2 + 2 116 | sty dst3 + 2 117 | 118 | 119 | ldx #$3c //bottom left byte offset 120 | !: 121 | 122 | src1: ldy $e000,x 123 | lda sprmir,y 124 | src3: ldy $e002,x 125 | dst3: sta $e002,x 126 | lda sprmir,y 127 | dst1: sta $e000,x 128 | 129 | src2: ldy $e001,x 130 | lda sprmir,y 131 | dst2: sta $e001,x 132 | 133 | txa 134 | axs #$03 //dex * 3. We save 2 cycles. 135 | 136 | bpl !- 137 | 138 | rts 139 | 140 | 141 | sprmir: 142 | .fill $100,0 143 | 144 | } 145 | 146 | 147 | //Displays the Get Ready and Game Over animations 148 | //load A with 0 for getready or 1 for gameover, then call init 149 | //call play once per frame to animate 150 | bigsign: 151 | { 152 | 153 | init: 154 | sta type 155 | 156 | lda #0 157 | sta sclock 158 | 159 | //clears the destination area 160 | ldx #71 161 | !: 162 | .for (var i = 0; i < 4; i++) 163 | sta bigsign_area + i * 72,x 164 | dex 165 | bpl !- 166 | 167 | //place 36 (empty) chars on screen, like this 168 | //00 02 04 ... 34 169 | //01 03 05 ... 35 170 | ldx #0 171 | 172 | lda #BIGSIGNCHAR 173 | clc 174 | !: sta $0400 + 12 * 40 + 11,x 175 | adc #1 176 | sta $0400 + 13 * 40 + 11,x 177 | adc #1 178 | inx 179 | cpx #18 //this will also clear the carry for all the iteration but the last one 180 | bne !- 181 | 182 | // make everything white 183 | lda #1 184 | ldx #17 185 | !: sta $d800 + 12 * 40 + 11,x 186 | sta $d800 + 13 * 40 + 11,x 187 | dex 188 | bpl !- 189 | rts 190 | 191 | //call once per frame 192 | play: 193 | lda sclock 194 | cmp #32 195 | bcs !done+ 196 | 197 | and #1 198 | bne !incdone+ 199 | 200 | ldx #0 201 | 202 | lda sclock 203 | lsr 204 | tax 205 | 206 | lda type 207 | bne !skp+ 208 | 209 | jsr scrgr 210 | jmp !incdone+ 211 | !skp: jsr scrgo 212 | 213 | !incdone: 214 | inc sclock 215 | !done: 216 | rts 217 | 218 | scrgr: 219 | 220 | ldy #0 221 | !: 222 | .for (var i = 0; i < 18; i ++) 223 | { 224 | lda getreadymap + 32 * i,x 225 | sta bigsign_area + 16 * i,y 226 | } 227 | iny 228 | inx 229 | cpy #16 230 | bne !- 231 | rts 232 | 233 | scrgo: 234 | 235 | ldy #0 236 | !: 237 | .for (var i = 0; i < 18; i ++) 238 | { 239 | lda gameovermap + 32 * i,x 240 | sta bigsign_area + 16 * i,y 241 | } 242 | iny 243 | inx 244 | cpy #16 245 | bne !- 246 | rts 247 | 248 | type: 249 | .byte 0 250 | sclock: 251 | .byte 0 252 | } -------------------------------------------------------------------------------- /src/weapons.asm: -------------------------------------------------------------------------------- 1 | //friendly fire is just a nice laser beam. 2 | //now, unlike the other objects in game, laser beams follow screen coordinates, rather than world coordinates 3 | //this makes things much easier on the cpu side, and the parallax discrepancies are not noticeable because the beam is very fast. 4 | //this is also consistent with the way the original draws the beam 5 | //there are a maximum of two beams on screen at the same time. 6 | beam: 7 | { 8 | 9 | fire: 10 | lda timer 11 | beq !canfire+ 12 | rts //sorry, dude, you can't shoot 13 | 14 | !canfire: 15 | 16 | :sfx(sfx_fire,1) 17 | 18 | lda #10 //you can shooot maximum one laser shot every 10 frames. (5 rounds per second on PAL) 19 | sta timer 20 | 21 | lda flipflop //which cof the two cannons is available? 22 | eor #1 23 | sta flipflop 24 | tax 25 | 26 | lda #1 27 | sta active,x 28 | 29 | lda spry 30 | sec 31 | sbc #31 32 | lsr 33 | lsr 34 | lsr 35 | 36 | sta y0,x 37 | 38 | lda spry 39 | sec 40 | sbc #31 41 | and #7 42 | lsr 43 | clc 44 | adc #BEAMCHAR 45 | sta char,x 46 | 47 | lda sprxl 48 | clc 49 | adc #4 50 | pha 51 | lda sprxh 52 | //and #1 53 | adc #0 54 | lsr 55 | pla 56 | ror 57 | lsr 58 | lsr 59 | 60 | 61 | // now we have the x offset in chars 62 | sta x0,x 63 | lda #4 64 | sta size,x 65 | 66 | lda #$00 67 | sta age,x 68 | 69 | lda facing 70 | sta direction,x 71 | 72 | beq !firesright+ 73 | 74 | //fires left 75 | lda x0,x 76 | sec 77 | sbc #2 78 | jmp !doneadjusting+ 79 | 80 | !firesright: 81 | lda x0,x 82 | clc 83 | adc #1 84 | !doneadjusting: 85 | sta x0,x 86 | 87 | rts 88 | 89 | 90 | update: 91 | lda timer 92 | beq !skp+ 93 | dec timer 94 | 95 | !skp: 96 | 97 | ldx #1 98 | !beamloop: 99 | lda active,x 100 | bne !ok+ 101 | jmp !next+ 102 | !ok: 103 | ldy y0,x 104 | lda screen40l,y 105 | sta wp0tmp 106 | sta wp0tmp + 4 107 | lda screen40h,y 108 | sta wp0tmp + 1 109 | clc 110 | adc #>[$d800 - $0400] 111 | sta wp0tmp + 5 112 | ldy x0,x 113 | 114 | lda size,x 115 | clc 116 | adc #1 117 | sta wp0tmp + 2 118 | 119 | 120 | lda direction,x 121 | beq !right+ 122 | //left 123 | lda #32 //empty char 124 | !: sta (wp0tmp),y 125 | dey 126 | bmi !doneerasing+ 127 | dec wp0tmp + 2 128 | bne !- 129 | jmp !doneerasing+ 130 | 131 | 132 | //one possible optimization here would be to only erase the delta 133 | //but the original does it this way, and so do we! 134 | //there's plenty of rastertime available anyway, so no point in making code too complicate 135 | !right: 136 | //first thing to do, erase the old beam 137 | lda #32 //empty char 138 | !: sta (wp0tmp),y 139 | iny 140 | cpy #40 141 | beq !doneerasing+ 142 | dec wp0tmp + 2 143 | bne !- 144 | 145 | 146 | !doneerasing: 147 | lda size,x 148 | clc 149 | adc #2 150 | sta size,x 151 | cmp #34 152 | bcc !ok+ 153 | !deactivate: 154 | lda #0 155 | sta active,x 156 | jmp !next+ 157 | !ok: 158 | //update age and color 159 | ldy age,x 160 | cpy #10 161 | beq !ok+ 162 | iny 163 | inc age,x 164 | !ok: 165 | lda gradient,y 166 | sta col2 + 1 167 | sta col1 + 1 168 | //update beam position and size 169 | lda direction,x 170 | beq !right+ 171 | //left 172 | lda x0,x 173 | sec 174 | sbc #2 175 | sta x0,x 176 | cmp #2 177 | bcs !ok+ 178 | jmp !deactivate- 179 | !right: 180 | lda x0,x 181 | clc 182 | adc #2 183 | sta x0,x 184 | cmp #35 185 | bcc !ok+ 186 | jmp !deactivate- 187 | !ok: 188 | //draw the new beam 189 | tay 190 | lda size,x 191 | sta wp0tmp + 2 192 | lda char,x 193 | sta chr1 + 1 194 | sta chr2 + 1 195 | 196 | lda direction,x 197 | beq !right+ 198 | //left 199 | 200 | 201 | !: 202 | chr1: lda #$00 203 | sta (wp0tmp),y 204 | col1: lda #$00 205 | sta (wp0tmp + 4),y 206 | dey 207 | beq !doneplotting+ 208 | dec wp0tmp + 2 209 | bne !- 210 | jmp !doneplotting+ 211 | !right: 212 | !: 213 | chr2: lda #$00 214 | sta (wp0tmp),y 215 | col2: lda #$00 216 | sta (wp0tmp + 4),y 217 | iny 218 | cpy #39 219 | beq !doneplotting+ 220 | dec wp0tmp + 2 221 | bne !- 222 | !doneplotting: 223 | 224 | !next: 225 | dex 226 | bmi !done+ 227 | jmp !beamloop- 228 | !done: 229 | rts 230 | 231 | timer: 232 | .byte 0 233 | 234 | flipflop: 235 | .byte 0 236 | 237 | active: 238 | .byte 0,0 239 | 240 | y0: //vertical offset in chars 241 | .byte 0,0 242 | 243 | char: //4 possible chars for the beam, according to the vertical offset 244 | .byte 0,0 245 | 246 | x0: //horizontal offset of the inner end of the beam in chars 247 | .byte 0,0 248 | 249 | direction: //0 = shooting right, 1 = shooting left 250 | .byte 0,0 251 | 252 | size: 253 | .byte 0,0 //this also works as a clock. 254 | 255 | age: 256 | .byte 0,0 //how long the beam has been on display, which also affects its length 257 | 258 | 259 | gradient: 260 | //beam colors 261 | .byte 0, $06,$06,$06,$06,$0e,$0e,$0e,$03,$03,$01 262 | 263 | } -------------------------------------------------------------------------------- /src/controls.asm: -------------------------------------------------------------------------------- 1 | 2 | //we don't use 2 complement math in this game, but rather the num + sign format. 3 | //it wastes one byte for each variable and it's slower for adc and sbc, but it's much faster when it comes to translating results to screen coordinates 4 | //We need few macros and functions to handle this weird format though 5 | // 6 | 7 | //adds an immediate 8 bit value to a 16 bit signed number 8 | .macro s16u8adcIMM(sign,num1,num2) 9 | { 10 | lda sign 11 | beq simpleadd 12 | //it's negative, so we have to do a subtraction. 13 | sec 14 | lda num1 15 | sbc #num2 16 | sta num1 17 | bcs ok // still negative, exit 18 | lda num1+1 //we should now decrement num1+1 19 | beq reset // check if zero first (decrementing would wrap arouund) 20 | dec num1+1 21 | bcc ok // this branch is always taken, because we are here because the carry is clear 22 | reset: 23 | sec 24 | lda #$00 25 | sbc num1 26 | sta num1 27 | lda #$00 28 | sta sign 29 | sec 30 | bcs ok 31 | 32 | simpleadd: // num1 is positive, just add 33 | clc 34 | lda num1 35 | adc #num2 36 | sta num1 37 | bcc ok 38 | inc num1+1 39 | ok: 40 | } 41 | 42 | 43 | //subtracts an immediate 8 bit value from a 16 bit signed integer 44 | .macro s16u8sbcIMM(sign,num1,num2) 45 | { 46 | lda sign 47 | bne simpleadd // if it's negative, doing a sub is like leaving the sign unaltered and doing add 48 | //it's positive, so we have to do a simple sbc 49 | sec 50 | lda num1 51 | sbc #num2 52 | sta num1 53 | bcs ok // still negative, exit 54 | lda num1+1 //we should now decrement num1+1 55 | beq reset // check if zero first (decrementing would wrap arouund) 56 | dec num1+1 57 | bcc ok // this branch is always taken, because we are here because the carry is clear 58 | 59 | reset: 60 | sec 61 | lda #$00 62 | sbc num1 63 | sta num1 64 | lda #$01 65 | sta sign 66 | //dec sign 67 | sec 68 | bcs ok 69 | 70 | simpleadd: // num1 is negative, just add 71 | clc 72 | lda num1 73 | adc #num2 74 | sta num1 75 | bcc ok 76 | inc num1+1 77 | ok: 78 | } 79 | 80 | 81 | 82 | controls: 83 | { 84 | 85 | lda $dc00 86 | 87 | jsr readjoy 88 | lda #$00 89 | rol 90 | sta fire 91 | 92 | //first check y 93 | 94 | cpy #0 95 | beq !horizontal+ 96 | cpy #$ff 97 | beq !goesup+ 98 | //goes down 99 | 100 | ldy ypos 101 | cpy #MAXY 102 | bcs !horizontal+ 103 | iny 104 | sty ypos 105 | jmp !horizontal+ 106 | !goesup: 107 | 108 | ldy ypos 109 | cpy #MINY 110 | beq !horizontal+ 111 | dey 112 | sty ypos 113 | 114 | !horizontal: 115 | 116 | cpx #1 117 | beq !right+ 118 | 119 | cpx #0 120 | bne !left+ 121 | 122 | jmp !decelerate+ 123 | 124 | !left: 125 | lda #1 126 | cmp facing 127 | beq !skp+ 128 | sta facing 129 | lda #4 130 | sta flipclock 131 | !skp: 132 | :s16u8sbcIMM(speed_sign,speed,ACCELERATION) 133 | jmp !clamp+ 134 | !right: 135 | lda #0 136 | cmp facing 137 | beq !skp+ 138 | sta facing 139 | lda #4 140 | sta flipclock 141 | !skp: 142 | :s16u8adcIMM(speed_sign,speed,ACCELERATION) 143 | 144 | !clamp: 145 | lda speed + 1 146 | cmp #MAXSPEED 147 | bcc !updatex+ 148 | 149 | lda #MAXSPEED 150 | sta speed + 1 151 | 152 | lda #0 153 | sta speed 154 | jmp !updatex+ 155 | 156 | !decelerate: 157 | lda speed 158 | ora speed + 1 159 | bne !moves+ 160 | jmp !button+ //no need to update 161 | !moves: 162 | sec 163 | lda speed 164 | sbc #DECELERATION 165 | sta speed 166 | lda speed + 1 167 | sbc #0 168 | sta speed + 1 169 | bcs !ok+ 170 | lda #0 171 | sta speed 172 | sta speed + 1 173 | 174 | !ok: 175 | jmp !updatex+ 176 | 177 | 178 | !updatex: 179 | lda speed_sign 180 | bne !left+ 181 | 182 | clc 183 | lda xpos 184 | adc speed 185 | sta xpos 186 | lda xpos + 1 187 | adc speed + 1 188 | sta xpos + 1 189 | lda xpos + 2 190 | adc #0 191 | sta xpos + 2 192 | //we must wrap around at #$0c00 to $200 193 | cmp #$0a 194 | bcc !ok+ 195 | !fix: 196 | //sec 197 | lda xpos + 1 198 | sbc #$00 199 | sta xpos + 1 200 | lda xpos + 2 201 | sbc #$0a 202 | sta xpos + 2 203 | 204 | sec 205 | lda camera_x + 1 206 | sbc #$00 207 | sta camera_x + 1 208 | lda camera_x + 2 209 | sbc #$0a 210 | sta camera_x + 2 211 | 212 | !ok: 213 | jmp !button+ 214 | 215 | !left: 216 | sec 217 | lda xpos 218 | sbc speed 219 | sta xpos 220 | lda xpos + 1 221 | sbc speed + 1 222 | sta xpos + 1 223 | lda xpos + 2 224 | sbc #0 225 | sta xpos + 2 226 | 227 | bcs !ok+ 228 | 229 | //we must wrap around at #$0000 -> $0a00 230 | 231 | lda xpos + 1 232 | adc #$00 233 | sta xpos + 1 234 | lda xpos + 2 235 | adc #$0a 236 | sta xpos + 2 237 | 238 | clc 239 | lda camera_x + 1 240 | adc #$00 241 | sta camera_x + 1 242 | lda camera_x + 2 243 | adc #$0a 244 | sta camera_x + 2 245 | 246 | !ok: 247 | jmp !button+ 248 | 249 | 250 | 251 | 252 | !button: 253 | lda fire 254 | bne !next+ 255 | 256 | jsr beam.fire 257 | !next: 258 | rts 259 | 260 | fire: 261 | .byte 0 262 | } 263 | 264 | 265 | readjoy: 266 | { 267 | 268 | djrrb: ldy #0 269 | ldx #0 270 | lsr 271 | bcs djr0 272 | dey 273 | djr0: lsr 274 | bcs djr1 275 | iny 276 | djr1: lsr 277 | bcs djr2 278 | dex 279 | djr2: lsr 280 | bcs djr3 281 | inx 282 | djr3: lsr 283 | rts 284 | } -------------------------------------------------------------------------------- /MANUAL.txt: -------------------------------------------------------------------------------- 1 | Chopper Command C64 2 | 3 | Coded by Antonio Savona 4 | Gfx by Steven Day 5 | Music by Saul Cross 6 | Sfx by Flemming Martins 7 | 8 | -------------------------------------------------------------------------- 9 | 10 | Activision 11 | Chopper Command 12 | Instructions 13 | 14 | Your first mission? Don't worry. Everyone gets a little nervous. 15 | Just make sure you carefully read this instruction manual first. 16 | You'll be dealing with some very sophisticated equipment, and an 17 | extremely tricky enemy. So, good luck. The guys on the ground are 18 | counting on ya! 19 | 20 | Activision 21 | 22 | Chopper Command Basics 23 | 24 | The object of the game is to accumulate as many points as possible by 25 | knocking out enemy aircraft, while protecting yourself and your truck 26 | convoys at the same time. 27 | 28 | 1. Hook up your video game system. Follow manufacturer's instructions. 29 | 30 | 2. Turn power ON. If no picture appears, check connection of your game 31 | system to TV, then repeat steps 1-2. 32 | 33 | 3. Load the game. 34 | 35 | 4. Plug in Joystick Controller in Port 2. 36 | 37 | 5. Press fire button sto start. 38 | 39 | 6. Allow helicopter to position itself on left side of screen. 40 | Action begins as soon as your helicopter is deployed, but you can press 41 | spacebar to pause the game. 42 | 43 | 7. Pressing the button will fire cannons. Holding the button down will 44 | activate the continuous fire mode. 45 | Pushing the Joystick up will cause your helicopter to ascend, pulling it 46 | back will bring it down. Moving the Joystick left or right will cause 47 | your helicopter to face to the left or to the right. 48 | Pushing the Joystick to the left or to the right will move your 49 | helicopter in that direction. 50 | 51 | 52 | Special Features of Chopper Command by Activision 53 | 54 | 1. The Long Range Scanner at the bottom of the screen will enable you 55 | to detect both approaching truck convoys (friendly) and enemy aircraft 56 | well ahead of time. The truck convoys appear as white "blips" on the 57 | very bottom of the scanner while enemy aircraft appear as white "blips" 58 | above the convoy. Your helicopter gunship is the black dot. You can 59 | calculate that the area represented on the long range scanner is 60 | roughly 5 miles, while the area portrayed on the large screen is about 61 | 1 mile. 62 | 63 | 2. There are increasing levels of intensity. Each level will start 64 | with a wave of twelve enemy ships and twelve trucks in your convoy. 65 | There are ten waves of enemy attackers, with each wave being faster 66 | than the one before. Take care! Enemy aircaft fire multi-warhead 67 | missiles which will split in two after being launched from their 68 | aircraft. These missiles can destroy both your helicopter and the 69 | trucks below, and, since you cannot shoot the missiles down, you must 70 | dodge them. 71 | 72 | 3. You have an unlimited supply of ammunition, so go ahead and blanket 73 | the sky with your laser cannons. 74 | 75 | 4. Scoring. Each time you shoot down an enemy helicopter, you earn 100 76 | points. For every enemy jet you shoot down, you will be credited with 77 | 200 points. Should you wipe out an entire wave of hostile aircraft, 78 | you will receive a bonus calculated by multiplying the number of trucks 79 | remaining in the convoy times the wave number achieved (one through 80 | ten) times 100. 81 | 82 | 5. You have helicopter reserves. You start the game with three 83 | choppers in your fleet. For every 10,000 points you score, an extra 84 | helicopter will be added to your squadron, up to a maximum of six. The 85 | number of extra choppers appears under the score. 86 | 87 | 88 | Getting the Feel of Chopper Command by Activision 89 | 90 | You'll be amazed how responsive your chopper is to the slightest 91 | movements of the Joystick. Pushing the Joystick up will cause your 92 | helicopter to climb; pulling it back will make your craft descend. 93 | Right or left movements will guide your gunship's horizontal motion. 94 | After flying in one direction, pushing the Joystick in the opposite 95 | direction will cause an immediate 180o turn. Notice also that your 96 | chopper "drifts" slightly after pivoting 180o (this should be helpful 97 | in better timing your shots). 98 | 99 | Your chopper can hover close to the ground, but be careful not to 100 | collide with your convoy. You'll destroy the helicopter and a truck. 101 | 102 | Remember, don't be too aggressive at first. Keep your chopper on the 103 | left side of the screen facing right, and size up the situation. This 104 | will give you time to better recognize enemy tactics. Then, when you 105 | feel more confident, you can seek out the enemy and get them before 106 | they get to you or your trucks. 107 | 108 | The long range scanner is a very useful tool. However, you'll have to 109 | keep your eyes on the larger video screen and the scanner at the same 110 | time. Watching one and not the other could be dangerous. Don't 111 | forget, the long range scanner is for estimating the positions of enemy 112 | aircraft and not for lining up shots. You cannot hit the enemy unless 113 | you can see them on the larger screen. 114 | 115 | 116 | Join the Activision "Commandos" 117 | 118 | If you reach a score of 10,000 points (or more) on the Cadet level, you 119 | will be eligible to join this prestigious organization. Just send us a 120 | picture of your TV screen and we'll present you with a special 121 | "Commandos" emblem. 122 | 123 | 124 | How to Become a "Commando" at Chopper Command by Activision 125 | 126 | Tips from Bob Whitehead, designer of Chopper Command 127 | 128 | Bob Whitehead is a Senior Designer at Activision. Before creating 129 | Chopper Command, Bob designed Boxing, Skiing and Stampede for 130 | Activsion. 131 | 132 | "As you'll soon discover, Chopper Command takes quick reflexes and keen 133 | coordination. However, there is a strategic side to the game as well." 134 | 135 | "For example, your truck convoys will always travel from the right to 136 | the left. And so will the enemy formations. Knowing this, you can 137 | position yourself at the left side of the screen and start firing as 138 | soon as the enemy aircraft appear. This is important because your 139 | helicopter's chances of being hit by a multi-warhead missile increase 140 | the closer the enemy aircraft get. The enemy pilots are real 141 | kamikazes, too, and they'll collide with you if they can't shoot you 142 | down." 143 | 144 | "Good luck! I hope you have as much fun playing Chopper Command as I 145 | had designing it. God Bless." 146 | 147 | Bob Whitehead 148 | P.S. Drop my a line. I'd sure like to hear how our guys are doing at 149 | the front. 150 | 151 | 152 | Look for more Activision video games wherever you buy video game 153 | cartridges. Drop us a note, and we'll gladly add your name to our 154 | mailing list and keep you posted on new Activision game cartridges as 155 | they become available. 156 | 157 | Activision 158 | Activision, Inc., 3255-2 Scott Blvd., Santa Clara, CA 95051 159 | 160 | ATARI and Video Computer System are trademarks of ATARI, INC. 161 | Tele-Games and Video Arcade are trademarks of Sears, Roebuck and Co. 162 | 163 | C 1982 Activision AX-015-03 Printed in U.S.A. 164 | 165 | -------------------------------------------------------------------------------- /src/radar.asm: -------------------------------------------------------------------------------- 1 | //the radar is 128 pixels wide and it should cover $a00 (2560) possible x positions. 2 | //We are working with fat pixels, which means that we reall have 64 possible x offsets in the radar. 3 | //Therefore each fat pixel corresponds to 40 pixels on the map. 4 | //We must divide by 40: that's like divided by 8 (three shifts) and then divide by 5, for which we already have a divtable 5 | 6 | radar: 7 | { 8 | 9 | init: 10 | lda #>radar_map 11 | sta p0radar + 1 12 | 13 | ldx #0 14 | lda #0 15 | !: sta radar_map,x 16 | inx 17 | bne !- 18 | 19 | 20 | ldx #[radar_data_end - radar_data_start] - 1 21 | !: sta radar_data_start,x 22 | dex 23 | bpl !- 24 | 25 | rts 26 | 27 | plot: 28 | lda clock 29 | and #1 30 | beq plot_friends 31 | jmp plot_foes 32 | 33 | 34 | plot_friends: 35 | 36 | //plot the hero, unless it's gameover 37 | lda lives 38 | bne !skp+ 39 | jmp !plotvans+ 40 | !skp: 41 | lda hero_previous_addr 42 | sta p0radar 43 | ldx hero_previous_off 44 | 45 | ldy hero_previous_y 46 | lda (p0radar),y 47 | and andmask,x 48 | sta (p0radar),y 49 | iny 50 | lda (p0radar),y 51 | and andmask,x 52 | sta (p0radar),y 53 | 54 | lda xpos + 1 55 | sta p0tmp 56 | lda xpos + 2 57 | sta p0tmp + 1 58 | :toradarx() 59 | tax 60 | sta hero_previous_off 61 | lda p0radar 62 | sta hero_previous_addr 63 | 64 | lda ypos 65 | sec 66 | sbc #MINY 67 | lsr 68 | lsr 69 | lsr 70 | 71 | sta hero_previous_y 72 | tay 73 | 74 | lda (p0radar),y 75 | ora friendormask,x 76 | sta (p0radar),y 77 | iny 78 | lda (p0radar),y 79 | ora friendormask,x 80 | sta (p0radar),y 81 | 82 | !plotvans: 83 | //first erase the old ones 84 | 85 | lda vans_previous_addr 86 | sta p0radar 87 | lda vans_previous_off 88 | sta p0tmp + 2 89 | 90 | lda #0 91 | sta p0tmp + 3 92 | 93 | !: 94 | ldx p0tmp + 2 95 | .for (var i = 0; i < 4; i++) 96 | { 97 | ldy #12 98 | lda (p0radar),y 99 | and andmask,x 100 | sta (p0radar),y 101 | iny 102 | lda (p0radar),y 103 | and andmask,x 104 | sta (p0radar),y 105 | lda p0radar 106 | clc 107 | adc #64 108 | sta p0radar 109 | } 110 | 111 | 112 | lda #3 113 | sec 114 | isc p0tmp + 3 115 | 116 | beq !done+ 117 | 118 | lda p0tmp + 2 119 | clc 120 | adc #2 121 | cmp #4 122 | bcc !skp+ 123 | sbc #4 124 | tax 125 | lda p0radar 126 | clc 127 | adc #16 128 | sta p0radar 129 | txa 130 | !skp: sta p0tmp + 2 131 | 132 | jmp !- 133 | !done: 134 | 135 | 136 | //now draw them 137 | lda vans.xpos_l 138 | sta p0tmp 139 | lda vans.xpos_h 140 | sta p0tmp + 1 141 | :toradarx() 142 | tax 143 | sta vans_previous_off 144 | sta p0tmp + 2 145 | 146 | lda p0radar 147 | sta vans_previous_addr 148 | 149 | lda #0 150 | sta p0tmp + 3 //loops on vans within a group 151 | 152 | !: 153 | lda p0tmp + 3 154 | sta p0tmp + 4 //loops on logical van number 155 | 156 | ldx p0tmp + 2 157 | .for (var i = 0; i < 4; i++) 158 | { 159 | ldy p0tmp + 4 160 | lda vans.status,y 161 | beq !skp+ 162 | 163 | ldy #12 164 | lda (p0radar),y 165 | ora friendormask,x 166 | sta (p0radar),y 167 | iny 168 | lda (p0radar),y 169 | ora friendormask,x 170 | sta (p0radar),y 171 | 172 | !skp: 173 | lda p0tmp + 4 174 | clc 175 | adc #3 176 | sta p0tmp + 4 177 | 178 | lda p0radar 179 | clc 180 | adc #64 181 | sta p0radar 182 | } 183 | 184 | inc p0tmp + 3 185 | lda p0tmp + 3 186 | cmp #3 187 | beq !done+ 188 | 189 | lda p0tmp + 2 190 | clc 191 | adc #2 192 | cmp #4 193 | bcc !skp+ 194 | sbc #4 195 | tax 196 | lda p0radar 197 | clc 198 | adc #16 199 | sta p0radar 200 | txa 201 | !skp: sta p0tmp + 2 202 | 203 | jmp !- 204 | !done: 205 | 206 | rts 207 | 208 | 209 | plot_foes: 210 | 211 | ldx #11 212 | !: stx p0tmp + 2 //loops on enemies 213 | 214 | lda enemies.status,x 215 | and #enemies.STATUS_ALIVE 216 | bne !ok+ 217 | jmp !ne+ 218 | !ok: 219 | //erase previous position 220 | lda enemy_previous_addr,x 221 | sta p0radar 222 | lda enemy_previous_off,x 223 | 224 | ldy enemy_previous_y,x 225 | 226 | tax 227 | 228 | lda (p0radar),y 229 | and andmask,x 230 | sta (p0radar),y 231 | iny 232 | lda (p0radar),y 233 | and andmask,x 234 | sta (p0radar),y 235 | 236 | //draw new position 237 | ldx p0tmp + 2 238 | 239 | lda enemies.xpos_l,x 240 | clc 241 | adc enemies.xdelta,x 242 | sta p0tmp 243 | lda enemies.xpos_h,x 244 | adc #0 245 | cmp #$0a 246 | bcc !skp+ 247 | sbc #$0a 248 | !skp: 249 | sta p0tmp + 1 250 | :toradarx() 251 | 252 | sta p0tmp + 3 253 | 254 | sta enemy_previous_off,x 255 | lda p0radar 256 | sta enemy_previous_addr,x 257 | 258 | lda enemies.ypos,x 259 | clc 260 | adc enemies.ydelta,x 261 | sec 262 | sbc #MINY 263 | lsr 264 | lsr 265 | lsr 266 | 267 | sta enemy_previous_y,x 268 | tay 269 | 270 | ldx p0tmp + 3 271 | lda (p0radar),y 272 | ora enemyormask,x 273 | sta (p0radar),y 274 | iny 275 | lda (p0radar),y 276 | ora enemyormask,x 277 | sta (p0radar),y 278 | 279 | ldx p0tmp + 2 280 | !ne: dex 281 | bmi !done+ 282 | jmp !- 283 | !done: 284 | rts 285 | 286 | radar_data_start: 287 | 288 | hero_previous_addr: 289 | .byte 0 290 | hero_previous_off: 291 | .byte 0 292 | hero_previous_y: 293 | .byte 0 294 | 295 | 296 | vans_previous_addr: 297 | .byte 0 298 | vans_previous_off: 299 | .byte 0 300 | 301 | enemy_previous_addr: 302 | .fill 12, 0 303 | enemy_previous_off: 304 | .fill 12, 0 305 | enemy_previous_y: 306 | .fill 12, 0 307 | 308 | pixelviewport: 309 | .byte 0,0 310 | 311 | radar_data_end: 312 | } 313 | 314 | 315 | 316 | //load p0tmp, p0tmp + 1 with object's word x-coordinate ($0-$a00). 317 | //Returns the pixel offset in the radar in the accumulator (0-63), adjusted in the viewport 318 | //sets p0tmp + 2, p0tmp + 3 with the char memory address at y = 0, which means we can just use (p0tmp + 2),y to plot 319 | .macro toradarx() 320 | { 321 | sec 322 | lda p0tmp 323 | sbc radar.pixelviewport 324 | sta p0tmp 325 | lda p0tmp + 1 326 | sbc radar.pixelviewport + 1 327 | bpl !+ 328 | clc 329 | adc #$0a 330 | !: 331 | lsr 332 | ror p0tmp 333 | lsr 334 | ror p0tmp 335 | lsr 336 | ror p0tmp 337 | sta p0tmp + 1 338 | 339 | lda p0tmp 340 | clc 341 | adc #div5 345 | sta pixsrc + 2 346 | 347 | pixsrc: lda div5 348 | 349 | 350 | sta p0tmp 351 | 352 | /* lsr 353 | lsr //char column 354 | asl 355 | asl 356 | asl 357 | asl 358 | */ 359 | //the following is equivalent 360 | and #%00111100 361 | asl 362 | asl 363 | 364 | sta p0radar 365 | 366 | lda p0tmp 367 | and #3 368 | 369 | } 370 | 371 | 372 | //the following are global, as we use them pretty much everywhere. 373 | 374 | friendormask: 375 | .byte %11000000 376 | .byte %00110000 377 | .byte %00001100 378 | .byte %00000011 379 | 380 | enemyormask: 381 | .byte %10000000 382 | .byte %00100000 383 | .byte %00001000 384 | .byte %00000010 385 | 386 | andmask: 387 | .byte %00111111 388 | .byte %11001111 389 | .byte %11110011 390 | .byte %11111100 391 | -------------------------------------------------------------------------------- /src/enemies.asm: -------------------------------------------------------------------------------- 1 | /* 2 | Enemies move within an horizontal range of 256 pixels, centered in the central truck of one of the four convoys 3 | The original Activision game moves the enemies quite randomly. Horizontally, they go end to end thorugh this range, but they can also randomly change their direction. 4 | Planes are slightly faster than the helicopters in their horizontal movement, but helicopters move vertically a bit more "aggressively". 5 | This aspect is quickly lost as you progress through the first waves as all enemies max out in their craziness scale and behave identically. 6 | In fact, enemies top speed is a function of the level. 7 | My implementation, recreates the origina logic, which was slotted to cope with memory limitations, but allows for finer tuning 8 | When an enemy initiates a movement, the horizontal speed is (f(direction) + f(level) + boost) pixels per second. 9 | boost is 0 or 1 pixel per frame, according to a random value. 10 | f(direction) = 1 if going right, 0 if going left. This is because the "world" moves left at 1 pixel per frame anyway and this compensates it 11 | Shooting happens also randomly but r-test is so frequent that it ends up feeling more like "whoever can, shoots" 12 | I don't particularly love this aspect, but it is a landmark feature of the original, therefore it stays. 13 | */ 14 | 15 | enemies: 16 | { 17 | .label STATUS_PLANE = %10000000 //plane or chopper. 0 = chopper, 1 = plane 18 | .label STATUS_ALIVE = %01000000 //alive 19 | .label STATUS_XDIRECTION = %00000001 //going left or right. 0 = goes right, 1 goes left 20 | .label STATUS_YDIRECTION = %00000010 //going up or down. 0 = goes down, 1 = goes up 21 | .label STATUS_YMOVES = %00000100 //moves vertically. 1 = moves vertically in the direction of STATUS_YDIRECTION, 0 = still 22 | .label STATUS_XBOOST = %00001000 //has a boost in horizontal speed. 0 = no boost, 1 = boost. 23 | .label STATUS_YBOOST = %00010000 //has a boost in vertical speed. 0 = no boost, 1 = boost. 24 | .label STATUS_EXPLODING = %00100000 //if the enemy is exploding it won't interact with the rest of the world. 25 | 26 | //call at level start to place the enemies 27 | deploy: 28 | ldx #12 29 | stx nenemies 30 | 31 | dex 32 | !: 33 | lda #0 34 | sta xdelta_sub,x 35 | sta ydelta_sub,x 36 | sta enemy_clock,x 37 | 38 | lda enemypos_init_l,x 39 | sta xpos_l,x 40 | lda enemypos_init_h,x 41 | sta xpos_h,x 42 | 43 | jsr random_ 44 | anc #63 45 | adc #32 46 | sta xdelta,x 47 | lda random_.random + 1 48 | anc #127 49 | adc xdelta,x 50 | //it'll be a random numeber between 32 and 224 51 | sta xdelta,x 52 | 53 | lda random_.random + 0 54 | anc #15 55 | adc #4 //range 4-19 on a possible movement range of 0-24 56 | sta ydelta,x 57 | 58 | lda random_.random + 2 59 | and #[STATUS_PLANE | STATUS_XDIRECTION | STATUS_YDIRECTION] 60 | ora #STATUS_ALIVE 61 | sta status,x 62 | 63 | dex 64 | bpl !- 65 | 66 | lda #$20 //jsr 67 | sta updateonly 68 | 69 | rts 70 | 71 | //First part of the AI of the enemies. 72 | update: 73 | //first we move the centre of gravity one pixel to the left, so the motion range is in sync with the vans 74 | ldx #11 75 | lda #$ff 76 | !: dcp xpos_l,x 77 | beq !luf+ 78 | !next: 79 | dex 80 | bpl !- 81 | jmp !displ+ 82 | !luf: 83 | dcp xpos_h,x 84 | beq !wrap+ 85 | jmp !next- 86 | 87 | !wrap: 88 | lda #$09 89 | sta xpos_h,x 90 | lda #$ff 91 | sta xpos_l,x 92 | jmp !next- 93 | 94 | 95 | 96 | 97 | //draws the enemies. It can also apply the second part of the AI 98 | draw: 99 | !displ: 100 | 101 | lda #[empty_sprite & $3fff] / 64 102 | 103 | sta sprf + 5 104 | sta sprf + 6 105 | sta sprf + 7 106 | 107 | lda #$ff 108 | sta reverselut 109 | sta reverselut + 1 110 | sta reverselut + 2 111 | 112 | 113 | //we need to account for the border. 114 | lda camera_x + 1 115 | sec 116 | sbc #24 117 | sta p0tmp + 2 118 | lda camera_x + 2 119 | sbc #0 120 | sta p0tmp + 3 121 | 122 | ldy #11 //loop on enemies 123 | ldx #2 //loop on hw spr 124 | !displayloop: 125 | lda status,y 126 | and #STATUS_ALIVE | STATUS_EXPLODING 127 | bne !alive+ 128 | jmp !next+ 129 | !alive: 130 | lda xpos_l,y 131 | clc 132 | adc xdelta,y 133 | sta p0tmp 134 | lda xpos_h,y 135 | adc #0 136 | sta p0tmp + 1 137 | 138 | sec 139 | lda p0tmp 140 | sbc p0tmp + 2 //camera_x + 1 141 | sta p0tmp 142 | lda p0tmp + 1 143 | sbc p0tmp + 3 //camera_x + 2 144 | 145 | bpl !skp+ 146 | 147 | clc 148 | adc #$0a 149 | jmp !noov+ 150 | 151 | //sta p0tmp + 1 152 | !skp: 153 | cmp #$0a 154 | bcc !noov+ 155 | 156 | //lda p0tmp + 1 157 | sbc #$0a 158 | 159 | !noov: 160 | sta p0tmp + 1 161 | 162 | cmp #>348 //#>336 163 | bcc !ondisplay+ 164 | beq !skp+ 165 | jmp !next+ 166 | !skp: lda p0tmp 167 | cmp #<348 //<336 168 | bcc !ondisplay+ 169 | jmp !next+ 170 | 171 | !ondisplay: 172 | tya 173 | sta reverselut,x 174 | 175 | //first, check if it's exploding 176 | lda status,y 177 | and #STATUS_EXPLODING 178 | beq !skp+ 179 | //inc $d020 180 | lda enemies.enemy_clock,y 181 | alr #6 182 | eor #3 183 | adc #[explosion_sprite & $3fff] / 64 184 | sta sprf + 5,x 185 | lda #1 186 | sta sprc + 5,x 187 | lda enemies.enemy_clock,y 188 | sec 189 | sbc #1 190 | sta enemies.enemy_clock,y 191 | bne !nodone+ 192 | lda #0 193 | sta status,y 194 | 195 | !nodone: 196 | jmp !pos+ 197 | 198 | !skp: 199 | lda status,y 200 | //and #STATUS_PLANE 201 | bmi !plane+ 202 | //heli 203 | lda #1 204 | sta sprc + 5,x 205 | 206 | lda enemy_clock,y 207 | beq !rot+ 208 | sec 209 | sbc #2 210 | sta enemy_clock,y 211 | lda #[enemy2_sprite & $3fff] / 64 + 2 212 | sta sprf + 5,x 213 | jmp !pos+ 214 | 215 | !rot: 216 | lda clock 217 | alr #%00000010 218 | adc #[enemy2_sprite & $3fff] / 64 219 | sta sprf + 5,x 220 | lda status,y 221 | anc #STATUS_XDIRECTION 222 | beq !pos+ 223 | lda sprf + 5,x 224 | adc #3 225 | sta sprf + 5,x 226 | jmp !pos+ 227 | 228 | !plane: 229 | lda #6 230 | sta sprc + 5,x 231 | lda enemy_clock,y 232 | beq !skp+ 233 | sec 234 | sbc #1 235 | sta enemy_clock,y 236 | lsr 237 | lsr 238 | !skp: 239 | clc 240 | adc #[enemy1_sprite & $3fff] / 64 241 | sta sprf + 5,x 242 | lda status,y 243 | anc #STATUS_XDIRECTION 244 | beq !pos+ 245 | lda sprf + 5,x 246 | adc #4 247 | sta sprf + 5,x 248 | !pos: 249 | 250 | lda p0tmp 251 | sta sprxl + 5,x 252 | lda p0tmp + 1 253 | sta sprxh + 5,x 254 | 255 | lda enemies.ypos,y 256 | clc 257 | adc enemies.ydelta,y 258 | sta spry + 5,x 259 | 260 | //now we must also apply the second part of the AI, unless the craft is exploding 261 | 262 | lda enemies.status,y 263 | and #STATUS_EXPLODING 264 | bne !skp+ 265 | 266 | updateonly: 267 | jsr move_enemy //this can be forced off by writing $2c (bit opcode) to updateonly 268 | !skp: 269 | 270 | !next: 271 | dex 272 | bpl !+ 273 | 274 | ldx #2 275 | !: 276 | dey 277 | bmi !done+ 278 | jmp !displayloop- 279 | !done: 280 | 281 | rts 282 | 283 | 284 | move_enemy: 285 | 286 | //can it shoot 287 | lda bullet.alive 288 | ora bullet.alive + 1 289 | bne !next+ //all bullets are already on display 290 | 291 | jsr random_ 292 | lda random_.random 293 | shootprob: 294 | cmp #4 //this is changed at level init 295 | 296 | bcs !next+ 297 | 298 | jsr bullet.fire 299 | 300 | !next: 301 | lda #0 302 | sta p0tmp + 4 //extra boost x 303 | sta p0tmp + 5 //y 304 | lda status,y 305 | and #STATUS_XBOOST 306 | beq !skp+ 307 | inc p0tmp + 4 308 | !skp: 309 | lda status,y 310 | and #STATUS_YBOOST 311 | beq !skp+ 312 | inc p0tmp + 5 313 | !skp: 314 | 315 | //dont move if first part of flipping animation is ongoing 316 | lda enemy_clock,y 317 | cmp #8 318 | bcs !next+ 319 | 320 | lda status,y 321 | and #STATUS_XDIRECTION 322 | bne !left+ 323 | //right 324 | lda xdelta_sub,y 325 | clc 326 | adc enemy_level_xspeed 327 | sta xdelta_sub,y 328 | lda xdelta,y 329 | adc enemy_level_xspeed + 1 330 | adc #1 331 | adc p0tmp + 4 332 | sta xdelta,y 333 | cmp #255 - 2 - ENEMY_MAX_XSPEED 334 | bcc !testflip+ 335 | jmp !flip+ 336 | 337 | !left: 338 | lda xdelta_sub,y 339 | sec 340 | sbc enemy_level_xspeed 341 | sta xdelta_sub,y 342 | lda xdelta,y 343 | sbc enemy_level_xspeed + 1 344 | sbc p0tmp + 4 345 | sta xdelta,y 346 | cmp #ENEMY_MAX_XSPEED + 2 347 | bcs !testflip+ 348 | jmp !flip+ 349 | 350 | !testflip: 351 | jsr random_ 352 | cmp status,y 353 | bne !next+ 354 | 355 | !flip: 356 | jsr random_ 357 | and #STATUS_XBOOST 358 | sta xb + 1 359 | lda status,y 360 | and #$ff - STATUS_XBOOST 361 | eor #STATUS_XDIRECTION 362 | xb: ora #$00 363 | sta status,y 364 | 365 | lda #16 //make sure it's even 366 | sta enemy_clock,y 367 | 368 | !next: //take care of vertical behavior 369 | 370 | lda status,y 371 | and #STATUS_YMOVES 372 | bne !moves+ 373 | jmp !testflip+ 374 | 375 | !moves: 376 | lda status,y 377 | and #STATUS_YDIRECTION 378 | beq !up+ 379 | //down 380 | lda ydelta_sub,y 381 | clc 382 | adc enemy_level_yspeed 383 | sta ydelta_sub,y 384 | lda ydelta,y 385 | adc enemy_level_yspeed + 1 386 | //adc #1 387 | adc p0tmp + 5 388 | sta ydelta,y 389 | cmp #32 - 4 - ENEMY_MAX_YSPEED 390 | bcc !testflip+ 391 | jmp !flip+ 392 | 393 | !up: 394 | lda ydelta_sub,y 395 | sec 396 | sbc enemy_level_yspeed 397 | sta ydelta_sub,y 398 | lda ydelta,y 399 | sbc enemy_level_yspeed + 1 400 | sbc p0tmp + 5 401 | sta ydelta,y 402 | cmp #ENEMY_MAX_YSPEED + 1 403 | bcs !testflip+ 404 | 405 | jmp !flip+ 406 | 407 | !testflip: //vertically we test two things: changing from still to moving, and direction. 408 | jsr random_ 409 | ora #STATUS_ALIVE //the alive bit is always one in status,y, because the enemy is alive. so p = 1/128 410 | cmp status,y 411 | bne !nostillchange+ 412 | //a change must occour. 413 | lda status,y 414 | eor #STATUS_YMOVES 415 | sta status,y 416 | !nostillchange: 417 | //check if we at least flip direction 418 | jsr random_ 419 | cmp status,y 420 | bne !next+ 421 | 422 | !flip: 423 | jsr random_ 424 | and #STATUS_YBOOST 425 | sta yb + 1 426 | lda status,y 427 | and #$ff - STATUS_YBOOST 428 | eor #STATUS_YDIRECTION 429 | yb: ora #$00 430 | sta status,y 431 | 432 | 433 | !next: 434 | 435 | rts 436 | 437 | 438 | .const init_list = List() 439 | .for (var group = 0; group < 4; group ++) 440 | .for (var i = 0; i < 3; i++) 441 | .eval init_list.add($0000 + $180 + $280 * group - 48) 442 | 443 | enemypos_init_l: 444 | .fill 12,init_list.get(i) 447 | 448 | 449 | //left end of the enemy motion range 450 | xpos_l: 451 | .fill 12,0 452 | 453 | xpos_h: 454 | .fill 12,0 455 | 456 | ypos: 457 | .for (var i = 0; i < 4; i++) 458 | { 459 | .byte MINY, MINY + 32, MINY + 64 460 | } 461 | 462 | 463 | xdelta: //current position within the motion range 464 | .fill 12,0 465 | 466 | xdelta_sub: //subpixel accuracy for xdelta 467 | .fill 12,0 468 | 469 | ydelta: 470 | .fill 12,0 471 | 472 | ydelta_sub: 473 | .fill 12,0 474 | 475 | enemy_clock: 476 | .fill 12,0 //used for enemy animations 477 | 478 | status: 479 | .fill 12,0 480 | 481 | 482 | reverselut: //this associates the hw sprite number (0-2) to the enemy logical number (0-11) 483 | .fill 3, $ff //ff means it's not associated 484 | 485 | } 486 | 487 | 488 | 489 | //there are 12 trucks. 4 convoys of 3 trucks. They move as a single object, but we still store all 12 of them for sake of consistency 490 | //there are never more than three vans on screen at the same time, which would be awesome if we were using sprites. But we can't. 491 | 492 | vans: 493 | { 494 | deploy: 495 | ldx #12 496 | stx nvans 497 | dex 498 | !: 499 | lda vanpos_init_l,x 500 | sta xpos_l,x 501 | lda vanpos_init_h,x 502 | sta xpos_h,x 503 | lda #1 504 | sta status,x 505 | dex 506 | bpl !- 507 | 508 | rts 509 | 510 | update: 511 | 512 | 513 | //first move the vans 514 | //we move all of them, regardless of their status, because it's not worth checking if they are alive at this stage 515 | 516 | ldx #11 517 | lda #$ff 518 | !: dcp vans.xpos_l,x 519 | beq !luf+ 520 | !next: 521 | dex 522 | bpl !- 523 | jmp !displ+ 524 | !luf: 525 | dcp vans.xpos_h,x 526 | beq !wrap+ 527 | jmp !next- 528 | 529 | !wrap: 530 | lda #$09 531 | sta vans.xpos_h,x 532 | lda #$ff 533 | sta vans.xpos_l,x 534 | jmp !next- 535 | 536 | draw: 537 | 538 | !displ: 539 | 540 | lda #0 541 | sta hwsp + 1 //loop on "hw" pseudo sprites 542 | 543 | lda #$ff 544 | sta reverselut 545 | sta reverselut + 1 546 | sta reverselut + 2 547 | 548 | //first calculate the hwscroll offset for all the vans (because the gap between vans is a multiple of 8 pixels) 549 | 550 | sec 551 | lda vans.xpos_l 552 | sbc camera_x + 1 553 | and #$07 554 | ora #$d0 555 | 556 | sta vanscroll 557 | 558 | //erase row 559 | ldx #39 560 | lda #32 561 | !: sta $0400 + 19 * 40,x 562 | dex 563 | bpl !- 564 | 565 | lda clock 566 | alr #%00000100 //and + lsr and carry clear as a result so adc is safe 567 | adc #VANCHAR //we flip between the two frames of the animation every 8 frames 568 | sta van_frame + 1 569 | 570 | //the vans are two chars wide. one char is hidden by the left border in 38 columns mode, but we still need to account for the other one 571 | //so we consider the viewport 41 chars wide instead of 38-40 572 | lda camera_x + 1 573 | sec 574 | sbc #8 575 | sta p0tmp + 2 576 | lda camera_x + 2 577 | sbc #0 578 | sta p0tmp + 3 579 | 580 | ldx #11 //loop on vans 581 | !: 582 | lda status,x 583 | beq !next+ 584 | 585 | sec 586 | lda vans.xpos_l,x 587 | sbc p0tmp + 2 588 | sta p0tmp 589 | lda vans.xpos_h,x 590 | sbc p0tmp + 3 591 | 592 | bpl !skp+ 593 | 594 | clc 595 | adc #$0a 596 | jmp !noov+ 597 | 598 | //sta p0tmp + 1 599 | !skp: 600 | cmp #$0a 601 | bcc !noov+ 602 | 603 | //lda p0tmp + 1 604 | sbc #$0a 605 | 606 | !noov: 607 | sta p0tmp + 1 608 | 609 | cmp #>320 610 | bcc !ondisplay+ 611 | bne !next+ 612 | lda p0tmp 613 | cmp #<320 614 | bcs !next+ 615 | 616 | !ondisplay: 617 | hwsp: ldy #0 618 | txa 619 | sta reverselut,y 620 | 621 | 622 | 623 | lda p0tmp 624 | lsr p0tmp + 1 625 | ror 626 | lsr p0tmp + 1 627 | ror 628 | lsr p0tmp + 1 629 | ror 630 | 631 | sta pseudox,y 632 | inc hwsp + 1 633 | 634 | clc 635 | adc #<[$0400 + 19 * 40 - 1] 636 | sta p0tmp 637 | lda p0tmp + 1 638 | adc #>[$0400 + 19 * 40 - 1] 639 | sta p0tmp + 1 640 | ldy #0 641 | van_frame: 642 | lda #VANCHAR 643 | sta (p0tmp),y 644 | iny 645 | ora #1 //this is like adding 1 to Accumulator, because VANCHAR is aligned to 16 bytes. 646 | sta (p0tmp),y 647 | 648 | !next: 649 | dex 650 | bpl !- 651 | !done: 652 | //we have drawn all the vans. Chances are that the char at position (18,39) is "dirty" with the front of the first van on screen 653 | //that would be under the border, so normally we wouldn't need to worry about that, but it could still trigger collisions with the enemy 654 | //so let's erase it for good measure 655 | lda #32 656 | sta $0400 + 19 * 40 - 1 657 | 658 | rts 659 | 660 | .const init_list = List() 661 | .for (var group = 0; group < 4; group ++) 662 | .for (var i = 0; i < 3; i++) 663 | .eval init_list.add($0000 + $180 + $280 * group + i * 80) 664 | 665 | vanpos_init_l: 666 | .fill 12,init_list.get(i) 669 | 670 | xpos_l: //pixels 671 | .fill 12,0 672 | xpos_h: 673 | .fill 12,0 674 | 675 | status: 676 | .fill 12,0 677 | 678 | vanscroll: 679 | .byte $c0 680 | 681 | pseudox: 682 | .fill 3, 0 683 | 684 | reverselut: 685 | .fill 3, $ff 686 | 687 | } 688 | 689 | 690 | //the enemy bullet-pair is done with two identical sprites. Initially they overlap. 691 | //after a while, they split and move only vertically. 692 | //this means that the bullets share the same horizontal position all through their lifetime 693 | //uses p0tmp + 4, + 5 694 | bullet: 695 | { 696 | 697 | fire: 698 | //if we are here, we expect that y contains the logical enemy number, and x the sprite number 699 | 700 | lda #1 701 | sta alive 702 | sta alive + 1 703 | 704 | lda #0 705 | sta xdir 706 | sta ydir 707 | 708 | jsr random_ 709 | lsr 710 | 711 | sta stage //random duration of stage one of the bullet, 0-127 frames 712 | 713 | bcc !skp+ 714 | 715 | ror ydir //50% chance of also moving vertically (in the direction of the player) 716 | 717 | 718 | lda spry 719 | cmp spry,x 720 | bcs !skp+ 721 | inc ydir //if hero chopper flies higher than enemy, bullet goes up. 722 | 723 | !skp: 724 | 725 | 726 | lda sprxh 727 | lsr 728 | lda sprxl 729 | ror 730 | sta p0tmp + 4 731 | 732 | lda sprxh,x 733 | lsr 734 | lda sprxl,x 735 | ror 736 | cmp p0tmp + 4 737 | 738 | bcc !skp+ 739 | 740 | inc xdir 741 | 742 | !skp: 743 | lda enemies.ypos,y 744 | clc 745 | adc enemies.ydelta,y 746 | 747 | sta ypos 748 | sta ypos + 1 749 | 750 | lda enemies.xpos_l,y 751 | clc 752 | adc enemies.xdelta,y 753 | sta xpos_l 754 | lda enemies.xpos_h,y 755 | adc #0 756 | sta xpos_h 757 | 758 | rts 759 | 760 | 761 | update: 762 | { 763 | lda #[empty_sprite & $3fff] / 64 764 | sta sprf + 3 765 | sta sprf + 4 766 | 767 | 768 | lda alive 769 | ora alive + 1 770 | bne !ok+ 771 | 772 | rts 773 | 774 | !ok: 775 | 776 | lda stage 777 | beq !stage2+ 778 | bmi !stage2+ 779 | 780 | sec 781 | sbc bulletspeed 782 | sta stage 783 | 784 | bcs !skp+ 785 | 786 | lda #0 787 | sta stage 788 | !skp: 789 | 790 | lda xdir 791 | bne !left+ 792 | 793 | //goes right 794 | lda xpos_l 795 | clc 796 | adc bulletspeed //#2 797 | sta xpos_l 798 | 799 | lda xpos_h 800 | adc #0 801 | cmp #$0a 802 | 803 | bcc !skp+ 804 | sbc #$0a 805 | !skp: 806 | sta xpos_h 807 | jmp !next+ 808 | 809 | !left: 810 | lda xpos_l 811 | sec 812 | sbc bulletspeed //#2 813 | sta xpos_l 814 | 815 | lda xpos_h 816 | sbc #0 817 | bpl !skp+ 818 | adc #$0a 819 | !skp: 820 | sta xpos_h 821 | 822 | !next: //vertical movement 823 | lda ydir 824 | bpl !disp+ // no vertical movement 825 | bne !up+ 826 | //down 827 | 828 | inc ypos 829 | inc ypos + 1 830 | lda ypos 831 | cmp #MAXY - 8 832 | bcc !disp+ 833 | lda #0 834 | sta stage 835 | jmp !disp+ 836 | !up: 837 | dec ypos 838 | dec ypos + 1 839 | lda ypos 840 | cmp #MINY + 8 841 | bcs !disp+ 842 | lda #0 843 | sta stage 844 | jmp !disp+ 845 | 846 | !stage2: 847 | 848 | lda alive 849 | beq !skp+ 850 | lda ypos 851 | sbc #4 852 | sta ypos 853 | cmp #MINY - 8 854 | bcs !skp+ 855 | lda #0 856 | sta alive 857 | !skp: 858 | lda alive + 1 859 | beq !skp+ 860 | bmi !skp+ //alive + 1 > 127 means the bullet sprite must be used for an explosion 861 | lda ypos + 1 862 | adc #4 863 | sta ypos + 1 864 | cmp #MAXY + 8 865 | bcc !skp+ 866 | lda #0 867 | sta alive + 1 868 | !skp: 869 | jmp !disp+ 870 | 871 | !disp: 872 | 873 | //we need to account for the border. 874 | lda camera_x + 1 875 | sec 876 | sbc #24 877 | sta p0tmp + 2 878 | lda camera_x + 2 879 | sbc #0 880 | sta p0tmp + 3 881 | 882 | 883 | ldx #1 //loop on hw spr 884 | !displayloop: 885 | lda alive,x 886 | beq !next+ 887 | 888 | sec 889 | lda xpos_l 890 | sbc p0tmp + 2 //camera_x + 1 891 | sta p0tmp 892 | lda xpos_h 893 | sbc p0tmp + 3 //camera_x + 2 894 | 895 | bpl !skp+ 896 | 897 | clc 898 | adc #$0a 899 | jmp !noov+ 900 | 901 | //sta p0tmp + 1 902 | !skp: 903 | cmp #$0a 904 | bcc !noov+ 905 | 906 | //lda p0tmp + 1 907 | sbc #$0a 908 | 909 | !noov: 910 | sta p0tmp + 1 911 | 912 | cmp #>348 // #>336 913 | bcc !ondisplay+ 914 | beq !lc+ 915 | !notondisplay: 916 | lda #0 917 | sta alive,x 918 | jmp !next+ 919 | 920 | !lc: 921 | lda p0tmp 922 | cmp #<348 923 | bcc !ondisplay+ 924 | jmp !notondisplay- 925 | 926 | 927 | !ondisplay: 928 | 929 | lda p0tmp 930 | sta sprxl + 3,x 931 | lda p0tmp + 1 932 | sta sprxh + 3,x 933 | 934 | lda ypos,x 935 | sta spry + 3,x 936 | 937 | lda #$0d 938 | sta sprc + 3,x 939 | !next: 940 | dex 941 | bpl !displayloop- 942 | 943 | 944 | ldx #[bullet_sprites & $3fff] / 64 + 1 945 | lda alive 946 | beq !b2+ 947 | lda stage 948 | beq !small+ 949 | dex 950 | !small: 951 | stx sprf + 3 952 | 953 | 954 | !b2: 955 | ldx #[bullet_sprites & $3fff] / 64 + 1 956 | lda alive + 1 957 | beq !done+ 958 | bmi !exp+ 959 | lda stage 960 | beq !small+ 961 | 962 | dex 963 | !small: 964 | stx sprf + 4 965 | jmp !done+ 966 | 967 | !exp: 968 | lda #0 969 | sta sprc + 4 970 | 971 | lda explosionframe 972 | sta sprf + 4 973 | 974 | clc 975 | adc #1 976 | cmp #[explosion_sprite & $3fff] / 64 + 4 977 | bcc !ok+ 978 | 979 | lda #0 980 | sta alive + 1 981 | 982 | lda #[explosion_sprite & $3fff] / 64 983 | 984 | !ok: 985 | sta explosionframe 986 | 987 | !done: 988 | rts 989 | 990 | } 991 | 992 | stage: // the bullets are joined if stage > 0, otherwise they are moving vertically. 993 | .byte 0 994 | 995 | xpos_l: 996 | .byte 0 997 | xpos_h: 998 | .byte 0 999 | 1000 | ypos: 1001 | .byte 0,0 1002 | 1003 | alive: 1004 | .byte 0,0 1005 | 1006 | //0 goes right, 1 goes left. A bullet always moves horizontally if stage >0 1007 | xdir: 1008 | .byte 0 1009 | //0 goes down, 1 goes up, >=128 it doesn't move vertically 1010 | ydir: 1011 | .byte 0 1012 | 1013 | explosionframe: 1014 | .byte [explosion_sprite & $3fff] / 64 1015 | } -------------------------------------------------------------------------------- /src/game.asm: -------------------------------------------------------------------------------- 1 | /* 2 | Chopper Command 3 | Coded by Antonio Savona 4 | */ 5 | 6 | //#define DEBUG 7 | 8 | //#define INVINCIBLE 9 | 10 | .const KOALA_TEMPLATE = "C64FILE, Bitmap=$0000, ScreenRam=$1f40, ColorRam=$2328, BackgroundColor = $2710" 11 | .var kla = LoadBinary("../assets/splash.kla", KOALA_TEMPLATE) 12 | 13 | .const p0start = $02 14 | .var p0current = p0start 15 | .function reservep0(n) 16 | { 17 | .var result = p0current 18 | .eval p0current = p0current + n 19 | .return result 20 | } 21 | 22 | .const sid = LoadSid("..\sid\chopper_command_c64.sid") 23 | 24 | //allocates page zero 25 | .const sprf = reservep0(8) 26 | .const sprxl = reservep0(8) 27 | .const spry = reservep0(8) 28 | .const sprxh = reservep0(8) 29 | .const sprc = reservep0(8) 30 | 31 | .const shadowd01f = reservep0(1) 32 | .const shadowd01e = reservep0(1) 33 | 34 | .const level = reservep0(1) 35 | .const enemy_level_xspeed = reservep0(2) // horizontal speed of the enemies, a function of the level 36 | .const enemy_level_yspeed = reservep0(2) // vertical speed of the enemies, a function of the level 37 | .const score = reservep0(6) 38 | 39 | 40 | .const tozero = sprf 41 | .const tozerosize = p0current //during init, we zero from sprxl to here 42 | 43 | .const nenemies = reservep0(1) //number of enemies left 44 | .const nvans = reservep0(1) //number of trucks left 45 | .const lives = reservep0(1) //lives 46 | 47 | //upon starting a mission, the chopper enjoys a little bit of invincibility. 48 | .const invincibility = reservep0(1) 49 | //invincibility can be explicit (blink = 1) or not. when an enemy dies the player is granted 8 frames of invincibility 50 | //to let the explosion animation run. 51 | .const blink = reservep0(1) 52 | 53 | .const camera_x = reservep0(3) //left hand side of the viewport, in pixels, with subpixel accuracy. 54 | 55 | .const clock = reservep0(1) //frame counter. Used for several animations 56 | .const xpos = reservep0(3) //player position in pixel with subpixel accuracy 57 | .const ypos = reservep0(1) //player vertical position in pixel. No subpixel accuracy is required here as there's no vertical acceleration 58 | .const speed_sign = reservep0(1) //0 = going right, 1 = going left 59 | .const speed = reservep0(2) //speed in pixels per frame, with subpixel accuracy 60 | .const facing = reservep0(1) //were the chopper is facing 61 | .const flipclock = reservep0(1) //when the elicopter changes direction, a counter is used to keep track of how many frames the intermediate sprite has been on display 62 | .const camera_target = reservep0(2) 63 | 64 | .const bulletspeed = reservep0(1) 65 | 66 | .const redraw = reservep0(1) //signals that all the objects have been updated and screen can be updated too 67 | .const hwscroll = reservep0(1) //hw scroll (0..7) 68 | .const clouds_hwscroll = reservep0(1) //clouds part 69 | 70 | .const p0tmp = reservep0(8) 71 | .const wp0tmp = reservep0(8) 72 | 73 | .const p0radar = reservep0(2) //points somewhere within the charset 74 | 75 | .const MAXY = 179 + 3 76 | .const MINY = 84 + 3 //make sure that maxy-miny = 96-1. That's 12 chars minus one pixel 77 | 78 | .const ACCELERATION = %01000000 //you can play with this and enjoy a snappy or ultra un-responsive chopper. These default values seem to match the original's responsiveness 79 | .const DECELERATION = %00010000 80 | 81 | .const MAXSPEED = 6 //helicopter max horizontal speed in pixels per frame. This can be altered as well, but high values will make it hard for the camera to catch up 82 | 83 | .const ENEMY_MAX_XSPEED = 8 //maximum pixel per frame speed for the enemies. Planes are 1 pixel per frame faster. Don't go too wild on this 84 | .const ENEMY_MAX_YSPEED = 4 85 | 86 | 87 | 88 | // Place loading bitmap 89 | .pc = $8000 "kla scrn" 90 | .fill 1000,kla.getScreenRam(i) 91 | 92 | .pc = $8400 "kla col" 93 | .fill 1000,kla.getColorRam(i) 94 | 95 | .pc = $a000 "kla bmp" 96 | .fill 8000,kla.getBitmap(i) 97 | 98 | 99 | .pc = $3800 "charset" 100 | charset: 101 | .const bgcharset = LoadBinary("..\bg\bg.imap") 102 | .fill bgcharset.getSize(),bgcharset.get(i) 103 | clouds_charset: 104 | .const clcharset = LoadBinary("..\bg\clouds.imap") 105 | .fill clcharset.getSize(),clcharset.get(i) 106 | .align $8 107 | radar_mountains: 108 | .import binary "..\radar\mountains.imap" 109 | 110 | .align $10 111 | .const VANCHAR = (* - charset) / 8 112 | van_char: 113 | .const van_png = LoadPicture("..\sprites\charvan.png") 114 | .for (var x = 0; x < 4; x++) 115 | .for (var y = 0; y < 8; y++) 116 | .byte van_png.getSinglecolorByte(x,y) 117 | 118 | .const BOTTOMBORDERCHAR = (* - charset) / 8 119 | .byte %11111111 120 | .byte %01010101 121 | .byte %01010101 122 | .byte %01010101 123 | .byte %01010101 124 | .byte %01010101 125 | .byte %01010101 126 | .byte %01010101 127 | 128 | .const FULLBOTTOMCHAR = (* - charset) / 8 129 | .fill 8, %01010101 130 | 131 | .const BEAMCHAR = (* - charset) / 8 132 | 133 | .byte %11111111 134 | .byte %00000000 135 | .byte %00000000 136 | .byte %00000000 137 | .byte %00000000 138 | .byte %00000000 139 | .byte %00000000 140 | .byte %00000000 141 | 142 | .byte %00000000 143 | .byte %00000000 144 | .byte %11111111 145 | .byte %00000000 146 | .byte %00000000 147 | .byte %00000000 148 | .byte %00000000 149 | .byte %00000000 150 | 151 | .byte %00000000 152 | .byte %00000000 153 | .byte %00000000 154 | .byte %00000000 155 | .byte %11111111 156 | .byte %00000000 157 | .byte %00000000 158 | .byte %00000000 159 | 160 | .byte %00000000 161 | .byte %00000000 162 | .byte %00000000 163 | .byte %00000000 164 | .byte %00000000 165 | .byte %00000000 166 | .byte %11111111 167 | .byte %00000000 168 | 169 | //mastering the art of pixelling! 170 | 171 | //this picture contains the large GAME OVER and GETREADY signs 172 | .const grgo = LoadPicture("..\assets\grgo.png") 173 | 174 | //target area for game over and get ready signs 175 | bigsign_area: 176 | .const BIGSIGNCHAR = (* - charset) / 8 177 | .fill 18*2*8,0 178 | 179 | 180 | //2 lines of 16 chars will do for the radar 181 | .align $100 182 | .const RADARMAPCHAR = (* - charset) / 8 183 | radar_map: 184 | .fill 16*2*8,0 185 | 186 | .pc = $3400 + 40 * 17 "credits" 187 | 188 | .text " Adaptation by " 189 | .text " " 190 | .text " A. Savona : Code " 191 | .text " S. Day : Graphics " 192 | .text " S. Cross : Music " 193 | .text " F. Martins : Sfx " 194 | .text " " 195 | .text " Copyright 1982, 1984 Activision, Inc. " 196 | 197 | .pc = $2900 "sprites" 198 | hero_sprite: 199 | :LoadSprites("..\sprites\hero.bin",0,4) 200 | .fill 4 * 64,0 201 | :LoadSprites("..\sprites\hero.bin",8,2) 202 | 203 | bullet_sprites: 204 | .import binary "..\sprites\bullet.bin" 205 | enemy1_sprite: 206 | :LoadSprites("..\sprites\enemy1.bin",0,4) 207 | .fill 4 * 64,0 208 | 209 | enemy2_sprite: 210 | :LoadSprites("..\sprites\enemy2.bin",0,3) 211 | .fill 2 * 64,0 212 | 213 | hud_chopper_sprite: 214 | .import binary "..\sprites\hud_chopper.bin" 215 | explosion_sprite: 216 | .import binary "..\sprites\explosion.bin" 217 | 218 | score_sprites: 219 | .fill 64 * 2, 0 220 | 221 | 222 | logo_sprites: 223 | .var logopic = LoadPicture("..\assets\activisionlogo2.png",List().add($0000ff, $000000, $ffffff, $5c4700)) 224 | .for (var s=0;s<6;s++) 225 | { 226 | .for (var y=0; y<21; y++) 227 | .for (var x=0; x<3; x++) 228 | .byte logopic.getMulticolorByte(s*3 + x,y) 229 | .byte 0 230 | } 231 | logo_bars: 232 | .var barpic = LoadPicture("..\assets\activisionlogo_bands.png",List().add($0000ff, $ffffff)) 233 | .for (var y=0; y<21; y++) 234 | .for (var x=0; x<3; x++) 235 | .byte barpic.getSinglecolorByte(x,y) 236 | .byte 0 237 | 238 | //a blank sprite is a convenient way of switching sprites off without messing around with $d015 too much 239 | empty_sprite: 240 | .fill 64 , 0 241 | 242 | .align $100 243 | .pc = * "bgpattern" 244 | bgpattern: 245 | .const bgmap = LoadBinary("..\bg\bg.iscr") 246 | .const bgpattern_borderchar = bgmap.get(5 * 40) 247 | 248 | 249 | .fill 20,bgmap.get(20 + i + 3 * 40) 250 | .fill 40,bgmap.get(i + 3 * 40) 251 | .fill 20,bgmap.get(i + 3 * 40) 252 | 253 | //row 2 254 | .fill 20,bgmap.get(20 + i + 4 * 40) 255 | .fill 40,bgmap.get(i + 4 * 40) 256 | .fill 20,bgmap.get(i + 4 * 40) 257 | 258 | 259 | //cloud pattern 260 | .align $100 261 | cloudpattern: 262 | .const clmap = LoadBinary("..\bg\clouds.iscr") 263 | .for (var i=0; i<4; i++) 264 | .fill 16,clmap.get(i) + [clouds_charset - charset]/8 265 | .for (var i=0; i<4; i++) 266 | .fill 16,clmap.get(i + 16) + [clouds_charset - charset]/8 267 | 268 | 269 | //charmap and colormap for the bottom area. 5 charlines 270 | bottom_charmap: 271 | 272 | //first line, just a single line 273 | .fill 40, BOTTOMBORDERCHAR 274 | 275 | .fill 12, FULLBOTTOMCHAR 276 | .fill 16, 32 //this will be covered by mountains 277 | .fill 12, FULLBOTTOMCHAR 278 | 279 | .fill 12, FULLBOTTOMCHAR 280 | .fill 16, RADARMAPCHAR + i * 2 281 | .fill 12, FULLBOTTOMCHAR 282 | 283 | .fill 12, FULLBOTTOMCHAR 284 | .fill 16, RADARMAPCHAR + i * 2 + 1 285 | .fill 12, FULLBOTTOMCHAR 286 | 287 | .fill 12, FULLBOTTOMCHAR 288 | .fill 16, 32 289 | .fill 12, FULLBOTTOMCHAR 290 | 291 | bottom_colormap: 292 | .fill 40,8 //black as the char color, for the border 293 | .fill 40,8 + 3 //cyan for the sky haze 294 | .fill 120, 8 //black for everything else. We will use it for the map as well 295 | 296 | //ok, here we go. 297 | .pc = $0801 298 | :BasicUpstart(main) 299 | 300 | 301 | .pc = $0820 "main" 302 | main: 303 | sei 304 | 305 | lda #$35 306 | sta $01 307 | 308 | jsr vsync 309 | 310 | //blank 311 | lda #$0b 312 | sta $d011 313 | 314 | //clear sid and vic 315 | ldx #$3f 316 | lda #0 317 | !: 318 | sta $d400,x 319 | sta $d000,x 320 | dex 321 | bpl !- 322 | 323 | //set MC color 1 and 2 324 | lda #$09 325 | sta $d022 326 | lda #$02 327 | sta $d023 328 | 329 | 330 | //init the sprites, mirroring them 331 | //normally there wouldn't be any need to do this, but we want to save few bytes after compression. 332 | //also, we can show off this clever sprite flipping function by yours truly 333 | 334 | jsr flip_sprite.init 335 | 336 | ldx #[hero_sprite & $3fff] / 64 337 | ldy #[hero_sprite & $3fff] / 64 + 5 338 | jsr flip_sprite.flip 339 | 340 | ldx #[hero_sprite & $3fff] / 64 + 1 341 | ldy #[hero_sprite & $3fff] / 64 + 4 342 | jsr flip_sprite.flip 343 | 344 | ldx #[hero_sprite & $3fff] / 64 + 2 345 | ldy #[hero_sprite & $3fff] / 64 + 7 346 | jsr flip_sprite.flip 347 | 348 | ldx #[hero_sprite & $3fff] / 64 + 3 349 | ldy #[hero_sprite & $3fff] / 64 + 6 350 | jsr flip_sprite.flip 351 | 352 | .for (var i = 0; i < 4; i++) 353 | { 354 | ldx #[enemy1_sprite & $3fff] / 64 + 0 + i 355 | ldy #[enemy1_sprite & $3fff] / 64 + 4 + i 356 | jsr flip_sprite.flip 357 | } 358 | 359 | .for (var i = 0; i < 2; i++) 360 | { 361 | ldx #[enemy2_sprite & $3fff] / 64 + 0 + i 362 | ldy #[enemy2_sprite & $3fff] / 64 + 3 + i 363 | jsr flip_sprite.flip 364 | } 365 | 366 | 367 | //disable NMI 368 | lda #irq_bg0.any_rti 371 | sta $fffb 372 | lda #$00 // stop Timer A 373 | sta $DD0E // 374 | sta $DD04 // set Timer A to 0, after starting 375 | sta $DD05 // NMI will occur immediately 376 | lda #$81 // 377 | sta $DD0D // set Timer A as source for NMI 378 | lda #$01 // 379 | sta $DD0E // start Timer A -> NMI 380 | 381 | 382 | jsr showpic 383 | 384 | 385 | ig: 386 | sei 387 | 388 | jsr splash 389 | 390 | jsr vsync 391 | lda #$0b 392 | sta $d011 393 | 394 | lda #$03 395 | sta $dd00 396 | 397 | lda #$d8 398 | sta $d016 399 | 400 | lda #%00011110 401 | sta $d018 402 | 403 | jsr clear_irq 404 | 405 | lda #irq_bg0 408 | sta $ffff 409 | lda #69 //73 410 | sta $d012 411 | 412 | jsr init_game 413 | jsr init_playfield 414 | 415 | cli 416 | 417 | il: 418 | jsr init_level 419 | jsr enemies.deploy 420 | jsr vans.deploy 421 | ih: 422 | jsr hero.init 423 | 424 | jsr getready 425 | 426 | gameloop: 427 | 428 | jsr controls 429 | jsr hero.update 430 | jsr vans.update 431 | jsr enemies.update 432 | jsr bullet.update 433 | jsr radar.plot 434 | //jsr beam.update //this is done in irq, to prevent some tearing effect on screen. Also, bullets will nicely get off screen when you die 435 | jsr camera.chase 436 | jsr plot_mountains 437 | jsr rotor 438 | jmp testcollisions //we cant jsr because testcollision can break out from this loop 439 | returnfromtestcollisions: 440 | jmp testgamestatus //we cant jsr because testgamestatus can break out from this loop 441 | returnfromtestgamestatus: 442 | inc clock 443 | 444 | jsr panelvsync 445 | #if DEBUG 446 | jsr debug 447 | #endif 448 | jmp gameloop 449 | 450 | //Tests some conditions that will break the main loop. Namely: 451 | //-all enemies destroyed: start new level 452 | //-All vans destroyed: game over 453 | //-Player out of choppers: game over 454 | //furthermore, it checks for spacebar being pressed (pause) and then spacebar again (back to game) 455 | //testgamestatus is inline because it must be able to break out from the main loop 456 | testgamestatus: 457 | { 458 | lda score + 5 459 | cmp #9 460 | beq !gameover+ //"good" gameover 461 | 462 | lda nvans 463 | beq !gameover+ //bad gameover. 464 | 465 | lda nenemies 466 | beq levelup 467 | 468 | lda #239 469 | cmp $dc01 470 | beq !pause+ 471 | 472 | jmp returnfromtestgamestatus 473 | 474 | !gameover: 475 | jmp gameover 476 | 477 | !pause: 478 | 479 | !: jsr vsync //wait for release 480 | cmp $dc01 481 | beq !- 482 | 483 | !: jsr vsync 484 | cmp $dc01 485 | bne !- 486 | 487 | !: jsr vsync //wait for release 488 | cmp $dc01 489 | beq !- 490 | 491 | jmp returnfromtestgamestatus 492 | 493 | 494 | levelup: 495 | 496 | //update score. 497 | //from the manual: Evrey time you complete a level, you get a bonus that is 100*level*ntrucksleft 498 | 499 | //wait some time with hero on display 500 | lda #0 501 | sta bullet.alive 502 | sta bullet.alive + 1 503 | 504 | lda #$1f 505 | jsr activewait 506 | 507 | jsr eraseplayarea 508 | 509 | lda #[empty_sprite & $3fff] / 64 510 | sta sprf + 2 511 | sta sprf + 3 512 | sta sprf + 4 513 | sta sprf + 5 514 | sta sprf + 6 515 | sta sprf + 7 516 | 517 | :sfx(sfx_bonus,2) 518 | 519 | lda level 520 | cmp #10 521 | bcc !ok+ 522 | lda #9 523 | !ok: 524 | sta p0tmp + 7 //multiplier 525 | 526 | ldx #11 //loops on vans 527 | !v: stx savex + 1 528 | lda vans.status,x 529 | beq !nextvan+ 530 | 531 | lda #0 532 | sta vans.status,x 533 | 534 | jsr vans.draw 535 | jsr radar.plot_friends 536 | 537 | lda p0tmp + 7 538 | sta p0tmp + 6 539 | 540 | !: lda #1 541 | jsr add_score 542 | dec p0tmp + 6 543 | bpl !- 544 | 545 | !nextvan: 546 | 547 | ldx #$03 548 | !delay: 549 | jsr panelvsync 550 | dex 551 | bpl !delay- 552 | 553 | savex: 554 | ldx #0 555 | dex 556 | bpl !v- 557 | 558 | ldx #$0f 559 | !: jsr panelvsync 560 | dex 561 | bpl !- 562 | 563 | inc level 564 | jmp il 565 | } 566 | 567 | 568 | #if DEBUG 569 | debug: 570 | { 571 | 572 | lda camera_x + 2 573 | lsr 574 | lsr 575 | lsr 576 | lsr 577 | tax 578 | lda digimap,x 579 | sta $0402 580 | lda camera_x + 2 581 | and #$0f 582 | tax 583 | lda digimap,x 584 | sta $0403 585 | 586 | lda camera_x + 1 587 | lsr 588 | lsr 589 | lsr 590 | lsr 591 | tax 592 | lda digimap,x 593 | sta $0404 594 | lda camera_x + 1 595 | and #$0f 596 | tax 597 | lda digimap,x 598 | sta $0405 599 | 600 | 601 | lda xpos + 2 602 | lsr 603 | lsr 604 | lsr 605 | lsr 606 | tax 607 | lda digimap,x 608 | sta $0407 609 | lda xpos + 2 610 | and #$0f 611 | tax 612 | lda digimap,x 613 | sta $0408 614 | 615 | lda xpos + 1 616 | lsr 617 | lsr 618 | lsr 619 | lsr 620 | tax 621 | lda digimap,x 622 | sta $0409 623 | lda xpos + 1 624 | and #$0f 625 | tax 626 | lda digimap,x 627 | sta $040a 628 | 629 | 630 | lda nenemies 631 | and #$0f 632 | tax 633 | lda digimap,x 634 | sta $040c 635 | 636 | rts 637 | 638 | digimap: 639 | .fill 10, '0' + i 640 | .fill 6, 'a'+ i 641 | } 642 | #endif 643 | 644 | 645 | //we over overrid the classic vblank with this. Our blank really starts at the Radar, and we need all the rastertime we can have 646 | panelvsync: 647 | { 648 | !: lda redraw 649 | beq !- 650 | lda #0 651 | sta redraw 652 | rts 653 | } 654 | 655 | 656 | //first in-game, raster irq. 657 | //triggers right after score and life have been displayed and re-uses sprites for in-game objects 658 | //it also performs all the $d021-$d022-$d023 tweaks to draw the sky gradient. 659 | irq_bg0: 660 | { 661 | sta savea + 1 662 | stx savex + 1 663 | sty savey + 1 664 | 665 | #if DEBUG 666 | lda #%00011110 667 | sta $d018 668 | #endif 669 | 670 | 671 | //place game sprites 672 | lda #0 673 | .for (var i=0; i<8; i++) 674 | { 675 | ldx sprf + i 676 | stx $0400 + $3f8 + i 677 | ldx sprc + i 678 | stx $d027 + i 679 | ldx sprxl + i 680 | stx $d000 + i * 2 681 | ldx spry + i 682 | stx $d001 + i * 2 683 | ldx sprxh + i 684 | beq !skp+ 685 | ora #[1 << i] 686 | !skp: 687 | } 688 | sta $d010 689 | 690 | 691 | lda #04 692 | ldx #74 693 | cpx $d012 694 | bne *-3 695 | sta $d021 696 | 697 | lda hwscroll 698 | sta $d016 699 | 700 | lda #$09 701 | sta $d022 702 | lda #$02 703 | sta $d023 704 | 705 | lda #$0e //badline 706 | ldx #5 707 | !: dex 708 | bpl !- 709 | sta $d021 710 | 711 | //clear collision register 712 | lda $d01f 713 | 714 | lda #$ff 715 | sta $d01b //char priority 716 | 717 | lda #%00000000 718 | sta $d01d 719 | 720 | lda #$ff 721 | sta $d015 722 | 723 | 724 | lda #04 725 | ldx #77 726 | cpx $d012 727 | bne *-3 728 | sta $d021 729 | 730 | lda #$0e 731 | ldx #78 732 | cpx $d012 733 | bne *-3 734 | sta $d021 735 | 736 | lda #04 737 | ldx #79 738 | cpx $d012 739 | bne *-3 740 | sta $d021 741 | 742 | lda #08 743 | ldx #81 744 | cpx $d012 745 | bne *-3 746 | sta $d021 747 | 748 | lda #04 749 | ldx #82 750 | cpx $d012 751 | bne *-3 752 | sta $d021 753 | 754 | //badline, so active wait for the win 755 | lda #08 756 | ldx #7 757 | !: dex 758 | bpl !- 759 | sta $d021 760 | 761 | lda #10 762 | ldx #84 763 | cpx $d012 764 | bne *-3 765 | sta $d021 766 | 767 | lda #$07 768 | ldx #85 769 | cpx $d012 770 | bne *-3 771 | sta $d021 772 | 773 | lda #13 774 | ldx #87 775 | cpx $d012 776 | bne *-3 777 | sta $d021 778 | 779 | lda #$01 780 | ldx #87 781 | cpx $d012 782 | bcs *-3 783 | sta $d021 784 | 785 | 786 | lda #08 787 | ldx #90 788 | cpx $d012 789 | bcs *-3 790 | sta $d021 791 | 792 | lda #$c0 793 | sta $d016 794 | 795 | //play music and effects 796 | jsr sid.play 797 | 798 | //reports to the game engine that objects have been placed and next frame can be calculated 799 | lda #1 800 | sta redraw 801 | 802 | lda #$d2-8 803 | sta $d012 804 | 805 | lda #irq_vans 808 | sta $ffff 809 | 810 | //ack irq 811 | lsr $d019 812 | 813 | savey: ldy #$00 814 | savea: lda #$00 815 | savex: ldx #$00 816 | any_rti: 817 | rti 818 | } 819 | 820 | 821 | //triggers at the trucks line. uses hardware scrolling to move the vans, which are chars 822 | irq_vans: 823 | { 824 | pha 825 | lda vans.vanscroll 826 | sta $d016 827 | 828 | lda #$d3 829 | sta $d012 830 | 831 | lda #irq_panel 834 | sta $ffff 835 | 836 | lsr $d019 837 | pla 838 | rti 839 | } 840 | 841 | //triggers right before the HUD. takes care of setting sprites for the logo and adjusting colors for the radars. 842 | //it also saves spr-spr and spr-bg registers, which will be used by the game engine to test various game conditions. 843 | //Sprites can therefore be reused for the logo. 844 | irq_panel: 845 | { 846 | pha 847 | txa 848 | pha 849 | 850 | 851 | lda #$d0 852 | sta $d016 853 | 854 | lda #$00 855 | sta $d01b //sprite priority 856 | 857 | lda $d01f 858 | sta shadowd01f 859 | 860 | lda $d01e 861 | sta shadowd01e 862 | 863 | lda #$e0 864 | sta $d012 865 | 866 | lda #$1b 867 | sta $d011 868 | 869 | lda #irq_panel2 872 | sta $ffff 873 | 874 | lsr $d019 875 | 876 | //set the sky color in radar 877 | lda #$0e 878 | sta $d021 879 | 880 | //place the logo 881 | ldx #$f0 //should be ee, but we do the play area of 13 pixels instead of 12, because we draw fat vertical pixels, and we want to prevent collision with logo sprites 882 | .for (var i = 2; i < 8; i++) 883 | { 884 | stx $d001 + i * 2 885 | lda #24 + 11*8 + 24 * (i-2) 886 | sta $d000 + i * 2 887 | } 888 | ldx #[logo_sprites & $3fff] / 64 889 | .for (var i = 2; i < 8; i++) 890 | { 891 | stx $0400 + $3f8 + i 892 | .if (i<7) 893 | inx 894 | } 895 | 896 | //place bars 897 | lda #[logo_bars & $3fff] / 64 898 | sta $0400 + $3f8 + 1 899 | 900 | lda #$f3 901 | sta $d003 902 | lda #24 + 11*8 + 32 903 | sta $d002 904 | 905 | lda #%11111110 906 | sta $d015 907 | lda #%11111100 908 | sta $d01c 909 | 910 | lda #$00 911 | sta $d010 912 | 913 | lda #0 914 | sta $d025 915 | lda #$09 916 | sta $d026 917 | 918 | //we don't really need to set sprite color for all of them 919 | lda #1 920 | sta $d02a 921 | sta $d02b 922 | sta $d02c 923 | sta $d02d 924 | 925 | pla 926 | tax 927 | pla 928 | rti 929 | } 930 | 931 | 932 | //more raster tweaks to set various colors in the panel 933 | irq_panel2: 934 | { 935 | pha 936 | 937 | lda #$08 938 | sta $d021 939 | 940 | lda #$f1 941 | sta $d012 942 | 943 | lda #$1b 944 | sta $d011 945 | 946 | lda #irq_panel3 949 | sta $ffff 950 | 951 | lsr $d019 952 | 953 | lda #1 954 | //waste 56 cycles 955 | .fill 28, $ea 956 | sta $d023 957 | 958 | pla 959 | rti 960 | } 961 | 962 | 963 | //more of the same. The HUD is much more complicated than it seems 964 | irq_panel3: 965 | { 966 | pha 967 | lda #$02 968 | sta $d028 969 | 970 | lda #irq_panel4 973 | sta $ffff 974 | lda #$f3 + 2 975 | sta $d012 976 | lsr $d019 977 | pla 978 | rti 979 | } 980 | 981 | //this draws the color bars of the activision logo. it needs several rastersplits as it's very colorful. 982 | irq_panel4: 983 | { 984 | sta savea + 1 985 | stx savex + 1 986 | sty savey + 1 987 | 988 | lda #$07 989 | ldx #$f3 + 2 990 | !: cpx $d012 991 | beq !- 992 | sta $d028 993 | lda #$05 994 | sta $d021 995 | 996 | 997 | lda #$0e 998 | ldx #$f3 + 4 999 | !: cpx $d012 1000 | bcs !- 1001 | 1002 | sta $d028 1003 | lda #$06 1004 | sta $d021 1005 | 1006 | //here it's safe to update the beams on screen 1007 | jsr beam.update 1008 | 1009 | lda #$08 1010 | sta $d012 1011 | 1012 | lda #$1b 1013 | sta $d011 1014 | 1015 | lda #irq_top 1018 | sta $ffff 1019 | 1020 | lsr $d019 1021 | 1022 | savea: lda #$00 1023 | savex: ldx #$00 1024 | savey: ldy #$00 1025 | rti 1026 | } 1027 | 1028 | 1029 | 1030 | 1031 | //first irq on screen. Does the clouds parallax, plaes score and lives 1032 | irq_top: 1033 | { 1034 | pha 1035 | txa 1036 | pha 1037 | #if DEBUG 1038 | lda #%00010100 1039 | sta $d018 1040 | #endif 1041 | 1042 | 1043 | lda #$0e 1044 | sta $d021 1045 | 1046 | lda clouds_hwscroll 1047 | #if DEBUG 1048 | and #%11101111 1049 | #endif 1050 | sta $d016 1051 | 1052 | lda #$0b 1053 | sta $d022 1054 | lda #$0a 1055 | sta $d023 1056 | 1057 | lda #irq_bg0 1060 | sta $ffff 1061 | 1062 | lda #69 // 73 1063 | sta $d012 1064 | 1065 | lsr $d019 1066 | 1067 | lda #$00 1068 | sta $d01c 1069 | 1070 | //place score 1071 | lda #[score_sprites & $3fff] / 64 1072 | sta $0400 + $3f8 1073 | lda #[score_sprites & $3fff] / 64 + 1 1074 | sta $0400 + $3f8 + 1 1075 | 1076 | lda #48 1077 | .for (var i = 0; i < 8; i++) 1078 | sta $d001 + i * 2 1079 | 1080 | lda #1 1081 | sta $d027 1082 | sta $d028 1083 | 1084 | lda #7 1085 | .for (var i = 2; i < 8; i++) 1086 | sta $d027 + i 1087 | 1088 | 1089 | ldx lives 1090 | lda centertable,x 1091 | clc 1092 | .for (var i = 2; i < 8; i++) 1093 | { 1094 | sta $d000 + 2 * i 1095 | .if (i < 7) 1096 | adc #16 1097 | } 1098 | dex 1099 | stx lleft + 1 1100 | ldx #0 1101 | lda #[hud_chopper_sprite & $3fff] / 64 1102 | !: 1103 | lleft: cpx #0 1104 | 1105 | bmi !ok+ 1106 | lda #[empty_sprite & $3fff] / 64 1107 | !ok: 1108 | sta $0400 + $3f8 + 2,x 1109 | inx 1110 | cpx #6 1111 | bne !- 1112 | !noleft: 1113 | 1114 | 1115 | 1116 | lda #$00 1117 | sta $d010 1118 | lda #$ff 1119 | sta $d015 1120 | 1121 | lda #%00000011 1122 | sta $d01d 1123 | 1124 | lda #160 - 24 1125 | sta $d000 1126 | lda #160 + 24 1127 | sta $d002 1128 | 1129 | 1130 | 1131 | pla 1132 | tax 1133 | pla 1134 | rti 1135 | 1136 | centertable: 1137 | .fill 7,168+24-8*i 1138 | } 1139 | 1140 | 1141 | 1142 | 1143 | getready: 1144 | { 1145 | jsr eraseplayarea 1146 | 1147 | lda #0 //getready 1148 | 1149 | jsr bigsign.init 1150 | 1151 | lda #0 1152 | sta bullet.alive 1153 | sta bullet.alive + 1 1154 | 1155 | lda #$3f 1156 | sta p0tmp + 7 1157 | 1158 | lda #$2c 1159 | sta enemies.updateonly 1160 | 1161 | !: 1162 | jsr panelvsync 1163 | 1164 | jsr bigsign.play 1165 | 1166 | //jsr controls 1167 | jsr hero.update 1168 | 1169 | jsr vans.draw 1170 | jsr enemies.draw 1171 | jsr bullet.update 1172 | jsr radar.plot 1173 | //jsr beam.update 1174 | jsr camera.chase 1175 | jsr plot_mountains 1176 | inc clock 1177 | dec p0tmp + 7 1178 | bne !- 1179 | 1180 | lda #$20 1181 | sta enemies.updateonly 1182 | 1183 | jmp eraseplayarea 1184 | } 1185 | 1186 | 1187 | gameover: 1188 | { 1189 | 1190 | jsr eraseplayarea 1191 | 1192 | //erase player from radar 1193 | lda radar.hero_previous_addr 1194 | sta p0radar 1195 | ldx radar.hero_previous_off 1196 | 1197 | ldy radar.hero_previous_y 1198 | lda (p0radar),y 1199 | and andmask,x 1200 | sta (p0radar),y 1201 | iny 1202 | lda (p0radar),y 1203 | and andmask,x 1204 | sta (p0radar),y 1205 | 1206 | lda #1 //gameover 1207 | 1208 | jsr bigsign.init 1209 | 1210 | lda #$2f 1211 | sta p0tmp + 7 //we keep game over on screem for sometime before we check for button 1212 | 1213 | lda #$2c 1214 | sta enemies.updateonly 1215 | 1216 | !: 1217 | jsr panelvsync 1218 | jsr bigsign.play 1219 | 1220 | //jsr controls 1221 | //jsr hero.update 1222 | jsr vans.update 1223 | jsr enemies.update 1224 | jsr bullet.update 1225 | jsr radar.plot 1226 | //jsr beam.update 1227 | jsr camera.chase 1228 | jsr plot_mountains 1229 | inc clock 1230 | 1231 | dec p0tmp + 7 1232 | bne !- 1233 | 1234 | lda #$20 1235 | sta enemies.updateonly 1236 | 1237 | !: lda #1 1238 | jsr activewait 1239 | 1240 | !skp: 1241 | lda $dc00 1242 | and #%00010000 1243 | bne !- 1244 | 1245 | jmp ig 1246 | 1247 | } 1248 | 1249 | 1250 | //waits while updating chopper and screen animations. 1251 | //used to let some sfx steam off before we move to next event 1252 | //uses p0tmp + 7 1253 | //load A with the wait duration 1254 | 1255 | activewait: 1256 | { 1257 | sta p0tmp + 7 //we keep game over on screem for sometime before we check for button 1258 | 1259 | lda #$2c 1260 | sta enemies.updateonly 1261 | 1262 | !: 1263 | jsr panelvsync 1264 | 1265 | //jsr controls 1266 | //jsr hero.update 1267 | jsr vans.update 1268 | jsr enemies.update 1269 | jsr bullet.update 1270 | jsr radar.plot 1271 | //jsr beam.update 1272 | jsr camera.chase 1273 | jsr plot_mountains 1274 | inc clock 1275 | 1276 | dec p0tmp + 7 1277 | bne !- 1278 | 1279 | lda #$20 1280 | sta enemies.updateonly 1281 | 1282 | rts 1283 | } 1284 | 1285 | 1286 | init_game: 1287 | { 1288 | 1289 | ldx #tozerosize -tozero -1 1290 | lda #0 1291 | !: sta tozero,x 1292 | dex 1293 | bpl !- 1294 | 1295 | lda #3 1296 | sta lives 1297 | 1298 | lda #00 1299 | sta level 1300 | 1301 | rts 1302 | } 1303 | 1304 | 1305 | //setup level difficulty 1306 | init_level: 1307 | { 1308 | 1309 | lda #0 1310 | sta enemy_level_xspeed 1311 | lda level 1312 | lsr 1313 | ror enemy_level_xspeed 1314 | clc 1315 | adc #1 1316 | cmp #ENEMY_MAX_XSPEED 1317 | bcc !ok+ 1318 | lda #0 1319 | sta enemy_level_xspeed 1320 | lda #ENEMY_MAX_XSPEED 1321 | !ok: 1322 | sta enemy_level_xspeed + 1 1323 | 1324 | ldx enemy_level_xspeed 1325 | lda enemy_level_xspeed + 1 1326 | 1327 | cmp #ENEMY_MAX_YSPEED 1328 | bcc !ok+ 1329 | ldx #0 1330 | lda #ENEMY_MAX_YSPEED 1331 | !ok: 1332 | stx enemy_level_yspeed 1333 | sta enemy_level_yspeed + 1 1334 | 1335 | 1336 | lda level 1337 | 1338 | clc 1339 | adc #3 1340 | cmp #16 1341 | bcc !ok+ 1342 | lda #16 1343 | !ok: 1344 | sta enemies.shootprob + 1 1345 | 1346 | lda #2 1347 | sta bulletspeed 1348 | lda level 1349 | cmp #4 1350 | bcc !done+ 1351 | inc bulletspeed 1352 | cmp #09 1353 | bcc !done+ 1354 | inc bulletspeed 1355 | cmp #14 1356 | bcc !done+ 1357 | inc bulletspeed 1358 | cmp #19 1359 | bcc !done+ 1360 | inc bulletspeed 1361 | !done: 1362 | 1363 | rts 1364 | } 1365 | 1366 | init_playfield: 1367 | { 1368 | 1369 | ldx #0 1370 | lda #32 1371 | !: sta $0400,x 1372 | sta $0500,x 1373 | sta $0600,x 1374 | sta $0700,x 1375 | inx 1376 | bne !- 1377 | 1378 | 1379 | ldx #119 1380 | !: 1381 | lda #$08 1382 | sta $d800 + 40 * 3,x 1383 | lda #$08 1384 | sta $d800,x 1385 | dex 1386 | bpl !- 1387 | 1388 | ldx #39 1389 | !: lda #bgpattern_borderchar 1390 | sta $0400 + 40 * 5,x 1391 | lda #0 1392 | sta $d800 + 40 * 19,x //vans coolor 1393 | dex 1394 | bpl !- 1395 | 1396 | ldx #200 1397 | !: lda bottom_charmap-1,x 1398 | sta $0400 + 40 * 20 -1,x 1399 | lda bottom_colormap-1,x 1400 | sta $d800 + 40 * 20 -1,x 1401 | dex 1402 | bne !- 1403 | 1404 | lda #[empty_sprite & $3fff] / 64 1405 | ldx #7 1406 | !: 1407 | sta sprf,x 1408 | dex 1409 | bpl !- 1410 | 1411 | jsr update_score 1412 | jsr radar.init 1413 | 1414 | rts 1415 | } 1416 | 1417 | 1418 | testcollisions: 1419 | { 1420 | //test if we killed some enemy 1421 | 1422 | lda dontcheck 1423 | beq !skp+ 1424 | dec dontcheck 1425 | jmp !next+ 1426 | 1427 | !skp: 1428 | 1429 | lda shadowd01f //spr-bg collision 1430 | sta p0tmp + 1 1431 | 1432 | and #%11100000 //hw sprites associated to enemies. 1433 | beq !next+ //not really needed, but most of the times no collision would be effective, so we might as well skip the bit-wise test 1434 | 1435 | ldx #2 //hw sprites 1436 | !: 1437 | asl p0tmp + 1 1438 | bcc !nb+ 1439 | 1440 | //an enemy was hit 1441 | ldy enemies.reverselut,x 1442 | 1443 | sty savey + 1 1444 | lda #2 1445 | ldx enemies.status,y 1446 | bmi !plane+ 1447 | lda #1 1448 | !plane: 1449 | jsr add_score 1450 | dec nenemies 1451 | 1452 | savey: ldy #0 1453 | lda enemies.status,y 1454 | and #($ff - enemies.STATUS_ALIVE) 1455 | ora #enemies.STATUS_EXPLODING 1456 | sta enemies.status,y 1457 | lda #7 1458 | sta enemies.enemy_clock,y 1459 | 1460 | //we can destroy x because we are exiting the loop on x anyway 1461 | 1462 | lda radar.enemy_previous_addr,y 1463 | sta p0radar 1464 | ldx radar.enemy_previous_off,y 1465 | 1466 | lda radar.enemy_previous_y,y 1467 | 1468 | tay 1469 | 1470 | lda (p0radar),y 1471 | and andmask,x 1472 | sta (p0radar),y 1473 | iny 1474 | lda (p0radar),y 1475 | and andmask,x 1476 | sta (p0radar),y 1477 | 1478 | lda #9 1479 | sta dontcheck 1480 | lda #10 1481 | sta invincibility 1482 | 1483 | :sfx(sfx_bexplosion,2) 1484 | 1485 | jmp !next+ //we can only kill one hw sprite per frame 1486 | 1487 | !nb: 1488 | dex 1489 | bpl !- 1490 | 1491 | !next: 1492 | 1493 | //test if a van has been hit 1494 | //vans can only be hit by the second bullet, so if that's not alive, let's pass 1495 | 1496 | lda bullet.alive + 1 1497 | beq !next+ 1498 | 1499 | lda bullet.ypos + 1 1500 | cmp #MAXY + 0 1501 | bcc !next+ 1502 | 1503 | //inc $d020 1504 | 1505 | //calculate bullet x position in chars 1506 | 1507 | lda sprxl + 4 1508 | sec 1509 | sbc #12 //accounts for the sprite x starting at -24 and the bullet being shifted 8 pixels into the sprite and being 4 pixels large 1510 | sta p0tmp 1511 | lda sprxh + 4 1512 | sbc #0 1513 | lsr 1514 | lda p0tmp 1515 | ror 1516 | lsr 1517 | lsr 1518 | 1519 | sta p0tmp 1520 | 1521 | //test active vans to see if one was hit 1522 | ldx #2 1523 | !: 1524 | lda vans.reverselut,x 1525 | bmi !nextvan+ 1526 | 1527 | ldy vans.pseudox,x 1528 | cpy p0tmp 1529 | beq !hit+ 1530 | iny 1531 | cpy p0tmp 1532 | beq !hit+ 1533 | iny 1534 | cpy p0tmp 1535 | beq !hit+ 1536 | 1537 | !nextvan: 1538 | dex 1539 | bpl !- 1540 | jmp !next+ 1541 | 1542 | 1543 | !hit: 1544 | dec nvans 1545 | tax 1546 | lda #0 1547 | sta vans.status,x 1548 | 1549 | lda #128 //special flag to signal bullet sprite being reused for the explosion 1550 | sta bullet.alive + 1 1551 | 1552 | :sfx(sfx_explosion,2) 1553 | 1554 | !next: 1555 | //test if we died 1556 | 1557 | #if INVINCIBLE 1558 | jmp !done+ 1559 | #endif 1560 | 1561 | lda invincibility 1562 | beq !ok+ 1563 | dec invincibility 1564 | 1565 | lda blink 1566 | bne !+ 1567 | jmp !done+ 1568 | !: 1569 | lda invincibility 1570 | and #1 1571 | 1572 | asl 1573 | adc #7 1574 | sta sprc 1575 | sta sprc + 1 1576 | 1577 | jmp !done+ 1578 | !ok: 1579 | lda #0 1580 | sta blink 1581 | 1582 | lda shadowd01e 1583 | and #$3 1584 | beq !testbg+ 1585 | 1586 | //sorry hero, you hit a enemy or a bullet. Rest in pieces. 1587 | //let's see if it's an enemy 1588 | lda shadowd01e 1589 | and #%11100000 1590 | beq !itwasabullet+ 1591 | 1592 | sta p0tmp + 1 1593 | 1594 | 1595 | ldx #2 //hw sprites 1596 | !: 1597 | asl p0tmp + 1 1598 | bcc !nb+ 1599 | 1600 | //an enemy was hit 1601 | ldy enemies.reverselut,x 1602 | 1603 | sty savey2 + 1 1604 | lda #2 1605 | ldx enemies.status,y 1606 | bmi !plane+ 1607 | lda #1 1608 | !plane: 1609 | jsr add_score 1610 | dec nenemies 1611 | 1612 | savey2: ldy #0 1613 | lda enemies.status,y 1614 | and #($ff - enemies.STATUS_ALIVE) 1615 | ora #enemies.STATUS_EXPLODING 1616 | sta enemies.status,y 1617 | lda #7 1618 | sta enemies.enemy_clock,y 1619 | 1620 | //we can destroy x because we are exiting the loop on x anyway 1621 | 1622 | lda radar.enemy_previous_addr,y 1623 | sta p0radar 1624 | ldx radar.enemy_previous_off,y 1625 | 1626 | lda radar.enemy_previous_y,y 1627 | 1628 | tay 1629 | 1630 | lda (p0radar),y 1631 | and andmask,x 1632 | sta (p0radar),y 1633 | iny 1634 | lda (p0radar),y 1635 | and andmask,x 1636 | sta (p0radar),y 1637 | 1638 | lda #9 1639 | sta dontcheck 1640 | lda #10 1641 | sta invincibility 1642 | 1643 | :sfx(sfx_bexplosion,2) 1644 | 1645 | jmp death //we can only kill one hw sprite per frame 1646 | 1647 | !nb: 1648 | dex 1649 | bpl !- 1650 | 1651 | !itwasabullet: 1652 | jmp death 1653 | 1654 | //if it crushed into a char it must be a van 1655 | !testbg: 1656 | lda shadowd01f 1657 | and #$3 1658 | beq !done+ 1659 | 1660 | //understand which van we hit 1661 | lda sprxl 1662 | sec 1663 | sbc #4 1664 | 1665 | sta p0tmp 1666 | lda sprxh 1667 | sbc #0 1668 | lsr 1669 | lda p0tmp 1670 | ror 1671 | lsr 1672 | lsr 1673 | 1674 | sta p0tmp 1675 | 1676 | //test active vans 1677 | ldx #2 1678 | !: 1679 | lda vans.reverselut,x 1680 | bmi !nextvan+ 1681 | 1682 | 1683 | lda vans.pseudox,x 1684 | sec 1685 | sbc p0tmp 1686 | cmp #4 1687 | bcc !hit+ 1688 | 1689 | cmp #$fc 1690 | 1691 | bcs !hit+ 1692 | 1693 | 1694 | !nextvan: 1695 | dex 1696 | bpl !- 1697 | jmp !done+ 1698 | 1699 | 1700 | !hit: 1701 | 1702 | dec nvans 1703 | lda vans.reverselut,x 1704 | tax 1705 | lda #0 1706 | sta vans.status,x 1707 | 1708 | lda sprxl 1709 | clc 1710 | adc #24 1711 | sta sprxl + 4 1712 | 1713 | lda sprxh 1714 | adc #0 1715 | sta sprxh + 4 1716 | lda spry 1717 | clc 1718 | adc #4 1719 | sta spry + 4 1720 | 1721 | jsr vans.draw 1722 | 1723 | :sfx(sfx_explosion,2) 1724 | 1725 | lda #[explosion_sprite & $3fff] / 64 1726 | sta sprf + 4 1727 | lda #0 1728 | sta sprc + 4 1729 | 1730 | !: 1731 | jsr vsync 1732 | jsr vsync 1733 | ldx sprf + 4 1734 | inx 1735 | cpx #[explosion_sprite & $3fff] / 64 + 4 1736 | beq !ef+ 1737 | stx sprf + 4 1738 | jmp !- 1739 | !ef: 1740 | lda #[empty_sprite & $3fff] / 64 1741 | sta sprf + 4 1742 | jmp death 1743 | 1744 | !done: 1745 | jmp returnfromtestcollisions 1746 | 1747 | dontcheck: 1748 | .byte 0 1749 | } 1750 | 1751 | 1752 | death: 1753 | { 1754 | 1755 | lda #$2c 1756 | sta enemies.updateonly 1757 | 1758 | lda #$20 1759 | sta p0tmp + 7 1760 | !: inc sprc //nice old-school color-cycle 1761 | inc sprc + 1 1762 | jsr enemies.draw 1763 | jsr vsync 1764 | dec p0tmp + 7 1765 | bne !- 1766 | 1767 | lda #$20 1768 | sta enemies.updateonly 1769 | 1770 | //for the explosion we use the chopper sprites and the next two hw sprites, one of which is a bullet sprites, for a grand total of 4. 1771 | //first remove bullets from screen: 1772 | lda #[empty_sprite & $3fff] / 64 1773 | sta sprf + 4 1774 | 1775 | :sfx(sfx_explosion,0) 1776 | 1777 | //store the upperleft corner of explosion 1778 | lda sprxl 1779 | clc 1780 | adc #08 // 1781 | sta p0tmp 1782 | lda sprxh 1783 | adc #0 1784 | sta p0tmp + 1 1785 | lda spry 1786 | sec 1787 | sbc #4 1788 | sta p0tmp + 2 1789 | 1790 | 1791 | jsr random_ 1792 | ldx #3 1793 | 1794 | !: lda random_.random,x 1795 | anc #15 1796 | adc p0tmp 1797 | sta sprxl,x 1798 | lda p0tmp + 1 1799 | adc #0 1800 | sta sprxh,x 1801 | dex 1802 | bpl !- 1803 | 1804 | jsr random_ 1805 | ldx #3 1806 | 1807 | !: lda random_.random,x 1808 | anc #15 1809 | adc p0tmp + 2 1810 | sta spry,x 1811 | dex 1812 | bpl !- 1813 | 1814 | lda #7 1815 | sta sprc 1816 | 1817 | lda #$0f 1818 | sta sprc + 1 1819 | 1820 | lda #$0e 1821 | sta sprc + 2 1822 | 1823 | lda #$0d 1824 | sta sprc + 3 1825 | 1826 | ldx #[explosion_sprite & $3fff] / 64 1827 | stx sprf 1828 | inx 1829 | stx sprf + 1 1830 | inx 1831 | stx sprf + 2 1832 | inx 1833 | stx sprf + 3 1834 | 1835 | 1836 | ldy #0 1837 | !: 1838 | tya 1839 | and #3 1840 | tax 1841 | 1842 | inc sprf,x 1843 | lda sprf,x 1844 | cmp #[explosion_sprite & $3fff] / 64 + 4 1845 | bne !skp+ 1846 | 1847 | lda #[explosion_sprite & $3fff] / 64 1848 | sta sprf,x 1849 | 1850 | jsr random_ 1851 | anc #15 1852 | adc p0tmp 1853 | sta sprxl,x 1854 | lda p0tmp + 1 1855 | adc #0 1856 | sta sprxh,x 1857 | lda random_.random + 1 1858 | anc #15 1859 | adc p0tmp + 2 1860 | sta spry,x 1861 | 1862 | !skp: 1863 | 1864 | cpy #4 1865 | bne !skp+ 1866 | 1867 | :sfx(sfx_bexplosion,1) 1868 | ldy #4 1869 | 1870 | !skp: 1871 | cpy #8 1872 | bne !skp+ 1873 | 1874 | :sfx(sfx_bexplosion,2) 1875 | ldy #8 1876 | !skp: 1877 | jsr vsync 1878 | iny 1879 | cpy #$40 1880 | bne !- 1881 | 1882 | 1883 | lda #[empty_sprite & $3fff] / 64 1884 | sta sprf 1885 | sta sprf + 1 1886 | sta sprf + 2 1887 | sta sprf + 3 1888 | 1889 | dec lives 1890 | bne !ok+ 1891 | jmp gameover 1892 | !ok: 1893 | jmp ih 1894 | 1895 | } 1896 | 1897 | //game area is 320 chars = 2560 pixels. It's a larger than Full HD Game! 1898 | //ranges from $000 to $0a00 1899 | //this is also where the wraparound logic headache starts. 1900 | plot_mountains: 1901 | { 1902 | 1903 | lda camera_x + 2 1904 | bpl !skp+ 1905 | 1906 | lda camera_x + 1 1907 | clc 1908 | adc #<[$0a00 - $500] 1909 | sta p0tmp 1910 | lda camera_x + 2 1911 | adc #>[$0a00 - $500] 1912 | 1913 | jmp !shift+ 1914 | 1915 | 1916 | //if camera_x > $0800, we must reduce it by 1280, that is $0500 (any number that is lartger than $c00 - $800 and multiple of 40 would do), otherwise, upon dividing by 8, it'll be larger than $0100 1917 | 1918 | !skp: cmp #$08 1919 | bcc !ok+ 1920 | 1921 | lda camera_x + 1 1922 | sec 1923 | sbc #$00 1924 | sta p0tmp 1925 | lda camera_x + 2 1926 | sbc #$05 1927 | jmp !shift+ 1928 | 1929 | !ok: 1930 | lda camera_x + 1 1931 | sta p0tmp 1932 | 1933 | lda camera_x + 2 1934 | !shift: 1935 | lsr 1936 | ror p0tmp 1937 | lsr 1938 | ror p0tmp 1939 | lsr 1940 | ror p0tmp 1941 | 1942 | lda p0tmp 1943 | tax 1944 | lda mod40,x 1945 | 1946 | sta bgsrc0 + 1 1947 | clc 1948 | adc #80 1949 | sta bgsrc1 + 1 1950 | 1951 | ldx #38 1952 | 1953 | bgsrc0: lda bgpattern,x 1954 | sta $0400 + 3 * 40,x 1955 | bgsrc1: lda bgpattern + 80,x 1956 | sta $0400 + 4 * 40,x 1957 | 1958 | dex 1959 | bpl bgsrc0 1960 | 1961 | //now let's plot the clouds. 1962 | 1963 | lda camera_x + 1 1964 | lsr 1965 | lsr 1966 | lsr 1967 | lsr 1968 | 1969 | clc 1970 | 1971 | sta csrc0 + 1 1972 | adc #64 1973 | sta csrc1 + 1 1974 | 1975 | ldx #39 1976 | csrc0: lda cloudpattern,x 1977 | sta $0400 + 40,x 1978 | csrc1: lda cloudpattern + 64,x 1979 | sta $0400 + 80,x 1980 | dex 1981 | bpl csrc0 1982 | 1983 | //plot radar mountains 1984 | ldx bgsrc0 + 1 //this is mod40, as computed by the first part of this function. 1985 | //now we must divide by 5 1986 | lda div5,x 1987 | sta p0tmp 1988 | asl 1989 | anc #%00000110 1990 | eor #%00000110 1991 | adc #<[radar_mountains-charset] / 8 1992 | tax 1993 | tay 1994 | iny 1995 | 1996 | lda #%00000100 1997 | bit p0tmp 1998 | beq !noswap+ 1999 | 2000 | tya 2001 | stx p0tmp 2002 | tax 2003 | ldy p0tmp 2004 | 2005 | !noswap: 2006 | 2007 | .for (var i = 0 ; i < 8; i++) 2008 | stx $0400 + 21 * 40 + 12 + i * 2 2009 | .for (var i = 0 ; i < 8; i++) 2010 | sty $0400 + 21 * 40 + 13 + i * 2 2011 | 2012 | 2013 | rts 2014 | } 2015 | 2016 | 2017 | clear_irq: 2018 | { 2019 | lda #$7f //CIA interrupt off 2020 | sta $dc0d 2021 | sta $dd0d 2022 | lda $dc0d 2023 | lda $dd0d 2024 | 2025 | lda #$01 //Raster interrupt on 2026 | sta $d01a 2027 | lsr $d019 2028 | rts 2029 | } 2030 | 2031 | 2032 | //it takes a lot of code to make a game 2033 | .import source "misc.asm" 2034 | .import source "sfx.asm" 2035 | .import source "controls.asm" 2036 | .import source "hero.asm" 2037 | .import source "camera.asm" 2038 | .import source "weapons.asm" 2039 | .import source "enemies.asm" 2040 | .import source "hud.asm" 2041 | .import source "radar.asm" 2042 | .import source "splash.asm" 2043 | 2044 | 2045 | mod40: 2046 | .fill 256, mod(i,40) 2047 | 2048 | div5: 2049 | .fill 320, i/5 2050 | 2051 | //maps char y to screen coordinates. we only need 20 chars of it 2052 | screen40l: 2053 | .fill 20, <[$0400 + i * 40] 2054 | screen40h: 2055 | .fill 20, >[$0400 + i * 40] 2056 | 2057 | 2058 | .pc = $6000 "splash screen scr" 2059 | .import binary "..\assets\credits.scr" 2060 | .pc = $4000 "splash screen map" 2061 | .import binary "..\assets\credits.map" 2062 | 2063 | //Saul Cross' Amazing tune 2064 | .pc = $7000 "sid" 2065 | .fill sid.size, sid.getData(i) 2066 | 2067 | .print p0current //how much page zero did we use? 2068 | 2069 | .pc = $7800 "bigsigns" 2070 | 2071 | .label getreadymap = * 2072 | .for (var cx = 0; cx < 18; cx++) 2073 | { 2074 | .fill 16,0 2075 | .for (var cy = 0; cy < 16; cy++) 2076 | .byte grgo.getSinglecolorByte(cx, cy) 2077 | } 2078 | 2079 | .label gameovermap = * 2080 | .for (var cx = 0; cx < 18; cx++) 2081 | { 2082 | .fill 16,0 2083 | .for (var cy = 0; cy < 16; cy++) 2084 | .byte grgo.getSinglecolorByte(cx, cy + 16) 2085 | } --------------------------------------------------------------------------------