Quick answer
An unboosted Creep's movement interval depends on two values: the fatigue generated by the body weight on the tile being entered, and the fatigue removed by active MOVE parts. Roads cost 1 fatigue per weight part, plains cost 2, and swamps cost 10. Each active unboosted MOVE part removes 2 fatigue per tick. Empty CARRY parts do not add weight.
The four movement rules
- Every non-MOVE weight part generates fatigue when the Creep moves.
- A road uses cost 1, a plain uses cost 2, and a swamp uses cost 10.
- Each active unboosted MOVE part removes 2 fatigue per tick.
- A Creep with positive fatigue cannot submit ordinary movement successfully.
The maximum normal speed is one tile per tick. Extra MOVE capacity beyond the amount required for zero carried fatigue does not create two-tile movement.
Count movement weight correctly
For an unboosted estimate, weight contains every non-MOVE body part except empty CARRY capacity. A loaded CARRY part counts when some resource occupies its 50-capacity segment.
function countLoadedCarryParts(
activeCarryParts,
usedCapacity
) {
if (
!Number.isInteger(activeCarryParts)
|| activeCarryParts < 0
|| !Number.isFinite(usedCapacity)
|| usedCapacity <= 0
) {
return 0;
}
return Math.min(
activeCarryParts,
Math.ceil(usedCapacity / CARRY_CAPACITY)
);
}
Examples for three unboosted CARRY parts:
| Used capacity | Loaded CARRY parts |
|---|---|
| 0 | 0 |
| 1–50 | 1 |
| 51–100 | 2 |
| 101–150 | 3 |
Calculate ticks per step
fatigue generated = weight parts × terrain cost
fatigue removed per tick = active MOVE parts × 2
estimated ticks per step = ceil(
fatigue generated / fatigue removed per tick
)
The result is never less than one tick. A Creep must have at least one active MOVE part to move under its own power.
const TERRAIN_COST = {
road: 1,
plain: 2,
swamp: 10
};
function estimateTicksPerStep(input) {
const {
activeMoveParts,
weightParts,
terrain
} = input;
const terrainCost = TERRAIN_COST[terrain];
if (
!Number.isInteger(activeMoveParts)
|| activeMoveParts <= 0
|| !Number.isInteger(weightParts)
|| weightParts < 0
|| !Number.isInteger(terrainCost)
) {
return {
movable: false,
reason: 'invalid-input'
};
}
const fatigueGenerated = weightParts * terrainCost;
const fatigueRemovedPerTick = activeMoveParts * 2;
return {
movable: true,
reason: 'estimated',
fatigueGenerated,
fatigueRemovedPerTick,
ticksPerStep: Math.max(
1,
Math.ceil(
fatigueGenerated / fatigueRemovedPerTick
)
)
};
}
Road, plain, and swamp examples
Consider a fully loaded [WORK, CARRY, MOVE] Creep. It has two weight parts and one MOVE part.
| Terrain | Generated | Removed per tick | Estimated interval |
|---|---|---|---|
| Road | 2 × 1 = 2 | 2 | 1 tick per step |
| Plain | 2 × 2 = 4 | 2 | 2 ticks per step |
| Swamp | 2 × 10 = 20 | 2 | 10 ticks per step |
Adding a second MOVE part changes the plain result to one tile per tick, but the same body still needs several ticks per swamp step.
Why empty CARRY parts change the result
[CARRY, CARRY, MOVE] has zero movement weight while empty, one weight part with up to 50 resources, and two weight parts above 50 resources. A hauler can therefore travel outward quickly and return more slowly when loaded.
Do not count every CARRY part automatically. Also do not assume all resources are energy; creep.store.getUsedCapacity() counts the complete load.
Complete unboosted movement calculator
State impact: this diagnostic reads the body and Store and returns an estimate. It does not move the Creep or write Memory.
const TERRAIN_COST = {
road: 1,
plain: 2,
swamp: 10
};
function countLoadedCarryParts(creep) {
const usedCapacity =
creep.store.getUsedCapacity() ?? 0;
const activeCarryParts =
creep.getActiveBodyparts(CARRY);
if (usedCapacity <= 0) {
return 0;
}
return Math.min(
activeCarryParts,
Math.ceil(usedCapacity / CARRY_CAPACITY)
);
}
function countWeightParts(creep) {
const activeOtherParts = creep.body.filter(
part =>
part.hits > 0
&& part.type !== MOVE
&& part.type !== CARRY
).length;
return activeOtherParts
+ countLoadedCarryParts(creep);
}
function estimateCreepMovement(creep, terrain) {
if (!creep || creep.spawning) {
return {
status: 'creep-unavailable'
};
}
const terrainCost = TERRAIN_COST[terrain];
const activeMoveParts =
creep.getActiveBodyparts(MOVE);
const weightParts = countWeightParts(creep);
if (!Number.isInteger(terrainCost)) {
return {
status: 'terrain-invalid'
};
}
if (activeMoveParts <= 0) {
return {
status: 'no-active-move-part',
activeMoveParts,
weightParts,
currentFatigue: creep.fatigue
};
}
const fatigueGenerated =
weightParts * terrainCost;
const fatigueRemovedPerTick =
activeMoveParts * 2;
return {
status: 'movement-estimated',
terrain,
activeMoveParts,
weightParts,
currentFatigue: creep.fatigue,
fatigueGenerated,
fatigueRemovedPerTick,
estimatedTicksPerStep: Math.max(
1,
Math.ceil(
fatigueGenerated / fatigueRemovedPerTick
)
)
};
}
module.exports.loop = function () {
const creep = Game.creeps.Hauler1;
if (!creep || Game.time % 25 !== 0) {
return;
}
console.log(JSON.stringify({
type: 'movement-estimate',
creepName: creep.name,
...estimateCreepMovement(creep, 'plain')
}));
};
The terrain argument describes the route segment being evaluated. Do not label one current-tile snapshot as a measured route speed.
Practical body ratios
| Expected terrain | Unboosted maximum-speed target |
|---|---|
| Mostly roads | 1 MOVE per 2 loaded weight parts |
| Mostly plains | 1 MOVE per 1 loaded weight part |
| Mostly swamps | 5 MOVE per 1 loaded weight part |
Swamp-speed bodies are often impractical. Roads, route changes, reduced payload, or MOVE boosts may be cheaper than adding enough MOVE parts for one-tile-per-tick swamp travel.
Why damage can slow a Creep
Use getActiveBodyparts(MOVE), not the original body array length. A MOVE part at zero hits no longer contributes movement recovery. The same body can therefore move normally before combat and accumulate fatigue afterward.
Live diagnosis should record active MOVE parts, Store usage, fatigue, terrain, and position over multiple ticks.
Terrain-sampling correction
The Chinese source's complete diagnostic read the Creep's current tile and used that value as the movement estimate. A route can enter a different terrain type on the next step. This English version requires the terrain being evaluated as an explicit input and labels the result as an estimate rather than a measured next-step fact.
Debugging checklist
- Use active MOVE parts, not the original count.
- Count only loaded CARRY capacity as weight.
- Use costs 1, 2, and 10 for road, plain, and swamp.
- Keep unboosted and boosted formulas separate.
- Include current fatigue in live logs.
- Evaluate the terrain of the route segment, not only the current tile.
- Do not confuse a static estimate with multi-tick proof.
- Check traffic and overwritten movement orders before adding MOVE parts.
Scope and next steps
This guide excludes MOVE boosts, pull chains, Power Creeps, traffic reservations, mixed-terrain route statistics, and live server timing. Continue with RoomPosition distance methods before using range as a path estimate.
Frequently asked questions
What is the plain-terrain ratio?
One active unboosted MOVE part per loaded weight part supports maximum normal speed on plains.
Why is an empty hauler faster?
Empty CARRY parts do not generate fatigue.
Does more MOVE exceed one tile per tick?
No. It can remove fatigue faster, but ordinary movement remains capped at one tile per tick.
Is the formula a live test?
No. Observe several ticks to include traffic, fatigue history, terrain changes, and command conflicts.