Mirroar/hivemind

View on GitHub
src/role/transporter.ts

Summary

Maintainability
F
6 days
Test Coverage
/* global PathFinder Room RoomPosition FIND_DROPPED_RESOURCES
STRUCTURE_CONTAINER RESOURCE_POWER RESOURCE_GHODIUM STRUCTURE_LAB REACTIONS
STRUCTURE_EXTENSION STRUCTURE_SPAWN STRUCTURE_TOWER STRUCTURE_NUKER ERR_NO_PATH
STRUCTURE_POWER_SPAWN TERRAIN_MASK_WALL LOOK_STRUCTURES RESOURCE_ENERGY
LOOK_CONSTRUCTION_SITES OK ORDER_SELL FIND_TOMBSTONES FIND_RUINS */

import balancer from 'excess-energy-balancer';
import hivemind from 'hivemind';
import Role from 'role/role';
import utilities from 'utilities';
import {getResourcesIn} from 'utils/store';
import {handleMapArea} from 'utils/map';

type TransporterDropOrderOption = {
    priority: number;
    weight: number;
    type: 'drop';
    resourceType: ResourceConstant;
};

type TransporterStructureOrderOption = {
    priority: number;
    weight: number;
    type: 'structure';
    object: AnyStoreStructure;
    resourceType: ResourceConstant;
};

type TransporterTombstoneOrderOption = {
    priority: number;
    weight: number;
    type: 'tombstone';
    object: Ruin | Tombstone;
    resourceType: ResourceConstant;
};

type TransporterPickupOrderOption = {
    priority: number;
    weight: number;
    type: 'resource';
    object: Resource;
    resourceType: ResourceConstant;
};

type TransporterPositionOrderOption = {
    priority: number;
    weight: number;
    type: 'position';
    object: RoomPosition;
    resourceType: ResourceConstant;
};

type TransporterSourceOrderOption = ResourceSourceTask | TransporterStructureOrderOption | TransporterPickupOrderOption | TransporterTombstoneOrderOption;

type TransporterGetEnergyOrder = {
    type: 'getEnergy' | 'getResource';
    target: Id<AnyStoreStructure | Resource | Ruin | Tombstone>;
    resourceType: ResourceConstant;
};

type TransporterOrder = TransporterGetEnergyOrder | ResourceSourceTask | ResourceDestinationTask;

declare global {
    interface TransporterCreep extends Creep {
        memory: TransporterCreepMemory;
        heapMemory: TransporterCreepHeapMemory;
    }

    interface TransporterCreepMemory extends CreepMemory {
        role: 'transporter';
        delivering?: boolean;
        order?: TransporterOrder;
        blockedPathCounter?: number;
    }

    interface TransporterCreepHeapMemory extends CreepHeapMemory {
        energyTakenFrom?: Id<AnyStoreStructure>;
        idlingFor?: number;
    }
}

function isResourceDestinationOrder(room: Room, order: TransporterOrder): order is ResourceDestinationTask {
    if ('type' in order && room.destinationDispatcher.hasProvider(order.type)) {
        return true;
    }

    return false;
}

function isStructureDestinationOrder(order: ResourceDestinationTask): order is StructureDestinationTask {
    return 'target' in order;
}

function isBayDestinationOrder(order: ResourceDestinationTask): order is BayDestinationTask {
    return order.type === 'bay';
}

function isResourceSourceOrder(room: Room, order: TransporterOrder): order is ResourceSourceTask {
    if ('type' in order && room.sourceDispatcher.hasProvider(order.type)) {
        return true;
    }

    return false;
}

function isStructureSourceOrder(order: ResourceSourceTask): order is StructureSourceTask {
    return 'target' in order;
}

function isCollectOrder(order: TransporterOrder): order is TransporterGetEnergyOrder {
    return order.type == 'getEnergy' || order.type == 'getResource';
}

