Skip to navigation

The Sentinel D source

Name: CheckForTileCentre [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Calculate max(|xCoordLo - 128|, |zCoordLo - 128|)
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTileAltitude calls CheckForTileCentre

Arguments: xCoordLo The low byte of an x-coordinate for comparing to the centre of a tile (i.e. a fractional part) zCoordLo The low byte of an z-coordinate for comparing to the centre of a tile (i.e. a fractional part)
.CheckForTileCentre \ We check the distance of the fractional coordinate in \ (xCoordLo, zCoordLo) from the centre of the tile by \ subtracting from (128, 128), as 128 represents 0.5 in \ fractional terms LDA xCoordLo \ Set A = |xCoordLo - 128| SEC SBC #128 BPL maxc1 EOR #%11111111 .maxc1 STA T \ Set T = |xCoordLo - 128| LDA zCoordLo \ Set A = |zCoordLo - 128| SEC SBC #128 BPL maxc2 EOR #%11111111 .maxc2 CMP T \ If A >= T then A is already set to the higher value BCS maxc3 \ out of A and T, so jump to maxc3 LDA T \ If we get here then A < T, so set A to the value of T \ so A is now set the higher value out of A and T .maxc3 STA T \ A is set to the higher value of A and T, so this does \ the following: \ \ T = max(A, T) \ \ = max(|xCoordLo - 128|, |zCoordLo - 128|) RTS \ Return from the subroutine
Name: GetObjectCoords [Show more] Type: Subroutine Category: 3D objects Summary: Get an object's coordinates
Context: See this subroutine on its own page References: This subroutine is called as follows: * FollowGazeVector (Part 1 of 5) calls GetObjectCoords * GetRowVisibility (Part 1 of 2) calls GetObjectCoords

This routine fetches the cartesian coordinates of object #X as three 24-bit numbers, as follows: xCoord(Hi Lo Bot) yCoord(Hi Lo Bot) zCoord(Hi Lo Bot) The high byte can be thought of as the integer with low and bottom bytes as the fractional part. Tile corners are integers in the x- and z-axis, and objects are placed in the centre of tiles by setting the fractional part to 0.5. So an object that's been placed directly on the tile that's anchored at (2, y, 3) will appear at (2.5, y, 3.5). The y-coordinate - i.e. the altitude of the object above the tile itself - depends on the object type and whether it is stacked on top of another object.
Arguments: X An object number
.GetObjectCoords LDA #0 \ Set the bottom byte of each coordinate to zero STA xCoordBot STA yCoordBot STA zCoordBot LDA #128 \ Set the low byte of the x- and -z-coordinates to 128 STA xCoordLo \ (which represents 0.5, so the object sits in the STA zCoordLo \ middle of the tile on which it is placed) LDA yObjectLo,X \ Set the low byte of the y-coordinate to the altitude STA yCoordLo \ of object #X LDA xObject,X \ Set the high byte of the x-coordinate to the value for STA xCoordHi \ object #X from the xObject table LDA yObjectHi,X \ Set the high byte of the y-coordinate to the value for STA yCoordHi \ object #X from the yObjectHi table LDA zObject,X \ Set the high byte of the z-coordinate to the value for STA zCoordHi \ object #X from the zObject table RTS \ Return from the subroutine
Name: DeleteObject [Show more] Type: Subroutine Category: 3D objects Summary: Delete an object, removing it from the landscape and vacating its object number
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrainObjectEnergy calls DeleteObject * ExpendEnemyEnergy calls DeleteObject * PerformHyperspace calls DeleteObject * ProcessActionKeys (Part 2 of 2) calls DeleteObject

Arguments: X The number of the object to delete
Returns: X X is preserved
.DeleteObject LDA xObject,X \ Set (xTile, zTile) to the tile coordinates of the STA xTile \ tile containing object #X LDA zObject,X STA zTile JSR GetTileData \ Set A to the tile data for the tile anchored at \ (xTile, zTile), which we ignore, but this also sets \ the tile page in tileDataPage and the index in Y, so \ tileDataPage+Y now points to the tile data entry in \ the tileData table for object #X LDA objectFlags,X \ Set A to the object flags for object #X CMP #%01000000 \ If both bits 6 and 7 of the object flags for object #X BCC delo1 \ are clear then object #X is not stacked on top of \ another object, so jump to delo1 to remove object #X \ from the tile itself \ If we get here then object #X is stacked on top of \ another object, and the number of that object is in \ bits 0 to 5 of the object flags for object #X, which \ is currently in A ORA #%11000000 \ Set bits of 6 and 7 of A to create a byte with the \ number of the object below object #X in bits 0 to 5, \ and bits 6 and 7 set \ \ We now set this as the updated tile data for the tile \ that used to contain object #X at the top of the \ stack, but which now contains the next object down on \ the top of the stack instead BNE delo2 \ Jump to delo2 to store A as the new tile data for this \ tile (this BNE is effectively a JMP as the value of A \ before the ORA has to have at least one of bits 6 and \ 7 set, so the result of the ORA is never zero) .delo1 \ If we get here then object #X is not stacked on top of \ another object, so we can remove the object from the \ tile by changing the tile data into the following \ format: \ \ * The low nibble contains the tile shape, which in \ this case is flat because only flat tiles can \ contain objects, so we need to set the low nibble \ to shape 0 (to indicate a flat tile) \ \ * The high nibble contains the altitude of the tile \ corner in the front-left corner of the tile \ \ The altitude of object #X is a 16-bit value in \ yObject(Hi Lo), where the yObjectLo part is \ effectively a fractional part of the altitude that \ describes how far the object is above the tile itself \ \ Tile altitudes are whole numbers, so the altitude of \ the tile on which object #X is placed is given in the \ high byte of the object's 16-bit altitude LDA yObjectHi,X \ Set the high nibble of A to the high byte of the ASL A \ altitude of object #X (which is the tile's altitude) ASL A \ and set the low nibble to zero to indicate a flat tile ASL A \ ASL A \ A is now in the format required for the tileData table \ for a flat tile that doesn't contain an object, so we \ can update the tile's data to remove object #X from \ the tile .delo2 STA (tileDataPage),Y \ Update the tile data for object #X to the value in A \ (as we set Y to the relevant index with the call to \ GetTileData above) LDA #%10000000 \ Set bit 7 of the object flags for object #X to denote STA objectFlags,X \ that object number X has not been allocated to an \ object and is reusable, so this effectively deletes \ the object RTS \ Return from the subroutine
Name: PlaceObjectOnTile [Show more] Type: Subroutine Category: 3D objects Summary: Place an object on a tile, putting it on top of any existing boulders or towers
Context: See this subroutine on its own page References: This subroutine is called as follows: * AddEnemiesToTiles calls PlaceObjectOnTile * PlaceObjectBelow calls PlaceObjectOnTile * ProcessActionKeys (Part 2 of 2) calls PlaceObjectOnTile * SpawnPlayer calls PlaceObjectOnTile

This routine sets the following (if successful): * X-th entry in (xObject, yObject, zObject) is set to the 3D coordinate of the newly added object, where the y-coordinate is yObject(Hi Lo) * X-th entry in objectFlags, bit 7 is clear to indicate that object #X has been allocated to an object * X-th entry in objectFlags, bit 6 is set if we add the object on top of a boulder or tower (and the object number of the boulder/tower is in bits 0-5) * X-th entry in yObjectLo = 224, so objects are spawned at a height of 224 above the tile itself, or 0.875 tile widths (this is used for the gaze vector calculations, so this is effectively the height of the object's eyes above the tile) * X-th entry in objectPitchAngle = -11, so objects look slightly downwards at a pitch angle of 15.5 degrees * X-th entry in objectYawAngle is set to a multiple of 11.25 degrees, as determined by the next seed * tileData for the tile is set to the object number in X in bits 0 to 5, and bits 6 and 7 are set to indicate that the tile contains an object
Arguments: X The number of the object to add to the tile (xTile, zTile) The tile coordinate where we place the object
Returns: C flag Status flag: * Clear if we successfully added the object to the tile * Set if we failed to add the object to the tile (if the tile is occupied by an object that is not a a boulder or tower, for example)
.PlaceObjectOnTile LDA xTile \ Set the 3D coordinate for object #X to (xTile, zTile) STA xObject,X \ by updating the X-th entries in the xObject and LDA zTile \ zObject tables STA zObject,X \ \ So this sets the x- and z-coordinates of the 3D \ coordinate for our object; we set the y-coordinate \ later JSR GetTileData \ Set A to the tile data for the tile anchored at \ (xTile, zTile), setting the C flag if the tile \ contains an object \ \ This also sets the tile page in tileDataPage and the \ tile number in Y, so tileDataPage+Y now points to the \ tile data entry in the tileData table BCC objt4 \ If the C flag is clear then this tile does not already \ have an object placed on it, so jump to objt4 to place \ object #X on the tile STY tileNumber \ Store the tile number in tileNumber so we can refer to \ it later AND #%00111111 \ Because the tile has an object on it, the tile data TAY \ contains the number of the top object on the tile in \ bits 0 to 5, so extract the object number into Y (so \ the tile effectively contains object #Y) LDA objectTypes,Y \ Set A to the type of object that's already on the tile \ (i.e. the type of object #Y) CMP #3 \ If the tile contains a boulder (an object of type 3), BEQ objt1 \ jump to objt1 to put the new object on top of the \ boulder CMP #6 \ If the tile doesn't contain the Sentinel's tower (type BNE objt6 \ 6) then it must contain an object on which we can't \ place our new object, so jump to objt6 to return from \ the subroutine without adding the object to the tile .objt1 \ If we get here then the object already on the tile is \ either a boulder (type 3) or the Sentinel's tower \ (type 6) \ \ In either case, we want to place our object on top of \ the object that is already there, which we can do by \ setting bit 6 of the object flags for the new object \ and putting the number of the existing object into \ bits 0 to 5 TYA \ Set the object flags for the object that we are adding ORA #%01000000 \ (i.e. object #X) so that bit 6 is set, and bits 0 to 5 STA objectFlags,X \ contain the number of the object that is already on \ the tile \ \ This denotes that our new object, object #X, is on top \ of object #Y LDA objectTypes,Y \ If the object that's already on the tile is not the CMP #6 \ Sentinel's tower (type 6), jump to objt2 BNE objt2 \ If we get here then we are placing object #X on top of \ the Sentinel's tower (type 6) in object #Y \ \ The next task is to calculate the altitude of the \ object when it is placed on top of the tower (i.e. the \ y-coordinate of the object, as the y-axis goes up and \ down in our 3D world) \ \ The tower is defined with a height of one coordinate \ (where a tile-sized cube is one coordinate across) \ \ Object y-coordinates are stored as 16-bit numbers in \ the form yObject(Hi Lo), with the low byte effectively \ acting like a fractional part, so to work out the \ y-coordinate for the object we are placing on top of \ the boulder, we need to add (1 0) to the tower's \ current y-coordinate, like this: \ \ yObject,X = yObject,Y + (1 0) \ \ We need to do this calculation for both bytes, \ starting with the low byte LDA yObjectLo,Y \ First we add the low bytes, by adding 0 to the Y-th STA yObjectLo,X \ entry in yObjectLo and storing this in the low byte of \ the X-th entry in yObjectLo (which we can do by simply \ copying the Y-th entry into the X-th entry) CLC \ Clear the C flag and set A = 1 so the addition at LDA #1 \ objt3 will do the following: \ \ A = yObjectHi,Y + 1 \ \ This will add the high bytes of the calculation to \ give the result we want: \ \ (A yObjectLo,X) = (yObjectHi,Y yObjectLo,Y) + (1 0) \ \ = ((yObjectHi,Y + 1) yObjectLo,Y) \ \ with the subsequent jump to objt5 storing A in \ yObjectHi,X as required BNE objt3 \ Jump to objt3 to do the calculation (this BNE is \ effectively a JMP as A is never zero) .objt2 \ If we get here then we are placing our new object #X \ on top of the boulder (type 3) in object #Y \ \ The next task is to calculate the altitude of the \ object when it is placed on top of the boulder (i.e. \ the y-coordinate of the object, as the y-axis goes up \ and down in our 3D world) \ \ Boulders are defined with a height of 0.5 coordinates \ (where a tile-sized cube is one coordinate across) \ \ Object y-coordinates are stored as 16-bit numbers in \ the form yObject(Hi Lo), with the low byte effectively \ acting like a fractional part, so to work out the \ y-coordinate for the object we are placing on top of \ the boulder, we need to add (0 128) to the boulder's \ current y-coordinate, like this: \ \ yObject,X = yObject,Y + (0 128) \ \ We need to do this calculation for both bytes, \ starting with the low byte LDA yObjectLo,Y \ First we add the low bytes, by adding 128 to the Y-th CLC \ entry in yObjectLo and storing this in the low byte of ADC #128 \ the result in the X-th entry in yObjectLo STA yObjectLo,X LDA #0 \ Set A = 0 so the following addition will add the high \ bytes to give the result we want: \ \ (A yObjectLo,X) = (yObjectHi,Y yObjectLo,Y) \ + (0 128) \ \ = (yObjectHi,Y (yObjectLo,Y + 128)) \ \ with the subsequent jump to objt5 storing A in \ yObjectHi,X as required .objt3 ADC yObjectHi,Y \ Add A to the high byte of the y-coordinate of the \ object beneath the one we are adding and store the \ result in A \ \ So (A yObjectLo,X) now contains the y-coordinate of \ the new object that we are placing on top of the \ boulder or tower LDY tileNumber \ Set Y to the tile number where we are adding the \ object, which we stored above JMP objt5 \ Jump to objt5 to store A as the y-coordinate of the \ new object on the tile, and update the various other \ object and tile tables for the new object .objt4 PHA \ Store the tile data for the tile on the stack so we \ can retrieve it below LDA #0 \ Clear bit 7 of the object's flags to indicate that STA objectFlags,X \ object #X is allocated to an object LDA #224 \ Set the object's entry in yObjectLo to 224 STA yObjectLo,X \ \ This places objects well above the tile, at a height \ of 224 / 256 = 0.875 coordinates above the tile itself \ \ This is used for the gaze vector calculations, so this \ is effectively the height of the object's eyes above \ the tile PLA \ Set A to the tile data for the tile, which we stored \ on the stack above LSR A \ The top nibble of the tile data contains the tile LSR A \ altitude, so this sets A to the tile altitude LSR A LSR A \ We now fall through into objt5 to set the tile \ y-coordinate for object #X to the tile altitude in A .objt5 STA yObjectHi,X \ Set the high byte of the 3D y-coordinate for object #X \ to the value of A by updating the X-th entry in the \ yObjectHi table \ \ We now have a full 3D coordinate for the object in \ (xObject, yObject, zObject), where yObject is stored \ as a 16-bit number in yObject(Hi Lo) TXA \ Set the tile data for this tile to object number X, ORA #%11000000 \ with bits 6 and 7 set to indicate that the tile now STA (tileDataPage),Y \ contains an object LDA #&F5 \ Set the object's pitch angle to -11, so the object STA objectPitchAngle,X \ looks down slightly at a pitch angle of 15.5 degrees \ (as 360 * 11 / 256 = 15.5) \ We now calculate the object's yaw angle, which \ determines the direction in which it is facing \ \ The degree system in the Sentinel looks like this: \ \ 0 \ -32 | +32 Overhead view of object \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ In this context, looking straight ahead means the \ object is looking into the screen, towards the back of \ the landscape JSR GetNextSeedNumber \ Set A to the next number from the landscape's sequence \ of seed numbers AND #%11111000 \ Convert A to be a multiple of 8 and in the range 0 to \ 248 (i.e. 0 to 31 * 8) \ \ This rotates the object so it is looking along one of \ 32 fixed rotations, each of which is a multiple of \ 11.25 degrees CLC \ Set A = A + 96 ADC #96 \ \ This doesn't change the fact that A is a multiple of \ 11.25 degrees, so it's presumably intended to make the \ player's rotation work well on the starting level STA objectYawAngle,X \ Set the object's objectYawAngle to the angle we just \ calculated in A CLC \ Clear the C flag to indicate that we have successfully \ added the object to the tile RTS \ Return from the subroutine .objt6 \ If we get here then the tile already contains an \ object and that object is not a boulder (type 3) or \ tower (type 6), so we can't add the new object to the \ tile SEC \ Set the C flag to indicate that we have failed to add \ the object to the tile RTS \ Return from the subroutine
Name: tileNumber [Show more] Type: Variable Category: 3D objects Summary: The tile number to which we are adding an object in the PlaceObjectOnTile routine
Context: See this variable on its own page References: This variable is used as follows: * PlaceObjectOnTile uses tileNumber
.tileNumber EQUB 0
Name: DrawUpdatedObject [Show more] Type: Subroutine Category: Drawing objects Summary: Draw an updated object on-screen, optionally with a dithered effect, and with or without the surrounding landscape
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTactics (Part 8 of 8) calls DrawUpdatedObject * ProcessGameplay calls DrawUpdatedObject * ShowGameOverScreen calls DrawUpdatedObject

Arguments: currentObject The number of the object to update on-screen viewingObject The number of the object that's viewing the object that we are updating drawLandscape Configure whether to draw the landscape behind the object: * Bit 7 clear = draw the landscape behind the object as well as the object * Bit 7 set = just draw the object, and configure the interrupt handler to draw random black dots on the screen if the Sentinel has won the game ditherObjectSights Configure how the update is drawn onto the screen: * Bit 6 set = dither the updated object onto the screen * Bit 7 set = remove the sights from the screen before drawing the object screenBackground The screen background to use when drawing the object: * 0 = fill with alternating colour 0/1 (blue/black) pixel rows, for the sky during gameplay * 3 = fill with solid colour 1 (black) and draw 240 randomly positioned stars on the background, for the game over screen
.updo1 LDA #%00000000 \ Clear bits 6 and 7 of ditherObjectSights so the next STA ditherObjectSights \ call to DrawUpdatedObject will, by default, draw the \ object straight onto the screen without a dithered \ effect, and will not remove the sights first STA drawLandscape \ Clear bit 7 of drawLandscape so the next call to \ DrawUpdatedObject will, by default, draw the landscape \ behind the object STA doNotDitherObject \ Clear bit 7 of doNotDitherObject to enable objects to \ be updated on the screen with a dithered effect RTS \ Return from the subroutine .DrawUpdatedObject JSR GetObjVisibility \ Calculate whether any part of the current object is \ visible on-screen and set the C flag as follows: \ \ * Clear = at least some of the object is visible \ on-screen \ \ * Set = the object is not visible on-screen \ \ If the object is visible, also set the following: \ \ * bufferColumns = the number of character columns \ in the screen buffer that the object spans (capped \ to a maximum value of 20) \ \ * objYawOffset = the yaw offset of the left edge of \ the object from the left edge of the screen, in \ character columns BCS updo1 \ If the C flag is set then the object is not visible \ on-screen, so jump to updo1 to reset the various \ dither-related variables and return from the \ subroutine as there is nothing to update on the screen LDA #25 \ Set ditherOuterLoop = 25, so the dithered effect in STA ditherOuterLoop \ the DitherScreenBuffer routine implements 25 outer \ loops, where each inner loop dithers 255 pixels to the \ screen LDA ditherObjectSights \ If bit 7 of ditherObjectSights is clear then we are BPL updo2 \ not configured to remove the sights before updating \ the object on-screen, so jump to updo2 to skip the \ following SEI \ Disable interrupts to ensure that the screen doesn't \ scroll while we are removing the sights, as that could \ corrupt the screen JSR RemoveSights \ Bit 7 of ditherObjectSights is set, so we need to \ remove the sights from the screen before we update the \ object on-screen, so call the RemoveSights routine CLI \ Re-enable interrupts .updo2 LDX viewingObject \ Set X to the viewing object LDA objYawOffset \ Set yawAdjustment(Hi Lo) = (objYawOffset 0) / 2 STA yawAdjustmentHi \ LDA #0 \ This converts the yaw offset in objYawOffset from LSR yawAdjustmentHi \ character columns into a yaw angle and stores the ROR A \ result in yawAdjustment(Hi Lo) STA yawAdjustmentLo \ \ The custom screen mode 5 used by the game is 40 \ character columns wide, and the screen is 20 yaw \ angles wide in terms of the game's coordinate system \ \ So we can simply halve the character column count in \ objYawOffset to convert it into a yaw angle LDA yawAdjustmentHi \ Add yawAdjustmentHi to the yaw angle for the viewing ADC objectYawAngle,X \ object, so the viewing object is now looking directly STA objectYawAngle,X \ at the left edge of the object, so when we draw the \ object, we will draw it into the screen buffer with \ the object aligned with the left edge of the buffer \ \ The addition works because the ROR instruction clears \ the C flag, as A is zero before the rotation LDY #0 \ Zero lastPanKeyPressed so that when we draw the screen STY lastPanKeyPressed \ buffer onto the screen below, we draw the buffer onto \ the screen in columns, working from left to right, in \ the same way as for a pan to the right (which is what \ a zero value of lastPanKeyPressed usually represents) LDA bufferColumns \ Set up the screen buffer so it will hold an object JSR ConfigureObjBuffer \ with the number of character columns that the object \ spans, as calculated by the call to GetObjVisibility \ above \ \ The object might be too wide to fit into the screen \ buffer (as we configure it as a column buffer to make \ sure the entire visible part of the object fits into \ the buffer height-wise), but this ensures that the \ buffer contains as much of the visible part of the \ object as possible LDA #25 \ Set A = 25 to pass to FillScreen, so we fill the \ screen buffer (as opposed to screen memory) LDY #24 \ Set Y = 24 to pass to FillScreen, so we fill 24 \ character rows of the screen buffer LDX bufferColumns \ Set X = bufferColumns to pass to FillScreen, so we \ fill bufferColumns character columns in the screen \ buffer JSR FillScreen \ Call FillScreen to fill the screen buffer with the \ background specified in screenBackground \ \ If we are updating an object as part of the gameplay, \ then screenBackground variable will have been zeroed \ in DrawTitleView before the gameplay starts, so this \ call to FillScreen will fill the buffer with \ alternating colour 0/1 (blue/black) pixel rows, for \ the sky \ \ If we are drawing an object on the game over screen \ then screenBackground will be 3 and the background \ will be a black screen with stars BIT drawLandscape \ If bit 7 of drawLandscape is clear then we need to BPL updo3 \ draw both the object and the surrounding landscape, so \ jump to updo3 to do this \ If bit 7 of drawLandscape is set then we are showing \ the game over screen, so we just need to draw the \ object on its own and without the surrounding \ landscape LDY currentObject \ Draw the current object into the screen buffer JSR DrawObject JMP updo4 \ Jump to updo4 to skip the following instruction and \ move on to updating the screen .updo3 JSR DrawLandscapeView \ Draw the object into the screen buffer, as part of the \ landscape view .updo4 \ By this point we have drawn the updated object into \ the screen buffer, so we now need to copy the results \ onto the screen LDY #0 \ Set screenBufferAddr(1 0) to the address of the left JSR SetBufferAddress \ column of the screen buffer LDX viewingObject \ Set X to the viewing object LDA #0 \ Set yawAdjustmentLo = 0 so when we draw the landscape STA yawAdjustmentLo \ again, it doesn't include the yaw adjustment for \ drawing this object SEC \ Subtract yawAdjustmentHi from the yaw angle for the LDA objectYawAngle,X \ viewing, so the reverses the addition that we did SBC yawAdjustmentHi \ above so the yaw angle of the viewing object is back STA objectYawAngle,X \ to the value it has for the normal landscape view \ We now calculate the screen address of the left edge \ of the object in the landscape view, as that's where \ we want to copy the contents of the screen buffer \ \ The landscape view is at viewScreenAddr(1 0) in screen \ memory, and the number of character columns between \ the left edge of the screen and the left edge of the \ object is in objYawOffset, so now we convert the \ character columns into screen memory bytes and add \ that to viewScreenAddr(1 0), and that gives us the \ address in screen memory of the top-left corner of the \ object's position on-screen LDA #0 \ Set (U A) = objYawOffset STA U LDA objYawOffset ASL A \ Set (U A) = (U A) * 8 ASL A \ = objYawOffset * 8 ASL A \ ROL U \ This converts the yaw offset in objYawOffset from \ character columns into pixel bytes and stores the \ result in (U A), as each character block contains \ eight bytes \ \ We don't have to ROL the first two shifts into U as \ we know objYawOffset is less than 40, so the first two \ ASLs will only ever shift zeroes into the C flag CLC \ Set (A objScreenAddr) = viewScreenAddr(1 0) + (U A) ADC viewScreenAddr STA objScreenAddr LDA viewScreenAddr+1 ADC U CMP #&80 \ If the high byte in A >= &80 then the new address is BCC updo5 \ past the end of screen memory, so subtract &20 from SBC #&20 \ the high byte so the address wraps around within the \ range of screen memory between &6000 and &8000 .updo5 STA objScreenAddr+1 \ Store the high byte of the result, so we now have: \ \ objScreenAddr(1 0) = viewScreenAddr(1 0) + (U A) \ \ with the address wrapped around as required \ \ So this gives us the address in screen memory of the \ top-left corner of the object's position on-screen BIT ditherObjectSights \ If bit 6 of ditherObjectSights is clear then we do not BVC updo7 \ draw the object on-screen with a dithered effect, so \ jump to updo7 to skip the following and move straight \ on to drawing the whole object to the screen BIT sentinelHasWon \ If bit 7 of sentinelHasWon is clear then the Sentinel BPL updo6 \ has not won the game, so skip the following to leave \ ditherOuterLoop set to 25, which is the correct amount \ of time to spend dithering when updating objects on \ landscape during gameplay LDA #40 \ The Sentinel has won and we are therefore drawing the STA ditherOuterLoop \ game over screen rather than updating an object in the \ landscape, so set ditherOuterLoop = 40 to make the \ dithered effect in the DitherScreenBuffer routine last \ for 40 outer loops rather than 25, where each inner \ loop dithers 255 pixels to the screen .updo6 JSR DitherScreenBuffer \ Dither the contents of the screen buffer onto the \ screen \ \ Note that this process does not update the entire \ object on-screen, so we will need to do that at updo7 \ below once we have finished dithering LDA sentinelHasWon \ If bit 7 of sentinelHasWon is set then the Sentinel BMI updo11 \ has won the game and we are therefore drawing the game \ over screen and we don't want to draw the victorious \ enemy completely, so jump to updo11 to skip the part \ where the object is fully drawn on-screen .updo7 \ By this point we have either finished dithering the \ updated object onto the screen (if dithering was \ configured), or dithering was not configured \ \ In either case we now need to draw the whole object \ onto the screen SEI \ Disable interrupts to ensure that the screen doesn't \ scroll while we are removing the sights, as that could \ corrupt the screen JSR RemoveSights \ Remove the sights from the screen before we update the \ object on-screen by calling the RemoveSights routine SEC \ Set bit 7 of doNotDrawSights so the DrawSights routine ROR doNotDrawSights \ does not draw the sights on-screen going forward \ \ This prevents the interrupt handler from drawing the \ sights, in case it starts processing a sights-related \ key press while we still are doing the updating CLI \ Re-enable interrupts LDA objScreenAddr \ Set toAddr(1 0) to the address of the object in screen STA toAddr \ memory which we put into objScreenAddr(1 0) above, so LDA objScreenAddr+1 \ we copy the screen contents from the screen buffer to STA toAddr+1 \ the correct place on-screen LDA bufferColumns \ We are now going to loop through the character columns STA loopCounter \ that we want to copy to the screen, so set a loop \ counter to the number of columns in bufferColumns, \ which we set to the column count for covering the \ whole object (or as many as we can fit into the column \ buffer, at least) JMP updo10 \ We are now ready to copy the configured number of \ character columns from the screen buffer to the \ screen, so jump into the following loop at updo10 .updo8 \ We have updated an on-screen character column, so now \ we move to the next character column to the right LDA objScreenAddr \ Set (A objScreenAddr) = objScreenAddr(1 0) + 8 CLC \ ADC #8 \ and (A toAddr) = objScreenAddr(1 0) + 8 STA objScreenAddr \ STA toAddr \ So this moves both the from and to addresses to the LDA objScreenAddr+1 \ next character column along ADC #0 CMP #&80 \ If the high byte in A >= &80 then the new address is BCC updo9 \ past the end of screen memory, so subtract &20 from SBC #&20 \ the high byte so the address wraps around within the \ range of screen memory between &6000 and &8000 .updo9 STA objScreenAddr+1 \ Store the high byte of the result, so we now have: \ \ objScreenAddr(1 0) = objScreenAddr(1 0) + 8 \ \ with the address wrapped around as required STA toAddr+1 \ Store the high byte of the result, so we now have: \ \ toAddr(1 0) = toAddr(1 0) + 8 \ \ with the address wrapped around as required .updo10 \ This is where we enter the screen-updating loop LDY lastPanKeyPressed \ Set Y to the value of lastPanKeyPressed, which we set \ to zero above \ \ Zero is normally the value for when we are panning the \ screen to the right, but we use it here because when \ we pan to the right, we draw the screen buffer onto \ the screen in columns, working from left to right, and \ that's also what we want to do when updating the \ object JSR ShowScreenBuffer \ Update the player's scrolling landscape view by \ copying the relevant parts of the screen buffer into \ screen memory DEC loopCounter \ Decrement the loop counter to move on to the next \ character column in the updated object BNE updo8 \ Loop back to keep drawing character columns from the \ screen buffer until we have drawn them all .updo11 \ We now need to check whether we have updated the whole \ object on-screen \ \ The column buffer is 24 character rows tall and up to \ 20 character columns wide, so if the on-screen object \ wider than 20 character columns (i.e. wider than half \ a screen width), then we can't fit the whole object \ into the column buffer in one go, so we need to draw \ it in two stages LDA objYawOffset \ Set objYawOffset = objYawOffset + bufferColumns CLC \ ADC bufferColumns \ This sets the yaw offset to the character column to STA objYawOffset \ the right of the on-screen block that we just drew \ to update the object LDA objYawWidth \ Set A = objYawWidth - bufferColumns SEC \ SBC bufferColumns \ We just drew bufferColumns character columns of the \ object and objYawWidth contains the full width of the \ on-screen object in character columns, so A now \ contains the number of character columns that we still \ have to draw BEQ updo12 \ If A = 0 then we have drawn the whole object, so jump \ to updo12 to finish off \ If we get here then A is non-zero so we still have \ more of the object to update on-screen, so we update \ the variables and loop back to draw the next set of \ 20 character columns to the right, dithering them in \ the same way if required STA objYawWidth \ Set the width of the on-screen object to A so it \ contains the number of character columns we still have \ to draw STA bufferColumns \ Configure the buffer width to the correct size for the \ second batch of character columns STA ditherOuterLoop \ Set ditherOuterLoop to A, so the dithered effect in \ the DitherScreenBuffer routine implements A outer \ loops, where each inner loop dithers 255 pixels to the \ screen JMP updo2 \ Jump back to updo2 to update the second part of the \ on-screen object .updo12 LSR doNotDrawSights \ Clear bit 7 of doNotDrawSights so the DrawSights \ routine is enabled once again LDA sightsAreVisible \ If bit 7 of sightsAreVisible is clear then the sights BPL updo13 \ are not currently being shown on-screen, so jump to \ updo13 to skip the following SEI \ Disable interrupts to ensure that the screen doesn't \ scroll while we are drawing the sights, as that could \ corrupt the screen JSR DrawSights \ Bit 7 of sightsAreVisible is set, so we need to draw \ the sights that we removed when updating the object, \ so call DrawSights to put them back CLI \ Re-enable interrupts .updo13 JMP updo1 \ We have finished updating the object, so jump to updo1 \ to reset the various dither-related variables and \ return from the subroutine
Name: screenBufferAddr [Show more] Type: Variable Category: Screen buffer Summary: Storage for the address for the current drawing operation in the screen buffer
Context: See this variable on its own page References: This variable is used as follows: * SetBufferAddress uses screenBufferAddr * ShowBufferColumn uses screenBufferAddr * ShowScreenBuffer uses screenBufferAddr
.screenBufferAddr EQUW &0000
Name: objScreenAddr [Show more] Type: Variable Category: Screen buffer Summary: The screen address of the object we are updating
Context: See this variable on its own page References: This variable is used as follows: * DitherScreenBuffer uses objScreenAddr * DrawUpdatedObject uses objScreenAddr
.objScreenAddr EQUW &0000
Name: ditherOuterLoop [Show more] Type: Variable Category: Screen buffer Summary: A counter for the inner loop when dithering objects to the screen in the DitherScreenBuffer routine
Context: See this variable on its own page References: This variable is used as follows: * DitherScreenBuffer uses ditherOuterLoop * DrawUpdatedObject uses ditherOuterLoop
.ditherOuterLoop EQUB 0
Name: yawAdjustmentHi [Show more] Type: Variable Category: Screen buffer Summary: The yaw offset of the left edge of the object being updated, relative to the left edge of the screen, in yaw angles
Context: See this variable on its own page References: This variable is used as follows: * DrawUpdatedObject uses yawAdjustmentHi
.yawAdjustmentHi EQUB 0 \ A yaw adjustment to apply to the landscape drawing \ process so that when we update objects on-screen, we \ can yaw the viewer's gaze to the right to move the \ left edge of the object to the left edge of the screen \ so the object will fit into the screen buffer as \ efficiently as possible (high byte)
Name: GetObjVisibility [Show more] Type: Subroutine Category: Gameplay Summary: Calculate whether any part of an object is visible on-screen, and if so, which character columns it spans on the screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * CheckObjVisibility calls GetObjVisibility * DrawUpdatedObject calls GetObjVisibility

Arguments: currentObject The number of the object to check
Returns: C flag The object's visibility: * Clear = at least some of the object is visible on-screen * Set = the object is not visible on-screen bufferColumns If the object is visible, this is set to the number of character columns in the screen buffer that the object spans objYawOffset If the object is visible, this is set to the yaw offset of the left edge of the object from the left edge of the screen, in character columns
.GetObjVisibility LDY currentObject \ Set Y to the number of the object to check (so we are \ checking object #Y) CPY playerObject \ If this is the player object, jump to objv6 to return BEQ objv6 \ from the subroutine with the C flag set to indicate \ that the object is not visible on-screen, as the \ player can never see themselves JSR GetObjectAngles \ Calculate the angles and distances of the vector from \ the viewer to object #Y and put them into the \ following variables: \ \ * Set objectViewYaw(Hi Lo) to the yaw angle of the \ viewer's gaze towards the object, relative to the \ screen \ \ * Set hypotenuse(Hi Lo) to the length of the 3D \ vector from the viewer to the object when \ projected down onto the ground plane \ We now work out the width of the object that we are \ looking at so we can work out whether any of it is \ on-screen LDY currentObject \ Set X to the type of the object that we are checking LDX objectTypes,Y LDA objectHalfWidth,X \ Set A to half the object width for this object type, \ as a fraction of a tile CMP minObjWidth \ If A < minObjWidth then set A = minObjWidth, so: BCS objv1 \ LDA minObjWidth \ A = max(minObjWidth, objectHalfWidth) \ \ So if minObjWidth is non-zero, we consider the object \ to be at least as wide as minObjWidth for the purposes \ of calculating whether it is visible on-screen .objv1 STA xDeltaLo \ Set (A xDeltaLo) = (0 A) LDA #0 \ = max(minObjWidth, objectHalfWidth) \ \ So xDeltaLo is the width of the object STA minObjWidth \ Set minObjWidth = 0 as we have used this figure in the \ visibility calculation for the object, so we need to \ reset it for the next time we check an object (so the \ default value for minObjWidth is zero and it is only \ overridden when a large object is replaced by a \ smaller object) JSR GetPitchAngleDelta \ At this point we construct a right-angled triangle \ with adjacent side hypotenuse(Hi Lo) and opposite \ side xDeltaLo, like this: \ \ object \ _.-+ \ _.-´ | \ _.-´ | \ _.-´ | \ _.-´ | xDeltaLo \ _.-´ | \ .-´ angle(Hi Lo) | \ viewer +--------------------------+ \ hypotenuse(Hi Lo) \ \ This triangle is flat on the ground with the viewer at \ the corner \ \ The adjacent side of length hypotenuse(Hi Lo) along \ the bottom of the triangle represents the projection \ onto the ground of the vector from the viewer to the \ object, as if there were a light shining down from \ above, so this is the projection of the gaze vector \ \ We want to calculate angle(Hi Lo), as that's the yaw \ angle delta between the projected gaze vector and the \ edge of the object, as the object has a yaw angle of \ xDeltaLo from the object centre to the side (as it's \ the half-width of the object) \ \ We can then use this yaw angle delta to work out \ whether the left and right edges of the object are \ within the left-right viewing arc of the screen, and \ therefore whether any part of the object is within the \ screen bounds \ \ To do this we call GetPitchAngleDelta, not because we \ are calculating a pitch angle, but because this \ routine calculates the angle of the above triangle as \ part of the process of calculating a pitch angle delta \ (so we just ignore the pitch angle result here) \ We now check whether the object is on screen in terms \ of yaw angles, so that's checking the object's edges \ against the left and right edges of the screen \ \ We start by checking the object against the left edge \ of the screen LDA objectViewYawLo \ Set (A T) = objectViewYaw(Hi Lo) - angle(Hi Lo) SEC \ SBC angleLo \ So (A T) contains the yaw angle of the left edge of STA T \ the object, relative to the screen LDA objectViewYawHi SBC angleHi BPL objv2 \ If the result in (A T) is positive then the left edge \ of the object is to the right of the left screen edge, \ so jump to objv2 to see how far it is to the right LDA #0 \ (A T) is negative, so the left edge of the object is \ to the left of the left screen edge, so set A = 0 to \ use as the yaw offset of the left edge of the object \ in objYawOffset BEQ objv3 \ Jump to objv3 to set objYawOffset to zero and move on \ to the checks for the right edge of the object .objv2 ASL T \ Double the yaw angle in (A T) ROL A \ \ This converts the yaw angle into character columns in \ screen memory \ \ The custom screen mode 5 used by the game is 40 \ character columns wide, and the screen is 20 yaw \ angles wide in terms of the game's coordinate system \ \ So we can simply double the yaw angle in (A T) to \ convert it into on-screen character columns CMP #40 \ If A >= 40 then the left edge is past the right edge BCS objv6 \ of the screen (as the screen is 40 columns wide), \ which means the object is not visible, so jump to \ objv6 to return from the subroutine with the C flag \ set to indicate that the object is not visible \ on-screen .objv3 STA objYawOffset \ Set objYawOffset to A, so it contains the yaw offset \ of the left edge of the object from the left edge of \ the screen, in character columns \ We now check the object against the right edge of the \ screen LDA objectViewYawLo \ Set (A T) = objectViewYaw(Hi Lo) + angle(Hi Lo) CLC \ ADC angleLo \ So (A T) contains the yaw angle of the right edge of STA T \ the object, relative to the screen LDA objectViewYawHi ADC angleHi BMI objv6 \ If the result in (A T) is negative then the right edge \ of the object is to the left of the left screen edge, \ so jump to objv6 to return from the subroutine with \ the C flag set to indicate that the object is not \ visible on-screen \ If we get here then the right edge of the object is to \ the right of the left screen edge, and we know from \ the first test that the left edge of the object is to \ the left of the right screen edge, so some of the \ object must be on-screen ASL T \ Double the yaw angle in (A T) ROL A \ \ This converts the yaw angle into character columns in \ screen memory \ \ The custom screen mode 5 used by the game is 40 \ character columns wide, and the screen is 20 yaw \ angles wide in terms of the game's coordinate system \ \ So we can simply double the yaw angle in (A T) to \ convert it into on-screen character columns CMP #40 \ If A >= 40 then set A = 39, so A = max(39, A) BCC objv4 \ LDA #39 \ This sets A to the yaw angle offset of the right edge \ of the object, in on-screen character columns, clipped \ to fit within the screen width of 40 columns .objv4 CLC \ Set objYawWidth = A + 1 - objYawOffset ADC #1 \ SEC \ This sets objYawWidth to the difference between the SBC objYawOffset \ yaw offsets for the right and left edges, so that's STA objYawWidth \ the width of the object in on-screen character columns \ \ We add 1 to ensure we take the fractional part of the \ yaw offsets into account, as we have ignored the low \ byte in T for this calculation BEQ objv6 \ If objYawWidth = 0 then the object has no on-screen \ width, so jump to objv6 to return from the subroutine \ with the C flag set to indicate that the object is not \ visible on-screen CMP #21 \ If A >= 21 then set A = 20, so A = max(20, A) BCC objv5 \ LDA #20 \ This clips A to the maximum of 20 character columns as \ this is the maximum width of the screen buffer when \ configured as a column buffer .objv5 STA bufferColumns \ Configure the screen buffer to use A character columns \ for drawing the object \ \ Note that this has a maximum value of 20, which is why \ close-up objects that take up a large amount of space \ on-screen are absorbed in two stages, left then right, \ as the object it too wide to fit into the column \ buffer's maximum width of 20 character columns, or \ half a screen CLC \ Clear the C flag to indicate that the object is \ visible on-screen RTS \ Return from the subroutine .objv6 SEC \ Set the C flag to indicate that the object is not \ visible on-screen RTS \ Return from the subroutine
Name: objectHalfWidth [Show more] Type: Variable Category: Gameplay Summary: Each object's width in terms of tile widths, for half the object
Context: See this variable on its own page References: This variable is used as follows: * GetObjVisibility uses objectHalfWidth
.objectHalfWidth EQUB 62 \ Object type 0: Robot 0.242 tiles EQUB 70 \ Object type 1: Sentry 0.273 tiles EQUB 114 \ Object type 2: Tree 0.445 tiles EQUB 122 \ Object type 3: Boulder 0.477 tiles EQUB 74 \ Object type 4: Meanie 0.289 tiles EQUB 78 \ Object type 5: The Sentinel 0.305 tiles EQUB 193 \ Object type 6: Sentinel's tower 0.754 tiles
Name: SpawnObject [Show more] Type: Subroutine Category: 3D objects Summary: Add a new object of the specified type to the objectTypes table
Context: See this subroutine on its own page References: This subroutine is called as follows: * AddEnemiesToTiles calls SpawnObject * ExpendEnemyEnergy calls SpawnObject * PerformHyperspace calls SpawnObject * SpawnPlayer calls SpawnObject * SpawnTrees calls SpawnObject * ProcessActionKeys (Part 2 of 2) calls via SpawnObject+3

This routine spawns a new object by searching the objectFlags table for a free number to allocate. If there is no free number then the routine returns with the C flag set, otherwise the C flag is clear, X and currentObject are set to the number of the new object, and the object type is added to the objectTypes table. Note that this routine only adds the object to the objectTypes table; it doesn't update the flags or add any other information about the object.
Arguments: A The type of object to spawn: * 0 = Robot (one of which is the player) * 1 = Sentry * 2 = Tree * 3 = Boulder * 4 = Meanie * 5 = The Sentinel * 6 = The Sentinel's tower
Returns: X The number of the new object (if successful) currentObject The number of the new object (if successful) C flag Status flag: * Clear if the object was successfully spawned * Set if there is no free number for the new object
Other entry points: SpawnObject+3 Spawn an object of the type specified in keyPress The keyPress and objectType variables share the same memory location, so this lets us store object types in the key press codes in keyLoggerConfig, so that pressing one of the "create" keys will automatically spawn that type of object
.SpawnObject STA objectType \ Store the object type in objectType for future \ reference LDX #63 \ In order to be able to create a new object, we need to \ find an unallocated object number in the objectFlags \ table \ \ The game can support up to 64 objects, each with its \ own number (0 to 63), so set a counter in X to work \ through the object numbers until we find one that is \ not allocated to an object .sobj1 LDA objectFlags,X \ Set A to the object flags for object #X, which are \ stored in the X-th entry in the objectFlags table BMI sobj2 \ If bit 7 of object flags for object #X is set then \ this object number is not yet allocated to an object, \ so jump to sobj2 use this number for our new object DEX \ Otherwise decrement the counter in X to move on to the \ next object number BPL sobj1 \ Loop back to sobj1 to check the next object number SEC \ If we get here then we have checked all 64 object \ numbers and none of them are free, so set the C flag \ to indicate that we have failed to spawn the object RTS \ Return from the subroutine .sobj2 \ If we get here then we have found an unallocated \ object number in the objectFlags table at index X STX currentObject \ Set currentObject to the object number in X LDA objectType \ Set the corresponding entry in the objectTypes table STA objectTypes,X \ to the object type that we are spawning, which we \ stored in objectType above CLC \ Clear the C flag to indicate that we have successfully \ added a new object to the objectFlags table RTS \ Return from the subroutine
Name: UpdatePlayerEnergy [Show more] Type: Subroutine Category: Gameplay Summary: Update the player's energy levels by adding or subtracting the amount of energy in a specific object
Context: See this subroutine on its own page References: This subroutine is called as follows: * PerformHyperspace calls UpdatePlayerEnergy * ProcessActionKeys (Part 2 of 2) calls UpdatePlayerEnergy

Arguments: X The number of the object whose energy is being added to or subtracted from the player's energy C flag Controls whether we add or subtract the energy: * Clear = add object #X's energy to the player's energy * Set = Subtract object #X's energy from the player's energy
Returns: C flag Result flag: * Clear if the player still has a positive energy level after the update * Set if the player now has a negative energy level, which means the Sentinel has won
.UpdatePlayerEnergy LDY objectTypes,X \ Set Y to the object type of object #X LDA playerEnergy \ Set A to the player's current energy level BCC uple1 \ If the C flag argument is clear, jump to uple1 to add \ the energies \ Otherwise the C flag argument is set, so we subtract \ object #X's energy from the player's energy in A SBC objectTypeEnergy,Y \ The objectTypeEnergy table contains the energy levels \ for each of the object types, so this subtracts the \ energy of object #X from the player's energy in A \ \ The subtraction works because we know the C flag is \ set, as we just passed through a BCC BCS uple2 \ If the subtraction didn't underflow then the player \ still has some energy left, so jump to uple2 to return \ from the subroutine with the C flag clear SEC \ The player now has negative energy, so return from the \ subroutine with the C flag set to indicate this RTS \ Return from the subroutine .uple1 \ If we get here then the C flag argument is clear, so \ we add object #X's energy to the player's energy in A ADC objectTypeEnergy,Y \ The objectTypeEnergy table contains the energy levels \ for each of the object types, so this adds the energy \ of object #X to the player's energy in A \ \ The addition works because we know the C flag is \ clear, as we got here by taking a BCC .uple2 AND #63 \ Cap the value in A to a maximum of 63, so the player's \ energy is always in the range 0 to 63 STA playerEnergy \ Update the player's energy level to the new level in A CLC \ Clear the C flag to indicate that the player still has \ a positive amount of energy (i.e. 0 or above) RTS \ Return from the subroutine
Name: objectTypeEnergy [Show more] Type: Variable Category: 3D objects Summary: The amount of energy required to create each object or the amount energy acquired when absorbing each object
Context: See this variable on its own page References: This variable is used as follows: * UpdatePlayerEnergy uses objectTypeEnergy
.objectTypeEnergy EQUB 3 \ Object type 0: Robot 3 energy units EQUB 3 \ Object type 1: Sentry 3 energy units EQUB 1 \ Object type 2: Tree 1 energy unit EQUB 2 \ Object type 3: Boulder 2 energy units EQUB 1 \ Object type 4: Meanie 1 energy unit EQUB 4 \ Object type 5: The Sentinel 4 energy units EQUB 0 \ Object type 6: Sentinel's tower 0 energy units
Name: PerformHyperspace [Show more] Type: Subroutine Category: Gameplay Summary: Hyperspace the player to a brand new tile, ideally at the same altitude as the current tile
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTactics (Part 2 of 8) calls PerformHyperspace * ProcessActionKeys (Part 1 of 2) calls PerformHyperspace

Returns: C flag Returns: * Clear if the hyperspace was a success, or if the player didn't have enough energy to hyperspace * Set if we could not spawn a robot to use as the destination for the hyperspace
.PerformHyperspace LDA #0 \ Spawn a robot (an object of type 0), returning the JSR SpawnObject \ object number of the new object in X and currentObject LDX playerObject \ Set A to the high byte of the altitude of the player LDA yObjectHi,X \ object (i.e. the high byte of the player object's \ y-coordinate in yObjectHi) CLC \ Increment A so it's one coordinate higher than the ADC #1 \ player object, so the call to PlaceObjectBelow will \ try to hyperspace the player to the same height as \ before LDX currentObject \ Set X to the object number of the new robot that we \ spawned above JSR PlaceObjectBelow \ Attempt to place the new robot on a tile that is below \ the maximum altitude specified in A (though we may end \ up placing the object higher than this) BCS hypr4 \ If the call to PlaceObjectBelow sets the C flag then \ the object has not been successfully placed, so jump \ to hypr4 to return from the subroutine with the C flag \ set to indicate that we haven't managed to hyperspace \ the player SEC \ Call UpdatePlayerEnergy with the C flag set to JSR UpdatePlayerEnergy \ subtract the amount of energy in object #X from the \ player's energy \ \ Object #X is the robot we just spawned, so this will \ subtract three energy units from the player BCC hypr1 \ If the player still has positive energy then the call \ to UpdatePlayerEnergy will clear the C flag, so jump \ to hypr1 to keep going \ If we get here then the player doesn't have enough \ energy to create a robot for hyperspacing JSR DeleteObject \ Delete object #X and remove it from the landscape, so \ we remove the robot that we just spawned LDA #3 \ Set screenBackground = 3 so the next time the screen STA screenBackground \ is cleared, it shows a black background with stars LDA #%10000000 \ Set bit 7 of hyperspaceEndsGame to indicate that the STA hyperspaceEndsGame \ game has ended with a hyperspace, and clear bit 6 to \ indicate that the game has ended because the player \ has run out of energy BNE hypr3 \ Jump to hypr3 to return from the subroutine (this BNE \ is effectively a JMP as A is never zero) .hypr1 LDA #0 \ Call the PlayMusic routine with A = 0 to play the JSR PlayMusic \ hyperspace music LDX playerObject \ If the player is not on the Sentinel's tile in terms LDA xObject,X \ of the x-coordinate, jump to hypr2 CMP xTileSentinel BNE hypr2 LDA zObject,X \ If the player is not on the Sentinel's tile in terms CMP zTileSentinel \ of the z-coordinate, jump to hypr2 BNE hypr2 \ The player is on the Sentinel's tile in both axes, so \ they must have hyperspaced while standing on top of \ the Sentinel's tower, so they have just completed this \ landscape LDA #%11000000 \ Set bit 7 of hyperspaceEndsGame to indicate that the STA hyperspaceEndsGame \ game has ended with a hyperspace, and set bit 6 to \ indicate that the game has ended because the player \ has won by hyperspacing from the Sentinel's tower LDA #%10000000 \ Set bit 7 of doNotPlayLandscape so that when we finish STA doNotPlayLandscape \ the landscape, the landscape generation process will \ return normally, without previewing the landscape \ \ As the last step in winning a game is to hyperspace \ onto the Sentinel's tower, this ensures that winning a \ level will then display that landscape's secret code \ rather than displaying the preview and making us play \ it again .hypr2 JSR FocusOnKeyAction \ Tell the game to start focusing effort on the key \ action that has been initiated (i.e. the hyperspace) LDX currentObject \ Set the player's object number to that of the new STX playerObject \ robot that we spawned above, so this effectively \ performs the hyperspace into the new robot .hypr3 LDA #%10000000 \ Set bit 7 of playerHasMovedTile to indicate that the STA playerHasMovedTile \ player has moved to a new tile CLC \ Clear the C flag to indicate that we have successfully \ hyperspaced the player .hypr4 RTS \ Return from the subroutine
Name: DrawObjectStack [Show more] Type: Subroutine Category: Drawing objects Summary: Draw an entire stack of objects
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawTileAndObjects calls DrawObjectStack

Arguments: A The tile data for the tile containing the stack
.DrawObjectStack AND #%00111111 \ Because the tile has an object on it, the tile data STA topObjectOnStack \ contains the number of the top object on the tile in \ bits 0 to 5, so extract the object number into \ topObjectOnStack LDX #0 \ We now count the number of objects that are stacked on \ the tile, so initialise a counter in X .stak1 INX \ Increment the object counter in X to record that we \ have found an object in the stack AND #%00111111 \ Because the tile has an object on it, the tile data TAY \ contains the number of the top object on the tile in \ bits 0 to 5, so extract the object number into Y (so \ object #Y is on the tile, though it might be part of \ an object stack) LDA objectFlags,Y \ Set A to the object flags for the object on the tile CMP #%01000000 \ If bit 6 of the object flags for object #Y is set BCS stak1 \ then object #Y is stacked on top of another object, \ and that object number is in bits 0 to 5 of the object \ flags, so jump to stak1 to increase the object count, \ extract that object number from A and check the flags \ again (so this works down through the stack of objects \ until we reach the object at the bottom of the stack) DEX \ Set objectStackCounter = X - 1 so we can use it as a STX objectStackCounter \ counter as we work through all the objects in the \ stack BEQ stak7 \ If there is only one object on the tile then the stack \ size will be zero, so jump to stak7 to draw the object \ stack from object #Y down (which in this case will \ just draw object #Y) \ If we get here then there is more than one object in \ the stack, so we now work out in which direction to \ draw the stack: top to bottom or bottom to top \ \ If the player is at a lower altitude than the object \ at the bottom of the stack, then the whole stack is \ above the player and the object on the top of the \ stack is the furthest away, so we need to draw the \ stack from top to bottom \ \ If it's the other way around and the player is at the \ same or higher altitude than the bottom of the stack, \ then we need to draw the stack from bottom to top, as \ the bottom object is the furthest away \ \ There is one exception: if the object stack is the \ Sentinel's tower and we are at the exact same height \ as the tower, then we draw the stack from top to \ bottom to ensure the Sentinel's feet are correctly \ hidden by the platform \ \ At this point, object #Y is the bottom object in the \ stack and object #topObjectOnStack is the top object .stak2 LDX playerObject \ Set X to the object number of the player LDA yObjectLo,X \ Set (A T) to the vertical distance between the player SEC \ and the object we are drawing, as follows: SBC yObjectLo,Y \ STA T \ (A T) = yObject,X - yObject,Y \ \ starting with the low bytes LDA yObjectHi,X \ And then the high bytes SBC yObjectHi,Y BMI stak6 \ If the result is negative then the player (object #X) \ is at a lower altitude than the object we are drawing \ (object #Y), so jump to stak6 to draw the entire \ object stack from top to bottom, starting with the top \ object and working down to the bottom \ If we get here then the player is level with or higher \ than the object we are drawing (object #Y) ORA T \ If at least one of A and T is non-zero then (A T) > 0, BNE stak3 \ then the player is at a higher altitude than the \ object we are drawing, so jump to stak3 to skip the \ following check \ If we get here then the player is level with the \ object we are drawing, so we need to check whether \ we are thinking of drawing the Sentinel's tower, \ because if we are, we need to draw the Sentinel before \ the tower to ensure the Sentinel's feet are correctly \ hidden by the platform LDA objectTypes,Y \ If the object we are drawing is the Sentinel's tower CMP #6 \ (an object of type 6), then jump to stak6 to draw the BEQ stak6 \ entire object stack from top to bottom, starting with \ the top object (the Sentinel) and working down to the \ bottom object (the tower) .stak3 \ If we get here then the player is higher than the \ object we are drawing (object #Y), so we need to draw \ the object stack upwards, from the bottom to the top \ \ This process is a bit convoluted, as the object flags \ only store object numbers that are below other objects \ rather than above, so we have to repeatedly count down \ the stack to work out which object to draw next \ \ The first time we enter this loop, object #Y is the \ object at the bottom of the stack, so this loop draws \ the stack from the very bottom to the top \ \ The second time we enter this loop, object #Y is the \ object above the original object #Y, so it's the next \ object up \ \ We loop around until we have drawn the whole stack \ from bottom to top \ \ This loop therefore draws object #Y and then works out \ which object is above object #Y in the stack and loops \ back to repeat the process, until we reach the top of \ the stack JSR DrawObject \ Draw object #Y onto the screen or into the screen \ buffer \ We now work out which object is above object #Y, which \ we do by counting down the stack by the right amount, \ from the top object down \ \ This count is stored in objectStackCounter, which \ contains the number of objects we still have to draw, \ which is the same as the number of objects above \ object #Y LDY topObjectOnStack \ Set Y to the number of the object on top of the stack, \ so we can start counting down from the top DEC objectStackCounter \ Decrement the object stack counter as we have just \ drawn object #Y BMI stak8 \ If we have drawn all the objects in the stack then the \ counter will now be negative, so jump to stak8 to \ return from the subroutine as we have drawn the whole \ stack LDX objectStackCounter \ Otherwise set X to the updated object stack counter, \ which is the number of objects above the object that \ we just drew BPL stak5 \ Jump to stak5 to start counting down the stack for X \ objects from the top, to give us the object that's \ above object #Y in the stack (this BPL is effectively \ a JMP as we know objectStackCounter is positive, so X \ must also be positive) .stak4 \ If we get here then we need to move down the object \ stack by one place from object #Y, updating Y to the \ number of the next object down LDA objectFlags,Y \ Set A to the object flags for object #Y AND #%00111111 \ Object #Y is stacked on top of another object, so the TAY \ number of the object below is in bits 0 to 5 of the \ object flags, so extract the next object number down \ the stack into Y DEX \ Decrement the counter in X as we have just moved down \ the stack by one place .stak5 \ This is the entry point for the loop that counts X \ objects down the stack, from the top object down BNE stak4 \ If X <> 0 then loop back to stak4 to move down the \ stack BEQ stak2 \ Otherwise jump to stak2 to draw object #Y, as that is \ the next object down the stack from the object we just \ drew (this BEQ is effectively a JMP as we just passed \ through a BNE) .stak6 LDY topObjectOnStack \ Set Y = topObjectOnStack so the following loop draws \ the entire object stack from top to bottom, starting \ with the top object and working down to the bottom .stak7 \ This loop draws all the objects in the stack in turn, \ from object #Y at the top, all the way down to the \ object at the bottom of the stack JSR DrawObject \ Draw object #Y onto the screen or into the screen \ buffer LDA objectFlags,Y \ Set A to the object flags for the object we just drew AND #%00111111 \ If object #Y is stacked on top of another object, then TAY \ the number of the object below is in bits 0 to 5 of \ the object flags, so extract the next object number \ down the stack into Y, so we can draw that object next DEC objectStackCounter \ Decrement the object stack counter as we have just \ drawn an object BPL stak7 \ Loop back until we have drawn all the objects in the \ stack, from object #Y down to the bottom of the stack .stak8 RTS \ Return from the subroutine
Name: FillScreen [Show more] Type: Subroutine Category: Graphics Summary: Fill a rectangular in screen memory or the screen buffer with the specified background
Context: See this subroutine on its own page References: This subroutine is called as follows: * ClearScreen calls FillScreen * DrawUpdatedObject calls FillScreen * PanLandscapeView calls FillScreen

Arguments: A Determines whether we fill screen memory or the screen buffer: * 0 = fill the screen * 25 = fill the screen buffer X The number of character columns to fill Y The number of character rows to fill screenBackground The type of background to fill the rectangle with: * 0 = fill with alternating colour 0/1 (blue/black) pixel rows, for the sky during gameplay * 1 = fill with solid colour 0 (blue) * 2 = fill with dithered pixels in colour 0 (blue) and colour 3 (e.g. green in landscape 0000) by alternating colour 3/0/3/0 and 0/3/0/3 pixel bytes * 3 = fill with solid colour 1 (black)
.FillScreen STA fillRowNumber \ Set fillRowNumber = A, so we start filling from this \ character row number \ \ When we are filling the screen buffer, this value is \ 25 + the row number, so we can use this value as a \ lookup into the screenRowAddrLo and screenRowAddrHi \ tables, and it will return the corresponding address \ from the bufferRowAddrLo or bufferRowAddrHi table, to \ give us the row address in the screen buffer STY screenRowCounter \ Set screenRowCounter = Y, so we will fill this many \ character rows TXA \ If X < 32 then each character row in the fill will fit CMP #32 \ into 256 bytes (as each character block is eight bytes BCC fill1 \ and 32 * 8 = 256), so jump to fill1 to set bit 7 of \ moreColumnsToFill to record this fact \ If we get here then each character row in the fill \ needs more than 256 bytes, so we set A to the number \ of columns that we need to fill beyond the 256-byte \ limit, which we get by subtracting 32 from the total \ number of columns we need to fill SBC #32 \ Set A = X - 32 to store in moreColumnsToFill below \ (this subtraction works as we just passed through a \ BCC instruction, so we know the C flag is set) LDX #32 \ Set X = 32 to set as the value of columnCounter below, \ so we fill 32 columns in the first iteration around \ the fill loop, and then fill moreColumnsToFill columns \ in the second iteration BNE fill2 \ Jump to fill2 to skip the following instruction (this \ BNE is effectively a JMP as X is never zero) .fill1 LDA #%10000000 \ Set bit 7 of moreColumnsToFill in the next instruction \ to indicate that this fill does not require more than \ 32 columns .fill2 STA moreColumnsToFill \ Set moreColumnsToFill to the value of A that we set \ above STX columnCounter \ Set columnCounter to the updated value of X, so this \ contains the number of columns to fill in the first \ iteration around the fill loop (i.e. the total number \ of columns if it's less than 32, or 32 if we need to \ do the fill in two stages) LDX screenBackground \ Set T to the backgroundEven pixel byte for the screen LDA backgroundEven,X \ background type in screenBackground, to give us the STA T \ pixel byte we should be clearing the screen to for \ even pixel rows LDA backgroundOdd,X \ Set U to the backgroundOdd pixel byte for the screen STA U \ background type in screenBackground, to give us the \ pixel byte we should be clearing the screen to for odd \ pixel rows .fill3 LDX fillRowNumber \ Set (Q P) to the entry from the screenRowAddrLo and LDA screenRowAddrLo,X \ screenRowAddrHi tables for the character row whose STA P \ number is in fillRowNumber LDA screenRowAddrHi,X \ STA Q \ When we are filling the screen buffer, fillRowNumber \ is 25 + the row number, so we can use this value as a \ lookup into the screenRowAddrLo and screenRowAddrHi \ tables, and it will return the corresponding address \ from the bufferRowAddrLo or bufferRowAddrHi table, to \ give us the row address in the screen buffer LDX columnCounter \ Set X to the number of columns that we need to fill LDA #1 \ Set loopCounter = 1 so we only do a maximum of two STA loopCounter \ fill stages, to prevent an incorrectly configured fill \ from filling up more than 64 columns \ We now fill X character blocks from screen address \ (Q P) onwards, alternating each pixel row between the \ pixel bytes in T and U .fill4 LDY #&FF \ Set Y = &FF so we increment Y to zero at the start of \ the following loop, so we can use it as an index into \ the block of screen memory starting at (P Q) .fill5 INY \ Increment the index in Y to point to the first byte \ in the character block LDA T \ Set the first byte in the character block to T STA (P),Y INY \ Increment the index in Y to point to the second byte \ in the character block LDA U \ Set the second byte in the character block to U STA (P),Y INY \ Increment the index in Y to point to the third byte \ in the character block LDA T \ Set the third byte in the character block to T STA (P),Y INY \ Increment the index in Y to point to the fourth byte \ in the character block LDA U \ Set the fourth byte in the character block to U STA (P),Y INY \ Increment the index in Y to point to the fifth byte \ in the character block LDA T \ Set the fifth byte in the character block to T STA (P),Y INY \ Increment the index in Y to point to the sixth byte \ in the character block LDA U \ Set the sixth byte in the character block to U STA (P),Y INY \ Increment the index in Y to point to the seventh byte \ in the character block LDA T \ Set the seventh byte in the character block to T STA (P),Y INY \ Increment the index in Y to point to the eighth byte \ in the character block LDA U \ Set the eighth byte in the character block to U STA (P),Y DEX \ Decrement the column counter in X BNE fill5 \ Loop back until we have filled X character blocks DEC loopCounter \ Decrement the loop counter BMI fill6 \ If we have already run two fill loops, jump to fill6 \ to move on to the next row, so we never do more than \ two fill stages LDX moreColumnsToFill \ Set X to the value of moreColumnsToFill, which either \ has bit 7 set, or contains the number of columns we \ still need to fill on top of the 32 we just filled BMI fill6 \ If bit 7 of moreColumnsToFill is set then each row in \ the fill fits into 256 bytes, so we will have already \ filled in the required bytes and can jump to fill6 to \ move on to the next row INC Q \ Increment the high byte of (Q P) to point to the next \ page in memory JMP fill4 \ Loop back to fill4 to continue filling along the \ character row, filling the number of columns in X to \ take us to the correct total number for the row .fill6 INC fillRowNumber \ Increment the row number to move down to the next \ character row DEC screenRowCounter \ Decrement the row counter BNE fill3 \ Loop back to fill the next character row until we have \ filled the number of rows in screenRowCounter RTS \ Return from the subroutine
Name: backgroundEven [Show more] Type: Variable Category: Graphics Summary: Pixel bytes for even pixel rows in the screen background
Context: See this variable on its own page References: This variable is used as follows: * FillScreen uses backgroundEven
.backgroundEven EQUB %00000000 \ Background 0 = colour 0 (even) \ colour 1 (odd) EQUB %00000000 \ Background 1 = colour 0 (even) \ colour 0 (odd) EQUB %10101010 \ Background 2 = colour 3/0/3/0 (even) \ colour 0/3/0/3 (odd) EQUB %00001111 \ Background 3 = colour 1 (even) \ colour 1 (odd)
Name: backgroundOdd [Show more] Type: Variable Category: Graphics Summary: Pixel bytes for odd pixel rows in the screen background
Context: See this variable on its own page References: This variable is used as follows: * FillScreen uses backgroundOdd
.backgroundOdd EQUB %00001111 \ Background 0 = colour 0 (even) \ colour 1 (odd) EQUB %00000000 \ Background 1 = colour 0 (even) \ colour 0 (odd) EQUB %01010101 \ Background 2 = colour 3/0/3/0 (even) \ colour 0/3/0/3 (odd) EQUB %00001111 \ Background 3 = colour 1 (even) \ colour 1 (odd)
Name: pixelBitMask [Show more] Type: Variable Category: Graphics Summary: A table for converting a pixel number in the range 0 to 3 into a screen mode 5 bit mask with that pixel's bits set and others clear
Context: See this variable on its own page References: This variable is used as follows: * DitherScreenBuffer uses pixelBitMask * DrawRandomDots uses pixelBitMask * DrawSights uses pixelBitMask
.pixelBitMask EQUB %10001000 \ Pixel bit mask with pixel 0 set EQUB %01000100 \ Pixel bit mask with pixel 1 set EQUB %00100010 \ Pixel bit mask with pixel 2 set EQUB %00010001 \ Pixel bit mask with pixel 3 set
Name: colourPixels [Show more] Type: Variable Category: Graphics Summary: A table that maps logical colours 0 to 3 to a four-pixel byte in that colour
Context: See this variable on its own page References: This variable is used as follows: * DrawPolygonLines (Part 1 of 4) uses colourPixels
.colourPixels EQUB %00000000 \ Four pixels of colour 0 EQUB %00001111 \ Four pixels of colour 1 EQUB %11110000 \ Four pixels of colour 2 EQUB %11111111 \ Four pixels of colour 3
Name: pixelsToLeft [Show more] Type: Variable Category: Graphics Summary: Pixel byte with all the pixels to the left of position X set
Context: See this variable on its own page References: This variable is used as follows: * DrawPolygonLines (Part 2 of 4) uses pixelsToLeft * DrawPolygonLines (Part 3 of 4) uses pixelsToLeft
.pixelsToLeft EQUB %00000000 EQUB %10001000 EQUB %11001100 EQUB %11101110
Name: pixelsToRight [Show more] Type: Variable Category: Graphics Summary: Pixel byte with all the pixels to the right of position X set
Context: See this variable on its own page References: This variable is used as follows: * DrawPolygonLines (Part 2 of 4) uses pixelsToRight * DrawPolygonLines (Part 3 of 4) uses pixelsToRight
.pixelsToRight EQUB %01110111 EQUB %00110011 EQUB %00010001 EQUB %00000000
Name: leftPixels [Show more] Type: Variable Category: Graphics Summary: Pixel byte with all the pixels to the right of position X + 1 set, to give the complementary pixel byte to pixelsToRight
Context: See this variable on its own page References: This variable is used as follows: * DrawPolygonLines (Part 2 of 4) uses leftPixels
.leftPixels EQUB %10001000 EQUB %11001100 EQUB %11101110 EQUB %11111111
Name: buffersOffsetLo [Show more] Type: Variable Category: Screen buffer Summary: An offset to add to the address of the right column screen buffer (low byte)
Context: See this variable on its own page References: This variable is used as follows: * DrawPolygonLines (Part 1 of 4) uses buffersOffsetLo
.buffersOffsetLo EQUB LO(0) \ Left row buffer EQUB LO(96) \ Right row buffer EQUB LO(0) \ Column buffer
Name: buffersOffsetHi [Show more] Type: Variable Category: Screen buffer Summary: An offset to add to the address of the right column screen buffer (low byte)
Context: See this variable on its own page References: This variable is used as follows: * DrawPolygonLines (Part 1 of 4) uses buffersOffsetHi
.buffersOffsetHi EQUB HI(0) \ Left row buffer EQUB HI(96) \ Right row buffer EQUB HI(0) \ Column buffer
Name: DrawPolygonLines (Part 1 of 4) [Show more] Type: Subroutine Category: Drawing polygons Summary: Draw an analysed polygon into the screen buffer
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawPolygon calls DrawPolygonLines

Arguments: yPolygonTop The pixel line number of the top of the polygon (the highest y-coordinate in the polygon) yPolygonBottom The pixel line number of the bottom of the polygon (the lowest y-coordinate in the polygon)
Returns: polygonGoesRight A flag to record whether the polygon fitted into the screen buffer: * Not 1 = at least one line in the polygon we just drew extends past the right edge of the buffer * 1 = the default value polygonGoesLeft A flag to record whether the polygon fitted into the screen buffer: * Not 1 = at least one line in the polygon we just drew extends past the left edge of the buffer * 1 = the default value
.DrawPolygonLines LDA #1 \ Set polygonGoesRight = 1 as the default value, which STA polygonGoesRight \ we can change to a different value if the polygon \ extends to the right of the screen buffer STA polygonGoesLeft \ Set polygonGoesLeft = 1 as the default value, which \ we can change to a different value if the polygon \ extends to the left of the screen buffer LDA yPolygonTop \ Set X = (yPolygonTop + yPolygonBottom) / 2 CLC \ ADC yPolygonBottom \ So X is the pixel line number of the middle pixel line ROR A \ in the polygon TAX LDA xPolygonRight,X \ If the pixel x-coordinate of the right edge is less CMP xPolygonLeft,X \ than the pixel x-coordinate of the left edge for the BCC dpol2 \ middle line, then the polygon must be facing away from \ us, so jump to dpol2 to return from the subroutine \ without drawing the polygon LDA #240 \ Set T = 240 - yPolygonTop - 1 CLC \ SBC yPolygonTop \ So T contains the screen y-coordinate of polygon pixel STA T \ line number yPolygonTop (as the screen y-coordinates \ increase as you go down the screen, unlike the polygon \ pixel line numbers which follow the same direction as \ the normal 3D world y-axis, which increases as you go \ up the screen) LSR A \ Set A = T / 8 LSR A \ LSR A \ So A is the number of the character row that contains \ the screen y-coordinate in T CLC \ If we are configured to draw into the screen buffer ADC screenOrBuffer \ then screenOrBuffer will be 25, so this makes us fetch \ row addresses from bufferRowAddrLo and bufferRowAddrHi \ (as bufferRowAddrLo - screenRowAddrLo is 25) \ \ Otherwise we are configured to draw directly onto the \ screen and screenOrBuffer is zero, in which case this \ addition doesn't change anything TAX \ Set X to the offset within the screenRowAddr(Hi Lo) \ table of the character row containing y-coordinate \ yPolygonTop, for either the screen or screen buffer \ (as configured in screenOrBuffer) LDA T \ Set A = T mod 8 AND #%00000111 \ \ So T contains the offset within the character block at \ the start of the character row that contains screen \ y-coordinate T CLC \ Set (S R) = A + screenRowAddr(Hi Lo) for row X ADC screenRowAddrLo,X \ STA R \ So (S R) contains the screen address of the start of LDA screenRowAddrHi,X \ the pixel row containing the pixel line number in STA S \ yPolygonTop LDY screenBufferType \ If this is the right row screen buffer then Y = 1, so LDA R \ add the following to (S R): CLC \ ADC buffersOffsetLo,Y \ (S R) = (S R) + 96 STA R \ LDA S \ This addition is performed for all buffer types, but ADC buffersOffsetHi,Y \ the addition is only non-zero for the right row buffer STA S LDA polygonColours \ Set A to the polygon colour byte for the polygon we \ we are drawing BIT blendPolygonEdges \ If bit 7 of blendPolygonEdges is clear then polygon BPL dpol1 \ edges should be drawn in the edge colour, so jump to \ dpol1 to skip the following \ If we get here then bit 7 of blendPolygonEdges is set, \ so we draw the polygon edges in the same colour as the \ polygon body (i.e. the fill colour), so the edges \ blend into the body \ \ We typically do this for distant objects, as distinct \ edge colours can make those object look messy AND #%11001111 \ Copy the fill colour from bits 2-3 of the polygon STA T \ colour byte into the edge colour in bits 4-5, so the LDA polygonColours \ polygon edge colour merges into the polygon body ASL A ASL A AND #%00110000 ORA T .dpol1 STA dpol16+1 \ Modify the following instruction in part 3: \ \ ORA edgePixelsLeft+&3C,X -> \ ORA edgePixelsLeft + polygonColours,X \ \ so the ORA instruction uses the correct byte for the \ the specified polygon colours from the edgePixelsLeft \ table STA dpol10+1 \ Modify the following instruction in part 2: \ \ ORA edgePixelsLeft+&3C,X -> \ ORA edgePixelsLeft + polygonColours,X \ \ so the ORA instruction uses the correct byte for the \ the specified polygon colours from the edgePixelsLeft \ table ORA #&40 \ Modify the following instruction in part 2: STA dpol14+1 \ \ ORA edgePixelsRight+&3C,X -> \ ORA edgePixelsRight + polygonColours,X \ \ so the ORA instruction uses the correct byte for the \ the specified polygon colours from the edgePixelsRight \ table LSR A \ Set Y to the fill colour from bits 2-3 of the polygon LSR A \ colour byte AND #%00000011 TAY LDA colourPixels,Y \ Set polygonFillPixels to a pixel byte containing four STA polygonFillPixels \ pixels set to the fill colour for the polygon LDY yPolygonTop \ Set Y to the pixel line number of the top of the \ polygon (the highest y-coordinate in the polygon in \ terms of 3D world coordinates, so higher values are \ higher up the screen) STY yPolygonLine \ Store the top pixel line number in yPolygonLine CPY yPolygonBottom \ If yPolygonTop >= yPolygonBottom then there is at BCS dpol13 \ least one visible line to draw in this polygon, so \ jump to dpol13 to draw the polygon, starting from \ row Y .dpol2 RTS \ Return from the subroutine
Name: DrawPolygonLines (Part 2 of 4) [Show more] Type: Subroutine Category: Drawing polygons Summary: Process the line overflowing the sides of the buffer, or fitting into one pixel byte, and loop on to the next polygon pixel line
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.dpol3 \ We jump back here to move on to the next pixel line in \ the polygon LDY yPolygonLine \ Set Y to the number of the polygon pixel line we are \ drawing (which decrements as we draw the polygon from \ top to bottom) CPY yPolygonBottom \ If Y = yPolygonBottom then we have reached the bottom BEQ dpol2 \ of the polygon, so jump to dpol2 to return from the \ subroutine as the whole polygon has now been drawn TYA \ Set A = Y mod 8 AND #%00000111 \ \ So A is the number of the polygon pixel line within \ the current character row BNE dpol11 \ If A is non-zero then the next polygon line (which \ will be on polygon pixel row A - 1) will still be in \ the same character row, so jump to dpol11 to move down \ one pixel line \ If we get here then A = 0, so the next polygon pixel \ line is in a different character row, so we need to \ move on to the next character row in the screen or the \ screen buffer LDA R \ Set (A R) = (S R) + &139 CLC \ = (S R) + 320 - 7 ADC #&39 \ STA R \ We add 320 to move on to the next row down on the LDA S \ screen, but we want to be in the top pixel row within ADC #&01 \ that character row, so we subtract 7 to move up to the \ top of the character block (as each row is eight pixel \ bytes high) CMP #&53 \ If the result of the addition is less than &5300, then BNE dpol4 \ we have not reached the end of row 15 in the screen \ buffer, so jump to dpol4 to skip the following \ If we get here then we need to wrap the screen buffer \ address around so we move from screenBufferRow15 to \ screenBufferRow16 LDA bufferRowAddrLo+16 \ Set (A R) to the address of screenBufferRow16 from the STA R \ bufferRowAddr(Hi Lo) LDA bufferRowAddrHi+16 .dpol4 STA S \ Store the high byte of the result, so we now have: \ \ (S R) = (S R) + 320 - 7 \ \ with the address wrapped around as required BNE dpol12 \ Jump to dpol12 to draw the next polygon pixel line \ (this BNE is effectively a JMP as the high byte of the \ address in A is never zero) .dpol5 \ If we get here then the right edge of the polygon line \ we are drawing extends past the left edge of the \ screen buffer LDA #0 \ Set polygonGoesLeft = 0 to denote that the polygon STA polygonGoesLeft \ extends past the left edge of the buffer, so if we \ are drawing in the right row buffer, this makes us \ draw the rest of the polygon in the left row buffer \ in the DrawPolygon routine BEQ dpol3 \ Jump to dpol3 to move on to the next pixel line in the \ polygon (this BEQ is effectively a JMP as A is always \ zero) .dpol6 \ If we get here then the left edge of the polygon line \ we are drawing extends past the right edge of the \ screen buffer LDA #0 \ Set polygonGoesRight = 0 to denote that the polygon STA polygonGoesRight \ extends past the right edge of the buffer, so if we \ are drawing in the left row buffer, this makes us draw \ the rest of the polygon in the right row buffer in the \ DrawPolygon routine BEQ dpol3 \ Jump to dpol3 to move on to the next pixel line in the \ polygon (this BEQ is effectively a JMP as A is always \ zero) .dpol7 \ If we get here then the right edge of the polygon line \ we are drawing extends past the right edge of the \ screen buffer LDA xBufferWidth \ Set xPolygonRightEdge = xBufferWidth * 2 ASL A \ STA xPolygonRightEdge \ This sets the screen x-coordinate of the right end of \ the polygon line we are drawing to the right edge of \ the screen buffer (we double the value in xBufferWidth \ to get the x-coordinate) STA polygonGoesRight \ Set polygonGoesRight to the value of A, which is at \ least 2 because xBufferWidth is non-zero and we just \ doubled it, so this has the same effect in the \ DrawPolygon routine as setting polygonGoesRight to 0 \ \ In other words, this denotes that the polygon extends \ past the right edge of the buffer, so if we are \ drawing in the left row buffer, this makes us draw the \ rest of the polygon in the right row buffer in the \ DrawPolygon routine BNE dpol15 \ Jump to dpol15 to draw the left edge and fill line for \ this polygon line (this BNE is effectively a JMP as A \ is never zero) .dpol8 \ If we get here then the left edge of the polygon line \ we are drawing extends past the left edge of the \ screen buffer \ \ We therefore need to draw the left end of the line in \ the screen buffer without the edge byte, and with the \ fill portion starting at the buffer's left edge LDA R \ Set (Q P) = (S R) - 8 SEC \ SBC #8 \ This sets (Q P) to the very first byte in the screen STA P \ row, so we don't reserve the first byte for the left LDA S \ edge in part 4 and instead draw the fill portion of SBC #0 \ the polygon line from the left edge of the screen row STA Q LDA #0 \ Set polygonGoesLeft = 0 to denote that the polygon STA polygonGoesLeft \ extends past the left edge of the buffer, so if we \ are drawing in the right row buffer, this makes us \ draw the rest of the polygon in the left row buffer \ in the DrawPolygon routine LDA #&F8 \ Set A = -8 to set the left edge of the line to the \ left edge of the screen (this is the same as setting \ the left edge to 0 and then subtracting 8 for the same \ reason we subtracted 8 from (Q P) above) BNE dpol17 \ Jump to dpol17 to draw the polygon line in part 4 \ (this BNE is effectively a JMP as A is never zero) .dpol9 \ If we get here then the pixel byte offset for the left \ edge is the same as the pixel byte offset for the \ right edge, so we need to draw both the left and right \ edges within the same pixel byte TXA \ Set X to the pixel number within the character block AND #%00000011 \ of the left edge of the polygon (as each character TAX \ block contains four pixels) LDA currentPixelByte \ Set A to the current screen contents for the pixel \ byte containing the right edge (which is the same as \ the pixel byte containing the left edge) \ \ We set this to the result of a LDA (R),Y instruction \ in part 3, so this is the same as fetching the current \ screen contents with LDA (R),Y AND pixelsToLeft,X \ Clear the pixels to the right of the left edge by \ AND'ing the current screen contents with a pixel byte \ with all the pixels to the left of position X set \ \ So A contains the existing screen contents for the \ pixels outside the polygon line, and with the polygon \ line pixels cleared, so now we can OR the polygon line \ pixels into the space we just cleared .dpol10 ORA edgePixelsLeft+&3C,X \ This instruction is modified in part 1 to point to \ the correct entry for the specified polygon \ colour: \ \ ORA edgePixelsLeft + polygonColours,X \ \ So this inserts the left edge of the polygon into \ the right part of the pixel byte in the correct \ colours for the currently configured fill and edge \ colours, with the edge pixels starting at pixel \ number X within the byte and ending at the right \ end of the pixel byte \ \ The original value of edgePixelsLeft+&3C is just \ workspace noise and has no meaning, it just sets \ the high byte to HI(edgePixelsLeft) AND leftPixels,X \ Clear all the pixels to the right of the right edge STA T \ and store the result in T, so this contains the pixel \ byte with the left edge in-place and the pixels for \ the right edge cleared \ \ The leftPixels bit mask is the complementary pixel \ byte to pixelsToRight, which we are about to use to \ fill in the right edge LDA (R),Y \ Set A to the current screen contents of the byte we \ are drawing both edges into AND pixelsToRight,X \ Clear the pixels to the left of the right edge by \ AND'ing the current screen contents with a pixel byte \ with all the pixels to the right of position X set \ \ So A contains the existing screen contents for the \ pixels outside the polygon line, and with the polygon \ line pixels cleared, so now we can OR the polygon line \ pixels into the space we just cleared ORA T \ Insert the right edge of the polygon into the pixel \ byte, with the edge pixels starting at the left end \ of the pixel byte and ending at pixel number X within \ the byte STA (R),Y \ Poke the pixel byte containing the left and right \ edges into screen memory JMP dpol3 \ Jump to dpol3 to move on to the next pixel line in the \ polygon .dpol11 INC R \ Increment the screen address in (S R) to move down one \ pixel row within the character row, ready for drawing \ the next polygon pixel line .dpol12 DEY \ Decrement the polygon line number in yPolygonLine to STY yPolygonLine \ move down one line in the polygon, from the top to the \ bottom
Name: DrawPolygonLines (Part 3 of 4) [Show more] Type: Subroutine Category: Drawing polygons Summary: Draw the left and right edges of the polygon line and fall into part 4 to draw the line in between
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.dpol13 \ This is the entry point for the loop that spans parts \ 2, 3 and 4 \ \ We jump here from part 1 with Y set to the number of \ the polygon pixel line at the top of the polygon \ \ We also get here on later iterations with Y set to the \ polygon pixel to draw next \ \ In either case we draw polygon pixel line Y and steps \ down the polygon one pixel line at a time until we \ reach the bottom LDA xPolygonRight,Y \ Set A to the pixel x-coordinate of the right edge for \ the line we are drawing CMP xPolygonLeft,Y \ If the pixel x-coordinate of the right edge is less BCC dpol2 \ than the pixel x-coordinate of the left edge for the \ line we are drawing, then the polygon must be facing \ away from us, so jump to dpol2 to return from the \ subroutine without drawing the polygon TAX \ Set X to the pixel x-coordinate of the right edge SBC xBufferLeft \ Set A = A - xBufferLeft \ \ So A is the distance between the right edge of the \ polygon and the left edge of the buffer \ \ This subtraction works because we just passed through \ a BCC, so we know the C flag is set BCC dpol5 \ If the subtraction underflowed then the right edge of \ the polygon is before the left edge of the buffer, so \ jump to dpol5 in part 2 to process this CMP xBufferWidth \ If A >= xBufferWidth then the right edge of the BCS dpol7 \ polygon is beyond the right edge of the buffer, so \ jump to dpol7 in part 2 to process this \ If we get here then the left and right edges of the \ polygon are both within the screen buffer ASL A \ Set Y to the offset within the character row of the AND #%11111000 \ pixel byte containing the right edge of the polygon TAY TXA \ Set X to the pixel number within the character block AND #%00000011 \ of the left edge of the polygon (as each character TAX \ block contains four pixels) STY xPolygonRightEdge \ Store the pixel byte offset within the character row \ of the right edge of the polygon in xPolygonRightEdge LDA (R),Y \ Set A to the current screen contents of the pixel byte \ where we want to draw the right edge of the polygon STA currentPixelByte \ Store the current screen contents for the right edge \ of the polygon in currentPixelByte, in case we need \ to draw the left and right edges in the same pixel \ byte AND pixelsToRight,X \ Clear the pixels to the left of the right edge by \ AND'ing the current screen contents with a pixel byte \ with all the pixels to the right of position X set \ \ So A contains the existing screen contents for the \ pixels outside the polygon line, and with the polygon \ line pixels cleared, so now we can OR the polygon line \ pixels into the space we just cleared .dpol14 ORA edgePixelsRight+&3C,X \ This instruction is modified in part 1 to point to \ the correct entry for the specified polygon \ colour: \ \ ORA edgePixelsRight + polygonColours,X \ \ So this inserts the right edge of the polygon into \ the left part of the pixel byte in the correct \ colours for the currently configured fill and edge \ colours, with the edge pixels starting at the left \ end of the pixel byte and ending at pixel number X \ within the byte \ \ The original value of edgePixelsRight+&3C is just \ workspace noise and has no meaning, it just sets \ the high byte to HI(edgePixelsRight) STA (R),Y \ Poke the pixel byte containing the right edge into \ screen memory, so we have now drawn the right edge of \ the polygon line .dpol15 LDY yPolygonLine \ Set A to the pixel x-coordinate of the left edge for LDA xPolygonLeft,Y \ the line we are drawing TAX \ Set X to the pixel x-coordinate of the left edge CMP xBufferRight \ If A >= xBufferRight then the left edge of the BCS dpol6 \ polygon is beyond the right edge of the buffer, so \ jump to dpol6 in part 2 to process this SEC \ Set A = A - xBufferLeft SBC xBufferLeft \ \ So A is the distance between the left edge of the \ polygon and the left edge of the buffer BCC dpol8 \ If the subtraction underflowed then the left edge of \ the polygon is before the left edge of the buffer, so \ jump to dpol8 in part 2 to process this ASL A \ Set Y to the offset within the character row of the AND #%11111000 \ pixel byte containing the left edge of the polygon TAY CPY xPolygonRightEdge \ If Y >= xPolygonRightEdge then the pixel byte offset BCS dpol9 \ for the left edge is the same as the pixel byte offset \ for the right edge, so jump to dpol9 to draw both \ edges within the same pixel byte TXA \ Set X to the pixel number within the character block AND #%00000011 \ of the left edge of the polygon (as each character TAX \ block contains four pixels) LDA (R),Y \ Set A to the current screen contents of the pixel byte \ where we want to draw the left edge of the polygon AND pixelsToLeft,X \ Clear the pixels to the right of the left edge by \ AND'ing the current screen contents with a pixel byte \ with all the pixels to the left of position X set \ \ So A contains the existing screen contents for the \ pixels outside the polygon line, and with the polygon \ line pixels cleared, so now we can OR the polygon line \ pixels into the space we just cleared .dpol16 ORA edgePixelsLeft+&3C,X \ This instruction is modified in part 1 to point to \ the correct entry for the specified polygon \ colour: \ \ ORA edgePixelsLeft + polygonColours,X \ \ So this inserts the left edge of the polygon into \ the right part of the pixel byte in the correct \ colours for the currently configured fill and edge \ colours, with the edge pixels starting at pixel \ number X within the byte and ending at the right \ end of the pixel byte \ \ The original value of edgePixelsLeft+&3C is just \ workspace noise and has no meaning, it just sets \ the high byte to HI(edgePixelsLeft) STA (R),Y \ Poke the pixel byte containing the left edge into \ screen memory, so we have now drawn the left edge of \ the polygon line \ Fall through into part 4 to draw a line between the \ left and right edges to finish off this line of the \ polygon
Name: DrawPolygonLines (Part 4 of 4) [Show more] Type: Subroutine Category: Drawing polygons Summary: Draw a horizontal pixel line of a specific length in character columns
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
\ We now draw the horizontal line between the left and \ right edges to complete the drawing of this polygon \ line, before looping back to draw the rest of the \ polygon TYA \ Set (Q P) = (S R) + Y CLC \ ADC R \ So (Q P) contains the screen address of the pixel byte STA P \ containing the left edge of the polygon line LDA S ADC #0 STA Q TYA \ Set A = Y \ \ So A contains the pixel byte offset within the screen \ character row of the left edge of the polygon line .dpol17 \ We now draw a horizontal line that starts in the left \ with the character column to the right of (Q P), as \ (Q P) contains the left edge \ \ And we want the line to end in the character column \ before the right edge, which we drew at pixel byte \ offset xPolygonRightEdge \ \ At this point, A contains the pixel byte offset of the \ left edge \ \ So we want to draw a line between pixel byte offset A \ and pixel byte offset xPolygonRightEdge \ \ Each character block contains eight bytes, so in terms \ of character columns, we want to draw a line that's \ this number of character columns wide: \ \ (xPolygonRightEdge - A) / 8 \ \ The following unrolled loop implements this, but in a \ reverse manner that means we have to negate the above \ value to calculate the correct entry point (as well as \ incorporating the fact that the jump points are spaced \ out by four bytes) SEC \ Set A = (A - xPolygonRightEdge) / 2 SBC xPolygonRightEdge \ LSR A \ So that's the above character column calculation, but \ negated and multiplied by 4 STA dpol18+1 \ Modify the following instruction below: \ \ BCC dpol19 -> BCC A \ \ so the BCC instruction jumps to the offset given in A, \ so the following routine draws a line of the length \ given in A, as follows: \ \ * When A = 0 * 4, draw from column 31 to column 1 \ \ * When A = 1 * 4, draw from column 30 to column 1 \ \ * When A = 2 * 4, draw from column 29 to column 1 \ \ ... \ \ * When A = 28 * 4, draw from column 3 to column 1 \ \ * When A = 29 * 4, draw from column 2 to column 1 \ \ * When A = 30 * 4, draw from column 1 to column 1 \ \ This works because there are four bytes in each of the \ LDY/STA instruction pairs below, and when the BCC is \ executed, the operand of the BCC is added to the \ address of the first instruction after the BCC \ \ In other words, this routine draws a line of length \ 31 - (A / 4) character columns LDA polygonFillPixels \ Set A to the contents of polygonFillPixels, which we \ set in part 1 to a pixel byte containing four pixels \ set to the fill colour for the polygon \ \ So the pixel byte we should use for drawing the line \ is in A CLC \ Clear the C flag so the BCC instruction below jumps to \ the correct entry to draw the specified number of \ character columns .dpol18 BCC dpol19 \ This instruction is modified in part 3 to jump to the \ correct entry in the following list for drawing a \ horizontal pixel line of the specified length \ \ The line length is specified in character columns (1 \ to 31) and the drawing starts with the pixel byte to \ the right of the pixel byte whose screen address is \ in (Q P), so (Q P) contains the left cap of the line \ and the line fills the polygon to the right for the \ specified number of character columns \ \ The line is drawn from right to left to enable the \ routine to be joined at the correct point for the \ required line length LDY #8 * 31 \ Draw a line from column 31 to column 1 STA (P),Y LDY #8 * 30 \ Draw a line from column 30 to column 1 STA (P),Y LDY #8 * 29 \ Draw a line from column 29 to column 1 STA (P),Y LDY #8 * 28 \ Draw a line from column 28 to column 1 STA (P),Y LDY #8 * 27 \ Draw a line from column 27 to column 1 STA (P),Y LDY #8 * 26 \ Draw a line from column 26 to column 1 STA (P),Y LDY #8 * 25 \ Draw a line from column 25 to column 1 STA (P),Y LDY #8 * 24 \ Draw a line from column 24 to column 1 STA (P),Y LDY #8 * 23 \ Draw a line from column 23 to column 1 STA (P),Y LDY #8 * 22 \ Draw a line from column 22 to column 1 STA (P),Y LDY #8 * 21 \ Draw a line from column 21 to column 1 STA (P),Y LDY #8 * 20 \ Draw a line from column 20 to column 1 STA (P),Y LDY #8 * 19 \ Draw a line from column 19 to column 1 STA (P),Y LDY #8 * 18 \ Draw a line from column 18 to column 1 STA (P),Y LDY #8 * 17 \ Draw a line from column 17 to column 1 STA (P),Y LDY #8 * 16 \ Draw a line from column 16 to column 1 STA (P),Y LDY #8 * 15 \ Draw a line from column 15 to column 1 STA (P),Y LDY #8 * 14 \ Draw a line from column 14 to column 1 STA (P),Y LDY #8 * 13 \ Draw a line from column 13 to column 1 STA (P),Y LDY #8 * 12 \ Draw a line from column 12 to column 1 STA (P),Y LDY #8 * 11 \ Draw a line from column 11 to column 1 STA (P),Y LDY #8 * 10 \ Draw a line from column 10 to column 1 STA (P),Y LDY #8 * 9 \ Draw a line from column 9 to column 1 STA (P),Y LDY #8 * 8 \ Draw a line from column 8 to column 1 STA (P),Y LDY #8 * 7 \ Draw a line from column 7 to column 1 STA (P),Y LDY #8 * 6 \ Draw a line from column 6 to column 1 STA (P),Y LDY #8 * 5 \ Draw a line from column 5 to column 1 STA (P),Y LDY #8 * 4 \ Draw a line from column 4 to column 1 STA (P),Y LDY #8 * 3 \ Draw a line from column 3 to column 1 STA (P),Y LDY #8 * 2 \ Draw a line from column 2 to column 1 STA (P),Y LDY #8 * 1 \ Draw a line from column 1 to column 1 STA (P),Y .dpol19 JMP dpol3 \ Loop back to part 2 to move on to the next pixel line \ in the polygon
Name: GetTileVisibility [Show more] Type: Subroutine Category: Drawing the landscape Summary: For each tile in the landscape, calculate whether the player can see that tile, to speed up the process of drawing the landscape
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainGameLoop calls GetTileVisibility

This routine sets a single bit in the tileVisibility table that records the visibility of that tile from the player's perspective (where 1 = visible, 0 = hidden). The position of the bit within the tileVisibility table is calculated in a rather complicated manner, presumably to make it harder to follow what's going on.
.GetTileVisibility LDA hyperspaceEndsGame \ If bit 7 of hyperspaceEndsGame is set then the game BMI tvis5 \ has ended because of a hyperspace, so jump to tvis5 \ to return from the subroutine \ \ This check appears to be unnecessary, as we only call \ GetTileVisibility from game2 in MainGameLoop, and we \ only reach game2 when bit 7 of hyperspaceEndsGame is \ clear, so this BMI will never be taken LDA #0 \ Set A = 0 to use when we zero the tileVisibility table STA drawingTableOffset \ Set drawingTableOffset = 0 so the call to first call \ to GetRowVisibility (for tile row 31) will populate \ the table at oddVisibility with the tile visibilities \ of the row being analysed LDX #127 \ We now zero the tileVisibility table, so set a counter \ in X for zeroing 128 bytes .tvis1 STA tileVisibility,X \ Zero the X-th byte of the tileVisibility table to \ indicate hidden tiles DEX \ Decrement the byte counter BPL tvis1 \ Loop back until we have zeroed both variables JSR GetTileAltitudes \ Calculate tile corner altitudes and maximum tile \ corner altitudes for each tile in the landscape \ \ This also sets the following: \ \ * (Q P) = &6000 \ \ * (S R) = &6020 \ \ So we have P = 0 and R = &20, ready to pass to the \ GetRowVisibility routine (which expects these values) LDA #31 \ Set zTileRow = 31 so the call to GetRowVisibility STA zTileRow \ analyses the back row of tiles JSR GetRowVisibility \ Calculate whether each tile corner in the rearmost row \ is obscured from the player by any intervening \ landscape, putting the results into 32 entries in the \ table at oddVisibility (as drawingTableOffset = 0) as \ follows: \ \ * %00000000 if the tile corner is not visible from \ the player's position \ \ * %11111111 if the tile corner is visible from the \ player's position DEC zTileRow \ Decrement zTileRow to move forward by one tile row .tvis2 LDA drawingTableOffset \ Flip drawingTableOffset between 0 and 32, so each EOR #32 \ call to GetRowVisibility alternates between storing STA drawingTableOffset \ the tile visibilities in oddVisibility and \ evenVisibility \ \ We EOR with 32 because: \ \ evenVisibility - oddVisibility = 32 \ \ So we can add the result in drawingTableOffset to \ oddVisibility to point to the correct table for \ storing the results JSR GetRowVisibility \ Calculate whether each tile corner in the row at \ zTileRow is obscured from the player by any \ intervening landscape, putting the results into 32 \ entries in the table at either oddVisibility or \ evenVisibility (depending on the parity of zTileRow) \ as follows: \ \ * %00000000 if the tile corner is not visible from \ the player's position \ \ * %11111111 if the tile corner is visible from the \ player's position \ \ So the current row is in either oddVisibility or \ evenVisibility and the previous row is in the other \ \ The call also sets (Q P) to the address of the table \ that contains the altitude and flatness data for tile \ corner row zTileRow, as follows: \ \ (Q P) = &6000 + (zTileRow 0) \ \ Each value in the table at (Q P) contains the flatness \ of the tile in bit 0 and the altitude of the tile \ corner in bits 1-4 LDX playerObject \ Set V to the high byte of the y-coordinate of the LDA yObjectHi,X \ player object STA V LDX #30 \ Set X = 30 to use as the column number, so we start \ iterating from the right, skipping the rightmost \ column as the tile corners in that column do not \ anchor any tiles (so X iterates from 30 to 0 in the \ following loop) .tvis3 TXA \ Set A to the X-th entry from (Q P), which contains the TAY \ altitude and flatness of the X-th tile in the row at LDA (P),Y \ zTileRow LDY #%11111111 \ Set Y to a bit mask containing all set bits, to use \ for flagging tiles as being visible in the calculation \ below LSR A \ Shift bit 0 into the C flag, so it contains the shape, \ and set A as the altitude of the tile BCS tvis4 \ If the tile is not flat then the C flag will be set, \ so jump to tvis4 with Y set to the bit mask for \ visible tiles CMP V \ If A <= V then the tile is at the same altitude or BCC tvis4 \ lower than the high byte of the player object's BEQ tvis4 \ altitude (which is the player's altitude rounded down \ to the nearest integer), so jump to tvis4 with Y set \ to the bit mask for visible tiles \ If we get here then the tile is flat and it is at a \ higher altitude than the player object, so INY \ Increment Y to zero to get a bit mask containing all \ clear bits, to use for flagging tiles as not being \ visible in the calculation below .tvis4 STY W \ Store the bit mask we just calculated in W \ We now store this tile's visibility as a single bit in \ in the tileVisibility table \ \ First we need to calculate the location of this tile's \ bit in the table, which we do by encoding the tile \ column number in X and row number in zTileRow into an \ offset into the table (in T) and a bit mask lookup \ (in Y) \ \ Presumably this calculation is complex to make it \ harder for people to work out what's going on (at \ least, I can't figure out why it's done this way) TXA \ X is the column number of the tile we are analysing, ASL A \ in the range 0 to 30 (%00000 to %11110), so shift that ASL A \ number into the top five bits of A and clear the last ASL A \ two bits, so that bits 5-7 contain bits 2-4 of the AND #%11100000 \ column number ORA zTileRow \ zTileRow is the row number of the tile we are LSR A \ analysing, again in the range 0 to 30 (%00000 to \ %11110), so put this into bits 0 to 4 of A and shift \ the whole thing to the right, so we lose bit 0 of the \ row number into the C flag (which we capture below) \ and bits 0 to 3 contain bits 1 to 4 of zTileRow STA T \ Set T to the value of A, so we now have the following \ in T: \ \ * Bit 7 is clear \ \ * Bits 4-6 contain bits 2-4 of the column number \ \ * Bits 0-3 contain bits 1-4 of the row number \ \ We have now encoded the tile number in T, which we can \ use as an index into the tileVisibility table when \ storing the tile's visibility TXA \ Set bits 0 to 2 of Y as follows: AND #%00000011 \ ROL A \ * Bit 0 contains bit 0 of the of the tile row (via TAY \ the C flag from above) \ \ * Bits 1-2 contain bits 0-1 of the column number LDA visibileBitMask,Y \ Set bitMask to a byte with all bits set except the EOR #%11111111 \ Y-th bit, counting from the left (so when Y is 0, bit STA bitMask \ 7 is clear, when Y is 1, bit 6 is clear and so on) LDA oddVisibility,X \ Set A to the combined visibility of the four tile ORA oddVisibility+1,X \ corners for the tile we are analysing, by OR'ing the ORA evenVisibility,X \ visibility bytes that we set for the four corners in ORA evenVisibility+1,X \ the call to GetRowVisibility: \ \ * oddVisibility+X contains the visibility for the \ tile's anchor (i.e. the X-th corner) \ \ * oddVisibility+X+1 contains the visibility for the \ corner to the right of the tile's anchor \ \ * evenVisibility+X contains the visibility for the \ corner behind the tile's anchor (i.e. the corner \ on the same x-coordinate but from the previous \ row's analysis) \ \ * evenVisibility+X+1 contains the visibility for the \ corner just to the right of the last one \ \ The GetRowVisibility routine sets visibility in the \ oddVisibility and evenVisibility tables as follows: \ \ * %00000000 if the tile corner is not visible \ \ * %11111111 if the tile corner is visible \ \ So OR'ing the four tile corners will give %11111111 if \ any of the tile corners are visible, and %00000000 if \ none of them are visible AND W \ Apply the W bit mask from above, which we set to \ %0000000 for tiles that are flat and above the level \ of the player, and %11111111 otherwise \ \ So this forces A to 0 (to indicate a hidden tile) for \ tiles that are flat and too high to be seen by the \ player, and leaves A alone for other tiles AND visibileBitMask,Y \ At this point A is either %00000000 for a hidden tile \ or %11111111 for a visible tile, so AND'ing it with \ the Y-th entry in visibileBitMask (which is a bit mask \ with all bits set except the Y-th bit, counting from \ the left) will clear all bits except the Y-th bit, \ which will be 0 for non-visible tiles and 1 for \ visible tiles STA U \ Store A in U, so U contains all clear bits apart from \ the Y-th bit, which contains the visibility of the \ tile we are processing LDY T \ Set A to the T-th byte in the tileVisibility table, LDA tileVisibility,Y \ which is where we want to store our visibility bit for \ the tile we are processing AND bitMask \ We set bitMask above to a byte with all bits set \ except the Y-th bit, counting from the left, which \ matches the bit that we used in U to store the tile's \ visibility bit, so this AND clears that bit in the \ byte we just fetched from the tileVisibility table ORA U \ U contains all clear bits apart from the Y-th bit, \ which contains the visibility of the tile we are \ processing, so this inserts that bit into the byte \ we just fetched from the tileVisibility table STA tileVisibility,Y \ Store the updated byte in the tileVisibility table so \ it contains the visibility bit for the tile in column \ X on row zTileRow DEX \ Decrement the column number to move left to the next \ tile in the row BPL tvis3 \ Loop back until we have processed all the tiles in the \ row DEC zTileRow \ Decrement zTileRow to move forward by one tile row BPL tvis2 \ Loop back until we have processed all the tile rows in \ the landscape .tvis5 RTS \ Return from the subroutine
Name: visibileBitMask [Show more] Type: Variable Category: Drawing the landscape Summary: A table for converting a number in the range 0 to 7 into a bit mask with only that bit set, when counting from the left
Context: See this variable on its own page References: This variable is used as follows: * GetTileViewAngles (Part 3 of 4) uses visibileBitMask * GetTileVisibility uses visibileBitMask
.visibileBitMask EQUB %10000000 EQUB %01000000 EQUB %00100000 EQUB %00010000 EQUB %00001000 EQUB %00000100 EQUB %00000010 EQUB %00000001
Name: GetRowVisibility (Part 1 of 2) [Show more] Type: Subroutine Category: Drawing the landscape Summary: Set up the calculations to determine whether each tile corner in a tile row is obscured from the player by any intervening landscape
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTileVisibility calls GetRowVisibility

This routine populates a specified table with the visibility of each tile corner in a row, populating the table with 32 entries as follows: * %00000000 if the tile corner is not visible from the player's position * %11111111 if the tile corner is visible from the player's position
Arguments: zTileRow The tile z-coordinate of the tile row to analyse drawingTableOffset Defines where we store the results of the analysis: * oddVisibility when drawingTableOffset = 0 * evenVisibility when drawingTableOffset = 32 P P is always zero, so (Q P) is of the form &xx00 R R is always &20, so (S R) is of the form &xx20
Returns: (Q P) The address of the table that contains the altitude and shape data for tile row zTileRow, like this: * (Q P) = &6000 + (zTileRow 0)
.GetRowVisibility LDA zTileRow \ Set the high byte of (Q P) to &60 + zTileRow CLC \ ADC #&60 \ This gives us the address where the GetTileAltitudes STA Q \ routine stored the altitude data for tile row zTileRow \ \ This works because we only ever call this routine with \ P = 0, and row zTile = 0 is at &6000, row zTile = 1 is \ at &6100 and so on LDA #31 \ Set xTileRow = 31 so we start iterating from the right STA xTileRow \ end of the current row (so xTileRow iterates from 31 \ to in the following loop) \ We now loop through each tile corner in the row, from \ right to left, in a loop that's split over part 1 and \ part 2 (and which has an anti-cracker interlude in the \ middle that has nothing to do with visibility checks) .rvis1 JSR ProcessSound \ Process any sounds or music that are being made in the \ background LDY #0 \ Set T = 0, so we can use it to capture the longest STY T \ axis in the vector calculation below DEY \ Set traceStepCounter = 255, so we start off by tracing STY traceStepCounter \ 255 steps (as the initial calculation calculates the \ vector to trace by dividing the full vector from the \ player to the tile corner by 256) \ \ We may reduce the number of steps below to make the \ process more efficient LDY xTileRow \ Set yTileRow to the altitude of the tile corner we are LDA (P),Y \ analysing LSR A \ STA yTileRow \ The value in the table at (Q P) contains the flatness \ of the tile in bit 0 and the altitude of the tile \ corner in bits 1-4, so we shift this value right by \ one place to extract the altitude LDX playerObject \ Fetch the cartesian coordinates of the player object JSR GetObjectCoords \ as three 24-bit numbers, as follows: \ \ xCoord(Hi Lo Bot) \ \ yCoord(Hi Lo Bot) \ \ zCoord(Hi Lo Bot) LDX #2 \ We now work through all three axes in turn, so set an \ axis counter in X to iterate through 2, 1 and 0 (for \ the z-axis, y-axis and x-axis respectively) \ \ The comments in the following loop will concentrate on \ the x-axis to keep things simple .rvis2 LDA #0 \ Zero the high byte of (xVector yVector zVector), which STA xVectorHi,X \ we will use to store the vector from the player to the \ tile corner SEC \ Set xVector(Lo Bot) = SBC xCoordLo,X \ ((xTileRow 0) - xCoord(Hi Lo)) / 256 STA xVectorBot,X \ LDA xTileRow,X \ So xVector(Lo Bot) contains the distance in the x-axis SBC xCoordHi,X \ from the player to the tile corner that we are STA xVectorLo,X \ analysing, divided by 256 \ \ The "divided by 256" part is subtle and important \ \ We want to divide the vector from the player to the \ tile corner into small steps, so we can move along the \ vector sequentially, checking on each step whether the \ vector is passing below ground level (in which case we \ will know that the line of sight from the player to \ the tile corner is obstructed by landscape) \ \ The above calculation subtracts two high byte/low byte \ 16-bit numbers in (xTileRow 0) and xCoord(Hi Lo) and \ puts the result into a low byte/bottom byte 16-bit \ number in xVector(Lo Bot) \ \ This is the same as dividing the result of the \ subtraction by 256, which we can see if we consider \ that this would be the normal high/low calculation: \ \ xVector(Hi Lo) = (xTileRow 0) - xCoord(Hi Lo) \ \ and we also have: \ \ xVector(Lo Bot) = xVector(Hi Lo) / 256 \ \ so we get: \ \ xVector(Lo Bot) \ = ((xTileRow 0) - xCoord(Hi Lo)) / 256 \ \ Also, it's worth noting that this is the vector from \ the player to the tile corner, because if you add this \ vector to the player's coordinates 256 times, then you \ get the tile corner's coordinates BPL rvis3 \ If the result is positive, jump to rvis3, with the low \ byte of the result (xVectorLo) in A DEC xVectorHi,X \ The result is negative, so set xVectorHi to %11111111 \ so it can be used as the high byte for the negative \ 24-bit number in xVector(Hi Lo Bot) LDA #0 \ Negate the result as follows (note that we ignore the SEC \ bottom byte, as we are only interested in the low byte SBC xVectorBot,X \ of the result): LDA #0 \ SBC xVectorLo,X \ (A *) = -xVector(Lo Bot) \ \ So this makes (A *) positive, to give this: \ \ (A *) = |xVectorLo xVectorBot| \ \ So the low byte of the result |xVectorLo| is in A .rvis3 \ By this point we have the following: \ \ * A = |xVectorLo| \ \ * xVectorHi is either 0 or %11111111, depending on \ the sign of xVector(Lo Bot) CMP T \ If A >= T then set T = A, so T keeps a record of the BCC rvis4 \ largest value of |xVectorLo| across all three axes STA T \ \ In other words, once we have finished looping through \ all three axes, T will contain the magnitude of the \ longest axis in the vector: \ \ T = max(|xVectorLo|, |yVectorLo|, |zVectorLo|) .rvis4 DEX \ Decrement the axis counter in X to move on to the next \ axis BPL rvis2 \ Loop back until we have processed all three axes \ At this point X is set to 255, which we use below when \ checking the secret entry code in CheckSecretStash LDA T \ If: ASL A \ ASL A \ T * 4 < 6 CMP #6 \ BCC rvis10 \ then: \ \ T < 1.5 \ \ and: \ \ max(|xVectorLo|, |yVectorLo|, |zVectorLo|) < 1.5 \ \ In other words, if this is true, then the longest axis \ of the vector from player to the tile corner is less \ than 1.5 (where each tile is of size 1), so the tile \ corner we are analysing is within 1.5 tile widths of \ the player \ \ We automatically mark all these close-by tiles as \ being potentially visible, so we jump to rvis10 with \ the C flag clear to store &FF as the visibility result \ (to record that this corner is potentially visible) \ The next step is an optimisation \ \ We could trace the vector from the player to the tile \ corner in steps of 256, but given that the landscape \ is only 32 tile corners across in each direction, this \ would mean that most of the time we would be stepping \ along the vector while staying above the same tile \ \ Instead we can scale up the size of our steps and do \ fewer of them, and as long as each individual step is \ smaller than a tile width, we can still check every \ landscape tile along the line of sight \ \ We therefore double the value in A, which contains the \ longest side of the vector, to be as high as possible \ while still fitting into one byte (which represents a \ fraction of a tile as it's the low byte of the vector) \ \ At the same time we double the size of the vector by \ doubling the following: \ \ xVector(Hi Lo Bot) \ \ yVector(Hi Lo Bot) \ \ zVector(Hi Lo Bot) \ \ though we don't actually need to shift the high bytes \ as we won't be shifting data out of the low bytes and \ they are either zero or %11111111 \ \ Because we have doubled the size of the vector, we \ also halve the number of steps in traceStepCounter \ \ This reduces the number of steps in the ray-tracing \ process as far as possible while keeping the size of \ each step to less than one tile (so that the tracing \ process still steps through each tile en route) .rvis5 ASL xVectorBot \ Double xVector(Lo Bot) to double the size of the ROL xVectorLo \ vector ASL yVectorBot ROL yVectorLo ASL zVectorBot ROL zVectorLo LSR traceStepCounter \ Halve traceStepCounter to halve the number of steps in \ the tracing process ASL A \ Shift A to the left to shift bit 7 into the C flag, to \ detect when A is as big as it can be within one byte BCC rvis5 \ Loop back to repeat the above scalings until we have \ scaled up the vector as much as we can \ So xVector(Hi Lo Bot) now contains the vector from the \ player to the tile corner, divided by 256 and scaled \ up as far as possible while still keeping the vector \ within the dimensions of a tile, and traceStepCounter \ contains the number of times this vector fits into the \ original vector from the player to the tile corner \ \ We can therefore trace the vector from the player to \ the tile corner by taking the player's coordinates and \ adding the xVector(Hi Lo Bot) vector traceStepCounter \ times, knowing that together, these steps will stop \ over every tile between the player and the tile corner \ in the process (we may stop over some tiles more than \ once, but that's OK) LDA zCoordHi \ Set zCoordHi = zCoordHi + &60 CLC \ ADC #&60 \ This sets zCoordHi to the high byte of the address for STA zCoordHi \ the extracted altitude data for the tile row on which \ the player is standing (as zCoordHi is the integer \ part of the player's z-coordinate, which is the number \ of the row containing the player) \ We now have a short interlude to check the secret code \ stash, as part of the game's anti-cracker code, and we \ pick up the tile visibility code in part 2
Name: CheckSecretStash [Show more] Type: Subroutine Category: Cracker protection Summary: Check whether the secret code stash is correctly set up, as part of the anti-cracker code
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.CheckSecretStash LDA doNotCheckSecret \ If bit 7 of doNotCheckSecret is set then jump down to BMI stas2 \ part 2 of GetRowVisibility to skip checking the secret \ code stash \ The following code does a check on the secret entry \ code for the current landscape to ensure that it \ matches the entered code in the keyboard input buffer \ \ If the check fails, then the game restarts by jumping \ to MainTitleLoop to display the title screen \ \ Specifically, the following code checks for four bytes \ in the secretCodeStash that correspond to the results \ of the comparisons made in the CheckSecretCode routine \ \ This ensures that crackers who manage to bypass the \ CheckSecretCode routine will find that the game \ restarts, unless they also disable this rather \ well-hidden check LDA stashOffset-255,X \ We know that X is 255 from the loop above, so this \ sets A = stashOffset \ \ The value of stashOffset is set in the SetSecretStash \ routine during the landscape drawing process, where it \ is set to a value that is unique and consistent for \ each individual landscape \ We now set stashAddr(1 0) to point to the four bytes \ in the secretCodeStash that correspond to the four \ comparisons we made for the secret entry code in the \ CheckSecretCode routine CLC \ Set stashAddr = A + 41 ADC #41 \ STA stashAddr \ So that's the low byte LDX #3 \ Set X = 3 so we can use it to count four bytes in the \ loop below (as well as in the following calculation) TXA \ Set stashAddr+1 = HI(secretCodeStash) - 3 + 3 CLC \ = HI(secretCodeStash) ADC #HI(secretCodeStash) - 3 STA stashAddr+1 \ So we now have the following: \ \ stashAddr(1 0) = secretCodeStash + stashOffset + 41 \ \ When the secretCodeStash gets populated in the \ CheckSecretCode routine, we add one byte for each \ iteration and comparison in the secret code generation \ process \ \ That process starts by performing 38 iterations and \ storing the results in the secretCodeStash from offset \ stashOffset to stashOffset + 37 \ \ It then generates the four BCD numbers that make up \ the secret code, storing the results in the stash from \ offset stashOffset + 38 to stashOffset + 41 \ \ (And it then generates one more result, but we ignore \ that) \ \ So stashAddr(1 0) points to the last of those bytes in \ the secretCodeStash, i.e. the byte at stashOffset + 41 \ \ The value that is stashed in the secretCodeStash is \ the result of subtracting the entered code from the \ generated code, which will be zero if they match, and \ then %01111111 is added to the result (%01111111 being \ the object flags for the Sentinel, which is all part \ of the obfuscation of this process) \ \ So if the secretCodeStash contains %01111111, this \ means that particular byte matched, so if all four \ bytes at offset stashOffset + 38 to stashOffset + 41 \ equal %01111111, this means the secret code was deemed \ correct by CheckSecretCode LDY #0 \ Set Y = 0 so we can fetch a value from the address in \ stashAddr(1 0) in the following (we don't change its \ value) \ We use X as the loop counter to work through all four \ bytes, as we set it to 3 above .stas1 LDA (stashAddr),Y \ Fetch the contents of address stashAddr(1 0) CMP #%01111111 \ If it does not match %01111111 then this byte from the BNE talt2 \ secret code was not matched by the CheckSecretCode \ routine (so it must have been bypassed by crackers), \ so jump to MainTitleLoop via talt2 to restart the game DEC stashAddr \ Decrement stashAddr(1 0) to point to the previous byte \ in memory (we decrement as we initialised stashAddr \ above to point to the last result byte in memory, so \ this moves on to the next of the four bytes) DEX \ Decrement the loop counter BPL stas1 \ Loop back until we have checked all four secret code \ bytes \ The four code bytes have now been checked, but we have \ one more check to do, that of the comparison just \ before the four bytes \ \ This comparison would have been between inputBuffer+4 \ and a BCD number from the landscape's sequence of seed \ numbers \ \ When the landscape code is entered, it is converted \ into four BCD numbers in inputBuffer, and the rest of \ the buffer is padded out with &FF, so inputBuffer+4 \ contains &FF at the point of comparison \ \ &FF is not a valid BCD number, so it can never match a \ BCD number from the landscape's sequence of seed \ numbers, so we know that this comparison can never \ have matched \ \ So if stashAddr(1 0) contains %01111111 to indicate a \ match, then we know that the stash has been modified \ by a cracker, so we restart the game LDA (stashAddr),Y \ Fetch the contents of address stashAddr(1 0) CMP #%01111111 \ If it matches %01111111 then we know the stash has BEQ talt2 \ been compromised, so jump to MainTitleLoop via talt2 \ to restart the game SEC \ Set bit 7 of doNotCheckSecret so we do not repeat the ROR doNotCheckSecret \ secret code check again (at least, until we reach the \ next landscape) .stas2 \ Fall through into part 2 of GetRowVisibility to \ continue with the visibility calculations
Name: GetRowVisibility (Part 2 of 2) [Show more] Type: Subroutine Category: Drawing the landscape Summary: Calculate whether each tile corner in a tile row is obscured from the player by any intervening landscape
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
\ In part 1 we set up xVector(Hi Lo Bot) with the vector \ from the player to the tile corner, split up into \ traceStepCounter steps, each of which is smaller than \ the size of a tile \ \ We can therefore trace the vector from the player to \ the tile corner by taking the player's coordinates and \ adding the vector: \ \ [ xVector(Hi Lo Bot) ] \ [ yVector(Hi Lo Bot) ] \ [ zVector(Hi Lo Bot) ] \ \ traceStepCounter times, knowing that together, these \ steps will stop over every tile between the player and \ the tile corner in the process (we may stop over some \ tiles more than once, but that's OK) \ \ I'll refer to this vector as the player's gaze, as it \ represents the line-of-sight from the player to the \ tile corner we are analysing \ \ So now we step along the player's gaze, checking on \ each step whether the gaze is passing over a tile that \ has a high enough altitude to block the player's view \ of the tile corner we are analysing .rvis6 LDX #2 \ We now work through all three axes in turn, so set an \ axis counter in X to iterate through 2, 1 and 0 (for \ the z-axis, y-axis and x-axis respectively) \ \ The comments in the following loop will concentrate on \ the x-axis to keep things simple CLC \ Clear the C flag so the initial addition at rvis8 will \ start properly BCC rvis8 \ Jump into the following loop at rvis8 so we start off \ that calculations with the z-axis and skip the bottom \ byte .rvis7 LDA xCoordBot,X \ For the y-axis calculation, we include the bottom byte ADC xVectorBot,X \ for more accuracy, so we calculate the following: STA xCoordBot,X \ \ xCoord(Hi Lo Bot) += xVector(Hi Lo Bot) .rvis8 \ This is where we join the loop to add the next step to \ the vector containing the current position along the \ player's gaze towards the tile corner we're analysing LDA xCoordLo,X \ Set xCoord(Hi Lo) += xVector(Hi Lo) ADC xVectorLo,X STA xCoordLo,X LDA xCoordHi,X ADC xVectorHi,X STA xCoordHi,X \ The calculation above adds the scaled vector from the \ player to the tile corner, which is in xVector, to the \ player's object coordinate, which is in xCoord \ \ So this calculation effectively follows the player's \ gaze towards the tile corner, adding one of the steps \ to the vector x-coordinate in xVector(Hi Lo) \ \ We repeat this for all three axes, calculating the \ x- and z-axes with 16-bit accuracy, and the y-axis \ with 24-bit accuracy (as y-coordinates are stored in \ this way) CLC \ Clear the C flag so the following additions work DEX \ Decrement the axis counter BEQ rvis8 \ If we are just about to calculate the x-axis, jump \ back to rvis8 to skip the bottom byte BPL rvis7 \ If we are about to calculate the y-axis, jump back to \ rvis7 to include the bottom byte \ We now work out whether this step has made the \ player's gaze pass over a tile that is high enough to \ block the player's view of the tile corner that we are \ heading for LDA zCoordHi \ Set the top byte of (S R) to the high byte of the STA S \ z-coordinate of our current step along gaze vector \ \ Because we added &60 to the starting z-coordinate in \ part 1, this will point to the &xx20 table of altitude \ data for the tile row over which the gaze is passing \ \ The GetTileAltitude routine populates the &xx20 table \ with the altitude of the highest tile corner for each \ tile, so (S R) now points to this data for the row \ over which we are passing LDY xCoordHi \ Set Y to the high byte of the x-coordinate of our \ current step along gaze vector, which is the tile \ x-coordinate of the tile that the gaze vector is \ currently passing over \ \ This means that (S R) + Y will contain the altitude \ of the highest tile corner for the tile that we are \ passing over LDA yCoordHi \ Set A to the high byte of the y-coordinate of our \ current step along gaze vector CMP (R),Y \ If yCoordHi < (S R) + Y then the altitude of the gaze BCC rvis9 \ vector is lower than the altitude of the highest point \ of the landscape on the tile over which we are \ passing, which means the gaze vector has hit the \ landscape before reaching the end \ \ So jump to rvis9 to record that this tile corner is \ not visible from the player's perspective, as it is \ hidden behind a higher part of the landscape between \ the player and the tile corner DEC traceStepCounter \ Otherwise decrement the step counter to keep moving \ along the vector BNE rvis6 \ If we haven't yet done all the steps, loop back to \ rvis6 to do the next step in the ray-trace \ If we get here then we have finished the ray-tracing \ process and we didn't bump into any tiles on the way, \ so the tile corner is not obscured by the landscape \ and is therefore deemed to be visible CLC \ Clear the C flag so we store %11111111 in the \ visibility table to indicate that this tile corner is \ visible BCC rvis10 \ Jump to rvis10 to store the result (this BCC is \ effectively a JMP as the C flag is always clear) .rvis9 SEC \ Set the C flag so we store %00000000 in the visibility \ table to indicate that this tile corner is not visible .rvis10 LDA xTileRow \ Set Y = drawingTableOffset + xTileRow ORA drawingTableOffset \ TAY \ The value of drawingTableOffset is either 0 or 32, so \ this creates an index in Y that we can use to store \ the result in either oddVisibility (when it is 0) or \ evenVisibility when it is 32) LDA #0 \ Set A = 0 - 0 - (1 - C) SBC #0 \ = C - 1 \ \ so this sets: \ \ * A = %00000000 if the C flag is set (not visible) \ \ * A = %11111111 if the C flag is clear (visible) STA oddVisibility,Y \ Store the result in either the oddVisibility or \ evenVisibility table, as determined by the value of \ drawingTableOffset DEC xTileRow \ Decrement xTileRow to move left along the tile row we \ are analysing BMI rvis11 \ If we have already processed the leftmost tile then \ jump to rvis11 to return from the subroutine JMP rvis1 \ Otherwise jump back to part 1 to analyse the next tile \ to the left .rvis11 RTS \ Return from the subroutine