ROOM ECONOMY · CONTAINER DECAY

Screeps Container Decay: Repair Before the Next Fatal Tick

Verification statusOfficial documentation: Checked August 18, 2026 — Container decay, Creep.repair(), range, scheduled return code, constants, and previous-tick EVENT_REPAIR fields · Engine source: screeps/engine 4.3.2 · 80977824199a596d174d392fd0cf8c458c21fcbd · Static code review: Passed — decay runway, repair-submission tick, identity checks, incomplete-path boundary, partial-action Energy estimate, exact event identity, and event-window handling

VERIFICATION

Evidence and test status

Official documentation
Checked August 18, 2026 — Container decay, Creep.repair(), range, scheduled return code, constants, and previous-tick EVENT_REPAIR fields
Engine source
screeps/engine 4.3.2 · 80977824199a596d174d392fd0cf8c458c21fcbd
Static code review
Passed — decay runway, repair-submission tick, identity checks, incomplete-path boundary, partial-action Energy estimate, exact event identity, and event-window handling
Live same-tick verification
Pending — no official-shard fatal-pulse repair trace or boosted WORK event transcript was collected

ticksToDecay is the next decay pulse, not the whole lifetime

StructureContainer.ticksToDecay tells you how many ticks remain before the Container's next decay pulse. It is not a countdown to guaranteed destruction. A Container with one tick left and plenty of hits can survive that pulse and receive another decay interval.

The current public constants are CONTAINER_HITS = 250000, CONTAINER_DECAY = 5000, CONTAINER_DECAY_TIME = 100, and CONTAINER_DECAY_TIME_OWNED = 500. The current engine chooses the owned interval when the room has a Controller with a level above zero. Private servers can change constants, so production code should read the globals instead of scattering copied numbers.

Estimate the visible decay runway, then invalidate it when state changes

If nothing else damages or repairs the Container and the room's decay interval stays the same, the visible state gives you a useful estimate of how many decay pulses remain:

function estimateContainerRunway(container, room) {
  const decayEventsUntilLoss = Math.ceil(
    container.hits / CONTAINER_DECAY
  );

  const interval = room.controller?.level > 0
    ? CONTAINER_DECAY_TIME_OWNED
    : CONTAINER_DECAY_TIME;

  return {
    nextDecayFatal: container.hits <= CONTAINER_DECAY,
    decayEventsUntilLoss,
    estimatedTicksUntilLoss:
      container.ticksToDecay
      + (decayEventsUntilLoss - 1) * interval
  };
}

This is a state-derived forecast, not a timer reservation. Incoming damage, another repairer, a change in room control, a private-server constant, or losing visibility can invalidate an older estimate. Re-resolve the Container by ID and recalculate before making the next maintenance decision.

Reserve a repair submission tick after the travel lower bound

Creep.repair() works at range 3. If a repairer is outside that range, movement consumes earlier tick opportunities; reaching range 3 does not let later JavaScript in the same tick observe the moved position. Your deadline therefore needs room for the movement lower bound and at least one tick window in which the repair intent can be submitted before the dangerous pulse.

function classifyRepairDeadline(container, pathResult, safetyTicks) {
  if (!pathResult || pathResult.incomplete) {
    return { actionable: false, reason: 'incomplete-path' };
  }

  const travelLowerBound = pathResult.path.length;
  const repairSubmissionTicks = 1;
  const normalizedSafetyTicks =
    Number.isInteger(safetyTicks) && safetyTicks >= 0
      ? safetyTicks
      : 1;

  const repairSubmissionSlack =
    container.ticksToDecay
    - travelLowerBound
    - repairSubmissionTicks
    - normalizedSafetyTicks;

  return {
    actionable: repairSubmissionSlack >= 0,
    reason: repairSubmissionSlack >= 0
      ? 'repair-window-fits'
      : 'repair-window-misses-deadline',
    travelLowerBound,
    repairSubmissionTicks,
    safetyTicks: normalizedSafetyTicks,
    repairSubmissionSlack
  };
}

This catches a dangerous off-by-one case. With zero policy safety margin, ticksToDecay = 1 and a one-step path is already too late: the current tick can submit movement, but the Creep cannot use its post-move position to submit an in-range repair before that pulse. A Creep already in range has a travel lower bound of zero, which is the separate same-tick edge case discussed below.