export default class TransporterRole extends Role {
    creep: TransporterCreep;

    constructor() {
        super();

        // Make sure transporters always run at least a little.
        this.stopAt = 0;
        this.throttleAt = 5000;
    }

    /**
     * Makes this creep behave like a transporter.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     */
    run(creep: TransporterCreep) {
        this.creep = creep;

        if (creep.memory.singleRoom && creep.memory.order && 'target' in creep.memory.order) {
            const target = Game.getObjectById<RoomObject & _HasId>(creep.memory.order.target);
            if (target && target.pos && target.pos.roomName !== creep.memory.singleRoom) {
                this.setTransporterState(creep.memory.delivering);
            }
        }

        if ((creep.heapMemory.idlingFor || 0) > 0) {
            creep.heapMemory.idlingFor--;
            creep.whenInRange(1, creep, () => {});
            return;
        }

        if (creep.store.getUsedCapacity() >= creep.store.getCapacity() * 0.9 && !creep.memory.delivering) {
            this.setTransporterState(true);
        }
        else if (creep.store.getUsedCapacity() <= creep.store.getCapacity() * 0.1 && creep.memory.delivering // Don't switch state if we're currently filling a bay.
            && (!creep.memory.order || !isResourceDestinationOrder(creep.room, creep.memory.order) || !isBayDestinationOrder(creep.memory.order))) {
            this.setTransporterState(false);
        }

        if (this.bayUnstuck()) return;

        if (creep.memory.delivering) {
            this.performDeliver();
            return;
        }

        // Make sure not to keep standing on resource drop stop.
        const storagePosition = creep.room.getStorageLocation();
        if (!creep.room.storage && storagePosition && creep.pos.x === storagePosition.x && creep.pos.y === storagePosition.y && (!creep.memory.order || !('target' in creep.memory.order))) {
            creep.move(_.random(1, 8) as DirectionConstant);
            return;
        }

        this.performGetResources();
    }

    /**
     * Puts this creep into or out of delivery mode.
     *
     * @param {boolean} delivering
     *   Whether this creep is delivering resources instead of collecting.
     */
    setTransporterState(delivering: boolean) {
        this.creep.memory.delivering = delivering;
        delete this.creep.memory.order;
    }

    /**
     * Makes sure creeps don't get stuck in bays.
     *
     * @return {boolean}
     *   True if the creep is trying to get free.
     */
    bayUnstuck(): boolean {
        const creep = this.creep;
        // If the creep is in a bay, but not delivering to that bay (any more), make it move out of the bay forcibly.
        for (const bay of creep.room.bays) {
            if (creep.pos.x !== bay.pos.x || creep.pos.y !== bay.pos.y) continue;
            if (bay.isBlocked()) continue;

            // It's fine if we're explicitly delivering to this bay right now.
            if (creep.memory.order && isResourceDestinationOrder(creep.room, creep.memory.order) && isBayDestinationOrder(creep.memory.order) && creep.memory.order.name === bay.name) continue;

            // We're standing in a bay that we're not delivering to.
            const terrain = new Room.Terrain(creep.pos.roomName);
            // @todo Bay's available tiles should by handled and cached by the bay itself.
            const availableTiles: RoomPosition[] = [];
            handleMapArea(creep.pos.x, creep.pos.y, (x, y) => {
                if (x === creep.pos.x && y === creep.pos.y) return;
                if (terrain.get(x, y) === TERRAIN_MASK_WALL) return;

                const pos = new RoomPosition(x, y, creep.pos.roomName);

                // Check if there's a structure here already.
                const structures = pos.lookFor(LOOK_STRUCTURES);
                if (_.some(structures, structure => !structure.isWalkable())) return;

                // Check if there's a construction site here already.
                const sites = pos.lookFor(LOOK_CONSTRUCTION_SITES);
                if (_.some(sites, site => !site.isWalkable())) return;

                availableTiles.push(pos);
            });

            if (availableTiles.length === 1) {
                // Move out of the way.
                const dir = creep.pos.getDirectionTo(availableTiles[0]);
                creep.move(dir);
                return true;
            }
        }

        return false;
    }

