Use this guide when
Use this workflow when idle Towers repair ordinary structures and you need to distinguish Tower repair from Creep repair, another Tower, damage, or natural decay. The exact proof is a prior-tick EVENT_REPAIR entry for the saved Tower and structure IDs. Net structure hits are secondary state only.
Require defense and healing gates first
function chooseTowerMode(input) {
if (input.attackTarget) return 'attack';
if (input.healTarget) return 'heal';
if (input.repairTarget) return 'repair';
return 'idle';
}
function getRepairableTowers(room, reserve) {
return room.find(FIND_MY_STRUCTURES, {
filter: structure =>
structure.structureType === STRUCTURE_TOWER
&& structure.isActive() === true
&& structure.store.getUsedCapacity(
RESOURCE_ENERGY
) >= reserve + TOWER_ENERGY_COST
}).sort((left, right) =>
left.id.localeCompare(right.id)
);
}
Attack-before-heal-before-repair and the Energy reserve are project policies. The important engineering boundary is that one dispatcher chooses one final action for each Tower.
Select an ordinary damaged structure
function getStructureHitRatio(structure) {
if (
!structure
|| !Number.isFinite(structure.hits)
|| !Number.isFinite(structure.hitsMax)
|| structure.hitsMax <= 0
) {
return null;
}
return structure.hits / structure.hitsMax;
}
function selectTowerRepairTarget(
towers,
structures,
ratioLimit
) {
return structures.filter(structure => {
const ratio = getStructureHitRatio(structure);
return Number.isFinite(ratio)
&& structure.hits < structure.hitsMax
&& ratio < ratioLimit
&& structure.structureType !== STRUCTURE_WALL
&& structure.structureType !== STRUCTURE_RAMPART;
}).sort((left, right) =>
getStructureHitRatio(left)
- getStructureHitRatio(right)
|| Math.min(...towers.map(tower =>
tower.pos.getRangeTo(left)
))
- Math.min(...towers.map(tower =>
tower.pos.getRangeTo(right)
))
|| left.id.localeCompare(right.id)
)[0] || null;
}
Walls and Ramparts remain outside this ratio policy because fortification targets usually use absolute hit bands and separate budgets.
Estimate range-adjusted repair
function getTowerRangeAmount(basePower, range) {
const clamped = Math.max(
TOWER_OPTIMAL_RANGE,
Math.min(TOWER_FALLOFF_RANGE, range)
);
const fraction = (
clamped - TOWER_OPTIMAL_RANGE
) / (
TOWER_FALLOFF_RANGE
- TOWER_OPTIMAL_RANGE
);
return Math.floor(
basePower * (1 - TOWER_FALLOFF * fraction)
);
}
function getTowerEffectModifier(tower) {
return (tower.effects || []).reduce(
(modifier, effect) => {
const relevant =
effect.ticksRemaining > 0
&& (
effect.effect === PWR_OPERATE_TOWER
|| effect.effect === PWR_DISRUPT_TOWER
);
const values = relevant
? POWER_INFO[effect.effect]?.effect
: null;
const factor = Array.isArray(values)
? values[effect.level - 1]
: null;
return Number.isFinite(factor)
? modifier * factor
: modifier;
},
1
);
}
function estimateTowerRepair(tower, target) {
const range = tower.pos.getRangeTo(target);
const rangeAmount = getTowerRangeAmount(
TOWER_POWER_REPAIR,
range
);
return {
range,
estimatedRepair: Math.floor(
rangeAmount * getTowerEffectModifier(tower)
)
};
}
The estimate limits obvious over-repair. The actual event amount and energySpent are stronger settlement evidence than the estimate or net hits.
Stop allocating when estimated damage is covered
function allocateTowerRepair(towers, target) {
let remaining = Math.max(
0,
target.hitsMax - target.hits
);
const allocations = [];
for (const tower of [...towers].sort(
(left, right) =>
left.pos.getRangeTo(target)
- right.pos.getRangeTo(target)
|| left.id.localeCompare(right.id)
)) {
if (remaining <= 0) break;
const estimate = estimateTowerRepair(
tower,
target
);
allocations.push({
tower,
target,
...estimate
});
remaining -= estimate.estimatedRepair;
}
return {
allocations,
estimatedRemaining: Math.max(0, remaining)
};
}
Concurrent damage or repairs can invalidate the estimate during resolution. Rebuild the plan from current objects on every approved repair tick.
Snapshot and submit one repair operation
function submitTowerRepair(request) {
const room = Game.rooms[request?.roomName];
if (!request || request.enabled !== true || !room) {
return {
status: room
? 'disabled-or-invalid'
: 'room-not-visible'
};
}
const reserve = Number.isFinite(
request.energyReserve
)
? Math.max(0, request.energyReserve)
: 500;
const ratioLimit = Number.isFinite(
request.ratioLimit
)
? Math.max(0, Math.min(1, request.ratioLimit))
: 0.8;
const towers = getRepairableTowers(
room,
reserve
);
const target = selectTowerRepairTarget(
towers,
room.find(FIND_STRUCTURES),
ratioLimit
);
if (!target || towers.length === 0) {
return {
status: towers.length === 0
? 'repair-energy-protected'
: 'no-repair-target'
};
}
const plan = allocateTowerRepair(
towers,
target
);
request.enabled = false;
request.submittedAt = Game.time;
request.target = {
id: target.id,
structureType: target.structureType,
hitsBefore: target.hits,
hitsMax: target.hitsMax
};
request.intents = plan.allocations.map(item => ({
towerId: item.tower.id,
targetId: target.id,
range: item.range,
estimatedRepair: item.estimatedRepair,
energyBefore:
item.tower.store.getUsedCapacity(
RESOURCE_ENERGY
),
result: item.tower.repair(target)
}));
request.status = request.intents.some(
intent => intent.result === OK
)
? 'accepted-pending-events'
: 'failed-review-required';
return {
status: request.status,
target: request.target,
intents: request.intents
};
}
The code assumes the dispatcher has already confirmed there is no attack or healing priority. The request is disabled before the first API call.
Match Tower ID, target ID, amount, and Energy
function verifyTowerRepair(request) {
if (
!request
|| request.status !== 'accepted-pending-events'
) {
return {
status: 'submission-not-accepted'
};
}
if (Game.time <= request.submittedAt) {
return { status: 'pending-next-tick' };
}
if (Game.time !== request.submittedAt + 1) {
return { status: 'event-window-missed' };
}
const room = Game.rooms[request.roomName];
if (!room) {
return { status: 'room-not-visible' };
}
const accepted = request.intents.filter(
intent => intent.result === OK
);
const events = room.getEventLog().filter(
event => event.event === EVENT_REPAIR
);
const matches = accepted.map(intent => {
const event = events.find(item =>
item.objectId === intent.towerId
&& item.data?.targetId === intent.targetId
);
const energyExact = event
? event.data?.energySpent
=== TOWER_ENERGY_COST
: false;
return {
towerId: intent.towerId,
targetId: intent.targetId,
found: Boolean(event),
amount: event?.data?.amount ?? null,
energySpent:
event?.data?.energySpent ?? null,
energyExact
};
});
const exactCount = matches.filter(
item => item.found && item.energyExact
).length;
return {
status: exactCount === accepted.length
? 'verified-tower-repair'
: exactCount === 0
? 'tower-events-missing'
: 'partial-event-match',
matches
};
}
An EVENT_REPAIR entry names both the actor and target and exposes the resolved amount and Energy spent. That is more specific than observing a structure's final hit count.
Use net hits only as supporting context
After event verification, recover the structure by ID and record its current hits when visible. A Road can decay, an enemy can damage the target, and Creeps or other Towers can repair it. Those effects explain why net hits may not equal the sum of Tower repair events.
Separate budget, acceptance, and event evidence
| Status | Meaning |
|---|---|
repair-energy-protected | No Tower could spend the action cost while preserving the configured reserve. |
accepted-pending-events | At least one repair call returned OK; event evidence is pending. |
event-window-missed | The previous-tick event log was not checked at the required tick. |
partial-event-match | Only some accepted Tower IDs produced an exact repair event. |
verified-tower-repair | Every accepted Tower matched the structure and the recorded Energy cost. |
Preserve documented return codes
Documented results are OK, ERR_NOT_OWNER, ERR_NOT_ENOUGH_ENERGY, ERR_INVALID_TARGET, and ERR_RCL_NOT_ENOUGH. The Tower can repair throughout its room; range changes the amount rather than producing ERR_NOT_IN_RANGE.
Keep fortifications and economy policy separate
This page owns ordinary-structure Tower repair and event verification. Wall and Rampart bands, road replacement economics, hauler scheduling, emergency defense reserves, and multi-room maintenance need separate policies.