Do not turn an incomplete PathFinder result into a numeric ETA. Even a complete path length is only a lower bound unless your movement model also accounts for fatigue, terrain, traffic, hostile blockers, Ramparts, room edges, and route changes.

Rank the most urgent Container without target churn

Current hits alone are a weak priority signal. Prefer a Container whose next pulse is fatal, then smaller repair-submission slack, shorter estimated lifetime, lower hits, shorter travel lower bound, and finally a stable ID. Persist the selected Container ID while the assignment remains valid instead of re-ranking every tick and making the repairer oscillate between similar targets.

A maintenance policy does not have to repair every Container to full health. You can target a local hit ratio plus a buffer of several decay pulses. That ratio and buffer are project policy, not official Screeps recommendations; name them as policy so readers do not confuse your risk tolerance with an engine rule.

Submit one repair decision and preserve the raw result

Before calling repair(), re-resolve the exact Container ID and validate the Creep. Fail closed if the supplied Creep is not yours or the stored ID no longer resolves to a Container. The repairer should be fully spawned, have active WORK parts, carry Energy, and be within range 3. If it is outside range, preserve the movement result separately and do not also label the repair as successful.

function submitContainerRepair(creep, containerId) {
  if (!creep?.my) return { status: 'not-owned-creep' };

  const container = Game.getObjectById(containerId);
  if (!container || container.structureType !== STRUCTURE_CONTAINER) {
    return { status: 'not-container' };
  }
  if (creep.spawning) return { status: 'creep-spawning' };
  if (creep.getActiveBodyparts(WORK) <= 0) {
    return { status: 'no-active-work' };
  }
  if (creep.store.getUsedCapacity(RESOURCE_ENERGY) <= 0) {
    return { status: 'no-energy' };
  }

  const range = creep.pos.getRangeTo(container);
  if (!Number.isFinite(range)) {
    return { status: 'invalid-range' };
  }

  if (range > 3) {
    const moveResult = creep.moveTo(container, { range: 3 });
    return { status: 'move-submitted', moveResult };
  }

  const repairResult = creep.repair(container);
  return {
    status: repairResult === OK
      ? 'repair-scheduled'
      : 'repair-rejected',
    repairResult,
    pending: repairResult === OK
      ? {
          tick: Game.time,
          roomName: creep.room.name,
          repairerId: creep.id,
          containerId: container.id,
          hitsBefore: container.hits,
          energyBefore:
            creep.store.getUsedCapacity(RESOURCE_ENERGY)
        }
      : null
  };
}

OK is submission evidence. It does not by itself prove how many hits were repaired, how much Energy the processor spent, or whether a later decay pulse still destroyed the Container.

Current-engine same-tick ordering is useful context, not an API contract

In the checked screeps/engine 4.3.2 room processor, Creep intents are processed before the later object-tick pass that applies Container decay. The checked repair processor can therefore add repair hits before that Container's decay handler runs in the same processor cycle.

This matters for the already-in-range edge case: if a Creep starts the tick within repair range and its repair is still valid, current-engine ordering can raise the Container above a fatal decay threshold before the decay pass. This does not rescue a Creep that still needs to move into range during that same tick, because action checks use the tick's starting position snapshot.

The ordering statement is an implementation observation from the checked engine revision, not a documented API guarantee for every future engine version or private server. Do not build normal maintenance around a zero-margin rescue; keep a positive safety buffer. Live official-shard evidence for the exact fatal-pulse ordering remains pending.

Verify the processed repair on the exact next tick

Room.getEventLog() returns events from the previous tick. Use that exact window to separate a scheduled repair from the amount actually processed: match EVENT_REPAIR by both the Creep ID and Container ID, then retain event.data.amount and event.data.energySpent.

