Mirroar/hivemind

View on GitHub
src/prototype/room.structures.ts

Summary

Maintainability
C
1 day
Test Coverage
/* global Room STRUCTURE_LINK CONTROLLER_STRUCTURES FIND_STRUCTURES */

import Bay from 'manager.bay';
import cache from 'utils/cache';
import LinkNetwork from 'link-network';

declare global {
    interface Room {
        structures: AnyStructure[];
        structuresByType: {
            [STRUCTURE_CONTAINER]: StructureContainer[];
            [STRUCTURE_EXTENSION]: StructureExtension[];
            [STRUCTURE_EXTRACTOR]: StructureExtractor[];
            [STRUCTURE_FACTORY]: StructureFactory[];
            [STRUCTURE_LAB]: StructureLab[];
            [STRUCTURE_LINK]: StructureLink[];
            [STRUCTURE_NUKER]: StructureNuker[];
            [STRUCTURE_OBSERVER]: StructureObserver[];
            [STRUCTURE_PORTAL]: StructurePortal[];
            [STRUCTURE_POWER_BANK]: StructurePowerBank[];
            [STRUCTURE_POWER_SPAWN]: StructurePowerSpawn[];
            [STRUCTURE_RAMPART]: StructureRampart[];
            [STRUCTURE_SPAWN]: StructureSpawn[];
            [STRUCTURE_TOWER]: StructureTower[];
            [STRUCTURE_WALL]: StructureWall[];
        };
        myStructures: AnyOwnedStructure[];
        myStructuresByType: {
            [STRUCTURE_EXTENSION]: StructureExtension[];
            [STRUCTURE_EXTRACTOR]: StructureExtractor[];
            [STRUCTURE_FACTORY]: StructureFactory[];
            [STRUCTURE_LAB]: StructureLab[];
            [STRUCTURE_LINK]: StructureLink[];
            [STRUCTURE_NUKER]: StructureNuker[];
            [STRUCTURE_OBSERVER]: StructureObserver[];
            [STRUCTURE_POWER_SPAWN]: StructurePowerSpawn[];
            [STRUCTURE_RAMPART]: StructureRampart[];
            [STRUCTURE_SPAWN]: StructureSpawn[];
            [STRUCTURE_TOWER]: StructureTower[];
            [STRUCTURE_WALL]: StructureWall[];
        };
        generateLinkNetwork: () => void;
        isClearingTerminal: () => boolean;
        isClearingStorage: () => boolean;
        isEvacuating: () => boolean;
        linkNetwork: LinkNetwork;
        setClearingTerminal: (clear: boolean) => void;
        setEvacuating: (evacuate: boolean) => void;
        getEnergyStructures: () => Array<StructureSpawn | StructureExtension>;
        isStripmine: () => boolean;
        setStripmine: (stripmine: boolean) => void;
    }

    interface RoomMemory {
        isEvacuating?: boolean;
        isStripmine?: boolean;
        isClearingTerminal?: boolean;
    }
}

// Define quick access property room.structures.
Object.defineProperty(Room.prototype, 'structures', {
    get(this: Room) {
        return cacheStructures(this).all;
    },
    enumerable: false,
    configurable: true,
});

// Define quick access property room.structuresByType.
Object.defineProperty(Room.prototype, 'structuresByType', {
    get(this: Room) {
        return cacheStructures(this).byType;
    },
    enumerable: false,
    configurable: true,
});

// Define quick access property room.myStructures.
Object.defineProperty(Room.prototype, 'myStructures', {
    get(this: Room) {
        return cacheStructures(this).mine;
    },
    enumerable: false,
    configurable: true,
});

// Define quick access property room.myStructuresByType.
Object.defineProperty(Room.prototype, 'myStructuresByType', {
    get(this: Room) {
        return cacheStructures(this).mineByType;
    },
    enumerable: false,
    configurable: true,
});

function cacheStructures(room: Room) {
    return cache.inObject(room, 'allStructures', 1, () => {
        const structures = room.find(FIND_STRUCTURES);
        const myStructures = [];
        const structuresByType = {};
        const myStructuresByType = {};

        for (const structure of structures) {
            if (!structuresByType[structure.structureType]) {
                structuresByType[structure.structureType] = [];
            }

            structuresByType[structure.structureType].push(structure);

            if ('my' in structure && structure.my) {
                myStructures.push(structure);
                if (!myStructuresByType[structure.structureType]) {
                    myStructuresByType[structure.structureType] = [];
                }

                myStructuresByType[structure.structureType].push(structure);
            }
        }

        return {
            all: structures,
            mine: myStructures,
            byType: structuresByType,
            mineByType: myStructuresByType,
        };
    });
}

/**
 * Creates and populates a room's link network.
 */
