StructureContainer.ticksToDecay 很容易被误读成“这个 Container 还剩多少 tick 就会消失”。
它实际表示的是:距离下一次腐化扣血还有多少 tick。下一次腐化后,只要 hits 仍大于 0,Container 会继续存在,并重新开始下一轮腐化倒计时。
因此,下面的判断并不能证明 Container 即将消失:
if (container.ticksToDecay < 50) {
console.log('Container 快没了');
}
一个满血 Container 即使只剩 1 tick 触发下一次腐化,也不会因此直接消失。真正需要同时检查的是:
- 当前
hits; - 每次腐化扣除的
CONTAINER_DECAY; - 当前房间使用哪一种腐化间隔;
- 维修 Creep 到达 Range 3 需要多少 tick;
- Creep 是否有有效
WORK和 Energy; repair()的真实返回值;- 下一 tick 是否出现对应维修事件。
本文解决的不是“怎样从 Container 取 Energy”。站内已有 Container withdraw() 指南。本页只处理一个独立问题:
Container 正在腐化时,怎样判断它是否真的接近损毁,并在来得及的情况下安排一次可验证的维修动作。
快速结论
第一版可靠方案可以按这条链路执行:
读取 hits 与 ticksToDecay
→ 估算还可承受多少次腐化
→ 计算维修者到达 Range 3 的时间
→ 判断下一次腐化是否致命
→ 选择最紧急的 Container
→ moveTo() 或 repair()
→ 保存返回码与对象 ID
→ 下一 tick 匹配 EVENT_REPAIR
当前官方常量中:
CONTAINER_HITS = 250000;
CONTAINER_DECAY = 5000;
CONTAINER_DECAY_TIME = 100;
CONTAINER_DECAY_TIME_OWNED = 500;
REPAIR_POWER = 100;
REPAIR_COST = 0.01;
这些值属于官方服务器常量。私服可以修改常量,因此生产代码应读取全局常量,而不是把数字散落在各个角色模块中。
ticksToDecay 不是消失倒计时
假设一个中立或保留房间中的 Container 当前状态为:
hits = 5,001
ticksToDecay = 20
下一次腐化发生在约 20 tick 后,扣除 5,000 hits,剩余 1 hit。它不会在该 tick 消失,但再经历下一轮腐化就会损毁。
可承受腐化次数为:
const decayEventsUntilLoss = Math.ceil(
container.hits / CONTAINER_DECAY
);
保守损毁时间估算为:
const interval = room.controller?.level > 0
? CONTAINER_DECAY_TIME_OWNED
: CONTAINER_DECAY_TIME;
const estimatedTicksUntilLoss =
container.ticksToDecay
+ (decayEventsUntilLoss - 1) * interval;
如果 hits 正好等于 CONTAINER_DECAY,下一次腐化就会使它归零:
const nextDecayFatal =
container.hits <= CONTAINER_DECAY;
这个判断比只看 ticksToDecay 更重要。
为什么受控房间和远程房间的风险不同
Screeps 引擎在 Container 腐化时,会根据房间 Controller 是否存在有效等级选择间隔:
Controller level > 0
→ CONTAINER_DECAY_TIME_OWNED
没有有效 Controller 等级
→ CONTAINER_DECAY_TIME
按当前常量:
- 有等级 Controller 的房间,每 500 tick 扣 5,000 hits;
- 中立、保留或没有有效 Controller 等级的房间,每 100 tick 扣 5,000 hits。
这意味着远程采集 Container 的平均维护压力通常更高。
但不要把该间隔写成永久事实。房间控制权变化、私服常量或特殊运行环境都可能让旧估算失效。每个可见 tick 都应重新读取当前房间状态。
先归一化维修策略
生产示例通常会把阈值放进配置。配置被误写为 NaN、负数、小数或超出范围的比例时,不能让排序结果、截止时间或历史上限一起失效。
const DEFAULT_CONTAINER_REPAIR_POLICY =
Object.freeze({
minimumHitsRatio: 0.8,
bufferDecayEvents: 2,
safetyTicks: 5,
historyLimit: 20
});
function normalizeContainerRepairPolicy(
value = {}
) {
return {
minimumHitsRatio:
Number.isFinite(value.minimumHitsRatio)
&& value.minimumHitsRatio > 0
&& value.minimumHitsRatio <= 1
? value.minimumHitsRatio
: DEFAULT_CONTAINER_REPAIR_POLICY
.minimumHitsRatio,
bufferDecayEvents:
Number.isInteger(value.bufferDecayEvents)
&& value.bufferDecayEvents >= 1
? value.bufferDecayEvents
: DEFAULT_CONTAINER_REPAIR_POLICY
.bufferDecayEvents,
safetyTicks:
Number.isInteger(value.safetyTicks)
&& value.safetyTicks >= 0
? value.safetyTicks
: DEFAULT_CONTAINER_REPAIR_POLICY
.safetyTicks,
historyLimit:
Number.isInteger(value.historyLimit)
&& value.historyLimit >= 1
? value.historyLimit
: DEFAULT_CONTAINER_REPAIR_POLICY
.historyLimit
};
}
这些默认值是本站示例策略,不是官方推荐值。
维修目标不必每次修到满血
“只要不是满血就维修”会浪费 Builder 的移动与 Energy,还可能抢占更重要的建设、Controller 或防御任务。
目标 hits 取以下两者的较大值:
hitsMax × minimumHitsRatio
或
足以承受 bufferDecayEvents + 1 次腐化的 hits
const targetHits = Math.min(
container.hitsMax,
Math.max(
Math.ceil(
container.hitsMax
* policy.minimumHitsRatio
),
CONTAINER_DECAY
* (policy.bufferDecayEvents + 1)
)
);
这样既不会让低血量 Container 只修一点就离开,也不会强制所有远程 Container 每次都修满。
维修截止时间要扣掉到达时间
Creep 可以在 Range 3 内执行 repair()。如果维修者尚未到达,真正可用时间不是完整的 ticksToDecay,而是:
截止余量
= ticksToDecay
- 到达 Range 3 的预计路径长度
- 安全余量
const deadlineSlack =
container.ticksToDecay
- travelTicks
- policy.safetyTicks;
当 deadlineSlack <= 0 时,不代表 Container 一定会损毁,但说明按当前静态路径估算,维修者可能无法在下一次腐化前进入有效范围。
路径长度仍然不是到达时间的完整证明。以下因素会让实际移动更慢:
- Creep 的 MOVE 比例和 fatigue;
- 沼泽或没有道路的地形;
- 其他 Creep 堵路;
- 动态 Rampart 通行状态;
- hostile Creep;
- 路径缓存失效;
- 房间边界和视野变化。
因此该结果只能叫“调度估算”,不能叫“已经来得及”。
可离线验证的损毁估算函数
下面的函数不依赖真实 Screeps 对象,可以直接放进 Node.js 测试。
function estimateContainerLoss(
container,
roomHasControllerLevel,
constants
) {
const hits = Number.isFinite(container?.hits)
? Math.max(0, container.hits)
: 0;
const hitsMax = Number.isFinite(container?.hitsMax)
? Math.max(0, container.hitsMax)
: 0;
const ticksToDecay =
Number.isFinite(container?.ticksToDecay)
? Math.max(0, container.ticksToDecay)
: 0;
const decayAmount = constants.CONTAINER_DECAY;
const interval = roomHasControllerLevel
? constants.CONTAINER_DECAY_TIME_OWNED
: constants.CONTAINER_DECAY_TIME;
if (
hits <= 0
|| hitsMax <= 0
|| !Number.isFinite(decayAmount)
|| decayAmount <= 0
|| !Number.isFinite(interval)
|| interval <= 0
) {
return {
valid: false,
reason: 'invalid-container-decay-input'
};
}
const decayEventsUntilLoss =
Math.ceil(hits / decayAmount);
return {
valid: true,
hits,
hitsMax,
ticksToDecay,
decayAmount,
interval,
decayEventsUntilLoss,
estimatedTicksUntilLoss:
ticksToDecay
+ (decayEventsUntilLoss - 1) * interval,
nextDecayFatal: hits <= decayAmount,
nextDecayHits: Math.max(
0,
hits - decayAmount
)
};
}
怎样选择最紧急的 Container
一个房间可能有多个 Source Container、Controller Container 或临时物流 Container。排序时不要只看最低 hits。
本文依次比较:
- 下一次腐化是否会直接损毁;
- 扣除路径与安全余量后的
deadlineSlack; - 估算损毁时间;
- 当前 hits;
- 路径长度;
- Container ID 作为稳定兜底。
function rankContainerRepairPlans(plans) {
return [...plans]
.filter(plan =>
plan
&& typeof plan.containerId === 'string'
&& (
plan.action === 'repair'
|| plan.action === 'move'
)
)
.sort((left, right) => {
if (left.urgent !== right.urgent) {
return left.urgent ? -1 : 1;
}
if (
left.deadlineSlack
!== right.deadlineSlack
) {
return left.deadlineSlack
- right.deadlineSlack;
}
if (
left.estimatedTicksUntilLoss
!== right.estimatedTicksUntilLoss
) {
return left.estimatedTicksUntilLoss
- right.estimatedTicksUntilLoss;
}
if (left.hits !== right.hits) {
return left.hits - right.hits;
}
if (left.travelTicks !== right.travelTicks) {
return left.travelTicks
- right.travelTicks;
}
return left.containerId.localeCompare(
right.containerId
);
});
}
稳定 ID 兜底可以防止条件完全相同时,目标顺序每 tick 跳动。
完整房间级维修示例
下面的代码只管理 Container 维修目标和动作提交。
它不会替代完整 Builder 角色,也不会自动决定去哪里补充 Energy。如果维修者没有 Energy,函数会返回 repairer-needs-energy。可复用站内的 工作状态切换 与 Container withdraw() 逻辑补齐取能阶段。
把房间名和维修者名称替换成自己的。
const DEFAULT_CONTAINER_REPAIR_POLICY =
Object.freeze({
minimumHitsRatio: 0.8,
bufferDecayEvents: 2,
safetyTicks: 5,
historyLimit: 20
});
function normalizeContainerRepairPolicy(
value = {}
) {
return {
minimumHitsRatio:
Number.isFinite(value.minimumHitsRatio)
&& value.minimumHitsRatio > 0
&& value.minimumHitsRatio <= 1
? value.minimumHitsRatio
: DEFAULT_CONTAINER_REPAIR_POLICY
.minimumHitsRatio,
bufferDecayEvents:
Number.isInteger(value.bufferDecayEvents)
&& value.bufferDecayEvents >= 1
? value.bufferDecayEvents
: DEFAULT_CONTAINER_REPAIR_POLICY
.bufferDecayEvents,
safetyTicks:
Number.isInteger(value.safetyTicks)
&& value.safetyTicks >= 0
? value.safetyTicks
: DEFAULT_CONTAINER_REPAIR_POLICY
.safetyTicks,
historyLimit:
Number.isInteger(value.historyLimit)
&& value.historyLimit >= 1
? value.historyLimit
: DEFAULT_CONTAINER_REPAIR_POLICY
.historyLimit
};
}
function pushBoundedHistory(
history,
entry,
requestedLimit
) {
const values = Array.isArray(history)
? history
: [];
const limit =
Number.isInteger(requestedLimit)
&& requestedLimit >= 1
? requestedLimit
: DEFAULT_CONTAINER_REPAIR_POLICY
.historyLimit;
return [...values, entry].slice(-limit);
}
function getContainerDecayInterval(room) {
return room.controller?.level > 0
? CONTAINER_DECAY_TIME_OWNED
: CONTAINER_DECAY_TIME;
}
function getContainerPathLength(
repairer,
container
) {
if (repairer.pos.inRangeTo(container, 3)) {
return 0;
}
const path = repairer.pos.findPathTo(
container,
{
range: 3,
ignoreCreeps: true,
maxOps: 2000
}
);
return path.length > 0
? path.length
: null;
}
function createContainerRepairPlan(
room,
repairer,
container,
rawPolicy
) {
const policy =
normalizeContainerRepairPolicy(rawPolicy);
const interval =
getContainerDecayInterval(room);
if (
!container
|| !Number.isFinite(container.hits)
|| !Number.isFinite(container.hitsMax)
|| !Number.isFinite(container.ticksToDecay)
|| container.hits <= 0
|| container.hitsMax <= 0
) {
return null;
}
const decayEventsUntilLoss = Math.ceil(
container.hits / CONTAINER_DECAY
);
const estimatedTicksUntilLoss =
container.ticksToDecay
+ (decayEventsUntilLoss - 1)
* interval;
const targetHits = Math.min(
container.hitsMax,
Math.max(
Math.ceil(
container.hitsMax
* policy.minimumHitsRatio
),
CONTAINER_DECAY
* (policy.bufferDecayEvents + 1)
)
);
if (container.hits >= targetHits) {
return null;
}
const travelTicks =
getContainerPathLength(
repairer,
container
);
if (!Number.isInteger(travelTicks)) {
return null;
}
const deadlineSlack =
container.ticksToDecay
- travelTicks
- policy.safetyTicks;
return {
containerId: container.id,
hits: container.hits,
hitsMax: container.hitsMax,
targetHits,
travelTicks,
deadlineSlack,
estimatedTicksUntilLoss,
nextDecayFatal:
container.hits <= CONTAINER_DECAY,
urgent:
container.hits <= CONTAINER_DECAY
|| deadlineSlack <= 0,
action:
repairer.pos.inRangeTo(container, 3)
? 'repair'
: 'move'
};
}
function rankContainerRepairPlans(plans) {
return [...plans]
.filter(plan =>
plan
&& typeof plan.containerId === 'string'
&& (
plan.action === 'repair'
|| plan.action === 'move'
)
)
.sort((left, right) => {
if (left.urgent !== right.urgent) {
return left.urgent ? -1 : 1;
}
if (left.deadlineSlack !== right.deadlineSlack) {
return left.deadlineSlack
- right.deadlineSlack;
}
if (
left.estimatedTicksUntilLoss
!== right.estimatedTicksUntilLoss
) {
return left.estimatedTicksUntilLoss
- right.estimatedTicksUntilLoss;
}
if (left.hits !== right.hits) {
return left.hits - right.hits;
}
if (left.travelTicks !== right.travelTicks) {
return left.travelTicks
- right.travelTicks;
}
return left.containerId.localeCompare(
right.containerId
);
});
}
function selectContainerRepairPlan(
room,
repairer,
rawPolicy
) {
const policy =
normalizeContainerRepairPolicy(rawPolicy);
const containers = room.find(
FIND_STRUCTURES,
{
filter: structure =>
structure.structureType
=== STRUCTURE_CONTAINER
&& structure.hits
< structure.hitsMax
}
);
const plans = containers
.map(container =>
createContainerRepairPlan(
room,
repairer,
container,
policy
)
)
.filter(Boolean);
return rankContainerRepairPlans(plans)[0]
?? null;
}
function verifyPreviousContainerRepair(
room,
memory,
rawPolicy
) {
const policy =
normalizeContainerRepairPolicy(rawPolicy);
const pending = memory.pending;
if (!pending) {
return;
}
const expectedTick = pending.submittedAt + 1;
if (Game.time < expectedTick) {
return;
}
if (Game.time > expectedTick) {
memory.history = pushBoundedHistory(
memory.history,
{
tick: Game.time,
state: 'missed-observation-window',
expectedTick,
containerId: pending.containerId
},
policy.historyLimit
);
delete memory.pending;
return;
}
const container = Game.getObjectById(
pending.containerId
);
const events = room.getEventLog();
const repairEvent = events.find(event =>
event.event === EVENT_REPAIR
&& event.objectId === pending.repairerId
&& event.data?.targetId
=== pending.containerId
);
const hitsAfter =
container
&& Number.isFinite(container.hits)
? container.hits
: null;
const hitsDelta =
Number.isFinite(hitsAfter)
? hitsAfter - pending.hitsBefore
: null;
let state = 'repair-event-not-found';
if (repairEvent && !container) {
state = 'repair-event-target-missing';
} else if (
repairEvent
&& Number.isFinite(hitsDelta)
&& hitsDelta > 0
) {
state =
'repair-event-and-hits-increased';
} else if (repairEvent) {
state =
'repair-event-with-net-offset';
} else if (!container) {
state = 'container-missing';
} else if (!Number.isFinite(hitsDelta)) {
state = 'repair-evidence-unavailable';
} else if (hitsDelta > 0) {
state =
'hits-increased-without-matched-event';
} else if (hitsDelta === 0) {
state = 'no-net-hits-change';
} else {
state = 'hits-decreased';
}
memory.history = pushBoundedHistory(
memory.history,
{
tick: Game.time,
state,
containerId: pending.containerId,
repairerId: pending.repairerId,
result: pending.result,
hitsBefore: pending.hitsBefore,
hitsAfter,
hitsDelta,
eventMatched: Boolean(repairEvent),
energySpent:
Number.isFinite(
repairEvent?.data?.energySpent
)
? repairEvent.data.energySpent
: null
},
policy.historyLimit
);
delete memory.pending;
}
function runContainerDecayRepair(
roomName,
repairerName,
rawPolicy = {}
) {
const policy =
normalizeContainerRepairPolicy(rawPolicy);
Memory.containerDecayRepair ??= {};
const memory =
Memory.containerDecayRepair;
const room = Game.rooms[roomName];
if (!room) {
return { state: 'room-not-visible' };
}
verifyPreviousContainerRepair(
room,
memory,
policy
);
const repairer = Game.creeps[repairerName];
if (!repairer || !repairer.my) {
return { state: 'repairer-missing' };
}
if (repairer.spawning) {
return { state: 'repairer-spawning' };
}
if (repairer.getActiveBodyparts(WORK) <= 0) {
return { state: 'repairer-no-active-work' };
}
if (
repairer.store.getUsedCapacity(
RESOURCE_ENERGY
) <= 0
) {
return { state: 'repairer-needs-energy' };
}
const plan = selectContainerRepairPlan(
room,
repairer,
policy
);
if (!plan) {
return { state: 'no-container-needs-repair' };
}
const container = Game.getObjectById(
plan.containerId
);
if (!container) {
return { state: 'container-missing' };
}
if (plan.action === 'move') {
const result = repairer.moveTo(
container,
{
range: 3,
reusePath: 5,
visualizePathStyle: {
stroke: '#ffffff'
}
}
);
return {
state: 'move-submitted',
result,
plan
};
}
const hitsBefore = container.hits;
const result = repairer.repair(container);
if (result === OK) {
memory.pending = {
submittedAt: Game.time,
containerId: container.id,
repairerId: repairer.id,
hitsBefore,
result
};
}
return {
state:
result === OK
? 'repair-submitted'
: 'repair-rejected',
result,
plan
};
}
module.exports.loop = function () {
const result = runContainerDecayRepair(
'W1N1',
'Builder1',
DEFAULT_CONTAINER_REPAIR_POLICY
);
if (
result.state === 'repair-rejected'
|| (
result.state === 'move-submitted'
&& result.result !== OK
&& result.result !== ERR_TIRED
)
) {
console.log({
type: 'container-decay-repair',
tick: Game.time,
...result
});
}
};
repair() 返回码怎样处理
| 返回值 | 含义 | 优先检查 |
|---|---|---|
OK |
当前 tick 接受了维修意图 | 保存 Creep ID、Container ID、hits 与 tick,下一 tick 验证 |
ERR_NOT_OWNER |
执行动作的 Creep 不是自己的 | repairer.my 与对象恢复逻辑 |
ERR_BUSY |
Creep 仍在出生 | repairer.spawning |
ERR_NOT_ENOUGH_RESOURCES |
Creep 没有足够 Energy | store.getUsedCapacity(RESOURCE_ENERGY) |
ERR_INVALID_TARGET |
目标不存在、不是可维修结构或已经满血 | 重新恢复 ID、检查结构类型与 hits |
ERR_NOT_IN_RANGE |
距离超过 Range 3 | moveTo(..., { range: 3 }) |
ERR_NO_BODYPART |
没有有效 WORK | getActiveBodyparts(WORK) |
不要把 OK 写成“Container 已经恢复”。它只证明命令被接受。
为什么下一 tick 优先看维修事件
只比较两次 hits 可能产生错误结论。
例如同一个结算窗口内:
- Creep 提交维修;
- Container 触发腐化;
- hostile Creep 或其他单位造成额外影响;
- 另一个维修者也对同一目标工作。
最终 hits 可能增加、保持不变,甚至下降。
因此证据应分层:
第一层:repair() 返回值
第二层:下一 tick 的 EVENT_REPAIR
第三层:Container 净 hits 变化
第四层:多 tick 是否持续保持在策略目标以上
精确事件匹配需要同时核对:
event.event === EVENT_REPAIR
&& event.objectId === repairer.id
&& event.data?.targetId === container.id
只有目标 ID 一致仍不够,因为另一个 Creep 也可能维修同一 Container。
站内的 Room.getEventLog() 事件指南 进一步解释了一 tick 观察窗口和对象身份。
维修 Energy 怎样估算
未强化的一个有效 WORK 每次维修基础值为:
REPAIR_POWER
Energy 成本由:
repairHits * REPAIR_COST
决定。
按当前常量,一个普通 WORK 每次基础维修 100 hits,消耗 1 Energy。
const repairPower =
activeWorkParts * REPAIR_POWER;
const actionsNeeded = Math.ceil(
missingHits / repairPower
);
const energyNeeded = Math.ceil(
actionsNeeded
* repairPower
* REPAIR_COST
);
该公式没有覆盖 WORK Boost。强化会改变维修能力和实际 Energy 使用,生产系统应读取活跃身体部件及 Boost 系数,而不是继续套用未强化基线。
为什么不建议让 Tower 维护所有 Container
Tower 可以维修结构,但距离会影响维修效果,并且每次动作固定消耗 Tower Energy。
在没有敌情时,Tower 可以承担临时维护;但长期让 Tower 自动把所有 Container 修满可能:
- 消耗防御储备;
- 与紧急治疗或攻击争用动作;
- 掩盖 Builder 调度缺口;
- 在远距离目标上获得较低效果;
- 让维修阈值和防御阈值互相冲突。
站内 Tower 维修阈值指南 适合处理 Tower 侧策略。本文的核心是 Creep repair() 与 Container 腐化截止时间。
常见误区
把 ticksToDecay 当成死亡倒计时
它只表示下一次腐化,不是剩余生命周期。
只看最低 hits
低 hits 但下一次腐化还很远的目标,未必比即将触发致命腐化的目标更紧急。
在远程房间使用受控房间间隔
中立或保留房间通常使用更短的腐化间隔,维修压力更高。
忽略到达 Range 3 的时间
调度器在 Container 只剩 5 tick 时才派出 20 格外的 Builder,通常已经太晚。
维修者没有 Energy 仍不断调用 repair()
应先切换取能状态,不要每 tick 重复产生资源不足返回码。
每 tick 更换维修目标
没有稳定排序与目标保持会导致 Creep 来回移动,真正紧急的 Container 反而得不到维修。
把 hits 增加当成某个 Creep 的唯一贡献
其他维修者、Tower 和腐化结算都会影响净值。应匹配 actor 与 target 的事件身份。
把离线公式当成真实到达证明
路径、交通、fatigue、敌人和视野都可能改变真实结果。
与现有文章的边界
| 页面 | 独立搜索意图 |
|---|---|
| Container withdraw() | 怎样从 Container 取 Energy |
| Builder 自动建造和维修 | 怎样在建设与普通维修之间切换 |
| Tower 维修阈值 | Tower 什么时候维修结构 |
| 本文 | Container 何时会因腐化进入真实损毁风险,怎样安排可验证维修 |
因此本文适合新建独立 URL,不应并入 withdraw 页面。
离线验证结果
本次验证覆盖 31 个场景,包括:
- 5,000 与 5,001 hits 的致命边界;
- 受控房间和中立房间的腐化间隔;
- 无效 hits、常量和策略输入;
- 维修者缺失、出生中、无 WORK、无 Energy;
- Range 3、未知距离、移动与无路径;
- 可执行计划保留 Container ID;
- 紧急程度、截止余量与稳定 ID 排序;
- 精确维修事件、目标消失、净值抵消和证据缺失;
- 错过下一 tick 观察窗口;
- bounded history 在非法 limit 下仍保持安全上限。
全部 JavaScript 代码块通过语法检查。
这些测试没有调用真实 Room.find()、findPathTo()、Creep.repair() 或官方 shard 结算。
适用边界
本文没有实现:
- 多房间 Builder 全局调度;
- Source Keeper 房间战斗保护;
- hostile Creep 撤退逻辑;
- Boost WORK 的完整成本模型;
- Tower 与 Creep 维修动作仲裁;
- 多维修者任务锁;
- 动态道路和拥堵预测;
- 房间失去视野后的远程状态缓存;
- Container 重建;
- 真实服务器的腐化与维修同 tick 顺序证明。
真实 Console、官方 shard、多 tick 维修稳定性和 Container 最终保活仍标记为待验证。
相关站内内容
- 怎样从 Container 取 Energy
- Builder 怎样自动建造和维修
- Tower 怎样按阈值维修
- 怎样按路径选择 Source
- moveTo() 返回 OK 但不移动怎么排查
- 怎样读取 Room Event Log
- 进入资源采集与房间经济模块
官方资料
资料与官方开源引擎核对日期:2026-08-06。语法检查和 31 个离线场景已通过;真实 Screeps 环境仍待验证。