Quick answer
Check that the room is visible and room.storage exists. When the hauler is empty, calculate only the Energy above a configurable reserve and cap the amount by Creep free capacity. When the hauler carries Energy, select a valid Spawn or Extension with free capacity using a stable priority rule, call transfer(), and preserve the carried Energy when no target exists. Treat every reserve and priority value as your own policy.
Guard room.storage access
function getVisibleStorage(roomName) {
if (typeof roomName !== 'string') {
return null;
}
const room = Game.rooms[roomName];
if (!room || !room.storage) {
return null;
}
return room.storage;
}
room.storage is a convenient property only for a currently visible room. A room can be valid while having no completed Storage, so do not chain directly into room.storage.store without a guard.
Calculate Energy above the reserve
A reserve prevents a general-purpose hauler from consuming the room's entire long-term stock.
function getStorageWithdrawableEnergy(input) {
const {
storageEnergy,
reserveEnergy,
creepFreeCapacity
} = input;
if (
!Number.isFinite(storageEnergy)
|| !Number.isFinite(reserveEnergy)
|| !Number.isFinite(creepFreeCapacity)
|| storageEnergy < 0
|| reserveEnergy < 0
|| creepFreeCapacity <= 0
) {
return 0;
}
return Math.min(
Math.max(0, storageEnergy - reserveEnergy),
creepFreeCapacity
);
}
For example, Storage with 25,000 Energy and a 20,000 reserve exposes at most 5,000 Energy to this task. The Creep's capacity may reduce the requested amount further.
const STORAGE_ENERGY_RESERVE = 20000;
This number is an example, not an official threshold. A defensive room, an upgrader rush, or a Terminal hub may need a different reserve.
Use a simple hauler state machine
The smallest useful state can come directly from the Creep Store:
function getStorageHaulerMode(creep) {
if (!creep) {
return 'missing';
}
return creep.store.getUsedCapacity(
RESOURCE_ENERGY
) === 0
? 'withdraw'
: 'deliver';
}
As long as the Creep still carries Energy, it remains in delivery mode. This avoids turning around after a partial transfer. A multi-resource hauler needs an explicit task resource rather than relying on Energy alone.
Choose delivery targets deterministically
This guide uses Spawn before Extension, then range, then structure ID. The order is a transparent baseline, not a universal room strategy.
const ENERGY_TARGET_PRIORITY = {
[STRUCTURE_SPAWN]: 0,
[STRUCTURE_EXTENSION]: 1
};
function selectStorageEnergyTarget(creep) {
const candidates = creep.room.find(
FIND_MY_STRUCTURES,
{
filter: structure => {
const priority = ENERGY_TARGET_PRIORITY[
structure.structureType
];
const free = structure.store
? structure.store.getFreeCapacity(
RESOURCE_ENERGY
)
: 0;
return Number.isInteger(priority)
&& Number.isFinite(free)
&& free > 0;
}
}
);
return candidates.sort((left, right) => {
const priorityDifference =
ENERGY_TARGET_PRIORITY[left.structureType]
- ENERGY_TARGET_PRIORITY[right.structureType];
if (priorityDifference !== 0) {
return priorityDifference;
}
const rangeDifference =
creep.pos.getRangeTo(left)
- creep.pos.getRangeTo(right);
if (rangeDifference !== 0) {
return rangeDifference;
}
return left.id.localeCompare(right.id);
})[0] || null;
}
Stable tie-breaking matters because a different target every tick can produce unnecessary path churn. A production room may add Towers, Labs, Power Spawn or Controller logistics through a higher-level dispatcher.
Complete Storage hauler example
function runStorageEnergyHauler(creep, reserveEnergy) {
if (!creep || creep.spawning === true) {
return { status: 'creep-unavailable' };
}
const storage = creep.room.storage;
if (!storage) {
return { status: 'storage-missing' };
}
const carriedEnergy = creep.store.getUsedCapacity(
RESOURCE_ENERGY
);
if (carriedEnergy === 0) {
const storageEnergy = storage.store.getUsedCapacity(
RESOURCE_ENERGY
);
const amount = getStorageWithdrawableEnergy({
storageEnergy,
reserveEnergy,
creepFreeCapacity: creep.store.getFreeCapacity(
RESOURCE_ENERGY
)
});
if (amount <= 0) {
return {
status: 'storage-reserve-protected',
storageEnergy,
reserveEnergy
};
}
const result = creep.withdraw(
storage,
RESOURCE_ENERGY,
amount
);
if (result === ERR_NOT_IN_RANGE) {
return {
status: 'moving-to-storage',
amount,
result,
moveResult: creep.moveTo(storage, {
range: 1,
reusePath: 10
})
};
}
return {
status: result === OK
? 'withdraw-scheduled'
: 'withdraw-rejected',
amount,
result,
storageEnergyBefore: storageEnergy
};
}
const target = selectStorageEnergyTarget(creep);
if (!target) {
return {
status: 'delivery-target-not-found',
carriedEnergy
};
}
const targetFree = target.store.getFreeCapacity(
RESOURCE_ENERGY
);
const amount = Math.min(carriedEnergy, targetFree);
if (!Number.isFinite(amount) || amount <= 0) {
return {
status: 'target-full',
targetId: target.id
};
}
const result = creep.transfer(
target,
RESOURCE_ENERGY,
amount
);
if (result === ERR_NOT_IN_RANGE) {
return {
status: 'moving-to-target',
targetId: target.id,
amount,
result,
moveResult: creep.moveTo(target, {
range: 1,
reusePath: 10
})
};
}
return {
status: result === OK
? 'transfer-scheduled'
: 'transfer-rejected',
targetId: target.id,
targetType: target.structureType,
targetFreeBefore: targetFree,
carriedEnergyBefore: carriedEnergy,
amount,
result
};
}
module.exports.loop = function () {
const creep = Game.creeps.Hauler1;
if (!creep) {
return;
}
const outcome = runStorageEnergyHauler(
creep,
STORAGE_ENERGY_RESERVE
);
if (
outcome.status === 'withdraw-rejected'
|| outcome.status === 'transfer-rejected'
|| Game.time % 100 === 0
) {
console.log(JSON.stringify({
type: 'storage-energy-hauler',
creepName: creep.name,
roomName: creep.room.name,
...outcome
}));
}
};
Handle same-tick capacity changes
Multiple haulers can inspect the same free capacity before any action settles. Their preflight amounts may therefore add up to more than the target can accept. Keep every return code, avoid printing success from preflight alone, and consider a room-level reservation map when multiple workers share targets.
function reserveEnergyCapacity(
reservations,
targetId,
requested,
currentFree
) {
const alreadyReserved = reservations[targetId] || 0;
const available = Math.max(
0,
currentFree - alreadyReserved
);
const amount = Math.min(requested, available);
reservations[targetId] = alreadyReserved + amount;
return amount;
}
A reservation is local coordination. It does not lock the official Store and must be rebuilt from current tick state.
Verify Store changes later
For withdrawal, save the Storage Energy and Creep Energy before the call. For delivery, save the target ID, target free capacity, Creep Energy and requested amount. On the next tick compare the recovered objects, while accounting for other room logistics.
function snapshotEnergyTransfer(creep, target) {
return {
gameTick: Game.time,
creepName: creep.name,
creepEnergy: creep.store.getUsedCapacity(
RESOURCE_ENERGY
),
targetId: target.id,
targetEnergy: target.store.getUsedCapacity(
RESOURCE_ENERGY
),
targetFree: target.store.getFreeCapacity(
RESOURCE_ENERGY
)
};
}
Handle return codes
| Code | Typical cause | Response |
|---|---|---|
OK | Action scheduled | Verify Store later |
ERR_NOT_OWNER | Creep not yours | Check selected Creep |
ERR_BUSY | Creep spawning | Wait |
ERR_NOT_ENOUGH_RESOURCES | Storage amount changed | Refresh stock and reserve |
ERR_INVALID_TARGET | Wrong object | Check Storage or target type |
ERR_FULL | Creep or target Store full | Refresh capacity |
ERR_NOT_IN_RANGE | Not adjacent | Move to range 1 |
ERR_INVALID_ARGS | Bad resource or amount | Validate amount |
Debugging checklist
- Confirm the room is visible.
- Guard
room.storage. - Read current Storage Energy.
- Calculate Energy above the configured reserve.
- Cap the amount by Creep free capacity.
- Use a stable target priority.
- Check target Energy free capacity.
- Keep the action return code.
- Coordinate same-tick reservations when needed.
- Verify Store changes on the next tick.
Scope and next steps
This example fills Spawn and Extension only. It does not optimize roads, Towers, Links, Labs, Factory, Terminal, multi-room hauling, emergency priorities or multiple resource types. Continue with Power Spawn processing for a controlled long-running Energy consumer.
Frequently asked questions
Why keep carried Energy when no target exists?
Silently dropping or returning it would add an unstated policy. The caller can decide whether to wait, fill another structure, or return to Storage.
Why prioritize Spawn before Extension?
It is a simple documented baseline. Your room scheduler may choose different priorities based on defense, spawning queues or distance.
Can two haulers both pass the capacity check?
Yes. They read current state before actions settle. Use return codes and optional local reservations to coordinate them.