    /**
     * Makes this creep deliver carried energy somewhere.
     */
    performDeliver() {
        const creep = this.creep;

        if (!this.ensureValidDeliveryTarget()) {
            creep.whenInRange(1, creep, () => {});
            delete creep.memory.order;
            return;
        }

        const order = creep.memory.order;
        if (isResourceDestinationOrder(creep.room, order)) {
            creep.room.destinationDispatcher.executeTask(order, {creep});
            return;
        }

        // Unknown target type, reset!
        hivemind.log('default').error('Unknown target type for delivery found!', JSON.stringify(order.type));
    }

    /**
     * Makes sure the creep has a valid target for resource delivery.
     *
     * @return {boolean}
     *   True if the target is valid and can receive the needed resource.
     */
    ensureValidDeliveryTarget(): boolean {
        const creep = this.creep;

        if (!creep.memory.order) this.calculateDeliveryTarget();
        if (!creep.memory.order) return false;

        if (isResourceDestinationOrder(creep.room, creep.memory.order)) {
            if (
                creep.memory.order.resourceType === RESOURCE_ENERGY
                && creep.heapMemory.energyTakenFrom
                && isStructureDestinationOrder(creep.memory.order)
                && creep.heapMemory.energyTakenFrom === creep.memory.order.target
            ) {
                // We're looping, taking energy and putting it right back.
                // Instead, we should wait for a while until new tasks show up.
                delete creep.memory.order;
                delete creep.heapMemory.energyTakenFrom;
                creep.heapMemory.idlingFor = 20;
                return false;
            }

            return creep.room.destinationDispatcher.validateTask(creep.memory.order, {creep});
        }

        return false;
    }

    /**
     * Sets a good energy delivery target for this creep.
     */
    calculateDeliveryTarget(): void {
        const creep = this.creep;
        creep.memory.order = creep.room.destinationDispatcher.getTask({creep});

        if (!creep.memory.order) {
            if (creep.store.getFreeCapacity() > 0) this.setTransporterState(false);
            return;
        }

        creep.room.visual.text('target: ' + creep.memory.order.type + '@' + creep.memory.order.priority, creep.pos);
    }

    /**
     * Makes this creep collect resources.
     *
     * @param {Function} calculateSourceCallback
     *   Optional callback to use when a new source target needs to be chosen.
     */
    performGetResources(sourceCallback?: () => void) {
        const creep = this.creep;
        if (!sourceCallback) {
            sourceCallback = () => {
                this.calculateSource();
            };
        }

        const calculateSourceCallback = () => {
            delete creep.heapMemory.energyTakenFrom;
            sourceCallback();

            const newOrder = creep.memory.order;
            if (newOrder && isResourceSourceOrder(creep.room, newOrder) && isStructureSourceOrder(newOrder) && newOrder.resourceType === RESOURCE_ENERGY) {
                creep.heapMemory.energyTakenFrom = newOrder.target;
            }
        };

        if (!this.ensureValidResourceSource(creep.memory.order, calculateSourceCallback)) {
            delete creep.memory.order;
            creep.whenInRange(1, creep, () => {});

            if (creep.memory.role === 'transporter') {
                if (creep.store.getUsedCapacity() > creep.store.getCapacity() * 0.1) {
                    // Deliver what we already have stored, if no more can be found for picking up.
                    this.setTransporterState(true);
                }
                else {
                    this.setTransporterState(false);
                }
            }

            return;
        }

        if (isResourceSourceOrder(creep.room, creep.memory.order)) {
            creep.room.sourceDispatcher.executeTask(creep.memory.order, {creep});
            return;
        }

        const target = Game.getObjectById(creep.memory.order.target);
        creep.whenInRange(1, target, () => {
            const resourceType = creep.memory.order.resourceType;
            let orderDone = false;
            if (target instanceof Resource) {
                orderDone = creep.pickup(target) === OK;
                if (
                    orderDone
                    && creep.store.getFreeCapacity() > target.amount
                ) {
                    const containers = _.filter(target.pos.lookFor(LOOK_STRUCTURES), s => s.structureType === STRUCTURE_CONTAINER) as StructureContainer[];
                    if (containers.length > 0 && (containers[0].store.getUsedCapacity(target.resourceType) || 0) > 0) {
                        // We have picked up resources dropped on the ground probably due to a full
                        // container. Pick up resources from the container next.
                        creep.memory.order = {
                            type: 'getResource',
                            target: containers[0].id,
                            resourceType: target.resourceType,
                        };
                        // Don't try to determine another source.
                        return;
                    }
                }
            }
            else if ('amount' in creep.memory.order) {
                orderDone = creep.withdraw(target, resourceType, Math.min(target.store.getUsedCapacity(resourceType), creep.memory.order.amount, creep.store.getFreeCapacity())) === OK;
            }
            else {
                orderDone = creep.withdraw(target, resourceType) === OK;
            }

            if (orderDone) {
                delete creep.memory.order;
                // @todo We may calculate a new order based on the projected contents of this creep's score.
                // calculateSourceCallback();
            }
        });
    }