function verifyPreviousRepair(pending) {
  if (!pending) return { status: 'no-pending-repair' };

  if (pending.tick !== Game.time - 1) {
    return {
      status: 'event-window-missed',
      submittedTick: pending.tick,
      observedTick: Game.time
    };
  }

  const room = Game.rooms[pending.roomName];
  if (!room) {
    return { status: 'room-not-visible' };
  }

  const event = room.getEventLog().find(candidate =>
    candidate.event === EVENT_REPAIR
    && candidate.objectId === pending.repairerId
    && candidate.data?.targetId === pending.containerId
  );

  const container = Game.getObjectById(pending.containerId);

  if (!event) {
    return {
      status: 'repair-event-not-found',
      containerExists: Boolean(container),
      hitsNow: container?.hits ?? null
    };
  }

  return {
    status: 'repair-event-observed',
    processedHits: event.data?.amount ?? null,
    energySpent: event.data?.energySpent ?? null,
    containerExists: Boolean(container),
    hitsBefore: pending.hitsBefore,
    hitsNow: container?.hits ?? null,
    energyBefore: pending.energyBefore
  };
}

A matching event proves that the checked actor processed a repair against the checked target in that event window. It does not prove the Container survived the rest of the processor cycle. If the event exists but the Container is absent on the next tick, preserve both facts: the repair processed, and the target was no longer present when observed.

Net hit change is supporting context only. Container decay, hostile damage, another Creep, or a Tower can offset the final hits. Missing the exact event-log observation window is an evidence gap, not proof that the repair failed.

Estimate unboosted Energy without charging the final partial action as a full action

For an unboosted Creep, each active WORK part contributes REPAIR_POWER base repair hits per action. The processor caps a repair by the target's missing hits and available Energy, then rounds the Energy spent for the repair effect. That means a final partial repair can cost less than another full repair action.

function estimateUnboostedRepairEnergy(
  missingHits,
  activeWorkParts
) {
  if (!Number.isFinite(missingHits) || missingHits <= 0) {
    return 0;
  }
  if (!Number.isInteger(activeWorkParts) || activeWorkParts <= 0) {
    return Infinity;
  }

  const perActionHits = activeWorkParts * REPAIR_POWER;
  const fullActions = Math.floor(missingHits / perActionHits);
  const finalPartialHits = missingHits % perActionHits;

  const fullActionEnergy = Math.ceil(
    perActionHits * REPAIR_COST
  );
  const finalPartialEnergy = finalPartialHits > 0
    ? Math.ceil(finalPartialHits * REPAIR_COST)
    : 0;

  return fullActions * fullActionEnergy + finalPartialEnergy;
}

For example, two unboosted WORK parts can repair up to 200 hits in one action. Repairing 201 missing hits takes one full 200-hit action and one 1-hit partial action; with the current public constants, that is 2 Energy plus 1 Energy, not two full 2-Energy actions.

Boosted WORK needs a separate model. In the checked 4.3.2 processor, boost repair output is added to the base repair effect while energySpent is calculated from the base repair effect. Do not simply multiply Energy cost by the repair boost. For a real processed action, the event's amount and energySpent fields are stronger evidence than a pre-action estimate.

Failure and evidence checklist

ObservationWhat it supportsWhat it does not prove
Path search is incompleteThis planner did not find a complete route under its current search limits and matrix.The Container is globally unreachable under every possible planner.
repair() returns OKThe repair intent passed the runtime submission checks.A particular hit amount, Energy cost, or survival outcome.
Matching EVENT_REPAIRThe exact actor-target repair processed; amount and energySpent describe that processed event.The target survived later decay or damage.
Container hits increasedNet state moved upward between observations.Which repairer caused the change when multiple writers exist.
Container missing next tickThe object is unavailable in the current observation.That the submitted repair never processed; check the previous event window.

Evidence and engine boundaries

The current official Container decay constants, Creep.repair() range, scheduled-return semantics, and Room repair-event fields were rechecked on August 18, 2026. The implementation notes in this revision were checked against screeps/engine 4.3.2 at commit 80977824199a596d174d392fd0cf8c458c21fcbd.

Timing boundary: the “repair intent before Container decay tick” statement is current-engine source behavior, not a permanent API contract. The deadline planner still reserves an explicit repair-submission window and a configurable safety margin.

Cost boundary: the unboosted helper estimates Energy for a fixed missing-hit amount with the current repair constants. Damage, decay, other repairers, changed constants, or boosts can change the real work remaining. The processed event remains the best per-action evidence.

Observation boundary: Screeps Console execution, official-shard fatal-pulse repair ordering, boosted WORK traces, hostile pressure, traffic delays, and multi-repairer locking remain Pending. No live result is fabricated.

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.