src/role/hauler.relay.ts
/* global FIND_DROPPED_RESOURCES RESOURCE_ENERGY OK LOOK_CONSTRUCTION_SITES
ERR_NO_PATH ERR_NOT_IN_RANGE STRUCTURE_CONTAINER STRUCTURE_ROAD
FIND_MY_CONSTRUCTION_SITES LOOK_STRUCTURES MAX_CONSTRUCTION_SITES */
// @todo Collect energy if it's lying on the path.
import hivemind from 'hivemind';
import RemoteMiningOperation from 'operation/remote-mining';
import Role from 'role/role';
import {encodePosition, decodePosition, serializePositionPath} from 'utils/serialization';
declare global {
interface RelayHaulerCreep extends Creep {
memory: RelayHaulerCreepMemory;
heapMemory: RelayHaulerCreepHeapMemory;
operation: RemoteMiningOperation;
}
interface RelayHaulerCreepMemory extends CreepMemory {
role: 'hauler.relay';
delivering?: boolean;
source?: string;
}
interface RelayHaulerCreepHeapMemory extends CreepHeapMemory {
deliveryTarget?: Id<AnyStoreStructure>;
order?: ResourceDestinationTask;
energyPickupTarget?: Id<Resource | Tombstone | Ruin | StructureContainer>;
}
}
export default class RelayHaulerRole extends Role {
actionTaken: boolean;
/**
* Makes a creep behave like a relay hauler.
*
* @param {Creep} creep
* The creep to run logic for.
*/
run(creep: RelayHaulerCreep) {
if (!hivemind.segmentMemory.isReady()) return;
// @todo If empty, but there's no operation, return home and wait (or suicide).
const isEmpty = creep.store.getUsedCapacity() === 0;
const isFull = creep.store.getUsedCapacity() >= creep.store.getCapacity() * 0.9;
const needsToReturn = isFull;
if (creep.memory.delivering && isEmpty) {
this.startPickup(creep);
}
else if (!creep.memory.delivering && needsToReturn) {
this.startDelivering(creep);
}
if (creep.memory.delivering) {
this.performDeliver(creep);
return;
}
this.performPickup(creep);
}
startPickup(creep: RelayHaulerCreep) {
delete creep.memory.delivering;
delete creep.heapMemory.deliveryTarget;
delete creep.heapMemory.energyPickupTarget;
this.determineTargetSource(creep);
const path = this.getPath(creep);
if (!path) return;
creep.setCachedPath(serializePositionPath(path), true, 1);
}
determineTargetSource(creep: RelayHaulerCreep) {
const harvestPositions = Game.rooms[creep.memory.sourceRoom].getRemoteHarvestSourcePositions();
const scoredPositions = [];
for (const position of harvestPositions) {
scoredPositions.push(this.scoreHarvestPosition(creep, position));
}
if (scoredPositions.length === 0) return;
const bestPosition = _.max(scoredPositions, 'energy');
if (bestPosition?.position) {
creep.memory.source = encodePosition(bestPosition.position);
creep.memory.operation = 'mine:' + bestPosition.position.roomName;
}
}
scoreHarvestPosition(creep: RelayHaulerCreep, position: RoomPosition) {
const targetPos = encodePosition(position);
const operation = Game.operationsByType.mining['mine:' + position.roomName];
if (!operation) return {position, energy: -1000};
const path = operation.getPaths()[targetPos];
const currentEnergy = operation.getEnergyForPickup(targetPos);
const maxHarvesterLifetime = _.max(
_.filter(Game.creepsByRole['harvester.remote'], (creep: RemoteHarvesterCreep) => creep.memory.source === targetPos),
(creep: Creep) => creep.ticksToLive,
).ticksToLive;
const projectedIncomeDuration = Math.min(maxHarvesterLifetime, path.travelTime);
const sourceMaxEnergy = operation.canReserveFrom(creep.memory.sourceRoom) ? SOURCE_ENERGY_CAPACITY : SOURCE_ENERGY_NEUTRAL_CAPACITY;
const projectedIncome = operation.hasContainer(targetPos) ? projectedIncomeDuration * sourceMaxEnergy / ENERGY_REGEN_TIME : 0;
const queuedHaulerCapacity = _.sum(
_.filter(Game.creepsByRole['hauler.relay'], (creep: RelayHaulerCreep) => creep.memory.source === targetPos && !creep.memory.delivering),
(creep: Creep) => creep.store.getFreeCapacity(RESOURCE_ENERGY),
);
const queuedBuilderCapacity = _.sum(
_.filter(Game.creepsByRole['builder.mines'], (creep: MineBuilderCreep) => creep.memory.source === targetPos && !creep.memory.returning),
(creep: Creep) => creep.store.getFreeCapacity(RESOURCE_ENERGY),
);
const attackPenalty = operation.isUnderAttack() ? 1000 : 0;
return {
position,
energy: currentEnergy
+ projectedIncome
- queuedHaulerCapacity
- queuedBuilderCapacity
- attackPenalty,
};
}
startDelivering(creep: RelayHaulerCreep) {
creep.memory.delivering = true;
const path = this.getPath(creep);
delete creep.memory.source;
delete creep.heapMemory.deliveryTarget;
delete creep.heapMemory.energyPickupTarget;
if (!path) return;
creep.setCachedPath(serializePositionPath(path), false, 1);
}
getPath(creep: RelayHaulerCreep): RoomPosition[] | null {
if (!creep.operation) return null;
const paths = creep.operation.getPaths();
if (!paths[creep.memory.source] || !paths[creep.memory.source].accessible) return null;
return paths[creep.memory.source].path;
}
/**
* Makes a creep deliver resources to another room.
*
* @param {Creep} creep
* The creep to run logic for.
*/
performDeliver(creep: RelayHaulerCreep) {
const sourceRoom = creep.memory.sourceRoom;
if (!Game.rooms[sourceRoom]) return;
if (this.performRelay(creep)) return;
// Transfer energy to nearby mine builders.
const creeps = creep.pos.findInRange(FIND_MY_CREEPS, 1, {
filter: creep => ['builder', 'builder.remote', 'builder.mines', 'upgrader'].includes(creep.memory.role) && creep.store.getFreeCapacity(RESOURCE_ENERGY) > creep.store.getCapacity() / 3,
});
if (creeps.length > 0) {
creep.transfer(_.sample(creeps), RESOURCE_ENERGY);
return;
}
if (this.pickupNearbyEnergy(creep)) return;
const hasTarget = creep.heapMemory.deliveryTarget && creep.isInRoom();
if (creep.pos.roomName === sourceRoom || hasTarget) {
const target = this.getDeliveryTarget(creep);
const targetPosition = target ? target.pos : Game.rooms[sourceRoom].getStorageLocation();
if (!targetPosition) return;
this.storeResources(creep, target);
return;
}
if (creep.hasCachedPath()) {
creep.followCachedPath();
if (creep.hasArrived()) {
creep.clearCachedPath();
}
}
else {
creep.moveToRange(Game.rooms[sourceRoom].getStorageLocation(), 1);
}
}
getDeliveryTarget(creep: RelayHaulerCreep) {
if (creep.heapMemory.deliveryTarget) {
const target = Game.getObjectById(creep.heapMemory.deliveryTarget);
if (target && target.store.getFreeCapacity(RESOURCE_ENERGY) > 0) {
return target;
}
delete creep.heapMemory.deliveryTarget;
}
const target = Game.rooms[creep.memory.sourceRoom].getBestStorageTarget(creep.store.energy, RESOURCE_ENERGY);
if (!target) return null;
creep.heapMemory.deliveryTarget = target.id;
return target;
}
storeResources(creep: RelayHaulerCreep, target?: AnyStoreStructure) {
this.transferEnergyToNearbyTargets(creep);
if (!creep.room.storage && !creep.room.terminal) {
if (!creep.heapMemory.order || !creep.room.destinationDispatcher.validateTask(creep.heapMemory.order, {creep})) {
creep.heapMemory.order = creep.room.destinationDispatcher.getTask({
creep,
resourceType: RESOURCE_ENERGY,
});
}
if (creep.heapMemory.order) {
creep.room.destinationDispatcher.executeTask(creep.heapMemory.order, {creep});
return;
}
}
// @todo If no storage is available, use default delivery method.
if (!target || creep.store[RESOURCE_ENERGY] > target.store.getFreeCapacity(RESOURCE_ENERGY)) {
this.dropResources(creep);
return;
}
creep.whenInRange(1, target, () => {
if (creep.transfer(target, RESOURCE_ENERGY) === OK) {
creep.operation?.addResourceGain(creep.store.energy, RESOURCE_ENERGY);
}
});
}
transferEnergyToNearbyTargets(creep: RelayHaulerCreep) {
if (creep.room.name !== creep.memory.sourceRoom) return;
if (creep.room.storage || creep.room.terminal) return;
const structures = _.filter([
...(creep.room.myStructuresByType[STRUCTURE_SPAWN] || []),
...(creep.room.myStructuresByType[STRUCTURE_EXTENSION] || []),
...(creep.room.myStructuresByType[STRUCTURE_TOWER] || []),
...(creep.room.myStructuresByType[STRUCTURE_POWER_SPAWN] || []),
],
(structure: AnyStoreStructure) =>
creep.pos.getRangeTo(structure.pos) <= 1
&& structure.store.getFreeCapacity(RESOURCE_ENERGY) > 0,
);
if (structures.length > 0) {
creep.transfer(_.sample(structures), RESOURCE_ENERGY);
return;
}
const creeps = creep.pos.findInRange(FIND_MY_CREEPS, 1, {
filter: creep => ['builder', 'builder.remote', 'builder.mines', 'upgrader'].includes(creep.memory.role) && creep.store.getFreeCapacity(RESOURCE_ENERGY) > 0,
});
if (creeps.length > 0) {
creep.transfer(_.sample(creeps), RESOURCE_ENERGY);
}
}
dropResources(creep: RelayHaulerCreep) {
const storageLocation = creep.room.getStorageLocation();
if (!storageLocation) {
// If there's no place to deliver, just drop the energy on the spot, somebody will probably pick it up.
if (creep.drop(RESOURCE_ENERGY) === OK) {
creep.operation?.addResourceGain(creep.store.energy, RESOURCE_ENERGY);
}
return;
}
creep.whenInRange(0, storageLocation, () => {
if (creep.drop(RESOURCE_ENERGY) === OK) {
creep.operation.addResourceGain(creep.store.energy, RESOURCE_ENERGY);
}
});
}
performRelay(creep: RelayHaulerCreep) {
if (!creep.hasCachedPath()) return false;
return false;
}
/**
* Makes a creep get energy from different rooms.
*
* @param {Creep} creep
* The creep to run logic for.
*/
performPickup(creep: RelayHaulerCreep) {
creep.say('p0');
const sourcePosition = decodePosition(creep.memory.source);
if (!sourcePosition) {
creep.say('newtar');
this.startDelivering(creep);
return;
}
if (
creep.pos.roomName === sourcePosition.roomName
&& this.getSource(creep)?.isDangerous()
&& creep.pos.getRangeTo(sourcePosition) <= 10
) {
if (_.size(creep.room.creepsByRole.skKiller) > 0) {
// We wait for SK killer to clean up.
creep.whenInRange(6, sourcePosition, () => {});
}
else {
// Too dangerous, return home.
this.startDelivering(creep);
}
return;
}
// Pick up energy / resources directly next to the creep.
// From drops, tombstones or ruins.
if (this.pickupNearbyEnergy(creep)) {
creep.say('ene'); return;
}
if (creep.hasCachedPath()) {
if (creep.hasArrived()) {
creep.clearCachedPath();
}
else if (creep.pos.roomName === sourcePosition.roomName && creep.pos.getRangeTo(sourcePosition) <= 3) {
creep.clearCachedPath();
}
else {
creep.say('follow');
creep.followCachedPath();
return;
}
}
creep.say('p1');
if (sourcePosition.roomName !== creep.pos.roomName) {
creep.moveToRange(sourcePosition, 1);
return;
}
// Get energy from target container.
if (!creep.operation) {
// Operation has probably ended. Return home.
this.startDelivering(creep);
return;
}
creep.say('p2');
const container = creep.operation.getContainer(creep.memory.source);
if (container) {
creep.say('container');
const hasActiveHarvester = _.some(creep.room.creepsByRole['harvester.remote'], (harvester: RemoteHarvesterCreep) => {
if (harvester.memory.source !== creep.memory.source) return false;
if (harvester.pos.roomName !== container.pos.roomName) return false;
if (harvester.pos.getRangeTo(container.pos) > 3) return false;
return true;
});
if (!hasActiveHarvester && container.store.getUsedCapacity(RESOURCE_ENERGY) < 20) {
this.startDelivering(creep);
return;
}
creep.whenInRange(1, container, () => {
const relevantAmountReached = (container.store.energy || 0) >= Math.min(creep.store.getCapacity() / 2, creep.store.getFreeCapacity());
if (relevantAmountReached) {
creep.withdraw(container, RESOURCE_ENERGY);
}
if (!hasActiveHarvester) {
creep.withdraw(container, RESOURCE_ENERGY);
}
this.startDelivering(creep);
});
}
else if (creep.pos.getRangeTo(sourcePosition) > 2) {
// If all else fails, make sure we're close enough to our source.
creep.whenInRange(2, sourcePosition, () => {
// We've reached the source and there's nothing left to pick up.
// Return home.
this.startDelivering(creep);
});
}
else {
// We're at the source. With no container, and no energy to pick up,
// return home.
this.startDelivering(creep);
}
}
getSource(creep: RelayHaulerCreep): Source {
const sourcePosition = decodePosition(creep.memory.source);
return creep.room.find(FIND_SOURCES, {
filter: source => source.pos.x === sourcePosition.x && source.pos.y === sourcePosition.y,
})[0];
}
/**
* Picks up dropped energy close to this creep.
*
* @param {Creep} creep
* The creep to run logic for.
*
* @return {boolean}
* True if a pickup was made this tick.
*/
pickupNearbyEnergy(creep: RelayHaulerCreep) {
if (creep.store.getFreeCapacity() === 0) return false;
if (creep.room.isMine()) return false;
// @todo Allow hauler to pick up other resources as well, but respect that
// when delivering.
// @todo Allow picking up from tombstones and ruins.
// Check if energy is on the ground nearby and pick that up.
const target = this.getNearbyEnergyTarget(creep);
if (target) {
creep.whenInRange(1, target, () => {
if (target instanceof Resource) {
creep.pickup(target);
}
else {
creep.withdraw(target, RESOURCE_ENERGY);
}
});
return true;
}
return false;
}
getNearbyEnergyTarget(creep: RelayHaulerCreep) {
if (creep.heapMemory.energyPickupTarget) {
const target = Game.getObjectById(creep.heapMemory.energyPickupTarget);
if (target && target.pos.roomName === creep.pos.roomName && ((target instanceof Resource) || target.store.getUsedCapacity(RESOURCE_ENERGY) >= 20)) {
return target;
}
delete creep.heapMemory.energyPickupTarget;
// If we just happened to pick up energy from the ground, check if
// there's also a full container nearby and empty that as well.
// This prevents overflowing containers from keeping haulers busy
// picking up spilled energy.
const container = creep.pos.findInRange(FIND_STRUCTURES, 1, {
filter: structure => structure.structureType === STRUCTURE_CONTAINER
&& structure.store.getFreeCapacity() < structure.store.getCapacity() * 0.1
&& structure.store.getUsedCapacity(RESOURCE_ENERGY) > 100,
}) as StructureContainer[];
if (container.length > 0) creep.heapMemory.energyPickupTarget = container[0].id;
return container[0];
}
// @todo Check if there's a valid (short) path to the resource.
const resources = creep.pos.findInRange(FIND_DROPPED_RESOURCES, 3, {
filter: resource => resource.resourceType === RESOURCE_ENERGY && resource.amount >= 20,
});
if (resources.length > 0) {
creep.heapMemory.energyPickupTarget = resources[0].id;
return resources[0];
}
const tombstone = creep.pos.findInRange(FIND_TOMBSTONES, 3, {
filter: tombstone => tombstone.store.getUsedCapacity(RESOURCE_ENERGY) >= 20,
});
if (tombstone.length > 0) {
creep.heapMemory.energyPickupTarget = tombstone[0].id;
return tombstone[0];
}
const ruin = creep.pos.findInRange(FIND_RUINS, 3, {
filter: ruin => ruin.store.getUsedCapacity(RESOURCE_ENERGY) >= 20,
});
if (ruin.length > 0) {
creep.heapMemory.energyPickupTarget = ruin[0].id;
return ruin[0];
}
return null;
}
}