    /**
     * Makes sure the creep has a valid target for resource pickup.
     *
     * @param {Function } calculateSourceCallback
     *   Callback to use when a new source target needs to be chosen.
     *
     * @return {boolean}
     *   True if the target is valid and contains the needed resource.
     */
    ensureValidResourceSource(order: TransporterOrder, calculateSourceCallback: () => void): order is TransporterGetEnergyOrder | ResourceSourceTask {
        const creep = this.creep;

        if (!order) {
            calculateSourceCallback();
            order = creep.memory.order;
        }

        if (!order) return false;

        if (isResourceSourceOrder(creep.room, order)) {
            return creep.room.sourceDispatcher.validateTask(order, {creep});
        }

        // The only valid source order type is `getEnergy` / `getResource`.
        if (!isCollectOrder(order)) return false;

        const target = Game.getObjectById(order.target);
        if (!target) return false;
        if (creep.memory.singleRoom && target.pos.roomName !== creep.memory.singleRoom) return false;

        const resourceType = order.resourceType;
        if ('store' in target && ((target as AnyStoreStructure).store.getUsedCapacity(resourceType)) > 0) return true;
        if (target instanceof Resource && target.amount > 0) return true;

        return false;
    }

    /**
     * Sets a good resource source target for this creep.
     */
    calculateSource() {
        const creep = this.creep;
        const best = utilities.getBestOption(this.getAvailableSources());

        if (!best) {
            delete creep.memory.order;
            return;
        }

        creep.room.visual.text('source: ' + best.type + '@' + best.priority, creep.pos);

        if (isResourceSourceOrder(creep.room, best)) {
            creep.memory.order = best;
            return;
        }

        creep.memory.order = {
            type: 'getResource',
            target: best.object.id,
            resourceType: best.resourceType,
        };
    }

    /**
     * Creates a priority list of resources available to this creep.
     *
     * @return {Array}
     *   A list of potential resource sources.
     */
    getAvailableSources(): TransporterSourceOrderOption[] {
        const creep = this.creep;
        const options = this.getAvailableEnergySources();

        const terminal = creep.room.terminal;
        const storage = creep.room.storage;

        // Don't pick up anything that's not energy if there's no place to store.
        if (!terminal && !storage) return options;

        const dispatcherTask = creep.room.sourceDispatcher.getTask({
            creep,
            resourceType: null,
        });
        if (dispatcherTask) options.push(dispatcherTask);

        this.addObjectResourceOptions(options, FIND_DROPPED_RESOURCES, 'resource');
        this.addObjectResourceOptions(options, FIND_TOMBSTONES, 'tombstone');
        this.addObjectResourceOptions(options, FIND_RUINS, 'tombstone');
        this.addContainerResourceOptions(options);
        this.addHighLevelResourceOptions(options);
        this.addEvacuatingRoomResourceOptions(options);

        return options;
    }

