LOGISTICS · STORAGE ENERGY RESERVE

How to Use Storage Energy Without Draining Your Reserve

Guard room.storage access, calculate withdrawable Energy above a configurable reserve, switch a hauler between withdrawal and delivery, select Spawn and Extension targets deterministically, handle action return codes, and verify same-tick capacity races.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — Room.storage, StructureStorage, Store, withdraw(), transfer() and return codes · Policy boundary: The Energy reserve and Spawn-first delivery order are explicit site strategies, not official optimal values

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — Room.storage, StructureStorage, Store, withdraw(), transfer() and return codes
Policy boundary
The Energy reserve and Spawn-first delivery order are explicit site strategies, not official optimal values
Execution boundary
OK schedules one action; Store and target capacity can still change through other same-tick operations
JavaScript syntax
Passed
Offline logistics review
Passed — missing Storage, reserve protection, Creep state, target capacity, amount and stable priority states
Screeps Console test
Pending
Live Storage reserve, pathing, same-tick capacity and delivery test
Pending
Last verified
July 26, 2026

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

CodeTypical causeResponse
OKAction scheduledVerify Store later
ERR_NOT_OWNERCreep not yoursCheck selected Creep
ERR_BUSYCreep spawningWait
ERR_NOT_ENOUGH_RESOURCESStorage amount changedRefresh stock and reserve
ERR_INVALID_TARGETWrong objectCheck Storage or target type
ERR_FULLCreep or target Store fullRefresh capacity
ERR_NOT_IN_RANGENot adjacentMove to range 1
ERR_INVALID_ARGSBad resource or amountValidate 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.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.