Mirroar/hivemind

View on GitHub
src/manager.bay.ts

Summary

Maintainability
B
5 hrs
Test Coverage
/* global FIND_STRUCTURES STRUCTURE_EXTENSION STRUCTURE_SPAWN
LOOK_STRUCTURES RESOURCE_ENERGY STRUCTURE_TOWER
STRUCTURE_LINK STRUCTURE_CONTAINER */

import cache from 'utils/cache';
import {encodePosition} from 'utils/serialization';
import {handleMapArea} from 'utils/map';

declare global {
    interface Room {
        bays: Bay[];
    }

    type BayStructureConstant = typeof bayStructures[number];
    type AnyBayStructure = ConcreteStructure<BayStructureConstant>;
}

const bayStructures = [STRUCTURE_SPAWN, STRUCTURE_EXTENSION, STRUCTURE_TOWER, STRUCTURE_LINK, STRUCTURE_CONTAINER];
const problematicStructures = [STRUCTURE_STORAGE, STRUCTURE_TERMINAL, STRUCTURE_FACTORY, STRUCTURE_LAB, STRUCTURE_NUKER, STRUCTURE_POWER_SPAWN];

export default class Bay {
    readonly pos: RoomPosition;
    readonly name: string;
    _hasHarvester: boolean;
    readonly extensions: AnyBayStructure[];
    energy: number;
    energyCapacity: number;

    /**
     * Bays collect extensions into a single entity for more efficient refilling.
     * @constructor
     *
     * @param {RoomPosition} pos
     *   Room position around which this bay is placed.
     * @param {boolean} hasHarvester
     *   Whether a harvester is in this bay to fill it.
     */
    constructor(pos: RoomPosition, hasHarvester: boolean) {
        this.pos = pos;
        this.name = encodePosition(pos);
        this._hasHarvester = hasHarvester;
        this.extensions = [];
        this.energy = 0;
        this.energyCapacity = 0;

        const bayExtensions = cache.inHeap(
            'bay-extensions:' + this.name,
            250,
            () => {
                const room = Game.rooms[this.pos.roomName];
                const ids: Array<Id<AnyBayStructure>> = [];
                for (const structureType of bayStructures) {
                    for (const structure of (room.structuresByType[structureType]) || []) {
                        if (structure.pos.getRangeTo(this.pos) > 1) continue;
                        if (!structure.isOperational()) continue;

                        ids.push(structure.id);
                    }
                }

                return ids;
            },
        );

        if (this.isBlocked()) return;

        for (const id of bayExtensions) {
            const extension = Game.getObjectById<AnyBayStructure>(id);
            if (!extension) continue;

            this.extensions.push(extension);

            if (extension instanceof StructureExtension || extension instanceof StructureSpawn) {
                this.energy += Math.min(extension.store.getUsedCapacity(RESOURCE_ENERGY), extension.store.getCapacity(RESOURCE_ENERGY));
                this.energyCapacity += extension.store.getCapacity(RESOURCE_ENERGY);
            }
        }
    }

    isBlocked(): boolean {
        return cache.inHeap('bay-blocked:' + this.name, 100, () => {
            // Do not add extensions to bay if center is blocked by a structure.
            const posStructures = this.pos.lookFor(LOOK_STRUCTURES);
            for (const structure of posStructures) {
                if (!structure.isWalkable()) {
                    return true;
                }
            }

            // Bay is also considered blocked if one of it's extensions is overfull.
            const hasOverfullExtension = cache.inHeap(
                'has-overfull-extensions:' + this.name,
                250,
                () => {
                    const room = Game.rooms[this.pos.roomName];
                    for (const structure of (room.structuresByType[STRUCTURE_EXTENSION]) || []) {
                        if (structure.pos.getRangeTo(this.pos) > 1) continue;
                        if (structure.store.getUsedCapacity(RESOURCE_ENERGY) > structure.store.getCapacity(RESOURCE_ENERGY)) {
                            return true;
                        }
                    }

                    return false;
                },
            );
            if (hasOverfullExtension) return true;

            // Do not add extensions to bay if another important structure is in the bay.
            const importantStructures = this.pos.findInRange(FIND_STRUCTURES, 1, {
                filter: structure => (problematicStructures as string[]).includes(structure.structureType) && structure.isOperational(),
            });
            return importantStructures.length > 0;
        });
    }

    /**
     * Checks if an extension is part of this bay.
     *
     * @param {Structure} extension
     *   The structure to check.
     *
     * @return {boolean}
     *   True if this extension is registered with this bay.
     */
    hasExtension(extension: AnyBayStructure): boolean {
        for (const ourExtension of this.extensions) {
            if (ourExtension.id === extension.id) return true;
        }

        return false;
    }

    /**
     * Checks if a harvester is in this bay.
     *
     * @return {boolean}
     *   True if a harvester is in this bay.
     */
    hasHarvester(): boolean {
        return this._hasHarvester;
    }

    /**
     * Checks if this bay needs to be filled with more energy.
     *
     * @return {boolean}
     *   True if more energy is needed.
     */
    needsRefill(): boolean {
        return this.energy < this.energyCapacity;
    }

    /**
     * Refills this bay using energy carried by the given creep.
     *
     * @param {Creep} creep
     *   A creep with carry parts and energy in store.
     * @return {boolean}
     *   True if more energy is needed.
     */
    refillFrom(creep: Creep) {
        const needsRefill = this.getStructuresNeedingRefill();
        if (needsRefill.length === 0) return false;

        const target = _.min(needsRefill, extension => (bayStructures as string[]).indexOf(extension.structureType));

        // Don't let harvesters refill containers they're on.
        if (
            target.structureType === STRUCTURE_CONTAINER
            && creep.memory.role === 'harvester'
            && creep.pos.isEqualTo(target.pos)
        ) {
            return false;
        }

        const targetCapacity = target.store.getFreeCapacity(RESOURCE_ENERGY);
        const amount = Math.min(creep.store.getUsedCapacity(RESOURCE_ENERGY), targetCapacity);
        const isLastTransfer = amount >= this.energyCapacity - this.energy || amount === creep.store.getUsedCapacity(RESOURCE_ENERGY);
        if (creep.transfer(target, RESOURCE_ENERGY) === OK && isLastTransfer) return false;

        return true;
    }

    getStructuresNeedingRefill() {
        return _.filter(this.extensions, (extension: AnyBayStructure) => {
            if (extension.store) return extension.store.getFreeCapacity(RESOURCE_ENERGY) > 0;

            return false;
        });
    }

    getExitPosition(): RoomPosition {
        const coords = cache.inHeap('exitCoords:' + this.name, 500, () => {
            let exitCoords;
            const room = Game.rooms[this.pos.roomName];
            handleMapArea(this.pos.x, this.pos.y, (x, y) => {
                const position = new RoomPosition(x, y, this.pos.roomName);
                if (room?.roomPlanner?.isPlannedLocation(position, 'road')) exitCoords = {x: position.x, y: position.y};
            });

            return exitCoords;
        });

        return new RoomPosition(coords.x, coords.y, this.pos.roomName);
    }
}