    /**
     * Creates a priority list of energy sources available to this creep.
     *
     * @return {Array}
     *   A list of potential energy sources.
     */
    getAvailableEnergySources(): TransporterSourceOrderOption[] {
        const creep = this.creep;
        const room = creep.room;
        const options: TransporterSourceOrderOption[] = [];

        const task = creep.room.sourceDispatcher.getTask({
            creep,
            resourceType: RESOURCE_ENERGY,
        });
        if (task) options.push(task);

        let priority = 0;
        if (room.energyAvailable < room.energyCapacityAvailable * 0.9) {
            // Spawning is important, so get energy when needed.
            priority = 4;
        }
        else if (room.terminal && room.storage && room.storage.store.energy > 5000 && room.terminal.store.energy < room.storage.store.energy * 0.05 && !room.isClearingTerminal()) {
            // Take some energy out of storage to put into terminal from time to time.
            priority = 2;
        }

        this.addObjectEnergySourceOptions(options, FIND_DROPPED_RESOURCES, 'resource', priority);
        this.addObjectEnergySourceOptions(options, FIND_TOMBSTONES, 'tombstone', priority);
        this.addObjectEnergySourceOptions(options, FIND_RUINS, 'tombstone', priority);

        return options;
    }

    /**
     * Adds options for picking up energy from room objects to priority list.
     *
     * @param {Array} options
     *   A list of potential energy sources.
     * @param {String} findConstant
     *   The type of find operation to run, e.g. FIND_DROPPED_RESOURCES.
     * @param {string} optionType
     *   Type designation of added resource options.
     */
    addObjectEnergySourceOptions(options: TransporterSourceOrderOption[], findConstant: FIND_RUINS | FIND_TOMBSTONES | FIND_DROPPED_RESOURCES, optionType: 'resource' | 'tombstone', storagePriority: number) {
        const creep = this.creep;

        // Get storage location, since that is a low priority source for transporters.
        const storagePosition = creep.room.getStorageLocation();

        // Look for energy on the ground.
        const targets = creep.room.find(findConstant, {
            filter: target => {
                const store = target instanceof Resource ? {[target.resourceType]: target.amount} : target.store;
                if ((store[RESOURCE_ENERGY] || 0) < 20) return false;
                if (!this.isSafePosition(creep, target.pos)) return false;

                // Const result = PathFinder.search(creep.pos, target.pos);
                // if (result.incomplete) return false;

                return true;
            },
        });

        for (const target of targets) {
            const store = target instanceof Resource ? {[target.resourceType]: target.amount} : target.store;
            const option = {
                priority: 4,
                weight: store[RESOURCE_ENERGY] / 100, // @todo Also factor in distance.
                type: optionType,
                object: target,
                resourceType: RESOURCE_ENERGY,
            };

            if (storagePosition && target.pos.x === storagePosition.x && target.pos.y === storagePosition.y) {
                option.priority = creep.memory.role === 'transporter' ? storagePriority : 5;
            }
            else {
                if (store[RESOURCE_ENERGY] < 100) option.priority--;
                if (store[RESOURCE_ENERGY] < 200) option.priority--;

                // If spawn / extensions need filling, transporters should not pick up
                // energy from random targets as readily, instead prioritize storage.
                if (creep.room.energyAvailable < creep.room.energyCapacityAvailable && creep.room.getCurrentResourceAmount(RESOURCE_ENERGY) > 5000 && creep.memory.role === 'transporter') option.priority -= 2;
            }

            if (store[RESOURCE_ENERGY] < creep.store.getCapacity() * 2) {
                option.priority -= creep.room.getCreepsWithOrder('getEnergy', target.id).length * 3;
                option.priority -= creep.room.getCreepsWithOrder('getResource', target.id).length * 3;
            }

            if (creep.room.storage && creep.room.getFreeStorage() < store[RESOURCE_ENERGY] && creep.room.getEffectiveAvailableEnergy() > 20_000) {
                // If storage is super full, try leaving stuff on the ground.
                option.priority -= 2;
            }

            options.push(option);
        }
    }

