22309 lines
1.5 MiB
22309 lines
1.5 MiB
; ============================================================================
|
|
; $0800-$6EFF - main game program (tracks 22-27, sectors 0-17 each)
|
|
; ============================================================================
|
|
; Entry point $0800 (JMP $0A6C). Contains the loader API, start-up sequence, raster interrupt, input
|
|
; handling, the battlefield and console screens, the unit simulation and the command interpreter.
|
|
|
|
.setcpu "6502"
|
|
.include "c64.inc"
|
|
.include "zeropage.inc"
|
|
|
|
; ---- references to code/data outside this file ----
|
|
D_0104 := $0104
|
|
bamDiskIdChar0 := $02A2
|
|
bamDiskIdChar1 := $02A3
|
|
bamDiskSerial := $02AB
|
|
basicVectorHijack := $0300
|
|
checksumXorConstant := $0400
|
|
mapNameSeedTable := $0401
|
|
play3Col28 := $0410
|
|
play3Col44 := $0420
|
|
play3Row10 := $0430
|
|
play3Row12 := $0432
|
|
play4Grp12 := $0464
|
|
play4Row12 := $04C8
|
|
play4Row13 := $04C9
|
|
play4Row20 := $04D0
|
|
play4Row21 := $04D1
|
|
play4Row35 := $04DF
|
|
play4Row43 := $04E7
|
|
chatMessageLine := $0548
|
|
D_0549 := $0549
|
|
switchToRequestedScreen := $06BA
|
|
runComcenStunnedBlackout := $0709
|
|
D_077E := $077E
|
|
unitGlyphBaseTable := $0799
|
|
D_07BE := $07BE
|
|
terrainColourTable := $07EA
|
|
generateBattlefieldMap := $6F04
|
|
applyDroneBlast := $72A1
|
|
steerDrone := $731E
|
|
drawRadarGlyph := $7396
|
|
getRadarCellGlyph := $74C3
|
|
radarSideBranchOpcode := $750A
|
|
missileLineFromComcen := $7717
|
|
clearMapNameField := $7836
|
|
startGameSession := $78D1
|
|
scrollRadarOnComcenMove := $7959
|
|
mapCellToRadarGrid := $7A13
|
|
isRadarGridCellInRange := $7A2B
|
|
redrawRadarMapWindow := $7A6B
|
|
waitForSpaceKey := $7DB8
|
|
handleMainMenuChoice := $7DE3
|
|
getDroneMapPosition := $81FE
|
|
showDroneAlert := $820F
|
|
fillDroneViewWindow := $821F
|
|
drawShotLineInViewport := $82BF
|
|
drawStatsTab := $8518
|
|
drawComcenSchematic := $8632
|
|
handleStarKeyToggle := $8658
|
|
filmViewWordTable := $86FB
|
|
msgShowViewWord := $8726
|
|
msgSpeedDigit := $8730
|
|
overlayVariantFlag := $87FF
|
|
sprite0Shape := $8A00
|
|
sprite1Shape := $8A40
|
|
sprite2Shape := $8A80
|
|
sprite2ShapeRow7 := $8A95
|
|
sprite3Shape := $8AC0
|
|
sprite4Shape := $8B00
|
|
D_8C00 := $8C00
|
|
D_8D00 := $8D00
|
|
D_8E00 := $8E00
|
|
D_8F00 := $8F00
|
|
vicCtrl1Shadow := $9000
|
|
vicCtrl2Shadow := $9001
|
|
vicMemPtrShadow := $9002
|
|
vicBorderShadow := $9003
|
|
vicBg0Shadow := $9004
|
|
vicBg1Shadow := $9005
|
|
vicBg2Shadow := $9006
|
|
spriteColourShadow := $9008
|
|
sprite1ColourShadow := $9009
|
|
sprite2ColourShadow := $900A
|
|
sprite3ColourShadow := $900B
|
|
sprite4ColourShadow := $900C
|
|
spriteXShadow := $9010
|
|
sprite1XShadow := $9011
|
|
sprite2XShadow := $9012
|
|
sprite3XShadow := $9013
|
|
sprite4XShadow := $9014
|
|
spriteYShadow := $9018
|
|
sprite1YShadow := $9019
|
|
sprite2YShadow := $901A
|
|
sprite3YShadow := $901B
|
|
sprite4YShadow := $901C
|
|
spriteXMsbShadow := $9020
|
|
spriteEnableShadow := $9022
|
|
spriteXExpandShadow := $9023
|
|
spriteYExpandShadow := $9024
|
|
spriteBuildBuffer := $9025
|
|
outgoingCmdRing := $9065
|
|
outgoingCmdRingEnd := $908C
|
|
outgoingCmdWriteIndex := $908D
|
|
outgoingCmdReadIndex := $908E
|
|
mergedCmdBuffer := $908F
|
|
mergedCmdReadIndex := $90B7
|
|
glyphBuildBuffer := $90B8
|
|
prevKeyCode := $90E9
|
|
newKeyEvent := $90EA
|
|
inputIdleFrames := $90EE
|
|
textCentreWidth := $90EF
|
|
lastStepRowDelta := $90F4
|
|
lastStepColDelta := $90F5
|
|
drawCellSaveCol := $90F6
|
|
drawCellSaveRow := $90F7
|
|
selectedUnit := $90F8
|
|
menuTabIndex := $90F9
|
|
currentUnit := $90FA
|
|
currentScreen := $90FB
|
|
stepDelayTimer := $90FC
|
|
unitProcessOrder := $90FD
|
|
unitOrderIndex := $9162
|
|
linkPollDelay := $9163
|
|
unitBaseOrder := $9164
|
|
shotLineTimer := $91C8
|
|
joyRepeatMask := $91C9
|
|
joyHoldCounter := $91CA
|
|
gameClock := $91CB
|
|
activeSpriteIndex := $91CC
|
|
menuItemIdList := $91CE
|
|
menuItemCount := $91D4
|
|
menuSelectedItem := $91D5
|
|
menuFirstRow := $91D6
|
|
infoPanelUnit := $91D7
|
|
pickedCellCol := $91D8
|
|
pickedCellRow := $91D9
|
|
currentUnitCol := $91DA
|
|
currentUnitRow := $91DB
|
|
boomerTargetTable := $91DC
|
|
fireTargetUnit := $91F0
|
|
fireTargetDistance := $91F1
|
|
circleStepsLeft := $91F2
|
|
spritePlotMode := $91F3
|
|
groupStatusTable := $91F4
|
|
highlightedUnit := $9204
|
|
highlightedGroupKey := $9205
|
|
selectedGroupKey := $9206
|
|
unusedFlag9207 := $9207
|
|
lastShownClock := $9208
|
|
uiLevel := $9209
|
|
groupBoxActive := $920A
|
|
droneHeading := $920B
|
|
droneControlFlags := $920D
|
|
exchangeAppliedFlag := $920E
|
|
frameWidth := $9221
|
|
frameHeight := $9222
|
|
linkTimeoutCounter := $9223
|
|
cellBackgroundColour := $9226
|
|
scanUnitTerrain := $9227
|
|
scanUnitType := $9228
|
|
scanUnitTerrainHeight := $9229
|
|
comcenStunStatus := $922A
|
|
overviewUnitPattern := $922B
|
|
currentUnitCloakBits := $922C
|
|
groupBoxRightOffset := $922E
|
|
groupBoxLeftOffset := $922F
|
|
groupBoxBottomOffset := $9230
|
|
groupBoxTopOffset := $9231
|
|
viewRedrawTimer := $9234
|
|
comcenUnit := $9236
|
|
radarOwnUnitColour := $9237
|
|
radarEnemyUnitColour := $9238
|
|
radarTerrainColour := $9239
|
|
pendingRedrawGroup := $923A
|
|
pendingRedrawUnit := $923B
|
|
commandSliceIndex := $923C
|
|
attackerType := $923D
|
|
markerSpriteTimer := $923E
|
|
gameOverFlags := $923F
|
|
hitDamage := $9240
|
|
comcenSystemStatus := $9241
|
|
comcenBattleStatus := $9242
|
|
comcenRadarStatus := $9243
|
|
comcenDroneStatus := $9244
|
|
comcenCloakedFlag := $9245
|
|
rleMarkerByte := $9246
|
|
rleUnpackFlag := $9247
|
|
lastTypedKey := $9248
|
|
messageQueue := $9249
|
|
messageQueueHead := $9251
|
|
messageQueueTail := $9252
|
|
incomingChatBuffer := $9253
|
|
incomingChatScrollSrc := $9254
|
|
incomingChatLength := $9277
|
|
typedLineBuffer := $9278
|
|
chatState := $929C
|
|
lastOrderedGroupKey := $929D
|
|
pendingGroupOrderCount := $929E
|
|
pendingOwnCmdCount := $929F
|
|
rawKeyCode := $92A0
|
|
persistentMessageId := $92A1
|
|
sideScoreLo := $92A2
|
|
side1ScoreLo := $92A3
|
|
sideScoreHi := $92A4
|
|
side1ScoreHi := $92A5
|
|
recyclerMode := $92A6
|
|
recyclerCol := $92A8
|
|
recyclerRow := $92AA
|
|
terrainPointsBySide := $92AC
|
|
dronesLeft := $92AE
|
|
missilesLeft := $92B0
|
|
comcenRevealTimer := $92B2
|
|
comcenSpeedClass := $92B4
|
|
opponentComcenSpeedClass := $92B5
|
|
exchangeActivityFlag := $92C2
|
|
savedViewOriginCol := $92C3
|
|
savedViewOriginRow := $92C4
|
|
menuBlinkState := $92C5
|
|
manualCheckTries := $92C7
|
|
screenShakeTimer := $92C8
|
|
gameEndCountdown := $92C9
|
|
comcenStatusSnapshot := $92D6
|
|
stepIntervalFrames := $92E3
|
|
shiftHeldFlag := $92E4
|
|
commodoreHeldFlag := $92E5
|
|
filmNameLength := $92E6
|
|
mergedCmdLength := $92EB
|
|
mergedCmdSplitIndex := $92EC
|
|
mergedCmdOwnerFlag := $92ED
|
|
currentCommandLength := $92EE
|
|
lastQuarterRound := $92EF
|
|
diskIdIndex := $92F0
|
|
filmEndPtr := $92F1
|
|
filmEndPtrHi := $92F2
|
|
setupToggleFlag := $92F3
|
|
spriteScrollFineX := $92F4
|
|
spriteScrollFineY := $92F5
|
|
droneViewOriginCol := $92F6
|
|
droneViewOriginRow := $92F7
|
|
missileFlightTick := $92F8
|
|
droneArrivalFlags := $92F9
|
|
lastQuarterFlag := $92FA
|
|
radarSweepTick := $92FB
|
|
revealAllUnits := $92FC
|
|
defaultMainMenuItem := $92FD
|
|
soloTrainerAtGameStart := $92FE
|
|
bitmapRowLoTable := $9300
|
|
bitmapRowLoTableFrom3 := $9303
|
|
bitmapRowHiTable := $9319
|
|
bitmapRowHiTableFrom3 := $931C
|
|
bitmapColLoTable := $9332
|
|
bitmapColLoTableFrom3 := $9335
|
|
bitmapColHiTable := $935A
|
|
bitmapColHiTableFrom3 := $935D
|
|
screenRowLoTable := $9382
|
|
screenRowLoTableFrom3 := $9385
|
|
screenRowHiTable := $939B
|
|
screenRowHiTableFrom3 := $939E
|
|
fontColorTable := $93B4
|
|
titleColorRow00Cell16 := $9810
|
|
windowFrameTileSets := $9824
|
|
windowFrameTileSetRaised := $982D
|
|
windowFrameTileSetOctagon := $9836
|
|
windowFrameTileSetPanel := $983F
|
|
windowFrameTileSetSolid := $9848
|
|
windowFrameTileSetBlank := $9851
|
|
windowFrameTileSetDither := $985A
|
|
windowFrameTileSetBevelA := $9863
|
|
windowFrameTileSetBevelB := $986C
|
|
windowFrameTileSetBevelC := $9875
|
|
titleScreenRow16Col18 := $9E92
|
|
titleScreenRow16Col36 := $9EA4
|
|
titleScreenRow20Col02 := $9F22
|
|
titleScreenRow20Col23 := $9F37
|
|
titleScreenRow20Col33 := $9F41
|
|
titleScreenRow21Col03 := $9F4B
|
|
titleScreenRow21Col13 := $9F55
|
|
titleScreenRow21Col23 := $9F5F
|
|
mapRowAddrLoTable := $BFB0
|
|
mapRowAddrHiTable := $BFD8
|
|
bootEntry := $C000
|
|
bootReadSectorLoop := $C008
|
|
printChar := $C01A
|
|
restoreRegsAXY := $C030
|
|
printNewline := $C041
|
|
bootDriveDataBit := $C06A
|
|
bootWaitClkAck := $C075
|
|
setCursorRow := $C097
|
|
updateCursorPointers := $C099
|
|
setFullScreenWindow := $C0D7
|
|
saveRegsAXY := $C0E8
|
|
bootReceiveSectorData := $C0EF
|
|
printTableEntry := $C100
|
|
printStringCentred := $C121
|
|
printNextStringCentred := $C12B
|
|
printString := $C133
|
|
printStringAtPointer := $C137
|
|
printDecimal := $C17D
|
|
D_C22B := $C22B
|
|
plotGlyphDoubleSize := $C24B
|
|
waitFrames := $C37F
|
|
leftoverSetC397 := $C397
|
|
clearMessageLine := $C39F
|
|
showMessageString := $C3D8
|
|
advanceFillPointer := $C3F9
|
|
restoreTextWindow := $C40E
|
|
saveTextWindow := $C41B
|
|
checkUiContinue := $C451
|
|
checkSetupUiContinue := $C470
|
|
updateStatusMessage := $C4EF
|
|
queueMessageUnlessFilm := $C560
|
|
queueMessage := $C566
|
|
redrawStatusMessage := $C5D7
|
|
clearChatStatusLine := $C62A
|
|
resetMessageQueue := $C6EF
|
|
showStatusMessage := $C6FF
|
|
handlePauseKey := $C70A
|
|
handleTextEntryKey := $C7D5
|
|
showWorkingMessage := $C853
|
|
promptPressButtonWhenReady := $C85B
|
|
exitIfGameEnded := $C86D
|
|
playSound := $C89A
|
|
requestSound := $C8A8
|
|
dequeueSoundRequest := $C8E9
|
|
updateComcenMoveSound := $C8FA
|
|
setMessageSlot := $C924
|
|
unitPictureHiTable := $CD00
|
|
showUnitPictureSprites := $CD0A
|
|
D_D900 := $D900
|
|
D_DA00 := $DA00
|
|
D_DB00 := $DB00
|
|
commJumpTable := $E000
|
|
commLinkControl := $E003
|
|
getLinkByte := $E006
|
|
putLinkByte := $E009
|
|
pollLinkStatus := $E00C
|
|
isOriginateMode := $E011
|
|
D_E013 := $E013
|
|
D_E014 := $E014
|
|
commKeyEntry := $E015
|
|
exchangeFlags := $E01D
|
|
rxPacketLength := $E01E
|
|
txPacketLength := $E01F
|
|
rxPacketBuffer := $E020
|
|
rxPacketArg1 := $E021
|
|
txPacketBuffer := $E027
|
|
txPacketArg1 := $E028
|
|
hostOutCount := $E02E
|
|
commBuildId := $E030
|
|
nmiChainVector := $E031
|
|
nmiChainVectorHi := $E032
|
|
ciaIcrShadow := $E033
|
|
nmiSuspendRequest := $E039
|
|
nmiSuspendAck := $E03A
|
|
isLinkActive := $E03B
|
|
linkStatus := $E03C
|
|
linkStatusSample := $E03D
|
|
runTrainerRoundHook := $E079
|
|
trainerUnitStepHook := $E0E8
|
|
handleDisplayToggleKey := $EEB9
|
|
unitColTable := $F640
|
|
unitFlagsTableAlias := $F688
|
|
unitRowTable := $F6A4
|
|
unitTypeTableAlias := $F6EC
|
|
unitFlagsTable := $F708
|
|
unitTypeTable := $F76C
|
|
unitDestColTable := $F7D0
|
|
unitPathColTableAlias := $F818
|
|
unitDestRowTable := $F834
|
|
unitPathColTable := $F898
|
|
unitPathRowTable := $F8FC
|
|
unitEnergyTableAlias := $F944
|
|
unitStunTable := $F960
|
|
unitEnergyTable := $F9C4
|
|
unitMovePointsTable := $FA28
|
|
unitTerrainUnderAlias := $FA70
|
|
unitHitFlashTable := $FA8C
|
|
unitTerrainUnderTable := $FAF0
|
|
unitDisplayFlagsTable := $FB54
|
|
initLineStepper := $FBB8
|
|
stepLine := $FBFA
|
|
multiply8x8 := $FCCD
|
|
divideSigned16by8 := $FCEC
|
|
nextScenarioRandom := $FD4A
|
|
nextGameRandom := $FD5D
|
|
ownPlayerName := $FFDE
|
|
opponentPlayerName := $FFE8
|
|
nmiVector := $FFFA
|
|
nmiVectorHi := $FFFB
|
|
irqVector := $FFFE
|
|
irqVectorHi := $FFFF
|
|
|
|
; Contents
|
|
; --------
|
|
; $0800 gameEntry the game's front door.
|
|
; $0804 loadSectors the game's own copy of the fast loader.
|
|
; $0843 writeSectors the write half of the fast loader: sends A consecutive sectors from the address
|
|
; set by setLoadDest to track Y starting at sector X, one page per sector.
|
|
; $085C setLoadDest names the memory the next transfer works on.
|
|
; $0863 writeOneSector writes a single sector: command $60, then the track and the sector number, then
|
|
; the 256 bytes.
|
|
; $088F sendSectorData pushes the 256 bytes at (wrSrcPtr) to the drive one handshaked byte at a time,
|
|
; then falls into finishTransfer to read back the status byte.
|
|
; $089C sendByteToDrive the slow, fully handshaked, full-duplex byte exchange with the drive; the other
|
|
; end is exchangeByteWithHost ($034B) in the drive code.
|
|
; $08EB readOneSector reads one sector into the load destination.
|
|
; $08FE readSectorBody the body of a sector read.
|
|
; $0943 finishTransfer shared tail of a read and a write: puts the sprite enable register back the way
|
|
; it was and collects the drive's status byte, which arrives on the ordinary handshaked path.
|
|
; $0950 receiveSectorData the fast half of the protocol and the only cycle-counted code in the game.
|
|
; $09A6 manualLookupCheck the manual look-up copy protection, and it is disabled in this build: the JSR
|
|
; that called it at $0A9B was assembled as BIT ($2C instead of $20), so it is never entered.
|
|
; $0A6C mainStart the cold start of the game, run once.
|
|
; $0AA3 returnToMainMenu the panic exit back to the main menu, used whenever the game has to abandon
|
|
; whatever it was doing: the end of a game, a lost link, the SHIFT + C= + F7 abort in the raster IRQ, and the
|
|
; fall-through from mainStart.
|
|
; $0AC8 startGameFromSetup the hand-over from the setup screens to the battle, entered by JMP from the
|
|
; setup overlay ($7E3F) once both sides have agreed on a game.
|
|
; $0B4F loadCommTailBuild1 not a routine of its own: the tail of startGameFromSetup, entered with X/Y
|
|
; already set at $0B4B.
|
|
; $0BB5 initIrqAndInputState brings the interrupt and input state back to a known point.
|
|
; $0BD8 disableCiaInterrupts masks every interrupt source in both CIAs and acknowledges whatever was already
|
|
; pending.
|
|
; $0BE7 initInterruptVectors finishes the interrupt set-up: it points the BRK escape of the raster handler
|
|
; at the normal IRQ exit and parks both the hardware NMI vector and the comm module's private copy of it on the bare RTI
|
|
; at $1298.
|
|
; $0C02 setNmiVectorRti points the hardware NMI vector at the bare RTI at $1298.
|
|
; $0C0D jumpToCd00IfPatched dead code.
|
|
; $0C1C resetGameVariables wipes the game's variables back to a cold state: zero page $15-$7F and $8C-$FF,
|
|
; the whole variable block $9008-$92FF, the eight-entry message queue and the last-key bytes.
|
|
; $0C66 installRasterIrq installs rasterIrqHandler as the interrupt routine.
|
|
; $0C8C showModemSetupPrompt puts a caller-supplied line above 'PRESS SPACE WHEN READY.' and waits for the
|
|
; space bar.
|
|
; $0CA0 resetMessagesAndBeep clears the message queue, shows message slot $20 immediately, beeps twice a
|
|
; third of a second apart and makes slot $21 the line the status bar falls back to once slot $20 has had its time.
|
|
; $0CBD waitForAnswerOriginateKey the ANSWER/ORIGINATE question at the start of a modem game.
|
|
; $0CE8 buildOwnerNameString assembles the 21-character line that names the two players on the menu panel,
|
|
; out of the two 10-byte name fields at the top of memory: ownPlayerName ($FFDE), copied out of the disk's BAM by
|
|
; checkDiskId, and opponentPlayerName ($FFE8), which the modem driver fills in from the other machine.
|
|
; $0D49 setJoystickRepeatDelay the joystick auto-repeat used by every menu and by the map scrolling.
|
|
; $0DB7 scanKeyboard the game's own keyboard matrix scanner; there is no KERNAL to ask, since the
|
|
; ROMs are banked out.
|
|
; $0E45 testKeyMask tests one named key for scanKeyboard: it selects the row given in Y, reads the
|
|
; columns twice until two reads agree, and masks out the single column bit the caller passed in A.
|
|
; $0E56 pollKeyboardEvent the keyboard front end, run once per frame from the raster IRQ (and only from
|
|
; there).
|
|
; $0EB1 checkFirePressed the game's 'has the player clicked?' primitive; almost every interactive loop
|
|
; spins on it with BNE.
|
|
; $0ED5 checkFirePressedInteractive checkFirePressed plus a 'may the player act at all?' gate, used by the loops
|
|
; that turn a click into an order.
|
|
; $0EE5 isPlayerInputAllowed the gate half of checkFirePressedInteractive without the button test: it jumps
|
|
; into that routine at $0EDA with A = 0, so it answers 'may the local player still act?' - Z=1 when no film is being
|
|
; watched and the game has not ended.
|
|
; $0EEA isFireHeld the level-triggered version of checkFirePressed: no ring-buffer check, no
|
|
; debounce reload and no click sound, so it keeps answering 0 for as long as the button is held (once it has been seen
|
|
; released).
|
|
; $0EFE stopAllSound Silences every SID voice by requesting sound id 0, which requestSound turns
|
|
; into
|
|
; $0F03 loadStartupOverlays One-shot start-up loader: fast-loads track 30 sectors 0-15 into $C000-$CFFF
|
|
; $0F16 loadCommModule Loads the opponent module behind the $E000 jump table and initialises it.
|
|
; $0F31 loadCommBuild2 Loads the modem-driver build of the comm module with interrupts off.
|
|
; $0F41 loadCommFinish Shared tail of the two comm-module load paths: does the final loadSectors with
|
|
; the
|
|
; $0F44 initCommModule Cold-initialises the freshly loaded comm module and grafts the game's own
|
|
; keyboard
|
|
; $0F55 loadSectorsDelayed Wrapper around loadSectors that first burns 8 raster frames through the $C000
|
|
; $0F5F suspendCommModule Asks the comm module to pause before the game touches the disk: the fast loader
|
|
; $0F71 resumeCommModule Counterpart of suspendCommModule: when the link is up it clears the request
|
|
; byte
|
|
; $0FB1 checkDiskId Verifies which disk is in the drive.
|
|
; $1018 diskErrorExit Loads the address of msgDiskError ('DISK ERROR.') and falls into
|
|
; $101C showDiskMessageAndFail Common failure exit of the disk routines: error beep, put the message on
|
|
; $1030 ensureOverlayAResident Makes sure the $6F00-$87FF overlay variant A (map generator, setup and menu
|
|
; $1047 loadOverlayA Reads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00-$7FFF
|
|
; and
|
|
; $1060 loadStringsAndCommTail Reads the modem status-message string block (track 29 sectors 9-10) into
|
|
; $1070 loadCommTailBuild2 Reads the last page group of the modem driver (track 34 sectors 12-15) into
|
|
; $1080 loadOverlayBIfNotResident Loads the $6F00 overlay variant B only when it is not already in memory.
|
|
; $1086 loadOverlayB Reads the $6F00-$87FF overlay variant B (the comcen missile and drone screens):
|
|
; track
|
|
; $10AB loadUnitTemplatesAndSetup Reads track 18 sectors 8-14 into $F000-$F6FF, then falls into
|
|
; $10BB loadSetupBlock Reads the saved setup block (track 18 sectors 15-16) into $0200-$03FF: the
|
|
; terrain
|
|
; $10CB suspendCommForDiskAccess Standard prologue for disk access: pause the comm module, then set
|
|
; $10D1 resumeCommAfterDiskAccess Epilogue for disk access: restart the comm module, forget the persistent
|
|
; $10DC waitForGameDiskInserted Blocks until the OZ game disk is in the drive.
|
|
; $10F1 rasterIrqHandler The game's only interrupt handler, entered through the RAM vector $FFFE at
|
|
; raster
|
|
; $1293 irqExit Pulls Y, X and A back off the stack and falls into irqRti.
|
|
; $1298 irqRti A bare RTI.
|
|
; $129C updateFrameTimers Per-frame housekeeping called from the raster IRQ.
|
|
; $130A stepRandomSeed Called from the IRQ every frame.
|
|
; $131B blinkCursorColour Blinks the map cursor.
|
|
; $1331 handleChatSend The outgoing half of the modem chat ('message to opponent'), run every frame
|
|
; from
|
|
; $1399 receiveChatCharacter The incoming half of the chat, called every frame from the IRQ.
|
|
; $13C4 appendChatCharacter Appends character A to the 36-byte incoming message buffer at $9253 and
|
|
; $1414 resumeMainMenuScreen Re-entry into the console screen (screen 3, the F7 screen with the STATS /
|
|
; $141D enterMainMenu Full entry into the console screen (screen 3): picks the default tab, draws the
|
|
; $1439 mainMenuLoop The console screen's main loop (screen 3, tabs STATS / REPAIR / MISC).
|
|
; $14F0 snapshotComcenStatus Remembers what the REPAIR tab is currently showing: copies the three comcen
|
|
; $1507 selectDefaultTab Chooses which console tab should be showing.
|
|
; $153B showMainMenu Builds the console screen from scratch: resets the glyph plotter's parameter
|
|
; block,
|
|
; $1565 drawMenuFrame Draws the console's two nested frames from the predefined window table: window
|
|
; $18
|
|
; $156F clearMenuTextWindow Sets the console panel's text colour and window bounds and blanks the window.
|
|
; $1575 setMenuTextWindow Picks the console text colour from the comcen stun state (red on the REPAIR tab
|
|
; $1578 setMenuWindowBounds Sets the text window that the console panels print into: rows 7 to 21, left
|
|
; $158F clearMenuWindowPlain Blanks the whole console-menu panel with text colour 0 (black text, no
|
|
; highlight EOR mask).
|
|
; $1599 drawGameSetupSummary Draws the fixed header of the console STATS tab (menu tab 0): the rules word
|
|
; STANDARD/CUSTOM, the game-type name, an optional '.
|
|
; $1643 drawMenuTabBar Draws the three console tabs of screen 3: prints the labels STATS, REPAIR and
|
|
; MISC on row 3 starting at column 10, each in its own colour, then paints the 2x2 tab box behind each of them in its
|
|
; normal colour, and finally resets the bitmap/colour draw modes to 'replace'.
|
|
; $168C setTabHighlightColour Redraws console tab X in its bright colour: looks the colour up in
|
|
; tabHighlightColourTable and joins setTabNormalColour's common tail.
|
|
; $1691 setTabNormalColour Redraws console tab X: stores the chosen colour and the tab index into the two
|
|
; self-modified operands, sets the character-block origin to row 1 / tabColumnTable[X], draws the 2x2 tab-box shape from
|
|
; $9804 and then fills those 2x2 screen-RAM colour cells with the tab colour.
|
|
; $16B7 blinkSelectedTab Called once per pass of mainMenuLoop.
|
|
; $16D6 printEnergyLabel Prints the fixed word 'ENERGY' of the REPAIR tab at window column $1C, row 8
|
|
; (absolute column 30 inside the menu panel whose left edge is 2), in the current menu text colour.
|
|
; $16EB setMenuTextColour Chooses the text colour byte for the console menu screen: normally $61 (white
|
|
; text, highlight EOR mask 6 matching the blue panel), but $21 while the REPAIR tab is open and our own comcen is
|
|
; stunned, because drawRepairTab then paints the panel red instead of blue.
|
|
; $16FF printEnergyValue Prints the comcen's energy as a percentage at column $22, row 9 of the REPAIR
|
|
; tab, right-aligned in a three-character field, but only when the value has changed since the last call.
|
|
; $175A drawRepairTab Builds the whole REPAIR tab: clears the panel in blue ($66) or, if our comcen
|
|
; is stunned, red ($22); unpacks the comcen schematic picture into the bitmap; paints a 20x10 colour block behind it;
|
|
; resets the blink and energy bookkeeping; draws the four system markers (only when not stunned); prints the ENERGY
|
|
; label and value; and, when stunned, prints 'COMCEN STUNNED!'.
|
|
; $17DA repairTabAction Fire-button handler of the REPAIR tab (entry 1 of menuItemHandlerTable at
|
|
; $1535, reached through the self-modified JSR at $148D).
|
|
; $188B toggleSystemMarker One blink step for the marker of the comcen system currently highlighted in the
|
|
; REPAIR WHICH menu: flips blinkPhase and then either draws the marker (new phase 0) or erases it with the schematic
|
|
; background colour $16 (new phase 1).
|
|
; $189D drawSystemMarker Draws the status marker of comcen system Y on the REPAIR tab schematic.
|
|
; $1919 eraseSystemMarker Fills the whole 3x3 colour-cell block of system Y's marker with the colour byte
|
|
; in X.
|
|
; $192D sendEnergyRepairCommand Queues the 3-byte command $9A (cmdAddEnergy) with the comcen's unit index and
|
|
; the repaired amount, so that both machines add the same energy to that unit.
|
|
; $193C attemptSystemRepair Resolves one repair attempt on the comcen system highlighted in the REPAIR
|
|
; WHICH menu.
|
|
; $19B0 blinkRepairMarker Blinks the marker of the selected comcen system for about one second while a
|
|
; repair attempt resolves.
|
|
; $19C9 printAttemptingRepair Positions the REPAIR panel's own message row (window column 10, screen column
|
|
; 12, screen row 20) and prints 'ATTEMPTING REPAIR...' there, then restores the text window the caller had.
|
|
; $19D6 clearRepairMessage Erases the REPAIR panel's message row by printing 20 copies of the blank glyph
|
|
; zp_3E over it, then restores the caller's text window.
|
|
; $19E1 setRepairMessageWindow Shared prologue of printAttemptingRepair and clearRepairMessage: saves the
|
|
; caller's 12-byte text state, picks the menu text colour, applies the menu panel geometry (left column 2, width $24,
|
|
; rows 7..$16) and places the cursor at window column 10 = screen column 12 of screen row 20.
|
|
; $19F3 printOptionsList Prints the body of the MISC tab.
|
|
; $1A49 miscTabAction Fire-button handler of the MISC tab (entry 2 of menuItemHandlerTable at $1535)
|
|
; and, through the JMP at $0AC5, the top of the whole main-menu loop at cold start.
|
|
; $1ADC endGameToMainMenu Tears a finished game session down and prepares the main menu.
|
|
; $1B33 hangUpModem Convenience entry: hang up and leave linkSessionState at 0 (no session).
|
|
; $1B35 hangUpModemSetState Drops the modem link and stores A as the new linkSessionState.
|
|
; $1B5F openCommLink Brings the link up if it is not already.
|
|
; $1B74 openLinkAndWaitForConnection openCommLink followed by waitForConnection (fall-through).
|
|
; $1B77 waitForConnection On the modem build, pins the status message 'WAITING FOR CONNECTION...', spins
|
|
; in waitForCarrier until the modem reports carrier, then clears the voice-pause bit of pauseState and blanks the
|
|
; message line.
|
|
; $1B9A waitForCarrier Polling loop that runs until the comm module reports carrier (bit 6 of the
|
|
; debounced carrier state $E03C).
|
|
; $1BC1 showMessageWaitForSpace Pins status message A on the message line and blocks until the player presses
|
|
; the space bar.
|
|
; $1BF7 voicePauseReconnect The VOICE PAUSE round trip: hang up so both players can talk on the phone, wait
|
|
; five seconds, then bring the line back up and wait for carrier.
|
|
; $1C08 hangUpAndPause Hangs up for a voice pause and then waits about five seconds.
|
|
; $1C17 disconnectFromOpponent Ends a link session cleanly: tells the opponent with command $8C reason 5
|
|
; (disconnect), pushes the packet out immediately and hangs up.
|
|
; $1C2C runMainMenu Runs the main menu whose text printOptionsList has just drawn: five items, or
|
|
; six when a link session is up and DISCONNECT/SET VOICE is offered.
|
|
; $1C7B isAsymmetricGameType Tests the game type held in gameTypeOptions bits 0-2 and returns Z=1 for
|
|
; $1C87 restoreSettingsAfterFilm Undoes what WATCH GAME FILM overwrote.
|
|
; $1CC1 setViewVerticalLimit Sets viewRowLimit, the exclusive map row limit the centre cell of the
|
|
; $1CD3 scrollViewBy Moves the 7x5-cell tactical view by one map cell: X = column delta, Y = row
|
|
; delta,
|
|
; $1D4E scrollViewToUnit Scrolls the battlefield view until unit X sits under the centre cursor, one map
|
|
; $1D97 setLineTargetToUnit Points the line stepper's end point at unit X.
|
|
; $1DE4 scrollViewBitmap Shifts the whole 7x5-cell tactical view one map cell (16 pixels = 2 characters)
|
|
; $1F4E keepGroupBoxInsideMap Nudges the view origin one cell at a time until the whole group formation
|
|
; $1FC3 enterMapScreen Entry point of the battlefield screen (game mode 0).
|
|
; $1FD0 runMapMainLoop The interactive loop of the battlefield screen and of the unit-placement phase.
|
|
; $20F4 scrollViewportByDirection Turns a direction code 0-7 into a column/row step and scrolls the view
|
|
; $2185 runTargetCursorLoop Modal loop that lets the player drive the cursor to a cell and then acts on
|
|
; $2264 previewPathToCursor Idle-time animation: while the player leaves the controls alone with a move
|
|
; $22A2 showUnitInfoPanel Fills the info panel under the tactical view (text window rows 12-20, columns
|
|
; $2367 checkUserInterrupt The 'may this passive display keep running?' test used by the idle loops of the
|
|
; info panel and the path preview.
|
|
; $2384 printUnitStatusLines Fills in the value column of the unit info panel for selectedUnit ($90F8): the
|
|
; group name on row 13, the energy percentage on row 17, the weapon state on row 18, the action on row 19 and one flag
|
|
; word (BLITZ / CLOAKED / DUG IN) on row 20, then adds up to two '*' zone markers in front of the unit name on row 12.
|
|
; $24E4 animatePathMarkerToDestination Sets the line-stepper target to the ordered destination of selectedUnit and
|
|
; falls into animatePathMarkerStep, so the mini-map marker crawls from the unit towards where it has been sent.
|
|
; $24F3 animatePathMarkerStep Advances the mini-map path marker by one cell every 4 frames, but only after
|
|
; the player has been idle for 90 frames.
|
|
; $2540 resetPathMarker Puts the path marker back at the start: sprite 1 off, marker cell and pass
|
|
; counter back to the selected unit, and sprite 1's shape reloaded.
|
|
; $256F resetPathMarkerToUnit Reloads the marker's pass counter with 5 and moves the marker cell onto the
|
|
; selected unit's map position, then falls into positionMarkerSprite to place sprite 1 there.
|
|
; $2585 positionMarkerSprite Converts a map cell to mini-map sprite coordinates (column*4+$1F, row*4+$39, or
|
|
; the group-box mapping when the mini-map is zoomed) and stores them in sprite 1's position shadows, which the raster
|
|
; IRQ copies to the VIC.
|
|
; $258F recenterViewportOnUnit Makes sure the selected unit sits under the battlefield cursor.
|
|
; $25B7 consumeSpaceKey Eats a pending SPACE.
|
|
; $25C4 callSetupOverlayHook Trampoline into overlay variant A at $8658 (handleStarKeyToggle), the per-frame
|
|
; '*' key handler of the non-battle phases: it toggles setupToggleFlag $92F3 bit 7 (DESTROY mode during unit placement,
|
|
; CUSTOM rules on the setup screen) and reports C=1 when it consumed the key.
|
|
; $25C7 getViewportOriginPointer Builds a map pointer for the top-left cell of the 7x5 battlefield view.
|
|
; $25E3 getViewportRowPointer Map pointer for the cell (viewOriginCol, Y).
|
|
; $25FA readMapCell The map accessor of the whole game: forms mapCellPtr = rowAddress[Y] + X from
|
|
; the 40-entry row address tables at $BFB0/$BFD8 (built by the start-up code at $0339 as $F000 + 40*row) and returns the
|
|
; byte of cell (column X, row Y) of the 40x40 battlefield.
|
|
; $260D readCellUnderCursor Reads the map cell under the battlefield cursor.
|
|
; $261C clearInfoPanel Selects the unit info panel as the text window and blanks it with the text
|
|
; engine's window clear, using the current textColour zp_3D and fill glyph zp_3E.
|
|
; $2622 setInfoPanelWindow Defines the text window that covers the unit info panel: columns 25-38, rows 12
|
|
; to 20 (row 21 is the exclusive limit).
|
|
; $2636 clearScreenAndRedrawGameScreen Rebuilds the battlefield display from scratch: sets the VIC up for the
|
|
; multicolour bitmap and wipes it grey, then falls into redrawGameScreen.
|
|
; $2639 redrawGameScreen Draws the whole battlefield screen: the three panel frames (mini-map, tactical
|
|
; view, info panel), the bottom status bar, the 40x40 overview map, the 7x5 tactical view and finally the current status
|
|
; message.
|
|
; $268F drawMiniMapFrame Draws window 0: the 22x22 character frame at column 0, row 0 that surrounds the
|
|
; 20x20 strategic mini-map.
|
|
; $2698 drawViewportFrame Draws window 1: the 16x12 character frame at column 24, row 0 around the
|
|
; tactical view, then falls into clearViewportTextArea to blank the interior.
|
|
; $26A1 clearViewportTextArea Declares the interior of the tactical view (rows 1-10, columns 25-38, i.e.
|
|
; $26B8 drawInfoPanelFrame Draws window 2: the 16x11 character frame at column 24, row 11 around the unit
|
|
; info panel.
|
|
; $26C1 drawViewport Redraws the tactical view: 5 rows of 7 map cells, each cell a 16x16 pixel
|
|
; multicolour tile occupying 2x2 character cells, into character rows 1-10 and columns 25-38.
|
|
; $2772 renderMapTile Builds the 16x16 pixel multicolour tile of one map cell in the 32-byte buffer
|
|
; at $04C8-$04E7 and writes the cell's four screen-RAM colour bytes.
|
|
; $28E1 drawMainWindowFrame Draws the frame of the bottom bar: window 3 on the battlefield screen, window 4
|
|
; on every other screen.
|
|
; $28F1 computeGameResult Turns the state at the end of a game into the two final point totals and the
|
|
; result message.
|
|
; $29B3 setOtherPlayerPoints Writes Y as the 16-bit point total of the side OTHER than X and returns with X
|
|
; $29C1 shakeScreenScroll One frame of the screen-shake effect used when a comcen is hit.
|
|
; $2A08 patchUnitSideBranches Re-points the two 'is this unit mine?' branches at the viewpoint currently
|
|
; $2A1A printOptionWord Prints one of the three 4-byte DAMAGE option words held at $0652 in the message
|
|
; $2A38 setBitmapDrawMode Chooses how a glyph's eight bitmap bytes are combined with what is already in
|
|
; the
|
|
; $2A47 setColourDrawMode The colour-plane twin of setBitmapDrawMode: chooses how drawColourByte is
|
|
; combined
|
|
; $2A56 resetDrawModes Puts both planes back to mode 0 (plain replace) after a highlight or overlay
|
|
; effect.
|
|
; $2A64 fillScreenColourRect Paints an X columns by Y rows rectangle of the multicolour bitmap's colour
|
|
; cells
|
|
; $2A9F calcCellPointers Loads the character cell coordinates from scratch18 (row) and scratch19
|
|
; (column) and
|
|
; $2AA3 calcCellPointersXY Turns a character cell (Y = row, X = column) into the two pointers every
|
|
; plotter
|
|
; $2AC3 drawCharAt Draws character A at screen row scratch18, column scratch19: computes the cell
|
|
; pointers and
|
|
; $2AC8 plotGlyph Draws one 8x8 glyph of the game's own font (114 glyphs at $9426, 8 bytes each)
|
|
; into the
|
|
; $2B18 drawCharBlock Draws a rectangular block of characters at windowOriginRow/windowOriginCol.
|
|
; $2B75 drawWindowInnerCorners Draws the small hollow-ring glyph $38 (font bytes at $95E6) in the four cells
|
|
; $2BB7 drawTallGlyph Draws one of the ten two-cell-tall symbols whose character-code pairs live at
|
|
; $9810
|
|
; $2BCE xorColourBlock2x2 Flashes one battlefield tile by EORing the colour byte A into the 2x2
|
|
; screen-RAM
|
|
; $2C7F drawWindowFrame Draws one of the 27 predefined windows selected by X.
|
|
; $2CA1 drawFrameBox Draws a rectangular frame from a 9-byte tile set at
|
|
; windowOriginRow/windowOriginCol,
|
|
; $2D1D drawFrameColumn Repeats the tile at offset Y of the current tile set down column scratch19,
|
|
; starting
|
|
; $2D2F drawFrameRow Repeats the tile at offset Y of the current tile set along row scratch18,
|
|
; starting at
|
|
; $2D41 initBitmapScreen Builds the game's display from scratch: a full-screen text window, grey border
|
|
; and
|
|
; $2DBD setTerrainTilePtr Points glyphPtr at the 8-byte terrain tile definition number A in the table at
|
|
; $2DDB clearMessageLineAndScreen Wipes the status/message line first, then falls into
|
|
; $2DDE clearCurrentScreenWindows Re-clears the content areas of whichever screen is active, dispatching on
|
|
; $2E0F magnifyWindow2x Redraws the character rectangle at windowOriginRow/windowOriginCol, frameWidth
|
|
; by
|
|
; $2F34 setMagnifyCellPointers Helper of magnifyWindow2x.
|
|
; $2F6A initSpritePointers Rewrites the eight sprite pointers in the last bytes of the video matrix,
|
|
; $2FA5 clearAllSpriteData Zeroes $8A00-$8BFF, the 64-byte shape blocks of all eight sprites, in one
|
|
; 256-pass
|
|
; $2FB2 clearSpriteShape Zeroes the 64-byte shape of sprite A at $8A00 + A*64.
|
|
; $2FCF setCursorShapeSquare Gives sprite 0, the map cursor, the 18-byte (6 rows of 24 pixels) hollow square
|
|
; $2FD3 installCursorShape Shared tail of setCursorShapeSquare and setCursorShapeCross: copies the 18-byte
|
|
; $2FF3 setCursorShapeCross The same as setCursorShapeSquare but installs the thick plus/cross shape at
|
|
; $9E80
|
|
; $2FFA setCursorShapeBlock Loads the small 4x4 solid block at $9E92 into the sprite named by
|
|
; $3023 mapCellToSpriteXY Converts a battlefield cell (X = column 0-39, Y = row 0-39) into the sprite
|
|
; $303A mapCellToSpriteXYRaw The plain cell-to-sprite conversion for the overview map: spriteX =
|
|
; $3049 positionCursorSprite Puts the sprite named by activeSpriteIndex ($91CC) over the cursor cell of
|
|
; $306C drawLineInPathSprite Draws a straight line into the shape of sprite 3 ($8AC0), which is the game's
|
|
; $308A plotPathCross Marks the end of a path/shot line by plotting the four orthogonal neighbours of
|
|
; the
|
|
; $30A1 plotPathSpritePixel Sets one pixel in the shape of sprite 3 at $8AC0.
|
|
; $30C0 buildGroupFormationSprite Builds the 'formation ghost' shown while a whole group is being ordered
|
|
; $31A3 enableSprites ORs the sprite mask in A into spriteEnableShadow ($9022), the shadow the raster
|
|
; IRQ
|
|
; $31AF plotSpritePixel Sets, clears or toggles one pixel of a sprite shape.
|
|
; $31CA setSpritePixel Entry point of plotSpritePixel that forces the ORA (set) operation.
|
|
; $3239 drawCircleInSprite Draws a circle of radius X (1-10) centred on pixel (12,10) - the middle of a
|
|
; $3266 computeCircleOctantPoints Expands one octant point (dx,dy) of a circle into the coordinates of all
|
|
; $32BF plotCircleOctantPoints Plots the eight mirrored points prepared by computeCircleOctantPoints into
|
|
; $32EA hideSprite1 Clears bit 1 of the sprite enable shadow and writes the result straight to
|
|
; $32F6 hideSprite0Dead Dead code: a byte-for-byte copy of hideSprite1 that clears bit 0 (sprite 0)
|
|
; $3302 showCrosshairSprite Loads the 63-byte diamond crosshair (the second shape of the pair at $9EA4,
|
|
; $3306 showCursorBoxSprite Same as showCrosshairSprite but copies the first shape of the pair ($9EA4-
|
|
; $3340 loadDirectionShapeSprite Copies sprite shape number A (0-7, one per facing direction) from the
|
|
; $3383 showComcenCrossMarker Puts the small cross marker at $9F22 into rows 7-13 of sprite 2, colours it
|
|
; $33B1 gridCellToSpriteCoords Converts a cell of overlay B's 21x17 radar grid (one map cell per 8x8
|
|
; $33C8 showExplosionMarker Shows the explosion marker for a weapon effect at map cell (X,Y).
|
|
; $3405 startExplosionMarkerTimer Two-instruction tail shared by both paths of showExplosionMarker: arms
|
|
; $340B getUnitSpriteCentreOnGrid Maps a battlefield cell onto overlay B's radar grid and returns the
|
|
; $341F updateExplosionMarker Keeps the explosion marker (sprite 4) on the right cell of the drone camera
|
|
; $3456 drawComcenRingSprite Builds the comcen marker of the drone screen: two concentric rings (radius 2
|
|
; $3458 drawRingsInSprite4 Shared body of drawComcenRingSprite: clears sprite 4 and draws A concentric
|
|
; $347F orderSelectedUnitToCursor Issues a MOVE order for the selected unit to the cell under the cursor:
|
|
; $34A4 updateUnitAtDestinationFlag Sets bit 7 of unitDisplayFlags ($FB54) for the unit in cmdParam0 when
|
|
; $34C6 applyMoveOrder Executor of command $80 MOVE (unit, column, row).
|
|
; $34D5 setUnitDestination Stores a destination for unit Y.
|
|
; $355D clampMapCoord Clamps a signed map coordinate to the battlefield: negative becomes 0, anything
|
|
; from
|
|
; $3568 markAllUnitsVisible Sets bits 6 and 7 (spotted this cycle, currently visible) of unitEnergy for
|
|
; $3576 ageUnitVisibility Run once per unit-processing cycle, before the spotting passes: every unit that
|
|
; $35C3 scanVisibleEnemies Builds the list of enemy units that unit Y can see (X bit 7 clear, the spotting
|
|
; $3746 checkLineOfSightStep Feeds the terrain class of one more cell into the running line-of-sight test.
|
|
; $375D initLineBetweenUnits Points the line walker at unit Y's cell and falls into initLineFromUnit, which
|
|
; sets the start to unit X's cell.
|
|
; $3767 initLineFromUnit Sets the line walker start to unit X's cell, runs initLineStepper ($FBB8) and
|
|
; reports the principal direction of the line: 0 = north or 1 = south for a row-major line, 2 = east or 3 = west for a
|
|
; column-major one.
|
|
; $378B setScanFilterOppositeSide Configures the unit filter of collectUnitsInRadius so that only units of the
|
|
; side opposite to unit Y are collected, by writing the compare value and the branch opcode of the test at $37FE/$3800.
|
|
; $379E collectAllUnitsInRadiusDead Unreferenced alternate entry of collectUnitsInRadius that turns the side filter
|
|
; into 'accept every unit' (compare against unit 0 with a BCC) before falling into the scan.
|
|
; $37AA collectUnitsInRadius Scans the square of half-width A around map cell (X,Y), clipped to the 40x40
|
|
; battlefield, and lists every cell holding a unit that passes the side filter patched by setScanFilterOppositeSide: the
|
|
; unit index goes into $0400[n] and its squared distance from the centre into $0432[n].
|
|
; $3870 distanceSquared Squared distance between two map cells: |dCol|^2 + |dRow|^2 taken from
|
|
; squaresTable.
|
|
; $38C2 resolveAttack Resolves one shot of unit X at unit Y.
|
|
; $3A5A drawShotLineSprite Draws the shot on the 4-pixel-per-cell overview map using hardware sprite 3.
|
|
; $3B22 applyHitToUnit Takes A points of damage off unit Y and deals with everything that follows.
|
|
; $3B52 applyHitToUnitNoAlert Alternate entry of applyHitToUnit that skips the block above, i.e.
|
|
; $3BDF reduceUnitEnergyBy Stores A as the damage in hitDamage $9240 and falls into reduceUnitEnergy.
|
|
; $3BE2 reduceUnitEnergy Subtracts hitDamage $9240 from the energy of unit Y, keeping the two visibility
|
|
; flags in bits 6-7 and stopping at 0.
|
|
; $3BFC recordUnitHit Records the visible effect of a hit on unit Y: the damage, capped at 20, is
|
|
; stored as $80|damage in the hit-effect byte $FA8C that drives the flash and the PINNED status word, the same number of
|
|
; movement points is taken off the signed counter $FA28 (clamped at -127), and bit 1 of the display flags asks for the
|
|
; console entry to be refreshed.
|
|
; $3C20 orderAttackUnitAtCursor Fire-button action of the attack cursor.
|
|
; $3C50 setBoomerTargetCmd Execution half of command $87 SET TARGET.
|
|
; $3C5D countBoomersBelow Counts the BOOMERs (type 2) with an index below X, which is the ordinal of
|
|
; boomer X among all boomers and therefore its slot in boomerTargetTable.
|
|
; $3C74 getBoomerTarget Reads the target lock of unit X.
|
|
; $3C85 selectBoomerTarget Picks what boomer Y shoots at this round.
|
|
; $3CF2 animateRingBurst Expanding-then-contracting ring drawn into hardware sprite 1.
|
|
; $3D57 rollComcenSystemDamage Rolls for collateral damage to the local comcen's three repairable systems
|
|
; after it has been hit.
|
|
; $3D8A initAllUnits Resets all 100 unit records from their start positions and puts every living
|
|
; unit back on the battlefield map, then clears the 16 per-group state bytes.
|
|
; $3DA0 initUnitRecord Resets the order state of one unit: its path position and its destination are
|
|
; snapped to the cell it stands on, the destination is marked 'idle', the zone-scored bits and the stun counter are
|
|
; cleared, the group membership is kept and the facing and side bits are rebuilt.
|
|
; $3DDA placeUnitOnMap Second half of the unit reset: restores the dug-in bit from the type byte,
|
|
; gives the unit a fresh movement allowance and full energy, copies the stationary and group bits into the display
|
|
; flags, remembers the terrain of the cell it stands on and writes $80|index into that map cell.
|
|
; $3E23 recycleDeadUnits Once per round each side that owns a recycler gets one destroyed robot back.
|
|
; $3EBD removeUnitFromMap Lifts unit X off the battlefield map: the terrain byte the unit was hiding is
|
|
; written back into its cell, the unit is marked as 'not on the map' (unitTerrainUnderTable = $FF) and its
|
|
; hit-flash/pinned countdown is cleared.
|
|
; $3EC2 removeUnitLeavingCell Same as removeUnitFromMap but the caller chooses what stays behind in the
|
|
; unit's cell (A) instead of the saved terrain.
|
|
; $3EE2 applyBlitzStepCost Charges a blitzing unit for the step it has just taken: it loses one energy
|
|
; point and gains 25 movement points (SET BLITZ = 'move extra fast'), and its next shot gets the charge bonus
|
|
; (unitFlagsTable bit 7).
|
|
; $3F27 drawRecyclerOnMap Puts the recycler glyph ($67) on the battlefield map at side Y's recycler
|
|
; position and hands the caller everything drawMapCell needs to show it.
|
|
; $3F57 checkEnemyRecyclerSpotted Reveals the opponent's recycler the first time one of our own uncloaked robots
|
|
; gets within 3 cells of it: it draws the recycler glyph on the map, queues message 8 'SPOTTED RECYCLER!' and then
|
|
; patches its own first byte to RTS so the test never runs again.
|
|
; $3F97 initialFacingForUnit Returns the facing a unit starts with: 0 (up, towards the enemy) for the local
|
|
; player's units and 1 (down) for the opponent's, since each player sees his own side at the bottom of the map.
|
|
; $3FA3 mirrorMapCoordinate Returns 39 - A, mirroring a map column or row across the 40x40 battlefield.
|
|
; $3FAC haltStunnedUnit Cancels the orders of a unit that is stunned (a drone hit stuns everything
|
|
; around it): its waypoint and destination are snapped back to the cell it stands on, it is marked stationary and it is
|
|
; dropped out of its group so the group does not wait for it.
|
|
; $3FE8 haltAllUnitsAndReveal Game-over clean-up: repairs the three comcen system status bytes, stops all 100
|
|
; units (stationary, nothing fired, nothing pinned, waypoint snapped back onto the unit) and reveals the whole board by
|
|
; marking every unit as spotted.
|
|
; $4027 endGame Ends the battle once the caller has stored the reason in gameEndReason $0BA8
|
|
; ($08 = the clock ran out, $A0/$C0 = one side's comcen was knocked out).
|
|
; $403B markComcenRevealed Launching a radar sweep or a drone gives the launching comcen away: it sets
|
|
; that side's comcen-reveal timer to 6 rounds and forces the visibility bits of that comcen so the other player can see
|
|
; it on the map.
|
|
; $406F waitFramesOrInput Interruptible pause that keeps the battle running: it advances the game one
|
|
; frame at a time for up to A frames and returns early on a fresh fire-button press, on the joystick leaving its centre
|
|
; position, or when the game-end countdown starts.
|
|
; $4094 runGameFrames Advances the game by A frames: each pass waits for one raster frame and then
|
|
; runs one updateGameFrame.
|
|
; $40B2 updateGameFrame The heartbeat of the game, called once per frame from every main and
|
|
; user-interface loop.
|
|
; $4213 processUnitStep Processes exactly one unit of the round order; this is an internal entry of
|
|
; updateGameFrame reached by the branch at $4206, not by a JSR, and it ends by falling into the shared tail at L_4537
|
|
; which bumps unitOrderIndex.
|
|
; $4559 tryStepTowardTarget walks the line stepper one cell from the current cell ($93,$92) toward the
|
|
; $45E6 finishRound end of a round, reached by JMP from updateGameFrame ($4210) once all 100 units
|
|
; have
|
|
; $4751 moveUnitToCell puts currentUnit into the cell ($93,$92) that tryStepTowardTarget just
|
|
; accepted:
|
|
; $47ED shuffleUnitOrder exchanges two randomly chosen entries of unitBaseOrder ($9164, the 100 unit
|
|
; $4805 randomUnitIndex returns a unit index 0-99 drawn uniformly from the scenario random generator:
|
|
; $480F followDirectedUnit keeps the battlefield view locked on the unit the console is following.
|
|
; $4860 updateHitFlashTimers ages the hit markers of all 100 units.
|
|
; $48A2 drawUnitAtPosition redraws unit Y where it stands: builds the map byte $80|unit and falls into
|
|
; $48A5 drawValueAtUnit draws cell value A at the position of unit Y.
|
|
; $48B1 updateIdleFlag recomputes bit 7 of unitDestCol ($F7D0) for currentUnit: it is set when the
|
|
; $48DD rebuildGroupTables recomputes the 16 entries of groupStatusTable ($91F4), one per group key
|
|
; $4987 averageGroupMoveVector averages the outstanding movement of one group.
|
|
; $4A02 followGroupMovement Gives the unit that is joining a group the movement the rest of the group
|
|
; already has: averageGroupMoveVector ($4987) returns the average (destination - position) vector of the members and the
|
|
; unit's new destination becomes its own cell plus that vector.
|
|
; $4A35 sendGroupMoveToCursor Fire-button action of the battlefield target cursor while a whole group is
|
|
; selected (uiLevel $9209 >= 2).
|
|
; $4A52 moveGroupKeepingFormation Executor of command $81, run in lock step on both machines.
|
|
; $4AA3 addSelectedUnitToGroup The 'ADDED' half of the group membership editor.
|
|
; $4AD4 joinUnitToGroup Executor of command $88.
|
|
; $4AF2 removeSelectedUnitFromGroup The 'REMOVED' half of the membership editor.
|
|
; $4B04 isPointWithinGroupRange Range test that stops a group from spreading over more than 8 cells.
|
|
; $4B5E computeGroupBounds Scans all 100 unit slots and accumulates the member count and the bounding box
|
|
; of one group.
|
|
; $4BBF showTooFarMessage Error feedback shared by the two group range checks: selects and clears the
|
|
; 14-column info panel (screen columns 25-38, rows 12-21), plays sound 3 (the rejection beep) and prints 'TOO FAR!' from
|
|
; the $C000 text overlay on screen row 16, in whatever colour the caller left in textColour.
|
|
; $4BD3 orderGroupNewPosition NEW POSITION action of the custom-formation editor.
|
|
; $4C59 setGroupMemberDestination Executor of command $98.
|
|
; $4C6B syncLocalGroupFlags Copies the authoritative group bits of unit X (unitFlagsTable & $7C: group
|
|
; number, side and member bit) into the local flag copy unitDisplayFlagsTable, keeping that byte's own bits 0, 1 and 7.
|
|
; $4C84 rotateGroupFormation Executor of command $A3 - the SWEEP LEFT / SWEEP RIGHT / REVERSE items of the
|
|
; FORMATION menu.
|
|
; $4D9F getCommandLength Looks the total length (command byte plus its parameters) of command A up in
|
|
; commandLengthTable and leaves it in currentCommandLength as well as in A.
|
|
; $4DA9 serviceCommandExchange Per-frame entry point of the lock-step command exchange, called from the main
|
|
; loop of every screen and from the overlays.
|
|
; $4DB7 processCommandExchange The heart of the lock-step protocol.
|
|
; $4E91 executeTurnCommands Runs every command of one exchange.
|
|
; $4F05 mergeIncomingPacket Appends the packet just received from the opponent ($E020, $E01E bytes) to the
|
|
; merged command buffer, marking the block as remote.
|
|
; $4F5E advancePacketCursor Helper of both packet mergers: keeps the command byte in A, looks its length up
|
|
; and leaves packetReadIndex pointing at the next command in the packet.
|
|
; $4F6B mergeOutgoingPacket Appends the copy of the packet this machine just sent ($E027, $E01F bytes) to
|
|
; the merged buffer, marking the block as own, so the local commands are executed at exactly the same point in the
|
|
; sequence as on the opponent's machine.
|
|
; $4FC4 readCmdArgs3 Argument reader for the 4-byte commands $80, $81, $8A, $8B, $98, $9C and $A6:
|
|
; takes the first parameter into cmdParam0 and falls through into readCmdArgs2, which picks up the other two.
|
|
; $4FD0 readCmdArgs2 Argument reader for the 3-byte commands $87, $88, $8E, $90, $9A, $A3 and $A5,
|
|
; and the tail of readCmdArgs3.
|
|
; $4FE4 mirrorCmdCoordsIfRemote Point mirror for the map coordinates carried by a command.
|
|
; $4FF7 readCmdArg1 Argument reader for all the 2-byte commands - the per-unit and per-group orders
|
|
; $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2 and $A4.
|
|
; $5005 mirrorCmdCoordsIfRemoteDec Coordinate mirror for the drone, which covers a 2x2 block of cells and is
|
|
; addressed by its top-left corner.
|
|
; $5011 cmdMoveUnit Handler of command $80 MOVE (4 bytes: unit, column, row), the ordinary move
|
|
; order given by clicking a cell with a single unit selected.
|
|
; $501A cmdMoveGroupRelative Handler of command $81 (4 bytes: unit, column, row) - the move order given
|
|
; while a whole group is selected.
|
|
; $5023 cmdCloakOn Handler of command $82 CLOAK ON (2 bytes: unit).
|
|
; $504B cmdCloakOff Handler of command $84 CLOAK OFF (2 bytes: unit); also used as a subroutine by
|
|
; cmdBlitzOn.
|
|
; $5062 cmdBlitzOff Handler of command $95 BLITZ OFF (2 bytes: unit); also used as a subroutine by
|
|
; cmdCloakOn.
|
|
; $5070 cmdBlitzOn Handler of command $93 BLITZ ON (2 bytes: unit), the mirror image of
|
|
; cmdCloakOn.
|
|
; $5098 cmdGroupCloakOn Handler of command $83 (2 bytes: group key).
|
|
; $50AA cmdGroupBlitzOn Handler of command $94 (2 bytes: group key).
|
|
; $50BE cmdGroupBlitzOff Handler of command $96 (2 bytes: group key) and, at $50C1, the blitz half of
|
|
; the patch block that configures the shared group routine.
|
|
; $50D2 cmdGroupCloakOff Handler of command $85 (2 bytes: group key) and, at $50D4, the cloak half of
|
|
; the patch block.
|
|
; $50EB applyGroupCloakOrBlitz The shared, heavily self-modified body of the four group cloak/blitz handlers;
|
|
; it is only ever reached by branching, never by JSR.
|
|
; $5151 updateComcenCloakLock The common tail of every cloak and blitz handler and the only writer of
|
|
; comcenCloakedFlag: it copies bit 7 of the local player's own comcen entry in unitPathColTable, so $9245 bit 7 is set
|
|
; exactly while that comcen is cloaked.
|
|
; $515D cmdClearTarget Handler of command $86 CLEAR TARGET (2 bytes: BOOMER unit).
|
|
; $516B cmdSetTarget Handler of command $87 SET TARGET (3 bytes: BOOMER unit, target unit).
|
|
; $5185 cmdJoinGroup Handler of command $88 JOIN GROUP (3 bytes: unit, group byte).
|
|
; $519B cmdLeaveGroup Handler of command $89 (2 bytes: unit): clears bit 6 ('in a group') of the
|
|
; unit's
|
|
; $51AB cmdPlaceUnit Handler of command $8A (4 bytes: unit, column, row): teleports a unit during
|
|
; the deployment
|
|
; $51EA cmdSetCustomMapSeed Handler of command $A6 (4 bytes: three seed bytes): sets mapFlipFlag ($0B9D) =
|
|
; 1 and
|
|
; $51EE cmdSetMapSeed Handler of command $8B (4 bytes: three seed bytes): mapFlipFlag = 0, the three
|
|
; arguments are
|
|
; $520C cmdComcenRepairedOrSetupReady Handler of the 1-byte command $9E, which means two different things.
|
|
; $523E cmdSessionControl Handler of command $8C (2 bytes: reason).
|
|
; $52AE getPauseClearMask Returns the AND mask that clears the resuming side's bit of a two-bit
|
|
; ready/pause byte.
|
|
; $52C8 cmdDroneMove Handler of command $8D (4 bytes: heading, column, row), the opponent's live
|
|
; drone telemetry.
|
|
; $532F saveRegsForCmdReturn Two-instruction helper for the handlers that are called from the middle of the
|
|
; packet
|
|
; $5336 cmdDroneDetonate Handler of command $8E (3 bytes: column, row).
|
|
; $537B cmdDroneRelease Handler of the 1-byte command $8F, queued by releaseDroneControl ($8230) when
|
|
; the player
|
|
; $538B cmdDroneShotDown Handler of the 1-byte command $92.
|
|
; $53A3 cmdDroneLaunch Handler of the 1-byte command $97: droneArrivalFlags = 0 for a fresh drone in
|
|
; the air.
|
|
; $53C0 cmdMissileStrike Handler of command $90 (3 bytes), which carries two different payloads.
|
|
; $542E cmdSelfDestruct Handler of command $91 (2 bytes: unit; bit 7 of the argument means 'the whole
|
|
; group this
|
|
; $5459 destroyUnit Removes unit cmdParam0 from play.
|
|
; $5470 cmdMoveKeepGroup Handler of command $98 MOVE TO GOAL (4 bytes: unit, column, row), the order
|
|
; issued to
|
|
; $5479 cmdAddEnergy Handler of command $9A (3 bytes: unit, amount), sent by the COMCEN energy
|
|
; repair on the
|
|
; $549E cmdHaltGroup Handler of command $9B (2 bytes: group byte), the group HALT order.
|
|
; $54E1 cmdSetRecycler Handler of command $9C (4 bytes: mode, column, row), sent by overlay A's
|
|
; chooseRecycler
|
|
; $5511 cmdChooseSide Handler of command $9D (2 bytes: the sender's side-preference token), the
|
|
; side-selection
|
|
; $553B sendChooseSide The sending half of the $9D exchange: copies playerSide into cmdParam0 and into
|
|
; $5548 cmdDigIn Handler of command $9F DIG IN (2 bytes: unit): calls setUnitDigBits with $20,
|
|
; i.e.
|
|
; $554C cmdDigOut Handler of command $A0 DIG OUT (2 bytes: unit): setUnitDigBits with 0 clears
|
|
; the dig-progress
|
|
; $555A setUnitDigBits Shared prologue of cmdDigIn and cmdDigOut: patches the new dig bits into the
|
|
; ORA operand
|
|
; $5562 applyDigBitsToUnit The two-line worker shared by the single-unit and the group dig handlers:
|
|
; $556D cmdGroupDigIn Handler of command $A1 (2 bytes: group byte): patches the dig bits to $20 and
|
|
; runs the
|
|
; $5571 cmdGroupDigOut Handler of command $A2 (2 bytes: group byte) and the entry of the shared group
|
|
; dig loop:
|
|
; $5596 cmdSetFormation Handler of command $A3 (3 bytes: group leader unit, rotation mode 0-2): reads
|
|
; the two
|
|
; $559C cmdSetGameType Handler of command $A4 (2 bytes): stores the argument into gameTypeOptions
|
|
; ($0BA3), which
|
|
; $55A5 cmdSetupOption Handler of command $A5 (3 bytes: kind, value), the generic carrier of overlay
|
|
; A's
|
|
; $55EC queueCmdBytePending Entry used by the 1-byte commands ($8F, $92, $97, $9E): counts one command in
|
|
; $55EF queueCmdByte Appends A to the 40-byte outgoing command ring at $9065, wrapping the write
|
|
; index $908D at
|
|
; $560A queueCmd4 Queues a 4-byte command: the code in A followed by cmdParam0, cmdParam1 and
|
|
; cmdParam2 (used for
|
|
; $560F queueCmd3 Queues a 3-byte command: the code in A followed by cmdParam1 and cmdParam2 (the
|
|
; commands of
|
|
; $561F queueCmd2 Queues a 2-byte command: the code in A followed by cmdParam0 (the commands of
|
|
; length 2 are the
|
|
; $562B sendOutgoingPacket Builds the packet for the next lock-step exchange and hands it to the comm
|
|
; module.
|
|
; $56B1 pollMenuSession Reached while a link is up but no game is running (commSessionState $0B9C bit
|
|
; 7): if the
|
|
; $56BB handleMenuSessionPacket Menu-level receive path: re-arms the 10-frame link poll delay and returns
|
|
; unless a
|
|
; $56F1 completeExchange Makes sure the packet exchange currently in flight finishes.
|
|
; $572B readyForNewGameHandshake Run before a new game over an existing link: finishes any pending exchange,
|
|
; puts
|
|
; $577C buildReadyPacket Fills the comm module's transmit buffer with the 2-byte packet $8C,$02 (session
|
|
; control,
|
|
; $578A recordExchangeToFilm Appends one lock-step exchange to the game film held in the RAM under the I/O
|
|
; area.
|
|
; $57BE writeFilmTimestamp Writes the time stamp of one game-film record at (filmPtr),Y.
|
|
; $57E7 advanceFilmPtr Adds the record length in Y to the 16-bit film write pointer filmPtr ($BC/$BD),
|
|
; so the next record starts where this one ended.
|
|
; $57F2 finalizeFilm Closes a game film that was being recorded: appends the end-of-film marker
|
|
; record ($FE = the game played itself out, $FF = it was aborted) with a time stamp if the buffer still has room, then
|
|
; rewinds and fills in the header at $D000 - total film length, format id, final clock, both 16-bit scores and the
|
|
; checksum over $D000-$D032 - and finally leaves filmPtr pointing at the end of the recording again.
|
|
; $585C setFilmPtrToEnd Points filmPtr ($BC/$BD) at filmEndPtr ($92F1/$92F2), i.e.
|
|
; $5867 saveFilmEndAndRewind Remembers the current film position as the end of the recording (filmEndPtr =
|
|
; filmPtr) and then rewinds filmPtr to the start of the buffer at $D000, ready for header access.
|
|
; $5874 readFilmByte Reads one byte of the game film out of the 4 KB of RAM that lies underneath the
|
|
; I/O area ($D000-$DFFF).
|
|
; $5893 writeFilmByte The write half of readFilmByte: stores A at (filmPtr),Y in the RAM under the
|
|
; I/O area, with the same 'sei / kill the CIA2 NMI / $01=$34 / restore' dance.
|
|
; $58B2 copyPageUnderIo Copies one 256-byte page with the I/O area banked out, used to move the unit
|
|
; tables in and out of the film's start-of-game snapshot.
|
|
; $58D5 rewindFilmPtr Sets filmPtr ($BC/$BD) to $D000, the first byte of the film / saved-game buffer
|
|
; in the RAM under the I/O area.
|
|
; $58DE verifyFilmHeader Sanity check on a film that has just been loaded from disk: recomputes the
|
|
; checksum over header bytes $D000-$D032 and compares it with the stored one at $D033, then checks that the format id at
|
|
; $D02F matches this build's gameVersionByte ($0B94).
|
|
; $5900 computeFilmHeaderChecksum Adds up the 51 header bytes $D000-$D032 into scratch18 ($18).
|
|
; $5915 waitForExchangeIdle Spins on the per-frame command exchange until the packet exchange in flight has
|
|
; completed, which the exchange code signals by clearing linkTimeoutCounter ($9223); checkLinkTimeout sets bit 7 of that
|
|
; counter while a packet is outstanding.
|
|
; $591E compareFilmPtrToEnd 16-bit unsigned compare of filmPtr against filmEndPtr.
|
|
; $5929 writeFilmSetupSnapshot Writes the start-of-game snapshot into the film header so that a replay can
|
|
; rebuild the battlefield exactly: the 20 setup bytes $92A2-$92B5 (scores, recycler mode and position, terrain points,
|
|
; drones, missiles, comcen speeds) go to header offsets $34-$47, the eight 100-byte unit arrays $F640-$F95F (column,
|
|
; row, flags, type, destination column/row, waypoint column/row) are copied to $D048-$D367, and the game clock is stored
|
|
; at $D030.
|
|
; $5983 getFilmSnapshotDestAddr Returns the constant address $D268 in A (low) and X (high): the highest
|
|
; destination page of the unit snapshot inside the film header, and the page just below the start of the command stream
|
|
; at $D368.
|
|
; $5988 getFilmSnapshotSrcAddr Returns the constant address $F860 in A (low) and X (high): the highest source
|
|
; page of the unit snapshot, i.e.
|
|
; $598D setFilmLimitToBufferEnd Sets filmEndPtr to $DFF0, the highest address a recording may reach.
|
|
; $5998 initFilmStream Prepares the film for recording once the snapshot is in place: writes the
|
|
; offset of the first command record ($0368) into header bytes $D002/$D003 and sets filmPtr to $D368, the first byte
|
|
; after the 800-byte unit snapshot.
|
|
; $59B3 isCmdRingNearlyFull Works out how much room is left in the 40-byte outgoing command ring at
|
|
; $9065-$908C and reports C=1 when fewer than 5 bytes are free, which is less than the longest command plus its
|
|
; arguments.
|
|
; $59D0 finishFilmPlayback End of a game film: reached when the replay runs past the last record, when the
|
|
; record length byte is $FF or when the aborted-game end marker is read.
|
|
; $5A14 checkLinkTimeout Watchdog used while the game is waiting for the opponent's packet.
|
|
; $5AD7 drawOverviewMap Draws the whole strategic overview: the 40x40 battlefield squeezed into the
|
|
; 20x20 character window at screen (1,1)-(20,20), four map cells (a 2x2 block) per character.
|
|
; $5B6C drawOverviewQuarterLine Puts the field markings back on top of an overview character.
|
|
; $5BAD buildOverviewQuadrant Colour-aware entry of the overview cell renderer.
|
|
; $5BD4 buildOverviewQuadrantNoColour ORs the 2x4 pixel pattern of map cell A into quadrant X of the 8-byte
|
|
; character buffer at $90B8, without touching the colour.
|
|
; $5C7C drawMapCell Redraws a single battlefield cell everywhere it can currently be seen.
|
|
; $5D1A drawViewportCell Draws one map cell as a 16x16 pixel tile (2x2 characters) in the 7x5 tactical
|
|
; view on the right of the battlefield screen.
|
|
; $5D7F drawScreen1Cell Redraws one map cell on screen 1, the comcen radar/scanner map, where the
|
|
; window is 21 cells wide by 17 tall at character (3,3) and every cell is a single 8x8 glyph.
|
|
; $5E03 clearOverviewWindow Points the text window at the 20x20 overview panel (left column 1, top row 1,
|
|
; $5E17 handleOwnUnitClick Entry taken by the battlefield main loop when the player clicks on one of his
|
|
; $5E24 runUnitOptionsMenu Single-unit branch of handleOwnUnitClick, also entered by JMP from the group
|
|
; $5FA6 exitUnitMenu Shared tail of every unit/group menu path: drops back to UI level 0 and
|
|
; releases the
|
|
; $5FB1 addMenuItem Appends item id X to menuItemIdList[menuItemCount++] and prints the item's name
|
|
; on the
|
|
; $5FD1 runMenuLoopWithFlag Alternative entry for the console and setup menus: with C = 1 it behaves
|
|
; $5FD7 runMenuLoop Menu selector without the unit-idle watch: patches menuIdleSnapshot+1 ($602A)
|
|
; to 0 and
|
|
; $5FDC runMenuLoopWatchUnit The generic vertical menu selector used by every in-game option menu.
|
|
; $60E5 unhighlightMenuRow Removes the highlight from the selected menu row, but only if it is currently
|
|
; $60EA toggleMenuRowHighlight Flips the highlight of menu row menuFirstRow + menuSelectedItem by EORing
|
|
; $612F runJoinGroupMenu The JOIN GROUP picker.
|
|
; $61CF runGroupOptionsMenu Group branch of handleOwnUnitClick.
|
|
; $633C printGroupTitleAndName Header of the group menu: prints GROUP (printGroupTitle) and, on the next
|
|
; $6358 printGroupTitle Moves the text cursor to column 4 of the info panel and prints the string GROUP
|
|
; $6366 clearGroupHighlight Drops the viewport's group highlight and repaints the 7x5 tactical view.
|
|
; $636E printSettingHeader Header of the group editor: clears the info panel in colour 4, moves to screen
|
|
; $6387 printFormationCentred Prints the word FORMATION ($C9D2) centred in the current text window; also
|
|
; $638E runGroupEditor Interactive editor for the group in selectedGroupKey, in MEMBERSHIP mode (zp_BB
|
|
; >=
|
|
; $64DA sendHaltGroupCmd Queues the 2-byte command $9B with the key of the currently selected group.
|
|
; $64E4 printSettingHeaderForLevel Clears the info panel in the colour that belongs to the current UI
|
|
; $64F6 printSettingWord Prints the word SETTING ($CB4F) centred in the current text window and ends
|
|
; $6500 promptForDestination runSetDestinationScreen - The SETTING / DESTINATION screen.
|
|
; $650E countGroupMembers Counts the live units that belong to the group whose key is in A.
|
|
; $652E runRepairWhichMenu Draws and runs the 'REPAIR WHICH:' menu of the REPAIR console tab.
|
|
; $65A0 runInGameOptionsMenu Runs the MISC-tab options menu while a battle is in progress.
|
|
; $65E7 waitForGroupRepositionsDone Before a group screen edits a group, wait until the re-position
|
|
; $65FF runFormationOptionsMenu The FORMATION sub-menu of the group options.
|
|
; $6637 soundPlayerTick The per-frame SID sound driver.
|
|
; $6651 updateSoundVoice Advances one SID voice by one frame.
|
|
; $66C7 runSoundScript Interprets one voice's script bytecode until it sets a note duration (or ends).
|
|
; $671C fetchSoundScriptByte Reads the next byte of a voice's script through the zero-page pointer at
|
|
; $6728 startSound Starts sound A (0-28) on the voices its table row names.
|
|
; $67CB unpackRleTo0200 Run-length decoder that always unpacks into the two pages $0200-$03FF.
|
|
; $681A fetchRleByte Returns the next byte of the packed stream and advances the pointer.
|
|
|
|
.org $0800
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; gameEntry - the game's front door. The boot loader does not return to its caller: it rebuilt the
|
|
; stack so that its final RTS lands in verifyProgramChecksum ($0461), and that routine ends with JMP
|
|
; $0800. This is also where the setup block jumps back to after a reset of the game state.
|
|
; In: nothing; $01 = $35 (I/O visible, no ROMs), interrupts pointing at the RTI in the boot loader.
|
|
; Out: never returns - control passes to mainStart.
|
|
; Called from: boot/eaBootFile $02D5, and game/scratchAndMessages0400 $04AB (the tail of
|
|
; verifyProgramChecksum).
|
|
; ----------------------------------------------------------------------
|
|
gameEntry:
|
|
jmp mainStart ; 0800 the three bytes at $0800 are the published entry point, so the real start-up code can live anywhere
|
|
|
|
|
|
; loaderStatus - sticky error flag of the fast loader. Bit 7 means 'the DISK ERROR message is
|
|
; currently on the status line'; the low bits keep the drive's error code from that failure ($02
|
|
; header not found, $03 no sync, $05 checksum, $08 write protected / wrong disk id). Only loadSectors
|
|
; reads or writes it.
|
|
loaderStatus:
|
|
.byte $00 ; 0803 . $00 = no error outstanding
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadSectors - the game's own copy of the fast loader. Reads A consecutive sectors of track Y,
|
|
; starting at sector X, into the address last given to setLoadDest, one sector per page. Every
|
|
; overlay, the battlefield map, the setup block and the trainer's playbook arrive through here. A
|
|
; drive error is not fatal: 'DISK ERROR.' goes up on the status line and the same sector is retried
|
|
; forever, so the player can close the drive door or put the right disk back in and the load simply
|
|
; continues.
|
|
; In: A = number of sectors, Y = track, X = first sector, loadDestLo/loadDestHi = destination page.
|
|
; Out: the sectors in memory, loadDestHi advanced one page per sector, C=0, the caller's I flag
|
|
; restored; ldTrack/ldSector/ldCount clobbered.
|
|
; Called from: $0B58, $0F10, $0F38, $0F41, $0F5C in this file, game/mapGenerator6F00 $7B2D and
|
|
; game/trainerAiE000 $E0B4/$E0D6/$ECB3.
|
|
; ----------------------------------------------------------------------
|
|
loadSectors:
|
|
php ; 0804 remember the caller's interrupt flag - the transfer itself is cycle counted
|
|
sei ; 0805 no interrupts from here to the RTS: a raster IRQ in the middle of a sector would lose bytes
|
|
sta ldCount ; 0806 sectors still to transfer
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass per sector: ask the drive for it, retry until it answers 'status 0', then move the
|
|
; destination on one page and step to the next sector of the same track.
|
|
; ----------------------------------------------------------------------
|
|
loadSectorsLoop:
|
|
jsr readOneSector ; 0808 fetch one sector into (loadDestLo/Hi); returns C=1 and the drive's error code in A on failure
|
|
bcc loadSectorOk ; 080B branch when the drive reported no error
|
|
ora #$80 ; 080D keep the error code but set bit 7 = 'the error message is on the screen'
|
|
sta loaderStatus ; 080F remember that so the message can be taken down after a successful retry
|
|
lda #$A2 ; 0812 low byte of msgDiskError ($0FA2 = 'DISK ERROR.')
|
|
ldy #$0F ; 0814 high byte of msgDiskError
|
|
jsr showMessageString ; 0816 print it centred on the status line (showMessageString $C3D8)
|
|
ldy ldTrack ; 0819 reload the track of the sector that failed...
|
|
ldx ldSector ; 081B ...and its sector number
|
|
jmp loadSectorsLoop ; 081D retry the same sector forever - the loader never gives up
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A sector came in cleanly. If an earlier attempt had put DISK ERROR on the status line, take it down
|
|
; again and put the message the game wants there back.
|
|
; ----------------------------------------------------------------------
|
|
loadSectorOk:
|
|
lda loaderStatus ; 0820 did any earlier attempt put DISK ERROR on the screen?
|
|
bpl loadSectorNext ; 0823 no message to take down - go straight on
|
|
and #$7F ; 0825 clear the 'message showing' bit, leaving the last error code behind for a post mortem
|
|
sta loaderStatus ; 0827 store the cleared flag
|
|
lda persistentMessageId ; 082A persistentMessageId = the message the status line should fall back to
|
|
jsr showStatusMessage ; 082D reprint it over the DISK ERROR text (showStatusMessage $C6FF)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Advance to the next sector of the run: one sector forward on the disk, one page forward in memory.
|
|
; ----------------------------------------------------------------------
|
|
loadSectorNext:
|
|
inc ldSector ; 0830 next sector of this track
|
|
inc loadDestHi ; 0832 next destination page - one sector is exactly 256 bytes
|
|
ldx ldSector ; 0835 sector number for the next request
|
|
ldy ldTrack ; 0837 track number for the next request (never changes inside one call)
|
|
dec ldCount ; 0839 one sector fewer to go
|
|
bne loadSectorsLoop ; 083B loop until the whole run has been read
|
|
plp ; 083D restore the caller's interrupt flag
|
|
clc ; 083E C=0: a read can only ever end successfully, it retries instead of failing
|
|
rts ; 083F back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Failure exit of writeSectors: the drive refused a sector (write protected, wrong disk id, or it
|
|
; could not find the header). Unlike a read this is not retried - the caller shows a message and
|
|
; gives up.
|
|
; ----------------------------------------------------------------------
|
|
writeSectorsFail:
|
|
plp ; 0840 restore the caller's interrupt flag
|
|
sec ; 0841 C=1 = the save failed
|
|
rts ; 0842 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeSectors - the write half of the fast loader: sends A consecutive sectors from the address set
|
|
; by setLoadDest to track Y starting at sector X, one page per sector. The drive encrypts each block
|
|
; with the same rolling XOR that a read decrypts with (cryptSectorBuffer $0531 in the drive code).
|
|
; This is the only path that ever sends command $60, and it is used solely to save a game film and the
|
|
; setup block to the save slots on track 18.
|
|
; In: A = number of sectors, Y = track, X = first sector, loadDestLo/loadDestHi = source page.
|
|
; Out: C=0 when every sector went out, C=1 on the first refusal; loadDestHi advanced.
|
|
; Called from: game/mapGenerator6F00 $7E85 (save game film) and $8194 (save setup).
|
|
; ----------------------------------------------------------------------
|
|
writeSectors:
|
|
php ; 0843 remember the caller's interrupt flag
|
|
sei ; 0844 interrupts off for the whole transfer
|
|
sta ldCount ; 0845 sectors still to write
|
|
writeSectorsLoop:
|
|
jsr writeOneSector ; 0847 write one sector; C=1 if the drive refused it
|
|
bcs writeSectorsFail ; 084A give up on the first error instead of retrying
|
|
inc ldSector ; 084C next sector of the track
|
|
inc loadDestHi ; 084E next source page
|
|
ldx ldSector ; 0851 sector number for the next request
|
|
ldy ldTrack ; 0853 track number for the next request
|
|
dec ldCount ; 0855 one sector fewer to go
|
|
bne writeSectorsLoop ; 0857 loop until the whole run has been written
|
|
plp ; 0859 restore the caller's interrupt flag
|
|
clc ; 085A C=0 = everything was written
|
|
rts ; 085B back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setLoadDest - names the memory the next transfer works on. It does not use a variable: it patches
|
|
; the operand bytes loadDestLo ($09A2) and loadDestHi ($09A3), which readSectorBody and writeOneSector
|
|
; then copy into the zero-page pointers the byte loops actually use. Only the page matters in
|
|
; practice, since every transfer is a whole number of 256-byte sectors.
|
|
; In: X = address low byte, Y = address high byte.
|
|
; Out: loadDestLo/loadDestHi patched.
|
|
; Called from: every overlay load in this file ($0B4F, $0F07, $0F1A, $0FC8, $104B, $1064, $1074,
|
|
; $108D, $10AF, $10BF), game/mapGenerator6F00 ($7B24, $7E7D, $7E8F, $7F7B, $818B) and the trainer
|
|
; ($E12C, $ECAA).
|
|
; ----------------------------------------------------------------------
|
|
setLoadDest:
|
|
stx loadDestLo ; 085C low byte of the destination (or source) address
|
|
sty loadDestHi ; 085F high byte; loadSectors/writeSectors increment this one as they advance page by page
|
|
rts ; 0862 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeOneSector - writes a single sector: command $60, then the track and the sector number, then the
|
|
; 256 bytes. CIA1 timer A is stopped across the transfer, exactly as on the read path, and started
|
|
; again afterwards - on this side it is only belt and braces, since the CIA interrupts were masked at
|
|
; start-up.
|
|
; In: Y = track, X = sector, loadDestLo/loadDestHi = the page to write.
|
|
; Out: C=1 when the drive's status byte is non-zero (through finishTransfer); ldTrack/ldSector set;
|
|
; wrSrcPtr points at the source page.
|
|
; Called from: writeSectors $0847.
|
|
; ----------------------------------------------------------------------
|
|
writeOneSector:
|
|
sei ; 0863 no interrupts - the handshake with the drive must not be broken
|
|
sty ldTrack ; 0864 keep the track where the caller's loop can find it again
|
|
stx ldSector ; 0866 same for the sector number
|
|
lda loadDestLo ; 0868 the page setLoadDest patched in...
|
|
sta wrSrcPtr ; 086B ...becomes the low byte of the zero-page source pointer
|
|
lda loadDestHi ; 086D high byte of the source page
|
|
sta wrSrcPtrHi ; 0870 sendSectorData reads the 256 bytes through (wrSrcPtr),y
|
|
lda #$60 ; 0872 $60 = the drive's 'write a sector' command (handleWriteCommand $0490 in the drive code)
|
|
jsr sendByteToDrive ; 0874 send the command byte
|
|
lda ldTrack ; 0877 the track to write to
|
|
jsr sendByteToDrive ; 0879 send it
|
|
lda ldSector ; 087C the sector to write to
|
|
jsr sendByteToDrive ; 087E send it
|
|
ldx #$00 ; 0881 CRA bit 0 = 0...
|
|
stx CIA1_CRA ; 0883 ...stops CIA1 timer A, the 60 Hz tick the KERNAL left running, so its underflow cannot disturb the exchange
|
|
jsr sendSectorData ; 0886 hand over the 256 bytes and collect the status byte
|
|
ldx #$01 ; 0889 CRA = $01: timer A on, continuous mode...
|
|
stx CIA1_CRA ; 088B ...restart it now that the drive is done
|
|
rts ; 088E C still holds finishTransfer's verdict
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendSectorData - pushes the 256 bytes at (wrSrcPtr) to the drive one handshaked byte at a time, then
|
|
; falls into finishTransfer to read back the status byte. The drive receives them the slow way too
|
|
; (writeSectorToDisk $04A0), so no cycle counting is needed on this side.
|
|
; In: wrSrcPtr/wrSrcPtrHi = the page to send.
|
|
; Out: 256 bytes sent; C=1 if the drive's status byte is non-zero.
|
|
; Called from: writeOneSector $0886.
|
|
; ----------------------------------------------------------------------
|
|
sendSectorData:
|
|
ldy #$00 ; 088F start at the first byte of the page
|
|
L_0891:
|
|
lda (wrSrcPtr),y ; 0891 next byte of the sector
|
|
jsr sendByteToDrive ; 0893 clock it out (the byte coming back is ignored)
|
|
iny ; 0896 advance through the page
|
|
bne L_0891 ; 0897 loop until Y wraps, i.e. all 256 bytes are gone
|
|
jmp finishTransfer ; 0899 read the drive's verdict and return it in C
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendByteToDrive - the slow, fully handshaked, full-duplex byte exchange with the drive; the other
|
|
; end is exchangeByteWithHost ($034B) in the drive code. Eight times over it samples the drive's bit
|
|
; off DATA IN, pulls CLK to acknowledge, puts its own bit on DATA OUT, releases CLK and waits for the
|
|
; drive to pull CLK back - one bit each way per handshake, so neither side has to be cycle accurate.
|
|
; Every command, track and sector byte goes out this way, and the status byte is collected by
|
|
; exchanging a dummy byte. Both shift registers work LSB first (LSR sendByteShift / ROR
|
|
; recvByteShift); the phase-1 note saying 'MSB first' was wrong.
|
|
; In: A = the byte to send.
|
|
; Out: A = the byte received from the drive, C=0, X=0, sendByteShift = 0, recvByteShift = the received
|
|
; byte.
|
|
; Called from: writeOneSector ($0874/$0879/$087E), sendSectorData ($0893), readSectorBody
|
|
; ($0900/$0905/$090A) and finishTransfer ($0949); also from the modem driver and the trainer at
|
|
; $EA1D/$EA29.
|
|
; CIA2 port A ($DD00) is the serial bus: bit 4 = CLK OUT, bit 5 = DATA OUT, bit 6 = CLK IN, bit 7 =
|
|
; DATA IN, and bits 0-1 are the VIC bank select, which must survive every write (the game runs in bank
|
|
; 2, so they read %01). The C64's outputs are inverted by the bus drivers - writing a 1 pulls the
|
|
; line low - while its inputs read the true line level, so a line that somebody is pulling reads back
|
|
; as 0. In the drive it is the other way round, which is why the two sides look mirrored.
|
|
; ----------------------------------------------------------------------
|
|
sendByteToDrive:
|
|
sta sendByteShift ; 089C the byte to send goes into the output shift register
|
|
ldx #$08 ; 089F eight bits, one per pass of the loop
|
|
L_08A1:
|
|
bit CIA2_PRA ; 08A1 poll the serial port; V = bit 6 = CLK IN
|
|
bvc L_08A1 ; 08A4 wait until the drive releases CLK - by then its bit is sitting on DATA
|
|
lda CIA2_PRA ; 08A6 read the port so the VIC bank bits can be written back unchanged
|
|
and #$DF ; 08A9 clear bit 5: release our DATA OUT so the line shows only what the drive drives
|
|
sta CIA2_PRA ; 08AB let go of DATA
|
|
lda CIA2_PRA ; 08AE sample the port with the drive's bit on DATA IN
|
|
cmp #$80 ; 08B1 C = bit 7 = the drive's data bit (line high = a 1 bit)
|
|
ror recvByteShift ; 08B3 shift it in from the top; after eight rounds the first bit has reached bit 0, i.e. LSB first
|
|
ora #$10 ; 08B6 set bit 4 = pull CLK low: 'I have taken your bit'
|
|
sta CIA2_PRA ; 08B8 assert CLK
|
|
and #$BF ; 08BB clear bit 6 in the copy: that is what the port reads back now that CLK is low
|
|
L_08BD:
|
|
cmp CIA2_PRA ; 08BD compare the live port against that snapshot
|
|
beq L_08BD ; 08C0 wait until it differs - the drive toggles DATA as its acknowledgement ($0364 in the drive code)
|
|
lsr sendByteShift ; 08C2 shift the next outgoing bit (LSB first) into the carry
|
|
lda #$00 ; 08C5 a 0 bit: leave DATA OUT clear so the line stays high
|
|
bcc L_08CB ; 08C7 branch when the bit to send is 0
|
|
lda #$20 ; 08C9 a 1 bit: bit 5 = DATA OUT, which pulls the line low; the drive reads its inputs inverted and sees a 1
|
|
L_08CB:
|
|
ora CIA2_PRA ; 08CB merge with the live port so the bank bits and CLK OUT survive
|
|
sta CIA2_PRA ; 08CE put the bit on the DATA line
|
|
and #$EF ; 08D1 clear bit 4 = release CLK: 'the bit is on the line, take it'
|
|
sta CIA2_PRA ; 08D3 release CLK
|
|
L_08D6:
|
|
bit CIA2_PRA ; 08D6 poll the port again; V = CLK IN
|
|
bvs L_08D6 ; 08D9 wait until the drive pulls CLK low as its acknowledgement ($037F in the drive code)
|
|
eor #$20 ; 08DB toggle our own DATA OUT bit - the handshake the drive waits for at $0389
|
|
sta CIA2_PRA ; 08DD write the toggled value
|
|
dex ; 08E0 one bit done
|
|
bne L_08A1 ; 08E1 round again until all eight bits have been exchanged
|
|
lda recvByteShift ; 08E3 return whatever the drive sent back (the status byte, when that is what we came for)
|
|
clc ; 08E6 C=0 - readSectorBody depends on this: its ADC #$2F must add exactly $2F
|
|
rts ; 08E7 back to the caller
|
|
|
|
|
|
; sendByteShift - the outgoing shift register of sendByteToDrive; LSR pushes bit 0 out first, so it
|
|
; holds 0 when the byte has gone.
|
|
sendByteShift:
|
|
.byte $00 ; 08E8 . outgoing bit buffer
|
|
|
|
; recvByteShift - the incoming shift register of sendByteToDrive; ROR feeds bits in at the top, so
|
|
; after eight of them the first bit received sits in bit 0. The second byte, $08EA, is never
|
|
; referenced - a pad byte. The
|
|
; $BE the register holds in the disk image is just what the assembler left behind.
|
|
recvByteShift:
|
|
.byte $BE,$00 ; 08E9 .. incoming bit buffer; the $BE it holds in the disk image is just whatever the assembler left behind
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readOneSector - reads one sector into the load destination. It stops CIA1 timer A for the duration
|
|
; (the 60 Hz tick the KERNAL left running) so that absolutely nothing can interfere with the
|
|
; cycle-counted receive loop, and restarts it afterwards.
|
|
; In: Y = track, X = sector, loadDestLo/loadDestHi = destination page.
|
|
; Out: 256 bytes in memory, ldTrack/ldSector set, C=1 and A = the error code when the drive reports a
|
|
; problem.
|
|
; Called from: loadSectors $0808.
|
|
; ----------------------------------------------------------------------
|
|
readOneSector:
|
|
sei ; 08EB interrupts off - from here on the transfer is counted in cycles
|
|
sty ldTrack ; 08EC keep the track for the retry logic in loadSectors
|
|
stx ldSector ; 08EE and the sector number
|
|
ldx #$00 ; 08F0 CRA bit 0 = 0...
|
|
stx CIA1_CRA ; 08F2 ...stop CIA1 timer A while the sector comes in
|
|
jsr readSectorBody ; 08F5 do the actual request and receive
|
|
ldx #$01 ; 08F8 CRA = $01: timer A running, continuous mode...
|
|
stx CIA1_CRA ; 08FA ...start it again
|
|
rts ; 08FD C and A carry the drive's verdict back to loadSectors
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readSectorBody - the body of a sector read. It sends command $80 with the track and sector, parks
|
|
; the sprites, works out the raster constant the receive loop uses to dodge bad lines, waits for the
|
|
; drive to announce that the sector is decoded, and then runs the fast 2-bits-per-port-read receive
|
|
; loop.
|
|
; In: ldTrack, ldSector, loadDestLo/loadDestHi.
|
|
; Out: 256 bytes at the destination, savedSpriteEnable = the sprite enable register as it was,
|
|
; ldCia2Bits = the VIC bank bits, the operand at $095F patched, C=1 on a drive error.
|
|
; Called from: readOneSector $08F5.
|
|
; ----------------------------------------------------------------------
|
|
readSectorBody:
|
|
lda #$80 ; 08FE $80 = the drive's 'read a sector' command (anything that is not $C0 or $60 in commandDispatchLoop $0313)
|
|
jsr sendByteToDrive ; 0900 send the command byte
|
|
lda ldTrack ; 0903 the track to read
|
|
jsr sendByteToDrive ; 0905 send it
|
|
lda ldSector ; 0908 the sector to read
|
|
jsr sendByteToDrive ; 090A send it
|
|
lda VIC_SPR_ENA ; 090D the sprite enable register as the game left it...
|
|
sta savedSpriteEnable ; 0910 ...saved so the display can be put back together afterwards
|
|
lda CIA2_PRA ; 0913 read CIA2 port A...
|
|
and #$03 ; 0916 ...and keep only bits 0-1, the VIC bank select (%01 = bank 2, where the bitmap and screen live)
|
|
sta ldCia2Bits ; 0918 the receive loop rewrites the port from this on every strobe, so the bank must never move
|
|
ora #$20 ; 091A add bit 5 = DATA OUT: pull DATA low, which tells the drive 'not ready for a byte yet'
|
|
sta CIA2_PRA ; 091C this also releases CLK OUT and the ATN and RS-232 lines
|
|
lda VIC_CTRL1 ; 091F $D011 holds the vertical fine scroll in bits 0-2...
|
|
and #$07 ; 0922 ...isolate it: a bad line happens whenever (raster AND 7) equals that value
|
|
adc #$2F ; 0924 $2F = one line before the first display line ($30); C is 0 here because sendByteToDrive ended with CLC
|
|
sta L_095E+1 ; 0926 patch the SBC operand of the receive loop at $095F, so the loop can test 'is the next line a bad line?'
|
|
L_0929:
|
|
bit CIA2_PRA ; 0929 poll the serial port; V = CLK IN
|
|
bvs L_0929 ; 092C wait until the drive pulls CLK low = 'the sector is read, decrypted and ready to go' ($044D in the drive code)
|
|
lda #$00 ; 092E no sprites at all...
|
|
sta VIC_SPR_ENA ; 0930 ...so the VIC cannot steal cycles with sprite DMA while the sector comes in
|
|
lda loadDestLo ; 0933 copy the patched destination address...
|
|
sta ldDestPtr ; 0936 ...into the zero-page pointer the receive loop stores through
|
|
lda loadDestHi ; 0938 high byte of the destination page
|
|
sta ldDestPtrHi ; 093B (ldDestPtr),y then walks the whole 256-byte sector
|
|
jsr receiveSectorData ; 093D take the 256 bytes
|
|
jmp finishTransfer ; 0940 put the sprites back and read the drive's status byte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; finishTransfer - shared tail of a read and a write: puts the sprite enable register back the way it
|
|
; was and collects the drive's status byte, which arrives on the ordinary handshaked path. Status 0
|
|
; means success; anything else is a DOS error code ($02 header not found, $03 no sync, $05 data
|
|
; checksum, $08 write protected or wrong disk id).
|
|
; In: savedSpriteEnable.
|
|
; Out: A = the status byte, C=1 when it is non-zero, sprites restored.
|
|
; Called from: readSectorBody $0940 and sendSectorData $0899, both by JMP, so its RTS returns to their
|
|
; callers.
|
|
; ----------------------------------------------------------------------
|
|
finishTransfer:
|
|
lda savedSpriteEnable ; 0943 the sprite enable register as it was before the transfer...
|
|
sta VIC_SPR_ENA ; 0946 ...restored, so the cursor and marker sprites come back
|
|
jsr sendByteToDrive ; 0949 exchange a dummy byte (the sprite mask happens to be in A) to collect the status byte
|
|
beq L_094F ; 094C status 0 = no error, leave C=0
|
|
sec ; 094E C=1 tells the caller the sector failed
|
|
L_094F:
|
|
rts ; 094F A still holds the status byte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; receiveSectorData - the fast half of the protocol and the only cycle-counted code in the game. The
|
|
; drive does not handshake per bit here: it writes four port values per byte at fixed intervals
|
|
; (sendSectorToHost $044B), each carrying two data bits on CLK and DATA, and the only synchronisation
|
|
; is per byte - the host releases DATA to ask for the next one and then samples four times at fixed
|
|
; cycle offsets. That is why sprites are off, CIA1 timer A is stopped, interrupts are disabled and
|
|
; each byte only starts on a raster line that is not immediately before a bad line. The four samples
|
|
; are folded together with LSR/EOR so that the two bits of sample 1 end up in bits 0-1 and those of
|
|
; sample 4 in bits 6-7; the VIC bank bits shift out of the bottom on their own, except in the last
|
|
; sample, which is why they are EORed out explicitly at $0984.
|
|
; In: ldDestPtr/ldDestPtrHi = destination page, ldCia2Bits = the VIC bank bits, the SBC operand at
|
|
; $095F.
|
|
; Out: 256 bytes stored at (ldDestPtr); the bus left with DATA pulled low.
|
|
; Called from: readSectorBody $093D.
|
|
; ----------------------------------------------------------------------
|
|
receiveSectorData:
|
|
bit CIA2_PRA ; 0950 poll the serial port; V = CLK IN
|
|
bvc receiveSectorData ; 0953 wait until the drive releases CLK again - it does that just before every byte ($0466 in the drive code)
|
|
ldy #$00 ; 0955 index of the first byte of the sector
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass per byte of the sector. The pass first waits for a safe raster line, then releases DATA to
|
|
; ask for the byte and samples the port four times, two bits at a time.
|
|
; ----------------------------------------------------------------------
|
|
receiveByteLoop:
|
|
sec ; 0957 SBC below must subtract exactly the patched constant
|
|
ldx ldCia2Bits ; 0958 the bank bits with CLK OUT and DATA OUT clear: writing this releases both lines
|
|
nop ; 095A one cycle of padding
|
|
L_095B:
|
|
lda VIC_RASTER ; 095B where is the raster right now?
|
|
L_095E:
|
|
sbc #$32 ; 095E operand patched at $0926 with (yscroll + $2F): subtracts the top of the display window
|
|
bcc L_0966 ; 0960 above the display window there are no bad lines - go straight ahead
|
|
and #$07 ; 0962 how far the raster is from the next bad line
|
|
beq L_095B ; 0964 wait while the next line would be a bad line: the VIC would steal 40 cycles in the middle of the byte
|
|
L_0966:
|
|
stx CIA2_PRA ; 0966 release CLK OUT and DATA OUT: 'send me the next byte' - the drive waits for exactly this at $046C
|
|
txa ; 0969 take a copy of the bank bits...
|
|
ora #$20 ; 096A ...with bit 5 set, i.e. DATA OUT asserted, ready to say 'got it' after the byte
|
|
tax ; 096C keep it in X until then
|
|
nop ; 096D padding so the first sample lands on the drive's first port write
|
|
nop ; 096E padding
|
|
bit loaderTimingPad ; 096F BIT on an unused zero-page byte: three cycles of padding, nothing is tested
|
|
lda CIA2_PRA ; 0971 sample 1: CLK IN and DATA IN carry data bits 0 and 1
|
|
lsr a ; 0974 shift them down two places...
|
|
lsr a ; 0975 ...so the next sample can be merged in above them (the bank bits fall off the bottom)
|
|
nop ; 0976 padding to hit the drive's second write
|
|
eor CIA2_PRA ; 0977 sample 2: data bits 2 and 3
|
|
lsr a ; 097A make room for the next pair...
|
|
lsr a ; 097B ...again dropping the bank bits of the older sample
|
|
nop ; 097C padding
|
|
nop ; 097D padding
|
|
nop ; 097E padding to hit the drive's third write
|
|
eor CIA2_PRA ; 097F sample 3: data bits 4 and 5
|
|
lsr a ; 0982 shift the four bits gathered so far down...
|
|
lsr a ; 0983 ...into their final positions
|
|
eor ldCia2Bits ; 0984 cancel the bank bits of the last sample in advance - they are not shifted out any more
|
|
eor CIA2_PRA ; 0986 sample 4: data bits 6 and 7; the byte is now complete
|
|
stx CIA2_PRA ; 0989 pull DATA low again: 'byte taken, do not send yet'
|
|
nop ; 098C padding so the store cannot overlap the drive's last write
|
|
sta (ldDestPtr),y ; 098D put the byte in the sector buffer
|
|
iny ; 098F next byte
|
|
bne receiveByteLoop ; 0990 loop until Y wraps: 256 bytes received
|
|
rts ; 0992 back to readSectorBody, which reads the status byte
|
|
|
|
|
|
; strBlockExecuteLeftover - the DOS command strings 'B-E 2 0 1 17' and '#' that the boot loader
|
|
; ($C32C) sends to start the drive code, left in this copy of the loader. The game never opens a
|
|
; KERNAL channel (the ROMs are banked out by the time $0800 runs), and nothing references these bytes:
|
|
; dead weight carried over when the loader was copied into the main program.
|
|
strBlockExecuteLeftover:
|
|
.byte "B-E 2 0 1 17",$00,"#",$00; 0993 'B-E 2 0 1 17' = block-execute track 1 sector 17 in the drive, then a 0 terminator, the filename '#' and another 0
|
|
|
|
; loadDestLo - operand byte of the LDA at $0868/$0933: the low byte of the transfer address, patched
|
|
; by setLoadDest.
|
|
loadDestLo:
|
|
.byte $00 ; 09A2 . low byte of the next transfer address
|
|
|
|
; loadDestHi - operand byte of the LDA at $086D/$0938: the high byte of the transfer address.
|
|
; loadSectors and writeSectors increment it after every sector, so it walks the destination page by
|
|
; page. $09A4 is a pad byte nothing touches.
|
|
loadDestHi:
|
|
.byte $00,$00 ; 09A3 .. high byte of the next transfer address
|
|
|
|
; savedSpriteEnable - VIC_SPR_ENA as it was when a sector transfer started; the sprites are blanked so
|
|
; that sprite DMA cannot steal cycles, and this puts them back.
|
|
savedSpriteEnable:
|
|
.byte $00 ; 09A5 . sprite enable register saved across one sector transfer
|
|
|
|
; ----------------------------------------------------------------------
|
|
; manualLookupCheck - the manual look-up copy protection, and it is disabled in this build: the JSR
|
|
; that called it at $0A9B was assembled as BIT ($2C instead of $20), so it is never entered. It picks
|
|
; one of the 32 map seeds in the table at $0401 using the boot-time random byte at $09C3, generates
|
|
; and draws that battlefield, prints 'IDENTIFY THIS MAP (SEE MANUAL)' and lets the player type the
|
|
; five-letter map name (the printable form of the 24-bit seed) three times. A wrong answer three
|
|
; times over wipes memory downwards from $C000 and hangs the machine. The current map seed and map
|
|
; name are saved on the stack and put back on success, so a real game is unaffected.
|
|
; In: the answer table mapNameSeedTable ($0401), the random byte at $09C3, the player's typing
|
|
; through enterMapName ($7717), which packs the five characters into scenarioSeed0-2 ($5A-$5C).
|
|
; Out: mapSeed and mapNameText restored, textColour/textPadColour = 0, the info panel cleared; on
|
|
; failure it never returns.
|
|
; Called from: nobody - the only reference is the disabled JSR at $0A9B.
|
|
; ----------------------------------------------------------------------
|
|
manualLookupCheck:
|
|
lda mapSeed ; 09A6 byte 0 of the map seed of the game in progress...
|
|
pha ; 09A9 ...onto the stack
|
|
lda mapSeed1 ; 09AA byte 1 of the map seed
|
|
pha ; 09AD saved
|
|
lda mapSeed2 ; 09AE byte 2 of the map seed
|
|
pha ; 09B1 saved
|
|
ldy #$00 ; 09B2 start at the first character of the map name
|
|
L_09B4:
|
|
lda mapNameText,y ; 09B4 the printable map name plus its terminator ($0BA9-$0BAE)
|
|
pha ; 09B7 push all six bytes; they come back off in reverse at $0A2B
|
|
iny ; 09B8 next character
|
|
cpy #$06 ; 09B9 five characters and the terminator byte
|
|
bcc L_09B4 ; 09BB loop until all six are on the stack
|
|
lda #$03 ; 09BD three attempts...
|
|
sta manualCheckTries ; 09BF ...counted down in manualCheckTries
|
|
lda #$00 ; 09C2 the operand byte $09C3 is the random seed the boot loader took from the SID noise generator ($C215) and verifyProgramChecksum preserves ($0461)
|
|
and #$1F ; 09C4 32 questions in the manual, so keep 5 bits
|
|
sta scratch18 ; 09C6 hold the question number
|
|
asl a ; 09C8 x2...
|
|
adc scratch18 ; 09C9 ...plus one more = x3, the stride of the 3-byte answer table
|
|
tay ; 09CB index the table with it
|
|
lda mapNameSeedTable,y ; 09CC byte 0 of that map's seed (also trainerPlaybook0200:play3Col13)
|
|
sta mapSeed ; 09CF make it the current map seed
|
|
iny ; 09D2 next byte of the entry
|
|
lda mapNameSeedTable,y ; 09D3 byte 1 (also trainerPlaybook0200:play3Col13)
|
|
sta mapSeed1 ; 09D6 stored
|
|
iny ; 09D9 last byte of the entry
|
|
lda mapNameSeedTable,y ; 09DA byte 2 (also trainerPlaybook0200:play3Col13)
|
|
sta mapSeed2 ; 09DD stored
|
|
jsr generateBattlefieldMap ; 09E0 build the 40x40 battlefield from that seed (map generator overlay, $6F04)
|
|
jsr drawOverviewMap ; 09E3 draw it in the overview window so the player can compare it with the manual
|
|
lda #$02 ; 09E6 colour byte $02 for the prompt cells: with the inverted font the strokes come out red on black
|
|
sta textColour ; 09E8 colour of the printed characters
|
|
sta textPadColour ; 09EA colour of the blanks that pad a centred line
|
|
jsr clearInfoPanel ; 09EC clear the info panel to that colour
|
|
lda #$47 ; 09EF low byte of the prompt - $0A47, one character before msgIdentifyMap, so the text starts with an extra space
|
|
ldy #$0A ; 09F1 high byte of the prompt
|
|
jsr printString ; 09F3 print 'IDENTIFY THIS MAP (SEE MANUAL)' (it contains two $0D line breaks)
|
|
L_09F6:
|
|
jsr clearMapNameField ; 09F6 blank the five-character name field ($7836 in the overlay)
|
|
jsr missileLineFromComcen ; 09F9 let the player type a map name; it is packed 5 bits per letter into scenarioSeed0-2 (also mapGenerator6F00:enterMapName)
|
|
lda scenarioSeed0 ; 09FC byte 0 of what was typed
|
|
cmp mapSeed ; 09FE against byte 0 of the seed the map was built from
|
|
bne L_0A11 ; 0A01 wrong - no need to look further
|
|
lda scenarioSeed1 ; 0A03 byte 1 typed
|
|
cmp mapSeed1 ; 0A05 against byte 1 of the seed
|
|
bne L_0A11 ; 0A08 wrong
|
|
lda scenarioSeed2 ; 0A0A byte 2 typed
|
|
cmp mapSeed2 ; 0A0C against byte 2 of the seed
|
|
beq L_0A29 ; 0A0F all three match: the player owns the manual
|
|
L_0A11:
|
|
lda #$03 ; 0A11 sound 3 = the error beep
|
|
jsr playSound ; 0A13 play it
|
|
dec manualCheckTries ; 0A16 one attempt gone
|
|
bne L_09F6 ; 0A19 ask again while attempts are left
|
|
ldy #$00 ; 0A1B index 0...
|
|
tya ; 0A1D ...and A = 0: the byte written everywhere
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Three wrong answers: erase the machine. The loop zeroes one page at a time, walking downwards from
|
|
; $C000 by decrementing the high byte of its own STA operand, and stops when that byte reaches 0.
|
|
; Long before then it wipes page $0A - its own code - so the CPU runs into zeroed memory and the
|
|
; machine has to be switched off. The tables above $C0FF survive, which is of no use to anybody.
|
|
; ----------------------------------------------------------------------
|
|
wipeMemoryLoop:
|
|
sta bootEntry,y ; 0A1E zero one byte; the operand high byte at $0A20 is what makes this walk down memory (also textEngineC000:bitPairHighTable)
|
|
iny ; 0A21 next byte of the page
|
|
bne wipeMemoryLoop ; 0A22 256 bytes per pass
|
|
dec wipeMemoryLoop+2 ; 0A24 step the STA operand down to the previous page
|
|
bne wipeMemoryLoop ; 0A27 keep going until the operand high byte wraps to 0 - by then this code is gone too
|
|
L_0A29:
|
|
ldy #$05 ; 0A29 restore the six saved map-name bytes, last pushed first
|
|
L_0A2B:
|
|
pla ; 0A2B pull one byte
|
|
sta mapNameText,y ; 0A2C back into the map name
|
|
dey ; 0A2F work backwards
|
|
bpl L_0A2B ; 0A30 until all six are back
|
|
pla ; 0A32 byte 2 of the saved map seed
|
|
sta mapSeed2 ; 0A33 restored
|
|
pla ; 0A36 byte 1
|
|
sta mapSeed1 ; 0A37 restored
|
|
pla ; 0A3A byte 0
|
|
sta mapSeed ; 0A3B restored - the game in progress never notices the detour
|
|
lda #$00 ; 0A3E back to colour 0...
|
|
sta textColour ; 0A40 ...for printed characters
|
|
sta textPadColour ; 0A42 ...and for the padding
|
|
jmp clearInfoPanel ; 0A44 clear the info panel again and return through it
|
|
|
|
|
|
; padA47: unknown, 1 bytes. single $20 byte before msgIdentifyMap
|
|
msgIdentifyMapLeadSpace:
|
|
.byte $20 ; 0A47 a plain space: printString is called with $0A47, so this is really the first character of the prompt below
|
|
|
|
; msgIdentifyMap: text, 36 bytes. copy-protection prompt
|
|
msgIdentifyMap:
|
|
.byte " IDENTIFY",$0D," THIS MAP",$0D," (SEE MANUAL",')'|$80; 0A48 'IDENTIFY THIS MAP (SEE MANUAL)' - the $0D bytes are line breaks and the ')' carries bit 7 as the terminator
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mainStart - the cold start of the game, run once. It silences the CIAs, parks both hardware vectors
|
|
; on an RTI, pulls in the runtime overlays (text engine, setup overlay, message strings and the $E000
|
|
; opponent module), loads the saved setup block and runs the little start-up routine at $0300 that
|
|
; came with it, installs the raster IRQ, paints the empty battlefield screen and drops into the main
|
|
; menu. The manual look-up check that used to run here is disabled in this build.
|
|
; In: nothing but the memory image the boot loader left behind.
|
|
; Out: everything initialised: overlays resident, IRQ running, currentScreen = 0, playerSide = $FE (no
|
|
; side chosen); falls straight into returnToMainMenu.
|
|
; Called from: gameEntry $0800.
|
|
; ----------------------------------------------------------------------
|
|
mainStart:
|
|
sei ; 0A6C no interrupts until the vectors are safe
|
|
jsr disableCiaInterrupts ; 0A6D mask and acknowledge both CIAs so no timer or serial interrupt can arrive
|
|
lda #$98 ; 0A70 low byte of irqRti ($1298)...
|
|
sta nmiVector ; 0A72 ...into the NMI vector - the ROMs are banked out, so these RAM vectors are the real ones
|
|
sta irqVector ; 0A75 and into the IRQ vector, until installRasterIrq puts the real handler there
|
|
lda #$12 ; 0A78 high byte of irqRti
|
|
sta nmiVectorHi ; 0A7A NMI vector high byte
|
|
sta irqVectorHi ; 0A7D IRQ vector high byte - any interrupt now just RTIs
|
|
cli ; 0A80 interrupts on again; nothing can happen except an RTI
|
|
jsr loadStartupOverlays ; 0A81 pull in $C000 (text engine), the $6F00 setup overlay, the message strings and the $E000 opponent module
|
|
jsr loadSetupBlock ; 0A84 load the saved setup block to $0200-$03FF (track 18 sectors 15-16)
|
|
jsr basicVectorHijack ; 0A87 run initGameSystems ($0300), the start-up code that came inside that block: RNG seed, fonts, map row tables, bitmap screen (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
jsr initIrqAndInputState ; 0A8A install the raster IRQ and clear the input state
|
|
jsr initInterruptVectors ; 0A8D point the NMI vector and the comm module's copy of it at the RTI, and arm the BRK exit
|
|
lda #$00 ; 0A90 screen 0...
|
|
sta currentScreen ; 0A92 ...is the battlefield map screen
|
|
jsr clearScreenAndRedrawGameScreen; 0A95 clear the bitmap and draw the empty game screen
|
|
jsr clearOverviewWindow ; 0A98 blank the 20x20 overview window in it
|
|
bit manualLookupCheck ; 0A9B assembled as BIT, not JSR: the manual look-up copy protection is switched off in this build
|
|
lda #$FE ; 0A9E $FE = no side chosen yet...
|
|
sta playerSide ; 0AA0 ...the setup screens or the link decide who plays side 0 and who side 1
|
|
|
|
; ----------------------------------------------------------------------
|
|
; returnToMainMenu - the panic exit back to the main menu, used whenever the game has to abandon
|
|
; whatever it was doing: the end of a game, a lost link, the SHIFT + C= + F7 abort in the raster IRQ,
|
|
; and the fall-through from mainStart. It cannot return to its caller, so it throws the stack away
|
|
; and rebuilds it.
|
|
; In: nothing.
|
|
; Out: VIC interrupts and all sprites off, stack pointer = $FD, input state reset, message queue
|
|
; empty, diskLoadDepth = 0, currentScreen = 3 (the menu screen); continues into the menu flow at
|
|
; $1A86.
|
|
; Called from: $126B (the abort hot key in the raster IRQ), $5A3F, game/textEngineC000 $C876,
|
|
; game/modemDriverE000 $EE0C, and by falling out of mainStart.
|
|
; ----------------------------------------------------------------------
|
|
returnToMainMenu:
|
|
lda #$00 ; 0AA3 0 into both VIC registers below
|
|
sta VIC_IRQ_MASK ; 0AA5 no more raster interrupts while the screen is rebuilt
|
|
sta VIC_SPR_ENA ; 0AA8 all eight sprites off - the cursor and marker sprites belong to the game screens
|
|
cli ; 0AAB allow interrupts again (the handler is still installed)
|
|
ldx #$FD ; 0AAC throw away whatever return addresses the abandoned code left...
|
|
txs ; 0AAE ...by resetting the stack pointer to $FD, which is where the game keeps it
|
|
jsr initBitmapScreen ; 0AAF clear the bitmap, screen and colour RAM to grey and set up the display registers
|
|
jsr initIrqAndInputState ; 0AB2 silence the sound, re-install the raster IRQ, lock the input out for 158 frames
|
|
jsr resetMessageQueue ; 0AB5 drop every pending status message
|
|
lda #$00 ; 0AB8 no disk load is in progress...
|
|
sta diskLoadDepth ; 0ABA ...so the IRQ may pass keypresses to the game again
|
|
lda #$03 ; 0ABD screen 3...
|
|
sta currentScreen ; 0ABF ...is the main menu / console screen
|
|
jsr showMainMenu ; 0AC2 draw the title, credits and menu frame
|
|
jmp L_1A86 ; 0AC5 into the menu's link-status flow, which ends up in mainMenuLoop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; startGameFromSetup - the hand-over from the setup screens to the battle, entered by JMP from the
|
|
; setup overlay ($7E3F) once both sides have agreed on a game. It resets the game state, makes sure
|
|
; the game disk is in the drive, swaps the $E000 opponent module if the player changed between modem
|
|
; and solo trainer, asks for ANSWER or ORIGINATE when a modem game is starting, loads the battlefield
|
|
; map and the setup block, negotiates the sides and the map over the link, brings overlay B in and
|
|
; finally jumps into the battlefield loop.
|
|
; In: setupPlayerSide, filmPlaybackMode, linkSessionState, isSoloTrainer / prevSoloTrainerFlag, the
|
|
; resident build id $E030, overlayVariantFlag $87FF.
|
|
; Out: the battle running - never returns; jumps to runMapMainLoop ($1FD0).
|
|
; Called from: game/mapGenerator6F00 $7E3F (JMP).
|
|
; ----------------------------------------------------------------------
|
|
startGameFromSetup:
|
|
lda #$00 ; 0AC8 0 into VIC_IRQ_MASK below
|
|
sta VIC_IRQ_MASK ; 0ACA no raster interrupts while the game state is torn down and rebuilt
|
|
lda setupPlayerSide ; 0ACD the side chosen in the setup screens...
|
|
sta playerSide ; 0AD0 ...becomes the side this machine plays (0 owns units 0-49, 1 owns units 50-99)
|
|
jsr resetGameVariables ; 0AD3 clear the zero page, the variable block and the message queue
|
|
jsr initIrqAndInputState ; 0AD6 sound off, raster IRQ back on, input locked out for a moment
|
|
jsr patchUnitSideBranches ; 0AD9 write the BCC/BCS opcode that decides 'is this unit mine?' into the two map renderers
|
|
jsr waitForGameDiskInserted ; 0ADC block until the game disk is in the drive (its BAM says OZ)
|
|
lda filmPlaybackMode ; 0ADF watching a game film?
|
|
bmi L_0AFF ; 0AE2 then there is no opponent module to arrange
|
|
lda isSoloTrainer ; 0AE4 the opponent the player picked: bit 7 set = solo trainer, clear = modem
|
|
sta soloTrainerAtGameStart ; 0AE7 remembered for the whole game (the flag itself is overwritten below)
|
|
eor commBuildId ; 0AEA compare it with the build id of the module actually resident ($FF trainer, $00 modem) (also trainerAiE000:commBuildId)
|
|
and #$80 ; 0AED only bit 7 says which build it is
|
|
beq L_0AF7 ; 0AEF same kind of opponent as last time - the module can stay
|
|
inc diskLoadDepth ; 0AF1 mark a disk load in progress so the IRQ swallows keypresses
|
|
jsr loadCommModule ; 0AF4 load the other $E000 module (track 35 for the trainer, track 18 s7 + track 34 for the modem)
|
|
L_0AF7:
|
|
lda linkSessionState ; 0AF7 is a link session already running?
|
|
bmi L_0AFF ; 0AFA yes - do not re-initialise the module underneath it
|
|
jsr initCommModule ; 0AFC cold-initialise the module and point its keyboard vector at scanKeyboard
|
|
L_0AFF:
|
|
lda linkSessionState ; 0AFF link already up?
|
|
bmi L_0B2C ; 0B02 then the modem has long since been set up
|
|
lda filmPlaybackMode ; 0B04 watching a film?
|
|
bmi L_0B2C ; 0B07 then no modem prompt either
|
|
ldy prevSoloTrainerFlag ; 0B09 what kind of opponent the previous game had
|
|
lda isSoloTrainer ; 0B0C and what this one has
|
|
sta prevSoloTrainerFlag ; 0B0F remember it for next time
|
|
bmi L_0B2C ; 0B12 solo trainer now: nothing to dial
|
|
tya ; 0B14 the kind of opponent the previous game had, back into A
|
|
bpl L_0B2C ; 0B15 the previous game was a modem game too: the modem is already set
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Switching from the solo trainer to a modem game: put the two-line prompt up and wait for A or O.
|
|
; Message slots $20 and $21 are the two run-time slots whose text pointers setMessageSlot patches.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$20 ; 0B17 message slot $20...
|
|
lda #$00 ; 0B19 ...low byte of msgGetOpponentOnPhoneAnd ($8800 = 'GET OPPONENT ON PHONE AND...')
|
|
ldy #$88 ; 0B1B ...high byte
|
|
jsr setMessageSlot ; 0B1D patch slot $20 to point at it
|
|
ldx #$21 ; 0B20 message slot $21...
|
|
lda #$1C ; 0B22 ...low byte of msgPressAOrOAndSetModem ($881C = 'PRESS A OR O AND SET MODEM.')
|
|
ldy #$88 ; 0B24 ...high byte
|
|
jsr setMessageSlot ; 0B26 patch slot $21
|
|
jsr waitForAnswerOriginateKey; 0B29 show them and wait until the player presses A (answer) or O (originate)
|
|
L_0B2C:
|
|
lda commBuildId ; 0B2C the build id of the module that is really resident... (also trainerAiE000:commBuildId)
|
|
sta isSoloTrainer ; 0B2F ...written back over the wanted flag, so $0BA5 always describes what is in memory ($FF trainer / $00 modem)
|
|
inc diskLoadDepth ; 0B32 disk load starting: swallow keypresses
|
|
jsr loadUnitTemplatesAndSetup; 0B35 load the unit start-position templates to $F000 and the setup block to $0200
|
|
lda #$00 ; 0B38 0 into diskLoadDepth
|
|
sta diskLoadDepth ; 0B3A the disk is done with for now
|
|
jsr resumeCommAfterDiskAccess; 0B3D let the comm module run again and clear the loading message
|
|
jsr startGameSession ; 0B40 agree the sides and the map with the opponent and set up all the battle state
|
|
inc diskLoadDepth ; 0B43 one more disk load coming
|
|
lda isSoloTrainer ; 0B46 solo trainer?
|
|
bpl L_0B5B ; 0B49 no - the modem build's $EC00 tail was loaded with the module itself
|
|
ldx #$00 ; 0B4B destination low byte $00...
|
|
ldy #$EC ; 0B4D ...high byte $EC: the trainer tail loads at $EC00
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadCommTailBuild1 - not a routine of its own: the tail of startGameFromSetup, entered with X/Y
|
|
; already set at $0B4B. It pulls the solo trainer's $EC00-$EFFF sub-overlay (the code that picks the
|
|
; opening play) off track 35 sectors 12-15. The modem build's tail comes from track 34 sectors 12-15
|
|
; through loadCommTailBuild2 ($1070) instead.
|
|
; In: X = $00, Y = $EC (the destination page).
|
|
; Out: $EC00-$EFFF loaded.
|
|
; Called from: falls in from $0B4D.
|
|
; ----------------------------------------------------------------------
|
|
loadCommTailBuild1:
|
|
jsr setLoadDest ; 0B4F destination $EC00
|
|
ldy #$23 ; 0B52 track 35 = the solo trainer's track
|
|
ldx #$0C ; 0B54 first sector 12
|
|
lda #$04 ; 0B56 four sectors = $EC00-$EFFF
|
|
jsr loadSectors ; 0B58 read them
|
|
L_0B5B:
|
|
lda #$00 ; 0B5B 0 into gamePhase
|
|
sta gamePhase ; 0B5D gamePhase 0 means the battle itself is running (non-zero = setup, film or menu)
|
|
jsr loadOverlayBIfNotResident; 0B5F bring the comcen missile/drone screens into $6F00 if the setup overlay is still there
|
|
lda #$00 ; 0B62 0 into the two bytes below
|
|
sta diskLoadDepth ; 0B64 no disk load in progress: the IRQ may deliver keys again
|
|
sta manualCheckTries ; 0B67 the manual-check attempt counter is not needed during a game
|
|
jsr resetMessageQueue ; 0B6A empty the message queue before the battle messages start
|
|
lda filmPlaybackMode ; 0B6D watching a film?
|
|
bmi L_0B77 ; 0B70 then do not announce the battle
|
|
lda #$1C ; 0B72 message $1C = 'THE BATTLE BEGINS!'
|
|
jsr queueMessage ; 0B74 queue it
|
|
L_0B77:
|
|
jsr buildOwnerNameString ; 0B77 build the '<us> <them>' name line shown on the menu panel
|
|
jmp runMapMainLoop ; 0B7A into the battlefield screen's main loop - this never returns
|
|
|
|
|
|
; inputLockoutTimer: byteTable, 2 bytes. $0B7D lockout timer, $0B7E settings byte
|
|
inputLockoutTimer:
|
|
.byte $00 ; 0B7D frames during which the IRQ ignores the keyboard and joystick and keeps the previous state
|
|
programChecksum:
|
|
.byte $DB ; 0B7E checksum of $0402-$87FF computed by verifyProgramChecksum and exchanged with the opponent so both sides can detect a different build
|
|
|
|
; ownerNameString: text, 21 bytes. owner name string built at run time
|
|
ownerNameString:
|
|
.byte $A0,$A0 ; 0B7F 21-byte owner/player name built at run time from ownPlayerName and opponentPlayerName, terminated with bit 7 set
|
|
.byte $A0,$A0,$A0,$A0,$A0,$A0 ; 0B81 more of the 21 blanks the owner-name line starts life as
|
|
.byte $A0,$A0 ; 0B87 more blanks of the owner-name line
|
|
.byte $A0 ; 0B89 more blanks of the owner-name line
|
|
.byte $A0,$A0,$A0,$A0,$A0,$A0,$A0; 0B8A .......
|
|
.byte $A0 ; 0B91 the last blanks before the terminator position
|
|
.byte $A0 ; 0B92 .
|
|
ownerNameEnd:
|
|
.byte $A0 ; 0B93 last byte of ownerNameString (the bit-7 terminator position)
|
|
|
|
; $0B94-$0BB4 - the game settings block: everything that describes the game being played and has to
|
|
; match on both machines. gameVersionByte and programChecksum are exchanged in the version handshake,
|
|
; mapSeed and mapFlipFlag pick the battlefield, gameTypeOptions the rules, playerSide and
|
|
; playsFirstFlag the sides, and the two 6-byte text fields at mapNameText and mapNameBackup hold the
|
|
; printable map name. The film header carries a copy of most of it.
|
|
gameVersionByte:
|
|
.byte $00 ; 0B94 game/film format id: written to film header offset $2F and verified on load, and sent in the link version handshake
|
|
mapSeed:
|
|
.byte $00 ; 0B95 24-bit seed of the current battlefield ($0B95-$0B97) = the 5-letter map name packed 5 bits per letter; sent with command $8B/$A6
|
|
mapSeed1:
|
|
.byte $00 ; 0B96 byte 1 of mapSeed
|
|
mapSeed2:
|
|
.byte $00 ; 0B97 byte 2 of mapSeed
|
|
prevMapSeed:
|
|
.byte $00 ; 0B98 3-byte copy of the previous game's mapSeed; if it is unchanged the saved map name is restored, otherwise the first-play flag is reset
|
|
prevMapSeed1:
|
|
.byte $00 ; 0B99 byte 1 of prevMapSeed
|
|
prevMapSeed2:
|
|
.byte $00 ; 0B9A byte 2 of prevMapSeed
|
|
filmPlaybackMode:
|
|
.byte $FF ; 0B9B bit 7 = a game film is being played back (commands come from the film, player input is locked out); bits 0-1 = the replay viewpoint 0 US, 1 THEM, 2 BOTH
|
|
linkSessionState:
|
|
.byte $00 ; 0B9C link session: 0 = none, 2 = game session agreed, $80 = link up at menu level, $FF = link lost
|
|
mapFlipFlag:
|
|
.byte $00 ; 0B9D 0/1: the chosen map is shown rotated 180 degrees (F1 TO FLIP); XORed with playerSide to decide the orientation and sent as command $A6 instead of $8B
|
|
prevMapFlipFlag:
|
|
.byte $00 ; 0B9E mapFlipFlag of the previous game; a change forces the first-play flag back to 1
|
|
playerSide:
|
|
.byte $FE ; 0B9F side this machine plays, 0 or 1 ($FE = not chosen yet); EOR 1 gives the opponent, and units 0-49 belong to side 0, 50-99 to side 1
|
|
setupPlayerSide:
|
|
.byte $FE ; 0BA0 side chosen in the setup screens, copied into playerSide when the game starts
|
|
playsFirstFlag:
|
|
.byte $00 ; 0BA1 non-zero = this side has 1ST PLAY; it alternates every game and is forced to 1 when the map or the options change
|
|
savedPlaysFirstFlag:
|
|
.byte $00 ; 0BA2 copy of playsFirstFlag taken at game start and restored after a film playback
|
|
gameTypeOptions:
|
|
.byte $07 ; 0BA3 bits 0-2 game type (0 SCRIMAGE .. 6 DEFENDER, names at $CCC8), bit 3 UNIT MENUS, bit 4 DAMAGE, bit 6 CUSTOM rules, bit 7 custom map
|
|
prevGameTypeOptions:
|
|
.byte $FF ; 0BA4 gameTypeOptions of the previous game; a difference forces the first-play flag back to 1
|
|
isSoloTrainer:
|
|
.byte $FF ; 0BA5 bit 7 set = PRACTICE WITH SOLO TRAINER (the track 35 computer opponent is behind the $E000 jump table); clear = a modem game (track 34)
|
|
prevSoloTrainerFlag:
|
|
.byte $FF ; 0BA6 previous value of isSoloTrainer; a change from set to clear triggers the GET OPPONENT ON PHONE prompt
|
|
diskLoadDepth:
|
|
.byte $01 ; 0BA7 nesting counter bumped around every disk load and overlay swap; while it is non-zero the IRQ throws keypresses away
|
|
gameEndReason:
|
|
.byte $01 ; 0BA8 0 while a game runs; 1 = aborted / no game, $08 = time up, $A0/$C0 = the comcen of side 0/1 was knocked out
|
|
mapNameText:
|
|
.byte $A0 ; 0BA9 5-character map name ($0BA9-$0BAD) from the 32-letter alphabet at $890C - the printable form of mapSeed
|
|
.byte $A0 ; 0BAA characters 2-5 of the printable map name
|
|
.byte $A0,$A0,$A0 ; 0BAB ...
|
|
mapNameTerminator:
|
|
.byte $A0 ; 0BAE byte after mapNameText: $8D while it is printed, '!' | $80 when the name is hidden from the opponent
|
|
mapNameBackup:
|
|
.byte $A0 ; 0BAF 6-byte copy of mapNameText and its terminator, kept with prevMapSeed and restored when the same map is used again
|
|
.byte $A0 ; 0BB0 the rest of the saved copy of the map name and its terminator
|
|
.byte $A0,$A0,$A0,$A0 ; 0BB1 ....
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initIrqAndInputState - brings the interrupt and input state back to a known point. Called at cold
|
|
; start, on every return to the main menu and at the start of a game. It silences the sound, installs
|
|
; the raster IRQ, re-arms the one-shot 'SPOTTED RECYCLER' check, blanks the chat line and locks the
|
|
; keyboard and joystick out for 158 frames so that the keypress or click that got here is not read
|
|
; again by the screen the player is about to land in.
|
|
; In: nothing.
|
|
; Out: raster IRQ installed, inputLockoutTimer = $9E, chat line blank, gameEndCountdown /
|
|
; shiftHeldFlag / comcenStunStatus = 0, requestedScreenKey = $FF, the NOP restored at $3F57.
|
|
; Called from: mainStart $0A8A, returnToMainMenu $0AB2 and startGameFromSetup $0AD6.
|
|
; ----------------------------------------------------------------------
|
|
initIrqAndInputState:
|
|
jsr stopAllSound ; 0BB5 silence all four sound slots
|
|
jsr installRasterIrq ; 0BB8 point $FFFE at rasterIrqHandler and enable the raster interrupt
|
|
lda #$EA ; 0BBB $EA = NOP...
|
|
sta checkEnemyRecyclerSpotted; 0BBD ...restored over the first byte of checkEnemyRecyclerSpotted, which patches itself to RTS ($60) once it has fired, so the 'SPOTTED RECYCLER!' message can happen again in the next game
|
|
lda #$9E ; 0BC0 $9E is used twice below: as a frame count and as a string terminator
|
|
sta inputLockoutTimer ; 0BC2 158 frames (about 2.6 seconds) during which the IRQ ignores the keyboard and the joystick
|
|
sta D_0549 ; 0BC5 and as the second byte of the chat line, where $9E is a bit-7 terminator that prints nothing - i.e. blank the incoming chat line
|
|
lda #$00 ; 0BC8 0 into the three flags below
|
|
sta gameEndCountdown ; 0BCA the end-of-game countdown is not running
|
|
sta shiftHeldFlag ; 0BCD no SHIFT held as far as the game is concerned
|
|
sta comcenStunStatus ; 0BD0 the comcen is not stunned
|
|
lda #$FF ; 0BD3 $FF = no key
|
|
sta requestedScreenKey ; 0BD5 forget any pending F-key screen request
|
|
rts ; 0BD7 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; disableCiaInterrupts - masks every interrupt source in both CIAs and acknowledges whatever was
|
|
; already pending. Writing $7F to an interrupt control register clears all five mask bits (bit 7 = 0
|
|
; means 'clear the bits I name'), and reading the register clears the latched flags.
|
|
; In: nothing.
|
|
; Out: CIA1 and CIA2 will not raise IRQ or NMI; their pending flags cleared.
|
|
; Called from: mainStart $0A6D and loadCommModule $0F22 (before the trainer build is read in).
|
|
; ----------------------------------------------------------------------
|
|
disableCiaInterrupts:
|
|
lda #$7F ; 0BD8 $7F = clear all five interrupt mask bits (bit 7 clear = 'clear the bits set below')
|
|
sta CIA1_ICR ; 0BDA CIA1 raises the IRQ line - no more timer or keyboard interrupts
|
|
sta CIA2_ICR ; 0BDD CIA2 raises the NMI line - no more serial or timer NMIs
|
|
lda CIA1_ICR ; 0BE0 reading the register clears any interrupt already latched in CIA1
|
|
lda CIA2_ICR ; 0BE3 and in CIA2
|
|
rts ; 0BE6 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initInterruptVectors - finishes the interrupt set-up: it points the BRK escape of the raster handler
|
|
; at the normal IRQ exit and parks both the hardware NMI vector and the comm module's private copy of
|
|
; it on the bare RTI at $1298. The comm module later replaces its own copy with the modem's NMI
|
|
; routine when the UART is running.
|
|
; In: nothing.
|
|
; Out: the JMP operand at $10FF/$1100 = irqExit ($1293); $E031/$E032 and $FFFA/$FFFB = irqRti ($1298).
|
|
; Called from: mainStart $0A8D.
|
|
; ----------------------------------------------------------------------
|
|
initInterruptVectors:
|
|
lda #$93 ; 0BE7 low byte of irqExit ($1293)
|
|
sta L_10FE+1 ; 0BE9 operand of the JMP at $10FE, which rasterIrqHandler takes when the pushed status byte says BRK - so a stray BRK just restores the registers and RTIs
|
|
lda #$12 ; 0BEC high byte of irqExit
|
|
sta L_10FE+2 ; 0BEE second operand byte of that JMP
|
|
lda #$98 ; 0BF1 low byte of irqRti ($1298)
|
|
sta nmiChainVector ; 0BF3 the comm module's copy of the NMI vector, which its own handler chains through (also trainerAiE000:nmiChainVector)
|
|
sta nmiVector ; 0BF6 and the hardware NMI vector itself
|
|
lda #$12 ; 0BF9 high byte of irqRti
|
|
sta nmiChainVectorHi ; 0BFB comm module copy, high byte (also trainerAiE000:nmiChainVectorHi)
|
|
sta nmiVectorHi ; 0BFE hardware NMI vector, high byte - a RESTORE keypress now does nothing
|
|
rts ; 0C01 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setNmiVectorRti - points the hardware NMI vector at the bare RTI at $1298. The solo trainer never
|
|
; uses an NMI, so loadCommModule jumps here after reading the trainer build in, to undo whatever the
|
|
; modem build had left in the vector.
|
|
; In: nothing.
|
|
; Out: $FFFA/$FFFB = $1298.
|
|
; Called from: loadCommModule $0F2E (JMP).
|
|
; ----------------------------------------------------------------------
|
|
setNmiVectorRti:
|
|
lda #$98 ; 0C02 low byte of irqRti
|
|
sta nmiVector ; 0C04 NMI vector low
|
|
lda #$12 ; 0C07 high byte of irqRti
|
|
sta nmiVectorHi ; 0C09 NMI vector high - an NMI now returns immediately
|
|
L_0C0C:
|
|
rts ; 0C0C shared RTS, also used by jumpToCd00IfPatched below
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; jumpToCd00IfPatched - dead code. It looks at $CD00 and, if it finds a JMP opcode there, turns the
|
|
; VIC interrupt off and jumps into it. Nothing calls it, and at run time $CD00 is unitPictureHiTable
|
|
; inside the $C000 overlay, i.e. ordinary data, so the test would practically never pass. It reads
|
|
; like a development hook for jumping into a monitor or a test routine loaded over the overlay.
|
|
; In: $CD00.
|
|
; Out: normally just an RTS; if the byte were $4C it would disable the raster interrupt and jump
|
|
; there.
|
|
; Called from: nothing in any traced code.
|
|
; ----------------------------------------------------------------------
|
|
jumpToCd00IfPatched:
|
|
lda unitPictureHiTable ; 0C0D first byte of the $C000 overlay's unit-picture table
|
|
cmp #$4C ; 0C10 $4C = JMP absolute: has somebody planted a jump there?
|
|
bne L_0C0C ; 0C12 no - return through the RTS at $0C0C
|
|
lda #$00 ; 0C14 0 into VIC_IRQ_MASK
|
|
sta VIC_IRQ_MASK ; 0C16 no raster interrupts, so the planted code owns the machine
|
|
jmp unitPictureHiTable ; 0C19 and run it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resetGameVariables - wipes the game's variables back to a cold state: zero page $15-$7F and $8C-$FF,
|
|
; the whole variable block $9008-$92FF, the eight-entry message queue and the last-key bytes. It
|
|
; deliberately leaves $00-$14 (the 6510 port and the loader's own scratch) and $80-$8B alone, and
|
|
; starts the variable block at $9008 so the VIC shadow registers at $9000-$9007 survive. Clearing the
|
|
; RNG state $57-$59 is intentional too: stepRandomSeed re-seeds it from the joystick as soon as it
|
|
; sees all three bytes zero.
|
|
; In: nothing.
|
|
; Out: zero page and $9008-$92FF cleared, messageQueue and lastTypedKey = $FF, newKeyEvent = $FF,
|
|
; gamePhase = $FF, filmPtr rewound to $D000, currentScreen = 3, the chat line blanked.
|
|
; Called from: startGameFromSetup $0AD3 and the setup block's initGameSystems ($0300).
|
|
; ----------------------------------------------------------------------
|
|
resetGameVariables:
|
|
ldy #$15 ; 0C1C start at $0015 - $00-$14 belong to the 6510 port and the loader
|
|
lda #$00 ; 0C1E the value written everywhere
|
|
L_0C20:
|
|
sta a:CPU_DDR,y ; 0C20 absolute,Y so it can reach the whole of page zero
|
|
iny ; 0C23 next byte
|
|
bpl L_0C20 ; 0C24 stop at $80, i.e. clear $15-$7F
|
|
ldy #$8C ; 0C26 second run: $8C upwards ($80-$8B are left alone)
|
|
L_0C28:
|
|
sta a:CPU_DDR,y ; 0C28 clear it
|
|
iny ; 0C2B next byte
|
|
bne L_0C28 ; 0C2C until Y wraps at the end of page zero
|
|
lda #$08 ; 0C2E $9008 is the first variable after the VIC shadow registers...
|
|
sta mapCellPtr ; 0C30 ...low byte of the fill pointer
|
|
lda #$90 ; 0C32 high byte...
|
|
sta mapCellPtrHi ; 0C34 ...so the fill starts at $9008
|
|
lda #$FF ; 0C36 $92FF is the last variable...
|
|
sta fillEndPtr ; 0C38 ...low byte of the limit
|
|
lda #$92 ; 0C3A high byte...
|
|
sta fillEndPtrHi ; 0C3C ...so the fill ends at $92FF
|
|
ldy #$00 ; 0C3E the indirect store always uses offset 0
|
|
L_0C40:
|
|
lda #$00 ; 0C40 zero
|
|
sta (mapCellPtr),y ; 0C42 clear one variable byte
|
|
jsr advanceFillPointer ; 0C44 step the pointer and set C when the limit is reached
|
|
bcc L_0C40 ; 0C47 loop until the whole block is clear
|
|
ldy #$07 ; 0C49 eight message queue slots...
|
|
lda #$FF ; 0C4B ...filled with $FF = empty
|
|
L_0C4D:
|
|
sta messageQueue,y ; 0C4D clear queue slot Y
|
|
dey ; 0C50 next slot
|
|
bpl L_0C4D ; 0C51 until all eight are done
|
|
sta lastTypedKey ; 0C53 $FF = nothing typed
|
|
sta newKeyEvent ; 0C56 $FF = no key event pending
|
|
sta gamePhase ; 0C59 $FF = not in the battle phase
|
|
jsr rewindFilmPtr ; 0C5B rewind the game-film pointer to the start of the buffer under the I/O area ($D000)
|
|
lda #$03 ; 0C5E screen 3...
|
|
sta currentScreen ; 0C60 ...the menu/console screen is where the game goes next
|
|
jmp clearChatStatusLine ; 0C63 blank the chat status line and return through it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; installRasterIrq - installs rasterIrqHandler as the interrupt routine. There is no KERNAL to go
|
|
; through: with $01 = $35 the hardware vector at $FFFE is the real one, and the handler rewrites it
|
|
; from the zero-page shadow on every pass, so a corrupted vector repairs itself. The interrupt is
|
|
; armed for raster line $FA (250), in the lower border below the 200 display lines, so all the
|
|
; per-frame work happens off screen.
|
|
; In: nothing.
|
|
; Out: $FFFE/$FFFF and zp_AB/zp_AC = $10F1, rasterIrqLine = $FA, irqInProgress = 0, VIC raster
|
|
; interrupt acknowledged and enabled.
|
|
; Called from: initIrqAndInputState $0BB8.
|
|
; ----------------------------------------------------------------------
|
|
installRasterIrq:
|
|
sei ; 0C66 no interrupts while the vector is half written
|
|
lda #$F1 ; 0C67 low byte of rasterIrqHandler ($10F1)
|
|
sta irqVectorShadowLo ; 0C69 the shadow the handler writes back to $FFFE on every interrupt
|
|
sta irqVector ; 0C6B and the live hardware vector
|
|
lda #$10 ; 0C6E high byte of rasterIrqHandler
|
|
sta irqVectorShadowHi ; 0C70 shadow high byte
|
|
sta irqVectorHi ; 0C72 hardware vector high byte
|
|
lda #$FA ; 0C75 raster line 250, in the lower border
|
|
sta rasterIrqLine ; 0C77 the handler re-arms VIC_RASTER from this every time
|
|
sta VIC_RASTER ; 0C79 arm the raster compare now
|
|
lda #$00 ; 0C7C 0 into irqInProgress
|
|
sta irqInProgress ; 0C7E the handler's re-entrancy guard starts clear
|
|
cli ; 0C80 interrupts on
|
|
lda #$01 ; 0C81 bit 0 = the raster interrupt
|
|
sta VIC_IRQ_FLAG ; 0C83 acknowledge anything already latched
|
|
sta VIC_IRQ_MASK ; 0C86 and enable raster interrupts
|
|
rts ; 0C89 back to the caller
|
|
|
|
|
|
; comcenUnitIndexBySide - the unit index of each side's COMCEN: 49 ($31) for side 0, 99 ($63) for side
|
|
; 1, the last unit of each side's 50. Indexed by the side number, so the code can reach either
|
|
; command centre's row in the unit arrays (energy, stun, position) without a multiply. Phase 1 called
|
|
; this perSideValueTable;
|
|
; $7BF2 in the setup overlay copies the entry straight into comcenUnit ($9236).
|
|
comcenUnitIndexBySide:
|
|
.byte $31,$63 ; 0C8A 1c side 0's comcen is unit 49, side 1's is unit 99
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showModemSetupPrompt - puts a caller-supplied line above 'PRESS SPACE WHEN READY.' and waits for the
|
|
; space bar. The setup overlay uses it for the modem dialogue ('PICK UP PHONE THEN PRESS SPACE.' and
|
|
; friends).
|
|
; In: A/Y = the string to show in message slot $20.
|
|
; Out: message slots $20 and $21 patched, both messages shown, then it tail-jumps into waitForSpaceKey
|
|
; ($7DB8) in the overlay, so it never returns here.
|
|
; Called from: game/mapGenerator6F00 $79E3.
|
|
; ----------------------------------------------------------------------
|
|
showModemSetupPrompt:
|
|
ldx #$20 ; 0C8C message slot $20 = the run-time slot for the caller's line
|
|
jsr setMessageSlot ; 0C8E patch its text pointer to A/Y
|
|
ldx #$21 ; 0C91 message slot $21 = the second run-time slot
|
|
lda #$37 ; 0C93 low byte of msgPressSpaceWhenReady ($8837)
|
|
ldy #$88 ; 0C95 high byte
|
|
jsr setMessageSlot ; 0C97 patch slot $21 to it
|
|
jsr resetMessagesAndBeep ; 0C9A show slot $20, beep twice and arm slot $21 as the fallback line
|
|
jmp waitForSpaceKey ; 0C9D wait for the space bar in the setup overlay; that routine returns to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resetMessagesAndBeep - clears the message queue, shows message slot $20 immediately, beeps twice a
|
|
; third of a second apart and makes slot $21 the line the status bar falls back to once slot $20 has
|
|
; had its time. The double beep is what tells the player across the room that the game wants
|
|
; something.
|
|
; In: message slots $20 and $21 already patched by the caller.
|
|
; Out: queue empty, slot $20 displayed, persistentMessageId = $21.
|
|
; Called from: showModemSetupPrompt $0C9A and waitForAnswerOriginateKey $0CBD.
|
|
; ----------------------------------------------------------------------
|
|
resetMessagesAndBeep:
|
|
jsr resetMessageQueue ; 0CA0 drop anything already queued
|
|
lda #$20 ; 0CA3 message id $20 = the first run-time slot
|
|
jsr queueMessage ; 0CA5 put it on the status line
|
|
lda #$03 ; 0CA8 sound 3 = the attention beep
|
|
jsr playSound ; 0CAA play it
|
|
lda #$14 ; 0CAD 20 frames = a third of a second
|
|
jsr waitFrames ; 0CAF wait, so the two beeps are heard as two
|
|
lda #$03 ; 0CB2 sound 3 again
|
|
jsr playSound ; 0CB4 play it
|
|
ldx #$21 ; 0CB7 message id $21 = the second run-time slot
|
|
stx persistentMessageId ; 0CB9 it becomes the line the status bar returns to when the queue drains
|
|
rts ; 0CBC back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForAnswerOriginateKey - the ANSWER/ORIGINATE question at the start of a modem game. It shows
|
|
; the two prompt lines, then spins until the player types A or O, and tells the comm module which of
|
|
; the two Hayes command strings to use ($E011 indexes the modem's init-string table). The spin also
|
|
; stirs the game RNG, so the moment the player answers seeds the map.
|
|
; In: message slots $20/$21 already patched by the caller; lastTypedKey fed by the raster IRQ.
|
|
; Out: $E011 = 0 (answer) or 1 (originate), inputLockoutTimer = 0, message queue emptied.
|
|
; Called from: startGameFromSetup $0B29.
|
|
; ----------------------------------------------------------------------
|
|
waitForAnswerOriginateKey:
|
|
jsr resetMessagesAndBeep ; 0CBD show the prompt and beep twice
|
|
lda #$FF ; 0CC0 $FF = nothing typed
|
|
sta lastTypedKey ; 0CC2 forget any key that was typed before the prompt went up
|
|
lda #$00 ; 0CC5 0 into inputLockoutTimer
|
|
sta inputLockoutTimer ; 0CC7 cancel the 158-frame lockout so the IRQ passes the keystroke through at once
|
|
L_0CCA:
|
|
jsr updateStatusMessage ; 0CCA keep the status line alive while waiting
|
|
jsr nextGameRandom ; 0CCD stir the 24-bit game RNG: the human reaction time is the entropy source
|
|
ldx #$00 ; 0CD0 X = 0 = ANSWER
|
|
ldy lastTypedKey ; 0CD2 the last key the IRQ recorded
|
|
cpy #$C1 ; 0CD5 $C1 = 'A' (PETSCII with bit 7 set) = answer the call
|
|
beq L_0CDE ; 0CD7 got it
|
|
inx ; 0CD9 X = 1 = ORIGINATE
|
|
cpy #$CF ; 0CDA $CF = 'O' = place the call
|
|
bne L_0CCA ; 0CDC neither - keep waiting
|
|
L_0CDE:
|
|
stx isOriginateMode ; 0CDE tell the modem driver which mode it is in; it indexes its init-string table with this (also trainerAiE000:isOriginateMode)
|
|
jmp resetMessageQueue ; 0CE1 clear the prompt away and return through resetMessageQueue
|
|
|
|
|
|
; strOurs - the four characters 'OURS', copied over the owner-name field when the disk carries no
|
|
; name. It has no bit-7 terminator because it is never printed directly: buildOwnerNameString copies
|
|
; it into ownPlayerName and the terminator is added to the assembled line afterwards.
|
|
strOurs:
|
|
.byte "OURS" ; 0CE4 default owner name for an unpersonalised disk
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildOwnerNameString - assembles the 21-character line that names the two players on the menu panel,
|
|
; out of the two 10-byte name fields at the top of memory: ownPlayerName ($FFDE), copied out of the
|
|
; disk's BAM by checkDiskId, and opponentPlayerName ($FFE8), which the modem driver fills in from the
|
|
; other machine. The first name is packed to the right of the first ten columns and the second to the
|
|
; left of the last ten, so the two meet either side of the single space in the middle. Trailing
|
|
; blanks of the first name and leading blanks of the second are dropped ($FFDE holds shifted spaces,
|
|
; $A0, which the AND #$7F turns into ordinary spaces).
|
|
; In: ownPlayerName and opponentPlayerName, 10 bytes each.
|
|
; Out: ownerNameString ($0B7F-$0B93) filled and terminated with bit 7 set on its last byte;
|
|
; ownPlayerName replaced by 'OURS' when it was blank.
|
|
; Called from: startGameFromSetup $0B77.
|
|
; ----------------------------------------------------------------------
|
|
buildOwnerNameString:
|
|
ldy #$14 ; 0CE8 21 characters, $0B7F-$0B93
|
|
lda #$20 ; 0CEA a plain space
|
|
L_0CEC:
|
|
sta ownerNameString,y ; 0CEC blank the whole line first
|
|
dey ; 0CEF next character
|
|
bpl L_0CEC ; 0CF0 until all 21 are blank
|
|
lda ownPlayerName ; 0CF2 first character of the owner name out of the disk BAM
|
|
cmp #$A0 ; 0CF5 $A0 = shifted space = the field was never personalised
|
|
bne L_0D04 ; 0CF7 the disk has a name - use it
|
|
ldy #$03 ; 0CF9 four characters of 'OURS'
|
|
L_0CFB:
|
|
lda strOurs,y ; 0CFB copy them...
|
|
sta ownPlayerName,y ; 0CFE ...over the first four bytes of the owner-name field
|
|
dey ; 0D01 next character
|
|
bpl L_0CFB ; 0D02 until all four are in
|
|
|
|
; ----------------------------------------------------------------------
|
|
; First name, right aligned: walk it backwards into columns 9..0 of the line. scratch18 stays 0 until
|
|
; the first real character has been placed and is negative afterwards, so blanks at the end of the
|
|
; field are skipped while blanks inside the name are kept.
|
|
; ----------------------------------------------------------------------
|
|
L_0D04:
|
|
lda #$00 ; 0D04 0 into the 'seen a character yet' flag
|
|
sta scratch18 ; 0D06 scratch18 = 0 means nothing placed so far
|
|
ldy #$09 ; 0D08 last character of the 10-byte field
|
|
ldx #$09 ; 0D0A column 9 = the rightmost of the first half of the line
|
|
L_0D0C:
|
|
lda ownPlayerName,y ; 0D0C one character of the owner name
|
|
and #$7F ; 0D0F clear bit 7: shifted characters ($A0 etc.) become plain ones
|
|
cmp #$20 ; 0D11 is it a space?
|
|
bne L_0D19 ; 0D13 no - place it
|
|
bit scratch18 ; 0D15 have any characters been placed yet?
|
|
bpl L_0D1F ; 0D17 no - this is a trailing blank, drop it
|
|
L_0D19:
|
|
sta ownerNameString,x ; 0D19 place the character in the line
|
|
dex ; 0D1C one column to the left
|
|
dec scratch18 ; 0D1D make scratch18 negative: from now on blanks count as part of the name
|
|
L_0D1F:
|
|
dey ; 0D1F previous character of the field
|
|
bpl L_0D0C ; 0D20 until the whole field has been scanned
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Second name, left aligned: columns 11..20, forwards through the field, with the same blank-squeezing
|
|
; rule - so this drops the leading blanks. Column 10 stays a space and separates the two names.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$0B ; 0D22 column 11, just past the separating space
|
|
ldy #$00 ; 0D24 first character of the opponent's name
|
|
sty scratch18 ; 0D26 nothing placed yet
|
|
L_0D28:
|
|
lda opponentPlayerName,y ; 0D28 one character of the opponent name
|
|
and #$7F ; 0D2B clear bit 7
|
|
cmp #$20 ; 0D2D is it a space?
|
|
bne L_0D35 ; 0D2F no - place it
|
|
bit scratch18 ; 0D31 have any characters been placed yet?
|
|
bpl L_0D3B ; 0D33 no - this is a leading blank, drop it
|
|
L_0D35:
|
|
sta ownerNameString,x ; 0D35 place the character
|
|
inx ; 0D38 one column to the right
|
|
dec scratch18 ; 0D39 make scratch18 negative so inner blanks survive
|
|
L_0D3B:
|
|
iny ; 0D3B next character of the field
|
|
cpy #$0A ; 0D3C ten characters in the field
|
|
bcc L_0D28 ; 0D3E loop until they are all scanned
|
|
lda ownerNameEnd ; 0D40 the last byte of the line ($0B93)
|
|
ora #$80 ; 0D43 set bit 7 on it...
|
|
sta ownerNameEnd ; 0D45 ...which is how every string in this game ends
|
|
rts ; 0D48 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setJoystickRepeatDelay - the joystick auto-repeat used by every menu and by the map scrolling. The
|
|
; caller sets a direction going and calls this to arm the delay before it happens again: 16 frames for
|
|
; the first repeat, 6 frames afterwards, so a nudge moves one step and a held stick runs. The raster
|
|
; IRQ clears joyRepeatMask when the stick returns to centre, which forces joyHoldCounter back to 0 and
|
|
; so restores the long first delay. A is preserved so callers can keep a value in it across the call.
|
|
; In: joyRepeatMask ($91C9), joyHoldCounter ($91CA).
|
|
; Out: joyRepeatTimer (zp_6C) = 16 or 6, joyHoldCounter stepped, joyRepeatMask = $7F, A unchanged.
|
|
; Called from: $20E7, $2230 and $60AF in this file and $8090/$849E in the setup overlay.
|
|
; ----------------------------------------------------------------------
|
|
setJoystickRepeatDelay:
|
|
ldy #$10 ; 0D49 16 frames: the pause before a held direction starts repeating
|
|
pha ; 0D4B the caller's A must survive
|
|
inc joyHoldCounter ; 0D4C count this step
|
|
lda joyHoldCounter ; 0D4F read the counter back
|
|
and joyRepeatMask ; 0D52 the IRQ clears this mask to 0 while the stick is centred, which zeroes the counter
|
|
sta joyHoldCounter ; 0D55 so the counter only grows while the stick is held
|
|
cmp #$01 ; 0D58 was this the first step after centring?
|
|
bcc L_0D5E ; 0D5A yes - keep the long 16-frame delay
|
|
ldy #$06 ; 0D5C no - 6 frames between repeats
|
|
L_0D5E:
|
|
sty joyRepeatTimer ; 0D5E arm the countdown; the raster IRQ decrements it every frame
|
|
lda #$7F ; 0D60 $7F lets the counter run again...
|
|
sta joyRepeatMask ; 0D62 ...until the IRQ sees the stick centred and clears the mask
|
|
pla ; 0D65 give the caller its A back
|
|
rts ; 0D66 back to the caller
|
|
|
|
|
|
; keyMatrixCodeTable - the whole C64 keyboard as 64 bytes, indexed by (port A row) * 8 + (port B
|
|
; column bit), which is exactly the order scanKeyboard finds a key in. Ordinary keys are their ASCII
|
|
; code with bit 7 set (so 'A' is $C1 and '3' is $B3), the four function keys are 0-3 because the game
|
|
; uses them as screen numbers (F1 = 0 map, F3 = 1 missile, F5 = 2 drone, F7 = 3 menu), and the control
|
|
; keys are ASCII control codes with bit 7 set: DEL $88, RETURN $8D, CRSR DOWN $8B, HOME $8C, CRSR
|
|
; RIGHT $8E, RUN/STOP $93, left arrow $9B, pound $83. Two magic entries: $FE marks the three modifier
|
|
; cells (both SHIFTs and the Commodore key), which the scanner tests separately and skips here, and
|
|
; $FF marks CTRL, which makes the scan report 'no key' - the game has no use for CTRL and treats
|
|
; holding it as a way to block the keyboard.
|
|
keyMatrixCodeTable:
|
|
.byte $88,$8D,$8E,$03,$00,$01,$02,$8B; 0D67 ........ row 0 (PA0): DEL, RETURN, CRSR RIGHT, F7=3, F1=0, F3=1, F5=2, CRSR DOWN
|
|
.byte $B3 ; 0D6F row 1 (PA1): 3 W A 4 Z S E, then LEFT SHIFT ($FE = handled separately)
|
|
.byte $D7,$C1,$B4,$DA,$D3,$C5,$FE; 0D70 .......
|
|
.byte $B5 ; 0D77 row 2 (PA2): 5 R D 6 C F T X
|
|
.byte $D2,$C4,$B6,$C3,$C6,$D4,$D8; 0D78 .......
|
|
.byte $B7 ; 0D7F row 3 (PA3): 7 Y G 8 B H U V
|
|
.byte $D9,$C7,$B8,$C2,$C8,$D5,$D6; 0D80 .......
|
|
.byte $B9 ; 0D87 row 4 (PA4): 9 I J 0 M K O N
|
|
.byte $C9,$CA,$B0,$CD,$CB,$CF,$CE; 0D88 .......
|
|
.byte $AB ; 0D8F row 5 (PA5): + P L - . : @ ,
|
|
.byte $D0,$CC,$AD,$AE,$BA,$C0,$AC; 0D90 .......
|
|
.byte $83 ; 0D97 row 6 (PA6): pound, *, ;, HOME, RIGHT SHIFT ($FE), =, up arrow, /
|
|
.byte $AA,$BB,$8C,$FE,$BD,$DE,$AF; 0D98 .......
|
|
.byte $B1 ; 0D9F row 7 (PA7): 1, left arrow, CTRL ($FF = report no key), 2, SPACE, C= ($FE), Q, RUN/STOP
|
|
.byte $9B,$FF,$B2,$A0,$FE,$D1,$93; 0DA0 .......
|
|
|
|
; shiftedKeyTable - what a key produces when SHIFT is held, for the sixteen codes $AC-$BB (',' '-' '.'
|
|
; '/', the digits '0'-'9', ':' and ';'); pollKeyboardEvent indexes it with keycode - $AC. The digits
|
|
; give the usual C64 symbols !"#$%&'() and shifted '0' stays '0', ',' '.' '/' become '<' '>' '?' and
|
|
; '-' stays '-'. The last two entries repeat those of '8' and '9', so shifted ':' and ';' come out as
|
|
; '(' and ')' instead of the brackets a C64 would give - the game's font has no square brackets.
|
|
; Letters are not in the table: SHIFT does not change them, the font has only one case.
|
|
shiftedKeyTable:
|
|
.byte $BC,$AD,$BE,$BF,$B0,$A1,$A2,$A3; 0DA7 ........ shifted , - . / 0 1 2 3 -> < - > ? 0 ! " #
|
|
.byte $A4 ; 0DAF shifted 4 5 6 7 8 9 : ; -> $ % & ' ( ) ( )
|
|
.byte $A5,$A6,$A7,$A8,$A9,$A8,$A9; 0DB0 .......
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scanKeyboard - the game's own keyboard matrix scanner; there is no KERNAL to ask, since the ROMs are
|
|
; banked out. It is not called directly: initCommModule patches its address into the comm module's
|
|
; key vector ($E013/$E014), and pollKeyboardEvent reaches it once a frame through the module's $E015
|
|
; entry, which lets the modem driver steal keys for its Commodore-key hot keys first. The scan first
|
|
; makes sure no joystick is confusing the matrix (a stick in port 1 pulls column lines low, one in
|
|
; port 2 pulls row lines low, and either looks exactly like a key), then tests the two SHIFT keys and
|
|
; the Commodore key as modifiers, and finally walks rows 7..0 and columns 7..0 and translates the
|
|
; first key it finds through keyMatrixCodeTable. Only one key is ever reported - the game needs no
|
|
; rollover.
|
|
; In: CIA1 ports; the row masks at D_2F8D ($2F8D) and the column bit masks at spriteBitMasks ($2F85).
|
|
; Out: A = key code or $FF for none, Y = $80 when a SHIFT is held (else 0), X = $80 when the Commodore
|
|
; key is held (else 0). It self-modifies the operands at $0E2B, $0E3A, $0E41 and $0E43, and
|
|
; leaves CIA1_DDRA = $FF, CIA1_DDRB = $00 and the last row mask in CIA1_PRA - the raster IRQ
|
|
; relies on that to read joystick 2 immediately afterwards.
|
|
; Called from: game/modemDriverE000 and game/trainerAiE000 through the patched vector at $E013
|
|
; ($E012/$E015).
|
|
; ----------------------------------------------------------------------
|
|
scanKeyboard:
|
|
ldx #$00 ; 0DB7 0 into both result operands below
|
|
stx L_0E40+1 ; 0DB9 the LDY operand at $0E41: assume no SHIFT is held
|
|
stx L_0E42+1 ; 0DBC the LDX operand at $0E43: assume the Commodore key is not held
|
|
stx CIA1_DDRB ; 0DBF port B all inputs - it reads the eight key columns
|
|
lda #$FF ; 0DC2 $FF into port A and its direction register
|
|
sta CIA1_DDRA ; 0DC4 port A all outputs - it selects the key rows (a 0 bit selects a row)
|
|
sta CIA1_PRA ; 0DC7 no row selected at all
|
|
cmp CIA1_PRB ; 0DCA with no row selected every column should read high
|
|
beq L_0DD2 ; 0DCD it does - the matrix is undisturbed
|
|
L_0DCF:
|
|
jmp L_0E3E ; 0DCF a column is being pulled low with no row selected: that is a joystick in port 1, so report 'no key'
|
|
|
|
L_0DD2:
|
|
cmp CIA1_PRA ; 0DD2 port A should also read back the $FF it is driving
|
|
bne L_0DCF ; 0DD5 it does not: a joystick in port 2 is pulling a row line low, so the scan would lie - report 'no key'
|
|
stx CIA1_PRA ; 0DD7 X is still 0: select all eight rows at once
|
|
cmp CIA1_PRB ; 0DDA if any key at all is down, some column now reads low
|
|
beq L_0DCF ; 0DDD nothing is down - report 'no key' without scanning
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The three modifier keys are wired into the matrix like any other key, so they have to be tested by
|
|
; name before the general scan, which skips their cells ($FE in keyMatrixCodeTable).
|
|
; ----------------------------------------------------------------------
|
|
ldy #$BF ; 0DDF row mask $BF = PA6 low (the row with RIGHT SHIFT)
|
|
lda #$10 ; 0DE1 column bit 4 = RIGHT SHIFT
|
|
jsr testKeyMask ; 0DE3 is it down?
|
|
beq L_0DF1 ; 0DE6 yes - record a shift
|
|
ldy #$FD ; 0DE8 row mask $FD = PA1 low (the row with LEFT SHIFT)
|
|
lda #$80 ; 0DEA column bit 7 = LEFT SHIFT
|
|
jsr testKeyMask ; 0DEC is that one down?
|
|
bne L_0DF6 ; 0DEF neither shift is down
|
|
L_0DF1:
|
|
lda #$80 ; 0DF1 $80 = shift held
|
|
sta L_0E40+1 ; 0DF3 patch it into the LDY at $0E40, which returns the shift flag in Y
|
|
L_0DF6:
|
|
ldy #$7F ; 0DF6 row mask $7F = PA7 low (the row with the Commodore key)
|
|
lda #$20 ; 0DF8 column bit 5 = the Commodore key
|
|
jsr testKeyMask ; 0DFA is it down?
|
|
bne L_0E04 ; 0DFD no
|
|
lda #$80 ; 0DFF $80 = Commodore key held
|
|
sta L_0E42+1 ; 0E01 patch it into the LDX at $0E42, which returns that flag in X
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The scan proper: eight rows, eight columns each, from the top down, stopping at the first key that
|
|
; is down. So when two keys are held the one with the higher row (and within a row the higher column)
|
|
; wins.
|
|
; ----------------------------------------------------------------------
|
|
L_0E04:
|
|
ldx #$07 ; 0E04 start at row 7
|
|
L_0E06:
|
|
ldy #$07 ; 0E06 start at column 7
|
|
lda invertedBitMasks,x ; 0E08 the row select pattern for this row (one bit low)
|
|
sta CIA1_PRA ; 0E0B select it on port A
|
|
L_0E0E:
|
|
lda CIA1_PRB ; 0E0E read the columns...
|
|
cmp CIA1_PRB ; 0E11 ...and read them again
|
|
bne L_0E0E ; 0E14 until two reads agree - cheap debounce against key bounce and the raster's noise
|
|
and spriteBitMasks,y ; 0E16 test the single column bit for this column
|
|
beq L_0E23 ; 0E19 zero means that key is down
|
|
L_0E1B:
|
|
dey ; 0E1B next column
|
|
bpl L_0E0E ; 0E1C until all eight of this row are done
|
|
dex ; 0E1E next row
|
|
bpl L_0E06 ; 0E1F until all eight rows are done
|
|
bmi L_0E3E ; 0E21 nothing found anywhere - report 'no key'
|
|
L_0E23:
|
|
txa ; 0E23 the row number...
|
|
asl a ; 0E24 ...times two...
|
|
asl a ; 0E25 ...times four...
|
|
asl a ; 0E26 ...times eight = the start of that row in the code table
|
|
sty L_0E2A+1 ; 0E27 patch the column number into the ORA operand at $0E2B
|
|
L_0E2A:
|
|
ora #$FF ; 0E2A operand patched just above: row * 8 + column = the index into keyMatrixCodeTable
|
|
stx L_0E39+1 ; 0E2C save the row number in the LDX operand at $0E3A, because X is about to be reused as the table index
|
|
tax ; 0E2F index the table
|
|
lda keyMatrixCodeTable,x ; 0E30 the key code for the key that is down
|
|
cmp #$FE ; 0E33 $FE and $FF are the two magic entries
|
|
bcc L_0E40 ; 0E35 a real key code - return it
|
|
bne L_0E3E ; 0E37 $FF = CTRL: pretend nothing is pressed
|
|
L_0E39:
|
|
ldx #$FF ; 0E39 $FE = one of the modifier cells: restore the row number (operand patched at $0E2C)...
|
|
jmp L_0E1B ; 0E3B ...and carry on scanning from the next column
|
|
|
|
L_0E3E:
|
|
lda #$FF ; 0E3E $FF = no key
|
|
L_0E40:
|
|
ldy #$FF ; 0E40 operand patched at $0DB9/$0DF3: Y = $80 when a shift is held, 0 otherwise
|
|
L_0E42:
|
|
ldx #$FF ; 0E42 operand patched at $0DBC/$0E01: X = $80 when the Commodore key is held, 0 otherwise
|
|
rts ; 0E44 back to pollKeyboardEvent (through the comm module)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; testKeyMask - tests one named key for scanKeyboard: it selects the row given in Y, reads the columns
|
|
; twice until two reads agree, and masks out the single column bit the caller passed in A. The mask
|
|
; is not kept in a register but patched into the AND operand at $0E54, so both registers stay free.
|
|
; In: Y = the port A row select pattern (one bit low), A = the column bit mask.
|
|
; Out: Z = 1 when that key is down; A = the masked port B reading; the operand at $0E54 patched.
|
|
; Called from: scanKeyboard $0DE3 (right shift), $0DEC (left shift) and $0DFA (Commodore key).
|
|
; ----------------------------------------------------------------------
|
|
testKeyMask:
|
|
sta L_0E53+1 ; 0E45 patch the caller's column mask into the AND at $0E53
|
|
sty CIA1_PRA ; 0E48 select the row
|
|
L_0E4B:
|
|
lda CIA1_PRB ; 0E4B read the columns...
|
|
cmp CIA1_PRB ; 0E4E ...and read them again
|
|
bne L_0E4B ; 0E51 until two reads agree
|
|
L_0E53:
|
|
and #$FF ; 0E53 keep only the column of the key we are asking about (operand patched at $0E45)
|
|
rts ; 0E55 Z = 1 means the key is down
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pollKeyboardEvent - the keyboard front end, run once per frame from the raster IRQ (and only from
|
|
; there). It asks the comm module for a key - which reaches scanKeyboard, after the modem driver has
|
|
; had its chance to swallow Commodore-key hot keys - and turns the raw 'this key is down' answer into
|
|
; events: a key that is the same as last frame is ignored until the 20-frame repeat timer runs out, so
|
|
; a held key repeats about three times a second. The four function keys (codes 0-3) are screen
|
|
; requests: they go only into requestedScreenKey (zp_66) and explicitly clear newKeyEvent, because
|
|
; nothing may type them into a text field. Every other key (all codes have bit 7 set) becomes a
|
|
; newKeyEvent for the line editors, a lastTypedKey for the menus, and is also written into
|
|
; requestedScreenKey, which cancels any stale F-key request. When a shift is held the sixteen
|
|
; punctuation and digit codes $AC-$BB are replaced from shiftedKeyTable.
|
|
; In: the comm module key entry $E015 -> scanKeyboard (A = code, Y = shift, X = Commodore);
|
|
; prevKeyCode, keyRepeatTimer.
|
|
; Out: rawKeyCode/shiftHeldFlag/commodoreHeldFlag ($92A0/$92E4/$92E5) = the raw scan, newKeyEvent
|
|
; ($90EA), prevKeyCode ($90E9), lastTypedKey ($9248), requestedScreenKey (zp_66) and
|
|
; keyRepeatTimer (zp_70).
|
|
; Called from: rasterIrqHandler $11E7 (only when the input lockout timer has expired).
|
|
; ----------------------------------------------------------------------
|
|
pollKeyboardEvent:
|
|
jsr commKeyEntry ; 0E56 read a key through the comm module, which vectors on to scanKeyboard (also trainerAiE000:commReadKeyEntry)
|
|
sta rawKeyCode ; 0E59 keep the raw code the scanner returned
|
|
sty shiftHeldFlag ; 0E5C Y = $80 when a shift was held during that scan
|
|
stx commodoreHeldFlag ; 0E5F X = $80 when the Commodore key was held
|
|
cmp #$FF ; 0E62 $FF = no key is down
|
|
beq L_0EA4 ; 0E64 nothing pressed - clear the pending state
|
|
cmp prevKeyCode ; 0E66 the same key as the previous frame?
|
|
bne L_0E6F ; 0E69 no - a genuinely new keypress
|
|
ldy keyRepeatTimer ; 0E6B the repeat countdown the IRQ decrements every frame
|
|
bne L_0EAB ; 0E6D still running: the key is being held, do not report it again yet
|
|
L_0E6F:
|
|
ldy #$14 ; 0E6F 20 frames until a held key repeats
|
|
sty keyRepeatTimer ; 0E71 arm the repeat timer
|
|
sta newKeyEvent ; 0E73 publish the key as this frame's event
|
|
sta prevKeyCode ; 0E76 remember it so the next frame can tell 'held' from 'pressed again'
|
|
cmp #$80 ; 0E79 codes below $80 are only the four function keys
|
|
bcc L_0E9B ; 0E7B F1/F3/F5/F7 are handled separately below
|
|
sta lastTypedKey ; 0E7D everything else is a typed character: the menus read it here
|
|
sta requestedScreenKey ; 0E80 and into the screen-request byte, where a printable code reads as 'no screen change wanted' and so cancels a stale F-key request
|
|
bit shiftHeldFlag ; 0E82 was a shift held?
|
|
bpl L_0E9A ; 0E85 no - the code stands as it is
|
|
sec ; 0E87 prepare an exact subtraction
|
|
sbc #$AC ; 0E88 $AC = ',' is the first code that has a shifted form
|
|
bcc L_0E9A ; 0E8A below the table - letters and control codes are unaffected
|
|
cmp #$10 ; 0E8C sixteen entries, $AC-$BB
|
|
bcs L_0E9A ; 0E8E above the table - unaffected
|
|
tax ; 0E90 the difference is the table index
|
|
lda shiftedKeyTable,x ; 0E91 the shifted form of that key
|
|
sta newKeyEvent ; 0E94 replace this frame's event...
|
|
sta lastTypedKey ; 0E97 ...and the typed character with it
|
|
L_0E9A:
|
|
rts ; 0E9A back to the IRQ
|
|
|
|
L_0E9B:
|
|
tay ; 0E9B hold the function key number (0-3) in Y
|
|
lda #$FF ; 0E9C $FF = no character event...
|
|
sta newKeyEvent ; 0E9E ...so a screen key can never be typed into a text field
|
|
sty requestedScreenKey ; 0EA1 the screen the player asked for: 0 map, 1 missile, 2 drone, 3 menu
|
|
rts ; 0EA3 back to the IRQ
|
|
|
|
L_0EA4:
|
|
lda #$FF ; 0EA4 $FF = none
|
|
sta requestedScreenKey ; 0EA6 no screen change pending
|
|
sta prevKeyCode ; 0EA8 and no previous key, so the very next press counts as new
|
|
L_0EAB:
|
|
lda #$FF ; 0EAB $FF = nothing to report
|
|
sta newKeyEvent ; 0EAD clear this frame's key event
|
|
rts ; 0EB0 back to the IRQ
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkFirePressed - the game's 'has the player clicked?' primitive; almost every interactive loop
|
|
; spins on it with BNE. It reports a click only on the edge: the button must have been seen released
|
|
; since the last accepted click (fireButtonArmed) and must be down now. An accepted click arms a
|
|
; 10-frame debounce and plays the click sound. It also refuses clicks when the outgoing command ring
|
|
; has less than five bytes free, which throttles the player instead of overflowing the link.
|
|
; In: joyFireUp (zp_5F, 0 = button down), fireButtonArmed (zp_79), fireDebounceTimer (zp_6D), the
|
|
; command ring indices through isCmdRingNearlyFull.
|
|
; Out: A = 0 (Z=1) for a fresh click, A = 1 otherwise; fireDebounceTimer = 10 and sound 1 queued on a
|
|
; click; fireButtonArmed maintained.
|
|
; Called from: about a dozen loops - $0ED5, $1470, $21E7, $5E68, $608E, $6226, $63FB here,
|
|
; $7751/$806F/$8127/$84A4/$878D in the overlays and $C865 in the text engine.
|
|
; ----------------------------------------------------------------------
|
|
checkFirePressed:
|
|
jsr isCmdRingNearlyFull ; 0EB1 C=1 when fewer than five bytes are free in the outgoing command ring
|
|
bcs L_0EC0 ; 0EB4 the link cannot take another order - swallow the click
|
|
lda fireDebounceTimer ; 0EB6 frames left of the debounce after the last accepted click
|
|
bne L_0EC0 ; 0EB8 still counting down - ignore the button
|
|
lda joyFireUp ; 0EBA 0 = the button is down right now
|
|
ora fireButtonArmed ; 0EBC the latch that says the button has been released since the last click
|
|
bne L_0EC3 ; 0EBE either of them non-zero means there is something to look at
|
|
L_0EC0:
|
|
lda #$01 ; 0EC0 A = 1: no click this time
|
|
rts ; 0EC2 callers loop on BNE, so this keeps them waiting
|
|
|
|
L_0EC3:
|
|
lda joyFireUp ; 0EC3 is the button actually down?
|
|
bne L_0ED2 ; 0EC5 no - it has just been released, so re-arm the latch
|
|
lda #$0A ; 0EC7 10 frames...
|
|
sta fireDebounceTimer ; 0EC9 ...during which further clicks are ignored (contact bounce and human double taps)
|
|
lda #$01 ; 0ECB sound 1 = the UI click
|
|
jsr playSound ; 0ECD play it so the player hears the press
|
|
lda #$00 ; 0ED0 A = 0: a fresh click, and 0 also disarms the latch below
|
|
L_0ED2:
|
|
sta fireButtonArmed ; 0ED2 store 0 (click taken) or the button-up state (latch armed)
|
|
rts ; 0ED4 A tells the caller which of the two happened
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkFirePressedInteractive - checkFirePressed plus a 'may the player act at all?' gate, used by the
|
|
; loops that turn a click into an order. It reports a click only when a game film is not being
|
|
; watched (filmPlaybackMode bit 7) and the game has not already ended (gameOverFlags bit 6, which is
|
|
; set when a game is over or aborted). Phase 1 called this checkFirePressedNotSolo, which is wrong:
|
|
; the solo trainer leaves filmPlaybackMode clear and its player clicks normally.
|
|
; In: the same joystick state as checkFirePressed, plus filmPlaybackMode ($0B9B) and gameOverFlags
|
|
; ($923F).
|
|
; Out: A = 0 only for an accepted click during live play; non-zero otherwise.
|
|
; Called from: $2076 here and $75FA/$8043 in the $6F00 overlays.
|
|
; ----------------------------------------------------------------------
|
|
checkFirePressedInteractive:
|
|
jsr checkFirePressed ; 0ED5 the ordinary edge-detected click
|
|
bne L_0EE4 ; 0ED8 no click - return non-zero without looking further
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shared entry: isPlayerInputAllowed ($0EE5) jumps in here with A = 0 to ask the same question without
|
|
; testing the button.
|
|
; ----------------------------------------------------------------------
|
|
L_0EDA:
|
|
lda filmPlaybackMode ; 0EDA watching a game film?
|
|
bmi L_0EE4 ; 0EDD then the player is a spectator: the click does not count (A is negative, i.e. non-zero)
|
|
lda gameOverFlags ; 0EDF the flags set when the game has ended or been aborted
|
|
and #$40 ; 0EE2 bit 6 blocks all further player commands; clear = the click is accepted (A = 0)
|
|
L_0EE4:
|
|
rts ; 0EE4 Z=1 only for an accepted click in a live game
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isPlayerInputAllowed - the gate half of checkFirePressedInteractive without the button test: it
|
|
; jumps into that routine at $0EDA with A = 0, so it answers 'may the local player still act?' - Z=1
|
|
; when no film is being watched and the game has not ended. Phase 1 named it getSoloOrFlag923F and
|
|
; thought it was unreferenced; the REPAIR tab uses it to decide whether the selected system may be
|
|
; repaired.
|
|
; In: filmPlaybackMode ($0B9B), gameOverFlags ($923F).
|
|
; Out: Z=1 (A = 0) when the player may act.
|
|
; Called from: $658B, the REPAIR tab menu loop.
|
|
; ----------------------------------------------------------------------
|
|
isPlayerInputAllowed:
|
|
lda #$00 ; 0EE5 A = 0 stands in for 'the button was pressed'
|
|
jmp L_0EDA ; 0EE7 run only the film/game-over test of checkFirePressedInteractive
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isFireHeld - the level-triggered version of checkFirePressed: no ring-buffer check, no debounce
|
|
; reload and no click sound, so it keeps answering 0 for as long as the button is held (once it has
|
|
; been seen released). Used by the two loops where an action has to run while the button is down -
|
|
; the map screen's group box at $2376 and the console at $407A.
|
|
; In: joyFireUp (zp_5F), fireButtonArmed (zp_79), fireDebounceTimer (zp_6D).
|
|
; Out: A = 0 while the button is held, A = 1 otherwise; fireButtonArmed re-armed when the button is
|
|
; up.
|
|
; Called from: $2376 and $407A.
|
|
; ----------------------------------------------------------------------
|
|
isFireHeld:
|
|
lda fireDebounceTimer ; 0EEA still inside the debounce of an earlier click?
|
|
bne L_0EF4 ; 0EEC then say 'not held'
|
|
lda joyFireUp ; 0EEE 0 = the button is down
|
|
ora fireButtonArmed ; 0EF0 the released-since-last-time latch
|
|
bne fireArmOrHeld ; 0EF2 something to look at
|
|
L_0EF4:
|
|
lda #$01 ; 0EF4 A = 1: the button is not held (or has never been released)
|
|
rts ; 0EF6 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Tail of isFireHeld ($0EEA): reached when the button is up, or is down and has already been seen
|
|
; released once (fireButtonArmed set). A=0 on return means 'the player is holding fire'.
|
|
; ----------------------------------------------------------------------
|
|
fireArmOrHeld:
|
|
lda joyFireUp ; 0EF7 re-read the latched fire state: 0 = button down, 1 = button up
|
|
beq L_0EFD ; 0EF9 button still down (and armed) -> return A=0 = held
|
|
sta fireButtonArmed ; 0EFB button is up: arm the latch so the next press counts, and return A=1 = not held
|
|
L_0EFD:
|
|
rts ; 0EFD back to the caller; Z=1 means fire is being held
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; stopAllSound - Silences every SID voice by requesting sound id 0, which requestSound turns into
|
|
; $FF/$FF/$FF/$00 in the four sound-request slots $92E7-$92EA so the IRQ dequeues 'sound 0' and the
|
|
; player releases all three voices.
|
|
; In: none
|
|
; Out: $92E7-$92EA reset, all voices silenced
|
|
; Called from: initIrqAndInputState ($0BB5) and $1AF7 (end of game)
|
|
; ----------------------------------------------------------------------
|
|
stopAllSound:
|
|
lda #$00 ; 0EFE sound id 0 = the 'silence everything' request
|
|
jmp playSound ; 0F00 tail-call the $C000 sound entry (playSound preserves X/Y and returns to our caller)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadStartupOverlays - One-shot start-up loader: fast-loads track 30 sectors 0-15 into $C000-$CFFF
|
|
; (the text/glyph engine, which replaces the boot loader that lived there), then loadOverlayA - which
|
|
; chains the message strings and the comm tail - and falls straight into loadCommModule.
|
|
; In: isSoloTrainer ($0BA5), read further down the chain
|
|
; Out: $C000-$CFFF, $6F00-$87FF variant A, $8800-$89FF, $EC00-$EFFF, $E000-$EBFF, comm module
|
|
; initialised
|
|
; Called from: mainStart ($0A81), once
|
|
; ----------------------------------------------------------------------
|
|
loadStartupOverlays:
|
|
ldx #$00 ; 0F03 destination low byte $00 ...
|
|
ldy #$C0 ; 0F05 ... and high byte $C0: the transfer lands at $C000
|
|
jsr setLoadDest ; 0F07 store both into the loader's self-modifying destination operands
|
|
ldy #$1E ; 0F0A track $1E = 30, the text/glyph engine overlay
|
|
lda #$10 ; 0F0C $10 = 16 sectors = 4 KB -> $C000-$CFFF
|
|
ldx #$00 ; 0F0E starting at sector 0
|
|
jsr loadSectors ; 0F10 pull it in over the boot loader that is still sitting at $C000
|
|
jsr loadOverlayA ; 0F13 then the $6F00 overlay variant A, the $8800 strings and the $EC00 comm tail
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadCommModule - Loads the opponent module behind the $E000 jump table and initialises it. With
|
|
; isSoloTrainer ($0BA5) bit 7 set it disables the CIA interrupts and reads track 35 sectors 0-11 (the
|
|
; solo trainer), then points the NMI vector at the bare RTI because the trainer never uses NMIs;
|
|
; otherwise it falls into loadCommBuild2 for the real modem driver.
|
|
; In: isSoloTrainer ($0BA5) bit 7
|
|
; Out: $E000-$EBFF filled, comm module initialised, (build 1) NMI vector = irqRti
|
|
; Called from: $0AF4 in startGameFromSetup, and by fall-through from loadStartupOverlays
|
|
; ----------------------------------------------------------------------
|
|
loadCommModule:
|
|
ldx #$00 ; 0F16 destination low byte $00 ...
|
|
ldy #$E0 ; 0F18 ... high byte $E0: the module always loads at $E000
|
|
jsr setLoadDest ; 0F1A arm the loader's destination
|
|
lda isSoloTrainer ; 0F1D which opponent did the player pick?
|
|
bpl loadCommBuild2 ; 0F20 bit 7 clear = modem game -> load build 2 (the software modem)
|
|
jsr disableCiaInterrupts ; 0F22 solo trainer: no CIA interrupts at all while the fast loader runs
|
|
ldy #$23 ; 0F25 track $23 = 35, the solo trainer
|
|
lda #$0C ; 0F27 $0C = 12 sectors = 3 KB -> $E000-$EBFF
|
|
ldx #$00 ; 0F29 from sector 0
|
|
jsr loadCommFinish ; 0F2B load it and run the module's init entry
|
|
jmp setNmiVectorRti ; 0F2E the trainer has no UART, so send NMIs straight to an RTI and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadCommBuild2 - Loads the modem-driver build of the comm module with interrupts off. Its first
|
|
; page comes from track 18 sector 7 (on the directory track, so it is not encrypted and holds the
|
|
; module's jump table and variable block); track 34 sectors 1-11 supply the rest. Falls into
|
|
; loadCommFinish.
|
|
; In: load destination already set to $E000
|
|
; Out: $E000-$EBFF = the modem driver
|
|
; Called from: loadCommModule ($0F20) when isSoloTrainer bit 7 is clear
|
|
; ----------------------------------------------------------------------
|
|
loadCommBuild2:
|
|
sei ; 0F31 no interrupts during the fast-loader handshake
|
|
ldy #$12 ; 0F32 track $12 = 18, the directory track
|
|
ldx #$07 ; 0F34 sector 7 holds the driver's first page
|
|
lda #$01 ; 0F36 just that one sector -> $E000-$E0FF
|
|
jsr loadSectors ; 0F38 read it (loadSectors leaves the destination advanced to $E100)
|
|
ldy #$22 ; 0F3B track $22 = 34, the rest of the modem driver
|
|
ldx #$01 ; 0F3D sectors 1..11 (sector 0 is filler)
|
|
lda #$0B ; 0F3F $0B = 11 sectors -> $E100-$EBFF
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadCommFinish - Shared tail of the two comm-module load paths: does the final loadSectors with the
|
|
; registers the caller set up and falls into initCommModule.
|
|
; In: A = sector count, Y = track, X = first sector, destination from setLoadDest
|
|
; Out: comm module loaded and initialised
|
|
; Called from: loadCommModule ($0F2B); reached by fall-through from loadCommBuild2
|
|
; ----------------------------------------------------------------------
|
|
loadCommFinish:
|
|
jsr loadSectors ; 0F41 final transfer of the module
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initCommModule - Cold-initialises the freshly loaded comm module and grafts the game's own keyboard
|
|
; scanner into it: the module's keyboard vector at $E013/$E014 is patched to scanKeyboard ($0DB7), so
|
|
; the module's $E012/$E015 entries scan the matrix with the game's routine (the KERNAL is banked out).
|
|
; In: comm module resident at $E000
|
|
; Out: $E013/$E014 = $0DB7, interrupts enabled
|
|
; Called from: loadCommFinish (fall-through) and directly from $0AFC
|
|
; ----------------------------------------------------------------------
|
|
initCommModule:
|
|
ldx #$01 ; 0F44 function code 1 = cold initialise the module
|
|
jsr commLinkControl ; 0F46 call the module's life-cycle entry $E003 (also trainerAiE000:commSetLinkStateEntry)
|
|
lda #$B7 ; 0F49 low byte of scanKeyboard ($0DB7) ...
|
|
sta D_E013 ; 0F4B ... into the operand of the module's keyboard JMP
|
|
lda #$0D ; 0F4E high byte $0D ...
|
|
sta D_E014 ; 0F50 ... completing the vector: $E012/$E015 now scan the game's own key matrix
|
|
cli ; 0F53 the module is live: let the raster IRQ run again
|
|
rts ; 0F54 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadSectorsDelayed - Wrapper around loadSectors that first burns 8 raster frames through the $C000
|
|
; frame delay, giving the drive time to settle (and the previous fast-load handshake time to finish)
|
|
; before the next request. Every overlay, map and setup load in this file goes through it.
|
|
; In: A = sector count, Y = track, X = first sector, destination from setLoadDest
|
|
; Out: as loadSectors (memory filled, C=1 on error)
|
|
; Called from: $0FD1, $1054, $105D, $106D, $1096, $109F, $10B8, $107D, $10C8 and overlay A ($7E97,
|
|
; $7F84)
|
|
; ----------------------------------------------------------------------
|
|
loadSectorsDelayed:
|
|
pha ; 0F55 the sector count must survive the delay
|
|
lda #$08 ; 0F56 8 frames ...
|
|
jsr waitFrames ; 0F58 ... of raster-timed idling (Y and X are preserved by waitFrames)
|
|
pla ; 0F5B get the sector count back
|
|
jmp loadSectors ; 0F5C now do the transfer and return to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; suspendCommModule - Asks the comm module to pause before the game touches the disk: the fast loader
|
|
; needs interrupts off and exact cycle timing, which a UART NMI would ruin. When the link is up it
|
|
; writes request code $C0 into the module's suspend-request byte $E039 and spins until the module
|
|
; acknowledges by setting bit 7 of $E03A. A is preserved across the call.
|
|
; In: $E03B (link active flag)
|
|
; Out: $E039 = $C0; blocks until $E03A bit 7 is set
|
|
; Called from: suspendCommForDiskAccess ($10CB)
|
|
; ----------------------------------------------------------------------
|
|
suspendCommModule:
|
|
pha ; 0F5F the caller's A must survive
|
|
lda isLinkActive ; 0F60 is the link actually up? (also trainerAiE000:isLinkActive)
|
|
beq L_0F6F ; 0F63 no link (or the solo trainer) - nothing to suspend
|
|
lda #$C0 ; 0F65 $C0 = 'disable every CIA2 NMI source' request code
|
|
sta nmiSuspendRequest ; 0F67 hand it to the module's request byte $E039 (also trainerAiE000:nmiSuspendRequest)
|
|
L_0F6A:
|
|
lda nmiSuspendAck ; 0F6A poll the acknowledge byte $E03A ... (also trainerAiE000:nmiSuspendAck)
|
|
bpl L_0F6A ; 0F6D ... until bit 7 says the module has parked itself
|
|
L_0F6F:
|
|
pla ; 0F6F restore the caller's A
|
|
rts ; 0F70 the disk may now be touched
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resumeCommModule - Counterpart of suspendCommModule: when the link is up it clears the request byte
|
|
; $E039 and spins until the module clears bit 7 of the acknowledge byte $E03A, i.e. until the UART
|
|
; NMIs are running again.
|
|
; In: $E03B (link active flag)
|
|
; Out: $E039 = 0; blocks until $E03A bit 7 is clear
|
|
; Called from: resumeCommAfterDiskAccess ($10D1)
|
|
; ----------------------------------------------------------------------
|
|
resumeCommModule:
|
|
lda isLinkActive ; 0F71 is the link up? (also trainerAiE000:isLinkActive)
|
|
beq L_0F80 ; 0F74 no link - nothing to resume
|
|
lda #$00 ; 0F76 0 = 'no suspend request'
|
|
sta nmiSuspendRequest ; 0F78 release the module (also trainerAiE000:nmiSuspendRequest)
|
|
L_0F7B:
|
|
lda nmiSuspendAck ; 0F7B poll the acknowledge byte ... (also trainerAiE000:nmiSuspendAck)
|
|
bmi L_0F7B ; 0F7E ... until the module reports it is running again
|
|
L_0F80:
|
|
rts ; 0F80 back to normal service
|
|
|
|
|
|
; Three status-line strings used by the disk routines below, each terminated by a character with bit 7
|
|
; set. showDiskMessageAndFail is called with A = low byte, Y = high byte ($0F).
|
|
msgNotADataDisk:
|
|
.byte "NOT A DATA DISK",'!'|$80; 0F81 shown when the disk in the drive is not the film/data disk ('EA')
|
|
|
|
; msgInsertGameDisk: text, 17 bytes. 'INSERT GAME DISK.' - shown when the 'OZ' game disk is missing.
|
|
msgInsertGameDisk:
|
|
.byte "INSERT GAME DISK",'.'|$80; 0F91 shown when the disk in the drive is not the game disk ('OZ')
|
|
|
|
; msgDiskError: text, 11 bytes. 'DISK ERROR.' - shown by diskErrorExit when the BAM read fails.
|
|
msgDiskError:
|
|
.byte "DISK ERROR",'.'|$80 ; 0FA2 shown when the BAM read itself failed (drive error or no disk)
|
|
|
|
; diskIdTable: two 2-character disk ids indexed by X (X=0 -> 'OZ' = the game disk, X=2 -> 'EA' = the
|
|
; film/data disk). checkDiskId reads the first character with diskIdTable,X and the second with
|
|
; $0FAE,X and compares them against the id field of the BAM at $02A2/$02A3.
|
|
diskIdTable:
|
|
.byte "O" ; 0FAD first character of the game-disk id 'OZ' = Ozark Softscape
|
|
D_0FAE:
|
|
.byte $5A ; 0FAE 'Z' completes 'OZ'; 'E','A' is the data-disk id used for game films
|
|
.byte $45,$41 ; 0FAF EA
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkDiskId - Verifies which disk is in the drive. Reads the BAM (track 18 sector 0, which is not
|
|
; encrypted) to $0200 and compares the disk id at $02A2/$02A3 with the two characters of
|
|
; diskIdTable[X]; the two expected characters and the 'silent' flag are stuffed into instruction
|
|
; operands rather than variables. For the game disk (X=0), outside film playback, it also copies the
|
|
; ten bytes at BAM offset $AB - the owner name the game writes onto each player's disk - into
|
|
; ownPlayerName ($FFDE), which buildOwnerNameString turns into the printed owner line and which is
|
|
; exchanged with the opponent. NOTE: the read clobbers $0200-$02FF, so callers reload the setup
|
|
; block.
|
|
; In: A = silent flag (bit 7 set = fail without a message), X = 0 ('OZ') or 2 ('EA')
|
|
; Out: C=0 ok / C=1 wrong disk or drive error, diskIdIndex ($92F0), $FFDE-$FFE7, $0200-$02FF clobbered
|
|
; Called from: waitForGameDisk ($10E3) and the film save/load code in overlay A ($7E53, $7E5C)
|
|
; ----------------------------------------------------------------------
|
|
checkDiskId:
|
|
sta L_0FFE+1 ; 0FB1 self-modifying: the caller's silent flag becomes the operand of the LDA #$FF at $0FFE
|
|
stx diskIdIndex ; 0FB4 remember which id we are looking for (X is about to be destroyed)
|
|
ldy diskIdTable,x ; 0FB7 Y = expected first id character ('O' or 'E')
|
|
lda D_0FAE,x ; 0FBA A = expected second id character ('Z' or 'A'), from diskIdTable+1
|
|
tax ; 0FBD park it in X so both can be written into the compare operands
|
|
sty L_0FDC+1 ; 0FBE self-modifying: patch the CMP #$FF at $0FDC with the expected first character
|
|
stx L_0FE3+1 ; 0FC1 self-modifying: patch the CMP #$FF at $0FE3 with the expected second character
|
|
ldx #$00 ; 0FC4 BAM destination low byte $00 ...
|
|
ldy #$02 ; 0FC6 ... high byte $02: the sector lands at $0200 (over the terrain rule table)
|
|
jsr setLoadDest ; 0FC8 arm the loader
|
|
lda #$01 ; 0FCB one sector
|
|
ldy #$12 ; 0FCD track $12 = 18, the directory track
|
|
ldx #$00 ; 0FCF sector 0 = the BAM
|
|
jsr loadSectorsDelayed ; 0FD1 read it with the usual 8-frame settling delay
|
|
bcc L_0FD9 ; 0FD4 C=0: the BAM is in memory
|
|
jmp diskErrorExit ; 0FD6 drive error / no disk -> 'DISK ERROR.' and C=1
|
|
|
|
L_0FD9:
|
|
lda bamDiskIdChar0 ; 0FD9 first character of the disk id in the BAM (also trainerPlaybook0200:play1Grp12)
|
|
L_0FDC:
|
|
cmp #$FF ; 0FDC compare against the expected first character (operand patched at $0FBE)
|
|
bne L_0FFE ; 0FDE wrong disk
|
|
lda bamDiskIdChar1 ; 0FE0 second character of the disk id (also trainerPlaybook0200:play1Grp13)
|
|
L_0FE3:
|
|
cmp #$FF ; 0FE3 compare against the expected second character (operand patched at $0FC1)
|
|
bne L_0FFE ; 0FE5 wrong disk
|
|
lda diskIdIndex ; 0FE7 which id were we checking?
|
|
bne L_1014 ; 0FEA non-zero = the 'EA' data disk: nothing else to do, report success
|
|
lda filmPlaybackMode ; 0FEC game disk: is a film being replayed?
|
|
bmi L_1014 ; 0FEF during playback the film supplies the names, so leave $FFDE alone
|
|
ldx #$09 ; 0FF1 copy 10 bytes, index 9 down to 0
|
|
L_0FF3:
|
|
lda bamDiskSerial,x ; 0FF3 BAM offset $AB = the owner name EA writes into the unused tail of the BAM (also trainerPlaybook0200:play1Grp21)
|
|
sta ownPlayerName,x ; 0FF6 -> ownPlayerName ($FFDE-$FFE7), used by buildOwnerNameString and the link handshake
|
|
dex ; 0FF9 next byte
|
|
bpl L_0FF3 ; 0FFA loop until index 0 has been copied
|
|
bmi L_1014 ; 0FFC always taken (X went to $FF): report success
|
|
L_0FFE:
|
|
lda #$FF ; 0FFE operand patched at $0FB1 with the caller's silent flag
|
|
bmi L_1016 ; 1000 bit 7 set = caller wants a silent failure -> just return C=1
|
|
lda #$91 ; 1002 low byte of msgInsertGameDisk ($0F91) ...
|
|
ldy #$0F ; 1004 ... and its high byte
|
|
ldx L_0FDC+1 ; 1006 which id did we expect (the patched compare operand)?
|
|
cpx #$45 ; 1009 'E' means we wanted the data disk 'EA'
|
|
bne L_1011 ; 100B wanted the game disk -> keep 'INSERT GAME DISK.'
|
|
L_100D:
|
|
lda #$81 ; 100D low byte of msgNotADataDisk ($0F81) - overlay A jumps straight in here at $7E71
|
|
ldy #$0F ; 100F ... and its high byte
|
|
L_1011:
|
|
jmp showDiskMessageAndFail ; 1011 show it, beep, pause and return C=1
|
|
|
|
L_1014:
|
|
clc ; 1014 C=0 = the expected disk is in the drive
|
|
rts ; 1015 success return
|
|
|
|
L_1016:
|
|
sec ; 1016 C=1 = wrong disk
|
|
rts ; 1017 silent failure return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; diskErrorExit - Loads the address of msgDiskError ('DISK ERROR.') and falls into
|
|
; showDiskMessageAndFail.
|
|
; In: none
|
|
; Out: message shown, C=1
|
|
; Called from: checkDiskId ($0FD6) and overlay A ($7E9D)
|
|
; ----------------------------------------------------------------------
|
|
diskErrorExit:
|
|
lda #$A2 ; 1018 low byte of msgDiskError ($0FA2) ...
|
|
ldy #$0F ; 101A ... and its high byte
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showDiskMessageAndFail - Common failure exit of the disk routines: error beep, put the message on
|
|
; the centred status line for about two seconds, blank the line again and return C=1.
|
|
; In: A = message address low byte, Y = high byte (bit-7 terminated string)
|
|
; Out: C=1, status line restored
|
|
; Called from: checkDiskId ($1011) and diskErrorExit by fall-through
|
|
; ----------------------------------------------------------------------
|
|
showDiskMessageAndFail:
|
|
pha ; 101C the message pointer's low byte has to survive the beep
|
|
lda #$03 ; 101D sound 3 = the error buzz
|
|
jsr playSound ; 101F queue it (playSound preserves Y, which still holds the message page)
|
|
pla ; 1022 get the low byte back
|
|
jsr showMessageString ; 1023 print the string centred on the 36-column status line
|
|
lda #$64 ; 1026 $64 = 100 frames, about 2 seconds ...
|
|
jsr waitFrames ; 1028 ... so the player can read it
|
|
jsr clearMessageLine ; 102B blank the status line again
|
|
sec ; 102E C=1 = the operation failed
|
|
rts ; 102F back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ensureOverlayAResident - Makes sure the $6F00-$87FF overlay variant A (map generator, setup and menu
|
|
; code) is in memory before control leaves for handleMainMenuChoice ($7DE3). overlayVariantFlag
|
|
; ($87FF) is the identity byte carried by whichever overlay is resident: $FF = variant B, so variant A
|
|
; must be read in full; $00 = variant A is already there and only the $8800 message strings and the
|
|
; $EC00 comm tail (which sub-overlays borrow) need refreshing.
|
|
; In: overlayVariantFlag ($87FF)
|
|
; Out: variant A resident plus fresh strings/comm tail; comm module suspended and resumed around the
|
|
; disk access
|
|
; Called from: $1C72, with diskLoadDepth bumped around the call
|
|
; ----------------------------------------------------------------------
|
|
ensureOverlayAResident:
|
|
jsr suspendCommForDiskAccess; 1030 park the comm module and put 'WORKING...' on the status line
|
|
jsr waitForGameDiskInserted ; 1033 make sure the OZ disk is really in the drive first
|
|
lda overlayVariantFlag ; 1036 identity byte of the resident $6F00 overlay (also mapGenerator6F00:overlayVariantFlag)
|
|
bmi L_1041 ; 1039 $FF = variant B is resident -> the whole of variant A has to be read
|
|
jsr loadStringsAndCommTail ; 103B variant A is already there: just refresh $8800-$89FF and $EC00-$EFFF
|
|
jmp resumeCommAfterDiskAccess; 103E restart the comm module, clear the message and return
|
|
|
|
L_1041:
|
|
jsr loadOverlayA ; 1041 read variant A (which chains the strings and the comm tail)
|
|
jmp resumeCommAfterDiskAccess; 1044 restart the comm module, clear the message and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadOverlayA - Reads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00-$7FFF and
|
|
; track 32 sectors 0-7 to $8000-$87FF. Variant A is the random map generator, the setup phase and the
|
|
; film menus. Falls into loadStringsAndCommTail.
|
|
; In: none
|
|
; Out: $6F00-$87FF = variant A (its last byte $87FF is the identity byte $00), plus $8800-$89FF and
|
|
; $EC00-$EFFF
|
|
; Called from: loadStartupOverlays ($0F13) and ensureOverlayAResident ($1041)
|
|
; ----------------------------------------------------------------------
|
|
loadOverlayA:
|
|
ldx #$00 ; 1047 destination low byte $00 ...
|
|
ldy #$6F ; 1049 ... high byte $6F: the overlay starts at $6F00
|
|
jsr setLoadDest ; 104B arm the loader
|
|
ldy #$1F ; 104E track $1F = 31
|
|
ldx #$00 ; 1050 from sector 0
|
|
lda #$11 ; 1052 $11 = 17 sectors -> $6F00-$7FFF
|
|
jsr loadSectorsDelayed ; 1054 read them
|
|
ldy #$20 ; 1057 track $20 = 32
|
|
ldx #$00 ; 1059 from sector 0
|
|
lda #$08 ; 105B 8 sectors -> $8000-$87FF, ending with the identity byte at $87FF
|
|
jsr loadSectorsDelayed ; 105D read them
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadStringsAndCommTail - Reads the modem status-message string block (track 29 sectors 9-10) into
|
|
; $8800-$89FF, then falls into loadCommTailBuild2. In a solo game the trainer overwrites this block
|
|
; with its own per-unit tables, so it has to be reloaded whenever the strings are wanted again.
|
|
; In: none
|
|
; Out: $8800-$89FF strings, $EC00-$EFFF comm tail
|
|
; Called from: ensureOverlayAResident ($103B); reached by fall-through from loadOverlayA
|
|
; ----------------------------------------------------------------------
|
|
loadStringsAndCommTail:
|
|
ldx #$00 ; 1060 destination low byte $00 ...
|
|
ldy #$88 ; 1062 ... high byte $88
|
|
jsr setLoadDest ; 1064 arm the loader
|
|
ldy #$1D ; 1067 track $1D = 29
|
|
ldx #$09 ; 1069 from sector 9
|
|
lda #$02 ; 106B 2 sectors -> $8800-$89FF
|
|
jsr loadSectorsDelayed ; 106D read them
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadCommTailBuild2 - Reads the last page group of the modem driver (track 34 sectors 12-15) into
|
|
; $EC00-$EFFF. Overlay A also calls it to put the comm tail back after the $EC00 sub-overlay from
|
|
; track 29 has temporarily occupied that space.
|
|
; In: none
|
|
; Out: $EC00-$EFFF = the modem driver tail
|
|
; Called from: overlay A ($7B33); reached by fall-through from loadStringsAndCommTail
|
|
; ----------------------------------------------------------------------
|
|
loadCommTailBuild2:
|
|
ldx #$00 ; 1070 destination low byte $00 ...
|
|
ldy #$EC ; 1072 ... high byte $EC
|
|
jsr setLoadDest ; 1074 arm the loader
|
|
ldy #$22 ; 1077 track $22 = 34
|
|
ldx #$0C ; 1079 from sector $0C = 12
|
|
lda #$04 ; 107B 4 sectors -> $EC00-$EFFF
|
|
jmp loadSectorsDelayed ; 107D read them and return straight to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadOverlayBIfNotResident - Loads the $6F00 overlay variant B only when it is not already in memory.
|
|
; overlayVariantFlag ($87FF) is $00 while variant A is resident and $FF while B is, so a positive
|
|
; value means the swap has to happen.
|
|
; In: overlayVariantFlag ($87FF)
|
|
; Out: see loadOverlayB, or nothing at all
|
|
; Called from: startGameFromSetup ($0B5F)
|
|
; ----------------------------------------------------------------------
|
|
loadOverlayBIfNotResident:
|
|
lda overlayVariantFlag ; 1080 identity byte of the resident $6F00 overlay (also mapGenerator6F00:overlayVariantFlag)
|
|
bpl loadOverlayB ; 1083 $00 = variant A is resident -> swap variant B in
|
|
rts ; 1085 variant B is already there, nothing to do
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadOverlayB - Reads the $6F00-$87FF overlay variant B (the comcen missile and drone screens): track
|
|
; 33 sectors 0-16 to $6F00-$7FFF and track 32 sectors 8-15 to $8000-$87FF. Afterwards it copies the
|
|
; byte at $206B into $750A: both are the opcode byte of a BCC/BCS that follows a 'is this unit index
|
|
; below 50' test, i.e. the side-dependent branch the main program has already patched for this
|
|
; player's side, propagated into the freshly loaded overlay.
|
|
; In: $206B (the patched branch opcode), overlayVariantFlag
|
|
; Out: $6F00-$87FF = variant B, $750A patched, comm module suspended and resumed
|
|
; Called from: loadOverlayBIfNotResident ($1083)
|
|
; ----------------------------------------------------------------------
|
|
loadOverlayB:
|
|
jsr suspendCommForDiskAccess; 1086 park the comm module and show 'WORKING...' - the fast loader needs the interrupts
|
|
ldx #$00 ; 1089 destination low byte $00 ...
|
|
ldy #$6F ; 108B ... high byte $6F
|
|
jsr setLoadDest ; 108D arm the loader
|
|
ldy #$21 ; 1090 track $21 = 33
|
|
lda #$11 ; 1092 $11 = 17 sectors -> $6F00-$7FFF
|
|
ldx #$00 ; 1094 from sector 0
|
|
jsr loadSectorsDelayed ; 1096 read them
|
|
ldy #$20 ; 1099 track $20 = 32
|
|
ldx #$08 ; 109B from sector 8 (sectors 0-7 of that track belong to variant A)
|
|
lda #$08 ; 109D 8 sectors -> $8000-$87FF
|
|
jsr loadSectorsDelayed ; 109F read them
|
|
lda D_206B ; 10A2 the branch opcode ($90 BCC / $B0 BCS) that patchOwnSideBranches wrote at $206B for this player's side
|
|
sta radarSideBranchOpcode ; 10A5 patch the matching 'cpy #$32 / branch' in the overlay at $7508-$750A
|
|
jmp resumeCommAfterDiskAccess; 10A8 restart the comm module, clear the message and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadUnitTemplatesAndSetup - Reads track 18 sectors 8-14 into $F000-$F6FF, then falls into
|
|
; loadSetupBlock. What is on the disk there is not a battlefield: it is four 400-byte unit
|
|
; start-position templates (columns, rows, flags and types for 100 units each) which the setup code
|
|
; picks from and copies into the live unit arrays; the 40x40 map is generated, not loaded.
|
|
; In: none
|
|
; Out: $F000-$F6FF = the start-position templates, $0200-$03FF = the setup block
|
|
; Called from: startGameFromSetup ($0B35), with diskLoadDepth bumped so the IRQ drops keypresses
|
|
; ----------------------------------------------------------------------
|
|
loadUnitTemplatesAndSetup:
|
|
ldx #$00 ; 10AB destination low byte $00 ...
|
|
ldy #$F0 ; 10AD ... high byte $F0
|
|
jsr setLoadDest ; 10AF arm the loader
|
|
ldy #$12 ; 10B2 track $12 = 18, the directory track
|
|
ldx #$08 ; 10B4 from sector 8
|
|
lda #$07 ; 10B6 7 sectors -> $F000-$F6FF (four 400-byte templates plus 192 unused bytes)
|
|
jsr loadSectorsDelayed ; 10B8 read them
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadSetupBlock - Reads the saved setup block (track 18 sectors 15-16) into $0200-$03FF: the terrain
|
|
; tile rule table at $0200 and the persisted game settings plus the little start-up routine at $0300.
|
|
; In: none
|
|
; Out: $0200-$03FF
|
|
; Called from: mainStart ($0A84); reached by fall-through from loadUnitTemplatesAndSetup
|
|
; ----------------------------------------------------------------------
|
|
loadSetupBlock:
|
|
ldx #$00 ; 10BB destination low byte $00 ...
|
|
ldy #$02 ; 10BD ... high byte $02
|
|
jsr setLoadDest ; 10BF arm the loader
|
|
ldy #$12 ; 10C2 track $12 = 18
|
|
ldx #$0F ; 10C4 from sector $0F = 15
|
|
lda #$02 ; 10C6 2 sectors -> $0200-$03FF
|
|
jmp loadSectorsDelayed ; 10C8 read them and return straight to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; suspendCommForDiskAccess - Standard prologue for disk access: pause the comm module, then set
|
|
; persistentMessageId to 1 and paint message 1 ('WORKING...') on the status line so the player knows
|
|
; the drive is busy.
|
|
; In: none
|
|
; Out: comm module paused, persistentMessageId ($92A1) = 1
|
|
; Called from: ensureOverlayAResident ($1030), loadOverlayB ($1086), waitForGameDiskInserted ($10DC)
|
|
; and overlay A ($7B7D, $7DF7, $7F57, $815E)
|
|
; ----------------------------------------------------------------------
|
|
suspendCommForDiskAccess:
|
|
jsr suspendCommModule ; 10CB stop the UART NMIs so the fast loader can keep its timing
|
|
jmp showWorkingMessage ; 10CE show the 'WORKING...' notice and return to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resumeCommAfterDiskAccess - Epilogue for disk access: restart the comm module, forget the persistent
|
|
; 'WORKING...' message and blank the status line.
|
|
; In: none
|
|
; Out: comm module running, persistentMessageId ($92A1) = 0, status line cleared
|
|
; Called from: $0B3D, $103E, $1044, $10A8 and overlay A ($7DFE, $7F95, $81A2, $81AF)
|
|
; ----------------------------------------------------------------------
|
|
resumeCommAfterDiskAccess:
|
|
jsr resumeCommModule ; 10D1 let the module's NMIs run again
|
|
lda #$00 ; 10D4 no message is being held on the line any more
|
|
sta persistentMessageId ; 10D6 clear the persistent message id
|
|
jmp clearMessageLine ; 10D9 blank the status line and return to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForGameDiskInserted - Blocks until the OZ game disk is in the drive. Suspends the comm module
|
|
; (and leaves it suspended for the caller), then loops: check the disk id verbosely, and on failure
|
|
; print 'PRESS BUTTON WHEN READY.' and wait for a fire click before trying again. Because that prompt
|
|
; stirs the RNG while it waits, the delay also randomises the map seed.
|
|
; In: none
|
|
; Out: returns only with C=0 from checkDiskId; comm module left suspended
|
|
; Called from: mainStart ($0ADC), ensureOverlayAResident ($1033) and overlay A ($81AC)
|
|
; ----------------------------------------------------------------------
|
|
waitForGameDiskInserted:
|
|
jsr suspendCommForDiskAccess; 10DC pause the comm module and show 'WORKING...'
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Retry loop: ask for the game disk until it is there.
|
|
; ----------------------------------------------------------------------
|
|
waitForGameDisk:
|
|
ldx #$00 ; 10DF id index 0 = the game disk 'OZ'
|
|
lda #$00 ; 10E1 A bit 7 clear = report a mismatch on the status line
|
|
jsr checkDiskId ; 10E3 read the BAM and compare the id
|
|
bcc L_10EE ; 10E6 C=0: the right disk is in the drive
|
|
jsr promptPressButtonWhenReady; 10E8 wrong disk: prompt and wait for the fire button
|
|
jmp waitForGameDisk ; 10EB then try again
|
|
|
|
L_10EE:
|
|
rts ; 10EE the game disk is present
|
|
|
|
|
|
; spriteEnableScratch: written by the raster IRQ at $11CB with the sprite enable mask it is about to
|
|
; put into VIC_SPR_ENA. Nothing ever reads it back (a dead store); $10F0 is unused padding before the
|
|
; interrupt handler.
|
|
spriteEnableScratch:
|
|
.byte $00,$00 ; 10EF .. copy of the last sprite enable mask written by the IRQ; never read
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rasterIrqHandler - The game's only interrupt handler, entered through the RAM vector $FFFE at raster
|
|
; line $FA (250), installed by installRasterIrq ($0C66). A BRK is recognised from the B flag in the
|
|
; pushed status byte and thrown away through the patchable JMP operand at $10FF/$1100 (a debug hook
|
|
; that initInterruptVectors points at irqExit). Phase 1 acknowledges the VIC interrupt, re-arms the
|
|
; raster line and the $FFFE/$FFFF vector from their zero-page shadows, guards against re-entry with
|
|
; irqInProgress, copies the two VIC control registers from their shadows and services the comm module
|
|
; ($E000 with X=0 and $E00C); this half also runs while a disk load is in progress. Phase 2 copies
|
|
; the remaining VIC shadows and the eight sprite colour/X/Y shadows (colours >= $10 hold two nibbles
|
|
; that alternate every other frame to make a sprite flash; positions can be biased by the fine-scroll
|
|
; offsets), bumps the 24-bit frame counter, re-enables interrupts, polls the keyboard and joystick 2,
|
|
; maintains the idle counter, the RNG and the six frame countdowns, watches for the SHIFT+C=+F7 abort,
|
|
; blinks the map cursor, ticks the frame timers, runs both halves of the chat and drives the sound.
|
|
; In: VIC and CIA1 registers, the shadow block $9000-$9024, inputLockoutTimer ($0B7D), diskLoadDepth
|
|
; ($0BA7), screenShakeTimer ($92C8), spriteScrollFineX/Y ($92F4/$92F5), comm entries $E000/$E00C
|
|
; Out: every VIC register, frameCounter (zp_45-zp_47), joyDirection/joyFireUp and their copies,
|
|
; requestedScreenKey, irqInProgress, the timers zp_6C-zp_71, the chat buffers and the sound state
|
|
; Called from: the hardware IRQ vector $FFFE (and BRK)
|
|
; ----------------------------------------------------------------------
|
|
rasterIrqHandler:
|
|
pha ; 10F1 save the registers the handler is about to use
|
|
txa ; 10F2 X ...
|
|
pha ; 10F3 ... onto the stack
|
|
tya ; 10F4 Y ...
|
|
pha ; 10F5 ... onto the stack
|
|
tsx ; 10F6 S now points 6 bytes below the pushed processor status
|
|
lda D_0104,x ; 10F7 $0104+S = the status byte the CPU pushed when it took the interrupt
|
|
and #$10 ; 10FA bit 4 = the B flag: set means this was a BRK, not a raster IRQ
|
|
beq irqNotBrk ; 10FC a real interrupt - carry on
|
|
L_10FE:
|
|
jmp irqExit ; 10FE BRK debug hook: the operand at $10FF/$1100 is patched by initInterruptVectors to irqExit
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Acknowledge the VIC interrupt and re-arm both the raster line and the RAM interrupt vector; the
|
|
; vector is rewritten every frame because the film code temporarily flips $01 to see the RAM under the
|
|
; I/O area.
|
|
; ----------------------------------------------------------------------
|
|
irqNotBrk:
|
|
lda #$01 ; 1101 bit 0 = the raster interrupt latch ...
|
|
sta VIC_IRQ_FLAG ; 1103 ... written back to clear it
|
|
lda rasterIrqLine ; 1106 the raster line the game wants ($FA = 250, below the visible playfield)
|
|
sta VIC_RASTER ; 1108 re-arm VIC_RASTER for the next frame
|
|
lda irqVectorShadowLo ; 110B shadow of the IRQ vector low byte ...
|
|
sta irqVector ; 110D ... written back into $FFFE
|
|
lda irqVectorShadowHi ; 1110 shadow of the high byte ...
|
|
sta irqVectorHi ; 1112 ... written back into $FFFF
|
|
lda irqInProgress ; 1115 is a previous invocation still running? (the handler re-enables interrupts halfway through)
|
|
bne irqBailOut ; 1117 yes - do nothing this frame
|
|
inc irqInProgress ; 1119 mark the handler as busy
|
|
lda vicCtrl1Shadow ; 111B shadow of VIC_CTRL1 (screen mode, raster bit 8, Y scroll)
|
|
sta VIC_CTRL1 ; 111E -> $D011
|
|
lda vicCtrl2Shadow ; 1121 shadow of VIC_CTRL2 (multicolour, X scroll)
|
|
sta VIC_CTRL2 ; 1124 -> $D016
|
|
ldx #$00 ; 1127 function 0 = the comm module's per-frame service tick
|
|
jsr commJumpTable ; 1129 run the UART / trainer bit-and-byte timing (also trainerAiE000:commRequestEntry)
|
|
jsr pollLinkStatus ; 112C and the suspend handshake plus the carrier-detect sampling (also trainerAiE000:commFrameTickEntry)
|
|
lda diskLoadDepth ; 112F how deeply nested is the current disk load?
|
|
bpl irqPhase2 ; 1132 always taken in practice - nothing ever makes diskLoadDepth negative, so the bail-out below is effectively dead
|
|
irqBailOut:
|
|
jmp irqExit ; 1134 bail out with only the comm module serviced
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Phase 2: push the rest of the VIC shadow block ($9002-$9006, $9020, $9023, $9024) into the chip.
|
|
; Keeping shadows and copying them once per frame means the game never has to worry about a partly
|
|
; updated register being displayed.
|
|
; ----------------------------------------------------------------------
|
|
irqPhase2:
|
|
lda vicMemPtrShadow ; 1137 shadow of VIC_MEM_PTR (bitmap at $A000, video matrix at $8C00 within VIC bank 2)
|
|
sta VIC_MEM_PTR ; 113A -> $D018
|
|
lda vicBorderShadow ; 113D border colour shadow
|
|
sta VIC_BORDER ; 1140 -> $D020
|
|
lda vicBg0Shadow ; 1143 background colour 0 shadow
|
|
sta VIC_BG0 ; 1146 -> $D021
|
|
lda vicBg1Shadow ; 1149 multicolour background 1 shadow
|
|
sta VIC_BG1 ; 114C -> $D022
|
|
lda vicBg2Shadow ; 114F multicolour background 2 shadow
|
|
sta VIC_BG2 ; 1152 -> $D023
|
|
lda spriteXMsbShadow ; 1155 sprite X bit-8 mask shadow
|
|
sta VIC_SPR_HI_X ; 1158 -> $D010
|
|
lda spriteXExpandShadow ; 115B sprite X expand mask shadow
|
|
sta VIC_SPR_EXP_X ; 115E -> $D01D
|
|
lda spriteYExpandShadow ; 1161 sprite Y expand mask shadow
|
|
sta VIC_SPR_EXP_Y ; 1164 -> $D017
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Sprite position bias: while screenShakeTimer is running the sprites are shifted by the current fine
|
|
; scroll offsets so they stay glued to the smooth-scrolled bitmap; otherwise both biases are zero. The
|
|
; two offsets are written into the ADC operands at $11B6 and $11BF.
|
|
; ----------------------------------------------------------------------
|
|
lda screenShakeTimer ; 1167 is the screen being shaken / fine-scrolled?
|
|
bne irqUseScrollOffsets ; 116A yes - derive the offsets from the scroll position
|
|
sta L_11B5+1 ; 116C no bias: patch the X adder at $11B5 with 0
|
|
sta L_11BE+1 ; 116F and the Y adder at $11BE with 0
|
|
beq irqSpriteLoopSetup ; 1172 always taken (A is still 0)
|
|
irqUseScrollOffsets:
|
|
lda spriteScrollFineY ; 1174 vertical fine-scroll position
|
|
and #$07 ; 1177 only the 0-7 pixel offset
|
|
sec ; 1179 prepare a borrow-free subtraction
|
|
sbc #$03 ; 117A centre it: -3..+4 pixels
|
|
sta L_11BE+1 ; 117C patch the Y adder operand at $11BE
|
|
lda spriteScrollFineX ; 117F horizontal fine-scroll position
|
|
and #$07 ; 1182 only the 0-7 pixel offset
|
|
sta L_11B5+1 ; 1184 patch the X adder operand at $11B5
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the eight sprite shadows into the VIC. Y counts the sprite index 7..0 and X the register
|
|
; offset 2*index, so $D000+X / $D001+X are that sprite's X and Y registers. A colour shadow below
|
|
; $10 is a plain colour; $10 and above packs two nibbles which are swapped every other frame to make
|
|
; the sprite flash between two colours.
|
|
; ----------------------------------------------------------------------
|
|
irqSpriteLoopSetup:
|
|
ldy #$07 ; 1187 start with sprite 7
|
|
ldx #$0E ; 1189 $0E = 7*2, the X/Y register offset of sprite 7
|
|
lda frameCounter ; 118B free-running frame counter ...
|
|
ror a ; 118D ... rotated right twice ...
|
|
ror a ; 118E ... so that bit 7 of the result is bit 0 of the frame counter
|
|
sta spriteFlashPhase ; 118F the flash phase: negative on every other frame
|
|
irqSpriteLoop:
|
|
lda spriteColourShadow,y ; 1191 colour shadow of sprite Y ($9008+Y)
|
|
cmp #$10 ; 1194 values 0-$0F are ordinary colours
|
|
bcc irqWriteSpriteColour ; 1196 plain colour - write it straight out
|
|
bit spriteFlashPhase ; 1198 two packed nibbles: only swap them on the flashing frames
|
|
bpl irqSpritePosition ; 119A off-phase frame - leave the colour register untouched
|
|
eor VIC_SPR0_COLOR,y ; 119C compare the shadow with what the register currently holds
|
|
and #$0F ; 119F look at the low nibble of the difference
|
|
cmp #$01 ; 11A1 C=1 when the register does not already show the shadow's low nibble
|
|
lda spriteColourShadow,y ; 11A3 reload the packed shadow byte
|
|
bcs irqWriteSpriteColour ; 11A6 register differs -> show the low nibble this time
|
|
lsr a ; 11A8 otherwise shift the high nibble down ...
|
|
lsr a ; 11A9 ... (4 shifts) ...
|
|
lsr a ; 11AA ... so the sprite alternates ...
|
|
lsr a ; 11AB ... between the two packed colours
|
|
irqWriteSpriteColour:
|
|
and #$0F ; 11AC only the colour bits
|
|
sta VIC_SPR0_COLOR,y ; 11AE -> $D027+Y, the sprite's colour register
|
|
irqSpritePosition:
|
|
lda spriteXShadow,y ; 11B1 X position shadow of sprite Y ($9010+Y)
|
|
clc ; 11B4 clear carry for the bias add
|
|
L_11B5:
|
|
adc #$FF ; 11B5 add the horizontal bias (operand patched at $116C or $1184)
|
|
sta VIC_SPR0_X,x ; 11B7 -> $D000+X, the sprite's X register
|
|
lda spriteYShadow,y ; 11BA Y position shadow of sprite Y ($9018+Y)
|
|
clc ; 11BD clear carry for the bias add
|
|
L_11BE:
|
|
adc #$FF ; 11BE add the vertical bias (operand patched at $116F or $117C)
|
|
sta VIC_SPR0_Y,x ; 11C0 -> $D001+X, the sprite's Y register
|
|
dex ; 11C3 step the register offset ...
|
|
dex ; 11C4 ... down by two
|
|
dey ; 11C5 next sprite
|
|
bpl irqSpriteLoop ; 11C6 loop until sprite 0 has been done
|
|
lda spriteEnableShadow ; 11C8 sprite enable mask shadow
|
|
sta spriteEnableScratch ; 11CB keep a copy at $10EF (nothing ever reads it back)
|
|
sta VIC_SPR_ENA ; 11CE -> $D015, so sprites appear and disappear in step with the rest of the frame
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Bump the free-running 24-bit frame counter zp_45/zp_46/zp_47.
|
|
; ----------------------------------------------------------------------
|
|
inc frameCounter ; 11D1 low byte of the frame counter
|
|
bne irqAfterSprites ; 11D3 no carry into the middle byte
|
|
inc frameCounterMid ; 11D5 middle byte
|
|
bne irqAfterSprites ; 11D7 no carry into the high byte
|
|
inc frameCounterHi ; 11D9 high byte: wraps after about 77 hours
|
|
|
|
; ----------------------------------------------------------------------
|
|
; From here on the handler runs with interrupts enabled - irqInProgress keeps a second raster IRQ from
|
|
; re-entering it. inputLockoutTimer counts frames during which the keyboard and joystick are not
|
|
; sampled at all, so joyDirection and joyFireUp keep whatever they had.
|
|
; ----------------------------------------------------------------------
|
|
irqAfterSprites:
|
|
cli ; 11DB everything time-critical is done; allow interrupts again
|
|
lda inputLockoutTimer ; 11DC is input still locked out (after a screen change or an abort)?
|
|
beq irqPollInput ; 11DF no - sample the keyboard and the stick
|
|
dec inputLockoutTimer ; 11E1 count the lockout down one frame ...
|
|
jmp irqIdleCounters ; 11E4 ... and skip the whole input poll
|
|
|
|
irqPollInput:
|
|
jsr pollKeyboardEvent ; 11E7 scan the key matrix through the comm module's keyboard entry
|
|
lda #$FF ; 11EA default 'joystick idle' pattern in case a key is pending
|
|
ldy requestedScreenKey ; 11EC did the poll leave a screen-select key pending (0-3 = F1/F3/F5/F7, $FF = none)?
|
|
bpl irqStoreJoystick ; 11EE yes - do not read the stick this frame
|
|
ldy newKeyEvent ; 11F0 is a typed key waiting to be consumed?
|
|
cpy #$FF ; 11F3 $FF = no key
|
|
bne irqStoreJoystick ; 11F5 a key is pending - do not read the stick this frame
|
|
lda #$FF ; 11F7 deselect every keyboard row ...
|
|
sta CIA1_PRA ; 11F9 ... so CIA1 port A shows joystick 2 instead of the matrix
|
|
L_11FC:
|
|
lda CIA1_PRA ; 11FC read the port ...
|
|
nop ; 11FF ... let it settle ...
|
|
cmp CIA1_PRA ; 1200 ... and read it again
|
|
bne L_11FC ; 1203 the two reads disagree (contact bounce) - try once more
|
|
irqStoreJoystick:
|
|
and #$1F ; 1205 keep bits 0-4: up, down, left, right, fire (all active low)
|
|
ldy #$01 ; 1207 assume the button is up
|
|
cmp #$10 ; 1209 bit 4 = the fire button
|
|
bcs L_120E ; 120B bit set = button not pressed - keep Y=1
|
|
dey ; 120D button down -> Y=0
|
|
L_120E:
|
|
sty joyFireUp ; 120E latch the fire state for the whole game
|
|
sty joyFireUpCopy ; 1210 second copy - nothing ever reads it (dead store)
|
|
and #$0F ; 1212 keep the four direction bits ($0F = centred)
|
|
sta joyDirection ; 1214 latch the direction for the whole game
|
|
sta joyDirectionCopy ; 1216 second copy - nothing ever reads it (dead store)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; inputIdleFrames counts consecutive frames with the stick centred and the button up, saturating at
|
|
; $FF; anything else resets it to 0. The pause/attract logic elsewhere uses it.
|
|
; ----------------------------------------------------------------------
|
|
irqIdleCounters:
|
|
lda joyDirection ; 1218 the latched direction nibble
|
|
cmp #$0F ; 121A $0F = all four switches open = centred
|
|
bne L_1227 ; 121C the stick is being pushed
|
|
lda #$00 ; 121E stick centred: forget any held direction ...
|
|
sta joyRepeatMask ; 1220 ... by clearing the direction repeat mask
|
|
lda joyFireUp ; 1223 is the fire button also up?
|
|
bne L_122E ; 1225 yes - nothing at all is being touched
|
|
L_1227:
|
|
lda #$00 ; 1227 some input is active ...
|
|
sta inputIdleFrames ; 1229 ... so restart the idle counter
|
|
beq L_1238 ; 122C always taken
|
|
L_122E:
|
|
lda inputIdleFrames ; 122E frames of no input so far
|
|
cmp #$FF ; 1231 already saturated?
|
|
beq L_1238 ; 1233 yes - leave it at $FF
|
|
inc inputIdleFrames ; 1235 another idle frame
|
|
L_1238:
|
|
jsr stepRandomSeed ; 1238 stir the 24-bit game RNG once per frame
|
|
ldx #$05 ; 123B index 5 down to 0 covers zp_71..zp_6C
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Decrement the six frame countdowns zp_6C-zp_71 (joystick repeat, fire debounce, menu blink, marker
|
|
; blink, key repeat and one spare), each only while it is non-zero.
|
|
; ----------------------------------------------------------------------
|
|
irqTimerLoop:
|
|
lda joyRepeatTimer,x ; 123D timer zp_6C+X
|
|
beq L_1243 ; 123F already expired - leave it alone
|
|
dec joyRepeatTimer,x ; 1241 one frame closer to expiry
|
|
L_1243:
|
|
dex ; 1243 next timer
|
|
bpl irqTimerLoop ; 1244 loop over all six
|
|
lda diskLoadDepth ; 1246 is a disk load or overlay swap in progress?
|
|
beq irqCheckAbortHotkey ; 1249 no - function keys may switch screens
|
|
lda #$FF ; 124B $FF = no screen requested ...
|
|
sta requestedScreenKey ; 124D ... so an F-key pressed during a load is thrown away
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The panic hotkey: SHIFT + C= + F7 abandons whatever is happening and returns to the main menu.
|
|
; returnToMainMenu resets the stack pointer, so this JMP never comes back - the interrupt frame is
|
|
; simply abandoned (interrupts were already re-enabled at $11DB).
|
|
; ----------------------------------------------------------------------
|
|
irqCheckAbortHotkey:
|
|
lda shiftHeldFlag ; 124F was SHIFT held during the key scan?
|
|
bpl irqPerFrameWork ; 1252 no - not the abort combination
|
|
lda commodoreHeldFlag ; 1254 was the Commodore key held too?
|
|
bpl irqPerFrameWork ; 1257 no - not the abort combination
|
|
lda requestedScreenKey ; 1259 which function key is pending?
|
|
cmp #$03 ; 125B 3 = F7
|
|
bne irqPerFrameWork ; 125D not F7 - carry on normally
|
|
lda #$01 ; 125F lock the input out ...
|
|
sta diskLoadDepth ; 1261 ... by pretending a disk load is running
|
|
lda #$FF ; 1264 $FF ...
|
|
sta inputLockoutTimer ; 1266 ... = the longest possible input lockout
|
|
sta requestedScreenKey ; 1269 ... and forget the pending F7
|
|
jmp returnToMainMenu ; 126B never returns: returnToMainMenu rebuilds the stack from scratch
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Per-frame housekeeping that is allowed to run with interrupts on.
|
|
; ----------------------------------------------------------------------
|
|
irqPerFrameWork:
|
|
lda currentScreen ; 126E which screen is showing?
|
|
bne L_127A ; 1271 not the battlefield screen - no map cursor to blink
|
|
lda gamePhase ; 1273 gamePhase: bit 7 marks the phases in which there is no map cursor to blink
|
|
bmi L_127A ; 1275 gamePhase negative -> skip the blink
|
|
jsr blinkCursorColour ; 1277 toggle the map cursor sprite colour
|
|
L_127A:
|
|
jsr updateFrameTimers ; 127A count the game's own frame timers down
|
|
jsr handleChatSend ; 127D outgoing chat: pick up typed characters and queue them to the opponent
|
|
jsr receiveChatCharacter ; 1280 incoming chat: take one received byte and add it to the message line
|
|
jsr dequeueSoundRequest ; 1283 take the highest-priority queued sound request into X
|
|
txa ; 1286 X = $FF when nothing is queued
|
|
bmi L_128C ; 1287 nothing to start
|
|
jsr startSound ; 1289 start sound A on its voices
|
|
L_128C:
|
|
jsr soundPlayerTick ; 128C advance the SID sound scripts by one frame
|
|
lda #$00 ; 128F the handler is finished ...
|
|
sta irqInProgress ; 1291 ... so a later raster IRQ may run it again
|
|
|
|
; ----------------------------------------------------------------------
|
|
; irqExit - Pulls Y, X and A back off the stack and falls into irqRti. It is both the normal end of
|
|
; rasterIrqHandler and the target that initInterruptVectors patches into the BRK-exit JMP at
|
|
; $10FF/$1100.
|
|
; In: the register frame pushed by rasterIrqHandler
|
|
; Out: registers restored, RTI
|
|
; Called from: $10FE (BRK) and $1134 (early bail-out); reached by fall-through at the end of the
|
|
; handler
|
|
; ----------------------------------------------------------------------
|
|
irqExit:
|
|
pla ; 1293 Y ...
|
|
tay ; 1294 ... restored
|
|
pla ; 1295 X ...
|
|
tax ; 1296 ... restored
|
|
pla ; 1297 and A
|
|
|
|
; ----------------------------------------------------------------------
|
|
; irqRti - A bare RTI. It is the NMI vector target for the whole game: initInterruptVectors and
|
|
; setNmiVectorRti put $1298 into $FFFA/$FFFB and into the comm module's own NMI chain vector
|
|
; ($E031/$E032), so a stray RS-232 NMI cannot disturb the game when no modem session is running.
|
|
; In: none
|
|
; Out: none
|
|
; Called from: the NMI vector; reached by fall-through from irqExit
|
|
; ----------------------------------------------------------------------
|
|
irqRti:
|
|
rti ; 1298 return from interrupt
|
|
|
|
|
|
; Three leftover bytes between irqRti and updateFrameTimers. Nothing references them and they do not
|
|
; disassemble into anything sensible - alignment padding or the tail of an edited-out routine.
|
|
padIrq1299:
|
|
.byte $00,$09,$13 ; 1299 ... unreferenced padding
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateFrameTimers - Per-frame housekeeping called from the raster IRQ. It bumps the two
|
|
; free-running animation ticks, then runs three groups of counters: an 8-frame group (the
|
|
; packet-exchange watchdog, the end-of-game countdown and linkWaitTimer), a 4-frame group (the
|
|
; status-message hold timer and the link poll delay) and a per-frame group (step delay, shot line,
|
|
; view redraw and marker sprite). Every countdown is guarded so it stops at 0.
|
|
; In: frameCounter (zp_45), gamePhase (zp_B1)
|
|
; Out: $92F8, $92FB, $9223, $92C9, zp_7F, zp_72, $9163, $90FC, $91C8, $9234, $923E
|
|
; Called from: rasterIrqHandler ($127A)
|
|
; ----------------------------------------------------------------------
|
|
updateFrameTimers:
|
|
inc missileFlightTick ; 129C animation tick used by the missile screen
|
|
inc radarSweepTick ; 129F animation tick used by the radar sweep
|
|
lda frameCounter ; 12A2 the frame counter drives all the slower groups
|
|
and #$07 ; 12A4 every 8th frame ...
|
|
bne L_12D1 ; 12A6 ... otherwise skip straight to the 4-frame group
|
|
lda frameCounter ; 12A8 which of the two 8-frame slots is this?
|
|
and #$0F ; 12AA low nibble 0 = one of the two, i.e. every 16 frames
|
|
beq bumpLinkWatchdog ; 12AC 16-frame slot: always bump the watchdog
|
|
lda gamePhase ; 12AE the odd 8-frame slot only counts when gamePhase is not $FF ...
|
|
cmp #$FF ; 12B0 ... so at gamePhase $FF the watchdog runs at half speed (every 16 frames)
|
|
beq L_12C1 ; 12B2 skip the watchdog this time
|
|
|
|
; ----------------------------------------------------------------------
|
|
; linkTimeoutCounter is the packet-exchange watchdog: checkLinkTimeout ($5A14) arms it with bit 7 set,
|
|
; a completed exchange clears it at $4E69, and here it counts up while negative and sticks at
|
|
; $FF, which is how the game notices that the opponent has stopped answering.
|
|
; ----------------------------------------------------------------------
|
|
bumpLinkWatchdog:
|
|
lda linkTimeoutCounter ; 12B4 the watchdog value
|
|
bpl L_12C1 ; 12B7 positive = not armed, nothing to do
|
|
clc ; 12B9 clear carry for the increment
|
|
adc #$01 ; 12BA one more tick towards the timeout
|
|
bcs L_12C1 ; 12BC already at $FF - saturate instead of wrapping to 0
|
|
sta linkTimeoutCounter ; 12BE store the advanced watchdog
|
|
L_12C1:
|
|
lda gameEndCountdown ; 12C1 gameEndCountdown: the delay from 'game over' to the summary screen
|
|
beq L_12CB ; 12C4 0 = not running
|
|
bmi L_12CB ; 12C6 negative = it has already reached its $80 end stop
|
|
inc gameEndCountdown ; 12C8 count it up towards $80
|
|
L_12CB:
|
|
lda linkWaitTimer ; 12CB linkWaitTimer: the timeout used while waiting for the comm module
|
|
beq L_12D1 ; 12CD already expired
|
|
dec linkWaitTimer ; 12CF one 8-frame tick closer to zero
|
|
L_12D1:
|
|
lda frameCounter ; 12D1 the frame counter again ...
|
|
and #$03 ; 12D3 ... for the every-4-frames group
|
|
bne L_12E5 ; 12D5 not a 4-frame slot
|
|
lda messageTimer ; 12D7 messageTimer: how long the status-line message stays up
|
|
beq L_12DD ; 12D9 already expired
|
|
dec messageTimer ; 12DB count it down
|
|
L_12DD:
|
|
lda linkPollDelay ; 12DD linkPollDelay: pacing of the link status polls
|
|
beq L_12E5 ; 12E0 already expired
|
|
dec linkPollDelay ; 12E2 count it down
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The per-frame group: four independent countdowns, each frozen once it reaches 0.
|
|
; ----------------------------------------------------------------------
|
|
L_12E5:
|
|
lda stepDelayTimer ; 12E5 stepDelayTimer: frames until the next unit step of the round
|
|
beq L_12ED ; 12E8 already expired
|
|
dec stepDelayTimer ; 12EA count it down
|
|
L_12ED:
|
|
lda shotLineTimer ; 12ED shotLineTimer: how long a shot line stays drawn in the viewport
|
|
beq L_12F5 ; 12F0 already expired
|
|
dec shotLineTimer ; 12F2 count it down
|
|
L_12F5:
|
|
lda viewRedrawTimer ; 12F5 viewRedrawTimer: frames until the 7x5 view is redrawn
|
|
beq L_1301 ; 12F8 already expired
|
|
cmp #$80 ; 12FA $80 is the 'frozen / redraw pending' sentinel ...
|
|
beq L_1301 ; 12FC ... which must not be counted down
|
|
dec viewRedrawTimer ; 12FE count it down
|
|
L_1301:
|
|
lda markerSpriteTimer ; 1301 markerSpriteTimer: how long the repair/target marker sprites stay visible
|
|
beq L_1309 ; 1304 already expired
|
|
dec markerSpriteTimer ; 1306 count it down
|
|
L_1309:
|
|
rts ; 1309 back to the interrupt handler
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; stepRandomSeed - Called from the IRQ every frame. If the 24-bit game RNG state has collapsed to all
|
|
; zeroes (which would lock the LFSR up) it re-seeds the low byte from the current joystick direction,
|
|
; so the generator both recovers and is fed a little entropy from the player, then steps the LFSR
|
|
; once.
|
|
; In: gameRngState0-2 (zp_57-zp_59), joyDirection (zp_5D)
|
|
; Out: the RNG state advanced, A = the new random byte
|
|
; Called from: rasterIrqHandler ($1238)
|
|
; ----------------------------------------------------------------------
|
|
stepRandomSeed:
|
|
lda gameRngState0 ; 130A byte 0 of the RNG state ...
|
|
ora gameRngState1 ; 130C ... ORed with byte 1 ...
|
|
ora gameRngState2 ; 130E ... and byte 2
|
|
bne L_1316 ; 1310 any bit set - the state is fine
|
|
lda joyDirection ; 1312 all zero: take the joystick nibble as fresh entropy ...
|
|
sta gameRngState0 ; 1314 ... and drop it into byte 0 so the LFSR can run again
|
|
L_1316:
|
|
jsr nextGameRandom ; 1316 advance the 24-bit LFSR in the high-memory block
|
|
rts ; 1319 A holds the new random byte
|
|
|
|
|
|
; cursorBlinkTimer: the 30-frame countdown of blinkCursorColour, stored in the code stream just in
|
|
; front of the routine that owns it.
|
|
cursorBlinkTimer:
|
|
.byte $00 ; 131A . counts 30..0 and then wraps; bit 7 signals 'time to toggle'
|
|
|
|
; ----------------------------------------------------------------------
|
|
; blinkCursorColour - Blinks the map cursor. A 30-frame countdown toggles bit 0 of the sprite 0
|
|
; colour shadow and copies the result to the sprite 2 shadow, so the two cursor sprites alternate
|
|
; between two adjacent colours. The IRQ only calls it while currentScreen is 0 (the battlefield
|
|
; screen) and gamePhase is not negative.
|
|
; In: cursorBlinkTimer ($131A), spriteColourShadow ($9008)
|
|
; Out: $131A, $9008, $900A
|
|
; Called from: rasterIrqHandler ($1277)
|
|
; ----------------------------------------------------------------------
|
|
blinkCursorColour:
|
|
dec cursorBlinkTimer ; 131B one frame off the blink countdown
|
|
bpl L_1330 ; 131E still positive - nothing to do this frame
|
|
lda #$1E ; 1320 $1E = 30 frames until the next toggle ...
|
|
sta cursorBlinkTimer ; 1322 ... reload the countdown
|
|
lda spriteColourShadow ; 1325 sprite 0 colour shadow
|
|
eor #$01 ; 1328 flip the lowest colour bit (e.g. between colours 6 and 7)
|
|
sta spriteColourShadow ; 132A back into the sprite 0 shadow
|
|
sta sprite2ColourShadow ; 132D and mirror it into the sprite 2 shadow so both halves of the cursor match
|
|
L_1330:
|
|
rts ; 1330 the IRQ picks the shadows up on the next frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleChatSend - The outgoing half of the modem chat ('message to opponent'), run every frame from
|
|
; the IRQ. chatState ($929C) is the state byte: 0 = off, $7F = 'open the typing line', and bit 7 set
|
|
; = typing, with bits 0-5 holding the cursor position in the outgoing line buffer $9278. Typing is
|
|
; entered by RETURN (or by another routine setting $7F) provided the module's transmit ring is empty;
|
|
; after that each frame hands one key to the $C000 line editor and queues the accepted character to
|
|
; the opponent. If the transmit ring fills up the line is terminated with RETURN instead.
|
|
; In: $E03B link flag, $E02E transmit ring count, filmNameLength ($92E6), chatState ($929C),
|
|
; newKeyEvent ($90EA)
|
|
; Out: chatState, the line buffer $9278.., newKeyEvent/lastTypedKey cleared, one byte queued through
|
|
; $E009, sounds
|
|
; Called from: rasterIrqHandler ($127D)
|
|
; ----------------------------------------------------------------------
|
|
handleChatSend:
|
|
lda isLinkActive ; 1331 is the link up? (also trainerAiE000:isLinkActive)
|
|
beq L_133B ; 1334 no link - chat is impossible
|
|
lda filmNameLength ; 1336 is the SAVE GAME FILM name field using the same line buffer?
|
|
beq L_1341 ; 1339 no - the chat may use it
|
|
L_133B:
|
|
lda #$00 ; 133B shut the chat down ...
|
|
sta chatState ; 133D ... by clearing the state byte
|
|
rts ; 1340 nothing more to do this frame
|
|
|
|
L_1341:
|
|
lda chatState ; 1341 the chat state byte
|
|
bmi L_1373 ; 1344 bit 7 set = already typing -> go and send the next character
|
|
cmp #$7F ; 1346 $7F = another routine asked for the typing line to be opened
|
|
beq L_1352 ; 1348 open it
|
|
lda newKeyEvent ; 134A otherwise look at the last key
|
|
cmp #$8D ; 134D $8D = RETURN opens the chat line
|
|
beq L_1352 ; 134F open it
|
|
L_1351:
|
|
rts ; 1351 nothing to do this frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Enter typing mode - but only if the module has nothing left to transmit.
|
|
; ----------------------------------------------------------------------
|
|
L_1352:
|
|
lda isLinkActive ; 1352 double-check the link is still up (also trainerAiE000:isLinkActive)
|
|
beq L_1351 ; 1355 it dropped - abandon
|
|
lda hostOutCount ; 1357 how many bytes are still queued for transmission? (also trainerAiE000:hostOutCount)
|
|
bne L_1351 ; 135A the ring is not empty - wait until it drains
|
|
lda #$C0 ; 135C $C0 = bit 7 (typing) + bit 6, cursor position 0 ...
|
|
sta chatState ; 135E ... becomes the new chat state
|
|
lda #$9E ; 1361 $9E = the cursor glyph ...
|
|
sta typedLineBuffer ; 1363 ... put at the head of the outgoing line so the player sees a caret
|
|
lda #$FF ; 1366 swallow the key ...
|
|
sta newKeyEvent ; 1368 ... that opened the line ...
|
|
sta lastTypedKey ; 136B ... and the typed-key copy as well
|
|
lda #$02 ; 136E sound 2 = the typing click
|
|
jmp requestSound ; 1370 requestSound is called directly (playSound is not re-entrant and this is inside the IRQ)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Typing mode: bits 0-5 of chatState are the cursor position. One key per frame is fed to the shared
|
|
; line editor and the accepted character is queued to the opponent, so the remote sees the message
|
|
; being typed letter by letter.
|
|
; ----------------------------------------------------------------------
|
|
L_1373:
|
|
and #$3F ; 1373 cursor position within the 36-byte line buffer
|
|
tay ; 1375 the editor wants it in Y
|
|
lda hostOutCount ; 1376 how full is the transmit ring? (also trainerAiE000:hostOutCount)
|
|
cmp #$10 ; 1379 $10 = 16 bytes = full
|
|
bcc L_1385 ; 137B room left - edit one more character
|
|
tya ; 137D ring full: force the line to end ...
|
|
ora #$80 ; 137E ... by setting bit 7 of the cursor position (the editor's 'RETURN' marker) ...
|
|
tay ; 1380 ... back into Y
|
|
lda #$8D ; 1381 and send RETURN instead of a character
|
|
bne L_138C ; 1383 always taken
|
|
L_1385:
|
|
lda #$A3 ; 1385 $A3 = max 35 characters with bit 7 = 'scroll the line left when it is full'
|
|
jsr handleTextEntryKey ; 1387 let the $C000 line editor consume newKeyEvent into the buffer
|
|
beq L_1351 ; 138A nothing was accepted this frame
|
|
L_138C:
|
|
pha ; 138C keep the accepted character
|
|
tya ; 138D the editor's updated cursor position
|
|
eor #$80 ; 138E toggle bit 7: a normal character leaves it set (still typing), RETURN clears it (line finished)
|
|
ora #$40 ; 1390 bit 6 is always set so the state byte is never mistaken for 0
|
|
sta chatState ; 1392 store the new chat state
|
|
pla ; 1395 get the character back
|
|
jmp putLinkByte ; 1396 queue it to the opponent and return to the IRQ (also trainerAiE000:commSendByteEntry)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; receiveChatCharacter - The incoming half of the chat, called every frame from the IRQ. It pulls one
|
|
; byte out of the comm module's receive ring and ignores anything below $80. RETURN ($8D) marks the
|
|
; message complete, DEL ($88) backspaces by overwriting the previous character with a space, and
|
|
; everything else is appended to the incoming message line.
|
|
; In: the comm receive ring through $E006 (C=1 = empty), incomingChatLength ($9277)
|
|
; Out: the $9253 buffer and the $0548 status line, $9277, messageTimer cleared, sound 5
|
|
; Called from: rasterIrqHandler ($1280)
|
|
; ----------------------------------------------------------------------
|
|
receiveChatCharacter:
|
|
jsr getLinkByte ; 1399 fetch one received byte (C=1 = nothing waiting) (also trainerAiE000:commGetReceivedByteEntry)
|
|
bcs L_1411 ; 139C nothing received this frame
|
|
cmp #$80 ; 139E chat characters always have bit 7 set ...
|
|
bcc L_1411 ; 13A0 ... so anything below $80 is not chat text
|
|
cmp #$8D ; 13A2 $8D = RETURN, the end of the opponent's message
|
|
bne L_13AD ; 13A4 not RETURN - try DEL
|
|
lda #$80 ; 13A6 $80 marks the message complete: bit 7 set, length 0 ...
|
|
sta incomingChatLength ; 13A8 ... so the next character starts a fresh message
|
|
bmi L_1408 ; 13AB always taken - go and beep
|
|
L_13AD:
|
|
cmp #$88 ; 13AD $88 = DEL
|
|
bne appendChatCharacter ; 13AF an ordinary character - append it
|
|
lda incomingChatLength ; 13B1 how long is the message so far?
|
|
beq L_1400 ; 13B4 empty - nothing to erase
|
|
bmi L_1400 ; 13B6 the previous message was already complete - nothing to erase
|
|
dec incomingChatLength ; 13B8 step back over the last character
|
|
lda #$20 ; 13BB a space ...
|
|
jsr appendChatCharacter ; 13BD ... written over it (this re-increments the length)
|
|
dec incomingChatLength ; 13C0 step back again so the next character lands on that space
|
|
rts ; 13C3 back to the IRQ
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; appendChatCharacter - Appends character A to the 36-byte incoming message buffer at $9253 and
|
|
; rewrites the visible status line from it. A negative incomingChatLength means the previous message
|
|
; was finished, so the buffer restarts at 0; when the buffer is full at 35 characters it is scrolled
|
|
; one byte left. The line is rewritten backwards from the newest character so that only that
|
|
; character carries bit 7 - the game's string terminator convention - and the printed line therefore
|
|
; always ends exactly where the message does.
|
|
; In: A = character, incomingChatLength ($9277), the $9253 buffer
|
|
; Out: $9253-$9276, $9277, the status line $0548-$056C, messageTimer = 0, sound 5
|
|
; Called from: receiveChatCharacter ($13AF as a branch, $13BD as a call)
|
|
; ----------------------------------------------------------------------
|
|
appendChatCharacter:
|
|
ldy incomingChatLength ; 13C4 current message length = the index of the next free byte
|
|
bpl L_13CE ; 13C7 a message is in progress
|
|
ldy #$00 ; 13C9 previous message finished: start a new one ...
|
|
sty incomingChatLength ; 13CB ... at offset 0
|
|
L_13CE:
|
|
cpy #$23 ; 13CE $23 = 35 characters, the width of the line
|
|
bcc L_13E5 ; 13D0 there is still room
|
|
pha ; 13D2 the buffer is full - save the new character
|
|
ldx #$00 ; 13D3 scroll the whole buffer one byte to the left ...
|
|
L_13D5:
|
|
lda incomingChatScrollSrc,x ; 13D5 ... reading from $9254 (buffer+1) ...
|
|
sta incomingChatBuffer,x ; 13D8 ... and writing to $9253
|
|
inx ; 13DB next byte
|
|
cpx #$24 ; 13DC $24 = 36 bytes
|
|
bcc L_13D5 ; 13DE keep scrolling
|
|
dey ; 13E0 the write position moves back one
|
|
dec incomingChatLength ; 13E1 and so does the recorded length
|
|
pla ; 13E4 get the new character back
|
|
L_13E5:
|
|
and #$7F ; 13E5 store it without the terminator bit
|
|
sta incomingChatBuffer,y ; 13E7 into the buffer at the write position
|
|
inc incomingChatLength ; 13EA the message is one character longer
|
|
ora #$80 ; 13ED the newest character carries the bit-7 terminator
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Rewrite the whole visible line backwards, newest character first, so that exactly one byte (the last
|
|
; one) has bit 7 set and the string printer stops there.
|
|
; ----------------------------------------------------------------------
|
|
chatLineWriteLoop:
|
|
sta D_0549,y ; 13EF write into the status line text at $0549+index
|
|
dey ; 13F2 walk backwards through the message
|
|
bmi L_13FB ; 13F3 the whole message has been written
|
|
lda incomingChatBuffer,y ; 13F5 earlier characters are stored plain (bit 7 clear)
|
|
jmp chatLineWriteLoop ; 13F8 keep going
|
|
|
|
L_13FB:
|
|
lda #$1E ; 13FB $1E = the leading marker glyph ...
|
|
sta chatMessageLine ; 13FD ... at $0548, in front of the message text
|
|
L_1400:
|
|
ldy messageQueueHead ; 1400 leftover test: the message queue head ...
|
|
cpy messageQueueTail ; 1403 ... compared with the tail ...
|
|
bne L_1408 ; 1406 ... but both outcomes continue at $1408, so this compare has no effect (dead code)
|
|
L_1408:
|
|
lda #$00 ; 1408 0 ...
|
|
sta messageTimer ; 140A ... clears the message hold timer so the chat line is shown at once
|
|
lda #$05 ; 140C sound 5 = the incoming-character click
|
|
jsr requestSound ; 140E requestSound directly - this runs inside the IRQ, where playSound is not re-entrant
|
|
L_1411:
|
|
rts ; 1411 back to the IRQ
|
|
|
|
|
|
; $1412 is a scratch byte the REPAIR tab uses as a 3..0 loop counter while it erases and redraws the
|
|
; four comcen system markers ($184B-$187C). $1413 holds the comcenStunStatus value that was current
|
|
; when drawRepairTab last ran ($175F), so the console loop can spot a change of stun state. The
|
|
; $7F written into $1412 at $141F is never read: the repair code always reloads it first.
|
|
repairLoopCounter:
|
|
.byte $00 ; 1412 . REPAIR tab marker loop counter; the $7F stored at $141F is a leftover dead store
|
|
repairTabStunCopy:
|
|
.byte $00 ; 1413 copy of comcenStunStatus taken when the REPAIR tab was last drawn
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resumeMainMenuScreen - Re-entry into the console screen (screen 3, the F7 screen with the STATS /
|
|
; REPAIR / MISC tabs) that does not rebuild the frame: it just re-picks the default tab, clears the
|
|
; text window and joins the console loop at its 'tab has changed' handler so the panel is redrawn.
|
|
; Used when the comcen-stunned blackout ($0709) hands control back to whichever screen was showing.
|
|
; In: none
|
|
; Out: menuTabIndex ($90F9) set, the tab panel redrawn; never returns (the console loop takes over)
|
|
; Called from: $076D, the screen dispatch at the end of runComcenStunnedBlackout
|
|
; ----------------------------------------------------------------------
|
|
resumeMainMenuScreen:
|
|
jsr selectDefaultTab ; 1414 pick the tab the current game state suggests
|
|
jsr clearMenuTextWindow ; 1417 blank the panel area under the tab bar
|
|
jmp drawSelectedTab ; 141A draw the newly selected tab and fall into the console loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; enterMainMenu - Full entry into the console screen (screen 3): picks the default tab, draws the
|
|
; whole screen (tab bar and frames) and enters the console loop with that tab's panel drawn.
|
|
; In: comcenStunStatus ($922A) and everything selectDefaultTab reads
|
|
; Out: the console screen drawn, menuTabIndex set; never returns
|
|
; Called from: $0706, the F7 branch of switchToRequestedScreen
|
|
; ----------------------------------------------------------------------
|
|
enterMainMenu:
|
|
lda #$7F ; 141D $7F into the REPAIR tab's scratch byte $1412 ...
|
|
sta repairLoopCounter ; 141F ... which nothing ever reads: the repair marker loops always reload it first, so this is a leftover dead store
|
|
ldy #$00 ; 1422 leftover tab pre-selection: default to tab 0 (STATS) ...
|
|
lda comcenStunStatus ; 1424 ... unless the comcen is stunned ...
|
|
bpl L_142A ; 1427 ... (bit 7 of comcenStunStatus) ...
|
|
iny ; 1429 ... in which case tab 1 (REPAIR)
|
|
L_142A:
|
|
sty menuTabIndex ; 142A store it - but selectDefaultTab three bytes later overwrites it unconditionally, so all of $1422-$142A is dead
|
|
jsr selectDefaultTab ; 142D really choose the tab
|
|
jsr showMainMenu ; 1430 draw the tab bar and both frames (it reads menuTabIndex for the text colour)
|
|
jsr selectDefaultTab ; 1433 choose the tab again - redundant, nothing showMainMenu does changes the inputs
|
|
jmp drawSelectedTab ; 1436 draw that tab's panel and fall into the console loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mainMenuLoop - The console screen's main loop (screen 3, tabs STATS / REPAIR / MISC). Every pass it
|
|
; runs one frame of the game engine (the battle keeps going while the console is open), blinks the
|
|
; selected tab, refreshes the REPAIR tab when the comcen stun state changes and the STATS tab when a
|
|
; command exchange has been applied, and honours the function keys. The joystick moves the selection
|
|
; left and right between the three tabs with a 20-frame repeat, and the fire button dispatches through
|
|
; tabActionTable. The loop never returns; every path jumps back to $1439.
|
|
; In: joyDirection (zp_5D), joyRepeatTimer (zp_6C), menuTabIndex ($90F9), comcenStunStatus ($922A),
|
|
; exchangeAppliedFlag ($920E)
|
|
; Out: menuTabIndex, the console screen; runs forever
|
|
; Called from: entered by fall-through/JMP from resumeMainMenuScreen and enterMainMenu
|
|
; ----------------------------------------------------------------------
|
|
mainMenuLoop:
|
|
jsr updateGameFrame ; 1439 keep the simulation running while the console is open
|
|
jsr blinkSelectedTab ; 143C toggle the selected tab between its bright and normal colour
|
|
lda menuTabIndex ; 143F which tab is showing?
|
|
cmp #$01 ; 1442 1 = REPAIR
|
|
bne L_1456 ; 1444 any other tab - skip the repair refresh
|
|
lda comcenStunStatus ; 1446 current comcen stun state ...
|
|
eor repairTabStunCopy ; 1449 ... against the copy taken when the tab was drawn ...
|
|
and #$80 ; 144C ... looking only at bit 7 (stunned or not)
|
|
beq L_1453 ; 144E unchanged - no full redraw needed
|
|
jsr drawRepairTab ; 1450 the stun state flipped: redraw the whole REPAIR panel in its new colour
|
|
L_1453:
|
|
jsr printEnergyValue ; 1453 refresh the comcen energy percentage (it only prints when the value changed)
|
|
L_1456:
|
|
lda exchangeAppliedFlag ; 1456 has a command exchange been applied since the STATS tab was drawn?
|
|
beq L_1463 ; 1459 no - nothing to refresh
|
|
lda menuTabIndex ; 145B which tab is showing?
|
|
bne L_1463 ; 145E only tab 0 (STATS) shows the numbers that just changed
|
|
jsr drawStatsTab ; 1460 recount the units and reprint the STATS panel (it clears the flag again) (also mapGenerator6F00:editDronesAndMissilesBody)
|
|
L_1463:
|
|
jsr switchToRequestedScreen ; 1463 let F1/F3/F5/F7 switch to another screen (never returns if it does)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Joystick handling: left and right move between the three tabs, everything else falls through to the
|
|
; fire-button test. joyRepeatTimer holds the auto-repeat delay so a held stick steps once every 20
|
|
; frames.
|
|
; ----------------------------------------------------------------------
|
|
lda joyRepeatTimer ; 1466 is the joystick auto-repeat still counting down?
|
|
bne L_1470 ; 1468 yes - ignore the stick, only look at the button
|
|
lda joyDirection ; 146A the latched joystick direction
|
|
cmp #$0F ; 146C $0F = centred
|
|
bne menuJoyLeftRight ; 146E the stick is being pushed - see which way
|
|
L_1470:
|
|
jsr checkFirePressed ; 1470 edge-detect a fresh fire-button press
|
|
bne mainMenuLoop ; 1473 no press - go round again
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fire on a tab: light that tab and call its handler through tabActionTable. The handler address is
|
|
; copied into the operand of the JSR at $148D, which is why the disassembly shows 'jsr $FFFF' there.
|
|
; ----------------------------------------------------------------------
|
|
ldx menuTabIndex ; 1475 the selected tab
|
|
jsr setTabHighlightColour ; 1478 show it in its bright colour while its action runs
|
|
lda menuTabIndex ; 147B tab index ...
|
|
asl a ; 147E ... times two ...
|
|
tay ; 147F ... is the offset into the table of 16-bit handler addresses
|
|
lda tabActionTable,y ; 1480 handler low byte ...
|
|
sta L_148D+1 ; 1483 ... self-modifying: into the operand of the JSR at $148D
|
|
iny ; 1486 next table byte
|
|
lda tabActionTable,y ; 1487 handler high byte ...
|
|
sta L_148D+2 ; 148A ... into the high half of that operand
|
|
L_148D:
|
|
jsr irqVectorHi ; 148D call the tab's fire handler (operand patched just above; tab 0 points at the RTS at $158E)
|
|
L_1490:
|
|
jmp mainMenuLoop ; 1490 back to the top of the loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Left/right tab movement. The direction bits are active low, so after masking bits 2 and 3, $04
|
|
; means right is pressed and $08 means left; $0C means neither.
|
|
; ----------------------------------------------------------------------
|
|
menuJoyLeftRight:
|
|
and #$0C ; 1493 keep the left (bit 2) and right (bit 3) switches
|
|
cmp #$0C ; 1495 $0C = neither of them pressed
|
|
beq mainMenuLoop ; 1497 up or down only - ignore it
|
|
ldx #$14 ; 1499 $14 = 20 frames ...
|
|
stx joyRepeatTimer ; 149B ... of auto-repeat delay before the selection may move again
|
|
pha ; 149D remember the direction
|
|
ldx menuTabIndex ; 149E the tab we are leaving ...
|
|
jsr setTabNormalColour ; 14A1 ... goes back to its normal colour
|
|
pla ; 14A4 get the direction back
|
|
cmp #$04 ; 14A5 $04 = right pressed (bit 3 low)
|
|
bne L_14B5 ; 14A7 not right, so it must be left
|
|
lda menuTabIndex ; 14A9 the selected tab
|
|
cmp #$02 ; 14AC 2 = MISC, the rightmost tab
|
|
beq L_1490 ; 14AE already at the right edge - ignore
|
|
inc menuTabIndex ; 14B0 move one tab to the right
|
|
bne L_14BD ; 14B3 always taken
|
|
L_14B5:
|
|
lda menuTabIndex ; 14B5 the selected tab
|
|
beq L_1490 ; 14B8 already at STATS, the left edge - ignore
|
|
dec menuTabIndex ; 14BA move one tab to the left
|
|
L_14BD:
|
|
lda #$02 ; 14BD sound 2 = the selection click
|
|
jsr playSound ; 14BF play it
|
|
ldx menuTabIndex ; 14C2 the newly selected tab ...
|
|
jsr setTabHighlightColour ; 14C5 ... gets its bright colour
|
|
jsr clearMenuTextWindow ; 14C8 blank the panel under the tab bar ready for the new contents
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Draw the panel of the currently selected tab. Entered directly by both console entry points as well
|
|
; as after a tab change.
|
|
; ----------------------------------------------------------------------
|
|
drawSelectedTab:
|
|
lda menuTabIndex ; 14CB which tab?
|
|
bne L_14D9 ; 14CE not tab 0
|
|
jsr drawGameSetupSummary ; 14D0 STATS: the game type, rules, play order, owner names and map id
|
|
jsr drawStatsTab ; 14D3 plus the unit counts, drones, missiles, comspeed and terrain points (in overlay B) (also mapGenerator6F00:editDronesAndMissilesBody)
|
|
jmp mainMenuLoop ; 14D6 back to the loop
|
|
|
|
L_14D9:
|
|
cmp #$01 ; 14D9 tab 1?
|
|
bne L_14E6 ; 14DB no - it must be MISC
|
|
jsr drawRepairTab ; 14DD REPAIR: the comcen schematic and its four system markers
|
|
jsr snapshotComcenStatus ; 14E0 remember the comcen status that is now on screen
|
|
jmp mainMenuLoop ; 14E3 back to the loop
|
|
|
|
L_14E6:
|
|
cmp #$02 ; 14E6 tab 2?
|
|
bne L_14ED ; 14E8 menuTabIndex is only ever 0, 1 or 2 (every writer is in this loop), so this branch is never taken
|
|
jsr printOptionsList ; 14EA MISC: print the option list (ABORT GAME, VOICE PAUSE, EXIT ...)
|
|
L_14ED:
|
|
jmp mainMenuLoop ; 14ED back to the loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; snapshotComcenStatus - Remembers what the REPAIR tab is currently showing: copies the three comcen
|
|
; system status bytes ($9242-$9244) into the snapshot at $92D7-$92D9 and the comcen's energy (the low
|
|
; 6 bits of its entry in unitEnergyTable) into $92D6. selectDefaultTab compares the live values
|
|
; against this snapshot to decide whether the player still has repairs to look at.
|
|
; In: comcenSystemStatus ($9241..), comcenUnit ($9236), unitEnergyTable ($F9C4)
|
|
; Out: comcenStatusSnapshot ($92D6-$92D9)
|
|
; Called from: mainMenuLoop ($14E0) and the end of repairTabAction ($187E)
|
|
; ----------------------------------------------------------------------
|
|
snapshotComcenStatus:
|
|
ldx #$03 ; 14F0 index 3 down to 1: the battle, radar and drone system bytes
|
|
L_14F2:
|
|
lda comcenSystemStatus,x ; 14F2 system status byte $9241+X
|
|
sta comcenStatusSnapshot,x ; 14F5 into the snapshot at $92D6+X
|
|
dex ; 14F8 next system
|
|
bne L_14F2 ; 14F9 stop at index 1 - slot 0 is filled from the energy below
|
|
ldx comcenUnit ; 14FB this side's comcen (unit 49 or 99)
|
|
lda unitEnergyTable,x ; 14FE its energy byte
|
|
and #$3F ; 1501 the low 6 bits are the energy 0-50 (the top bits are flags)
|
|
sta comcenStatusSnapshot ; 1503 snapshot slot 0 = the comcen energy
|
|
rts ; 1506 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; selectDefaultTab - Chooses which console tab should be showing. If the game is over or has not
|
|
; started (gameEndReason non-zero) it opens MISC (2), where ABORT/EXIT live. Otherwise it opens
|
|
; REPAIR (1) when the comcen is stunned or when anything on the comcen has changed since the last
|
|
; snapshotComcenStatus, and MISC (2) when nothing needs attention. STATS (0) is never chosen here.
|
|
; In: gameEndReason ($0BA8), comcenStunStatus ($922A), comcenSystemStatus, comcenStatusSnapshot,
|
|
; comcenUnit, unitEnergyTable
|
|
; Out: menuTabIndex ($90F9)
|
|
; Called from: resumeMainMenuScreen ($1414) and enterMainMenu ($142D, $1433)
|
|
; ----------------------------------------------------------------------
|
|
selectDefaultTab:
|
|
ldy #$02 ; 1507 assume tab 2 = MISC
|
|
lda gameEndReason ; 1509 0 while a game is actually running
|
|
bne L_1531 ; 150C no game in progress - MISC it is
|
|
ldy #$00 ; 150E a game is running: assume tab 0 = STATS for now
|
|
lda comcenStunStatus ; 1510 is the comcen stunned?
|
|
bmi L_152F ; 1513 yes - the player needs the REPAIR tab
|
|
ldx #$03 ; 1515 compare the three system bytes with the snapshot, index 3 down to 1
|
|
L_1517:
|
|
lda comcenSystemStatus,x ; 1517 live system status $9241+X ...
|
|
cmp comcenStatusSnapshot,x ; 151A ... against the snapshot $92D6+X
|
|
bne L_152F ; 151D something changed - show REPAIR
|
|
dex ; 151F next system
|
|
bne L_1517 ; 1520 stop at index 1
|
|
ldx comcenUnit ; 1522 this side's comcen
|
|
lda unitEnergyTable,x ; 1525 its energy byte
|
|
and #$3F ; 1528 keep the 0-50 energy value
|
|
cmp comcenStatusSnapshot ; 152A has the comcen lost energy since the snapshot?
|
|
beq L_1531 ; 152D unchanged - nothing to repair, leave Y at 2 = MISC
|
|
L_152F:
|
|
ldy #$01 ; 152F something needs attention: tab 1 = REPAIR
|
|
L_1531:
|
|
sty menuTabIndex ; 1531 store the chosen tab
|
|
rts ; 1534 done
|
|
|
|
|
|
; tabActionTable: the three fire-button handlers of the console tabs, one 16-bit little-endian address
|
|
; each, indexed by menuTabIndex*2 and copied into the JSR operand at $148D.
|
|
; +0 tab 0 STATS -> $158E, the RTS at the end of setMenuWindowBounds, i.e. no action at all
|
|
; +2 tab 1 REPAIR -> repairTabAction ($17DA), the 'repair which system' menu
|
|
; +4 tab 2 MISC -> miscTabAction ($1A49), the ABORT GAME / VOICE PAUSE / EXIT option list
|
|
tabActionTable:
|
|
.addr $158E ; 1535 three handler addresses, one per console tab
|
|
.addr repairTabAction ; 1537
|
|
.addr miscTabAction ; 1539
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showMainMenu - Builds the console screen from scratch: resets the glyph plotter's parameter block,
|
|
; sets the background colour, draws the STATS/REPAIR/MISC tab bar and the two nested frames, and ends
|
|
; by repainting the status message line.
|
|
; In: none
|
|
; Out: the whole screen 3 drawn; textDrawParams, vicBg0Shadow ($9004), cellBackgroundColour ($9226),
|
|
; screenBuiltFlag (zp_41)
|
|
; Called from: returnToMainMenu ($0AC2) and enterMainMenu ($1430)
|
|
; ----------------------------------------------------------------------
|
|
showMainMenu:
|
|
lda #$00 ; 153B 0 into the four glyph-plotter parameters below
|
|
sta useDrawColourFlag ; 153D useFixedColourFlag = 0: take the colour from the text state, not a fixed byte
|
|
sta drawColourByte ; 1540 drawColourByte = 0
|
|
sta blockFillFlag ; 1543 blockFillFlag = 0
|
|
sta blockFillChar ; 1546 blockFillChar = 0
|
|
lda #$0C ; 1549 colour $0C = medium grey ...
|
|
sta vicBg0Shadow ; 154B ... as the screen background (the IRQ copies the shadow into $D021)
|
|
lda #$00 ; 154E 0 ...
|
|
sta cellBackgroundColour ; 1550 ... = the cell background colour used by the tile drawing
|
|
lda #$01 ; 1553 1 ...
|
|
sta screenBuiltFlag ; 1555 ... marks 'a screen has been built' (written by every screen builder, read nowhere)
|
|
lda #$00 ; 1557 0 ...
|
|
sta blockFillFlag ; 1559 ... into blockFillFlag again - redundant, it was already cleared at $1543
|
|
jsr drawMenuTabBar ; 155C print STATS, REPAIR and MISC and colour their three tab boxes
|
|
jsr drawMenuFrame ; 155F draw the outer and inner frames
|
|
jmp redrawStatusMessage ; 1562 repaint whatever message belongs on the status line and return to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawMenuFrame - Draws the console's two nested frames from the predefined window table: window $18
|
|
; is the outer 40x20 box starting at row 5, window $19 the inner 38x18 box at row 6. Falls into
|
|
; clearMenuTextWindow.
|
|
; In: none
|
|
; Out: the frames drawn, then the text window set and cleared
|
|
; Called from: showMainMenu ($155F)
|
|
; ----------------------------------------------------------------------
|
|
drawMenuFrame:
|
|
ldx #$18 ; 1565 window $18 = column 0, row 5, 40x20 cells - the outer frame
|
|
jsr drawWindowFrame ; 1567 draw it from its 9-tile frame set
|
|
ldx #$19 ; 156A window $19 = column 1, row 6, 38x18 cells - the inner frame
|
|
jsr drawWindowFrame ; 156C draw it too
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearMenuTextWindow - Sets the console panel's text colour and window bounds and blanks the window.
|
|
; In: comcenStunStatus ($922A), menuTabIndex ($90F9)
|
|
; Out: textColour (zp_3D), the window bounds zp_39-zp_40, the panel area cleared
|
|
; Called from: resumeMainMenuScreen ($1417), mainMenuLoop ($14C8), $1ACA, $1AD6 and overlay A ($7EA0)
|
|
; ----------------------------------------------------------------------
|
|
clearMenuTextWindow:
|
|
jsr setMenuTextWindow ; 156F colour and window bounds for the panel
|
|
jmp bootDriveDataBit ; 1572 blank it and return to our caller (also textEngineC000:clearTextWindow)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setMenuTextWindow - Picks the console text colour from the comcen stun state (red on the REPAIR tab
|
|
; while the comcen is stunned) and falls into setMenuWindowBounds.
|
|
; In: comcenStunStatus ($922A), menuTabIndex ($90F9)
|
|
; Out: textColour (zp_3D) plus the window bounds
|
|
; Called from: clearMenuTextWindow ($156F), $1866 and $1A80
|
|
; ----------------------------------------------------------------------
|
|
setMenuTextWindow:
|
|
jsr setMenuTextColour ; 1575 $61 normally, $21 on a stunned REPAIR tab
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setMenuWindowBounds - Sets the text window that the console panels print into: rows 7 to 21, left
|
|
; column 2, 36 characters wide, cursor homed at the top left of the window. Its final RTS at $158E
|
|
; doubles as the do-nothing handler of the STATS tab in tabActionTable.
|
|
; In: none
|
|
; Out: textCursorCol=0, textWindowTop=7, textCursorRow=7, textWindowLeft=2, textWindowBottom=$16,
|
|
; textWindowWidth=$24
|
|
; Called from: setMenuTextWindow by fall-through, clearMenuWindowPlain ($1593) and $19E7
|
|
; ----------------------------------------------------------------------
|
|
setMenuWindowBounds:
|
|
lda #$00 ; 1578 cursor column 0 ...
|
|
sta textCursorCol ; 157A ... relative to the window's left edge
|
|
lda #$07 ; 157C row 7 = the first line under the tab bar and frame ...
|
|
sta textWindowTop ; 157E ... as the top of the window ...
|
|
sta textCursorRow ; 1580 ... and as the starting cursor row
|
|
lda #$02 ; 1582 screen column 2 ...
|
|
sta textWindowLeft ; 1584 ... is the window's left edge
|
|
lda #$16 ; 1586 $16 = row 22 is the exclusive bottom limit ...
|
|
sta textWindowBottom ; 1588 ... so the window covers rows 7-21
|
|
lda #$24 ; 158A $24 = 36 characters ...
|
|
sta textWindowWidth ; 158C ... is the window width and the wrap point
|
|
rts ; 158E also the STATS tab's 'do nothing' fire handler (tabActionTable entry 0)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearMenuWindowPlain - Blanks the whole console-menu panel with text colour 0 (black text, no
|
|
; highlight EOR mask). This is the screen-id-3 arm of clearCurrentScreenWindows ($2DDE), which
|
|
; re-clears the content area of whichever screen is active; it is reached only by the JMP at $2E0A, so
|
|
; it returns straight to that routine's caller (typically the $C000 overlay's resume-after-pause path
|
|
; at $C75E).
|
|
; In: nothing in registers; textFillGlyph zp_3E supplies the blank glyph
|
|
; Out: zp_3D = 0, the menu window geometry of setMenuWindowBounds (columns 2..37, rows 7..21) and the
|
|
; whole panel filled with blanks
|
|
; Called from: JMP at $2E0A (clearCurrentScreenWindows, screen id 3)
|
|
; ----------------------------------------------------------------------
|
|
clearMenuWindowPlain:
|
|
lda #$00 ; 158F colour byte 0: black text and an all-zero highlight EOR mask
|
|
sta textColour ; 1591 the text engine writes zp_3D into colour RAM for every cell it touches
|
|
jsr setMenuWindowBounds ; 1593 menu panel window: left column 2, width $24, rows 7..$16
|
|
jmp bootDriveDataBit ; 1596 fill the panel with the blank glyph zp_3E and return to clearCurrentScreenWindows' caller (also textEngineC000:clearTextWindow)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawGameSetupSummary - Draws the fixed header of the console STATS tab (menu tab 0): the rules word
|
|
; STANDARD/CUSTOM, the game-type name, an optional '. 1ST PLAY'/'. 2ND PLAY' suffix, then the run-time
|
|
; owner-name line and the column of row labels (GRUNTS/RIDERS/.../MAP ID) that drawStatsTab ($8518,
|
|
; overlay B) later fills numbers in beside. For a CUSTOM game it adds the ' MENUS xxxx DAMAGE xxx.'
|
|
; summary line.
|
|
; In: gameTypeOptions $0BA3 (bits 0-2 game type, bit 6 CUSTOM rules), playsFirstFlag $0BA1,
|
|
; ownerNameString $0B7F, statsTabLabels $86AC and msgOptionSummary $0636 (both only valid while
|
|
; overlay B / the string block is resident)
|
|
; Out: text drawn into the bitmap from row 7 down; zp_3B window left ends at 10 (or 8 for CUSTOM),
|
|
; zp_3D = $6E, zp_3F/zp_40 left after the last character; self-modifies the operand at $15DD
|
|
; Called from: mainMenuLoop at $14D0 when tab 0 (STATS) becomes the selected tab
|
|
; Line 1 of the STATS tab: '<STANDARD|CUSTOM> <GAME TYPE>[. 1ST PLAY]'.
|
|
; ----------------------------------------------------------------------
|
|
drawGameSetupSummary:
|
|
lda #$6F ; 1599 colour byte $6F: light-grey text (colour RAM low nibble), highlight EOR mask 6
|
|
sta textColour ; 159B set the text colour for the heading line
|
|
lda #$00 ; 159D cursor to the left edge of the window
|
|
sta textCursorCol ; 159F textCursorCol is relative to textWindowLeft
|
|
lda #$07 ; 15A1 heading starts at screen column 7
|
|
sta textWindowLeft ; 15A3 window left edge = 7
|
|
lda #$07 ; 15A5 first line of the panel is screen row 7
|
|
sta textCursorRow ; 15A7 absolute cursor row
|
|
jsr updateCursorPointers ; 15A9 recompute the bitmap and colour-RAM pointers for that cell
|
|
ldx #$01 ; 15AC assume rulesNameTable entry 1 = CUSTOM
|
|
lda gameTypeOptions ; 15AE game settings byte: bits 0-2 type, bit 3 UNIT MENUS, bit 4 DAMAGE, bit 6 CUSTOM rules
|
|
pha ; 15B1 keep the settings byte on the stack for the game-type lookup below
|
|
and #$40 ; 15B2 bit 6 = CUSTOM rules selected
|
|
bne L_15C2 ; 15B4 branch if CUSTOM - keep table entry 1 and always print the play order
|
|
jsr isAsymmetricGameType ; 15B6 Z=1 for game type 1 (QB SNEAK) or 6 (DEFENDER), the asymmetric ones
|
|
beq L_15C0 ; 15B9 branch if this type shows 1ST/2ND PLAY - no padding needed
|
|
lda #$04 ; 15BB four blanks...
|
|
jsr bootReceiveSectorData ; 15BD ...printFillChars ($C0EF): centres 'STANDARD <type>' when no ' 1ST PLAY' follows (also textEngineC000:printFillChars)
|
|
L_15C0:
|
|
ldx #$00 ; 15C0 rulesNameTable entry 0 = STANDARD
|
|
L_15C2:
|
|
stx L_15DC+1 ; 15C2 self-modifying code: patches the LDA #imm operand at $15DD, so $15DC loads 1 for CUSTOM and 0 for STANDARD
|
|
lda #$11 ; 15C5 low byte of rulesNameTable
|
|
ldy #$CC ; 15C7 $CC11 = rulesNameTable, two 8-byte bit-7-terminated entries
|
|
jsr printTableEntry ; 15C9 print entry X ($CC11 + X*8): 'STANDARD' or 'CUSTOM'
|
|
lda #$01 ; 15CC one blank...
|
|
jsr bootReceiveSectorData ; 15CE ...between the rules word and the game type name (also textEngineC000:printFillChars)
|
|
pla ; 15D1 recover the settings byte
|
|
and #$07 ; 15D2 bits 0-2 = game type 0..6
|
|
tax ; 15D4 use it as the table index
|
|
lda #$C8 ; 15D5 low byte of scenarioNameTable
|
|
ldy #$CC ; 15D7 $CCC8 = scenarioNameTable, seven 8-byte names
|
|
jsr printTableEntry ; 15D9 print SCRIMAGE / QB SNEAK / THE BOMB / FACE-OFF / SLUGGERS / FULL WAR / DEFENDER
|
|
L_15DC:
|
|
lda #$FF ; 15DC operand patched at $15C2: 1 = CUSTOM rules, 0 = STANDARD
|
|
bne L_15E5 ; 15DE CUSTOM rules always show whose play it is
|
|
jsr isAsymmetricGameType ; 15E0 standard rules: only QB SNEAK and DEFENDER have a play order
|
|
bne L_1606 ; 15E3 symmetric game type under standard rules - skip the play-order suffix
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Suffix '. 1ST PLAY' / '. 2ND PLAY'.
|
|
; ----------------------------------------------------------------------
|
|
L_15E5:
|
|
lda #$AE ; 15E5 $AE = '.' with bit 7 set; printChar masks bit 7 off
|
|
jsr printChar ; 15E7 print the full stop after the game type
|
|
lda #$02 ; 15EA two blanks...
|
|
jsr bootReceiveSectorData ; 15EC ...before the play-order words (also textEngineC000:printFillChars)
|
|
lda #$FE ; 15EF low byte of '1ST'
|
|
ldy #$CB ; 15F1 $CBFE = '1ST'
|
|
ldx playsFirstFlag ; 15F3 $0BA1 non-zero = this machine has first play this game
|
|
bne L_15FC ; 15F6 branch if we play first - keep the '1ST' pointer
|
|
lda #$01 ; 15F8 low byte of '2ND'
|
|
ldy #$CC ; 15FA $CC01 = '2ND'
|
|
L_15FC:
|
|
jsr printString ; 15FC print '1ST' or '2ND'
|
|
lda #$F9 ; 15FF low byte of ' PLAY'
|
|
ldy #$CB ; 1601 $CBF9 = ' PLAY'
|
|
jsr printString ; 1603 print ' PLAY'
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Owner-name line plus the column of STATS row labels, then the CUSTOM rules summary.
|
|
; ----------------------------------------------------------------------
|
|
L_1606:
|
|
jsr printNewline ; 1606 carriage return: column 0 of the next panel row
|
|
lda #$0A ; 1609 label block starts at screen column 10
|
|
sta textWindowLeft ; 160B move the window left edge
|
|
lda #$03 ; 160D indent three more columns (absolute column 13)
|
|
sta textCursorCol ; 160F cursor column relative to the window
|
|
jsr updateCursorPointers ; 1611 recompute the cell pointers for the new window/cursor
|
|
dec textColour ; 1614 $6F -> $6E: light-blue body text instead of light grey
|
|
lda #$7F ; 1616 low byte of ownerNameString
|
|
ldy #$0B ; 1618 $0B7F = the 21-character owner/player line built at run time by buildOwnerNameString
|
|
jsr printString ; 161A print it; printString leaves zp_A2/zp_A3 just past the terminator
|
|
lda #$AC ; 161D low byte of statsTabLabels
|
|
ldy #$86 ; 161F $86AC lives in the resident $6F00 overlay B (comcenScreens6F00)
|
|
jsr printString ; 1621 one string with embedded CRs draws GRUNTS/RIDERS/BOOMERS/SPIES/DRONES/MISSILES/COMSPEED/TER PTS/RECYCLER/MAP ID
|
|
lda gameTypeOptions ; 1624 settings byte again
|
|
and #$40 ; 1627 bit 6 = CUSTOM rules
|
|
beq L_1639 ; 1629 standard rules - nothing more to print
|
|
lda #$08 ; 162B custom summary starts at screen column 8
|
|
sta textWindowLeft ; 162D move the window left edge once more
|
|
jsr printNewline ; 162F carriage return onto the summary row
|
|
lda #$36 ; 1632 low byte of optionSummaryTemplate
|
|
ldy #$06 ; 1634 $0636 = ' MENUS xxxx DAMAGE xxx.' - overlay B has already patched the xxxx/xxx fields in place
|
|
jsr printString ; 1636 print the rules summary
|
|
L_1639:
|
|
rts ; 1639 done - back to mainMenuLoop
|
|
|
|
|
|
; tabColumnTable - screen column of the top-left cell of each console tab box, indexed by tab number
|
|
; (0 STATS, 1 REPAIR, 2 MISC). Each box is 2x2 character cells at row 1. Read by
|
|
; setTabNormalColour/setTabHighlightColour.
|
|
tabColumnTable:
|
|
.byte $0B,$13,$1B ; 163A ... STATS at column $0B, REPAIR at $13, MISC at $1B
|
|
|
|
; tabColourTable - normal screen-RAM colour byte of each tab box (upper nibble = multicolour pair 01,
|
|
; lower nibble = pair 10). One entry per tab 0..2.
|
|
tabColourTable:
|
|
.byte $05,$06,$02 ; 163D ... STATS black/green $05, REPAIR black/blue $06, MISC black/red $02
|
|
|
|
; tabHighlightColourTable - bright variant of tabColourTable used while a tab is selected and
|
|
; blinking; same three-entry layout. A zero entry would make setTabHighlightColour fall through to the
|
|
; normal colour, but none is zero.
|
|
tabHighlightColourTable:
|
|
.byte $0D,$0E,$0A ; 1640 ... STATS black/light green $0D, REPAIR black/light blue $0E, MISC black/light red $0A
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawMenuTabBar - Draws the three console tabs of screen 3: prints the labels STATS, REPAIR and MISC
|
|
; on row 3 starting at column 10, each in its own colour, then paints the 2x2 tab box behind each of
|
|
; them in its normal colour, and finally resets the bitmap/colour draw modes to 'replace'.
|
|
; In: nothing; uses the character-block at $9804 and the three colour/column tables at $163A-$1642
|
|
; Out: labels and tab boxes drawn; zp_21 used as the 2..0 tab counter (setTabNormalColour clobbers X);
|
|
; text window left as the full screen, zp_3D = $C2, zp_3F/zp_40 after 'MISC'
|
|
; Called from: showMainMenu at $155C, when the STATS/REPAIR/MISC screen is built
|
|
; Print the three tab labels on row 3, two/three blanks apart.
|
|
; ----------------------------------------------------------------------
|
|
drawMenuTabBar:
|
|
lda #$CD ; 1643 colour byte $CD: light-green text, highlight EOR mask $C - matches the green STATS tab
|
|
sta textColour ; 1645 set the label colour
|
|
jsr setFullScreenWindow ; 1647 print over the whole screen: left 0, top 0, bottom $19, width $28
|
|
lda #$0A ; 164A labels start at column 10
|
|
sta textCursorCol ; 164C cursor column (window left is 0 here, so this is absolute)
|
|
lda #$03 ; 164E tab labels sit on screen row 3
|
|
sta textCursorRow ; 1650 absolute cursor row
|
|
jsr updateCursorPointers ; 1652 recompute the cell pointers
|
|
lda #$70 ; 1655 low byte of 'STATS'
|
|
ldy #$CB ; 1657 $CB70 = 'STATS'
|
|
jsr printString ; 1659 print it
|
|
lda #$02 ; 165C two blanks...
|
|
jsr bootReceiveSectorData ; 165E ...between STATS and REPAIR (also textEngineC000:printFillChars)
|
|
lda #$C6 ; 1661 colour byte $C6: blue text, matching the blue REPAIR tab
|
|
sta textColour ; 1663 switch colour before the next label
|
|
lda #$75 ; 1665 low byte of 'REPAIR'
|
|
ldy #$CB ; 1667 $CB75 = 'REPAIR'
|
|
jsr printString ; 1669 print it
|
|
lda #$03 ; 166C three blanks...
|
|
jsr bootReceiveSectorData ; 166E ...between REPAIR and MISC (also textEngineC000:printFillChars)
|
|
lda #$C2 ; 1671 colour byte $C2: red text, matching the red MISC tab
|
|
sta textColour ; 1673 switch colour again
|
|
lda #$6C ; 1675 low byte of 'MISC'
|
|
ldy #$CB ; 1677 $CB6C = 'MISC'
|
|
jsr printString ; 1679 print it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Paint the three tab boxes, from MISC (2) down to STATS (0). The loop counter has to live in zero
|
|
; page because setTabNormalColour reloads X for fillScreenColourRect.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$02 ; 167C start with tab 2 = MISC
|
|
stx scratch21 ; 167E keep the tab index in zp_21 across the call
|
|
L_1680:
|
|
jsr setTabNormalColour ; 1680 draw tab X's box shape and fill it with its normal colour
|
|
dec scratch21 ; 1683 next tab down
|
|
ldx scratch21 ; 1685 reload the index that setTabNormalColour destroyed
|
|
bpl L_1680 ; 1687 loop while the index is still 0..2
|
|
jmp resetDrawModes ; 1689 restore bitmap and colour draw mode 0 (replace) and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setTabHighlightColour - Redraws console tab X in its bright colour: looks the colour up in
|
|
; tabHighlightColourTable and joins setTabNormalColour's common tail. A zero table entry would fall
|
|
; through to the normal colour, but all three entries are non-zero, so the BNE is always taken.
|
|
; In: X = tab index 0..2
|
|
; Out: the tab's 2x2 box redrawn and recoloured; see setTabNormalColour for the details
|
|
; Called from: mainMenuLoop at $1478 (fire pressed on the tab) and $14C5 (tab changed), and
|
|
; blinkSelectedTab at $16D0
|
|
; ----------------------------------------------------------------------
|
|
setTabHighlightColour:
|
|
lda tabHighlightColourTable,x; 168C bright colour byte of tab X ($0D/$0E/$0A)
|
|
bne L_1694 ; 168F always taken; a zero entry would mean 'no bright colour, use the normal one'
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setTabNormalColour - Redraws console tab X: stores the chosen colour and the tab index into the two
|
|
; self-modified operands, sets the character-block origin to row 1 / tabColumnTable[X], draws the 2x2
|
|
; tab-box shape from $9804 and then fills those 2x2 screen-RAM colour cells with the tab colour.
|
|
; In: X = tab index 0..2; tabColumnTable, tabColourTable/tabHighlightColourTable
|
|
; Out: D_2A2D = 1 (row), D_2A2E = tab column, D_9221/D_9222 = 2x2 (set by drawCharBlock), four bitmap
|
|
; cells and four screen-RAM colour bytes updated; A/X/Y clobbered; patches the operands at $16AD
|
|
; and $16AF
|
|
; Called from: mainMenuLoop at $14A1, drawMenuTabBar's loop at $1680, blinkSelectedTab at $16D3, and
|
|
; by falling through from setTabHighlightColour
|
|
; ----------------------------------------------------------------------
|
|
setTabNormalColour:
|
|
lda tabColourTable,x ; 1691 normal colour byte of tab X ($05/$06/$02)
|
|
L_1694:
|
|
sta L_16AE+1 ; 1694 self-modifying code: the colour becomes the LDA #imm operand at $16AF
|
|
stx L_16AC+1 ; 1697 self-modifying code: the tab index becomes the LDX #imm operand at $16AD - dead, see $16AC
|
|
lda #$01 ; 169A tab boxes sit on character row 1
|
|
sta windowOriginRow ; 169C block/fill origin row for drawCharBlock and fillScreenColourRect
|
|
lda tabColumnTable,x ; 169F screen column of this tab ($0B, $13 or $1B)
|
|
sta windowOriginCol ; 16A2 block/fill origin column
|
|
lda #$04 ; 16A5 low byte of the tab-box shape
|
|
ldy #$98 ; 16A7 $9804 = character block: width 2, height 2, glyphs $45,$46,$47,$48
|
|
jsr drawCharBlock ; 16A9 draw the 2x2 box into the bitmap at (row 1, tab column)
|
|
L_16AC:
|
|
ldx #$FF ; 16AC operand written at $1697 = the tab index; dead code - X is reloaded two instructions later
|
|
L_16AE:
|
|
lda #$FF ; 16AE operand written at $1694 = the tab colour byte
|
|
ldx #$02 ; 16B0 fill width: 2 character cells
|
|
ldy #$02 ; 16B2 fill height: 2 character cells
|
|
jmp fillScreenColourRect ; 16B4 paint the 2x2 colour cells and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; blinkSelectedTab - Called once per pass of mainMenuLoop. It does nothing until the IRQ frame
|
|
; countdown zp_6E reaches 0; then it reloads it with 10 frames, flips blinkPhase and repaints the
|
|
; currently selected tab in either its bright or its normal colour, so the open tab blinks about three
|
|
; times a second.
|
|
; In: zp_6E menuBlinkTimer, menuTabIndex $90F9 (selected tab), blinkPhase $1889
|
|
; Out: zp_6E = 10, blinkPhase toggled, the selected tab recoloured
|
|
; Called from: mainMenuLoop at $143C
|
|
; ----------------------------------------------------------------------
|
|
blinkSelectedTab:
|
|
lda menuBlinkTimer ; 16B7 IRQ frame countdown shared by the tab blink and the menu-row blink
|
|
beq L_16BC ; 16B9 branch when the countdown has expired
|
|
rts ; 16BB still counting down - leave the tab as it is
|
|
|
|
L_16BC:
|
|
lda #$0A ; 16BC 10 frames per blink phase
|
|
sta menuBlinkTimer ; 16BE restart the countdown (the raster IRQ decrements it)
|
|
ldx menuTabIndex ; 16C0 $90F9 = tab currently open: 0 STATS, 1 REPAIR, 2 MISC
|
|
bpl L_16C6 ; 16C3 branch if a tab is selected
|
|
rts ; 16C5 negative index = no tab open, nothing to blink
|
|
|
|
L_16C6:
|
|
lda blinkPhase ; 16C6 current blink phase, 0 or 1
|
|
eor #$01 ; 16C9 flip it
|
|
sta blinkPhase ; 16CB store the new phase (also read by the marker blink)
|
|
beq L_16D3 ; 16CE phase 0 - draw the tab in its normal colour
|
|
jmp setTabHighlightColour ; 16D0 phase 1 - draw it bright
|
|
|
|
L_16D3:
|
|
jmp setTabNormalColour ; 16D3 tail call: normal colour
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printEnergyLabel - Prints the fixed word 'ENERGY' of the REPAIR tab at window column $1C, row 8
|
|
; (absolute column 30 inside the menu panel whose left edge is 2), in the current menu text colour.
|
|
; In: none; setMenuTextColour picks $61 or $21 depending on the comcen stun flag
|
|
; Out: zp_3D, zp_3F, zp_40 set; the label drawn
|
|
; Called from: drawRepairTab at $17BE
|
|
; ----------------------------------------------------------------------
|
|
printEnergyLabel:
|
|
jsr setMenuTextColour ; 16D6 white text, or white on the red panel while the comcen is stunned
|
|
lda #$08 ; 16D9 row 8 of the screen
|
|
sta textCursorRow ; 16DB absolute cursor row
|
|
lda #$1C ; 16DD column $1C relative to the panel's left edge (2) = screen column 30
|
|
sta textCursorCol ; 16DF cursor column
|
|
jsr updateCursorPointers ; 16E1 recompute the cell pointers
|
|
lda #$B8 ; 16E4 low byte of 'ENERGY'
|
|
ldy #$CB ; 16E6 $CBB8 = 'ENERGY'
|
|
jmp printString ; 16E8 print it and return to drawRepairTab
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setMenuTextColour - Chooses the text colour byte for the console menu screen: normally $61 (white
|
|
; text, highlight EOR mask 6 matching the blue panel), but $21 while the REPAIR tab is open and our
|
|
; own comcen is stunned, because drawRepairTab then paints the panel red instead of blue.
|
|
; In: comcenStunStatus $922A (bit 7 = stunned), menuTabIndex $90F9
|
|
; Out: zp_3D textColour
|
|
; Called from: setMenuTextWindow $1575, printEnergyLabel $16D6, printEnergyValue $1702,
|
|
; setRepairMessageWindow $19E4
|
|
; ----------------------------------------------------------------------
|
|
setMenuTextColour:
|
|
lda #$61 ; 16EB default: white text on the blue panel, EOR mask 6 for the row highlight
|
|
ldy comcenStunStatus ; 16ED $922A bit 7 = our comcen is stunned (bits 0-3 hold the stun counter)
|
|
bpl L_16FB ; 16F0 not stunned - keep $61
|
|
ldy menuTabIndex ; 16F2 which tab is open?
|
|
cpy #$01 ; 16F5 tab 1 = REPAIR
|
|
bne L_16FB ; 16F7 the other tabs keep the blue-panel colour
|
|
lda #$21 ; 16F9 REPAIR tab while stunned: white text, EOR mask 2 for the red panel
|
|
L_16FB:
|
|
sta textColour ; 16FB store the chosen colour
|
|
rts ; 16FD return
|
|
|
|
|
|
; lastEnergyShown - the raw comcen energy value (0..50) that printEnergyValue last drew, so the
|
|
; reading is only redrawn when it actually changes. drawRepairTab presets it to $FF to force the first
|
|
; print.
|
|
lastEnergyShown:
|
|
.byte $00 ; 16FE . on disk 0; drawRepairTab writes $FF to force the first print
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printEnergyValue - Prints the comcen's energy as a percentage at column $22, row 9 of the REPAIR
|
|
; tab, right-aligned in a three-character field, but only when the value has changed since the last
|
|
; call. It then classifies the same value into comcenSystemStatus[0], the ENERGY entry of the four
|
|
; comcen systems: 0 = OK at 100%, $40 = degraded from 21% up, $80 = critical below that.
|
|
; In: comcenUnit $9236 (49 or 99), unitEnergyTable $F9C4 (bits 0-5 = 0..50 energy), lastEnergyShown
|
|
; $16FE
|
|
; Out: lastEnergyShown updated, comcenSystemStatus $9241 updated, up to three characters drawn; the
|
|
; caller's text window is saved to $92B6 and restored
|
|
; Called from: mainMenuLoop at $1453 (every pass while the REPAIR tab is open), drawRepairTab at
|
|
; $17C1, drawSystemMarker at $18A0
|
|
; Print the percentage, padded with leading blanks so it always occupies three cells.
|
|
; ----------------------------------------------------------------------
|
|
printEnergyValue:
|
|
jsr saveTextWindow ; 16FF save the caller's 12-byte text-window state to $92B6
|
|
jsr setMenuTextColour ; 1702 menu text colour ($61, or $21 while stunned)
|
|
lda #$09 ; 1705 the reading sits on row 9
|
|
sta textCursorRow ; 1707 absolute cursor row
|
|
lda #$22 ; 1709 column $22 relative to the panel left edge (2) = screen column 36
|
|
sta textCursorCol ; 170B cursor column
|
|
jsr updateCursorPointers ; 170D recompute the cell pointers
|
|
ldy comcenUnit ; 1710 our own comcen: unit 49 (side 0) or 99 (side 1)
|
|
lda unitEnergyTable,y ; 1713 energy array of the unit records, $64 apart from $F640
|
|
and #$3F ; 1716 bits 0-5 hold 0..50; the upper bits are other unit flags
|
|
cmp lastEnergyShown ; 1718 compare with the value already on screen
|
|
beq L_173C ; 171B unchanged - skip the (slow) redraw
|
|
sta lastEnergyShown ; 171D remember the new raw value
|
|
asl a ; 1720 x2 turns 0..50 into a 0..100 percentage
|
|
cmp #$64 ; 1721 100?
|
|
bcs L_1737 ; 1723 three digits - no padding
|
|
pha ; 1725 keep the percentage
|
|
lda #$01 ; 1726 one blank...
|
|
jsr bootReceiveSectorData ; 1728 ...so a two-digit value is right-aligned (also textEngineC000:printFillChars)
|
|
pla ; 172B get the percentage back
|
|
cmp #$0A ; 172C 10?
|
|
bcs L_1737 ; 172E two digits - one blank was enough
|
|
pha ; 1730 keep the percentage
|
|
lda #$01 ; 1731 a second blank...
|
|
jsr bootReceiveSectorData ; 1733 ...so a one-digit value is right-aligned too (also textEngineC000:printFillChars)
|
|
pla ; 1736 get the percentage back
|
|
L_1737:
|
|
ldy #$00 ; 1737 printDecimal wants a 24-bit value: middle byte 0 (zp_7E is already 0)
|
|
jsr printDecimal ; 1739 print it without leading zeros
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Recompute the ENERGY entry of comcenSystemStatus from the same reading. This runs even when the
|
|
; printing was skipped, so drawSystemMarker always sees a fresh status.
|
|
; ----------------------------------------------------------------------
|
|
L_173C:
|
|
jsr restoreTextWindow ; 173C put the caller's text window back
|
|
ldy comcenUnit ; 173F comcen unit index again
|
|
lda unitEnergyTable,y ; 1742 its energy byte
|
|
and #$3F ; 1745 0..50
|
|
asl a ; 1747 0..100 percent
|
|
ldy #$00 ; 1748 assume status 0 = system OK
|
|
cmp #$64 ; 174A exactly 100%?
|
|
beq L_1756 ; 174C yes - the energy system counts as undamaged
|
|
ldy #$40 ; 174E $40 = degraded (yellow marker)
|
|
cmp #$15 ; 1750 21%?
|
|
bcs L_1756 ; 1752 21% or more - degraded
|
|
ldy #$80 ; 1754 $80 = critical/damaged (red marker)
|
|
L_1756:
|
|
sty comcenSystemStatus ; 1756 store into comcenSystemStatus[0], the ENERGY system
|
|
rts ; 1759 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawRepairTab - Builds the whole REPAIR tab: clears the panel in blue ($66) or, if our comcen is
|
|
; stunned, red ($22); unpacks the comcen schematic picture into the bitmap; paints a 20x10 colour
|
|
; block behind it; resets the blink and energy bookkeeping; draws the four system markers (only when
|
|
; not stunned); prints the ENERGY label and value; and, when stunned, prints 'COMCEN STUNNED!'. The
|
|
; stun flag is copied to $1413 so mainMenuLoop can notice a change and redraw.
|
|
; In: comcenStunStatus $922A, comcenUnit $9236, unitEnergyTable $F9C4
|
|
; Out: $1413 = stun flag snapshot, zp_6E = 0, showOkMarkers = 0, blinkPhase = 0 then 1,
|
|
; lastEnergyShown = $FF, comcenEnergySnapshot = current energy, menuSelectedItem $91D5 left at 4
|
|
; (the (EXIT) row of the REPAIR WHICH menu), the panel redrawn
|
|
; Called from: mainMenuLoop at $1450 (stun state changed) and $14DD (REPAIR tab selected)
|
|
; Clear the panel and draw the comcen schematic with its backing colour block.
|
|
; ----------------------------------------------------------------------
|
|
drawRepairTab:
|
|
ldy #$66 ; 175A assume panel colour $66: blue text/EOR mask 6
|
|
lda comcenStunStatus ; 175C bit 7 = our comcen is stunned
|
|
sta repairTabStunCopy ; 175F snapshot for mainMenuLoop's 'has the stun state changed?' test at $1449
|
|
bpl L_1766 ; 1762 not stunned - keep the blue panel
|
|
ldy #$22 ; 1764 stunned: colour $22, a red panel
|
|
L_1766:
|
|
sty textColour ; 1766 set the panel text colour
|
|
jsr bootDriveDataBit ; 1768 blank the whole menu window in that colour (also textEngineC000:clearTextWindow)
|
|
jsr drawComcenSchematic ; 176B $8632 in overlay B: RLE-unpacks the comcen side view into the bitmap at row 10, column 13 (the same address holds unrelated code in overlay A) (also mapGenerator6F00:printOnOffMenuAndApplyBit)
|
|
lda #$0D ; 176E colour-block origin column 13
|
|
sta windowOriginCol ; 1770 fillScreenColourRect origin column
|
|
lda #$0A ; 1773 colour-block origin row 10
|
|
sta windowOriginRow ; 1775 fillScreenColourRect origin row
|
|
ldx #$14 ; 1778 20 cells wide - the width of the schematic
|
|
ldy #$0A ; 177A 10 cells tall
|
|
lda textColour ; 177C take the panel colour...
|
|
and #$0F ; 177E ...keep its low nibble (6 blue, or 2 red)...
|
|
ora #$10 ; 1780 ...and put white (1) in the upper nibble: $16 normally, $12 when stunned
|
|
jsr fillScreenColourRect ; 1782 paint the schematic's background colour cells
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Reset the blink/energy bookkeeping the marker code depends on.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$00 ; 1785 zero for the three flags below
|
|
stx menuBlinkTimer ; 1787 blink countdown expired, so the next blink happens at once
|
|
stx showOkMarkers ; 1789 only the REPAIR WHICH menu wants markers on healthy systems
|
|
stx blinkPhase ; 178C blink phase 0
|
|
ldx #$FF ; 178F sentinel value
|
|
stx lastEnergyShown ; 1791 force printEnergyValue to actually draw the reading
|
|
ldx comcenUnit ; 1794 our comcen's unit index
|
|
lda unitEnergyTable,x ; 1797 its energy byte
|
|
and #$3F ; 179A 0..50
|
|
sta comcenEnergySnapshot ; 179C remember it; attemptSystemRepair works from this snapshot
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Draw the four system markers, unless the comcen is stunned (then it has no readable status at all).
|
|
; $91D5 doubles as the 3..0 loop counter.
|
|
; ----------------------------------------------------------------------
|
|
lda comcenStunStatus ; 179F stun flag once more
|
|
bmi L_17BE ; 17A2 stunned - no markers, jump straight to the ENERGY readout
|
|
ldy #$03 ; 17A4 start at system 3 (DRONE)
|
|
sty menuSelectedItem ; 17A6 menuSelectedItem is reused as the loop counter here
|
|
L_17A9:
|
|
jsr drawSystemMarker ; 17A9 draw the marker of system Y
|
|
dec menuSelectedItem ; 17AC next system down
|
|
ldy menuSelectedItem ; 17AF reload the counter into Y (drawSystemMarker clobbers it)
|
|
bpl L_17A9 ; 17B2 loop over systems 3,2,1,0
|
|
lda #$01 ; 17B4 blink phase 1 = 'marker currently drawn'
|
|
sta blinkPhase ; 17B6 so the first toggle erases it
|
|
lda #$04 ; 17B9 menu row 4 of the REPAIR WHICH menu = (EXIT)
|
|
sta menuSelectedItem ; 17BB leave the selection on (EXIT)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ENERGY label and reading, plus the stun banner.
|
|
; ----------------------------------------------------------------------
|
|
L_17BE:
|
|
jsr printEnergyLabel ; 17BE print the word ENERGY at column 30, row 8
|
|
jsr printEnergyValue ; 17C1 print the percentage at column 36, row 9 and refresh comcenSystemStatus[0]
|
|
lda comcenStunStatus ; 17C4 stunned?
|
|
bpl L_17D7 ; 17C7 no - done
|
|
lda #$05 ; 17C9 banner starts at column 5 of the panel (screen column 7)
|
|
sta textCursorCol ; 17CB cursor column
|
|
jsr updateCursorPointers ; 17CD recompute the cell pointers (the row is wherever printEnergyValue left it)
|
|
lda #$42 ; 17D0 low byte of 'COMCEN STUNNED!'
|
|
ldy #$87 ; 17D2 $8742 in overlay B (msgComcenStunned)
|
|
jsr printString ; 17D4 print the banner
|
|
L_17D7:
|
|
rts ; 17D7 return
|
|
|
|
|
|
; comcenEnergySnapshot - the comcen's raw energy (0..50) as of the last REPAIR-tab redraw or repair
|
|
; attempt. attemptSystemRepair sizes the repair from it and adds the repaired amount back locally,
|
|
; because the real unitEnergyTable entry only changes once the $9A command has travelled through the
|
|
; lock-step exchange.
|
|
comcenEnergySnapshot:
|
|
.byte $00 ; 17D8 . 0..50, refreshed by drawRepairTab and attemptSystemRepair
|
|
|
|
; repairAmount - energy points granted by the last successful ENERGY repair, computed as (50 -
|
|
; comcenEnergySnapshot)/8 + 1 and sent as the parameter of command $9A.
|
|
repairAmount:
|
|
.byte $00 ; 17D9 . parameter of the $9A cmdAddEnergy command
|
|
|
|
; ----------------------------------------------------------------------
|
|
; repairTabAction - Fire-button handler of the REPAIR tab (entry 1 of menuItemHandlerTable at $1535,
|
|
; reached through the self-modified JSR at $148D). It does nothing unless the DAMAGE option is on, the
|
|
; game is not a film playback and a game is actually running. If the comcen is stunned it runs a
|
|
; single stun-recovery attempt: message, sound, 200 frames of real-time battle, then - only while the
|
|
; stun counter is below 8 - a 1-in-8 chance of queueing command $9E, which clears the stun on both
|
|
; machines. If the comcen is not stunned it halts the comcen if it is still moving or under group
|
|
; orders, blanks the four markers and opens the REPAIR WHICH menu.
|
|
; In: gameTypeOptions $0BA3 bit 4 (DAMAGE), filmPlaybackMode $0B9B bit 7, gameEndReason $0BA8,
|
|
; comcenStunStatus $922A, comcenUnit $9236, unitDisplayFlagsTable $FB54,
|
|
; unitColTable/unitRowTable, nextGameRandom
|
|
; Out: command $9E or $80 queued into the outgoing ring; sounds $12/$13/$18; the battle advanced by
|
|
; runGameFrames; blinkPhase, showOkMarkers, $1412 (3..0 loop counter) and the four markers
|
|
; updated; tail-calls snapshotSetupId so the menu's 'anything changed?' test is re-armed
|
|
; Called from: mainMenuLoop through menuItemHandlerTable[1] at $148D
|
|
; Guards: repairs need the DAMAGE option, a live game and player control.
|
|
; ----------------------------------------------------------------------
|
|
repairTabAction:
|
|
lda gameTypeOptions ; 17DA game settings byte
|
|
and #$10 ; 17DD bit 4 = the DAMAGE option is on
|
|
beq L_1822 ; 17DF no damage model - the REPAIR tab is decoration only
|
|
lda filmPlaybackMode ; 17E1 bit 7 = a game film is playing back
|
|
bmi L_1822 ; 17E4 during playback the player may not issue commands
|
|
lda gameEndReason ; 17E6 0 while a game is running
|
|
bne L_1822 ; 17E9 no game in progress - nothing to repair
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Stunned comcen: one attempt at shaking the stun off.
|
|
; ----------------------------------------------------------------------
|
|
lda comcenStunStatus ; 17EB bit 7 = comcen stunned
|
|
bpl L_1823 ; 17EE not stunned - go and open the REPAIR WHICH menu
|
|
jsr printAttemptingRepair ; 17F0 print 'ATTEMPTING REPAIR...' on the panel's message row
|
|
lda #$18 ; 17F3 sound $18 = the repair work noise
|
|
jsr playSound ; 17F5 start it
|
|
lda #$C8 ; 17F8 200 frames...
|
|
jsr runGameFrames ; 17FA ...of real-time battle while the crew works
|
|
jsr clearRepairMessage ; 17FD erase the message row again
|
|
ldy #$12 ; 1800 assume sound $12 = failure
|
|
lda comcenStunStatus ; 1802 stun status byte
|
|
and #$0F ; 1805 bits 0-3 = how badly stunned (counts down elsewhere)
|
|
cmp #$08 ; 1807 8 or more?
|
|
bcs L_1819 ; 1809 too badly stunned - the attempt cannot succeed
|
|
jsr nextGameRandom ; 180B shared RNG so both machines and a replayed film agree
|
|
and #$07 ; 180E one chance in eight
|
|
bne L_1819 ; 1810 not this time - keep the failure sound
|
|
lda #$9E ; 1812 $9E = cmdComcenRepairedOrSetupReady
|
|
jsr queueCmdBytePending ; 1814 queue the 1-byte command; both machines clear the stun counter
|
|
ldy #$13 ; 1817 sound $13 = success
|
|
L_1819:
|
|
tya ; 1819 move the chosen sound id into A
|
|
jsr playSound ; 181A play it
|
|
lda #$14 ; 181D 20 more frames...
|
|
jsr runGameFrames ; 181F ...so the sound is heard before the menu redraws
|
|
L_1822:
|
|
rts ; 1822 return to mainMenuLoop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Not stunned: halt the comcen if necessary, blank the markers and open the 'REPAIR WHICH:' menu. A
|
|
; comcen that is driving or following a group order has to stop before it can be worked on, which is
|
|
; done by ordering it to move to the cell it already occupies - cmdMoveUnit also drops its group
|
|
; membership.
|
|
; ----------------------------------------------------------------------
|
|
L_1823:
|
|
ldy comcenUnit ; 1823 our comcen's unit index
|
|
lda unitDisplayFlagsTable,y ; 1826 $FB54: bit 7 = idle/arrived, bit 6 = acting on a group order
|
|
bpl L_182F ; 1829 bit 7 clear = still moving - it must be halted
|
|
and #$40 ; 182B bit 6 = following a group order
|
|
beq L_1840 ; 182D idle and ungrouped - no halt command needed
|
|
L_182F:
|
|
sty cmdParam0 ; 182F command argument 0 = the unit to move
|
|
lda unitColTable,y ; 1831 current map column of the comcen
|
|
sta cmdParam1 ; 1834 command argument 1 = destination column
|
|
lda unitRowTable,y ; 1836 current map row
|
|
sta cmdParam2 ; 1839 command argument 2 = destination row
|
|
lda #$80 ; 183B $80 = cmdMoveUnit
|
|
jsr queueCmd4 ; 183D queue the 4-byte command: 'move to where you already are' = halt and leave the group
|
|
L_1840:
|
|
ldy #$01 ; 1840 blink phase 1...
|
|
sty blinkPhase ; 1842 ...so the first toggle in the menu erases the marker
|
|
dey ; 1845 Y = 0
|
|
sty showOkMarkers ; 1846 healthy systems stay invisible while the markers are wiped
|
|
ldy #$03 ; 1849 start at system 3
|
|
sty repairLoopCounter ; 184B $1412 is the 3..0 loop counter here (drawRepairTab used $91D5 instead)
|
|
L_184E:
|
|
ldx #$16 ; 184E colour $16 = the schematic background (white/blue)
|
|
jsr eraseSystemMarker ; 1850 blank system Y's 3x3 marker area
|
|
dec repairLoopCounter ; 1853 next system down
|
|
ldy repairLoopCounter ; 1856 reload the counter into Y
|
|
bpl L_184E ; 1859 loop over systems 3,2,1,0
|
|
lda #$01 ; 185B now show markers for healthy systems too...
|
|
sta showOkMarkers ; 185D ...so the player can pick any of the four in the menu
|
|
jsr runRepairWhichMenu ; 1860 $652E: draw and run the 'REPAIR WHICH:' menu (ENERGY/BATTLE/RADAR/DRONE/(EXIT))
|
|
jsr consumeSpaceKey ; 1863 swallow a pending space key so it is not re-processed
|
|
jsr setMenuTextWindow ; 1866 put the menu panel's text window and colour back
|
|
lda #$00 ; 1869 back to 'only damaged systems get a marker'
|
|
sta showOkMarkers ; 186B clear the flag
|
|
ldy #$03 ; 186E start at system 3 again
|
|
sty repairLoopCounter ; 1870 reuse $1412 as the counter
|
|
L_1873:
|
|
jsr drawSystemMarker ; 1873 redraw system Y's marker with its new status
|
|
dec repairLoopCounter ; 1876 next system down
|
|
ldy repairLoopCounter ; 1879 reload the counter
|
|
bpl L_1873 ; 187C loop over systems 3,2,1,0
|
|
jmp snapshotComcenStatus ; 187E re-arm selectDefaultMenuItem's 'has anything changed?' snapshot and return
|
|
|
|
|
|
; systemMarkerColumnTable - screen column of the marker of each comcen system, indexed by system
|
|
; number: 0 ENERGY, 1 BATTLE, 2 RADAR, 3 DRONE. The marker is a plus of five colour cells inside a 3x3
|
|
; block whose top-left corner is this column and systemMarkerRowTable[Y].
|
|
systemMarkerColumnTable:
|
|
.byte $0D,$10,$14,$19 ; 1881 .... ENERGY $0D, BATTLE $10, RADAR $14, DRONE $19
|
|
|
|
; systemMarkerRowTable - screen row of each comcen system marker, same index as
|
|
; systemMarkerColumnTable. All four sit inside the schematic block drawn at row 10.
|
|
systemMarkerRowTable:
|
|
.byte $0E,$0A,$0A,$0C ; 1885 .... ENERGY $0E, BATTLE $0A, RADAR $0A, DRONE $0C
|
|
|
|
; blinkPhase - the shared 0/1 blink phase used by both the console tab blink (blinkSelectedTab) and
|
|
; the comcen system marker blink (toggleSystemMarker). 0 = the item is currently shown in its 'on'
|
|
; state.
|
|
blinkPhase:
|
|
.byte $00 ; 1889 . 0 or 1
|
|
|
|
; showOkMarkers - when non-zero, drawSystemMarker also draws a marker for a system whose status is OK
|
|
; (green) instead of erasing its cells. Set only while the REPAIR WHICH menu is open, so all four
|
|
; systems can be selected.
|
|
showOkMarkers:
|
|
.byte $00 ; 188A . 1 only while the REPAIR WHICH menu is up
|
|
|
|
; ----------------------------------------------------------------------
|
|
; toggleSystemMarker - One blink step for the marker of the comcen system currently highlighted in the
|
|
; REPAIR WHICH menu: flips blinkPhase and then either draws the marker (new phase 0) or erases it with
|
|
; the schematic background colour $16 (new phase 1).
|
|
; In: menuSelectedItem $91D5 = system 0..3, blinkPhase $1889
|
|
; Out: blinkPhase toggled; falls through to drawSystemMarker or jumps to eraseSystemMarker with X =
|
|
; $16
|
|
; Called from: blinkRepairMarker at $19C1, and toggleMenuRowHighlight at $6108 whenever the menu
|
|
; selector is running in repair mode (selectedUnit $90F8 == $FF) on a row below 4, so the marker
|
|
; blinks in step with the highlighted row
|
|
; ----------------------------------------------------------------------
|
|
toggleSystemMarker:
|
|
ldy menuSelectedItem ; 188B the highlighted comcen system, 0 ENERGY .. 3 DRONE
|
|
ldx #$16 ; 188E $16 = white/blue, the schematic background - used if we are erasing
|
|
lda blinkPhase ; 1890 current blink phase
|
|
eor #$01 ; 1893 flip it
|
|
sta blinkPhase ; 1895 store the new phase
|
|
beq drawSystemMarker ; 1898 new phase 0 - draw the marker (falls straight into drawSystemMarker)
|
|
jmp eraseSystemMarker ; 189A new phase 1 - wipe the 3x3 marker block
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawSystemMarker - Draws the status marker of comcen system Y on the REPAIR tab schematic. The
|
|
; marker is a plus made of five 1x1 colour-cell fills around (systemMarkerRowTable[Y],
|
|
; systemMarkerColumnTable[Y]): white/green $15 when the system is OK, white/red $12 when its status
|
|
; byte has bit 7 set (damaged) and white/yellow $17 otherwise ($40 = degraded). A healthy system is
|
|
; wiped instead of drawn unless showOkMarkers is set. Y has to be stashed in a self-modified operand
|
|
; because the energy refresh in the first line destroys it.
|
|
; In: Y = system index 0..3, comcenSystemStatus $9241, showOkMarkers $188A
|
|
; Out: five (or nine, when erasing) screen-RAM colour cells at $8C00 updated; D_2A2A holds the fill
|
|
; colour, D_2A2D/D_2A2E the running row/column; the operand at $18A4 patched; the ENERGY reading
|
|
; refreshed as a side effect
|
|
; Called from: drawRepairTab's loop at $17A9, repairTabAction's redraw loop at $1873, and by falling
|
|
; through from toggleSystemMarker
|
|
; Pick the marker colour from the system's status byte.
|
|
; ----------------------------------------------------------------------
|
|
drawSystemMarker:
|
|
sty L_18A3+1 ; 189D self-modifying code: save Y into the LDY #imm operand at $18A4 - printEnergyValue clobbers Y
|
|
jsr printEnergyValue ; 18A0 refresh the percentage; this also recomputes comcenSystemStatus[0] for the ENERGY system
|
|
L_18A3:
|
|
ldy #$FF ; 18A3 operand written at $189D = the system index
|
|
ldx #$15 ; 18A5 assume $15 = white/green (system OK)
|
|
lda comcenSystemStatus,y ; 18A7 $9241+Y: 0 OK, $40 degraded, $80 damaged
|
|
beq L_18B5 ; 18AA status 0 - keep green
|
|
ldx #$12 ; 18AC $12 = white/red (damaged)
|
|
lda comcenSystemStatus,y ; 18AE look at the status again
|
|
bmi L_18B5 ; 18B1 bit 7 set = damaged - keep red
|
|
ldx #$17 ; 18B3 $17 = white/yellow (degraded, i.e. $40)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A healthy system normally shows nothing at all; only the REPAIR WHICH menu wants green markers so
|
|
; that every system can be picked.
|
|
; ----------------------------------------------------------------------
|
|
L_18B5:
|
|
cpx #$15 ; 18B5 is the colour the 'OK' green?
|
|
bne L_18C3 ; 18B7 no - draw the marker
|
|
lda showOkMarkers ; 18B9 are healthy systems supposed to show a marker?
|
|
bne L_18C3 ; 18BC yes (REPAIR WHICH menu open) - draw it
|
|
ldx #$16 ; 18BE $16 = schematic background
|
|
jmp eraseSystemMarker ; 18C0 wipe the whole 3x3 block instead and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Paint the plus: (r,c+1), (r+2,c+1), (r+1,c), (r+1,c+1), (r+1,c+2). D_2A2D/D_2A2E are stepped between
|
|
; the five 1x1 fills; fillScreenColourRect reloads D_2A2A from A, so the colour is fetched again
|
|
; before each fill.
|
|
; ----------------------------------------------------------------------
|
|
L_18C3:
|
|
stx drawColourByte ; 18C3 keep the chosen colour byte in the fill-colour slot
|
|
lda systemMarkerColumnTable,y; 18C6 screen column of this system's marker
|
|
sta windowOriginCol ; 18C9 fill origin column
|
|
lda systemMarkerRowTable,y ; 18CC screen row of this system's marker
|
|
sta windowOriginRow ; 18CF fill origin row
|
|
inc windowOriginCol ; 18D2 step right one cell: top arm of the plus
|
|
lda drawColourByte ; 18D5 marker colour
|
|
ldx #$01 ; 18D8 1 cell wide
|
|
ldy #$01 ; 18DA 1 cell tall
|
|
jsr fillScreenColourRect ; 18DC paint (row, col+1)
|
|
inc windowOriginRow ; 18DF down one row...
|
|
inc windowOriginRow ; 18E2 ...and one more: bottom arm
|
|
lda drawColourByte ; 18E5 marker colour
|
|
ldx #$01 ; 18E8 1 cell wide
|
|
ldy #$01 ; 18EA 1 cell tall
|
|
jsr fillScreenColourRect ; 18EC paint (row+2, col+1)
|
|
dec windowOriginRow ; 18EF back up to the middle row
|
|
dec windowOriginCol ; 18F2 back left to the original column
|
|
lda drawColourByte ; 18F5 marker colour
|
|
ldx #$01 ; 18F8 1 cell wide
|
|
ldy #$01 ; 18FA 1 cell tall
|
|
jsr fillScreenColourRect ; 18FC paint (row+1, col) - left arm
|
|
inc windowOriginCol ; 18FF step right
|
|
lda drawColourByte ; 1902 marker colour
|
|
ldx #$01 ; 1905 1 cell wide
|
|
ldy #$01 ; 1907 1 cell tall
|
|
jsr fillScreenColourRect ; 1909 paint (row+1, col+1) - centre
|
|
inc windowOriginCol ; 190C step right again
|
|
lda drawColourByte ; 190F marker colour
|
|
ldx #$01 ; 1912 1 cell wide
|
|
ldy #$01 ; 1914 1 cell tall
|
|
jmp fillScreenColourRect ; 1916 paint (row+1, col+2) - right arm, and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; eraseSystemMarker - Fills the whole 3x3 colour-cell block of system Y's marker with the colour byte
|
|
; in X. Every caller passes $16, the white/blue background the schematic is drawn on, so this blanks
|
|
; the marker.
|
|
; In: Y = system index 0..3, X = colour byte (always $16)
|
|
; Out: D_2A2D/D_2A2E set to the marker origin, nine screen-RAM colour bytes overwritten
|
|
; Called from: repairTabAction's wipe loop at $1850, toggleSystemMarker at $189A (blink off),
|
|
; drawSystemMarker at $18C0 (healthy system with showOkMarkers clear)
|
|
; ----------------------------------------------------------------------
|
|
eraseSystemMarker:
|
|
lda systemMarkerColumnTable,y; 1919 screen column of this system's marker
|
|
sta windowOriginCol ; 191C fill origin column
|
|
lda systemMarkerRowTable,y ; 191F screen row of this system's marker
|
|
sta windowOriginRow ; 1922 fill origin row
|
|
txa ; 1925 the colour byte arrives in X, fillScreenColourRect wants it in A
|
|
ldx #$03 ; 1926 3 cells wide
|
|
ldy #$03 ; 1928 3 cells tall - covers the whole plus
|
|
jmp fillScreenColourRect ; 192A paint the block and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendEnergyRepairCommand - Queues the 3-byte command $9A (cmdAddEnergy) with the comcen's unit index
|
|
; and the repaired amount, so that both machines add the same energy to that unit. This is the only
|
|
; part of a comcen repair that travels over the link - the status of systems 1-3 is purely local
|
|
; cosmetics.
|
|
; In: comcenUnit $9236, repairAmount $17D9
|
|
; Out: zp_74 = unit, zp_75 = amount, the command written into the outgoing ring and pendingOwnCmdCount
|
|
; $929F bumped
|
|
; Called from: attemptSystemRepair at $196C
|
|
; ----------------------------------------------------------------------
|
|
sendEnergyRepairCommand:
|
|
lda comcenUnit ; 192D our comcen (unit 49 or 99)
|
|
sta cmdParam1 ; 1930 command argument 1 = unit index
|
|
lda repairAmount ; 1932 energy points won by this repair
|
|
sta cmdParam2 ; 1935 command argument 2 = amount
|
|
lda #$9A ; 1937 $9A = cmdAddEnergy
|
|
jmp queueCmd3 ; 1939 queue the 3-byte command and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; attemptSystemRepair - Resolves one repair attempt on the comcen system highlighted in the REPAIR
|
|
; WHICH menu. It shows the message, plays the work sound and rolls the shared RNG: the attempt
|
|
; succeeds with probability 1/2 for the ENERGY system and 1/4 for the other three, and only if the
|
|
; system is not already OK. For ENERGY it computes (50 - snapshot)/8 + 1 points, sends them with
|
|
; command $9A and updates the snapshot (capped at 50); for the others it just clears the status byte.
|
|
; Success plays sound $13, failure $12, each surrounded by about a second of marker blinking during
|
|
; which the real-time battle keeps running.
|
|
; In: menuSelectedItem $91D5 = system 0..3, comcenSystemStatus $9241, comcenEnergySnapshot $17D8,
|
|
; comcenUnit $9236, unitEnergyTable $F9C4, nextGameRandom
|
|
; Out: repairAmount, comcenEnergySnapshot, comcenSystemStatus[Y], one $9A command, sounds $12/$13/$18,
|
|
; zp_6E = 0, blinkPhase = 0 and the message row cleared
|
|
; Called from: the REPAIR WHICH menu loop at $6590 (skipped in solo/film mode)
|
|
; Announce the attempt and roll for success.
|
|
; ----------------------------------------------------------------------
|
|
attemptSystemRepair:
|
|
jsr printAttemptingRepair ; 193C print 'ATTEMPTING REPAIR...'
|
|
lda #$18 ; 193F sound $18 = repair work noise
|
|
jsr playSound ; 1941 start it
|
|
ldy menuSelectedItem ; 1944 the system the player picked: 0 ENERGY, 1 BATTLE, 2 RADAR, 3 DRONE
|
|
jsr nextGameRandom ; 1947 shared RNG - both machines and a replayed film draw the same number
|
|
cpy #$00 ; 194A the ENERGY system?
|
|
bne L_1950 ; 194C systems 1-3 keep both random bits
|
|
and #$01 ; 194E ENERGY: one bit only, so a 1-in-2 chance
|
|
L_1950:
|
|
and #$03 ; 1950 1-in-4 for systems 1-3 (already narrowed to 1-in-2 above)
|
|
bne L_1990 ; 1952 non-zero - the attempt failed
|
|
lda comcenSystemStatus,y ; 1954 status of the chosen system
|
|
beq L_1990 ; 1957 already OK - treat it as a failure, there is nothing to repair
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ENERGY: work out how much energy the crew restores and tell the opponent.
|
|
; ----------------------------------------------------------------------
|
|
cpy #$00 ; 1959 the ENERGY system?
|
|
bne L_197F ; 195B systems 1-3 just get their status cleared
|
|
lda #$32 ; 195D 50 = a full comcen energy bar
|
|
sec ; 195F prepare the subtraction
|
|
sbc comcenEnergySnapshot ; 1960 how much energy is missing
|
|
lsr a ; 1963 divide by 8...
|
|
lsr a ; 1964 ...
|
|
lsr a ; 1965 ...so a badly hurt comcen recovers faster
|
|
clc ; 1966 prepare the add
|
|
adc #$01 ; 1967 always at least one point
|
|
sta repairAmount ; 1969 remember what we granted
|
|
jsr sendEnergyRepairCommand ; 196C queue command $9A so both machines credit the energy
|
|
clc ; 196F prepare the add
|
|
lda comcenEnergySnapshot ; 1970 energy before the repair
|
|
adc repairAmount ; 1973 plus the repaired amount
|
|
cmp #$33 ; 1976 51 or more?
|
|
bcc L_197C ; 1978 still below the maximum
|
|
lda #$32 ; 197A clamp at 50
|
|
L_197C:
|
|
sta comcenEnergySnapshot ; 197C update the local snapshot (the real table only changes when $9A executes)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Success: blink, clear the status, chime. Failure joins at $1990.
|
|
; ----------------------------------------------------------------------
|
|
L_197F:
|
|
jsr blinkRepairMarker ; 197F about a second of marker blinking while the battle runs on
|
|
ldy menuSelectedItem ; 1982 the chosen system again
|
|
beq L_198C ; 1985 ENERGY - its status is recomputed by printEnergyValue, not stored here
|
|
lda #$00 ; 1987 0 = system OK
|
|
sta comcenSystemStatus,y ; 1989 mark BATTLE/RADAR/DRONE repaired
|
|
L_198C:
|
|
lda #$13 ; 198C sound $13 = repair succeeded
|
|
bne L_1995 ; 198E always taken
|
|
L_1990:
|
|
jsr blinkRepairMarker ; 1990 failure: the same blink pause so both outcomes take the same time
|
|
lda #$12 ; 1993 sound $12 = repair failed
|
|
L_1995:
|
|
jsr playSound ; 1995 play the outcome sound
|
|
jsr blinkRepairMarker ; 1998 another second of blinking while it sounds
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Re-read the real energy (the $9A command may have been applied in the meantime) and clean up the
|
|
; blink state and the message row.
|
|
; ----------------------------------------------------------------------
|
|
ldy comcenUnit ; 199B our comcen
|
|
lda unitEnergyTable,y ; 199E its energy byte
|
|
and #$3F ; 19A1 0..50
|
|
sta comcenEnergySnapshot ; 19A3 refresh the snapshot from the live table
|
|
lda #$00 ; 19A6 zero...
|
|
sta menuBlinkTimer ; 19A8 ...so the menu-row blink fires again on the next pass
|
|
sta blinkPhase ; 19AA ...and the marker is left in the 'drawn' phase
|
|
jmp clearRepairMessage ; 19AD erase 'ATTEMPTING REPAIR...' and return to the menu loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; blinkRepairMarker - Blinks the marker of the selected comcen system for about one second while a
|
|
; repair attempt resolves. It arms the IRQ frame countdown zp_6F with 60 and toggles the marker every
|
|
; 20 game frames; runGameFrames is used rather than a plain delay so that the real-time battle (and
|
|
; the lock-step command exchange) keeps running. It bails out at once if the game has ended.
|
|
; In: menuSelectedItem $91D5, gameEndCountdown $92C9
|
|
; Out: zp_6F counted down by the raster IRQ, blinkPhase toggled several times, the marker left in
|
|
; whichever phase it ended on, the battle advanced
|
|
; Called from: attemptSystemRepair at $197F, $1990 and $1998
|
|
; ----------------------------------------------------------------------
|
|
blinkRepairMarker:
|
|
lda #$3C ; 19B0 60 frames, about one second
|
|
sta markerBlinkTimer ; 19B2 the raster IRQ counts zp_6F down
|
|
L_19B4:
|
|
lda #$14 ; 19B4 20 frames...
|
|
jsr runGameFrames ; 19B6 ...of real-time battle between toggles
|
|
lda gameEndCountdown ; 19B9 non-zero once the game is over
|
|
bne L_19C8 ; 19BC the game ended while we blinked - stop immediately
|
|
ldy menuSelectedItem ; 19BE the highlighted comcen system
|
|
jsr toggleSystemMarker ; 19C1 flip the marker on or off
|
|
lda markerBlinkTimer ; 19C4 has the one-second countdown expired?
|
|
bne L_19B4 ; 19C6 no - keep blinking
|
|
L_19C8:
|
|
rts ; 19C8 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printAttemptingRepair - Positions the REPAIR panel's own message row (window column 10, screen
|
|
; column 12, screen row 20) and prints 'ATTEMPTING REPAIR...' there, then restores the text window the
|
|
; caller had.
|
|
; In: none
|
|
; Out: 20 characters drawn on the panel's message row; the text window restored from the single save
|
|
; slot $92B6
|
|
; Called from: repairTabAction at $17F0 and attemptSystemRepair at $193C
|
|
; ----------------------------------------------------------------------
|
|
printAttemptingRepair:
|
|
jsr setRepairMessageWindow ; 19C9 save the caller's window and move to column 10, row 20
|
|
lda #$E5 ; 19CC low byte of the message
|
|
ldy #$CB ; 19CE $CBE5 = 'ATTEMPTING REPAIR...'
|
|
jsr printString ; 19D0 print it
|
|
jmp restoreTextWindow ; 19D3 put the caller's text window back and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearRepairMessage - Erases the REPAIR panel's message row by printing 20 copies of the blank glyph
|
|
; zp_3E over it, then restores the caller's text window. (The routine it calls is the $C000 overlay's
|
|
; printFillChars at $C0EF; the listing's bootReceiveSectorData label is a stale leftover from the boot
|
|
; loader that used to live at $C000.)
|
|
; In: zp_3E fill glyph
|
|
; Out: 20 cells of the panel's message row blanked; the text window restored from $92B6
|
|
; Called from: repairTabAction at $17FD and attemptSystemRepair at $19AD
|
|
; ----------------------------------------------------------------------
|
|
clearRepairMessage:
|
|
jsr setRepairMessageWindow ; 19D6 same window and cursor position as printAttemptingRepair
|
|
lda #$14 ; 19D9 20 characters - the length of 'ATTEMPTING REPAIR...'
|
|
jsr bootReceiveSectorData ; 19DB printFillChars ($C0EF): print the blank glyph that many times (also textEngineC000:printFillChars)
|
|
jmp restoreTextWindow ; 19DE put the caller's text window back and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setRepairMessageWindow - Shared prologue of printAttemptingRepair and clearRepairMessage: saves the
|
|
; caller's 12-byte text state, picks the menu text colour, applies the menu panel geometry (left
|
|
; column 2, width $24, rows 7..$16) and places the cursor at window column 10 = screen column 12 of
|
|
; screen row 20.
|
|
; In: comcenStunStatus and menuTabIndex through setMenuTextColour
|
|
; Out: $92B6-$92C1 = saved window, zp_39-zp_40 = menu window, zp_3D = $61 or $21, cursor pointers
|
|
; recomputed
|
|
; Called from: printAttemptingRepair $19C9, clearRepairMessage $19D6
|
|
; ----------------------------------------------------------------------
|
|
setRepairMessageWindow:
|
|
jsr saveTextWindow ; 19E1 copy zp_39-zp_44 to $92B6 (there is only one save slot, so this cannot nest)
|
|
jsr setMenuTextColour ; 19E4 $61, or $21 while the stunned-comcen panel is red
|
|
jsr setMenuWindowBounds ; 19E7 menu panel window: left column 2, width $24, rows 7..$16
|
|
lda #$0A ; 19EA column 10 of the window (screen column 12)
|
|
sta textCursorCol ; 19EC cursor column
|
|
lda #$14 ; 19EE screen row 20 - the panel's message row
|
|
jmp setCursorRow ; 19F0 set the row, recompute the pointers and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printOptionsList - Prints the body of the MISC tab. While a game is running it lists the in-game
|
|
; options ABORT GAME, VOICE PAUSE (only for a modem game that is not a film playback) and ' (EXIT)'.
|
|
; Otherwise it prints the five main-menu lines COMPETE WITH MODEM OPPONENT / PRACTICE WITH SOLO
|
|
; TRAINER / WATCH GAME FILM / SAVE GAME FILM / LOAD GAME FILM, plus DISCONNECT/SET VOICE when the comm
|
|
; module reports the link is up.
|
|
; In: gameEndReason $0BA8, gameEndCountdown $92C9, filmPlaybackMode $0B9B, isSoloTrainer $0BA5,
|
|
; link-active flag $E03B
|
|
; Out: the item text drawn from row 9 down; zp_39 = 9, zp_3B = 6, zp_3C = $1D, zp_3D = $61, zp_3F = 0,
|
|
; zp_40 left after the last line
|
|
; Called from: mainMenuLoop at $14EA, miscTabAction at $1A53, $1A83 and $1AD0
|
|
; Set up the option-list window and print the ' OPTIONS' heading.
|
|
; ----------------------------------------------------------------------
|
|
printOptionsList:
|
|
lda #$06 ; 19F3 items start at screen column 6
|
|
sta textWindowLeft ; 19F5 window left edge
|
|
lda #$09 ; 19F7 first row of the list is screen row 9
|
|
sta textWindowTop ; 19F9 window top
|
|
sta textCursorRow ; 19FB and the cursor row
|
|
lda #$1D ; 19FD 29 columns wide - also the width of the menu highlight bar
|
|
sta textWindowWidth ; 19FF window width
|
|
lda #$00 ; 1A01 column 0 of the window
|
|
sta textCursorCol ; 1A03 cursor column
|
|
jsr updateCursorPointers ; 1A05 recompute the cell pointers
|
|
lda #$61 ; 1A08 white text, EOR mask 6 for the highlight bar
|
|
sta textColour ; 1A0A set the colour
|
|
lda #$13 ; 1A0C low byte of the heading
|
|
ldy #$CB ; 1A0E $CB13 = ' OPTIONS' followed by a CR
|
|
jsr printString ; 1A10 print it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Decide which list to print.
|
|
; ----------------------------------------------------------------------
|
|
lda gameEndReason ; 1A13 0 while a game is running
|
|
ora gameEndCountdown ; 1A16 non-zero while the end-of-game sequence runs (endGame sets it to $78 and the IRQ counts it up to $80)
|
|
bne L_1A36 ; 1A19 no live game - print the main menu instead
|
|
|
|
; ----------------------------------------------------------------------
|
|
; In-game options.
|
|
; ----------------------------------------------------------------------
|
|
lda #$A6 ; 1A1B low byte of 'ABORT GAME'
|
|
ldy #$CC ; 1A1D $CCA6 = 'ABORT GAME' + CR
|
|
jsr printString ; 1A1F print it; the pointer is left at $CCB1 = 'VOICE PAUSE'
|
|
lda filmPlaybackMode ; 1A22 bit 7 = film playback
|
|
bmi L_1A2F ; 1A25 you cannot ring your opponent during a replay
|
|
lda isSoloTrainer ; 1A27 bit 7 = solo trainer game
|
|
bmi L_1A2F ; 1A2A the trainer has no phone - skip VOICE PAUSE
|
|
jsr printStringAtPointer ; 1A2C print the next string in the block: 'VOICE PAUSE' + CR
|
|
L_1A2F:
|
|
lda #$B2 ; 1A2F low byte of ' (EXIT)'
|
|
ldy #$CA ; 1A31 $CAB2 = ' (EXIT)', the tail of the formation-menu string block
|
|
jmp printString ; 1A33 print the last item and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Main menu (no game running).
|
|
; ----------------------------------------------------------------------
|
|
L_1A36:
|
|
lda #$1F ; 1A36 low byte of mainMenuItemText
|
|
ldy #$CC ; 1A38 $CC1F = four CR-separated lines: COMPETE WITH MODEM OPPONENT / PRACTICE WITH SOLO TRAINER / WATCH GAME FILM / SAVE GAME FILM
|
|
jsr printString ; 1A3A print all four; the pointer is left at $CC75
|
|
jsr printStringAtPointer ; 1A3D print the next string: 'LOAD GAME FILM '
|
|
lda isLinkActive ; 1A40 comm module's link-active flag (also trainerAiE000:isLinkActive)
|
|
beq L_1A48 ; 1A43 no link - the menu has five items
|
|
jsr printStringAtPointer ; 1A45 link up: print 'DISCONNECT/SET VOICE ' as item 5
|
|
L_1A48:
|
|
rts ; 1A48 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; miscTabAction - Fire-button handler of the MISC tab (entry 2 of menuItemHandlerTable at $1535) and,
|
|
; through the JMP at $0AC5, the top of the whole main-menu loop at cold start. With a game running it
|
|
; runs the in-game options menu; picking ABORT GAME tells a connected opponent with command $8C reason
|
|
; 0, waits up to $26 ticks for the exchange to drain, hangs up and restores the status message. It
|
|
; then falls into an endless loop that clears the panel, undoes any film-playback settings, reprints
|
|
; the option list and runs the main menu.
|
|
; In: gameEndCountdown $92C9, gameEndReason $0BA8, filmPlaybackMode $0B9B, isSoloTrainer $0BA5, link
|
|
; flags $E03B/$E030, linkSessionState $0B9C, persistentMessageId $92A1
|
|
; Out: menu state $91D4/$91D5/$91D6, command $8C queued, linkPollDelay $9163 = 0, zp_7F timeout, the
|
|
; operand at $1AC0 patched with the saved message id; may end the session through
|
|
; endGameToMainMenu. Never returns while no game is running - runMainMenu leaves through
|
|
; handleMainMenuChoice instead
|
|
; Called from: mainMenuLoop through menuItemHandlerTable[2] at $148D, and JMP $0AC5 (cold start into
|
|
; the main menu)
|
|
; Run the in-game options menu, if there is a game to have options about.
|
|
; ----------------------------------------------------------------------
|
|
miscTabAction:
|
|
lda gameEndCountdown ; 1A49 non-zero during the countdown after a game has just ended
|
|
bne L_1AC7 ; 1A4C the game is over - go straight to the session teardown
|
|
lda gameEndReason ; 1A4E 0 while a game is running
|
|
bne L_1ACA ; 1A51 no game at all - go straight to the main-menu loop
|
|
jsr printOptionsList ; 1A53 print ABORT GAME / [VOICE PAUSE] / (EXIT)
|
|
ldy #$02 ; 1A56 assume two items
|
|
lda filmPlaybackMode ; 1A58 bit 7 = film playback
|
|
bmi L_1A63 ; 1A5B no VOICE PAUSE during a replay
|
|
lda isSoloTrainer ; 1A5D bit 7 = solo trainer
|
|
bmi L_1A63 ; 1A60 no VOICE PAUSE against the trainer
|
|
iny ; 1A62 modem game: three items
|
|
L_1A63:
|
|
sty menuItemCount ; 1A63 tell the menu selector how many rows there are
|
|
dey ; 1A66 index of the last item
|
|
sty menuSelectedItem ; 1A67 preselect (EXIT)
|
|
dec textWindowLeft ; 1A6A shift the window left edge to 5 so the highlight bar starts one column left of the text
|
|
lda textWindowLeft ; 1A6C the new left edge...
|
|
sta textCursorCol ; 1A6E ...also used as the cursor column; toggleMenuRowHighlight forces column 0 anyway, so this looks vestigial
|
|
lda #$0B ; 1A70 the first menu row is screen row 11
|
|
sta menuFirstRow ; 1A72 the selector adds the selection index to this
|
|
jsr setCursorRow ; 1A75 put the cursor on that row
|
|
jsr runInGameOptionsMenu ; 1A78 run the selector; it handles VOICE PAUSE itself and returns Y = chosen item
|
|
ldy menuSelectedItem ; 1A7B which item was picked?
|
|
beq L_1A86 ; 1A7E item 0 = ABORT GAME
|
|
jsr setMenuTextWindow ; 1A80 (EXIT): restore the menu window and colour...
|
|
jmp printOptionsList ; 1A83 ...reprint the list and return to mainMenuLoop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ABORT GAME (also the cold-start entry from $0AC5). Tell the opponent, drain the link and hang up,
|
|
; then fall through into the session teardown.
|
|
; ----------------------------------------------------------------------
|
|
L_1A86:
|
|
lda persistentMessageId ; 1A86 the message currently pinned on the status line
|
|
sta L_1ABF+1 ; 1A89 self-modifying code: it becomes the LDA #imm operand at $1AC0, so it can be restored at $1ABF
|
|
lda isLinkActive ; 1A8C is the comm link up? (also trainerAiE000:isLinkActive)
|
|
beq L_1ABF ; 1A8F no link - nothing to tell anybody
|
|
lda linkSessionState ; 1A91 0 none, 2 game session agreed, $80 link up at menu level, $FF link lost
|
|
bmi L_1ABF ; 1A94 $80/$FF: no game session to abort
|
|
beq L_1ABC ; 1A96 0: no session at all - just hang up
|
|
lda #$00 ; 1A98 reason 0 = 'I am leaving the game'
|
|
sta cmdParam0 ; 1A9A command argument 0
|
|
sta linkPollDelay ; 1A9C clear the exchange throttle so the packet goes out at full speed
|
|
lda commBuildId ; 1A9F $E030 = comm build id: $00 modem driver, $FF solo trainer (also trainerAiE000:commBuildId)
|
|
bmi L_1ABC ; 1AA2 the trainer discards everything we send - skip the handshake
|
|
jsr showWorkingMessage ; 1AA4 put 'WORKING...' on the status line
|
|
lda #$8C ; 1AA7 $8C = cmdSessionControl
|
|
jsr queueCmd2 ; 1AA9 queue the 2-byte command (code + reason)
|
|
lda #$26 ; 1AAC $26 ticks...
|
|
sta linkWaitTimer ; 1AAE ...as the timeout (zp_7F is decremented every 8th frame by the IRQ)
|
|
L_1AB0:
|
|
jsr serviceCommandExchange ; 1AB0 keep the lock-step exchange running so the packet is actually sent
|
|
lda exchangeFlags ; 1AB3 $E01D = the comm module's exchange flag byte (also trainerAiE000:commInterfaceBlock)
|
|
beq L_1ABF ; 1AB6 all flags clear - nothing is in flight any more
|
|
lda linkWaitTimer ; 1AB8 has the timeout expired?
|
|
bne L_1AB0 ; 1ABA no - keep servicing the link
|
|
L_1ABC:
|
|
jsr hangUpModem ; 1ABC drop the line (prompting the player to pick up the handset first)
|
|
L_1ABF:
|
|
lda #$FF ; 1ABF operand patched at $1A89 = the message id that was on the status line
|
|
sta persistentMessageId ; 1AC1 pin it again
|
|
jsr showStatusMessage ; 1AC4 and redisplay it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Session teardown and the main-menu loop. runMainMenu normally never comes back: it jumps into
|
|
; handleMainMenuChoice in the $6F00 overlay.
|
|
; ----------------------------------------------------------------------
|
|
L_1AC7:
|
|
jsr endGameToMainMenu ; 1AC7 reset all the per-game state (also entered directly when the game just ended)
|
|
L_1ACA:
|
|
jsr clearMenuTextWindow ; 1ACA clear the menu panel
|
|
jsr restoreSettingsAfterFilm; 1ACD if we were watching a film, put the real game settings back
|
|
jsr printOptionsList ; 1AD0 print the five- or six-item main menu
|
|
jsr runMainMenu ; 1AD3 run it; only DISCONNECT/SET VOICE returns here
|
|
jsr clearMenuTextWindow ; 1AD6 clear the panel again
|
|
jmp miscTabAction ; 1AD9 and start over
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; endGameToMainMenu - Tears a finished game session down and prepares the main menu. It clears the
|
|
; end-of-game countdown and the pause state, empties the outgoing command ring, silences the sound
|
|
; unless a win/lose jingle is still playing, marks 'no game in progress', writes the game film out
|
|
; when the battle really ended, chooses a sensible default main-menu item, and leaves the link either
|
|
; flushed and up ($0B9C = $80) or down (0).
|
|
; In: sndVoiceSoundId $67C8, comm build id $E030, link-active flag $E03B, gamePhase zp_B1,
|
|
; filmPlaybackMode $0B9B, linkSessionState $0B9C, gameEndReason $0BA8
|
|
; Out: $92C9 = 0, zp_BE = 0, $908D/$908E = 0, defaultMainMenuItem $92FD, gameEndReason $0BA8 >= 1,
|
|
; film header written, $0B9C = 0 or $80
|
|
; Called from: miscTabAction at $1AC7 and by JMP from the $C000 overlay at $C879 when $92C9 goes
|
|
; negative
|
|
; Clear the per-game state.
|
|
; ----------------------------------------------------------------------
|
|
endGameToMainMenu:
|
|
lda #$00 ; 1ADC zero for the five stores below
|
|
sta gameEndCountdown ; 1ADE cancel the end-of-game countdown
|
|
sta pauseState ; 1AE1 clear own/opponent pause and voice-pause bits
|
|
sta outgoingCmdWriteIndex ; 1AE3 empty the outgoing command ring (write index)
|
|
sta outgoingCmdReadIndex ; 1AE6 and its read index
|
|
sta defaultMainMenuItem ; 1AE9 default main-menu item 0 = COMPETE WITH MODEM OPPONENT
|
|
lda sndVoiceSoundId ; 1AEC sound id currently playing on the 'voice' channel
|
|
cmp #$0D ; 1AEF $0D = the 'we won' fanfare
|
|
beq L_1AFA ; 1AF1 let it finish
|
|
cmp #$0E ; 1AF3 $0E = the 'we lost' theme
|
|
beq L_1AFA ; 1AF5 let that finish too
|
|
jsr stopAllSound ; 1AF7 otherwise silence all three SID voices
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pick the default main-menu item and finalise the film.
|
|
; ----------------------------------------------------------------------
|
|
L_1AFA:
|
|
ldy commBuildId ; 1AFA $E030: $FF = the solo trainer module is behind the $E000 jump table (also trainerAiE000:commBuildId)
|
|
bpl L_1B07 ; 1AFD modem build - the link flag means something
|
|
lda #$00 ; 1AFF a trainer game never has a real link...
|
|
sta isLinkActive ; 1B01 ...so force the link-active flag down (also trainerAiE000:isLinkActive)
|
|
inc defaultMainMenuItem ; 1B04 default item 1 = PRACTICE WITH SOLO TRAINER
|
|
L_1B07:
|
|
lda gameEndReason ; 1B07 0 while a game was running
|
|
bne L_1B0F ; 1B0A already non-zero - leave the real end reason alone
|
|
inc gameEndReason ; 1B0C 0 -> 1 = 'no game in progress'
|
|
L_1B0F:
|
|
lda gamePhase ; 1B0F zp_B1 is 0 only during the actual battle
|
|
bne L_1B20 ; 1B11 we were in setup, film or menu phase - no film to close
|
|
lda filmPlaybackMode ; 1B13 bit 7 = we were watching a film, not recording one
|
|
bmi L_1B20 ; 1B16 nothing to write in that case
|
|
jsr finalizeFilm ; 1B18 write the end marker, length, clock, scores and checksum into the film header
|
|
lda #$02 ; 1B1B default item 2 = WATCH GAME FILM
|
|
sta defaultMainMenuItem ; 1B1D so the player can review what just happened
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Leave the link in a defined state.
|
|
; ----------------------------------------------------------------------
|
|
L_1B20:
|
|
lda isLinkActive ; 1B20 is the comm link up? (also trainerAiE000:isLinkActive)
|
|
beq L_1B2F ; 1B23 no - session state 0
|
|
lda linkSessionState ; 1B25 current session state
|
|
bmi L_1B2D ; 1B28 $80/$FF - already at menu level, nothing in flight
|
|
jsr completeExchange ; 1B2A flush the packet exchange that is still in progress
|
|
L_1B2D:
|
|
lda #$80 ; 1B2D $80 = link still up, but at menu level
|
|
L_1B2F:
|
|
sta linkSessionState ; 1B2F store the new session state
|
|
rts ; 1B32 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; hangUpModem - Convenience entry: hang up and leave linkSessionState at 0 (no session).
|
|
; In: none
|
|
; Out: see hangUpModemSetState
|
|
; Called from: miscTabAction $1ABC, disconnectFromOpponent $1C28, and by JMP from $56DB, $5722, $5771
|
|
; ----------------------------------------------------------------------
|
|
hangUpModem:
|
|
lda #$00 ; 1B33 the new linkSessionState is 0 = no session
|
|
|
|
; ----------------------------------------------------------------------
|
|
; hangUpModemSetState - Drops the modem link and stores A as the new linkSessionState. On the modem
|
|
; build it first tells the player 'PICK UP PHONE THEN PRESS SPACE.' (message $17) and waits for the
|
|
; space bar, so the voice call survives the modem going on-hook; then it waits up to $26 ticks for bit
|
|
; 7 of the debounced carrier state $E03C to clear before calling the comm module's link-control entry
|
|
; with X = 3 (hang up, then shut the UART down).
|
|
; In: A = new value for linkSessionState $0B9C; link-active flag $E03B, comm build id $E030, carrier
|
|
; state $E03C
|
|
; Out: $0B9C = A, the comm module's link variables $E039-$E047 cleared, zp_7F used as the timeout; the
|
|
; operand at $1B5A is patched with A
|
|
; Called from: hangUpModem (fall-through) and hangUpAndPause at $1C0F. NOTE: the survey text had
|
|
; messages $17 and $18 swapped between this routine and openCommLink; the pointer tables at
|
|
; $0500/$0524 give id $17 = $8897 'PICK UP PHONE THEN PRESS SPACE.' and id $18 = $88B6 'PRESS
|
|
; SPACE, WAIT, HANGUP PHONE.'
|
|
; ----------------------------------------------------------------------
|
|
hangUpModemSetState:
|
|
sta L_1B59+1 ; 1B35 self-modifying code: A becomes the LDA #imm operand at $1B5A, restored at $1B59
|
|
lda isLinkActive ; 1B38 is the link up at all? (also trainerAiE000:isLinkActive)
|
|
beq L_1B5B ; 1B3B no - just record the new session state
|
|
lda commBuildId ; 1B3D $E030: $FF = solo trainer module, $00 = real modem driver (also trainerAiE000:commBuildId)
|
|
bmi L_1B54 ; 1B40 the trainer has no phone line - skip the prompt
|
|
lda #$17 ; 1B42 message $17 = 'PICK UP PHONE THEN PRESS SPACE.'
|
|
jsr showMessageWaitForSpace ; 1B44 show it and block until the player presses space
|
|
lda #$26 ; 1B47 $26 ticks...
|
|
sta linkWaitTimer ; 1B49 ...as a timeout (zp_7F is decremented every 8th frame by the IRQ)
|
|
L_1B4B:
|
|
lda linkStatus ; 1B4B debounced carrier state; bit 7 mirrors the 'sample has settled' bit the driver puts in $E03D (also trainerAiE000:linkStatus)
|
|
bpl L_1B54 ; 1B4E bit 7 clear = the line state is no longer settled, i.e. the modem has reacted - stop waiting (probably)
|
|
lda linkWaitTimer ; 1B50 timeout expired?
|
|
bne L_1B4B ; 1B52 no - keep watching the carrier
|
|
L_1B54:
|
|
ldx #$03 ; 1B54 X = 3: hang up, wait for the UART to idle, then shut it down and clear the link variables
|
|
jsr commLinkControl ; 1B56 call the $E003 entry of whichever comm module is loaded (also trainerAiE000:commSetLinkStateEntry)
|
|
L_1B59:
|
|
lda #$FF ; 1B59 operand patched at $1B35 = the requested session state
|
|
L_1B5B:
|
|
sta linkSessionState ; 1B5B store it
|
|
rts ; 1B5E return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; openCommLink - Brings the link up if it is not already. On the modem build it first shows message
|
|
; $18 'PRESS SPACE, WAIT, HANGUP PHONE.' and waits for the space bar - the player presses space so the
|
|
; modem goes off-hook, waits, and then puts the handset down - and then calls the comm module's
|
|
; link-control entry with X = 0, which sets the link flags $E03B/$E03C/$E03D.
|
|
; In: link-active flag $E03B, isSoloTrainer $0BA5
|
|
; Out: the comm module's link flags set
|
|
; Called from: openLinkAndWaitForConnection $1B74 and the $6F00 overlay A at $7962. NOTE: the survey
|
|
; text had this message swapped with the one used by hangUpModemSetState
|
|
; ----------------------------------------------------------------------
|
|
openCommLink:
|
|
lda isLinkActive ; 1B5F is the link already up? (also trainerAiE000:isLinkActive)
|
|
bne L_1B73 ; 1B62 yes - nothing to do
|
|
lda isSoloTrainer ; 1B64 bit 7 = solo trainer game
|
|
bmi L_1B6E ; 1B67 the trainer needs no prompt
|
|
lda #$18 ; 1B69 message $18 = 'PRESS SPACE, WAIT, HANGUP PHONE.'
|
|
jsr showMessageWaitForSpace ; 1B6B show it and block until the player presses space
|
|
L_1B6E:
|
|
ldx #$00 ; 1B6E X = 0: open the link
|
|
jsr commLinkControl ; 1B70 call the $E003 entry of whichever comm module is loaded (also trainerAiE000:commSetLinkStateEntry)
|
|
L_1B73:
|
|
rts ; 1B73 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; openLinkAndWaitForConnection - openCommLink followed by waitForConnection (fall-through).
|
|
; In: none
|
|
; Out: see the two callees
|
|
; Called from: voicePauseReconnect at $1BFA
|
|
; ----------------------------------------------------------------------
|
|
openLinkAndWaitForConnection:
|
|
jsr openCommLink ; 1B74 bring the line up (with the player prompt on a modem build)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForConnection - On the modem build, pins the status message 'WAITING FOR CONNECTION...', spins
|
|
; in waitForCarrier until the modem reports carrier, then clears the voice-pause bit of pauseState and
|
|
; blanks the message line. On the solo trainer build it returns immediately.
|
|
; In: isSoloTrainer $0BA5
|
|
; Out: persistentMessageId $92A1, zp_18, zp_BE bit 7 cleared, message line cleared
|
|
; Called from: openLinkAndWaitForConnection (fall-through) and the $6F00 overlay A at $7965
|
|
; ----------------------------------------------------------------------
|
|
waitForConnection:
|
|
lda isSoloTrainer ; 1B77 bit 7 = solo trainer game
|
|
bmi L_1B99 ; 1B7A no line to wait for - return at once
|
|
lda #$10 ; 1B7C message $10 = 'WAITING FOR CONNECTION...'
|
|
sta persistentMessageId ; 1B7E pin it so it is not replaced by queued messages
|
|
jsr showStatusMessage ; 1B81 show it immediately
|
|
lda #$FF ; 1B84 preset the (never tested) wait counter
|
|
sta scratch18 ; 1B86 zp_18 - see the note in waitForCarrier
|
|
jsr waitForCarrier ; 1B88 spin until the modem reports carrier
|
|
lda pauseState ; 1B8B pause/disconnect flags
|
|
and #$7F ; 1B8D clear bit 7 = the voice pause is over
|
|
sta pauseState ; 1B8F store them back
|
|
lda #$00 ; 1B91 no pinned message any more
|
|
sta persistentMessageId ; 1B93 release the status line
|
|
jsr clearMessageLine ; 1B96 blank the status line and return
|
|
L_1B99:
|
|
rts ; 1B99 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForCarrier - Polling loop that runs until the comm module reports carrier (bit 6 of the
|
|
; debounced carrier state $E03C). Each pass it services the status/message system, runs the link
|
|
; watchdog when the game is in the $FF phase, waits two frames and steps a countdown in zp_18 while
|
|
; the raw carrier sample shows no carrier. Once carrier appears it silences the sound and returns.
|
|
; In: gamePhase zp_B1, carrier state $E03C, raw carrier sample $E03D, zp_18
|
|
; Out: sound silenced; zp_18 counted down but never tested by anybody, so the countdown is vestigial
|
|
; (and zp_18 is a shared scratch that the printing inside updateStatusMessage may clobber anyway)
|
|
; Called from: waitForConnection at $1B88 and by its own loop
|
|
; ----------------------------------------------------------------------
|
|
waitForCarrier:
|
|
jsr updateStatusMessage ; 1B9A keep the status line and chat display alive while we wait
|
|
lda gamePhase ; 1B9D zp_B1 = $FF marks the pre-game / side-selection phase
|
|
cmp #$FF ; 1B9F the only phase in which a stalled link has to be noticed here
|
|
bne L_1BA6 ; 1BA1 any other phase - no watchdog
|
|
jsr checkLinkTimeout ; 1BA3 'PHONE TROUBLE.' / 'TRY AGAIN.' watchdog on the packet exchange
|
|
L_1BA6:
|
|
lda #$02 ; 1BA6 two frames...
|
|
jsr waitFrames ; 1BA8 ...of raster-timed delay per poll
|
|
bit linkStatusSample ; 1BAB raw carrier sample from the last IRQ (also trainerAiE000:linkStatusSample)
|
|
bvs L_1BB6 ; 1BAE bit 6 set = the sample sees carrier, so do not count down
|
|
dec scratch18 ; 1BB0 step the (unused) no-carrier countdown
|
|
bne L_1BB6 ; 1BB2 not at zero yet
|
|
inc scratch18 ; 1BB4 hold it at 1 instead of wrapping round to $FF
|
|
L_1BB6:
|
|
bit linkStatus ; 1BB6 debounced carrier state (also trainerAiE000:linkStatus)
|
|
bvc waitForCarrier ; 1BB9 bit 6 clear = still no carrier - keep waiting
|
|
lda #$00 ; 1BBB sound 0 = silence all voices
|
|
jmp playSound ; 1BBD silence the dial tone/noise and return to waitForConnection
|
|
|
|
|
|
; orphanRts1BC0 - a stray $60 (RTS) between waitForCarrier and showMessageWaitForSpace. Nothing
|
|
; branches or jumps to it and waitForCarrier always leaves through the JMP at $1BBD, so it is
|
|
; unreachable - probably the tail of an earlier version of the wait loop.
|
|
orphanRts1BC0:
|
|
.byte $60 ; 1BC0 ` unreachable RTS
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showMessageWaitForSpace - Pins status message A on the message line and blocks until the player
|
|
; presses the space bar. It discards any buffered incoming chat text so the prompt is not overwritten,
|
|
; leaves chat typing mode, and while waiting keeps the message system running and the shared RNG
|
|
; stirring. On exit it empties the message queue, locks the keyboard out for 30 frames and clears the
|
|
; key latch so the space press is not seen twice.
|
|
; In: A = message id (used with $17 'PICK UP PHONE THEN PRESS SPACE.' and $18 'PRESS SPACE, WAIT,
|
|
; HANGUP PHONE.')
|
|
; Out: persistentMessageId $92A1, incomingChatLength $9277 = 0, chatState $929C = 0, inputLockoutTimer
|
|
; $0B7D = $1E, lastTypedKey $9248 = $FF, message queue emptied
|
|
; Called from: hangUpModemSetState $1B44 and openCommLink $1B6B
|
|
; ----------------------------------------------------------------------
|
|
showMessageWaitForSpace:
|
|
sta persistentMessageId ; 1BC1 pin the message so nothing queued replaces it
|
|
ldy #$00 ; 1BC4 zero
|
|
sty incomingChatLength ; 1BC6 throw away buffered incoming chat text so it cannot overwrite the prompt
|
|
jsr showStatusMessage ; 1BC9 A still holds the message id: show it with its sound now
|
|
lda #$FF ; 1BCC no key seen yet
|
|
sta lastTypedKey ; 1BCE clear the key latch
|
|
lda #$00 ; 1BD1 zero for the three stores below
|
|
sta chatState ; 1BD3 leave chat typing mode
|
|
sta inputLockoutTimer ; 1BD6 accept keys immediately (no input lockout)
|
|
sta incomingChatLength ; 1BD9 clear the chat length again - showStatusMessage may have refilled it at $C52E
|
|
L_1BDC:
|
|
jsr updateStatusMessage ; 1BDC service the status line and chat display
|
|
jsr nextGameRandom ; 1BDF keep stepping the shared RNG so the wait does not bias it
|
|
ldy lastTypedKey ; 1BE2 last key code seen by the IRQ keyboard scan
|
|
cpy #$A0 ; 1BE5 $A0 = space (the game sets bit 7 on its key codes)
|
|
bne L_1BDC ; 1BE7 anything else - keep waiting
|
|
jsr resetMessageQueue ; 1BE9 empty the message queue and refresh the status line
|
|
lda #$1E ; 1BEC 30 frames...
|
|
sta inputLockoutTimer ; 1BEE ...during which the IRQ ignores the keyboard
|
|
lda #$FF ; 1BF1 no key
|
|
sta lastTypedKey ; 1BF3 clear the latch so the space press is not processed again
|
|
rts ; 1BF6 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; voicePauseReconnect - The VOICE PAUSE round trip: hang up so both players can talk on the phone,
|
|
; wait five seconds, then bring the line back up and wait for carrier. Finally it clears bit 7 of
|
|
; menuSelectedItem, which is the flag runInGameOptionsMenu spins on at $65D5 while the pause lasts.
|
|
; In: linkSessionState $0B9C
|
|
; Out: zp_7B (see below), menuSelectedItem $91D5 bit 7 cleared, the link re-established
|
|
; Called from: the VOICE PAUSE item of runInGameOptionsMenu at $65C1, the link watchdog at $5A42 and
|
|
; the $C000 overlay at $C768 (when the opponent requested the pause)
|
|
; ----------------------------------------------------------------------
|
|
voicePauseReconnect:
|
|
jsr hangUpAndPause ; 1BF7 hang up and wait 250 frames so the players can talk
|
|
jsr openLinkAndWaitForConnection; 1BFA put the call back into data mode and wait for carrier
|
|
sta shownMessageId ; 1BFD stores whatever A the return path left; in practice bit 7 is always set ($FF on the trainer path, a screen/colour RAM page high byte otherwise), which marks the status line as free - almost certainly unintended but harmless
|
|
lda menuSelectedItem ; 1BFF the menu selection doubles as the pause flag
|
|
and #$7F ; 1C02 clear bit 7
|
|
sta menuSelectedItem ; 1C04 this releases the wait loop in runInGameOptionsMenu at $65D5
|
|
rts ; 1C07 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; hangUpAndPause - Hangs up for a voice pause and then waits about five seconds. The new session state
|
|
; is the current one, except that a negative state ($80 link up at menu level, $FF link lost) is
|
|
; replaced by 0.
|
|
; In: linkSessionState $0B9C
|
|
; Out: $0B9C updated by hangUpModemSetState; 250 frames of raster-timed delay
|
|
; Called from: voicePauseReconnect at $1BF7
|
|
; ----------------------------------------------------------------------
|
|
hangUpAndPause:
|
|
lda linkSessionState ; 1C08 current session state
|
|
bpl L_1C0F ; 1C0B 0 or 2 - keep it as the state to restore
|
|
lda #$00 ; 1C0D $80/$FF becomes 0 = no session
|
|
L_1C0F:
|
|
jsr hangUpModemSetState ; 1C0F prompt, drop carrier and store the state
|
|
lda #$FA ; 1C12 250 frames, about five seconds...
|
|
jmp waitFrames ; 1C14 ...to give the modems time to release the line, then return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; disconnectFromOpponent - Ends a link session cleanly: tells the opponent with command $8C reason 5
|
|
; (disconnect), pushes the packet out immediately and hangs up. Does nothing when no session is active
|
|
; (linkSessionState bit 7 clear).
|
|
; In: linkSessionState $0B9C
|
|
; Out: zp_73 = 5, the $8C command queued and transmitted, the link closed and $0B9C = 0
|
|
; Called from: runMainMenu at $1C60 (DISCONNECT/SET VOICE) and $1C6C (before starting a solo game),
|
|
; the link watchdog at $5A2F, and by JMP from $56B8
|
|
; ----------------------------------------------------------------------
|
|
disconnectFromOpponent:
|
|
lda linkSessionState ; 1C17 session state; bit 7 set = link up
|
|
bpl L_1C2B ; 1C1A no session - nothing to disconnect
|
|
lda #$05 ; 1C1C reason 5 = disconnect (cmdSessionControl sets the opponent's pauseState to $80)
|
|
sta cmdParam0 ; 1C1E command argument 0
|
|
lda #$8C ; 1C20 $8C = cmdSessionControl
|
|
jsr queueCmd2 ; 1C22 queue the 2-byte command
|
|
jsr sendOutgoingPacket ; 1C25 build and hand the packet to the comm module right away
|
|
jsr hangUpModem ; 1C28 then drop the line, leaving linkSessionState = 0
|
|
L_1C2B:
|
|
rts ; 1C2B return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMainMenu - Runs the main menu whose text printOptionsList has just drawn: five items, or six when
|
|
; a link session is up and DISCONNECT/SET VOICE is offered. It preselects defaultMainMenuItem, marks
|
|
; the menu phase (selectedUnit and gamePhase = $F0, so the selector services the command exchange
|
|
; instead of the battle) and calls the generic selector with C = 0 (no slow-down before the last row).
|
|
; Item 5 just disconnects and returns to miscTabAction; every other item reloads the $6F00 overlay and
|
|
; jumps into handleMainMenuChoice, so the routine does not return.
|
|
; In: linkSessionState $0B9C, defaultMainMenuItem $92FD, the text window left by printOptionsList
|
|
; Out: menuItemCount $91D4, menuSelectedItem $91D5, menuFirstRow $91D6 = $0B, selectedUnit $90F8 =
|
|
; $F0, gamePhase zp_B1 = $F0, zp_3D/zp_42 = $60, diskLoadDepth $0BA7 bumped around the overlay
|
|
; reload
|
|
; Called from: miscTabAction at $1AD3
|
|
; Set the menu up: item count, preselection, geometry and the menu-phase flags.
|
|
; ----------------------------------------------------------------------
|
|
runMainMenu:
|
|
ldy #$05 ; 1C2C five items: COMPETE / PRACTICE / WATCH FILM / SAVE FILM / LOAD FILM
|
|
lda linkSessionState ; 1C2E session state; bit 7 set = a link session exists
|
|
bpl L_1C34 ; 1C31 no session - DISCONNECT/SET VOICE was not printed
|
|
iny ; 1C33 six items, the last one being DISCONNECT/SET VOICE
|
|
L_1C34:
|
|
sty menuItemCount ; 1C34 tell the selector how many rows there are
|
|
lda defaultMainMenuItem ; 1C37 the item endGameToMainMenu or selectDefaultMenuItem thought most useful
|
|
sta menuSelectedItem ; 1C3A preselect it
|
|
dec textWindowLeft ; 1C3D shift the window left edge to 5 so the highlight bar starts left of the text
|
|
lda textWindowLeft ; 1C3F the new left edge...
|
|
sta textCursorCol ; 1C41 ...also stored as the cursor column; toggleMenuRowHighlight forces column 0, so this looks vestigial
|
|
lda #$0B ; 1C43 the first menu row is screen row 11
|
|
sta menuFirstRow ; 1C45 the selector adds the selection index to this
|
|
jsr setCursorRow ; 1C48 put the cursor on that row
|
|
lda #$F0 ; 1C4B $F0 = 'no unit is being watched'...
|
|
sta selectedUnit ; 1C4D ...so the selector skips the unit-death and group-highlight logic
|
|
sta gamePhase ; 1C50 zp_B1 = $F0: menu phase, bits 6/7 set, so the selector runs serviceCommandExchange instead of the battle tick
|
|
lda #$60 ; 1C52 colour $60: black text, EOR mask 6 for the highlight bar
|
|
sta textColour ; 1C54 text colour
|
|
sta textPadColour ; 1C56 and the colour used to pad centred strings
|
|
clc ; 1C58 C = 0: no extra delay before the joystick can reach the last row
|
|
jsr runMenuLoopWithFlag ; 1C59 run the selector; it returns with Y = the chosen item
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Act on the choice.
|
|
; ----------------------------------------------------------------------
|
|
cpy #$05 ; 1C5C item 5 = DISCONNECT/SET VOICE
|
|
bne L_1C63 ; 1C5E any other item starts or loads something
|
|
jmp disconnectFromOpponent ; 1C60 just drop the link and return to miscTabAction, which reprints the menu
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Tail of runMainMenu, reached for menu items 0-4 (0 COMPETE WITH MODEM OPPONENT, 1 PRACTICE WITH SOLO
|
|
; TRAINER, 2 WATCH GAME FILM, 3 SAVE GAME FILM, 4 LOAD GAME FILM; item 5 DISCONNECT/SET VOICE was
|
|
; dealt with at $1C60). The chosen item is still in Y and is acted on by overlay A.
|
|
; ----------------------------------------------------------------------
|
|
L_1C63:
|
|
lda #$0F ; 1C63 15 frames
|
|
jsr waitFrames ; 1C65 let the player see the item he picked before the drive starts up
|
|
cpy #$01 ; 1C68 menu item 1 = PRACTICE WITH SOLO TRAINER?
|
|
bne L_1C6F ; 1C6A any other choice leaves an open modem link alone
|
|
jsr disconnectFromOpponent ; 1C6C a solo game has nobody on the line: send $8C reason 5 and hang up first
|
|
L_1C6F:
|
|
inc diskLoadDepth ; 1C6F $0BA7 non-zero makes the raster IRQ swallow keys while the drive is busy
|
|
jsr ensureOverlayAResident ; 1C72 reload the $6F00 overlay variant named by $87FF plus the message strings and comm tail
|
|
dec diskLoadDepth ; 1C75 load finished - the keyboard is live again
|
|
jmp handleMainMenuChoice ; 1C78 enter overlay A at $7DE3, which acts on menu item Y; it never comes back here
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isAsymmetricGameType - Tests the game type held in gameTypeOptions bits 0-2 and returns Z=1 for type
|
|
; 1 (QB SNEAK) or type 6 (DEFENDER), the two scenarios in which the two sides have different jobs and
|
|
; the setup summary therefore prints '1ST PLAY' / '2ND PLAY'.
|
|
; In: gameTypeOptions ($0BA3) bits 0-2
|
|
; Out: A = game type 0-6, Z=1 when the type is asymmetric
|
|
; Called from: drawGameSetupSummary ($15B6 and $15E0)
|
|
; ----------------------------------------------------------------------
|
|
isAsymmetricGameType:
|
|
lda gameTypeOptions ; 1C7B game options: bits 0-2 type, bit 3 unit menus, bit 4 damage, bits 6-7 custom rules/map
|
|
and #$07 ; 1C7E keep only the game type 0-6 (SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER)
|
|
cmp #$01 ; 1C80 type 1 = QB SNEAK: one side sneaks, the other defends
|
|
beq L_1C86 ; 1C82 asymmetric - return with Z=1
|
|
cmp #$06 ; 1C84 type 6 = DEFENDER, the other asymmetric scenario; this compare sets the returned Z
|
|
L_1C86:
|
|
rts ; 1C86 Z=1 the two sides have different jobs, Z=0 both play the same game
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; restoreSettingsAfterFilm - Undoes what WATCH GAME FILM overwrote. When the film-playback flag is
|
|
; set it leaves film mode and, unless the backup slot is marked empty ($FF), copies the pre-film map
|
|
; name, map seed, play order, map orientation and game options back over the live ones, so the main
|
|
; menu again describes the player's own game instead of the film's.
|
|
; In: filmPlaybackMode ($0B9B) bit 7; prevGameTypeOptions ($0BA4) = $FF when nothing was saved; the
|
|
; backups $0BAF-$0BB4, $0B98-$0B9A, $0BA2 and $0B9E
|
|
; Out: filmPlaybackMode = 0; mapNameText $0BA9-$0BAE, mapSeed $0B95-$0B97, playsFirstFlag $0BA1,
|
|
; mapFlipFlag $0B9D and gameTypeOptions $0BA3 restored
|
|
; Called from: the main-menu flow at $1ACD, just before the options list is reprinted
|
|
; ----------------------------------------------------------------------
|
|
restoreSettingsAfterFilm:
|
|
lda filmPlaybackMode ; 1C87 $0B9B bit 7 = a game film is being watched
|
|
bpl L_1CC0 ; 1C8A not a film: the live settings are already the real ones
|
|
lda #$00 ; 1C8C leave film mode
|
|
sta filmPlaybackMode ; 1C8E $0B9B = 0 also lets outgoing commands be queued again
|
|
lda prevGameTypeOptions ; 1C91 $0BA4 = the game options saved before the film was started
|
|
cmp #$FF ; 1C94 $FF marks 'nothing was ever saved here'
|
|
beq L_1CC0 ; 1C96 no backup: keep whatever the film left behind
|
|
ldy #$05 ; 1C98 6 bytes: the 5-character map name plus its bit-7 terminator
|
|
L_1C9A:
|
|
lda mapNameBackup,y ; 1C9A $0BAF+y = the saved map id text
|
|
sta mapNameText,y ; 1C9D $0BA9+y = the map id the STATS tab and the setup summary print
|
|
dey ; 1CA0 next byte down
|
|
bpl L_1C9A ; 1CA1 loop over all 6 bytes
|
|
ldy #$02 ; 1CA3 3 bytes of the 24-bit map seed
|
|
L_1CA5:
|
|
lda prevMapSeed,y ; 1CA5 $0B98+y = saved seed byte
|
|
sta mapSeed,y ; 1CA8 $0B95+y = the seed the generator turns into the battlefield
|
|
dey ; 1CAB next seed byte down
|
|
bpl L_1CA5 ; 1CAC loop over all three
|
|
lda savedPlaysFirstFlag ; 1CAE $0BA2 = who had the first play before the film
|
|
sta playsFirstFlag ; 1CB1 $0BA1 drives the '1ST PLAY' / '2ND PLAY' line
|
|
lda prevMapFlipFlag ; 1CB4 $0B9E = saved map orientation (0 normal, 1 rotated 180 degrees)
|
|
sta mapFlipFlag ; 1CB7 $0B9D, read by the map generator
|
|
lda prevGameTypeOptions ; 1CBA and finally the scenario and rule bits
|
|
sta gameTypeOptions ; 1CBD $0BA3 back to the player's own game type
|
|
L_1CC0:
|
|
rts ; 1CC0 done - the main menu can be reprinted
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setViewVerticalLimit - Sets viewRowLimit, the exclusive map row limit the centre cell of the
|
|
; battlefield view has to stay under: 40 during the battle, 20 in the setup/film/menu phases where a
|
|
; player may only look at his own half of the map. In the restricted case the row passed in Y is
|
|
; biased by -20 as well, so the caller can test it against 0..19; scrollViewBy adds the 20 back before
|
|
; it stores the new view origin.
|
|
; In: Y = a map row (a row delta from scrollViewBy, the view origin from keepGroupBoxInsideMap),
|
|
; gamePhase (zp_B1)
|
|
; Out: viewRowLimit (zp_F8) = $28 or $14, Y reduced by $14 in the restricted phase, A clobbered
|
|
; Called from: scrollViewBy ($1CD7) and keepGroupBoxInsideMap ($1F56)
|
|
; ----------------------------------------------------------------------
|
|
setViewVerticalLimit:
|
|
lda #$28 ; 1CC1 40 = the full height of the battlefield map
|
|
sta viewRowLimit ; 1CC3 zp_F8 bounds the centre (cursor) row of the view
|
|
lda gamePhase ; 1CC5 zp_B1 is 0 only while the battle proper runs
|
|
beq L_1CD2 ; 1CC7 battle: all 40 rows may be looked at
|
|
lda #$14 ; 1CC9 20 rows
|
|
sta viewRowLimit ; 1CCB setup/film/menu phase: a player may only see his own half of the map
|
|
tya ; 1CCD take the row value the caller handed in
|
|
sec ; 1CCE prepare an exact subtraction
|
|
sbc #$14 ; 1CCF shift it down by 20 so map rows 20-39 test as 0-19
|
|
tay ; 1CD1 hand the biased row back in Y
|
|
L_1CD2:
|
|
rts ; 1CD2 zp_F8 is set either way
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scrollViewBy - Moves the 7x5-cell tactical view by one map cell: X = column delta, Y = row delta,
|
|
; each -1, 0 or +1. The cursor is always the centre cell (viewOriginCol+3, viewOriginRow+2), so a
|
|
; step is refused when that cell - or, while a group formation box is being dragged, any edge of the
|
|
; box - would leave the 40-column map or the row window set by setViewVerticalLimit. A refused axis
|
|
; has its delta zeroed; the deltas that survive stay in scrollDeltaCol/scrollDeltaRow, the mini-map
|
|
; cursor sprite is moved, and control falls through into scrollViewBitmap, which shifts the pixels.
|
|
; In: X = dx, Y = dy (-1/0/+1); viewOriginCol/Row (zp_A5/zp_A6); gamePhase; groupBoxActive ($920A)
|
|
; bit 7 with the box edge offsets $922E-$9231
|
|
; Out: viewOriginCol/Row updated; scrollDeltaCol/scrollDeltaRow (zp_F0/zp_F1) = the deltas actually
|
|
; applied; viewRowLimit set; activeSpriteIndex = 0 and sprite 0 repositioned; jumps into
|
|
; scrollViewBitmap and returns from there. Self-modifies the LDA # operands at $1CF6+1 and
|
|
; $1D2E+1
|
|
; Called from: scrollViewToUnit ($1D8F) and scrollViewportByDirection ($2179)
|
|
; ----------------------------------------------------------------------
|
|
scrollViewBy:
|
|
stx scrollDeltaCol ; 1CD3 remember the requested column step (-1/0/+1)
|
|
sty scrollDeltaRow ; 1CD5 and the requested row step
|
|
jsr setViewVerticalLimit ; 1CD7 zp_F8 = 40 or 20; in the restricted phases Y comes back biased by -20
|
|
tya ; 1CDA row delta
|
|
clc ; 1CDB plain add
|
|
adc viewOriginRow ; 1CDC candidate new top row of the view
|
|
tay ; 1CDE keep the candidate origin in Y
|
|
clc ; 1CDF plain add
|
|
adc #$02 ; 1CE0 the cursor cell sits 2 rows below the top edge of the 5-cell-high view
|
|
cmp viewRowLimit ; 1CE2 is the new cursor row still inside the allowed rows?
|
|
bcs L_1D0E ; 1CE4 off the map (or out of your own half): refuse the vertical step
|
|
bit groupBoxActive ; 1CE6 $920A bit 7 = a group formation box is riding on the cursor
|
|
bpl L_1D00 ; 1CE9 no box: the centre cell alone decides
|
|
sta L_1CF6+1 ; 1CEB self-modifying: park the candidate cursor row in the LDA # operand at $1CF6+1
|
|
clc ; 1CEE plain add
|
|
adc groupBoxTopOffset ; 1CEF $9231 = signed offset from the cursor to the top edge of the box
|
|
cmp viewRowLimit ; 1CF2 unsigned compare, so a wrapped (negative) row fails too
|
|
bcs L_1D0E ; 1CF4 the box would hang off the top of the map: refuse
|
|
L_1CF6:
|
|
lda #$FF ; 1CF6 operand written at $1CEB: the candidate cursor row
|
|
clc ; 1CF8 plain add
|
|
adc groupBoxBottomOffset ; 1CF9 $9230 = signed offset to the bottom edge of the box
|
|
cmp viewRowLimit ; 1CFC bottom edge still on the map?
|
|
bcs L_1D0E ; 1CFE the box would hang off the bottom: refuse
|
|
L_1D00:
|
|
lda gamePhase ; 1D00 which phase are we in?
|
|
beq L_1D09 ; 1D02 battle: the candidate row needs no correction
|
|
tya ; 1D04 candidate origin row, still biased
|
|
clc ; 1D05 plain add
|
|
adc #$14 ; 1D06 undo the -20 bias setViewVerticalLimit applied
|
|
tay ; 1D08 back into Y for the store below
|
|
L_1D09:
|
|
sty viewOriginRow ; 1D09 accept the vertical scroll
|
|
jmp L_1D12 ; 1D0B now try the horizontal step
|
|
|
|
L_1D0E:
|
|
lda #$00 ; 1D0E no vertical movement
|
|
sta scrollDeltaRow ; 1D10 vertical step clamped: tell scrollViewBitmap not to shift any rows
|
|
L_1D12:
|
|
txa ; 1D12 column delta
|
|
clc ; 1D13 plain add
|
|
adc viewOriginCol ; 1D14 candidate new left column of the view
|
|
tax ; 1D16 keep the candidate origin in X
|
|
clc ; 1D17 plain add
|
|
adc #$03 ; 1D18 the cursor cell sits 3 columns right of the left edge of the 7-cell-wide view
|
|
cmp #$28 ; 1D1A 40 map columns
|
|
bcs L_1D3D ; 1D1C off the map: refuse the horizontal step
|
|
bit groupBoxActive ; 1D1E group formation box active?
|
|
bpl L_1D38 ; 1D21 no box: accept
|
|
sta L_1D2E+1 ; 1D23 self-modifying: park the candidate cursor column in the LDA # operand at $1D2E+1
|
|
clc ; 1D26 plain add
|
|
adc groupBoxLeftOffset ; 1D27 $922F = signed offset to the left edge of the box
|
|
cmp #$28 ; 1D2A left edge still on the map?
|
|
bcs L_1D3D ; 1D2C box would stick out to the left: refuse
|
|
L_1D2E:
|
|
lda #$FF ; 1D2E operand written at $1D23: the candidate cursor column
|
|
clc ; 1D30 plain add
|
|
adc groupBoxRightOffset ; 1D31 $922E = signed offset to the right edge of the box
|
|
cmp #$28 ; 1D34 right edge still on the map?
|
|
bcs L_1D3D ; 1D36 box would stick out to the right: refuse
|
|
L_1D38:
|
|
stx viewOriginCol ; 1D38 accept the horizontal scroll
|
|
jmp L_1D41 ; 1D3A both axes decided
|
|
|
|
L_1D3D:
|
|
lda #$00 ; 1D3D no horizontal movement
|
|
sta scrollDeltaCol ; 1D3F horizontal step clamped: no column shift
|
|
L_1D41:
|
|
lda #$00 ; 1D41 sprite 0
|
|
sta activeSpriteIndex ; 1D43 $91CC = the sprite positionCursorSprite works on: the mini-map cursor box
|
|
jsr positionCursorSprite ; 1D46 move it onto the new centre cell (the mini-map draws each map cell 4x4 pixels)
|
|
jmp scrollViewBitmap ; 1D49 shift bitmap, screen and colour RAM and redraw the cells that came into sight
|
|
|
|
|
|
; scrollTargetUnit / scrollStepDelay - the two parameters of scrollViewToUnit. They live in memory
|
|
; because that routine gives the registers up to updateGameFrame between steps.
|
|
scrollTargetUnit:
|
|
.byte $00 ; 1D4C . scrollTargetUnit: unit index the view is being scrolled onto
|
|
|
|
; scrollStepDelay - frames between two scroll steps; 0 makes scrollViewToUnit jump to the unit in one
|
|
; go without animating.
|
|
scrollStepDelay:
|
|
.byte $00 ; 1D4D . scrollStepDelay: frames between steps; 0 = jump there without animating
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scrollViewToUnit - Scrolls the battlefield view until unit X sits under the centre cursor, one map
|
|
; cell per step along a Bresenham line that starts at the cell the caller left in lineCurRow/
|
|
; lineCurCol (normally the current cursor cell). With A = 0 the whole walk runs at once; with A != 0
|
|
; each step waits A frames on timer zp_6E while updateGameFrame keeps the game, the sound and the link
|
|
; running, and the walk is abandoned as soon as the player touches the joystick. The target is
|
|
; re-read every step, so the view follows a unit that is still moving.
|
|
; In: A = frames per step (0 = no animation), X = unit index, lineCurRow/lineCurCol (zp_92/zp_93) =
|
|
; the cell the walk starts from
|
|
; Out: view scrolled (viewOriginCol/Row); scrollTargetUnit/scrollStepDelay; line stepper state
|
|
; zp_8C-zp_97; zp_6E used as the step timer
|
|
; Called from: recenterViewportOnUnit ($25B3, A=1, the selected unit) and $485C (A=0, the unit shown
|
|
; in the info panel)
|
|
; ----------------------------------------------------------------------
|
|
scrollViewToUnit:
|
|
sta scrollStepDelay ; 1D4E frames between steps (0 = immediate)
|
|
stx scrollTargetUnit ; 1D51 unit to centre the view on
|
|
jsr setLineTargetToUnit ; 1D54 line end point (zp_95/zp_94) = the unit's map cell
|
|
jsr initLineStepper ; 1D57 set up the Bresenham walk from the cursor cell to the unit
|
|
L_1D5A:
|
|
lda scrollStepDelay ; 1D5A animated or immediate?
|
|
beq L_1D7D ; 1D5D no animation wanted: take the next step straight away
|
|
lda lineCurRow ; 1D5F updateGameFrame steps the units through the same line stepper,
|
|
pha ; 1D61 so save the walker's row
|
|
lda lineCurCol ; 1D62 and its column
|
|
pha ; 1D64 on the stack across the call
|
|
jsr updateGameFrame ; 1D65 one frame of engine, message, sound and link work
|
|
pla ; 1D68 restore our own walk position
|
|
sta lineCurCol ; 1D69 zp_93 = current column
|
|
pla ; 1D6B and the row
|
|
sta lineCurRow ; 1D6C zp_92
|
|
lda joyDirection ; 1D6E zp_5D = joystick nibble latched by the raster IRQ
|
|
cmp #$0F ; 1D70 $0F = stick centred
|
|
bne L_1D96 ; 1D72 the player took over: stop the automatic scroll
|
|
lda menuBlinkTimer ; 1D74 zp_6E, an IRQ frame countdown reused here as the step timer
|
|
bne L_1D5A ; 1D76 this step is not due yet: spin another frame
|
|
lda scrollStepDelay ; 1D78 step delay
|
|
sta menuBlinkTimer ; 1D7B reload the timer for the next step
|
|
L_1D7D:
|
|
ldx scrollTargetUnit ; 1D7D the unit we are chasing
|
|
jsr setLineTargetToUnit ; 1D80 re-read its position: it may have moved a cell meanwhile
|
|
jsr initLineStepper ; 1D83 re-aim the line from the cell we are on now
|
|
jsr stepLine ; 1D86 walk one cell; the applied deltas land in $90F4/$90F5
|
|
ldx lastStepColDelta ; 1D89 $90F5 = column step just taken (-1/0/+1)
|
|
ldy lastStepRowDelta ; 1D8C $90F4 = row step just taken (-1/0/+1)
|
|
jsr scrollViewBy ; 1D8F scroll the view by that one cell
|
|
lda lineStepsLeft ; 1D92 zp_91 = steps still to walk
|
|
bne L_1D5A ; 1D94 keep going until the unit is under the cursor
|
|
L_1D96:
|
|
rts ; 1D96 view centred (or the player interrupted)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setLineTargetToUnit - Points the line stepper's end point at unit X. The unit records are parallel
|
|
; 100-byte arrays $64 apart from $F640, so the unit's column and row are unitColTable[X] and
|
|
; unitRowTable[X].
|
|
; In: X = unit index 0-99
|
|
; Out: lineEndCol (zp_95), lineEndRow (zp_94)
|
|
; Called from: scrollViewToUnit ($1D54 and $1D80)
|
|
; ----------------------------------------------------------------------
|
|
setLineTargetToUnit:
|
|
lda unitColTable,x ; 1D97 $F640+unit = the unit's map column
|
|
sta lineEndCol ; 1D9A zp_95 = target column of the line stepper
|
|
lda unitRowTable,x ; 1D9C $F6A4+unit = the unit's map row (the next 100-byte array, $64 further on)
|
|
sta lineEndRow ; 1D9F zp_94 = target row
|
|
rts ; 1DA1 the caller now calls initLineStepper
|
|
|
|
|
|
; Parameter tables for scrollViewBitmap: 22 tables of three bytes, all indexed by a scroll direction
|
|
; biased to 0/1/2 = -1/0/+1. The 9 tables read with X describe the horizontal shift (which byte or
|
|
; character range of a row is copied, and in which direction), the 13 read with Y describe the
|
|
; vertical shift (which character row the copy starts on, where it goes and the row stride). The view
|
|
; is 7x5 map cells of 2x2 characters at screen column 25, character row 1: bitmap $A208, screen RAM
|
|
; $8C41, colour RAM $D841 (screen RAM + $4C00). One character row of it is 14 characters = 112
|
|
; consecutive bitmap bytes, and one map cell is 2 characters = 16 bitmap bytes. scrollXStartIndex -
|
|
; first Y index of the bitmap byte copy.
|
|
scrollXStartIndex:
|
|
.byte $5F,$00,$00 ; 1DA2 _.. $5F,$00,$00 = first Y index of the bitmap byte copy (count down from $5F, or up from 0)
|
|
|
|
; scrollXEndIndex: byteTable, 3 bytes. end Y index of the bitmap byte copy ($FF,$70,$60)
|
|
scrollXEndIndex:
|
|
.byte $FF,$70,$60 ; 1DA5 .p` $FF,$70,$60 = index the copy stops at: $5F..0 down, 0..$6F up, 0..$5F up
|
|
|
|
; scrollXStepOpcode: byteTable, 3 bytes. DEY ($88) or INY ($C8) opcode patched into both copy loops
|
|
scrollXStepOpcode:
|
|
.byte $88,$C8,$C8 ; 1DA8 ... $88 DEY (copy right, backwards) or $C8 INY (copy left / straight, forwards)
|
|
|
|
; scrollXBitmapSrcOffset: byteTable, 3 bytes. byte offset added to the bitmap source address (0,0,$10)
|
|
scrollXBitmapSrcOffset:
|
|
.byte $00,$00,$10 ; 1DAB ... $00,$00,$10 = +16 bytes (2 characters = 1 map cell) is added to the source when the view moves right
|
|
|
|
; scrollXBitmapDstOffset: byteTable, 3 bytes. byte offset added to the bitmap destination address
|
|
; ($10,0,0)
|
|
scrollXBitmapDstOffset:
|
|
.byte $10,$00,$00 ; 1DAE ... $10,$00,$00 = the same 16-byte shift on the destination when the view moves left
|
|
|
|
; scrollYBitmapSrcLo: byteTable, 3 bytes. per vertical direction: bitmap source address lo
|
|
; ($AAC8,$A208,$A488)
|
|
scrollYBitmapSrcLo:
|
|
.byte $C8,$08,$88 ; 1DB1 ... $C8,$08,$88 = low byte of $AAC8/$A208/$A488, the first bitmap row copied (char rows 8/1/3)
|
|
|
|
; scrollYBitmapSrcHi: byteTable, 3 bytes. bitmap source address hi
|
|
scrollYBitmapSrcHi:
|
|
.byte $AA,$A2,$A4 ; 1DB4 ... $AA,$A2,$A4 = high bytes of those three bitmap addresses
|
|
|
|
; scrollYBitmapDstLo: byteTable, 3 bytes. bitmap destination address lo ($AD48,$A208,$A208)
|
|
scrollYBitmapDstLo:
|
|
.byte $48,$08,$08 ; 1DB7 H.. $48,$08,$08 = low byte of $AD48/$A208/$A208, where that row is copied to (char rows 10/1/1)
|
|
|
|
; scrollYBitmapDstHi: byteTable, 3 bytes. bitmap destination address hi
|
|
scrollYBitmapDstHi:
|
|
.byte $AD,$A2,$A2 ; 1DBA ... $AD,$A2,$A2 = high bytes of the destinations
|
|
|
|
; scrollYBitmapStrideLo: byteTable, 3 bytes. bitmap row stride lo ($FEC0 = -320 when scrolling up,
|
|
; $0140 = +320 otherwise)
|
|
scrollYBitmapStrideLo:
|
|
.byte $C0,$40,$40 ; 1DBD .@@ $C0,$40,$40 = low byte of the row stride -320/+320/+320 (one character row of the bitmap)
|
|
|
|
; scrollYBitmapStrideHi: byteTable, 3 bytes. bitmap row stride hi
|
|
scrollYBitmapStrideHi:
|
|
.byte $FE,$01,$01 ; 1DC0 ... $FE,$01,$01 = high byte of the same stride
|
|
|
|
; scrollYRowCount: byteTable, 3 bytes. character rows to copy (8,10,8)
|
|
scrollYRowCount:
|
|
.byte $08,$0A,$08 ; 1DC3 ... 8,10,8 character rows to copy: all 10 when only columns move, 8 when 2 rows are freed
|
|
|
|
; scrollXScreenStartIndex: byteTable, 3 bytes. start Y index of the screen/colour copy ($0B,0,0)
|
|
scrollXScreenStartIndex:
|
|
.byte $0B,$00,$00 ; 1DC6 ... $0B,$00,$00 = first Y index of the 14-character screen/colour copy
|
|
|
|
; scrollXScreenEndIndex: byteTable, 3 bytes. end Y index of the screen/colour copy ($FF,$0E,$0C)
|
|
scrollXScreenEndIndex:
|
|
.byte $FF,$0E,$0C ; 1DC9 ... $FF,$0E,$0C = index it stops at: $0B..0 down, 0..$0D up, 0..$0B up
|
|
|
|
; scrollXScreenSrcOffset: byteTable, 3 bytes. column offset of the screen source (0,0,2)
|
|
scrollXScreenSrcOffset:
|
|
.byte $00,$00,$02 ; 1DCC ... $00,$00,$02 = 2 characters added to the screen source when the view moves right
|
|
|
|
; scrollXScreenDstOffset: byteTable, 3 bytes. column offset of the screen destination (2,0,0)
|
|
scrollXScreenDstOffset:
|
|
.byte $02,$00,$00 ; 1DCF ... $02,$00,$00 = 2 characters added to the screen destination when the view moves left
|
|
|
|
; scrollYScreenSrcLo: byteTable, 3 bytes. screen RAM source address lo ($8D59 row 8, $8C41 row 1,
|
|
; $8C91 row 3; column 25)
|
|
scrollYScreenSrcLo:
|
|
.byte $59,$41,$91 ; 1DD2 YA. $59,$41,$91 = low byte of $8D59/$8C41/$8C91: screen RAM column 25 of char rows 8/1/3
|
|
|
|
; scrollYScreenSrcHi: byteTable, 3 bytes. screen RAM source address hi (colour RAM = +$4C00)
|
|
scrollYScreenSrcHi:
|
|
.byte $8D,$8C,$8C ; 1DD5 ... $8D,$8C,$8C = high bytes; the colour RAM copy uses the same addresses + $4C00
|
|
|
|
; scrollYScreenDstLo: byteTable, 3 bytes. screen RAM destination address lo ($8DA9, $8C41, $8C41)
|
|
scrollYScreenDstLo:
|
|
.byte $A9,$41,$41 ; 1DD8 .AA $A9,$41,$41 = low byte of $8DA9/$8C41/$8C41: column 25 of char rows 10/1/1
|
|
|
|
; scrollYScreenDstHi: byteTable, 3 bytes. screen RAM destination address hi
|
|
scrollYScreenDstHi:
|
|
.byte $8D,$8C,$8C ; 1DDB ... $8D,$8C,$8C = high bytes of the screen destinations
|
|
|
|
; scrollYScreenStrideLo: byteTable, 3 bytes. screen row stride lo ($FFD8 = -40 up, $0028 = +40)
|
|
scrollYScreenStrideLo:
|
|
.byte $D8,$28,$28 ; 1DDE .(( $D8,$28,$28 = low byte of the screen row stride -40/+40/+40
|
|
|
|
; scrollYScreenStrideHi: byteTable, 3 bytes. screen row stride hi
|
|
scrollYScreenStrideHi:
|
|
.byte $FF,$00,$00 ; 1DE1 ... $FF,$00,$00 = high byte of the screen row stride
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scrollViewBitmap - Shifts the whole 7x5-cell tactical view one map cell (16 pixels = 2 characters)
|
|
; in the direction left in scrollDeltaCol/scrollDeltaRow and then redraws the cells that scrolled into
|
|
; sight. Both deltas are first biased to 0/1/2 = -1/0/+1 and used to index the 22 tables at
|
|
; $1DA2; the values are poked into two self-modified copy loops - one moving 96 or 112 bitmap bytes
|
|
; per character row, one moving 12 or 14 bytes of screen RAM and of colour RAM - which are run once
|
|
; per character row (8 or 10 of them) with the row stride added between passes. A one-cell shift is
|
|
; 16 bitmap bytes or 2 characters, so a 12-character window is copied 2 characters over and the
|
|
; exposed column (5 cells) and/or row (7 cells) is drawn from the map.
|
|
; In: scrollDeltaCol/scrollDeltaRow (zp_F0/zp_F1) = -1/0/+1; viewOriginCol/Row, already updated by
|
|
; scrollViewBy
|
|
; Out: bitmap $A208.., screen RAM $8C41.. and colour RAM $D841.. scrolled and the new edge drawn;
|
|
; zp_F0/zp_F1 left biased to 0/1/2; zp_F2-zp_F7 and $54-$56 used as scratch. Self-modifies
|
|
; $1E81+1, $1E83+1/+2, $1E86+1/+2, $1E89, $1E8A+1, $1EB0+1, $1EB2+1/+2, $1EB5+1/+2, $1EB8+1/+2,
|
|
; $1EBB+1/+2, $1EBE and $1EBF+1
|
|
; Called from: scrollViewBy ($1D49, by JMP - this is that routine's tail)
|
|
; ----------------------------------------------------------------------
|
|
scrollViewBitmap:
|
|
inc scrollDeltaCol ; 1DE4 bias the column step -1/0/+1 into a table index 0/1/2
|
|
ldx scrollDeltaCol ; 1DE6 X selects the horizontal-shift parameters
|
|
inc scrollDeltaRow ; 1DE8 same for the row step
|
|
ldy scrollDeltaRow ; 1DEA Y selects the vertical-shift parameters
|
|
lda scrollXStartIndex,x ; 1DEC start index of the bitmap copy
|
|
sta L_1E81+1 ; 1DEF patch the LDY # that opens the bitmap loop
|
|
lda scrollXEndIndex,x ; 1DF2 end index of the bitmap copy
|
|
sta L_1E8A+1 ; 1DF5 patch the CPY # that closes it
|
|
lda scrollYBitmapSrcLo,y ; 1DF8 first bitmap row to read from
|
|
clc ; 1DFB plain add
|
|
adc scrollXBitmapSrcOffset,x; 1DFC +16 bytes = one map cell to the right on the source when the view scrolls right
|
|
sta L_1E83+1 ; 1DFF patch the low byte of the LDA abs,y source operand
|
|
lda scrollYBitmapSrcHi,y ; 1E02 high byte of the source row address
|
|
adc #$00 ; 1E05 take the carry out of the low byte
|
|
sta L_1E83+2 ; 1E07 patch the high byte of the source operand
|
|
lda scrollYBitmapDstLo,y ; 1E0A bitmap row to write to
|
|
clc ; 1E0D plain add
|
|
adc scrollXBitmapDstOffset,x; 1E0E +16 bytes on the destination instead when the view scrolls left
|
|
sta L_1E86+1 ; 1E11 patch the low byte of the STA abs,y destination operand
|
|
lda scrollYBitmapDstHi,y ; 1E14 high byte of the destination row address
|
|
adc #$00 ; 1E17 carry out of the low byte
|
|
sta L_1E86+2 ; 1E19 patch the high byte of the destination operand
|
|
lda scrollXStepOpcode,x ; 1E1C $88 DEY or $C8 INY
|
|
sta scrollBitmapStepOpcode ; 1E1F patch the step instruction of the bitmap loop
|
|
lda scrollYRowCount,y ; 1E22 8 or 10 character rows
|
|
sta scrollRowCount ; 1E25 zp_F6 counts the rows still to move
|
|
lda scrollYBitmapStrideLo,y ; 1E27 +320 or -320 = one character row of the bitmap
|
|
sta bitmapRowStride ; 1E2A zp_F2 low byte of the bitmap row stride
|
|
lda scrollYBitmapStrideHi,y ; 1E2C sign/high byte of the stride
|
|
sta bitmapRowStrideHi ; 1E2F zp_F3
|
|
lda scrollXScreenStartIndex,x; 1E31 start index of the screen/colour copy
|
|
sta L_1EB0+1 ; 1E34 patch the LDY # that opens the screen loop
|
|
lda scrollXScreenEndIndex,x ; 1E37 end index of the screen copy
|
|
sta L_1EBF+1 ; 1E3A patch its CPY #
|
|
lda scrollYScreenSrcLo,y ; 1E3D first screen RAM row to read from
|
|
clc ; 1E40 plain add
|
|
adc scrollXScreenSrcOffset,x; 1E41 +2 characters on the source when the view scrolls right
|
|
sta L_1EB2+1 ; 1E44 patch the screen RAM source operand (low)
|
|
sta L_1EB8+1 ; 1E47 the colour RAM source has the same low byte
|
|
lda scrollYScreenSrcHi,y ; 1E4A high byte of the screen row address ($8C/$8D)
|
|
adc #$00 ; 1E4D carry out of the low byte
|
|
sta L_1EB2+2 ; 1E4F patch the screen RAM source operand (high) - screen RAM is at $8C00
|
|
adc #$4C ; 1E52 +$4C00 turns a screen RAM address into the matching colour RAM address ($D800)
|
|
sta L_1EB8+2 ; 1E54 patch the colour RAM source operand (high)
|
|
lda scrollYScreenDstLo,y ; 1E57 screen RAM row to write to
|
|
clc ; 1E5A plain add
|
|
adc scrollXScreenDstOffset,x; 1E5B +2 characters on the destination instead when the view scrolls left
|
|
sta L_1EB5+1 ; 1E5E patch the screen RAM destination operand (low)
|
|
sta L_1EBB+1 ; 1E61 colour RAM destination shares the low byte
|
|
lda scrollYScreenDstHi,y ; 1E64 high byte of the destination row
|
|
adc #$00 ; 1E67 carry out of the low byte
|
|
sta L_1EB5+2 ; 1E69 patch the screen RAM destination operand (high)
|
|
adc #$4C ; 1E6C same $4C00 bias into colour RAM
|
|
sta L_1EBB+2 ; 1E6E patch the colour RAM destination operand (high)
|
|
lda scrollXStepOpcode,x ; 1E71 $88 DEY or $C8 INY again
|
|
sta scrollScreenStepOpcode ; 1E74 patch the step instruction of the screen loop
|
|
lda scrollYScreenStrideLo,y ; 1E77 +40 or -40 = one screen row
|
|
sta screenRowStride ; 1E7A zp_F4 low byte of the screen row stride
|
|
lda scrollYScreenStrideHi,y ; 1E7C sign/high byte
|
|
sta screenRowStrideHi ; 1E7F zp_F5
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One character row of the view: move 96 bytes (a 12-character window shifted by one map cell) or all
|
|
; 112 bytes of the row in the bitmap, then the matching 12 or 14 bytes of screen RAM and of colour
|
|
; RAM. Every operand that the listing shows as irqVectorHi ($FFFF) is a placeholder that was
|
|
; rewritten above, and the INY at $1E89/$1EBE may have been replaced by a DEY, so that a shifted copy
|
|
; walks away from the overlap instead of into it.
|
|
; ----------------------------------------------------------------------
|
|
L_1E81:
|
|
ldy #$FF ; 1E81 start index, patched at $1DEF ($5F downwards, or 0 upwards)
|
|
L_1E83:
|
|
lda irqVectorHi,y ; 1E83 source byte of this bitmap row (operand patched at $1DFF/$1E07)
|
|
L_1E86:
|
|
sta irqVectorHi,y ; 1E86 destination byte (operand patched at $1E11/$1E19)
|
|
scrollBitmapStepOpcode:
|
|
iny ; 1E89 INY or DEY, patched at $1E1F: copy forwards or backwards so a shift cannot eat itself
|
|
L_1E8A:
|
|
cpy #$FF ; 1E8A end index, patched at $1DF5
|
|
bne L_1E83 ; 1E8C next byte of this character row (96 or 112 bytes)
|
|
lda L_1E86+1 ; 1E8E step the destination pointer one character row on
|
|
clc ; 1E91 plain add
|
|
adc bitmapRowStride ; 1E92 +320 or -320
|
|
sta L_1E86+1 ; 1E94 write the new low byte back into the STA operand
|
|
lda L_1E86+2 ; 1E97 high byte of the destination
|
|
adc bitmapRowStrideHi ; 1E9A with the sign of the stride
|
|
sta L_1E86+2 ; 1E9C write it back
|
|
lda L_1E83+1 ; 1E9F the same for the source pointer
|
|
clc ; 1EA2 plain add
|
|
adc bitmapRowStride ; 1EA3 +320 or -320
|
|
sta L_1E83+1 ; 1EA5 write the new low byte into the LDA operand
|
|
lda L_1E83+2 ; 1EA8 high byte of the source
|
|
adc bitmapRowStrideHi ; 1EAB with the sign of the stride
|
|
sta L_1E83+2 ; 1EAD write it back
|
|
L_1EB0:
|
|
ldy #$FF ; 1EB0 start index of the character copy, patched at $1E34
|
|
L_1EB2:
|
|
lda irqVectorHi,y ; 1EB2 screen RAM source (the tile's colour nibbles), operand patched at $1E44/$1E4F
|
|
L_1EB5:
|
|
sta irqVectorHi,y ; 1EB5 screen RAM destination, patched at $1E5E/$1E69
|
|
L_1EB8:
|
|
lda irqVectorHi,y ; 1EB8 the same character in colour RAM at $D800, patched at $1E47/$1E54
|
|
L_1EBB:
|
|
sta irqVectorHi,y ; 1EBB colour RAM destination, patched at $1E61/$1E6E
|
|
scrollScreenStepOpcode:
|
|
iny ; 1EBE INY or DEY, patched at $1E74
|
|
L_1EBF:
|
|
cpy #$FF ; 1EBF end index, patched at $1E3A
|
|
bne L_1EB2 ; 1EC1 next character of this row (12 or 14 of them)
|
|
lda L_1EB5+1 ; 1EC3 step the screen destination one row on
|
|
clc ; 1EC6 plain add
|
|
adc screenRowStride ; 1EC7 +40 or -40
|
|
sta L_1EB5+1 ; 1EC9 write it back into the screen STA operand
|
|
sta L_1EBB+1 ; 1ECC and into the colour RAM STA operand, which shares the low byte
|
|
lda L_1EB5+2 ; 1ECF high byte of the screen destination
|
|
adc screenRowStrideHi ; 1ED2 with the sign of the stride
|
|
sta L_1EB5+2 ; 1ED4 write it back
|
|
clc ; 1ED7 prepare the colour RAM bias
|
|
adc #$4C ; 1ED8 screen page + $4C = colour RAM page
|
|
sta L_1EBB+2 ; 1EDA write the colour RAM destination high byte
|
|
lda L_1EB2+1 ; 1EDD step the screen source one row on
|
|
clc ; 1EE0 plain add
|
|
adc screenRowStride ; 1EE1 +40 or -40
|
|
sta L_1EB2+1 ; 1EE3 write it back into the screen LDA operand
|
|
sta L_1EB8+1 ; 1EE6 and into the colour RAM LDA operand
|
|
lda L_1EB2+2 ; 1EE9 high byte of the screen source
|
|
adc screenRowStrideHi ; 1EEC with the sign of the stride
|
|
sta L_1EB2+2 ; 1EEE write it back
|
|
clc ; 1EF1 prepare the colour RAM bias
|
|
adc #$4C ; 1EF2 +$4C pages = $D800
|
|
sta L_1EB8+2 ; 1EF4 write the colour RAM source high byte
|
|
dec scrollRowCount ; 1EF7 one character row of the view is done
|
|
bne L_1E81 ; 1EF9 8 or 10 rows in all
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The cells that scrolled into sight still hold the old picture: redraw the newly exposed column (5
|
|
; cells) and/or the newly exposed row (7 cells) straight from the map. $54/$55/$56 are the cell byte
|
|
; / column / row triple that drawViewportCell expects.
|
|
; ----------------------------------------------------------------------
|
|
lda viewOriginCol ; 1EFB left edge of the view
|
|
ldx scrollDeltaCol ; 1EFD 0/1/2 = the view moved left / not at all / right
|
|
cpx #$01 ; 1EFF 1 = no horizontal movement
|
|
beq L_1F24 ; 1F01 nothing new appeared at the sides
|
|
bcc L_1F08 ; 1F03 moved left: the freshly exposed column is the left edge itself
|
|
clc ; 1F05 plain add
|
|
adc #$06 ; 1F06 moved right: it is the 7th (rightmost) column of the view
|
|
L_1F08:
|
|
sta savedRegX ; 1F08 $55 = column of the cell to draw
|
|
lda viewOriginRow ; 1F0A top row of the view
|
|
sta savedRegY ; 1F0C $56 = row of the cell to draw
|
|
lda #$05 ; 1F0E the view is 5 cells tall
|
|
sta redrawCellCount ; 1F10 zp_F7 counts the cells still to draw
|
|
L_1F12:
|
|
ldx savedRegX ; 1F12 X = map column
|
|
ldy savedRegY ; 1F14 Y = map row
|
|
jsr readMapCell ; 1F16 read the map byte: terrain code, or $80 | unit index when a unit stands there
|
|
sta savedRegA ; 1F19 $54 = the cell byte drawViewportCell expects
|
|
jsr drawViewportCell ; 1F1B render the 16x16 pixel tile into the bitmap
|
|
inc savedRegY ; 1F1E next row down the exposed column
|
|
dec redrawCellCount ; 1F20 one cell done
|
|
bne L_1F12 ; 1F22 5 cells of the column
|
|
L_1F24:
|
|
lda viewOriginRow ; 1F24 top row of the view
|
|
ldy scrollDeltaRow ; 1F26 0/1/2 = the view moved up / not at all / down
|
|
cpy #$01 ; 1F28 1 = no vertical movement
|
|
beq L_1F4D ; 1F2A nothing new appeared above or below: done
|
|
bcc L_1F31 ; 1F2C moved up: the freshly exposed row is the top edge itself
|
|
clc ; 1F2E plain add
|
|
adc #$04 ; 1F2F moved down: it is the 5th (bottom) row of the view
|
|
L_1F31:
|
|
sta savedRegY ; 1F31 $56 = row of the cells to draw
|
|
lda viewOriginCol ; 1F33 start at the left edge
|
|
sta savedRegX ; 1F35 $55 = column of the cell to draw
|
|
lda #$07 ; 1F37 the view is 7 cells wide
|
|
sta redrawCellCount ; 1F39 zp_F7 counts them down
|
|
L_1F3B:
|
|
ldx savedRegX ; 1F3B X = map column
|
|
ldy savedRegY ; 1F3D Y = map row
|
|
jsr readMapCell ; 1F3F read the map byte
|
|
sta savedRegA ; 1F42 $54 = cell byte
|
|
jsr drawViewportCell ; 1F44 draw the 16x16 tile
|
|
inc savedRegX ; 1F47 next column along the exposed row
|
|
dec redrawCellCount ; 1F49 one cell done
|
|
bne L_1F3B ; 1F4B 7 cells of the row
|
|
L_1F4D:
|
|
rts ; 1F4D the view now matches the map again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; keepGroupBoxInsideMap - Nudges the view origin one cell at a time until the whole group formation
|
|
; box (the cursor cell plus the signed edge offsets $922E-$9231 built by buildGroupFormationSprite)
|
|
; fits inside the map: 40 columns wide and, vertically, inside the row window set by
|
|
; setViewVerticalLimit. Every nudge restarts the four edge tests; if at least one nudge was needed
|
|
; the view is repainted.
|
|
; In: viewOriginCol/Row (zp_A5/zp_A6); groupBoxLeftOffset/RightOffset/TopOffset/BottomOffset
|
|
; ($922F/$922E/$9231/$9230); gamePhase
|
|
; Out: viewOriginCol/Row corrected, viewRowLimit set, zp_18/zp_19 scratch; tail-jumps to drawViewport
|
|
; when anything moved
|
|
; Called from: buildGroupFormationSprite ($319D), right after the box offsets are computed
|
|
; ----------------------------------------------------------------------
|
|
keepGroupBoxInsideMap:
|
|
lda #$01 ; 1F4E 1 so the first DEC below leaves 0
|
|
sta scratch19 ; 1F50 zp_19 = 0 means 'the view has not been nudged yet'
|
|
L_1F52:
|
|
dec scratch19 ; 1F52 count this pass; any pass after the first leaves zp_19 non-zero
|
|
ldy viewOriginRow ; 1F54 current top row of the view
|
|
jsr setViewVerticalLimit ; 1F56 zp_F8 = 40 or 20; Y comes back biased by -20 in the setup phase
|
|
tya ; 1F59 top row
|
|
clc ; 1F5A plain add
|
|
adc #$02 ; 1F5B cursor row = top edge + 2
|
|
sta scratch18 ; 1F5D zp_18 keeps the cursor row for the second test
|
|
clc ; 1F5F plain add
|
|
adc groupBoxTopOffset ; 1F60 $9231 = signed offset to the top edge of the group box
|
|
cmp viewRowLimit ; 1F63 still inside the allowed rows?
|
|
bcc L_1F6C ; 1F65 yes: test the bottom edge
|
|
inc viewOriginRow ; 1F67 box sticks out above the map: slide the view one row down
|
|
jmp L_1F52 ; 1F69 and start the four tests again
|
|
|
|
L_1F6C:
|
|
lda scratch18 ; 1F6C cursor row again
|
|
clc ; 1F6E plain add
|
|
adc groupBoxBottomOffset ; 1F6F $9230 = signed offset to the bottom edge of the box
|
|
cmp viewRowLimit ; 1F72 still inside the allowed rows?
|
|
bcc L_1F7B ; 1F74 yes: go on to the columns
|
|
dec viewOriginRow ; 1F76 box sticks out below: slide the view one row up
|
|
jmp L_1F52 ; 1F78 and re-test
|
|
|
|
L_1F7B:
|
|
lda viewOriginCol ; 1F7B current left column of the view
|
|
clc ; 1F7D plain add
|
|
adc #$03 ; 1F7E cursor column = left edge + 3
|
|
sta scratch18 ; 1F80 zp_18 keeps the cursor column
|
|
clc ; 1F82 plain add
|
|
adc groupBoxLeftOffset ; 1F83 $922F = signed offset to the left edge of the box
|
|
cmp #$28 ; 1F86 40 map columns
|
|
bcc L_1F8F ; 1F88 inside: test the right edge
|
|
inc viewOriginCol ; 1F8A box sticks out to the left: slide the view one column right
|
|
jmp L_1F52 ; 1F8C and re-test
|
|
|
|
L_1F8F:
|
|
lda scratch18 ; 1F8F cursor column again
|
|
clc ; 1F91 plain add
|
|
adc groupBoxRightOffset ; 1F92 $922E = signed offset to the right edge of the box
|
|
cmp #$28 ; 1F95 40 map columns
|
|
bcc L_1F9E ; 1F97 the whole box fits: done
|
|
dec viewOriginCol ; 1F99 box sticks out to the right: slide the view one column left
|
|
jmp L_1F52 ; 1F9B and re-test
|
|
|
|
L_1F9E:
|
|
lda scratch19 ; 1F9E 0 only if the box already fitted on the very first pass
|
|
beq L_1FA5 ; 1FA0 nothing moved: leave the screen alone
|
|
jmp drawViewport ; 1FA2 the view moved: repaint all 35 cells and return from there
|
|
|
|
L_1FA5:
|
|
rts ; 1FA5 view origin now keeps the whole group box on the map
|
|
|
|
|
|
; joystickDirectionTable - joystick nibble to direction code. The raster IRQ latches bits 0-4 of CIA1
|
|
; port A (joystick 2) into zp_5D, active low, so $0F means centred: bit 0 up, bit 1 down, bit 2 left,
|
|
; bit 3 right. The table maps that nibble to a direction code 0-7 (0 N, 1 S, 2 E, 3 W, 4 NW, 5 NE, 6
|
|
; SE, 7 SW) or to $FF for 'no usable direction'. Logically it is 16 bytes; the last four
|
|
; ($FF,$01,$00,$FF for nibbles $0C-$0F) were split off by the disassembler and are the first four
|
|
; bytes of the block labelled diagonalDirectionTable.
|
|
joystickDirectionTable:
|
|
.byte $FF,$FF,$FF,$FF,$FF,$06,$05,$02; 1FA6 ........ nibbles $00-$07: only $05 (down+right) -> 6 SE, $06 (up+right) -> 5 NE and $07 (right) -> 2 E are real
|
|
.byte $FF,$07,$04,$03 ; 1FAE ....
|
|
|
|
; $1FB2-$1FB5 are still joystickDirectionTable (nibble $0C impossible, $0D = down -> 1 S, $0E = up ->
|
|
; 0 N, $0F = centred -> $FF). diagonalDirectionTable proper is $1FB6-$1FB9: for direction codes 4-7 it
|
|
; packs (index of the column delta << 4) | index of the row delta into directionDeltaTable - NW =
|
|
; (-1,-1), NE = (+1,-1), SE = (+1,+1), SW = (-1,+1). scrollViewportByDirection ($2111), the comcen
|
|
; screens ($736F, $76CF) and the drone AI ($EF58) all index it with the direction code, so only
|
|
; offsets 4-7 are ever read. The eight bytes at $1FBA (01 00 03 02 06 07 04 05) are an
|
|
; opposite-direction table - N<->S, E<->W, NW<->SE, NE<->SW - that no unit references; together with
|
|
; the $00 pad at $1FC2 they look like leftover data.
|
|
diagonalDirectionTable:
|
|
.byte $FF,$01,$00,$FF,$30,$20,$21,$31; 1FB2 ....0 !1 $1FB2-$1FB5 finish joystickDirectionTable; $1FB6-$1FB9 = diagonals 4-7; $1FBA-$1FC1 unused
|
|
.byte $01,$00,$03,$02,$06,$07,$04,$05; 1FBA ........
|
|
.byte $00 ; 1FC2 .
|
|
|
|
; ----------------------------------------------------------------------
|
|
; enterMapScreen - Entry point of the battlefield screen (game mode 0). Restores the viewport origin
|
|
; the player left it at and repaints the whole screen - the three frames, the strategic mini-map, the
|
|
; tactical view and the status bar - then falls straight into runMapMainLoop.
|
|
; In: savedViewOriginCol/Row ($92C3/$92C4)
|
|
; Out: viewOriginCol/Row set, screen redrawn; does not return - falls into runMapMainLoop
|
|
; Called from: switchToRequestedScreen ($06F5, by JMP, when F1 selects screen 0)
|
|
; ----------------------------------------------------------------------
|
|
enterMapScreen:
|
|
lda savedViewOriginCol ; 1FC3 $92C3, written every pass of the map main loop
|
|
sta viewOriginCol ; 1FC6 zp_A5 = map column of the left edge of the 7x5 view
|
|
lda savedViewOriginRow ; 1FC8 $92C4
|
|
sta viewOriginRow ; 1FCB zp_A6 = map row of the top edge of the view
|
|
jsr redrawGameScreen ; 1FCD rebuild frames, mini-map, tactical view and status bar, then fall into the main loop
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMapMainLoop - The interactive loop of the battlefield screen and of the unit-placement phase. It
|
|
; resets the cursor state, draws the view, and then loops: one game frame, then hover handling
|
|
; (resting 15 frames on one of your own units pops up the info panel) and click handling (a click
|
|
; selects the unit, or destroys it during placement while DESTROY mode is armed), then joystick
|
|
; scrolling with auto-repeat. During the battle (gamePhase = 0) an F1/F3/F5/F7 request is honoured by
|
|
; switchToRequestedScreen, which rebuilds the stack and jumps to the new screen instead of returning;
|
|
; during unit placement the overlay-A '*' hook is polled every pass and the routine returns when F1 is
|
|
; pressed or the phase ends. Units of the other side are filtered out by two 'cmp #$32' tests whose
|
|
; following branch opcode ($206B, $2084) is written at run time from the local player's side.
|
|
; In: gamePhase (zp_B1), joyDirection (zp_5D), inputIdleFrames ($90EE), requestedScreenKey (zp_66),
|
|
; gameEndCountdown ($92C9), revealAllUnits ($92FC), filmPlaybackMode ($0B9B), comcenUnit ($9236),
|
|
; comcenCloakedFlag ($9245), setupToggleFlag ($92F3), the unit arrays and the map
|
|
; Out: savedViewOriginCol/Row kept up to date; selectedUnit ($90F8); info panel, cursor sprites and
|
|
; view redrawn; command $91 queued in DESTROY mode; returns only from the placement phase
|
|
; Called from: enterMapScreen (fall-through), startGameFromSetup ($0B7A), the screen dispatcher
|
|
; ($075E) and the setup phase of overlay A ($7B4E)
|
|
; ----------------------------------------------------------------------
|
|
runMapMainLoop:
|
|
lda #$FF ; 1FD0 no key
|
|
sta lastTypedKey ; 1FD2 $9248: forget anything typed before the battlefield came up
|
|
sta infoPanelUnit ; 1FD5 $91D7 = no unit is being shown in the info panel
|
|
lda #$00 ; 1FD8 clear
|
|
sta pathMarkerState ; 1FDA $24E1 = the mini-map path marker has no passes left to run
|
|
sta cursorTargetMode ; 1FDD zp_BB = 0: the cursor is a plain move cursor, not a target picker
|
|
sta screenBuiltFlag ; 1FDF zp_41, a spare slot of the saved text state; nothing ever reads it (dead store)
|
|
lda savedViewOriginCol ; 1FE1 return to the part of the map the player was looking at
|
|
sta viewOriginCol ; 1FE4 zp_A5
|
|
lda savedViewOriginRow ; 1FE6 $92C4
|
|
sta viewOriginRow ; 1FE9 zp_A6
|
|
jsr showCursorBoxSprite ; 1FEB sprite 2 = the expanded box that frames the centre cell of the view
|
|
lda gamePhase ; 1FEE 0 = the battle proper, non-zero = unit placement
|
|
beq L_1FFA ; 1FF0 battle: no standing message
|
|
lda #$0A ; 1FF2 message 10 = 'SETUP UNITS. PRESS F 1 WHEN DONE.'
|
|
sta persistentMessageId ; 1FF4 $92A1 = the message the status line falls back to
|
|
jsr queueMessage ; 1FF7 and show it right now as well
|
|
L_1FFA:
|
|
lda #$00 ; 1FFA clear
|
|
sta uiLevel ; 1FFC $9209 = 0: just browsing, no unit or group selected
|
|
sta activeSpriteIndex ; 1FFF $91CC = sprite 0 for positionCursorSprite
|
|
sta textColour ; 2002 zp_3D = 0 so the panel is cleared to black
|
|
jsr clearInfoPanel ; 2004 blank the info panel window (rows 12-20, columns 25-38)
|
|
lda #$FF ; 2007 no highlight
|
|
sta highlightedUnit ; 2009 $9204 = no unit is drawn inverted in the view
|
|
sta highlightedGroupKey ; 200C $9205 = no group is drawn inverted either
|
|
jsr drawViewport ; 200F repaint the 7x5 tactical view from the map
|
|
jsr positionCursorSprite ; 2012 put the mini-map cursor sprite on the centre cell
|
|
jsr setCursorShapeSquare ; 2015 sprite 0 = the hollow square cursor
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Main loop. One pass is one game frame plus one look at the joystick.
|
|
; ----------------------------------------------------------------------
|
|
L_2018:
|
|
lda viewOriginCol ; 2018 remember where the player is looking...
|
|
sta savedViewOriginCol ; 201A $92C3, so the console and drone screens can come back to this spot
|
|
lda viewOriginRow ; 201D same for the row
|
|
sta savedViewOriginRow ; 201F $92C4
|
|
lda gamePhase ; 2022 which phase?
|
|
beq L_2033 ; 2024 0 = the battle proper
|
|
jsr callSetupOverlayHook ; 2026 unit placement: let overlay A's '*' handler ($8658) toggle DESTROY mode
|
|
lda requestedScreenKey ; 2029 zp_66: 0-3 = F1/F3/F5/F7 pressed, $FF = none
|
|
beq L_2032 ; 202B F1 means 'finished placing units'
|
|
jsr checkSetupUiContinue ; 202D may the placement UI keep running?
|
|
bne L_2036 ; 2030 yes: carry on
|
|
L_2032:
|
|
rts ; 2032 back to the setup code in overlay A
|
|
|
|
L_2033:
|
|
jsr switchToRequestedScreen ; 2033 battle: F1/F3/F5/F7 switch screens, and that call never returns
|
|
L_2036:
|
|
jsr updateGameFrame ; 2036 one game frame: engine step, messages, chat, sound and the link
|
|
lda joyDirection ; 2039 joystick nibble latched by the IRQ
|
|
cmp #$0F ; 203B $0F = centred
|
|
beq L_2042 ; 203D resting on a cell: look at what is under the cursor
|
|
jmp L_20D7 ; 203F stick deflected: straight to the scroll handler
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Hovering: after 15 idle frames on one of your own units the info panel is opened. Everything below
|
|
; runs only while the stick is centred.
|
|
; ----------------------------------------------------------------------
|
|
L_2042:
|
|
lda inputIdleFrames ; 2042 $90EE = frames since the last stick or button activity
|
|
cmp #$0F ; 2045 15 frames, about a quarter of a second
|
|
bcc L_2076 ; 2047 not hovering long enough yet
|
|
jsr readCellUnderCursor ; 2049 map byte under the cursor cell (viewOriginCol+3, viewOriginRow+2)
|
|
bpl L_2076 ; 204C bit 7 clear = plain terrain, no unit to describe
|
|
ldy gameEndCountdown ; 204E $92C9 counts down while the end-of-game sequence runs
|
|
bne L_2076 ; 2051 do not open panels while the game is ending
|
|
and #$7F ; 2053 low 7 bits of an occupied cell = the unit index
|
|
bit revealAllUnits ; 2055 $92FC bit 7 = beginner display, every enemy unit visible
|
|
bmi L_206D ; 2058 then any unit may be inspected
|
|
bit filmPlaybackMode ; 205A $0B9B bit 7 = watching a game film
|
|
bmi L_206D ; 205D a film viewer may inspect both sides
|
|
cmp comcenUnit ; 205F $9236 = your own COMCEN's unit index (49 or 99)
|
|
beq L_206D ; 2062 your comcen can always be inspected
|
|
ldy comcenCloakedFlag ; 2064 $9245 bit 7 = your own comcen is cloaked
|
|
bmi L_2076 ; 2067 a cloaked comcen is off the air: no unit reports
|
|
cmp #$32 ; 2069 50 = the boundary between the two sides' halves of the unit arrays
|
|
D_206B:
|
|
bcs L_2076 ; 206B opcode patched by patchOwnSideBranches ($7BE6) to BCC or BCS: skip the opponent's units
|
|
L_206D:
|
|
sta selectedUnit ; 206D $90F8 = the unit the panel will describe
|
|
jsr showUnitInfoPanel ; 2070 print its stats and hold them while the cursor rests on the unit
|
|
jsr consumeSpaceKey ; 2073 if SPACE closed the panel, swallow the key so nothing else acts on it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fire button: select one of your own units, or - during unit placement with DESTROY mode armed - send
|
|
; the self-destruct command for it.
|
|
; ----------------------------------------------------------------------
|
|
L_2076:
|
|
jsr checkFirePressedInteractive; 2076 A = 0 only for a fresh click that counts (not in a film, not after game over)
|
|
bne L_20D7 ; 2079 no click: fall through to scrolling
|
|
jsr readCellUnderCursor ; 207B map byte under the cursor
|
|
bpl L_20D7 ; 207E clicked on empty terrain
|
|
and #$7F ; 2080 unit index under the cursor
|
|
cmp #$32 ; 2082 50 = the side boundary again
|
|
D_2084:
|
|
bcs L_20D7 ; 2084 opcode patched by patchOwnSideBranches ($7BE9): ignore clicks on the opponent's units
|
|
sta selectedUnit ; 2086 $90F8 = the clicked unit
|
|
tay ; 2089 Y = unit index for the array lookups
|
|
lda unitStunTable,y ; 208A $F960: bits 0-2 = stun counter, bits 5-7 = dug-in state
|
|
and #$07 ; 208D keep the stun counter
|
|
bne L_20D7 ; 208F a stunned unit takes no orders
|
|
cpy comcenUnit ; 2091 is this the comcen itself?
|
|
beq L_209B ; 2094 the comcen is always reachable
|
|
bit comcenCloakedFlag ; 2096 $9245 bit 7 = own comcen cloaked
|
|
bmi L_20D7 ; 2099 with the comcen cloaked you cannot give orders
|
|
L_209B:
|
|
lda gamePhase ; 209B battle or unit placement?
|
|
beq L_20CC ; 209D battle: open the unit selection / options flow
|
|
lda setupToggleFlag ; 209F $92F3 bit 7 = DESTROY mode, armed with '*' during placement
|
|
bpl L_20CC ; 20A2 normal placement click: pick the unit up instead
|
|
lda #$00 ; 20A4 clear
|
|
sta setupToggleFlag ; 20A6 DESTROY is one-shot: disarm it again
|
|
lda unitDisplayFlagsTable,y ; 20A9 $FB54: bit 6 = the unit is in a group, bits 2-4 = which group
|
|
and #$40 ; 20AC test the 'in a group' bit
|
|
beq L_20B4 ; 20AE a loner: destroy just this unit
|
|
tya ; 20B0 unit index
|
|
ora #$80 ; 20B1 bit 7 of the argument means 'the whole group this unit belongs to'
|
|
tay ; 20B3 back into Y
|
|
L_20B4:
|
|
sty cmdParam0 ; 20B4 zp_73 = the command's argument byte
|
|
lda #$91 ; 20B6 command $91 = self destruct
|
|
jsr queueCmd2 ; 20B8 queue the 2-byte command for the engine and for the opponent
|
|
lda #$0A ; 20BB back to message 10, 'SETUP UNITS. PRESS F 1 WHEN DONE.'
|
|
sta persistentMessageId ; 20BD $92A1 = fallback message
|
|
sta shownMessageId ; 20C0 zp_7B = the message on the status line right now
|
|
lda #$3C ; 20C2 60 frames = one second
|
|
sta joyRepeatTimer ; 20C4 zp_6C: no scrolling for a second
|
|
sta inputLockoutTimer ; 20C6 $0B7D: the IRQ ignores stick and keys too, so one click destroys exactly one unit
|
|
jmp L_20D7 ; 20C9 join the scroll handler
|
|
|
|
L_20CC:
|
|
jsr handleOwnUnitClick ; 20CC battle: unit or group selection, info panel, destination or options menu
|
|
lda #$FF ; 20CF no key
|
|
sta lastTypedKey ; 20D1 drop whatever key the menus left behind
|
|
jmp L_1FFA ; 20D4 rebuild the browsing state: uiLevel 0, panel cleared, view repainted
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Joystick scrolling with auto-repeat. Reached from every path above, so a click and a scroll can
|
|
; never happen in the same pass.
|
|
; ----------------------------------------------------------------------
|
|
L_20D7:
|
|
lda joyRepeatTimer ; 20D7 zp_6C, the auto-repeat delay counted down by the IRQ
|
|
bne L_20ED ; 20D9 still inside the delay: do not scroll yet
|
|
ldy joyDirection ; 20DB the latched joystick nibble
|
|
lda #$00 ; 20DD clear
|
|
sta inputLockoutTimer ; 20DF $0B7D = 0: stick and keyboard are live again
|
|
lda joystickDirectionTable,y; 20E2 nibble -> direction code 0-7, or $FF when centred/impossible
|
|
bmi L_20ED ; 20E5 no usable direction
|
|
jsr setJoystickRepeatDelay ; 20E7 16 frames before the first repeat, 6 between repeats
|
|
jsr scrollViewportByDirection; 20EA move the view one cell in that direction
|
|
L_20ED:
|
|
jmp L_2018 ; 20ED next pass of the main loop
|
|
|
|
|
|
; directionDeltaTable - the signed one-cell steps shared by every direction decoder in the game (the
|
|
; comcen missile screen and the drone AI read the same four bytes): index 0 = row delta for north, 1 =
|
|
; row delta for south, 2 = column delta for east, 3 = column delta for west.
|
|
directionDeltaTable:
|
|
.byte $FF,$01,$01,$FF ; 20F0 .... $FF,$01,$01,$FF = -1 north, +1 south, +1 east, -1 west
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scrollViewportByDirection - Turns a direction code 0-7 into a column/row step and scrolls the view
|
|
; one cell with scrollViewBy. Codes 0-3 move on one axis only; codes 4-7 read the packed byte in
|
|
; diagonalDirectionTable for both axes and stretch the auto-repeat delay by 1.5 so a diagonal does not
|
|
; cover ground faster than a straight move. In the range-limited target mode (cursorTargetMode
|
|
; positive, i.e. aiming a BOOMER) the step is only taken if the new cursor cell is still inside the
|
|
; selected unit's firing range.
|
|
; In: A = direction code 0-7; joyRepeatTimer (zp_6C); cursorTargetMode (zp_BB); selectedUnit
|
|
; ($90F8); viewOriginCol/Row; unitTerrainUnderTable ($FAF0)
|
|
; Out: view scrolled; joyRepeatTimer stretched for diagonals; fireDebounceTimer (zp_6D) = 10;
|
|
; fireButtonArmed (zp_79) = 0; zp_22/zp_24/zp_27/zp_28 scratch. Self-modifies the LDX/LDY
|
|
; operands at $2175+1 and $2177+1
|
|
; Called from: runMapMainLoop ($20EA), runTargetCursorLoop ($2233) and the console tab loop ($6435)
|
|
; ----------------------------------------------------------------------
|
|
scrollViewportByDirection:
|
|
ldy #$00 ; 20F4 default row step 0
|
|
ldx #$00 ; 20F6 default column step 0
|
|
cmp #$04 ; 20F8 codes 4-7 are the diagonals
|
|
bcs L_2110 ; 20FA handle them separately
|
|
cmp #$02 ; 20FC codes 2-3 are east and west
|
|
bcs L_2108 ; 20FE those move on the column axis
|
|
tay ; 2100 codes 0-1 (north/south) index the delta table directly
|
|
lda directionDeltaTable,y ; 2101 -1 for north, +1 for south
|
|
tay ; 2104 Y = row step
|
|
jmp L_212E ; 2105 column step stays 0
|
|
|
|
L_2108:
|
|
tax ; 2108 codes 2-3 select entries 2-3
|
|
lda directionDeltaTable,x ; 2109 +1 for east, -1 for west
|
|
tax ; 210C X = column step
|
|
jmp L_212E ; 210D row step stays 0
|
|
|
|
L_2110:
|
|
tay ; 2110 direction code 4-7
|
|
lda diagonalDirectionTable,y; 2111 $30 NW, $20 NE, $21 SE, $31 SW = (column delta index << 4) | row delta index
|
|
pha ; 2114 keep the packed byte
|
|
and #$0F ; 2115 low nibble = index of the row delta
|
|
tay ; 2117 into Y
|
|
lda directionDeltaTable,y ; 2118 -1 or +1
|
|
tay ; 211B Y = row step
|
|
pla ; 211C packed byte back
|
|
lsr a ; 211D shift the high nibble down...
|
|
lsr a ; 211E ...
|
|
lsr a ; 211F ...
|
|
lsr a ; 2120 = index of the column delta
|
|
tax ; 2121 into X
|
|
lda directionDeltaTable,x ; 2122 -1 or +1
|
|
tax ; 2125 X = column step
|
|
lda joyRepeatTimer ; 2126 current auto-repeat delay
|
|
lsr a ; 2128 half of it
|
|
clc ; 2129 plain add
|
|
adc joyRepeatTimer ; 212A delay * 1.5
|
|
sta joyRepeatTimer ; 212C a diagonal step covers more ground, so it repeats more slowly
|
|
|
|
; ----------------------------------------------------------------------
|
|
; In BOOMER target mode the cursor may not leave the unit's firing range, so the step is tried out on
|
|
; paper first: squared distance from the unit to the cell we would move to, against r*(r+1) for the
|
|
; unit's range.
|
|
; ----------------------------------------------------------------------
|
|
L_212E:
|
|
lda cursorTargetMode ; 212E zp_BB: 0 = move order, >0 = range-limited target, <0 = free cell pick
|
|
beq L_2179 ; 2130 move order: scroll freely
|
|
bmi L_2179 ; 2132 free cell pick: scroll freely
|
|
stx L_2175+1 ; 2134 self-modifying: stash the column step in the LDX # operand at $2175+1
|
|
sty L_2177+1 ; 2137 and the row step in the LDY # operand at $2177+1
|
|
txa ; 213A column step
|
|
clc ; 213B plain add
|
|
adc viewOriginCol ; 213C left edge of the view
|
|
clc ; 213E plain add
|
|
adc #$03 ; 213F +3 = the cursor cell after the move
|
|
sta cellColA ; 2141 zp_22 = column of point A for distanceSquared
|
|
tya ; 2143 row step
|
|
clc ; 2144 plain add
|
|
adc viewOriginRow ; 2145 top edge of the view
|
|
clc ; 2147 plain add
|
|
adc #$02 ; 2148 +2 = the cursor row after the move
|
|
sta cellRowA ; 214A zp_24 = row of point A
|
|
ldy selectedUnit ; 214C the unit that is doing the shooting
|
|
lda unitColTable,y ; 214F $F640+unit
|
|
sta cellColB ; 2152 zp_27 = column of point B (the unit)
|
|
lda unitRowTable,y ; 2154 $F6A4+unit
|
|
sta cellRowB ; 2157 zp_28 = row of point B
|
|
jsr distanceSquared ; 2159 A = dx*dx + dy*dy, each delta capped at 15 and the sum at $FF
|
|
sta cellColA ; 215C reuse zp_22 to hold that squared distance
|
|
ldx selectedUnit ; 215E the firing unit again
|
|
ldy unitTerrainUnderTable,x ; 2161 $FAF0 = the terrain code hidden underneath the unit
|
|
ldx D_077E,y ; 2164 $077E indexed by the raw cell value = terrain class 0-8
|
|
lda terrainSightBonusTable,x; 2167 $35BB = 0,0,0,1,1,2,2,0 extra cells of reach for standing high
|
|
clc ; 216A plain add
|
|
adc #$06 ; 216B a BOOMER shoots 6 cells, plus the terrain bonus
|
|
tax ; 216D range 6-8 as a table index
|
|
lda rangeDistanceSqTable,x ; 216E $35B0 = r*(r+1), the squared distance a target has to stay under
|
|
cmp cellColA ; 2171 compare it with the distance to the cell we would move to
|
|
bcc L_217C ; 2173 out of range: leave the cursor where it is
|
|
L_2175:
|
|
ldx #$FF ; 2175 column step, operand patched at $2134
|
|
L_2177:
|
|
ldy #$FF ; 2177 row step, operand patched at $2137
|
|
L_2179:
|
|
jsr scrollViewBy ; 2179 scroll the view, and the cursor with it, by one cell
|
|
L_217C:
|
|
lda #$0A ; 217C 10 frames
|
|
sta fireDebounceTimer ; 217E zp_6D: ignore the fire button briefly after a scroll
|
|
lda #$00 ; 2180 clear
|
|
sta fireButtonArmed ; 2182 zp_79: the button must be seen released again before the next click counts
|
|
rts ; 2184 the caller loops back and reads the stick again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runTargetCursorLoop - Modal loop that lets the player drive the cursor to a cell and then acts on
|
|
; it. cursorTargetMode picks the flavour: 0 = choose a destination for the selected unit, or for its
|
|
; whole group when uiLevel is 2 (the formation ghost is built into sprite 0); positive = choose a
|
|
; target for a BOOMER (cross cursor, movement limited to its range); negative = only report the cell
|
|
; the player stops on in pickedCellCol/pickedCellRow. Each pass runs one game frame, previews the
|
|
; route after 90 idle frames, scrolls on joystick, and on a fire click issues the order. The normal
|
|
; exits scroll the view back onto the unit and restore the plain battlefield cursor.
|
|
; In: cursorTargetMode (zp_BB), selectedUnit ($90F8), uiLevel ($9209), gamePhase bit 6,
|
|
; stepDelayTimer ($90FC), inputIdleFrames ($90EE), joystick and fire button
|
|
; Out: an order queued by orderSelectedUnitToCursor / sendGroupMoveToCursor / orderAttackUnitAtCursor,
|
|
; or pickedCellCol/Row ($91D8/$91D9) for a free pick; cursorTargetMode = 0, groupBoxActive = 0,
|
|
; sprites 0 and 1 blanked and un-expanded, sprite 2 back to the box cursor
|
|
; Called from: the unit destination flow ($4BE1, $5F64, $650A) and overlay A's setup screen ($83FF)
|
|
; ----------------------------------------------------------------------
|
|
runTargetCursorLoop:
|
|
lda #$FF ; 2185 no unit
|
|
sta infoPanelUnit ; 2187 $91D7: nothing is being described while the target cursor is up
|
|
lda #$00 ; 218A sprite 0
|
|
sta activeSpriteIndex ; 218C $91CC = the sprite that carries the cursor shape
|
|
lda cursorTargetMode ; 218F zp_BB selects the flavour of this loop
|
|
beq L_219B ; 2191 0 = choosing a destination
|
|
bmi L_21A8 ; 2193 negative = free cell pick: plain block cursor
|
|
jsr setCursorShapeCross ; 2195 positive = attack target: thick cross into sprite 0
|
|
jmp L_21AE ; 2198 sprite 2 is left as the caller set it
|
|
|
|
L_219B:
|
|
lda uiLevel ; 219B $9209: 0 browsing, 1 single unit, 2 whole group
|
|
cmp #$02 ; 219E 2 or more = a group is selected
|
|
bcc L_21A8 ; 21A0 single unit: plain block cursor
|
|
jsr buildGroupFormationSprite; 21A2 group move: sprite 0 shows every member as a dot, and the box offsets are built
|
|
jmp L_21AB ; 21A5 on to the crosshair
|
|
|
|
L_21A8:
|
|
jsr setCursorShapeBlock ; 21A8 small solid block into sprite $91CC
|
|
L_21AB:
|
|
jsr showCrosshairSprite ; 21AB sprite 2 = the diamond crosshair over the centre cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Cursor loop: one pass per frame until the player clicks, the unit dies or the UI is aborted.
|
|
; ----------------------------------------------------------------------
|
|
L_21AE:
|
|
bit gamePhase ; 21AE zp_B1 bit 6 marks the phases in which the engine must not run
|
|
bvc L_21B8 ; 21B0 normal game: run a whole frame
|
|
jsr serviceCommandExchange ; 21B2 setup/film phase: only keep the lock-step link serviced
|
|
jmp L_21C0 ; 21B5 skip the frame update
|
|
|
|
L_21B8:
|
|
lda stepDelayTimer ; 21B8 $90FC counts down to the next engine step
|
|
bne L_21C0 ; 21BB one frame update per engine step is enough
|
|
jsr updateGameFrame ; 21BD engine, messages, sound, link
|
|
L_21C0:
|
|
lda inputIdleFrames ; 21C0 $90EE = frames since the last input
|
|
cmp #$5A ; 21C3 90 frames = one and a half seconds
|
|
bcc L_21CE ; 21C5 player is still busy
|
|
lda cursorTargetMode ; 21C7 only the plain move cursor previews a route
|
|
bne L_21CE ; 21C9 target or free-pick cursor: no preview
|
|
jsr previewPathToCursor ; 21CB crawl the marker sprite from the unit to the cursor cell
|
|
L_21CE:
|
|
ldy selectedUnit ; 21CE the unit being ordered
|
|
lda unitTypeTable,y ; 21D1 $F76C: bit 7 set = the unit has been destroyed
|
|
bpl L_21D9 ; 21D4 still alive: carry on
|
|
jmp L_2239 ; 21D6 it died while we were aiming: clean up and re-centre
|
|
|
|
L_21D9:
|
|
jsr checkUiContinue ; 21D9 SPACE, a function key or the end of the game abort every modal UI
|
|
bne L_21E1 ; 21DC A = 1: keep going
|
|
jmp L_223C ; 21DE aborted: clean up without scrolling back
|
|
|
|
L_21E1:
|
|
lda joyDirection ; 21E1 joystick nibble
|
|
cmp #$0F ; 21E3 $0F = centred
|
|
bne L_2220 ; 21E5 deflected: scroll the cursor
|
|
jsr checkFirePressed ; 21E7 fresh press of the fire button?
|
|
bne L_2220 ; 21EA no click: fall through to the scroll handler
|
|
lda cursorTargetMode ; 21EC which kind of pick is this?
|
|
beq L_220B ; 21EE 0 = a move order
|
|
bmi L_21F8 ; 21F0 negative = the caller only wants the cell
|
|
jsr orderAttackUnitAtCursor ; 21F2 positive = attack: queue command $87 against the unit under the cursor
|
|
jmp L_2239 ; 21F5 order sent: clean up
|
|
|
|
L_21F8:
|
|
lda viewOriginCol ; 21F8 left edge of the view
|
|
clc ; 21FA plain add
|
|
adc #$03 ; 21FB +3 = the cursor column
|
|
sta pickedCellCol ; 21FD $91D8 = the cell the caller asked for
|
|
lda viewOriginRow ; 2200 top edge of the view
|
|
clc ; 2202 plain add
|
|
adc #$02 ; 2203 +2 = the cursor row
|
|
sta pickedCellRow ; 2205 $91D9
|
|
jmp L_2263 ; 2208 return at once and leave the sprites to the caller
|
|
|
|
L_220B:
|
|
lda uiLevel ; 220B $9209: single unit or whole group?
|
|
cmp #$02 ; 220E 2 = group
|
|
bcs L_2218 ; 2210 group move
|
|
jsr orderSelectedUnitToCursor; 2212 single unit: queue command $80 unit,column,row
|
|
jmp L_2239 ; 2215 order sent: clean up
|
|
|
|
L_2218:
|
|
jsr sendGroupMoveToCursor ; 2218 queue command $81, move the whole group to the cursor cell
|
|
bcs L_2236 ; 221B sendGroupMoveToCursor always returns C=0, so this 'stay in the loop' branch is dead code
|
|
jmp L_2239 ; 221D order sent: clean up
|
|
|
|
L_2220:
|
|
lda joyRepeatTimer ; 2220 zp_6C auto-repeat delay
|
|
bne L_2236 ; 2222 still inside the delay
|
|
ldy joyDirection ; 2224 joystick nibble
|
|
lda #$00 ; 2226 clear
|
|
sta inputLockoutTimer ; 2228 $0B7D = 0: input is live
|
|
lda joystickDirectionTable,y; 222B nibble -> direction code 0-7, $FF when centred
|
|
bmi L_2236 ; 222E no usable direction
|
|
jsr setJoystickRepeatDelay ; 2230 16 frames for the first step, 6 for the repeats
|
|
jsr scrollViewportByDirection; 2233 move the cursor one cell (range-checked in target mode)
|
|
L_2236:
|
|
jmp L_21AE ; 2236 next pass of the cursor loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Leaving the target cursor: put the sprites and the cursor mode back the way the battlefield loop
|
|
; expects them. $2239 re-centres the view on the unit first; $223C is entered directly when the UI
|
|
; was aborted.
|
|
; ----------------------------------------------------------------------
|
|
L_2239:
|
|
jsr recenterViewportOnUnit ; 2239 scroll the view back until the unit is under the cursor again
|
|
L_223C:
|
|
lda #$00 ; 223C clear
|
|
sta groupBoxActive ; 223E $920A: the formation box is no longer being dragged
|
|
sta cursorTargetMode ; 2241 zp_BB back to the plain move cursor
|
|
lda #$01 ; 2243 sprite 1
|
|
jsr clearSpriteShape ; 2245 blank its 64-byte shape (the path marker)
|
|
lda #$00 ; 2248 sprite 0
|
|
jsr clearSpriteShape ; 224A blank its shape (cursor or formation ghost)
|
|
lda spriteXExpandShadow ; 224D $9023 = shadow of VIC_SPR_EXP_X
|
|
and #$FC ; 2250 clear bits 0-1: sprites 0 and 1 back to normal width
|
|
sta spriteXExpandShadow ; 2252 the IRQ copies the shadow to the VIC
|
|
lda spriteYExpandShadow ; 2255 $9024 = shadow of VIC_SPR_EXP_Y
|
|
and #$FC ; 2258 sprites 0 and 1 back to normal height
|
|
sta spriteYExpandShadow ; 225A write the shadow back
|
|
jsr resetPathMarkerToUnit ; 225D park the path marker back on the unit, ready for five more passes
|
|
jsr showCursorBoxSprite ; 2260 sprite 2 back to the normal centre-cell box
|
|
L_2263:
|
|
rts ; 2263 back to the menu or setup code that asked for a cell
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; previewPathToCursor - Idle-time animation: while the player leaves the controls alone with a move
|
|
; cursor up, the marker sprite crawls along the straight line from the selected unit to the cursor
|
|
; cell so the intended route can be seen on the strategic mini-map. The target is re-read every pass
|
|
; (the view may still scroll) and the animation stops as soon as checkUserInterrupt reports stick,
|
|
; button, key or link activity.
|
|
; In: selectedUnit ($90F8), viewOriginCol/Row, unit arrays $F640/$F6A4/$F76C
|
|
; Out: lineEndCol/lineEndRow (zp_95/zp_94), marker state $24E1-$24E3, sprite 1 animated and finally
|
|
; hidden
|
|
; Called from: runTargetCursorLoop ($21CB) after 90 idle frames
|
|
; ----------------------------------------------------------------------
|
|
previewPathToCursor:
|
|
ldy selectedUnit ; 2264 the unit being ordered
|
|
lda viewOriginCol ; 2267 left edge of the view
|
|
clc ; 2269 plain add
|
|
adc #$03 ; 226A +3 = cursor column
|
|
cmp unitColTable,y ; 226C $F640+unit
|
|
bne L_227B ; 226F different column: there is a route to show
|
|
lda viewOriginRow ; 2271 top edge of the view
|
|
clc ; 2273 plain add
|
|
adc #$02 ; 2274 +2 = cursor row
|
|
cmp unitRowTable,y ; 2276 $F6A4+unit
|
|
beq L_229F ; 2279 cursor sits on the unit itself: nothing to preview
|
|
L_227B:
|
|
jsr resetPathMarker ; 227B put the marker on the unit and give it five passes
|
|
L_227E:
|
|
ldy selectedUnit ; 227E the unit being ordered
|
|
lda unitTypeTable,y ; 2281 $F76C bit 7 = destroyed
|
|
bmi L_229F ; 2284 it died: stop the animation
|
|
jsr updateGameFrame ; 2286 keep the game and the link running while we animate
|
|
lda #$03 ; 2289 3
|
|
clc ; 228B plain add
|
|
adc viewOriginCol ; 228C left edge of the view
|
|
sta lineEndCol ; 228E zp_95 = cursor column, re-read every pass in case the view scrolled
|
|
lda #$02 ; 2290 2
|
|
clc ; 2292 plain add
|
|
adc viewOriginRow ; 2293 top edge of the view
|
|
sta lineEndRow ; 2295 zp_94 = cursor row
|
|
jsr animatePathMarkerStep ; 2297 advance the marker one cell every 4 frames
|
|
jsr checkUserInterrupt ; 229A C=1 as soon as the player or the link wants attention
|
|
bcc L_227E ; 229D still idle: keep animating
|
|
L_229F:
|
|
jmp hideSprite1 ; 229F turn the marker sprite off and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showUnitInfoPanel - Fills the info panel under the tactical view (text window rows 12-20, columns
|
|
; 25-38) with the description of unit selectedUnit: the type name and unit number, the GROUP / ENERGY
|
|
; / WEAPON / ACTION labels, the values printed by printUnitStatusLines and the three sprites that make
|
|
; up the unit picture. It then holds that display while the cursor rests on the unit, reprinting the
|
|
; values whenever the unit is being stepped or shot at or a lock-step exchange has been applied, and
|
|
; animating the destination marker. Moving off the unit, an interrupt or the end of the placement
|
|
; phase clears the panel and the picture sprites again.
|
|
; In: selectedUnit ($90F8), uiLevel ($9209), the unit arrays, stepDelayTimer ($90FC), currentUnit
|
|
; ($90FA), fireTargetUnit ($91F0), exchangeAppliedFlag ($920E), highlightedGroupKey ($9205),
|
|
; cursorTargetMode, gamePhase
|
|
; Out: panel drawn and cleared again; infoPanelUnit ($91D7) = the unit while it is shown and $FF
|
|
; afterwards; textCentreWidth = 0; sprites 5-7 disabled; text window reset to full screen.
|
|
; Self-modifies the LDX # operand at $22EF+1
|
|
; Called from: runMapMainLoop ($2070, hovering), handleOwnUnitClick ($5E4C) and the console unit list
|
|
; ($63F5)
|
|
; ----------------------------------------------------------------------
|
|
showUnitInfoPanel:
|
|
lda #$00 ; 22A2 clear
|
|
sta textCentreWidth ; 22A4 $90EF = 0: print flush left, no centring
|
|
sta textFillGlyph ; 22A7 zp_3E: blanks are glyph 0
|
|
sta rawGlyphMode ; 22A9 zp_43: characters go through the normal ASCII translation
|
|
ldy uiLevel ; 22AB $9209: 0 browsing, 1 unit selected, 2 group selected
|
|
lda uiLevelTextColourTable,y; 22AE $5E14 = 9 / 5 / 4: the panel text colour for that level
|
|
sta textColour ; 22B1 zp_3D = colour byte written for every character cell
|
|
jsr clearInfoPanel ; 22B3 open and blank the info panel window (rows 12-20, columns 25-38)
|
|
lda #$02 ; 22B6 column 2
|
|
sta textCursorCol ; 22B8 zp_3F: start two characters in from the left edge of the panel
|
|
jsr updateCursorPointers ; 22BA recompute the bitmap and colour pointers for that cell
|
|
ldx selectedUnit ; 22BD the unit on display
|
|
lda unitTypeTable,x ; 22C0 $F76C: type in the low bits, bit 7 = dead
|
|
and #$07 ; 22C3 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN
|
|
tax ; 22C5 X = table index
|
|
stx L_22EF+1 ; 22C6 self-modifying: the LDX # at $22EF+1 that passes the type to the picture routine
|
|
lda #$64 ; 22C9 low byte of $C964
|
|
ldy #$C9 ; 22CB high byte: the five 8-byte unit type names in the $C000 overlay
|
|
jsr printTableEntry ; 22CD print entry X of that table
|
|
lda #$A0 ; 22D0 space
|
|
jsr printChar ; 22D2 separate the name from the number
|
|
lda selectedUnit ; 22D5 the unit index 0-99
|
|
ldy #$00 ; 22D8 middle byte of the 24-bit value = 0 (zp_7E holds the high byte)
|
|
jsr printDecimal ; 22DA print it in decimal without leading zeros
|
|
jsr printNewline ; 22DD next panel row
|
|
lda #$E9 ; 22E0 low byte of $CAE9
|
|
ldy #$CA ; 22E2 high byte: the GROUP / ENERGY / WEAPON / ACTION label block
|
|
jsr printString ; 22E4 print the four labels
|
|
jsr printUnitStatusLines ; 22E7 fill in the value column beside them
|
|
lda unitPictureHiTable ; 22EA $CD00 = first high byte of the unit picture address table
|
|
bpl L_22F4 ; 22ED not a high address: the $C000 overlay holds no pictures, skip them
|
|
L_22EF:
|
|
ldx #$FF ; 22EF unit type, operand patched at $22C6
|
|
jsr showUnitPictureSprites ; 22F1 unpack the picture and show it as sprites 5, 6 and 7
|
|
L_22F4:
|
|
jsr hideSprite1 ; 22F4 the path marker must not sit over the panel
|
|
ldx selectedUnit ; 22F7 the unit on display
|
|
lda unitDestColTable,x ; 22FA $F7D0: bit 7 set = the unit has no destination
|
|
bmi L_2330 ; 22FD nowhere to go: skip the marker set-up
|
|
jsr resetPathMarker ; 22FF park the marker on the unit, ready to walk toward the destination
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Panel loop: hold the display while the cursor stays on this unit, reprinting the values only when
|
|
; something can actually have changed.
|
|
; ----------------------------------------------------------------------
|
|
L_2302:
|
|
lda stepDelayTimer ; 2302 $90FC: is an engine step due?
|
|
bne L_2333 ; 2305 no: only animate the marker and test for input
|
|
lda selectedUnit ; 2307 the unit on display
|
|
sta infoPanelUnit ; 230A $91D7 tells the rest of the game which unit the panel is showing
|
|
jsr updateGameFrame ; 230D one full game frame
|
|
jsr readCellUnderCursor ; 2310 map byte under the cursor
|
|
bpl L_234D ; 2313 cursor moved onto empty terrain: close the panel
|
|
and #$7F ; 2315 unit index under the cursor
|
|
cmp selectedUnit ; 2317 still the same unit?
|
|
bne L_234D ; 231A a different unit: close the panel
|
|
cmp currentUnit ; 231C $90FA = the unit the engine is stepping right now
|
|
beq L_2330 ; 231F it is moving: reprint its status
|
|
cmp fireTargetUnit ; 2321 $91F0 = the unit currently being shot at
|
|
beq L_2330 ; 2324 it is under fire: reprint its status
|
|
lda exchangeAppliedFlag ; 2326 $920E bit 7 = a lock-step exchange was applied (one turn tick)
|
|
bpl L_2333 ; 2329 nothing has changed since the last print
|
|
lda #$00 ; 232B clear
|
|
sta exchangeAppliedFlag ; 232D consume the tick
|
|
L_2330:
|
|
jsr printUnitStatusLines ; 2330 reprint group, energy, weapon and action
|
|
L_2333:
|
|
lda cursorTargetMode ; 2333 zp_BB < 0 = free cell pick
|
|
bmi L_233C ; 2335 then always show where the unit is heading
|
|
lda highlightedGroupKey ; 2337 $9205 = $FF when no group is highlighted
|
|
bpl L_233F ; 233A a group is highlighted: no individual destination marker
|
|
L_233C:
|
|
jsr animatePathMarkerToDestination; 233C crawl the marker from the unit toward its destination
|
|
L_233F:
|
|
jsr checkUserInterrupt ; 233F C=1 on stick, button, key or link activity
|
|
bcs L_234D ; 2342 player wants something: close the panel
|
|
lda gamePhase ; 2344 battle or unit placement?
|
|
beq L_2302 ; 2346 battle: keep the panel up
|
|
jsr callSetupOverlayHook ; 2348 placement: let overlay A see the '*' key
|
|
bcc L_2302 ; 234B C=0, nothing happened: keep the panel up
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Leaving the panel: wipe it, drop the marker and the three unit-picture sprites.
|
|
; ----------------------------------------------------------------------
|
|
L_234D:
|
|
lda #$FF ; 234D no unit
|
|
sta infoPanelUnit ; 234F $91D7 = the panel is empty again
|
|
lda #$00 ; 2352 colour 0
|
|
sta textColour ; 2354 zp_3D: clear the panel to black
|
|
jsr clearInfoPanel ; 2356 wipe the panel window
|
|
jsr hideSprite1 ; 2359 path marker sprite off
|
|
lda spriteEnableShadow ; 235C $9022 = shadow of VIC_SPR_ENA
|
|
and #$1F ; 235F clear bits 5-7: the three unit-picture sprites go away
|
|
sta spriteEnableShadow ; 2361 the IRQ writes the shadow to the VIC
|
|
jmp setFullScreenWindow ; 2364 text window back to the whole screen, and return through it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkUserInterrupt - The 'may this passive display keep running?' test used by the idle loops of the
|
|
; info panel and the path preview. It returns C=0 only while the text engine agrees the UI may
|
|
; continue (checkUiContinue $C451), no function key is waiting to switch screens, the joystick is
|
|
; centred and the fire button is not held. Anything else returns C=1 and freezes stick and keyboard
|
|
; for 30 frames so the same push is not consumed twice.
|
|
; In: requestedScreenKey zp_66 ($FF = no function key), joyDirection zp_5D ($0F = centred), joyFireUp
|
|
; zp_5F / fireButtonArmed zp_79 through isFireHeld, plus everything checkUiContinue looks at
|
|
; (filmPlaybackMode, gameEndCountdown, pauseState, gamePhase, lastTypedKey)
|
|
; Out: C=0 keep going / C=1 stop; on C=1 inputLockoutTimer $0B7D = $1E (30 frames)
|
|
; Called from: the path-preview loop at $229A and the info-panel idle loop at $233F
|
|
; ----------------------------------------------------------------------
|
|
checkUserInterrupt:
|
|
jsr checkUiContinue ; 2367 ask the text engine whether a modal display may still run (A=1 = yes)
|
|
beq L_237D ; 236A branch if it says stop: game ending, paused, SPACE typed or a screen change pending
|
|
lda requestedScreenKey ; 236C function key latched by the IRQ: 0-3 = F1/F3/F5/F7, $FF = none pending
|
|
bpl L_237D ; 236E branch if a function key is waiting to switch screens
|
|
lda joyDirection ; 2370 joystick 2 direction nibble latched by the IRQ (active low)
|
|
cmp #$0F ; 2372 $0F = no direction pulled low = stick centred
|
|
bne L_237D ; 2374 branch if the player has pushed the stick
|
|
jsr isFireHeld ; 2376 level test of the fire button: A=0 while it is held down
|
|
beq L_237D ; 2379 branch if the button is down
|
|
clc ; 237B nothing interrupted: C=0 tells the caller to carry on
|
|
rts ; 237C back to the idle loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Something wants attention: stop the display and swallow the input for half a second so the menu the
|
|
; caller returns to does not immediately act on the same stick push or key.
|
|
; ----------------------------------------------------------------------
|
|
L_237D:
|
|
lda #$1E ; 237D 30 frames = about half a second
|
|
sta inputLockoutTimer ; 237F the IRQ ignores stick and keyboard while this counts down
|
|
sec ; 2382 C=1 = the caller must tear its display down
|
|
rts ; 2383 return to the caller's loop, which exits on C=1
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printUnitStatusLines - Fills in the value column of the unit info panel for selectedUnit ($90F8):
|
|
; the group name on row 13, the energy percentage on row 17, the weapon state on row 18, the action on
|
|
; row 19 and one flag word (BLITZ / CLOAKED / DUG IN) on row 20, then adds up to two '*' zone markers
|
|
; in front of the unit name on row 12. The labels GROUP:/ENERGY:/WEAPON:/ACTION: themselves are
|
|
; printed once by showUnitInfoPanel from the string at $CAE9.
|
|
; In: selectedUnit $90F8; the unit arrays unitDisplayFlagsTable $FB54 (bit 0 fired, bit 1 pinned,
|
|
; bits 2-4 group id, bit 6 grouped), unitEnergyTable $F9C4, unitTypeTable $F76C, unitStunTable
|
|
; $F960, unitPathColTable $F898 (waypoint column + cloak bits), unitPathRowTable $F8FC (waypoint
|
|
; row + blitz bits), unitDestColTable $F7D0, unitDestRowTable $F834, unitMovePointsTable $FA28;
|
|
; currentUnit $90FA, gameClock $91CB, markerBlinkTimer zp_6F
|
|
; Out: text drawn into the bitmap; textWindowLeft zp_3B / textWindowWidth zp_3C / textCursorCol zp_3F
|
|
; / textCursorRow zp_40 left pointing into the panel, markerBlinkTimer = 20 when FIRED starts,
|
|
; scratch18/scratch19 clobbered
|
|
; Called from: showUnitInfoPanel, once when the panel is opened ($22E7) and again on every refresh
|
|
; while the unit is in combat ($2330)
|
|
; Set up the narrow value column of the info panel. The panel window itself is columns 25-38; the
|
|
; values are printed in columns 31/32 so they line up behind the labels.
|
|
; ----------------------------------------------------------------------
|
|
printUnitStatusLines:
|
|
lda #$20 ; 2384 column 32 = the value column of the panel
|
|
sta textWindowLeft ; 2386 text engine window left edge
|
|
lda #$09 ; 2388 9 characters wide, i.e. out to the right frame
|
|
sta textWindowWidth ; 238A text engine window width / wrap point
|
|
dec textWindowLeft ; 238C the group name starts one column further left (column 31)
|
|
lda #$00 ; 238E first character of the window
|
|
sta textCursorCol ; 2390 cursor column, relative to textWindowLeft
|
|
lda #$0D ; 2392 screen row 13 = the GROUP: line
|
|
jsr setCursorRow ; 2394 move the text cursor there (recomputes the bitmap pointers)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; GROUP: line - the group name, or ' - ' when the unit is not in a group.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$00 ; 2397 string index 0 = the ' - ' filler at $C9A4
|
|
ldy selectedUnit ; 2399 the unit the info panel is showing
|
|
lda unitDisplayFlagsTable,y ; 239C its group/status flags ($FB54)
|
|
and #$40 ; 239F bit 6 = the unit belongs to a group
|
|
bne L_23A9 ; 23A1 branch if it is a group member
|
|
lda #$A4 ; 23A3 low byte of the ' - ' string table entry at $C9A4
|
|
ldy #$C9 ; 23A5 high byte; entry 0 of that 8-byte-per-entry table is ' - '
|
|
bne L_23B5 ; 23A7 always taken
|
|
L_23A9:
|
|
lda unitDisplayFlagsTable,y ; 23A9 re-read the group flags
|
|
and #$1C ; 23AC bits 2-4 hold the group id
|
|
lsr a ; 23AE shift the id down...
|
|
lsr a ; 23AF ...to 0-6 = ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER
|
|
tax ; 23B0 index into the name table
|
|
lda #$2C ; 23B1 low byte of groupNameTable $C92C (7 names of 8 bytes)
|
|
ldy #$C9 ; 23B3 high byte
|
|
L_23B5:
|
|
jsr printTableEntry ; 23B5 print entry X of the 8-byte-per-entry string table at A/Y
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ENERGY: line - the energy byte is 0-50 and is shown as a percentage (x2).
|
|
; ----------------------------------------------------------------------
|
|
inc textWindowLeft ; 23B8 value column back to 32 for all the remaining lines
|
|
inc textCursorRow ; 23BA the GROUP: label is followed by three blank rows...
|
|
inc textCursorRow ; 23BC ...row 15...
|
|
inc textCursorRow ; 23BE ...row 16
|
|
jsr printNewline ; 23C0 newline lands on row 17 = the ENERGY: line, cursor at column 0
|
|
ldx selectedUnit ; 23C3 the selected unit again
|
|
lda unitEnergyTable,x ; 23C6 energy byte ($F9C4)
|
|
and #$3F ; 23C9 bits 0-5 = energy 0-50; bits 6-7 are the 'spotted by the enemy' flags
|
|
asl a ; 23CB x2 gives 0-100 percent
|
|
ldy #$00 ; 23CC high byte of the number to print = 0
|
|
jsr printDecimal ; 23CE print it as decimal digits
|
|
lda #$A5 ; 23D1 '%'
|
|
jsr printChar ; 23D3 print it
|
|
lda #$A0 ; 23D6 trailing space, wipes the third digit of a previous 100%
|
|
jsr printChar ; 23D8 print it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; WEAPON: line - READY / RELOAD / FIRED, or ' - ' for a spy, a comcen or a cloaked unit, or STUN'D
|
|
; while the unit is stunned. Entries are 8 bytes apart in the table at $C98C: 0 READY, 1 RELOAD, 2
|
|
; FIRED, 3 ' - ', 4 REPAIR, 5 MOVE, 6 PINNED, 7 STUN'D.
|
|
; ----------------------------------------------------------------------
|
|
jsr printNewline ; 23DB newline to row 18 = the WEAPON: line
|
|
ldx selectedUnit ; 23DE selected unit
|
|
lda unitPathColTable,x ; 23E1 waypoint column + cloak bits ($F898; bit 7 = cloaked)
|
|
sta scratch18 ; 23E4 keep the cloak byte for the test below
|
|
lda unitTypeTable,x ; 23E6 type byte ($F76C)
|
|
sta scratch19 ; 23E9 keep it as well
|
|
lda unitStunTable,x ; 23EB stun / dig-in byte ($F960)
|
|
and #$0F ; 23EE low nybble = clicks of stun left
|
|
beq L_23F6 ; 23F0 branch if the unit is not stunned
|
|
ldx #$07 ; 23F2 table entry 7 = STUN'D
|
|
bne L_2428 ; 23F4 always taken
|
|
L_23F6:
|
|
ldx #$03 ; 23F6 table entry 3 = ' - ' (no weapon state to show)
|
|
lda scratch19 ; 23F8 unit type byte
|
|
and #$07 ; 23FA bits 0-2 = 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN
|
|
cmp #$03 ; 23FC SPY and COMCEN are types 3 and 4...
|
|
bcs L_2428 ; 23FE ...and carry no weapon, so leave ' - '
|
|
lda markerBlinkTimer ; 2400 the 20-frame hold started when FIRED was printed (shared blink timer zp_6F)
|
|
bne L_2441 ; 2402 while it runs skip the field entirely so FIRED stays on screen
|
|
lda scratch18 ; 2404 the cloak byte saved above
|
|
bmi L_2428 ; 2406 a cloaked unit cannot fire: show ' - '
|
|
ldx #$00 ; 2408 table entry 0 = READY
|
|
ldy selectedUnit ; 240A selected unit
|
|
lda unitDisplayFlagsTable,y ; 240D status flags
|
|
and #$01 ; 2410 bit 0 = the unit has fired and is reloading
|
|
beq L_2428 ; 2412 not fired -> READY
|
|
inx ; 2414 table entry 1 = RELOAD
|
|
cpy currentUnit ; 2415 is this the unit the simulation is stepping right now?
|
|
bne L_2428 ; 2418 no -> just RELOAD
|
|
tya ; 241A unit index
|
|
eor gameClock ; 241B mix it with the game clock...
|
|
and #$01 ; 241E ...and keep bit 0, so only every other click flashes
|
|
bne L_2428 ; 2420 off click -> RELOAD
|
|
lda #$14 ; 2422 20 frames
|
|
sta markerBlinkTimer ; 2424 hold the field so FIRED stays readable
|
|
ldx #$02 ; 2426 table entry 2 = FIRED
|
|
L_2428:
|
|
lda #$8C ; 2428 low byte of the weapon-word table $C98C
|
|
ldy #$C9 ; 242A high byte
|
|
jsr printTableEntry ; 242C print entry X
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A boomer with a locked target gets a '*' after the weapon word.
|
|
; ----------------------------------------------------------------------
|
|
ldx selectedUnit ; 242F selected unit
|
|
jsr getBoomerTarget ; 2432 boomers return their target byte, other types a small positive value
|
|
tay ; 2435 into Y for the test
|
|
lda #$A0 ; 2436 space = nothing to mark
|
|
cpy #$80 ; 2438 bit 7 of the target byte = a target is locked
|
|
bcc L_243E ; 243A no target -> print the space
|
|
lda #$AA ; 243C '*' = this boomer has a target locked
|
|
L_243E:
|
|
jsr printChar ; 243E print the marker
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ACTION: line - ' - ' / REPAIR / MOVE / PINNED / STUN'D / DIGGIN, entries 0-5 of the 8-byte-per-entry
|
|
; table at $C9A4.
|
|
; ----------------------------------------------------------------------
|
|
L_2441:
|
|
jsr printNewline ; 2441 newline to row 19 = the ACTION: line
|
|
ldx #$02 ; 2444 table entry 2 = MOVE
|
|
ldy selectedUnit ; 2446 selected unit
|
|
lda unitDestColTable,y ; 2449 ordered destination column ($F7D0); bit 7 = no destination
|
|
bmi L_2464 ; 244C branch if the unit has nowhere to go
|
|
lda unitDisplayFlagsTable,y ; 244E status flags
|
|
and #$02 ; 2451 bit 1 = the unit was hit and is pinned down
|
|
beq L_245C ; 2453 not pinned -> just check the movement points
|
|
lda unitMovePointsTable,y ; 2455 movement points ($FA28)
|
|
cmp #$0A ; 2458 fewer than 10 points left?
|
|
bcc L_2461 ; 245A yes -> PINNED rather than MOVE
|
|
L_245C:
|
|
lda unitMovePointsTable,y ; 245C movement points again
|
|
bpl L_246C ; 245F still positive -> MOVE
|
|
L_2461:
|
|
inx ; 2461 table entry 3 = PINNED (out of movement)
|
|
bne L_246C ; 2462 always taken
|
|
L_2464:
|
|
ldx #$00 ; 2464 table entry 0 = ' - ' (nothing to do)
|
|
lda unitDestRowTable,y ; 2466 destination row ($F834); bit 7 = the unit is regaining energy
|
|
bpl L_246C ; 2469 not repairing -> leave ' - '
|
|
inx ; 246B table entry 1 = REPAIR
|
|
L_246C:
|
|
lda unitStunTable,y ; 246C stun / dig-in byte
|
|
and #$0F ; 246F low nybble = stun clicks left
|
|
beq L_2477 ; 2471 not stunned
|
|
ldx #$04 ; 2473 table entry 4 = STUN'D
|
|
bne L_2482 ; 2475 always taken
|
|
L_2477:
|
|
lda unitStunTable,y ; 2477 stun / dig-in byte again
|
|
bmi L_2482 ; 247A bit 7 = already dug in, that is a flag word not an action
|
|
and #$E0 ; 247C bits 5-7 also count the dig-in progress
|
|
beq L_2482 ; 247E not digging in
|
|
ldx #$05 ; 2480 table entry 5 = DIGGIN
|
|
L_2482:
|
|
lda #$A4 ; 2482 low byte of the action-word table $C9A4
|
|
ldy #$C9 ; 2484 high byte
|
|
jsr printTableEntry ; 2486 print entry X
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Flag line - at most one of BLITZ, CLOAKED or DUG IN, printed further left (column 28) because the
|
|
; words are longer than the value column.
|
|
; ----------------------------------------------------------------------
|
|
lda #$1C ; 2489 column 28 for the wider flag word
|
|
sta textWindowLeft ; 248B text window left edge
|
|
jsr printNewline ; 248D newline to row 20
|
|
ldx selectedUnit ; 2490 selected unit
|
|
lda unitPathRowTable,x ; 2493 waypoint row + blitz bits ($F8FC)
|
|
bpl L_249E ; 2496 bit 7 clear = blitz is off
|
|
lda #$82 ; 2498 low byte of 'BLITZ' at $CB82
|
|
ldy #$CB ; 249A high byte
|
|
bne L_24B2 ; 249C always taken
|
|
L_249E:
|
|
lda unitPathColTable,x ; 249E waypoint column + cloak bits ($F898)
|
|
bpl L_24A9 ; 24A1 bit 7 clear = not cloaked
|
|
lda #$7B ; 24A3 low byte of 'CLOAKED' at $CB7B
|
|
ldy #$CB ; 24A5 high byte
|
|
bne L_24B2 ; 24A7 always taken
|
|
L_24A9:
|
|
lda unitStunTable,x ; 24A9 stun / dig-in byte
|
|
bpl L_24B5 ; 24AC bit 7 clear = not dug in: print no flag word at all
|
|
lda #$87 ; 24AE low byte of 'DUG IN' at $CB87
|
|
ldy #$CB ; 24B0 high byte
|
|
L_24B2:
|
|
jsr printString ; 24B2 print the bit-7 terminated string at A/Y
|
|
L_24B5:
|
|
jsr bootWaitClkAck ; 24B5 wipe whatever the previously shown unit left below this line (also textEngineC000:clearToWindowBottom)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Zone markers - unitTypeTable bits 3-4 record which half of the enemy ground this unit has already
|
|
; scored (bit 3 = the near zone, columns 10-19; bit 4 = the far end zone, columns 0-9). One '*' is
|
|
; printed in front of the unit name for either, a second one for the far zone.
|
|
; ----------------------------------------------------------------------
|
|
lda #$20 ; 24B8 value column back to 32
|
|
sta textWindowLeft ; 24BA text window left edge
|
|
ldx selectedUnit ; 24BC selected unit
|
|
lda unitTypeTable,x ; 24BF type byte
|
|
and #$18 ; 24C2 bits 3-4 = zones this unit has already scored
|
|
beq L_24E0 ; 24C4 no marker to print
|
|
pha ; 24C6 keep the two bits over the window change
|
|
jsr setInfoPanelWindow ; 24C7 back to the full panel window (columns 25-38); leaves A = $0C
|
|
ldy #$00 ; 24CA column 0 of the window = screen column 25
|
|
sty textCursorCol ; 24CC cursor column
|
|
jsr setCursorRow ; 24CE A is still the $0C left by setInfoPanelWindow, so this lands on row 12 = the unit name line
|
|
lda #$AA ; 24D1 '*'
|
|
jsr printChar ; 24D3 print it in front of the name
|
|
pla ; 24D6 get the zone bits back
|
|
and #$10 ; 24D7 bit 4 = the far, double-value end zone
|
|
beq L_24E0 ; 24D9 near zone only -> one marker
|
|
lda #$AA ; 24DB a second '*'
|
|
jsr printChar ; 24DD print it
|
|
L_24E0:
|
|
rts ; 24E0 done: the caller redraws the sprites
|
|
|
|
|
|
; pathMarkerState - three RAM bytes living inside the code, the whole state of the animated path
|
|
; marker (sprite 1) that walks from the selected unit to its ordered destination on the mini-map:
|
|
; passes still to run, and the map cell the marker currently occupies.
|
|
pathMarkerState:
|
|
.byte $05 ; 24E1 . passes still to walk before the marker jumps back to the unit (reloaded with 5)
|
|
pathMarkerCol:
|
|
.byte $00 ; 24E2 map column of the marker
|
|
pathMarkerRow:
|
|
.byte $00 ; 24E3 map row of the marker
|
|
|
|
; ----------------------------------------------------------------------
|
|
; animatePathMarkerToDestination - Sets the line-stepper target to the ordered destination of
|
|
; selectedUnit and falls into animatePathMarkerStep, so the mini-map marker crawls from the unit
|
|
; towards where it has been sent. A unit with no destination just gets sprite 1 switched off.
|
|
; In: selectedUnit $90F8, unitDestColTable $F7D0 (bit 7 set = no destination), unitDestRowTable $F834
|
|
; Out: lineEndCol zp_95 / lineEndRow zp_94 = the destination cell, then everything
|
|
; animatePathMarkerStep does; sprite 1 hidden when there is no destination
|
|
; Called from: the info-panel idle loop ($233C) and the unit menu loops at $6065 and $6214
|
|
; ----------------------------------------------------------------------
|
|
animatePathMarkerToDestination:
|
|
ldx selectedUnit ; 24E4 the unit the panel is showing
|
|
lda unitDestColTable,x ; 24E7 its ordered destination column; bit 7 = no destination
|
|
bmi L_253C ; 24EA nothing ordered -> just hide the marker
|
|
sta lineEndCol ; 24EC line stepper target column
|
|
lda unitDestRowTable,x ; 24EE destination row
|
|
sta lineEndRow ; 24F1 line stepper target row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; animatePathMarkerStep - Advances the mini-map path marker by one cell every 4 frames, but only after
|
|
; the player has been idle for 90 frames. Each step walks one cell of the Bresenham line from
|
|
; pathMarkerCol/Row towards lineEndCol/Row and moves sprite 1 there; at the end of the line the pass
|
|
; counter is decremented and the sprite is blinked, and when the counter runs past zero
|
|
; resetPathMarker puts the marker back on the unit.
|
|
; In: inputIdleFrames $90EE, joyRepeatTimer zp_6C (used here as the step delay), pathMarkerState
|
|
; $24E1, pathMarkerCol/Row $24E2/$24E3, target lineEndCol zp_95 / lineEndRow zp_94
|
|
; Out: pathMarkerCol/Row advanced, sprite 1 position shadows $9011/$9019 and enable bit 1 of $9022,
|
|
; joyRepeatTimer = 4, line stepper state zp_8C-zp_97
|
|
; Called from: the path-preview loop at $2297, and by fall-through from animatePathMarkerToDestination
|
|
; ----------------------------------------------------------------------
|
|
animatePathMarkerStep:
|
|
lda inputIdleFrames ; 24F3 frames since the player last moved the stick or pressed a key
|
|
cmp #$5A ; 24F6 90 frames = 1.5 seconds of doing nothing
|
|
bcs L_24FD ; 24F8 idle long enough: run the animation
|
|
jmp L_253C ; 24FA player is active -> no marker on the map
|
|
|
|
L_24FD:
|
|
lda joyRepeatTimer ; 24FD shared frame countdown, used here as the delay between marker steps
|
|
beq L_2504 ; 24FF expired: time for the next cell
|
|
jmp L_253F ; 2501 still counting: leave the marker where it stands
|
|
|
|
L_2504:
|
|
lda #$04 ; 2504 4 frames per cell
|
|
sta joyRepeatTimer ; 2506 reload the step delay
|
|
lda pathMarkerCol ; 2508 marker column
|
|
sta lineCurCol ; 250B line stepper current column = where the marker stands
|
|
lda pathMarkerRow ; 250D marker row
|
|
sta lineCurRow ; 2510 line stepper current row
|
|
jsr initLineStepper ; 2512 set up the walk to the target cell; A = number of steps
|
|
beq L_252B ; 2515 zero steps (marker already on the target) -> count this as a finished pass
|
|
jsr stepLine ; 2517 advance one cell along the line
|
|
ldy lineCurRow ; 251A new row
|
|
sty pathMarkerRow ; 251C remember it
|
|
ldx lineCurCol ; 251F new column
|
|
stx pathMarkerCol ; 2521 remember it
|
|
jsr positionMarkerSprite ; 2524 put sprite 1 on that mini-map cell
|
|
lda lineStepsLeft ; 2527 steps still to walk
|
|
bne L_2537 ; 2529 more to come -> keep the marker visible
|
|
|
|
; ----------------------------------------------------------------------
|
|
; End of one pass over the line: blink the marker by toggling sprite 1, and after six passes (the
|
|
; counter starts at 5 and is reset by resetPathMarker) start over at the unit.
|
|
; ----------------------------------------------------------------------
|
|
L_252B:
|
|
dec pathMarkerState ; 252B one pass of the line finished
|
|
bmi resetPathMarker ; 252E counter ran past zero -> reload the marker onto the unit
|
|
lda spriteEnableShadow ; 2530 sprite enable shadow
|
|
and #$02 ; 2533 bit 1 = sprite 1, the marker
|
|
bne L_253C ; 2535 currently visible -> hide it, which makes the line end blink
|
|
L_2537:
|
|
lda #$02 ; 2537 sprite 1 bit
|
|
jmp enableSprites ; 2539 show the marker (and return)
|
|
|
|
L_253C:
|
|
jmp hideSprite1 ; 253C no marker wanted: switch sprite 1 off
|
|
|
|
L_253F:
|
|
rts ; 253F nothing to do this frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resetPathMarker - Puts the path marker back at the start: sprite 1 off, marker cell and pass counter
|
|
; back to the selected unit, and sprite 1's shape reloaded. Normally that is the small grey block;
|
|
; when the mini-map is showing the zoomed group box ($920A bit 7) the marker instead copies sprite 0's
|
|
; cursor box so it matches the enlarged cells.
|
|
; In: selectedUnit $90F8, groupBoxActive $920A bit 7, sprite 0 shape at $8A00
|
|
; Out: sprite 1 shape at $8A40 and colour shadow $9009, activeSpriteIndex $91CC left 0,
|
|
; pathMarkerState/Col/Row reloaded, sprite 1 enabled in the zoomed case
|
|
; Called from: the info-panel entry and idle loops ($227B, $22FF), the unit menu at $5F18, and from
|
|
; animatePathMarkerStep when the pass counter runs out ($252E)
|
|
; ----------------------------------------------------------------------
|
|
resetPathMarker:
|
|
jsr hideSprite1 ; 2540 marker off while its shape is swapped
|
|
jsr resetPathMarkerToUnit ; 2543 pass counter back to 5 and the marker back onto the unit
|
|
lda groupBoxActive ; 2546 bit 7 set = the mini-map is zoomed onto the group box
|
|
bmi L_2559 ; 2549 zoomed: use a copy of the cursor box instead
|
|
lda #$01 ; 254B sprite 1
|
|
sta activeSpriteIndex ; 254D tell setCursorShapeBlock which sprite to load
|
|
jsr setCursorShapeBlock ; 2550 load the small solid block shape ($9E92) and colour it grey
|
|
lda #$00 ; 2553 sprite 0
|
|
sta activeSpriteIndex ; 2555 back to the default sprite for the cursor routines
|
|
rts ; 2558 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Zoomed mini-map: the marker has to be as large as the cursor box, so sprite 1 simply gets a copy of
|
|
; sprite 0's current shape.
|
|
; ----------------------------------------------------------------------
|
|
L_2559:
|
|
ldy #$3F ; 2559 64 shape bytes, last one first
|
|
L_255B:
|
|
lda sprite0Shape,y ; 255B byte of the sprite 0 cursor box ($8A00)
|
|
sta sprite1Shape,y ; 255E into the sprite 1 shape ($8A40)
|
|
dey ; 2561 next byte down
|
|
bpl L_255B ; 2562 until all 64 are copied
|
|
lda #$02 ; 2564 sprite 1 bit
|
|
jsr enableSprites ; 2566 make the marker visible
|
|
lda #$0C ; 2569 grey
|
|
sta sprite1ColourShadow ; 256B sprite 1 colour shadow, copied to $D028 by the IRQ
|
|
rts ; 256E done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resetPathMarkerToUnit - Reloads the marker's pass counter with 5 and moves the marker cell onto the
|
|
; selected unit's map position, then falls into positionMarkerSprite to place sprite 1 there.
|
|
; In: selectedUnit $90F8, unitColTable $F640, unitRowTable $F6A4
|
|
; Out: pathMarkerState $24E1 = 5, pathMarkerCol/Row $24E2/$24E3, sprite 1 position shadows; X =
|
|
; column, Y = row
|
|
; Called from: the path-preview entry at $225D and resetPathMarker ($2543)
|
|
; ----------------------------------------------------------------------
|
|
resetPathMarkerToUnit:
|
|
lda #$05 ; 256F five more passes over the line
|
|
sta pathMarkerState ; 2571 reload the pass counter
|
|
ldy selectedUnit ; 2574 the selected unit
|
|
lda unitColTable,y ; 2577 its map column
|
|
sta pathMarkerCol ; 257A marker column
|
|
tax ; 257D X = column for positionMarkerSprite
|
|
lda unitRowTable,y ; 257E its map row
|
|
sta pathMarkerRow ; 2581 marker row
|
|
tay ; 2584 Y = row for positionMarkerSprite
|
|
|
|
; ----------------------------------------------------------------------
|
|
; positionMarkerSprite - Converts a map cell to mini-map sprite coordinates (column*4+$1F, row*4+$39,
|
|
; or the group-box mapping when the mini-map is zoomed) and stores them in sprite 1's position
|
|
; shadows, which the raster IRQ copies to the VIC.
|
|
; In: X = map column, Y = map row
|
|
; Out: sprite1XShadow $9011, sprite1YShadow $9019
|
|
; Called from: animatePathMarkerStep ($2524) and by fall-through from resetPathMarkerToUnit
|
|
; ----------------------------------------------------------------------
|
|
positionMarkerSprite:
|
|
jsr mapCellToSpriteXY ; 2585 cell -> sprite coordinates on the 40x40 mini-map (4 pixels per cell)
|
|
stx sprite1XShadow ; 2588 sprite 1 X shadow
|
|
sty sprite1YShadow ; 258B sprite 1 Y shadow
|
|
rts ; 258E done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; recenterViewportOnUnit - Makes sure the selected unit sits under the battlefield cursor. The cursor
|
|
; is always the centre cell of the 7x5 view (viewOriginCol+3, viewOriginRow+2); if the unit is
|
|
; somewhere else the view is scrolled to it one cell per frame by scrollViewToUnit.
|
|
; In: selectedUnit $90F8, viewOriginCol zp_A5 / viewOriginRow zp_A6, unitColTable $F640 /
|
|
; unitRowTable $F6A4
|
|
; Out: lineCurCol zp_93 / lineCurRow zp_92 = the cursor cell (the scroll starts there); the view
|
|
; scrolled and redrawn
|
|
; Called from: the info-panel entry at $2239 and the unit selection code at $4BF6 and $4C4C
|
|
; ----------------------------------------------------------------------
|
|
recenterViewportOnUnit:
|
|
ldx selectedUnit ; 258F the unit that must end up under the cursor
|
|
lda #$03 ; 2592 the cursor sits 3 cells right of the view origin
|
|
clc ; 2594 plain add
|
|
adc viewOriginCol ; 2595 map column of the left edge of the view
|
|
sta lineCurCol ; 2597 cursor column, also the line stepper start
|
|
lda #$02 ; 2599 and 2 cells below the top edge
|
|
clc ; 259B plain add
|
|
adc viewOriginRow ; 259C map row of the top edge of the view
|
|
sta lineCurRow ; 259E cursor row
|
|
lda lineCurCol ; 25A0 cursor column
|
|
cmp unitColTable,x ; 25A2 same column as the unit?
|
|
bne L_25AE ; 25A5 no -> scroll
|
|
lda lineCurRow ; 25A7 cursor row
|
|
cmp unitRowTable,x ; 25A9 same row as the unit?
|
|
beq L_25B6 ; 25AC the unit is already under the cursor: nothing to do
|
|
L_25AE:
|
|
lda #$01 ; 25AE 1 frame per scroll step, i.e. animated
|
|
ldx selectedUnit ; 25B0 the unit to scroll to
|
|
jsr scrollViewToUnit ; 25B3 walk the view over to it cell by cell
|
|
L_25B6:
|
|
rts ; 25B6 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; consumeSpaceKey - Eats a pending SPACE. The IRQ leaves the last printable key in lastTypedKey
|
|
; ($9248) and several loops close on SPACE, so a screen that was itself closed with SPACE clears the
|
|
; key before returning, otherwise the next loop would close immediately as well.
|
|
; In: lastTypedKey $9248
|
|
; Out: lastTypedKey = $FF (no key) when it held $A0 (space)
|
|
; Called from: $1863 and $2073
|
|
; ----------------------------------------------------------------------
|
|
consumeSpaceKey:
|
|
lda lastTypedKey ; 25B7 last printable key latched by the IRQ
|
|
cmp #$A0 ; 25BA $A0 = space (PETSCII with bit 7 set)
|
|
bne L_25C3 ; 25BC some other key: leave it for the caller
|
|
lda #$FF ; 25BE 'no key pending'
|
|
sta lastTypedKey ; 25C0 swallow the space so the next loop does not act on it
|
|
L_25C3:
|
|
rts ; 25C3 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; callSetupOverlayHook - Trampoline into overlay variant A at $8658 (handleStarKeyToggle), the
|
|
; per-frame '*' key handler of the non-battle phases: it toggles setupToggleFlag $92F3 bit 7 (DESTROY
|
|
; mode during unit placement, CUSTOM rules on the setup screen) and reports C=1 when it consumed the
|
|
; key. It may only be called while gamePhase zp_B1 is non-zero, because in overlay variant B address
|
|
; $8658 is in the middle of an instruction.
|
|
; In: gamePhase zp_B1 must be non-zero (overlay A resident); the hook itself reads chatState $929C
|
|
; and lastTypedKey $9248
|
|
; Out: C = 1 when '*' was pressed and the toggle changed, C = 0 otherwise
|
|
; Called from: the map main loop at $2026 and the info-panel loop at $2348
|
|
; ----------------------------------------------------------------------
|
|
callSetupOverlayHook:
|
|
jmp handleStarKeyToggle ; 25C4 into overlay A: '*' toggles DESTROY / CUSTOM, C=1 when it acted
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getViewportOriginPointer - Builds a map pointer for the top-left cell of the 7x5 battlefield view.
|
|
; The view origin may be off the map (it is clamped only for the cursor cell), so a negative row is
|
|
; handled by taking row 0 and stepping the pointer back 40 bytes per row, and a negative column by the
|
|
; sign-extended add in getViewportRowPointer.
|
|
; In: viewOriginCol zp_A5, viewOriginRow zp_A6, the map row address tables $BFB0/$BFD8
|
|
; Out: mapCellPtr zp_48/zp_49 -> the origin cell (may point in front of $F000 when the view hangs off
|
|
; the map), A = that cell's byte only when the origin is really on the map, Y = 0
|
|
; Called from: drawViewport ($26D1) and the $6F00 overlays ($71D4, $71F9, $740D, $7430, $7BCC)
|
|
; ----------------------------------------------------------------------
|
|
getViewportOriginPointer:
|
|
ldy viewOriginRow ; 25C7 map row of the top edge of the view
|
|
bpl getViewportRowPointer ; 25C9 row is on the map: ordinary row lookup
|
|
ldy #$00 ; 25CB the view hangs off the top of the map: start from row 0
|
|
jsr getViewportRowPointer ; 25CD pointer to (viewOriginCol, row 0)
|
|
ldy viewOriginRow ; 25D0 the negative row count, e.g. $FE = two rows above the map
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Step the pointer back one map row (40 cells) per row above the map, counting Y up to 0.
|
|
; ----------------------------------------------------------------------
|
|
L_25D2:
|
|
lda mapCellPtr ; 25D2 pointer low byte
|
|
sec ; 25D4 no borrow
|
|
sbc #$28 ; 25D5 one map row = 40 cells
|
|
sta mapCellPtr ; 25D7 store it back
|
|
lda mapCellPtrHi ; 25D9 pointer high byte
|
|
sbc #$00 ; 25DB propagate the borrow
|
|
sta mapCellPtrHi ; 25DD store it back
|
|
iny ; 25DF one row less to go
|
|
bne L_25D2 ; 25E0 until the negative row count reaches 0
|
|
rts ; 25E2 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getViewportRowPointer - Map pointer for the cell (viewOriginCol, Y). A negative view column is
|
|
; handled by reading column 0 and then adding the signed offset to the pointer, so the caller gets a
|
|
; pointer that is correct even when the view sticks out over the left edge of the map.
|
|
; In: Y = map row 0-39, viewOriginCol zp_A5
|
|
; Out: mapCellPtr zp_48/zp_49, A = the cell byte when the column is on the map, Y = 0
|
|
; Called from: getViewportOriginPointer ($25CD and the branch at $25C9)
|
|
; ----------------------------------------------------------------------
|
|
getViewportRowPointer:
|
|
ldx viewOriginCol ; 25E3 map column of the left edge of the view
|
|
bpl readMapCell ; 25E5 column is on the map: plain lookup, and its result is our result
|
|
ldx #$00 ; 25E7 the view hangs off the left edge: read column 0 instead
|
|
jsr readMapCell ; 25E9 pointer to (column 0, row Y)
|
|
lda mapCellPtr ; 25EC pointer low byte
|
|
clc ; 25EE plain add
|
|
adc viewOriginCol ; 25EF add the negative column offset...
|
|
sta mapCellPtr ; 25F1 store it back
|
|
lda mapCellPtrHi ; 25F3 pointer high byte
|
|
adc #$FF ; 25F5 ...sign extended: -1 in the high byte
|
|
sta mapCellPtrHi ; 25F7 store it back
|
|
rts ; 25F9 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readMapCell - The map accessor of the whole game: forms mapCellPtr = rowAddress[Y] + X from the
|
|
; 40-entry row address tables at $BFB0/$BFD8 (built by the start-up code at $0339 as $F000 + 40*row)
|
|
; and returns the byte of cell (column X, row Y) of the 40x40 battlefield.
|
|
; In: X = map column 0-39, Y = map row 0-39; row address tables $BFB0 (lo) / $BFD8 (hi)
|
|
; Out: A = the map byte (bit 7 set: bits 0-6 are the index of the unit standing there and the covered
|
|
; terrain is in $FAF0+unit; otherwise a terrain code, $40 = open ground), mapCellPtr zp_48/zp_49
|
|
; -> that cell, Y = 0
|
|
; Called from: about 30 places - the main program ($1F16, $1F3F, $25E9, $36FB, $37D7, $3C2C, $3E06,
|
|
; $3E38, $3ECD, $3F3C, $42E7, $4571, $476A, $5400, $2619), both $6F00 overlays and the solo
|
|
; trainer ($E97B, $E9B2)
|
|
; ----------------------------------------------------------------------
|
|
readMapCell:
|
|
txa ; 25FA map column
|
|
clc ; 25FB no carry into the add
|
|
adc mapRowAddrLoTable,y ; 25FC + low byte of the address of map row Y ($F000 + 40*row)
|
|
sta mapCellPtr ; 25FF map pointer low byte
|
|
lda #$00 ; 2601 only the carry goes into the high byte
|
|
adc mapRowAddrHiTable,y ; 2603 + high byte of the row address
|
|
sta mapCellPtrHi ; 2606 map pointer high byte
|
|
ldy #$00 ; 2608 index 0 for the indirect read
|
|
lda (mapCellPtr),y ; 260A the cell byte: bit 7 = a unit stands here, else a terrain code
|
|
rts ; 260C done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readCellUnderCursor - Reads the map cell under the battlefield cursor. The cursor never moves: it is
|
|
; always the centre cell of the 7x5 view, i.e. (viewOriginCol+3, viewOriginRow+2); the joystick
|
|
; scrolls the view instead.
|
|
; In: viewOriginCol zp_A5, viewOriginRow zp_A6
|
|
; Out: A = the map byte, mapCellPtr zp_48/zp_49 -> that cell, X = cursor column, Y = 0
|
|
; Called from: the map main loop ($2049, $207B), the info-panel loop ($2310) and the console at $63D9
|
|
; ----------------------------------------------------------------------
|
|
readCellUnderCursor:
|
|
lda viewOriginCol ; 260D map column of the left edge of the view
|
|
clc ; 260F plain add
|
|
adc #$03 ; 2610 the cursor is the 4th of the 7 visible columns
|
|
tax ; 2612 X = cursor column
|
|
lda viewOriginRow ; 2613 map row of the top edge of the view
|
|
clc ; 2615 plain add
|
|
adc #$02 ; 2616 the cursor is the 3rd of the 5 visible rows
|
|
tay ; 2618 Y = cursor row
|
|
jmp readMapCell ; 2619 read that cell and return its byte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearInfoPanel - Selects the unit info panel as the text window and blanks it with the text engine's
|
|
; window clear, using the current textColour zp_3D and fill glyph zp_3E.
|
|
; In: textColour zp_3D, textFillGlyph zp_3E
|
|
; Out: the text window set to rows 12-20 / columns 25-38 and cleared; text cursor zp_3F/zp_40 homed
|
|
; Called from: 20 places - the manual check ($09EC/$0A44), the map loop ($2004, $22B3, $2356),
|
|
; redrawGameScreen ($267C), the screen switcher ($2DEE), the unit and console menus ($4BBF, $4C3D,
|
|
; $5E49, $5E9F, $6151, $6201, $6245, $6374, $6441, $64EE, $65FF) and overlay A
|
|
; ----------------------------------------------------------------------
|
|
clearInfoPanel:
|
|
jsr setInfoPanelWindow ; 261C rows 12-20, columns 25-38
|
|
jmp bootDriveDataBit ; 261F fill the window with textFillGlyph and return (also textEngineC000:clearTextWindow)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setInfoPanelWindow - Defines the text window that covers the unit info panel: columns 25-38, rows 12
|
|
; to 20 (row 21 is the exclusive limit). It resets the window to full screen first so that leftovers
|
|
; of a narrower window do not survive. It exits with A = $0C, which printUnitStatusLines relies on to
|
|
; reach row 12.
|
|
; In: -
|
|
; Out: textWindowTop zp_39 = $0C, textWindowBottom zp_3A = $15, textWindowLeft zp_3B = $19,
|
|
; textWindowWidth zp_3C = $0E; A = $0C
|
|
; Called from: clearInfoPanel ($261C) and printUnitStatusLines ($24C7)
|
|
; ----------------------------------------------------------------------
|
|
setInfoPanelWindow:
|
|
jsr setFullScreenWindow ; 2622 start from a full-screen window
|
|
lda #$19 ; 2625 screen column 25
|
|
sta textWindowLeft ; 2627 window left edge
|
|
lda #$0E ; 2629 14 columns -> columns 25-38
|
|
sta textWindowWidth ; 262B window width / wrap point
|
|
lda #$15 ; 262D row 21 = the exclusive bottom limit
|
|
sta textWindowBottom ; 262F window bottom
|
|
lda #$0C ; 2631 top row 12
|
|
sta textWindowTop ; 2633 window top; A stays $0C for the caller
|
|
rts ; 2635 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearScreenAndRedrawGameScreen - Rebuilds the battlefield display from scratch: sets the VIC up for
|
|
; the multicolour bitmap and wipes it grey, then falls into redrawGameScreen.
|
|
; In: -
|
|
; Out: the whole screen, see initBitmapScreen and redrawGameScreen
|
|
; Called from: start-up ($0A95) and overlay A ($7D59, $8232, $8559)
|
|
; ----------------------------------------------------------------------
|
|
clearScreenAndRedrawGameScreen:
|
|
jsr initBitmapScreen ; 2636 VIC into multicolour bitmap mode, bitmap and colour RAM cleared to grey
|
|
|
|
; ----------------------------------------------------------------------
|
|
; redrawGameScreen - Draws the whole battlefield screen: the three panel frames (mini-map, tactical
|
|
; view, info panel), the bottom status bar, the 40x40 overview map, the 7x5 tactical view and finally
|
|
; the current status message. gamePhase zp_B1 gates the map: bit 7 (menu phases) leaves the whole map
|
|
; area empty, bit 6 (unit setup) draws the overview but not the tactical view.
|
|
; In: gamePhase zp_B1, currentScreen $90FB (via drawMainWindowFrame), the map at $F000 and the unit
|
|
; arrays, shownMessageId zp_7B
|
|
; Out: vicCtrl2Shadow $9001 = $D8 (multicolour), vicBg0Shadow $9004 = 7, cellBackgroundColour $9226 =
|
|
; 0, the glyph plotter parameters $2A29-$2A2C = 0, textColour zp_3D, the whole screen redrawn
|
|
; Called from: $1FCD (the battlefield screen entry), and by fall-through from
|
|
; clearScreenAndRedrawGameScreen
|
|
; ----------------------------------------------------------------------
|
|
redrawGameScreen:
|
|
jsr patchUnitSideBranches ; 2639 re-patch the own/enemy branch in renderMapTile for the current side and replay viewpoint
|
|
lda #$D8 ; 263C $D016: multicolour on (bit 4), 40 columns (bit 3), no fine scroll
|
|
sta vicCtrl2Shadow ; 263E the raster IRQ copies this shadow to $D016
|
|
lda #$0C ; 2641 grey
|
|
sta textColour ; 2643 text colour used for the frames
|
|
jsr setFullScreenWindow ; 2645 text window = the whole screen
|
|
jsr bootDriveDataBit ; 2648 blank the entire bitmap through the text engine (also textEngineC000:clearTextWindow)
|
|
lda #$00 ; 264B reset the glyph plotter parameter block...
|
|
sta useDrawColourFlag ; 264D ...no forced colour...
|
|
sta drawColourByte ; 2650 ...colour byte 0...
|
|
sta blockFillFlag ; 2653 ...no block fill...
|
|
sta blockFillChar ; 2656 ...and fill character 0
|
|
lda #$07 ; 2659 yellow
|
|
sta vicBg0Shadow ; 265B $D021 background = the colour of the %00 pixels of the map tiles
|
|
lda #$00 ; 265E black
|
|
sta cellBackgroundColour ; 2660 background colour used behind drawn character cells
|
|
lda #$00 ; 2663 redundant: the block-fill flag was already cleared at $2653
|
|
sta blockFillFlag ; 2665 (leftover store, no effect)
|
|
jsr drawMiniMapFrame ; 2668 22x22 frame at column 0 around the strategic map
|
|
jsr drawViewportFrame ; 266B 16x12 frame at column 24 around the tactical view; also clears its interior
|
|
jsr drawInfoPanelFrame ; 266E 16x11 frame at column 24 row 11 around the unit info panel
|
|
jsr drawMainWindowFrame ; 2671 the 38x3 status bar across the bottom
|
|
lda gamePhase ; 2674 0 during the battle, non-zero during setup and in the menus
|
|
bpl L_2682 ; 2676 bit 7 clear -> the battlefield may be drawn
|
|
lda #$00 ; 2678 colour 0
|
|
sta textColour ; 267A text colour for the clear
|
|
jsr clearInfoPanel ; 267C menu phases: leave the info panel blank
|
|
jmp L_268C ; 267F and skip both map drawings
|
|
|
|
L_2682:
|
|
jsr drawOverviewMap ; 2682 the whole 40x40 map at 4x4 pixels per cell
|
|
bit gamePhase ; 2685 test gamePhase again
|
|
bvs L_268C ; 2687 bit 6 set (unit placement) -> no tactical view
|
|
jsr drawViewport ; 2689 the 7x5 cell tactical view
|
|
L_268C:
|
|
jmp redrawStatusMessage ; 268C put the current status message back on the bottom bar
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawMiniMapFrame - Draws window 0: the 22x22 character frame at column 0, row 0 that surrounds the
|
|
; 20x20 strategic mini-map.
|
|
; In: -
|
|
; Out: textColour zp_3D = $0C, the frame on screen, the window geometry left in
|
|
; $2A2D/$2A2E/$9221/$9222 by drawWindowFrame
|
|
; Called from: redrawGameScreen ($2668)
|
|
; ----------------------------------------------------------------------
|
|
drawMiniMapFrame:
|
|
lda #$0C ; 268F grey
|
|
sta textColour ; 2691 colour of the frame characters
|
|
ldx #$00 ; 2693 window 0 = column 0, row 0, 22x22
|
|
jmp drawWindowFrame ; 2695 draw it and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawViewportFrame - Draws window 1: the 16x12 character frame at column 24, row 0 around the
|
|
; tactical view, then falls into clearViewportTextArea to blank the interior.
|
|
; In: -
|
|
; Out: textColour zp_3D, the frame drawn and the 14x10 interior cleared
|
|
; Called from: redrawGameScreen ($266B)
|
|
; ----------------------------------------------------------------------
|
|
drawViewportFrame:
|
|
lda #$0C ; 2698 grey
|
|
sta textColour ; 269A colour of the frame characters
|
|
ldx #$01 ; 269C window 1 = column 24, row 0, 16x12
|
|
jsr drawWindowFrame ; 269E draw the frame
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearViewportTextArea - Declares the interior of the tactical view (rows 1-10, columns 25-38, i.e.
|
|
; the 7x5 map cells at 2x2 characters each) as the text window and clears it in colour 0.
|
|
; In: -
|
|
; Out: textWindowTop zp_39 = 1, textWindowBottom zp_3A = $0B, textWindowLeft zp_3B = $19,
|
|
; textWindowWidth zp_3C = $0E, textColour zp_3D = 0; the area blanked
|
|
; Called from: the screen-switch clear at $2DF4, overlay A ($8425), and by fall-through from
|
|
; drawViewportFrame
|
|
; ----------------------------------------------------------------------
|
|
clearViewportTextArea:
|
|
lda #$01 ; 26A1 first character row of the view
|
|
sta textWindowTop ; 26A3 window top
|
|
lda #$19 ; 26A5 screen column 25
|
|
sta textWindowLeft ; 26A7 window left edge
|
|
lda #$0E ; 26A9 14 columns = 7 map cells
|
|
sta textWindowWidth ; 26AB window width
|
|
lda #$0B ; 26AD row 11 = exclusive limit, so rows 1-10 = 5 map rows
|
|
sta textWindowBottom ; 26AF window bottom
|
|
lda #$00 ; 26B1 colour 0
|
|
sta textColour ; 26B3 text colour for the clear
|
|
jmp bootDriveDataBit ; 26B5 blank the area and return (also textEngineC000:clearTextWindow)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawInfoPanelFrame - Draws window 2: the 16x11 character frame at column 24, row 11 around the unit
|
|
; info panel.
|
|
; In: -
|
|
; Out: textColour zp_3D = $0C, the frame on screen
|
|
; Called from: redrawGameScreen ($266E)
|
|
; ----------------------------------------------------------------------
|
|
drawInfoPanelFrame:
|
|
lda #$0C ; 26B8 grey
|
|
sta textColour ; 26BA colour of the frame characters
|
|
ldx #$02 ; 26BC window 2 = column 24, row 11, 16x11
|
|
jmp drawWindowFrame ; 26BE draw it and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawViewport - Redraws the tactical view: 5 rows of 7 map cells, each cell a 16x16 pixel multicolour
|
|
; tile occupying 2x2 character cells, into character rows 1-10 and columns 25-38. It walks the map
|
|
; from the view origin, has renderMapTile build each tile in the buffer at $04C8-$04E7 and then copies
|
|
; the 32 bytes into the bitmap - 16 bytes to the upper character pair and 16 to the pair one character
|
|
; row (320 bytes) lower, through a store whose address is self-modified. The view origin and the text
|
|
; plotter's scratch bytes are saved and restored, because the loop walks the origin to tell
|
|
; renderMapTile which map cell it is drawing.
|
|
; In: viewOriginCol zp_A5 / viewOriginRow zp_A6, the map at $F000 and the unit arrays, gamePhase
|
|
; zp_B1 bit 6 (suppresses units)
|
|
; Out: bitmap, screen RAM and colour RAM of the 14x10 character area; scanResultCount zp_2F and
|
|
; magnifySrcCol zp_31 used as row/column counters, mapCellPtr zp_48/zp_49, bitmapPtr
|
|
; zp_B4-screenPtrHi zp_B7, $2A2D/$2A2E, drawCellSaveCol/Row $90F6/$90F7; self-modifies the store
|
|
; at tileCopyStore ($2703)
|
|
; Called from: everywhere the battlefield changes - the map main loop ($1FA2, $200F), redrawGameScreen
|
|
; ($2689), the tick loop ($4023, $413A, $4152), the unit menus ($4BDE, $5E38, $5E7F, $605F, $61F8,
|
|
; $63A4, $636B, $64BB, $64CE), overlay A ($83F5), the screen switcher ($075B) and the text engine
|
|
; ($C797)
|
|
; ----------------------------------------------------------------------
|
|
drawViewport:
|
|
lda scratch18 ; 26C1 the text plotter's row byte...
|
|
pha ; 26C3 ...is borrowed as the character row counter, so save it
|
|
lda scratch19 ; 26C4 the text plotter's column byte...
|
|
pha ; 26C6 ...is borrowed as the character column counter
|
|
lda viewOriginCol ; 26C7 map column of the view origin
|
|
sta drawCellSaveCol ; 26C9 saved: the loop walks the origin cell by cell
|
|
lda viewOriginRow ; 26CC map row of the view origin
|
|
sta drawCellSaveRow ; 26CE saved as well
|
|
jsr getViewportOriginPointer; 26D1 mapCellPtr -> the map cell at the top-left corner of the view
|
|
lda #$01 ; 26D4 character row 1 = the first row inside the frame
|
|
sta windowOriginRow ; 26D6 window origin row used by the colour writes
|
|
sta scratch18 ; 26D9 row for calcCellPointers
|
|
lda #$00 ; 26DB start at the top
|
|
sta scanResultCount ; 26DD viewport row counter 0-4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Outer loop: one row of 7 map cells = two character rows.
|
|
; ----------------------------------------------------------------------
|
|
L_26DF:
|
|
lda #$19 ; 26DF character column 25 = the first column inside the frame
|
|
sta windowOriginCol ; 26E1 window origin column
|
|
sta scratch19 ; 26E4 column for calcCellPointers
|
|
lda #$00 ; 26E6 start at the left
|
|
sta magnifySrcCol ; 26E8 viewport column counter 0-6
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Inner loop: one map cell = one 16x16 tile = 2x2 characters.
|
|
; ----------------------------------------------------------------------
|
|
L_26EA:
|
|
jsr calcCellPointers ; 26EA bitmapPtr zp_B4/B5 and screenPtr zp_B6/B7 for character (row,column)
|
|
ldy magnifySrcCol ; 26ED offset of this cell inside the map row
|
|
lda (mapCellPtr),y ; 26EF the map byte of the cell
|
|
jsr renderMapTile ; 26F1 build its 32-byte tile in $04C8 and write the cell colours
|
|
lda bitmapPtr ; 26F4 bitmap address of the upper-left character
|
|
sta tileCopyStore+1 ; 26F6 patch the destination of the copy store...
|
|
lda bitmapPtrHi ; 26F9 ...
|
|
sta tileCopyStore+2 ; 26FB ...so it writes into this cell
|
|
ldy #$00 ; 26FE start at the first tile byte
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the tile buffer into the bitmap: bytes $00-$0F are the two upper characters (which are adjacent
|
|
; in a C64 bitmap), bytes $10-$1F the two characters one character row lower.
|
|
; ----------------------------------------------------------------------
|
|
L_2700:
|
|
lda play4Row12,y ; 2700 byte of the tile buffer
|
|
tileCopyStore:
|
|
sta irqVectorHi,y ; 2703 into the bitmap; the absolute address is patched at $26F6/$26FB and $2714/$271B (the unpatched operand in the file happens to read $FFFF)
|
|
iny ; 2706 next byte
|
|
cpy #$20 ; 2707 32 bytes = four characters
|
|
bcs L_2720 ; 2709 tile finished
|
|
cpy #$10 ; 270B the first 16 bytes are the upper character pair
|
|
bne L_2700 ; 270D not there yet
|
|
clc ; 270F no carry into the add
|
|
lda bitmapPtr ; 2710 bitmap address of the cell again
|
|
adc #$30 ; 2712 +$130, and Y has already reached $10, so the store lands at +$140 = 320 = one character row down
|
|
sta tileCopyStore+1 ; 2714 re-patch the store...
|
|
lda bitmapPtrHi ; 2717 ...
|
|
adc #$01 ; 2719 ...
|
|
sta tileCopyStore+2 ; 271B ...to the lower character pair
|
|
bne L_2700 ; 271E always taken (the bitmap lives at $A000)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Cell done: step one map cell to the right = two character columns.
|
|
; ----------------------------------------------------------------------
|
|
L_2720:
|
|
inc scratch19 ; 2720 one map cell is two characters wide...
|
|
inc scratch19 ; 2722 ...so advance the character column by 2
|
|
inc viewOriginCol ; 2724 renderMapTile reads the view origin as the coordinates of the cell it draws
|
|
inc magnifySrcCol ; 2726 next cell of this row
|
|
ldy magnifySrcCol ; 2728 the column counter
|
|
cpy #$07 ; 272A 7 cells across the view
|
|
bcc L_26EA ; 272C keep going along the row
|
|
inc scratch18 ; 272E row done: one map cell is two characters high...
|
|
inc scratch18 ; 2730 ...so advance the character row by 2
|
|
inc viewOriginRow ; 2732 and the map row the next line draws
|
|
lda drawCellSaveCol ; 2734 the saved view origin column
|
|
sta viewOriginCol ; 2737 back to the left edge of the view
|
|
inc scanResultCount ; 2739 one more viewport row done
|
|
ldy scanResultCount ; 273B the row counter
|
|
cpy #$05 ; 273D 5 cell rows in the view
|
|
bcs L_2751 ; 273F all rows drawn
|
|
lda mapCellPtr ; 2741 map pointer low byte
|
|
clc ; 2743 plain add
|
|
adc #$28 ; 2744 one map row = 40 cells
|
|
sta mapCellPtr ; 2746 store it back
|
|
lda mapCellPtrHi ; 2748 map pointer high byte
|
|
adc #$00 ; 274A propagate the carry
|
|
sta mapCellPtrHi ; 274C store it back
|
|
jmp L_26DF ; 274E draw the next row of cells
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Restore everything the loop borrowed: the view origin and the text plotter's scratch bytes.
|
|
; ----------------------------------------------------------------------
|
|
L_2751:
|
|
lda drawCellSaveCol ; 2751 saved origin column
|
|
sta viewOriginCol ; 2754 restore it (the loop walked it across the view)
|
|
lda drawCellSaveRow ; 2756 saved origin row
|
|
sta viewOriginRow ; 2759 restore it
|
|
pla ; 275B saved text column
|
|
sta scratch19 ; 275C give the text plotter its scratch byte back
|
|
pla ; 275E saved text row
|
|
sta scratch18 ; 275F and the other one
|
|
rts ; 2761 done
|
|
|
|
|
|
; nybbleToMulticolourTable - expands the four pixels of one nybble of a terrain shape into one
|
|
; multicolour bitmap byte: every set bit becomes the pixel pair %10, i.e. bit 0 -> $02, bit 1 ->
|
|
; $08, bit 2 -> $20, bit 3 -> $80. %10 pixels take their colour from the low nybble of the
|
|
; screen RAM byte, which renderMapTile fills with the terrain colour; %00 pixels show the
|
|
; $D021 background (yellow) and the %11 pixels of the unit glyphs come from colour RAM.
|
|
nybbleToMulticolourTable:
|
|
.byte $00,$02,$08,$0A,$20,$22,$28,$2A; 2762 .... "(* nybbles $0-$7: bit 0 -> $02, bit 1 -> $08, bit 2 -> $20
|
|
.byte $80 ; 276A nybbles $8-$F: bit 3 -> $80; $AA = all four pixels set
|
|
.byte $82,$88,$8A,$A0,$A2,$A8,$AA; 276B .......
|
|
|
|
; ----------------------------------------------------------------------
|
|
; renderMapTile - Builds the 16x16 pixel multicolour tile of one map cell in the 32-byte buffer at
|
|
; $04C8-$04E7 and writes the cell's four screen-RAM colour bytes. The cell byte in A is either a
|
|
; terrain code (the 8x8 one-bit shape at $9C6E + (code-$40)*8, doubled in both directions through
|
|
; nybbleToMulticolourTable) or, with bit 7 set, a unit index, in which case the terrain hidden
|
|
; underneath is drawn first and the 32-byte unit glyph is ORed on top and the unit colour written to
|
|
; colour RAM. Cells outside the 40x40 map use the four border shapes $68-$6B. The caller copies the
|
|
; buffer into the bitmap.
|
|
; In: A = the map cell byte; viewOriginCol zp_A5 / viewOriginRow zp_A6 = that cell's map coordinates;
|
|
; screenPtr zp_B6/zp_B7 -> its screen RAM cell; gamePhase zp_B1 bit 6 suppresses units;
|
|
; ownUnitColour zp_77 / enemyUnitColour zp_78; filmPlaybackMode $0B9B, revealAllUnits $92FC,
|
|
; highlightedUnit $9204, highlightedGroupKey $9205, uiLevel $9209; the branch opcodes at
|
|
; ownSideBranchOpcode ($2840) and unitVisibleBranchOpcode ($2846) decide which half of the unit
|
|
; array is 'ours'
|
|
; Out: the 32-byte tile buffer $04C8-$04E7; the terrain colour in the four screen RAM bytes at
|
|
; (zp_B6)+0/+1/+40/+41 and, when a unit is drawn, the unit colour in the matching colour RAM bytes
|
|
; (zp_B7 is advanced by $4C, so screenPtr points into $D800 on exit); tileColour zp_32, glyphPtr
|
|
; zp_B2/B3; self-modifies mapCellByteImm+1 ($2808), terrainShapeRowImm+1 ($27D4),
|
|
; terrainShapeRowCounter+1 ($27F2), unitGlyphEorMask+1 ($28B8) and cloakSkipFlagImm+1 ($28C0)
|
|
; Called from: drawViewport ($26F1) and the single-cell redraw at $5D45
|
|
; Off-map check: the view may hang over the edge of the battlefield. Both coordinates are compared
|
|
; unsigned against 40, which also catches the negative (>= $80) values.
|
|
; ----------------------------------------------------------------------
|
|
renderMapTile:
|
|
sta mapCellByteImm+1 ; 2772 keep the cell byte in the LDA # at mapCellByteImm for the unit pass below
|
|
tay ; 2775 Y = cell byte
|
|
lda viewOriginCol ; 2776 map column of this cell
|
|
cmp #$28 ; 2778 40 = past the right edge (negative columns are >= 40 unsigned too)
|
|
bcs L_2782 ; 277A off the map
|
|
lda viewOriginRow ; 277C map row of this cell
|
|
cmp #$28 ; 277E 40 = past the bottom edge
|
|
bcc L_2790 ; 2780 the cell is on the battlefield
|
|
L_2782:
|
|
lda viewOriginRow ; 2782 off-map: pick one of four border patterns from the coordinates...
|
|
clc ; 2784 plain add
|
|
adc viewOriginCol ; 2785 ...so the border is dithered rather than uniform
|
|
and #$03 ; 2787 four patterns
|
|
clc ; 2789 no carry
|
|
adc #$68 ; 278A shapes $68-$6B tile everything outside the battlefield
|
|
tay ; 278C Y = border shape code
|
|
sta mapCellByteImm+1 ; 278D and make the unit pass see plain terrain, not a unit index
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Terrain selection: when a unit stands on the cell the map byte holds its index, and the terrain it
|
|
; covers is kept in unitTerrainUnder ($FAF0 + unit). unitTerrainUnderAlias is that table biased by
|
|
; -$80 so it can be indexed with the raw $80|unit byte.
|
|
; ----------------------------------------------------------------------
|
|
L_2790:
|
|
tya ; 2790 the cell byte
|
|
bpl L_2797 ; 2791 bit 7 clear = plain terrain, use it directly
|
|
lda unitTerrainUnderAlias,y ; 2793 a unit stands here: fetch the terrain it is hiding
|
|
tay ; 2796 Y = terrain code
|
|
L_2797:
|
|
cmp #$40 ; 2797 $40 = open ground, which has no shape at all
|
|
bne L_27A8 ; 2799 real terrain: draw its shape
|
|
lda #$00 ; 279B blank tile
|
|
ldy #$1F ; 279D 32 buffer bytes
|
|
L_279F:
|
|
sta play4Row12,y ; 279F clear one
|
|
dey ; 27A2 next
|
|
bpl L_279F ; 27A3 until the buffer is empty
|
|
jmp mapCellByteImm ; 27A5 straight on to the unit pass; open ground keeps the colours already on screen
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Terrain colour and shape: terrainClassTable ($077E indexed by the raw cell value) maps the cell code
|
|
; to a terrain class 0-8, terrainColourTable gives the colour of that class (7 plain, 3 river, 5
|
|
; forest, 8 contours/hills/summit, 0 for the off-map classes).
|
|
; ----------------------------------------------------------------------
|
|
L_27A8:
|
|
ldx D_077E,y ; 27A8 terrain class 0-8 of this cell code
|
|
lda terrainColourTable,x ; 27AB colour nibble of that class
|
|
sta tileColour ; 27AE remember it for the screen RAM writes
|
|
tya ; 27B0 the cell code
|
|
sec ; 27B1 no borrow
|
|
sbc #$40 ; 27B2 shape index = code - $40 for the ordinary terrain codes...
|
|
bcs L_27B8 ; 27B4 ...
|
|
adc #$20 ; 27B6 ...and code - $20 for the contour codes below $40
|
|
L_27B8:
|
|
jsr setTerrainTilePtr ; 27B8 glyphPtr zp_B2/B3 -> the 8-byte shape at $9C6E + index*8
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Expansion loop: eight shape rows, each one byte of 8 one-bit pixels. The low nybble (right half)
|
|
; becomes the right-hand character, the high nybble the left-hand one, and every row is written twice
|
|
; so the shape is doubled vertically as well as horizontally. Buffer layout:
|
|
; $04C8 upper-left character, $04D0 upper-right, $04D8 lower-left, $04E0 lower-right.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$00 ; 27BB buffer offset, two bytes per shape row
|
|
ldy #$00 ; 27BD shape row 0
|
|
sty terrainShapeRowCounter+1; 27BF the loop counter lives in the LDY # at terrainShapeRowCounter
|
|
L_27C2:
|
|
lda (glyphPtr),y ; 27C2 one row of the 8x8 terrain shape
|
|
sta terrainShapeRowImm+1 ; 27C4 keep it in the LDA # at terrainShapeRowImm for the high nybble
|
|
and #$0F ; 27C7 the right four pixels
|
|
tay ; 27C9 index into the expansion table
|
|
lda nybbleToMulticolourTable,y; 27CA each set bit becomes a %10 pixel pair
|
|
sta play4Row20,x ; 27CD right-hand character, first scanline
|
|
sta play4Row21,x ; 27D0 and again: the shape is doubled vertically
|
|
terrainShapeRowImm:
|
|
lda #$FF ; 27D3 the shape row saved at $27C4
|
|
lsr a ; 27D5 shift the left four pixels...
|
|
lsr a ; 27D6 ...down...
|
|
lsr a ; 27D7 ...into...
|
|
lsr a ; 27D8 ...bits 0-3
|
|
tay ; 27D9 index into the expansion table
|
|
lda nybbleToMulticolourTable,y; 27DA expand them the same way
|
|
sta play4Row12,x ; 27DD left-hand character, first scanline
|
|
sta play4Row13,x ; 27E0 doubled
|
|
inx ; 27E3 two scanlines...
|
|
inx ; 27E4 ...per shape row
|
|
cpx #$08 ; 27E5 after four rows the upper character pair is full
|
|
bne L_27EE ; 27E7 still in the upper half
|
|
txa ; 27E9 buffer offset
|
|
clc ; 27EA no carry
|
|
adc #$08 ; 27EB skip the 8 bytes of the upper-right character to reach $04D8
|
|
tax ; 27ED continue in the lower character pair
|
|
L_27EE:
|
|
inc terrainShapeRowCounter+1; 27EE next shape row
|
|
terrainShapeRowCounter:
|
|
ldy #$FF ; 27F1 the row counter (operand patched at $27BF/$27EE)
|
|
cpy #$08 ; 27F3 eight rows in a shape
|
|
bcc L_27C2 ; 27F5 keep expanding
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Terrain colour into screen RAM: in multicolour mode the low nybble of the screen RAM byte is the
|
|
; colour of the %10 pixels the terrain shape is made of, the high nybble that of the %01 pixels (0 =
|
|
; black here). All four characters of the tile get the same byte.
|
|
; ----------------------------------------------------------------------
|
|
lda tileColour ; 27F7 the terrain colour
|
|
ldy #$00 ; 27F9 first character of the tile
|
|
sta (screenPtr),y ; 27FB upper-left
|
|
iny ; 27FD next column
|
|
sta (screenPtr),y ; 27FE upper-right
|
|
ldy #$28 ; 2800 one screen row = 40 bytes
|
|
sta (screenPtr),y ; 2802 lower-left
|
|
iny ; 2804 next column
|
|
sta (screenPtr),y ; 2805 lower-right
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Unit pass. mapCellByteImm+1 holds the map cell byte stored at $2772 (or the border code at
|
|
; $278D); bit 7 set means the low 7 bits are the index of the unit standing here.
|
|
; ----------------------------------------------------------------------
|
|
mapCellByteImm:
|
|
lda #$FF ; 2807 the map cell byte patched in at $2772/$278D
|
|
bpl L_2859 ; 2809 no unit on this cell: the tile is finished
|
|
bit gamePhase ; 280B gamePhase bit 6 = draw terrain only (unit placement phase)
|
|
bvs L_2859 ; 280D skip the unit
|
|
and #$7F ; 280F unit index 0-99
|
|
ldy #$00 ; 2811 default values...
|
|
sty unitGlyphEorMask+1 ; 2813 ...glyph EOR mask 0 = draw the glyph as it is...
|
|
sty cloakSkipFlagImm+1 ; 2816 ...and scanline-skip flag 0 = draw every line
|
|
ldy ownUnitColour ; 2819 6 = blue, the local player's colour
|
|
sty tileColour ; 281B assume the unit is ours
|
|
tay ; 281D Y = unit index
|
|
lda unitTypeTable,y ; 281E type byte of the unit
|
|
bpl L_2831 ; 2821 bit 7 clear = the unit is alive
|
|
and #$07 ; 2823 dead: bits 0-2 still give the type for the glyph
|
|
tax ; 2825 glyph selector
|
|
lda #$01 ; 2826 white
|
|
sta tileColour ; 2828 wrecks are drawn white...
|
|
lda #$FF ; 282A ...
|
|
sta unitGlyphEorMask+1 ; 282C ...and inverted
|
|
bne L_2878 ; 282F always taken: a destroyed unit is visible to both players
|
|
L_2831:
|
|
and #$07 ; 2831 alive: type 0-4
|
|
tax ; 2833 glyph selector
|
|
lda unitPathColTable,y ; 2834 waypoint column + cloak bits ($F898)
|
|
bpl L_283E ; 2837 bit 7 clear = not cloaked
|
|
lda #$FF ; 2839 cloaked...
|
|
sta cloakSkipFlagImm+1 ; 283B ...so only every other scanline of the glyph is drawn (a see-through ghost)
|
|
L_283E:
|
|
cpy #$32 ; 283E 50 = the first unit of side 1; units 0-49 are side 0
|
|
ownSideBranchOpcode:
|
|
bcc unitVisibleBranchOpcode ; 2840 opcode patched from the local side by $7BD2 in the overlay ($90 BCC when we are side 0, $B0 BCS when side 1): branch when this is one of our units
|
|
lda enemyUnitColour ; 2842 2 = red
|
|
sta tileColour ; 2844 enemy units are drawn red
|
|
unitVisibleBranchOpcode:
|
|
bcc L_285C ; 2846 same test, opcode patched by patchUnitSideBranches (inverted for the THEM replay viewpoint): our own units are always visible
|
|
lda filmPlaybackMode ; 2848 film playback state
|
|
cmp #$82 ; 284B $82 = playing back with viewpoint 2 = BOTH, which sees everything
|
|
bcs L_285C ; 284D omniscient: draw the enemy unit
|
|
lda revealAllUnits ; 284F $92FC bit 7 = reveal every unit (set when the game is over)
|
|
bmi L_285C ; 2852 draw it
|
|
lda unitEnergyTable,y ; 2854 energy byte of the enemy unit
|
|
bmi L_285C ; 2857 bit 7 = this unit has been spotted by us
|
|
L_2859:
|
|
jmp L_28E0 ; 2859 unspotted enemy: leave the terrain tile as it is
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The unit is going to be drawn. First the two overlays that change the tile rather than the glyph:
|
|
; the dug-in bars and the white hit flash.
|
|
; ----------------------------------------------------------------------
|
|
L_285C:
|
|
lda unitStunTable,y ; 285C stun / dig-in byte
|
|
bpl L_286F ; 285F bit 7 clear = not dug in
|
|
lda #$FF ; 2861 a solid row of %11 pixels
|
|
sta play4Row12 ; 2863 top scanline of the upper-left character...
|
|
sta play4Row35 ; 2866 ...bottom scanline of the lower-left one...
|
|
sta play4Row20 ; 2869 ...top scanline of the upper-right...
|
|
sta play4Row43 ; 286C ...and bottom of the lower-right: a bar above and below the dug-in unit
|
|
L_286F:
|
|
lda unitHitFlashTable,y ; 286F $FA8C bit 7 = the unit has just been hit
|
|
bpl L_2878 ; 2872 no flash
|
|
lda #$01 ; 2874 white
|
|
sta tileColour ; 2876 flash the unit white this frame
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Highlight: the unit the player has selected, or any member of the selected group while the console
|
|
; is at group level (uiLevel 2), is drawn in reverse video.
|
|
; ----------------------------------------------------------------------
|
|
L_2878:
|
|
cpy highlightedUnit ; 2878 the unit the player currently has selected
|
|
beq L_2895 ; 287B this is it -> invert it
|
|
lda uiLevel ; 287D which selection level the console is in
|
|
cmp #$02 ; 2880 2 = a whole group is selected
|
|
bne L_289A ; 2882 single-unit level: no group highlight
|
|
lda unitDisplayFlagsTable,y ; 2884 group/status flags
|
|
and #$40 ; 2887 bit 6 = the unit is in a group
|
|
beq L_289A ; 2889 not grouped
|
|
lda unitDisplayFlagsTable,y ; 288B flags again
|
|
and #$3C ; 288E bits 2-5 = group id plus the side bit
|
|
cmp highlightedGroupKey ; 2890 the selected group's key
|
|
bne L_289A ; 2893 different group: no highlight
|
|
L_2895:
|
|
lda #$FF ; 2895 invert every glyph byte...
|
|
sta unitGlyphEorMask+1 ; 2897 ...by patching the EOR mask at unitGlyphEorMask
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fetch the unit glyph: 20 shapes of 32 bytes, indexed by unit type * 4 + facing through the word
|
|
; table unitGlyphPointerTable ($6C35). unitGlyphBaseTable gives $2C, $30, $34, $38, $3C for GRUNT,
|
|
; RIDER, BOOMER, SPY, COMCEN; the same numbers are the overview-map symbols, so
|
|
; $2C is subtracted again here.
|
|
; ----------------------------------------------------------------------
|
|
L_289A:
|
|
lda unitFlagsTable,y ; 289A flags byte of the unit
|
|
and #$03 ; 289D bits 0-1 = facing 0-3
|
|
clc ; 289F no carry
|
|
adc unitGlyphBaseTable,x ; 28A0 + the base glyph number of the unit type
|
|
sec ; 28A3 no borrow
|
|
sbc #$2C ; 28A4 back to 0-19 = type*4 + facing
|
|
asl a ; 28A6 two bytes per pointer
|
|
tay ; 28A7 table index
|
|
lda unitGlyphPointerTable,y ; 28A8 low byte of the glyph address
|
|
sta glyphPtr ; 28AB glyph pointer
|
|
iny ; 28AD next table byte
|
|
lda unitGlyphPointerTable,y ; 28AE high byte of the glyph address
|
|
sta glyphPtrHi ; 28B1 glyph pointer high
|
|
ldy #$1F ; 28B3 32 glyph bytes, last first
|
|
L_28B5:
|
|
lda (glyphPtr),y ; 28B5 one byte of the unit glyph
|
|
unitGlyphEorMask:
|
|
eor #$FF ; 28B7 invert mask: $00 (as-is) or $FF (reverse video), patched at $2813/$282C/$2897
|
|
ora play4Row12,y ; 28B9 OR the unit on top of the terrain tile...
|
|
sta play4Row12,y ; 28BC ...in the tile buffer
|
|
cloakSkipFlagImm:
|
|
lda #$FF ; 28BF cloak flag: $00 or $FF, patched at $2816/$283B
|
|
bpl L_28C4 ; 28C1 not cloaked: one step per byte
|
|
dey ; 28C3 cloaked: skip a scanline so the unit shows as a half-drawn ghost
|
|
L_28C4:
|
|
dey ; 28C4 next glyph byte
|
|
bpl L_28B5 ; 28C5 until all 32 are merged
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Unit colour into colour RAM: the %11 pixels of a unit glyph take their colour from $D800, which is
|
|
; $4C00 above the screen RAM at $8C00.
|
|
; ----------------------------------------------------------------------
|
|
lda screenPtrHi ; 28C7 screen RAM pointer high byte ($8Cxx)
|
|
clc ; 28C9 no carry
|
|
adc #$4C ; 28CA +$4C00 turns it into a colour RAM pointer ($D8xx)
|
|
sta screenPtrHi ; 28CC keep the colour RAM pointer
|
|
ldy #$00 ; 28CE redundant: Y is loaded again at $28D2
|
|
lda tileColour ; 28D0 blue for our units, red for the enemy, white when hit or destroyed
|
|
ldy #$00 ; 28D2 first character of the tile
|
|
sta (screenPtr),y ; 28D4 upper-left
|
|
iny ; 28D6 next column
|
|
sta (screenPtr),y ; 28D7 upper-right
|
|
ldy #$28 ; 28D9 one screen row = 40 bytes
|
|
sta (screenPtr),y ; 28DB lower-left
|
|
iny ; 28DD next column
|
|
sta (screenPtr),y ; 28DE lower-right
|
|
L_28E0:
|
|
rts ; 28E0 tile finished
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawMainWindowFrame - Draws the frame of the bottom bar: window 3 on the battlefield screen, window
|
|
; 4 on every other screen. Both are the same 38x3 box at column 1, row 22; they differ only in the
|
|
; 9-tile frame set used, so the status bar looks different in the console and menu screens.
|
|
; In: currentScreen $90FB (0 = battlefield)
|
|
; Out: textColour zp_3D = $0C, the frame drawn, window geometry left in $2A2D/$2A2E/$9221/$9222
|
|
; Called from: redrawGameScreen ($2671) and both $6F00 overlays ($712E, $7A65)
|
|
; ----------------------------------------------------------------------
|
|
drawMainWindowFrame:
|
|
lda #$0C ; 28E1 grey
|
|
sta textColour ; 28E3 colour of the frame characters
|
|
ldx #$03 ; 28E5 window 3 = the battlefield status bar (column 1, row 22, 38x3)
|
|
ldy currentScreen ; 28E7 which screen is up
|
|
beq L_28EE ; 28EA 0 = battlefield: use window 3
|
|
ldx #$04 ; 28EC window 4 = the same box with the other frame style
|
|
L_28EE:
|
|
jmp drawWindowFrame ; 28EE draw it and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computeGameResult - Turns the state at the end of a game into the two final point totals and the
|
|
; result message. The score pair $92A2/$92A4 and $92A3/$92A5 is indexed by the side that was scored
|
|
; AGAINST, so the larger entry belongs to the losing side. On points: equal totals give 250 each and
|
|
; message $13 'IT WAS A DRAW.', otherwise the winner gets 300 + min(difference/2, 150) and the loser
|
|
; 200, with message $11 'WE LOST ON POINTS!' or $12 'WE WON ON POINTS!'. On a knockout (gameEndReason
|
|
; bit 7): both comcens down is again a draw, otherwise the winner gets 500, the loser 150 and message
|
|
; $14 'WE WERE KNOCKED OUT!' or $15 'WE WON BY A KNOCKOUT!'. The chosen message is queued at the end
|
|
; and the status line is released. Y is preserved through a self-modified LDY # at $29B0.
|
|
; In: Y (returned unchanged), gameEndReason $0BA8 ($08 = clock ran out, $A0/$C0 = the comcen of side
|
|
; 0/1 was knocked out), playerSide $0B9F, the running scores
|
|
; sideScoreLo/side1ScoreLo/sideScoreHi/side1ScoreHi $92A2-$92A5
|
|
; Out: $92A2-$92A5 rewritten with the final points, gameOverFlags $923F = $C0, persistentMessageId
|
|
; $92A1 = $9E and chatMessageLine $0548 = $9E (status line free), chatState $929C = 0, messages
|
|
; $16 and the result message queued; scratch18/scratch19 clobbered; self-modifies $29B0+1
|
|
; Called from: endGame ($4032)
|
|
; ----------------------------------------------------------------------
|
|
computeGameResult:
|
|
sty L_29B0+1 ; 28F1 save Y in the LDY # at $29B0 so the caller gets it back
|
|
jsr resetMessageQueue ; 28F4 throw away anything still queued for the status line
|
|
lda #$C0 ; 28F7 bits 6-7 = the game is over and player input is locked out
|
|
sta gameOverFlags ; 28F9 game-over flags
|
|
lda gameEndReason ; 28FC why the game ended
|
|
bmi L_2976 ; 28FF bit 7 set = a comcen was knocked out
|
|
lda #$16 ; 2901 message $16 'GAME OVER.'
|
|
jsr queueMessage ; 2903 queue it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Decide the result on points. The 16-bit totals are indexed by the side that was scored against, so
|
|
; subtracting them tells which side lost and by how much.
|
|
; ----------------------------------------------------------------------
|
|
lda sideScoreLo ; 2906 points scored against side 0, low byte
|
|
sec ; 2909 no borrow
|
|
sbc side1ScoreLo ; 290A minus the points scored against side 1
|
|
sta scratch18 ; 290D difference low byte
|
|
lda sideScoreHi ; 290F high bytes...
|
|
sbc side1ScoreHi ; 2912 ...with the borrow
|
|
sta scratch19 ; 2915 difference high byte
|
|
ora scratch18 ; 2917 is the difference zero?
|
|
bne L_292F ; 2919 no: somebody won on points
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A tie: both sides get 250 points. setOtherPlayerPoints flips X each time, so the two calls write
|
|
; slot 1 and then slot 0.
|
|
; ----------------------------------------------------------------------
|
|
L_291B:
|
|
ldx #$00 ; 291B start from side 0
|
|
ldy #$FA ; 291D 250 points
|
|
jsr setOtherPlayerPoints ; 291F writes the total of the other side (1) and leaves X = 1
|
|
ldy #$FA ; 2922 250 points again
|
|
jsr setOtherPlayerPoints ; 2924 writes the total of side 0 and leaves X = 0
|
|
lda #$13 ; 2927 message $13 'IT WAS A DRAW.'
|
|
sta persistentMessageId ; 2929 remember it as the result message
|
|
jmp L_299D ; 292C queue it and finish
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Somebody won: make the difference positive and remember which side lost (X). The winner's points go
|
|
; into the loser's score slot, because that slot counts what was scored against the loser.
|
|
; ----------------------------------------------------------------------
|
|
L_292F:
|
|
ldx #$00 ; 292F assume side 0 was outscored
|
|
bcs L_2946 ; 2931 no borrow: more was scored against side 0, so side 0 lost
|
|
lda scratch18 ; 2933 otherwise negate the difference...
|
|
eor #$FF ; 2935 ...
|
|
clc ; 2937 ...
|
|
adc #$01 ; 2938 ...
|
|
sta scratch18 ; 293A ...low byte
|
|
lda scratch19 ; 293C ...
|
|
eor #$FF ; 293E ...
|
|
adc #$00 ; 2940 ...
|
|
sta scratch19 ; 2942 ...high byte
|
|
ldx #$01 ; 2944 side 1 is the loser
|
|
L_2946:
|
|
lsr scratch19 ; 2946 half the point difference...
|
|
ror scratch18 ; 2948 ...16 bits
|
|
lda scratch19 ; 294A high byte of half the difference
|
|
bne L_2954 ; 294C over 255: clamp
|
|
lda scratch18 ; 294E low byte
|
|
cmp #$96 ; 2950 150 = the cap on the winning margin
|
|
bcc L_2956 ; 2952 below the cap: use it
|
|
L_2954:
|
|
lda #$96 ; 2954 clamp to 150
|
|
L_2956:
|
|
clc ; 2956 no carry
|
|
adc #$2C ; 2957 $012C = 300 base points for the winner
|
|
sta sideScoreLo,x ; 2959 into the loser's slot, which holds the winner's total
|
|
lda #$00 ; 295C high byte...
|
|
adc #$01 ; 295E ...= 1 plus any carry from the 300 + margin add
|
|
sta sideScoreHi,x ; 2960 winner's total, high byte
|
|
ldy #$11 ; 2963 message $11 'WE LOST ON POINTS!'
|
|
cpx playerSide ; 2965 is the losing side ours?
|
|
beq L_296B ; 2968 yes: keep message $11
|
|
iny ; 296A no: message $12 'WE WON ON POINTS!'
|
|
L_296B:
|
|
sty persistentMessageId ; 296B remember the result message
|
|
ldy #$C8 ; 296E 200 consolation points
|
|
jsr setOtherPlayerPoints ; 2970 into the winner's slot, i.e. the loser's total
|
|
jmp L_299D ; 2973 queue the message, free the status line and restore Y
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Knockout ending of computeGameResult (reached from $28FF when gameEndReason $0BA8 bit 7 is set).
|
|
; Bits 5-6 of gameEndReason were built at $3BBC-$3BC9 from the destroyed comcen's side bit and ORed
|
|
; into the old value, so $20 = side 0's comcen died, $40 = side 1's, $60 = both died in one exchange.
|
|
; X ends up holding the LOSING side. Both score slots are indexed by the OPPONENT of the side they
|
|
; belong to: hits are added at $3B88 with X = the side of the unit that was hit, and
|
|
; drawStatusScoreLine prints slot (playerSide EOR 1) as 'US'. Storing into slot X therefore credits
|
|
; the winner, and setOtherPlayerPoints fills in the loser's slot.
|
|
; ----------------------------------------------------------------------
|
|
L_2976:
|
|
ldx #$00 ; 2976 assume side 0 is the loser (X = index of the knocked-out side)
|
|
and #$60 ; 2978 keep only the two knockout bits of gameEndReason (bit 5 = side 0 lost its comcen, bit 6 = side 1)
|
|
cmp #$60 ; 297A both comcens destroyed in the same exchange?
|
|
beq L_291B ; 297C yes - reuse the draw path at $291B: 250 points each and message $13 'IT WAS A DRAW.'
|
|
cmp #$40 ; 297E otherwise test the side-1 bit ($40) on its own
|
|
bcc L_2983 ; 2980 only bit 5 set - side 0 was knocked out, leave X = 0
|
|
inx ; 2982 bit 6 set - side 1 was knocked out, so the loser is side 1
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Award the knockout scores: 500 to the winner, 150 to the loser.
|
|
; ----------------------------------------------------------------------
|
|
L_2983:
|
|
lda #$F4 ; 2983 low byte of the winner's final total, $01F4 = 500 points
|
|
sta sideScoreLo,x ; 2985 slot X is the loser's opponent, i.e. the winner
|
|
lda #$01 ; 2988 high byte of 500
|
|
sta sideScoreHi,x ; 298A store the winner's 16-bit total
|
|
ldy #$14 ; 298D message $14 = 'WE WERE KNOCKED OUT!' (string $87BA)
|
|
cpx playerSide ; 298F was the knocked-out side the side this machine plays?
|
|
beq L_2995 ; 2992 yes - keep the 'we were knocked out' text
|
|
iny ; 2994 no - message $15 = 'WE WON BY A KNOCKOUT!' (string $87CE)
|
|
L_2995:
|
|
sty persistentMessageId ; 2995 remember the result message id so the status line can reshow it
|
|
ldy #$96 ; 2998 the knocked-out side keeps only $96 = 150 points
|
|
jsr setOtherPlayerPoints ; 299A write 150 into the other slot (X EOR 1), which belongs to the loser
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Common tail of every ending (draw, points and knockout): show the result once, then park the status
|
|
; line and leave chat entry.
|
|
; ----------------------------------------------------------------------
|
|
L_299D:
|
|
lda persistentMessageId ; 299D the result message id chosen above ($11-$15)
|
|
jsr queueMessage ; 29A0 push it onto the status-line message queue ($C566)
|
|
lda #$9E ; 29A3 $9E = the 'nothing pending' marker used by the message system
|
|
sta persistentMessageId ; 29A5 stop the status line from repeating the result message
|
|
sta chatMessageLine ; 29A8 and blank the chat text line slot at $0548 the same way
|
|
lda #$00 ; 29AB leave chat/typing mode
|
|
sta chatState ; 29AD clear the chat state machine at $929C
|
|
L_29B0:
|
|
ldy #$FF ; 29B0 operand patched at $28F1 with the caller's Y, so Y is returned unchanged
|
|
rts ; 29B2 back to endGame ($4032)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setOtherPlayerPoints - Writes Y as the 16-bit point total of the side OTHER than X and returns with
|
|
; X already flipped. The two totals live in the parallel slots sideScoreLo/sideScoreHi ($92A2/$92A3
|
|
; and
|
|
; $92A4/$92A5) which are indexed by the opponent of the side that owns them, so computeGameResult uses
|
|
; this to fill in the second player after storing the first with sideScoreLo,x.
|
|
; In: X = side index 0/1, Y = the point value (0-255)
|
|
; Out: sideScoreLo[X EOR 1] = Y, sideScoreHi[X EOR 1] = 0, X = X EOR 1, A = 0
|
|
; Called from: computeGameResult ($291F, $2924 draw path; $2970 points path; $299A knockout path)
|
|
; ----------------------------------------------------------------------
|
|
setOtherPlayerPoints:
|
|
txa ; 29B3 take the side index that was just written
|
|
eor #$01 ; 29B4 flip it: 0 <-> 1
|
|
tax ; 29B6 index the other side's score slot
|
|
tya ; 29B7 the value the caller passed in Y
|
|
sta sideScoreLo,x ; 29B8 low byte of that side's 16-bit total
|
|
lda #$00 ; 29BB result totals are always below 256 ...
|
|
sta sideScoreHi,x ; 29BD ... so clear the high byte
|
|
rts ; 29C0 X is left flipped, which is what the caller relies on
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; shakeScreenScroll - One frame of the screen-shake effect used when a comcen is hit. Takes one byte
|
|
; from the game PRNG and offers bits 0-2 as a new $D011 fine Y scroll and bits 3-5 as a new $D016 fine
|
|
; X scroll; each is used only if it is non-zero and smaller than the remaining shake amount
|
|
; screenShakeTimer, otherwise the resting values ($0B for $D011, $08 for $D016) are put back. The
|
|
; chosen low nibbles are merged into the shadow registers vicCtrl1Shadow/vicCtrl2Shadow (the raster
|
|
; IRQ copies those to the VIC) and kept in spriteScrollFineY/spriteScrollFineX so the IRQ can shift
|
|
; the sprites by the same amount at $1174-$1186. Because the low nibble is replaced wholesale, an
|
|
; accepted shake value also clears bit 3 (RSEL/CSEL), so a shaking frame drops to 24 rows / 38 columns
|
|
; - part of the jolt.
|
|
; In: screenShakeTimer ($92C8), vicCtrl1Shadow ($9000), vicCtrl2Shadow ($9001), PRNG state zp_57-59
|
|
; Out: vicCtrl1Shadow, vicCtrl2Shadow, spriteScrollFineY ($92F5), spriteScrollFineX ($92F4);
|
|
; self-modified operands at $29D6, $29E6, $29F3, $2A03
|
|
; Called from: the battle main loop at $41C7, once per frame while screenShakeTimer counts down
|
|
; ----------------------------------------------------------------------
|
|
shakeScreenScroll:
|
|
jsr nextGameRandom ; 29C1 one byte from the 24-bit game PRNG at $FD5D (both machines step it in lock step)
|
|
sta L_29D5+1 ; 29C4 patch the LDA #imm at $29D5 with the raw random byte (vertical candidate)
|
|
lsr a ; 29C7 shift the byte down three places ...
|
|
lsr a ; 29C8 ... so that bits 3-5 become bits 0-2 ...
|
|
lsr a ; 29C9 ... giving an independent horizontal candidate
|
|
sta L_29F2+1 ; 29CA patch the LDA #imm at $29F2 with it
|
|
lda vicCtrl1Shadow ; 29CD current $D011 shadow
|
|
and #$F0 ; 29D0 keep bits 4-7 (DEN, BMM, ECM, RST8) and drop the RSEL/Y-scroll nibble
|
|
sta L_29E5+1 ; 29D2 patch the ORA #imm at $29E5 with the preserved high nibble
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Vertical jitter: pick a new $D011 low nibble.
|
|
; ----------------------------------------------------------------------
|
|
L_29D5:
|
|
lda #$FF ; 29D5 operand patched at $29C4 = the random byte
|
|
and #$07 ; 29D7 reduce it to a 0-7 fine Y scroll
|
|
beq L_29E0 ; 29D9 0 would not move anything - use the resting value instead
|
|
cmp screenShakeTimer ; 29DB only accept a jitter smaller than the shake energy still left
|
|
bcc L_29E2 ; 29DE accepted: this frame is displaced by A raster lines
|
|
L_29E0:
|
|
lda #$0B ; 29E0 resting $D011 low nibble: RSEL 25 rows (bit 3) + Y-scroll 3
|
|
L_29E2:
|
|
sta spriteScrollFineY ; 29E2 remember it so the IRQ can offset the sprites by the same amount
|
|
L_29E5:
|
|
ora #$FF ; 29E5 operand patched at $29D2 = the preserved bits 4-7 of $D011
|
|
sta vicCtrl1Shadow ; 29E7 the raster IRQ writes this shadow to $D011
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Horizontal jitter: the same again for the $D016 low nibble.
|
|
; ----------------------------------------------------------------------
|
|
lda vicCtrl2Shadow ; 29EA current $D016 shadow
|
|
and #$F0 ; 29ED keep bits 4-7 (MCM and the unused high bits) and drop the CSEL/X-scroll nibble
|
|
sta L_2A02+1 ; 29EF patch the ORA #imm at $2A02 with it
|
|
L_29F2:
|
|
lda #$FF ; 29F2 operand patched at $29CA = the random byte shifted down 3
|
|
and #$07 ; 29F4 reduce it to a 0-7 fine X scroll
|
|
beq L_29FD ; 29F6 0 would not move anything
|
|
cmp screenShakeTimer ; 29F8 again, only a jitter below the remaining shake energy
|
|
bcc L_29FF ; 29FB accepted: this frame is displaced by A pixels sideways
|
|
L_29FD:
|
|
lda #$08 ; 29FD resting $D016 low nibble: CSEL 40 columns (bit 3) + X-scroll 0
|
|
L_29FF:
|
|
sta spriteScrollFineX ; 29FF remember it for the sprite offset at $117F
|
|
L_2A02:
|
|
ora #$FF ; 2A02 operand patched at $29EF = the preserved bits 4-7 of $D016
|
|
sta vicCtrl2Shadow ; 2A04 the raster IRQ writes this shadow to $D016
|
|
rts ; 2A07 the caller decrements screenShakeTimer afterwards
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; patchUnitSideBranches - Re-points the two 'is this unit mine?' branches at the viewpoint currently
|
|
; being drawn. Overlay A stores $90 (BCC) or $B0 (BCS) at $2840 from playerSide (see $7BC6), which
|
|
; makes the 'cpy #$32' unit-index test at $283E select the local side. This routine copies that
|
|
; opcode byte into the following visibility test at $2846 (renderMapTile) and into its twin at $5BFE
|
|
; (the overview map renderer), inverting BCC <-> BCS with EOR $20 when filmPlaybackMode bit 0 is set,
|
|
; i.e. when a game film is being replayed from the opponent's point of view (viewpoint 1 = THEM).
|
|
; Viewpoint 2 = BOTH has bit 0 clear and is handled separately by the 'cmp #$82' tests at $284B/$5C03,
|
|
; which reveal everything.
|
|
; In: filmPlaybackMode ($0B9B) bit 0, the opcode byte at $2840
|
|
; Out: the opcode bytes at $2846 and $5BFE
|
|
; Called from: start-up ($0AD9), the battlefield screen rebuild ($2639) and the replay viewpoint key
|
|
; ($4134)
|
|
; ----------------------------------------------------------------------
|
|
patchUnitSideBranches:
|
|
lda filmPlaybackMode ; 2A08 film playback flags: bit 7 = replaying, bits 0-1 = viewpoint 0 US / 1 THEM / 2 BOTH
|
|
lsr a ; 2A0B shift viewpoint bit 0 into the carry
|
|
lda ownSideBranchOpcode ; 2A0C the side-select branch opcode overlay A wrote: $90 = BCC (side 0), $B0 = BCS (side 1)
|
|
bcc L_2A13 ; 2A0F viewpoint bit 0 clear (US or BOTH) - use the opcode unchanged
|
|
eor #$20 ; 2A11 viewpoint THEM - flip BCC <-> BCS so the enemy's units count as 'ours'
|
|
L_2A13:
|
|
sta unitVisibleBranchOpcode ; 2A13 patch the opcode of the branch at $2846 inside renderMapTile
|
|
sta D_5BFE ; 2A16 and the identical branch at $5BFE in the overview-map renderer
|
|
rts ; 2A19 the two renderers now test 'is this unit on the viewed side?' the right way round
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printOptionWord - Prints one of the three 4-byte DAMAGE option words held at $0652 in the message
|
|
; block - 'FULL' ($0652), 'HALF' ($0656), 'NONE' ($065A), each bit-7 terminated - through the text
|
|
; engine's string printer $C133. The overlays call it with the per-side damage setting recyclerMode
|
|
; ($92A6,side) to fill the XXX field of the rules summary line at $0636.
|
|
; In: A = word index 0 FULL / 1 HALF / 2 NONE
|
|
; Out: the word is printed at the current text cursor; A/Y = the string address handed to printString
|
|
; Called from: the setup screens of both $6F00 overlays ($843B, $85D8, $85E9)
|
|
; ----------------------------------------------------------------------
|
|
printOptionWord:
|
|
asl a ; 2A1A index * 2 ...
|
|
asl a ; 2A1B ... * 4: the words are 4 bytes apart
|
|
clc ; 2A1C prepare the 16-bit add
|
|
adc #$52 ; 2A1D low byte of the 'FULL' entry, $0652
|
|
pha ; 2A1F keep the low byte while the high byte is built
|
|
lda #$00 ; 2A20 start the high byte at 0 ...
|
|
adc #$06 ; 2A22 ... plus $06 and any carry, giving $06xx
|
|
tay ; 2A24 printString wants the high byte in Y
|
|
pla ; 2A25 and the low byte in A
|
|
jmp printString ; 2A26 tail-call the text engine's string printer
|
|
|
|
|
|
; Parameter block shared by every glyph plotter in this file and by both $6F00 overlays (all seven
|
|
; bytes just happen to be initialised to $01). Every screen builder sets them before calling
|
|
; drawWindowFrame, drawCharBlock, fillScreenColourRect or drawCharAt:
|
|
; +0 useDrawColourFlag non-zero = ignore the per-glyph colour table and colour with drawColourByte
|
|
; +1 drawColourByte the screen-RAM colour byte (high nibble foreground, low nibble background)
|
|
; +2 blockFillFlag non-zero = drawCharBlock ignores its character data and fills with
|
|
; blockFillChar
|
|
; +3 blockFillChar the character used for that fill
|
|
; +4 windowOriginRow top screen row (0-24) of the window the plotters draw into
|
|
; +5 windowOriginCol left screen column (0-39) of that window
|
|
; +6 (spare) never read by any traced code
|
|
useDrawColourFlag:
|
|
.byte $01 ; 2A29 . useDrawColourFlag: read by plotGlyph at $2AD1; 0 = take the colour from fontColorTable
|
|
drawColourByte:
|
|
.byte $01 ; 2A2A drawColourByte: written by fillScreenColourRect and used by plotGlyph and $2A85
|
|
blockFillFlag:
|
|
.byte $01 ; 2A2B blockFillFlag: tested by drawCharBlock at $2B54
|
|
blockFillChar:
|
|
.byte $01 ; 2A2C blockFillChar: the glyph drawCharBlock repeats when the flag is set
|
|
windowOriginRow:
|
|
.byte $01 ; 2A2D windowOriginRow: top row used by fillScreenColourRect, drawCharBlock, drawFrameBox, magnifyWindow2x
|
|
windowOriginCol:
|
|
.byte $01 ; 2A2E windowOriginCol: left column for the same routines; the 7th byte is a spare nobody reads
|
|
.byte $01 ; 2A2F .
|
|
|
|
; The two opcode tables that drive the four draw modes. Mode 0 replace, 1 OR, 2 EOR, 3 keep existing
|
|
; (the CMP just throws the new value away, leaving the byte that was read first).
|
|
bitmapDrawOpcodes:
|
|
.byte $B1,$11,$51,$D1 ; 2A30 ..Q. (zp),Y opcodes for the bitmap combine at $2AF3/$C22B: $B1 LDA, $11 ORA, $51 EOR, $D1 CMP
|
|
|
|
; colourDrawOpcodes: byteTable, 4 bytes. opcodes LDA/ORA/EOR/CMP abs ($AD,$0D,$4D,$CD) for the colour
|
|
; draw modes 0-3
|
|
colourDrawOpcodes:
|
|
.byte $AD,$0D,$4D,$CD ; 2A34 ..M. absolute opcodes for the colour combine at $2B00/$2A85: $AD LDA, $0D ORA, $4D EOR, $CD CMP
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setBitmapDrawMode - Chooses how a glyph's eight bitmap bytes are combined with what is already in
|
|
; the
|
|
; $A000 bitmap. It overwrites the opcode of the second LDA of the read-modify-write pair inside
|
|
; plotGlyph
|
|
; ($2AF3) and of the identical pair in the text engine's own glyph plotter ($C22B), so mode 0
|
|
; replaces, 1 ORs the glyph in, 2 EORs it (used for highlighting) and 3 leaves the bitmap untouched.
|
|
; A is preserved through the self-modified operand at $2A45.
|
|
; In: Y = draw mode 0-3, A = value to preserve
|
|
; Out: the opcode bytes at $2AF3 and $C22B; A unchanged
|
|
; Called from: resetDrawModes ($2A5B) and the setup/menu screens of both overlays ($701E, $7AA1)
|
|
; ----------------------------------------------------------------------
|
|
setBitmapDrawMode:
|
|
sta L_2A44+1 ; 2A38 stash A in the operand of the LDA #imm at $2A44 so it survives
|
|
lda bitmapDrawOpcodes,y ; 2A3B pick the (zp),Y opcode for this mode
|
|
sta glyphBitmapOpcode ; 2A3E patch the bitmap combine instruction inside plotGlyph
|
|
sta D_C22B ; 2A41 and the same instruction in the $C000 text engine's plotter
|
|
L_2A44:
|
|
lda #$FF ; 2A44 operand patched at $2A38 - restores the caller's A
|
|
rts ; 2A46 back to the caller with A intact
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setColourDrawMode - The colour-plane twin of setBitmapDrawMode: chooses how drawColourByte is
|
|
; combined with the colour byte already in screen RAM by patching the absolute LDA at $2B00 (inside
|
|
; plotGlyph) and at $2A85 (inside fillScreenColourRect). Mode 0 replaces, 1 ORs, 2 EORs
|
|
; (xorColourBlock2x2 uses this to flash a tile), 3 keeps the existing colour. A is preserved through
|
|
; the operand at $2A54.
|
|
; In: Y = draw mode 0-3, A = value to preserve
|
|
; Out: the opcode bytes at $2B00 and $2A85; A unchanged
|
|
; Called from: resetDrawModes ($2A5E), xorColourBlock2x2 ($2BD0) and overlay screen code ($7B5B)
|
|
; ----------------------------------------------------------------------
|
|
setColourDrawMode:
|
|
sta L_2A53+1 ; 2A47 stash A in the operand of the LDA #imm at $2A53
|
|
lda colourDrawOpcodes,y ; 2A4A pick the absolute opcode for this mode
|
|
sta glyphColourOpcode ; 2A4D patch the colour combine inside plotGlyph
|
|
sta colourFillOpcode ; 2A50 and the one inside fillScreenColourRect
|
|
L_2A53:
|
|
lda #$FF ; 2A53 operand patched at $2A47 - restores the caller's A
|
|
rts ; 2A55 back to the caller with A intact
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resetDrawModes - Puts both planes back to mode 0 (plain replace) after a highlight or overlay
|
|
; effect. Y is preserved through the self-modified operand at $2A62.
|
|
; In: Y = value to preserve
|
|
; Out: the opcodes at $2AF3, $C22B, $2B00 and $2A85 are all LDA again; Y unchanged
|
|
; Called from: $1689 and xorColourBlock2x2 ($2BDA) here, and from the overlays ($7033, $70F2, $7AB6,
|
|
; $7B40, $7B86, $810E)
|
|
; ----------------------------------------------------------------------
|
|
resetDrawModes:
|
|
sty L_2A61+1 ; 2A56 stash Y in the operand of the LDY #imm at $2A61
|
|
ldy #$00 ; 2A59 mode 0 = replace
|
|
jsr setBitmapDrawMode ; 2A5B patch the two bitmap combine instructions
|
|
jsr setColourDrawMode ; 2A5E patch the two colour combine instructions
|
|
L_2A61:
|
|
ldy #$FF ; 2A61 operand patched at $2A56 - restores the caller's Y
|
|
rts ; 2A63 back to the caller with Y intact
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; fillScreenColourRect - Paints an X columns by Y rows rectangle of the multicolour bitmap's colour
|
|
; cells (screen RAM at $8C00, 40 bytes per row) starting at the window origin
|
|
; windowOriginRow/windowOriginCol with the colour byte A, combined according to the current colour
|
|
; draw mode. Only the colour plane is touched - the bitmap itself keeps whatever it held, which is
|
|
; how panels are recoloured without redrawing.
|
|
; In: A = screen-RAM colour byte, X = width in cells (1-40), Y = height in rows,
|
|
; windowOriginRow/windowOriginCol, the mode opcode at $2A85
|
|
; Out: the rectangle recoloured; drawColourByte = A; zp_B6/B7, zp_1B (rows left) and zp_1C (width-1)
|
|
; clobbered
|
|
; Called from: everywhere - $1782, $18DC, $18EC, $18FC, $1909, $2BD7 and jumps at $16B4, $1916, $192A
|
|
; here, plus a dozen sites in each $6F00 overlay
|
|
; ----------------------------------------------------------------------
|
|
fillScreenColourRect:
|
|
sta drawColourByte ; 2A64 remember the colour byte where the patched instruction at $2A85 can read it
|
|
sty scratch1B ; 2A67 zp_1B counts the rows still to fill
|
|
ldy windowOriginRow ; 2A69 the window's top screen row
|
|
lda screenRowHiTable,y ; 2A6C high byte of $8C00 + row*40 from the screen row table
|
|
sta screenPtrHi ; 2A6F high byte of the first cell of the top row
|
|
lda screenRowLoTable,y ; 2A71 low byte of $8C00 + row*40
|
|
clc ; 2A74 clear the carry before adding the column offset
|
|
adc windowOriginCol ; 2A75 step right to the window's left column
|
|
sta screenPtr ; 2A78 zp_B6/B7 now points at the first cell of the top row
|
|
bcc L_2A7E ; 2A7A no carry out of the column add
|
|
inc screenPtrHi ; 2A7C carry - bump the high byte
|
|
L_2A7E:
|
|
dex ; 2A7E width-1 = the highest column offset ...
|
|
stx scratch1C ; 2A7F ... kept in zp_1C as the per-row start index
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Outer loop: one screen row per pass. Inner loop walks the row right to left.
|
|
; ----------------------------------------------------------------------
|
|
L_2A81:
|
|
ldy scratch1C ; 2A81 start each row at its rightmost cell
|
|
L_2A83:
|
|
lda (screenPtr),y ; 2A83 read the cell's current colour (needed by the OR/EOR/keep modes)
|
|
colourFillOpcode:
|
|
lda drawColourByte ; 2A85 opcode patched by setColourDrawMode: LDA / ORA / EOR / CMP drawColourByte
|
|
sta (screenPtr),y ; 2A88 write the combined colour back
|
|
dey ; 2A8A next cell to the left
|
|
bpl L_2A83 ; 2A8B until offset 0 has been done
|
|
dec scratch1B ; 2A8D one row finished
|
|
beq L_2A9E ; 2A8F all rows done
|
|
clc ; 2A91 clear the carry before the row step
|
|
lda screenPtr ; 2A92 advance the pointer ...
|
|
adc #$28 ; 2A94 ... by one screen row (40 bytes)
|
|
sta screenPtr ; 2A96 pointer now addresses the same column one row down
|
|
bcc L_2A81 ; 2A98 no page crossed
|
|
inc screenPtrHi ; 2A9A carry into the high byte
|
|
bne L_2A81 ; 2A9C always taken - back for the next row
|
|
L_2A9E:
|
|
rts ; 2A9E the rectangle is filled
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; calcCellPointers - Loads the character cell coordinates from scratch18 (row) and scratch19 (column)
|
|
; and falls into calcCellPointersXY. This is the entry every routine in this file uses.
|
|
; In: scratch18 = screen row 0-24, scratch19 = screen column 0-39
|
|
; Out: as calcCellPointersXY; Y = row, X = column
|
|
; Called from: $26EA, drawCharAt ($2AC4), drawCharBlock ($2B4E), setMagnifyCellPointers ($2F44 and the
|
|
; tail jump at $2F67) and $5B15
|
|
; ----------------------------------------------------------------------
|
|
calcCellPointers:
|
|
ldy scratch18 ; 2A9F Y = character row
|
|
ldx scratch19 ; 2AA1 X = character column
|
|
|
|
; ----------------------------------------------------------------------
|
|
; calcCellPointersXY - Turns a character cell (Y = row, X = column) into the two pointers every
|
|
; plotter needs: bitmapPtr = $A000 + row*320 + column*8 (the eight bytes of that cell in the
|
|
; multicolour bitmap) and screenPtr = $8C00 + row*40 + column (its colour byte). Both are looked up
|
|
; in the six address tables at $9300 rather than multiplied: bitmap row lo/hi at $9300/$9319 (25
|
|
; entries), column*8 lo/hi at
|
|
; $9332/$935A (40 entries) and screen row lo/hi at $9382/$939B (25 entries).
|
|
; In: Y = screen row 0-24, X = screen column 0-39
|
|
; Out: bitmapPtr/bitmapPtrHi (zp_B4/B5), screenPtr/screenPtrHi (zp_B6/B7); X and Y unchanged
|
|
; Called from: $5CB6, $5D3E here, the text engine's cell setter ($C0A7) and the overlays ($864B)
|
|
; ----------------------------------------------------------------------
|
|
calcCellPointersXY:
|
|
lda bitmapRowLoTable,y ; 2AA3 low byte of $A000 + row*320
|
|
clc ; 2AA6 clear the carry for the 16-bit add
|
|
adc bitmapColLoTable,x ; 2AA7 plus column*8 (low)
|
|
sta bitmapPtr ; 2AAA low byte of the cell's bitmap address
|
|
lda bitmapRowHiTable,y ; 2AAC high byte of $A000 + row*320
|
|
adc bitmapColHiTable,x ; 2AAF plus column*8 (high: 0 for columns 0-31, 1 for 32-39)
|
|
sta bitmapPtrHi ; 2AB2 zp_B4/B5 = address of the cell's 8 bitmap bytes
|
|
txa ; 2AB4 the column is also the byte offset within a screen row
|
|
clc ; 2AB5 clear the carry for the screen-RAM add
|
|
adc screenRowLoTable,y ; 2AB6 plus the low byte of $8C00 + row*40
|
|
sta screenPtr ; 2AB9 low byte of the cell's screen-RAM address
|
|
lda screenRowHiTable,y ; 2ABB high byte of $8C00 + row*40 ($8C-$8F)
|
|
adc #$00 ; 2ABE add only the carry - the screen fits in four pages
|
|
sta screenPtrHi ; 2AC0 zp_B6/B7 = address of the cell's colour byte
|
|
rts ; 2AC2 both pointers are ready for plotGlyph
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawCharAt - Draws character A at screen row scratch18, column scratch19: computes the cell pointers
|
|
; and falls straight into plotGlyph.
|
|
; In: A = character code 0-113, scratch18 = row, scratch19 = column
|
|
; Out: see plotGlyph
|
|
; Called from: drawWindowInnerCorners, drawTallGlyph, drawFrameBox, drawFrameRow/Column here ($2B95,
|
|
; $2B9E, $2BA7, $2BB3, $2BBD, $2BC8, $2CC5, $2CD0, $2CDB, $2CE7, $2D23, $2D35) and from the
|
|
; overlays
|
|
; ($6F6F, $6F76, $6F7D)
|
|
; ----------------------------------------------------------------------
|
|
drawCharAt:
|
|
pha ; 2AC3 the cell pointer helper clobbers A
|
|
jsr calcCellPointers ; 2AC4 work out where row/column lands in the bitmap and in screen RAM
|
|
pla ; 2AC7 get the character code back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; plotGlyph - Draws one 8x8 glyph of the game's own font (114 glyphs at $9426, 8 bytes each) into the
|
|
; bitmap cell at bitmapPtr, combining the bytes according to the bitmap draw mode, then sets the
|
|
; cell's colour at screenPtr. The colour is the glyph's own default from fontColorTable ($93B4) when
|
|
; useDrawColourFlag is 0 and that entry is non-zero; otherwise it is drawColourByte combined according
|
|
; to the colour draw mode. When the display is in multicolour mode ($D016 bit 4 set in
|
|
; vicCtrl2Shadow) the routine also writes textColour into the matching colour RAM cell, reached by
|
|
; adding $4C to the high byte of screenPtr ($8C00 + $4C00 = $D800).
|
|
; In: A = character code 0-113; bitmapPtr/screenPtr from calcCellPointers; useDrawColourFlag,
|
|
; drawColourByte, textColour (zp_3D), vicCtrl2Shadow; the mode opcodes at $2AF3 and $2B00
|
|
; Out: 8 bitmap bytes, 1 screen-RAM colour byte and optionally 1 colour RAM byte written; glyphPtr
|
|
; (zp_B2/B3) = the glyph's font address; screenPtrHi is left pointing into colour RAM in
|
|
; multicolour mode; A/X/Y clobbered
|
|
; Called from: drawCharBlock ($2B5F) and by falling through from drawCharAt
|
|
; ----------------------------------------------------------------------
|
|
plotGlyph:
|
|
tay ; 2AC8 keep the character code in Y for the font-colour lookup
|
|
lda #$00 ; 2AC9 clear the glyph pointer before building it
|
|
sta glyphPtr ; 2ACB dead store - glyphPtr is overwritten at $2AE7 before it is used
|
|
sta glyphPtrHi ; 2ACD clear the high byte so the three ROLs below can shift into it
|
|
lda #$00 ; 2ACF default colour value 0 = 'this glyph has no colour of its own'
|
|
ldx useDrawColourFlag ; 2AD1 does the caller insist on drawColourByte for every glyph?
|
|
bne L_2AD9 ; 2AD4 yes - keep the 0 so the fill colour is used
|
|
lda fontColorTable,y ; 2AD6 no - take this glyph's default colour byte from the 114-entry table at $93B4
|
|
L_2AD9:
|
|
tax ; 2AD9 park it in X across the glyph copy
|
|
tya ; 2ADA character code back into A
|
|
asl a ; 2ADB code * 2 ...
|
|
rol glyphPtrHi ; 2ADC carry the overflow into the high byte
|
|
asl a ; 2ADE ... * 4 ...
|
|
rol glyphPtrHi ; 2ADF carry again
|
|
asl a ; 2AE1 ... * 8: each glyph is 8 bytes
|
|
rol glyphPtrHi ; 2AE2 and again - glyphPtrHi now holds bits 8-10 of code*8
|
|
clc ; 2AE4 clear the carry for the base-address add
|
|
adc #$26 ; 2AE5 add the low byte of the font base $9426
|
|
sta glyphPtr ; 2AE7 low byte of $9426 + code*8
|
|
lda glyphPtrHi ; 2AE9 bits 8-10 of code*8
|
|
adc #$94 ; 2AEB and its high byte
|
|
sta glyphPtrHi ; 2AED zp_B2/B3 now points at the glyph's 8 rows
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the glyph into the bitmap cell, bottom row first.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$07 ; 2AEF 8 pixel rows, counted 7 down to 0
|
|
L_2AF1:
|
|
lda (bitmapPtr),y ; 2AF1 read the byte already in the bitmap (only the OR/EOR/keep modes need it)
|
|
glyphBitmapOpcode:
|
|
lda (glyphPtr),y ; 2AF3 opcode patched by setBitmapDrawMode: LDA / ORA / EOR / CMP (glyphPtr),y
|
|
sta (bitmapPtr),y ; 2AF5 store the combined pixel row
|
|
dey ; 2AF7 next pixel row up
|
|
bpl L_2AF1 ; 2AF8 until row 0 has been written
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Now the colour plane: one screen-RAM byte, plus a colour RAM byte in multicolour mode.
|
|
; ----------------------------------------------------------------------
|
|
iny ; 2AFA Y = 0, the single colour byte of this cell
|
|
txa ; 2AFB the glyph's default colour saved in X
|
|
bne L_2B03 ; 2AFC non-zero - write it straight in, ignoring the draw mode
|
|
lda (screenPtr),y ; 2AFE otherwise read the colour that is already there
|
|
glyphColourOpcode:
|
|
lda drawColourByte ; 2B00 opcode patched by setColourDrawMode: LDA / ORA / EOR / CMP drawColourByte
|
|
L_2B03:
|
|
sta (screenPtr),y ; 2B03 store the cell colour into screen RAM at $8C00+
|
|
lda vicCtrl2Shadow ; 2B05 shadow of $D016
|
|
and #$10 ; 2B08 bit 4 = MCM; the game runs hires ($C8) for text and multicolour ($D8) for the map
|
|
beq L_2B17 ; 2B0A hires - one colour byte is enough
|
|
clc ; 2B0C clear the carry for the pointer bump
|
|
lda screenPtrHi ; 2B0D high byte of the screen-RAM cell address
|
|
adc #$4C ; 2B0F $8C00 + $4C00 = $D800: the same cell in colour RAM
|
|
sta screenPtrHi ; 2B11 zp_B6/B7 now addresses the same cell in colour RAM
|
|
lda textColour ; 2B13 the text engine's current colour supplies the third multicolour pair
|
|
sta (screenPtr),y ; 2B15 third colour of the multicolour pixel pair
|
|
L_2B17:
|
|
rts ; 2B17 one cell drawn
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawCharBlock - Draws a rectangular block of characters at windowOriginRow/windowOriginCol. A/Y
|
|
; point at the block: byte 0 = width in cells, byte 1 = height, then width*height character codes
|
|
; stored row by row. When blockFillFlag is non-zero the character data is ignored and every cell is
|
|
; drawn with blockFillChar instead (the 2-byte header is still read), which is how the screen builders
|
|
; erase a panel. The header values are also left in frameWidth/frameHeight for drawWindowInnerCorners.
|
|
; In: A/Y = lo/hi address of the block; windowOriginRow, windowOriginCol, blockFillFlag,
|
|
; blockFillChar
|
|
; Out: frameWidth ($9221) = width, frameHeight ($9222) = height; blockPtr (zp_B8/B9) = the character
|
|
; data;
|
|
; scratch18/19 = last cell, scratch1B = row, scratch1C = column, scratch1D = data index
|
|
; Called from: the main menu builder ($16A9) and the panel builders of both overlays ($6F13, $6F24,
|
|
; $6F53,
|
|
; $70A3, $70D6, $7B1B and the jump at $726E)
|
|
; ----------------------------------------------------------------------
|
|
drawCharBlock:
|
|
sta blockPtr ; 2B18 keep the block address in zp_B8/B9
|
|
sty blockPtrHi ; 2B1A high byte of the block address
|
|
ldy #$00 ; 2B1C read the 2-byte header through (blockPtr),y
|
|
lda (blockPtr),y ; 2B1E header byte 0 = width in character cells
|
|
sta frameWidth ; 2B20 also published for drawWindowInnerCorners
|
|
inc blockPtr ; 2B23 step past the width byte
|
|
bne L_2B29 ; 2B25 no page crossing
|
|
inc blockPtrHi ; 2B27 carry into the high byte
|
|
L_2B29:
|
|
lda (blockPtr),y ; 2B29 header byte 1 = height in character rows
|
|
sta frameHeight ; 2B2B also published for drawWindowInnerCorners
|
|
inc blockPtr ; 2B2E step past the height byte - zp_B8/B9 now addresses the character data
|
|
bne L_2B34 ; 2B30 no page crossing
|
|
inc blockPtrHi ; 2B32 carry into the high byte
|
|
L_2B34:
|
|
lda #$00 ; 2B34 start at the top row of the block
|
|
sta scratch1B ; 2B36 row counter = 0
|
|
sta scratch1D ; 2B38 index into the character data = 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Row loop: scratch1B counts rows, scratch1C counts columns, scratch1D walks the data linearly.
|
|
; ----------------------------------------------------------------------
|
|
L_2B3A:
|
|
lda #$00 ; 2B3A start at the left column of this row
|
|
sta scratch1C ; 2B3C column counter back to 0 for this row
|
|
lda windowOriginRow ; 2B3E the window's top row ...
|
|
clc ; 2B41 clear the carry for the row add
|
|
adc scratch1B ; 2B42 ... plus the row we are on
|
|
sta scratch18 ; 2B44 target screen row for drawCharAt
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Column loop: one character per cell.
|
|
; ----------------------------------------------------------------------
|
|
L_2B46:
|
|
lda windowOriginCol ; 2B46 the window's left column ...
|
|
clc ; 2B49 clear the carry for the column add
|
|
adc scratch1C ; 2B4A ... plus the column we are on
|
|
sta scratch19 ; 2B4C target screen column
|
|
jsr calcCellPointers ; 2B4E point zp_B4-zp_B7 at that cell
|
|
lda blockFillChar ; 2B51 assume the fill character
|
|
ldx blockFillFlag ; 2B54 is this a plain fill rather than a character block?
|
|
bne L_2B5F ; 2B57 yes - draw blockFillChar and leave the data untouched
|
|
ldy scratch1D ; 2B59 no - index the next byte of the block data
|
|
lda (blockPtr),y ; 2B5B fetch the character for this cell
|
|
inc scratch1D ; 2B5D and advance the data index
|
|
L_2B5F:
|
|
jsr plotGlyph ; 2B5F draw it into the cell the pointers already address
|
|
inc scratch1C ; 2B62 next column
|
|
lda scratch1C ; 2B64 column just drawn
|
|
cmp frameWidth ; 2B66 have we reached the block's width?
|
|
bcc L_2B46 ; 2B69 keep going until the whole row is drawn
|
|
inc scratch1B ; 2B6B next row
|
|
lda scratch1B ; 2B6D row just finished
|
|
cmp frameHeight ; 2B6F have we reached the block's height?
|
|
bcc L_2B3A ; 2B72 keep going until every row is drawn
|
|
rts ; 2B74 the whole block is on screen
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawWindowInnerCorners - Draws the small hollow-ring glyph $38 (font bytes at $95E6) in the four
|
|
; cells just inside the corners of the current window: rows windowOriginRow+1 and
|
|
; windowOriginRow+frameHeight-1, columns windowOriginCol+1 and windowOriginCol+frameWidth-1. It is
|
|
; always called straight after drawWindowFrame or drawCharBlock, so frameWidth/frameHeight are the
|
|
; extents those routines left behind. Only the two $6F00 overlays use it, to stud the corners of their
|
|
; menu and setup panels.
|
|
; In: windowOriginRow, windowOriginCol, frameWidth ($9221), frameHeight ($9222)
|
|
; Out: four characters drawn; scratch18/19 (cell), scratch1A (right column), scratch1B (bottom row)
|
|
; Called from: overlay panel builders only ($6FFD, $7092, $7A80, $7B0A, $7B4C)
|
|
; ----------------------------------------------------------------------
|
|
drawWindowInnerCorners:
|
|
lda windowOriginRow ; 2B75 window top row
|
|
sta scratch18 ; 2B78 scratch18 = the row of the top studs (bumped below)
|
|
clc ; 2B7A clear the carry for the height add
|
|
adc frameHeight ; 2B7B plus the height ...
|
|
sta scratch1B ; 2B7E ... = the bottom row of the frame
|
|
lda windowOriginCol ; 2B80 window left column
|
|
sta scratch19 ; 2B83 scratch19 = the column of the left studs (bumped below)
|
|
clc ; 2B85 clear the carry for the width add
|
|
adc frameWidth ; 2B86 plus the width ...
|
|
sta scratch1A ; 2B89 ... = the right column of the frame
|
|
inc scratch18 ; 2B8B move one row down, inside the frame
|
|
inc scratch19 ; 2B8D and one column right
|
|
dec scratch1A ; 2B8F one column left of the right edge
|
|
dec scratch1B ; 2B91 one row up from the bottom edge
|
|
lda #$38 ; 2B93 glyph $38 = the small hollow ring at $95E6
|
|
jsr drawCharAt ; 2B95 top-left stud
|
|
lda scratch1A ; 2B98 the column just inside the right edge
|
|
sta scratch19 ; 2B9A move to the right-hand column
|
|
lda #$38 ; 2B9C the same ring glyph again
|
|
jsr drawCharAt ; 2B9E top-right stud
|
|
lda scratch1B ; 2BA1 the row just inside the bottom edge
|
|
sta scratch18 ; 2BA3 move down to the bottom row
|
|
lda #$38 ; 2BA5 the same ring glyph again
|
|
jsr drawCharAt ; 2BA7 bottom-right stud
|
|
lda windowOriginCol ; 2BAA back to the window's left column ...
|
|
sta scratch19 ; 2BAD back to the window's left column
|
|
inc scratch19 ; 2BAF ... plus one, inside the frame
|
|
lda #$38 ; 2BB1 the fourth ring glyph
|
|
jsr drawCharAt ; 2BB3 bottom-left stud
|
|
rts ; 2BB6 all four studs drawn
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawTallGlyph - Draws one of the ten two-cell-tall symbols whose character-code pairs live at $9810
|
|
; (20 bytes: top code at $9810+A*2, bottom code at $9810+A*2+1). The top half goes at
|
|
; scratch18/scratch19 and the bottom half one row lower; scratch18 is restored on the way out. Used
|
|
; by the overlays for the double-height icons on the comcen and setup screens.
|
|
; In: A = symbol index 0-9, scratch18 = top row, scratch19 = column
|
|
; Out: two characters drawn; scratch18/scratch19 unchanged
|
|
; Called from: overlay screens only ($6FD9, $6FE1, $6FE9, $7055, $7AD8, $7AE0)
|
|
; ----------------------------------------------------------------------
|
|
drawTallGlyph:
|
|
asl a ; 2BB7 two character codes per symbol
|
|
pha ; 2BB8 keep the doubled index across the first draw
|
|
tax ; 2BB9 index the top half of the pair
|
|
lda titleColorRow00Cell16,x ; 2BBA top-half character code (also graphicsData9300:tallDigitGlyphPairTable)
|
|
jsr drawCharAt ; 2BBD draw the top cell
|
|
inc scratch18 ; 2BC0 the bottom half sits one row lower
|
|
pla ; 2BC2 get the doubled index back
|
|
tax ; 2BC3 index it again for the bottom half
|
|
inx ; 2BC4 the odd entry of the pair
|
|
lda titleColorRow00Cell16,x ; 2BC5 bottom-half character code (also graphicsData9300:tallDigitGlyphPairTable)
|
|
jsr drawCharAt ; 2BC8 draw the bottom cell
|
|
dec scratch18 ; 2BCB put the caller's row back
|
|
rts ; 2BCD both halves of the symbol are drawn
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; xorColourBlock2x2 - Flashes one battlefield tile by EORing the colour byte A into the 2x2 screen-RAM
|
|
; colour cells at windowOriginRow/windowOriginCol (a map cell is 16x16 pixels = 2x2 character cells in
|
|
; the magnified view), then puts both draw modes back to plain replace. Calling it twice with the
|
|
; same value restores the original colours.
|
|
; In: A = colour byte to toggle, windowOriginRow/windowOriginCol
|
|
; Out: four screen-RAM colour bytes toggled; drawColourByte = A; both draw modes reset to 0
|
|
; Called from: the overlays only ($75DA, $807F, $814D)
|
|
; ----------------------------------------------------------------------
|
|
xorColourBlock2x2:
|
|
ldy #$02 ; 2BCE colour draw mode 2 = EOR
|
|
jsr setColourDrawMode ; 2BD0 patch the two colour combine instructions
|
|
ldx #$02 ; 2BD3 2 cells wide ...
|
|
ldy #$02 ; 2BD5 ... and 2 cells tall = one map tile
|
|
jsr fillScreenColourRect ; 2BD7 toggle the four colour bytes
|
|
jmp resetDrawModes ; 2BDA restore replace mode and return through it
|
|
|
|
|
|
; The 27 predefined screen windows, held as four parallel 27-byte tables plus a table of frame
|
|
; tile-set pointers, all indexed by the window number passed to drawWindowFrame in X. The ones used
|
|
; by this file are 0 = the 22x22 overview panel at (0,0), 1 = the 16x12 battlefield view frame at
|
|
; column 24, 2 = the 16x11 console panel below it, 3 and 4 = the 38x3 message frame at row 22 (3 in
|
|
; battle, 4 elsewhere),
|
|
; $18/$19 = the full-width main-menu frames. Windows 5-$16 belong to the $6F00 overlays' setup, stats
|
|
; and
|
|
; comcen screens.
|
|
windowLeftTable:
|
|
.byte $00,$18,$18,$01,$01,$01,$02,$1B; 2BDD ........ left screen column of windows 0-7: overview 0, view 24, console 24, message 1, message 1, ...
|
|
.byte $1F ; 2BE5 windows 8-15
|
|
.byte $1D,$1C,$1D,$1E,$01,$01,$0B; 2BE6 .......
|
|
.byte $0C ; 2BED windows $10-$17
|
|
.byte $1D,$20,$1E,$0D,$12,$0D,$0D; 2BEE . .....
|
|
.byte $00 ; 2BF5 windows $18-$1A: the two main-menu frames at columns 0 and 1, and a 30-wide box at column 5
|
|
.byte $01,$05 ; 2BF6 ..
|
|
|
|
; windowTopTable: byteTable, 27 bytes. Top screen row of windows 0-26
|
|
windowTopTable:
|
|
.byte $00,$00,$0B,$16,$16,$01,$02,$0F; 2BF8 ........ top screen row of windows 0-7: overview 0, view 0, console 11, message 22, message 22, ...
|
|
.byte $10 ; 2C00 windows 8-15
|
|
.byte $08,$00,$01,$02,$03,$0C,$04; 2C01 .......
|
|
.byte $05 ; 2C08 windows $10-$17
|
|
.byte $03,$04,$0A,$06,$0A,$06,$04; 2C09 .......
|
|
.byte $05 ; 2C10 windows $18-$1A
|
|
.byte $06,$00 ; 2C11 ..
|
|
|
|
; windowWidthTable: byteTable, 27 bytes. Width in cells (including the frame) of windows 0-26; e.g.
|
|
; window 0 = 22x22 around the 20x20 overview map, window 1 = 16x12 around the 7x5 cell (14x10 char)
|
|
; battlefield view
|
|
windowWidthTable:
|
|
.byte $16,$10,$10,$26,$26,$19,$17,$0C; 2C13 ...&&... width in cells (frame included) of windows 0-7: 22, 16, 16, 38, 38, ...
|
|
.byte $04 ; 2C1B windows 8-15
|
|
.byte $08,$0B,$09,$07,$09,$09,$12; 2C1C .......
|
|
.byte $10 ; 2C23 windows $10-$17
|
|
.byte $0B,$05,$0A,$0E,$04,$0E,$0E; 2C24 .......
|
|
.byte $28 ; 2C2B windows $18-$1A: 40, 38 and 30 cells wide
|
|
.byte $26,$1E ; 2C2C &.
|
|
|
|
; windowHeightTable: byteTable, 27 bytes. Height in cells of windows 0-26
|
|
windowHeightTable:
|
|
.byte $16,$0C,$0B,$03,$03,$15,$13,$07; 2C2E ........ height in cells (frame included) of windows 0-7: 22, 12, 11, 3, 3, ...
|
|
.byte $04 ; 2C36 windows 8-15
|
|
.byte $07,$08,$06,$04,$09,$09,$10; 2C37 .......
|
|
.byte $0E ; 2C3E windows $10-$17
|
|
.byte $07,$04,$0B,$0C,$04,$0C,$0C; 2C3F .......
|
|
.byte $14 ; 2C46 windows $18-$1A: 20, 18 and 7 rows tall
|
|
.byte $12,$07 ; 2C47 ..
|
|
|
|
; One little-endian pointer per window to its 9-byte frame tile set in $9824-$987D. A set is laid out
|
|
; top-left, top edge, top-right, left edge, centre (never used), right edge, bottom-left, bottom edge,
|
|
; bottom-right. $9824, $982D, $9836 and $983F are four different line styles, $9848 is all glyph $03
|
|
; (a solid block) and $9851 is all glyph $00 (blank, i.e. the frame erases itself), while $985A,
|
|
; $9863,
|
|
; $986C and $9875 are the four heavy panel styles used by windows 0-3.
|
|
windowFramePtrTable:
|
|
.addr windowFrameTileSetDither; 2C49 window 0 - overview panel
|
|
.byte $63 ; 2C4B window 1 - battlefield view
|
|
.byte $98 ; 2C4C .
|
|
.byte $6C ; 2C4D window 2 - console panel
|
|
.byte $98 ; 2C4E .
|
|
.byte $75 ; 2C4F window 3 - message frame during the battle
|
|
.byte $98 ; 2C50 .
|
|
.byte $24 ; 2C51 window 4 - message frame outside the battle
|
|
.byte $98 ; 2C52 .
|
|
.byte $36 ; 2C53 window 5
|
|
.byte $98 ; 2C54 .
|
|
.byte $3F ; 2C55 window 6
|
|
.byte $98 ; 2C56 .
|
|
.byte $2D ; 2C57 window 7
|
|
.byte $98 ; 2C58 .
|
|
.byte $24 ; 2C59 window 8
|
|
.byte $98 ; 2C5A .
|
|
.byte $2D ; 2C5B window 9
|
|
.byte $98 ; 2C5C .
|
|
.byte $2D ; 2C5D window $0A
|
|
.byte $98 ; 2C5E .
|
|
.byte $24 ; 2C5F window $0B
|
|
.byte $98 ; 2C60 .
|
|
.byte $48 ; 2C61 window $0C - solid block frame
|
|
.byte $98 ; 2C62 .
|
|
.byte $36 ; 2C63 window $0D
|
|
.byte $98 ; 2C64 .
|
|
.byte $36 ; 2C65 window $0E
|
|
.byte $98 ; 2C66 .
|
|
.byte $36 ; 2C67 window $0F
|
|
.byte $98 ; 2C68 .
|
|
.byte $3F ; 2C69 window $10
|
|
.byte $98 ; 2C6A .
|
|
.byte $2D ; 2C6B window $11
|
|
.byte $98 ; 2C6C .
|
|
.byte $24 ; 2C6D window $12
|
|
.byte $98 ; 2C6E .
|
|
.byte $2D ; 2C6F window $13
|
|
.byte $98 ; 2C70 .
|
|
.byte $51 ; 2C71 window $14 - blank tiles, so drawing this window erases the frame
|
|
.byte $98 ; 2C72 .
|
|
.byte $51 ; 2C73 window $15 - blank tiles as well
|
|
.byte $98 ; 2C74 .
|
|
.byte $48 ; 2C75 window $16 - solid block frame
|
|
.byte $98 ; 2C76 .
|
|
.byte $48 ; 2C77 window $17 - solid block frame
|
|
.byte $98 ; 2C78 .
|
|
.byte $36 ; 2C79 window $18 - main menu outer frame
|
|
.byte $98 ; 2C7A .
|
|
.byte $3F ; 2C7B window $19 - main menu inner frame
|
|
.byte $98 ; 2C7C .
|
|
.byte $2D ; 2C7D window $1A
|
|
.byte $98 ; 2C7E .
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawWindowFrame - Draws one of the 27 predefined windows selected by X. Copies its left column, top
|
|
; row, width and height from the four parallel tables into windowOriginCol/windowOriginRow/frameWidth/
|
|
; frameHeight, fetches its 9-byte frame tile set from windowFramePtrTable and falls into drawFrameBox.
|
|
; In: X = window number 0-26
|
|
; Out: windowOriginRow/Col and frameWidth/frameHeight set (the latter two are then decremented by
|
|
; drawFrameBox, so they end up as the right/bottom extents); the frame is drawn; A/X/Y clobbered
|
|
; Called from: $1567, $156C, $269E and the jumps at $2695, $26BE, $28EE here, and from sixteen sites
|
|
; in
|
|
; each $6F00 overlay
|
|
; ----------------------------------------------------------------------
|
|
drawWindowFrame:
|
|
lda windowLeftTable,x ; 2C7F left screen column of this window
|
|
sta windowOriginCol ; 2C82 left column of the window the plotters will use
|
|
lda windowTopTable,x ; 2C85 top screen row
|
|
sta windowOriginRow ; 2C88 top row of that window
|
|
lda windowWidthTable,x ; 2C8B width in cells
|
|
sta frameWidth ; 2C8E width in cells, before drawFrameBox decrements it
|
|
lda windowHeightTable,x ; 2C91 height in cells
|
|
sta frameHeight ; 2C94 height in cells, before drawFrameBox decrements it
|
|
txa ; 2C97 the pointer table has two bytes per window ...
|
|
asl a ; 2C98 ... so double the index
|
|
tax ; 2C99 index the pointer table
|
|
lda windowFramePtrTable,x ; 2C9A low byte of this window's tile set
|
|
inx ; 2C9D step to the high byte of the pointer
|
|
ldy windowFramePtrTable,x ; 2C9E high byte - drawFrameBox expects A/Y
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawFrameBox - Draws a rectangular frame from a 9-byte tile set at windowOriginRow/windowOriginCol,
|
|
; frameWidth by frameHeight cells. The tile set is indexed 0 top-left, 1 top edge, 2 top-right, 3
|
|
; left edge, 4 centre (never read), 5 right edge, 6 bottom-left, 7 bottom edge, 8 bottom-right. The
|
|
; four corners are drawn first, then the bottom, top, left and right runs through
|
|
; drawFrameRow/drawFrameColumn, each of which stops before the corner it would overwrite. Width and
|
|
; height are decremented on entry so they become the right/bottom offsets, and are left that way for
|
|
; drawWindowInnerCorners.
|
|
; In: A/Y = lo/hi address of the 9-byte tile set; windowOriginRow, windowOriginCol, frameWidth,
|
|
; frameHeight; the current colour state (useDrawColourFlag / drawColourByte / fontColorTable)
|
|
; Out: the frame is drawn into the bitmap and screen RAM; frameWidth and frameHeight are each 1
|
|
; smaller;
|
|
; scratch1A = right column, scratch1B = bottom row, blockPtr (zp_B8/B9) = the tile set
|
|
; Called from: drawWindowFrame by fall-through, and directly by overlay B ($724A) with its own tile
|
|
; set
|
|
; ----------------------------------------------------------------------
|
|
drawFrameBox:
|
|
sta blockPtr ; 2CA1 keep the tile set address in zp_B8/B9
|
|
sty blockPtrHi ; 2CA3 zp_B8/B9 = the 9-byte tile set
|
|
dec frameWidth ; 2CA5 width in cells becomes the offset of the rightmost column
|
|
dec frameHeight ; 2CA8 height in cells becomes the offset of the bottom row
|
|
lda windowOriginRow ; 2CAB window top row ...
|
|
sta scratch18 ; 2CAE ... is the row of the top corners
|
|
clc ; 2CB0 clear the carry for the height add
|
|
adc frameHeight ; 2CB1 top row + height-1 ...
|
|
sta scratch1B ; 2CB4 ... = the bottom row, kept in scratch1B as the column loops' stop value
|
|
lda windowOriginCol ; 2CB6 window left column ...
|
|
sta scratch19 ; 2CB9 ... is the column of the left corners
|
|
clc ; 2CBB clear the carry for the width add
|
|
adc frameWidth ; 2CBC left column + width-1 ...
|
|
sta scratch1A ; 2CBF ... = the right column, kept in scratch1A as the row loops' stop value
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The four corners first, so the edge runs can stop one cell short of each.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$00 ; 2CC1 tile 0 = top-left corner
|
|
lda (blockPtr),y ; 2CC3 tile 0 of the set
|
|
jsr drawCharAt ; 2CC5 draw it at (top, left)
|
|
lda scratch1A ; 2CC8 move to the right column
|
|
sta scratch19 ; 2CCA move to the right-hand column
|
|
ldy #$02 ; 2CCC tile 2 = top-right corner
|
|
lda (blockPtr),y ; 2CCE tile 2 of the set
|
|
jsr drawCharAt ; 2CD0 draw it at (top, right)
|
|
lda scratch1B ; 2CD3 move down to the bottom row
|
|
sta scratch18 ; 2CD5 move down to the bottom row
|
|
ldy #$08 ; 2CD7 tile 8 = bottom-right corner
|
|
lda (blockPtr),y ; 2CD9 tile 8 of the set
|
|
jsr drawCharAt ; 2CDB draw it at (bottom, right)
|
|
lda windowOriginCol ; 2CDE back to the left column
|
|
sta scratch19 ; 2CE1 back to the left-hand column
|
|
ldy #$06 ; 2CE3 tile 6 = bottom-left corner
|
|
lda (blockPtr),y ; 2CE5 tile 6 of the set
|
|
jsr drawCharAt ; 2CE7 draw it at (bottom, left)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Now the four edges. Row and column are still at the bottom-left corner.
|
|
; ----------------------------------------------------------------------
|
|
inc scratch19 ; 2CEA start one column right of the bottom-left corner
|
|
ldy #$07 ; 2CEC tile 7 = bottom edge
|
|
jsr drawFrameRow ; 2CEE run it along the bottom row up to (not including) the right corner
|
|
ldy windowOriginRow ; 2CF1 back to the top row
|
|
sty scratch18 ; 2CF4 row of the top edge
|
|
ldy windowOriginCol ; 2CF6 and one column right of the left corner
|
|
iny ; 2CF9 one column right of the top-left corner
|
|
sty scratch19 ; 2CFA first column of the top edge run
|
|
ldy #$01 ; 2CFC tile 1 = top edge
|
|
jsr drawFrameRow ; 2CFE run it along the top row
|
|
ldy windowOriginRow ; 2D01 one row below the top-left corner
|
|
iny ; 2D04 one row below the top-left corner
|
|
sty scratch18 ; 2D05 first row of the left edge run
|
|
ldy windowOriginCol ; 2D07 in the window's left column
|
|
sty scratch19 ; 2D0A the window's left column
|
|
ldy #$03 ; 2D0C tile 3 = left edge
|
|
jsr drawFrameColumn ; 2D0E run it down the left side up to (not including) the bottom corner
|
|
ldy windowOriginRow ; 2D11 one row below the top-right corner
|
|
iny ; 2D14 one row below the top-right corner
|
|
sty scratch18 ; 2D15 first row of the right edge run
|
|
lda scratch1A ; 2D17 in the right-hand column
|
|
sta scratch19 ; 2D19 the window's right column
|
|
ldy #$05 ; 2D1B tile 5 = right edge - and fall into drawFrameColumn to draw it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawFrameColumn - Repeats the tile at offset Y of the current tile set down column scratch19,
|
|
; starting at row scratch18 and stopping when the row reaches scratch1B (exclusive), so the corner
|
|
; already drawn there is not overwritten. Used for the left and right frame edges.
|
|
; In: Y = tile offset in the set, blockPtr = the tile set, scratch18 = first row, scratch19 = column,
|
|
; scratch1B = stop row
|
|
; Out: scratch18 = scratch1B, scratch1C = the tile's character code, the cells drawn
|
|
; Called from: drawFrameBox ($2D0E) and by fall-through from $2D1B
|
|
; ----------------------------------------------------------------------
|
|
drawFrameColumn:
|
|
lda (blockPtr),y ; 2D1D fetch the edge tile's character code once
|
|
sta scratch1C ; 2D1F keep it in zp_1C - drawCharAt clobbers A
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One cell per row until the bottom corner is reached.
|
|
; ----------------------------------------------------------------------
|
|
L_2D21:
|
|
lda scratch1C ; 2D21 the edge tile
|
|
jsr drawCharAt ; 2D23 draw it at the current row/column
|
|
inc scratch18 ; 2D26 step one row down
|
|
ldy scratch18 ; 2D28 the row we have just moved to
|
|
cpy scratch1B ; 2D2A reached the row holding the corner?
|
|
bne L_2D21 ; 2D2C not yet - keep going
|
|
rts ; 2D2E the column is finished one cell above the corner
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawFrameRow - Repeats the tile at offset Y of the current tile set along row scratch18, starting at
|
|
; column scratch19 and stopping when the column reaches scratch1A (exclusive). Used for the top and
|
|
; bottom frame edges.
|
|
; In: Y = tile offset in the set, blockPtr = the tile set, scratch18 = row, scratch19 = first column,
|
|
; scratch1A = stop column
|
|
; Out: scratch19 = scratch1A, scratch1C = the tile's character code, the cells drawn
|
|
; Called from: drawFrameBox ($2CEE bottom edge, $2CFE top edge)
|
|
; ----------------------------------------------------------------------
|
|
drawFrameRow:
|
|
lda (blockPtr),y ; 2D2F fetch the edge tile's character code once
|
|
sta scratch1C ; 2D31 keep it in zp_1C - drawCharAt clobbers A
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One cell per column until the corner column is reached.
|
|
; ----------------------------------------------------------------------
|
|
L_2D33:
|
|
lda scratch1C ; 2D33 the edge tile
|
|
jsr drawCharAt ; 2D35 draw it at the current row/column
|
|
inc scratch19 ; 2D38 step one column right
|
|
ldy scratch19 ; 2D3A the column we have just moved to
|
|
cpy scratch1A ; 2D3C reached the column holding the corner?
|
|
bne L_2D33 ; 2D3E not yet - keep going
|
|
rts ; 2D40 the run is finished one cell before the corner
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initBitmapScreen - Builds the game's display from scratch: a full-screen text window, grey border
|
|
; and background, colour RAM filled with grey, all 1024 screen-RAM bytes set to $CC (grey foreground
|
|
; on grey background so an empty bitmap looks uniform), the whole 8000-byte bitmap $A000-$BF3F
|
|
; cleared, and then the three VIC control registers and their shadows set for a hires bitmap in VIC
|
|
; bank 2 - $D016 = $C8 (40 columns, hires), $D018 = $38 (video matrix $8C00, bitmap $A000) and $D011 =
|
|
; $3B (bitmap mode, 25 rows, Y-scroll 3). Finally initSpritePointers rewrites the eight sprite
|
|
; pointers the screen fill just overwrote.
|
|
; In: none
|
|
; Out: VIC_BORDER/BG0/CTRL1/CTRL2/MEM_PTR and the shadows $9000-$9004; colour RAM, screen RAM and
|
|
; bitmap
|
|
; cleared; screenBuiltFlag = 1, decimalValueHigh = 0, spriteEnableShadow = 0, sprite pointers
|
|
; $8FF8-$8FFF = $28-$2F; mapCellPtr (zp_48/49) clobbered
|
|
; Called from: mainStart ($0AAF), drawBattlefieldScreen ($2636), the screen switcher ($06E8) and the
|
|
; start-up block ($0318 by JMP)
|
|
; ----------------------------------------------------------------------
|
|
initBitmapScreen:
|
|
jsr setFullScreenWindow ; 2D41 text window = the whole 40x25 screen
|
|
lda #$01 ; 2D44 mark the screen as (re)built
|
|
sta screenBuiltFlag ; 2D46 every screen builder sets this flag; nothing ever reads it
|
|
lda #$0C ; 2D48 colour $0C = medium grey
|
|
sta vicBorderShadow ; 2D4A shadow the IRQ writes back to $D020
|
|
sta VIC_BORDER ; 2D4D grey border
|
|
sta vicBg0Shadow ; 2D50 shadow of $D021
|
|
sta VIC_BG0 ; 2D53 grey background
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fill all four pages of colour RAM ($D800-$DBFF) with grey.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$00 ; 2D56 256 offsets per page
|
|
lda #$0C ; 2D58 grey again (the STAs below clobber A)
|
|
L_2D5A:
|
|
sta COLOR_RAM,y ; 2D5A $D800-$D8FF
|
|
sta D_D900,y ; 2D5D $D900-$D9FF
|
|
sta D_DA00,y ; 2D60 $DA00-$DAFF
|
|
sta D_DB00,y ; 2D63 $DB00-$DBFF (only $D800-$DBE7 is real colour RAM)
|
|
iny ; 2D66 next offset in the page
|
|
bne L_2D5A ; 2D67 256 bytes per page
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fill the video matrix $8C00-$8FFF with $CC: in bitmap mode the high nibble is the '1' colour and the
|
|
; low nibble the '0' colour, so grey on grey hides the empty bitmap.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$00 ; 2D69 256 offsets per page
|
|
lda #$CC ; 2D6B $CC = grey foreground, grey background
|
|
L_2D6D:
|
|
sta D_8C00,y ; 2D6D $8C00-$8CFF
|
|
sta D_8D00,y ; 2D70 $8D00-$8DFF
|
|
sta D_8E00,y ; 2D73 $8E00-$8EFF
|
|
sta D_8F00,y ; 2D76 $8F00-$8FFF (this also wipes the sprite pointers at $8FF8, restored below)
|
|
iny ; 2D79 next offset in the page
|
|
bne L_2D6D ; 2D7A all four pages of the video matrix
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Clear the 8000-byte bitmap $A000-$BF3F (25 rows x 320 bytes).
|
|
; ----------------------------------------------------------------------
|
|
lda #$00 ; 2D7C point zp_48/49 at the start of the bitmap ...
|
|
sta mapCellPtr ; 2D7E start of the bitmap ...
|
|
lda #$A0 ; 2D80 ...
|
|
sta mapCellPtrHi ; 2D82 ... $A000
|
|
lda #$00 ; 2D84 0 for the store below and for the bitmap fill
|
|
sta decimalValueHigh ; 2D86 clear the third byte of printDecimal's 24-bit accumulator (zp_7E); the printer clears it again
|
|
ldy #$FF ; 2D88 fill each page from offset $FF downwards
|
|
lda #$00 ; 2D8A the bitmap is cleared to background pixels
|
|
L_2D8C:
|
|
sta (mapCellPtr),y ; 2D8C clear one bitmap byte
|
|
dey ; 2D8E work down through the page
|
|
cpy #$FF ; 2D8F wrapped past 0 - the page is done
|
|
bne L_2D8C ; 2D91 not wrapped yet - keep clearing
|
|
inc mapCellPtrHi ; 2D93 next page
|
|
ldx mapCellPtrHi ; 2D95 check how far up the bitmap we have got
|
|
cpx #$BF ; 2D97 $BF00 is the last, partial page
|
|
bcc L_2D8C ; 2D99 still below it - clear a whole page
|
|
bne L_2DA1 ; 2D9B past $BF00 - the bitmap is finished
|
|
ldy #$3F ; 2D9D $BF00-$BF3F: the final 64 bytes of the 8000-byte bitmap
|
|
bne L_2D8C ; 2D9F always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Switch the VIC into the game's display mode.
|
|
; ----------------------------------------------------------------------
|
|
L_2DA1:
|
|
lda #$C8 ; 2DA1 $D016 = $C8: hires (bit 4 clear), 40 columns (bit 3), X-scroll 0
|
|
sta vicCtrl2Shadow ; 2DA3 shadow the raster IRQ writes back to $D016
|
|
sta VIC_CTRL2 ; 2DA6 and the register itself
|
|
lda #$38 ; 2DA9 $D018 = $38: video matrix at bank+$0C00 = $8C00, bitmap at bank+$2000 = $A000
|
|
sta vicMemPtrShadow ; 2DAB shadow of $D018
|
|
sta VIC_MEM_PTR ; 2DAE and the register itself
|
|
lda #$3B ; 2DB1 $D011 = $3B: display on, bitmap mode, 25 rows, Y-scroll 3
|
|
sta vicCtrl1Shadow ; 2DB3 shadow of $D011
|
|
sta VIC_CTRL1 ; 2DB6 and the register itself - the bitmap screen is now live
|
|
jsr initSpritePointers ; 2DB9 put the eight sprite pointers back after the screen fill
|
|
rts ; 2DBC the display is ready
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setTerrainTilePtr - Points glyphPtr at the 8-byte terrain tile definition number A in the table at
|
|
; $9C6E and turns off the per-glyph colour override so the caller's drawColourByte is used. The
|
|
; caller at
|
|
; $27B2 forms A by subtracting $40 from the map byte, and $27C2 expands the 8 bytes into the
|
|
; 16x16-pixel
|
|
; cell of the magnified battlefield view.
|
|
; In: A = terrain tile index (map cell byte - $40)
|
|
; Out: glyphPtr/glyphPtrHi (zp_B2/B3) = $9C6E + A*8, useDrawColourFlag = 0, Y = 0
|
|
; Called from: renderMapTile ($27B8) and the radar tile renderer of overlay B ($73A4)
|
|
; ----------------------------------------------------------------------
|
|
setTerrainTilePtr:
|
|
ldy #$00 ; 2DBD Y = 0 is used as the zero source for the three stores below
|
|
sty glyphPtr ; 2DBF clear the tile pointer before building it
|
|
sty glyphPtrHi ; 2DC1 clear the high byte so the three ROLs can shift into it
|
|
sty useDrawColourFlag ; 2DC3 0 = let the caller's drawColourByte colour the cell
|
|
asl a ; 2DC6 index * 2 ...
|
|
rol glyphPtrHi ; 2DC7 carry the overflow into the high byte
|
|
asl a ; 2DC9 ... * 4 ...
|
|
rol glyphPtrHi ; 2DCA carry again
|
|
asl a ; 2DCC ... * 8: eight bytes per terrain tile
|
|
rol glyphPtrHi ; 2DCD and again - glyphPtrHi holds bits 8-10 of index*8
|
|
clc ; 2DCF clear the carry for the base-address add
|
|
adc #$6E ; 2DD0 add the low byte of the tile table base $9C6E
|
|
sta glyphPtr ; 2DD2 low byte of $9C6E + index*8
|
|
lda glyphPtrHi ; 2DD4 bits 8-10 of index*8
|
|
adc #$9C ; 2DD6 and its high byte
|
|
sta glyphPtrHi ; 2DD8 zp_B2/B3 now points at the 8-byte terrain tile
|
|
rts ; 2DDA the caller can now read the eight tile bytes through (glyphPtr),y
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearMessageLineAndScreen - Wipes the status/message line first, then falls into
|
|
; clearCurrentScreenWindows to clear the rest of whatever screen is showing.
|
|
; In: currentScreen ($90FB)
|
|
; Out: see clearCurrentScreenWindows
|
|
; Called from: the F1/F3/F5/F7 screen switcher at $070C
|
|
; ----------------------------------------------------------------------
|
|
clearMessageLineAndScreen:
|
|
jsr clearMessageLine ; 2DDB blank the bottom message line ($C39F)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearCurrentScreenWindows - Re-clears the content areas of whichever screen is active, dispatching
|
|
; on currentScreen: 0 = battlefield (sprites off, text colour 0, the console info panel unless the
|
|
; game is paused, then the overview window and the viewport text area), 1 = the comcen radar screen
|
|
; (overlay B's
|
|
; $7A6B), 2 = the drone screen (only sprite 1 enabled, then overlay B's $821F), 3 = the menu screen.
|
|
; All
|
|
; four exits are tail calls.
|
|
; In: currentScreen ($90FB), pauseState (zp_BE)
|
|
; Out: spriteEnableShadow = 0 (or 2 on the drone screen), textColour = 0 on the battlefield; the
|
|
; per-screen clear routine is jumped to
|
|
; Called from: clearMessageLineAndScreen by fall-through, and from the text engine at $C75E (after the
|
|
; text window has been saved, e.g. when a pause ends)
|
|
; ----------------------------------------------------------------------
|
|
clearCurrentScreenWindows:
|
|
lda #$00 ; 2DDE 0 is used both as the sprite mask and as the text colour below
|
|
sta spriteEnableShadow ; 2DE0 hide every sprite while the screen is rebuilt
|
|
ldy currentScreen ; 2DE3 which screen is showing? 0 battlefield, 1 comcen radar, 2 drone, 3 menu
|
|
bne L_2DF7 ; 2DE6 not the battlefield - dispatch below
|
|
sta textColour ; 2DE8 battlefield: draw the panels in colour 0
|
|
lda pauseState ; 2DEA pause / disconnect flags
|
|
bne L_2DF1 ; 2DEC while paused the console keeps the pause text - do not wipe it
|
|
jsr clearInfoPanel ; 2DEE clear the console info panel (rows 12-21, columns 25-38)
|
|
L_2DF1:
|
|
jsr clearOverviewWindow ; 2DF1 clear the 20x20 overview map window
|
|
jmp clearViewportTextArea ; 2DF4 clear the text area of the battlefield viewport and return through it
|
|
|
|
L_2DF7:
|
|
dey ; 2DF7 screen 1?
|
|
bne L_2DFD ; 2DF8 no
|
|
jmp redrawRadarMapWindow ; 2DFA comcen radar screen - overlay B redraws the radar map window
|
|
|
|
L_2DFD:
|
|
dey ; 2DFD screen 2?
|
|
bne L_2E0A ; 2DFE no - it must be screen 3, the menu
|
|
lda #$02 ; 2E00 drone screen: only sprite 1 (the drone marker) stays visible
|
|
sta spriteEnableShadow ; 2E02 %00000010 - sprite 1 only
|
|
lda #$00 ; 2E05 argument 0 = clear the whole drone view window
|
|
jmp fillDroneViewWindow ; 2E07 overlay B's drone window filler (variant A holds unrelated code at this address) (also mapGenerator6F00:runGameTypeMenu)
|
|
|
|
L_2E0A:
|
|
jmp clearMenuWindowPlain ; 2E0A menu screen: plain clear of the menu window
|
|
|
|
|
|
; Two work flags of magnifyWindow2x, set when the source rectangle has an odd width or height so that
|
|
; its outermost row/column is drawn at half size and the zoom stays centred.
|
|
magnifyWidthOdd:
|
|
.byte $00 ; 2E0D . magnifyWidthOdd: $FF when frameWidth is odd
|
|
magnifyHeightOdd:
|
|
.byte $00 ; 2E0E magnifyHeightOdd: $FF when frameHeight is odd
|
|
|
|
; ----------------------------------------------------------------------
|
|
; magnifyWindow2x - Redraws the character rectangle at windowOriginRow/windowOriginCol, frameWidth by
|
|
; frameHeight cells, at twice the size in place and centred on itself: every source cell (its 8 bitmap
|
|
; bytes plus its screen-RAM colour byte) is re-plotted as a 2x2 cell block through the text engine's
|
|
; double-size glyph renderer plotGlyphDoubleSize ($C24B), at (windowOriginRow - ceil(h/2) + 2*row,
|
|
; windowOriginCol - ceil(w/2) + 2*col). The rectangle is walked as four quadrants, each from the
|
|
; outer edge inwards, so no source cell is destroyed before it has been read. An odd width or height
|
|
; makes the routine step half a cell beyond each edge and suppress the outer half of those cells
|
|
; through the quadrant mask magnifyEdgeMask (zp_BA), which keeps the zoom centred. It first waits for
|
|
; raster line $80 to reduce tearing.
|
|
; In: windowOriginRow, windowOriginCol, frameWidth ($9221), frameHeight ($9222)
|
|
; Out: the area redrawn twice the size; frameWidth/frameHeight decremented when even; scratch20 =
|
|
; ceil(w/2), scratch21 = ceil(h/2), textCursorCol/textCursorRow (zp_3F/zp_40) = destination
|
|
; origin,
|
|
; magnifySrcRow/Col (zp_30/31) = counters, textColour = the current cell's colour,
|
|
; magnifyEdgeMask
|
|
; left at 0, zp_B2-zp_B9 clobbered
|
|
; Called from: overlay B's zoom-in animation at $756B, four times over a 7-wide by 6-tall area at row
|
|
; 9,
|
|
; column 16
|
|
; ----------------------------------------------------------------------
|
|
magnifyWindow2x:
|
|
lda #$00 ; 2E0F clear both odd-size flags before measuring the rectangle
|
|
sta magnifyWidthOdd ; 2E11 assume an even width ...
|
|
sta magnifyHeightOdd ; 2E14 ... and an even height
|
|
lda frameWidth ; 2E17 source rectangle width in cells
|
|
lsr a ; 2E1A half of it, carry = the odd bit
|
|
sta scratch20 ; 2E1B zp_20 = how many columns each half-pass covers
|
|
bcc L_2E28 ; 2E1D even width - nothing special to do
|
|
inc scratch20 ; 2E1F odd: round the half width up
|
|
lda #$FF ; 2E21 $FF = odd
|
|
sta magnifyWidthOdd ; 2E23 remember that the left/right edge cells are drawn half width
|
|
bmi L_2E2B ; 2E26 always taken ($FF is negative)
|
|
L_2E28:
|
|
dec frameWidth ; 2E28 even width: turn the count into the index of the last column
|
|
L_2E2B:
|
|
lda windowOriginCol ; 2E2B source left column ...
|
|
sec ; 2E2E prepare the subtraction
|
|
sbc scratch20 ; 2E2F ... minus half the width = destination left column
|
|
sta textCursorCol ; 2E31 zp_3F holds the destination origin column (the text cursor is free here)
|
|
lda frameHeight ; 2E33 source rectangle height in cells
|
|
lsr a ; 2E36 half of it
|
|
sta scratch21 ; 2E37 zp_21 = how many rows each half-pass covers
|
|
bcc L_2E44 ; 2E39 even height
|
|
inc scratch21 ; 2E3B odd: round the half height up
|
|
lda #$FF ; 2E3D $FF = odd
|
|
sta magnifyHeightOdd ; 2E3F remember that the top/bottom edge cells are drawn half height
|
|
bmi L_2E47 ; 2E42 always taken
|
|
L_2E44:
|
|
dec frameHeight ; 2E44 even height: turn the count into the index of the last row
|
|
L_2E47:
|
|
lda windowOriginRow ; 2E47 source top row ...
|
|
sec ; 2E4A prepare the subtraction
|
|
sbc scratch21 ; 2E4B ... minus half the height = destination top row
|
|
sta textCursorRow ; 2E4D zp_40 holds the destination origin row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait for the raster to reach line $80 so the redraw starts at a predictable place on screen.
|
|
; ----------------------------------------------------------------------
|
|
L_2E4F:
|
|
lda VIC_RASTER ; 2E4F current raster line
|
|
cmp #$80 ; 2E52 line 128, roughly the middle of the display
|
|
bne L_2E4F ; 2E54 spin until it comes round
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Quadrant pass 1 and 2: the top half of the rectangle, row 0 downwards. For each row the left half
|
|
; is drawn left-to-right and then the right half right-to-left, so a cell is always read before the
|
|
; enlarged copy of its neighbour can cover it.
|
|
; ----------------------------------------------------------------------
|
|
lda #$00 ; 2E56 top-half row counter
|
|
sta magnifySrcRow ; 2E58 start at the topmost source row
|
|
lda magnifyHeightOdd ; 2E5A odd height?
|
|
beq L_2E63 ; 2E5D no - no vertical clipping on this row
|
|
lda #$01 ; 2E5F bit 0 = suppress the top cells of the 2x2 block ...
|
|
sta magnifyEdgeMask ; 2E61 ... so the top row is drawn only half tall
|
|
L_2E63:
|
|
lda #$00 ; 2E63 left-half column counter
|
|
sta magnifySrcCol ; 2E65 start at the leftmost source column
|
|
lda magnifyWidthOdd ; 2E67 odd width?
|
|
beq L_2E72 ; 2E6A no - no horizontal clipping on this column
|
|
lda magnifyEdgeMask ; 2E6C add to the mask that is already there
|
|
ora #$04 ; 2E6E bit 2 = suppress the left cells of the 2x2 block
|
|
sta magnifyEdgeMask ; 2E70 so the leftmost column is drawn half wide
|
|
L_2E72:
|
|
jsr setMagnifyCellPointers ; 2E72 set up source and destination pointers for this cell
|
|
ldy #$00 ; 2E75 the colour byte is the single byte at the source screen cell
|
|
lda (blockPtr),y ; 2E77 the cell's colour byte, read from the source screen RAM copy in zp_B8/B9
|
|
sta textColour ; 2E79 plotGlyphDoubleSize colours all four cells with zp_3D
|
|
jsr plotGlyphDoubleSize ; 2E7B expand the cell into a 2x2 block at the destination
|
|
lda magnifyEdgeMask ; 2E7E read the mask back
|
|
and #$03 ; 2E80 clear the left/right suppression bits, keep the top/bottom ones for the rest of the row
|
|
sta magnifyEdgeMask ; 2E82 only the top/bottom bits survive into the next cell
|
|
inc magnifySrcCol ; 2E84 next column to the right
|
|
lda magnifySrcCol ; 2E86 the column just drawn
|
|
cmp scratch20 ; 2E88 compare against the half width
|
|
bcc L_2E72 ; 2E8A keep going until the middle of the row
|
|
lda frameWidth ; 2E8C now the right half: start at the outermost column ...
|
|
sta magnifySrcCol ; 2E8F ... which for an odd width is half a cell past the rectangle
|
|
lda magnifyWidthOdd ; 2E91 odd width?
|
|
beq L_2E9C ; 2E94 no
|
|
lda magnifyEdgeMask ; 2E96 add to the mask that is already there
|
|
ora #$08 ; 2E98 bit 3 = suppress the right cells, so this column is drawn half wide
|
|
sta magnifyEdgeMask ; 2E9A so the rightmost column is drawn half wide
|
|
L_2E9C:
|
|
jsr setMagnifyCellPointers ; 2E9C set up the pointers for this cell
|
|
ldy #$00 ; 2E9F the colour byte is the single byte at the source screen cell
|
|
lda (blockPtr),y ; 2EA1 its colour byte
|
|
sta textColour ; 2EA3 colour for the 2x2 block
|
|
jsr plotGlyphDoubleSize ; 2EA5 expand it
|
|
lda magnifyEdgeMask ; 2EA8 read the mask back
|
|
and #$03 ; 2EAA drop the left/right suppression again
|
|
sta magnifyEdgeMask ; 2EAC only the top/bottom bits survive into the next cell
|
|
dec magnifySrcCol ; 2EAE next column to the left
|
|
lda magnifySrcCol ; 2EB0 the column just drawn
|
|
cmp scratch20 ; 2EB2 compare against the half width
|
|
bcs L_2E9C ; 2EB4 keep going while we are still right of the middle
|
|
lda magnifyEdgeMask ; 2EB6 read the mask back
|
|
and #$0C ; 2EB8 row finished: drop the top/bottom suppression, keep nothing else
|
|
sta magnifyEdgeMask ; 2EBA only the left/right bits survive into the next row
|
|
inc magnifySrcRow ; 2EBC next row down
|
|
lda magnifySrcRow ; 2EBE the row just finished
|
|
cmp scratch21 ; 2EC0 compare against the half height
|
|
bcc L_2E63 ; 2EC2 keep going until the middle row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Quadrant pass 3 and 4: the bottom half, walked upwards from the outermost row.
|
|
; ----------------------------------------------------------------------
|
|
lda frameHeight ; 2EC4 start at the outermost row ...
|
|
sta magnifySrcRow ; 2EC7 ... which for an odd height is half a cell below the rectangle
|
|
lda magnifyHeightOdd ; 2EC9 odd height?
|
|
beq L_2ED2 ; 2ECC no
|
|
lda #$02 ; 2ECE bit 1 = suppress the bottom cells ...
|
|
sta magnifyEdgeMask ; 2ED0 ... so this row is drawn only half tall
|
|
L_2ED2:
|
|
lda #$00 ; 2ED2 left-half column counter
|
|
sta magnifySrcCol ; 2ED4 start at the leftmost source column again
|
|
lda magnifyWidthOdd ; 2ED6 odd width?
|
|
beq L_2EE1 ; 2ED9 no
|
|
lda magnifyEdgeMask ; 2EDB add to the mask that is already there
|
|
ora #$04 ; 2EDD bit 2 = suppress the left cells
|
|
sta magnifyEdgeMask ; 2EDF so the leftmost column is drawn half wide
|
|
L_2EE1:
|
|
jsr setMagnifyCellPointers ; 2EE1 set up the pointers for this cell
|
|
ldy #$00 ; 2EE4 read the source cell's colour byte
|
|
lda (blockPtr),y ; 2EE6 its colour byte
|
|
sta textColour ; 2EE8 colour for the 2x2 block
|
|
jsr plotGlyphDoubleSize ; 2EEA expand it
|
|
lda magnifyEdgeMask ; 2EED read the mask back
|
|
and #$03 ; 2EEF drop the left/right suppression
|
|
sta magnifyEdgeMask ; 2EF1 only the top/bottom bits survive into the next cell
|
|
inc magnifySrcCol ; 2EF3 next column to the right
|
|
lda magnifySrcCol ; 2EF5 the column just drawn
|
|
cmp scratch20 ; 2EF7 compare against the half width
|
|
bcc L_2EE1 ; 2EF9 until the middle of the row
|
|
lda frameWidth ; 2EFB right half of this row: outermost column ...
|
|
sta magnifySrcCol ; 2EFE start the right half at the outermost column
|
|
lda magnifyWidthOdd ; 2F00 odd width?
|
|
beq L_2F0B ; 2F03 no
|
|
lda magnifyEdgeMask ; 2F05 add to the mask that is already there
|
|
ora #$08 ; 2F07 bit 3 = suppress the right cells
|
|
sta magnifyEdgeMask ; 2F09 so the rightmost column is drawn half wide
|
|
L_2F0B:
|
|
jsr setMagnifyCellPointers ; 2F0B set up the pointers for this cell
|
|
ldy #$00 ; 2F0E read the source cell's colour byte
|
|
lda (blockPtr),y ; 2F10 its colour byte
|
|
sta textColour ; 2F12 colour for the 2x2 block
|
|
jsr plotGlyphDoubleSize ; 2F14 expand it
|
|
lda magnifyEdgeMask ; 2F17 read the mask back
|
|
and #$03 ; 2F19 drop the left/right suppression
|
|
sta magnifyEdgeMask ; 2F1B only the top/bottom bits survive into the next cell
|
|
dec magnifySrcCol ; 2F1D next column to the left
|
|
lda magnifySrcCol ; 2F1F the column just drawn
|
|
cmp scratch20 ; 2F21 compare against the half width
|
|
bcs L_2F0B ; 2F23 while still right of the middle
|
|
lda magnifyEdgeMask ; 2F25 read the mask back
|
|
and #$0C ; 2F27 row finished - this also leaves magnifyEdgeMask at 0 on the final pass
|
|
sta magnifyEdgeMask ; 2F29 only the left/right bits survive into the next row
|
|
dec magnifySrcRow ; 2F2B next row up
|
|
lda magnifySrcRow ; 2F2D the row just finished
|
|
cmp scratch21 ; 2F2F compare against the half height
|
|
bcs L_2ED2 ; 2F31 keep going while still below the middle row
|
|
rts ; 2F33 the whole rectangle has been redrawn at double size
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setMagnifyCellPointers - Helper of magnifyWindow2x. Computes the pointers of the source cell
|
|
; (windowOriginRow + magnifySrcRow, windowOriginCol + magnifySrcCol), copies them into glyphPtr (so
|
|
; the bitmap cell is used as the 'glyph' to enlarge) and blockPtr (so its screen-RAM colour byte can
|
|
; be read), then computes the destination cell (zp_40 + 2*row, zp_3F + 2*column) into
|
|
; bitmapPtr/screenPtr.
|
|
; In: magnifySrcRow/magnifySrcCol (zp_30/31), windowOriginRow/Col, textCursorRow/Col (zp_40/zp_3F)
|
|
; Out: glyphPtr (zp_B2/B3) = source bitmap cell, blockPtr (zp_B8/B9) = source screen cell,
|
|
; bitmapPtr/screenPtr (zp_B4-B7) = destination cell, scratch18/19 = destination row/column
|
|
; Called from: magnifyWindow2x ($2E72, $2E9C, $2EE1, $2F0B)
|
|
; ----------------------------------------------------------------------
|
|
setMagnifyCellPointers:
|
|
clc ; 2F34 clear the carry for the row add
|
|
lda magnifySrcRow ; 2F35 row offset within the rectangle ...
|
|
adc windowOriginRow ; 2F37 ... plus the rectangle's top row
|
|
sta scratch18 ; 2F3A absolute source row
|
|
clc ; 2F3C clear the carry for the column add
|
|
lda magnifySrcCol ; 2F3D column offset within the rectangle ...
|
|
adc windowOriginCol ; 2F3F ... plus the rectangle's left column
|
|
sta scratch19 ; 2F42 absolute source column
|
|
jsr calcCellPointers ; 2F44 zp_B4-zp_B7 now point at the source cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy both source pointers aside before they are recomputed for the destination. LDA zp,Y does not
|
|
; exist on the 6502, so these are absolute accesses into page zero.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$01 ; 2F47 two bytes each, high byte first
|
|
L_2F49:
|
|
lda a:bitmapPtr,y ; 2F49 bitmapPtr -> ...
|
|
sta a:glyphPtr,y ; 2F4C ... glyphPtr: plotGlyphDoubleSize reads the source cell as if it were a font glyph
|
|
lda a:screenPtr,y ; 2F4F screenPtr -> ...
|
|
sta a:blockPtr,y ; 2F52 ... blockPtr: the caller reads the source cell's colour byte through it
|
|
dey ; 2F55 high byte then low byte
|
|
bpl L_2F49 ; 2F56 two bytes of each pointer copied
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Now the destination cell: the zoom doubles every offset.
|
|
; ----------------------------------------------------------------------
|
|
lda magnifySrcRow ; 2F58 row offset within the rectangle
|
|
asl a ; 2F5A row offset * 2
|
|
clc ; 2F5B clear the carry for the origin add
|
|
adc textCursorRow ; 2F5C plus the destination top row
|
|
sta scratch18 ; 2F5E destination row
|
|
lda magnifySrcCol ; 2F60 column offset within the rectangle
|
|
asl a ; 2F62 column offset * 2 (no CLC needed - the offset is always below $80, so ASL clears the carry)
|
|
adc textCursorCol ; 2F63 plus the destination left column
|
|
sta scratch19 ; 2F65 destination column
|
|
jmp calcCellPointers ; 2F67 leave zp_B4-zp_B7 pointing at the destination cell and return through it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initSpritePointers - Rewrites the eight sprite pointers in the last bytes of the video matrix,
|
|
; $8FF8-$8FFF, with $28..$2F. In VIC bank 2 a pointer value n means shape data at $8000 + n*64, so
|
|
; sprite 0 takes its shape from $8A00 and sprite 7 from $8BC0 - the 512-byte block clearAllSpriteData
|
|
; wipes. Also clears the sprite enable shadow. Needed after initBitmapScreen fills $8C00-$8FFF.
|
|
; In: none
|
|
; Out: spriteEnableShadow = 0, $8FF8-$8FFF = $28-$2F; mapCellPtr (zp_48/49) = $8FF8; A/X/Y clobbered
|
|
; Called from: initBitmapScreen ($2DB9) only
|
|
; ----------------------------------------------------------------------
|
|
initSpritePointers:
|
|
lda #$00 ; 2F6A no sprite is on screen while the display is rebuilt
|
|
sta spriteEnableShadow ; 2F6C no sprites visible yet
|
|
lda #$F8 ; 2F6F low byte of $8FF8 ...
|
|
sta mapCellPtr ; 2F71 ...
|
|
lda #$8F ; 2F73 ... high byte
|
|
sta mapCellPtrHi ; 2F75 zp_48/49 = $8FF8, the first sprite pointer
|
|
ldy #$07 ; 2F77 eight pointers, counted down
|
|
ldx #$2F ; 2F79 $2F = sprite 7's shape at $8000 + $2F*64 = $8BC0
|
|
L_2F7B:
|
|
txa ; 2F7B the pointer value for this sprite
|
|
sta (mapCellPtr),y ; 2F7C $8FF8+Y = the pointer of sprite Y
|
|
dex ; 2F7E next shape block down
|
|
dey ; 2F7F next sprite down
|
|
bpl L_2F7B ; 2F80 until sprite 0 gets $28 = $8A00
|
|
adc #$01 ; 2F82 leftover: A becomes $29/$2A and is never used by the only caller
|
|
rts ; 2F84 the eight sprite pointers are back in place
|
|
|
|
|
|
; The two bit-mask tables. spriteBitMasks is the set mask for bit n and is used both for sprite
|
|
; enable bits ($3065) and as the keyboard column mask ($0E16); invertedBitMasks is its complement,
|
|
; used to clear a sprite bit ($337D) and as the keyboard row mask ($0E08).
|
|
spriteBitMasks:
|
|
.byte $01,$02,$04,$08,$10,$20,$40,$80; 2F85 ..... @. set masks $01,$02,$04 ... $80 for bit / sprite / keyboard column n
|
|
invertedBitMasks:
|
|
.byte $FE ; 2F8D clear masks $FE,$FD,$FB ... $7F - the same table complemented (was D_2F8D)
|
|
.byte $FD,$FB,$F7,$EF,$DF,$BF,$7F; 2F8E .......
|
|
|
|
; Sprite shape addresses split into two tables so that $8A00 + n*64 can be formed with a single 16-bit
|
|
; add (see clearSpriteShape).
|
|
spriteOffsetLoTable:
|
|
.byte $00,$40,$80,$C0,$00,$40,$80,$C0; 2F95 .@...@.. low byte of n*64 for sprites 0-7
|
|
|
|
; spriteOffsetHiTable: byteTable, 8 bytes. High byte of n*64 for sprite 0-7 (0,0,0,0,1,1,1,1)
|
|
spriteOffsetHiTable:
|
|
.byte $00,$00,$00,$00,$01,$01,$01,$01; 2F9D ........ high byte of n*64: 0 for sprites 0-3, 1 for sprites 4-7
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearAllSpriteData - Zeroes $8A00-$8BFF, the 64-byte shape blocks of all eight sprites, in one
|
|
; 256-pass loop that writes two pages at a time.
|
|
; In: none
|
|
; Out: $8A00-$8BFF = 0; A = 0, X = 0
|
|
; Called from: the start-up block at $0386 (by JMP, but the routine ends in RTS)
|
|
; ----------------------------------------------------------------------
|
|
clearAllSpriteData:
|
|
ldx #$00 ; 2FA5 one pass over 256 offsets
|
|
txa ; 2FA7 the fill value is 0
|
|
L_2FA8:
|
|
sta sprite0Shape,x ; 2FA8 sprites 0-3 ($8A00-$8AFF)
|
|
sta sprite4Shape,x ; 2FAB sprites 4-7 ($8B00-$8BFF)
|
|
inx ; 2FAE next offset
|
|
bne L_2FA8 ; 2FAF 256 offsets, both pages at once
|
|
rts ; 2FB1 all eight sprite shapes are blank
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearSpriteShape - Zeroes the 64-byte shape of sprite A at $8A00 + A*64. The address is built from
|
|
; the two half tables and patched into the operand of the STA abs,Y at $2FC8, which in the raw image
|
|
; still holds its unpatched $FFFF operand (hence the 'sta irqVectorHi,y' the disassembler shows).
|
|
; In: A = sprite number 0-7
|
|
; Out: that sprite's 64 shape bytes are 0; the operand at $2FC9/$2FCA is patched; A = 0, Y = $FF
|
|
; Called from: $2245, $224A, setCursorShapeSquare ($2FDB), setCursorShapeBlock ($2FFD), $3385, $346A,
|
|
; $3CFF and the jump at $3D53, plus the overlay screens ($7184, $76A1, $7781, $8241)
|
|
; ----------------------------------------------------------------------
|
|
clearSpriteShape:
|
|
tay ; 2FB2 index the two half tables with the sprite number
|
|
clc ; 2FB3 clear the carry for the 16-bit add
|
|
lda #$00 ; 2FB4 the low half of the base address $8A00 is zero
|
|
adc spriteOffsetLoTable,y ; 2FB6 low byte of sprite*64
|
|
sta L_2FC8+1 ; 2FB9 patch the low operand byte of the STA at $2FC8
|
|
lda #$8A ; 2FBC high byte of the shape area base $8A00 ...
|
|
adc spriteOffsetHiTable,y ; 2FBE ... plus the high byte of sprite*64
|
|
sta L_2FC8+2 ; 2FC1 patch the high operand byte
|
|
ldy #$3F ; 2FC4 64 bytes per sprite shape
|
|
lda #$00 ; 2FC6 the fill value
|
|
L_2FC8:
|
|
sta irqVectorHi,y ; 2FC8 operand patched at $2FB9/$2FC1 - really STA $8A00+sprite*64,Y
|
|
dey ; 2FCB work down through the 64 bytes
|
|
bpl L_2FC8 ; 2FCC all 64 bytes cleared
|
|
rts ; 2FCE the sprite is blank and ready to be filled
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setCursorShapeSquare - Gives sprite 0, the map cursor, the 18-byte (6 rows of 24 pixels) hollow
|
|
; square at $9E6E, sets its colour shadow to 0 and enables it. The sprite is Y-expanded elsewhere so
|
|
; the six rows cover a whole 12-pixel map cell.
|
|
; In: none
|
|
; Out: sprite 0's shape at $8A00, spriteColourShadow ($9008) = 0, sprite 0 enabled
|
|
; Called from: $2015, $4C53, $5E40, $5E87, $61F5, $6393 - whenever a unit is picked up or ordered
|
|
; ----------------------------------------------------------------------
|
|
setCursorShapeSquare:
|
|
lda #$6E ; 2FCF low byte of the hollow-square shape at $9E6E
|
|
ldy #$9E ; 2FD1 high byte
|
|
|
|
; ----------------------------------------------------------------------
|
|
; installCursorShape - Shared tail of setCursorShapeSquare and setCursorShapeCross: copies the 18-byte
|
|
; shape whose address is in A/Y into sprite 0, after blanking the rest of the sprite, and enables it
|
|
; in black. The source address is planted in the operand of the LDA abs,Y at $2FE0.
|
|
; In: A/Y = lo/hi address of an 18-byte (6 row) sprite shape
|
|
; Out: sprite 0's shape rewritten, spriteColourShadow = 0, spriteEnableShadow bit 0 set; the operand
|
|
; at
|
|
; $2FE1/$2FE2 patched
|
|
; Called from: setCursorShapeSquare by fall-through and setCursorShapeCross ($2FF7 by JMP)
|
|
; ----------------------------------------------------------------------
|
|
installCursorShape:
|
|
sta L_2FE0+1 ; 2FD3 patch the low operand byte of the LDA at $2FE0
|
|
sty L_2FE0+2 ; 2FD6 and its high operand byte
|
|
lda #$00 ; 2FD9 sprite 0 = the map cursor
|
|
jsr clearSpriteShape ; 2FDB blank all 64 bytes so the previous shape cannot show through
|
|
ldy #$11 ; 2FDE 18 bytes = 6 rows of 3 bytes
|
|
L_2FE0:
|
|
lda irqVectorHi,y ; 2FE0 operand patched at $2FD3/$2FD6 - really LDA $9E6E,Y or $9E80,Y
|
|
sta sprite0Shape,y ; 2FE3 sprite 0's shape data at $8A00
|
|
dey ; 2FE6 work down through the 18 bytes
|
|
bpl L_2FE0 ; 2FE7 18 bytes copied
|
|
lda #$00 ; 2FE9 colour 0 = black; shadow values below $10 are steady, $10 and above flash between two nibbles
|
|
sta spriteColourShadow ; 2FEB sprite 0's colour shadow, applied to $D027 by the raster IRQ
|
|
lda #$01 ; 2FEE sprite 0's enable bit
|
|
jmp enableSprites ; 2FF0 OR it into the enable shadow and return through it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setCursorShapeCross - The same as setCursorShapeSquare but installs the thick plus/cross shape at
|
|
; $9E80 instead. Used for the ranged attack cursor, when cursorTargetMode (zp_BB) is positive.
|
|
; In: none
|
|
; Out: sprite 0 = the cross shape, colour 0, enabled
|
|
; Called from: the cursor selector at $2195
|
|
; ----------------------------------------------------------------------
|
|
setCursorShapeCross:
|
|
lda #$80 ; 2FF3 low byte of the cross shape at $9E80
|
|
ldy #$9E ; 2FF5 high byte
|
|
jmp installCursorShape ; 2FF7 share the copy/enable tail
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setCursorShapeBlock - Loads the small 4x4 solid block at $9E92 into the sprite named by
|
|
; activeSpriteIndex ($91CC) and sets that sprite's colour shadow to $0C (grey) - except for sprite 0,
|
|
; which keeps whatever colour the cursor code gave it. Note the shape address is formed with six
|
|
; ASLs, so it only reaches sprites 0-3; both callers pass 0 or 1.
|
|
; In: activeSpriteIndex ($91CC) = sprite number (0 or 1 in practice)
|
|
; Out: that sprite's 64-byte shape holds the block; spriteColourShadow[n] = $0C for n other than 0
|
|
; Called from: the cursor selector at $21A8 (plain battlefield cursor) and $2550 (sprite 1 when no
|
|
; group
|
|
; box is up)
|
|
; ----------------------------------------------------------------------
|
|
setCursorShapeBlock:
|
|
lda activeSpriteIndex ; 2FFA which sprite is being dressed
|
|
jsr clearSpriteShape ; 2FFD blank its 64 shape bytes first
|
|
lda activeSpriteIndex ; 3000 the sprite number again
|
|
asl a ; 3003 sprite number * 2 ...
|
|
asl a ; 3004 ... * 4 ...
|
|
asl a ; 3005 ... * 8 ...
|
|
asl a ; 3006 ... * 16 ...
|
|
asl a ; 3007 ... * 32 ...
|
|
asl a ; 3008 ... * 64 = its offset inside the shape block (wraps above sprite 3)
|
|
tax ; 3009 X = the byte offset of this sprite's shape inside $8A00
|
|
ldy #$00 ; 300A start at the first byte of the shape
|
|
L_300C:
|
|
lda titleScreenRow16Col18,y ; 300C the 18-byte solid block shape at $9E92 (also graphicsData9300:cursorBlockShape)
|
|
sta sprite0Shape,x ; 300F into $8A00 + sprite*64
|
|
inx ; 3012 next destination byte
|
|
iny ; 3013 next source byte
|
|
cpy #$12 ; 3014 18 bytes = 6 rows of 3
|
|
bcc L_300C ; 3016 18 bytes = 6 rows of 3
|
|
lda #$0C ; 3018 colour $0C = medium grey
|
|
ldx activeSpriteIndex ; 301A which sprite was dressed?
|
|
beq L_3022 ; 301D sprite 0 keeps the cursor colour set by installCursorShape
|
|
sta spriteColourShadow,x ; 301F any other sprite is drawn grey
|
|
L_3022:
|
|
rts ; 3022 done - the cursor sprite is dressed
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mapCellToSpriteXY - Converts a battlefield cell (X = column 0-39, Y = row 0-39) into the sprite
|
|
; coordinates that put a marker on that cell of the overview (mini) map in the left panel. While a
|
|
; group formation box is being dragged (groupBoxActive $920A bit 7 set) the cell is first shifted by
|
|
; the box's top-left offsets, so the formation ghost sprite is anchored at the corner of the box
|
|
; instead of at the cursor cell, and the result is nudged one pixel right and down to line up with the
|
|
; 4x4 pixel blocks the overview map is drawn with.
|
|
; In: X = map column, Y = map row, groupBoxActive $920A, groupBoxLeftOffset $922F, groupBoxTopOffset
|
|
; $9231
|
|
; Out: X = sprite X coordinate, Y = sprite Y coordinate; A clobbered
|
|
; Called from: positionMarkerSprite ($2585), positionCursorSprite ($3055), the unit-follow code
|
|
; ($483F)
|
|
; ----------------------------------------------------------------------
|
|
mapCellToSpriteXY:
|
|
lda groupBoxActive ; 3023 $920A = $FF while a group formation box is being dragged around the map
|
|
bpl mapCellToSpriteXYRaw ; 3026 branch if no group box is active: convert the cell as it stands
|
|
txa ; 3028 group box: start from the requested map column
|
|
clc ; 3029 clear carry for the offset addition
|
|
adc groupBoxLeftOffset ; 302A $922F = column offset of the box's left edge from the cursor cell (may be negative)
|
|
tax ; 302D X = column of the top-left cell of the box
|
|
tya ; 302E same for the row
|
|
clc ; 302F clear carry for the row offset addition
|
|
adc groupBoxTopOffset ; 3030 $9231 = row offset of the box's top edge from the cursor cell
|
|
tay ; 3033 Y = row of the top-left cell of the box
|
|
jsr mapCellToSpriteXYRaw ; 3034 convert that corner cell to overview-map sprite coordinates
|
|
inx ; 3037 +1 pixel in X: the 4x4 cell blocks are drawn one pixel into their cell
|
|
iny ; 3038 +1 pixel in Y, likewise
|
|
rts ; 3039 return with the shifted sprite coordinates
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mapCellToSpriteXYRaw - The plain cell-to-sprite conversion for the overview map: spriteX = column*4
|
|
; + $1F, spriteY = row*4 + $39. The whole 40x40 battlefield is drawn in the left panel at 4x4 pixels
|
|
; per cell starting at screen pixel (7,7); $1F and $39 are that origin plus the VIC sprite coordinate
|
|
; offsets ($18 horizontally, $32 vertically).
|
|
; In: X = map column 0-39, Y = map row 0-39
|
|
; Out: X = sprite X coordinate, Y = sprite Y coordinate; A clobbered
|
|
; Called from: mapCellToSpriteXY ($3026/$3034), showExplosionMarker ($33E5), the shot-line setup
|
|
; ($3A70, $3A7B)
|
|
; ----------------------------------------------------------------------
|
|
mapCellToSpriteXYRaw:
|
|
txa ; 303A map column 0-39
|
|
asl a ; 303B column*2
|
|
asl a ; 303C column*4 - the overview map uses 4 pixels per cell
|
|
clc ; 303D clear carry before adding the map origin
|
|
adc #$1F ; 303E sprite X of overview column 0 = 24 (sprite origin) + 7 (left edge of the map)
|
|
tax ; 3040 return the sprite X in X
|
|
tya ; 3041 map row 0-39
|
|
asl a ; 3042 row*2
|
|
asl a ; 3043 row*4
|
|
clc ; 3044 clear carry before adding the map origin
|
|
adc #$39 ; 3045 sprite Y of overview row 0 = 50 (sprite origin) + 7 (top edge of the map)
|
|
tay ; 3047 return the sprite Y in Y
|
|
rts ; 3048 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; positionCursorSprite - Puts the sprite named by activeSpriteIndex ($91CC) over the cursor cell of
|
|
; the overview map. The cursor cell is always the middle of the 7x5 tactical view, i.e. (viewOriginCol
|
|
; + 3, viewOriginRow + 2). Nothing happens when activeSpriteIndex is negative (no cursor sprite).
|
|
; In: viewOriginCol zp_A5, viewOriginRow zp_A6, activeSpriteIndex $91CC, group-box state
|
|
; Out: spriteXShadow/spriteYShadow of that sprite, spriteEnableShadow bit set; A/X/Y clobbered
|
|
; Called from: the viewport scroller ($1D46), battlefield entry ($2012), buildGroupFormationSprite
|
|
; ($31A0, by JMP) and the map generator overlay ($83F2)
|
|
; ----------------------------------------------------------------------
|
|
positionCursorSprite:
|
|
lda viewOriginCol ; 3049 map column of the left edge of the 7x5 tactical view
|
|
clc ; 304B clear carry before stepping to the cursor column
|
|
adc #$03 ; 304C +3 = the middle column of the seven, where the cursor sits
|
|
tax ; 304E X = cursor column
|
|
lda viewOriginRow ; 304F map row of the top edge of the view
|
|
clc ; 3051 clear carry before stepping to the cursor row
|
|
adc #$02 ; 3052 +2 = the middle row of the five
|
|
tay ; 3054 Y = cursor row
|
|
jsr mapCellToSpriteXY ; 3055 cell -> overview sprite coordinates (shifted when a group box is active)
|
|
txa ; 3058 keep the sprite X byte
|
|
ldx activeSpriteIndex ; 3059 $91CC = which sprite is currently acting as the cursor
|
|
bmi L_306B ; 305C negative = no cursor sprite wanted, leave everything alone
|
|
sta spriteXShadow,x ; 305E $9010+n: shadow of the VIC sprite X register, copied out by the raster IRQ
|
|
tya ; 3061 sprite Y
|
|
sta spriteYShadow,x ; 3062 $9018+n: shadow of the VIC sprite Y register
|
|
lda spriteBitMasks,x ; 3065 $2F85[n] = 1 << n, the enable bit of this sprite
|
|
jsr enableSprites ; 3068 turn the sprite on (unless sprites are suppressed)
|
|
L_306B:
|
|
rts ; 306B done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawLineInPathSprite - Draws a straight line into the shape of sprite 3 ($8AC0), which is the game's
|
|
; path/shot-line sprite. It uses the resident high-memory Bresenham stepper ($FBB8 init, $FBFA step),
|
|
; but here its zp_92-zp_95 coordinates are pixel positions inside the 24x21 sprite, not map cells. A
|
|
; negative argument makes the walker skip two pixels after every plotted one, giving a dotted line
|
|
; (used for a BOOMER's arcing shot).
|
|
; In: A = 0 solid / $80 dotted; lineCurRow/Col zp_92/zp_93 = start pixel, lineEndRow/Col zp_94/zp_95
|
|
; = end pixel
|
|
; Out: pixels set in $8AC0-$8AFF; the line-walker state is consumed, zp_92/zp_93 left at the end point
|
|
; Called from: the shot-line builder ($3B11) and the drone heading indicator in overlay B ($8266)
|
|
; ----------------------------------------------------------------------
|
|
drawLineInPathSprite:
|
|
sta L_307C+1 ; 306C patch the LDA # at $307C with the dot flag: 0 = solid, $80 = dotted
|
|
jsr initLineStepper ; 306F $FBB8: set up the Bresenham walk from (zp_93,zp_92) to (zp_95,zp_94)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the line one pixel at a time, plotting into sprite 3.
|
|
; ----------------------------------------------------------------------
|
|
L_3072:
|
|
jsr plotPathSpritePixel ; 3072 set the pixel the walker is standing on
|
|
lda lineStepsLeft ; 3075 steps still to be taken
|
|
beq L_3089 ; 3077 branch when the end point has been reached
|
|
jsr stepLine ; 3079 $FBFA: advance the walker one pixel
|
|
L_307C:
|
|
lda #$FF ; 307C dot flag patched at $306C
|
|
bpl L_3072 ; 307E solid line: plot every pixel
|
|
jsr stepLine ; 3080 dotted line: skip this pixel...
|
|
jsr stepLine ; 3083 ...and the next one, so only every third pixel is drawn
|
|
jmp L_3072 ; 3086 plot the pixel we landed on
|
|
|
|
L_3089:
|
|
rts ; 3089 line finished
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; plotPathCross - Marks the end of a path/shot line by plotting the four orthogonal neighbours of the
|
|
; walker's current pixel into sprite 3: left, up, down and (by falling through into
|
|
; plotPathSpritePixel) right. Together with the line's own last pixel this makes a small cross over
|
|
; the target.
|
|
; In: lineCurRow zp_92, lineCurCol zp_93 = the pixel to mark
|
|
; Out: 4 more pixels set in $8AC0; zp_93 left one greater than on entry
|
|
; Called from: the shot-line builder ($3B14), immediately after drawLineInPathSprite
|
|
; ----------------------------------------------------------------------
|
|
plotPathCross:
|
|
dec lineCurCol ; 308A one pixel to the left of the end point
|
|
jsr plotPathSpritePixel ; 308C plot it
|
|
inc lineCurCol ; 308F back to the end column
|
|
dec lineCurRow ; 3091 one pixel above the end point
|
|
jsr plotPathSpritePixel ; 3093 plot it
|
|
inc lineCurRow ; 3096 back to the end row
|
|
inc lineCurRow ; 3098 one pixel below the end point
|
|
jsr plotPathSpritePixel ; 309A plot it
|
|
dec lineCurRow ; 309D back to the end row
|
|
inc lineCurCol ; 309F one pixel to the right - fall through and plot that one too
|
|
|
|
; ----------------------------------------------------------------------
|
|
; plotPathSpritePixel - Sets one pixel in the shape of sprite 3 at $8AC0. A sprite row is 3 bytes, so
|
|
; the byte index is row*3 + column/8 and the bit is taken from the $80>>n mask table at $C008. The
|
|
; column/8 part is patched into the ADC # at $30B3 rather than added with a register.
|
|
; In: lineCurRow zp_92 = pixel row 0-20, lineCurCol zp_93 = pixel column 0-23
|
|
; Out: one bit set in $8AC0-$8AFF; A/X/Y clobbered, the operand at $30B4 patched
|
|
; Called from: drawLineInPathSprite ($3072), plotPathCross ($308C/$3093/$309A and by fall-through),
|
|
; and directly by overlay B ($7852, $785E)
|
|
; ----------------------------------------------------------------------
|
|
plotPathSpritePixel:
|
|
lda lineCurCol ; 30A1 pixel column 0-23 inside the sprite
|
|
lsr a ; 30A3 column/2
|
|
lsr a ; 30A4 column/4
|
|
lsr a ; 30A5 column/8 = which of the three bytes of a sprite row holds the pixel
|
|
sta L_30B3+1 ; 30A6 patch the ADC # at $30B3 with that byte offset
|
|
lda lineCurCol ; 30A9 the column again
|
|
and #$07 ; 30AB bit number 0-7 inside the byte
|
|
tax ; 30AD index into the bit mask table
|
|
lda lineCurRow ; 30AE pixel row 0-20
|
|
asl a ; 30B0 row*2
|
|
adc lineCurRow ; 30B1 row*3 = offset of the start of that sprite row (3 bytes per row)
|
|
L_30B3:
|
|
adc #$FF ; 30B3 + byte within the row, patched at $30A6
|
|
tay ; 30B5 Y = byte offset inside the 63-byte shape
|
|
lda sprite3Shape,y ; 30B6 $8AC0 = shape of sprite 3, the path/shot line sprite
|
|
ora bootReadSectorLoop,x ; 30B9 $C008[bit] = $80 >> bit (also textEngineC000:bitMaskTable_C008)
|
|
sta sprite3Shape,y ; 30BC set the pixel
|
|
rts ; 30BF done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildGroupFormationSprite - Builds the 'formation ghost' shown while a whole group is being ordered
|
|
; to a new place: every living member of the selected group is drawn as one 2x2-pixel dot into sprite
|
|
; 0, laid out exactly as the group's ordered destinations are laid out on the map. Because sprite 0 is
|
|
; X- and Y-expanded, one dot covers the 4x4 pixels the overview map gives a cell. The bounding box of
|
|
; the destinations is turned into offsets relative to the dragged unit, so the ghost follows the
|
|
; cursor, groupBoxActive is set, the view is scrolled until the box fits on the map and the ghost is
|
|
; positioned.
|
|
; In: selectedGroupKey $9206, selectedUnit $90F8, unit arrays $F76C/$FB54/$F7D0/$F834
|
|
; Out: sprite 0 shape, spriteBuildBuffer $9025, groupBox offsets $922E-$9231, groupBoxActive $920A =
|
|
; $FF,
|
|
; spritePlotMode $91F3 = $40, X/Y expand shadows bits 0-1 set, view origin possibly moved
|
|
; Called from: runTargetCursorLoop ($21A2), when the group being ordered has 2 or more members
|
|
; ----------------------------------------------------------------------
|
|
buildGroupFormationSprite:
|
|
lda selectedGroupKey ; 30C0 $9206 = group id bits of the group being ordered
|
|
ldy #$80 ; 30C3 bit 7 = measure the members' ordered destinations, not their current cells
|
|
jsr computeGroupBounds ; 30C5 $4B5E -> zp_18/zp_19 = min column/row, zp_1B/zp_1C = max column/row
|
|
ldy #$3F ; 30C8 clear the 64-byte shape build buffer
|
|
lda #$00 ; 30CA with zeroes
|
|
L_30CC:
|
|
sta spriteBuildBuffer,y ; 30CC $9025 = scratch sprite shape used while the ghost is assembled
|
|
dey ; 30CF next byte down
|
|
bpl L_30CC ; 30D0 loop over all 64 bytes
|
|
lda #$40 ; 30D2 bit 6 = 'set the pixel'
|
|
sta spritePlotMode ; 30D4 $91F3 tells plotSpritePixel which operation to use
|
|
lda scratch1B ; 30D7 max column of the group's destinations
|
|
sta groupBoxRightOffset ; 30D9 $922E = right edge of the group box
|
|
lda scratch18 ; 30DC min column
|
|
sta groupBoxLeftOffset ; 30DE $922F = left edge of the group box
|
|
lda scratch1C ; 30E1 max row
|
|
sta groupBoxBottomOffset ; 30E3 $9230 = bottom edge of the group box
|
|
lda scratch19 ; 30E6 min row
|
|
sta groupBoxTopOffset ; 30E8 $9231 = top edge of the group box
|
|
lda selectedGroupKey ; 30EB group id bits again
|
|
ora #$40 ; 30EE bit 6 = 'this unit is in a group'
|
|
sta scratch1B ; 30F0 the value unitDisplayFlags bits 2-6 must have for a member of this group
|
|
ldx #$63 ; 30F2 start at unit 99, the last unit record
|
|
stx scratch1A ; 30F4 keep the loop counter in memory (X is needed for the pixel plotter)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk units 99..0 and plot a 2x2 dot for every living member of the group at its ordered destination,
|
|
; relative to the top-left corner of the group box.
|
|
; ----------------------------------------------------------------------
|
|
L_30F6:
|
|
lda unitTypeTable,x ; 30F6 $F76C[unit] = type byte, bit 7 set = destroyed
|
|
bmi L_3142 ; 30F9 skip dead units
|
|
lda unitDisplayFlagsTable,x ; 30FB $FB54[unit] = local flags copy
|
|
and #$7C ; 30FE keep the group id (bits 2-4) and the in-a-group bit (6)
|
|
cmp scratch1B ; 3100 does it belong to the group being ordered?
|
|
bne L_3142 ; 3102 no - try the next unit
|
|
lda unitDestRowTable,x ; 3104 $F834[unit] = ordered destination row, bit 7 = already standing there
|
|
and #$7F ; 3107 drop that flag, keep the row
|
|
sec ; 3109 set carry for the subtraction
|
|
sbc groupBoxTopOffset ; 310A row inside the group box
|
|
asl a ; 310D x2: two sprite pixels per map cell (the sprite is Y-expanded)
|
|
tay ; 310E Y = pixel row of this member's dot
|
|
sty scratch19 ; 310F stash it, plotSpritePixel needs Y for each of the four pixels
|
|
lda unitDestColTable,x ; 3111 $F7D0[unit] = ordered destination column
|
|
and #$7F ; 3114 drop the 'already there' flag
|
|
sec ; 3116 set carry for the subtraction
|
|
sbc groupBoxLeftOffset ; 3117 column inside the group box
|
|
asl a ; 311A x2: two sprite pixels per map cell (the sprite is X-expanded)
|
|
tax ; 311B X = pixel column of this member's dot
|
|
stx scratch18 ; 311C stash it too
|
|
lda #$FF ; 311E bit 7 set = plot into spriteBuildBuffer $9025 instead of a real sprite
|
|
jsr setSpritePixel ; 3120 top-left pixel of the 2x2 dot
|
|
ldx scratch18 ; 3123 pixel column again
|
|
inx ; 3125 one pixel to the right
|
|
ldy scratch19 ; 3126 same pixel row
|
|
lda #$FF ; 3128 again into the build buffer
|
|
jsr setSpritePixel ; 312A top-right pixel
|
|
ldx scratch18 ; 312D pixel column
|
|
ldy scratch19 ; 312F pixel row
|
|
iny ; 3131 one pixel down
|
|
lda #$FF ; 3132 build buffer
|
|
jsr setSpritePixel ; 3134 bottom-left pixel
|
|
ldx scratch18 ; 3137 pixel column
|
|
inx ; 3139 one right
|
|
ldy scratch19 ; 313A pixel row
|
|
iny ; 313C one down
|
|
lda #$FF ; 313D build buffer
|
|
jsr setSpritePixel ; 313F bottom-right pixel - the dot is complete
|
|
L_3142:
|
|
dec scratch1A ; 3142 step to the next unit
|
|
ldx scratch1A ; 3144 reload the loop counter
|
|
bpl L_30F6 ; 3146 keep going until unit 0 has been handled
|
|
ldy #$3F ; 3148 64 bytes to copy
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Publish the finished shape and set up the sprite geometry.
|
|
; ----------------------------------------------------------------------
|
|
L_314A:
|
|
lda spriteBuildBuffer,y ; 314A the assembled ghost
|
|
sta sprite0Shape,y ; 314D $8A00 = shape of sprite 0
|
|
dey ; 3150 next byte down
|
|
bpl L_314A ; 3151 loop
|
|
lda spriteXExpandShadow ; 3153 $9023 = shadow of VIC_SPR_EXP_X
|
|
ora #$03 ; 3156 double the width of sprites 0 and 1
|
|
sta spriteXExpandShadow ; 3158 store the shadow back
|
|
lda spriteYExpandShadow ; 315B $9024 = shadow of VIC_SPR_EXP_Y
|
|
ora #$03 ; 315E double the height too, so one 2x2 dot covers a whole 4x4 overview cell
|
|
sta spriteYExpandShadow ; 3160 store the shadow back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Turn the absolute box corners into offsets from the dragged unit's destination so that
|
|
; mapCellToSpriteXY can keep the box under the cursor while the player scrolls.
|
|
; ----------------------------------------------------------------------
|
|
ldy selectedUnit ; 3163 $90F8 = the unit the player is actually dragging
|
|
lda unitDestColTable,y ; 3166 its ordered destination column
|
|
and #$7F ; 3169 without the 'already there' flag
|
|
sta scratch18 ; 316B keep it
|
|
lda unitDestRowTable,y ; 316D its ordered destination row
|
|
and #$7F ; 3170 without the flag
|
|
sta scratch19 ; 3172 keep it
|
|
lda groupBoxLeftOffset ; 3174 left edge of the box
|
|
sec ; 3177 set carry for the subtraction
|
|
sbc scratch18 ; 3178 minus the dragged unit's column
|
|
sta groupBoxLeftOffset ; 317A $922F is now an offset relative to the cursor
|
|
lda groupBoxRightOffset ; 317D right edge
|
|
sec ; 3180 set carry for the subtraction
|
|
sbc scratch18 ; 3181 minus the dragged unit's column
|
|
sta groupBoxRightOffset ; 3183 $922E likewise
|
|
lda groupBoxTopOffset ; 3186 top edge
|
|
sec ; 3189 set carry for the subtraction
|
|
sbc scratch19 ; 318A minus the dragged unit's row
|
|
sta groupBoxTopOffset ; 318C $9231 likewise
|
|
lda groupBoxBottomOffset ; 318F bottom edge
|
|
sec ; 3192 set carry for the subtraction
|
|
sbc scratch19 ; 3193 minus the dragged unit's row
|
|
sta groupBoxBottomOffset ; 3195 $9230 likewise
|
|
lda #$FF ; 3198 flag value
|
|
sta groupBoxActive ; 319A $920A: from now on mapCellToSpriteXY applies the box offsets
|
|
jsr keepGroupBoxInsideMap ; 319D $1F4E: scroll the view until the whole box fits inside the 40x40 map
|
|
jmp positionCursorSprite ; 31A0 place the ghost over the cursor cell and switch it on, then return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; enableSprites - ORs the sprite mask in A into spriteEnableShadow ($9022), the shadow the raster IRQ
|
|
; copies into VIC_SPR_ENA. The store is skipped while comcenStunStatus ($922A) bit 6 marks the comcen
|
|
; knock-out blackout round, so no sprite can reappear during it; A still returns the combined mask.
|
|
; In: A = sprite bit mask (1 << sprite number)
|
|
; Out: spriteEnableShadow updated unless suppressed; A = old shadow OR the mask
|
|
; Called from: everywhere a cursor, marker or ghost is switched on - $2539, $2566, $2FF0, $3068,
|
|
; $333D, $3380, $33AE, $3402, $344A, $3B19, $3D04, $484A, the $C000 overlay and both $6F00
|
|
; overlays
|
|
; ----------------------------------------------------------------------
|
|
enableSprites:
|
|
ora spriteEnableShadow ; 31A3 $9022 = shadow of VIC_SPR_ENA, one bit per sprite
|
|
bit comcenStunStatus ; 31A6 $922A = comcen stun/knock-out status
|
|
bvs L_31AE ; 31A9 bit 6 set = knock-out blackout round: keep every sprite off
|
|
sta spriteEnableShadow ; 31AB the raster IRQ writes this to $D015 next frame
|
|
L_31AE:
|
|
rts ; 31AE A holds the combined mask either way
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; plotSpritePixel - Sets, clears or toggles one pixel of a sprite shape. The operation is chosen by
|
|
; spritePlotMode ($91F3): bit 7 = toggle (EOR), bit 6 = set (ORA), neither = clear (AND with the
|
|
; inverted mask). It works by patching the opcode and the mask of the read-modify-write instruction at
|
|
; $3232. A negative sprite number selects the 64-byte build buffer at $9025 instead of the real sprite
|
|
; data at $8A00 + n*64.
|
|
; In: A = sprite number 0-7 (bit 7 set = use $9025), X = pixel column 0-23, Y = pixel row 0-20,
|
|
; spritePlotMode $91F3
|
|
; Out: one pixel modified; spritePlotPtr zp_98/zp_99 = byte pointer, spritePlotWork clobbered;
|
|
; self-modifies $31EE (sprite number), $3232 (opcode) and $3233 (mask)
|
|
; Called from: plotCircleOctantPoints ($32CE) and, through setSpritePixel, buildGroupFormationSprite
|
|
; ----------------------------------------------------------------------
|
|
plotSpritePixel:
|
|
bit spritePlotMode ; 31AF $91F3 selects the operation
|
|
bmi L_31C0 ; 31B2 bit 7 set = toggle the pixel
|
|
bvs setSpritePixel ; 31B4 bit 6 set = set the pixel
|
|
sta L_31ED+1 ; 31B6 clear mode: stash the sprite number in the LDX # operand at $31EE
|
|
lda #$29 ; 31B9 opcode of AND #imm
|
|
sta spritePlotOperation ; 31BB patch the read-modify-write instruction at $3232
|
|
bne L_31D2 ; 31BE always taken ($29 is never zero)
|
|
L_31C0:
|
|
sta L_31ED+1 ; 31C0 toggle mode: stash the sprite number
|
|
lda #$49 ; 31C3 opcode of EOR #imm
|
|
sta spritePlotOperation ; 31C5 patch the operation
|
|
bne L_31D2 ; 31C8 always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setSpritePixel - Entry point of plotSpritePixel that forces the ORA (set) operation. Note that the
|
|
; mask is still inverted further down unless spritePlotMode has bit 6 or 7 set, so callers set
|
|
; spritePlotMode = $40 first; buildGroupFormationSprite does exactly that.
|
|
; In: A = sprite number (bit 7 = build buffer), X = pixel column, Y = pixel row
|
|
; Out: the pixel is set
|
|
; Called from: buildGroupFormationSprite ($3120, $312A, $3134, $313F) and by branch from $31B4
|
|
; ----------------------------------------------------------------------
|
|
setSpritePixel:
|
|
sta L_31ED+1 ; 31CA stash the sprite number in the LDX # operand at $31EE
|
|
lda #$09 ; 31CD opcode of ORA #imm
|
|
sta spritePlotOperation ; 31CF patch the operation at $3232
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shared body: build a pointer to the byte holding the pixel and the bit mask to apply.
|
|
; ----------------------------------------------------------------------
|
|
L_31D2:
|
|
stx spritePlotWork ; 31D2 keep the pixel column
|
|
lda L_31ED+1 ; 31D5 the sprite number stashed above
|
|
bpl L_31E5 ; 31D8 0-7 = a real sprite
|
|
lda #$25 ; 31DA negative: use the scratch shape buffer at $9025...
|
|
sta spritePlotPtr ; 31DC low byte of the pointer
|
|
lda #$90 ; 31DE high byte $90
|
|
sta spritePlotPtrHi ; 31E0 pointer = $9025
|
|
jmp L_31FE ; 31E2 ...which needs no per-sprite offset
|
|
|
|
L_31E5:
|
|
lda #$00 ; 31E5 real sprite: base of the sprite data block
|
|
sta spritePlotPtr ; 31E7 low byte
|
|
lda #$8A ; 31E9 high byte $8A
|
|
sta spritePlotPtrHi ; 31EB pointer = $8A00
|
|
L_31ED:
|
|
ldx #$FF ; 31ED sprite number, patched at $31B6/$31C0/$31CA
|
|
lda spritePlotPtr ; 31EF add the sprite's 64-byte offset
|
|
clc ; 31F1 clear carry for the 16-bit addition of the sprite offset
|
|
adc spriteOffsetLoTable,x ; 31F2 $2F95[n] = low byte of n*64
|
|
sta spritePlotPtr ; 31F5 store it back
|
|
lda spritePlotPtrHi ; 31F7 high byte of the pointer
|
|
adc spriteOffsetHiTable,x ; 31F9 $2F9D[n] = high byte of n*64
|
|
sta spritePlotPtrHi ; 31FC pointer now addresses the shape of sprite n
|
|
L_31FE:
|
|
lda spritePlotWork ; 31FE the pixel column
|
|
tax ; 3201 keep a copy in X
|
|
and #$07 ; 3202 bit number inside the byte
|
|
sta spritePlotWork ; 3204 save it for the mask lookup
|
|
txa ; 3207 column again
|
|
lsr a ; 3208 column/2
|
|
lsr a ; 3209 column/4
|
|
lsr a ; 320A column/8 = byte 0, 1 or 2 of a sprite row
|
|
clc ; 320B clear carry before folding the byte offset into the pointer
|
|
adc spritePlotPtr ; 320C fold it into the pointer
|
|
sta spritePlotPtr ; 320E low byte
|
|
bcc L_3214 ; 3210 no carry out
|
|
inc spritePlotPtrHi ; 3212 carry into the high byte
|
|
L_3214:
|
|
ldx spritePlotWork ; 3214 bit number
|
|
lda bootReadSectorLoop,x ; 3217 $C008[bit] = $80 >> bit (also textEngineC000:bitMaskTable_C008)
|
|
bit spritePlotMode ; 321A which operation was chosen?
|
|
bmi L_3223 ; 321D toggle: EOR with the plain mask
|
|
bvs L_3223 ; 321F set: ORA with the plain mask
|
|
eor #$FF ; 3221 clear: AND with the inverted mask
|
|
L_3223:
|
|
sta spritePlotOperation+1 ; 3223 patch the immediate operand of the operation at $3232
|
|
tya ; 3226 pixel row 0-20
|
|
sta spritePlotWork ; 3227 keep it
|
|
asl a ; 322A row*2
|
|
clc ; 322B clear carry for row*2 + row
|
|
adc spritePlotWork ; 322C row*3 = byte index of that sprite row (3 bytes per row)
|
|
tay ; 322F Y = index into the shape
|
|
lda (spritePlotPtr),y ; 3230 read the shape byte
|
|
spritePlotOperation:
|
|
ora #$FF ; 3232 patched: ORA/AND/EOR with the patched mask
|
|
sta (spritePlotPtr),y ; 3234 write the modified byte back
|
|
rts ; 3236 done
|
|
|
|
|
|
; Two work bytes living inside the code.
|
|
spritePlotWork:
|
|
.byte $00 ; 3237 . scratch used by plotSpritePixel: pixel column, then bit number, then pixel row
|
|
circlePointIndex:
|
|
.byte $00 ; 3238 which of the eight mirrored circle points plotCircleOctantPoints is plotting (7 down to 0)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawCircleInSprite - Draws a circle of radius X (1-10) centred on pixel (12,10) - the middle of a
|
|
; 24x21 sprite - into sprite A, using 8-way octant symmetry. The dx/dy point lists of each radius live
|
|
; in $9F69.. and are reached through the pointer tables at $9F37/$9F41 (dx) and $9F4B/$9F55 (dy); the
|
|
; index of the last point of the octant comes from $9F5F. spritePlotMode decides whether the pixels
|
|
; are set, cleared or toggled. Radius 1 degenerates into a filled 3x3 dot.
|
|
; In: A = sprite number (bit 7 = build buffer), X = radius 1-10, spritePlotMode $91F3
|
|
; Out: the circle is plotted; activeSpriteIndex $91CC = A, circleStepsLeft $91F2 = $FF,
|
|
; circleDxPtr/circleDyPtr zp_AD-zp_B0 point at the two lists
|
|
; Called from: drawRingsInSprite4 ($3471), the unit blob markers ($3D13, $3D43) and overlay B's
|
|
; range rings ($7190-$71DA)
|
|
; ----------------------------------------------------------------------
|
|
drawCircleInSprite:
|
|
sta activeSpriteIndex ; 3239 $91CC: plotCircleOctantPoints will plot into this sprite
|
|
dex ; 323C the tables are indexed by radius-1
|
|
lda titleScreenRow20Col23,x ; 323D $9F37[r-1] = low byte of the dx list (also graphicsData9300:circleDxPtrLoTable)
|
|
sta circleDxPtr ; 3240 zp_AD
|
|
lda titleScreenRow20Col33,x ; 3242 $9F41[r-1] = high byte (always $9F) (also graphicsData9300:circleDxPtrHiTable)
|
|
sta circleDxPtrHi ; 3245 zp_AE
|
|
lda titleScreenRow21Col03,x ; 3247 $9F4B[r-1] = low byte of the dy list (also graphicsData9300:circleDyPtrLoTable)
|
|
sta circleDyPtr ; 324A zp_AF
|
|
lda titleScreenRow21Col13,x ; 324C $9F55[r-1] = high byte (also graphicsData9300:circleDyPtrHiTable)
|
|
sta circleDyPtrHi ; 324F zp_B0
|
|
ldy titleScreenRow21Col23,x ; 3251 $9F5F[r-1] = index of the last point of the octant (also graphicsData9300:circleStepCountTable)
|
|
sty circleStepsLeft ; 3254 $91F2 = octant point counter
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the octant point list from the last entry down to entry 0.
|
|
; ----------------------------------------------------------------------
|
|
L_3257:
|
|
jsr computeCircleOctantPoints; 3257 mirror point Y into the 8 symmetric coordinates
|
|
jsr plotCircleOctantPoints ; 325A plot those 8 pixels
|
|
dec circleStepsLeft ; 325D previous point of the octant
|
|
ldy circleStepsLeft ; 3260 reload it as the index
|
|
bpl L_3257 ; 3263 loop until point 0 has been plotted
|
|
rts ; 3265 circle complete
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computeCircleOctantPoints - Expands one octant point (dx,dy) of a circle into the coordinates of all
|
|
; eight mirrored points, writing them into circlePointX0-7 ($32DA) and circlePointY0-7 ($32E2). The
|
|
; centre is pixel (12,10), the middle of a 24x21 sprite.
|
|
; In: Y = index into the octant lists, circleDxPtr zp_AD/zp_AE, circleDyPtr zp_AF/zp_B0
|
|
; Out: $32DA-$32E9 filled with 8 X and 8 Y coordinates
|
|
; Called from: drawCircleInSprite ($3257)
|
|
; ----------------------------------------------------------------------
|
|
computeCircleOctantPoints:
|
|
clc ; 3266 prepare the additions
|
|
lda #$0C ; 3267 12 = centre column of a 24-pixel-wide sprite
|
|
adc (circleDxPtr),y ; 3269 +dx of this octant point
|
|
sta circlePointX0 ; 326B X of mirrored point 0
|
|
sta circlePointX7 ; 326E X of mirrored point 7
|
|
sec ; 3271 prepare the subtractions
|
|
lda #$0C ; 3272 centre column again
|
|
sbc (circleDxPtr),y ; 3274 12-dx
|
|
sta circlePointX3 ; 3276 X of mirrored point 3
|
|
sta circlePointX4 ; 3279 X of mirrored point 4
|
|
clc ; 327C clear carry for the next addition
|
|
lda #$0C ; 327D centre column
|
|
adc (circleDyPtr),y ; 327F 12+dy - the octant swap that mirrors about the diagonal
|
|
sta circlePointX1 ; 3281 X of mirrored point 1
|
|
sta circlePointX6 ; 3284 X of mirrored point 6
|
|
sec ; 3287 set carry for the next subtraction
|
|
lda #$0C ; 3288 centre column
|
|
sbc (circleDyPtr),y ; 328A 12-dy
|
|
sta circlePointX2 ; 328C X of mirrored point 2
|
|
sta circlePointX5 ; 328F X of mirrored point 5
|
|
clc ; 3292 clear carry for the next addition
|
|
lda #$0A ; 3293 10 = centre row of a 21-pixel-high sprite
|
|
adc (circleDyPtr),y ; 3295 10+dy
|
|
sta circlePointY0 ; 3297 Y of mirrored point 0
|
|
sta circlePointY3 ; 329A Y of mirrored point 3
|
|
sec ; 329D set carry for the next subtraction
|
|
lda #$0A ; 329E centre row
|
|
sbc (circleDyPtr),y ; 32A0 10-dy
|
|
sta circlePointY4 ; 32A2 Y of mirrored point 4
|
|
sta circlePointY7 ; 32A5 Y of mirrored point 7
|
|
clc ; 32A8 clear carry for the next addition
|
|
lda #$0A ; 32A9 centre row
|
|
adc (circleDxPtr),y ; 32AB 10+dx
|
|
sta circlePointY1 ; 32AD Y of mirrored point 1
|
|
sta circlePointY2 ; 32B0 Y of mirrored point 2
|
|
sec ; 32B3 set carry for the next subtraction
|
|
lda #$0A ; 32B4 centre row
|
|
sbc (circleDxPtr),y ; 32B6 10-dx
|
|
sta circlePointY5 ; 32B8 Y of mirrored point 5
|
|
sta circlePointY6 ; 32BB Y of mirrored point 6
|
|
rts ; 32BE all eight points ready
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; plotCircleOctantPoints - Plots the eight mirrored points prepared by computeCircleOctantPoints into
|
|
; the sprite named by activeSpriteIndex, counting circlePointIndex ($3238) down from 7 to 0.
|
|
; In: circlePointX0-7 $32DA, circlePointY0-7 $32E2, activeSpriteIndex $91CC, spritePlotMode $91F3
|
|
; Out: 8 pixels plotted; circlePointIndex = $FF
|
|
; Called from: drawCircleInSprite ($325A)
|
|
; ----------------------------------------------------------------------
|
|
plotCircleOctantPoints:
|
|
ldx #$07 ; 32BF eight symmetric points per octant step
|
|
stx circlePointIndex ; 32C1 keep the point index in memory, X is needed by the plotter
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Plot the eight mirrored points of the current octant step.
|
|
; ----------------------------------------------------------------------
|
|
L_32C4:
|
|
ldy circlePointY0,x ; 32C4 $32E2[n] = Y coordinate of point n
|
|
lda circlePointX0,x ; 32C7 $32DA[n] = X coordinate of point n
|
|
tax ; 32CA X = pixel column
|
|
lda activeSpriteIndex ; 32CB the sprite chosen by drawCircleInSprite
|
|
jsr plotSpritePixel ; 32CE set/clear/toggle the pixel
|
|
dec circlePointIndex ; 32D1 next point
|
|
ldx circlePointIndex ; 32D4 reload the index
|
|
bpl L_32C4 ; 32D7 loop until point 0 has been plotted
|
|
rts ; 32D9 done
|
|
|
|
|
|
; Sixteen RAM bytes inside the code: the eight X coordinates ($32DA-$32E1) and the eight Y coordinates
|
|
; ($32E2-$32E9) of the mirrored points of one circle step. Written by computeCircleOctantPoints and
|
|
; read, indexed 0-7, by plotCircleOctantPoints.
|
|
circlePointX0:
|
|
.byte $00 ; 32DA . X coordinate of mirrored circle point 0
|
|
circlePointX1:
|
|
.byte $00 ; 32DB X coordinate of mirrored circle point 1
|
|
circlePointX2:
|
|
.byte $00 ; 32DC X coordinate of mirrored circle point 2
|
|
circlePointX3:
|
|
.byte $00 ; 32DD X coordinate of mirrored circle point 3
|
|
circlePointX4:
|
|
.byte $00 ; 32DE X coordinate of mirrored circle point 4
|
|
circlePointX5:
|
|
.byte $00 ; 32DF X coordinate of mirrored circle point 5
|
|
circlePointX6:
|
|
.byte $00 ; 32E0 X coordinate of mirrored circle point 6
|
|
circlePointX7:
|
|
.byte $00 ; 32E1 X coordinate of mirrored circle point 7
|
|
circlePointY0:
|
|
.byte $00 ; 32E2 Y coordinate of mirrored circle point 0
|
|
circlePointY1:
|
|
.byte $00 ; 32E3 Y coordinate of mirrored circle point 1
|
|
circlePointY2:
|
|
.byte $00 ; 32E4 Y coordinate of mirrored circle point 2
|
|
circlePointY3:
|
|
.byte $00 ; 32E5 Y coordinate of mirrored circle point 3
|
|
circlePointY4:
|
|
.byte $00 ; 32E6 Y coordinate of mirrored circle point 4
|
|
circlePointY5:
|
|
.byte $00 ; 32E7 Y coordinate of mirrored circle point 5
|
|
circlePointY6:
|
|
.byte $00 ; 32E8 Y coordinate of mirrored circle point 6
|
|
circlePointY7:
|
|
.byte $00 ; 32E9 Y coordinate of mirrored circle point 7
|
|
|
|
; ----------------------------------------------------------------------
|
|
; hideSprite1 - Clears bit 1 of the sprite enable shadow and writes the result straight to
|
|
; VIC_SPR_ENA, so the unit marker sprite disappears in the current frame instead of at the next raster
|
|
; interrupt.
|
|
; In: spriteEnableShadow $9022
|
|
; Out: $9022 and $D015 with bit 1 cleared; A clobbered
|
|
; Called from: the battlefield cursor and selection code ($229F, $22F4, $2359, $253C, $2540, $3D4E)
|
|
; ----------------------------------------------------------------------
|
|
hideSprite1:
|
|
lda spriteEnableShadow ; 32EA $9022 = shadow of VIC_SPR_ENA
|
|
and #$FD ; 32ED clear bit 1 = sprite 1, the unit marker on the overview map
|
|
sta spriteEnableShadow ; 32EF keep the shadow in step
|
|
sta VIC_SPR_ENA ; 32F2 write $D015 at once so the marker vanishes immediately
|
|
rts ; 32F5 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; hideSprite0Dead - Dead code: a byte-for-byte copy of hideSprite1 that clears bit 0 (sprite 0)
|
|
; instead of bit 1. No traced code refers to $32F6, and callers that want sprite 0 off clear the
|
|
; shadow themselves, so this is a leftover of an earlier build.
|
|
; In: none (never called)
|
|
; Out: would clear bit 0 of $9022 and $D015
|
|
; ----------------------------------------------------------------------
|
|
hideSprite0Dead:
|
|
lda spriteEnableShadow ; 32F6 dead code: sprite enable shadow
|
|
and #$FE ; 32F9 clear bit 0 = sprite 0
|
|
sta spriteEnableShadow ; 32FB store the shadow
|
|
sta VIC_SPR_ENA ; 32FE write it to $D015
|
|
rts ; 3301 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showCrosshairSprite - Loads the 63-byte diamond crosshair (the second shape of the pair at $9EA4,
|
|
; bytes $9EE3-$9F21) into sprite 2 and falls into the shared body at $3308, which paints it black,
|
|
; doubles it in both directions, parks it over the centre cell of the 7x5 tactical view and enables
|
|
; it.
|
|
; In: none
|
|
; Out: sprite 2 shape/colour/size/position set, sprite 2 enabled
|
|
; Called from: runTargetCursorLoop ($21AB) when the player is choosing an attack target
|
|
; ----------------------------------------------------------------------
|
|
showCrosshairSprite:
|
|
ldx #$7D ; 3302 $7D = last byte of shape 1 ($9EE3-$9F21), the diamond crosshair
|
|
bne L_3308 ; 3304 always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showCursorBoxSprite - Same as showCrosshairSprite but copies the first shape of the pair ($9EA4-
|
|
; $9EE2), the hollow box that frames the cursor cell of the tactical view.
|
|
; In: none
|
|
; Out: sprite 2 holds the box, black, expanded, at sprite X 256 / Y $4D, enabled
|
|
; Called from: battlefield entry ($1FEB, $2260) and the order-cancel path ($4C56, by JMP)
|
|
; ----------------------------------------------------------------------
|
|
showCursorBoxSprite:
|
|
ldx #$3E ; 3306 $3E = last byte of shape 0 ($9EA4-$9EE2), the hollow cursor box
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shared body: copy 63 bytes backwards into sprite 2, then make it a black, double-sized frame sitting
|
|
; over the centre cell of the 7x5 tactical view.
|
|
; ----------------------------------------------------------------------
|
|
L_3308:
|
|
ldy #$3E ; 3308 63 bytes, copied from the end backwards
|
|
L_330A:
|
|
lda titleScreenRow16Col36,x ; 330A $9EA4 holds the two 63-byte shapes back to back (also graphicsData9300:cursorBoxSprite)
|
|
sta sprite2Shape,y ; 330D $8A80 = shape of sprite 2
|
|
dex ; 3310 previous source byte
|
|
dey ; 3311 previous destination byte
|
|
bpl L_330A ; 3312 loop over all 63 bytes
|
|
lda #$00 ; 3314 colour 0
|
|
sta sprite2ColourShadow ; 3316 $900A = sprite 2 colour shadow: black
|
|
lda spriteXExpandShadow ; 3319 $9023 = X expansion shadow
|
|
ora #$04 ; 331C double the width of sprite 2
|
|
sta spriteXExpandShadow ; 331E store it back
|
|
lda spriteYExpandShadow ; 3321 $9024 = Y expansion shadow
|
|
ora #$04 ; 3324 double the height too, so the frame covers a whole 16x16 view cell
|
|
sta spriteYExpandShadow ; 3326 store it back
|
|
lda #$00 ; 3329 low byte of the sprite X coordinate
|
|
sta sprite2XShadow ; 332B $9012 = sprite 2 X shadow
|
|
lda spriteXMsbShadow ; 332E $9020 = shadow of the sprite X MSB register
|
|
ora #$04 ; 3331 set bit 2: sprite 2 X coordinate becomes 256
|
|
sta spriteXMsbShadow ; 3333 store it back
|
|
lda #$4D ; 3336 sprite Y 77
|
|
sta sprite2YShadow ; 3338 $901A: the centre cell of the 7x5 tactical view
|
|
lda #$04 ; 333B sprite 2 enable bit
|
|
jmp enableSprites ; 333D switch it on and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadDirectionShapeSprite - Copies sprite shape number A (0-7, one per facing direction) from the
|
|
; decompressed shape buffer at $0200 + A*64 into sprite 0 ($8A00) when activeSpriteIndex is 0, or into
|
|
; sprite 5 ($8B40) otherwise, and enables that sprite. The buffer is filled by the RLE decompressor at
|
|
; $67CB from the compressed shape sets at $FD70/$FE24.
|
|
; In: A = shape index 0-7, activeSpriteIndex $91CC = target sprite (0 or 5)
|
|
; Out: 63 bytes of sprite 0 or 5 rewritten and the sprite enabled; spritePlotPtr zp_98/zp_99
|
|
; clobbered;
|
|
; self-modifies the destination address of the STA at $3373
|
|
; Called from: the unit direction display ($5322) and overlay B ($802A, $810B, $829E)
|
|
; ----------------------------------------------------------------------
|
|
loadDirectionShapeSprite:
|
|
sta spritePlotPtrHi ; 3340 shape number in the high byte = shape*256
|
|
lda #$00 ; 3342 low byte starts at 0
|
|
lsr spritePlotPtrHi ; 3344 16-bit shift right...
|
|
ror a ; 3346 ...through the low byte
|
|
lsr spritePlotPtrHi ; 3347 and again
|
|
ror a ; 3349 shape*64
|
|
sta spritePlotPtr ; 334A low byte of the source pointer
|
|
lda #$00 ; 334C no extra low offset
|
|
ldy #$02 ; 334E $0200 = base of the decompressed shape buffer
|
|
clc ; 3350 clear carry for the 16-bit pointer addition
|
|
adc spritePlotPtr ; 3351 add the shape offset
|
|
sta spritePlotPtr ; 3353 source pointer low byte
|
|
tya ; 3355 base page $02
|
|
adc spritePlotPtrHi ; 3356 plus the high byte of shape*64
|
|
sta spritePlotPtrHi ; 3358 source pointer = $0200 + shape*64
|
|
ldx #$00 ; 335A default destination low byte
|
|
ldy #$8A ; 335C default destination high byte: $8A00 = sprite 0
|
|
lda activeSpriteIndex ; 335E $91CC = which sprite to load
|
|
beq L_3367 ; 3361 sprite 0 keeps the default
|
|
ldx #$40 ; 3363 otherwise $8B40...
|
|
ldy #$8B ; 3365 ...= the shape of sprite 5
|
|
L_3367:
|
|
stx L_3373+1 ; 3367 patch the low byte of the STA absolute,x at $3373
|
|
sty L_3373+2 ; 336A patch its high byte
|
|
ldy #$3E ; 336D 63 bytes to copy
|
|
ldx #$3E ; 336F same index for source and destination
|
|
L_3371:
|
|
lda (spritePlotPtr),y ; 3371 read from the shape buffer
|
|
L_3373:
|
|
sta irqVectorHi,x ; 3373 patched to $8A00,x or $8B40,x; the $FFFF resting value is never used
|
|
dex ; 3376 previous destination byte
|
|
dey ; 3377 previous source byte
|
|
bpl L_3371 ; 3378 loop over all 63 bytes
|
|
ldx activeSpriteIndex ; 337A which sprite was loaded
|
|
lda spriteBitMasks,x ; 337D $2F85[n] = 1 << n
|
|
jmp enableSprites ; 3380 switch it on and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showComcenCrossMarker - Puts the small cross marker at $9F22 into rows 7-13 of sprite 2, colours it
|
|
; light red and parks it on the comcen's cell of overlay B's 21x17 radar grid (grid column 10, row 8 -
|
|
; the grid is always drawn centred on the comcen).
|
|
; In: none
|
|
; Out: sprite 2 shape/colour/position set and enabled, gridCursorCol zp_67 = 10, gridCursorRow zp_68 =
|
|
; 8
|
|
; Called from: overlay B ($75B6) while the RADAR/MISSILE screen is being built
|
|
; ----------------------------------------------------------------------
|
|
showComcenCrossMarker:
|
|
lda #$02 ; 3383 sprite 2
|
|
jsr clearSpriteShape ; 3385 blank its 64 shape bytes first
|
|
ldy #$14 ; 3388 21 bytes = 7 sprite rows of 3 bytes
|
|
L_338A:
|
|
lda titleScreenRow20Col02,y ; 338A $9F22 = the small cross/plus shape (also graphicsData9300:comcenCrossMarkerShape)
|
|
sta sprite2ShapeRow7,y ; 338D $8A95 = sprite 2 shape + $15, i.e. row 7 of the sprite
|
|
dey ; 3390 previous byte
|
|
bpl L_338A ; 3391 loop over the 21 bytes
|
|
lda #$0A ; 3393 colour $0A = light red
|
|
sta sprite2ColourShadow ; 3395 $900A = sprite 2 colour shadow
|
|
ldx #$0A ; 3398 grid column 10 - where the own comcen always sits
|
|
stx gridCursorCol ; 339A zp_67 = radar grid cursor column
|
|
ldy #$08 ; 339C grid row 8
|
|
sty gridCursorRow ; 339E zp_68 = radar grid cursor row
|
|
jsr isRadarGridCellInRange ; 33A0 overlay B range test; its carry result is never examined here, so the call is a leftover
|
|
jsr gridCellToSpriteCoords ; 33A3 grid cell -> sprite coordinates
|
|
stx sprite2XShadow ; 33A6 $9012 = sprite 2 X shadow
|
|
sty sprite2YShadow ; 33A9 $901A = sprite 2 Y shadow
|
|
lda #$04 ; 33AC sprite 2 enable bit
|
|
jmp enableSprites ; 33AE switch it on and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; gridCellToSpriteCoords - Converts a cell of overlay B's 21x17 radar grid (one map cell per 8x8
|
|
; character cell) into sprite coordinates: spriteX = column*8 + $27, spriteY = row*8 + $43. The grid
|
|
; starts at character position (3,3); the two constants fold in the VIC sprite origin (24,50) and the
|
|
; offset of the marker shapes inside their sprite.
|
|
; In: X = grid column 0-20, Y = grid row 0-16
|
|
; Out: X = sprite X, Y = sprite Y; A clobbered
|
|
; Called from: showComcenCrossMarker ($33A3), getUnitSpriteCentreOnGrid ($3410) and overlay B
|
|
; ($7709, $77BC)
|
|
; ----------------------------------------------------------------------
|
|
gridCellToSpriteCoords:
|
|
txa ; 33B1 grid column
|
|
asl a ; 33B2 column*2
|
|
asl a ; 33B3 column*4
|
|
asl a ; 33B4 column*8 - the radar grid uses one 8x8 character cell per map cell
|
|
clc ; 33B5 clear carry before adding the grid origin
|
|
adc #$27 ; 33B6 sprite X of grid column 0
|
|
tax ; 33B8 return it in X
|
|
tya ; 33B9 grid row
|
|
asl a ; 33BA row*2
|
|
asl a ; 33BB row*4
|
|
asl a ; 33BC row*8
|
|
clc ; 33BD clear carry before adding the grid origin
|
|
adc #$43 ; 33BE sprite Y of grid row 0
|
|
tay ; 33C0 return it in Y
|
|
rts ; 33C1 done
|
|
|
|
|
|
; Three 2-entry tables indexed by explosion type: 0 = comcen missile strike, 1 = drone detonation.
|
|
; They give the sprite offsets that centre the ring blob on the target cell and the number of
|
|
; concentric rings that make the blob.
|
|
explosionMarkerXOffset:
|
|
.byte $0A,$08 ; 33C2 .. X offsets: 10 pixels for a missile strike, 8 for a drone blast
|
|
explosionMarkerYOffset:
|
|
.byte $08 ; 33C4 matching Y offsets: 8 and 9 pixels
|
|
.byte $09 ; 33C5 .
|
|
explosionMarkerRingCount:
|
|
.byte $02 ; 33C6 ring counts: 2 rings for a missile strike, 4 for the bigger drone blast
|
|
.byte $04 ; 33C7 .
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showExplosionMarker - Shows the explosion marker for a weapon effect at map cell (X,Y). On the
|
|
; battlefield screen it draws a blob of concentric rings into sprite 4 and puts it straight on the
|
|
; 4x4-per-cell overview map; a missile strike that arrives while a drone is in the air instead just
|
|
; records the cell in the two patched operands of updateExplosionMarker, which keeps the marker in
|
|
; step with the scrolling drone camera window. Either way markerSpriteTimer is set to 90 frames, and
|
|
; the per-frame update in updateGameFrame switches sprite 4 off when it expires. The survey called
|
|
; this routine dead; it is not - it is called by two command handlers.
|
|
; In: A = effect type (0 = comcen missile strike, 1 = drone detonation), X = map column, Y = map row;
|
|
; droneControlFlags $920D, currentScreen $90FB
|
|
; Out: sprite 4 shape/position/colour, markerSpriteTimer $923E = 90; self-modifies $33EA (the effect
|
|
; index) and, on the drone path, $3425/$3437 (the marker cell)
|
|
; Called from: cmdDroneDetonate ($5375, A=1) and cmdMissileStrike ($541C, A=0)
|
|
; ----------------------------------------------------------------------
|
|
showExplosionMarker:
|
|
sta L_33E9+1 ; 33C8 patch the LDX # at $33E9 with the effect index (0 missile, 1 drone)
|
|
cmp #$00 ; 33CB is this the comcen missile strike?
|
|
bne L_33E0 ; 33CD no - a drone blast always goes on the overview map
|
|
lda droneControlFlags ; 33CF $920D = drone control flags
|
|
bpl L_33E0 ; 33D2 bit 7 clear = no drone in the air, so the drone camera is not on screen
|
|
jsr startExplosionMarkerTimer; 33D4 start the 90-frame marker countdown
|
|
stx loadExplosionMarkerCol+1; 33D7 remember the blast column in the LDA # of updateExplosionMarker
|
|
sty loadExplosionMarkerRow+1; 33DA and the blast row
|
|
jmp updateExplosionMarker ; 33DD position sprite 4 inside the drone camera window and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Battlefield path: build the ring blob and drop it straight on the overview map.
|
|
; ----------------------------------------------------------------------
|
|
L_33E0:
|
|
lda currentScreen ; 33E0 $90FB = which screen is showing; 0 = the battlefield
|
|
bne L_340A ; 33E3 not on the battlefield - there is nothing to draw on
|
|
jsr mapCellToSpriteXYRaw ; 33E5 blast cell -> overview map sprite coordinates
|
|
txa ; 33E8 sprite X of the cell
|
|
L_33E9:
|
|
ldx #$FF ; 33E9 effect index, patched at $33C8
|
|
sec ; 33EB set carry for the centring subtraction
|
|
sbc explosionMarkerXOffset,x; 33EC shift left so the ring blob is centred on the cell
|
|
sta sprite4XShadow ; 33EF $9014 = sprite 4 X shadow
|
|
tya ; 33F2 sprite Y of the cell
|
|
sec ; 33F3 set carry for the centring subtraction
|
|
sbc explosionMarkerYOffset,x; 33F4 shift up by the matching Y offset
|
|
sta sprite4YShadow ; 33F7 $901C = sprite 4 Y shadow
|
|
lda explosionMarkerRingCount,x; 33FA how many concentric rings this effect uses
|
|
jsr drawRingsInSprite4 ; 33FD draw them into sprite 4
|
|
lda #$10 ; 3400 sprite 4 enable bit
|
|
jsr enableSprites ; 3402 switch the marker on
|
|
|
|
; ----------------------------------------------------------------------
|
|
; startExplosionMarkerTimer - Two-instruction tail shared by both paths of showExplosionMarker: arms
|
|
; the explosion marker for 90 frames (about 1.5 seconds).
|
|
; In: none
|
|
; Out: markerSpriteTimer $923E = $5A
|
|
; Called from: showExplosionMarker ($33D4) and by fall-through from $3402
|
|
; ----------------------------------------------------------------------
|
|
startExplosionMarkerTimer:
|
|
lda #$5A ; 3405 $5A = 90 frames
|
|
sta markerSpriteTimer ; 3407 $923E; updateGameFrame counts it down and hides sprite 4 at zero
|
|
L_340A:
|
|
rts ; 340A return (also the exit taken when the battlefield screen is not showing)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getUnitSpriteCentreOnGrid - Maps a battlefield cell onto overlay B's radar grid and returns the
|
|
; sprite coordinates of the centre of that grid cell, or C=1 when the cell lies outside the 21x17 grid
|
|
; drawn around the comcen.
|
|
; In: X = map column, Y = map row
|
|
; Out: C=0 with X/Y = sprite coordinates of the cell centre, or C=1 and X/Y meaningless
|
|
; Called from: the unit tracking code ($5307) and overlay B ($7A03)
|
|
; ----------------------------------------------------------------------
|
|
getUnitSpriteCentreOnGrid:
|
|
jsr mapCellToRadarGrid ; 340B overlay B $7A13: map cell -> radar grid cell relative to the comcen, C=1 when off-grid
|
|
bcs L_341E ; 340E outside the 21x17 grid: return with C=1
|
|
jsr gridCellToSpriteCoords ; 3410 grid cell -> sprite coordinates of its top-left
|
|
tya ; 3413 sprite Y
|
|
clc ; 3414 clear carry before adding the half-cell offset
|
|
adc #$04 ; 3415 +4 = half a character cell, i.e. the centre of the cell
|
|
tay ; 3417 back into Y
|
|
txa ; 3418 sprite X
|
|
clc ; 3419 clear carry before adding the half-cell offset
|
|
adc #$04 ; 341A +4 for the same reason
|
|
tax ; 341C back into X
|
|
clc ; 341D C=0 = the cell is on the grid
|
|
L_341E:
|
|
rts ; 341E return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateExplosionMarker - Keeps the explosion marker (sprite 4) on the right cell of the drone camera
|
|
; window, which is 14 columns by 12 rows of 8x8 pixel cells starting at the view origin. The marker
|
|
; cell lives in the two immediate operands at $3425 and $3437, patched by showExplosionMarker. When
|
|
; the countdown has expired or the cell has scrolled out of the window, sprite 4 is switched off
|
|
; instead. The survey believed nothing ever patched the operands; showExplosionMarker does.
|
|
; In: markerSpriteTimer $923E, viewOriginCol zp_A5, viewOriginRow zp_A6, the operands at $3425/$3437
|
|
; Out: sprite 4 position shadows $9014/$901C and its enable bit; A clobbered
|
|
; Called from: showExplosionMarker ($33DD, by JMP) and overlay B's drone camera step ($827E)
|
|
; ----------------------------------------------------------------------
|
|
updateExplosionMarker:
|
|
lda markerSpriteTimer ; 341F $923E, counted down once per frame by updateGameFrame
|
|
beq L_344D ; 3422 zero = no explosion pending: make sure sprite 4 is off
|
|
loadExplosionMarkerCol:
|
|
lda #$00 ; 3424 blast column, patched at $33D7
|
|
sec ; 3426 set carry for the subtraction of the view origin
|
|
sbc viewOriginCol ; 3427 column relative to the left edge of the drone camera window
|
|
cmp #$0E ; 3429 the window is 14 cells wide
|
|
bcs L_344D ; 342B outside it (this also catches negative differences): hide the marker
|
|
asl a ; 342D x2
|
|
asl a ; 342E x4
|
|
asl a ; 342F x8 - the drone camera draws one map cell per 8x8 character cell
|
|
clc ; 3430 clear carry before adding the window origin
|
|
adc #$78 ; 3431 sprite X that centres the ring blob on window column 0
|
|
sta sprite4XShadow ; 3433 $9014 = sprite 4 X shadow
|
|
loadExplosionMarkerRow:
|
|
lda #$FF ; 3436 blast row, patched at $33DA
|
|
sec ; 3438 set carry for the subtraction of the view origin
|
|
sbc viewOriginRow ; 3439 row relative to the top of the window
|
|
cmp #$0C ; 343B the window is 12 cells high
|
|
bcs L_344D ; 343D outside it: hide the marker
|
|
asl a ; 343F x2
|
|
asl a ; 3440 x4
|
|
asl a ; 3441 x8
|
|
clc ; 3442 clear carry before adding the window origin
|
|
adc #$5C ; 3443 sprite Y that centres the blob on window row 0
|
|
sta sprite4YShadow ; 3445 $901C = sprite 4 Y shadow
|
|
lda #$10 ; 3448 sprite 4 enable bit
|
|
jmp enableSprites ; 344A switch it on and return to the caller
|
|
|
|
L_344D:
|
|
lda spriteEnableShadow ; 344D $9022 = sprite enable shadow
|
|
and #$EF ; 3450 clear bit 4 = sprite 4, the explosion marker
|
|
sta spriteEnableShadow ; 3452 store it back (the raster IRQ writes $D015)
|
|
rts ; 3455 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawComcenRingSprite - Builds the comcen marker of the drone screen: two concentric rings (radius 2
|
|
; then radius 1, which is a filled 3x3 dot) in sprite 4, coloured with the flashing pair $12 so it
|
|
; alternates between white and red.
|
|
; In: none
|
|
; Out: sprite 4 shape rebuilt, sprite4ColourShadow $900C = $12, sprite 4 left disabled here,
|
|
; activeSpriteIndex $91CC = 4, spritePlotMode $91F3 = $40
|
|
; Called from: overlay B ($80E1), right after the view has been centred on the comcen
|
|
; ----------------------------------------------------------------------
|
|
drawComcenRingSprite:
|
|
lda #$02 ; 3456 two concentric rings for the comcen marker
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawRingsInSprite4 - Shared body of drawComcenRingSprite: clears sprite 4 and draws A concentric
|
|
; rings into it, radius A down to radius 1, then gives it the flashing colour pair $12. The ring count
|
|
; is held in the LDX # operand at $346E and counted down there.
|
|
; In: A = number of rings / radius of the outermost one
|
|
; Out: sprite 4 shape and colour; sprite 4 disabled while it is rebuilt; self-modifies $346E
|
|
; Called from: showExplosionMarker ($33FD, A = 2 or 4) and by fall-through from drawComcenRingSprite
|
|
; ----------------------------------------------------------------------
|
|
drawRingsInSprite4:
|
|
sta L_346D+1 ; 3458 patch the LDX # at $346D with the outermost radius / ring count
|
|
lda spriteEnableShadow ; 345B $9022 = sprite enable shadow
|
|
and #$EF ; 345E clear bit 4: hide sprite 4 while its shape is rebuilt
|
|
sta spriteEnableShadow ; 3460 store it back
|
|
lda #$40 ; 3463 bit 6 = 'set the pixel'
|
|
sta spritePlotMode ; 3465 $91F3 = plot mode for drawCircleInSprite
|
|
lda #$04 ; 3468 sprite 4
|
|
jsr clearSpriteShape ; 346A blank its 64 shape bytes
|
|
L_346D:
|
|
ldx #$FF ; 346D current radius, patched at $3458 and decremented below
|
|
lda #$04 ; 346F draw into sprite 4
|
|
jsr drawCircleInSprite ; 3471 one circle of that radius, centred on sprite pixel (12,10)
|
|
dec L_346D+1 ; 3474 next smaller radius
|
|
bne L_346D ; 3477 loop down to radius 1, which fills the centre with a 3x3 dot
|
|
lda #$12 ; 3479 $12 = flashing colour pair: alternates colour 2 (red) and colour 1 (white)
|
|
sta sprite4ColourShadow ; 347B $900C = sprite 4 colour shadow
|
|
rts ; 347E done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; orderSelectedUnitToCursor - Issues a MOVE order for the selected unit to the cell under the cursor:
|
|
; takes the unit out of its group, queues command $80 unit,col,row into the outgoing ring (so the
|
|
; engine, the opponent and the game film all see the same bytes) and refreshes the 'already at the
|
|
; destination' flag.
|
|
; In: selectedUnit $90F8, viewOriginCol zp_A5, viewOriginRow zp_A6
|
|
; Out: cmdParam0-2 zp_73-zp_75 = unit/column/row, $FB54[unit] bit 6 cleared and bit 7 refreshed,
|
|
; 4 bytes queued for the command interpreter, C=0
|
|
; Called from: the fire-button handler of the battlefield cursor loop ($2212)
|
|
; ----------------------------------------------------------------------
|
|
orderSelectedUnitToCursor:
|
|
lda viewOriginCol ; 347F left edge column of the tactical view
|
|
clc ; 3481 clear carry before stepping to the cursor column
|
|
adc #$03 ; 3482 +3 = the cursor column
|
|
sta cmdParam1 ; 3484 zp_74 = command argument 1 (destination column)
|
|
lda viewOriginRow ; 3486 top edge row of the tactical view
|
|
clc ; 3488 clear carry before stepping to the cursor row
|
|
adc #$02 ; 3489 +2 = the cursor row
|
|
sta cmdParam2 ; 348B zp_75 = command argument 2 (destination row)
|
|
ldx selectedUnit ; 348D $90F8 = the unit being ordered
|
|
lda unitDisplayFlagsTable,x ; 3490 $FB54[unit] = local display/group flags
|
|
and #$BF ; 3493 clear bit 6: a unit given its own order leaves its group
|
|
sta unitDisplayFlagsTable,x ; 3495 store the flags back
|
|
stx cmdParam0 ; 3498 zp_73 = command argument 0 (unit index)
|
|
lda #$80 ; 349A command byte $80 = MOVE unit,col,row
|
|
jsr queueCmd4 ; 349C $560A: queue the 4 bytes for the engine, the link and the film
|
|
jsr updateUnitAtDestinationFlag; 349F set or clear the 'standing on its destination' bit
|
|
clc ; 34A2 C=0 tells the cursor loop the order was accepted
|
|
rts ; 34A3 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateUnitAtDestinationFlag - Sets bit 7 of unitDisplayFlags ($FB54) for the unit in cmdParam0 when
|
|
; its current cell is the same as the ordered destination in cmdParam1/cmdParam2, and clears it
|
|
; otherwise. The bit is what the console and the map drawing use to tell a moving unit from an idle
|
|
; one.
|
|
; In: cmdParam0 zp_73 = unit, cmdParam1 zp_74 = destination column, cmdParam2 zp_75 = destination row
|
|
; Out: $FB54[unit] bit 7; self-modifies the ORA operand at $34C1
|
|
; Called from: orderSelectedUnitToCursor ($349F) and the command executors at $4A4D and $4C10
|
|
; ----------------------------------------------------------------------
|
|
updateUnitAtDestinationFlag:
|
|
ldy cmdParam0 ; 34A4 zp_73 = unit index
|
|
lda #$00 ; 34A6 assume the unit still has somewhere to go
|
|
ldx unitColTable,y ; 34A8 $F640[unit] = current map column
|
|
cpx cmdParam1 ; 34AB same as the ordered column?
|
|
bne L_34B8 ; 34AD no - it is still travelling
|
|
ldx unitRowTable,y ; 34AF $F6A4[unit] = current map row
|
|
cpx cmdParam2 ; 34B2 same as the ordered row?
|
|
bne L_34B8 ; 34B4 no - still travelling
|
|
lda #$80 ; 34B6 bit 7 = the unit is already standing on its destination
|
|
L_34B8:
|
|
sta L_34C0+1 ; 34B8 patch the ORA # at $34C0 with $80 or $00
|
|
lda unitDisplayFlagsTable,y ; 34BB $FB54[unit]
|
|
and #$7F ; 34BE clear the old 'at destination' bit
|
|
L_34C0:
|
|
ora #$FF ; 34C0 patched: put the freshly computed bit back
|
|
sta unitDisplayFlagsTable,y ; 34C2 store the flags
|
|
rts ; 34C5 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyMoveOrder - Executor of command $80 MOVE (unit, column, row). It takes the unit out of its
|
|
; group, republishes the group bits into the local flags copy, and falls into setUnitDestination to
|
|
; store the clamped destination. The survey listed this as an unreferenced older version; it is in
|
|
; fact live - cmdMoveUnit jumps here from $5017 - and there is no other move executor in the build.
|
|
; In: cmdParam0 zp_73 = unit, cmdParam1/cmdParam2 zp_74/zp_75 = destination (already mirrored for a
|
|
; remote command)
|
|
; Out: $F708[unit] bit 6 cleared, $FB54[unit] group bits refreshed, then everything setUnitDestination
|
|
; does
|
|
; Called from: cmdMoveUnit ($5017, by JMP)
|
|
; ----------------------------------------------------------------------
|
|
applyMoveOrder:
|
|
ldx cmdParam0 ; 34C6 zp_73 = the unit being ordered
|
|
lda unitFlagsTable,x ; 34C8 $F708[unit] = authoritative unit flags (facing, side, group)
|
|
and #$BF ; 34CB clear bit 6: an individual move order takes the unit out of its group
|
|
sta unitFlagsTable,x ; 34CD store the flags back
|
|
jsr syncLocalGroupFlags ; 34D0 $4C6B: copy the group bits into the $FB54 display flags
|
|
ldy cmdParam0 ; 34D3 Y = unit index for the destination code below
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setUnitDestination - Stores a destination for unit Y. A comcen whose speed option is class 4 (fixed)
|
|
; has its destination forced back to its own cell; a destination equal to the current cell is stored
|
|
; with bit 7 set, which marks the unit idle and is mirrored into unitDisplayFlags bit 7; anything else
|
|
; is clamped to the 40x40 map. The walking waypoint is reset to the unit's current cell (except for
|
|
; the solo trainer's own units, which keep theirs), and the order cancels any dig-in in progress.
|
|
; In: Y = unit index, cmdParam1/cmdParam2 zp_74/zp_75 = destination column/row, gamePhase zp_B1,
|
|
; comcenSpeedClass $92B4 / opponentComcenSpeedClass $92B5, comcenUnit $9236,
|
|
; soloTrainerAtGameStart $92FE
|
|
; Out: $F7D0/$F834 destination, $FB54 bit 7, $F898/$F8FC waypoint, $F960 bits 5-7 cleared,
|
|
; $F76C bit 6 cleared; self-modifies the ORA operand at $3521
|
|
; Called from: the group formation move ($4A31, $4A9B), the destination command ($4D21), the group
|
|
; order tail ($4C68, by JMP) and by fall-through from applyMoveOrder
|
|
; ----------------------------------------------------------------------
|
|
setUnitDestination:
|
|
lda gamePhase ; 34D5 zp_B1 = 0 only while the battle is actually running
|
|
bne L_34FA ; 34D7 in the setup, film and menu phases the comcen speed rule does not apply
|
|
cpy #$31 ; 34D9 unit 49 = player 0's comcen
|
|
beq L_34E1 ; 34DB it is a comcen
|
|
cpy #$63 ; 34DD unit 99 = player 1's comcen
|
|
bne L_34FA ; 34DF not a comcen: nothing special
|
|
L_34E1:
|
|
lda comcenSpeedClass ; 34E1 $92B4 = the local player's COMCEN SPEED option
|
|
cpy comcenUnit ; 34E4 $9236 = index of the local player's comcen
|
|
beq L_34EC ; 34E7 it is our own comcen, use our own option
|
|
lda opponentComcenSpeedClass; 34E9 $92B5 = the opponent's comcen speed class
|
|
L_34EC:
|
|
cmp #$04 ; 34EC class 4 = the comcen may not move at all
|
|
bne L_34FA ; 34EE any other class: the ordered destination stands
|
|
lda unitColTable,y ; 34F0 immobile comcen: destination column...
|
|
sta cmdParam1 ; 34F3 ...forced to where it already stands
|
|
lda unitRowTable,y ; 34F5 and the same for the row
|
|
sta cmdParam2 ; 34F8 zp_75
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Store the destination: equal to the current cell means 'idle', anything else is clamped to the map,
|
|
; and the idle bit is mirrored into the display flags.
|
|
; ----------------------------------------------------------------------
|
|
L_34FA:
|
|
lda cmdParam1 ; 34FA destination column
|
|
cmp unitColTable,y ; 34FC $F640[unit] = current column
|
|
bne L_350E ; 34FF different - the unit really has to move
|
|
lda cmdParam2 ; 3501 destination row
|
|
cmp unitRowTable,y ; 3503 $F6A4[unit] = current row
|
|
bne L_350E ; 3506 different - the unit really has to move
|
|
lda cmdParam1 ; 3508 same cell: take the column again
|
|
ora #$80 ; 350A bit 7 = 'destination reached, nothing to do'
|
|
bmi L_3513 ; 350C always taken (bit 7 was just set)
|
|
L_350E:
|
|
lda cmdParam1 ; 350E destination column
|
|
jsr clampMapCoord ; 3510 keep it inside the 40x40 battlefield
|
|
L_3513:
|
|
sta unitDestColTable,y ; 3513 $F7D0[unit] = destination column (bit 7 = idle)
|
|
and #$80 ; 3516 isolate the idle bit
|
|
sta L_3520+1 ; 3518 patch the ORA # at $3520 with it
|
|
lda unitDisplayFlagsTable,y ; 351B $FB54[unit] = display flags
|
|
and #$7F ; 351E clear the old idle bit
|
|
L_3520:
|
|
ora #$FF ; 3520 patched: mirror the new idle bit into bit 7
|
|
sta unitDisplayFlagsTable,y ; 3522 store the display flags
|
|
lda cmdParam2 ; 3525 destination row
|
|
jsr clampMapCoord ; 3527 clamp it to the map as well
|
|
sta unitDestRowTable,y ; 352A $F834[unit] = destination row
|
|
bit soloTrainerAtGameStart ; 352D $92FE bit 7 = this game is against the solo trainer
|
|
bpl L_3536 ; 3530 modem or film game: always reset the waypoint
|
|
cpy #$32 ; 3532 units 50-99 are the trainer's own units
|
|
bcs L_354C ; 3534 the trainer maintains their waypoints itself
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Reset the walking waypoint to the cell the unit is standing on, keeping the two flag bits in the top
|
|
; of those bytes.
|
|
; ----------------------------------------------------------------------
|
|
L_3536:
|
|
lda unitPathColTable,y ; 3536 $F898[unit] = waypoint column, bits 6-7 are flags
|
|
and #$C0 ; 3539 keep only those two flag bits
|
|
ora unitColTable,y ; 353B waypoint column = the cell the unit is standing on
|
|
sta unitPathColTable,y ; 353E store it back
|
|
lda unitPathRowTable,y ; 3541 $F8FC[unit] = waypoint row, bits 6-7 are flags
|
|
and #$C0 ; 3544 keep the flag bits
|
|
ora unitRowTable,y ; 3546 waypoint row = current row
|
|
sta unitPathRowTable,y ; 3549 store it back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; An order to move cancels any dig-in the unit had started or completed.
|
|
; ----------------------------------------------------------------------
|
|
L_354C:
|
|
lda unitStunTable,y ; 354C $F960[unit]: low nibble stun counter, bits 5-7 dig progress
|
|
and #$1F ; 354F clear bits 5-7 - the unit stops digging in
|
|
sta unitStunTable,y ; 3551 store it back
|
|
lda unitTypeTable,y ; 3554 $F76C[unit] = type byte
|
|
and #$BF ; 3557 clear bit 6 = no longer dug in / fortified
|
|
sta unitTypeTable,y ; 3559 store it back
|
|
rts ; 355C the order is stored
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clampMapCoord - Clamps a signed map coordinate to the battlefield: negative becomes 0, anything from
|
|
; 40 upwards becomes 39.
|
|
; In: A = coordinate
|
|
; Out: A = 0..39
|
|
; Called from: setUnitDestination ($3510 for the column, $3527 for the row)
|
|
; ----------------------------------------------------------------------
|
|
clampMapCoord:
|
|
bpl L_3561 ; 355D already zero or positive
|
|
lda #$00 ; 355F negative coordinates clamp to column/row 0
|
|
L_3561:
|
|
cmp #$28 ; 3561 40 = width and height of the battlefield
|
|
bcc L_3567 ; 3563 inside the map
|
|
lda #$27 ; 3565 39 = last valid column/row
|
|
L_3567:
|
|
rts ; 3567 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; markAllUnitsVisible - Sets bits 6 and 7 (spotted this cycle, currently visible) of unitEnergy for
|
|
; all 100 units, i.e. reveals the whole battlefield. Used at the end of a game and whenever no game is
|
|
; in progress.
|
|
; In: none
|
|
; Out: $F9C4[0..99] bits 6-7 set; Y = $FF, A clobbered
|
|
; Called from: the game reset at $4018 and ageUnitVisibility ($3579, by branch)
|
|
; ----------------------------------------------------------------------
|
|
markAllUnitsVisible:
|
|
ldy #$63 ; 3568 unit 99 down to unit 0
|
|
L_356A:
|
|
lda unitEnergyTable,y ; 356A $F9C4[unit]: bits 0-5 energy, bit 6 spotted, bit 7 visible
|
|
ora #$C0 ; 356D mark it both spotted and visible
|
|
sta unitEnergyTable,y ; 356F store it back
|
|
dey ; 3572 previous unit
|
|
bpl L_356A ; 3573 loop over all 100 units
|
|
rts ; 3575 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; ageUnitVisibility - Run once per unit-processing cycle, before the spotting passes: every unit that
|
|
; was spotted during the last cycle (energy bit 6) stays visible (bit 7) and everything else fades
|
|
; out, and bit 6 is cleared ready for the coming scan. It also resets the unit-scan side filter to
|
|
; 'accept every unit'. When the game is over it simply reveals everything instead.
|
|
; In: gameEndReason $0BA8, unitEnergyTable $F9C4
|
|
; Out: $F9C4 visibility bits rewritten, the collectUnitsInRadius filter at $37FF/$3800 set to
|
|
; compare against 0 with BCC; self-modifies $359C
|
|
; Called from: finishRound ($4660)
|
|
; ----------------------------------------------------------------------
|
|
ageUnitVisibility:
|
|
lda gameEndReason ; 3576 $0BA8 = nonzero once the game has ended
|
|
bne markAllUnitsVisible ; 3579 game over: show every unit instead of ageing the flags
|
|
lda #$00 ; 357B compare value 0...
|
|
sta scanSideFilterCompare+1 ; 357D ...into the CMP # of the unit scan filter at $37FE
|
|
lda #$90 ; 3580 opcode of BCC
|
|
sta scanSideFilterBranch ; 3582 so the filter at $3800 accepts every unit index
|
|
ldy #$00 ; 3585 unit 0 upwards
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shift each unit's visibility: bit 6 (spotted last cycle) becomes bit 7 (visible now).
|
|
; ----------------------------------------------------------------------
|
|
L_3587:
|
|
ldx #$80 ; 3587 assume the unit stays visible
|
|
lda unitEnergyTable,y ; 3589 $F9C4[unit]
|
|
and #$40 ; 358C bit 6 = it was spotted during the last scan cycle
|
|
bne L_3592 ; 358E it was - keep it visible
|
|
ldx #$00 ; 3590 it was not - it fades out of sight
|
|
L_3592:
|
|
lda unitEnergyTable,y ; 3592 read the byte again
|
|
and #$3F ; 3595 keep the energy value, drop both visibility bits
|
|
sta L_359B+1 ; 3597 patch the ORA # at $359B with it
|
|
txa ; 359A $80 (visible) or $00 (hidden)
|
|
L_359B:
|
|
ora #$FF ; 359B patched energy value
|
|
sta unitEnergyTable,y ; 359D bit 7 = visible, bit 6 cleared ready for this cycle's scan
|
|
iny ; 35A0 next unit
|
|
cpy #$64 ; 35A1 100 units
|
|
bcc L_3587 ; 35A3 loop
|
|
rts ; 35A5 done
|
|
|
|
|
|
; sightRangeByType - base range in cells, indexed by unit type for the spotting pass (entries 0-4) and
|
|
; by type+5 for the attack pass (entries 5-9). The terrain bonus below is added on top and the sum is
|
|
; capped at 10.
|
|
sightRangeByType:
|
|
.byte $03,$03,$03,$05,$03,$03,$03,$06; 35A6 ........ spotting: GRUNT 3, RIDER 3, BOOMER 3, SPY 5, COMCEN 3; attack: GRUNT 3, RIDER 3, BOOMER 6
|
|
.byte $05 ; 35AE attack range of SPY 5 and COMCEN 3 (entries 8 and 9)
|
|
.byte $03 ; 35AF .
|
|
|
|
; rangeDistanceSqTable - r*(r+1) for r = 0..10. scanVisibleEnemies compares the squared distance of a
|
|
; candidate against this, which rounds the square search box into a circle.
|
|
rangeDistanceSqTable:
|
|
.byte $00,$02,$06,$0C,$14,$1E,$2A,$38; 35B0 ......*8 r*(r+1) for r = 0..7: 0, 2, 6, 12, 20, 30, 42, 56
|
|
.byte $48 ; 35B8 r*(r+1) for r = 8, 9, 10: 72, 90, 110
|
|
.byte $5A,$6E ; 35B9 Zn
|
|
|
|
; terrainSightBonusTable - extra range given by the terrain class under the observer (0-7); doubled on
|
|
; the spotting pass, counted once on the attack pass.
|
|
terrainSightBonusTable:
|
|
.byte $00,$00,$00,$01,$01,$02,$02,$00; 35BB ........ classes 0-7: 0, 0, 0, 1, 1, 2, 2, 0 extra cells of range
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scanVisibleEnemies - Builds the list of enemy units that unit Y can see (X bit 7 clear, the spotting
|
|
; pass) or can shoot at (X bit 7 set, the attack-target pass). The effective range is
|
|
; sightRangeByType[type, +5 when attacking] plus the terrain bonus of the cell the unit stands on
|
|
; (doubled while spotting), capped at 10 cells. collectUnitsInRadius fills $0400 with the enemy units
|
|
; in the square and $0432 with their squared distances; this routine then walks that list and strikes
|
|
; off ($0400[n] = $FF) every candidate that is out of range, dead, not yet visible (attack pass),
|
|
; protected by the spy/hidden rules (spotting pass) or hidden behind a ridge. $0464[n] receives the
|
|
; direction 0-3 from the scanner to the candidate. Candidates that are not in the unit's preferred
|
|
; direction (zp_6B) are pushed one step further away, which biases a unit towards the target it is
|
|
; already facing. The first sighting of an enemy spy or comcen queues status message $0F / $0D. Only
|
|
; the head of the routine ($35C3-$366E) belongs to this chunk; it continues to $3745.
|
|
; In: X = pass (bit 7: 0 = spotting, 1 = attack targets), Y = the scanning unit; unit arrays, the
|
|
; map,
|
|
; the terrain class table at $077E
|
|
; Out: $0400 candidate list (entries struck off with $FF), $0432 squared distances, $0464 directions,
|
|
; scanResultCount zp_2F = index of the last entry ($FF = none), fireTargetDistance $91F1 = range,
|
|
; scanUnitTerrain/$9227, scanUnitType/$9228, scanUnitTerrainHeight/$9229; self-modifies $35F9
|
|
; (range table index) and $362B (distance limit)
|
|
; Called from: the boomer target search ($3C8A) and the two scan passes of processUnitStep ($43AA
|
|
; spotting, $43DC attack targets)
|
|
; ----------------------------------------------------------------------
|
|
scanVisibleEnemies:
|
|
stx scratch20 ; 35C3 zp_20 = pass flag: bit 7 clear = who can I see, bit 7 set = who can I shoot at
|
|
sty scratch21 ; 35C5 zp_21 = the unit doing the scanning
|
|
lda unitTypeTable,y ; 35C7 $F76C[unit] = type byte
|
|
and #$07 ; 35CA unit type 0-4 (GRUNT, RIDER, BOOMER, SPY, COMCEN)
|
|
sta scanUnitType ; 35CC $9228 = type of the scanning unit, used by the spy/hidden rules below
|
|
bit scratch20 ; 35CF which pass is this?
|
|
bpl L_35D6 ; 35D1 spotting pass uses sightRangeByType entries 0-4
|
|
clc ; 35D3 clear carry before shifting to the attack half of the table
|
|
adc #$05 ; 35D4 attack pass uses entries 5-9
|
|
L_35D6:
|
|
sta L_35F8+1 ; 35D6 patch the LDX # at $35F8 with that table index
|
|
jsr setScanFilterOppositeSide; 35D9 $378B: set the collectUnitsInRadius filter to 'units of the other side only'
|
|
lda unitTerrainUnderTable,y ; 35DC $FAF0[unit] = the terrain tile hidden under the unit
|
|
sta scanUnitTerrain ; 35DF $9227 = that tile, kept for the callers
|
|
tax ; 35E2 index it
|
|
lda D_077E,x ; 35E3 $077E[tile] = terrain class 0-7
|
|
tax ; 35E6 index the class tables with it
|
|
lda terrainHeightTable,x ; 35E7 $38B3[class] = height, used by the line-of-sight walk
|
|
sta scanUnitTerrainHeight ; 35EA $9229 = height the observer is standing at
|
|
lda terrainSightBonusTable,x; 35ED extra range for standing on high ground
|
|
beq L_35F7 ; 35F0 flat ground: no bonus
|
|
bit scratch20 ; 35F2 which pass?
|
|
bmi L_35F7 ; 35F4 attack pass: the bonus counts once
|
|
asl a ; 35F6 spotting pass: high ground doubles the terrain bonus
|
|
L_35F7:
|
|
clc ; 35F7 prepare the addition
|
|
L_35F8:
|
|
ldx #$FF ; 35F8 range table index, patched at $35D6
|
|
adc sightRangeByType,x ; 35FA base range for this unit type and pass
|
|
cmp #$0A ; 35FD 10 cells is the hard limit
|
|
bcc L_3603 ; 35FF below it
|
|
lda #$0A ; 3601 clamp to 10
|
|
L_3603:
|
|
sta fireTargetDistance ; 3603 $91F1 = effective range of this scan
|
|
tax ; 3606 index the squared-distance table with it
|
|
lda rangeDistanceSqTable,x ; 3607 r*(r+1) = the squared distance a target must stay below
|
|
sta L_362A+1 ; 360A patch the CMP # at $362A with that limit
|
|
ldx unitColTable,y ; 360D $F640[unit] = the scanner's column
|
|
lda unitRowTable,y ; 3610 $F6A4[unit] = its row
|
|
tay ; 3613 Y = centre row
|
|
lda fireTargetDistance ; 3614 half width of the square to search = the range
|
|
jsr collectUnitsInRadius ; 3617 $37AA: gather the enemy units in that square into $0400 / $0432
|
|
stx scanResultCount ; 361A zp_2F = index of the last candidate ($FF if none)
|
|
stx scratch18 ; 361C zp_18 = index of the candidate being examined
|
|
bpl L_3623 ; 361E at least one candidate
|
|
jmp scanCandidatesDone ; 3620 nothing in range: return with zp_2F = $FF
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Candidate loop: zp_18 walks the list from the last entry down to entry 0. A candidate that fails any
|
|
; test is struck off at $3735 by writing $FF into $0400[n].
|
|
; ----------------------------------------------------------------------
|
|
L_3623:
|
|
lda checksumXorConstant,x ; 3623 $0400[n] = index of the candidate unit (also trainerPlaybook0200:play3Col12)
|
|
tay ; 3626 Y = candidate unit
|
|
lda play3Row12,x ; 3627 $0432[n] = squared distance from the scanner
|
|
L_362A:
|
|
cmp #$FF ; 362A patched at $360A with r*(r+1) for the effective range
|
|
bcc L_3631 ; 362C inside the circle
|
|
L_362E:
|
|
jmp rejectScanCandidate ; 362E outside it: strike this candidate off the list
|
|
|
|
L_3631:
|
|
ldx scratch21 ; 3631 the scanning unit
|
|
jsr initLineBetweenUnits ; 3633 $375D: set up the line walker from scanner to candidate; A = direction 0-3
|
|
ldx scratch18 ; 3636 candidate index again
|
|
sta play4Grp12,x ; 3638 $0464[n] = direction of this candidate from the scanner
|
|
cmp targetDirection ; 363B zp_6B = the direction the unit prefers (where its current target is)
|
|
beq L_3657 ; 363D same direction: no penalty
|
|
lda scanResultCount ; 363F how many candidates were found
|
|
cmp #$01 ; 3641 index 0 means a single candidate
|
|
bcc L_3657 ; 3643 only one candidate: no penalty, take it whatever the direction
|
|
lda play3Row12,x ; 3645 squared distance again
|
|
cmp #$03 ; 3648 squared distance below 3 = an adjacent or diagonal cell
|
|
bcc L_3657 ; 364A point blank: no penalty
|
|
clc ; 364C otherwise...
|
|
adc #$01 ; 364D ...count it as one step further away than it is
|
|
sta play3Row12,x ; 364F store the penalised distance back
|
|
cmp L_362A+1 ; 3652 did the penalty push it out of range?
|
|
bcs L_362E ; 3655 yes: strike it off
|
|
L_3657:
|
|
lda unitTypeTable,y ; 3657 $F76C[candidate] = type byte
|
|
bmi L_362E ; 365A bit 7 = destroyed: strike it off
|
|
and #$07 ; 365C candidate type 0-4
|
|
bit scratch20 ; 365E which pass?
|
|
bpl L_3671 ; 3660 spotting pass: apply the spy and hidden-unit rules
|
|
lda unitEnergyTable,y ; 3662 attack pass: $F9C4[candidate] bit 7 = the target is currently visible
|
|
bpl L_362E ; 3665 you cannot shoot at something you have not spotted
|
|
lda L_35F8+1 ; 3667 the range table index of this scan
|
|
cmp #$07 ; 366A index 7 = BOOMER attacking; 5 and 6 are GRUNT and RIDER
|
|
bcc beginLineOfSightWalk ; 366C grunts and riders must have a clear line of sight
|
|
jmp nextScanCandidate ; 366E boomers, spies and comcens fire without one: accept this candidate
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Spotting pass only (scan mode zp_20 bit 7 clear): the two kinds of target that need more than plain
|
|
; range. A = candidate unit type 0-4, X = its index in the candidate lists, Y = its unit index, zp_18
|
|
; = the same list index.
|
|
; ----------------------------------------------------------------------
|
|
L_3671:
|
|
cmp #$03 ; 3671 is the candidate a SPY (type 3)?
|
|
bne L_3685 ; 3673 no - fall through to the cloak rule
|
|
lda scanUnitType ; 3675 type of the unit that is doing the looking
|
|
cmp #$03 ; 3678 only a SPY can see another SPY
|
|
bne L_362E ; 367A anything else never notices it -> drop the candidate
|
|
lda play3Row12,x ; 367C squared distance from the $0432 list
|
|
cmp #$14 ; 367F a spy is visible only inside dist^2 20
|
|
bcs L_362E ; 3681 farther away -> drop the candidate
|
|
bcc beginLineOfSightWalk ; 3683 close enough, but it still needs a clear line of sight (always taken, C=0 here)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A cloaked unit ($F898 bit 7, set by the CLOAKING ON command) can only be made out from close up, and
|
|
; a SPY has twice the range for it.
|
|
; ----------------------------------------------------------------------
|
|
L_3685:
|
|
lda unitPathColTable,y ; 3685 cloak flag of the candidate ($F898 bit 7)
|
|
bpl beginLineOfSightWalk ; 3688 not cloaked -> ordinary line-of-sight test
|
|
lda play3Row12,x ; 368A squared distance to the cloaked unit
|
|
cmp #$0C ; 368D nobody sees a cloaked unit beyond dist^2 12
|
|
bcs L_362E ; 368F too far -> drop the candidate
|
|
ldy scanUnitType ; 3691 type of the spotter (this overwrites the candidate index in Y; $36AC reloads it)
|
|
cpy #$03 ; 3694 is the spotter itself a SPY?
|
|
beq beginLineOfSightWalk ; 3696 a spy detects cloaked units out to the full dist^2 12
|
|
cmp #$06 ; 3698 everyone else only inside dist^2 6 (A still holds the distance)
|
|
bcs L_362E ; 369A too far for a non-spy -> drop the candidate
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Line-of-sight walk. The line from the spotter to the candidate was set up by initLineBetweenUnits
|
|
; at $3633; zp_1E carries the terrain height of the previous cell and zp_1F the ridge state, both used
|
|
; by checkLineOfSightStep.
|
|
; ----------------------------------------------------------------------
|
|
beginLineOfSightWalk:
|
|
lda #$80 ; 369C $80 = the ground has not risen yet along this line
|
|
sta scratch1F ; 369E reset the ridge tracker
|
|
lda scanUnitTerrainHeight ; 36A0 terrain height under the spotter itself
|
|
sta scratch1E ; 36A3 the height walk starts at the spotter's own level
|
|
losWalkNextCell:
|
|
jsr stepLine ; 36A5 advance the line by one map cell
|
|
bne L_36F7 ; 36A8 steps left -> this is a cell between the two units
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The line has reached the candidate's own cell: forest cover and one last height step.
|
|
; ----------------------------------------------------------------------
|
|
ldx scratch18 ; 36AA index of the candidate in the scan lists
|
|
ldy checksumXorConstant,x ; 36AC its unit index (the $0400 candidate list lies over the boot checksum table) (also trainerPlaybook0200:play3Col12)
|
|
ldx unitTerrainUnderTable,y ; 36AF map byte hidden underneath the candidate ($FAF0)
|
|
lda D_077E,x ; 36B2 -> terrain class 0-8 (the table is addressed with the raw cell byte)
|
|
cmp #$02 ; 36B5 class 2 = forest
|
|
bne L_36C7 ; 36B7 open ground -> only the height test is left
|
|
lda unitEnergyTable,y ; 36B9 energy byte; bit 7 = the unit is already visible
|
|
bmi L_36C7 ; 36BC a unit that has been seen once stays visible even in the trees
|
|
ldx scratch18 ; 36BE back to the candidate's list index
|
|
lda play3Row12,x ; 36C0 its squared distance
|
|
cmp #$06 ; 36C3 inside dist^2 6 the trees do not hide it
|
|
bcs rejectScanCandidate ; 36C5 farther away and in forest -> not visible
|
|
L_36C7:
|
|
ldx unitTerrainUnderTable,y ; 36C7 terrain byte under the candidate again (X was reused above)
|
|
lda D_077E,x ; 36CA -> terrain class
|
|
tax ; 36CD checkLineOfSightStep wants the class in X
|
|
jsr checkLineOfSightStep ; 36CE last height step: does a ridge stand between us and the target?
|
|
bcs rejectScanCandidate ; 36D1 blocked -> not visible
|
|
cpy #$32 ; 36D3 unit 50 = first unit of side 1: is the candidate one of the local player's units?
|
|
D_36D5:
|
|
bcc nextScanCandidate ; 36D5 branch opcode patched per side by overlay A ($7BD8: $90 BCC for side 0, $B0 BCS for side 1) - our own unit, accept it without a message
|
|
lda unitEnergyTable,y ; 36D7 an enemy: bit 7 = it was already visible
|
|
bmi nextScanCandidate ; 36DA not a first sighting -> no message
|
|
lda unitTypeTable,y ; 36DC candidate's type byte
|
|
and #$07 ; 36DF strip the dead/dug-in bits
|
|
cmp #$03 ; 36E1 only SPY (3) and COMCEN (4) are announced
|
|
bcc nextScanCandidate ; 36E3 grunt/rider/boomer -> accept silently
|
|
bne L_36EB ; 36E5 not a spy -> test for the comcen
|
|
lda #$0F ; 36E7 message $0F 'SPOTTED ENEMY SPY!'
|
|
bne L_36F1 ; 36E9 (always taken)
|
|
L_36EB:
|
|
cmp #$04 ; 36EB type 4 = COMCEN?
|
|
bne nextScanCandidate ; 36ED neither -> accept silently
|
|
lda #$0D ; 36EF message $0D 'SPOTTED ENEMY COMCEN!'
|
|
L_36F1:
|
|
jsr queueMessageUnlessFilm ; 36F1 queue it (dropped during solo/film playback)
|
|
jmp nextScanCandidate ; 36F4 candidate accepted, on to the next one
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; An intermediate cell of the line. A unit standing in it can stop a shot but never blocks plain
|
|
; spotting; forest and a ridge stop both.
|
|
; ----------------------------------------------------------------------
|
|
L_36F7:
|
|
ldx lineCurCol ; 36F7 column the line walker has reached
|
|
ldy lineCurRow ; 36F9 and its row
|
|
jsr readMapCell ; 36FB read that map cell (N set = a unit stands there)
|
|
bpl L_3724 ; 36FE bare terrain -> straight to the height test
|
|
tay ; 3700 unit cell: Y = $80|index, which the base-$80 alias tables undo
|
|
lda scratch20 ; 3701 scan mode
|
|
bpl L_3721 ; 3703 spotting pass: units in the way never block the view
|
|
lda unitTypeTableAlias,y ; 3705 attack pass: type of the unit standing in the line of fire ($F76C-$80)
|
|
and #$07 ; 3708 strip the dead/dug-in bits
|
|
cmp #$03 ; 370A type 3 = SPY
|
|
beq L_3721 ; 370C a spy is too small to stop a shot
|
|
lda unitFlagsTableAlias,y ; 370E flags of the blocking unit ($F708-$80)
|
|
ldx scratch21 ; 3711 the unit that wants to fire
|
|
eor unitFlagsTable,x ; 3713 compare the two flag bytes
|
|
and #$20 ; 3716 bit 5 = side
|
|
bne rejectScanCandidate ; 3718 an enemy unit in the way stops the shot
|
|
lda scanUnitTerrainHeight ; 371A one of ours in the way: are we firing from high ground?
|
|
bne L_3721 ; 371D yes -> shoot over his head
|
|
beq rejectScanCandidate ; 371F on the flat a friendly unit blocks the shot (always taken)
|
|
L_3721:
|
|
lda unitTerrainUnderAlias,y ; 3721 terrain byte hidden under that unit ($FAF0-$80)
|
|
L_3724:
|
|
tax ; 3724 map byte -> table index
|
|
lda D_077E,x ; 3725 -> terrain class of this cell
|
|
tax ; 3728 checkLineOfSightStep wants the class in X
|
|
cpx #$02 ; 3729 class 2 = forest
|
|
beq rejectScanCandidate ; 372B woods in the way cut the line completely
|
|
jsr checkLineOfSightStep ; 372D feed this cell's height into the ridge test
|
|
bcs rejectScanCandidate ; 3730 a ridge cuts the line -> candidate not visible
|
|
jmp losWalkNextCell ; 3732 keep walking toward the target
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Candidate rejected: $FF in its slot of the $0400 list tells every reader to ignore the entry.
|
|
; $373C is the tail of the per-candidate loop whose head is at $3623.
|
|
; ----------------------------------------------------------------------
|
|
rejectScanCandidate:
|
|
ldx scratch18 ; 3735 index of the candidate
|
|
lda #$FF ; 3737 $FF = 'no unit'
|
|
sta checksumXorConstant,x ; 3739 strike it off the candidate list (also trainerPlaybook0200:play3Col12)
|
|
nextScanCandidate:
|
|
dec scratch18 ; 373C the list is walked backwards
|
|
ldx scratch18 ; 373E reload the index
|
|
bmi scanCandidatesDone ; 3740 index went negative -> every candidate has been tested
|
|
jmp L_3623 ; 3742 test the next candidate
|
|
|
|
scanCandidatesDone:
|
|
rts ; 3745 back to the caller (the per-unit update loop or selectBoomerTarget)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkLineOfSightStep - Feeds the terrain class of one more cell into the running line-of-sight test.
|
|
; It compares the height of that cell with the height of the previous one: a rise is remembered in
|
|
; zp_1F, and a fall after a rise means a ridge stands between the two units and hides the target.
|
|
; Equal heights, or a fall while the line has never risen (simply looking downhill), leave the line
|
|
; clear.
|
|
; In: X = terrain class 0-7 of the next cell, zp_1E = height of the previous cell, zp_1F = $80 until
|
|
; the line has gone uphill
|
|
; Out: C = 1 blocked / C = 0 still clear, zp_1E = height of this cell, zp_1F = size of the rise; A and
|
|
; X clobbered
|
|
; Called from: scanVisibleEnemies ($36CE for the target's own cell, $372D for each cell in between)
|
|
; ----------------------------------------------------------------------
|
|
checkLineOfSightStep:
|
|
lda terrainHeightTable,x ; 3746 height 0-4 of this terrain class
|
|
tax ; 3749 keep it as the new 'previous height'
|
|
sec ; 374A prepare the subtraction
|
|
sbc scratch1E ; 374B difference to the height of the previous cell
|
|
beq L_3755 ; 374D same level -> nothing changes, the line stays clear
|
|
stx scratch1E ; 374F remember this cell's height for the next step
|
|
bcc L_3757 ; 3751 borrow: the ground drops here
|
|
sta scratch1F ; 3753 the ground rises: record the rise, which clears the $80 'no rise yet' marker
|
|
L_3755:
|
|
clc ; 3755 C = 0: the line of sight is still clear
|
|
rts ; 3756 back to the walk
|
|
|
|
L_3757:
|
|
lda scratch1F ; 3757 the ground is dropping - had it risen before?
|
|
bmi L_3755 ; 3759 still $80: we are only looking downhill, and that hides nothing
|
|
sec ; 375B a drop after a rise = a ridge in between
|
|
rts ; 375C C = 1: the target is hidden
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initLineBetweenUnits - Points the line walker at unit Y's cell and falls into initLineFromUnit,
|
|
; which sets the start to unit X's cell. Together they are the standard 'walk from unit X to unit Y'
|
|
; set-up.
|
|
; In: X = unit the line starts at, Y = unit the line ends at
|
|
; Out: zp_94/zp_95 = target row/column plus everything initLineFromUnit produces (A = direction code
|
|
; 0-3, line walker state zp_8C-zp_97)
|
|
; Called from: scanVisibleEnemies ($3633)
|
|
; ----------------------------------------------------------------------
|
|
initLineBetweenUnits:
|
|
lda unitColTable,y ; 375D column of the far unit
|
|
sta lineEndCol ; 3760 line walker target column
|
|
lda unitRowTable,y ; 3762 row of the far unit
|
|
sta lineEndRow ; 3765 line walker target row (falls into initLineFromUnit)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initLineFromUnit - Sets the line walker start to unit X's cell, runs initLineStepper ($FBB8) and
|
|
; reports the principal direction of the line: 0 = north or 1 = south for a row-major line, 2 = east
|
|
; or 3 = west for a column-major one. That value is the unit facing code kept in bits 0-1 of $F708
|
|
; (stored at $4464 and used by the combat facing bonus).
|
|
; In: X = unit the line starts at, zp_94/zp_95 = target row/column
|
|
; Out: A = direction code 0-3, line walker state zp_8C-zp_97 set up (X = 1 for a row-major line)
|
|
; Called from: initLineBetweenUnits (falls through), $428E where a unit is turned toward its
|
|
; destination
|
|
; ----------------------------------------------------------------------
|
|
initLineFromUnit:
|
|
lda unitColTable,x ; 3767 column of the unit the line starts at
|
|
sta lineCurCol ; 376A line walker start column
|
|
lda unitRowTable,x ; 376C its row
|
|
sta lineCurRow ; 376F line walker start row
|
|
jsr initLineStepper ; 3771 set up the Bresenham walk; it returns X = 1 when the row is the major axis
|
|
cpx #$01 ; 3774 was the line mostly vertical?
|
|
bne L_3782 ; 3776 no, it is mostly horizontal -> report east or west
|
|
lda #$00 ; 3778 facing 0 = north (up)
|
|
bit lineStepRow ; 377A row step is $01 (down) or $FF (up)
|
|
bmi L_378A ; 377C stepping up -> north
|
|
lda #$01 ; 377E facing 1 = south (down)
|
|
bne L_378A ; 3780 (always taken)
|
|
L_3782:
|
|
lda #$02 ; 3782 facing 2 = east (right)
|
|
bit lineStepCol ; 3784 column step is $01 (right) or $FF (left)
|
|
bpl L_378A ; 3786 stepping right -> east
|
|
lda #$03 ; 3788 facing 3 = west (left)
|
|
L_378A:
|
|
rts ; 378A A = direction/facing code 0-3
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setScanFilterOppositeSide - Configures the unit filter of collectUnitsInRadius so that only units of
|
|
; the side opposite to unit Y are collected, by writing the compare value and the branch opcode of the
|
|
; test at $37FE/$3800. A side 0 scanner (unit < 50) keeps the units >= 50 with a BCC, a side 1
|
|
; scanner keeps the units < 50 with a BCS.
|
|
; In: Y = the reference unit whose enemies are wanted
|
|
; Out: scanSideFilterCompare+1 = $32 and scanSideFilterBranch = $90 (BCC) or $B0 (BCS); A clobbered
|
|
; Called from: scanVisibleEnemies ($35D9), checkEnemyRecyclerSpotted ($3F64)
|
|
; ----------------------------------------------------------------------
|
|
setScanFilterOppositeSide:
|
|
lda #$32 ; 378B unit 50 = the first unit of side 1
|
|
sta scanSideFilterCompare+1 ; 378D patch the CMP operand of the scan filter
|
|
lda #$90 ; 3790 $90 = BCC, i.e. skip everything below unit 50
|
|
cpy #$32 ; 3792 is the reference unit itself on side 0?
|
|
bcc L_3798 ; 3794 yes -> keep the units of side 1
|
|
eor #$20 ; 3796 side 1 scanner: $90 EOR $20 = $B0 = BCS, keep the units of side 0
|
|
L_3798:
|
|
sta scanSideFilterBranch ; 3798 patch the branch opcode of the scan filter
|
|
rts ; 379B the filter is now set for the next collectUnitsInRadius
|
|
|
|
|
|
; Leftover of an older build: as code these two bytes are 'bcc $3798', the tail of an earlier version
|
|
; of the side test. Overlay A's per-side patcher still stores its branch opcode here ($7BDB), where
|
|
; it now has no effect at all.
|
|
leftoverScanSideBranch:
|
|
.byte $90,$FA ; 379C .. $90 $FA = the dead 'bcc $3798' of an earlier layout
|
|
|
|
; ----------------------------------------------------------------------
|
|
; collectAllUnitsInRadiusDead - Unreferenced alternate entry of collectUnitsInRadius that turns the
|
|
; side filter into 'accept every unit' (compare against unit 0 with a BCC) before falling into the
|
|
; scan. Nothing calls it - ageUnitVisibility ($357D) writes the same two bytes inline - so this is
|
|
; leftover code from an earlier build.
|
|
; In: A = radius, X = centre column, Y = centre row
|
|
; Out: dead code; it would leave scanSideFilterCompare+1 = 0 and scanSideFilterBranch = $90
|
|
; Called from: nothing (dead code, rendered as .byte in the listing)
|
|
; ----------------------------------------------------------------------
|
|
collectAllUnitsInRadiusDead:
|
|
pha ; 379E keep the radius across the patching
|
|
lda #$00 ; 379F compare the unit index against 0 ...
|
|
sta scanSideFilterCompare+1 ; 37A1 -> the compare operand of the scan filter
|
|
lda #$90 ; 37A4 ... with a BCC, so no unit is ever skipped
|
|
sta scanSideFilterBranch ; 37A6 -> the branch opcode of the scan filter
|
|
pla ; 37A9 radius back into A and fall into the scan
|
|
|
|
; ----------------------------------------------------------------------
|
|
; collectUnitsInRadius - Scans the square of half-width A around map cell (X,Y), clipped to the 40x40
|
|
; battlefield, and lists every cell holding a unit that passes the side filter patched by
|
|
; setScanFilterOppositeSide: the unit index goes into $0400[n] and its squared distance from the
|
|
; centre into $0432[n]. When more than one unit was found, side 1 reverses the lists so that both
|
|
; machines break distance ties identically.
|
|
; In: A = radius in cells, X = centre column, Y = centre row, playerSide $0B9F, the map at $F000
|
|
; Out: candidate lists $0400/$0432, X = zp_29 = index of the last entry ($FF = nothing found);
|
|
; zp_22-zp_29 and zp_48/zp_49 used as scratch; operand scanReverseLimit+1 patched
|
|
; Called from: scanVisibleEnemies ($3617), checkEnemyRecyclerSpotted ($3F72)
|
|
; ----------------------------------------------------------------------
|
|
collectUnitsInRadius:
|
|
sta cellColA ; 37AA keep the radius (zp_22 becomes the running column further down)
|
|
asl a ; 37AC 2 * radius
|
|
sta scanColCount ; 37AD width of the scan square (the +1 comes at $37DA)
|
|
sta scanRowCount ; 37AF and its height
|
|
stx cellColB ; 37B1 centre column = point B of distanceSquared
|
|
sty cellRowB ; 37B3 centre row
|
|
txa ; 37B5 centre column back into A
|
|
sec ; 37B6 prepare the subtraction
|
|
sbc cellColA ; 37B7 leftmost column = centre - radius
|
|
bcs L_37C3 ; 37B9 still on the map -> use it
|
|
txa ; 37BB clipped at the left edge: the square still ends at centre + radius ...
|
|
clc ; 37BC prepare the addition
|
|
adc cellColA ; 37BD centre + radius
|
|
sta scanColCount ; 37BF ... so that is all the width there is left
|
|
lda #$00 ; 37C1 and the scan starts at column 0
|
|
L_37C3:
|
|
sta scratch23 ; 37C3 first column of every scanned row
|
|
tax ; 37C5 -> X for readMapCell
|
|
tya ; 37C6 centre row back into A
|
|
sec ; 37C7 prepare the subtraction
|
|
sbc cellColA ; 37C8 topmost row = centre - radius (zp_22 still holds the radius)
|
|
bcs L_37D4 ; 37CA still on the map -> use it
|
|
tya ; 37CC clipped at the top edge ...
|
|
clc ; 37CD prepare the addition
|
|
adc cellColA ; 37CE centre + radius
|
|
sta scanRowCount ; 37D0 ... so there are fewer rows to scan
|
|
lda #$00 ; 37D2 and the scan starts at row 0
|
|
L_37D4:
|
|
sta cellRowA ; 37D4 row the scan starts on
|
|
tay ; 37D6 -> Y for readMapCell
|
|
jsr readMapCell ; 37D7 zp_48/49 = address of the top left cell of the square, Y = 0
|
|
inc scanColCount ; 37DA width = 2*radius+1 cells
|
|
inc scanRowCount ; 37DC height = 2*radius+1 rows
|
|
lda #$FF ; 37DE $FF ...
|
|
sta scanListIndex ; 37E0 ... = the candidate list is still empty
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Row loop: one map row of the square per pass; zp_48/49 walks down the map 40 bytes at a time.
|
|
; ----------------------------------------------------------------------
|
|
L_37E2:
|
|
lda cellRowA ; 37E2 row about to be scanned
|
|
bmi L_3818 ; 37E4 negative row -> nothing to do here
|
|
cmp #$28 ; 37E6 the battlefield is 40 rows deep
|
|
bcs L_3818 ; 37E8 past the bottom edge -> skip the row
|
|
ldy #$00 ; 37EA Y = offset from the start of the map row
|
|
lda scratch23 ; 37EC first column of the square
|
|
sta cellColA ; 37EE restart at the left edge of the square
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Column loop over one row of the square.
|
|
; ----------------------------------------------------------------------
|
|
L_37F0:
|
|
lda cellColA ; 37F0 column about to be tested
|
|
bmi L_3811 ; 37F2 off the left edge of the map
|
|
cmp #$28 ; 37F4 40 columns
|
|
bcs L_3811 ; 37F6 off the right edge
|
|
lda (mapCellPtr),y ; 37F8 the map byte of this cell
|
|
bpl L_3811 ; 37FA bit 7 clear = plain terrain, no unit here
|
|
and #$7F ; 37FC bit 7 is the 'unit here' marker; the rest is the unit index 0-99
|
|
scanSideFilterCompare:
|
|
cmp #$FF ; 37FE operand patched by setScanFilterOppositeSide ($32) or by the dead entry above (0)
|
|
scanSideFilterBranch:
|
|
bcc L_3811 ; 3800 opcode patched BCC/BCS: drop the units of the unwanted side
|
|
inc scanListIndex ; 3802 one more candidate
|
|
ldx scanListIndex ; 3804 its slot number
|
|
sta checksumXorConstant,x ; 3806 unit index -> the $0400 candidate list (also trainerPlaybook0200:play3Col12)
|
|
jsr distanceSquared ; 3809 dist^2 between this cell (zp_22/zp_24) and the centre (zp_27/zp_28)
|
|
ldx scanListIndex ; 380C same slot
|
|
sta play3Row12,x ; 380E -> the parallel $0432 distance list
|
|
L_3811:
|
|
inc cellColA ; 3811 next column
|
|
iny ; 3813 next byte of the map row
|
|
cpy scanColCount ; 3814 whole width of the square done?
|
|
bcc L_37F0 ; 3816 no -> next cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; End of a row: step the map pointer down to the next one.
|
|
; ----------------------------------------------------------------------
|
|
L_3818:
|
|
lda mapCellPtr ; 3818 map row pointer, low byte
|
|
clc ; 381A prepare the addition
|
|
adc #$28 ; 381B 40 bytes per map row
|
|
sta mapCellPtr ; 381D store it back
|
|
lda mapCellPtrHi ; 381F high byte
|
|
adc #$00 ; 3821 propagate the carry
|
|
sta mapCellPtrHi ; 3823 zp_48/49 now points at the next row of the square
|
|
inc cellRowA ; 3825 next row number
|
|
dec scanRowCount ; 3827 one row less to do
|
|
bne L_37E2 ; 3829 until the whole square has been scanned
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Side 1 reverses both lists. The two machines see the battlefield mirrored and therefore scan the
|
|
; square in opposite directions; reversing makes them pick the same unit whenever two candidates tie
|
|
; on distance.
|
|
; ----------------------------------------------------------------------
|
|
ldx scanListIndex ; 382B index of the last entry
|
|
bmi L_385D ; 382D nothing found at all
|
|
beq L_385D ; 382F only one entry - nothing to reverse
|
|
lda playerSide ; 3831 0 = the local player is side 0, 1 = side 1
|
|
beq L_385D ; 3834 side 0 keeps the natural scan order
|
|
ldy #$00 ; 3836 Y walks up from the front while X walks down from the back
|
|
L_3838:
|
|
lda checksumXorConstant,y ; 3838 swap the two unit slots ... (also trainerPlaybook0200:play3Col12)
|
|
pha ; 383B keep the front entry
|
|
lda checksumXorConstant,x ; 383C back entry (also trainerPlaybook0200:play3Col12)
|
|
sta checksumXorConstant,y ; 383F -> to the front (also trainerPlaybook0200:play3Col12)
|
|
pla ; 3842 front entry back off the stack
|
|
sta checksumXorConstant,x ; 3843 -> to the back (also trainerPlaybook0200:play3Col12)
|
|
lda play3Row12,y ; 3846 ... and the two matching distances
|
|
pha ; 3849 keep the front distance
|
|
lda play3Row12,x ; 384A back distance
|
|
sta play3Row12,y ; 384D -> to the front
|
|
pla ; 3850 front distance back off the stack
|
|
sta play3Row12,x ; 3851 -> to the back
|
|
iny ; 3854 next pair from the front
|
|
dex ; 3855 next pair from the back
|
|
stx scanReverseLimit+1 ; 3856 patch the loop limit with the index that is walking down
|
|
scanReverseLimit:
|
|
cpy #$FF ; 3859 operand = X
|
|
bcc L_3838 ; 385B keep swapping until the two indexes meet
|
|
L_385D:
|
|
ldx scanListIndex ; 385D X = index of the last entry, $FF when nothing was found
|
|
rts ; 385F back to the caller
|
|
|
|
|
|
; squaresTable - n*n for n = 0..15. distanceSquared uses it forwards; cellDistance in the opponent
|
|
; module ($E832) and the map generator ($7831) use it backwards as a square root.
|
|
squaresTable:
|
|
.byte $00,$01,$04,$09,$10,$19,$24,$31; 3860 ......$1 0,1,4,9,16,25,36,49 = n*n for n = 0-7
|
|
.byte $40 ; 3868 64,81,100,121,144,169,196,225 = n*n for n = 8-15
|
|
.byte $51,$64,$79,$90,$A9,$C4,$E1; 3869 Qdy....
|
|
|
|
; ----------------------------------------------------------------------
|
|
; distanceSquared - Squared distance between two map cells: |dCol|^2 + |dRow|^2 taken from
|
|
; squaresTable. Each delta is clamped to 15 cells and the sum saturates at $FF, which makes it a
|
|
; usable range metric everywhere on the 40x40 map without a square root.
|
|
; In: zp_22/zp_24 = column/row of the cell, zp_27/zp_28 = column/row of the reference point
|
|
; Out: A = squared distance ($FF when it overflows); X clobbered
|
|
; Called from: collectUnitsInRadius ($3809), the target cursor range check ($2159), cellDistance in
|
|
; the opponent module ($E82A)
|
|
; ----------------------------------------------------------------------
|
|
distanceSquared:
|
|
lda cellColA ; 3870 column of the cell
|
|
sec ; 3872 prepare the subtraction
|
|
sbc cellColB ; 3873 minus the reference column
|
|
bpl L_387C ; 3875 already positive
|
|
eor #$FF ; 3877 negate to get |dCol| ...
|
|
clc ; 3879 prepare the addition
|
|
adc #$01 ; 387A ... two's complement
|
|
L_387C:
|
|
tax ; 387C -> table index
|
|
cpx #$10 ; 387D squaresTable only covers 0-15
|
|
bcc L_3883 ; 387F in range
|
|
ldx #$0F ; 3881 clamp the distance at 15 cells
|
|
L_3883:
|
|
lda squaresTable,x ; 3883 dCol^2
|
|
pha ; 3886 hold on to it while the row delta is worked out
|
|
lda cellRowA ; 3887 row of the cell
|
|
sec ; 3889 prepare the subtraction
|
|
sbc cellRowB ; 388A minus the reference row
|
|
bpl L_3893 ; 388C already positive
|
|
eor #$FF ; 388E negate to get |dRow| ...
|
|
clc ; 3890 prepare the addition
|
|
adc #$01 ; 3891 ... two's complement
|
|
L_3893:
|
|
tax ; 3893 -> table index
|
|
cpx #$10 ; 3894 again only 0-15 are tabulated
|
|
bcc L_389A ; 3896 in range
|
|
ldx #$0F ; 3898 clamp at 15 cells
|
|
L_389A:
|
|
pla ; 389A dCol^2 back off the stack
|
|
clc ; 389B prepare the addition
|
|
adc squaresTable,x ; 389C + dRow^2
|
|
bcc L_38A3 ; 389F no overflow
|
|
lda #$FF ; 38A1 saturate at $FF instead of wrapping round
|
|
L_38A3:
|
|
rts ; 38A3 A = dCol^2 + dRow^2
|
|
|
|
|
|
; baseDamageTable - base damage of one shot: 3 rows of 5 bytes, row = attacker type (0 GRUNT, 1 RIDER,
|
|
; 2 BOOMER; spies and comcens never fire) and column = target type (0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY,
|
|
; 4 COMCEN). Read as baseDamageTable[attackerRow*5 + targetType] at $38F4.
|
|
baseDamageTable:
|
|
.byte $04,$05,$19,$08,$0C,$03,$04,$14; 38A4 ........ GRUNT vs grunt 4, rider 5, boomer 25, spy 8, comcen 12; RIDER vs grunt 3, rider 4, boomer 20
|
|
.byte $06 ; 38AC RIDER vs spy 6, comcen 8; BOOMER vs grunt 12, rider 16, boomer 8, spy 8, comcen 20
|
|
.byte $08,$0C,$10,$08,$08,$14 ; 38AD ......
|
|
|
|
; terrainHeightTable - height (cover level) of each terrain class 0-7: class 0 blank, 1 river and 2
|
|
; forest are flat, 3 lower contour = 1, 4 hill = 2, 5 upper contour = 3, 6 summit = 4, 7 recycler = 0.
|
|
; Used by the line-of-sight ridge test and by the combat height bonus.
|
|
terrainHeightTable:
|
|
.byte $00,$00,$00,$01,$02,$03,$04,$00; 38B3 ........ heights of terrain classes 0-7: 0,0,0,1,2,3,4,0
|
|
|
|
; combatScratch - the seven work bytes of resolveAttack, held inside the code: target unit, attacker
|
|
; unit, the running damage total, and the four map coordinates of the shot. The coordinates have to
|
|
; be saved because overlay B's drawShotLineInViewport destroys zp_92-zp_95.
|
|
combatScratch:
|
|
.byte $00 ; 38BB . the unit being shot at
|
|
combatAttackerUnit:
|
|
.byte $00 ; 38BC the unit doing the shooting
|
|
combatDamage:
|
|
.byte $00 ; 38BD running damage total; half of it is what actually gets applied
|
|
combatAttackerRow:
|
|
.byte $00 ; 38BE attacker's map row
|
|
combatAttackerCol:
|
|
.byte $00 ; 38BF attacker's map column
|
|
combatTargetRow:
|
|
.byte $00 ; 38C0 target's map row
|
|
combatTargetCol:
|
|
.byte $00 ; 38C1 target's map column
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resolveAttack - Resolves one shot of unit X at unit Y. Only GRUNT/RIDER/BOOMER (types 0-2) carry a
|
|
; weapon. The damage starts at baseDamageTable[attacker][target] and is then modified: a blitzing
|
|
; attacker that has built up a charge ($F708 bit 7) throws up to 15 energy (5 for a BOOMER) into the
|
|
; shot and pays 3/4 of that itself; +2 when the attacker is dug in and -25% when the target is; plus
|
|
; the height difference of the two cells; +10 when the target stands in a river (class 1) and +2 on a
|
|
; contour (class 3 or 5); and for non-BOOMERs +20 when the target faces the same way (shot in the
|
|
; back) or +10 for a flank shot. A zero or negative total becomes 2, and half of the total is applied
|
|
; through applyHitToUnit. If the battlefield screen is up and the comcen display is alive the shot is
|
|
; drawn both in the zoom window and on the overview map, then the shot or kill sound plays.
|
|
; In: X = attacker unit, Y = target unit; the unit record arrays, terrain classes $077E,
|
|
; currentScreen $90FB, comcenStunStatus $922A
|
|
; Out: target damaged via applyHitToUnit, the attacker may lose energy, attackerType $923D,
|
|
; $38BB-$38C1 scratch, shot graphics (sprite 3 and the zoom window) and a sound; the operands
|
|
; loadShotSoundId+1, loadChargeEnergySpent+1 and subtractDugInDiscount+1 are patched
|
|
; Called from: the per-unit update loop ($44A0)
|
|
; ----------------------------------------------------------------------
|
|
resolveAttack:
|
|
stx combatAttackerUnit ; 38C2 remember the attacker
|
|
sty combatScratch ; 38C5 and the target
|
|
lda #$00 ; 38C8 no offset yet ...
|
|
sta scratch1A ; 38CA row offset into baseDamageTable (5 entries per attacker type)
|
|
lda unitTypeTable,x ; 38CC attacker's type byte
|
|
and #$07 ; 38CF 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN
|
|
sta attackerType ; 38D1 $923D, also read by both shot-drawing routines
|
|
cmp #$03 ; 38D4 types 3 and 4 have no weapon
|
|
bcc L_38DB ; 38D6 only grunts, riders and boomers shoot
|
|
jmp L_3A59 ; 38D8 a spy or comcen never fires - nothing happens at all
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pick the row of baseDamageTable that belongs to the attacker's type.
|
|
; ----------------------------------------------------------------------
|
|
L_38DB:
|
|
cmp #$02 ; 38DB BOOMER?
|
|
bne L_38E3 ; 38DD no
|
|
lda #$0A ; 38DF boomer row = table entries 10-14
|
|
sta scratch1A ; 38E1 -> row offset 10
|
|
L_38E3:
|
|
cmp #$01 ; 38E3 RIDER?
|
|
bne L_38EB ; 38E5 no, so it is a GRUNT and the offset stays 0
|
|
lda #$05 ; 38E7 rider row = table entries 5-9
|
|
sta scratch1A ; 38E9 -> row offset 5
|
|
L_38EB:
|
|
lda unitTypeTable,y ; 38EB target's type byte
|
|
and #$07 ; 38EE type 0-4 selects the column
|
|
clc ; 38F0 prepare the addition
|
|
adc scratch1A ; 38F1 + the attacker's row offset
|
|
tax ; 38F3 -> table index
|
|
lda baseDamageTable,x ; 38F4 base damage for this attacker/target pairing
|
|
sta combatDamage ; 38F7 start the running damage total
|
|
lda #$0A ; 38FA sound 10 = ordinary shot
|
|
ldx attackerType ; 38FC attacker type
|
|
cpx #$02 ; 38FF BOOMER?
|
|
bne L_3905 ; 3901 no -> keep sound 10
|
|
lda #$0B ; 3903 sound 11 = boomer shell
|
|
L_3905:
|
|
sta loadShotSoundId+1 ; 3905 patch the LDA # of the sound played when the shot is over
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Blitz charge: a unit with blitz switched on ($F8FC bit 7) collects a one-shot charge flag in
|
|
; $F708 bit 7 while it moves ($3EE2). Firing spends the charge, adding energy to the damage and
|
|
; taking 3/4 of that energy off the attacker itself.
|
|
; ----------------------------------------------------------------------
|
|
ldx combatAttackerUnit ; 3908 the attacker
|
|
lda unitPathRowTable,x ; 390B $F8FC bit 7 = blitz is switched on
|
|
bpl L_3950 ; 390E not blitzing -> no charge bonus
|
|
lda unitFlagsTable,x ; 3910 $F708 bit 7 = a charge was built up by moving
|
|
bpl L_3950 ; 3913 nothing charged up
|
|
and #$7F ; 3915 clear the charge bit ...
|
|
sta unitFlagsTable,x ; 3917 ... it is used up by this shot
|
|
lda #$0F ; 391A up to 15 energy may be thrown into the charge
|
|
ldy scratch1A ; 391C which damage row is in use?
|
|
cpy #$0A ; 391E row offset 10 = BOOMER
|
|
bcc L_3924 ; 3920 grunt or rider
|
|
lda #$05 ; 3922 a boomer only spends 5
|
|
L_3924:
|
|
sta scratch18 ; 3924 the amount we would like to spend
|
|
lda unitEnergyTable,x ; 3926 attacker's energy byte
|
|
and #$3F ; 3929 bits 0-5 = energy 0-50, bits 6-7 are the visibility flags
|
|
cmp scratch18 ; 392B enough energy for the whole charge?
|
|
bcc L_3931 ; 392D no - it cannot spend more than it has
|
|
lda scratch18 ; 392F yes - spend the full amount
|
|
L_3931:
|
|
sta loadChargeEnergySpent+1 ; 3931 patch the amount spent into the LDA # below
|
|
clc ; 3934 prepare the addition
|
|
adc combatDamage ; 3935 the running damage total
|
|
sta combatDamage ; 3938 the whole amount is added to the damage
|
|
ldy combatAttackerUnit ; 393B the attacker itself pays for it
|
|
loadChargeEnergySpent:
|
|
lda #$FF ; 393E operand = the energy just spent
|
|
lsr a ; 3940 /2
|
|
sta scratch18 ; 3941 keep half of it
|
|
lsr a ; 3943 /4
|
|
clc ; 3944 prepare the addition
|
|
adc scratch18 ; 3945 1/2 + 1/4 = 3/4 of the amount spent
|
|
jsr reduceUnitEnergyBy ; 3947 take that off the attacker's own energy
|
|
ldy combatScratch ; 394A Y back to the target
|
|
ldx combatAttackerUnit ; 394D X back to the attacker
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Dug-in modifiers: $F76C bit 6 marks a unit that has finished digging in.
|
|
; ----------------------------------------------------------------------
|
|
L_3950:
|
|
lda unitTypeTable,x ; 3950 attacker's type byte
|
|
and #$40 ; 3953 bit 6 = dug in / fortified
|
|
beq L_395D ; 3955 not dug in
|
|
inc combatDamage ; 3957 +1 ...
|
|
inc combatDamage ; 395A ... +1 = +2 damage when firing from a prepared position
|
|
L_395D:
|
|
lda unitTypeTable,y ; 395D target's type byte
|
|
and #$40 ; 3960 bit 6 = the target is dug in
|
|
beq L_3975 ; 3962 not dug in
|
|
lda combatDamage ; 3964 running damage total
|
|
lsr a ; 3967 /2
|
|
lsr a ; 3968 /4
|
|
sta subtractDugInDiscount+1 ; 3969 patch the subtraction below with damage/4
|
|
lda combatDamage ; 396C the total again
|
|
sec ; 396F prepare the subtraction
|
|
subtractDugInDiscount:
|
|
sbc #$FF ; 3970 operand = damage/4: a dug-in target takes 25% less
|
|
sta combatDamage ; 3972 the reduced total
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Terrain modifiers: the height difference between the two cells, plus a bonus for a target caught in
|
|
; the open (a river) or on a slope.
|
|
; ----------------------------------------------------------------------
|
|
L_3975:
|
|
ldx unitTerrainUnderTable,y ; 3975 map byte hidden under the target
|
|
lda D_077E,x ; 3978 -> terrain class 0-8
|
|
sta scratch1C ; 397B keep the target's terrain class
|
|
tax ; 397D -> height table index
|
|
lda terrainHeightTable,x ; 397E height of the target's cell
|
|
sta scratch1E ; 3981 keep it for the difference below
|
|
ldx combatAttackerUnit ; 3983 the attacker
|
|
ldy unitTerrainUnderTable,x ; 3986 map byte hidden under the attacker
|
|
lda D_077E,y ; 3989 -> terrain class
|
|
sta scratch1D ; 398C stored but never read again - a dead store
|
|
tay ; 398E -> height table index
|
|
lda terrainHeightTable,y ; 398F height of the attacker's cell
|
|
ldy combatScratch ; 3992 Y back to the target unit
|
|
sec ; 3995 prepare the subtraction
|
|
sbc scratch1E ; 3996 attacker height - target height
|
|
clc ; 3998 prepare the addition
|
|
adc combatDamage ; 3999 + the running total
|
|
sta combatDamage ; 399C firing downhill adds, firing uphill subtracts
|
|
lda scratch1C ; 399F terrain class the target is standing on
|
|
cmp #$01 ; 39A1 class 1 = river
|
|
bne L_39B1 ; 39A3 not a river
|
|
lda combatDamage ; 39A5 running damage total
|
|
clc ; 39A8 prepare the addition
|
|
adc #$0A ; 39A9 +10: a target in the water has no cover at all
|
|
sta combatDamage ; 39AB store the total again
|
|
jmp L_39BF ; 39AE the river bonus and the slope bonus are exclusive
|
|
|
|
L_39B1:
|
|
cmp #$03 ; 39B1 class 3 = lower contour
|
|
beq L_39B9 ; 39B3 yes
|
|
cmp #$05 ; 39B5 class 5 = upper contour
|
|
bne L_39BF ; 39B7 any other class gives no bonus
|
|
L_39B9:
|
|
inc combatDamage ; 39B9 +1 ...
|
|
inc combatDamage ; 39BC ... +1 = +2 for a target caught on an open slope
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Facing bonus. Facing codes are 0 north, 1 south, 2 east, 3 west, so EOR 1 gives the opposite
|
|
; direction: the same facing means the target has its back to us, the opposite facing means it is
|
|
; looking straight at us, and the other two are a flank shot. Boomers lob shells and get no bonus at
|
|
; all.
|
|
; ----------------------------------------------------------------------
|
|
L_39BF:
|
|
lda attackerType ; 39BF attacker type
|
|
cmp #$02 ; 39C2 BOOMER?
|
|
beq L_39F0 ; 39C4 artillery gets no facing bonus
|
|
lda unitFlagsTable,x ; 39C6 attacker's facing, bits 0-1 of $F708
|
|
and #$03 ; 39C9 facing bits only
|
|
sta scratch1A ; 39CB keep it
|
|
lda unitFlagsTable,y ; 39CD target's facing
|
|
and #$03 ; 39D0 facing bits only
|
|
cmp scratch1A ; 39D2 both looking the same way?
|
|
bne L_39E1 ; 39D4 no
|
|
lda combatDamage ; 39D6 running damage total
|
|
clc ; 39D9 prepare the addition
|
|
adc #$14 ; 39DA +20: the target has its back to the shooter
|
|
sta combatDamage ; 39DC store the total again
|
|
bne L_39F0 ; 39DF (always taken, the total cannot be 0 here)
|
|
L_39E1:
|
|
eor #$01 ; 39E1 the facing opposite to the target's
|
|
cmp scratch1A ; 39E3 is the target looking straight at the shooter?
|
|
beq L_39F0 ; 39E5 head on -> no bonus
|
|
lda combatDamage ; 39E7 the remaining two facings are a flank shot
|
|
clc ; 39EA prepare the addition
|
|
adc #$0A ; 39EB +10
|
|
sta combatDamage ; 39ED store the total again
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Apply the shot.
|
|
; ----------------------------------------------------------------------
|
|
L_39F0:
|
|
lda combatDamage ; 39F0 final total
|
|
bmi L_39F7 ; 39F3 the modifiers can drive it below zero
|
|
bne L_39F9 ; 39F5 or exactly to zero
|
|
L_39F7:
|
|
lda #$02 ; 39F7 every hit does at least 2 points (before halving)
|
|
L_39F9:
|
|
ldy combatScratch ; 39F9 the target unit
|
|
lsr a ; 39FC only half of the accumulated total is really applied
|
|
jsr applyHitToUnit ; 39FD damage the target, score it and maybe end the game
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Draw the shot, but only while the player is actually looking at the battlefield and the comcen
|
|
; display is alive. Overlay B draws it as fat pixels in the 7x5 zoom window and drawShotLineSprite
|
|
; draws the same shot on the 4-pixel-per-cell overview map.
|
|
; ----------------------------------------------------------------------
|
|
lda currentScreen ; 3A00 0 = the battlefield screen (F1) is the one on show
|
|
bne L_3A4A ; 3A03 radar, drone or menu screen -> no shot graphics
|
|
bit comcenStunStatus ; 3A05 $922A = $FF (so bit 6 is set) while the comcen is knocked out
|
|
bvs L_3A4A ; 3A08 blacked-out display -> draw nothing
|
|
ldy combatScratch ; 3A0A the target
|
|
lda unitColTable,y ; 3A0D its column
|
|
sta combatTargetCol ; 3A10 saved, because the overlay routine destroys zp_92-zp_95
|
|
sta lineEndCol ; 3A13 line walker target column
|
|
lda unitRowTable,y ; 3A15 its row
|
|
sta combatTargetRow ; 3A18 saved copy
|
|
sta lineEndRow ; 3A1B line walker target row
|
|
ldx combatAttackerUnit ; 3A1D the attacker
|
|
lda unitColTable,x ; 3A20 its column
|
|
sta combatAttackerCol ; 3A23 saved copy
|
|
sta lineCurCol ; 3A26 line walker start column
|
|
lda unitRowTable,x ; 3A28 its row
|
|
sta combatAttackerRow ; 3A2B saved copy
|
|
sta lineCurRow ; 3A2E line walker start row
|
|
jsr drawShotLineInViewport ; 3A30 overlay B: white fat-pixel shot line in the 7x5 zoom window (also mapGenerator6F00:runMapTypeMenu)
|
|
lda combatTargetRow ; 3A33 put the four endpoints back for the sprite version
|
|
sta lineEndRow ; 3A36 target row
|
|
lda combatTargetCol ; 3A38 saved target column
|
|
sta lineEndCol ; 3A3B target column
|
|
lda combatAttackerRow ; 3A3D saved attacker row
|
|
sta lineCurRow ; 3A40 attacker row
|
|
lda combatAttackerCol ; 3A42 saved attacker column
|
|
sta lineCurCol ; 3A45 attacker column
|
|
jsr drawShotLineSprite ; 3A47 and draw the same shot on the small overview map
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Sound: a kill overrides the shot sound patched in at $3905.
|
|
; ----------------------------------------------------------------------
|
|
L_3A4A:
|
|
ldy combatScratch ; 3A4A the target
|
|
loadShotSoundId:
|
|
lda #$FF ; 3A4D operand = sound 10 (shot) or 11 (boomer shell), patched at $3905
|
|
ldx unitTypeTable,y ; 3A4F bit 7 of the type byte = the target was destroyed
|
|
bpl L_3A56 ; 3A52 still alive -> ordinary shot sound
|
|
lda #$09 ; 3A54 sound 9 = kill
|
|
L_3A56:
|
|
jsr playSound ; 3A56 queue it
|
|
L_3A59:
|
|
rts ; 3A59 shared exit, also used by the 'spy/comcen cannot fire' case
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawShotLineSprite - Draws the shot on the 4-pixel-per-cell overview map using hardware sprite 3.
|
|
; Both cells are converted to sprite pixel coordinates; because a sprite only covers 24x21 pixels the
|
|
; start point is walked toward the target until the rest of the line fits that box. Sprite 3 is then
|
|
; placed on the top left corner of the box (+2 pixels so the line sits in the middle of the 4x4 cell)
|
|
; in white, the segment is plotted into the sprite shape (dotted for a BOOMER) with a small diamond at
|
|
; the impact end, and shotLineTimer gives the IRQ 8 frames before it hides the sprite again.
|
|
; In: zp_92/zp_93 = attacker row/column, zp_94/zp_95 = target row/column (map cells), attackerType
|
|
; $923D
|
|
; Out: sprite 3 shape at $8AC0, position $9013/$901B, colour $900B = 1 (white), bit 3 of the sprite
|
|
; enable shadow $9022, shotLineTimer $91C8 = 8; zp_48 and the whole line walker state clobbered
|
|
; Called from: resolveAttack ($3A47)
|
|
; Hide sprite 3 and wipe its shape before rebuilding it.
|
|
; ----------------------------------------------------------------------
|
|
drawShotLineSprite:
|
|
lda spriteEnableShadow ; 3A5A shadow of VIC_SPR_ENA $D015
|
|
and #$F7 ; 3A5D clear bit 3: sprite 3 off while its shape is rebuilt
|
|
sta spriteEnableShadow ; 3A5F write the shadow back
|
|
ldy #$3F ; 3A62 64 bytes of sprite data
|
|
lda #$00 ; 3A64 blank byte
|
|
L_3A66:
|
|
sta sprite3Shape,y ; 3A66 clear sprite 3's shape at $8AC0
|
|
dey ; 3A69 next byte
|
|
bpl L_3A66 ; 3A6A until all 64 are cleared
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Convert both map cells into sprite pixel coordinates (column*4+$1F, row*4+$39).
|
|
; ----------------------------------------------------------------------
|
|
ldx lineEndCol ; 3A6C target column
|
|
ldy lineEndRow ; 3A6E target row
|
|
jsr mapCellToSpriteXYRaw ; 3A70 -> sprite pixel position on the overview map
|
|
stx lineEndCol ; 3A73 from here on the line walker works in pixels, not cells
|
|
sty lineEndRow ; 3A75 in sprite pixels as well
|
|
ldx lineCurCol ; 3A77 attacker column
|
|
ldy lineCurRow ; 3A79 attacker row
|
|
jsr mapCellToSpriteXYRaw ; 3A7B -> sprite pixel position
|
|
stx lineCurCol ; 3A7E line walker start, in pixels
|
|
sty lineCurRow ; 3A80 in sprite pixels as well
|
|
jsr initLineStepper ; 3A82 set up the Bresenham walk over the pixel line
|
|
lda #$00 ; 3A85 step counter ...
|
|
sta mapCellPtr ; 3A87 counts the pixels dropped off the far end - nothing ever reads it (leftover)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A sprite is only 24x21 pixels, so walk the start point toward the target until what is left of the
|
|
; line fits inside one sprite; the tail nearest the shooter simply is not drawn.
|
|
; ----------------------------------------------------------------------
|
|
L_3A89:
|
|
lda lineEndRow ; 3A89 how tall is the remaining segment?
|
|
sec ; 3A8B prepare the subtraction
|
|
sbc lineCurRow ; 3A8C end row - current row
|
|
bcs L_3A94 ; 3A8E positive already
|
|
eor #$FF ; 3A90 |dy| ...
|
|
adc #$01 ; 3A92 ... two's complement
|
|
L_3A94:
|
|
cmp #$14 ; 3A94 a sprite is 21 pixel rows tall
|
|
bcs L_3AA7 ; 3A96 still too tall -> drop one more step
|
|
lda lineEndCol ; 3A98 how wide is it?
|
|
sec ; 3A9A prepare the subtraction
|
|
sbc lineCurCol ; 3A9B end column - current column
|
|
bcs L_3AA3 ; 3A9D positive already
|
|
eor #$FF ; 3A9F |dx| ...
|
|
adc #$01 ; 3AA1 ... two's complement
|
|
L_3AA3:
|
|
cmp #$17 ; 3AA3 a sprite is 24 pixel columns wide
|
|
bcc L_3AB1 ; 3AA5 the rest of the line fits -> draw it
|
|
L_3AA7:
|
|
jsr stepLine ; 3AA7 walk one pixel closer to the target
|
|
beq L_3AB1 ; 3AAA the whole line has been consumed
|
|
inc mapCellPtr ; 3AAC count the dropped step (dead store)
|
|
jmp L_3A89 ; 3AAE test the shortened line again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Put sprite 3 on the top left corner of the remaining segment.
|
|
; ----------------------------------------------------------------------
|
|
L_3AB1:
|
|
ldx lineCurCol ; 3AB1 assume the current point is the left edge
|
|
lda lineStepCol ; 3AB3 which way does the walk run?
|
|
bpl L_3AB9 ; 3AB5 to the right -> the current point really is leftmost
|
|
ldx lineEndCol ; 3AB7 to the left -> the target end is leftmost
|
|
L_3AB9:
|
|
ldy lineCurRow ; 3AB9 same for the top edge
|
|
lda lineStepRow ; 3ABB which way vertically?
|
|
bpl L_3AC1 ; 3ABD downwards -> the current point is topmost
|
|
ldy lineEndRow ; 3ABF upwards -> the target end is topmost
|
|
L_3AC1:
|
|
cpx lineEndCol ; 3AC1 a perfectly vertical line?
|
|
bne L_3AC6 ; 3AC3 no
|
|
dex ; 3AC5 shift the sprite one pixel left so the line is not on its edge column
|
|
L_3AC6:
|
|
cpy lineEndRow ; 3AC6 a perfectly horizontal line?
|
|
bne L_3ACB ; 3AC8 no
|
|
dey ; 3ACA shift it one pixel up
|
|
L_3ACB:
|
|
stx sprite3XShadow ; 3ACB $9013 = shadow of sprite 3's X position
|
|
sty sprite3YShadow ; 3ACE $901B = shadow of sprite 3's Y position
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Make both endpoints relative to the sprite origin, then centre the sprite in the map cell.
|
|
; ----------------------------------------------------------------------
|
|
lda lineCurCol ; 3AD1 line start column
|
|
sec ; 3AD3 prepare the subtraction
|
|
sbc sprite3XShadow ; 3AD4 minus the sprite origin
|
|
sta lineCurCol ; 3AD7 start column inside the sprite (0-23)
|
|
lda lineEndCol ; 3AD9 line end column
|
|
sec ; 3ADB prepare the subtraction
|
|
sbc sprite3XShadow ; 3ADC minus the sprite origin
|
|
sta lineEndCol ; 3ADF end column inside the sprite
|
|
lda lineCurRow ; 3AE1 line start row
|
|
sec ; 3AE3 prepare the subtraction
|
|
sbc sprite3YShadow ; 3AE4 minus the sprite origin
|
|
sta lineCurRow ; 3AE7 start row inside the sprite (0-20)
|
|
lda lineEndRow ; 3AE9 line end row
|
|
sec ; 3AEB prepare the subtraction
|
|
sbc sprite3YShadow ; 3AEC minus the sprite origin
|
|
sta lineEndRow ; 3AEF end row inside the sprite
|
|
lda sprite3XShadow ; 3AF1 sprite X position
|
|
clc ; 3AF4 prepare the addition
|
|
adc #$02 ; 3AF5 +2 pixels: a map cell is 4x4 pixels, so this centres the line inside it
|
|
sta sprite3XShadow ; 3AF7 store the sprite X position
|
|
lda sprite3YShadow ; 3AFA sprite Y position
|
|
clc ; 3AFD prepare the addition
|
|
adc #$02 ; 3AFE same +2 vertically
|
|
sta sprite3YShadow ; 3B00 store the sprite Y position
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Plot the segment into the sprite shape and show it for 8 frames.
|
|
; ----------------------------------------------------------------------
|
|
lda #$01 ; 3B03 colour 1 = white
|
|
sta sprite3ColourShadow ; 3B05 $900B = shadow of sprite 3's colour register $D02A
|
|
lda attackerType ; 3B08 attacker type
|
|
cmp #$02 ; 3B0B BOOMER?
|
|
bne L_3B11 ; 3B0D no -> solid line
|
|
lda #$80 ; 3B0F bit 7 = draw the line dotted (a lobbed shell)
|
|
L_3B11:
|
|
jsr drawLineInPathSprite ; 3B11 plot the segment into sprite 3's shape
|
|
jsr plotPathCross ; 3B14 small diamond at the impact end
|
|
lda #$08 ; 3B17 sprite 3
|
|
jsr enableSprites ; 3B19 switch it on
|
|
lda #$08 ; 3B1C 8 frames
|
|
sta shotLineTimer ; 3B1E the IRQ hides the sprite again when this runs out
|
|
rts ; 3B21 back to resolveAttack
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyHitToUnit - Takes A points of damage off unit Y and deals with everything that follows. For
|
|
; one of the local player's own units it warns when a spy is shot at for the first time (message $0E)
|
|
; and shakes the screen with sound 8 when the player's own comcen is hit. Either comcen halves the
|
|
; damage, caps it at the energy it has left, has one of its systems knocked out at random when it is
|
|
; ours, and scores 2 points per energy point for the attacking side. Then the energy is reduced and
|
|
; the hit recorded; a unit that runs out of energy is marked destroyed, while a comcen instead ends
|
|
; the game through endGame.
|
|
; In: A = damage points, Y = target unit
|
|
; Out: hitDamage $9240 = the damage actually applied; the target's energy $F9C4, movement points
|
|
; $FA28, hit effect $FA8C, display flags $FB54 and type byte $F76C; scores $92A2-$92A5 and $9208;
|
|
; screenShakeTimer $92C8; gameEndReason $0BA8; sounds, messages and a redraw of the cell
|
|
; Called from: resolveAttack ($39FD), cmdMissileStrike ($5413), the drone blast in overlay B ($7312);
|
|
; the alternate entry applyHitToUnitNoAlert ($3B52) from destroyUnit ($5465)
|
|
; Reactions that only happen for the local player's own units. The branch opcode at $3B2C is patched
|
|
; per side by overlay A ($7BEC), so 'below unit 50' means 'mine' for side 0 and 'the opponent's' for
|
|
; side 1.
|
|
; ----------------------------------------------------------------------
|
|
applyHitToUnit:
|
|
sta hitDamage ; 3B22 $9240 = the damage points to apply
|
|
lda unitTypeTable,y ; 3B25 target's type byte
|
|
and #$07 ; 3B28 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN
|
|
cpy #$32 ; 3B2A unit 50 = the first unit of side 1
|
|
D_3B2C:
|
|
bcs L_3B5A ; 3B2C opcode patched per side ($7BEC): the opponent's units skip the alerts below
|
|
cmp #$03 ; 3B2E one of ours - is it a SPY?
|
|
bne L_3B42 ; 3B30 no
|
|
lda unitEnergyTable,y ; 3B32 its energy byte
|
|
and #$3F ; 3B35 bits 0-5 = energy 0-50
|
|
cmp #$32 ; 3B37 still untouched at 50?
|
|
bne L_3B40 ; 3B39 already damaged -> warn only on the very first hit
|
|
lda #$0E ; 3B3B message $0E 'SPY UNDER ATTACK!'
|
|
jsr queueMessageUnlessFilm ; 3B3D queue it (dropped during solo/film playback)
|
|
L_3B40:
|
|
lda #$03 ; 3B40 restore the type value the message call clobbered
|
|
L_3B42:
|
|
cmp #$04 ; 3B42 is this our own COMCEN being hit?
|
|
bne L_3B5A ; 3B44 no -> straight on
|
|
lda #$08 ; 3B46 8 frames
|
|
sta screenShakeTimer ; 3B48 $92C8: shake the screen while the comcen is being hit
|
|
jsr playSound ; 3B4B sound 8 = own comcen hit
|
|
lda #$04 ; 3B4E restore the type value
|
|
bne L_3B5A ; 3B50 (always taken)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyHitToUnitNoAlert - Alternate entry of applyHitToUnit that skips the block above, i.e. the spy
|
|
; warning, the screen shake and the comcen hit sound that only fire for the local player's own units.
|
|
; Everything else - the comcen half-damage and scoring, the energy loss and the destruction handling -
|
|
; is identical.
|
|
; In: A = damage points, Y = target unit
|
|
; Out: as applyHitToUnit
|
|
; Called from: destroyUnit ($5465) with A = $34, i.e. more than any unit's 50 energy, so the SELF
|
|
; DESTRUCT command always kills
|
|
; ----------------------------------------------------------------------
|
|
applyHitToUnitNoAlert:
|
|
sta hitDamage ; 3B52 alternate entry used by SELF DESTRUCT ($5465): damage without the own-unit alerts
|
|
lda unitTypeTable,y ; 3B55 target's type byte
|
|
and #$07 ; 3B58 type 0-4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A hit on either comcen: half damage, capped at what is left, and the only thing in the game that
|
|
; scores points. The score table is indexed by the damaged side - slot X holds the points scored
|
|
; against side X, which is the other side's running total (see $2965).
|
|
; ----------------------------------------------------------------------
|
|
L_3B5A:
|
|
cmp #$04 ; 3B5A type 4 = COMCEN
|
|
bne L_3B93 ; 3B5C an ordinary unit -> straight to the damage
|
|
cpy comcenUnit ; 3B5E $9236 = the local player's own comcen (unit 49 or 99)
|
|
bne L_3B69 ; 3B61 the opponent's comcen -> no system damage rolled here
|
|
jsr rollComcenSystemDamage ; 3B63 knock out one of our comcen's systems at random
|
|
ldy comcenUnit ; 3B66 the call clobbers Y
|
|
L_3B69:
|
|
lsr hitDamage ; 3B69 a comcen only ever takes half damage
|
|
lda unitEnergyTable,y ; 3B6C its energy byte
|
|
and #$3F ; 3B6F energy it has left
|
|
cmp hitDamage ; 3B71 more damage than energy?
|
|
bcs L_3B79 ; 3B74 no
|
|
sta hitDamage ; 3B76 never score more than the comcen still has
|
|
L_3B79:
|
|
ldx #$00 ; 3B79 X = the side that was hit
|
|
cpy #$32 ; 3B7B unit 50 = first unit of side 1
|
|
bcc L_3B80 ; 3B7D side 0
|
|
inx ; 3B7F units 50-99 belong to side 1
|
|
L_3B80:
|
|
inc lastShownClock ; 3B80 make the clock/score line redraw itself ($C519 compares this with the game clock)
|
|
lda hitDamage ; 3B83 the damage actually applied
|
|
asl a ; 3B86 2 points per energy point taken off a comcen
|
|
clc ; 3B87 prepare the addition
|
|
adc sideScoreLo,x ; 3B88 slot X = points scored against side X = the attacking side's total
|
|
sta sideScoreLo,x ; 3B8B 16-bit score, low byte
|
|
bcc L_3B93 ; 3B8E no carry
|
|
inc sideScoreHi,x ; 3B90 carry into the high byte of the 16-bit score
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Take the energy off and record the hit.
|
|
; ----------------------------------------------------------------------
|
|
L_3B93:
|
|
lda hitDamage ; 3B93 (reloading A is redundant - reduceUnitEnergy reads $9240 itself)
|
|
jsr reduceUnitEnergy ; 3B96 energy = energy - damage, with a floor of 0
|
|
lda hitDamage ; 3B99 the damage again
|
|
jsr recordUnitHit ; 3B9C flash the unit, pin it down and flag a status refresh
|
|
lda unitEnergyTable,y ; 3B9F its energy byte
|
|
and #$3F ; 3BA2 any energy left?
|
|
bne L_3BDB ; 3BA4 still alive -> just redraw it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The unit ran out of energy. An ordinary unit is marked destroyed and gets the death flash; a comcen
|
|
; never carries the dead bit - losing it ends the game instead.
|
|
; ----------------------------------------------------------------------
|
|
lda unitTypeTable,y ; 3BA6 type byte
|
|
ora #$80 ; 3BA9 bit 7 = destroyed
|
|
sta unitTypeTable,y ; 3BAB the unit is dead
|
|
and #$07 ; 3BAE type 0-4
|
|
cmp #$04 ; 3BB0 was it a COMCEN?
|
|
bne L_3BD6 ; 3BB2 an ordinary unit -> death flash
|
|
lda unitTypeTable,y ; 3BB4 type byte again
|
|
and #$7F ; 3BB7 a comcen never carries the dead bit ...
|
|
sta unitTypeTable,y ; 3BB9 ... it stays on the map
|
|
lda unitFlagsTable,y ; 3BBC flags of the destroyed comcen
|
|
and #$20 ; 3BBF bit 5 = its side
|
|
clc ; 3BC1 prepare the addition
|
|
adc #$20 ; 3BC2 $20 = side 0 knocked out, $40 = side 1 knocked out
|
|
ora #$80 ; 3BC4 bit 7 = the game ended by a knockout
|
|
ora gameEndReason ; 3BC6 keep any reason that was already set
|
|
sta gameEndReason ; 3BC9 $0BA8 = the game end reason
|
|
ldx #$01 ; 3BCC assume the opponent's comcen fell
|
|
cpy #$32 ; 3BCE unit 50 = first unit of side 1
|
|
D_3BD0:
|
|
bcs L_3BD3 ; 3BD0 opcode patched per side ($7BEF)
|
|
dex ; 3BD2 X = 0 when it was our own comcen that fell
|
|
L_3BD3:
|
|
jmp endGame ; 3BD3 halt everything, reveal the units and show the result
|
|
|
|
L_3BD6:
|
|
lda #$85 ; 3BD6 $80 = a hit effect is pending, 5 = the 'destroyed' flash
|
|
sta unitHitFlashTable,y ; 3BD8 $FA8C = hit effect of the unit
|
|
L_3BDB:
|
|
jsr drawUnitAtPosition ; 3BDB redraw the unit's cell on both map panels
|
|
rts ; 3BDE back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; reduceUnitEnergyBy - Stores A as the damage in hitDamage $9240 and falls into reduceUnitEnergy.
|
|
; In: A = energy points to take off, Y = unit
|
|
; Out: see reduceUnitEnergy
|
|
; Called from: resolveAttack ($3947, the blitz charge the attacker pays for) and $3EF8 (1 point per
|
|
; blitz step)
|
|
; ----------------------------------------------------------------------
|
|
reduceUnitEnergyBy:
|
|
sta hitDamage ; 3BDF $9240 = the energy points to take off
|
|
|
|
; ----------------------------------------------------------------------
|
|
; reduceUnitEnergy - Subtracts hitDamage $9240 from the energy of unit Y, keeping the two visibility
|
|
; flags in bits 6-7 and stopping at 0.
|
|
; In: Y = unit, hitDamage $9240
|
|
; Out: $F9C4[Y] updated; the ORA operand orVisibilityBits+1 is patched with the saved flags
|
|
; Called from: applyHitToUnit ($3B96); reduceUnitEnergyBy falls into it
|
|
; ----------------------------------------------------------------------
|
|
reduceUnitEnergy:
|
|
lda unitEnergyTable,y ; 3BE2 energy byte: bits 0-5 = 0-50, bits 6-7 = the spotted/visible flags
|
|
pha ; 3BE5 keep the whole byte for a moment
|
|
and #$C0 ; 3BE6 isolate the two visibility flags
|
|
sta orVisibilityBits+1 ; 3BE8 patch them into the ORA below
|
|
pla ; 3BEB the byte back off the stack
|
|
and #$3F ; 3BEC energy value only
|
|
sec ; 3BEE prepare the subtraction
|
|
sbc hitDamage ; 3BEF minus the damage
|
|
bcs orVisibilityBits ; 3BF2 no borrow -> the result is still positive
|
|
lda #$00 ; 3BF4 energy never goes below 0
|
|
orVisibilityBits:
|
|
ora #$FF ; 3BF6 operand = the two visibility bits saved at $3BE6
|
|
sta unitEnergyTable,y ; 3BF8 energy and visibility flags back into the array
|
|
rts ; 3BFB back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; recordUnitHit - Records the visible effect of a hit on unit Y: the damage, capped at 20, is stored
|
|
; as $80|damage in the hit-effect byte $FA8C that drives the flash and the PINNED status word, the
|
|
; same number of movement points is taken off the signed counter $FA28 (clamped at -127), and bit 1 of
|
|
; the display flags asks for the console entry to be refreshed.
|
|
; In: A = damage points, Y = unit
|
|
; Out: $FA8C[Y] = $80|damage, $FA28[Y] reduced, $FB54[Y] bit 1 set; the SBC operand
|
|
; subtractPinnedDamage+1 is patched
|
|
; Called from: applyHitToUnit ($3B9C)
|
|
; ----------------------------------------------------------------------
|
|
recordUnitHit:
|
|
cmp #$14 ; 3BFC more than 20 points?
|
|
bcc L_3C02 ; 3BFE no
|
|
lda #$14 ; 3C00 the effect saturates at 20 points
|
|
L_3C02:
|
|
sta subtractPinnedDamage+1 ; 3C02 patch the subtraction below
|
|
ora #$80 ; 3C05 bit 7 = a hit effect is pending on this unit
|
|
sta unitHitFlashTable,y ; 3C07 $FA8C drives the hit flash and the PINNED status word
|
|
lda unitMovePointsTable,y ; 3C0A $FA28 = signed movement points
|
|
sec ; 3C0D prepare the subtraction
|
|
subtractPinnedDamage:
|
|
sbc #$FF ; 3C0E operand = the damage: being shot at pins the unit down
|
|
bvc L_3C14 ; 3C10 no signed overflow
|
|
lda #$81 ; 3C12 clamp at -127
|
|
L_3C14:
|
|
sta unitMovePointsTable,y ; 3C14 movement points back into the array
|
|
lda unitDisplayFlagsTable,y ; 3C17 display flags
|
|
ora #$02 ; 3C1A bit 1 = the unit's console entry needs refreshing
|
|
sta unitDisplayFlagsTable,y ; 3C1C display flags back into the array
|
|
rts ; 3C1F back to applyHitToUnit
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; orderAttackUnitAtCursor - Fire-button action of the attack cursor. It reads the map cell under the
|
|
; battlefield cursor (always the centre of the 7x5 view, viewOriginCol+3 / viewOriginRow+2); if an
|
|
; enemy unit stands there it queues command $87 SET TARGET carrying the selected unit and that target,
|
|
; otherwise it just beeps.
|
|
; In: zp_A5/zp_A6 = view origin, selectedUnit $90F8, the map, unit flags $F708
|
|
; Out: cmdParam1/cmdParam2 (zp_74/zp_75) and a 3-byte command $87 in the outgoing ring, or sound 3
|
|
; Called from: the target cursor loop ($21F2, with cursorTargetMode zp_BB > 0)
|
|
; ----------------------------------------------------------------------
|
|
orderAttackUnitAtCursor:
|
|
lda viewOriginCol ; 3C20 left edge of the 7x5 battlefield view
|
|
clc ; 3C22 prepare the addition
|
|
adc #$03 ; 3C23 the cursor sits 3 cells in ...
|
|
tax ; 3C25 -> X = cursor column
|
|
lda viewOriginRow ; 3C26 top edge of the view
|
|
clc ; 3C28 prepare the addition
|
|
adc #$02 ; 3C29 ... and 2 cells down: the centre cell
|
|
tay ; 3C2B -> Y = cursor row
|
|
jsr readMapCell ; 3C2C read the cell under the cursor
|
|
bpl L_3C4B ; 3C2F bare terrain -> nothing to shoot at
|
|
tay ; 3C31 Y = $80|index, which the base-$80 alias tables undo
|
|
ldx selectedUnit ; 3C32 the unit the order is for
|
|
lda unitFlagsTableAlias,y ; 3C35 flags of the unit under the cursor ($F708-$80)
|
|
eor unitFlagsTable,x ; 3C38 compare them with the selected unit's flags
|
|
and #$20 ; 3C3B bit 5 = side
|
|
beq L_3C4B ; 3C3D same side -> one of ours, not a target
|
|
stx cmdParam1 ; 3C3F argument 1 = the unit that will fire
|
|
tya ; 3C41 the target's cell byte
|
|
and #$7F ; 3C42 strip the map's 'unit here' marker bit
|
|
sta cmdParam2 ; 3C44 argument 2 = the target unit
|
|
lda #$87 ; 3C46 command $87 SET TARGET
|
|
jmp queueCmd3 ; 3C48 queue the code plus its two arguments for both machines
|
|
|
|
L_3C4B:
|
|
lda #$03 ; 3C4B sound 3 = error beep
|
|
jmp playSound ; 3C4D nothing was ordered
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setBoomerTargetCmd - Execution half of command $87 SET TARGET. The boomer's ordinal among all
|
|
; boomers is the index into boomerTargetTable, where the target unit is stored with bit 7 set to mark
|
|
; it as a lock the player asked for. The listing still shows these 13 bytes as data; they are entered
|
|
; by the JMP $3C50 at $5182 inside the command dispatcher.
|
|
; In: cmdParam0 zp_73 = boomer unit index, cmdParam2 zp_75 = target unit index
|
|
; Out: boomerTargetTable $91DC[ordinal] = $80|target; Y = the ordinal, X = the boomer index
|
|
; Called from: cmdSetTarget ($5182), the handler of command $87 on both machines
|
|
; ----------------------------------------------------------------------
|
|
setBoomerTargetCmd:
|
|
ldx cmdParam0 ; 3C50 boomer unit index
|
|
jsr countBoomersBelow ; 3C52 Y = its ordinal among all boomers = its slot in $91DC
|
|
lda cmdParam2 ; 3C55 target unit index
|
|
ora #$80 ; 3C57 bit 7 = the player locked this target on purpose
|
|
sta boomerTargetTable,y ; 3C59 one entry per boomer
|
|
rts ; 3C5C back to the command dispatcher
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; countBoomersBelow - Counts the BOOMERs (type 2) with an index below X, which is the ordinal of
|
|
; boomer X among all boomers and therefore its slot in boomerTargetTable. The limit is patched into
|
|
; the CPX at $3C6F.
|
|
; In: X = unit index (exclusive upper limit)
|
|
; Out: Y = the number of boomers below X, X = the input value, C = 1; the operand countBoomersLimit+1
|
|
; is patched
|
|
; Called from: setBoomerTargetCmd ($3C52), getBoomerTarget ($3C7E)
|
|
; ----------------------------------------------------------------------
|
|
countBoomersBelow:
|
|
stx countBoomersLimit+1 ; 3C5D patch the loop limit with the unit we are looking for
|
|
ldy #$00 ; 3C60 boomers counted so far
|
|
ldx #$00 ; 3C62 start at unit 0
|
|
L_3C64:
|
|
lda unitTypeTable,x ; 3C64 type byte of this unit
|
|
and #$07 ; 3C67 strip the dead/dug-in bits
|
|
cmp #$02 ; 3C69 type 2 = BOOMER
|
|
bne L_3C6E ; 3C6B not a boomer
|
|
iny ; 3C6D count it
|
|
L_3C6E:
|
|
inx ; 3C6E next unit
|
|
countBoomersLimit:
|
|
cpx #$FF ; 3C6F operand = the unit index passed in
|
|
bcc L_3C64 ; 3C71 count every boomer below it
|
|
rts ; 3C73 Y = the boomer's slot in boomerTargetTable
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getBoomerTarget - Reads the target lock of unit X. For a BOOMER it returns C = 1, Y = the boomer's
|
|
; slot and A = its boomerTargetTable entry (bit 7 = locked by the player, bits 0-6 = the target unit);
|
|
; any other unit type returns C = 0 and a non-zero, positive A. Callers test C and N: the unit
|
|
; options menu offers CLEAR TARGET or SET TARGET, and the update loop drops the lock when the boomer
|
|
; can no longer fire.
|
|
; In: X = unit index
|
|
; Out: for a boomer C = 1, A = $91DC[ordinal], Y = the ordinal; otherwise C = 0 and A = type EOR 2
|
|
; Called from: $2432, selectBoomerTarget ($3C8F), the per-unit update loop ($444D), cmdClearTarget
|
|
; ($5162), the unit options menu ($5EFF)
|
|
; ----------------------------------------------------------------------
|
|
getBoomerTarget:
|
|
clc ; 3C74 C = 0 will mean 'not a boomer'
|
|
lda unitTypeTable,x ; 3C75 type byte
|
|
and #$07 ; 3C78 strip the dead/dug-in bits
|
|
eor #$02 ; 3C7A zero only for type 2 = BOOMER
|
|
bne L_3C84 ; 3C7C any other type: return C=0 and A = type EOR 2
|
|
jsr countBoomersBelow ; 3C7E Y = the boomer's slot in $91DC (this also returns C=1)
|
|
lda boomerTargetTable,y ; 3C81 bit 7 = locked, bits 0-6 = the target unit
|
|
L_3C84:
|
|
rts ; 3C84 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; selectBoomerTarget - Picks what boomer Y shoots at this round. It first runs the weapon-range scan
|
|
; (scanVisibleEnemies with X = $80) to build the candidate lists, then walks them: if the target the
|
|
; player locked on is still listed it is kept and nothing else is looked at; otherwise an enemy COMCEN
|
|
; wins outright and, failing that, the nearest candidate is taken - and the now stale lock in
|
|
; boomerTargetTable is cleared.
|
|
; In: Y = boomer unit index; boomerTargetTable $91DC; the candidate lists $0400/$0432/$0464 and zp_2F
|
|
; produced by the scan
|
|
; Out: fireTargetUnit $91F0 = the chosen target ($FF = none), fireTargetDistance $91F1 = its squared
|
|
; distance, targetDirection zp_6B = the direction 0-3 to turn to; $91DC[ordinal] cleared when the
|
|
; lock was lost; zp_18/zp_19/zp_1A scratch; operand loadBoomerUnit+1 patched
|
|
; Called from: the per-unit update loop ($4425) for units of type 2
|
|
; ----------------------------------------------------------------------
|
|
selectBoomerTarget:
|
|
sty loadBoomerUnit+1 ; 3C85 remember the boomer: the scan below needs X and Y for itself
|
|
ldx #$80 ; 3C88 bit 7 = weapon-range scan, not a spotting scan
|
|
jsr scanVisibleEnemies ; 3C8A build the candidate lists $0400/$0432/$0464
|
|
loadBoomerUnit:
|
|
ldx #$FF ; 3C8D operand = the boomer unit index
|
|
jsr getBoomerTarget ; 3C8F A = its lock byte, Y = its slot in $91DC
|
|
sty scratch1A ; 3C92 keep the slot for the 'lost the target' case below
|
|
eor #$80 ; 3C94 strip the lock bit -> a plain unit index ($80 when nothing is locked)
|
|
sta scratch19 ; 3C96 the unit this boomer is currently locked on
|
|
lda #$FF ; 3C98 $FF ...
|
|
sta fireTargetDistance ; 3C9A ... no best distance yet
|
|
sta fireTargetUnit ; 3C9D $91F0 = no target chosen yet
|
|
ldx scanResultCount ; 3CA0 index of the last candidate
|
|
stx scratch18 ; 3CA2 the loop walks the list backwards
|
|
bmi L_3CD9 ; 3CA4 nothing within weapon range at all
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the candidate list backwards looking for the best target.
|
|
; ----------------------------------------------------------------------
|
|
L_3CA6:
|
|
ldx scratch18 ; 3CA6 current candidate
|
|
ldy checksumXorConstant,x ; 3CA8 its unit index, or $FF where scanVisibleEnemies rejected the entry (also trainerPlaybook0200:play3Col12)
|
|
bmi L_3CD5 ; 3CAB rejected entry -> skip it
|
|
cpy scratch19 ; 3CAD is this the target the player locked on?
|
|
beq L_3CE2 ; 3CAF yes -> keep firing at it and stop looking
|
|
cpy #$31 ; 3CB1 unit 49 = side 0's comcen
|
|
beq L_3CB9 ; 3CB3 yes
|
|
cpy #$63 ; 3CB5 unit 99 = side 1's comcen
|
|
bne L_3CBF ; 3CB7 an ordinary unit -> compare distances
|
|
L_3CB9:
|
|
lda #$00 ; 3CB9 a comcen outranks everything ...
|
|
sta scratch18 ; 3CBB ... and the DEC below ends the search right after this entry
|
|
beq L_3CCA ; 3CBD (always taken; note that the comcen's distance is never stored)
|
|
L_3CBF:
|
|
lda play3Row12,x ; 3CBF squared distance of this candidate
|
|
cmp fireTargetDistance ; 3CC2 against the best so far
|
|
bcs L_3CD5 ; 3CC5 farther away -> keep the old one
|
|
sta fireTargetDistance ; 3CC7 a new nearest candidate
|
|
L_3CCA:
|
|
lda checksumXorConstant,x ; 3CCA its unit index (also trainerPlaybook0200:play3Col12)
|
|
sta fireTargetUnit ; 3CCD $91F0 = the unit that will be fired at
|
|
lda play4Grp12,x ; 3CD0 direction 0-3 toward it
|
|
sta targetDirection ; 3CD3 zp_6B, copied into the unit's facing bits at $4464
|
|
L_3CD5:
|
|
dec scratch18 ; 3CD5 next candidate (the list is walked backwards)
|
|
bpl L_3CA6 ; 3CD7 until the whole list has been looked at
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Reached whenever the locked target was not in the list any more (and also when there never was one):
|
|
; the lock is dropped, so the unit options menu offers SET TARGET again.
|
|
; ----------------------------------------------------------------------
|
|
L_3CD9:
|
|
ldy scratch1A ; 3CD9 the boomer's slot in $91DC
|
|
lda #$00 ; 3CDB 0 = no target locked
|
|
sta boomerTargetTable,y ; 3CDD clear the stale lock
|
|
beq L_3CF0 ; 3CE0 (always taken)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The locked target is still within range: keep it and ignore everything else in the list.
|
|
; ----------------------------------------------------------------------
|
|
L_3CE2:
|
|
sty fireTargetUnit ; 3CE2 Y still holds the locked unit
|
|
lda play3Row12,x ; 3CE5 its squared distance
|
|
sta fireTargetDistance ; 3CE8 $91F1 = distance to the locked target
|
|
lda play4Grp12,x ; 3CEB direction 0-3 toward it
|
|
sta targetDirection ; 3CEE zp_6B = direction to turn to
|
|
L_3CF0:
|
|
rts ; 3CF0 back to the per-unit update loop
|
|
|
|
|
|
; ringAnimStep - one byte of state for animateRingBurst below: the radius index of the circle frame
|
|
; currently being drawn into or erased from sprite 1 (1..A-1 going out, A-1..1 coming back).
|
|
ringAnimStep:
|
|
.byte $00 ; 3CF1 . radius index of the ring frame being drawn/erased (0 when the effect is idle)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; animateRingBurst - Expanding-then-contracting ring drawn into hardware sprite 1. It clears the
|
|
; sprite shape, switches the sprite on, draws circle outlines of radius 1,2,..,A-1 into it
|
|
; (spritePlotMode $40 = OR the pixels in), plays the explosion sound at full size and then erases the
|
|
; same frames in reverse order (spritePlotMode 0 = AND them out) before hiding the sprite again.
|
|
; In: A = number of radius steps, patched into the CPX operand at L_3D24+1 (drawCircleInSprite only
|
|
; has point lists for radii 1-10); sprite 1 must already be positioned through the shadow
|
|
; registers $9011/$9019.
|
|
; Out: sprite 1 shape built and cleared again, spriteEnableShadow bit 1 set and then cleared,
|
|
; ringAnimStep = 0, spritePlotMode left at 0, sprite1ColourShadow = $12, unusedByte7D = 0 (dead
|
|
; store), sound $17 played. Every wait runs through runGameFrames, so the battle keeps ticking
|
|
; during the animation.
|
|
; Called from: the $6F00 overlay only - $767B with A=3 (the ring that marks a unit being placed / the
|
|
; kick-off) and $7A0E with A=$0B (the comcen knock-out explosion).
|
|
; ----------------------------------------------------------------------
|
|
animateRingBurst:
|
|
sta L_3D24+1 ; 3CF2 patch the loop limit: A becomes the operand of the CPX at $3D24, i.e. the largest radius + 1
|
|
lda #$12 ; 3CF5 $12 = packed flash colour pair (high nibble 1 = white, low nibble 2 = red)
|
|
sta sprite1ColourShadow ; 3CF7 sprite 1 colour shadow; the IRQ alternates the two nibbles so the ring flashes white/red
|
|
jsr leftoverSetC397 ; 3CFA wait until the raster is below the visible screen so building the shape does not tear (the $C000 overlay resident here is waitRasterBelowScreen; the label is the boot loader's leftover at the same address) (also textEngineC000:waitRasterBelowScreen)
|
|
lda #$01 ; 3CFD sprite number 1 (its shape lives at $8A00 + 1*64 = $8A40)
|
|
jsr clearSpriteShape ; 3CFF wipe the 64 shape bytes before the first frame is plotted
|
|
lda #$02 ; 3D02 bit mask %00000010 = sprite 1
|
|
jsr enableSprites ; 3D04 OR it into spriteEnableShadow ($9022) so the IRQ turns sprite 1 on
|
|
lda #$40 ; 3D07 plot mode $40 = set (OR) the pixels of the circle
|
|
sta spritePlotMode ; 3D09 drawCircleInSprite reads spritePlotMode for every plotted point
|
|
ldx #$01 ; 3D0C start with radius 1 (a filled 3x3 dot)
|
|
stx ringAnimStep ; 3D0E keep the radius index in ringAnimStep as well as in X
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Growing phase: draw radius 1, 2, ... up to the caller's limit minus 1.
|
|
; ----------------------------------------------------------------------
|
|
ringExpandLoop:
|
|
lda #$01 ; 3D11 sprite number 1 (drawCircleInSprite takes the sprite in A, the radius in X)
|
|
jsr drawCircleInSprite ; 3D13 plot the circle of radius X into sprite 1
|
|
lda ringAnimStep ; 3D16 run 1 game frame on odd radii and none on even ones ...
|
|
and #$01 ; 3D19 ... so the ring grows about one step every other frame
|
|
jsr runGameFrames ; 3D1B let the engine advance while the ring expands
|
|
inc ringAnimStep ; 3D1E next radius
|
|
ldx ringAnimStep ; 3D21 reload it into X for the compare and for the next drawCircleInSprite
|
|
L_3D24:
|
|
cpx #$FF ; 3D24 operand patched at $3CF2 with the caller's step count
|
|
bcc ringExpandLoop ; 3D26 keep growing while the radius is below the requested maximum
|
|
lda #$00 ; 3D28 dead store: nothing anywhere reads zp $7D
|
|
sta unusedByte7D ; 3D2A (a leftover of an earlier version of the effect)
|
|
lda #$17 ; 3D2C sound $17 = the explosion noise, played at full ring size
|
|
jsr playSound ; 3D2E queue it on a SID voice
|
|
lda #$05 ; 3D31 hold the largest ring for 5 frames
|
|
jsr runGameFrames ; 3D33 the engine keeps running while we wait
|
|
lda #$00 ; 3D36 plot mode 0 = clear (AND out) the pixels again
|
|
sta spritePlotMode ; 3D38 so the same circle calls now erase what they drew
|
|
dec ringAnimStep ; 3D3B step back to the last radius that was actually drawn
|
|
ldx ringAnimStep ; 3D3E X = that radius
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shrinking phase: erase the frames in reverse order, largest first, with no frame delay.
|
|
; ----------------------------------------------------------------------
|
|
ringShrinkLoop:
|
|
lda #$01 ; 3D41 sprite number 1
|
|
jsr drawCircleInSprite ; 3D43 erase the circle of radius X
|
|
dec ringAnimStep ; 3D46 next smaller radius
|
|
ldx ringAnimStep ; 3D49 X = radius for the next pass
|
|
bne ringShrinkLoop ; 3D4C stop once radius 1 has been erased
|
|
jsr hideSprite1 ; 3D4E clear bit 1 of the sprite enable shadow and of VIC_SPR_ENA at once
|
|
lda #$01 ; 3D51 sprite number 1
|
|
jmp clearSpriteShape ; 3D53 tail call: blank its 64 shape bytes and return to the caller
|
|
|
|
|
|
; comcenSystemIndex - loop counter of rollComcenSystemDamage: the comcen system being rolled for,
|
|
; counting 3, 2, 1 (1 = BATTLE, 2 = RADAR, 3 = DRONE; index 0 is the derived energy status and is
|
|
; never damaged here).
|
|
comcenSystemIndex:
|
|
.byte $00 ; 3D56 . the comcen system index 3..1 while the damage roll runs
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rollComcenSystemDamage - Rolls for collateral damage to the local comcen's three repairable systems
|
|
; after it has been hit. It runs only in a live game with the DAMAGE rule switched on; for each
|
|
; system that is still healthy it sets the status to $80 (damaged) when a random 6-bit number is
|
|
; smaller than the damage of the hit, so a big hit is much more likely to knock a system out.
|
|
; In: hitDamage $9240 = the damage the hit just did; filmPlaybackMode $0B9B bit 7 (film / solo
|
|
; replay), gameTypeOptions $0BA3 bit 4 (the DAMAGE option), gameEndReason $0BA8 (0 = a game is
|
|
; running).
|
|
; Out: comcenSystemStatus[1..3] ($9242-$9244) may become $80 = damaged; comcenSystemIndex left 0; A/Y
|
|
; clobbered. The system states are never sent over the link - each machine damages and repairs
|
|
; its own comcen locally.
|
|
; Called from: applyHitToUnit at $3B63, only when the unit that was hit is the local comcen.
|
|
; ----------------------------------------------------------------------
|
|
rollComcenSystemDamage:
|
|
lda filmPlaybackMode ; 3D57 bit 7 set = watching a game film / solo replay ...
|
|
bmi L_3D89 ; 3D5A ... in which case no new damage may be rolled
|
|
lda gameTypeOptions ; 3D5C the rule options of this game
|
|
and #$10 ; 3D5F bit 4 = the DAMAGE option ('comcen systems can be damaged')
|
|
beq L_3D89 ; 3D61 the systems are immune when the option is off
|
|
lda gameEndReason ; 3D63 non-zero = the battle has ended (or has not started)
|
|
bne L_3D89 ; 3D66 no damage rolls outside a running game
|
|
lda #$03 ; 3D68 start at system 3 (DRONE)
|
|
sta comcenSystemIndex ; 3D6A comcenSystemIndex is the loop counter
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Roll once for each of the three repairable systems 3, 2, 1 (DRONE, RADAR, BATTLE).
|
|
; ----------------------------------------------------------------------
|
|
comcenSystemRollLoop:
|
|
ldy comcenSystemIndex ; 3D6D Y = system index
|
|
lda comcenSystemStatus,y ; 3D70 $9241+Y: 0 = healthy, $40 = degraded, $80 = damaged
|
|
bmi L_3D84 ; 3D73 already damaged, nothing left to break
|
|
jsr nextGameRandom ; 3D75 next pseudo random byte
|
|
and #$3F ; 3D78 keep 6 bits: 0..63
|
|
cmp hitDamage ; 3D7A compare it against the damage of the hit (energy runs 0-50)
|
|
bcs L_3D84 ; 3D7D roll too high: this system survives
|
|
lda #$80 ; 3D7F $80 = damaged
|
|
sta comcenSystemStatus,y ; 3D81 mark the system as knocked out
|
|
L_3D84:
|
|
dec comcenSystemIndex ; 3D84 next lower system
|
|
bne comcenSystemRollLoop ; 3D87 stop after system 1; index 0 is the energy display status, not a system
|
|
L_3D89:
|
|
rts ; 3D89 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initAllUnits - Resets all 100 unit records from their start positions and puts every living unit
|
|
; back on the battlefield map, then clears the 16 per-group state bytes. Run once when a battle
|
|
; starts, after the start-position template has been copied into the unit arrays.
|
|
; In: unitColTable/unitRowTable hold the start coordinates, unitTypeTable the types and
|
|
; unitFlagsTable the group bits taken from the template.
|
|
; Out: every per-unit array reset by initUnitRecord/placeUnitOnMap, map cells at the unit positions
|
|
; set to $80|index, groupStatusTable $91F4-$9203 zeroed; scratch18 = $FF.
|
|
; Called from: overlay A (the setup phase) at $7AEF.
|
|
; ----------------------------------------------------------------------
|
|
initAllUnits:
|
|
ldy #$63 ; 3D8A start at unit 99, the last unit of side 1
|
|
sty scratch18 ; 3D8C initUnitRecord takes the unit index in scratch18
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Reset units 99 down to 0.
|
|
; ----------------------------------------------------------------------
|
|
initUnitLoop:
|
|
jsr initUnitRecord ; 3D8E reset one unit and place it on the map
|
|
dec scratch18 ; 3D91 next lower unit index
|
|
bpl initUnitLoop ; 3D93 loop until scratch18 goes negative (unit 0 done)
|
|
ldy #$0F ; 3D95 16 group slots (group key = unitFlagsTable bits 2-5, i.e. 7 groups x 2 sides)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Clear the 16 groupStatusTable entries (slowest speed class in bits 0-2, worst terrain class in bits
|
|
; 3-5).
|
|
; ----------------------------------------------------------------------
|
|
clearGroupStatusLoop:
|
|
lda #$00 ; 3D97 no speed or terrain class recorded yet
|
|
sta groupStatusTable,y ; 3D99 $91F4+Y
|
|
dey ; 3D9C next group slot
|
|
bpl clearGroupStatusLoop ; 3D9D until all 16 are cleared
|
|
rts ; 3D9F return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initUnitRecord - Resets the order state of one unit: its path position and its destination are
|
|
; snapped to the cell it stands on, the destination is marked 'idle', the zone-scored bits and the
|
|
; stun counter are cleared, the group membership is kept and the facing and side bits are rebuilt.
|
|
; Falls straight through into placeUnitOnMap, which finishes the reset and puts the unit on the map.
|
|
; In: scratch18 = unit index (loaded into X here).
|
|
; Out: unitPathCol/Row = unitCol/Row (cloak and blitz bits cleared), unitDestCol = col|$80
|
|
; (stationary), unitDestRow = row, unitTypeTable bits 3-4 (zone scored) cleared, unitStunTable =
|
|
; 0, unitFlagsTable = group bits + facing + side bit; then everything placeUnitOnMap does.
|
|
; Called from: initAllUnits ($3D8E) for every unit at game start and recycleDeadUnits ($3E94) for a
|
|
; robot respawning at the recycler.
|
|
; ----------------------------------------------------------------------
|
|
initUnitRecord:
|
|
ldx scratch18 ; 3DA0 X = unit index (the caller passes it in scratch18)
|
|
lda unitColTable,x ; 3DA2 the unit's current map column
|
|
sta unitPathColTable,x ; 3DA5 path position = map position; bits 6-7 (cloak) are cleared as a side effect
|
|
ora #$80 ; 3DA8 bit 7 of unitDestCol = 'at its destination / stationary'
|
|
sta unitDestColTable,x ; 3DAA destination column = where it stands, marked idle
|
|
lda unitRowTable,x ; 3DAD its current map row
|
|
sta unitPathRowTable,x ; 3DB0 path row = map row; bits 6-7 (blitz) cleared with it
|
|
sta unitDestRowTable,x ; 3DB3 destination row = current row (bit 7 = 'repairing' cleared too)
|
|
lda unitTypeTable,x ; 3DB6 type byte: bits 0-2 type, bits 3-4 zone scored, bit 6 dug in, bit 7 dead
|
|
and #$E7 ; 3DB9 $E7 keeps everything except bits 3 and 4 = the two 'already scored this zone' flags
|
|
sta unitTypeTable,x ; 3DBB so a reset unit can score its terrain zones again
|
|
lda #$00 ; 3DBE clear the stun / dig-in byte ...
|
|
sta unitStunTable,x ; 3DC0 ... no stun counter and no dig-in progress
|
|
lda unitFlagsTable,x ; 3DC3 flags: facing 0-1, group id 2-4, side 5, group member 6, blitz charge 7
|
|
and #$5C ; 3DC6 $5C keeps bits 2-4 (group id) and bit 6 (is a group member)
|
|
sta unitFlagsTable,x ; 3DC8 facing, side and the one-shot charge bit are dropped
|
|
jsr initialFacingForUnit ; 3DCB A = 0 (face up) for our units, 1 (face down) for the opponent's
|
|
ora unitFlagsTable,x ; 3DCE merge the facing into bits 0-1
|
|
cpx #$32 ; 3DD1 units 0-49 are side 0, 50-99 side 1 (a fixed test here, not one of the patched ones)
|
|
bcc L_3DD7 ; 3DD3 side 0 leaves bit 5 clear
|
|
ora #$20 ; 3DD5 bit 5 = side 1
|
|
L_3DD7:
|
|
sta unitFlagsTable,x ; 3DD7 store the rebuilt flag byte and fall into placeUnitOnMap
|
|
|
|
; ----------------------------------------------------------------------
|
|
; placeUnitOnMap - Second half of the unit reset: restores the dug-in bit from the type byte, gives
|
|
; the unit a fresh movement allowance and full energy, copies the stationary and group bits into the
|
|
; display flags, remembers the terrain of the cell it stands on and writes $80|index into that map
|
|
; cell. A unit whose type byte has bit 7 set (destroyed) becomes an empty slot instead ($FF in
|
|
; unitTypeTable and unitTerrainUnderTable) and is not put on the map.
|
|
; In: X = scratch18 = unit index; unitColTable/unitRowTable = the cell to occupy.
|
|
; Out: unitStunTable bit 7 = dug in, unitHitFlashTable = 0, unitMovePointsTable = 10, unitEnergyTable
|
|
; = 50 (100%), unitDisplayFlagsTable = stationary bit + group bits, unitTerrainUnderTable = the
|
|
; terrain hidden by the unit, map cell at (col,row) = $80|index; mapCellPtr points at that cell, Y
|
|
; = 0.
|
|
; Called from: initUnitRecord (fall-through) and overlay A at $7D83, which re-places every unit once
|
|
; the setup phase has moved them around.
|
|
; ----------------------------------------------------------------------
|
|
placeUnitOnMap:
|
|
lda unitTypeTable,x ; 3DDA the type byte again
|
|
and #$40 ; 3DDD bit 6 = the unit has finished digging in
|
|
asl a ; 3DDF shift it into bit 7 ...
|
|
sta unitStunTable,x ; 3DE0 ... which is the 'dug in' bit of the stun / dig-in byte
|
|
lda #$00 ; 3DE3 clear the hit-flash / pinned countdown ...
|
|
sta unitHitFlashTable,x ; 3DE5 ... so the unit is drawn normally
|
|
lda #$0A ; 3DE8 10 movement points to start with
|
|
sta unitMovePointsTable,x ; 3DEA the signed movement-point balance ($FA28)
|
|
lda #$32 ; 3DED 50 energy points = 100% (the console shows it as a percentage)
|
|
sta unitEnergyTable,x ; 3DEF energy in bits 0-5; the visibility bits 6-7 stay clear (not spotted yet)
|
|
lda unitDestColTable,x ; 3DF2 the destination byte initUnitRecord has just written
|
|
and #$80 ; 3DF5 keep only bit 7 = stationary
|
|
sta unitDisplayFlagsTable,x ; 3DF7 the display flags start as 'stationary, nothing fired, not pinned'
|
|
jsr syncLocalGroupFlags ; 3DFA copy the authoritative group bits (unitFlagsTable & $7C) into the display copy
|
|
ldy scratch18 ; 3DFD Y = unit index
|
|
ldx unitColTable,y ; 3DFF X = its map column
|
|
lda unitRowTable,y ; 3E02 A = its map row ...
|
|
tay ; 3E05 ... which readMapCell expects in Y
|
|
jsr readMapCell ; 3E06 A = the byte in that cell, mapCellPtr = its address, Y = 0
|
|
ldx scratch18 ; 3E09 X = unit index again
|
|
sta unitTerrainUnderTable,x ; 3E0B remember the terrain the unit is about to cover
|
|
lda unitTypeTable,x ; 3E0E the type byte
|
|
bpl L_3E1D ; 3E11 bit 7 clear = the unit is alive, put it on the map
|
|
lda #$FF ; 3E13 $FF marks an unused unit slot ...
|
|
sta unitTypeTable,x ; 3E15 ... in the type array ...
|
|
sta unitTerrainUnderTable,x ; 3E18 ... and in the terrain-under array (= 'not on the map')
|
|
bmi L_3E22 ; 3E1B always taken ($FF is negative): a destroyed unit is not drawn
|
|
L_3E1D:
|
|
txa ; 3E1D A = unit index
|
|
ora #$80 ; 3E1E an occupied map cell holds $80|index
|
|
sta (mapCellPtr),y ; 3E20 claim the cell (Y is still 0 from readMapCell)
|
|
L_3E22:
|
|
rts ; 3E22 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; recycleDeadUnits - Once per round each side that owns a recycler gets one destroyed robot back. For
|
|
; a side whose recycler cell is not occupied it scans that side's 50 units downwards from the comcen
|
|
; for destroyed ones (type bit 7 set, but not the $FF empty-slot marker). With recycler rate FULL
|
|
; (mode 0) the first destroyed unit found is respawned at once; with HALF (mode 1) two destroyed units
|
|
; of the SAME type are needed - the first is thrown away (slot set to $FF) and the second respawns.
|
|
; The new robot takes the recycler's coordinates, keeps its type, loses all group membership and is
|
|
; run through initUnitRecord.
|
|
; In: gameEndReason $0BA8 (must be 0), recyclerCol/recyclerRow $92A8/$92AA (bit 7 set = that side has
|
|
; no recycler, rate NONE), recyclerMode $92A6 (0 = FULL, 1 = HALF), perSideValueTable $0C8A = each
|
|
; side's comcen index ($31/$63), the unit arrays.
|
|
; Out: at most one unit per side respawned: its unitCol/unitRow, unitTypeTable, unitFlagsTable and
|
|
; everything initUnitRecord touches; for the local side pendingRedrawUnit = the new unit and
|
|
; message 5 'UNIT RECYCLED.' is queued. scratch18/19/1A/1B/1C/1D used as scratch.
|
|
; Called from: finishRound at $465A, i.e. once per completed round of 100 unit steps.
|
|
; ----------------------------------------------------------------------
|
|
recycleDeadUnits:
|
|
lda gameEndReason ; 3E23 0 while a battle is in progress
|
|
beq L_3E29 ; 3E26 carry on only while the game is running
|
|
rts ; 3E28 no recycling once the game is over
|
|
|
|
L_3E29:
|
|
lda #$01 ; 3E29 start with side 1 ...
|
|
sta scratch1A ; 3E2B ... scratch1A is the side index, counted down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Outer loop over the two sides, 1 then 0.
|
|
; ----------------------------------------------------------------------
|
|
recyclerSideLoop:
|
|
ldy scratch1A ; 3E2D Y = side index
|
|
ldx recyclerCol,y ; 3E2F X = that side's recycler column; bit 7 set = no recycler (rate NONE)
|
|
bmi L_3E3B ; 3E32 no recycler for this side - N is still set, so the test at $3E3B sends us on
|
|
lda recyclerRow,y ; 3E34 recycler row ...
|
|
tay ; 3E37 ... readMapCell wants the row in Y and the column in X
|
|
jsr readMapCell ; 3E38 A = what is standing on the recycler cell
|
|
L_3E3B:
|
|
bmi L_3EB5 ; 3E3B N set = no recycler, or a unit is parked on the recycler: skip this side
|
|
ldy scratch1A ; 3E3D Y = side index
|
|
lda recyclerMode,y ; 3E3F recycler rate: 0 = FULL (one for one), 1 = HALF (two of a type for one)
|
|
sta L_3E5B+1 ; 3E42 patch it into the LDY immediate at $3E5B so the rate is tested inside the loop
|
|
ldx comcenUnitIndexBySide,y ; 3E45 $0C8A = comcen unit index per side ($31 = 49, $63 = 99)
|
|
stx scratch1C ; 3E48 scan downwards from the comcen
|
|
ldy #$32 ; 3E4A 50 units per side
|
|
sty scratch1D ; 3E4C scratch1D = units left to examine
|
|
sty scratch19 ; 3E4E scratch19 = type of the previous destroyed unit; 50 can never match a real type (0-4)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Scan this side's 50 units from the comcen downwards, looking for destroyed robots.
|
|
; ----------------------------------------------------------------------
|
|
recyclerScanLoop:
|
|
ldx scratch1C ; 3E50 X = unit being examined
|
|
lda unitTypeTable,x ; 3E52 type byte
|
|
bpl L_3EAF ; 3E55 bit 7 clear = the unit is alive, keep looking
|
|
cmp #$FF ; 3E57 $FF = empty slot (already recycled away)
|
|
beq L_3EAF ; 3E59 not a destroyed unit either
|
|
L_3E5B:
|
|
ldy #$FF ; 3E5B operand patched at $3E42 with the recycler rate; the LDY sets Z from it
|
|
bne L_3E65 ; 3E5D rate HALF: a matching pair is needed
|
|
and #$07 ; 3E5F rate FULL: keep the plain type in bits 0-2 ...
|
|
sta scratch19 ; 3E61 ... as the type of the unit to respawn
|
|
bpl respawnUnitAtRecycler ; 3E63 always taken (a type is 0-4): respawn this one immediately
|
|
L_3E65:
|
|
and #$07 ; 3E65 rate HALF: plain type of this destroyed unit
|
|
cmp scratch19 ; 3E67 does it match the type of the destroyed unit found before it?
|
|
bne L_3EAB ; 3E69 no - remember this one and keep scanning
|
|
ldx scratch1B ; 3E6B X = the first unit of the pair
|
|
jsr removeUnitFromMap ; 3E6D take it off the map, restoring the terrain under it
|
|
lda #$FF ; 3E70 $FF = empty slot ...
|
|
sta unitTypeTable,x ; 3E72 ... the first of the pair is consumed and gone for good
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Respawn unit scratch1C of type scratch19 on the recycler cell.
|
|
; ----------------------------------------------------------------------
|
|
respawnUnitAtRecycler:
|
|
ldx scratch1C ; 3E75 X = the unit that will come back
|
|
jsr removeUnitFromMap ; 3E77 make sure it is not on the map anywhere
|
|
ldy scratch1A ; 3E7A Y = side index
|
|
lda recyclerCol,y ; 3E7C recycler column ...
|
|
sta unitColTable,x ; 3E7F ... becomes the new unit's column
|
|
lda recyclerRow,y ; 3E82 recycler row ...
|
|
sta unitRowTable,x ; 3E85 ... becomes the new unit's row
|
|
lda scratch19 ; 3E88 the plain type (bit 7 cleared, so the unit is alive again)
|
|
sta unitTypeTable,x ; 3E8A store it back into the type array
|
|
lda #$00 ; 3E8D no facing, no group, side bit rebuilt by initUnitRecord ...
|
|
sta unitFlagsTable,x ; 3E8F ... a recycled robot always comes back ungrouped
|
|
stx scratch18 ; 3E92 initUnitRecord takes the unit index in scratch18
|
|
jsr initUnitRecord ; 3E94 reset its orders and put it on the map at the recycler
|
|
lda scratch1A ; 3E97 which side got the replacement?
|
|
cmp playerSide ; 3E99 only tell the player about his own robots
|
|
bne L_3EB5 ; 3E9C the opponent's recycling is silent here (his machine reports it)
|
|
lda scratch18 ; 3E9E the new unit ...
|
|
sta pendingRedrawUnit ; 3EA0 ... is queued for a redraw by the next updateGameFrame
|
|
lda #$05 ; 3EA3 message 5 = 'UNIT RECYCLED.'
|
|
jsr queueMessageUnlessFilm ; 3EA5 queue it (dropped while a film is playing)
|
|
jmp L_3EB5 ; 3EA8 one respawn per side per round: go to the next side
|
|
|
|
L_3EAB:
|
|
sta scratch19 ; 3EAB remember this destroyed unit's type ...
|
|
stx scratch1B ; 3EAD ... and its index, as the first half of a possible pair
|
|
L_3EAF:
|
|
dec scratch1C ; 3EAF next lower unit index
|
|
dec scratch1D ; 3EB1 one of the 50 units done
|
|
bne recyclerScanLoop ; 3EB3 keep scanning until this side's 50 units are exhausted
|
|
L_3EB5:
|
|
dec scratch1A ; 3EB5 next side
|
|
bmi L_3EBC ; 3EB7 both sides done
|
|
jmp recyclerSideLoop ; 3EB9 round two, for side 0
|
|
|
|
L_3EBC:
|
|
rts ; 3EBC return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; removeUnitFromMap - Lifts unit X off the battlefield map: the terrain byte the unit was hiding is
|
|
; written back into its cell, the unit is marked as 'not on the map' (unitTerrainUnderTable = $FF) and
|
|
; its hit-flash/pinned countdown is cleared. Does nothing when the unit is not on the map already.
|
|
; In: X = unit index; unitTerrainUnderTable[X] = the saved terrain ($FF = not on the map);
|
|
; unitColTable/unitRowTable = its cell.
|
|
; Out: map cell restored to terrain, unitTerrainUnderTable[X] = $FF, unitHitFlashTable[X] = 0,
|
|
; mapCellPtr points at the cell, Y = 0.
|
|
; Called from: recycleDeadUnits ($3E6D and $3E77) and overlay A at $7D9A (clearing the board before
|
|
; the units are re-placed).
|
|
; ----------------------------------------------------------------------
|
|
removeUnitFromMap:
|
|
lda unitTerrainUnderTable,x ; 3EBD the terrain byte hidden under the unit
|
|
bmi L_3EE1 ; 3EC0 $FF = the unit is not on the map, nothing to restore
|
|
|
|
; ----------------------------------------------------------------------
|
|
; removeUnitLeavingCell - Same as removeUnitFromMap but the caller chooses what stays behind in the
|
|
; unit's cell (A) instead of the saved terrain. Used when a destroyed unit should leave a
|
|
; wreck/marker glyph rather than plain terrain.
|
|
; In: A = byte to write into the unit's map cell, X = unit index.
|
|
; Out: map cell = A, unitTerrainUnderTable[X] = $FF, unitHitFlashTable[X] = 0, Y = 0; the LDX operand
|
|
; at L_3ED7+1 is patched with the unit index.
|
|
; Called from: updateHitFlashTimers at $4894, when the flash of a destroyed unit expires.
|
|
; ----------------------------------------------------------------------
|
|
removeUnitLeavingCell:
|
|
stx L_3ED7+1 ; 3EC2 park the unit index in the LDX operand at $3ED7 (X is needed for the column)
|
|
pha ; 3EC5 save the byte to leave in the cell
|
|
ldy unitRowTable,x ; 3EC6 Y = the unit's map row
|
|
lda unitColTable,x ; 3EC9 A = its map column ...
|
|
tax ; 3ECC ... readMapCell wants the column in X
|
|
jsr readMapCell ; 3ECD sets mapCellPtr to the unit's cell (and returns Y = 0)
|
|
pla ; 3ED0 get the byte to leave behind back
|
|
ldy #$00 ; 3ED1 index 0 of the cell pointer
|
|
sta (mapCellPtr),y ; 3ED3 write it into the map
|
|
lda #$FF ; 3ED5 $FF = this unit is no longer on the map
|
|
L_3ED7:
|
|
ldx #$FF ; 3ED7 operand patched at $3EC2 with the unit index
|
|
sta unitTerrainUnderTable,x ; 3ED9 mark the unit as off the map
|
|
lda #$00 ; 3EDC clear the hit-flash / pinned countdown ...
|
|
sta unitHitFlashTable,x ; 3EDE ... so nothing tries to redraw the vacated cell later
|
|
L_3EE1:
|
|
rts ; 3EE1 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyBlitzStepCost - Charges a blitzing unit for the step it has just taken: it loses one energy
|
|
; point and gains 25 movement points (SET BLITZ = 'move extra fast'), and its next shot gets the
|
|
; charge bonus (unitFlagsTable bit 7). A unit that has arrived or that has run out of energy pays
|
|
; nothing. When the blitzing unit is the local comcen in a live battle it also starts the comcen
|
|
; engine sound.
|
|
; In: Y = unit index; the caller has already checked that the blitz bit (unitPathRowTable bit 7) is
|
|
; set; gamePhase zp_B1, filmPlaybackMode $0B9B, comcenUnit $9236, D_67CA = the sound currently on
|
|
; SID voice 2.
|
|
; Out: unitFlagsTable[Y] bit 7 set, one energy point taken through reduceUnitEnergyBy (hitDamage = 1),
|
|
; unitMovePointsTable[Y] += 25 saturating at $7F; possibly sound $16.
|
|
; Called from: processUnitStep at $433C after a successful move step, and by the blitz command
|
|
; handlers at $5130 / $5094.
|
|
; ----------------------------------------------------------------------
|
|
applyBlitzStepCost:
|
|
lda unitDestColTable,y ; 3EE2 destination byte, bit 7 = the unit is already where it wanted to go
|
|
bmi L_3F26 ; 3EE5 a stationary unit cannot blitz
|
|
lda unitEnergyTable,y ; 3EE7 energy byte
|
|
and #$3F ; 3EEA bits 0-5 = energy 0-50, bits 6-7 are visibility
|
|
beq L_3F26 ; 3EEC no energy left, the blitz fizzles
|
|
lda unitFlagsTable,y ; 3EEE unit flags
|
|
ora #$80 ; 3EF1 bit 7 = one-shot charge bonus applied to the next attack
|
|
sta unitFlagsTable,y ; 3EF3 store it back
|
|
lda #$01 ; 3EF6 one energy point is the price of a blitz step
|
|
jsr reduceUnitEnergyBy ; 3EF8 hitDamage = 1, then subtract it from unit Y's energy
|
|
lda gamePhase ; 3EFB 0 only during the real-time battle
|
|
ora filmPlaybackMode ; 3EFD and only when this is not a film / solo replay
|
|
bne L_3F19 ; 3F00 no engine sound outside a live battle
|
|
cpy comcenUnit ; 3F02 only the local player's own comcen makes engine noise
|
|
bne L_3F19 ; 3F05 any other unit is silent
|
|
lda sndVoice2SoundId ; 3F07 the sound id currently playing on SID voice 2
|
|
beq L_3F14 ; 3F0A voice free
|
|
cmp #$14 ; 3F0C $14 = comcen moving (normal speed)
|
|
beq L_3F14 ; 3F0E the engine is already running, restart it as a blitz
|
|
cmp #$16 ; 3F10 $16 = comcen blitzing
|
|
bne L_3F19 ; 3F12 some other effect owns the voice - do not interrupt it
|
|
L_3F14:
|
|
lda #$16 ; 3F14 sound $16 = comcen engine at blitz speed
|
|
jsr playSound ; 3F16 request it
|
|
L_3F19:
|
|
lda unitMovePointsTable,y ; 3F19 current movement-point balance (signed)
|
|
clc ; 3F1C clear the carry for the addition ...
|
|
adc #$19 ; 3F1D +25 points buys roughly one extra step
|
|
bvc L_3F23 ; 3F1F no signed overflow, keep the sum
|
|
lda #$7F ; 3F21 clamp at +127 so the balance stays positive
|
|
L_3F23:
|
|
sta unitMovePointsTable,y ; 3F23 store the new balance
|
|
L_3F26:
|
|
rts ; 3F26 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawRecyclerOnMap - Puts the recycler glyph ($67) on the battlefield map at side Y's recycler
|
|
; position and hands the caller everything drawMapCell needs to show it. If a unit happens to be
|
|
; standing on the recycler cell the glyph is stored in that unit's 'terrain under the unit' entry
|
|
; instead, so it appears as soon as the unit leaves, and the unit's own map byte is returned for the
|
|
; redraw. Returns immediately when that side has no recycler.
|
|
; In: Y = side index (0 or 1); recyclerCol $92A8 / recyclerRow $92AA (bit 7 of the column set = no
|
|
; recycler).
|
|
; Out: X = column, Y = row, A = the byte to draw ($67, or $80|unit when a unit is on the cell); the
|
|
; map cell or unitTerrainUnderTable updated; the three immediate operands at $3F51/$3F53/$3F55 are
|
|
; patched with the return values.
|
|
; Called from: checkEnemyRecyclerSpotted ($3F86), the recycler-placement command handler ($550D),
|
|
; overlay A ($7B92, $7D6A) and the modem driver ($EE47, which jumps here to draw our own recycler
|
|
; at game start).
|
|
; ----------------------------------------------------------------------
|
|
drawRecyclerOnMap:
|
|
lda #$67 ; 3F27 $67 = the recycler glyph in the terrain character set
|
|
sta L_3F54+1 ; 3F29 default return value: patch it into the LDA at $3F54
|
|
ldx recyclerCol,y ; 3F2C X = recycler column; bit 7 set = this side has no recycler
|
|
bpl L_3F32 ; 3F2F there is one, go on
|
|
rts ; 3F31 rate NONE: nothing to draw
|
|
|
|
L_3F32:
|
|
stx L_3F50+1 ; 3F32 return the column through the LDX operand at $3F50
|
|
lda recyclerRow,y ; 3F35 recycler row ...
|
|
tay ; 3F38 ... into Y for readMapCell
|
|
sty L_3F52+1 ; 3F39 return the row through the LDY operand at $3F52
|
|
jsr readMapCell ; 3F3C A = current contents of the recycler cell, mapCellPtr = its address
|
|
bpl L_3F4C ; 3F3F bit 7 clear = the cell is free terrain
|
|
sta L_3F54+1 ; 3F41 a unit is standing there: return its map byte so the caller redraws the unit
|
|
tay ; 3F44 Y = $80|unit index
|
|
lda #$67 ; 3F45 the recycler glyph ...
|
|
sta unitTerrainUnderAlias,y ; 3F47 ... goes into that unit's saved-terrain slot ($FA70+$80+unit = $FAF0+unit) so it reappears when the unit moves off
|
|
bne L_3F50 ; 3F4A always taken ($67 is not zero)
|
|
L_3F4C:
|
|
lda #$67 ; 3F4C free cell: write the recycler glyph ...
|
|
sta (mapCellPtr),y ; 3F4E ... straight into the map (Y = 0 from readMapCell)
|
|
L_3F50:
|
|
ldx #$FF ; 3F50 operand patched at $3F32: X = recycler column
|
|
L_3F52:
|
|
ldy #$FF ; 3F52 operand patched at $3F39: Y = recycler row
|
|
L_3F54:
|
|
lda #$FF ; 3F54 operand patched at $3F29/$3F41: A = the cell value to draw
|
|
rts ; 3F56 return with X/Y/A set up for drawMapCell
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkEnemyRecyclerSpotted - Reveals the opponent's recycler the first time one of our own uncloaked
|
|
; robots gets within 3 cells of it: it draws the recycler glyph on the map, queues message 8 'SPOTTED
|
|
; RECYCLER!' and then patches its own first byte to RTS so the test never runs again.
|
|
; In: playerSide $0B9F, perSideValueTable $0C8A (comcen index per side), the opponent's
|
|
; recyclerCol/recyclerRow; the area-scan lists at $0400/$0432.
|
|
; Out: the enemy recycler cell drawn, message 8 queued, the byte at $3F57 changed from NOP to RTS.
|
|
; The byte is set back to NOP by initIrqAndInputState ($0BBD) at game start, and the modem driver
|
|
; writes RTS there directly ($EE3E) when the opponent told us he has no recycler.
|
|
; Called from: finishRound at $4657, once per round.
|
|
; ----------------------------------------------------------------------
|
|
checkEnemyRecyclerSpotted:
|
|
nop ; 3F57 self-patched: NOP while the search is live, $60 (RTS) once the recycler has been spotted
|
|
lda playerSide ; 3F58 the local player's side (0 or 1)
|
|
eor #$01 ; 3F5B the other side
|
|
tax ; 3F5D X = opponent side index
|
|
sta L_3F84+1 ; 3F5E patch it into the LDY operand at $3F84 for the later drawRecyclerOnMap
|
|
ldy comcenUnitIndexBySide,x ; 3F61 Y = the opponent's comcen unit index ($31 or $63)
|
|
jsr setScanFilterOppositeSide; 3F64 set the area-scan filter to 'units of the side opposite to Y', i.e. our own units
|
|
ldy recyclerRow,x ; 3F67 Y = the opponent's recycler row
|
|
lda recyclerCol,x ; 3F6A A = its column; bit 7 set = he has no recycler
|
|
bmi L_3F96 ; 3F6D nothing to spot
|
|
tax ; 3F6F X = column (collectUnitsInRadius wants column in X, row in Y)
|
|
lda #$03 ; 3F70 search radius 3 cells around the recycler
|
|
jsr collectUnitsInRadius ; 3F72 collect our units in that square into $0400 (index) / $0432 (distance)
|
|
bmi L_3F96 ; 3F75 X = $FF: no unit of ours is near the recycler
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the candidate list backwards looking for one robot that is not cloaked - a cloaked robot cannot
|
|
; report what it sees.
|
|
; ----------------------------------------------------------------------
|
|
recyclerSpotterLoop:
|
|
ldy checksumXorConstant,x ; 3F77 candidate unit index from the scan list at $0400+X (the label is a leftover from the copy-protection table that shares this address) (also trainerPlaybook0200:play3Col12)
|
|
lda unitPathColTable,y ; 3F7A path column byte; bit 7 set = the unit is cloaked
|
|
bpl L_3F84 ; 3F7D uncloaked: the recycler has been spotted
|
|
dex ; 3F7F next candidate
|
|
bpl recyclerSpotterLoop ; 3F80 until the list is exhausted
|
|
bmi L_3F96 ; 3F82 all nearby units are cloaked - try again next round
|
|
L_3F84:
|
|
ldy #$FF ; 3F84 operand patched at $3F5E: Y = the opponent's side index
|
|
jsr drawRecyclerOnMap ; 3F86 put the recycler glyph into the map and return the cell in X/Y/A
|
|
jsr drawMapCell ; 3F89 redraw that cell in the overview map and in the tactical view
|
|
lda #$08 ; 3F8C message 8 = 'SPOTTED RECYCLER!'
|
|
jsr queueMessageUnlessFilm ; 3F8E queue it (silent while a film is playing)
|
|
lda #$60 ; 3F91 $60 = RTS opcode
|
|
sta checkEnemyRecyclerSpotted; 3F93 disable this routine permanently: the recycler stays visible from now on
|
|
L_3F96:
|
|
rts ; 3F96 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initialFacingForUnit - Returns the facing a unit starts with: 0 (up, towards the enemy) for the
|
|
; local player's units and 1 (down) for the opponent's, since each player sees his own side at the
|
|
; bottom of the map.
|
|
; In: X = unit index; the branch opcode at D_3F9E is patched by overlay A ($7BD5) to BCC when the
|
|
; player is side 0 and BCS when he is side 1, so 'mine' means index < 50 or index >= 50
|
|
; respectively.
|
|
; Out: A = 0 or 1 (facing code, bits 0-1 of unitFlagsTable).
|
|
; Called from: initUnitRecord ($3DCB) only.
|
|
; ----------------------------------------------------------------------
|
|
initialFacingForUnit:
|
|
lda playerSide ; 3F97 dead instruction: the value is thrown away by the LDA #$00 that follows (leftover of an earlier version that read the side here)
|
|
lda #$00 ; 3F9A assume facing 0 = up
|
|
cpx #$32 ; 3F9C unit index against 50, the boundary between the two sides
|
|
initialFacingSideBranch:
|
|
bcc L_3FA2 ; 3F9E opcode byte patched per player side by overlay A ($7BD5): BCC for side 0, BCS for side 1
|
|
eor #$01 ; 3FA0 the opponent's units face the other way (facing 1 = down)
|
|
L_3FA2:
|
|
rts ; 3FA2 return the facing code in A
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorMapCoordinate - Returns 39 - A, mirroring a map column or row across the 40x40 battlefield.
|
|
; Both machines run the same simulation but each player looks at the board from his own end, so every
|
|
; coordinate that crosses the link is passed through here.
|
|
; In: A = coordinate 0..39.
|
|
; Out: A = 39 - A; C reflects the internal negate, not a result.
|
|
; Called from: moveUnitToCell ($4785, zone scoring), the command handlers ($4FEA/$4FF1), overlay A
|
|
; ($7013), the modem driver ($ECC9) and the solo trainer (six places).
|
|
; ----------------------------------------------------------------------
|
|
mirrorMapCoordinate:
|
|
sec ; 3FA3 prepare the subtraction
|
|
sbc #$27 ; 3FA4 A - 39
|
|
eor #$FF ; 3FA6 one's complement ...
|
|
clc ; 3FA8 clear the carry the SEC/SBC above left set ...
|
|
adc #$01 ; 3FA9 ... plus one = two's complement, so A = 39 - A
|
|
rts ; 3FAB return the mirrored coordinate
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; haltStunnedUnit - Cancels the orders of a unit that is stunned (a drone hit stuns everything around
|
|
; it): its waypoint and destination are snapped back to the cell it stands on, it is marked stationary
|
|
; and it is dropped out of its group so the group does not wait for it. With the UNIT MENUS rule off
|
|
; only the comcen can be stunned this way, and in a solo game the trainer's own units (index >= 50)
|
|
; are left alone.
|
|
; In: Y = unit index; gameTypeOptions $0BA3 bit 3 = the UNIT MENUS option; comcenUnit $9236;
|
|
; soloTrainerAtGameStart $92FE bit 7.
|
|
; Out: unitPathCol/Row = unitCol/Row, unitDestCol = col|$80 (stationary), unitDestRow = row,
|
|
; unitFlagsTable bit 6 (group member) cleared, unitDisplayFlagsTable bit 6 cleared and bit 7
|
|
; (stationary) set.
|
|
; Called from: processUnitStep at $4272 while the unit's stun counter is running, and from the
|
|
; drone-screen overlay at $72DF.
|
|
; ----------------------------------------------------------------------
|
|
haltStunnedUnit:
|
|
lda gameTypeOptions ; 3FAC the rule options of this game
|
|
and #$08 ; 3FAF bit 3 = UNIT MENUS (cloak / blitz / dig in available)
|
|
bne L_3FB8 ; 3FB1 with the advanced commands on, every stunned unit is halted
|
|
cpy comcenUnit ; 3FB3 with them off only the local comcen reacts to a stun ...
|
|
bne L_3FE7 ; 3FB6 ... every other unit keeps its orders
|
|
L_3FB8:
|
|
lda soloTrainerAtGameStart ; 3FB8 bit 7 = this game was started against the solo trainer
|
|
bpl L_3FC1 ; 3FBB modem game: halt either side's units
|
|
cpy #$32 ; 3FBD units 50-99 belong to the trainer ...
|
|
bcs L_3FE7 ; 3FBF ... and it moves them itself, so leave them alone
|
|
L_3FC1:
|
|
lda unitColTable,y ; 3FC1 the cell the unit stands on
|
|
sta unitPathColTable,y ; 3FC4 waypoint/path column = current column (cloak bits cleared with it)
|
|
ora #$80 ; 3FC7 bit 7 = at destination / stationary
|
|
sta unitDestColTable,y ; 3FC9 destination column = where it stands
|
|
lda unitRowTable,y ; 3FCC current row
|
|
sta unitPathRowTable,y ; 3FCF waypoint/path row = current row (blitz bits cleared)
|
|
sta unitDestRowTable,y ; 3FD2 destination row too - the unit has nowhere to go
|
|
lda unitFlagsTable,y ; 3FD5 unit flags
|
|
and #$BF ; 3FD8 $BF clears bit 6 = 'member of a group'
|
|
sta unitFlagsTable,y ; 3FDA a stunned unit drops out of its group
|
|
lda unitDisplayFlagsTable,y ; 3FDD the display copy of the flags
|
|
and #$BF ; 3FE0 clear its group-member bit as well ...
|
|
ora #$80 ; 3FE2 ... and set bit 7 = stationary
|
|
sta unitDisplayFlagsTable,y ; 3FE4 store the display flags back
|
|
L_3FE7:
|
|
rts ; 3FE7 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; haltAllUnitsAndReveal - Game-over clean-up: repairs the three comcen system status bytes, stops all
|
|
; 100 units (stationary, nothing fired, nothing pinned, waypoint snapped back onto the unit) and
|
|
; reveals the whole board by marking every unit as spotted. Redraws the overview and the tactical
|
|
; view when the battlefield screen is up.
|
|
; In: currentScreen $90FB (0 = battlefield).
|
|
; Out: comcenSystemStatus[1..3] = 0, unitDisplayFlagsTable bits 0-1 cleared and bit 7 set for all
|
|
; units, unitPathCol/Row = unitCol/Row keeping the cloak/blitz bits, every unit's visibility bits
|
|
; 6-7 set by markAllUnitsVisible, screen redrawn.
|
|
; Called from: endGame ($402F) only.
|
|
; ----------------------------------------------------------------------
|
|
haltAllUnitsAndReveal:
|
|
lda #$00 ; 3FE8 0 = system healthy ...
|
|
sta comcenBattleStatus ; 3FEA ... comcen system 1 (BATTLE)
|
|
sta comcenRadarStatus ; 3FED ... comcen system 2 (RADAR)
|
|
sta comcenDroneStatus ; 3FF0 ... comcen system 3 (DRONE); the damage is irrelevant now
|
|
ldy #$63 ; 3FF3 walk units 99 down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Stop every unit and snap its waypoint back onto its real position.
|
|
; ----------------------------------------------------------------------
|
|
haltUnitLoop:
|
|
lda unitDisplayFlagsTable,y ; 3FF5 display flags of the unit
|
|
and #$FC ; 3FF8 $FC clears bit 0 (fired/reloading) and bit 1 (pinned)
|
|
ora #$80 ; 3FFA bit 7 = stationary
|
|
sta unitDisplayFlagsTable,y ; 3FFC store them back
|
|
lda unitPathColTable,y ; 3FFF path column
|
|
and #$C0 ; 4002 keep bits 6-7 = the cloak flags
|
|
ora unitColTable,y ; 4004 merge the unit's real column in
|
|
sta unitPathColTable,y ; 4007 the waypoint is now the unit's own cell
|
|
lda unitPathRowTable,y ; 400A path row
|
|
and #$C0 ; 400D keep bits 6-7 = the blitz flags
|
|
ora unitRowTable,y ; 400F merge the unit's real row in
|
|
sta unitPathRowTable,y ; 4012 store the waypoint row
|
|
dey ; 4015 next unit
|
|
bpl haltUnitLoop ; 4016 until unit 0 has been done
|
|
jsr markAllUnitsVisible ; 4018 set the visibility bits 6-7 of every unit's energy byte: the board is revealed
|
|
lda currentScreen ; 401B which screen is showing?
|
|
bne L_4026 ; 401E only the battlefield screen (0) needs redrawing
|
|
jsr drawOverviewMap ; 4020 redraw the 40x40 overview map on the left
|
|
jsr drawViewport ; 4023 redraw the 7x5 tactical view on the right
|
|
L_4026:
|
|
rts ; 4026 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; endGame - Ends the battle once the caller has stored the reason in gameEndReason $0BA8 ($08 = the
|
|
; clock ran out, $A0/$C0 = one side's comcen was knocked out). It drops any drone in flight, halts
|
|
; and reveals all units, works out and shows the result, and starts the countdown that takes the
|
|
; player back to the menu.
|
|
; In: gameEndReason $0BA8; the two 16-bit score totals $92A2-$92A5 (read by computeGameResult).
|
|
; Out: droneControlFlags $920D = 0, droneArrivalFlags $92F9 = 0, all units halted and revealed, result
|
|
; message queued, gameEndCountdown $92C9 = $78 (the IRQ bumps it every 8th frame; at $80
|
|
; exitIfGameEnded leaves for the menu, i.e. about 64 frames later).
|
|
; Called from: applyHitToUnit at $3BD3 (comcen knocked out, via JMP) and finishRound at $460B (clock
|
|
; ran out).
|
|
; ----------------------------------------------------------------------
|
|
endGame:
|
|
lda #$00 ; 4027 clear the drone state ...
|
|
sta droneControlFlags ; 4029 ... no drone is under control any more
|
|
sta droneArrivalFlags ; 402C ... and no drone is on its way in
|
|
jsr haltAllUnitsAndReveal ; 402F stop every unit and reveal the whole board
|
|
jsr computeGameResult ; 4032 compare the scores, queue the WON/LOST/DRAW message and set gameOverFlags
|
|
lda #$78 ; 4035 $78 = start value of the game-end countdown
|
|
sta gameEndCountdown ; 4037 the IRQ counts it up every 8 frames; when it reaches $80 the game returns to the menu
|
|
rts ; 403A return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; markComcenRevealed - Launching a radar sweep or a drone gives the launching comcen away: it sets
|
|
; that side's comcen-reveal timer to 6 rounds and forces the visibility bits of that comcen so the
|
|
; other player can see it on the map. (The disassembler prints this routine as .byte because it is
|
|
; only reached through the command dispatch table.)
|
|
; In: X = 0 for our own launch, non-zero for the opponent's; playerSide $0B9F; perSideValueTable
|
|
; $0C8A = comcen index per side ($31/$63).
|
|
; Out: comcenRevealTimer[side] $92B2 = 6, unitEnergyTable[that comcen] bits 6-7 set (spotted +
|
|
; visible); for the opponent's launch the comcen is also queued for redraw in pendingRedrawUnit.
|
|
; A and Y clobbered.
|
|
; Called from: the radar command handler ($53BC) and the drone command handler ($53F1).
|
|
; ----------------------------------------------------------------------
|
|
markComcenRevealed:
|
|
lda playerSide ; 403B the local player's side
|
|
cpx #$00 ; 403E X = 0 means we launched
|
|
beq L_4044 ; 4040 our own side then
|
|
eor #$01 ; 4042 otherwise the opponent's side
|
|
L_4044:
|
|
tay ; 4044 Y = the side whose comcen is exposed
|
|
lda #$06 ; 4045 6 rounds of exposure
|
|
sta comcenRevealTimer,y ; 4047 $92B2+side, ticked down once per round by finishRound
|
|
lda comcenUnitIndexBySide,y ; 404A comcen unit index of that side ($31 = 49 or $63 = 99)
|
|
tay ; 404D Y = the comcen unit
|
|
lda unitEnergyTable,y ; 404E its energy byte
|
|
ora #$C0 ; 4051 bits 6-7 = seen this cycle + visible
|
|
sta unitEnergyTable,y ; 4053 the comcen is now drawn on the enemy's map
|
|
cpx #$00 ; 4056 was this the opponent's launch?
|
|
beq L_405D ; 4058 our own comcen is already drawn, nothing to redraw
|
|
sty pendingRedrawUnit ; 405A queue the opponent's comcen for a redraw next frame
|
|
L_405D:
|
|
rts ; 405D return
|
|
|
|
|
|
; terrainMoveCostTable - movement points charged for one step, indexed by the terrain class 0-7 that
|
|
; tryStepTowardTarget derived from the cell the unit is standing on (class table at $077E), or by the
|
|
; group's worst terrain class for a grouped unit. Spies always use class 0. A diagonal step costs 8
|
|
; points more. 20, 65, 30, 40, 20, 40, 20, 20 - class 1 (hills/water) is by far the most expensive.
|
|
; Read by tryStepTowardTarget ($45DA) and by rebuildGroupTables ($493A). Note the $077E class table
|
|
; can return 8, which reads one byte past the end into speedClassRateTable[0] = 12.
|
|
terrainMoveCostTable:
|
|
.byte $14,$41,$1E,$28,$14,$28,$14,$14; 405E .A.(.(.. cost per step for terrain classes 0-7: 20,65,30,40,20,40,20,20
|
|
|
|
; speedClassRateTable - movement points handed out at the end of each round, indexed by the unit type
|
|
; for types 0-3 (GRUNT 12, RIDER 25, BOOMER 12, SPY 25); for a comcen (type 4) the index is that
|
|
; side's COMCEN SPEED setting ($92B4/$92B5) instead, and for a grouped unit it is the group's slowest
|
|
; class (groupStatusTable bits 0-2). Entry 4 = 0 means an immobile comcen. Read by finishRound ($46A4)
|
|
; and rebuildGroupTables ($4958).
|
|
speedClassRateTable:
|
|
.byte $0C,$19,$0C,$19,$00 ; 4066 ..... points per round by speed class 0-4: 12,25,12,25,0
|
|
|
|
; zoneScoredBitTable - the unitTypeTable bit that records 'this unit has already scored the terrain
|
|
; points of that zone': [1] = $08 for the near zone (mirrored column 10-19), [2] = $10 for the far
|
|
; zone (column 0-9). Entry 0 ($28) is never indexed - the zone number computed in moveUnitToCell is
|
|
; only ever 1 or 2. Read by moveUnitToCell ($479A and $47DE).
|
|
zoneScoredBitTable:
|
|
.byte $28,$08,$10 ; 406B (.. index 0 unused, 1 = near zone bit $08, 2 = far zone bit $10
|
|
|
|
; unitMovedFlag - a single byte, not a table: set to 1 by moveUnitToCell when it really moved the
|
|
; current unit, cleared at the top of every unit step. processUnitStep reads it at $436F to decide
|
|
; whether the cell the unit came from has to be redrawn.
|
|
unitMovedFlag:
|
|
.byte $00 ; 406E . 1 = the current unit changed map cells during this step
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitFramesOrInput - Interruptible pause that keeps the battle running: it advances the game one
|
|
; frame at a time for up to A frames and returns early on a fresh fire-button press, on the joystick
|
|
; leaving its centre position, or when the game-end countdown starts.
|
|
; In: A = number of frames to wait; Y = the value to hand back in Y (parked in the LDY operand at
|
|
; L_4091+1).
|
|
; Out: Y = the entry value, A/X clobbered, the game advanced by however many frames actually ran. The
|
|
; remaining frame count lives in the LDA operand at L_4085+1.
|
|
; Called from: the menu/console loops at $63C5, $6457 and $64C5.
|
|
; ----------------------------------------------------------------------
|
|
waitFramesOrInput:
|
|
sty L_4091+1 ; 406F remember the caller's Y in the LDY operand at $4091 so it survives the wait
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One frame per pass; the remaining count is kept in the LDA operand at $4085.
|
|
; ----------------------------------------------------------------------
|
|
waitFrameLoop:
|
|
sta L_4085+1 ; 4072 store the frames still to wait into the LDA operand at $4085
|
|
lda #$01 ; 4075 advance the game by exactly one frame ...
|
|
jsr runGameFrames ; 4077 ... so the real-time battle continues during the pause
|
|
jsr isFireHeld ; 407A returns A = 0 while the fire button is held (after having been released once)
|
|
beq L_4091 ; 407D button down: stop waiting
|
|
lda joyDirection ; 407F joystick direction nibble, active low
|
|
cmp #$0F ; 4081 $0F = centred
|
|
bne L_4091 ; 4083 joystick pushed: stop waiting
|
|
L_4085:
|
|
lda #$FF ; 4085 operand patched at $4072: frames still to wait
|
|
sec ; 4087 prepare the subtraction ...
|
|
sbc #$01 ; 4088 one frame gone
|
|
beq L_4091 ; 408A the whole wait has elapsed
|
|
ldy gameEndCountdown ; 408C non-zero once the game has ended and the menu countdown runs
|
|
beq waitFrameLoop ; 408F keep waiting while the game is still live
|
|
L_4091:
|
|
ldy #$FF ; 4091 operand patched at $406F: restore the caller's Y
|
|
rts ; 4093 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runGameFrames - Advances the game by A frames: each pass waits for one raster frame and then runs
|
|
; one updateGameFrame. Stops early once the game-end countdown has started; A = 0 returns at once.
|
|
; This is the routine every wait loop in the game and in both overlays uses so that the real-time
|
|
; battle keeps ticking while the user interface is busy.
|
|
; In: A = frame count; Y is preserved through the operand at L_40AF+1.
|
|
; Out: Y = the entry value, A/X clobbered, the game state advanced.
|
|
; Called from: 7 places in the main program ($17FA, $181F, $19B6, $3D1B, $3D33, $4077, $648A) and 9 in
|
|
; the $6F00 overlays.
|
|
; ----------------------------------------------------------------------
|
|
runGameFrames:
|
|
sty L_40AF+1 ; 4094 park the caller's Y in the LDY operand at $40AF
|
|
cmp #$00 ; 4097 zero frames requested?
|
|
beq L_40AF ; 4099 then there is nothing to do
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One raster frame plus one engine update per pass.
|
|
; ----------------------------------------------------------------------
|
|
runOneFrameLoop:
|
|
pha ; 409B save the remaining count on the stack
|
|
lda #$01 ; 409C wait for exactly one frame ...
|
|
jsr waitFrames ; 409E ... by polling the raster register
|
|
jsr updateGameFrame ; 40A1 run the per-frame game update
|
|
pla ; 40A4 get the count back
|
|
sec ; 40A5 prepare the subtraction ...
|
|
sbc #$01 ; 40A6 one frame done
|
|
beq L_40AF ; 40A8 all requested frames have run
|
|
ldy gameEndCountdown ; 40AA non-zero once the game has ended
|
|
beq runOneFrameLoop ; 40AD otherwise keep running frames
|
|
L_40AF:
|
|
ldy #$FF ; 40AF operand patched at $4094: restore the caller's Y
|
|
rts ; 40B1 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateGameFrame - The heartbeat of the game, called once per frame from every main and
|
|
; user-interface loop. In order it: (1) leaves for the menu if the game-end countdown has expired;
|
|
; (2) handles the film replay speed keys 1-9, or takes a short cut straight to the engine while the
|
|
; comcen knock-out blackout is running; (3) services the status line and the CLR/HOME key, which
|
|
; cycles a film's viewpoint through SHOW US / SHOW THEM / SHOW BOTH; (4) handles the RUN/STOP pause
|
|
; and switches off the shot-line and marker sprites when their timers expire; (5) performs the pending
|
|
; single-unit and whole-group redraws; (6) runs the engine - every stepIntervalFrames frames one unit
|
|
; of the round order is stepped, every tenth unit the queued and received commands are exchanged and
|
|
; applied, and after all 100 units the round is finished.
|
|
; In: filmPlaybackMode $0B9B, comcenStunStatus $922A bit 6, lastTypedKey $9248, currentScreen $90FB,
|
|
; gamePhase zp_B1, stepDelayTimer $90FC, stepIntervalFrames $92E3, unitOrderIndex $9162,
|
|
; unitProcessOrder $90FD, the timers $9234/$91C8/$923E/$92C8 and the redraw requests $923A/$923B.
|
|
; Out: one unit step performed (processUnitStep) or the round finished (finishRound); map, sprites,
|
|
; status line and message queue updated; stepDelayTimer reloaded. May not return at all:
|
|
; exitIfGameEnded can jump to the menu, and handlePauseKey discards the return address while the
|
|
; game is paused.
|
|
; Called from: 12 places in the main program (including runGameFrames $40A1), the $6F00 overlays and
|
|
; the comcen blackout loop at $0737.
|
|
; ----------------------------------------------------------------------
|
|
updateGameFrame:
|
|
jsr exitIfGameEnded ; 40B2 jumps to the menu (and never returns) once gameEndCountdown has reached $80
|
|
lda filmPlaybackMode ; 40B5 bit 7 set = a game film is being replayed
|
|
bmi L_40C5 ; 40B8 film: the digit keys set the replay speed
|
|
bit comcenStunStatus ; 40BA bit 6 = the comcen knock-out blackout at $0709 is running
|
|
bvc L_40EB ; 40BD normal frame: do the whole update
|
|
jsr handlePauseKey ; 40BF blackout: only the RUN/STOP pause handler runs ...
|
|
jmp L_41BD ; 40C2 ... then go straight to the engine, leaving the screen frozen
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Film replay: keys '1'-'9' set the step interval (1 = slowest, 9 = fastest) and show 'SPEED n'.
|
|
; ----------------------------------------------------------------------
|
|
L_40C5:
|
|
lda lastTypedKey ; 40C5 the last key the IRQ picked up (PETSCII with bit 7 set)
|
|
sec ; 40C8 prepare the subtraction ...
|
|
sbc #$B1 ; 40C9 $B1 = '1'|$80, the first speed key
|
|
bcc L_40EB ; 40CB below '1': not a speed key
|
|
cmp #$09 ; 40CD nine speed keys, '1' to '9'
|
|
bcs L_40EB ; 40CF above '9': not a speed key
|
|
sta scratch18 ; 40D1 A = digit - 1, i.e. 0..8
|
|
lda #$08 ; 40D3 8 = the slowest interval ...
|
|
sec ; 40D5 prepare the subtraction ...
|
|
sbc scratch18 ; 40D6 interval = 8 - (digit-1) = 9 - digit ...
|
|
sta stepIntervalFrames ; 40D8 ... so '1' waits 8 frames per unit step and '9' waits none
|
|
lda lastTypedKey ; 40DB the key code again
|
|
sta msgSpeedDigit ; 40DE poke it into the 'SPEED *' template at $8730; its bit 7 doubles as the string terminator
|
|
lda #$07 ; 40E1 message 7 = 'SPEED n'
|
|
jsr queueMessage ; 40E3 queue it (always, even in a film)
|
|
lda #$FF ; 40E6 $FF = no key ...
|
|
sta lastTypedKey ; 40E8 ... so the digit is consumed and not seen again
|
|
L_40EB:
|
|
jsr updateStatusMessage ; 40EB service the status line / message queue once per frame
|
|
lda gamePhase ; 40EE 0 only during the real-time battle
|
|
bne L_413D ; 40F0 no viewpoint switching outside the battle
|
|
lda currentScreen ; 40F2 screen id, 0 = the battlefield
|
|
bne L_413D ; 40F5 the CLR/HOME key only works on the battlefield screen
|
|
lda lastTypedKey ; 40F7 the last key pressed
|
|
cmp #$8C ; 40FA $8C = CLR/HOME in the game's own key table
|
|
bne L_413D ; 40FC some other key: nothing to do here
|
|
lda #$FF ; 40FE $FF = no key ...
|
|
sta lastTypedKey ; 4100 ... consume CLR/HOME
|
|
lda #$3C ; 4103 60 frames ...
|
|
sta inputLockoutTimer ; 4105 ... of input lockout so the key does not repeat
|
|
lda filmPlaybackMode ; 4108 bit 7 set = we are watching a film
|
|
bpl L_413D ; 410B in a live game CLR/HOME only eats the keypress
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Cycle the film viewpoint $80 (SHOW US) - $81 (SHOW THEM) - $82 (SHOW BOTH) and back.
|
|
; ----------------------------------------------------------------------
|
|
clc ; 410D clear the carry for the increment ...
|
|
adc #$01 ; 410E next viewpoint
|
|
cmp #$83 ; 4110 past SHOW BOTH?
|
|
bcc L_4116 ; 4112 no, keep it
|
|
lda #$80 ; 4114 wrap round to $80 = SHOW US
|
|
L_4116:
|
|
sta filmPlaybackMode ; 4116 store the new playback mode (bit 7 stays set)
|
|
and #$03 ; 4119 bits 0-1 = the viewpoint number 0-2
|
|
asl a ; 411B double ...
|
|
asl a ; 411C times 4: the words in filmViewWordTable are 4 bytes each
|
|
tay ; 411D Y = offset of 'US ' / 'THEM' / 'BOTH'
|
|
ldx #$00 ; 411E X = destination index
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the 4-byte viewpoint word into the 'SHOW ****' message template at $8726.
|
|
; ----------------------------------------------------------------------
|
|
copyFilmViewWordLoop:
|
|
lda filmViewWordTable,y ; 4120 one character of the viewpoint word ($86FB in the resident overlay)
|
|
sta msgShowViewWord,x ; 4123 into the four asterisks of message 6
|
|
inx ; 4126 next destination byte
|
|
iny ; 4127 next source byte
|
|
cpx #$04 ; 4128 all four characters copied?
|
|
bcc copyFilmViewWordLoop ; 412A no, keep copying
|
|
lda #$06 ; 412C message 6 = 'SHOW US/THEM/BOTH'
|
|
jsr queueMessage ; 412E queue it
|
|
jsr updateStatusMessage ; 4131 and print it immediately
|
|
jsr patchUnitSideBranches ; 4134 re-patch the 'is this unit mine' branches for the new viewpoint
|
|
jsr drawOverviewMap ; 4137 redraw the overview map from the new point of view
|
|
jsr drawViewport ; 413A and the tactical view
|
|
L_413D:
|
|
jsr handlePauseKey ; 413D RUN/STOP pause handling (may drop the return address and skip the rest of the frame)
|
|
lda currentScreen ; 4140 screen id
|
|
bne L_4167 ; 4143 the view/shot-line timers only matter on the battlefield screen
|
|
lda viewRedrawTimer ; 4145 countdown before the tactical view is redrawn
|
|
beq L_415A ; 4148 not armed
|
|
and #$7F ; 414A the IRQ stops counting at $80 ...
|
|
bne L_415A ; 414C ... so only the exact value $80 means 'delay expired'
|
|
ldx viewOriginCol ; 414E X = left edge of the view
|
|
ldy viewOriginRow ; 4150 Y = top edge of the view
|
|
jsr drawViewport ; 4152 redraw the 7x5 tactical view
|
|
lda #$00 ; 4155 disarm the timer ...
|
|
sta viewRedrawTimer ; 4157 ... until something asks for another delayed redraw
|
|
L_415A:
|
|
lda shotLineTimer ; 415A countdown of the shot-line sprite
|
|
bne L_4167 ; 415D still visible
|
|
lda spriteEnableShadow ; 415F sprite enable shadow
|
|
and #$F7 ; 4162 $F7 clears bit 3 = sprite 3, the shot / path line
|
|
sta spriteEnableShadow ; 4164 the line disappears when its timer runs out
|
|
L_4167:
|
|
lda currentScreen ; 4167 screen id
|
|
and #$01 ; 416A only even screen ids (battlefield and the missile screen) use the marker sprite
|
|
bne L_417B ; 416C odd screen: leave the sprites alone
|
|
lda markerSpriteTimer ; 416E countdown of the marker sprite
|
|
bne L_417B ; 4171 still running
|
|
lda spriteEnableShadow ; 4173 sprite enable shadow
|
|
and #$EF ; 4176 $EF clears bit 4 = sprite 4, the repair/target marker
|
|
sta spriteEnableShadow ; 4178 switch the marker off when its timer expires
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Deferred redraws: one single unit and one whole group can be queued from anywhere.
|
|
; ----------------------------------------------------------------------
|
|
L_417B:
|
|
ldy pendingRedrawUnit ; 417B unit queued for a redraw ($FF = none)
|
|
bmi L_418D ; 417E nothing queued
|
|
lda unitTerrainUnderTable,y ; 4180 terrain under that unit; $FF = it is not on the map
|
|
bmi L_4188 ; 4183 a unit that is off the map cannot be drawn
|
|
jsr drawUnitAtPosition ; 4185 draw the unit in the overview and in the tactical view
|
|
L_4188:
|
|
lda #$FF ; 4188 $FF = nothing queued ...
|
|
sta pendingRedrawUnit ; 418A ... the request has been serviced
|
|
L_418D:
|
|
lda pendingRedrawGroup ; 418D group key queued for a redraw ($FF = none)
|
|
bmi L_41BD ; 4190 nothing queued: go on to the engine
|
|
ora #$40 ; 4192 bit 6 = 'is a group member', so the value matches unitFlagsTable & $7C
|
|
sta pendingRedrawGroup ; 4194 store the completed group key back
|
|
lda #$63 ; 4197 walk units 99 ...
|
|
sta scratch18 ; 4199 ... down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Redraw every living unit that belongs to the queued group.
|
|
; ----------------------------------------------------------------------
|
|
groupRedrawLoop:
|
|
ldy scratch18 ; 419B Y = unit index
|
|
lda unitTypeTable,y ; 419D type byte
|
|
bmi L_41B4 ; 41A0 bit 7 set = dead or unused slot
|
|
lda unitFlagsTable,y ; 41A2 unit flags
|
|
and #$7C ; 41A5 $7C = group id bits 2-4, side bit 5, member bit 6
|
|
cmp pendingRedrawGroup ; 41A7 does this unit belong to the queued group?
|
|
bne L_41B4 ; 41AA no
|
|
lda unitTerrainUnderTable,y ; 41AC terrain under the unit
|
|
bmi L_41B4 ; 41AF $FF = the unit is not on the map
|
|
jsr drawUnitAtPosition ; 41B1 redraw it
|
|
L_41B4:
|
|
dec scratch18 ; 41B4 next unit
|
|
bpl groupRedrawLoop ; 41B6 until unit 0 has been checked
|
|
lda #$FF ; 41B8 $FF = nothing queued ...
|
|
sta pendingRedrawGroup ; 41BA ... the group redraw is done
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The engine proper: one unit of the round order is stepped every stepIntervalFrames frames.
|
|
; ----------------------------------------------------------------------
|
|
L_41BD:
|
|
lda stepDelayTimer ; 41BD frames still to wait before the next unit step (counted down by the IRQ)
|
|
bne L_420F ; 41C0 not time yet - nothing more to do this frame
|
|
lda screenShakeTimer ; 41C2 screen-shake counter, set when the comcen is hit
|
|
beq L_41CD ; 41C5 no shake running
|
|
jsr shakeScreenScroll ; 41C7 jitter the fine scroll registers by a random amount
|
|
dec screenShakeTimer ; 41CA one shake frame used up
|
|
L_41CD:
|
|
lda stepIntervalFrames ; 41CD frames between unit steps (film speed keys change this)
|
|
ldx #$0A ; 41D0 10 units between command exchanges during the battle
|
|
ldy gamePhase ; 41D2 0 = the battle is running
|
|
beq L_41DA ; 41D4 battle: keep both values
|
|
ldx #$32 ; 41D6 setup phase: exchange commands only every 50 units ...
|
|
lda #$00 ; 41D8 ... and run the unit steps as fast as possible
|
|
L_41DA:
|
|
sta stepDelayTimer ; 41DA reload the inter-step delay
|
|
lda unitOrderIndex ; 41DD position in the round order, 0-99
|
|
beq L_41FB ; 41E0 index 0 needs no exchange (finishRound has just run one)
|
|
ldy #$00 ; 41E2 high byte of the dividend = 0
|
|
jsr divideSigned16by8 ; 41E4 A = index / X, Y = index mod X (X is 10, or 50 in the setup phase)
|
|
cpy #$00 ; 41E7 remainder zero?
|
|
bne L_41FB ; 41E9 not on a time-slice boundary - step the unit without an exchange
|
|
sta commandSliceIndex ; 41EB the time slice number (0-9) that identifies this exchange in the film
|
|
jsr processCommandExchange ; 41EE swap packets with the opponent and execute both sides' commands
|
|
lda linkTimeoutCounter ; 41F1 non-zero = the link watchdog is still waiting for the opponent's packet
|
|
bne L_420A ; 41F4 the opponent is late: postpone the step and try again next frame
|
|
lda #$80 ; 41F6 bit 7 = the commands of this slice have been applied ...
|
|
sta exchangeAppliedFlag ; 41F8 ... which the setup and console screens poll
|
|
L_41FB:
|
|
jsr updateHitFlashTimers ; 41FB age the per-unit hit-flash timers and redraw the cells whose flash ended
|
|
ldy unitOrderIndex ; 41FE position in the round order
|
|
ldx unitProcessOrder,y ; 4201 X = the unit to process, taken from the shuffled round order
|
|
cpy #$64 ; 4204 100 units make one round
|
|
bcc processUnitStep ; 4206 still units left: step this one (falls into processUnitStep, not a JSR)
|
|
bcs L_4210 ; 4208 all 100 done: finish the round
|
|
L_420A:
|
|
lda #$00 ; 420A opponent not ready: clear the delay ...
|
|
sta stepDelayTimer ; 420C ... so the exchange is retried on the very next frame
|
|
L_420F:
|
|
rts ; 420F return
|
|
|
|
L_4210:
|
|
jmp finishRound ; 4210 tick the game clock, hand out movement points and rebuild the round order
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; processUnitStep - Processes exactly one unit of the round order; this is an internal entry of
|
|
; updateGameFrame reached by the branch at $4206, not by a JSR, and it ends by falling into the shared
|
|
; tail at L_4537 which bumps unitOrderIndex. Broadly it: shuffles two entries of the base order with
|
|
; the lock-step RNG so both machines agree; skips dead units and everything once the game is over;
|
|
; counts a stunned unit's stun down and halts it; turns a moving unit toward its destination; realises
|
|
; a unit whose path position has run ahead of its map position by walking back to the first free cell;
|
|
; takes one movement step with tryStepTowardTarget, paying terrain and blitz costs; redraws the cell
|
|
; it vacated; keeps the tactical view on a directed unit; and then (past this chunk, from $43A4) spots
|
|
; enemies, picks a target, fires, digs in or repairs.
|
|
; In: X = unit index taken from unitProcessOrder[unitOrderIndex]; the unit record arrays $F640-$FBB7;
|
|
; gamePhase zp_B1; gameEndReason $0BA8; gameOverFlags $923F.
|
|
; Out: the unit's position, path position, movement points, flags and energy updated; map cells and
|
|
; the screen redrawn; currentUnit $90FA, scanUnitType $9228, scanUnitTerrain $9227,
|
|
; currentUnitCloakBits $922C, currentUnitCol/Row $91DA/$91DB set for the helper routines;
|
|
; exchangeActivityFlag $92C2 set when the unit really moved.
|
|
; Called from: updateGameFrame at $4206 (BCC), once per unit step.
|
|
; ----------------------------------------------------------------------
|
|
processUnitStep:
|
|
stx currentUnit ; 4213 currentUnit is the unit index every helper in this routine reads
|
|
lda gameOverFlags ; 4216 $C0 once the game is over (set by computeGameResult)
|
|
bpl L_421E ; 4219 game still running: process the unit
|
|
L_421B:
|
|
jmp endUnitStep ; 421B everything frozen: just advance the round order
|
|
|
|
L_421E:
|
|
lda gamePhase ; 421E 0 = the real-time battle
|
|
beq L_4229 ; 4220 battle: shuffle the round order
|
|
lda #$50 ; 4222 setup phase: 80 movement points ...
|
|
sta unitMovePointsTable,x ; 4224 ... so units reach their placement instantly
|
|
bne L_422C ; 4227 always taken ($50 is not zero): skip the shuffle in the setup phase
|
|
L_4229:
|
|
jsr shuffleUnitOrder ; 4229 swap two random entries of the base order, using the lock-step RNG so both machines shuffle identically
|
|
L_422C:
|
|
ldx currentUnit ; 422C X = the unit being processed
|
|
lda unitTypeTable,x ; 422F type byte
|
|
bmi L_421B ; 4232 bit 7 set = destroyed or an empty slot: nothing to do
|
|
and #$07 ; 4234 bits 0-2 = the type 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN
|
|
sta scanUnitType ; 4236 the sight/attack scans and the scoring code read it from here
|
|
lda #$00 ; 4239 clear the 'this unit changed cells' flag ...
|
|
sta unitMovedFlag ; 423B ... moveUnitToCell sets it again if the unit really moves
|
|
lda unitPathColTable,x ; 423E path column byte
|
|
and #$C0 ; 4241 bits 6-7 = the cloak flags
|
|
sta currentUnitCloakBits ; 4243 kept aside so they survive the path-column rewrite at $4350
|
|
lda unitTerrainUnderTable,x ; 4246 the terrain byte hidden under the unit
|
|
sta scanUnitTerrain ; 4249 used for the sight bonus, the move cost and to redraw the vacated cell
|
|
lda gameEndReason ; 424C non-zero once the battle has ended
|
|
bne L_4275 ; 424F game over: skip movement and combat, go to the tail
|
|
lda unitStunTable,x ; 4251 stun / dig-in byte
|
|
and #$0F ; 4254 low nibble = rounds of stun left
|
|
beq L_4278 ; 4256 not stunned: normal processing
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Stunned unit: count one round of stun off, keep the dig-in nibble, and cancel its orders.
|
|
; ----------------------------------------------------------------------
|
|
tay ; 4258 keep the stun count in Y
|
|
lda unitStunTable,x ; 4259 read the byte again ...
|
|
and #$F0 ; 425C ... and isolate the dig-in progress in bits 4-7
|
|
sta L_4265+1 ; 425E patch it into the ORA operand at $4265 so it survives the decrement
|
|
tya ; 4261 A = stun count
|
|
sec ; 4262 prepare the subtraction ...
|
|
sbc #$01 ; 4263 one round of stun served
|
|
L_4265:
|
|
ora #$FF ; 4265 operand patched at $425E: put the dig-in nibble back
|
|
sta unitStunTable,x ; 4267 store the new stun/dig-in byte
|
|
lda #$00 ; 426A a stunned unit gets no movement points ...
|
|
sta unitMovePointsTable,x ; 426C ... so it cannot move even if it had orders
|
|
ldy currentUnit ; 426F Y = unit index
|
|
jsr haltStunnedUnit ; 4272 cancel its orders and drop it out of its group
|
|
L_4275:
|
|
jmp unitStepSkipFiring ; 4275 skip movement and combat for this unit
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Moving unit: point it at its destination so the facing bits and the scans agree.
|
|
; ----------------------------------------------------------------------
|
|
L_4278:
|
|
jsr updateIdleFlag ; 4278 recompute bit 7 of unitDestCol: set when the destination equals the unit's cell
|
|
bmi L_42A0 ; 427B already at its destination: no facing update needed
|
|
ldx currentUnit ; 427D X = unit index
|
|
lda unitDestColTable,x ; 4280 destination column ...
|
|
and #$3F ; 4283 ... without the stationary bit
|
|
sta lineEndCol ; 4285 target column of the line walker
|
|
lda unitDestRowTable,x ; 4287 destination row ...
|
|
and #$3F ; 428A ... without the repairing bit
|
|
sta lineEndRow ; 428C target row of the line walker
|
|
jsr initLineFromUnit ; 428E set the line walker up from the unit's cell; A = principal direction 0-3
|
|
sta scratch18 ; 4291 keep the direction
|
|
ldx currentUnit ; 4293 X = unit index
|
|
lda unitFlagsTable,x ; 4296 unit flags
|
|
and #$FC ; 4299 $FC clears the facing bits 0-1
|
|
ora scratch18 ; 429B insert the new facing
|
|
sta unitFlagsTable,x ; 429D the unit now faces the way it is walking
|
|
L_42A0:
|
|
jsr updateComcenMoveSound ; 42A0 start or stop the local comcen's engine sound to match its movement
|
|
ldx currentUnit ; 42A3 X = unit index
|
|
lda unitColTable,x ; 42A6 the column the unit is standing on ...
|
|
sta lineEndCol ; 42A9 ... as the line walker's target ...
|
|
sta currentUnitCol ; 42AB ... and remembered so the vacated cell can be redrawn at $4377
|
|
lda unitRowTable,x ; 42AE the row it is standing on ...
|
|
sta lineEndRow ; 42B1 ... as the line walker's target row ...
|
|
sta currentUnitRow ; 42B3 ... and remembered for the redraw
|
|
lda unitDestRowTable,x ; 42B6 destination row ...
|
|
and #$3F ; 42B9 ... without the repairing bit
|
|
cmp lineEndRow ; 42BB is the unit already on its destination row?
|
|
bne L_42CB ; 42BD no: it still has somewhere to go
|
|
lda unitDestColTable,x ; 42BF destination column ...
|
|
and #$3F ; 42C2 ... without the stationary bit
|
|
cmp lineEndCol ; 42C4 and on its destination column?
|
|
bne L_42CB ; 42C6 no: it still has somewhere to go
|
|
jmp L_436F ; 42C8 the unit has arrived: no movement this step
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A unit may step onto an occupied cell: then only its path position (F898/F8FC) moves while its map
|
|
; position stays behind. Here the map position is dragged forward again, one cell at a time, to the
|
|
; first free cell on the way to the path position.
|
|
; ----------------------------------------------------------------------
|
|
L_42CB:
|
|
lda unitPathRowTable,x ; 42CB path row ...
|
|
and #$3F ; 42CE ... without the blitz bits ...
|
|
sta lineCurRow ; 42D0 ... as the line walker's current row
|
|
lda unitPathColTable,x ; 42D2 path column ...
|
|
and #$3F ; 42D5 ... without the cloak bits ...
|
|
sta lineCurCol ; 42D7 ... as the line walker's current column
|
|
cmp lineEndCol ; 42D9 path column against the unit's real column
|
|
bne walkBackToFreeCellLoop ; 42DB different: the unit is out of step with its path
|
|
lda lineCurRow ; 42DD compare the rows too
|
|
cmp lineEndRow ; 42DF compare the path row with the unit's real row
|
|
beq L_42FB ; 42E1 path position equals map position: nothing to catch up
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk from the path position back toward the map position until a free cell turns up.
|
|
; ----------------------------------------------------------------------
|
|
walkBackToFreeCellLoop:
|
|
ldx lineCurCol ; 42E3 X = column of the cell being tested
|
|
ldy lineCurRow ; 42E5 Y = its row
|
|
jsr readMapCell ; 42E7 A = the cell contents, mapCellPtr = its address
|
|
bpl L_42F6 ; 42EA bit 7 clear: this cell is free, the unit can materialise here
|
|
jsr initLineStepper ; 42EC re-aim the walker from this cell at the unit's real cell
|
|
jsr stepLine ; 42EF step one cell closer
|
|
bne walkBackToFreeCellLoop ; 42F2 still cells left to try
|
|
beq L_42FB ; 42F4 arrived at the unit's own cell without finding a gap
|
|
L_42F6:
|
|
sta scratch19 ; 42F6 keep the terrain byte of the free cell for moveUnitToCell
|
|
jsr moveUnitToCell ; 42F8 move the unit's map position into that cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Now take one real step from the path position toward the destination.
|
|
; ----------------------------------------------------------------------
|
|
L_42FB:
|
|
ldy currentUnit ; 42FB Y = unit index
|
|
lda unitDestColTable,y ; 42FE destination column (the stationary bit is clear here, we tested it above) ...
|
|
sta lineEndCol ; 4301 ... is the walker's target column
|
|
lda unitDestRowTable,y ; 4303 destination row ...
|
|
sta lineEndRow ; 4306 ... is the walker's target row
|
|
lda unitPathColTable,y ; 4308 path column ...
|
|
and #$3F ; 430B ... without the cloak bits ...
|
|
sta lineCurCol ; 430D ... is where the step starts
|
|
lda unitPathRowTable,y ; 430F path row ...
|
|
and #$3F ; 4312 ... without the blitz bits ...
|
|
sta lineCurRow ; 4314 ... is where the step starts
|
|
jsr tryStepTowardTarget ; 4316 try one step: C=1 and A = points left if it is allowed, C=0 if not
|
|
bcs L_4329 ; 4319 step accepted
|
|
txa ; 431B X = $FF when the cell was blocked or off the map, = the unit index when the points ran out
|
|
bpl L_4326 ; 431C not enough movement points: just wait for the next round
|
|
ldx currentUnit ; 431E blocked: X = unit index again
|
|
lda #$0F ; 4321 park the balance at 15 points ...
|
|
sta unitMovePointsTable,x ; 4323 ... so the unit retries soon instead of banking points
|
|
L_4326:
|
|
jmp L_436F ; 4326 no movement this step
|
|
|
|
L_4329:
|
|
ldy currentUnit ; 4329 Y = unit index
|
|
sta unitMovePointsTable,y ; 432C store the balance left after paying for the step
|
|
lda gamePhase ; 432F 0 = the real-time battle
|
|
bne L_433F ; 4331 no blitz costs during the setup phase
|
|
lda unitPathRowTable,y ; 4333 path row bit 7 = blitz active
|
|
bpl L_433F ; 4336 not blitzing
|
|
lda scratch19 ; 4338 cell byte returned by tryStepTowardTarget; 0 = no step was actually taken
|
|
beq L_433F ; 433A nothing moved, nothing to pay for
|
|
jsr applyBlitzStepCost ; 433C pay one energy point for +25 movement points and arm the charge bonus
|
|
L_433F:
|
|
lda scratch19 ; 433F the cell the unit stepped into
|
|
bmi L_4348 ; 4341 bit 7 set = another unit is standing there: only the path position advances
|
|
beq L_436F ; 4343 0 = no step taken at all
|
|
jsr moveUnitToCell ; 4345 free cell: really move the unit and redraw both cells
|
|
L_4348:
|
|
ldx currentUnit ; 4348 X = unit index
|
|
lda lineCurCol ; 434B new path column ...
|
|
ora currentUnitCloakBits ; 434D ... with the cloak bits put back ...
|
|
sta unitPathColTable,x ; 4350 ... stored as the unit's new path position
|
|
lda unitPathRowTable,x ; 4353 old path row
|
|
and #$C0 ; 4356 keep bits 6-7 = the blitz flags
|
|
ora lineCurRow ; 4358 merge the new row in
|
|
sta unitPathRowTable,x ; 435A store the new path row
|
|
lda #$01 ; 435D 1 = something happened this turn ...
|
|
sta exchangeActivityFlag ; 435F ... which the setup phase uses to decide whether the clock ticks
|
|
lda lineStepsLeft ; 4362 steps the line walker still has to go
|
|
bne L_436F ; 4364 the unit is still on its way
|
|
lda scratch19 ; 4366 it has reached its destination cell - is that cell occupied?
|
|
bpl L_436F ; 4368 no, the unit is really there
|
|
lda #$64 ; 436A 100 movement points ...
|
|
sta unitMovePointsTable,x ; 436C ... so a unit blocked on its own destination keeps pushing every round
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Common tail: repaint the cell the unit left, keep the view on it, refresh its flags.
|
|
; ----------------------------------------------------------------------
|
|
L_436F:
|
|
lda unitMovedFlag ; 436F did moveUnitToCell actually move the unit?
|
|
beq L_4380 ; 4372 no: the old cell still shows the unit
|
|
ldy currentUnitRow ; 4374 Y = the row it started the step on
|
|
ldx currentUnitCol ; 4377 X = the column it started on
|
|
lda scanUnitTerrain ; 437A A = the terrain that was hidden under it
|
|
jsr drawMapCell ; 437D repaint that cell in the overview map and in the tactical view
|
|
L_4380:
|
|
jsr followDirectedUnit ; 4380 if this unit is the one being directed, scroll the view onto it
|
|
jsr updateIdleFlag ; 4383 recompute the stationary bit; returns A = unitDestCol byte, Y = currentUnit
|
|
and #$80 ; 4386 keep bit 7 = stationary
|
|
sta L_4390+1 ; 4388 patch it into the ORA operand at $4390
|
|
lda unitDisplayFlagsTable,y ; 438B the display copy of the flags
|
|
and #$7F ; 438E clear its bit 7
|
|
L_4390:
|
|
ora #$FF ; 4390 operand patched at $4388: the fresh stationary bit
|
|
sta unitDisplayFlagsTable,y ; 4392 so the console shows MOVE or READY correctly
|
|
lda unitFlagsTable,y ; 4395 unit flags
|
|
and #$03 ; 4398 bits 0-1 = the direction the unit faces
|
|
sta targetDirection ; 439A the sight and attack scans prefer targets in this direction
|
|
lda unitPathColTable,y ; 439C path column bit 7 = the unit is cloaked
|
|
bpl spotEnemiesInSight ; 439F not cloaked: go on to spot and fire (next chunk, from $43A4)
|
|
L_43A1:
|
|
jmp unitStepSkipFiring ; 43A1 a cloaked unit neither spots nor fires: jump to the tail
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Spotting pass (part of processUnitStep, which starts at $4213). Only a unit that is not cloaked and
|
|
; only a running battle gets here: it looks around with scanVisibleEnemies in sight mode and makes
|
|
; every enemy it can see visible. The three parallel 50-entry scan lists live in the shared
|
|
; $0400 scratch page: $0400 = unit index ($FF = candidate rejected), $0432 = squared distance,
|
|
; $0464 = direction 0-3. (The $0400 label reads 'checksumXorConstant' because the boot checksum
|
|
; constant lives at the same address; at run time this page is pure scratch.)
|
|
; ----------------------------------------------------------------------
|
|
spotEnemiesInSight:
|
|
lda gamePhase ; 43A4 zp_B1 = 0 only while the battle is actually running
|
|
bne L_43A1 ; 43A6 setup / film / menu phase: nobody spots or fires, go to the no-fire path
|
|
ldx #$00 ; 43A8 X = 0 selects the sight-range pass (see, do not shoot)
|
|
jsr scanVisibleEnemies ; 43AA Y is still currentUnit: fill $0400/$0432/$0464 with the enemies in sight
|
|
ldx scanResultCount ; 43AD zp_2F = index of the last list entry ($FF = nothing in sight)
|
|
stx scratch18 ; 43AF walk the list backwards from that index
|
|
bmi chooseFireTarget ; 43B1 branch if nothing was spotted at all
|
|
|
|
; ----------------------------------------------------------------------
|
|
; For every candidate the scan kept: mark it seen this visibility cycle and, if it was not visible
|
|
; before, make it visible and draw it on the map.
|
|
; ----------------------------------------------------------------------
|
|
L_43B3:
|
|
ldy checksumXorConstant,x ; 43B3 $0400+n = unit index of scan result n (also trainerPlaybook0200:play3Col12)
|
|
bmi L_43CA ; 43B6 $FF = the scan rejected this candidate (out of range / no line of sight)
|
|
lda unitEnergyTable,y ; 43B8 energy byte: bits 0-5 energy 0-50, bit 6 = seen this cycle, bit 7 = visible
|
|
ora #$40 ; 43BB bit 6: ageUnitVisibility will shift this into bit 7 at the end of the round
|
|
sta unitEnergyTable,y ; 43BD store the refreshed visibility
|
|
bmi L_43CA ; 43C0 bit 7 already set: the enemy was visible before, no redraw needed
|
|
ora #$80 ; 43C2 newly spotted: make it visible right now
|
|
sta unitEnergyTable,y ; 43C4 store it
|
|
jsr drawUnitAtPosition ; 43C7 draw unit Y on the overview map and in the 7x5 view
|
|
L_43CA:
|
|
dec scratch18 ; 43CA step down to the previous list entry
|
|
ldx scratch18 ; 43CC reload the index for the LDY above
|
|
bpl L_43B3 ; 43CE loop until entry 0 has been handled
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Target selection. Boomers keep a target of their own (selectBoomerTarget); everybody else runs a
|
|
; second scan in weapon-range mode and picks the nearest candidate, except that an enemy comcen is
|
|
; always preferred over anything else.
|
|
; ----------------------------------------------------------------------
|
|
chooseFireTarget:
|
|
ldy currentUnit ; 43D0 scanVisibleEnemies wants the scanning unit in Y again
|
|
lda scanUnitType ; 43D3 $9228 = type of the unit being stepped (0 GRUNT 1 RIDER 2 BOOMER 3 SPY 4 COMCEN)
|
|
cmp #$02 ; 43D6 BOOMER?
|
|
beq L_4425 ; 43D8 boomers lock onto a target and keep it
|
|
ldx #$80 ; 43DA X bit 7 set = weapon-range pass (only enemies that can be shot at)
|
|
jsr scanVisibleEnemies ; 43DC rebuild the scan lists for firing
|
|
ldx scanResultCount ; 43DF index of the last candidate
|
|
stx scratch18 ; 43E1 loop counter
|
|
bmi unitStepSkipFiring ; 43E3 nothing in weapon range: no shot this step
|
|
lda #$FF ; 43E5 $FF = 'none' for all three running best values
|
|
sta fireTargetDistance ; 43E7 $91F1 = squared distance of the best target so far
|
|
sta fireTargetUnit ; 43EA $91F0 = the chosen target unit
|
|
sta scratch1B ; 43ED $1B = direction 0-3 toward the chosen target
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Loop over the candidates, keeping the nearest one; an enemy comcen (unit 49 or 99) wins immediately
|
|
; and cuts the loop short.
|
|
; ----------------------------------------------------------------------
|
|
L_43EF:
|
|
ldx scratch18 ; 43EF current candidate index
|
|
lda checksumXorConstant,x ; 43F1 $0400+n = the candidate's unit index (also trainerPlaybook0200:play3Col12)
|
|
bmi L_441A ; 43F4 $FF = rejected entry, skip it
|
|
cmp #$31 ; 43F6 unit 49 = side 0 comcen
|
|
beq L_43FE ; 43F8 a comcen is taken whatever the distance
|
|
cmp #$63 ; 43FA unit 99 = side 1 comcen
|
|
bne L_4404 ; 43FC an ordinary unit has to win on distance
|
|
L_43FE:
|
|
lda #$00 ; 43FE comcen found
|
|
sta scratch18 ; 4400 force the loop to end after this entry (DEC makes it $FF)
|
|
beq L_440F ; 4402 always taken (A = 0): take this candidate
|
|
L_4404:
|
|
lda play3Row12,x ; 4404 $0432+n = squared distance of this candidate
|
|
cmp fireTargetDistance ; 4407 nearer than the best so far?
|
|
bcs L_441A ; 440A no: keep the previous best
|
|
sta fireTargetDistance ; 440C new best distance
|
|
L_440F:
|
|
lda checksumXorConstant,x ; 440F take this candidate... (also trainerPlaybook0200:play3Col12)
|
|
sta fireTargetUnit ; 4412 ...as the unit to shoot at
|
|
lda play4Grp12,x ; 4415 $0464+n = direction 0-3 from the firing unit to that enemy
|
|
sta scratch1B ; 4418 remember it as the facing to turn to
|
|
L_441A:
|
|
dec scratch18 ; 441A next candidate down
|
|
bpl L_43EF ; 441C until entry 0 (or the forced end after a comcen)
|
|
lda scratch1B ; 441E direction of the winner
|
|
sta targetDirection ; 4420 zp_6B: the unit will be turned this way below
|
|
jmp L_4428 ; 4422 join the common 'may it fire?' test
|
|
|
|
L_4425:
|
|
jsr selectBoomerTarget ; 4425 boomer: keep or re-choose its lock, sets $91F0/$91F1/zp_6B
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Decide whether the unit is allowed to shoot at the target that was just found.
|
|
; ----------------------------------------------------------------------
|
|
L_4428:
|
|
ldx fireTargetUnit ; 4428 the chosen target
|
|
bmi unitStepSkipFiring ; 442B $FF = nothing to shoot at
|
|
lda scanUnitType ; 442D type of the firing unit
|
|
cmp #$03 ; 4430 types 3 (SPY) and 4 (COMCEN) carry no weapon
|
|
bcs L_4457 ; 4432 they never fire: drop the target
|
|
ldy currentUnit ; 4434 the firing unit
|
|
lda unitPathColTable,y ; 4437 bit 7 of $F898 = the unit is cloaked
|
|
bmi L_4457 ; 443A unreachable: a cloaked unit already branched to L_443E at $43A1
|
|
bpl faceAndFireAtTarget ; 443C always taken: keep the target and fire below
|
|
|
|
; ----------------------------------------------------------------------
|
|
; unitStepSkipFiring - joined by every unit that cannot shoot this step (setup phase, cloaked, no
|
|
; target in range). The unit keeps the facing it had, and a boomer that can no longer reach its
|
|
; locked target loses the lock.
|
|
; ----------------------------------------------------------------------
|
|
unitStepSkipFiring:
|
|
ldx currentUnit ; 443E the unit being stepped
|
|
lda unitFlagsTable,x ; 4441 authoritative flag byte
|
|
and #$03 ; 4444 bits 0-1 = facing
|
|
sta targetDirection ; 4446 zp_6B: keep looking the way it looked
|
|
lda unitPathColTable,x ; 4448 bit 7 of $F898 = cloaked
|
|
bmi L_4457 ; 444B a cloaked unit keeps its boomer lock for later
|
|
jsr getBoomerTarget ; 444D C=1, A = boomerTargetTable entry and Y = boomer ordinal for a boomer; A = type EOR 2 (positive) otherwise
|
|
bpl L_4457 ; 4450 not a boomer, or no target locked (bit 7 clear)
|
|
lda #$00 ; 4452 clear the lock
|
|
sta boomerTargetTable,y ; 4454 $91DC+ordinal: this boomer has lost sight of its target
|
|
L_4457:
|
|
lda #$FF ; 4457 no shot is taken this step
|
|
sta fireTargetUnit ; 4459 $91F0 = none
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Turn the unit toward its target and fire. Each unit only fires on every other round: the test is
|
|
; (gameClock EOR unit index) AND 1, so half of the units shoot on even rounds and half on odd ones,
|
|
; which spreads the work and the shot animations over two rounds.
|
|
; ----------------------------------------------------------------------
|
|
faceAndFireAtTarget:
|
|
ldy currentUnit ; 445C the unit being stepped
|
|
lda unitFlagsTable,y ; 445F authoritative flag byte
|
|
and #$FC ; 4462 clear the facing bits 0-1
|
|
ora targetDirection ; 4464 insert the direction of the target (or the old facing)
|
|
sta unitFlagsTable,y ; 4466 store the new facing
|
|
lda gameClock ; 4469 $91CB = rounds left, counts down, so its bit 0 alternates every round
|
|
eor currentUnit ; 446C stagger the units against each other
|
|
and #$01 ; 446F is this the unit's own firing round?
|
|
bne L_447B ; 4471 no: leave the fired bit alone
|
|
lda unitDisplayFlagsTable,y ; 4473 local display copy of the unit flags
|
|
and #$FE ; 4476 clear bit 0 = 'has fired' so the console shows READY again
|
|
sta unitDisplayFlagsTable,y ; 4478 store it
|
|
L_447B:
|
|
lda fireTargetUnit ; 447B is there a target?
|
|
bmi runStationaryUnitUpkeep ; 447E no shot: go on to the standing-still upkeep
|
|
lda gameClock ; 4480 the same parity test again
|
|
eor currentUnit ; 4483 gameClock EOR unit index
|
|
and #$01 ; 4486 fire only on this unit's round
|
|
bne runStationaryUnitUpkeep ; 4488 not this round
|
|
lda unitEnergyTable,y ; 448A energy/visibility byte of the firing unit
|
|
ora #$C0 ; 448D firing gives the unit away: seen (bit 6) and visible (bit 7)
|
|
sta unitEnergyTable,y ; 448F store it
|
|
ldx currentUnit ; 4492 the firing unit
|
|
lda unitDisplayFlagsTable,x ; 4495 local display flags
|
|
ora #$01 ; 4498 bit 0 = fired (console status FIRED / RELOAD)
|
|
sta unitDisplayFlagsTable,x ; 449A store it
|
|
ldy fireTargetUnit ; 449D Y = the target unit
|
|
jsr resolveAttack ; 44A0 X = attacker, Y = target: damage, sound and the shot line
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Upkeep of a unit that is standing on its destination (bit 7 of unitDestCol set). It stops blitzing,
|
|
; and if it is neither stunned nor firing it either advances its dig-in or repairs one energy point
|
|
; per round.
|
|
; ----------------------------------------------------------------------
|
|
runStationaryUnitUpkeep:
|
|
ldy currentUnit ; 44A3 the unit being stepped
|
|
lda unitDestColTable,y ; 44A6 bit 7 set = the unit has arrived / has no move order
|
|
bpl finishUnitStep ; 44A9 still travelling: nothing to do here
|
|
lda unitRowTable,y ; 44AB map row 0-39
|
|
sta unitPathRowTable,y ; 44AE re-sync the path row and clear its bit 7 = blitz (a standing unit stops charging)
|
|
lda unitStunTable,y ; 44B1 stun/dig-in byte
|
|
and #$0F ; 44B4 low nibble = rounds of stun left
|
|
bne finishUnitStep ; 44B6 a stunned unit neither digs in nor repairs
|
|
lda unitDestRowTable,y ; 44B8 destination row
|
|
and #$7F ; 44BB clear bit 7 = 'repairing', it is recomputed every round
|
|
sta unitDestRowTable,y ; 44BD store it back
|
|
sta unitPathRowTable,y ; 44C0 destination equals position here, so this also refreshes the path row
|
|
lda unitDisplayFlagsTable,y ; 44C3 local display flags
|
|
and #$01 ; 44C6 bit 0 = the unit fired this round
|
|
bne finishUnitStep ; 44C8 shooting cancels digging in and repairing
|
|
lda unitStunTable,y ; 44CA stun/dig-in byte
|
|
bmi L_44E3 ; 44CD bit 7 = the unit is already dug in
|
|
and #$E0 ; 44CF bits 5-7 = dig-in progress (command $9F starts it with $20)
|
|
beq L_44EB ; 44D1 progress 0 = not digging in: try to repair instead
|
|
lda #$01 ; 44D3 something is still happening this round...
|
|
sta exchangeActivityFlag ; 44D5 ...so the setup-phase clock keeps running ($92C2)
|
|
lda unitStunTable,y ; 44D8 dig-in byte again
|
|
clc ; 44DB clear carry for the add
|
|
adc #$20 ; 44DC one more step of digging in; three steps carry into bit 7
|
|
sta unitStunTable,y ; 44DE store the progress
|
|
bpl finishUnitStep ; 44E1 not finished yet
|
|
L_44E3:
|
|
lda unitTypeTable,y ; 44E3 type byte
|
|
ora #$40 ; 44E6 bit 6 = dug in / fortified (resolveAttack takes 25% off the damage)
|
|
sta unitTypeTable,y ; 44E8 store it
|
|
L_44EB:
|
|
cpy #$31 ; 44EB unit 49 = side 0 comcen
|
|
beq finishUnitStep ; 44ED a comcen never repairs itself here (only the console REPAIR panel repairs it)
|
|
cpy #$63 ; 44EF unit 99 = side 1 comcen
|
|
beq finishUnitStep ; 44F1 same for the other comcen
|
|
lda unitEnergyTable,y ; 44F3 energy/visibility byte
|
|
sta scratch18 ; 44F6 keep the whole byte
|
|
and #$C0 ; 44F8 bits 6-7 = seen / visible
|
|
sta scratch19 ; 44FA save the visibility bits
|
|
lda scratch18 ; 44FC the byte again
|
|
and #$3F ; 44FE bits 0-5 = energy 0-50 (displayed as 0-100%)
|
|
cmp #$32 ; 4500 already at the maximum of 50?
|
|
bcs finishUnitStep ; 4502 yes: nothing to repair
|
|
adc #$01 ; 4504 +1 energy point this round (C is clear after the BCS)
|
|
ora scratch19 ; 4506 put the visibility bits back
|
|
sta unitEnergyTable,y ; 4508 store the new energy
|
|
lda unitDestRowTable,y ; 450B destination row
|
|
ora #$80 ; 450E bit 7 = repairing, the console status word becomes REPAIR
|
|
sta unitDestRowTable,y ; 4510 store it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; End of one unit's step: give the solo trainer a look at the unit that was just moved, clear its 'hit
|
|
; last round' marker and redraw it.
|
|
; ----------------------------------------------------------------------
|
|
finishUnitStep:
|
|
lda gamePhase ; 4513 battle only
|
|
bne L_4529 ; 4515 setup / film / menu: no AI hook
|
|
lda filmPlaybackMode ; 4517 $0B9B bit 7 = a game film is being replayed
|
|
bmi L_4529 ; 451A the film supplies the opponent's orders, the trainer must not run
|
|
lda isSoloTrainer ; 451C $0BA5 bit 7 = PRACTICE WITH SOLO TRAINER
|
|
bpl L_4529 ; 451F modem game: the opponent thinks on his own machine
|
|
lda gameEndReason ; 4521 $0BA8 <> 0 = no game running
|
|
bne L_4529 ; 4524 nothing to think about
|
|
jsr trainerUnitStepHook ; 4526 $E0E8 in the trainer overlay: per-unit bookkeeping of the computer opponent
|
|
L_4529:
|
|
ldy currentUnit ; 4529 the unit being stepped
|
|
lda unitDisplayFlagsTable,y ; 452C local display flags
|
|
and #$FD ; 452F clear bit 1 = 'was hit during the last round' (console status PINNED)
|
|
sta unitDisplayFlagsTable,y ; 4531 store it
|
|
jsr drawUnitAtPosition ; 4534 repaint the unit where it now stands
|
|
|
|
; ----------------------------------------------------------------------
|
|
; endUnitStep - common exit of processUnitStep (dead and frozen units jump straight here from
|
|
; $421B). Advances the round order and refreshes the own comcen's stun status, which is also what
|
|
; triggers the COMCEN STUNNED cut-scene.
|
|
; ----------------------------------------------------------------------
|
|
endUnitStep:
|
|
inc unitOrderIndex ; 4537 $9162: the next engine step takes the following entry of unitProcessOrder
|
|
lda comcenStunStatus ; 453A $922A: 0 = comcen fine, $80|n = stunned for n rounds, $FF = knocked out
|
|
cmp #$FF ; 453D $FF is written by applyDroneBlastToCell ($730D) when the own comcen is knocked out
|
|
bne L_4549 ; 453F not knocked out
|
|
ldy filmPlaybackMode ; 4541 $0B9B bit 7 = film playback
|
|
bmi L_4549 ; 4544 the cut-scene is skipped while a film is replaying
|
|
jmp runComcenStunnedBlackout; 4546 $0709: one blacked-out round, then the message COMCEN STUNNED!
|
|
|
|
L_4549:
|
|
ldy comcenUnit ; 4549 $9236 = 49 or 99, the local player's comcen
|
|
lda unitStunTable,y ; 454C its stun/dig-in byte
|
|
and #$0F ; 454F low nibble = rounds of stun left
|
|
beq L_4555 ; 4551 not stunned: status byte 0
|
|
ora #$80 ; 4553 bit 7 = the comcen is stunned (read by the console and the status line)
|
|
L_4555:
|
|
sta comcenStunStatus ; 4555 publish the comcen status
|
|
rts ; 4558 one unit of the round has been processed
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; tryStepTowardTarget - walks the line stepper one cell from the current cell ($93,$92) toward the
|
|
; target ($95,$94) and decides whether currentUnit may enter that cell and can pay for the step. A
|
|
; line with no steps returns C=1 and $19 = 0 (the unit is already there). Cells outside the 40x40 map
|
|
; are refused (C=0, X=$FF); an occupied cell may only be entered by a spy, by a cloaked unit when the
|
|
; occupant is cloaked too, when the occupant is on the same side, or when the occupant is a spy. The
|
|
; cost is terrainMoveCostTable[class], where the class is the group's worst terrain class for a
|
|
; grouped unit, the class of the ground under the unit otherwise and always 0 for a spy, plus 8 more
|
|
; for a diagonal step.
|
|
; In: $92/$93 = current row/column, $94/$95 = target row/column, $90FA currentUnit, unit arrays
|
|
; $F708/$F76C/$F898/$FA28/$FAF0, $91F4 groupStatusTable, terrain class table $077E
|
|
; Out: C=1 accepted, A = movement points left after the step, $19 = the destination cell byte,
|
|
; $92/$93 advanced onto it, zp_48/$49 point at it, X = currentUnit; C=0 refused, X=$FF when
|
|
; the cell is off the map or blocked and X = currentUnit when the points are not there yet;
|
|
; zp_8C-$97 and zp_A9 hold the line state, $18/$19 clobbered
|
|
; Called from: processUnitStep $4316 (the real step) and finishRound $470D (look-ahead while the
|
|
; round order is built)
|
|
; ----------------------------------------------------------------------
|
|
tryStepTowardTarget:
|
|
jsr initLineStepper ; 4559 set up the Bresenham walk from ($93,$92) to ($95,$94); A = number of steps
|
|
bne L_4562 ; 455C there is at least one cell to walk
|
|
sta scratch19 ; 455E $19 = 0: there is no cell to enter (A is 0 here)
|
|
sec ; 4560 C=1 = 'nothing in the way' - the unit is already on its destination
|
|
rts ; 4561 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Take one step along the line and check the cell it lands on: inside the map, and either empty or
|
|
; shareable with the unit that stands there.
|
|
; ----------------------------------------------------------------------
|
|
L_4562:
|
|
jsr stepLine ; 4562 advance ($93,$92) by one cell; zp_A9 = 1 if the step was diagonal
|
|
ldx lineCurCol ; 4565 candidate column
|
|
cpx #$28 ; 4567 the battlefield is 40 columns wide
|
|
bcs refuseStep ; 4569 >= 40 (or wrapped negative): off the map
|
|
ldy lineCurRow ; 456B candidate row
|
|
cpy #$28 ; 456D 40 rows
|
|
bcs refuseStep ; 456F off the map
|
|
jsr readMapCell ; 4571 A = the map byte of that cell, zp_48/$49 = its address
|
|
ldx currentUnit ; 4574 the unit that wants to move
|
|
sta scratch19 ; 4577 $19 = the destination cell byte (bit 7 set = a unit stands there)
|
|
lda unitTypeTable,x ; 4579 type byte of the mover
|
|
and #$07 ; 457C unit type 0-4
|
|
sec ; 457E prepare the subtraction
|
|
sbc #$03 ; 457F gives exactly 0 for a SPY (type 3)
|
|
sta scratch18 ; 4581 $18 = 0 marks the mover as a spy
|
|
ldy scratch19 ; 4583 Y = the cell byte $80|unit, used to index the -$80 biased unit tables
|
|
bpl chargeMovementCost ; 4585 bit 7 clear = plain terrain, only the movement cost is left to check
|
|
lda scratch18 ; 4587 is the mover a spy?
|
|
beq chargeMovementCost ; 4589 a spy may walk into any occupied cell
|
|
lda unitPathColTable,x ; 458B bit 7 of $F898 = the mover is cloaked
|
|
bpl L_4595 ; 458E not cloaked: only the side decides
|
|
lda unitPathColTableAlias,y ; 4590 $F818 = $F898-$80, so the raw map byte indexes the occupant's cloak flag
|
|
bmi chargeMovementCost ; 4593 two cloaked units may share a cell
|
|
L_4595:
|
|
lda unitFlagsTableAlias,y ; 4595 $F688 = $F708-$80: flag byte of the occupant
|
|
eor unitFlagsTable,x ; 4598 compare it with the mover's flags...
|
|
and #$20 ; 459B ...in bit 5, which is the side bit
|
|
beq chargeMovementCost ; 459D same side: units of one army walk through each other
|
|
lda unitTypeTableAlias,y ; 459F $F6EC = $F76C-$80: type byte of the occupant
|
|
and #$07 ; 45A2 unit type 0-4
|
|
cmp #$03 ; 45A4 an enemy SPY does not block a cell
|
|
beq chargeMovementCost ; 45A6 step allowed
|
|
refuseStep:
|
|
clc ; 45A8 C=0 = step refused
|
|
ldx #$FF ; 45A9 X = $FF tells the caller 'off the map or blocked'
|
|
bcc L_45E5 ; 45AB always taken (C was just cleared)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Charge the step against the unit's movement-point balance.
|
|
; ----------------------------------------------------------------------
|
|
chargeMovementCost:
|
|
lda unitFlagsTable,x ; 45AD flag byte of the mover
|
|
and #$40 ; 45B0 bit 6 = the unit belongs to a group
|
|
beq L_45C7 ; 45B2 not in a group: use its own ground
|
|
lda unitFlagsTable,x ; 45B4 flag byte again
|
|
and #$3C ; 45B7 group key in bits 2-5 (group number 0-6 plus the side bit)
|
|
lsr a ; 45B9 shift the key down...
|
|
lsr a ; 45BA ...to a group index 0-15
|
|
tay ; 45BB index into the group table
|
|
lda groupStatusTable,y ; 45BC $91F4+group = the group's movement byte
|
|
and #$38 ; 45BF bits 3-5 = the worst terrain class any member stands on
|
|
lsr a ; 45C1 right-align it...
|
|
lsr a ; 45C2 ...three places...
|
|
lsr a ; 45C3 ...to a class 0-8; a group crosses country as slowly as its worst-placed member
|
|
jmp L_45D2 ; 45C4 charge that class
|
|
|
|
L_45C7:
|
|
ldy unitTerrainUnderTable,x ; 45C7 terrain code hidden under the unit
|
|
lda D_077E,y ; 45CA $077E table: terrain code -> movement class 0-8
|
|
ldy scratch18 ; 45CD is the mover a spy?
|
|
bne L_45D2 ; 45CF no: use the terrain class
|
|
tya ; 45D1 a spy always moves at class 0 (Y = 0 here)
|
|
L_45D2:
|
|
tay ; 45D2 Y = movement class
|
|
clc ; 45D3 C=0 is the answer if the balance turns out to be negative
|
|
lda unitMovePointsTable,x ; 45D4 accumulated movement points of the unit
|
|
bmi L_45E5 ; 45D7 negative balance (set by recordUnitHit): the unit has to wait
|
|
sec ; 45D9 prepare the subtraction
|
|
sbc terrainMoveCostTable,y ; 45DA $405E: 20/65/30/40/20/40/20/20 points per class
|
|
bcc L_45E5 ; 45DD not enough points saved up yet: C=0 with X = the unit index
|
|
ldy isDiagonalStep ; 45DF zp_A9 = 1 when stepLine moved in both axes
|
|
beq L_45E5 ; 45E1 orthogonal step: accepted, C=1, A = points left
|
|
sbc #$08 ; 45E3 a diagonal step costs 8 points more
|
|
L_45E5:
|
|
rts ; 45E5 back to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; finishRound - end of a round, reached by JMP from updateGameFrame ($4210) once all 100 units have
|
|
; been stepped. It ticks the game clock (in the setup phase only when something actually happened),
|
|
; ends the game when the clock reaches 0 and warns at the last quarter, keeps a knocked-out comcen
|
|
; visible for a few rounds, then - during the battle only - runs the trainer's round hook, the
|
|
; recycler, the group tables and the visibility ageing, hands every unit its movement points and
|
|
; finally builds the next round's processing order.
|
|
; In: $0BA8 gameEndReason, $91CB gameClock, zp_B1 gamePhase, $92C2 exchangeActivityFlag, $92EF
|
|
; lastQuarterRound, $92B2 comcenRevealTimer, $0B9B/$0BA5 opponent flags, unit arrays, $9164
|
|
; unitBaseOrder
|
|
; Out: $91CB decremented, $92FA/message $1D at the last quarter, $F9C4 visibility, $FA28 movement
|
|
; points, $91F4 group table, $90FD unitProcessOrder rebuilt, $9162 = 0, $90FC = 0, the $0400
|
|
; scratch page used as round-order marks and $0464 as a stack; zp_1A/zp_1B are the stack
|
|
; pointer and the base-order cursor
|
|
; Called from: updateGameFrame (jmp $4210); the map generator jumps into the tail at L_4657 ($7BC3)
|
|
; after shuffling the initial order
|
|
; ----------------------------------------------------------------------
|
|
finishRound:
|
|
lda gameEndReason ; 45E6 $0BA8 <> 0 = no game is running
|
|
bne L_462B ; 45E9 then the clock must not move
|
|
lda gameClock ; 45EB $91CB = rounds left, counting down
|
|
beq L_462B ; 45EE already expired
|
|
ldy gamePhase ; 45F0 0 = the battle is running
|
|
beq L_45FE ; 45F2 in the battle every round costs a tick
|
|
ldy exchangeActivityFlag ; 45F4 $92C2 = something moved, dug in or was ordered this round
|
|
beq L_462B ; 45F7 setup phase: an idle round does not cost time
|
|
ldy #$00 ; 45F9 consume the flag...
|
|
sty exchangeActivityFlag ; 45FB ...so the next setup round has to earn its tick again
|
|
L_45FE:
|
|
sec ; 45FE prepare the subtraction
|
|
sbc #$01 ; 45FF one round gone
|
|
sta gameClock ; 4601 store the clock
|
|
bne L_4611 ; 4604 time left
|
|
lda #$08 ; 4606 end reason 8 = the clock ran out
|
|
sta gameEndReason ; 4608 record it for endGame and the score screen
|
|
jsr endGame ; 460B halt the units, reveal everything and show the result
|
|
jmp L_462B ; 460E carry on with the comcen reveal timers
|
|
|
|
L_4611:
|
|
cmp lastQuarterRound ; 4611 $92EF = the round at which the last quarter begins
|
|
bne L_462B ; 4614 not that round
|
|
lda gamePhase ; 4616 battle or setup?
|
|
beq L_4621 ; 4618 battle: warn the player
|
|
lda #$FF ; 461A setup phase reuses the same counter as a free-running timer...
|
|
sta gameClock ; 461C ...so wrap it to 255 instead of warning
|
|
bne L_462B ; 461F always taken (A = $FF)
|
|
L_4621:
|
|
lda #$1D ; 4621 message $1D = 'LAST QTR WARNING!'
|
|
jsr queueMessage ; 4623 queue it for the status line
|
|
lda #$80 ; 4626 bit 7 = last quarter
|
|
sta lastQuarterFlag ; 4628 $92FA: the status line prints the quarter from here on
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Per-side comcen reveal timer: endGame sets $92B2+side to 6 when that comcen is knocked out, and for
|
|
; those rounds the comcen is forced visible so both players can see it.
|
|
; ----------------------------------------------------------------------
|
|
L_462B:
|
|
ldx #$01 ; 462B side 1 first, then side 0
|
|
L_462D:
|
|
lda comcenRevealTimer,x ; 462D $92B2+side, counted down here
|
|
beq L_4640 ; 4630 not revealed
|
|
dec comcenRevealTimer,x ; 4632 one round less
|
|
ldy comcenUnitIndexBySide,x ; 4635 $0C8A: 49 for side 0, 99 for side 1 = that side's comcen unit
|
|
lda unitEnergyTable,y ; 4638 energy/visibility byte of the comcen
|
|
ora #$C0 ; 463B keep both visibility bits set
|
|
sta unitEnergyTable,y ; 463D store it
|
|
L_4640:
|
|
dex ; 4640 next side
|
|
bpl L_462D ; 4641 loop over sides 1 and 0
|
|
lda gamePhase ; 4643 0 = the battle is running
|
|
beq L_464A ; 4645 do the full end-of-round work
|
|
jmp resetRoundCounters ; 4647 setup phase: only reset the step counters
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Once-per-round engine work, battle only. L_4657 is also entered by JMP from the map generator
|
|
; ($7BC3, shuffleInitialUnitOrder) to build the very first round order, which is why the loop below
|
|
; tests gamePhase again before it hands out movement points.
|
|
; ----------------------------------------------------------------------
|
|
L_464A:
|
|
lda filmPlaybackMode ; 464A $0B9B bit 7 = a film is being replayed
|
|
bmi L_4657 ; 464D the film already contains the opponent's orders
|
|
lda isSoloTrainer ; 464F $0BA5 bit 7 = solo trainer
|
|
bpl L_4657 ; 4652 modem game: the opponent thinks on his own machine
|
|
jsr runTrainerRoundHook ; 4654 $E079: let the computer opponent plan this round
|
|
L_4657:
|
|
jsr checkEnemyRecyclerSpotted; 4657 may reveal the opponent's recycler if one of our units is close enough
|
|
jsr recycleDeadUnits ; 465A rebuild dead units at the recyclers
|
|
jsr rebuildGroupTables ; 465D recompute each group's speed and terrain class
|
|
jsr ageUnitVisibility ; 4660 shift 'seen this cycle' into 'visible' and clear the seen bits
|
|
ldy #$63 ; 4663 walk the units from 99 down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handOutMovePoints - give every unit this round's movement points and clear its round-order mark in
|
|
; the $0400 scratch page. A unit that is dead or standing still is pegged at 15 points; a moving unit
|
|
; adds speedClassRateTable[class] to its balance, saturating at +127. The class is the unit type, or
|
|
; the group's slowest class for a group member, or the per-player COMCEN SPEED setting for a comcen.
|
|
; ----------------------------------------------------------------------
|
|
handOutMovePoints:
|
|
lda gamePhase ; 4665 0 = battle (the map generator can enter here during setup)
|
|
bne L_46B2 ; 4667 setup: no points, only clear the mark
|
|
lda unitTypeTable,y ; 4669 type byte
|
|
bmi L_4673 ; 466C bit 7 = destroyed
|
|
ldx unitDestColTable,y ; 466E destination column, bit 7 = at destination / no order
|
|
bpl L_467A ; 4671 the unit still has somewhere to go: it earns points
|
|
L_4673:
|
|
lda #$0F ; 4673 standing or dead: a flat 15 points
|
|
sta unitMovePointsTable,y ; 4675 store the balance
|
|
bne L_46B2 ; 4678 always taken (A = $0F)
|
|
L_467A:
|
|
and #$07 ; 467A A is still the type byte: bits 0-2 = the unit type, which is also the speed class
|
|
tax ; 467C X = speed class
|
|
lda unitFlagsTable,y ; 467D flag byte
|
|
and #$40 ; 4680 bit 6 = member of a group
|
|
bne L_4696 ; 4682 a group moves at one common speed
|
|
cpx #$04 ; 4684 type 4 = COMCEN
|
|
bne L_46A4 ; 4686 ordinary unit: its type is its class
|
|
ldx comcenSpeedClass ; 4688 $92B4 = the local player's COMCEN SPEED option
|
|
cpy comcenUnit ; 468B $9236 = the local player's comcen
|
|
beq L_46A4 ; 468E yes: use the own setting
|
|
ldx opponentComcenSpeedClass; 4690 $92B5 = the opponent's COMCEN SPEED option
|
|
jmp L_46A4 ; 4693 look the rate up
|
|
|
|
L_4696:
|
|
lda unitFlagsTable,y ; 4696 flag byte
|
|
and #$3C ; 4699 group key in bits 2-5
|
|
lsr a ; 469B shift it down...
|
|
lsr a ; 469C ...to a group index 0-15
|
|
tax ; 469D index the group table
|
|
lda groupStatusTable,x ; 469E $91F4+group
|
|
and #$07 ; 46A1 bits 0-2 = the class of the group's slowest member
|
|
tax ; 46A3 X = that class
|
|
L_46A4:
|
|
lda speedClassRateTable,x ; 46A4 $4066: 12/25/12/25/0 points per round for classes 0-4
|
|
clc ; 46A7 clear carry for the addition
|
|
adc unitMovePointsTable,y ; 46A8 add to the balance the unit has saved up
|
|
bvc L_46AF ; 46AB no overflow past +127
|
|
lda #$7F ; 46AD saturate: the balance is a signed byte
|
|
L_46AF:
|
|
sta unitMovePointsTable,y ; 46AF store the new balance
|
|
L_46B2:
|
|
lda #$00 ; 46B2 mark 0 = this unit has not been placed in the round order yet
|
|
sta checksumXorConstant,y ; 46B4 $0400+unit (the same page as the scan lists, which are finished with) (also trainerPlaybook0200:play3Col12)
|
|
dey ; 46B7 next unit down
|
|
bpl handOutMovePoints ; 46B8 until unit 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Build unitProcessOrder for the coming round out of the shuffled unitBaseOrder. A unit whose next
|
|
; step would land on a unit that has not been ordered yet is pushed on a stack at $0464 and the unit
|
|
; in front of it is ordered first, so a queue of units all move in the same round instead of advancing
|
|
; one cell per round. Marks in $0400+unit: 0 = not ordered, 1 = waiting on the stack, 5 = already
|
|
; ordered. zp_1A = stack pointer, zp_1B = cursor into unitBaseOrder.
|
|
; ----------------------------------------------------------------------
|
|
lda #$00 ; 46BA start the new order at entry 0
|
|
sta unitOrderIndex ; 46BC $9162 is reused here as the write index into unitProcessOrder
|
|
lda #$FF ; 46BF empty markers
|
|
sta scratch1A ; 46C1 $1A = stack pointer of the waiting stack ($FF = empty)
|
|
sta scratch1B ; 46C3 $1B = cursor into unitBaseOrder (the first INC gives entry 0)
|
|
buildRoundOrder:
|
|
ldx scratch1A ; 46C5 anything waiting on the stack?
|
|
bmi L_46DA ; 46C7 no: take the next unit of the base order
|
|
dec scratch1A ; 46C9 pop it
|
|
ldy play4Grp12,x ; 46CB $0464+sp = the unit that was blocked (same buffer as the scan direction list)
|
|
lda checksumXorConstant,y ; 46CE its mark (also trainerPlaybook0200:play3Col12)
|
|
sec ; 46D1 prepare the subtraction
|
|
sbc #$01 ; 46D2 mark 1 (waiting) becomes 0 (unordered) so it can be placed now
|
|
sta checksumXorConstant,y ; 46D4 store the mark (also trainerPlaybook0200:play3Col12)
|
|
jmp L_46E6 ; 46D7 and try to place it
|
|
|
|
L_46DA:
|
|
inc scratch1B ; 46DA next entry of the shuffled base order
|
|
ldy scratch1B ; 46DC read the cursor
|
|
cpy #$64 ; 46DE all 100 entries used?
|
|
bcs resetRoundCounters ; 46E0 the order is complete
|
|
lda unitBaseOrder,y ; 46E2 $9164+n = a unit index
|
|
tay ; 46E5 Y = the unit to place
|
|
L_46E6:
|
|
lda checksumXorConstant,y ; 46E6 its mark (also trainerPlaybook0200:play3Col12)
|
|
bne buildRoundOrder ; 46E9 already ordered or already waiting: take the next one
|
|
placeUnitInRoundOrder:
|
|
sty currentUnit ; 46EB the unit we are trying to place
|
|
lda unitTypeTable,y ; 46EE type byte
|
|
bmi appendToRoundOrder ; 46F1 a dead unit blocks nothing: order it straight away
|
|
lda unitPathColTable,y ; 46F3 path column
|
|
and #$3F ; 46F6 drop the cloak bits 6-7
|
|
sta lineCurCol ; 46F8 line stepper start column
|
|
lda unitPathRowTable,y ; 46FA path row
|
|
and #$3F ; 46FD drop the blitz bit
|
|
sta lineCurRow ; 46FF line stepper start row
|
|
lda unitDestColTable,y ; 4701 destination column
|
|
bmi appendToRoundOrder ; 4704 bit 7 = standing still: order it straight away
|
|
sta lineEndCol ; 4706 line stepper target column
|
|
lda unitDestRowTable,y ; 4708 destination row
|
|
sta lineEndRow ; 470B line stepper target row
|
|
jsr tryStepTowardTarget ; 470D look one cell ahead; nothing is moved, only tested
|
|
bcc appendToRoundOrder ; 4710 blocked or too few movement points: order it now
|
|
lda scratch19 ; 4712 the cell it would step onto
|
|
bpl appendToRoundOrder ; 4714 free terrain: nothing to wait for
|
|
and #$7F ; 4716 a unit stands there: its index
|
|
sta scratch19 ; 4718 keep it
|
|
tax ; 471A index the marks with it
|
|
lda checksumXorConstant,x ; 471B has the blocking unit been ordered or stacked already? (also trainerPlaybook0200:play3Col12)
|
|
bne appendToRoundOrder ; 471E yes: then this unit can be ordered now
|
|
lda currentUnit ; 4720 push this unit...
|
|
inc scratch1A ; 4723 ...onto the waiting stack
|
|
ldx scratch1A ; 4725 new stack pointer
|
|
sta play4Grp12,x ; 4727 $0464+sp = the waiting unit
|
|
tax ; 472A index the marks with it
|
|
inc checksumXorConstant,x ; 472B mark 1 = waiting for the unit in front of it (also trainerPlaybook0200:play3Col12)
|
|
ldy scratch19 ; 472E now place the blocking unit first
|
|
jmp placeUnitInRoundOrder ; 4730 and repeat the same test for it
|
|
|
|
appendToRoundOrder:
|
|
ldy unitOrderIndex ; 4733 write position in the round order
|
|
lda currentUnit ; 4736 the unit that has just been settled
|
|
sta unitProcessOrder,y ; 4739 $90FD+n = the unit that will be stepped n-th next round
|
|
inc unitOrderIndex ; 473C one more unit ordered
|
|
tay ; 473F index the marks with the unit
|
|
lda #$05 ; 4740 mark 5 = ordered
|
|
sta checksumXorConstant,y ; 4742 store the mark (also trainerPlaybook0200:play3Col12)
|
|
jmp buildRoundOrder ; 4745 back to the stack / the base order
|
|
|
|
resetRoundCounters:
|
|
lda #$00 ; 4748 restart the counters
|
|
sta stepDelayTimer ; 474A $90FC = 0: the very next frame starts the new round
|
|
sta unitOrderIndex ; 474D $9162 = 0: begin at the first unit of the new order
|
|
rts ; 4750 the round is finished
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; moveUnitToCell - puts currentUnit into the cell ($93,$92) that tryStepTowardTarget just accepted:
|
|
; writes $80|unit into that cell (zp_48 still points at it), restores the terrain that was hidden
|
|
; under the unit into the cell it leaves, updates unitCol/unitRow and saves the terrain of the new
|
|
; cell. It then scores territory for the step (see the block comment at $4781).
|
|
; In: $90FA currentUnit, $93/$92 = new column/row, $19 = the byte that was in the new cell,
|
|
; zp_48/$49 -> that cell, $9228 scanUnitType, $92AC terrainPointsBySide, patched branch at $4783
|
|
; Out: both map cells rewritten, $F640/$F6A4/$FAF0/$F76C of the unit updated, $92A2/$92A4 scores,
|
|
; $9208 bumped so the status line redraws, $406E unitMovedFlag and $92C2 set; zp_48 low byte is
|
|
; reused as the point accumulator and left clobbered
|
|
; Called from: processUnitStep $42F8 (catching up with the path position) and $4345 (the normal step)
|
|
; ----------------------------------------------------------------------
|
|
moveUnitToCell:
|
|
inc unitMovedFlag ; 4751 $406E: tells processUnitStep to repaint the cell that was left
|
|
lda #$01 ; 4754 something happened this round...
|
|
sta exchangeActivityFlag ; 4756 ...so the setup-phase clock may tick ($92C2)
|
|
ldy #$00 ; 4759 offset 0 for the (zp_48),Y stores
|
|
ldx currentUnit ; 475B the moving unit
|
|
txa ; 475E its index
|
|
ora #$80 ; 475F a map cell holding a unit is $80 | unit index
|
|
sta (mapCellPtr),y ; 4761 zp_48 still points at the destination cell from readMapCell
|
|
ldy unitRowTable,x ; 4763 row of the cell being left
|
|
lda unitColTable,x ; 4766 column of the cell being left
|
|
tax ; 4769 readMapCell wants the column in X
|
|
jsr readMapCell ; 476A point zp_48 at the old cell (A = its content, unused)
|
|
ldy #$00 ; 476D offset 0 again
|
|
ldx currentUnit ; 476F the moving unit
|
|
lda unitTerrainUnderTable,x ; 4772 the terrain that was hidden under it
|
|
sta (mapCellPtr),y ; 4775 put that terrain back into the old cell
|
|
lda lineCurCol ; 4777 column the line walker stepped onto
|
|
sta unitColTable,x ; 4779 the unit now stands there
|
|
lda lineCurRow ; 477C row the line walker stepped onto
|
|
sta unitRowTable,x ; 477E store the new row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Territory scoring. A holds the unit's new row. For the opponent's units the row is mirrored
|
|
; (39-row) so that both sides are measured from their own baseline: the branch opcode at $4783 is
|
|
; patched by patchUnitSideBranches ($7BC6 in the map generator) with $90 = BCC when this machine plays
|
|
; side 0 and $B0 = BCS when it plays side 1, so 'own units' always skip the mirror. Row < 20 means
|
|
; the unit has crossed into the enemy half (zone 1), row < 10 means it has reached the enemy's rear
|
|
; (zone 2). Each zone pays a unit only once - the receipts are bits 3 and 4 of its type byte. The
|
|
; value is the owner's TERRAIN PTS setting, times 16 for a comcen and times 2 for the far zone.
|
|
; ----------------------------------------------------------------------
|
|
cpx #$32 ; 4781 unit index < 50 = side 0, >= 50 = side 1
|
|
zoneRowSideBranch:
|
|
bcc L_4788 ; 4783 opcode byte patched per side ($90 BCC for side 0, $B0 BCS for side 1): own units skip the mirror
|
|
jsr mirrorMapCoordinate ; 4785 39 - row: measure the opponent from his own end of the field
|
|
L_4788:
|
|
cmp #$0A ; 4788 within 10 rows of the enemy baseline?
|
|
bcs L_4790 ; 478A no
|
|
lda #$02 ; 478C zone 2 = the enemy's rear
|
|
bne L_4796 ; 478E always taken
|
|
L_4790:
|
|
cmp #$14 ; 4790 still in the enemy half (rows 10-19)?
|
|
bcs L_47E7 ; 4792 no, the unit is in its own half: nothing to score
|
|
lda #$01 ; 4794 zone 1 = the enemy half
|
|
L_4796:
|
|
sta L_47BF+1 ; 4796 keep the zone number in the LDA # operand at $47C0
|
|
tay ; 4799 index the receipt table with it
|
|
lda zoneScoredBitTable,y ; 479A $406B: zone 1 -> bit 3 ($08), zone 2 -> bit 4 ($10)
|
|
and unitTypeTable,x ; 479D has this unit already been paid for this zone?
|
|
bne L_47E7 ; 47A0 yes: a zone pays only once per unit
|
|
ldy #$00 ; 47A2 index 0
|
|
cpx #$32 ; 47A4 which side does the unit belong to?
|
|
bcs L_47A9 ; 47A6 side 1 unit: index 0
|
|
iny ; 47A8 side 0 unit: index 1 - both arrays are indexed by the opposing side
|
|
L_47A9:
|
|
lda terrainPointsBySide,y ; 47A9 $92AC+i = that player's TERRAIN PTS setting (0 disables territory points)
|
|
beq L_47E7 ; 47AC territory scores nothing in this game
|
|
sta mapCellPtr ; 47AE zp_48 doubles as the point accumulator; the cell pointer has done its work
|
|
lda scanUnitType ; 47B0 type of the unit that moved
|
|
cmp #$04 ; 47B3 COMCEN?
|
|
bne L_47BF ; 47B5 ordinary unit
|
|
asl mapCellPtr ; 47B7 a comcen in enemy territory is worth 16 times as much...
|
|
asl mapCellPtr ; 47B9 ...x2...
|
|
asl mapCellPtr ; 47BB ...x4...
|
|
asl mapCellPtr ; 47BD ...x8, x16
|
|
L_47BF:
|
|
lda #$FF ; 47BF operand patched at $4796 = the zone number 1 or 2
|
|
pha ; 47C1 keep the zone for the receipt below
|
|
cmp #$02 ; 47C2 the enemy's rear?
|
|
bne L_47C8 ; 47C4 no
|
|
asl mapCellPtr ; 47C6 the far zone is worth double
|
|
L_47C8:
|
|
inc lastShownClock ; 47C8 $9208 <> gameClock forces the status line to redraw the score
|
|
lda mapCellPtr ; 47CB the points earned
|
|
clc ; 47CD clear carry for the 16-bit add
|
|
adc sideScoreLo,y ; 47CE $92A2+i: the score array is indexed by the losing side, so this is the mover's own score
|
|
sta sideScoreLo,y ; 47D1 store the low byte
|
|
lda sideScoreHi,y ; 47D4 $92A4+i = high byte
|
|
adc #$00 ; 47D7 add the carry
|
|
sta sideScoreHi,y ; 47D9 store it
|
|
pla ; 47DC the zone number again
|
|
tay ; 47DD index the receipt table
|
|
lda zoneScoredBitTable,y ; 47DE bit 3 or bit 4
|
|
ora unitTypeTable,x ; 47E1 mark the unit as paid for this zone
|
|
sta unitTypeTable,x ; 47E4 store the type byte
|
|
L_47E7:
|
|
lda scratch19 ; 47E7 the byte that was in the new cell (its terrain)
|
|
sta unitTerrainUnderTable,x ; 47E9 hide it under the unit so it can be restored when the unit leaves
|
|
rts ; 47EC the move is complete
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; shuffleUnitOrder - exchanges two randomly chosen entries of unitBaseOrder ($9164, the 100 unit
|
|
; indices the round order is built from). The indices come from randomUnitIndex, which draws on the
|
|
; shared scenario RNG, so both machines shuffle in exactly the same way.
|
|
; In: $9164 unitBaseOrder, scenario RNG state $5A-$5C
|
|
; Out: two entries of unitBaseOrder exchanged, RNG advanced twice; A/X/Y clobbered
|
|
; Called from: processUnitStep $4229 (once per unit stepped during the battle) and the map
|
|
; generator $7BBC (51 swaps at game start)
|
|
; ----------------------------------------------------------------------
|
|
shuffleUnitOrder:
|
|
nop ; 47ED leftover byte at the entry point - probably an instruction that was patched out
|
|
jsr randomUnitIndex ; 47EE first slot 0-99
|
|
tax ; 47F1 keep it in X
|
|
jsr randomUnitIndex ; 47F2 second slot 0-99
|
|
tay ; 47F5 keep it in Y
|
|
lda unitBaseOrder,y ; 47F6 entry Y...
|
|
pha ; 47F9 ...onto the stack
|
|
lda unitBaseOrder,x ; 47FA entry X...
|
|
sta unitBaseOrder,y ; 47FD ...into slot Y
|
|
pla ; 4800 the saved entry...
|
|
sta unitBaseOrder,x ; 4801 ...into slot X
|
|
rts ; 4804 the two entries have been exchanged
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; randomUnitIndex - returns a unit index 0-99 drawn uniformly from the scenario random generator: a
|
|
; 7-bit sample is rejected and redrawn while it is 100 or more, so the distribution stays flat.
|
|
; In: scenario RNG state $5A-$5C
|
|
; Out: A = 0..99, C=0; RNG advanced (at least once)
|
|
; Called from: shuffleUnitOrder $47EE and $47F2
|
|
; ----------------------------------------------------------------------
|
|
randomUnitIndex:
|
|
jsr nextScenarioRandom ; 4805 $FD4A: the 24-bit generator both machines keep in step
|
|
and #$7F ; 4808 0-127
|
|
cmp #$64 ; 480A 100 units
|
|
bcs randomUnitIndex ; 480C reject 100-127 and draw again
|
|
rts ; 480E A = a unit index
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; followDirectedUnit - keeps the battlefield view locked on the unit the console is following. When
|
|
; the battlefield screen is up and infoPanelUnit ($91D7) is the unit currently being stepped, it
|
|
; computes the cell under the view cursor (viewOriginCol+3, viewOriginRow+2) and, if the unit is not
|
|
; on it, moves sprite 0 (the tracking cursor) to the unit on the overview map, switches the sprite on
|
|
; and scrolls the 7x5 view onto the unit without animation.
|
|
; In: $90FB currentScreen, $91D7 infoPanelUnit, $90FA currentUnit, zp_A5/zp_A6 view origin,
|
|
; $F640/$F6A4 unit position
|
|
; Out: $91DA/$91DB = the cursor cell, $9010/$9018 sprite 0 position, $9022 sprite 0 enabled,
|
|
; zp_92/zp_93 and zp_A5/zp_A6 moved by scrollViewToUnit
|
|
; Called from: processUnitStep $4380, after the unit has taken its step
|
|
; ----------------------------------------------------------------------
|
|
followDirectedUnit:
|
|
lda currentScreen ; 480F $90FB: 0 = the battlefield screen
|
|
bne L_485F ; 4812 the console screens do not follow units
|
|
ldy infoPanelUnit ; 4814 $91D7 = the unit whose panel/menu is open ($FF = none)
|
|
bmi L_485F ; 4817 nothing is being followed
|
|
cpy currentUnit ; 4819 is it the unit that is being stepped right now?
|
|
bne L_485F ; 481C no: leave the view alone
|
|
lda #$03 ; 481E the view is 7 cells wide...
|
|
clc ; 4820 clear carry for the add
|
|
adc viewOriginCol ; 4821 ...so its centre column is the left edge + 3
|
|
sta currentUnitCol ; 4823 $91DA = column of the cell under the cursor
|
|
lda #$02 ; 4826 the view is 5 cells high...
|
|
clc ; 4828 clear carry
|
|
adc viewOriginRow ; 4829 ...so its centre row is the top edge + 2
|
|
sta currentUnitRow ; 482B $91DB = row of the cell under the cursor
|
|
ldx unitColTable,y ; 482E column of the followed unit
|
|
lda unitRowTable,y ; 4831 row of the followed unit
|
|
tay ; 4834 readable by the compare below
|
|
cpx currentUnitCol ; 4835 is the unit already under the cursor?
|
|
bne L_483F ; 4838 no
|
|
cpy currentUnitRow ; 483A check the row too
|
|
beq L_485F ; 483D the view is already centred on it: nothing to do
|
|
L_483F:
|
|
jsr mapCellToSpriteXY ; 483F X/Y = sprite coordinates of that cell on the overview map
|
|
stx spriteXShadow ; 4842 $9010 = sprite 0 X (the tracking cursor)
|
|
sty spriteYShadow ; 4845 $9018 = sprite 0 Y
|
|
lda #$01 ; 4848 mask for sprite 0
|
|
jsr enableSprites ; 484A switch it on (unless sprites are suppressed)
|
|
lda currentUnitCol ; 484D the scroll starts at the current cursor cell
|
|
sta lineCurCol ; 4850 line stepper start column
|
|
lda currentUnitRow ; 4852 cursor row
|
|
sta lineCurRow ; 4855 line stepper start row
|
|
lda #$00 ; 4857 0 ticks per step = jump there without animating
|
|
ldx infoPanelUnit ; 4859 the unit to centre on
|
|
jsr scrollViewToUnit ; 485C $1D4E: walk the view over to it
|
|
L_485F:
|
|
rts ; 485F done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateHitFlashTimers - ages the hit markers of all 100 units. unitHitFlashTable ($FA8C) is
|
|
; $80|damage while a unit is flashing (the map drawing code at $286F paints such a unit white); the
|
|
; low nibble counts engine steps down and the high nibble is left alone. When the count expires the
|
|
; marker is cleared and the cell is repainted: a living unit is drawn again, and a destroyed one has
|
|
; the terrain that was under it written back into the map and drawn instead.
|
|
; In: $FA8C unitHitFlashTable, $F76C unitTypeTable, $FAF0 unitTerrainUnderTable
|
|
; Out: $FA8C aged, map cells and screen updated for expired markers; zp_18 is the loop index
|
|
; Called from: updateGameFrame $41FB, once per engine step
|
|
; ----------------------------------------------------------------------
|
|
updateHitFlashTimers:
|
|
lda #$63 ; 4860 walk the units from 99...
|
|
sta scratch18 ; 4862 ...down to 0 in zp_18
|
|
L_4864:
|
|
ldx scratch18 ; 4864 current unit index
|
|
lda unitHitFlashTable,x ; 4866 $FA8C: 0 = no marker, else $80|damage with the countdown in bits 0-3
|
|
beq L_489D ; 4869 this unit is not flashing
|
|
tay ; 486B keep the whole byte
|
|
and #$F0 ; 486C high nibble = the marker bit plus the top bits of the damage
|
|
sta L_4879+1 ; 486E patch it into the ORA operand at $487A
|
|
tya ; 4871 the byte again
|
|
and #$0F ; 4872 the countdown in the low nibble
|
|
beq L_4881 ; 4874 expired: take the marker down
|
|
sec ; 4876 prepare the subtraction
|
|
sbc #$01 ; 4877 one engine step less
|
|
L_4879:
|
|
ora #$FF ; 4879 operand patched at $486E: put the high nibble back
|
|
sta unitHitFlashTable,x ; 487B store the aged marker
|
|
jmp L_489D ; 487E next unit
|
|
|
|
L_4881:
|
|
lda #$00 ; 4881 clear the marker...
|
|
sta unitHitFlashTable,x ; 4883 ...so the unit stops flashing white
|
|
lda unitTypeTable,x ; 4886 type byte
|
|
bmi L_4890 ; 4889 bit 7 = the unit was destroyed
|
|
txa ; 488B living unit: its index...
|
|
ora #$80 ; 488C ...as the map byte $80|unit
|
|
bne L_4898 ; 488E always taken (the value is >= $80)
|
|
L_4890:
|
|
lda unitTerrainUnderTable,x ; 4890 the terrain that was hidden under the wreck
|
|
pha ; 4893 keep it for the redraw
|
|
jsr removeUnitLeavingCell ; 4894 write that terrain into the map cell and free the unit's slot ($FAF0 = $FF)
|
|
pla ; 4897 the terrain byte again
|
|
L_4898:
|
|
ldy scratch18 ; 4898 Y = the unit whose cell is redrawn
|
|
jsr drawValueAtUnit ; 489A repaint the cell in the view and on the overview map
|
|
L_489D:
|
|
dec scratch18 ; 489D next unit down
|
|
bpl L_4864 ; 489F until unit 0
|
|
rts ; 48A1 all markers aged
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawUnitAtPosition - redraws unit Y where it stands: builds the map byte $80|unit and falls into
|
|
; drawValueAtUnit.
|
|
; In: Y = unit index
|
|
; Out: see drawValueAtUnit
|
|
; Called from: $3BDB (after damage), $4185/$41B1 (pending unit/group redraws), $43C7 (newly spotted
|
|
; enemy) and $4534 (end of a unit step)
|
|
; ----------------------------------------------------------------------
|
|
drawUnitAtPosition:
|
|
tya ; 48A2 the unit index
|
|
ora #$80 ; 48A3 a map cell holding a unit reads $80 | index
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawValueAtUnit - draws cell value A at the position of unit Y.
|
|
; In: A = the cell byte to draw, Y = unit index
|
|
; Out: the cell is repainted in the 7x5 view and on the overview map; A/X/Y clobbered
|
|
; Called from: updateHitFlashTimers $489A, and by falling through from drawUnitAtPosition
|
|
; ----------------------------------------------------------------------
|
|
drawValueAtUnit:
|
|
pha ; 48A5 keep the cell value
|
|
ldx unitColTable,y ; 48A6 X = the unit's map column
|
|
lda unitRowTable,y ; 48A9 the unit's map row...
|
|
tay ; 48AC ...into Y as drawMapCell wants it
|
|
pla ; 48AD the cell value again
|
|
jmp drawMapCell ; 48AE $5C7C: repaint that cell everywhere it is visible
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateIdleFlag - recomputes bit 7 of unitDestCol ($F7D0) for currentUnit: it is set when the
|
|
; destination equals the unit's map position (nothing left to do) and cleared while the unit still has
|
|
; somewhere to go. The other bits of the byte are kept. The result is delivered in A so the caller
|
|
; can copy the same bit into the display flags.
|
|
; In: $90FA currentUnit, $F640/$F6A4 position, $F7D0/$F834 destination
|
|
; Out: A = the new $F7D0 byte (bit 7 = at destination), Y = currentUnit, N reflects that bit
|
|
; Called from: processUnitStep $4278 (before the step) and $4383 (after it)
|
|
; ----------------------------------------------------------------------
|
|
updateIdleFlag:
|
|
ldy currentUnit ; 48B1 the unit being stepped
|
|
lda #$00 ; 48B4 assume 'still travelling'...
|
|
sta L_48D7+1 ; 48B6 ...by clearing the ORA operand at $48D8
|
|
lda unitDestRowTable,y ; 48B9 destination row
|
|
and #$3F ; 48BC bits 0-5 only: bit 7 is the repair flag
|
|
cmp unitRowTable,y ; 48BE is the unit already on that row?
|
|
bne L_48D2 ; 48C1 no: it still has to move
|
|
lda unitDestColTable,y ; 48C3 destination column
|
|
and #$3F ; 48C6 bits 0-5 only
|
|
cmp unitColTable,y ; 48C8 is it on that column too?
|
|
bne L_48D2 ; 48CB no: it still has to move
|
|
lda #$80 ; 48CD bit 7 = at destination
|
|
sta L_48D7+1 ; 48CF patch it into the ORA operand at $48D8
|
|
L_48D2:
|
|
lda unitDestColTable,y ; 48D2 the destination column byte
|
|
and #$7F ; 48D5 drop the old flag, keep the coordinate
|
|
L_48D7:
|
|
ora #$FF ; 48D7 operand patched above: 0 or $80
|
|
sta unitDestColTable,y ; 48D9 store the refreshed byte
|
|
rts ; 48DC A = that byte, Y = currentUnit
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rebuildGroupTables - recomputes the 16 entries of groupStatusTable ($91F4), one per group key (7
|
|
; groups per side plus the side bit). For every living member it works out the speed class (the unit
|
|
; type, or the per-player COMCEN SPEED setting for a comcen) and the terrain class of the ground it
|
|
; stands on (always 0 for a spy) and keeps the group's worst terrain class in bits 3-5 and the class
|
|
; of its slowest member in bits 0-2, so a group crosses country like its slowest member on the worst
|
|
; ground any member occupies. Bits 6-7 (group blitzing / group cloaked) are carried over from the
|
|
; previous value for groups that still have members. Scratch: $0400+g member count, $0410+g worst
|
|
; movement cost, $0420+g best movement rate, $0430+g the previous entry.
|
|
; In: unit arrays $F708/$F76C/$FAF0, $92B4/$92B5 comcen speed classes, $9236 comcenUnit, tables
|
|
; $405E terrainMoveCostTable, $4066 speedClassRateTable, $077E terrain classes
|
|
; Out: $91F4-$9203 rewritten, $0400-$043F used as scratch; zp_18/zp_19/zp_1A clobbered
|
|
; Called from: finishRound $465D, once per round
|
|
; ----------------------------------------------------------------------
|
|
rebuildGroupTables:
|
|
ldy #$0F ; 48DD 16 group slots: 7 groups per side, keyed by bits 2-5 of the unit flags
|
|
L_48DF:
|
|
lda groupStatusTable,y ; 48DF the entry as it was last round
|
|
sta play3Row10,y ; 48E2 $0430+g: keep it for its blitz/cloak bits
|
|
lda #$00 ; 48E5 start from scratch
|
|
sta groupStatusTable,y ; 48E7 clear the entry
|
|
sta checksumXorConstant,y ; 48EA $0400+g = member count (also trainerPlaybook0200:play3Col12)
|
|
sta play3Col28,y ; 48ED $0410+g = worst movement cost met so far
|
|
lda #$FF ; 48F0 worse than any real rate
|
|
sta play3Col44,y ; 48F2 $0420+g = best (lowest) movement rate met so far
|
|
dey ; 48F5 next slot
|
|
bpl L_48DF ; 48F6 until slot 0
|
|
lda #$63 ; 48F8 scan the units from 99...
|
|
sta scratch1A ; 48FA ...down to 0 in zp_1A
|
|
L_48FC:
|
|
ldy scratch1A ; 48FC current unit
|
|
lda unitTypeTable,y ; 48FE type byte
|
|
bmi L_496D ; 4901 bit 7 = destroyed: a dead member does not slow the group down
|
|
lda unitFlagsTable,y ; 4903 flag byte
|
|
and #$40 ; 4906 bit 6 = the unit is in a group
|
|
beq L_496D ; 4908 not a member
|
|
lda unitFlagsTable,y ; 490A flag byte again
|
|
and #$3C ; 490D group key in bits 2-5
|
|
lsr a ; 490F shift it down...
|
|
lsr a ; 4910 ...to a group index 0-15
|
|
tax ; 4911 index the group tables with it
|
|
inc checksumXorConstant,x ; 4912 one more member in this group (also trainerPlaybook0200:play3Col12)
|
|
lda unitTypeTable,y ; 4915 type byte
|
|
and #$07 ; 4918 unit type 0-4 = speed class
|
|
cmp #$04 ; 491A COMCEN?
|
|
bne L_4929 ; 491C ordinary unit: its type is its class
|
|
lda comcenSpeedClass ; 491E $92B4 = the local player's COMCEN SPEED option
|
|
cpy comcenUnit ; 4921 $9236 = the local player's comcen
|
|
beq L_4929 ; 4924 yes: keep the own setting
|
|
lda opponentComcenSpeedClass; 4926 $92B5 = the opponent's COMCEN SPEED option
|
|
L_4929:
|
|
sta scratch18 ; 4929 zp_18 = the member's speed class
|
|
sec ; 492B prepare the subtraction
|
|
sbc #$03 ; 492C class 3 = SPY
|
|
beq L_4937 ; 492E a spy ignores the terrain: class 0 (A is 0 here)
|
|
lda unitTerrainUnderTable,y ; 4930 terrain code hidden under the member
|
|
tay ; 4933 index the class table with it
|
|
lda D_077E,y ; 4934 $077E: terrain code -> movement class
|
|
L_4937:
|
|
sta scratch19 ; 4937 zp_19 = the member's terrain class
|
|
tay ; 4939 index the cost table
|
|
lda terrainMoveCostTable,y ; 493A $405E: movement points that class costs per step
|
|
cmp play3Col28,x ; 493D worse ground than the group has met so far?
|
|
bcc L_4956 ; 4940 no: leave the terrain class alone
|
|
sta play3Col28,x ; 4942 remember the new worst cost
|
|
lda scratch19 ; 4945 the class that caused it
|
|
asl a ; 4947 shift it...
|
|
asl a ; 4948 ...into...
|
|
asl a ; 4949 ...bits 3-5
|
|
sta scratch19 ; 494A keep it
|
|
lda groupStatusTable,x ; 494C the group entry
|
|
and #$07 ; 494F keep the speed class bits 0-2
|
|
ora scratch19 ; 4951 insert the new terrain class
|
|
sta groupStatusTable,x ; 4953 store the entry
|
|
L_4956:
|
|
ldy scratch18 ; 4956 the member's speed class again
|
|
lda speedClassRateTable,y ; 4958 $4066: movement points that class earns per round
|
|
cmp play3Col44,x ; 495B slower than the slowest member so far?
|
|
bcs L_496D ; 495E no: leave the speed class alone
|
|
sta play3Col44,x ; 4960 remember the new lowest rate
|
|
lda groupStatusTable,x ; 4963 the group entry
|
|
and #$F8 ; 4966 keep the terrain class bits 3-5
|
|
ora scratch18 ; 4968 insert the slower class into bits 0-2
|
|
sta groupStatusTable,x ; 496A store the entry
|
|
L_496D:
|
|
dec scratch1A ; 496D next unit down
|
|
bpl L_48FC ; 496F until unit 0
|
|
ldx #$0F ; 4971 final pass over the 16 slots
|
|
L_4973:
|
|
lda checksumXorConstant,x ; 4973 did the group have any living member? (also trainerPlaybook0200:play3Col12)
|
|
beq L_4980 ; 4976 no: the entry stays cleared
|
|
lda play3Row10,x ; 4978 last round's entry
|
|
and #$C0 ; 497B bit 7 = group cloaked, bit 6 = group blitzing
|
|
ora groupStatusTable,x ; 497D add them to the freshly computed classes
|
|
L_4980:
|
|
sta groupStatusTable,x ; 4980 store the entry
|
|
dex ; 4983 next slot
|
|
bpl L_4973 ; 4984 until slot 0
|
|
rts ; 4986 the group table is up to date
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; averageGroupMoveVector - averages the outstanding movement of one group. Over all 100 units it sums
|
|
; (unitDestCol & $7F) - unitCol and (unitDestRow & $7F) - unitRow as two sign-extended 16-bit totals,
|
|
; counting only living units whose authoritative flag byte has the member bit 6 set and whose group
|
|
; key (bits 2-5) equals cmdParam2, and divides both totals by the member count. Its only caller is
|
|
; followGroupMovement, so it runs when a unit joins a group and has to inherit the heading the group
|
|
; is already moving on.
|
|
; In: zp_75 cmdParam2 = the group key in bits 2-5 (group number plus side bit); unit arrays $F640
|
|
; column, $F6A4 row, $F708 flags, $F76C type, $F7D0/$F834 destination
|
|
; Out: A = member count (Z=1 when the group is empty), X = average column delta, Y = average row
|
|
; delta; zp_18-zp_1C destroyed; self-modifies the sign-extension ADC operands at $49C6 and $49E3
|
|
; Called from: followGroupMovement $4A04
|
|
; ----------------------------------------------------------------------
|
|
averageGroupMoveVector:
|
|
lda #$00 ; 4987 clear the accumulators
|
|
sta scratch18 ; 4989 zp_18/zp_19 = 16-bit sum of the column deltas
|
|
sta scratch19 ; 498B high byte of that sum
|
|
sta scratch1A ; 498D zp_1A/zp_1B = 16-bit sum of the row deltas
|
|
sta scratch1B ; 498F high byte of that sum
|
|
sta scratch1C ; 4991 zp_1C = number of members found
|
|
ldy #$63 ; 4993 walk the units from 99 down to 0
|
|
L_4995:
|
|
lda unitTypeTable,y ; 4995 type byte
|
|
bmi L_49E6 ; 4998 bit 7 = destroyed: not counted
|
|
lda unitFlagsTable,y ; 499A flag byte
|
|
and #$40 ; 499D bit 6 = the unit is in a group
|
|
beq L_49E6 ; 499F not a member
|
|
lda unitFlagsTable,y ; 49A1 flag byte again
|
|
and #$3C ; 49A4 group key in bits 2-5
|
|
cmp cmdParam2 ; 49A6 zp_75 holds the wanted key in the same bit positions
|
|
bne L_49E6 ; 49A8 a member of some other group
|
|
inc scratch1C ; 49AA count this member
|
|
ldx #$00 ; 49AC sign-extension byte for a positive delta
|
|
lda unitDestColTable,y ; 49AE destination column
|
|
and #$7F ; 49B1 drop bit 7 = at destination
|
|
sec ; 49B3 prepare the subtraction
|
|
sbc unitColTable,y ; 49B4 how far this member still has to travel sideways
|
|
bpl L_49BB ; 49B7 positive: extend with 0
|
|
ldx #$FF ; 49B9 negative: extend with $FF
|
|
L_49BB:
|
|
stx L_49C5+1 ; 49BB patch the sign into the ADC operand at $49C6
|
|
clc ; 49BE clear carry for the 16-bit add
|
|
adc scratch18 ; 49BF add the low byte to the running total
|
|
sta scratch18 ; 49C1 store it
|
|
lda scratch19 ; 49C3 high byte of the total
|
|
L_49C5:
|
|
adc #$FF ; 49C5 operand patched above: add the sign extension plus the carry
|
|
sta scratch19 ; 49C7 store it
|
|
ldx #$00 ; 49C9 the same again for the row
|
|
lda unitDestRowTable,y ; 49CB destination row
|
|
and #$7F ; 49CE drop bit 7 = repairing
|
|
sec ; 49D0 prepare the subtraction
|
|
sbc unitRowTable,y ; 49D1 how far this member still has to travel vertically
|
|
bpl L_49D8 ; 49D4 positive: extend with 0
|
|
ldx #$FF ; 49D6 negative: extend with $FF
|
|
L_49D8:
|
|
stx L_49E2+1 ; 49D8 patch the sign into the ADC operand at $49E3
|
|
clc ; 49DB clear carry
|
|
adc scratch1A ; 49DC add the low byte
|
|
sta scratch1A ; 49DE store it
|
|
lda scratch1B ; 49E0 high byte of the total
|
|
L_49E2:
|
|
adc #$FF ; 49E2 operand patched above
|
|
sta scratch1B ; 49E4 store it
|
|
L_49E6:
|
|
dey ; 49E6 next unit down
|
|
bpl L_4995 ; 49E7 until unit 0
|
|
lda scratch18 ; 49E9 column total, low byte
|
|
ldy scratch19 ; 49EB column total, high byte (carries the sign)
|
|
ldx scratch1C ; 49ED divide by the number of members
|
|
jsr divideSigned16by8 ; 49EF $FCEC: signed 16 by 8 division
|
|
pha ; 49F2 keep the average column delta
|
|
lda scratch1A ; 49F3 row total, low byte
|
|
ldy scratch1B ; 49F5 row total, high byte
|
|
ldx scratch1C ; 49F7 the same divisor
|
|
jsr divideSigned16by8 ; 49F9 average the row delta too
|
|
tay ; 49FC Y = average row delta
|
|
pla ; 49FD the column result
|
|
tax ; 49FE X = average column delta
|
|
lda scratch1C ; 49FF A = member count, Z=1 when the group turned out to be empty
|
|
rts ; 4A01 back to followGroupMovement
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; followGroupMovement - Gives the unit that is joining a group the movement the rest of the group
|
|
; already has: averageGroupMoveVector ($4987) returns the average (destination - position) vector of
|
|
; the members and the unit's new destination becomes its own cell plus that vector. The destination is
|
|
; applied through the shared destination setter sub_34D5, which clamps it to the map and maintains the
|
|
; at-destination bit. A group with no members leaves the unit untouched.
|
|
; In: Y = index of the joining unit; cmdParam2 ($75) = group key (unitFlagsTable bits 2-5), read by
|
|
; averageGroupMoveVector; unit position tables $F640/$F6A4
|
|
; Out: cmdParam1/cmdParam2 = the new destination cell, written into unitDestColTable/unitDestRowTable
|
|
; by sub_34D5; scratch18/scratch19 = average column/row step, scratch1D = saved unit index
|
|
; Called from: joinUnitToGroup ($4ADE) only, i.e. the executor of command $88 JOIN GROUP
|
|
; ----------------------------------------------------------------------
|
|
followGroupMovement:
|
|
sty scratch1D ; 4A02 remember the joining unit: averageGroupMoveVector needs Y for its own 100-unit scan
|
|
jsr averageGroupMoveVector ; 4A04 average (destination - position) over the members of group cmdParam2: A = member count, X = average column step, Y = average row step
|
|
beq L_4A34 ; 4A07 branch if the group has no members at all (count 0): leave this unit's destination alone
|
|
stx scratch18 ; 4A09 keep the group's average column step (signed, may be negative)
|
|
sty scratch19 ; 4A0B keep the group's average row step
|
|
ldy scratch1D ; 4A0D point Y back at the joining unit
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The next two instructions look like a bug: the test was meant to be 'is the average vector (dx,dy)
|
|
; zero?', but TYA here loads the unit index rather than the average row step that was just stored in
|
|
; scratch19. The result is that the stand-still path below is only reachable for unit index 0, and
|
|
; then only when the average column step is zero as well. It is harmless in the usual case (adding
|
|
; zero to the unit's own cell gives the same destination), except for unit 0 joining a group whose
|
|
; average dx is 0 and dy is not: that unit is told to stand still.
|
|
; ----------------------------------------------------------------------
|
|
tya ; 4A0F A = the unit index (this was probably meant to be LDA scratch19, the average row step)
|
|
ora scratch18 ; 4A10 OR in the average column step and test the pair for zero
|
|
bne L_4A21 ; 4A12 branch unless both bytes are zero - true for every unit except index 0 with a zero average column step
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Stand-still: the destination becomes the unit's own cell.
|
|
; ----------------------------------------------------------------------
|
|
lda unitColTable,y ; 4A14 column of the cell the unit stands on
|
|
sta cmdParam1 ; 4A17 cmdParam1 = destination column for sub_34D5
|
|
lda unitRowTable,y ; 4A19 row of the cell the unit stands on
|
|
sta cmdParam2 ; 4A1C cmdParam2 = destination row
|
|
jmp L_4A31 ; 4A1E go and apply the destination
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Follow the group: destination = own cell + the group's average movement vector.
|
|
; ----------------------------------------------------------------------
|
|
L_4A21:
|
|
lda scratch19 ; 4A21 average row step of the group
|
|
clc ; 4A23 plain add, no carry in
|
|
adc unitRowTable,y ; 4A24 new destination row = own row + average row step
|
|
sta cmdParam2 ; 4A27 cmdParam2 = destination row
|
|
lda scratch18 ; 4A29 average column step of the group
|
|
clc ; 4A2B plain add
|
|
adc unitColTable,y ; 4A2C new destination column = own column + average column step
|
|
sta cmdParam1 ; 4A2F cmdParam1 = destination column
|
|
L_4A31:
|
|
jsr setUnitDestination ; 4A31 shared destination setter: clamps to columns/rows 0-39, writes unitDestColTable/unitDestRowTable of unit Y, refreshes the at-destination bit 7 in unitDisplayFlagsTable and stands the unit up (clears the dug-in bit 6 of unitTypeTable)
|
|
L_4A34:
|
|
rts ; 4A34 back to joinUnitToGroup
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendGroupMoveToCursor - Fire-button action of the battlefield target cursor while a whole group is
|
|
; selected (uiLevel $9209 >= 2). It queues command $81 with the selected unit as the group leader and
|
|
; the cell under the cursor (view origin + 3 columns, + 2 rows - the centre of the 7x5 tactical view)
|
|
; as the ordered position, then refreshes the leader's local at-destination flag. Unlike the
|
|
; single-unit order $80 it leaves the unit's member bit alone, so the leader stays in its group.
|
|
; In: selectedUnit ($90F8) = the group leader, viewOriginCol/viewOriginRow (zp_A5/zp_A6) = top-left
|
|
; cell of the tactical view
|
|
; Out: cmdParam0/1/2 = unit, column, row; 4 bytes ($81,unit,col,row) queued in the outgoing command
|
|
; ring by queueCmd4 ($560A); unitDisplayFlags bit 7 of the leader refreshed; C=0 always (the
|
|
; caller tests C)
|
|
; Called from: runTargetCursorLoop ($2218), the fire-click branch taken when uiLevel is 2 or more
|
|
; ----------------------------------------------------------------------
|
|
sendGroupMoveToCursor:
|
|
lda #$03 ; 4A35 the cursor sits 3 cells right of the view origin (centre column of the 7-cell wide view)
|
|
clc ; 4A37 plain add
|
|
adc viewOriginCol ; 4A38 column of the cell under the cursor
|
|
sta cmdParam1 ; 4A3A command argument 2 = ordered column
|
|
lda #$02 ; 4A3C the cursor sits 2 cells below the view origin (centre row of the 5-cell high view)
|
|
clc ; 4A3E plain add
|
|
adc viewOriginRow ; 4A3F row of the cell under the cursor
|
|
sta cmdParam2 ; 4A41 command argument 3 = ordered row
|
|
lda selectedUnit ; 4A43 the unit the player selected acts as the group's leader
|
|
sta cmdParam0 ; 4A46 command argument 1 = leader unit index
|
|
lda #$81 ; 4A48 command $81 = move group keeping formation (4 bytes: unit, column, row)
|
|
jsr queueCmd4 ; 4A4A queue $81 with its three arguments; it runs on both machines when the exchange is executed
|
|
jsr updateUnitAtDestinationFlag; 4A4D immediate local feedback: set bit 7 of unitDisplayFlags[leader] if the ordered cell is the one it already stands on, clear it otherwise
|
|
clc ; 4A50 C=0 = the order was accepted
|
|
rts ; 4A51 back to the target cursor loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; moveGroupKeepingFormation - Executor of command $81, run in lock step on both machines. It works out
|
|
; the offset between the leader's current destination and the newly ordered cell and subtracts that
|
|
; offset from the destination of every live member of the leader's group, so the leader ends up
|
|
; exactly on the ordered cell and the relative shape of the formation is preserved. Membership is
|
|
; taken from the authoritative unitFlagsTable, not from the UI copy. Members whose shifted destination
|
|
; falls off the map are clamped by sub_34D5, which does distort the formation at the map edges.
|
|
; In: cmdParam0 ($73) = leader unit index, cmdParam1/cmdParam2 = ordered column/row (already mirrored
|
|
; for a remote command by mirrorCmdCoordsIfRemote)
|
|
; Out: unitDestColTable/unitDestRowTable of every member rewritten through sub_34D5; cmdParam0 is
|
|
; overwritten with the group key, fillEndPtr (zp_4A) is the 99..0 loop counter; self-modifies the
|
|
; SBC operands at $4A8E (column offset) and $4A98 (row offset)
|
|
; Called from: cmdMoveGroupRelative ($5020), the handler of command $81
|
|
; ----------------------------------------------------------------------
|
|
moveGroupKeepingFormation:
|
|
ldy cmdParam0 ; 4A52 Y = the leader unit named by the command
|
|
lda unitDestColTable,y ; 4A54 leader's current destination column
|
|
and #$7F ; 4A57 drop bit 7, the at-destination/idle marker kept in the same byte
|
|
sec ; 4A59 borrow clear for a true subtract
|
|
sbc cmdParam1 ; 4A5A column offset = leader destination column - ordered column
|
|
sta L_4A8D+1 ; 4A5C patch the SBC operand at $4A8E: every member is shifted by this column offset
|
|
lda unitDestRowTable,y ; 4A5F leader's current destination row
|
|
and #$7F ; 4A62 drop the idle marker
|
|
sec ; 4A64 borrow clear
|
|
sbc cmdParam2 ; 4A65 row offset = leader destination row - ordered row
|
|
sta L_4A97+1 ; 4A67 patch the SBC operand at $4A98 with the row offset
|
|
lda unitFlagsTable,y ; 4A6A authoritative flag byte of the leader
|
|
and #$3C ; 4A6D keep group number (bits 2-4) and side (bit 5)
|
|
ora #$40 ; 4A6F add the in-a-group bit 6 so the value can be compared against flags & $7C
|
|
sta cmdParam0 ; 4A71 cmdParam0 now carries the group key instead of the leader index
|
|
lda #$63 ; 4A73 start the scan at unit 99, the last slot
|
|
sta fillEndPtr ; 4A75 zp_4A is the loop counter over the 100 unit slots
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk all 100 unit slots from 99 down to 0 and shift every live member of the leader's group by the
|
|
; same offset. zp_4A holds the index; Y is reloaded from it because sub_34D5 is called inside the
|
|
; loop.
|
|
; ----------------------------------------------------------------------
|
|
L_4A77:
|
|
ldy fillEndPtr ; 4A77 index of the unit being considered
|
|
lda unitTypeTable,y ; 4A79 its type byte; bit 7 marks a dead or empty slot
|
|
bmi L_4A9E ; 4A7C skip dead units
|
|
lda unitFlagsTable,y ; 4A7E authoritative flags of this unit
|
|
and #$7C ; 4A81 group number, side and the in-a-group bit
|
|
cmp cmdParam0 ; 4A83 does it belong to the leader's group?
|
|
bne L_4A9E ; 4A85 no: try the next slot
|
|
lda unitDestColTable,y ; 4A87 member's current destination column
|
|
and #$7F ; 4A8A without the idle marker
|
|
sec ; 4A8C borrow clear
|
|
L_4A8D:
|
|
sbc #$FF ; 4A8D operand patched at $4A5C: subtract the group's column offset
|
|
sta cmdParam1 ; 4A8F cmdParam1 = the member's new destination column
|
|
lda unitDestRowTable,y ; 4A91 member's current destination row
|
|
and #$7F ; 4A94 without the idle marker
|
|
sec ; 4A96 borrow clear
|
|
L_4A97:
|
|
sbc #$FF ; 4A97 operand patched at $4A67: subtract the group's row offset
|
|
sta cmdParam2 ; 4A99 cmdParam2 = the member's new destination row
|
|
jsr setUnitDestination ; 4A9B apply it to unit Y (clamped to 0-39; the leader lands exactly on the ordered cell)
|
|
L_4A9E:
|
|
dec fillEndPtr ; 4A9E next lower unit index
|
|
bpl L_4A77 ; 4AA0 loop until slot 0 has been handled
|
|
rts ; 4AA2 back to the command dispatcher
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; addSelectedUnitToGroup - The 'ADDED' half of the group membership editor. isPointWithinGroupRange
|
|
; rejects a unit that is more than 8 cells from the group's current bounding box - then 'TOO FAR!' is
|
|
; shown and C=1 returned. Otherwise the local flag copy is updated at once so the viewport highlights
|
|
; the unit immediately, and command $88 is queued so that both machines perform the join in lock step.
|
|
; In: selectedGroupKey ($9206) = group key (group number in bits 2-4, side in bit 5), selectedUnit
|
|
; ($90F8)
|
|
; Out: C=0 accepted: unitDisplayFlags[unit] = (old & $83) | key | $40 and 3 bytes ($88, unit, key)
|
|
; queued by queueCmd3 ($560F); C=1 rejected, with the TOO FAR! message and sound 3
|
|
; Called from: the JOIN GROUP menu ($61C6) and the group membership editor ($6483)
|
|
; ----------------------------------------------------------------------
|
|
addSelectedUnitToGroup:
|
|
lda selectedGroupKey ; 4AA3 group being edited (bits 2-4 group number, bit 5 side)
|
|
ldy selectedUnit ; 4AA6 Y below $80 means: test this unit's map cell against the members' map cells
|
|
jsr isPointWithinGroupRange ; 4AA9 refuse a unit further than 8 cells from the group's bounding box
|
|
bcc L_4AB4 ; 4AAC C=0: it is close enough, add it
|
|
jsr showTooFarMessage ; 4AAE C=1: beep and print 'TOO FAR!' in the info panel
|
|
jmp L_4AD2 ; 4AB1 return with C=1, nothing changed
|
|
|
|
L_4AB4:
|
|
ldx selectedUnit ; 4AB4 the unit to add
|
|
lda unitDisplayFlagsTable,x ; 4AB7 its local flag copy, the one the viewport and the editor read
|
|
and #$83 ; 4ABA keep the two facing bits and bit 7 (at destination); drop any old group bits
|
|
ora selectedGroupKey ; 4ABC insert the new group number and side
|
|
ora #$40 ; 4ABF and the in-a-group bit
|
|
sta unitDisplayFlagsTable,x ; 4AC1 instant local feedback; the authoritative $F708 only changes when command $88 executes
|
|
stx cmdParam1 ; 4AC4 command argument 1 = unit index
|
|
lda selectedGroupKey ; 4AC6 the group it joins
|
|
sta cmdParam2 ; 4AC9 command argument 2 = group key
|
|
lda #$88 ; 4ACB command $88 = join group (3 bytes: unit, group key)
|
|
jsr queueCmd3 ; 4ACD queue it for both machines
|
|
clc ; 4AD0 C=0 = added
|
|
rts ; 4AD1 back to the editor
|
|
|
|
L_4AD2:
|
|
sec ; 4AD2 C=1 = rejected as too far away
|
|
rts ; 4AD3 back to the editor
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; joinUnitToGroup - Executor of command $88. It first gives the unit the group's current movement
|
|
; through followGroupMovement, then writes the group key and the member bit into the authoritative
|
|
; unitFlagsTable and mirrors the new bits into the display copy with syncLocalGroupFlags. Only the two
|
|
; facing bits survive the mask, so joining a group also clears the unit's pending first-strike bit 7,
|
|
; and the side bit comes from the command argument rather than from the unit.
|
|
; In: cmdParam1 ($74) = unit index, cmdParam2 ($75) = group key, both fetched by readCmdArgs2 in
|
|
; cmdJoinGroup
|
|
; Out: unitFlagsTable[unit] = (old & $03) | key | $40 and unitDisplayFlags[unit] resynced; the unit's
|
|
; destination is set to the group's average movement; self-modifies the LDX operand at $4AE2 and
|
|
; the ORA operand at $4AE9
|
|
; Called from: cmdJoinGroup ($5198), the handler of command $88
|
|
; ----------------------------------------------------------------------
|
|
joinUnitToGroup:
|
|
ldy cmdParam1 ; 4AD4 unit index argument of command $88
|
|
sty L_4AE1+1 ; 4AD6 patch the LDX # operand at $4AE2 with it - followGroupMovement clobbers X and Y
|
|
lda cmdParam2 ; 4AD9 group key argument
|
|
sta L_4AE8+1 ; 4ADB patch the ORA # operand at $4AE9 with the group key
|
|
jsr followGroupMovement ; 4ADE let the new member pick up the group's average movement (Y still holds the unit index)
|
|
L_4AE1:
|
|
ldx #$FF ; 4AE1 operand patched at $4AD6: the joining unit
|
|
lda unitFlagsTable,x ; 4AE3 its authoritative flag byte
|
|
and #$03 ; 4AE6 keep only the facing bits 0-1; the old group bits and the first-strike bit 7 are dropped
|
|
L_4AE8:
|
|
ora #$FF ; 4AE8 operand patched at $4ADB: group number and side from the command
|
|
ora #$40 ; 4AEA mark the unit as being in a group
|
|
sta unitFlagsTable,x ; 4AEC this is the copy both machines agree on
|
|
jmp syncLocalGroupFlags ; 4AEF mirror the new group bits into the display copy $FB54 and return from there
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; removeSelectedUnitFromGroup - The 'REMOVED' half of the membership editor. It clears the member bit
|
|
; 6 in the selected unit's local flag copy straight away so the viewport highlight updates on the next
|
|
; frame, and queues the 2-byte command $89, whose executor cmdLeaveGroup ($519B) clears the same bit
|
|
; in the authoritative unitFlagsTable on both machines and re-syncs the copy.
|
|
; In: selectedUnit ($90F8)
|
|
; Out: unitDisplayFlags[unit] &= $BF, cmdParam0 = unit, 2 bytes ($89, unit) queued by queueCmd2
|
|
; ($561F)
|
|
; Called from: the group membership editor ($64B1)
|
|
; ----------------------------------------------------------------------
|
|
removeSelectedUnitFromGroup:
|
|
ldy selectedUnit ; 4AF2 the unit the cursor is on
|
|
lda unitDisplayFlagsTable,y ; 4AF5 its local flag copy
|
|
and #$BF ; 4AF8 clear bit 6 so it no longer counts as a group member for the display
|
|
sta unitDisplayFlagsTable,y ; 4AFA store it back for immediate feedback
|
|
lda #$89 ; 4AFD command $89 = leave group (2 bytes: unit)
|
|
sty cmdParam0 ; 4AFF command argument = unit index
|
|
jmp queueCmd2 ; 4B01 queue it and return from queueCmd2
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isPointWithinGroupRange - Range test that stops a group from spreading over more than 8 cells. It
|
|
; builds the group's bounding box with computeGroupBounds and then demands that the tested point be
|
|
; less than 9 cells from each of the four edges of that box (which also bounds the box once the point
|
|
; has been added). Y below $80 tests unit Y's map position against the members' positions - used when
|
|
; a unit joins; Y = $80 tests the cell picked with the target cursor against the members' destinations
|
|
; - used when a member is re-positioned. An empty group always passes.
|
|
; In: A = group key, Y = unit index or $80; pickedCellCol/pickedCellRow ($91D8/$91D9) when Y = $80
|
|
; Out: C=0 within range, C=1 too far; scratch18-scratch1D clobbered (bounding box and mode);
|
|
; self-modifies the LDY operand at $4B0F
|
|
; Called from: addSelectedUnitToGroup ($4AA9) and orderGroupNewPosition ($4BEE)
|
|
; ----------------------------------------------------------------------
|
|
isPointWithinGroupRange:
|
|
sty L_4B0E+1 ; 4B04 patch the LDY # at $4B0F with the mode: a unit index, or $80 = use the picked cell
|
|
jsr computeGroupBounds ; 4B07 A and Y are still the arguments: build the group's bounding box (positions or destinations)
|
|
lda scratch1A ; 4B0A number of members found
|
|
beq L_4B5A ; 4B0C an empty group has no box - accept anything
|
|
L_4B0E:
|
|
ldy #$FF ; 4B0E operand patched at $4B04
|
|
bpl L_4B1B ; 4B10 index below $80: measure the unit's own cell
|
|
ldx pickedCellCol ; 4B12 $80: measure the cell picked with the target cursor
|
|
ldy pickedCellRow ; 4B15 its row
|
|
jmp L_4B22 ; 4B18 on to the four edge tests
|
|
|
|
L_4B1B:
|
|
ldx unitColTable,y ; 4B1B X = column of the unit being tested
|
|
lda unitRowTable,y ; 4B1E its row
|
|
tay ; 4B21 Y = row of the unit being tested
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Four edge tests, each |point - edge| < 9: right edge, left edge, bottom edge, top edge of the
|
|
; bounding box. Any distance of 9 or more fails the whole test.
|
|
; ----------------------------------------------------------------------
|
|
L_4B22:
|
|
txa ; 4B22 column of the tested point
|
|
sec ; 4B23 borrow clear
|
|
sbc scratch1B ; 4B24 distance to the rightmost member
|
|
bcs L_4B2C ; 4B26 already positive
|
|
eor #$FF ; 4B28 otherwise negate to get the absolute value
|
|
adc #$01 ; 4B2A (two's complement: EOR $FF then +1)
|
|
L_4B2C:
|
|
cmp #$09 ; 4B2C a group may never span more than 8 cells
|
|
bcs L_4B5C ; 4B2E 9 or more away from the right edge: too far
|
|
txa ; 4B30 column of the tested point again
|
|
sec ; 4B31 borrow clear
|
|
sbc scratch18 ; 4B32 distance to the leftmost member
|
|
bcs L_4B3A ; 4B34 already positive
|
|
eor #$FF ; 4B36 negate
|
|
adc #$01 ; 4B38 +1 completes the two's complement
|
|
L_4B3A:
|
|
cmp #$09 ; 4B3A same 8-cell limit
|
|
bcs L_4B5C ; 4B3C too far to the right of the left edge
|
|
tya ; 4B3E row of the tested point
|
|
sec ; 4B3F borrow clear
|
|
sbc scratch1C ; 4B40 distance to the bottom-most member
|
|
bcs L_4B48 ; 4B42 already positive
|
|
eor #$FF ; 4B44 negate
|
|
adc #$01 ; 4B46 +1
|
|
L_4B48:
|
|
cmp #$09 ; 4B48 8-cell limit
|
|
bcs L_4B5C ; 4B4A too far below the bottom edge
|
|
tya ; 4B4C row of the tested point again
|
|
sec ; 4B4D borrow clear
|
|
sbc scratch19 ; 4B4E distance to the top-most member
|
|
bcs L_4B56 ; 4B50 already positive
|
|
eor #$FF ; 4B52 negate
|
|
adc #$01 ; 4B54 +1
|
|
L_4B56:
|
|
cmp #$09 ; 4B56 8-cell limit
|
|
bcs L_4B5C ; 4B58 too far above the top edge
|
|
L_4B5A:
|
|
clc ; 4B5A C=0: the point is within 8 cells of all four edges
|
|
rts ; 4B5B accepted
|
|
|
|
L_4B5C:
|
|
sec ; 4B5C C=1: too far from the group
|
|
rts ; 4B5D rejected
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computeGroupBounds - Scans all 100 unit slots and accumulates the member count and the bounding box
|
|
; of one group. Membership is tested on the local flag copy unitDisplayFlagsTable ((flags & $7C) ==
|
|
; key | $40) and dead slots are skipped. Bit 7 of the mode byte (the Y register on entry) selects
|
|
; whether the box is built from the members' current map cells or from their ordered destinations. An
|
|
; empty group leaves min = $FF and max = 0.
|
|
; In: A = group key (group number and side), Y = mode (bit 7 set = use destinations); unitTypeTable,
|
|
; unitDisplayFlagsTable, unitColTable/unitRowTable or unitDestColTable/unitDestRowTable
|
|
; Out: A = scratch1A = member count, X = scratch18 = min column, Y = scratch19 = min row, scratch1B =
|
|
; max column, scratch1C = max row, scratch1D = the mode byte; self-modifies the CMP operand at
|
|
; $4B80
|
|
; Called from: isPointWithinGroupRange ($4B07) and buildGroupFormationSprite ($30C5), which draws the
|
|
; group's formation ghost from the destination box
|
|
; ----------------------------------------------------------------------
|
|
computeGroupBounds:
|
|
ora #$40 ; 4B5E add the in-a-group bit to the group key
|
|
sta L_4B7F+1 ; 4B60 patch the CMP # at $4B80 with the key every member must match
|
|
sty scratch1D ; 4B63 remember the mode: bit 7 set = measure destinations, clear = measure positions
|
|
lda #$00 ; 4B65 zero
|
|
sta scratch1A ; 4B67 member count = 0
|
|
sta scratch1B ; 4B69 max column = 0 (only a member can raise it)
|
|
sta scratch1C ; 4B6B max row = 0
|
|
lda #$FF ; 4B6D $FF
|
|
sta scratch18 ; 4B6F min column = $FF so the first member always lowers it
|
|
sta scratch19 ; 4B71 min row = $FF
|
|
ldx #$63 ; 4B73 scan unit slots 99 down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass over the 100 unit slots: count the members of the group and grow the bounding box around
|
|
; either their positions or their destinations.
|
|
; ----------------------------------------------------------------------
|
|
L_4B75:
|
|
lda unitTypeTable,x ; 4B75 type byte of this slot
|
|
bmi L_4BB5 ; 4B78 bit 7 = dead or empty slot, skip it
|
|
lda unitDisplayFlagsTable,x ; 4B7A the UI copy of the flags - this is what the membership editor manipulates
|
|
and #$7C ; 4B7D group number, side and the in-a-group bit
|
|
L_4B7F:
|
|
cmp #$FF ; 4B7F operand patched at $4B60: is this unit a member of the group?
|
|
bne L_4BB5 ; 4B81 no: next slot
|
|
inc scratch1A ; 4B83 count the member
|
|
lda unitColTable,x ; 4B85 its map column
|
|
bit scratch1D ; 4B88 test bit 7 of the mode byte
|
|
bpl L_4B91 ; 4B8A positions requested: keep the map column
|
|
lda unitDestColTable,x ; 4B8C destinations requested: use the ordered column instead
|
|
and #$7F ; 4B8F without the at-destination marker in bit 7
|
|
L_4B91:
|
|
cmp scratch1B ; 4B91 beyond the rightmost column so far?
|
|
bcc L_4B97 ; 4B93 no
|
|
sta scratch1B ; 4B95 grow the box to the right
|
|
L_4B97:
|
|
cmp scratch18 ; 4B97 left of the leftmost column so far?
|
|
bcs L_4B9D ; 4B99 no
|
|
sta scratch18 ; 4B9B grow the box to the left
|
|
L_4B9D:
|
|
lda unitRowTable,x ; 4B9D its map row
|
|
bit scratch1D ; 4BA0 test the mode byte again
|
|
bpl L_4BA9 ; 4BA2 positions requested
|
|
lda unitDestRowTable,x ; 4BA4 destinations requested: use the ordered row
|
|
and #$7F ; 4BA7 without the at-destination marker
|
|
L_4BA9:
|
|
cmp scratch1C ; 4BA9 below the bottom-most row so far?
|
|
bcc L_4BAF ; 4BAB no
|
|
sta scratch1C ; 4BAD grow the box downwards
|
|
L_4BAF:
|
|
cmp scratch19 ; 4BAF above the top-most row so far?
|
|
bcs L_4BB5 ; 4BB1 no
|
|
sta scratch19 ; 4BB3 grow the box upwards
|
|
L_4BB5:
|
|
dex ; 4BB5 next lower slot
|
|
bpl L_4B75 ; 4BB6 loop until slot 0 has been examined
|
|
lda scratch1A ; 4BB8 return the member count in A (Z=1 for an empty group)
|
|
ldx scratch18 ; 4BBA X = minimum column of the box
|
|
ldy scratch19 ; 4BBC Y = minimum row of the box
|
|
rts ; 4BBE the maxima stay in scratch1B/scratch1C
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showTooFarMessage - Error feedback shared by the two group range checks: selects and clears the
|
|
; 14-column info panel (screen columns 25-38, rows 12-21), plays sound 3 (the rejection beep) and
|
|
; prints 'TOO FAR!' from the $C000 text overlay on screen row 16, in whatever colour the caller left
|
|
; in textColour.
|
|
; In: textColour (zp_3D) and textFillGlyph (zp_3E) as set by the caller
|
|
; Out: info panel window selected and blanked, sound 3 queued, the message printed (tail call into
|
|
; printString)
|
|
; Called from: addSelectedUnitToGroup ($4AAE) and orderGroupNewPosition ($4BF3)
|
|
; ----------------------------------------------------------------------
|
|
showTooFarMessage:
|
|
jsr clearInfoPanel ; 4BBF select the info panel window (columns 25-38, rows 12-21) and blank it
|
|
lda #$03 ; 4BC2 sound 3 = the short rejection beep
|
|
jsr playSound ; 4BC4 queue it with the sound driver
|
|
lda #$10 ; 4BC7 screen row 16, in the middle of the info panel
|
|
jsr setCursorRow ; 4BC9 move the text cursor there
|
|
lda #$31 ; 4BCC low byte of the string address
|
|
ldy #$CB ; 4BCE $CB31 = ' TOO FAR!' in the text overlay
|
|
jmp printString ; 4BD0 print it and return from printString
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; orderGroupNewPosition - NEW POSITION action of the custom-formation editor. It highlights just the
|
|
; selected unit, redraws the viewport and runs the target cursor loop, which in this mode
|
|
; (cursorTargetMode negative) only reports the picked cell in pickedCellCol/pickedCellRow. If the pick
|
|
; was not aborted and lies within 8 cells of the group's destination box it queues command $98
|
|
; unit,column,row, refreshes the local at-destination flag, counts the order in the pending-order pair
|
|
; $929D/$929E and prints 'RE-POSITIONED'; otherwise it shows TOO FAR! and re-centres the viewport on
|
|
; the unit. It always ends by restoring the editor's cursor state, because the cursor loop zeroes
|
|
; cursorTargetMode when the pick is aborted.
|
|
; In: selectedUnit ($90F8), selectedGroupKey ($9206), pickedCellCol/pickedCellRow ($91D8/$91D9)
|
|
; filled by runTargetCursorLoop, checkUiContinue ($C451)
|
|
; Out: command $98 queued (queueCmd4) or a message shown; highlightedUnit/highlightedGroupKey,
|
|
; lastOrderedGroupKey/pendingGroupOrderCount, cursorTargetMode = $80, textColour/textPadColour =
|
|
; 4; self-modifies the string pointer operands at $4C46/$4C48
|
|
; Called from: the group membership/formation editor ($64AB)
|
|
; ----------------------------------------------------------------------
|
|
orderGroupNewPosition:
|
|
lda #$FF ; 4BD3 $FF = no group highlighted ...
|
|
sta highlightedGroupKey ; 4BD5 ... so only the single selected unit is drawn highlighted
|
|
lda selectedUnit ; 4BD8 the unit being re-positioned
|
|
sta highlightedUnit ; 4BDB highlight it in the viewport
|
|
jsr drawViewport ; 4BDE redraw the 7x5 tactical view with the new highlight
|
|
jsr runTargetCursorLoop ; 4BE1 run the target cursor; cursorTargetMode is negative here, so it only reports the picked cell in $91D8/$91D9 and returns
|
|
jsr checkUiContinue ; 4BE4 may the modal UI continue? (0 = a screen change, SPACE or the end of the game aborted it)
|
|
beq L_4BF9 ; 4BE7 aborted: skip the order and just restore the cursor
|
|
ldy #$80 ; 4BE9 $80 = compare the picked cell with the members' destinations, not their positions
|
|
lda selectedGroupKey ; 4BEB the group being edited
|
|
jsr isPointWithinGroupRange ; 4BEE is the picked cell within 8 cells of the group's destination box?
|
|
bcc L_4BFC ; 4BF1 C=0: accepted
|
|
jsr showTooFarMessage ; 4BF3 C=1: beep and print 'TOO FAR!'
|
|
jsr recenterViewportOnUnit ; 4BF6 scroll the viewport back so the unit is under the cursor again
|
|
L_4BF9:
|
|
jmp L_4C4F ; 4BF9 join the common tail
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The pick was accepted: send command $98 (move keeping group membership) for this unit.
|
|
; ----------------------------------------------------------------------
|
|
L_4BFC:
|
|
lda pickedCellCol ; 4BFC column picked with the cursor
|
|
sta cmdParam1 ; 4BFF command argument 2 = column
|
|
lda pickedCellRow ; 4C01 row picked with the cursor
|
|
sta cmdParam2 ; 4C04 command argument 3 = row
|
|
ldy selectedUnit ; 4C06 the unit being re-positioned
|
|
sty cmdParam0 ; 4C09 command argument 1 = unit index
|
|
lda #$98 ; 4C0B command $98 = set destination without leaving the group (4 bytes)
|
|
jsr queueCmd4 ; 4C0D queue it for both machines
|
|
jsr updateUnitAtDestinationFlag; 4C10 local feedback: refresh bit 7 of unitDisplayFlags[unit] (at destination or not)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Count how many re-position orders are outstanding for this group. The editor at $65E7 waits for
|
|
; pendingGroupOrderCount to come back to zero (each executed $98 decrements it in
|
|
; setGroupMemberDestination) before it lets the whole group be given a new goal.
|
|
; ----------------------------------------------------------------------
|
|
ldy cmdParam0 ; 4C13 the unit just ordered
|
|
lda unitDisplayFlagsTable,y ; 4C15 its local flag copy
|
|
and #$3C ; 4C18 group number and side, without the member bit
|
|
cmp lastOrderedGroupKey ; 4C1A same group as the previous NEW POSITION order?
|
|
bne L_4C25 ; 4C1D no: start a new count
|
|
inc pendingGroupOrderCount ; 4C1F yes: one more order outstanding for this group
|
|
jmp L_4C2D ; 4C22 on to the message
|
|
|
|
L_4C25:
|
|
sta lastOrderedGroupKey ; 4C25 remember the group these orders belong to
|
|
lda #$01 ; 4C28 this is the first outstanding order ...
|
|
sta pendingGroupOrderCount ; 4C2A ... for the new group
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Confirm the order with a centred 'RE-POSITIONED' in the info panel.
|
|
; ----------------------------------------------------------------------
|
|
L_4C2D:
|
|
lda #$99 ; 4C2D low byte of $CB99 = 'RE-POSITIONED'
|
|
ldy #$CB ; 4C2F high byte
|
|
sta L_4C45+1 ; 4C31 patch the LDA # operand at $4C46 with the string pointer low byte
|
|
sty L_4C47+1 ; 4C34 patch the LDY # operand at $4C48 with the high byte
|
|
lda #$04 ; 4C37 colour 4 (purple) for the confirmation
|
|
sta textColour ; 4C39 text colour
|
|
sta textPadColour ; 4C3B and the colour of the blanks that pad the centred line
|
|
jsr clearInfoPanel ; 4C3D blank the info panel
|
|
lda #$10 ; 4C40 screen row 16
|
|
jsr setCursorRow ; 4C42 move the cursor there
|
|
L_4C45:
|
|
lda #$FF ; 4C45 operand patched at $4C31
|
|
L_4C47:
|
|
ldy #$FF ; 4C47 operand patched at $4C34
|
|
jsr printStringCentred ; 4C49 print the string centred in the 14-column panel
|
|
jsr recenterViewportOnUnit ; 4C4C scroll the viewport back onto the unit
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Common tail: the target cursor loop zeroes cursorTargetMode when the pick was aborted, so the
|
|
; editor's 'free cell pick' mode and its two cursor sprites are set up again here.
|
|
; ----------------------------------------------------------------------
|
|
L_4C4F:
|
|
lda #$80 ; 4C4F $80 = free cell pick mode
|
|
sta cursorTargetMode ; 4C51 restore it for the next round of the editor
|
|
jsr setCursorShapeSquare ; 4C53 sprite 0 = the hollow square cell cursor
|
|
jmp showCursorBoxSprite ; 4C56 sprite 2 = the centre box of the viewport, and return from there
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setGroupMemberDestination - Executor of command $98. If the unit's group key matches the group
|
|
; remembered in lastOrderedGroupKey it decrements pendingGroupOrderCount - this is the acknowledgement
|
|
; the formation editor waits for - and then sets the unit's destination to the ordered cell through
|
|
; sub_34D5. Unlike command $80 it never touches the group bits, so the re-positioned unit stays a
|
|
; member of its group.
|
|
; In: cmdParam0 ($73) = unit index, cmdParam1/cmdParam2 = column/row (already mirrored for a remote
|
|
; command), lastOrderedGroupKey ($929D)
|
|
; Out: unitDestColTable/unitDestRowTable of the unit written via sub_34D5; pendingGroupOrderCount
|
|
; ($929E) decremented when the group matches
|
|
; Called from: cmdMoveKeepGroup ($5476), the handler of command $98
|
|
; ----------------------------------------------------------------------
|
|
setGroupMemberDestination:
|
|
ldy cmdParam0 ; 4C59 the unit named by the command
|
|
lda unitDisplayFlagsTable,y ; 4C5B its local flag copy
|
|
and #$3C ; 4C5E group number and side
|
|
cmp lastOrderedGroupKey ; 4C60 is it the group the local editor is waiting for?
|
|
bne L_4C68 ; 4C63 no: this order was not counted
|
|
dec pendingGroupOrderCount ; 4C65 one of the outstanding re-position orders has now been executed
|
|
L_4C68:
|
|
jmp setUnitDestination ; 4C68 set the destination (clamped, at-destination bit maintained) and return from there; the group bits are left alone
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; syncLocalGroupFlags - Copies the authoritative group bits of unit X (unitFlagsTable & $7C: group
|
|
; number, side and member bit) into the local flag copy unitDisplayFlagsTable, keeping that byte's own
|
|
; bits 0, 1 and 7. Called after every command that changes group membership and when a unit is created
|
|
; or reset, so the UI copy can never drift away from the simulation.
|
|
; In: X = unit index
|
|
; Out: unitDisplayFlags[X] = (old & $83) | (unitFlagsTable[X] & $7C); self-modifies the ORA operand at
|
|
; $4C79
|
|
; Called from: the move executor ($34D0), unit creation ($3DFA), joinUnitToGroup ($4AEF) and
|
|
; cmdLeaveGroup ($51A8)
|
|
; ----------------------------------------------------------------------
|
|
syncLocalGroupFlags:
|
|
lda unitFlagsTable,x ; 4C6B authoritative flag byte of the unit
|
|
and #$7C ; 4C6E group number (bits 2-4), side (bit 5) and the in-a-group bit 6
|
|
sta L_4C78+1 ; 4C70 patch the ORA # operand at $4C79 with those bits
|
|
lda unitDisplayFlagsTable,x ; 4C73 the local copy the display reads
|
|
and #$83 ; 4C76 keep its facing bits 0-1 and the at-destination bit 7
|
|
L_4C78:
|
|
ora #$FF ; 4C78 operand patched at $4C70: the group bits from the simulation
|
|
sta unitDisplayFlagsTable,x ; 4C7A store the merged byte
|
|
rts ; 4C7D back to the caller
|
|
|
|
|
|
; formationRotateRules - two 3-byte rule sets that turn the FORMATION menu item into a coordinate
|
|
; transform for rotateGroupFormation. Index = the rotation mode carried by command $A3 (0 SWEEP LEFT,
|
|
; 1 SWEEP RIGHT, 2 REVERSE). $4C7E[mode] describes what happens to the member's column offset dx,
|
|
; $4C81[mode] what happens to its row offset dy: bit 7 = negate the component, bit 0 = store it as the
|
|
; new row offset ($75) instead of the new column offset ($74). The three modes are therefore (dy,-dx)
|
|
; = 90 degrees anticlockwise, (-dy,dx) = 90 degrees clockwise and (-dx,-dy) = 180 degrees.
|
|
formationRotateRules:
|
|
.byte $81,$01,$80 ; 4C7E ... rule for the column offset: mode 0 negate+store as row, mode 1 store as row, mode 2 negate+store as column
|
|
formationRotateRulesDy:
|
|
.byte $00,$80,$81 ; 4C81 ... rule for the row offset: mode 0 store as column, mode 1 negate+store as column, mode 2 negate+store as row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rotateGroupFormation - Executor of command $A3 - the SWEEP LEFT / SWEEP RIGHT / REVERSE items of the
|
|
; FORMATION menu. It rotates the whole formation about the leader's destination cell: for every live
|
|
; member of the leader's group the offset (dx,dy) from that cell is transformed with the two rule
|
|
; bytes of formationRotateRules and the new destination leader+offset is applied with sub_34D5. During
|
|
; the setup phase the resulting row is clamped into the issuing player's own half of the map, so a
|
|
; rotation can never push units across the middle line.
|
|
; In: cmdParam1 ($74) = leader unit index, cmdParam2 ($75) = rotation mode 0-2 (both from
|
|
; readCmdArgs2), cmdFromOpponent ($76) bit 7, gamePhase (zp_B1)
|
|
; Out: unitDestColTable/unitDestRowTable of every member rewritten through sub_34D5;
|
|
; cmdParam1/cmdParam2 are reused as the working destination; ten self-modified immediates: $4CC4
|
|
; loop index, $4CD0 group key, $4CDA/$4CFE leader column, $4CDC/$4CE5 column rule, $4CEF/$4D05
|
|
; leader row, $4CF1/$4CFA row rule
|
|
; Called from: cmdSetFormation ($5599), the handler of command $A3
|
|
; ----------------------------------------------------------------------
|
|
rotateGroupFormation:
|
|
ldy cmdParam1 ; 4C84 the leader unit named by the command
|
|
lda unitDestColTable,y ; 4C86 leader's destination column - the formation turns about this cell
|
|
and #$7F ; 4C89 drop the at-destination marker in bit 7
|
|
sta L_4CD9+1 ; 4C8B patch the SBC at $4CDA: leader column, used to get each member's dx
|
|
sta L_4CFD+1 ; 4C8E patch the LDA # at $4CFE: same value, used to rebuild the absolute column
|
|
lda unitDestRowTable,y ; 4C91 leader's destination row
|
|
and #$7F ; 4C94 drop the at-destination marker
|
|
sta L_4CEE+1 ; 4C96 patch the SBC at $4CEF: leader row, used to get each member's dy
|
|
sta L_4D04+1 ; 4C99 patch the LDA # at $4D05: same value for the absolute row
|
|
lda unitFlagsTable,y ; 4C9C authoritative flags of the leader
|
|
and #$3C ; 4C9F group number (bits 2-4) and side (bit 5)
|
|
ora #$40 ; 4CA1 add the in-a-group bit so it can be compared with flags & $7C
|
|
sta L_4CCF+1 ; 4CA3 patch the CMP # at $4CD0 with the group key
|
|
ldy cmdParam2 ; 4CA6 rotation mode 0-2 (0 SWEEP LEFT, 1 SWEEP RIGHT, 2 REVERSE)
|
|
lda formationRotateRules,y ; 4CA8 rule byte for the column offset
|
|
sta L_4CDB+1 ; 4CAB patch the LDX # at $4CDC: bit 7 of the rule decides whether dx is negated
|
|
and #$01 ; 4CAE bit 0 of the rule = which parameter byte the transformed value goes to
|
|
sta L_4CE4+1 ; 4CB0 patch the LDX # at $4CE5: 0 = store as column, 1 = store as row
|
|
lda formationRotateRulesDy,y; 4CB3 rule byte for the row offset
|
|
sta L_4CF0+1 ; 4CB6 patch the LDX # at $4CF1: bit 7 = negate dy
|
|
and #$01 ; 4CB9 bit 0 of that rule
|
|
sta L_4CF9+1 ; 4CBB patch the LDX # at $4CFA: destination parameter for the transformed dy
|
|
ldy #$63 ; 4CBE start at unit slot 99
|
|
sty L_4CC3+1 ; 4CC0 the loop index lives in the LDY # operand at $4CC4 (Y is needed by sub_34D5)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk all 100 unit slots. For every live member of the leader's group: take its offset from the
|
|
; leader's destination, run the offset through the rotation rules, add it back to the leader's
|
|
; destination and order the member to the resulting cell.
|
|
; ----------------------------------------------------------------------
|
|
L_4CC3:
|
|
ldy #$FF ; 4CC3 operand patched at $4CC0 and decremented at $4D24: the unit being considered
|
|
lda unitTypeTable,y ; 4CC5 its type byte
|
|
bmi L_4D24 ; 4CC8 bit 7 = dead or empty slot, skip it
|
|
lda unitFlagsTable,y ; 4CCA authoritative flags
|
|
and #$7C ; 4CCD group number, side and member bit
|
|
L_4CCF:
|
|
cmp #$FF ; 4CCF operand patched at $4CA3: a member of the leader's group?
|
|
bne L_4D24 ; 4CD1 no: next slot
|
|
lda unitDestColTable,y ; 4CD3 member's destination column
|
|
and #$7F ; 4CD6 without the at-destination marker
|
|
sec ; 4CD8 borrow clear
|
|
L_4CD9:
|
|
sbc #$FF ; 4CD9 operand patched at $4C8B: dx = member column - leader column
|
|
L_4CDB:
|
|
ldx #$FF ; 4CDB operand patched at $4CAB: only the N flag of the rule byte matters here
|
|
bpl L_4CE4 ; 4CDD rule bit 7 clear: keep dx as it is
|
|
eor #$FF ; 4CDF rule bit 7 set: negate dx ...
|
|
clc ; 4CE1 ... carry clear ...
|
|
adc #$01 ; 4CE2 ... +1 completes the two's complement
|
|
L_4CE4:
|
|
ldx #$FF ; 4CE4 operand patched at $4CB0: 0 = the transformed dx becomes the new column offset, 1 = the new row offset
|
|
sta cmdParam1,x ; 4CE6 store it into cmdParam1 ($74) or cmdParam2 ($75)
|
|
lda unitDestRowTable,y ; 4CE8 member's destination row
|
|
and #$7F ; 4CEB without the at-destination marker
|
|
sec ; 4CED borrow clear
|
|
L_4CEE:
|
|
sbc #$FF ; 4CEE operand patched at $4C96: dy = member row - leader row
|
|
L_4CF0:
|
|
ldx #$FF ; 4CF0 operand patched at $4CB6: N flag of the row rule
|
|
bpl L_4CF9 ; 4CF2 rule bit 7 clear: keep dy
|
|
eor #$FF ; 4CF4 negate dy ...
|
|
clc ; 4CF6 ... carry clear ...
|
|
adc #$01 ; 4CF7 ... +1
|
|
L_4CF9:
|
|
ldx #$FF ; 4CF9 operand patched at $4CBB: which parameter byte receives the transformed dy
|
|
sta cmdParam1,x ; 4CFB store it into cmdParam1 or cmdParam2
|
|
L_4CFD:
|
|
lda #$FF ; 4CFD operand patched at $4C8E: leader's destination column
|
|
clc ; 4CFF plain add
|
|
adc cmdParam1 ; 4D00 absolute column = leader column + transformed column offset
|
|
sta cmdParam1 ; 4D02 cmdParam1 = the member's new destination column
|
|
L_4D04:
|
|
lda #$FF ; 4D04 operand patched at $4C99: leader's destination row
|
|
clc ; 4D06 plain add
|
|
adc cmdParam2 ; 4D07 absolute row = leader row + transformed row offset
|
|
|
|
; ----------------------------------------------------------------------
|
|
; During the setup phase each side may only place units in its own half of the map, so a rotation that
|
|
; would carry a unit across the middle line (row 20) is clamped. The local player always owns rows
|
|
; 20-39 of his own view, the opponent rows 0-19.
|
|
; ----------------------------------------------------------------------
|
|
ldx gamePhase ; 4D09 0 = the battle is running, non-zero = setup/menu phase
|
|
beq L_4D1F ; 4D0B in battle a rotation may use the whole map
|
|
ldx cmdFromOpponent ; 4D0D did this command come from the opponent?
|
|
bpl L_4D19 ; 4D0F no: it is our own order
|
|
cmp #$14 ; 4D11 remote order: rows must stay in the top half (0-19)
|
|
bcc L_4D1F ; 4D13 already there
|
|
lda #$13 ; 4D15 clamp to row 19, the last row of the opponent's half
|
|
bne L_4D1F ; 4D17 always taken
|
|
L_4D19:
|
|
cmp #$14 ; 4D19 own order: rows must stay in the bottom half (20-39)
|
|
bcs L_4D1F ; 4D1B already there
|
|
lda #$14 ; 4D1D clamp to row 20, the first row of our own half
|
|
L_4D1F:
|
|
sta cmdParam2 ; 4D1F cmdParam2 = the member's new destination row
|
|
jsr setUnitDestination ; 4D21 order unit Y to that cell (clamped to the map, at-destination bit maintained)
|
|
L_4D24:
|
|
dec L_4CC3+1 ; 4D24 next lower unit slot, straight in the LDY operand
|
|
bpl L_4CC3 ; 4D27 loop until slot 0 has been handled
|
|
rts ; 4D29 back to the command dispatcher
|
|
|
|
|
|
; commandHandlerTable - 39 little-endian handler addresses, one per command byte $80-$A6. The
|
|
; dispatcher in executeTurnCommands indexes it with (command & $3F) * 2 and copies the entry into the
|
|
; operand of the JSR at $4EEE. The $8D entry is $FFFF (rendered as irqVectorHi) because that command
|
|
; is consumed by the packet mergers and never reaches the dispatcher. Handlers marked 'immediate'
|
|
; below are likewise executed while the packets are merged.
|
|
commandHandlerTable:
|
|
D_4D2B = commandHandlerTable+1
|
|
.addr cmdMoveUnit ; 4D2A $80 MOVE unit (4 bytes: unit, column, row)
|
|
.byte $1A ; 4D2C $81 move group keeping formation (4 bytes)
|
|
.byte $50 ; 4D2D P
|
|
.byte $23 ; 4D2E $82 cloak on, single unit (2 bytes)
|
|
.byte $50 ; 4D2F P
|
|
.byte $98 ; 4D30 $83 cloak on, whole group (2 bytes)
|
|
.byte $50 ; 4D31 P
|
|
.byte $4B ; 4D32 $84 cloak off, single unit (2 bytes)
|
|
.byte $50 ; 4D33 P
|
|
.byte $D2 ; 4D34 $85 cloak off, whole group (2 bytes)
|
|
.byte $50 ; 4D35 P
|
|
.byte $5D ; 4D36 $86 clear a boomer's target (2 bytes)
|
|
.byte $51 ; 4D37 Q
|
|
.byte $6B ; 4D38 $87 set a boomer's target (3 bytes)
|
|
.byte $51 ; 4D39 Q
|
|
.byte $85 ; 4D3A $88 join group (3 bytes: unit, group key)
|
|
.byte $51 ; 4D3B Q
|
|
.byte $9B ; 4D3C $89 leave group (2 bytes: unit)
|
|
.byte $51 ; 4D3D Q
|
|
.byte $AB ; 4D3E $8A place unit at a cell (4 bytes) - the setup phase
|
|
.byte $51 ; 4D3F Q
|
|
.byte $EE ; 4D40 $8B set the map seed (4 bytes: 3 seed bytes)
|
|
.byte $51 ; 4D41 Q
|
|
.byte $3E ; 4D42 $8C session control: abort, pause, resume, disconnect (2 bytes: reason)
|
|
.byte $52 ; 4D43 R
|
|
.byte $FF ; 4D44 $8D drone position report - never dispatched: the mergers run it at once, so the slot holds $FFFF
|
|
.byte $FF ; 4D45 .
|
|
.byte $36 ; 4D46 $8E detonate the drone (3 bytes: column, row)
|
|
.byte $53 ; 4D47 S
|
|
.byte $7B ; 4D48 $8F release drone control (1 byte)
|
|
.byte $53 ; 4D49 S
|
|
.byte $C0 ; 4D4A $90 comcen missile strike (3 bytes: column, row)
|
|
.byte $53 ; 4D4B S
|
|
.byte $2E ; 4D4C $91 self destruct, unit or its whole group (2 bytes)
|
|
.byte $54 ; 4D4D T
|
|
.byte $8B ; 4D4E $92 the drone reached its target (1 byte)
|
|
.byte $53 ; 4D4F S
|
|
.byte $70 ; 4D50 $93 blitz on, single unit (2 bytes)
|
|
.byte $50 ; 4D51 P
|
|
.byte $AA ; 4D52 $94 blitz on, whole group (2 bytes)
|
|
.byte $50 ; 4D53 P
|
|
.byte $62 ; 4D54 $95 blitz off, single unit (2 bytes)
|
|
.byte $50 ; 4D55 P
|
|
.byte $BE ; 4D56 $96 blitz off, whole group (2 bytes)
|
|
.byte $50 ; 4D57 P
|
|
.byte $A3 ; 4D58 $97 drone launched (1 byte)
|
|
.byte $53 ; 4D59 S
|
|
.byte $70 ; 4D5A $98 move but keep group membership (4 bytes)
|
|
.byte $54 ; 4D5B T
|
|
.byte $B1 ; 4D5C $99 menu-level link poll (1 byte) - the handler is the link keep-alive itself
|
|
.byte $56 ; 4D5D V
|
|
.byte $79 ; 4D5E $9A add energy to a unit (3 bytes: unit, amount)
|
|
.byte $54 ; 4D5F T
|
|
.byte $9E ; 4D60 $9B halt every member of a group (2 bytes)
|
|
.byte $54 ; 4D61 T
|
|
.byte $E1 ; 4D62 $9C set the recycler mode and cell (4 bytes)
|
|
.byte $54 ; 4D63 T
|
|
.byte $11 ; 4D64 $9D choose a side (2 bytes)
|
|
.byte $55 ; 4D65 U
|
|
.byte $0C ; 4D66 $9E comcen repaired / setup finished (1 byte)
|
|
.byte $52 ; 4D67 R
|
|
.byte $48 ; 4D68 $9F dig in, single unit (2 bytes)
|
|
.byte $55 ; 4D69 U
|
|
.byte $4C ; 4D6A $A0 dig out, single unit (2 bytes)
|
|
.byte $55 ; 4D6B U
|
|
.byte $6D ; 4D6C $A1 dig in, whole group (2 bytes)
|
|
.byte $55 ; 4D6D U
|
|
.byte $71 ; 4D6E $A2 dig out, whole group (2 bytes)
|
|
.byte $55 ; 4D6F U
|
|
.byte $96 ; 4D70 $A3 rotate the formation (3 bytes: leader, mode 0-2)
|
|
.byte $55 ; 4D71 U
|
|
.byte $9C ; 4D72 $A4 set the game type / option byte (2 bytes)
|
|
.byte $55 ; 4D73 U
|
|
.byte $A5 ; 4D74 $A5 set one setup option (3 bytes: kind, value)
|
|
.byte $55 ; 4D75 U
|
|
.byte $EA ; 4D76 $A6 set a custom map seed (4 bytes) - $51EA, rendered as raw bytes because the table was typed one entry short
|
|
.byte $51 ; 4D77 Q
|
|
|
|
; commandLengthTable - total length in bytes (the command byte included) of each command
|
|
; $80-$A6, indexed by command & $3F. getCommandLength is the only reader; the packet mergers,
|
|
; the dispatcher, the outgoing packet builder and the comm module all use it to step over a command
|
|
; they do not want to look inside.
|
|
commandLengthTable:
|
|
.byte $04,$04,$02,$02,$02,$02,$02,$03; 4D78 ........ $80-$87: move 4, group move 4, cloak on/off and group cloak on/off 2 each, clear target 2, set target 3
|
|
.byte $03 ; 4D80 $88-$8F: join group 3, leave group 2, place unit 4, map seed 4, session control 2, drone report 4, detonate 3, release drone 1
|
|
.byte $02,$04,$04,$02,$04,$03,$01; 4D81 .......
|
|
.byte $03 ; 4D88 $90-$97: missile 3, self destruct 2, drone arrived 1, blitz on 2, group blitz on 2, blitz off 2, group blitz off 2, drone launched 1
|
|
.byte $02,$01,$02,$02,$02,$02,$01; 4D89 .......
|
|
.byte $04 ; 4D90 $98-$9F: move keeping group 4, menu poll 1, add energy 3, halt group 2, recycler 4, choose side 2, setup ready 1, dig in 2
|
|
.byte $01,$03,$02,$04,$02,$01,$02; 4D91 .......
|
|
.byte $02 ; 4D98 $A0-$A6: dig out 2, group dig in 2, group dig out 2, formation 3, game type 2, setup option 3, custom map seed 4
|
|
.byte $02,$02,$03,$02,$03,$04 ; 4D99 ......
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getCommandLength - Looks the total length (command byte plus its parameters) of command A up in
|
|
; commandLengthTable and leaves it in currentCommandLength as well as in A.
|
|
; In: A = command byte $80-$A6 (bit 7 is ignored)
|
|
; Out: A = currentCommandLength ($92EE) = 1..4, X = the table index (command & $3F)
|
|
; Called from: the dispatcher ($4ECF), advancePacketCursor ($4F5F), the outgoing packet builder
|
|
; ($565C) and the solo trainer ($EA4C, $EA92)
|
|
; ----------------------------------------------------------------------
|
|
getCommandLength:
|
|
and #$3F ; 4D9F strip bit 7: commands $80-$A6 become indices $00-$26
|
|
tax ; 4DA1 index the length table with it
|
|
lda commandLengthTable,x ; 4DA2 1, 2, 3 or 4 bytes
|
|
sta currentCommandLength ; 4DA5 the mergers and the dispatcher use this to skip to the next command
|
|
rts ; 4DA8 length also returned in A
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; serviceCommandExchange - Per-frame entry point of the lock-step command exchange, called from the
|
|
; main loop of every screen and from the overlays. It services the status-message system and then,
|
|
; unless the 10-frame throttle timer linkPollDelay is still running, falls into
|
|
; processCommandExchange. During film playback the throttle is ignored so records are consumed as fast
|
|
; as their time stamps allow.
|
|
; In: filmPlaybackMode ($0B9B) bit 7, linkPollDelay ($9163) counted down by the raster IRQ
|
|
; Out: as processCommandExchange, plus the status line refreshed
|
|
; Called from: the map/battlefield/console loops ($1AB0, $21B2, $600D), waitForExchangeIdle ($5915),
|
|
; the map generator overlay and the text engine ($C76E)
|
|
; ----------------------------------------------------------------------
|
|
serviceCommandExchange:
|
|
jsr updateStatusMessage ; 4DA9 let the status line show/expire the current message
|
|
bit filmPlaybackMode ; 4DAC is a game film being played back?
|
|
bmi processCommandExchange ; 4DAF yes: never throttle, the film's own time stamps do the pacing
|
|
ldy linkPollDelay ; 4DB1 frames still to wait before the next packet exchange
|
|
beq processCommandExchange ; 4DB4 timer expired: try to exchange
|
|
rts ; 4DB6 still throttled, come back next frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; processCommandExchange - The heart of the lock-step protocol. During film playback it reads records
|
|
; from the film buffer whose time stamp (exchange slot commandSliceIndex plus the game clock) has
|
|
; come, copies their command bytes into the merged command buffer and executes them; a record header
|
|
; of $FE stops the film where it is and $FF runs the end-of-film handler. In live play it waits for
|
|
; the comm module to report a completed exchange ($E01D bit 6), then merges the packet just sent and
|
|
; the packet just received into one buffer - always with player 0's commands first, so both machines
|
|
; execute the same sequence - and falls into executeTurnCommands.
|
|
; In: filmPlaybackMode ($0B9B), linkSessionState ($0B9C), gameEndReason ($0BA8), playerSide ($0B9F),
|
|
; the comm module's $E01D/$E03B, film pointer zp_BC/zp_BD, commandSliceIndex ($923C), gameClock
|
|
; ($91CB)
|
|
; Out: mergedCmdBuffer filled and executed;
|
|
; mergedCmdLength/mergedCmdSplitIndex/linkPollDelay/linkTimeoutCounter/mergeFillIndex set;
|
|
; self-modifies $4DE9 (a copy of the time-stamp byte), $4DF4 (the previous record's clock) and
|
|
; $4E20 (offset of the first command byte in the record)
|
|
; Called from: serviceCommandExchange (fall-through and branches) and updateGameFrame ($41EE), which
|
|
; calls it once every ten units of the round
|
|
; ----------------------------------------------------------------------
|
|
processCommandExchange:
|
|
lda filmPlaybackMode ; 4DB7 bit 7 = a game film is being replayed
|
|
bpl L_4E39 ; 4DBA clear: this is a live game, use the link
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Film playback. Records are consumed from the film buffer until one is found whose time stamp is
|
|
; still in the future; several records may share one time stamp, so this is a loop.
|
|
; ----------------------------------------------------------------------
|
|
L_4DBC:
|
|
lda gameEndReason ; 4DBC 0 while the game runs
|
|
bne L_4DC9 ; 4DBF the game has ended: stop feeding records, only keep the link alive
|
|
jsr compareFilmPtrToEnd ; 4DC1 has the film pointer reached the recorded end?
|
|
bcc L_4DD2 ; 4DC4 no: there is another record to look at
|
|
L_4DC6:
|
|
jsr finishFilmPlayback ; 4DC6 end of the buffer: restore the final scores and clock and start the end sequence
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Nothing to execute this frame. If a link session exists (a film can be watched while the modem link
|
|
; is still up) give the menu-level packet handler a turn.
|
|
; ----------------------------------------------------------------------
|
|
L_4DC9:
|
|
lda linkSessionState ; 4DC9 0 = no link, $80 = link up at menu level, 2 = game session
|
|
beq L_4DD1 ; 4DCC no link at all: just return
|
|
jmp pollMenuSession ; 4DCE service the menu-level link (carrier check plus keep-alive) and return from there
|
|
|
|
L_4DD1:
|
|
rts ; 4DD1 nothing to do this frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Read the next film record. Layout: [ splitIndex<<4 | length ] [ 1 or 2 time-stamp bytes ] [ length
|
|
; command bytes ]. The time-stamp byte holds the exchange slot within the round in its low nibble and
|
|
; the negated clock delta in its high nibble; a zero high nibble means the next byte is the absolute
|
|
; game clock. Header $FE = the film ends here, $FF = the recorded game was aborted (a real header can
|
|
; never reach $EF because a merged buffer holds at most 14 bytes).
|
|
; ----------------------------------------------------------------------
|
|
L_4DD2:
|
|
ldy #$00 ; 4DD2 offset 0 of the record
|
|
jsr readFilmByte ; 4DD4 read it from the film buffer in the RAM hidden under the I/O area
|
|
sta mergedCmdLength ; 4DD7 park the header byte in mergedCmdLength for now
|
|
iny ; 4DDA offset 1
|
|
jsr readFilmByte ; 4DDB the time-stamp byte
|
|
sta L_4DE8+1 ; 4DDE patch the LDA # at $4DE9 so the byte can be re-read without touching the film again
|
|
and #$0F ; 4DE1 low nibble = the exchange slot within the round (commandSliceIndex, one exchange per ten units processed)
|
|
cmp commandSliceIndex ; 4DE3 only replay this record when the round has reached the same slot it was recorded in
|
|
bne L_4DC9 ; 4DE6 this record belongs to another slot: not yet
|
|
L_4DE8:
|
|
lda #$FF ; 4DE8 operand patched at $4DDE: the time-stamp byte again
|
|
lsr a ; 4DEA shift the high nibble down ...
|
|
lsr a ; 4DEB ...
|
|
lsr a ; 4DEC ...
|
|
lsr a ; 4DED ... A = the clock delta code
|
|
beq L_4DF8 ; 4DEE 0 = the long form: an absolute clock byte follows
|
|
ora #$F0 ; 4DF0 sign-extend the 4-bit delta to a negative byte
|
|
sec ; 4DF2 the writer subtracted one extra, add it back
|
|
L_4DF3:
|
|
adc #$00 ; 4DF3 operand patched at $4E01: the clock of the previous record, giving this record's clock
|
|
jmp L_4DFC ; 4DF5 join the clock comparison
|
|
|
|
L_4DF8:
|
|
iny ; 4DF8 long form: offset 2
|
|
jsr readFilmByte ; 4DF9 read the absolute game clock of the record
|
|
L_4DFC:
|
|
cmp gameClock ; 4DFC is this record due now?
|
|
bne L_4DC9 ; 4DFF no, it is in the future: come back next frame
|
|
sta L_4DF3+1 ; 4E01 remember this clock as the base for the next record's delta
|
|
iny ; 4E04 step past the time stamp
|
|
sty L_4E1F+1 ; 4E05 patch the LDY # at $4E20: offset of the first command byte in this record
|
|
lda mergedCmdLength ; 4E08 the header byte again
|
|
cmp #$FE ; 4E0B $FE = the recorded game reached its own end
|
|
beq L_4DD1 ; 4E0D freeze here: return every frame so the last picture stays up
|
|
bcs L_4DC6 ; 4E0F $FF = the recorded game was aborted: run the end-of-film handler
|
|
pha ; 4E11 keep the header byte
|
|
and #$0F ; 4E12 low nibble = number of command bytes in the record
|
|
sta mergedCmdLength ; 4E14 that is the length of the merged buffer for this exchange
|
|
pla ; 4E17 header byte back
|
|
lsr a ; 4E18 shift the high nibble down ...
|
|
lsr a ; 4E19 ...
|
|
lsr a ; 4E1A ...
|
|
lsr a ; 4E1B ... A = where player 1's commands start
|
|
sta mergedCmdSplitIndex ; 4E1C the dispatcher flips the owner flag at this index
|
|
L_4E1F:
|
|
ldy #$FF ; 4E1F operand patched at $4E05: first command byte of the record
|
|
ldx #$00 ; 4E21 write into the merged buffer from index 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the record's command bytes out of the film buffer into the merged command buffer.
|
|
; ----------------------------------------------------------------------
|
|
L_4E23:
|
|
jsr readFilmByte ; 4E23 next byte of the record
|
|
sta mergedCmdBuffer,x ; 4E26 into the merged command buffer
|
|
iny ; 4E29 next film offset
|
|
inx ; 4E2A next buffer index
|
|
cpx mergedCmdLength ; 4E2B all command bytes copied?
|
|
bcc L_4E23 ; 4E2E no: keep copying
|
|
jsr advanceFilmPtr ; 4E30 advance the film pointer by the whole record (Y = its length)
|
|
jsr executeTurnCommands ; 4E33 execute the commands exactly as a live exchange would
|
|
jmp L_4DBC ; 4E36 look for another record with the same time stamp
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Live play. Everything below runs once per completed packet exchange with the opponent (or with the
|
|
; solo trainer, which fakes the same exchange).
|
|
; ----------------------------------------------------------------------
|
|
L_4E39:
|
|
lda linkSessionState ; 4E39 link state: 0 none, 2 game session agreed, $80 link up at menu level, $FF lost
|
|
bpl L_4E41 ; 4E3C bit 7 clear: a game session is running, do the full exchange
|
|
jmp pollMenuSession ; 4E3E menu level: only the carrier check and the menu packet handler, then return
|
|
|
|
L_4E41:
|
|
jsr exitIfGameEnded ; 4E41 returns without coming back here if the game is already over
|
|
bne L_4E55 ; 4E44 the end-of-game countdown is running: no more exchanges
|
|
lda isLinkActive ; 4E46 comm module flag: is the link (UART or trainer) running at all? (also trainerAiE000:isLinkActive)
|
|
beq L_4E55 ; 4E49 no link: nothing to exchange
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait for the comm module to report 'this frame's packets have been swapped' (bit 6 of the exchange
|
|
; flag byte $E01D). Normally the routine simply returns and is called again next frame; once the
|
|
; watchdog has been waiting for about half a minute checkLinkTimeout returns C=1 and the game
|
|
; busy-waits here with PHONE TROUBLE on the status line until the packet arrives or the watchdog gives
|
|
; up and returns to the main menu.
|
|
; ----------------------------------------------------------------------
|
|
L_4E4B:
|
|
bit exchangeFlags ; 4E4B $E01D bit 6 = the exchange is complete (also trainerAiE000:commInterfaceBlock)
|
|
bvs L_4E56 ; 4E4E both packets are in: go and merge them
|
|
jsr checkLinkTimeout ; 4E50 arm/advance the link watchdog (it counts in the raster IRQ)
|
|
bcs L_4E4B ; 4E53 C=1 = already overdue: spin here instead of giving the frame back
|
|
L_4E55:
|
|
rts ; 4E55 not ready yet, try again next frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Both packets are available: reset the timers and merge the two command streams.
|
|
; ----------------------------------------------------------------------
|
|
L_4E56:
|
|
lda sndVoiceSoundId ; 4E56 sound currently playing on the voice
|
|
cmp #$15 ; 4E59 $15 = the 'waiting for the opponent' sound
|
|
bne L_4E62 ; 4E5B some other sound: leave it alone
|
|
lda #$00 ; 4E5D sound 0 = silence
|
|
jsr playSound ; 4E5F stop the waiting sound now that the opponent answered
|
|
L_4E62:
|
|
ldy #$0A ; 4E62 10 frames ...
|
|
sty linkPollDelay ; 4E64 ... before the next exchange is attempted
|
|
ldx #$00 ; 4E67 0 also clears bit 7 ...
|
|
stx linkTimeoutCounter ; 4E69 ... which stops the IRQ advancing the link watchdog
|
|
stx mergeFillIndex ; 4E6C the merged command buffer starts out empty
|
|
lda playerSide ; 4E6E which side is this machine playing?
|
|
bne L_4E81 ; 4E71 player 1: the opponent's commands go first
|
|
jsr mergeOutgoingPacket ; 4E73 player 0: our own commands go into the buffer first
|
|
ldx mergeFillIndex ; 4E76 bytes written so far
|
|
stx mergedCmdSplitIndex ; 4E78 = the index at which player 1's commands start
|
|
jsr mergeIncomingPacket ; 4E7B then append the packet received from the opponent
|
|
jmp L_4E8C ; 4E7E on to the dispatcher
|
|
|
|
L_4E81:
|
|
jsr mergeIncomingPacket ; 4E81 player 1: the opponent is player 0, so his commands go first
|
|
ldx mergeFillIndex ; 4E84 bytes written so far
|
|
stx mergedCmdSplitIndex ; 4E86 = the index at which player 1's (our own) commands start
|
|
jsr mergeOutgoingPacket ; 4E89 then append the copy of what we just sent
|
|
L_4E8C:
|
|
ldx mergeFillIndex ; 4E8C total number of bytes in the merged buffer
|
|
stx mergedCmdLength ; 4E8E and fall straight into executeTurnCommands
|
|
|
|
; ----------------------------------------------------------------------
|
|
; executeTurnCommands - Runs every command of one exchange. It walks the merged buffer from index 0 to
|
|
; mergedCmdLength, keeping cmdFromOpponent up to date: the owner flag starts at 'remote' or 'own'
|
|
; depending on which side this machine plays and is toggled when the read index reaches
|
|
; mergedCmdSplitIndex, the boundary between the two players' blocks. Own commands decrement the
|
|
; outstanding-order count. A byte with bit 7 set is dispatched through commandHandlerTable with the
|
|
; self-modified JSR at $4EEE; a byte with bit 7 clear is a cancelled command (a handler may overwrite
|
|
; its own command byte, e.g. $8E becomes $0E at $534F) and is skipped by its table length. In live
|
|
; play the whole exchange is then appended to the game film, the packet is acknowledged and the next
|
|
; outgoing packet is built and sent.
|
|
; In: mergedCmdBuffer ($908F), mergedCmdLength ($92EB), mergedCmdSplitIndex ($92EC), playerSide
|
|
; ($0B9F), filmPlaybackMode ($0B9B)
|
|
; Out: the game state changed by the handlers; mergedCmdReadIndex, mergedCmdOwnerFlag,
|
|
; cmdFromOpponent, exchangeActivityFlag bit 0 and pendingOwnCmdCount updated; in live play the
|
|
; film grows and the next packet is transmitted
|
|
; Called from: processCommandExchange - by fall-through for a live exchange and by JSR at $4E33 for a
|
|
; film record
|
|
; ----------------------------------------------------------------------
|
|
executeTurnCommands:
|
|
lda #$00 ; 4E91 start at the first byte ...
|
|
sta mergedCmdReadIndex ; 4E93 ... of the merged command buffer
|
|
ldy playerSide ; 4E96 which side is this machine?
|
|
beq L_4E9D ; 4E99 player 0: the first block of commands is our own (owner flag 0)
|
|
lda #$80 ; 4E9B player 1: the first block belongs to player 0, i.e. the opponent
|
|
L_4E9D:
|
|
sta mergedCmdOwnerFlag ; 4E9D owner of the block currently being executed
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Command loop. Every command in the buffer is executed on both machines in exactly this order, which
|
|
; is what keeps the two simulations identical.
|
|
; ----------------------------------------------------------------------
|
|
L_4EA0:
|
|
ldy mergedCmdReadIndex ; 4EA0 index of the next command byte
|
|
cpy mergedCmdLength ; 4EA3 past the end of the merged buffer?
|
|
bcs L_4EF4 ; 4EA6 yes: the exchange has been executed
|
|
lda exchangeActivityFlag ; 4EA8 activity flag read by the round timer
|
|
ora #$01 ; 4EAB bit 0 = at least one command was executed this round
|
|
sta exchangeActivityFlag ; 4EAD in the setup phase the clock only ticks while something happens
|
|
cpy mergedCmdSplitIndex ; 4EB0 have we reached the boundary between the two players' commands?
|
|
bne L_4EBD ; 4EB3 no: same owner as the previous command
|
|
lda mergedCmdOwnerFlag ; 4EB5 current owner
|
|
eor #$80 ; 4EB8 flip between own ($00) and opponent ($80)
|
|
sta mergedCmdOwnerFlag ; 4EBA everything from here belongs to the other player
|
|
L_4EBD:
|
|
lda mergedCmdOwnerFlag ; 4EBD owner of this command
|
|
sta cmdFromOpponent ; 4EC0 the handlers test bit 7 of this to mirror coordinates and to suppress local-only side effects
|
|
bmi L_4EC7 ; 4EC2 a command from the opponent: nothing of ours to account for
|
|
dec pendingOwnCmdCount ; 4EC4 one of our own queued orders has now been executed
|
|
L_4EC7:
|
|
ldy mergedCmdReadIndex ; 4EC7 index of the command byte
|
|
lda mergedCmdBuffer,y ; 4ECA the command byte itself
|
|
bmi L_4EDB ; 4ECD bit 7 set = a live command, dispatch it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A byte with bit 7 clear is a command that was cancelled after it was queued (a handler can clear bit
|
|
; 7 of its own byte in the buffer, e.g. cmdDroneDetonate writes $0E over its $8E). Its length is still
|
|
; looked up so the reader can step over the whole command.
|
|
; ----------------------------------------------------------------------
|
|
jsr getCommandLength ; 4ECF look the length up anyway - the low 6 bits still identify the command
|
|
clc ; 4ED2 plain add
|
|
adc mergedCmdReadIndex ; 4ED3 skip the whole command ...
|
|
sta mergedCmdReadIndex ; 4ED6 ... without executing it
|
|
bne L_4EA0 ; 4ED9 always taken (the index can never wrap in a 14-byte buffer)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Dispatch: copy the handler address out of commandHandlerTable into the operand of the JSR below and
|
|
; call it. The handler itself reads its parameters with readCmdArg1/2/3, which also advance
|
|
; mergedCmdReadIndex to the next command.
|
|
; ----------------------------------------------------------------------
|
|
L_4EDB:
|
|
lda mergedCmdBuffer,y ; 4EDB the command byte
|
|
and #$3F ; 4EDE $80-$A6 becomes index $00-$26
|
|
asl a ; 4EE0 two bytes per table entry
|
|
tax ; 4EE1 X = byte offset into commandHandlerTable
|
|
lda commandHandlerTable,x ; 4EE2 low byte of the handler address
|
|
sta L_4EEE+1 ; 4EE5 patch the JSR operand at $4EEF
|
|
lda D_4D2B,x ; 4EE8 high byte of the handler address
|
|
sta L_4EEE+2 ; 4EEB patch the JSR operand at $4EF0
|
|
L_4EEE:
|
|
jsr irqVectorHi ; 4EEE call the handler (the $FFFF shown here is only the unpatched placeholder from the $8D table slot)
|
|
jmp L_4EA0 ; 4EF1 on to the next command
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; End of the exchange. While a film is being replayed there is nothing to record and no packet to
|
|
; answer, so the routine simply returns.
|
|
; ----------------------------------------------------------------------
|
|
L_4EF4:
|
|
lda filmPlaybackMode ; 4EF4 bit 7 = film playback
|
|
bpl L_4EFA ; 4EF7 live game: record and answer
|
|
rts ; 4EF9 film playback: done
|
|
|
|
L_4EFA:
|
|
jsr recordExchangeToFilm ; 4EFA append this exchange (header, time stamp, command bytes) to the film in the RAM under I/O
|
|
ldx #$02 ; 4EFD comm module function 2 = acknowledge/consume the completed exchange
|
|
jsr commJumpTable ; 4EFF clears $E01D/$E01E/$E01F so the next exchange can start (also trainerAiE000:commRequestEntry)
|
|
jmp sendOutgoingPacket ; 4F02 build the next outgoing packet from the command ring and hand it to the comm module
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mergeIncomingPacket - Appends the packet just received from the opponent ($E020, $E01E bytes) to the
|
|
; merged command buffer, marking the block as remote. Commands are copied whole, their length taken
|
|
; from commandLengthTable. Three command bytes are special: $97 (drone launched) also clears the drone
|
|
; flags, $92 (drone arrived) sets them, while $8D (the opponent's drone position report) and $8C
|
|
; (session control: abort, pause, resume) are executed here and now and never enter the buffer; the
|
|
; abort case of $8C even unwinds this loop by dropping two return addresses.
|
|
; In: the comm module's receive buffer $E020-$E026 and its length $E01E, mergeFillIndex (zp_69) =
|
|
; bytes already in the merged buffer
|
|
; Out: mergedCmdBuffer filled from mergeFillIndex on, mergeFillIndex advanced, packetReadIndex (zp_6A)
|
|
; used as the cursor, cmdFromOpponent = $80, droneArrivalFlags possibly changed
|
|
; Called from: processCommandExchange ($4E7B for player 0, $4E81 for player 1)
|
|
; ----------------------------------------------------------------------
|
|
mergeIncomingPacket:
|
|
lda rxPacketLength ; 4F05 number of bytes the opponent sent this exchange (also trainerAiE000:rxPacketLength)
|
|
beq L_4F5D ; 4F08 empty packet: nothing to merge
|
|
lda #$80 ; 4F0A $80 = these commands come from the opponent ...
|
|
sta cmdFromOpponent ; 4F0C ... so cmdDroneMove and cmdSessionControl mirror their coordinates
|
|
ldy #$00 ; 4F0E start at the first byte of the packet
|
|
sty packetReadIndex ; 4F10 zp_6A is the read cursor inside the packet
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the received packet command by command.
|
|
; ----------------------------------------------------------------------
|
|
L_4F12:
|
|
ldy packetReadIndex ; 4F12 offset of the next command in the packet
|
|
lda rxPacketBuffer,y ; 4F14 its command byte (also trainerAiE000:rxPacketBuffer)
|
|
jsr advancePacketCursor ; 4F17 look up its length and point zp_6A at the following command
|
|
cmp #$97 ; 4F1A $97 = the opponent launched his drone
|
|
bne L_4F23 ; 4F1C not $97
|
|
ldx #$00 ; 4F1E clear all drone state ...
|
|
stx droneArrivalFlags ; 4F20 ... a new drone flight starts; the $97 command itself is copied into the buffer as well
|
|
L_4F23:
|
|
cmp #$8D ; 4F23 $8D = the opponent's drone position report
|
|
bne L_4F2E ; 4F25 not $8D
|
|
iny ; 4F27 step to its first parameter byte
|
|
jsr cmdDroneMove ; 4F28 run it immediately (it only moves the drone sprite and the view) and do not copy it
|
|
jmp L_4F56 ; 4F2B on to the next command
|
|
|
|
L_4F2E:
|
|
cmp #$8C ; 4F2E $8C = session control (abort, pause, resume, disconnect)
|
|
bne L_4F3C ; 4F30 not $8C
|
|
iny ; 4F32 step to the reason byte
|
|
lda rxPacketBuffer,y ; 4F33 read it (also trainerAiE000:rxPacketBuffer)
|
|
jsr cmdSessionControl ; 4F36 act on it at once - an abort must work even mid-buffer - and do not copy it
|
|
jmp L_4F56 ; 4F39 on to the next command
|
|
|
|
L_4F3C:
|
|
cmp #$92 ; 4F3C $92 = the opponent's drone reached its target
|
|
bne L_4F45 ; 4F3E not $92
|
|
lda #$80 ; 4F40 remember it in the drone flags ...
|
|
sta droneArrivalFlags ; 4F42 ... the drone screen watches this byte
|
|
L_4F45:
|
|
ldx mergeFillIndex ; 4F45 X = where in the merged buffer this command goes
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the whole command (byte plus parameters) into the merged buffer.
|
|
; ----------------------------------------------------------------------
|
|
L_4F47:
|
|
lda rxPacketBuffer,y ; 4F47 next byte of the command (also trainerAiE000:rxPacketBuffer)
|
|
sta mergedCmdBuffer,x ; 4F4A into the merged command buffer
|
|
inc mergeFillIndex ; 4F4D the buffer grew by one byte
|
|
inx ; 4F4F next buffer position
|
|
iny ; 4F50 next packet position
|
|
dec currentCommandLength ; 4F51 one byte of this command done
|
|
bne L_4F47 ; 4F54 keep going until the whole command has been copied
|
|
L_4F56:
|
|
ldy packetReadIndex ; 4F56 the cursor advancePacketCursor left
|
|
cpy rxPacketLength ; 4F58 reached the end of the packet? (also trainerAiE000:rxPacketLength)
|
|
bcc L_4F12 ; 4F5B no: next command
|
|
L_4F5D:
|
|
rts ; 4F5D the received packet has been merged
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; advancePacketCursor - Helper of both packet mergers: keeps the command byte in A, looks its length
|
|
; up and leaves packetReadIndex pointing at the next command in the packet.
|
|
; In: A = command byte, Y = its offset inside the packet
|
|
; Out: A unchanged, packetReadIndex (zp_6A) = Y + length, currentCommandLength = the length
|
|
; Called from: mergeIncomingPacket ($4F17) and mergeOutgoingPacket ($4F7D)
|
|
; ----------------------------------------------------------------------
|
|
advancePacketCursor:
|
|
pha ; 4F5E save the command byte - getCommandLength destroys A
|
|
jsr getCommandLength ; 4F5F A = its total length, also stored in currentCommandLength
|
|
tya ; 4F62 offset of this command
|
|
clc ; 4F63 plain add
|
|
adc currentCommandLength ; 4F64 plus its length
|
|
sta packetReadIndex ; 4F67 = offset of the next command in the packet
|
|
pla ; 4F69 hand the command byte back
|
|
rts ; 4F6A the caller compares it against the special command bytes
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mergeOutgoingPacket - Appends the copy of the packet this machine just sent ($E027, $E01F bytes) to
|
|
; the merged buffer, marking the block as own, so the local commands are executed at exactly the same
|
|
; point in the sequence as on the opponent's machine. $8D is not copied - it only re-centres the
|
|
; viewport on the drone and closes one outstanding order - and $8C is executed at once, like in the
|
|
; incoming merger.
|
|
; In: the comm module's transmit copy $E027-$E02D and its length $E01F, mergeFillIndex (zp_69)
|
|
; Out: mergedCmdBuffer filled, mergeFillIndex advanced, packetReadIndex used as the cursor,
|
|
; cmdFromOpponent = 0, viewOriginCol/viewOriginRow or pendingOwnCmdCount possibly changed
|
|
; Called from: processCommandExchange ($4E73 for player 0, $4E89 for player 1)
|
|
; ----------------------------------------------------------------------
|
|
mergeOutgoingPacket:
|
|
lda txPacketLength ; 4F6B number of bytes in the packet we just sent (also trainerAiE000:txPacketLength)
|
|
beq L_4FC3 ; 4F6E we sent nothing: nothing to merge
|
|
lda #$00 ; 4F70 0 = these are our own commands ...
|
|
sta cmdFromOpponent ; 4F72 ... so their coordinates are already in our own frame of reference
|
|
ldy #$00 ; 4F74 start at the first byte
|
|
sty packetReadIndex ; 4F76 zp_6A is the read cursor inside the packet
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk our own packet command by command.
|
|
; ----------------------------------------------------------------------
|
|
L_4F78:
|
|
ldy packetReadIndex ; 4F78 offset of the next command
|
|
lda txPacketBuffer,y ; 4F7A its command byte (also trainerAiE000:txPacketBuffer)
|
|
jsr advancePacketCursor ; 4F7D look up its length and point zp_6A at the next command
|
|
cmp #$8D ; 4F80 $8D = our own drone position report
|
|
bne L_4F94 ; 4F82 not $8D
|
|
lda droneViewOriginCol ; 4F84 column the drone screen chose ...
|
|
sta viewOriginCol ; 4F87 ... becomes the tactical view origin ...
|
|
lda droneViewOriginRow ; 4F89 ... together with the row ...
|
|
sta viewOriginRow ; 4F8C ... so the view follows the drone
|
|
L_4F8E:
|
|
dec pendingOwnCmdCount ; 4F8E the report counted as an outstanding order when it was queued
|
|
jmp L_4FBC ; 4F91 and it is not copied into the merged buffer
|
|
|
|
L_4F94:
|
|
cmp #$8C ; 4F94 $8C = session control
|
|
bne L_4FA2 ; 4F96 not $8C
|
|
iny ; 4F98 step to the reason byte
|
|
lda txPacketBuffer,y ; 4F99 read it (also trainerAiE000:txPacketBuffer)
|
|
jsr cmdSessionControl ; 4F9C act on our own pause/abort at once, then also count it off
|
|
jmp L_4F8E ; 4F9F and skip the copy
|
|
|
|
L_4FA2:
|
|
cmp #$92 ; 4FA2 $92 = our drone reached its target
|
|
bne L_4FAB ; 4FA4 not $92
|
|
lda #$80 ; 4FA6 set the drone flag ...
|
|
sta droneArrivalFlags ; 4FA8 ... the drone screen watches this byte; the command is copied as well
|
|
L_4FAB:
|
|
ldx mergeFillIndex ; 4FAB X = where in the merged buffer this command goes
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the whole command into the merged buffer.
|
|
; ----------------------------------------------------------------------
|
|
L_4FAD:
|
|
lda txPacketBuffer,y ; 4FAD next byte of the command (also trainerAiE000:txPacketBuffer)
|
|
sta mergedCmdBuffer,x ; 4FB0 into the merged buffer
|
|
iny ; 4FB3 next packet position
|
|
inx ; 4FB4 next buffer position
|
|
inc mergeFillIndex ; 4FB5 the buffer grew by one byte
|
|
dec currentCommandLength ; 4FB7 one byte of this command done
|
|
bne L_4FAD ; 4FBA until the whole command has been copied
|
|
L_4FBC:
|
|
ldy packetReadIndex ; 4FBC the cursor advancePacketCursor left
|
|
cpy txPacketLength ; 4FBE end of our packet? (also trainerAiE000:txPacketLength)
|
|
bcc L_4F78 ; 4FC1 no: next command
|
|
L_4FC3:
|
|
rts ; 4FC3 our own packet has been merged
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readCmdArgs3 - Argument reader for the 4-byte commands $80, $81, $8A, $8B, $98, $9C and $A6: takes
|
|
; the first parameter into cmdParam0 and falls through into readCmdArgs2, which picks up the other
|
|
; two. The read index ends up on the next command, matching commandLengthTable.
|
|
; In: mergedCmdReadIndex ($90B7) = offset of the command byte in mergedCmdBuffer
|
|
; Out: cmdParam0/cmdParam1/cmdParam2 ($73/$74/$75) = the three parameters, mergedCmdReadIndex advanced
|
|
; by 4, A/Y clobbered
|
|
; Called from: cmdMoveUnit ($5011), cmdMoveGroupRelative ($501A), cmdPlaceUnit ($51AB), cmdSetMapSeed
|
|
; ($51F3), cmdMoveKeepGroup ($5470) and cmdSetRecycler ($54E1)
|
|
; ----------------------------------------------------------------------
|
|
readCmdArgs3:
|
|
ldy mergedCmdReadIndex ; 4FC4 offset of the command byte
|
|
iny ; 4FC7 first parameter follows it
|
|
lda mergedCmdBuffer,y ; 4FC8 read it
|
|
sta cmdParam0 ; 4FCB cmdParam0, usually the unit or group
|
|
inc mergedCmdReadIndex ; 4FCD count that byte, then fall into readCmdArgs2 for the remaining two
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readCmdArgs2 - Argument reader for the 3-byte commands $87, $88, $8E, $90, $9A, $A3 and $A5, and the
|
|
; tail of readCmdArgs3. It reads the two bytes that follow the command byte into cmdParam1/cmdParam2
|
|
; and leaves the read index on the next command (advance 3, or 4 when entered from readCmdArgs3).
|
|
; In: mergedCmdReadIndex ($90B7), mergedCmdBuffer ($908F)
|
|
; Out: cmdParam1/cmdParam2 ($74/$75), mergedCmdReadIndex advanced, A/Y clobbered
|
|
; Called from: cmdSetTarget ($516B), cmdJoinGroup ($5185), cmdDroneDetonate ($5336), cmdMissileStrike
|
|
; ($53C0), cmdAddEnergy ($5479), cmdSetFormation ($5596), cmdSetupOption ($55A5) and readCmdArgs3
|
|
; ----------------------------------------------------------------------
|
|
readCmdArgs2:
|
|
ldy mergedCmdReadIndex ; 4FD0 current read offset (already past parameter 1 when entered from $4FC4)
|
|
iny ; 4FD3 next byte
|
|
lda mergedCmdBuffer,y ; 4FD4 read it
|
|
sta cmdParam1 ; 4FD7 cmdParam1, usually the map column
|
|
iny ; 4FD9 next byte
|
|
lda mergedCmdBuffer,y ; 4FDA read it
|
|
sta cmdParam2 ; 4FDD cmdParam2, usually the map row
|
|
iny ; 4FDF step past it
|
|
sty mergedCmdReadIndex ; 4FE0 the read index now points at the next command byte
|
|
rts ; 4FE3 back to the handler
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorCmdCoordsIfRemote - Point mirror for the map coordinates carried by a command. Both machines
|
|
; run one shared simulation while each player sees his own side at the bottom of the map, so every
|
|
; coordinate that arrives over the link has to be rotated 180 degrees (39-x, 39-y) before it is used.
|
|
; Own commands are left alone.
|
|
; In: cmdFromOpponent (zp_76) bit 7, cmdParam1 = column, cmdParam2 = row
|
|
; Out: cmdParam1/cmdParam2 replaced by 39-column/39-row for a remote command, untouched for an own
|
|
; one; A clobbered
|
|
; Called from: cmdMoveUnit, cmdMoveGroupRelative, cmdPlaceUnit, cmdMissileStrike, cmdMoveKeepGroup,
|
|
; cmdSetRecycler and mirrorCmdCoordsIfRemoteDec
|
|
; ----------------------------------------------------------------------
|
|
mirrorCmdCoordsIfRemote:
|
|
bit cmdFromOpponent ; 4FE4 did this command come from the opponent?
|
|
bpl L_4FF6 ; 4FE6 no: our own coordinates are already in the local frame
|
|
lda cmdParam1 ; 4FE8 the column
|
|
jsr mirrorMapCoordinate ; 4FEA 39 - column
|
|
sta cmdParam1 ; 4FED back into cmdParam1
|
|
lda cmdParam2 ; 4FEF the row
|
|
jsr mirrorMapCoordinate ; 4FF1 39 - row
|
|
sta cmdParam2 ; 4FF4 back into cmdParam2
|
|
L_4FF6:
|
|
rts ; 4FF6 coordinates are now in this machine's frame of reference
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readCmdArg1 - Argument reader for all the 2-byte commands - the per-unit and per-group orders
|
|
; $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2 and $A4. It reads the single byte after the
|
|
; command byte into cmdParam0 and steps the read index to the next command.
|
|
; In: mergedCmdReadIndex ($90B7), mergedCmdBuffer ($908F)
|
|
; Out: cmdParam0 ($73) = the unit index or group key, mergedCmdReadIndex advanced by 2, A/Y clobbered
|
|
; Called from: the cloak/blitz/dig/group handlers ($5031, $504B, $5062, $507E, $50FA, $515D, $519B,
|
|
; $542E, $549E, $5511, $555D, $5576, $559C)
|
|
; ----------------------------------------------------------------------
|
|
readCmdArg1:
|
|
ldy mergedCmdReadIndex ; 4FF7 offset of the command byte
|
|
iny ; 4FFA its single parameter follows
|
|
lda mergedCmdBuffer,y ; 4FFB read it
|
|
sta cmdParam0 ; 4FFE cmdParam0 = unit index or group key
|
|
iny ; 5000 step past it
|
|
sty mergedCmdReadIndex ; 5001 the read index now points at the next command
|
|
rts ; 5004 back to the handler
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorCmdCoordsIfRemoteDec - Coordinate mirror for the drone, which covers a 2x2 block of cells and
|
|
; is addressed by its top-left corner. For a remote command it mirrors the corner and then decrements
|
|
; both coordinates, giving 38-x / 38-y, which is the top-left corner of the mirrored block. No-op for
|
|
; an own command.
|
|
; In: cmdFromOpponent (zp_76), cmdParam1/cmdParam2 = the drone's top-left cell
|
|
; Out: cmdParam1/cmdParam2 = 38-column / 38-row for a remote command; A clobbered
|
|
; Called from: cmdDroneMove ($52DC) and cmdDroneDetonate ($5359)
|
|
; ----------------------------------------------------------------------
|
|
mirrorCmdCoordsIfRemoteDec:
|
|
lda cmdFromOpponent ; 5005 did this command come from the opponent?
|
|
bpl L_5010 ; 5007 no: leave the coordinates alone
|
|
jsr mirrorCmdCoordsIfRemote ; 5009 mirror the corner cell (39-x, 39-y)
|
|
dec cmdParam1 ; 500C the mirrored 2x2 block starts one column earlier ...
|
|
dec cmdParam2 ; 500E ... and one row earlier
|
|
L_5010:
|
|
rts ; 5010 back to the drone handler
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdMoveUnit - Handler of command $80 MOVE (4 bytes: unit, column, row), the ordinary move order
|
|
; given by clicking a cell with a single unit selected. It reads and mirrors the arguments and
|
|
; tail-jumps into applyMoveOrderOld ($34C6), which drops the unit out of any group, re-syncs its
|
|
; display flags and sets the destination.
|
|
; In: the merged command buffer and its read index; cmdFromOpponent for the mirroring
|
|
; Out: unit destination/waypoint tables updated via $34C6
|
|
; Called from: reached only through commandHandlerTable, entry $80
|
|
; ----------------------------------------------------------------------
|
|
cmdMoveUnit:
|
|
jsr readCmdArgs3 ; 5011 unit, column and row into cmdParam0/1/2
|
|
jsr mirrorCmdCoordsIfRemote ; 5014 a move ordered by the opponent is expressed in his own view of the map
|
|
jmp applyMoveOrder ; 5017 leave the group, refresh the flags and set the destination
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdMoveGroupRelative - Handler of command $81 (4 bytes: unit, column, row) - the move order given
|
|
; while a whole group is selected. It reads and mirrors the arguments and tail-jumps into
|
|
; moveGroupKeepingFormation, which shifts every member of the named unit's group by the same offset so
|
|
; the formation is kept.
|
|
; In: the merged command buffer and its read index; cmdFromOpponent
|
|
; Out: destinations of all members of the leader's group
|
|
; Called from: reached only through commandHandlerTable, entry $81
|
|
; ----------------------------------------------------------------------
|
|
cmdMoveGroupRelative:
|
|
jsr readCmdArgs3 ; 501A leader unit, ordered column and row
|
|
jsr mirrorCmdCoordsIfRemote ; 501D mirror the ordered cell for a command from the opponent
|
|
jmp moveGroupKeepingFormation; 5020 shift the whole group by leader-destination minus ordered cell
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdCloakOn - Handler of command $82 CLOAK ON (2 bytes: unit). Cloak and blitz are mutually
|
|
; exclusive, so the unit's blitz bits are cleared first by calling cmdBlitzOff, which consumes the
|
|
; same argument; the read index is therefore saved in a self-modified operand and restored before the
|
|
; argument is read again. SPY units (type 3) are permanently cloaked and are rejected, everybody else
|
|
; gets bits 6-7 of unitPathColTable set (cloak ordered plus cloak active) and falls into the shared
|
|
; tail at $5058.
|
|
; In: the merged command buffer; unitTypeTable for the SPY test
|
|
; Out: unitPathColTable[unit] |= $C0 and unitPathRowTable[unit] &= $3F; pendingRedrawUnit and
|
|
; comcenCloakedFlag through the tail; self-modifies the LDA operand at $502D
|
|
; Called from: reached only through commandHandlerTable, entry $82
|
|
; ----------------------------------------------------------------------
|
|
cmdCloakOn:
|
|
ldy mergedCmdReadIndex ; 5023 current read index in the merged buffer
|
|
sty L_502C+1 ; 5026 patch the LDA # at $502D with it - cmdBlitzOff will consume the argument too
|
|
jsr cmdBlitzOff ; 5029 clear the unit's blitz bits first: a unit cannot be cloaked and blitzing at once
|
|
L_502C:
|
|
lda #$FF ; 502C operand patched at $5026: the saved read index
|
|
sta mergedCmdReadIndex ; 502E rewind so the same argument can be read again
|
|
jsr readCmdArg1 ; 5031 cmdParam0 = the unit
|
|
ldy cmdParam0 ; 5034 Y = unit index
|
|
lda unitTypeTable,y ; 5036 its type byte
|
|
and #$07 ; 5039 keep only the type, dropping the dead and dug-in bits
|
|
cmp #$03 ; 503B type 3 = SPY
|
|
beq L_504A ; 503D spies are always cloaked: nothing to do
|
|
lda unitPathColTable,y ; 503F waypoint column byte, whose top two bits hold the cloak state
|
|
ora #$C0 ; 5042 bit 7 = cloak active, bit 6 = cloak ordered
|
|
sta unitPathColTable,y ; 5044 the unit is now cloaked
|
|
jmp L_5058 ; 5047 join the shared tail
|
|
|
|
L_504A:
|
|
rts ; 504A a SPY needs no cloak order
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdCloakOff - Handler of command $84 CLOAK OFF (2 bytes: unit); also used as a subroutine by
|
|
; cmdBlitzOn. It clears bits 6-7 of unitPathColTable. The shared tail at $5058 notes the unit in
|
|
; pendingRedrawUnit for an own command only - the opponent's cloaking must not trigger a redraw here -
|
|
; and then falls into updateComcenCloakLock. There is no SPY exemption on this side, unlike in the
|
|
; group handler.
|
|
; In: cmdParam0 after readCmdArg1, cmdFromOpponent
|
|
; Out: unitPathColTable[unit] &= $3F; pendingRedrawUnit ($923B) = unit for an own command;
|
|
; comcenCloakedFlag refreshed
|
|
; Called from: commandHandlerTable entry $84, and cmdBlitzOn ($5076)
|
|
; ----------------------------------------------------------------------
|
|
cmdCloakOff:
|
|
jsr readCmdArg1 ; 504B cmdParam0 = the unit
|
|
ldy cmdParam0 ; 504E Y = unit index
|
|
lda unitPathColTable,y ; 5050 waypoint column byte
|
|
and #$3F ; 5053 clear bits 6-7: cloak off
|
|
sta unitPathColTable,y ; 5055 store it back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shared tail of the single-unit cloak handlers: only our own cloak changes are worth a local redraw,
|
|
; and the console's inspect lock has to follow the state of our own comcen.
|
|
; ----------------------------------------------------------------------
|
|
L_5058:
|
|
lda cmdFromOpponent ; 5058 did this command come from the opponent?
|
|
bmi L_505F ; 505A yes: no local redraw
|
|
sty pendingRedrawUnit ; 505C ask the per-frame tick loop to redraw this unit
|
|
L_505F:
|
|
jmp updateComcenCloakLock ; 505F refresh 'my comcen is cloaked' and return from there
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdBlitzOff - Handler of command $95 BLITZ OFF (2 bytes: unit); also used as a subroutine by
|
|
; cmdCloakOn. It clears bits 6-7 of unitPathRowTable, the blitz ordered/active pair.
|
|
; In: the merged command buffer and its read index
|
|
; Out: unitPathRowTable[unit] &= $3F; cmdParam0 = unit, Y = unit
|
|
; Called from: commandHandlerTable entry $95, and cmdCloakOn ($5029)
|
|
; ----------------------------------------------------------------------
|
|
cmdBlitzOff:
|
|
jsr readCmdArg1 ; 5062 cmdParam0 = the unit
|
|
ldy cmdParam0 ; 5065 Y = unit index
|
|
lda unitPathRowTable,y ; 5067 waypoint row byte, whose top two bits hold the blitz state
|
|
and #$3F ; 506A clear bits 6-7: no longer blitzing
|
|
sta unitPathRowTable,y ; 506C store it back
|
|
rts ; 506F back to the dispatcher (or to cmdCloakOn)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdBlitzOn - Handler of command $93 BLITZ ON (2 bytes: unit), the mirror image of cmdCloakOn. It
|
|
; saves and restores the read index around a call to cmdCloakOff (blitz cancels cloak), rejects SPY
|
|
; units, sets bits 6-7 of unitPathRowTable and tail-jumps into applyBlitzStepCost, which pays one
|
|
; energy point for the extra 25 movement points.
|
|
; In: the merged command buffer; unitTypeTable for the SPY test
|
|
; Out: unitPathRowTable[unit] |= $C0, unitPathColTable[unit] &= $3F, energy and movement points
|
|
; through $3EE2; self-modifies the LDA operand at $507A
|
|
; Called from: reached only through commandHandlerTable, entry $93
|
|
; ----------------------------------------------------------------------
|
|
cmdBlitzOn:
|
|
ldy mergedCmdReadIndex ; 5070 current read index
|
|
sty L_5079+1 ; 5073 patch the LDA # at $507A - cmdCloakOff consumes the argument as well
|
|
jsr cmdCloakOff ; 5076 clear the cloak bits first: a unit cannot be cloaked and blitzing at once
|
|
L_5079:
|
|
lda #$FF ; 5079 operand patched at $5073: the saved read index
|
|
sta mergedCmdReadIndex ; 507B rewind so the argument can be read again
|
|
jsr readCmdArg1 ; 507E cmdParam0 = the unit
|
|
ldy cmdParam0 ; 5081 Y = unit index
|
|
lda unitTypeTable,y ; 5083 its type byte
|
|
and #$07 ; 5086 keep only the type
|
|
cmp #$03 ; 5088 type 3 = SPY
|
|
beq L_5097 ; 508A spies cannot blitz
|
|
lda unitPathRowTable,y ; 508C waypoint row byte
|
|
ora #$C0 ; 508F bit 7 = blitz active, bit 6 = blitz ordered
|
|
sta unitPathRowTable,y ; 5091 the unit is now blitzing
|
|
jmp applyBlitzStepCost ; 5094 charge the energy and grant the extra movement points, then return from there
|
|
|
|
L_5097:
|
|
rts ; 5097 a SPY cannot blitz: nothing to do
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdGroupCloakOn - Handler of command $83 (2 bytes: group key). It runs cmdGroupBlitzOff for the same
|
|
; group first (the two states are exclusive), saving and restoring the read index around it, then
|
|
; enters the patch block of the shared group routine at $50D4 with X = $80 so the body works on
|
|
; unitPathColTable with OR $C0 and sets bit 7 of the group's status entry.
|
|
; In: the merged command buffer; the group key argument carries the group number in bits 2-4 and the
|
|
; side in bit 5
|
|
; Out: unitPathColTable of every living non-SPY member |= $C0, unitPathRowTable &= $3F,
|
|
; groupStatusTable[group] |= $80, pendingRedrawGroup and comcenCloakedFlag; self-modifies the LDA
|
|
; operand at $50A2
|
|
; Called from: reached only through commandHandlerTable, entry $83
|
|
; ----------------------------------------------------------------------
|
|
cmdGroupCloakOn:
|
|
ldy mergedCmdReadIndex ; 5098 current read index
|
|
sty L_50A1+1 ; 509B patch the LDA # at $50A2 - the blitz handler consumes the argument as well
|
|
jsr cmdGroupBlitzOff ; 509E clear the group's blitz bits first
|
|
L_50A1:
|
|
lda #$FF ; 50A1 operand patched at $509B: the saved read index
|
|
sta mergedCmdReadIndex ; 50A3 rewind so the group key can be read again
|
|
ldx #$80 ; 50A6 X = $80: cloak variant, group status bit 7
|
|
bne L_50D4 ; 50A8 always taken: enter the cloak patch block
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdGroupBlitzOn - Handler of command $94 (2 bytes: group key). It runs cmdGroupCloakOff for the
|
|
; group first, then enters the shared patch block at $50C1 with X = $C0 and A = $40, so the body works
|
|
; on unitPathRowTable with OR $C0, calls applyBlitzStepCost for every member and sets
|
|
; groupStatusTable[group] = (old & $BF) | $40.
|
|
; In: the merged command buffer
|
|
; Out: unitPathRowTable of every living non-SPY member |= $C0, one energy point and 25 movement points
|
|
; per member, groupStatusTable[group] bit 6 set; self-modifies the LDA operand at $50B4
|
|
; Called from: reached only through commandHandlerTable, entry $94
|
|
; ----------------------------------------------------------------------
|
|
cmdGroupBlitzOn:
|
|
ldy mergedCmdReadIndex ; 50AA current read index
|
|
sty L_50B3+1 ; 50AD patch the LDA # at $50B4 - the cloak handler consumes the argument as well
|
|
jsr cmdGroupCloakOff ; 50B0 clear the group's cloak bits first
|
|
L_50B3:
|
|
lda #$FF ; 50B3 operand patched at $50AD: the saved read index
|
|
sta mergedCmdReadIndex ; 50B5 rewind so the group key can be read again
|
|
ldx #$C0 ; 50B8 per-unit bits to OR in: blitz ordered + active
|
|
lda #$40 ; 50BA bit 6 of the group's status entry = the group is blitzing
|
|
bne L_50C1 ; 50BC always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdGroupBlitzOff - Handler of command $96 (2 bytes: group key) and, at $50C1, the blitz half of the
|
|
; patch block that configures the shared group routine. With X = A = 0 it selects: no blitz cost,
|
|
; group status OR $00, group status AND $BF and table unitPathRowTable, so every member's blitz bits
|
|
; are cleared.
|
|
; In: the merged command buffer
|
|
; Out: unitPathRowTable of every member &= $3F, groupStatusTable[group] &= $BF; patches the operands
|
|
; at $512D, $514B and $514D and the table operands via applyGroupCloakOrBlitz
|
|
; Called from: commandHandlerTable entry $96, and cmdGroupCloakOn ($509E)
|
|
; ----------------------------------------------------------------------
|
|
cmdGroupBlitzOff:
|
|
ldx #$00 ; 50BE X = 0: no bits to set in the members' waypoint byte
|
|
txa ; 50C0 A = 0 as well
|
|
L_50C1:
|
|
sta L_514C+1 ; 50C1 patch the ORA # at $514D: bits to set in the group status entry
|
|
sta L_512C+1 ; 50C4 patch the LDA # at $512D: non-zero here means 'charge the blitz cost per member'
|
|
lda #$BF ; 50C7 keep everything but bit 6 ...
|
|
sta L_514A+1 ; 50C9 ... patch the AND # at $514B (bit 6 = the group is blitzing)
|
|
lda #$FC ; 50CC low byte of unitPathRowTable ($F8FC), the blitz table
|
|
ldy #$F8 ; 50CE its high byte
|
|
bne applyGroupCloakOrBlitz ; 50D0 always taken: run the shared body
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdGroupCloakOff - Handler of command $85 (2 bytes: group key) and, at $50D4, the cloak half of the
|
|
; patch block. With X = 0 it selects: no blitz cost, group status AND $7F, group status OR $00 and
|
|
; table unitPathColTable, so the cloak bits of every member are cleared. Entered with X = $80 by
|
|
; cmdGroupCloakOn, which turns the same block into the cloak-on variant.
|
|
; In: the merged command buffer; X = 0 (cloak off) or $80 (cloak on)
|
|
; Out: unitPathColTable of every member &= $3F (or |= $C0), groupStatusTable[group] bit 7 cleared or
|
|
; set, pendingRedrawGroup for an own command, comcenCloakedFlag refreshed
|
|
; Called from: commandHandlerTable entry $85, cmdGroupBlitzOn ($50B0) and cmdGroupCloakOn ($50A8)
|
|
; ----------------------------------------------------------------------
|
|
cmdGroupCloakOff:
|
|
ldx #$00 ; 50D2 X = 0: clear the cloak bits
|
|
L_50D4:
|
|
lda #$00 ; 50D4 0 = no blitz cost in this variant ...
|
|
sta L_512C+1 ; 50D6 ... patch the LDA # at $512D
|
|
lda #$7F ; 50D9 keep everything but bit 7 ...
|
|
sta L_514A+1 ; 50DB ... patch the AND # at $514B (bit 7 = the group is cloaked)
|
|
stx L_514C+1 ; 50DE patch the ORA # at $514D: $00 for cloak off, $80 for cloak on
|
|
lda #$98 ; 50E1 low byte of unitPathColTable ($F898), the cloak table
|
|
ldy #$F8 ; 50E3 its high byte
|
|
cpx #$00 ; 50E5 which variant was requested?
|
|
beq applyGroupCloakOrBlitz ; 50E7 cloak off: the members' bits are cleared (OR $00)
|
|
ldx #$C0 ; 50E9 cloak on: OR $C0 into every member's waypoint byte
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyGroupCloakOrBlitz - The shared, heavily self-modified body of the four group cloak/blitz
|
|
; handlers; it is only ever reached by branching, never by JSR. It stores the chosen table address
|
|
; into the LDA/STA operands of the unit loop, reads the group key argument, notes the group in
|
|
; pendingRedrawGroup for an own cloak command, and walks all 100 unit slots skipping dead units,
|
|
; non-members and SPYs. For every member it optionally charges the blitz cost and rewrites table[unit]
|
|
; = (old & $3F) | patchedBits. Finally it updates the group's entry in groupStatusTable and falls into
|
|
; updateComcenCloakLock.
|
|
; In: the patched operands at $512D (blitz cost gate), $5134/$513B (table address), $5139 (per-unit
|
|
; bits), $514B/$514D (group status mask and bits); cmdParam0 = group key after readCmdArg1;
|
|
; unitFlagsTable and unitTypeTable
|
|
; Out: cloak or blitz bits of every matching unit, groupStatusTable[group], pendingRedrawGroup
|
|
; ($923A), comcenCloakedFlag ($9245)
|
|
; Called from: the four group handlers $83/$85/$94/$96 by branch ($50D0 and $50E7)
|
|
; ----------------------------------------------------------------------
|
|
applyGroupCloakOrBlitz:
|
|
stx L_5138+1 ; 50EB patch the ORA # at $5139: the bits every member gets ($00 or $C0)
|
|
sta L_5133+1 ; 50EE patch the low byte of the LDA abs,y at $5134 ...
|
|
sta L_513A+1 ; 50F1 ... and of the STA abs,y at $513B
|
|
sty L_5133+2 ; 50F4 patch the high byte of the LDA at $5135 ...
|
|
sty L_513A+2 ; 50F7 ... and of the STA at $513C ($F898 cloak or $F8FC blitz)
|
|
jsr readCmdArg1 ; 50FA cmdParam0 = the group key argument
|
|
lda L_514A+1 ; 50FD the group status AND mask that was patched in
|
|
cmp #$7F ; 5100 $7F means the cloak variant is running
|
|
bne L_510D ; 5102 blitz: no redraw needed, cloaking is what changes the picture
|
|
lda cmdFromOpponent ; 5104 did this command come from the opponent?
|
|
bmi L_510D ; 5106 yes: the opponent's cloaking must not redraw our screen
|
|
lda cmdParam0 ; 5108 our own group ...
|
|
sta pendingRedrawGroup ; 510A ... is handed to the tick loop to be redrawn
|
|
L_510D:
|
|
lda cmdParam0 ; 510D the group key
|
|
ora #$40 ; 510F add the in-a-group bit so it matches unitFlagsTable & $7C
|
|
sta cmdParam0 ; 5111 and keep it there for the loop
|
|
ldy #$63 ; 5113 scan unit slots 99 down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass over all 100 unit slots. Dead slots, units of other groups and SPYs are skipped; SPYs are
|
|
; permanently cloaked and can never blitz, so they are exempt from both orders.
|
|
; ----------------------------------------------------------------------
|
|
L_5115:
|
|
lda unitTypeTable,y ; 5115 type byte of this slot
|
|
bmi L_513D ; 5118 bit 7 = dead or empty, skip it
|
|
lda unitFlagsTable,y ; 511A authoritative flags
|
|
and #$7C ; 511D group number, side and member bit
|
|
cmp cmdParam0 ; 511F a member of the group named by the command?
|
|
bne L_513D ; 5121 no: next slot
|
|
lda unitTypeTable,y ; 5123 type byte again
|
|
and #$07 ; 5126 keep only the type
|
|
cmp #$03 ; 5128 type 3 = SPY
|
|
beq L_513D ; 512A spies are exempt from group cloak and blitz orders
|
|
L_512C:
|
|
lda #$FF ; 512C operand patched at $50C4/$50D6: non-zero only for the blitz-on variant
|
|
beq L_5133 ; 512E zero: no cost to charge
|
|
jsr applyBlitzStepCost ; 5130 blitz on: pay one energy point for 25 extra movement points
|
|
L_5133:
|
|
lda irqVectorHi,y ; 5133 operand patched at $50EE/$50F4: the member's cloak ($F898) or blitz ($F8FC) byte
|
|
and #$3F ; 5136 keep the waypoint coordinate in bits 0-5
|
|
L_5138:
|
|
ora #$FF ; 5138 operand patched at $50EB: set both state bits, or none
|
|
L_513A:
|
|
sta irqVectorHi,y ; 513A operand patched at $50F1/$50F7: store the byte back
|
|
L_513D:
|
|
dey ; 513D next lower slot
|
|
bpl L_5115 ; 513E loop until slot 0 has been examined
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Finally record the new state in the group's own entry so that the console and the group menu can
|
|
; show CLOAKED / BLITZ without scanning the units again.
|
|
; ----------------------------------------------------------------------
|
|
lda cmdParam0 ; 5140 the group key with its member bit
|
|
and #$3C ; 5142 group number and side ...
|
|
lsr a ; 5144 ... shifted down ...
|
|
lsr a ; 5145 ... to give the 0-15 index (group 0-7 of side 0, 8-15 of side 1)
|
|
tay ; 5146 Y = index into groupStatusTable
|
|
lda groupStatusTable,y ; 5147 current status: speed class in bits 0-2, terrain class in bits 3-5, blitz bit 6, cloak bit 7
|
|
L_514A:
|
|
and #$FF ; 514A operand patched at $50C9/$50DB: clear the bit this command owns
|
|
L_514C:
|
|
ora #$FF ; 514C operand patched at $50C1/$50DE: and set it if the order was 'on'
|
|
sta groupStatusTable,y ; 514E store the new group status
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateComcenCloakLock - The common tail of every cloak and blitz handler and the only writer of
|
|
; comcenCloakedFlag: it copies bit 7 of the local player's own comcen entry in unitPathColTable, so
|
|
; $9245 bit 7 is set exactly while that comcen is cloaked. The console reads it to lock the inspection
|
|
; panels ($2064, $2096) and both $6F00 overlays read it as well.
|
|
; In: comcenUnit ($9236) = 49 or 99, unitPathColTable; no arguments
|
|
; Out: comcenCloakedFlag ($9245) = unitPathColTable[own comcen] & $80; A and Y clobbered
|
|
; Called from: the fall-through from applyGroupCloakOrBlitz, the single-unit cloak tail ($505F) and
|
|
; both $6F00 overlays ($72E7)
|
|
; ----------------------------------------------------------------------
|
|
updateComcenCloakLock:
|
|
ldy comcenUnit ; 5151 index of this player's own comcen (49 for side 0, 99 for side 1)
|
|
lda unitPathColTable,y ; 5154 its waypoint column byte
|
|
and #$80 ; 5157 bit 7 = the comcen is cloaked right now
|
|
sta comcenCloakedFlag ; 5159 the console and the overlays test this byte
|
|
rts ; 515C back to the dispatcher
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdClearTarget - Handler of command $86 CLEAR TARGET (2 bytes: BOOMER unit). getBoomerTarget turns
|
|
; the unit index into that unit's ordinal among all BOOMERs and the matching entry of
|
|
; boomerTargetTable is zeroed, so the BOOMER stops holding fire for a particular enemy. The carry that
|
|
; getBoomerTarget returns is not tested, so a non-BOOMER argument would clear a slot chosen by a
|
|
; leftover Y; only the unit menu of a BOOMER offers the item, so that never happens in practice.
|
|
; In: mergedCmdBuffer $908F / mergedCmdReadIndex $90B7 (the queued command), cmdParam0 after
|
|
; readCmdArg1
|
|
; Out: boomerTargetTable $91DC[ordinal] = 0; mergedCmdReadIndex += 2; X = unit, Y = ordinal
|
|
; Called from: commandHandlerTable slot for $86 ($4D36), dispatched by executeTurnCommands at $4EEE
|
|
; ----------------------------------------------------------------------
|
|
cmdClearTarget:
|
|
jsr readCmdArg1 ; 515D read the single argument byte into cmdParam0 and step mergedCmdReadIndex past this 2-byte command
|
|
ldx cmdParam0 ; 5160 X = the BOOMER's unit index
|
|
jsr getBoomerTarget ; 5162 $3C74: for a BOOMER (unitTypeTable & 7 == 2) returns Y = its ordinal among all BOOMERs
|
|
lda #$00 ; 5165 0 = no target and no lock
|
|
sta boomerTargetTable,y ; 5167 clear this BOOMER's slot of the 20-entry boomerTargetTable at $91DC
|
|
rts ; 516A done - CLEAR TARGET touches nothing but the target table
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetTarget - Handler of command $87 SET TARGET (3 bytes: BOOMER unit, target unit). It only
|
|
; rearranges the parameter bytes so that setBoomerTargetCmd ($3C50) finds the BOOMER in cmdParam0 and
|
|
; the target in cmdParam2, then jumps there; the result is boomerTargetTable[BOOMER ordinal] = target
|
|
; | $80. The command is sent by sub_3C20 when the player puts the cursor on an enemy unit with a
|
|
; BOOMER selected. The EOR #$80 on cmdFromOpponent at $5171 is undone again at $517C without anything
|
|
; reading it in between, and the store at
|
|
; $5177 is immediately overwritten at $5180 - both are leftovers from an earlier version.
|
|
; In: the queued command in mergedCmdBuffer; cmdParam1 = BOOMER, cmdParam2 = target after
|
|
; readCmdArgs2
|
|
; Out: boomerTargetTable $91DC[ordinal] = target | $80 via $3C50; cmdParam0 = BOOMER; read index += 3
|
|
; Called from: commandHandlerTable slot for $87 ($4D38)
|
|
; ----------------------------------------------------------------------
|
|
cmdSetTarget:
|
|
jsr readCmdArgs2 ; 516B read the two arguments: cmdParam1 = the BOOMER, cmdParam2 = the target unit; read index += 3
|
|
lda cmdFromOpponent ; 516E take a copy of the own/remote flag of the command being executed
|
|
pha ; 5170 push it
|
|
eor #$80 ; 5171 flip bit 7 = 'this command came from the opponent' ...
|
|
sta cmdFromOpponent ; 5173 ... and store the flipped value back; nothing between here and $517C looks at it, so the flip does nothing (leftover)
|
|
lda cmdParam2 ; 5175 A = the target unit index
|
|
sta cmdParam0 ; 5177 cmdParam0 = target - overwritten again at $5180, so this store is dead
|
|
sta cmdParam2 ; 5179 write cmdParam2 back over itself - a no-op
|
|
pla ; 517B pull the untouched own/remote flag
|
|
sta cmdFromOpponent ; 517C undo the EOR of $5171
|
|
lda cmdParam1 ; 517E A = the BOOMER's unit index
|
|
sta cmdParam0 ; 5180 setBoomerTargetCmd wants the BOOMER in cmdParam0 and the target in cmdParam2
|
|
jmp setBoomerTargetCmd ; 5182 $3C50: boomerTargetTable[BOOMER ordinal] = cmdParam2 | $80 (target unit plus the 'locked' bit)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdJoinGroup - Handler of command $88 JOIN GROUP (3 bytes: unit, group byte). It jumps to
|
|
; joinUnitToGroup ($4AD4), which makes the unit follow the group's movement and sets unitFlagsTable =
|
|
; (old & 3) | group | $40. The eight instructions at $5188-$5197 are a shuffle whose only lasting
|
|
; effect is cmdParam0 = group byte, and nothing downstream reads cmdParam0 - the whole block is dead
|
|
; code.
|
|
; In: the queued command; cmdParam1 = unit, cmdParam2 = group byte (id bits 2-4, side bit 5)
|
|
; Out: unitFlagsTable $F708[unit] and unitDisplayFlagsTable $FB54[unit] via $4AD4; read index += 3
|
|
; Called from: commandHandlerTable slot for $88 ($4D3A)
|
|
; ----------------------------------------------------------------------
|
|
cmdJoinGroup:
|
|
jsr readCmdArgs2 ; 5185 read the two arguments: cmdParam1 = unit index, cmdParam2 = group byte (id in bits 2-4, side in bit 5)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Eight instructions of register shuffling whose only lasting effect is cmdParam0 = the group byte,
|
|
; and nothing reads cmdParam0 afterwards. Dead code left over from an earlier calling convention.
|
|
; ----------------------------------------------------------------------
|
|
lda cmdParam1 ; 5188 A = the unit index
|
|
sta cmdParam0 ; 518A cmdParam0 = unit - overwritten again at $5192
|
|
lda cmdParam0 ; 518C read the same byte straight back
|
|
sta cmdParam1 ; 518E store the unit index back into cmdParam1 - a no-op
|
|
lda cmdParam2 ; 5190 A = the group byte
|
|
sta cmdParam0 ; 5192 cmdParam0 = group byte; nothing downstream ever reads cmdParam0, so even this is pointless
|
|
lda cmdParam0 ; 5194 read it back again
|
|
sta cmdParam2 ; 5196 store the group byte back into cmdParam2 - another no-op
|
|
jmp joinUnitToGroup ; 5198 $4AD4: make unit cmdParam1 follow group cmdParam2 and set unitFlagsTable = (old & 3) | group | $40
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdLeaveGroup - Handler of command $89 (2 bytes: unit): clears bit 6 ('in a group') of the unit's
|
|
; unitFlagsTable entry and tail-jumps to syncLocalGroupFlags ($4C6B), which mirrors bits 2-6 into
|
|
; unitDisplayFlagsTable so the overview map stops drawing the unit's group marker. Sent by
|
|
; removeSelectedUnitFromGroup ($4AF2), the GROUP menu item REMOVED.
|
|
; In: the queued command; cmdParam0 = unit index
|
|
; Out: unitFlagsTable $F708[unit] &= $BF, unitDisplayFlagsTable $FB54[unit] refreshed; read index += 2
|
|
; Called from: commandHandlerTable slot for $89 ($4D3C)
|
|
; ----------------------------------------------------------------------
|
|
cmdLeaveGroup:
|
|
jsr readCmdArg1 ; 519B cmdParam0 = the unit index; read index += 2
|
|
ldx cmdParam0 ; 519E X = the unit
|
|
lda unitFlagsTable,x ; 51A0 $F708 flags: facing bits 0-1, group id bits 2-4, side bit 5, 'in a group' bit 6
|
|
and #$BF ; 51A3 clear bit 6 - the unit no longer belongs to a group
|
|
sta unitFlagsTable,x ; 51A5 write the flags back
|
|
jmp syncLocalGroupFlags ; 51A8 $4C6B: copy bits 2-6 into unitDisplayFlagsTable so the map stops drawing the group marker
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdPlaceUnit - Handler of command $8A (4 bytes: unit, column, row): teleports a unit during the
|
|
; deployment phase. The unit's cloak bits (unitPathColTable & $C0) and blitz bits (unitPathRowTable &
|
|
; $C0) are first saved into the ORA operands at $51C8 and $51E4, then the new column is written to
|
|
; unitColTable, to unitPathColTable (keeping the cloak state) and to unitDestColTable with the
|
|
; 'arrived' bit 7, bit 7 of unitDisplayFlagsTable is set, and the new row goes to unitRowTable,
|
|
; unitDestRowTable and unitPathRowTable (keeping the blitz state). The $F000 map itself is not touched
|
|
; here.
|
|
; In: the queued command; cmdParam0 = unit, cmdParam1/cmdParam2 = column/row (mirrored when remote)
|
|
; Out: $F640, $F6A4, $F7D0, $F834, $F898, $F8FC and $FB54 of the unit; self-modifies $51C9 and $51E5
|
|
; Called from: commandHandlerTable slot for $8A ($4D3E)
|
|
; ----------------------------------------------------------------------
|
|
cmdPlaceUnit:
|
|
jsr readCmdArgs3 ; 51AB read three arguments: cmdParam0 = unit, cmdParam1 = column, cmdParam2 = row; read index += 4
|
|
jsr mirrorCmdCoordsIfRemote ; 51AE a remote player sends his own coordinates: convert them with 39-v so both machines agree
|
|
ldy cmdParam0 ; 51B1 Y = the unit index into every one of the $64-spaced unit arrays
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Save the two flag pairs that live in the top bits of the waypoint bytes, so the rewrite below can
|
|
; put them back: cloak in unitPathColTable bits 6-7, blitz in unitPathRowTable bits 6-7.
|
|
; ----------------------------------------------------------------------
|
|
lda unitPathColTable,y ; 51B3 $F898 waypoint column: bits 0-5 column, bits 6-7 = cloak ordered / cloak active
|
|
and #$C0 ; 51B6 keep only the two cloak bits
|
|
sta L_51C8+1 ; 51B8 patch them into the ORA immediate at $51C8 so the rewrite below preserves them
|
|
lda unitPathRowTable,y ; 51BB $F8FC waypoint row: bits 0-5 row, bits 6-7 = blitz ordered / blitz active
|
|
and #$C0 ; 51BE keep only the two blitz bits
|
|
sta L_51E4+1 ; 51C0 patch them into the ORA immediate at $51E4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Column: current position, waypoint (cloak preserved) and destination (marked as already reached).
|
|
; ----------------------------------------------------------------------
|
|
lda cmdParam1 ; 51C3 A = the new map column
|
|
sta unitColTable,y ; 51C5 $F640 = the unit's current column
|
|
L_51C8:
|
|
ora #$FF ; 51C8 immediate operand patched at $51B8 with the saved cloak bits
|
|
sta unitPathColTable,y ; 51CA waypoint column = the new column with the cloak state untouched
|
|
and #$3F ; 51CD strip the cloak bits off again
|
|
ora #$80 ; 51CF bit 7 of unitDestColTable means 'already standing on its destination'
|
|
sta unitDestColTable,y ; 51D1 $F7D0 destination column = the new column, marked as reached
|
|
lda unitDisplayFlagsTable,y ; 51D4 $FB54 draw flags of the unit
|
|
ora #$80 ; 51D7 bit 7 = idle / at destination, the copy the drawing code looks at
|
|
sta unitDisplayFlagsTable,y ; 51D9 write the draw flags back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Row: current position, destination and waypoint (blitz preserved).
|
|
; ----------------------------------------------------------------------
|
|
lda cmdParam2 ; 51DC A = the new map row
|
|
sta unitRowTable,y ; 51DE $F6A4 = the unit's current row
|
|
sta unitDestRowTable,y ; 51E1 $F834 destination row = the same row (rows carry no flag bit)
|
|
L_51E4:
|
|
ora #$FF ; 51E4 immediate operand patched at $51C0 with the saved blitz bits
|
|
sta unitPathRowTable,y ; 51E6 waypoint row = the new row with the blitz state untouched
|
|
rts ; 51E9 note that the $F000 map cell is never updated here - the setup screen redraws the board itself
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetCustomMapSeed - Handler of command $A6 (4 bytes: three seed bytes): sets mapFlipFlag ($0B9D) =
|
|
; 1 and falls into cmdSetMapSeed. Overlay A sends $A6 instead of $8B at $79BD when the player picked
|
|
; the flipped (180-degree rotated) view of the map, so both machines build the battlefield the same
|
|
; way round.
|
|
; In: the queued command
|
|
; Out: mapFlipFlag = 1, then everything cmdSetMapSeed does
|
|
; Called from: the last entry of commandHandlerTable ($4D76, command $A6)
|
|
; ----------------------------------------------------------------------
|
|
cmdSetCustomMapSeed:
|
|
lda #$01 ; 51EA command $A6: the sender chose the flipped map variant
|
|
bne L_51F0 ; 51EC always taken - join the shared body
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetMapSeed - Handler of command $8B (4 bytes: three seed bytes): mapFlipFlag = 0, the three
|
|
; arguments are copied into the 24-bit map seed $0B95-$0B97, and bit 7 of gamePhase is cleared. The
|
|
; player who owns the map sends this at the start of a session; the other machine's
|
|
; waitForMapSeedCommand ($7C1C) spins on gamePhase bit 7 until this handler runs, after which both
|
|
; generate the identical 40x40 battlefield.
|
|
; In: the queued command; cmdParam0/1/2 = the seed bytes after readCmdArgs3
|
|
; Out: mapFlipFlag $0B9D, mapSeed $0B95-$0B97, gamePhase zp_B1 &= $7F; read index += 4
|
|
; Called from: commandHandlerTable slot for $8B ($4D40), and by fall-through from cmdSetCustomMapSeed
|
|
; ----------------------------------------------------------------------
|
|
cmdSetMapSeed:
|
|
lda #$00 ; 51EE command $8B: the map in its generated orientation
|
|
L_51F0:
|
|
sta mapFlipFlag ; 51F0 $0B9D: 0 = map as generated, 1 = rotated 180 degrees (the F1 TO FLIP option on the map screen)
|
|
jsr readCmdArgs3 ; 51F3 read the three seed bytes into cmdParam0/1/2; read index += 4
|
|
lda cmdParam0 ; 51F6 seed byte 0
|
|
sta mapSeed ; 51F8 $0B95: byte 0 of the 24-bit battlefield seed, which is the 5-letter map name packed 5 bits per letter
|
|
lda cmdParam1 ; 51FB seed byte 1
|
|
sta mapSeed1 ; 51FD $0B96
|
|
lda cmdParam2 ; 5200 seed byte 2
|
|
sta mapSeed2 ; 5202 $0B97
|
|
lda gamePhase ; 5205 zp_B1 game phase
|
|
and #$7F ; 5207 clear bit 7 = 'still waiting for the map seed'
|
|
sta gamePhase ; 5209 this releases waitForMapSeedCommand ($7C1C) on the receiving machine
|
|
rts ; 520B both machines now hold the same seed and will generate the identical battlefield
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdComcenRepairedOrSetupReady - Handler of the 1-byte command $9E, which means two different things.
|
|
; While the battle runs (gamePhase = 0) it is the result of a successful COMCEN repair from the REPAIR
|
|
; console tab ($1812): the stun counter in the low nibble of the sending side's COMCEN unitStunTable
|
|
; entry is cleared. In every other phase it is the READY / phase-sync acknowledgement of
|
|
; sendPhaseSyncAndWait ($7BF9): the sender's bit of gamePhase is cleared with getPauseClearMask, input
|
|
; is locked out for 32 frames, a fake SPACE key event is injected so any 'press space' loop falls
|
|
; through, and the chat state is reset.
|
|
; In: gamePhase zp_B1, cmdFromOpponent zp_76, playerSide $0B9F, perSideValueTable $0C8A
|
|
; Out: battle: unitStunTable $F960[comcen] &= $F0. Setup: zp_B1 masked, $0B7D = $20, $90EA = $20,
|
|
; $929C = 0.
|
|
; mergedCmdReadIndex += 1 in both cases
|
|
; Called from: commandHandlerTable slot for $9E ($4D66)
|
|
; ----------------------------------------------------------------------
|
|
cmdComcenRepairedOrSetupReady:
|
|
inc mergedCmdReadIndex ; 520C a 1-byte command: just step the read index over it
|
|
ldx gamePhase ; 520F zp_B1 is zero only while the battle itself is running
|
|
beq L_5228 ; 5211 battle: this is a successful COMCEN repair
|
|
jsr getPauseClearMask ; 5213 setup: A = the AND mask that clears the sending side's 'not ready' bit
|
|
and gamePhase ; 5216 apply it to the phase byte
|
|
sta gamePhase ; 5218 once both bits are gone sendPhaseSyncAndWait ($7BF9) stops waiting
|
|
lda #$20 ; 521A $20 = 32
|
|
sta inputLockoutTimer ; 521C $0B7D: ignore keyboard and joystick for the next 32 frames
|
|
sta newKeyEvent ; 521F inject key code $20 (SPACE) so any 'press space' wait loop falls through at once
|
|
lda #$00 ; 5222 0
|
|
sta chatState ; 5224 $929C: abandon any chat line that was being typed
|
|
rts ; 5227 phase sync done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Battle phase: command $9E means the sending side's COMCEN has finished repairing itself, so clear
|
|
; its stun counter.
|
|
; ----------------------------------------------------------------------
|
|
L_5228:
|
|
lda playerSide ; 5228 $0B9F = the side this machine plays, 0 or 1
|
|
ldy cmdFromOpponent ; 522B who sent the repair?
|
|
bpl L_5231 ; 522D our own command: our own COMCEN was repaired
|
|
eor #$01 ; 522F remote command: the other side's COMCEN
|
|
L_5231:
|
|
tax ; 5231 X = the sender's absolute side
|
|
ldy comcenUnitIndexBySide,x ; 5232 $0C8A = {$31,$63}: unit 49 is side 0's COMCEN, unit 99 is side 1's
|
|
lda unitStunTable,y ; 5235 $F960: low nibble = stun/knock-out counter, bit 4 spare, bits 5-7 = dig state
|
|
and #$F0 ; 5238 clear the stun counter - the COMCEN is back on line
|
|
sta unitStunTable,y ; 523A write it back, leaving the dig bits alone
|
|
rts ; 523D done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSessionControl - Handler of command $8C (2 bytes: reason). It is never dispatched out of the
|
|
; merged buffer: both packet mergers call it directly ($4F36 and $4F9C) with A = the reason byte while
|
|
; the packets are being merged, so the effect is immediate and the bytes are not copied into the film.
|
|
; Reason 0 = the game is being abandoned: gameOverFlags = $C0, the message queue is cleared, 'GAME
|
|
; ABORTED.' is shown for a remote abort, the end-of-game countdown is armed, and the dispatcher is
|
|
; abandoned by pulling two return addresses off the stack. Reasons 1 and 2 return at once ($8C,$02 is
|
|
; the menu READY packet handled elsewhere). Reason 3 = link timeout, reason 5 = deliberate hang-up,
|
|
; anything else (4) = resume. Everything except reason 0 is skipped during film playback.
|
|
; In: A = reason byte, X/Y = the merge loop's registers, cmdFromOpponent zp_76, $0B9B, $0BA8
|
|
; Out: pauseState zp_BE, $92A1, $0B7D, $929C, $92C9, $923F, zp_72, the message queue; returns through
|
|
; $532A
|
|
; with X/Y restored, or never returns at all for reason 0
|
|
; Called from: mergeIncomingPacket $4F36 and mergeOutgoingPacket $4F9C
|
|
; ----------------------------------------------------------------------
|
|
cmdSessionControl:
|
|
jsr saveRegsForCmdReturn ; 523E this handler runs inside the packet merge loop, so stash its X and Y into the operands at $532B/$532D
|
|
sta cmdParam0 ; 5241 A holds the reason byte the merger fetched out of the packet
|
|
cmp #$03 ; 5243 reasons 3 and up are the pause / hang-up / resume group
|
|
bcs L_5272 ; 5245 branch to the pause group
|
|
cmp #$00 ; 5247 reason 0 = the game is being abandoned
|
|
beq L_524C ; 5249 handle the abort
|
|
rts ; 524B reason 1 is unused; reason 2 is the menu-level READY packet handled by readyForNewGameHandshake
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Reason 0 - the game is being abandoned, by us or by the opponent.
|
|
; ----------------------------------------------------------------------
|
|
L_524C:
|
|
lda #$C0 ; 524C $C0 = game finished, no result computed
|
|
sta gameOverFlags ; 524E $923F game-over flags
|
|
lda #$62 ; 5251 98 ticks of end-of-game countdown when no battle was running
|
|
ldy gameEndReason ; 5253 $0BA8 is non-zero whenever no battle is in progress
|
|
bne L_5266 ; 5256 no battle: keep the $62 countdown and say nothing
|
|
jsr resetMessageQueue ; 5258 a battle was running: throw away everything queued for the status line
|
|
lda cmdFromOpponent ; 525B who aborted?
|
|
bpl L_5269 ; 525D our own abort - the local menu code has already set the countdown
|
|
lda #$03 ; 525F message $03
|
|
jsr queueMessage ; 5261 'GAME ABORTED.' - the opponent quit on us
|
|
lda #$71 ; 5264 113 ticks so the message can be read before the menu returns
|
|
L_5266:
|
|
sta gameEndCountdown ; 5266 $92C9 counts down to the return to the main menu
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Abandon the whole dispatcher. This handler was entered as JSR cmdSessionControl from inside
|
|
; mergeIncomingPacket/mergeOutgoingPacket, which themselves were entered as JSR from
|
|
; serviceCommandExchange, so four PLAs drop both return addresses and the JMP below returns straight
|
|
; to serviceCommandExchange's own caller.
|
|
; ----------------------------------------------------------------------
|
|
L_5269:
|
|
pla ; 5269 discard the return address of this handler ...
|
|
pla ; 526A (second byte)
|
|
pla ; 526B ... and that of mergeIncomingPacket / mergeOutgoingPacket ...
|
|
pla ; 526C ... so the RTS below lands in the caller of serviceCommandExchange instead
|
|
ldx #$02 ; 526D comm function 2 = acknowledge and consume the finished exchange
|
|
jmp commJumpTable ; 526F $E000; its RTS unwinds two levels because of the four PLAs above (also trainerAiE000:commRequestEntry)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Reasons 3 and up - the pause, hang-up and resume group. All of them are ignored during film
|
|
; playback.
|
|
; ----------------------------------------------------------------------
|
|
L_5272:
|
|
bit filmPlaybackMode ; 5272 $0B9B bit 7 = a game film is being replayed
|
|
bmi L_52AB ; 5275 during playback the pause / resume traffic is ignored
|
|
ldy #$00 ; 5277 0
|
|
sty chatState ; 5279 cancel any chat line being typed
|
|
ldy #$3C ; 527C $3C = 60 frames, about one second
|
|
sty inputLockoutTimer ; 527E hold the player's input off while the pause state changes
|
|
cmp #$03 ; 5281 reason 3 = link timeout
|
|
bne L_528E ; 5283 not the timeout reason
|
|
sta pauseState ; 5285 zp_BE = 3: both sides counted as paused
|
|
lda #$19 ; 5287 message $19
|
|
sta persistentMessageId ; 5289 'TIMEOUT. RUN/STOP TO RESUME.' stays on the line until someone resumes
|
|
bne L_52A8 ; 528C always taken - go and queue the message
|
|
L_528E:
|
|
cmp #$05 ; 528E reason 5 = the link was hung up on purpose
|
|
bne L_5296 ; 5290 not the hang-up reason
|
|
lda #$80 ; 5292 bit 7 of pauseState = link dropped (DISCONNECT / SET VOICE)
|
|
bne L_529D ; 5294 always taken
|
|
L_5296:
|
|
ldx pauseState ; 5296 any other reason (4 = RESUME) clears only the sender's own bit
|
|
jsr getPauseClearMask ; 5298 A = $FD for our resume, $FE for his, $FC in a solo game
|
|
and pauseState ; 529B clear that bit of the pause state
|
|
L_529D:
|
|
sta pauseState ; 529D store the new pause state
|
|
bne L_52AB ; 529F someone is still paused - leave the message up
|
|
sta persistentMessageId ; 52A1 A = 0: no persistent message any more
|
|
sta messageTimer ; 52A4 zp_72 = 0 so the next message appears immediately
|
|
lda #$1B ; 52A6 message $1B 'RESUMED'
|
|
L_52A8:
|
|
jsr queueMessage ; 52A8 put it on the status line
|
|
L_52AB:
|
|
jmp L_532A ; 52AB leave through the shared tail that restores the merge loop's X and Y
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getPauseClearMask - Returns the AND mask that clears the resuming side's bit of a two-bit
|
|
; ready/pause byte. cmdSessionControl applies it to pauseState (zp_BE) and
|
|
; cmdComcenRepairedOrSetupReady applies it to gamePhase (zp_B1). In a solo-trainer game there is no
|
|
; independent opponent, so both bits are cleared at once ($FC); otherwise an own command clears bit 0
|
|
; ($FD) and a remote command clears bit 1 ($FE), and in the remote case the message 'OPPONENT READY.'
|
|
; is queued if his bit was still set.
|
|
; In: X = the pause/phase byte before this command, isSoloTrainer $0BA5, cmdFromOpponent zp_76
|
|
; Out: A = $FC, $FD or $FE; may queue message $04; Y clobbered
|
|
; Called from: cmdSessionControl $5298 and cmdComcenRepairedOrSetupReady $5213
|
|
; ----------------------------------------------------------------------
|
|
getPauseClearMask:
|
|
lda #$FC ; 52AE mask that clears both pause bits at once
|
|
ldy isSoloTrainer ; 52B0 $0BA5 bit 7 = PRACTICE WITH SOLO TRAINER
|
|
bmi L_52C7 ; 52B3 solo: there is no independent opponent, so clear both bits together
|
|
lda #$FD ; 52B5 mask that clears bit 0 = this machine's bit
|
|
ldy cmdFromOpponent ; 52B7 who sent the command?
|
|
bpl L_52C7 ; 52B9 our own command: bit 0 is the one to clear
|
|
txa ; 52BB A = the pause/phase byte as it was before this command (passed in X)
|
|
and #$02 ; 52BC bit 1 = the opponent was still flagged as not ready
|
|
beq L_52C5 ; 52BE he was already ready - nothing to announce
|
|
lda #$04 ; 52C0 message $04
|
|
jsr queueMessage ; 52C2 'OPPONENT READY.'
|
|
L_52C5:
|
|
lda #$FE ; 52C5 mask that clears bit 1 = the opponent's bit
|
|
L_52C7:
|
|
rts ; 52C7 A = the AND mask
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDroneMove - Handler of command $8D (4 bytes: heading, column, row), the opponent's live drone
|
|
; telemetry. Only mergeIncomingPacket calls it, with Y pointing at the heading byte inside the
|
|
; received packet $E020; mergeOutgoingPacket deals with our own $8D by re-centring the tactical view
|
|
; instead, which is why the commandHandlerTable slot for $8D is a dummy - the byte never reaches the
|
|
; merged buffer. The handler mirrors the 2x2 drone origin into our coordinate frame and, while the
|
|
; COMCEN radar screen is showing and the drone has not already finished, parks sprite 0 on the drone's
|
|
; cell with the shape for the reversed heading.
|
|
; In: Y = index of the heading byte in $E020, cmdFromOpponent zp_76, droneArrivalFlags $92F9, $90FB
|
|
; Out: cmdParam0/1/2, droneControlFlags $920D bit 0, $9022, $92F6/$92F7, $9010/$9018, $91CC, $9008 and
|
|
; sprite 0's shape; X/Y restored from the operands patched by saveRegsForCmdReturn
|
|
; Called from: mergeIncomingPacket $4F28
|
|
; ----------------------------------------------------------------------
|
|
cmdDroneMove:
|
|
lda rxPacketBuffer,y ; 52C8 read straight out of the received packet at $E020; Y points at the heading byte (also trainerAiE000:rxPacketBuffer)
|
|
sta cmdParam0 ; 52CB heading 0-7 of the opponent's drone
|
|
iny ; 52CD next packet byte
|
|
lda rxPacketBuffer,y ; 52CE fetch it (also trainerAiE000:rxPacketBuffer)
|
|
sta cmdParam1 ; 52D1 drone map column in the sender's coordinate frame
|
|
iny ; 52D3 next packet byte
|
|
lda rxPacketBuffer,y ; 52D4 fetch it (also trainerAiE000:rxPacketBuffer)
|
|
sta cmdParam2 ; 52D7 drone map row in the sender's coordinate frame
|
|
jsr saveRegsForCmdReturn ; 52D9 remember the merge loop's X and Y for the common exit at $532A
|
|
jsr mirrorCmdCoordsIfRemoteDec; 52DC mirror into our frame and step back one cell, because the drone occupies a 2x2 block
|
|
lda droneArrivalFlags ; 52DF $92F9 tracks the life of the drone
|
|
and #$30 ; 52E2 bit 4 = it reached its target, bit 5 = its kill burst has already been played
|
|
bne L_532A ; 52E4 the drone is finished - ignore stale telemetry
|
|
lda droneControlFlags ; 52E6 $920D drone state flags
|
|
ora #$01 ; 52E9 bit 0 = an enemy drone is currently being tracked on our display
|
|
sta droneControlFlags ; 52EB store the flags back
|
|
ldy currentScreen ; 52EE $90FB: 0 = battlefield, 1 = COMCEN radar screen, 2 = drone screen, 3 = menus
|
|
cpy #$01 ; 52F1 is the radar screen showing?
|
|
bne L_532A ; 52F3 the enemy drone blip only exists on the radar screen
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Move sprite 0, the enemy drone blip, to the drone's cell on the 21x17 radar grid. The sprite is
|
|
; switched off first so that a drone which has flown off the grid simply disappears.
|
|
; ----------------------------------------------------------------------
|
|
lda spriteEnableShadow ; 52F5 $9022 shadow of VIC_SPR_ENA
|
|
and #$FE ; 52F8 clear bit 0 - hide sprite 0 until we know the drone is inside the grid
|
|
sta spriteEnableShadow ; 52FA store the sprite enable shadow back
|
|
ldx cmdParam1 ; 52FD X = drone column
|
|
stx droneViewOriginCol ; 52FF $92F6 = the tracked drone's map column
|
|
ldy cmdParam2 ; 5302 Y = drone row
|
|
sty droneViewOriginRow ; 5304 $92F7 = its map row
|
|
jsr getUnitSpriteCentreOnGrid; 5307 $340B: converts map cell X,Y into the sprite X/Y of that cell's centre on the 21x17 radar grid; C=1 when off-grid
|
|
bcs L_532A ; 530A off the visible grid - leave sprite 0 switched off
|
|
sty spriteYShadow ; 530C $9018 = sprite 0 Y position shadow
|
|
stx spriteXShadow ; 530F $9010 = sprite 0 X position shadow
|
|
lda #$00 ; 5312 0
|
|
sta activeSpriteIndex ; 5314 $91CC = 0 so loadDirectionShapeSprite fills sprite 0's shape at $8A00
|
|
lda cmdParam0 ; 5317 the heading as the sender sees it
|
|
clc ; 5319 clear carry for the add
|
|
adc #$04 ; 531A +4 of the 8 compass points = the opposite direction ...
|
|
cmp #$08 ; 531C past the eighth point?
|
|
bcc L_5322 ; 531E no wrap needed
|
|
sbc #$08 ; 5320 ... modulo 8; reversing the heading is exactly what the 180-degree map mirror demands
|
|
L_5322:
|
|
jsr loadDirectionShapeSprite; 5322 $3340: copy shape A out of the decompressed set at $0200 into sprite 0 and enable it
|
|
lda #$01 ; 5325 VIC colour 1 = white
|
|
sta spriteColourShadow ; 5327 $9008 = sprite 0 colour shadow
|
|
L_532A:
|
|
ldx #$FF ; 532A immediate operand patched by saveRegsForCmdReturn: restore the merge loop's X
|
|
L_532C:
|
|
ldy #$FF ; 532C immediate operand patched by saveRegsForCmdReturn: restore the merge loop's Y
|
|
rts ; 532E back into the middle of the packet merge loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; saveRegsForCmdReturn - Two-instruction helper for the handlers that are called from the middle of
|
|
; the packet merge loop instead of from the table dispatcher: it copies X and Y into the LDX/LDY
|
|
; immediate operands at
|
|
; $532B and $532D, so the shared exit at $532A hands the merger back exactly the loop registers it
|
|
; had.
|
|
; In: X, Y
|
|
; Out: self-modifies $532B and $532D
|
|
; Called from: cmdSessionControl $523E and cmdDroneMove $52D9
|
|
; ----------------------------------------------------------------------
|
|
saveRegsForCmdReturn:
|
|
stx L_532A+1 ; 532F write X into the LDX # operand of the shared exit at $532A
|
|
sty L_532C+1 ; 5332 write Y into the LDY # operand at $532C
|
|
rts ; 5335 done - the exit will now hand the caller back its own registers
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDroneDetonate - Handler of command $8E (3 bytes: column, row). For our own command it clears the
|
|
; 'strike pending' bit 5 of droneControlFlags. If droneArrivalFlags bit 7 is already set the drone
|
|
; died or arrived before the order was exchanged, so the detonation is cancelled by overwriting the
|
|
; $8E byte in the merged buffer with $0E in place - bit 7 clear makes executeTurnCommands skip it
|
|
; while $0E & $3F still gives length 3, so the film keeps a correctly sized no-op. Otherwise the drone
|
|
; is marked detonated, the coordinates are mirrored, sound $11 plays, applyDroneBlast damages the 2x2
|
|
; blast area and the blast-ring sprite is drawn; a remote detonation finally clears our copy of his
|
|
; drone state through the tail at $537E.
|
|
; In: the queued command, cmdFromOpponent zp_76, droneArrivalFlags $92F9, filmPlaybackMode $0B9B
|
|
; Out: $92F9, $920D, the command byte in mergedCmdBuffer, sound, blast damage and the marker sprite
|
|
; Called from: commandHandlerTable slot for $8E ($4D46)
|
|
; ----------------------------------------------------------------------
|
|
cmdDroneDetonate:
|
|
jsr readCmdArgs2 ; 5336 cmdParam1 = target column, cmdParam2 = target row; read index += 3
|
|
lda cmdFromOpponent ; 5339 whose drone is detonating?
|
|
bmi L_5345 ; 533B the opponent's - nothing local to clean up
|
|
lda droneControlFlags ; 533D $920D drone state flags
|
|
and #$DF ; 5340 clear bit 5 = 'a strike of ours is pending' (strikeWithDrone set it at $8187)
|
|
sta droneControlFlags ; 5342 store the flags back
|
|
L_5345:
|
|
lda droneArrivalFlags ; 5345 $92F9 bit 7 = the drone has already finished its flight
|
|
bpl L_5354 ; 5348 still airborne, so the strike counts
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The drone had already finished when the detonate order came round, so cancel the command in place
|
|
; instead of executing it.
|
|
; ----------------------------------------------------------------------
|
|
ldy mergedCmdReadIndex ; 534A readCmdArgs2 left the read index 3 past the command byte ...
|
|
lda #$0E ; 534D $0E has bit 7 clear so executeTurnCommands skips it, and $0E & $3F still yields length 3
|
|
sta outgoingCmdRingEnd,y ; 534F $908C+readIndex is mergedCmdBuffer[command byte]: neuter the $8E in place, so the film records a correctly sized no-op
|
|
bne L_537E ; 5352 always taken (A = $0E)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A real detonation: mark the drone as burst, damage the 2x2 blast area and draw the ring.
|
|
; ----------------------------------------------------------------------
|
|
L_5354:
|
|
lda #$40 ; 5354 $40 = the drone has detonated
|
|
sta droneArrivalFlags ; 5356 $92F9 drone life flags
|
|
jsr mirrorCmdCoordsIfRemoteDec; 5359 mirror the 2x2 blast origin into our frame for a remote strike
|
|
lda filmPlaybackMode ; 535C $0B9B bit 7 = film playback
|
|
bmi L_5365 ; 535F during playback everything is audible
|
|
lda cmdFromOpponent ; 5361 whose strike is it?
|
|
bpl L_536A ; 5363 our own strike is silent here - the drone screen already made its noise
|
|
L_5365:
|
|
lda #$11 ; 5365 sound $11 = the drone explosion
|
|
jsr playSound ; 5367 hand it to the sound driver
|
|
L_536A:
|
|
lda #$19 ; 536A $19 = 25 points of blast damage
|
|
jsr applyDroneBlast ; 536C overlay $72A1: damage whatever stands in the four cells of the 2x2 blast (also mapGenerator6F00:blobWriteTile)
|
|
ldx cmdParam1 ; 536F blast column
|
|
ldy cmdParam2 ; 5371 blast row
|
|
lda #$01 ; 5373 marker style 1 = the drone burst ring (offsets 8,9 and colour 4 from the table at $33C2)
|
|
jsr showExplosionMarker ; 5375 $33C8: put the blast-ring sprite on that cell and start its 90-frame timer
|
|
jmp L_537E ; 5378 join the shared tail
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDroneRelease - Handler of the 1-byte command $8F, queued by releaseDroneControl ($8230) when the
|
|
; player lets go of the drone. It only steps the read index and falls into the tail shared with
|
|
; cmdDroneDetonate, which for a remote command clears bits 0-5 of droneControlFlags - our copy of the
|
|
; opponent's drone state - while bits 6-7, which describe our own drone flight, are preserved.
|
|
; In: cmdFromOpponent zp_76, droneControlFlags $920D
|
|
; Out: mergedCmdReadIndex += 1; $920D &= $C0 for a remote command
|
|
; Called from: commandHandlerTable slot for $8F ($4D48); the tail at $537E also from cmdDroneDetonate
|
|
; ----------------------------------------------------------------------
|
|
cmdDroneRelease:
|
|
inc mergedCmdReadIndex ; 537B 1-byte command $8F: step the read index over it
|
|
L_537E:
|
|
lda cmdFromOpponent ; 537E shared tail of $8E and $8F: whose drone was it?
|
|
bpl L_538A ; 5380 our own drone state belongs to the drone screen, not here
|
|
lda droneControlFlags ; 5382 $920D drone state flags
|
|
and #$C0 ; 5385 keep bits 6-7 (our own flight) and clear bits 0-5, our copy of his drone state
|
|
sta droneControlFlags ; 5387 store the flags back
|
|
L_538A:
|
|
rts ; 538A done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDroneShotDown - Handler of the 1-byte command $92. The only place that ever queues it is
|
|
; checkStrikeHitsDrone ($79CC), which runs right after a COMCEN missile is fired and asks whether the
|
|
; aim point falls on one of the 2x2 cells the tracked drone occupies: $92 therefore means 'a missile
|
|
; has shot the drone down', not 'the drone arrived'. It clears bits 6-7 of droneControlFlags (no drone
|
|
; flight in progress any more) and, for our own command, sets bit 4 of droneArrivalFlags so
|
|
; showDroneKillBurst ($79DD) plays the kill effect on the next frame. Both packet mergers additionally
|
|
; set droneArrivalFlags = $80 the moment a $92 is seen in either packet, and sendOutgoingPacket drops
|
|
; a queued $92 when the drone detonated first.
|
|
; In: cmdFromOpponent zp_76, droneControlFlags $920D, droneArrivalFlags $92F9
|
|
; Out: $920D &= $3F; $92F9 |= $10 for an own command; mergedCmdReadIndex += 1
|
|
; Called from: commandHandlerTable slot for $92 ($4D4E)
|
|
; (the survey called this cmdDroneArrived; renamed because $92 is the shot-down report, and the drone
|
|
; reaching its target is reported with $8E/$8F instead)
|
|
; ----------------------------------------------------------------------
|
|
cmdDroneShotDown:
|
|
inc mergedCmdReadIndex ; 538B 1-byte command $92: step the read index over it
|
|
lda droneControlFlags ; 538E $920D drone state flags
|
|
and #$3F ; 5391 clear bits 6-7: no drone flight is in progress any more
|
|
sta droneControlFlags ; 5393 store the flags back
|
|
lda cmdFromOpponent ; 5396 who fired the missile that killed the drone?
|
|
bmi L_53A2 ; 5398 the opponent scored the kill - nothing else to record here
|
|
lda droneArrivalFlags ; 539A $92F9 drone life flags
|
|
ora #$10 ; 539D bit 4 = we shot a drone down; showDroneKillBurst ($79DD) picks this up next frame and plays the effect
|
|
sta droneArrivalFlags ; 539F store the flags back
|
|
L_53A2:
|
|
rts ; 53A2 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDroneLaunch - Handler of the 1-byte command $97: droneArrivalFlags = 0 for a fresh drone in the
|
|
; air. The opponent's launch also raises the 'DRONE ALERT!' message with sound through overlay B's
|
|
; showDroneAlert. Either way, if that side still has drones one is spent and markComcenRevealed
|
|
; exposes the launching COMCEN for six rounds - the price of flying a drone.
|
|
; In: cmdFromOpponent zp_76, dronesLeft $92AE
|
|
; Out: $92F9 = 0, $92AE[side] decremented, COMCEN reveal timer and visibility bits via $403B
|
|
; Called from: commandHandlerTable slot for $97 ($4D58)
|
|
; ----------------------------------------------------------------------
|
|
cmdDroneLaunch:
|
|
inc mergedCmdReadIndex ; 53A3 1-byte command $97: step the read index over it
|
|
ldx #$00 ; 53A6 index 0 = this machine's allowances
|
|
stx droneArrivalFlags ; 53A8 $92F9 = 0: a fresh drone is in the air
|
|
lda cmdFromOpponent ; 53AB who launched?
|
|
bpl L_53B4 ; 53AD our own launch - the drone screen has already told the player
|
|
jsr showDroneAlert ; 53AF overlay $820F: sound 7 plus message $20 'DRONE ALERT!'
|
|
ldx #$01 ; 53B2 index 1 = the opponent's allowances
|
|
L_53B4:
|
|
lda dronesLeft,x ; 53B4 $92AE[side] = drones that side still has
|
|
beq L_53BF ; 53B7 already out of drones - nothing to charge
|
|
dec dronesLeft,x ; 53B9 spend one drone
|
|
jsr markComcenRevealed ; 53BC $403B: launching exposes that side's COMCEN for 6 rounds (X selects the side)
|
|
L_53BF:
|
|
rts ; 53BF done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdMissileStrike - Handler of command $90 (3 bytes), which carries two different payloads. During a
|
|
; setup phase (gamePhase non-zero) the two bytes are the sender's drone and missile allowances and are
|
|
; stored in dronesLeft/missilesLeft for that side - this is what overlay A's editDronesAndMissiles
|
|
; sends. During the battle they are the target cell of a COMCEN missile: if the firing side still has
|
|
; missiles and the game has not ended, one missile is spent, markComcenRevealed exposes the firing
|
|
; COMCEN, the coordinates are mirrored for a remote shot and the unit standing on the cell takes 14
|
|
; points of damage, halved to 7 for a COMCEN. The blast-ring sprite is drawn in either case and sound
|
|
; $0C plays unless it is our own live shot.
|
|
; In: the queued command, gamePhase zp_B1, cmdFromOpponent zp_76, gameEndReason $0BA8, the $F000 map
|
|
; Out: $92AE/$92B0[side], unit energy and score via $3B22, the blast-ring sprite, sound
|
|
; Called from: commandHandlerTable slot for $90 ($4D4A)
|
|
; ----------------------------------------------------------------------
|
|
cmdMissileStrike:
|
|
jsr readCmdArgs2 ; 53C0 read the two payload bytes into cmdParam1/cmdParam2; read index += 3
|
|
lda gamePhase ; 53C3 zp_B1 is zero only while the battle runs
|
|
beq L_53D9 ; 53C5 battle: the payload is a target cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Setup phase: the two bytes are the sender's drone and missile allowances, not a target cell.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$00 ; 53C7 setup phase: the payload is the sender's drone/missile allowance; index 0 = us
|
|
lda cmdFromOpponent ; 53C9 who sent it?
|
|
bpl L_53CE ; 53CB our own settings
|
|
inx ; 53CD index 1 = the opponent
|
|
L_53CE:
|
|
lda cmdParam1 ; 53CE first byte = number of drones
|
|
sta dronesLeft,x ; 53D0 $92AE[side]
|
|
lda cmdParam2 ; 53D3 second byte = number of missiles
|
|
sta missilesLeft,x ; 53D5 $92B0[side]
|
|
rts ; 53D8 allowances stored
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Battle phase: the two bytes are the target cell of a COMCEN missile.
|
|
; ----------------------------------------------------------------------
|
|
L_53D9:
|
|
ldx #$00 ; 53D9 battle: index 0 = our own missile stock
|
|
lda cmdFromOpponent ; 53DB who fired?
|
|
bpl L_53E4 ; 53DD our own shot needs no mirroring
|
|
jsr mirrorCmdCoordsIfRemote ; 53DF turn his aim point into our coordinates (39-v)
|
|
ldx #$01 ; 53E2 index 1 = the opponent's missile stock
|
|
L_53E4:
|
|
lda missilesLeft,x ; 53E4 $92B0[side]
|
|
beq L_542D ; 53E7 the firing side has no missiles left - ignore the shot
|
|
lda gameEndReason ; 53E9 $0BA8 is non-zero once the game has ended
|
|
bne L_542D ; 53EC no shots after the final whistle
|
|
dec missilesLeft,x ; 53EE spend one missile
|
|
jsr markComcenRevealed ; 53F1 firing gives the firing COMCEN away for 6 rounds
|
|
ldx cmdParam1 ; 53F4 target column
|
|
cpx #$28 ; 53F6 $28 = 40, the width of the battlefield
|
|
bcs L_5416 ; 53F8 off the map: draw the ring but hit nothing
|
|
ldy cmdParam2 ; 53FA target row
|
|
cpy #$28 ; 53FC $28 = 40, the height of the battlefield
|
|
bcs L_5416 ; 53FE off the map
|
|
jsr readMapCell ; 5400 $25FA: A = the $F000 map byte of column X, row Y
|
|
bpl L_5416 ; 5403 bit 7 clear = plain terrain, nobody is standing there
|
|
and #$7F ; 5405 bits 0-6 = the index of the unit occupying the cell
|
|
tay ; 5407 Y = the victim
|
|
lda #$0E ; 5408 $0E = 14 points of missile damage
|
|
cpy #$31 ; 540A unit 49 = side 0's COMCEN
|
|
beq L_5412 ; 540C halve it
|
|
cpy #$63 ; 540E unit 99 = side 1's COMCEN
|
|
bne L_5413 ; 5410 an ordinary unit takes the full 14
|
|
L_5412:
|
|
lsr a ; 5412 a COMCEN only takes half, 7 points
|
|
L_5413:
|
|
jsr applyHitToUnit ; 5413 $3B22: subtract the energy, score it, explode the unit or knock the COMCEN out
|
|
L_5416:
|
|
ldx cmdParam1 ; 5416 blast column
|
|
ldy cmdParam2 ; 5418 blast row
|
|
lda #$00 ; 541A marker style 0 = the missile blast ring (offsets 10,8 and colour 2 from the table at $33C2)
|
|
jsr showExplosionMarker ; 541C $33C8: draw the ring sprite on the target cell
|
|
lda cmdFromOpponent ; 541F whose shot?
|
|
bmi L_5428 ; 5421 the opponent's shot is always audible
|
|
lda filmPlaybackMode ; 5423 $0B9B bit 7 = film playback
|
|
bpl L_542D ; 5426 our own shot is silent in live play - the radar screen made its own noise
|
|
L_5428:
|
|
lda #$0C ; 5428 sound $0C = missile impact
|
|
jsr playSound ; 542A hand it to the sound driver
|
|
L_542D:
|
|
rts ; 542D done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSelfDestruct - Handler of command $91 (2 bytes: unit; bit 7 of the argument means 'the whole
|
|
; group this unit belongs to'). A plain unit index falls straight into destroyUnit. For a group the
|
|
; match value (unitFlagsTable & $3C) | $40 is patched into the CMP operand at $544D and units 99..0
|
|
; are scanned, calling destroyUnit on every member; because the $3C mask keeps the side bit 5, a group
|
|
; can never span both armies.
|
|
; In: the queued command; cmdParam0 = unit index, bit 7 = group mode; unitFlagsTable $F708
|
|
; Out: unitTypeTable $F76C entries set to $FF; self-modifies $544E; cmdParam0 reused as the loop
|
|
; counter
|
|
; Called from: commandHandlerTable slot for $91 ($4D4C)
|
|
; ----------------------------------------------------------------------
|
|
cmdSelfDestruct:
|
|
jsr readCmdArg1 ; 542E cmdParam0 = the unit index; bit 7 set means 'the whole group this unit belongs to'
|
|
lda cmdParam0 ; 5431 look at the argument
|
|
bpl destroyUnit ; 5433 a plain unit index: destroy just that one and return through destroyUnit
|
|
and #$7F ; 5435 strip the 'whole group' flag to get the unit index
|
|
tay ; 5437 Y = the named unit
|
|
lda unitFlagsTable,y ; 5438 its $F708 flags
|
|
and #$3C ; 543B group id in bits 2-4 plus the side bit 5, so a group can never span both armies
|
|
ora #$40 ; 543D bit 6 = 'in a group', which every member carries
|
|
sta L_544D+1 ; 543F patch the CMP immediate of the scan loop with the group signature to look for
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Scan units 99..0 and destroy every living member whose group signature matches the one patched into
|
|
; the CMP at $544D. cmdParam0 doubles as the loop counter because destroyUnit reads its victim from
|
|
; there.
|
|
; ----------------------------------------------------------------------
|
|
lda #$63 ; 5442 $63 = 99, the highest unit index
|
|
sta cmdParam0 ; 5444 reuse cmdParam0 as the loop counter, because destroyUnit reads its victim from there
|
|
L_5446:
|
|
ldy cmdParam0 ; 5446 Y = the unit currently being tested
|
|
lda unitFlagsTable,y ; 5448 its $F708 flags
|
|
and #$7C ; 544B group id, side and in-group bit
|
|
L_544D:
|
|
cmp #$FF ; 544D immediate operand patched at $543F with the group signature
|
|
bne L_5454 ; 544F not a member of the group
|
|
jsr destroyUnit ; 5451 blow this member up
|
|
L_5454:
|
|
dec cmdParam0 ; 5454 step down one unit
|
|
bpl L_5446 ; 5456 keep going until unit 0 has been tested
|
|
rts ; 5458 the whole group is gone
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; destroyUnit - Removes unit cmdParam0 from play. A COMCEN (unit 49 or 99) is never allowed to
|
|
; self-destruct and returns unchanged. Everything else takes 52 points of damage through sub_3B52 -
|
|
; more than the 50-point maximum energy, so the kill is certain and runs through the normal explosion,
|
|
; sound, score and redraw path - and then has its slot freed outright by writing $FF into
|
|
; unitTypeTable.
|
|
; In: cmdParam0 zp_73 = unit index
|
|
; Out: unitTypeTable $F76C[unit] = $FF plus all the side effects of sub_3B52; Y = unit index
|
|
; Called from: cmdSelfDestruct (branch at $5433 and JSR at $5451)
|
|
; ----------------------------------------------------------------------
|
|
destroyUnit:
|
|
ldy cmdParam0 ; 5459 Y = the unit to destroy (also the loop counter of cmdSelfDestruct)
|
|
cpy #$31 ; 545B unit 49 = side 0's COMCEN
|
|
beq L_546F ; 545D a COMCEN may never self-destruct
|
|
cpy #$63 ; 545F unit 99 = side 1's COMCEN
|
|
beq L_546F ; 5461 likewise protected
|
|
lda #$34 ; 5463 $34 = 52 points, more than the 50-point maximum energy, so the kill is certain
|
|
jsr applyHitToUnitNoAlert ; 5465 the damage path of applyHitToUnit without its attacker/SPY preamble: explosion, sound, score, redraw
|
|
ldy cmdParam0 ; 5468 reload the unit index (sub_3B52 clobbers Y)
|
|
lda #$FF ; 546A $FF = free slot, bit 7 set = dead
|
|
sta unitTypeTable,y ; 546C $F76C: release the record so nothing draws or scans it again
|
|
L_546F:
|
|
rts ; 546F done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdMoveKeepGroup - Handler of command $98 MOVE TO GOAL (4 bytes: unit, column, row), the order
|
|
; issued to each member of a group in turn by $4C0B. It reads and mirrors the arguments and jumps to
|
|
; setGroupMemberDestination ($4C59), which counts the order off pendingGroupOrderCount when the unit
|
|
; belongs to the group being ordered and then sets the destination through $34D5 without disturbing
|
|
; the group bits - unlike command $80, which drops the unit out of its group.
|
|
; In: the queued command; cmdParam0 = unit, cmdParam1/cmdParam2 = goal column/row
|
|
; Out: destination tables of the unit, pendingGroupOrderCount $929E; read index += 4
|
|
; Called from: commandHandlerTable slot for $98 ($4D5A)
|
|
; ----------------------------------------------------------------------
|
|
cmdMoveKeepGroup:
|
|
jsr readCmdArgs3 ; 5470 cmdParam0 = unit, cmdParam1/cmdParam2 = goal column/row; read index += 4
|
|
jsr mirrorCmdCoordsIfRemote ; 5473 mirror a remote player's goal into our coordinates
|
|
jmp setGroupMemberDestination; 5476 $4C59: count the order off pendingGroupOrderCount and set the destination without touching the group bits
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdAddEnergy - Handler of command $9A (3 bytes: unit, amount), sent by the COMCEN energy repair on
|
|
; the REPAIR tab. Unless the game has already ended by a COMCEN knock-out ($0BA8 bit 7) the amount is
|
|
; added to bits 0-5 of the unit's unitEnergyTable entry and saturated at 50; the two visibility bits
|
|
; 6-7 are saved into the ORA operand at $5498 and put back afterwards.
|
|
; In: the queued command; cmdParam1 = unit, cmdParam2 = energy to add; gameEndReason $0BA8
|
|
; Out: unitEnergyTable $F9C4[unit]; self-modifies $5499; read index += 3
|
|
; Called from: commandHandlerTable slot for $9A ($4D5E)
|
|
; ----------------------------------------------------------------------
|
|
cmdAddEnergy:
|
|
jsr readCmdArgs2 ; 5479 cmdParam1 = the unit, cmdParam2 = the energy to add; read index += 3
|
|
ldy cmdParam1 ; 547C Y = the unit being repaired
|
|
lda gameEndReason ; 547E $0BA8 bit 7 set = a COMCEN has been knocked out
|
|
bmi L_549D ; 5481 no repairs once the game has been decided
|
|
lda unitEnergyTable,y ; 5483 $F9C4: bits 0-5 energy 0-50, bits 6-7 = spotted / visible flags
|
|
tax ; 5486 keep the whole byte in X
|
|
and #$C0 ; 5487 isolate the two visibility bits
|
|
sta L_5498+1 ; 5489 patch them into the ORA immediate at $5498 so they survive the rewrite
|
|
txa ; 548C get the byte back
|
|
and #$3F ; 548D the current energy on its own
|
|
clc ; 548F clear carry for the add
|
|
adc cmdParam2 ; 5490 add the repair amount
|
|
cmp #$32 ; 5492 $32 = 50 = full energy
|
|
bcc L_5498 ; 5494 still below the cap
|
|
lda #$32 ; 5496 saturate at 50
|
|
L_5498:
|
|
ora #$FF ; 5498 immediate operand patched at $5489 with the saved visibility bits
|
|
sta unitEnergyTable,y ; 549A store the repaired energy
|
|
L_549D:
|
|
rts ; 549D done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdHaltGroup - Handler of command $9B (2 bytes: group byte), the group HALT order. Units 99..0 are
|
|
; scanned and for every living member (unitFlagsTable & $7C equal to the group byte | $40) the current
|
|
; position is copied into the destination and waypoint, the two 'arrived' bits (unitDestColTable bit 7
|
|
; and unitDisplayFlagsTable bit 7) are set, the cloak bits of the waypoint column are preserved
|
|
; through the ORA operand patched at $54CF, and the blitz bits of the waypoint row are dropped because
|
|
; a plain row value is written over them.
|
|
; In: the queued command; cmdParam0 = group byte; unitTypeTable $F76C, unitFlagsTable $F708
|
|
; Out: $F7D0, $F834, $F898, $F8FC and $FB54 of every member; self-modifies $54D0; read index += 2
|
|
; Called from: commandHandlerTable slot for $9B ($4D60)
|
|
; ----------------------------------------------------------------------
|
|
cmdHaltGroup:
|
|
jsr readCmdArg1 ; 549E cmdParam0 = the group byte (id in bits 2-4, side in bit 5); read index += 2
|
|
lda cmdParam0 ; 54A1 the group byte
|
|
ora #$40 ; 54A3 bit 6 = 'in a group', so the byte can be compared with unitFlagsTable & $7C directly
|
|
sta cmdParam0 ; 54A5 keep it as the match value
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk units 99..0 and stop every living member of the group where it stands.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$63 ; 54A7 start at unit 99 and walk down to 0
|
|
L_54A9:
|
|
lda unitTypeTable,y ; 54A9 $F76C, bit 7 set = dead or free slot
|
|
bmi L_54DD ; 54AC skip dead units
|
|
lda unitFlagsTable,y ; 54AE the unit's $F708 flags
|
|
and #$7C ; 54B1 group id, side and in-group bit
|
|
cmp cmdParam0 ; 54B3 does it belong to the group being halted?
|
|
bne L_54DD ; 54B5 no - next unit
|
|
lda unitColTable,y ; 54B7 the member's current column
|
|
sta L_54CF+1 ; 54BA patch it into the ORA immediate at $54CF
|
|
ora #$80 ; 54BD bit 7 = standing on its destination
|
|
sta unitDestColTable,y ; 54BF $F7D0 destination column = where the unit already is
|
|
lda unitDisplayFlagsTable,y ; 54C2 $FB54 draw flags
|
|
ora #$80 ; 54C5 bit 7 = idle / at destination
|
|
sta unitDisplayFlagsTable,y ; 54C7 store the draw flags back
|
|
lda unitPathColTable,y ; 54CA $F898 waypoint column
|
|
and #$C0 ; 54CD keep only the cloak bits 6-7
|
|
L_54CF:
|
|
ora #$FF ; 54CF immediate operand patched at $54BA with the member's current column
|
|
sta unitPathColTable,y ; 54D1 waypoint = current position, cloak state untouched
|
|
lda unitRowTable,y ; 54D4 the member's current row
|
|
sta unitDestRowTable,y ; 54D7 $F834 destination row = where the unit already is
|
|
sta unitPathRowTable,y ; 54DA $F8FC waypoint row likewise; a row is below 64, so this also clears the blitz bits 6-7
|
|
L_54DD:
|
|
dey ; 54DD step down one unit
|
|
bpl L_54A9 ; 54DE continue until unit 0 has been tested
|
|
rts ; 54E0 the whole group has stopped
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetRecycler - Handler of command $9C (4 bytes: mode, column, row), sent by overlay A's
|
|
; chooseRecycler ($8436) during the setup of the game types that have a recycler. recyclerMode[side]
|
|
; takes the mode, with side = the sender's absolute side (playerSide, EOR 1 when the command is
|
|
; remote); for modes 0 and 1 the coordinates are mirrored for a remote sender and stored in
|
|
; recyclerCol/recyclerRow[side]. Our own recycler is additionally painted onto the overview map, the
|
|
; enemy one is not - it has to be spotted first.
|
|
; In: the queued command; cmdParam0 = mode, cmdParam1/2 = column/row; playerSide $0B9F,
|
|
; cmdFromOpponent
|
|
; Out: $92A6/$92A8/$92AA indexed by side, map redraw via $3F27; self-modifies $550C; read index += 4
|
|
; Called from: commandHandlerTable slot for $9C ($4D62)
|
|
; ----------------------------------------------------------------------
|
|
cmdSetRecycler:
|
|
jsr readCmdArgs3 ; 54E1 cmdParam0 = mode, cmdParam1/cmdParam2 = column/row; read index += 4
|
|
lda playerSide ; 54E4 $0B9F = the side this machine plays
|
|
ldy cmdFromOpponent ; 54E7 who sent the command?
|
|
bpl L_54ED ; 54E9 our own choice
|
|
eor #$01 ; 54EB remote: the sender is the other side
|
|
L_54ED:
|
|
tay ; 54ED Y = the sender's absolute side, the index of the per-side recycler tables
|
|
sta L_550B+1 ; 54EE also stash it in the LDY immediate at $550B, because Y is reused before the draw call
|
|
lda cmdParam0 ; 54F1 the chosen recycler mode
|
|
sta recyclerMode,y ; 54F3 $92A6[side]
|
|
cmp #$02 ; 54F6 modes 2 and up carry no coordinates
|
|
bcs L_54FD ; 54F8 skip the mirror for those
|
|
jsr mirrorCmdCoordsIfRemote ; 54FA mirror a remote placement into our coordinate frame
|
|
L_54FD:
|
|
lda cmdParam1 ; 54FD recycler column
|
|
sta recyclerCol,y ; 54FF $92A8[side]
|
|
lda cmdParam2 ; 5502 recycler row
|
|
sta recyclerRow,y ; 5504 $92AA[side]
|
|
lda cmdFromOpponent ; 5507 whose recycler is it?
|
|
bmi L_5510 ; 5509 the enemy recycler stays hidden until it is spotted
|
|
L_550B:
|
|
ldy #$FF ; 550B immediate operand patched at $54EE with our own side index
|
|
jsr drawRecyclerOnMap ; 550D $3F27: write the recycler glyph $67 into the map cell (or under whichever unit stands there)
|
|
L_5510:
|
|
rts ; 5510 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdChooseSide - Handler of command $9D (2 bytes: the sender's side-preference token), the
|
|
; side-selection handshake. In a solo game playerSide is simply forced to 0. In a modem game only the
|
|
; opponent's copy of the command matters: our own token was parked in $90F8 by sendChooseSide, and the
|
|
; player with the higher token becomes side 1 (the carry of the CPY is rotated into A). gamePhase is
|
|
; then set to $DF, whose clear bit 5 releases the wait loop at $797C. If both tokens are identical
|
|
; nobody wins: a fresh random token with bit 7 set is drawn and the code falls into sendChooseSide to
|
|
; send command $9D again, leaving gamePhase untouched so the loop keeps waiting. That only terminates
|
|
; because the two machines' game RNGs have drifted apart before a game starts (they are forced into
|
|
; step only once the map seed is exchanged).
|
|
; In: the queued command; cmdParam0 = his token, selectedUnit $90F8 = ours, isSoloTrainer $0BA5,
|
|
; zp_76
|
|
; Out: playerSide $0B9F = 0 or 1 and gamePhase zp_B1 = $DF, or a retry through sendChooseSide
|
|
; Called from: commandHandlerTable slot for $9D ($4D64)
|
|
; ----------------------------------------------------------------------
|
|
cmdChooseSide:
|
|
jsr readCmdArg1 ; 5511 cmdParam0 = the sender's side-preference token (0 or $FF from the setup screen, or a random tie-break byte)
|
|
lda isSoloTrainer ; 5514 $0BA5 bit 7 = PRACTICE WITH SOLO TRAINER
|
|
bpl L_551D ; 5517 modem game: settle it against the opponent
|
|
lda #$00 ; 5519 solo: the human always plays side 0
|
|
beq L_552B ; 551B always taken
|
|
L_551D:
|
|
lda cmdFromOpponent ; 551D which copy of the command is this?
|
|
bpl L_5532 ; 551F our own copy tells us nothing we did not already know
|
|
lda #$00 ; 5521 build the resulting side number in A
|
|
ldy selectedUnit ; 5523 $90F8 holds our own token, parked there by sendChooseSide
|
|
cpy cmdParam0 ; 5526 compare it with his; carry set means our token is the higher one
|
|
beq L_5533 ; 5528 identical tokens - nobody wins, so re-pick
|
|
rol a ; 552A A = the carry: 1 when our token was higher, so we take side 1
|
|
L_552B:
|
|
sta playerSide ; 552B $0B9F is 0 or 1 from now on
|
|
lda #$DF ; 552E $DF clears bit 5 of the phase byte ...
|
|
sta gamePhase ; 5530 ... which is the bit the side-selection loop at $797C spins on
|
|
L_5532:
|
|
rts ; 5532 the side handshake is finished
|
|
|
|
L_5533:
|
|
jsr nextGameRandom ; 5533 $FD5D: draw a fresh token; the two RNG states have drifted apart before a game starts, so the values probably differ
|
|
ora #$80 ; 5536 bit 7 marks it as a tie-break retry token
|
|
sta playerSide ; 5538 park it in playerSide, from where sendChooseSide picks it up below (fall-through)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendChooseSide - The sending half of the $9D exchange: copies playerSide into cmdParam0 and into
|
|
; selectedUnit $90F8 (where cmdChooseSide later finds our own token) and queues the 2-byte command
|
|
; $9D. At this point playerSide still holds a preference token - 0 or $FF chosen by the setup screen,
|
|
; or a random byte with bit 7 set after a tie - not yet a side number.
|
|
; In: playerSide $0B9F
|
|
; Out: cmdParam0 zp_73, selectedUnit $90F8, two bytes appended to the outgoing command ring
|
|
; Called from: overlay A's side-selection screen ($7976), and by fall-through from cmdChooseSide's tie
|
|
; path
|
|
; ----------------------------------------------------------------------
|
|
sendChooseSide:
|
|
lda playerSide ; 553B our own preference token (0 or $FF from the setup screen, or a tie-break byte)
|
|
sta cmdParam0 ; 553E the argument byte of command $9D
|
|
sta selectedUnit ; 5540 $90F8 doubles as the 'our own pick' scratch that cmdChooseSide compares against
|
|
lda #$9D ; 5543 command $9D CHOOSE SIDE
|
|
jmp queueCmd2 ; 5545 append the 2-byte command to the outgoing ring
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDigIn - Handler of command $9F DIG IN (2 bytes: unit): calls setUnitDigBits with $20, i.e.
|
|
; unitStunTable bits 5-7 = 001. The per-unit step code then advances those bits by $20 for every round
|
|
; the unit stays still until it counts as fortified.
|
|
; In: the queued command; cmdParam0 = unit after setUnitDigBits
|
|
; Out: unitStunTable $F960[unit] = (old & $1F) | $20
|
|
; Called from: commandHandlerTable slot for $9F ($4D68)
|
|
; ----------------------------------------------------------------------
|
|
cmdDigIn:
|
|
lda #$20 ; 5548 $20 = unitStunTable bits 5-7 set to 001, 'digging in'
|
|
bne setUnitDigBits ; 554A always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdDigOut - Handler of command $A0 DIG OUT (2 bytes: unit): setUnitDigBits with 0 clears the
|
|
; dig-progress bits 5-7 of unitStunTable, and bit 6 of unitTypeTable - the 'fortified' flag the combat
|
|
; code consults - is cleared as well.
|
|
; In: the queued command; cmdParam0 = unit
|
|
; Out: unitStunTable $F960[unit] &= $1F, unitTypeTable $F76C[unit] &= $BF
|
|
; Called from: commandHandlerTable slot for $A0 ($4D6A)
|
|
; ----------------------------------------------------------------------
|
|
cmdDigOut:
|
|
lda #$00 ; 554C 0 = unitStunTable bits 5-7 cleared, neither digging nor dug in
|
|
jsr setUnitDigBits ; 554E clear the dig bits and leave Y = the unit
|
|
lda unitTypeTable,y ; 5551 $F76C of that unit
|
|
and #$BF ; 5554 clear bit 6 = 'fortified', the flag the combat code consults
|
|
sta unitTypeTable,y ; 5556 store the type byte back
|
|
rts ; 5559 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setUnitDigBits - Shared prologue of cmdDigIn and cmdDigOut: patches the new dig bits into the ORA
|
|
; operand at $5567, reads the unit argument, leaves it in Y and falls into applyDigBitsToUnit.
|
|
; In: A = the new bits 5-7 ($20 or $00); the queued command
|
|
; Out: self-modifies $5568; cmdParam0 and Y = the unit; unitStunTable[unit] rewritten
|
|
; Called from: cmdDigIn (branch at $554A) and cmdDigOut ($554E)
|
|
; ----------------------------------------------------------------------
|
|
setUnitDigBits:
|
|
sta L_5567+1 ; 555A patch the new dig bits into the ORA immediate at $5567
|
|
jsr readCmdArg1 ; 555D cmdParam0 = the unit index; read index += 2
|
|
ldy cmdParam0 ; 5560 Y = the unit, and it stays there for cmdDigOut
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyDigBitsToUnit - The two-line worker shared by the single-unit and the group dig handlers:
|
|
; unitStunTable[Y] = (old & $1F) | patchedBits, so the low-nibble stun counter and bit 4 survive while
|
|
; the dig-state bits 5-7 are replaced wholesale.
|
|
; In: Y = unit index; the ORA operand at $5568 patched by setUnitDigBits or by the group entry at
|
|
; $5573
|
|
; Out: unitStunTable $F960[Y]
|
|
; Called from: the group loop at $558F, and by fall-through from setUnitDigBits
|
|
; ----------------------------------------------------------------------
|
|
applyDigBitsToUnit:
|
|
lda unitStunTable,y ; 5562 $F960: low nibble = stun counter, bit 4 spare, bits 5-7 = dig state
|
|
and #$1F ; 5565 keep the stun counter and bit 4
|
|
L_5567:
|
|
ora #$FF ; 5567 immediate operand patched at $555A or $5573 with the new dig bits
|
|
sta unitStunTable,y ; 5569 store the new dig state
|
|
rts ; 556C done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdGroupDigIn - Handler of command $A1 (2 bytes: group byte): patches the dig bits to $20 and runs
|
|
; the shared group loop that follows.
|
|
; In: the queued command; cmdParam0 = group byte
|
|
; Out: unitStunTable $F960 of every living member = (old & $1F) | $20
|
|
; Called from: commandHandlerTable slot for $A1 ($4D6C)
|
|
; ----------------------------------------------------------------------
|
|
cmdGroupDigIn:
|
|
lda #$20 ; 556D $20 = dig bits 001 for every member
|
|
bne L_5573 ; 556F always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdGroupDigOut - Handler of command $A2 (2 bytes: group byte) and the entry of the shared group dig
|
|
; loop: patches the dig bits to 0, reads the group byte, ORs $40 into it so it can be compared with
|
|
; unitFlagsTable & $7C, and walks units 99..0 calling applyDigBitsToUnit for every living member.
|
|
; Unlike cmdDigOut it never clears bit 6 of unitTypeTable, so a group dug in as a group can only be
|
|
; un-fortified one unit at a time.
|
|
; In: the queued command; cmdParam0 = group byte; unitTypeTable $F76C, unitFlagsTable $F708
|
|
; Out: unitStunTable $F960 of every member; self-modifies $5568; read index += 2
|
|
; Called from: commandHandlerTable slot for $A2 ($4D6E), and by branch from cmdGroupDigIn
|
|
; ----------------------------------------------------------------------
|
|
cmdGroupDigOut:
|
|
lda #$00 ; 5571 0 = clear the dig bits of every member
|
|
L_5573:
|
|
sta L_5567+1 ; 5573 patch the ORA immediate shared with applyDigBitsToUnit
|
|
jsr readCmdArg1 ; 5576 cmdParam0 = the group byte; read index += 2
|
|
lda cmdParam0 ; 5579 the group byte
|
|
ora #$40 ; 557B bit 6 = 'in a group', so it matches unitFlagsTable & $7C directly
|
|
sta cmdParam0 ; 557D keep it as the match value
|
|
ldy #$63 ; 557F walk units 99 down to 0
|
|
L_5581:
|
|
lda unitTypeTable,y ; 5581 $F76C, bit 7 set = dead or free slot
|
|
bmi L_5592 ; 5584 skip dead units
|
|
lda unitFlagsTable,y ; 5586 the unit's $F708 flags
|
|
and #$7C ; 5589 group id, side and in-group bit
|
|
cmp cmdParam0 ; 558B is it a member of this group?
|
|
bne L_5592 ; 558D no - next unit
|
|
jsr applyDigBitsToUnit ; 558F rewrite this member's dig bits
|
|
L_5592:
|
|
dey ; 5592 step down one unit
|
|
bpl L_5581 ; 5593 continue until unit 0 has been tested
|
|
rts ; 5595 note that, unlike cmdDigOut, this never clears bit 6 of unitTypeTable
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetFormation - Handler of command $A3 (3 bytes: group leader unit, rotation mode 0-2): reads the
|
|
; two arguments and jumps to rotateGroupFormation ($4C84), which transforms every member's offset from
|
|
; the leader's destination with the rule pair at $4C7E/$4C81 and re-issues the destinations.
|
|
; In: the queued command; cmdParam1 = leader unit index, cmdParam2 = rotation mode
|
|
; Out: the destinations of all the leader's group members; read index += 3
|
|
; Called from: commandHandlerTable slot for $A3 ($4D70)
|
|
; ----------------------------------------------------------------------
|
|
cmdSetFormation:
|
|
jsr readCmdArgs2 ; 5596 cmdParam1 = the group leader's unit index, cmdParam2 = rotation mode 0-2; read index += 3
|
|
jmp rotateGroupFormation ; 5599 $4C84: rotate every member's offset from the leader using the rules at $4C7E
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetGameType - Handler of command $A4 (2 bytes): stores the argument into gameTypeOptions ($0BA3),
|
|
; which carries the game type in bits 0-2, the UNIT MENUS and DAMAGE switches in bits 3-4, CUSTOM
|
|
; rules in bit 6 and the custom-map flag in bit 7. Sent by overlay A's runMapTypeMenu ($833E) so both
|
|
; machines agree on the scenario before the map is generated.
|
|
; In: the queued command; cmdParam0 = the options byte
|
|
; Out: gameTypeOptions $0BA3; read index += 2
|
|
; Called from: commandHandlerTable slot for $A4 ($4D72)
|
|
; ----------------------------------------------------------------------
|
|
cmdSetGameType:
|
|
jsr readCmdArg1 ; 559C cmdParam0 = the options byte; read index += 2
|
|
lda cmdParam0 ; 559F the options byte
|
|
sta gameTypeOptions ; 55A1 $0BA3: bits 0-2 game type, bit 3 UNIT MENUS, bit 4 DAMAGE, bit 6 CUSTOM rules, bit 7 custom map
|
|
rts ; 55A4 both machines now agree on the scenario
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cmdSetupOption - Handler of command $A5 (3 bytes: kind, value), the generic carrier of overlay A's
|
|
; custom-rules editors. Kind 0 (HANDICAP) adds value*10 to the 16-bit counter $92A2/$92A4 indexed by
|
|
; the sender's absolute side; because that counter holds the points scored against a side, this is a
|
|
; head start credited to the sender's opponent, and the index has to be restored from the operand at
|
|
; $55C1 because multiply8x8 returns with X = 0. Kind 1 (TERRAIN PTS) stores the value in $92AC for the
|
|
; sender's opponent. Kind 2 and above - only kind 3, COMCEN SPEED, is ever sent - stores it in $92B4
|
|
; indexed 0 = us, 1 = him.
|
|
; In: the queued command; cmdParam1 = kind, cmdParam2 = value; playerSide $0B9F, cmdFromOpponent
|
|
; zp_76
|
|
; Out: $92A2/$92A4, $92AC or $92B4; self-modifies $55C2; read index += 3
|
|
; Called from: commandHandlerTable slot for $A5 ($4D74)
|
|
; ----------------------------------------------------------------------
|
|
cmdSetupOption:
|
|
jsr readCmdArgs2 ; 55A5 cmdParam1 = option kind, cmdParam2 = value; read index += 3
|
|
lda playerSide ; 55A8 $0B9F = the side this machine plays
|
|
ldy cmdFromOpponent ; 55AB who sent the option?
|
|
bpl L_55B1 ; 55AD our own setting
|
|
eor #$01 ; 55AF remote: the other side
|
|
L_55B1:
|
|
tax ; 55B1 X = the sender's absolute side
|
|
sta L_55C1+1 ; 55B2 stash it in the LDX immediate at $55C1, because multiply8x8 returns with X = 0
|
|
ldy cmdParam1 ; 55B5 the option kind
|
|
bne L_55D1 ; 55B7 kinds 1 and up
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Kind 0 - HANDICAP: add value*10 to the 16-bit score already credited against the sender's side.
|
|
; ----------------------------------------------------------------------
|
|
lda cmdParam2 ; 55B9 kind 0 = HANDICAP: the value counts in units of ten points
|
|
ldy #$0A ; 55BB multiplier 10
|
|
jsr multiply8x8 ; 55BD $FCCD: A = low byte and Y = high byte of value*10
|
|
clc ; 55C0 clear carry for the 16-bit add
|
|
L_55C1:
|
|
ldx #$FF ; 55C1 immediate operand patched at $55B2: restore the sender's side index
|
|
adc sideScoreLo,x ; 55C3 $92A2/$92A3 = the 16-bit points scored against that side, so this is a head start for his opponent
|
|
sta sideScoreLo,x ; 55C6 store the new low byte
|
|
tya ; 55C9 product high byte
|
|
adc sideScoreHi,x ; 55CA $92A4/$92A5 = the high halves of the two side scores
|
|
sta sideScoreHi,x ; 55CD store the new high byte
|
|
rts ; 55D0 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Kind 1 - TERRAIN PTS: the per-cell bonus for holding enemy ground, stored against the sender's
|
|
; opponent.
|
|
; ----------------------------------------------------------------------
|
|
L_55D1:
|
|
cpy #$02 ; 55D1 kind 2 and up?
|
|
bcs L_55DF ; 55D3 yes - COMCEN SPEED and friends
|
|
txa ; 55D5 kind 1 = TERRAIN PTS
|
|
eor #$01 ; 55D6 the bonus is credited against the sender's opponent
|
|
tax ; 55D8 X = that side
|
|
lda cmdParam2 ; 55D9 the value
|
|
sta terrainPointsBySide,x ; 55DB $92AC[side]: points per cell of enemy ground held, read at $47A9
|
|
rts ; 55DE done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Kind 2 and up - in practice only kind 3, COMCEN SPEED, indexed 0 = us / 1 = the opponent.
|
|
; ----------------------------------------------------------------------
|
|
L_55DF:
|
|
ldx #$00 ; 55DF kind 2 and up (only kind 3, COMCEN SPEED, is ever sent): index 0 = us
|
|
lda cmdFromOpponent ; 55E1 who sent it?
|
|
bpl L_55E6 ; 55E3 our own setting
|
|
inx ; 55E5 index 1 = the opponent
|
|
L_55E6:
|
|
lda cmdParam2 ; 55E6 the value
|
|
sta comcenSpeedClass,x ; 55E8 $92B4/$92B5; the value 4 is what makes $34E1 refuse to move a COMCEN at all
|
|
rts ; 55EB done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueCmdBytePending - Entry used by the 1-byte commands ($8F, $92, $97, $9E): counts one command in
|
|
; pendingOwnCmdCount and falls into queueCmdByte to append the single byte.
|
|
; In: A = the command byte
|
|
; Out: pendingOwnCmdCount $929F incremented, one byte appended to the outgoing ring
|
|
; Called from: $1814 in this file, and $79CC/$809A/$823C in both $6F00 overlays
|
|
; ----------------------------------------------------------------------
|
|
queueCmdBytePending:
|
|
inc pendingOwnCmdCount ; 55EC $929F counts commands queued but not yet executed; this entry is used by the 1-byte commands
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueCmdByte - Appends A to the 40-byte outgoing command ring at $9065, wrapping the write index
|
|
; $908D at 40, and always leaves an $FF end marker one byte past the new tail. It does nothing at all
|
|
; while a game film is being replayed, since the player issues no commands then.
|
|
; In: A = byte, outgoingCmdWriteIndex $908D, filmPlaybackMode $0B9B
|
|
; Out: outgoingCmdRing $9065[], $908D advanced; Y clobbered
|
|
; Called from: queueCmd4/queueCmd3/queueCmd2 ($560A, $5612, $5617, $561C, $5622, $5627)
|
|
; ----------------------------------------------------------------------
|
|
queueCmdByte:
|
|
bit filmPlaybackMode ; 55EF $0B9B bit 7 = a film is being replayed
|
|
bmi L_5609 ; 55F2 during playback the player issues no commands, so drop the byte
|
|
ldy outgoingCmdWriteIndex ; 55F4 $908D = the write cursor of the 40-byte ring at $9065
|
|
sta outgoingCmdRing,y ; 55F7 store the byte in the ring
|
|
iny ; 55FA advance the cursor
|
|
cpy #$28 ; 55FB $28 = 40 bytes in the ring
|
|
bcc L_5601 ; 55FD no wrap needed
|
|
ldy #$00 ; 55FF wrap back to the start
|
|
L_5601:
|
|
sty outgoingCmdWriteIndex ; 5601 save the new write cursor
|
|
lda #$FF ; 5604 $FF = end-of-queue marker
|
|
sta outgoingCmdRing,y ; 5606 always keep a terminator one byte past the last command byte
|
|
L_5609:
|
|
rts ; 5609 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueCmd4 - Queues a 4-byte command: the code in A followed by cmdParam0, cmdParam1 and cmdParam2
|
|
; (used for
|
|
; $80 MOVE, $81, $8A, $8B, $8D, $98, $9C, $A6). It queues the code and cmdParam0 itself and then falls
|
|
; into
|
|
; queueCmd3, which counts the command once and queues the remaining two arguments.
|
|
; In: A = command code, cmdParam0/1/2 zp_73-zp_75
|
|
; Out: four bytes in the outgoing ring, pendingOwnCmdCount $929F incremented once
|
|
; Called from: $183D, $349C, $4A4A, $4C0D, $5646 and overlay A ($79BF, $8438)
|
|
; ----------------------------------------------------------------------
|
|
queueCmd4:
|
|
jsr queueCmdByte ; 560A 4-byte command: queue the command code held in A ...
|
|
lda cmdParam0 ; 560D ... then cmdParam0, and fall into queueCmd3 for the rest
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueCmd3 - Queues a 3-byte command: the code in A followed by cmdParam1 and cmdParam2 (the commands
|
|
; of length 3 are $87, $88, $8E, $90, $9A, $A3 and $A5). It is also the tail of queueCmd4, which
|
|
; arrives here with A = cmdParam0 and its command code already in the ring, which is why the count is
|
|
; incremented here.
|
|
; In: A = command code (or cmdParam0 when entered from queueCmd4), cmdParam1/cmdParam2
|
|
; Out: three bytes in the outgoing ring, pendingOwnCmdCount $929F incremented once
|
|
; Called from: $1939, $3C48, $4ACD, $6630 and both $6F00 overlays ($7667, $81A4, $84E9, $850E, $8544,
|
|
; $85D8)
|
|
; ----------------------------------------------------------------------
|
|
queueCmd3:
|
|
inc pendingOwnCmdCount ; 560F count the command once (queueCmd4 arrives here having already queued its code)
|
|
jsr queueCmdByte ; 5612 queue the command code, or cmdParam0 when entered from queueCmd4
|
|
lda cmdParam1 ; 5615 second argument
|
|
jsr queueCmdByte ; 5617 queue it
|
|
lda cmdParam2 ; 561A third argument
|
|
jmp queueCmdByte ; 561C queue it and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueCmd2 - Queues a 2-byte command: the code in A followed by cmdParam0 (the commands of length 2
|
|
; are the unit, group and reason commands $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2 and $A4).
|
|
; In: A = command code, cmdParam0 zp_73
|
|
; Out: two bytes in the outgoing ring, pendingOwnCmdCount $929F incremented once
|
|
; Called from: $1AA9, $1C22, $20B8, $4B01, $5545, $5FA3, $62DE, $64E1, $65CD, $8340 and $C7A7
|
|
; ----------------------------------------------------------------------
|
|
queueCmd2:
|
|
inc pendingOwnCmdCount ; 561F count the command in pendingOwnCmdCount
|
|
jsr queueCmdByte ; 5622 queue the command code held in A
|
|
lda cmdParam0 ; 5625 its single argument
|
|
jmp queueCmdByte ; 5627 queue it and return to the caller
|
|
|
|
|
|
; outPacketFill: byteTable, 1 bytes. work variable of sendOutgoingPacket (bytes placed in the outgoing
|
|
; packet)
|
|
outPacketFill:
|
|
.byte $00 ; 562A . number of bytes already placed in the outgoing packet by sendOutgoingPacket
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendOutgoingPacket - Builds the packet for the next lock-step exchange and hands it to the comm
|
|
; module. If our drone is under joystick control, is not paused and has not finished, it is first
|
|
; stepped one cell and an $8D heading/column/row report is queued. Whole commands are then copied out
|
|
; of the outgoing ring into the comm module's transmit buffer at $E027 as long as the packet stays
|
|
; below 5 bytes; a $92 is dropped while droneArrivalFlags bit 6 says the drone detonated instead of
|
|
; arriving, and an $8E found while no drone flight is in progress flushes the entire ring and sends an
|
|
; empty packet. Finally $E01F is set to the packet length and the comm module is asked to transmit.
|
|
; In: outgoingCmdRing $9065 with $908D/$908E, droneControlFlags $920D, droneArrivalFlags $92F9,
|
|
; pauseState
|
|
; Out: $E027.., $E01F, outgoingCmdReadIndex $908E, outPacketFill $562A; self-modifies $566B
|
|
; Called from: executeTurnCommands $4F02, disconnectFromOpponent $1C25 and overlay A $7979
|
|
; ----------------------------------------------------------------------
|
|
sendOutgoingPacket:
|
|
bit droneControlFlags ; 562B $920D bit 6 = our own drone is under joystick control
|
|
bvc L_5649 ; 562E no drone of ours to steer
|
|
lda pauseState ; 5630 zp_BE is non-zero while either side is paused
|
|
bne L_5649 ; 5632 do not step the drone while the game is paused
|
|
lda droneArrivalFlags ; 5634 $92F9 bit 7 = the drone has already finished its flight
|
|
bmi L_5649 ; 5637 nothing left to report
|
|
jsr steerDrone ; 5639 overlay $731E: turn droneHeading toward the joystick and move the drone one cell (also mapGenerator6F00:forestBlockClassSw)
|
|
jsr getDroneMapPosition ; 563C overlay $81FE: cmdParam1/cmdParam2 = droneViewOrigin + (6,5) = the drone's own map cell (also mapGenerator6F00:waitFramesServicingComm)
|
|
lda droneHeading ; 563F $920B = the current 0-7 compass heading
|
|
sta cmdParam0 ; 5642 first argument of the report
|
|
lda #$8D ; 5644 command $8D DRONE POSITION
|
|
jsr queueCmd4 ; 5646 queue heading, column and row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fill the outgoing packet from the command ring. Only whole commands are copied and the packet holds
|
|
; at most 4 command bytes, so a long burst of orders is spread over several exchanges.
|
|
; ----------------------------------------------------------------------
|
|
L_5649:
|
|
ldy outgoingCmdReadIndex ; 5649 $908E = the read cursor of the outgoing ring
|
|
lda #$00 ; 564C 0
|
|
sta outPacketFill ; 564E the packet starts out empty
|
|
L_5651:
|
|
cpy outgoingCmdWriteIndex ; 5651 has the read cursor caught the write cursor?
|
|
beq L_56A3 ; 5654 the ring is empty - send what we have
|
|
lda outgoingCmdRing,y ; 5656 the next command byte in the ring
|
|
sta L_566A+1 ; 5659 patch it into the LDA # at $566A so the special-case tests below can look at it
|
|
jsr getCommandLength ; 565C $4D9F: total length of the command from commandLengthTable, also left in $92EE
|
|
clc ; 565F clear carry for the add
|
|
adc outPacketFill ; 5660 how long the packet would be with this command in it
|
|
cmp #$05 ; 5663 a packet carries at most 4 command bytes
|
|
bcs L_56A3 ; 5665 it would not fit - leave it in the ring until next exchange
|
|
ldx outPacketFill ; 5667 X = the write offset inside the packet buffer
|
|
L_566A:
|
|
lda #$FF ; 566A immediate operand patched at $5659 with the command byte
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Two special cases are filtered out here rather than at queue time, because whether they are still
|
|
; valid only becomes clear when the packet is built.
|
|
; ----------------------------------------------------------------------
|
|
cmp #$92 ; 566C $92 = 'the drone reached its target'
|
|
bne L_5679 ; 566E not a $92
|
|
bit droneArrivalFlags ; 5670 $92F9 bit 6 = the drone detonated instead of being shot down
|
|
bvc L_5689 ; 5673 bit 6 clear: the shot-down report is still valid, so copy it
|
|
dex ; 5675 drop the stale $92: back the packet cursor up so the INX at $5696 cancels out
|
|
jmp L_568F ; 5676 step over it in the ring without copying anything
|
|
|
|
L_5679:
|
|
cmp #$8E ; 5679 $8E = 'detonate the drone'
|
|
bne L_5689 ; 567B not an $8E
|
|
bit droneControlFlags ; 567D $920D bit 7 = a drone flight of ours really is in progress
|
|
bmi L_5689 ; 5680 it is - copy the detonate command normally
|
|
ldy outgoingCmdWriteIndex ; 5682 stale detonate: throw the whole ring away by moving the read cursor to the write cursor ...
|
|
ldx #$00 ; 5685 ... and send an empty packet this exchange
|
|
beq L_56A6 ; 5687 always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the remaining bytes of this command from the ring into the packet, wrapping the ring index at
|
|
; 40.
|
|
; ----------------------------------------------------------------------
|
|
L_5689:
|
|
lda outgoingCmdRing,y ; 5689 copy one byte of the command
|
|
sta txPacketBuffer,x ; 568C $E027 is the comm module's 7-byte transmit buffer (also trainerAiE000:txPacketBuffer)
|
|
L_568F:
|
|
iny ; 568F advance the ring cursor
|
|
cpy #$28 ; 5690 $28 = 40 bytes in the ring
|
|
bcc L_5696 ; 5692 no wrap needed
|
|
ldy #$00 ; 5694 wrap back to the start
|
|
L_5696:
|
|
inx ; 5696 advance the packet cursor
|
|
dec currentCommandLength ; 5697 $92EE counts the bytes of this command down
|
|
bne L_5689 ; 569A copy the rest of the command
|
|
stx outPacketFill ; 569C remember how full the packet is
|
|
cpx #$04 ; 569F is there room for at least one more byte?
|
|
bcc L_5651 ; 56A1 yes - look at the next command in the ring
|
|
L_56A3:
|
|
ldx outPacketFill ; 56A3 X = the finished packet length
|
|
L_56A6:
|
|
sty outgoingCmdReadIndex ; 56A6 the ring is consumed up to here
|
|
stx txPacketLength ; 56A9 $E01F = the number of bytes the comm module should transmit (also trainerAiE000:txPacketLength)
|
|
ldx #$01 ; 56AC comm function 1 = request a send
|
|
jmp commJumpTable ; 56AE $E000; the modem driver queues the packet, the solo trainer throws it away (also trainerAiE000:commRequestEntry)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pollMenuSession - Reached while a link is up but no game is running (commSessionState $0B9C bit 7):
|
|
; if the carrier is gone the session is torn down, otherwise the menu-level packet handler runs. Note
|
|
; that the commandHandlerTable slot for command $99 also points here, but nothing in the game ever
|
|
; queues a $99, so that entry is unreachable filler.
|
|
; In: the comm module's carrier byte $E03C
|
|
; Out: falls into handleMenuSessionPacket, or jumps to disconnectFromOpponent
|
|
; Called from: processCommandExchange $4DCE and $4E3E
|
|
; ----------------------------------------------------------------------
|
|
pollMenuSession:
|
|
lda linkStatus ; 56B1 the comm module's carrier state byte (also trainerAiE000:linkStatus)
|
|
and #$40 ; 56B4 bit 6 = carrier present
|
|
bne handleMenuSessionPacket ; 56B6 still connected: service the menu-level traffic
|
|
jmp disconnectFromOpponent ; 56B8 $1C17: carrier lost - tear the session down and hang up
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleMenuSessionPacket - Menu-level receive path: re-arms the 10-frame link poll delay and returns
|
|
; unless a packet has arrived. A 2-byte [$8C,$05] means the opponent hung up, so we hang up too;
|
|
; [$8C,$02] means he is ready for a new game and puts 'OPPONENT READY FOR NEW GAME.' on the status
|
|
; line, rate limited by the middle byte of the frame counter so the message cannot repeat every
|
|
; exchange. Every other packet just falls into completeExchange to be consumed.
|
|
; In: $E01D, $E01E, $E020/$E021, frameCounterMid zp_46
|
|
; Out: linkPollDelay $9163 = 10, message $1F, zp_46 reset; falls into completeExchange
|
|
; Called from: pollMenuSession (branch at $56B6) and the text engine's pause handler $C737
|
|
; ----------------------------------------------------------------------
|
|
handleMenuSessionPacket:
|
|
ldy #$0A ; 56BB 10 frames
|
|
sty linkPollDelay ; 56BD $9163: do not poll the link again for 10 frames
|
|
lda exchangeFlags ; 56C0 $E01D exchange flags (also trainerAiE000:commInterfaceBlock)
|
|
bmi L_56C6 ; 56C3 bit 7 = a packet from the opponent has arrived
|
|
rts ; 56C5 nothing received - come back next poll
|
|
|
|
L_56C6:
|
|
lda rxPacketLength ; 56C6 $E01E = length of the received packet (also trainerAiE000:rxPacketLength)
|
|
cmp #$02 ; 56C9 only 2-byte menu packets mean anything here
|
|
bne completeExchange ; 56CB wrong length - just finish the exchange
|
|
lda rxPacketBuffer ; 56CD first byte of the received packet (also trainerAiE000:rxPacketBuffer)
|
|
cmp #$8C ; 56D0 $8C = session control
|
|
bne completeExchange ; 56D2 not a session-control packet
|
|
lda rxPacketArg1 ; 56D4 the reason byte (also trainerAiE000:rxPacketArg1)
|
|
cmp #$05 ; 56D7 reason 5 = the opponent hung up
|
|
bne L_56DE ; 56D9 not a hang-up
|
|
jmp hangUpModem ; 56DB $1B33: drop our end of the line too
|
|
|
|
L_56DE:
|
|
cmp #$02 ; 56DE reason 2 = READY FOR A NEW GAME
|
|
bne completeExchange ; 56E0 some other reason - ignore it
|
|
lda frameCounterMid ; 56E2 zp_46, the middle byte of the free-running frame counter, is abused as a crude rate limit
|
|
cmp #$03 ; 56E4 about 3*256 frames since the last announcement
|
|
bcc completeExchange ; 56E6 too soon - say nothing this time
|
|
lda #$1F ; 56E8 message $1F
|
|
jsr queueMessage ; 56EA 'OPPONENT READY FOR NEW GAME.'
|
|
lda #$00 ; 56ED 0
|
|
sta frameCounterMid ; 56EF restart the rate limit (and, as a side effect, the frame counter's middle byte)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; completeExchange - Makes sure the packet exchange currently in flight finishes. Nothing pending
|
|
; returns at once; an already completed exchange is consumed; if our own packet is out or a send is
|
|
; already requested it only waits; otherwise an empty packet is sent to give the opponent something to
|
|
; answer. It then spins for up to $4B ticks of linkWaitTimer, servicing the status line, until the
|
|
; exchange completes, and consumes it. A timeout hangs the modem up.
|
|
; In: $E01D exchange flags, linkSessionState $0B9C
|
|
; Out: $E01F, linkWaitTimer zp_7F, the comm module's exchange state; or jumps to hangUpModem
|
|
; Called from: $1B2A, readyForNewGameHandshake $572B, handleMenuSessionPacket (four branches) and
|
|
; $EE06
|
|
; ----------------------------------------------------------------------
|
|
completeExchange:
|
|
ldy exchangeFlags ; 56F1 $E01D exchange flags (also trainerAiE000:commInterfaceBlock)
|
|
beq L_572A ; 56F4 nothing in flight - nothing to finish
|
|
tya ; 56F6 look at the flags
|
|
and #$40 ; 56F7 bit 6 = the exchange has completed
|
|
bne L_5725 ; 56F9 just consume it
|
|
tya ; 56FB look at the flags again
|
|
and #$20 ; 56FC bit 5 = our packet has already gone out
|
|
bne L_5712 ; 56FE only his answer is missing - wait for it
|
|
tya ; 5700 look at the flags again
|
|
and #$01 ; 5701 bit 0 = a send has already been requested
|
|
bne L_5712 ; 5703 wait for it
|
|
lda #$00 ; 5705 0
|
|
sta txPacketLength ; 5707 $E01F = 0: nothing to say, so send a zero-length packet (also trainerAiE000:txPacketLength)
|
|
lda linkSessionState ; 570A A carries the session state down to the comm module
|
|
ldx #$01 ; 570D comm function 1 = request a send
|
|
jsr commJumpTable ; 570F hand it over (also trainerAiE000:commRequestEntry)
|
|
L_5712:
|
|
lda #$4B ; 5712 $4B = 75 ticks; zp_7F is decremented every 8th frame, so roughly ten seconds
|
|
sta linkWaitTimer ; 5714 arm the timeout
|
|
L_5716:
|
|
jsr updateStatusMessage ; 5716 $C4EF keeps the status line, chat and clock alive while we spin
|
|
bit exchangeFlags ; 5719 $E01D exchange flags (also trainerAiE000:commInterfaceBlock)
|
|
bvs L_5725 ; 571C bit 6: the exchange completed
|
|
lda linkWaitTimer ; 571E the timeout counts down in the raster IRQ
|
|
bne L_5716 ; 5720 keep waiting
|
|
jmp hangUpModem ; 5722 the opponent never answered - hang up the modem
|
|
|
|
L_5725:
|
|
ldx #$02 ; 5725 comm function 2 = acknowledge and clear the finished exchange
|
|
jmp commJumpTable ; 5727 $E000, then return to our caller (also trainerAiE000:commRequestEntry)
|
|
|
|
L_572A:
|
|
rts ; 572A nothing to do
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readyForNewGameHandshake - Run before a new game over an existing link: finishes any pending
|
|
; exchange, puts 'WAITING FOR OPPONENT...' up as a persistent message and then repeatedly sends
|
|
; [$8C,$02] and waits for the exchange to complete. When the opponent's packet is also [$8C,$02] the
|
|
; session state becomes 2 (game session), the message is cleared and the status line is wiped;
|
|
; [$8C,$05] means he hung up; anything else is consumed and the READY packet is sent again.
|
|
; In: $E01D/$E01E/$E020/$E021
|
|
; Out: linkSessionState $0B9C = 2, persistentMessageId $92A1 cleared; or jumps to hangUpModem
|
|
; Called from: overlay A $795A
|
|
; ----------------------------------------------------------------------
|
|
readyForNewGameHandshake:
|
|
jsr completeExchange ; 572B make sure no half-finished exchange is left over from the previous game
|
|
lda #$0B ; 572E message $0B
|
|
sta persistentMessageId ; 5730 'WAITING FOR OPPONENT...' stays on the status line for the whole handshake
|
|
jsr queueMessage ; 5733 show it now
|
|
L_5736:
|
|
jsr buildReadyPacket ; 5736 put $8C,$02 = READY FOR A NEW GAME in the transmit buffer
|
|
ldx #$01 ; 5739 comm function 1 = request a send
|
|
jsr commJumpTable ; 573B hand it over (also trainerAiE000:commRequestEntry)
|
|
L_573E:
|
|
jsr updateStatusMessage ; 573E service the status line while waiting
|
|
bit exchangeFlags ; 5741 $E01D exchange flags (also trainerAiE000:commInterfaceBlock)
|
|
bvc L_573E ; 5744 spin until bit 6 says the exchange completed
|
|
lda rxPacketLength ; 5746 length of the packet that came back (also trainerAiE000:rxPacketLength)
|
|
cmp #$02 ; 5749 a menu packet is exactly 2 bytes
|
|
bne L_5774 ; 574B anything else: consume it and try again
|
|
lda rxPacketBuffer ; 574D first byte of his packet (also trainerAiE000:rxPacketBuffer)
|
|
cmp #$8C ; 5750 $8C = session control
|
|
bne L_5774 ; 5752 not session control - try again
|
|
lda rxPacketArg1 ; 5754 his reason byte (also trainerAiE000:rxPacketArg1)
|
|
cmp #$02 ; 5757 2 = he is ready for a new game as well
|
|
bne L_576D ; 5759 not a READY - check for a hang-up
|
|
lda #$02 ; 575B 2 = a game session has been agreed
|
|
sta linkSessionState ; 575D $0B9C session state
|
|
lda #$00 ; 5760 0
|
|
sta persistentMessageId ; 5762 take the WAITING message off the line
|
|
ldx #$02 ; 5765 comm function 2 = consume the finished exchange
|
|
jsr commJumpTable ; 5767 hand it over (also trainerAiE000:commRequestEntry)
|
|
jmp clearMessageLine ; 576A $C39F wipes the status line and returns to overlay A's caller
|
|
|
|
L_576D:
|
|
cmp #$05 ; 576D reason 5 = he hung up instead
|
|
bne L_5774 ; 576F some other reason - consume it and resend
|
|
jmp hangUpModem ; 5771 $1B33: drop our end of the line too
|
|
|
|
L_5774:
|
|
ldx #$02 ; 5774 comm function 2 = consume whatever it was
|
|
jsr commJumpTable ; 5776 hand it over (also trainerAiE000:commRequestEntry)
|
|
jmp L_5736 ; 5779 and send our READY packet again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildReadyPacket - Fills the comm module's transmit buffer with the 2-byte packet $8C,$02 (session
|
|
; control, reason 2 = READY FOR A NEW GAME) and sets the transmit length $E01F to 2.
|
|
; In: none
|
|
; Out: $E027 = $8C, $E028 = $02, $E01F = 2
|
|
; Called from: readyForNewGameHandshake $5736
|
|
; ----------------------------------------------------------------------
|
|
buildReadyPacket:
|
|
lda #$8C ; 577C command $8C session control
|
|
sta txPacketBuffer ; 577E first byte of the comm module's transmit buffer (also trainerAiE000:txPacketBuffer)
|
|
lda #$02 ; 5781 reason 2 = READY FOR A NEW GAME
|
|
sta txPacketArg1 ; 5783 second byte of the transmit buffer (also trainerAiE000:txPacketArg1)
|
|
sta txPacketLength ; 5786 $E01F = 2 bytes to transmit (the same value as the reason byte only by coincidence) (also trainerAiE000:txPacketLength)
|
|
rts ; 5789 the packet is ready to be sent
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; recordExchangeToFilm - Appends one lock-step exchange to the game film held in the RAM under the I/O
|
|
; area. Nothing is written when the exchange carried no commands, when the battle is not running, or
|
|
; when the film buffer is full. A record is [ (splitIndex << 4) | length ] [ time stamp, 1 or 2 bytes
|
|
; ] [ length command bytes ]; the split index says how many of the bytes belong to the first player,
|
|
; so playback can restore the own/remote flag exactly. Every byte goes through writeFilmByte, which
|
|
; banks the I/O area out around the store, and filmPtr is advanced by the length of the record at the
|
|
; end.
|
|
; In: mergedCmdLength $92EB, mergedCmdSplitIndex $92EC, mergedCmdBuffer $908F, filmPtr zp_BC/zp_BD,
|
|
; filmEndPtr $92F1/$92F2, gamePhase zp_B1
|
|
; Out: film bytes written, filmPtr advanced; X = length, Y = record size
|
|
; Called from: executeTurnCommands $4EFA
|
|
; ----------------------------------------------------------------------
|
|
recordExchangeToFilm:
|
|
lda mergedCmdLength ; 578A $92EB = number of command bytes merged for this exchange
|
|
beq L_57BD ; 578D nothing happened - do not waste a film record
|
|
lda gamePhase ; 578F zp_B1 is zero only during the battle
|
|
bne L_57BD ; 5791 the setup screens and menus are not filmed
|
|
jsr compareFilmPtrToEnd ; 5793 $591E: C = 1 once filmPtr has reached filmEndPtr
|
|
bcs L_57BD ; 5796 the film buffer in the RAM under the I/O area is full
|
|
ldx #$00 ; 5798 X = index into mergedCmdBuffer
|
|
ldy #$00 ; 579A Y = offset within the film record being written
|
|
lda mergedCmdSplitIndex ; 579C $92EC = how many of the merged bytes belong to the first player
|
|
asl a ; 579F shift it ...
|
|
asl a ; 57A0 ... into ...
|
|
asl a ; 57A1 ... the high ...
|
|
asl a ; 57A2 ... nibble of the header byte
|
|
ora mergedCmdLength ; 57A3 low nibble = the total length; both always fit in four bits
|
|
jsr writeFilmByte ; 57A6 $5893 stores at (filmPtr),Y with $01 = $34 so the RAM under $D000 is visible
|
|
iny ; 57A9 next byte of the record
|
|
jsr writeFilmTimestamp ; 57AA $57BE: one or two bytes of clock delta plus the sub-tick index; it advances Y itself
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the exchange's command bytes verbatim; playback feeds exactly these bytes back through
|
|
; executeTurnCommands, which is why a film replays identically whether the opponent was a person or
|
|
; the solo trainer.
|
|
; ----------------------------------------------------------------------
|
|
L_57AD:
|
|
lda mergedCmdBuffer,x ; 57AD copy the merged command bytes verbatim - this is what playback re-executes
|
|
jsr writeFilmByte ; 57B0 write it to the film
|
|
iny ; 57B3 next film byte
|
|
inx ; 57B4 next command byte
|
|
cpx mergedCmdLength ; 57B5 all of them written?
|
|
bcc L_57AD ; 57B8 no - keep copying
|
|
jsr advanceFilmPtr ; 57BA $57E7: filmPtr += Y, the length of the record just written
|
|
L_57BD:
|
|
rts ; 57BD done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeFilmTimestamp - Writes the time stamp of one game-film record at (filmPtr),Y. The stamp is the
|
|
; game clock, stored as a 4-bit delta against the clock of the previous record whenever the clock has
|
|
; moved by 0..14 ticks (the usual case, since the clock ticks down once per round), otherwise as a
|
|
; full byte. The low nibble of the first byte always carries commandSliceIndex ($923C), the number of
|
|
; the 10-unit slice of the round this exchange belongs to; a zero high nibble is what tells the reader
|
|
; that an absolute clock byte follows. The previous clock is kept in the SBC operand at $57C3, which
|
|
; startGameSession ($7911) clears when a new film starts.
|
|
; In: Y = offset of the next free byte inside the record; gameClock $91CB; commandSliceIndex $923C;
|
|
; filmPtr $BC/$BD; the previous clock in the operand L_57C2+1 ($57C3)
|
|
; Out: 1 or 2 bytes written into the film buffer under the I/O area; Y advanced past them; L_57C2+1
|
|
; updated to the current clock
|
|
; Called from: recordExchangeToFilm ($57AA) for every recorded exchange and finalizeFilm ($5808) for
|
|
; the end-of-film marker. The playback counterpart is inside processCommandExchange at
|
|
; $4DE1-$4E04.
|
|
; ----------------------------------------------------------------------
|
|
writeFilmTimestamp:
|
|
lda gameClock ; 57BE current game clock (a round counter that runs down to 0: 63, 127 or 254 at kick-off depending on the game type)
|
|
clc ; 57C1 clear carry so the SBC below subtracts one extra: delta = clock - previousClock - 1
|
|
L_57C2:
|
|
sbc #$00 ; 57C2 operand L_57C2+1 ($57C3) = clock stored with the previous record, reset to 0 by startGameSession ($7911)
|
|
cmp #$F1 ; 57C4 delta $F1..$FF means the clock fell by 0..14 ticks, which fits into one nibble
|
|
bcc L_57D2 ; 57C6 branch when the clock moved by more than 14 ticks (or not backwards at all): the long two-byte form is needed
|
|
asl a ; 57C8 shift the delta nibble up into bits 4-7 of the time-stamp byte (1 of 4)
|
|
asl a ; 57C9 shift the delta nibble up (2 of 4)
|
|
asl a ; 57CA shift the delta nibble up (3 of 4)
|
|
asl a ; 57CB shift the delta nibble up (4 of 4); the low nibble is now free
|
|
ora commandSliceIndex ; 57CC low nibble = command slice number (unitOrderIndex/10, 0-9); playback compares it at $4DE3 and rejects the record if it does not match
|
|
jmp L_57DC ; 57CF one byte is enough - go and store it
|
|
|
|
L_57D2:
|
|
lda commandSliceIndex ; 57D2 long form: a first byte whose high nibble is 0 tells the reader that an absolute clock byte follows
|
|
jsr writeFilmByte ; 57D5 store it at (filmPtr),Y in the RAM hidden under the I/O area
|
|
iny ; 57D8 step to the next byte of the record
|
|
lda gameClock ; 57D9 second byte of the long form = the absolute game clock
|
|
L_57DC:
|
|
jsr writeFilmByte ; 57DC store the time-stamp byte
|
|
lda gameClock ; 57DF the clock this record was stamped with...
|
|
sta L_57C2+1 ; 57E2 ...becomes the base of the next record's delta (self-modifies the SBC operand at $57C3)
|
|
iny ; 57E5 count the time-stamp byte that was just written
|
|
rts ; 57E6 back to the caller with Y past the time stamp
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; advanceFilmPtr - Adds the record length in Y to the 16-bit film write pointer filmPtr ($BC/$BD), so
|
|
; the next record starts where this one ended.
|
|
; In: Y = number of bytes just written; filmPtr $BC/$BD
|
|
; Out: filmPtr advanced by Y; A clobbered
|
|
; Called from: processCommandExchange ($4E30) after replaying a record, recordExchangeToFilm ($57BA)
|
|
; and finalizeFilm ($580B).
|
|
; ----------------------------------------------------------------------
|
|
advanceFilmPtr:
|
|
tya ; 57E7 Y = number of bytes the record occupied
|
|
clc ; 57E8 plain 16-bit add, no carry in
|
|
adc filmPtr ; 57E9 add to the low byte of the film pointer
|
|
sta filmPtr ; 57EB store it back
|
|
bcc L_57F1 ; 57ED skip the high byte unless the add wrapped past $FF
|
|
inc filmPtrHi ; 57EF carry into the high byte of the film pointer
|
|
L_57F1:
|
|
rts ; 57F1 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; finalizeFilm - Closes a game film that was being recorded: appends the end-of-film marker record
|
|
; ($FE = the game played itself out, $FF = it was aborted) with a time stamp if the buffer still has
|
|
; room, then rewinds and fills in the header at $D000 - total film length, format id, final clock,
|
|
; both 16-bit scores and the checksum over $D000-$D032 - and finally leaves filmPtr pointing at the
|
|
; end of the recording again.
|
|
; In: filmPtr $BC/$BD, filmEndPtr $92F1/$92F2, gameEndReason $0BA8 (bit 0 = aborted), gameVersionByte
|
|
; $0B94, gameClock $91CB, the two scores $92A2-$92A5, playerSide $0B9F
|
|
; Out: end marker appended, header bytes $D000/$D001 (length), $D02F (format id), $D031 (final clock),
|
|
; $D00D-$D010 (scores) and $D033 (checksum) written; filmEndPtr = end of the recording; filmPtr =
|
|
; filmEndPtr
|
|
; Called from: endGameToMainMenu ($1B18) only, and only when a film was actually being recorded.
|
|
; ----------------------------------------------------------------------
|
|
finalizeFilm:
|
|
jsr compareFilmPtrToEnd ; 57F2 C=1 when the recording has already reached the $DFF0 limit
|
|
bcs L_580E ; 57F5 buffer full - there is no room for a marker, just write the header
|
|
ldy #$FE ; 57F7 $FE = 'the game reached its own end'; playback at $4E0B simply stops on it
|
|
lda gameEndReason ; 57F9 why the game ended: 0 running, 1 aborted, $08 time up, $A0/$C0 a comcen was knocked out
|
|
and #$01 ; 57FC bit 0 is set only for reason 1 = the game was aborted
|
|
beq L_5801 ; 57FE played out to the end - keep the $FE marker
|
|
iny ; 5800 $FF = aborted game; playback at $4E0F turns this into finishFilmPlayback
|
|
L_5801:
|
|
tya ; 5801 the marker byte
|
|
ldy #$00 ; 5802 offset 0 of the record = where a normal record keeps its length byte
|
|
jsr writeFilmByte ; 5804 write the marker
|
|
iny ; 5807 offset 1
|
|
jsr writeFilmTimestamp ; 5808 the marker carries a time stamp like any other record
|
|
jsr advanceFilmPtr ; 580B filmPtr += 2 or 3 bytes of marker record
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Rewind and fill in the 52-byte film header at $D000. Offsets used here: $00/$01 length,
|
|
; $2F format id, $31 final clock, $0D-$10 the two scores, $33 checksum. The rest of the header
|
|
; ($02/$03 command-stream offset, $04 solo flag, $05 side, $06-$0B map name, $0C options, $11-$24
|
|
; player names, $2B-$2D map seed, $30 starting clock) is written by initFilmStream,
|
|
; writeFilmSetupSnapshot and the save-film code in overlay A.
|
|
; ----------------------------------------------------------------------
|
|
L_580E:
|
|
jsr saveFilmEndAndRewind ; 580E filmEndPtr = end of the recording, then filmPtr back to $D000
|
|
ldy #$00 ; 5811 header offset 0
|
|
lda filmEndPtr ; 5813 low byte of the end address...
|
|
sec ; 5816 ...minus the start of the buffer
|
|
sbc #$00 ; 5817 $D000 low byte: length = filmEndPtr - $D000
|
|
tax ; 5819 park the low byte of the length in X
|
|
lda filmEndPtrHi ; 581A high byte of the end address...
|
|
sbc #$D0 ; 581D ...minus $D0 = high byte of the length
|
|
pha ; 581F keep the high byte on the stack, writeFilmByte needs A
|
|
txa ; 5820 low byte of the length back into A
|
|
jsr writeFilmByte ; 5821 $D000 = film length, low byte
|
|
iny ; 5824 header offset 1
|
|
pla ; 5825 high byte of the length
|
|
jsr writeFilmByte ; 5826 $D001 = film length, high byte
|
|
lda gameVersionByte ; 5829 $0B94 = the game/film format id also used in the link version handshake
|
|
ldy #$2F ; 582C header offset $2F
|
|
jsr writeFilmByte ; 582E $D02F = format id; verifyFilmHeader refuses a film whose id differs from this build's
|
|
iny ; 5831 offset $30 already holds the clock at kick-off (writeFilmSetupSnapshot wrote it)
|
|
iny ; 5832 step over it to offset $31
|
|
lda gameClock ; 5833 clock left on the board when the game ended
|
|
jsr writeFilmByte ; 5836 $D031 = final clock; finishFilmPlayback reads it back
|
|
ldx playerSide ; 5839 start with the local player's score (playerSide 0 or 1)
|
|
ldy #$10 ; 583C header offset $10 = high byte of the recorder's own score
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Write both 16-bit scores, own side first: $D00F/$D010 = this machine's score,
|
|
; $D00D/$D00E = the opponent's. X alternates between the two sides with EOR #$01 and Y walks
|
|
; down $10, $0F, $0E, $0D. finishFilmPlayback reads them back in exactly the same order, so a film
|
|
; always shows the scores from the point of view of whoever is watching it.
|
|
; ----------------------------------------------------------------------
|
|
L_583E:
|
|
lda sideScoreHi,x ; 583E score high byte of side X ($92A4 + side)
|
|
jsr writeFilmByte ; 5841 store it
|
|
dey ; 5844 next header offset down
|
|
lda sideScoreLo,x ; 5845 score low byte of side X ($92A2 + side)
|
|
jsr writeFilmByte ; 5848 store it
|
|
dey ; 584B next header offset down
|
|
txa ; 584C side index
|
|
eor #$01 ; 584D flip to the other side
|
|
tax ; 584F back into X
|
|
cpy #$0D ; 5850 stop once both score pairs ($D010..$D00D) are written
|
|
bcs L_583E ; 5852 second pass for the opponent's score
|
|
jsr computeFilmHeaderChecksum; 5854 sum $D000-$D032 into scratch18 (rewinds filmPtr as a side effect)
|
|
ldy #$33 ; 5857 header offset $33
|
|
jsr writeFilmByte ; 5859 $D033 = header checksum; falls straight into setFilmPtrToEnd
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setFilmPtrToEnd - Points filmPtr ($BC/$BD) at filmEndPtr ($92F1/$92F2), i.e. back at the end of the
|
|
; film after the header has been read or written.
|
|
; In: filmEndPtr $92F1/$92F2
|
|
; Out: filmPtr = filmEndPtr; A clobbered
|
|
; Called from: the tail of finalizeFilm (fall-through from $5859) and finishFilmPlayback ($59F9).
|
|
; ----------------------------------------------------------------------
|
|
setFilmPtrToEnd:
|
|
lda filmEndPtr ; 585C end of the recording, low byte
|
|
sta filmPtr ; 585F into the film pointer
|
|
lda filmEndPtrHi ; 5861 end of the recording, high byte
|
|
sta filmPtrHi ; 5864 into the film pointer
|
|
rts ; 5866 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; saveFilmEndAndRewind - Remembers the current film position as the end of the recording (filmEndPtr =
|
|
; filmPtr) and then rewinds filmPtr to the start of the buffer at $D000, ready for header access.
|
|
; In: filmPtr $BC/$BD
|
|
; Out: filmEndPtr $92F1/$92F2 = old filmPtr; filmPtr = $D000; A clobbered
|
|
; Called from: finalizeFilm ($580E) when a recording is closed and finishFilmPlayback ($59D0) when a
|
|
; replay reaches the end.
|
|
; ----------------------------------------------------------------------
|
|
saveFilmEndAndRewind:
|
|
lda filmPtr ; 5867 current film position, low byte
|
|
sta filmEndPtr ; 5869 becomes the end-of-film limit
|
|
lda filmPtrHi ; 586C current film position, high byte
|
|
sta filmEndPtrHi ; 586E becomes the end-of-film limit
|
|
jmp rewindFilmPtr ; 5871 and rewind to $D000 so the header can be read or written
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readFilmByte - Reads one byte of the game film out of the 4 KB of RAM that lies underneath the I/O
|
|
; area ($D000-$DFFF). Interrupts are disabled, the CIA2 interrupt mask (the modem NMI) is cleared,
|
|
; $01 is switched from $35 to $34 so the RAM shows through, the byte is fetched and everything is put
|
|
; back, including the mask the comm module keeps in $E033. X is preserved through the self-modified
|
|
; LDX # at $5890.
|
|
; In: Y = offset from filmPtr $BC/$BD
|
|
; Out: A = the byte; X, Y, filmPtr unchanged; CPU_PORT back at $35 and CIA2_ICR back at the comm
|
|
; module's mask
|
|
; Called from: the film playback path in processCommandExchange ($4DD4-$4E23), verifyFilmHeader,
|
|
; computeFilmHeaderChecksum and finishFilmPlayback in this file, from the film directory/load code
|
|
; of overlay A ($7CDB-$7D35) and from the modem driver's $EE00 tail ($EE54-$EEAB).
|
|
; ----------------------------------------------------------------------
|
|
readFilmByte:
|
|
stx L_5890+1 ; 5874 self-modify the LDX # at $5891 so X survives the call
|
|
ldx #$7F ; 5877 $7F clears every mask bit when written to a CIA interrupt-control register
|
|
sei ; 5879 no interrupts while the I/O area is banked away
|
|
stx CIA2_ICR ; 587A disable all CIA2 interrupt sources, i.e. the modem UART's NMI
|
|
nop ; 587D 3-cycle gap: the mask is written twice as a guard against an NMI arriving mid-store
|
|
stx CIA2_ICR ; 587E second write of the mask
|
|
ldx #$34 ; 5881 $34 = RAM everywhere except that the KERNAL/BASIC ROMs stay out; the $D000 I/O block disappears
|
|
stx CPU_PORT ; 5883 switch the film buffer into view
|
|
lda (filmPtr),y ; 5885 read the film byte
|
|
inc CPU_PORT ; 5887 $34 -> $35 with one instruction: I/O visible again
|
|
ldx ciaIcrShadow ; 5889 the comm module's copy of the CIA2 interrupt mask it wants installed (also trainerAiE000:ciaIcrShadow)
|
|
stx CIA2_ICR ; 588C re-enable the modem NMI sources
|
|
cli ; 588F interrupts back on
|
|
L_5890:
|
|
ldx #$FF ; 5890 operand patched at $5874: restores the caller's X
|
|
rts ; 5892 A holds the film byte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeFilmByte - The write half of readFilmByte: stores A at (filmPtr),Y in the RAM under the I/O
|
|
; area, with the same 'sei / kill the CIA2 NMI / $01=$34 / restore' dance. X is preserved through the
|
|
; self-modified LDX # at $58AF.
|
|
; In: A = byte to store; Y = offset from filmPtr $BC/$BD
|
|
; Out: one byte written to the film buffer; A, X, Y unchanged
|
|
; Called from: recordExchangeToFilm and writeFilmTimestamp ($57A6-$57DC), finalizeFilm ($5804-$5859),
|
|
; writeFilmSetupSnapshot ($5936), initFilmStream ($59A1/$59A7) and the save-film code of overlay A
|
|
; ($79F9-$7AD0).
|
|
; ----------------------------------------------------------------------
|
|
writeFilmByte:
|
|
stx L_58AF+1 ; 5893 self-modify the LDX # at $58B0 so X survives the call
|
|
ldx #$7F ; 5896 $7F clears every mask bit of a CIA interrupt-control register
|
|
sei ; 5898 no interrupts while the I/O area is banked away
|
|
stx CIA2_ICR ; 5899 disable all CIA2 interrupt sources (the modem NMI)
|
|
nop ; 589C 3-cycle gap between the two mask writes
|
|
stx CIA2_ICR ; 589D second write of the mask
|
|
ldx #$34 ; 58A0 $34 = the $D000 I/O block replaced by the RAM underneath
|
|
stx CPU_PORT ; 58A2 switch the film buffer into view
|
|
sta (filmPtr),y ; 58A4 store the film byte
|
|
inc CPU_PORT ; 58A6 $34 -> $35: I/O visible again
|
|
ldx ciaIcrShadow ; 58A8 the comm module's wanted CIA2 interrupt mask (also trainerAiE000:ciaIcrShadow)
|
|
stx CIA2_ICR ; 58AB re-enable the modem NMI sources
|
|
cli ; 58AE interrupts back on
|
|
L_58AF:
|
|
ldx #$FF ; 58AF operand patched at $5893: restores the caller's X
|
|
rts ; 58B1 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; copyPageUnderIo - Copies one 256-byte page with the I/O area banked out, used to move the unit
|
|
; tables in and out of the film's start-of-game snapshot. Source and destination are the operands of
|
|
; the LDA abs,Y at $58C3/$58C4 and the STA abs,Y at $58C6/$58C7; the caller patches them first. As
|
|
; disassembled both instructions show $FFFB (irqVectorHi) because that is the unpatched byte pair
|
|
; sitting in the file.
|
|
; In: source address in the operand at $58C3/$58C4, destination in the operand at $58C6/$58C7
|
|
; Out: 256 bytes copied; A, X, Y clobbered; CPU_PORT and CIA2_ICR restored
|
|
; Called from: writeFilmSetupSnapshot ($5952/$596B) here, from overlay A ($7F87, $8184) and from the
|
|
; modem driver's $EE00 tail ($EE73, $EE8C); the operand bytes are also patched by all three.
|
|
; ----------------------------------------------------------------------
|
|
copyPageUnderIo:
|
|
ldx #$7F ; 58B2 $7F clears every mask bit of a CIA interrupt-control register
|
|
sei ; 58B4 no interrupts while the I/O area is banked away
|
|
stx CIA2_ICR ; 58B5 disable all CIA2 interrupt sources (the modem NMI)
|
|
nop ; 58B8 3-cycle gap between the two mask writes
|
|
stx CIA2_ICR ; 58B9 second write of the mask
|
|
ldx #$34 ; 58BC $34 = the $D000 I/O block replaced by the RAM underneath
|
|
stx CPU_PORT ; 58BE bank the film buffer in
|
|
ldy #$00 ; 58C0 copy 256 bytes, index 0 upwards
|
|
L_58C2:
|
|
lda irqVectorHi,y ; 58C2 source operand at $58C3/$58C4, patched by the caller (shown as $FFFB = the unpatched bytes)
|
|
L_58C5:
|
|
sta irqVectorHi,y ; 58C5 destination operand at $58C6/$58C7, patched by the caller (shown as $FFFB = the unpatched bytes)
|
|
iny ; 58C8 next byte
|
|
bne L_58C2 ; 58C9 loop until Y wraps back to 0, i.e. after all 256 bytes
|
|
inc CPU_PORT ; 58CB $34 -> $35: I/O visible again
|
|
ldx ciaIcrShadow ; 58CD the comm module's wanted CIA2 interrupt mask (also trainerAiE000:ciaIcrShadow)
|
|
stx CIA2_ICR ; 58D0 re-enable the modem NMI sources
|
|
cli ; 58D3 interrupts back on
|
|
rts ; 58D4 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rewindFilmPtr - Sets filmPtr ($BC/$BD) to $D000, the first byte of the film / saved-game buffer in
|
|
; the RAM under the I/O area.
|
|
; In: -
|
|
; Out: filmPtr = $D000; A clobbered
|
|
; Called from: resetGameVariables ($0C5B), verifyFilmHeader, computeFilmHeaderChecksum,
|
|
; writeFilmSetupSnapshot, initFilmStream, the tail of saveFilmEndAndRewind ($5871) and the modem
|
|
; driver's $EE00 tail ($EE99).
|
|
; ----------------------------------------------------------------------
|
|
rewindFilmPtr:
|
|
lda #$00 ; 58D5 low byte of $D000
|
|
sta filmPtr ; 58D7 into the film pointer
|
|
lda #$D0 ; 58D9 high byte of $D000 - the film buffer starts right under the VIC registers
|
|
sta filmPtrHi ; 58DB into the film pointer
|
|
rts ; 58DD done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; verifyFilmHeader - Sanity check on a film that has just been loaded from disk: recomputes the
|
|
; checksum over header bytes $D000-$D032 and compares it with the stored one at $D033, then checks
|
|
; that the format id at $D02F matches this build's gameVersionByte ($0B94). On success it returns the
|
|
; film's solo/modem flag from $D004.
|
|
; In: the film header in the RAM under I/O
|
|
; Out: C=0 and A = $D004 (the isSoloTrainer flag the film was recorded with) when the film is good;
|
|
; C=1 when it is not, and the caller prints 'NOT A GAME FILM!' (message $09)
|
|
; Called from: the LOAD GAME FILM code of overlay A ($81F4).
|
|
; ----------------------------------------------------------------------
|
|
verifyFilmHeader:
|
|
jsr rewindFilmPtr ; 58DE point filmPtr at the start of the header
|
|
jsr computeFilmHeaderChecksum; 58E1 sum $D000-$D032 into scratch18
|
|
ldy #$33 ; 58E4 header offset $33 = the stored checksum
|
|
jsr readFilmByte ; 58E6 read it
|
|
cmp scratch18 ; 58E9 compare with the checksum just computed
|
|
bne L_58F7 ; 58EB mismatch: this is not a valid film
|
|
ldy #$2F ; 58ED header offset $2F = the format id the film was written with
|
|
jsr readFilmByte ; 58EF read it
|
|
cmp gameVersionByte ; 58F2 must equal this build's gameVersionByte $0B94
|
|
beq L_58F9 ; 58F5 same version - accept the film
|
|
L_58F7:
|
|
sec ; 58F7 C=1 tells the caller to reject the file
|
|
rts ; 58F8 reject
|
|
|
|
L_58F9:
|
|
ldy #$04 ; 58F9 header offset $04 = the solo-trainer flag stored with the film
|
|
jsr readFilmByte ; 58FB read it into A for the caller
|
|
clc ; 58FE C=0 = the film is valid
|
|
rts ; 58FF done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computeFilmHeaderChecksum - Adds up the 51 header bytes $D000-$D032 into scratch18 ($18). The ADC
|
|
; inside the loop is never preceded by a CLC, so the carry out of each byte feeds into the next one;
|
|
; readFilmByte is careful not to disturb the carry. Also leaves filmPtr rewound to $D000.
|
|
; In: the film header in the RAM under I/O
|
|
; Out: scratch18 = the checksum; filmPtr = $D000; A/Y clobbered
|
|
; Called from: finalizeFilm ($5854) when writing a film and verifyFilmHeader ($58E1) when checking
|
|
; one.
|
|
; ----------------------------------------------------------------------
|
|
computeFilmHeaderChecksum:
|
|
jsr rewindFilmPtr ; 5900 point filmPtr at $D000 so Y is a plain header offset
|
|
lda #$00 ; 5903 start the running sum at zero
|
|
sta scratch18 ; 5905 scratch18 accumulates the checksum
|
|
ldy #$32 ; 5907 walk header offsets $32 down to $00 - 51 bytes, everything except the checksum byte itself
|
|
clc ; 5909 clear the carry once; from here it chains from byte to byte
|
|
L_590A:
|
|
jsr readFilmByte ; 590A fetch header byte Y (readFilmByte leaves the carry alone)
|
|
adc scratch18 ; 590D add it to the running sum, carrying in from the previous add
|
|
sta scratch18 ; 590F keep the sum
|
|
dey ; 5911 next header offset down
|
|
bpl L_590A ; 5912 loop until offset 0 has been added and Y goes negative
|
|
rts ; 5914 scratch18 now holds the checksum
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForExchangeIdle - Spins on the per-frame command exchange until the packet exchange in flight
|
|
; has completed, which the exchange code signals by clearing linkTimeoutCounter ($9223);
|
|
; checkLinkTimeout sets bit 7 of that counter while a packet is outstanding.
|
|
; In: linkTimeoutCounter $9223
|
|
; Out: returns when $9223 is zero; everything serviceCommandExchange touches
|
|
; Called from: overlay A after sending setup commands ($797C, $7C09, $7C1C).
|
|
; ----------------------------------------------------------------------
|
|
waitForExchangeIdle:
|
|
jsr serviceCommandExchange ; 5915 one pass of the lock-step exchange plus the status-line service ($4DA9)
|
|
lda linkTimeoutCounter ; 5918 non-zero while a packet exchange is still outstanding (bit 7 is set by checkLinkTimeout)
|
|
bne waitForExchangeIdle ; 591B keep servicing the link until the exchange settles
|
|
rts ; 591D exchange complete
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; compareFilmPtrToEnd - 16-bit unsigned compare of filmPtr against filmEndPtr. While recording,
|
|
; filmEndPtr is the $DFF0 buffer limit, so C=1 means the film buffer is full; while playing back it is
|
|
; the real end of the film, so C=1 means the replay has run out of records.
|
|
; In: filmPtr $BC/$BD, filmEndPtr $92F1/$92F2
|
|
; Out: C=1 when filmPtr >= filmEndPtr; A clobbered
|
|
; Called from: processCommandExchange ($4DC1) on the playback path, recordExchangeToFilm ($5793) and
|
|
; finalizeFilm ($57F2).
|
|
; ----------------------------------------------------------------------
|
|
compareFilmPtrToEnd:
|
|
lda filmPtr ; 591E film pointer, low byte
|
|
cmp filmEndPtr ; 5920 compare with the limit, low byte (sets the borrow for the SBC below)
|
|
lda filmPtrHi ; 5923 film pointer, high byte
|
|
sbc filmEndPtrHi ; 5925 subtract the limit's high byte: C=1 when filmPtr >= filmEndPtr
|
|
rts ; 5928 only the carry matters to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeFilmSetupSnapshot - Writes the start-of-game snapshot into the film header so that a replay can
|
|
; rebuild the battlefield exactly: the 20 setup bytes $92A2-$92B5 (scores, recycler mode and position,
|
|
; terrain points, drones, missiles, comcen speeds) go to header offsets $34-$47, the eight 100-byte
|
|
; unit arrays $F640-$F95F (column, row, flags, type, destination column/row, waypoint column/row) are
|
|
; copied to $D048-$D367, and the game clock is stored at $D030.
|
|
; In: $92A2-$92B5, the unit arrays at $F640-$F95F, gameClock $91CB
|
|
; Out: $D034-$D047, $D048-$D367 and $D030 filled; filmPtr left at $D000; the copyPageUnderIo operands
|
|
; at $58C3-$58C7 left patched
|
|
; Called from: startGameSession in overlay A ($7B80) when a recorded game begins.
|
|
; ----------------------------------------------------------------------
|
|
writeFilmSetupSnapshot:
|
|
lda #$34 ; 5929 point filmPtr at header offset $34 ($D034)...
|
|
sta filmPtr ; 592B ...low byte
|
|
lda #$D0 ; 592D ...high byte
|
|
sta filmPtrHi ; 592F filmPtr = $D034, so Y counts from 0 through the setup block
|
|
ldy #$00 ; 5931 start at the first setup byte
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the 20 bytes of per-game settings $92A2-$92B5 (both 16-bit scores, recycler mode, recycler
|
|
; column and row per side, terrain points, drones left, missiles left, comcen reveal timers and the
|
|
; two comcen speed classes) into header offsets $34-$47.
|
|
; ----------------------------------------------------------------------
|
|
L_5933:
|
|
lda sideScoreLo,y ; 5933 $92A2 + Y: the 20-byte block of per-game settings starting with the two 16-bit scores
|
|
jsr writeFilmByte ; 5936 store it in the header
|
|
iny ; 5939 next setting
|
|
cpy #$14 ; 593A 20 bytes, $92A2 through $92B5
|
|
bcc L_5933 ; 593C loop over the whole settings block
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Snapshot the first eight unit arrays ($F640-$F95F, 800 bytes) into $D048-$D367. The copy is done
|
|
; with four 256-byte page copies whose destination is always source - $25F8; because 800 is not a
|
|
; multiple of 256 the four pages overlap, so only the first $20 bytes of the very first copy
|
|
; ($F640-$F65F) actually survive - the rest is redone by the three copies that follow. The arrays
|
|
; covered are column, row, flags/facing, type, destination column, destination row, waypoint column
|
|
; and waypoint row, i.e. everything needed to place the units again at the start of a replay.
|
|
; ----------------------------------------------------------------------
|
|
lda #$48 ; 593E destination low byte $48
|
|
sta L_58C5+1 ; 5940 into the STA operand of copyPageUnderIo ($58C6)
|
|
lda #$D0 ; 5943 destination high byte $D0
|
|
sta L_58C5+2 ; 5945 -> destination $D048
|
|
lda #$40 ; 5948 source low byte $40
|
|
sta L_58C2+1 ; 594A into the LDA operand of copyPageUnderIo ($58C3)
|
|
lda #$F6 ; 594D source high byte $F6
|
|
sta L_58C2+2 ; 594F -> source $F640, the unit column array
|
|
jsr copyPageUnderIo ; 5952 copy $F640-$F73F to $D048-$D147 (only the first $20 bytes of this survive - see the block note)
|
|
jsr getFilmSnapshotDestAddr ; 5955 A=$68, X=$D2: the top destination page $D268
|
|
sta L_58C5+1 ; 5958 destination low byte
|
|
stx L_58C5+2 ; 595B destination high byte
|
|
jsr getFilmSnapshotSrcAddr ; 595E A=$60, X=$F8: the matching top source page $F860
|
|
sta L_58C2+1 ; 5961 source low byte
|
|
stx L_58C2+2 ; 5964 source high byte
|
|
lda #$02 ; 5967 three more page copies to do (counter runs 2, 1, 0)
|
|
sta scratch19 ; 5969 scratch19 is the page counter
|
|
L_596B:
|
|
jsr copyPageUnderIo ; 596B copy one page; first $F860->$D268, then $F760->$D168, then $F660->$D068
|
|
dec L_58C2+2 ; 596E step the source down one page
|
|
dec L_58C5+2 ; 5971 step the destination down one page
|
|
dec scratch19 ; 5974 one page done
|
|
bpl L_596B ; 5976 loop while the counter is still non-negative - three iterations
|
|
jsr rewindFilmPtr ; 5978 back to $D000 so Y is a plain header offset again
|
|
ldy #$30 ; 597B header offset $30
|
|
lda gameClock ; 597D clock at kick-off; finalizeFilm later stores the final clock at offset $31
|
|
jmp writeFilmByte ; 5980 $D030 = starting clock, and return through writeFilmByte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getFilmSnapshotDestAddr - Returns the constant address $D268 in A (low) and X (high): the highest
|
|
; destination page of the unit snapshot inside the film header, and the page just below the start of
|
|
; the command stream at $D368. It exists as a routine so that the modem driver's $EE00 tail can reach
|
|
; the same constant when it restores a snapshot.
|
|
; In: -
|
|
; Out: A = $68, X = $D2
|
|
; Called from: writeFilmSetupSnapshot ($5955), initFilmStream ($59AA) and the modem driver's $EE00
|
|
; tail ($EE76).
|
|
; ----------------------------------------------------------------------
|
|
getFilmSnapshotDestAddr:
|
|
lda #$68 ; 5983 low byte of $D268
|
|
ldx #$D2 ; 5985 high byte of $D268 - the last page of the unit snapshot in the film header
|
|
rts ; 5987 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getFilmSnapshotSrcAddr - Returns the constant address $F860 in A (low) and X (high): the highest
|
|
; source page of the unit snapshot, i.e. the last full page of the eight unit arrays that end at
|
|
; $F95F. Shared with the snapshot restore code in the modem driver's $EE00 tail.
|
|
; In: -
|
|
; Out: A = $60, X = $F8
|
|
; Called from: writeFilmSetupSnapshot ($595E) and the modem driver's $EE00 tail ($EE7F).
|
|
; ----------------------------------------------------------------------
|
|
getFilmSnapshotSrcAddr:
|
|
lda #$60 ; 5988 low byte of $F860
|
|
ldx #$F8 ; 598A high byte of $F860 - the top source page of the unit-array snapshot
|
|
rts ; 598C done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setFilmLimitToBufferEnd - Sets filmEndPtr to $DFF0, the highest address a recording may reach.
|
|
; While a film is being recorded compareFilmPtrToEnd tests against this value, so it is the point
|
|
; where recording silently stops; the last 16 bytes of the $D000 page are left unused.
|
|
; In: -
|
|
; Out: filmEndPtr $92F1/$92F2 = $DFF0; A clobbered
|
|
; Called from: startGameSession in overlay A ($78D1) at the start of every game.
|
|
; ----------------------------------------------------------------------
|
|
setFilmLimitToBufferEnd:
|
|
lda #$F0 ; 598D low byte of $DFF0
|
|
sta filmEndPtr ; 598F into the recording limit
|
|
lda #$DF ; 5992 high byte of $DFF0 - the last 16 bytes of the buffer are kept free
|
|
sta filmEndPtrHi ; 5994 into the recording limit
|
|
rts ; 5997 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initFilmStream - Prepares the film for recording once the snapshot is in place: writes the offset of
|
|
; the first command record ($0368) into header bytes $D002/$D003 and sets filmPtr to $D368, the first
|
|
; byte after the 800-byte unit snapshot.
|
|
; In: -
|
|
; Out: $D002/$D003 = $0368; filmPtr = $D368; A/X/Y clobbered
|
|
; Called from: startGameSession in overlay A ($7B8C), right after writeFilmSetupSnapshot.
|
|
; ----------------------------------------------------------------------
|
|
initFilmStream:
|
|
jsr rewindFilmPtr ; 5998 point filmPtr at $D000 so Y is a plain header offset
|
|
lda #$68 ; 599B low byte of the command-stream offset $0368
|
|
ldx #$02 ; 599D X builds the high byte of the offset and then of the address
|
|
ldy #$02 ; 599F header offset $02
|
|
jsr writeFilmByte ; 59A1 $D002 = $68
|
|
iny ; 59A4 header offset $03
|
|
inx ; 59A5 X = 3
|
|
txa ; 59A6 high byte of the offset $0368
|
|
jsr writeFilmByte ; 59A7 $D003 = $03; the film records therefore start $0368 bytes into the file
|
|
jsr getFilmSnapshotDestAddr ; 59AA A=$68, X=$D2 - the last snapshot page
|
|
sta filmPtr ; 59AD filmPtr low byte = $68
|
|
inx ; 59AF one page past the snapshot: $D2 -> $D3
|
|
stx filmPtrHi ; 59B0 filmPtr = $D368, the first free byte for command records
|
|
rts ; 59B2 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isCmdRingNearlyFull - Works out how much room is left in the 40-byte outgoing command ring at
|
|
; $9065-$908C and reports C=1 when fewer than 5 bytes are free, which is less than the longest command
|
|
; plus its arguments. checkFirePressed calls it first, so while the ring is congested the fire button
|
|
; simply does nothing and no order can be lost. Equal read and write indices mean the ring is empty,
|
|
; which is reported as plenty of room.
|
|
; In: outgoingCmdWriteIndex $908D, outgoingCmdReadIndex $908E
|
|
; Out: C=1 = nearly full (refuse new orders), C=0 = room to spare; A clobbered
|
|
; Called from: checkFirePressed ($0EB1) only.
|
|
; ----------------------------------------------------------------------
|
|
isCmdRingNearlyFull:
|
|
lda outgoingCmdReadIndex ; 59B3 index the transmitter has reached
|
|
sec ; 59B6 prepare a true subtraction
|
|
sbc outgoingCmdWriteIndex ; 59B7 free space = readIndex - writeIndex when the reader is ahead
|
|
beq L_59CC ; 59BA both indices equal: the ring is empty, so it is certainly not full
|
|
bcs L_59C8 ; 59BC reader ahead of the writer: the difference is already the free space
|
|
lda outgoingCmdReadIndex ; 59BE writer has wrapped past the reader...
|
|
clc ; 59C1 prepare a plain add
|
|
adc #$28 ; 59C2 ...so add the ring size of 40 bytes ($9065-$908C)...
|
|
sec ; 59C4 prepare a true subtraction
|
|
sbc outgoingCmdWriteIndex ; 59C5 ...before taking the difference again
|
|
L_59C8:
|
|
cmp #$05 ; 59C8 5 bytes is the safety margin - the longest command is 4 bytes
|
|
bcc L_59CE ; 59CA less than 5 bytes free: report 'nearly full'
|
|
L_59CC:
|
|
clc ; 59CC C=0: there is room for another order
|
|
rts ; 59CD done
|
|
|
|
L_59CE:
|
|
sec ; 59CE C=1: the caller must refuse the order
|
|
rts ; 59CF done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; finishFilmPlayback - End of a game film: reached when the replay runs past the last record, when the
|
|
; record length byte is $FF or when the aborted-game end marker is read. It rewinds, restores the
|
|
; game clock and both 16-bit scores from the header (which finalizeFilm wrote when the film was made),
|
|
; marks the game as over, queues message 2 'END OF FILM.', locks the player out of the controls and
|
|
; starts the short end-of-game countdown.
|
|
; In: the film header at $D000, playerSide $0B9F
|
|
; Out: gameClock $91CB, scores $92A2-$92A5, gameEndReason $0BA8 incremented, filmPtr back at the end,
|
|
; gameOverFlags $923F = $C0, persistentMessageId $92A1 and chatMessageLine $0548 = $9E,
|
|
; gameEndCountdown $92C9 = $7D
|
|
; Called from: processCommandExchange ($4DC6) - both from the end-of-buffer test at $4DC4 and from the
|
|
; end-marker test at $4E0F.
|
|
; ----------------------------------------------------------------------
|
|
finishFilmPlayback:
|
|
jsr saveFilmEndAndRewind ; 59D0 remember where the film ended and rewind filmPtr to $D000
|
|
ldy #$31 ; 59D3 header offset $31 = the clock the recorded game ended on
|
|
jsr readFilmByte ; 59D5 read it
|
|
sta gameClock ; 59D8 show the same time left as the original game did
|
|
ldx playerSide ; 59DB the viewer's own side (0 or 1)
|
|
ldy #$10 ; 59DE header offset $10 = high byte of the recorder's score, which becomes the viewer's
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Read both 16-bit scores back out of the header in the same own-side-first order finalizeFilm wrote
|
|
; them in, so a replay always shows US and THEM from the point of view of whoever is watching.
|
|
; ----------------------------------------------------------------------
|
|
L_59E0:
|
|
jsr readFilmByte ; 59E0 score high byte from the header
|
|
sta sideScoreHi,x ; 59E3 $92A4 + side
|
|
dey ; 59E6 next header offset down
|
|
jsr readFilmByte ; 59E7 score low byte from the header
|
|
sta sideScoreLo,x ; 59EA $92A2 + side
|
|
dey ; 59ED next header offset down
|
|
txa ; 59EE side index
|
|
eor #$01 ; 59EF flip to the other side
|
|
tax ; 59F1 back into X
|
|
cpy #$0D ; 59F2 stop once both score pairs ($D010..$D00D) have been read
|
|
bcs L_59E0 ; 59F4 second pass for the other side
|
|
inc gameEndReason ; 59F6 0 means 'a game is running'; anything else ends it - here it just has to become non-zero
|
|
jsr setFilmPtrToEnd ; 59F9 put filmPtr back at the end so compareFilmPtrToEnd keeps saying 'finished'
|
|
lda #$02 ; 59FC message 2 = 'END OF FILM.' ($8707, sound 3)
|
|
jsr queueMessage ; 59FE queue it for the status line
|
|
lda #$C0 ; 5A01 bits 7 and 6: game over, and bit 6 blocks the fire button in checkFirePressedInteractive
|
|
sta gameOverFlags ; 5A03 $923F = $C0
|
|
lda #$9E ; 5A06 $9E is a bit-7 terminator that prints nothing
|
|
sta persistentMessageId ; 5A08 a negative persistentMessageId makes the status bar fall back to the CLICKS/US/THEM score line
|
|
sta chatMessageLine ; 5A0B and blank the chat line at $0548
|
|
lda #$7D ; 5A0E $7D: the IRQ counts this up towards $80 every 8 frames, so about 24 frames of grace
|
|
sta gameEndCountdown ; 5A10 non-zero gameEndCountdown also stops runGameFrames and the cursor handling
|
|
rts ; 5A13 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkLinkTimeout - Watchdog used while the game is waiting for the opponent's packet. It arms
|
|
; linkTimeoutCounter ($9223) by setting bit 7, which is what makes the raster IRQ count it up (one
|
|
; step every 8 frames, or every 16 while gamePhase is $FF, saturating at $FF). Below $E1 it just
|
|
; returns C=0 and the caller goes back to its frame loop; from $E1 on it puts 'PHONE TROUBLE.' on the
|
|
; status line and returns C=1, which makes the caller busy-wait instead; at $FF the link is declared
|
|
; dead - during the battle it hangs up, redials and starts the wait again, in the setup phases it
|
|
; disconnects, says 'TRY AGAIN.' and jumps back to the main menu.
|
|
; In: linkTimeoutCounter $9223, gamePhase $B1 (bits 6-7 clear = the battle is running),
|
|
; sndVoiceSoundId $67C8
|
|
; Out: C=1 = keep waiting in a tight loop, C=0 = come back next frame; $9223 bit 7 set; may set
|
|
; linkSessionState $0B9C = $FF, prevSoloTrainerFlag $0BA6 = $C8 and jump to returnToMainMenu
|
|
; Called from: waitForCarrier ($1BA3) and the wait-for-packet loop of processCommandExchange ($4E50).
|
|
; ----------------------------------------------------------------------
|
|
checkLinkTimeout:
|
|
lda linkTimeoutCounter ; 5A14 how long the current packet has been outstanding
|
|
L_5A17:
|
|
ora #$80 ; 5A17 bit 7 arms the counter: updateFrameTimers ($12B4) only advances it while it is negative
|
|
sta linkTimeoutCounter ; 5A19 write it back
|
|
cmp #$E1 ; 5A1C $E1 = 97 steps past the armed value $80; at one step per 8 frames that is roughly 13 seconds of silence
|
|
bcc L_5A57 ; 5A1E still early days - let the caller carry on with its normal frame loop
|
|
cmp #$FF ; 5A20 $FF = the counter has saturated: the link is gone
|
|
bne L_5A49 ; 5A22 between $E1 and $FE: only warn
|
|
lda gamePhase ; 5A24 0 while the battle runs; bits 6-7 mark the side-selection, setup, film and menu phases
|
|
and #$C0 ; 5A26 isolate those two phase bits
|
|
beq L_5A42 ; 5A28 the battle is running: try to get the call back rather than throwing the game away
|
|
lda #$FF ; 5A2A $FF = link lost
|
|
sta linkSessionState ; 5A2C record it in the session state
|
|
jsr disconnectFromOpponent ; 5A2F send the disconnect command and hang the modem up
|
|
lda #$23 ; 5A32 message $23 = 'TRY AGAIN.' ($0608, sound 3)
|
|
jsr showStatusMessage ; 5A34 show it on the status line
|
|
lda #$C8 ; 5A37 $C8 in prevSoloTrainerFlag forces the GET OPPONENT ON PHONE prompt next time round
|
|
sta prevSoloTrainerFlag ; 5A39 store it
|
|
jsr waitFrames ; 5A3C A is still $C8, so this waits 200 frames before leaving the message
|
|
jmp returnToMainMenu ; 5A3F abandon the game and rebuild the main menu
|
|
|
|
L_5A42:
|
|
jsr voicePauseReconnect ; 5A42 battle case: hang up, ask for a voice pause and dial again
|
|
lda #$00 ; 5A45 restart the watchdog from zero...
|
|
beq L_5A17 ; 5A47 ...always taken: re-enter at $5A17, which ORs in bit 7 and returns C=0
|
|
L_5A49:
|
|
lda #$15 ; 5A49 sound $15 is the phone-trouble warble
|
|
cmp sndVoiceSoundId ; 5A4B voice 0 of the sound driver
|
|
beq L_5A55 ; 5A4E already complaining - do not restart the message
|
|
lda #$22 ; 5A50 message $22 = 'PHONE TROUBLE.' ($05FA, sound $15)
|
|
jsr showStatusMessage ; 5A52 show it on the status line
|
|
L_5A55:
|
|
sec ; 5A55 C=1: the caller keeps spinning here instead of running game frames
|
|
rts ; 5A56 done
|
|
|
|
L_5A57:
|
|
clc ; 5A57 C=0: nothing wrong yet, carry on normally
|
|
rts ; 5A58 done
|
|
|
|
|
|
; Overview-map tile patterns. On the strategic map each battlefield cell is only 2 multicolour pixels
|
|
; wide and 4 scanlines tall, so a whole cell is described by four bytes - one per scanline - each of
|
|
; which carries both horizontal halves of the character (bits 7-4 for the left cell of the pair, bits
|
|
; 3-0 for the right); the quadrant mask in quadrantPixelMask picks the half that is wanted. A pixel
|
|
; pair of %00 is background (VIC background 0 = 7 yellow), %01 is the screen RAM upper nibble (6 blue
|
|
; = own units), %10 the lower nibble (2 red = enemy units) and %11 the colour RAM nibble (the terrain
|
|
; colour). overviewPatternLo/Hi are a split pointer table indexed by the terrain index (map cell -
|
|
; $40, or cell - $20 for the $21-$3F contour set), 33 entries covering indices 0-$20;
|
|
; buildOverviewQuadrantNoColour reads them at $5C40/$5C44.
|
|
overviewPatternLo:
|
|
.byte $9B,$9F,$A3,$A7,$AB,$9F,$A3,$A7; 5A59 ........ terrain 0 = blank ($5A9B), 1-4 = the four lower-contour edges ($5A9F/A3/A7/AB)
|
|
.byte $AB ; 5A61 terrain 5-8 repeat the same four contour edges, 9-12 all use the full-height stripe $5AAF
|
|
.byte $AF,$AF,$AF,$AF,$B3,$B3,$B3; 5A62 .......
|
|
.byte $B3 ; 5A69 terrain 13-20 all use $5AB3 (a single solid scanline), 21-23 start the river set ($5AC7/CB/CF)
|
|
.byte $B3,$B3,$B3,$B3,$C7,$CB,$CF; 5A6A .......
|
|
.byte $D3 ; 5A71 terrain 24 = $5AD3 (river), 25-28 = $5AC3 (river bank), 29-31 = $5ABB (forest stipple)
|
|
.byte $C3,$C3,$C3,$C3,$BB,$BB,$BB; 5A72 .......
|
|
.byte $BB ; 5A79 terrain $20 = $5ABB, the last forest entry
|
|
|
|
; High bytes of the same 33 pointers - every pattern lives in this page, so all entries are $5A.
|
|
overviewPatternHi:
|
|
.byte $5A,$5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A7A ZZZZZZZZ all patterns are in page $5A (entries 0-7)
|
|
.byte $5A ; 5A82 entries 8-15
|
|
.byte $5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A83 ZZZZZZZ
|
|
.byte $5A ; 5A8A entries 16-23
|
|
.byte $5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A8B ZZZZZZZ
|
|
.byte $5A ; 5A92 entries 24-31
|
|
.byte $5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A93 ZZZZZZZ
|
|
.byte $5A ; 5A9A entry 32, the last terrain index
|
|
|
|
; The 15 distinct 4-byte patterns, one row per pattern. Two of them are not reached through the
|
|
; pointer table at all: $5AB7 (solid) is the unit blob loaded at $5C14 and $5ABF (every other
|
|
; scanline) is the cloaked-unit blob loaded at $5C1C. In the bit art below $33 lights the right pixel
|
|
; of a half, $CC the left one and $FF both.
|
|
overviewPatterns:
|
|
.byte $00,$00,$00,$00 ; 5A9B .... $5A9B blank - terrain index 0 (map cell $40), all background
|
|
.byte $00 ; 5A9F $5A9F contour edge: the lower two scanlines only
|
|
.byte $00,$33,$33 ; 5AA0 .33
|
|
.byte $00 ; 5AA3 $5AA3 contour edge: lower two scanlines plus one pixel on scanline 1
|
|
.byte $CC,$33,$33 ; 5AA4 .33
|
|
.byte $33 ; 5AA7 $5AA7 contour edge: the top scanline only
|
|
.byte $00,$00,$00 ; 5AA8 ...
|
|
.byte $33 ; 5AAB $5AAB contour edge: top two scanlines
|
|
.byte $CC,$00,$00 ; 5AAC ...
|
|
.byte $33 ; 5AAF $5AAF full-height stripe - terrain indices 9-12
|
|
.byte $33,$33,$33 ; 5AB0 333
|
|
.byte $00 ; 5AB3 $5AB3 one solid scanline - terrain indices 13-$20's base shape
|
|
.byte $FF,$00,$00 ; 5AB4 ...
|
|
.byte $FF ; 5AB7 $5AB7 solid 2x4 blob = a unit; loaded directly at $5C14 and recoloured by the $55/$AA mask
|
|
.byte $FF,$FF,$FF ; 5AB8 ...
|
|
.byte $33 ; 5ABB $5ABB alternating stipple - the forest terrain
|
|
.byte $CC,$33,$CC ; 5ABC .3.
|
|
.byte $FF ; 5ABF $5ABF every other scanline = a cloaked unit; loaded directly at $5C1C
|
|
.byte $00,$FF,$00 ; 5AC0 ...
|
|
.byte $CC ; 5AC3 $5AC3 river bank
|
|
.byte $FF,$FF,$33 ; 5AC4 ..3
|
|
.byte $33 ; 5AC7 $5AC7 river, notched at the top
|
|
.byte $FF,$FF,$FF ; 5AC8 ...
|
|
.byte $CC ; 5ACB $5ACB river, notched at the top on the other side
|
|
.byte $FF,$FF,$FF ; 5ACC ...
|
|
.byte $FF ; 5ACF $5ACF river, notched at the bottom
|
|
.byte $FF,$FF,$33 ; 5AD0 ..3
|
|
.byte $FF ; 5AD3 $5AD3 river, notched at the bottom on the other side
|
|
.byte $FF,$FF,$CC ; 5AD4 ...
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawOverviewMap - Draws the whole strategic overview: the 40x40 battlefield squeezed into the 20x20
|
|
; character window at screen (1,1)-(20,20), four map cells (a 2x2 block) per character. For every
|
|
; character it clears the 8-byte build buffer at $90B8, ORs in the four cell patterns as quadrants,
|
|
; overlays the quarter/midfield lines, copies the 8 scanlines into the $A000 bitmap, writes $62 into
|
|
; screen RAM (upper nibble 6 = blue for own units, lower nibble 2 = red for the enemy) and the terrain
|
|
; colour into colour RAM at +$4C00.
|
|
; In: the $F000 map, the unit arrays, the patched side branches at $5BF7/$5BFE
|
|
; Out: 400 characters of bitmap, screen RAM and colour RAM rewritten; vicBg0Shadow $9004 = 7 (yellow);
|
|
; mapCellPtr $48/$49, scratch18/scratch19 and glyphBuildBuffer clobbered
|
|
; Called from: manualLookupCheck ($09E3), the battlefield screen builder ($2682, $4020, $4137),
|
|
; overlay A/B in many places ($76B8, $7702, $774C, $7B0C, $7D5C, $83F8, $8553), the $0400 block
|
|
; ($0758) and the text engine ($C794).
|
|
; ----------------------------------------------------------------------
|
|
drawOverviewMap:
|
|
lda #$07 ; 5AD7 colour 7 = yellow: the plain-ground background of the whole overview
|
|
sta vicBg0Shadow ; 5AD9 VIC background 0 shadow; the IRQ copies it to $D021
|
|
lda #$00 ; 5ADC map pointer low byte...
|
|
sta mapCellPtr ; 5ADE mapCellPtr low byte
|
|
lda #$F0 ; 5AE0 ...and high byte: the first cell of the 40x40 map at $F000
|
|
sta mapCellPtrHi ; 5AE2 mapCellPtr high byte - the pointer now addresses map cell (0,0)
|
|
lda #$01 ; 5AE4 first character row of the window
|
|
sta scratch18 ; 5AE6 scratch18 = character row 1..20
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Outer loop over the 20 character rows. Each pass covers two map rows; after the inner loop has
|
|
; walked 20 characters (40 map cells) the pointer is advanced by another 40 to skip the odd map row
|
|
; that the same characters already covered.
|
|
; ----------------------------------------------------------------------
|
|
L_5AE8:
|
|
lda #$01 ; 5AE8 first character column of the window
|
|
sta scratch19 ; 5AEA scratch19 = character column 1..20
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Inner loop: build one overview character out of a 2x2 block of map cells.
|
|
; ----------------------------------------------------------------------
|
|
L_5AEC:
|
|
ldx #$07 ; 5AEC 8 scanlines to clear
|
|
lda #$00 ; 5AEE blank
|
|
L_5AF0:
|
|
sta glyphBuildBuffer,x ; 5AF0 clear scanline X of the character being built at $90B8
|
|
dex ; 5AF3 next scanline down
|
|
bpl L_5AF0 ; 5AF4 until all 8 are blank
|
|
ldx #$00 ; 5AF6 quadrant 0 = top-left
|
|
ldy #$00 ; 5AF8 map offset 0 = the cell at (col, row)
|
|
lda (mapCellPtr),y ; 5AFA read the map cell
|
|
jsr buildOverviewQuadrant ; 5AFC OR its 2x4 pattern into the top-left quadrant and pick up its terrain colour
|
|
iny ; 5AFF map offset 1 = the cell to the right
|
|
inx ; 5B00 quadrant 1 = top-right
|
|
lda (mapCellPtr),y ; 5B01 read the map cell
|
|
jsr buildOverviewQuadrant ; 5B03 OR it into the top-right quadrant
|
|
ldy #$28 ; 5B06 map offset $28 = 40 = the same column one map row down
|
|
inx ; 5B08 quadrant 2 = bottom-left
|
|
lda (mapCellPtr),y ; 5B09 read the map cell
|
|
jsr buildOverviewQuadrant ; 5B0B OR it into the bottom-left quadrant
|
|
iny ; 5B0E map offset $29 = the cell diagonally down-right
|
|
inx ; 5B0F quadrant 3 = bottom-right
|
|
lda (mapCellPtr),y ; 5B10 read the map cell
|
|
jsr buildOverviewQuadrant ; 5B12 OR it into the bottom-right quadrant; its terrain colour is the one that ends up in colour RAM
|
|
jsr calcCellPointers ; 5B15 bitmapPtr/screenPtr for character (scratch19, scratch18); returns Y = character row, X = column
|
|
jsr drawOverviewQuarterLine ; 5B18 add the quarter or midfield line to scanline 0 if this character row carries one
|
|
ldy #$07 ; 5B1B 8 scanlines to store
|
|
L_5B1D:
|
|
lda glyphBuildBuffer,y ; 5B1D scanline Y of the finished character
|
|
sta (bitmapPtr),y ; 5B20 into the $A000 bitmap
|
|
dey ; 5B22 next scanline
|
|
bpl L_5B1D ; 5B23 until all 8 are written
|
|
lda #$62 ; 5B25 $62: multicolour pair %01 -> colour 6 blue (own units), %10 -> colour 2 red (enemy units)
|
|
ldy #$00 ; 5B27 one byte per character
|
|
sta (screenPtr),y ; 5B29 into screen RAM at $8C00
|
|
lda screenPtrHi ; 5B2B screen RAM page...
|
|
clc ; 5B2D carry must be clear before the page add below
|
|
adc #$4C ; 5B2E ...+ $4C00 turns $8Cxx into $D8xx = colour RAM
|
|
sta screenPtrHi ; 5B30 screenPtr now points at the colour RAM byte of this character
|
|
lda tileColour ; 5B32 terrain colour of the last cell of the 2x2 block, left in tileColour by buildOverviewQuadrant
|
|
sta (screenPtr),y ; 5B34 colour RAM supplies the %11 pixel pairs, i.e. the terrain itself
|
|
lda mapCellPtr ; 5B36 advance the map pointer...
|
|
clc ; 5B38 plain 16-bit add of the two-cell step
|
|
adc #$02 ; 5B39 ...by two cells - one character wide
|
|
sta mapCellPtr ; 5B3B store the advanced map pointer, low byte
|
|
lda mapCellPtrHi ; 5B3D carry into the map pointer high byte
|
|
adc #$00 ; 5B3F propagate the carry into the high byte
|
|
sta mapCellPtrHi ; 5B41 store the advanced map pointer, high byte
|
|
inc scratch19 ; 5B43 next character column
|
|
lda scratch19 ; 5B45 the character column just finished
|
|
cmp #$15 ; 5B47 columns 1 to 20 inclusive
|
|
bcc L_5AEC ; 5B49 keep going along the row
|
|
lda mapCellPtr ; 5B4B the row just drawn consumed 40 cells = one map row...
|
|
clc ; 5B4D plain 16-bit add of the 40-cell step
|
|
adc #$28 ; 5B4E ...so skip the second map row it also covered
|
|
sta mapCellPtr ; 5B50 store the advanced map pointer, low byte
|
|
lda mapCellPtrHi ; 5B52 carry into the map pointer high byte
|
|
adc #$00 ; 5B54 propagate the carry into the high byte
|
|
sta mapCellPtrHi ; 5B56 store the advanced map pointer, high byte
|
|
inc scratch18 ; 5B58 next character row
|
|
lda scratch18 ; 5B5A the character row just finished
|
|
cmp #$15 ; 5B5C rows 1 to 20 inclusive
|
|
bcs L_5B63 ; 5B5E all 20 rows done
|
|
jmp L_5AE8 ; 5B60 next character row of the overview
|
|
|
|
L_5B63:
|
|
rts ; 5B63 the whole 20x20 overview has been redrawn
|
|
|
|
|
|
; Quadrant geometry of an overview character. Quadrant 0 = top-left, 1 = top-right, 2 = bottom-left,
|
|
; 3 = bottom-right; each is 2 multicolour pixels wide and 4 scanlines tall.
|
|
quadrantPixelMask:
|
|
.byte $F0,$0F,$F0,$0F ; 5B64 .... AND mask per quadrant: $F0 keeps the left pixel pair of the character, $0F the right one
|
|
|
|
; quadrantRowOffset: byteTable, 4 bytes. first scanline of quadrant 0-3: 0,0,4,4 (4 rows each)
|
|
quadrantRowOffset:
|
|
.byte $00,$00,$04,$04 ; 5B68 .... first scanline per quadrant: 0 for the two top quadrants, 4 for the two bottom ones
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawOverviewQuarterLine - Puts the field markings back on top of an overview character. Modem Wars
|
|
; is played on a football pitch, and the overview shows the quarter lines at map rows 10, 20 and 30,
|
|
; which land on the first scanline of character rows 6, 11 and 16. On the midfield row (11) both
|
|
; pixel pairs of the character get colour 1, on rows 6 and 16 only the right-hand one; a half that
|
|
; already reads %1010 - a solid enemy-unit blob - is left alone so the line does not put a blue dot in
|
|
; the middle of a red unit. Only scanline 0 of the buffer at $90B8 is touched.
|
|
; In: Y = character row of the overview (1-20); glyphBuildBuffer $90B8
|
|
; Out: glyphBuildBuffer[0] possibly modified; A/X/Y clobbered; self-modifies the operands at $5B95 and
|
|
; $5BA8
|
|
; Called from: drawOverviewMap ($5B18) for every character and drawMapCell ($5CFD) when a single cell
|
|
; is redrawn.
|
|
; ----------------------------------------------------------------------
|
|
drawOverviewQuarterLine:
|
|
ldx #$00 ; 5B6C X = 0 means 'draw both pixel pairs'
|
|
cpy #$0B ; 5B6E character row 11 = map rows 20/21 - the midfield line
|
|
beq L_5B7B ; 5B70 midfield: leave X at 0 so both halves get a marker
|
|
cpy #$06 ; 5B72 character row 6 = map rows 10/11 - the first quarter line
|
|
beq L_5B7A ; 5B74 row 6: only the right-hand pixel carries this quarter line
|
|
cpy #$10 ; 5B76 character row 16 = map rows 30/31 - the third quarter line
|
|
bne L_5BAC ; 5B78 any other row carries no field marking at all
|
|
L_5B7A:
|
|
dex ; 5B7A X = $FF: quarter lines are drawn in the right-hand pixel only
|
|
L_5B7B:
|
|
stx L_5B94+1 ; 5B7B self-modify the LDA # at $5B95 - it gates the left-hand half below
|
|
lda glyphBuildBuffer ; 5B7E scanline 0 of the character being built
|
|
and #$F0 ; 5B81 keep the left pixel pair (pixels 0 and 1)
|
|
tay ; 5B83 hold it in Y while the right half is dealt with
|
|
lda glyphBuildBuffer ; 5B84 scanline 0 again
|
|
and #$0F ; 5B87 keep the right pixel pair (pixels 2 and 3)
|
|
tax ; 5B89 hold it in X
|
|
eor #$0A ; 5B8A %1010 = both pixels colour 2 = a solid enemy blob
|
|
beq L_5B94 ; 5B8C leave an enemy unit alone rather than dotting it
|
|
txa ; 5B8E the right half
|
|
and #$0C ; 5B8F keep pixel 2, clear pixel 3
|
|
ora #$01 ; 5B91 pixel 3 (the rightmost of the character) := colour 1
|
|
tax ; 5B93 back into X
|
|
L_5B94:
|
|
lda #$FF ; 5B94 operand patched at $5B7B: $FF for a quarter line, $00 for the midfield line
|
|
bmi L_5BA3 ; 5B96 quarter line: only the right-hand pixel is drawn, skip the left half
|
|
tya ; 5B98 the left half
|
|
eor #$A0 ; 5B99 %10100000 = both pixels colour 2 = a solid enemy blob
|
|
beq L_5BA3 ; 5B9B leave an enemy unit alone
|
|
tya ; 5B9D the left half again
|
|
and #$C0 ; 5B9E keep pixel 0, clear pixel 1
|
|
ora #$10 ; 5BA0 pixel 1 := colour 1
|
|
tay ; 5BA2 back into Y
|
|
L_5BA3:
|
|
sty L_5BA7+1 ; 5BA3 self-modify the ORA # at $5BA8 with the finished left half
|
|
txa ; 5BA6 the finished right half
|
|
L_5BA7:
|
|
ora #$FF ; 5BA7 operand patched at $5BA3: recombine the two halves
|
|
sta glyphBuildBuffer ; 5BA9 write scanline 0 of the character back
|
|
L_5BAC:
|
|
rts ; 5BAC done (also the exit for rows that carry no line)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildOverviewQuadrant - Colour-aware entry of the overview cell renderer. It looks the terrain
|
|
; colour of map cell A up (terrainClassTable $07BE indexed by the terrain index, then
|
|
; terrainColourTable $07EA) and leaves it in tileColour ($32) for the caller to write into colour RAM,
|
|
; then falls into buildOverviewQuadrantNoColour to OR the pattern into quadrant X. A cell holding a
|
|
; unit is coloured after the terrain hidden under it. Cell $40 and any out-of-range value leave the
|
|
; colour untouched, so the character keeps the colour of the previous quadrant.
|
|
; In: A = map cell byte ($21-$6B terrain, or $80|unitIndex); X = quadrant 0-3; Y = anything
|
|
; (restored)
|
|
; Out: tileColour $32 = the terrain colour nibble; the quadrant ORed into glyphBuildBuffer $90B8;
|
|
; A/X/Y restored
|
|
; Called from: drawOverviewMap ($5AFC, $5B03, $5B0B, $5B12), once per cell of the 2x2 block.
|
|
; ----------------------------------------------------------------------
|
|
buildOverviewQuadrant:
|
|
sty L_5C79+1 ; 5BAD stash Y in the LDY # at $5C7A so the exit can restore it
|
|
stx L_5C77+1 ; 5BB0 stash the quadrant number in the LDX # at $5C78; $5C4F reads it back
|
|
pha ; 5BB3 keep the raw cell byte for the pattern lookup
|
|
tay ; 5BB4 test bit 7 of the cell byte
|
|
bpl L_5BBA ; 5BB5 plain terrain cell - use it directly
|
|
lda unitTerrainUnderAlias,y ; 5BB7 a unit stands here: colour after the terrain it hides ($FAF0 + index, reached as $FA70 + $80 + index)
|
|
L_5BBA:
|
|
sec ; 5BBA prepare a true subtraction of the terrain base
|
|
sbc #$40 ; 5BBB terrain cells $41-$60 become indices 1-$20
|
|
beq L_5BD0 ; 5BBD cell $40 is the blank tile: keep the colour of the previous quadrant
|
|
bcs L_5BC3 ; 5BBF cell above $40 - the index is already right
|
|
adc #$20 ; 5BC1 cells below $40 (the $21-$3F contour set) become index cell-$20
|
|
L_5BC3:
|
|
cmp #$21 ; 5BC3 33 valid terrain indices, 0 to $20
|
|
bcs L_5BD0 ; 5BC5 out of range: keep the colour of the previous quadrant
|
|
tay ; 5BC7 index the class table with it
|
|
ldx D_07BE,y ; 5BC8 terrainClassTable read as $07BE + index: class 0-8 (0 plain, 1 river, 2 forest, 3/5 contour, 4/6 hill, 7 recycler)
|
|
lda terrainColourTable,x ; 5BCB $07EA: 7 yellow plain, 3 cyan river, 5 green forest, 8 orange contours and hills, 0 black
|
|
sta tileColour ; 5BCE the caller stores this in colour RAM for the whole character
|
|
L_5BD0:
|
|
pla ; 5BD0 the cell byte again
|
|
jmp L_5BDA ; 5BD1 enter buildOverviewQuadrantNoColour past the two saves we have already done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildOverviewQuadrantNoColour - ORs the 2x4 pixel pattern of map cell A into quadrant X of the
|
|
; 8-byte character buffer at $90B8, without touching the colour. Terrain cells pick their 4-byte
|
|
; pattern through overviewPatternLo/Hi. A cell holding a unit gets the solid blob $5AB7 (or $5ABF
|
|
; when the unit is cloaked, bit 7 of the waypoint-column array $F898) masked with overviewUnitPattern
|
|
; $922B, which is $55 (every pixel pair %01 = blue) for our own units and $AA (%10 = red) for the
|
|
; opponent's. A unit is drawn only if it is ours, if the omniscient replay view is on, if reveal-all
|
|
; is set or if it has been spotted (bit 7 of the energy array $F9C4); otherwise, and for dead units
|
|
; and while the comcen map system is knocked out ($9242 bit 7), the terrain hidden under the unit is
|
|
; drawn instead.
|
|
; In: A = map cell byte; X = quadrant 0-3; Y = anything (restored); overviewUnitPattern $922B,
|
|
; comcenBattleStatus $9242, filmPlaybackMode $0B9B, revealAllUnits $92FC, unit arrays
|
|
; $F76C/$F898/$F9C4/$FAF0
|
|
; Out: four scanlines of glyphBuildBuffer $90B8 ORed; A/X/Y restored; self-modifies the operands at
|
|
; $5C19, $5C65, $5C66, $5C68, $5C6A, $5C74, $5C78, $5C7A
|
|
; Called from: drawMapCell ($5CF6) and, past its two save instructions, from buildOverviewQuadrant
|
|
; ($5BD1).
|
|
; ----------------------------------------------------------------------
|
|
buildOverviewQuadrantNoColour:
|
|
sty L_5C79+1 ; 5BD4 stash Y in the LDY # at $5C7A so the exit can restore it
|
|
stx L_5C77+1 ; 5BD7 stash the quadrant number in the LDX # at $5C78; $5C4F reads it back
|
|
L_5BDA:
|
|
ldy #$FF ; 5BDA $FF = no unit recolouring, let the pattern through unchanged
|
|
sty L_5C67+1 ; 5BDC self-modify the AND # at $5C68 (the unit colour mask)
|
|
tay ; 5BDF Y = the cell byte, so unit arrays are indexed through their $80-biased aliases
|
|
bpl L_5C2B ; 5BE0 bit 7 clear = plain terrain: skip everything unit related
|
|
lda comcenBattleStatus ; 5BE2 $9242 = the comcen's map system; bit 7 set means it has been knocked out
|
|
bmi L_5C23 ; 5BE5 no map system: the overview shows bare terrain, units are invisible
|
|
lda unitPathColTableAlias,y ; 5BE7 waypoint-column array $F898 + index (via the $F818 alias); bit 7 = the unit is cloaked
|
|
sta L_5C18+1 ; 5BEA park it in the LDX # at $5C19 - the cloak test runs after the pattern has been chosen
|
|
lda unitTypeTableAlias,y ; 5BED unit type array $F76C + index (via the $F6EC alias); bit 7 = destroyed
|
|
bmi L_5C23 ; 5BF0 dead unit: draw the terrain it is lying on instead
|
|
lda overviewUnitPattern ; 5BF2 $922B = $55: every pixel pair %01, i.e. screen RAM upper nibble = colour 6 blue
|
|
cpy #$B2 ; 5BF5 $B2 = $80|50: C=1 for unit indices 50-99, the second half of the unit array
|
|
D_5BF7:
|
|
bcc L_5BFB ; 5BF7 opcode patched by patchOwnSideBranches ($7BE1): BCC for side 0, BCS for side 1, so it is taken for OUR units
|
|
eor #$FF ; 5BF9 opponent's unit: $55 becomes $AA, every pixel pair %10 = screen RAM lower nibble = colour 2 red
|
|
L_5BFB:
|
|
sta L_5C67+1 ; 5BFB this mask recolours the solid $FF pattern (self-modifies the AND # at $5C68)
|
|
D_5BFE:
|
|
bcc L_5C14 ; 5BFE opcode patched by patchUnitSideBranches ($2A16), inverted when a replay is watching THEM: units of the watched side are always visible
|
|
lda filmPlaybackMode ; 5C00 $0B9B: bit 7 = film playback, bits 0-1 = the viewpoint
|
|
cmp #$82 ; 5C03 $80|2 = playback with the SHOW BOTH viewpoint
|
|
bcs L_5C11 ; 5C05 omniscient replay: the enemy unit is drawn too
|
|
lda revealAllUnits ; 5C07 $92FC bit 7 = reveal every unit (set when the game ends)
|
|
bmi L_5C11 ; 5C0A reveal-all: draw the enemy unit
|
|
lda unitEnergyTableAlias,y ; 5C0C energy array $F9C4 + index (via the $F944 alias); bit 7 = this enemy has been spotted
|
|
bpl L_5C23 ; 5C0F never spotted: only the terrain it is standing on is shown
|
|
L_5C11:
|
|
lda L_5C67+1 ; 5C11 dead instruction - A is overwritten by the LDA # below and no flag is tested (leftover)
|
|
L_5C14:
|
|
lda #$B7 ; 5C14 pattern $5AB7 = the solid 2x4 unit blob
|
|
ldy #$5A ; 5C16 high byte of the pattern address
|
|
L_5C18:
|
|
ldx #$FF ; 5C18 operand patched at $5BEA with the unit's waypoint-column byte; bit 7 = cloaked
|
|
bpl L_5C49 ; 5C1A not cloaked: keep the solid blob
|
|
lda #$BF ; 5C1C cloaked: pattern $5ABF, only every other scanline is drawn
|
|
ldy #$5A ; 5C1E high byte of the pattern address
|
|
jmp L_5C49 ; 5C20 go and plot it
|
|
|
|
L_5C23:
|
|
lda #$FF ; 5C23 $FF = no unit recolouring...
|
|
sta L_5C67+1 ; 5C25 ...so the terrain pattern goes through untouched (self-modifies the AND # at $5C68)
|
|
lda unitTerrainUnderAlias,y ; 5C28 terrain byte hidden underneath the unit ($FAF0 + index, via the $FA70 alias)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shared terrain path: turn the cell byte into a terrain index 0-$20 and fetch the matching 4-byte
|
|
; pattern out of the split pointer table at $5A59/$5A7A.
|
|
; ----------------------------------------------------------------------
|
|
L_5C2B:
|
|
sec ; 5C2B prepare a true subtraction of the terrain base
|
|
sbc #$40 ; 5C2C terrain cells $41-$60 become indices 1-$20
|
|
beq L_5C38 ; 5C2E cell $40 is the blank tile
|
|
bcs L_5C34 ; 5C30 cell above $40 - the index is already right
|
|
adc #$20 ; 5C32 cells below $40 (the $21-$3F contour set) become index cell-$20
|
|
L_5C34:
|
|
cmp #$21 ; 5C34 33 valid terrain indices, 0 to $20
|
|
bcc L_5C3F ; 5C36 in range - look the pattern up
|
|
L_5C38:
|
|
lda #$9B ; 5C38 blank pattern $5A9B: nothing but background
|
|
ldy #$5A ; 5C3A high byte of the pattern address
|
|
jmp L_5C49 ; 5C3C go and plot it
|
|
|
|
L_5C3F:
|
|
tay ; 5C3F terrain index into Y
|
|
lda overviewPatternLo,y ; 5C40 low byte of this terrain's 4-byte pattern
|
|
pha ; 5C43 park it while the high byte is fetched
|
|
lda overviewPatternHi,y ; 5C44 high byte (always $5A)
|
|
tay ; 5C47 into Y for the store below
|
|
pla ; 5C48 low byte back into A
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Plot the chosen 4-byte pattern into the quadrant: each of the four scanlines is masked with the unit
|
|
; colour mask, then with the half-of-the-character mask, and ORed into the build buffer.
|
|
; ----------------------------------------------------------------------
|
|
L_5C49:
|
|
sta L_5C64+1 ; 5C49 self-modify the LDA abs,Y at $5C64 with the pattern address, low byte
|
|
sty L_5C64+2 ; 5C4C ...and high byte
|
|
ldx L_5C77+1 ; 5C4F the quadrant number saved on entry
|
|
lda quadrantPixelMask,x ; 5C52 $F0 = the left pixel pair of the character, $0F = the right one
|
|
sta L_5C69+1 ; 5C55 self-modify the second AND # at $5C6A
|
|
lda quadrantRowOffset,x ; 5C58 0 for the top quadrants, 4 for the bottom ones
|
|
tax ; 5C5B X = first scanline of the quadrant inside the 8-byte buffer
|
|
clc ; 5C5C plain add, no carry in
|
|
adc #$04 ; 5C5D each quadrant is 4 scanlines tall
|
|
sta L_5C73+1 ; 5C5F self-modify the CPX # at $5C74 with the end scanline
|
|
ldy #$00 ; 5C62 start at byte 0 of the pattern
|
|
L_5C64:
|
|
lda irqVectorHi,y ; 5C64 operand patched at $5C49: scanline Y of the chosen pattern
|
|
L_5C67:
|
|
and #$FF ; 5C67 operand patched at $5BDC/$5BFB/$5C25: $55 own unit, $AA enemy unit, $FF terrain
|
|
L_5C69:
|
|
and #$FF ; 5C69 operand patched at $5C55: keep only this quadrant's half of the byte
|
|
ora glyphBuildBuffer,x ; 5C6B OR into the character being built at $90B8
|
|
sta glyphBuildBuffer,x ; 5C6E store it back
|
|
iny ; 5C71 next pattern byte
|
|
inx ; 5C72 next scanline of the character
|
|
L_5C73:
|
|
cpx #$FF ; 5C73 operand patched at $5C5F: first scanline + 4
|
|
bcc L_5C64 ; 5C75 four scanlines per quadrant
|
|
L_5C77:
|
|
ldx #$FF ; 5C77 operand patched at $5BB0/$5BD7: restore the caller's quadrant number
|
|
L_5C79:
|
|
ldy #$FF ; 5C79 operand patched at $5BAD/$5BD4: restore the caller's Y
|
|
L_5C7B:
|
|
rts ; 5C7B done (drawMapCell borrows this RTS for its blackout exit)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawMapCell - Redraws a single battlefield cell everywhere it can currently be seen. On the
|
|
; battlefield screen that means the 16x16 tile in the 7x5 tactical view (only when the cell is inside
|
|
; it) plus one 2x4-pixel quadrant of the strategic overview, which is rebuilt in place: the character
|
|
; is read back out of the bitmap, the quadrant's old pixels are cleared, the new pattern is ORed in,
|
|
; the field markings are restored and the character is written back. On the comcen radar screen it
|
|
; hands the work to drawScreen1Cell. Nothing is drawn at all during the comcen knock-out blackout.
|
|
; In: A = the map cell byte, X = map column, Y = map row (saved in savedRegA/X/Y $54-$56);
|
|
; currentScreen $90FB; viewOriginCol/Row $A5/$A6; comcenStunStatus $922A bit 6
|
|
; Out: bitmap, screen RAM and colour RAM updated; A/X/Y restored to the values passed in;
|
|
; self-modifies the operands at $5CD1, $5CE9, $5CEF, $5CFA and $5CFC
|
|
; Called from: the unit redraw helpers at $3F89 and $437D and, as a tail jump, from $48AE - i.e.
|
|
; whenever a unit moves, is hit or is removed.
|
|
; ----------------------------------------------------------------------
|
|
drawMapCell:
|
|
bit comcenStunStatus ; 5C7C $922A: bit 6 is set while the comcen knock-out blackout sequence runs
|
|
bvs L_5C7B ; 5C7F blackout in progress - draw nothing (borrows the RTS at the end of the routine above)
|
|
jsr saveRegsAXY ; 5C81 $C0E8: A/X/Y -> savedRegA/savedRegX/savedRegY = cell byte, column, row
|
|
ldx currentScreen ; 5C84 0 = battlefield screen, 1 = comcen radar screen, 3 = menus
|
|
beq L_5C8C ; 5C87 battlefield: the tactical view and the overview both need updating
|
|
jmp L_5D0A ; 5C89 any other screen: skip straight to the screen-1 test
|
|
|
|
L_5C8C:
|
|
lda savedRegX ; 5C8C map column of the cell
|
|
sec ; 5C8E prepare a true subtraction
|
|
sbc viewOriginCol ; 5C8F position relative to the left edge of the tactical view
|
|
cmp #$07 ; 5C91 the view is 7 cells wide; an unsigned compare also rejects columns left of the origin
|
|
bcs L_5CA1 ; 5C93 outside the view - only the overview needs the update
|
|
lda savedRegY ; 5C95 map row of the cell
|
|
sec ; 5C97 prepare a true subtraction
|
|
sbc viewOriginRow ; 5C98 position relative to the top edge of the tactical view
|
|
cmp #$05 ; 5C9A the view is 5 cells tall
|
|
bcs L_5CA1 ; 5C9C outside the view
|
|
jsr drawViewportCell ; 5C9E redraw the 16x16 tile inside the tactical view
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Overview update: find the character that holds this cell, read it back out of the bitmap, erase just
|
|
; the quadrant belonging to the cell, OR the new pattern in, restore the field marking and write the
|
|
; character back. The colour RAM nibble is deliberately left alone - a single cell cannot change the
|
|
; colour of the 2x2 block it shares.
|
|
; ----------------------------------------------------------------------
|
|
L_5CA1:
|
|
jsr restoreRegsAXY ; 5CA1 $C030: A/X/Y = cell byte, column, row again
|
|
txa ; 5CA4 map column
|
|
lsr a ; 5CA5 two map columns share one overview character
|
|
clc ; 5CA6 the LSR above may have left the carry set - clear it before the add
|
|
adc #$01 ; 5CA7 the overview window starts at screen column 1
|
|
tax ; 5CA9 X = character column
|
|
tya ; 5CAA map row
|
|
lsr a ; 5CAB two map rows share one overview character
|
|
clc ; 5CAC the LSR above may have left the carry set - clear it before the add
|
|
adc #$01 ; 5CAD the overview window starts at screen row 1
|
|
tay ; 5CAF Y = character row
|
|
sty L_5CFB+1 ; 5CB0 self-modify the LDY # at $5CFC - the quarter-line pass needs the character row
|
|
stx L_5CF9+1 ; 5CB3 self-modify the LDX # at $5CFA (the value it loads is never used - see $5CF9)
|
|
jsr calcCellPointersXY ; 5CB6 bitmapPtr/screenPtr of that character cell
|
|
ldy #$07 ; 5CB9 8 scanlines
|
|
L_5CBB:
|
|
lda (bitmapPtr),y ; 5CBB read scanline Y of the character out of the bitmap...
|
|
sta glyphBuildBuffer,y ; 5CBD ...into the build buffer at $90B8
|
|
dey ; 5CC0 next scanline
|
|
bpl L_5CBB ; 5CC1 until all 8 have been copied back
|
|
lda savedRegY ; 5CC3 map row of the cell
|
|
and #$01 ; 5CC5 odd row = the lower half of the character
|
|
asl a ; 5CC7 quadrants 2 and 3 are the lower ones
|
|
sta L_5CD0+1 ; 5CC8 self-modify the ADC # at $5CD1
|
|
lda savedRegX ; 5CCB map column of the cell
|
|
and #$01 ; 5CCD odd column = the right half of the character
|
|
clc ; 5CCF clear the carry for the quadrant add below
|
|
L_5CD0:
|
|
adc #$FF ; 5CD0 operand patched at $5CC8: quadrant = (row&1)*2 + (col&1), so 0-3
|
|
tax ; 5CD2 X = quadrant number
|
|
pha ; 5CD3 keep it across the clearing loop
|
|
lda quadrantPixelMask,x ; 5CD4 $F0 or $0F: the half of the byte this quadrant owns
|
|
eor #$FF ; 5CD7 invert it into an AND mask that clears exactly those pixels
|
|
sta L_5CE8+1 ; 5CD9 self-modify the AND # at $5CE9
|
|
lda quadrantRowOffset,x ; 5CDC first scanline of the quadrant, 0 or 4
|
|
tax ; 5CDF X = scanline index into the build buffer
|
|
adc #$04 ; 5CE0 +4 scanlines; the carry is still clear from the ADC at $5CD0, whose result can never exceed 3
|
|
sta L_5CEE+1 ; 5CE2 self-modify the CPX # at $5CEF with the end scanline
|
|
L_5CE5:
|
|
lda glyphBuildBuffer,x ; 5CE5 scanline X of the character
|
|
L_5CE8:
|
|
and #$FF ; 5CE8 operand patched at $5CD9: blank this quadrant's pixels, leave the neighbour's alone
|
|
sta glyphBuildBuffer,x ; 5CEA store it back
|
|
inx ; 5CED next scanline
|
|
L_5CEE:
|
|
cpx #$FF ; 5CEE operand patched at $5CE2: first scanline + 4
|
|
bcc L_5CE5 ; 5CF0 four scanlines per quadrant
|
|
pla ; 5CF2 quadrant number back off the stack
|
|
tax ; 5CF3 into X for the call below
|
|
lda savedRegA ; 5CF4 the new map cell byte
|
|
jsr buildOverviewQuadrantNoColour; 5CF6 OR its pattern into the freshly cleared quadrant (the colour is left untouched)
|
|
L_5CF9:
|
|
ldx #$FF ; 5CF9 operand patched at $5CB3; dead - drawOverviewQuarterLine opens with LDX #$00 (leftover)
|
|
L_5CFB:
|
|
ldy #$FF ; 5CFB operand patched at $5CB0: the character row, which the quarter-line test needs
|
|
jsr drawOverviewQuarterLine ; 5CFD put the quarter or midfield line back on scanline 0 if this row carries one
|
|
ldy #$07 ; 5D00 8 scanlines
|
|
L_5D02:
|
|
lda glyphBuildBuffer,y ; 5D02 scanline Y of the rebuilt character
|
|
sta (bitmapPtr),y ; 5D05 back into the $A000 bitmap
|
|
dey ; 5D07 next scanline
|
|
bpl L_5D02 ; 5D08 until all 8 are written
|
|
L_5D0A:
|
|
lda currentScreen ; 5D0A which screen is up
|
|
cmp #$01 ; 5D0D screen 1 = the comcen radar/scanner map
|
|
bne L_5D17 ; 5D0F not the radar screen - nothing more to do
|
|
jsr restoreRegsAXY ; 5D11 A/X/Y = cell byte, column, row
|
|
jsr drawScreen1Cell ; 5D14 redraw the cell in the radar window as well
|
|
L_5D17:
|
|
jmp restoreRegsAXY ; 5D17 leave with A/X/Y exactly as the caller passed them
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawViewportCell - Draws one map cell as a 16x16 pixel tile (2x2 characters) in the 7x5 tactical
|
|
; view on the right of the battlefield screen. The screen position is character column 2*(col -
|
|
; viewOriginCol) + 25 and row 2*(row - viewOriginRow) + 1. renderMapTile reads the cell's own map
|
|
; coordinates out of viewOriginCol/Row, so those are borrowed for the duration and restored
|
|
; afterwards; it builds the 32-byte tile in the buffer at $04C8 and writes the four screen RAM colour
|
|
; bytes itself, and this routine copies the 32 bytes into the bitmap - the first 16 at the tile
|
|
; origin, the second 16 one character row (320 bytes) lower.
|
|
; In: savedRegA/X/Y ($54/$55/$56) = cell byte, map column, map row; viewOriginCol/Row $A5/$A6
|
|
; Out: four character cells of the $A000 bitmap and the matching screen/colour RAM written;
|
|
; drawCellSaveCol/Row $90F6/$90F7 used as scratch; viewOriginCol/Row restored; self-modifies the
|
|
; operand at $5D58/$5D59
|
|
; Called from: drawMapCell ($5C9E) and the view scroller scrollViewBitmap ($1F1B, $1F44) when a new
|
|
; row or column is exposed.
|
|
; ----------------------------------------------------------------------
|
|
drawViewportCell:
|
|
lda viewOriginCol ; 5D1A current left edge of the tactical view
|
|
sta drawCellSaveCol ; 5D1C keep it: viewOriginCol is about to be borrowed
|
|
lda viewOriginRow ; 5D1F current top edge of the tactical view
|
|
sta drawCellSaveRow ; 5D21 keep it too
|
|
ldy savedRegY ; 5D24 map row of the cell
|
|
ldx savedRegX ; 5D26 map column of the cell
|
|
txa ; 5D28 map column of the cell
|
|
sec ; 5D29 prepare a true subtraction
|
|
sbc viewOriginCol ; 5D2A 0-6: position of the cell inside the 7-cell-wide view
|
|
stx viewOriginCol ; 5D2C renderMapTile reads the cell's own column out of $A5
|
|
asl a ; 5D2E each cell is 2 characters wide
|
|
clc ; 5D2F the ASL above may have set the carry - clear it before the add
|
|
adc #$19 ; 5D30 the tactical view starts at screen character column 25
|
|
tax ; 5D32 X = character column of the tile
|
|
tya ; 5D33 map row of the cell
|
|
sec ; 5D34 prepare a true subtraction
|
|
sbc viewOriginRow ; 5D35 0-4: position of the cell inside the 5-cell-tall view
|
|
sty viewOriginRow ; 5D37 renderMapTile reads the cell's own row out of $A6
|
|
asl a ; 5D39 each cell is 2 characters tall
|
|
clc ; 5D3A the ASL above may have set the carry - clear it before the add
|
|
adc #$01 ; 5D3B the tactical view starts at screen character row 1
|
|
tay ; 5D3D Y = character row of the tile
|
|
jsr calcCellPointersXY ; 5D3E bitmapPtr = the tile's top-left character, screenPtr = its screen RAM byte
|
|
ldy #$00 ; 5D41 renderMapTile writes screen and colour RAM at (screenPtr),Y
|
|
lda savedRegA ; 5D43 the map cell byte
|
|
jsr renderMapTile ; 5D45 build the 32-byte 16x16 multicolour tile in $04C8 and colour the four screen cells
|
|
lda bitmapPtr ; 5D48 bitmap destination, low byte...
|
|
sta L_5D57+1 ; 5D4A ...into the STA operand at $5D58
|
|
lda bitmapPtrHi ; 5D4D ...high byte...
|
|
sta L_5D57+2 ; 5D4F ...into the STA operand at $5D59
|
|
ldy #$00 ; 5D52 32 tile bytes, index 0 upwards
|
|
L_5D54:
|
|
lda play4Row12,y ; 5D54 byte Y of the tile buffer at $04C8
|
|
L_5D57:
|
|
sta irqVectorHi,y ; 5D57 operand patched at $5D4A/$5D68: the current row of characters in the $A000 bitmap
|
|
iny ; 5D5A next tile byte
|
|
cpy #$20 ; 5D5B 32 bytes = four 8-byte character cells
|
|
bcs L_5D74 ; 5D5D whole tile copied
|
|
cpy #$10 ; 5D5F after 16 bytes the upper pair of characters is done
|
|
bne L_5D54 ; 5D61 still in the upper row - keep copying
|
|
clc ; 5D63 plain 16-bit add of the $130 step
|
|
lda bitmapPtr ; 5D64 bitmap origin of the tile...
|
|
adc #$30 ; 5D66 ...plus $130; Y is already $10, so the next store lands at origin + $140 = 320 bytes = one character row lower
|
|
sta L_5D57+1 ; 5D68 new destination, low byte
|
|
lda bitmapPtrHi ; 5D6B bitmap origin of the tile, high byte...
|
|
adc #$01 ; 5D6D ...plus the carry and the $100 of the $130 step
|
|
sta L_5D57+2 ; 5D6F new destination, high byte
|
|
bne L_5D54 ; 5D72 always taken - a bitmap address in $A000-$BFFF never has a zero high byte
|
|
L_5D74:
|
|
lda drawCellSaveCol ; 5D74 the real left edge of the view
|
|
sta viewOriginCol ; 5D77 put it back
|
|
lda drawCellSaveRow ; 5D79 the real top edge of the view
|
|
sta viewOriginRow ; 5D7C put it back
|
|
rts ; 5D7E done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawScreen1Cell - Redraws one map cell on screen 1, the comcen radar/scanner map, where the window
|
|
; is 21 cells wide by 17 tall at character (3,3) and every cell is a single 8x8 glyph. If the cell
|
|
; holds our own comcen the window has to follow it, so the whole job is handed to the overlay routine
|
|
; at $7959; otherwise the cell is clipped against the window, the bitmap and colour RAM pointers are
|
|
; built from the row/column tables at $9300 with a +3 character offset, the radar palette is set (dark
|
|
; grey for our units and the terrain, white for the enemy) and the overlay routines at $74C3 and $7396
|
|
; turn the cell byte into a glyph and plot it. viewOriginCol/Row are borrowed as the cell's
|
|
; coordinates while that happens.
|
|
; In: savedRegA/X/Y ($54/$55/$56) = cell byte, map column, map row; viewOriginCol/Row $A5/$A6 =
|
|
; top-left cell of the radar window; comcenUnit $9236
|
|
; Out: one glyph in the bitmap and one byte in colour RAM;
|
|
; radarOwnUnitColour/radarEnemyUnitColour/radarTerrainColour $9237-$9239 set; bitmapPtr/screenPtr
|
|
; and drawCellSaveCol/Row $90F6/$90F7 used; viewOriginCol/Row restored
|
|
; Called from: drawMapCell ($5D14) and, in both $6F00 overlay variants, from $794F.
|
|
; ----------------------------------------------------------------------
|
|
drawScreen1Cell:
|
|
lda savedRegA ; 5D7F the map cell byte
|
|
bpl L_5D8D ; 5D81 plain terrain - nothing special about it
|
|
and #$7F ; 5D83 strip the unit marker to get the unit index
|
|
cmp comcenUnit ; 5D85 $9236 = 49 or 99, this player's own command centre
|
|
bne L_5D8D ; 5D88 some other unit - draw it normally
|
|
jmp scrollRadarOnComcenMove ; 5D8A the radar window is centred on our comcen, so a comcen move scrolls the whole window ($7959)
|
|
|
|
L_5D8D:
|
|
lda savedRegX ; 5D8D map column of the cell
|
|
sec ; 5D8F prepare a true subtraction
|
|
sbc viewOriginCol ; 5D90 column relative to the left edge of the radar window
|
|
tax ; 5D92 keep it in X for the bitmap column table
|
|
cmp #$15 ; 5D93 the radar window shows 21 columns
|
|
bcc L_5D98 ; 5D95 inside the window
|
|
L_5D97:
|
|
rts ; 5D97 off the window - nothing to redraw
|
|
|
|
L_5D98:
|
|
lda savedRegY ; 5D98 map row of the cell
|
|
sec ; 5D9A prepare a true subtraction
|
|
sbc viewOriginRow ; 5D9B row relative to the top edge of the radar window
|
|
tay ; 5D9D keep it in Y for the bitmap row table
|
|
cmp #$11 ; 5D9E the radar window shows 17 rows
|
|
bcs L_5D97 ; 5DA0 off the window
|
|
lda #$0B ; 5DA2 dead instruction - the next one loads the very same value (leftover)
|
|
lda #$0B ; 5DA4 colour $0B = dark grey
|
|
sta radarOwnUnitColour ; 5DA6 $9237: the colour our own units are plotted in on the radar
|
|
sta radarTerrainColour ; 5DA9 $9239: and the colour the terrain glyphs get
|
|
lda #$01 ; 5DAC colour 1 = white
|
|
sta radarEnemyUnitColour ; 5DAE $9238: enemy blips stand out against the grey
|
|
clc ; 5DB1 plain 16-bit add of row base + column offset
|
|
lda bitmapRowLoTableFrom3,y ; 5DB2 bitmap row address table $9300, offset by 3 rows - the window starts at character row 3
|
|
adc bitmapColLoTableFrom3,x ; 5DB5 column*8 table $9332, offset by 3 columns - and at character column 3
|
|
sta bitmapPtr ; 5DB8 bitmapPtr low byte
|
|
lda bitmapRowHiTableFrom3,y ; 5DBA high half of the row table ($9319 + 3)
|
|
adc bitmapColHiTableFrom3,x ; 5DBD high half of the column table ($935A + 3)
|
|
sta bitmapPtrHi ; 5DC0 bitmapPtr high byte
|
|
clc ; 5DC2 plain 16-bit add
|
|
lda screenRowLoTableFrom3,y ; 5DC3 screen RAM row address table $9382, offset by 3 rows
|
|
adc #$03 ; 5DC6 plus the 3-column indent of the window
|
|
sta screenPtr ; 5DC8 screenPtr low byte
|
|
lda screenRowHiTableFrom3,y ; 5DCA high half of the screen row table ($939B + 3)
|
|
adc #$00 ; 5DCD propagate the carry
|
|
sta screenPtrHi ; 5DCF screenPtr high byte
|
|
clc ; 5DD1 plain 16-bit add
|
|
txa ; 5DD2 the cell's column inside the window
|
|
adc screenPtr ; 5DD3 add it to the row address
|
|
sta screenPtr ; 5DD5 screenPtr low byte = row base + 3 + window column
|
|
lda screenPtrHi ; 5DD7 screenPtr high byte...
|
|
adc #$4C ; 5DD9 $8C00 + $4C00 = $D800: point straight at colour RAM
|
|
sta screenPtrHi ; 5DDB screenPtr now addresses colour RAM directly
|
|
lda viewOriginCol ; 5DDD current left edge of the radar window
|
|
sta drawCellSaveCol ; 5DDF keep it: viewOriginCol is about to be borrowed
|
|
lda viewOriginRow ; 5DE2 current top edge of the radar window
|
|
sta drawCellSaveRow ; 5DE4 keep it too
|
|
jsr restoreRegsAXY ; 5DE7 A = cell byte, X = map column, Y = map row
|
|
stx viewOriginCol ; 5DEA getRadarCellGlyph reads the cell's own column out of $A5
|
|
sty viewOriginRow ; 5DEC ...and its row out of $A6
|
|
ldy #$00 ; 5DEE Y = 0, the offset drawRadarGlyph stores the colour byte at
|
|
lda savedRegA ; 5DF0 the map cell byte
|
|
jsr getRadarCellGlyph ; 5DF2 overlay routine $74C3: cell byte -> glyph index in A (bit 7 = noise) and colour in tileColour (also mapGenerator6F00:contourFillWithInterior)
|
|
jsr drawRadarGlyph ; 5DF5 overlay routine $7396: plot the 8x8 glyph at (bitmapPtr) and its colour at (screenPtr),Y (also mapGenerator6F00:jitterForestSeedColumn)
|
|
lda drawCellSaveCol ; 5DF8 the real left edge of the radar window
|
|
sta viewOriginCol ; 5DFB put it back
|
|
lda drawCellSaveRow ; 5DFD the real top edge of the radar window
|
|
sta viewOriginRow ; 5E00 put it back
|
|
rts ; 5E02 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearOverviewWindow - Points the text window at the 20x20 overview panel (left column 1, top row 1,
|
|
; width 20, bottom row limit 21) and blanks it with the text engine's window clear.
|
|
; In: -
|
|
; Out: zp_39-zp_3C = 1/21/1/20, the overview area cleared
|
|
; Called from: mainStart ($0A98), clearCurrentScreenWindows ($2DF1) and overlay A ($76FC, $8235)
|
|
; ----------------------------------------------------------------------
|
|
clearOverviewWindow:
|
|
lda #$01 ; 5E03 text window left edge = screen character column 1
|
|
sta textWindowLeft ; 5E05 textWindowLeft = 1
|
|
sta textWindowTop ; 5E07 and textWindowTop = 1: the overview frame starts at character (1,1)
|
|
lda #$14 ; 5E09 20 characters wide
|
|
sta textWindowWidth ; 5E0B textWindowWidth = 20 (the 40x40 map is drawn 4x4 pixels per cell inside this frame)
|
|
lda #$15 ; 5E0D row limit 21 (exclusive), i.e. rows 1-20
|
|
sta textWindowBottom ; 5E0F textWindowBottom = 21
|
|
jmp bootDriveDataBit ; 5E11 tail-call the text engine's window clear ($C06A) and return to the caller (also textEngineC000:clearTextWindow)
|
|
|
|
|
|
; uiLevelTextColourTable: byteTable, 3 bytes. Info-panel text colour per uiLevel: $09 (browsing), $05
|
|
; (unit selected), $04 (group). Read at $22AE (showUnitInfoPanel) and $64E7.
|
|
uiLevelTextColourTable:
|
|
.byte $09,$05,$04 ; 5E14 ... 3 bytes indexed by uiLevel ($9209): 0 browsing = colour 9, 1 unit selected = colour 5, 2 group = colour 4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleOwnUnitClick - Entry taken by the battlefield main loop when the player clicks on one of his
|
|
; own, unstunned units (index already in selectedUnit $90F8). A unit that is a member of a group is
|
|
; handed to runGroupOptionsMenu; a lone unit falls through into runUnitOptionsMenu.
|
|
; In: selectedUnit $90F8, unitDisplayFlagsTable $FB54 bit 6
|
|
; Out: control passes to one of the two option-menu routines, both of which end in exitUnitMenu
|
|
; Called from: the battlefield fire-button handler ($20CC)
|
|
; ----------------------------------------------------------------------
|
|
handleOwnUnitClick:
|
|
ldy selectedUnit ; 5E17 index (0-99) of the own, unstunned unit the player just clicked on
|
|
lda unitDisplayFlagsTable,y ; 5E1A its local flag copy $FB54: bit 7 idle, bit 6 in a group, bits 2-5 group key, bits 0-1 facing
|
|
and #$40 ; 5E1D test bit 6 = 'this unit belongs to a group'
|
|
beq runUnitOptionsMenu ; 5E1F lone unit: fall through into the single-unit branch
|
|
jmp runGroupOptionsMenu ; 5E21 group member: the whole group is commanded instead
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runUnitOptionsMenu - Single-unit branch of handleOwnUnitClick, also entered by JMP from the group
|
|
; menu item DIRECT UNIT ($62F6). It highlights the unit and shows its info panel (which waits there
|
|
; until the stick moves or the button is pressed again); a deflected stick then starts SETTING
|
|
; DESTINATION, while a second click builds and runs the UNIT OPTIONS menu - but only when the UNIT
|
|
; MENUS option (gameTypeOptions $0BA3 bit 3) is on. The chosen item queues its command byte with the
|
|
; unit index as the argument.
|
|
; In: selectedUnit $90F8, unit arrays $F76C/$F898/$F8FC/$F960/$FB54, joyDirection zp_5D, gamePhase
|
|
; zp_B1, gameTypeOptions $0BA3 bit 3, comcenUnit $9236
|
|
; Out: highlightedUnit/highlightedGroupKey/uiLevel/menuLoopMode and the menu state set, viewport and
|
|
; info panel redrawn, menuIdleSnapshot+1 patched, one of the commands $82/$84/$86/$93/$95/$9F/$A0
|
|
; queued (or $87 through the target cursor, $88 through the join-group menu); falls into
|
|
; exitUnitMenu
|
|
; Called from: handleOwnUnitClick ($5E1F) and the group menu item DIRECT UNIT ($62F6)
|
|
; ----------------------------------------------------------------------
|
|
runUnitOptionsMenu:
|
|
lda #$00 ; 5E24 0 into the three text-engine mode bytes below
|
|
sta rawGlyphMode ; 5E26 rawGlyphMode off: printChar translates ASCII again
|
|
sta textCentreWidth ; 5E28 textCentreWidth 0 = print left-aligned at the cursor
|
|
sta textFillGlyph ; 5E2B fill/pad with glyph 0 (space)
|
|
lda #$FF ; 5E2D $FF = no group highlighted
|
|
sta highlightedGroupKey ; 5E2F clear the viewport group highlight
|
|
lda selectedUnit ; 5E32 the clicked unit...
|
|
sta highlightedUnit ; 5E35 ...is the one the viewport highlights
|
|
jsr drawViewport ; 5E38 repaint the 7x5-cell tactical view with the new highlight
|
|
lda #$01 ; 5E3B UI level 1
|
|
sta uiLevel ; 5E3D uiLevel 1 = a single unit is selected (info-panel colour 5)
|
|
jsr setCursorShapeSquare ; 5E40 sprite 0 becomes the hollow-square cell cursor
|
|
lda #$05 ; 5E43 colour 5 = the unit-level text colour (uiLevelTextColourTable[1])
|
|
sta textColour ; 5E45 ink colour for the info panel
|
|
sta textPadColour ; 5E47 and the same colour for the blanks that pad centred lines
|
|
jsr clearInfoPanel ; 5E49 clear the info-panel window (columns 25-38, rows 12-21)
|
|
jsr showUnitInfoPanel ; 5E4C draw the unit's status there; that routine loops until the player deflects the stick, clicks again or the display is interrupted
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Work out what ended the info panel: the unit died, the stick was deflected (start a move order) or
|
|
; the fire button was pressed (open the options menu).
|
|
; ----------------------------------------------------------------------
|
|
ldy selectedUnit ; 5E4F re-read the unit: the battle kept running under the panel
|
|
lda unitTypeTable,y ; 5E52 unit type byte $F76C, bit 7 = destroyed
|
|
bmi L_5E65 ; 5E55 the unit was killed while its panel was up: just leave
|
|
lda joyDirection ; 5E57 joystick nibble latched by the IRQ, active low ($0F = centred)
|
|
cmp #$0F ; 5E59 still centred?
|
|
beq L_5E68 ; 5E5B centred: then it was the fire button that ended the panel
|
|
lda #$00 ; 5E5D clear the input lockout...
|
|
sta inputLockoutTimer ; 5E5F ...so the destination cursor reacts to the stick immediately
|
|
jsr promptForDestination ; 5E62 stick deflected: run SETTING / DESTINATION for this unit (ends in command $80)
|
|
L_5E65:
|
|
jmp exitUnitMenu ; 5E65 back to the battlefield loop with uiLevel 0
|
|
|
|
L_5E68:
|
|
jsr checkFirePressed ; 5E68 was there a fresh fire-button press? (Z = 1 when yes)
|
|
bne L_5E65 ; 5E6B no press - the panel simply timed out: leave
|
|
lda gameTypeOptions ; 5E6D game type and option bits $0BA3
|
|
and #$08 ; 5E70 bit 3 = the UNIT MENUS option (set by the FULL WAR/$1D and DEFENDER/$1E presets, or by hand in a custom game)
|
|
beq L_5E65 ; 5E72 this scenario has no unit option menus: leave
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Second click with the UNIT MENUS option on: re-establish the highlight and the cursor sprite
|
|
; (showUnitInfoPanel used sprites 1-4 for the unit picture) and set up the menu state.
|
|
; ----------------------------------------------------------------------
|
|
lda #$FF ; 5E74 $FF = no group highlight
|
|
sta highlightedGroupKey ; 5E76 clear the group highlight again
|
|
lda selectedUnit ; 5E79 the clicked unit...
|
|
sta highlightedUnit ; 5E7C ...stays highlighted
|
|
jsr drawViewport ; 5E7F repaint the view: showUnitInfoPanel moved the picture sprites and the path marker
|
|
lda #$01 ; 5E82 UI level 1
|
|
sta uiLevel ; 5E84 uiLevel = 1 (unit)
|
|
jsr setCursorShapeSquare ; 5E87 restore the square cursor sprite (the info panel may have used sprites 1-4)
|
|
lda #$FF ; 5E8A $FF
|
|
sta menuLoopMode ; 5E8C menuLoopMode $FF: the menu loop animates this unit's path marker (no group preview)
|
|
lda #$01 ; 5E8F 1
|
|
sta uiLevel ; 5E91 redundant - uiLevel was already set to 1 at $5E84
|
|
lda #$00 ; 5E94 0
|
|
sta menuSelectedItem ; 5E96 start the selection on the first menu row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Build the UNIT OPTIONS list. Re-entered from $5F22 when the menu loop reports that the unit's idle
|
|
; flag changed, because that swaps the DIG items for the BLITZ items. Every item is appended with
|
|
; addMenuItem, which also prints its name on the next row.
|
|
; ----------------------------------------------------------------------
|
|
buildUnitOptionsMenu:
|
|
lda #$05 ; 5E99 unit-level colour 5
|
|
sta textColour ; 5E9B ink colour
|
|
sta textPadColour ; 5E9D pad colour
|
|
jsr clearInfoPanel ; 5E9F clear the info panel before printing the menu
|
|
lda #$0A ; 5EA2 $CB0A = ' UNIT' + CR
|
|
ldy #$CB ; 5EA4 high byte of the heading string
|
|
jsr printString ; 5EA6 print the first heading line
|
|
jsr printStringAtPointer ; 5EA9 print the string that follows it, $CB13 ' OPTIONS' + CR + CR
|
|
ldy selectedUnit ; 5EAC unit index for the flag look-ups below
|
|
ldx #$00 ; 5EAF 0
|
|
stx menuItemCount ; 5EB1 start with an empty item list
|
|
ldx #$02 ; 5EB4 item id 2 = CLOAKING ON
|
|
lda unitPathColTable,y ; 5EB6 $F898 (waypoint column), bit 6 = 'cloaking ordered'
|
|
and #$40 ; 5EB9 test the cloak-order bit
|
|
beq L_5EBF ; 5EBB not cloaked: offer CLOAKING ON
|
|
ldx #$03 ; 5EBD already cloaked: item id 3 = NO CLOAKING
|
|
L_5EBF:
|
|
jsr addMenuItem ; 5EBF append the item and print its name
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The second item depends on what the unit is doing: a unit that stands still can DIG IN or DIG OUT, a
|
|
; unit that is moving can be given or denied a BLITZ order. The same flag byte is snapshotted into the
|
|
; menu loop so that the list is rebuilt when that state changes.
|
|
; ----------------------------------------------------------------------
|
|
ldy selectedUnit ; 5EC2 unit index
|
|
lda unitDisplayFlagsTable,y ; 5EC5 $FB54 flags, bit 7 = the unit is standing still
|
|
ora #$01 ; 5EC8 force the byte nonzero: 0 is the 'do not watch the unit' sentinel of the menu loop
|
|
sta menuIdleSnapshot+1 ; 5ECA self-modify the LDA #imm at menuIdleSnapshot ($602A): the loop returns $80 when this bit 7 changes
|
|
bpl L_5EDB ; 5ECD bit 7 clear = the unit is moving: offer the blitz items
|
|
ldx #$11 ; 5ECF item id 17 = DIG OUT
|
|
lda unitStunTable,y ; 5ED1 $F960, bits 5-7 = dug-in / digging state
|
|
and #$E0 ; 5ED4 isolate the dig bits
|
|
bne L_5EE8 ; 5ED6 already dug in or digging: DIG OUT
|
|
inx ; 5ED8 otherwise item id 18 = DIG IN
|
|
bne L_5EE8 ; 5ED9 always taken (X = $12)
|
|
L_5EDB:
|
|
ldx #$0A ; 5EDB item id 10 = SET BLITZ
|
|
ldy selectedUnit ; 5EDD unit index
|
|
lda unitPathRowTable,y ; 5EE0 $F8FC (waypoint row), bit 6 = 'blitz ordered'
|
|
and #$40 ; 5EE3 test the blitz-order bit
|
|
beq L_5EE8 ; 5EE5 blitz off: offer SET BLITZ
|
|
inx ; 5EE7 blitz on: item id 11 = BLITZ OFF
|
|
L_5EE8:
|
|
jsr addMenuItem ; 5EE8 append the dig/blitz item
|
|
|
|
; ----------------------------------------------------------------------
|
|
; JOIN GROUP is offered to every unit except the command centre; a BOOMER additionally gets SET TARGET
|
|
; or CLEAR TARGET while a battle is in progress.
|
|
; ----------------------------------------------------------------------
|
|
ldy selectedUnit ; 5EEB unit index
|
|
cpy comcenUnit ; 5EEE is this the player's own command centre (unit 49 or 99)?
|
|
beq L_5F0E ; 5EF1 the comcen cannot join a group and has no weapon target
|
|
ldx #$07 ; 5EF3 item id 7 = JOIN GROUP
|
|
jsr addMenuItem ; 5EF5 append it
|
|
lda gamePhase ; 5EF8 gamePhase zp_B1 is 0 only while the battle runs
|
|
bne L_5F0E ; 5EFA setup or film phase: no target item
|
|
ldx selectedUnit ; 5EFC unit index
|
|
jsr getBoomerTarget ; 5EFF C = 1 and A = the boomer's target byte if unit X is a BOOMER (type 2)
|
|
bcc L_5F0E ; 5F02 not a boomer: no target item
|
|
ldx #$04 ; 5F04 item id 4 = CLEAR TARGET
|
|
tay ; 5F06 test the target byte's bit 7
|
|
bmi L_5F0B ; 5F07 bit 7 set = a target is locked: offer CLEAR TARGET
|
|
ldx #$05 ; 5F09 no target yet: item id 5 = SET TARGET
|
|
L_5F0B:
|
|
jsr addMenuItem ; 5F0B append the target item
|
|
L_5F0E:
|
|
ldx #$0C ; 5F0E item id 12 = (EXIT), always the last row
|
|
jsr addMenuItem ; 5F10 append it
|
|
lda #$0F ; 5F13 screen row 15
|
|
sta menuFirstRow ; 5F15 the item list starts on row 15 of the info panel
|
|
jsr resetPathMarker ; 5F18 put the mini-map path marker back on the unit
|
|
jsr runMenuLoopWatchUnit ; 5F1B run the selector: A = chosen item id, Y = chosen row (or $80)
|
|
cpy #$80 ; 5F1E $80 = the unit's idle flag changed while the menu was open
|
|
bne L_5F25 ; 5F20 no: act on the chosen item
|
|
jmp buildUnitOptionsMenu ; 5F22 yes: DIG vs BLITZ is wrong now, rebuild the whole menu
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Dispatch on the chosen item id. Every branch either queues a command byte through the common tail at
|
|
; $5F9E (argument = the unit index) or runs a sub-dialogue of its own.
|
|
; ----------------------------------------------------------------------
|
|
L_5F25:
|
|
sta menuSelectedItem ; 5F25 keep the chosen item id (from here on $91D5 holds an id, not a row)
|
|
ldy selectedUnit ; 5F28 leftover load - every path below reloads X or Y (dead)
|
|
lda menuSelectedItem ; 5F2B the chosen item id
|
|
cmp #$02 ; 5F2E CLOAKING ON?
|
|
bne L_5F36 ; 5F30 no
|
|
ldy #$82 ; 5F32 command $82 = cloak on (2 bytes: unit)
|
|
bne L_5F3C ; 5F34 always taken
|
|
L_5F36:
|
|
cmp #$03 ; 5F36 NO CLOAKING?
|
|
bne L_5F4A ; 5F38 no
|
|
ldy #$84 ; 5F3A command $84 = cloak off (2 bytes: unit)
|
|
L_5F3C:
|
|
ldx selectedUnit ; 5F3C unit index
|
|
lda unitPathColTable,x ; 5F3F flip the local copy of the cloak bit at once...
|
|
eor #$40 ; 5F42 ...bit 6 of $F898...
|
|
sta unitPathColTable,x ; 5F44 ...so the display shows the new state before the command is executed
|
|
tya ; 5F47 command byte back into A
|
|
bne L_5F9E ; 5F48 always taken: queue it
|
|
L_5F4A:
|
|
cmp #$04 ; 5F4A CLEAR TARGET?
|
|
bne L_5F52 ; 5F4C no
|
|
lda #$86 ; 5F4E command $86 = clear the boomer's target (2 bytes: unit)
|
|
bne L_5F9E ; 5F50 always taken
|
|
L_5F52:
|
|
cmp #$05 ; 5F52 SET TARGET?
|
|
bne L_5F6A ; 5F54 no
|
|
jsr printSettingHeaderForLevel; 5F56 print 'SETTING' centred in the uiLevel colour
|
|
lda #$61 ; 5F59 $CB61 = 'TARGET'
|
|
ldy #$CB ; 5F5B high byte
|
|
jsr printStringCentred ; 5F5D print it centred under SETTING
|
|
lda #$05 ; 5F60 cursor mode 5
|
|
sta cursorTargetMode ; 5F62 zp_BB > 0 = range-limited attack cursor
|
|
jsr runTargetCursorLoop ; 5F64 let the player pick the target; the cursor loop queues command $87 itself
|
|
jmp exitUnitMenu ; 5F67 done
|
|
|
|
L_5F6A:
|
|
cmp #$07 ; 5F6A JOIN GROUP?
|
|
bne L_5F74 ; 5F6C no
|
|
jsr runJoinGroupMenu ; 5F6E run the group picker; it queues command $88 itself
|
|
jmp exitUnitMenu ; 5F71 done
|
|
|
|
L_5F74:
|
|
cmp #$0A ; 5F74 SET BLITZ?
|
|
bne L_5F7C ; 5F76 no
|
|
ldy #$93 ; 5F78 command $93 = blitz on (2 bytes: unit)
|
|
bne L_5F82 ; 5F7A always taken
|
|
L_5F7C:
|
|
cmp #$0B ; 5F7C BLITZ OFF?
|
|
bne L_5F90 ; 5F7E no
|
|
ldy #$95 ; 5F80 command $95 = blitz off (2 bytes: unit)
|
|
L_5F82:
|
|
ldx selectedUnit ; 5F82 unit index
|
|
lda unitPathRowTable,x ; 5F85 flip the local blitz bit immediately...
|
|
eor #$40 ; 5F88 ...bit 6 of $F8FC...
|
|
sta unitPathRowTable,x ; 5F8A ...for instant menu feedback
|
|
tya ; 5F8D command byte back into A
|
|
bne L_5F9E ; 5F8E always taken
|
|
L_5F90:
|
|
cmp #$11 ; 5F90 DIG OUT?
|
|
bne L_5F98 ; 5F92 no
|
|
lda #$A0 ; 5F94 command $A0 = dig out (2 bytes: unit)
|
|
bne L_5F9E ; 5F96 always taken
|
|
L_5F98:
|
|
cmp #$12 ; 5F98 DIG IN?
|
|
bne exitUnitMenu ; 5F9A anything else (item 12 = (EXIT)) just closes the menu
|
|
lda #$9F ; 5F9C command $9F = dig in (2 bytes: unit)
|
|
L_5F9E:
|
|
ldy selectedUnit ; 5F9E the unit index...
|
|
sty cmdParam0 ; 5FA1 ...is the single argument of all of these commands
|
|
jsr queueCmd2 ; 5FA3 queue 'command byte, unit' in the outgoing ring, then fall into exitUnitMenu
|
|
|
|
; ----------------------------------------------------------------------
|
|
; exitUnitMenu - Shared tail of every unit/group menu path: drops back to UI level 0 and releases the
|
|
; info panel, then returns to the battlefield main loop.
|
|
; In: -
|
|
; Out: uiLevel $9209 = 0, infoPanelUnit $91D7 = $FF
|
|
; Called from: $5E65, $5F67, $5F71, $5F9A and the fall-through at $5FA3, $6339 and $64D7
|
|
; ----------------------------------------------------------------------
|
|
exitUnitMenu:
|
|
lda #$00 ; 5FA6 0
|
|
sta uiLevel ; 5FA8 uiLevel 0 = back to browsing the map
|
|
lda #$FF ; 5FAB $FF
|
|
sta infoPanelUnit ; 5FAD no unit owns the info panel any more
|
|
rts ; 5FB0 return to the battlefield main loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; addMenuItem - Appends item id X to menuItemIdList[menuItemCount++] and prints the item's name on the
|
|
; current text line. Ids 0-11 come from the strings at $C9D2, ids 12-18 from those at $CAB2; both are
|
|
; printed with printTableEntry, which strides 8 bytes, so the first group is indexed with 2*id (its
|
|
; strings are padded with leading blanks onto a 16-byte grid). Every string ends with a CR, so the
|
|
; cursor drops to the next row by itself. Ids 1 (MOVE TO GOAL) and 9 (SELF DESTRCT) exist in the
|
|
; string table but are never passed to this routine in this build.
|
|
; In: X = item id (0 FORMATION, 1 MOVE TO GOAL, 2 CLOAKING ON, 3 NO CLOAKING, 4 CLEAR TARGET,
|
|
; 5 SET TARGET, 6 MEMBERSHIP, 7 JOIN GROUP, 8 DIRECT UNIT, 9 SELF DESTRCT, 10 SET BLITZ,
|
|
; 11 BLITZ OFF, 12 (EXIT), 13 ENERGY, 14 BATTLE, 15 RADAR, 16 DRONE, 17 DIG OUT, 18 DIG IN);
|
|
; menuItemCount $91D4 and the text cursor
|
|
; Out: menuItemIdList[n] = id, menuItemCount incremented, one line printed; A/X/Y clobbered
|
|
; Called from: runUnitOptionsMenu ($5EBF, $5EE8, $5EF5, $5F0B, $5F10), runGroupOptionsMenu ($6268,
|
|
; $628E, $6293, $6298, $629D, $62A2) and runRepairWhichMenu ($6568, $6575)
|
|
; ----------------------------------------------------------------------
|
|
addMenuItem:
|
|
txa ; 5FB1 item id into A
|
|
ldy menuItemCount ; 5FB2 next free slot of the item list
|
|
sta menuItemIdList,y ; 5FB5 remember which id sits on which row
|
|
inc menuItemCount ; 5FB8 one more item
|
|
sec ; 5FBB prepare an exact subtraction
|
|
sbc #$0C ; 5FBC ids 12-18 come from the second string table
|
|
bcc L_5FC7 ; 5FBE ids 0-11: the 16-byte-strided UNIT/GROUP OPTIONS strings
|
|
tax ; 5FC0 X = id - 12
|
|
lda #$B2 ; 5FC1 $CAB2 = ' (EXIT)', base of the 8-byte console strings
|
|
ldy #$CA ; 5FC3 high byte
|
|
bne L_5FCE ; 5FC5 always taken
|
|
L_5FC7:
|
|
txa ; 5FC7 id 0-11 back into A
|
|
asl a ; 5FC8 double it: printTableEntry strides 8 bytes but these strings sit on a 16-byte grid
|
|
tax ; 5FC9 index for printTableEntry
|
|
lda #$D2 ; 5FCA $C9D2 = 'FORMATION', base of the option strings
|
|
ldy #$C9 ; 5FCC high byte
|
|
L_5FCE:
|
|
jmp printTableEntry ; 5FCE print the entry at base + 8*X; each string ends with CR so the cursor drops to the next row
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMenuLoopWithFlag - Alternative entry for the console and setup menus: with C = 1 it behaves
|
|
; exactly like runMenuLoop; with C = 0 it puts $FF into the slow-down row operand menuSlowRowTest+1 so
|
|
; there is no extra pause before the last item, and leaves the unit-idle watch operand untouched.
|
|
; In: C flag, menu state menuItemCount/menuSelectedItem/menuFirstRow
|
|
; Out: as runMenuLoopWatchUnit
|
|
; Called from: runMainMenu ($1C59) and overlay A ($7F40, $835D, $83CE)
|
|
; ----------------------------------------------------------------------
|
|
runMenuLoopWithFlag:
|
|
bcs runMenuLoop ; 5FD1 C = 1: behave exactly like runMenuLoop
|
|
ldx #$FF ; 5FD3 C = 0: $FF disables the extra pause before the last row
|
|
bmi L_5FE1 ; 5FD5 always taken; this path skips the store at $5FD9, so menuIdleSnapshot+1 keeps whatever the previous menu left there (harmless: these callers use the pseudo-units $F0/$FE/$FF, for which the watch is skipped anyway)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMenuLoop - Menu selector without the unit-idle watch: patches menuIdleSnapshot+1 ($602A) to 0 and
|
|
; falls into runMenuLoopWatchUnit, which then sets the slow-down row to itemCount-2.
|
|
; In: menu state menuItemCount/menuSelectedItem/menuFirstRow, selectedUnit $90F8, menuLoopMode $612E
|
|
; Out: A = the selected item id, Y = the selected row
|
|
; Called from: runJoinGroupMenu ($61BC), runRepairWhichMenu ($6581), runInGameOptionsMenu ($65A9),
|
|
; runFormationOptionsMenu ($661D) and runMenuLoopWithFlag ($5FD1)
|
|
; ----------------------------------------------------------------------
|
|
runMenuLoop:
|
|
lda #$00 ; 5FD7 0
|
|
sta menuIdleSnapshot+1 ; 5FD9 patch menuIdleSnapshot+1 ($602A): 0 turns the unit-idle watch off
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMenuLoopWatchUnit - The generic vertical menu selector used by every in-game option menu. It arms
|
|
; a 20-frame input lockout, highlights the current row, and then loops once per frame: it stirs the
|
|
; RNG, runs the update that suits the phase (updateGameFrame during a battle, the '*' / F1 hot keys
|
|
; plus serviceCommandExchange in the setup and film phases), keeps the info panel on the selected
|
|
; unit, blinks the selected row every 20 frames, updates the map feedback chosen by menuLoopMode (path
|
|
; marker, or a live preview of the group the current row stands for) and reads the joystick: up/down
|
|
; step through the rows without wrap-around, fire confirms. It returns early with A = Y = $80 when the
|
|
; watched unit's idle flag ($FB54 bit 7) no longer matches the snapshot in menuIdleSnapshot+1, and it
|
|
; forces the last item ((EXIT)) when the unit dies, the game-over countdown starts or checkUiContinue
|
|
; asks for an abort.
|
|
; In: menuItemCount $91D4, menuSelectedItem $91D5, menuFirstRow $91D6, menuLoopMode $612E,
|
|
; selectedUnit $90F8, joyDirection zp_5D, joyRepeatTimer zp_6C, menuBlinkTimer zp_6E, gamePhase
|
|
; zp_B1, gameEndCountdown $92C9, gameEndReason $0BA8 and the patched operands
|
|
; menuIdleSnapshot+1 / menuSlowRowTest+1
|
|
; Out: A = menuItemIdList[menuSelectedItem], Y = menuSelectedItem (or A = Y = $80 meaning 'rebuild the
|
|
; menu'), menuSelectedItem updated, selectedGroupKey/highlightedGroupKey updated in group-preview
|
|
; mode, menuBlinkState, inputLockoutTimer $0B7D, joyRepeatTimer, menuMoveDirection+1 patched; the
|
|
; selected row is left highlighted
|
|
; Called from: runUnitOptionsMenu ($5F1B), runGroupOptionsMenu ($62AA) and through runMenuLoop
|
|
; ----------------------------------------------------------------------
|
|
runMenuLoopWatchUnit:
|
|
ldx menuItemCount ; 5FDC number of rows
|
|
dex ; 5FDF ...
|
|
dex ; 5FE0 count - 2 = the row just before the last one
|
|
L_5FE1:
|
|
stx menuSlowRowTest+1 ; 5FE1 patch the CMP #imm at menuSlowRowTest ($60BA): landing on this row adds a pause
|
|
lda #$14 ; 5FE4 20 frames
|
|
sta inputLockoutTimer ; 5FE6 ignore held-over input for 20 frames...
|
|
sta joyRepeatTimer ; 5FE9 ...and do not step the selection for 20 frames
|
|
lda #$01 ; 5FEB 1
|
|
sta menuBlinkState ; 5FED menuBlinkState 1 = the selected row is currently shown normally
|
|
L_5FF0:
|
|
jsr toggleMenuRowHighlight ; 5FF0 highlight the selected row and restart the 20-frame blink timer
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass per frame while the menu is open: keep the world running, watch the unit, update the map
|
|
; feedback, blink the selected row and read the joystick.
|
|
; ----------------------------------------------------------------------
|
|
menuLoopFrame:
|
|
jsr nextGameRandom ; 5FF3 stir the 24-bit game RNG once per pass
|
|
lda gamePhase ; 5FF6 gamePhase zp_B1
|
|
cmp #$FF ; 5FF8 $FF = the pre-battle setup phase (overlay A resident)
|
|
beq L_6002 ; 5FFA yes: poll overlay A's hot keys
|
|
and #$C0 ; 5FFC bits 6/7 flag the non-battle phases
|
|
beq L_6013 ; 5FFE both clear: a battle is running - do the full per-frame update
|
|
bne L_600D ; 6000 otherwise only service the opponent link
|
|
L_6002:
|
|
jsr handleStarKeyToggle ; 6002 setup phase: overlay A's '*' hot key (DESTROY-mode toggle)
|
|
lda isSoloTrainer ; 6005 $0BA5 bit 7 = solo trainer game
|
|
bpl L_600D ; 6008 modem game: no F1 handler
|
|
jsr handleDisplayToggleKey ; 600A trainer only: F1 switches the BEGINNER/STANDARD display (also trainerSetupEC00:storeGroupCentreRow)
|
|
L_600D:
|
|
jsr serviceCommandExchange ; 600D run one step of the lock-step command exchange
|
|
jmp L_6068 ; 6010 skip the battle-only bookkeeping
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Battle phase: run one game frame, then check that the watched unit is still alive and still in the
|
|
; same movement state.
|
|
; ----------------------------------------------------------------------
|
|
L_6013:
|
|
lda selectedUnit ; 6013 the selected unit...
|
|
sta infoPanelUnit ; 6016 ...keeps owning the info panel while the menu is open
|
|
jsr updateGameFrame ; 6019 one frame of the battle simulation and its display
|
|
bit gamePhase ; 601C gamePhase bit 6 = paused / waiting for the opponent
|
|
bvs L_6068 ; 601E paused: do not touch the unit or the map highlight
|
|
ldx selectedUnit ; 6020 selected unit
|
|
bmi L_6068 ; 6023 $F0/$FE/$FF are console pseudo-units, not real units
|
|
lda unitTypeTable,x ; 6025 type byte, bit 7 = destroyed
|
|
bmi menuLoopExitLast ; 6028 the unit died under the menu: close it on the last item
|
|
menuIdleSnapshot:
|
|
lda #$FF ; 602A operand patched at $5ECA/$6273 with the unit's $FB54 flags (or with 0 by runMenuLoop = watch disabled)
|
|
beq L_6037 ; 602C 0: the caller does not want the unit watched
|
|
eor unitDisplayFlagsTable,x ; 602E compare the snapshot with the live flags
|
|
and #$80 ; 6031 only the 'standing still' bit matters
|
|
beq L_6037 ; 6033 unchanged: carry on
|
|
tay ; 6035 A = Y = $80 tells the caller to rebuild the menu
|
|
rts ; 6036 early return with the rebuild code
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Map feedback while the menu is open: either animate the selected unit's path marker or, in the
|
|
; join-group picker, highlight the group that the current row stands for.
|
|
; ----------------------------------------------------------------------
|
|
L_6037:
|
|
lda menuLoopMode ; 6037 $FF unit menu, $01 group menu, $00/$20 join-group picker (side bit)
|
|
bmi L_6065 ; 603A unit menu: just animate the path marker
|
|
lda menuLoopMode ; 603C re-read the mode
|
|
and #$01 ; 603F odd = group menu
|
|
bne L_6065 ; 6041 group menu: also just animate the path marker
|
|
lda menuSelectedItem ; 6043 join-group picker: the selected row is the group number
|
|
cmp #$08 ; 6046 never true - the list has 8 rows (0-7); probably an off-by-one, the (EXIT) row looks like it was meant to clear the highlight
|
|
bne L_604F ; 6048 always taken
|
|
lda #$FF ; 604A would mean 'no group highlighted'
|
|
jmp L_6057 ; 604C unreachable in this build
|
|
|
|
L_604F:
|
|
asl a ; 604F group number...
|
|
asl a ; 6050 ...times 4 = the group-id field of $FB54
|
|
ora menuLoopMode ; 6051 add the player's side bit ($20 for units 50-99)
|
|
sta selectedGroupKey ; 6054 the group the highlight is previewing
|
|
L_6057:
|
|
cmp highlightedGroupKey ; 6057 is the viewport already showing that group?
|
|
beq L_6068 ; 605A yes: nothing to redraw
|
|
sta highlightedGroupKey ; 605C remember the highlighted group
|
|
jsr drawViewport ; 605F repaint the viewport with the new group highlighted
|
|
jmp L_6068 ; 6062 on to the blink handling
|
|
|
|
L_6065:
|
|
jsr animatePathMarkerToDestination; 6065 walk the mini-map path marker one step along the unit's route
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Blink the selected row, then decide whether the menu has to close (game over, or an abort request
|
|
; from checkUiContinue).
|
|
; ----------------------------------------------------------------------
|
|
L_6068:
|
|
lda menuBlinkTimer ; 6068 zp_6E, counted down by the IRQ
|
|
bne L_606F ; 606A not expired yet
|
|
jsr toggleMenuRowHighlight ; 606C every 20 frames: flip the highlight of the selected row
|
|
L_606F:
|
|
lda gameEndCountdown ; 606F $92C9 counts down while the game-over sequence runs
|
|
bne menuLoopExitLast ; 6072 game over: close the menu on the last item
|
|
lda gameEndReason ; 6074 $0BA8 is nonzero once the game has ended
|
|
bne L_6088 ; 6077 after the game the abort test is skipped so the console menus stay usable
|
|
jsr checkUiContinue ; 6079 returns 0 when SPACE, a screen change or a pause wants this menu closed
|
|
bne L_6088 ; 607C keep going
|
|
menuLoopExitLast:
|
|
ldy menuItemCount ; 607E number of items
|
|
dey ; 6081 index of the last one
|
|
sty menuSelectedItem ; 6082 force the selection onto the last item, normally (EXIT)
|
|
jmp menuLoopReturnSelection ; 6085 return that selection
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Joystick handling: fire confirms the selection; up/down step through the rows without wrap-around,
|
|
; with the usual 16/6-frame auto-repeat plus an extra pause before the last row.
|
|
; ----------------------------------------------------------------------
|
|
L_6088:
|
|
lda joyDirection ; 6088 joystick nibble, active low
|
|
cmp #$0F ; 608A centred?
|
|
bne L_6093 ; 608C deflected: try to move the selection
|
|
jsr checkFirePressed ; 608E Z = 1 on a fresh fire-button press
|
|
beq menuLoopReturnSelection ; 6091 fire: confirm the current selection
|
|
L_6093:
|
|
ldy joyRepeatTimer ; 6093 auto-repeat delay still running?
|
|
bne L_60B7 ; 6095 yes: nothing to do this frame
|
|
lda joyDirection ; 6097 joystick nibble
|
|
ora #$0C ; 6099 force the left/right bits high: only up and down move the selection
|
|
ldy #$FF ; 609B -1 = one row up
|
|
cmp #$0E ; 609D $0E = bit 0 low = up
|
|
beq L_60A7 ; 609F up
|
|
ldy #$01 ; 60A1 +1 = one row down
|
|
cmp #$0D ; 60A3 $0D = bit 1 low = down
|
|
bne L_60B7 ; 60A5 left/right only: ignore
|
|
L_60A7:
|
|
tya ; 60A7 the step (-1 or +1)
|
|
sty menuMoveDirection+1 ; 60A8 patch the LDY #imm at menuMoveDirection ($60BE) with the direction
|
|
clc ; 60AB clear carry for the add
|
|
adc menuSelectedItem ; 60AC candidate row = current + step
|
|
jsr setJoystickRepeatDelay ; 60AF 16 frames before the first repeat, 6 between repeats (A is preserved)
|
|
cmp menuItemCount ; 60B2 past the last row (or $FF above the first)?
|
|
bcc menuSlowRowTest ; 60B5 in range
|
|
L_60B7:
|
|
jmp menuLoopFrame ; 60B7 out of range - the selection does not wrap: just keep looping
|
|
|
|
menuSlowRowTest:
|
|
cmp #$FF ; 60BA operand = itemCount-2, or $FF when runMenuLoopWithFlag turned the pause off
|
|
bne L_60C6 ; 60BC not the slow-down row
|
|
menuMoveDirection:
|
|
ldy #$FF ; 60BE operand = the move direction stored at $60A8
|
|
bmi L_60C6 ; 60C0 moving up: no extra delay
|
|
ldy #$1E ; 60C2 30 frames
|
|
sty joyRepeatTimer ; 60C4 moving down onto the second-to-last row: pause so the stick cannot run straight into (EXIT)
|
|
L_60C6:
|
|
pha ; 60C6 keep the new row
|
|
jsr unhighlightMenuRow ; 60C7 restore the colours of the row being left
|
|
pla ; 60CA new row back
|
|
sta menuSelectedItem ; 60CB it is the selection now
|
|
lda #$02 ; 60CE sound 2 = menu-move click
|
|
jsr playSound ; 60D0 play it
|
|
jmp L_5FF0 ; 60D3 highlight the new row and loop
|
|
|
|
menuLoopReturnSelection:
|
|
lda menuBlinkState ; 60D6 0 = the row is already highlighted
|
|
beq L_60DE ; 60D9 nothing to do
|
|
jsr toggleMenuRowHighlight ; 60DB leave the chosen row highlighted
|
|
L_60DE:
|
|
ldy menuSelectedItem ; 60DE the chosen row
|
|
lda menuItemIdList,y ; 60E1 A = its item id; Y still holds the row index
|
|
rts ; 60E4 return to the menu's dispatcher
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; unhighlightMenuRow - Removes the highlight from the selected menu row, but only if it is currently
|
|
; applied (menuBlinkState = 0); otherwise it returns at once. Falls into toggleMenuRowHighlight.
|
|
; In: menuBlinkState $92C5, menuSelectedItem, menuFirstRow, textWindowWidth zp_3C, textColour zp_3D
|
|
; Out: the row's colour bytes restored, menuBlinkState = 1
|
|
; Called from: the menu loop ($60C7), runRepairWhichMenu ($6593), runInGameOptionsMenu ($65DD, $65E3)
|
|
; and overlay A ($845C)
|
|
; ----------------------------------------------------------------------
|
|
unhighlightMenuRow:
|
|
lda menuBlinkState ; 60E5 0 = the row is currently EORed (highlighted)
|
|
bne L_612D ; 60E8 already normal: nothing to undo
|
|
|
|
; ----------------------------------------------------------------------
|
|
; toggleMenuRowHighlight - Flips the highlight of menu row menuFirstRow + menuSelectedItem by EORing
|
|
; the colour byte of every cell of that row with textColour, flips menuBlinkState and restarts the
|
|
; 20-frame blink timer. Special case for the REPAIR WHICH menu (selectedUnit = $FF, row < 4): if the
|
|
; comcen is stunned it throws its return address away and jumps into the menu loop's exit path so the
|
|
; menu closes on (EXIT); otherwise it also blinks that comcen system's marker on the schematic.
|
|
; In: menuSelectedItem, menuFirstRow, textWindowWidth zp_3C, textColour zp_3D, selectedUnit $90F8,
|
|
; comcenStunStatus $922A, menuBlinkState $92C5
|
|
; Out: menuBlinkTimer zp_6E = 20, the row's colour bytes EORed, menuBlinkState flipped, screenPtr
|
|
; zp_B6/zp_B7 and the text cursor moved
|
|
; Called from: the menu loop ($5FF0, $606C, $60DB), unhighlightMenuRow and overlay A ($8066, $847B)
|
|
; ----------------------------------------------------------------------
|
|
toggleMenuRowHighlight:
|
|
lda #$14 ; 60EA 20 frames
|
|
sta menuBlinkTimer ; 60EC restart the blink countdown
|
|
lda selectedUnit ; 60EE selected unit, or a console pseudo-unit
|
|
bpl L_610B ; 60F1 a real unit: no schematic marker to blink
|
|
cmp #$FF ; 60F3 $FF marks the REPAIR WHICH menu ($F0 = main menu, $FE = in-game options)
|
|
bne L_610B ; 60F5 some other menu
|
|
ldy menuSelectedItem ; 60F7 selected row
|
|
cpy #$04 ; 60FA rows 0-3 are the four comcen systems
|
|
bcs L_610B ; 60FC the (EXIT) row has no schematic marker
|
|
lda comcenStunStatus ; 60FE $922A bit 7 = the comcen is knocked out
|
|
bpl L_6108 ; 6101 comcen alive: blink its system marker as well
|
|
pla ; 6103 comcen stunned: discard the return address...
|
|
pla ; 6104 ...of the menu-loop caller...
|
|
jmp menuLoopExitLast ; 6105 ...and force the loop to close on the last item (only valid when called from runMenuLoopWatchUnit)
|
|
|
|
L_6108:
|
|
jsr toggleSystemMarker ; 6108 blink the marker of the selected comcen system on the schematic
|
|
|
|
; ----------------------------------------------------------------------
|
|
; EOR the colour byte of every cell of the selected row. In multicolour bitmap mode the text engine's
|
|
; screenPtr points into colour RAM $D800, which supplies the colour of the glyph pixels, so this makes
|
|
; the row alternate between its text colour and black.
|
|
; ----------------------------------------------------------------------
|
|
L_610B:
|
|
lda menuFirstRow ; 610B first row of the item list
|
|
clc ; 610E clear carry
|
|
adc menuSelectedItem ; 610F + selection = screen row of the selected item
|
|
ldy #$00 ; 6112 0
|
|
sty textCursorCol ; 6114 start at the left edge of the text window
|
|
jsr setCursorRow ; 6116 set the row and recompute screenPtr (which points into colour RAM $D800 in multicolour mode); X and Y are preserved
|
|
ldy textWindowWidth ; 6119 window width in characters
|
|
dey ; 611B index of the last cell
|
|
L_611C:
|
|
lda (screenPtr),y ; 611C colour byte of this cell
|
|
eor textColour ; 611E EOR with the text colour: a cell holding textColour turns black, and back again on the next call
|
|
sta (screenPtr),y ; 6120 write it back
|
|
dey ; 6122 previous cell
|
|
bpl L_611C ; 6123 all cells of the row
|
|
lda menuBlinkState ; 6125 flip the state flag...
|
|
eor #$01 ; 6128 ...between 1 (normal) and 0 (EORed)
|
|
sta menuBlinkState ; 612A store it
|
|
L_612D:
|
|
rts ; 612D done
|
|
|
|
|
|
; menuLoopMode: byteTable, 1 bytes. Variable byte embedded in code: map-highlight mode of the menu
|
|
; loop ($FF unit menu, $01 group menu, $00/$20 join-group picker side bit).
|
|
menuLoopMode:
|
|
.byte $00 ; 612E . map-highlight mode of the menu loop: $FF unit menu (animate the path marker), $01 group menu, $00/$20 join-group picker (the value is the player's side bit)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runJoinGroupMenu - The JOIN GROUP picker. Lists the seven groups ALPHA..GROVER with their current
|
|
; member counts plus an (EXIT) row from screen row 13, preselecting the group the unit is already in.
|
|
; The menu loop runs in group-preview mode, so moving the selection highlights that group on the
|
|
; viewport. Picking a group calls addSelectedUnitToGroup, which queues command $88; if the unit is too
|
|
; far from the group (C = 1, TOO FAR! shown) the list is redrawn and the question repeated.
|
|
; In: selectedUnit $90F8, unitDisplayFlagsTable $FB54 (group number bits 2-4, side bit 5)
|
|
; Out: uiLevel = 2, menuItemCount = 8, menuFirstRow = 13, menuLoopMode = the unit's side bit,
|
|
; selectedGroupKey = the previewed/chosen group, command $88 unit,group queued; $9207 = $FF
|
|
; (dead)
|
|
; Called from: the UNIT OPTIONS item JOIN GROUP ($5F6E)
|
|
; ----------------------------------------------------------------------
|
|
runJoinGroupMenu:
|
|
lda #$02 ; 612F UI level 2
|
|
sta uiLevel ; 6131 uiLevel 2 = group (info-panel colour 4)
|
|
ldy selectedUnit ; 6134 the unit that wants to join
|
|
lda unitDisplayFlagsTable,y ; 6137 $FB54 flags
|
|
and #$1C ; 613A group number in bits 2-4
|
|
lsr a ; 613C shift it down...
|
|
lsr a ; 613D ...to 0-6
|
|
tax ; 613E into X
|
|
stx menuSelectedItem ; 613F preselect the group the unit currently belongs to
|
|
|
|
; ----------------------------------------------------------------------
|
|
; (Re)draw the picker: heading, one row per group with its name and member count, then the (EXIT) row.
|
|
; ----------------------------------------------------------------------
|
|
drawJoinGroupList:
|
|
lda #$00 ; 6142 0
|
|
sta rawGlyphMode ; 6144 ASCII translation on
|
|
sta textCentreWidth ; 6146 no automatic centring
|
|
sta textFillGlyph ; 6149 blank with glyph 0
|
|
lda #$04 ; 614B colour 4 = the group-level text colour
|
|
sta textColour ; 614D ink
|
|
sta textPadColour ; 614F pad
|
|
jsr clearInfoPanel ; 6151 clear the info panel
|
|
lda #$25 ; 6154 $CB25 = 'WHICH GROUP:'
|
|
ldy #$CB ; 6156 high byte
|
|
jsr printString ; 6158 print the heading
|
|
jsr printNewline ; 615B next row
|
|
lda #$00 ; 615E 0
|
|
sta cellColA ; 6160 zp_22 is reused here as the group counter 0-6
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One row per group 0-6: the name from the 8-byte table at $C92C, a space, then the number of living
|
|
; members of that group on the clicking unit's side, right-aligned to two columns.
|
|
; ----------------------------------------------------------------------
|
|
L_6162:
|
|
lda #$2C ; 6162 $C92C = the seven 8-byte group names ALPHA..GROVER
|
|
ldy #$C9 ; 6164 high byte
|
|
ldx cellColA ; 6166 group number
|
|
jsr printTableEntry ; 6168 print the name at $C92C + 8*group
|
|
lda #$A0 ; 616B $A0 = space
|
|
jsr printChar ; 616D separate the name from the count
|
|
lda cellColA ; 6170 group number...
|
|
asl a ; 6172 ...times 4...
|
|
asl a ; 6173 ...= the group-id field of the flag byte
|
|
ldy selectedUnit ; 6174 the joining unit
|
|
cpy #$32 ; 6177 index < 50 = one of player 0's units (a fixed test: the side comes from the index, not from who is playing)
|
|
bcc L_617D ; 6179 side 0: leave bit 5 clear
|
|
ora #$20 ; 617B side 1: set bit 5 of the group key
|
|
L_617D:
|
|
jsr countGroupMembers ; 617D A = number of living members of that group
|
|
cmp #$0A ; 6180 two digits?
|
|
bcs L_618B ; 6182 yes: no padding
|
|
pha ; 6184 keep the count
|
|
lda #$A0 ; 6185 $A0 = space
|
|
jsr printChar ; 6187 right-align single-digit counts
|
|
pla ; 618A count back
|
|
L_618B:
|
|
ldy #$00 ; 618B high byte of the value to print
|
|
jsr printDecimal ; 618D print the member count
|
|
jsr printNewline ; 6190 next row
|
|
inc cellColA ; 6193 next group
|
|
lda cellColA ; 6195 ...
|
|
cmp #$07 ; 6197 seven groups
|
|
bcc L_6162 ; 6199 loop
|
|
lda #$B2 ; 619B $CAB2 = ' (EXIT)' as the eighth row
|
|
ldy #$CA ; 619D high byte
|
|
jsr printString ; 619F print it
|
|
lda #$0D ; 61A2 screen row 13
|
|
sta menuFirstRow ; 61A4 the list starts on row 13 of the info panel
|
|
lda #$08 ; 61A7 8
|
|
sta menuItemCount ; 61A9 7 groups + (EXIT)
|
|
lda #$FF ; 61AC $FF
|
|
sta unusedFlag9207 ; 61AE $9207 is written here and at $639B but never read anywhere - dead store
|
|
ldx selectedUnit ; 61B1 the joining unit
|
|
lda unitDisplayFlagsTable,x ; 61B4 $FB54 flags
|
|
and #$20 ; 61B7 keep only the side bit
|
|
sta menuLoopMode ; 61B9 menuLoopMode 0/$20: the loop previews the group the current row stands for
|
|
jsr runMenuLoop ; 61BC run the selector without the unit-idle watch
|
|
lda menuSelectedItem ; 61BF the selected row = the group number (0-6), 7 = (EXIT)
|
|
cmp #$07 ; 61C2 (EXIT)?
|
|
bcs L_61CE ; 61C4 yes: nothing to do
|
|
jsr addSelectedUnitToGroup ; 61C6 queue $88 unit,group; C = 1 when the unit is too far from the group
|
|
bcc L_61CE ; 61C9 joined
|
|
jmp drawJoinGroupList ; 61CB TOO FAR! was shown: redraw the list and ask again
|
|
|
|
L_61CE:
|
|
rts ; 61CE back to the unit menu
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runGroupOptionsMenu - Group branch of handleOwnUnitClick. It takes the group key from the clicked
|
|
; unit's flags, highlights every member, prints the GROUP + name header and waits: a stick deflection
|
|
; sends the whole group SETTING DESTINATION, a click opens the GROUP OPTIONS menu (only when the UNIT
|
|
; MENUS option $0BA3 bit 3 is on) offering CLOAKING, DIG or BLITZ, MEMBERSHIP, FORMATION, DIRECT UNIT
|
|
; and (EXIT). The chosen item either queues a group command ($83/$85/$94/$96/$A1/$A2, argument = the
|
|
; group key), opens the group editor or the formation sub-menu, or hands the unit to
|
|
; runUnitOptionsMenu.
|
|
; In: selectedUnit $90F8, unit arrays $F898/$F8FC/$F960/$FB54, joyDirection zp_5D, gameTypeOptions
|
|
; $0BA3 bit 3
|
|
; Out: selectedGroupKey/highlightedGroupKey = group key, highlightedUnit = $FF, uiLevel = 2,
|
|
; menuLoopMode = 1, cmdParam0 = group key with one group command queued, cursorTargetMode zp_BB
|
|
; set for the editor; ends with clearGroupHighlight and exitUnitMenu
|
|
; Called from: handleOwnUnitClick ($5E21)
|
|
; ----------------------------------------------------------------------
|
|
runGroupOptionsMenu:
|
|
lda #$00 ; 61CF 0
|
|
sta rawGlyphMode ; 61D1 ASCII translation on
|
|
sta textCentreWidth ; 61D3 no automatic centring
|
|
sta textFillGlyph ; 61D6 blank with glyph 0
|
|
ldy selectedUnit ; 61D8 the clicked unit
|
|
lda unitDisplayFlagsTable,y ; 61DB $FB54 flags
|
|
and #$3C ; 61DE group key = group number (bits 2-4) plus the side bit 5
|
|
sta selectedGroupKey ; 61E0 this is the group being commanded
|
|
sta highlightedGroupKey ; 61E3 and the group the viewport highlights
|
|
lda #$01 ; 61E6 1
|
|
sta menuLoopMode ; 61E8 menuLoopMode 1 = animate the path marker, do not preview groups
|
|
lda #$FF ; 61EB $FF
|
|
sta highlightedUnit ; 61ED no single-unit highlight - the whole group is marked instead
|
|
lda #$02 ; 61F0 2
|
|
sta uiLevel ; 61F2 uiLevel 2 = group
|
|
jsr setCursorShapeSquare ; 61F5 square cell cursor
|
|
jsr drawViewport ; 61F8 repaint the viewport with the group highlighted
|
|
lda #$04 ; 61FB colour 4 = group level
|
|
sta textColour ; 61FD ink
|
|
sta textPadColour ; 61FF pad
|
|
jsr clearInfoPanel ; 6201 clear the info panel
|
|
lda #$0F ; 6204 screen row 15
|
|
sta textCursorRow ; 6206 heading row (printGroupTitle sets the column)
|
|
jsr printGroupTitleAndName ; 6208 print 'GROUP' and the group's name
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait for the player: a stick deflection means 'move the whole group', a click opens the GROUP
|
|
; OPTIONS menu. The path marker keeps animating meanwhile.
|
|
; ----------------------------------------------------------------------
|
|
waitForGroupClick:
|
|
lda selectedUnit ; 620B the clicked unit...
|
|
sta infoPanelUnit ; 620E ...keeps the info panel
|
|
jsr updateGameFrame ; 6211 one frame of the battle
|
|
jsr animatePathMarkerToDestination; 6214 walk the path marker along the group's route
|
|
lda joyDirection ; 6217 joystick nibble
|
|
cmp #$0F ; 6219 centred?
|
|
beq L_6226 ; 621B centred: look for a click
|
|
jsr waitForGroupRepositionsDone; 621D deflected: first let any pending re-position orders be applied
|
|
jsr promptForDestination ; 6220 then run SETTING / DESTINATION for the whole group (uiLevel 2 makes it command $81)
|
|
jmp groupMenuDone ; 6223 done
|
|
|
|
L_6226:
|
|
jsr checkFirePressed ; 6226 fresh fire-button press?
|
|
beq L_6233 ; 6229 clicked: open the GROUP OPTIONS menu
|
|
jsr checkUiContinue ; 622B may this UI keep running?
|
|
bne waitForGroupClick ; 622E yes: keep waiting
|
|
L_6230:
|
|
jmp groupMenuDone ; 6230 aborted
|
|
|
|
L_6233:
|
|
lda gameTypeOptions ; 6233 game type and option bits
|
|
and #$08 ; 6236 bit 3 = the UNIT MENUS option
|
|
beq L_6230 ; 6238 option off: no group menu in this scenario
|
|
lda #$00 ; 623A 0
|
|
sta menuSelectedItem ; 623C start on the first row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Build the GROUP OPTIONS list. Re-entered from $62B1 when the clicked unit's idle flag changed. Same
|
|
; first two items as the unit menu, then the group-only items.
|
|
; ----------------------------------------------------------------------
|
|
buildGroupOptionsMenu:
|
|
lda #$04 ; 623F colour 4
|
|
sta textColour ; 6241 ink
|
|
sta textPadColour ; 6243 pad
|
|
jsr clearInfoPanel ; 6245 clear the info panel
|
|
jsr printGroupTitle ; 6248 print 'GROUP' at column 4
|
|
lda #$13 ; 624B $CB13 = ' OPTIONS' + CR + CR
|
|
ldy #$CB ; 624D high byte
|
|
jsr printString ; 624F print the second heading line
|
|
ldy selectedUnit ; 6252 leftover from the unit-menu version: Y is reloaded at $625A (dead)
|
|
ldx #$00 ; 6255 0
|
|
stx menuItemCount ; 6257 empty item list
|
|
ldy selectedUnit ; 625A the clicked unit stands for the whole group
|
|
ldx #$02 ; 625D item id 2 = CLOAKING ON
|
|
lda unitPathColTable,y ; 625F $F898 bit 6 = 'cloaking ordered'
|
|
and #$40 ; 6262 test it
|
|
beq L_6268 ; 6264 not cloaked: CLOAKING ON
|
|
ldx #$03 ; 6266 item id 3 = NO CLOAKING
|
|
L_6268:
|
|
jsr addMenuItem ; 6268 append it
|
|
ldy selectedUnit ; 626B unit index
|
|
lda unitDisplayFlagsTable,y ; 626E $FB54 flags
|
|
ora #$01 ; 6271 keep the snapshot nonzero
|
|
sta menuIdleSnapshot+1 ; 6273 patch menuIdleSnapshot+1 ($602A) exactly as the unit menu does
|
|
bpl L_6284 ; 6276 bit 7 clear = moving: offer the blitz items
|
|
ldx #$11 ; 6278 item id 17 = DIG OUT
|
|
lda unitStunTable,y ; 627A $F960 bits 5-7 = dug in / digging
|
|
and #$E0 ; 627D isolate them
|
|
bne L_628E ; 627F already digging: DIG OUT
|
|
inx ; 6281 item id 18 = DIG IN
|
|
bne L_628E ; 6282 always taken
|
|
L_6284:
|
|
ldx #$0A ; 6284 item id 10 = SET BLITZ
|
|
lda unitPathRowTable,y ; 6286 $F8FC bit 6 = 'blitz ordered'
|
|
and #$40 ; 6289 test it
|
|
beq L_628E ; 628B blitz off: SET BLITZ
|
|
inx ; 628D item id 11 = BLITZ OFF
|
|
L_628E:
|
|
jsr addMenuItem ; 628E append the dig/blitz item
|
|
ldx #$06 ; 6291 item id 6 = MEMBERSHIP
|
|
jsr addMenuItem ; 6293 append it
|
|
ldx #$00 ; 6296 item id 0 = FORMATION
|
|
jsr addMenuItem ; 6298 append it
|
|
ldx #$08 ; 629B item id 8 = DIRECT UNIT
|
|
jsr addMenuItem ; 629D append it
|
|
ldx #$0C ; 62A0 item id 12 = (EXIT)
|
|
jsr addMenuItem ; 62A2 append it
|
|
lda #$0F ; 62A5 screen row 15
|
|
sta menuFirstRow ; 62A7 the list starts on row 15
|
|
jsr runMenuLoopWatchUnit ; 62AA run the selector: A = item id, Y = row (or $80)
|
|
cpy #$80 ; 62AD $80 = the unit's idle flag changed
|
|
bne L_62B4 ; 62AF no: act on the item
|
|
jmp buildGroupOptionsMenu ; 62B1 yes: rebuild (DIG and BLITZ swap places)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Dispatch on the chosen item id. All the command branches end at $62D7, which supplies the group key
|
|
; as the command's argument.
|
|
; ----------------------------------------------------------------------
|
|
L_62B4:
|
|
sta menuSelectedItem ; 62B4 keep the chosen item id
|
|
ldy selectedUnit ; 62B7 dead load, as in the unit menu
|
|
lda menuSelectedItem ; 62BA the chosen item id
|
|
cmp #$02 ; 62BD CLOAKING ON?
|
|
bne L_62C5 ; 62BF no
|
|
ldy #$83 ; 62C1 command $83 = cloak on (2 bytes: group key)
|
|
bne L_62CB ; 62C3 always taken
|
|
L_62C5:
|
|
cmp #$03 ; 62C5 NO CLOAKING?
|
|
bne L_62E4 ; 62C7 no
|
|
ldy #$85 ; 62C9 command $85 = cloak off (2 bytes: group key)
|
|
L_62CB:
|
|
ldx selectedUnit ; 62CB the clicked unit
|
|
lda unitPathColTable,x ; 62CE flip only its local cloak bit...
|
|
eor #$40 ; 62D1 ...bit 6 of $F898...
|
|
sta unitPathColTable,x ; 62D3 ...so the menu shows the new state at once (the handler does the whole group)
|
|
tya ; 62D6 command byte back into A
|
|
queueGroupCommand:
|
|
pha ; 62D7 keep the command byte
|
|
lda selectedGroupKey ; 62D8 the group key...
|
|
sta cmdParam0 ; 62DB ...is the single argument of every group command
|
|
pla ; 62DD command byte back
|
|
jsr queueCmd2 ; 62DE queue 'command, group key'
|
|
jmp groupMenuDone ; 62E1 done
|
|
|
|
L_62E4:
|
|
cmp #$06 ; 62E4 MEMBERSHIP?
|
|
bne L_62F2 ; 62E6 no
|
|
lda #$00 ; 62E8 0
|
|
sta cursorTargetMode ; 62EA zp_BB >= 0 selects membership mode
|
|
L_62EC:
|
|
jsr runGroupEditor ; 62EC run the group editor
|
|
jmp groupMenuDone ; 62EF done
|
|
|
|
L_62F2:
|
|
cmp #$08 ; 62F2 DIRECT UNIT?
|
|
bne L_62F9 ; 62F4 no
|
|
jmp runUnitOptionsMenu ; 62F6 hand the clicked unit to the single-unit menu (which ends in exitUnitMenu)
|
|
|
|
L_62F9:
|
|
cmp #$00 ; 62F9 FORMATION? (item id 0)
|
|
bne L_630A ; 62FB no
|
|
jsr runFormationOptionsMenu ; 62FD SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT); Y = the chosen row
|
|
cpy #$03 ; 6300 row 3 = CUSTOM
|
|
bne groupMenuDone ; 6302 the three canned formations are sent by the sub-menu itself (command $A3)
|
|
lda #$80 ; 6304 $80
|
|
sta cursorTargetMode ; 6306 zp_BB bit 7 selects custom-formation mode
|
|
bmi L_62EC ; 6308 always taken: run the editor in formation mode
|
|
L_630A:
|
|
cmp #$0A ; 630A SET BLITZ?
|
|
bne L_6312 ; 630C no
|
|
ldy #$94 ; 630E command $94 = blitz on (2 bytes: group key)
|
|
bne L_6318 ; 6310 always taken
|
|
L_6312:
|
|
cmp #$0B ; 6312 BLITZ OFF?
|
|
bne L_6326 ; 6314 no
|
|
ldy #$96 ; 6316 command $96 = blitz off (2 bytes: group key)
|
|
L_6318:
|
|
ldx selectedUnit ; 6318 the clicked unit
|
|
lda unitPathRowTable,x ; 631B flip its local blitz bit...
|
|
eor #$40 ; 631E ...bit 6 of $F8FC...
|
|
sta unitPathRowTable,x ; 6320 ...for instant feedback
|
|
tya ; 6323 command byte back into A
|
|
bne queueGroupCommand ; 6324 always taken
|
|
L_6326:
|
|
cmp #$11 ; 6326 DIG OUT?
|
|
bne L_632E ; 6328 no
|
|
lda #$A2 ; 632A command $A2 = dig out (2 bytes: group key)
|
|
bne queueGroupCommand ; 632C always taken
|
|
L_632E:
|
|
cmp #$12 ; 632E DIG IN?
|
|
bne groupMenuDone ; 6330 anything else, including (EXIT): just close
|
|
lda #$A1 ; 6332 command $A1 = dig in (2 bytes: group key)
|
|
bne queueGroupCommand ; 6334 always taken
|
|
groupMenuDone:
|
|
jsr clearGroupHighlight ; 6336 drop the group highlight and repaint the viewport
|
|
jmp exitUnitMenu ; 6339 uiLevel 0, no info-panel unit, return to the battlefield loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printGroupTitleAndName - Header of the group menu: prints GROUP (printGroupTitle) and, on the next
|
|
; row at column 4, the name of group (selectedGroupKey >> 2) & 7 from the 8-byte name table at $C92C,
|
|
; followed by a newline.
|
|
; In: selectedGroupKey $9206
|
|
; Out: two lines printed, textCursorCol zp_3F = 4
|
|
; Called from: runGroupOptionsMenu ($6208)
|
|
; ----------------------------------------------------------------------
|
|
printGroupTitleAndName:
|
|
jsr printGroupTitle ; 633C print 'GROUP' + CR at column 4
|
|
lda #$04 ; 633F column 4
|
|
sta textCursorCol ; 6341 indent the name under the title
|
|
jsr updateCursorPointers ; 6343 recompute the bitmap/colour pointers for the new column
|
|
lda selectedGroupKey ; 6346 group key
|
|
lsr a ; 6349 shift out the two low bits...
|
|
lsr a ; 634A ...
|
|
and #$07 ; 634B ...leaving the group number 0-6 (the side bit is dropped)
|
|
tax ; 634D index for the name table
|
|
lda #$2C ; 634E $C92C = the seven 8-byte group names
|
|
ldy #$C9 ; 6350 high byte
|
|
jsr printTableEntry ; 6352 print the name at $C92C + 8*group
|
|
jmp printNewline ; 6355 and move to the next row
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printGroupTitle - Moves the text cursor to column 4 of the info panel and prints the string GROUP
|
|
; ($CB1F, which ends with a CR).
|
|
; In: the current text window
|
|
; Out: one line printed, textCursorCol zp_3F = 4
|
|
; Called from: runGroupOptionsMenu ($6248) and printGroupTitleAndName ($633C)
|
|
; ----------------------------------------------------------------------
|
|
printGroupTitle:
|
|
lda #$04 ; 6358 column 4
|
|
sta textCursorCol ; 635A indent the title
|
|
jsr updateCursorPointers ; 635C recompute the bitmap/colour pointers
|
|
lda #$1F ; 635F $CB1F = 'GROUP' (ends with CR)
|
|
ldy #$CB ; 6361 high byte
|
|
jmp printString ; 6363 print it and return to the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearGroupHighlight - Drops the viewport's group highlight and repaints the 7x5 tactical view.
|
|
; In: -
|
|
; Out: highlightedGroupKey $9205 = $FF, viewport redrawn
|
|
; Called from: runGroupOptionsMenu ($6336) and runGroupEditor ($64D4)
|
|
; ----------------------------------------------------------------------
|
|
clearGroupHighlight:
|
|
lda #$FF ; 6366 $FF
|
|
sta highlightedGroupKey ; 6368 no group highlighted any more
|
|
jmp drawViewport ; 636B repaint the 7x5 viewport without the highlight
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printSettingHeader - Header of the group editor: clears the info panel in colour 4, moves to screen
|
|
; row 15 and prints SETTING centred, followed by MEMBERSHIP ($CA32) when cursorTargetMode zp_BB is
|
|
; positive or FORMATION ($C9D2) when its bit 7 is set.
|
|
; In: cursorTargetMode zp_BB
|
|
; Out: textColour/textPadColour = 4, two centred lines printed
|
|
; Called from: runGroupEditor ($63B0, and again after every hover info panel at $63F8)
|
|
; ----------------------------------------------------------------------
|
|
printSettingHeader:
|
|
lda #$04 ; 636E colour 4 = group level
|
|
sta textColour ; 6370 ink
|
|
sta textPadColour ; 6372 pad
|
|
jsr clearInfoPanel ; 6374 clear the info panel
|
|
lda #$0F ; 6377 screen row 15
|
|
jsr setCursorRow ; 6379 position the cursor there
|
|
jsr printSettingWord ; 637C print 'SETTING' centred, then a newline
|
|
lda #$32 ; 637F $CA32 = 'MEMBERSHIP' (entry 6 of the 16-byte option strings)
|
|
ldy #$CA ; 6381 high byte
|
|
bit cursorTargetMode ; 6383 zp_BB: bit 7 set = custom formation mode
|
|
bpl L_638B ; 6385 membership mode: print MEMBERSHIP
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printFormationCentred - Prints the word FORMATION ($C9D2) centred in the current text window; also
|
|
; the tail of printSettingHeader in custom-formation mode.
|
|
; In: the current text window
|
|
; Out: one centred line printed
|
|
; Called from: runFormationOptionsMenu ($6602) and by fall-through from printSettingHeader
|
|
; ----------------------------------------------------------------------
|
|
printFormationCentred:
|
|
lda #$D2 ; 6387 $C9D2 = 'FORMATION' (entry 0 of the 16-byte option strings)
|
|
ldy #$C9 ; 6389 high byte
|
|
L_638B:
|
|
jmp printStringCentred ; 638B print the word centred in the info panel
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runGroupEditor - Interactive editor for the group in selectedGroupKey, in MEMBERSHIP mode (zp_BB >=
|
|
; 0) or custom FORMATION mode (zp_BB bit 7 set). It halts the group with command $9B (formation mode
|
|
; first waits for pending re-positions), then lets the player scroll the viewport with the joystick
|
|
; and shows hover information for an own unit after 30 idle frames. Clicking an own unit adds it
|
|
; (command $88, ADDED / TOO FAR!) or removes it (command $89, REMOVED) in membership mode and asks for
|
|
; a NEW POSITION (command $98) in formation mode; the command centre is always refused with sound 3.
|
|
; Clicking empty ground or an enemy prints FINISHED and ends the editor. Only the part up to $6467
|
|
; belongs to this chunk; the routine continues to $64D9.
|
|
; In: selectedGroupKey $9206, cursorTargetMode zp_BB (mode), joyDirection zp_5D, joyRepeatTimer
|
|
; zp_6C,
|
|
; inputIdleFrames $90EE, comcenUnit $9236, unitDisplayFlagsTable $FB54 and the side-branch opcode
|
|
; at groupEditorOwnSideBranch ($63E2)
|
|
; Out: selectedUnit $90F8 = the unit under the cursor (bit 7 set = empty cell or enemy), uiLevel = 2,
|
|
; infoPanelUnit = $FF, $9207 = $FF (dead), highlightedGroupKey, joyRepeatMask/joyHoldCounter,
|
|
; inputLockoutTimer, commands $9B/$88/$89/$98 queued, viewport scrolled and redrawn
|
|
; Called from: the GROUP OPTIONS items MEMBERSHIP and FORMATION/CUSTOM ($62EC)
|
|
; ----------------------------------------------------------------------
|
|
runGroupEditor:
|
|
lda #$02 ; 638E 2
|
|
sta uiLevel ; 6390 uiLevel 2 = group
|
|
jsr setCursorShapeSquare ; 6393 square cell cursor
|
|
lda #$FF ; 6396 $FF
|
|
sta infoPanelUnit ; 6398 the info panel belongs to the editor, not to a unit
|
|
sta unusedFlag9207 ; 639B dead store - $9207 is never read (see $61AE)
|
|
lda selectedGroupKey ; 639E the group being edited
|
|
sta highlightedGroupKey ; 63A1 highlight all of its members
|
|
jsr drawViewport ; 63A4 repaint the viewport
|
|
lda #$00 ; 63A7 0
|
|
sta rawGlyphMode ; 63A9 ASCII translation on
|
|
sta textCentreWidth ; 63AB no automatic centring
|
|
sta textFillGlyph ; 63AE blank with glyph 0
|
|
jsr printSettingHeader ; 63B0 'SETTING' + MEMBERSHIP or FORMATION
|
|
lda cursorTargetMode ; 63B3 zp_BB: bit 7 set = custom formation mode
|
|
bpl L_63C0 ; 63B5 membership mode
|
|
jsr waitForGroupRepositionsDone; 63B7 formation mode: wait until earlier re-position orders have been applied
|
|
jsr sendHaltGroupCmd ; 63BA command $9B halts every member so the group stays put while it is edited
|
|
jmp groupEditorActOnUnit ; 63BD formation mode starts straight away with the unit the player clicked on
|
|
|
|
L_63C0:
|
|
jsr sendHaltGroupCmd ; 63C0 membership mode: halt the group as well
|
|
lda #$78 ; 63C3 120 frames
|
|
jsr waitFramesOrInput ; 63C5 let the halt take effect; returns early on a click or a stick deflection
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Main editor loop: one game frame per pass, hover information for the unit under the cursor, click
|
|
; handling and joystick scrolling of the viewport.
|
|
; ----------------------------------------------------------------------
|
|
groupEditorLoop:
|
|
jsr updateGameFrame ; 63C8 one frame of the battle
|
|
jsr checkUiContinue ; 63CB may this UI keep running?
|
|
bne L_63D3 ; 63CE yes
|
|
jmp L_64D4 ; 63D0 SPACE / screen change / game over: leave the editor
|
|
|
|
L_63D3:
|
|
lda joyDirection ; 63D3 joystick nibble
|
|
cmp #$0F ; 63D5 centred?
|
|
bne groupEditorScroll ; 63D7 deflected: scroll the viewport
|
|
jsr readCellUnderCursor ; 63D9 map byte under the cursor = the centre cell of the 7x5 view
|
|
eor #$80 ; 63DC flip bit 7: a unit cell ($80|index) becomes positive, plain terrain becomes negative
|
|
bmi L_63E6 ; 63DE empty cell: nothing selectable
|
|
cmp #$32 ; 63E0 unit index < 50 = side 0
|
|
groupEditorOwnSideBranch:
|
|
bcc L_63E6 ; 63E2 opcode byte patched by patchOwnSideBranches ($7BCF): $90 BCC for side 0, $B0 BCS for side 1, so the branch is taken when the unit is ours
|
|
ora #$80 ; 63E4 an enemy unit counts as 'nothing under the cursor'
|
|
L_63E6:
|
|
sta selectedUnit ; 63E6 bit 7 set = empty cell or enemy unit
|
|
lda inputIdleFrames ; 63E9 frames since the player last touched the controls
|
|
cmp #$1E ; 63EC 30 frames
|
|
bcc L_63FB ; 63EE still moving: no hover info
|
|
lda selectedUnit ; 63F0 the cell under the cursor
|
|
bmi L_63FB ; 63F3 only own units get an info panel
|
|
jsr showUnitInfoPanel ; 63F5 show its status while the player hovers
|
|
jsr printSettingHeader ; 63F8 then restore the editor's own heading
|
|
L_63FB:
|
|
jsr checkFirePressed ; 63FB fresh fire-button press?
|
|
beq groupEditorActOnUnit ; 63FE clicked: act on the cell
|
|
jsr checkUiContinue ; 6400 may this UI keep running?
|
|
bne groupEditorScroll ; 6403 yes
|
|
jmp L_64D4 ; 6405 aborted: leave the editor
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Scroll the viewport one cell per auto-repeat step. $6418-$6434 is an inline copy of
|
|
; setJoystickRepeatDelay ($0D49) - the same fourteen instructions, not a JSR.
|
|
; ----------------------------------------------------------------------
|
|
groupEditorScroll:
|
|
lda joyRepeatTimer ; 6408 auto-repeat delay still running?
|
|
bne groupEditorLoop ; 640A yes: next frame
|
|
ldy joyDirection ; 640C joystick nibble as the table index
|
|
lda #$00 ; 640E 0
|
|
sta inputLockoutTimer ; 6410 cancel the input lockout: scrolling must respond at once
|
|
lda joystickDirectionTable,y; 6413 direction code 0-7 (N,S,E,W,NW,NE,SE,SW), $FF when centred or invalid
|
|
bmi groupEditorLoop ; 6416 no usable direction
|
|
ldy #$10 ; 6418 16 frames before the first repeat
|
|
pha ; 641A keep the direction code
|
|
inc joyHoldCounter ; 641B count how long the stick has been held
|
|
lda joyHoldCounter ; 641E ...
|
|
and joyRepeatMask ; 6421 the IRQ clears this mask while the stick is centred, so a new press restarts the count at 0
|
|
sta joyHoldCounter ; 6424 store the masked counter
|
|
cmp #$01 ; 6427 first step after centring?
|
|
bcc L_642D ; 6429 yes: keep the 16-frame delay
|
|
ldy #$06 ; 642B held: repeat every 6 frames
|
|
L_642D:
|
|
sty joyRepeatTimer ; 642D arm the repeat timer
|
|
lda #$7F ; 642F $7F
|
|
sta joyRepeatMask ; 6431 let the hold counter grow again
|
|
pla ; 6434 direction code back
|
|
jsr scrollViewportByDirection; 6435 scroll the viewport one cell in that direction
|
|
jmp groupEditorLoop ; 6438 next frame
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The player clicked: act on whatever the cursor stands on. Empty ground or an enemy finishes the
|
|
; editor, the command centre is refused, and any other own unit is added, removed or re-positioned by
|
|
; the code that follows this chunk.
|
|
; ----------------------------------------------------------------------
|
|
groupEditorActOnUnit:
|
|
lda #$04 ; 643B colour 4
|
|
sta textColour ; 643D ink
|
|
sta textPadColour ; 643F pad
|
|
jsr clearInfoPanel ; 6441 clear the info panel for the result message
|
|
lda #$10 ; 6444 screen row 16
|
|
jsr setCursorRow ; 6446 position the cursor there
|
|
ldy selectedUnit ; 6449 the cell under the cursor
|
|
bpl L_645D ; 644C one of our own units: act on it
|
|
lda #$47 ; 644E $CB47 = 'FINISHED'
|
|
ldy #$CB ; 6450 high byte
|
|
jsr printStringCentred ; 6452 print it centred
|
|
lda #$3C ; 6455 60 frames
|
|
jsr waitFramesOrInput ; 6457 show it briefly
|
|
jmp L_64D4 ; 645A clicking empty ground or an enemy ends the editor
|
|
|
|
L_645D:
|
|
cpy comcenUnit ; 645D is it the player's command centre (unit 49 or 99)?
|
|
bne L_646A ; 6460 no: an ordinary unit
|
|
lda #$03 ; 6462 sound 3 = rejected
|
|
jsr playSound ; 6464 play it
|
|
jmp L_64BE ; 6467 the comcen can never be a group member: ignore the click and go round again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Click on one of our own units while the group editor is running. Y = the unit index (selectedUnit,
|
|
; already known to be alive, ours and not the comcen). Four cases:
|
|
; MEMBERSHIP mode (cursorTargetMode >= 0): outsider -> ADDED / TOO FAR!, member -> REMOVED
|
|
; FORMATION mode (cursorTargetMode < 0): outsider -> NOT IN GROUP, member -> NEW POSITION
|
|
; ----------------------------------------------------------------------
|
|
L_646A:
|
|
lda unitDisplayFlagsTable,y ; 646A local flag copy of the clicked unit: facing 0-1, group id 2-4, side 5, member 6
|
|
and #$3C ; 646D keep the group key = group id (bits 2-4) plus the side bit ($20)
|
|
cmp selectedGroupKey ; 646F does it already carry the key of the group being edited?
|
|
beq L_6496 ; 6472 yes - go on to test the real membership bit
|
|
L_6474:
|
|
lda cursorTargetMode ; 6474 editor mode: >= 0 = MEMBERSHIP, < 0 = custom FORMATION
|
|
bpl L_6483 ; 6476 MEMBERSHIP mode: try to enlist this outsider
|
|
lda #$03 ; 6478 sound 3 = the error beep
|
|
jsr playSound ; 647A FORMATION mode may only re-position units that are already members
|
|
lda #$8D ; 647D 'NOT IN GROUP' ($CB8D), low byte
|
|
ldy #$CB ; 647F ...high byte
|
|
bne L_64B8 ; 6481 always taken (A = $8D) - go and print the refusal
|
|
L_6483:
|
|
jsr addSelectedUnitToGroup ; 6483 membership add: C=1 when the unit is more than 8 cells from the group (TOO FAR! already shown)
|
|
bcc L_6490 ; 6486 accepted -> report ADDED
|
|
lda #$3C ; 6488 60 frames
|
|
jsr runGameFrames ; 648A let the real-time battle run on so TOO FAR! stays readable
|
|
jmp L_64D1 ; 648D straight back to the cursor loop; the panel already holds the message
|
|
|
|
L_6490:
|
|
lda #$3B ; 6490 'ADDED' ($CB3B), low byte
|
|
ldy #$CB ; 6492 ...high byte
|
|
bne L_64B8 ; 6494 always taken (A = $3B)
|
|
L_6496:
|
|
lda unitDisplayFlagsTable,y ; 6496 the key matches - now check the 'is in a group' bit itself
|
|
and #$40 ; 6499 bit 6 of the local flags
|
|
beq L_6474 ; 649B key matches but the member bit is clear (stale key) - treat it as an outsider
|
|
lda cursorTargetMode ; 649D editor mode again
|
|
bpl L_64B1 ; 649F MEMBERSHIP mode: this member is being removed
|
|
jsr printSettingHeaderForLevel; 64A1 FORMATION mode: 'SETTING' header in the info panel, in the uiLevel colour
|
|
lda #$A6 ; 64A4 'NEW POSITION' ($CBA6), low byte
|
|
ldy #$CB ; 64A6 ...high byte
|
|
jsr printStringCentred ; 64A8 centred under SETTING
|
|
jsr orderGroupNewPosition ; 64AB run the free cell pick and queue command $98 unit,col,row
|
|
jmp L_64BE ; 64AE skip the ADDED/REMOVED print - orderGroupNewPosition has drawn its own feedback
|
|
|
|
L_64B1:
|
|
jsr removeSelectedUnitFromGroup; 64B1 clear the local member bit and queue command $89 unit
|
|
lda #$40 ; 64B4 'REMOVED' ($CB40), low byte
|
|
ldy #$CB ; 64B6 ...high byte
|
|
L_64B8:
|
|
jsr printStringCentred ; 64B8 print ADDED / REMOVED / NOT IN GROUP centred in the info panel
|
|
jsr drawViewport ; 64BB redraw the 7x5 tactical view so the member highlight is current
|
|
L_64BE:
|
|
jsr checkUiContinue ; 64BE may the editor keep running? 0 = game over, screen change or SPACE
|
|
beq L_64D4 ; 64C1 no - tear the editor down
|
|
lda #$3C ; 64C3 60 frames
|
|
jsr waitFramesOrInput ; 64C5 hold the message, but return early on a fresh stick move or button press
|
|
lda selectedGroupKey ; 64C8 the group being edited...
|
|
sta highlightedGroupKey ; 64CB ...is highlighted again (NEW POSITION had highlighted a single unit)
|
|
jsr drawViewport ; 64CE redraw the viewport with the group highlight back
|
|
L_64D1:
|
|
jmp groupEditorLoop ; 64D1 back to the top of the group-editor cursor loop at $63C8
|
|
|
|
L_64D4:
|
|
jsr clearGroupHighlight ; 64D4 highlightedGroupKey = $FF and redraw - no more group highlight
|
|
jmp exitUnitMenu ; 64D7 uiLevel = 0, info-panel unit = $FF, return to the battlefield loop
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendHaltGroupCmd - Queues the 2-byte command $9B with the key of the currently selected group. Its
|
|
; executor ($549E) stops every member: destination = current position, idle bit set, cloak and blitz
|
|
; orders cleared. The group editor halts the group first so the player edits settled units.
|
|
; In: selectedGroupKey ($9206) = group id in bits 2-4 plus the side bit $20
|
|
; Out: cmdParam0 = the group key, 2 bytes appended to the outgoing command ring by queueCmd2
|
|
; Called from: runGroupEditor at $63BA (formation mode) and $63C0 (membership mode)
|
|
; ----------------------------------------------------------------------
|
|
sendHaltGroupCmd:
|
|
lda selectedGroupKey ; 64DA the group being edited
|
|
sta cmdParam0 ; 64DD becomes the single argument byte of the command
|
|
lda #$9B ; 64DF command $9B = halt every member of a group
|
|
jmp queueCmd2 ; 64E1 queue code + argument and return through queueCmd2
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printSettingHeaderForLevel - Clears the info panel in the colour that belongs to the current UI
|
|
; level, moves to row 15 and falls into printSettingWord, which prints SETTING centred plus a newline.
|
|
; The caller then prints the object of the order (TARGET, DESTINATION, NEW POSITION).
|
|
; In: uiLevel ($9209) 0-2, uiLevelTextColourTable at $5E14 ($09 browsing, $05 unit, $04 group)
|
|
; Out: textColour = textPadColour = that colour, info panel cleared, cursor on row 15
|
|
; Called from: the SET TARGET item at $5F56, the NEW POSITION branch at $64A1, setUnitDestination
|
|
; ----------------------------------------------------------------------
|
|
printSettingHeaderForLevel:
|
|
ldy uiLevel ; 64E4 0 = just browsing, 1 = one unit selected, 2 = a whole group
|
|
lda uiLevelTextColourTable,y; 64E7 $09 / $05 / $04 - the info-panel colour byte for that level
|
|
sta textColour ; 64EA colour of every character cell printed from here on
|
|
sta textPadColour ; 64EC ...and of the blanks that pad a centred string
|
|
jsr clearInfoPanel ; 64EE select and clear the info panel (columns 25-38, rows 12-21)
|
|
lda #$0F ; 64F1 row 15, three rows below the top of the panel
|
|
jsr setCursorRow ; 64F3 move the cursor there without touching the column
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printSettingWord - Prints the word SETTING ($CB4F) centred in the current text window and ends the
|
|
; line, leaving the cursor on the next row for the object word.
|
|
; In: the text window and colour set up by the caller
|
|
; Out: SETTING printed, cursor on the following row
|
|
; Called from: printSettingHeader at $637C, and by falling out of printSettingHeaderForLevel
|
|
; ----------------------------------------------------------------------
|
|
printSettingWord:
|
|
lda #$4F ; 64F6 'SETTING' ($CB4F), low byte
|
|
ldy #$CB ; 64F8 ...high byte
|
|
jsr printStringCentred ; 64FA centred across the 14-column info panel
|
|
jmp printNewline ; 64FD end the line and return through printNewline
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runSetDestinationScreen - The SETTING / DESTINATION screen. Prints the header, selects the plain
|
|
; move-order cursor and runs the target cursor loop, which issues the move order for the selected unit
|
|
; or, at uiLevel 2, for every member of the selected group. Entered as soon as the player pushes the
|
|
; stick after picking a unit or a group.
|
|
; In: selectedUnit ($90F8), uiLevel ($9209), selectedGroupKey ($9206)
|
|
; Out: cursorTargetMode = 0, a move order queued by runTargetCursorLoop
|
|
; Called from: the unit branch at $5E62 and the group branch at $6220 of handleOwnUnitClick
|
|
; promptForDestination - the console flow for 'set this unit's destination': print SETTING and
|
|
; DESTINATION, put the cursor in plain move mode and drive runTargetCursorLoop until the player
|
|
; commits or aborts, which queues the move order. Not to be confused with setUnitDestination $34D5,
|
|
; which is the engine routine that actually writes a unit's destination and applies the comcen speed
|
|
; rule.
|
|
; ----------------------------------------------------------------------
|
|
promptForDestination:
|
|
jsr printSettingHeaderForLevel; 6500 'SETTING' in the colour of the current UI level
|
|
jsr printNextStringCentred ; 6503 the string right after SETTING is 'DESTINATION' ($CB56) - print it centred too
|
|
lda #$00 ; 6506 0 = plain move cursor: no range limit, the pick becomes a move order
|
|
sta cursorTargetMode ; 6508 cursorTargetMode selects the behaviour of runTargetCursorLoop
|
|
jsr runTargetCursorLoop ; 650A drive the cursor until the player commits or aborts
|
|
rts ; 650D back to the caller (a JMP would have done)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; countGroupMembers - Counts the live units that belong to the group whose key is in A. Used by the
|
|
; WHICH GROUP: list to print the member count beside each group name.
|
|
; In: A = group key (group number * 4 plus the side bit $20)
|
|
; Out: A = scratch18 = number of members, scratch19 = key | $40, Y = $FF
|
|
; Called from: the join-group list builder at $617D
|
|
; ----------------------------------------------------------------------
|
|
countGroupMembers:
|
|
ora #$40 ; 650E add bit 6, the 'is in a group' flag, to the pattern we look for
|
|
sta scratch19 ; 6510 keep the wanted flag pattern here for the compare inside the loop
|
|
ldy #$63 ; 6512 walk all 100 unit slots, 99 down to 0
|
|
lda #$00 ; 6514 member count starts at zero
|
|
sta scratch18 ; 6516 scratch18 accumulates the member count
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Scan every unit slot; count the live ones whose local flags carry exactly this group key.
|
|
; ----------------------------------------------------------------------
|
|
L_6518:
|
|
lda unitTypeTable,y ; 6518 unit type, bit 7 set = the unit is dead
|
|
bmi L_6528 ; 651B dead units are not members
|
|
lda unitDisplayFlagsTable,y ; 651D local flag copy
|
|
and #$7C ; 6520 group id (bits 2-4), side (bit 5) and member bit (bit 6)
|
|
cmp scratch19 ; 6522 same group and actually a member?
|
|
bne L_6528 ; 6524 no
|
|
inc scratch18 ; 6526 yes - one more member
|
|
L_6528:
|
|
dey ; 6528 next unit slot
|
|
bpl L_6518 ; 6529 until index $FF wraps the sign bit
|
|
lda scratch18 ; 652B return the count in A as well
|
|
rts ; 652D the caller wants the count in A
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runRepairWhichMenu - Draws and runs the 'REPAIR WHICH:' menu of the REPAIR console tab. It opens a
|
|
; 7-column text window at column 3 spanning rows 9-19, prints the header, appends the four comcen
|
|
; systems (ENERGY, BATTLE, RADAR, DRONE) and (EXIT) as menu items, then loops: run the selector, and
|
|
; for anything but (EXIT) make one repair attempt on the chosen system. selectedUnit = $FF is the
|
|
; sentinel that makes the menu highlight blink the matching system marker on the comcen picture.
|
|
; In: the comcen state read by attemptSystemRepair; filmPlaybackMode ($0B9B) and gameOverFlags
|
|
; ($923F) through isPlayerInputAllowed; menuSelectedItem ($91D5) after each selector pass
|
|
; Out: text window $39-$3E, menu state $91D4/$91D5/$91D6/$91CE, selectedUnit = $FF; may queue the
|
|
; $9A energy-repair command and clear comcenSystemStatus[system]; window erased on exit
|
|
; Called from: repairTabAction at $1860, which has just erased the four system markers and set
|
|
; showOkMarkers = 1 so undamaged systems are shown too
|
|
; ----------------------------------------------------------------------
|
|
runRepairWhichMenu:
|
|
lda #$00 ; 652E zero for the text modes and the menu counters below
|
|
sta rawGlyphMode ; 6530 $43 = 0: strings are ASCII, control codes (CR) honoured
|
|
sta textCentreWidth ; 6532 $90EF = 0: no centring field, text starts at the left edge
|
|
sta textFillGlyph ; 6535 $3E = 0: glyph 0 is the blank used by the window clear
|
|
sta menuItemCount ; 6537 the item list starts out empty
|
|
sta menuSelectedItem ; 653A highlight will start on the first item
|
|
lda #$09 ; 653D row 9, just under the top of the left map panel
|
|
sta textWindowTop ; 653F first row of the menu window
|
|
clc ; 6541 clear the carry for the add that follows
|
|
adc #$0A ; 6542 the window is 10 rows tall
|
|
sta textWindowBottom ; 6544 row 19 = row limit of the window
|
|
lda #$03 ; 6546 column 3, inside the 22-column map panel
|
|
sta textWindowLeft ; 6548 left edge of the menu window
|
|
lda #$07 ; 654A 7 characters wide - enough for 'REPAIR' and ' ENERGY'
|
|
sta textWindowWidth ; 654C wrap point and default centring width
|
|
lda #$61 ; 654E video-matrix colour byte $61: %01 pixel pairs blue (6), %10 pairs white (1)
|
|
sta textColour ; 6550 the clear below and the header are drawn in it
|
|
jsr bootDriveDataBit ; 6552 paint the whole window blank and home the cursor to row 9 (also textEngineC000:clearTextWindow)
|
|
jsr printNewline ; 6555 leave row 9 empty; the text starts on row 10
|
|
lda #$D6 ; 6558 'REPAIR' CR 'WHICH:' CR CR ($CBD6), low byte
|
|
ldy #$CB ; 655A ...high byte
|
|
jsr printString ; 655C two header rows plus a blank one
|
|
lda textCursorRow ; 655F the trailing CRs left the cursor on row 13
|
|
sta menuFirstRow ; 6561 row 13 is the first row the highlight bar may sit on
|
|
ldx #$0D ; 6564 menu item id $0D = ' ENERGY' (ids 12-16 index the strings at $CAB2)
|
|
stx scratch1C ; 6566 id of the item currently being added
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Append the four comcen systems as menu items 13,14,15,16 = ENERGY, BATTLE, RADAR, DRONE. Each
|
|
; addMenuItem prints the name (its string ends in CR) so the rows fill downwards.
|
|
; ----------------------------------------------------------------------
|
|
L_6568:
|
|
jsr addMenuItem ; 6568 record the id in menuItemIdList and print its name
|
|
inc scratch1C ; 656B next system
|
|
ldx scratch1C ; 656D addMenuItem takes the id in X
|
|
cpx #$11 ; 656F $11 = one past ' DRONE', so ids $0D-$10 are added
|
|
bcc L_6568 ; 6571 keep going while there are systems left
|
|
ldx #$0C ; 6573 item id $0C = ' (EXIT)', always the last entry
|
|
jsr addMenuItem ; 6575 menu now holds 5 items
|
|
lda #$60 ; 6578 colour byte $60 for the rows the highlight will run over
|
|
sta textColour ; 657A the item rows and the highlight bar use this
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Menu loop: pick a system, repair it, remove the highlight and start over until (EXIT).
|
|
; ----------------------------------------------------------------------
|
|
L_657C:
|
|
lda #$FF ; 657C $FF = repair-menu mode...
|
|
sta selectedUnit ; 657E ...so the blinking highlight also blinks the comcen system marker
|
|
jsr runMenuLoop ; 6581 wait for the player to choose (returns the index in Y and in menuSelectedItem)
|
|
ldy menuSelectedItem ; 6584 0 ENERGY, 1 BATTLE, 2 RADAR, 3 DRONE, 4 (EXIT)
|
|
cpy #$04 ; 6587 the (EXIT) row
|
|
beq L_6599 ; 6589 leave the menu
|
|
jsr isPlayerInputAllowed ; 658B Z = 1 only when no film is playing and the game is not already over
|
|
bne L_6593 ; 658E repairs are locked out - just re-run the menu
|
|
jsr attemptSystemRepair ; 6590 one repair roll on the system in menuSelectedItem (1/2 for energy, 1/4 otherwise)
|
|
L_6593:
|
|
jsr unhighlightMenuRow ; 6593 un-highlight the row before the selector redraws it
|
|
jmp L_657C ; 6596 back into the menu
|
|
|
|
L_6599:
|
|
lda #$66 ; 6599 colour byte $66: both nibbles blue, i.e. wipe the window out
|
|
sta textColour ; 659B the clear below paints the whole window in it
|
|
jmp bootDriveDataBit ; 659D erase the menu window and return to repairTabAction (also textEngineC000:clearTextWindow)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runInGameOptionsMenu - Runs the MISC-tab options menu while a battle is in progress. The caller has
|
|
; already printed the items (ABORT GAME, VOICE PAUSE when a modem game is running, (EXIT)) and set
|
|
; menuItemCount / menuFirstRow. Every index other than 1 is handed back to the caller. Index 1 is
|
|
; VOICE PAUSE: with a carrier up it tells the opponent with command $8C reason 5 and then waits until
|
|
; voicePauseReconnect (called from the $C000 overlay at $C768) clears bit 7 of menuSelectedItem;
|
|
; without a carrier it just hangs up and redials itself. Afterwards the menu is restarted.
|
|
; In: menuItemCount ($91D4) and menuFirstRow ($91D6) set by the caller, filmPlaybackMode ($0B9B),
|
|
; isSoloTrainer ($0BA5), the comm module link flags at $E03C
|
|
; Out: Y = menuSelectedItem (0 = ABORT GAME, last = (EXIT)), selectedUnit = $FE, textColour = $60,
|
|
; command $8C/5 possibly queued, menu highlight removed
|
|
; Called from: miscTabAction at $1A78; tail-jumps to itself at $65E0 after a voice pause
|
|
; ----------------------------------------------------------------------
|
|
runInGameOptionsMenu:
|
|
lda #$FE ; 65A0 $FE = plain menu: the highlight does not track a unit or a comcen marker
|
|
sta selectedUnit ; 65A2 the menu selector reads this sentinel every frame
|
|
lda #$60 ; 65A5 colour byte $60 for the menu rows (%01 blue, %10 black)
|
|
sta textColour ; 65A7 colour of the item rows
|
|
jsr runMenuLoop ; 65A9 run the selector; returns the chosen index in Y
|
|
cpy #$01 ; 65AC index 1 = VOICE PAUSE
|
|
bcc L_65E3 ; 65AE index 0 = ABORT GAME - let miscTabAction deal with it
|
|
bne L_65E3 ; 65B0 index 2 = (EXIT) - likewise
|
|
lda filmPlaybackMode ; 65B2 film record/playback flag
|
|
bne L_65E3 ; 65B5 a film has no phone call to pause
|
|
lda isSoloTrainer ; 65B7 bit 7 set = the solo trainer is the opponent
|
|
bmi L_65E3 ; 65BA the trainer has no phone call either
|
|
bit linkStatus ; 65BC comm module link flags; bit 6 = carrier present (also trainerAiE000:linkStatus)
|
|
bvs L_65C7 ; 65BF carrier up - warn the opponent before hanging up
|
|
jsr voicePauseReconnect ; 65C1 no carrier: hang up and re-open the link straight away
|
|
jmp L_65DD ; 65C4 join the common tail: unhighlight and restart the menu
|
|
|
|
L_65C7:
|
|
lda #$05 ; 65C7 reason 5 of command $8C = 'hanging up for a voice call'
|
|
sta cmdParam0 ; 65C9 the single argument byte
|
|
lda #$8C ; 65CB command $8C = session control
|
|
jsr queueCmd2 ; 65CD queue the two bytes for the opponent
|
|
lda #$81 ; 65D0 bit 7 = 'waiting for the call to come back'
|
|
sta menuSelectedItem ; 65D2 menuSelectedItem doubles as the wait flag
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Spin here while the players talk on the phone. updateGameFrame keeps the battle, the message system
|
|
; and the link running; voicePauseReconnect clears bit 7 when the modems have connected again.
|
|
; ----------------------------------------------------------------------
|
|
L_65D5:
|
|
jsr updateGameFrame ; 65D5 one game frame (engine, messages, comm poll)
|
|
lda menuSelectedItem ; 65D8 voicePauseReconnect clears bit 7 when the link is back
|
|
bmi L_65D5 ; 65DB still waiting for the reconnect
|
|
L_65DD:
|
|
jsr unhighlightMenuRow ; 65DD take the highlight off the VOICE PAUSE row
|
|
jmp runInGameOptionsMenu ; 65E0 and put the whole options menu up again
|
|
|
|
L_65E3:
|
|
jsr unhighlightMenuRow ; 65E3 the caller redraws, so drop the highlight first
|
|
rts ; 65E6 Y still holds the chosen index
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForGroupRepositionsDone - Before a group screen edits a group, wait until the re-position orders
|
|
; already sent for that same group have been executed, so the editor works on settled positions. If
|
|
; the player gives up meanwhile (checkUiContinue = 0) the routine pops its own return address so the
|
|
; RTS lands in the caller's caller and the whole group operation is abandoned.
|
|
; In: selectedGroupKey ($9206), lastOrderedGroupKey ($929D), pendingGroupOrderCount ($929E)
|
|
; Out: nothing, but the return may unwind one stack level
|
|
; Called from: the GROUP screen at $621D and the group editor at $63B7
|
|
; ----------------------------------------------------------------------
|
|
waitForGroupRepositionsDone:
|
|
lda selectedGroupKey ; 65E7 the group we are about to work on
|
|
cmp lastOrderedGroupKey ; 65EA the group the last batch of re-position orders was for
|
|
bne L_65FE ; 65ED a different group - nothing of ours is in flight
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait while orders are still outstanding, keeping the real-time battle running.
|
|
; ----------------------------------------------------------------------
|
|
L_65EF:
|
|
lda pendingGroupOrderCount ; 65EF re-position orders queued but not yet executed
|
|
beq L_65FE ; 65F2 all settled - carry on
|
|
jsr updateGameFrame ; 65F4 run one frame of the battle and the link
|
|
jsr checkUiContinue ; 65F7 0 = game over, screen change or SPACE
|
|
bne L_65EF ; 65FA keep waiting
|
|
pla ; 65FC abandon: throw away the low byte of our return address...
|
|
pla ; 65FD ...and the high byte, so the RTS below returns to the caller's caller
|
|
L_65FE:
|
|
rts ; 65FE return - normally, or one level higher when the wait was abandoned
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runFormationOptionsMenu - The FORMATION sub-menu of the group options. Prints the FORMATION OPTIONS
|
|
; heading and the five items (SWEEP LEFT, SWEEP RIGHT, REVERSE, CUSTOM, (EXIT)) as one multi-line
|
|
; string, then runs the selector. Items 0-2 are rotations and are sent straight to both machines as
|
|
; command $A3 leader,mode; 3 (CUSTOM) and 4 ((EXIT)) are returned to the caller.
|
|
; In: selectedUnit ($90F8) = the unit the formation is rotated about, menuFirstRow ($91D6) = 15,
|
|
; already set by the group menu at $62A7
|
|
; Out: Y = menuSelectedItem, cmdParam1/cmdParam2 = leader and mode, command $A3 queued, item count 5
|
|
; Called from: the group options menu at $62FD
|
|
; ----------------------------------------------------------------------
|
|
runFormationOptionsMenu:
|
|
jsr clearInfoPanel ; 65FF select and clear the 14-column info panel (columns 25-38, rows 12-21)
|
|
jsr printFormationCentred ; 6602 'FORMATION' ($C9D2) centred on row 12
|
|
lda #$13 ; 6605 ' OPTIONS' CR CR ($CB13), low byte
|
|
ldy #$CB ; 6607 ...high byte
|
|
jsr printString ; 6609 heading plus a blank row, leaving the cursor on row 15
|
|
lda #$8C ; 660C the five item lines at $CA8C, separated by CR
|
|
ldy #$CA ; 660E ...high byte
|
|
jsr printString ; 6610 SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT) on rows 15-19
|
|
lda #$00 ; 6613 start at the top of the list
|
|
sta menuSelectedItem ; 6615 start the highlight on SWEEP LEFT
|
|
lda #$05 ; 6618 five rows to choose from
|
|
sta menuItemCount ; 661A five selectable rows
|
|
jsr runMenuLoop ; 661D wait for the choice
|
|
ldy menuSelectedItem ; 6620 0 SWEEP LEFT, 1 SWEEP RIGHT, 2 REVERSE, 3 CUSTOM, 4 (EXIT)
|
|
cpy #$03 ; 6623 indices 3 (CUSTOM) and 4 ((EXIT)) are not rotations
|
|
bcs L_6636 ; 6625 CUSTOM and (EXIT) are handled by the caller
|
|
sty cmdParam2 ; 6627 third command byte = rotation mode 0-2
|
|
lda selectedUnit ; 6629 the group leader / clicked unit
|
|
sta cmdParam1 ; 662C second command byte
|
|
lda #$A3 ; 662E command $A3 = rotate the leader's formation
|
|
jsr queueCmd3 ; 6630 queue code + two arguments
|
|
ldy menuSelectedItem ; 6633 hand the choice back in Y
|
|
L_6636:
|
|
rts ; 6636 Y = the chosen index
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; soundPlayerTick - The per-frame SID sound driver. It is skipped while startSound is still
|
|
; rebuilding the voice tables, writes the master volume and then advances the three voices. X holds
|
|
; voice*2 throughout the driver, because every per-voice value lives at a fixed offset plus voice*2
|
|
; inside the 48-byte block at sndVoiceState.
|
|
; In: sndInitLock ($67C5), sndVolume ($67C2), the voice state at $6792-$67C1, the script pointers
|
|
; in zero page $D8-$DD
|
|
; Out: SID registers $D400-$D418, updated voice state, sndVoiceSoundId cleared for finished voices
|
|
; Called from: the raster IRQ at $128C (which first dequeues a request with $C8E9 and, if there is
|
|
; one, calls startSound at $1289)
|
|
; ----------------------------------------------------------------------
|
|
soundPlayerTick:
|
|
lda sndInitLock ; 6637 non-zero while startSound is half way through filling the voice tables
|
|
beq L_663D ; 663A safe to run
|
|
rts ; 663C skip this tick rather than play a half-built voice
|
|
|
|
L_663D:
|
|
lda sndVolume ; 663D $0C while anything is sounding, 0 when the driver has been silenced
|
|
sta SID_VOLUME ; 6640 $D418 bits 0-3 = master volume (bits 4-6 are the filter mode, left at 0)
|
|
beq L_66BE ; 6643 volume 0 - nothing can be heard, so do no work
|
|
ldx #$00 ; 6645 X = voice*2: SID voice 1
|
|
jsr updateSoundVoice ; 6647 advance SID voice 1
|
|
ldx #$02 ; 664A SID voice 2
|
|
jsr updateSoundVoice ; 664C advance SID voice 2
|
|
ldx #$04 ; 664F SID voice 3 - fall through into updateSoundVoice, whose RTS returns for us
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateSoundVoice - Advances one SID voice by one frame. An idle voice (duration 0) is left alone.
|
|
; Otherwise the 16-bit frequency and pulse-width slides are added to the current values, the note
|
|
; duration is counted down and, when it expires, the voice's script is interpreted up to its next 'set
|
|
; duration' command; a script that ends without setting one releases the voice and frees its
|
|
; sndVoiceSoundId slot. Finally the whole voice image is written to the SID registers.
|
|
; In: X = voice*2 (0, 2 or 4), the voice state rows at $6792-$67C1
|
|
; Out: SID voice registers at $D400 + voice*7, updated state, X preserved
|
|
; Called from: soundPlayerTick at $6647 and $664C, and by falling through at $664F
|
|
; ----------------------------------------------------------------------
|
|
updateSoundVoice:
|
|
lda sndVoiceState,x ; 6651 duration counter of this voice ($6792 + voice*2)
|
|
beq L_66BE ; 6654 0 = the voice is idle, nothing to do
|
|
clc ; 6656 clear the carry for the 16-bit frequency add
|
|
lda sndVoiceFreqStepLo,x ; 6657 per-tick frequency slide, low byte
|
|
adc sndVoiceFreqLo,x ; 665A current oscillator frequency, low byte
|
|
sta sndVoiceFreqLo,x ; 665D new oscillator frequency, low byte
|
|
lda sndVoiceFreqStepHi,x ; 6660 slide high byte, with the carry from the low half
|
|
adc sndVoiceFreqHi,x ; 6663 current frequency high byte, plus the carry from the low half
|
|
sta sndVoiceFreqHi,x ; 6666 new oscillator frequency, high byte
|
|
clc ; 6669 clear the carry for the 16-bit pulse-width add
|
|
lda sndVoicePulseStepLo,x ; 666A per-tick pulse-width sweep, low byte
|
|
adc sndVoicePulseLo,x ; 666D current pulse width, low byte
|
|
sta sndVoicePulseLo,x ; 6670 new pulse width, low byte
|
|
lda sndVoicePulseStepHi,x ; 6673 sweep high byte plus carry
|
|
adc sndVoicePulseHi,x ; 6676 current pulse width high byte, plus carry
|
|
sta sndVoicePulseHi,x ; 6679 new pulse width, high byte
|
|
dec sndVoiceState,x ; 667C one tick off the remaining note duration
|
|
bne L_6691 ; 667F note still running - just refresh the SID registers
|
|
jsr runSoundScript ; 6681 duration expired: interpret the script up to its next duration command
|
|
lda sndVoiceState,x ; 6684 did the script set a new duration?
|
|
bne L_6691 ; 6687 yes - the voice carries on
|
|
txa ; 6689 no, the sound is over: turn voice*2...
|
|
lsr a ; 668A ...back into a plain voice number 0-2
|
|
tay ; 668B Y = voice number 0-2
|
|
lda #$00 ; 668C 0 = no sound playing on this voice
|
|
sta sndVoiceSoundId,y ; 668E mark the voice free so requestSound may hand it to another sound
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Write the voice image out to the SID. Y = 0, 7 or $0E, the register offset of this voice.
|
|
; ----------------------------------------------------------------------
|
|
L_6691:
|
|
ldy sidVoiceRegOffsetTable,x; 6691 $00 / $07 / $0E - distance of this voice's registers from $D400
|
|
lda sndVoiceFreqLo,x ; 6694 frequency low byte of this voice
|
|
sta SID_V1_FREQ_LO,y ; 6697 $D400 + voice*7 = frequency low
|
|
lda sndVoiceFreqHi,x ; 669A frequency high byte
|
|
sta SID_V1_FREQ_HI,y ; 669D $D401 + voice*7 = frequency high
|
|
lda sndVoicePulseLo,x ; 66A0 pulse width low byte
|
|
sta SID_V1_PW_LO,y ; 66A3 $D402 + voice*7 = pulse width low
|
|
lda sndVoicePulseHi,x ; 66A6 pulse width high byte
|
|
sta SID_V1_PW_HI,y ; 66A9 $D403 + voice*7 = pulse width high (only bits 0-3 of it matter)
|
|
lda sndVoiceAttackDecay,x ; 66AC attack/decay byte
|
|
sta SID_V1_AD,y ; 66AF $D405 + voice*7 = attack (high nibble) and decay (low nibble)
|
|
lda sndVoiceSustainRelease,x; 66B2 sustain/release byte
|
|
sta SID_V1_SR,y ; 66B5 $D406 + voice*7 = sustain level and release rate
|
|
lda sndVoiceCtrlByte,x ; 66B8 waveform + gate byte, e.g. $41 pulse, $21 sawtooth, $81 noise, gate off = bit 0 clear
|
|
sta SID_V1_CTRL,y ; 66BB $D404 + voice*7 - written last so the gate change lands after the new envelope
|
|
L_66BE:
|
|
rts ; 66BE shared exit of soundPlayerTick and updateSoundVoice
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Script byte $80-$FF: 'jitter the pitch'. Only bit 7 matters; the value of the byte is ignored. The
|
|
; current map/scenario random byte is added to the frequency low byte, which is what gives the shot
|
|
; and drone scripts their rasp. Note there is no CLC first, so the carry left by the BIT/ fetch
|
|
; sequence can add one extra - harmless for a noise effect. Reached only by the BMI at $66CD.
|
|
; ----------------------------------------------------------------------
|
|
L_66BF:
|
|
lda scenarioSeed0 ; 66BF byte 0 of the 24-bit scenario seed, used here as a cheap random value
|
|
adc sndVoiceFreqLo,x ; 66C1 add it to the oscillator frequency low byte
|
|
sta sndVoiceFreqLo,x ; 66C4 and fall straight back into the script interpreter
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runSoundScript - Interprets one voice's script bytecode until it sets a note duration (or ends).
|
|
; Byte formats:
|
|
; $80-$FF add the scenario random byte to the frequency low byte (handler at $66BF)
|
|
; $40-$7F loop/jump: bits 0-5 select a counter slot ($06 = loop counter A, $07 = counter B),
|
|
; followed by a 16-bit target address, low byte first. A counter of 0 jumps always;
|
|
; otherwise it is decremented and the jump is taken while it is still non-zero.
|
|
; $00-$0A store the following byte (two bytes for slots from $12 up) into the state slot named by
|
|
; sndCommandSlotTable: 0 duration (and stop here), 1 waveform/gate, 2 loop counter A,
|
|
; 3 loop counter B, 4/5 spare, 6 frequency, 7 pulse width, 8 AD/SR, 9 frequency slide,
|
|
; 10 pulse-width slide. A duration of 0 ends the sound.
|
|
; In: X = voice*2, the script pointer in zero page ($D8 + X), sndScriptByte
|
|
; Out: the voice state rows updated, the script pointer advanced, sndScratch clobbered
|
|
; Called from: updateSoundVoice at $6681, and re-entered from its own $66EB / $6710 / $6719
|
|
; ----------------------------------------------------------------------
|
|
runSoundScript:
|
|
jsr fetchSoundScriptByte ; 66C7 next script byte, also stored in sndScriptByte for the test below
|
|
bit sndScriptByte ; 66CA N = bit 7, V = bit 6, Z = 1 only when the byte is 0 (the 'set duration' command)
|
|
bmi L_66BF ; 66CD $80-$FF: jitter the frequency
|
|
bvs L_66EE ; 66CF $40-$7F: loop or jump
|
|
php ; 66D1 remember Z: command 0 must stop the interpreter after it has run
|
|
tay ; 66D2 Y = command number 0-10
|
|
txa ; 66D3 X = voice*2
|
|
clc ; 66D4 clear the carry before adding the slot offset
|
|
adc sndCommandSlotTable,y ; 66D5 plus the offset of that command's slot inside the 48-byte state block
|
|
tay ; 66D8 Y now indexes the exact byte to write
|
|
jsr fetchSoundScriptByte ; 66D9 operand byte
|
|
sta sndVoiceState,y ; 66DC store it in the slot
|
|
cpy #$12 ; 66DF slots from $12 on (frequency, pulse, AD/SR, the two slides) are 16-bit
|
|
bcc L_66EA ; 66E1 one-byte slot - done
|
|
iny ; 66E3 second byte of the pair, at slot+1
|
|
jsr fetchSoundScriptByte ; 66E4 high byte of a 16-bit parameter
|
|
sta sndVoiceState,y ; 66E7 stored one byte further on, in slot+1
|
|
L_66EA:
|
|
plp ; 66EA recover 'was the command 0?'
|
|
bne runSoundScript ; 66EB any other command - keep interpreting
|
|
rts ; 66ED command 0 set the note duration; stop until it runs out
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Loop/jump command $40-$7F. The two script bytes after it are the target address, low byte first;
|
|
; they are pushed and only stored into the pointer if the jump is taken.
|
|
; ----------------------------------------------------------------------
|
|
L_66EE:
|
|
and #$3F ; 66EE bits 0-5 = which counter slot ($06 = loop A, $07 = loop B)
|
|
sta sndScratch ; 66F0 keep the slot offset
|
|
jsr fetchSoundScriptByte ; 66F3 target address low byte
|
|
pha ; 66F6 push it
|
|
jsr fetchSoundScriptByte ; 66F7 target address high byte
|
|
pha ; 66FA push it as well (so it comes off the stack first)
|
|
clc ; 66FB clear the carry before indexing the counter
|
|
txa ; 66FC voice*2
|
|
adc sndScratch ; 66FD plus the counter slot
|
|
tay ; 6700 Y = this voice's copy of that counter
|
|
lda sndVoiceState,y ; 6701 this voice's copy of the selected loop counter
|
|
beq L_6713 ; 6704 a counter of 0 means 'jump unconditionally'
|
|
sec ; 6706 set the carry for the subtract
|
|
sbc #$01 ; 6707 count one repetition off
|
|
sta sndVoiceState,y ; 6709 write the decremented repeat count back
|
|
bne L_6713 ; 670C repetitions left - take the jump
|
|
pla ; 670E loop finished: drop the target high byte...
|
|
pla ; 670F ...and the low byte
|
|
jmp runSoundScript ; 6710 carry on with the byte after the jump
|
|
|
|
L_6713:
|
|
pla ; 6713 top of stack is the high byte pushed at $66FA
|
|
sta soundScriptPtrHi,x ; 6714 script pointer high byte for this voice ($D9 + voice*2)
|
|
pla ; 6716 then the low byte
|
|
sta soundScriptPtr,x ; 6717 script pointer low byte ($D8 + voice*2)
|
|
jmp runSoundScript ; 6719 continue interpreting at the jump target
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; fetchSoundScriptByte - Reads the next byte of a voice's script through the zero-page pointer at
|
|
; $D8 + voice*2, keeps a copy in sndScriptByte for the BIT test in runSoundScript, and advances the
|
|
; pointer.
|
|
; In: X = voice*2, the 16-bit pointer at zero page $D8/$D9 + X
|
|
; Out: A = the byte, sndScriptByte = the byte, pointer incremented
|
|
; Called from: runSoundScript at $66C7, $66D9, $66E4, $66F3 and $66F7
|
|
; ----------------------------------------------------------------------
|
|
fetchSoundScriptByte:
|
|
lda (soundScriptPtr,x) ; 671C (zp,X) with X = voice*2 picks this voice's script pointer
|
|
sta sndScriptByte ; 671E kept so runSoundScript can BIT it without disturbing A
|
|
inc soundScriptPtr,x ; 6721 advance the pointer low byte
|
|
bne L_6727 ; 6723 no page crossing
|
|
inc soundScriptPtrHi,x ; 6725 carry into the high byte
|
|
L_6727:
|
|
rts ; 6727 A holds the byte, the pointer now points past it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; startSound - Starts sound A (0-28) on the voices its table row names. Each row of soundPointerTable
|
|
; holds three script addresses, one per voice; a zero entry means this sound does not use that voice,
|
|
; which is then left playing whatever it had. For every voice it does use, the script pointer is
|
|
; loaded, the voice's eight state slots are zeroed, the sound id is recorded and the duration is set
|
|
; to 2 so the script is first interpreted on the next-but-one driver tick.
|
|
; In: A = sound id 0-28, soundPointerTable at $6831
|
|
; Out: sndVolume, sndCurrentSoundId, sndScratch, sndInitLock, sndStartVoiceIndex, sndVoiceSoundId,
|
|
; the zero-page script pointers $D8-$DD and the voice state; X and Y clobbered
|
|
; Called from: the raster IRQ at $1289 with the id dequeued by $C8E9
|
|
; ----------------------------------------------------------------------
|
|
startSound:
|
|
ldy #$03 ; 6728 3 = the number of voices, used as the loop counter...
|
|
sty sndInitLock ; 672A ...and, while it is non-zero, as the 'driver busy' lock read by soundPlayerTick
|
|
ldy #$0C ; 672D volume 12 of 15
|
|
sty sndVolume ; 672F the driver writes this to $D418 every tick
|
|
sta sndCurrentSoundId ; 6732 remember the id; it is copied into every voice slot this sound takes
|
|
asl a ; 6735 id*2
|
|
sta sndScratch ; 6736 keep id*2 so it can be added to id*4 below
|
|
asl a ; 6739 id*4
|
|
clc ; 673A clear the carry for the id*4 + id*2 sum
|
|
adc sndScratch ; 673B id*4 + id*2 = id*6, the row stride of soundPointerTable
|
|
tax ; 673E X = byte offset of this sound's row
|
|
ldy #$00 ; 673F Y = voice*2, starting at voice 0
|
|
sty sndStartVoiceIndex ; 6741 plain voice number 0-2, kept in parallel with Y
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Once per voice: take the script address from the table row, or skip the voice if it is 0.
|
|
; ----------------------------------------------------------------------
|
|
L_6744:
|
|
lda soundPointerTableHi,x ; 6744 high byte of this voice's script address (soundPointerTable+1, indexed by X)
|
|
beq L_6775 ; 6747 0 = this sound does not use this voice; leave it playing
|
|
sta a:soundScriptPtrHi,y ; 6749 absolute,Y because there is no STA zero page,Y - writes $D9 + voice*2
|
|
lda soundPointerTable,x ; 674C low byte of the script address
|
|
sta a:soundScriptPtr,y ; 674F writes $D8 + voice*2
|
|
tya ; 6752 voice*2 for this voice
|
|
pha ; 6753 save voice*2 across the clear loop, which walks Y through the whole block
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Clear this voice's two bytes in each of the eight 6-byte rows of the state block, i.e. duration,
|
|
; waveform, both loop counters, the spares, frequency, pulse, AD/SR and the two slides. Y starts at
|
|
; voice*2 and steps by 6 until it passes the end of the 48-byte block.
|
|
; ----------------------------------------------------------------------
|
|
L_6754:
|
|
lda #$00 ; 6754 the value written into every slot
|
|
sta sndVoiceState,y ; 6756 first byte of the row for this voice
|
|
sta sndVoiceCtrlByte,y ; 6759 second byte of the row for this voice
|
|
tya ; 675C current offset inside the state block
|
|
clc ; 675D clear the carry for the +6 row step
|
|
adc #$06 ; 675E next 6-byte row
|
|
tay ; 6760 Y = the same voice's slot in the next row
|
|
cmp #$30 ; 6761 $30 = 48 bytes = eight rows: stop once Y has walked past the block
|
|
bcc L_6754 ; 6763 keep clearing while Y is still inside the 48-byte block
|
|
ldy sndStartVoiceIndex ; 6765 voice number 0-2
|
|
lda sndCurrentSoundId ; 6768 the sound now playing
|
|
sta sndVoiceSoundId,y ; 676B record it so requestSound can weigh priorities against it
|
|
pla ; 676E get voice*2 back
|
|
tay ; 676F voice*2 back in Y
|
|
lda #$02 ; 6770 duration 2 ticks...
|
|
sta sndVoiceState,y ; 6772 ...so the script is first run on the second driver tick, not this one
|
|
L_6775:
|
|
inx ; 6775 next 16-bit entry of the table row
|
|
inx ; 6776 table entries are 16-bit, so step X by two
|
|
iny ; 6777 next voice*2
|
|
iny ; 6778 and Y by two, to the next voice's slot
|
|
inc sndStartVoiceIndex ; 6779 next voice number
|
|
dec sndInitLock ; 677C 3, 2, 1, 0 - and reaching 0 also releases soundPlayerTick again
|
|
bne L_6744 ; 677F three voices per sound
|
|
rts ; 6781 every voice this sound uses is now armed
|
|
|
|
|
|
; sidVoiceRegOffsetTable - 5 bytes, indexed by voice*2 (0, 2, 4); the two odd bytes are padding. Value
|
|
; = distance of that voice's SID registers from $D400: $00, $07, $0E. Read at $6691.
|
|
sidVoiceRegOffsetTable:
|
|
.byte $00 ; 6782 voice 0 -> $D400, (pad), voice 1 -> $D407, (pad), voice 2 -> $D40E
|
|
.byte $00,$07,$00,$0E ; 6783 ....
|
|
|
|
; sndCommandSlotTable - 11 bytes, indexed by script command 0-10. Value = the offset of that
|
|
; command's state slot from sndVoiceState ($6792); the driver adds voice*2 to it. Slots from $12 up
|
|
; hold 16-bit values and take two operand bytes. Read at $66D5.
|
|
sndCommandSlotTable:
|
|
.byte $00 ; 6787 0 duration, 1 waveform/gate, 2 loop A, 3 loop B, 4/5 spare, 6 frequency, 7 pulse width
|
|
.byte $01,$06,$07,$0C,$0D,$12,$18; 6788 .......
|
|
.byte $1E ; 678F 8 AD/SR, 9 frequency slide, 10 pulse-width slide
|
|
.byte $24,$2A ; 6790 $*
|
|
|
|
; sndVoiceState - 48 bytes of work RAM: eight 6-byte rows, each row holding one parameter for all
|
|
; three voices at row + voice*2. The driver indexes everything from here.
|
|
; $6792 row 0: +0 note duration in ticks, +1 waveform/gate byte
|
|
; $6798 row 1: +0 loop counter A, +1 loop counter B
|
|
; $679E row 2: +0/+1 two spare slots (script commands 4 and 5, never used by the shipped scripts)
|
|
; $67A4 row 3: 16-bit oscillator frequency
|
|
; $67AA row 4: 16-bit pulse width
|
|
; $67B0 row 5: +0 attack/decay, +1 sustain/release
|
|
; $67B6 row 6: 16-bit frequency slide added every tick
|
|
; $67BC row 7: 16-bit pulse-width sweep added every tick
|
|
; startSound zeroes one voice's two bytes in every row; the whole block is RAM, the $00s below are
|
|
; only the state the loader happens to leave behind.
|
|
sndVoiceState:
|
|
.byte $00 ; 6792 note duration of voice 0 (voices 1 and 2 at $6794 and $6796)
|
|
|
|
; Waveform/gate byte written to $D404 + voice*7: $11 triangle, $21 sawtooth, $41 pulse,
|
|
; $81 noise, each with the gate in bit 0; clearing bit 0 ($00/$20/$80) starts the release.
|
|
sndVoiceCtrlByte:
|
|
.byte $00 ; 6793 waveform/gate byte of voice 0 ($6795 and $6797 for voices 1 and 2)
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; 6794 ........
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; 679C ........
|
|
|
|
; 16-bit oscillator frequency, voice 0 at $67A4/$67A5, voice 1 at $67A6/$67A7, voice 2 at
|
|
; $67A8/$67A9. Written to $D400/$D401 + voice*7 every tick.
|
|
sndVoiceFreqLo:
|
|
.byte $00 ; 67A4 oscillator frequency of voice 0, low byte
|
|
sndVoiceFreqHi:
|
|
.byte $00 ; 67A5 oscillator frequency of voice 0, high byte (then voices 1 and 2)
|
|
.byte $00,$00,$00,$00 ; 67A6 ....
|
|
|
|
; 16-bit pulse width -> $D402/$D403 + voice*7 (only the low 12 bits are used by the SID).
|
|
sndVoicePulseLo:
|
|
.byte $00 ; 67AA pulse width of voice 0, low byte
|
|
sndVoicePulseHi:
|
|
.byte $00 ; 67AB pulse width of voice 0, high byte (then voices 1 and 2)
|
|
.byte $00,$00,$00,$00 ; 67AC ....
|
|
|
|
; Envelope: attack/decay -> $D405 + voice*7, sustain/release -> $D406 + voice*7.
|
|
sndVoiceAttackDecay:
|
|
.byte $00 ; 67B0 attack/decay of voice 0
|
|
sndVoiceSustainRelease:
|
|
.byte $00 ; 67B1 sustain/release of voice 0 (then voices 1 and 2)
|
|
.byte $00,$00,$00,$00 ; 67B2 ....
|
|
|
|
; 16-bit signed frequency slide added to the frequency once per tick (the pitch bends).
|
|
sndVoiceFreqStepLo:
|
|
.byte $00 ; 67B6 per-tick frequency slide of voice 0, low byte
|
|
sndVoiceFreqStepHi:
|
|
.byte $00 ; 67B7 per-tick frequency slide of voice 0, high byte (then voices 1 and 2)
|
|
.byte $00,$00,$00,$00 ; 67B8 ....
|
|
|
|
; 16-bit signed pulse-width sweep added to the pulse width once per tick.
|
|
sndVoicePulseStepLo:
|
|
.byte $00 ; 67BC per-tick pulse-width sweep of voice 0, low byte
|
|
sndVoicePulseStepHi:
|
|
.byte $00 ; 67BD per-tick pulse-width sweep of voice 0, high byte (then voices 1 and 2)
|
|
.byte $00,$00,$00,$00 ; 67BE ....
|
|
|
|
; Sound driver work RAM, six separate bytes:
|
|
; $67C2 sndVolume master volume written to $D418 every tick ($0C playing, 0 silent)
|
|
; $67C3 sndScriptByte the script byte just fetched, kept for the BIT test at $66CA
|
|
; $67C4 sndScratch id*2 in startSound, loop slot number in runSoundScript
|
|
; $67C5 sndInitLock non-zero while startSound rebuilds the tables; also its voice counter
|
|
; $67C6 sndCurrentSoundId the id startSound is installing
|
|
; $67C7 sndStartVoiceIndex the plain voice number 0-2 startSound is working on
|
|
sndVolume:
|
|
.byte $00 ; 67C2 master volume written to $D418 every tick
|
|
sndScriptByte:
|
|
.byte $00 ; 67C3 the script byte last fetched
|
|
sndScratch:
|
|
.byte $00 ; 67C4 scratch: id*2 in startSound, loop slot in runSoundScript
|
|
sndInitLock:
|
|
.byte $00 ; 67C5 non-zero = startSound is busy, soundPlayerTick must not run
|
|
sndCurrentSoundId:
|
|
.byte $00 ; 67C6 the sound id startSound is installing
|
|
sndStartVoiceIndex:
|
|
.byte $00 ; 67C7 voice number 0-2 startSound is working on
|
|
|
|
; sndVoiceSoundId - the sound id currently playing on each of the three voices, 0 = free. The driver
|
|
; clears an entry when that voice's script ends; requestSound in the $C000 overlay ($C87D priority
|
|
; classes) reads it to decide whether a new sound may take a busy voice. $67CA (voice 2) is read on
|
|
; its own at $3F07 and $C909/$C913 to see whether the comcen engine sound is running.
|
|
sndVoiceSoundId:
|
|
.byte $00 ; 67C8 voice 0 and voice 1
|
|
.byte $00 ; 67C9 .
|
|
|
|
; Voice 2 - the drone/comcen engine voice; tested by name elsewhere.
|
|
sndVoice2SoundId:
|
|
.byte $00 ; 67CA sound id playing on voice 2 (0 = free)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; unpackRleTo0200 - Run-length decoder that always unpacks into the two pages $0200-$03FF. The first
|
|
; byte of the source block is the escape marker; in the stream 'marker, count, value' emits value
|
|
; count times and any other byte is a literal. The source pointer is kept as the self-modified
|
|
; operand of fetchRleByte.
|
|
; In: A/Y = address of the packed block
|
|
; Out: $0200-$03FF filled, rleMarkerByte ($9246) = the marker, rleUnpackFlag ($9247) = 0,
|
|
; glyphPtr/glyphPtrHi, scratch1A and scratch1C clobbered, fetchRleByte's operand left past the
|
|
; end of the block
|
|
; Called from: overlay B at $7596 (block at $FD70) and $7FF0 (block at $FE24), and the $C000 overlay
|
|
; at $CD10 for the five unit pictures at $CD52/$CDD3/$CE3B/$CECD/$CF3A; each caller then copies
|
|
; the unpacked page somewhere else
|
|
; ----------------------------------------------------------------------
|
|
unpackRleTo0200:
|
|
sta L_67D6+1 ; 67CB self-modify: low byte of the LDX operand at $67D6, which reads the marker
|
|
sty L_67D6+2 ; 67CE and its high byte
|
|
ldx #$00 ; 67D1 X = 0 for the flag store below
|
|
stx rleUnpackFlag ; 67D3 $9247 = 0 (the overlay-B bitmap unpacker keeps a state flag here)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The operand of this LDX was patched two instructions ago. The $FFFF the disassembly shows is simply
|
|
; what the loader left in the file - the instruction is never executed unpatched.
|
|
; ----------------------------------------------------------------------
|
|
L_67D6:
|
|
ldx irqVectorHi ; 67D6 first byte of the block = the escape marker (operand patched at $67CB/$67CE)
|
|
stx rleMarkerByte ; 67D9 keep the marker; the overlay-B unpacker at $865E compares against the same byte
|
|
tax ; 67DC A still holds the source low byte
|
|
inx ; 67DD step past the marker byte
|
|
bne L_67E1 ; 67DE no page crossing
|
|
iny ; 67E0 carry into the source high byte
|
|
L_67E1:
|
|
stx fetchRleByte+1 ; 67E1 point fetchRleByte at the first packed byte (self-modified operand $681B)
|
|
sty fetchRleByte+2 ; 67E4 and its high byte ($681C)
|
|
lda #$00 ; 67E7 destination low byte
|
|
sta glyphPtr ; 67E9 destination pointer = $0200
|
|
lda #$02 ; 67EB destination page $02
|
|
sta glyphPtrHi ; 67ED the unpacked data always lands at $0200
|
|
lda #$02 ; 67EF two pages of output
|
|
sta scratch1A ; 67F1 two pages to fill: $0200-$03FF
|
|
ldy #$00 ; 67F3 offset inside the current page
|
|
ldx #$01 ; 67F5 run counter: one byte still owed before the next stream byte is read
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Emit one byte per pass. X counts down the bytes still owed by the current run; when it reaches 0
|
|
; the next stream byte is fetched, which is either a literal (run of 1) or the escape marker followed
|
|
; by a count and a value.
|
|
; ----------------------------------------------------------------------
|
|
L_67F7:
|
|
dex ; 67F7 one byte of the current run consumed
|
|
bne L_680E ; 67F8 still repeating - store the same value again
|
|
jsr fetchRleByte ; 67FA next byte of the packed stream
|
|
ldx #$01 ; 67FD assume a literal: a run of exactly one
|
|
cmp rleMarkerByte ; 67FF is it the escape marker?
|
|
bne L_680E ; 6802 no - store it as it stands
|
|
jsr fetchRleByte ; 6804 escape: the repeat count comes next
|
|
sta scratch1C ; 6807 remember how many copies to emit
|
|
jsr fetchRleByte ; 6809 and then the byte to repeat
|
|
ldx scratch1C ; 680C X = how many copies to emit
|
|
L_680E:
|
|
sta (glyphPtr),y ; 680E store into $0200 + page*256 + Y
|
|
iny ; 6810 next byte of the current page
|
|
bne L_67F7 ; 6811 until the page is full
|
|
inc glyphPtrHi ; 6813 next destination page
|
|
dec scratch1A ; 6815 one page finished
|
|
bne L_67F7 ; 6817 two pages done?
|
|
rts ; 6819 $0200-$03FF now hold the unpacked block
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; fetchRleByte - Returns the next byte of the packed stream and advances the pointer. The pointer is
|
|
; the absolute operand of the LDA below ($681B/$681C), patched by whoever starts an unpack.
|
|
; In: the self-modified operand at $681B/$681C
|
|
; Out: A = X = the byte, the operand incremented by one
|
|
; Called from: unpackRleTo0200 at $67FA/$6804/$6809, and from overlay B's bitmap unpacker at
|
|
; $8659/$8663/$8668, which sets $681B itself at $8644 and shares rleMarkerByte
|
|
; ----------------------------------------------------------------------
|
|
fetchRleByte:
|
|
lda irqVectorHi ; 681A read through the self-modified pointer; the $FFFF shown is only the loader's filler
|
|
tax ; 681D keep a copy - the routine hands the byte back in X as well
|
|
lda #$01 ; 681E the pointer is advanced by one byte
|
|
clc ; 6820 clear the carry for the 16-bit increment
|
|
adc fetchRleByte+1 ; 6821 advance the stream pointer low byte
|
|
sta fetchRleByte+1 ; 6824 the operand of the LDA above is the pointer itself
|
|
lda #$00 ; 6827 only the carry has to reach the high byte
|
|
adc fetchRleByte+2 ; 6829 and its high byte
|
|
sta fetchRleByte+2 ; 682C high byte of the advanced stream pointer
|
|
txa ; 682F return the byte in A
|
|
rts ; 6830 byte returned in A (and in X)
|
|
|
|
|
|
; soundPointerTable - 29 rows of 3 words, 6 bytes per row, indexed by sound id * 6. Word n is the
|
|
; script address for SID voice n, or $0000 when the sound leaves that voice alone. startSound reads
|
|
; the high byte through soundPointerTableHi (= this label + 1) and the low byte through this label.
|
|
; Sound 0 points all three voices at sndScriptSilence, so 'play sound 0' stops everything; ids $1A,
|
|
; $1B and $1C do the same for one voice each. The names below come from the call sites of each id:
|
|
; $01 button click, $02 menu/typing click, $03 error beep, $04 alert chime, $05 incoming chat
|
|
; character, $06 phone prompt, $07 drone alert, $08 comcen hit, $09 unit destroyed, $0A no known
|
|
; caller, $0B boomer shot, $0C missile blast, $0D win fanfare, $0E loss fanfare, $0F drone launch,
|
|
; $10 drone turn, $11 drone strike, $12 repair failed, $13 repair succeeded, $14 comcen engine loop,
|
|
; $15 phone trouble, $16 own comcen moving, $17 damage/radar sweep, $18 attempting repair, $19 game
|
|
; over / warning chord.
|
|
soundPointerTable:
|
|
soundPointerTableHi = soundPointerTable+1
|
|
.addr sndScriptSilence ; 6831 sound $00 = stop all three voices
|
|
.addr sndScriptSilence ; 6833
|
|
.addr sndScriptSilence ; 6835
|
|
.addr sndScriptButtonClick ; 6837
|
|
.addr CPU_DDR ; 6839
|
|
.addr CPU_DDR ; 683B
|
|
.addr sndScriptMenuClick ; 683D
|
|
.addr CPU_DDR ; 683F
|
|
.addr CPU_DDR ; 6841
|
|
.addr sndScriptErrorBeep ; 6843
|
|
.addr CPU_DDR ; 6845
|
|
.addr CPU_DDR ; 6847
|
|
.addr sndScriptAlertChime ; 6849
|
|
.addr CPU_DDR ; 684B
|
|
.addr CPU_DDR ; 684D
|
|
.addr sndScriptChatKeyClick ; 684F
|
|
.addr CPU_DDR ; 6851
|
|
.addr CPU_DDR ; 6853
|
|
.addr sndScriptPhonePromptV0 ; 6855
|
|
.addr sndScriptPhonePromptV1 ; 6857
|
|
.addr sndScriptSilence ; 6859
|
|
.addr sndScriptDroneAlert ; 685B
|
|
.addr CPU_DDR ; 685D
|
|
.addr CPU_DDR ; 685F
|
|
.addr CPU_DDR ; 6861
|
|
.addr sndScriptComcenHit ; 6863
|
|
.addr CPU_DDR ; 6865
|
|
.addr CPU_DDR ; 6867
|
|
.addr sndScriptUnitDestroyed ; 6869
|
|
.addr CPU_DDR ; 686B
|
|
.addr CPU_DDR ; 686D
|
|
.addr sndScriptUnusedShot ; 686F
|
|
.addr CPU_DDR ; 6871
|
|
.addr CPU_DDR ; 6873
|
|
.addr sndScriptBoomerShot ; 6875
|
|
.addr CPU_DDR ; 6877
|
|
.addr sndScriptMissileBlast ; 6879
|
|
.addr CPU_DDR ; 687B
|
|
.addr CPU_DDR ; 687D
|
|
.addr sndScriptWinFanfareV0 ; 687F
|
|
.addr sndScriptWinFanfareV1 ; 6881
|
|
.addr sndScriptWinFanfareV2 ; 6883
|
|
.addr sndScriptLossFanfareV0 ; 6885
|
|
.addr sndScriptLossFanfareV1 ; 6887
|
|
.addr sndScriptLossFanfareV2 ; 6889
|
|
.addr CPU_DDR ; 688B
|
|
.addr CPU_DDR ; 688D
|
|
.addr sndScriptDroneLaunch ; 688F
|
|
.addr CPU_DDR ; 6891
|
|
.addr CPU_DDR ; 6893
|
|
.addr sndScriptDroneTurn ; 6895
|
|
.addr CPU_DDR ; 6897
|
|
.addr CPU_DDR ; 6899
|
|
.addr sndScriptDroneStrike ; 689B
|
|
.addr CPU_DDR ; 689D
|
|
.addr CPU_DDR ; 689F
|
|
.addr sndScriptRepairFail ; 68A1
|
|
.addr CPU_DDR ; 68A3
|
|
.addr CPU_DDR ; 68A5
|
|
.addr sndScriptRepairOk ; 68A7
|
|
.addr CPU_DDR ; 68A9
|
|
.addr CPU_DDR ; 68AB
|
|
.addr sndScriptComcenEngine ; 68AD
|
|
.addr sndScriptPhoneTroubleV0 ; 68AF
|
|
.addr sndScriptPhoneTroubleV1 ; 68B1
|
|
.addr sndScriptSilence ; 68B3
|
|
.addr CPU_DDR ; 68B5
|
|
.addr CPU_DDR ; 68B7
|
|
.addr sndScriptOwnComcenMove ; 68B9
|
|
.addr CPU_DDR ; 68BB
|
|
.addr CPU_DDR ; 68BD
|
|
.addr sndScriptDamageSweep ; 68BF
|
|
.addr CPU_DDR ; 68C1
|
|
.addr CPU_DDR ; 68C3
|
|
.addr sndScriptRepairAttempt ; 68C5
|
|
.addr sndScriptGameOverV0 ; 68C7
|
|
.addr sndScriptGameOverV1 ; 68C9
|
|
.addr sndScriptGameOverV2 ; 68CB
|
|
.addr sndScriptSilence ; 68CD
|
|
.addr CPU_DDR ; 68CF
|
|
.addr CPU_DDR ; 68D1
|
|
.addr CPU_DDR ; 68D3
|
|
.addr sndScriptSilence ; 68D5
|
|
.addr CPU_DDR ; 68D7
|
|
.addr CPU_DDR ; 68D9
|
|
.addr CPU_DDR ; 68DB
|
|
.addr sndScriptSilence ; 68DD
|
|
|
|
; Sound script data - the 854 bytes $68DF-$6C34 are sound-driver bytecode interpreted by
|
|
; runSoundScript. The scripts are packed back to back; the labels through to $6C1B are exactly the
|
|
; entry points soundPointerTable names, plus the shared end-of-sound tail at $68E3. Several scripts
|
|
; jump into the middle of another one ($6ACE, $6B09, $6B24, $6C1E) to share a tail or a loop body,
|
|
; which is why the pieces below are not independent.
|
|
;
|
|
; sndScriptSilence: waveform 0 (gate off) then a 60-tick rest. Used by sound $00 on all three voices
|
|
; to stop everything, as the unused third voice of sounds $06 and $15, and by the single-voice stop
|
|
; sounds $1A, $1B and $1C.
|
|
sndScriptSilence:
|
|
.byte $01,$00,$00,$3C ; 68DF ...<
|
|
|
|
; Waveform 0, duration 0. A duration of 0 makes updateSoundVoice free the voice, so this is the
|
|
; common 'end of sound' tail every '46 E3 68' jump in the scripts points at.
|
|
sndScriptEnd:
|
|
.byte $01,$00,$00,$00 ; 68E3 ....
|
|
|
|
; Sound $15 (PHONE TROUBLE) voice 0: one tone, then an unconditional jump into the voice 1 script at
|
|
; $68F6 so both voices run the same alternating body.
|
|
sndScriptPhoneTroubleV0:
|
|
.byte $06,$5D,$0F,$08,$00,$30,$46,$F6; 68E7 .]...0F.
|
|
.byte $68 ; 68EF h
|
|
|
|
; Sound $15 voice 1: two-tone alarm looping for ever at $68F9 (only stopped by sound $00).
|
|
sndScriptPhoneTroubleV1:
|
|
.byte $06,$54,$14,$08,$00,$10,$07,$00; 68F0 .T......
|
|
.byte $01,$01,$41,$00,$19,$01,$00,$00; 68F8 ..A.....
|
|
.byte $19,$46,$F9,$68 ; 6900 .F.h
|
|
|
|
; Sound $01, voice 0: the fire-button click played by checkFirePressed.
|
|
sndScriptButtonClick:
|
|
.byte $08,$00,$80,$06,$00,$62,$07,$00; 6904 .....b..
|
|
.byte $08,$01,$41,$00,$01,$46,$E3,$68; 690C ..A..F.h
|
|
|
|
; Sound $03, voice 0: the error beep (TOO FAR!, rejected key, disk failure).
|
|
sndScriptErrorBeep:
|
|
.byte $08,$08,$00,$06,$00,$29,$07,$00; 6914 .....)..
|
|
.byte $08,$01,$41,$00,$0F,$46,$E3,$68; 691C ..A..F.h
|
|
|
|
; Sound $02, voice 0: the short click when a menu selection moves or a chat key is taken.
|
|
sndScriptMenuClick:
|
|
.byte $06,$00,$18,$07,$00,$08,$01,$41; 6924 .......A
|
|
.byte $00,$01,$46,$E3,$68 ; 692C ..F.h
|
|
|
|
; Sound $04, voice 0: the chime that goes with the alert messages (SPOTTED..., STUNNED...).
|
|
sndScriptAlertChime:
|
|
.byte $08,$00,$60,$06,$C6,$70,$07,$00; 6931 ..`..p..
|
|
.byte $08,$01,$41,$00,$0F,$46,$E3,$68; 6939 ..A..F.h
|
|
|
|
; Sound $05, voice 0: the click for each character received from the opponent's keyboard.
|
|
sndScriptChatKeyClick:
|
|
.byte $08,$00,$50,$06,$64,$32,$02,$03; 6941 ..P.d2..
|
|
.byte $01,$21,$00,$04,$01,$00,$00,$02; 6949 .!......
|
|
.byte $46,$49,$69,$00,$00 ; 6951 FIi..
|
|
|
|
; Sound $0C, voice 0: the comcen missile explosion (a falling sawtooth, freq slide $0095).
|
|
sndScriptMissileBlast:
|
|
.byte $08,$00,$80,$01,$21,$06,$60,$16; 6956 ....!.`.
|
|
.byte $09,$95,$00,$00,$1E,$01,$00,$00; 695E ........
|
|
.byte $1E,$00,$00 ; 6966 ...
|
|
|
|
; Sound $07, voice 0: two-note warning that the opponent has launched a drone.
|
|
sndScriptDroneAlert:
|
|
.byte $01,$21,$08,$00,$60,$02,$03,$06; 6969 .!..`...
|
|
.byte $9C,$1A,$00,$14,$06,$4A,$12,$00; 6971 .....J..
|
|
.byte $14,$46,$70,$69,$46,$E3,$68; 6979 .FpiF.h
|
|
|
|
; Sound $06 voice 0: the two-voice attention chord behind the 'PICK UP PHONE' / 'HANG UP PHONE'
|
|
; messages (message ids $17 and $18 in messageSoundTable).
|
|
sndScriptPhonePromptV0:
|
|
.byte $08,$00,$20,$07,$00,$08,$02,$02; 6980 .. .....
|
|
.byte $06,$82,$54,$01,$41,$00,$04,$01; 6988 ..T.A...
|
|
.byte $00,$00,$03,$46,$88,$69,$06,$45; 6990 ...F.i.E
|
|
.byte $4B,$01,$41,$00,$04,$01,$00,$00; 6998 K.A.....
|
|
.byte $03,$02,$02,$06,$BF,$5D,$01,$41; 69A0 .....].A
|
|
.byte $00,$04,$01,$00,$00,$03,$46,$A3; 69A8 ......F.
|
|
.byte $69,$06,$82,$54,$01,$41,$00,$04; 69B0 i..T.A..
|
|
.byte $01,$00,$00,$03,$06,$BF,$5D,$01; 69B8 ......].
|
|
.byte $41,$00,$04,$01,$00,$00,$03,$00; 69C0 A.......
|
|
.byte $00 ; 69C8 .
|
|
|
|
; Sound $06 voice 1: the second part of the same chord.
|
|
sndScriptPhonePromptV1:
|
|
.byte $08,$00,$20,$07,$00,$08,$02,$02; 69C9 .. .....
|
|
.byte $06,$C1,$2C,$01,$41,$00,$04,$01; 69D1 ..,.A...
|
|
.byte $00,$00,$03,$46,$D1,$69,$06,$BD; 69D9 ...F.i..
|
|
.byte $36,$01,$41,$00,$04,$01,$00,$00; 69E1 6.A.....
|
|
.byte $03,$06,$BF,$31,$01,$41,$00,$04; 69E9 ...1.A..
|
|
.byte $01,$00,$00,$03,$06,$C1,$2C,$01; 69F1 ......,.
|
|
.byte $41,$00,$04,$01,$00,$00,$03,$06; 69F9 A.......
|
|
.byte $BB,$3B,$01,$41,$00,$04,$01,$00; 6A01 .;.A....
|
|
.byte $00,$03,$06,$C1,$2C,$01,$41,$00; 6A09 ....,.A.
|
|
.byte $04,$01,$00,$00,$03,$00,$00; 6A11 .......
|
|
|
|
; Sound $08, voice 1: noise burst played when a comcen is hit.
|
|
sndScriptComcenHit:
|
|
.byte $01,$81,$08,$00,$FB,$02,$0E,$06; 6A18 ........
|
|
.byte $5A,$02,$00,$03,$01,$80,$06,$90; 6A20 Z.......
|
|
.byte $01,$00,$03,$46,$1F,$6A,$00,$00; 6A28 ...F.j..
|
|
|
|
; Sound $09, voice 1: the noise-into-tone whoosh when a unit is destroyed.
|
|
sndScriptUnitDestroyed:
|
|
.byte $01,$81,$08,$06,$9B,$06,$BC,$02; 6A30 ........
|
|
.byte $00,$04,$01,$80,$09,$12,$00,$02; 6A38 ........
|
|
.byte $08,$06,$E8,$03,$00,$08,$46,$41; 6A40 ......FA
|
|
.byte $6A,$46,$E3,$68 ; 6A48 jF.h
|
|
|
|
; Sound $0A, voice 1. No call site for id $0A was found anywhere in the game, so this script looks
|
|
; like a leftover shot effect; it is the only script that uses the bit-7 'jitter' command.
|
|
sndScriptUnusedShot:
|
|
.byte $01,$21,$08,$00,$38,$06,$5C,$12; 6A4C .!..8.\.
|
|
.byte $80,$02,$04,$09,$5C,$12,$00,$01; 6A54 ....\...
|
|
.byte $09,$A4,$ED,$00,$01,$46,$57,$6A; 6A5C .....FWj
|
|
.byte $09,$5C,$12,$00,$01,$01,$20,$09; 6A64 .\.... .
|
|
.byte $D4,$FE,$00,$14,$46,$E3,$68; 6A6C ....F.h
|
|
|
|
; Sound $0B, voice 1: the boomer's shot (noise with a downward slide).
|
|
sndScriptBoomerShot:
|
|
.byte $01,$81,$08,$07,$00,$06,$84,$03; 6A73 ........
|
|
.byte $09,$D8,$FF,$00,$12,$46,$E3,$68; 6A7B .....F.h
|
|
|
|
; Sound $11, voice 2: the drone detonation / strike flash.
|
|
sndScriptDroneStrike:
|
|
.byte $01,$41,$08,$00,$50,$07,$00,$08; 6A83 .A..P...
|
|
.byte $06,$40,$9C,$09,$38,$FF,$00,$2D; 6A8B .@..8..-
|
|
.byte $01,$00,$00,$02,$09,$00,$00; 6A93 .......
|
|
|
|
; Sound $17, voice 2: the tone under the expanding damage/radar frames ($3D2E, $79F5).
|
|
sndScriptDamageSweep:
|
|
.byte $01,$81,$08,$09,$00,$06,$00,$02; 6A9A ........
|
|
.byte $00,$1E,$46,$E3,$68 ; 6AA2 ..F.h
|
|
|
|
; Sound $0D voice 0: the winning chord (WE WON ON POINTS / BY A KNOCKOUT). Voices 0 and 1 set their
|
|
; own pitch and then jump into the shared tail at $6ACE inside the voice 2 script.
|
|
sndScriptWinFanfareV0:
|
|
.byte $01,$21,$08,$00,$47,$06,$47,$06; 6AA7 .!..G.G.
|
|
.byte $09,$A6,$00,$46,$CE,$6A ; 6AAF ...F.j
|
|
|
|
; Sound $0D voice 1: same body, different pitch; also jumps to the tail at $6ACE.
|
|
sndScriptWinFanfareV1:
|
|
.byte $01,$21,$08,$00,$47,$06,$47,$06; 6AB5 .!..G.G.
|
|
.byte $09,$5D,$00,$46,$CE,$6A ; 6ABD .].F.j
|
|
|
|
; Sound $0D voice 2: the third note and, from $6ACE on, the shared decay all three voices run.
|
|
sndScriptWinFanfareV2:
|
|
.byte $01,$21,$08,$00,$47,$06,$47,$06; 6AC3 .!..G.G.
|
|
.byte $09,$16,$00,$00,$78,$09,$00,$00; 6ACB ....x...
|
|
.byte $00,$5A,$01,$20,$00,$14,$46,$E3; 6AD3 .Z. ..F.
|
|
.byte $68 ; 6ADB h
|
|
|
|
; Sound $0E voice 0: the losing chord (WE LOST ON POINTS / WE WERE KNOCKED OUT). Voices 0 and 1 jump
|
|
; into the shared tail at $6B09 inside the voice 2 script.
|
|
sndScriptLossFanfareV0:
|
|
.byte $01,$21,$08,$00,$4D,$06,$87,$21; 6ADC .!..M..!
|
|
.byte $00,$2D,$09,$C2,$FF,$46,$09,$6B; 6AE4 .-...F.k
|
|
|
|
; Sound $0E voice 1: same body one interval down; also jumps to $6B09.
|
|
sndScriptLossFanfareV1:
|
|
.byte $01,$21,$08,$00,$4D,$06,$6B,$2F; 6AEC .!..M.k/
|
|
.byte $00,$2D,$09,$A5,$FF,$46,$09,$6B; 6AF4 .-...F.k
|
|
|
|
; Sound $0E voice 2: the third note plus the shared falling tail from $6B09.
|
|
sndScriptLossFanfareV2:
|
|
.byte $01,$21,$08,$00,$4D,$06,$3C,$32; 6AFC .!..M.<2
|
|
.byte $00,$2D,$09,$9F,$FF,$00,$78,$09; 6B04 .-....x.
|
|
.byte $00,$00,$00,$14,$01,$20,$00,$C8; 6B0C ..... ..
|
|
.byte $46,$E3,$68 ; 6B14 F.h
|
|
|
|
; Sound $0F, voice 2: the drone launch, which then settles into the engine loop at $6B24.
|
|
sndScriptDroneLaunch:
|
|
.byte $01,$81,$08,$0A,$20,$06,$E8,$03; 6B17 .... ...
|
|
.byte $09,$0A,$00,$00,$F0,$01,$81,$08; 6B1F ........
|
|
.byte $00,$20,$06,$48,$0D,$09,$00,$00; 6B27 . .H....
|
|
.byte $00,$01,$07,$96,$00,$01,$41,$00; 6B2F ......A.
|
|
.byte $01,$46,$24,$6B ; 6B37 .F$k
|
|
|
|
; Sound $10, voice 2: the pitch wobble as the drone turns one step, ending by jumping back into the
|
|
; engine loop of sndScriptDroneLaunch at $6B24.
|
|
sndScriptDroneTurn:
|
|
.byte $06,$48,$0D,$08,$99,$20,$02,$07; 6B3B .H... ..
|
|
.byte $09,$0E,$00,$01,$81,$00,$01,$07; 6B43 ........
|
|
.byte $96,$00,$01,$41,$00,$01,$46,$43; 6B4B ...A..FC
|
|
.byte $6B,$02,$07,$01,$81,$09,$F2,$FF; 6B53 k.......
|
|
.byte $00,$01,$01,$41,$00,$01,$46,$56; 6B5B ...A..FV
|
|
.byte $6B,$46,$24,$6B ; 6B63 kF$k
|
|
|
|
; Sound $18, voice 2: the repeating warble under 'ATTEMPTING REPAIR...'.
|
|
sndScriptRepairAttempt:
|
|
.byte $06,$08,$07,$08,$00,$20,$07,$00; 6B67 ..... ..
|
|
.byte $08,$01,$41,$09,$C8,$00,$00,$06; 6B6F ..A.....
|
|
.byte $09,$50,$FB,$00,$01,$09,$38,$FF; 6B77 .P....8.
|
|
.byte $00,$06,$09,$00,$05,$00,$01,$46; 6B7F .......F
|
|
.byte $72,$6B ; 6B87 rk
|
|
|
|
; Sound $13, voice 2: the rising sweep that reports a successful repair.
|
|
sndScriptRepairOk:
|
|
.byte $06,$A4,$06,$01,$21,$08,$00,$20; 6B89 ....!..
|
|
.byte $02,$1E,$09,$C8,$00,$00,$02,$09; 6B91 ........
|
|
.byte $6A,$FF,$00,$01,$46,$93,$6B,$46; 6B99 j...F.kF
|
|
.byte $E3,$68 ; 6BA1 .h
|
|
|
|
; Sound $12, voice 2: the two-counter stutter that reports a failed repair.
|
|
sndScriptRepairFail:
|
|
.byte $06,$E0,$15,$01,$21,$08,$00,$20; 6BA3 ....!..
|
|
.byte $02,$03,$03,$0A,$09,$06,$FF,$00; 6BAB ........
|
|
.byte $01,$09,$96,$00,$00,$01,$47,$AF; 6BB3 ......G.
|
|
.byte $6B,$09,$A8,$FD,$00,$01,$46,$AD; 6BBB k.....F.
|
|
.byte $6B,$46,$E3,$68 ; 6BC3 kF.h
|
|
|
|
; Sound $16, voice 2: played while the player's own comcen is moving in a live game.
|
|
sndScriptOwnComcenMove:
|
|
.byte $06,$E8,$03,$08,$00,$40,$09,$04; 6BC7 .....@..
|
|
.byte $00,$03,$02,$02,$0F,$01,$81,$00; 6BCF ........
|
|
.byte $01,$01,$21,$00,$01,$46,$D4,$6B; 6BD7 ..!..F.k
|
|
.byte $09,$FC,$FF,$47,$D2,$6B,$09,$00; 6BDF ...G.k..
|
|
.byte $00 ; 6BE7 .
|
|
|
|
; Sound $14, voice 2: the comcen engine loop, kept running while the comcen is moving and stopped
|
|
; again with sound $1C.
|
|
sndScriptComcenEngine:
|
|
.byte $06,$E8,$03,$08,$00,$10,$01,$81; 6BE8 ........
|
|
.byte $00,$01,$01,$21,$00,$01,$46,$EE; 6BF0 ...!..F.
|
|
.byte $6B,$00,$00 ; 6BF8 k..
|
|
|
|
; Sound $19 voice 0: the alarm chord used for GAME OVER, TIMEOUT, RESUMED and the LAST QTR WARNING.
|
|
sndScriptGameOverV0:
|
|
.byte $08,$00,$70,$07,$00,$08,$01,$41; 6BFB ..p....A
|
|
.byte $02,$16,$06,$06,$B3,$00,$01,$06; 6C03 ........
|
|
.byte $8B,$96,$00,$01,$46,$05,$6C,$46; 6C0B ....F.lF
|
|
.byte $E3,$68 ; 6C13 .h
|
|
|
|
; Sound $19 voice 1: sets its own pitch and jumps into the middle of the voice 2 script at $6C1E, so
|
|
; voices 1 and 2 share one body.
|
|
sndScriptGameOverV1:
|
|
.byte $06,$FA,$A8,$46,$1E,$6C ; 6C15 ...F.l
|
|
|
|
; Sound $19 voice 2: the body both it and voice 1 run from $6C1E.
|
|
sndScriptGameOverV2:
|
|
.byte $06,$00,$AE,$01,$41,$07,$00,$08; 6C1B ....A...
|
|
.byte $02,$16,$08,$00,$70,$00,$01,$08; 6C23 ....p...
|
|
.byte $00,$30,$00,$01,$46,$25,$6C,$46; 6C2B .0..F%lF
|
|
.byte $E3,$68 ; 6C33 .h
|
|
|
|
; unitGlyphPointerTable - 20 words, one per unit picture, indexed by unitType*4 + facing. The
|
|
; viewport tile builder computes the index at $289A-$28A7 as (unitFlagsTable[unit] & 3) +
|
|
; unitGlyphBaseTable[type] - $2C, where unitGlyphBaseTable ($0799) holds $2C,$30,$34,$38,$3C for
|
|
; GRUNT, RIDER, BOOMER, SPY and COMCEN, so the subtraction turns it into type*4 + facing. It then
|
|
; copies the 32 bytes at the pointer into the cell buffer at $28B5.
|
|
unitGlyphPointerTable:
|
|
.addr unitGlyphGruntUp ; 6C35 GRUNT facing 0
|
|
.addr unitGlyphGruntDown ; 6C37
|
|
.addr unitGlyphGruntLeft ; 6C39
|
|
.addr unitGlyphGruntRight ; 6C3B
|
|
.addr unitGlyphRiderUp ; 6C3D
|
|
.addr unitGlyphRiderDown ; 6C3F
|
|
.addr unitGlyphRiderLeft ; 6C41
|
|
.addr unitGlyphRiderRight ; 6C43
|
|
.addr unitGlyphBoomerUp ; 6C45
|
|
.addr unitGlyphBoomerDown ; 6C47
|
|
.addr unitGlyphBoomerLeft ; 6C49
|
|
.addr unitGlyphBoomerRight ; 6C4B
|
|
.addr unitGlyphSpyUp ; 6C4D
|
|
.addr unitGlyphSpyDown ; 6C4F
|
|
.addr unitGlyphSpyLeft ; 6C51
|
|
.addr unitGlyphSpyRight ; 6C53
|
|
.addr unitGlyphComcenUp ; 6C55
|
|
.addr unitGlyphComcenDown ; 6C57
|
|
.addr unitGlyphComcenLeft ; 6C59
|
|
.addr unitGlyphComcenRight ; 6C5B
|
|
|
|
; Unit glyph shapes - the 640 bytes $6C5D-$6EDC are 20 unit pictures of 32 bytes, one for each unit
|
|
; type and facing, in the order unitGlyphPointerTable indexes them (type*4 + facing). A picture is one
|
|
; map cell of the 7x5 tactical view: 2x2 character cells = 16x16 pixels, stored as top-left,
|
|
; top-right, bottom-left and bottom-right, 8 bytes each, which is why the bit art below shows each
|
|
; picture as four separate 8x8 blocks rather than one square. The bitmap is multicolour, so a pixel is
|
|
; a pair of bits and every pixel drawn here is %11, whose colour comes from colour RAM - that is how
|
|
; one shape serves both sides in their own colour. Facings 0 and 1 are a vertical mirror pair and 2
|
|
; and 3 a horizontal pair; the survey reads them as up, down, left, right.
|
|
;
|
|
; GRUNT, facing 0 (up) - glyph index 0. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphGruntUp:
|
|
.byte $00 ; 6C5D ........
|
|
.byte $00 ; 6C5E ........
|
|
.byte $00 ; 6C5F ........
|
|
.byte $03 ; 6C60 ......##
|
|
.byte $03 ; 6C61 ......##
|
|
.byte $03 ; 6C62 ......##
|
|
.byte $0F ; 6C63 ....####
|
|
.byte $0F ; 6C64 ....####
|
|
|
|
.byte $00 ; 6C65 ........
|
|
.byte $00 ; 6C66 ........
|
|
.byte $00 ; 6C67 ........
|
|
.byte $00 ; 6C68 ........
|
|
.byte $00 ; 6C69 ........
|
|
.byte $00 ; 6C6A ........
|
|
.byte $C0 ; 6C6B ##......
|
|
.byte $C0 ; 6C6C ##......
|
|
|
|
.byte $33 ; 6C6D ..##..##
|
|
.byte $3F ; 6C6E ..######
|
|
.byte $3F ; 6C6F ..######
|
|
.byte $3F ; 6C70 ..######
|
|
.byte $0F ; 6C71 ....####
|
|
.byte $00 ; 6C72 ........
|
|
.byte $00 ; 6C73 ........
|
|
.byte $00 ; 6C74 ........
|
|
|
|
.byte $30 ; 6C75 ..##....
|
|
.byte $F0 ; 6C76 ####....
|
|
.byte $F0 ; 6C77 ####....
|
|
.byte $F0 ; 6C78 ####....
|
|
.byte $C0 ; 6C79 ##......
|
|
.byte $00 ; 6C7A ........
|
|
.byte $00 ; 6C7B ........
|
|
.byte $00 ; 6C7C ........
|
|
|
|
|
|
; GRUNT, facing 1 (down) - glyph index 1. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphGruntDown:
|
|
.byte $00 ; 6C7D ........
|
|
.byte $00 ; 6C7E ........
|
|
.byte $00 ; 6C7F ........
|
|
.byte $0F ; 6C80 ....####
|
|
.byte $3F ; 6C81 ..######
|
|
.byte $3F ; 6C82 ..######
|
|
.byte $3F ; 6C83 ..######
|
|
.byte $33 ; 6C84 ..##..##
|
|
|
|
.byte $00 ; 6C85 ........
|
|
.byte $00 ; 6C86 ........
|
|
.byte $00 ; 6C87 ........
|
|
.byte $C0 ; 6C88 ##......
|
|
.byte $F0 ; 6C89 ####....
|
|
.byte $F0 ; 6C8A ####....
|
|
.byte $F0 ; 6C8B ####....
|
|
.byte $30 ; 6C8C ..##....
|
|
|
|
.byte $0F ; 6C8D ....####
|
|
.byte $0F ; 6C8E ....####
|
|
.byte $03 ; 6C8F ......##
|
|
.byte $03 ; 6C90 ......##
|
|
.byte $03 ; 6C91 ......##
|
|
.byte $00 ; 6C92 ........
|
|
.byte $00 ; 6C93 ........
|
|
.byte $00 ; 6C94 ........
|
|
|
|
.byte $C0 ; 6C95 ##......
|
|
.byte $C0 ; 6C96 ##......
|
|
.byte $00 ; 6C97 ........
|
|
.byte $00 ; 6C98 ........
|
|
.byte $00 ; 6C99 ........
|
|
.byte $00 ; 6C9A ........
|
|
.byte $00 ; 6C9B ........
|
|
.byte $00 ; 6C9C ........
|
|
|
|
|
|
; GRUNT, facing 2 (left) - glyph index 2. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphGruntLeft:
|
|
.byte $00 ; 6C9D ........
|
|
.byte $00 ; 6C9E ........
|
|
.byte $00 ; 6C9F ........
|
|
.byte $0F ; 6CA0 ....####
|
|
.byte $0F ; 6CA1 ....####
|
|
.byte $3F ; 6CA2 ..######
|
|
.byte $3C ; 6CA3 ..####..
|
|
.byte $3F ; 6CA4 ..######
|
|
|
|
.byte $00 ; 6CA5 ........
|
|
.byte $00 ; 6CA6 ........
|
|
.byte $00 ; 6CA7 ........
|
|
.byte $00 ; 6CA8 ........
|
|
.byte $00 ; 6CA9 ........
|
|
.byte $C0 ; 6CAA ##......
|
|
.byte $C0 ; 6CAB ##......
|
|
.byte $FC ; 6CAC ######..
|
|
|
|
.byte $3F ; 6CAD ..######
|
|
.byte $3C ; 6CAE ..####..
|
|
.byte $3F ; 6CAF ..######
|
|
.byte $0F ; 6CB0 ....####
|
|
.byte $0F ; 6CB1 ....####
|
|
.byte $00 ; 6CB2 ........
|
|
.byte $00 ; 6CB3 ........
|
|
.byte $00 ; 6CB4 ........
|
|
|
|
.byte $FC ; 6CB5 ######..
|
|
.byte $C0 ; 6CB6 ##......
|
|
.byte $C0 ; 6CB7 ##......
|
|
.byte $00 ; 6CB8 ........
|
|
.byte $00 ; 6CB9 ........
|
|
.byte $00 ; 6CBA ........
|
|
.byte $00 ; 6CBB ........
|
|
.byte $00 ; 6CBC ........
|
|
|
|
|
|
; GRUNT, facing 3 (right) - glyph index 3. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphGruntRight:
|
|
.byte $00 ; 6CBD ........
|
|
.byte $00 ; 6CBE ........
|
|
.byte $00 ; 6CBF ........
|
|
.byte $00 ; 6CC0 ........
|
|
.byte $00 ; 6CC1 ........
|
|
.byte $03 ; 6CC2 ......##
|
|
.byte $03 ; 6CC3 ......##
|
|
.byte $3F ; 6CC4 ..######
|
|
|
|
.byte $00 ; 6CC5 ........
|
|
.byte $00 ; 6CC6 ........
|
|
.byte $00 ; 6CC7 ........
|
|
.byte $F0 ; 6CC8 ####....
|
|
.byte $F0 ; 6CC9 ####....
|
|
.byte $FC ; 6CCA ######..
|
|
.byte $3C ; 6CCB ..####..
|
|
.byte $FC ; 6CCC ######..
|
|
|
|
.byte $3F ; 6CCD ..######
|
|
.byte $03 ; 6CCE ......##
|
|
.byte $03 ; 6CCF ......##
|
|
.byte $00 ; 6CD0 ........
|
|
.byte $00 ; 6CD1 ........
|
|
.byte $00 ; 6CD2 ........
|
|
.byte $00 ; 6CD3 ........
|
|
.byte $00 ; 6CD4 ........
|
|
|
|
.byte $FC ; 6CD5 ######..
|
|
.byte $3C ; 6CD6 ..####..
|
|
.byte $FC ; 6CD7 ######..
|
|
.byte $F0 ; 6CD8 ####....
|
|
.byte $F0 ; 6CD9 ####....
|
|
.byte $00 ; 6CDA ........
|
|
.byte $00 ; 6CDB ........
|
|
.byte $00 ; 6CDC ........
|
|
|
|
|
|
; RIDER, facing 0 (up) - glyph index 4. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphRiderUp:
|
|
.byte $00 ; 6CDD ........
|
|
.byte $00 ; 6CDE ........
|
|
.byte $00 ; 6CDF ........
|
|
.byte $00 ; 6CE0 ........
|
|
.byte $03 ; 6CE1 ......##
|
|
.byte $33 ; 6CE2 ..##..##
|
|
.byte $33 ; 6CE3 ..##..##
|
|
.byte $0F ; 6CE4 ....####
|
|
|
|
.byte $00 ; 6CE5 ........
|
|
.byte $00 ; 6CE6 ........
|
|
.byte $00 ; 6CE7 ........
|
|
.byte $00 ; 6CE8 ........
|
|
.byte $00 ; 6CE9 ........
|
|
.byte $30 ; 6CEA ..##....
|
|
.byte $30 ; 6CEB ..##....
|
|
.byte $C0 ; 6CEC ##......
|
|
|
|
.byte $0F ; 6CED ....####
|
|
.byte $FF ; 6CEE ########
|
|
.byte $F0 ; 6CEF ####....
|
|
.byte $C0 ; 6CF0 ##......
|
|
.byte $00 ; 6CF1 ........
|
|
.byte $00 ; 6CF2 ........
|
|
.byte $00 ; 6CF3 ........
|
|
.byte $00 ; 6CF4 ........
|
|
|
|
.byte $C0 ; 6CF5 ##......
|
|
.byte $FC ; 6CF6 ######..
|
|
.byte $3C ; 6CF7 ..####..
|
|
.byte $0C ; 6CF8 ....##..
|
|
.byte $00 ; 6CF9 ........
|
|
.byte $00 ; 6CFA ........
|
|
.byte $00 ; 6CFB ........
|
|
.byte $00 ; 6CFC ........
|
|
|
|
|
|
; RIDER, facing 1 (down) - glyph index 5. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphRiderDown:
|
|
.byte $00 ; 6CFD ........
|
|
.byte $00 ; 6CFE ........
|
|
.byte $00 ; 6CFF ........
|
|
.byte $C0 ; 6D00 ##......
|
|
.byte $F0 ; 6D01 ####....
|
|
.byte $FF ; 6D02 ########
|
|
.byte $0F ; 6D03 ....####
|
|
.byte $0F ; 6D04 ....####
|
|
|
|
.byte $00 ; 6D05 ........
|
|
.byte $00 ; 6D06 ........
|
|
.byte $00 ; 6D07 ........
|
|
.byte $0C ; 6D08 ....##..
|
|
.byte $3C ; 6D09 ..####..
|
|
.byte $FC ; 6D0A ######..
|
|
.byte $C0 ; 6D0B ##......
|
|
.byte $C0 ; 6D0C ##......
|
|
|
|
.byte $33 ; 6D0D ..##..##
|
|
.byte $33 ; 6D0E ..##..##
|
|
.byte $03 ; 6D0F ......##
|
|
.byte $00 ; 6D10 ........
|
|
.byte $00 ; 6D11 ........
|
|
.byte $00 ; 6D12 ........
|
|
.byte $00 ; 6D13 ........
|
|
.byte $00 ; 6D14 ........
|
|
|
|
.byte $30 ; 6D15 ..##....
|
|
.byte $30 ; 6D16 ..##....
|
|
.byte $00 ; 6D17 ........
|
|
.byte $00 ; 6D18 ........
|
|
.byte $00 ; 6D19 ........
|
|
.byte $00 ; 6D1A ........
|
|
.byte $00 ; 6D1B ........
|
|
.byte $00 ; 6D1C ........
|
|
|
|
|
|
; RIDER, facing 2 (left) - glyph index 6. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphRiderLeft:
|
|
.byte $00 ; 6D1D ........
|
|
.byte $00 ; 6D1E ........
|
|
.byte $3C ; 6D1F ..####..
|
|
.byte $3C ; 6D20 ..####..
|
|
.byte $0C ; 6D21 ....##..
|
|
.byte $0F ; 6D22 ....####
|
|
.byte $0F ; 6D23 ....####
|
|
.byte $0F ; 6D24 ....####
|
|
|
|
.byte $00 ; 6D25 ........
|
|
.byte $00 ; 6D26 ........
|
|
.byte $00 ; 6D27 ........
|
|
.byte $00 ; 6D28 ........
|
|
.byte $C0 ; 6D29 ##......
|
|
.byte $C0 ; 6D2A ##......
|
|
.byte $00 ; 6D2B ........
|
|
.byte $F0 ; 6D2C ####....
|
|
|
|
.byte $0F ; 6D2D ....####
|
|
.byte $0F ; 6D2E ....####
|
|
.byte $0F ; 6D2F ....####
|
|
.byte $0C ; 6D30 ....##..
|
|
.byte $3C ; 6D31 ..####..
|
|
.byte $3C ; 6D32 ..####..
|
|
.byte $00 ; 6D33 ........
|
|
.byte $00 ; 6D34 ........
|
|
|
|
.byte $F0 ; 6D35 ####....
|
|
.byte $00 ; 6D36 ........
|
|
.byte $C0 ; 6D37 ##......
|
|
.byte $C0 ; 6D38 ##......
|
|
.byte $00 ; 6D39 ........
|
|
.byte $00 ; 6D3A ........
|
|
.byte $00 ; 6D3B ........
|
|
.byte $00 ; 6D3C ........
|
|
|
|
|
|
; RIDER, facing 3 (right) - glyph index 7. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphRiderRight:
|
|
.byte $00 ; 6D3D ........
|
|
.byte $00 ; 6D3E ........
|
|
.byte $00 ; 6D3F ........
|
|
.byte $00 ; 6D40 ........
|
|
.byte $0C ; 6D41 ....##..
|
|
.byte $0F ; 6D42 ....####
|
|
.byte $03 ; 6D43 ......##
|
|
.byte $3F ; 6D44 ..######
|
|
|
|
.byte $00 ; 6D45 ........
|
|
.byte $00 ; 6D46 ........
|
|
.byte $F0 ; 6D47 ####....
|
|
.byte $F0 ; 6D48 ####....
|
|
.byte $C0 ; 6D49 ##......
|
|
.byte $C0 ; 6D4A ##......
|
|
.byte $C0 ; 6D4B ##......
|
|
.byte $C0 ; 6D4C ##......
|
|
|
|
.byte $3F ; 6D4D ..######
|
|
.byte $03 ; 6D4E ......##
|
|
.byte $0F ; 6D4F ....####
|
|
.byte $0C ; 6D50 ....##..
|
|
.byte $00 ; 6D51 ........
|
|
.byte $00 ; 6D52 ........
|
|
.byte $00 ; 6D53 ........
|
|
.byte $00 ; 6D54 ........
|
|
|
|
.byte $C0 ; 6D55 ##......
|
|
.byte $C0 ; 6D56 ##......
|
|
.byte $C0 ; 6D57 ##......
|
|
.byte $C0 ; 6D58 ##......
|
|
.byte $F0 ; 6D59 ####....
|
|
.byte $F0 ; 6D5A ####....
|
|
.byte $00 ; 6D5B ........
|
|
.byte $00 ; 6D5C ........
|
|
|
|
|
|
; BOOMER, facing 0 (up) - glyph index 8. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphBoomerUp:
|
|
.byte $00 ; 6D5D ........
|
|
.byte $0F ; 6D5E ....####
|
|
.byte $03 ; 6D5F ......##
|
|
.byte $03 ; 6D60 ......##
|
|
.byte $03 ; 6D61 ......##
|
|
.byte $03 ; 6D62 ......##
|
|
.byte $03 ; 6D63 ......##
|
|
.byte $C3 ; 6D64 ##....##
|
|
|
|
.byte $00 ; 6D65 ........
|
|
.byte $C0 ; 6D66 ##......
|
|
.byte $00 ; 6D67 ........
|
|
.byte $00 ; 6D68 ........
|
|
.byte $00 ; 6D69 ........
|
|
.byte $00 ; 6D6A ........
|
|
.byte $00 ; 6D6B ........
|
|
.byte $0C ; 6D6C ....##..
|
|
|
|
.byte $FF ; 6D6D ########
|
|
.byte $FF ; 6D6E ########
|
|
.byte $FF ; 6D6F ########
|
|
.byte $FF ; 6D70 ########
|
|
.byte $FF ; 6D71 ########
|
|
.byte $C0 ; 6D72 ##......
|
|
.byte $00 ; 6D73 ........
|
|
.byte $00 ; 6D74 ........
|
|
|
|
.byte $FC ; 6D75 ######..
|
|
.byte $FC ; 6D76 ######..
|
|
.byte $FC ; 6D77 ######..
|
|
.byte $FC ; 6D78 ######..
|
|
.byte $FC ; 6D79 ######..
|
|
.byte $0C ; 6D7A ....##..
|
|
.byte $00 ; 6D7B ........
|
|
.byte $00 ; 6D7C ........
|
|
|
|
|
|
; BOOMER, facing 1 (down) - glyph index 9. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphBoomerDown:
|
|
.byte $00 ; 6D7D ........
|
|
.byte $00 ; 6D7E ........
|
|
.byte $C0 ; 6D7F ##......
|
|
.byte $FF ; 6D80 ########
|
|
.byte $FF ; 6D81 ########
|
|
.byte $FF ; 6D82 ########
|
|
.byte $FF ; 6D83 ########
|
|
.byte $FF ; 6D84 ########
|
|
|
|
.byte $00 ; 6D85 ........
|
|
.byte $00 ; 6D86 ........
|
|
.byte $0C ; 6D87 ....##..
|
|
.byte $FC ; 6D88 ######..
|
|
.byte $FC ; 6D89 ######..
|
|
.byte $FC ; 6D8A ######..
|
|
.byte $FC ; 6D8B ######..
|
|
.byte $FC ; 6D8C ######..
|
|
|
|
.byte $C3 ; 6D8D ##....##
|
|
.byte $03 ; 6D8E ......##
|
|
.byte $03 ; 6D8F ......##
|
|
.byte $03 ; 6D90 ......##
|
|
.byte $03 ; 6D91 ......##
|
|
.byte $03 ; 6D92 ......##
|
|
.byte $0F ; 6D93 ....####
|
|
.byte $00 ; 6D94 ........
|
|
|
|
.byte $0C ; 6D95 ....##..
|
|
.byte $00 ; 6D96 ........
|
|
.byte $00 ; 6D97 ........
|
|
.byte $00 ; 6D98 ........
|
|
.byte $00 ; 6D99 ........
|
|
.byte $00 ; 6D9A ........
|
|
.byte $C0 ; 6D9B ##......
|
|
.byte $00 ; 6D9C ........
|
|
|
|
|
|
; BOOMER, facing 2 (left) - glyph index 10. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphBoomerLeft:
|
|
.byte $00 ; 6D9D ........
|
|
.byte $00 ; 6D9E ........
|
|
.byte $00 ; 6D9F ........
|
|
.byte $FF ; 6DA0 ########
|
|
.byte $3F ; 6DA1 ..######
|
|
.byte $3F ; 6DA2 ..######
|
|
.byte $3F ; 6DA3 ..######
|
|
.byte $3F ; 6DA4 ..######
|
|
|
|
.byte $00 ; 6DA5 ........
|
|
.byte $00 ; 6DA6 ........
|
|
.byte $00 ; 6DA7 ........
|
|
.byte $C0 ; 6DA8 ##......
|
|
.byte $00 ; 6DA9 ........
|
|
.byte $00 ; 6DAA ........
|
|
.byte $03 ; 6DAB ......##
|
|
.byte $03 ; 6DAC ......##
|
|
|
|
.byte $3F ; 6DAD ..######
|
|
.byte $3F ; 6DAE ..######
|
|
.byte $3F ; 6DAF ..######
|
|
.byte $3F ; 6DB0 ..######
|
|
.byte $3F ; 6DB1 ..######
|
|
.byte $FF ; 6DB2 ########
|
|
.byte $00 ; 6DB3 ........
|
|
.byte $00 ; 6DB4 ........
|
|
|
|
.byte $FF ; 6DB5 ########
|
|
.byte $03 ; 6DB6 ......##
|
|
.byte $03 ; 6DB7 ......##
|
|
.byte $00 ; 6DB8 ........
|
|
.byte $00 ; 6DB9 ........
|
|
.byte $C0 ; 6DBA ##......
|
|
.byte $00 ; 6DBB ........
|
|
.byte $00 ; 6DBC ........
|
|
|
|
|
|
; BOOMER, facing 3 (right) - glyph index 11. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphBoomerRight:
|
|
.byte $00 ; 6DBD ........
|
|
.byte $00 ; 6DBE ........
|
|
.byte $00 ; 6DBF ........
|
|
.byte $03 ; 6DC0 ......##
|
|
.byte $00 ; 6DC1 ........
|
|
.byte $00 ; 6DC2 ........
|
|
.byte $C0 ; 6DC3 ##......
|
|
.byte $C0 ; 6DC4 ##......
|
|
|
|
.byte $00 ; 6DC5 ........
|
|
.byte $00 ; 6DC6 ........
|
|
.byte $00 ; 6DC7 ........
|
|
.byte $FF ; 6DC8 ########
|
|
.byte $FC ; 6DC9 ######..
|
|
.byte $FC ; 6DCA ######..
|
|
.byte $FC ; 6DCB ######..
|
|
.byte $FC ; 6DCC ######..
|
|
|
|
.byte $FF ; 6DCD ########
|
|
.byte $C0 ; 6DCE ##......
|
|
.byte $C0 ; 6DCF ##......
|
|
.byte $00 ; 6DD0 ........
|
|
.byte $00 ; 6DD1 ........
|
|
.byte $03 ; 6DD2 ......##
|
|
.byte $00 ; 6DD3 ........
|
|
.byte $00 ; 6DD4 ........
|
|
|
|
.byte $FC ; 6DD5 ######..
|
|
.byte $FC ; 6DD6 ######..
|
|
.byte $FC ; 6DD7 ######..
|
|
.byte $FC ; 6DD8 ######..
|
|
.byte $FC ; 6DD9 ######..
|
|
.byte $FF ; 6DDA ########
|
|
.byte $00 ; 6DDB ........
|
|
.byte $00 ; 6DDC ........
|
|
|
|
|
|
; SPY, facing 0 (up) - glyph index 12. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphSpyUp:
|
|
.byte $00 ; 6DDD ........
|
|
.byte $00 ; 6DDE ........
|
|
.byte $00 ; 6DDF ........
|
|
.byte $00 ; 6DE0 ........
|
|
.byte $30 ; 6DE1 ..##....
|
|
.byte $30 ; 6DE2 ..##....
|
|
.byte $3C ; 6DE3 ..####..
|
|
.byte $0C ; 6DE4 ....##..
|
|
|
|
.byte $00 ; 6DE5 ........
|
|
.byte $00 ; 6DE6 ........
|
|
.byte $00 ; 6DE7 ........
|
|
.byte $00 ; 6DE8 ........
|
|
.byte $30 ; 6DE9 ..##....
|
|
.byte $30 ; 6DEA ..##....
|
|
.byte $F0 ; 6DEB ####....
|
|
.byte $C0 ; 6DEC ##......
|
|
|
|
.byte $0F ; 6DED ....####
|
|
.byte $03 ; 6DEE ......##
|
|
.byte $03 ; 6DEF ......##
|
|
.byte $03 ; 6DF0 ......##
|
|
.byte $00 ; 6DF1 ........
|
|
.byte $00 ; 6DF2 ........
|
|
.byte $00 ; 6DF3 ........
|
|
.byte $00 ; 6DF4 ........
|
|
|
|
.byte $C0 ; 6DF5 ##......
|
|
.byte $00 ; 6DF6 ........
|
|
.byte $00 ; 6DF7 ........
|
|
.byte $00 ; 6DF8 ........
|
|
.byte $00 ; 6DF9 ........
|
|
.byte $00 ; 6DFA ........
|
|
.byte $00 ; 6DFB ........
|
|
.byte $00 ; 6DFC ........
|
|
|
|
|
|
; SPY, facing 1 (down) - glyph index 13. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphSpyDown:
|
|
.byte $00 ; 6DFD ........
|
|
.byte $00 ; 6DFE ........
|
|
.byte $00 ; 6DFF ........
|
|
.byte $03 ; 6E00 ......##
|
|
.byte $03 ; 6E01 ......##
|
|
.byte $03 ; 6E02 ......##
|
|
.byte $0F ; 6E03 ....####
|
|
.byte $0C ; 6E04 ....##..
|
|
|
|
.byte $00 ; 6E05 ........
|
|
.byte $00 ; 6E06 ........
|
|
.byte $00 ; 6E07 ........
|
|
.byte $00 ; 6E08 ........
|
|
.byte $00 ; 6E09 ........
|
|
.byte $00 ; 6E0A ........
|
|
.byte $C0 ; 6E0B ##......
|
|
.byte $C0 ; 6E0C ##......
|
|
|
|
.byte $3C ; 6E0D ..####..
|
|
.byte $30 ; 6E0E ..##....
|
|
.byte $30 ; 6E0F ..##....
|
|
.byte $00 ; 6E10 ........
|
|
.byte $00 ; 6E11 ........
|
|
.byte $00 ; 6E12 ........
|
|
.byte $00 ; 6E13 ........
|
|
.byte $00 ; 6E14 ........
|
|
|
|
.byte $F0 ; 6E15 ####....
|
|
.byte $30 ; 6E16 ..##....
|
|
.byte $30 ; 6E17 ..##....
|
|
.byte $00 ; 6E18 ........
|
|
.byte $00 ; 6E19 ........
|
|
.byte $00 ; 6E1A ........
|
|
.byte $00 ; 6E1B ........
|
|
.byte $00 ; 6E1C ........
|
|
|
|
|
|
; SPY, facing 2 (left) - glyph index 14. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphSpyLeft:
|
|
.byte $00 ; 6E1D ........
|
|
.byte $00 ; 6E1E ........
|
|
.byte $00 ; 6E1F ........
|
|
.byte $00 ; 6E20 ........
|
|
.byte $00 ; 6E21 ........
|
|
.byte $03 ; 6E22 ......##
|
|
.byte $03 ; 6E23 ......##
|
|
.byte $3C ; 6E24 ..####..
|
|
|
|
.byte $00 ; 6E25 ........
|
|
.byte $00 ; 6E26 ........
|
|
.byte $00 ; 6E27 ........
|
|
.byte $F0 ; 6E28 ####....
|
|
.byte $F0 ; 6E29 ####....
|
|
.byte $00 ; 6E2A ........
|
|
.byte $00 ; 6E2B ........
|
|
.byte $00 ; 6E2C ........
|
|
|
|
.byte $3C ; 6E2D ..####..
|
|
.byte $03 ; 6E2E ......##
|
|
.byte $03 ; 6E2F ......##
|
|
.byte $00 ; 6E30 ........
|
|
.byte $00 ; 6E31 ........
|
|
.byte $00 ; 6E32 ........
|
|
.byte $00 ; 6E33 ........
|
|
.byte $00 ; 6E34 ........
|
|
|
|
.byte $00 ; 6E35 ........
|
|
.byte $00 ; 6E36 ........
|
|
.byte $00 ; 6E37 ........
|
|
.byte $F0 ; 6E38 ####....
|
|
.byte $F0 ; 6E39 ####....
|
|
.byte $00 ; 6E3A ........
|
|
.byte $00 ; 6E3B ........
|
|
.byte $00 ; 6E3C ........
|
|
|
|
|
|
; SPY, facing 3 (right) - glyph index 15. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphSpyRight:
|
|
.byte $00 ; 6E3D ........
|
|
.byte $00 ; 6E3E ........
|
|
.byte $00 ; 6E3F ........
|
|
.byte $3C ; 6E40 ..####..
|
|
.byte $3C ; 6E41 ..####..
|
|
.byte $03 ; 6E42 ......##
|
|
.byte $03 ; 6E43 ......##
|
|
.byte $00 ; 6E44 ........
|
|
|
|
.byte $00 ; 6E45 ........
|
|
.byte $00 ; 6E46 ........
|
|
.byte $00 ; 6E47 ........
|
|
.byte $00 ; 6E48 ........
|
|
.byte $00 ; 6E49 ........
|
|
.byte $00 ; 6E4A ........
|
|
.byte $00 ; 6E4B ........
|
|
.byte $F0 ; 6E4C ####....
|
|
|
|
.byte $00 ; 6E4D ........
|
|
.byte $03 ; 6E4E ......##
|
|
.byte $03 ; 6E4F ......##
|
|
.byte $3C ; 6E50 ..####..
|
|
.byte $3C ; 6E51 ..####..
|
|
.byte $00 ; 6E52 ........
|
|
.byte $00 ; 6E53 ........
|
|
.byte $00 ; 6E54 ........
|
|
|
|
.byte $F0 ; 6E55 ####....
|
|
.byte $00 ; 6E56 ........
|
|
.byte $00 ; 6E57 ........
|
|
.byte $00 ; 6E58 ........
|
|
.byte $00 ; 6E59 ........
|
|
.byte $00 ; 6E5A ........
|
|
.byte $00 ; 6E5B ........
|
|
.byte $00 ; 6E5C ........
|
|
|
|
|
|
; COMCEN, facing 0 (up) - glyph index 16. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphComcenUp:
|
|
.byte $00 ; 6E5D ........
|
|
.byte $0F ; 6E5E ....####
|
|
.byte $0F ; 6E5F ....####
|
|
.byte $3F ; 6E60 ..######
|
|
.byte $3F ; 6E61 ..######
|
|
.byte $FF ; 6E62 ########
|
|
.byte $F0 ; 6E63 ####....
|
|
.byte $FF ; 6E64 ########
|
|
|
|
.byte $00 ; 6E65 ........
|
|
.byte $C0 ; 6E66 ##......
|
|
.byte $C0 ; 6E67 ##......
|
|
.byte $F0 ; 6E68 ####....
|
|
.byte $F0 ; 6E69 ####....
|
|
.byte $FC ; 6E6A ######..
|
|
.byte $3C ; 6E6B ..####..
|
|
.byte $FC ; 6E6C ######..
|
|
|
|
.byte $FF ; 6E6D ########
|
|
.byte $FF ; 6E6E ########
|
|
.byte $FF ; 6E6F ########
|
|
.byte $FF ; 6E70 ########
|
|
.byte $FF ; 6E71 ########
|
|
.byte $FF ; 6E72 ########
|
|
.byte $FF ; 6E73 ########
|
|
.byte $00 ; 6E74 ........
|
|
|
|
.byte $FC ; 6E75 ######..
|
|
.byte $FC ; 6E76 ######..
|
|
.byte $FC ; 6E77 ######..
|
|
.byte $FC ; 6E78 ######..
|
|
.byte $FC ; 6E79 ######..
|
|
.byte $FC ; 6E7A ######..
|
|
.byte $FC ; 6E7B ######..
|
|
.byte $00 ; 6E7C ........
|
|
|
|
|
|
; COMCEN, facing 1 (down) - glyph index 17. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphComcenDown:
|
|
.byte $00 ; 6E7D ........
|
|
.byte $00 ; 6E7E ........
|
|
.byte $FF ; 6E7F ########
|
|
.byte $FF ; 6E80 ########
|
|
.byte $FF ; 6E81 ########
|
|
.byte $FF ; 6E82 ########
|
|
.byte $FF ; 6E83 ########
|
|
.byte $FF ; 6E84 ########
|
|
|
|
.byte $00 ; 6E85 ........
|
|
.byte $00 ; 6E86 ........
|
|
.byte $FC ; 6E87 ######..
|
|
.byte $FC ; 6E88 ######..
|
|
.byte $FC ; 6E89 ######..
|
|
.byte $FC ; 6E8A ######..
|
|
.byte $FC ; 6E8B ######..
|
|
.byte $FC ; 6E8C ######..
|
|
|
|
.byte $FF ; 6E8D ########
|
|
.byte $FF ; 6E8E ########
|
|
.byte $F0 ; 6E8F ####....
|
|
.byte $FF ; 6E90 ########
|
|
.byte $3F ; 6E91 ..######
|
|
.byte $3F ; 6E92 ..######
|
|
.byte $0F ; 6E93 ....####
|
|
.byte $0F ; 6E94 ....####
|
|
|
|
.byte $FC ; 6E95 ######..
|
|
.byte $FC ; 6E96 ######..
|
|
.byte $3C ; 6E97 ..####..
|
|
.byte $FC ; 6E98 ######..
|
|
.byte $F0 ; 6E99 ####....
|
|
.byte $F0 ; 6E9A ####....
|
|
.byte $C0 ; 6E9B ##......
|
|
.byte $C0 ; 6E9C ##......
|
|
|
|
|
|
; COMCEN, facing 2 (left) - glyph index 18. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphComcenLeft:
|
|
.byte $00 ; 6E9D ........
|
|
.byte $00 ; 6E9E ........
|
|
.byte $FF ; 6E9F ########
|
|
.byte $FF ; 6EA0 ########
|
|
.byte $FF ; 6EA1 ########
|
|
.byte $FF ; 6EA2 ########
|
|
.byte $FF ; 6EA3 ########
|
|
.byte $FF ; 6EA4 ########
|
|
|
|
.byte $00 ; 6EA5 ........
|
|
.byte $00 ; 6EA6 ........
|
|
.byte $C0 ; 6EA7 ##......
|
|
.byte $F0 ; 6EA8 ####....
|
|
.byte $FC ; 6EA9 ######..
|
|
.byte $FF ; 6EAA ########
|
|
.byte $3F ; 6EAB ..######
|
|
.byte $3F ; 6EAC ..######
|
|
|
|
.byte $FF ; 6EAD ########
|
|
.byte $FF ; 6EAE ########
|
|
.byte $FF ; 6EAF ########
|
|
.byte $FF ; 6EB0 ########
|
|
.byte $FF ; 6EB1 ########
|
|
.byte $FF ; 6EB2 ########
|
|
.byte $00 ; 6EB3 ........
|
|
.byte $00 ; 6EB4 ........
|
|
|
|
.byte $3F ; 6EB5 ..######
|
|
.byte $3F ; 6EB6 ..######
|
|
.byte $3F ; 6EB7 ..######
|
|
.byte $FC ; 6EB8 ######..
|
|
.byte $F0 ; 6EB9 ####....
|
|
.byte $C0 ; 6EBA ##......
|
|
.byte $00 ; 6EBB ........
|
|
.byte $00 ; 6EBC ........
|
|
|
|
|
|
; COMCEN, facing 3 (right) - glyph index 19. 32 bytes: TL, TR, BL, BR.
|
|
unitGlyphComcenRight:
|
|
.byte $00 ; 6EBD ........
|
|
.byte $00 ; 6EBE ........
|
|
.byte $03 ; 6EBF ......##
|
|
.byte $0F ; 6EC0 ....####
|
|
.byte $3F ; 6EC1 ..######
|
|
.byte $FF ; 6EC2 ########
|
|
.byte $FC ; 6EC3 ######..
|
|
.byte $FC ; 6EC4 ######..
|
|
|
|
.byte $00 ; 6EC5 ........
|
|
.byte $00 ; 6EC6 ........
|
|
.byte $FF ; 6EC7 ########
|
|
.byte $FF ; 6EC8 ########
|
|
.byte $FF ; 6EC9 ########
|
|
.byte $FF ; 6ECA ########
|
|
.byte $FF ; 6ECB ########
|
|
.byte $FF ; 6ECC ########
|
|
|
|
.byte $FC ; 6ECD ######..
|
|
.byte $FC ; 6ECE ######..
|
|
.byte $FC ; 6ECF ######..
|
|
.byte $3F ; 6ED0 ..######
|
|
.byte $0F ; 6ED1 ....####
|
|
.byte $03 ; 6ED2 ......##
|
|
.byte $00 ; 6ED3 ........
|
|
.byte $00 ; 6ED4 ........
|
|
|
|
.byte $FF ; 6ED5 ########
|
|
.byte $FF ; 6ED6 ########
|
|
.byte $FF ; 6ED7 ########
|
|
.byte $FF ; 6ED8 ########
|
|
.byte $FF ; 6ED9 ########
|
|
.byte $FF ; 6EDA ########
|
|
.byte $00 ; 6EDB ........
|
|
.byte $00 ; 6EDC ........
|
|
|
|
|
|
; padding6EDD - 35 zero bytes filling out the last sector of the main program to $6EFF. Nothing reads
|
|
; them; the first byte the game does use above this is the overlay window at $6F00.
|
|
padding6EDD:
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; 6EDD ........ unused filler to the end of the $0800-$6EFF image
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; 6EE5 ........
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; 6EED ........
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; 6EF5 ........
|
|
.byte $00,$00,$00 ; 6EFD ...
|