Quick answer
Recover three Lab objects from IDs, require ownership and activity, read the two input mineral types, resolve the product through REACTIONS, check at least LAB_REACTION_AMOUNT of both reagents, compatible output mineral, free capacity, range 2, and zero cooldown. Disable the one-time request before outputLab.runReaction(inputA, inputB), then verify the output Store and cooldown on the next tick.
Use two input Labs and one output Lab
function getOwnedActiveLab(id) {
const lab = typeof id === 'string'
? Game.getObjectById(id)
: null;
if (
!lab
|| lab.structureType !== STRUCTURE_LAB
|| lab.my !== true
|| lab.isActive() !== true
) {
return null;
}
return lab;
}
The three IDs must resolve to three distinct Labs in the same room. The output Lab is the object that calls runReaction().
Resolve the recipe from REACTIONS
function getReactionProduct(inputA, inputB) {
if (
typeof inputA !== 'string'
|| typeof inputB !== 'string'
) {
return null;
}
return REACTIONS[inputA]?.[inputB] ?? null;
}
Do not trust a product string stored in Memory without comparing it to the current reagent mineral types.
Check reagent amount and output capacity
function getLabMineral(lab) {
const mineralType = lab.mineralType;
return mineralType
? {
type: mineralType,
amount: lab.store.getUsedCapacity(mineralType)
}
: null;
}
function canReceiveReaction(outputLab, product) {
if (
outputLab.mineralType
&& outputLab.mineralType !== product
) {
return false;
}
return outputLab.store.getFreeCapacity(product)
>= LAB_REACTION_AMOUNT;
}
Each input needs at least LAB_REACTION_AMOUNT of its reagent. The output Lab needs enough capacity for that same reaction amount.
Check range and cooldown
function labsInReactionRange(outputLab, inputA, inputB) {
return outputLab.pos.inRangeTo(inputA, 2)
&& outputLab.pos.inRangeTo(inputB, 2);
}
The output Lab must have cooldown === 0. The cooldown after a successful reaction is determined by REACTION_TIME[product].
Build a testable reaction plan
function evaluateReaction(input) {
const { outputLab, inputA, inputB } = input;
if (!outputLab || !inputA || !inputB) {
return { ready: false, reason: 'lab-missing' };
}
if (
outputLab.id === inputA.id
|| outputLab.id === inputB.id
|| inputA.id === inputB.id
) {
return { ready: false, reason: 'lab-ids-not-distinct' };
}
if (
outputLab.room.name !== inputA.room.name
|| outputLab.room.name !== inputB.room.name
) {
return { ready: false, reason: 'different-rooms' };
}
const reagentA = getLabMineral(inputA);
const reagentB = getLabMineral(inputB);
if (!reagentA || !reagentB) {
return { ready: false, reason: 'reagent-missing' };
}
const product = getReactionProduct(
reagentA.type,
reagentB.type
);
if (!product) {
return { ready: false, reason: 'recipe-invalid' };
}
if (
reagentA.amount < LAB_REACTION_AMOUNT
|| reagentB.amount < LAB_REACTION_AMOUNT
) {
return { ready: false, reason: 'reagent-insufficient', product };
}
if (!canReceiveReaction(outputLab, product)) {
return { ready: false, reason: 'output-unavailable', product };
}
if (!labsInReactionRange(outputLab, inputA, inputB)) {
return { ready: false, reason: 'input-out-of-range', product };
}
if (outputLab.cooldown > 0) {
return { ready: false, reason: 'output-cooling-down', product };
}
return { ready: true, reason: 'ready', product };
}
Complete one-time reaction example
State impact: this code may schedule one real Lab reaction and writes a request snapshot. It never retries automatically.
module.exports.loop = function () {
const request = Memory.labReactionRequest;
if (!request || request.enabled !== true) {
return;
}
const outputLab = getOwnedActiveLab(request.outputLabId);
const inputA = getOwnedActiveLab(request.inputLabAId);
const inputB = getOwnedActiveLab(request.inputLabBId);
const plan = evaluateReaction({ outputLab, inputA, inputB });
request.lastCheckedAt = Game.time;
request.lastStatus = plan.reason;
if (!plan.ready) {
request.preview = { product: plan.product ?? null };
return;
}
request.enabled = false;
request.submittedAt = Game.time;
request.snapshot = {
outputLabId: outputLab.id,
inputLabAId: inputA.id,
inputLabBId: inputB.id,
product: plan.product,
outputBefore:
outputLab.store.getUsedCapacity(plan.product),
inputABefore:
inputA.store.getUsedCapacity(inputA.mineralType),
inputBBefore:
inputB.store.getUsedCapacity(inputB.mineralType),
cooldownBefore: outputLab.cooldown
};
const result = outputLab.runReaction(inputA, inputB);
request.result = result;
request.resultAt = Game.time;
request.status = result === OK
? 'accepted-pending-verification'
: 'failed-review-required';
};
Verify output on the next tick
function verifyReaction(request) {
const snapshot = request?.snapshot;
const outputLab = getOwnedActiveLab(
snapshot?.outputLabId
);
if (!snapshot || !outputLab) {
return { verified: false, reason: 'snapshot-or-lab-missing' };
}
const outputNow = outputLab.store.getUsedCapacity(
snapshot.product
);
return {
verified:
request.result === OK
&& outputNow >= snapshot.outputBefore
+ LAB_REACTION_AMOUNT,
outputDelta: outputNow - snapshot.outputBefore,
cooldownNow: outputLab.cooldown
};
}
Traffic, hauling, another reaction module, or resource withdrawal can change the Store before verification. A production system should coordinate Lab ownership across modules.
Handle return codes
| Code | Meaning |
|---|---|
OK | Reaction scheduled |
ERR_NOT_ENOUGH_RESOURCES | Input reagent unavailable |
ERR_INVALID_TARGET | Input target is not a valid Lab |
ERR_FULL | Output cannot receive product |
ERR_NOT_IN_RANGE | An input Lab is outside range 2 |
ERR_INVALID_ARGS | Reagents do not form a valid product |
ERR_TIRED | Output Lab is cooling down |
Debugging checklist
- Recover three distinct owned active Labs.
- Require one room.
- Read current input mineral types.
- Resolve the product from
REACTIONS. - Check both reagent amounts.
- Check output mineral compatibility and capacity.
- Check range 2 and cooldown.
- Disable before the call.
- Save Store snapshots and the return code.
- Verify output and cooldown on the next tick.
Scope and next steps
This guide does not implement reaction chains, Lab assignment, hauling, reverse reactions, demand forecasting, or multi-output scheduling. Continue with boosting eligible Creep parts.
Frequently asked questions
Can input Lab order be reversed?
Use the current REACTIONS lookup instead of assuming a recipe order.
Can several output Labs share inputs?
Yes when each output is in range and resources are sufficient.
Should a failed reaction retry every tick?
No. Require a new reviewed request.
Is cooldown a proof of success?
It is supporting evidence; compare Store deltas and coordinate other Lab users.