Quick answer
When an owned room has no live harvesters, run a narrow emergency branch before normal replacement logic. Count current Creeps, choose one active idle Spawn, require enough current Energy for [WORK, CARRY, MOVE], generate a unique name, run dryRun, and submit exactly one real request. Do not let every Spawn independently react to the same zero-harvester condition.
Run emergency recovery before normal spawning
Clean dead Creep Memory
→ count current live roles
→ run emergency recovery
→ if no Spawn was used, run normal spawning
Normal spawning may wait for a larger harvester, Builder, Upgrader, hauler, or full energyCapacityAvailable. When Energy income has collapsed, those goals must yield to restoring the minimum harvesting loop.
Count current live harvesters
function countLiveCreepsByRole(role, roomName) {
return Object.values(Game.creeps)
.filter(creep =>
creep.memory?.role === role
&& (
creep.memory?.homeRoom === roomName
|| creep.room.name === roomName
)
)
.length;
}
Do not count only Memory.creeps; stale names may remain after death. See the cleanup guide. A larger system should also account for a harvester already being spawned so another Spawn does not submit a duplicate.
Use a minimum viable body
const EMERGENCY_BODY = [WORK, CARRY, MOVE];
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);
}
The current constants make this a 200-Energy body that can harvest, carry, and move. Deriving the threshold from the body keeps the check correct when the body changes.
Choose only one Spawn
This pattern is unsafe in a multi-Spawn room:
for (const spawn of Object.values(Game.spawns)) {
if (harvesterCount === 0) {
spawn.spawnCreep(body, name);
}
}
Each Spawn can see the same zero count. Select one active idle Spawn in a stable order:
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;
}
Name sorting is a deterministic tie-breaker, not a universal business priority.
Create a unique recovery name
function createEmergencyName(room, spawn) {
return [
'EmergencyHarvester',
room.name,
spawn.name,
Game.time
].join('-');
}
A unique name avoids obvious collisions but does not replace centralized Spawn coordination. Two independent modules should not both submit the same recovery role.
Complete emergency recovery example
State impact: this script can start one minimum harvester in an owned room. Replace W1N1 with the real room name. It returns immediately after a successful emergency submission so normal spawning cannot use another Spawn in the same branch.
const EMERGENCY_BODY = [WORK, CARRY, MOVE];
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 countLiveCreepsByRole(role, roomName) {
return Object.values(Game.creeps)
.filter(creep =>
creep.memory?.role === role
&& (
creep.memory?.homeRoom === roomName
|| creep.room.name === roomName
)
)
.length;
}
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 runEmergencyRecovery(room) {
if (!room?.controller?.my) {
return {
status: 'owned-room-unavailable'
};
}
const harvesterCount = countLiveCreepsByRole(
'harvester',
room.name
);
if (harvesterCount > 0) {
return {
status: 'harvester-exists',
harvesterCount
};
}
const spawn = selectEmergencySpawn(room);
if (!spawn) {
return {
status: 'spawn-unavailable'
};
}
const minimumCost = getBodyCost(EMERGENCY_BODY);
if (room.energyAvailable < minimumCost) {
return {
status: 'energy-not-enough',
energyAvailable: room.energyAvailable,
minimumCost
};
}
const name = createEmergencyName(room, spawn);
const memory = {
role: 'harvester',
homeRoom: room.name,
emergency: true,
memoryVersion: 1
};
const dryRunResult = spawn.spawnCreep(
EMERGENCY_BODY,
name,
{
memory,
dryRun: true
}
);
if (dryRunResult !== OK) {
return {
status: 'dry-run-failed',
spawnName: spawn.name,
name,
dryRunResult
};
}
const result = spawn.spawnCreep(
EMERGENCY_BODY,
name,
{ memory }
);
return {
status: result === OK
? 'emergency-spawn-submitted'
: 'emergency-spawn-failed',
spawnName: spawn.name,
name,
minimumCost,
dryRunResult,
result
};
}
module.exports.loop = function () {
const room = Game.rooms.W1N1;
if (!room) {
return;
}
const outcome = runEmergencyRecovery(room);
if (
outcome.status === 'dry-run-failed'
|| outcome.status === 'emergency-spawn-failed'
) {
console.log(JSON.stringify({
type: 'emergency-recovery-failed',
roomName: room.name,
...outcome
}));
}
if (outcome.status === 'emergency-spawn-submitted') {
return;
}
// Continue normal spawning only when the
// emergency branch did not use a Spawn.
};
Why the real result still matters after dryRun
A dry run does not start spawning. Another module can use the selected Spawn, consume Energy, or submit a conflicting name afterward. The real spawnCreep() result remains the final result for that request.
What happens below 200 Energy
An ordinary owned room with no harvesting workforce and less than the minimum body's current cost cannot create that body from nothing. Recovery may require a surviving hauler, support from another room, manual task reassignment, or ultimately a respawn decision.
The special initial Spawn safety refill
Official respawn documentation describes a special safety rule for an initial Spawn: it starts with 300 Energy and receives 1 Energy per tick until the room's Spawns and Extensions together hold 300. This is an initial-Spawn protection mechanism. Do not assume that every established room or reconstructed Spawn will regenerate Energy this way.
Debugging checklist
- Clean stale Creep Memory before role counts.
- Count current live Creeps in the intended room.
- Run emergency recovery before normal role priorities.
- Use a minimum body with the required abilities.
- Calculate its cost from
BODYPART_COST. - Select one active idle Spawn only.
- Generate a unique bounded name.
- Save dryRun and final results separately.
- Return after a successful emergency submission.
- Do not generalize the initial Spawn safety refill.
Scope and next steps
This article does not implement cross-room aid, Spawn queues, transporting Energy into a collapsed room, detecting a harvester currently in production, replacement lifetime prediction, or respawn automation. Those systems require a broader colony scheduler.
Frequently asked questions
Why run emergency recovery first?
Normal priorities may wait for larger bodies or less urgent roles while the room has no Energy income.
Why count Game.creeps?
It represents current Creeps; Memory may retain dead names.
Why choose only one Spawn?
Multiple idle Spawns can see the same zero-harvester state and submit duplicates.
Does every empty room refill Spawn Energy automatically?
No. The documented 1-Energy safety refill is specific to an initial Spawn.