Quick answer
Find owned regular Creeps with 0 < hits < hitsMax, select the lowest hit ratio first, then the largest missing-hit count, nearest available Tower range and name as a stable tie-breaker. Let only owned active Towers with at least TOWER_ENERGY_COST Energy call tower.heal(target), keep each raw result, and verify target hits on a later tick.
const result = tower.heal(target);
if (result === OK) {
// The heal action was scheduled.
}
Keep the returned code even when it is OK. OK means the action was accepted and scheduled; it does not prove how many hits the target will have after tick resolution.
The current Tower-heal baseline costs TOWER_ENERGY_COST = 10 Energy per action. Healing is 400 at range 5 or less, falls linearly through range 20, and is 100 at range 20 or farther. A Tower can target valid Creeps across its room, so distance does not produce ERR_NOT_IN_RANGE.
Filter valid injured Creeps
function getInjuredOwnedCreeps(room) {
return room.find(FIND_MY_CREEPS, {
filter: creep =>
Number.isFinite(creep.hits)
&& Number.isFinite(creep.hitsMax)
&& creep.hits > 0
&& creep.hitsMax > 0
&& creep.hits < creep.hitsMax
});
}
StructureTower.heal() itself accepts a Creep or PowerCreep in the same room. This controller intentionally queries owned regular Creeps only with FIND_MY_CREEPS; it also ignores dead or malformed objects and avoids spending actions on full-health targets.
Rank urgency before distance
function selectTowerHealTarget(towers, creeps) {
const injured = creeps.filter(creep =>
Number.isFinite(creep.hits)
&& Number.isFinite(creep.hitsMax)
&& creep.hits > 0
&& creep.hitsMax > 0
&& creep.hits < creep.hitsMax
);
if (towers.length === 0 || injured.length === 0) {
return null;
}
return [...injured].sort((left, right) => {
const ratioDifference =
left.hits / left.hitsMax
- right.hits / right.hitsMax;
if (ratioDifference !== 0) {
return ratioDifference;
}
const missingDifference =
(right.hitsMax - right.hits)
- (left.hitsMax - left.hits);
if (missingDifference !== 0) {
return missingDifference;
}
const leftRange = Math.min(
...towers.map(tower =>
tower.pos.getRangeTo(left)
)
);
const rightRange = Math.min(
...towers.map(tower =>
tower.pos.getRangeTo(right)
)
);
if (leftRange !== rightRange) {
return leftRange - rightRange;
}
return left.name.localeCompare(right.name);
})[0] || null;
}
These priorities are explainable but not universal. A combat system may consider incoming damage, role, boosts, retreat state, Rampart position or defender importance.
Require active Towers with Energy
function getAvailableHealingTowers(room) {
return room.find(FIND_MY_STRUCTURES, {
filter: structure =>
structure.structureType === STRUCTURE_TOWER
&& structure.isActive() === true
&& structure.store.getUsedCapacity(
RESOURCE_ENERGY
) >= TOWER_ENERGY_COST
});
}
A Tower action uses Energy and one Tower should have one dispatcher per tick. Preflight cannot stop a separate module from assigning attack or repair afterward.
Complete Tower healing example
function runTowerHealing(room) {
if (!room) {
return { status: 'room-not-visible' };
}
const towers = getAvailableHealingTowers(room);
if (towers.length === 0) {
return { status: 'no-available-tower' };
}
const target = selectTowerHealTarget(
towers,
getInjuredOwnedCreeps(room)
);
if (!target) {
return { status: 'no-injured-creep' };
}
const snapshot = {
gameTick: Game.time,
targetId: target.id,
targetName: target.name,
hits: target.hits,
hitsMax: target.hitsMax,
hitRatio: target.hits / target.hitsMax,
missingHits: target.hitsMax - target.hits
};
const results = towers.map(tower => ({
towerId: tower.id,
range: tower.pos.getRangeTo(target),
energyBefore: tower.store.getUsedCapacity(
RESOURCE_ENERGY
),
result: tower.heal(target)
}));
const acceptedCount = results.filter(
item => item.result === OK
).length;
return {
status: acceptedCount === results.length
? 'heal-scheduled'
: acceptedCount > 0
? 'heal-partial'
: 'heal-rejected',
snapshot,
results
};
}
The aggregate status never replaces the raw Tower results: heal-partial means at least one Tower returned OK and at least one did not.
function getTowerHealingState() {
if (!Memory.towerHealing) {
Memory.towerHealing = {};
}
return Memory.towerHealing;
}
function saveTowerHealingAttempt(state, room, outcome) {
if (!outcome.snapshot || !outcome.results) {
return;
}
state.pendingObservation = {
tick: outcome.snapshot.gameTick,
roomName: room.name,
targetId: outcome.snapshot.targetId,
targetName: outcome.snapshot.targetName,
hitsBefore: outcome.snapshot.hits,
results: outcome.results.map(item => ({
towerId: item.towerId,
range: item.range,
result: item.result
}))
};
}
function reviewPreviousTowerHealing(state) {
const pending = state.pendingObservation;
if (!pending || pending.tick >= Game.time) {
return null;
}
const target = Game.getObjectById(pending.targetId);
const observation = {
scheduledTick: pending.tick,
observedTick: Game.time,
roomName: pending.roomName,
targetId: pending.targetId,
targetName: pending.targetName,
hitsBefore: pending.hitsBefore,
hitsNow: target ? target.hits : null,
targetVisible: Boolean(target),
results: pending.results
};
state.lastObservation = observation;
delete state.pendingObservation;
return observation;
}
module.exports.loop = function () {
const state = getTowerHealingState();
reviewPreviousTowerHealing(state);
const roomName = state.roomName;
if (typeof roomName !== 'string' || roomName.length === 0) {
console.log(
'Tower healing is not configured. Set '
+ 'Memory.towerHealing.roomName to a visible owned room name.'
);
return;
}
const room = Game.rooms[roomName];
if (!room) {
console.log(
'Tower healing room is not visible: ' + roomName
);
return;
}
const outcome = runTowerHealing(room);
saveTowerHealingAttempt(state, room, outcome);
if (
outcome.status === 'heal-partial'
|| outcome.status === 'heal-rejected'
) {
console.log(JSON.stringify({
type: 'tower-heal-action',
roomName,
...outcome
}));
}
};
Set Memory.towerHealing.roomName to your own visible owned room name before enabling this loop; the example does not assume any specific room. The tracker keeps only one pending heal attempt and one latest later-tick observation. It stores IDs, hit snapshots and raw return codes rather than caching a Creep object across ticks.
Understand multi-Tower over-heal
This baseline focuses every available Tower on the same injured Creep. That is easy to inspect but can spend more Energy than necessary when one Tower would fill the missing hits.
function getMissingHits(creep) {
return Math.max(0, creep.hitsMax - creep.hits);
}
A more advanced allocator can use range-adjusted healing, active Tower power effects and already assigned healing. If it is trying to predict combat outcomes rather than only current missing hits, expected incoming damage is another useful input. This article does not claim focus healing is optimal.
Keep one Tower action dispatcher
A common room policy is attack before heal before repair. That order is not enforced by the Tower API; your code must select one action for each Tower.
function chooseTowerMode(input) {
if (input.attackTarget) {
return 'attack';
}
if (input.healTarget) {
return 'heal';
}
if (input.repairTarget) {
return 'repair';
}
return 'idle';
}
Refresh targets every tick
The target may heal to full, die, leave the room, move closer to another Tower or receive new damage. Save only IDs and snapshots for diagnostics; run the selection again from current visible objects.
Verify healing later
The complete example moves the previous attempt into Memory.towerHealing.lastObservation on a later tick. Compare hitsBefore with hitsNow only when targetVisible is true; a missing target does not prove the heal failed. Healing may also be partly or fully offset by damage in the same resolution window, so a final hit difference is evidence of net state rather than a perfect measurement of one Tower's contribution.
Handle return codes
| Code | Meaning | Review |
|---|---|---|
OK | Healing scheduled | Target hits later |
ERR_NOT_OWNER | Tower not yours | Selection and ownership |
ERR_NOT_ENOUGH_ENERGY | Insufficient Energy | Tower Store / Energy |
ERR_INVALID_TARGET | Creep invalid | Refresh current target |
ERR_RCL_NOT_ENOUGH | Tower inactive | RCL and isActive() |
Debugging checklist
- Use
FIND_MY_CREEPSfor this controller scope. - Require valid positive hits and hitsMax.
- Exclude full-health Creeps.
- Document the hit-ratio policy.
- Use stable tie-breakers.
- Require active owned Towers.
- Check
TOWER_ENERGY_COST. - Use one action dispatcher.
- Inspect every raw heal return code, including partial failure.
- Compare later net hits only when the target is observable.
Scope and next steps
This guide does not integrate Power Creeps into the controller, build a full range-adjusted healing allocator, calculate Tower power effects, predict incoming damage, split healing or prevent over-heal. Continue with Tower repair thresholds and reserves.
Frequently asked questions
Why not always heal the nearest Creep?
The nearest Creep may have only minor damage while a farther defender is close to death. This policy ranks urgency before distance.
Why use the name as the final tie-breaker?
It makes otherwise equal target selection stable without pretending the name has combat value.
Can attack and heal functions both run?
Separate functions can both be called, but a controlled system should choose one intended action per Tower and preserve the final return code.