Use this guide when
Use this recovery branch when an owned visible room has no live general harvester and no matching recovery Creep is already being spawned. The example assumes a general harvester needs active WORK, CARRY, and MOVE. A fixed miner-and-hauler economy should define a different minimum contract instead of copying that assumption.
Choose the dynamic body guide when the room still has working Energy income and can wait for a normal target body. Choose the spawn return-code guide when one specific request is already failing.
Define what counts as recovered
A role label alone is not enough. A Creep whose active harvesting or delivery parts have been destroyed may still be present in Game.creeps but unable to restore the room's Energy loop.
const RECOVERY_ROLE = 'harvester';
const EMERGENCY_BODY = [WORK, CARRY, MOVE];
function isCapableGeneralHarvester(creep, roomName) {
return Boolean(
creep
&& creep.my === true
&& creep.spawning !== true
&& creep.memory?.role === RECOVERY_ROLE
&& creep.memory?.homeRoom === roomName
&& creep.getActiveBodyparts(WORK) > 0
&& creep.getActiveBodyparts(CARRY) > 0
&& creep.getActiveBodyparts(MOVE) > 0
);
}
This contract is project policy. It is intentionally explicit so that a damaged or differently assigned Creep does not silently block emergency recovery.
Use four recovery states
| State | Meaning | Next action |
|---|---|---|
capable-harvester-exists | The room already has a live Creep that satisfies the recovery contract. | Run normal spawning. |
recovery-spawning | The exact accepted recovery name is still present in spawn.spawning. | Do not submit another recovery request. |
recovery-creep-ready | The accepted name now exists in Game.creeps and satisfies the contract. | Clear the pending record and resume normal policy. |
recovery-needed | No capable live or in-production recovery Creep exists. | Choose one Spawn and submit one minimum request. |
Verify an accepted request before planning another
Save the exact Spawn ID and requested Creep name only after the real spawnCreep() call returns OK. On later ticks, recover current objects rather than retaining live references.
function verifyEmergencyRecovery(room) {
const pending = room?.memory?.emergencyRecovery;
if (!pending) {
return { status: 'no-pending-recovery' };
}
if (Game.time <= pending.submittedAt) {
return {
status: 'accepted-this-tick',
creepName: pending.creepName
};
}
const creep = Game.creeps[pending.creepName];
if (isCapableGeneralHarvester(creep, room.name)) {
delete room.memory.emergencyRecovery;
return {
status: 'recovery-creep-ready',
creepName: creep.name,
creepId: creep.id
};
}
const spawn = Game.getObjectById(pending.spawnId);
if (spawn?.spawning?.name === pending.creepName) {
return {
status: 'recovery-spawning',
creepName: pending.creepName,
remainingTime: spawn.spawning.remainingTime,
needTime: spawn.spawning.needTime
};
}
const latestExpectedTick =
pending.submittedAt
+ pending.bodyLength * CREEP_SPAWN_TIME
+ 1;
return {
status: Game.time > latestExpectedTick
? 'recovery-overdue'
: 'accepted-request-not-observed',
creepName: pending.creepName,
submittedAt: pending.submittedAt,
latestExpectedTick
};
}
bodyLength * CREEP_SPAWN_TIME is a base planning boundary. The current spawn.spawning.needTime and remainingTime are better live observations once the Spawn exposes them. A missing match must remain a failure state; do not erase it by submitting a second request immediately.
Submit one minimum recovery request
function getBodyCost(body) {
return body.reduce((total, part) => {
const cost = BODYPART_COST[part];
if (!Number.isFinite(cost)) {
throw new TypeError(
'Unknown body part: ' + String(part)
);
}
return total + cost;
}, 0);
}
function selectEmergencySpawn(room) {
return room.find(FIND_MY_SPAWNS)
.filter(spawn =>
spawn.my === true
&& spawn.isActive()
&& !spawn.spawning
)
.sort((left, right) =>
left.name.localeCompare(right.name)
)[0] ?? null;
}
function createEmergencyName(room, spawn) {
return [
'EmergencyHarvester',
room.name,
spawn.name,
Game.time
].join('-');
}
function submitEmergencyRecovery(room) {
if (!room?.controller?.my) {
return { status: 'owned-room-unavailable' };
}
const capable = Object.values(Game.creeps)
.filter(creep =>
isCapableGeneralHarvester(creep, room.name)
);
if (capable.length > 0) {
return {
status: 'capable-harvester-exists',
count: capable.length
};
}
const pendingState = verifyEmergencyRecovery(room);
if (
pendingState.status !== 'no-pending-recovery'
&& pendingState.status !== 'recovery-overdue'
) {
return pendingState;
}
const spawn = selectEmergencySpawn(room);
if (!spawn) {
return { status: 'spawn-unavailable' };
}
const minimumCost = getBodyCost(EMERGENCY_BODY);
if (room.energyAvailable < minimumCost) {
return {
status: 'energy-below-minimum',
energyAvailable: room.energyAvailable,
minimumCost
};
}
const creepName = createEmergencyName(room, spawn);
const memory = {
role: RECOVERY_ROLE,
homeRoom: room.name,
emergencyRecovery: true,
memoryVersion: 1
};
const dryRunResult = spawn.spawnCreep(
EMERGENCY_BODY,
creepName,
{
memory,
dryRun: true
}
);
if (dryRunResult !== OK) {
return {
status: 'dry-run-rejected',
spawnId: spawn.id,
creepName,
dryRunResult
};
}
const result = spawn.spawnCreep(
EMERGENCY_BODY,
creepName,
{ memory }
);
if (result !== OK) {
return {
status: 'spawn-rejected-after-dry-run',
spawnId: spawn.id,
creepName,
dryRunResult,
result
};
}
room.memory.emergencyRecovery = {
spawnId: spawn.id,
creepName,
submittedAt: Game.time,
bodyLength: EMERGENCY_BODY.length
};
return {
status: 'emergency-spawn-accepted',
spawnId: spawn.id,
creepName,
submittedAt: Game.time,
minimumCost,
dryRunResult,
result
};
}
The pending record is written after the final return code, not after the dry run. A dry run checks current conditions but does not reserve the Spawn, the name, or the room's Energy.
Put recovery ahead of normal Spawn policy
module.exports.loop = function () {
const room = Game.rooms.W1N1;
if (!room) {
return;
}
const outcome = submitEmergencyRecovery(room);
if (
outcome.status === 'emergency-spawn-accepted'
|| outcome.status === 'recovery-spawning'
|| outcome.status === 'accepted-this-tick'
|| outcome.status === 'accepted-request-not-observed'
) {
return;
}
if (
outcome.status === 'dry-run-rejected'
|| outcome.status === 'spawn-rejected-after-dry-run'
|| outcome.status === 'recovery-overdue'
) {
console.log(JSON.stringify({
type: 'emergency-recovery',
roomName: room.name,
...outcome
}));
return;
}
// Continue ordinary quotas only when recovery
// neither used nor owns a Spawn request.
};
Normal role quotas should not overwrite the diagnostic state of an accepted emergency request. Centralize Spawn submission if more than one module can reach this branch.
When the room is below the minimum cost
The current standard constants make [WORK, CARRY, MOVE] cost 200 Energy. An established room with no capable workforce and less than the configured minimum cannot create that body from nothing. Recovery may require a surviving hauler, a different valid role contract, Energy support from another room, manual reassignment, or a respawn decision.
The initial-Spawn safety refill is a special respawn protection. Do not generalize it to every established or rebuilt Spawn.
Interpret the current-tick result correctly
| Result | Meaning here | What to inspect next |
|---|---|---|
OK | The Spawn accepted this request. | The exact name in spawn.spawning and later Game.creeps. |
ERR_BUSY | Another request owns the Spawn. | Your centralized Spawn coordinator. |
ERR_NAME_EXISTS | The requested name already exists. | The pending record and naming policy. |
ERR_NOT_ENOUGH_ENERGY | Current Spawn-and-Extension Energy changed or was insufficient. | room.energyAvailable and competing calls. |
ERR_INVALID_ARGS | The body, name, or options are malformed. | The captured request snapshot. |
ERR_RCL_NOT_ENOUGH | The selected Spawn is inactive at this Controller level. | spawn.isActive() and ownership. |
Production adaptation notes
A mature colony should count capability groups rather than one role string, include remote miners and haulers only when they can actually restore this room, and let one Spawn scheduler own all final calls. Keep pending records bounded, retain a terminal failure long enough to diagnose it, and do not report recovery complete until the new Creep exists and satisfies the declared contract.
Verification status and evidence boundary
The code and API boundaries were checked statically and the pure decision branches were reviewed offline. No real room collapse, Spawn contention, powered spawning, Console output, or live multi-tick recovery sequence was available. Those checks remain pending.