3876 lines
255 KiB
ArmAsm
3876 lines
255 KiB
ArmAsm
; ============================================================================
|
|
; $E000-$EFFF - solo trainer: the computer opponent (track 35 sectors 0-15)
|
|
; ============================================================================
|
|
; Loaded by $0F1A when the solo trainer was chosen ($0BA5 bit 7 set). It presents the same jump table as
|
|
; the modem driver but fakes the link: instead of sending and receiving over a wire it plans the moves of
|
|
; side 1 and injects them straight into the received-packet buffer. $EE00-$EFFF is swapped for the drone
|
|
; AI (track 29 sectors 4-5) while its drone is airborne.
|
|
|
|
.setcpu "6502"
|
|
.include "c64.inc"
|
|
.include "zeropage.inc"
|
|
|
|
; ---- references to code/data outside this file ----
|
|
checksumXorConstant := $0400
|
|
play4Grp12 := $0464
|
|
D_077E := $077E
|
|
loadSectors := $0804
|
|
setLoadDest := $085C
|
|
filmPlaybackMode := $0B9B
|
|
gameTypeOptions := $0BA3
|
|
gameEndReason := $0BA8
|
|
D_2009 := $2009
|
|
readMapCell := $25FA
|
|
terrainSightBonusTable := $35BB
|
|
squaresTable := $3860
|
|
distanceSquared := $3870
|
|
D_3E99 := $3E99
|
|
mirrorMapCoordinate := $3FA3
|
|
D_434C := $434C
|
|
D_4CE0 := $4CE0
|
|
getCommandLength := $4D9F
|
|
D_60E0 := $60E0
|
|
L_754C := $754C
|
|
D_774C := $774C
|
|
mapCellToRadarGrid := $7A13
|
|
msgWeWonByAKnockout := $87CE
|
|
msgGetOpponentOnPhoneAnd := $8800
|
|
D_8831 := $8831
|
|
D_8832 := $8832
|
|
D_8864 := $8864
|
|
D_8895 := $8895
|
|
D_8896 := $8896
|
|
msgPickUpPhoneThenPressSpace := $8897
|
|
D_8898 := $8898
|
|
D_8899 := $8899
|
|
D_88A0 := $88A0
|
|
D_88D2 := $88D2
|
|
D_88DE := $88DE
|
|
D_88EA := $88EA
|
|
D_88F6 := $88F6
|
|
D_8902 := $8902
|
|
D_890E := $890E
|
|
D_891A := $891A
|
|
D_8926 := $8926
|
|
D_8932 := $8932
|
|
D_893E := $893E
|
|
D_894A := $894A
|
|
D_8952 := $8952
|
|
D_8953 := $8953
|
|
D_8954 := $8954
|
|
D_8955 := $8955
|
|
D_8956 := $8956
|
|
D_8957 := $8957
|
|
D_8958 := $8958
|
|
D_895A := $895A
|
|
D_895B := $895B
|
|
D_895D := $895D
|
|
msgOpponentUsingNonStandardGame := $895E
|
|
D_895F := $895F
|
|
D_8960 := $8960
|
|
D_8961 := $8961
|
|
D_8962 := $8962
|
|
D_8963 := $8963
|
|
D_8964 := $8964
|
|
D_8965 := $8965
|
|
D_8966 := $8966
|
|
D_8967 := $8967
|
|
D_8968 := $8968
|
|
D_8969 := $8969
|
|
D_896A := $896A
|
|
D_896B := $896B
|
|
D_896C := $896C
|
|
D_896D := $896D
|
|
D_896E := $896E
|
|
D_896F := $896F
|
|
D_8970 := $8970
|
|
D_8971 := $8971
|
|
D_8972 := $8972
|
|
D_8973 := $8973
|
|
D_8974 := $8974
|
|
D_8975 := $8975
|
|
D_8976 := $8976
|
|
D_8977 := $8977
|
|
D_8979 := $8979
|
|
D_897A := $897A
|
|
D_897B := $897B
|
|
D_897C := $897C
|
|
D_8D03 := $8D03
|
|
vicBorderShadow := $9003
|
|
lastStepRowDelta := $90F4
|
|
lastStepColDelta := $90F5
|
|
currentUnit := $90FA
|
|
gameClock := $91CB
|
|
fireTargetUnit := $91F0
|
|
droneHeading := $920B
|
|
droneControlFlags := $920D
|
|
comcenUnit := $9236
|
|
dronesLeft := $92AE
|
|
opponentDronesLeft := $92AF
|
|
opponentMissilesLeft := $92B1
|
|
currentCommandLength := $92EE
|
|
droneViewOriginCol := $92F6
|
|
droneViewOriginRow := $92F7
|
|
D_A989 := $A989
|
|
D_B908 := $B908
|
|
unitColTable := $F640
|
|
side0ComcenCol := $F671
|
|
side1UnitColTable := $F672
|
|
side1ComcenCol := $F6A3
|
|
unitRowTable := $F6A4
|
|
side0ComcenRow := $F6D5
|
|
side1UnitRowTable := $F6D6
|
|
side1ComcenRow := $F707
|
|
side1UnitFlagsTable := $F73A
|
|
unitTypeTable := $F76C
|
|
side1UnitTypeTable := $F79E
|
|
unitDestColTable := $F7D0
|
|
side1UnitDestColTable := $F802
|
|
side1ComcenDestCol := $F833
|
|
unitDestRowTable := $F834
|
|
side1UnitDestRowTable := $F866
|
|
side1ComcenDestRow := $F897
|
|
side1ComcenStun := $F9C3
|
|
side0ComcenEnergy := $F9F5
|
|
side1ComcenEnergy := $FA27
|
|
unitTerrainUnderTable := $FAF0
|
|
side1ComcenDisplayFlags := $FBB7
|
|
initLineStepper := $FBB8
|
|
stepLine := $FBFA
|
|
divideSigned16by8 := $FCEC
|
|
nextGameRandom := $FD5D
|
|
|
|
; Contents
|
|
; --------
|
|
; $E000 commRequestEntry Jump-table slot 0 of the opponent module (JMP handleCommRequest).
|
|
; $E003 commSetLinkStateEntry Jump-table slot 1 (JMP setCommLinkState): brings the pretend link up or down.
|
|
; $E006 commGetReceivedByteEntry Jump-table slot 2 (JMP reportNoReceivedByte).
|
|
; $E009 commSendByteEntry Jump-table slot 3 (JMP commNoOpClc).
|
|
; $E00C commFrameTickEntry Jump-table slot 4 (JMP commNoOpClc).
|
|
; $E012 commKeyboardScanVector A JMP whose two operand bytes $E013/$E014 are the module's keyboard-scan
|
|
; vector.
|
|
; $E015 commReadKeyEntry Jump-table slot 5 (JMP commKeyboardScanVector).
|
|
; $E043 handleCommRequest Body of commRequestEntry.
|
|
; $E060 setCommLinkState Body of commSetLinkStateEntry.
|
|
; $E075 reportNoReceivedByte SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in
|
|
; the trainer build.
|
|
; $E077 commNoOpClc CLC + RTS: the shared no-op used as the body of commSendByteEntry,
|
|
; commFrameTickEntry and the unpatched keyboard vector.
|
|
; $E079 runTrainerRoundHook Once-per-round hook, called from finishRound ($4654) right after the game clock
|
|
; has been decremented and only in a solo game ($0BA5 bit 7 set, film playback excluded).
|
|
; $E082 buildTrainerReplyPacket Entered with X=1 from sendOutgoingPacket, i.e.
|
|
; $E0E8 trainerUnitStepHook Per-unit hook called at the end of processUnitStep ($4526) for currentUnit,
|
|
; i.e.
|
|
; $E128 setSubOverlayLoadDest Points the fast loader at $EE00 (X = destination low byte, Y = high byte) and
|
|
; tail-jumps into setLoadDest.
|
|
; $E12F pickTrainerRallyPoint Chooses the AI's current objective: a cell scattered at random around the human
|
|
; comcen (unit 49), stored as the rally column/row that every attacking group heads for.
|
|
; $E14A randomiseCoordinate Returns A = base coordinate plus a random offset, clamped to 0..39.
|
|
; $E170 planTrainerRound Top-level planner of the solo trainer, run once per game round.
|
|
; $E21F pushTrainerComcenForward Attack-mode tail of planTrainerRound (a branch target, not a JSR entry).
|
|
; $E266 retreatTrainerComcen Defend-mode tail of planTrainerRound (a branch target).
|
|
; $E2BD scanTrainerUnitsForOrders Walks the AI's 50 unit slots from ordinal 49 (unit 99, the comcen) down to 0,
|
|
; stopping as soon as four commands have been queued this round.
|
|
; $E2F1 decideTrainerGroupAction Decides what the AI group owning unit ordinal Y should do.
|
|
; $E339 aimGroupAtOwnComcen Internal tail (JMP target from retreatTrainerComcen $E2B3, fall-through from
|
|
; $E336): points the line target at the AI comcen's cell, then falls into the shared code at $E343 that copies the group
|
|
; centre into the search origin and the line target into the wanted destination, and jumps to issueTrainerGroupOrders.
|
|
; $E3D0 aimGroupAtHomeLine Internal branch target used when the group still has members that have not
|
|
; reached the player's end zones and has no contact with his comcen: the target column stays where the group already is
|
|
; and the target row becomes 29 plus the group's own vertical extents, i.e.
|
|
; $E42A findTrainerAdvanceCell Threat-aware path search.
|
|
; $E49E issueTrainerGroupOrders Issues the move orders for the group in $8955.
|
|
; $E531 computeTrainerGroupStats Gathers the statistics of AI group Y.
|
|
; $E5FF clampGroupTargetToMap Clips the wanted group centre in zp_22/zp_24 so that the whole formation box
|
|
; (reach left/right in $890E/$8926 and up/down in $891A/$8932) still fits inside the 40x40 map, stores the clipped cell
|
|
; as the group's target and returns the movement delta relative to the group's average ordered destination in zp_2A
|
|
; (columns, also kept in $8958) and zp_2B (rows).
|
|
; $E66C offsetPointInDirection Computes the cell that lies Y steps in compass direction X from the reference
|
|
; point (zp_27 column, zp_28 row), using dirToColDelta/dirToRowDelta and offsetCoordinateClamped.
|
|
; $E69C isGroupDestinationTooRisky influence-map test of one candidate destination for the AI group in
|
|
; currentAiGroup ($8955).
|
|
; $E773 buildUnitThreatTables rebuilds the two 100-entry influence-map tables that isGroupDestinationTooRisky
|
|
; reads.
|
|
; $E7B9 multiplyAbyY 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y =
|
|
; 0.
|
|
; $E7D3 buildDirectionFanTowardTarget finds the compass heading from the line start (lineCurCol,lineCurRow) to the
|
|
; line end
|
|
; $E7E3 buildDirectionFan fills the eight-byte scratch buffer $8896-$889D with the move directions the
|
|
; path search should try,
|
|
; $E80E getStepDirection sets a Bresenham walker up between (lineCurCol,lineCurRow) and
|
|
; (lineEndCol,lineEndRow), takes exactly one
|
|
; $E82A cellDistance integer distance between two map cells.
|
|
; $E83C updateFocusGroupCentre keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row)
|
|
; current.
|
|
; $E882 computeGroupCentroid averages the position and the ordered destination of every living member of one
|
|
; AI group.
|
|
; $E919 orderAiUnitToCell the trainer's central 'issue a move order' helper.
|
|
; $E937 queueMirroredMoveOrder second half of orderAiUnitToCell.
|
|
; $E95D isPathToPlayerComcenClear walks a Bresenham line from the current line position (which the caller has set
|
|
; to the group
|
|
; $E997 loadGroupCentreIntoLinePos copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6)
|
|
; into the line
|
|
; $E9A6 scoreTargetBlock scores the 2x2 block of map cells whose top-left corner is (X,Y) as a
|
|
; bombardment target.
|
|
; $E9F6 addUnitTargetValue helper of scoreTargetBlock.
|
|
; $EA1B offsetCoordinateClamped returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps of
|
|
; the signed step X, held
|
|
; $EA38 mirrorBlockCornerForOpponent converts the top-left corner of a 2x2 object (the drone) into the coordinates
|
|
; that the engine's
|
|
; $EA46 queueAiCommand appends the command staged in $8896..$8899 to the AI's 50-byte order queue at
|
|
; $88A0.
|
|
; $EA7B injectAiCommandsIntoPacket the heart of the solo trainer.
|
|
; $EACF updateAiUnitTracking per-unit hook, run from trainerUnitStepHook after the engine has stepped each
|
|
; unit.
|
|
; $EB67 runAiComcenActions spends whatever is left of the AI comcen's three command slots for this round.
|
|
; $EBA5 maybeQueueComcenEnergyRepair the trainer's version of the human console's energy repair.
|
|
; $EBDD queueComcenCommand shared tail of the comcen actions: stores A as the command byte and jumps into
|
|
; queueAiCommand, leaving
|
|
; $EBE4 haltAiComcen orders the AI comcen to move to the cell it is already standing on.
|
|
; $EBEF assessThreatAroundAiComcen builds a directional pressure map around the AI comcen and picks the direction
|
|
; to run.
|
|
; $EC82 rankThreatDirections Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just turned
|
|
; every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one of the eight compass
|
|
; buckets $8896-$889D.
|
|
; $EC91 smoothThreatRing Circular 1-2-1 blur of the eight-direction threat ring.
|
|
; $ECD4 findQuietestDirectionArc For every leading direction y = 7..0 it adds the blurred pressures of y, y-1
|
|
; and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle direction y-1.
|
|
; $ED0B runAiMissileTurn The computer opponent's one 'weapon' decision per lock-step exchange.
|
|
; $EDD5 fireMissileAtSpottedUnit Second half of the missile turn, reached whenever no anti-drone shot was taken.
|
|
; $EE00 targetEnemyComcenWithMissile Fall-through tail of fireMissileAtSpottedUnit: when the AI has no remembered
|
|
; attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49) instead, but only while that
|
|
; comcen is currently visible.
|
|
; $EE2D queueMissileStrikeCommand Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler
|
|
; cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to have received from
|
|
; the opponent.
|
|
; $EE55 randomlySignValue Returns A or -A with equal probability, using bit 0 of the lock-step game RNG.
|
|
; $EE6A isUnitOnAiRadar Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17
|
|
; radar grid centred on the computer opponent's own COMCEN.
|
|
; $EE85 launchAiDrone Sends the computer opponent's drone up when one is due.
|
|
; $EEE0 chooseAiDroneTarget Picks what the computer opponent should send its next drone at and leaves the
|
|
; unit index in $8976.
|
|
; $EF59 pickDroneTargetAlongThreatAxis Tail of chooseAiDroneTarget for the case where the human comcen is no use as
|
|
; a drone target.
|
|
|
|
.org $E000
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commRequestEntry - Jump-table slot 0 of the opponent module (JMP handleCommRequest). The game calls
|
|
; it every raster IRQ with X=0 ($1129), with X=1 from sendOutgoingPacket ($56AE) to hand over the
|
|
; outgoing packet, with X=2 to acknowledge a completed exchange ($4EFF/$526F) and with X=3 to cancel a
|
|
; pending send. In this build (track 35 = SOLO TRAINER) there is no link at all: X=1 runs one step of
|
|
; the computer opponent and leaves its orders in the receive buffer.
|
|
; In: X = request code 0..3
|
|
; Out: see handleCommRequest ($E043)
|
|
; Called from: mainProgram0800 $1129 (raster IRQ, X=0), $4EFF and $526F (X=2), $56AE (X=1),
|
|
; $570F/$5727/$573B/$5767/$5776 (menu-level exchange)
|
|
; ----------------------------------------------------------------------
|
|
commRequestEntry:
|
|
jmp handleCommRequest ; E000 slot 0: per-frame poll / packet request, X selects the function
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commSetLinkStateEntry - Jump-table slot 1 (JMP setCommLinkState): brings the pretend link up or
|
|
; down.
|
|
; In: X = 0 open, >= 1 close
|
|
; Out: isLinkActive / linkStatus / linkStatusSample ($E03B/$E03C/$E03D), and $E01F when closing
|
|
; Called from: initCommModule $0F46 (X=1), hangUpModemSetState $1B56 and $1B70 (X=3)
|
|
; ----------------------------------------------------------------------
|
|
commSetLinkStateEntry:
|
|
jmp setCommLinkState ; E003 slot 1: open (X=0) or close (X>0) the link flags
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commGetReceivedByteEntry - Jump-table slot 2 (JMP reportNoReceivedByte). In the modem build this
|
|
; pops the UART receive ring; the trainer has no wire, so it always answers 'nothing received' and the
|
|
; opponent chat line stays empty in a solo game.
|
|
; In: none
|
|
; Out: C=1 always (no byte)
|
|
; Called from: receiveChatCharacter $1399
|
|
; ----------------------------------------------------------------------
|
|
commGetReceivedByteEntry:
|
|
jmp reportNoReceivedByte ; E006 slot 2: fetch a received chat byte - the trainer never has one
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commSendByteEntry - Jump-table slot 3 (JMP commNoOpClc). The chat character handed over is simply
|
|
; dropped and C=0 ('accepted') is returned.
|
|
; In: A = byte to send (ignored)
|
|
; Out: C=0
|
|
; Called from: handleChatSend $1396 (JMP, not JSR)
|
|
; ----------------------------------------------------------------------
|
|
commSendByteEntry:
|
|
jmp commNoOpClc ; E009 slot 3: send a chat byte - the trainer throws it away
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commFrameTickEntry - Jump-table slot 4 (JMP commNoOpClc). In the modem build this services the
|
|
; software UART once per frame; with no serial line there is nothing to do.
|
|
; In: none
|
|
; Out: C=0
|
|
; Called from: rasterIrqHandler $112C
|
|
; ----------------------------------------------------------------------
|
|
commFrameTickEntry:
|
|
jmp commNoOpClc ; E00C slot 4: per-IRQ service tick - nothing to service without a UART
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Two one-instruction stubs the module can point a vector at. $E00F is a plain RTS and $E010 an RTI;
|
|
; $E010 is the initial value of nmiChainVector ($E031/$E032), so an NMI arriving before the game
|
|
; patches
|
|
; that vector simply returns.
|
|
; ----------------------------------------------------------------------
|
|
commRtsRtiStubs:
|
|
rts ; E00F $60 = RTS: harmless body for any vector pointed here
|
|
|
|
.byte $40 ; E010 $40 = RTI: the module's default NMI handler, target of the initial $E031/$E032 = $E010
|
|
isOriginateMode:
|
|
.byte $00 ; E011 answer/originate flag: waitForAnswerOriginateKey ($0CDE) stores 0 for 'A' and 1 for 'O'. Modem-only; nothing in the trainer build reads it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commKeyboardScanVector - A JMP whose two operand bytes $E013/$E014 are the module's keyboard-scan
|
|
; vector. initCommModule patches them to the game's scanKeyboard ($0DB7) with the two stores at
|
|
; $0F4B/$0F50; until then the vector points at commNoOpClc, so no key is ever reported.
|
|
; In: none
|
|
; Out: whatever scanKeyboard returns (A = key code, X/Y = shift / Commodore-key flags)
|
|
; Called from: commReadKeyEntry $E015 (JMP). The XREF also lists a call from $E2EC, but that address
|
|
; is mid-instruction here - it belongs to the modem build that shares this address range.
|
|
; ----------------------------------------------------------------------
|
|
commKeyboardScanVector:
|
|
jmp commNoOpClc ; E012 operand bytes $E013/$E014 are the keyboard vector, patched to scanKeyboard $0DB7 by $0F4B/$0F50
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commReadKeyEntry - Jump-table slot 5 (JMP commKeyboardScanVector). This is the only route by which
|
|
; the game reads the keyboard, so a comm module that owns the CPU can steal keys before the game sees
|
|
; them.
|
|
; In: none
|
|
; Out: key code in A, shift / Commodore-key flags in X and Y
|
|
; Called from: pollKeyboardEvent $0E56 (every frame)
|
|
; ----------------------------------------------------------------------
|
|
commReadKeyEntry:
|
|
jmp commKeyboardScanVector ; E015 slot 5: read one key through the patchable vector above
|
|
|
|
|
|
; Dead space. The modem build (track 34) puts a sixth jump-table entry at $E018; the trainer build
|
|
; leaves five $00 bytes here. Nothing in this file or in the main program calls $E018, and executing
|
|
; it would BRK.
|
|
unusedJumpSlotE018:
|
|
.byte $00 ; E018 five unused $00 bytes where the modem build has its sixth jump-table slot
|
|
.byte $00,$00,$00,$00 ; E019 ....
|
|
|
|
; $E01D-$E042 is the fixed interface block that both comm-module builds present to the game at exactly
|
|
; the
|
|
; same addresses: the packet-exchange flags, the received and outgoing packet lengths and their two
|
|
; 7-byte buffers, the build id, the NMI chain vector, the CIA2 ICR shadow, the disk-access suspend
|
|
; handshake and the three link flags. In the trainer build most of it is only ever written by
|
|
; handleCommRequest and setCommLinkState; $E034-$E038 and $E03E-$E042 are modem-build variables that
|
|
; stay zero for ever.
|
|
commInterfaceBlock:
|
|
.byte $00 ; E01D exchange flags: bit 7 = opponent packet received, bit 6 = exchange complete, bits 0/5 = send requested / in progress. handleCommRequest pins it at $C0
|
|
rxPacketLength:
|
|
.byte $00 ; E01E length 0-7 of the 'received' packet the trainer fabricates in $E020
|
|
txPacketLength:
|
|
.byte $00 ; E01F length of the outgoing packet the game built in $E027; the trainer never reads the bytes
|
|
rxPacketBuffer:
|
|
.byte $00 ; E020 received-packet buffer $E020-$E026, byte 0 = the command byte the engine will execute
|
|
rxPacketArg1:
|
|
.byte $00 ; E021 received packet byte 1 (first command argument)
|
|
rxPacketArg2:
|
|
.byte $00 ; E022 received packet byte 2
|
|
rxPacketArg3:
|
|
.byte $00 ; E023 received packet byte 3
|
|
rxPacketArg4:
|
|
.byte $00 ; E024 received packet bytes 4-6, the tail of the 7-byte buffer
|
|
.byte $00,$00 ; E025 ..
|
|
txPacketBuffer:
|
|
.byte $00 ; E027 outgoing-packet buffer $E027-$E02D, filled by sendOutgoingPacket ($568C); discarded here
|
|
txPacketArg1:
|
|
.byte $00 ; E028 outgoing packet byte 1
|
|
txPacketArg2:
|
|
.byte $00 ; E029 outgoing packet byte 2
|
|
txPacketArg3:
|
|
.byte $00 ; E02A outgoing packet bytes 3-6
|
|
.byte $00,$00,$00 ; E02B ...
|
|
hostOutCount:
|
|
.byte $00 ; E02E chat transmit-queue depth; handleChatSend ($1357/$1376) only opens typing while it is 0, so in a solo game the editor opens and the text goes nowhere
|
|
peerCommBuildId:
|
|
.byte $00 ; E02F build id reported by the peer; never filled in without a link
|
|
commBuildId:
|
|
.byte $FF ; E030 comm build id: $FF = build 1 = the solo trainer (track 35). The game compares it with linkTypeFlag $0BA5 at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D to see whether the right module is resident
|
|
nmiChainVector:
|
|
.byte $10 ; E031 NMI chain vector low byte, initially $10 -> $E010 (the RTI above); the game patches it to $1298 at $0BF3
|
|
nmiChainVectorHi:
|
|
.byte $E0 ; E032 NMI chain vector high byte ($E0 here, patched to $12)
|
|
ciaIcrShadow:
|
|
.byte $7F ; E033 CIA2 ICR mask shadow; $7F means 'clear every CIA2 interrupt source'. readFilmByte ($5889/$58A8/$58CD) writes this back to $DD0D after banking the film RAM in - the trainer wants no CIA2 NMI at all
|
|
L_E034:
|
|
.byte $00 ; E034 $E034-$E037: modem-build link variables (command phase, retry state); never touched here
|
|
.byte $00,$00,$00 ; E035 ...
|
|
D_E038:
|
|
.byte $00 ; E038 modem-build link variable; unused in the trainer build
|
|
nmiSuspendRequest:
|
|
.byte $00 ; E039 disk-access suspend request: suspendCommModule stores $C0 here at $0F67, resumeCommModule 0 at $0F78
|
|
nmiSuspendAck:
|
|
.byte $00 ; E03A suspend acknowledge; handleCommRequest copies $E039 into it so the wait at $0F6A/$0F7B always succeeds at once
|
|
isLinkActive:
|
|
.byte $00 ; E03B link-active flag, $FF after setCommLinkState(X=0); read all over the main program ($0F60, $1331, $1A40, $4E46, ...)
|
|
linkStatus:
|
|
.byte $00 ; E03C carrier / link status; kept at $FF so the bit-6 test in pollMenuSession ($56B1) and the bit-7 test in hangUpModemSetState ($1B4B) always pass
|
|
linkStatusSample:
|
|
.byte $00 ; E03D $E03D = link-ready sample read at $1BAB; $E03E is a modem-build byte and stays zero
|
|
.byte $00 ; E03E .
|
|
D_E03F:
|
|
.byte $00 ; E03F modem-build reply timer; unused here
|
|
D_E040:
|
|
.byte $00 ; E040 modem-build connection phase; unused here
|
|
D_E041:
|
|
.byte $00 ; E041 modem-build 'skip init string' flag; unused here
|
|
D_E042:
|
|
.byte $00 ; E042 modem-build UART restart request; unused here
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleCommRequest - Body of commRequestEntry. X=1 jumps to buildTrainerReplyPacket, which is where
|
|
; the computer opponent actually runs. X=3 forgets the outgoing packet. Every other code (X=0
|
|
; per-frame poll, X=2 acknowledge) simply declares the exchange finished by storing $C0 in the
|
|
; exchange flags and mirroring the suspend request into the suspend acknowledge.
|
|
; In: X = request code 0..3
|
|
; Out: $E01D = $C0 and $E03A = $E039, or $E01F = 0 for X=3, or a whole AI turn for X=1
|
|
; Called from: commRequestEntry $E000
|
|
; ----------------------------------------------------------------------
|
|
handleCommRequest:
|
|
cpx #$01 ; E043 request 1 = 'transmit the outgoing packet'?
|
|
D_E045:
|
|
bne L_E04A ; E045 no - test the remaining request codes
|
|
D_E047:
|
|
jmp buildTrainerReplyPacket ; E047 yes: instead of transmitting, run one step of the computer opponent
|
|
|
|
L_E04A:
|
|
cpx #$03 ; E04A request 3 = 'cancel the pending send'?
|
|
D_E04C:
|
|
bne L_E054 ; E04C no - fall through to the poll / acknowledge case
|
|
D_E04E:
|
|
lda #$00 ; E04E cancel: drop whatever was queued
|
|
sta txPacketLength ; E050 outgoing packet length = 0
|
|
rts ; E053 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Poll (X=0) and acknowledge (X=2) share this tail. Because the flags are unconditionally set to 'own
|
|
; packet sent AND opponent packet received', processCommandExchange ($4DB7) never waits for a peer and
|
|
; the suspend/resume handshake around every disk access completes on the first poll.
|
|
; ----------------------------------------------------------------------
|
|
L_E054:
|
|
lda #$C0 ; E054 $C0 = bit 7 'opponent packet received' + bit 6 'exchange complete'
|
|
D_E056:
|
|
sta commInterfaceBlock ; E056 report the exchange as finished the instant it is asked about
|
|
D_E059:
|
|
lda nmiSuspendRequest ; E059 take the disk-access suspend request...
|
|
sta nmiSuspendAck ; E05C ...and answer it immediately, so $0F5F/$0F71 never block
|
|
D_E05F:
|
|
rts ; E05F done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setCommLinkState - Body of commSetLinkStateEntry. X=0 stores $FF into the three link flags
|
|
; $E03D/$E03C/$E03B, X>=1 stores 0 in all three and also clears the outgoing packet length. Since the
|
|
; carrier byte $E03C is $FF whenever the link is 'open', every carrier test in the main program sees a
|
|
; healthy connection.
|
|
; In: X = 0 open, >= 1 close
|
|
; Out: $E03B, $E03C, $E03D and (on close) $E01F
|
|
; Called from: commSetLinkStateEntry $E003
|
|
; ----------------------------------------------------------------------
|
|
setCommLinkState:
|
|
lda #$FF ; E060 $FF = link up
|
|
cpx #$01 ; E062 X=0 means open, anything else means close
|
|
bcc L_E06B ; E064 open - keep $FF
|
|
lda #$00 ; E066 close - the three flags become 0
|
|
sta txPacketLength ; E068 and the outgoing packet is discarded
|
|
L_E06B:
|
|
sta linkStatusSample ; E06B link-ready sample
|
|
sta linkStatus ; E06E carrier status - $FF makes every carrier test in the game succeed
|
|
L_E071:
|
|
sta isLinkActive ; E071 link-active flag the main program polls everywhere
|
|
rts ; E074 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; reportNoReceivedByte - SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in
|
|
; the trainer build.
|
|
; In: none
|
|
; Out: C=1
|
|
; Called from: commGetReceivedByteEntry $E006 (JMP)
|
|
; ----------------------------------------------------------------------
|
|
reportNoReceivedByte:
|
|
sec ; E075 C=1 = 'no character available'
|
|
rts ; E076 the trainer never delivers opponent chat text
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commNoOpClc - CLC + RTS: the shared no-op used as the body of commSendByteEntry, commFrameTickEntry
|
|
; and the unpatched keyboard vector. $E078 is the bare RTS on its own.
|
|
; In: none
|
|
; Out: C=0
|
|
; Called from: commSendByteEntry $E009, commFrameTickEntry $E00C, commKeyboardScanVector $E012 (all
|
|
; JMP)
|
|
; ----------------------------------------------------------------------
|
|
commNoOpClc:
|
|
clc ; E077 C=0 = 'accepted' / 'nothing to report'
|
|
commNoOpRts:
|
|
rts ; E078 shared bare RTS
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runTrainerRoundHook - Once-per-round hook, called from finishRound ($4654) right after the game
|
|
; clock has been decremented and only in a solo game ($0BA5 bit 7 set, film playback excluded). Unless
|
|
; the drone-flight sub-overlay is currently sitting at $EE00 it runs planTrainerRound, the computer
|
|
; opponent's 'think' routine.
|
|
; In: $8977 drone timer (bit 7 = drone sub-overlay resident)
|
|
; Out: see planTrainerRound ($E170)
|
|
; Called from: finishRound $4654
|
|
; ----------------------------------------------------------------------
|
|
runTrainerRoundHook:
|
|
lda D_8977 ; E079 drone timer; bit 7 marks the drone-flight overlay as resident at $EE00
|
|
bmi L_E081 ; E07C the drone-flight AI occupies $EE00-$EFFF, so planTrainerRound must not run (it would call chooseAiDroneTarget at $EEE0)
|
|
D_E07E:
|
|
jsr planTrainerRound ; E07E run the once-per-round planner
|
|
L_E081:
|
|
rts ; E081 back to finishRound
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildTrainerReplyPacket - Entered with X=1 from sendOutgoingPacket, i.e. once per lock-step
|
|
; exchange, immediately after the human's packet was handed over. It clears the received-packet length
|
|
; and gives up at once during the setup phase, during film playback, after the game has ended or while
|
|
; the game is paused. Otherwise it manages the swappable $EE00-$EFFF quarter of the module: while a
|
|
; drone is airborne it loads track 29 sectors 4-5 there (the drone-flight AI) and calls $EE00 every
|
|
; exchange; when the drone is gone it reloads track 35 sectors 14-15 (its own missile/target tail). In
|
|
; the normal case it ticks the launch countdown down and calls injectAiCommandsIntoPacket, which moves
|
|
; whole queued AI commands into the received-packet buffer $E020.
|
|
; In: gamePhase zp_B1, filmPlaybackMode $0B9B, gameEndReason $0BA8, pauseState zp_BE,
|
|
; droneControlFlags $920D, drone timer $8977
|
|
; Out: $E01E/$E020.. = the fabricated 'received' packet, $8976 drone target, $8977 timer, and
|
|
; $EE00-$EFFF possibly reloaded from disk
|
|
; Called from: handleCommRequest $E047 (JMP, request code 1)
|
|
; Guards first: the AI only produces orders while a real battle is running.
|
|
; ----------------------------------------------------------------------
|
|
buildTrainerReplyPacket:
|
|
lda #$00 ; E082 start with an empty incoming packet
|
|
D_E084:
|
|
sta rxPacketLength ; E084 received packet length = 0
|
|
lda gamePhase ; E087 gamePhase is 0 only while the battle proper runs
|
|
bne L_E0E7 ; E089 side selection / unit setup / menus - stay silent
|
|
lda filmPlaybackMode ; E08B film playback flag
|
|
D_E08E:
|
|
bmi L_E0E7 ; E08E bit 7 = replaying a film; the recorded stream supplies the orders
|
|
D_E090:
|
|
lda gameEndReason ; E090 non-zero once the game has ended
|
|
bne L_E0E7 ; E093 game over - nothing left to order
|
|
lda pauseState ; E095 pause / disconnect state
|
|
bne L_E0E7 ; E097 paused - freeze the opponent as well
|
|
|
|
; ----------------------------------------------------------------------
|
|
; $EE00-$EFFF is a third overlay layer inside this module. $8977 bit 7 says the drone-flight AI
|
|
; (track 29 sectors 4-5) is resident there; otherwise this module's own missile/target tail (track 35
|
|
; sectors 14-15) is. The two loads below swap between them as the AI's drone takes off and lands.
|
|
; ----------------------------------------------------------------------
|
|
lda D_8977 ; E099 drone timer: bit 7 = the drone-flight overlay is resident at $EE00
|
|
bpl L_E0C1 ; E09C our own tail is resident - take the normal path
|
|
lda droneControlFlags ; E09E drone control flags
|
|
D_E0A1:
|
|
and #$01 ; E0A1 bit 0 = a drone-move command is in force, i.e. a drone is airborne
|
|
D_E0A3:
|
|
beq L_E0AB ; E0A3 drone gone - swap our own tail back in
|
|
L_E0A5:
|
|
jsr targetEnemyComcenWithMissile; E0A5 still flying: call the drone-flight AI now loaded at $EE00 (track 29 s4-5, NOT the code assembled at $EE00 in this file)
|
|
D_E0A8:
|
|
jmp L_E0E7 ; E0A8 done for this exchange
|
|
|
|
L_E0AB:
|
|
jsr setSubOverlayLoadDest ; E0AB point the fast loader at $EE00
|
|
ldy #$23 ; E0AE Y = track $23 = 35, this module's own track
|
|
lda #$02 ; E0B0 A = 2 sectors
|
|
ldx #$0E ; E0B2 X = first sector 14: the native $EE00-$EFFF tail
|
|
jsr loadSectors ; E0B4 reload it over the drone AI
|
|
lda #$FF ; E0B7 $FF = no drone target chosen
|
|
sta D_8976 ; E0B9 let chooseAiDroneTarget pick a fresh one
|
|
lda #$03 ; E0BC 3 exchanges of cool-down before the next launch
|
|
sta D_8977 ; E0BE storing 3 also clears bit 7, marking the drone overlay as gone
|
|
L_E0C1:
|
|
lda droneControlFlags ; E0C1 drone control flags
|
|
and #$01 ; E0C4 bit 0 = a drone is airborne
|
|
beq L_E0DC ; E0C6 no drone - just run the normal turn
|
|
lda #$FF ; E0C8 $FF marks the drone overlay resident and blocks planning
|
|
sta D_8977 ; E0CA store it in the drone timer
|
|
jsr setSubOverlayLoadDest ; E0CD point the fast loader at $EE00
|
|
ldy #$1D ; E0D0 Y = track $1D = 29
|
|
ldx #$04 ; E0D2 X = first sector 4
|
|
lda #$02 ; E0D4 A = 2 sectors: the drone-flight AI
|
|
jsr loadSectors ; E0D6 load it over our own tail
|
|
jmp L_E0A5 ; E0D9 and fly the drone straight away
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Normal exchange: tick the drone launch countdown, then move as many complete AI commands as fit out
|
|
; of the queue at $88A0 into the received-packet buffer $E020.
|
|
; ----------------------------------------------------------------------
|
|
L_E0DC:
|
|
lda D_8977 ; E0DC launch countdown
|
|
beq L_E0E4 ; E0DF already 0 - launchAiDrone may fire this exchange
|
|
dec D_8977 ; E0E1 otherwise tick it down by one exchange
|
|
L_E0E4:
|
|
jsr injectAiCommandsIntoPacket; E0E4 copy queued AI orders into the fabricated received packet
|
|
L_E0E7:
|
|
rts ; E0E7 return to sendOutgoingPacket
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; trainerUnitStepHook - Per-unit hook called at the end of processUnitStep ($4526) for currentUnit,
|
|
; i.e. once for every living unit of both sides every round. For one of the AI's own units (index >=
|
|
; 50) that has now scored both end zones (bits 3 and 4 of the type byte) it sets bit 7 of that unit's
|
|
; 'has penetrated' flag so the planner stops counting it as still at home; for the AI comcen (unit 99)
|
|
; it also samples the 'was hit' bit of its display flags into the current and the one-round-sticky
|
|
; comcen-hit flags. Everything else is passed to updateAiUnitTracking, the AI's observer.
|
|
; In: currentUnit $90FA, unitTypeTable $F76C, comcen display flags $FBB7, drone timer $8977
|
|
; Out: bit 7 of $8864+ordinal, $897B comcen hit this round, $897C comcen hit this or last round;
|
|
; otherwise calls $EACF
|
|
; Called from: processUnitStep $4526
|
|
; ----------------------------------------------------------------------
|
|
trainerUnitStepHook:
|
|
ldy currentUnit ; E0E8 the unit the engine has just stepped
|
|
lda unitTypeTable,y ; E0EB type byte: bit 7 = dead, bits 3/4 = end zones already scored, bits 0-2 = type
|
|
bmi L_E127 ; E0EE dead slot - nothing to observe
|
|
cpy #$32 ; E0F0 $32 = 50: units 0-49 are the human player's
|
|
bcc L_E11F ; E0F2 human unit - hand it to the observer
|
|
and #$18 ; E0F4 keep only the two zone-scored bits
|
|
L_E0F6:
|
|
cmp #$18 ; E0F6 both the near zone (bit 3, columns 10-19) and the far zone (bit 4, columns 0-9) scored?
|
|
bne L_E11F ; E0F8 not deep in the player's half yet
|
|
lda D_8832,y ; E0FA $8832+unit is $8864+ordinal: this AI unit's 'has penetrated' flag
|
|
ora #$80 ; E0FD mark it penetrated...
|
|
sta D_8832,y ; E0FF ...so computeTrainerGroupStats stops counting it as still at home
|
|
cpy #$63 ; E102 $63 = 99 = the AI's own comcen
|
|
bne L_E11F ; E104 any other AI unit - hand it to the observer
|
|
lda D_897B ; E106 this round's 'comcen was hit' flag...
|
|
sta D_897C ; E109 ...becomes the one-round-old sticky flag
|
|
ldx #$00 ; E10C assume 'not hit' this round
|
|
lda side1ComcenDisplayFlags ; E10E display flags of unit 99 ($FB54 array)
|
|
L_E111:
|
|
and #$02 ; E111 bit 1 is set by recordUnitHit ($3C1A) whenever the unit takes damage
|
|
beq L_E119 ; E113 not hit
|
|
dex ; E115 X = $FF = hit
|
|
stx D_897C ; E116 a fresh hit also re-arms the sticky flag
|
|
L_E119:
|
|
stx D_897B ; E119 record whether the comcen was hit this round
|
|
L_E11C:
|
|
jmp L_E127 ; E11C done with the comcen
|
|
|
|
L_E11F:
|
|
lda D_8977 ; E11F drone timer, bit 7 = the drone-flight overlay is resident, i.e. the AI's drone is airborne
|
|
bmi L_E127 ; E122 the AI suspends its unit observation for as long as its drone is in the air
|
|
jsr updateAiUnitTracking ; E124 track what this unit has seen and what it is shooting at
|
|
L_E127:
|
|
rts ; E127 back to processUnitStep
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setSubOverlayLoadDest - Points the fast loader at $EE00 (X = destination low byte, Y = high byte)
|
|
; and tail-jumps into setLoadDest. Used before each of the two sub-overlay loads in
|
|
; buildTrainerReplyPacket.
|
|
; In: none
|
|
; Out: fast-loader destination = $EE00
|
|
; Called from: $E0AB and $E0CD
|
|
; ----------------------------------------------------------------------
|
|
setSubOverlayLoadDest:
|
|
ldx #$00 ; E128 destination low byte = $00
|
|
ldy #$EE ; E12A destination high byte = $EE
|
|
jmp setLoadDest ; E12C hand $EE00 to the fast loader's setLoadDest
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pickTrainerRallyPoint - Chooses the AI's current objective: a cell scattered at random around the
|
|
; human comcen (unit 49), stored as the rally column/row that every attacking group heads for.
|
|
; Refreshed every six rounds, and immediately whenever the human comcen becomes visible.
|
|
; In: side0ComcenCol $F671, side0ComcenRow $F6D5, the game RNG
|
|
; Out: $8969 rally column, $896A rally row
|
|
; Called from: planTrainerRound $E188; trainerSetupEC00 initTrainerAi $EC47 (once at game start)
|
|
; ----------------------------------------------------------------------
|
|
pickTrainerRallyPoint:
|
|
lda side0ComcenCol ; E12F column of the human comcen (unit 49)
|
|
jsr randomiseCoordinate ; E132 scatter it by a random offset
|
|
sta D_8969 ; E135 rally column the AI's groups head for
|
|
lda side0ComcenRow ; E138 row of the human comcen
|
|
jsr randomiseCoordinate ; E13B scatter that as well
|
|
sta D_896A ; E13E rally row
|
|
rts ; E141 done
|
|
|
|
|
|
; randomCoordinateOffsetTable - eight signed cell offsets, picked with nextGameRandom AND 7 by
|
|
; randomiseCoordinate and then scaled by 1, 2 or 4 according to how well the AI knows where the human
|
|
; comcen is.
|
|
randomCoordinateOffsetTable:
|
|
.byte $01 ; E142 offsets +1,-1,+1,+1,-1,-1,+2 (entries 0-6)
|
|
.byte $FF,$01,$01,$FF,$FF,$02 ; E143 ......
|
|
L_E149:
|
|
.byte $FE ; E149 entry 7: -2 cells
|
|
|
|
; ----------------------------------------------------------------------
|
|
; randomiseCoordinate - Returns A = base coordinate plus a random offset, clamped to 0..39. The base
|
|
; is written into the ADC immediate at $E164 and the offset comes from randomCoordinateOffsetTable
|
|
; indexed by nextGameRandom AND 7. The offset is used raw while the human comcen is actually visible
|
|
; (bit 7 of its energy byte), doubled once it has ever been seen ($896C bit 7) and quadrupled
|
|
; otherwise - so the AI aims accurately at a comcen it can see and wanders when it cannot. $E14D is an
|
|
; alternate entry that reuses the base already patched in.
|
|
; In: A = base coordinate 0-39, side0ComcenEnergy $F9F5, $896C 'comcen has been seen', the game RNG
|
|
; Out: A = coordinate 0..39; self-modifies the operand byte at $E164 (L_E163+1)
|
|
; Called from: pickTrainerRallyPoint $E132 and $E13B
|
|
; ----------------------------------------------------------------------
|
|
randomiseCoordinate:
|
|
sta L_E163+1 ; E14A patch the base coordinate into the ADC immediate at $E164
|
|
randomiseCoordinateSameBase:
|
|
jsr nextGameRandom ; E14D alternate entry that reuses the base already patched in (no caller in this build)
|
|
and #$07 ; E150 keep 3 bits: offset table index 0-7
|
|
tay ; E152 use it as the table index
|
|
lda randomCoordinateOffsetTable,y; E153 signed cell offset -2..+2
|
|
bit side0ComcenEnergy ; E156 energy byte of the human comcen; bit 7 = 'currently spotted'
|
|
bmi L_E162 ; E159 visible right now - aim with the raw offset
|
|
asl a ; E15B not visible: double the scatter
|
|
bit D_896C ; E15C sticky 'the comcen has been seen at some point' flag
|
|
bmi L_E162 ; E15F seen before - x2 scatter is enough
|
|
asl a ; E161 never seen at all - x4, so the AI just roams
|
|
L_E162:
|
|
clc ; E162 prepare the add
|
|
L_E163:
|
|
adc #$FF ; E163 operand byte patched at $E14A with the base coordinate
|
|
bpl L_E169 ; E165 result still positive - keep it
|
|
lda #$00 ; E167 clamp to the left/top edge of the map
|
|
L_E169:
|
|
cmp #$28 ; E169 $28 = 40 columns/rows on the battlefield
|
|
L_E16B:
|
|
bcc L_E16F ; E16B inside the map
|
|
lda #$27 ; E16D clamp to the last cell, 39
|
|
L_E16F:
|
|
rts ; E16F A = coordinate 0-39
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; planTrainerRound - Top-level planner of the solo trainer, run once per game round. It resets the
|
|
; per-round order counter, refreshes the rally point, works out this round's action budget from the AI
|
|
; comcen's energy and stun state, and then either spends the round on comcen repair (budget below 20)
|
|
; or evaluates the threat around its own comcen and decides between attack mode ($8963 = 0, plus a
|
|
; drone decision) and defend mode ($8963 = $FF). It then re-plans every AI unit whose replan clock has
|
|
; come due and finishes by walking the comcen forward behind the assault group or retreating it.
|
|
; In: gameEndReason $0BA8, comcen records $F9F5/$FA27/$F9C3, gameTypeOptions $0BA3, gameClock $91CB,
|
|
; the RNG and the whole $8800-$897C AI work block
|
|
; Out: $8965 budget, $8968 order count, $896B rally timer, $896C comcen-seen flag, $8960-$8963 threat
|
|
; result, commands appended to the queue at $88A0
|
|
; Called from: runTrainerRoundHook $E07E
|
|
; ----------------------------------------------------------------------
|
|
planTrainerRound:
|
|
lda gameEndReason ; E170 non-zero once the game has ended
|
|
beq L_E176 ; E173 still playing
|
|
rts ; E175 game over - no plan
|
|
|
|
L_E176:
|
|
sta D_8968 ; E176 A is 0 here: clear the count of commands queued this round
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The rally point (a cell near the human comcen) is refreshed every six rounds, or at once on the
|
|
; round the human comcen becomes visible.
|
|
; ----------------------------------------------------------------------
|
|
lda side0ComcenEnergy ; E179 human comcen energy byte, bit 7 = currently spotted
|
|
bpl L_E183 ; E17C not spotted this round
|
|
sta D_896C ; E17E spotted: bit 7 sticks in $896C, so randomiseCoordinate scatters less from now on...
|
|
bmi L_E188 ; E181 ...and the rally point is refreshed immediately
|
|
L_E183:
|
|
dec D_896B ; E183 otherwise count the rally-point refresh timer down
|
|
bpl L_E190 ; E186 not due yet
|
|
L_E188:
|
|
jsr pickTrainerRallyPoint ; E188 pick a new random cell near the human comcen
|
|
lda #$05 ; E18B 5 = next refresh in six rounds
|
|
sta D_896B ; E18D reload the timer
|
|
|
|
; ----------------------------------------------------------------------
|
|
; This round's action budget. Normally it is the AI comcen's own energy (0-50); with the COMCEN
|
|
; DAMAGE option on ($0BA3 bit 4) the comcen's stun counter modifies it. A budget under 20 means the
|
|
; AI can only afford to repair and refuel its comcen.
|
|
; ----------------------------------------------------------------------
|
|
L_E190:
|
|
lda side1ComcenEnergy ; E190 AI comcen energy (unit 99)
|
|
and #$3F ; E193 bits 0-5 hold the 0-50 energy points
|
|
sta D_8965 ; E195 that is this round's action budget
|
|
lda side1ComcenStun ; E198 AI comcen stun byte (unit 99 of the $F960 array)
|
|
and #$0F ; E19B low nibble = the STUN'D counter 0-15 shown on the console
|
|
sta D_8971 ; E19D remember how badly the comcen is stunned
|
|
lda gameTypeOptions ; E1A0 game options byte
|
|
and #$10 ; E1A3 bit 4 = the COMCEN DAMAGE option
|
|
beq L_E1C1 ; E1A5 damage rules off - use a flat budget
|
|
lda D_8971 ; E1A7 stun counter
|
|
beq L_E1C6 ; E1AA not stunned - keep the energy budget
|
|
cmp #$0F ; E1AC $0F = maximum stun
|
|
D_E1AE:
|
|
beq L_E1D0 ; E1AE completely out - the AI does nothing at all this round
|
|
cmp #$08 ; E1B0 stun 8-14
|
|
bcs L_E1C1 ; E1B2 heavy stun (8-14) takes the flat 50 budget while a light stun (1-7) is penalised 16 points below - counter-intuitive, possibly a slip for a branch to $E1CD
|
|
lda D_8965 ; E1B4 stun 1-7: take the energy budget...
|
|
sec ; E1B7 prepare the subtraction
|
|
sbc #$10 ; E1B8 ...and charge a 16 point penalty
|
|
bcc L_E1CD ; E1BA budget went negative - only repair the comcen
|
|
sta D_8965 ; E1BC store the reduced budget
|
|
bcs L_E1C6 ; E1BF always taken (the SBC above left C=1)
|
|
L_E1C1:
|
|
lda #$32 ; E1C1 $32 = 50 = a full budget
|
|
sta D_8965 ; E1C3 store it
|
|
L_E1C6:
|
|
lda D_8965 ; E1C6 this round's budget
|
|
cmp #$14 ; E1C9 $14 = 20 points
|
|
L_E1CB:
|
|
bcs L_E1D1 ; E1CB enough left to manoeuvre
|
|
L_E1CD:
|
|
jsr runAiComcenActions ; E1CD too little - spend the round on comcen repair and energy
|
|
L_E1D0:
|
|
rts ; E1D0 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Threat assessment around the AI's own comcen. assessThreatAroundAiComcen buckets the human units by
|
|
; compass direction and returns the overall threat, the deepest weapon-range penetration and the
|
|
; safest direction; those three become $8962, $8961 and $8960. The AI defends when an enemy weapon
|
|
; actually covers the comcen and it has either no assault group to rely on or has just been hit.
|
|
; ----------------------------------------------------------------------
|
|
L_E1D1:
|
|
lda side1ComcenCol ; E1D1 AI comcen column
|
|
sta cellColB ; E1D4 reference point B of the threat scan
|
|
lda side1ComcenRow ; E1D6 AI comcen row
|
|
sta cellRowB ; E1D9 reference point B of the threat scan
|
|
jsr assessThreatAroundAiComcen; E1DB bucket the human units around it into 8 compass directions
|
|
sta D_8962 ; E1DE A = overall threat score
|
|
sty D_8960 ; E1E1 Y = safest direction 0-7 (8 = nothing anywhere)
|
|
stx D_8961 ; E1E4 X = deepest weapon-range penetration, 0 = no enemy weapon covers the comcen
|
|
ldy #$00 ; E1E7 assume attack mode (0)
|
|
L_E1E9:
|
|
cpx #$00 ; E1E9 is any enemy weapon covering the comcen?
|
|
beq L_E1F9 ; E1EB no threat at all - attack
|
|
dey ; E1ED Y = $FF = defend
|
|
bit D_895D ; E1EE is an assault group selected? bit 7 = none
|
|
bmi L_E1F9 ; E1F1 no group to rely on - defend
|
|
bit D_897C ; E1F3 was the comcen hit this round or last?
|
|
bmi L_E1F9 ; E1F6 yes - defend
|
|
iny ; E1F8 threatened but coping - attack after all
|
|
L_E1F9:
|
|
sty D_8963 ; E1F9 0 = attack, $FF = defend
|
|
tya ; E1FC A = the mode just chosen
|
|
bne L_E202 ; E1FD defending - do not spend a drone
|
|
jsr chooseAiDroneTarget ; E1FF attacking - decide what the next drone should be sent at
|
|
L_E202:
|
|
jsr scanTrainerUnitsForOrders; E202 issue orders to every AI unit whose replan clock has come due
|
|
lda D_8968 ; E205 commands queued so far this round
|
|
cmp #$05 ; E208 5 is the per-round ceiling
|
|
L_E20A:
|
|
bcs L_E21E ; E20A budget spent - stop here
|
|
jsr updateFocusGroupCentre ; E20C refresh the assault group's id and centre
|
|
ldx D_8963 ; E20F attack (0) or defend ($FF)?
|
|
bmi retreatTrainerComcen ; E212 defend - pull the comcen back and recall the group
|
|
lda D_8965 ; E214 action budget
|
|
cmp #$20 ; E217 $20 = 32 points
|
|
bcs pushTrainerComcenForward; E219 plenty of energy - walk the comcen up behind the attack
|
|
jsr runAiComcenActions ; E21B low budget - repair the comcen instead
|
|
L_E21E:
|
|
rts ; E21E done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushTrainerComcenForward - Attack-mode tail of planTrainerRound (a branch target, not a JSR entry).
|
|
; If an assault group exists, the comcen is not stunned and its own replan clock has come due, it
|
|
; queues a MOVE for the AI comcen to a cell one step beyond the enemy's reach from the assault group's
|
|
; centre, and re-arms the comcen's replan clock to gameClock - 3. This is how the trainer walks its
|
|
; command centre up behind its attack.
|
|
; In: $895D assault group id, $8971 comcen stun, $8831 comcen replan clock, gameClock $91CB, $8960
|
|
; safest direction, $8961 enemy reach, $895E/$895F assault group centre
|
|
; Out: $8831 re-armed, one $80/$81 MOVE queued through orderAiUnitToCell
|
|
; Called from: planTrainerRound $E219 (branch)
|
|
; ----------------------------------------------------------------------
|
|
pushTrainerComcenForward:
|
|
ldy D_895D ; E21F assault group id, bit 7 = none selected
|
|
L_E222:
|
|
bmi L_E265 ; E222 no group to follow
|
|
lda D_8971 ; E224 comcen stun counter
|
|
bne L_E265 ; E227 stunned - it cannot move
|
|
lda D_8831 ; E229 the comcen's own replan clock
|
|
cmp gameClock ; E22C the clock counts down, so 'stamp >= clock' means the round has arrived
|
|
bcc L_E265 ; E22F not due yet
|
|
lda gameClock ; E231 current round
|
|
sec ; E234 prepare the subtraction
|
|
sbc #$03 ; E235 next comcen replan in 3 rounds
|
|
L_E237:
|
|
bcs L_E23B ; E237 no underflow
|
|
lda #$00 ; E239 clamp at round 0
|
|
L_E23B:
|
|
sta D_8831 ; E23B re-arm the comcen replan clock
|
|
ldx D_8960 ; E23E safest direction from the threat scan
|
|
lda dirToColDelta,x ; E241 column step -1/0/+1 for that direction
|
|
tax ; E244 X = column step for offsetCoordinateClamped
|
|
ldy D_8961 ; E245 how deep the enemy weapons reach
|
|
iny ; E248 one cell beyond that
|
|
lda msgOpponentUsingNonStandardGame; E249 $895E = assault group centre column (the label is a stale message-string name)
|
|
jsr offsetCoordinateClamped ; E24C column = centre + step * distance, clamped to 0..39
|
|
sta scratch18 ; E24F stash the target column
|
|
ldx D_8960 ; E251 same direction...
|
|
lda dirToRowDelta,x ; E254 ...row step -1/0/+1
|
|
tax ; E257 X = row step
|
|
L_E258:
|
|
lda D_895F ; E258 assault group centre row
|
|
jsr offsetCoordinateClamped ; E25B row = centre + step * distance, clamped
|
|
ldx scratch18 ; E25E X = column, A = row
|
|
L_E260:
|
|
ldy #$31 ; E260 $31 = AI ordinal 49 = unit 99, the AI comcen
|
|
jsr orderAiUnitToCell ; E262 queue the MOVE
|
|
L_E265:
|
|
rts ; E265 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; retreatTrainerComcen - Defend-mode tail of planTrainerRound (a branch target). It stamps the commit
|
|
; clock $896E with the current round - from then on groups are allowed to use their full combat value
|
|
; - and, unless the comcen is stunned, queues a MOVE that walks it away along the safest direction by
|
|
; as many cells as the enemy reaches. If the threat score still exceeds the recall threshold $897A, or
|
|
; the comcen was stunned, it recomputes the assault group's statistics and jumps to
|
|
; aimGroupAtOwnComcen to pull the whole group home.
|
|
; In: $8971 comcen stun, $8960 safest direction, $8961 enemy reach, $8962 threat score, $897A recall
|
|
; threshold, $895D assault group, comcen position $F6A3/$F707
|
|
; Out: $896E commit clock, $895A cleared, one comcen MOVE and possibly a full group recall
|
|
; Called from: planTrainerRound $E212 (branch)
|
|
; ----------------------------------------------------------------------
|
|
retreatTrainerComcen:
|
|
lda gameClock ; E266 current round
|
|
sta D_896E ; E269 stamp the commit clock: groups may now use their full strength
|
|
lda D_8971 ; E26C comcen stun counter
|
|
bne L_E29F ; E26F stunned - it cannot run, go straight to recalling the group
|
|
ldx D_8960 ; E271 safest direction
|
|
lda dirToColDelta,x ; E274 its column step
|
|
tax ; E277 X = column step
|
|
ldy D_8961 ; E278 run as far as the enemy reaches
|
|
lda side1ComcenCol ; E27B AI comcen column
|
|
jsr offsetCoordinateClamped ; E27E clamped destination column
|
|
sta scratch18 ; E281 stash it
|
|
L_E283:
|
|
ldx D_8960 ; E283 safest direction again
|
|
lda dirToRowDelta,x ; E286 its row step
|
|
tax ; E289 X = row step
|
|
lda side1ComcenRow ; E28A AI comcen row
|
|
sub_E28D:
|
|
jsr offsetCoordinateClamped ; E28D clamped destination row
|
|
ldx scratch18 ; E290 X = column
|
|
ldy #$31 ; E292 $31 = AI ordinal 49 = unit 99
|
|
jsr orderAiUnitToCell ; E294 queue the comcen MOVE
|
|
lda D_8962 ; E297 threat score from the scan
|
|
L_E29A:
|
|
cmp D_897A ; E29A recall threshold, set to 5 by the setup overlay at $EC44
|
|
bcc L_E2B6 ; E29D manageable - leave the assault group where it is
|
|
L_E29F:
|
|
lda D_895D ; E29F assault group id
|
|
cmp #$FF ; E2A2 $FF = no group at all
|
|
beq L_E2B6 ; E2A4 nothing to recall
|
|
and #$0F ; E2A6 strip the disable bit, leaving the group index 0-11
|
|
tay ; E2A8 Y = the group index for computeTrainerGroupStats
|
|
jsr computeTrainerGroupStats; E2A9 recompute its members, strength, centre and box
|
|
bcs L_E2B6 ; E2AC empty group - nothing to recall
|
|
lda #$00 ; E2AE pretend the group has no slow units...
|
|
sta D_895A ; E2B0 ...so issueTrainerGroupOrders does not double the replan delay - this is an urgent recall
|
|
jmp aimGroupAtOwnComcen ; E2B3 march the whole group home to the comcen
|
|
|
|
L_E2B6:
|
|
rts ; E2B6 done
|
|
|
|
|
|
; unitCombatValueTable - combat weight per unit type, indexed by the type bits 0-2 of $F76C. Summed
|
|
; into the 16-bit group value by computeTrainerGroupStats and also copied into the per-unit influence
|
|
; weight page $0464 by buildUnitThreatTables ($E7AF).
|
|
unitCombatValueTable:
|
|
.byte $02 ; E2B7 GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0
|
|
.byte $02,$03,$00,$00 ; E2B8 ....
|
|
|
|
; trainerUnitScanIndex - the loop counter of scanTrainerUnitsForOrders, kept as a byte inside the code
|
|
; stream rather than in the $8800 work block. Written at $E2BF and $E2EB, read at $E2CE.
|
|
trainerUnitScanIndex:
|
|
.byte $00 ; E2BC current AI ordinal being scanned, 49 down to 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scanTrainerUnitsForOrders - Walks the AI's 50 unit slots from ordinal 49 (unit 99, the comcen) down
|
|
; to 0, stopping as soon as four commands have been queued this round. A dead slot has its replan
|
|
; clock zeroed and is removed from every group; a slot whose replan clock has been reached by the
|
|
; down-counting game clock is handed to decideTrainerGroupAction, which re-plans the whole group that
|
|
; unit belongs to. The default replan urgency is reloaded with 5 before every unit.
|
|
; In: $8968 orders this round, gameClock $91CB, side1UnitTypeTable $F79E, replan clocks $8800, group
|
|
; ids $8832
|
|
; Out: $E2BC, $8800 and $8832 entries, $896D urgency; orders queued by the callee
|
|
; Called from: planTrainerRound $E202
|
|
; ----------------------------------------------------------------------
|
|
scanTrainerUnitsForOrders:
|
|
lda #$31 ; E2BD $31 = 49, the highest AI ordinal (unit 99)
|
|
sta trainerUnitScanIndex ; E2BF start the walk at the comcen and count down
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Loop over AI ordinals 49..0. It maintains the embedded counter at $E2BC and stops early once four
|
|
; commands have been queued.
|
|
; ----------------------------------------------------------------------
|
|
L_E2C2:
|
|
lda D_8968 ; E2C2 commands queued so far this round
|
|
cmp #$04 ; E2C5 4 is the ceiling for unit orders
|
|
bcs L_E2F0 ; E2C7 budget spent
|
|
lda #$05 ; E2C9 default replan urgency: re-plan again in at most 5 rounds
|
|
sta D_896D ; E2CB reload the default urgency before every unit
|
|
ldy trainerUnitScanIndex ; E2CE current AI ordinal 49..0
|
|
lda side1UnitTypeTable,y ; E2D1 type byte of unit ordinal+50
|
|
bpl L_E2E0 ; E2D4 still alive
|
|
lda #$00 ; E2D6 dead: clear its replan clock...
|
|
sta msgGetOpponentOnPhoneAnd,y; E2D8 ...at $8800+ordinal
|
|
lda #$FF ; E2DB ...and take it out of every group
|
|
sta D_8832,y ; E2DD $FF = belongs to no group
|
|
L_E2E0:
|
|
lda msgGetOpponentOnPhoneAnd,y; E2E0 this unit's replan clock
|
|
cmp gameClock ; E2E3 the clock counts down; stamp >= clock means the planned round has come
|
|
L_E2E6:
|
|
bcc L_E2EB ; E2E6 not due yet
|
|
jsr decideTrainerGroupAction; E2E8 re-plan the whole group this unit belongs to
|
|
L_E2EB:
|
|
dec trainerUnitScanIndex ; E2EB next ordinal down
|
|
bpl L_E2C2 ; E2EE loop until ordinal 0 has been done
|
|
L_E2F0:
|
|
rts ; E2F0 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; decideTrainerGroupAction - Decides what the AI group owning unit ordinal Y should do. Group 0 is the
|
|
; comcen's own escort and is only re-planned while the comcen is unstunned and no assault group is
|
|
; selected, in which case group 0 simply inherits the comcen's own ordered destination. For any group
|
|
; it caches the group flags, trims them to the low nibble and calls computeTrainerGroupStats; an empty
|
|
; group aborts. It then picks one of four behaviours: march home to the AI comcen (the home-defence
|
|
; group, or any group during a recall), charge the rally point when the group has already penetrated
|
|
; and made contact, form a defensive line on row 29 when it has not, or - for group 0 - run a free
|
|
; direction search out from the comcen.
|
|
; In: Y = AI unit ordinal, $8832 group ids, $8971 comcen stun, $895D assault group, comcen
|
|
; destination $F833/$F897, $8964 home group, $8966 flag cache, zp_1D members still at home
|
|
; Out: $88D2/$88DE group target, $893E group flags, $896D urgency, $8955-$8963 group scratch; ends in
|
|
; one of the order-issuing tails
|
|
; Called from: scanTrainerUnitsForOrders $E2E8
|
|
; ----------------------------------------------------------------------
|
|
decideTrainerGroupAction:
|
|
lda D_8832,y ; E2F1 group id of this AI unit
|
|
bne L_E31A ; E2F4 groups 1..11 are ordinary objectives
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Group 0 is the comcen's own escort. It is only worth re-planning while the comcen can still move
|
|
; and no assault group has been picked; then it just adopts the comcen's own destination as its
|
|
; target.
|
|
; ----------------------------------------------------------------------
|
|
lda D_8971 ; E2F6 group 0: is the comcen stunned?
|
|
bne L_E319 ; E2F9 stunned - leave the escort alone
|
|
ldx #$03 ; E2FB urgency 3 for the comcen's escort
|
|
stx D_896D ; E2FD urgency for group 0
|
|
ldx D_895D ; E300 is an assault group already selected?
|
|
bpl L_E319 ; E303 yes - group 0 is not the one to re-plan
|
|
lda side1ComcenDestCol ; E305 AI comcen ordered destination column
|
|
and #$3F ; E308 strip the flag bits, keep the 0-39 cell
|
|
sta D_88D2 ; E30A group 0 target column = wherever the comcen is heading
|
|
lda side1ComcenDestRow ; E30D AI comcen ordered destination row
|
|
and #$3F ; E310 strip the flag bits, keep the 0-39 cell
|
|
sta D_88DE ; E312 group 0 target row
|
|
lda #$00 ; E315 A = group id 0
|
|
beq L_E31A ; E317 always taken
|
|
L_E319:
|
|
rts ; E319 nothing to do for this group
|
|
|
|
L_E31A:
|
|
tay ; E31A Y = group id
|
|
L_E31B:
|
|
lda D_893E,y ; E31B flags of that group
|
|
sta D_8966 ; E31E keep a copy for the tests below (bit 6 = has made contact with the player's comcen)
|
|
and #$0F ; E321 clear bits 4-7, leaving the start-delay nibble
|
|
sta D_893E,y ; E323 write the trimmed flags back
|
|
jsr computeTrainerGroupStats; E326 members, strength, centre and bounding box of the group
|
|
bcc L_E32C ; E329 the group has members
|
|
rts ; E32B empty group - nothing to order
|
|
|
|
L_E32C:
|
|
ldy D_8955 ; E32C group id again
|
|
cpy D_8964 ; E32F the home-defence group chosen at setup ($EC39/$ECA3)
|
|
L_E332:
|
|
bne L_E356 ; E332 not the home group - decide between charging and digging in
|
|
lda #$02 ; E334 urgency 2 - re-plan the home guard often
|
|
sta D_896D ; E336 urgency for the home guard
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aimGroupAtOwnComcen - Internal tail (JMP target from retreatTrainerComcen $E2B3, fall-through from
|
|
; $E336): points the line target at the AI comcen's cell, then falls into the shared code at $E343
|
|
; that copies the group centre into the search origin and the line target into the wanted destination,
|
|
; and jumps to issueTrainerGroupOrders. Used to recall a group to defend the comcen.
|
|
; In: comcen position $F6A3/$F707, group centre zp_93/zp_92 (left there by computeTrainerGroupStats)
|
|
; Out: zp_27/zp_28 = search origin, zp_22/zp_24 = wanted group destination; tail-jumps to $E49E
|
|
; Called from: $E2B3 (JMP) and by falling through from decideTrainerGroupAction $E336
|
|
; ----------------------------------------------------------------------
|
|
aimGroupAtOwnComcen:
|
|
lda side1ComcenCol ; E339 AI comcen column
|
|
sta lineEndCol ; E33C line target column
|
|
lda side1ComcenRow ; E33E AI comcen row
|
|
sta lineEndRow ; E341 line target row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendGroupStraightToTarget - shared tail (also reached from $E3AF and $E3CD): move the group in a
|
|
; straight line to (lineEndCol, lineEndRow) with no threat search at all.
|
|
; ----------------------------------------------------------------------
|
|
sendGroupStraightToTarget:
|
|
lda lineCurCol ; E343 group centre column (set by computeTrainerGroupStats)
|
|
sta cellColB ; E345 search origin / reference column
|
|
lda lineCurRow ; E347 group centre row
|
|
sta cellRowB ; E349 search origin / reference row
|
|
lda lineEndCol ; E34B line target column
|
|
sta cellColA ; E34D wanted group destination column
|
|
lda lineEndRow ; E34F line target row
|
|
sta cellRowA ; E351 wanted group destination row
|
|
sub_E353:
|
|
jmp issueTrainerGroupOrders ; E353 issue the move orders for the whole group
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Not the home guard. A group that still has members which have not scored both end zones, and which
|
|
; has not yet made contact with the player's comcen (flags bit 6), falls back to a defensive line;
|
|
; anything else pushes on towards the rally point.
|
|
; ----------------------------------------------------------------------
|
|
L_E356:
|
|
bit D_8966 ; E356 cached group flags
|
|
bvs L_E35F ; E359 bit 6 set: the group has already made contact with the player's comcen
|
|
lda scratch1D ; E35B count of members that have not scored both end zones
|
|
bne aimGroupAtHomeLine ; E35D some are still at home - form a defensive line instead
|
|
L_E35F:
|
|
ldy D_8955 ; E35F group id
|
|
L_E362:
|
|
bne L_E37A ; E362 groups 1..11 head for the rally point
|
|
lda D_8963 ; E364 group 0 (the comcen escort): attack or defend?
|
|
bmi L_E379 ; E367 defending - the escort stays where it is
|
|
lda D_8960 ; E369 safest direction around the comcen
|
|
sta scratch19 ; E36C use it as the fan heading
|
|
jsr buildDirectionFan ; E36E lay out the 8 candidate directions in $8896-$889D
|
|
lda #$05 ; E371 try to advance 5 cells
|
|
sta D_895B ; E373 wanted advance distance for the search
|
|
jmp findTrainerAdvanceCell ; E376 run the threat-aware path search
|
|
|
|
L_E379:
|
|
rts ; E379 nothing to do
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Groups 1..11: head for the rally point. How boldly depends on whether the human comcen has been
|
|
; found, how much of the game is left and whether this group has already made contact with it.
|
|
; ----------------------------------------------------------------------
|
|
L_E37A:
|
|
lda D_8969 ; E37A rally column near the human comcen
|
|
sta lineEndCol ; E37D line target column
|
|
lda D_896A ; E37F rally row
|
|
sta lineEndRow ; E382 line target row
|
|
L_E384:
|
|
lda side0ComcenEnergy ; E384 human comcen energy byte, bit 7 = spotted
|
|
bmi L_E392 ; E387 spotted
|
|
lda gameClock ; E389 rounds left in the game
|
|
cmp #$28 ; E38C $28 = 40 rounds
|
|
bcs searchGroupPathToTarget ; E38E plenty of time left - just search for a safe advance
|
|
bcc L_E39F ; E390 late in the game - be twice as bold
|
|
L_E392:
|
|
bit D_8966 ; E392 cached group flags
|
|
bvc searchGroupPathToTarget ; E395 no contact with the comcen yet - just search
|
|
lda D_893E,y ; E397 group flags
|
|
ora #$80 ; E39A bit 7 = this group has the spotted comcen in its sights
|
|
sta D_893E,y ; E39C store the flags back
|
|
L_E39F:
|
|
asl D_8956 ; E39F double the group's combat value, low byte...
|
|
D_E3A2:
|
|
rol D_8957 ; E3A2 ...and high byte, so the risk test accepts twice as much opposition
|
|
bit D_8966 ; E3A5 cached group flags
|
|
bvc searchGroupPathToTarget ; E3A8 no contact - fall back to the cautious search
|
|
lda D_8976 ; E3AA the unit the next drone is aimed at
|
|
cmp #$31 ; E3AD $31 = 49 = the human comcen
|
|
beq sendGroupStraightToTarget; E3AF the drone is already going for the comcen - the group beelines to the rally point too
|
|
jsr isPathToPlayerComcenClear; E3B1 is the straight line to the human comcen free of living enemy combat units?
|
|
bcc searchGroupPathToTarget ; E3B4 blocked - fall back to the cautious search
|
|
sub_E3B6:
|
|
ldy D_8955 ; E3B6 group id
|
|
sub_E3B9:
|
|
lda D_8902,y ; E3B9 number of members
|
|
sub_E3BC:
|
|
cmp #$04 ; E3BC 4 or more
|
|
bcc L_E3C8 ; E3BE too small to commit
|
|
lda D_893E,y ; E3C0 group flags
|
|
ora #$20 ; E3C3 bit 5 = committed to the all-out attack (chooseAiDroneTarget weighs this doubled)
|
|
sta D_893E,y ; E3C5 store the committed flag back
|
|
L_E3C8:
|
|
lda #$03 ; E3C8 urgency 3
|
|
sta D_896D ; E3CA re-plan this group again in 3 rounds at the latest
|
|
jmp sendGroupStraightToTarget; E3CD charge straight at the rally point
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aimGroupAtHomeLine - Internal branch target used when the group still has members that have not
|
|
; reached the player's end zones and has no contact with his comcen: the target column stays where the
|
|
; group already is and the target row becomes 29 plus the group's own vertical extents, i.e. a
|
|
; defensive line inside the AI's own half. While the commit clock $896E has not been passed the
|
|
; group's combat value is also capped at 9, which makes the risk test reject anything but a very quiet
|
|
; cell.
|
|
; In: group centre zp_93, group spans $891A/$8932, commit clock $896E, gameClock $91CB, group value
|
|
; $8956/$8957
|
|
; Out: zp_94/zp_95 target cell, $8956/$8957 possibly capped at 9; falls into the path search at $E400
|
|
; Called from: decideTrainerGroupAction $E35D (branch)
|
|
; ----------------------------------------------------------------------
|
|
aimGroupAtHomeLine:
|
|
lda lineCurCol ; E3D0 group centre column
|
|
sta lineEndCol ; E3D2 hold the column, only the row changes
|
|
lda #$1D ; E3D4 $1D = row 29
|
|
ldy D_8955 ; E3D6 group id
|
|
clc ; E3D9 prepare the two adds
|
|
adc D_891A,y ; E3DA plus the group's reach above its centre
|
|
adc D_8932,y ; E3DD plus its reach below, so the whole formation box sits inside the AI's own half
|
|
sta lineEndRow ; E3E0 defensive line row
|
|
lda D_896E ; E3E2 commit clock, set at setup ($ED85) and re-stamped by every comcen retreat
|
|
cmp gameClock ; E3E5 the clock counts down: stamp >= clock means the commit round has passed
|
|
bcs searchGroupPathToTarget ; E3E8 committed - let the group use its real strength
|
|
L_E3EA:
|
|
lda D_8957 ; E3EA group combat value, high byte
|
|
bne L_E3F6 ; E3ED over 255 - definitely cap it
|
|
lda D_8956 ; E3EF low byte
|
|
L_E3F2:
|
|
cmp #$09 ; E3F2 9 is the cautious ceiling
|
|
bcc searchGroupPathToTarget ; E3F4 already weak enough
|
|
L_E3F6:
|
|
lda #$00 ; E3F6 cap the value at 9 so the risk test only accepts very quiet cells
|
|
sta D_8957 ; E3F8 high byte of the capped value = 0
|
|
lda #$09 ; E3FB 9 = the cautious ceiling
|
|
sta D_8956 ; E3FD low byte of the capped value
|
|
|
|
; ----------------------------------------------------------------------
|
|
; searchGroupPathToTarget - shared entry (branch target from $E38E, $E395, $E3A8, $E3B4, $E3E8 and
|
|
; $E3F4): measure how far the target is, build the eight-way candidate fan around the heading towards
|
|
; it and run the threat-aware search. When the group already stands on its target there is no
|
|
; heading, so the search is skipped and the members are merely re-stamped with a two-round replan
|
|
; delay.
|
|
; ----------------------------------------------------------------------
|
|
searchGroupPathToTarget:
|
|
lda lineCurCol ; E400 group centre column
|
|
sta cellColB ; E402 search origin column
|
|
lda lineCurRow ; E404 group centre row
|
|
sta cellRowB ; E406 search origin row
|
|
lda lineEndCol ; E408 target column
|
|
sta cellColA ; E40A point A of the distance measure
|
|
lda lineEndRow ; E40C target row
|
|
sta cellRowA ; E40E point A row
|
|
jsr cellDistance ; E410 distance from the group to its target, 0-16 cells
|
|
sta D_895B ; E413 that is how far the group would like to advance
|
|
D_E416:
|
|
jsr buildDirectionFanTowardTarget; E416 heading towards the target plus the 8 candidate directions in $8896-$889D
|
|
lda scratch19 ; E419 heading 0-7, or $FF when the group already stands on the target
|
|
bpl findTrainerAdvanceCell ; E41B valid heading - search for a safe cell along it
|
|
lda #$00 ; E41D already there: zero movement delta
|
|
sta scratch2A ; E41F no column shift
|
|
sta scratch2B ; E421 no row shift
|
|
lda #$02 ; E423 replan again in 2 rounds
|
|
sta scanListIndex ; E425 zp_29 = replan delay in rounds
|
|
jmp L_E4C7 ; E427 skip the search and just re-stamp the members
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; findTrainerAdvanceCell - Threat-aware path search. It first builds the per-unit influence map
|
|
; (radius in $0400, weight in $0464) with buildUnitThreatTables. Then, for each of the eight direction
|
|
; ranks in the fan at $8896, it steps outward from the search origin in jumps of two cells, testing
|
|
; every candidate with isGroupDestinationTooRisky; it keeps extending while the cell is acceptable, up
|
|
; to the wanted advance distance $895B or the per-rank cap in maxAdvanceByRankTable, and moves on to
|
|
; the next rank as soon as a candidate is rejected before either limit is reached. If no rank works
|
|
; the group stays put (direction 0, distance 0). The winning direction and distance are finally turned
|
|
; into the absolute target cell zp_22/zp_24 and the code falls into issueTrainerGroupOrders.
|
|
; In: zp_27/zp_28 search origin, $8896-$889D direction fan, $895B wanted distance, $8956/$8957 group
|
|
; strength
|
|
; Out: zp_22/zp_24 = chosen target cell, zp_23 = distance, zp_26 = direction, $0400/$0464 influence
|
|
; map; falls into $E49E
|
|
; Called from: decideTrainerGroupAction $E376 (JMP) and $E41B (branch)
|
|
; ----------------------------------------------------------------------
|
|
findTrainerAdvanceCell:
|
|
jsr buildUnitThreatTables ; E42A build the per-unit influence map in the $0400/$0464 scratch pages
|
|
D_E42D:
|
|
lda #$00 ; E42D start with direction rank 0, the best one in the fan
|
|
D_E42F:
|
|
sta cellColA ; E42F zp_22 doubles as the rank index for the length of the search
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Outer loop over the eight direction ranks; inner loop at $E435 pushes two cells further out at a
|
|
; time along the current rank while the destination stays acceptable.
|
|
; ----------------------------------------------------------------------
|
|
L_E431:
|
|
lda #$00 ; E431 distance reached along this rank = 0
|
|
sta scratch23 ; E433 zp_23 = distance reached along this rank
|
|
L_E435:
|
|
ldy cellColA ; E435 current rank
|
|
ldx D_8896,y ; E437 the direction the fan put in that rank
|
|
stx scanRowCount ; E43A keep it as the candidate heading
|
|
lda scratch23 ; E43C distance reached so far
|
|
clc ; E43E prepare the add
|
|
adc #$02 ; E43F probe two cells further out
|
|
sta scratch2A ; E441 candidate distance
|
|
tay ; E443 Y = distance for the risk test
|
|
D_E444:
|
|
jsr isGroupDestinationTooRisky; E444 too much enemy influence on the formation box at that spot?
|
|
D_E447:
|
|
bcs L_E464 ; E447 rejected - stop extending this rank
|
|
ldx scanRowCount ; E449 candidate heading
|
|
ldy scratch2A ; E44B candidate distance
|
|
jsr offsetPointInDirection ; E44D does the cell itself stay on the 40x40 map?
|
|
bcs L_E464 ; E450 off the map - stop extending
|
|
lda scratch2A ; E452 accept the new distance
|
|
sta scratch23 ; E454 remember it as the distance reached on this rank
|
|
cmp D_895B ; E456 as far as the group wanted to go?
|
|
D_E459:
|
|
bcs L_E480 ; E459 yes - take it
|
|
D_E45B:
|
|
ldy cellColA ; E45B rank
|
|
cmp maxAdvanceByRankTable,y ; E45D per-rank distance cap: 5,5,5,3,3,1,1,1
|
|
bcs L_E480 ; E460 cap reached - take it
|
|
bcc L_E435 ; E462 keep pushing outward along this rank
|
|
L_E464:
|
|
lda scratch23 ; E464 distance already accepted on this rank
|
|
cmp D_895B ; E466 far enough anyway?
|
|
bcs L_E480 ; E469 yes - take it
|
|
ldy cellColA ; E46B rank
|
|
cmp maxAdvanceByRankTable,y ; E46D per-rank cap
|
|
bcs L_E480 ; E470 at the cap - take it
|
|
inc cellColA ; E472 try the next-best direction
|
|
lda cellColA ; E474 the new rank index
|
|
cmp #$08 ; E476 the fan holds 8 candidate directions
|
|
bcc L_E431 ; E478 still ranks left to try
|
|
lda #$00 ; E47A nothing worked at all: stay put
|
|
sta scratch23 ; E47C distance 0...
|
|
L_E47E:
|
|
sta scanRowCount ; E47E ...and direction 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Turn the chosen (direction, distance) pair back into an absolute cell and fall into the order
|
|
; issuer.
|
|
; ----------------------------------------------------------------------
|
|
L_E480:
|
|
ldx scanRowCount ; E480 chosen heading
|
|
lda dirToColDelta,x ; E482 its column step -1/0/+1
|
|
tax ; E485 X = column step
|
|
L_E486:
|
|
ldy scratch23 ; E486 chosen distance
|
|
lda cellColB ; E488 search origin column
|
|
jsr offsetCoordinateClamped ; E48A target column = origin + step * distance, clamped to 0..39
|
|
sta cellColA ; E48D chosen target column
|
|
ldx scanRowCount ; E48F chosen heading
|
|
lda dirToRowDelta,x ; E491 its row step -1/0/+1
|
|
tax ; E494 X = row step
|
|
ldy scratch23 ; E495 chosen distance
|
|
lda cellRowB ; E497 search origin row
|
|
jsr offsetCoordinateClamped ; E499 target row, clamped
|
|
sta cellRowA ; E49C chosen target row - fall into the order issuer
|
|
|
|
; ----------------------------------------------------------------------
|
|
; issueTrainerGroupOrders - Issues the move orders for the group in $8955. It first stamps every
|
|
; member's replan clock with the current round so the scan loop cannot pick the group again, clips the
|
|
; wanted target with clampGroupTargetToMap (which also yields the movement delta zp_2A/zp_2B relative
|
|
; to the group's average ordered destination), derives a replan delay from the distance to the target
|
|
; (doubled when the group contains GRUNTs or BOOMERs, floored at 4 rounds and capped by the urgency
|
|
; $896D), and then walks all 50 AI slots giving every member a MOVE to its own destination shifted by
|
|
; the group delta - so the whole formation translates without changing shape. A member that also
|
|
; belongs to a real in-game group propagates its new replan clock to all its formation mates.
|
|
; In: $8955 current group, zp_22/zp_24 wanted target, gameClock $91CB, $895A slow-unit flag, $896D
|
|
; urgency, $8832 group ids, destinations $F802/$F866, unit flags $F73A
|
|
; Out: replan clocks $8800, group target $88D2/$88DE, commands appended to the queue at $88A0, zp_29
|
|
; delay, zp_2A/zp_2B delta
|
|
; Called from: aimGroupAtOwnComcen $E353 (JMP) and by falling through from findTrainerAdvanceCell
|
|
; $E49C
|
|
; Stamp every member of the group with the current round, so that scanTrainerUnitsForOrders will not
|
|
; re-plan the same group again later in this pass.
|
|
; ----------------------------------------------------------------------
|
|
issueTrainerGroupOrders:
|
|
ldy #$31 ; E49E walk AI ordinals 49..0
|
|
L_E4A0:
|
|
lda D_8832,y ; E4A0 group id of this unit
|
|
cmp D_8955 ; E4A3 the group being ordered
|
|
bne L_E4AE ; E4A6 not a member
|
|
lda gameClock ; E4A8 current round
|
|
sta msgGetOpponentOnPhoneAnd,y; E4AB stamp the member's replan clock ($8800+ordinal)
|
|
L_E4AE:
|
|
dey ; E4AE next ordinal
|
|
bpl L_E4A0 ; E4AF loop over all 50 slots
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Work out where the formation may actually stand and how long it should be left alone afterwards.
|
|
; ----------------------------------------------------------------------
|
|
jsr clampGroupTargetToMap ; E4B1 clip the target so the whole formation box stays on the map; leaves the delta in zp_2A/zp_2B
|
|
jsr cellDistance ; E4B4 distance from the group centre to the clipped target
|
|
bit D_895A ; E4B7 does the group contain GRUNTs or BOOMERs?
|
|
bpl L_E4BD ; E4BA all fast - use the raw distance
|
|
asl a ; E4BC slow group - leave it alone twice as long
|
|
L_E4BD:
|
|
sta scanListIndex ; E4BD replan delay in rounds
|
|
cmp #$04 ; E4BF minimum 4 rounds
|
|
bcs L_E4C7 ; E4C1 already at least 4
|
|
lda #$04 ; E4C3 floor the delay at 4
|
|
sta scanListIndex ; E4C5 store the floored delay
|
|
L_E4C7:
|
|
lda D_896D ; E4C7 urgency of this group (2 home guard, 3 comcen escort / committed attack, 5 default)
|
|
cmp scanListIndex ; E4CA is the delay longer than the urgency allows?
|
|
bcs L_E4D0 ; E4CC no
|
|
sta scanListIndex ; E4CE cap the delay at the urgency
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Second pass over all 50 AI slots: every member gets a MOVE to its own ordered destination plus the
|
|
; group delta, and a new replan clock of gameClock - delay. zp_22 is reused here as the loop counter.
|
|
; ----------------------------------------------------------------------
|
|
L_E4D0:
|
|
lda #$31 ; E4D0 walk AI ordinals 49..0 again
|
|
sub_E4D2:
|
|
sta cellColA ; E4D2 zp_22 is the loop counter for this pass
|
|
L_E4D4:
|
|
ldy cellColA ; E4D4 current ordinal
|
|
lda D_8832,y ; E4D6 its group id
|
|
cmp D_8955 ; E4D9 the group being ordered
|
|
bne L_E529 ; E4DC not a member
|
|
lda msgGetOpponentOnPhoneAnd,y; E4DE its replan clock
|
|
cmp gameClock ; E4E1 must still carry the stamp written above
|
|
bcc L_E529 ; E4E4 a formation mate already re-planned it
|
|
lda side1UnitDestColTable,y ; E4E6 the unit's own ordered destination column
|
|
and #$3F ; E4E9 drop the flag bits, keep the 0-39 cell
|
|
clc ; E4EB prepare the add
|
|
adc scratch2A ; E4EC shift it by the group's column delta
|
|
tax ; E4EE X = target column
|
|
lda side1UnitDestRowTable,y ; E4EF its ordered destination row
|
|
and #$3F ; E4F2 drop the flag bits, keep the 0-39 cell
|
|
clc ; E4F4 prepare the add
|
|
adc scratch2B ; E4F5 shift it by the group's row delta
|
|
jsr orderAiUnitToCell ; E4F7 queue $80 MOVE (or $81 MOVE GROUP) for this unit
|
|
L_E4FA:
|
|
ldy cellColA ; E4FA ordinal again
|
|
lda gameClock ; E4FC current round
|
|
sec ; E4FF prepare the subtraction
|
|
sbc scanListIndex ; E500 next replan in 'delay' rounds
|
|
sub_E502:
|
|
bcs L_E506 ; E502 no underflow
|
|
lda #$00 ; E504 clamp at round 0
|
|
L_E506:
|
|
sta msgGetOpponentOnPhoneAnd,y; E506 new replan clock for this unit
|
|
sta scratch18 ; E509 keep a copy for the formation mates
|
|
lda side1UnitFlagsTable,y ; E50B the unit's authoritative flags byte
|
|
and #$7C ; E50E bits 2-6: in-game group id (2-4), side (5) and the 'in a group' bit (6)
|
|
cmp #$40 ; E510 bit 6 clear means the unit is not in a player-style group
|
|
bcc L_E529 ; E512 no formation mates to update
|
|
L_E514:
|
|
sta scratch19 ; E514 the group key to match
|
|
ldx #$31 ; E516 scan all 50 AI ordinals
|
|
L_E518:
|
|
lda side1UnitFlagsTable,x ; E518 flags of that unit
|
|
sub_E51B:
|
|
and #$7C ; E51B same mask
|
|
cmp scratch19 ; E51D same in-game group?
|
|
bne L_E526 ; E51F no
|
|
lda scratch18 ; E521 the replan clock just computed
|
|
sta msgGetOpponentOnPhoneAnd,x; E523 give the formation mate the same clock, so the group is not ordered twice
|
|
L_E526:
|
|
dex ; E526 next ordinal
|
|
bpl L_E518 ; E527 loop over all 50 slots
|
|
L_E529:
|
|
dec cellColA ; E529 next ordinal
|
|
bmi L_E530 ; E52B all 50 slots done
|
|
L_E52D:
|
|
jmp L_E4D4 ; E52D keep going (out of branch range)
|
|
|
|
L_E530:
|
|
rts ; E530 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computeTrainerGroupStats - Gathers the statistics of AI group Y. It scans the 50 AI slots and, for
|
|
; every living member, sums the per-type combat weights into the 16-bit group value, notes whether the
|
|
; group contains slow units (GRUNT or BOOMER), counts the members that have not yet scored both end
|
|
; zones, and tracks the minimum and maximum of the members' ordered destinations. computeGroupCentroid
|
|
; then supplies the member count and the average current and ordered positions; those become the group
|
|
; centre, the group size and the four formation-box offsets (reach left, right, up, down). Returns C=1
|
|
; for an empty group.
|
|
; In: Y = group id, $8832 group ids, types $F79E, destinations $F802/$F866, penetration flags $8864,
|
|
; $895D assault group, $8895 comcen penetration flag
|
|
; Out: $8955 current group, $8956/$8957 value, $895A slow flag, $8902 size, $88EA/$88F6 centre,
|
|
; $890E/$8926/$891A/$8932 box, zp_1D still-at-home count, zp_93/zp_92 = centre, zp_1E/zp_1F =
|
|
; average destination; C=1 if the group is empty
|
|
; Called from: retreatTrainerComcen $E2A9 and decideTrainerGroupAction $E326
|
|
; ----------------------------------------------------------------------
|
|
computeTrainerGroupStats:
|
|
sty D_8955 ; E531 remember which group is being measured
|
|
sub_E534:
|
|
ldx #$00 ; E534 0 for the accumulators
|
|
stx scratch1D ; E536 members that have not scored both end zones
|
|
stx D_8956 ; E538 combat value low byte
|
|
stx D_8957 ; E53B combat value high byte
|
|
stx cellRowA ; E53E zp_24 = highest destination column seen (max tracker starts at 0)
|
|
sub_E540:
|
|
stx scanColCount ; E540 zp_25 = highest destination row seen
|
|
stx D_895A ; E542 no slow units found yet
|
|
dex ; E545 $FF for the minimum trackers
|
|
stx cellColA ; E546 zp_22 = lowest destination column seen
|
|
stx scratch23 ; E548 zp_23 = lowest destination row seen
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Loop over AI ordinals 49..0 accumulating strength, the slow-unit flag, the still-at-home count and
|
|
; the bounding box of the members' ordered destinations.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$31 ; E54A walk AI ordinals 49..0
|
|
L_E54C:
|
|
lda D_8832,y ; E54C group id of this unit
|
|
cmp D_8955 ; E54F the group being measured
|
|
bne L_E5A2 ; E552 not a member
|
|
lda side1UnitTypeTable,y ; E554 type byte
|
|
bmi L_E5A2 ; E557 dead
|
|
and #$07 ; E559 bits 0-2 = unit type 0-4
|
|
tax ; E55B X = unit type, used as the index into unitCombatValueTable
|
|
beq L_E562 ; E55C type 0 GRUNT is slow
|
|
cpx #$02 ; E55E type 2 BOOMER is slow too
|
|
bne L_E567 ; E560 RIDER, SPY and COMCEN are not
|
|
L_E562:
|
|
lda #$FF ; E562 $FF = the group contains slow units
|
|
sta D_895A ; E564 flag the group as slow
|
|
L_E567:
|
|
lda unitCombatValueTable,x ; E567 combat weight of this type (2,2,3,0,0)
|
|
clc ; E56A prepare the add
|
|
L_E56B:
|
|
adc D_8956 ; E56B add it into the 16-bit group value
|
|
sta D_8956 ; E56E new low byte
|
|
lda D_8957 ; E571 high byte
|
|
sub_E574:
|
|
adc #$00 ; E574 carry into the high byte
|
|
sta D_8957 ; E576 new high byte
|
|
lda D_8864,y ; E579 this unit's 'has scored both end zones' flag
|
|
bmi L_E580 ; E57C already deep in the player's half
|
|
inc scratch1D ; E57E still at home - count it
|
|
L_E580:
|
|
lda side1UnitDestColTable,y ; E580 its ordered destination column
|
|
and #$3F ; E583 strip the flag bits
|
|
cmp cellColA ; E585 below the running minimum?
|
|
bcs L_E58B ; E587 no
|
|
sta cellColA ; E589 new leftmost destination
|
|
L_E58B:
|
|
cmp cellRowA ; E58B above the running maximum?
|
|
bcc L_E591 ; E58D no
|
|
sta cellRowA ; E58F new rightmost destination
|
|
L_E591:
|
|
lda side1UnitDestRowTable,y ; E591 its ordered destination row
|
|
and #$3F ; E594 strip the flag bits
|
|
cmp scratch23 ; E596 below the running minimum?
|
|
bcs L_E59C ; E598 no
|
|
sta scratch23 ; E59A new topmost destination
|
|
L_E59C:
|
|
cmp scanColCount ; E59C above the running maximum?
|
|
bcc L_E5A2 ; E59E no
|
|
sta scanColCount ; E5A0 new bottom-most destination
|
|
L_E5A2:
|
|
dey ; E5A2 next ordinal
|
|
bpl L_E54C ; E5A3 loop over all 50 slots
|
|
ldy D_8955 ; E5A5 group id
|
|
cpy D_895D ; E5A8 is this the assault group?
|
|
bne L_E5B4 ; E5AB no
|
|
lda D_8895 ; E5AD $8895 = $8864+49 = the AI comcen's own 'has penetrated' flag
|
|
bmi L_E5B4 ; E5B0 the comcen is already deep in enemy ground
|
|
inc scratch1D ; E5B2 otherwise the assault group counts as one more unit still at home
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Average the members' positions and destinations, then store the centre, the size and the four
|
|
; formation-box offsets that the risk test and the target clipper work with.
|
|
; ----------------------------------------------------------------------
|
|
L_E5B4:
|
|
ldy #$31 ; E5B4 scan ordinals 49..0
|
|
sub_E5B6:
|
|
lda D_8955 ; E5B6 group id to match
|
|
jsr computeGroupCentroid ; E5B9 average position and average ordered destination of the members
|
|
D_E5BC:
|
|
sty scratch18 ; E5BC Y = average row
|
|
D_E5BE:
|
|
ldy D_8955 ; E5BE group id
|
|
L_E5C1:
|
|
lda scratch1C ; E5C1 member count
|
|
sub_E5C3:
|
|
sta D_8902,y ; E5C3 group size
|
|
bne L_E5CA ; E5C6 the group has members
|
|
sec ; E5C8 C=1 = empty group
|
|
rts ; E5C9 nothing more to do
|
|
|
|
L_E5CA:
|
|
txa ; E5CA X = average column
|
|
sta D_88EA,y ; E5CB group centre column
|
|
sta lineCurCol ; E5CE and the line walker's current column, for the callers
|
|
lda scratch18 ; E5D0 average row
|
|
sta D_88F6,y ; E5D2 group centre row
|
|
L_E5D5:
|
|
sta lineCurRow ; E5D5 and the line walker's current row
|
|
lda scratch1E ; E5D7 average ordered destination column
|
|
sec ; E5D9 prepare the subtraction
|
|
sbc cellColA ; E5DA minus the leftmost destination
|
|
sta D_890E,y ; E5DC = how far the formation reaches left of its centre
|
|
lda cellRowA ; E5DF rightmost destination
|
|
sec ; E5E1 prepare the subtraction
|
|
sbc scratch1E ; E5E2 minus the average
|
|
L_E5E4:
|
|
clc ; E5E4 clear carry for the +1 below
|
|
adc #$01 ; E5E5 +1 so the box includes the edge cell itself
|
|
sta D_8926,y ; E5E7 = how far the formation reaches right
|
|
lda scratch1F ; E5EA average ordered destination row
|
|
sec ; E5EC prepare the subtraction
|
|
sbc scratch23 ; E5ED minus the topmost destination
|
|
sta D_891A,y ; E5EF = reach above the centre
|
|
lda scanColCount ; E5F2 bottom-most destination
|
|
sec ; E5F4 prepare the subtraction
|
|
sbc scratch1F ; E5F5 minus the average
|
|
clc ; E5F7 clear carry for the +1 below
|
|
L_E5F8:
|
|
adc #$01 ; E5F8 +1 for the edge cell
|
|
sta D_8932,y ; E5FA = reach below the centre
|
|
clc ; E5FD C=0 = the group exists
|
|
rts ; E5FE done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clampGroupTargetToMap - Clips the wanted group centre in zp_22/zp_24 so that the whole formation box
|
|
; (reach left/right in $890E/$8926 and up/down in $891A/$8932) still fits inside the 40x40 map, stores
|
|
; the clipped cell as the group's target and returns the movement delta relative to the group's
|
|
; average ordered destination in zp_2A (columns, also kept in $8958) and zp_2B (rows).
|
|
; In: zp_22/zp_24 wanted centre, zp_1E/zp_1F average ordered destination, $8955 current group, the
|
|
; four box offsets
|
|
; Out: zp_22/zp_24 clipped, zp_2A/zp_2B delta, $8958, $88D2/$88DE group target
|
|
; Called from: issueTrainerGroupOrders $E4B1; trainerSetupEC00 shiftAiGroupDestinations $EF00
|
|
; ----------------------------------------------------------------------
|
|
clampGroupTargetToMap:
|
|
lda cellColA ; E5FF wanted target column
|
|
sec ; E601 prepare the subtraction
|
|
ldy D_8955 ; E602 group id
|
|
sbc D_890E,y ; E605 minus the formation's reach to the left
|
|
bpl L_E60F ; E608 left edge still >= column 0
|
|
lda D_890E,y ; E60A too far left: the leftmost the centre may sit at
|
|
sta cellColA ; E60D push the centre right so the box fits
|
|
L_E60F:
|
|
lda cellColA ; E60F target column
|
|
clc ; E611 prepare the add
|
|
adc D_8926,y ; E612 plus the reach to the right
|
|
cmp #$28 ; E615 $28 = 40 columns
|
|
bcc L_E620 ; E617 right edge fits
|
|
lda #$27 ; E619 $27 = column 39; the CMP above left C=1, so this SBC is exact without a SEC
|
|
sbc D_8926,y ; E61B minus the reach right
|
|
sta cellColA ; E61E clipped centre column
|
|
L_E620:
|
|
lda cellColA ; E620 clipped column
|
|
sec ; E622 prepare the subtraction
|
|
ldy D_8955 ; E623 group id (Y already holds it - a redundant reload)
|
|
sbc scratch1E ; E626 minus the group's average ordered column
|
|
L_E628:
|
|
sta D_8958 ; E628 column delta, also kept here for the setup overlay
|
|
sta scratch2A ; E62B column delta the caller adds to every member
|
|
lda cellRowA ; E62D wanted target row
|
|
sec ; E62F prepare the subtraction
|
|
ldy D_8955 ; E630 group id (redundant reload)
|
|
sbc D_891A,y ; E633 minus the reach upward
|
|
bpl L_E63D ; E636 top edge still >= row 0
|
|
lda D_891A,y ; E638 too high: the topmost the centre may sit at
|
|
sta cellRowA ; E63B push the centre down so the box fits
|
|
L_E63D:
|
|
lda cellRowA ; E63D target row
|
|
clc ; E63F prepare the add
|
|
adc D_8932,y ; E640 plus the reach downward
|
|
cmp #$28 ; E643 $28 = 40 rows
|
|
bcc L_E64F ; E645 bottom edge fits
|
|
lda #$27 ; E647 $27 = row 39
|
|
sec ; E649 explicit SEC here, unlike the column path above which relies on the CMP carry
|
|
sbc D_8932,y ; E64A minus the reach down
|
|
sta cellRowA ; E64D clipped centre row
|
|
L_E64F:
|
|
lda cellRowA ; E64F clipped row
|
|
sec ; E651 prepare the subtraction
|
|
ldy D_8955 ; E652 group id (redundant reload)
|
|
sbc scratch1F ; E655 minus the group's average ordered row
|
|
sub_E657:
|
|
sta scratch2B ; E657 row delta the caller adds to every member
|
|
lda cellColA ; E659 clipped column
|
|
sta D_88D2,y ; E65B remember it as this group's target cell
|
|
lda cellRowA ; E65E clipped row
|
|
sta D_88DE,y ; E660 and its target row
|
|
L_E663:
|
|
rts ; E663 done
|
|
|
|
|
|
; maxAdvanceByRankTable - how many cells the path search may advance for direction-preference rank
|
|
; 0..7. The three best headings may be followed up to 5 cells, the next two 3 cells and the worst
|
|
; three only 1, so a group that has to detour a long way round also moves a shorter distance.
|
|
maxAdvanceByRankTable:
|
|
.byte $05 ; E664 5,5,5,3,3,1,1,1 cells per rank
|
|
.byte $05,$05,$03,$03,$01,$01,$01; E665 .......
|
|
|
|
; ----------------------------------------------------------------------
|
|
; offsetPointInDirection - Computes the cell that lies Y steps in compass direction X from the
|
|
; reference point (zp_27 column, zp_28 row), using dirToColDelta/dirToRowDelta and
|
|
; offsetCoordinateClamped. The result is left in zp_18 (column) and zp_19 (row); carry set means the
|
|
; raw coordinate left the 40x40 map, and when the column already left it the row is not computed at
|
|
; all.
|
|
; In: X = direction 0..7 (0 = north, clockwise), Y = number of steps, zp_27/zp_28 = origin column/row
|
|
; Out: zp_18/zp_19 = target cell, C=1 when the point fell off the map; self-modifies the LDX immediate
|
|
; at $E67D (L_E67C+1)
|
|
; Called from: findTrainerAdvanceCell $E44D, isGroupDestinationTooRisky $E69C, the drone target scan
|
|
; $EF9F
|
|
; ----------------------------------------------------------------------
|
|
offsetPointInDirection:
|
|
stx L_E67C+1 ; E66C patch the direction into the LDX immediate at $E67D so it can be reused for the row table
|
|
lda dirToColDelta,x ; E66F column delta -1/0/+1 of that direction
|
|
tax ; E672 X = column step for offsetCoordinateClamped
|
|
L_E673:
|
|
lda cellColB ; E673 origin column
|
|
jsr offsetCoordinateClamped ; E675 column + step * distance, clamped to 0..39
|
|
bcs L_E689 ; E678 left the map - report failure, the row is not computed
|
|
sta scratch18 ; E67A candidate column
|
|
L_E67C:
|
|
ldx #$FF ; E67C operand patched at $E66C with the direction index
|
|
lda dirToRowDelta,x ; E67E row delta -1/0/+1 of that direction
|
|
tax ; E681 X = row step
|
|
lda cellRowB ; E682 origin row
|
|
jsr offsetCoordinateClamped ; E684 row + step * distance, clamped to 0..39
|
|
sta scratch19 ; E687 candidate row
|
|
L_E689:
|
|
rts ; E689 C=1 when either coordinate had to be clamped back onto the 40x40 map
|
|
|
|
|
|
; Compass direction tables shared by the whole trainer AI. Directions are numbered 0-7 clockwise from
|
|
; north: 0 N, 1 NE, 2 E, 3 SE, 4 S, 5 SW, 6 W, 7 NW. A ninth entry (index 8) holds the delta pair 0,0
|
|
; so that 'no direction at all' can be expressed as an ordinary index - assessThreatAroundAiComcen
|
|
; returns 8 when nothing threatens the AI comcen. Read by $E241/$E274 (comcen push/retreat),
|
|
; $E482 (path search), offsetPointInDirection ($E66F/$E67E) and getStepDirection ($E816/$E81E).
|
|
dirToColDelta:
|
|
.byte $00,$01,$01,$01,$00,$FF,$FF,$FF; E68A ........ column step of directions 0-7: N 0, NE +1, E +1, SE +1, S 0, SW -1, W -1, NW -1
|
|
.byte $00 ; E692 index 8: the null direction, column step 0
|
|
|
|
; dirToRowDelta: byteTable, 9 bytes. Row delta of the same 8 directions: -1,-1,0,+1,+1,+1,0,-1, plus
|
|
; the 9th zero entry. Direction 0 = north, 2 = east, 4 = south, 6 = west.
|
|
dirToRowDelta:
|
|
.byte $FF,$FF,$00,$01,$01,$01,$00,$FF; E693 ........ row step of directions 0-7: N -1, NE -1, E 0, SE +1, S +1, SW +1, W 0, NW -1
|
|
.byte $00 ; E69B index 8: the null direction, row step 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isGroupDestinationTooRisky - influence-map test of one candidate destination for the AI group in
|
|
; currentAiGroup ($8955). Steps Y cells in direction X away from (cellColB,cellRowB) with
|
|
; offsetPointInDirection, grows that cell into the rectangle the whole formation would cover (the four
|
|
; span bytes $890E left / $891A up / $8926 right / $8932 down, clamped to 0..39) and plants the four
|
|
; edges into the four CMP operands of its own scan loop. It then walks all 100 units and sums a
|
|
; signed 16-bit score: every unit whose square influence area (side 2*unitThreatRadius $0400) overlaps
|
|
; the rectangle adds unitThreatWeight ($0464) if it is one of the human player's units 0-49 and
|
|
; subtracts it if it is one of the AI's own 50-99 (units of the moving group itself were already given
|
|
; radius 0 by buildUnitThreatTables). The verdict compares the enemy surplus with the group's own
|
|
; combat value $8956/$8957.
|
|
; In: X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row; $8955
|
|
; currentAiGroup, $8954 aiGroupCount,
|
|
; span tables $890E/$891A/$8926/$8932, threat tables $0400/$0464 (built by
|
|
; buildUnitThreatTables), group value $8956/$8957,
|
|
; $896E aggression-phase clock, $91CB game clock
|
|
; Out: C=1 = reject (off the map, or enemy threat >= own combat value); C=0 = acceptable. zp_18/zp_19
|
|
; = the candidate cell,
|
|
; zp_1A/zp_1B = the signed 16-bit threat surplus, zp_1C = the radius of the unit being weighed.
|
|
; Self-modifies the CMP operands at $E704, $E70B, $E715 and $E71C.
|
|
; Called from: findTrainerAdvanceCell ($E444) only.
|
|
; ----------------------------------------------------------------------
|
|
isGroupDestinationTooRisky:
|
|
jsr offsetPointInDirection ; E69C candidate cell = origin (cellColB,cellRowB) moved Y cells in direction X
|
|
L_E69F:
|
|
bcc L_E6A2 ; E69F the candidate is on the map, so weigh it
|
|
rts ; E6A1 the step ran off the edge: return C=1 = reject
|
|
|
|
L_E6A2:
|
|
ldy D_8955 ; E6A2 Y = the group being moved
|
|
cpy D_8954 ; E6A5 is it one of the aiGroupCount live groups?
|
|
bcc L_E6AC ; E6A8 yes: measure the threat over its formation footprint
|
|
clc ; E6AA no such group, so there is nothing to be afraid of
|
|
rts ; E6AB return C=0 = destination acceptable
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Grow the candidate cell into the rectangle the whole formation would stand on, clamp it to the 40x40
|
|
; map, and write its four edges straight into the four CMP instructions of the unit scan below.
|
|
; ----------------------------------------------------------------------
|
|
L_E6AC:
|
|
lda scratch18 ; E6AC candidate column
|
|
sec ; E6AE prepare the subtraction
|
|
sbc D_890E,y ; E6AF minus the group's span to the left = left edge of the formation box
|
|
bpl L_E6B6 ; E6B2 still on the map
|
|
lda #$00 ; E6B4 hangs off the left edge: clamp the box to column 0
|
|
L_E6B6:
|
|
sta L_E714+1 ; E6B6 self-modify: the CMP at $E714 now tests against the box's left column
|
|
lda scratch18 ; E6B9 candidate column again
|
|
clc ; E6BB prepare the addition
|
|
adc D_8926,y ; E6BC plus the group's span to the right = right edge of the box
|
|
cmp #$28 ; E6BF past the last column (40)?
|
|
bcc L_E6C5 ; E6C1 no, keep it
|
|
L_E6C3:
|
|
lda #$27 ; E6C3 yes: clamp the box to column 39
|
|
L_E6C5:
|
|
sta L_E71B+1 ; E6C5 self-modify: the CMP at $E71B now tests against the box's right column
|
|
L_E6C8:
|
|
lda scratch19 ; E6C8 candidate row
|
|
sec ; E6CA prepare the subtraction
|
|
sbc D_891A,y ; E6CB minus the group's span upwards = top edge of the box
|
|
bpl L_E6D2 ; E6CE still on the map
|
|
lda #$00 ; E6D0 hangs off the top: clamp the box to row 0
|
|
L_E6D2:
|
|
sta L_E703+1 ; E6D2 self-modify: the CMP at $E703 now tests against the box's top row
|
|
lda scratch19 ; E6D5 candidate row again
|
|
clc ; E6D7 prepare the addition
|
|
adc D_8932,y ; E6D8 plus the group's span downwards = bottom edge of the box
|
|
cmp #$28 ; E6DB past the last row (40)?
|
|
bcc L_E6E1 ; E6DD no, keep it
|
|
lda #$27 ; E6DF yes: clamp the box to row 39
|
|
L_E6E1:
|
|
sta L_E70A+1 ; E6E1 self-modify: the CMP at $E70A now tests against the box's bottom row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk all 100 units, unit 99 down to unit 0, and accumulate a signed 16-bit 'how dangerous is this
|
|
; square' score in zp_1A/zp_1B: enemy influence adds, friendly influence subtracts. A unit only
|
|
; counts when its influence square (centre = its cell, half-width = unitThreatRadius) overlaps the
|
|
; formation box just computed.
|
|
; ----------------------------------------------------------------------
|
|
lda #$00 ; E6E4 clear the running score
|
|
sta scratch1A ; E6E6 threat surplus, low byte
|
|
sta scratch1B ; E6E8 threat surplus, high byte
|
|
ldy #$63 ; E6EA start at unit 99
|
|
L_E6EC:
|
|
lda checksumXorConstant,y ; E6EC unitThreatRadius[unit] from the $0400 influence page (also trainerPlaybook0200:play3Col12)
|
|
beq L_E740 ; E6EF radius 0 (destroyed, spy/comcen, or a member of the moving group): the unit does not count
|
|
L_E6F1:
|
|
sta scratch1C ; E6F1 keep this unit's influence radius
|
|
cpy #$32 ; E6F3 unit 50-99 = one of the AI's own?
|
|
bcc L_E6FC ; E6F5 no, it is one of the human player's units 0-49
|
|
lda D_8955 ; E6F7 own unit: is the group being moved group 0, the AI comcen's own group?
|
|
beq L_E740 ; E6FA yes: friendly cover is not credited to the comcen group, so skip own units entirely
|
|
L_E6FC:
|
|
lda unitRowTable,y ; E6FC row of this unit
|
|
L_E6FF:
|
|
tax ; E6FF keep the row for the second test
|
|
clc ; E700 prepare the addition
|
|
adc scratch1C ; E701 row + radius = bottom edge of its influence square
|
|
L_E703:
|
|
cmp #$FF ; E703 operand patched at $E6D2 with the box's top row
|
|
bcc L_E740 ; E705 its influence stops above the box: no overlap, next unit
|
|
txa ; E707 the row again (the CMP above left C=1 for the subtraction)
|
|
sbc scratch1C ; E708 row - radius = top edge of its influence square
|
|
L_E70A:
|
|
cmp #$FF ; E70A operand patched at $E6E1 with the box's bottom row
|
|
bcs L_E740 ; E70C its influence starts below the box: no overlap
|
|
lda unitColTable,y ; E70E column of this unit
|
|
tax ; E711 keep the column for the second test
|
|
L_E712:
|
|
adc scratch1C ; E712 column + radius = right edge of its influence square (the CMP above left C=0)
|
|
L_E714:
|
|
cmp #$FF ; E714 operand patched at $E6B6 with the box's left column
|
|
bcc L_E740 ; E716 its influence stops left of the box: no overlap
|
|
txa ; E718 the column again (the CMP above left C=1 for the subtraction)
|
|
sbc scratch1C ; E719 column - radius = left edge of its influence square
|
|
L_E71B:
|
|
cmp #$FF ; E71B operand patched at $E6C5 with the box's right column
|
|
bcs L_E740 ; E71D its influence starts right of the box: no overlap
|
|
cpy #$32 ; E71F the squares overlap, so whose unit is it?
|
|
bcc addEnemyThreatWeight ; E721 human player's unit 0-49: its weight adds to the danger
|
|
lda scratch1A ; E723 one of the AI's own: subtract its weight instead (friendly cover)
|
|
sbc play4Grp12,y ; E725 unitThreatWeight[unit] from the $0464 influence page (the CPY above left C=1)
|
|
sta scratch1A ; E728 store the surplus low byte
|
|
lda scratch1B ; E72A surplus high byte
|
|
sbc #$00 ; E72C propagate the borrow
|
|
sta scratch1B ; E72E store it back
|
|
jmp L_E740 ; E730 on to the next unit
|
|
|
|
addEnemyThreatWeight:
|
|
lda scratch1A ; E733 surplus low byte
|
|
adc play4Grp12,y ; E735 add unitThreatWeight[unit] (the CPY at $E71F left C=0)
|
|
sta scratch1A ; E738 store it back
|
|
lda scratch1B ; E73A surplus high byte
|
|
sub_E73C:
|
|
adc #$00 ; E73C propagate the carry
|
|
sta scratch1B ; E73E store it back
|
|
L_E740:
|
|
dey ; E740 next unit
|
|
bpl L_E6EC ; E741 until unit 0 has been weighed
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Verdict. A negative or zero surplus means the AI already dominates the square. Otherwise the enemy
|
|
; surplus is compared with this group's own combat value, and an exact draw is settled by the game
|
|
; clock and the RNG so that both machines would reach the same answer.
|
|
; ----------------------------------------------------------------------
|
|
judgeGroupThreatTotal:
|
|
lda scratch1B ; E743 sign of the 16-bit surplus
|
|
bpl L_E749 ; E745 not negative: weigh it against the group's own strength
|
|
L_E747:
|
|
clc ; E747 friendly influence outweighs the enemy there
|
|
rts ; E748 C=0 = destination acceptable
|
|
|
|
L_E749:
|
|
lda scratch1A ; E749 surplus low byte
|
|
ora scratch1B ; E74B is the whole 16-bit surplus exactly zero?
|
|
beq L_E747 ; E74D no enemy influence at all: accept
|
|
lda scratch1A ; E74F surplus low byte
|
|
sec ; E751 prepare the subtraction
|
|
L_E752:
|
|
sbc D_8956 ; E752 minus the group's own combat value, low byte
|
|
L_E755:
|
|
sta scratch1A ; E755 keep the difference
|
|
lda scratch1B ; E757 surplus high byte
|
|
sbc D_8957 ; E759 minus the group's combat value, high byte
|
|
sta scratch1B ; E75C keep it
|
|
ora scratch1A ; E75E is the difference exactly zero?
|
|
bne L_E772 ; E760 no: return the carry of the subtraction (C=1 = threat >= own strength = reject)
|
|
lda D_896E ; E762 dead heat: consult the aggression-phase clock set at setup ($EF00 area) or by retreatTrainerComcen
|
|
L_E765:
|
|
cmp gameClock ; E765 has the game reached that round? the game clock counts down, so 'phase clock >= clock' means yes
|
|
bcs L_E76D ; E768 yes, late enough in the game: toss a coin
|
|
sec ; E76A still early: play safe
|
|
L_E76B:
|
|
bcs L_E772 ; E76B always taken (the BCS above fell through with C=1): return C=1 = reject
|
|
L_E76D:
|
|
jsr nextGameRandom ; E76D step the lock-step game RNG so both machines stay in sync
|
|
cmp #$80 ; E770 C=1 for half of the random values, i.e. reject a tie 50 percent of the time
|
|
L_E772:
|
|
rts ; E772 carry tells the caller whether the destination is too dangerous
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildUnitThreatTables - rebuilds the two 100-entry influence-map tables that
|
|
; isGroupDestinationTooRisky reads. For each unit 99..0 it writes an influence radius into $0400 and a
|
|
; combat weight into $0464. Destroyed units, spies and comcens (type 3 and 4) and every AI unit
|
|
; belonging to the group that is about to be moved get radius 0 and weight 0. A grunt or rider gets
|
|
; base radius 3, a boomer 6, both plus the terrain sight bonus of the tile the unit is standing on, so
|
|
; units on high ground project further. The weight comes from unitCombatValueTable ($E2B7) =
|
|
; 2,2,3,0,0.
|
|
; In: $8955 currentAiGroup, aiUnitGroup ($8800+absolute unit index), unitTypeTable $F76C,
|
|
; unitTerrainUnderTable $FAF0,
|
|
; terrainClassTable $077E, terrainSightBonusTable $35BB, unitCombatValueTable $E2B7
|
|
; Out: $0400[0..99] = influence radius, $0464[0..99] = combat weight; zp_18 and zp_19 clobbered
|
|
; Called from: findTrainerAdvanceCell ($E42A), immediately before its search loop.
|
|
; One entry per unit, walking backwards from unit 99 to unit 0.
|
|
; ----------------------------------------------------------------------
|
|
buildUnitThreatTables:
|
|
ldy #$63 ; E773 start at unit 99
|
|
L_E775:
|
|
cpy #$32 ; E775 is this one of the AI's own units 50-99?
|
|
bcc L_E781 ; E777 no, it is a human player unit: no group test needed
|
|
lda msgGetOpponentOnPhoneAnd,y; E779 aiUnitGroup[unit] - the group table addressed by absolute index ($8800+unit = $8832+ordinal)
|
|
cmp D_8955 ; E77C does it belong to the very group we are about to move?
|
|
beq L_E786 ; E77F yes: a group must not weigh its own members
|
|
L_E781:
|
|
lda unitTypeTable,y ; E781 unit type byte, bit 7 = destroyed
|
|
bpl L_E78A ; E784 alive: work out its influence radius
|
|
L_E786:
|
|
lda #$00 ; E786 destroyed, or a member of the moving group: no influence
|
|
beq L_E7A7 ; E788 always taken (A = 0): store radius 0
|
|
L_E78A:
|
|
ldx #$03 ; E78A default influence radius 3 cells
|
|
L_E78C:
|
|
and #$07 ; E78C unit type 0-4 without the status bits
|
|
sta scratch19 ; E78E keep the type for the weight look-up
|
|
cmp #$02 ; E790 GRUNT (0) or RIDER (1)?
|
|
bcc L_E798 ; E792 yes: keep radius 3
|
|
bne L_E786 ; E794 type 3 SPY or 4 COMCEN: they project no threat at all
|
|
ldx #$06 ; E796 BOOMER (2): its guns reach twice as far, radius 6
|
|
L_E798:
|
|
stx scratch18 ; E798 stash the base radius
|
|
ldx unitTerrainUnderTable,y ; E79A terrain code of the tile hidden under the unit
|
|
lda D_077E,x ; E79D terrain class 0-8 of that tile
|
|
tax ; E7A0 class becomes the index
|
|
lda terrainSightBonusTable,x; E7A1 extra range the terrain grants: 0, 1 or 2 cells
|
|
clc ; E7A4 prepare the addition
|
|
adc scratch18 ; E7A5 radius = base radius + terrain bonus
|
|
L_E7A7:
|
|
sta checksumXorConstant,y ; E7A7 unitThreatRadius[unit] into the $0400 scratch page (also trainerPlaybook0200:play3Col12)
|
|
tax ; E7AA is the radius zero?
|
|
beq L_E7B2 ; E7AB yes: A is already 0, so the weight becomes 0 too
|
|
ldx scratch19 ; E7AD unit type
|
|
lda unitCombatValueTable,x ; E7AF combat weight: GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0
|
|
L_E7B2:
|
|
sta play4Grp12,y ; E7B2 unitThreatWeight[unit] into the $0464 scratch page
|
|
L_E7B5:
|
|
dey ; E7B5 next unit
|
|
bpl L_E775 ; E7B6 until unit 0 has an entry
|
|
rts ; E7B8 both influence tables are current
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; multiplyAbyY - 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y = 0. The
|
|
; multiplicand is copied into its own ADC operand and Y is restored from a second patched operand
|
|
; before returning, so the caller's step count survives the call.
|
|
; In: A = multiplicand, Y = count
|
|
; Out: A = A*Y; Y unchanged; X untouched, flags clobbered; self-modifies the operands at $E7CB and
|
|
; $E7D0
|
|
; Called from: offsetCoordinateClamped ($EA22) only, where A is a step of 0, +1 or $FF and Y a cell
|
|
; distance.
|
|
; ----------------------------------------------------------------------
|
|
multiplyAbyY:
|
|
sty L_E7CF+1 ; E7B9 self-modify: stash the count in the LDY at $E7CF so Y can be handed back
|
|
sta L_E7CA+1 ; E7BC self-modify: the multiplicand becomes the operand of the ADC at $E7CA
|
|
cpy #$00 ; E7BF multiply by zero?
|
|
bne L_E7C6 ; E7C1 no: run the addition loop
|
|
tya ; E7C3 Y is 0, so A = 0
|
|
beq L_E7CE ; E7C4 always taken: return zero
|
|
L_E7C6:
|
|
dey ; E7C6 one addition per unit of the count
|
|
beq L_E7CE ; E7C7 count used up: A holds the product
|
|
L_E7C9:
|
|
clc ; E7C9 clear the carry before each addition
|
|
L_E7CA:
|
|
adc #$FF ; E7CA operand patched at $E7BC: add the multiplicand once more
|
|
bne L_E7C6 ; E7CC keep adding; a running total of exactly 0 would end the loop early, which the callers' 0/+1/-1 steps never produce
|
|
L_E7CE:
|
|
pha ; E7CE hold the product across the Y restore
|
|
L_E7CF:
|
|
ldy #$FF ; E7CF operand patched at $E7B9: give the caller its count back
|
|
pla ; E7D1 product back into A
|
|
rts ; E7D2 A = A*Y
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildDirectionFanTowardTarget - finds the compass heading from the line start
|
|
; (lineCurCol,lineCurRow) to the line end (lineEndCol,lineEndRow) with getStepDirection, saves it in
|
|
; zp_19 and falls into buildDirectionFan so the eight candidate move directions are laid out around
|
|
; that heading. Nothing is built when start and target are the same cell.
|
|
; In: zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column, $8955 currentAiGroup
|
|
; Out: zp_19 = heading 0-7 ($FF and no fan when start = target); $8896-$889D = the eight candidate
|
|
; directions;
|
|
; zp_18 and the line-stepper state $8C-$97 clobbered
|
|
; Called from: the group-movement code at $E416.
|
|
; ----------------------------------------------------------------------
|
|
buildDirectionFanTowardTarget:
|
|
jsr getStepDirection ; E7D3 heading of the first Bresenham step from the group centre toward its target
|
|
sty scratch19 ; E7D6 remember the heading for buildDirectionFan
|
|
bpl buildDirectionFan ; E7D8 a real heading 0-7: lay the eight candidates out around it
|
|
rts ; E7DA Y = $FF, the group is already on its target cell: no fan to build
|
|
|
|
|
|
; dirMirrorTable - eight bytes, entry d = (8 - d) AND 7, i.e. the mirror of a heading offset about the
|
|
; north-south axis (0 stays 0, +1 becomes -1 and so on). buildDirectionFan runs the offsets of a
|
|
; 'left-handed' group through it so that half the groups sweep clockwise and half anticlockwise. The
|
|
; label in the middle of the table is a branch target of the modem-driver build at the same address,
|
|
; not a separate block.
|
|
dirMirrorTable:
|
|
.byte $00,$07,$06,$05,$04 ; E7DB ..... mirror of heading offsets 0-4: 0, -1, -2, -3, -4 expressed modulo 8
|
|
L_E7E0:
|
|
.byte $03 ; E7E0 mirror of heading offsets 5-7: +3, +2, +1
|
|
.byte $02,$01 ; E7E1 ..
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildDirectionFan - fills the eight-byte scratch buffer $8896-$889D with the move directions the
|
|
; path search should try, in order of preference: entry i = (dirFanOffsetTable[i] + heading + 8) AND
|
|
; 7. dirFanOffsetTable ($894A) is built once per game by the setup overlay's
|
|
; buildAiHeadingSearchOrder ($EEC3) as 0, +/-1, +/-1, +/-2, +/-2, +/-3, +/-3, +/-4 with a random
|
|
; left/right bias, so the search always tries straight ahead first and fans outwards. When bit 0 of
|
|
; the group's groupFlags is set every offset is mirrored through dirMirrorTable, giving that group the
|
|
; opposite handedness.
|
|
; In: zp_19 = wanted heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable
|
|
; Out: $8896-$889D = eight candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered
|
|
; Called from: decideTrainerGroupAction ($E36E) and by falling through from
|
|
; buildDirectionFanTowardTarget ($E7D8).
|
|
; The list is consumed by the move search at $E431-$E47E, whose per-rank distance caps are in
|
|
; maxAdvanceByRankTable ($E664).
|
|
; ----------------------------------------------------------------------
|
|
buildDirectionFan:
|
|
ldx #$00 ; E7E3 assume the fan is not mirrored
|
|
ldy D_8955 ; E7E5 the group being moved
|
|
lda D_893E,y ; E7E8 its groupFlags; the low nibble is the random start delay handed out at setup
|
|
and #$01 ; E7EB bit 0 of that delay serves as the group's fixed handedness
|
|
beq L_E7F0 ; E7ED clear: fan out the normal way
|
|
dex ; E7EF set: X = $FF, mirror every offset
|
|
L_E7F0:
|
|
stx scratch18 ; E7F0 mirror flag for the loop below
|
|
ldy #$07 ; E7F2 fill the eight preference slots, 7 down to 0
|
|
L_E7F4:
|
|
lda D_894A,y ; E7F4 heading offset of preference rank Y
|
|
L_E7F7:
|
|
bit scratch18 ; E7F7 is this group mirrored? (BIT leaves A alone)
|
|
bpl L_E7FF ; E7F9 no: use the offset as it is
|
|
tax ; E7FB offset becomes the index
|
|
lda dirMirrorTable,x ; E7FC yes: swap left for right
|
|
L_E7FF:
|
|
clc ; E7FF clear the carry before adding
|
|
adc scratch19 ; E800 candidate = wanted heading + offset
|
|
clc ; E802 the addition above may have carried
|
|
adc #$08 ; E803 bias by a whole turn so a negative offset stays positive
|
|
L_E805:
|
|
and #$07 ; E805 wrap back into a compass direction 0-7
|
|
sta D_8896,y ; E807 store the candidate for preference rank Y
|
|
L_E80A:
|
|
dey ; E80A next rank
|
|
bpl L_E7F4 ; E80B until all eight ranks are filled
|
|
rts ; E80D the fan is ready for the path search
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getStepDirection - sets a Bresenham walker up between (lineCurCol,lineCurRow) and
|
|
; (lineEndCol,lineEndRow), takes exactly one step, and converts the deltas that step applied ($90F5
|
|
; column, $90F4 row) back into one of the eight compass directions by searching
|
|
; dirToColDelta/dirToRowDelta from index 7 downwards. stepLine zeroes both deltas before it moves, so
|
|
; a zero-length line leaves 0,0 - which matches no entry 0-7 and comes out as Y = $FF.
|
|
; In: zp_92/zp_93 = start row/column, zp_94/zp_95 = end row/column
|
|
; Out: Y = direction 0-7, or $FF when start and end are the same cell; A = the last delta compared;
|
|
; line-stepper state $8C-$97 and $90F4/$90F5 updated
|
|
; Called from: buildDirectionFanTowardTarget ($E7D3), assessThreatAroundAiComcen ($EC35), the drone
|
|
; code ($EEC7)
|
|
; and the $EE00 drone-flight overlay ($EEB1).
|
|
; ----------------------------------------------------------------------
|
|
getStepDirection:
|
|
jsr initLineStepper ; E80E set the Bresenham walker up between the two cells
|
|
jsr stepLine ; E811 take one step; it records the deltas it applied in $90F5 (column) and $90F4 (row), zeroing them first
|
|
ldy #$07 ; E814 search the direction tables backwards from 7
|
|
L_E816:
|
|
lda dirToColDelta,y ; E816 column step of direction Y
|
|
cmp lastStepColDelta ; E819 does it match the step just taken?
|
|
bne L_E826 ; E81C no: try the next direction
|
|
lda dirToRowDelta,y ; E81E row step of direction Y
|
|
cmp lastStepRowDelta ; E821 does that match too?
|
|
beq L_E829 ; E824 both match: Y is the heading
|
|
L_E826:
|
|
dey ; E826 next direction down
|
|
bpl L_E816 ; E827 index 8 (the null delta) is deliberately never tested, so Y = $FF means 'the line has no step'
|
|
L_E829:
|
|
rts ; E829 Y = compass direction, or $FF
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; cellDistance - integer distance between two map cells. distanceSquared ($3870) gives |dcol|^2 +
|
|
; |drow|^2 (each delta capped at 15, the sum saturating at $FF with C=1), and this routine converts it
|
|
; back through squaresTable ($3860) into the largest n with n*n <= dsq. A saturated squared distance
|
|
; simply reports 16.
|
|
; In: zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row
|
|
; Out: A = X = distance 0-16; Y untouched
|
|
; Called from: $E410, $E4B4, assessThreatAroundAiComcen ($EC24), the missile code ($ED43/$EE0C/$EE5C)
|
|
; and the drone overlay.
|
|
; ----------------------------------------------------------------------
|
|
cellDistance:
|
|
jsr distanceSquared ; E82A A = squared distance between (cellColA,cellRowA) and (cellColB,cellRowB), C=1 if it saturated at 255
|
|
ldx #$10 ; E82D 16 is the largest distance this routine reports
|
|
bcs L_E83A ; E82F saturated: everything beyond 15 cells is simply 'far'
|
|
dex ; E831 otherwise start the search at 15
|
|
L_E832:
|
|
cmp squaresTable,x ; E832 is the squared distance at least X*X?
|
|
bcs L_E83A ; E835 yes: X is the integer distance
|
|
dex ; E837 try the next smaller radius
|
|
bne L_E832 ; E838 X = 0 falls out of the loop as distance 0
|
|
L_E83A:
|
|
txa ; E83A hand the distance back in A as well as X
|
|
rts ; E83B A = X = distance in cells
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateFocusGroupCentre - keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row)
|
|
; current. The focus group is the assault group the AI comcen walks along behind. When the id has
|
|
; bit 7 set the centre falls back on the AI comcen's own cell, and a suspended group is taken back
|
|
; into service as soon as bit 7 of its groupFlags has cleared. An active group whose groupFlags bit 7
|
|
; is set (it has reached its objective) is suspended by ORing $80 into the id. Otherwise the centroid
|
|
; of the group's combat members is recomputed, and a group that has lost every member is dropped
|
|
; ($895D = $FF).
|
|
; In: $895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707, $8832 aiUnitGroup
|
|
; Out: $895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered
|
|
; Called from: planTrainerRound ($E20C), once per round just before the comcen push/retreat tail.
|
|
; ----------------------------------------------------------------------
|
|
updateFocusGroupCentre:
|
|
ldy D_895D ; E83C focusGroupId: $FF = no group, bit 7 set = the group is suspended
|
|
bpl L_E860 ; E83F an active group id 0-15: go and refresh its centre
|
|
lda side1ComcenCol ; E841 no usable group: fall back on the AI comcen's own cell (unit 99)
|
|
L_E844:
|
|
sta msgOpponentUsingNonStandardGame; E844 focus centre column ($895E)
|
|
lda side1ComcenRow ; E847 AI comcen row
|
|
sta D_895F ; E84A focus centre row
|
|
cpy #$FF ; E84D was the focus group dropped altogether?
|
|
beq L_E85F ; E84F yes: nothing left to revive
|
|
tya ; E851 suspended group: recover its id
|
|
and #$0F ; E852 strip the suspend bit
|
|
tay ; E854 index the group tables with it
|
|
lda D_893E,y ; E855 its groupFlags
|
|
L_E858:
|
|
bmi L_E85F ; E858 bit 7 still set (the group is at its objective): leave it suspended
|
|
sty D_895D ; E85A the flag cleared: put the group back in service
|
|
bpl L_E860 ; E85D always taken (an id 0-15 is positive): recompute its centre
|
|
L_E85F:
|
|
rts ; E85F nothing to update this round
|
|
|
|
L_E860:
|
|
lda D_893E,y ; E860 groupFlags of the active focus group
|
|
bpl L_E86C ; E863 still on its way: recompute the centre
|
|
tya ; E865 bit 7 set: the group has arrived
|
|
ora #$80 ; E866 mark the id as suspended
|
|
sta D_895D ; E868 store it back; the centre keeps the comcen fallback from the next call
|
|
rts ; E86B done for this round
|
|
|
|
L_E86C:
|
|
lda D_895D ; E86C group id for the centroid scan
|
|
ldy #$2D ; E86F scan AI ordinals 45 down to 0 - the combat units only, since 46-48 are the spies and 49 the comcen
|
|
jsr computeGroupCentroid ; E871 A = member count, X = average column, Y = average row
|
|
bne L_E87B ; E874 the group still has members
|
|
lda #$FF ; E876 no member left
|
|
sta D_895D ; E878 drop the focus group; X/Y are 0/$FF here, so the centre is stale for the one round until the fallback runs
|
|
L_E87B:
|
|
stx msgOpponentUsingNonStandardGame; E87B focus centre column
|
|
sty D_895F ; E87E focus centre row
|
|
rts ; E881 the comcen now has something to escort
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computeGroupCentroid - averages the position and the ordered destination of every living member of
|
|
; one AI group. It scans AI ordinals Y down to 0 (absolute units 50+Y down to 50), accumulates four
|
|
; 16-bit sums and divides each by the member count with divideSigned16by8. The wanted group id is
|
|
; patched into its own CMP, and the two averaged coordinates are handed back through two patched
|
|
; immediate operands so that A can carry the member count.
|
|
; In: A = group id, Y = highest AI ordinal to scan (usually $31 = 49 or $2D = 45); $8832 aiUnitGroup,
|
|
; $F79E unitType,
|
|
; $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow (all indexed by AI ordinal)
|
|
; Out: A = member count (Z set when the group is empty), X = average column, Y = average row, zp_1E =
|
|
; average destination column,
|
|
; zp_1F = average destination row, zp_1C = member count; zp_18-zp_21 clobbered;
|
|
; self-modifies the operands at $E89D, $E913 and $E915
|
|
; Called from: computeTrainerGroupStats ($E5B9) and updateFocusGroupCentre ($E871).
|
|
; ----------------------------------------------------------------------
|
|
computeGroupCentroid:
|
|
sta L_E89C+1 ; E882 self-modify: the wanted group id becomes the operand of the CMP at $E89C
|
|
lda #$00 ; E885 clear all nine accumulator bytes
|
|
sta scratch18 ; E887 column sum, low
|
|
sta scratch19 ; E889 column sum, high
|
|
sta scratch1A ; E88B row sum, low
|
|
sta scratch1B ; E88D row sum, high
|
|
sta scratch1C ; E88F member count
|
|
sta scratch1E ; E891 ordered-destination column sum, low
|
|
L_E893:
|
|
sta scratch1F ; E893 ordered-destination column sum, high
|
|
sta scratch20 ; E895 ordered-destination row sum, low
|
|
sta scratch21 ; E897 ordered-destination row sum, high
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass per AI ordinal, from the caller's starting ordinal down to 0.
|
|
; ----------------------------------------------------------------------
|
|
L_E899:
|
|
lda D_8832,y ; E899 aiUnitGroup[ordinal] - which group this AI unit belongs to
|
|
L_E89C:
|
|
cmp #$FF ; E89C operand patched at $E882: is it the group we are averaging?
|
|
L_E89E:
|
|
bne L_E8DF ; E89E no: next ordinal
|
|
lda side1UnitTypeTable,y ; E8A0 type byte of AI unit 50+ordinal
|
|
bmi L_E8DF ; E8A3 bit 7 set: destroyed, so it does not count
|
|
inc scratch1C ; E8A5 one more live member
|
|
lda side1UnitColTable,y ; E8A7 its current column
|
|
clc ; E8AA clear the carry
|
|
adc scratch18 ; E8AB into the 16-bit column sum
|
|
sta scratch18 ; E8AD store the low byte
|
|
lda scratch19 ; E8AF column sum, high byte
|
|
adc #$00 ; E8B1 propagate the carry
|
|
sta scratch19 ; E8B3 store it back
|
|
lda side1UnitRowTable,y ; E8B5 its current row
|
|
clc ; E8B8 clear the carry
|
|
adc scratch1A ; E8B9 into the 16-bit row sum
|
|
sta scratch1A ; E8BB store the low byte
|
|
lda scratch1B ; E8BD row sum, high byte
|
|
adc #$00 ; E8BF propagate the carry
|
|
sta scratch1B ; E8C1 store it back
|
|
lda side1UnitDestColTable,y ; E8C3 the column it has been ordered to
|
|
and #$3F ; E8C6 the coordinate is in bits 0-5; bits 6-7 are order flags
|
|
clc ; E8C8 clear the carry
|
|
adc scratch1E ; E8C9 into the destination-column sum
|
|
sta scratch1E ; E8CB store the low byte
|
|
bcc L_E8D1 ; E8CD no carry
|
|
inc scratch1F ; E8CF carry into the high byte
|
|
L_E8D1:
|
|
lda side1UnitDestRowTable,y ; E8D1 the row it has been ordered to
|
|
and #$3F ; E8D4 coordinate bits only
|
|
clc ; E8D6 clear the carry
|
|
adc scratch20 ; E8D7 into the destination-row sum
|
|
L_E8D9:
|
|
sta scratch20 ; E8D9 store the low byte
|
|
bcc L_E8DF ; E8DB no carry
|
|
inc scratch21 ; E8DD carry into the high byte
|
|
L_E8DF:
|
|
dey ; E8DF next ordinal
|
|
bpl L_E899 ; E8E0 down to AI ordinal 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Divide the four sums by the member count. The two positional averages are returned in X and Y
|
|
; through the two immediate operands patched below.
|
|
; ----------------------------------------------------------------------
|
|
ldx scratch1C ; E8E2 member count is the divisor
|
|
beq L_E916 ; E8E4 no members: skip the divisions and report zero
|
|
lda scratch18 ; E8E6 column sum, low
|
|
ldy scratch19 ; E8E8 column sum, high
|
|
jsr divideSigned16by8 ; E8EA average column
|
|
sta L_E912+1 ; E8ED self-modify: it becomes the operand of the LDX at $E912
|
|
lda scratch1A ; E8F0 row sum, low
|
|
L_E8F2:
|
|
ldy scratch1B ; E8F2 row sum, high
|
|
ldx scratch1C ; E8F4 divisor again (divideSigned16by8 clobbers X)
|
|
jsr divideSigned16by8 ; E8F6 average row
|
|
sta L_E914+1 ; E8F9 self-modify: it becomes the operand of the LDY at $E914
|
|
ldx scratch1C ; E8FC divisor again
|
|
lda scratch1E ; E8FE destination-column sum, low
|
|
ldy scratch1F ; E900 destination-column sum, high
|
|
jsr divideSigned16by8 ; E902 average ordered destination column
|
|
sta scratch1E ; E905 returned in zp_1E
|
|
ldx scratch1C ; E907 divisor again
|
|
sub_E909:
|
|
lda scratch20 ; E909 destination-row sum, low
|
|
ldy scratch21 ; E90B destination-row sum, high
|
|
jsr divideSigned16by8 ; E90D average ordered destination row
|
|
sta scratch1F ; E910 returned in zp_1F, overwriting the sum byte that has just been consumed
|
|
L_E912:
|
|
ldx #$FF ; E912 operand patched at $E8ED: X = average column
|
|
L_E914:
|
|
ldy #$FF ; E914 operand patched at $E8F9: Y = average row
|
|
L_E916:
|
|
lda scratch1C ; E916 A = member count, Z set when the group is empty
|
|
rts ; E918 caller gets count in A, centre in X/Y, average destination in zp_1E/zp_1F
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; orderAiUnitToCell - the trainer's central 'issue a move order' helper. It does nothing when the
|
|
; unit's ordered destination already is the requested cell, so redundant orders never take up room in
|
|
; the queue or in the packet. Otherwise it falls into queueMirroredMoveOrder, which mirrors both
|
|
; coordinates (the engine will mirror them back because it believes the command arrived from a remote
|
|
; opponent) and queues $80 MOVE UNIT or $81 MOVE GROUP.
|
|
; In: X = target column, A = target row, Y = AI ordinal 0-49 (absolute unit = Y+50)
|
|
; Out: nothing when the order is redundant; otherwise $8896 = $80/$81, $8897 = absolute unit index,
|
|
; $8898/$8899 = mirrored column/row, and the command appended to the AI queue at $88A0
|
|
; Called from: retreatTrainerComcen ($E262), pushTrainerComcenForward ($E294), issueTrainerGroupOrders
|
|
; ($E4F7)
|
|
; and haltAiComcen ($EBEC).
|
|
; ----------------------------------------------------------------------
|
|
orderAiUnitToCell:
|
|
stx D_8898 ; E919 stage the target column as the command's first coordinate
|
|
sta D_8899 ; E91C and the target row as the second
|
|
lda side1UnitDestColTable,y ; E91F the column AI unit 50+Y has already been ordered to
|
|
and #$3F ; E922 coordinate bits only; bits 6-7 are order flags
|
|
L_E924:
|
|
cmp D_8898 ; E924 same column as the new order?
|
|
bne L_E934 ; E927 no: the order is worth sending
|
|
lda side1UnitDestRowTable,y ; E929 the row it has already been ordered to
|
|
and #$3F ; E92C coordinate bits only
|
|
cmp D_8899 ; E92E same row too?
|
|
bne L_E934 ; E931 no: send the order
|
|
rts ; E933 the unit is already under exactly this order: queue nothing
|
|
|
|
L_E934:
|
|
lda D_8898 ; E934 target column, ready for the mirror below
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueMirroredMoveOrder - second half of orderAiUnitToCell. Mirrors the staged coordinates with
|
|
; mirrorMapCoordinate (39 - v), picks command $80 cmdMoveUnit or $81 cmdMoveGroup according to bit 6
|
|
; of the unit's flags, converts the AI ordinal into the absolute unit index and tail-jumps into
|
|
; queueAiCommand. Both players see their own side at the bottom of the map, so an incoming command's
|
|
; coordinates are always mirrored by the engine; the trainer pre-mirrors so the round trip comes out
|
|
; right.
|
|
; In: $8898/$8899 = target column/row, Y = AI ordinal 0-49, $F73A unitFlags indexed by ordinal
|
|
; Out: the four bytes $8896-$8899 staged and queued
|
|
; Called from: reached only by falling through from orderAiUnitToCell ($E934); the XREF entries
|
|
; $E917/$EA01 belong to the
|
|
; modem-driver build that occupies the same addresses.
|
|
; ----------------------------------------------------------------------
|
|
queueMirroredMoveOrder:
|
|
jsr mirrorMapCoordinate ; E937 39 - column: the engine mirrors it back when it 'receives' the packet
|
|
sta D_8898 ; E93A store the mirrored column
|
|
lda D_8899 ; E93D target row
|
|
jsr mirrorMapCoordinate ; E940 39 - row
|
|
sta D_8899 ; E943 store the mirrored row
|
|
ldx #$80 ; E946 command $80 = cmdMoveUnit
|
|
lda side1UnitFlagsTable,y ; E948 unitFlags of AI unit 50+Y
|
|
and #$40 ; E94B bit 6 = the unit is a member of an in-game group
|
|
beq L_E950 ; E94D not grouped: keep $80
|
|
inx ; E94F grouped: use $81 = cmdMoveGroup so its formation follows
|
|
L_E950:
|
|
stx D_8896 ; E950 command byte
|
|
tya ; E953 AI ordinal 0-49
|
|
clc ; E954 clear the carry
|
|
adc #$32 ; E955 plus 50: the AI plays side 1, so its units are 50-99
|
|
sta msgPickUpPhoneThenPressSpace; E957 first parameter of the command ($8897)
|
|
L_E95A:
|
|
jmp queueAiCommand ; E95A append the staged command to the AI order queue
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isPathToPlayerComcenClear - walks a Bresenham line from the current line position (which the caller
|
|
; has set to the group centre) to the human player's comcen (unit 49) and reports whether any living
|
|
; enemy fighting unit stands on it. Only human grunts, riders and boomers (types 0-2) block; the AI's
|
|
; own units, destroyed units, spies and the comcen do not. Both exits put the group centre back into
|
|
; the line position through loadGroupCentreIntoLinePos.
|
|
; In: zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player
|
|
; comcen cell, map at $F000
|
|
; Out: C=1 = the lane to the comcen is open, C=0 = blocked or the group id was invalid; zp_92/zp_93 =
|
|
; the group centre;
|
|
; zp_48/zp_49 (map pointer) and the line-stepper state $8C-$97 clobbered
|
|
; Called from: decideTrainerGroupAction ($E3B1), to decide whether the group may make a straight run
|
|
; at the comcen.
|
|
; ----------------------------------------------------------------------
|
|
isPathToPlayerComcenClear:
|
|
ldy D_8955 ; E95D the group asking for a straight run
|
|
L_E960:
|
|
cpy D_8954 ; E960 is it one of the live groups?
|
|
bcs L_E995 ; E963 no: report 'blocked' and do nothing else
|
|
lda side0ComcenCol ; E965 column of the human player's comcen (unit 49)
|
|
sta lineEndCol ; E968 line target column
|
|
lda side0ComcenRow ; E96A its row
|
|
sub_E96D:
|
|
sta lineEndRow ; E96D line target row
|
|
jsr initLineStepper ; E96F set the walker up from the current position to the comcen
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the lane one cell at a time, stopping at the first live enemy fighting unit.
|
|
; ----------------------------------------------------------------------
|
|
L_E972:
|
|
jsr stepLine ; E972 advance one cell along the lane
|
|
sub_E975:
|
|
beq loadGroupCentreIntoLinePos; E975 the line ran out without a blocker: restore the centre and return C=1
|
|
ldx lineCurCol ; E977 column the walker is standing on
|
|
ldy lineCurRow ; E979 and its row
|
|
L_E97B:
|
|
jsr readMapCell ; E97B map byte of that cell; bit 7 set means a unit occupies it
|
|
bpl L_E972 ; E97E bare terrain: keep walking
|
|
and #$7F ; E980 strip the occupancy bit to get the unit index
|
|
L_E982:
|
|
cmp #$32 ; E982 one of the AI's own units 50-99?
|
|
bcs L_E972 ; E984 yes: a unit never blocks its own side's lane
|
|
L_E986:
|
|
tay ; E986 human player's unit: index it
|
|
lda unitTypeTable,y ; E987 its type byte
|
|
bmi L_E972 ; E98A destroyed: not an obstacle
|
|
and #$07 ; E98C unit type 0-4
|
|
cmp #$03 ; E98E type 3 SPY or 4 COMCEN?
|
|
bcs L_E972 ; E990 yes: neither blocks a charge
|
|
jsr loadGroupCentreIntoLinePos; E992 a live grunt, rider or boomer sits in the lane: put the group centre back
|
|
L_E995:
|
|
clc ; E995 C=0 = the lane is blocked (also the exit for an invalid group id)
|
|
rts ; E996 carry tells the caller whether it may charge
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadGroupCentreIntoLinePos - copies the current group's centre (groupCentreCol $88EA /
|
|
; groupCentreRow $88F6) into the line walker's position and returns with C=1. It doubles as the 'path
|
|
; is clear' exit of isPathToPlayerComcenClear, which is why it ends with SEC.
|
|
; In: $8955 currentAiGroup, $88EA/$88F6 per-group centre tables
|
|
; Out: zp_93 = centre column, zp_92 = centre row, C=1; A and Y clobbered
|
|
; Called from: isPathToPlayerComcenClear ($E992) and as its C=1 exit ($E975).
|
|
; ----------------------------------------------------------------------
|
|
loadGroupCentreIntoLinePos:
|
|
ldy D_8955 ; E997 the current group
|
|
lda D_88EA,y ; E99A its centre column
|
|
sta lineCurCol ; E99D line walker column
|
|
lda D_88F6,y ; E99F its centre row
|
|
L_E9A2:
|
|
sta lineCurRow ; E9A2 line walker row
|
|
sec ; E9A4 C=1: this is also the 'lane is clear' answer of isPathToPlayerComcenClear
|
|
rts ; E9A5 line position restored
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; scoreTargetBlock - scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment
|
|
; target. A missile and a drone both cover 2x2 cells, so all four are added up: every unit standing
|
|
; in them contributes targetValueByUnitType, positive for the human player's units and negative for
|
|
; the AI's own. A block with a negative total is never chosen; otherwise, when the total beats the
|
|
; running best in zp_1C, the corner is remembered in zp_95/zp_94 and the highest human unit index
|
|
; found in the block becomes the nominal target in zp_1D (which favours the comcen, unit 49).
|
|
; In: X = column, Y = row of the top-left cell; zp_1C = best score so far (may be negative to accept
|
|
; anything),
|
|
; zp_1D = best target so far; map at $F000, unitTypeTable $F76C
|
|
; Out: zp_1A = the block's score, zp_1B = highest human unit index in it; on an improvement zp_1C,
|
|
; zp_1D, zp_94 and zp_95 are
|
|
; updated; zp_48/zp_49 = map pointer; self-modifies the operands at $E9E4 and $E9E8
|
|
; Called from: the missile/drone target scan at $EFA8, and from the $EE00 drone-flight overlay.
|
|
; ----------------------------------------------------------------------
|
|
scoreTargetBlock:
|
|
stx L_E9E3+1 ; E9A6 self-modify: keep the block's column in the LDA at $E9E3
|
|
sty L_E9E7+1 ; E9A9 self-modify: keep the block's row in the LDA at $E9E7
|
|
lda #$00 ; E9AC clear the accumulators
|
|
sta scratch1A ; E9AE score of this 2x2 block
|
|
sta scratch1B ; E9B0 highest human unit index seen in it
|
|
jsr readMapCell ; E9B2 top-left cell; readMapCell also leaves mapCellPtr pointing at it
|
|
bpl L_E9BA ; E9B5 bare terrain
|
|
jsr addUnitTargetValue ; E9B7 a unit stands here: score it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The other three cells of the block are reached straight through mapCellPtr: +1 is the cell to the
|
|
; right, +40 the cell below (the map is 40 cells wide) and +41 the diagonal.
|
|
; ----------------------------------------------------------------------
|
|
L_E9BA:
|
|
ldy #$01 ; E9BA offset +1 = the cell to the right
|
|
lda (mapCellPtr),y ; E9BC read it
|
|
bpl L_E9C3 ; E9BE bare terrain
|
|
jsr addUnitTargetValue ; E9C0 score the unit standing there
|
|
L_E9C3:
|
|
ldy #$28 ; E9C3 offset +40 = one map row down
|
|
lda (mapCellPtr),y ; E9C5 read it
|
|
bpl L_E9CC ; E9C7 bare terrain
|
|
jsr addUnitTargetValue ; E9C9 score it
|
|
L_E9CC:
|
|
ldy #$29 ; E9CC offset +41 = down and to the right
|
|
lda (mapCellPtr),y ; E9CE read it
|
|
bpl L_E9D5 ; E9D0 bare terrain
|
|
jsr addUnitTargetValue ; E9D2 score it
|
|
L_E9D5:
|
|
lda scratch1A ; E9D5 total value of the block
|
|
bmi L_E9F0 ; E9D7 negative: the AI's own units dominate it, so never bombard it
|
|
L_E9D9:
|
|
cmp scratch1C ; E9D9 better than the best block found so far?
|
|
bcs L_E9E1 ; E9DB yes: take it
|
|
bit scratch1C ; E9DD no, but is the running best itself still negative (nothing chosen yet)?
|
|
bpl L_E9F0 ; E9DF no: keep the previous best
|
|
L_E9E1:
|
|
sta scratch1C ; E9E1 new best score
|
|
L_E9E3:
|
|
lda #$FF ; E9E3 operand patched at $E9A6: the block's column
|
|
sta lineEndCol ; E9E5 remember it as the aim point
|
|
L_E9E7:
|
|
lda #$FF ; E9E7 operand patched at $E9A9: the block's row
|
|
L_E9E9:
|
|
sta lineEndRow ; E9E9 aim point row
|
|
lda scratch1B ; E9EB the biggest human unit index in the block
|
|
sta scratch1D ; E9ED becomes the nominal target unit (49 = the comcen wins any tie)
|
|
rts ; E9EF a new best target has been recorded
|
|
|
|
L_E9F0:
|
|
rts ; E9F0 rejected: the running best is left alone
|
|
|
|
|
|
; targetValueByUnitType - five bytes indexed by unit type, the bombardment value of a unit standing in
|
|
; a candidate target block. Boomers and the comcen are worth four times a grunt.
|
|
targetValueByUnitType:
|
|
.byte $01,$02,$04,$01,$04 ; E9F1 ..... GRUNT 1, RIDER 2, BOOMER 4, SPY 1, COMCEN 4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; addUnitTargetValue - helper of scoreTargetBlock. Takes a map cell byte that holds $80 plus a unit
|
|
; index, ignores destroyed units, and adds targetValueByUnitType[type] to the block score in zp_1A -
|
|
; negated when the unit belongs to the AI itself, so the trainer will not shell its own troops. For a
|
|
; human unit it also keeps the largest index seen, which is how the human comcen (unit 49) ends up
|
|
; being preferred as the nominal target.
|
|
; In: A = map cell byte with bit 7 set, unitTypeTable $F76C, targetValueByUnitType $E9F1, zp_1A/zp_1B
|
|
; accumulators
|
|
; Out: zp_1A += the (possibly negated) value, zp_1B = highest human unit index seen; X and Y clobbered
|
|
; Called from: scoreTargetBlock, once per cell of the 2x2 block ($E9B7, $E9C0, $E9C9, $E9D2).
|
|
; ----------------------------------------------------------------------
|
|
addUnitTargetValue:
|
|
and #$7F ; E9F6 drop the 'a unit is here' bit: what is left is the unit index
|
|
tax ; E9F8 index the unit tables with it
|
|
lda unitTypeTable,x ; E9F9 its type byte
|
|
bmi L_EA1A ; E9FC destroyed: worth nothing
|
|
and #$07 ; E9FE unit type 0-4
|
|
tay ; EA00 index the value table
|
|
lda targetValueByUnitType,y ; EA01 bombardment value of that type
|
|
cpx #$32 ; EA04 unit 50-99 = one of the AI's own?
|
|
bcs L_EA10 ; EA06 yes: its value counts against the block
|
|
cpx scratch1B ; EA08 human unit: is it the highest index seen in this block so far?
|
|
bcc L_EA15 ; EA0A no: just add its value
|
|
stx scratch1B ; EA0C yes: it becomes the block's nominal target
|
|
bcs L_EA15 ; EA0E always taken (the BCS above fell through with C=1)
|
|
L_EA10:
|
|
eor #$FF ; EA10 negate the value ...
|
|
clc ; EA12 clear the carry ...
|
|
adc #$01 ; EA13 ... two's complement, so friendly units subtract
|
|
L_EA15:
|
|
clc ; EA15 clear the carry before accumulating
|
|
adc scratch1A ; EA16 add into the block score
|
|
sta scratch1A ; EA18 store it back
|
|
L_EA1A:
|
|
rts ; EA1A cell scored
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; offsetCoordinateClamped - returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps
|
|
; of the signed step X, held inside the 40-cell battlefield, with the carry reporting whether the
|
|
; unclamped value had left the map. The base coordinate and the caller's step count are both parked
|
|
; in immediate operands so nothing but A is disturbed.
|
|
; In: A = base coordinate, X = signed step (0, +1 or $FF), Y = number of steps
|
|
; Out: A = clamped coordinate 0-39, C=1 when the raw value was negative or past 39, Y unchanged;
|
|
; self-modifies the operands at $EA27 and $EA36
|
|
; Called from: pushTrainerComcenForward/retreatTrainerComcen ($E24C/$E25B/$E27E/$E28D), the path
|
|
; search ($E48A/$E499)
|
|
; and offsetPointInDirection ($E675/$E684).
|
|
; ----------------------------------------------------------------------
|
|
offsetCoordinateClamped:
|
|
sty L_EA35+1 ; EA1B self-modify: save the step count in the LDY at $EA35 so Y survives the multiply
|
|
sta L_EA26+1 ; EA1E self-modify: the base coordinate becomes the operand of the ADC at $EA26
|
|
txa ; EA21 A = the signed step, 0, +1 or $FF
|
|
jsr multiplyAbyY ; EA22 A = step * number of steps
|
|
clc ; EA25 clear the carry
|
|
L_EA26:
|
|
adc #$FF ; EA26 operand patched at $EA1E: add the base coordinate
|
|
tay ; EA28 keep the raw, unclamped result for the range test
|
|
L_EA29:
|
|
bpl L_EA2D ; EA29 positive: no low clamp needed
|
|
lda #$00 ; EA2B walked off the top or left edge: clamp to 0
|
|
L_EA2D:
|
|
cmp #$28 ; EA2D past the last cell (40)?
|
|
bcc L_EA33 ; EA2F no, keep it
|
|
lda #$27 ; EA31 yes: clamp to 39
|
|
L_EA33:
|
|
cpy #$28 ; EA33 C=1 when the raw value was 40 or more, which also covers a negative value (>= $80)
|
|
L_EA35:
|
|
ldy #$FF ; EA35 operand patched at $EA1B: hand the caller its step count back
|
|
rts ; EA37 A = clamped coordinate, C=1 if it had to be clamped
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorBlockCornerForOpponent - converts the top-left corner of a 2x2 object (the drone) into the
|
|
; coordinates that the engine's mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn
|
|
; back into that same corner: X = 39-(X+1), Y = 39-(Y+1). Mirroring alone would give the bottom-right
|
|
; corner of the mirrored block, hence the pre-increment.
|
|
; In: X = column, Y = row of the block's top-left cell
|
|
; Out: X, Y = the mirrored corner; A clobbered
|
|
; Called from: the fake drone-position packet at $EEB1 and $EF8F, and from the drone-flight overlay.
|
|
; ----------------------------------------------------------------------
|
|
mirrorBlockCornerForOpponent:
|
|
inx ; EA38 the engine subtracts 1 after mirroring, so add it here first
|
|
txa ; EA39 column+1 into A
|
|
jsr mirrorMapCoordinate ; EA3A 39 - (column+1)
|
|
L_EA3D:
|
|
tax ; EA3D mirrored column back into X
|
|
iny ; EA3E same for the row
|
|
tya ; EA3F row+1 into A
|
|
jsr mirrorMapCoordinate ; EA40 39 - (row+1)
|
|
tay ; EA43 mirrored row back into Y
|
|
rts ; EA44 corner is now expressed the way a remote opponent would have sent it
|
|
|
|
|
|
; aiCmdQueuePeak - a single byte of data sitting in the code stream between
|
|
; mirrorBlockCornerForOpponent and queueAiCommand: the high-water mark of the AI's order queue. It is
|
|
; written and compared only inside queueAiCommand and read nowhere else, so it is a development
|
|
; leftover.
|
|
aiCmdQueuePeak:
|
|
.byte $00 ; EA45 . longest the AI order queue has ever grown this game; nothing ever reads it back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueAiCommand - appends the command staged in $8896..$8899 to the AI's 50-byte order queue at
|
|
; $88A0. It looks the total command length up with getCommandLength, keeps a high-water mark in
|
|
; aiCmdQueuePeak, and when the queue would reach 50 bytes it drops the command and turns the border
|
|
; blue instead, as a visible warning that the AI is producing orders faster than the lock-step packets
|
|
; can carry them away.
|
|
; In: $8896-$8899 = the staged command, $8952 aiCmdQueueLength, commandLengthTable through
|
|
; getCommandLength ($4D9F)
|
|
; Out: queue at $88A0 extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the
|
|
; copy counter),
|
|
; $EA45 peak updated; on overflow $9003 (the border-colour shadow) = 6 and the command is thrown
|
|
; away
|
|
; Called from: queueMirroredMoveOrder ($E95A) and queueComcenCommand ($EBE0); the trainer setup
|
|
; overlay has its own
|
|
; call at its own $EC82.
|
|
; ----------------------------------------------------------------------
|
|
queueAiCommand:
|
|
inc D_8968 ; EA46 count one more command issued this round
|
|
lda D_8896 ; EA49 the staged command byte
|
|
jsr getCommandLength ; EA4C currentCommandLength ($92EE) = 1 command byte plus its parameters
|
|
clc ; EA4F clear the carry
|
|
adc D_8952 ; EA50 how long the queue would be afterwards
|
|
cmp aiCmdQueuePeak ; EA53 beyond the previous high-water mark?
|
|
bcc L_EA5B ; EA56 no
|
|
sta aiCmdQueuePeak ; EA58 record the new peak
|
|
L_EA5B:
|
|
cmp #$32 ; EA5B the queue at $88A0 holds 50 bytes
|
|
bcc L_EA65 ; EA5D it still fits
|
|
lda #$06 ; EA5F colour 6 = blue
|
|
sta vicBorderShadow ; EA61 overflow: recolour the border through the VIC shadow and drop the command
|
|
rts ; EA64 command lost (the round counter at $8968 was already bumped)
|
|
|
|
L_EA65:
|
|
ldx #$00 ; EA65 copy from the first staging byte
|
|
ldy D_8952 ; EA67 append at the current end of the queue
|
|
L_EA6A:
|
|
lda D_8896,x ; EA6A next staged byte
|
|
sta D_88A0,y ; EA6D into the queue
|
|
inx ; EA70 advance the staging index
|
|
iny ; EA71 advance the queue index
|
|
inc D_8952 ; EA72 the queue is one byte longer
|
|
dec currentCommandLength ; EA75 one byte of this command done
|
|
bne L_EA6A ; EA78 until the whole command has been copied
|
|
rts ; EA7A the order is now waiting to be handed to the engine
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; injectAiCommandsIntoPacket - the heart of the solo trainer. Where the modem build would decode a
|
|
; packet that arrived over the wire, this build fabricates one: it copies whole commands out of the
|
|
; AI's own queue at $88A0 straight into the comm module's received-packet buffer ($E020, length
|
|
; $E01E), so the lock-step engine executes the computer opponent's orders through exactly the same
|
|
; command interpreter a human opponent's orders would use. It first runs the missile/drone decision,
|
|
; which may claim the whole packet for itself, and then moves as many complete commands as fit while
|
|
; the packet stays under 8 bytes, shifting the remainder of the queue down after each one.
|
|
; In: $8952 aiCmdQueueLength, the queue at $88A0, $E01E current packet length, getCommandLength
|
|
; ($4D9F)
|
|
; Out: $E020.. filled, $E01E updated, the queue compacted and $8952 reduced; $8966 cleared then
|
|
; possibly set by runAiMissileTurn;
|
|
; $92EE clobbered
|
|
; Called from: buildTrainerReplyPacket ($E0E4), once per lock-step exchange.
|
|
; ----------------------------------------------------------------------
|
|
injectAiCommandsIntoPacket:
|
|
lda #$00 ; EA7B clear the flag first
|
|
sta D_8966 ; EA7D $8966 bit 7 will mean 'the missile/drone code owns this packet'
|
|
jsr runAiMissileTurn ; EA80 offer the AI a missile shot or a drone launch; it writes straight into the packet
|
|
lda D_8966 ; EA83 did it claim the packet?
|
|
bmi L_EACE ; EA86 yes: leave the order queue for the next exchange
|
|
lda D_8952 ; EA88 is anything queued at all?
|
|
beq L_EACE ; EA8B no: send an empty packet
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One iteration per queued command: measure it, check that it still fits in the packet, copy it into
|
|
; the packet, then slide the rest of the queue down over it.
|
|
; ----------------------------------------------------------------------
|
|
L_EA8D:
|
|
ldy #$00 ; EA8D read from the front of the queue
|
|
lda D_88A0,y ; EA8F its command byte
|
|
jsr getCommandLength ; EA92 currentCommandLength = its total length
|
|
lda rxPacketLength ; EA95 bytes already in the fabricated 'received' packet
|
|
clc ; EA98 clear the carry
|
|
adc currentCommandLength ; EA99 plus this command
|
|
cmp #$08 ; EA9C a packet carries at most 7 command bytes
|
|
bcs L_EACE ; EA9E it would not fit: the rest waits for the next exchange
|
|
D_EAA0:
|
|
ldx rxPacketLength ; EAA0 append at the current end of the packet buffer
|
|
L_EAA3:
|
|
lda D_88A0,y ; EAA3 next byte of the command
|
|
sta rxPacketBuffer,x ; EAA6 into the received-packet buffer at $E020
|
|
inx ; EAA9 advance the packet index
|
|
iny ; EAAA advance the queue index
|
|
cpy currentCommandLength ; EAAB whole command copied?
|
|
bcc L_EAA3 ; EAAE no: keep copying
|
|
stx rxPacketLength ; EAB0 new packet length
|
|
ldx #$00 ; EAB3 now compact the queue from its start
|
|
L_EAB5:
|
|
lda D_88A0,y ; EAB5 byte just past the command that was taken
|
|
D_EAB8:
|
|
sta D_88A0,x ; EAB8 slide it down to the front
|
|
D_EABB:
|
|
inx ; EABB advance the write index
|
|
D_EABC:
|
|
iny ; EABC advance the read index
|
|
D_EABD:
|
|
cpy D_8952 ; EABD until the old end of the queue
|
|
D_EAC0:
|
|
bcc L_EAB5 ; EAC0 keep shifting
|
|
D_EAC2:
|
|
lda D_8952 ; EAC2 old queue length
|
|
D_EAC5:
|
|
sec ; EAC5 prepare the subtraction
|
|
D_EAC6:
|
|
sbc currentCommandLength ; EAC6 minus the command just handed over
|
|
sta D_8952 ; EAC9 new queue length
|
|
bne L_EA8D ; EACC more orders waiting: try to fit another into the same packet
|
|
L_EACE:
|
|
rts ; EACE the packet the engine is about to 'receive' is ready
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; updateAiUnitTracking - per-unit hook, run from trainerUnitStepHook after the engine has stepped each
|
|
; unit. It maintains three pieces of AI awareness. For a human unit it latches the first
|
|
; grunt/rider/boomer that has picked the AI comcen as its fire target into comcenAttackerUnit ($8972).
|
|
; For one of its own units it marks that unit's group as having the human comcen in reach (groupFlags
|
|
; bit 6) and stamps the unit for an immediate replan, once the comcen has been spotted and the unit is
|
|
; within 8 cells of it. Finally, an AI spy (units 96-98) that has reached the opponent's end zone
|
|
; while still inside an ordinary group is split off into a brand new group of its own, seeded with its
|
|
; current destination, so it can operate alone.
|
|
; In: $90FA currentUnit, $F76C unitTypeTable, $91F0 fireTargetUnit, $F9F5 human comcen energy byte
|
|
; (bit 7 = spotted),
|
|
; $F671/$F6D5 human comcen cell, $F640/$F6A4 unit cells, aiUnitOrderRound ($87CE+unit),
|
|
; aiUnitGroup ($8800+unit),
|
|
; aiUnitDeepFlag ($8832+unit), $91CB game clock, $8954 aiGroupCount, $8964 home group id
|
|
; Out: $8972 comcenAttackerUnit, groupFlags $893E bit 6, $8953 last allocated group id, the unit's
|
|
; group and round stamp,
|
|
; and the new group's target cell $88D2/$88DE
|
|
; Called from: trainerUnitStepHook ($E124), for every unit the engine steps.
|
|
; ----------------------------------------------------------------------
|
|
updateAiUnitTracking:
|
|
ldy currentUnit ; EACF the unit the engine has just stepped
|
|
cpy #$32 ; EAD2 unit 0-49 or 50-99?
|
|
bcs L_EAF1 ; EAD4 one of the AI's own: run the contact tracking
|
|
ldx D_8972 ; EAD6 comcenAttackerUnit, $FF while nobody is known to be shooting at the AI comcen
|
|
bpl L_EAEE ; EAD9 already latched: nothing to learn
|
|
lda fireTargetUnit ; EADB the unit this human unit has picked as its fire target
|
|
cmp #$63 ; EADE unit 99 = the AI comcen
|
|
bne L_EAEE ; EAE0 aiming at something else
|
|
lda unitTypeTable,y ; EAE2 its type byte
|
|
and #$07 ; EAE5 unit type 0-4
|
|
cmp #$03 ; EAE7 type 3 SPY or 4 COMCEN?
|
|
bcs L_EAEE ; EAE9 yes: they cannot shoot, so they are not a threat to record
|
|
D_EAEB:
|
|
sty D_8972 ; EAEB remember the first unit that is aiming at the AI comcen
|
|
L_EAEE:
|
|
jmp L_EB66 ; EAEE human unit handled
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; AI unit: has it got close enough to the human comcen to count as contact? The two coordinate tests
|
|
; accept a difference in -8..+7 by comparing the signed byte against $F8 and then against 8.
|
|
; ----------------------------------------------------------------------
|
|
L_EAF1:
|
|
lda side0ComcenEnergy ; EAF1 energy byte of the human comcen; bit 7 doubles as 'the AI has spotted it'
|
|
bpl L_EB2C ; EAF4 not spotted yet: there is no contact to record
|
|
lda side0ComcenCol ; EAF6 column of the human comcen
|
|
sec ; EAF9 prepare the subtraction
|
|
sbc unitColTable,y ; EAFA minus this AI unit's column
|
|
cmp #$F8 ; EAFD is the difference at least -8?
|
|
D_EAFF:
|
|
bcs L_EB05 ; EAFF yes, within 8 columns to one side
|
|
D_EB01:
|
|
cmp #$08 ; EB01 is it less than +8?
|
|
sub_EB03:
|
|
bcs L_EB2C ; EB03 no: more than 8 columns away
|
|
L_EB05:
|
|
lda side0ComcenRow ; EB05 row of the human comcen
|
|
sec ; EB08 prepare the subtraction
|
|
sbc unitRowTable,y ; EB09 minus this AI unit's row
|
|
cmp #$F8 ; EB0C is the difference at least -8?
|
|
bcs L_EB14 ; EB0E yes
|
|
cmp #$08 ; EB10 is it less than +8?
|
|
bcs L_EB2C ; EB12 no: more than 8 rows away
|
|
L_EB14:
|
|
ldx msgGetOpponentOnPhoneAnd,y; EB14 aiUnitGroup[unit], the group table addressed by absolute index
|
|
cpx D_8964 ; EB17 is it the home/guard group?
|
|
beq L_EB66 ; EB1A yes: that group is not supposed to chase the comcen
|
|
lda D_893E,x ; EB1C its groupFlags
|
|
ora #$40 ; EB1F bit 6 = this group has the human comcen within reach
|
|
sta D_893E,x ; EB21 store the flags back
|
|
bmi L_EB2C ; EB24 bit 7 set (the group is already suspended): do not restamp its clock
|
|
lda gameClock ; EB26 the down-counting round clock
|
|
sta msgWeWonByAKnockout,y ; EB29 aiUnitOrderRound[unit]: stamping the current clock makes the planner replan this unit at once (also mapGenerator6F00:msgWeWonByAKnockout)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; An AI spy (units 96-98) that has scored both end zones is promoted out of its group into a group of
|
|
; its own so it can act independently behind the human lines.
|
|
; ----------------------------------------------------------------------
|
|
L_EB2C:
|
|
cpy #$63 ; EB2C the AI comcen itself (unit 99)?
|
|
bcs L_EB66 ; EB2E yes: nothing else to do for it
|
|
cpy #$60 ; EB30 units 96-98 are the AI's three SPYs
|
|
bcc L_EB66 ; EB32 any other unit: done
|
|
lda D_8832,y ; EB34 aiUnitDeepFlag[unit], bit 7 = the unit has scored both territory zones
|
|
bpl L_EB66 ; EB37 this spy has not got that far yet
|
|
lda msgGetOpponentOnPhoneAnd,y; EB39 its current group id
|
|
cmp D_8954 ; EB3C does it still belong to one of the ordinary groups?
|
|
bcs L_EB66 ; EB3F no (already split off, or $FF): leave it alone
|
|
cmp D_8964 ; EB41 is it in the home/guard group?
|
|
beq L_EB66 ; EB44 yes: that one stays together
|
|
inc D_8953 ; EB46 allocate the next group id; note that aiGroupCount $8954 is not bumped with it, and that ids past 11 would run off the end of the twelve-entry per-group tables
|
|
lda D_8953 ; EB49 the new id
|
|
tax ; EB4C index the per-group tables with it
|
|
sta msgGetOpponentOnPhoneAnd,y; EB4D the spy becomes a group of its own
|
|
lda gameClock ; EB50 the current round
|
|
sta msgWeWonByAKnockout,y ; EB53 stamp it so the planner picks the new group up on the next scan (also mapGenerator6F00:msgWeWonByAKnockout)
|
|
lda unitDestColTable,y ; EB56 the destination the spy is already heading for
|
|
and #$3F ; EB59 coordinate bits only
|
|
sta D_88D2,x ; EB5B becomes the new group's target column
|
|
lda unitDestRowTable,y ; EB5E and its destination row
|
|
and #$3F ; EB61 coordinate bits only
|
|
sta D_88DE,x ; EB63 becomes the new group's target row
|
|
L_EB66:
|
|
rts ; EB66 unit tracked
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runAiComcenActions - spends whatever is left of the AI comcen's three command slots for this round.
|
|
; A stunned comcen needs two of them: one to halt itself and one for the 1-in-8 repair attempt that
|
|
; queues command $9E, exactly the odds the human REPAIR tab uses. An unstunned comcen is halted once
|
|
; and then spends every remaining slot on maybeQueueComcenEnergyRepair.
|
|
; In: $8968 commands already issued this round, $8971 the comcen's stun counter (low nibble of
|
|
; unitStunTable[99])
|
|
; Out: $8967 = the comcen's remaining slot budget, commands queued through queueAiCommand; returns
|
|
; silently when there is
|
|
; no budget left
|
|
; Called from: planTrainerRound at $E1CD (comcen too weak to attack) and $E21B (defend mode).
|
|
; ----------------------------------------------------------------------
|
|
runAiComcenActions:
|
|
lda #$03 ; EB67 the comcen may spend at most 3 command slots in a round
|
|
sec ; EB69 prepare the subtraction
|
|
sbc D_8968 ; EB6A minus the orders the planner has already queued
|
|
bcc L_EBE3 ; EB6D already over budget: do nothing at all
|
|
sta D_8967 ; EB6F slots left for the comcen
|
|
tax ; EB72 keep the budget in X as well
|
|
lda D_8971 ; EB73 the comcen's stun counter
|
|
beq L_EB96 ; EB76 not stunned: go straight to the energy repairs
|
|
cpx #$02 ; EB78 a stun repair attempt costs two slots
|
|
bcc L_EBE3 ; EB7A not enough left
|
|
dex ; EB7C spend one ...
|
|
dex ; EB7D ... and the second
|
|
stx D_8967 ; EB7E the reduced budget
|
|
jsr haltAiComcen ; EB81 halt the comcen; a moving comcen cannot be worked on
|
|
jsr nextGameRandom ; EB84 step the lock-step RNG
|
|
and #$07 ; EB87 one chance in eight
|
|
bne L_EBE3 ; EB89 the repair attempt failed this round
|
|
lda #$9E ; EB8B command $9E = cmdComcenRepairedOrSetupReady, which clears the comcen stun counter on both machines
|
|
jsr queueComcenCommand ; EB8D queue it ($8897/$8898 are ignored, the command is one byte long)
|
|
inc D_8967 ; EB90 cancel the DEC that the shared loop tail below is about to do
|
|
jmp L_EB9F ; EB93 join the energy-repair loop with the remaining budget
|
|
|
|
L_EB96:
|
|
txa ; EB96 slots left
|
|
beq L_EBE3 ; EB97 none: nothing to do
|
|
jsr haltAiComcen ; EB99 halt the comcen so it can repair
|
|
L_EB9C:
|
|
jsr maybeQueueComcenEnergyRepair; EB9C spend one slot on an energy repair
|
|
L_EB9F:
|
|
dec D_8967 ; EB9F one slot used
|
|
bne L_EB9C ; EBA2 keep spending until the budget is exhausted
|
|
rts ; EBA4 the comcen has done what it can this round
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; maybeQueueComcenEnergyRepair - the trainer's version of the human console's energy repair. With 50
|
|
; percent probability, and only while the AI comcen's energy is below 45 of 50, it queues command $9A
|
|
; cmdAddEnergy for unit 99 with the same amount the human repair uses, (50 - energy) / 8 + 1. The
|
|
; projected energy is accumulated in $8965 (which planTrainerRound seeded from the comcen's real
|
|
; energy and uses as its action budget), and the amount is nudged by one whenever that projection
|
|
; would land on a multiple of four.
|
|
; In: $FA27 = unitEnergyTable[99] (energy in bits 0-5), $8965 projected comcen energy, nextGameRandom
|
|
; Out: $8896 = $9A, $8897 = $63 (unit 99), $8898 = the amount, command queued; $8965 raised by the
|
|
; same amount
|
|
; Called from: runAiComcenActions ($EB9C), once per remaining comcen command slot.
|
|
; ----------------------------------------------------------------------
|
|
maybeQueueComcenEnergyRepair:
|
|
jsr nextGameRandom ; EBA5 step the lock-step RNG
|
|
and #$01 ; EBA8 an energy repair succeeds one time in two, as on the human console
|
|
beq L_EBE3 ; EBAA failed this time: spend the slot on nothing
|
|
lda side1ComcenEnergy ; EBAC energy byte of AI comcen unit 99
|
|
and #$3F ; EBAF energy 0-50 lives in bits 0-5; bits 6-7 are visibility flags
|
|
cmp #$2D ; EBB1 45 out of 50 or better?
|
|
bcs L_EBE3 ; EBB3 healthy enough: do not waste a command on it
|
|
sta scratch18 ; EBB5 keep the current energy
|
|
lda #$32 ; EBB7 50 = a full comcen
|
|
sec ; EBB9 prepare the subtraction
|
|
sbc scratch18 ; EBBA the shortfall
|
|
lsr a ; EBBC divide by 8 ...
|
|
lsr a ; EBBD ...
|
|
lsr a ; EBBE ... so repairs get smaller as the comcen recovers
|
|
clc ; EBBF clear the carry
|
|
adc #$01 ; EBC0 amount = shortfall/8 + 1
|
|
sta D_8898 ; EBC2 second parameter of the command
|
|
clc ; EBC5 clear the carry
|
|
adc D_8965 ; EBC6 add it to the energy the comcen is projected to end up with
|
|
sta D_8965 ; EBC9 keep the projection
|
|
and #$03 ; EBCC did the projection land on a multiple of four?
|
|
bne L_EBD6 ; EBCE no: the amount stands
|
|
inc D_8898 ; EBD0 yes: repair one point more so it does not
|
|
inc D_8965 ; EBD3 and keep the projection in step with it
|
|
L_EBD6:
|
|
lda #$63 ; EBD6 unit 99 = the AI comcen
|
|
sta msgPickUpPhoneThenPressSpace; EBD8 first parameter of the command ($8897)
|
|
lda #$9A ; EBDB command $9A = cmdAddEnergy (unit, amount); falls straight into queueComcenCommand
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueComcenCommand - shared tail of the comcen actions: stores A as the command byte and jumps into
|
|
; queueAiCommand, leaving
|
|
; $8897 and $8898 as the caller staged them.
|
|
; In: A = command byte, $8897/$8898 = parameters already staged
|
|
; Out: the command appended to the AI order queue
|
|
; Called from: runAiComcenActions ($EB8D) and by falling through from maybeQueueComcenEnergyRepair
|
|
; ($EBDB).
|
|
; ----------------------------------------------------------------------
|
|
queueComcenCommand:
|
|
sta D_8896 ; EBDD command byte into the staging area
|
|
jmp queueAiCommand ; EBE0 append the staged bytes to the queue
|
|
|
|
L_EBE3:
|
|
rts ; EBE3 shared 'do nothing this slot' exit of the comcen action code
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; haltAiComcen - orders the AI comcen to move to the cell it is already standing on.
|
|
; orderAiUnitToCell turns that into a real
|
|
; $80/$81 command only when the comcen currently has some other destination, so this stops a moving
|
|
; comcen and costs nothing when
|
|
; it is already still. A comcen has to be stationary before it can repair or launch.
|
|
; In: $F6A3 unitCol[99], $F707 unitRow[99]
|
|
; Out: possibly one $80/$81 command queued; nothing at all when the comcen is already halted
|
|
; Called from: runAiComcenActions ($EB81 stunned path, $EB99 repair path).
|
|
; ----------------------------------------------------------------------
|
|
haltAiComcen:
|
|
ldx side1ComcenCol ; EBE4 the column the AI comcen is standing on
|
|
lda side1ComcenRow ; EBE7 and its row
|
|
ldy #$31 ; EBEA AI ordinal 49 = absolute unit 99, the AI comcen
|
|
jmp orderAiUnitToCell ; EBEC ordering it to its own cell halts it; a redundant order is dropped
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; assessThreatAroundAiComcen - builds a directional pressure map around the AI comcen and picks the
|
|
; direction to run. Every living human combat unit (ordinals 0-45: the 28 grunts, 12 riders and 6
|
|
; boomers) within 20 cells adds a weight into one of eight compass buckets at $8896-$889D: 1 for
|
|
; merely being nearby, +1 within 10 cells, and +4 more when the comcen is inside that unit's weapon
|
|
; range (5 cells, 9 for a boomer). The base 1 is dropped once the comcen has actually been hit
|
|
; ($897C), so only units that can reach it steer the escape. The tail rankThreatDirections ($EC82,
|
|
; just past this chunk) smooths the ring and returns the centre of the quietest three-direction arc.
|
|
; In: zp_27/zp_28 = AI comcen column/row (set by the caller at $E1D1),
|
|
; unitTypeTable/unitColTable/unitRowTable,
|
|
; $897C = 'the comcen was hit this round or last', zp_57 game RNG low byte for the tie break
|
|
; Out: A = (total pressure - number of threats) / 2 = overall threat level, X = deepest weapon-range
|
|
; surplus (0 = nothing in
|
|
; range), Y = safest compass direction 0-7, or 8 (the null delta entry) when nothing threatens;
|
|
; $8896-$889D and $0400-$0407 hold the direction buckets, zp_18-zp_23 clobbered
|
|
; Called from: planTrainerRound ($E1DB), which files the results into $8962/$8960/$8961, and the drone
|
|
; code ($EF7B).
|
|
; ----------------------------------------------------------------------
|
|
assessThreatAroundAiComcen:
|
|
lda #$00 ; EBEF clear the accumulators
|
|
sta scratch1F ; EBF1 total enemy pressure
|
|
sta scratch20 ; EBF3 number of units that count as a threat
|
|
sta scratch21 ; EBF5 deepest weapon-range surplus seen
|
|
sta scratch1E ; EBF7 cleared but never read again in either half of the routine: a leftover
|
|
ldy #$07 ; EBF9 clear the eight compass buckets, 7 down to 0
|
|
L_EBFB:
|
|
sta D_8896,y ; EBFB bucket for direction Y
|
|
D_EBFE:
|
|
dey ; EBFE next direction
|
|
bpl L_EBFB ; EBFF until all eight are clear
|
|
|
|
; ----------------------------------------------------------------------
|
|
; One pass per human combat unit, ordinal 45 down to 0 (the roster is 28 grunts, 12 riders, 6 boomers,
|
|
; 3 spies and the comcen, so 0-45 is exactly the units that can shoot).
|
|
; ----------------------------------------------------------------------
|
|
lda #$2D ; EC01 scan the human player's units 45 down to 0
|
|
sta scratch18 ; EC03 loop counter
|
|
L_EC05:
|
|
ldx scratch18 ; EC05 the unit being weighed
|
|
lda unitTypeTable,x ; EC07 its type byte
|
|
D_EC0A:
|
|
bmi L_EC74 ; EC0A destroyed: skip it
|
|
ldy #$05 ; EC0C weapon range 5 cells for a grunt or rider
|
|
and #$02 ; EC0E bit 1 of the type marks a BOOMER (type 2); a SPY would match too, but the scan never reaches one
|
|
beq L_EC14 ; EC10 not a boomer: keep range 5
|
|
L_EC12:
|
|
ldy #$09 ; EC12 a boomer reaches 9 cells
|
|
L_EC14:
|
|
sty scratch19 ; EC14 weapon range of this unit
|
|
lda unitColTable,x ; EC16 its column
|
|
sta cellColA ; EC19 point A of the distance test
|
|
sta lineEndCol ; EC1B and the target cell of the direction test
|
|
lda unitRowTable,x ; EC1D its row
|
|
sta cellRowA ; EC20 point A row
|
|
sta lineEndRow ; EC22 target row
|
|
jsr cellDistance ; EC24 distance from the AI comcen, which the caller left in cellColB/cellRowB
|
|
cmp #$14 ; EC27 20 cells
|
|
bcs L_EC74 ; EC29 too far away to matter
|
|
sub_EC2B:
|
|
sta scratch1A ; EC2B keep the distance
|
|
lda cellColB ; EC2D the comcen's own cell ...
|
|
sta lineCurCol ; EC2F ... is where the direction is measured from
|
|
lda cellRowB ; EC31 comcen row
|
|
sta lineCurRow ; EC33 line walker row
|
|
jsr getStepDirection ; EC35 Y = compass direction from the comcen toward this unit
|
|
bpl L_EC3C ; EC38 a real direction 0-7
|
|
ldy #$00 ; EC3A the unit is standing on the comcen's own cell: charge it to direction 0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Weigh this unit: a base 1 for being nearby (suppressed once the comcen is actually under fire), +1
|
|
; within 10 cells and +4 more when the comcen sits inside its weapon range.
|
|
; ----------------------------------------------------------------------
|
|
L_EC3C:
|
|
inc scratch20 ; EC3C one more threatening unit
|
|
ldx #$00 ; EC3E the weight this unit will contribute
|
|
bit D_897C ; EC40 was the comcen hit in this round or the previous one?
|
|
bmi L_EC46 ; EC43 yes: only units that can actually reach it get any weight
|
|
inx ; EC45 no: every nearby enemy is worth 1
|
|
L_EC46:
|
|
lda scratch1A ; EC46 the distance again
|
|
cmp #$0A ; EC48 closer than 10 cells?
|
|
bcs L_EC5E ; EC4A no: it only counts as a distant presence
|
|
inx ; EC4C +1 for being close
|
|
lda scratch19 ; EC4D its weapon range
|
|
sec ; EC4F prepare the subtraction
|
|
sbc scratch1A ; EC50 range minus distance: how deep the comcen sits inside its envelope
|
|
bcc L_EC5E ; EC52 the comcen is out of range of this unit
|
|
sub_EC54:
|
|
inx ; EC54 +1 ...
|
|
inx ; EC55 +2 ...
|
|
inx ; EC56 +3 ...
|
|
inx ; EC57 +4 more: the comcen is inside this unit's weapon range
|
|
sub_EC58:
|
|
cmp scratch21 ; EC58 is this the deepest penetration so far?
|
|
L_EC5A:
|
|
bcc L_EC5E ; EC5A no
|
|
sta scratch21 ; EC5C remember the worst range surplus, returned in X
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Add the weight to its compass bucket and to the running total, both saturating at 255.
|
|
; ----------------------------------------------------------------------
|
|
L_EC5E:
|
|
txa ; EC5E the weight of this unit
|
|
clc ; EC5F clear the carry
|
|
adc D_8896,y ; EC60 into the bucket of its compass direction
|
|
bcc L_EC67 ; EC63 no overflow
|
|
lda #$FF ; EC65 saturate the bucket at 255
|
|
L_EC67:
|
|
sta D_8896,y ; EC67 store the bucket back
|
|
txa ; EC6A the weight again
|
|
clc ; EC6B clear the carry
|
|
L_EC6C:
|
|
adc scratch1F ; EC6C into the overall pressure total
|
|
bcc L_EC72 ; EC6E no overflow
|
|
lda #$FF ; EC70 saturate the total at 255
|
|
L_EC72:
|
|
sta scratch1F ; EC72 store it back
|
|
L_EC74:
|
|
dec scratch18 ; EC74 next human unit
|
|
bpl L_EC05 ; EC76 until unit 0 has been weighed
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Hand the ring over to rankThreatDirections, or - when nothing at all threatened the comcen - report
|
|
; the null direction 8, whose entry in dirToColDelta/dirToRowDelta is the zero step.
|
|
; ----------------------------------------------------------------------
|
|
lda scratch20 ; EC78 did anything threaten the comcen?
|
|
bne rankThreatDirections ; EC7A yes: smooth the ring and pick the quietest arc
|
|
tax ; EC7C no threats: A = X = 0
|
|
ldy #$08 ; EC7D direction 8 = the null entry of the direction tables, i.e. 'no need to move'
|
|
jmp threatScanDone ; EC7F share the RTS at the end of rankThreatDirections
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rankThreatDirections - Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just
|
|
; turned every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one
|
|
; of the eight compass buckets $8896-$889D. This half blurs that ring circularly, finds the centre of
|
|
; the quietest three-direction arc and returns the summary the planner stores in $8960-$8962.
|
|
; In: $8896-$889D = raw per-direction pressure, zp_1F = total pressure, zp_20 = number of units that
|
|
; contributed, zp_21 = deepest weapon-range penetration, zp_57 = low byte of the lock-step game
|
|
; RNG state (tie break)
|
|
; Out: A = (zp_1F - zp_20) / 2 = overall threat level, X = zp_21, Y = zp_18 = the quietest compass
|
|
; direction 0-7; $8896-$889D holds the blurred ring and $0400-$0407 the raw copy; zp_18, zp_19,
|
|
; zp_1A and zp_23 clobbered
|
|
; Called from: branch at $EC7A only (the tail of $EBEF); when no unit threatened at all the path at
|
|
; $EC7C returns A=0, X=0, Y=8 instead - index 8 is the deliberate null entry (0,0) of
|
|
; dirToColDelta/dirToRowDelta
|
|
; Take an unsmoothed copy of the eight direction buckets into the $0400 scratch page, because the blur
|
|
; below overwrites $8896-$889D while it still needs each direction's original neighbours.
|
|
;
|
|
; A warning for readers of the rest of this chunk: $EC00-$EFFF is shared by four different overlay
|
|
; images (this trainer tail, the modem driver, the track 29 trainer setup and the track 29 drone AI),
|
|
; and XREF merges their cross references. Many of the L_/D_/sub_ labels below are branch targets of
|
|
; one of the other builds that happen to land on an instruction boundary here; in this file they sit
|
|
; in the middle of a routine and nothing reaches them.
|
|
; ----------------------------------------------------------------------
|
|
rankThreatDirections:
|
|
ldy #$07 ; EC82 start at direction 7 (north-west) and walk the ring backwards
|
|
copyThreatRingLoop:
|
|
lda D_8896,y ; EC84 raw pressure of direction Y from the 8-byte ring at $8896
|
|
sta checksumXorConstant,y ; EC87 keep the untouched copy in the $0400 scratch page (the same page finishRound uses for scan lists) (also trainerPlaybook0200:play3Col12)
|
|
dey ; EC8A next direction down
|
|
bpl copyThreatRingLoop ; EC8B until all 8 directions have been copied
|
|
lda #$07 ; EC8D the blur pass also starts at direction 7
|
|
sta scratch23 ; EC8F zp_23 = the direction currently being smoothed
|
|
|
|
; ----------------------------------------------------------------------
|
|
; smoothThreatRing - Circular 1-2-1 blur of the eight-direction threat ring. For i = 7 down to 0 it
|
|
; computes (2*raw[i] + raw[i-1] + raw[i+1]) / 4, reading the untouched copy from $0400 and writing the
|
|
; result back over $8896+i. Indices wrap modulo 8, so north's neighbours are north-west and
|
|
; north-east.
|
|
; In: $0400-$0407 = raw direction weights, zp_23 = direction index counter (7 on entry)
|
|
; Out: $8896-$889D = blurred weights; zp_19 = the sum's high bits; zp_23 ends at $FF; A/X/Y clobbered
|
|
; Called from: nothing - this is the loop head of rankThreatDirections, branched to from the DEC/BPL
|
|
; at $ECCA. (XREF shows a JSR from $7AE9, but that call belongs to the other overlay that shares
|
|
; these addresses.)
|
|
; ----------------------------------------------------------------------
|
|
smoothThreatRing:
|
|
lda scratch23 ; EC91 i = the direction being smoothed
|
|
L_EC93:
|
|
tax ; EC93 X indexes the centre sample
|
|
tay ; EC94 Y will index the neighbour on the counter-clockwise side ...
|
|
dey ; EC95 ... i - 1
|
|
bpl blurSumStart ; EC96 no wrap needed while i-1 is still 0..7
|
|
ldy #$07 ; EC98 i was 0, so its counter-clockwise neighbour is direction 7
|
|
blurSumStart:
|
|
lda #$00 ; EC9A clear the two extra bits the weighted sum can produce
|
|
L_EC9C:
|
|
sta scratch19 ; EC9C zp_19 = bits 8-9 of the running sum
|
|
lda checksumXorConstant,x ; EC9E raw pressure of the centre direction (also trainerPlaybook0200:play3Col12)
|
|
asl a ; ECA1 double it - the centre direction carries weight 2
|
|
bcc addCounterClockwiseNeighbour; ECA2 no carry out of the doubling
|
|
inc scratch19 ; ECA4 carry the 9th bit into zp_19
|
|
addCounterClockwiseNeighbour:
|
|
clc ; ECA6 clear carry before adding a neighbour
|
|
adc checksumXorConstant,y ; ECA7 + the counter-clockwise neighbour, weight 1 (also trainerPlaybook0200:play3Col12)
|
|
bcc stepToClockwiseNeighbour; ECAA no carry out of the add
|
|
inc scratch19 ; ECAC carry into the high bits again
|
|
stepToClockwiseNeighbour:
|
|
inx ; ECAE step X to the clockwise neighbour, i + 1
|
|
cpx #$08 ; ECAF past the last direction?
|
|
bcc addClockwiseNeighbour ; ECB1 no, i+1 is still inside the ring
|
|
ldx #$00 ; ECB3 wrap i+1 round to direction 0
|
|
addClockwiseNeighbour:
|
|
clc ; ECB5 clear carry before the last add
|
|
adc checksumXorConstant,x ; ECB6 + the clockwise neighbour, weight 1 (also trainerPlaybook0200:play3Col12)
|
|
bcc finishBlurredValue ; ECB9 no carry
|
|
inc scratch19 ; ECBB carry into the high bits
|
|
finishBlurredValue:
|
|
lsr scratch19 ; ECBD divide the 10-bit sum by 4: high bits down ...
|
|
ror a ; ECBF ... into A (first halving)
|
|
lsr scratch19 ; ECC0 and again ...
|
|
ror a ; ECC2 ... A = (2*raw[i] + raw[i-1] + raw[i+1]) / 4
|
|
ldy scratch23 ; ECC3 Y = i again
|
|
sta D_8896,y ; ECC5 write the blurred pressure back into the live ring at $8896+i
|
|
dec scratch23 ; ECC8 next direction down
|
|
bpl smoothThreatRing ; ECCA repeat for directions 7..0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Now look for the calmest 135-degree arc: three adjacent directions whose blurred pressures add up to
|
|
; the smallest total. The centre of that arc is the direction the comcen will run towards.
|
|
; ----------------------------------------------------------------------
|
|
lda #$FF ; ECCC $FF = no arc chosen yet, and as an arc sum 255 is worse than anything real
|
|
sta scratch18 ; ECCE zp_18 = best (quietest) centre direction so far
|
|
sta scratch19 ; ECD0 zp_19 = the best arc sum so far
|
|
ldy #$07 ; ECD2 test the eight arcs starting with the one led by direction 7
|
|
|
|
; ----------------------------------------------------------------------
|
|
; findQuietestDirectionArc - For every leading direction y = 7..0 it adds the blurred pressures of y,
|
|
; y-1 and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle
|
|
; direction y-1. The result is the centre of the calmest 135-degree wedge around the reference point.
|
|
; Exact ties are decided by bit 7 of the game RNG state, so the trainer does not always break them the
|
|
; same way.
|
|
; In: $8896-$889D = blurred direction weights, zp_57 = game RNG state byte 0, zp_18/zp_19 preset to
|
|
; $FF
|
|
; Out: zp_18 = quietest centre direction 0-7, zp_19 = its arc sum; zp_1A = scratch for the candidate
|
|
; centre; A/X/Y clobbered
|
|
; Called from: nothing - loop head of rankThreatDirections, branched to from the DEY/BPL at $ECFE.
|
|
; (The XREF calls from $EC74/$EC7E belong to the modem-driver build that shares this address.)
|
|
; ----------------------------------------------------------------------
|
|
findQuietestDirectionArc:
|
|
tya ; ECD4 A = the arc's leading direction
|
|
tax ; ECD5 X indexes the first of the three samples
|
|
lda D_8896,x ; ECD6 blurred pressure of direction y
|
|
dex ; ECD9 step to y - 1, the middle of the arc
|
|
bpl addArcMiddleDirection ; ECDA no wrap needed
|
|
ldx #$07 ; ECDC y was 0, so the middle direction is 7
|
|
addArcMiddleDirection:
|
|
stx scratch1A ; ECDE remember the middle direction - that is what the caller receives
|
|
clc ; ECE0 clear carry before the add
|
|
adc D_8896,x ; ECE1 + pressure of the middle direction
|
|
dex ; ECE4 step to y - 2, the trailing direction
|
|
bpl addArcTrailingDirection ; ECE5 no wrap needed
|
|
L_ECE7:
|
|
ldx #$07 ; ECE7 wrap y-2 round to direction 7
|
|
addArcTrailingDirection:
|
|
clc ; ECE9 clear carry before the last add
|
|
adc D_8896,x ; ECEA + pressure of the trailing direction; A = the arc total (8-bit, and it can wrap if the whole ring is saturated at $FF)
|
|
L_ECED:
|
|
cmp scratch19 ; ECED compare with the quietest arc found so far
|
|
bcc rememberQuietestArc ; ECEF strictly quieter - take this arc
|
|
bne nextArcCandidate ; ECF1 louder - keep the previous best
|
|
L_ECF3:
|
|
bit gameRngState0 ; ECF3 a tie: toss a coin on bit 7 of the game RNG state. zp_57 is not stepped here, so every tie inside one call falls the same way
|
|
bmi nextArcCandidate ; ECF5 coin says keep the arc that was found first
|
|
rememberQuietestArc:
|
|
sta scratch19 ; ECF7 remember the new best arc sum ...
|
|
ldx scratch1A ; ECF9 ... and its middle direction ...
|
|
stx scratch18 ; ECFB ... as the escape direction to return
|
|
nextArcCandidate:
|
|
dey ; ECFD next leading direction
|
|
bpl findQuietestDirectionArc; ECFE eight arcs in all
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Return the summary: A = threat level, X = deepest weapon-range penetration, Y = escape direction.
|
|
; Subtracting the unit count removes the +1 base weight each threatening unit contributed, so what is
|
|
; left is the 'extra' pressure from being close or inside somebody's weapon range.
|
|
; ----------------------------------------------------------------------
|
|
lda scratch1F ; ED00 total pressure accumulated over all threatening units
|
|
sec ; ED02 no borrow
|
|
sbc scratch20 ; ED03 minus one per threatening unit: strip the base weight each one added
|
|
lsr a ; ED05 halve it - A is the threat level the planner keeps in $8962
|
|
ldy scratch18 ; ED06 Y = the quietest direction; callers add 4 mod 8 to get the direction the enemy is in
|
|
ldx scratch21 ; ED08 X = the largest weapon-range surplus seen, 0 = nothing has this point in range ($8961)
|
|
threatScanDone:
|
|
rts ; ED0A shared exit; the 'nothing threatens at all' path at $EC7F jumps here with A=0, X=0, Y=8
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runAiMissileTurn - The computer opponent's one 'weapon' decision per lock-step exchange. It first
|
|
; offers to launch the AI's own drone (launchAiDrone), then, if the human is steering a drone of his
|
|
; own and the AI still has missiles and is reloaded, it decides whether to shoot that drone down: the
|
|
; aim point is the drone's cell, a reluctance score is built from the range to the AI comcen, the
|
|
; comcen's stun state and energy and a missile-conservation rule, and the shot either kills the drone
|
|
; (command $92 plus the $90 burst) or lands scattered around it. Whatever happens it falls through
|
|
; into fireMissileAtSpottedUnit.
|
|
; In: $8966 action budget for this exchange (0 = free), $920D drone state flags, $92B1 AI missiles
|
|
; left, $92AE human drones left, $8970 reload countdown, $92F6/$92F7 drone camera window origin,
|
|
; $F6A3/$F707 AI comcen cell, $F9C3 comcen stun, $FA27 comcen energy, $920B/$896F drone heading,
|
|
; game RNG
|
|
; Out: may put command $92 and/or $90 col,row into the fabricated 'received' packet ($E020, length
|
|
; $E01E); $8970 reload timer, $8966 spent, $8968 += 2;
|
|
; zp_22/zp_23/zp_24/zp_27/zp_28/zp_29/zp_2B/zp_18 clobbered
|
|
; Called from: injectAiCommandsIntoPacket ($EA80), which itself runs once per exchange from the $E000
|
|
; jump-table entry with X=1
|
|
; The AI is allowed exactly one drone launch or missile shot per exchange: injectAiCommandsIntoPacket
|
|
; clears $8966 to 0 before calling, and each action decrements it to $FF.
|
|
; ----------------------------------------------------------------------
|
|
runAiMissileTurn:
|
|
jsr launchAiDrone ; ED0B first chance of the exchange: send a drone up if one is due
|
|
lda D_8966 ; ED0E $8966 = this exchange's action budget
|
|
bmi skipAntiDroneShot ; ED11 the drone launch already used it - go straight to the ground-target path
|
|
lda side1ComcenCol ; ED13 column of the AI's own comcen (unit 99)
|
|
sta cellColB ; ED16 zp_27/zp_28 = point B of every cellDistance below: the comcen is what the AI defends
|
|
L_ED18:
|
|
lda side1ComcenRow ; ED18 row of the AI comcen
|
|
sta cellRowB ; ED1B zp_28 = reference row
|
|
bit droneControlFlags ; ED1D $920D bit 6 = the human is steering his own drone right now
|
|
bvs considerAntiDroneShot ; ED20 he is - think about shooting it down
|
|
skipAntiDroneShot:
|
|
jmp fireMissileAtSpottedUnit; ED22 no drone to shoot at, so look for an ordinary target
|
|
|
|
considerAntiDroneShot:
|
|
lda opponentMissilesLeft ; ED25 $92B1 = missiles the computer opponent has left
|
|
beq skipAntiDroneShot ; ED28 out of missiles
|
|
lda D_8970 ; ED2A $8970 = reload countdown; $FF (negative) means ready to fire
|
|
L_ED2D:
|
|
bpl skipAntiDroneShot ; ED2D still reloading
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Aim at the human's drone. $92F6/$92F7 are the top-left corner of his 14x12 drone camera window and
|
|
; the drone always sits in the middle of it, hence the +6/+5.
|
|
; ----------------------------------------------------------------------
|
|
lda #$00 ; ED2F 0 = 'the comcen is undamaged' until proved otherwise
|
|
L_ED31:
|
|
sta scratch2B ; ED31 zp_2B = comcen-damaged flag
|
|
lda droneViewOriginCol ; ED33 left edge of the drone camera window
|
|
clc ; ED36 clear carry for the add
|
|
adc #$06 ; ED37 the drone sits 6 columns into the window
|
|
sta cellColA ; ED39 zp_22 = aim column (point A)
|
|
lda droneViewOriginRow ; ED3B top edge of the drone camera window
|
|
clc ; ED3E clear carry
|
|
adc #$05 ; ED3F the drone sits 5 rows into the window
|
|
sta cellRowA ; ED41 zp_24 = aim row
|
|
L_ED43:
|
|
jsr cellDistance ; ED43 A = range in cells (0-16) from the AI comcen to the drone
|
|
sta scratch23 ; ED46 zp_23 = reluctance to fire, starting at that range
|
|
lsr a ; ED48 half the range ...
|
|
sta scanListIndex ; ED49 ... is what the reload timer will be set to if the shot goes off
|
|
lda side1ComcenStun ; ED4B stun byte of the AI comcen (unit 99)
|
|
and #$0F ; ED4E bits 0-3 = the stun counter
|
|
beq checkComcenEnergy ; ED50 not stunned - check the energy instead
|
|
dec scratch2B ; ED52 zp_2B = $FF: remember that the comcen is stunned - it costs another point of reluctance at $ED69
|
|
L_ED54:
|
|
cmp #$0E ; ED54 stun counter 14 or 15 = the comcen is effectively out of action
|
|
bcs skipAntiDroneShot ; ED56 too badly stunned to shoot at a drone
|
|
bcc bumpReluctance ; ED58 always taken (the CMP above set carry only for >= $0E): skip the energy test
|
|
checkComcenEnergy:
|
|
lda side1ComcenEnergy ; ED5A energy byte of the AI comcen
|
|
and #$03 ; ED5D low two bits of the 0-50 energy value
|
|
bne applyStunPenalty ; ED5F not a multiple of 4 - no extra hesitation
|
|
bumpReluctance:
|
|
inc scratch23 ; ED61 energy divisible by 4: +1 reluctance, a cheap energy-driven hesitation
|
|
applyStunPenalty:
|
|
lda scratch23 ; ED63 A = the reluctance BEFORE the stun bump below
|
|
bit scratch2B ; ED65 was the comcen stunned? (zp_2B = $FF if so)
|
|
bpl testCloseShot ; ED67 no
|
|
inc scratch23 ; ED69 stunned: +1 reluctance. A keeps the old value, so the test at $ED6B still uses the unbumped score
|
|
testCloseShot:
|
|
cmp #$03 ; ED6B reluctance under 3 - a close shot from a healthy comcen: fire without the conservation test
|
|
bcc fireAntiDroneMissile ; ED6D take the shot
|
|
testReluctanceLimit:
|
|
lda scratch23 ; ED6F reload the (possibly bumped) reluctance
|
|
cmp #$05 ; ED71 5 or more = far away and/or a hurt comcen ...
|
|
bcs skipAntiDroneShot ; ED73 ... not worth a missile
|
|
lda dronesLeft ; ED75 $92AE = drones the human still has in stock
|
|
L_ED78:
|
|
asl a ; ED78 x2 ...
|
|
asl a ; ED79 ... x4: four missiles are reserved for every future drone
|
|
sta scratch18 ; ED7A zp_18 = the reserve
|
|
lda opponentMissilesLeft ; ED7C missiles the AI owns
|
|
sec ; ED7F no borrow wanted
|
|
sbc scratch18 ; ED80 missiles above the reserve
|
|
bcs clampMissileSurplus ; ED82 positive surplus
|
|
lda #$00 ; ED84 below the reserve - treat the surplus as 0
|
|
clampMissileSurplus:
|
|
clc ; ED86 clear carry
|
|
adc #$01 ; ED87 surplus + 1
|
|
cmp scratch23 ; ED89 compare with the reluctance
|
|
bcc fireMissileAtSpottedUnit; ED8B too few spare missiles for a shot this reluctant - save it for later
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The shot goes off. Whether it actually hits is decided by one random byte against a threshold that
|
|
; grows with the reluctance and jumps by $40 when the drone has changed heading since the last shot,
|
|
; so a jinking drone is much harder to hit.
|
|
; ----------------------------------------------------------------------
|
|
fireAntiDroneMissile:
|
|
lda scanListIndex ; ED8D half the range, computed at $ED48
|
|
sta D_8970 ; ED8F start the reload countdown
|
|
inc scratch23 ; ED92 reluctance + 1 so that even a perfect shot can miss
|
|
lda scratch23 ; ED94 A = reluctance + 1
|
|
asl a ; ED96 x2 ...
|
|
asl a ; ED97 x4 ...
|
|
asl a ; ED98 x8 ...
|
|
asl a ; ED99 x16 ...
|
|
asl a ; ED9A x32: the miss threshold is (reluctance+1) * 32
|
|
ldx droneHeading ; ED9B $920B = the heading of the drone right now
|
|
cpx D_896F ; ED9E $896F = the heading it had at the last shot
|
|
beq storeMissThreshold ; EDA1 unchanged - the drone is flying straight
|
|
clc ; EDA3 clear carry
|
|
adc #$40 ; EDA4 it jinked: +$40 makes a hit far less likely
|
|
storeMissThreshold:
|
|
sta scratch18 ; EDA6 zp_18 = miss threshold
|
|
jsr nextGameRandom ; EDA8 step the lock-step game RNG (both machines would get the same byte)
|
|
cmp scratch18 ; EDAB random byte against the threshold
|
|
bcs droneShotDownCleanly ; EDAD random >= threshold: a clean hit on the drone
|
|
lsr scratch23 ; EDAF a miss: scatter radius = reluctance / 2 ...
|
|
inc scratch23 ; EDB1 ... + 1, so it is at least one cell off
|
|
lda scratch23 ; EDB3 A = scatter radius
|
|
jsr randomlySignValue ; EDB5 randomly plus or minus
|
|
clc ; EDB8 clear carry
|
|
adc cellColA ; EDB9 shift the aim column
|
|
sta cellColA ; EDBB zp_22 = the missed column
|
|
lda scratch23 ; EDBD A = scatter radius again
|
|
L_EDBF:
|
|
jsr randomlySignValue ; EDBF randomly plus or minus
|
|
clc ; EDC2 clear carry
|
|
adc cellRowA ; EDC3 shift the aim row
|
|
sta cellRowA ; EDC5 zp_24 = the missed row
|
|
jmp queueScatteredStrike ; EDC7 queue the strike at the scattered cell
|
|
|
|
droneShotDownCleanly:
|
|
lda #$92 ; EDCA command $92 = DRONE SHOT DOWN (handler cmdDroneShotDown $538B, clears $920D bits 6-7)
|
|
sta rxPacketBuffer ; EDCC it is the first byte of the packet - $E01E is still 0 whenever this path is reached
|
|
inc rxPacketLength ; EDCF packet is now 1 byte long
|
|
queueScatteredStrike:
|
|
jsr queueMissileStrikeCommand; EDD2 append the $90 strike so the human also sees the missile burst on the drone's cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; fireMissileAtSpottedUnit - Second half of the missile turn, reached whenever no anti-drone shot was
|
|
; taken. It fires a comcen missile at the unit stored in $8972 (the last human unit seen shooting at
|
|
; the AI comcen) or, when there is none or it is dead, at the human comcen itself while that comcen is
|
|
; visible. The target must sit on the AI comcen's 21x17 radar grid; the reload timer is then set to
|
|
; half the range and the $90 strike is queued. The tail remembers the human drone's heading for the
|
|
; jink test and ticks the reload timer down.
|
|
; In: $920D bit 6, $92AE human drones left, $92B1 AI missiles, $8970 reload countdown, $8966 action
|
|
; budget, $8972 current missile target, $F76C unit type/dead bits, $F9F5 human comcen
|
|
; energy/visibility, $920B drone heading, zp_27/zp_28 = AI comcen cell
|
|
; Out: $8972 ($FF when the target was dropped), $8970, $896F, and via queueMissileStrikeCommand the
|
|
; bytes $90 col,row in the fabricated packet
|
|
; Called from: JMP at $ED22 and BCC at $ED8B in runAiMissileTurn, plus the fall-through from $EDD2
|
|
; Missile conservation: while the human still owns drones the AI keeps two missiles in hand for each
|
|
; of them, so it will not empty the rack on ground targets and be defenceless against the next drone.
|
|
; ----------------------------------------------------------------------
|
|
fireMissileAtSpottedUnit:
|
|
bit droneControlFlags ; EDD5 $920D bit 6 = the human is flying a drone
|
|
bvs forgetMissileTarget ; EDD8 he is - the anti-drone code above owns this exchange, so no ground shot
|
|
lda dronesLeft ; EDDA drones the human still has
|
|
asl a ; EDDD two missiles reserved per drone
|
|
sta scratch18 ; EDDE zp_18 = the reserve
|
|
lda opponentMissilesLeft ; EDE0 missiles the AI owns
|
|
beq forgetMissileTarget ; EDE3 none at all
|
|
sec ; EDE5 no borrow wanted
|
|
sbc scratch18 ; EDE6 missiles minus the reserve
|
|
bcc forgetMissileTarget ; EDE8 fewer than the reserve - hold fire
|
|
beq forgetMissileTarget ; EDEA exactly the reserve - hold fire as well
|
|
lda D_8970 ; EDEC reload countdown
|
|
bpl missileTurnTail ; EDEF still reloading (>= 0): skip to the tail, which ticks it down
|
|
lda D_8966 ; EDF1 this exchange's action budget
|
|
bmi forgetMissileTarget ; EDF4 already spent on the drone launch - drop the target and leave
|
|
ldx D_8972 ; EDF6 $8972 = human unit last seen shooting at the AI comcen, $FF = none known
|
|
bmi targetEnemyComcenWithMissile; EDF9 none - try the human comcen instead
|
|
lda unitTypeTable,x ; EDFB type byte of that unit
|
|
bpl checkTargetOnRadar ; EDFE still alive (bit 7 clear) - shoot at it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; targetEnemyComcenWithMissile - Fall-through tail of fireMissileAtSpottedUnit: when the AI has no
|
|
; remembered attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49)
|
|
; instead, but only while that comcen is currently visible.
|
|
; In: $F9F5 = energy/visibility byte of unit 49 (bit 7 = visible right now)
|
|
; Out: X = $31 when the comcen may be shot at, otherwise branches to $EE19 which clears the target
|
|
; Called from: BMI at $EDF9 and fall-through from $EDFE. XREF lists 'call:E0A5', but that JSR only
|
|
; ever runs while the track 29 drone-flight overlay occupies $EE00-$EFFF, so it never enters this
|
|
; code
|
|
; ----------------------------------------------------------------------
|
|
targetEnemyComcenWithMissile:
|
|
lda side0ComcenEnergy ; EE00 energy/visibility byte of the human comcen
|
|
bpl forgetMissileTarget ; EE03 bit 7 clear = not visible; nothing to shoot at
|
|
ldx #$31 ; EE05 $31 = 49 = the human player's COMCEN
|
|
checkTargetOnRadar:
|
|
jsr isUnitOnAiRadar ; EE07 put the target cell in zp_22/zp_24 and test it against the AI comcen's radar grid
|
|
bcs forgetMissileTarget ; EE0A outside the 21x17 grid - the AI cannot see it, so no shot
|
|
jsr cellDistance ; EE0C range from the AI comcen (zp_27/zp_28) to the target
|
|
L_EE0F:
|
|
lsr a ; EE0F half of it ...
|
|
sta D_8970 ; EE10 ... becomes the reload countdown: long shots take longer to repeat
|
|
jsr queueMissileStrikeCommand; EE13 append the $90 col,row strike to the fabricated packet
|
|
jmp missileTurnTail ; EE16 keep $8972 so the same unit stays the target next exchange
|
|
|
|
forgetMissileTarget:
|
|
lda #$FF ; EE19 $FF = no target
|
|
sta D_8972 ; EE1B forget the remembered attacker
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Shared tail: remember the human drone's heading so the next anti-drone shot can tell whether it
|
|
; jinked, and tick the reload countdown down towards $FF = ready.
|
|
; ----------------------------------------------------------------------
|
|
missileTurnTail:
|
|
lda droneHeading ; EE1E current heading of the human's drone
|
|
sta D_896F ; EE21 $896F is compared against it at $ED9E next time
|
|
lda D_8970 ; EE24 reload countdown
|
|
bmi missileTurnDone ; EE27 already at $FF: the launcher is ready, leave it alone
|
|
dec D_8970 ; EE29 one exchange closer to being reloaded
|
|
missileTurnDone:
|
|
rts ; EE2C back to injectAiCommandsIntoPacket
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueMissileStrikeCommand - Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler
|
|
; cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to
|
|
; have received from the opponent. Both coordinates are pre-mirrored with mirrorMapCoordinate (39-v)
|
|
; because the command dispatcher un-mirrors every parameter that arrives from a remote player.
|
|
; In: zp_22 = strike column, zp_24 = strike row, $E01E = current packet length
|
|
; Out: three bytes appended at $E020+, $E01E advanced by 3, $8966 spent (decremented to $FF), $8968
|
|
; charged two of the round's command slots; X clobbered
|
|
; Called from: $EDD2 (anti-drone shot) and $EE13 (shot at a spotted unit)
|
|
; ----------------------------------------------------------------------
|
|
queueMissileStrikeCommand:
|
|
ldx rxPacketLength ; EE2D X = current length of the fabricated incoming packet
|
|
lda #$90 ; EE30 $90 = MISSILE STRIKE
|
|
sta rxPacketBuffer,x ; EE32 command byte into the received-packet buffer
|
|
inx ; EE35 next byte
|
|
lda cellColA ; EE36 strike column
|
|
jsr mirrorMapCoordinate ; EE38 $3FA3: 39 - column, undoing the mirror the engine will apply to a remote command
|
|
sta rxPacketBuffer,x ; EE3B first parameter
|
|
L_EE3E:
|
|
inx ; EE3E next byte
|
|
lda cellRowA ; EE3F strike row
|
|
L_EE41:
|
|
jsr mirrorMapCoordinate ; EE41 mirror it as well
|
|
sta rxPacketBuffer,x ; EE44 second parameter
|
|
inx ; EE47 past the last byte
|
|
stx rxPacketLength ; EE48 packet is three bytes longer
|
|
dec D_8966 ; EE4B this exchange's single action is now spent
|
|
inc D_8968 ; EE4E charge the first of the two command slots a missile costs
|
|
inc D_8968 ; EE51 and the second: a missile counts double against the four commands a round allows
|
|
L_EE54:
|
|
rts ; EE54 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; randomlySignValue - Returns A or -A with equal probability, using bit 0 of the lock-step game RNG.
|
|
; Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss
|
|
; the human's drone.
|
|
; In: A = magnitude
|
|
; Out: A = zp_18 = +A or -A (two's complement); the game RNG has been stepped
|
|
; Called from: $EDB5 (column offset) and $EDBF (row offset)
|
|
; ----------------------------------------------------------------------
|
|
randomlySignValue:
|
|
sta scratch18 ; EE55 keep the magnitude
|
|
jsr nextGameRandom ; EE57 one random byte from the lock-step RNG
|
|
and #$01 ; EE5A just its lowest bit
|
|
beq returnSignedValue ; EE5C 0 - leave the magnitude positive
|
|
lda scratch18 ; EE5E 1 - negate it: two's complement ...
|
|
eor #$FF ; EE60 ... invert ...
|
|
clc ; EE62 clear carry for the +1
|
|
adc #$01 ; EE63 ... and add one
|
|
sta scratch18 ; EE65 zp_18 = -magnitude
|
|
returnSignedValue:
|
|
lda scratch18 ; EE67 return the signed value
|
|
rts ; EE69 back to the missile scatter
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; isUnitOnAiRadar - Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17
|
|
; radar grid centred on the computer opponent's own COMCEN. Because mapCellToRadarGrid centres the
|
|
; grid on comcenUnit ($9236), this routine swaps that variable to $63 (unit 99, side 1's comcen) for
|
|
; the test and puts it back to $31 (unit 49) afterwards - the unconditional $31 is proof that this
|
|
; module always assumes the human is side 0.
|
|
; In: X = unit index, $F640/$F6A4 = unit column/row arrays
|
|
; Out: zp_22 = column, zp_24 = row of that unit (point A for the next cellDistance), C = 0 when the
|
|
; cell is on the AI's radar grid and C = 1 when it is off it; $9236 left at $31; X and Y clobbered
|
|
; Called from: $EE07 (missile targeting) and $EEFA (drone targeting in THE BOMB). Note it calls into
|
|
; the $6F00 comcen-screens overlay, so overlay B must be the resident one
|
|
; ----------------------------------------------------------------------
|
|
isUnitOnAiRadar:
|
|
lda unitColTable,x ; EE6A map column of unit X
|
|
sta cellColA ; EE6D zp_22 = point A column, also the strike coordinate if a missile follows
|
|
lda unitRowTable,x ; EE6F map row of unit X
|
|
L_EE72:
|
|
sta cellRowA ; EE72 zp_24 = point A row
|
|
tay ; EE74 Y = row, as mapCellToRadarGrid wants it
|
|
ldx cellColA ; EE75 X = column
|
|
L_EE77:
|
|
lda #$63 ; EE77 $63 = 99 = side 1's COMCEN
|
|
sta comcenUnit ; EE79 pretend the local comcen is the AI's, so the radar grid is centred on it
|
|
jsr mapCellToRadarGrid ; EE7C $7A13 in the comcen-screens overlay: C=0 when the cell is inside the 21 x 17 grid
|
|
lda #$31 ; EE7F $31 = 49 = the human player's COMCEN
|
|
sta comcenUnit ; EE81 restore comcenUnit - the rest of the game expects the local player's comcen there
|
|
rts ; EE84 carry from the grid test survives both stores
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; launchAiDrone - Sends the computer opponent's drone up when one is due. It writes the launch
|
|
; straight into the packet the game is about to 'receive': $97 DRONE LAUNCH followed by a $8D DRONE
|
|
; MOVE that places the drone on the AI comcen and points it at the chosen target unit. The AI keeps
|
|
; its own copy of the drone position in $8974/$8975 and a 100-step fuel counter in $8973; $8977 = 3
|
|
; holds the next launch off for three exchanges.
|
|
; In: $8977 launch delay (must be 0), $8976 target unit (>= 0), $920D bit 7 (no drone of the human's
|
|
; in the air), $F6A3/$F707 AI comcen cell, $F640/$F6A4 target cell
|
|
; Out: $E020 = $97, $E021-$E024 = $8D heading,col,row, $E01E = 5; $8974/$8975 launch cell, $8973 = $64
|
|
; fuel, $8977 = 3, $896F heading, $8966 spent; zp_92-zp_95 line-stepper state clobbered
|
|
; Called from: $ED0B, the first thing runAiMissileTurn does each exchange
|
|
; Three gates before a launch: the delay must have run out, a target must have been picked by
|
|
; chooseAiDroneTarget, and no drone may be in the air already - the engine tracks only one at a time.
|
|
; ----------------------------------------------------------------------
|
|
launchAiDrone:
|
|
lda D_8977 ; EE85 $8977 = exchanges to wait before the next launch ($FF also means the drone-flight overlay is resident)
|
|
bne noDroneLaunch ; EE88 not due yet
|
|
lda D_8976 ; EE8A $8976 = the unit the drone should be sent at
|
|
bmi noDroneLaunch ; EE8D bit 7 set = no target chosen
|
|
lda droneControlFlags ; EE8F $920D drone state flags
|
|
bmi noDroneLaunch ; EE92 bit 7 = a drone flight is already in progress - wait for it to end
|
|
dec D_8966 ; EE94 spend this exchange's single action
|
|
lda #$97 ; EE97 $97 = DRONE LAUNCH (handler cmdDroneLaunch $53A3: 'DRONE ALERT!' and one drone off the opponent's stock)
|
|
sta rxPacketBuffer ; EE99 first byte of the fabricated packet
|
|
lda #$8D ; EE9C $8D = DRONE MOVE, 4 bytes: heading, column, row
|
|
sta rxPacketArg1 ; EE9E second byte
|
|
L_EEA1:
|
|
ldx side1ComcenCol ; EEA1 the drone starts over the AI's own comcen
|
|
stx D_8974 ; EEA4 $8974 = the AI's private copy of the drone column
|
|
stx lineCurCol ; EEA7 zp_93 = line-stepper start column
|
|
ldy side1ComcenRow ; EEA9 comcen row
|
|
sty D_8975 ; EEAC $8975 = drone row
|
|
L_EEAF:
|
|
sty lineCurRow ; EEAF zp_92 = line-stepper start row
|
|
jsr mirrorBlockCornerForOpponent; EEB1 add one to each coordinate and mirror: exactly undoes the mirror-then-decrement the engine applies to a remote $8D (the drone is a 2x2 object)
|
|
stx rxPacketArg3 ; EEB4 column parameter of the $8D
|
|
sty rxPacketArg4 ; EEB7 row parameter
|
|
ldy D_8976 ; EEBA the chosen target unit
|
|
lda unitColTable,y ; EEBD its column ...
|
|
sta lineEndCol ; EEC0 ... becomes the line-stepper target column
|
|
lda unitRowTable,y ; EEC2 its row ...
|
|
sta lineEndRow ; EEC5 ... the target row
|
|
jsr getStepDirection ; EEC7 one Bresenham step towards the target: Y = compass direction 0-7, A = that direction's row delta
|
|
sta D_896F ; EECA remember the heading for the AI's own bookkeeping
|
|
sta rxPacketArg2 ; EECD heading parameter of the $8D - but this is A, the row delta (-1/0/+1), not the direction index in Y. cmdDroneMove draws sprite shape (heading+4) AND 7 = 3, 4 or 5, so the AI's drone is always drawn flying south towards the player. Probably a slip for TYA
|
|
L_EED0:
|
|
lda #$05 ; EED0 5 bytes: one $97 plus four for the $8D
|
|
sta rxPacketLength ; EED2 hand the packet to the engine as if the opponent had sent it
|
|
lda #$64 ; EED5 $64 = 100 steps of fuel, one per exchange, matching the human's drone tank
|
|
sta D_8973 ; EED7 $8973 = the AI's own fuel counter
|
|
lda #$03 ; EEDA wait three exchanges before considering another launch
|
|
sta D_8977 ; EEDC $8977 also stays non-negative, marking the native $EE00 tail as the resident one
|
|
noDroneLaunch:
|
|
rts ; EEDF nothing to launch
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; chooseAiDroneTarget - Picks what the computer opponent should send its next drone at and leaves the
|
|
; unit index in $8976. It bails out when the AI has no drones left or a target is still pending. In
|
|
; game type 2 (THE BOMB) the only worthwhile target is the human COMCEN, as soon as it is visible or
|
|
; shows on the AI's radar. Otherwise, once the comcen is visible, it adds up the strength of every
|
|
; group that has reached it (double for a group with a clear run at it) and, at a total of 8, commits
|
|
; to an all-out attack: every group is flagged committed, all 46 combat units are made due for new
|
|
; orders, the drone is held back 21 exchanges and then aimed at the comcen. If the comcen is not
|
|
; visible it falls into pickDroneTargetAlongThreatAxis.
|
|
; In: $92AF drones the AI has left, $8976, $0BA3 bits 0-2 = game type, $F9F5 human comcen visibility,
|
|
; $8902 group strengths, $893E group flags, $8953 highest group id, $8954 group count, $91CB game
|
|
; clock
|
|
; Out: $8976 = target unit ($31 = the human comcen), $893E bit 6 set for every group, $8800-$882D
|
|
; stamped with the clock, $8977 = $15; zp_18, zp_19 clobbered
|
|
; Called from: planTrainerRound ($E1FF), once per round and only while the AI is in attack mode ($8963
|
|
; = 0)
|
|
; ----------------------------------------------------------------------
|
|
chooseAiDroneTarget:
|
|
lda opponentDronesLeft ; EEE0 $92AF = drones the computer opponent still owns
|
|
beq droneTargetDone ; EEE3 none left - it will never launch again
|
|
lda D_8976 ; EEE5 $8976 = pending drone target
|
|
bpl droneTargetDone ; EEE8 bit 7 clear = a target is already chosen, so leave it alone
|
|
lda gameTypeOptions ; EEEA game options byte
|
|
L_EEED:
|
|
and #$07 ; EEED bits 0-2 = the game type
|
|
cmp #$02 ; EEEF type 2 = THE BOMB, where only the comcen counts
|
|
bne chooseTargetByGroupPressure; EEF1 any other game type
|
|
lda side0ComcenEnergy ; EEF3 energy/visibility of the human comcen
|
|
bmi targetHumanComcen ; EEF6 bit 7 = visible right now: send the drone at it
|
|
ldx #$31 ; EEF8 $31 = 49 = the human comcen
|
|
jsr isUnitOnAiRadar ; EEFA is it at least on the AI comcen's radar grid?
|
|
L_EEFD:
|
|
bcs droneTargetDone ; EEFD not on the radar either - no drone target this round
|
|
bcc targetHumanComcen ; EEFF on the radar - target it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Every other game type: measure how much of the AI's army has actually converged on the human comcen.
|
|
; $893E bit 5 = this group has a clear line to the comcen (set at $E3C3), bit 6 = one of its members
|
|
; has
|
|
; come within 8 cells of it (set at $EB1F).
|
|
; ----------------------------------------------------------------------
|
|
chooseTargetByGroupPressure:
|
|
lda side0ComcenEnergy ; EF01 comcen visibility again
|
|
bpl pickDroneTargetAlongThreatAxis; EF04 not visible - go and look for a worthwhile cluster of units instead
|
|
ldy D_8954 ; EF06 $8954 = number of AI groups ...
|
|
dey ; EF09 ... so the last group index is one less
|
|
lda #$00 ; EF0A zp_18 = 0
|
|
sta scratch18 ; EF0C running total of converging strength
|
|
sumConvergingGroupsLoop:
|
|
lda D_8902,y ; EF0E $8902 = number of units in group Y
|
|
sta scratch19 ; EF11 zp_19 = this group's weight
|
|
ldx D_893E,y ; EF13 group flags
|
|
txa ; EF16 test them ...
|
|
and #$20 ; EF17 ... bit 5 = the group has a clear run at the human comcen
|
|
beq testGroupContactFlag ; EF19 no clear run - try the contact bit
|
|
asl scratch19 ; EF1B a clear run counts double
|
|
jmp addGroupWeight ; EF1D add it in
|
|
|
|
testGroupContactFlag:
|
|
txa ; EF20 flags again
|
|
and #$40 ; EF21 bit 6 = a member of this group has reached the comcen
|
|
L_EF23:
|
|
beq nextGroupInSum ; EF23 neither flag - this group does not count
|
|
addGroupWeight:
|
|
lda scratch19 ; EF25 the group's weight
|
|
clc ; EF27 clear carry
|
|
L_EF28:
|
|
adc scratch18 ; EF28 add to the running total
|
|
sta scratch18 ; EF2A keep the total
|
|
nextGroupInSum:
|
|
dey ; EF2C next group down
|
|
bpl sumConvergingGroupsLoop ; EF2D until group 0 has been counted
|
|
lda scratch18 ; EF2F converging strength
|
|
cmp #$08 ; EF31 eight units' worth is the trigger for the all-out attack
|
|
bcc pickDroneTargetAlongThreatAxis; EF33 not enough yet - go back to hunting clusters
|
|
|
|
; ----------------------------------------------------------------------
|
|
; All-out attack: flag every group as committed, then stamp the order clock of all 46 mobile AI units
|
|
; with the current game clock. scanTrainerUnitsForOrders re-plans a unit whose stamp has caught up
|
|
; with the down-counting clock ($E2E3), so this makes the whole army replan on the next scan.
|
|
; ----------------------------------------------------------------------
|
|
commitAllGroups:
|
|
ldy D_8953 ; EF35 $8953 = highest group id in use
|
|
commitGroupLoop:
|
|
lda D_893E,y ; EF38 flags of group Y
|
|
ora #$40 ; EF3B bit 6 = committed to the attack
|
|
sta D_893E,y ; EF3D store them back
|
|
dey ; EF40 next group down
|
|
bpl commitGroupLoop ; EF41 until group 0 is done
|
|
ldy #$2D ; EF43 $2D = 45: units 50..95, the AI's 28 grunts, 12 riders and 6 boomers (its 3 spies and comcen are left alone)
|
|
lda gameClock ; EF45 $91CB = the down-counting game clock
|
|
stampOrderClockLoop:
|
|
sta msgGetOpponentOnPhoneAnd,y; EF48 $8800+ordinal = the round this unit may next be given orders
|
|
dey ; EF4B next AI unit
|
|
bpl stampOrderClockLoop ; EF4C all 46 of them
|
|
lda #$15 ; EF4E $15 = 21 exchanges ...
|
|
sta D_8977 ; EF50 ... before the drone follows the ground attack in
|
|
targetHumanComcen:
|
|
lda #$31 ; EF53 $31 = 49 = the human COMCEN
|
|
sta D_8976 ; EF55 that is the drone's target
|
|
droneTargetDone:
|
|
rts ; EF58 no target this round
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pickDroneTargetAlongThreatAxis - Tail of chooseAiDroneTarget for the case where the human comcen is
|
|
; no use as a drone target. It scans the threat around the AI's current objective (the escorted
|
|
; group's centre, or the AI comcen when no group is being escorted), and if the pressure reaches that
|
|
; group's own strength and something really has the point in weapon range, it walks a ray in the
|
|
; direction the enemy is coming from ((quietest+4) AND 7) at radii 4 to 14, scoring each 2x2 block of
|
|
; cells with scoreTargetBlock. The highest-numbered human unit in the best-scoring block becomes the
|
|
; drone target, and the acceptance threshold $8979 is cut to three quarters so the AI grows less
|
|
; choosy as the game goes on.
|
|
; In: $895D escorted group ($FF = none), $88EA/$88F6 group centre, $8902 group strength, $8979
|
|
; block-score threshold (set per opening play by the trainer setup overlay: $FF, $FF, 8, $0C,
|
|
; $0C), $F6A3/$F707 AI comcen cell
|
|
; Out: $8976 = drone target unit (untouched when nothing scored), $8979 scaled by 3/4, $8956 = the
|
|
; threshold used, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius,
|
|
; zp_18/zp_19/zp_22/zp_24/zp_27/zp_28 and zp_94/zp_95 clobbered
|
|
; Called from: BPL at $EF04 and BCC at $EF33 inside chooseAiDroneTarget - it is that routine's
|
|
; fall-through tail, not a separate entry
|
|
; Choose the point to scan around and the pressure it takes to make the AI bother: the group the
|
|
; comcen escorts and its member count, or the AI comcen itself with a fixed threshold of 3.
|
|
; ----------------------------------------------------------------------
|
|
pickDroneTargetAlongThreatAxis:
|
|
ldx side1ComcenCol ; EF59 default scan point: the AI's own comcen column
|
|
ldy side1ComcenRow ; EF5C and row
|
|
lda #$03 ; EF5F default threat threshold
|
|
sta D_8956 ; EF61 $8956 doubles as the trainer's 'how much pressure is worth reacting to' value
|
|
lda D_895D ; EF64 $895D = the group the AI comcen escorts
|
|
bmi storeScanReferencePoint ; EF67 bit 7 = no group is being escorted - keep the comcen defaults
|
|
tay ; EF69 Y = that group
|
|
lda D_8902,y ; EF6A $8902 = its member count
|
|
L_EF6D:
|
|
sta D_8956 ; EF6D a big group needs more pressure before its drone is called in
|
|
ldx D_88EA,y ; EF70 the group's centre column ...
|
|
lda D_88F6,y ; EF73 ... and centre row ...
|
|
tay ; EF76 ... become the scan point
|
|
storeScanReferencePoint:
|
|
stx cellColB ; EF77 zp_27 = reference column for the threat scan
|
|
sty cellRowB ; EF79 zp_28 = reference row (the scan routine is written for the comcen but works on any point)
|
|
jsr assessThreatAroundAiComcen; EF7B A = threat level, X = deepest weapon-range penetration, Y = quietest compass direction
|
|
cmp D_8956 ; EF7E compare the measured pressure with the threshold
|
|
bcc pickDroneTargetDone ; EF81 too quiet around the objective - no drone target
|
|
txa ; EF83 X = the range surplus
|
|
beq pickDroneTargetDone ; EF84 nothing actually has the objective in weapon range - not worth a drone either
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk a ray from the objective straight into the enemy - the direction opposite the quietest one -
|
|
; and score every 2x2 block of cells along it from 4 to 14 cells out. scoreTargetBlock keeps the best
|
|
; score in zp_1C and the strongest human unit of that block in zp_1D.
|
|
; ----------------------------------------------------------------------
|
|
tya ; EF86 A = the quietest direction
|
|
clc ; EF87 clear carry
|
|
adc #$04 ; EF88 +4 of the 8 compass points = the exact opposite ...
|
|
and #$07 ; EF8A ... modulo 8: the direction the enemy is pressing from
|
|
sta scratch1E ; EF8C zp_1E = the ray's direction
|
|
lda #$04 ; EF8E start 4 cells out from the objective
|
|
sta scratch1F ; EF90 zp_1F = the current radius
|
|
lda D_8979 ; EF92 $8979 = the score a block has to beat
|
|
sta scratch1C ; EF95 zp_1C = the score to beat. A value with bit 7 set (the $FF that plays 0 and 1 start with) makes scoreTargetBlock accept any block whose score is not negative
|
|
lda #$FF ; EF97 $FF = no unit picked yet
|
|
sta scratch1D ; EF99 zp_1D = the unit the best block yields, none yet
|
|
scanRayLoop:
|
|
ldx scratch1E ; EF9B direction of the ray
|
|
ldy scratch1F ; EF9D how far out
|
|
jsr offsetPointInDirection ; EF9F zp_18/zp_19 = the cell zp_1F cells along the ray, clamped to the map (offsetPointInDirection patches its own LDX operand at $E67C+1 with the direction index); C=1 means it fell off the 40x40 map
|
|
bcs pickBestScoredBlock ; EFA2 the ray ran off the 40x40 map - stop scanning
|
|
ldx scratch18 ; EFA4 X = candidate column
|
|
ldy scratch19 ; EFA6 Y = candidate row
|
|
jsr scoreTargetBlock ; EFA8 score the 2x2 block at that corner - grunt 1, rider 2, boomer 4, spy 1, comcen 4, negative for the AI's own units. scoreTargetBlock patches $E9E3+1/$E9E7+1 with X and Y so a winning block can drop its corner into zp_95/zp_94; nothing here uses that copy
|
|
inc scratch1F ; EFAB one cell further out
|
|
lda scratch1F ; EFAD current radius
|
|
cmp #$0F ; EFAF stop after 14 cells
|
|
bcc scanRayLoop ; EFB1 keep walking the ray
|
|
pickBestScoredBlock:
|
|
lda scratch1D ; EFB3 zp_1D = highest-numbered human unit standing in the best block. Since the roster runs 0-27 grunts, 28-39 riders, 40-45 boomers, 46-48 spies, 49 comcen, the highest index is also the most valuable unit there
|
|
bmi pickDroneTargetDone ; EFB5 still $FF - no block beat the threshold, so no drone target this round
|
|
sta D_8976 ; EFB7 it becomes the drone's target ($8976); launchAiDrone aims at it on a later exchange
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Having spent a target, drop the acceptance threshold to three quarters, so later drones settle for
|
|
; smaller clusters. Plays 0 and 1 start at $FF, which counts as 'accept anything'; after three
|
|
; targets the threshold lands on $6C = 108, which no 2x2 block (maximum 16) can ever reach again.
|
|
; ----------------------------------------------------------------------
|
|
lda D_8979 ; EFBA the current threshold
|
|
lsr a ; EFBD /2 ...
|
|
lsr a ; EFBE ... /4
|
|
sta scratch18 ; EFBF zp_18 = a quarter of it
|
|
lda D_8979 ; EFC1 the threshold again
|
|
sec ; EFC4 no borrow wanted
|
|
sbc scratch18 ; EFC5 threshold minus a quarter = 3/4 of it
|
|
bcc pickDroneTargetDone ; EFC7 never taken: A - A/4 cannot borrow, so this is a redundant guard
|
|
sta D_8979 ; EFC9 store the reduced threshold
|
|
pickDroneTargetDone:
|
|
rts ; EFCC back to the planner
|
|
|
|
|
|
; strayTextTail - The last three characters, 'CE' and '.' with bit 7 set, of the string 'INSERT DATA
|
|
; DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the modem-driver build of this overlay slot.
|
|
; Here the trainer's code runs up to $EFCC, so only this fragment survives.
|
|
; In: -
|
|
; Out: -
|
|
; Called from: nothing; unreferenced
|
|
; $EFCD-$EFFF is not code or data of the trainer: it is the tail of the disk sector, and every build
|
|
; of
|
|
; the $EC00/$EE00 overlay slot carries the same mastering-tool leftovers here. Nothing in the
|
|
; disassembly reads any of it.
|
|
strayTextTail:
|
|
.byte "CE",'.'|$80 ; EFCD 'CE' plus '.' with bit 7 set - the tail of a string that belongs to another build
|
|
|
|
; msgDatagamecustomOff2: text, 18 bytes. 'DATAGAMECUSTOM OFF' with bit 7 on the final F.
|
|
; Byte-identical to the same block in ovl_E000_T34, ovl_EC00_T29 and ovl_EE00_T29 - mastering-tool
|
|
; filler at the tail of every $EC00/$EE00 overlay build. No reference anywhere in the disassembly.
|
|
msgDatagamecustomOff2:
|
|
.byte "DATA" ; EFD0 'DATA' - start of the shared filler block found at the end of every $EC00/$EE00 overlay
|
|
.byte "GAME" ; EFD4 'GAME'
|
|
.byte "CUSTOM OF",'F'|$80 ; EFD8 'CUSTOM OFF', bit 7 set on the last F to terminate the string
|
|
|
|
; msgDestroyOff2: text, 11 bytes. 'DESTROY OFF' with bit 7 on the final F. Same shared filler block;
|
|
; unreferenced.
|
|
msgDestroyOff2:
|
|
.byte "DESTROY OF",'F'|$80 ; EFE2 'DESTROY OFF', bit 7 on the final F; same filler block, never referenced
|
|
|
|
; msgAccepted2: text, 8 bytes. 'ACCEPTED' with bit 7 on the final D. Same shared filler block;
|
|
; unreferenced.
|
|
msgAccepted2:
|
|
.byte "ACCEPTE",'D'|$80 ; EFED 'ACCEPTED', bit 7 on the final D; same filler block, never referenced
|
|
|
|
; Sector padding: $C0,$DF then $00,$FF to the end of the page. Identical in all four builds of this
|
|
; overlay slot apart from the very last byte, and read by nothing.
|
|
overlayTailFiller:
|
|
.byte $C0,$DF,$00,$FF,$00,$FF,$00,$FF; EFF5 ........ $C0,$DF then alternating $00,$FF padding to the end of the page
|
|
.byte $00,$FF,$FF ; EFFD ... last three padding bytes; $EFFF is $FF here and in the track 29 variants, $00 in the modem driver
|