;----------------------------------------------------------------------------- ; game.inc ; Part of manic miner, the zx spectrum game, made for Apple II ; ; Stefan Wessels, 2020 ; This is free and unencumbered software released into the public domain. ;----------------------------------------------------------------------------- .segment "CODE" ;----------------------------------------------------------------------------- .proc gameLoop jsr gameNewGame ; init things that need 1-time game init nextLevel: jsr tilesPrepForLevel ; move 8 tiles for this level to tilesInstances restart: jsr gameInitStage ; init the stage, incl unpacking the level, tile prep etc playLoop: jsr inputGet ; read the keyboard lda demoMode ; check for demo mode beq :+ ; if not demo, move willy jsr gameDemoTick ; tick the demo if demo mode jmp :++ ; skip moving willy : jsr willyMove ; move the main character based on user input (or conveyor or gravity) : jsr gameAI ; run all the monster AI ldx fullScreenClearCount ; get the flag for what level of screen clear is needed jsr screenClear ; and clear the screen jsr tilesAnimateKeys ; copy the 16 bytes for the next "key" frame into place jsr tilesAnimateConveyor ; update the conveyor tile jsr screenDrawSprites ; draw all the enemies lda demoMode ; get the event state bne :+ ; if so skip showing willy jsr screenDrawWilly ; not demo - draw willy with collision detection bcc :+ ; carry set is a collision, clear is no problem lda eventState ; on collision, set the die event ora #EVENT_DIED sta eventState : jsr screenDrawLevel ; show all the tiles lda updateUICount ; see if the UI needs an update beq :+ jsr uiUpdate ; if needed, update the appropriate UI components : ldx numSprites ; The door's index jsr screenDrawSprite ; render the door over everything else, no collision jsr screenSwap ; swap to the newly rendered screen jsr audioPlayNote ; play in-game music if needed lda eventState ; see if any events fired beq playLoop ; keep looping while no events jsr gameEvent ; process the event, return value in a bit bit1Mask ; EVENT_LEVEL_RESTART bne restart bit bit2Mask ; EVENT_NEXT_LEVEL bne nextLevel lda demoMode ; no Game Over when the demo ends bne :+ jsr gameOver ; wasn't a demo, show Game Over : jsr textCheckHighScore ; game over - see if a new high score was set rts .endproc ;----------------------------------------------------------------------------- .proc gameNewGame lda #START_LIVES ; init player number of lives sta lives lda #LEVEL_Central_Cavern ; Init these game vars to 0 sta currLevel lda #0 ; audioPlayNote uses muiscL as well sta musicL ; track progress through in-game song ldx #5 ; six, zero-based score digits lda #'0' ; load a with '0' : sta score, x ; set all 6 score digits to '0' dex bpl :- rts .endproc ;----------------------------------------------------------------------------- .proc gameInitStage count = tmpBot + 0 spriteIdx = tmpBot + 1 instanceIdx = tmpBot + 2 ldy currLevel ; Skip some stuff for "Game Over" pseudo-level cpy #20 bcs notDemo jsr levelUnpack ; decompress a level to levelLayout jsr levelPlaceKeys ; put the keys into the world jsr textSetLevelText ; create the gfx cache for the name ldy currLevel ; tables are indexed by level lda demoMode ; see if this is demo mode beq notDemo ; branch if game lda willyx ; use willy screen 0 for willyXPos in demo (force leftEdge to 0) bne :+ notDemo: lda willyx, y ; set willy X up : sta willyXPos ldx cameraMode lda #CAMERA_MODE_SCROLL sta cameraMode jsr willyMove::positionScreen ; set the screen position based on willy's X stx cameraMode lda willyy, y ; set willy's Y sta willyYPos lda willyStartDir, y ; and see what facing direction sta willyDir beq :+ ; if right (0) then set that as the animFrame lda #4 ; if left (1) set the anim frame to 4 : sta willyFrame ; and save the anim frame postWilly: lda #3 ; AI will count down and runs before clearScreen sta fullScreenClearCount ; so set to 3 to get full clear on both buffers lda #AIR_SPEED ; set counter for ticking down air sta airFlow lda #32 ; set how many air cols there are sta airCols lda #$7f ; the air "bar" tip is a character that draws less sta airTipGfx ; bar over time, till it rolls to the prev column lda #$ff ; match all masks jsr uiUpdateComponent ; so force all UI elements to draw lda level_sprites_count, y ; get how many enemy sprites the level has sta count ; save for init sta numSprites ; save for the game to use dec numSprites ; make this count 0 based lda level_sprites_offset, y ; see where in the sprite tables this sprite starts tay ; move the offset into x - sprite tables indexed by this ldx #0 ; y will be an enemy instance index stx eventState ; init he event state stx livesFrame ; but use this 0 to init some variables stx movementMask ; current movement for willy stx conveyorMask ; direction conveyor under willy is moving (0=no conveyor under willy) stx userKeyMask ; keys the user has pressed to move willy stx willyJumpCounter ; 0 means no jump, 1-18 is in a jump, beyond is a fall height stx spriteFramesIdx clc ; clear carry - will always enter below with carry clear next: sty spriteIdx ; save which sprite is being processed (x clobbered) stx instanceIdx ; save y, also clobbered lda #0 sta spriteFrame, x ; init the sprite specific frame counter to 0 lda sprites_x, y ; load the basic sprite variables and put into zero page sta spriteXPos, x ; from 0 .. numSprites - 1 lda sprites_y, y ; the addressing is unfortunate - sta ,x is zero-page 2 byte sta spriteYPos, x ; but it's not worth swapping x and y for this bit of code lda sprites_min, y sta spriteMin, x lda sprites_max, y sta spriteMax, x lda sprites_speed, y sta spriteSpeed, x sta spriteTick, x lda sprites_dir, y sta spriteDir, x lda sprites_class, y sta spriteClass, x lda sprites_colors, y ; get the color sta spriteColor, x lda sprites_bitmaps, y ; get the index into the sprite data for this sprite's images jsr spriteInstanceSpriteFrames ldx instanceIdx ; restore y cpx numSprites beq prep lda spriteClass, x bit bit1Mask ; CLASS_FOUR_FRAME beq :+ lda #4 bne :++ : lda #8 : adc spriteFramesIdx, x sta spriteFramesIdx + 1, x ldy spriteIdx iny ; next sprite in sprite table inx ; next instance dec count beq prep jmp next prep: ldx currLevel ; some levels need special handling cpx #LEVEL_Eugenes_Lair ; eugene needs colored copies bne :+ jsr spriteEugeneSetup jmp fixDoor : cpx #LEVEL_Miner_Willy_meets_the_Kong ; kong levels need switches beq kong cpx #LEVEL_Return_of_the_Alien_Kong_Beast bne :+ kong: lda #DATA_SWITCH1 sta levelLayout + 6 lda #DATA_SWITCH2 sta levelLayout + 18 jmp fixDoor : cpx #LEVEL_Skylab_Landing_Bay ; skylab needs satellites to get X values bne :+ ldx #2 ; init the falling satellites skyLabPos: lda skylabXPos, x sta spriteXPos, x txa sta spriteTick, x dex bpl skyLabPos : cpx #LEVEL_The_Final_Barrier bne fixDoor dec numSprites ; hide the victory door sprite fixDoor: jmp spriteDoorSetup ; sprites are set up - door is special .endproc ;----------------------------------------------------------------------------- .proc gameAI lda fullScreenClearCount ; get the state beq :+ ; if it's zero, move on dec fullScreenClearCount ; count this down : dec airFlow ; deal with the air countdown. airFlow is "time" counter bne airDone jsr gameAirDecrease airDone: inc livesFrame ; move the lives anim frame along lda livesFrame ; through the first 4 frames only cmp #16 ; by dividing by 4 (in essence) to get a slower animation bcc :+ lda #0 ; start at frame 0 when wrapping : sta livesFrame and #3 ; only update every 4 frames (not & 3) bne :+ lda #UI_COMPONENT_LIVES ; tell ui to update lives jsr uiUpdateComponent : ldx numSprites ; now set up all the sprites for this level loop: lda spriteClass, x ; start with the class bit CLASS_DOOR ; and if it's a door, treat that separate beq :+ jmp door : bit CLASS_MOVE_Y ; see if it moves vertically beq horizontal ; no - then it moves horizontally jmp vertical ; yes - them move it vertically horizontal: lda spriteSpeed, x ; get speed bit bit0Mask ; Is the speed 1 beq rtick ; if not, advance this sprite dec spriteTick, x ; dec the ticker bpl next ; if ge 0 then ignore sprite sta spriteTick, x ; reset ticker to speed and fall through to run rtick: lda spriteDir, x ; get the direction bne left ; 1 is left, 0 is right right: inc spriteFrame, x ; move the frame lda spriteFrame, x ; load it cmp #4 ; see if it's 4 bcc next ; if 4+, done here inc spriteXPos, x ; up the x position lda spriteXPos, x ; load it cmp spriteMax, x ; see if it's ge max bcs rightEnd ; if yes, end of going right lda #0 ; no, reset sta spriteFrame, x ; frame to 0 beq next ; BRA. done with this sprite rightEnd: dec spriteXPos, x ; set back to last valid x lda #1 ; load left sta spriteDir, x ; and set direction left lda #7 ; 7 is most right position of sprite sta spriteFrame, x ; and set the frame to that fix4: lda spriteClass, x ; get the class bit CLASS_FOUR_FRAME ; see if it has the (only) 4 frames flag set beq next ; if not, done with this sprite (frame 7 is good) lda #3 ; drop the 7 to 3 sta spriteFrame, x ; set the frame jmp next left: dec spriteFrame, x ; move the frame lda spriteFrame ,x ; load it cmp #$ff ; see if it is now lt 0 (3,2,1 -> 0 overflow) beq stepLeft ; if it is, move the col left cmp #3 ; if it's 3, (7,6,5,4 -> 3 overflow) bne next ; if not, done with this sprite stepLeft: dec spriteXPos, x ; move the column left lda spriteXPos, x ; load it cmp spriteMin, x ; compare to minimum bcc leftEnd ; if less than minimum, too far, past the left edge lda #7 ; keep going, load 7 sta spriteFrame ,x ; set frame back to 7 bne fix4 ; BRA, and check for a 4-frame sprite leftEnd: inc spriteXPos, x ; put the column back to in range lda #0 ; load 0 sta spriteDir, x ; set as direction (right) sta spriteFrame, x ; and frame (most left frame) beq next ; BRA, done with this sprite postVMove: lda spriteClass, x ; after a vertical update check these special cases bit CLASS_EUGENE ; Eugene bne eugene bit CLASS_KONG ; Kong beq :+ jmp kong : bit CLASS_SKYLAB ; skylab beq :+ jmp skylab : inc spriteFrame, x ; otherwise go to next frame lda spriteFrame, x and #3 sta spriteFrame, x next: dex ; get previous sprite bpl goTop ; if ge 0 then still a sprite to process rts ; all sprites done - exit goTop: jmp loop vertical: lda spriteDir, x ; get direction 1 = UP, 0 = DOWN beq down bmi postVMove ; if the spriteDir is lt $ff, stationary sprite up: lda spriteYPos, x ; get the Y position sec sbc spriteSpeed, x ; move up by the speed cmp spriteMin, x ; see if at top bcc upEnd ; overshot top sta spriteYPos, x ; update Y position bcs postVMove ; BRA upEnd: lda spriteClass, x ; get the class bit CLASS_HOLDATEND ; should it stop or bounce bne stop ; HOLDATEND means stop lda #0 ; change direction sta spriteDir, x ; to DOWN (0) beq postVMove ; BRA down: lda spriteYPos, x ; get the Y clc adc spriteSpeed, x ; add the speed cmp spriteMax, x ; see of at end bcs downEnd ; at or past end sta spriteYPos, x ; still good, update Y position bcc postVMove ; BRA downEnd: lda spriteClass, x ; same as upEnd bit CLASS_HOLDATEND bne stop lda #1 ; but mark for moving UP (1) sta spriteDir, x bne postVMove ; BRA maybe down? stop: lda #$ff ; set the direction to -1 (lt 0) sta spriteDir, x bne postVMove ; BRA door: lda keysToCollect ; check if all keys have been collected bne next ; no - nothing more to do frameToggle: dec spriteTick, x ; count down for animation bpl next ; gt 0, nothing more lda spriteFrame, x ; get the frame eor #1 ; toggle between 1 and 0 sta spriteFrame, x ; update the frame lda spriteSpeed, x ; get the anim speed sta spriteTick, x ; save it to the tick jmp next eugene: lda keysToCollect ; eugene changes behavior when all keys collected bne eugeneNormal ; not all keys, carry on lda #0 ; all keys - force eugene down sta spriteDir, x inc spriteFrame, x ; cycle through the 5 colors lda spriteFrame, x cmp #4 bcc :+ lda #0 : sta spriteFrame, x ; save the new frame eugeneNormal: jmp next kong: lda spriteMax, x ; if kong's max is 0 he's still up beq frameToggle lda spriteDir, x ; if he's not up see what his direction is bpl kongFall ; gt 0, then he's still falling cmp #$FE ; $fe he's been erased so done with him beq kongDone ; $ff he's just reached the bottom dec spriteDir, x ; turn $ff into $fe txa ; put the sprite index into a pha ; and save it lda spriteFramesIdx, x ; get the index to the 1st kong frame tax ; put that in x inx ; and skip the 2 frames where inx ; kong is standing ldy #2 ; want to clear 2 frames jsr spriteClearFrames ; and make the falling frames blank pla ; get the sprite index tax ; and put it back in x kongDone: jmp next kongFall: ldx #3 ; digit 3 (100's) lda #1 ; add 1 jsr textAddScore ; add to score lda #UI_COMPONENT_SCORE ; show changes jsr uiUpdateComponent jmp frameToggle skylab: lda spriteDir, x ; get the direction of the falling satellite cmp #$ff ; see if it's reached its end beq :+ ; yes it has jmp next : inc spriteFrame, x ; advance the collapsing frame lda spriteFrame, x ; load that frame cmp #8 ; see if it's the last bcs :+ ; yes jmp next : lda spriteTick, x ; get the tick (hich is an index in this case) clc adc #3 ; advance by 3 (3 satellites at a time) so next index for this satellite cmp #12 ; (3*4 is 12) - there are 4 stating locations per satellite bcc :+ ; not rolled over and #3 ; reset this satellite to 1st starting location (index) : sta spriteTick, x ; save the tick tay ; put into Y lda skylabXPos, y ; get the actual start position, based on y, for this satellite sta spriteXPos, x ; put that into the satellite lda #0 ; reset the frame, position and direction all to 0 sta spriteFrame, x sta spriteYPos, x sta spriteDir, x jmp next .endproc ;----------------------------------------------------------------------------- .proc gameDemoTick dec demoTimer ; timer counts down bne :+ ; if not yet zero, nothing to do lda #DEMO_TIMER_DURATION ; reset the timer sta demoTimer lda #UI_COMPONENT_NAME ; mark the level name as needing an update jsr uiUpdateComponent lda leftEdge ; scroll the screen clc adc demoDirection ; based on the scrolling direction sta leftEdge beq nextDemoLevel ; if the edge is 0 then done with level cmp #12 ; at 12, the level flips scroll direction bne :+ lda #$ff ; at 12, the scroll direction becomes -1 sta demoDirection : rts nextDemoLevel: lda #DEMO_TIMER_INITAL ; set for a longer initial hold at a new level sta demoTimer lda #1 ; set the scroll direction to be right (1) sta demoDirection lda #0 ; set the edge to be the very left sta leftEdge lda #EVENT_NEXT_LEVEL ; fire a new level event ora eventState sta eventState rts .endproc ;----------------------------------------------------------------------------- .proc gameEvent bit bit0Mask ; EVENT_DIED beq :+ ; if not, must be end of level dec lives ; died, so take a life bmi died ; all lives lost ends the game lda #0 sta tmpBot jsr screenInvertVisibleScreen lda #EVENT_LEVEL_RESTART ; still alive so restart the level rts : bit bit4Mask ; EVENT_CHEAT_JUMP bne done ; if jumping, just go lda currLevel ; check the level cmp #19 ; is this the last level bcc :+ ; if not, go to screen invert lda cheatActive ; last level - got here by cheating? bne :+ ; yes - go screen invert lda demoMode ; is this demo mode? bne :+ ; yes - go to screen invert jsr gameVictory ; played properly through, get the victory : ldx #8 ; do an inverse screen effect : stx sizeL txa and #3 clc adc #1 sta tmpBot jsr screenInvertVisibleScreen ldx sizeL dex bne :- lda demoMode ; is this demo mode bne incLevel ; skip the air/score, just go to next level nextLevel: jsr screenSwap::valueSwap ; do the air countdown/add score routine now, on front screen airLoop: ldx #5 ; digit 5 (1's) lda #7 ; add 7 jsr textAddScore ; add to score ldx #4 ; digit 4 (10's) lda #1 ; add 1 (so add 17 per tick) jsr textAddScore ; add to score jsr gameAirDecrease ; run the decrease air lda airCols ; get the remaining bar length asl ; mult * 4 asl eor #$7f ; and reverse (ignore MSB which is 0) - this is the freq ldy #6 ; duration for the freq jsr audioPlayNote::freq ; make a sound of this freq and duration jsr uiUpdate ; and show the updates lda eventState ; get the event state bit bit0Mask ; check for EVENT_DIED beq airLoop ; not dead means more air left jsr screenSwap::valueSwap ; go back to the back screen incLevel: inc currLevel ; set the current level to the next ldx currLevel cpx #20 ; see if this is the last level + 1 bcc done ; if not, all is well lda demoMode ; check again for demo beq :+ ; if not, roll over to level 1 and keep playing died: lda #EVENT_DIED ; demo also ends with a death event rts : ldx #0 ; not demo, past last level, start at 1st level again stx currLevel done: lda #EVENT_NEXT_LEVEL ; return in a the action (next level) rts .endproc ;----------------------------------------------------------------------------- .proc gameAirDecrease lda #AIR_SPEED sta airFlow lda #UI_COMPONENT_AIR ; tick the air down, so update the UI jsr uiUpdateComponent ldx airCols ; see if it's an odd or even column inx ; but the air draws from an odd column so the 1 bit is backwards txa and #1 tax ; x 0 is even, x 1 is odd lda airTipGfx ; see what the tip looks like cmp maskGreen, x ; if it's all green, time to drop another column beq colDec lsr ; not all green, so adjust the tip by dropping 2 bits (1 white pixel) lsr ora maskGreenHi, x ; and replace that with a green pixel (appropriate for odd/even column) bne airOk colDec: dec airCols ; one less bar bpl :+ lda eventState ; out of air causes death ora #EVENT_DIED sta eventState lda #0 sta airCols ; lock airCols at 0 lda maskGreen, x ; lock to all green for the tip bne airOk : lda maskNewTip, x ; start a new (mostly white) tip, appropriate for odd/even airOk: sta airTipGfx rts .endproc ;----------------------------------------------------------------------------- .proc gameOver iter = currLevel ; how many times to loop bootPart: ldx #20 ; game over level is 20 (0 based) stx currLevel jsr gameInitStage ldx #0 ; clear the top part of the screen jsr screenClear ldx #0 ; draw the boot jsr screenDrawSprite ldx #$60 ; start of boot-drop freq stx iter lda #1 ; pretend there's a key so Boot (EUGENE) doesn't animate sta keysToCollect ; and the "door" doesn't switch to the second frame tax ; also draw the pedestal (door) jsr screenDrawSprite ; draw the boot lda #2 ; set willy to frame 2 sta willyFrame jsr screenDrawWilly ; show willy on the pedestal jsr screenSwap ; make it all visible jsr screenSwap::valueSwap ; fake the front as the back bootLoop: jsr gameAI ; run the AI to move the boot down dec iter ; raise freq dec iter lda audioMask ; see if the audio will play or skip and #AUDIO_SOUND beq otherDelay ; audio won't delay so "fake" an audio delay lda iter ; get the freq ldy #$80 ; duration for the freq (also slows the boot down) jsr audioPlayNote::freq ; make a sound of this freq and duration jmp :+ otherDelay: lda iter lsr lsr tay jsr uiDelay::ySet : ldx #0 ; go draw the boot jsr screenDrawSprite lda spriteDir ; see if the boot has reached the pedestal bpl bootLoop ; net yet, keep going gameOverPart: color = tmpBot + 1 ; index into color masks arrays xPos = tmpBot + 2 ; x for string yPos = tmpBot + 3 ; y for string textL = tmpBot + 4 ; string pointer textH = tmpBot + 5 len = tmpBot + 6 ; how many characters (0 based) lda #$20 sta iter ; how many times to loop lda #(7*8) ; Y for string sta yPos lda #4 ; starting color sta color cycleLoop: lda #4 ; print GAME at x 4 sta xPos lda #roTextGame sta textH lda #4 ; 0-3 characters sta len jsr textColorCycle ; show the text in color lda #13 ; print OVER at x 13 sta xPos lda #roTextOver sta textH lda #4 ; also 0-3 characters in length sta len jsr textColorCycle ; and show over with color ldy #$30 ; delay the iteration of color jsr uiDelay::ySet dec iter ; one less iteration to do bpl cycleLoop ; do all iterations rts .endproc ;----------------------------------------------------------------------------- .proc gameVictory lda #19 ; put willy above the door sta willyXPos ; outside the caverns lda #0 sta willyFrame lda #2*8 sta willyYPos ldx fullScreenClearCount ; get the flag for what level of screen clear is needed jsr screenClear ; and clear the screen jsr screenDrawSprites ; draw all the enemies jsr screenDrawWilly ; not demo - draw willy with collision detection jsr screenDrawLevel ; show all the tiles ldx numSprites ; The door's index inx jsr screenDrawSprite ; render the door over everything else, no collision jsr screenSwap ; swap to the newly rendered screen audioPart: freq = tmpBot + 0 ; freq duration = tmpBot + 1 ; duration iteration = tmpBot + 2 ; iteration lda #50 ; 50 iterations sta iteration lda #0 ; init freq and duration sta freq sta duration loop: lda duration ; start with the duration adc iteration ; add the iteration counter * 3 adc iteration adc iteration sta freq ; save as the freq ldy duration ; put duration in Y lda audioMask ; see if the audio will play or skip and #AUDIO_SOUND bne audioOn ; if on, use the freq "API" : ldx freq ; a bit ridiculous to redo playNote stuff here : dex ; but I want the audio code to all go through the bne :- ; same "API" for consistency dey bne :-- beq postFreq audioOn: lda freq jsr audioPlayNote::freq ; make the sound if sound enabled postFreq: dec iteration ; dec the iterations bne loop ; loop till all iterations done rts .endproc