Use this guide when
Use the first half of this guide to report current Construction Site progress. Use the tracked-build workflow when you need to prove that one specific Builder's accepted build() intent produced the next tick's build event.
Choose the site-creation guide when the site does not exist yet. This page does not create, remove, or prioritize sites for an entire room.
Read the official progress fields
function summarizeConstructionProgress(site) {
const progress = Number.isFinite(site?.progress)
? site.progress
: 0;
const total = Number.isFinite(site?.progressTotal)
? site.progressTotal
: 0;
return {
progress,
total,
remaining: Math.max(0, total - progress),
percent: total > 0
? Math.min(
100,
Math.max(
0,
Math.floor((progress / total) * 100)
)
)
: 0
};
}
remaining and percent are display calculations. They are not fields on the official object and they do not provide a completion time.
Build a stable current-state report
function getOwnedConstructionReport() {
return Object.values(Game.constructionSites)
.map(site => ({
id: site.id,
structureType: site.structureType,
roomName: site.pos.roomName,
roomVisible: Boolean(site.room),
x: site.pos.x,
y: site.pos.y,
...summarizeConstructionProgress(site)
}))
.sort((left, right) =>
left.remaining - right.remaining
|| left.roomName.localeCompare(
right.roomName
)
|| left.structureType.localeCompare(
right.structureType
)
|| left.id.localeCompare(right.id)
);
}
Use site.pos.roomName for identity. The optional room object may be unavailable when the room is not visible.
Preflight one tracked Builder action
The diagnostic below tracks one accepted build at a time. It is not a room-wide Builder scheduler.
function inspectTrackedBuildRequest(
creep,
site
) {
if (
!creep
|| creep.my !== true
|| creep.spawning === true
) {
return { ready: false, reason: 'builder-unavailable' };
}
if (!site || site.my !== true) {
return { ready: false, reason: 'site-unavailable' };
}
if (creep.room.name !== site.pos.roomName) {
return { ready: false, reason: 'different-room' };
}
if (creep.getActiveBodyparts(WORK) <= 0) {
return { ready: false, reason: 'no-active-work' };
}
if (
creep.store.getUsedCapacity(
RESOURCE_ENERGY
) <= 0
) {
return { ready: false, reason: 'no-energy' };
}
if (!creep.pos.inRangeTo(site, 3)) {
return { ready: false, reason: 'not-in-range' };
}
return { ready: true, reason: 'ready' };
}
Range, active parts, Energy, ownership, and current object identity are preconditions. They do not prove the intent settled.
Submit and save the exact identities
function submitTrackedBuild(
creep,
site
) {
const room = creep?.room;
const existing = room?.memory?.trackedBuild;
if (existing) {
return {
status: 'tracked-build-already-pending',
submittedAt: existing.submittedAt,
builderId: existing.builderId,
siteId: existing.siteId
};
}
const check = inspectTrackedBuildRequest(
creep,
site
);
if (!check.ready) {
if (
check.reason === 'not-in-range'
&& creep
&& site
) {
return {
status: 'moving-to-site',
moveResult: creep.moveTo(site, {
range: 3,
reusePath: 10
})
};
}
return {
status: 'build-preflight-failed',
reason: check.reason
};
}
const progressBefore = site.progress;
const result = creep.build(site);
if (result !== OK) {
return {
status: 'build-rejected',
builderId: creep.id,
siteId: site.id,
result
};
}
room.memory.trackedBuild = {
submittedAt: Game.time,
builderId: creep.id,
builderName: creep.name,
siteId: site.id,
roomName: site.pos.roomName,
x: site.pos.x,
y: site.pos.y,
structureType: site.structureType,
progressBefore,
progressTotal: site.progressTotal
};
return {
status: 'build-accepted',
builderId: creep.id,
siteId: site.id,
submittedAt: Game.time,
result
};
}
OK means the intent was accepted in the current tick. The pending record is created only after that result.
Match the next tick's exact build event
function findCompletedStructure(
room,
pending
) {
return room.lookForAt(
LOOK_STRUCTURES,
pending.x,
pending.y
).find(structure =>
structure.structureType
=== pending.structureType
) ?? null;
}
function verifyTrackedBuild(room) {
const pending = room?.memory?.trackedBuild;
if (!pending) {
return { status: 'no-tracked-build' };
}
if (Game.time <= pending.submittedAt) {
return { status: 'accepted-this-tick' };
}
if (Game.time !== pending.submittedAt + 1) {
delete room.memory.trackedBuild;
return {
status: 'verification-window-missed',
submittedAt: pending.submittedAt,
checkedAt: Game.time
};
}
const matches = room.getEventLog()
.filter(event =>
event.event === EVENT_BUILD
&& event.objectId === pending.builderId
&& event.data?.targetId === pending.siteId
);
const site = Game.getObjectById(
pending.siteId
);
const structure = site
? null
: findCompletedStructure(room, pending);
if (matches.length === 0) {
delete room.memory.trackedBuild;
return {
status: structure
? 'structure-observed-without-matching-event'
: site
? 'site-active-without-matching-event'
: 'site-missing-without-matching-event',
progressNow: site?.progress ?? null,
structureId: structure?.id ?? null
};
}
if (matches.length > 1) {
delete room.memory.trackedBuild;
return {
status: 'matching-build-event-ambiguous',
matchCount: matches.length
};
}
const event = matches[0];
delete room.memory.trackedBuild;
return {
status: structure
? 'build-event-verified-site-completed'
: site
? 'build-event-verified-site-active'
: 'build-event-verified-site-missing',
eventAmount: event.data?.amount ?? null,
progressBefore: pending.progressBefore,
progressNow: site?.progress ?? null,
structureId: structure?.id ?? null
};
}
The event binds one Builder ID to one Site ID. The Site's net progress is supporting context: other Builders can add progress in the same tick. If the Site disappeared, the exact coordinate and structure type separate observed completion from a missing or removed Site.
Verify before submitting the next sample
module.exports.loop = function () {
const room = Game.rooms.W1N1;
if (!room) {
return;
}
const verification = verifyTrackedBuild(room);
if (verification.status === 'accepted-this-tick') {
return;
}
const builder = Game.creeps.Builder1;
const site = room.find(
FIND_MY_CONSTRUCTION_SITES
)[0];
if (!builder || !site) {
return;
}
const submission = submitTrackedBuild(
builder,
site
);
if (submission.status === 'build-rejected') {
console.log(JSON.stringify({
type: 'tracked-build',
roomName: room.name,
...submission
}));
}
};
A real production loop should choose the Site deterministically and should not use array position zero as its permanent priority rule. The abbreviated selection keeps this section focused on event identity.
Interpret disappearance carefully
| Observation | What it proves |
|---|---|
Matching EVENT_BUILD and Site still present | The exact Builder contributed to that Site; current progress is net room state. |
| Matching event and completed structure at the same coordinate | The exact Builder contributed in the completion tick and the structure replacement is visible. |
| Structure observed but no matching event | Completion is visible, but it cannot be attributed to this tracked Builder. |
| Site and structure both absent | The Site is missing or was removed; completion is not proven. |
| Room not visible during the next-tick window | The exact event cannot be recovered later from this one-tick log window. |
Do not invent an ETA from remaining work
Completion time depends on active WORK, carried Energy, range, movement, fatigue, traffic, task allocation, interruptions, Builder replacement, competing Sites, and the number of accepted build intents. A remaining-progress value is useful state, not a tick or wall-clock promise.
Return-code boundaries
| Code | Meaning here | Response |
|---|---|---|
OK | The build intent was accepted. | Save Builder and Site IDs and inspect the next tick. |
ERR_NOT_IN_RANGE | The Builder is farther than range 3. | Move, then retry on a later tick. |
ERR_NOT_ENOUGH_RESOURCES | The Builder lacks Energy. | Return to its Energy phase. |
ERR_NO_BODYPART | No active WORK part remains. | Replace or reassign the Creep. |
ERR_INVALID_TARGET | The target is not a valid owned Construction Site. | Refresh the Site object and ownership. |
ERR_BUSY | The Creep is spawning. | Wait for the Creep to finish. |
Production adaptation notes
Use the current-state report continuously, but sample exact Builder events only when diagnostics require attribution. Keep one pending sample per room or use unique request IDs for multiple samples. Report missed-window and ambiguous states, then release the pending slot so a later diagnostic sample can run; never rewrite those states as successful progress.
Verification status and evidence boundary
The progress calculations, event identity, completion/removal branches, and missed-window behavior were checked statically and offline. No real Builder, Site replacement, room-visibility loss, Console output, or live multi-tick event sequence was available. Those checks remain pending.