Room.prototype.generateLinkNetwork = function (this: Room) {
    const links = _.filter(
        this.myStructuresByType[STRUCTURE_LINK],
        s => s.isOperational(),
    );

    if (links.length <= 0) {
        return;
    }

    this.linkNetwork = new LinkNetwork();
    // @todo Controller and source links should be gotten through functions that
    // use the room planner.
    const controllerLinkId = this.memory.controllerLink;
    const sourceLinkIds = [];
    for (const source of this.sources) {
        const link = source.getNearbyLink();
        if (!link) continue;

        sourceLinkIds.push(link.id);
    }

    // Add links to network.
    for (const link of links) {
        if (link.id === controllerLinkId) {
            if (sourceLinkIds.includes(link.id)) {
                this.linkNetwork.addInOutLink(link);
            }
            else {
                this.linkNetwork.addOutLink(link);
            }
        }
        else if (sourceLinkIds.includes(link.id)) {
            this.linkNetwork.addInLink(link);
        }
        else {
            this.linkNetwork.addNeutralLink(link);
        }
    }
};

/**
 * Starts evacuation process for a room to prepare it for being abandoned.
 *
 * @param {boolean} evacuate
 *   Whether to evacuate this room or not.
 */
Room.prototype.setEvacuating = function (this: Room, evacuate: boolean) {
    this.memory.isEvacuating = evacuate;
};

/**
* Checks if a room is currently evacuating.
*
* @return {boolean}
*   Whether this room should be evacuated.
*/
Room.prototype.isEvacuating = function (this: Room) {
    return this.memory.isEvacuating && this.terminal?.isOperational();
};

Room.prototype.setStripmine = function (this: Room, enable: boolean) {
    this.memory.isStripmine = enable;
};

Room.prototype.isStripmine = function (this: Room) {
    return this.memory.isStripmine;
};

/**
* Starts emptying a rooms terminal and keeps it empty.
*
* @param {boolean} clear
*   Whether to clear this room's terminal.
*/
Room.prototype.setClearingTerminal = function (this: Room, clear: boolean) {
    this.memory.isClearingTerminal = clear;
};

/**
* Checks if a room's terminal should be emptied.
*
* @return {boolean}
*   Whether this room's terminal is being cleared.
*/
Room.prototype.isClearingTerminal = function (this: Room) {
    if (!this.terminal) return false;

    if (this.storage && this.roomManager && this.roomManager.hasMisplacedTerminal()) {
        return true;
    }

    return this.memory.isClearingTerminal;
};

/**
 * Checks if a room's storage should be emptied.
 */
Room.prototype.isClearingStorage = function (this: Room) {
    if (!this.storage) return false;
    if (this.isClearingTerminal()) return false;

    if (this.isEvacuating()) return true;
    if (this.terminal && this.roomManager && this.roomManager.hasMisplacedStorage()) {
        return true;
    }

    return false;
};

Room.prototype.getEnergyStructures = function (this: Room): Array<StructureSpawn | StructureExtension> {
    if (!this.roomPlanner) return undefined;

    // Short cache time, because priority of bays may shift as sources deplete.
    const ids = cache.inHeap('energyStructures:' + this.name, 1, () => {
        const structures: Array<Id<StructureSpawn | StructureExtension>> = [];

        for (const bay of getBaysByPriority(this)) {
            for (const structure of bay.extensions) {
                if (structure.structureType !== STRUCTURE_SPAWN && structure.structureType !== STRUCTURE_EXTENSION) continue;

                structures.push(structure.id);
            }
        }

        // Add energy structures outside of bays last, because they're less
        // efficient to refill.
        for (const structure of getUnmappedEnergyStructures(this, structures)) {
            structures.push(structure.id);
        }

        return structures;
    });

    return _.map<Id<StructureSpawn | StructureExtension>, StructureSpawn | StructureExtension>(ids, Game.getObjectById);
};

function getBaysByPriority(room: Room): Bay[] {
    const center = room.roomPlanner.getRoomCenter();
    return _.sortBy(room.bays, bay => {
        if (bay.energyCapacity <= 0) return 999;

        // Prefer partially filled bays. Lower priority means empty it first.
        let priority = bay.energy / bay.energyCapacity;

        // Greatly prefer emptying bays filled by harvesters.
        // Unless they don't have enough reserves.
        for (const source of room.sources) {
            if (source.pos.getRangeTo(bay.pos) > 1) continue;

            const missingEnergy = Math.max(0, source.energy + bay.energyCapacity - (source.getNearbyContainer()?.store[RESOURCE_ENERGY] || 0));
            priority = missingEnergy / bay.energyCapacity;
            break;
        }

        // Lastly, closer bays get preferred.
        const distance = center.getRangeTo(bay.pos);
        return priority + distance / 50;
    });
}

function getUnmappedEnergyStructures(room: Room, map: Array<Id<Structure>>): Array<StructureSpawn | StructureExtension> {
    const center = room.roomPlanner.getRoomCenter();
    return _.sortBy(
        _.filter(
            [...(room.myStructuresByType[STRUCTURE_SPAWN] || []), ...(room.myStructuresByType[STRUCTURE_EXTENSION] || [])],
            s => !map.includes(s.id) && s.isOperational(),
        ),
        s => s.pos.getRangeTo(center),
    );
}