; asmsyntax=asmM6502 ; ************************************************************* ; ** ** ; ** Copyright © Jan Hermanns 2013 All Rights Reserved. ** ; ** ** ; ** COMMERCIAL USE OF THIS FILE IS NOT ALLOWED WITHOUT ** ; ** EXPRESS AND PRIOR WRITTEN CONSENT OF THE AUTHOR. ** ; ** ** ; ************************************************************* ;{{{ ; You need a working installation of DASM in order to compile this file. ; ; To compile the NTSC version you need to do this: ; dasm blinky.asm -f3 -oBlinky_NTSC.bin -DNTSC=1 -I ; ; To compile the PAL version: ; dasm blinky.asm -f3 -oBlinky_PAL.bin -I ; ; The -I option specifies where "vcs.h" and "macro.h" are located on your ; computer. You can omit the -I option, if you copy both files in the same ; directory as "blinky.asm". ; ; Example: To compile the NTSC Version on my computer I need to do this: ; ; dasm blinky.asm -f3 -oBlinky_NTSC.bin -DNTSC=1 -I/home/hermanns/dasm/ ;}}} ;{{{ ; TODOs ; + Namen der Bad-Guys: Tooby, Bolly, Shooty. Name des Spielers Blinky ; - Springen "entprellen" ; - Darstellung der Leben sieht kommisch aus, beim Wechsel von 5 zu 4 Leben ; + Probleme mit Kollisionsabfrage oben reproduzieren (moeglicherweise ; wenn Level gerade gescrollt wird) ; + Goldstuecke zaehlen ; + Farben fuer Titelscreen, Next Level, und Endscreen konstant weiss machen ; + Farbe fuer LEVEL5 ; + Platz schaffen, fuer LEVEL-Tabelle ; + Startposition von LEVEL3 veraendern ; + Sound der Gegner disablen, wenn sie nicht mehr sichtbar sind ; + Perfect nur anzeigen bei perfektem Spiel ; + Es gibt immer noch ein Problem beim Aufprallen des Balls, wenn der ; Level gerade gescrollt wird (das lag an der Normalisierung der Y-Koordinate) ; + Screenflash bei Extraleben ; + Sound-FX fuer Level-Ende (oder zumindest alle Sounds abstellen) ; + Warteanimation verfeinern (Blinky sollte mit dem Fuss tippen) ; + Codierung der Leveldaten kann effizienter gestaltet werden (z.B. ; X/Y-Startposition des Spielers in einem statt zwei Bytes) => WONTFIX - ; lohnt sich nicht ; + Sound-FX fuer Schuss (Laser-Sound) ; + Diamant (WONTFIX - zu wenig Platz!) ; + Variable fuer gravitation einsparen ; + PAL-Version ; + Sound-FX bei Verlust eines Lebens ; + INITxSND Routinen refactoren ; + Dokumentieren, wie man das Problem nachstellen kann, dass zuviele ; Scanlines verbraucht werden. Danach mit aelterer Version testen, um zu ; schauen, ob es am Sound liegt. ; + Hintergrundfarbe immer schwarz (d.h. diese Farbinformation aus den ; Level-Daten entfernen) ; + LOV3MACHINE Sprites entfernen ; + Der Ball "verspringt" manchmal ; + Springen auch mit Feuerknopf ; + Naechster Level laden ; + Manchaml wird das Gold fuer kurze Zeit falsch (oben) angezeigt ; + Gold einsammeln (Lebensanzeige umfaerben, ggf. Leben addieren) ; + Schwarzer Hintergrund fuer Lebensanzeige ; + Titelscreen ; + Langsamer sterben ; + Lebensanzeige ; + Man scheint immer direkt zweimal hintereinander zu sterben ; + Warum werden manchmal zuviele Scanlines verbraucht (breakif { _scan > $105 }) ; + Enemy-Sprite soll nicht so "tief" im Playfield versinken ; + Exit soll pulsieren ; + Ball verschwindet manchmal ; + Beim Scrollen werden 286 Scanlines verbraucht ; + "Doppel" Ball ; + "Doppel" Schlauch ; + Kollisionsabfrage fuer "Doppel-Schlauch" ; + Level Exit mittels Ball-Sprite ; + Gold zentrieren ; + Schuss zentrieren ; + Kollisionsabfragebug "Schlauch-Umkehr" ;}}} IFCONST NTSC echo "---------- NTSC Version ------------------------------" ELSE echo "---------- PAL Version -------------------------------" ENDIF ; --------------------------------------------------------------------------- processor 6502 include "vcs.h" include "macro.h" ;{{{ ; ----- G L O S S A R ------------------------------------------------------- ; ; Level-Daten: Damit sind saemtliche Daten eines Levels gemeint. ; Die Level-Daten enthalten also neben den Positionen der Gegner/Objekt ; auch die Spielfeld-Daten und die Farbinformationen. Wie die ; Level-Daten im Detail aufgebaut sind ist im Kommentar der NEWLEVEL ; Subroutine genau beschrieben. ; ; Spielfeld-Daten: Damit sind (in Abgrenzung zu den Level-Daten) nur ; die Daten gemeint die noetig sind, um das Spielfeld anzuzeigen ; (technisch gesehen sind das also die Daten mit denen die logischen ; Spielfeld-Register beladen werden). ; ; Blockreihe: Ein Spielfeld ist aus Blockreihen aufgebaut. Eine ; Blockreihe besteht aus 16 Bloecken, die beliebig an-/ausgeschaltet ; sein koennen. D.h. jede Blockreihe eines Spielfelds kann durch 2 Bytes ; definiert werden. Auf dem Bildschirm wird immer ein Ausschnitt von 11 ; Blockreihen eines Spielfelds dargestellt, der dann nach Bedarf nach ; unten gescrollt wird, bis das Ende eines Spielfelds erreicht ist. ;}}} ;{{{ ; ----- C o n s t a n t s ------------------------------------------------------ ENDSCREEN = $FB00 TITLE_PAGE_1 = $FC00 TITLE_PAGE_2 = $FD00 SPRITE_PAGE = $FE00 PFDATA = $FF00 RNDSEED = $15 DISABLED = $FF IFCONST NTSC HERO_ACCEL_X = 8 HERO_ACCEL_Y_LSB = 120 HERO_ACCEL_Y_MSB = 2 BALL_ACCEL_Y_LSB = 200 BALL_ACCEL_Y_MSB = 2 TUBE_ANIM_SPEED = 3 ANIM_SPEED = 4 BALL_ANIM_SPEED = 4 GRAVITY_CONST_FALL = $B0 ; Gravitation beim Fallen GRAVITY_CONST_RISE = $E6 ; Gravitation des Balls beim Aufsteigen VERT_BLANK_TIM64T = 43 LIFEBAR_TIM64T = 19 TEXT_OFFSET_TIM64T = 229 ;(192*76) / 64 == 228 OVRSCBEG_TIM64T = 35 ;(30*76) / 64 == 35 TITLSCRN_TIM64T = 50 TITLSCRN_COLOR = $24 LEVELEND_TIM64T = 70 ENDSCRN_TIM64T = 60 ELSE HERO_ACCEL_X = 9 HERO_ACCEL_Y_LSB = 120 HERO_ACCEL_Y_MSB = 2 BALL_ACCEL_Y_LSB = 220 BALL_ACCEL_Y_MSB = 2 TUBE_ANIM_SPEED = 2 ANIM_SPEED = 3 BALL_ANIM_SPEED = 3 GRAVITY_CONST_FALL = $8C ; Gravitation beim Fallen GRAVITY_CONST_RISE = $E4 ; Gravitation des Balls beim Aufsteigen VERT_BLANK_TIM64T = 53 LIFEBAR_TIM64T = 40 TEXT_OFFSET_TIM64T = 255 ; (228*76) / 64 == 271 > 255 OVRSCBEG_TIM64T = 42 ; (36*76) / 64 == 42 TITLSCRN_TIM64T = 47 TITLSCRN_COLOR = $44 LEVELEND_TIM64T = 70 ENDSCRN_TIM64T = 60 ENDIF DEATH_ANIM_SPEED = 3 BLINKING_SPEED = 6 ; Blinzel-Geschindigkeit FOOTIP_SPEED = 10 TRAP_SPEED = 2 SPRITE_HEIGHT = 8 GRAVITY_CONST_JUMP = %11110000 ; Gravitation beim Springen VERT_BLK_RES = 11 ; Anzahl der Baender (d.h. der Blockreihen) BLK_HEIGHT = 16 ; Hoehe eines Bandes in Anzahl Scanlines SCROLL_SPEED = 3 ; Higher = Slower (Min=1) NEW_GAME = %00000100 NEW_GAME_STARTED = %11111011 X_MOT_STOP_THR = $31 ; Schwellwert fuer schnelles Abbremsen PFREGS = 12 THREE_COPIES_CLOSE = %00000011 MAXLIVES = 6 MAXGOLD = 17 LASTLEVEL = 5 ; Levels fangen bei 0 an ; Das lost-a-life Flag zeigt an, dass der Spieler waehrend des Spiels ; mindestens schon ein Leben verloren hat. Wir benoetigen diese Information im ; Endscreen, um ggf. das Perfect (alle Goldstuecke wurden eingesammelt und kein ; Leben verloren) anzuzeigen. LOST_A_LIFE = %10000000 ; Hiermit kann das Bit der Variable flag freigestellt werden, das ; anzeigt ob das Spielfeld gerade gescrollt wird. ; SCROLL_FLAG_MASK = %00001000 DISABLE_SCROLL_FLAG = %11110111 DEATH_FLAG_MASK = %01000000 DISABLE_DEATH_FLAG = %10111111 LVLEND_FLAG_MASK = %00000010 DISABLE_LVLEND_FLAG = %11111101 BALLSIZ_FLAG_MASK = %00110000 DISABLE_BALLSIZ_FALG = %11001111 MAX_BALLSIZE = %00110000 BALLSIZE_INCREMENT = %00010000 ; ; Raender fuer Spielfeldbegrenzungen ; LEFT_MARGIN = 1 ;16 RIGHT_MARGIN = 149;83;149 ; (160 / 2) + (SPRITE_WIDTH / 2) = 84 ; ; Der Trick ist hier, dass der Zustand mit dem Offset des Hero-Sprites ; in der SPRITE_PAGE uebereinstimmt. ; Auch die Anordnung der Sprites (und damit die Ordnung der folgenden ; Konstanten) machen wir uns zu Nutze (in der Abfrage der vertikalen ; Joystick-Position. ; HERO_WAITING = %11100000 HERO_FALLING = %00010000 HERO_WALKING = %00001110 HERO_JUMPING = %00000001 ; Die folgende Konstante zeigt auf das letzte Sprite der "Gehen"-Animation BEGIN_WALKING_ANIM = %00000010 END_WALKING_ANIM = %00001000 BEGIN_WAITING_ANIM = %00100000 BLINK_WAITING_ANIM = %01000000 ; Mit dieser Konstante koennen die Bits, des enemyState freigestellt werden, ; die Auskunft darueber geben, welcher Enemy dargestellt werden soll. ENEMY_TYPE_MASK = %11100000 ; Mit dieser Konstante koennen die Bits, des enemyState freigestellt werden, ; die Auskunft ueber die aktuelle Animationsphase des Enemys geben. ENEMY_ANIM_MASK = %00000111 ; Durch ver-unden des enemyState mit dieser Konstante, wird die Animation ; zurueckgesetzt. ENEMY_CLEAR_ANIM = %11111000 ; Mit dieser Konstante kann das Bit des enemyState freigestellt werden, ; das Auskunft darueber gibt, in welche horizontale Richtung sich der Enemy ; bewegt. ENEMY_H_DIR_MASK = %00001000 ENEMY_TUBE = %01100000 ; Schlauch ENEMY_BALL = %00100000 ; Ball NO_ENEMY = %00000000 ; kein Gegner aktiv ; Mit dieser Konstante kann das Bit des enemyState freigestellt werden, ; das Auskunft darueber gibt, ob der Ball gerade am ""auftitschen" ist (d.h. ob ; gerade die Animation laeuft). ENEMY_V_DIR_MASK = %00010000 ; Der Tube hat zwar nur 7 Animationsphasen, aber wir zaehlen dessen Phasen ; beginnend bei 1. TUBE_BEGIN_ANIM = %00000001 TUBE_END_ANIM = %00000111 ; Der Ball hat nur 3 Animationsphasen, wir zaehlen beginnend bei 1 BALL_BEGIN_ANIM = %00000001 BALL_END_ANIM = %00000100 ENEMY_DIR_LEFT = %00001000 ENEMY_DIR_RIGHT = %00000000 BALL_IS_BOUNCING = %11101111 ENEMY_FALLING = %00010000 SWCHA_P0_UP = %00010000 ENABLE_SWCHA_P0_UP = %11101111 SWCHA_P0_NO_MOVE = %11110000 ; ; Die folgenden Konstanten geben an, welche Art von Objekt/Gegner von der ; Routine NEXTENEM positioniert werden muss. ; BALL_1 = ENEMY_BALL BALL_2 = %01000000 TUBE_1 = ENEMY_TUBE TUBE_2 = %10000000 TRAP = %10100000 GOLD = %11000000 DOOR = %11100000 ; ; Konstanten fuer Audio-Ausgabe ; SND0_MASK = %00001111 ; Wert durch VerANDen freistellen SND1_MASK = %11110000 ; Wert durch VerANDen freistellen CLEAR_SND0 = %11110000 ; Wert durch VerANDen loeschen CLEAR_SND1 = %00001111 ; Wert durch VerANDen loeschen BALL_SOUND = %00000001 ; Wert duch VerORen setzen TUBE_SOUND = %00000010 ; Wert duch VerORen setzen GOLD_SOUND = %00010000 ; Wert duch VerORen setzen EXTRA_SOUND = %00100000 ; Wert duch VerORen setzen LASER_SOUND = %01000000 ; Wert duch VerORen setzen ;}}} ;{{{ ; ----- V a r i a b l e s ------------------------------------------------------ SEG.U VARS ORG $80 pf1l ds PFREGS pf2l ds PFREGS pf0r ds PFREGS pf1r ds PFREGS pf2r ds PFREGS scroll ds 1 ; Der levelptr zeigt auf die Spielfeld-Daten und muesste daher ; korrekterweise besser pfptr heissen. ; levelptr ds 2 ; Der levelptroff wird jedesmal gesetzt, wenn neue Level-Daten geladen ; werden. Er gibt an, wie weit die Spielfeld-Daten des Levels vom Anfang ; der Speicherseite (auf der sich die Spielfeld-Daten befinden) entfernt ; sind. Damit kann man dann relativ einfach die aktuelle Level-Position ; ausrechnen, indem den levelptroff vom LSB des levelptr subtrahiert ; (das ist natuerlich vereinfacht ausgedrueckt, weil hier noch nicht ; beachtet wird, dass jede Blockreihe eines Levels durch zwei Bytes ; definiert wird). ; levelptroff ds 1 heroState ds 1 heroX ds 2 heroY ds 2 enemyX ds 2 enemyY ds 2 heroSpeedX ds 2 heroSpeedY ds 2 enemySpeedY ds 2 ; D5-D7: Geben Auskunft darueber, welcher Enemy dargestellt werden soll. ; D0-D2: Enthaelt die Animationsphase ; enemyState ds 1 enemyposptr ds 2 goldcoord ds 2 ; X/Y Koordinate fuer Goldstuecke trapcoord ds 2 ; X/Y Koordinate fuer Schuesse doorcoord ds 2 ; X/Y Koordinate fuer Levelausgaenge exitAnimTimer ds 1 heroAnimTimer ds 1 enemyAnimTimer ds 1 ; Bit D0 (Trap-Flag) wird verwendet, als Richtungsindikator fuer die Trap ; Bit D1 (Levelend-Flag) zeigt an, ob der Spieler gerade ein Level beendet hat ; Bit D2 zeigt an, dass ein neues Spiel gestartet wurde ; Bit D3 zeigt an, ob das Spielfeld gerade gescrollt wird ; Bits D4,D5 (Ballsize-Flag) werden als Ball-Size verwendet (um den Exit ; zu animieren). ; Bit D6 (Death-Flag) zeigt an, ob der Spieler gerade ein Leben verloren hat flag ds 1 ; Die folgenden Variablen duerfen waehrend eines Spiels (d.h. wenn ein ; neuer Level geladen wird oder der Spieler ein Leben verloren hat) ; *nicht* geloescht werden. ; PERSIST level ds 1 ; D0-D3 zeigt, welcher Level gerade gespielt wird lives ds 1 ; D7 wird als Lost-a-Life Flag verwendet ; D0-D6 werden als Zaehler fuer die eingesammelten Goldstueck verwendet goldcnt ds 1 rnd ds 1 ; Koennte man auch wiederherstellen... ; Damit waehrend des Spiel-Kernals die SPRITE6 Routine verwendet werden ; kann, muessen die folgenden Register (waehrend der Ausfuehrung von ; SPRITE6) zwischengespeichert werden: ; ; REFP0 (1 Bit) ; REFP1 (1 Bit) ; NUSIZ1 (5 Bit) ; COLUP0 (7 Bit) ; COLUP1 (7 Bit) ; ; Das NUSIZ0 Register muss uebrigens nichts zwischengespeichert werden, ; da es waehrend des Spiels immer einen Konstanten Wert von %00010000 ; hat (da der Spieler immer "normal" dargestellt wird und das Gold ; immer 2 clocks breit ist). ; NUSIZ1_COPY ds 1 COLUP0_REFP0_COPY ds 1 ; D0: REFP0, D1-D7: COLUP0 COLUP1_REFP1_COPY ds 1 ; D0: REFP1, D1-D7: COLUP1 ; Die folgenden Variablen werden nur temporaer bei der Benutzung der ; SPRITE6 Routine benoetigt. ; sprite6_ptr1 ds 2 sprite6_ptr2 ds 2 sprite6_ptr3 ds 2 sprite6_ptr4 ds 2 sprite6_ptr5 ds 2 sprite6_ptr6 ds 2 sprite6_height ds 1 sprite6_color ds 1 temp ds 2 ; Die folgenden Variablen werden fuer die Erzeugung der Audiosignale ; benoetigt. ; freq0 ds 1 ; Aktuelle Frequenz fuer Audiokanal 0 vol0 ds 1 ; Aktuelle Lautstaerke von Audiokanal 0 freq1 ds 1 ; Aktuelle Frequenz fuer Audiokanal 1 vol1 ds 1 ; Aktuelle Lautstaerke von Audiokanal 1 sounds ds 1 ; D0-D3 Sound fuer Audiokanal 0 ; D4-D7 Sound fuer Audiokanal 1 free1 ds 1 ; Hier ist noch ein freies Byte free2 ds 1 ; Hier ist noch ein freies Byte ; Die 6 Bytes freies RAM koennen nicht genutzt werden, da sie als ; Stack verwendet werden. ; echo ($100 - *) , "bytes of RAM left. Stack needs 6 Bytes." ;}}} ;{{{ ; ----- A l i a s e s ---------------------------------------------------------- ; In dieser Variable wird kurzfristig der Zustand des Joysticks ; zwischengespeichert. joystick = sprite6_ptr1 ; Die Spielfeld-Farbe waehrend der "Sterben" Animation. death = enemyAnimTimer ; Der Timer fuer die Animation beim Verlust eines Lebens kann sich nicht ; mit dem exitAnimTimer ins Gehege kommen ; deathAnimTimer = exitAnimTimer ; Der Timer fuer die Darstellung des "NEXT LEVEL" Text kann sich nicht ; mit dem exitAnimTimer ins Gehege kommen ; levelendAnimTimer = exitAnimTimer ; Der Timer fuer zum Entprellen des Feuerknopfs nach der Darstellung des ; End-Screens kann sich nicht mit dem exitAnimTimer ins Gehege kommen ; fireDebounceTimer = exitAnimTimer ; Die folgenden beiden Pointer werden pro Frame immer neu gesetzt und ; nur im Hauptkernal verwendet und koennen sich daher nicht mit den ; sprite6_ptr Variablen ins Gehege kommne. ; enemySpritePtr = sprite6_ptr1 heroSpritePtr = sprite6_ptr2 ; Die Variablen pos1..pos4 werden nur temporaere beim Laden einer neuen ; Blockreihe verwendet und koennen sich daher mit den sprite6_ptr ; Variablen nicht ins Gehege kommen. ; pos1 = sprite6_ptr1 pos2 = sprite6_ptr1 + 1 pos3 = sprite6_ptr2 pos4 = sprite6_ptr2 + 1 goldX = goldcoord goldY = goldcoord + 1 trapX = trapcoord trapY = trapcoord + 1 doorX = doorcoord doorY = doorcoord + 1 xHeroLSB = heroX ; Subpixelposition xHeroMSB = heroX + 1 ; Pixelposition yHeroLSB = heroY ; Subpixelposition yHeroMSB = heroY + 1 ; Pixelposition xEnemyLSB = enemyX ; Subpixelposition xEnemyMSB = enemyX + 1 ; Pixelposition yEnemyLSB = enemyY ; Subpixelposition yEnemyMSB = enemyY + 1 ; Pixelposition xSpeedLSB = heroSpeedX xSpeedMSB = heroSpeedX + 1 ySpeedLSB = heroSpeedY ySpeedMSB = heroSpeedY + 1 ; Waehrend der Warte-Animation werden die Variablen fuer die horizontale ; Geschwindigkeit als Timer benutzt. ; bored = xSpeedMSB footimer = xSpeedLSB blinktimer = heroAnimTimer yEnemySpeedLSB = enemySpeedY yEnemySpeedMSB = enemySpeedY + 1 tempLSB = temp tempMSB = temp + 1 temp1 = tempLSB temp2 = tempMSB blockHeightCnt = temp + 1 ;}}} SEG ORG $F000 RESET CLEAN_START LDA #RNDSEED STA rnd JSR VSYN_VBL JMP TITLSCRN ;{{{ ; MOVSPRIT - Horizontale Spritepositionierung ; ; Die folgende Routine positioniert das, im X-Register uebergebene Sprite ; horizontal, an die im Aku angegebene X-Position. ; ; ACHTUNG: Da diese Routine im Timing sehr kritisch ist, muss dafuer ; gesorgt sein, dass sie vollstaendig auf nur einer Speicherseite ; untergebracht ist. ; MOVSPRIT SUBROUTINE CLC ADC #4 CMP #$A0 BCC .skipwrp SBC #$A0 .skipwrp EOR #$FF ;2 0-159 is now 255-96 CLC ;2 STA WSYNC ;3 begin line 1 .divloop ADC #15 ;2 BCC .divloop ;54 max SBC #7 ;2 ASL ;2 ASL ;2 ASL ;2 ASL ;2 -Shift left 4 places STA HMP0,X ;4 68 STA RESP0,X ;4 72 STA WSYNC ;3 RTS ;6 ;}}} ;{{{ ; SPRITE6 - Diese Routine dient dazu 6 verschiedene Sprites ; nebeneinander darzustellen. Sie ist dazu gedacht innerhalb eines ; Kernals aufgerufen zu werden. ; ; Dazu muessen vor dem Aufruf allerdings die Variablen ; sprite6_ptr1...sprite6_ptr6 mit den Adressen der darzustellenden Sprites ; beladen werden und die Variable sprite6_height gesetzt werden (das ; impliziert natuerlich, dass alle 6 Sprites die gleiche Hoehe haben). ; ; ACHTUNG: Da diese Routine im Timing sehr kritisch ist, muss dafuer ; gesorgt sein, dass sie vollstaendig auf nur einer Speicherseite ; untergebracht ist. ; SPRITE6 subroutine LDA sprite6_color ; Farbe der 6 Sprites setzen STA COLUP0 STA COLUP1 LDA #THREE_COPIES_CLOSE ; STA NUSIZ0 STA NUSIZ1 LDA #0 ; Alle relevanten Sprite-Register STA GRP0 ; zuruecksetzen... STA GRP1 STA GRP0 STA REFP0 STA REFP1 STA HMP1 STA WSYNC ; Jetzt geht's los, Timing ist gefragt ;-) PHA ; 3 Prozessorzyklen verbraten... PLA ; 4 Prozessorzyklen verbraten... LDA #1 STA VDELP0 STA VDELP1 LDX #4 .waste DEX BNE .waste STA RESP0 STA RESP1 LDA #$F0 STA HMP0 STA WSYNC STA HMOVE DEC sprite6_height .loop LDY sprite6_height LDA (sprite6_ptr1),y STA GRP0 STA WSYNC LDA (sprite6_ptr2),y STA GRP1 LDA (sprite6_ptr3),y STA GRP0 LDA (sprite6_ptr4),y STA temp LDA (sprite6_ptr5),y TAX LDA (sprite6_ptr6),y TAY LDA temp STA GRP1 STX GRP0 STY GRP1 STA GRP0 DEC sprite6_height BPL .loop LDA NUSIZ1_COPY ; Jetzt alle relevanten Sprite-Register STA NUSIZ1 ; wiederherstellen... LDA COLUP0_REFP0_COPY STA COLUP0 ASL ASL ASL STA REFP0 LDA COLUP1_REFP1_COPY STA COLUP1 ASL ASL ASL STA REFP1 LDA #0 STA GRP0 STA GRP1 STA GRP0 RTS ;}}} ;{{{ ; OVRSVBL - Leitet den Overscan ein und wartet diesen ab. Danach wird der ; Vertical-Sync ausgefuehrt und der Timer fuer den Vertical-Blank gesetzt. OVRSVBL subroutine JSR OVRSCBEG ; Overscan einleiten JSR OVRSCEND ; und abwarten... JSR VSYN_VBL RTS ;}}} ;{{{ ; VSYN_VBL - Fuehrt zunaechst einen Vertical-Sync aus und setzt dann den Timer ; fuer den Vertical-Blank. VSYN_VBL subroutine VERTICAL_SYNC LDA #VERT_BLANK_TIM64T ; Timer fuer Vertical-Blank STA TIM64T ; setzen. RTS ;}}} GAMELOOP JSR VSYN_VBL ;{{{ ; NEWGAME - Ggf. wird ein neues Spiel initialisiert ; NEWGAME subroutine LDA flag ; Flag laden und pruefen, ob ein AND #NEW_GAME ; neues Spiel gestartet werden soll? BEQ .resume ; Nein, altes Spiel fortsetzen... LDA flag ; Ja, daher Flag umbiegen AND #NEW_GAME_STARTED STA flag LDA #3 ; Spieler beginnt mit 3 Leben. STA lives LDA #0 STA goldcnt ; Gold-Counter auf null setzen STA level ; 1.Level laden JMP NEWLEVEL .resume ;}}} ;{{{ ; VISIBLE - Befinden sich die Trap, Gold, Door und Gegner noch im ; sichtbaren Bereich oder muessen sie disabled werden? ; ; Der sichtbare Bereich sind die 176 Scanlines (BLK_HEIGHT * ; VERT_BLK_RES) oberhalb der Lebensanzeige (die Y-Koordinate 176 ; befindet sich uebrigens am oberen Bildschirmrand, weil der Kernal ; runterzaehlt). ; ; Ob ein Objekt/Gegener vom Kernal angezeigt wird oder nicht, haengt ; lediglich von seiner Y-Koordinate ab (liegt diese im Intervall ; [0..176], dann zeigt der Kernal es an). D.h. die einzige Moeglichkeit ; ein Objekt/Gegner auszuschalten liegt darin dafuer zu sorgen, dass ; seine Y-Koordinate groesser als 176 ist. ; ; Als oberen Schwellwert benutzen wir allerdings nicht 176, sondern 190 ; und zwar weil es vorkommen kann, dass ein Ball ganz oben am Bildschirm ; dargestellt wird und nach dem Aufprellen dann (fuer kurze Zeit) eine ; Y-Koordinate erreichen kann, die ausserhalb des sichtbaren Bereichs ; liegt. Wuerden wir in diesem Fall 176 als Grenze ansetzen, wuerde der ; Ball nur einmal aufprellen und dann disabled werden. ; ; Auch die Y-Koordinaten von "ausgeschalteten" Objekten/Gegnern werden ; von der SCRLDOWN Routine stur dekrementiert. Wir muessen also ; auch dafuer sorgen, dass die Y-Koordinaten von "ausgeschalteten" ; Objekten/Gegnern nicht soweit dekrementiert werden, dass sie in den ; sichtbaren Bereich gelangen und dann faelschlicherweise angezeigt ; werden (aus diesem Grund setzen wir die Y-Koordinate "ausgeschalteten" ; Objekten/Gegnern immer auf $FF). ; ; Fuer den unteren Schwellwert nehmen wir uebrigens 4 anstatt 0, da es ; andernfalls zu Darstellungsproblemen bei Lebensanzeige kommt. ; VISIBLE subroutine LDA #4 LDX #190 LDY #DISABLED CMP trapY BCC .chktra2 ; Branch if (4 < trapY) STY trapY .chktra2 CPX trapY BCS .chkgold ; Branch if (177 >= trapY) STY trapY .chkgold CMP goldY BCC .chkgol2 STY goldY .chkgol2 CPX goldY BCS .chkdoor STY goldY .chkdoor CMP doorY BCC .chkdoo2 STY doorY .chkdoo2 CPX doorY BCS .chkenem STY doorY .chkenem CMP yEnemyMSB BCC .chkene2 STY yEnemyMSB .chkene2 CPX yEnemyMSB BCS .endchk STY yEnemyMSB INY ; Y := 0 STY yEnemySpeedMSB STY yEnemySpeedLSB STY enemyState .endchk ;}}} ;{{{ ; VITALCHK - Prueft, ob der Spieler gestorben ist. ; Wenn das der Fall ist, dann Blenden wir den Spielfeld langsam aus. ; freq VITALCHK subroutine BIT flag ; Death-Flag laden und freistellen. BVC .vital ; Ist der Spieler gestorben? DEC deathAnimTimer ; Ja, naechster Farbwechsel faellig? BNE .contdie ; Nein, also Routine beenden. LDX #DEATH_ANIM_SPEED ; Ja, also erstmal Timer STX deathAnimTimer ; resetten dann Sound-FX LDX freq1 ; Frequenz fuer Audiokanal 1 laden INX ; Jetzt wird der Frequenz-Wert INX ; erhoeht, dadurch wird eine tiefere STX freq1 ; Frequenz gewaehlt STX AUDF1 ; und abgespielt. LDA death ; Farbe des Spielfelds STA COLUPF ; wechseln DEC death ; Noch ein Farbwechsel? BNE .contdie ; Ja LDA flag ; Nein, daher AND #DISABLE_DEATH_FLAG ; Death-Flag ausschalten STA flag LDA #0 ; Und Audiokanal 1 ganz leise STA AUDV1 ; drehen. DEC lives ; Hat der Spieler noch Leben uebrig? BNE .loadlev ; Ja, also aktuellen Level neu laden. JMP TITLSCRN ; Nein, daher zum Titel-Screen .loadlev JMP NEWLEVEL .contdie JMP PREKERN .vital ;}}} LDA #0 STA COLUBK JSR LEVELEND JSR NEXTENEM JSR JOYHORIZ LDA heroState AND #HERO_WAITING ; Ist der Spieler am Warten? BNE JOYUP ; Ja, also direkt zur Joystick-Abfrage JSR STOPWALK JSR FRICTION ;{{{ ; MOVHORIZ - Hier wird die horizontale Geschwindigkeit zur X-Position des ; Spielers addiert, um die neue horizontale Spielerpostion auszurechnen. ; MOVHORIZ subroutine LDX #Sprites ; Sprite des Spieler in den Sprite-Pointer STA heroSpritePtr + 1 ; laden. JSR LOADHERO LDX #heroSpritePtr ; Sprite-Pointer entsprechend der LDY yHeroMSB ; Y-Koordinate des Spielers entsprechend JSR ADJUST ; positionieren LDA yHeroMSB JSR VERTADJ STY VDELP0 LDX #HMP0-HMP0 LDA xHeroMSB JSR MOVSPRIT LDA #>Enemy STA enemySpritePtr + 1 JSR LOADENMY LDX #enemySpritePtr LDY yEnemyMSB JSR ADJUST LDX yEnemyMSB TXA LDX #enemySpritePtr JSR VERTADJ STY VDELP1 LDX #HMP1-HMP0 LDA xEnemyMSB JSR MOVSPRIT LDX #HMM0-HMP0 LDA goldX JSR MOVSPRIT LDX #HMM1-HMP0 LDA trapX JSR MOVSPRIT LDX #HMBL-HMP0 LDA doorX JSR MOVSPRIT STA WSYNC STA HMOVE JSR VBLNKEND ;}}} ;{{{ ; KERNAL - Hier wird der Bildschirm gezeichnet (Hintergrund, Sprites, ; Missels, Ball) ; KERNAL subroutine IFNCONST NTSC LDX #18 .pal STA WSYNC DEX BNE .pal ENDIF LDA scroll STA blockHeightCnt ; Blockreihe initialisieren. LDA #0 ; Enemy-Sprite-Daten fuer ersten Durchlauf STA temp LDY #178 ; Y enthaelt immer die aktuelle Scanline .kernal LDX #ENABL ;2 58 TXS ;2 60 LDX temp ;2 62 DEY ;2 64 Scanlinecounter aktualisieren. DEY ;2 66 BEQ .endKERN ;2 68 STA WSYNC ;3 71 Naechste Scanline beginnen STA GRP1 ;3 3 Enemy-Sprite zeichen; triggert u.U. GRP0 CPY doorY ;3 6 PHP ;3 9 CPY trapY ;3 12 PHP ;3 15 NOP ;2 17 Synchronisieren LDA pf1l,X ;4 21 STA PF1 ;3 24 LDA pf2l,X ;4 28 STA PF2 ;3 31 LDA pf0r,X ;4 35 STA PF0 ;3 38 LDA pf1r,X ;4 42 STA PF1 ;3 45 LDA pf2r,X ;4 49 STA PF2 ;3 52 STX temp ;3 55 Index des aktuell. Band zwischenspeichern LDX #0 ;2 57 In der naechsten Scanline, soll das STX PF0 ;3 60 Spielfeld deaktiviert werden. CPY goldY ;3 63 PHP ;3 66 TYA ;2 68 Vorbereitungen fuer SKPHERO SEC ;2 70 in der naechsten Scanline. STX PF1 ;3 73 Spielfeld in naechster Scanline deaktiv STA WSYNC ;3 76 Naechste Scanline beginnen. STX PF2 ;3 3 Spielfeld deaktivieren SBC yHeroMSB ;3 6 ADC #SPRITE_HEIGHT*2 ;2 8 BCC .skphero ;2 10 LDA (heroSpritePtr),y ;6 16 .cont1 STA GRP0 ;3 19 TYA ;2 21 SEC ;2 23 SBC yEnemyMSB ;3 26 ADC #SPRITE_HEIGHT*2 ;2 28 BCC .skpenem ;2 30 LDA (enemySpritePtr),y;6 36 .cont2 DEC blockHeightCnt ;5 41 Counter fuer innere Schleife dekremntrn BNE .kernal ;2 43 Ggf. naechste Zeile des aktuellen Bands? INC temp ;5 48 Nein, also Index f akt. Band dekremntrn LDX #BLK_HEIGHT/2 ;2 50 Zaehler fuer die Zeilen der naechsten STX blockHeightCnt ;3 53 Blockreihe initialisieren. BNE .kernal ;3 56 branch-always .skphero LDA #0 BEQ .cont1 .skpenem LDA #0 BEQ .cont2 .endKERN STY WSYNC ;}}} ;{{{ ; LIFEBAR - Stellt die Lebensanzeige am unteren Bildschirmrand dar. ; Wir befinden uns gerade bei Scanline 216, d.h. bei NTSC haben wir noch ; genau 13 (= 262-216-30-3) Scanlines Zeit. ; LIFEBAR LDA #LIFEBAR_TIM64T STA TIM64T ; Timer fuer Scanlines setzen DEC yEnemyMSB ; Die 2 Einheiten, die wir fuer die bessere DEC yEnemyMSB ; Darstellung addiert haben, wieder abziehen LDA #3 STA sprite6_height LDA goldcnt AND #%00000011 TAX LDA #GOLDCOL,X STA sprite6_color LDA #>SPRITE_PAGE ; Basisadresse der Sprite-Speicherseite laden LDY #0 (dann sind wir sowieso zu schnell)? CPX #X_MOT_STOP_THR ;2 Nein, MSB=0 also LSB u Schwellwrt vergleichen BCS .dontstp ;3 Wurde des Schwellwert unterschritten? STA xSpeedMSB ;3 Ja, also Geschwindigkeit auf 0 setzen (in A STA xSpeedLSB ;3 steht ja noch die 0) .dontstp RTS ;6 ;=49 ;}}} ;{{{ ; FRICTION - Damit die Steuerung des Spielers sich natuerlicher ; anfuehlt, berechnen wir die Reibung und subtrahieren diese von der ; aktuellen horizontalen Geschwindigkeit des Spielers. Das sorgt zum ; einen dafuer, dass es eine Hoechstgeschwindigkeit gibt und zum ; anderen, dass die Geschwindigkeit allmaehlig abgebremst wird, wenn der ; Joystick weder nach links noch nach rechts bewegt wurde. ; ; Es wird ein kleiner Bruchteil (hier 1/32) der aktuellen ; Geschwindigkeit berechnet und von eben dieser (der aktuellen ; Geschwindigkeit) subtrahiert. Da dieser Bruchteil irgendwann ungefaehr ; identisch mit Geschwindigkeitskonstante HERO_ACCEL_X ist, wird die ; Hoechstgeschwindigkeit limitiert, da die Addition der zuvor hinzu ; addierten Geschindigkeitskonstante ja dadurch aufgehoben wird. ; FRICTION SUBROUTINE LDY #1 ;2 STY temp1 ;3 DEY ;2 LDA xSpeedMSB ;3 BMI .negativ ;3 ORA xSpeedLSB ;2 BEQ .end ;3 STY temp1 ;3 DEY ;2 .negativ LDA xSpeedMSB ;3 STA temp2 ;3 LDA xSpeedLSB ;3 LDX #5 ;2 =34 .div32 LSR temp2 ;5 ROR ;2 DEX ;2 BNE .div32 ;3 =34 + (12*5) = 94 EOR #$FF ;2 CLC ;2 ADC temp1 ;3 LDX # logische Y-Koord? BCS .end ;3 Ja, daher Routine beenden. CLC ;2 Nein, daher naechste Pruefung. ADC #11 ;2 Ist die "untere" Levelposition TAY ;2 Fuer Umrechnung zwischenspeichern. CMP (enemyposptr,X) ;6 kleiner als die logische Y-Koordinate? BCC .end ;3 Ja, daher Routine beenden. LDA flag ;3 Nein, aber wird Spielfeld gerade AND #SCROLL_FLAG_MASK ; gescrollt? BNE .end ;3 Ja, daher Rouine beenden. TYA ;2 Logische Y-Koordinate in konkrete SEC ;2 Screen-Koordinate umrechnen, indem die SBC (enemyposptr,X) ;6 logische Y-Koordinate von der ASL ;2 unteren Levelposition subtrahiert- ASL ;2 und mit 16 multipliziert wird. ASL ;2 ASL ;2 TAY ;2 Ergebnis im Y-Register sichern. INC enemyposptr ;5 Pointer auf 2.Byte setzen. LDA (enemyposptr,X) ;6 Gegner/Objekt AND #%11100000 ;2 Typ freistellen PHA ;3 und auf dem Stack sichern. LDA (enemyposptr,X) ;6 Logische X-Koordinate AND #%00011111 ;2 freistellen ASL ;2 und mit 4 multiplizieren. ASL ;2 CLC ;2 Die 4 Bits des ungenutzten PF0L-Registers ADC #16 ;2 durch Addition von 16 kompensieren. TAX ;2 Ergebnis im X-Register sichern. INC enemyposptr ;5 Pointer auf naechste Position setzen. PLA ;4 Gegner/Objekt Typ in Akku laden. CMP #TRAP ;2 BNE .gold ;3 LDA #trapcoord ;2 JSR SAVCOORD ;6 LDX #%00110000 ;2 Ein Schuss soll immer 8 Pixel breit sein. BNE .nusiz1 ;3 branch-always .gold CMP #GOLD ;2 BNE .door ;3 LDA #goldcoord ;2 JSR SAVCOORD ;6 RTS ;6 .door CMP #DOOR ;2 BNE .tubebal ;3 LDA #doorcoord ;2 JSR SAVCOORD ;6 RTS ;6 .tubebal STX xEnemyMSB ;3 +15 STY yEnemyMSB ;3 ; Achtung hier wird der Wert fuer den Schuss ueberschrieben, daher NUSIZ1 merken ; und hier nur die untersten drei Bits veraendern LDX #%00110000 ;2 Wir muessen den Wert fuer NUSIZ1 STA enemyState ;3 abhaengig davon setzen, ob es sich um einen CMP #BALL_1 ;2 einzelnen Ball/Schlauch handelt (0) BEQ .nusiz1 ;3 oder um einen Doppel-Ball/Schlauch (2). CMP #TUBE_1 ;2 Dabei muessen wir natuerlich beachten, dass BEQ .nusiz1 ;3 Ein Schuss immer 8 Pixel breit ist. LDX #%00110010 ;2 .nusiz1 STX NUSIZ1 ;3 STX NUSIZ1_COPY ;3 .end RTS ;6 ;}}} ;{{{ ; SAVCOORD - Speichert den Wert im X-Register an die Adresse im Akku. ; Subtrahiert 6 zum Wert im Y-Register und speichert diesen an die ; nachfolgende Adresse. ; SAVCOORD subroutine STA temp1 TXA LDX temp1 STA $00,X INX TYA SEC SBC #6 STA $00,X RTS ;}}} ;{{{ ; ENEMYPF - Die folgende Routine prueft, ob es eine Kollision zwischen ; dem Gegner und dem Spielfeld gibt. Zu diesem Zweck wird die Routine ; CHKPFCOL mit den Werten Werten (xEnemyMSB + Akku) und (yEnemyMSB + ; Y-Register) aufgerufen. ; ENEMYPF subroutine CLC ADC xEnemyMSB TAX TYA CLC ADC yEnemyMSB JSR CHKPFCOL RTS ;}}} ;{{{ ; HEROPF - Die folgende Routine prueft, ob es eine Kollision zwischen ; dem Spieler und dem Spielfeld gibt. Zu diesem Zweck wird die Routine ; CHKPFCOL mit den Werten Werten (xHeroMSB + Akku) und (yHeroMSB + ; Y-Register) aufgerufen. ; HEROPF subroutine CLC ADC xHeroMSB TAX TYA CLC ADC yHeroMSB JSR CHKPFCOL RTS ;}}} ;{{{ ; CHKPFCOL - Prueft, ob der uebergebene Punkt mit dem Spielfeld ; kollidiert. Die X-Koordinate des Punkts wird im X-Register uebergeben, ; die Y-Koordinate im Akkumulator. Bei einer Kollision ist das Zero-Flag ; *nicht* gesetzt (d.h. gesetztes Zero-Flag bedeutet, dass es keine ; Kollision gab)! ; CHKPFCOL subroutine TAY ; Y-Koordinate zwischenspeichern LDA flag ; Scroll-Flag laden AND #SCROLL_FLAG_MASK ; und freistellen CMP #SCROLL_FLAG_MASK ; Wird das Level gerade gescrollt? TYA ; vorher Y-Koordinate wiederherstellen BCC .noscrol ; Nein, Level wird nicht gescrollt. LDA scroll ; "Scroll-Offset" mit zwei ASL ; multiplizieren. STA temp ; Den Scroll-Offset von 16 (=BLK_HEIGHT) LDA #BLK_HEIGHT ; subtrahieren. SEC SBC temp STA temp TYA ; Den ausgerechneten Wert von anfangs uebergebenen SEC ; Y-Koordinate subtrahieren. SBC temp .noscrol CPX #144 ;2 Ist die X-Koordinate >= 144 ? BCS .end ;2 Ja, also kann es keine Kollision geben. CPX #16 ;2 Ist die X-Koordinate < 16 BCC .end ;2 Ja, also kann es keine Kollision geben. CMP #$B0 ;2 Ist die Y-Koordinate >= 176 ? BCS .end ;2 Ja, also kann es keine Kollision geben. LSR ;2 Die Y-Koordinate wird durch 16 geteilt LSR ;2 (Hoehe einer Blockreihe). LSR ;2 LSR ;2 CLC ;2 Da die niedrigsten Blockreihen die hoechsten ADC #-10 ;2 Y-Koordinaten haben, muessen wir den Index EOR #$FF ;2 "umdrehen" indem wir 10-index rechnen (hier TAY ;2 mittels 2er-Komplement). Im Y-Register steht INY ;2 jetzt der Offset fuer die pfXX-Variablen. TXA ;2 X-Koordinate in Akku laden CMP #48 ;2 Ist die X-Koordinate < 48? BCS .pf2l ; Nein, naechster Check. SEC ; Ja, also erstmal 16 subtrahieren (das pf1l SBC #16 ; bildet die Koordinaten [16-48] ab). LDX pf1l,Y ; PF-Register laden. JMP .chkpf1 .pf2l CMP #80 ; Ist die X-Koordinate < 80? BCS .pf0r ; Nein, naechster Check. SEC ; Ja, also erstmal 48 subtrahieren (das pf2l SBC #48 ; bildet die Koordinaten [48-80] ab). LDX pf2l,Y ; Inhalt d PF-Regs laden, da PF2 von Natur aus JMP .check ; schon umgedreht ist, direkt zur Kollisionsabfr .pf0r CMP #96 ; Ist die X-Koordinate < 96? BCS .pf1r ; Nein, naechster Check. SEC ; Ja, daher 80 subtrahieren (das pf0r bildet die SBC #80 ; Koordinaten [80-96] ab, ist ja nur 4-Bit breit) LDX pf0r,Y ; Inhalt des PF-Registers laden. TAY ; (x-80) im Y-Register zwischenspeichern. TXA ; Wert des PF-Registers in Akku laden. LSR ; Da vom PF0 nur die Bits D4-D7 verwendet werden LSR ; sorgen wir dafuer, dass diese in die Bits D0-D3 LSR ; verschoben werden (umgedreht ist das PF0 ja von LSR ; Natur aus). TAX ; Das "verschobene" PF-Register in X laden. TYA ; Die "normalisierte" X-Koord wiederherstellen. JMP .check ; Kollisionsabfrage durchfuehren. .pf1r CMP #128 BCS .pf2r SEC SBC #96 LDX pf1r,Y .chkpf1 LSR ; Die "normalisierte" X-Koordinate wird durch 4 LSR ; (Breite eines Blocks) geteilt. Da das PF1-Reg CLC ; von links nach rechts angezeigt wird, drehen ADC #-7 ; wir die Bitreihenfolge um, in dem wir 7-x EOR #$FF ; Rechnen (hier mittels 2er-Komplement). TAY INY TXA AND .bitmask,Y RTS .pf2r CMP #144 BCS .end SEC SBC #128 LDX pf2r,Y .check LSR ; Die "normalisierte" X-Koordinate wird durch 4 LSR ; (Breite eines Blocks) geteilt. Dadurch wissen TAY ; wir, welches Bit wir ueberpruefen muessen, um TXA ; rauszufinden, ob es eine Kollision gab. AND .bitmask,Y RTS .end LDA #0 RTS .bitmask .byte #%00000001 .byte #%00000010 .byte #%00000100 .byte #%00001000 .byte #%00010000 .byte #%00100000 .byte #%01000000 .byte #%10000000 ;}}} ;{{{ ; SCRLDOWN - ; SCRLDOWN subroutine DEC yHeroMSB ;5 DEC yHeroMSB ;5 DEC yEnemyMSB ;5 DEC yEnemyMSB ;5 DEC goldY ;5 DEC goldY ;5 DEC trapY ;5 DEC trapY ;5 DEC doorY ;5 DEC doorY ;5 LDA #BLK_HEIGHT/2 ;2 CMP scroll ;3 BNE .skip ;2 LDA #0 ;2 STA scroll ;3 JSR SCRLBLKR ;6 +587 JSR BLOCKROW ;6 +643 JSR CONVTOPF ;6 +59 .skip INC scroll ;5 LDA scroll ;3 CMP #BLK_HEIGHT/2 ;2 BNE .end ;2 LDA flag ; Scroll-Flag ausschalten AND #DISABLE_SCROLL_FLAG STA flag .end RTS ;6 103 ;}}} ;{{{ ; BLOCKROW - Laedt die naechste Blockreihe ; ; Die Spielfelddaten sind im Speicher zeilenweise abgelegt (mit Zeile ist hier ; eine Blockreihe gemeint). Dabei steht die oberste Blockreihe am Anfang des ; Speichers und die unterste Blockreihe (dort wo das Level beginnt) am Ende. ; D.h. die Spielfelddaten muessen von hinten nach vorne ausgelesen werden. ; ; Eine Blockreihe besteht aus 32 Bits, allerdings verwenden wir nur die halbe ; Aufloesung (d.h. jeder Block einer Blockreihe ist 2 Bit breit). Dadurch kann ; eine Blockreihe durch genau zwei Bytes definiert werden. ; ; Die folgende Routine laedt die beiden Bytes, die hinter dem `levelptr' ; stehen, verdoppelt jedes Bit, und schiebt die Bits in die temporaeren ; Variablen pos1, pos2, pos3 und pos4. ; BLOCKROW subroutine LDY #2 ; Index fuer levelptr initialisieren. LDA #8 ; Bit-Counter initialisieren und in STA temp1 ; temp1 ablegen. LDA (levelptr),Y ; 1.Level-Byte laden. .loop1 LSR ; Carry-Flag mit aktuellem Bit bestuecken. PHP ; Carry-Bit auf dem Stack sichern. ROR pos3 ; Carry-Bit in die temporaeren Variablen ROR pos4 ; pos3 und pos4 schieben. PLP ; Carry-Bit wiederherstellen und nochmal ROR pos3 ; in die temporaeren Variablen pos3 und pos4 ROR pos4 ; schieben (ein Block ist immer 2 Bit breit). DEC temp1 ; Haben wir alle Bits verdoppelt? BNE .loop1 ; Nein, weiter mit naechstem Bit. LDA #8 ; Ja, Bit-Counter fuer naechstes Level-Byte STA temp1 ; initialisieren und in temp1 ablegen. DEY ; Naechstes Level-Byte laden. LDA (levelptr),Y ; .loop2 LSR ; Carry-Flag mit aktuellem Bit bestuecken. PHP ; Carry-Bit auf dem Stack sichern. ROR pos1 ; Carry-Bit in die temporaeren Variablen ROR pos2 ; pos1 und pos2 schieben. PLP ; Carry-Bit wiederherstellen und nochmal in ROR pos1 ; die temporaeren Variablen pos1 und pos2 ROR pos2 ; schieben. DEC temp1 ; Haben wir alle Bits verdoppelt? BNE .loop2 ; Nein, weiter mit naechstem Bit. DEC levelptr ; Ja, daher levelptr 2 Bytes Richtung DEC levelptr ; Anfang verschieben. RTS ;}}} ;{{{ ; LOADPF - Laedt einen kompletten Spielfeld-Ausschnitt ; LOADPF subroutine LDA #PFREGS-1 STA temp2 .loop JSR SCRLBLKR JSR BLOCKROW JSR CONVTOPF DEC temp2 BNE .loop RTS ;}}} ;{{{ ; SCRLBLKR - Diese Routine scrollt das Spielfeld nach oben. Zu diesem ; Zweck werden saemtliche Blockreihen eine Position nach unten kopiert. ; SCRLBLKR subroutine LDX #10 ;2 Initialisierung Quellindex LDY #11 ;2 Initialisierung Zielindex .loop LDA pf1l,X ;4 STA pf1l,Y ;5 LDA pf2l,X ;4 STA pf2l,Y ;5 LDA pf0r,X ;4 STA pf0r,Y ;5 LDA pf1r,X ;4 STA pf1r,Y ;5 LDA pf2r,X ;4 STA pf2r,Y ;5 DEX ;2 DEY ;2 (11*52) + 10 = 582 BNE .loop ;3/2 RTS ;6 ;}}} ;{{{ ; CONVTOPF - Die logischen Grafiken in pos1..pos4 werden in die ; technischen Register pf1l..pf2r geschrieben. ; ; +----------+----------+----------+----------+ ; | pos1 | pos2 | pos3 | pos4 | ; +----------+----------+----------+----------+ ; | pf1l | pf2l | pf0r| r1fp | pf2r| ; +----------+----------+----------+----------+ ; ; Zu beachten ist, dass das Hi-Nibble von pos4 als Hi-Nibble von ; pf1r verwendet wird und das Lo-Nibble von pos3 als Lo-Nibble von ; pf1r verwendet wird (wird im Diagramm durch die spiegelverkehrte ; Schreibweise von pf1r angedeutet). ; CONVTOPF subroutine LDA pos1 STA pf1l LDA pos2 STA pf2l LDA pos3 AND #%11110000 STA pf0r LDA pos3 AND #%00001111 STA pf1r LDA pos4 AND #%11110000 ORA pf1r STA pf1r LDA pos4 AND #%00001111 STA pf2r RTS ;}}} ;{{{ ; VBLNKEND - Wartet auf das Ende des Vertical-Blank ; VBLNKEND subroutine LDA INTIM ; Timer checken, Vertical-Blank schon vorbei? BNE VBLNKEND ; Nein, also weiter warten STA WSYNC STA VBLANK ; Enable beam/stop VBLANK RTS ;}}} ;{{{ ; OVRSCEND - Wartet auf das Ende der Overscan-Periode ; OVRSCEND subroutine LDA INTIM ; Timer checken, ist Overscan schon vorbei? BNE OVRSCEND ; Nein, also weiter warten STA WSYNC RTS ;}}} ;{{{ ; OVRSCBEG - Leitet die Overscan-Periode ein ; OVRSCBEG subroutine LDA #$42 ; Overscan einleiten LDA #%01000010 STA VBLANK LDA #OVRSCBEG_TIM64T STA TIM64T ; Timer setzen RTS ;}}} ;{{{ ; TITLEPTR - setzt die sprite6-Pointer fuer den Titelbildschirm. ; Die Basisadresse der Speicherseite auf der sich die Sprite-Daten ; befinden, werden im Y-Register uebergeben. ; Der Index auf die Daten des 6ten Sprites werden im Akku uebergeben. ; Die Hoehe der Sprites wird in der Variablen sprite6_height uebergeben. ; TITLEPTR subroutine LDX #10 .setptr STA sprite6_ptr1,X STY sprite6_ptr1+1,X SEC SBC sprite6_height DEX DEX BPL .setptr RTS ;}}} ;{{{ ; TITLSCRN - Diese Routine zeigt den Titel-Screen solange an, bis der ; Feuerknopf gedrueckt wurde ; TITLSCRN subroutine JSR VBLNKEND ; und abwarten... LDA #TEXT_OFFSET_TIM64T ; Timer fuer NTSC/PAL Bild STA TIM64T ; bestuecken LDY #TITLSCRN_TIM64T ; Einige Scanlines abwarten, .wait STA WSYNC ; bevor wir den Text angezeigen... DEY BNE .wait LDA #$0F STA sprite6_color LDA #32 ; Jetzt die "BLINKY goes up" Grafik STA sprite6_height ; anzeigen. LDY #>TITLE_PAGE_1 ; Dazu die Basisadresse der Speicherseite LDA #TITLE_PAGE_2 ; Dazu die Basisadresse der Speicherseite LDA #TITLE_PAGE_1 ; Dazu die Basisadresse der Speicherseite LDA #TITLE_PAGE_1 ; Basisadresse der Speicherseite LDA #ENDSCREEN ; Basisadresse der Speicherseite LDA #ENDSCREEN ; Basisadresse der Speicherseite LDA #