5335 lines
357 KiB
ArmAsm
5335 lines
357 KiB
ArmAsm
; ============================================================================
|
|
; $6F00-$87FF - random battlefield generator and setup phase (track 31 sectors 0-16 + track 32 sectors 0-7)
|
|
; ============================================================================
|
|
; Loaded by $1047. Generates the 40x40 map (river, hills, forest, end zones) from the game seed, derives
|
|
; the 5-character map id the two players compare, runs the unit setup screen and saves/loads game films.
|
|
; Shares its address range with the comcen screens overlay, which replaces it once the battle starts.
|
|
|
|
.setcpu "6502"
|
|
.include "c64.inc"
|
|
.include "zeropage.inc"
|
|
|
|
; ---- references to code/data outside this file ----
|
|
contourRuleTable := $0200
|
|
basicVectorHijack := $0300
|
|
play1Row07 := $0301
|
|
play1Row08 := $0302
|
|
play1Row10 := $0304
|
|
play1Row13 := $0307
|
|
storeRngSeedByte1 := $030F
|
|
play1Row22 := $0310
|
|
loadRngSeedByte2 := $0311
|
|
play1Row24 := $0312
|
|
storeRngSeedByte2 := $0313
|
|
play1Row26 := $0314
|
|
play1Row34 := $031C
|
|
play1Row42 := $0324
|
|
storeFontEndLo := $0325
|
|
storeFontEndHi := $0329
|
|
invertFontByteLoop := $032D
|
|
play2Grp02 := $032E
|
|
play2Grp06 := $0332
|
|
checksumXorConstant := $0400
|
|
play3Row42 := $0450
|
|
play4Col22 := $04A0
|
|
optionSummaryTemplate := $0636
|
|
optionWordTable := $064D
|
|
tileVariantMaskTable := $0770
|
|
D_077E := $077E
|
|
loadSectors := $0804
|
|
writeSectors := $0843
|
|
setLoadDest := $085C
|
|
startGameFromSetup := $0AC8
|
|
inputLockoutTimer := $0B7D
|
|
mapSeed := $0B95
|
|
mapSeed1 := $0B96
|
|
mapSeed2 := $0B97
|
|
prevMapSeed := $0B98
|
|
prevMapSeed1 := $0B99
|
|
prevMapSeed2 := $0B9A
|
|
filmPlaybackMode := $0B9B
|
|
linkSessionState := $0B9C
|
|
mapFlipFlag := $0B9D
|
|
prevMapFlipFlag := $0B9E
|
|
playerSide := $0B9F
|
|
setupPlayerSide := $0BA0
|
|
playsFirstFlag := $0BA1
|
|
savedPlaysFirstFlag := $0BA2
|
|
gameTypeOptions := $0BA3
|
|
prevGameTypeOptions := $0BA4
|
|
isSoloTrainer := $0BA5
|
|
diskLoadDepth := $0BA7
|
|
gameEndReason := $0BA8
|
|
mapNameText := $0BA9
|
|
mapNameTerminator := $0BAE
|
|
mapNameBackup := $0BAF
|
|
comcenUnitIndexBySide := $0C8A
|
|
showModemSetupPrompt := $0C8C
|
|
setJoystickRepeatDelay := $0D49
|
|
checkFirePressed := $0EB1
|
|
loadSectorsDelayed := $0F55
|
|
checkDiskId := $0FB1
|
|
L_100D := $100D
|
|
diskErrorExit := $1018
|
|
loadCommTailBuild2 := $1070
|
|
suspendCommForDiskAccess := $10CB
|
|
resumeCommAfterDiskAccess := $10D1
|
|
waitForGameDiskInserted := $10DC
|
|
clearMenuTextWindow := $156F
|
|
openCommLink := $1B5F
|
|
waitForConnection := $1B77
|
|
runMapMainLoop := $1FD0
|
|
D_206B := $206B
|
|
D_2084 := $2084
|
|
runTargetCursorLoop := $2185
|
|
getViewportOriginPointer := $25C7
|
|
readMapCell := $25FA
|
|
clearInfoPanel := $261C
|
|
clearScreenAndRedrawGameScreen := $2636
|
|
clearViewportTextArea := $26A1
|
|
drawViewport := $26C1
|
|
ownSideBranchOpcode := $2840
|
|
printOptionWord := $2A1A
|
|
positionCursorSprite := $3049
|
|
D_36D5 := $36D5
|
|
leftoverScanSideBranch := $379C
|
|
D_3B2C := $3B2C
|
|
D_3BD0 := $3BD0
|
|
initAllUnits := $3D8A
|
|
placeUnitOnMap := $3DDA
|
|
removeUnitFromMap := $3EBD
|
|
drawRecyclerOnMap := $3F27
|
|
initialFacingSideBranch := $3F9E
|
|
mirrorMapCoordinate := $3FA3
|
|
updateGameFrame := $40B2
|
|
L_4657 := $4657
|
|
zoneRowSideBranch := $4783
|
|
shuffleUnitOrder := $47ED
|
|
serviceCommandExchange := $4DA9
|
|
D_4DF4 := $4DF4
|
|
sendChooseSide := $553B
|
|
queueCmdBytePending := $55EC
|
|
queueCmd4 := $560A
|
|
queueCmd3 := $560F
|
|
queueCmd2 := $561F
|
|
sendOutgoingPacket := $562B
|
|
readyForNewGameHandshake := $572B
|
|
D_57C3 := $57C3
|
|
readFilmByte := $5874
|
|
writeFilmByte := $5893
|
|
copyPageUnderIo := $58B2
|
|
D_58C3 := $58C3
|
|
D_58C4 := $58C4
|
|
D_58C6 := $58C6
|
|
D_58C7 := $58C7
|
|
verifyFilmHeader := $58DE
|
|
waitForExchangeIdle := $5915
|
|
writeFilmSetupSnapshot := $5929
|
|
setFilmLimitToBufferEnd := $598D
|
|
initFilmStream := $5998
|
|
drawOverviewMap := $5AD7
|
|
D_5BF7 := $5BF7
|
|
clearOverviewWindow := $5E03
|
|
runMenuLoopWithFlag := $5FD1
|
|
unhighlightMenuRow := $60E5
|
|
toggleMenuRowHighlight := $60EA
|
|
groupEditorOwnSideBranch := $63E2
|
|
mapNameAlphabet := $890C
|
|
spriteEnableShadow := $9022
|
|
spriteBuildBuffer := $9025
|
|
newKeyEvent := $90EA
|
|
textCentreWidth := $90EF
|
|
selectedUnit := $90F8
|
|
currentScreen := $90FB
|
|
unitProcessOrder := $90FD
|
|
unitBaseOrder := $9164
|
|
gameClock := $91CB
|
|
menuItemCount := $91D4
|
|
menuSelectedItem := $91D5
|
|
menuFirstRow := $91D6
|
|
pickedCellCol := $91D8
|
|
pickedCellRow := $91D9
|
|
currentUnitCol := $91DA
|
|
currentUnitRow := $91DB
|
|
droneFuel := $9220
|
|
droneRadarOwnColour := $9224
|
|
droneRadarEnemyColour := $9225
|
|
overviewUnitPattern := $922B
|
|
comcenUnit := $9236
|
|
pendingRedrawGroup := $923A
|
|
pendingRedrawUnit := $923B
|
|
comcenBattleStatus := $9242
|
|
lastTypedKey := $9248
|
|
messageQueueHead := $9251
|
|
messageQueueTail := $9252
|
|
incomingChatLength := $9277
|
|
typedLineBuffer := $9278
|
|
chatState := $929C
|
|
lastOrderedGroupKey := $929D
|
|
pendingOwnCmdCount := $929F
|
|
persistentMessageId := $92A1
|
|
recyclerMode := $92A6
|
|
side1RecyclerMode := $92A7
|
|
recyclerCol := $92A8
|
|
side1RecyclerCol := $92A9
|
|
recyclerRow := $92AA
|
|
terrainPointsBySide := $92AC
|
|
dronesLeft := $92AE
|
|
opponentDronesLeft := $92AF
|
|
missilesLeft := $92B0
|
|
comcenSpeedClass := $92B4
|
|
menuBlinkState := $92C5
|
|
setupPhaseMode := $92C6
|
|
comcenStatusSnapshot := $92D6
|
|
stepIntervalFrames := $92E3
|
|
filmNameLength := $92E6
|
|
lastQuarterRound := $92EF
|
|
diskIdIndex := $92F0
|
|
setupToggleFlag := $92F3
|
|
lastQuarterFlag := $92FA
|
|
revealAllUnits := $92FC
|
|
defaultMainMenuItem := $92FD
|
|
soloTrainerAtGameStart := $92FE
|
|
printChar := $C01A
|
|
printNewline := $C041
|
|
bootToggleDataAck := $C07A
|
|
setCursorRow := $C097
|
|
updateCursorPointers := $C099
|
|
bootReceiveSectorData := $C0EF
|
|
printTableEntry := $C100
|
|
printStringCentred := $C121
|
|
printNextStringCentred := $C12B
|
|
printString := $C133
|
|
printStringAtPointer := $C137
|
|
printDecimal := $C17D
|
|
waitFrames := $C37F
|
|
clearMessageLine := $C39F
|
|
queueMessage := $C566
|
|
resetMessageQueue := $C6EF
|
|
showWaitingForOpponent := $C7C3
|
|
handleTextEntryKey := $C7D5
|
|
showWorkingMessage := $C853
|
|
playSound := $C89A
|
|
setMessageSlot := $C924
|
|
msgCustom := $CC19
|
|
exchangeFlags := $E01D
|
|
isLinkActive := $E03B
|
|
clearBattlefieldMap := $EC00
|
|
initTrainerAi := $EC0F
|
|
setViewOriginToHome := $EC1C
|
|
loadMapSeedIntoScenarioRng := $EC2B
|
|
makeMapPointSymmetric := $EC54
|
|
rotateMapAndMirrorCodes := $EC58
|
|
mirrorAllUnitCoordinates := $EC91
|
|
loadUnitStartTemplate := $ECF3
|
|
exchangePlayerNamesAndSettings := $ED43
|
|
measureRangeToTarget := $EE26
|
|
loadFilmStartSnapshot := $EE4A
|
|
toggleBattleDisplayMode := $EEDD
|
|
addRowDeltaToDroneRow := $EF77
|
|
diskNameSlot := $EFB6
|
|
D_F30B := $F30B
|
|
D_F333 := $F333
|
|
D_F334 := $F334
|
|
D_F600 := $F600
|
|
unitColTable := $F640
|
|
unitTypeTable := $F76C
|
|
side0ComcenType := $F79D
|
|
unitTerrainUnderAlias := $FA70
|
|
unitTerrainUnderTable := $FAF0
|
|
multiply8x8 := $FCCD
|
|
addRandomJitter := $FD1E
|
|
nextScenarioRandom := $FD4A
|
|
nextGameRandom := $FD5D
|
|
ownPlayerName := $FFDE
|
|
irqVectorHi := $FFFF
|
|
|
|
; Contents
|
|
; --------
|
|
; $6F04 generateBattlefieldMap entry point of the procedural battlefield generator.
|
|
; $6F36 runMapGeneratorPasses the eight terrain passes, entered by fall-through from
|
|
; $6F91 serviceLinkWhileGenerating lets the opponent link breathe during the long map build.
|
|
; $6F9E pickHillSummitSites decides how many hill summits the map gets and finds a blank cell for each.
|
|
; $701C findBlankSitesOnLanes gives every summit site from zp_2A to $0300-1 a column and a row.
|
|
; $708E advanceLaneIndex steps the lane cursor zp_2B down through the four shuffled lane columns in
|
|
; $7097 advanceRowCandidate steps the candidate-row cursor zp_29 down through $0325[0..zp_28],
|
|
; $70A0 shuffleLaneColumns builds a random permutation of 0-3 (identity, then four random transpositions)
|
|
; $70D9 paintHillSummits paints every site chosen by pickHillSummitSites as a blob of summit tiles.
|
|
; $710A growLowerHillsBesideSlopes second terrain layer.
|
|
; $71B0 growTerrainBlob the generic random blob painter used by every terrain pass.
|
|
; $72F6 checkAlignedBlockForForest constrains forest to whole 2x2 blocks on an even grid.
|
|
; $7340 placeForestClusters the last terrain pass.
|
|
; $73AD paintForestClusterSeeds paints eight small forest blobs covering the 4x2 cell block from
|
|
; $73D4 paintForestBlobAt grows one 4-cell forest blob at (zp_93,zp_92) if both coordinates are inside
|
|
; $73EE applyContourTiles draws the contour ring around a hill layer.
|
|
; $7444 applyContourRuleAtCell decides what one cell becomes, from which of its eight neighbours are
|
|
; $74FE clearMapMarkers Clean-up pass of the map generator: walks all 1600 cells of the 40x40
|
|
; $7548 generateRiver Draws the river across the whole battlefield, left edge to right edge, as the
|
|
; $759E chooseNextRiverSegment Picks the segment type of the next river cell into genTileType ($0312).
|
|
; $761C advanceRiverPosition Moves the river cursor one cell according to the segment just drawn: type 1
|
|
; $762E riverStepRight Step stub for every segment type that leaves the cell to the right (0, 2 and
|
|
; the
|
|
; $7632 riverStepUp Step stub for segment type 3, the elbow that turns the river upward: moves the
|
|
; $7636 riverStepDown Step stub for segment type 1, the elbow that turns the river downward: moves
|
|
; the
|
|
; $764A markRiverBanks Reserves the ground around the current river cell so the hill and forest passes
|
|
; $76AD mapNameEntryScreen The 'TYPE MAP NAME' page of the game setup.
|
|
; $7717 enterMapName Reads a five-character map name at column 4, row 16 and converts it into the
|
|
; $782D setMapNameCursor Moves the text cursor to the start of the map-name field: column 4 of the
|
|
; $7836 clearMapNameField Fills the five map-name characters $0BA9-$0BAD with spaces.
|
|
; $7841 printMapName Prints the five-character map name at its place on the setup screen.
|
|
; $7850 seedToMapName Spells a 24-bit map seed out as its five-character name: five times take the
|
|
; low
|
|
; $7872 randomTileVariant Picks a random visual variant of a terrain code so that large areas of the
|
|
; $78D1 startGameSession The whole 'start a game' sequence, from a cleared battle state to the first
|
|
; $7BB2 shuffleInitialUnitOrder builds the order in which the 100 units are processed each round.
|
|
; $7BC6 patchOwnSideBranches burns the local player's side into the engine's self-modifying
|
|
; $7BF9 sendPhaseSyncAndWait the 'I am ready' handshake that separates the setup phases.
|
|
; $7C1C waitForMapSeedCommand waits until the map seed sent by the other machine has arrived.
|
|
; $7C2E applyGameTypeDefaults loads the per-side game parameters of the chosen game type from the two
|
|
; $7CA2 removeUnitsUpToY marks unit slots Y down to 0 as 'not in this game': unitTypeTable ($F76C)
|
|
; $7CAE selectSideDefaultsTable points zp_48/zp_49 at the game-type default table of side X: $892C
|
|
; $7CBF readDefaultAndAdvanceRow reads the byte of the current 5-byte table row that belongs to the
|
|
; $7CD1 startFilmPlayback the game-start path taken when WATCH GAME FILM was chosen ($0B9B bit 7).
|
|
; $7D70 initUnitOrderLists fills the two 100-entry unit order lists unitBaseOrder ($9164) and
|
|
; $7D7D placeAllUnitsOnMap runs placeUnitOnMap for every unit 99 down to 0, which resets the unit's
|
|
; $7D8B setGameClockForType looks the length of the game up in roundsByGameType ($78C3: 63, 63, 31,
|
|
; $7D98 clearUnitsAndRecyclersFromMap takes every unit 99..0 off the map (removeUnitFromMap puts the
|
|
; $7DA7 clearRecyclerCell blanks side X's recycler cell: if that side has a recycler (recyclerCol bit
|
|
; $7DB8 waitForSpaceKey 'PRESS SPACE WHEN READY' wait used by the modem setup prompts.
|
|
; $7DE3 handleMainMenuChoice acts on the item the player picked in the main menu.
|
|
; $7E39 setModeFlagsAndStartGame shared tail of handleMainMenuChoice: X becomes isSoloTrainer $0BA5
|
|
; $7E4F checkDiskAndLoadFilmDirectory works out which disk is in the drive and loads its film
|
|
; $7E74 writeFilmDirectory writes the 512-byte film directory (30 entries of 16 bytes plus slack)
|
|
; $7E8B loadFilmDirectory reads track 18 sectors 2-3 (the 30 x 16-byte film names) into $0300-$04FF.
|
|
; $7EA0 drawFilmDirectoryPage draws one page of the film menu: the centred title 'SAVE GAME FILM' or
|
|
; $7F3F runLoadFilmMenu LOAD GAME FILM.
|
|
; $7FA6 setFilmDirectoryPagePtr points zp_48/zp_49 at the first directory entry of the current page
|
|
; $7FCB nextFilmDirectoryPage steps filmDirectoryPage on modulo 3 and redraws the list.
|
|
; $7FDD runSaveFilmMenu SAVE GAME FILM.
|
|
; $810C printFilmSlotEntry redraws the currently selected row of the film menu in colour A: moves the
|
|
; $8143 filmSlotToTrack turns the selected directory slot into the disk track its film occupies:
|
|
; $815E saveFilmToSlot writes the film in the buffer under I/O to the disk: rewrites the directory
|
|
; $81A5 promptInsertGameDisk end of the save/load film flow: shows 'INSERT GAME DISK AND PRESS
|
|
; $81B2 promptInsertDataDisk entry of promptInsertDiskAndWaitSpace with the word 'DATA' ($EFD0).
|
|
; $81B6 promptInsertDiskAndWaitSpace shows 'INSERT xxxx DISK AND PRESS SPACE.' and waits for the
|
|
; $81F4 checkFilmInMemory is the buffer under I/O at $D000 a valid game film? verifyFilmHeader
|
|
; $81FE waitFramesServicingComm waits about four seconds (240 IRQ frames counted down in zp_6C)
|
|
; $820B beginGameTypeSelection one-off prologue of the game-type selection screen, run only by the side that
|
|
; $821F runGameTypeMenu the GAME TYPE: selection screen.
|
|
; $8279 printGameTypeNamesLoop not a routine in variant A: this is the body of runGameTypeMenu's loop that
|
|
; $82A4 applyCustomPresetTail not a routine in variant A: the continuation inside runGameTypeMenu that ORs
|
|
; $82BF runMapTypeMenu second half of the setup menus: the STANDARD / CUSTOM map choice
|
|
; (gameTypeOptions
|
|
; $833C sendGameTypeCommand queues the 2-byte command $A4 (cmdSetGameType, handler $559C) carrying the
|
|
; $8343 printMenuTitle common menu prologue: preselects row X, prints the title string A/Y centred in
|
|
; the
|
|
; $8356 runMenuWidth10 runs the generic selector with a 10-character highlight bar; falls into
|
|
; $8358 runMenuWithWidth sets the highlight width, backs the window up one column so the bar has a
|
|
; leading
|
|
; $8364 resetSelectionAndView blanks the tactical viewport for a full-screen menu: sets $9242 bit 7 (the
|
|
; $8378 chooseRecycler RECYCLER setup for the scenarios that have one (game types 4-6).
|
|
; $843B printOptionWordLine prints option word A (0 FULL, 1 HALF, 2 NONE, from the 4-byte entries at $0652)
|
|
; $8441 clearInfoPanelSetRow sets the text and pad colours to 8 (orange), blanks the info panel window and
|
|
; $8451 editValueWithJoystick the generic numeric option editor of the custom setup.
|
|
; $84AA applyHandicapPoints last step of the game-start options.
|
|
; $84EC editTerrainPoints the CUSTOM option 'TERRAIN PTS' (0..2).
|
|
; $84F1 editTerrainPointsForSide entry into editTerrainPoints with the $92AC index already in A.
|
|
; $8511 editDronesAndMissiles the CUSTOM options 'DRONES' (0..9) and 'MISSILES' (0..36), edited one after
|
|
; $8518 editDronesAndMissilesBody not a real entry point in variant A: it is the JSR beginValueEdit inside
|
|
; $8547 runGameStartOptions the screen shown once per game after the setup has been saved and before unit
|
|
; $85BC editComcenSpeed the CUSTOM option 'COMCEN SPEED' (0..3).
|
|
; $85DB beginValueEdit common prologue of all four value editors: remembers the label string in two
|
|
; $8601 setValueEditWindow tail of beginValueEdit: marks the menu as a plain one (no unit attached) and
|
|
; sets
|
|
; $860B editOptionBitOnOff a two-row OFF/ON menu for one bit of gameTypeOptions, used by the game-type
|
|
; $862D runOnOffMenuAndApplyBit tail of editOptionBitOnOff that sets the item count and falls into the
|
|
; $8632 printOnOffMenuAndApplyBit inner tail of editOptionBitOnOff: prints the words OFF ($064D) and ON
|
|
; $8658 handleStarKeyToggle per-frame hook the main program calls while overlay A is resident and the game
|
|
; $8673 showToggleStateMessage shows the state of the '*' toggle.
|
|
; $86AA setDestroyModeMessage re-aims the '*' toggle at the unit-placement phase: the off-state persistent
|
|
; $86C3 showCustomSummaryMessage builds the status line 'CUSTOM:' [+ 'MAP,' when a named map is in use] +
|
|
; $8722 setPersistentMessage registers the string A/Y as message slot $1E (setMessageSlot writes the pointer
|
|
; $872E appendOnOffWord writes 'OFF,' (A = 0) or 'ON, ' (A != 0) into the template at $0636 starting at
|
|
; $8756 showScenarioBannerAndWaitForButton prints 'STANDARD' or 'CUSTOM' (from $CC11, chosen by $0BA3 bit
|
|
; $8795 printCentredTableEntry sets the centring field width to 13 columns and prints entry X of a table of
|
|
|
|
.org $6F00
|
|
|
|
|
|
; laneColumnTable - the four vertical 'lanes' of the battlefield along which hill summits are placed:
|
|
; columns 6, 15, 25 and 34 of the 40-column map. shuffleLaneColumns copies them into
|
|
; $0329-$032C in a random order so successive summits land in different parts of the map.
|
|
; (The label is only marked sub_6F00 because the comcen-screens overlay has code at this address.)
|
|
laneColumnTable:
|
|
.byte $06,$0F,$19,$22 ; 6F00 ..." lane columns 6, 15, 25, 34 (indexed 0-3 by the shuffled permutation)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; generateBattlefieldMap - entry point of the procedural battlefield generator. Derives the map-style
|
|
; flags for the chosen game type, decides whether the map is point-symmetric, puts 'WORKING...' on the
|
|
; status line, blanks all 1600 map cells to $40 and re-seeds the scenario RNG from the 24-bit map
|
|
; seed, then falls into runMapGeneratorPasses. The battlefield is never loaded from disk: seed + game
|
|
; type completely determine the terrain, which is why both machines produce the identical map and why
|
|
; a map can be named by its seed.
|
|
; In: gameTypeOptions $0BA3 bits 0-2 = game type 0-6, mapSeed $0B95-$0B97; needs the contour rule
|
|
; table at $0200 (track 18 s15) and the $EC00 comm-tail helpers resident
|
|
; Out: $032E map style, $032D symmetry flags, map $F000-$F63F built, scenario RNG $5A-$5C advanced,
|
|
; map name text $0BA9-$0BAD, generator scratch $0300-$032C, $0400-$04EF and $9025-$9028 clobbered
|
|
; Called from: manualLookupCheck ($09E0) to show the map the manual asks about, and from the setup
|
|
; screens of this overlay ($76AD map editor, $76FF, $7A02 game start, $7D23 film playback)
|
|
; Choose the map style from the game type: QB SNEAK and DEFENDER get special terrain.
|
|
; ----------------------------------------------------------------------
|
|
generateBattlefieldMap:
|
|
lda gameTypeOptions ; 6F04 gameTypeOptions from the settings block
|
|
and #$07 ; 6F07 bits 0-2 = game type (0 SCRIMAGE .. 6 DEFENDER)
|
|
tay ; 6F09 keep the type in Y for the two comparisons below
|
|
beq storeMapStyleFlags ; 6F0A type 0 (SCRIMAGE): A is already 0 = ordinary random map
|
|
lda #$40 ; 6F0C style $40 = DEFENDER map (river always, exactly 4 hills, 1-4 forest clusters)
|
|
cpy #$06 ; 6F0E game type 6 = DEFENDER?
|
|
beq storeMapStyleFlags ; 6F10 yes - keep style $40
|
|
lda #$80 ; 6F12 style $80 = QB SNEAK map (no river, at most 1 hill, 10 forest clusters)
|
|
cpy #$01 ; 6F14 game type 1 = QB SNEAK?
|
|
beq storeMapStyleFlags ; 6F16 yes - keep style $80
|
|
lda #$00 ; 6F18 every other game type: no special style
|
|
storeMapStyleFlags:
|
|
sta play2Grp02 ; 6F1A $032E mapGenGameFlags: bit 7 = QB SNEAK style, bit 6 = DEFENDER style
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A map whose name ends in '=' (seed bits 0-4 all set) is built point-symmetric.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$00 ; 6F1D assume an asymmetric map
|
|
lda mapSeed ; 6F1F low byte of the 24-bit map seed
|
|
and #$1F ; 6F22 bits 0-4 are the last character of the 5-letter map name
|
|
cmp #$1F ; 6F24 $1F = the alphabet's last symbol '='
|
|
bne storeMapSymmetryFlag ; 6F26 not a symmetric map code - leave the flag clear
|
|
ldy #$80 ; 6F28 bit 7 = build a point-symmetric battlefield
|
|
storeMapSymmetryFlag:
|
|
sty invertFontByteLoop ; 6F2A $032D mapSymmetryFlags (bit 6 is set later if the river crosses the centre) (also trainerPlaybook0200:play2Grp01)
|
|
jsr showWorkingMessage ; 6F2D show message 1 'WORKING...' - generating the map takes several seconds
|
|
jsr clearBattlefieldMap ; 6F30 fill the whole 40x40 map $F000-$F63F with blank terrain $40 (also trainerSetupEC00:mainGroupByPlanTable)
|
|
jsr loadMapSeedIntoScenarioRng; 6F33 scenario RNG $5A-$5C := mapSeed; returns the three seed bytes in A/Y/X
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMapGeneratorPasses - the eight terrain passes, entered by fall-through from
|
|
; generateBattlefieldMap. Order: map name from the seed; river with its keep-clear markers; hill
|
|
; summit sites; summit blobs; erase the $80 markers; upper contour ring; lower hill bodies; erase the
|
|
; $81 markers; lower contour ring; forest clusters. A symmetric map is finished by copying the top
|
|
; half onto the bottom half rotated 180 degrees, with a three-cell patch that joins the two halves of
|
|
; the river at the centre. The opponent link is serviced between passes.
|
|
; In: A/Y/X = the three map seed bytes, $032D symmetry flags, $032E map style, RNG $5A-$5C
|
|
; Out: the finished map at $F000-$F63F, map name text $0BA9-$0BAD, generator scratch $0300-$032C;
|
|
; ends with JMP resetMessageQueue, which clears 'WORKING...' and returns to the caller
|
|
; Called from: nothing in this overlay (fall-through only); the JSR $7125 the XREF lists belongs to
|
|
; the comcen-screens overlay that shares this address range
|
|
; Pass 1-2: name the map, then carve the river.
|
|
; ----------------------------------------------------------------------
|
|
runMapGeneratorPasses:
|
|
jsr seedToMapName ; 6F36 write the seed as the 5-character map name at $0BA9 (A/Y/X still hold it)
|
|
jsr generateRiver ; 6F39 carve the river ($55-$5C) and reserve its banks with the $80/$81 markers
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pass 3-5: hill summits, then the upper slope ring around them.
|
|
; ----------------------------------------------------------------------
|
|
jsr pickHillSummitSites ; 6F3C choose how many summits to place in the top half and on which lanes
|
|
jsr serviceLinkWhileGenerating; 6F3F give the modem/trainer link a turn - generation must not stall the exchange
|
|
jsr paintHillSummits ; 6F42 paint each summit as a blob of tile $65/$66
|
|
lda #$80 ; 6F45 $80 = the outer (wide) river keep-clear marker
|
|
jsr clearMapMarkers ; 6F47 turn those markers back into blank $40 before the contour pass reads them
|
|
jsr serviceLinkWhileGenerating; 6F4A service the link again
|
|
lda #$00 ; 6F4D contour offset 0: rule-table tiles are used unchanged ($41-$54)
|
|
sta play1Row42 ; 6F4F $0324 = amount subtracted from every rule-table tile code
|
|
lda #$65 ; 6F52 $65 = summit tile, terrain class 6
|
|
jsr applyContourTiles ; 6F54 draw the upper slope contour $41-$54 around everything of class >= 6
|
|
jsr serviceLinkWhileGenerating; 6F57 service the link
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pass 6-7: widen the hills into bodies and draw their lower contour ring.
|
|
; ----------------------------------------------------------------------
|
|
jsr growLowerHillsBesideSlopes; 6F5A grow lower-hill bodies ($61-$64) beside the upper slopes
|
|
lda #$81 ; 6F5D $81 = the inner river keep-clear marker
|
|
jsr clearMapMarkers ; 6F5F erase those markers too - the banks are now safe from the lower ring
|
|
lda #$20 ; 6F62 contour offset $20: rule tiles $41-$54 become the lower ring $21-$34
|
|
sta play1Row42 ; 6F64 $0324
|
|
lda #$61 ; 6F67 $61 = lower hill tile, terrain class 4
|
|
jsr applyContourTiles ; 6F69 draw the lower contour around everything of class >= 4
|
|
jsr serviceLinkWhileGenerating; 6F6C service the link
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pass 8: forest, then the optional 180-degree mirror for symmetric maps.
|
|
; ----------------------------------------------------------------------
|
|
jsr placeForestClusters ; 6F6F scatter the forest clusters ($5D-$60)
|
|
lda invertFontByteLoop ; 6F72 mapSymmetryFlags (also trainerPlaybook0200:play2Grp01)
|
|
bpl mapGeneratorDone ; 6F75 bit 7 clear - asymmetric map, nothing more to do
|
|
jsr makeMapPointSymmetric ; 6F77 overwrite the bottom half with the 180-degree rotation of the top half (also trainerSetupEC00:checkForTheBombGameType)
|
|
bit invertFontByteLoop ; 6F7A bit 6 = the river jogged up to row 19 at column 20 (set by chooseNextRiverSegment) (also trainerPlaybook0200:play2Grp01)
|
|
bvc mapGeneratorDone ; 6F7D the river does not meet itself at the centre - done
|
|
lda #$58 ; 6F7F $58 = river bend left/up
|
|
sta D_F333 ; 6F81 cell (19,20) = $F000 + 20*40 + 19
|
|
lda #$55 ; 6F84 $55 = river bend down/right
|
|
sta D_F30B ; 6F86 cell (19,19) = $F000 + 19*40 + 19
|
|
joinRiverClearCentre:
|
|
lda #$40 ; 6F89 $40 = blank terrain
|
|
sta D_F334 ; 6F8B cell (20,20): erase the mirrored stub so the two river halves join cleanly
|
|
mapGeneratorDone:
|
|
jmp resetMessageQueue ; 6F8E clear the 'WORKING...' message and return to generateBattlefieldMap's caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; serviceLinkWhileGenerating - lets the opponent link breathe during the long map build. Does nothing
|
|
; while a game film is being replayed (no opponent) or while the battle loop is running (the frame
|
|
; handler already services the link there); otherwise runs one command-exchange step.
|
|
; In: filmPlaybackMode $0B9B bit 7, gamePhase zp_B1 bit 7
|
|
; Out: whatever serviceCommandExchange ($4DA9) does - packets sent/received, messages updated
|
|
; Called from: runMapGeneratorPasses ($6F3F, $6F4A, $6F57, $6F6C) and the map-name screen ($773A)
|
|
; ----------------------------------------------------------------------
|
|
serviceLinkWhileGenerating:
|
|
lda filmPlaybackMode ; 6F91 filmPlaybackMode: bit 7 = watching a recorded game
|
|
bmi linkServiceNotNeeded ; 6F94 film playback - there is nobody on the other end
|
|
lda gamePhase ; 6F96 gamePhase: bit 7 set in the setup/menu phases
|
|
bpl linkServiceNotNeeded ; 6F98 bit 7 clear = the battle loop is running and already services the link
|
|
jmp serviceCommandExchange ; 6F9A exchange one round of commands with the opponent (tail call)
|
|
|
|
linkServiceNotNeeded:
|
|
rts ; 6F9D nothing to do
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pickHillSummitSites - decides how many hill summits the map gets and finds a blank cell for each.
|
|
; The count is rolled as (rnd AND 3) + (rnd AND 1), re-rolled to 1-4 if it came out exactly 1;
|
|
; DEFENDER always gets 4 and QB SNEAK at most 1. Each site gets a blob size of 2-5 cells, and four
|
|
; candidate rows in the range 7-14 are rolled. findBlankSitesOnLanes then places the top-half sites;
|
|
; afterwards the count is doubled, the candidate rows are mirrored to 39-row and the same routine runs
|
|
; again by fall-through, so every summit gets a point-mirrored twin in the bottom half.
|
|
; In: $032E map style bits 6/7, scenario RNG
|
|
; Out: $0300 = total number of sites placed, $0307[0..7] blob sizes, $0314[i]/$031C[i] site
|
|
; column/row, $0325[0..3] candidate rows, $0329-$032C lane columns; zp_18, zp_28-zp_2B
|
|
; Called from: runMapGeneratorPasses ($6F3C)
|
|
; Roll the number of summits for the top half of the map.
|
|
; ----------------------------------------------------------------------
|
|
pickHillSummitSites:
|
|
lda #$00 ; 6F9E start the top-half pass at site 0
|
|
sta scratch2A ; 6FA0 zp_2A = index of the site being placed; the first pass starts at site 0
|
|
lda #$04 ; 6FA2 DEFENDER gets exactly 4 summits
|
|
bit play2Grp02 ; 6FA4 mapGenGameFlags
|
|
bvs storeHillSiteCount ; 6FA7 bit 6 (DEFENDER style) - keep the fixed count of 4
|
|
jsr nextScenarioRandom ; 6FA9 first roll
|
|
and #$03 ; 6FAC 0-3
|
|
sta scratch18 ; 6FAE remember it
|
|
jsr nextScenarioRandom ; 6FB0 second roll
|
|
and #$01 ; 6FB3 0-1
|
|
clc ; 6FB5 prepare the add
|
|
adc scratch18 ; 6FB6 count = (rnd AND 3) + (rnd AND 1) = 0-4, biased towards 1-3
|
|
cmp #$01 ; 6FB8 did it come out as a single lone summit?
|
|
bne limitHillsForQbSneak ; 6FBA no - keep the count
|
|
jsr nextScenarioRandom ; 6FBC yes - re-roll rather than build a one-hill map
|
|
and #$03 ; 6FBF 0-3
|
|
clc ; 6FC1 prepare the add
|
|
adc #$01 ; 6FC2 1-4 summits instead
|
|
limitHillsForQbSneak:
|
|
bit play2Grp02 ; 6FC4 mapGenGameFlags
|
|
bpl storeHillSiteCount ; 6FC7 bit 7 clear - not QB SNEAK, keep the count
|
|
and #$01 ; 6FC9 QB SNEAK: 0 or 1 summit only, the field has to stay open
|
|
storeHillSiteCount:
|
|
sta basicVectorHijack ; 6FCB $0300 = summit count (the generator uses the $0200-$03FF setup block as scratch RAM) (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
cmp #$00 ; 6FCE redundant: every path above already set Z from A
|
|
bne setRowCandidateLimit ; 6FD0 at least one summit
|
|
noHillSitesAtAll:
|
|
jmp storeHillSitesPlaced ; 6FD2 no hills at all - store 0 through the common exit and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Roll one blob size per site (mirrored to the twin) and four candidate rows 7-14.
|
|
; ----------------------------------------------------------------------
|
|
setRowCandidateLimit:
|
|
sta cellRowB ; 6FD5 zp_28 = highest index of the candidate-row list...
|
|
dec cellRowB ; 6FD7 ...= count-1, so 0-3 (there are four candidate rows)
|
|
ldy #$00 ; 6FD9 Y walks the top-half sites 0..count-1
|
|
ldx basicVectorHijack ; 6FDB X walks their bottom-half twins, which start at index count (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
hillBlobSizeLoop:
|
|
jsr nextScenarioRandom ; 6FDE roll the size of this summit blob
|
|
and #$03 ; 6FE1 0-3
|
|
clc ; 6FE3 prepare the add
|
|
adc #$02 ; 6FE4 blob size 2-5 cells
|
|
sta play1Row13,y ; 6FE6 $0307[y] = size of top-half site y
|
|
sta play1Row13,x ; 6FE9 $0307[count+y] = the mirrored twin gets the same size
|
|
iny ; 6FEC next top-half site
|
|
inx ; 6FED next bottom-half twin
|
|
cpy basicVectorHijack ; 6FEE all sites given a size? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
bcc hillBlobSizeLoop ; 6FF1 one size per site
|
|
ldy #$03 ; 6FF3 four candidate rows, filled backwards 3..0
|
|
hillRowCandidateLoop:
|
|
jsr nextScenarioRandom ; 6FF5 roll one candidate row
|
|
and #$07 ; 6FF8 0-7
|
|
clc ; 6FFA prepare the add
|
|
adc #$07 ; 6FFB row 7-14: summits stay clear of the top edge and of the centre line
|
|
sta storeFontEndLo,y ; 6FFD $0325[y] = candidate row list used by findBlankSitesOnLanes (also trainerPlaybook0200:play1Row43)
|
|
sta play1Row34,y ; 7000 preload $031C[y] as well so an unplaced site still holds a sane row
|
|
dey ; 7003 next candidate-row slot
|
|
bpl hillRowCandidateLoop ; 7004 four candidate rows in all
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Place the top half, then mirror everything into the bottom half.
|
|
; ----------------------------------------------------------------------
|
|
jsr findBlankSitesOnLanes ; 7006 resolve the top-half sites onto the four lanes
|
|
asl basicVectorHijack ; 7009 double the placed count - each site also gets a point-mirrored twin (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
beq noHillSitesAtAll ; 700C nothing could be placed - store 0 and return
|
|
ldy #$03 ; 700E mirror the four candidate rows
|
|
mirrorHillRowCandidates:
|
|
lda play1Row34,y ; 7010 row of site y (the accepted row, or the preloaded candidate)
|
|
jsr mirrorMapCoordinate ; 7013 39 - row: the same distance from the bottom edge
|
|
sta storeFontEndLo,y ; 7016 candidate rows 25-32 for the bottom half (also trainerPlaybook0200:play1Row43)
|
|
dey ; 7019 next of the four candidate rows
|
|
bpl mirrorHillRowCandidates ; 701A falls into findBlankSitesOnLanes for sites count..2*count-1
|
|
|
|
; ----------------------------------------------------------------------
|
|
; findBlankSitesOnLanes - gives every summit site from zp_2A to $0300-1 a column and a row. The four
|
|
; lane columns are reshuffled first; each site then takes the next lane and the next candidate row and
|
|
; tries up to 21 jittered columns (+/-6 around the lane) until it finds a blank ($40) cell. If a whole
|
|
; row fails it moves on to the next candidate row, and if every row fails the site is dropped ($0300
|
|
; and zp_2A are decremented). Entered by JSR for the top half and by fall-through for the mirrored
|
|
; bottom half, which is why zp_2A is not reset here.
|
|
; In: zp_2A = first site index, $0300 = requested count, zp_28 = highest candidate-row index,
|
|
; $0325[] candidate rows, map at $F000
|
|
; Out: $0314[i] site column, $031C[i] site row, $0300 = sites actually placed, $0329-$032C lanes;
|
|
; zp_1D/zp_1E/zp_1F/zp_29/zp_2B scratch
|
|
; Called from: pickHillSummitSites ($7006, and by fall-through from $701A)
|
|
; ----------------------------------------------------------------------
|
|
findBlankSitesOnLanes:
|
|
jsr shuffleLaneColumns ; 701C randomise the order of the four lane columns 6/15/25/34
|
|
lda #$03 ; 701F lane index 3 = the last entry of the shuffled lane list
|
|
sta scratch2B ; 7021 zp_2B = lane index; advanceLaneIndex counts down 3,2,1,0,3,...
|
|
lda cellRowB ; 7023 highest valid candidate-row index (= count-1)
|
|
sta scanListIndex ; 7025 zp_29 = start at the top of the candidate-row list
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Per site: try candidate rows in turn until a blank cell is found.
|
|
; ----------------------------------------------------------------------
|
|
startSitePlacement:
|
|
lda scanListIndex ; 7027 current candidate-row index
|
|
sta scratch1F ; 7029 zp_1F remembers which candidate row this site started with
|
|
tryRowCandidate:
|
|
lda #$14 ; 702B 21 column attempts per candidate row (counter $14 down through 0)
|
|
sta scratch1E ; 702D zp_1E = attempts left
|
|
tryJitteredLaneColumn:
|
|
ldy scratch2B ; 702F current lane
|
|
lda storeFontEndHi,y ; 7031 its column: 6, 15, 25 or 34 (also trainerPlaybook0200:play1Row47)
|
|
ldy #$03 ; 7034 Y = 3 jitter steps, i.e. an offset of 0, +/-2, +/-4 or +/-6 columns
|
|
jitterLaneColumn:
|
|
jsr addRandomJitter ; 7036 scatter the site sideways around its lane column
|
|
cmp #$28 ; 7039 still inside the 40-column map?
|
|
bcs tryJitteredLaneColumn ; 703B off the map - roll again (a rejected roll does not use up an attempt)
|
|
ldy scratch2A ; 703D index of the site being placed
|
|
sta play1Row26,y ; 703F $0314[i] = provisional column
|
|
tax ; 7042 X = column for readMapCell
|
|
ldy scanListIndex ; 7043 current candidate-row slot
|
|
lda storeFontEndLo,y ; 7045 the candidate row for this attempt (also trainerPlaybook0200:play1Row43)
|
|
sta scratch1D ; 7048 zp_1D keeps it until the cell is known to be free
|
|
tay ; 704A Y = row for readMapCell
|
|
jsr readMapCell ; 704B zp_48/49 -> map cell (column,row)
|
|
ldy #$00 ; 704E offset 0 = the cell itself
|
|
lda (mapCellPtr),y ; 7050 what is already there
|
|
cmp #$40 ; 7052 $40 = blank terrain (the river and its markers are not)
|
|
beq hillSiteAccepted ; 7054 free - take this site
|
|
dec scratch1E ; 7056 one attempt used up
|
|
bpl tryJitteredLaneColumn ; 7058 try another column on the same row
|
|
jsr advanceRowCandidate ; 705A this row is hopeless - step to the next candidate row
|
|
lda scanListIndex ; 705D the candidate row we have wrapped round to
|
|
cmp scratch1F ; 705F have we cycled back to the row we started on?
|
|
rowCandidateWrapTest:
|
|
bne tryRowCandidate ; 7061 no - give that row its own 21 attempts
|
|
dec basicVectorHijack ; 7063 every candidate row failed: drop this site from the count (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
dec scratch2A ; 7066 and let the next site re-use its index
|
|
jmp advanceToNextHillSite ; 7068 site dropped - skip the accept path
|
|
|
|
hillSiteAccepted:
|
|
ldy scratch2A ; 706B index of the site just placed
|
|
lda scratch1D ; 706D the row that worked
|
|
sta play1Row34,y ; 706F $031C[i] = site row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Advance lane and row candidate so consecutive summits are spread out.
|
|
; ----------------------------------------------------------------------
|
|
advanceToNextHillSite:
|
|
jsr advanceLaneIndex ; 7072 next site uses the next lane column
|
|
lda scratch1F ; 7075 the candidate row this site started from
|
|
sta scanListIndex ; 7077 restore the candidate row this site started from...
|
|
jsr advanceRowCandidate ; 7079 ...then step it once, so the next site starts on a different row
|
|
inc scratch2A ; 707C next site index
|
|
lda scratch2A ; 707E site index just used
|
|
cmp basicVectorHijack ; 7080 all requested sites handled? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
bcs storeHillSitesPlaced ; 7083 all sites handled - store the real count
|
|
placeNextHillSite:
|
|
jmp startSitePlacement ; 7085 place the next site
|
|
|
|
storeHillSitesPlaced:
|
|
lda scratch2A ; 7088 zp_2A = number of sites actually placed
|
|
sta basicVectorHijack ; 708A $0300 corrected, so the painters only walk real sites (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
hillSitesDone:
|
|
rts ; 708D returns with $0300 = the number of sites that really exist
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; advanceLaneIndex - steps the lane cursor zp_2B down through the four shuffled lane columns in
|
|
; $0329-$032C, wrapping 0 back to 3.
|
|
; In: zp_2B
|
|
; Out: zp_2B = next lane index 0-3
|
|
; Called from: findBlankSitesOnLanes ($7072)
|
|
; ----------------------------------------------------------------------
|
|
advanceLaneIndex:
|
|
dec scratch2B ; 708E next lane
|
|
bpl laneIndexDone ; 7090 still a valid index
|
|
lda #$03 ; 7092 lane index 3
|
|
sta scratch2B ; 7094 wrap round to the last of the four lanes
|
|
laneIndexDone:
|
|
rts ; 7096 return the wrapped lane index
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; advanceRowCandidate - steps the candidate-row cursor zp_29 down through $0325[0..zp_28], wrapping 0
|
|
; back to zp_28 (= summit count - 1).
|
|
; In: zp_29, zp_28
|
|
; Out: zp_29 = next candidate-row index
|
|
; Called from: findBlankSitesOnLanes ($705A, $7079)
|
|
; ----------------------------------------------------------------------
|
|
advanceRowCandidate:
|
|
dec scanListIndex ; 7097 next candidate row
|
|
bpl rowCandidateDone ; 7099 still a valid index
|
|
lda cellRowB ; 709B highest candidate index
|
|
sta scanListIndex ; 709D wrap round to the top of the list
|
|
rowCandidateDone:
|
|
rts ; 709F return the wrapped candidate-row index
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; shuffleLaneColumns - builds a random permutation of 0-3 (identity, then four random transpositions)
|
|
; in the sprite build buffer $9025-$9028, which is free while the map is generated, and writes
|
|
; laneColumnTable[perm[y]] into $0329-$032C. Summits are therefore distributed over the four vertical
|
|
; lanes of the map in a different order for every map.
|
|
; In: scenario RNG, laneColumnTable $6F00
|
|
; Out: $0329-$032C = the four lane columns in random order; $9025-$9028 and zp_18 clobbered
|
|
; Called from: findBlankSitesOnLanes ($701C)
|
|
; ----------------------------------------------------------------------
|
|
shuffleLaneColumns:
|
|
lda #$03 ; 70A0 load the swap count
|
|
sta scratch18 ; 70A2 zp_18 = swap counter: 3,2,1,0 = four swaps
|
|
tay ; 70A4 Y = 3
|
|
laneIdentityLoop:
|
|
tya ; 70A5 A = Y
|
|
sta spriteBuildBuffer,y ; 70A6 $9025[y] = y: start from the identity permutation
|
|
dey ; 70A9 next slot
|
|
bpl laneIdentityLoop ; 70AA four entries 3..0
|
|
laneSwapLoop:
|
|
jsr nextScenarioRandom ; 70AC pick the first index...
|
|
and #$03 ; 70AF 0-3
|
|
tay ; 70B1 Y = first slot to swap
|
|
jsr nextScenarioRandom ; 70B2 ...and the second (may be the same, which is a no-op swap)
|
|
and #$03 ; 70B5 0-3
|
|
tax ; 70B7 X = second slot to swap (may be the same)
|
|
lda spriteBuildBuffer,y ; 70B8 perm[y]
|
|
pha ; 70BB save it on the stack
|
|
lda spriteBuildBuffer,x ; 70BC perm[x]
|
|
sta spriteBuildBuffer,y ; 70BF perm[y] = perm[x]
|
|
pla ; 70C2 the old perm[y]
|
|
sta spriteBuildBuffer,x ; 70C3 exchange perm[y] and perm[x]
|
|
dec scratch18 ; 70C6 one swap done
|
|
bpl laneSwapLoop ; 70C8 four swaps in all
|
|
ldy #$03 ; 70CA Y = 3: fill the lane list backwards
|
|
laneColumnFillLoop:
|
|
ldx spriteBuildBuffer,y ; 70CC permuted index 0-3
|
|
lda laneColumnTable,x ; 70CF lane column 6, 15, 25 or 34
|
|
sta storeFontEndHi,y ; 70D2 $0329-$032C = shuffled lane columns (also trainerPlaybook0200:play1Row47)
|
|
dey ; 70D5 next lane slot
|
|
bpl laneColumnFillLoop ; 70D6 four lanes
|
|
rts ; 70D8 the four lane columns are ready
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; paintHillSummits - paints every site chosen by pickHillSummitSites as a blob of summit tiles. Each
|
|
; blob is 2-5 cells of tile $65 (randomTileVariant may pick $66) grown from the site with a horizontal
|
|
; spread of 200/256 and a vertical spread of 32/256, so summits are wide and flat rather than round.
|
|
; Does nothing when no sites were placed.
|
|
; In: $0300 site count, $0307[] blob sizes, $0314[]/$031C[] site coordinates
|
|
; Out: summit tiles written to the map; $0301 = $65, $0304 = 0, zp_2A, zp_A7/zp_A8
|
|
; Called from: runMapGeneratorPasses ($6F42)
|
|
; ----------------------------------------------------------------------
|
|
paintHillSummits:
|
|
lda basicVectorHijack ; 70D9 number of summit sites (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
beq summitsDone ; 70DC no hills on this map
|
|
lda #$00 ; 70DE clear A for the two stores below
|
|
sta play1Row10 ; 70E0 $0304 blob flags = 0: no 2x2 block constraint
|
|
sta scratch2A ; 70E3 start at site 0
|
|
lda #$65 ; 70E5 $65 = summit tile, terrain class 6
|
|
sta play1Row07 ; 70E7 $0301 = the tile growTerrainBlob paints
|
|
summitBlobLoop:
|
|
ldy scratch2A ; 70EA index of the site being painted
|
|
lda play1Row26,y ; 70EC site column
|
|
sta blobOriginCol ; 70EF zp_A7 = blob origin column
|
|
lda play1Row34,y ; 70F1 site row
|
|
sta blobOriginRow ; 70F4 zp_A8 = blob origin row
|
|
lda play1Row13,y ; 70F6 A = 2-5 cells to place
|
|
ldx #$C8 ; 70F9 X = horizontal spread 200/256 - summits stretch east/west
|
|
ldy #$20 ; 70FB Y = vertical spread 32/256 - and stay thin north/south
|
|
jsr growTerrainBlob ; 70FD paint this summit
|
|
inc scratch2A ; 7100 next site
|
|
lda scratch2A ; 7102 site index just painted
|
|
cmp basicVectorHijack ; 7104 all sites painted? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
bcc summitBlobLoop ; 7107 every site, top-half original and mirrored twin alike
|
|
summitsDone:
|
|
rts ; 7109 no more sites
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; growLowerHillsBesideSlopes - second terrain layer. Scans every interior cell (column and row 1-38)
|
|
; and, when a blank cell touches an upper slope tile (terrain class 5, $41-$54), grows a 5-cell blob
|
|
; of lower-hill tile $61 from it. The chance is 1 in 4 for the north and south neighbours and 1 in 2
|
|
; for west and east, so the hill bodies spread sideways like the summits. The first matching neighbour
|
|
; wins. Skipped entirely when the map has no hills.
|
|
; In: $0300 summit count, the map, terrainClassTable $077E, scenario RNG
|
|
; Out: lower-hill tiles written to the map; $0301 = $61, $0304 = 0, zp_A7/zp_A8 = the scan cell
|
|
; Called from: runMapGeneratorPasses ($6F5A)
|
|
; ----------------------------------------------------------------------
|
|
growLowerHillsBesideSlopes:
|
|
lda basicVectorHijack ; 710A were any summits placed? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
|
|
bne lowerHillScanInit ; 710D there are hills to widen
|
|
jmp lowerHillScanDone ; 710F no hills - nothing to widen
|
|
|
|
lowerHillScanInit:
|
|
lda #$00 ; 7112 clear A for the two stores below
|
|
sta play1Row10 ; 7114 $0304 blob flags = 0: no 2x2 block constraint
|
|
sta scratch2A ; 7117 clears zp_2A as a side effect; nothing in this routine reads it
|
|
lda #$61 ; 7119 $61 = lower hill tile ($61-$64 variants), terrain class 4
|
|
sta play1Row07 ; 711B $0301 = the tile growTerrainBlob paints
|
|
lda #$01 ; 711E row 1
|
|
sta blobOriginRow ; 7120 scan starts at row 1 (the outermost ring is never used)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Scan loop over the interior cells. zp_A7/zp_A8 hold the cell; zp_48/49 is set to the top-left
|
|
; corner of its 3x3 block so the neighbours are at fixed offsets $01 N, $28 W, $29 self, $2A E,
|
|
; $51 S.
|
|
; ----------------------------------------------------------------------
|
|
lowerHillRowStart:
|
|
lda #$01 ; 7122 column 1
|
|
sta blobOriginCol ; 7124 each row starts at column 1
|
|
lowerHillCellScan:
|
|
ldx blobOriginCol ; 7126 X = column...
|
|
dex ; 7128 ...minus 1
|
|
ldy blobOriginRow ; 7129 Y = row...
|
|
dey ; 712B ...minus 1
|
|
jsr readMapCell ; 712C zp_48/49 -> cell (col-1,row-1), the top-left of the 3x3 block
|
|
ldy #$29 ; 712F offset $29 = the cell itself
|
|
lda (mapCellPtr),y ; 7131 the terrain code of the scan cell
|
|
cmp #$40 ; 7133 only blank cells can start a lower hill
|
|
lowerHillCellOccupied:
|
|
bne lowerHillNextColumn ; 7135 occupied - move on
|
|
ldy #$01 ; 7137 offset $01 = north neighbour (col,row-1)
|
|
lda (mapCellPtr),y ; 7139 north neighbour's cell
|
|
bmi lowerHillTestWest ; 713B bit 7 set = a unit or a river keep-clear marker, not terrain
|
|
tay ; 713D use the terrain code as the class-table index
|
|
lda D_077E,y ; 713E terrain class of the north neighbour
|
|
lowerHillNorthClassTest:
|
|
cmp #$05 ; 7141 class 5 = upper slope contour $41-$54
|
|
bne lowerHillTestWest ; 7143 not a slope - try the west neighbour
|
|
jsr nextScenarioRandom ; 7145 roll for the north slope
|
|
cmp #$40 ; 7148 1 chance in 4 for a north slope
|
|
bcc growLowerHillHere ; 714A won - grow a hill body here
|
|
lowerHillTestWest:
|
|
ldy #$28 ; 714C offset $28 = west neighbour (col-1,row)
|
|
lda (mapCellPtr),y ; 714E west neighbour's cell
|
|
bmi lowerHillTestEast ; 7150 unit or marker - skip
|
|
tay ; 7152 use the terrain code as the class-table index
|
|
lda D_077E,y ; 7153 terrain class of the west neighbour
|
|
cmp #$05 ; 7156 upper slope?
|
|
bne lowerHillTestEast ; 7158 no - try the east neighbour
|
|
jsr nextScenarioRandom ; 715A roll for the west slope
|
|
cmp #$80 ; 715D 1 chance in 2 for a side slope - hills widen east/west
|
|
bcc growLowerHillHere ; 715F won - grow a hill body here
|
|
lowerHillTestEast:
|
|
ldy #$2A ; 7161 offset $2A = east neighbour (col+1,row)
|
|
lowerHillReadEast:
|
|
lda (mapCellPtr),y ; 7163 east neighbour's cell
|
|
bmi lowerHillTestSouth ; 7165 unit or marker - skip
|
|
tay ; 7167 use the terrain code as the class-table index
|
|
lda D_077E,y ; 7168 terrain class of the east neighbour
|
|
cmp #$05 ; 716B upper slope?
|
|
bne lowerHillTestSouth ; 716D no - try the south neighbour
|
|
jsr nextScenarioRandom ; 716F roll for the east slope
|
|
cmp #$80 ; 7172 1 chance in 2
|
|
bcc growLowerHillHere ; 7174 won - grow a hill body here
|
|
lowerHillTestSouth:
|
|
ldy #$51 ; 7176 offset $51 = south neighbour (col,row+1)
|
|
lda (mapCellPtr),y ; 7178 south neighbour's cell
|
|
bmi lowerHillNextColumn ; 717A unit or marker - give up on this cell
|
|
tay ; 717C use the terrain code as the class-table index
|
|
lda D_077E,y ; 717D terrain class of the south neighbour
|
|
cmp #$05 ; 7180 upper slope?
|
|
bne lowerHillNextColumn ; 7182 no - nothing to do at this cell
|
|
jsr nextScenarioRandom ; 7184 roll for the south slope
|
|
cmp #$40 ; 7187 1 chance in 4 (inverted test: falls through to grow)
|
|
bcs lowerHillNextColumn ; 7189 lost the roll
|
|
growLowerHillHere:
|
|
ldx #$40 ; 718B X = horizontal spread 64/256
|
|
ldy #$10 ; 718D Y = vertical spread 16/256 - the body hugs the slope
|
|
lda #$05 ; 718F A = 5 cells
|
|
jsr growTerrainBlob ; 7191 zp_A7/zp_A8 already hold the scan cell = the blob origin
|
|
lowerHillNextColumn:
|
|
inc blobOriginCol ; 7194 next column
|
|
lda blobOriginCol ; 7196 the column just finished
|
|
cmp #$27 ; 7198 columns 1-38 only
|
|
bcs lowerHillNextRow ; 719A row finished
|
|
jmp lowerHillCellScan ; 719C scan the next cell of this row
|
|
|
|
lowerHillNextRow:
|
|
inc blobOriginRow ; 719F next row
|
|
lda blobOriginRow ; 71A1 the row just finished
|
|
cmp #$27 ; 71A3 rows 1-38 only
|
|
bcs lowerHillScanDone ; 71A5 whole map scanned
|
|
jmp lowerHillRowStart ; 71A7 scan the next row
|
|
|
|
lowerHillScanDone:
|
|
rts ; 71AA the whole interior has been scanned
|
|
|
|
|
|
; unusedBitTable71AB - $08,$04,$02,$01,$00. Dead in this overlay: nothing between $6F00 and $87FF
|
|
; reads it (the $71AC reference in XREF belongs to the comcen-screens overlay). It looks like a
|
|
; leftover direction-bit table for growTerrainBlob, which builds the same bits with immediate operands
|
|
; instead.
|
|
unusedBitTable71AB:
|
|
.byte $08,$04,$02,$01,$00 ; 71AB ..... unused direction-bit constants
|
|
|
|
; ----------------------------------------------------------------------
|
|
; growTerrainBlob - the generic random blob painter used by every terrain pass. Starting at
|
|
; (zp_A7,zp_A8) it performs a depth-first walk with an explicit 80-entry stack in page 4 ($0400
|
|
; columns, $0450 rows, $04A0 tried-direction flags) and paints tile $0301 - passed through
|
|
; randomTileVariant - on blank ($40) cells until A cells have been placed, the stack empties or the
|
|
; stack overflows. From each cell the four directions are tried once (flag bits 0 N, 1 W, 2 E, 3 S in
|
|
; zp_6B); a neighbour is taken when it is on the map, blank, and a scenario random byte is below the X
|
|
; (west/east) or Y (north/south) threshold, so X and Y control the horizontal and vertical spread out
|
|
; of 256. A newly pushed cell has the direction back towards the origin pre-marked as tried, which
|
|
; keeps the blob growing outwards. With $0304 bit 6 set (forest) a cell is only painted when
|
|
; checkAlignedBlockForForest approves its 2x2 block.
|
|
; In: A = cells to paint, X = horizontal spread 0-255, Y = vertical spread 0-255,
|
|
; zp_A7/zp_A8 = origin, $0301 = base tile, $0304 = blob flags
|
|
; Out: map cells painted; zp_18, zp_1A, zp_6B, zp_A5/zp_A6, zp_B2 stack level, zp_B3 painted count,
|
|
; $91DA/$91DB and $0400-$04EF clobbered. NOTE: $0400-$04EF is the page that also holds the
|
|
; copy-protection answer table at $0401 - harmless, because manualLookupCheck reads its answer
|
|
; before it calls the generator
|
|
; Called from: paintHillSummits ($70FD), growLowerHillsBesideSlopes ($7191), paintForestBlobAt ($73EA)
|
|
; Set up the walk: thresholds, counters and stack entry 0 = the origin cell.
|
|
; ----------------------------------------------------------------------
|
|
growTerrainBlob:
|
|
stx currentUnitCol ; 71B0 X = horizontal accept threshold, parked in the engine's currentUnitCol (free here)
|
|
sty currentUnitRow ; 71B3 Y = vertical accept threshold, parked in currentUnitRow
|
|
sta scratch18 ; 71B6 zp_18 = how many cells this blob should get
|
|
lda #$00 ; 71B8 clear A for the three stores below
|
|
sta glyphPtrHi ; 71BA zp_B3 = cells painted so far
|
|
sta play4Col22 ; 71BC $04A0[0] = tried-direction flags of the origin entry
|
|
sta glyphPtr ; 71BF zp_B2 = stack fill level (index of the next free entry)
|
|
lda play1Row10 ; 71C1 blob flags: bit 6 = only paint inside a clean 2x2 block
|
|
sta targetDirection ; 71C4 zp_6B = flags of the cell currently being expanded
|
|
lda blobOriginCol ; 71C6 the walk starts at the blob origin
|
|
sta viewOriginCol ; 71C8 zp_A5 = column of the current cell
|
|
sta checksumXorConstant ; 71CA $0400[0] = origin column (also trainerPlaybook0200:play3Col12)
|
|
lda blobOriginRow ; 71CD origin row
|
|
sta viewOriginRow ; 71CF zp_A6 = row of the current cell
|
|
sta play3Row42 ; 71D1 $0450[0] = origin row
|
|
jsr getViewportOriginPointer; 71D4 zp_48/49 -> the origin cell (copes with negative coordinates)
|
|
ldy #$00 ; 71D7 offset 0 = the cell itself
|
|
jmp blobPaintCell ; 71D9 paint the origin, then push it as the first stack entry
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pop the top stack entry and try its four directions in turn. zp_48/49 is aimed one row above the
|
|
; cell, so N/W/E/S are at offsets $00/$27/$29/$50.
|
|
; ----------------------------------------------------------------------
|
|
blobPopStack:
|
|
dec glyphPtr ; 71DC drop the fill level to the entry on top of the stack
|
|
bpl blobResumeCell ; 71DE there is still an entry to expand
|
|
jmp blobFinished ; 71E0 stack empty - this blob is finished
|
|
|
|
blobResumeCell:
|
|
ldy glyphPtr ; 71E3 y = the entry now on top of the stack
|
|
lda play4Col22,y ; 71E5 $04A0[y] = directions already tried from this cell
|
|
ora play1Row10 ; 71E8 re-apply the global blob flags (bit 6 survives every push/pop)
|
|
sta targetDirection ; 71EB zp_6B = the flags of the cell being expanded
|
|
ldx checksumXorConstant,y ; 71ED $0400[y] = its column (also trainerPlaybook0200:play3Col12)
|
|
lda play3Row42,y ; 71F0 $0450[y] = its row
|
|
tay ; 71F3 A -> Y so it can be decremented
|
|
dey ; 71F4 aim one row above the cell...
|
|
sty viewOriginRow ; 71F5 ...so the north neighbour sits at offset 0
|
|
stx viewOriginCol ; 71F7 zp_A5 = the cell's column
|
|
jsr getViewportOriginPointer; 71F9 zp_48/49 -> (column, row-1)
|
|
lda targetDirection ; 71FC tried-direction flags
|
|
and #$01 ; 71FE bit 0 = north already tried
|
|
bne blobTryWest ; 7200 yes - go on to the west neighbour
|
|
lda targetDirection ; 7202 tried-direction flags
|
|
blobMarkNorthTried:
|
|
ora #$01 ; 7204 bit 0 = north
|
|
sta targetDirection ; 7206 mark north as tried
|
|
lda viewOriginRow ; 7208 row-1 above the top edge of the map?
|
|
bmi blobTryWest ; 720A yes - try west
|
|
ldy #$00 ; 720C offset $00 = north neighbour
|
|
lda (mapCellPtr),y ; 720E the north neighbour's cell
|
|
cmp #$40 ; 7210 only blank terrain can be painted
|
|
bne blobTryWest ; 7212 not blank - cannot grow there
|
|
jsr nextScenarioRandom ; 7214 roll for the vertical spread
|
|
cmp currentUnitRow ; 7217 accept while the random byte is below the vertical spread
|
|
bcs blobTryWest ; 721A lost the roll - try west
|
|
blobGrowWest:
|
|
jmp blobPaintCell ; 721C grow into the north neighbour (zp_A5/zp_A6 and Y already point at it)
|
|
|
|
blobTryWest:
|
|
inc viewOriginRow ; 721F undo the -1: back to the cell's own row
|
|
blobStepWest:
|
|
dec viewOriginCol ; 7221 step one column west
|
|
lda targetDirection ; 7223 tried-direction flags
|
|
and #$02 ; 7225 bit 1 = west already tried
|
|
bne blobTryEast ; 7227 west already tried from this cell
|
|
lda targetDirection ; 7229 tried-direction flags
|
|
ora #$02 ; 722B bit 1 = west
|
|
sta targetDirection ; 722D mark west as tried
|
|
lda viewOriginCol ; 722F column-1 off the left edge?
|
|
bmi blobTryEast ; 7231 column -1 is off the map
|
|
ldy #$27 ; 7233 offset $27 = west neighbour (-1 column, +1 row from the pointer)
|
|
lda (mapCellPtr),y ; 7235 the west neighbour's cell
|
|
cmp #$40 ; 7237 blank?
|
|
bne blobTryEast ; 7239 not blank
|
|
jsr nextScenarioRandom ; 723B roll for the horizontal spread
|
|
cmp currentUnitCol ; 723E accept while the random byte is below the horizontal spread
|
|
bcc blobGrowWest ; 7241 grow into the west neighbour
|
|
blobTryEast:
|
|
inc viewOriginCol ; 7243 back to the cell's own column...
|
|
inc viewOriginCol ; 7245 two columns east: the cell's column + 1
|
|
lda targetDirection ; 7247 tried-direction flags
|
|
and #$04 ; 7249 bit 2 = east already tried
|
|
bne blobTrySouth ; 724B east already tried
|
|
lda targetDirection ; 724D tried-direction flags
|
|
ora #$04 ; 724F bit 2 = east
|
|
sta targetDirection ; 7251 mark east as tried
|
|
lda viewOriginCol ; 7253 the column the east neighbour sits in
|
|
cmp #$28 ; 7255 column 40 is off the right edge
|
|
bcs blobTrySouth ; 7257 column 40 is off the map
|
|
ldy #$29 ; 7259 offset $29 = east neighbour (+1 column, +1 row)
|
|
lda (mapCellPtr),y ; 725B the east neighbour's cell
|
|
blobTestEastBlank:
|
|
cmp #$40 ; 725D blank?
|
|
bne blobTrySouth ; 725F not blank
|
|
jsr nextScenarioRandom ; 7261 roll for the horizontal spread
|
|
cmp currentUnitCol ; 7264 horizontal spread again
|
|
bcc blobPaintCell ; 7267 grow into the east neighbour
|
|
blobTrySouth:
|
|
dec viewOriginCol ; 7269 back to the cell's own column
|
|
inc viewOriginRow ; 726B step one row south
|
|
lda targetDirection ; 726D tried-direction flags
|
|
and #$08 ; 726F bit 3 = south already tried
|
|
blobSouthAlreadyTried:
|
|
bne blobCellExhausted ; 7271 south already tried
|
|
lda targetDirection ; 7273 tried-direction flags
|
|
ora #$08 ; 7275 bit 3 = south
|
|
sta targetDirection ; 7277 mark south as tried
|
|
lda viewOriginRow ; 7279 the row the south neighbour sits in
|
|
cmp #$28 ; 727B row 40 is off the bottom edge
|
|
bcs blobCellExhausted ; 727D row 40 is off the map
|
|
ldy #$50 ; 727F offset $50 = south neighbour (+2 rows)
|
|
lda (mapCellPtr),y ; 7281 the south neighbour's cell
|
|
cmp #$40 ; 7283 blank?
|
|
bne blobCellExhausted ; 7285 not blank
|
|
jsr nextScenarioRandom ; 7287 roll for the vertical spread
|
|
blobTestSouthChance:
|
|
cmp currentUnitRow ; 728A vertical spread again
|
|
bcc blobPaintCell ; 728D grow into the south neighbour
|
|
blobCellExhausted:
|
|
jmp blobPopStack ; 728F all four directions used up - pop the next cell
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Paint the accepted cell (Y = its offset from zp_48/49) and push it on the stack.
|
|
; ----------------------------------------------------------------------
|
|
blobPaintCell:
|
|
lda (mapCellPtr),y ; 7292 the candidate cell
|
|
cmp #$40 ; 7294 blank terrain?
|
|
bne blobPushCell ; 7296 not blank (only possible for the origin) - push it without painting
|
|
bit targetDirection ; 7298 bit 6 = the 2x2 forest constraint
|
|
bvc blobWriteTile ; 729A bit 6 clear - paint unconditionally
|
|
jsr checkAlignedBlockForForest; 729C forest: the aligned 2x2 block must contain only blank and forest
|
|
bcs blobPushCell ; 729F block rejected - do not paint, but still expand from here
|
|
blobWriteTile:
|
|
lda play1Row07 ; 72A1 base tile for this pass
|
|
jsr randomTileVariant ; 72A4 add a random variant (Y is preserved through a self-modified LDY at $7891+1)
|
|
sta (mapCellPtr),y ; 72A7 paint the cell
|
|
inc glyphPtrHi ; 72A9 one more cell of this blob placed
|
|
lda glyphPtrHi ; 72AB cells placed so far
|
|
cmp scratch18 ; 72AD reached the requested number of cells?
|
|
bcs blobFinished ; 72AF yes - blob finished
|
|
blobPushCell:
|
|
ldy glyphPtr ; 72B1 y = the parent entry that is being expanded
|
|
blobSaveParentFlags:
|
|
lda targetDirection ; 72B3 the parent's flags with the direction just used marked as tried
|
|
sta play4Col22,y ; 72B5 $04A0[parent] = its updated tried-direction flags
|
|
iny ; 72B8 the child entry goes one slot higher
|
|
lda viewOriginCol ; 72B9 the accepted cell's column
|
|
sta checksumXorConstant,y ; 72BB $0400[child] = column (also trainerPlaybook0200:play3Col12)
|
|
lda viewOriginRow ; 72BE and its row
|
|
sta play3Row42,y ; 72C0 $0450[child] = row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pre-mark the direction back towards the blob origin as already tried, so a blob never folds back on
|
|
; itself.
|
|
; ----------------------------------------------------------------------
|
|
lda #$00 ; 72C3 clear A
|
|
sta scratch1A ; 72C5 zp_1A = the child's preset flags
|
|
lda #$02 ; 72C7 $02 = 'west already tried'
|
|
ldx viewOriginCol ; 72C9 the child's column
|
|
cpx blobOriginCol ; 72CB child column against the blob origin column
|
|
beq blobRowBias ; 72CD same column - no horizontal bias
|
|
bcs blobStoreColumnBias ; 72CF child is east of the origin - block growth back west
|
|
lda #$04 ; 72D1 $04 = 'east already tried' (child is west of the origin)
|
|
blobStoreColumnBias:
|
|
sta scratch1A ; 72D3 horizontal bias
|
|
blobRowBias:
|
|
lda #$01 ; 72D5 $01 = 'north already tried'
|
|
ldx viewOriginRow ; 72D7 the child's row
|
|
cpx blobOriginRow ; 72D9 child row against the blob origin row
|
|
beq blobStoreChildFlags ; 72DB same row - no vertical bias
|
|
bcs blobMergeRowBias ; 72DD child is below the origin - block growth back north
|
|
lda #$08 ; 72DF $08 = 'south already tried' (child is above the origin)
|
|
blobMergeRowBias:
|
|
ora scratch1A ; 72E1 combine with the horizontal bias
|
|
sta scratch1A ; 72E3 zp_1A = the child's preset tried-direction flags
|
|
blobStoreChildFlags:
|
|
lda scratch1A ; 72E5 the child's preset tried-direction flags
|
|
sta play4Col22,y ; 72E7 $04A0[child] = its preset flags
|
|
iny ; 72EA point one past the child entry
|
|
sty glyphPtr ; 72EB zp_B2 = new fill level = child + 1
|
|
cpy #$50 ; 72ED 80 entries is all that fits ($0400/$0450/$04A0 x 80 = $0400-$04EF)
|
|
bcc blobExpandChild ; 72EF still room on the stack
|
|
rts ; 72F1 stack full - abandon this blob
|
|
|
|
blobExpandChild:
|
|
jmp blobPopStack ; 72F2 expand the cell just pushed
|
|
|
|
blobFinished:
|
|
rts ; 72F5 blob finished: cell count reached, or the stack ran dry
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkAlignedBlockForForest - constrains forest to whole 2x2 blocks on an even grid. It takes the
|
|
; block containing (zp_A5,zp_A6) - both coordinates rounded down to even - and returns C = 0 only if
|
|
; all four cells are terrain class 0 (blank $40) or class 2 (forest $5D-$60). On success zp_48/49 is
|
|
; re-pointed at the caller's own cell with Y = 0, so the caller can store straight through it. Note
|
|
; that the river keep-clear markers $80/$81 index past terrainClassTable into the zero filler at
|
|
; $07F3, so they read as class 0 here; growTerrainBlob's own $40 test still refuses to paint them.
|
|
; In: zp_A5/zp_A6 = the cell, map, terrainClassTable $077E
|
|
; Out: C = 0 accept / C = 1 reject; zp_48/49, A/X/Y clobbered
|
|
; Called from: growTerrainBlob ($729C) when $0304 bit 6 is set
|
|
; ----------------------------------------------------------------------
|
|
checkAlignedBlockForForest:
|
|
lda viewOriginCol ; 72F6 column of the cell about to be painted
|
|
and #$FE ; 72F8 round the column down to the even grid
|
|
tax ; 72FA X = block column
|
|
lda viewOriginRow ; 72FB its row
|
|
and #$FE ; 72FD round the row down as well
|
|
tay ; 72FF Y = block row
|
|
forestBlockTestNw:
|
|
jsr readMapCell ; 7300 A = the block's top-left cell, zp_48/49 -> it, Y = 0
|
|
tax ; 7303 use the terrain code as the class-table index
|
|
lda D_077E,x ; 7304 terrain class of that cell
|
|
beq forestBlockTestNe ; 7307 class 0 = blank: acceptable
|
|
cmp #$02 ; 7309 class 2 = forest: also acceptable
|
|
bne forestBlockReject ; 730B anything else (river, hill, contour, unit) rejects the block
|
|
forestBlockTestNe:
|
|
iny ; 730D offset 1 = top-right cell
|
|
lda (mapCellPtr),y ; 730E the top-right cell of the block
|
|
forestBlockClassNe:
|
|
tax ; 7310 use the terrain code as the class-table index
|
|
forestBlockLookupNe:
|
|
lda D_077E,x ; 7311 its terrain class
|
|
beq forestBlockTestSw ; 7314 blank
|
|
forestBlockCompareNe:
|
|
cmp #$02 ; 7316 or forest
|
|
bne forestBlockReject ; 7318 not blank or forest - reject the block
|
|
forestBlockTestSw:
|
|
ldy #$28 ; 731A offset $28 = bottom-left cell
|
|
lda (mapCellPtr),y ; 731C the bottom-left cell of the block
|
|
forestBlockClassSw:
|
|
tax ; 731E use the terrain code as the class-table index
|
|
lda D_077E,x ; 731F its terrain class
|
|
beq forestBlockTestSe ; 7322 blank
|
|
cmp #$02 ; 7324 or forest
|
|
bne forestBlockReject ; 7326 not blank or forest - reject the block
|
|
forestBlockTestSe:
|
|
iny ; 7328 offset $29 = bottom-right cell
|
|
lda (mapCellPtr),y ; 7329 the bottom-right cell of the block
|
|
tax ; 732B use the terrain code as the class-table index
|
|
lda D_077E,x ; 732C its terrain class
|
|
beq forestBlockAccept ; 732F blank
|
|
cmp #$02 ; 7331 or forest
|
|
bne forestBlockReject ; 7333 not blank or forest - reject the block
|
|
forestBlockAccept:
|
|
ldx viewOriginCol ; 7335 the caller's own column
|
|
ldy viewOriginRow ; 7337 and row
|
|
jsr readMapCell ; 7339 re-point zp_48/49 at the caller's own cell (leaves Y = 0)
|
|
clc ; 733C C = 0: this block may be forested
|
|
forestBlockAcceptRts:
|
|
rts ; 733D C = 0 from $733C
|
|
|
|
forestBlockReject:
|
|
sec ; 733E C = 1: the block holds something else
|
|
rts ; 733F C = 1 set at $733E
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; placeForestClusters - the last terrain pass. Rolls a number of forest clusters (3-10 normally, with
|
|
; a 1-in-32 chance of no forest at all; exactly 10 for QB SNEAK; 1-4 for DEFENDER), gives each cluster
|
|
; a random centre at column/row 4-35 and scatters 6 seed points around it (+/-16 columns, +/-8 rows,
|
|
; re-rolled while off the map). Every seed becomes a 4x2 patch of small blobs through
|
|
; paintForestClusterSeeds. Blob flags $40 make all of it respect the 2x2 forest grid.
|
|
; In: $032E map style, scenario RNG
|
|
; Out: forest tiles $5D-$60 in the map; $0301, $0302, $0304, zp_2A cluster counter, zp_2B seed
|
|
; counter, zp_92-zp_95, zp_A5/zp_A6
|
|
; Called from: runMapGeneratorPasses ($6F6F)
|
|
; ----------------------------------------------------------------------
|
|
placeForestClusters:
|
|
lda #$40 ; 7340 blob flag bit 6: forest only grows inside a clean aligned 2x2 block
|
|
sta play1Row10 ; 7342 $0304 blob flags
|
|
lda #$41 ; 7345 $41 = upper contour tile
|
|
sta play1Row08 ; 7347 dead store: nothing in this overlay ever reads $0302 (XREF has no reader)
|
|
lda #$5D ; 734A $5D = forest tile ($5D-$60 variants), terrain class 2
|
|
storeForestTile:
|
|
sta play1Row07 ; 734C $0301 = the tile growTerrainBlob paints
|
|
lda #$09 ; 734F QB SNEAK: 9 -> ten clusters, a deliberately cluttered field
|
|
bit play2Grp02 ; 7351 mapGenGameFlags
|
|
bmi limitForestForDefender ; 7354 bit 7 = QB SNEAK style - keep the fixed count
|
|
jsr nextScenarioRandom ; 7356 roll the cluster count
|
|
and #$1F ; 7359 0-31
|
|
beq forestClustersDone ; 735B 1 chance in 32 of a completely bare map
|
|
and #$07 ; 735D 0-7...
|
|
clc ; 735F prepare the add
|
|
adc #$02 ; 7360 ...+2, so 3-10 clusters in total
|
|
limitForestForDefender:
|
|
bit play2Grp02 ; 7362 mapGenGameFlags
|
|
bvc storeForestClusterCount ; 7365 bit 6 clear - not DEFENDER
|
|
and #$03 ; 7367 DEFENDER: at most 3, i.e. 1-4 clusters (the map has to stay open)
|
|
storeForestClusterCount:
|
|
sta scratch2A ; 7369 zp_2A = clusters left minus one
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Per cluster: pick a centre, then scatter six seed patches around it.
|
|
; ----------------------------------------------------------------------
|
|
forestClusterLoop:
|
|
jsr nextScenarioRandom ; 736B roll the cluster centre column
|
|
forestCentreFromRandom:
|
|
and #$1F ; 736E 0-31
|
|
clc ; 7370 prepare the add
|
|
adc #$04 ; 7371 cluster centre column 4-35
|
|
sta lineEndCol ; 7373 zp_95 = centre column, the base for the column jitter
|
|
sta viewOriginRow ; 7375 vestigial: growTerrainBlob overwrites zp_A6 at once, and this is a column, not a row
|
|
jsr nextScenarioRandom ; 7377 roll the cluster centre row
|
|
and #$1F ; 737A 0-31
|
|
clc ; 737C prepare the add
|
|
adc #$04 ; 737D cluster centre row 4-35
|
|
sta lineEndRow ; 737F zp_94 = centre row, the base for the row jitter
|
|
sta viewOriginCol ; 7381 vestigial in the same way (a row written to the view-origin column)
|
|
lda #$05 ; 7383 six seeds per cluster (5 down through 0)
|
|
sta scratch2B ; 7385 zp_2B = six seed points per cluster (5 down through 0)
|
|
forestSeedRowRetry:
|
|
lda lineEndRow ; 7387 centre row
|
|
ldy #$04 ; 7389 Y = 4 jitter steps = an offset of 0, +/-2, +/-4, +/-6 or +/-8 rows
|
|
jsr addRandomJitter ; 738B scatter the seed row around the cluster centre
|
|
cmp #$28 ; 738E still on the map?
|
|
bcs forestSeedRowRetry ; 7390 off the map - roll the row again
|
|
sta lineCurRow ; 7392 zp_92 = seed row
|
|
forestSeedColumnRetry:
|
|
lda lineEndCol ; 7394 centre column
|
|
jitterForestSeedColumn:
|
|
ldy #$08 ; 7396 Y = 8 jitter steps = up to +/-16 columns, so clusters are wide
|
|
jsr addRandomJitter ; 7398 scatter the seed column around the cluster centre
|
|
cmp #$28 ; 739B still on the map?
|
|
bcs forestSeedColumnRetry ; 739D off the map - roll the column again
|
|
sta lineCurCol ; 739F zp_93 = seed column
|
|
jsr paintForestClusterSeeds ; 73A1 paint the 4x2 patch of blobs around this seed
|
|
dec scratch2B ; 73A4 one seed done
|
|
bpl forestSeedRowRetry ; 73A6 six seeds per cluster
|
|
dec scratch2A ; 73A8 one cluster done
|
|
bpl forestClusterLoop ; 73AA next cluster
|
|
forestClustersDone:
|
|
rts ; 73AC all forest placed
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; paintForestClusterSeeds - paints eight small forest blobs covering the 4x2 cell block from column
|
|
; zp_93-2 to zp_93+1 and rows zp_92 to zp_92+1, walking it in the order (c,r), (c+1,r), (c+1,r+1),
|
|
; (c,r+1), (c-2,r), (c-1,r), (c-1,r+1) and finally (c-2,r+1) by fall-through. Because the block is
|
|
; four columns wide it always covers two whole 2x2 forest blocks.
|
|
; In: zp_93 = seed column, zp_92 = seed row, $0301/$0304 from placeForestClusters
|
|
; Out: forest painted; on exit zp_93 = c-2 and zp_92 = r+1 (they are not restored - the caller
|
|
; re-rolls both for the next seed)
|
|
; Called from: placeForestClusters ($73A1)
|
|
; ----------------------------------------------------------------------
|
|
paintForestClusterSeeds:
|
|
jsr paintForestBlobAt ; 73AD seed (c,r)
|
|
forestSeedStepEast:
|
|
inc lineCurCol ; 73B0 -> column c+1
|
|
jsr paintForestBlobAt ; 73B2 (c+1,r)
|
|
inc lineCurRow ; 73B5 -> row r+1
|
|
forestSeedPaintSouthEast:
|
|
jsr paintForestBlobAt ; 73B7 (c+1,r+1)
|
|
dec lineCurCol ; 73BA -> column c
|
|
forestSeedPaintSouth:
|
|
jsr paintForestBlobAt ; 73BC (c,r+1)
|
|
dec lineCurCol ; 73BF -> column c-1
|
|
dec lineCurCol ; 73C1 -> column c-2
|
|
dec lineCurRow ; 73C3 -> row r
|
|
jsr paintForestBlobAt ; 73C5 (c-2,r)
|
|
inc lineCurCol ; 73C8 -> column c-1
|
|
jsr paintForestBlobAt ; 73CA (c-1,r)
|
|
inc lineCurRow ; 73CD -> row r+1
|
|
jsr paintForestBlobAt ; 73CF (c-1,r+1)
|
|
dec lineCurCol ; 73D2 -> column c-2, then fall through for the eighth seed (c-2,r+1)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; paintForestBlobAt - grows one 4-cell forest blob at (zp_93,zp_92) if both coordinates are inside the
|
|
; 40x40 map; out-of-range coordinates (including the $FF a DEC produced at column 0) are silently
|
|
; ignored. Spread is 220/256 in both axes, so the blobs are compact and round.
|
|
; In: zp_93 column, zp_92 row, $0301 tile, $0304 flags
|
|
; Out: up to 4 forest cells; zp_A7/zp_A8 = the blob origin
|
|
; Called from: paintForestClusterSeeds (all eight seeds, $73AD-$73D2 and by fall-through)
|
|
; ----------------------------------------------------------------------
|
|
paintForestBlobAt:
|
|
lda lineCurCol ; 73D4 seed column
|
|
cmp #$28 ; 73D6 columns 0-39 only
|
|
bcs forestSeedOffMap ; 73D8 off the map (or wrapped to $FF by a DEC) - ignore this seed
|
|
sta blobOriginCol ; 73DA zp_A7 = blob origin column
|
|
lda lineCurRow ; 73DC seed row
|
|
cmp #$28 ; 73DE rows 0-39 only
|
|
bcs forestSeedOffMap ; 73E0 off the map - ignore
|
|
sta blobOriginRow ; 73E2 zp_A8 = blob origin row
|
|
growForestSeedBlob:
|
|
ldy #$DC ; 73E4 Y = vertical spread 220/256
|
|
ldx #$DC ; 73E6 X = horizontal spread 220/256 - forest grows evenly in all directions
|
|
lda #$04 ; 73E8 A = 4 cells per seed
|
|
jsr growTerrainBlob ; 73EA grow the blob
|
|
forestSeedOffMap:
|
|
rts ; 73ED seed outside the map, or blob finished
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyContourTiles - draws the contour ring around a hill layer. A is the layer's interior tile ($65
|
|
; summit or $61 lower hill); zp_8E becomes its terrain class (6 or 4) and zp_91 the class of its ring
|
|
; (5 or 3). All 1600 cells are scanned in order: a cell that already holds a ring tile of this layer
|
|
; is erased to blank and re-evaluated, cells of the interior class or higher and river cells (class 1)
|
|
; are left alone, and everything else is handed to applyContourRuleAtCell. When that returns C = 1 it
|
|
; has changed a cell that earlier cells depended on, so the scan is backed up two columns and two rows
|
|
; and continues from there until the whole map is stable.
|
|
; In: A = interior tile, $0324 = contour offset (0 for $41-$54, $20 for $21-$34), contour rule
|
|
; table at $0200, terrainClassTable $077E, map
|
|
; Out: contour tiles in the map; $0312 = interior tile, zp_8E, zp_91, zp_2D/zp_2E scan position,
|
|
; zp_8C/zp_8D random-bit state, zp_21
|
|
; Called from: runMapGeneratorPasses ($6F54 with $65, $6F69 with $61)
|
|
; ----------------------------------------------------------------------
|
|
applyContourTiles:
|
|
sta play1Row24 ; 73EE $0312 = the interior tile this pass belongs to
|
|
tay ; 73F1 use the tile code as the class-table index
|
|
lda D_077E,y ; 73F2 its terrain class: 6 for the summit, 4 for the lower hill
|
|
sta lineMajorLen ; 73F5 zp_8E = the 'high ground' class for the neighbour test
|
|
sta lineStepsLeft ; 73F7 zp_91 starts as the interior class...
|
|
dec lineStepsLeft ; 73F9 zp_91 = class of this layer's contour ring (5 or 3)
|
|
lda #$00 ; 73FB clear A for the three stores below
|
|
sta lineErrRow ; 73FD zp_8C = 0: force a fresh random byte on the first irregular rule
|
|
sta contourScanCol ; 73FF column 0
|
|
sta contourScanRow ; 7401 start the scan at cell (0,0)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Scan every cell of the map. zp_2E/zp_2D are the cell; zp_48/49 addresses the top-left corner of its
|
|
; 3x3 block, so the cell itself is at offset $29.
|
|
; ----------------------------------------------------------------------
|
|
contourScanCell:
|
|
ldx contourScanCol ; 7403 X = scan column
|
|
ldy contourScanRow ; 7405 Y = scan row
|
|
dex ; 7407 step back one column to the block corner
|
|
dey ; 7408 address the block corner (column-1,row-1)
|
|
stx viewOriginCol ; 7409 zp_A5 = column-1
|
|
sty viewOriginRow ; 740B zp_A6 = row-1
|
|
jsr getViewportOriginPointer; 740D zp_48/49 -> the corner, clipped correctly for negative coordinates
|
|
ldy #$29 ; 7410 offset $29 = the cell itself
|
|
contourReadCell:
|
|
lda (mapCellPtr),y ; 7412 the cell's terrain code
|
|
bmi contourNextColumn ; 7414 bit 7 = a unit or a river keep-clear marker - never touched
|
|
tax ; 7416 use it as the class-table index
|
|
lda D_077E,x ; 7417 terrain class of the cell
|
|
contourCompareRingClass:
|
|
cmp lineStepsLeft ; 741A is it already a ring tile of this layer?
|
|
contourCellIsOldRing:
|
|
beq contourEraseOldRing ; 741C yes - erase it so the rule table can redraw it
|
|
bcs contourNextColumn ; 741E class above the ring = hill interior or higher - leave it
|
|
cmp #$01 ; 7420 class 1 = river
|
|
bne contourApplyRule ; 7422 not the river - run the rule
|
|
beq contourNextColumn ; 7424 river cells are never overwritten by a contour
|
|
contourEraseOldRing:
|
|
lda #$40 ; 7426 $40 = blank
|
|
sta (mapCellPtr),y ; 7428 erase the stale ring tile
|
|
contourApplyRule:
|
|
jsr applyContourRuleAtCell ; 742A let the neighbourhood rule table decide what this cell becomes
|
|
bcs contourScanCell ; 742D C = 1: a neighbour changed, restart from the backed-up position
|
|
contourNextColumn:
|
|
inc contourScanCol ; 742F next column
|
|
ldx contourScanCol ; 7431 the column just finished
|
|
cpx #$28 ; 7433 columns 0-39
|
|
bcc contourScanCell ; 7435 not at the right edge yet
|
|
inc contourScanRow ; 7437 next row
|
|
ldx #$00 ; 7439 column 0 of the next row
|
|
stx contourScanCol ; 743B back to column 0
|
|
ldy contourScanRow ; 743D the row just finished
|
|
cpy #$28 ; 743F rows 0-39
|
|
bcc contourScanCell ; 7441 not at the bottom yet
|
|
rts ; 7443 the whole map is stable
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyContourRuleAtCell - decides what one cell becomes, from which of its eight neighbours are high
|
|
; ground. It builds an 8-bit mask (bit 0 NW, 1 N, 2 NE, 3 W, 4 E, 5 SW, 6 S, 7 SE) of the neighbours
|
|
; whose terrain class is >= zp_8E, clears the bits that would point off the map, and uses the mask as
|
|
; the index into the 256-byte rule table at $0200 (track 18 sector 15):
|
|
; $00 leave the cell alone, C = 0
|
|
; $01-$7F paint that tile minus the contour offset $0324, with a random variant, C = 0
|
|
; $80-$8F irregular: 50% chance (bit taken from a random byte, 7 bits per byte) to erase the
|
|
; neighbour named by the low 3 bits when it is of the interior class; otherwise fill
|
|
; the cell with the interior tile. Either way C = 1
|
|
; $90-$FE same 50% erase attempt, but if it does not fire the cell is left alone, C = 0
|
|
; $FF fill the cell with the interior tile $0312, C = 1
|
|
; C = 1 also backs the scan position zp_2E/zp_2D up by two columns and two rows (floored at 0) so
|
|
; applyContourTiles re-examines the cells around the change.
|
|
; In: zp_2E/zp_2D = the cell, zp_48/49 -> its 3x3 block corner, zp_8E interior class, $0312
|
|
; interior tile, $0324 contour offset, rule table $0200, terrainClassTable $077E
|
|
; Out: C flag, map cells changed, zp_2D/zp_2E, zp_21 mask, zp_18 rule value, zp_8C/zp_8D random bits
|
|
; Called from: applyContourTiles ($742A)
|
|
; Build the high-ground neighbour mask, bit 7 (SE) down to bit 0 (NW).
|
|
; ----------------------------------------------------------------------
|
|
applyContourRuleAtCell:
|
|
lda #$00 ; 7444 clear the neighbour mask
|
|
sta scratch21 ; 7446 zp_21 = neighbour mask
|
|
ldx #$07 ; 7448 walk the eight neighbours backwards: 7 = SE .. 0 = NW
|
|
contourNeighbourLoop:
|
|
ldy neighbourOffsetTable,x ; 744A offset of neighbour X from the block corner
|
|
lda (mapCellPtr),y ; 744D that neighbour's terrain code
|
|
bmi contourNextNeighbour ; 744F bit 7 = unit or keep-clear marker - not high ground
|
|
tay ; 7451 use it as the class-table index
|
|
lda D_077E,y ; 7452 terrain class of the neighbour
|
|
cmp lineMajorLen ; 7455 high ground = class >= the interior class of this pass
|
|
bcc contourNextNeighbour ; 7457 lower - leave the bit clear
|
|
lda scratch21 ; 7459 mask built so far
|
|
ora bitMaskTable,x ; 745B bit X: NW,N,NE,W,E,SW,S,SE = bits 0-7
|
|
sta scratch21 ; 745E zp_21 = mask so far
|
|
contourNextNeighbour:
|
|
dex ; 7460 next neighbour
|
|
bpl contourNeighbourLoop ; 7461 all eight neighbours
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Clear the mask bits that point off the edge of the 40x40 map.
|
|
; ----------------------------------------------------------------------
|
|
ldx contourScanCol ; 7463 the cell's own column
|
|
ldy contourScanRow ; 7465 its row
|
|
lda scratch21 ; 7467 the raw mask
|
|
cpx #$00 ; 7469 on the left edge?
|
|
bne contourClipRightEdge ; 746B not on the left edge
|
|
and #$D6 ; 746D $D6 clears bits 0/3/5 = NW, W, SW
|
|
contourClipRightEdge:
|
|
cpx #$27 ; 746F on the right edge (column 39)?
|
|
bne contourClipTopEdge ; 7471 not on the right edge
|
|
and #$6B ; 7473 $6B clears bits 2/4/7 = NE, E, SE
|
|
contourClipTopEdge:
|
|
cpy #$00 ; 7475 on the top edge?
|
|
bne contourClipBottomEdge ; 7477 not on the top edge
|
|
and #$F8 ; 7479 $F8 clears bits 0/1/2 = NW, N, NE
|
|
contourClipBottomEdge:
|
|
cpy #$27 ; 747B on the bottom edge (row 39)?
|
|
bne contourLookupRule ; 747D not on the bottom edge
|
|
and #$1F ; 747F $1F clears bits 5/6/7 = SW, S, SE
|
|
contourLookupRule:
|
|
sta scratch21 ; 7481 zp_21 = the clipped mask
|
|
tay ; 7483 use it as the rule-table index
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Look the neighbourhood up in the 256-byte contour rule table loaded to $0200 from track 18 sector
|
|
; 15.
|
|
; ----------------------------------------------------------------------
|
|
lda contourRuleTable,y ; 7484 rule byte for this exact neighbourhood (also trainerPlaybook0200:trainerPlay0)
|
|
beq contourLeaveCell ; 7487 $00 = this neighbourhood needs no contour - leave the cell (C = 0)
|
|
bpl contourRingTileFromRule ; 7489 $01-$7F = a contour tile code - paint it
|
|
cmp #$FF ; 748B $FF = the cell is surrounded, fill it with the hill interior
|
|
beq contourFillWithInterior ; 748D fill the cell with the interior tile and force a rescan
|
|
sta scratch18 ; 748F $80-$FE: irregular rule; zp_18 keeps the value
|
|
dec lineErrRow ; 7491 any random bits left in zp_8D?
|
|
bpl contourTakeRandomBit ; 7493 yes
|
|
lda #$06 ; 7495 seven bits per random byte: counter 6 down through 0
|
|
sta lineErrRow ; 7497 seven bits are taken from each random byte
|
|
jsr nextScenarioRandom ; 7499 fetch a fresh batch of random bits
|
|
sta lineErrCol ; 749C zp_8D = fresh random bits
|
|
contourTakeRandomBit:
|
|
lsr lineErrCol ; 749E shift out the next bit
|
|
bcc contourMaybeFillCell ; 74A0 bit clear - do not erase a neighbour this time
|
|
lda scratch18 ; 74A2 the rule value
|
|
and #$07 ; 74A4 low 3 bits of the rule value = which neighbour it wants removed
|
|
tax ; 74A6 neighbour index 0-7
|
|
ldy neighbourOffsetTable,x ; 74A7 its offset from the block corner
|
|
lda (mapCellPtr),y ; 74AA that neighbour's terrain code
|
|
bmi contourMaybeFillCell ; 74AC unit or marker - leave it alone
|
|
tax ; 74AE use the terrain code as the class-table index
|
|
lda D_077E,x ; 74AF terrain class of that neighbour
|
|
cmp lineMajorLen ; 74B2 only a cell of the interior class may be nibbled away
|
|
bne contourMaybeFillCell ; 74B4 not a cell of the hill interior - leave it alone
|
|
lda #$40 ; 74B6 $40 = blank
|
|
sta (mapCellPtr),y ; 74B8 erase the neighbour - this is what makes hill edges irregular
|
|
jmp contourBackUpScan ; 74BA C = 1 and back the scan up
|
|
|
|
contourMaybeFillCell:
|
|
lda scratch18 ; 74BD the rule value again
|
|
cmp #$90 ; 74BF rule values $90-$FE...
|
|
bcs contourLeaveCell ; 74C1 ...leave the cell untouched when the erase did not fire (C = 0)
|
|
contourFillWithInterior:
|
|
lda play1Row24 ; 74C3 values $80-$8F (and $FF): fill the cell with the hill interior tile
|
|
jsr contourWriteTile ; 74C6 write it into the centre cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A cell changed under the scan: back up two columns and two rows so applyContourTiles re-evaluates
|
|
; the neighbourhood, and return C = 1.
|
|
; ----------------------------------------------------------------------
|
|
contourBackUpScan:
|
|
lda contourScanCol ; 74C9 current scan column
|
|
sec ; 74CB prepare the subtract
|
|
sbc #$02 ; 74CC two columns back
|
|
bpl contourStoreScanCol ; 74CE still positive
|
|
lda #$00 ; 74D0 but never before column 0
|
|
contourStoreScanCol:
|
|
sta contourScanCol ; 74D2 zp_2E = backed-up scan column
|
|
lda contourScanRow ; 74D4 current scan row
|
|
contourBackUpRow:
|
|
sec ; 74D6 prepare the subtract
|
|
sbc #$02 ; 74D7 two rows back
|
|
bpl contourStoreScanRow ; 74D9 still positive
|
|
lda #$00 ; 74DB but never before row 0
|
|
contourStoreScanRow:
|
|
sta contourScanRow ; 74DD zp_2D = backed-up scan row
|
|
sec ; 74DF C = 1: applyContourTiles restarts its scan here
|
|
contourRescanReturn:
|
|
rts ; 74E0 C = 1 set at $74DF
|
|
|
|
contourRingTileFromRule:
|
|
sec ; 74E1 prepare the subtract
|
|
sbc play1Row42 ; 74E2 tile = rule value - $0324: offset 0 keeps $41-$54, offset $20 gives $21-$34
|
|
contourWriteTile:
|
|
ldy #$29 ; 74E5 offset $29 = the cell itself
|
|
jsr randomTileVariant ; 74E7 pick a random variant of the tile (Y is preserved)
|
|
sta (mapCellPtr),y ; 74EA paint the contour tile
|
|
contourLeaveCell:
|
|
clc ; 74EC C = 0: the scan carries on normally
|
|
rts ; 74ED C = 0 set at $74EC
|
|
|
|
|
|
; Two lookup tables shared by the contour/edge pass applyContourRuleAtCell ($7444, just above this
|
|
; chunk). That routine keeps zp_48/zp_49 pointing at the top-left cell of the 3x3 block around the
|
|
; cell it is examining, so the centre cell is at offset +$29 (one row of 40 = $28, plus one column).
|
|
neighbourOffsetTable:
|
|
.byte $00,$01,$02,$28,$2A ; 74EE ...(* neighbour offsets NW=+$00, N=+$01, NE=+$02, W=+$28 (one row down), E=+$2A
|
|
L_74F3:
|
|
.byte $50 ; 74F3 ...SW=+$50 (two rows down), S=+$51, SE=+$52; label L_74F3 only exists because variant B has code here
|
|
.byte $51,$52 ; 74F4 QR
|
|
|
|
; bitMaskTable: byteTable, 8 bytes. $01,$02,$04,$08,$10,$20,$40,$80 - single-bit masks used by the
|
|
; terrain fill pass at $745B.
|
|
bitMaskTable:
|
|
.byte $01,$02,$04,$08,$10,$20,$40,$80; 74F6 ..... @. bit n of the neighbour mask built at $745B, in the same NW,N,NE,W,E,SW,S,SE order as the offsets above
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearMapMarkers - Clean-up pass of the map generator: walks all 1600 cells of the 40x40 battlefield
|
|
; ($F000-$F63F) and turns every cell that still holds the reservation marker in A back into blank
|
|
; terrain $40. markRiverBanks ($764A) stamps $80 (river outskirts) and $81 (river bank) into empty
|
|
; cells so the hill/forest passes leave the river valley alone; this routine removes them again once
|
|
; those passes are done. The map is scanned as six whole pages through a self-modified absolute,Y
|
|
; load/store pair plus the 64-byte tail at $F600.
|
|
; In: A = marker byte to erase ($80 or $81); the map at $F000-$F63F
|
|
; Out: every cell equal to A becomes $40; A and Y clobbered, X untouched. Self-modifies the
|
|
; compare operands at $751C/$753C and the page bytes of the load/store at $7517-$7518 and
|
|
; $7522-$7523
|
|
; Called from: generateBattlefieldMap ($6F47 with $80, $6F5F with $81)
|
|
; ----------------------------------------------------------------------
|
|
clearMapMarkers:
|
|
sta markerScanTest+1 ; 74FE patch the marker value into the CMP #imm at $751B (main-page test)
|
|
sta markerTailTest+1 ; 7501 patch the same marker into the CMP #imm at $753B (last-64-cells test)
|
|
lda #$00 ; 7504 map starts at $F000, so the low byte of both patched operands is 0
|
|
sta markerScanLoad+1 ; 7506 low byte of the self-modified LDA abs,Y at $7516
|
|
sta markerScanStore+1 ; 7509 low byte of the self-modified STA abs,Y at $7521
|
|
startMarkerScanAtF000:
|
|
lda #$F0 ; 750C first page of the map is $F0xx (label only reachable from variant B's code)
|
|
sta markerScanLoad+2 ; 750E high byte of the self-modified LDA abs,Y
|
|
sta markerScanStore+2 ; 7511 high byte of the self-modified STA abs,Y
|
|
ldy #$00 ; 7514 start at the first cell of the page
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Page loop: 256 cells at a time from $F000 through $F5FF. The absolute address of both the load and
|
|
; the store is the self-modified operand; the assembled placeholder is $FFFF (rendered as
|
|
; irqVectorHi), never the address actually used.
|
|
; ----------------------------------------------------------------------
|
|
markerScanLoad:
|
|
lda irqVectorHi,y ; 7516 read map cell (operand patched to the current page, $F000-$F500)
|
|
bpl markerScanNext ; 7519 plain terrain has bit 7 clear - nothing to undo
|
|
markerScanTest:
|
|
cmp #$FF ; 751B compare against the marker being erased (operand written at $74FE)
|
|
bne markerScanNext ; 751D some other bit-7 cell (a unit marker $80|index cannot occur yet) - leave it
|
|
lda #$40 ; 751F $40 = blank terrain
|
|
markerScanStore:
|
|
sta irqVectorHi,y ; 7521 write the blank back (operand patched to the current page)
|
|
markerScanNext:
|
|
iny ; 7524 next cell in this page
|
|
bne markerScanLoad ; 7525 keep going until the page wraps back to offset 0
|
|
inc markerScanLoad+2 ; 7527 step the load address to the next page
|
|
inc markerScanStore+2 ; 752A step the store address with it
|
|
lda markerScanStore+2 ; 752D look at the page just reached
|
|
cmp #$F6 ; 7530 $F600 is past the last full page of the map
|
|
bcc markerScanLoad ; 7532 not there yet - scan the next 256 cells
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Tail: the map is 1600 = 6*256 + 64 bytes, so the last 64 cells at $F600-$F63F are handled separately
|
|
; (backwards, so DEY/BPL can end the loop).
|
|
; ----------------------------------------------------------------------
|
|
ldy #$3F ; 7534 last cell of the 64-byte tail ($F63F)
|
|
markerTailLoop:
|
|
lda D_F600,y ; 7536 read cell $F600+Y
|
|
bpl markerTailNext ; 7539 plain terrain (bit 7 clear) - nothing to undo here
|
|
markerTailTest:
|
|
cmp #$FF ; 753B compare against the marker being erased (operand written at $7501)
|
|
bne markerTailNext ; 753D not this marker - leave the cell alone
|
|
lda #$40 ; 753F $40 = blank terrain
|
|
sta D_F600,y ; 7541 restore the blank cell
|
|
markerTailNext:
|
|
dey ; 7544 walk down to cell 0 of the tail
|
|
bpl markerTailLoop ; 7545 loop while the index is still positive
|
|
rts ; 7547 map is clean again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; generateRiver - Draws the river across the whole battlefield, left edge to right edge, as the first
|
|
; terrain feature of a generated map. A river cell is terrain code $55 + segment type (0-7 ->
|
|
; $55-$5C); the segment type also decides whether the cursor then steps right, up or down. Around
|
|
; every cell it reserves the banks with markRiverBanks so the later hill and forest passes cannot
|
|
; build on top of the water.
|
|
; In: mapGenGameFlags $032E (bit 7 = QB SNEAK, bit 6 = DEFENDER), mapSymmetryFlags $032D (bit 7 =
|
|
; point-symmetric map), scenario RNG $5A-$5C via nextScenarioRandom ($FD4A), map at $F000
|
|
; Out: river tiles $55-$5C written into the map plus $80/$81 bank markers around them; genCol
|
|
; $030F, genRow $0310, riverCurSegment $0311 and genTileType $0312 left at the right edge;
|
|
; mapSymmetryFlags bit 6 may be set by chooseNextRiverSegment. Returns with no river drawn at
|
|
; all for QB SNEAK, and with probability 1/8 for the game types other than DEFENDER
|
|
; Called from: runMapGeneratorPasses ($6F39)
|
|
; ----------------------------------------------------------------------
|
|
generateRiver:
|
|
bit play2Grp02 ; 7548 map-style flags derived from the game type
|
|
bmi noRiverOnThisMap ; 754B bit 7 = QB SNEAK: that scenario is played on dry land, no river
|
|
D_754D:
|
|
ldy #$14 ; 754D default start row for a symmetric map: row 20, the centre line
|
|
D_754F:
|
|
lda invertFontByteLoop ; 754F map symmetry flags (also trainerPlaybook0200:play2Grp01)
|
|
sub_7552:
|
|
bmi startRiverAtRow ; 7552 bit 7 = point-symmetric map (seed ends in '='): keep the fixed centre row
|
|
jsr nextScenarioRandom ; 7554 otherwise roll for the start row
|
|
L_7557:
|
|
and #$0F ; 7557 0-15...
|
|
clc ; 7559 clear carry for the add
|
|
adc #$0C ; 755A ...plus 12 = a start row of 12-27, well inside the 40-row map
|
|
tay ; 755C keep the start row in Y
|
|
bit play2Grp02 ; 755D map-style flags again, this time for bit 6
|
|
bvs startRiverAtRow ; 7560 bit 6 = DEFENDER: that scenario always has a river
|
|
jsr nextScenarioRandom ; 7562 for every other game type roll once more
|
|
and #$07 ; 7565 3 random bits
|
|
bne startRiverAtRow ; 7567 7 chances in 8: draw the river
|
|
noRiverOnThisMap:
|
|
rts ; 7569 1 chance in 8 (and always for QB SNEAK): leave the map dry
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the river from column 0 to column 39. genTileType ($0312) holds the segment chosen for the
|
|
; *next* cell; it starts as 5, a plain horizontal straight, so the river enters the map flat.
|
|
; ----------------------------------------------------------------------
|
|
startRiverAtRow:
|
|
sty play1Row22 ; 756A genRow = start row
|
|
lda #$00 ; 756D column 0 = the left edge of the map
|
|
sta storeRngSeedByte1 ; 756F genCol = 0, the left edge of the map (also trainerPlaybook0200:play1Row21)
|
|
lda #$05 ; 7572 segment type 5 = a horizontal straight variant ($5A)
|
|
sta play1Row24 ; 7574 genTileType = the segment to draw at the first cell
|
|
riverCellLoop:
|
|
ldy play1Row22 ; 7577 current row...
|
|
ldx storeRngSeedByte1 ; 757A ...and column of the river cursor (also trainerPlaybook0200:play1Row21)
|
|
jsr readMapCell ; 757D mapCellPtr ($48/$49) = &map[row][col] (the returned cell value is discarded)
|
|
lda play1Row24 ; 7580 segment type chosen for this cell
|
|
sta loadRngSeedByte2 ; 7583 riverCurSegment = it, so markRiverBanks/advanceRiverPosition see the same segment (also trainerPlaybook0200:play1Row23)
|
|
D_7586:
|
|
clc ; 7586 clear carry before the tile base is added
|
|
L_7587:
|
|
adc #$55 ; 7587 river terrain codes are $55 + segment type, i.e. $55-$5C
|
|
ldy #$00 ; 7589 mapCellPtr already points at the cell itself
|
|
sta (mapCellPtr),y ; 758B paint the river cell
|
|
jsr chooseNextRiverSegment ; 758D decide the segment for the next cell (may also jog the cursor on a symmetric map)
|
|
jsr markRiverBanks ; 7590 reserve the 6x6 block of banks around this cell with $80/$81
|
|
jsr advanceRiverPosition ; 7593 step the cursor right, up or down according to riverCurSegment
|
|
lda storeRngSeedByte1 ; 7596 column reached... (also trainerPlaybook0200:play1Row21)
|
|
cmp #$28 ; 7599 ...40 = one past the right edge?
|
|
bcc riverCellLoop ; 759B no - draw the next river cell
|
|
rts ; 759D the river has crossed the whole map
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; chooseNextRiverSegment - Picks the segment type of the next river cell into genTileType ($0312).
|
|
; Segment types: 0 = elbow leaving right after an up step ($55), 1 = elbow leaving downward ($56), 2 =
|
|
; elbow leaving right after a down step ($57), 3 = elbow leaving upward ($58), 4-7 = horizontal
|
|
; straight variants ($59-$5C). Only types 1 and 3 move the cursor vertically, so the river runs flat
|
|
; with the occasional one-row jog. Rules: 1 is always followed by 2 and 3 by 0 (the elbow that turns
|
|
; the flow back to the right); an elbow is always followed by a straight; a straight is followed by
|
|
; another straight about half the time, otherwise by a jog. Columns 0-1 and 38-39 are forced flat,
|
|
; and column 37 may not start a jog (the elbow after it would fall off the map). On a point-symmetric
|
|
; map the river must run along row 20 and meet its own 180-degree mirror image in the middle, so at
|
|
; column 20 it steps back two columns and up one row with probability 7/8.
|
|
; In: riverCurSegment $0311, genCol $030F, genRow $0310, mapSymmetryFlags $032D, scenario RNG
|
|
; Out: genTileType $0312 = next segment 0-7; on a symmetric map genCol/genRow may be moved and
|
|
; mapSymmetryFlags bit 6 set to record that the jog was made
|
|
; Called from: generateRiver ($758D)
|
|
; Symmetric maps: the river is drawn only once and the caller mirrors the map afterwards, so the line
|
|
; has to arrive at the centre of the map in a shape that joins its own rotated copy.
|
|
; ----------------------------------------------------------------------
|
|
chooseNextRiverSegment:
|
|
ldx storeRngSeedByte1 ; 759E column of the cell just drawn (also trainerPlaybook0200:play1Row21)
|
|
lda invertFontByteLoop ; 75A1 map symmetry flags (also trainerPlaybook0200:play2Grp01)
|
|
bpl chooseFromCurrentSegment; 75A4 bit 7 clear = ordinary random map: use the normal segment rules
|
|
cpx #$14 ; 75A6 symmetric map: has the river reached the centre column 20?
|
|
bne forceHorizontalSegment ; 75A8 no - force a horizontal straight so it stays on row 20
|
|
jsr nextScenarioRandom ; 75AA at the centre, roll for the jog
|
|
and #$07 ; 75AD 3 random bits
|
|
beq forceHorizontalSegment ; 75AF 1 chance in 8: no jog, stay flat
|
|
lda invertFontByteLoop ; 75B1 map symmetry flags (also trainerPlaybook0200:play2Grp01)
|
|
ora #$40 ; 75B4 bit 6 = 'the centre jog has been made' (read by the symmetry fix-up)
|
|
sta invertFontByteLoop ; 75B6 write them back with the jog recorded (also trainerPlaybook0200:play2Grp01)
|
|
checkCentreRow:
|
|
ldy play1Row22 ; 75B9 current row
|
|
cpy #$14 ; 75BC only jog when the river is still on the centre row 20
|
|
bne forceHorizontalSegment ; 75BE otherwise just keep it flat
|
|
dec storeRngSeedByte1 ; 75C0 back up two columns... (also trainerPlaybook0200:play1Row21)
|
|
dec storeRngSeedByte1 ; 75C3 ...so the two cells about to be redrawn form the joint (also trainerPlaybook0200:play1Row21)
|
|
jogRiverUpOneRow:
|
|
dec play1Row22 ; 75C6 and move up to row 19, above the mirror line
|
|
bne forceHorizontalSegment ; 75C9 always taken (row 19 is not 0) - draw a straight there
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Ordinary maps: the next segment follows from the current one. Elbows 1 and 3 must be closed by
|
|
; their partner elbow, everything else picks randomly between straights and jogs.
|
|
; ----------------------------------------------------------------------
|
|
chooseFromCurrentSegment:
|
|
lda loadRngSeedByte2 ; 75CB segment just drawn (also trainerPlaybook0200:play1Row23)
|
|
cmp #$01 ; 75CE was it the 'turn downward' elbow?
|
|
bne checkUpElbow ; 75D0 not the downward elbow - test the next case
|
|
lda #$02 ; 75D2 yes - the cell below it must be the elbow that turns the flow right again
|
|
bne clampSegmentToColumn ; 75D4 always taken (A = 2)
|
|
checkUpElbow:
|
|
cmp #$03 ; 75D6 was it the 'turn upward' elbow?
|
|
bne chooseStraightOrJog ; 75D8 not the upward elbow either - fall through to the random choice
|
|
lda #$00 ; 75DA yes - the cell above it must be elbow 0, which turns the flow right again
|
|
beq clampSegmentToColumn ; 75DC always taken (A = 0)
|
|
chooseStraightOrJog:
|
|
cmp #$03 ; 75DE types 0-2 are elbows, 4-7 are straights
|
|
bcs rollNextSegment ; 75E0 current cell is a straight - it may be followed by a jog
|
|
jsr nextScenarioRandom ; 75E2 current cell was an elbow: the next one must be a straight
|
|
and #$03 ; 75E5 pick one of the four straight variants...
|
|
makeStraightSegment:
|
|
clc ; 75E7 clear carry for the add
|
|
adc #$04 ; 75E8 ...i.e. segment 4-7
|
|
bne clampSegmentToColumn ; 75EA always taken (the value is 4-7)
|
|
rollNextSegment:
|
|
jsr nextScenarioRandom ; 75EC random byte
|
|
bpl maskSegmentType ; 75EF random bit 7 clear: leave the value alone, so a jog stays possible
|
|
ora #$04 ; 75F1 ...force bit 2, which restricts the result to a straight 4-7
|
|
maskSegmentType:
|
|
and #$07 ; 75F3 keep 3 bits: a segment type 0-7
|
|
beq chooseStraightOrJog ; 75F5 type 0 is an elbow that cannot start a jog - draw a straight instead
|
|
rejectClosingElbow:
|
|
cmp #$02 ; 75F7 type 2 likewise closes a jog rather than starting one
|
|
beq chooseStraightOrJog ; 75F9 reject it the same way (A is 0 or 2, so the retry always takes the straight path)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Edge guard: a jog needs room for its closing elbow, so the first two and the last three columns are
|
|
; always drawn flat.
|
|
; ----------------------------------------------------------------------
|
|
clampSegmentToColumn:
|
|
ldx storeRngSeedByte1 ; 75FB column of the cell just drawn (also trainerPlaybook0200:play1Row21)
|
|
cpx #$02 ; 75FE columns 0-1: the river must enter the map horizontally
|
|
bcc forceHorizontalSegment ; 7600 column 0 or 1 - force a horizontal straight
|
|
cpx #$25 ; 7602 is the cell in column 37, the last one that could start a jog?
|
|
bcc storeNextSegment ; 7604 columns 2-36: accept whatever was chosen
|
|
bne forceHorizontalSegment ; 7606 columns 38-39: force a straight so the river leaves the map horizontally
|
|
cmp #$01 ; 7608 column 37: reject a downward jog...
|
|
beq forceHorizontalSegment ; 760A downward jog rejected - draw a straight instead
|
|
cmp #$03 ; 760C ...and an upward jog, since the closing elbow would land in column 39
|
|
L_760E:
|
|
bne storeNextSegment ; 760E not a jog - the straight that was chosen is fine in column 37
|
|
forceHorizontalSegment:
|
|
jsr nextScenarioRandom ; 7610 random byte
|
|
and #$03 ; 7613 one of the four...
|
|
clc ; 7615 clear carry for the add
|
|
adc #$04 ; 7616 ...horizontal straight variants 4-7
|
|
storeNextSegment:
|
|
sta play1Row24 ; 7618 genTileType = segment type of the next river cell
|
|
rts ; 761B back to generateRiver
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; advanceRiverPosition - Moves the river cursor one cell according to the segment just drawn: type 1
|
|
; (elbow turning downward) steps a row down, type 3 (elbow turning upward) a row up, every other type
|
|
; one column right. Implemented as a jump through a two-part address table, so the JMP operand at
|
|
; $762C/$762D is rewritten on every call.
|
|
; In: riverCurSegment $0311, tables riverStepLo $763A / riverStepHi $7642
|
|
; Out: genCol $030F or genRow $0310 stepped by one; the operand of the JMP at $762B is self-modified
|
|
; Called from: generateRiver ($7593)
|
|
; ----------------------------------------------------------------------
|
|
advanceRiverPosition:
|
|
ldy loadRngSeedByte2 ; 761C segment type of the cell just drawn (also trainerPlaybook0200:play1Row23)
|
|
lda riverStepLo,y ; 761F low byte of the step routine for that type
|
|
sta riverStepJump+1 ; 7622 patch it into the JMP operand at $762C
|
|
lda riverStepHi,y ; 7625 high byte (always $76)
|
|
sta riverStepJump+2 ; 7628 patch it into $762D
|
|
riverStepJump:
|
|
jmp irqVectorHi ; 762B jump to riverStepRight/Up/Down; the assembled operand $FFFF is only a placeholder
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; riverStepRight - Step stub for every segment type that leaves the cell to the right (0, 2 and the
|
|
; straights 4-7): advances the river cursor one column.
|
|
; In: none
|
|
; Out: genCol $030F + 1
|
|
; Called from: the patched JMP at $762B in advanceRiverPosition
|
|
; ----------------------------------------------------------------------
|
|
riverStepRight:
|
|
inc storeRngSeedByte1 ; 762E river cursor moves one column right (also trainerPlaybook0200:play1Row21)
|
|
rts ; 7631 back to generateRiver, which called advanceRiverPosition
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; riverStepUp - Step stub for segment type 3, the elbow that turns the river upward: moves the cursor
|
|
; one row up (towards row 0).
|
|
; In: none
|
|
; Out: genRow $0310 - 1
|
|
; Called from: the patched JMP at $762B in advanceRiverPosition
|
|
; ----------------------------------------------------------------------
|
|
riverStepUp:
|
|
dec play1Row22 ; 7632 river cursor moves one row up
|
|
rts ; 7635 back to generateRiver, which called advanceRiverPosition
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; riverStepDown - Step stub for segment type 1, the elbow that turns the river downward: moves the
|
|
; cursor one row down.
|
|
; In: none
|
|
; Out: genRow $0310 + 1
|
|
; Called from: the patched JMP at $762B in advanceRiverPosition
|
|
; ----------------------------------------------------------------------
|
|
riverStepDown:
|
|
inc play1Row22 ; 7636 river cursor moves one row down
|
|
rts ; 7639 back to generateRiver, which called advanceRiverPosition
|
|
|
|
|
|
; Split address table of the three step stubs, indexed by segment type 0-7 and read by
|
|
; advanceRiverPosition ($761F/$7625). Only type 1 ($7636 = down) and type 3 ($7632 = up) move the
|
|
; river vertically; every other type maps to $762E (right).
|
|
riverStepLo:
|
|
.byte $2E,$36,$2E,$32,$2E ; 763A .6.2. low bytes for types 0-4: right, down, right, up, right
|
|
L_763F:
|
|
.byte $2E ; 763F low bytes for types 5-7: all three straights step right
|
|
.byte $2E,$2E ; 7640 ..
|
|
|
|
; riverStepHi: addrTable, 8 bytes. high bytes ($76) of the river step routine addresses.
|
|
riverStepHi:
|
|
.byte $76,$76,$76,$76,$76,$76,$76,$76; 7642 vvvvvvvv high bytes: all three stubs live in page $76
|
|
|
|
; ----------------------------------------------------------------------
|
|
; markRiverBanks - Reserves the ground around the current river cell so the hill and forest passes
|
|
; that follow keep the river valley clear. Away from the map edges it addresses the 6x6 block whose
|
|
; top-left corner is (genCol & ~1) - 2, (genRow & ~1) - 2, writes $80 into every blank cell of the
|
|
; 20-cell perimeter and $81 into the 16 cells of the 4x4 core (upgrading a $80 already there). Only
|
|
; cells that are still blank ($40 or lower) are touched, so terrain already placed - including the
|
|
; river itself - survives. In columns 0-1 and 38-39 only the four cells of the river's own column get
|
|
; $81. clearMapMarkers ($74FE) erases both marker values again at the end of the generator.
|
|
; In: genCol $030F, genRow $0310, map addressed through readMapCell ($25FA -> mapCellPtr zp_48/49),
|
|
; tables riverZoneOuterOffsets $7894 and riverZoneInnerOffsets $78A8
|
|
; Out: $80/$81 written into blank map cells; mapCellPtr points at the block origin; A/X/Y clobbered
|
|
; Called from: generateRiver ($7590)
|
|
; ----------------------------------------------------------------------
|
|
markRiverBanks:
|
|
ldx storeRngSeedByte1 ; 764A column of the river cell just drawn (also trainerPlaybook0200:play1Row21)
|
|
cpx #$02 ; 764D columns 0 and 1 have no room for a 6x6 block to their left
|
|
bcs checkRightEdgeColumn ; 764F inside the map - do the full 6x6 reservation
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Edge columns (0, 1, 38, 39): reserve only the four cells directly above and below the river in the
|
|
; river's own column, using the first four entries of riverZoneOuterOffsets ($28, $50, $78, $A0 = one,
|
|
; two, three and four rows down).
|
|
; ----------------------------------------------------------------------
|
|
markEdgeColumnOnly:
|
|
lda play1Row22 ; 7651 row of the river cell
|
|
and #$FE ; 7654 snap it down to an even row (the generator works in 2x2 blocks)
|
|
tay ; 7656 Y = the even row
|
|
dey ; 7657 two rows above...
|
|
dey ; 7658 ...the even row, so the four offsets cover evenRow-1 to evenRow+2
|
|
jsr readMapCell ; 7659 mapCellPtr = &map[evenRow-2][genCol] (X still holds the column)
|
|
ldx #$03 ; 765C four offsets, walked backwards
|
|
markEdgeCellLoop:
|
|
ldy riverZoneOuterOffsets,x ; 765E offset of the cell: +1, +2, +3 or +4 rows in the same column
|
|
lda (mapCellPtr),y ; 7661 read that cell
|
|
bmi writeBankMarkerEdge ; 7663 bit 7 set = already an $80/$81 reservation marker - upgrade it
|
|
cmp #$41 ; 7665 $40 and below is blank ground
|
|
bcs markEdgeCellNext ; 7667 real terrain is already here - leave it alone
|
|
writeBankMarkerEdge:
|
|
lda #$81 ; 7669 $81 = river bank, the strongest reservation
|
|
sta (mapCellPtr),y ; 766B reserve the cell as river bank
|
|
markEdgeCellNext:
|
|
dex ; 766D next of the four offsets
|
|
bpl markEdgeCellLoop ; 766E loop over all four offsets
|
|
rts ; 7670 done with the edge column
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Interior columns 2-37: build the 6x6 block, mark its 20-cell perimeter $80 and its 4x4 core $81.
|
|
; ----------------------------------------------------------------------
|
|
checkRightEdgeColumn:
|
|
cpx #$26 ; 7671 is the river within two columns of the right edge (38 or 39)?
|
|
bcs markEdgeColumnOnly ; 7673 yes - too close to the right edge for a 6x6 block
|
|
txa ; 7675 column of the river cell
|
|
and #$FE ; 7676 snap the column down to an even one
|
|
tax ; 7678 X = the even column
|
|
lda play1Row22 ; 7679 row of the river cell
|
|
and #$FE ; 767C snap the row down to an even one
|
|
tay ; 767E Y = the even row
|
|
dex ; 767F step two columns left...
|
|
dex ; 7680 ...to the left edge of the 6x6 block
|
|
dey ; 7681 step two rows up...
|
|
dey ; 7682 ...to the top edge of the 6x6 block
|
|
jsr readMapCell ; 7683 mapCellPtr = &map[evenRow-2][evenCol-2]
|
|
ldx #$13 ; 7686 20 perimeter offsets, walked backwards
|
|
markOuterCellLoop:
|
|
ldy riverZoneOuterOffsets,x ; 7688 offset of the next perimeter cell inside the 6x6 block
|
|
lda (mapCellPtr),y ; 768B read it
|
|
cmp #$41 ; 768D anything above $40 - real terrain or an existing marker - is left as it is
|
|
bcs markOuterCellNext ; 768F already terrain or a marker - leave it
|
|
lda #$80 ; 7691 $80 = river outskirts, the weaker reservation
|
|
sta (mapCellPtr),y ; 7693 reserve the perimeter cell
|
|
markOuterCellNext:
|
|
dex ; 7695 next perimeter offset
|
|
bpl markOuterCellLoop ; 7696 loop over all 20 perimeter offsets
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The 4x4 core of the block, i.e. the cells that actually touch the water, gets the stronger $81
|
|
; marker; an $80 written by a neighbouring river cell is upgraded to $81 here.
|
|
; ----------------------------------------------------------------------
|
|
markInnerCells:
|
|
ldx #$0F ; 7698 16 core offsets, walked backwards
|
|
markInnerCellLoop:
|
|
ldy riverZoneInnerOffsets,x ; 769A offset of the next core cell
|
|
lda (mapCellPtr),y ; 769D read it
|
|
bmi writeBankMarkerInner ; 769F already a marker ($80 or $81) - upgrade to $81
|
|
cmp #$41 ; 76A1 $40 and below is blank ground
|
|
bcs markInnerCellNext ; 76A3 real terrain is already here - leave it
|
|
writeBankMarkerInner:
|
|
lda #$81 ; 76A5 $81 = river bank
|
|
sta (mapCellPtr),y ; 76A7 reserve the core cell as river bank
|
|
markInnerCellNext:
|
|
dex ; 76A9 next core offset
|
|
bpl markInnerCellLoop ; 76AA loop over all 16 core offsets
|
|
rts ; 76AC back to generateRiver
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mapNameEntryScreen - The 'TYPE MAP NAME' page of the game setup. A map name is simply the game's
|
|
; 24-bit map seed written in base 32 with the alphabet at $890C, so typing a name picks a battlefield
|
|
; and the two players only have to agree on those five characters. The routine draws the current map,
|
|
; then loops: enterMapName returns a new seed (C=0) - adopt it, regenerate and redraw the map, ask
|
|
; again; or the fire button (C=1) - print 'ACCEPTED' and return to the setup menu.
|
|
; In: mapSeed $0B95-$0B97, mapFlipFlag $0B9D, keyboard and joystick
|
|
; Out: mapSeed and mapFlipFlag updated, the map at $F000 regenerated and drawn in the overview
|
|
; window, text colour zp_3D / pad colour zp_42 = 9
|
|
; Called from: the setup menu, by JMP from $8339 (the MAP item)
|
|
; ----------------------------------------------------------------------
|
|
mapNameEntryScreen:
|
|
jsr generateBattlefieldMap ; 76AD build the 40x40 map from the current seed
|
|
lda mapFlipFlag ; 76B0 is the map shown rotated 180 degrees?
|
|
beq drawMapAndClearName ; 76B3 flip flag clear - show the map as generated
|
|
jsr rotateMapAndMirrorCodes ; 76B5 yes - rotate the map and mirror the direction-dependent terrain codes
|
|
drawMapAndClearName:
|
|
jsr drawOverviewMap ; 76B8 paint the whole map into the 20x20 overview window
|
|
jsr clearMapNameField ; 76BB blank the five name characters $0BA9-$0BAD
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Prompt loop. Each pass reprints both prompts and calls enterMapName; it ends only when the player
|
|
; presses the fire button.
|
|
; ----------------------------------------------------------------------
|
|
mapNamePromptLoop:
|
|
lda #$09 ; 76BE colour 9 = brown, the setup-panel text colour
|
|
sta textColour ; 76C0 text colour
|
|
sta textPadColour ; 76C2 and the colour of the padding around centred text
|
|
jsr clearInfoPanel ; 76C4 clear the info panel under the tactical view
|
|
lda #$77 ; 76C7 string low byte...
|
|
ldy #$06 ; 76C9 ...and high byte: msgTypeMapName $0677, 'TYPE MAP NAME / (F7 RANDOM) / (F5 SYM/RND)'
|
|
jsr printString ; 76CB print the three-line prompt
|
|
printAcceptPrompt:
|
|
lda #$00 ; 76CE leftmost column of the panel
|
|
sta textCursorCol ; 76D0 set it as the cursor column
|
|
lda #$13 ; 76D2 row 19 of the text grid
|
|
jsr setCursorRow ; 76D4 move the cursor there
|
|
lda #$9F ; 76D7 string low byte...
|
|
ldy #$06 ; 76D9 ...and high byte: msgBtnToAccept $069F, 'BTN TO ACCEPT / (F 1 TO FLIP)'
|
|
jsr printString ; 76DB print the two-line prompt
|
|
jsr enterMapName ; 76DE run the interactive name entry
|
|
bcs acceptCurrentMap ; 76E1 C=1: the fire button was pressed - accept the map on screen
|
|
lda scenarioSeed0 ; 76E3 C=0: enterMapName left a fresh 24-bit seed in $5A-$5C
|
|
sta mapSeed ; 76E5 adopt it as the map seed...
|
|
lda scenarioSeed1 ; 76E8 seed byte 1...
|
|
sta mapSeed1 ; 76EA ...into mapSeed1
|
|
lda scenarioSeed2 ; 76ED seed byte 2...
|
|
sta mapSeed2 ; 76EF ...into mapSeed2
|
|
jsr clearBattlefieldMap ; 76F2 wipe the map back to blank terrain $40 (also trainerSetupEC00:mainGroupByPlanTable)
|
|
lda #$00 ; 76F5 colour 0
|
|
sta textColour ; 76F7 text colour 0 while redrawing
|
|
sta mapFlipFlag ; 76F9 a freshly named map is always shown unflipped
|
|
jsr clearOverviewWindow ; 76FC clear the 20x20 overview window
|
|
jsr generateBattlefieldMap ; 76FF generate the new battlefield from the new seed
|
|
redrawOverviewAfterNewMap:
|
|
jsr drawOverviewMap ; 7702 draw it
|
|
jmp mapNamePromptLoop ; 7705 and ask for another name
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Accept: the map currently on screen is the one that will be played.
|
|
; ----------------------------------------------------------------------
|
|
acceptCurrentMap:
|
|
jsr clearInfoPanel ; 7708 clear the info panel
|
|
lda #$0F ; 770B row 15
|
|
jsr setCursorRow ; 770D move the cursor to row 15
|
|
lda #$ED ; 7710 string low byte...
|
|
L_7712:
|
|
ldy #$EF ; 7712 ...and high byte: 'ACCEPTED' at $EFED in the $EC00 comm-module tail; tail-call back to the menu
|
|
jmp printStringCentred ; 7714 centre the word in the panel and return to the setup menu
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; enterMapName - Reads a five-character map name at column 4, row 16 and converts it into the 24-bit
|
|
; map seed in $5A-$5C. The field is underlined with five $25 glyphs and the cursor cell is marked by
|
|
; writing colour $11 into the cell's colour-RAM byte. Only the 32 characters of the alphabet at $890C
|
|
; (space, A-Z, + - * / =) are accepted, five bits each, first character in the high bits; $88 (DEL)
|
|
; erases; any other key just beeps. F1 flips the displayed map, F7 takes a random seed from the game
|
|
; RNG and F5 does the same but forces the low five bits to 1 so the name ends in '=' - the marker for
|
|
; a point-symmetric map. The fire button ends entry with C=1. The character counter lives at $0313;
|
|
; the symbol commandDispatchLoop comes from the code that occupies $0300-$03FF in the other overlay,
|
|
; it has nothing to do with commands here.
|
|
; In: key events from newKeyEvent $90EA, function key requestedScreenKey zp_66, the fire button via
|
|
; checkFirePressed ($0EB1), game RNG $57-$59, alphabet $890C
|
|
; Out: C=0 - $5A-$5C hold the chosen 24-bit seed and $0BA9-$0BAD its five characters; C=1 - the fire
|
|
; button was pressed and nothing was chosen. $0313 = characters typed, mapFlipFlag $0B9D may
|
|
; have been toggled, zp_18 scratch, text cursor moved
|
|
; Called from: mapNameEntryScreen ($76DE) and the manual look-up copy protection ($09F9)
|
|
; ----------------------------------------------------------------------
|
|
enterMapName:
|
|
jsr printMapName ; 7717 print the (blank) name field and leave the cursor after it
|
|
lda #$04 ; 771A column 4 again - printMapName left the cursor on the row below the field
|
|
sta textCursorCol ; 771C cursor back to column 4, the first character of the field
|
|
jsr updateCursorPointers ; 771E recompute the bitmap and colour-RAM pointers for that cell
|
|
lda #$25 ; 7721 glyph $25 = the underline character
|
|
sta textFillGlyph ; 7723 make it the fill glyph
|
|
lda #$05 ; 7725 five character cells to underline
|
|
jsr bootReceiveSectorData ; 7727 $C0EF is printFillChars: underline the five character cells (also textEngineC000:printFillChars)
|
|
lda #$00 ; 772A glyph 0 = the normal blank
|
|
sta textFillGlyph ; 772C restore the normal blank glyph
|
|
sta storeRngSeedByte2 ; 772E $0313 = number of characters typed so far = 0 (also trainerPlaybook0200:play1Row25)
|
|
sta scenarioSeed0 ; 7731 clear the 24-bit seed accumulator...
|
|
sta scenarioSeed1 ; 7733 seed byte 1
|
|
sta scenarioSeed2 ; 7735 ...all three bytes
|
|
jsr clearMapNameField ; 7737 and blank the five name characters
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Input loop: one pass per frame. It services the opponent link, handles F1 (flip), tests the fire
|
|
; button and then consumes at most one key event.
|
|
; ----------------------------------------------------------------------
|
|
mapNameInputLoop:
|
|
jsr serviceLinkWhileGenerating; 773A keep the modem/trainer exchange alive while the player types
|
|
lda requestedScreenKey ; 773D function key from the IRQ poll: 0-3 = F1/F3/F5/F7, $FF = none
|
|
bne checkAcceptButton ; 773F not F1 (function key code 0) - no flip
|
|
toggleMapFlip:
|
|
lda mapFlipFlag ; 7741 F1: flip the map end for end
|
|
eor #$01 ; 7744 toggle bit 0
|
|
sta mapFlipFlag ; 7746 store the new flip state
|
|
jsr rotateMapAndMirrorCodes ; 7749 rotate the map 180 degrees and mirror the direction-dependent terrain codes
|
|
jsr drawOverviewMap ; 774C redraw the overview window
|
|
lda #$00 ; 774F A = 0 so the fire test below starts from a known value (A is not used by it)
|
|
checkAcceptButton:
|
|
jsr checkFirePressed ; 7751 fresh press of the fire button? (returns A=0 on a new press)
|
|
bne handleTypedKey ; 7754 no press - carry on reading keys
|
|
lda #$01 ; 7756 sound 1 = the click checkFirePressed has just played; this repeats it
|
|
jsr playSound ; 7758 a second click on top of the one checkFirePressed already played
|
|
sec ; 775B C=1 tells the caller 'accepted, no new name'
|
|
rts ; 775C return to the caller with 'accepted'
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Key handling. newKeyEvent ($90EA) holds the last key the IRQ saw, $FF when there is none; it is
|
|
; reset to $FF once the key has been dealt with.
|
|
; ----------------------------------------------------------------------
|
|
handleTypedKey:
|
|
lda newKeyEvent ; 775D last key event
|
|
cmp #$FF ; 7760 $FF means the IRQ has not seen a key
|
|
beq checkNameComplete ; 7762 no key this frame - go on to the completion tests
|
|
ldy storeRngSeedByte2 ; 7764 characters typed so far (also trainerPlaybook0200:play1Row25)
|
|
bne checkDeleteKey ; 7767 not the first one - keep what is on screen
|
|
pha ; 7769 first keystroke: save the key
|
|
jsr setMapNameCursor ; 776A cursor back to the start of the field
|
|
jsr bootToggleDataAck ; 776D and wipe the underlined field (also textEngineC000:clearToEndOfLine)
|
|
pla ; 7770 restore the key
|
|
checkDeleteKey:
|
|
cmp #$88 ; 7771 $88 = the DEL key
|
|
bne lookUpKeyInAlphabet ; 7773 not DEL - try the alphabet
|
|
ldy storeRngSeedByte2 ; 7775 characters typed so far (also trainerPlaybook0200:play1Row25)
|
|
beq keyRejected ; 7778 field already empty - just beep
|
|
dec storeRngSeedByte2 ; 777A one character less (also trainerPlaybook0200:play1Row25)
|
|
jsr printChar ; 777D printChar sees $88 as a backspace: step back, erase the glyph, step back again
|
|
ldy #$00 ; 7780 offset 0 = the cell the cursor has just stepped back to
|
|
lda #$11 ; 7782 colour $11 = the cursor block
|
|
sta (screenPtr),y ; 7784 mark the freed cell as the cursor (screenPtr points into colour RAM here)
|
|
iny ; 7786 offset 1 = the cell to its right
|
|
lda textColour ; 7787 normal text colour
|
|
sta (screenPtr),y ; 7789 restore the cell to the right, which held the cursor block before the delete
|
|
ldy storeRngSeedByte2 ; 778B index of the freed character (also trainerPlaybook0200:play1Row25)
|
|
lda #$20 ; 778E space
|
|
storeBlankInNameField:
|
|
sta mapNameText,y ; 7790 blank it in the name buffer $0BA9-$0BAD
|
|
bne playKeyClick ; 7793 always taken ($20 is not 0) - play the key click
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Ordinary character: look it up in the 32-entry map-name alphabet at $890C. A key that is not in the
|
|
; alphabet is rejected with the error beep.
|
|
; ----------------------------------------------------------------------
|
|
lookUpKeyInAlphabet:
|
|
sta scratch18 ; 7795 keep the raw key (bit 7 may be set by the keyboard scanner)
|
|
and #$7F ; 7797 strip bit 7 to get plain ASCII
|
|
ldy #$1F ; 7799 search the alphabet backwards from index 31
|
|
alphabetSearchLoop:
|
|
cmp mapNameAlphabet,y ; 779B mapNameAlphabet[y]: space, A-Z, +, -, *, /, =
|
|
beq storeNameCharacter ; 779E found it - Y is the 5-bit code of the character
|
|
dey ; 77A0 try the previous alphabet entry
|
|
bpl alphabetSearchLoop ; 77A1 keep searching down to index 0
|
|
keyRejected:
|
|
lda #$03 ; 77A3 sound 3 = the reject beep
|
|
bne playKeySound ; 77A5 always taken (A = 3) - play the reject beep
|
|
storeNameCharacter:
|
|
ldy storeRngSeedByte2 ; 77A7 index of the next free character (also trainerPlaybook0200:play1Row25)
|
|
sta mapNameText,y ; 77AA store the character in the name buffer
|
|
inc storeRngSeedByte2 ; 77AD one character more (also trainerPlaybook0200:play1Row25)
|
|
lda scratch18 ; 77B0 the raw key as typed
|
|
jsr printChar ; 77B2 draw it into the field
|
|
lda storeRngSeedByte2 ; 77B5 characters typed so far (also trainerPlaybook0200:play1Row25)
|
|
cmp #$05 ; 77B8 are all five characters of the field filled now?
|
|
bcs playKeyClick ; 77BA yes - no cursor block past the end of the field
|
|
ldy #$00 ; 77BC offset 0 = the cell the cursor has just advanced to
|
|
lda #$11 ; 77BE colour $11 = the cursor block
|
|
sta (screenPtr),y ; 77C0 mark the next cell as the cursor
|
|
playKeyClick:
|
|
lda #$02 ; 77C2 sound 2 = the key click
|
|
playKeySound:
|
|
jsr playSound ; 77C4 play it
|
|
lda #$FF ; 77C7 $FF = no key pending
|
|
sta newKeyEvent ; 77C9 mark the key event as consumed
|
|
|
|
; ----------------------------------------------------------------------
|
|
; After every frame: five characters means the name is complete, otherwise F5/F7 may still pick a
|
|
; random one. nextGameRandom is stepped every pass so the random seed depends on how long the player
|
|
; took.
|
|
; ----------------------------------------------------------------------
|
|
checkNameComplete:
|
|
ldy storeRngSeedByte2 ; 77CC characters typed (also trainerPlaybook0200:play1Row25)
|
|
cpy #$05 ; 77CF five = a complete map name
|
|
bcs packNameIntoSeed ; 77D1 pack it into the 24-bit seed
|
|
jsr nextGameRandom ; 77D3 stir the game RNG once per frame
|
|
lda gameRngState0 ; 77D6 low byte of the new RNG state
|
|
ldy requestedScreenKey ; 77D8 function key: 0-3 = F1/F3/F5/F7, $FF = none
|
|
bmi waitForMoreInput ; 77DA $FF - no function key, keep waiting
|
|
checkRandomKeys:
|
|
cpy #$02 ; 77DC F5 has function key code 2, F7 has 3
|
|
bcs checkSymmetricRandom ; 77DE one of them - take a random map
|
|
waitForMoreInput:
|
|
jmp mapNameInputLoop ; 77E0 F1 or F3 - back to the input loop
|
|
|
|
checkSymmetricRandom:
|
|
bne storeRandomSeed ; 77E3 F7 (3) gives a completely random map
|
|
ora #$1F ; 77E5 F5 (2) sets the low five bits, so the name ends in '=' = a point-symmetric map
|
|
storeRandomSeed:
|
|
sta scenarioSeed0 ; 77E7 seed byte 0 = the RNG low byte
|
|
ldy gameRngState1 ; 77E9 RNG byte 1
|
|
sty scenarioSeed1 ; 77EB seed byte 1
|
|
ldx gameRngState2 ; 77ED RNG byte 2
|
|
stx scenarioSeed2 ; 77EF seed byte 2
|
|
jsr seedToMapName ; 77F1 spell the seed out as five characters in $0BA9-$0BAD (A/Y/X already hold it)
|
|
jmp showNameAndAccept ; 77F4 show it and return C=0
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pack the five typed characters into the 24-bit seed: index of the character in the 32-entry
|
|
; alphabet, five bits each, first character in the most significant bits.
|
|
; ----------------------------------------------------------------------
|
|
packNameIntoSeed:
|
|
ldy #$00 ; 77F7 start at the first character, and use Y as the zero to clear the accumulator
|
|
clearSeedAccumulator:
|
|
sty scenarioSeed0 ; 77F9 seed = 0...
|
|
sty scenarioSeed1 ; 77FB seed byte 1
|
|
sty scenarioSeed2 ; 77FD ...all 24 bits
|
|
packNameCharLoop:
|
|
lda mapNameText,y ; 77FF character Y of the name
|
|
ldx #$1F ; 7802 search the alphabet backwards from index 31
|
|
alphabetIndexLoop:
|
|
cmp mapNameAlphabet,x ; 7804 compare the character with alphabet entry X
|
|
beq shiftSeedLeftFive ; 7807 found - X is its 5-bit code
|
|
dex ; 7809 try the previous alphabet entry
|
|
bpl alphabetIndexLoop ; 780A search all 32 alphabet entries
|
|
inx ; 780C unreachable: every character in the buffer was validated when it was typed...
|
|
brk ; 780D ...so this BRK (which would crash) is really an assertion
|
|
shiftSeedLeftFive:
|
|
stx scratch18 ; 780E keep the 5-bit character code
|
|
ldx #$05 ; 7810 shift the accumulator five bits left
|
|
shiftSeedBitLoop:
|
|
asl scenarioSeed0 ; 7812 24-bit shift left by one...
|
|
rol scenarioSeed1 ; 7814 carry into seed byte 1...
|
|
rol scenarioSeed2 ; 7816 ...and on into seed byte 2
|
|
dex ; 7818 count the five shifts
|
|
bne shiftSeedBitLoop ; 7819 five single-bit shifts in all
|
|
lda scenarioSeed0 ; 781B low byte of the accumulator
|
|
and #$E0 ; 781D redundant: the five ASLs already left bits 0-4 clear
|
|
ora scratch18 ; 781F merge the character code into the low five bits
|
|
sta scenarioSeed0 ; 7821 store it back
|
|
iny ; 7823 next character of the name
|
|
cpy #$05 ; 7824 all five characters packed?
|
|
bcc packNameCharLoop ; 7826 keep packing until all five are in
|
|
showNameAndAccept:
|
|
jsr printMapName ; 7828 print the finished name in the field
|
|
D_782B:
|
|
clc ; 782B C=0: the caller gets a new seed in $5A-$5C
|
|
rts ; 782C return to the caller with the new seed
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setMapNameCursor - Moves the text cursor to the start of the map-name field: column 4 of the current
|
|
; text window, row 16.
|
|
; In: the text window set up by the caller (left column zp_3B)
|
|
; Out: textCursorCol zp_3F = 4, textCursorRow zp_40 = $10, cursor pointers zp_B4-zp_B7 recomputed
|
|
; Called from: enterMapName ($776A) and printMapName ($7841)
|
|
; ----------------------------------------------------------------------
|
|
setMapNameCursor:
|
|
lda #$04 ; 782D the map-name field starts at column 4
|
|
sta textCursorCol ; 782F set it as the cursor column
|
|
lda #$10 ; 7831 row 16
|
|
jmp setCursorRow ; 7833 sets the row and recomputes the cell pointers
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearMapNameField - Fills the five map-name characters $0BA9-$0BAD with spaces.
|
|
; In: none
|
|
; Out: mapNameText $0BA9-$0BAD = $20, Y = $FF
|
|
; Called from: mapNameEntryScreen ($76BB), enterMapName ($7737) and the manual look-up copy
|
|
; protection ($09F6)
|
|
; ----------------------------------------------------------------------
|
|
clearMapNameField:
|
|
ldy #$04 ; 7836 five characters, index 4 down to 0
|
|
clearNameCharLoop:
|
|
lda #$20 ; 7838 space
|
|
sta mapNameText,y ; 783A mapNameText[y]
|
|
dey ; 783D walk down to character 0
|
|
L_783E:
|
|
bpl clearNameCharLoop ; 783E until index 0 has been written
|
|
rts ; 7840 the field is blank
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printMapName - Prints the five-character map name at its place on the setup screen. The byte after
|
|
; the name, $0BAE, is used as the string terminator: $8D is a carriage return ($0D) with bit 7 set, so
|
|
; the name is printed and the cursor drops to the next row.
|
|
; In: mapNameText $0BA9-$0BAD
|
|
; Out: mapNameTerminator $0BAE = $8D, the name drawn at column 4 row 16, cursor on row 17
|
|
; Called from: enterMapName ($7717 and $7828)
|
|
; ----------------------------------------------------------------------
|
|
printMapName:
|
|
jsr setMapNameCursor ; 7841 cursor to column 4, row 16
|
|
lda #$8D ; 7844 $8D = CR with bit 7 = 'last character of the string'
|
|
setMapNameTerminator:
|
|
sta mapNameTerminator ; 7846 terminate the name after five characters
|
|
lda #$A9 ; 7849 string low byte...
|
|
ldy #$0B ; 784B ...and high byte: mapNameText $0BA9
|
|
jmp printString ; 784D print it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; seedToMapName - Spells a 24-bit map seed out as its five-character name: five times take the low
|
|
; five bits as an index into the 32-character alphabet at $890C and shift the seed right by five, so
|
|
; the last character carries the lowest bits. This is the exact inverse of the packing loop at
|
|
; $77F7, and it is why a map's name and its seed are the same number.
|
|
; In: A = seed byte 0, Y = seed byte 1, X = seed byte 2 (the same order as $5A/$5B/$5C)
|
|
; Out: mapNameText $0BA9-$0BAD; zp_18-zp_1A used as the shift register; X and Y clobbered
|
|
; Called from: runMapGeneratorPasses ($6F36, to name the map it has just built) and enterMapName
|
|
; ($77F1, after an F5/F7 random seed)
|
|
; ----------------------------------------------------------------------
|
|
seedToMapName:
|
|
sta scratch18 ; 7850 shift register byte 0
|
|
sty scratch19 ; 7852 byte 1
|
|
stx scratch1A ; 7854 byte 2
|
|
ldy #$04 ; 7856 fill the name from the last character backwards
|
|
seedCharLoop:
|
|
lda scratch18 ; 7858 low byte of the remaining seed
|
|
and #$1F ; 785A the low five bits are one character
|
|
tax ; 785C use the 5-bit code as an index into the alphabet
|
|
lda mapNameAlphabet,x ; 785D mapNameAlphabet: space, A-Z, +, -, *, /, =
|
|
sta mapNameText,y ; 7860 store it in the name buffer
|
|
shiftSeedRightFive:
|
|
ldx #$05 ; 7863 shift the seed right by five bits
|
|
shiftSeedRightBit:
|
|
lsr scratch1A ; 7865 24-bit shift right by one...
|
|
ror scratch19 ; 7867 carry down into byte 1...
|
|
ror scratch18 ; 7869 ...and on into byte 0
|
|
dex ; 786B count the five shifts
|
|
bne shiftSeedRightBit ; 786C five single-bit shifts in all
|
|
dey ; 786E next character to the left
|
|
L_786F:
|
|
bpl seedCharLoop ; 786F loop over all five characters
|
|
rts ; 7871 the five characters are in $0BA9-$0BAD
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; randomTileVariant - Picks a random visual variant of a terrain code so that large areas of the same
|
|
; terrain do not look tiled. The mask table at $0770 (tileVariantMaskTable, part of the terrain rules
|
|
; loaded from track 18) is indexed by tile code - $40, or tile code - $20 for the codes below $40, and
|
|
; gives 0, 1 or 3 = the number of extra variants; the result is the base code plus a random value
|
|
; masked with it. Y is preserved through a self-modified LDY #imm.
|
|
; In: A = base terrain code, tileVariantMaskTable $0770, scenario RNG
|
|
; Out: A = the code with random variant bits added; Y preserved, X untouched. Self-modifies the
|
|
; operands at $788D (AND #mask), $7890 (ADC #base) and $7892 (LDY #savedY)
|
|
; Called from: the hill/forest fill pass ($72A4) and the contour pass ($74E7)
|
|
; ----------------------------------------------------------------------
|
|
randomTileVariant:
|
|
nop ; 7872 leftover NOP - probably a patched-out instruction
|
|
sta addBaseTileCode+1 ; 7873 remember the base tile code in the ADC #imm at $788F
|
|
sec ; 7876 set carry for the subtraction
|
|
sbc #$40 ; 7877 terrain codes $40 and up index the mask table directly
|
|
adjustLowTileIndex:
|
|
bcs lookUpVariantMask ; 7879 code was $40 or higher - the index is code - $40
|
|
adc #$20 ; 787B code below $40: add $20 back, giving an index of code - $20
|
|
lookUpVariantMask:
|
|
sty restoreCallerY+1 ; 787D save Y in the LDY #imm at $7891 so the caller's Y survives
|
|
tay ; 7880 index into the variant mask table
|
|
lda tileVariantMaskTable,y ; 7881 number of extra variants as a bit mask: 0, 1 or 3
|
|
beq combineVariantWithBase ; 7884 0 = this terrain has only one look, A is already the answer
|
|
sta maskRandomVariant+1 ; 7886 patch the mask into the AND #imm at $788C
|
|
jsr nextScenarioRandom ; 7889 random byte
|
|
maskRandomVariant:
|
|
and #$FF ; 788C keep only as many random bits as the terrain has variants (operand patched above)
|
|
combineVariantWithBase:
|
|
clc ; 788E clear carry for the add
|
|
addBaseTileCode:
|
|
adc #$FF ; 788F add the base tile code back (operand patched at $7873)
|
|
restoreCallerY:
|
|
ldy #$FF ; 7891 restore the caller's Y (operand patched at $787D)
|
|
D_7893:
|
|
rts ; 7893 A is the terrain code to paint
|
|
|
|
|
|
; The 6x6 reservation block used by markRiverBanks, expressed as offsets from its top-left cell. The
|
|
; map row stride is $28 (40 cells). Entries 0-3 are the left column (rows 1-4) and are also used on
|
|
; their own near the map edges, where they address the four cells above and below the river in its own
|
|
; column; entries 4-9 are the top row, 10-13 the right column (rows 1-4) and 14-19 the bottom row.
|
|
; Together they are the 20-cell perimeter, marked $80.
|
|
riverZoneOuterOffsets:
|
|
.byte $28,$50,$78,$A0 ; 7894 (Px. left column, rows 1-4 ($28 = one row): also the edge-column-only case
|
|
D_7898:
|
|
.byte $00 ; 7898 top row, columns 0-5, then the right column rows 1-2 ($2D = row 1 col 5, $55 = row 2 col 5)
|
|
.byte $01,$02,$03,$04,$05,$2D,$55; 7899 .....-U
|
|
.byte $7D ; 78A0 right column rows 3-4, then the bottom row ($C8 = row 5 col 0) columns 0-5
|
|
.byte $A5,$C8,$C9,$CA,$CB,$CC,$CD; 78A1 .......
|
|
|
|
; The inner 4x4 cells (rows 1-4, columns 1-4) of the same 6x6 block: the ground that actually touches
|
|
; the water, marked $81 by markRiverBanks.
|
|
riverZoneInnerOffsets:
|
|
.byte $29,$2A,$2B,$2C,$51,$52,$53,$54; 78A8 )*+,QRST rows 1 and 2, columns 1-4
|
|
.byte $79 ; 78B0 rows 3 and 4, columns 1-4
|
|
.byte $7A,$7B,$7C,$A1,$A2,$A3,$A4; 78B1 z{|....
|
|
|
|
; Which of the four 400-byte unit start-position templates at $F000 (+0, +400, +800, +1200) each game
|
|
; type uses, indexed by gameTypeOptions & 7: SCRIMAGE, QB SNEAK and THE BOMB share template 2,
|
|
; FACE-OFF uses 1, SLUGGERS 3, FULL WAR and DEFENDER 0.
|
|
unitLayoutByGameType:
|
|
.byte $02 ; 78B8 . game type 0 SCRIMAGE -> template 2
|
|
L_78B9:
|
|
.byte $02 ; 78B9 types 1 QB SNEAK, 2 THE BOMB -> 2; 3 FACE-OFF -> 1; 4 SLUGGERS -> 3; 5 FULL WAR, 6 DEFENDER -> 0
|
|
.byte $02,$01,$03,$00,$00 ; 78BA .....
|
|
|
|
; Translates the COMCEN SPEED option (0-3, as edited by editComcenSpeed) into the engine's internal
|
|
; speed class. Applied to both entries of comcenSpeedClass $92B4/$92B5 at $7B6E.
|
|
comcenSpeedClassMap:
|
|
.byte $04,$00,$01,$05 ; 78BF .... setting 0 -> class 4, 1 -> 0, 2 -> 1, 3 -> 5
|
|
|
|
; Game length in rounds for each game type (gameTypeOptions & 7), loaded into gameClock $91CB by
|
|
; setGameClockForType ($7D8B). A quarter of this value is the last-quarter warning round $92EF.
|
|
roundsByGameType:
|
|
.byte $3F,$3F,$1F,$7F ; 78C3 ??.. SCRIMAGE 63, QB SNEAK 63, THE BOMB 31, FACE-OFF 127
|
|
L_78C7:
|
|
.byte $7F ; 78C7 SLUGGERS 127, FULL WAR 254, DEFENDER 254
|
|
.byte $FE,$FE ; 78C8 ..
|
|
|
|
; Default gameTypeOptions byte for each of the seven items of the game-type menu, read at $828F. Bits
|
|
; 0-2 are the game type itself; bit 3 (UNIT MENUS) and bit 4 (DAMAGE) are preset for the four long
|
|
; scenarios.
|
|
gameTypeOptionsByMenuItem:
|
|
.byte $00 ; 78CA . item 0 SCRIMAGE -> $00
|
|
sub_78CB:
|
|
.byte $01 ; 78CB QB SNEAK $01, THE BOMB $02, FACE-OFF $13, SLUGGERS $14, FULL WAR $1D, DEFENDER $1E
|
|
.byte $02,$13,$14,$1D,$1E ; 78CC .....
|
|
|
|
; ----------------------------------------------------------------------
|
|
; startGameSession - The whole 'start a game' sequence, from a cleared battle state to the first frame
|
|
; of play. It resets the per-battle variables, then either branches off to the game-film replay path
|
|
; or negotiates the live game: agree which player is side 0 and which is side 1, let side 0 pick the
|
|
; game type and the map and send the map seed as command $8B (or $A6 for a custom map), exchange
|
|
; player names and check the opponent's build, load the unit start template, record the whole setup in
|
|
; the film header at $D000, generate and (for side 1) rotate the battlefield, run the unit deployment
|
|
; phase, and finally set the clock, place the units and the recyclers and jump into the battle.
|
|
; In: the settings block $0B95-$0BB4 (map seed, film mode, link state, side, game type, trainer
|
|
; flag), the comm module behind the $E000 jump table, the unit start templates at $F000
|
|
; Out: the complete battle state - map, units, clock, film header and message slots; playerSide
|
|
; $0B9F resolved to 0 or 1 and the side-dependent branch opcodes patched. Does not return:
|
|
; ends in JMP finaliseRecyclerSetup ($EE26) or JMP startFilmPlayback ($7CD1)
|
|
; Called from: startGameFromSetup ($0B40)
|
|
; Reset everything the previous battle may have left behind. $FF is the 'none/unset' marker for the
|
|
; redraw requests, the clock, the last ordered group and both recycler positions; unitCol[0] =
|
|
; $FF additionally tells loadUnitStartTemplate ($ECF3) that the unit arrays still have to be filled
|
|
; from the template on disk.
|
|
; ----------------------------------------------------------------------
|
|
startGameSession:
|
|
jsr setFilmLimitToBufferEnd ; 78D1 film recording may use the whole buffer up to $DFF0
|
|
lda #$FF ; 78D4 $FF = 'none' for everything that follows
|
|
sta gamePhase ; 78D6 gamePhase != 0: not in the battle loop yet
|
|
sta pendingRedrawGroup ; 78D8 no group redraw pending
|
|
sta pendingRedrawUnit ; 78DB no unit redraw pending
|
|
sta gameClock ; 78DE game clock not started
|
|
sta lastOrderedGroupKey ; 78E1 no group has been given an order yet
|
|
sta unitColTable ; 78E4 unitCol[0] = $FF marks the unit arrays as 'still the disk template'
|
|
clearRecyclerPositions:
|
|
sta recyclerCol ; 78E7 side 0 has no recycler placed yet
|
|
sta side1RecyclerCol ; 78EA nor side 1
|
|
lda #$32 ; 78ED 50 = a comcen at full energy
|
|
sta comcenStatusSnapshot ; 78EF comcen status snapshot starts at full
|
|
sta lastQuarterRound ; 78F2 last-quarter warning round; overwritten with rounds/4 at $7BA3
|
|
lda #$02 ; 78F5 colour 2 = red
|
|
sta enemyUnitColour ; 78F7 the opponent's units are drawn red
|
|
lda #$09 ; 78F9 colour 9 = brown
|
|
sta droneRadarEnemyColour ; 78FB enemy blips on the drone radar
|
|
lda #$06 ; 78FE colour 6 = blue
|
|
setOwnUnitColour:
|
|
sta ownUnitColour ; 7900 the local player's units are drawn blue
|
|
lda #$0B ; 7902 colour 11 = dark grey
|
|
sta droneRadarOwnColour ; 7904 own blips on the drone radar
|
|
lda #$55 ; 7907 %01010101 = all four pixels of an overview blip in multicolour pattern 1
|
|
sta overviewUnitPattern ; 7909 bit pattern used for a unit in the 20x20 overview map
|
|
lda #$00 ; 790C 0 for the three variables below
|
|
sta gameEndReason ; 790E no end-of-game reason yet
|
|
sta D_57C3 ; 7911 reset the base of the film's delta timestamps (operand of the SBC in writeFilmTimestamp)
|
|
sta D_4DF4 ; 7914 and the matching base in the film replay decoder (operand of the ADC at $4DF3)
|
|
lda #$03 ; 7917 screen id 3
|
|
sta currentScreen ; 7919 screen 3 while the game starts up
|
|
checkFilmPlayback:
|
|
lda filmPlaybackMode ; 791C WATCH GAME FILM?
|
|
bpl startLiveGame ; 791F bit 7 clear - not a replay, start a live game
|
|
jmp startFilmPlayback ; 7921 yes - the replay path sets everything up from the film header instead
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Live game. First decide which side this machine plays. playerSide $0B9F is used both as a
|
|
; *request* (0 = 'I want side 0', $FF = 'I want side 1', anything else = 'no preference') and,
|
|
; after cmdChooseSide ($5511) has compared the two requests, as the answer (0 or 1).
|
|
; ----------------------------------------------------------------------
|
|
startLiveGame:
|
|
lda isSoloTrainer ; 7924 solo trainer flag (bit 7)
|
|
sta revealAllUnits ; 7927 in a trainer game the opponent's units are always visible
|
|
lda #$04 ; 792A the engine steps once every four frames
|
|
sta stepIntervalFrames ; 792C the engine advances the battle every 4th frame
|
|
lda playerSide ; 792F side left over from the previous game, or $80|random after a tie
|
|
bpl alternateSideFromLastGame; 7932 a real side (0 or 1) - alternate from it
|
|
requestSideFromLink:
|
|
lda #$01 ; 7934 1 = 'no preference': let the tie-break value below decide
|
|
sta playerSide ; 7936 store the 'no preference' marker
|
|
bne openLinkForSession ; 7939 always taken (A = 1)
|
|
alternateSideFromLastGame:
|
|
inc playsFirstFlag ; 793B count games played on the same setup
|
|
ldy playsFirstFlag ; 793E read the counter back
|
|
cpy #$02 ; 7941 has the same setup now been played twice?
|
|
bcc encodeSidePreference ; 7943 not yet - keep the same side as last time
|
|
ldy #$00 ; 7945 wrap the counter to 0
|
|
sty playsFirstFlag ; 7947 restart the count
|
|
eor #$01 ; 794A ...and swap sides
|
|
encodeSidePreference:
|
|
cmp #$00 ; 794C is the requested side 0?
|
|
beq storeSidePreference ; 794E yes - ask for side 0
|
|
lda #$FF ; 7950 side 1 is requested as $FF, the highest value, so cmdChooseSide awards it side 1
|
|
storeSidePreference:
|
|
sta playerSide ; 7952 this is the side we will ask for
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Bring the link up. A session that is already running only needs the 'ready for a new game'
|
|
; handshake; otherwise the modem is opened and we wait for carrier. On the solo-trainer build both
|
|
; calls return at once.
|
|
; ----------------------------------------------------------------------
|
|
openLinkForSession:
|
|
lda linkSessionState ; 7955 link session state ($FF = a session is already established)
|
|
bpl openLinkFresh ; 7958 no session yet - dial/answer
|
|
jsr readyForNewGameHandshake; 795A exchange the [$8C,02] READY packets with the opponent
|
|
lda isLinkActive ; 795D is the comm module still live? (also trainerAiE000:isLinkActive)
|
|
bne resolveSidePreference ; 7960 yes - skip the connection wait
|
|
openLinkFresh:
|
|
jsr openCommLink ; 7962 'PICK UP PHONE THEN PRESS SPACE.' and open the link
|
|
jsr waitForConnection ; 7965 'WAITING FOR CONNECTION...' until carrier; leaves its countdown in zp_18
|
|
resolveSidePreference:
|
|
lda playerSide ; 7968 did we ask for side 0?
|
|
beq sendSideChoice ; 796B yes - send that
|
|
cmp #$FF ; 796D or for side 1?
|
|
beq sendSideChoice ; 796F yes - send that
|
|
lda scratch18 ; 7971 no preference: use the carrier-wait countdown as a random tie-break value
|
|
sta playerSide ; 7973 that is what we will ask for
|
|
sendSideChoice:
|
|
jsr sendChooseSide ; 7976 queue command $9D with our request (also copied to $90F8 for the comparison)
|
|
D_7979:
|
|
jsr sendOutgoingPacket ; 7979 push the packet out now
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait until cmdChooseSide ($5511) has seen the opponent's request and written the agreed side; that
|
|
; handler sets gamePhase to $DF, clearing bit 5, which is the flag this loop watches.
|
|
; ----------------------------------------------------------------------
|
|
waitForSideAgreement:
|
|
jsr waitForExchangeIdle ; 797C run frames until the packet exchange is idle
|
|
lda gamePhase ; 797F phase flags
|
|
and #$20 ; 7981 bit 5 = 'side selection still running'
|
|
bne waitForSideAgreement ; 7983 still not agreed - keep exchanging
|
|
lda #$FF ; 7985 $FF = setup phase, no drawing yet
|
|
markSetupPhase:
|
|
sta gamePhase ; 7987 gamePhase = $FF: setup, not battle
|
|
lda #$01 ; 7989 1 = a game session is now running on the link
|
|
sta linkSessionState ; 798B a game session is now established on the link
|
|
jsr patchOwnSideBranches ; 798E patch the eleven self-modified 'is this unit mine?' branches for our side
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Side 0 chooses the game type and the map and tells side 1 about it; side 1 just shows 'OPPONENT
|
|
; PICKING GAME/MAP.' and waits for the $8B/$A6 command.
|
|
; ----------------------------------------------------------------------
|
|
chooseOrAwaitGameAndMap:
|
|
lda playerSide ; 7991 are we side 0, the player who picks?
|
|
beq pickGameTypeAndMap ; 7994 side 0 - we are the player who picks the game and the map
|
|
lda #$1A ; 7996 message $1A = 'OPPONENT PICKING GAME/MAP.'
|
|
sta persistentMessageId ; 7998 keep it on screen until something replaces it
|
|
jsr queueMessage ; 799B queue it for display
|
|
jmp afterMapSeedExchange ; 799E and wait for the map seed to arrive
|
|
|
|
pickGameTypeAndMap:
|
|
jsr beginGameTypeSelection ; 79A1 run the GAME TYPE / MAP TYPE / TYPE MAP NAME setup screens
|
|
jsr showWorkingMessage ; 79A4 'WORKING...'
|
|
lda mapSeed ; 79A7 map seed byte 0...
|
|
sta cmdParam0 ; 79AA ...as command argument 0
|
|
lda mapSeed1 ; 79AC seed byte 1
|
|
sta cmdParam1 ; 79AF argument 1
|
|
lda mapSeed2 ; 79B1 seed byte 2
|
|
sta cmdParam2 ; 79B4 argument 2
|
|
lda #$8B ; 79B6 command $8B = set map seed
|
|
ldy mapFlipFlag ; 79B8 is the map flipped?
|
|
beq queueMapSeedCommand ; 79BB map not flipped - keep plain command $8B
|
|
lda #$A6 ; 79BD command $A6 = set map seed and mark it a custom/flipped map
|
|
queueMapSeedCommand:
|
|
jsr queueCmd4 ; 79BF queue the command with its three argument bytes
|
|
afterMapSeedExchange:
|
|
jsr waitForMapSeedCommand ; 79C2 both sides wait here until gamePhase bit 7 is cleared by cmdSetMapSeed
|
|
lda #$FF ; 79C5 $FF = 'no input allowed'
|
|
sta inputLockoutTimer ; 79C7 block the joystick/keyboard for the rest of the setup
|
|
clearPendingScreenKey:
|
|
sta requestedScreenKey ; 79CA and forget any pending F1-F7 screen request
|
|
lda #$DB ; 79CC $DB is this build's version stamp
|
|
sta selectedUnit ; 79CE exchangePlayerNamesAndSettings overwrites $90F8 with the EOR of the peer's version bytes
|
|
jsr exchangePlayerNamesAndSettings; 79D1 swap the ten-character player names and the settings block over the link (also trainerAiE000:L_ED43)
|
|
lda #$7F ; 79D4 $7F
|
|
sta gamePhase ; 79D6 gamePhase $7F: setup phase, drawing enabled
|
|
checkOpponentVersion:
|
|
lda selectedUnit ; 79D8 did the version handshake come back as our own stamp?
|
|
cmp #$DB ; 79DB compare it with our own version stamp
|
|
beq loadTemplateAndDefaults ; 79DD yes - the opponent runs the same build
|
|
lda #$5E ; 79DF string low byte...
|
|
ldy #$89 ; 79E1 ...and high byte: 'OPPONENT USING NON-STANDARD GAME.' at $895E
|
|
jsr showModemSetupPrompt ; 79E3 show it plus 'PRESS SPACE WHEN READY.' and wait for the space bar
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Both sides now know the game type and the map, so they can build the same starting position and
|
|
; write the same setup into the film header at $D000.
|
|
; ----------------------------------------------------------------------
|
|
loadTemplateAndDefaults:
|
|
jsr loadUnitStartTemplate ; 79E6 copy the unit start-position template for this game type into $F640 (also trainerAiE000:L_ECF3, trainerSetupEC00:copyPlanRecordLoop)
|
|
jsr applyGameTypeDefaults ; 79E9 load the per-side scenario parameters (drones, missiles, comcen speed, recycler mode)
|
|
jsr sendPhaseSyncAndWait ; 79EC command $9E: wait until both machines have got this far
|
|
jsr showWorkingMessage ; 79EF 'WORKING...'
|
|
ldy #$2B ; 79F2 film header offset $2B = the map seed
|
|
ldx #$00 ; 79F4 start with seed byte 0
|
|
recordMapSeedLoop:
|
|
lda mapSeed,x ; 79F6 seed byte X
|
|
jsr writeFilmByte ; 79F9 write it into the film header
|
|
iny ; 79FC next film header offset
|
|
inx ; 79FD next seed byte
|
|
cpx #$03 ; 79FE all three seed bytes written?
|
|
bcc recordMapSeedLoop ; 7A00 keep writing the seed into the film header
|
|
jsr generateBattlefieldMap ; 7A02 build the 40x40 battlefield from the seed (this also spells out the map name)
|
|
lda playerSide ; 7A05 our side
|
|
sta setupPlayerSide ; 7A08 remember the side for the setup screens
|
|
|
|
; ----------------------------------------------------------------------
|
|
; With CUSTOM map type (gameTypeOptions bit 7) the map is not one of the maps printed in the manual,
|
|
; so its name is marked with a trailing '!' and hidden from the player who did not choose it - side 1
|
|
; sees '#####' instead of the five characters.
|
|
; ----------------------------------------------------------------------
|
|
lda gameTypeOptions ; 7A0B game options
|
|
bpl terminateMapName ; 7A0E bit 7 clear = STANDARD map type, name stays as generated
|
|
lda #$21 ; 7A10 '!' replaces the carriage return after the name
|
|
sta mapNameTerminator ; 7A12 so the name prints as five characters plus '!'
|
|
lda playerSide ; 7A15 our side
|
|
beq terminateMapName ; 7A18 side 0 chose the map and may see its name
|
|
ldy #$04 ; 7A1A five characters
|
|
lda #$23 ; 7A1C '#'
|
|
hideMapNameLoop:
|
|
sta mapNameText,y ; 7A1E blank out the name for the player who did not pick it
|
|
dey ; 7A21 all five characters
|
|
bpl hideMapNameLoop ; 7A22 loop over all five characters
|
|
terminateMapName:
|
|
lda mapNameTerminator ; 7A24 the byte after the name
|
|
ora #$80 ; 7A27 set bit 7 so the text engine stops there
|
|
sta mapNameTerminator ; 7A29 write the terminator back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Alternation bookkeeping: playsFirstFlag $0BA1 makes the two players swap sides every second game,
|
|
; but only while the setup stays the same. Changing the game type, the map seed or the flip resets it
|
|
; to 1. The previous map's name is cached at $0BAF so a repeated map keeps the name the player typed
|
|
; rather than the one derived from the seed.
|
|
; ----------------------------------------------------------------------
|
|
lda gameTypeOptions ; 7A2C game options
|
|
cmp prevGameTypeOptions ; 7A2F same options as the last game?
|
|
storePrevGameTypeOptions:
|
|
sta prevGameTypeOptions ; 7A32 remember them either way
|
|
beq compareWithPreviousMap ; 7A35 unchanged - keep alternating
|
|
lda #$01 ; 7A37 1 = 'first game with this setup'
|
|
sta playsFirstFlag ; 7A39 options changed: restart the side alternation
|
|
compareWithPreviousMap:
|
|
lda mapSeed ; 7A3C seed byte 0...
|
|
cmp prevMapSeed ; 7A3F ...same as last game?
|
|
bne rememberNewMap ; 7A42 no - a new map
|
|
lda mapSeed1 ; 7A44 seed byte 1
|
|
cmp prevMapSeed1 ; 7A47 seed byte 1 - same as last game?
|
|
bne rememberNewMap ; 7A4A different - a new map
|
|
lda mapSeed2 ; 7A4C seed byte 2
|
|
cmp prevMapSeed2 ; 7A4F seed byte 2 - same as last game?
|
|
bne rememberNewMap ; 7A52 different - a new map
|
|
ldy #$05 ; 7A54 six bytes: the five name characters plus the terminator
|
|
restoreMapNameLoop:
|
|
lda mapNameBackup,y ; 7A56 same map as last time: restore the name it was given then
|
|
sta mapNameText,y ; 7A59 into the live name buffer
|
|
dey ; 7A5C all six bytes
|
|
bpl restoreMapNameLoop ; 7A5D loop over all six bytes
|
|
bmi checkFlipChanged ; 7A5F always taken
|
|
rememberNewMap:
|
|
lda mapSeed ; 7A61 seed byte 0
|
|
sta prevMapSeed ; 7A64 remember the new seed...
|
|
lda mapSeed1 ; 7A67 seed byte 1
|
|
sta prevMapSeed1 ; 7A6A store it
|
|
lda mapSeed2 ; 7A6D seed byte 2
|
|
sta prevMapSeed2 ; 7A70 ...all three bytes
|
|
lda #$01 ; 7A73 1 = 'first game with this setup'
|
|
sta playsFirstFlag ; 7A75 new map: restart the side alternation
|
|
saveMapNameStart:
|
|
ldy #$05 ; 7A78 six bytes: name plus terminator
|
|
saveMapNameLoop:
|
|
lda mapNameText,y ; 7A7A cache the name so a rematch on the same map shows it again
|
|
sta mapNameBackup,y ; 7A7D into the backup buffer $0BAF
|
|
dey ; 7A80 all six bytes
|
|
bpl saveMapNameLoop ; 7A81 loop over all six bytes
|
|
checkFlipChanged:
|
|
lda mapFlipFlag ; 7A83 map flip
|
|
cmp prevMapFlipFlag ; 7A86 was the map flipped the same way last game?
|
|
beq recordFilmHeader ; 7A89 unchanged - keep alternating sides
|
|
sta prevMapFlipFlag ; 7A8B remember the new flip
|
|
lda #$01 ; 7A8E 1 = 'first game with this setup'
|
|
sta playsFirstFlag ; 7A90 flip changed: restart the side alternation
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Write the setup into the film header at $D000, so a recorded game can be replayed exactly: +4
|
|
; trainer flag, +5 side, +6..+$B map name, +$C game options, +$11..$24 both player names, +$2B..$2D
|
|
; map seed, +$2E play order and flip. startFilmPlayback ($7CD1) reads them back.
|
|
; ----------------------------------------------------------------------
|
|
recordFilmHeader:
|
|
lda playsFirstFlag ; 7A93 play-order counter
|
|
sta savedPlaysFirstFlag ; 7A96 keep the play order for the end-of-game screen
|
|
lda playsFirstFlag ; 7A99 play-order counter again
|
|
asl a ; 7A9C play order in bits 1-2...
|
|
ora mapFlipFlag ; 7A9D ...and the map flip in bit 0
|
|
ldy #$2E ; 7AA0 film header offset $2E
|
|
jsr writeFilmByte ; 7AA2 record it
|
|
ldy #$04 ; 7AA5 film header offset 4
|
|
lda isSoloTrainer ; 7AA7 was this a solo-trainer game?
|
|
jsr writeFilmByte ; 7AAA record it
|
|
iny ; 7AAD offset 5
|
|
lda playerSide ; 7AAE which side the recording player was on
|
|
jsr writeFilmByte ; 7AB1 record it
|
|
iny ; 7AB4 offset 6
|
|
ldx #$00 ; 7AB5 start with the first character
|
|
recordMapNameLoop:
|
|
lda mapNameText,x ; 7AB7 map name character X
|
|
recordFilmByteAndAdvance:
|
|
jsr writeFilmByte ; 7ABA record it
|
|
iny ; 7ABD next film header offset
|
|
inx ; 7ABE next character
|
|
cpx #$06 ; 7ABF five characters plus the terminator
|
|
bcc recordMapNameLoop ; 7AC1 loop over the six name bytes
|
|
lda gameTypeOptions ; 7AC3 the game options byte
|
|
jsr writeFilmByte ; 7AC6 offset $C = the game options byte
|
|
ldy #$11 ; 7AC9 offset $11
|
|
ldx #$00 ; 7ACB start with the first name byte
|
|
recordPlayerNamesLoop:
|
|
lda ownPlayerName,x ; 7ACD $FFDE holds both ten-character player names
|
|
jsr writeFilmByte ; 7AD0 record it
|
|
iny ; 7AD3 next film header offset
|
|
inx ; 7AD4 next name byte
|
|
cpx #$14 ; 7AD5 20 bytes = both ten-character player names
|
|
bcc recordPlayerNamesLoop ; 7AD7 loop over all 20 name bytes
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Each player must see his own end zone at the bottom of the screen, so side 1 plays on a map that is
|
|
; rotated 180 degrees and on unit coordinates that are mirrored with it. A flipped map inverts that
|
|
; decision, which is what the EOR tests.
|
|
; ----------------------------------------------------------------------
|
|
lda playerSide ; 7AD9 our side
|
|
eor mapFlipFlag ; 7ADC combined with the requested flip
|
|
beq checkMirrorUnits ; 7ADF side 0 on an unflipped map: leave the map as generated
|
|
jsr rotateMapAndMirrorCodes ; 7AE1 otherwise rotate it 180 degrees and mirror the direction-dependent terrain codes
|
|
checkMirrorUnits:
|
|
lda playerSide ; 7AE4 our side
|
|
beq initUnitsAndRunOptions ; 7AE7 side 0 keeps the template coordinates
|
|
jsr mirrorAllUnitCoordinates; 7AE9 side 1 mirrors all six unit coordinate arrays to match the rotated map (also trainerAiE000:smoothThreatRing, trainerSetupEC00:usePlanFour)
|
|
initUnitsAndRunOptions:
|
|
jsr setViewOriginToHome ; 7AEC view origin to (17,28), i.e. the middle of our own end
|
|
jsr initAllUnits ; 7AEF reset all 100 unit records from their start positions and put them on the map
|
|
lda #$FF ; 7AF2 $FF = clock not running
|
|
sta gameClock ; 7AF4 clock stays unset until the battle starts
|
|
jsr runGameStartOptions ; 7AF7 STANDARD/CUSTOM scenario banner and the custom option editors
|
|
lda #$00 ; 7AFA 0 for both variables below
|
|
sta textColour ; 7AFC back to text colour 0
|
|
sta setupToggleFlag ; 7AFE '*' toggle off
|
|
jsr clearInfoPanel ; 7B01 clear the info panel
|
|
jsr sendPhaseSyncAndWait ; 7B04 command $9E: wait until the opponent has finished his options too
|
|
lda #$00 ; 7B07 0
|
|
sta comcenBattleStatus ; 7B09 no comcen alert pending
|
|
jsr drawOverviewMap ; 7B0C draw the battlefield overview
|
|
jsr resetMessageQueue ; 7B0F empty the message queue
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Solo trainer only: the $EC00-$EFFF tail is swapped for the trainer's opening-play chooser (track 29
|
|
; sectors 0-3), run once, and then the normal comm tail is loaded back over it.
|
|
; ----------------------------------------------------------------------
|
|
lda isSoloTrainer ; 7B12 solo trainer?
|
|
bpl checkSetupPhaseNeeded ; 7B15 no - a modem game, nothing to load
|
|
inc diskLoadDepth ; 7B17 mark that a disk load is in progress (nested loads are counted)
|
|
jsr setGameClockForType ; 7B1A the trainer setup wants the game length already set
|
|
jsr showWorkingMessage ; 7B1D 'WORKING...'
|
|
ldx #$00 ; 7B20 destination low byte...
|
|
ldy #$EC ; 7B22 ...and high byte: $EC00
|
|
jsr setLoadDest ; 7B24 tell the fast loader where the sectors go
|
|
ldy #$1D ; 7B27 track 29
|
|
ldx #$00 ; 7B29 first sector 0
|
|
lda #$04 ; 7B2B four sectors = $EC00-$EFFF
|
|
jsr loadSectors ; 7B2D load the solo-trainer setup overlay
|
|
jsr initTrainerAi ; 7B30 let it pick this game's opening play
|
|
jsr loadCommTailBuild2 ; 7B33 restore the comm-module tail at $EC00 from track 34 sectors 12-15
|
|
dec diskLoadDepth ; 7B36 disk load finished
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Unit deployment phase. SCRIMAGE (type 0) and THE BOMB (type 2) start from the fixed template and
|
|
; skip it; every other game type lets both players drag their units around the map first. Note that
|
|
; this test looks at the game type directly and not at setupPhaseMode $92C6, which
|
|
; applyGameTypeDefaults derived from the same game type.
|
|
; ----------------------------------------------------------------------
|
|
checkSetupPhaseNeeded:
|
|
lda gameTypeOptions ; 7B39 the game options byte
|
|
and #$07 ; 7B3C bits 0-2 = the game type
|
|
beq waitForPacketExchange ; 7B3E type 0 SCRIMAGE: no deployment phase
|
|
cmp #$02 ; 7B40 is the game type 2, THE BOMB?
|
|
beq waitForPacketExchange ; 7B42 type 2 THE BOMB: none either
|
|
runUnitSetupPhase:
|
|
lda #$3F ; 7B44 gamePhase $3F = unit deployment
|
|
sta gamePhase ; 7B46 store the deployment phase
|
|
jsr shuffleInitialUnitOrder ; 7B48 shuffle the unit processing order (both machines get the same order from the map seed)
|
|
jsr setDestroyModeMessage ; 7B4B point the '*' toggle at 'DESTROY OFF' and the 'SETUP UNITS.' prompt
|
|
jsr runMapMainLoop ; 7B4E run the battlefield screen until the player presses F1
|
|
lda #$00 ; 7B51 0 = all sprites off
|
|
sta spriteEnableShadow ; 7B53 turn the cursor sprites off
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Do not start the battle until every order the player queued during deployment has actually been sent
|
|
; to the opponent.
|
|
; ----------------------------------------------------------------------
|
|
drainOwnCommandQueue:
|
|
lda pendingOwnCmdCount ; 7B56 commands of ours still waiting to be sent
|
|
checkQueueEmpty:
|
|
beq syncAfterSetup ; 7B59 queue empty - synchronise and go
|
|
jsr updateGameFrame ; 7B5B run one more frame, which services the link
|
|
jmp drainOwnCommandQueue ; 7B5E look again
|
|
|
|
syncAfterSetup:
|
|
jsr sendPhaseSyncAndWait ; 7B61 command $9E: both machines have finished deploying
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Final start-up: wait for one complete packet exchange, fix up the derived settings, snapshot the
|
|
; starting position into the film and put everything on the map.
|
|
; ----------------------------------------------------------------------
|
|
waitForPacketExchange:
|
|
bit exchangeFlags ; 7B64 comm exchange flag byte $E01D (also trainerAiE000:commInterfaceBlock)
|
|
bvc waitForPacketExchange ; 7B67 spin until bit 6 says a full packet exchange has completed
|
|
jsr setGameClockForType ; 7B69 gameClock = the number of rounds for this game type
|
|
ldx #$01 ; 7B6C index 1 = the opponent, then 0 = us
|
|
mapComcenSpeedLoop:
|
|
ldy comcenSpeedClass,x ; 7B6E COMCEN SPEED setting 0-3 for that side
|
|
lda comcenSpeedClassMap,y ; 7B71 translate it into the engine speed class 4, 0, 1 or 5
|
|
sta comcenSpeedClass,x ; 7B74 store it back over the setting
|
|
dex ; 7B77 then side 0, our own
|
|
bpl mapComcenSpeedLoop ; 7B78 loop over both sides
|
|
jsr clearUnitsAndRecyclersFromMap; 7B7A take every unit and both recyclers off the map again
|
|
jsr suspendCommForDiskAccess; 7B7D pause the comm module: the snapshot banks RAM in under the I/O area
|
|
jsr writeFilmSetupSnapshot ; 7B80 copy the settings and the unit arrays into the film header at $D034-$D367
|
|
jsr placeAllUnitsOnMap ; 7B83 put all 100 units back on the map
|
|
setBattleClock:
|
|
jsr setGameClockForType ; 7B86 gameClock = the number of rounds, also returned in A
|
|
sta gameClock ; 7B89 redundant: setGameClockForType has already stored the value in gameClock
|
|
jsr initFilmStream ; 7B8C film command stream starts at $D368
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Common tail, also the point the film replay path jumps back into ($7D6D). Both recyclers are now
|
|
; drawn (the replay path drew the opponent's just before jumping here), the unit order is shuffled one
|
|
; last time with gamePhase temporarily 0 so the battle-mode path of the round-order rebuild runs, and
|
|
; the last-quarter warning round is computed.
|
|
; ----------------------------------------------------------------------
|
|
beginBattle:
|
|
ldy playerSide ; 7B8F our own side
|
|
jsr drawRecyclerOnMap ; 7B92 draw our recycler on the map
|
|
lda #$00 ; 7B95 0
|
|
sta gamePhase ; 7B97 gamePhase 0 = battle, so shuffleInitialUnitOrder rebuilds the round order the battle way
|
|
jsr shuffleInitialUnitOrder ; 7B99 reshuffle from the map seed, identically on both machines
|
|
dec gamePhase ; 7B9C back to $FF until the battle loop clears it
|
|
jsr setGameClockForType ; 7B9E A = the game length in rounds
|
|
lsr a ; 7BA1 halve...
|
|
sub_7BA2:
|
|
lsr a ; 7BA2 divide by four
|
|
sta lastQuarterRound ; 7BA3 the last quarter of the game starts at this round ('LAST QTR' warning)
|
|
ldx #$20 ; 7BA6 message slot $20
|
|
lda #$ED ; 7BA8 string low byte...
|
|
ldy #$87 ; 7BAA ...and high byte: 'DRONE ALERT!' at $87ED in this overlay
|
|
jsr setMessageSlot ; 7BAC install it as message $20
|
|
jmp measureRangeToTarget ; 7BAF hand over to the comm-module tail, which finishes the recyclers and starts the battle (also modemDriverE000:finaliseRecyclerSetup)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; shuffleInitialUnitOrder - builds the order in which the 100 units are processed each round. Re-seeds
|
|
; the scenario RNG from the shared 24-bit map seed, resets unitBaseOrder ($9164) and unitProcessOrder
|
|
; ($90FD) to the identity permutation and then performs 51 random swaps of unitBaseOrder. Because the
|
|
; RNG is seeded from the map seed both machines shuffle identically without exchanging a single byte.
|
|
; Ends by jumping into the round-order rebuild at L_4657 (the tail of finishRound), whose RTS returns
|
|
; to our caller.
|
|
; In: mapSeed $0B95-$0B97; scenario RNG state zp_5A-zp_5C
|
|
; Out: $9164 shuffled, $90FD reset to 0..99, RNG advanced, zp_18 used as the swap counter;
|
|
; continues in L_4657 (recycle dead units, rebuild the group tables, age visibility)
|
|
; Called from: $7B48 and $7B99, the two game-start paths of this overlay
|
|
; ----------------------------------------------------------------------
|
|
shuffleInitialUnitOrder:
|
|
jsr loadMapSeedIntoScenarioRng; 7BB2 copy the 24-bit map seed $0B95-$0B97 into the scenario RNG state zp_5A-zp_5C
|
|
jsr initUnitOrderLists ; 7BB5 start from the identity permutation 0..99 in both order lists
|
|
lda #$32 ; 7BB8 $32 = 50, so the loop below runs 51 times (counter $32..$00)
|
|
sta scratch18 ; 7BBA zp_18 = number of swaps still to do
|
|
shuffleSwapLoop:
|
|
jsr shuffleUnitOrder ; 7BBC one swap: exchange two randomly chosen entries of unitBaseOrder ($47ED)
|
|
dec scratch18 ; 7BBF count this swap off
|
|
bpl shuffleSwapLoop ; 7BC1 loop until the counter goes negative - 51 swaps in all
|
|
jmp L_4657 ; 7BC3 continue in the round-order rebuild at the end of finishRound; its RTS returns to our caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; patchOwnSideBranches - burns the local player's side into the engine's self-modifying 'is this unit
|
|
; mine?' tests. Each of those tests compares a unit index with $32 (50) and is followed by a branch
|
|
; whose opcode byte is a variable: BCC ($90) means 'own units are 0-49' (side 0), BCS ($B0) means 'own
|
|
; units are 50-99' (side 1). Seven sites get that opcode and four sites, phrased the other way round
|
|
; ('skip if NOT mine'), get the inverted one. Finally caches this side's comcen unit index.
|
|
; In: playerSide $0B9F, perSideValueTable $0C8A ($31/$63 = unit 49/99)
|
|
; Out: branch opcodes at $63E2 $2840 $3F9E $36D5 $379C $4783 $5BF7 and the inverted opcodes at
|
|
; $206B $2084 $3B2C $3BD0; comcenUnit $9236
|
|
; Called from: $798E (start of a normal game) and $7D20 (start of film playback)
|
|
; ----------------------------------------------------------------------
|
|
patchOwnSideBranches:
|
|
lda #$90 ; 7BC6 $90 = BCC: for side 0 'own unit' means index < 50
|
|
ldy playerSide ; 7BC8 which side is the local player on?
|
|
beq L_7BCF ; 7BCB side 0 keeps the BCC
|
|
lda #$B0 ; 7BCD $B0 = BCS: for side 1 'own unit' means index >= 50
|
|
L_7BCF:
|
|
sta groupEditorOwnSideBranch; 7BCF runGroupEditor $63E0 'cpy #$32': marks the picked unit with bit 7 when it is not ours
|
|
sta ownSideBranchOpcode ; 7BD2 renderMapTile $283E 'cpy #$32': the other side's units are drawn in the enemy colour
|
|
sta initialFacingSideBranch ; 7BD5 initialFacingForUnit $3F9C 'cpx #$32': own units start facing up (0), enemies down (1)
|
|
sta D_36D5 ; 7BD8 scanVisibleEnemies $36D3 'cpy #$32': only units of the opposite side enter the spotted list
|
|
sta leftoverScanSideBranch ; 7BDB $379C is two leftover bytes ($90 $FA) in front of collectAllUnitsInRadiusDead - a dead branch, this store changes nothing
|
|
sta zoneRowSideBranch ; 7BDE moveUnitToCell $4781 'cpx #$32': the column is mirrored before scoring an end zone for the other side
|
|
sta D_5BF7 ; 7BE1 buildOverviewQuadrant $5BF5: inverts the mini-map pattern for the other side (that test uses the map-cell form, 'cpy #$B2' = $80|50)
|
|
eor #$20 ; 7BE4 $90 EOR $20 = $B0 and back - the four sites below test 'skip if NOT mine'
|
|
sta D_206B ; 7BE6 runMapMainLoop $2069 'cmp #$32': the info panel only opens for our own units
|
|
sta D_2084 ; 7BE9 runMapMainLoop $2082 'cmp #$32': only our own units can be picked with the joystick
|
|
sta D_3B2C ; 7BEC applyHitToUnit $3B2A 'cpy #$32': the spy/comcen hit reactions are for our units only
|
|
sta D_3BD0 ; 7BEF applyHitToUnit $3BCE 'cpy #$32': decides whether endGame is entered with X=0 (we lost) or X=1
|
|
lda comcenUnitIndexBySide,y ; 7BF2 $0C8A: comcen index of side Y - 49 for side 0, 99 for side 1
|
|
sta comcenUnit ; 7BF5 cache it in comcenUnit $9236 for the rest of the game
|
|
rts ; 7BF8 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendPhaseSyncAndWait - the 'I am ready' handshake that separates the setup phases. Queues the
|
|
; 1-byte command $9E and then keeps running frames until both ready bits of gamePhase zp_B1 are clear:
|
|
; the $9E handler ($520C) clears the sending side's bit on both machines (bit 0 = us, bit 1 = the
|
|
; opponent). While waiting it runs a full game frame during the battle, or only services the link
|
|
; when gamePhase says we are in a menu/setup phase.
|
|
; In: gamePhase zp_B1 (bits 0/1 = side not ready, bits 6/7 = not in the battle), the link
|
|
; Out: zp_B1 low two bits cleared by the command handler, persistentMessageId $92A1 = 0
|
|
; Called from: $79EC, $7B04 and $7B61 - the three setup-phase boundaries
|
|
; ----------------------------------------------------------------------
|
|
sendPhaseSyncAndWait:
|
|
lda #$9E ; 7BF9 $9E = 'setup phase ready' (during the battle the same command means 'comcen repaired')
|
|
jsr queueCmdBytePending ; 7BFB queue it for the opponent and count it as one of our own pending commands
|
|
lda gamePhase ; 7BFE pass the ready/pause bits to the message code in A
|
|
L_7C00:
|
|
jsr showWaitingForOpponent ; 7C00 on a modem game with bit 0 still set this shows message $0B 'WAITING FOR OPPONENT'
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait loop: re-read the ready bits every frame until both sides have executed the $9E.
|
|
; ----------------------------------------------------------------------
|
|
phaseSyncWaitLoop:
|
|
lda gamePhase ; 7C03 current ready/pause bits
|
|
and #$C0 ; 7C05 bits 6-7 set = menu or setup phase, the battle is not running
|
|
beq L_7C0F ; 7C07 battle phase: run a whole game frame
|
|
L_7C09:
|
|
jsr waitForExchangeIdle ; 7C09 setup phase: just service the link until the packet exchange is idle
|
|
jmp L_7C12 ; 7C0C and re-test
|
|
|
|
L_7C0F:
|
|
jsr updateGameFrame ; 7C0F battle phase: one complete game update (input, movement, drawing, link)
|
|
L_7C12:
|
|
lda gamePhase ; 7C12 read the ready bits again
|
|
and #$03 ; 7C14 bit 0 = our $9E not executed yet, bit 1 = the opponent's not seen yet
|
|
bne phaseSyncWaitLoop ; 7C16 keep waiting while either side is still not ready
|
|
sta persistentMessageId ; 7C18 A is 0 here: drop the 'WAITING FOR OPPONENT' status message
|
|
rts ; 7C1B both sides are ready
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForMapSeedCommand - waits until the map seed sent by the other machine has arrived. gamePhase
|
|
; zp_B1 bit 7 is set by the caller and cleared by the handler of the map-seed commands
|
|
; $8B / $A6 ($51EA), so this loop simply services the link until that command has been executed.
|
|
; In: gamePhase zp_B1 bit 7, the link
|
|
; Out: loops until zp_B1 bit 7 is clear; persistentMessageId $92A1 = 0
|
|
; Called from: $79C2 (the side that waits for its opponent to choose the map)
|
|
; ----------------------------------------------------------------------
|
|
waitForMapSeedCommand:
|
|
jsr waitForExchangeIdle ; 7C1C service the link until the current packet exchange goes idle
|
|
lda gamePhase ; 7C1F bit 7 is only cleared by the $8B/$A6 map-seed command handler at $51EA
|
|
bmi waitForMapSeedCommand ; 7C21 keep polling while the seed has not arrived
|
|
lda #$00 ; 7C23 the seed is in: clear the status message
|
|
sta persistentMessageId ; 7C25 persistentMessageId $92A1 = 0
|
|
rts ; 7C28 both machines now hold the same map seed
|
|
|
|
|
|
; setupPhaseModeTable - one byte per game-type class (the folded column 0-4 computed by
|
|
; applyGameTypeDefaults), copied into setupPhaseMode $92C6: 0 = no setup phase at all (SCRIMAGE, QB
|
|
; SNEAK), 1 = both sides deploy their units and terrain (THE BOMB, FACE-OFF, SLUGGERS, FULL WAR), $80
|
|
; = only side 0 deploys (DEFENDER).
|
|
setupPhaseModeTable:
|
|
.byte $00,$00,$01,$01,$80 ; 7C29 ..... classes 0-4: 0, 0, 1, 1, $80
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyGameTypeDefaults - loads the per-side game parameters of the chosen game type from the two 5x5
|
|
; default tables at $892C (side 0) and $8945 (side 1) in the $8800 overlay. The game type
|
|
; $0BA3 bits 0-2 is first folded into a table column: 0 SCRIMAGE, 1 QB SNEAK, 2 THE BOMB /
|
|
; FACE-OFF / SLUGGERS, 3 FULL WAR, 4 DEFENDER; the same column selects the setup-phase mode. Table
|
|
; rows 0-1 (terrain points, recycler mode) are stored by absolute side, rows 2-4 (comcen speed class,
|
|
; drones, missiles) by relative index (0 = us, 1 = the opponent). Afterwards the two special rosters
|
|
; are applied: QB SNEAK takes units 0-6 out of play, THE BOMB takes out units 0-98, re-creates unit 49
|
|
; as a COMCEN and gives each side an extra drone.
|
|
; In: gameTypeOptions $0BA3 bits 0-2, playerSide $0B9F, tables $892C/$8945, setupPhaseModeTable
|
|
; Out: setupPhaseMode $92C6; terrainPointsBySide $92AC and recyclerMode $92A6 per absolute side;
|
|
; comcenSpeedClass $92B4, dronesLeft $92AE, missilesLeft $92B0 per relative index; unit slots
|
|
; emptied in $F76C/$FAF0; zp_18, zp_48/zp_49; self-modifies $7C85+1 and $7CBF+1
|
|
; Called from: $79E9, once, after the game type has been agreed
|
|
; ----------------------------------------------------------------------
|
|
applyGameTypeDefaults:
|
|
lda gameTypeOptions ; 7C2E the agreed game type and option bits
|
|
and #$07 ; 7C31 bits 0-2 = type 0 SCRIMAGE .. 6 DEFENDER
|
|
tay ; 7C33 keep the type in Y while it is folded into a table column
|
|
sty L_7C85+1 ; 7C34 self-modifying: the raw game type becomes the operand of the LDA #$FF at $7C85
|
|
cpy #$02 ; 7C37 types 0 and 1 have a column of their own
|
|
bcc L_7C44 ; 7C39 SCRIMAGE and QB SNEAK: column = type
|
|
bne L_7C3E ; 7C3B types 3-6 go straight to the common DEY
|
|
iny ; 7C3D type 2 (THE BOMB) is bumped to 3 so the DEY puts it on column 2
|
|
L_7C3E:
|
|
dey ; 7C3E types 2-6: one column less
|
|
cpy #$03 ; 7C3F columns 0-2 are final
|
|
bcc L_7C44 ; 7C41 THE BOMB and FACE-OFF share column 2
|
|
dey ; 7C43 SLUGGERS, FULL WAR and DEFENDER lose one more: columns 2, 3 and 4
|
|
L_7C44:
|
|
sty readDefaultAndAdvanceRow+1; 7C44 self-modifying: the column index becomes the LDY operand of readDefaultAndAdvanceRow ($7CC0)
|
|
lda setupPhaseModeTable,y ; 7C47 setup-phase mode for this class of game
|
|
sta setupPhaseMode ; 7C4A $92C6: 0 = no setup phase, 1 = both sides deploy, $80 = only side 0 deploys
|
|
ldx playerSide ; 7C4D first pass: X = the local player's absolute side
|
|
lda #$00 ; 7C50 relative index 0 = us
|
|
sta scratch18 ; 7C52 zp_18 = 0 (us) then 1 (the opponent)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Two passes over the 5-row default table, ours first. Rows 0-1 are stored under the absolute side
|
|
; number (they describe a side of the battlefield), rows 2-4 under the relative index (they describe a
|
|
; player: 0 = this machine, 1 = the other one).
|
|
; ----------------------------------------------------------------------
|
|
sideDefaultsLoop:
|
|
jsr selectSideDefaultsTable ; 7C54 point zp_48/zp_49 at the 5x5 default table of the side in X
|
|
jsr readDefaultAndAdvanceRow; 7C57 row 0 of the selected column = terrain points
|
|
sta terrainPointsBySide,x ; 7C5A $92AC+side: terrain points this side may place in the setup phase
|
|
jsr readDefaultAndAdvanceRow; 7C5D row 1 = recycler mode
|
|
sta recyclerMode,x ; 7C60 $92A6+side: whether and how that side gets a recycler
|
|
ldx scratch18 ; 7C63 rows 2-4 are indexed by the relative side instead
|
|
jsr readDefaultAndAdvanceRow; 7C65 row 2 = comcen speed class
|
|
sta comcenSpeedClass,x ; 7C68 $92B4+rel (later mapped through comcenSpeedClassMap at $7B6E)
|
|
jsr readDefaultAndAdvanceRow; 7C6B row 3 = number of drones
|
|
D_7C6E:
|
|
sta dronesLeft,x ; 7C6E $92AE+rel; the D_7C6E label only exists because the comcen-screens overlay has code here
|
|
jsr readDefaultAndAdvanceRow; 7C71 row 4 = number of missiles
|
|
sta missilesLeft,x ; 7C74 $92B0+rel
|
|
lda playerSide ; 7C77 second pass: the opponent's absolute side
|
|
eor #$01 ; 7C7A side EOR 1
|
|
tax ; 7C7C X = opponent side for the next pass
|
|
inc scratch18 ; 7C7D relative index 1 = the opponent
|
|
lda scratch18 ; 7C7F two passes only
|
|
cmp #$02 ; 7C81 0 = us, 1 = them
|
|
bcc sideDefaultsLoop ; 7C83 go round again for the opponent
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Roster adjustments for the two game types that are not played with all 50 units a side.
|
|
; ----------------------------------------------------------------------
|
|
L_7C85:
|
|
lda #$FF ; 7C85 operand patched at $7C34: A = the raw game type 0-6
|
|
cmp #$01 ; 7C87 type 1 = QB SNEAK
|
|
beq removeQbSneakUnits ; 7C89 QB SNEAK is played with a handful of units only
|
|
cmp #$02 ; 7C8B type 2 = THE BOMB
|
|
bne L_7CAD ; 7C8D every other type keeps the full roster
|
|
ldy #$62 ; 7C8F THE BOMB: take units 0-98 out of play ($62 = 98)
|
|
jsr removeUnitsUpToY ; 7C91 empty those unit slots
|
|
lda #$04 ; 7C94 type 4 = COMCEN
|
|
sta side0ComcenType ; 7C96 $F79D = unitTypeTable+49: put side 0's comcen back into play
|
|
inc dronesLeft ; 7C99 one extra drone for us ...
|
|
inc opponentDronesLeft ; 7C9C ... and one for the opponent: THE BOMB is fought with comcens and drones alone
|
|
rts ; 7C9F done
|
|
|
|
removeQbSneakUnits:
|
|
ldy #$06 ; 7CA0 QB SNEAK: take units 0-6 out of play
|
|
|
|
; ----------------------------------------------------------------------
|
|
; removeUnitsUpToY - marks unit slots Y down to 0 as 'not in this game': unitTypeTable ($F76C) = $FF
|
|
; (bit 7 = dead / no such unit) and unitTerrainUnderTable ($FAF0) = $FF ('not standing on the map').
|
|
; Entered with Y = 6 for QB SNEAK and Y = $62 for THE BOMB. The RTS at $7CAD is also the shared exit
|
|
; of applyGameTypeDefaults.
|
|
; In: Y = highest unit index to remove
|
|
; Out: $F76C[0..Y] = $FAF0[0..Y] = $FF, Y = $FF
|
|
; Called from: $7C91 (applyGameTypeDefaults) and by fall-through from $7CA0
|
|
; ----------------------------------------------------------------------
|
|
removeUnitsUpToY:
|
|
lda #$FF ; 7CA2 $FF = empty unit slot
|
|
sta unitTypeTable,y ; 7CA4 unitTypeTable[Y] = $FF: bit 7 marks it dead, the whole byte marks it absent
|
|
sta unitTerrainUnderTable,y ; 7CA7 unitTerrainUnderTable[Y] = $FF: this unit occupies no map cell
|
|
dey ; 7CAA next lower unit
|
|
bpl removeUnitsUpToY ; 7CAB down to unit 0
|
|
L_7CAD:
|
|
rts ; 7CAD shared exit of applyGameTypeDefaults
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; selectSideDefaultsTable - points zp_48/zp_49 at the game-type default table of side X: $892C for
|
|
; side 0 and $8945 for side 1. Both live in the $8800 modem-strings overlay and hold 5 rows of 5
|
|
; bytes; the row is the parameter and the column the game-type class.
|
|
; In: X = side 0/1
|
|
; Out: zp_48/zp_49 = address of row 0 of that table
|
|
; Called from: $7C54 (applyGameTypeDefaults)
|
|
; ----------------------------------------------------------------------
|
|
selectSideDefaultsTable:
|
|
lda #$2C ; 7CAE low byte of $892C = side 0's defaults
|
|
ldy #$89 ; 7CB0 high byte (the $8800 strings overlay)
|
|
cpx #$00 ; 7CB2 side 0?
|
|
beq L_7CBA ; 7CB4 yes: keep $892C
|
|
lda #$45 ; 7CB6 low byte of $8945 = side 1's defaults, 25 bytes further on
|
|
ldy #$89 ; 7CB8 high byte
|
|
L_7CBA:
|
|
sta mapCellPtr ; 7CBA zp_48 = pointer to the current table row
|
|
sty mapCellPtrHi ; 7CBC zp_49 = high byte
|
|
rts ; 7CBE ready for readDefaultAndAdvanceRow
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readDefaultAndAdvanceRow - reads the byte of the current 5-byte table row that belongs to the chosen
|
|
; game-type column and then advances zp_48/zp_49 to the next row. The LDY operand at
|
|
; $7CC0 is the column index, written by applyGameTypeDefaults at $7C44.
|
|
; In: zp_48/zp_49 = current row, column index patched into $7CC0
|
|
; Out: A = the table value, zp_48/zp_49 += 5
|
|
; Called from: $7C57, $7C5D, $7C65, $7C6B, $7C71 (all inside applyGameTypeDefaults)
|
|
; ----------------------------------------------------------------------
|
|
readDefaultAndAdvanceRow:
|
|
ldy #$FF ; 7CBF operand patched at $7C44: Y = game-type column 0-4
|
|
lda (mapCellPtr),y ; 7CC1 the default for this parameter and this game type
|
|
pha ; 7CC3 hold it while the pointer moves on
|
|
lda mapCellPtr ; 7CC4 advance the row pointer ...
|
|
clc ; 7CC6 clear carry before the add
|
|
adc #$05 ; 7CC7 ... by 5 bytes = one table row
|
|
sta mapCellPtr ; 7CC9 store it back
|
|
bcc L_7CCF ; 7CCB no page crossing
|
|
inc mapCellPtrHi ; 7CCD carry into the high byte
|
|
L_7CCF:
|
|
pla ; 7CCF get the value back
|
|
rts ; 7CD0 A = the default value
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; startFilmPlayback - the game-start path taken when WATCH GAME FILM was chosen ($0B9B bit 7).
|
|
; Everything the film needs is in its 52-byte header in the RAM under I/O at $D000, written by
|
|
; $7A78-$7AD8 when the game was recorded; this routine reads it back with readFilmByte and
|
|
; rebuilds the game from it: link type, side, game type, both player names, map seed, map orientation
|
|
; and who plays first. The battlefield itself is not stored - it is regenerated from the seed and
|
|
; rotated if necessary. Then the recorded starting state is restored, the units are placed, the
|
|
; screen is drawn and the common game-start tail at L_7B8F is entered.
|
|
; In: film header at $D000 (zp_BC/zp_BD = $D000, left there by checkFilmInMemory)
|
|
; Out: $0BA8=0, $92FC=0, $92FE, $0B9F, $0BA3, $FFDE-$FFF1, $0B95-$0B97, $0B9D, $0BA1, $0BA9-$0BAE,
|
|
; $90FB=0, $92FA=$FF, map at $F000 regenerated, all units placed; jumps to L_7B8F
|
|
; Called from: $7921 (JMP, from the game-start dispatcher)
|
|
; ----------------------------------------------------------------------
|
|
startFilmPlayback:
|
|
lda #$00 ; 7CD1 no game-over reason yet
|
|
sta gameEndReason ; 7CD3 $0BA8 = 0: the game has not ended
|
|
sta revealAllUnits ; 7CD6 $92FC = 0: start the replay with the normal one-sided view
|
|
ldy #$04 ; 7CD9 header byte 4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Read the recorded game parameters out of the film header at $D000 (RAM under I/O).
|
|
; ----------------------------------------------------------------------
|
|
jsr readFilmByte ; 7CDB read one header byte through zp_BC/zp_BD with the ROMs and I/O banked out
|
|
sta soloTrainerAtGameStart ; 7CDE $92FE: was the recorded game played against the trainer or over a modem?
|
|
iny ; 7CE1 header byte 5
|
|
jsr readFilmByte ; 7CE2 read it
|
|
sta playerSide ; 7CE5 the side the film was recorded from - the replay is watched through that player's eyes
|
|
ldy #$0C ; 7CE8 header byte $0C
|
|
jsr readFilmByte ; 7CEA read it
|
|
sta gameTypeOptions ; 7CED $0BA3: game type and option bits of the recorded game
|
|
ldy #$11 ; 7CF0 header bytes $11-$24 = the two 10-byte player names
|
|
ldx #$00 ; 7CF2 X walks the name block at $FFDE
|
|
filmNameByteLoop:
|
|
jsr readFilmByte ; 7CF4 next name byte
|
|
sta ownPlayerName,x ; 7CF7 $FFDE = our name, $FFE8 = the opponent's
|
|
iny ; 7CFA next header offset
|
|
inx ; 7CFB next destination byte
|
|
cpx #$14 ; 7CFC $14 = 20 bytes = two 10-character names
|
|
bcc filmNameByteLoop ; 7CFE keep copying
|
|
ldy #$2B ; 7D00 header bytes $2B-$2D = the 24-bit map seed
|
|
ldx #$00 ; 7D02 X = seed byte 0-2
|
|
filmSeedByteLoop:
|
|
jsr readFilmByte ; 7D04 next seed byte
|
|
sta mapSeed,x ; 7D07 $0B95-$0B97: the number the map name is written from
|
|
iny ; 7D0A next header offset
|
|
inx ; 7D0B next seed byte
|
|
cpx #$03 ; 7D0C three bytes
|
|
bcc filmSeedByteLoop ; 7D0E keep copying
|
|
jsr readFilmByte ; 7D10 header byte $2E packs two flags
|
|
tax ; 7D13 keep the packed byte in X
|
|
and #$01 ; 7D14 bit 0 = map orientation
|
|
sta mapFlipFlag ; 7D16 $0B9D: the map was shown rotated 180 degrees
|
|
txa ; 7D19 the packed byte again
|
|
lsr a ; 7D1A shift the orientation bit out
|
|
and #$03 ; 7D1B bits 1-2 = who plays first
|
|
sta playsFirstFlag ; 7D1D $0BA1
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Rebuild the battlefield: the film stores only the seed, never the 1600 map cells.
|
|
; ----------------------------------------------------------------------
|
|
jsr patchOwnSideBranches ; 7D20 burn the film's side into the engine's self-modifying side tests
|
|
jsr generateBattlefieldMap ; 7D23 regenerate the identical 40x40 map from the seed
|
|
lda playerSide ; 7D26 the generator always builds the map for side 0 ...
|
|
eor mapFlipFlag ; 7D29 ... so compare the viewing side with the recorded orientation
|
|
beq L_7D31 ; 7D2C orientations agree: leave the map alone
|
|
jsr rotateMapAndMirrorCodes ; 7D2E turn the battlefield 180 degrees and mirror the direction-dependent terrain codes
|
|
L_7D31:
|
|
ldy #$06 ; 7D31 header bytes 6-$0B = the 6-byte map-id text
|
|
ldx #$00 ; 7D33 X walks $0BA9-$0BAE
|
|
filmMapIdByteLoop:
|
|
jsr readFilmByte ; 7D35 next character of the map name
|
|
sta mapNameText,x ; 7D38 restore it: generateBattlefieldMap has just overwritten $0BA9 with its own copy
|
|
iny ; 7D3B next header offset
|
|
inx ; 7D3C next character
|
|
cpx #$06 ; 7D3D six characters
|
|
bcc filmMapIdByteLoop ; 7D3F keep copying
|
|
jsr loadFilmStartSnapshot ; 7D41 restore the recorded starting state (unit arrays, scores, clock) from $D034 onwards (also trainerSetupEC00:checkGroupStartsAtOnce)
|
|
lda gameTypeOptions ; 7D44 the recorded option bits
|
|
and #$40 ; 7D47 bit 6 = CUSTOM rules
|
|
beq L_7D4E ; 7D49 standard rules: nothing to announce
|
|
jsr showCustomSummaryMessage; 7D4B put the 'CUSTOM: ...' summary on the status line
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Put the restored units on the map and draw the battlefield screen.
|
|
; ----------------------------------------------------------------------
|
|
L_7D4E:
|
|
jsr placeAllUnitsOnMap ; 7D4E write $80|index into the map cell of every living unit
|
|
jsr setViewOriginToHome ; 7D51 move the viewport over the player's own end of the field
|
|
lda #$00 ; 7D54 screen id 0 ...
|
|
sta currentScreen ; 7D56 ... = the battlefield screen
|
|
jsr clearScreenAndRedrawGameScreen; 7D59 clear the bitmap and redraw the frames, panels and console
|
|
jsr drawOverviewMap ; 7D5C draw the 20x20 overview map in the left panel
|
|
lda #$FF ; 7D5F bit 7 set ...
|
|
sta lastQuarterFlag ; 7D61 ... lets the status line keep the clock and score up to date ($C50D)
|
|
lda playerSide ; 7D64 our side
|
|
eor #$01 ; 7D67 EOR 1 = the opponent's side
|
|
tay ; 7D69 drawRecyclerOnMap takes the side in Y
|
|
jsr drawRecyclerOnMap ; 7D6A put the opponent's recycler glyph back on the map
|
|
jmp beginBattle ; 7D6D join the common game-start tail (the film's own recycler is drawn there)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initUnitOrderLists - fills the two 100-entry unit order lists unitBaseOrder ($9164) and
|
|
; unitProcessOrder ($90FD) with the identity permutation, 99 down to 0.
|
|
; In: -
|
|
; Out: $9164[i] = $90FD[i] = i for i = 0..99, Y = $FF
|
|
; Called from: $7BB5 (shuffleInitialUnitOrder), before the base list is shuffled
|
|
; ----------------------------------------------------------------------
|
|
initUnitOrderLists:
|
|
ldy #$63 ; 7D70 $63 = 99, the last unit index
|
|
fillOrderListLoop:
|
|
tya ; 7D72 entry Y gets the value Y
|
|
sta unitBaseOrder,y ; 7D73 $9164: the shuffled base order built by shuffleInitialUnitOrder
|
|
sta unitProcessOrder,y ; 7D76 $90FD: the working order the round loop walks
|
|
dey ; 7D79 next lower unit
|
|
bpl fillOrderListLoop ; 7D7A down to unit 0
|
|
rts ; 7D7C both lists are now 0..99
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; placeAllUnitsOnMap - runs placeUnitOnMap for every unit 99 down to 0, which resets the unit's
|
|
; energy, move points and flags, saves the terrain under it and writes $80|index into its map cell (a
|
|
; dead unit becomes an empty slot instead). The callee wants the index in zp_18 as well as in X.
|
|
; In: the unit column/row arrays, the map at $F000
|
|
; Out: every living unit is on the map, zp_18 = $FF
|
|
; Called from: $7B83 (game start) and $7D4E (film playback)
|
|
; ----------------------------------------------------------------------
|
|
placeAllUnitsOnMap:
|
|
lda #$63 ; 7D7D start at unit 99
|
|
sta scratch18 ; 7D7F placeUnitOnMap reads the index from zp_18 as well as from X
|
|
placeNextUnit:
|
|
ldx scratch18 ; 7D81 X = the unit to place
|
|
jsr placeUnitOnMap ; 7D83 reset the unit and write $80|index into its map cell, saving the terrain underneath
|
|
dec scratch18 ; 7D86 next lower unit
|
|
bpl placeNextUnit ; 7D88 down to unit 0
|
|
rts ; 7D8A all 100 slots handled
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setGameClockForType - looks the length of the game up in roundsByGameType ($78C3: 63, 63, 31, 127,
|
|
; 127, 254, 254 rounds for types 0-6) and starts the game clock with it.
|
|
; In: gameTypeOptions $0BA3 bits 0-2
|
|
; Out: gameClock $91CB = A = the number of rounds
|
|
; Called from: $7B1A, $7B69, $7B86 and $7B9E (which shifts A right twice to get the quarter length)
|
|
; ----------------------------------------------------------------------
|
|
setGameClockForType:
|
|
lda gameTypeOptions ; 7D8B the game type and option bits
|
|
and #$07 ; 7D8E bits 0-2 = type 0-6
|
|
tay ; 7D90 index the length table
|
|
lda roundsByGameType,y ; 7D91 $78C3: 63, 63, 31, 127, 127, 254, 254 rounds
|
|
sta gameClock ; 7D94 $91CB counts one down per round
|
|
rts ; 7D97 A still holds the length for the caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearUnitsAndRecyclersFromMap - takes every unit 99..0 off the map (removeUnitFromMap puts the
|
|
; terrain that was hidden under it back) and then blanks both recycler cells, leaving a map that holds
|
|
; terrain only. Falls into clearRecyclerCell for side 1.
|
|
; In: the unit arrays, recyclerCol/recyclerRow $92A8/$92AA
|
|
; Out: the map at $F000-$F63F contains no units and no recyclers
|
|
; Called from: $7B7A, just before the film snapshot is written and the units are re-placed
|
|
; ----------------------------------------------------------------------
|
|
clearUnitsAndRecyclersFromMap:
|
|
ldx #$63 ; 7D98 start at unit 99
|
|
removeNextUnit:
|
|
jsr removeUnitFromMap ; 7D9A restore the terrain under the unit and mark it as not on the map
|
|
dex ; 7D9D next lower unit
|
|
bpl removeNextUnit ; 7D9E down to unit 0
|
|
ldx #$00 ; 7DA0 side 0 ...
|
|
jsr clearRecyclerCell ; 7DA2 ... blank its recycler cell
|
|
ldx #$01 ; 7DA5 side 1: fall into the same code
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearRecyclerCell - blanks side X's recycler cell: if that side has a recycler (recyclerCol bit 7
|
|
; clear) the map byte at (recyclerCol, recyclerRow) is set to the empty terrain code $40.
|
|
; In: X = side 0/1, recyclerCol $92A8, recyclerRow $92AA
|
|
; Out: one map cell = $40, zp_48/zp_49 point at it
|
|
; Called from: $7DA2 and by fall-through from $7DA5
|
|
; ----------------------------------------------------------------------
|
|
clearRecyclerCell:
|
|
lda recyclerCol,x ; 7DA7 $92A8+side; bit 7 set means this side has no recycler placed
|
|
bmi L_7DB7 ; 7DAA no recycler: nothing to blank
|
|
ldy recyclerRow,x ; 7DAC Y = recycler row
|
|
tax ; 7DAF X = recycler column
|
|
jsr readMapCell ; 7DB0 zp_48/zp_49 = address of that map cell; Y comes back as 0
|
|
lda #$40 ; 7DB3 $40 = empty terrain
|
|
sta (mapCellPtr),y ; 7DB5 wipe the recycler out of the map
|
|
L_7DB7:
|
|
rts ; 7DB7 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForSpaceKey - 'PRESS SPACE WHEN READY' wait used by the modem setup prompts. Clears the
|
|
; keyboard and chat state and then loops servicing the link and stirring the game RNG until the space
|
|
; bar ($A0) turns up in lastTypedKey; afterwards it empties the message queue and locks the input out
|
|
; for half a second. Stirring the RNG on every pass is what makes the map seed depend on how long the
|
|
; player took to press space.
|
|
; In: lastTypedKey $9248 (written by the IRQ keyboard scan)
|
|
; Out: $9248 = $FF, chatState $929C = 0, incomingChatLength $9277 = 0, inputLockoutTimer $0B7D =
|
|
; 30 frames, message queue emptied, game RNG zp_57-zp_59 advanced
|
|
; Called from: showModemSetupPrompt ($0C9D, JMP), so this RTS returns to that routine's caller
|
|
; ----------------------------------------------------------------------
|
|
waitForSpaceKey:
|
|
lda #$FF ; 7DB8 $FF = no key pending
|
|
sta lastTypedKey ; 7DBA throw away whatever was typed before
|
|
lda #$00 ; 7DBD zero for the three variables below
|
|
sta chatState ; 7DBF leave chat entry mode
|
|
sta inputLockoutTimer ; 7DC2 no input lockout while we wait
|
|
sta incomingChatLength ; 7DC5 discard a half-received chat line
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Wait loop: keep the link alive and stir the random generator until space is pressed.
|
|
; ----------------------------------------------------------------------
|
|
waitSpaceLoop:
|
|
jsr serviceCommandExchange ; 7DC8 service the lock-step command exchange
|
|
jsr nextGameRandom ; 7DCB step the 24-bit game RNG - this is what randomises the map seed
|
|
ldy lastTypedKey ; 7DCE the last key the IRQ saw
|
|
cpy #$A0 ; 7DD1 $A0 = the space bar
|
|
bne waitSpaceLoop ; 7DD3 keep waiting
|
|
jsr resetMessageQueue ; 7DD5 space pressed: empty the message queue and refresh the status line
|
|
lda #$1E ; 7DD8 $1E = 30 frames
|
|
sta inputLockoutTimer ; 7DDA ignore input for half a second so the key is not seen twice
|
|
lda #$FF ; 7DDD $FF = no key pending
|
|
sta lastTypedKey ; 7DDF consume the space
|
|
rts ; 7DE2 returns into showModemSetupPrompt's caller
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleMainMenuChoice - acts on the item the player picked in the main menu. 0 COMPETE WITH MODEM
|
|
; OPPONENT and 1 PRACTICE WITH SOLO TRAINER set the opponent-module flag and start a game; 2 WATCH
|
|
; GAME FILM needs a valid film in $D000 and starts the game in playback mode; 3 SAVE GAME FILM (also
|
|
; needs a valid film) and 4 LOAD GAME FILM ask for the data disk, load its film directory and run the
|
|
; save or load slot menu, then ask for the game disk again.
|
|
; In: menuSelectedItem $91D5, isSoloTrainer $0BA5, the film buffer at $D000
|
|
; Out: $0BA5, $0B9B, defaultMainMenuItem $92FD = 2, filmDirectoryPage $7FA1 = 0, filmMenuMode
|
|
; $7FA2; items 0-2 leave through startGameFromSetup ($0AC8)
|
|
; Called from: runMainMenu ($1C78, JMP)
|
|
; ----------------------------------------------------------------------
|
|
handleMainMenuChoice:
|
|
ldy menuSelectedItem ; 7DE3 the item the selector returned
|
|
sty L_7E0E+1 ; 7DE6 self-modifying: the item number becomes the operand of the LDA #$FF at $7E0E
|
|
cpy #$03 ; 7DE9 items 0-2 start a game
|
|
bcc L_7E2B ; 7DEB handle those below
|
|
bne L_7DF4 ; 7DED item 4 = LOAD GAME FILM: there is nothing in memory to check
|
|
jsr checkFilmInMemory ; 7DEF item 3 = SAVE GAME FILM: refuse unless $D000 holds a valid film
|
|
bcs L_7E2A ; 7DF2 no film: 'NOT A GAME FILM!' has been shown, give up
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Save/load film: get the data disk in the drive and read its film directory.
|
|
; ----------------------------------------------------------------------
|
|
L_7DF4:
|
|
jsr promptInsertDataDisk ; 7DF4 show 'INSERT DATA DISK AND PRESS SPACE.'
|
|
jsr suspendCommForDiskAccess; 7DF7 pause the modem/trainer module - the fast loader runs with interrupts off
|
|
jsr checkDiskAndLoadFilmDirectory; 7DFA identify the disk and read the 30-entry film directory to $0300
|
|
php ; 7DFD keep the C flag across the resume
|
|
jsr resumeCommAfterDiskAccess; 7DFE restart the comm module and clear the status line
|
|
plp ; 7E01 get the result back
|
|
bcs L_7E2A ; 7E02 no usable disk in the drive: the message has already been shown
|
|
lda #$02 ; 7E04 item 2 = WATCH GAME FILM ...
|
|
sta defaultMainMenuItem ; 7E06 ... becomes the pre-selected main-menu item next time round
|
|
lda #$00 ; 7E09 start on ...
|
|
sta filmDirectoryPage ; 7E0B ... the first directory page
|
|
L_7E0E:
|
|
lda #$FF ; 7E0E operand patched at $7DE6: A = the menu item (3 or 4)
|
|
sec ; 7E10 prepare the subtraction
|
|
sbc #$03 ; 7E11 item 3 -> 0, item 4 -> 1
|
|
sta filmMenuMode ; 7E13 $7FA2: 0 = save film, 1 = load film
|
|
jsr drawFilmDirectoryPage ; 7E16 draw the ten slots of the current page
|
|
lda filmMenuMode ; 7E19 which of the two menus?
|
|
bne L_7E24 ; 7E1C 1 = load
|
|
jsr runSaveFilmMenu ; 7E1E SAVE: pick a slot, type a name and write the film to disk
|
|
jmp promptInsertGameDisk ; 7E21 then ask for the game disk again
|
|
|
|
L_7E24:
|
|
jsr runLoadFilmMenu ; 7E24 LOAD: pick a slot and read the film into $D000
|
|
jmp promptInsertGameDisk ; 7E27 then ask for the game disk again
|
|
|
|
L_7E2A:
|
|
rts ; 7E2A nothing to do
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Items 0-2: choose the opponent module / playback mode and start a game.
|
|
; ----------------------------------------------------------------------
|
|
L_7E2B:
|
|
cpy #$02 ; 7E2B item 2 = WATCH GAME FILM
|
|
bne L_7E42 ; 7E2D items 0 and 1 below
|
|
jsr checkFilmInMemory ; 7E2F a film can only be watched if one is loaded
|
|
bcs L_7E2A ; 7E32 invalid film: back to the menu
|
|
ldx isSoloTrainer ; 7E34 keep the current opponent module - a film can be watched either way
|
|
lda #$82 ; 7E37 $0B9B = $82: bit 7 = playing a film back, bits 0-1 = 2 = show both sides
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setModeFlagsAndStartGame - shared tail of handleMainMenuChoice: X becomes isSoloTrainer $0BA5 ($FF =
|
|
; solo trainer, 0 = modem opponent) and A becomes filmPlaybackMode $0B9B, then the game is started.
|
|
; In: X = new $0BA5, A = new $0B9B
|
|
; Out: $0BA5, $0B9B; jumps to startGameFromSetup ($0AC8)
|
|
; Called from: $7E47 and $7E4D (branches), and by fall-through from $7E37
|
|
; ----------------------------------------------------------------------
|
|
setModeFlagsAndStartGame:
|
|
stx isSoloTrainer ; 7E39 $0BA5: $FF = play the solo trainer, 0 = play a modem opponent
|
|
sta filmPlaybackMode ; 7E3C $0B9B: bit 7 = watch a game film
|
|
jmp startGameFromSetup ; 7E3F leave the menu and start the game
|
|
|
|
L_7E42:
|
|
lda #$00 ; 7E42 item 0 COMPETE WITH MODEM OPPONENT: no film ...
|
|
tax ; 7E44 ... and X = 0 = the modem driver
|
|
cpy #$01 ; 7E45 item 1 = PRACTICE WITH SOLO TRAINER?
|
|
bne setModeFlagsAndStartGame; 7E47 no: start the modem game
|
|
lda #$00 ; 7E49 solo trainer: no film either
|
|
ldx #$FF ; 7E4B $FF selects the trainer module at $E000
|
|
bne setModeFlagsAndStartGame; 7E4D always taken (X is $FF)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkDiskAndLoadFilmDirectory - works out which disk is in the drive and loads its film directory.
|
|
; The game disk ('OZ') is tried first and silently, because films may also be kept on it; otherwise a
|
|
; data disk ('EA') is required, and a data disk must additionally have every block allocated in its
|
|
; BAM ($0203-$028F all zero) so that CBM DOS can never overwrite the raw film tracks. diskIdIndex
|
|
; $92F0 is left at 0 for the game disk and 2 for a data disk, which later decides whether the
|
|
; directory has one page or three. Falls into loadFilmDirectory.
|
|
; In: the disk in the drive
|
|
; Out: C=0 and the directory at $0300-$04FF, or C=1 with a message shown; $92F0, BAM at $0200
|
|
; Called from: $7DFA (handleMainMenuChoice)
|
|
; ----------------------------------------------------------------------
|
|
checkDiskAndLoadFilmDirectory:
|
|
ldx #$00 ; 7E4F X = 0: look for the 'OZ' game disk
|
|
lda #$80 ; 7E51 bit 7 = fail silently, this is only a first guess
|
|
jsr checkDiskId ; 7E53 read the BAM and compare the disk id
|
|
bcc L_7E6D ; 7E56 the game disk can hold films too: use it
|
|
ldx #$02 ; 7E58 X = 2: look for the 'EA' data disk
|
|
lda #$00 ; 7E5A this time complain if it is not there
|
|
jsr checkDiskId ; 7E5C read the BAM and compare the disk id
|
|
bcs L_7E70 ; 7E5F neither disk: 'NOT A DATA DISK!' has been shown, return C=1
|
|
ldy #$03 ; 7E61 the BAM's per-track allocation entries start at $0203
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A real data disk has been fully allocated by the format utility, so every BAM entry must be zero (no
|
|
; free blocks anywhere). Otherwise this is an ordinary CBM disk whose files would be destroyed by
|
|
; writing raw tracks.
|
|
; ----------------------------------------------------------------------
|
|
checkBamAllocatedLoop:
|
|
lda contourRuleTable,y ; 7E63 a byte of the BAM allocation map (the $0200 buffer, contourRuleTable's address) (also trainerPlaybook0200:trainerPlay0)
|
|
bne L_7E71 ; 7E66 any non-zero byte means free blocks exist: not a data disk
|
|
iny ; 7E68 next BAM byte
|
|
cpy #$90 ; 7E69 $0203-$028F covers all 35 tracks
|
|
bcc checkBamAllocatedLoop ; 7E6B keep checking
|
|
L_7E6D:
|
|
jmp loadFilmDirectory ; 7E6D the disk is usable: read its film directory
|
|
|
|
L_7E70:
|
|
rts ; 7E70 C=1: no usable disk
|
|
|
|
L_7E71:
|
|
jmp L_100D ; 7E71 'NOT A DATA DISK!' ($0F81) and C=1
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeFilmDirectory - writes the 512-byte film directory (30 entries of 16 bytes plus slack) from
|
|
; $0300-$04FF back to track 18 sectors 2-3. A write error ends in diskErrorExit.
|
|
; In: the directory at $0300
|
|
; Out: track 18 sectors 2-3 rewritten, C=0
|
|
; Called from: $8164 (saveFilmToSlot), before the film itself is written
|
|
; ----------------------------------------------------------------------
|
|
writeFilmDirectory:
|
|
lda #$08 ; 7E74 8 frames ...
|
|
jsr waitFrames ; 7E76 ... to let the drive settle after the previous access
|
|
ldx #$00 ; 7E79 destination low byte $00 ...
|
|
ldy #$03 ; 7E7B ... high byte $03: the buffer at $0300
|
|
jsr setLoadDest ; 7E7D tell the fast loader where the data is
|
|
lda #$02 ; 7E80 2 sectors ...
|
|
tax ; 7E82 ... starting at sector 2 ...
|
|
ldy #$12 ; 7E83 ... of track 18, the directory track
|
|
jsr writeSectors ; 7E85 the drive encrypts and writes both sectors
|
|
bcs filmDirectoryDiskError ; 7E88 write failed
|
|
rts ; 7E8A directory saved
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadFilmDirectory - reads track 18 sectors 2-3 (the 30 x 16-byte film names) into $0300-$04FF. A
|
|
; read error ends in diskErrorExit. Tail of checkDiskAndLoadFilmDirectory.
|
|
; In: -
|
|
; Out: $0300-$04FF = film directory, C=0
|
|
; Called from: $7E6D (JMP)
|
|
; ----------------------------------------------------------------------
|
|
loadFilmDirectory:
|
|
ldx #$00 ; 7E8B destination low byte $00 ...
|
|
ldy #$03 ; 7E8D ... high byte $03: the buffer at $0300
|
|
jsr setLoadDest ; 7E8F tell the fast loader where to put the data
|
|
lda #$02 ; 7E92 2 sectors ...
|
|
tax ; 7E94 ... from sector 2 ...
|
|
ldy #$12 ; 7E95 ... of track 18
|
|
jsr loadSectorsDelayed ; 7E97 wait 8 frames, then read both sectors
|
|
bcs filmDirectoryDiskError ; 7E9A read failed
|
|
rts ; 7E9C directory loaded
|
|
|
|
filmDirectoryDiskError:
|
|
jmp diskErrorExit ; 7E9D show 'DISK ERROR.' and return C=1
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; drawFilmDirectoryPage - draws one page of the film menu: the centred title 'SAVE GAME FILM' or 'LOAD
|
|
; GAME FILM', then ten entries printed as a right-aligned slot number, a space and the 16-character
|
|
; film name, then ' (MORE)' when a three-page data disk is in the drive and finally ' (EXIT)'. Ends
|
|
; by setting up the menu selector: item count, window column 11 / width 18 (the name field, so that
|
|
; the highlight bar and the redraws leave the slot numbers alone), first row 9 and the highlight EOR
|
|
; mask.
|
|
; In: filmDirectoryPage $7FA1, filmMenuMode $7FA2, diskIdIndex $92F0, directory at $0300
|
|
; Out: the menu drawn; menuItemCount $91D4, menuSelectedItem $91D5 = 0, menuFirstRow $91D6 = 9,
|
|
; zp_3B = 11, zp_3C = 18, zp_3D = $60, zp_48/zp_49 = entry pointer, $90F8 and $9220 counters
|
|
; Called from: $7E16 and $7FDA (nextFilmDirectoryPage, JMP)
|
|
; ----------------------------------------------------------------------
|
|
drawFilmDirectoryPage:
|
|
jsr clearMenuTextWindow ; 7EA0 clear the menu window (screen column 2, 36 wide, from row 7)
|
|
lda #$66 ; 7EA3 'SAVE GAME FILM' at $CC66 ...
|
|
ldy #$CC ; 7EA5 ... in the text overlay
|
|
ldx filmMenuMode ; 7EA7 0 = save, 1 = load
|
|
beq L_7EB0 ; 7EAA save: keep that title
|
|
lda #$75 ; 7EAC 'LOAD GAME FILM' at $CC75 ...
|
|
ldy #$CC ; 7EAE ... in the text overlay
|
|
L_7EB0:
|
|
jsr printStringCentred ; 7EB0 centre the title in the 36-column menu window
|
|
lda #$09 ; 7EB3 indent the list ...
|
|
sta textWindowLeft ; 7EB5 ... to screen column 9
|
|
jsr printNewline ; 7EB7 down to the first list row (row 8)
|
|
lda #$16 ; 7EBA 22 columns: 'nn ' plus a 16-character name
|
|
sta textWindowWidth ; 7EBC also the wrap width used while printing
|
|
lda #$01 ; 7EBE dead store: setFilmDirectoryPagePtr overwrites $90F8 two instructions later
|
|
sta selectedUnit ; 7EC0 (leftover initialisation of the slot number)
|
|
jsr setFilmDirectoryPagePtr ; 7EC3 zp_48/zp_49 = first entry of this page, $90F8 = the slot number it is called
|
|
lda #$09 ; 7EC6 9 ...
|
|
sta droneFuel ; 7EC8 ... = ten entries per page (the counter runs 9..0); $9220 is only the drone fuel during a battle
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Entry loop: prints 'nn name' for the ten slots of this page, advancing zp_48/zp_49 by one 16-byte
|
|
; directory entry each time.
|
|
; ----------------------------------------------------------------------
|
|
printDirectoryEntryLoop:
|
|
lda selectedUnit ; 7ECB the slot number 1-30 printed in front of the name
|
|
cmp #$0A ; 7ECE numbers below 10 are one digit ...
|
|
bcs L_7ED7 ; 7ED0 ... two digits need no padding
|
|
lda #$A0 ; 7ED2 $A0 = space
|
|
jsr printChar ; 7ED4 pad one-digit numbers so all names start in the same column
|
|
L_7ED7:
|
|
lda selectedUnit ; 7ED7 the slot number
|
|
ldy #$00 ; 7EDA Y = middle byte of the 24-bit value printDecimal takes (the top byte zp_7E is left at 0)
|
|
jsr printDecimal ; 7EDC print it in decimal without leading zeros
|
|
lda #$A0 ; 7EDF $A0 = space
|
|
jsr printChar ; 7EE1 separate the number from the name
|
|
lda mapCellPtr ; 7EE4 pointer to this directory entry ...
|
|
ldy mapCellPtrHi ; 7EE6 ... low/high
|
|
jsr printString ; 7EE8 print the 16-character name (its last byte carries bit 7)
|
|
jsr printNewline ; 7EEB next screen line
|
|
lda mapCellPtr ; 7EEE advance the entry pointer ...
|
|
clc ; 7EF0 clear carry for the 16-bit add
|
|
adc #$10 ; 7EF1 ... by $10 = one 16-byte directory entry
|
|
sta mapCellPtr ; 7EF3 store it back
|
|
lda mapCellPtrHi ; 7EF5 high byte
|
|
adc #$00 ; 7EF7 carry only
|
|
sta mapCellPtrHi ; 7EF9 store it back
|
|
inc selectedUnit ; 7EFB next slot number
|
|
dec droneFuel ; 7EFE one entry done
|
|
bpl printDirectoryEntryLoop ; 7F01 ten entries per page
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Trailing menu items and the selector setup.
|
|
; ----------------------------------------------------------------------
|
|
lda #$0B ; 7F03 11 items ...
|
|
sta menuItemCount ; 7F05 ... = ten slots plus (EXIT)
|
|
lda diskIdIndex ; 7F08 0 = the game disk, 2 = a data disk
|
|
beq L_7F1C ; 7F0B the game disk holds only one page of ten slots
|
|
inc menuItemCount ; 7F0D a data disk has three pages, so there is a 12th item: (MORE)
|
|
lda #$03 ; 7F10 three blank columns ...
|
|
jsr bootReceiveSectorData ; 7F12 ... so that '(MORE)' lines up with the names ($C0EF is printFillChars in the text overlay) (also textEngineC000:printFillChars)
|
|
lda #$81 ; 7F15 ' (MORE)' at $EF81 ...
|
|
ldy #$EF ; 7F17 ... in the comm-module tail
|
|
jsr printString ; 7F19 print it (the string ends with a CR)
|
|
L_7F1C:
|
|
lda #$03 ; 7F1C three blank columns ...
|
|
jsr bootReceiveSectorData ; 7F1E ... for the last item too (also textEngineC000:printFillChars)
|
|
lda #$B2 ; 7F21 ' (EXIT)' at $CAB2 ...
|
|
ldy #$CA ; 7F23 ... the tail of the formation-menu string block
|
|
jsr printString ; 7F25 print it
|
|
lda #$0B ; 7F28 the selector window starts at screen column 11 ...
|
|
sta textWindowLeft ; 7F2A ... i.e. one column in front of the names
|
|
lda #$12 ; 7F2C 18 characters wide ...
|
|
sta textWindowWidth ; 7F2E ... = the space plus the 16-character name, and the width of the highlight bar
|
|
lda #$09 ; 7F30 the first slot sits on screen row 9 ...
|
|
sta menuFirstRow ; 7F32 ... menuFirstRow, to which the item number is added
|
|
lda #$00 ; 7F35 start the bar ...
|
|
sta menuSelectedItem ; 7F37 ... on the first slot
|
|
lda #$60 ; 7F3A $60 = the EOR mask that flips a row between the two text colours
|
|
sta textColour ; 7F3C zp_3D is both the plot colour and the highlight mask
|
|
rts ; 7F3E the page is ready for the selector
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runLoadFilmMenu - LOAD GAME FILM. Runs the standard menu selector over the directory page; (MORE)
|
|
; turns to the next page and starts again, (EXIT) leaves. Picking a slot converts it to a disk track
|
|
; and reads that track's 16 sectors one at a time into $0200, copying each page into the film buffer
|
|
; $D000-$DFFF that lives under the I/O area, and finally validates the header.
|
|
; In: menuSelectedItem $91D5, diskIdIndex $92F0, filmDirectoryPage $7FA1, the disk
|
|
; Out: $D000-$DFFF = the film, $91D5 = the track it came from, zp_18/zp_19, the copy pointers
|
|
; $58C3/$58C4 (source) and $58C6/$58C7 (destination)
|
|
; Called from: $7E24 (handleMainMenuChoice) and $7F54 (itself, after a page turn)
|
|
; ----------------------------------------------------------------------
|
|
runLoadFilmMenu:
|
|
sec ; 7F3F C=1: ordinary menu behaviour (the bar pauses before it reaches the last row)
|
|
jsr runMenuLoopWithFlag ; 7F40 run the selector until fire or RETURN
|
|
lda menuSelectedItem ; 7F43 the chosen item
|
|
cmp #$0A ; 7F46 items 0-9 are film slots
|
|
bcc L_7F57 ; 7F48 load that slot
|
|
bne L_7F9E ; 7F4A item 11 is always (EXIT)
|
|
lda diskIdIndex ; 7F4C item 10 is (MORE) only when a data disk is in the drive
|
|
beq L_7F9E ; 7F4F on the game disk item 10 is (EXIT)
|
|
jsr nextFilmDirectoryPage ; 7F51 turn to the next page and redraw it
|
|
jmp runLoadFilmMenu ; 7F54 and run the selector again
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Load one film: 16 sectors of one track, each read into $0200 and then copied into the film buffer
|
|
; under the I/O area at $D000.
|
|
; ----------------------------------------------------------------------
|
|
L_7F57:
|
|
jsr suspendCommForDiskAccess; 7F57 pause the comm module for the disk access
|
|
jsr filmSlotToTrack ; 7F5A slot -> track number
|
|
sta scratch19 ; 7F5D zp_19 = the track to read
|
|
lda #$00 ; 7F5F copyPageUnderIo destination low byte = $00 ...
|
|
sta D_58C6 ; 7F61 ... (operand of the STA at $58C5)
|
|
lda #$D0 ; 7F64 ... high byte $D0: the film buffer under I/O
|
|
sta D_58C7 ; 7F66 the destination page walks $D000..$DF00
|
|
lda #$00 ; 7F69 copyPageUnderIo source low byte = $00 ...
|
|
sta D_58C3 ; 7F6B ... (operand of the LDA at $58C2)
|
|
lda #$02 ; 7F6E ... high byte $02: the sector buffer at $0200
|
|
sta D_58C4 ; 7F70 the source stays at $0200 for every sector
|
|
lda #$00 ; 7F73 start with ...
|
|
sta scratch18 ; 7F75 ... sector 0 in zp_18
|
|
loadFilmSectorLoop:
|
|
ldx #$00 ; 7F77 destination low byte $00 ...
|
|
ldy #$02 ; 7F79 ... high byte $02: read the sector to $0200
|
|
jsr setLoadDest ; 7F7B tell the fast loader where to put it
|
|
lda #$01 ; 7F7E one sector ...
|
|
ldy scratch19 ; 7F80 ... from the film's track ...
|
|
ldx scratch18 ; 7F82 ... at the current sector number
|
|
jsr loadSectorsDelayed ; 7F84 wait 8 frames, then read it
|
|
jsr copyPageUnderIo ; 7F87 copy those 256 bytes into the film buffer with the I/O area banked out
|
|
inc D_58C7 ; 7F8A next destination page ($D000, $D100, ...)
|
|
inc scratch18 ; 7F8D next sector
|
|
lda scratch18 ; 7F8F sector counter
|
|
cmp #$10 ; 7F91 16 sectors = the whole 4 KB film buffer
|
|
bcc loadFilmSectorLoop ; 7F93 keep reading
|
|
jsr resumeCommAfterDiskAccess; 7F95 restart the comm module and clear the status line
|
|
jsr checkFilmInMemory ; 7F98 does the loaded data have a valid film header?
|
|
bcc L_7F9E ; 7F9B valid
|
|
rts ; 7F9D invalid: checkFilmInMemory has already shown 'NOT A GAME FILM!'
|
|
|
|
L_7F9E:
|
|
jmp resetMessageQueue ; 7F9E valid: clear the message queue and return
|
|
|
|
|
|
; filmDirectoryPage - which of the three directory pages is on screen (0-2). A game disk only ever
|
|
; shows page 0; a data disk holds 30 films in three pages of ten.
|
|
filmDirectoryPage:
|
|
.byte $00 ; 7FA1 . current page 0-2
|
|
|
|
; filmMenuMode - 0 = SAVE GAME FILM, 1 = LOAD GAME FILM (set from the main-menu item - 3).
|
|
filmMenuMode:
|
|
.byte $00 ; 7FA2 . 0 = save, 1 = load
|
|
|
|
; filmPageFirstSlotTable - the slot number printed for the first entry of each page: page 0 starts at
|
|
; 1, page 1 at 11 and page 2 at 21, so the 30 slots are numbered 1-30 on screen.
|
|
filmPageFirstSlotTable:
|
|
.byte $01,$0B,$15 ; 7FA3 ... first slot number of pages 0, 1 and 2
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setFilmDirectoryPagePtr - points zp_48/zp_49 at the first directory entry of the current page ($0300
|
|
; + page * $A0, ten entries of 16 bytes per page) and puts the slot number that entry is shown as (1,
|
|
; 11 or 21) into $90F8 for the numbering loop.
|
|
; In: filmDirectoryPage $7FA1, filmPageFirstSlotTable $7FA3
|
|
; Out: zp_48/zp_49 = entry pointer, $90F8 = first slot number
|
|
; Called from: $7EC3 (drawFilmDirectoryPage) and $7FDD (runSaveFilmMenu)
|
|
; ----------------------------------------------------------------------
|
|
setFilmDirectoryPagePtr:
|
|
lda #$00 ; 7FA6 low byte of $0300 ...
|
|
sta mapCellPtr ; 7FA8 ... the directory buffer
|
|
lda #$03 ; 7FAA high byte $03 ...
|
|
sta mapCellPtrHi ; 7FAC ... zp_48/zp_49 = $0300
|
|
ldy filmDirectoryPage ; 7FAE current page 0-2
|
|
lda filmPageFirstSlotTable,y; 7FB1 1, 11 or 21
|
|
sta selectedUnit ; 7FB4 $90F8 = the slot number of the first entry on this page
|
|
tya ; 7FB7 page number into A
|
|
beq L_7FCA ; 7FB8 page 0 needs no offset
|
|
addDirectoryPageOffsetLoop:
|
|
lda mapCellPtr ; 7FBA add one page ...
|
|
clc ; 7FBC clear carry for the 16-bit add
|
|
adc #$A0 ; 7FBD ... $A0 = 10 entries of 16 bytes
|
|
sta mapCellPtr ; 7FBF store it back
|
|
lda mapCellPtrHi ; 7FC1 high byte
|
|
adc #$00 ; 7FC3 carry only
|
|
sta mapCellPtrHi ; 7FC5 store it back
|
|
dey ; 7FC7 one page done
|
|
bne addDirectoryPageOffsetLoop; 7FC8 repeat for each page skipped
|
|
L_7FCA:
|
|
rts ; 7FCA zp_48/zp_49 now points at entry 0 of the page
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; nextFilmDirectoryPage - steps filmDirectoryPage on modulo 3 and redraws the list.
|
|
; In: filmDirectoryPage $7FA1
|
|
; Out: $7FA1 advanced (wrapping 2 -> 0); jumps to drawFilmDirectoryPage
|
|
; Called from: $7F51 (load menu) and $80CC (save menu), both on the (MORE) item
|
|
; ----------------------------------------------------------------------
|
|
nextFilmDirectoryPage:
|
|
inc filmDirectoryPage ; 7FCB step to the next page
|
|
D_7FCE:
|
|
lda filmDirectoryPage ; 7FCE read it back (the D_7FCE label only exists because the comcen-screens overlay has a table here)
|
|
cmp #$03 ; 7FD1 three pages of ten slots
|
|
bcc L_7FDA ; 7FD3 still in range
|
|
lda #$00 ; 7FD5 wrap ...
|
|
sta filmDirectoryPage ; 7FD7 ... back to page 0
|
|
L_7FDA:
|
|
jmp drawFilmDirectoryPage ; 7FDA redraw the ten entries of the new page
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runSaveFilmMenu - SAVE GAME FILM. A private copy of the generic menu selector (runMenuLoopWatchUnit
|
|
; $5FDC) with a text field built into it: while a slot 0-9 is highlighted every key goes to the line
|
|
; editor handleTextEntryKey (at most 16 characters into the buffer
|
|
; $9278) and the typed text is echoed on that row with a block cursor behind it. Moving the bar
|
|
; up or down throws the typed text away; fire or RETURN accepts. (MORE) turns the page, (EXIT)
|
|
; leaves, and a slot with typed text gets the name copied into its 16-byte directory entry (padded
|
|
; with spaces, bit 7 on the last byte) before saveFilmToSlot writes directory and film.
|
|
; In: menuItemCount $91D4, menuSelectedItem $91D5, menuFirstRow $91D6, filmDirectoryPage $7FA1,
|
|
; the keyboard through $90EA, the joystick zp_5D, the frame timers zp_6C and zp_6E
|
|
; Out: Y = $91D5 (after a save that is the disk track, not the slot), filmNameLength $92E6 = 0,
|
|
; $92C5, the typed name in $9278 and in the directory entry, the film on disk;
|
|
; zp_4A/zp_4B = pointer to the selected directory entry; self-modifies $809B+1 and $809F+1
|
|
; Called from: $7E1E (handleMainMenuChoice) and $80CF (itself, after a page turn)
|
|
; ----------------------------------------------------------------------
|
|
runSaveFilmMenu:
|
|
jsr setFilmDirectoryPagePtr ; 7FDD zp_48/zp_49 = the first entry of this page
|
|
ldx menuItemCount ; 7FE0 number of menu items
|
|
dex ; 7FE3 count - 1 ...
|
|
dex ; 7FE4 ... - 1 = the row the bar has to slow down in front of
|
|
stx L_809B+1 ; 7FE5 self-modifying: that row becomes the operand of the CMP #$FF at $809B
|
|
lda #$14 ; 7FE8 $14 = 20 frames ...
|
|
sta joyRepeatTimer ; 7FEA ... of input lockout before the stick is read
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Entered again after every move of the bar: clear the typed text, work out the address of the
|
|
; selected directory entry and draw the row highlighted.
|
|
; ----------------------------------------------------------------------
|
|
saveMenuRedrawRow:
|
|
lda #$00 ; 7FEC start with ...
|
|
sta filmNameLength ; 7FEE ... an empty name ...
|
|
sta menuBlinkState ; 7FF1 ... the row highlighted ($92C5 = 0) ...
|
|
sta fillEndPtrHi ; 7FF4 ... and the high byte of the entry pointer cleared
|
|
L_7FF6:
|
|
lda menuSelectedItem ; 7FF6 the selected item
|
|
cmp #$0A ; 7FF9 items 0-9 are slots ...
|
|
bcs L_8010 ; 7FFB ... (MORE) and (EXIT) have no directory entry
|
|
ldy #$04 ; 7FFD shift four times ...
|
|
L_7FFF:
|
|
asl a ; 7FFF ... to multiply the item number by 16 ...
|
|
rol fillEndPtrHi ; 8000 ... into A (low) and zp_4B (high)
|
|
dey ; 8002 four shifts
|
|
bne L_7FFF ; 8003 keep shifting
|
|
clc ; 8005 add the page pointer ...
|
|
adc mapCellPtr ; 8006 ... low byte
|
|
sta fillEndPtr ; 8008 zp_4A = address of the entry
|
|
lda fillEndPtrHi ; 800A high byte ...
|
|
adc mapCellPtrHi ; 800C ... plus the carry
|
|
sta fillEndPtrHi ; 800E zp_4B = high byte of the entry pointer
|
|
L_8010:
|
|
lda #$01 ; 8010 colour $01 = the highlighted row (black on white)
|
|
jsr printFilmSlotEntry ; 8012 draw the row (name, or (MORE)/(EXIT))
|
|
lda #$9E ; 8015 $9E = the line-editor terminator ...
|
|
sta typedLineBuffer ; 8017 ... so the typed-name buffer starts out empty
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Input loop: one pass per frame. Keys go into the name field, the highlight blinks every 20 frames
|
|
; and the joystick moves the bar or confirms the choice.
|
|
; ----------------------------------------------------------------------
|
|
saveMenuInputLoop:
|
|
jsr serviceCommandExchange ; 801A keep the lock-step command exchange running while the player types
|
|
lda menuSelectedItem ; 801D the selected item
|
|
cmp #$0A ; 8020 only slots 0-9 have a name to type
|
|
bcs L_805E ; 8022 (MORE)/(EXIT): skip the text field
|
|
ldy filmNameLength ; 8024 Y = current cursor position in the name buffer
|
|
lda #$10 ; 8027 A = 16 = maximum length, bit 7 clear = beep instead of scrolling when full
|
|
jsr handleTextEntryKey ; 8029 let the shared line editor handle one key press
|
|
beq L_805E ; 802C nothing was accepted this frame
|
|
tya ; 802E Y comes back with bit 7 set when RETURN was pressed
|
|
bpl L_8034 ; 802F an ordinary character: echo the line
|
|
jmp saveMenuAccept ; 8031 RETURN accepts the entry just like the fire button
|
|
|
|
L_8034:
|
|
sty filmNameLength ; 8034 remember the new cursor position
|
|
lda #$01 ; 8037 colour $01 = the highlighted row ...
|
|
ldy menuBlinkState ; 8039 ... but $92C5 tells which half of the blink we are in
|
|
beq L_8040 ; 803C row currently highlighted: keep $01
|
|
lda #$61 ; 803E $61 = the un-highlighted colour ($01 EOR the $60 mask)
|
|
L_8040:
|
|
sta textColour ; 8040 the colour the text engine writes for each cell
|
|
lda #$00 ; 8042 back to ...
|
|
sta textCursorCol ; 8044 ... column 0 of the name field (screen column 11)
|
|
jsr updateCursorPointers ; 8046 recompute the bitmap and colour pointers for that cell
|
|
jsr bootToggleDataAck ; 8049 erase the rest of the row (also textEngineC000:clearToEndOfLine)
|
|
lda #$A0 ; 804C $A0 = space
|
|
jsr printChar ; 804E one blank column in front of the name
|
|
lda #$78 ; 8051 the typed-name buffer $9278 ...
|
|
ldy #$92 ; 8053 ... low/high
|
|
jsr printString ; 8055 print what has been typed so far
|
|
ldy #$00 ; 8058 the cell the cursor now sits in
|
|
lda #$11 ; 805A $11 = both colour nibbles white = a solid block
|
|
L_805C:
|
|
sta (screenPtr),y ; 805C draw the block cursor behind the text
|
|
L_805E:
|
|
lda menuBlinkTimer ; 805E blink timer, counted down by the IRQ
|
|
bne L_8069 ; 8060 not time yet
|
|
lda #$60 ; 8062 $60 = the EOR mask ...
|
|
sta textColour ; 8064 ... used as the toggle colour
|
|
jsr toggleMenuRowHighlight ; 8066 flip the row between $01 and $61 and restart the 20-frame timer
|
|
L_8069:
|
|
lda joyDirection ; 8069 the joystick direction latched by the IRQ (active low)
|
|
cmp #$0F ; 806B $0F = centred
|
|
bne L_8074 ; 806D the stick is being pushed: do not test fire
|
|
jsr checkFirePressed ; 806F edge-detect a new fire-button press
|
|
beq saveMenuAccept ; 8072 fire pressed: accept the entry
|
|
L_8074:
|
|
ldy joyRepeatTimer ; 8074 auto-repeat delay still running?
|
|
bne L_8098 ; 8076 yes: ignore the stick this frame
|
|
lda joyDirection ; 8078 the joystick nibble again
|
|
ora #$0C ; 807A mask the left/right bits out, only up and down move the bar
|
|
ldy #$FF ; 807C -1 = move up
|
|
cmp #$0E ; 807E $0E = up (bit 0 low)
|
|
beq L_8088 ; 8080 move up
|
|
ldy #$01 ; 8082 +1 = move down
|
|
cmp #$0D ; 8084 $0D = down (bit 1 low)
|
|
bne L_8098 ; 8086 neither: nothing to do
|
|
L_8088:
|
|
tya ; 8088 A = the step, -1 or +1
|
|
sty L_809F+1 ; 8089 self-modifying: the direction becomes the operand of the LDY #$FF at $809F
|
|
clc ; 808C prepare the add
|
|
L_808D:
|
|
adc menuSelectedItem ; 808D new selection = old selection + step
|
|
jsr setJoystickRepeatDelay ; 8090 start the auto-repeat delay (16 frames first, then 6)
|
|
cmp menuItemCount ; 8093 past the last item?
|
|
bcc L_809B ; 8096 no: take the new selection
|
|
L_8098:
|
|
jmp saveMenuInputLoop ; 8098 out of range: leave the bar where it is
|
|
|
|
L_809B:
|
|
cmp #$FF ; 809B operand patched at $7FE5: the row in front of the last one
|
|
bne L_80A7 ; 809D an ordinary row: take it
|
|
L_809F:
|
|
ldy #$FF ; 809F operand patched at $8089: the direction we are moving in
|
|
bmi L_80A7 ; 80A1 moving up: no extra delay
|
|
ldy #$1E ; 80A3 moving down into the last-but-one row: pause ...
|
|
sty joyRepeatTimer ; 80A5 ... 30 frames, so the bar does not run into (MORE)/(EXIT) by accident
|
|
L_80A7:
|
|
pha ; 80A7 hold the new selection
|
|
lda #$61 ; 80A8 $61 = the un-highlighted colour
|
|
jsr printFilmSlotEntry ; 80AA redraw the row we are leaving (this also wipes the typed text)
|
|
pla ; 80AD the new selection
|
|
sta menuSelectedItem ; 80AE make it current
|
|
lda #$02 ; 80B1 sound 2 ...
|
|
jsr playSound ; 80B3 ... the menu click
|
|
jmp saveMenuRedrawRow ; 80B6 start again with an empty name field on the new row
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The choice has been accepted with fire or RETURN: turn the page, leave, or write the film into the
|
|
; selected slot.
|
|
; ----------------------------------------------------------------------
|
|
saveMenuAccept:
|
|
lda #$08 ; 80B9 8 frames ...
|
|
jsr waitFrames ; 80BB ... so the button press is not seen twice
|
|
lda menuSelectedItem ; 80BE the accepted item
|
|
cmp #$0A ; 80C1 items 0-9 are film slots
|
|
bcc L_80D2 ; 80C3 save into that slot
|
|
bne L_8103 ; 80C5 item 11 is always (EXIT)
|
|
lda diskIdIndex ; 80C7 0 = game disk: item 10 is (EXIT) too
|
|
beq L_8103 ; 80CA leave the menu
|
|
jsr nextFilmDirectoryPage ; 80CC (MORE): turn to the next page ...
|
|
jmp runSaveFilmMenu ; 80CF ... and run the selector again
|
|
|
|
L_80D2:
|
|
lda filmNameLength ; 80D2 how many characters were typed?
|
|
beq L_80FB ; 80D5 nothing typed: keep the old name
|
|
and #$7F ; 80D7 mask the RETURN bit off (it cannot be set here - defensive)
|
|
sta scratch18 ; 80D9 zp_18 = length of the typed name
|
|
ldy #$0F ; 80DB start at the last byte of the 16-byte entry
|
|
lda #$A0 ; 80DD $A0 = space
|
|
bne L_80E4 ; 80DF always taken - enter the loop at the test
|
|
padDirectoryNameLoop:
|
|
sta (fillEndPtr),y ; 80E1 pad this position with a space
|
|
dey ; 80E3 next lower position
|
|
L_80E4:
|
|
cpy scratch18 ; 80E4 pad everything from the end down to the typed length
|
|
bcs padDirectoryNameLoop ; 80E6 keep padding
|
|
tya ; 80E8 Y = length - 1
|
|
bmi L_80FB ; 80E9 nothing typed at all
|
|
copyTypedNameLoop:
|
|
lda typedLineBuffer,y ; 80EB a character of the typed name
|
|
sta (fillEndPtr),y ; 80EE into the directory entry
|
|
dey ; 80F0 next lower character
|
|
bpl copyTypedNameLoop ; 80F1 copy the whole name
|
|
ldy #$0F ; 80F3 the last byte of the entry ...
|
|
lda (fillEndPtr),y ; 80F5 ... gets ...
|
|
ora #$80 ; 80F7 ... bit 7 set: that is the string terminator
|
|
sta (fillEndPtr),y ; 80F9 store it back
|
|
L_80FB:
|
|
lda #$01 ; 80FB colour $01 = highlighted
|
|
jsr printFilmSlotEntry ; 80FD redraw the row from the directory entry, so the saved name is shown
|
|
jsr saveFilmToSlot ; 8100 write the directory and the 16 sectors of the film to disk
|
|
L_8103:
|
|
lda #$00 ; 8103 clear ...
|
|
sta filmNameLength ; 8105 ... the typed length
|
|
ldy menuSelectedItem ; 8108 Y = the selection ($91D5, which filmSlotToTrack has replaced with the track)
|
|
rts ; 810B back to the main menu
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printFilmSlotEntry - redraws the currently selected row of the film menu in colour A: moves the
|
|
; cursor to menuFirstRow + menuSelectedItem, column 0 of the name window (screen column 11), clears
|
|
; the rest of the line and prints a space followed by the entry text - the 16-byte name at zp_4A/zp_4B
|
|
; for slots 0-9, ' (MORE)' for item 10 on a data disk, ' (EXIT)' otherwise. The slot number printed by
|
|
; drawFilmDirectoryPage sits in columns 9-10 and is left alone.
|
|
; In: A = colour byte, menuSelectedItem $91D5, menuFirstRow $91D6, diskIdIndex $92F0, zp_4A/zp_4B
|
|
; Out: one menu row redrawn; zp_3D = A, zp_3F = 0
|
|
; Called from: $8012, $80AA and $80FD (all in runSaveFilmMenu)
|
|
; ----------------------------------------------------------------------
|
|
printFilmSlotEntry:
|
|
sta textColour ; 810C the colour the text engine writes for each cell ($01 highlighted, $61 normal)
|
|
lda menuSelectedItem ; 810E the selected item
|
|
clc ; 8111 clear carry for the add
|
|
adc menuFirstRow ; 8112 + the first menu row = the screen row of this entry
|
|
ldy #$00 ; 8115 column 0 ...
|
|
sty textCursorCol ; 8117 ... of the name window (screen column 11)
|
|
jsr setCursorRow ; 8119 move the cursor there and recompute the cell pointers
|
|
jsr bootToggleDataAck ; 811C erase the old contents of the row (also textEngineC000:clearToEndOfLine)
|
|
lda #$A0 ; 811F $A0 = space
|
|
jsr printChar ; 8121 one blank column in front of the name
|
|
L_8124:
|
|
lda #$B2 ; 8124 ' (EXIT)' at $CAB2 ...
|
|
ldy #$CA ; 8126 ... the default text for the last item
|
|
ldx menuSelectedItem ; 8128 the selected item
|
|
cpx #$0A ; 812B items 0-9 are film slots
|
|
bcc L_813C ; 812D print the name of the entry
|
|
L_812F:
|
|
bne L_8140 ; 812F item 11 is always (EXIT)
|
|
ldx diskIdIndex ; 8131 0 = game disk
|
|
beq L_8140 ; 8134 on the game disk item 10 is (EXIT) as well
|
|
lda #$81 ; 8136 ' (MORE)' at $EF81 ...
|
|
ldy #$EF ; 8138 ... in the comm-module tail
|
|
bne L_8140 ; 813A always taken
|
|
L_813C:
|
|
lda fillEndPtr ; 813C pointer to the 16-byte directory entry ...
|
|
ldy fillEndPtrHi ; 813E ... low/high
|
|
L_8140:
|
|
jmp printString ; 8140 print it and return through printString
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; filmSlotToTrack - turns the selected directory slot into the disk track its film occupies: page * 10
|
|
; + item + 2, plus one more when the result would be the directory track 18. Slots 0-29 therefore
|
|
; live on tracks 2-17 and 19-32, sixteen sectors each. The track replaces menuSelectedItem $91D5 and
|
|
; is also returned in A.
|
|
; In: filmDirectoryPage $7FA1, menuSelectedItem $91D5
|
|
; Out: A = $91D5 = track number 2-32
|
|
; Called from: $7F5A (load) and $8161 (save)
|
|
; ----------------------------------------------------------------------
|
|
filmSlotToTrack:
|
|
lda filmDirectoryPage ; 8143 the page the slot is on
|
|
beq L_814D ; 8146 page 0: no offset
|
|
ldy #$0A ; 8148 ten slots ...
|
|
jsr multiply8x8 ; 814A ... per page; A comes back holding the low byte of page * 10
|
|
L_814D:
|
|
clc ; 814D add ...
|
|
adc menuSelectedItem ; 814E ... the item number 0-9 ...
|
|
adc #$02 ; 8151 ... and 2, because tracks 0 and 1 are not used for films
|
|
cmp #$12 ; 8153 $12 = 18 = the directory track
|
|
bcc L_815A ; 8155 below it: use the track as it is
|
|
L_8157:
|
|
clc ; 8157 at or above track 18 ...
|
|
adc #$01 ; 8158 ... skip the directory track
|
|
L_815A:
|
|
sta menuSelectedItem ; 815A the selection now holds the track number
|
|
rts ; 815D A = the track
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; saveFilmToSlot - writes the film in the buffer under I/O to the disk: rewrites the directory (with
|
|
; the new name in it) and then copies the 16 pages of $D000-$DFFF one at a time into the
|
|
; $0200 sector buffer and writes each as one sector 0-15 of the film's track.
|
|
; In: menuSelectedItem $91D5 = slot, the film at $D000-$DFFF, the directory at $0300
|
|
; Out: track sectors 0-15 written, $91D5 = the track used, zp_18/zp_19, the copy pointers
|
|
; $58C3/$58C4 (source) and $58C6/$58C7 (destination)
|
|
; Called from: $8100 (runSaveFilmMenu)
|
|
; ----------------------------------------------------------------------
|
|
saveFilmToSlot:
|
|
jsr suspendCommForDiskAccess; 815E pause the comm module for the disk access
|
|
jsr filmSlotToTrack ; 8161 slot -> track number
|
|
jsr writeFilmDirectory ; 8164 write the 2 directory sectors first, so the name is on disk even if the film write fails
|
|
L_8167:
|
|
lda #$00 ; 8167 copyPageUnderIo source low byte = $00 ...
|
|
sta D_58C3 ; 8169 ... (operand of the LDA at $58C2)
|
|
lda #$D0 ; 816C ... high byte $D0: the film buffer under I/O
|
|
sta D_58C4 ; 816E the source page walks $D000..$DF00
|
|
lda #$00 ; 8171 copyPageUnderIo destination low byte = $00 ...
|
|
sta D_58C6 ; 8173 ... (operand of the STA at $58C5)
|
|
lda #$02 ; 8176 ... high byte $02: the sector buffer
|
|
sta D_58C7 ; 8178 the destination stays at $0200
|
|
lda #$00 ; 817B start with ...
|
|
sta scratch18 ; 817D ... sector 0 in zp_18
|
|
lda menuSelectedItem ; 817F the track filmSlotToTrack worked out
|
|
L_8182:
|
|
sta scratch19 ; 8182 zp_19 = the track to write
|
|
writeFilmSectorLoop:
|
|
jsr copyPageUnderIo ; 8184 copy one page of the film into $0200 with the I/O area banked out
|
|
ldx #$00 ; 8187 source low byte $00 ...
|
|
ldy #$02 ; 8189 ... high byte $02: write from $0200
|
|
jsr setLoadDest ; 818B tell the fast loader where the data is
|
|
ldy scratch19 ; 818E the film's track ...
|
|
ldx scratch18 ; 8190 ... and the current sector number
|
|
lda #$01 ; 8192 one sector at a time
|
|
jsr writeSectors ; 8194 the drive encrypts and writes it
|
|
inc D_58C4 ; 8197 next source page ($D000, $D100, ...)
|
|
inc scratch18 ; 819A next sector
|
|
lda scratch18 ; 819C sector counter
|
|
cmp #$10 ; 819E 16 sectors = the whole 4 KB film
|
|
bcc writeFilmSectorLoop ; 81A0 keep writing
|
|
jmp resumeCommAfterDiskAccess; 81A2 restart the comm module and clear the status line
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; promptInsertGameDisk - end of the save/load film flow: shows 'INSERT GAME DISK AND PRESS SPACE.',
|
|
; then makes sure the 'OZ' disk really is in the drive and restarts the comm module.
|
|
; In: -
|
|
; Out: the game disk is in the drive, the comm module runs again
|
|
; Called from: $7E21 and $7E27 (JMP, after the save and load menus)
|
|
; ----------------------------------------------------------------------
|
|
promptInsertGameDisk:
|
|
lda #$D4 ; 81A5 low byte of 'GAME' at $EFD4 ...
|
|
L_81A7:
|
|
ldy #$EF ; 81A7 ... in the comm-module tail
|
|
jsr promptInsertDiskAndWaitSpace; 81A9 patch it into the message and wait for space
|
|
jsr waitForGameDiskInserted ; 81AC keep asking until checkDiskId finds the 'OZ' disk
|
|
jmp resumeCommAfterDiskAccess; 81AF restart the comm module and clear the status line
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; promptInsertDataDisk - entry of promptInsertDiskAndWaitSpace with the word 'DATA' ($EFD0).
|
|
; In: -
|
|
; Out: see promptInsertDiskAndWaitSpace
|
|
; Called from: $7DF4 (handleMainMenuChoice)
|
|
; ----------------------------------------------------------------------
|
|
promptInsertDataDisk:
|
|
lda #$D0 ; 81B2 low byte of 'DATA' at $EFD0 ...
|
|
ldy #$EF ; 81B4 ... in the comm-module tail; fall through
|
|
|
|
; ----------------------------------------------------------------------
|
|
; promptInsertDiskAndWaitSpace - shows 'INSERT xxxx DISK AND PRESS SPACE.' and waits for the space
|
|
; bar. The four characters at $EFB6 inside the message template at $EFAF are overwritten with the
|
|
; word passed in A/Y ('DATA' or 'GAME'), the template is installed as message slot $1E and shown as
|
|
; the persistent status message; the link keeps being serviced while waiting.
|
|
; In: A/Y = address of a 4-character disk name ($EFD0 'DATA' or $EFD4 'GAME')
|
|
; Out: $EFB6-$EFB9 patched, message slot $1E = $EFAF, $92A1, lastTypedKey $9248 = $FF, the
|
|
; message line erased; self-modifies the operand of the LDA at $81C7
|
|
; Called from: $81A9 (promptInsertGameDisk) and by fall-through from promptInsertDataDisk
|
|
; ----------------------------------------------------------------------
|
|
promptInsertDiskAndWaitSpace:
|
|
sta patchDiskNameLoop+1 ; 81B6 self-modifying: the word's low byte becomes the operand of the LDA at $81C7 ...
|
|
sty patchDiskNameLoop+2 ; 81B9 ... and its high byte the second operand byte
|
|
ldx #$1E ; 81BC message slot $1E is the free 'dynamic' slot
|
|
lda #$AF ; 81BE 'INSERT DATA DISK AND PRESS SPACE.' at $EFAF ...
|
|
ldy #$EF ; 81C0 ... in the comm-module tail
|
|
jsr setMessageSlot ; 81C2 install that string as message $1E
|
|
ldy #$03 ; 81C5 four characters to patch
|
|
patchDiskNameLoop:
|
|
lda irqVectorHi,y ; 81C7 operand patched at $81B6/$81B9: reads 'DATA' or 'GAME' (assembled as an LDA $FFFF,y)
|
|
sta diskNameSlot,y ; 81CA overwrite the word inside the message template
|
|
dey ; 81CD next character
|
|
bpl patchDiskNameLoop ; 81CE all four
|
|
L_81D0:
|
|
lda #$1E ; 81D0 message $1E ...
|
|
sta persistentMessageId ; 81D2 ... becomes the persistent status message
|
|
jsr queueMessage ; 81D5 put it on the status line
|
|
lda #$FF ; 81D8 $FF = no key pending
|
|
sta lastTypedKey ; 81DA forget the last key
|
|
waitSpaceForDiskLoop:
|
|
jsr serviceCommandExchange ; 81DD keep the link alive while the player swaps disks
|
|
lda lastTypedKey ; 81E0 the last key the IRQ saw
|
|
cmp #$A0 ; 81E3 $A0 = the space bar
|
|
bne waitSpaceForDiskLoop ; 81E5 keep waiting
|
|
lda #$02 ; 81E7 sound 2 ...
|
|
jsr playSound ; 81E9 ... the acknowledging click
|
|
lda #$00 ; 81EC no persistent message ...
|
|
sta persistentMessageId ; 81EE ... any more
|
|
jmp clearMessageLine ; 81F1 erase the message line and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; checkFilmInMemory - is the buffer under I/O at $D000 a valid game film? verifyFilmHeader recomputes
|
|
; the header checksum, compares it with header byte $33 and checks the version byte
|
|
; $2F against $0B94. A valid film returns C=0 (and leaves zp_BC/zp_BD = $D000 for the readers
|
|
; that follow); otherwise message 9 'NOT A GAME FILM!' is shown for about four seconds and C=1 comes
|
|
; back.
|
|
; In: the film header at $D000
|
|
; Out: C=0 and A = header byte 4 when valid, C=1 otherwise; zp_BC/zp_BD = $D000
|
|
; Called from: $7DEF, $7E2F (main menu) and $7F98 (after loading a film)
|
|
; ----------------------------------------------------------------------
|
|
checkFilmInMemory:
|
|
jsr verifyFilmHeader ; 81F4 check the header checksum and version byte
|
|
bcc L_820A ; 81F7 C=0: it is a film
|
|
lda #$09 ; 81F9 message 9 = 'NOT A GAME FILM!'
|
|
jsr queueMessage ; 81FB queue it, then fall into the four-second wait
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitFramesServicingComm - waits about four seconds (240 IRQ frames counted down in zp_6C) while
|
|
; still servicing the command exchange, then returns C=1. In this overlay it is only ever reached by
|
|
; falling out of checkFilmInMemory, so that the 'NOT A GAME FILM!' message stays readable before the
|
|
; caller carries on. (The main program's call at $563C is to the drone routine that the
|
|
; comcen-screens overlay has at this same address, not to this code.)
|
|
; In: -
|
|
; Out: C=1, joyRepeatTimer zp_6C = 0
|
|
; Called from: fall-through from $81FB
|
|
; ----------------------------------------------------------------------
|
|
waitFramesServicingComm:
|
|
lda #$F0 ; 81FE $F0 = 240 frames ...
|
|
sta joyRepeatTimer ; 8200 ... about four seconds in one of the IRQ's frame countdowns
|
|
waitFramesLinkLoop:
|
|
jsr serviceCommandExchange ; 8202 keep the link alive while the message is on screen
|
|
lda joyRepeatTimer ; 8205 the IRQ counts the timer down
|
|
bne waitFramesLinkLoop ; 8207 wait until it reaches zero
|
|
sec ; 8209 C=1 = 'no usable film'
|
|
L_820A:
|
|
rts ; 820A shared RTS: also the C=0 exit of checkFilmInMemory
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; beginGameTypeSelection - one-off prologue of the game-type selection screen, run only by the side
|
|
; that picks the scenario (side 0). Throws away any queued status message, forces the map screen,
|
|
; tells showToggleStateMessage that the '*' toggle is OFF-with-no-message, installs 'CUSTOM OFF'
|
|
; ($EFD8) as message slot $1E and falls into runGameTypeMenu.
|
|
; In: nothing (message slot $1E and the patch operand at $868D are the things it sets up)
|
|
; Out: D_90FB = 0, $868D = 0 (off-state persistent message id), message slot $1E -> $EFD8
|
|
; Called from: startGameSession $79A1 (side 0 only; side 1 waits with message $1A instead)
|
|
; ----------------------------------------------------------------------
|
|
beginGameTypeSelection:
|
|
jsr resetMessageQueue ; 820B drop everything still queued in the status-message ring before the setup screen appears
|
|
lda #$00 ; 820E 0 = the normal map/battlefield screen (not a comcen panel)
|
|
sta currentScreen ; 8210 currentScreen $90FB = 0
|
|
sta L_868C+1 ; 8213 patch the LDX operand at $868D: with the toggle OFF show persistent message id 0 (none)
|
|
ldx #$1E ; 8216 X = message slot $1E, the reusable 'toggle state' slot
|
|
lda #$D8 ; 8218 low byte of 'CUSTOM OFF' at $EFD8 (in the comm module's $EC00 tail)
|
|
ldy #$EF ; 821A high byte of $EFD8
|
|
jsr setMessageSlot ; 821C register $EFD8 as message $1E, then fall into runGameTypeMenu
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runGameTypeMenu - the GAME TYPE: selection screen. Aims the ON/OFF patch at the word inside 'CUSTOM
|
|
; OFF', derives the '*' toggle ($92F3 bit 7) from gameTypeOptions bit 6, redraws the screen, lists the
|
|
; seven scenario names and runs the selector. A new scenario takes its options byte from
|
|
; gameTypeOptionsByMenuItem ($78CA); re-choosing the same scenario under CUSTOM rules keeps the byte
|
|
; the player already tuned. With CUSTOM rules the two OFF/ON sub-menus 'UNIT MENUS' (bit 3) and
|
|
; 'DAMAGE' (bit 4) follow, then it falls into runMapTypeMenu.
|
|
; In: D_0BA3 gameTypeOptions, D_0BA5 isSoloTrainer, D_92F3, joystick/menu input
|
|
; Out: D_0BA3, D_92F3, D_91D4 = 7, D_91D5, zp_3D/zp_42 = 9, zp_B1 = $BF on the custom path;
|
|
; self-modifies $829E (previous game type), $8319 (chosen game type) and $869A/$869B
|
|
; Called from: fall-through from beginGameTypeSelection. The JMP $2E07 listed in XREF is the main
|
|
; program jumping to overlay variant B's routine at this address, not this code.
|
|
; Aim the '*' toggle machinery at the 'CUSTOM OFF' message and seed the toggle from the options byte.
|
|
; ----------------------------------------------------------------------
|
|
runGameTypeMenu:
|
|
lda #$DF ; 821F low byte of $EFDF - the 'OFF' inside 'CUSTOM OFF'
|
|
sta L_8699+1 ; 8221 patch the STA operand at $869A: showToggleStateMessage writes ON/OFF there
|
|
lda #$EF ; 8224 high byte of $EFDF
|
|
sta L_8699+2 ; 8226 patch $869B, the high half of that STA operand
|
|
lda gameTypeOptions ; 8229 gameTypeOptions $0BA3
|
|
and #$40 ; 822C bit 6 = CUSTOM rules
|
|
asl a ; 822E shift bit 6 up into bit 7
|
|
sta setupToggleFlag ; 822F setupToggleFlag $92F3 bit 7 = '*' toggle; here it means 'CUSTOM rules on'
|
|
jsr clearScreenAndRedrawGameScreen; 8232 grey the bitmap and redraw the whole game screen frame
|
|
jsr clearOverviewWindow ; 8235 blank the 20x20 character overview window on the left
|
|
lda #$09 ; 8238 text colour code 9 (brown)
|
|
sta textColour ; 823A colour byte written for every printed character cell
|
|
sta textPadColour ; 823C same colour for the blanks that pad a centred string
|
|
jsr resetSelectionAndView ; 823E no unit selected, viewport parked off the map so it stays blank
|
|
lda #$0C ; 8241 text cursor row 12
|
|
jsr clearInfoPanelSetRow ; 8243 clear the info panel in colour 8 and put the cursor on row 12
|
|
lda isSoloTrainer ; 8246 isSoloTrainer $0BA5, bit 7 = PRACTICE WITH SOLO TRAINER
|
|
bpl L_824E ; 8249 branch when this is a modem game
|
|
jsr toggleBattleDisplayMode ; 824B solo game: flip the beginner/standard battle display and queue the 'F 1 FOR ...' prompt (also trainerSetupEC00:storeHeadingOffset)
|
|
L_824E:
|
|
lda setupToggleFlag ; 824E the CUSTOM toggle just derived from $0BA3 bit 6
|
|
beq L_8256 ; 8251 branch when CUSTOM rules are off - nothing to announce
|
|
jsr showToggleStateMessage ; 8253 show 'CUSTOM ON' as the persistent status message
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Validate the stored game type (0-6), preselect it and print the seven scenario names.
|
|
; ----------------------------------------------------------------------
|
|
L_8256:
|
|
lda gameTypeOptions ; 8256 gameTypeOptions
|
|
and #$07 ; 8259 bits 0-2 = game type
|
|
cmp #$07 ; 825B only 0-6 exist (0 SCRIMAGE .. 6 DEFENDER)
|
|
bcc L_8264 ; 825D branch if the stored type is valid
|
|
lda #$00 ; 825F out of range: fall back to SCRIMAGE
|
|
sta gameTypeOptions ; 8261 and reset the whole options byte
|
|
L_8264:
|
|
sta L_829D+1 ; 8264 patch the CPY operand at $829E with the game type in force before the menu
|
|
tax ; 8267 X = preselected menu row = current game type
|
|
lda #$BD ; 8268 'GAME TYPE:' title at $CCBD
|
|
ldy #$CC ; 826A high byte of $CCBD
|
|
jsr printMenuTitle ; 826C print it centred, set the menu column and record the first item row
|
|
lda #$07 ; 826F seven scenarios
|
|
sta menuItemCount ; 8271 menuItemCount $91D4
|
|
sta scratch18 ; 8274 zp_18 also counts the print loop
|
|
L_8276:
|
|
jsr printStringAtPointer ; 8276 print the next scenario name; printStringAtPointer walks on to the following 8-byte entry
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printGameTypeNamesLoop - not a routine in variant A: this is the body of runGameTypeMenu's loop that
|
|
; prints the seven scenario names at $CCC8, one per line, counting down zp_18. The label exists only
|
|
; because overlay variant B has a real routine (advanceDroneCamera) at this address.
|
|
; In: zp_18 = names left, string pointer zp_A2/zp_A3 already inside the $CCC8 table
|
|
; Out: seven lines of text, zp_18 = 0
|
|
; Called from: nothing in variant A (fall-through / JMP $8280 inside runGameTypeMenu)
|
|
; ----------------------------------------------------------------------
|
|
printGameTypeNamesLoop:
|
|
dec scratch18 ; 8279 one name printed
|
|
beq L_8283 ; 827B after the seventh do not add another newline
|
|
jsr printNewline ; 827D next line
|
|
jmp L_8276 ; 8280 print the next name
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Run the selector, then turn the chosen row into a gameTypeOptions byte.
|
|
; ----------------------------------------------------------------------
|
|
L_8283:
|
|
jsr runMenuWidth10 ; 8283 run the menu selector with a 10-character highlight bar; A = chosen row
|
|
tay ; 8286 Y = chosen game type 0-6
|
|
sty L_8318+1 ; 8287 patch the LDY operand at $8319 so runMapTypeMenu knows which scenario was picked
|
|
lda #$00 ; 828A 0 - reset the row for the next menu
|
|
sta menuSelectedItem ; 828C reset menuSelectedItem for the next menu
|
|
lda gameTypeOptionsByMenuItem,y; 828F default options byte for this scenario (gameTypeOptionsByMenuItem $78CA)
|
|
bit setupToggleFlag ; 8292 test the CUSTOM toggle in $92F3
|
|
bmi L_829D ; 8295 branch when CUSTOM rules are on
|
|
sta gameTypeOptions ; 8297 STANDARD rules: take the preset unchanged
|
|
jmp L_82F7 ; 829A and skip the two custom sub-menus
|
|
|
|
L_829D:
|
|
cpy #$FF ; 829D compare the chosen row with the game type in force (operand patched at $8264)
|
|
bne applyCustomPresetTail ; 829F a different scenario -> use the table preset in A
|
|
lda gameTypeOptions ; 82A1 same scenario re-chosen -> keep the options byte the player already tuned (bits 3/4/7)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyCustomPresetTail - not a routine in variant A: the continuation inside runGameTypeMenu that ORs
|
|
; the CUSTOM bit ($40) into the scenario's options byte, stores it and runs the two OFF/ON sub-menus
|
|
; 'UNIT MENUS' (bit 3) and 'DAMAGE' (bit 4). Variant B has a real routine
|
|
; (readDroneHeadingFromJoystick) at this address.
|
|
; In: A = options byte to install
|
|
; Out: D_0BA3 with bit 6 set and bits 3/4 from the sub-menus, zp_B1 = $BF
|
|
; Called from: nothing in variant A (fall-through / BNE $829F)
|
|
; ----------------------------------------------------------------------
|
|
applyCustomPresetTail:
|
|
ora #$40 ; 82A4 bit 6 = CUSTOM rules
|
|
sta gameTypeOptions ; 82A6 gameTypeOptions $0BA3
|
|
lda #$BF ; 82A9 $BF - non-battle phase with bits 7 and 6 set for the setup menus
|
|
sta gamePhase ; 82AB gamePhase zp_B1
|
|
ldx #$08 ; 82AD X = mask $08 = UNIT MENUS
|
|
lda #$9B ; 82AF 'UNIT MENUS' label at $EF9B
|
|
ldy #$EF ; 82B1 high byte of $EF9B
|
|
jsr editOptionBitOnOff ; 82B3 OFF/ON menu for bit 3
|
|
D_82B6:
|
|
ldx #$10 ; 82B6 X = mask $10 = DAMAGE
|
|
D_82B8:
|
|
lda #$A7 ; 82B8 'DAMAGE' label at $EFA7
|
|
D_82BA:
|
|
ldy #$EF ; 82BA high byte of $EFA7
|
|
D_82BC:
|
|
jsr editOptionBitOnOff ; 82BC OFF/ON menu for bit 4, then fall into runMapTypeMenu
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMapTypeMenu - second half of the setup menus: the STANDARD / CUSTOM map choice (gameTypeOptions
|
|
; bit 7). Sends the finished options byte to the opponent (command $A4), loads the scenario's unit
|
|
; start-position template, and forces playsFirstFlag when the options changed since the last game. A
|
|
; CUSTOM map jumps to the 'TYPE MAP NAME' screen; a STANDARD map takes a fresh 24-bit map seed from
|
|
; the shared game RNG - with the low 5 bits forced to 1 (a name ending in '=' = point-symmetric map)
|
|
; for every scenario except QB SNEAK and DEFENDER, which get an asymmetric seed and only when this
|
|
; side has 1ST PLAY (otherwise the previous map is kept so the two sides can swap roles on it).
|
|
; In: D_0BA3, D_0BA4 prevGameTypeOptions, D_0BA1 playsFirstFlag, zp_57-zp_59 game RNG, menu input
|
|
; Out: D_0BA3 bit 7, D_0BA1, D_0B95-D_0B97 mapSeed, zp_B1 = $FF, zp_73, command $A4 queued
|
|
; Called from: fall-through from editOptionBitOnOff / runGameTypeMenu. The JSR $82BF at $3A30
|
|
; (resolveAttack) targets overlay variant B's drawShotLineInViewport, not this code.
|
|
; ----------------------------------------------------------------------
|
|
runMapTypeMenu:
|
|
jsr resetSelectionAndView ; 82BF hide the viewport and clear the selection state
|
|
lda #$0E ; 82C2 text cursor row 14
|
|
jsr clearInfoPanelSetRow ; 82C4 clear the info panel, cursor to row 14
|
|
lda #$77 ; 82C7 'MAP TYPE' title at $EF77
|
|
ldy #$EF ; 82C9 high byte of $EF77
|
|
ldx #$00 ; 82CB preselect row 0 = STANDARD
|
|
L_82CD:
|
|
bit gameTypeOptions ; 82CD bit 7 of gameTypeOptions = custom (named) map
|
|
bpl L_82D3 ; 82D0 branch when a random map is in force
|
|
inx ; 82D2 preselect row 1 = CUSTOM
|
|
L_82D3:
|
|
jsr printMenuTitle ; 82D3 print the title, set the menu column, record the first item row
|
|
lda #$02 ; 82D6 two items
|
|
sta menuItemCount ; 82D8 menuItemCount $91D4
|
|
lda #$11 ; 82DB 'STANDARD' at $CC11
|
|
ldy #$CC ; 82DD high byte of $CC11
|
|
jsr printString ; 82DF print item 0
|
|
jsr printNewline ; 82E2 next line
|
|
jsr printStringAtPointer ; 82E5 print item 1 - 'CUSTOM' follows immediately at $CC19
|
|
jsr runMenuWidth10 ; 82E8 run the selector, 10-character bar
|
|
lda gameTypeOptions ; 82EB current options byte
|
|
and #$7F ; 82EE clear bit 7 (map type)
|
|
ldy menuSelectedItem ; 82F0 0 = STANDARD, 1 = CUSTOM
|
|
beq L_82F7 ; 82F3 branch when STANDARD was chosen
|
|
ora #$80 ; 82F5 set bit 7 = custom (named) map
|
|
L_82F7:
|
|
sta gameTypeOptions ; 82F7 store the finished gameTypeOptions
|
|
jsr sendGameTypeCommand ; 82FA tell the opponent the whole options byte (command $A4)
|
|
jsr loadUnitStartTemplate ; 82FD copy this scenario's 400-byte unit start template into the live unit arrays (also trainerAiE000:L_ECF3, trainerSetupEC00:copyPlanRecordLoop)
|
|
lda #$FF ; 8300 $FF = menu / setup phase
|
|
sta gamePhase ; 8302 gamePhase zp_B1
|
|
lda gameTypeOptions ; 8304 the new options byte
|
|
cmp prevGameTypeOptions ; 8307 compare with the options of the previous game
|
|
beq L_8311 ; 830A branch when nothing changed - keep the alternating 1ST PLAY flag
|
|
lda #$01 ; 830C 1 = this side has 1ST PLAY
|
|
sta playsFirstFlag ; 830E a new scenario always gives this side 1ST PLAY
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Map seed. A CUSTOM map is typed in by the player; a STANDARD map is drawn from the shared 24-bit
|
|
; game RNG state zp_57-zp_59, so both machines end up with the same battlefield.
|
|
; ----------------------------------------------------------------------
|
|
L_8311:
|
|
ldy menuSelectedItem ; 8311 map type chosen above
|
|
bne L_8339 ; 8314 CUSTOM map -> let the player type the five-character map name
|
|
lda gameRngState0 ; 8316 byte 0 of the shared game RNG state
|
|
L_8318:
|
|
ldy #$FF ; 8318 Y = chosen game type (operand patched at $8287)
|
|
cpy #$06 ; 831A 6 = DEFENDER (asymmetric scenario)
|
|
beq L_8322 ; 831C DEFENDER - handle it with QB SNEAK
|
|
cpy #$01 ; 831E 1 = QB SNEAK (asymmetric scenario)
|
|
bne L_8329 ; 8320 any other scenario -> symmetric map
|
|
L_8322:
|
|
ldx playsFirstFlag ; 8322 asymmetric scenario: does this side have 1ST PLAY?
|
|
beq L_8338 ; 8325 no -> leave mapSeed alone, so the same map is replayed with the roles swapped
|
|
bne L_832B ; 8327 yes -> store the RNG state unchanged (asymmetric map)
|
|
L_8329:
|
|
ora #$1F ; 8329 force the low 5 bits, i.e. a map name ending in '=' - the generator then mirrors the map
|
|
L_832B:
|
|
sta mapSeed ; 832B mapSeed byte 0 $0B95
|
|
lda gameRngState1 ; 832E byte 1 of the RNG state
|
|
sta mapSeed1 ; 8330 mapSeed byte 1
|
|
lda gameRngState2 ; 8333 byte 2 of the RNG state
|
|
sta mapSeed2 ; 8335 mapSeed byte 2
|
|
L_8338:
|
|
rts ; 8338 done - the caller at $79A7 sends the seed to the opponent as command $8B/$A6
|
|
|
|
L_8339:
|
|
jmp mapNameEntryScreen ; 8339 custom map: 'TYPE MAP NAME' screen (which comes back through startGameSession)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendGameTypeCommand - queues the 2-byte command $A4 (cmdSetGameType, handler $559C) carrying the
|
|
; chosen gameTypeOptions byte, so the opponent's $0BA3 ends up identical.
|
|
; In: A = gameTypeOptions byte
|
|
; Out: zp_73 = A, command $A4 queued, D_929F incremented
|
|
; Called from: runMapTypeMenu $82FA
|
|
; ----------------------------------------------------------------------
|
|
sendGameTypeCommand:
|
|
sta cmdParam0 ; 833C cmdParam0 = the options byte
|
|
L_833E:
|
|
lda #$A4 ; 833E command $A4 = cmdSetGameType
|
|
jmp queueCmd2 ; 8340 queue command + 1 argument
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printMenuTitle - common menu prologue: preselects row X, prints the title string A/Y centred in the
|
|
; current window, then moves the text window to column $1C (28) and records the row the first item
|
|
; will be printed on as menuFirstRow.
|
|
; In: X = row to preselect, A/Y = bit-7 terminated title string
|
|
; Out: D_91D5 = X, D_91D6 = first item row, zp_3B = $1C, cursor pointers updated
|
|
; Called from: $826C (GAME TYPE:), $82D3 (MAP TYPE), $8588 (' SCENARIO'), $862A (OFF/ON menus)
|
|
; ----------------------------------------------------------------------
|
|
printMenuTitle:
|
|
stx menuSelectedItem ; 8343 menuSelectedItem = the preselected row
|
|
jsr printStringCentred ; 8346 print the title centred in the current window width
|
|
lda #$1C ; 8349 column 28 - left edge of the menu column
|
|
sta textWindowLeft ; 834B textWindowLeft zp_3B
|
|
jsr updateCursorPointers ; 834D recompute the bitmap/colour pointers for the new column
|
|
lda textCursorRow ; 8350 the row the cursor now sits on
|
|
sta menuFirstRow ; 8352 menuFirstRow $91D6 - the selector highlights menuFirstRow + menuSelectedItem
|
|
rts ; 8355 menu title and first row are set up
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMenuWidth10 - runs the generic selector with a 10-character highlight bar; falls into
|
|
; runMenuWithWidth.
|
|
; In: menu state D_91D4/D_91D5/D_91D6
|
|
; Out: A = D_91D5 = chosen row
|
|
; Called from: $8283 (game type), $82E8 (map type), $8599 (STANDARD/CUSTOM scenario)
|
|
; ----------------------------------------------------------------------
|
|
runMenuWidth10:
|
|
lda #$0A ; 8356 highlight bar 10 characters wide
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runMenuWithWidth - sets the highlight width, backs the window up one column so the bar has a leading
|
|
; blank, and runs the generic menu selector with C = 0 (no extra slow-down row before the last item).
|
|
; In: A = highlight width, D_91D4 item count, D_91D5 preselected row, D_91D6 first row
|
|
; Out: A = D_91D5 = chosen row, zp_3C = width, zp_3B decremented
|
|
; Called from: runMenuWidth10 (fall-through) and editOptionBitOnOff $8641
|
|
; ----------------------------------------------------------------------
|
|
runMenuWithWidth:
|
|
sta textWindowWidth ; 8358 textWindowWidth zp_3C is also the width of the highlight bar
|
|
dec textWindowLeft ; 835A start the bar one column left of the text
|
|
clc ; 835C C = 0 -> no slow-down row in the selector
|
|
jsr runMenuLoopWithFlag ; 835D joystick/fire driven selection loop
|
|
lda menuSelectedItem ; 8360 return the chosen row
|
|
rts ; 8363 A = the chosen row
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; resetSelectionAndView - blanks the tactical viewport for a full-screen menu: sets $9242 bit 7 (the
|
|
; overview then draws plain terrain instead of unit blips), clears the selected unit, forces the map
|
|
; screen and parks the view origin at (45,45), which is outside the 40x40 map.
|
|
; In: nothing
|
|
; Out: D_9242 = $FE, D_90F8 = $FE, D_90FB = 0, zp_A5 = zp_A6 = $2D
|
|
; Called from: $823E, $82BF, $8398, $8422, $854B, $8614
|
|
; ----------------------------------------------------------------------
|
|
resetSelectionAndView:
|
|
lda #$FE ; 8364 $FE - bit 7 set, low bits 'no unit'
|
|
sta comcenBattleStatus ; 8366 $9242 bit 7 makes the overview map draw the terrain under each unit
|
|
sta selectedUnit ; 8369 selectedUnit $90F8 = $FE = nothing selected
|
|
lda #$00 ; 836C 0 = map screen
|
|
sta currentScreen ; 836E currentScreen 0 = map screen
|
|
lda #$2D ; 8371 45 - past the last map row/column (0-39)
|
|
sta viewOriginCol ; 8373 view origin column
|
|
sta viewOriginRow ; 8375 view origin row: the 7x5 viewport now shows nothing
|
|
rts ; 8377 viewport is now blank
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; chooseRecycler - RECYCLER setup for the scenarios that have one (game types 4-6). Solo games force
|
|
; both sides to NONE. With CUSTOM options a FULL/HALF/NONE menu is shown, otherwise the scenario
|
|
; default stands and NONE returns at once. For FULL or HALF the player drives the target cursor over
|
|
; the map until he picks a cell whose terrain is blank ($40, looked up under a unit if one stands
|
|
; there); the result goes out as command $9C (mode, column, row), NONE as $9C with column/row = $FF.
|
|
; In: D_0BA5, D_0B9F playerSide, D_92A6 recyclerMode[side], D_8450 customSetupChosen, player input
|
|
; Out: D_92A6/D_92A7 (solo), D_91D5, zp_73-zp_75, command $9C queued, D_9022 = 0, zp_BB = $FF;
|
|
; self-modifies the LDA operand at $8433 with the chosen mode
|
|
; Called from: runGameStartOptions $85B6 (only for game types >= 4)
|
|
; ----------------------------------------------------------------------
|
|
chooseRecycler:
|
|
lda isSoloTrainer ; 8378 isSoloTrainer $0BA5
|
|
bpl L_8385 ; 837B modem game -> keep the per-side settings
|
|
lda #$02 ; 837D 2 = NONE
|
|
sta recyclerMode ; 837F side 0 recycler mode $92A6
|
|
sta side1RecyclerMode ; 8382 side 1 recycler mode $92A7 - the solo trainer never uses a recycler
|
|
L_8385:
|
|
ldx playerSide ; 8385 playerSide 0/1
|
|
ldy recyclerMode,x ; 8388 this side's recycler mode
|
|
sty menuSelectedItem ; 838B preselect it in the menu
|
|
lda customSetupChosen ; 838E customSetupChosen $8450 - nonzero when CUSTOM was picked at game start
|
|
bne L_8398 ; 8391 CUSTOM -> show the RECYCLER menu
|
|
cpy #$02 ; 8393 2 = NONE
|
|
bne L_83D1 ; 8395 a preset FULL or HALF still has to be placed on the map
|
|
rts ; 8397 NONE and no menu -> nothing to do at all
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The RECYCLER menu: title plus the three option words FULL / HALF / NONE from $0652.
|
|
; ----------------------------------------------------------------------
|
|
L_8398:
|
|
jsr resetSelectionAndView ; 8398 hide the viewport behind the menu
|
|
lda #$0E ; 839B text cursor row 14
|
|
jsr clearInfoPanelSetRow ; 839D clear the info panel, cursor to row 14
|
|
lda #$92 ; 83A0 'RECYCLER' at $EF92
|
|
ldy #$EF ; 83A2 high byte of $EF92
|
|
jsr printStringCentred ; 83A4 print it centred
|
|
lda #$1E ; 83A7 column 30
|
|
sta textWindowLeft ; 83A9 textWindowLeft zp_3B
|
|
jsr updateCursorPointers ; 83AB recompute the cursor pointers for the new column
|
|
lda textCursorRow ; 83AE row of the first item
|
|
sta menuFirstRow ; 83B0 menuFirstRow $91D6
|
|
lda #$03 ; 83B3 three items
|
|
sta menuItemCount ; 83B5 menuItemCount $91D4
|
|
lda #$00 ; 83B8 option word 0 = FULL
|
|
jsr printOptionWordLine ; 83BA print it and a newline
|
|
lda #$01 ; 83BD option word 1 = HALF
|
|
jsr printOptionWordLine ; 83BF print it and a newline
|
|
lda #$02 ; 83C2 option word 2 = NONE
|
|
jsr printOptionWordLine ; 83C4 print it and a newline
|
|
dec textWindowLeft ; 83C7 highlight bar starts one column left of the words
|
|
lda #$06 ; 83C9 bar 6 characters wide
|
|
sta textWindowWidth ; 83CB textWindowWidth zp_3C
|
|
clc ; 83CD C = 0 -> no slow-down row
|
|
jsr runMenuLoopWithFlag ; 83CE run the selector; Y comes back holding the chosen row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Turn the chosen mode into command $9C. NONE needs no map cell, FULL/HALF make the player pick one.
|
|
; ----------------------------------------------------------------------
|
|
L_83D1:
|
|
sty L_8432+1 ; 83D1 patch the LDA operand at $8433 - the mode becomes cmdParam0
|
|
cpy #$02 ; 83D4 2 = NONE
|
|
bne L_83E0 ; 83D6 FULL or HALF -> place it on the map
|
|
lda #$FF ; 83D8 $FF = no cell
|
|
sta cmdParam1 ; 83DA cmdParam1 = column
|
|
sta cmdParam2 ; 83DC cmdParam2 = row
|
|
bmi L_8432 ; 83DE always taken ($FF is negative) - jump straight to the send
|
|
L_83E0:
|
|
lda #$0F ; 83E0 text cursor row 15
|
|
jsr clearInfoPanelSetRow ; 83E2 clear the info panel, cursor to row 15
|
|
lda #$89 ; 83E5 'LOCATING' at $EF89
|
|
ldy #$EF ; 83E7 high byte of $EF89
|
|
L_83E9:
|
|
jsr printStringCentred ; 83E9 print 'LOCATING' centred
|
|
jsr printNextStringCentred ; 83EC and the string that follows it, 'RECYCLER' at $EF92
|
|
jsr setViewOriginToHome ; 83EF put the view origin over this side's home end of the map
|
|
jsr positionCursorSprite ; 83F2 show the cursor sprite over the centre cell
|
|
jsr drawViewport ; 83F5 draw the 7x5 cell tactical view
|
|
jsr drawOverviewMap ; 83F8 draw the whole-map overview
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Pick loop: the recycler may only stand on empty terrain, so a cell holding anything else beeps and
|
|
; the cursor comes back.
|
|
; ----------------------------------------------------------------------
|
|
L_83FB:
|
|
lda #$FF ; 83FB $FF = free cell pick (result reported, no command executed)
|
|
sta cursorTargetMode ; 83FD cursorTargetMode zp_BB
|
|
jsr runTargetCursorLoop ; 83FF drive the cursor until fire; the cell lands in $91D8/$91D9
|
|
ldx pickedCellCol ; 8402 chosen column
|
|
ldy pickedCellRow ; 8405 chosen row
|
|
jsr readMapCell ; 8408 A = the map byte of that cell
|
|
bpl L_8411 ; 840B bit 7 clear -> plain terrain, use it directly
|
|
tay ; 840D bit 7 set: Y = $80|unitIndex
|
|
L_840E:
|
|
lda unitTerrainUnderAlias,y ; 840E $FA70 + $80 + unit = unitTerrainUnderTable[unit], the terrain hidden by that unit
|
|
L_8411:
|
|
cmp #$40 ; 8411 $40 = blank terrain
|
|
beq L_841D ; 8413 blank -> accept the cell
|
|
lda #$03 ; 8415 sound 3 - cell rejected
|
|
jsr playSound ; 8417 play it
|
|
L_841A:
|
|
jmp L_83FB ; 841A ask for another cell
|
|
|
|
L_841D:
|
|
lda #$00 ; 841D 0 - no sprites
|
|
sta spriteEnableShadow ; 841F turn every sprite off again
|
|
jsr resetSelectionAndView ; 8422 park the view off the map
|
|
jsr clearViewportTextArea ; 8425 clear the text area under the viewport
|
|
lda pickedCellCol ; 8428 chosen column
|
|
sta cmdParam1 ; 842B cmdParam1
|
|
lda pickedCellRow ; 842D chosen row
|
|
sta cmdParam2 ; 8430 cmdParam2
|
|
L_8432:
|
|
lda #$FF ; 8432 operand patched at $83D1 with the recycler mode
|
|
sta cmdParam0 ; 8434 cmdParam0 = mode
|
|
L_8436:
|
|
lda #$9C ; 8436 command $9C = cmdSetRecycler
|
|
jmp queueCmd4 ; 8438 queue command + 3 arguments
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printOptionWordLine - prints option word A (0 FULL, 1 HALF, 2 NONE, from the 4-byte entries at
|
|
; $0652) followed by a newline.
|
|
; In: A = word index 0-2
|
|
; Out: one line of text at the cursor
|
|
; Called from: chooseRecycler $83BA, $83BF, $83C4
|
|
; ----------------------------------------------------------------------
|
|
printOptionWordLine:
|
|
jsr printOptionWord ; 843B print the 4-character word from $0652 + A*4
|
|
jmp printNewline ; 843E and end the line
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearInfoPanelSetRow - sets the text and pad colours to 8 (orange), blanks the info panel window and
|
|
; puts the cursor on row A. Used before every setup panel in this overlay.
|
|
; In: A = text cursor row
|
|
; Out: zp_3D = zp_42 = 8, panel cleared, zp_40 = A, cursor pointers updated
|
|
; Called from: $8243, $82C4, $839D, $83E2, $8572, $85E6, $8619, $8758
|
|
; ----------------------------------------------------------------------
|
|
clearInfoPanelSetRow:
|
|
pha ; 8441 remember the wanted row across the clear
|
|
lda #$08 ; 8442 colour code 8 (orange)
|
|
sta textColour ; 8444 text colour zp_3D
|
|
sta textPadColour ; 8446 pad colour zp_42
|
|
jsr clearInfoPanel ; 8448 set the info-panel text window and blank it
|
|
pla ; 844B the wanted row again
|
|
L_844C:
|
|
jmp setCursorRow ; 844C position the cursor on it
|
|
|
|
|
|
; Two one-byte variables of the value editors, sitting between the routines that use them.
|
|
editValue:
|
|
.byte $00 ; 844F . current value of the joystick value editor (limits and meaning depend on the caller)
|
|
|
|
; customSetupChosen: byteTable, 1 bytes. nonzero = CUSTOM options chosen at game start
|
|
customSetupChosen:
|
|
.byte $00 ; 8450 . 0 = STANDARD scenario options, 1 = CUSTOM chosen on the game-start screen ($40 briefly at $8564)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editValueWithJoystick - the generic numeric option editor of the custom setup. It repeatedly prints
|
|
; editValue * Y in decimal at column 1 of the one-row value window, keeps the link and the blinking
|
|
; highlight alive, and applies joystick up as +1 and down as -1, rejecting anything that is not below
|
|
; the limit (a decrement below 0 wraps to $FF and is rejected the same way). Returns when the fire
|
|
; button is clicked.
|
|
; In: A = exclusive upper limit, Y = display multiplier, D_844F = starting value, window set up by
|
|
; beginValueEdit, zp_5D joystick, zp_6C repeat timer, zp_6E blink timer
|
|
; Out: D_844F = edited value, D_92C5 = 1, number printed, sound 2 on every change;
|
|
; self-modifies the CMP operand at $8493 (limit) and the LDY operand at $846A (multiplier)
|
|
; Called from: applyHandicapPoints $84DB, editTerrainPoints $8500, editDronesAndMissiles $851F/$8536,
|
|
; editComcenSpeed $85CA
|
|
; ----------------------------------------------------------------------
|
|
editValueWithJoystick:
|
|
sta L_8492+1 ; 8451 patch the CMP operand at $8493 with the exclusive upper limit
|
|
sty L_8469+1 ; 8454 patch the LDY operand at $846A with the display multiplier
|
|
lda #$01 ; 8457 1 = un-highlighted
|
|
sta menuBlinkState ; 8459 menuBlinkState 1 = the row is currently un-highlighted
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Redraw pass: un-highlight the row, print value * multiplier, then wait in the input loop.
|
|
; ----------------------------------------------------------------------
|
|
L_845C:
|
|
jsr unhighlightMenuRow ; 845C take the highlight off so the number can be redrawn
|
|
lda #$01 ; 845F column 1 of the value window (window left is 29, so absolute column 30)
|
|
L_8461:
|
|
sta textCursorCol ; 8461 textCursorCol zp_3F
|
|
jsr updateCursorPointers ; 8463 recompute the cursor pointers
|
|
lda editValue ; 8466 current value
|
|
L_8469:
|
|
ldy #$FF ; 8469 multiplier (operand patched at $8454)
|
|
jsr multiply8x8 ; 846B 16-bit multiply: A = product low, Y = product high
|
|
jsr printDecimal ; 846E print it as decimal without leading zeros
|
|
jsr bootToggleDataAck ; 8471 blank the rest of the line so a shorter number leaves no digits behind (also textEngineC000:clearToEndOfLine)
|
|
L_8474:
|
|
jsr serviceCommandExchange ; 8474 keep the lock-step command exchange running while the player thinks
|
|
lda menuBlinkTimer ; 8477 frames left before the next highlight flip
|
|
bne L_847E ; 8479 not yet
|
|
jsr toggleMenuRowHighlight ; 847B flip the highlight of the value row
|
|
L_847E:
|
|
lda joyRepeatTimer ; 847E joystick auto-repeat delay still running?
|
|
bne L_84A4 ; 8480 yes -> only look at the fire button
|
|
lda joyDirection ; 8482 active-low joystick nibble (bit 0 up, bit 1 down, bit 2 left, bit 3 right)
|
|
ora #$0C ; 8484 force the left/right bits high so they are ignored
|
|
cmp #$0F ; 8486 $0F = neither up nor down
|
|
beq L_84A4 ; 8488 stick centred (or only sideways) -> no change
|
|
asl a ; 848A up -> $1C, down -> $1A
|
|
sec ; 848B borrow into the subtraction
|
|
sbc #$1B ; 848C $1C-$1B = +1 (up), $1A-$1B = -1 (down)
|
|
clc ; 848E no carry into the add
|
|
adc editValue ; 848F apply the step to the current value
|
|
L_8492:
|
|
cmp #$FF ; 8492 limit (operand patched at $8451); a decrement below 0 wraps to $FF and fails here too
|
|
bcs L_849E ; 8494 out of range -> leave the value alone
|
|
sta editValue ; 8496 store the new value
|
|
lda #$02 ; 8499 sound 2 - the value changed
|
|
jsr playSound ; 849B play it
|
|
L_849E:
|
|
jsr setJoystickRepeatDelay ; 849E 16 frames before the first repeat, 6 between repeats
|
|
jmp L_845C ; 84A1 redraw the number
|
|
|
|
L_84A4:
|
|
jsr checkFirePressed ; 84A4 new fire click?
|
|
bne L_8474 ; 84A7 no -> keep servicing the link and blinking
|
|
rts ; 84A9 value confirmed
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; applyHandicapPoints - last step of the game-start options. Works out the default handicap of the
|
|
; two asymmetric scenarios (QB SNEAK gives side 0 a 2, DEFENDER gives side 1 a $14) and, under CUSTOM
|
|
; rules, lets the player edit it (0..50, displayed multiplied by 10). A nonzero result is sent as
|
|
; command $A5 kind 0, which cmdSetupOption turns into value*10 points for the sender's side; zero is
|
|
; not sent.
|
|
; In: D_0BA3 bits 0-2 game type, D_0B9F playerSide, D_92F3 bit 7 CUSTOM toggle
|
|
; Out: zp_74 = 0, zp_75 = value, command $A5 queued (only when nonzero); info panel used
|
|
; Called from: JMP from runGameStartOptions $85B9
|
|
; ----------------------------------------------------------------------
|
|
applyHandicapPoints:
|
|
ldx #$00 ; 84AA default handicap 0
|
|
lda gameTypeOptions ; 84AC gameTypeOptions
|
|
and #$07 ; 84AF bits 0-2 = game type
|
|
cmp #$01 ; 84B1 1 = QB SNEAK
|
|
bne L_84BC ; 84B3 not QB SNEAK
|
|
lda playerSide ; 84B5 playerSide
|
|
bne L_84C7 ; 84B8 only side 0 (the side on offence) gets the QB SNEAK handicap
|
|
ldx #$02 ; 84BA 2 -> 20 points
|
|
L_84BC:
|
|
cmp #$06 ; 84BC 6 = DEFENDER (A is the game type, or playerSide 0 after the branch above)
|
|
bne L_84C7 ; 84BE not DEFENDER
|
|
L_84C0:
|
|
lda playerSide ; 84C0 playerSide
|
|
beq L_84C7 ; 84C3 only side 1 gets the DEFENDER handicap
|
|
ldx #$14 ; 84C5 $14 = 20 -> 200 points
|
|
L_84C7:
|
|
bit setupToggleFlag ; 84C7 CUSTOM toggle $92F3 bit 7
|
|
bmi L_84D0 ; 84CA custom rules -> let the player edit the number
|
|
txa ; 84CC A = the scenario default
|
|
L_84CD:
|
|
bne L_84E1 ; 84CD zero -> send nothing at all
|
|
rts ; 84CF no handicap in this scenario for this side
|
|
|
|
L_84D0:
|
|
lda #$52 ; 84D0 'HANDICAP' at $EF52
|
|
ldy #$EF ; 84D2 high byte of $EF52
|
|
jsr beginValueEdit ; 84D4 X still holds the default, which becomes the starting value
|
|
lda #$33 ; 84D7 limit $33 = 51, so 0..50
|
|
ldy #$0A ; 84D9 displayed multiplied by 10 (0..500 points)
|
|
L_84DB:
|
|
jsr editValueWithJoystick ; 84DB edit it with the joystick
|
|
L_84DE:
|
|
lda editValue ; 84DE the edited value
|
|
L_84E1:
|
|
sta cmdParam2 ; 84E1 cmdParam2 = the handicap
|
|
lda #$00 ; 84E3 kind 0 = handicap points
|
|
sta cmdParam1 ; 84E5 cmdParam1
|
|
lda #$A5 ; 84E7 command $A5 = cmdSetupOption
|
|
jmp queueCmd3 ; 84E9 queue command + 2 arguments
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editTerrainPoints - the CUSTOM option 'TERRAIN PTS' (0..2). $92AC is indexed with the complement of
|
|
; the side, so the entry this routine edits (index playerSide EOR 1) is exactly the one $47A9 uses
|
|
; when one of this player's own units reaches enemy ground. Sent as command $A5 kind 1, which
|
|
; cmdSetupOption stores at the same complemented index on both machines.
|
|
; In: D_0B9F playerSide, D_92AC
|
|
; Out: zp_74 = 1, zp_75 = value, command $A5 queued
|
|
; Called from: runGameStartOptions $85A7
|
|
; ----------------------------------------------------------------------
|
|
editTerrainPoints:
|
|
lda playerSide ; 84EC playerSide
|
|
eor #$01 ; 84EF $92AC is indexed 1-side, so this picks this player's own entry
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editTerrainPointsForSide - entry into editTerrainPoints with the $92AC index already in A. Nothing
|
|
; in variant A reaches it (the XREF caller $83DE belongs to overlay variant B, where $84F1 is
|
|
; plotDiamondMarker).
|
|
; In: A = $92AC index (0 or 1)
|
|
; Out: as editTerrainPoints
|
|
; Called from: fall-through only in variant A
|
|
; ----------------------------------------------------------------------
|
|
editTerrainPointsForSide:
|
|
tay ; 84F1 Y = the table index
|
|
ldx terrainPointsBySide,y ; 84F2 current TERRAIN PTS setting = starting value
|
|
lda #$5A ; 84F5 'TERRAIN PTS' at $EF5A
|
|
ldy #$EF ; 84F7 high byte of $EF5A
|
|
jsr beginValueEdit ; 84F9 set up the one-row value window
|
|
lda #$03 ; 84FC limit 3, so 0..2
|
|
ldy #$01 ; 84FE displayed as it is
|
|
jsr editValueWithJoystick ; 8500 edit it with the joystick
|
|
lda editValue ; 8503 the edited value
|
|
sta cmdParam2 ; 8506 cmdParam2
|
|
lda #$01 ; 8508 kind 1 = terrain points
|
|
sta cmdParam1 ; 850A cmdParam1
|
|
lda #$A5 ; 850C command $A5 = cmdSetupOption
|
|
jmp queueCmd3 ; 850E queue command + 2 arguments
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editDronesAndMissiles - the CUSTOM options 'DRONES' (0..9) and 'MISSILES' (0..36), edited one after
|
|
; the other and sent together as command $90; during the setup phase cmdMissileStrike simply stores
|
|
; the two numbers as the sender's dronesLeft/missilesLeft.
|
|
; In: D_92AE dronesLeft[own], D_92B0 missilesLeft[own]
|
|
; Out: zp_74 = drones, zp_75 = missiles, command $90 queued; self-modifies the LDA operand at $853A
|
|
; Called from: runGameStartOptions $85AA
|
|
; ----------------------------------------------------------------------
|
|
editDronesAndMissiles:
|
|
ldx dronesLeft ; 8511 own drone count = starting value
|
|
lda #$65 ; 8514 'DRONES' at $EF65
|
|
ldy #$EF ; 8516 high byte of $EF65
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editDronesAndMissilesBody - not a real entry point in variant A: it is the JSR beginValueEdit inside
|
|
; editDronesAndMissiles. The label only exists because the main program's JSR $8518 at $1460/$14D3
|
|
; targets overlay variant B's drawStatsTab.
|
|
; In: A/Y = label string, X = starting value (already loaded by editDronesAndMissiles)
|
|
; Out: as editDronesAndMissiles
|
|
; Called from: fall-through only in variant A
|
|
; ----------------------------------------------------------------------
|
|
editDronesAndMissilesBody:
|
|
jsr beginValueEdit ; 8518 set up the one-row value window for DRONES
|
|
lda #$0A ; 851B limit $0A, so 0..9
|
|
ldy #$01 ; 851D displayed as it is
|
|
jsr editValueWithJoystick ; 851F edit it
|
|
lda editValue ; 8522 the drone count
|
|
sta L_8539+1 ; 8525 patch the LDA operand at $853A so it survives the second edit
|
|
ldx missilesLeft ; 8528 own missile count = starting value
|
|
lda #$9E ; 852B 'MISSILES' at $CC9E
|
|
L_852D:
|
|
ldy #$CC ; 852D high byte of $CC9E
|
|
jsr beginValueEdit ; 852F set up the value window again
|
|
lda #$25 ; 8532 limit $25 = 37, so 0..36
|
|
ldy #$01 ; 8534 displayed as it is
|
|
L_8536:
|
|
jsr editValueWithJoystick ; 8536 edit it
|
|
L_8539:
|
|
lda #$FF ; 8539 drone count (operand patched at $8525)
|
|
sta cmdParam1 ; 853B cmdParam1 = drones
|
|
lda editValue ; 853D missile count
|
|
sta cmdParam2 ; 8540 cmdParam2 = missiles
|
|
lda #$90 ; 8542 command $90 = cmdMissileStrike (a setup-phase store here, a missile launch during battle)
|
|
jmp queueCmd3 ; 8544 queue command + 2 arguments
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runGameStartOptions - the screen shown once per game after the setup has been saved and before unit
|
|
; placement. It draws the 'STANDARD/CUSTOM <scenario> SCENARIO' banner and waits for a click, then,
|
|
; if the scenario carries CUSTOM rules ($0BA3 bit 6), offers a STANDARD/CUSTOM choice; picking CUSTOM
|
|
; runs the COMCEN SPEED, TERRAIN PTS, DRONES and MISSILES editors. Scenarios 4-6 then place the
|
|
; recycler, and it finishes in applyHandicapPoints.
|
|
; In: D_0BA3 gameTypeOptions, D_0B9F playerSide, menu/text engine state
|
|
; Out: zp_B1 = $7F, D_8450 customSetupChosen, D_92F3 bit 7, setup commands $A5/$90/$9C queued
|
|
; Called from: startGameSession $7AF7
|
|
; ----------------------------------------------------------------------
|
|
runGameStartOptions:
|
|
lda #$7F ; 8547 $7F = setup phase with drawing enabled
|
|
sta gamePhase ; 8549 gamePhase zp_B1
|
|
jsr resetSelectionAndView ; 854B park the viewport off the map
|
|
L_854E:
|
|
lda playerSide ; 854E playerSide
|
|
bne L_8559 ; 8551 side 1 needs the whole screen redrawn
|
|
jsr drawOverviewMap ; 8553 side 0 only refreshes the overview map
|
|
jmp L_855C ; 8556 skip the side-1 redraw
|
|
|
|
L_8559:
|
|
jsr clearScreenAndRedrawGameScreen; 8559 grey the bitmap and redraw the game screen frame
|
|
L_855C:
|
|
jsr showScenarioBannerAndWaitForButton; 855C show the scenario banner and wait for the fire button
|
|
lda gameTypeOptions ; 855F gameTypeOptions
|
|
and #$40 ; 8562 bit 6 = CUSTOM rules
|
|
sta customSetupChosen ; 8564 $40 or 0 (overwritten with 0/1 by the menu below)
|
|
asl a ; 8567 shift bit 6 up into bit 7
|
|
L_8568:
|
|
sta setupToggleFlag ; 8568 setupToggleFlag $92F3 bit 7
|
|
beq L_85AD ; 856B STANDARD rules -> skip the whole custom block
|
|
jsr showCustomSummaryMessage; 856D put 'CUSTOM: ... MENUS x, DAMAGE y.' on the status line
|
|
lda #$0D ; 8570 text cursor row 13
|
|
jsr clearInfoPanelSetRow ; 8572 clear the info panel, cursor to row 13
|
|
lda gameTypeOptions ; 8575 gameTypeOptions
|
|
and #$07 ; 8578 bits 0-2 = game type
|
|
tax ; 857A X = scenario index
|
|
lda #$C8 ; 857B scenario name table at $CCC8 (8-byte entries)
|
|
ldy #$CC ; 857D high byte of $CCC8
|
|
jsr printCentredTableEntry ; 857F print the scenario name centred in 13 columns
|
|
lda #$04 ; 8582 ' SCENARIO' at $CC04
|
|
ldy #$CC ; 8584 high byte of $CC04
|
|
ldx #$00 ; 8586 preselect row 0 = STANDARD
|
|
jsr printMenuTitle ; 8588 print it as the menu title
|
|
lda #$02 ; 858B two items
|
|
sta menuItemCount ; 858D menuItemCount $91D4
|
|
jsr printStringAtPointer ; 8590 'STANDARD' follows at $CC11
|
|
jsr printNewline ; 8593 next line
|
|
jsr printStringAtPointer ; 8596 'CUSTOM' follows at $CC19
|
|
jsr runMenuWidth10 ; 8599 run the selector
|
|
sta customSetupChosen ; 859C 0 = STANDARD, 1 = CUSTOM
|
|
lda customSetupChosen ; 859F redundant reload - A already holds the choice
|
|
beq L_85AD ; 85A2 STANDARD -> keep the scenario defaults
|
|
jsr editComcenSpeed ; 85A4 COMCEN SPEED
|
|
jsr editTerrainPoints ; 85A7 TERRAIN PTS
|
|
jsr editDronesAndMissiles ; 85AA DRONES and MISSILES
|
|
L_85AD:
|
|
lda gameTypeOptions ; 85AD gameTypeOptions
|
|
and #$07 ; 85B0 bits 0-2 = game type
|
|
cmp #$04 ; 85B2 types 4-6 are SLUGGERS, FULL WAR and DEFENDER
|
|
bcc L_85B9 ; 85B4 the other scenarios have no recycler
|
|
jsr chooseRecycler ; 85B6 choose and place the recycler
|
|
L_85B9:
|
|
jmp applyHandicapPoints ; 85B9 finish with the handicap
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editComcenSpeed - the CUSTOM option 'COMCEN SPEED' (0..3). Sent as command $A5 kind 3, which
|
|
; cmdSetupOption stores in $92B4 of the sender; at $7B6E the setting is mapped through the table at
|
|
; $78BF into the comcen's actual step rate, and $34D5 refuses to move a comcen whose class is 4.
|
|
; In: D_92B4 comcenSpeedClass[own]
|
|
; Out: zp_74 = 3, zp_75 = value, command $A5 queued
|
|
; Called from: runGameStartOptions $85A4
|
|
; ----------------------------------------------------------------------
|
|
editComcenSpeed:
|
|
ldx comcenSpeedClass ; 85BC own comcen speed class = starting value
|
|
lda #$6B ; 85BF 'COMCEN SPEED' at $EF6B
|
|
ldy #$EF ; 85C1 high byte of $EF6B
|
|
jsr beginValueEdit ; 85C3 set up the one-row value window
|
|
lda #$04 ; 85C6 limit 4, so 0..3
|
|
ldy #$01 ; 85C8 displayed as it is
|
|
jsr editValueWithJoystick ; 85CA edit it with the joystick
|
|
lda editValue ; 85CD the edited value
|
|
sta cmdParam2 ; 85D0 cmdParam2
|
|
lda #$03 ; 85D2 kind 3 = comcen speed
|
|
sta cmdParam1 ; 85D4 cmdParam1
|
|
lda #$A5 ; 85D6 command $A5 = cmdSetupOption
|
|
jmp queueCmd3 ; 85D8 queue command + 2 arguments
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; beginValueEdit - common prologue of all four value editors: remembers the label string in two
|
|
; self-modified operands, seeds editValue, clears the info panel to row 15, prints the label centred,
|
|
; then moves the text window to column $1D (29) and makes the next line the single menu row. Falls
|
|
; into setValueEditWindow.
|
|
; In: A/Y = bit-7 terminated label, X = starting value
|
|
; Out: D_844F = X, D_91D5 = 0, D_91D6 = the value row, D_90F8 = $FE, zp_3B = $1D, zp_3C = 5;
|
|
; self-modifies $85EA/$85EC with the label pointer
|
|
; Called from: $84D4 (HANDICAP), $84F9 (TERRAIN PTS), $8518 (DRONES), $852F (MISSILES), $85C3
|
|
; (COMCEN SPEED)
|
|
; ----------------------------------------------------------------------
|
|
beginValueEdit:
|
|
sta L_85E9+1 ; 85DB patch the LDA operand at $85EA with the label's low byte
|
|
sty L_85EB+1 ; 85DE patch the LDY operand at $85EC with its high byte
|
|
stx editValue ; 85E1 editValue = the starting value
|
|
lda #$0F ; 85E4 text cursor row 15
|
|
jsr clearInfoPanelSetRow ; 85E6 clear the info panel, cursor to row 15
|
|
L_85E9:
|
|
lda #$FF ; 85E9 label low byte (patched at $85DB)
|
|
L_85EB:
|
|
ldy #$FF ; 85EB label high byte (patched at $85DE)
|
|
jsr printStringCentred ; 85ED print the label centred
|
|
lda #$1D ; 85F0 column 29 - where the number goes
|
|
sta textWindowLeft ; 85F2 textWindowLeft zp_3B
|
|
jsr printNewline ; 85F4 the value goes on the next line
|
|
L_85F7:
|
|
lda textCursorRow ; 85F7 that row
|
|
sta menuFirstRow ; 85F9 menuFirstRow $91D6 - the single row the editor blinks
|
|
lda #$00 ; 85FC 0 = the only row
|
|
sta menuSelectedItem ; 85FE menuSelectedItem 0 - a one-row menu
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setValueEditWindow - tail of beginValueEdit: marks the menu as a plain one (no unit attached) and
|
|
; sets the highlight bar to 5 characters. The separate label exists only because $8601 is a real
|
|
; routine (printNumberRightAligned) in overlay variant B; in A it is reached by fall-through alone.
|
|
; In: nothing
|
|
; Out: D_90F8 = $FE, zp_3C = 5
|
|
; Called from: fall-through only in variant A
|
|
; ----------------------------------------------------------------------
|
|
setValueEditWindow:
|
|
lda #$FE ; 8601 $FE = plain menu, no unit behind it (keeps the REPAIR-menu special case out of the blinker)
|
|
sta selectedUnit ; 8603 selectedUnit $90F8
|
|
lda #$05 ; 8606 highlight bar 5 characters wide
|
|
sta textWindowWidth ; 8608 textWindowWidth zp_3C
|
|
rts ; 860A the value window is ready
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; editOptionBitOnOff - a two-row OFF/ON menu for one bit of gameTypeOptions, used by the game-type
|
|
; screen for 'UNIT MENUS' (mask $08) and 'DAMAGE' (mask $10). The label pointer and the mask go into
|
|
; self-modified operands, the current state of the bit preselects the row, and the chosen row writes
|
|
; the bit back into $0BA3.
|
|
; In: A/Y = label string, X = bit mask, D_0BA3
|
|
; Out: D_0BA3 with that bit updated, D_91D4 = 2, D_91D5 = chosen row;
|
|
; self-modifies $8622 (mask), $8627/$8629 (label pointer)
|
|
; Called from: runGameTypeMenu $82B3 (UNIT MENUS) and $82BC (DAMAGE)
|
|
; ----------------------------------------------------------------------
|
|
editOptionBitOnOff:
|
|
sta L_8626+1 ; 860B patch the LDA operand at $8627 with the label's low byte
|
|
sty L_8628+1 ; 860E patch the LDY operand at $8629 with its high byte
|
|
stx L_8621+1 ; 8611 patch the AND operand at $8622 with the bit mask
|
|
jsr resetSelectionAndView ; 8614 park the viewport off the map
|
|
lda #$0E ; 8617 text cursor row 14
|
|
jsr clearInfoPanelSetRow ; 8619 clear the info panel, cursor to row 14
|
|
ldx #$00 ; 861C preselect row 0 = OFF
|
|
lda gameTypeOptions ; 861E gameTypeOptions
|
|
L_8621:
|
|
and #$FF ; 8621 mask (operand patched at $8611)
|
|
beq L_8626 ; 8623 bit clear -> leave OFF preselected
|
|
inx ; 8625 bit set -> preselect row 1 = ON
|
|
L_8626:
|
|
lda #$FF ; 8626 label low byte (patched at $860B)
|
|
L_8628:
|
|
ldy #$FF ; 8628 label high byte (patched at $860E)
|
|
jsr printMenuTitle ; 862A print the label as the menu title
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runOnOffMenuAndApplyBit - tail of editOptionBitOnOff that sets the item count and falls into the
|
|
; printing part. Only a real entry point in overlay variant B (setTextColumn).
|
|
; In: menu title already printed, mask in the operand at $8622
|
|
; Out: D_0BA3 updated
|
|
; Called from: fall-through only in variant A
|
|
; ----------------------------------------------------------------------
|
|
runOnOffMenuAndApplyBit:
|
|
lda #$02 ; 862D two items
|
|
sta menuItemCount ; 862F menuItemCount $91D4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printOnOffMenuAndApplyBit - inner tail of editOptionBitOnOff: prints the words OFF ($064D) and ON
|
|
; ($0650), runs the 5-wide selector and clears or sets the mask bit in gameTypeOptions. The main
|
|
; program's JSR $8632 at $176B targets overlay variant B's drawComcenSchematic, not this code.
|
|
; In: menu state, mask in the operand at $8622, D_0BA3
|
|
; Out: D_0BA3 bit cleared or set, D_91D5 = chosen row
|
|
; Called from: fall-through only in variant A
|
|
; ----------------------------------------------------------------------
|
|
printOnOffMenuAndApplyBit:
|
|
lda #$4D ; 8632 'OFF' at $064D
|
|
ldy #$06 ; 8634 high byte of $064D
|
|
jsr printString ; 8636 print row 0
|
|
jsr printNewline ; 8639 next line
|
|
jsr printStringAtPointer ; 863C 'ON' follows at $0650
|
|
lda #$05 ; 863F highlight bar 5 characters wide
|
|
jsr runMenuWithWidth ; 8641 run the selector
|
|
tay ; 8644 Y = 0 OFF, 1 ON
|
|
lda #$FF ; 8645 $FF - build the inverse of the mask
|
|
eor L_8621+1 ; 8647 invert the mask (operand at $8622)
|
|
and gameTypeOptions ; 864A clear that bit in gameTypeOptions
|
|
cpy #$00 ; 864D was ON chosen?
|
|
beq L_8654 ; 864F no - leave the bit clear
|
|
ora L_8621+1 ; 8651 yes - set it again
|
|
L_8654:
|
|
sta gameTypeOptions ; 8654 store the updated options byte
|
|
rts ; 8657 the option bit is updated
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleStarKeyToggle - per-frame hook the main program calls while overlay A is resident and the game
|
|
; is not in the battle phase. A '*' keypress ($AA) that is not part of a chat line is consumed and
|
|
; flips bit 7 of setupToggleFlag; the routine then falls into showToggleStateMessage and returns C =
|
|
; 1. Any other key returns C = 0. The flag means CUSTOM rules on the scenario setup screen and
|
|
; DESTROY mode during unit placement (where clicking one of your own units sends $91 self destruct at
|
|
; $209F).
|
|
; In: D_929C chatState, D_9248 lastTypedKey, D_92F3 setupToggleFlag
|
|
; Out: C = 1 with D_92F3 bit 7 flipped and D_9248 = $FF when '*' was pressed, otherwise C = 0
|
|
; Called from: main program trampoline $25C4 (JMP, battlefield loop when zp_B1 != 0) and runMenuLoop
|
|
; $6002 (JSR, when zp_B1 == $FF)
|
|
; ----------------------------------------------------------------------
|
|
handleStarKeyToggle:
|
|
lda chatState ; 8658 chatState $929C, bit 7 set while a chat line is being typed
|
|
bmi L_8664 ; 865B typing -> the '*' belongs to the text
|
|
lda lastTypedKey ; 865D last typed key (PETSCII with bit 7 set)
|
|
cmp #$AA ; 8660 $AA = '*'
|
|
beq L_8666 ; 8662 it is the toggle key
|
|
L_8664:
|
|
clc ; 8664 C = 0: key not handled here
|
|
rts ; 8665 C = 0, key not handled
|
|
|
|
L_8666:
|
|
lda #$FF ; 8666 $FF = no key pending
|
|
sta lastTypedKey ; 8668 consume the '*' so nothing else sees it
|
|
lda setupToggleFlag ; 866B setupToggleFlag
|
|
eor #$80 ; 866E flip bit 7
|
|
sta setupToggleFlag ; 8670 store it back and fall into showToggleStateMessage
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showToggleStateMessage - shows the state of the '*' toggle. It empties the message queue, patches
|
|
; the three characters 'ON'+terminator or 'OFF' into the message text at the address held in the
|
|
; operand at
|
|
; $869A/$869B ($EFDF inside 'CUSTOM OFF', or $EFEA inside 'DESTROY OFF'), makes $1E the persistent
|
|
; message while the mode is on (and the id in the operand at $868D - 0 on the setup screen, $0A =
|
|
; 'SETUP UNITS. PRESS F 1 WHEN DONE.' during placement - while it is off) and queues message $1E.
|
|
; In: D_92F3 bit 7; operands $868D and $869A/$869B set by $820B/$821F or setDestroyModeMessage
|
|
; Out: D_9252 = D_9251 (queue emptied), D_92A1, zp_48/zp_49, zp_72 = 0, message $1E queued, C = 1
|
|
; Called from: fall-through from handleStarKeyToggle, and directly from runGameTypeMenu $8253
|
|
; ----------------------------------------------------------------------
|
|
showToggleStateMessage:
|
|
lda messageQueueHead ; 8673 message queue write index
|
|
sta messageQueueTail ; 8676 read index = write index -> the queue is empty
|
|
lda #$00 ; 8679 0 - show the next message immediately
|
|
sta messageTimer ; 867B messageTimer 0 so the new message appears at once
|
|
lda #$50 ; 867D 'ON' at $0650
|
|
ldy #$06 ; 867F high byte of $0650
|
|
ldx #$1E ; 8681 persistent message id $1E while the mode is on
|
|
bit setupToggleFlag ; 8683 setupToggleFlag bit 7
|
|
bmi L_868E ; 8686 toggle on -> keep 'ON' and id $1E
|
|
lda #$4D ; 8688 'OFF' at $064D
|
|
ldy #$06 ; 868A high byte of $064D
|
|
L_868C:
|
|
ldx #$FF ; 868C off-state persistent message id (operand patched at $8213 = 0, or at $86AC = $0A)
|
|
L_868E:
|
|
stx persistentMessageId ; 868E persistentMessageId $92A1
|
|
sta mapCellPtr ; 8691 zp_48 = word pointer low
|
|
sty mapCellPtrHi ; 8693 zp_49 = word pointer high
|
|
ldy #$02 ; 8695 copy three bytes (for 'ON' that is 'O', 'N'|$80 and the stray 'F' of the next word)
|
|
L_8697:
|
|
lda (mapCellPtr),y ; 8697 read the word
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The STA operand below is a variable: $820B/$821F point it at $EFDF ('CUSTOM OFF') and
|
|
; setDestroyModeMessage points it at $EFEA ('DESTROY OFF'). The $FFFF assembled here is just the byte
|
|
; pattern that happens to be on disk.
|
|
; ----------------------------------------------------------------------
|
|
L_8699:
|
|
sta irqVectorHi,y ; 8699 destination patched at $869A/$869B ($EFDF inside 'CUSTOM OFF' or $EFEA inside 'DESTROY OFF'); the $FFFF assembled here is only the byte pattern on disk
|
|
dey ; 869C next byte down
|
|
bpl L_8697 ; 869D three bytes copied?
|
|
lda #$00 ; 869F 0 again
|
|
sta messageTimer ; 86A1 redundant - messageTimer was already zeroed at $867B
|
|
lda #$1E ; 86A3 message id $1E
|
|
jsr queueMessage ; 86A5 queue it
|
|
sec ; 86A8 C = 1: the key was handled
|
|
rts ; 86A9 C = 1, key handled
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setDestroyModeMessage - re-aims the '*' toggle at the unit-placement phase: the off-state persistent
|
|
; message becomes id $0A ('SETUP UNITS. PRESS F 1 WHEN DONE.'), message slot $1E becomes 'DESTROY OFF'
|
|
; ($EFE2) and the ON/OFF patch address becomes $EFEA, the 'OFF' inside that string. The setup screen
|
|
; does the same at $820B/$821F with 'CUSTOM OFF' ($EFD8/$EFDF) and off-message 0.
|
|
; In: nothing
|
|
; Out: $868D = $0A, message slot $1E -> $EFE2, $869A/$869B = $EFEA
|
|
; Called from: startGameSession $7B4B, just before the unit placement loop
|
|
; ----------------------------------------------------------------------
|
|
setDestroyModeMessage:
|
|
lda #$0A ; 86AA message id $0A = 'SETUP UNITS. PRESS F 1 WHEN DONE.'
|
|
sta L_868C+1 ; 86AC patch the LDX operand at $868D
|
|
ldx #$1E ; 86AF message slot $1E
|
|
lda #$E2 ; 86B1 'DESTROY OFF' at $EFE2
|
|
ldy #$EF ; 86B3 high byte of $EFE2
|
|
jsr setMessageSlot ; 86B5 register it as message $1E
|
|
lda #$EA ; 86B8 $EFEA = the 'OFF' inside 'DESTROY OFF'
|
|
sta L_8699+1 ; 86BA patch $869A
|
|
lda #$EF ; 86BD high byte of $EFEA
|
|
sta L_8699+2 ; 86BF patch $869B
|
|
rts ; 86C2 the '*' toggle now drives the DESTROY message
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showCustomSummaryMessage - builds the status line 'CUSTOM:' [+ 'MAP,' when a named map is in use] +
|
|
; ' MENUS OFF,/ON, DAMAGE OFF./ON.' in the buffer at $0332 and registers it as the persistent
|
|
; message. The prefix is copied character by character from 'CUSTOM' ($CC19) and 'MAP TYPE' ($EF77);
|
|
; the option part is punched into the template ' MENUS XXXX DAMAGE XXX.' at $0636 by appendOnOffWord
|
|
; from bits 3 and 4 of gameTypeOptions, and the finished template is then appended behind the prefix.
|
|
; In: D_0BA3 bits 3, 4 and 7; the template at $0636 and the words at $064D
|
|
; Out: text in $0332.., the $0636 template overwritten in place, message slot $1E = $0332,
|
|
; D_92A1 = $1E, message queued; self-modifies the ADC operand at $8712 with the prefix length
|
|
; Called from: startFilmPlayback $7D4B and runGameStartOptions $856D
|
|
; ----------------------------------------------------------------------
|
|
showCustomSummaryMessage:
|
|
ldy #$00 ; 86C3 start of the output buffer
|
|
L_86C5:
|
|
lda msgCustom,y ; 86C5 'CUSTOM' at $CC19
|
|
and #$7F ; 86C8 strip the bit-7 string terminator
|
|
sta play2Grp06,y ; 86CA message buffer at $0332
|
|
iny ; 86CD next output position
|
|
cpy #$06 ; 86CE six characters of 'CUSTOM'
|
|
bcc L_86C5 ; 86D0 keep copying until six are done
|
|
lda #$3A ; 86D2 ':'
|
|
sta play2Grp06,y ; 86D4 append it to the buffer
|
|
iny ; 86D7 next output position
|
|
bit gameTypeOptions ; 86D8 gameTypeOptions bit 7 = custom (named) map
|
|
bpl L_86F3 ; 86DB random map -> no 'MAP,' in the summary
|
|
ldx #$00 ; 86DD start at the first character of 'MAP TYPE'
|
|
L_86DF:
|
|
lda addRowDeltaToDroneRow,x ; 86DF 'MAP TYPE' at $EF77 (also modemDriverE000:msgMapType, trainerAiE000:storeScanReferencePoint)
|
|
and #$7F ; 86E2 strip any terminator bit
|
|
sta play2Grp06,y ; 86E4 append to the buffer
|
|
inx ; 86E7 next source character
|
|
iny ; 86E8 next output position
|
|
cpx #$03 ; 86E9 only the first three characters, 'MAP'
|
|
bcc L_86DF ; 86EB keep copying until three are done
|
|
lda #$2C ; 86ED ','
|
|
sta play2Grp06,y ; 86EF append it to the buffer
|
|
iny ; 86F2 next output position
|
|
L_86F3:
|
|
sty L_8711+1 ; 86F3 patch the ADC operand at $8712 with the length of the prefix built so far
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Fill the two XXXX fields of the ' MENUS XXXX DAMAGE XXX.' template at $0636, put the terminator
|
|
; right behind the last word, then copy the whole template backwards onto the end of the prefix.
|
|
; ----------------------------------------------------------------------
|
|
ldx #$07 ; 86F6 template index 7 = the first X of ' MENUS XXXX'
|
|
lda gameTypeOptions ; 86F8 gameTypeOptions
|
|
D_86FB:
|
|
and #$08 ; 86FB bit 3 = UNIT MENUS
|
|
jsr appendOnOffWord ; 86FD write 'OFF,' or 'ON, ' there
|
|
ldx #$13 ; 8700 template index 19 = the XXX of ' DAMAGE XXX'
|
|
lda gameTypeOptions ; 8702 gameTypeOptions
|
|
and #$10 ; 8705 bit 4 = DAMAGE
|
|
jsr appendOnOffWord ; 8707 write 'OFF,' or 'ON, ' there
|
|
lda #$AE ; 870A '.' with bit 7 set = end of string
|
|
sta optionSummaryTemplate,x ; 870C put it right behind the last word written
|
|
txa ; 870F X = index of that terminator
|
|
clc ; 8710 no carry into the add
|
|
L_8711:
|
|
adc #$FF ; 8711 + prefix length (operand patched at $86F3)
|
|
tay ; 8713 Y = destination index of the last template byte
|
|
L_8714:
|
|
lda optionSummaryTemplate,x ; 8714 copy the template backwards so source and destination cannot collide
|
|
sta play2Grp06,y ; 8717 into the message buffer
|
|
dey ; 871A next destination byte down
|
|
dex ; 871B next template byte down
|
|
bpl L_8714 ; 871C down to template index 0
|
|
lda #$32 ; 871E the finished string at $0332
|
|
ldy #$03 ; 8720 high byte of $0332
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setPersistentMessage - registers the string A/Y as message slot $1E (setMessageSlot writes the
|
|
; pointer tables at $0500/$0524), makes $1E the persistent status message and queues it.
|
|
; In: A/Y = bit-7 terminated string
|
|
; Out: D_0500[$1E]/D_0524[$1E] = A/Y, D_92A1 = $1E, message queued, X = $1E
|
|
; Called from: showScenarioBannerAndWaitForButton $8787, plus fall-through from
|
|
; showCustomSummaryMessage
|
|
; ----------------------------------------------------------------------
|
|
setPersistentMessage:
|
|
ldx #$1E ; 8722 message slot $1E
|
|
jsr setMessageSlot ; 8724 setMessageSlot leaves X alone
|
|
txa ; 8727 A = $1E
|
|
sta persistentMessageId ; 8728 persistentMessageId $92A1 - the line reappears after any transient message
|
|
jmp queueMessage ; 872B queue it for display
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; appendOnOffWord - writes 'OFF,' (A = 0) or 'ON, ' (A != 0) into the template at $0636 starting at
|
|
; index X: copies the bit-7 terminated word from $064D (OFF) or $064D+3 (ON), adds a comma and, for
|
|
; ON, a trailing space so both forms occupy four characters. X comes back pointing at the comma.
|
|
; In: A = option state (0 = off), X = destination index inside $0636
|
|
; Out: four characters written at $0636+X, X = index of the comma
|
|
; Called from: showCustomSummaryMessage $86FD (UNIT MENUS) and $8707 (DAMAGE)
|
|
; ----------------------------------------------------------------------
|
|
appendOnOffWord:
|
|
pha ; 872E remember the state for the trailing-space decision
|
|
tay ; 872F Y = 0 selects 'OFF' at $064D
|
|
D_8730:
|
|
beq L_8734 ; 8730 off -> keep Y = 0
|
|
ldy #$03 ; 8732 on -> 'ON' at $064D+3
|
|
L_8734:
|
|
lda optionWordTable,y ; 8734 next character of the word
|
|
bmi L_8740 ; 8737 bit 7 set = last character
|
|
sta optionSummaryTemplate,x ; 8739 store it in the template
|
|
inx ; 873C next template position
|
|
iny ; 873D next source character
|
|
bne L_8734 ; 873E always - continue with the next character
|
|
L_8740:
|
|
and #$7F ; 8740 strip the terminator bit
|
|
sta optionSummaryTemplate,x ; 8742 store the last character
|
|
inx ; 8745 next template position
|
|
lda #$2C ; 8746 ','
|
|
sta optionSummaryTemplate,x ; 8748 store it
|
|
pla ; 874B the saved state
|
|
beq L_8755 ; 874C 'OFF,' is already four characters
|
|
inx ; 874E one position past the comma
|
|
lda #$20 ; 874F ' ' so that 'ON, ' is four too
|
|
sta optionSummaryTemplate,x ; 8751 store the padding blank
|
|
dex ; 8754 leave X on the comma
|
|
L_8755:
|
|
rts ; 8755 X points at the comma
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; showScenarioBannerAndWaitForButton - prints 'STANDARD' or 'CUSTOM' (from $CC11, chosen by $0BA3 bit
|
|
; 6), the scenario name (from $CCC8, bits 0-2) and ' SCENARIO' ($CC04), each centred in 13 columns,
|
|
; puts 'PRESS BUTTON WHEN READY.' ($CBBE) on the status line and then services the link until the fire
|
|
; button is clicked.
|
|
; In: D_0BA3, the fire button, the command exchange
|
|
; Out: info panel text, D_92A1 = $1E, status line cleared on exit
|
|
; Called from: runGameStartOptions $855C
|
|
; ----------------------------------------------------------------------
|
|
showScenarioBannerAndWaitForButton:
|
|
lda #$0F ; 8756 text cursor row 15
|
|
jsr clearInfoPanelSetRow ; 8758 clear the info panel, cursor to row 15
|
|
ldx #$00 ; 875B entry 0 = STANDARD
|
|
lda gameTypeOptions ; 875D gameTypeOptions
|
|
and #$40 ; 8760 bit 6 = CUSTOM rules
|
|
beq L_8765 ; 8762 standard rules
|
|
inx ; 8764 entry 1 = CUSTOM
|
|
L_8765:
|
|
lda #$11 ; 8765 rules name table at $CC11 (8-byte entries)
|
|
ldy #$CC ; 8767 high byte of $CC11
|
|
jsr printCentredTableEntry ; 8769 print it centred in 13 columns
|
|
jsr printNewline ; 876C next line
|
|
lda gameTypeOptions ; 876F gameTypeOptions
|
|
and #$07 ; 8772 bits 0-2 = game type
|
|
tax ; 8774 X = scenario index
|
|
lda #$C8 ; 8775 scenario name table at $CCC8 (8-byte entries)
|
|
ldy #$CC ; 8777 high byte of $CCC8
|
|
jsr printCentredTableEntry ; 8779 print the scenario name centred
|
|
lda #$04 ; 877C ' SCENARIO' at $CC04 (it starts and ends with a CR)
|
|
ldy #$CC ; 877E high byte of $CC04
|
|
jsr printStringCentred ; 8780 print it centred
|
|
lda #$BE ; 8783 'PRESS BUTTON WHEN READY.' at $CBBE
|
|
ldy #$CB ; 8785 high byte of $CBBE
|
|
jsr setPersistentMessage ; 8787 show it as the persistent status message
|
|
L_878A:
|
|
jsr serviceCommandExchange ; 878A keep the two machines in lock step while waiting
|
|
jsr checkFirePressed ; 878D new fire click?
|
|
bne L_878A ; 8790 no -> keep servicing
|
|
jmp clearMessageLine ; 8792 erase the status line and return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; printCentredTableEntry - sets the centring field width to 13 columns and prints entry X of a table
|
|
; of 8-byte bit-7 terminated strings whose base is in A/Y. Used for the STANDARD/CUSTOM words and for
|
|
; the seven scenario names.
|
|
; In: A/Y = table base, X = entry index
|
|
; Out: D_90EF = $0D, string printed centred (printStringAtPointer clears D_90EF again)
|
|
; Called from: $857F, $8769, $8779
|
|
; ----------------------------------------------------------------------
|
|
printCentredTableEntry:
|
|
pha ; 8795 keep the table base low byte across the store
|
|
lda #$0D ; 8796 13 columns
|
|
sta textCentreWidth ; 8798 textCentreWidth $90EF - the field the next string is centred in
|
|
pla ; 879B table base low byte again
|
|
jmp printTableEntry ; 879C print entry X (base + X*8)
|
|
|
|
|
|
; Result and alert message strings shared with overlay variant B. From $879F on the bytes are
|
|
; identical in both variants; only $8789-$879E differ, so in variant A messages $11 ('WE LOST ON
|
|
; POINTS!') and $12 ('WE WON ON POINTS!') are broken while messages $13-$16 and 'DRONE ALERT!' still
|
|
; read correctly. They are only ever shown at game end, when variant B is resident.
|
|
msgWeWonOnPointsTail:
|
|
.byte "ON ON POINTS",'!'|$80 ; 879F tail of message $12 'WE WON ON POINTS!'; its first four bytes at $879B are code here
|
|
|
|
; msgItWasADraw: text, 14 bytes. 'IT WAS A DRAW.' = message $13 (game result)
|
|
msgItWasADraw:
|
|
.byte "IT WAS A DRAW",'.'|$80 ; 87AC message $13 (pointer $0500/$0524 index $13 = $87AC)
|
|
|
|
; msgWeWereKnockedOut: text, 20 bytes. 'WE WERE KNOCKED OUT!' = message $14
|
|
msgWeWereKnockedOut:
|
|
.byte "WE WERE KNOCKED OUT",'!'|$80; 87BA message $14
|
|
|
|
; msgWeWonByKnockout: text, 21 bytes. 'WE WON BY A KNOCKOUT!' = message $15. Comm build 1 (T35, solo
|
|
; trainer) reuses $87CE+unit as a per-unit work table ($EB29/$EB53 store the game clock there), so
|
|
; with the trainer resident these strings are clobbered.
|
|
msgWeWonByAKnockout:
|
|
.byte "WE WON BY A KNOCKOUT",'!'|$80; 87CE message $15; the solo trainer overlay (T35) reuses $87CE+unit as a per-unit work table ($EB29/$EB53 store the game clock there), so these strings are clobbered in a solo game
|
|
|
|
; msgGameOver: text, 10 bytes. 'GAME OVER.' = message $16
|
|
msgGameOver:
|
|
.byte "GAME OVER",'.'|$80 ; 87E3 message $16
|
|
|
|
; msgDroneAlert: text, 12 bytes. 'DRONE ALERT!' - installed as message slot $20 by overlay A at $7BA6
|
|
; (sub_C924 X=$20)
|
|
msgDroneAlert:
|
|
.byte "DRONE ALERT",'!'|$80 ; 87ED installed as run-time message slot $20 by overlay A at $7BA6
|
|
|
|
; overlayTailPadding: unknown, 6 bytes. six zero bytes of padding before the variant flag
|
|
overlayTailPadding:
|
|
.byte $00,$00,$00,$00,$00,$00 ; 87F9 ...... six unused bytes between the last string and the identity flag; referenced by nothing
|
|
|
|
; overlayVariantFlag: byteTable, 1 bytes. $00 = variant A, $FF = variant B (bit 7 tested by the main
|
|
; program at $1036/$1080)
|
|
overlayVariantFlag:
|
|
.byte $00 ; 87FF . $00 = variant A (map generator and setup) is resident; bit 7 is tested at $1036/$1080
|