Quick answer
A dynamic body should not simply spend every available Energy unit. Start with a role-specific minimum unit, calculate its cost from BODYPART_COST, repeat it only while current room.energyAvailable and the 50-part limit permit, and apply a role-specific maximum. Keep the body-planning function separate from the decision to spawn now or wait for more Energy.
energyAvailable versus energyCapacityAvailable
| Property | Meaning | Typical policy |
|---|---|---|
room.energyAvailable | Energy currently loaded in all room Spawns and Extensions. | Spawn the best affordable body now. |
room.energyCapacityAvailable | Total capacity of those Spawns and Extensions. | Wait until enough Energy is loaded for the desired body. |
This example uses current energyAvailable, which is useful for emergency or degraded spawning. A normal replacement system may prefer to wait when current workers still have enough lifetime.
Define one role-specific body unit
const WORKER_UNIT = [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 unit costs 200 Energy using current official constants. The code derives that value instead of scattering the number across multiple branches.
Why the example stops at 16 units
16 units × 3 parts = 48 parts
17 units × 3 parts = 51 parts
A regular Creep body can contain at most 50 parts. Therefore this three-part pattern can repeat at most 16 times, even when the room can afford more.
Pure repeated-body builder
function buildRepeatedBody(input) {
const {
energyAvailable,
unit,
maximumParts = 50,
maximumUnits = Infinity
} = input;
if (
!Number.isFinite(energyAvailable)
|| energyAvailable < 0
|| !Array.isArray(unit)
|| unit.length === 0
|| !Number.isInteger(maximumParts)
|| maximumParts < 1
|| (
maximumUnits !== Infinity
&& !Number.isFinite(maximumUnits)
)
|| maximumUnits < 0
) {
return {
valid: false,
reason: 'invalid-input',
body: []
};
}
const unitCost = getBodyCost(unit);
const unitsByEnergy = Math.floor(
energyAvailable / unitCost
);
const unitsByParts = Math.floor(
maximumParts / unit.length
);
const units = Math.max(
0,
Math.min(
unitsByEnergy,
unitsByParts,
Math.floor(maximumUnits)
)
);
const body = [];
for (let index = 0; index < units; index += 1) {
body.push(...unit);
}
return {
valid: true,
reason: body.length > 0
? 'ready'
: 'energy-below-minimum',
body,
units,
unitCost,
bodyCost: units * unitCost,
spawnTime: body.length * CREEP_SPAWN_TIME
};
}
The function uses only input values and official constants. That makes boundary tests independent of a live room.
Calculate spawn time
CREEP_SPAWN_TIME is the base number of ticks per body part. The plan reports:
const spawnTime = body.length * CREEP_SPAWN_TIME;
A 48-part Creep occupies a Spawn much longer than a three-part Creep. Larger bodies therefore require earlier replacement scheduling.
Body order changes damage behavior
These bodies have equal cost and part counts:
[WORK, CARRY, MOVE, WORK, CARRY, MOVE]
[WORK, WORK, CARRY, CARRY, MOVE, MOVE]
They do not lose abilities in the same order when damaged because earlier parts absorb damage first. Repeating a unit preserves a visible ratio but is not universally optimal. Haulers, fixed miners, upgraders, and combat Creeps need different ordering rules.
Complete dynamic spawn example
State impact: the planning functions do not mutate the game. The final call may start one Worker when no current Worker exists and the Spawn is idle.
const WORKER_UNIT = [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 buildRepeatedBody(input) {
const {
energyAvailable,
unit,
maximumParts = 50,
maximumUnits = Infinity
} = input;
if (
!Number.isFinite(energyAvailable)
|| energyAvailable < 0
|| !Array.isArray(unit)
|| unit.length === 0
) {
return {
valid: false,
reason: 'invalid-input',
body: []
};
}
const unitCost = getBodyCost(unit);
const units = Math.max(
0,
Math.min(
Math.floor(energyAvailable / unitCost),
Math.floor(maximumParts / unit.length),
Math.floor(maximumUnits)
)
);
const body = [];
for (let index = 0; index < units; index += 1) {
body.push(...unit);
}
return {
valid: true,
reason: body.length > 0
? 'ready'
: 'energy-below-minimum',
body,
units,
unitCost,
bodyCost: units * unitCost,
spawnTime: body.length * CREEP_SPAWN_TIME
};
}
function createUniqueName(spawn, role) {
return [role, spawn.name, Game.time].join('-');
}
module.exports.loop = function () {
const spawn = Game.spawns.Spawn1;
if (!spawn || spawn.spawning) {
return;
}
const existingWorkers = Object.values(Game.creeps)
.filter(creep =>
creep.memory?.role === 'worker'
);
if (existingWorkers.length > 0) {
return;
}
const plan = buildRepeatedBody({
energyAvailable: spawn.room.energyAvailable,
unit: WORKER_UNIT,
maximumParts: 50,
maximumUnits: 5
});
if (!plan.valid || plan.body.length === 0) {
return;
}
const name = createUniqueName(spawn, 'Worker');
const memory = {
role: 'worker',
bodyUnits: plan.units,
memoryVersion: 1
};
const dryRunResult = spawn.spawnCreep(
plan.body,
name,
{
memory,
dryRun: true
}
);
if (dryRunResult !== OK) {
console.log(JSON.stringify({
type: 'dynamic-body-dry-run-failed',
spawnName: spawn.name,
name,
dryRunResult,
plan
}));
return;
}
const result = spawn.spawnCreep(
plan.body,
name,
{ memory }
);
if (result !== OK) {
console.log(JSON.stringify({
type: 'dynamic-body-spawn-failed',
spawnName: spawn.name,
name,
result,
plan
}));
}
};
Set a role-specific maximum
A room that can afford 48 parts does not necessarily need one 48-part general Worker. Use a project value such as:
maximumUnits: 5
Reasons include spawn time, movement speed, parallelism, the need to reserve Energy for other roles, and task throughput. This maximum is a project policy, not an official Screeps value.
When to spawn now or wait
- No harvesters remain: spawn the minimum viable recovery body now.
- A Builder quota is slightly low: waiting for a larger body may be reasonable.
- Defense is urgent: use a current-Energy fallback plan.
- Existing workers have long lifetime: wait for the desired capacity.
The body builder answers “what can I afford?” It does not answer “should I spawn this tick?”
Return-code checks
Handle ERR_BUSY, ERR_NAME_EXISTS, ERR_NOT_ENOUGH_ENERGY, ERR_INVALID_ARGS, ERR_NOT_OWNER, and ERR_RCL_NOT_ENOUGH. spawnCreep() does not return ERR_NOT_IN_RANGE. Use the focused return-code guide for full diagnostics.
Debugging checklist
- Choose between current Energy and waiting for capacity deliberately.
- Define a minimum body that can perform the role.
- Calculate cost from
BODYPART_COST. - Enforce the 50-part limit.
- Set a role-specific maximum.
- Report body cost, unit count, and spawn time.
- Consider body ordering under damage.
- Keep body planning separate from spawn timing.
- Use dryRun before the real request.
- Save the final return code.
Scope and next steps
This article does not optimize specialized miners, haulers, combat bodies, boosts, road movement, lifetime replacement lead time, or multi-Spawn queues. Continue with emergency recovery when no harvester remains.
Frequently asked questions
What is energyAvailable versus energyCapacityAvailable?
The first is current loaded Energy; the second is total room spawning capacity.
Why only 16 three-part units?
Sixteen use 48 parts. Seventeen would use 51 and exceed the limit.
Should the body spend every Energy unit?
No. Valid role capability and configured limits matter more than eliminating every remainder.
Does this function decide when to spawn?
No. It produces a plan; room policy decides whether to use it now.