{
  "chunk": "game_ovl_E000_T35_EC82",
  "unit": "game/ovl_E000_T35",
  "routines": [
    {
      "addr": "EC82",
      "name": "rankThreatDirections",
      "summary": "Second half of sub_EBEF (the routine starts at $EBEF, outside this chunk). sub_EBEF has just scanned the human player's mobile units 0-45 and accumulated a saturating 'enemy pressure' weight per compass direction into the 8 bytes $8896-$889D, the total into zp_1F, the number of threatening units into zp_20 and the largest weapon-range surplus into zp_21. This tail copies the 8-direction ring to the $0400 scratch page, smooths it (sub-loop at $EC91), searches the quietest 3-direction arc (sub-loop at $ECD4) and returns the summary. If zp_20 was 0 the caller path at $EC7C returns A=0, X=0, Y=8 instead (index 8 of the direction tables $E68A/$E693 is the null delta 0,0).",
      "inputs": "zp_1F total threat weight, zp_20 threatening unit count, zp_21 max range surplus, $8896-$889D per-direction weights, zp_57 (game RNG low byte) for the tie break",
      "outputs": "A = (zp_1F - zp_20) / 2 = overall threat level; Y = zp_18 = index 0-7 of the safest (lowest-pressure) compass direction, or 8 when nothing threatens; X = zp_21; $0400-$0407 and $8896-$889D overwritten; zp_18, zp_19, zp_1A, zp_23 clobbered",
      "confidence": "high"
    },
    {
      "addr": "EC91",
      "name": "smoothThreatRing",
      "summary": "Inner loop of rankThreatDirections (branch target of the DEC zp_23 / BPL at $ECCA, not a JSR target in this build). For each direction i = 7..0 it computes (2*ring[i] + ring[i-1] + ring[i+1]) / 4 with a 2-bit carry kept in zp_19, reading the unsmoothed copy from $0400,i and writing the result back to $8896,i. Indices wrap modulo 8, so it is a circular 1-2-1 blur of the 8-direction threat ring.",
      "inputs": "$0400-$0407 = unsmoothed direction weights, zp_23 = current direction index",
      "outputs": "$8896-$889D = smoothed weights; zp_19 carry scratch; zp_23 counts down to $FF",
      "confidence": "high"
    },
    {
      "addr": "ECD4",
      "name": "findQuietestDirectionArc",
      "summary": "Second inner loop of rankThreatDirections (branch target of $ECFE). For each y = 7..0 it sums the smoothed weights of the three adjacent directions y, y-1, y-2 (wrapping) and keeps the smallest sum in zp_19 with the centre direction (y-1) in zp_18, so the result is the middle of the calmest 135-degree arc. Ties are broken pseudo-randomly by BIT zp_57 / BMI at $ECF3 (zp_57 is the low byte of the lock-step game RNG state $57-$59). The three-term sum is 8-bit and can wrap if all weights are large.",
      "inputs": "$8896-$889D smoothed weights, zp_57",
      "outputs": "zp_18 = quietest direction 0-7, zp_19 = its arc sum; zp_1A scratch",
      "confidence": "high"
    },
    {
      "addr": "ED0B",
      "name": "runAiMissileTurn",
      "summary": "The computer opponent's missile/drone decision, called once per command exchange from sub_EA7B ($EA80), which is itself reached from the packet-transmit entry sub_E000 (X=1) at $E082. It first offers to launch the AI's own drone (sub_EE85), then, if the human player currently has a drone in the air ($920D bit 6) and the AI still has missiles ($92B1), decides whether to shoot at that drone: the drone cell is $92F6+6 / $92F7+5, the aim point goes into zp_22/zp_24 and a reluctance score is built in zp_23 from the range to the AI comcen, the comcen's stun state ($F9C3 & $0F), its energy bits and a missile-conservation test against the human's remaining drones ($92AE). A shot fires either as a clean kill (command $92 written at $E020, i.e. 'drone finished') or as a near miss whose impact point is scattered by +/-(zp_23/2+1) cells with sub_EE55. It then falls through into the code at $EDD5/$EE00 that fires at an ordinary spotted unit.",
      "inputs": "$8966 action budget, $920D droneStateFlags, $92B1 AI missiles left, $92AE human drones left, $92F6/$92F7 drone view origin, $F6A3/$F707 AI comcen col/row, $F9C3, $FA27, $8970 reload timer, $920B/$896F drone heading, game RNG",
      "outputs": "may append command $92 and/or $90 col,row to the fabricated incoming packet ($E020/$E01E), $8970 reload timer, $8966 decremented, $8968 += 2, zp_22/zp_23/zp_24/zp_29/zp_2B/zp_18 clobbered",
      "confidence": "medium"
    },
    {
      "addr": "EDD5",
      "name": "fireMissileAtSpottedUnit",
      "summary": "Continuation of runAiMissileTurn (branch/JMP target only). When no player drone is being steered, the AI still has more missiles than twice the human's remaining drones, its reload timer $8970 has expired and its action budget $8966 is not spent, it fires a missile at the unit index held in $8972 (or, falling through at $EE00, at the human comcen unit $31 when that comcen is currently visible, $F9F5 bit 7). sub_EE6A checks that the target sits inside the AI comcen's 21x17 radar grid; the reload timer is then set to half the range and sub_EE2D queues the $90 strike. Ends by remembering the current drone heading in $896F and ticking $8970 down.",
      "inputs": "$920D, $92AE, $92B1, $8970, $8966, $8972 current missile target, $F76C unit type/alive flags, $F9F5, $920B",
      "outputs": "$8972 (set to $FF when no shot), $8970, $896F, packet bytes via sub_EE2D",
      "confidence": "medium"
    },
    {
      "addr": "EE00",
      "name": "targetEnemyComcenWithMissile",
      "summary": "Fall-through label inside fireMissileAtSpottedUnit: if the human comcen (unit $31) is currently visible ($F9F5 bit 7) it becomes the missile target. IMPORTANT: although XREF lists 'sub_EE00 call:E0A5', the JSR at $E0A5 does NOT reach this code - $EE00-$EFFF is a swappable sub-overlay slot and $E0A5 is only executed while the track 29 s4-5 variant (disassembly/game/ovl_EE00_T29.s, the AI's drone-flying code) is loaded there. In this T35 build $EE00 is never entered from outside.",
      "inputs": "$F9F5 = energy/visibility byte of unit 49 (the human comcen)",
      "outputs": "X = $31 when the comcen is visible, otherwise branches away to $EE19",
      "confidence": "high"
    },
    {
      "addr": "EE2D",
      "name": "queueMissileStrikeCommand",
      "summary": "Appends a 3-byte command $90 col,row (missile strike, handler cmdMissileStrike $53C0) to the fabricated incoming packet at $E020, length $E01E. Both coordinates go through mirrorMapCoordinate ($3FA3, 39-v) because the packet is presented to the engine as if it had arrived from the remote player, and the command dispatcher un-mirrors remote parameters. Also spends one AI action ($8966--) and charges two units of packet cost ($8968 += 2).",
      "inputs": "zp_22 = strike column, zp_24 = strike row, $E01E current packet length",
      "outputs": "$E020+ packet bytes, $E01E advanced by 3, $8966 decremented, $8968 incremented twice, X clobbered",
      "confidence": "high"
    },
    {
      "addr": "EE55",
      "name": "randomlySignValue",
      "summary": "Returns A or -A with equal probability, using bit 0 of nextGameRandom ($FD5D). Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss the player's drone.",
      "inputs": "A = magnitude",
      "outputs": "A = zp_18 = +/-A (two's complement), game RNG advanced",
      "confidence": "high"
    },
    {
      "addr": "EE6A",
      "name": "isUnitOnAiRadar",
      "summary": "Loads unit X's map position into zp_22/zp_24 and tests whether that cell lies inside the 21x17 radar grid centred on the AI's comcen: it temporarily forces comcenUnit $9236 to $63 (unit 99 = side 1 comcen), calls overlay B's mapCellToRadarGrid ($7A13) and restores $9236 to $31 (unit 49 = the local player's comcen). The unconditional restore to $31 shows that this module always assumes the local player is side 0 and the computer opponent is side 1.",
      "inputs": "X = unit index, $F640/$F6A4 unit column/row arrays",
      "outputs": "zp_22 = column, zp_24 = row, C = 0 when the cell is on the AI's radar grid, C = 1 otherwise; $9236 left at $31; X/Y clobbered",
      "confidence": "high"
    },
    {
      "addr": "EE85",
      "name": "launchAiDrone",
      "summary": "Called first thing by runAiMissileTurn. When the launch delay $8977 has run out, a drone target unit has been chosen ($8976 >= 0) and no drone is airborne ($920D bit 7 clear), it fabricates the opponent's drone launch directly into the incoming packet: $E020 = $97 (launch), $E021 = $8D (drone move) with heading, column and row parameters, $E01E = 5. The launch point is the AI comcen's cell converted with sub_EA38 (v+1 then mirrored = 38-v, pre-compensating the mirror-and-decrement the $8D handler applies to remote commands). The heading byte is whatever sub_E80E leaves in A, which is the row component of the direction step (-1/0/+1); cmdDroneMove turns it into sprite shape (heading+4)&7 = 3/4/5, i.e. the drone is always drawn flying towards the player - possibly deliberate, possibly a slip for the direction index that sub_E80E returns in Y.",
      "inputs": "$8977 launch delay, $8976 target unit, $920D, $F6A3/$F707 AI comcen position, $F640/$F6A4 target position",
      "outputs": "$E020-$E024 = $97,$8D,heading,col,row; $E01E = 5; $8974/$8975 = launch column/row; $8973 = $64 (100 drone steps); $8977 = 3; $8966 decremented; zp_92-zp_95 line-stepper state clobbered",
      "confidence": "medium"
    },
    {
      "addr": "EEE0",
      "name": "chooseAiDroneTarget",
      "summary": "Called once per AI round from $E1FF. Decides what the computer opponent should send its next drone at and stores the chosen unit index in $8976. Returns immediately if the AI has no drones left ($92AF = 0) or a target is already pending. In game type 2 (THE BOMB) the only target is the human comcen, once it is visible or on the AI's radar. Otherwise, if the human comcen is visible it sums the weights $8902[i] of every objective flagged $20 (doubled) or $40 in $893E[i]; when that sum reaches 8 the AI commits to an all-out attack: it sets bit 6 in every objective flag, re-stamps the order clock of all 46 mobile AI units (the 46-byte array at $8800, indexed relative to unit 50) with the game clock $91CB, sets the launch delay $8977 = $15 and targets the comcen. If the comcen is not visible it falls into pickDroneTargetAlongThreatAxis.",
      "inputs": "$92AF AI drones left, $8976, $0BA3 game type, $F9F5 comcen visibility, $8902/$893E objective tables, $8953/$8954 objective count, $91CB game clock",
      "outputs": "$8976 = target unit index ($31 for the comcen), $893E[] bit 6 set, $8800-$882D = $91CB, $8977 = $15; zp_18, zp_19 clobbered",
      "confidence": "medium"
    },
    {
      "addr": "EF59",
      "name": "pickDroneTargetAlongThreatAxis",
      "summary": "Tail of chooseAiDroneTarget, reached when the human comcen is not a viable drone target. It takes the AI's current objective ($895D) - its map cell from $88EA/$88F6 and its threat threshold from $8902, or the AI comcen and threshold 3 when no objective is active - and runs the threat scan sub_EBEF around it. If the measured pressure reaches the threshold and at least one enemy unit is in weapon range, it walks a ray in the direction opposite the safest one ((Y+4) & 7, i.e. straight into the enemy) at radii 4..14 with sub_E66C and scores each 2x2 block of cells with sub_E9A6 (unit values 1/2/4/1/4 for GRUNT/RIDER/BOOMER/SPY/COMCEN). The best cluster's unit index becomes the drone target $8976, and the acceptance threshold $8979 is reduced to 75% of its value so the AI grows less picky over the game.",
      "inputs": "$895D current objective, $88EA/$88F6 objective column/row, $8902 objective weights, $8979 target value threshold, $F6A3/$F707 AI comcen position",
      "outputs": "$8976 target unit index, $8979 scaled down, $8956 threshold copy, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius, zp_22/zp_24/zp_27/zp_28 clobbered",
      "confidence": "medium"
    }
  ],
  "variables": [
    {"addr": "18", "scope": "zp", "name": "aiScratch18", "meaning": "General scratch; in rankThreatDirections it holds the index of the quietest direction, in chooseAiDroneTarget the objective-weight accumulator, in runAiMissileTurn a missile-conservation term", "confidence": "high"},
    {"addr": "19", "scope": "zp", "name": "aiScratch19", "meaning": "Carry accumulator of the direction blur ($EC9C-$ECC2), then the best (smallest) 3-direction arc sum; also the per-objective weight in chooseAiDroneTarget", "confidence": "high"},
    {"addr": "1A", "scope": "zp", "name": "arcCentreDirection", "meaning": "Centre direction (y-1) of the arc currently being summed by findQuietestDirectionArc; also the 2x2 cluster score inside sub_E9A6", "confidence": "high"},
    {"addr": "1B", "scope": "zp", "name": "bestClusterUnit", "meaning": "Unit index found by sub_E9A6 in the 2x2 block it is scoring", "confidence": "medium"},
    {"addr": "1C", "scope": "zp", "name": "droneTargetBestScore", "meaning": "Running best 2x2 cluster score while scanning for a drone target; initialised from $8979", "confidence": "high"},
    {"addr": "1D", "scope": "zp", "name": "droneTargetBestUnit", "meaning": "Unit index of the best drone target found so far, $FF = none", "confidence": "high"},
    {"addr": "1E", "scope": "zp", "name": "droneScanDirection", "meaning": "Compass direction 0-7 of the ray walked when looking for a drone target ((safestDirection + 4) & 7)", "confidence": "high"},
    {"addr": "1F", "scope": "zp", "name": "droneScanRadius", "meaning": "Radius 4..14 along that ray; in sub_EBEF the same location is the saturating total threat weight", "confidence": "high"},
    {"addr": "20", "scope": "zp", "name": "threatUnitCount", "meaning": "Number of enemy units counted by the sub_EBEF threat scan (0 selects the 'no direction' return path at $EC7C)", "confidence": "high"},
    {"addr": "21", "scope": "zp", "name": "maxRangeSurplus", "meaning": "Largest (weaponRange - distance) found by the threat scan; returned in X and used by callers as 'something is in range of us'", "confidence": "high"},
    {"addr": "22", "scope": "zp", "name": "cellColumn", "meaning": "Map column argument of distanceSquared ($3870) and of the queued $90 strike command", "confidence": "high"},
    {"addr": "23", "scope": "zp", "name": "aiScratch23", "meaning": "Direction index of the blur loop; in runAiMissileTurn the accumulated reluctance score of the anti-drone shot", "confidence": "high"},
    {"addr": "24", "scope": "zp", "name": "cellRow", "meaning": "Map row argument of distanceSquared and of the queued $90 strike command", "confidence": "high"},
    {"addr": "27", "scope": "zp", "name": "centreColumn", "meaning": "Column of the reference point the threat scan and sub_E66C work around (usually the AI comcen or the current objective)", "confidence": "high"},
    {"addr": "28", "scope": "zp", "name": "centreRow", "meaning": "Row of that reference point", "confidence": "high"},
    {"addr": "29", "scope": "zp", "name": "missileReloadValue", "meaning": "Half the range to the drone; loaded into $8970 as the AI's missile reload delay", "confidence": "medium"},
    {"addr": "2B", "scope": "zp", "name": "comcenStunnedPenalty", "meaning": "$00 normally, $FF when the AI comcen is stunned ($F9C3 & $0F != 0); its bit 7 adds one to the anti-drone reluctance score", "confidence": "medium"},
    {"addr": "57", "scope": "zp", "name": "gameRandomLow", "meaning": "Low byte of the lock-step game RNG state $57-$59 (nextGameRandom $FD5D); BIT zp_57 at $ECF3 breaks ties between equally quiet directions", "confidence": "high"},
    {"addr": "92", "scope": "zp", "name": "lineStartRow", "meaning": "Line-stepper start row (initLineStepper $FBB8 convention: $92/$93 = start row/column, $94/$95 = end row/column)", "confidence": "high"},
    {"addr": "93", "scope": "zp", "name": "lineStartColumn", "meaning": "Line-stepper start column", "confidence": "high"},
    {"addr": "94", "scope": "zp", "name": "lineEndRow", "meaning": "Line-stepper end row", "confidence": "high"},
    {"addr": "95", "scope": "zp", "name": "lineEndColumn", "meaning": "Line-stepper end column", "confidence": "high"},
    {"addr": "0400", "scope": "abs", "name": "threatRingScratch", "meaning": "8 bytes of the $0400 page (the start-up copy-protection answer table, free once the game runs and cleared every round by finishRound) used as the unsmoothed copy of the direction ring during the blur", "confidence": "high"},
    {"addr": "8800", "scope": "abs", "name": "aiOrderClockStamp", "meaning": "50-byte array $8800-$8831 indexed by AI unit index - 50: the value of the game clock $91CB when that unit last received an objective. Read as $87CE,unit with the absolute unit index. chooseAiDroneTarget re-stamps entries 0-45 when it commits to an all-out attack. Overlays the pre-game message text 'GET OPPONENT ON PHONE AND...', which is dead once a battle starts", "confidence": "medium"},
    {"addr": "8832", "scope": "abs", "name": "aiUnitObjective", "meaning": "50-byte array $8832-$8863 indexed by AI unit index - 50 giving the objective id assigned to that unit; the same bytes are addressed as $8800,unit with the absolute index 50-99 elsewhere in the module", "confidence": "medium"},
    {"addr": "8896", "scope": "abs", "name": "threatByDirection", "meaning": "8-byte ring of accumulated enemy pressure per compass direction, filled by sub_EBEF and smoothed in place at $ECC5; the same 8 bytes double as the AI's command staging buffer in sub_EA46", "confidence": "high"},
    {"addr": "88EA", "scope": "abs", "name": "objectiveColumn", "meaning": "Map column of each AI objective (12-entry table, parallel with $88D2/$88DE/$88F6/$8902/$890E/$891A/$8926/$8932/$893E)", "confidence": "medium"},
    {"addr": "88F6", "scope": "abs", "name": "objectiveRow", "meaning": "Map row of each AI objective", "confidence": "medium"},
    {"addr": "8902", "scope": "abs", "name": "objectiveWeight", "meaning": "Per-objective weight/threat threshold; summed in chooseAiDroneTarget and copied into $8956 as the threat threshold of the current objective", "confidence": "medium"},
    {"addr": "893E", "scope": "abs", "name": "objectiveFlags", "meaning": "Per-objective flag byte; bit 5 doubles its weight and bit 6 marks it committed. chooseAiDroneTarget ORs bit 6 into every entry when it launches the all-out attack", "confidence": "medium"},
    {"addr": "8953", "scope": "abs", "name": "objectiveCountMinus1", "meaning": "Index of the last AI objective (set by the track 29 $EC00 setup overlay)", "confidence": "medium"},
    {"addr": "8954", "scope": "abs", "name": "objectiveCount", "meaning": "Number of AI objectives ($8953 + 1)", "confidence": "medium"},
    {"addr": "8956", "scope": "abs", "name": "objectiveThreatThreshold", "meaning": "Threat level the sub_EBEF scan must reach before the AI reacts around the current objective; default 3", "confidence": "medium"},
    {"addr": "895D", "scope": "abs", "name": "currentObjective", "meaning": "Index of the objective the AI is currently working on, bit 7 set = none", "confidence": "medium"},
    {"addr": "8966", "scope": "abs", "name": "aiActionBudget", "meaning": "Reset to 0 at $EA7D each exchange and decremented by every AI action; once negative no further orders are generated and the queued-command flush at $EA83 is skipped", "confidence": "medium"},
    {"addr": "8968", "scope": "abs", "name": "aiPacketCostCounter", "meaning": "Accumulated cost of the AI's activity this round (each missile strike adds 2); compared with 5 at $E205", "confidence": "medium"},
    {"addr": "896F", "scope": "abs", "name": "lastDroneHeading", "meaning": "Copy of droneHeading $920B taken at the end of every AI missile turn; if the drone has turned since then the anti-drone shot gets a $40 accuracy penalty. Also overwritten with the heading byte the AI puts in its own $8D launch command", "confidence": "medium"},
    {"addr": "8970", "scope": "abs", "name": "missileReloadTimer", "meaning": "Counts down once per exchange; while it is not negative the AI may not fire another missile", "confidence": "high"},
    {"addr": "8972", "scope": "abs", "name": "missileTargetUnit", "meaning": "Unit index the AI is currently shooting missiles at, $FF = none", "confidence": "medium"},
    {"addr": "8973", "scope": "abs", "name": "aiDroneStepsLeft", "meaning": "Set to $64 (100) when the AI launches a drone; the track 29 $EE00 drone overlay counts it down and emits $8F (drone released) when it runs out", "confidence": "high"},
    {"addr": "8974", "scope": "abs", "name": "aiDroneColumn", "meaning": "Current/launch map column of the AI's drone (initialised to the AI comcen column)", "confidence": "medium"},
    {"addr": "8975", "scope": "abs", "name": "aiDroneRow", "meaning": "Current/launch map row of the AI's drone", "confidence": "medium"},
    {"addr": "8976", "scope": "abs", "name": "aiDroneTargetUnit", "meaning": "Unit index the AI's next drone is aimed at, $FF = none. Set by chooseAiDroneTarget, consumed by launchAiDrone", "confidence": "high"},
    {"addr": "8977", "scope": "abs", "name": "aiDroneOverlayState", "meaning": "$FF = the track 29 s4-5 drone-flying overlay is resident at $EE00; 0-$15 = countdown before the next launch is allowed (launchAiDrone requires 0 and sets 3, chooseAiDroneTarget sets $15). Ticked down at $E0E1", "confidence": "medium"},
    {"addr": "8979", "scope": "abs", "name": "droneTargetValueThreshold", "meaning": "Minimum 2x2 cluster value a drone target must reach; multiplied by 0.75 every time a target is accepted, so the AI becomes less selective as the game goes on. Initialised by the track 29 $EC00 setup overlay ($EC3F/$ECCF there)", "confidence": "medium"},
    {"addr": "897C", "scope": "abs", "name": "aiAggressionFlag", "meaning": "Bit 7 read at $EC40 (adds one to every unit's threat weight when clear) and at $E1F3; paired with $897B in the comcen-exposure code at $E106", "confidence": "low"},
    {"addr": "91CB", "scope": "abs", "name": "gameClock", "meaning": "Rounds remaining, counts down; stamped into aiOrderClockStamp", "confidence": "high"},
    {"addr": "920B", "scope": "abs", "name": "droneHeading", "meaning": "Current heading 0-7 of the drone under local control", "confidence": "high"},
    {"addr": "920D", "scope": "abs", "name": "droneStateFlags", "meaning": "Drone state: bit 0 a drone move command is in flight, bit 6 the drone is being steered locally, bit 7 a drone is active", "confidence": "high"},
    {"addr": "92AE", "scope": "abs", "name": "dronesLeftOwn", "meaning": "Drones the local (human) side has left; the AI uses 4x and 2x this value as a missile-conservation term", "confidence": "high"},
    {"addr": "92AF", "scope": "abs", "name": "dronesLeftOpponent", "meaning": "Drones the computer opponent has left; chooseAiDroneTarget returns immediately when it is 0", "confidence": "high"},
    {"addr": "92B1", "scope": "abs", "name": "missilesLeftOpponent", "meaning": "Missiles the computer opponent has left", "confidence": "high"},
    {"addr": "92F6", "scope": "abs", "name": "droneViewOriginCol", "meaning": "Drone camera window origin column; the drone's own cell is this + 6", "confidence": "high"},
    {"addr": "92F7", "scope": "abs", "name": "droneViewOriginRow", "meaning": "Drone camera window origin row; the drone's own cell is this + 5", "confidence": "high"},
    {"addr": "9236", "scope": "abs", "name": "comcenUnit", "meaning": "Unit index of the comcen the radar grid is centred on; isUnitOnAiRadar flips it to $63 and back to $31", "confidence": "high"},
    {"addr": "E01E", "scope": "abs", "name": "incomingPacketLength", "meaning": "Number of command bytes in the packet the engine believes it received from the opponent (read by mergeIncomingPacket at $4F05/$4F58); in this build the AI writes it directly", "confidence": "high"},
    {"addr": "E020", "scope": "abs", "name": "incomingPacketBuffer", "meaning": "Command bytes of the opponent's packet ($4F14/$4F33/$4F47, cmdDroneMove $52C8); fabricated here by the AI", "confidence": "high"},
    {"addr": "F640", "scope": "abs", "name": "unitColumn", "meaning": "100-entry array of unit map columns; $F6A3 is unit 99 (the AI comcen)", "confidence": "high"},
    {"addr": "F6A4", "scope": "abs", "name": "unitRow", "meaning": "100-entry array of unit map rows; $F707 is unit 99", "confidence": "high"},
    {"addr": "F76C", "scope": "abs", "name": "unitTypeFlags", "meaning": "100-entry array of unit type (bits 0-2) and flags; bit 7 set = unit dead / not in play", "confidence": "high"},
    {"addr": "F9C3", "scope": "abs", "name": "comcen99StunState", "meaning": "$F960 + $63: the stun/dug-in byte of unit 99 (the AI comcen); low nibble non-zero = stunned", "confidence": "medium"},
    {"addr": "F9F5", "scope": "abs", "name": "comcen49Visibility", "meaning": "$F9C4 + $31: the energy/visibility byte of unit 49 (the human comcen); bit 7 set = currently visible (set by ageUnitVisibility $3576)", "confidence": "high"},
    {"addr": "FA27", "scope": "abs", "name": "comcen99Visibility", "meaning": "$F9C4 + $63: the energy/visibility byte of unit 99 (the AI comcen); bits 0-1 are used as a small jitter in the anti-drone decision", "confidence": "medium"}
  ],
  "dataBlocks": [
    {"addr": "EFCD", "length": 3, "type": "text", "name": "strayTextTail", "description": "$43,$45,$AE = 'CE' plus '.'|$80. Stale tail of the string 'INSERT DATA DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the T34 build of this overlay slot; here the same bytes are preceded by code, so only the last three characters survive. Unreferenced."},
    {"addr": "EFD0", "length": 18, "type": "text", "name": "msgDatagamecustomOff2", "description": "'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."},
    {"addr": "EFE2", "length": 11, "type": "text", "name": "msgDestroyOff2", "description": "'DESTROY OFF' with bit 7 on the final F. Same shared filler block; unreferenced."},
    {"addr": "EFED", "length": 8, "type": "text", "name": "msgAccepted2", "description": "'ACCEPTED' with bit 7 on the final D. Same shared filler block; unreferenced."},
    {"addr": "EFF5", "length": 11, "type": "unknown", "name": "overlayTailFiller", "description": "$C0,$DF then $00,$FF repeated to the end of the page. Identical in all four builds of the $EC00/$EE00 overlay slot except for the very last byte ($FF here and in the track 29 variants, $00 in ovl_E000_T34). Looks like leftover buffer content from the mastering system; unreferenced."}
  ],
  "misclassified": [],
  "insights": [
    "This build of the $E000 module is the SOLO TRAINER, not a modem driver. loadCommModule ($0F16) picks build 1 (track 35) when $0BA5 bit 7 is set, and everywhere the main program tests that flag it skips the modem behaviour ('PICK UP PHONE THEN PRESS SPACE.' at $1B64, 'WAITING FOR CONNECTION...' at $1B77, carrier handling). The jump-table entry sub_E000 with X=1, which sendOutgoingPacket ($562B) uses to transmit, lands at $E082 and runs a complete computer opponent that fabricates the reply packet in $E020/$E01E. So $E020/$E01E = incoming packet buffer + length, $E027/$E01F = outgoing packet buffer + length.",
    "The AI always plays side 1: it works on unit 99 ($F6A3/$F707/$FA27/$F9C3) as its own comcen and unit 49 ($F9F5) as the enemy comcen, scans enemy units 0-45 in the threat pass, and isUnitOnAiRadar restores comcenUnit $9236 to $31 unconditionally. Every coordinate it puts in a packet goes through mirrorMapCoordinate ($3FA3) - and sub_EA38 additionally adds 1 first, pre-compensating the mirror-and-decrement that cmdDroneMove applies to remote $8D parameters.",
    "$EE00-$EFFF is a third overlay layer inside the module. The default content is track 35 s14-15 (the missile/target code in this chunk); $E0CD-$E0D9 swaps in track 29 s4-5 (disassembly/game/ovl_EE00_T29.s, the drone-flying AI) while the AI's drone is airborne and $E0AB-$E0B6 reloads track 35 s14-15 afterwards. $8977 = $FF marks the drone overlay being resident. The 'jsr sub_EE00' at $E0A5 therefore never enters the code at $EE00 in this file - that address is only a fall-through label of runAiMissileTurn here.",
    "disassembly/XREF.txt merges references from ALL builds that share an address range, so almost every caller it lists for $EC82-$EFFD is wrong for this file. sub_EC91/sub_ECD4/sub_ECF3/sub_ED43/sub_EE4A/sub_EEB9/sub_EEDD and the labels msgBeginnerDisplaySet($EF28)/msgMapType($EF77)/D_EFB6 come from ovl_E000_T34 or ovl_EC00_T29 (the $EC00 slot the setup overlay A works with); in ovl_E000_T35 those addresses are mid-instruction or plain loop heads. The only genuine entries into this chunk are $ED0B (from $EA80), $EE2D, $EE55, $EE6A, $EE85 (all internal) and $EEE0 (from $E1FF); ovl_6F00_B and main_0800 never call into $EC00-$EFFF.",
    "Threat model used by the AI (sub_EBEF, whose tail is this chunk): for every enemy unit within 20 cells of a reference point it adds a weight to the compass direction the unit lies in - +1 base (unless $897C bit 7), +1 if closer than 10 cells, +4 if the unit's weapon range (5, or 9 for types with bit 1 set, i.e. BOOMER/SPY) covers the point. The 8-direction ring is then blurred 1-2-1 circularly and the calmest 3-direction arc is picked. Return values: A = overall threat, X = largest in-range surplus, Y = safest direction (8 = nothing there, which is exactly the null entry of the direction delta tables $E68A/$E693). Callers use (Y+4)&7 to get the direction the enemy is coming from.",
    "Direction encoding used throughout the module: index 0-7 clockwise from north, column delta table at $E68A = 0,+1,+1,+1,0,-1,-1,-1 and row delta table at $E693 = -1,-1,0,+1,+1,+1,0,-1, both with a 9th entry 0 so index 8 means 'nowhere'. sub_E80E returns this index in Y (A holds the row delta) and sub_E66C(X = direction, Y = distance) returns the clipped cell in zp_18/zp_19 with C = 1 when it fell off the 40x40 map.",
    "The AI's per-unit bookkeeping lives in the tail of the $8800 message block, which is dead once a battle starts: $8800-$8831 = order time stamp (game clock) per AI unit, indexed by unitIndex-50 (also addressed as $87CE,absoluteUnitIndex), and $8832-$8863 = objective id per AI unit (also addressed as $8800,absoluteUnitIndex). $8896-$889D is the 8-direction threat ring, and the AI parameter block $8952-$897C is initialised by the track 29 $EC00 sub-overlay ($EC1C-$ECD2 there) before the real $EC00 code is loaded over it at $0B4F.",
    "Missile behaviour is deliberately imperfect: an anti-drone shot compares nextGameRandom against ((reluctance+1)<<5) + $40 if the drone changed heading; on failure the impact point is scattered by randomlySignValue, on success the AI also queues command $92 so the engine treats the drone as destroyed. Missiles are conserved with an explicit rule - the AI will not shoot unless missilesLeft exceeds 2x (or 4x, for the anti-drone case) the number of drones the human still has."
  ]
}
