{ "chunk": "game_main_0800_3C50", "unit": "game/main_0800", "routines": [ { "addr": "3C50", "name": "setBoomerTargetCmd", "summary": "Receive handler of command $87 (SET TARGET for a boomer): X = cmdParam0 (boomer unit index), countBoomersBelow gives its ordinal in Y, then boomerTargetTable[Y] = cmdParam2 | $80 (target unit index with the lock bit). The disassembly shows these 13 bytes as data; they are reached by the JMP $3C50 at $5182 inside the command dispatcher (also mis-listed as data at $515D-$523D). The sending side is sub_3C20 (cursor on an enemy unit while a unit is selected).", "inputs": "zp_73 = boomer unit index, zp_75 = target unit index (bits 0-6)", "outputs": "D_91DC[boomer ordinal] = target | $80; Y = ordinal; X = unit index", "confidence": "high" }, { "addr": "3C5D", "name": "countBoomersBelow", "summary": "Counts the units with type 2 (boomers) whose index is lower than X, i.e. returns the ordinal of boomer X among all boomers. The limit X is patched into the CPX operand at $3C70. Used to index boomerTargetTable.", "inputs": "X = unit index (exclusive upper limit)", "outputs": "Y = number of boomers with index < X; X = input X; A = last type byte; C = 1", "confidence": "high" }, { "addr": "3C74", "name": "getBoomerTarget", "summary": "If unit X is a boomer (unitTypeTable & 7 == 2) returns its entry of boomerTargetTable (bit 7 set = target locked, bits 0-6 = target unit index) with C = 1; for any other unit type returns with C = 0 and A = type ^ 2 (nonzero, positive). Callers use C/N: the unit option menu shows CLEAR TARGET/SET TARGET, and the unit update loop clears the lock when a boomer can no longer fire.", "inputs": "X = unit index", "outputs": "C = 1 and A = boomerTargetTable[Y], Y = boomer ordinal (boomer); C = 0 otherwise", "confidence": "high" }, { "addr": "3C85", "name": "selectBoomerTarget", "summary": "Chooses the firing target for boomer Y. Runs the spotting scan (sub_35C3 with X=$80 = weapon-range mode) which fills candidateUnitList/candidateDistList/candidateDirList with visible enemies, then walks the list: if the previously locked target (boomerTargetTable entry ^ $80) is still listed it is kept and the routine returns at once; otherwise the enemy comcen (unit 49 or 99) is preferred, else the nearest candidate, and the lock in boomerTargetTable is cleared. Called from the per-unit update loop at $4425 for units of type 2.", "inputs": "Y = boomer unit index; D_0400/D_0432/D_0464 lists produced by sub_35C3; zp_2F = list count-1", "outputs": "D_91F0 = chosen target unit ($FF none), D_91F1 = its distance, zp_6B = direction to it (0-3); D_91DC[ordinal] cleared when the old lock is lost; zp_18/19/1A scratch", "confidence": "high" }, { "addr": "3CF2", "name": "animateRingBurst", "summary": "Plays an expanding-then-contracting ring animation on hardware sprite 1 (colour shadow $9009 = $12, i.e. flashing white/red): clears the sprite shape, enables sprite bit 1, then for radius index 1..A-1 draws a circle frame (sub_3239 with spritePlotMode $40 = set pixels), waiting one tick each (sub_4094); at the largest radius plays sound $17, waits 5 ticks, then erases the frames in reverse order (spritePlotMode 0) and disables the sprite. Overlay B calls it with A=3 (unit placement / kick-off, $767B) and A=$0B (comcen knock-out explosion, $7A0E).", "inputs": "A = number of radius steps (patched into the CPX at $3D25); sprite 1 position already set in $9011/$9019", "outputs": "sprite 1 shape and enable bit modified, D_3CF1 loop counter, D_91F3 plot mode (left 0), zp_7D = 0 (never read), sound $17", "confidence": "medium" }, { "addr": "3D57", "name": "rollComcenSystemDamage", "summary": "When the player's comcen is hit (called from the damage routine at $3B63 only for unit == comcenUnit) and the DAMAGE option (gameTypeOptions bit 4) is on, in a linked game (soloModeFlag bit 7 clear) with a game in progress, rolls for each of the three comcen systems 3..1 that is not yet damaged: if (random & $3F) < damageAmount the system status is set to $80 (damaged).", "inputs": "D_9240 = damage amount of the hit; D_0B9B, D_0BA3, D_0BA8", "outputs": "D_9241[1..3] may become $80; D_3D56 loop index; A/Y clobbered", "confidence": "high" }, { "addr": "3D8A", "name": "initAllUnits", "summary": "Game start: runs initUnitRecord for every unit 99..0 (zp_18 = index), which resets each unit from its start coordinates and places it on the map, then clears the 16 groupStatusTable bytes. Called from overlay A at $7AEF.", "inputs": "unit start positions in unitCol/unitRow, unitTypeTable", "outputs": "all per-unit arrays reset, map cells marked with $80|unit, D_91F4-D_9203 = 0", "confidence": "high" }, { "addr": "3DA0", "name": "initUnitRecord", "summary": "Resets the order/destination state of unit zp_18 (X): waypoint = destination = current position (unitDestCol gets bit 7 = idle), clears unitTypeTable bits 3-4, unitStunTable = 0, keeps only bits 2-4 and 6 of unitFlagsTable (group and membership), sets the facing bits from initialFacingForUnit and bit 5 (side) for units >= 50, then falls into placeUnitOnMap.", "inputs": "zp_18 = X = unit index", "outputs": "F898/F8FC/F7D0/F834/F76C/F960/F708 of the unit; continues in placeUnitOnMap", "confidence": "high" }, { "addr": "3DDA", "name": "placeUnitOnMap", "summary": "Second half of the unit reset, also called on its own by overlay A ($7D83, all units) to re-place units: unitStunTable bit 7 = dug-in bit (unitTypeTable bit 6), pinnedTimer = 0, movePoints = 10, energy = 50 (100%), unitDisplayFlags bit 7 = idle bit, copies group/side bits into unitDisplayFlags (sub_4C6B), computes the map pointer of the unit's cell, saves the cell contents in unitUnderCell and writes $80|index into the map; a dead unit (type byte bit 7) instead becomes an empty slot ($FF in unitTypeTable and unitUnderCell).", "inputs": "zp_18 = X = unit index; unitCol/unitRow", "outputs": "F960/FA8C/FA28/F9C4/FB54/FAF0 of the unit, map cell at (col,row), zp_48/49 = map pointer", "confidence": "high" }, { "addr": "3E23", "name": "recycleDeadUnits", "summary": "Per-tick recycler logic (called at $465A). For each side 1..0 that has a recycler (recyclerCol bit 7 clear) whose cell is not occupied by a unit: scans the side's 50 units from the comcen downwards for dead units (type byte bit 7 set but not $FF). If recyclerMode[side] is 0 the first dead unit is respawned immediately; otherwise two dead units of the same type are needed - the first is freed (slot $FF) and the second is respawned. The respawned unit gets the recycler coordinates, its type, flags 0 and goes through initUnitRecord. For the local side it stores the unit in recycledUnit and queues message 5 (UNIT RECYCLED).", "inputs": "D_0BA8 (no game -> return), D_92A8/D_92AA recycler coordinates, D_92A6 recycler mode, D_0C8A last unit index per side, unit arrays", "outputs": "unit arrays and map updated, D_923B, message 5; zp_18/19/1A/1B/1C/1D scratch", "confidence": "high" }, { "addr": "3EBD", "name": "removeUnitFromMap", "summary": "Takes unit X off the map if it is on it (unitUnderCell bit 7 clear): restores the saved terrain byte into the unit's map cell, marks unitUnderCell = $FF and clears pinnedTimer. Falls into removeUnitLeavingCell with A = saved cell.", "inputs": "X = unit index, unitUnderCell[X], unitCol/unitRow", "outputs": "map cell restored, FAF0[X] = $FF, FA8C[X] = 0, zp_48/49", "confidence": "high" }, { "addr": "3EC2", "name": "removeUnitLeavingCell", "summary": "Like removeUnitFromMap but the caller supplies the byte to leave in the unit's map cell in A (used at $4894 to leave a specific marker behind). Stores A at the unit's map position, then unitUnderCell = $FF and pinnedTimer = 0.", "inputs": "A = new map cell value, X = unit index", "outputs": "map cell, FAF0[X] = $FF, FA8C[X] = 0; Y = 0", "confidence": "high" }, { "addr": "3EE2", "name": "applyBlitzStepCost", "summary": "Called after a successful movement step of unit Y when the blitz bit (unitWaypointRow bit 7) is set: if the unit is moving (unitDestCol bit 7 clear) and still has energy, sets unitFlagsTable bit 7 (charge bonus for the next attack), takes 1 point of energy and adds 25 movement points (saturating at $7F). Plays sound $16 when the unit is the player's own comcen in a live game and no other sound ($14/$16) is busy.", "inputs": "Y = unit index; zp_B1, D_0B9B, D_9236, D_67CA", "outputs": "F708[Y] bit 7, F9C4[Y] energy-1 (via sub_3BDF, D_9240 = 1), FA28[Y] += 25", "confidence": "medium" }, { "addr": "3F27", "name": "drawRecyclerOnMap", "summary": "Writes the recycler glyph ($67) into the map at side Y's recycler position (recyclerCol/recyclerRow). If a unit occupies that cell the glyph goes into that unit's unitUnderCell entry instead (via the $FA70,Y = $FAF0 + (Y & $7F) trick) so it reappears when the unit leaves. Returns the cell coordinates and the byte to draw so the caller can JSR drawMapCell. Returns immediately if the side has no recycler.", "inputs": "Y = side index (0/1); D_92A8/D_92AA", "outputs": "X = column, Y = row, A = cell value to draw ($67 or the $80|unit byte); map or unitUnderCell updated; operands at $3F51/$3F53/$3F55 patched", "confidence": "high" }, { "addr": "3F57", "name": "checkEnemyRecyclerSpotted", "summary": "Once per tick until it succeeds: collects the local player's units within radius 3 of the opponent's recycler (sub_378B/sub_37AA with the opponent's comcen index as the 'enemy' reference) and, if any of them is not cloaked (unitWaypointCol bit 7 clear), draws the enemy recycler on the map (drawRecyclerOnMap + drawMapCell), queues message 8 (SPOTTED RECYCLER!) and patches its own first byte to RTS so it never runs again. The byte is restored to NOP at game init ($0BBD) and set to RTS by the comm module when the opponent has no recycler ($EE3E).", "inputs": "D_0B9F playerSide, D_0C8A, D_92A8/D_92AA of the opponent", "outputs": "map cell, message 8, self-patch at $3F57; candidate lists at $0400/$0432", "confidence": "high" }, { "addr": "3F97", "name": "initialFacingForUnit", "summary": "Returns the initial facing code for unit X: 0 (up) for units of the local player's side, 1 (down) for the other side. The branch opcode at $3F9E is patched by overlay A ($7BD5) to BCC for side 0 and BCS for side 1 so that 'own' means index < 50 or >= 50 respectively. The leading LDA D_0B9F is dead (overwritten by LDA #0).", "inputs": "X = unit index; opcode at $3F9E", "outputs": "A = 0 or 1", "confidence": "high" }, { "addr": "3FA3", "name": "mirrorMapCoordinate", "summary": "Returns 39 - A: mirrors a map column/row across the 40-cell battlefield. Used by the comm module and overlays to convert coordinates between the two players' viewpoints (each player sees his own side at the bottom).", "inputs": "A = coordinate 0..39", "outputs": "A = 39 - A", "confidence": "high" }, { "addr": "3FAC", "name": "haltStunnedUnit", "summary": "Called from the unit update loop while unit Y is stunned (stun counter nonzero). Unless gameTypeOptions bit 3 is set it only acts on the player's comcen; and when linkTypeAtStart bit 7 is set it ignores units of side 1 (index >= 50). It stops the unit: waypoint and destination = current position, unitDestCol bit 7 (idle) set, unitFlagsTable bit 6 (group membership) cleared, unitDisplayFlags bit 7 set and bit 6 cleared.", "inputs": "Y = unit index; D_0BA3, D_9236, D_92FE", "outputs": "F898/F7D0/F8FC/F834/F708/FB54 of the unit", "confidence": "medium" }, { "addr": "3FE8", "name": "haltAllUnitsAndReveal", "summary": "Game-over helper: clears D_9242/D_9243/D_9244, then for all 100 units clears the fired/hit bits and sets the idle bit of unitDisplayFlags and sets the waypoint to the current position (keeping the cloak/blitz bits), marks every unit as spotted (sub_3568 sets energy bits 6-7) and, if the battlefield screen is active, redraws the overview (sub_5AD7) and the view (sub_26C1).", "inputs": "D_90FB currentScreen", "outputs": "FB54/F898/F8FC/F9C4 of all units, D_9242-D_9244 = 0, screen redraw", "confidence": "high" }, { "addr": "4027", "name": "endGame", "summary": "Ends the battle after the caller has stored the end reason in D_0BA8 ($08 = clock ran out at $4608, $A0/$C0 = a comcen was knocked out at $3BD3): clears D_920D and D_92F9, halts and reveals all units, shows the result/score message (sub_28F1) and starts the 120-tick game-end countdown D_92C9.", "inputs": "D_0BA8 end reason, scores D_92A2-D_92A5 (via sub_28F1)", "outputs": "D_920D = 0, D_92F9 = 0, D_92C9 = $78, all units halted", "confidence": "high" } ], "variables": [ { "addr": "18", "scope": "zp", "name": "tempByte18", "meaning": "scratch: unit index passed to initUnitRecord/placeUnitOnMap, list index in selectBoomerTarget, damage cap in the attack routine", "confidence": "high" }, { "addr": "19", "scope": "zp", "name": "tempByte19", "meaning": "scratch: locked target index in selectBoomerTarget, unit type of the dead unit in recycleDeadUnits, next-cell value in the movement code", "confidence": "high" }, { "addr": "1A", "scope": "zp", "name": "tempByte1A", "meaning": "scratch: boomer ordinal in selectBoomerTarget, side index loop in recycleDeadUnits", "confidence": "high" }, { "addr": "1B", "scope": "zp", "name": "firstDeadUnit", "meaning": "recycleDeadUnits: index of the first dead unit of the current type found (freed when a second one is found); elsewhere direction scratch", "confidence": "medium" }, { "addr": "1C", "scope": "zp", "name": "scanUnitIndex", "meaning": "recycleDeadUnits: unit index cursor running from the side's comcen downwards", "confidence": "medium" }, { "addr": "1D", "scope": "zp", "name": "scanUnitsLeft", "meaning": "recycleDeadUnits: number of units of the side still to scan (50 down to 1)", "confidence": "medium" }, { "addr": "2F", "scope": "zp", "name": "candidateCount", "meaning": "index of the last entry of the candidate lists at $0400/$0432/$0464 built by sub_37AA (count-1, $FF = none)", "confidence": "high" }, { "addr": "48", "scope": "zp", "name": "mapCellPtr", "meaning": "pointer (lo) into the $F000 battlefield map set by sub_25FA (X = column, Y = row); $49 = hi", "confidence": "high" }, { "addr": "6B", "scope": "zp", "name": "unitFacing", "meaning": "facing/aim direction of the unit being processed: 0 up, 1 down, 2 left, 3 right (stored in unitFlagsTable bits 0-1); set to the direction of the chosen target", "confidence": "high" }, { "addr": "73", "scope": "zp", "name": "cmdParam0", "meaning": "first parameter of a received command (unit index for setBoomerTargetCmd)", "confidence": "high" }, { "addr": "75", "scope": "zp", "name": "cmdParam2", "meaning": "third parameter of a received command (target unit index for setBoomerTargetCmd)", "confidence": "high" }, { "addr": "7D", "scope": "zp", "name": "unusedByte7D", "meaning": "written with 0 by animateRingBurst and never read anywhere (dead store)", "confidence": "high" }, { "addr": "B1", "scope": "zp", "name": "gamePhase", "meaning": "0 while the battle runs; nonzero in setup/film/menu phases (blitz cost and comcen sound are skipped)", "confidence": "high" }, { "addr": "0400", "scope": "abs", "name": "candidateUnitList", "meaning": "work list (up to 50 entries) of enemy unit indices found by sub_37AA within a radius; entries set to $FF by sub_35C3 when line of sight fails. Overlays the copy-protection answer table, which is only needed at start-up", "confidence": "high" }, { "addr": "0432", "scope": "abs", "name": "candidateDistList", "meaning": "parallel to candidateUnitList: dx^2 + dy^2 distance metric (table D_3860, saturating at $FF)", "confidence": "high" }, { "addr": "0464", "scope": "abs", "name": "candidateDirList", "meaning": "parallel to candidateUnitList: direction 0-3 (up/down/left/right) from the viewing unit to the candidate (sub_375D)", "confidence": "high" }, { "addr": "0B9B", "scope": "abs", "name": "soloModeFlag", "meaning": "bit 7 set = solo/film mode: no comcen system damage rolls, no blitz sound, messages via sub_C560 suppressed", "confidence": "high" }, { "addr": "0B9F", "scope": "abs", "name": "playerSide", "meaning": "local player's side 0/1; EOR 1 gives the opponent", "confidence": "high" }, { "addr": "0BA3", "scope": "abs", "name": "gameTypeOptions", "meaning": "bits 0-2 game type, bit 3 = stunned units of any kind are halted (otherwise only the comcen), bit 4 = DAMAGE option", "confidence": "medium" }, { "addr": "0BA8", "scope": "abs", "name": "gameEndReason", "meaning": "0 while a game is in progress; nonzero ends it: $08 clock expired, $A0/$C0 = comcen of side 0/1 knocked out (bit 5/6 from unitFlagsTable bit 5), also set by menus when no game runs", "confidence": "medium" }, { "addr": "0C8A", "scope": "abs", "name": "lastUnitOfSide", "meaning": "2 bytes $31,$63: index of the last unit (the comcen) of side 0 and side 1; units 0-49 belong to side 0, 50-99 to side 1", "confidence": "high" }, { "addr": "3CF1", "scope": "abs", "name": "ringAnimStep", "meaning": "current radius index of animateRingBurst", "confidence": "high" }, { "addr": "3D56", "scope": "abs", "name": "comcenSystemIndex", "meaning": "loop index 3..1 of rollComcenSystemDamage", "confidence": "high" }, { "addr": "67CA", "scope": "abs", "name": "currentSoundId", "meaning": "id of the sound effect currently playing in the $C89A sound driver (0 = none); applyBlitzStepCost only starts sound $16 when this is 0, $14 or $16", "confidence": "medium" }, { "addr": "9009", "scope": "abs", "name": "sprite1ColourShadow", "meaning": "colour shadow of sprite 1 ($9008+1); $12 = two alternating colours (white/red flashing) for the ring burst", "confidence": "high" }, { "addr": "90FB", "scope": "abs", "name": "currentScreen", "meaning": "0 = battlefield view; haltAllUnitsAndReveal redraws only then", "confidence": "high" }, { "addr": "91DC", "scope": "abs", "name": "boomerTargetTable", "meaning": "one byte per boomer (ordinal among type-2 units, up to 20 entries $91DC-$91EF): bit 7 = target locked, bits 0-6 = target unit index; 0 = no target", "confidence": "high" }, { "addr": "91F0", "scope": "abs", "name": "chosenTargetUnit", "meaning": "unit index selected as firing target by the spotting/targeting code for the unit being updated; $FF = none", "confidence": "high" }, { "addr": "91F1", "scope": "abs", "name": "chosenTargetDist", "meaning": "distance metric of chosenTargetUnit (also the search radius passed to sub_37AA by sub_35C3)", "confidence": "high" }, { "addr": "91F3", "scope": "abs", "name": "spritePlotMode", "meaning": "mode for the sprite shape plotter sub_31AF: $40 = set pixels, $80 = toggle (EOR), 0 = clear pixels", "confidence": "medium" }, { "addr": "91F4", "scope": "abs", "name": "groupStatusTable", "meaning": "16 bytes, one per group (unitFlagsTable bits 2-5): bits 3-5 = terrain class of the slowest member, bits 0-2 = type-derived value; rebuilt each tick by sub_48DD, cleared by initAllUnits", "confidence": "low" }, { "addr": "920D", "scope": "abs", "name": "screenStateFlags920D", "meaning": "flag byte cleared by endGame and on screen switches (bit 0 set at $52EB); exact meaning unknown", "confidence": "low" }, { "addr": "9236", "scope": "abs", "name": "comcenUnit", "meaning": "index of the local player's comcen unit (49 or 99)", "confidence": "high" }, { "addr": "923B", "scope": "abs", "name": "recycledUnit", "meaning": "index of the unit most recently recycled on the local side (set by recycleDeadUnits, read at $417B)", "confidence": "medium" }, { "addr": "9240", "scope": "abs", "name": "damageAmount", "meaning": "amount of the current hit being applied (parameter of sub_3BDF / the damage routine); compared with random & $3F for system damage", "confidence": "high" }, { "addr": "9241", "scope": "abs", "name": "comcenSystemStatus", "meaning": "4 bytes: [0] energy status, [1..3] status of the three comcen systems, $80 = damaged", "confidence": "high" }, { "addr": "9242", "scope": "abs", "name": "unitDrawOverride", "meaning": "bit 7 set changes how units are drawn (sub_5BDA skips the type/side lookup); set by overlay A, cleared by haltAllUnitsAndReveal", "confidence": "low" }, { "addr": "9243", "scope": "abs", "name": "radarEventFlag9243", "meaning": "nonzero enables a random event in overlay B screen 1 ($7604); cleared at game end", "confidence": "low" }, { "addr": "9244", "scope": "abs", "name": "radarEventFlag9244", "meaning": "nonzero enables a random event in overlay B screen 2 ($804D); cleared at game end", "confidence": "low" }, { "addr": "92A6", "scope": "abs", "name": "recyclerMode", "meaning": "2 bytes, one per side: recycler setting from the setup screen (FULL/HALF/NONE): 0 = every dead unit is recycled, 1 = two dead units of a type yield one, 2 = no recycler (coordinates $FF)", "confidence": "medium" }, { "addr": "92A8", "scope": "abs", "name": "recyclerCol", "meaning": "2 bytes, one per side: map column of the side's recycler, bit 7 set = none", "confidence": "high" }, { "addr": "92AA", "scope": "abs", "name": "recyclerRow", "meaning": "2 bytes, one per side: map row of the side's recycler", "confidence": "high" }, { "addr": "92C9", "scope": "abs", "name": "gameEndCountdown", "meaning": "set to $78 by endGame; nonzero = game over sequence running (aborts waits, ticks in sub_4094)", "confidence": "high" }, { "addr": "92F9", "scope": "abs", "name": "viewStateFlags92F9", "meaning": "flag byte cleared by endGame and when a screen is switched; set by overlay B ($79EB) with bit 5 and a toggled bit 4", "confidence": "low" }, { "addr": "92FE", "scope": "abs", "name": "linkTypeAtStart", "meaning": "copy of linkTypeFlag taken at game start; bit 7 set makes haltStunnedUnit ignore side-1 units", "confidence": "medium" }, { "addr": "F640", "scope": "abs", "name": "unitCol", "meaning": "per-unit map column (100 entries; all per-unit arrays are 100 bytes apart)", "confidence": "high" }, { "addr": "F6A4", "scope": "abs", "name": "unitRow", "meaning": "per-unit map row", "confidence": "high" }, { "addr": "F708", "scope": "abs", "name": "unitFlagsTable", "meaning": "per-unit flags: bits 0-1 facing (0 up,1 down,2 left,3 right), bits 2-4 group number, bit 5 side (1 = units 50-99), bit 6 group membership active, bit 7 blitz charge ready (attack bonus pending)", "confidence": "medium" }, { "addr": "F76C", "scope": "abs", "name": "unitTypeTable", "meaning": "per-unit type byte: bits 0-2 type (0 grunt, 1 rider, 2 boomer, 3 spy, 4 comcen; 5-7 drone/missile class), bit 6 dug in, bit 7 dead; $FF = empty slot", "confidence": "medium" }, { "addr": "F7D0", "scope": "abs", "name": "unitDestCol", "meaning": "per-unit destination column (bits 0-5); bit 7 set = idle (no move order, destination reached)", "confidence": "high" }, { "addr": "F834", "scope": "abs", "name": "unitDestRow", "meaning": "per-unit destination row (bits 0-5); bit 7 = resting/recovering energy flag", "confidence": "medium" }, { "addr": "F898", "scope": "abs", "name": "unitWaypointCol", "meaning": "per-unit next waypoint column (bits 0-5); bit 6 = cloaking ordered (CLOAKING ON), bit 7 = cloak active (unit hidden, does not spot or fire)", "confidence": "medium" }, { "addr": "F8FC", "scope": "abs", "name": "unitWaypointRow", "meaning": "per-unit next waypoint row (bits 0-5); bit 6 = blitz ordered (SET BLITZ), bit 7 = blitz in effect while moving", "confidence": "medium" }, { "addr": "F960", "scope": "abs", "name": "unitStunTable", "meaning": "per-unit: bits 0-3 stun counter, bits 5-6 dig-in progress, bit 7 dug in", "confidence": "medium" }, { "addr": "F9C4", "scope": "abs", "name": "unitEnergyTable", "meaning": "per-unit energy bits 0-5 (0..50 = 0..100%), bit 6 = seen, bit 7 = visible/spotted", "confidence": "high" }, { "addr": "FA28", "scope": "abs", "name": "unitMovePoints", "meaning": "per-unit movement point accumulator (signed, starts at 10): a step costs the terrain value from D_405E (+8 diagonal), being hit subtracts the damage (pinned), blitz adds 25", "confidence": "medium" }, { "addr": "FA8C", "scope": "abs", "name": "unitPinnedTimer", "meaning": "per-unit: bit 7 set with the hit amount (max $14) when the unit was pinned by fire; 0 = free; $85 for a knocked-out comcen", "confidence": "medium" }, { "addr": "FAF0", "scope": "abs", "name": "unitUnderCell", "meaning": "per-unit saved map byte of the cell the unit stands on (restored when it leaves); $FF = unit is not on the map", "confidence": "high" }, { "addr": "FB54", "scope": "abs", "name": "unitDisplayFlags", "meaning": "per-unit: bit 0 fired this tick, bit 1 was hit this tick, bits 2-6 copy of unitFlagsTable bits 2-6 (group/side/membership), bit 7 idle/stationary", "confidence": "medium" } ], "dataBlocks": [ { "addr": "3CF1", "length": 1, "type": "byteTable", "name": "ringAnimStep", "description": "loop counter of animateRingBurst" }, { "addr": "3D56", "length": 1, "type": "byteTable", "name": "comcenSystemIndex", "description": "loop counter of rollComcenSystemDamage" } ], "misclassified": [ { "addr": "3C50", "length": 13, "actual": "code", "evidence": "bytes A6 73 20 5D 3C A5 75 09 80 99 DC 91 60 = LDX $73 / JSR $3C5D / LDA $75 / ORA #$80 / STA $91DC,Y / RTS; reached by JMP $3C50 at $5182 (bytes 4C 50 3C inside the .byte run at $5181), the handler of command $87" }, { "addr": "515D", "length": 40, "actual": "code", "evidence": "outside this chunk but related: $515D-$5184 disassembles to JSR $4FF7 / LDX $73 / JSR $3C74 / LDA #0 / STA $91DC,Y / RTS (clear boomer target command) and JSR $4FD0 / parameter shuffling / JMP $3C50 (set boomer target command); the whole .byte run $50D9-$523D appears to be command handlers" } ], "insights": [ "Unit records: 100 units, index 0-49 = side 0, 50-99 = side 1, the last unit of each side (49, 99) is the comcen. Fourteen parallel 100-byte arrays start at $F640 right after the 40x40 map: F640 col, F6A4 row, F708 flags (facing/group/side), F76C type (0 grunt, 1 rider, 2 boomer, 3 spy, 4 comcen; bit 6 dug in, bit 7 dead, $FF empty), F7D0/F834 destination (F7D0 bit 7 = idle), F898/F8FC waypoint (bit 6/7 cloak flags resp. blitz flags), F960 stun/dig-in, F9C4 energy (0-50) + spotted bits, FA28 movement points, FA8C pinned timer, FAF0 saved map byte under the unit, FB54 display flags.", "A unit on the battlefield map is stored as $80 | unitIndex in the cell; sub_25FA(X = col, Y = row) returns the cell byte in A (N flag = unit present) and the cell address in zp_48/49. Routines index other per-unit arrays with the raw cell byte by using base-$80 addresses (e.g. $FA70,Y = unitUnderCell, $F688,Y = unitFlagsTable, $F818,Y = unitWaypointCol, $F6EC,Y = unitTypeTable).", "The map cell value $67 is the recycler glyph. Each side's recycler position is D_92A8/D_92AA (bit 7 = none, when recyclerMode D_92A6 = 2). The own recycler is drawn at game start by the comm module (JMP sub_3F27); the enemy recycler only appears after one of the player's uncloaked units comes within radius 3 (sub_3F57, message 8 'SPOTTED RECYCLER!', then the routine patches itself to RTS). Dead units respawn at the recycler (sub_3E23, message 5 'UNIT RECYCLED'); with recyclerMode 1 two dead units of one type are needed for one respawn.", "Candidate lists at $0400 (unit index), $0432 (distance metric dx^2+dy^2 from table D_3860, capped $FF) and $0464 (direction 0-3) are built by sub_37AA (A = radius, X/Y = centre) for the units of the side opposite to the reference unit passed to sub_378B, which patches the CMP #$32 / BCC-BCS pair at $37FE/$3800. sub_35C3 then removes the ones without line of sight (entry = $FF). zp_2F holds count-1. These lists overwrite the copy-protection answer table at $0401, which is only used at start-up.", "Boomers (type 2) keep a persistent target lock in boomerTargetTable ($91DC, indexed by the boomer's ordinal among all boomers, computed by sub_3C5D). SET TARGET / CLEAR TARGET in the unit option menu send commands $87 (handler $3C50, hidden as data) and the clear command (handler at $515D, also hidden as data). sub_3C85 keeps the lock while the target stays visible, otherwise prefers the enemy comcen, then the nearest visible enemy.", "Game end: the caller stores the reason in D_0BA8 ($08 time up, $A0/$C0 comcen of side 0/1 destroyed) and calls sub_4027, which halts and reveals all units, shows the result (sub_28F1 compares the 16-bit scores D_92A2/D_92A4 vs D_92A3/D_92A5) and starts the $78-tick countdown D_92C9. The comcen never gets the dead bit; its destruction sets D_0BA8 instead.", "Overlay A's sub_7BC6 patches branch opcodes in the resident code per player side ($90 BCC for side 0, $B0 BCS for side 1) at $63E2, $2840, $3F9E, $36D5, $379C, $4783, $5BF7 and $206B (EOR $20 variant): every 'is this one of my units' test in main code is a CPX/CPY #$32 followed by a patched branch. In this chunk that affects sub_3F97 (initial facing: own units face up).", "Message ids used here map through the pointer tables at $0500/$0524 (lo/hi) into the $8800 strings: 5 = 'UNIT RECYCLED', 8 = 'SPOTTED RECYCLER!', $0C = 'COMCEN STUNNED!'. sub_C560 drops the message in solo/film mode ($0B9B bit 7); sub_C566 always queues it (priority-sorted 8-entry queue at $9249).", "The unit option menu strings live in the $C000 overlay: items 0-$0B at $C9D2 + 16*n (FORMATION, MOVE TO GOAL, CLOAKING ON, NO CLOAKING, CLEAR TARGET, SET TARGET, MEMBERSHIP, JOIN GROUP, DIRECT UNIT, SELF DESTRUCT, SET BLITZ, BLITZ OFF) and items $0C+ at $CAB2 + 8*n ((EXIT), ENERGY, BATTLE, RADAR, DRONE, DIG OUT, DIG IN). The menu code at $5E99 shows which unit bits they toggle: F898 bit 6 cloak, F8FC bit 6 blitz, FB54 bit 7 / F960 bits 5-7 dig in." ] }