924 lines
63 KiB
ArmAsm
924 lines
63 KiB
ArmAsm
; ============================================================================
|
|
; $EC00-$EFFF - solo trainer setup: picks the opening play and seeds the AI (track 29 sectors 0-3)
|
|
; ============================================================================
|
|
; Loaded by $7B24 from the map generator overlay, over the tail of the trainer module.
|
|
|
|
.setcpu "6502"
|
|
.include "c64.inc"
|
|
.include "zeropage.inc"
|
|
|
|
; ---- references to code/data outside this file ----
|
|
play4Grp12 := $0464
|
|
play4Grp32 := $0478
|
|
play4Col02 := $048C
|
|
play4Col22 := $04A0
|
|
loadSectors := $0804
|
|
setLoadDest := $085C
|
|
gameTypeOptions := $0BA3
|
|
diagonalDirectionTable := $1FB2
|
|
directionDeltaTable := $20F0
|
|
headingToDirectionTable := $7316
|
|
msgGetOpponentOnPhoneAnd := $8800
|
|
D_8832 := $8832
|
|
D_8863 := $8863
|
|
D_8864 := $8864
|
|
D_8896 := $8896
|
|
msgPickUpPhoneThenPressSpace := $8897
|
|
D_8898 := $8898
|
|
D_88D2 := $88D2
|
|
D_88DE := $88DE
|
|
D_88EA := $88EA
|
|
D_88F6 := $88F6
|
|
D_8902 := $8902
|
|
D_893E := $893E
|
|
D_8949 := $8949
|
|
D_8952 := $8952
|
|
D_8953 := $8953
|
|
D_8954 := $8954
|
|
D_8955 := $8955
|
|
D_895D := $895D
|
|
D_8960 := $8960
|
|
D_8964 := $8964
|
|
D_8966 := $8966
|
|
D_8967 := $8967
|
|
D_8968 := $8968
|
|
D_896B := $896B
|
|
D_896C := $896C
|
|
D_896E := $896E
|
|
D_896F := $896F
|
|
D_8970 := $8970
|
|
D_8972 := $8972
|
|
D_8974 := $8974
|
|
D_8975 := $8975
|
|
D_8976 := $8976
|
|
D_8977 := $8977
|
|
D_8978 := $8978
|
|
D_8979 := $8979
|
|
D_897A := $897A
|
|
D_897B := $897B
|
|
D_897C := $897C
|
|
gameClock := $91CB
|
|
terrainPointsBySide := $92AC
|
|
rxPacketLength := $E01E
|
|
rxPacketBuffer := $E020
|
|
rxPacketArg1 := $E021
|
|
rxPacketArg2 := $E022
|
|
rxPacketArg3 := $E023
|
|
serviceCommTick := $E12F
|
|
computeTrainerGroupStats := $E531
|
|
rearmCarrierTimer := $E5FF
|
|
cellDistance := $E82A
|
|
mirrorBlockCornerForOpponent := $EA38
|
|
queueAiCommand := $EA46
|
|
side1UnitColTable := $F672
|
|
side1UnitRowTable := $F6D6
|
|
side1ComcenFlags := $F76B
|
|
side1UnitTypeTable := $F79E
|
|
side1UnitDestColTable := $F802
|
|
side1ComcenDestCol := $F833
|
|
side1UnitDestRowTable := $F866
|
|
side1ComcenDestRow := $F897
|
|
side1ComcenDisplayFlags := $FBB7
|
|
multiply8x8 := $FCCD
|
|
divideSigned16by8 := $FCEC
|
|
nextGameRandom := $FD5D
|
|
|
|
; Contents
|
|
; --------
|
|
; $EC0F initTrainerAi Builds the whole opening plan of the solo trainer (the computer opponent, side
|
|
; 1 =
|
|
; $ED8E rebuildAiGroupTables Recomputes all of the trainer's per-group and per-unit bookkeeping from the
|
|
; $EEC3 buildAiHeadingSearchOrder Fills the 8-byte table at $894A with the order in which the resident AI
|
|
; $EEED orderAiGroupToPoint Orders AI group Y to move so that its formation centre ends up on column X,
|
|
; $EF00 shiftAiGroupDestinations Moves a whole AI group without breaking its formation.
|
|
; $EF27 staleDroneSteerTail DEAD CODE in this overlay.
|
|
; $EFAB staleDroneTargetPickerTail 34 stale bytes, unreachable and unusable here.
|
|
|
|
.org $EC00
|
|
|
|
|
|
; Three parallel 5-byte tables indexed by the AI plan number 0-4 that initTrainerAi derives from the
|
|
; game type and keeps in $8966. They are read together at $ECB9-$ECD1.
|
|
mainGroupByPlanTable:
|
|
.byte $FF,$FF,$04,$04,$04 ; EC00 ..... plan 0-4 -> spearhead group id: none for the two 11-group plans, group 4 for the 5-group plans
|
|
|
|
; groupCountByPlanTable: byteTable, 5 bytes. Per AI plan 0-4: highest group index ($0A, $0A, 4, 4, 4)
|
|
; stored in $8953; $8954 gets this value + 1.
|
|
groupCountByPlanTable:
|
|
.byte $0A,$0A,$04,$04,$04 ; EC05 ..... plan 0-4 -> highest group index: 10 (11 groups) for plans 0/1, 4 (5 groups) for plans 2-4
|
|
|
|
; aiPlanParamTable: byteTable, 5 bytes. Per AI plan 0-4: drone score threshold ($FF, $FF, 8, $0C, $0C)
|
|
; copied to both $8978 and $8979.
|
|
aiPlanParamTable:
|
|
.byte $FF,$FF,$08,$0C,$0C ; EC0A ..... plan 0-4 -> drone detonation score threshold: $FF (unreachable score = never detonate) for plans 0/1, 8 or 12 for plans 2-4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initTrainerAi - Builds the whole opening plan of the solo trainer (the computer opponent, side 1 =
|
|
; units 50-99). Clears the AI's scalar work area in the $8800 block, picks a first objective near the
|
|
; human comcen, turns the game type ($0BA3 bits 0-2) into a plan number 0-4, loads the five 150-byte
|
|
; opening plays from track 29 sectors 6-8 into $0200-$04FF, copies the chosen play's 50 group ids and
|
|
; 50 destination cells into $8832 and unitDestCol/unitDestRow of units 50-99, re-aims every group at a
|
|
; random point for the bigger plans, rebuilds the per-group tables and works out the round at which
|
|
; the AI changes gear ($896E).
|
|
; In: $0BA3 game type bits 0-2; $91CB game clock (the game length in rounds, put there by
|
|
; setGameClockForType at $7B1A); $92AC TERRAIN PTS setting of side 1; unit arrays unitFlagsTable
|
|
; $F708, unitTypeTable $F76C, unitDestColTable $F7D0, unitDestRowTable $F834; the three plan
|
|
; tables at $EC00/$EC05/$EC0A; the disk, through setLoadDest/loadSectors; nextGameRandom
|
|
; Out: $0200-$04FF overwritten with the playbook; the AI work area $8800-$8979 initialised; group ids
|
|
; $8832-$8863 and unitDestCol/unitDestRow of units 50-99 set; unitFlagsTable[99]=$21 and
|
|
; unitDisplayFlags[99]=$20 for plans 4 (FULL WAR / DEFENDER); unitDestCol[99] bits 6-7 cleared
|
|
; for
|
|
; THE BOMB; $E01E=0; one $A5,00,$14 setup command queued for DEFENDER; $91CB left at $FF;
|
|
; zp_18-zp_2B and zp_48-zp_4D clobbered
|
|
; Called from: $7B30 in game/mapGenerator6F00 - the only call, made once, right after $7B24 loads this
|
|
; overlay (track 29 sectors 0-3) and immediately before $7B33 puts the modem-driver tail (track
|
|
; 34
|
|
; sectors 12-15) back over it. Everything in this file therefore runs exactly once, in a solo
|
|
; game
|
|
; Clear the trainer's scalar variables. The whole $8800-$89FF page pair still holds the modem status
|
|
; strings loaded from track 29 s9-10, which are dead in a solo game, so every AI variable has to be
|
|
; written before it can be read. X = 0 for the flags and counters, then X = $FF for the index
|
|
; variables whose 'none' value is $FF.
|
|
; ----------------------------------------------------------------------
|
|
initTrainerAi:
|
|
ldx #$00 ; EC0F X = 0: the value for every counter and flag cleared below
|
|
stx rxPacketLength ; EC11 no fake incoming packet is pending yet (the comm module's rx packet length) (also trainerAiE000:rxPacketLength)
|
|
L_EC14:
|
|
stx D_896C ; EC14 'human comcen has been spotted' flag = 0; randomiseCoordinate ($E14A) widens its aim while this is clear
|
|
stx D_8952 ; EC17 AI command-queue length = 0: the queue at $88A0 is empty
|
|
stx D_8960 ; EC1A safest compass direction from the last threat sweep round the AI comcen = 0
|
|
stx D_896B ; EC1D rounds left before the AI re-picks its rally point = 0, so it re-picks on the first round
|
|
stx D_8970 ; EC20 missile-pass scratch of the resident $EC00 tail = 0
|
|
stx D_8968 ; EC23 commands queued by the AI this round = 0
|
|
stx D_8977 ; EC26 drone state = 0: no drone airborne and the $EE00 drone-flying overlay is not resident ($FF marks it resident)
|
|
stx D_897B ; EC29 previous-round 'AI comcen destroyed' latch = 0
|
|
stx D_897C ; EC2C 'AI comcen destroyed' flag tested by planTrainerRound = 0
|
|
dex ; EC2F X = $FF: the 'none' value for the index variables that follow
|
|
stx D_895D ; EC30 no spearhead group yet for the AI comcen to escort
|
|
stx D_8976 ; EC33 no drone target unit picked
|
|
stx D_8972 ; EC36 no player unit is being hunted for having shot at the AI comcen
|
|
stx D_8964 ; EC39 no home-defence group yet; a real one is chosen at $EC9C unless the plan is QB SNEAK
|
|
L_EC3C:
|
|
stx D_8978 ; EC3C drone detonation score threshold = $FF (never); replaced from aiPlanParamTable at $ECCC
|
|
stx D_8979 ; EC3F and its decaying working copy = $FF
|
|
lda #$05 ; EC42 comcen threat threshold
|
|
sta D_897A ; EC44 the resident AI only reacts to pressure on its own comcen once the measured threat reaches 5 ($E29A)
|
|
jsr serviceCommTick ; EC47 really trainerAiE000:pickTrainerRallyPoint ($E12F) - the modem driver's name for this address is a collision; it stores a random cell near the human comcen in $8969/$896A as the AI's first objective (also trainerAiE000:pickTrainerRallyPoint)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Turn the game type into an AI plan number 0-4 (kept in $8966):
|
|
; 0 SCRIMAGE -> plan 0, 1 QB SNEAK -> plan 1 (no home-defence group),
|
|
; 2 THE BOMB -> plan 0 with no playbook at all (only the two comcens exist),
|
|
; 3 FACE-OFF -> plan 2, 4 SLUGGERS -> plan 3, 5 FULL WAR -> plan 4,
|
|
; 6 DEFENDER -> plan 4 plus a queued point award for the defending computer.
|
|
; ----------------------------------------------------------------------
|
|
lda gameTypeOptions ; EC4A the game options byte written by the setup screens
|
|
and #$07 ; EC4D bits 0-2 = game type 0 SCRIMAGE, 1 QB SNEAK, 2 THE BOMB, 3 FACE-OFF, 4 SLUGGERS, 5 FULL WAR, 6 DEFENDER
|
|
tay ; EC4F Y = game type
|
|
cpy #$02 ; EC50 is the game type below 2?
|
|
bcc L_EC93 ; EC52 SCRIMAGE and QB SNEAK use the plan number that equals the game type
|
|
checkForTheBombGameType:
|
|
bne L_EC6C ; EC54 branch if the game type is 3 or higher; fall through only for game type 2 = THE BOMB
|
|
|
|
; ----------------------------------------------------------------------
|
|
; THE BOMB: both sides own nothing but their comcen, so there is no formation to deploy. Free the AI
|
|
; comcen to move and pretend it is the whole of group 0.
|
|
; ----------------------------------------------------------------------
|
|
lda side1ComcenDestCol ; EC56 ordered destination column of the AI comcen, unitDestCol[99]
|
|
and #$3F ; EC59 keep the 0-39 column only, clearing the idle bit 7 so the engine treats the comcen as still moving
|
|
sta side1ComcenDestCol ; EC5B store it back
|
|
L_EC5E:
|
|
ldy #$00 ; EC5E group 0
|
|
sty D_8863 ; EC60 group id of AI slot 49 = unit 99, the AI comcen: the only live AI unit in THE BOMB
|
|
sty D_8953 ; EC63 highest group index in use = 0
|
|
sty D_8966 ; EC66 AI plan number = 0
|
|
jmp L_ED6C ; EC69 skip the playbook load and the group scatter entirely and go straight to the table rebuild
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Game types 3-6. Y is biased down by one so that FACE-OFF and SLUGGERS map straight onto plans 2 and
|
|
; 3, while FULL WAR and DEFENDER both end up on plan 4.
|
|
; ----------------------------------------------------------------------
|
|
L_EC6C:
|
|
dey ; EC6C Y = gameType - 1: FACE-OFF -> 2, SLUGGERS -> 3, FULL WAR -> 4, DEFENDER -> 5
|
|
cpy #$04 ; EC6D is the biased type below 4?
|
|
L_EC6F:
|
|
bcc L_EC93 ; EC6F FACE-OFF -> plan 2, SLUGGERS -> plan 3
|
|
beq L_EC88 ; EC71 FULL WAR (biased type 4) -> plan 4, without the extra point award
|
|
|
|
; ----------------------------------------------------------------------
|
|
; DEFENDER only: stage command $A5 (cmdSetupOption, $55A5) with kind 0 and value 20 and queue it as if
|
|
; the remote player had sent it, so the handler adds 20 x 10 = 200 purchase points to the AI's side
|
|
; score - the handicap that pays for the defending computer's extra force.
|
|
; ----------------------------------------------------------------------
|
|
lda #$A5 ; EC73 command byte $A5 = cmdSetupOption
|
|
sta D_8896 ; EC75 byte 0 of the AI's 3-byte command scratch
|
|
lda #$00 ; EC78 argument 1 = kind 0: 'add points to the sender's side'
|
|
sta msgPickUpPhoneThenPressSpace; EC7A byte 1 of the command scratch; the msg... label here is a $8800 message-string address collision
|
|
lda #$14 ; EC7D argument 2 = 20 points
|
|
sta D_8898 ; EC7F byte 2 of the command scratch
|
|
L_EC82:
|
|
jsr queueAiCommand ; EC82 append the staged 3-byte command to the AI's outgoing queue at $88A0
|
|
dec D_8968 ; EC85 undo the 'commands queued this round' bump queueAiCommand made: this is a setup option, not a battle order
|
|
|
|
; ----------------------------------------------------------------------
|
|
; FULL WAR and DEFENDER: reset the AI comcen's own flags. unitDisplayFlags mirrors bits 2-6 of
|
|
; unitFlagsTable, so $21/$20 means facing 1, side bit 5 set (side 1), no group membership, and the
|
|
; idle bit 7 clear - i.e. the comcen counts as still moving (probably so it relocates during these two
|
|
; long games).
|
|
; ----------------------------------------------------------------------
|
|
L_EC88:
|
|
ldy #$21 ; EC88 $21 = facing 1 (bits 0-1), group id 0 (bits 2-4), side 1 (bit 5), not in a group (bit 6 clear)
|
|
sty side1ComcenFlags ; EC8A unitFlagsTable[99] - the AI comcen
|
|
dey ; EC8D $20 = the same value with the facing bits stripped, which is what the engine mirrors into the display flags
|
|
sty side1ComcenDisplayFlags ; EC8E unitDisplayFlags[99]: bit 7 (arrived/idle) clear, bit 6 (group order active) clear
|
|
usePlanFour:
|
|
ldy #$04 ; EC91 FULL WAR and DEFENDER both use plan 4
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Record the plan and choose the home-defence group: a random group 1, 2 or 3 that will be parked on
|
|
; the AI comcen instead of joining the attack. QB SNEAK deliberately keeps none back.
|
|
; ----------------------------------------------------------------------
|
|
L_EC93:
|
|
sty D_8966 ; EC93 remember the plan number 0-4 for the rest of the setup and for the resident AI
|
|
lda #$FF ; EC96 $FF = 'no home-defence group'
|
|
cpy #$01 ; EC98 is this plan 1 (QB SNEAK)?
|
|
L_EC9A:
|
|
beq L_ECA3 ; EC9A QB SNEAK throws everything forward: leave the guard group as $FF
|
|
L_EC9C:
|
|
jsr nextGameRandom ; EC9C roll for a guard group
|
|
and #$03 ; EC9F keep the low two bits: 0-3
|
|
beq L_EC9C ; ECA1 reject 0 - group 0 is the one that follows the spearhead, so it is never the guard; re-roll
|
|
L_ECA3:
|
|
sta D_8964 ; ECA3 home-defence group id 1-3
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Load the AI playbook: track 29 sectors 6-8 -> $0200-$04FF, over the terrain tile rules (which the
|
|
; map generator has already finished with) and the copy-protection answer table. The 768 bytes hold
|
|
; five 150-byte records, one per plan: 50 group ids, then 50 destination columns, then 50 destination
|
|
; rows, all indexed by AI slot 0-49 = unit 50-99. The caller has already bumped diskLoadDepth
|
|
; ($7B17).
|
|
; ----------------------------------------------------------------------
|
|
L_ECA6:
|
|
ldx #$00 ; ECA6 destination low byte = $00
|
|
ldy #$02 ; ECA8 destination high byte = $02, i.e. load to $0200
|
|
jsr setLoadDest ; ECAA tell the fast loader where the next sectors go
|
|
ldy #$1D ; ECAD track 29
|
|
ldx #$06 ; ECAF first sector 6
|
|
lda #$03 ; ECB1 three sectors = 768 bytes = the five opening plays
|
|
jsr loadSectors ; ECB3 pull them in over $0200-$04FF
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the plan's fixed parameters out of the three tables at the top of this overlay.
|
|
; ----------------------------------------------------------------------
|
|
ldy D_8966 ; ECB6 Y = plan number, the index into all three 5-byte tables (and still in Y when multiply8x8 is called at $ECD4)
|
|
lda mainGroupByPlanTable,y ; ECB9 spearhead group for this plan
|
|
sta D_895D ; ECBC the group the AI comcen escorts and that group 0 is told to follow; $FF for plans 0/1
|
|
ldx groupCountByPlanTable,y ; ECBF highest group index used by this plan
|
|
stx D_8953 ; ECC2 store it
|
|
inx ; ECC5 +1
|
|
stx D_8954 ; ECC6 number of groups (11 or 5), the loop limit the resident AI uses at $E6A5/$E960/$EB3C
|
|
lda aiPlanParamTable,y ; ECC9 drone detonation score threshold for this plan
|
|
sta D_8978 ; ECCC the fixed threshold
|
|
sta D_8979 ; ECCF and the working copy that the drone code decays to 3/4 as it gets desperate
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Point three zero-page pointers at the three 50-byte arrays of the selected 150-byte plan record:
|
|
; mapCellPtr ($48) -> group ids, fillEndPtr ($4A) -> destination columns, planRowPtr ($4C) ->
|
|
; rows.
|
|
; Base = $0200 + 150 * planNumber.
|
|
; ----------------------------------------------------------------------
|
|
lda #$96 ; ECD2 150 = the size of one plan record
|
|
buildPlanRecordPointers:
|
|
jsr multiply8x8 ; ECD4 A x Y = 150 x plan number; returns the product low byte in A and the high byte in Y
|
|
sta mapCellPtr ; ECD7 low byte of the record's offset into the playbook
|
|
iny ; ECD9 add $100 ...
|
|
iny ; ECDA ... and another $100, because the playbook was loaded at $0200
|
|
sty mapCellPtrHi ; ECDB mapCellPtr now points at the record's 50 group ids
|
|
clc ; ECDD clear carry for the add below
|
|
L_ECDE:
|
|
adc #$32 ; ECDE +50 bytes: past the group ids
|
|
bcc L_ECE3 ; ECE0 no page crossing
|
|
iny ; ECE2 carry into the high byte
|
|
L_ECE3:
|
|
sta fillEndPtr ; ECE3 fillEndPtr points at the record's 50 destination columns
|
|
sty fillEndPtrHi ; ECE5 high byte of that pointer
|
|
L_ECE7:
|
|
clc ; ECE7 clear carry again
|
|
adc #$32 ; ECE8 +50 more bytes: past the destination columns
|
|
bcc L_ECED ; ECEA no page crossing
|
|
iny ; ECEC carry into the high byte
|
|
L_ECED:
|
|
sta planRowPtr ; ECED planRowPtr points at the record's 50 destination rows
|
|
sty planRowPtrHi ; ECEF high byte of that pointer
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy the plan into the live arrays, AI slot 49 down to 0 (units 99 down to 50).
|
|
; ----------------------------------------------------------------------
|
|
ldy #$31 ; ECF1 49 = the last AI slot; the loop counts down to 0
|
|
copyPlanRecordLoop:
|
|
lda (mapCellPtr),y ; ECF3 group id of this unit in the chosen opening play
|
|
sta D_8832,y ; ECF5 $8832 + slot = the AI's own group-id array (the resident AI addresses it as $8800 + absolute unit index)
|
|
lda (fillEndPtr),y ; ECF8 destination column from the play
|
|
sta side1UnitDestColTable,y ; ECFA unitDestCol[50 + slot]
|
|
L_ECFD:
|
|
lda (planRowPtr),y ; ECFD destination row from the play
|
|
sta side1UnitDestRowTable,y ; ECFF unitDestRow[50 + slot]
|
|
dey ; ED02 previous slot
|
|
bpl copyPlanRecordLoop ; ED03 loop until all 50 AI units have their group and destination
|
|
lda D_8966 ; ED05 which plan is this?
|
|
cmp #$02 ; ED08 plans 2, 3 and 4 re-aim every group; plans 0 and 1 keep the playbook's own destinations
|
|
L_ED0A:
|
|
bcc L_ED6C ; ED0A SCRIMAGE / QB SNEAK: nothing more to place, go and rebuild the tables
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Plans 2-4 only: scatter the groups. Every group from the highest down to 1 is given a random target
|
|
; - any column 4-35, and a row in an 8-row band 7 rows above the group's own average ordered row - and
|
|
; the whole formation is translated there. Group 0 is handled separately afterwards: it is sent to
|
|
; the same point as the spearhead group.
|
|
; ----------------------------------------------------------------------
|
|
lda D_8953 ; ED0C highest group index of this plan
|
|
sta D_8967 ; ED0F group loop counter, counting down to 0
|
|
L_ED12:
|
|
ldy D_8967 ; ED12 Y = the group being placed
|
|
jsr computeTrainerGroupStats; ED15 measure group Y ($E531): member count, combat value, bounding box, centre and average ordered cell; C=1 if the group is empty (then zp_1F below is stale, but the group has no members to move either)
|
|
L_ED18:
|
|
jsr nextGameRandom ; ED18 roll a column
|
|
and #$1F ; ED1B 0-31 ...
|
|
clc ; ED1D clear carry for the add below
|
|
adc #$04 ; ED1E ... + 4 = column 4-35, keeping the formation clear of both map edges
|
|
sta cellColA ; ED20 zp_22 = wanted formation centre column
|
|
L_ED22:
|
|
sta D_88D2,y ; ED22 and remember it as group Y's target column
|
|
L_ED25:
|
|
lda scratch1F ; ED25 zp_1F = the group's average ordered row, from computeTrainerGroupStats
|
|
cmp #$10 ; ED27 is that row above 16?
|
|
bcs L_ED2D ; ED29 no clamp needed
|
|
lda #$0F ; ED2B floor it at 15 so the band computed below cannot go negative
|
|
L_ED2D:
|
|
sec ; ED2D prepare to subtract
|
|
sbc #$07 ; ED2E start the 8-row band 7 rows before the group's own row
|
|
L_ED30:
|
|
sta scratch18 ; ED30 base row of the band
|
|
rollGroupTargetRowJitter:
|
|
jsr nextGameRandom ; ED32 roll a row offset
|
|
D_ED35:
|
|
and #$07 ; ED35 0-7 rows of jitter inside the band
|
|
clc ; ED37 clear carry for the add
|
|
adc scratch18 ; ED38 base + jitter
|
|
sta cellRowA ; ED3A zp_24 = wanted formation centre row
|
|
sta D_88DE,y ; ED3C and remember it as group Y's target row
|
|
jsr shiftAiGroupDestinations; ED3F clip the point to the map and shift every member of the group by the same delta
|
|
L_ED42:
|
|
dec D_8967 ; ED42 next group down
|
|
bmi L_ED59 ; ED45 counter went negative: every group has been placed
|
|
bne L_ED12 ; ED47 still above group 0: keep scattering
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The counter has reached group 0. Group 0 is the follow-up wave: instead of a random point it is
|
|
; ordered to the spearhead group's target, so the two arrive together.
|
|
; ----------------------------------------------------------------------
|
|
ldy D_895D ; ED49 spearhead group id for this plan
|
|
bmi L_ED12 ; ED4C $FF = no spearhead (only possible for plans 0/1, which never get here): fall back to a random point
|
|
ldx D_88D2,y ; ED4E the spearhead group's target column
|
|
lda D_88DE,y ; ED51 and its target row
|
|
L_ED54:
|
|
ldy #$00 ; ED54 group 0
|
|
jsr orderAiGroupToPoint ; ED56 order group 0 to the same cell
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Finally station the home-defence group on the AI comcen's own destination.
|
|
; ----------------------------------------------------------------------
|
|
L_ED59:
|
|
ldy D_8964 ; ED59 home-defence group id
|
|
bmi L_ED6C ; ED5C $FF: QB SNEAK and THE BOMB keep nobody back
|
|
lda side1ComcenDestCol ; ED5E AI comcen's ordered column, unitDestCol[99]
|
|
L_ED61:
|
|
and #$3F ; ED61 strip the idle bit 7 to leave a plain 0-39 column
|
|
L_ED63:
|
|
tax ; ED63 X = target column, as orderAiGroupToPoint expects
|
|
lda side1ComcenDestRow ; ED64 AI comcen's ordered row, unitDestRow[99]
|
|
and #$3F ; ED67 strip the idle bit
|
|
jsr orderAiGroupToPoint ; ED69 order the guard group (still in Y) onto the comcen
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Common tail for every plan: rebuild the per-group bookkeeping, then decide when the AI changes gear.
|
|
; gameClock counts DOWN from the game length, so subtracting 32-95 gives the clock value the game will
|
|
; reach after that many rounds; the resident AI compares $91CB against $896E at $E3E5 and
|
|
; $E765. For plans 0/1 the value is the full game length, which makes those comparisons true for the
|
|
; whole game.
|
|
; ----------------------------------------------------------------------
|
|
L_ED6C:
|
|
jsr rebuildAiGroupTables ; ED6C build the group averages, the per-unit activation clocks and the heading search order
|
|
L_ED6F:
|
|
jsr nextGameRandom ; ED6F roll the delay before the AI changes gear
|
|
and #$3F ; ED72 0-63 ...
|
|
ora #$20 ; ED74 ... with bit 5 forced on: 32-95 rounds
|
|
sta scratch18 ; ED76 keep it
|
|
L_ED78:
|
|
lda gameClock ; ED78 the game length in rounds (setGameClockForType at $7B1A left it here for us: 63/63/31/127/127/254/254 by game type)
|
|
ldy D_8966 ; ED7B which plan?
|
|
cpy #$02 ; ED7E plans 0 and 1 ...
|
|
bcc L_ED85 ; ED80 ... use the whole game length unchanged
|
|
sec ; ED82 prepare to subtract
|
|
sbc scratch18 ; ED83 plans 2-4: the switch happens once 32-95 rounds have been played
|
|
L_ED85:
|
|
sta D_896E ; ED85 the clock value at which the AI's second phase begins
|
|
lda #$FF ; ED88 $FF = 'clock not running'
|
|
sta gameClock ; ED8A put the game clock back the way the setup screen left it at $7AF2; the real countdown is loaded again at $7B86 once play starts
|
|
L_ED8D:
|
|
rts ; ED8D back to $7B33, which immediately reloads the modem-driver tail over this overlay
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rebuildAiGroupTables - Recomputes all of the trainer's per-group and per-unit bookkeeping from the
|
|
; current unit arrays. Clears the twelve group slots and gives each a random 0-15 round start delay,
|
|
; then walks AI slots 49-0 accumulating each live unit's ordered destination and current cell into its
|
|
; group's 16-bit sums, stamping the unit's activation clock, flagging units that already stand on
|
|
; their destination, and dropping dead units out of their group. Finally it divides the four sums by
|
|
; the member count so the group arrays hold averages: $88D2/$88DE = average ordered destination,
|
|
; $88EA/$88F6 = centre of the group as it stands now.
|
|
; In: $8832 group id per AI slot, $8953 highest group, $8964 home-defence group, $8966 plan number,
|
|
; $91CB game length, $92AC TERRAIN PTS of side 1, unitColTable+50 $F672, unitRowTable+50 $F6D6,
|
|
; unitTypeTable+50 $F79E, unitDestColTable+50 $F802, unitDestRowTable+50 $F866, nextGameRandom
|
|
; Out: group arrays $88D2/$88DE/$88EA/$88F6 (averages), $8902 member count, $893E start delay and the
|
|
; matching sum high bytes at $0464/$0478/$048C/$04A0, 12 entries each; $8800-$8831 activation
|
|
; clock per AI slot; $8864-$8895 zone flag per AI slot; $8832 entries of dead units set to $FF;
|
|
; bit 7 of unitDestCol set for units already at their destination; zp_18/1A/1B/1C/22/24/27/28
|
|
; used
|
|
; Called from: $ED6C in initTrainerAi (this file) - the only caller in this overlay
|
|
; Clear the twelve group slots. The 16-bit sums are split: the low bytes live in the $8800 block next
|
|
; to the other group arrays, the high bytes in the $0400 page (which held the copy-protection answer
|
|
; table and the scan lists and is free once a game is running). The D_0464/D_0478/D_048C/
|
|
; writeSectorToDisk labels are address collisions from the $0400 unit and mean nothing here.
|
|
; ----------------------------------------------------------------------
|
|
rebuildAiGroupTables:
|
|
jsr buildAiHeadingSearchOrder; ED8E refresh the random left/right heading search order at $894A
|
|
ldy #$0B ; ED91 group slots 11 down to 0 (only 0..$8953 are really used, but all twelve are cleared)
|
|
L_ED93:
|
|
lda #$00 ; ED93 the value written into all nine group arrays below
|
|
sta play4Grp12,y ; ED95 high byte of the group's ordered-column sum
|
|
sta play4Grp32,y ; ED98 high byte of the ordered-row sum
|
|
sta play4Col02,y ; ED9B high byte of the current-column sum
|
|
sta play4Col22,y ; ED9E $04A0: high byte of the current-row sum (the writeSectorToDisk label is a $0400-page collision)
|
|
sta D_8902,y ; EDA1 member count
|
|
sta D_88D2,y ; EDA4 low byte of the ordered-column sum, which becomes the average
|
|
sta D_88DE,y ; EDA7 low byte of the ordered-row sum
|
|
sta D_88EA,y ; EDAA low byte of the current-column sum
|
|
sta D_88F6,y ; EDAD low byte of the current-row sum
|
|
jsr nextGameRandom ; EDB0 roll this group's start delay
|
|
and #$0F ; EDB3 0-15 rounds
|
|
sta D_893E,y ; EDB5 the group waits that many rounds before it starts obeying its orders
|
|
dey ; EDB8 next group down
|
|
bpl L_ED93 ; EDB9 loop over all twelve group slots
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Walk the 50 AI slots (unit 50+slot), 49 down to 0. Each live unit contributes to its group's four
|
|
; running sums and gets its own activation clock and idle flag; each dead one is unhooked from its
|
|
; group.
|
|
; ----------------------------------------------------------------------
|
|
lda #$31 ; EDBB 49 = the last AI slot
|
|
sta scratch18 ; EDBD zp_18 = the AI slot being processed
|
|
L_EDBF:
|
|
ldy scratch18 ; EDBF Y = AI slot
|
|
lda terrainPointsBySide ; EDC1 $92AC = the TERRAIN PTS setting of side 1 (the AI), 0-3, set by the setup screens
|
|
bne L_EDC8 ; EDC4 terrain points are switched on
|
|
lda #$80 ; EDC6 switched off: force the flag on for every unit
|
|
L_EDC8:
|
|
and #$80 ; EDC8 with points on the value is 1-3, so bit 7 is clear and the flag ends up 0
|
|
L_EDCA:
|
|
sta D_8864,y ; EDCA per-unit zone flag; computeTrainerGroupStats ($E579) counts the members whose bit 7 is CLEAR, i.e. the ones that still have a zone bonus to go and earn
|
|
lda side1UnitTypeTable,y ; EDCD type byte of this AI unit
|
|
bpl L_EDDC ; EDD0 bit 7 = dead; branch for a live unit
|
|
L_EDD2:
|
|
lda #$FF ; EDD2 $FF = 'in no group'
|
|
sta D_8832,y ; EDD4 a dead unit is unhooked from its group so no later pass counts it
|
|
lda #$00 ; EDD7 leftover: A is never used, the jump target only decrements the slot counter
|
|
jmp L_EE6B ; EDD9 on to the next AI slot
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Live unit: add its ordered destination and its current cell to its group's four 16-bit sums, and
|
|
; leave both cells in the zero page for the cellDistance test further down.
|
|
; ----------------------------------------------------------------------
|
|
L_EDDC:
|
|
ldx D_8832,y ; EDDC X = the unit's group id
|
|
inc D_8902,x ; EDDF one more member in that group
|
|
lda side1UnitDestColTable,y ; EDE2 ordered destination column
|
|
and #$3F ; EDE5 strip the idle bit 7 to leave a plain 0-39 column
|
|
sta cellColB ; EDE7 zp_27 = point B for the cellDistance call at $EE5C
|
|
clc ; EDE9 clear carry before the 16-bit add
|
|
adc D_88D2,x ; EDEA add into the group's ordered-column sum ...
|
|
sta D_88D2,x ; EDED ... low byte
|
|
lda #$00 ; EDF0 A = 0: only the carry is being added on
|
|
adc play4Grp12,x ; EDF2 propagate the carry ...
|
|
sta play4Grp12,x ; EDF5 ... into the high byte at $0464
|
|
lda side1UnitDestRowTable,y ; EDF8 ordered destination row
|
|
and #$3F ; EDFB strip the idle bit
|
|
sta cellRowB ; EDFD zp_28 = row of point B
|
|
clc ; EDFF clear carry before the 16-bit add
|
|
sub_EE00:
|
|
adc D_88DE,x ; EE00 add into the group's ordered-row sum ...
|
|
sta D_88DE,x ; EE03 ... low byte
|
|
lda #$00 ; EE06 A = 0: only the carry is being added on
|
|
adc play4Grp32,x ; EE08 propagate the carry ...
|
|
sta play4Grp32,x ; EE0B ... into the high byte at $0478
|
|
lda side1UnitColTable,y ; EE0E the unit's current column, unitCol[50 + slot]
|
|
sta cellColA ; EE11 zp_22 = point A for cellDistance
|
|
clc ; EE13 clear carry before the 16-bit add
|
|
L_EE14:
|
|
adc D_88EA,x ; EE14 add into the group's current-column sum ...
|
|
sta D_88EA,x ; EE17 ... low byte
|
|
lda play4Col02,x ; EE1A high byte at $048C ...
|
|
adc #$00 ; EE1D ... plus the carry
|
|
sta play4Col02,x ; EE1F store it back
|
|
lda side1UnitRowTable,y ; EE22 the unit's current row, unitRow[50 + slot]
|
|
sta cellRowA ; EE25 zp_24 = row of point A
|
|
clc ; EE27 clear carry before the 16-bit add
|
|
L_EE28:
|
|
adc D_88F6,x ; EE28 add into the group's current-row sum ...
|
|
sta D_88F6,x ; EE2B ... low byte
|
|
lda play4Col22,x ; EE2E high byte at $04A0 ...
|
|
adc #$00 ; EE31 ... plus the carry
|
|
sta play4Col22,x ; EE33 store it back
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Stamp the unit's activation clock. The clock counts down, so subtracting the group's 0-15 round
|
|
; delay produces the clock value at which this unit wakes up. SCRIMAGE (plan 0) starts everybody at
|
|
; once, and so do group 0 and the home-defence group in every plan.
|
|
; ----------------------------------------------------------------------
|
|
ldy #$00 ; EE36 Y = 0 = 'stagger the groups'
|
|
lda D_8966 ; EE38 which plan?
|
|
bne L_EE3E ; EE3B any plan but 0 staggers the start
|
|
dey ; EE3D plan 0 (SCRIMAGE): Y = $FF = 'no staggered start'
|
|
L_EE3E:
|
|
sty scratch1A ; EE3E zp_1A holds the marker; only its bit 7 is tested
|
|
ldy scratch18 ; EE40 Y = the AI slot again
|
|
ldx D_8832,y ; EE42 X = its group id
|
|
lda gameClock ; EE45 the full game length in rounds - the clock value at the moment play starts
|
|
bit scratch1A ; EE48 test bit 7 of the plan-0 marker without disturbing A
|
|
checkGroupStartsAtOnce:
|
|
bmi L_EE59 ; EE4A plan 0: this unit is active from round one
|
|
cpx D_8964 ; EE4C is this the home-defence group?
|
|
beq L_EE59 ; EE4F the guard group never waits - it has to be in place from the start
|
|
cpx #$00 ; EE51 is this group 0?
|
|
beq L_EE59 ; EE53 group 0 follows the spearhead and never waits either
|
|
applyGroupStartDelay:
|
|
sec ; EE55 prepare to subtract
|
|
sbc D_893E,x ; EE56 otherwise subtract the group's 0-15 round delay from the clock
|
|
L_EE59:
|
|
sta msgGetOpponentOnPhoneAnd,y; EE59 $8800 + slot = the clock value at which this unit starts obeying orders (the msg... label is a $8800 message-string address collision)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A unit that is already standing on its ordered destination gets the idle bit set, so the engine will
|
|
; not spend movement points shuffling it.
|
|
; ----------------------------------------------------------------------
|
|
jsr cellDistance ; EE5C distance between the unit's cell (zp_22/zp_24) and its ordered destination (zp_27/zp_28)
|
|
cmp #$00 ; EE5F redundant - cellDistance already returns with the flags set from its own TXA
|
|
bne L_EE6B ; EE61 still has somewhere to go
|
|
lda side1UnitDestColTable,y ; EE63 its ordered column
|
|
ora #$80 ; EE66 set bit 7 = 'arrived / idle'
|
|
sta side1UnitDestColTable,y ; EE68 store it back
|
|
L_EE6B:
|
|
dec scratch18 ; EE6B next AI slot down
|
|
bmi L_EE72 ; EE6D the counter went negative: all 50 slots done
|
|
jmp L_EDBF ; EE6F round again (too far for a branch)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Turn the four 16-bit sums of every group 0..$8953 into averages by dividing by the member count. A
|
|
; group with no members divides by zero here; the quotient is meaningless but the group has no units
|
|
; for anything to act on either.
|
|
; ----------------------------------------------------------------------
|
|
L_EE72:
|
|
lda D_8953 ; EE72 highest group index in use
|
|
sta scratch1B ; EE75 zp_1B = the group being averaged, counting down to 0
|
|
L_EE77:
|
|
ldx scratch1B ; EE77 X = group index
|
|
lda D_8902,x ; EE79 how many members it has
|
|
sta scratch1C ; EE7C zp_1C = the divisor, reloaded before each of the four divisions
|
|
ldy play4Grp12,x ; EE7E high byte of the ordered-column sum
|
|
lda D_88D2,x ; EE81 low byte
|
|
ldx scratch1C ; EE84 X = divisor
|
|
jsr divideSigned16by8 ; EE86 Y:A / X
|
|
ldx scratch1B ; EE89 X = group index again
|
|
L_EE8B:
|
|
sta D_88D2,x ; EE8B the group's average ordered column
|
|
ldy play4Grp32,x ; EE8E high byte of the ordered-row sum
|
|
lda D_88DE,x ; EE91 low byte
|
|
ldx scratch1C ; EE94 X = divisor
|
|
jsr divideSigned16by8 ; EE96 Y:A / X
|
|
ldx scratch1B ; EE99 X = group index
|
|
sta D_88DE,x ; EE9B the group's average ordered row
|
|
ldy play4Col02,x ; EE9E high byte of the current-column sum
|
|
L_EEA1:
|
|
lda D_88EA,x ; EEA1 low byte
|
|
ldx scratch1C ; EEA4 X = divisor
|
|
jsr divideSigned16by8 ; EEA6 Y:A / X
|
|
ldx scratch1B ; EEA9 X = group index
|
|
sta D_88EA,x ; EEAB the group's current centre column
|
|
ldy play4Col22,x ; EEAE high byte of the current-row sum at $04A0
|
|
lda D_88F6,x ; EEB1 low byte
|
|
ldx scratch1C ; EEB4 X = divisor
|
|
L_EEB6:
|
|
jsr divideSigned16by8 ; EEB6 Y:A / X
|
|
storeGroupCentreRow:
|
|
ldx scratch1B ; EEB9 X = group index
|
|
sta D_88F6,x ; EEBB the group's current centre row
|
|
L_EEBE:
|
|
dec scratch1B ; EEBE next group down
|
|
bpl L_EE77 ; EEC0 loop until group 0 has been averaged
|
|
rts ; EEC2 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildAiHeadingSearchOrder - Fills the 8-byte table at $894A with the order in which the resident AI
|
|
; tries headings: 0 first, then +/-1, +/-2, +/-3 and finally 4 (the opposite heading), every value
|
|
; folded into 0-7. The sign of each pair is decided by a coin toss, so a given game has a consistent
|
|
; random left-or-right bias. Because -4 == +4 mod 8 the last entry is always 4, and the first entry
|
|
; is always 0 (its 'negate' at $EED4 acts on a magnitude that is still zero).
|
|
; In: nextGameRandom
|
|
; Out: $894A-$8951 = eight heading offsets; zp_18 scratch; A and Y clobbered
|
|
; Called from: $ED8E in rebuildAiGroupTables (this file) - the only caller in this overlay
|
|
; ----------------------------------------------------------------------
|
|
buildAiHeadingSearchOrder:
|
|
lda #$00 ; EEC3 start from a zero heading offset
|
|
sta scratch18 ; EEC5 zp_18 = the running heading offset, starting at 0
|
|
ldy #$01 ; EEC7 Y = 1 writes the first entry, $8949+1 = $894A; the loop runs Y = 1..8
|
|
L_EEC9:
|
|
tya ; EEC9 A = the entry number
|
|
lsr a ; EECA A = Y/2 = the magnitude of this pair, and C = 1 on the second (odd) entry of each pair
|
|
bcs L_EED4 ; EECB second entry of a pair: reuse the magnitude with the opposite sign
|
|
sta scratch18 ; EECD first entry of a pair: adopt the new magnitude 0, 1, 2, 3 or 4
|
|
jsr nextGameRandom ; EECF toss a coin ...
|
|
bpl storeHeadingOffset ; EED2 ... heads (bit 7 clear): take this pair positive first, tails: fall through and negate
|
|
L_EED4:
|
|
lda scratch18 ; EED4 negate the current magnitude ...
|
|
eor #$FF ; EED6 ... one's complement ...
|
|
clc ; EED8 prepare the +1 of the two's complement negate
|
|
adc #$01 ; EED9 ... plus one
|
|
sta scratch18 ; EEDB keep the signed offset
|
|
storeHeadingOffset:
|
|
lda scratch18 ; EEDD the signed offset, -4..+4
|
|
L_EEDF:
|
|
clc ; EEDF clear carry for the bias below
|
|
sub_EEE0:
|
|
adc #$08 ; EEE0 bias by 8 so a negative offset stays positive ...
|
|
and #$07 ; EEE2 ... and fold it into a heading 0-7
|
|
sta D_8949,y ; EEE4 $8949+Y, i.e. $894A..$8951
|
|
iny ; EEE7 next entry
|
|
cpy #$09 ; EEE8 eight entries written?
|
|
bcc L_EEC9 ; EEEA keep going while Y is below 9
|
|
rts ; EEEC done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; orderAiGroupToPoint - Orders AI group Y to move so that its formation centre ends up on column X,
|
|
; row A. The two coordinates are stashed by patching the immediate operands of the two LDA # at
|
|
; $EEF8 and $EEFC, because computeTrainerGroupStats clobbers A, X and Y. An empty group is left
|
|
; alone. Falls into shiftAiGroupDestinations.
|
|
; In: X = target column, A = target row, Y = group index; $8832 group ids, the unit arrays
|
|
; Out: self-modifies $EEF9 and $EEFD (the operand bytes); $8955 = Y; the group's target ($88D2/$88DE)
|
|
; and the unitDestCol/unitDestRow of every member are updated; returns with C=1 and nothing
|
|
; changed when the group has no live member
|
|
; Called from: $ED56 and $ED69 in initTrainerAi (this file)
|
|
; Self-modifying code: $EEED writes the target column into the operand byte $EEF9 (of the LDA # at
|
|
; $EEF8) and $EEF0 writes the target row into $EEFD (of the LDA # at $EEFC). The $FF operands shown
|
|
; in the listing are just what the overlay was assembled with.
|
|
; ----------------------------------------------------------------------
|
|
orderAiGroupToPoint:
|
|
stx L_EEF8+1 ; EEED park the target column in the operand byte of the LDA # at $EEF8
|
|
sta L_EEFC+1 ; EEF0 park the target row in the operand byte of the LDA # at $EEFC
|
|
jsr computeTrainerGroupStats; EEF3 measure group Y ($E531): member count, bounding box, centre and average ordered cell; clobbers A/X/Y
|
|
bcs L_EF26 ; EEF6 C=1 means the group has no live member - nothing to order
|
|
L_EEF8:
|
|
lda #$FF ; EEF8 operand patched at $EEED = the target column
|
|
sta cellColA ; EEFA zp_22 = wanted formation centre column
|
|
L_EEFC:
|
|
lda #$FF ; EEFC operand patched at $EEF0 = the target row
|
|
sta cellRowA ; EEFE zp_24 = wanted formation centre row
|
|
|
|
; ----------------------------------------------------------------------
|
|
; shiftAiGroupDestinations - Moves a whole AI group without breaking its formation. The wanted centre
|
|
; in zp_22/zp_24 is first clipped so the group's bounding box still fits on the 40x40 map and stored
|
|
; as the group's target; the resulting shift is then added to the ordered destination of every unit
|
|
; whose group id matches $8955, so the formation is translated as a block. The AND #$3F on the way
|
|
; also clears the idle bit 7, which restarts any member that had already arrived.
|
|
; In: zp_22/zp_24 = wanted centre, $8955 = the group (set by computeTrainerGroupStats), $8832 group
|
|
; ids, unitDestColTable+50 $F802, unitDestRowTable+50 $F866, the group's box
|
|
; $890E/$891A/$8926/$8932
|
|
; Out: $88D2/$88DE = the group's clipped target, unitDestCol/unitDestRow of every member shifted,
|
|
; zp_2A/zp_2B = the shift applied, zp_22/zp_24 clipped, $8958
|
|
; Called from: $ED3F in initTrainerAi and by falling out of orderAiGroupToPoint (both in this file)
|
|
; ----------------------------------------------------------------------
|
|
shiftAiGroupDestinations:
|
|
jsr rearmCarrierTimer ; EF00 really trainerAiE000:clampGroupTargetToMap ($E5FF) - the modem driver's name for this address is a collision. It clips zp_22/zp_24 so the whole formation box stays on the map, stores the result as the group's target in $88D2/$88DE and returns the shift in zp_2A (columns) and zp_2B (rows) (also trainerAiE000:clampGroupTargetToMap)
|
|
ldy #$31 ; EF03 AI slots 49 down to 0
|
|
L_EF05:
|
|
lda D_8832,y ; EF05 group id of this AI unit
|
|
cmp D_8955 ; EF08 the group computeTrainerGroupStats last measured
|
|
bne L_EF23 ; EF0B not a member: skip it
|
|
lda side1UnitDestColTable,y ; EF0D its ordered column
|
|
stripIdleBitFromDestCol:
|
|
and #$3F ; EF10 keep the 0-39 column, dropping the idle bit 7 so the unit is set moving again
|
|
clc ; EF12 clear carry for the shift add
|
|
adc scratch2A ; EF13 apply the group's column shift
|
|
sta side1UnitDestColTable,y ; EF15 store the new ordered column
|
|
lda side1UnitDestRowTable,y ; EF18 its ordered row
|
|
and #$3F ; EF1B keep the 0-39 row only
|
|
clc ; EF1D clear carry for the shift add
|
|
adc scratch2B ; EF1E apply the group's row shift
|
|
L_EF20:
|
|
sta side1UnitDestRowTable,y ; EF20 store the new ordered row
|
|
L_EF23:
|
|
dey ; EF23 next AI slot
|
|
bpl L_EF05 ; EF24 loop over all 50
|
|
L_EF26:
|
|
rts ; EF26 done (also the early exit for an empty group)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; staleDroneSteerTail - DEAD CODE in this overlay. From $EF27 to $EFFF the track 29 sector 0-3 image
|
|
; is byte for byte identical to the track 29 sector 4-5 drone-flying overlay (game/droneAiEE00), and
|
|
; these bytes are the tail of that overlay's steerDroneAndEmitMove, whose real entry point is $EF0F.
|
|
; Nothing in this file reaches $EF27 - the routine above ends with an RTS at $EF26 - and the whole
|
|
; page is thrown away again by $7B33 a few instructions after initTrainerAi returns. What the code
|
|
; does where it is live: turns the drone heading $896F by the step in X (C=1 forces one step
|
|
; anticlockwise), converts the new heading into a column/row delta through headingToDirectionTable
|
|
; ($7316), diagonalDirectionTable ($1FB2) and directionDeltaTable ($20F0), advances the drone by one
|
|
; cell, and then writes a 4-byte command $8D (cmdDroneMove: heading, column, row) straight into the
|
|
; comm module's INCOMING packet buffer at $E020 with $E01E = 4 - i.e. the trainer hands the engine a
|
|
; packet as though the remote player had sent it. The coordinates are pre-mirrored so that the
|
|
; engine's mirror-and-decrement of a remote $8D lands them back on the right cell.
|
|
; In: X = heading change, C = 1 forces -1, $896F drone heading, $8974/$8975 drone column/row
|
|
; Out: $896F, $8974/$8975 advanced one cell, $E020-$E023 = $8D + heading + mirrored column/row,
|
|
; $E01E = 4
|
|
; Called from: nothing in this file (live at $EF0F-$EF9D in game/droneAiEE00)
|
|
; ----------------------------------------------------------------------
|
|
staleDroneSteerTail:
|
|
bcc L_EF2B ; EF27 carry clear: use the turn amount the caller put in X
|
|
ldx #$FF ; EF29 carry set: turn one step anticlockwise
|
|
L_EF2B:
|
|
txa ; EF2B A = the signed turn
|
|
L_EF2C:
|
|
clc ; EF2C clear carry for the heading add
|
|
adc D_896F ; EF2D add it to the current drone heading
|
|
and #$07 ; EF30 wrap into 0-7 (8 compass points)
|
|
sta D_896F ; EF32 the new heading
|
|
L_EF35:
|
|
ldy D_896F ; EF35 Y = heading 0-7
|
|
L_EF38:
|
|
lda headingToDirectionTable,y; EF38 $7316 in the $6F00 overlay: heading -> direction code (0-1 row only, 2-3 column only, 4-7 diagonal) (also mapGenerator6F00:forestBlockCompareNe)
|
|
ldy #$00 ; EF3B Y = row delta, assume 0
|
|
ldx #$00 ; EF3D X = column delta, assume 0
|
|
cmp #$04 ; EF3F is the code a diagonal?
|
|
bcs L_EF57 ; EF41 codes 4-7 move on both axes
|
|
cmp #$02 ; EF43 is the code a pure-column move?
|
|
bcs L_EF4F ; EF45 codes 2-3 move along the column only
|
|
tay ; EF47 codes 0-1: row only
|
|
L_EF48:
|
|
lda directionDeltaTable,y ; EF48 $20F0: direction code -> signed delta
|
|
tay ; EF4B Y = row delta
|
|
jmp L_EF6D ; EF4C on to the position update
|
|
|
|
L_EF4F:
|
|
tax ; EF4F codes 2-3
|
|
lda directionDeltaTable,x ; EF50 $20F0: direction code -> signed delta
|
|
L_EF53:
|
|
tax ; EF53 X = column delta
|
|
jmp L_EF6D ; EF54 on to the position update
|
|
|
|
L_EF57:
|
|
tay ; EF57 codes 4-7
|
|
L_EF58:
|
|
lda diagonalDirectionTable,y; EF58 $1FB2: packs the two component codes of a diagonal into the two nibbles
|
|
pha ; EF5B keep the packed pair
|
|
and #$0F ; EF5C low nibble = the row component
|
|
tay ; EF5E index the delta table with it
|
|
lda directionDeltaTable,y ; EF5F $20F0: -> signed delta
|
|
tay ; EF62 Y = row delta
|
|
pla ; EF63 get the packed pair back
|
|
lsr a ; EF64 shift the high nibble down ...
|
|
lsr a ; EF65 ...
|
|
lsr a ; EF66 ...
|
|
lsr a ; EF67 ... = the column component
|
|
tax ; EF68 index the delta table with it
|
|
lda directionDeltaTable,x ; EF69 $20F0: -> signed delta
|
|
tax ; EF6C X = column delta
|
|
L_EF6D:
|
|
txa ; EF6D A = column delta
|
|
clc ; EF6E clear carry for the position add
|
|
adc D_8974 ; EF6F current drone column
|
|
sta D_8974 ; EF72 advance it one cell
|
|
tax ; EF75 X = the new column, for mirrorBlockCornerForOpponent
|
|
tya ; EF76 A = row delta
|
|
L_EF77:
|
|
clc ; EF77 clear carry for the position add
|
|
adc D_8975 ; EF78 current drone row
|
|
sta D_8975 ; EF7B advance it one cell
|
|
tay ; EF7E Y = the new row, for mirrorBlockCornerForOpponent
|
|
lda #$8D ; EF7F command $8D = cmdDroneMove
|
|
sta rxPacketBuffer ; EF81 byte 0 of the comm module's INCOMING packet buffer: the trainer forges the packet the engine is about to read (also trainerAiE000:rxPacketBuffer)
|
|
lda D_896F ; EF84 the drone's heading
|
|
clc ; EF87 clear carry for the add
|
|
adc #$04 ; EF88 +4 ...
|
|
and #$07 ; EF8A ... modulo 8 = the same heading seen from the other end of the mirrored map
|
|
sta rxPacketArg1 ; EF8C byte 1 of the packet (also trainerAiE000:rxPacketArg1)
|
|
jsr mirrorBlockCornerForOpponent; EF8F $EA38: X,Y = 39-(X+1), 39-(Y+1), pre-compensating the mirror-and-decrement the engine applies to a remote $8D
|
|
stx rxPacketArg2 ; EF92 byte 2 of the packet = mirrored column (also trainerAiE000:rxPacketArg2)
|
|
sty rxPacketArg3 ; EF95 byte 3 of the packet = mirrored row (also trainerAiE000:rxPacketArg3)
|
|
lda #$04 ; EF98 packet length 4
|
|
L_EF9A:
|
|
sta rxPacketLength ; EF9A tell the game a 4-byte packet has arrived; mergeIncomingPacket ($4F05) will execute it (also trainerAiE000:rxPacketLength)
|
|
rts ; EF9D done
|
|
|
|
|
|
; measureDroneDistanceToTarget - DEAD in this overlay (nothing here calls it), but a complete 13-byte
|
|
; routine: it copies the drone's cell $8974/$8975 into the reference point zp_27/zp_28 and tail-jumps
|
|
; to cellDistance ($E82A), which returns the integer distance 0-16 between the cell in zp_22/zp_24 and
|
|
; the drone. Y is left untouched, which is what lets the live caller keep a unit index in Y across
|
|
; the call. Rendered as .byte because no reachable code in this file references it; it disassembles
|
|
; as LDA $8974 / STA $27 / LDA $8975 / STA $28 / JMP $E82A and is the same routine as
|
|
; droneDistanceToCell at $EF9E in game/droneAiEE00.
|
|
; In: zp_22/zp_24 = cell to measure against, $8974/$8975 = drone cell
|
|
; Out: A = X = distance 0-16, zp_27/zp_28 = drone cell, Y preserved
|
|
; Called from: nothing in this file ($EE26 and $EEFC in game/droneAiEE00)
|
|
measureDroneDistanceToTarget:
|
|
.byte $AD ; EF9E AD 74 89 = LDA $8974 (drone column), 85 27 = STA zp_27, AD 75 89 = LDA $8975 (drone row)
|
|
.byte $74,$89,$85,$27,$AD,$75,$89; EF9F t..'.u.
|
|
.byte $85 ; EFA6 85 ...
|
|
.byte $28 ; EFA7 ... 28 = STA zp_28, then 4C 2A E8 = JMP cellDistance ($E82A)
|
|
.byte $4C,$2A,$E8 ; EFA8 L*.
|
|
|
|
; ----------------------------------------------------------------------
|
|
; staleDroneTargetPickerTail - 34 stale bytes, unreachable and unusable here. In the solo trainer's
|
|
; own $EC00 tail (track 35 sectors 12-15) and in the drone overlay (track 29 sectors 4-5) - both of
|
|
; which carry these exact bytes - they are the end of the drone's target picker: bump the candidate
|
|
; index zp_1F, loop back to $EF9B for the next of 15 candidate blocks, then adopt the best player unit
|
|
; found (zp_1D) as the drone's target $8976 and lower the detonation threshold $8979 to three quarters
|
|
; so the drone will settle for a weaker target next time round. In THIS image the BCC at $EFB1
|
|
; branches to $EF9B, which is the middle of the STA $E01E at $EF9A, so the fragment could not run even
|
|
; if something jumped to it. In the modem driver's tail (track 34 sectors 12-15) the same addresses
|
|
; hold the middle of the message 'INSERT DATA DISK AND PRESS SPACE.' instead. The bytes only survive
|
|
; so that the shared strings from $EFCD on land at their fixed addresses.
|
|
; In: zp_1D best candidate unit, zp_1F candidate index, $8979
|
|
; Out: $8976 drone target, $8979 reduced to 3/4, zp_18 scratch
|
|
; Called from: nothing - dead in this overlay
|
|
; ----------------------------------------------------------------------
|
|
staleDroneTargetPickerTail:
|
|
inc scratch1F ; EFAB next candidate block
|
|
lda scratch1F ; EFAD how many have been scored
|
|
cmp #$0F ; EFAF 15 candidate blocks per pass
|
|
bcc L_EF9A+1 ; EFB1 branches to $EF9B - a real instruction only in the images where this fragment is live; here it is the middle of the STA $E01E at $EF9A
|
|
L_EFB3:
|
|
lda scratch1D ; EFB3 the best player unit found by the scoring pass
|
|
bmi L_EFCC ; EFB5 bit 7 set = no candidate at all
|
|
sta D_8976 ; EFB7 adopt it as the drone's target unit
|
|
lda D_8979 ; EFBA the current detonation score threshold
|
|
lsr a ; EFBD /2 ...
|
|
lsr a ; EFBE ... /4
|
|
sta scratch18 ; EFBF keep a quarter of it
|
|
lda D_8979 ; EFC1 the threshold again
|
|
sec ; EFC4 prepare to subtract
|
|
sbc scratch18 ; EFC5 threshold - threshold/4 = three quarters of it
|
|
bcc L_EFCC ; EFC7 underflow: leave the threshold alone
|
|
sta D_8979 ; EFC9 lower the bar so the drone will accept a weaker target next time
|
|
L_EFCC:
|
|
rts ; EFCC done
|
|
|
|
|
|
; $EFCD-$EFFE is byte for byte identical in all three images that can occupy this page (the trainer
|
|
; setup overlay here, the solo trainer's own tail from track 35 and the modem driver's tail from track
|
|
; 34), and only the very last byte at $EFFF differs. That is deliberate: the setup screens in overlay
|
|
; A print these strings by absolute address and must not care which image is resident. Only the T34
|
|
; image, which $7B33 restores immediately after this overlay has run, also carries the 'INSERT xxxx
|
|
; DISK AND PRESS SPACE.' template that begins at $EFAF - here those bytes are the stale code above, so
|
|
; the three characters below are all that is left of that message.
|
|
msgSpaceTailFragment:
|
|
.byte "CE",'.'|$80 ; EFCD tail of 'INSERT xxxx DISK AND PRESS SPACE.' - the terminator '.' has bit 7 set
|
|
|
|
; msgDataGameCustomOff: text, 18 bytes. 'DATAGAMECUSTOM OF'+'F'|$80; almost certainly addressed at two
|
|
; offsets ('DATAGAME' at $EFD0 and 'CUSTOM OFF' at $EFD8), only the final F carries the bit-7
|
|
; terminator.
|
|
msgDataDiskName:
|
|
.byte "DATA" ; EFD0 'DATA' - the 4-character disk name promptInsertDataDisk ($81B2) patches into the message template at $EFB6
|
|
msgGameDiskName:
|
|
.byte "GAME" ; EFD4 'GAME' - the disk name promptInsertGameDisk ($81A5) uses instead
|
|
msgCustomOff:
|
|
.byte "CUSTOM OF",'F'|$80 ; EFD8 'CUSTOM OFF' - installed as message slot $1E by overlay A; its ON/OFF patch points at the 'OFF' at $EFDF
|
|
|
|
; msgDestroyOff: text, 11 bytes. 'DESTROY OF'+'F'|$80.
|
|
msgDestroyOff:
|
|
.byte "DESTROY OF",'F'|$80 ; EFE2 'DESTROY OFF' - message slot $1E for the DESTROY toggle; the ON/OFF patch points at the 'OFF' at $EFEA
|
|
|
|
; msgAccepted: text, 8 bytes. 'ACCEPTE'+'D'|$80.
|
|
msgAccepted:
|
|
.byte "ACCEPTE",'D'|$80 ; EFED 'ACCEPTED' - printed centred on row 15 by overlay A when the fire button confirms an edited value
|
|
|
|
; overlayTailPadding_EFF5: byteTable, 11 bytes. $C0,$DF then four $00,$FF pairs and a trailing byte
|
|
; ($FF here, $00 in the T34 tail). Purpose unknown; the same bytes end every $EC00/$EE00 overlay
|
|
; variant.
|
|
overlayPageTailBytes:
|
|
.byte $C0,$DF,$00,$FF,$00,$FF,$00,$FF; EFF5 ........ unreferenced trailing bytes, the same in every image of this page
|
|
.byte $00 ; EFFD the last byte is $FF here and in the track 35 trainer tail, $00 in the track 34 modem tail
|
|
.byte $FF,$FF ; EFFE ..
|