;----------------------------------------------------------------------------- ; screen.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" ;----------------------------------------------------------------------------- ; Swap visible HGR pages and update currPageH to the back page hi byte .proc screenSwap lda backPage beq :+ bit HISCR jmp valueSwap : bit LOWSCR valueSwap: lda backPage ; pretend flip the screen eor #1 sta backPage lda #$60 ; pretend flip the screen Hi value so all drawing eor currPageH ; happens on the front screen sta currPageH rts .endproc ;----------------------------------------------------------------------------- ; x 0 cleats top (play area minus 8 pixel row), x non-0 clears whole screen .proc screenClear lda #$00 ldy backPage beq page1 jmp page2 page1: cpx #0 bne :+ jmp p1p2 : ldx #39 p1l1: .repeat $48, Row sta $100 * (>HGRPage1 + (Row + $78) & $07 << 2 | (Row + $78) & $30 >> 4) | (Row + $78) & $08 << 4 | (Row + $78) & $C0 >> 1 | (Row + $78) & $C0 >> 3, x .endrepeat dex bmi p1p2 jmp p1l1 p1p2: ldx #39 p1l2: .repeat $78, Row sta $100 * (>HGRPage1 + Row & $07 << 2 | Row & $30 >> 4) | Row & $08 << 4 | Row & $C0 >> 1 | Row & $C0 >> 3, x .endrepeat dex bmi done0 jmp p1l2 done0: rts page2: cpx #0 bne :+ jmp p2p2 : ldx #39 p2l1: .repeat $48, Row sta $100 * (>HGRPage2 + (Row + $78) & $07 << 2 | (Row + $78) & $30 >> 4) | (Row + $78) & $08 << 4 | (Row + $78) & $C0 >> 1 | (Row + $78) & $C0 >> 3, x .endrepeat dex bmi p2p2 jmp p2l1 p2p2: ldx #39 p2l2: .repeat $78, Row sta $100 * (>HGRPage2 + Row & $07 << 2 | Row & $30 >> 4) | Row & $08 << 4 | Row & $C0 >> 1 | Row & $C0 >> 3, x .endrepeat dex bmi done1 jmp p2l2 done1: rts .endproc ;----------------------------------------------------------------------------- .proc screenDrawLevelName clc lda #20 ; screen is 20 "characters" wide adc leftEdge ; but maybe is offset from the left asl ; mult by 2 for columns tay ; put into y dey ; but 0 based so 0-39 or whatever (not 1-40) ldx #39 ; screen index is 0-39 lda currPageH cmp #$20 bne page2 : lda levelNameGfx0 + $0000, y ; load the cache sta $2050, x ; store to the screen (the hi byte is updated) lda levelNameGfx0 + $0040, y ; but the lo byte ($50) is correct for row 16 (0 based) sta $2450, x lda levelNameGfx0 + $0080, y ; the cache is linear so each line is $40 from previous sta $2850, x lda levelNameGfx0 + $00C0, y sta $2C50, x lda levelNameGfx0 + $0100, y sta $3050, x lda levelNameGfx0 + $0140, y sta $3450, x lda levelNameGfx0 + $0180, y sta $3850, x lda levelNameGfx0 + $01C0, y sta $3C50, x dey ; previous cache byte dex ; previous screen byte bpl :- ; keep going till all 40 columns copied rts page2: lda levelNameGfx0 + $0000, y ; load the cache sta $4050, x ; store to the screen (the hi byte is updated) lda levelNameGfx0 + $0040, y ; but the lo byte ($50) is correct for row 16 (0 based) sta $4450, x lda levelNameGfx0 + $0080, y sta $4850, x lda levelNameGfx0 + $00C0, y sta $4C50, x lda levelNameGfx0 + $0100, y sta $5050, x lda levelNameGfx0 + $0140, y sta $5450, x lda levelNameGfx0 + $0180, y sta $5850, x lda levelNameGfx0 + $01C0, y sta $5C50, x dey ; previous cache byte dex ; previous screen byte bpl page2 ; keep going till all 40 columns copied rts .endproc ;----------------------------------------------------------------------------- .proc screenDrawAirFrame lda currPageH ; see if this is to page 1 or 2 cmp #$20 bne page2 ldx #28 ; draw the long green lines that outline the bar : lda #%00101010 ; green left sta $20DA, x sta $24DA, x sta $38DA, x sta $3CDA, x lda #%01010101 ; green right sta $20DB, x sta $24DB, x sta $38DB, x sta $3CDB, x dex ; step back in 2's for left/right dex bpl :- ldx #31 ; draw the even longer white bar that counts down lda #%01111111 ; white left & right : sta $28D7, x sta $2CD7, x sta $30D7, x sta $34D7, x dex ; in 1's since left/right is the same $7f bpl :- rts page2: ; the same as page 1, just diff buffer ldx #28 : lda #%0101010 sta $40DA, x sta $44DA, x sta $58DA, x sta $5CDA, x lda #%01010101 sta $40DB, x sta $44DB, x sta $58DB, x sta $5CDB, x dex dex bpl :- ldx #31 lda #%01111111 ; white left & right : sta $48D7, x sta $4CD7, x sta $50D7, x sta $54D7, x dex bpl :- rts .endproc ;----------------------------------------------------------------------------- .proc screenDrawAirRemaining lda airTipGfx ; the "graphic" to use ldx airCols ; where to draw the tip of the graph cpx #3 ; last few cols are against orange bpl :+ ora #$80 ; in the last few cols so make green orange : ldy currPageH ; which page cpy #$20 bne page2 sta $28D7, x ; write the tip into page 1 sta $2CD7, x sta $30D7, x sta $34D7, x rts page2: sta $48D7, x ; write the tip into page 2 sta $4CD7, x sta $50D7, x sta $54D7, x rts .endproc ;----------------------------------------------------------------------------- .proc screenDrawLevel tileReadL = srcPtrL ; tile to draw's data tileReadH = srcPtrH scrnPtrL = dstPtrL ; location in screen buffer to draw to scrnPtrH = dstPtrH rows = sizeL ; rows to draw (15 -> 0) cols = sizeH ; cols to draw (20 + leftEdge -> leftEdge) willyYRowEnd = tmpBot + 0 ; row where willy overlaps ends (+1) willyXPosStart = tmpBot + 1 ; col where willy overlaps start willyXPosEnd = tmpBot + 2 ; row where willy overlaps ends (+1) rowStartL = tmpBot + 3 ; current screen draw row start rowStartH = tmpBot + 4 height = tmpBot + 5 ; normally 8 except for collapsing platforms lda #0 sta tilesRendered lda willyYPos ; see if willy is across 2 or 3 rows and #7 beq :+ lda #3 bne :++ : lda #2 : clc adc willyYRow ; and set up the end row (+1) sta willyYRowEnd lda willyXPos ; calculate the cols that willy overlaps sec sbc leftEdge sta willyXPosStart clc adc #2 sta willyXPosEnd clc ; in demo-mode, the prev didn't make sense and carry is set lda leftEdge ; col where to start drawing (right, bottom) adc #<(levelLayout + (PLAY_ROWS - 1) * PLAY_COLS) sta colLoop + 1 lda #>(levelLayout + (PLAY_ROWS - 1) * PLAY_COLS) sta colLoop + 2 ; levelLayout aligned so carry always clear lda #PLAY_ROWS - 1 ; zero based rows to process sta rows ; draw rows bottom up rowLoop: clc ldy rows ; rows are 8 lines high lda mult8, y tay lda rowL, y ; put the row in screen space sta rowStartL ; and save in a temp place lda rowH, y adc currPageH sta rowStartH ldx #VISIBLE_COLS - 1 ; draw 20 columns (0 based) colLoop: lda PLACEHOLDER, x ; get the tile from the unpacked level bne :+ ; if non-zero process that tile jmp prevCol ; 0 (space) - skip to next tile to process : cmp #DATA_COLLAPSE ; see if the tile is a collapsing tile bcc notCollapse ; the $70 could be $71->$77 for the collapsed state cmp #DATA_KEY bcc isCollapse ; if it's less than $80 it is a collapsing platform beq notCollapseClc ; is it a key cmp #DATA_DOOR ; if not a key, is it a door bcc setupSwitch ; if < door then it's a switch jmp prevCol ; door tile isn't a rendered tile, only a collision tile isCollapse: and #7 ; get the level of collapse tay lda collapseHeight, y ; see how many lines remain to draw sta height lda mult1024H, y ; and move the screen pointer down as well sta scrnPtrH lda #DATA_COLLAPSE ; and set up to draw the tile from the top (for fewer lines) bne setupTile ; BRA setupSwitch: ldy #0 ; this code duplicated from notCollapse since setupTile is skipped sty scrnPtrH ; not a collapsing tile so set the screen ptr to top ldy #8 ; and set the height to 8 lines to draw sty height and #1 ; is this an on switch? beq :+ ; no lda #16 ; yes - the image is 16 bytes further (tile36) : adc #tile35 ; and set the hi byte too adc #0 ; turns out tile36 is in a different block (if I remove these 2 bytes ;) sta tileReadH ; points at the tile data bne :+ ; skip the portion where the tile pointer is setup notCollapseClc: clc notCollapse: ldy #0 ; not a collapsing tile so set the screen ptr to top sty scrnPtrH ldy #8 ; and set the height to 8 lines to draw sty height setupTile: adc #<(tilesInstances - TILE_BYTES) ; Index 1 has it's bytes at + 0, so move start 1 tile back sta tileReadL lda #>(tilesInstances - TILE_BYTES) + 1 ; carry always set, so + 1 sta tileReadH ; points at the tile data : inc tilesRendered txa ; get the col asl ; * 2 for 2-byte wide tiles and clears carry adc rowStartL ; add to screen row start to get tile screen dest sta scrnPtrL lda rowStartH adc scrnPtrH ; add the hi offset for collapsed tiles sta scrnPtrH intersect: lda rows ; see if willy intersects this row cmp willyYRow bcc noOverlap ; is willy below this row? cmp willyYRowEnd bcs noOverlapClc ; is willy's end above this row txa ; rows intersect, now check the cols cmp willyXPosStart bcc noOverlap ; is col < willyStartCol cmp willyXPosEnd bcs noOverlapClc ; is col > willyEndCol (>= willyEnd + 1) overlap: ; willy will be "under" these tiles, so "or" in the ldy #0 ; tile to the screen. Since willy's bits are 11, willy lda (tileReadL), y ; will "appear" as though he's in front of the tile ora (scrnPtrL), y ; see noOverlap for code explanation sta (scrnPtrL), y ldy #1 lda (tileReadL), y ora (scrnPtrL), y sta (scrnPtrL), y lda tileReadL adc #2 sta tileReadL lda scrnPtrH adc #$04 sta scrnPtrH dec height bne overlap beq prevCol noOverlapClc: clc noOverlap: ; tile's don't intercept with willy so just write to screen ldy #0 ; start with a byte at 0 offset (left) lda (tileReadL), y ; read tile byte sta (scrnPtrL), y ; write to screen ldy #1 ; do the same for the right byte at offset 1 lda (tileReadL), y sta (scrnPtrL), y lda tileReadL ; advance the tile ptr by 2 for the 2 just written adc #2 sta tileReadL lda scrnPtrH ; advance the screen ptr to next row ($400) adc #$04 sta scrnPtrH dec height ; done one more row bne noOverlap ; keep going till all rows done prevCol: dex ; go to col to the left bmi :+ ; reached the end of the row? jmp colLoop ; not yet end of row so go do this col : dec rows ; done another row bmi done ; done all rows? lda colLoop + 1 ; not yet - setp up a row in the unpacked tile area sec sbc #32 sta colLoop + 1 bcs :+ dec colLoop + 2 : jmp rowLoop ; go do the next row's worth of tiles done: lda currLevel cmp #LEVEL_Solar_Power_Generator ; Is this the solar room bne :+ ; if not, move on jmp screenSolarBeam ; solar room needs a solar beam : rts .endproc ;----------------------------------------------------------------------------- .proc screenDrawSprites count = sizeL ldx numSprites ; render the AI dex ; ignore the door stx count : jsr screenDrawSprite dec count ldx count bpl :- rts .endproc ;----------------------------------------------------------------------------- .proc screenDrawSprite col = sizeH lines = tmpBot + 0 lda #0 sta read + 1 ; in case of left clipping, reset read lda #3 sta stripLen + 1 ; in case of right clipping, reset length lda spriteXPos, x ; col in 0..31 range sec sbc leftEdge ; move by the left edge bpl leftOK ; if ge 0 then no clipping left cmp #$ff beq lClip drawOffScreen: sta lines lda spriteClass, x bit CLASS_MOVE_Y beq :+ ; if yes, ignore jmp screenDrawOffscreenSprite ; an off-screen amoeba gets an indicator : rts lClip: lda #2 sta read + 1 lda #1 sta stripLen + 1 lda #0 bne rightOK ; BRA leftOK: cmp #19 ; clip on the right bcc rightOK beq rClip bne drawOffScreen ; BRA rClip: lda #1 sta stripLen + 1 lda #19 rightOK: asl ; multiply times 2 sta col ; screen column for where to start draw lda spriteFrame, x adc spriteFramesIdx, x tay lda mult64H, y ; and add the 64 * frame hi adc #>spriteInstances ; add the hi offset sta read + 2 lda mult64L, y adc #sprites sta srcPtrH ; save as srcPtr txa ; how many lives (0 based) into a for column offset calc oloop: asl ; mult * 4 to set them apart asl sta sizeH ; store the column in sizeH lda #16 ; draw 16 lines sta dstPtrL ; store the lines in dstPrkL ldy #22 * 8 ; Draw the lives in row 19 (0 based) lda srcPtrL ; copy the srcPtr to where to read from sta read + 1 lda srcPtrH sta read + 2 loop: lda sizeH ; start with the column adc rowL, y ; add the row start sta write + 1 lda rowH, y adc currPageH ; and the page sta write + 2 ; "write" points at the line to copy 4 bytes to ldx #3 ; copy offsets 0..3 read: lda PLACEHOLDER, x ; get the willy bytes write: sta PLACEHOLDER, x ; put them on screen dex bpl read ; do all the bytes dec dstPtrL ; one more line done beq next ; all lines done, see if more lives to draw lda read + 1 ; not all done, advance read by 4 bytes adc #4 sta read + 1 bcc :+ inc read + 2 clc : iny ; and go to next line bne loop ; BRA next: dec sizeL ; dec the count of lives to draw lda sizeL ; get it in a to calc the column bpl oloop ; and go do that if ge 0 lda allDone ; sentinel to stop looping when cheat active beq maybeCheat ; if not set then check if cheat is active done: rts ; exit maybeCheat: lda cheatActive ; see if cheat mode is on beq done ; if not then done sta allDone ; set sentinel ldx lives ; get the column to draw the boot txa asl ; mult * 4 to set them apart asl sta sizeH ; store the column in sizeH lda #16 ; draw 16 lines sta dstPtrL ; store the lines in dstPrkL ldy #22 * 8 ; Draw the lives in row 19 (0 based) lda #sprite19dw sta read + 2 jmp loop .endproc ;----------------------------------------------------------------------------- .proc screenDrawWilly count = sizeL col = sizeH lines = tmpBot + 0 willyByte = tmpBot + 1 collision = tmpBot + 2 lda #0 sta collision lda willyXPos ; col in 0..31 range sec sbc leftEdge ; move by the left edge asl ; multiply times 2 sta col ; screen column for where to start draw lda willyFrame ; get the frame tay ; index through y lda mult64L, y ; get the frame * 64 lo adc #sprite00 ; add the hi offset adc mult64H, y ; and add the 64 * frame hi sta willyRead + 2 ; willyRead now a pointer at the frame to show lda willyYPos ; get the y position of the sprite tay lda #16 ; sprites are 16 high sta lines loop: lda col ; start with the column adc rowL, y ; add the row start sta screenRead + 1 sta write + 1 lda rowH, y adc currPageH ; and the page sta screenRead + 2 ; write points at the line to copy 4 bytes to sta write + 2 ; write points at the line to copy 4 bytes to stripLen: ldx #3 ; copy offsets 0..3 willyRead: lda PLACEHOLDER, x ; get the bytes from src instance sta willyByte screenRead: lda PLACEHOLDER, x eor willyByte write: sta PLACEHOLDER, x ; put them on screen and willyByte cmp willyByte beq :+ inc collision : dex bpl willyRead ; do all the bytes dec lines ; one more line done beq done clc lda willyRead + 1 ; not all done, advance read by 4 bytes adc #4 sta willyRead + 1 bcc :+ inc willyRead + 2 clc : iny ; and go to next line bne loop ; BRA done: lda collision beq :+ sec rts : clc rts .endproc ;----------------------------------------------------------------------------- ; Mask the front screen with the color index passed in at tmpBot + 0 (colIdx) .proc screenInvertVisibleScreen colIdx = tmpBot + 0 rows = tmpBot + 1 hiresRows = tmpBot + 2 colL = tmpBot + 3 colR = tmpBot + 4 jsr screenSwap::valueSwap ldx colIdx ; load the color mask based on the index lda masksLeft, x sta colL lda masksRight, x sta colR lda #PLAY_ROWS ; init a counter for how many rows to do sta rows lda #0 ; init the index to the starting hires row for the same "text" row sta hiresRows clc rowLoop: ldy hiresRows ; init the pointer to the top line of the hires row lda rowL, y sta dstPtrL lda currPageH adc rowH, y sta dstPtrH ldy #2 * (VISIBLE_COLS - 1) ; start at column 38 colLoop: ldx #8 ; 8 hires rows per text row : lda (dstPtrL), y ; get the left col/byte on screen and #%01111111 ; ignore the msb eor colL ; eor with the color sta (dstPtrL), y ; write it back iny ; go to the right col lda (dstPtrL), y and #%01111111 eor colR sta (dstPtrL), y dey ; back to left dex ; one more hires row done beq :+ ; all done? lda dstPtrH ; not yet adc #4 ; move hires row down by 1 sta dstPtrH bne :- ; BRA to do all 8 hires rows : lda dstPtrH ; move the hires ptr back to the top row sec ; for the text row being processed sbc #7*4 sta dstPtrH clc ; clear carry from last set next: dey ; move to the prev col right dey ; and move to the prev col left bpl colLoop ; and repeat for all columns ge 0 dec rows ; done a row beq done ; stop when all rows done lda hiresRows ; more to do so move the hires clc ; starting row down by 8 to be at the adc #8 ; start of the next "text" row sta hiresRows jmp rowLoop ; go back and do this row now done: jsr screenSwap::valueSwap ; put the screen buffer info back rts ; all done .endproc ;----------------------------------------------------------------------------- .proc screenSolarBeam worldPtrL = srcPtrL worldPtrH = srcPtrH screenPtrL = dstPtrL screenPtrH = dstPtrH column = sizeL row = sizeH screenCol = tmpBot + 0 direction = tmpBot + 1 collision = tmpBot + 2 isWilly = tmpBot + 3 lda #0 ; init some local variables to 0 sta row sta direction sta collision sta isWilly lda #23 ; column where solar beam starts sta column sta worldPtrL ; also the column in levelLayout (aligned in mem) lda #>levelLayout ; make worldPtr point at the start of the beam sta worldPtrH while: lda column ; start with the column sec sbc leftEdge ; and see if it's on screen bmi beamLeft ; if c - le <= 0 then c is left of screen and done sta screenCol ; save this cmp #20 ; if c - le >= 20 then c is to right of screen bcc onScreen ; c - l1 >= 0 and < 20 so visible offScreen: ldy #0 ; beam is to the right so may become visible lda (worldPtrL), y ; see if beam hits a world tile beq :+ ; if not, process beam beamLeft: jmp done : ldx numSprites ; need to see if beam hits a sprite (not door) to bend checkLoop: dex bmi checkCollision ; checked all sprites lda spriteXPos, x ; get the sprite x cmp column ; compare to the beam x beq matchX ; if it matches it's a hit bcs checkLoop ; sprite is to the right of the beam adc #1 ; right col of the sprite cmp column ; is it in the beam? bne checkLoop ; if not this sprite is out matchX: lda spriteYPos, x ; see if y also matches cmp row ; test against beam height beq matchy bcs checkLoop ; sprite top is lower than beam adc #16 ; prep to test bottom of sprite cmp row ; against the beam bcc checkLoop ; sprite bottom is above beam matchy: inc collision ; set the collision bne checkCollision ; and exit the check since sprites don't overlap onScreen: lda willyYPos ; see if the beam intersects willy adc #16 ; if willyY + 16 >= row and willyY <= row + 8 cmp row ; then willy intersects in Y bcc drawBlock clc lda row adc #8 cmp willyYPos bcc drawBlock lda column ; if col >= willyX and col < willyX + 3 cmp willyXPos ; then willy intersects in X also, so hitting willy bcc drawBlock lda willyXPos clc adc #2 cmp column bcc drawBlock sta isWilly ; set isWIlly to true when hitting willy clc drawBlock: ldy row ; set up to draw lda screenCol ; on screen asl ; which is 2 bytes per column adc rowL, y sta screenPtrL lda rowH, y adc currPageH sta screenPtrH ldx #8 ; beam block is 8 rows high ldy #0 ; y 0 and 1 for left and right blockLoop: lda (screenPtrL), y ; get what's on screen (sprite) beq nocol ; if nothing just overwrite inc collision ; hit a sprite nocol: ora #%10101010 ; orange left sta (screenPtrL), y ; write to screen iny ; go to right column lda (screenPtrL), y ; repeat for right col beq :+ inc collision : ora #%11010101 sta (screenPtrL), y dey ; set y back to 0 dex ; done one more row beq checkCollision ; if all rows done, then done with plot lda screenPtrH ; move down a row by adding $0400 adc #$04 sta screenPtrH bne blockLoop ; BRA checkCollision: lda isWilly ; was willy hit beq chkDirChng ; if not, normal collision checks lda #2 ; don't set airflow to less than 2 so both cmp airFlow ; buffers get to draw bcs :+ sta airFlow : lda #0 ; willy was hit so reset locals sta isWilly beq setCollision ; go move the beam chkDirChng: lda collision ; see if there was a collision beq moveBeam ; if there wasn't move the beam lda direction ; there was a sprite collision eor #1 ; so change the direction of the beam sta direction lda #0 setCollision: sta collision ; reset the collision counter moveBeam: lda direction ; get the beam direction beq down ; 0 is down and 1 is left left: dec column ; move a world column left lda worldPtrL ; and move the pointer as well bne :+ dec worldPtrH : dec worldPtrL jmp checkWorld ; see that the new location isn't a world tile down: clc lda row ; move the row adc #8 ; down by 8 (1 block height) sta row lda worldPtrL ; and move the world pointer as well adc #32 sta worldPtrL bcc checkWorld inc worldPtrH checkWorld: lda (worldPtrL), y ; load a world location bne done ; if occupied, beam dies jmp while ; keep going till beam dies done: rts .endproc