    /**
     * Adds options for picking up resources from certain objects to priority list.
     *
     * @param {Array} options
     *   A list of potential resource sources.
     * @param {String} findConstant
     *   The type of find operation to run, e.g. FIND_DROPPED_RESOURCES.
     * @param {string} optionType
     *   Type designation of added resource options.
     */
    addObjectResourceOptions(options: TransporterSourceOrderOption[], findConstant: FIND_RUINS | FIND_TOMBSTONES | FIND_DROPPED_RESOURCES, optionType: 'resource' | 'tombstone') {
        const creep = this.creep;

        // Look for resources on the ground.
        const targets = creep.room.find(findConstant, {
            filter: target => {
                if (!this.isSafePosition(creep, target.pos)) return false;

                const storeAmount = target instanceof Resource ? target.amount : target.store.getUsedCapacity();
                if (storeAmount > 10) {
                    const result = PathFinder.search(creep.pos, target.pos);
                    if (!result.incomplete) return true;
                }

                return false;
            },
        });

        for (const target of targets) {
            const store = target instanceof Resource ? {[target.resourceType]: target.amount} : target.store;
            for (const resourceType of getResourcesIn(store)) {
                if (resourceType === RESOURCE_ENERGY) continue;
                if (store[resourceType] === 0) continue;

                const option = {
                    priority: 4,
                    weight: store[resourceType] / 30, // @todo Also factor in distance.
                    type: optionType,
                    object: target,
                    resourceType,
                };

                if (resourceType === RESOURCE_POWER) {
                    option.priority++;
                }

                if (creep.room.getFreeStorage() < store[resourceType]) {
                    // If storage is super full, try leaving stuff on the ground.
                    continue;
                }

                if (store[resourceType] < creep.store.getCapacity() * 2) {
                    option.priority -= creep.room.getCreepsWithOrder('getEnergy', target.id).length * 2;
                    option.priority -= creep.room.getCreepsWithOrder('getResource', target.id).length * 2;
                }

                options.push(option);
            }
        }
    }

    /**
     * Adds options for picking up resources from containers to priority list.
     *
     * @param {Array} options
     *   A list of potential resource sources.
     */
    addContainerResourceOptions(options: TransporterSourceOrderOption[]) {
        const room = this.creep.room;
        // We need a decent place to store these resources.
        if (!room.terminal && !room.storage) return;

        // Take non-energy out of containers.
        const containers = _.filter(
            room.structuresByType[STRUCTURE_CONTAINER],
            structure => this.isSafePosition(this.creep, structure.pos),
        );

        for (const container of containers) {
            for (const resourceType of getResourcesIn(container.store)) {
                if (resourceType === RESOURCE_ENERGY) continue;
                if (container.store[resourceType] === 0) continue;

                let isEmptyMineralContainer = false;
                for (const mineral of room.minerals) {
                    if (
                        container.id === mineral.getNearbyContainer()?.id
                        && resourceType === mineral.mineralType
                        && container.store[resourceType] < CONTAINER_CAPACITY / 2
                    ) {
                        isEmptyMineralContainer = true;
                        break;
                    }
                }

                if (isEmptyMineralContainer) continue;

                const option: TransporterStructureOrderOption = {
                    priority: 3,
                    weight: container.store[resourceType] / 20, // @todo Also factor in distance.
                    type: 'structure',
                    object: container,
                    resourceType,
                };

                option.priority -= room.getCreepsWithOrder('getResource', container.id).length * 2;

                options.push(option);
            }
        }
    }

