{ "chunk": "game_main_0800_32DA", "unit": "game/main_0800", "routines": [ { "addr": "32EA", "name": "hideSprite1", "summary": "Clears bit 1 of the sprite-enable shadow ($9022) and writes the result straight to VIC_SPR_ENA, turning sprite 1 off immediately (sprite 1 is the unit-selection sprite drawn by sub_3CF2). Called from the battlefield cursor / selection code ($229F, $22F4, $2359, $253C, $2540, $3D4E).", "inputs": "spriteEnableShadow $9022", "outputs": "$9022 and $D015 with bit 1 cleared; A clobbered", "confidence": "high" }, { "addr": "3302", "name": "showCrosshairSprite", "summary": "Loads the 63-byte diamond/crosshair shape ($9EE3-$9F21, offset X=$7D into the $9EA4 shape pair) into sprite 2 data ($8A80) and falls into the shared body at $3308 which makes sprite 2 black, double-size, positioned at X=256 (MSB set, $9012=0) Y=$4D - the centre of the 7x5 battlefield view - and enables it. Used when a target/attack cursor is active ($21AB).", "inputs": "none", "outputs": "sprite 2 data, $900A=0, $9023/$9024 bit 2 set, $9012=0, $9020 bit 2 set, $901A=$4D, sprite 2 enabled via sub_31A3", "confidence": "medium" }, { "addr": "3306", "name": "showCursorBoxSprite", "summary": "Same as showCrosshairSprite but copies the square box shape ($9EA4-$9EE2, offset X=$3E) - the normal battlefield centre cursor - into sprite 2 and shows it black and expanded at (256,$4D). Called when the battlefield is (re)entered ($1FEB, $2260) and from $4C56.", "inputs": "none", "outputs": "sprite 2 data and VIC shadow registers as for showCrosshairSprite", "confidence": "medium" }, { "addr": "3340", "name": "loadDirectionShapeSprite", "summary": "Copies the 63-byte sprite shape number A (0-7, a facing/direction) from the decompressed shape buffer at $0200+64*A into sprite 0 data ($8A00) when activeSpriteIndex $91CC is 0, otherwise into sprite 5 data ($8B40), and then enables sprite $91CC. The buffer is filled by the RLE decompressor sub_67CB from the compressed sets at $FD70/$FE24. Callers pass the direction byte $920B ($5322, overlay B $802A/$810B/$829E).", "inputs": "A = shape index 0-7, $91CC = target sprite (0 or 5)", "outputs": "$8A00 or $8B40 sprite data, sprite enabled; zp_98/zp_99 = source pointer (clobbered), X/Y clobbered", "confidence": "medium" }, { "addr": "3383", "name": "showComcenCrossMarker", "summary": "Clears sprite 2 (sub_2FB2 A=2), copies the 21-byte cross/plus shape at $9F22 into rows 7-13 of sprite 2 ($8A95), colours it light red ($900A=$0A), positions it at grid cell (10,8) converted by gridCellToSpriteCoords (the comcen cell of overlay B's 21x17 strategic grid) and enables sprite 2. Called by overlay B at $75B6 when screen 1 is built.", "inputs": "none", "outputs": "sprite 2 data/colour/position, zp_67=10, zp_68=8, sprite 2 enabled", "confidence": "medium" }, { "addr": "33B1", "name": "gridCellToSpriteCoords", "summary": "Converts an 8x8-pixel grid cell (X = column, Y = row) of the overlay B strategic map into sprite coordinates: X = X*8+$27, Y = Y*8+$43.", "inputs": "X = grid column, Y = grid row", "outputs": "X = sprite X, Y = sprite Y", "confidence": "high" }, { "addr": "340B", "name": "getUnitSpriteCentreOnGrid", "summary": "Calls overlay B's sub_7A13 (map cell X/Y -> grid cell relative to the comcen, C=1 when outside the 21x17 grid); if inside, converts the cell with gridCellToSpriteCoords and adds 4 to both so the result is the cell centre. Returns C=0 on success, C=1 when off-grid.", "inputs": "X = map column, Y = map row (passed to sub_7A13)", "outputs": "C = 0 and X/Y = sprite coordinates of the cell centre, or C = 1", "confidence": "medium" }, { "addr": "341F", "name": "updateMarkerSprite", "summary": "If the countdown $923E is nonzero and the marker cell (immediate operands at $3425/$3437, column 0 / row $FF in this build) lies within 14 columns and 12 rows of the view origin, positions sprite 4 at X=col*8+$78, Y=row*8+$5C and enables it; otherwise disables sprite 4. Nothing in this build sets $923E or patches the operands (only the dead code at $33C8), so in practice it only clears sprite 4. Called by overlay B's screen loop at $827E.", "inputs": "$923E, zp_A5/zp_A6 view origin, patched operands $3425/$3437", "outputs": "$9014/$901C sprite 4 position, sprite 4 enable bit set or cleared", "confidence": "medium" }, { "addr": "3456", "name": "drawComcenRingSprite", "summary": "Disables sprite 4, sets spritePlotMode $91F3=$40 (OR pixels), clears sprite 4 data (sub_2FB2 A=4) and draws two concentric circles of radius 2 and 1 into it with sub_3239 (centre (12,10)), then gives sprite 4 the flashing colour pair $12. Overlay B calls it at $80E1 right after centring the view on the comcen, so this is the comcen marker of screen 2.", "inputs": "none", "outputs": "sprite 4 data, $900C=$12, $91CC=4, $91F3=$40; self-modifies the loop counter at $346E", "confidence": "medium" }, { "addr": "347F", "name": "orderSelectedUnitToCursor", "summary": "Builds a move order for the selected unit ($90F8): cmdParam1/2 (zp_74/zp_75) = cursor cell (viewOriginCol+3, viewOriginRow+2), clears bit 6 of the unit's status flags ($FB54), sends command $80 unit,col,row through sub_560A, then calls updateUnitAtDestinationFlag. Returns C=0. Fire-button handler of the battlefield ($2212).", "inputs": "$90F8 selected unit, zp_A5/zp_A6 view origin", "outputs": "zp_73/74/75 command parameters, $FB54[unit] bits 6/7, 4 bytes queued in the outgoing command ring, C=0", "confidence": "medium" }, { "addr": "34A4", "name": "updateUnitAtDestinationFlag", "summary": "Sets bit 7 of unitStatusFlags ($FB54) for unit zp_73 when the unit's map position ($F640/$F6A4) equals the ordered destination in zp_74/zp_75, clears it otherwise. Also called from the command executors at $4A4D and $4C10.", "inputs": "zp_73 = unit, zp_74 = destination column, zp_75 = destination row", "outputs": "$FB54[unit] bit 7; self-modifies the ORA operand at $34C1", "confidence": "medium" }, { "addr": "3568", "name": "markAllUnitsVisible", "summary": "Sets bits 6 and 7 (spotted this cycle + visible) in unitEnergy ($F9C4) for all 100 units, i.e. reveals every unit. Called at the end of the reset routine at $4018 and by ageUnitVisibility when no game is in progress.", "inputs": "none", "outputs": "$F9C4[0..99] |= $C0; Y = $FF", "confidence": "high" }, { "addr": "3576", "name": "ageUnitVisibility", "summary": "If noGameInProgress ($0BA8) is nonzero, jumps to markAllUnitsVisible. Otherwise sets the unit-scan side filter to 'all units' (compare operand $37FF=0, opcode $3800=BCC) and for every unit copies energy bit 6 into bit 7 and clears bit 6: units spotted during the last scan cycle stay visible, everything else fades. Called once per unit-processing cycle at $4660, before the spotting passes.", "inputs": "$0BA8, $F9C4[]", "outputs": "$F9C4[] visibility bits, $37FF/$3800 scan filter", "confidence": "high" }, { "addr": "35C3", "name": "scanVisibleEnemies", "summary": "Builds the list of enemy units that unit Y can see (X bit 7 clear = spotting pass) or shoot at (X bit 7 set = attack-target pass) into scanUnitList $0400 / scanDistSqList $0432 / scanDirectionList $0464. Range = sightRangeByType[type (+5 for attack)] + terrainSightBonus[terrain class under the unit] (doubled for spotting), capped at 10; collectUnitsInRadius gathers opposite-side units in the square, then each candidate is rejected ($0400[n]=$FF) if its squared distance exceeds rangeDistanceSq[range] (+1 penalty when not in the preferred direction zp_6B), if it is destroyed, if (attack pass) it is not yet visible, if (spotting) it is a spy seen by a non-spy or farther than dist2 20, or a 'hidden' unit ($F898 bit 7) farther than dist2 12 (6 for non-spies). Grunts/Riders (and all spotting) then need a clear line of sight (checkLineOfSightStep over each cell, enemy units block, friendly units block only from flat ground, spies are transparent, terrain class 2 blocks, a class-2 target farther than dist2 6 is hidden unless already spotted); Boomers/spies/comcen skip the LOS test when attacking. First sighting of an enemy spy/comcen queues status message $0F/$0D.", "inputs": "X = mode (bit 7: 0 spotting, 1 attack targets), Y = scanning unit; unit tables, map, $077E terrain classes", "outputs": "$0400/$0432/$0464 list with zp_2F = last index ($FF none), $91F1 = range, $9227/$9228/$9229 = unit terrain cell/type/height, zp_18-zp_29 scratch; self-modifies $35F9, $362B", "confidence": "medium" }, { "addr": "3746", "name": "checkLineOfSightStep", "summary": "Feeds one more terrain class X into the running line-of-sight test: compares terrainHeight[X] with the previous height zp_1E; a rise is remembered in zp_1F, a fall after a rise means a ridge hides the target and returns C=1. Equal heights or falls while no rise has happened yet (looking down from the start) return C=0.", "inputs": "X = terrain class of the next cell, zp_1E = previous height, zp_1F = $80 until the line went uphill", "outputs": "C = 1 blocked / 0 clear, zp_1E updated, zp_1F = rise amount", "confidence": "high" }, { "addr": "375D", "name": "initLineBetweenUnits", "summary": "Sets the line-walker target (zp_95/zp_94) to unit Y's position and falls into initLineFromUnit with X as the start unit.", "inputs": "X = start unit, Y = end unit", "outputs": "see initLineFromUnit", "confidence": "high" }, { "addr": "3767", "name": "initLineFromUnit", "summary": "Sets the line-walker start (zp_93/zp_92) to unit X's position, calls initLineStepper ($FBB8) and returns the principal direction of the line as 0 = north, 1 = south (row-major lines, by zp_96) or 2 = east, 3 = west (column-major lines, by zp_97). The value becomes the unit's facing (bits 0-1 of $F708) at $4464. Also called from $428E.", "inputs": "X = unit, zp_94/zp_95 = target row/column", "outputs": "A = direction code 0-3, line walker state zp_8C-zp_97", "confidence": "high" }, { "addr": "378B", "name": "setScanFilterOppositeSide", "summary": "Configures collectUnitsInRadius to keep only units of the side opposite to unit Y: compare operand $37FF = $32 and branch opcode $3800 = BCC ($90) when Y < 50 (side 0 scanner keeps units >= 50) or BCS ($B0) when Y >= 50.", "inputs": "Y = unit index", "outputs": "$37FF, $3800 patched; A clobbered", "confidence": "high" }, { "addr": "37AA", "name": "collectUnitsInRadius", "summary": "Scans the map square of half-width A around cell (X,Y), clipped to 0..39, for cells holding a unit (bit 7 set, low bits = unit index) that passes the side filter at $37FE/$3800, storing the unit index in $0400[n] and its squared distance (distanceSquared) in $0432[n]. When more than one unit was found and playerSide ($0B9F) is nonzero the list is reversed, so both players get mirror-symmetric tie breaking. Returns X = last index ($FF when empty).", "inputs": "A = radius, X = centre column, Y = centre row, $0B9F, map via sub_25FA", "outputs": "$0400/$0432 lists, X = zp_29 = count-1, zp_22-zp_29 scratch, zp_48/zp_49 map pointer; self-modifies $385A", "confidence": "high" }, { "addr": "3870", "name": "distanceSquared", "summary": "Returns |zp_22-zp_27|^2 + |zp_24-zp_28|^2 using squaresTable (each delta capped at 15, sum saturating at $FF).", "inputs": "zp_22/zp_24 = cell column/row, zp_27/zp_28 = centre column/row", "outputs": "A = squared distance (max $FF), X clobbered", "confidence": "high" }, { "addr": "38C2", "name": "resolveAttack", "summary": "Resolves an attack of unit X on unit Y. Only Grunts/Riders/Boomers (types 0-2) attack. Damage = baseDamageTable[attacker type][target type]; a moving attacker ($F8FC bit 7) whose first-strike flag ($F708 bit 7) is set sacrifices up to 15 energy (5 for Boomers) once: the amount is added to the damage and 3/4 of it is taken from the attacker. +2 if the attacker has $F76C bit 6, -25% if the target has it; + height difference of the terrain (terrainHeightTable); +10 if the target stands on terrain class 1, +2 on class 3 or 5; for non-Boomers a facing comparison adds +20 when the target faces the same way (hit from behind) and +10 for a flank hit. A zero/negative result becomes 2; half of the total is applied by applyHitToUnit. On the battlefield screen (and comcen not knocked out) the shot is drawn by overlay B's sub_82BF and drawShotLineSprite. Finally plays sound 9 if the target died, else 11 for a Boomer shot or 10. Called from the unit-processing loop at $44A0.", "inputs": "X = attacker unit, Y = target unit; unit tables, $077E terrain classes, $90FB, $922A", "outputs": "$38BB-$38C1 scratch, $923D attacker type, target energy/status updated, sprites 3 and zoom-window line, sound; self-modifies $3A4E, $393F, $3971", "confidence": "medium" }, { "addr": "3A5A", "name": "drawShotLineSprite", "summary": "Shows the shot between (zp_93,zp_92) and (zp_95,zp_94) on the 4-pixel-per-cell overview map using sprite 3: disables and clears the sprite, converts both cells to sprite pixels (sub_303A), walks the Bresenham line until the remaining segment fits a 23x20 pixel box, places sprite 3 at the segment's top-left corner (+2) in white, draws the segment with sub_306C (dotted when the attacker is a Boomer, $923D = 2) plus the diamond end marker sub_308A, enables sprite 3 and sets shotLineTimer $91C8 = 8 frames.", "inputs": "zp_92-zp_95 = attacker row/col and target row/col (map cells), $923D", "outputs": "sprite 3 data ($8AC0), $9013/$901B, $900B=1, $9022 bit 3, $91C8=8; zp_48 used as step counter", "confidence": "high" }, { "addr": "3B22", "name": "applyHitToUnit", "summary": "Applies A damage points to unit Y. If the unit belongs to the local player (branch at $3B2C patched per side): a full-energy spy being hit queues message $0E, a comcen hit sets the shake counter $92C8 = 8 and plays sound 8. For any comcen: the player's own comcen gets a random system damaged (sub_3D57), the damage is halved and capped at the remaining energy, scoreDirtyCounter $9208 is bumped and 2*damage is added to the 16-bit score of the attacking side ($92A2/$92A4 indexed by target side). Then reduceUnitEnergy and recordUnitHit run; when the energy reaches 0 the unit is marked destroyed ($F76C bit 7, hit effect $85, redraw) - except a comcen, which stays but ends the game: $0BA8 |= $80 | ($20 or $40 by side) and sub_4027 is entered with X = 1 if the opponent's comcen fell, 0 for the local one.", "inputs": "A = damage, Y = target unit", "outputs": "$9240 = applied damage, $F9C4/$FA28/$FA8C/$FB54/$F76C of the target, $9208, $92A2-$92A5, $92C8, $0BA8, sounds/messages, unit redraw via sub_48A2", "confidence": "medium" }, { "addr": "3BDF", "name": "reduceUnitEnergyBy", "summary": "Stores A into hitDamage $9240 and falls into reduceUnitEnergy. Used by resolveAttack for the first-strike energy sacrifice and by $3EF8 (1 point).", "inputs": "A = amount, Y = unit", "outputs": "see reduceUnitEnergy", "confidence": "high" }, { "addr": "3BE2", "name": "reduceUnitEnergy", "summary": "Subtracts hitDamage $9240 from the low 6 bits of unitEnergy[Y] with a floor of 0, preserving the visibility bits 6-7.", "inputs": "Y = unit, $9240", "outputs": "$F9C4[Y]; self-modifies the ORA operand at $3BF7", "confidence": "high" }, { "addr": "3BFC", "name": "recordUnitHit", "summary": "Caps the damage A at 20, stores $80|damage as the unit's hit effect ($FA8C), subtracts it from the signed suppression value $FA28 (clamped at -127 on overflow) and sets bit 1 of unitStatusFlags ($FB54) to flag the unit for a status refresh.", "inputs": "A = damage, Y = unit", "outputs": "$FA8C[Y], $FA28[Y], $FB54[Y] bit 1; self-modifies $3C0F", "confidence": "medium" }, { "addr": "3C20", "name": "orderAttackUnitAtCursor", "summary": "Reads the map cell under the battlefield cursor (viewOriginCol+3, viewOriginRow+2). If it holds a unit whose side bit ($F708 bit 5) differs from the selected unit's, sends command $87 with cmdParam1 = selected unit and cmdParam2 = target unit (sub_560F); otherwise plays the error sound 3. Fire handler at $21F2.", "inputs": "zp_A5/zp_A6, $90F8 selected unit, map, $F708", "outputs": "zp_74/zp_75, 3 command bytes queued, or sound 3", "confidence": "medium" }, { "addr": "32F6", "name": "hideSprite0Dead", "summary": "Unreferenced copy of hideSprite1 that clears bit 0 (sprite 0) instead; rendered as .byte in the listing.", "inputs": "none", "outputs": "would clear $9022/$D015 bit 0", "confidence": "high" }, { "addr": "33C8", "name": "setMarkerSpriteDead", "summary": "Unreferenced older routine (listed as .byte $33C2-$340A, with a 6-byte offset/colour table at $33C2): stores A in its own operand, and either patches the marker cell into updateMarkerSprite ($3425/$3437) and starts the 90-frame countdown $923E, or on the battlefield places sprite 4 on the overview map via sub_303A minus table offsets. Its JSR $3458 lands mid-instruction in the current build, proving it is stale.", "inputs": "A = index 0/1, X/Y = cell", "outputs": "dead code", "confidence": "medium" }, { "addr": "34C6", "name": "applyMoveOrderOld", "summary": "Unreferenced older executor of the move order (listed as .byte $34C6-$355C): for unit zp_73 clears $F708 bit 6, calls sub_4C6B, snaps a comcen's destination to itself under some condition ($92B4/$92B5 == 4), stores the clamped destination (bit 7 set when equal to the position) in $F7D0/$F834, updates $FB54 bit 7, copies the position into the low bits of $F898/$F8FC unless it is an opponent unit in a linked game ($92FE bit 7), and clears $F960 bits 5-7 and $F76C bit 6. The live version is around $3F9E-$4016.", "inputs": "zp_73/zp_74/zp_75", "outputs": "dead code", "confidence": "medium" }, { "addr": "355D", "name": "clampMapCoordOld", "summary": "Helper of applyMoveOrderOld: negative values become 0, values >= 40 become 39.", "inputs": "A = coordinate", "outputs": "A clamped to 0..39", "confidence": "high" }, { "addr": "379E", "name": "collectAllUnitsInRadiusDead", "summary": "Unreferenced alternate entry to collectUnitsInRadius (listed as .byte): saves A, sets the side filter to 'all units' ($37FF = 0, $3800 = BCC), restores A and falls into $37AA. The two bytes before it ($379C-$379D, $90 $FA) are a leftover branch that overlay A's side-patch routine still writes to ($7BDB), harmlessly.", "inputs": "A = radius, X/Y = centre", "outputs": "dead code", "confidence": "medium" }, { "addr": "3C50", "name": "assignBoomerTargetOld", "summary": "Unreferenced older handler (listed as .byte): X = zp_73, counts the Boomers before it with sub_3C5D and stores zp_75|$80 into boomerTargetTable $91DC[Y]. The live equivalent writes $91DC at $4454.", "inputs": "zp_73 = unit, zp_75 = target", "outputs": "dead code", "confidence": "medium" } ], "variables": [ { "addr": "18", "scope": "zp", "name": "tempByte18", "meaning": "scratch: list loop index in scanVisibleEnemies, energy temp in resolveAttack", "confidence": "high" }, { "addr": "1A", "scope": "zp", "name": "tempByte1A", "meaning": "scratch in resolveAttack: damage-table row offset (0/5/10 by attacker type), later the attacker's facing", "confidence": "high" }, { "addr": "1C", "scope": "zp", "name": "targetTerrainClass", "meaning": "terrain class (0-8) of the cell under the attack target", "confidence": "medium" }, { "addr": "1D", "scope": "zp", "name": "attackerTerrainClass", "meaning": "terrain class of the cell under the attacker", "confidence": "medium" }, { "addr": "1E", "scope": "zp", "name": "losHeight", "meaning": "running terrain height along the line of sight (checkLineOfSightStep); in resolveAttack the target's terrain height", "confidence": "high" }, { "addr": "1F", "scope": "zp", "name": "losRise", "meaning": "$80 until the line of sight went uphill, then the last positive height step; a fall afterwards blocks the view", "confidence": "high" }, { "addr": "20", "scope": "zp", "name": "scanMode", "meaning": "bit 7 clear = spotting pass, set = attack-target pass (X on entry to scanVisibleEnemies)", "confidence": "high" }, { "addr": "21", "scope": "zp", "name": "scanUnit", "meaning": "unit whose sight/fire range is being scanned", "confidence": "high" }, { "addr": "22", "scope": "zp", "name": "scanCol", "meaning": "collectUnitsInRadius: radius on entry, then the current map column of the scan", "confidence": "high" }, { "addr": "23", "scope": "zp", "name": "scanStartCol", "meaning": "first (clipped) map column of the scan square", "confidence": "high" }, { "addr": "24", "scope": "zp", "name": "scanRow", "meaning": "current map row of the scan square", "confidence": "high" }, { "addr": "25", "scope": "zp", "name": "scanColCount", "meaning": "number of columns to scan per row (2r+1, clipped at the left edge)", "confidence": "high" }, { "addr": "26", "scope": "zp", "name": "scanRowCount", "meaning": "number of rows left to scan", "confidence": "high" }, { "addr": "27", "scope": "zp", "name": "scanCentreCol", "meaning": "centre column of the scan (distanceSquared reference)", "confidence": "high" }, { "addr": "28", "scope": "zp", "name": "scanCentreRow", "meaning": "centre row of the scan", "confidence": "high" }, { "addr": "29", "scope": "zp", "name": "scanListIndex", "meaning": "index of the last entry written to scanUnitList ($FF = empty)", "confidence": "high" }, { "addr": "2F", "scope": "zp", "name": "scanListLast", "meaning": "copy of the last list index returned by collectUnitsInRadius; read by the callers of scanVisibleEnemies ($43AD, $43DF, $3CA0)", "confidence": "high" }, { "addr": "48", "scope": "zp", "name": "mapCellPtr", "meaning": "pointer (lo) to the current map cell set by sub_25FA and advanced by 40 per row in collectUnitsInRadius (same zp as fillPtr); drawShotLineSprite reuses it as a step counter", "confidence": "high" }, { "addr": "49", "scope": "zp", "name": "mapCellPtrHi", "meaning": "map cell pointer (hi)", "confidence": "high" }, { "addr": "6B", "scope": "zp", "name": "targetDirection", "meaning": "direction code (0 N, 1 S, 2 E, 3 W) of the unit's current target; candidates in another direction get +1 distance in scanVisibleEnemies; written from scanDirectionList by the target pickers ($3CD3, $3CEE, $4420, $4446) and copied into $F708 bits 0-1 (facing) at $4464", "confidence": "medium" }, { "addr": "73", "scope": "zp", "name": "cmdParam0", "meaning": "first parameter of an outgoing command (unit index)", "confidence": "high" }, { "addr": "74", "scope": "zp", "name": "cmdParam1", "meaning": "second command parameter (destination column / attacker unit)", "confidence": "high" }, { "addr": "75", "scope": "zp", "name": "cmdParam2", "meaning": "third command parameter (destination row / target unit)", "confidence": "high" }, { "addr": "92", "scope": "zp", "name": "lineCurRow", "meaning": "line walker current row (start row on entry); also the cursor row", "confidence": "high" }, { "addr": "93", "scope": "zp", "name": "lineCurCol", "meaning": "line walker current column", "confidence": "high" }, { "addr": "94", "scope": "zp", "name": "lineEndRow", "meaning": "line walker target row", "confidence": "high" }, { "addr": "95", "scope": "zp", "name": "lineEndCol", "meaning": "line walker target column", "confidence": "high" }, { "addr": "96", "scope": "zp", "name": "lineStepRow", "meaning": "row step of the line (+1/-1); sign gives north/south in initLineFromUnit", "confidence": "high" }, { "addr": "97", "scope": "zp", "name": "lineStepCol", "meaning": "column step of the line (+1/-1); sign gives east/west", "confidence": "high" }, { "addr": "98", "scope": "zp", "name": "spritePlotPtr", "meaning": "pointer (lo) into sprite data used by the sprite pixel plotter sub_31AF; loadDirectionShapeSprite uses it as the copy source pointer", "confidence": "high" }, { "addr": "99", "scope": "zp", "name": "spritePlotPtrHi", "meaning": "sprite data pointer (hi)", "confidence": "high" }, { "addr": "A5", "scope": "zp", "name": "viewOriginCol", "meaning": "map column of the left edge of the battlefield view; cursor cell = +3", "confidence": "high" }, { "addr": "A6", "scope": "zp", "name": "viewOriginRow", "meaning": "map row of the top edge of the battlefield view; cursor cell = +2", "confidence": "high" }, { "addr": "B1", "scope": "zp", "name": "gamePhase", "meaning": "0 during battle; nonzero in setup/menu phases (units do not scan or attack)", "confidence": "medium" }, { "addr": "0200", "scope": "abs", "name": "spriteShapeBuffer", "meaning": "$0200-$03FF: 8 x 64-byte sprite shapes produced by the RLE decompressor sub_67CB (sources $FD70, $FE24 in overlay B, unit pictures $CD52.. for sub_CD0A); loadDirectionShapeSprite reads shape A from $0200+64*A. Overwrites the saved setup block loaded at boot.", "confidence": "medium" }, { "addr": "0400", "scope": "abs", "name": "scanUnitList", "meaning": "up to 50 unit indices found by collectUnitsInRadius ($FF = entry rejected); reuses the copy-protection table area", "confidence": "high" }, { "addr": "0432", "scope": "abs", "name": "scanDistSqList", "meaning": "squared distance of each scanUnitList entry from the scanning unit", "confidence": "high" }, { "addr": "0464", "scope": "abs", "name": "scanDirectionList", "meaning": "principal direction code (0 N,1 S,2 E,3 W) from the scanning unit to each list entry", "confidence": "high" }, { "addr": "077E", "scope": "abs", "name": "terrainClassTable", "meaning": "128-entry table indexed by map cell value (0-$7F): terrain class 0-8 (blank $40 -> 0, $41-$54 -> 5, $55-$5C -> 1, $5D-$60 -> 2, $61-$64 -> 4, $65-$66 -> 6, $67/$6C -> 7, $68-$6B/$6F-$72 -> 8, $6D -> 3, $6E -> 5); the class indexes terrainHeightTable and terrainSightBonusTable", "confidence": "medium" }, { "addr": "0B9F", "scope": "abs", "name": "playerSide", "meaning": "local player's side 0/1; nonzero reverses the scan list order; overlay A uses it to patch the side-test branches in this chunk", "confidence": "high" }, { "addr": "0BA8", "scope": "abs", "name": "noGameInProgress", "meaning": "nonzero when no game runs; applyHitToUnit ORs in $80 | $20 (side-0 comcen destroyed) or $80 | $40 (side-1 comcen destroyed) when a comcen dies", "confidence": "medium" }, { "addr": "8A00", "scope": "abs", "name": "sprite0Data", "meaning": "sprite 0 shape (64 bytes); sprite n data lives at $8A00+64*n (pointers $A8+n in the $8C00 screen)", "confidence": "high" }, { "addr": "8A80", "scope": "abs", "name": "sprite2Data", "meaning": "sprite 2 shape: battlefield centre cursor box / crosshair, or the comcen cross marker in overlay B", "confidence": "high" }, { "addr": "8AC0", "scope": "abs", "name": "sprite3Data", "meaning": "sprite 3 shape: shot line drawn by drawShotLineSprite on the overview map", "confidence": "high" }, { "addr": "8B40", "scope": "abs", "name": "sprite5Data", "meaning": "sprite 5 shape (direction shape when $91CC = 5; first of the 3-sprite unit picture copied by sub_CD0A)", "confidence": "medium" }, { "addr": "900A", "scope": "abs", "name": "sprite2ColorShadow", "meaning": "colour of sprite 2 (0 black cursor, $0A comcen marker)", "confidence": "high" }, { "addr": "900B", "scope": "abs", "name": "sprite3ColorShadow", "meaning": "colour of sprite 3 (1 white for the shot line)", "confidence": "high" }, { "addr": "900C", "scope": "abs", "name": "sprite4ColorShadow", "meaning": "colour of sprite 4 ($12 = flashing white/red comcen rings)", "confidence": "high" }, { "addr": "9012", "scope": "abs", "name": "sprite2XShadow", "meaning": "sprite 2 X position (low byte)", "confidence": "high" }, { "addr": "9013", "scope": "abs", "name": "sprite3XShadow", "meaning": "sprite 3 X position", "confidence": "high" }, { "addr": "9014", "scope": "abs", "name": "sprite4XShadow", "meaning": "sprite 4 X position", "confidence": "high" }, { "addr": "901A", "scope": "abs", "name": "sprite2YShadow", "meaning": "sprite 2 Y position", "confidence": "high" }, { "addr": "901B", "scope": "abs", "name": "sprite3YShadow", "meaning": "sprite 3 Y position", "confidence": "high" }, { "addr": "901C", "scope": "abs", "name": "sprite4YShadow", "meaning": "sprite 4 Y position", "confidence": "high" }, { "addr": "9020", "scope": "abs", "name": "spriteXMsbShadow", "meaning": "shadow of $D010; bit 2 set puts the cursor sprite at X=256", "confidence": "high" }, { "addr": "9022", "scope": "abs", "name": "spriteEnableShadow", "meaning": "shadow of $D015; sub_31A3 sets bits, the routines here clear bits 1/3/4 directly", "confidence": "high" }, { "addr": "9023", "scope": "abs", "name": "spriteXExpandShadow", "meaning": "shadow of $D01D (bit 2 set for the cursor sprite)", "confidence": "high" }, { "addr": "9024", "scope": "abs", "name": "spriteYExpandShadow", "meaning": "shadow of $D017", "confidence": "high" }, { "addr": "90F8", "scope": "abs", "name": "selectedUnit", "meaning": "index of the currently selected unit", "confidence": "high" }, { "addr": "90FB", "scope": "abs", "name": "currentScreenId", "meaning": "0 = battlefield (shots are drawn only then)", "confidence": "high" }, { "addr": "91C8", "scope": "abs", "name": "shotLineTimer", "meaning": "frames the shot-line sprite 3 stays visible (set to 8; $415A disables the sprite when it reaches 0)", "confidence": "high" }, { "addr": "91CC", "scope": "abs", "name": "activeSpriteIndex", "meaning": "sprite number (0-7) that the sprite plotting/copy routines (sub_31AF, sub_3239, loadDirectionShapeSprite) target; bit 7 set = plot into the $9025 scratch shape", "confidence": "medium" }, { "addr": "91DC", "scope": "abs", "name": "boomerTargetTable", "meaning": "per-Boomer ordered target unit ($80|unit, 0 = none), indexed by the Boomer's ordinal among type-2 units (sub_3C5D)", "confidence": "medium" }, { "addr": "91F1", "scope": "abs", "name": "scanRange", "meaning": "sight/fire range (0-10) computed by scanVisibleEnemies; the callers then reuse it as the best candidate distance", "confidence": "medium" }, { "addr": "91F3", "scope": "abs", "name": "spritePlotMode", "meaning": "pixel operation of sub_31AF: $00 clear (AND), $40 set (ORA), $80 toggle (EOR)", "confidence": "high" }, { "addr": "9208", "scope": "abs", "name": "scoreDirtyCounter", "meaning": "incremented whenever a side scores; $C519 redraws the score line when it differs from $91CB", "confidence": "medium" }, { "addr": "9227", "scope": "abs", "name": "scanUnitTerrain", "meaning": "map cell value under the scanning unit", "confidence": "high" }, { "addr": "9228", "scope": "abs", "name": "scanUnitType", "meaning": "type (0-4) of the scanning unit", "confidence": "high" }, { "addr": "9229", "scope": "abs", "name": "scanUnitTerrainHeight", "meaning": "terrain height under the scanning unit; nonzero lets the unit see over friendly units", "confidence": "high" }, { "addr": "922A", "scope": "abs", "name": "comcenStunStatus", "meaning": "bit 6 set while the comcen is knocked out (no sprite enabling, no shot drawing)", "confidence": "medium" }, { "addr": "9236", "scope": "abs", "name": "comcenUnit", "meaning": "index of the local player's COMCEN (49 or 99)", "confidence": "high" }, { "addr": "923D", "scope": "abs", "name": "attackerType", "meaning": "type of the attacking unit in resolveAttack (2 = Boomer selects dotted shot line and sound 11)", "confidence": "high" }, { "addr": "923E", "scope": "abs", "name": "markerTimer", "meaning": "countdown gating updateMarkerSprite; never set nonzero in this build (only by dead code)", "confidence": "medium" }, { "addr": "9240", "scope": "abs", "name": "hitDamage", "meaning": "damage currently being applied to a unit (reduceUnitEnergy/recordUnitHit/applyHitToUnit)", "confidence": "high" }, { "addr": "92A2", "scope": "abs", "name": "sideScoreLo", "meaning": "low bytes of two 16-bit scores indexed by the side whose comcen was hit (0/1): each comcen hit adds 2 x damage; read by the score display ($2906, $5845, $C6E6)", "confidence": "medium" }, { "addr": "92A4", "scope": "abs", "name": "sideScoreHi", "meaning": "high bytes of the two 16-bit scores", "confidence": "medium" }, { "addr": "92C8", "scope": "abs", "name": "spriteShakeFrames", "meaning": "set to 8 when the local comcen is hit; while nonzero the IRQ adds the fine-scroll offsets to the sprite positions (screen shake)", "confidence": "medium" }, { "addr": "F640", "scope": "abs", "name": "unitCol", "meaning": "map column of each unit (100 entries; units 0-49 side 0, 50-99 side 1, comcen = 49/99)", "confidence": "high" }, { "addr": "F6A4", "scope": "abs", "name": "unitRow", "meaning": "map row of each unit", "confidence": "high" }, { "addr": "F708", "scope": "abs", "name": "unitFlags", "meaning": "per-unit: bits 0-1 facing (0 N,1 S,2 E,3 W), bit 5 side, bit 7 first-strike bonus available (consumed by resolveAttack); bits 2-4/6 group-related", "confidence": "medium" }, { "addr": "F76C", "scope": "abs", "name": "unitType", "meaning": "per-unit: bits 0-2 type (0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN per the names at $C964), bit 6 combat modifier (+2 dealt, -25% taken), bit 7 destroyed", "confidence": "high" }, { "addr": "F7D0", "scope": "abs", "name": "unitDestCol", "meaning": "per-unit ordered destination column, bit 7 set while a move is pending", "confidence": "medium" }, { "addr": "F834", "scope": "abs", "name": "unitDestRow", "meaning": "per-unit ordered destination row (bit 7 flag)", "confidence": "medium" }, { "addr": "F898", "scope": "abs", "name": "unitAltCol", "meaning": "per-unit secondary column (bits 0-5) with flags: bit 6 toggled by MISC-menu commands $84/$85, bit 7 = unit inactive/hidden (does not scan or attack, harder to spot)", "confidence": "low" }, { "addr": "F8FC", "scope": "abs", "name": "unitAltRow", "meaning": "per-unit secondary row (bits 0-5); bit 6 toggled by commands $95/$96; bit 7 set = unit is moving (enables the first-strike energy sacrifice)", "confidence": "low" }, { "addr": "F960", "scope": "abs", "name": "unitStunTable", "meaning": "per-unit stun counter (low nibble)", "confidence": "medium" }, { "addr": "F9C4", "scope": "abs", "name": "unitEnergy", "meaning": "per-unit: bits 0-5 energy 0-50, bit 6 spotted in the current scan cycle, bit 7 visible to the local player", "confidence": "high" }, { "addr": "FA28", "scope": "abs", "name": "unitSuppression", "meaning": "per-unit signed value reduced by each hit (clamped at -127); read by the status display and movement cost code", "confidence": "low" }, { "addr": "FA8C", "scope": "abs", "name": "unitHitEffect", "meaning": "per-unit effect code: $80|damage after a hit, $85 when destroyed, 0 none", "confidence": "medium" }, { "addr": "FAF0", "scope": "abs", "name": "unitTerrainCell", "meaning": "map cell value that the unit stands on (the map itself holds $80|unit); $FF for dead units", "confidence": "high" }, { "addr": "FB54", "scope": "abs", "name": "unitStatusFlags", "meaning": "per-unit: bit 0 attacked, bit 1 hit (status refresh), bits 2-6 group/formation, bit 6 cleared by a move order, bit 7 unit is at its ordered destination", "confidence": "medium" } ], "dataBlocks": [ { "addr": "32DA", "length": 16, "type": "byteTable", "name": "circlePointBuffer", "description": "RAM: 8 X coordinates ($32DA-$32E1) and 8 Y coordinates ($32E2-$32E9) of the current octant point of a circle, filled by sub_3266 and plotted by sub_32BF" }, { "addr": "33C2", "length": 6, "type": "byteTable", "name": "markerOffsetTableDead", "description": "three 2-entry tables (X offset $0A/$08, Y offset $08/$09, colour $02/$04) used only by the dead routine at $33C8" }, { "addr": "35A6", "length": 10, "type": "byteTable", "name": "sightRangeByType", "description": "range in cells per unit type: entries 0-4 spotting (Grunt 3, Rider 3, Boomer 3, Spy 5, Comcen 3), entries 5-9 attacking (3, 3, 6, 5, 3)" }, { "addr": "35B0", "length": 11, "type": "byteTable", "name": "rangeDistanceSqTable", "description": "r*(r+1) for r = 0..10: squared-distance threshold that a target must be below to be within range r" }, { "addr": "35BB", "length": 8, "type": "byteTable", "name": "terrainSightBonusTable", "description": "extra range by terrain class 0-7 (0,0,0,1,1,2,2,0), doubled for spotting" }, { "addr": "3860", "length": 16, "type": "byteTable", "name": "squaresTable", "description": "n*n for n = 0..15 (also used by overlay A $7831 and the comm module $E832)" }, { "addr": "38A4", "length": 15, "type": "byteTable", "name": "baseDamageTable", "description": "base damage [attacker type 0-2][target type 0-4]: Grunt 4,5,25,8,12; Rider 3,4,20,6,8; Boomer 12,16,8,8,20" }, { "addr": "38B3", "length": 8, "type": "byteTable", "name": "terrainHeightTable", "description": "height/cover level by terrain class 0-7 (0,0,0,1,2,3,4,0) used for line of sight and the combat height bonus" }, { "addr": "38BB", "length": 7, "type": "byteTable", "name": "combatScratch", "description": "RAM: $38BB target unit, $38BC attacker unit, $38BD damage accumulator, $38BE/$38BF attacker row/col, $38C0/$38C1 target row/col (saved around the overlay shot drawing)" } ], "misclassified": [ { "addr": "32F6", "length": 12, "actual": "code", "evidence": "decodes as LDA $9022 / AND #$FE / STA $9022 / STA $D015 / RTS, an unreferenced twin of sub_32EA for sprite 0" }, { "addr": "33C8", "length": 67, "actual": "code", "evidence": "clean 6502 sequence (STA $33EA / CMP #0 / BNE / LDA $920D / BPL / JSR $3405 / STX $3425 / STY $3437 / JMP $341F ...) that patches the operands of sub_341F and reads $90FB; unreferenced dead code. $33C2-$33C7 remain data used by it" }, { "addr": "34C6", "length": 162, "actual": "code", "evidence": "decodes as a complete routine ($34C6-$355C, RTS at $355C) plus a clamp helper $355D-$3567 operating on $73-$75 and the unit tables; unreferenced older move-order executor" }, { "addr": "379E", "length": 12, "actual": "code", "evidence": "PHA / LDA #0 / STA $37FF / LDA #$90 / STA $3800 / PLA falling into sub_37AA: an alternate 'all units' entry of collectUnitsInRadius; $379C-$379D ($90 $FA) is a leftover branch opcode that overlay A still patches at $7BDB" }, { "addr": "3C50", "length": 13, "actual": "code", "evidence": "LDX $73 / JSR $3C5D / LDA $75 / ORA #$80 / STA $91DC,Y / RTS - unreferenced older Boomer-target handler" }, { "addr": "36D5", "length": 2, "actual": "code", "evidence": "labelled D_36D5 only because overlay A writes the branch opcode ($90/$B0 by player side) at $7BD8; likewise D_3B2C ($7BEC), D_3BD0 ($7BEF) and D_3800 (patched by sub_378B/sub_3576) are instructions" } ], "insights": [ "Address/line mismatch: the chunk spec says lines 5252-6187 but those lines hold $3040-$3625; the address range $32DA-$3C4F is at file lines 5604-6835 and that is what this survey covers.", "Unit records are 15 parallel 100-byte arrays starting at $F640 (stride $64): $F640 col, $F6A4 row, $F708 flags (facing bits 0-1, side bit 5, first-strike bit 7), $F76C type (0 GRUNT 1 RIDER 2 BOOMER 3 SPY 4 COMCEN, bit 7 destroyed), $F7D0/$F834 destination col/row (bit 7 pending), $F898/$F8FC secondary col/row with mode bits, $F960 stun, $F9C4 energy (0-50) + visibility bits 6/7, $FA28 signed suppression, $FA8C hit effect, $FAF0 terrain cell under the unit, $FB54 status flags. Units 0-49 belong to side 0 with the comcen at 49, units 50-99 to side 1 with the comcen at 99 ($0C8A table, $9236).", "The map cell of an occupied square holds $80|unitIndex; the terrain the unit covers is kept in $FAF0. Code often indexes the tables with Y = cell value by using a base 128 lower ($F688,y = $F708+unit, $F6EC,y = $F76C+unit, $FA70,y = $FAF0+unit). Terrain values map to classes 0-8 through $077E, and classes to heights through $38B3 (LOS and combat) and sight bonuses through $35BB.", "Side-dependent branches are patched at run time: overlay A's routine at $7BC6-$7BF8 writes BCC/BCS ($90/$B0 chosen by playerSide $0B9F) into the 'cpy #$32' tests at $206B, $2084, $2840, $36D5, $379C (stale), $3B2C, $3BD0, $3F9E, $4783, $5BF7, $63E2 and sets comcenUnit $9236. The unit scanner has its own filter patch ($37FF compare value, $3800 opcode) set by setScanFilterOppositeSide.", "Visibility model: each unit-processing cycle calls ageUnitVisibility (energy bit 7 := bit 6, bit 6 := 0), then every own unit runs scanVisibleEnemies in spotting mode (X=0) which sets bit 6 on everything it sees and bit 7 + redraw on newly seen units, followed by an attack-target pass (X=$80) whose best candidate (closest, comcen preferred) is attacked via resolveAttack. Sight ranges come from $35A6 (+terrain bonus, max 10) and distance tests use squared distances against r*(r+1) from $35B0; line of sight uses the Bresenham walker $FBB8/$FBFA with a ridge rule (a descent after an ascent blocks) in checkLineOfSightStep. Boomers fire at range 6 without line of sight but only at already-visible targets; spies see 5 cells and are the only units that can spot spies beyond dist2 20.", "Combat (resolveAttack/applyHitToUnit): damage = baseDamageTable[attacker][target] + optional one-time first-strike energy sacrifice (up to 15, Boomers 5) + terrain height difference + 10 on class-1 terrain (+2 on class 3/5) + facing bonus (20 from behind, 10 from the flank, 0 head-on) +/- the $F76C bit 6 modifier; half of that is applied. Comcens take half again, capped at remaining energy, and every comcen hit adds 2x damage to the attacker's 16-bit score ($92A2/$92A4 per side) and bumps $9208. A comcen reaching 0 energy ends the game: $0BA8 |= $80|$20<<(side+0/1) and sub_4027 is entered with X=1 if the opponent's comcen died, 0 if the local one did.", "Sprite allocation seen here (data at $8A00+64*n): sprite 1 = unit selection marker (hidden by sub_32EA), sprite 2 = black double-size centre cursor of the battlefield view (box $9EA4 or diamond $9EE3 at X=256,Y=$4D) and the comcen cross marker in overlay B, sprite 3 = white shot line on the 4px/cell overview map (shown 8 frames, $91C8), sprite 4 = comcen rings / vestigial marker, sprites 0/5 = direction shapes from the $0200 buffer, sprites 5-7 = unit picture via sub_CD0A. sub_31A3 is 'enable sprites in A' (suppressed while the comcen is knocked out), sub_2FB2 clears sprite A's data, sub_31AF/sub_31CA plot a pixel (X,Y) into sprite $91CC with mode $91F3, sub_3239 draws a circle of radius X from the octant tables at $9F37-$9F68.", "$0200-$03FF is reused at run time as an RLE-decompressed sprite-shape buffer (sub_67CB, escape-byte run-length format; 8 shapes of 64 bytes). Overlay B decompresses $FD70 (screen 1) and $FE24 (screen 2) from the 'unreferenced' high-memory data into it; sub_CD0A decompresses the five unit pictures at $CD52/$CDD3/$CE3B/$CECD/$CF3A and copies the first 192 bytes into sprites 5-7.", "Five unreferenced code fragments in this range are rendered as .byte: $32F6 (hide sprite 0), $33C8 (old marker-sprite setter, stale JSR into the middle of sub_3456), $34C6 (old move-order executor with clamp helper $355D), $379E (all-units scan entry), $3C50 (old Boomer target assignment). Together with the never-written $923E/$3425/$3437 they show this build kept an older, partly removed feature (a timed marker sprite).", "Self-modifying operands in this chunk: $346E loop counter, $34C1 ORA value, $3373/$3374 copy destination, $359C ORA value, $35F9 range-table index, $362B distance threshold, $37FF/$3800 scan filter, $385A reverse-loop bound, $393F sacrifice amount, $3971 damage reduction, $3A4E sound number, $3BF7 visibility bits, $3C0F hit amount, and the marker cell at $3425/$3437. Annotators should treat these as variables, not constants.", "The scan list $0400/$0432/$0464 (50 entries each) overlays the copy-protection table, which is only needed during the manual check at start-up." ] }