Separate the repair stage from target identity
A fortification stage answers how far the room is willing to repair. It does not answer which Repairer acted, which structure received the action, or whether several Repairers selected the same weak target. Treat those as separate contracts:
- a reviewed room policy defines eligible structure types and current hits stages;
- one coordinator assigns a target to each Repairer once per tick;
- only an accepted
repair()call creates a pending verification record; - the next tick matches the exact Repairer and structure in
EVENT_REPAIR.
Do not use hitsMax as the operating target. Walls and Ramparts can absorb room Energy indefinitely, while spawning, defense response, upgrading and logistics have shorter deadlines.
Define a room policy
function readFortificationPolicy(roomName) {
const value = Memory.fortificationPolicy?.[roomName];
if (
!value
|| value.enabled !== true
|| !Number.isFinite(value.wallStage)
|| !Number.isFinite(value.rampartStage)
|| value.wallStage <= 0
|| value.rampartStage <= 0
) {
return { enabled: false };
}
return {
enabled: true,
stages: {
[STRUCTURE_WALL]: value.wallStage,
[STRUCTURE_RAMPART]: value.rampartStage
}
};
}
Separate Wall and Rampart stages when their jobs differ. A Rampart protecting a Spawn may deserve a different policy from an outer Wall. The values remain player strategy; no official API constant declares a safe hits target.
Reserve targets in one coordinator
function createRepairCoordinator(room, policy) {
const assignedCreeps = new Set();
const reservedTargets = new Set();
function chooseTarget(creep) {
const candidates = room.find(FIND_STRUCTURES, {
filter: structure => {
const stage = policy.stages[structure.structureType];
return Number.isFinite(stage)
&& structure.hits < structure.hitsMax
&& structure.hits < stage
&& !reservedTargets.has(structure.id);
}
});
return candidates.sort((left, right) => {
const leftStage = policy.stages[left.structureType];
const rightStage = policy.stages[right.structureType];
const leftDeficit = (leftStage - left.hits) / leftStage;
const rightDeficit = (rightStage - right.hits) / rightStage;
return rightDeficit - leftDeficit
|| left.hits - right.hits
|| creep.pos.getRangeTo(left) - creep.pos.getRangeTo(right)
|| left.id.localeCompare(right.id);
})[0] || null;
}
return {
assign(creep) {
if (!creep || assignedCreeps.has(creep.id)) {
return { status: 'creep-already-assigned' };
}
assignedCreeps.add(creep.id);
const target = chooseTarget(creep);
if (!target) return { status: 'stage-complete' };
reservedTargets.add(target.id);
return { status: 'assigned', target };
},
release(targetId) {
reservedTargets.delete(targetId);
}
};
}
A target reservation is a local scheduling rule, not a game lock. It prevents this coordinator from sending every Repairer to the same lowest-hits structure. A more advanced policy may permit several Repairers on a breach, but that should be explicit rather than accidental.
Record only accepted repair calls
function submitFortificationRepair(coordinator, creep) {
if (
!creep
|| creep.my !== true
|| creep.spawning === true
|| creep.getActiveBodyparts(WORK) <= 0
|| creep.store.getUsedCapacity(RESOURCE_ENERGY) <= 0
) {
return { status: 'repairer-unavailable' };
}
const assignment = coordinator.assign(creep);
if (assignment.status !== 'assigned') return assignment;
const target = assignment.target;
if (!creep.pos.inRangeTo(target, 3)) {
coordinator.release(target.id);
return {
status: 'moving',
moveResult: creep.moveTo(target, { range: 3, reusePath: 5 }),
targetId: target.id
};
}
const result = creep.repair(target);
if (result !== OK) {
coordinator.release(target.id);
return { status: 'repair-rejected', result, targetId: target.id };
}
Memory.pendingFortificationRepairs ??= {};
Memory.pendingFortificationRepairs[creep.id] = {
submittedAt: Game.time,
roomName: creep.room.name,
creepId: creep.id,
creepName: creep.name,
targetId: target.id,
targetType: target.structureType,
hitsBefore: target.hits,
energyBefore: creep.store.getUsedCapacity(RESOURCE_ENERGY)
};
return { status: 'repair-accepted', result, targetId: target.id };
}
Movement is not a repair reservation across ticks. Release the local target when the Creep still needs to travel, then select again from current room state on a later tick. Save a pending record only after the real action returns OK.
Verify the exact repair event
function verifyFortificationRepair(pending) {
if (!pending) return { status: 'no-pending-repair' };
if (Game.time <= pending.submittedAt) {
return { status: 'wait-for-next-tick' };
}
if (Game.time > pending.submittedAt + 1) {
return { status: 'repair-event-window-missed' };
}
const room = Game.rooms[pending.roomName];
if (!room) return { status: 'room-not-visible' };
const matches = room.getEventLog().filter(event =>
event.event === EVENT_REPAIR
&& event.objectId === pending.creepId
&& event.data?.targetId === pending.targetId
);
if (matches.length === 0) {
return { status: 'accepted-repair-event-missing' };
}
if (matches.length > 1) {
return { status: 'repair-event-ambiguous', count: matches.length };
}
const event = matches[0];
const target = Game.getObjectById(pending.targetId);
return {
status: 'repair-event-observed',
amount: event.data?.amount ?? null,
energySpent: event.data?.energySpent ?? null,
hitsBefore: pending.hitsBefore,
hitsNow: target?.hits ?? null
};
}
The room event log is a one-tick evidence window. Run verification before submitting the next action for that Repairer, then clear or archive the pending record after producing a terminal result.
Use event amount without overclaiming
The official engine records the Repairer as objectId, the repaired structure as targetId, the repair output as amount, and consumed Energy as energySpent. That identity is stronger than a net hits delta. The structure may also receive Tower repair, another Creep repair, or damage during the same processed tick, so hitsNow - hitsBefore is not necessarily equal to this one event amount.
Keep missing and ambiguous states visible
| Status | Meaning | Next action |
|---|---|---|
stage-complete | No eligible structure is below its configured stage | Stop repairing; do not auto-raise the stage |
moving | No repair call was accepted | Inspect movement and retry from current state |
repair-rejected | The API rejected the current action | Use the saved return code |
accepted-repair-event-missing | An accepted call lacks the expected next-tick event | Keep the discrepancy; do not replace it with a hits guess |
repair-event-ambiguous | More than one exact match was found | Inspect duplicate dispatch or corrupted pending state |
repair-event-window-missed | Verification ran too late | Record the missed window and sample a later action |
Production integration boundary
Call the coordinator once per owned room and route all fortification Repairers through it. Keep emergency Tower logic, hostile response, repairer Energy supply and stage promotion in separate policies. Console execution, live multi-tick results and genuine room screenshots remain pending.
Official documentation
Review the official Screeps API for Creep.repair(), Room.getEventLog(), EVENT_REPAIR, Walls, Ramparts, body boosts and action return codes before adapting the examples.