    /**
     * Adds options for picking up resources for nukers and power spawns.
     *
     * @param {Array} options
     *   A list of potential resource sources.
     */
    addHighLevelResourceOptions(options: TransporterSourceOrderOption[]) {
        const room = this.creep.room;
        if (room.isEvacuating()) return;

        // Take ghodium if nuker needs it.
        if (room.nuker && room.nuker.store.getFreeCapacity(RESOURCE_GHODIUM) > 0) {
            const target = room.getBestStorageSource(RESOURCE_GHODIUM);
            if (target && target.store[RESOURCE_GHODIUM] > 0) {
                const option = {
                    priority: 2,
                    weight: 0, // @todo Also factor in distance.
                    type: 'structure',
                    object: target,
                    resourceType: RESOURCE_GHODIUM,
                };

                options.push(option);
            }
        }

        // Take power if power spawn needs it.
        if (room.powerSpawn && room.powerSpawn.store[RESOURCE_POWER] < room.powerSpawn.store.getCapacity(RESOURCE_POWER) * 0.1 && balancer.maySpendEnergyOnPowerProcessing()) {
            const target = room.getBestStorageSource(RESOURCE_POWER);
            if (target && target.store[RESOURCE_POWER] > 0) {
                // @todo Limit amount since power spawn can only hold 100 power at a time.
                // @todo Make sure only 1 creep does this at a time.
                const option = {
                    priority: 3,
                    weight: 0, // @todo Also factor in distance.
                    type: 'structure',
                    object: target,
                    resourceType: RESOURCE_POWER,
                };

                if (room.isFullOnPower()) {
                    option.priority++;
                }

                options.push(option);
            }
        }
    }

    /**
     * Adds options for picking up resources for moving to terminal.
     *
     * @param {Array} options
     *   A list of potential resource sources.
     */
    addEvacuatingRoomResourceOptions(options: TransporterSourceOrderOption[]) {
        const room = this.creep.room;
        if (!room.isEvacuating()) return;

        // Take everything out of labs.
        const labs = room.myStructuresByType[STRUCTURE_LAB] || [];
        for (const lab of labs) {
            if (room.boostManager.isLabUsedForBoosting(lab.id)) continue;

            if (lab.store[RESOURCE_ENERGY] > 0) {
                options.push({
                    priority: 3,
                    weight: 0,
                    type: 'structure',
                    object: lab,
                    resourceType: RESOURCE_ENERGY,
                });
            }

            if (lab.mineralType) {
                options.push({
                    priority: 3,
                    weight: 0,
                    type: 'structure',
                    object: lab,
                    resourceType: lab.mineralType,
                });
            }
        }

        // @todo Destroy nuker once storage is empty so we can pick up contained resources.
    }

    /**
     * Makes this creep collect energy.
     *
     * @param {Creep} creep
     *   The creep to run logic for.
     */
    performGetEnergy(creep: TransporterCreep) {
        this.creep = creep;
        this.performGetResources(() => {
            this.calculateEnergySource();
        });
    }

    /**
     * Sets a good energy source target for this creep.
     */
    calculateEnergySource() {
        const creep = this.creep;
        const best = utilities.getBestOption(this.getAvailableEnergySources());

        if (!best) {
            delete creep.memory.order;
            creep.room.visual.text('no source :(', creep.pos);
            return;
        }

        creep.room.visual.text('source: ' + best.type + '@' + best.priority, creep.pos);

        if (isResourceSourceOrder(creep.room, best)) {
            creep.memory.order = best;
            return;
        }

        creep.memory.order = {
            type: 'getEnergy',
            target: best.object.id,
            resourceType: best.resourceType,
        };
    }
}