diff --git a/src/Colony.ts b/src/Colony.ts index c23be5588..ee236d91e 100644 --- a/src/Colony.ts +++ b/src/Colony.ts @@ -3,6 +3,7 @@ import {$} from './caching/GlobalCache'; import {log} from './console/log'; import {StoreStructure} from './declarations/typeGuards'; import {DirectiveExtract} from './directives/resource/extract'; +import {DirectivePoisonRoom} from './directives/offense/poisonRoom'; import {_HARVEST_MEM_DOWNTIME, _HARVEST_MEM_USAGE, DirectiveHarvest} from './directives/resource/harvest'; import {HiveCluster} from './hiveClusters/_HiveCluster'; import {CommandCenter} from './hiveClusters/commandCenter'; @@ -311,7 +312,7 @@ export class Colony { $.set(this, 'sources', () => _.sortBy(_.flatten(_.map(this.rooms, room => room.sources)), source => source.pos.getMultiRoomRangeTo(this.pos))); for (const source of this.sources) { - DirectiveHarvest.createIfNotPresent(source.pos, 'pos'); + !DirectivePoisonRoom.isPresent(source.pos, 'room') && DirectiveHarvest.createIfNotPresent(source.pos, 'pos'); } $.set(this, 'extractors', () => _(this.rooms) @@ -320,7 +321,7 @@ export class Colony { .filter(e => (e!.my && e!.room.my) || Cartographer.roomType(e!.room.name) != ROOMTYPE_CONTROLLER) .sortBy(e => e!.pos.getMultiRoomRangeTo(this.pos)).value() as StructureExtractor[]); - if (this.controller.level >= 6) { + if (this.controller.level >= 6 && this.terminal) { _.forEach(this.extractors, extractor => DirectiveExtract.createIfNotPresent(extractor.pos, 'pos')); } $.set(this, 'repairables', () => _.flatten(_.map(this.rooms, room => room.repairables))); @@ -546,6 +547,13 @@ export class Colony { return allAssets; } + private runPowerSpawn() { + if (this.powerSpawn && this.assets.energy > 300000 && this.powerSpawn.energy > 50 + && this.powerSpawn.power > 0) { + this.powerSpawn.processPower(); + } + } + /** * Initializes the state of the colony each tick */ @@ -567,6 +575,7 @@ export class Colony { this.linkNetwork.run(); // Run the link network this.roadLogistics.run(); // Run the road network this.roomPlanner.run(); // Run the room planner + this.runPowerSpawn(); // Run power spawn - short term this.stats(); // Log stats per tick } diff --git a/src/Overseer.ts b/src/Overseer.ts index 4f221128d..c214e7249 100644 --- a/src/Overseer.ts +++ b/src/Overseer.ts @@ -8,6 +8,7 @@ import {DirectiveOutpost} from './directives/colony/outpost'; import {DirectiveGuard} from './directives/defense/guard'; import {DirectiveInvasionDefense} from './directives/defense/invasionDefense'; import {DirectiveOutpostDefense} from './directives/defense/outpostDefense'; +import {DirectivePoisonRoom} from './directives/offense/poisonRoom'; import {Directive} from './directives/Directive'; import {Notifier} from './directives/Notifier'; import {DirectiveBootstrap} from './directives/situational/bootstrap'; @@ -215,7 +216,7 @@ export class Overseer implements IOverseer { // Place defensive directive after hostiles have been present for a long enough time const safetyData = RoomIntel.getSafetyData(colony.room.name); const invasionIsPersistent = safetyData.unsafeFor > 20; - if (invasionIsPersistent) { + if (invasionIsPersistent && !DirectivePoisonRoom.isPresent(colony.pos, 'room')) { DirectiveInvasionDefense.createIfNotPresent(colony.controller.pos, 'room'); } } @@ -242,10 +243,14 @@ export class Overseer implements IOverseer { if (alreadyAColony || alreadyAnOutpost) { return false; } - const alreadyOwned = RoomIntel.roomOwnedBy(roomName); - const alreadyReserved = RoomIntel.roomReservedBy(roomName); - const disregardReservations = !onPublicServer() || MY_USERNAME == MUON; - if (alreadyOwned || (alreadyReserved && !disregardReservations)) { + let alreadyOwned = RoomIntel.roomOwnedBy(roomName); + let alreadyReserved = RoomIntel.roomReservedBy(roomName); + let isBlocked = Game.flags[roomName+"-Block"] != null; + if (isBlocked) { + //Game.notify("Room " + roomName + " is blocked, not expanding there."); + } + let disregardReservations = !onPublicServer() || MY_USERNAME == MUON; + if (alreadyOwned || (alreadyReserved && !disregardReservations) || isBlocked) { return false; } const neighboringRooms = _.values(Game.map.describeExits(roomName)) as string[]; @@ -297,7 +302,8 @@ export class Overseer implements IOverseer { } // Place pioneer directives in case the colony doesn't have a spawn for some reason if (Game.time % 25 == 0 && colony.spawns.length == 0 && - !DirectiveClearRoom.isPresent(colony.pos, 'room')) { + !DirectiveClearRoom.isPresent(colony.pos, 'room') && + !DirectivePoisonRoom.isPresent(colony.pos, 'room')) { // verify that there are no spawns (not just a caching glitch) const spawns = Game.rooms[colony.name]!.find(FIND_MY_SPAWNS); if (spawns.length == 0) { diff --git a/src/console/Console.ts b/src/console/Console.ts index 3fa5c9e82..e1b0698f0 100644 --- a/src/console/Console.ts +++ b/src/console/Console.ts @@ -5,6 +5,7 @@ import {color, toColumns} from '../utilities/utils'; import {asciiLogoRL, asciiLogoSmall} from '../visuals/logos'; import {DEFAULT_OVERMIND_SIGNATURE, MY_USERNAME, USE_PROFILER} from '../~settings'; import {log} from './log'; +import {Overlord} from "../overlords/Overlord"; type RecursiveObject = { [key: string]: number | RecursiveObject }; @@ -26,6 +27,7 @@ export class OvermindConsole { global.setLogLevel = log.setLogLevel; global.suspendColony = this.suspendColony; global.unsuspendColony = this.unsuspendColony; + global.listSuspendedColonies = this.listSuspendedColonies; global.openRoomPlanner = this.openRoomPlanner; global.closeRoomPlanner = this.closeRoomPlanner; global.cancelRoomPlanner = this.cancelRoomPlanner; @@ -36,6 +38,7 @@ export class OvermindConsole { global.listConstructionSites = this.listConstructionSites; global.listDirectives = this.listDirectives; global.listPersistentDirectives = this.listPersistentDirectives; + global.directiveInfo = this.directiveInfo; global.removeAllLogisticsDirectives = this.removeAllLogisticsDirectives; global.removeFlagsByColor = this.removeFlagsByColor; global.removeErrantFlags = this.removeErrantFlags; @@ -44,6 +47,7 @@ export class OvermindConsole { global.endRemoteDebugSession = this.endRemoteDebugSession; global.profileMemory = this.profileMemory; global.cancelMarketOrders = this.cancelMarketOrders; + global.setRoomUpgradeRate = this.setRoomUpgradeRate; } // Help, information, and operational changes ====================================================================== @@ -68,6 +72,7 @@ export class OvermindConsole { descr['setLogLevel(int)'] = 'set the logging level from 0 - 4'; descr['suspendColony(roomName)'] = 'suspend operations within a colony'; descr['unsuspendColony(roomName)'] = 'resume operations within a suspended colony'; + descr['listSuspendedColonies()'] = 'Prints all suspended colonies'; descr['openRoomPlanner(roomName)'] = 'open the room planner for a room'; descr['closeRoomPlanner(roomName)'] = 'close the room planner and save changes'; descr['cancelRoomPlanner(roomName)'] = 'close the room planner and discard changes'; @@ -78,6 +83,7 @@ export class OvermindConsole { descr['listConstructionSites(filter?)'] = 'list all construction sites matching an optional filter'; descr['listDirectives(filter?)'] = 'list directives, matching a filter if specified'; descr['listPersistentDirectives()'] = 'print type, name, pos of every persistent directive'; + descr['directiveInfo(directiveFlag)'] = 'print type, name, pos of every creep in directive'; descr['removeFlagsByColor(color, secondaryColor)'] = 'remove flags that match the specified colors'; descr['removeErrantFlags()'] = 'remove all flags which don\'t match a directive'; descr['deepCleanMemory()'] = 'deletes all non-critical portions of memory (be careful!)'; @@ -250,6 +256,17 @@ export class OvermindConsole { } } + static listSuspendedColonies(): string { + let msg = 'Colonies currently suspended: \n'; + for (let i in Memory.colonies) { + let colonyMemory = Memory.colonies[i] as ColonyMemory | undefined; + if (colonyMemory && colonyMemory.suspend == true) { + msg += 'Colony ' + i + ' \n'; + } + } + return msg; + } + // Room planner control ============================================================================================ static openRoomPlanner(roomName: string): string { @@ -377,6 +394,40 @@ export class OvermindConsole { return `Removed ${count} flags.`; } + static directiveInfo(flagName: string): string { + let msg = ''; + let directive = Overmind.directives[flagName]; + if (!directive) { + return `ERROR: Name is not a current directive` + } + msg += `Type: ${directive.directiveName}`.padRight(20) + + `Name: ${directive.name}`.padRight(25) + + `Pos: ${directive.pos.print}\n`; + for (let overlordName of Object.keys(directive.overlords)) { + let overlord = directive.overlords[overlordName] as Overlord; + msg += JSON.stringify(overlord.creepUsageReport) + `\n`; + let zerg = overlord.getZerg(); + let combatZerg = overlord.getCombatZerg(); + for (let [roleName, zergArray] of Object.entries(zerg)) { + msg += `Role: ${roleName} \n`; + for (let zerg of zergArray) { + msg += `Name: ${zerg.name} Room: ${zerg.pos.print} TTL/Spawning: ${zerg.ticksToLive || zerg.spawning} \n`; + } + } + msg += `Combat zerg \n`; + for (let [roleName, zergArray] of Object.entries(combatZerg)) { + msg += `Role: ${roleName} \n`; + for (let zerg of zergArray) { + msg += `Name: ${zerg.name} Room: ${zerg.pos.print} TTL/Spawning: ${zerg.ticksToLive || zerg.spawning} \n`; + } + } + } + + return msg; + } + + + // Structure management ============================================================================================ @@ -419,6 +470,15 @@ export class OvermindConsole { return `Destroyed ${room.barriers.length} barriers.`; } + // Colony Management ================================================================================================= + + static setRoomUpgradeRate(roomName: string, rate: number): string { + let colony: Colony = Overmind.colonies[roomName]; + colony.upgradeSite.memory.speedFactor = rate; + + return `Colony ${roomName} is now upgrading at a rate of ${rate} barriers.`; + } + // Memory management =============================================================================================== diff --git a/src/creepSetups/setups.ts b/src/creepSetups/setups.ts index 6e34dd94d..9f04d13f3 100644 --- a/src/creepSetups/setups.ts +++ b/src/creepSetups/setups.ts @@ -23,6 +23,9 @@ export const Roles = { healer : 'transfuser', bunkerGuard : 'bunkerGuard', dismantler: 'lurker', + drill : 'drill', + coolant : 'coolant', + roomPoisoner: 'salter', }; /** @@ -32,8 +35,9 @@ export const Setups = { drones: { extractor: new CreepSetup(Roles.drone, { - pattern : [WORK, WORK, CARRY, MOVE], + pattern : [WORK, WORK, MOVE], sizeLimit: Infinity, + prefix: [CARRY, CARRY] }), miners: { @@ -178,7 +182,12 @@ export const Setups = { sizeLimit: 5, }), - } + }, + + roomPoisoner: new CreepSetup(Roles.roomPoisoner, { + pattern : [WORK, CARRY, MOVE, MOVE], + sizeLimit: 4, + }), }; @@ -230,14 +239,19 @@ export const CombatSetups = { sizeLimit: Infinity, }), + distraction: new CreepSetup(Roles.ranged, { + pattern : [MOVE, MOVE, MOVE, RANGED_ATTACK, MOVE], + sizeLimit: 1, + }), + default: new CreepSetup(Roles.ranged, { pattern : [RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, MOVE, MOVE, MOVE, MOVE, HEAL], sizeLimit: Infinity, }), boosted_T3: new CreepSetup(Roles.ranged, { - pattern : [TOUGH, TOUGH, RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, - MOVE, MOVE, HEAL], + pattern : [TOUGH, RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, RANGED_ATTACK, + MOVE, MOVE, HEAL, HEAL], sizeLimit: Infinity, }), @@ -349,4 +363,23 @@ export const CombatSetups = { }, + drill: { + default: new CreepSetup(Roles.drill, { + pattern : [MOVE, ATTACK, ATTACK, MOVE], + sizeLimit: Infinity, + }), + }, + + coolant: { + default: new CreepSetup(Roles.coolant, { + pattern : [HEAL, MOVE], + sizeLimit: Infinity, + }), + small: new CreepSetup(Roles.coolant, { + pattern : [HEAL, MOVE], + sizeLimit: 16, + }), + } + + }; diff --git a/src/declarations/memory.d.ts b/src/declarations/memory.d.ts index 433e3a40d..89565bf0d 100644 --- a/src/declarations/memory.d.ts +++ b/src/declarations/memory.d.ts @@ -1,4 +1,11 @@ type operationMode = 'manual' | 'semiautomatic' | 'automatic'; +/** + * 0: Basic + * 1: Collect from enemy storage/terminal + * 2: Collect from all sources TBD + * 3: Collect all and mine walls for energy TBD + */ +type resourceCollectionMode = number; interface RawMemory { _parsed: any; @@ -21,7 +28,13 @@ interface Memory { operationMode: operationMode; log: LoggerMemory; enableVisuals: boolean; - }; + resourceCollectionMode: resourceCollectionMode; + powerCollection: { + enabled: boolean; + maxRange: number; + minPower: number; + }; + } profiler?: any; stats: any; constructionSites: { [id: string]: number }; @@ -29,6 +42,11 @@ interface Memory { resetBucket?: boolean; haltTick?: number; combatPlanner: any; + reinforcementLearning?: any; + playerCreepTracker: { + [playerName: string]: CreepTracker + }; + zoneRooms: { [roomName: string]: { [type: string]: number} }; reinforcementLearning?: { enabled?: boolean; verbosity?: number; @@ -127,6 +145,13 @@ interface PathingMemory { weightedDistances: { [pos1Name: string]: { [pos2Name: string]: number; } }; } +interface CreepTracker { + creeps: { [name: string]: number }; // first tick seen + types: { [type: string]: number}; // amount seen + parts: { [bodyPart: string]: number}; // quantity + boosts: { [boostType: string]: number}; // how many boosts are spent +} + interface FlagMemory { [_MEM.TICK]?: number; [_MEM.EXPIRATION]?: number; diff --git a/src/declarations/prototypes.d.ts b/src/declarations/prototypes.d.ts index 1d0e9f187..093f452a9 100644 --- a/src/declarations/prototypes.d.ts +++ b/src/declarations/prototypes.d.ts @@ -7,6 +7,17 @@ interface Creep { inRampart: boolean; } +interface PowerCreep { + hitsPredicted?: number; + intel?: { [property: string]: number }; + memory: CreepMemory; + fatigue: number; + body: BodyPartDefinition[]; + boosts: _ResourceConstantSansEnergy[]; + boostCounts: { [boostType: string]: number }; + inRampart: boolean; +} + interface ConstructionSite { isWalkable: boolean; } diff --git a/src/directives/Directive.ts b/src/directives/Directive.ts index 6b56b6b34..4f41368fc 100644 --- a/src/directives/Directive.ts +++ b/src/directives/Directive.ts @@ -317,6 +317,7 @@ export abstract class Directive { if (this.isPresent(pos, scope)) { return; // do nothing if flag is already here } + const room = Game.rooms[pos.roomName] as Room | undefined; if (!room) { if (!opts.memory) { diff --git a/src/directives/colony/clearRoom.ts b/src/directives/colony/clearRoom.ts index c74dc013e..475e906ed 100644 --- a/src/directives/colony/clearRoom.ts +++ b/src/directives/colony/clearRoom.ts @@ -5,6 +5,10 @@ import {Cartographer, ROOMTYPE_CONTROLLER} from '../../utilities/Cartographer'; import {printRoomName} from '../../utilities/utils'; import {MY_USERNAME} from '../../~settings'; import {Directive} from '../Directive'; +import {Zerg} from "../../zerg/Zerg"; +import {DirectiveHaul} from "../resource/haul"; +import {Pathing} from "../../movement/Pathing"; +import {DirectiveDismantle} from "../targeting/dismantle"; /** @@ -26,9 +30,12 @@ export class DirectiveClearRoom extends Directive { // Remove if misplaced if (Cartographer.roomType(this.pos.roomName) != ROOMTYPE_CONTROLLER) { log.warning(`${this.print}: ${printRoomName(this.pos.roomName)} is not a controller room; ` + - `removing directive!`); + `removing directive!`); this.remove(true); } + if (Memory.settings.resourceCollectionMode && Memory.settings.resourceCollectionMode >= 1) { + this.memory.keepStorageStructures = true; + } } spawnMoarOverlords() { @@ -42,7 +49,7 @@ export class DirectiveClearRoom extends Directive { private removeAllStructures(): boolean { const keepStorageStructures = this.memory.keepStorageStructures !== undefined - ? this.memory.keepStorageStructures : true; + ? this.memory.keepStorageStructures : true; const keepRoads = this.memory.keepRoads !== undefined ? this.memory.keepRoads : true; const keepContainers = this.memory.keepContainers !== undefined ? this.memory.keepContainers : true; @@ -52,9 +59,14 @@ export class DirectiveClearRoom extends Directive { for (const s of allStructures) { if (s.structureType == STRUCTURE_CONTROLLER) continue; if (keepStorageStructures && - (s.structureType == STRUCTURE_STORAGE || s.structureType == STRUCTURE_TERMINAL)) { + (s.structureType == STRUCTURE_STORAGE || s.structureType == STRUCTURE_TERMINAL) && !s.isEmpty) { + // Create a collection flag + DirectiveHaul.createIfNotPresent(s.pos, 'pos'); continue; } + if (s.structureType == STRUCTURE_NUKER && s.energy > 50000) { + DirectiveHaul.createIfNotPresent(s.pos, 'pos'); + } if (keepRoads && s.structureType == STRUCTURE_ROAD) { continue; } @@ -74,14 +86,49 @@ export class DirectiveClearRoom extends Directive { } + private findStructureBlockingController(pioneer: Zerg): Structure | undefined { + let blockingPos = Pathing.findBlockingPos(pioneer.pos, pioneer.room.controller!.pos, + _.filter(pioneer.room.structures, s => !s.isWalkable)); + if (blockingPos) { + let structure = blockingPos.lookFor(LOOK_STRUCTURES)[0]; + if (structure) { + return structure; + } else { + log.error(`${this.print}: no structure at blocking pos ${blockingPos.print}! (Why?)`); + } + } + } + run() { // Remove if structures are done if (this.room && this.room.my) { const done = this.removeAllStructures(); if (done) { - this.room.controller!.unclaim(); + let res = this.room.controller!.unclaim(); + // Clear up flags + for (let flag of this.room.flags) { + if (!DirectiveClearRoom.filter(flag) && !DirectiveHaul.filter(flag)) { + flag.remove(); + } + } log.notify(`Removing clearRoom directive in ${this.pos.roomName}: operation completed.`); - this.remove(); + if (res == OK) { + this.remove(); + } + } + // Clear path if controller is not reachable + } else if (this.room && this.room.creeps.length > 1) { + let currentlyDismantlingLocations = DirectiveDismantle.find(this.room.flags); + + if (currentlyDismantlingLocations.length == 0) { + let pathablePos = this.room.creeps[0] ? this.room.creeps[0].pos + : Pathing.findPathablePosition(this.room.name); + let blockingLocation = Pathing.findBlockingPos(pathablePos, this.room.controller!.pos, + _.filter(this.room.structures, s => !s.isWalkable)); + if (blockingLocation && !Directive.isPresent(blockingLocation, 'pos')) { + log.notify(`Adding dismantle directive for ${this.pos.roomName} to reach controller.`); + DirectiveDismantle.create(blockingLocation); + } } } diff --git a/src/directives/defense/invasionDefense.ts b/src/directives/defense/invasionDefense.ts index 5b27611e7..f9d1fcb59 100644 --- a/src/directives/defense/invasionDefense.ts +++ b/src/directives/defense/invasionDefense.ts @@ -34,7 +34,6 @@ export class DirectiveInvasionDefense extends Directive { } spawnMoarOverlords() { - if (!this.room) { return; } @@ -66,13 +65,68 @@ export class DirectiveInvasionDefense extends Directive { this.alert(`Invasion (hostiles: ${numHostiles})`, NotifierPriority.Critical); } + recordBaddies () { + if (!this.room) { + return; + } + let mem = Memory.playerCreepTracker; + let hostiles = this.room.hostiles; + hostiles.forEach(creep => { + if (!mem[creep.owner.username]) { + mem[creep.owner.username] = { + creeps: {}, + types: {}, + parts: {}, + boosts: {}, + }; + } + let playerMem = mem[creep.owner.username]; + if (!playerMem.creeps[creep.name]) { + playerMem.creeps[creep.name] = Game.time; + const creepType = creep.name.substr(0, creep.name.indexOf(" ")); + if (creepType == creep.name) { + // memory protection if they don't split name + return; + } + playerMem.types[creepType] = (playerMem.types[creepType]+1) || 1 ; + for (const bodyPart of creep.body) { + playerMem.parts[bodyPart.type] = (playerMem.parts[bodyPart.type])+1 || 1; + if (bodyPart.boost) { + playerMem.boosts[bodyPart.boost] = (playerMem.boosts[bodyPart.boost])+1 || 1; + } + } + } + }); + } + + cleanUpPlayerMem() { + let mem = Memory.playerCreepTracker; + for (let player of _.keys(mem)) { + let tracker = mem[player]; + for (let creep of _.keys(tracker.creeps)) { + if (tracker.creeps[creep] + 1500 < Game.time) { + delete tracker.creeps[creep]; + } + } + } + } + run(): void { if (!this.room || this.room.hostiles.length > 0) { this.memory.safeSince = Game.time; + this.recordBaddies(); + } + + if (Game.time % 5000 == 0) { + // clean up, ya this shit + this.cleanUpPlayerMem(); + } + if (this.room && this.room!.name == 'W13N45') { + CombatIntel.computeCreepDamagePotentialMatrix(this.room.dangerousPlayerHostiles); } // If there are no hostiles left in the room and everyone's healed, then remove the flag if (this.room && this.room.hostiles.length == 0 && - Game.time - this.memory.safeSince > 100 && this.room.hostileStructures.length == 0) { + (Game.time - this.memory.safeSince) > this.safeEndTime && this.room.hostileStructures.length == 0) { if (_.filter(this.room.creeps, creep => creep.hits < creep.hitsMax).length == 0) { this.remove(); } diff --git a/src/directives/initializer.ts b/src/directives/initializer.ts index 081a081bd..f60422506 100644 --- a/src/directives/initializer.ts +++ b/src/directives/initializer.ts @@ -12,6 +12,7 @@ import {Directive} from './Directive'; import {DirectiveControllerAttack} from './offense/controllerAttack'; import {DirectivePairDestroy} from './offense/pairDestroy'; import {DirectiveSwarmDestroy} from './offense/swarmDestroy'; +import {DirectivePoisonRoom} from './offense/poisonRoom'; import {DirectiveExtract} from './resource/extract'; import {DirectiveHarvest} from './resource/harvest'; import {DirectiveHaul} from './resource/haul'; @@ -25,6 +26,9 @@ import {DirectiveTargetSiege} from './targeting/siegeTarget'; import {DirectiveTerminalEmergencyState} from './terminalState/terminalState_emergency'; import {DirectiveTerminalEvacuateState} from './terminalState/terminalState_evacuate'; import {DirectiveTerminalRebuildState} from './terminalState/terminalState_rebuild'; +import {DirectivePowerMine} from "./resource/powerMine"; +import {DirectiveHarass} from "./offense/harass"; +import {DirectiveBaseOperator} from "./powerCreeps/baseOperator"; /** * This is the initializer for directives, which maps flags by their color code to the corresponding directive @@ -58,6 +62,10 @@ export function DirectiveWrapper(flag: Flag): Directive | undefined { return new DirectivePairDestroy(flag); case COLOR_PURPLE: return new DirectiveControllerAttack(flag); + case COLOR_BROWN: + return new DirectivePoisonRoom(flag); + case COLOR_WHITE: + return new DirectiveHarass(flag); } break; @@ -92,6 +100,8 @@ export function DirectiveWrapper(flag: Flag): Directive | undefined { return new DirectiveExtract(flag); case COLOR_BLUE: return new DirectiveHaul(flag); + case COLOR_RED: + return new DirectivePowerMine(flag); } break; @@ -128,6 +138,13 @@ export function DirectiveWrapper(flag: Flag): Directive | undefined { return new DirectiveRPBunker(flag); } break; + // Power directives ==================================================================================== + case COLOR_CYAN: + switch (flag.secondaryColor) { + case COLOR_PURPLE: + return new DirectiveBaseOperator(flag); + } + break; } } diff --git a/src/directives/offense/harass.ts b/src/directives/offense/harass.ts new file mode 100644 index 000000000..4fc6732b2 --- /dev/null +++ b/src/directives/offense/harass.ts @@ -0,0 +1,90 @@ +import {RoomIntel} from '../../intel/RoomIntel'; +import {profile} from '../../profiler/decorator'; +import {Directive} from '../Directive'; +import {HarassOverlord} from "../../overlords/offense/harass"; +import {log, Log} from "../../console/log"; +import {MY_USERNAME} from "../../~settings"; + +interface DirectiveHarassMemory extends FlagMemory { + enhanced?: boolean; + targetPlayer?: string; + roomsToHarass: string[]; +} + +/** + * Harass Directive that wanders through enemy rooms killing stuff + * Placed on an enemy room, it will harass all of it's remotes periodically + */ +@profile +export class DirectiveHarass extends Directive { + + static directiveName = 'harass'; + static color = COLOR_RED; + static secondaryColor = COLOR_WHITE; + + memory: DirectiveHarassMemory; + + constructor(flag: Flag) { + super(flag); + this.memory.targetPlayer = RoomIntel.roomOwnedBy(flag.pos.roomName); + if (this.memory.targetPlayer == MY_USERNAME) { + log.error(`Ahhhhhh harassing self in room ${flag.pos.roomName}`); + this.remove(); + } + if (this.memory.targetPlayer) { + this.memory.roomsToHarass = this.findNearbyReservedRooms(flag.pos.roomName, this.memory.targetPlayer); + } + } + + spawnMoarOverlords() { + // For now, just spawn from RCL 5+ rooms + this.overlords.harassOverlord = new HarassOverlord(this, this.memory.enhanced); + } + + init(): void { + // if + // if (!this.memory.roomsToHarass && this.memory.targetPlayer) + + + } + + findNearbyReservedRoomsForHarassment() { + if (this.memory.targetPlayer) { + return this.findNearbyReservedRooms(this.flag.pos.roomName, this.memory.targetPlayer); + } + return []; + } + + /** + * Finds the rooms to harass + * + * @param roomName + * @param playerName + */ + findNearbyReservedRooms(roomName: string, playerName: string): string[] { + if (!this.memory.targetPlayer) { + log.error(`Unable to find which player to harass in room ${roomName}`); + return []; + } + let reservedByTargetPlayer: string[] = []; + let adjacentRooms = _.values(Game.map.describeExits(roomName)) as string[]; + adjacentRooms.forEach(room => { + if (RoomIntel.roomReservedBy(room) == playerName) { + reservedByTargetPlayer.push(room); + // This will double add rooms next to owned rooms, making it more likely to harass them + (_.values(Game.map.describeExits(room)) as string[]).forEach(room => { + if (RoomIntel.roomReservedBy(room) == playerName) { + reservedByTargetPlayer.push(room); + } + }) + } + }); + Game.notify(`Looking for nearby rooms to harass, found ${reservedByTargetPlayer}`); + return reservedByTargetPlayer; + } + + run(): void { + // Probably something modifying frequency of harassment + + } +} diff --git a/src/directives/offense/poisonRoom.ts b/src/directives/offense/poisonRoom.ts new file mode 100644 index 000000000..8eba65d93 --- /dev/null +++ b/src/directives/offense/poisonRoom.ts @@ -0,0 +1,166 @@ +import {ClaimingOverlord} from '../../overlords/colonization/claimer'; +import {StationaryScoutOverlord} from '../../overlords/scouting/stationary'; +import {RoomPoisonerOverlord} from '../../overlords/offense/roomPoisoner'; +import {log} from '../../console/log'; +import {profile} from '../../profiler/decorator'; +import {Cartographer, ROOMTYPE_CONTROLLER} from '../../utilities/Cartographer'; +import {printRoomName} from '../../utilities/utils'; +import {MY_USERNAME} from '../../~settings'; +import {Directive} from '../Directive'; +import {DirectiveControllerAttack} from './controllerAttack'; +import {DirectiveOutpostDefense} from '../defense/outpostDefense'; + + +/** + * Poison sources in remote rooms by claiming controller, walling in its sources and controller then unclaiming it. + */ +@profile +export class DirectivePoisonRoom extends Directive { + + static directiveName = 'contaminate'; + static color = COLOR_RED; + static secondaryColor = COLOR_BROWN; + static requiredRCL = 4; + static poisonSourcesOnly = false; + + walkableSourcePosisions: RoomPosition[]; + walkableControllerPosisions: RoomPosition[]; + + overlords: { + claim: ClaimingOverlord; + roomPoisoner: RoomPoisonerOverlord; + scout: StationaryScoutOverlord; + }; + + constructor(flag: Flag) { + super(flag, colony => colony.level >= DirectivePoisonRoom.requiredRCL); + // Remove if misplaced + if (Cartographer.roomType(this.pos.roomName) != ROOMTYPE_CONTROLLER) { + log.warning(`${this.print}: ${printRoomName(this.pos.roomName)} is not a controller room; ` + + `removing directive!`); + this.remove(true); + } + /* commented: keep the directive running for constant checking by stationary scout + // remove if already contaminated (if visible) + if (this.isPoisoned()) { + log.warning(`${this.print}: ${printRoomName(this.pos.roomName)} is already contaminated; ` + + `removing directive!`); + this.remove(true); + } + + // remove if no spare GCL + if(_.filter(Game.rooms, room => room.my).length == Game.gcl.level){ + log.warning(`${this.print}: ${printRoomName(this.pos.roomName)} not enough GCL; ` + + `removing directive!`); + this.remove(true); + } + */ + } + + spawnMoarOverlords() { + if(!this.pos.isVisible){ + this.overlords.scout = new StationaryScoutOverlord(this); + } else if(this.room && this.room.dangerousPlayerHostiles.length == 0 && !this.isPoisoned()){ + this.overlords.claim = new ClaimingOverlord(this); + this.overlords.roomPoisoner = new RoomPoisonerOverlord(this); + } + } + + init() { + this.alert(`Poisoning Room ${this.pos.roomName}`); + if(this.room && this.room.controller){ + this.walkableSourcePosisions = _.filter(_.flatten(_.map(this.room.sources, s => s.pos.neighbors)),pos => pos.isWalkable(true)); + this.walkableControllerPosisions = _.filter(this.room.controller!.pos.neighbors, pos => pos.isWalkable(true)); + } + if(this.room && this.room.controller && this.room.controller.reservation && this.room.controller.reservation.ticksToEnd > 500){ + DirectiveControllerAttack.createIfNotPresent(this.room.controller.pos,'room'); + } + if(this.room && this.room.playerHostiles.length > 0 && !this.isPoisoned()){ + DirectiveOutpostDefense.createIfNotPresent(new RoomPosition(25,25,this.room.name),'room'); + } + } + + private poison() { + if (this.room && this.room.controller!.level > 1) { + //wall csites are not walkable, block sources only if roomPoisoner.carry.energy > 0 + if (this.overlords.roomPoisoner.roomPoisoners.length && + this.overlords.roomPoisoner.roomPoisoners[0].carry.energy > 0){ + //Check for walkable source.pos.neighbors and place wall constuction site + if(this.walkableSourcePosisions.length){ + _.forEach(this.walkableSourcePosisions,pos=>{pos.createConstructionSite(STRUCTURE_WALL)}); + } + } else { + //remove all csites if roomPoisoner.carry.energy == 0, wall csites are not walkable + if(this.room){ + _.forEach(this.room.constructionSites, csite => {csite.remove();} ); + } + } + if(!DirectivePoisonRoom.poisonSourcesOnly){ + //Check for walkable controller.pos.neighbors and place wall constuction site + if(this.walkableControllerPosisions.length){ + _.forEach(this.walkableControllerPosisions,pos=>{pos.createConstructionSite(STRUCTURE_WALL)}); + } + } + + } + } + private isPoisoned(): boolean { + let result = false; + if (this.room && this.room.controller!.level > 1) { + result = !!this.walkableSourcePosisions && !this.walkableSourcePosisions.length; + if(!DirectivePoisonRoom.poisonSourcesOnly){ + result = result && !!this.walkableControllerPosisions && !this.walkableControllerPosisions.length; + } + return result; + } else { + return false; + } + } + + run() { + + + if (Game.time % 25 == 0 && this.room && this.room.my) { + + if(_.filter(Game.rooms, room => room.my).length == Game.gcl.level){ + log.warning(`${this.print}: ${printRoomName(this.pos.roomName)} not enough GCL; ` + + `for contamination directive!`); + } + + //kill claimer if room claimed, it is can be blocking wall csite creation + if (this.overlords.claim.claimers.length){ + this.overlords.claim.claimers[0].suicide(); + } + //remove any containers that can be next to sources + if(this.room.containers.length){ + _.forEach(this.room.containers, container => {container.destroy();}); + } + + //remove any hostile consituction sites + _.forEach(this.room.find(FIND_HOSTILE_CONSTRUCTION_SITES), csite => {csite.remove();}); + + if (this.isPoisoned()) { + //remove roads before unclaiming + if(this.room.roads.length > 0){ + _.forEach(this.room.roads, road => {road.destroy();} ); + } + this.room.controller!.unclaim(); + //log.notify(`Removing poisonRoom directive in ${this.pos.roomName}: operation completed.`); + //this.remove(); + } else { + this.poison(); + } + } + + // Remove if owned by other player + if (Game.time % 10 == 2 && this.room && !!this.room.owner && this.room.owner != MY_USERNAME) { + log.notify(`Removing poisonRoom directive in ${this.pos.roomName}: room already owned by another player.`); + this.remove(); + } + } +} + + + + + diff --git a/src/directives/offense/swarmDestroy.ts b/src/directives/offense/swarmDestroy.ts index eea957e82..f02fc7644 100644 --- a/src/directives/offense/swarmDestroy.ts +++ b/src/directives/offense/swarmDestroy.ts @@ -13,13 +13,14 @@ export class DirectiveSwarmDestroy extends Directive { static directiveName = 'destroy'; static color = COLOR_RED; static secondaryColor = COLOR_RED; + static requiredRCL = 6; overlords: { destroy: SwarmDestroyerOverlord; }; constructor(flag: Flag) { - super(flag); + super(flag, colony => colony.level >= DirectiveSwarmDestroy.requiredRCL); } spawnMoarOverlords() { diff --git a/src/directives/powerCreeps/baseOperator.ts b/src/directives/powerCreeps/baseOperator.ts new file mode 100644 index 000000000..0bee0feb3 --- /dev/null +++ b/src/directives/powerCreeps/baseOperator.ts @@ -0,0 +1,273 @@ +import {CombatPlanner, SiegeAnalysis} from "../../strategy/CombatPlanner"; +import {profile} from "../../profiler/decorator"; +import {Directive} from "../Directive"; +import {log} from "../../console/log"; +import {Visualizer} from "../../visuals/Visualizer"; +import {Power} from "./powers/genericPower"; +import {GenerateOps} from "./powers/generateOps"; + + +interface DirectiveBaseOperatorMemory extends FlagMemory { + powerPriorities: PowerConstant[]; +} + +export enum types { + opgen, + baseoperator, + basedefender +} + +/** + * Simple directive to run a power creep where the flag name is the power creep name + */ +@profile +export class DirectiveBaseOperator extends Directive { + + static directiveName = 'BaseOperator'; + static color = COLOR_CYAN; + static secondaryColor = COLOR_PURPLE; + + memory: DirectiveBaseOperatorMemory; + + // Power Creep Hack + powerCreep: PowerCreep; + + defaultPowerPriorities: PowerConstant[] = [ + PWR_GENERATE_OPS, + PWR_REGEN_SOURCE, + PWR_OPERATE_TOWER, + PWR_OPERATE_LAB, + PWR_OPERATE_SPAWN, + PWR_OPERATE_EXTENSION, + PWR_REGEN_MINERAL]; + + // overlords: { + // scout?: StationaryScoutOverlord; + // destroy?: SwarmDestroyerOverlord | PairDestroyerOverlord; + // guard?: OutpostDefenseOverlord; + // controllerAttack?: ControllerAttackerOverlord; + // }; + + constructor(flag: Flag) { + super(flag); + this.powerCreep = Game.powerCreeps[flag.name]; + if (!this.powerCreep) { + log.error(`Power Creep not found for ${this.print}, deleting directive`); + this.remove(); + } + this.memory.powerPriorities = this.memory.powerPriorities || this.defaultPowerPriorities; + } + + spawnMoarOverlords() { + } + + init(): void { + + } + + + // Wrapped powerCreep methods =========================================================================================== + + renew(powerSource: StructurePowerBank | StructurePowerSpawn) { + if (this.powerCreep.pos.inRangeToPos(powerSource.pos, 1)) { + return this.powerCreep.renew(powerSource); + } else { + return this.powerCreep.moveTo(powerSource, {ignoreRoads: true, range: 1, swampCost: 1, reusePath: 0, visualizePathStyle: {lineStyle: "dashed", fill: 'yellow'}}); + } + } + + enablePower(controller: StructureController) { + log.alert(`Trying to enable power for ${controller} with `); + if (this.powerCreep.pos.inRangeToPos(controller.pos, 1)) { + return this.powerCreep.enableRoom(controller); + } else { + //let path = this.powerCreep.pos.findPathTo(controller, {ignoreRoads: true, range: 1, swampCost: 1}); + //log.alert(`Trying to enable power for ${controller} with ${JSON.stringify(path)}`); + //return this.powerCreep.moveByPath(path); + return this.powerCreep.moveTo(controller.pos, {ignoreRoads: true, range: 1, swampCost: 1, reusePath: 0, visualizePathStyle: {lineStyle: "solid"}}); + } + } + + usePower(power: PowerConstant) { + console.log(`The power constant is ${power}`) + switch(power) { + case PWR_GENERATE_OPS: return new GenerateOps(this.powerCreep); +// case PWR_OPERATE_SPAWN: return this.operateSpawn(); + } + + } + // + // /** + // * Generate 1/2/4/6/8 ops resource units. Cooldown 50 ticks. Required creep level: 0/2/7/14/22. + // */ + // generateOps() { + // if (this.powerCreep.powers[PWR_GENERATE_OPS].cooldown !> 0) { + // return this.powerCreep.usePower(PWR_GENERATE_OPS); + // } + // return ERR_TIRED; + // } + // + // operateSpawn(spawn?: StructureSpawn) { + // // if (this.powerCreep.powers[PWR_oper]) + // // if (!spawn) { + // // spawn = _.first(this.room!.spawns.filter(spawn => spawn.effects.length == 0)); + // // if (!spawn) { + // // return ERR; + // // } + // // } + // if (this.pos.inRangeToPos(spawn.pos, 1)) { + // return this.powerCreep.usePower(PWR_OPERATE_SPAWN, spawn); + // } else { + // return this.powerCreep.moveTo(spawn); + // } + // } + // + // operateTower(tower: StructureTower) { + // if (this.pos.inRangeToPos(tower.pos, POWER_INFO[PWR_OPERATE_TOWER].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_TOWER, tower); + // } else { + // return this.powerCreep.moveTo(tower); + // } + // } + // + // operateStorage(storage: StructureStorage) { + // if (this.pos.inRangeToPos(storage.pos, POWER_INFO[PWR_OPERATE_STORAGE].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_STORAGE, storage); + // } else { + // return this.powerCreep.moveTo(storage); + // } + // } + // + // operateExtensions(container: StructureStorage | StructureTerminal | StructureContainer) { + // if (this.pos.inRangeToPos(container.pos, POWER_INFO[PWR_OPERATE_EXTENSION].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_EXTENSION, container); + // } else { + // return this.powerCreep.moveTo(container); + // } + // } + // + // operateObserver(observer: StructureObserver) { + // if (this.pos.inRangeToPos(observer.pos, POWER_INFO[PWR_OPERATE_OBSERVER].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_OBSERVER, observer); + // } else { + // return this.powerCreep.moveTo(observer); + // } + // } + // + // operateTerminal(terminal: StructureTerminal) { + // if (this.pos.inRangeToPos(terminal.pos, POWER_INFO[PWR_OPERATE_TERMINAL].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_TERMINAL, terminal); + // } else { + // return this.powerCreep.moveTo(terminal); + // } + // } + // + // operatePower(power: StructurePowerSpawn) { + // if (this.pos.inRangeToPos(power.pos, POWER_INFO[PWR_OPERATE_POWER].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_POWER, power); + // } else { + // return this.powerCreep.moveTo(power); + // } + // } + // + // operateController(controller: StructureController) { + // if (this.pos.inRangeToPos(controller.pos, POWER_INFO[PWR_OPERATE_CONTROLLER].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_CONTROLLER, controller); + // } else { + // return this.powerCreep.moveTo(controller); + // } + // } + // + // // operateFactory(factory: StructureFactory) { + // // if (this.pos.inRangeToPos(factory.pos, POWER_INFO[PWR_OPERATE_FACTORY].range)) { + // // return this.powerCreep.usePower(PWR_OPERATE_FACTORY, factory); + // // } else { + // // return this.moveTo(factory); + // // } + // // } + // + // shield() { + // if (this.powerCreep.powers[PWR_SHIELD].cooldown !> 0) { + // return this.powerCreep.usePower(PWR_SHIELD); + // } + // return ERR_TIRED; + // } + // + // regenSource(source : Source) { + // if (this.pos.inRangeToPos(source.pos, POWER_INFO[PWR_REGEN_SOURCE].range)) { + // return this.powerCreep.usePower(PWR_REGEN_SOURCE, source); + // } else { + // return this.powerCreep.moveTo(source); + // } + // } + // + // regenMineral(mineral: Mineral) { + // if (this.pos.inRangeToPos(mineral.pos, POWER_INFO[PWR_REGEN_MINERAL].range)) { + // return this.powerCreep.usePower(PWR_REGEN_MINERAL, mineral); + // } else { + // return this.powerCreep.moveTo(mineral); + // } + // } + // + // fortify(rampart: StructureRampart) { + // if (this.pos.inRangeToPos(rampart.pos, POWER_INFO[PWR_FORTIFY].range)) { + // return this.powerCreep.usePower(PWR_FORTIFY, rampart); + // } else { + // return this.powerCreep.moveTo(rampart); + // } + // } + // + // operateLab(lab: StructureLab) { + // if (this.pos.inRangeToPos(lab.pos, POWER_INFO[PWR_OPERATE_LAB].range)) { + // return this.powerCreep.usePower(PWR_OPERATE_LAB, lab); + // } else { + // return this.powerCreep.moveTo(lab); + // } + // } + + + runPowers() { + const priorities = this.memory.powerPriorities; + console.log(`Powerid of priority list of ${priorities}`); + for (let powerId in priorities) { + console.log(`Powerid of ${powerId} and list of ${priorities}`); + let powerToUse = this.usePower(priorities[powerId]); + if (powerToUse && powerToUse.operatePower()) { + break; + } + } + } + + + run(): void { + + // For the power creeps that just sit on power spawn + const isStationary = this.powerCreep.name.toLowerCase().indexOf(types.basedefender.toString()); + + console.log(`Running power creep ${JSON.stringify(this.powerCreep)} with ttl ${this.powerCreep.ticksToLive} with ${this.room!.powerSpawn}`); + if (!this.room) { + return; + } else if (!this.powerCreep.ticksToLive && this.room && this.room.powerSpawn) { + // Spawn creep + let res = this.powerCreep.spawn(this.room.powerSpawn); + log.alert(`Running ${this.powerCreep} with spawn of ${res}`); + } else if (this.room.controller && !this.room.controller.isPowerEnabled && !isStationary) { + // Enable power + let res = this.enablePower(this.room.controller); + log.alert(`Running ${this.powerCreep} with enable power of ${res}`); + } else if (this.powerCreep && this.powerCreep.ticksToLive && this.powerCreep.ticksToLive < 900 && this.room.powerSpawn) { + let res = this.renew(this.room.powerSpawn); + log.alert(`Running ${this.powerCreep} with renew of ${res}`); + } else { + let res = this.runPowers(); + log.alert(`Running ${this.powerCreep} with power of ${res}`); + } + + + } + + visuals(): void { + Visualizer.marker(this.pos, {color: 'red'}); + } + +} \ No newline at end of file diff --git a/src/directives/powerCreeps/powers/generateOps.ts b/src/directives/powerCreeps/powers/generateOps.ts new file mode 100644 index 000000000..2f02070bd --- /dev/null +++ b/src/directives/powerCreeps/powers/generateOps.ts @@ -0,0 +1,31 @@ +import {profile} from "../../../profiler/decorator"; +import {Power} from "./genericPower"; +import {log} from "../../../console/log"; + +export const powerId = PWR_GENERATE_OPS; + +/** + * An abstract class for encapsulating power creep power usage. + */ +@profile +export class GenerateOps extends Power { + + constructor(powerCreep: PowerCreep, target?: RoomObject) { + super(powerCreep, target); + } + + operatePower() { + if (this.powerCreep.carry.ops && this.powerCreep.carry.ops > (this.powerCreep.carryCapacity * 0.9)) { + const terminal = this.powerCreep.room!.terminal; + if (!terminal) { + log.error(`Ops power creep with no storage`); + } else { + this.powerCreep.moveTo(terminal); + this.powerCreep.transfer(terminal, RESOURCE_OPS, this.powerCreep.carry.ops); + } + } else { + return this.powerCreep.usePower(powerId); + } + return ERR_TIRED; + } +} \ No newline at end of file diff --git a/src/directives/powerCreeps/powers/genericPower.ts b/src/directives/powerCreeps/powers/genericPower.ts new file mode 100644 index 000000000..97a07042e --- /dev/null +++ b/src/directives/powerCreeps/powers/genericPower.ts @@ -0,0 +1,72 @@ +import {profile} from "../../../profiler/decorator"; +import {powerId} from "./generateOps"; +import {log} from "../../../console/log"; + +/** + * An abstract class for encapsulating power creep power usage. + */ +@profile +export abstract class Power { + static powerId: PowerConstant; + + _target: { // Data for the target the task is directed to: + ref: string; // Target id or name + _pos: ProtoPos; // Target position's coordinates in case vision is lost + }; + + + _powerCreep: { + name: string; + }; + + constructor(powerCreep: PowerCreep, target?: RoomObject) { + log.notify(`Creating power task for ${powerCreep}`); + this._powerCreep = { + name: powerCreep.name, + }; + if (target) { + this._target = { + ref : target.ref, + _pos: target.pos, + } + } + + } + + /** + * Dereferences the Task's target + */ + get target(): RoomObject | null { + return deref(this._target.ref); + } + + canRunPower() { + const power = this.powerCreep.powers[powerId]; + return power && power.level > 0 && power.cooldown == 0; + } + + /** + * Return the wrapped creep which is executing this task + */ + get powerCreep(): PowerCreep { // Get task's own creep by its name + // Returns zerg wrapper instead of creep to use monkey-patched functions + return Game.powerCreeps[this._powerCreep.name]; + } + + /** + * Set the creep which is executing this task + */ + set powerCreep(pc: PowerCreep) { + this._powerCreep.name = pc.name; + } + + run() { + if (this.canRunPower()) { + this.operatePower() + } + } + + operatePower() { + + } +} \ No newline at end of file diff --git a/src/directives/resource/extract.ts b/src/directives/resource/extract.ts index 7a75ba02a..989c3e58a 100644 --- a/src/directives/resource/extract.ts +++ b/src/directives/resource/extract.ts @@ -47,6 +47,9 @@ export class DirectiveExtract extends Directive { if (this.colony.level < 6) { log.notify(`Removing extraction directive in ${this.pos.roomName}: room RCL insufficient.`); this.remove(); + } else if (!this.colony.terminal) { + log.notify(`Removing extraction directive in ${this.pos.roomName}: room is missing terminal.`); + this.remove(); } } diff --git a/src/directives/resource/haul.ts b/src/directives/resource/haul.ts index ead800fb9..1d04e9d3f 100644 --- a/src/directives/resource/haul.ts +++ b/src/directives/resource/haul.ts @@ -10,7 +10,7 @@ interface DirectiveHaulMemory extends FlagMemory { /** - * Hauling directive: spawns hauler creeps to move large amounts of resourecs from a location (e.g. draining a storage) + * Hauling directive: spawns hauler creeps to move large amounts of resources from a location (e.g. draining a storage) */ @profile export class DirectiveHaul extends Directive { @@ -21,6 +21,7 @@ export class DirectiveHaul extends Directive { private _store: StoreDefinition; private _drops: { [resourceType: string]: Resource[] }; + private _finishAtTime: number; memory: DirectiveHaulMemory; @@ -106,7 +107,11 @@ export class DirectiveHaul extends Directive { } run(): void { - if (this.totalResources == 0) { + if (_.sum(this.store) == 0 && this.pos.isVisible) { + // If everything is picked up, crudely give enough time to bring it back + this._finishAtTime = this._finishAtTime || (Game.time + 300); + } + if (Game.time >= this._finishAtTime) { this.remove(); } } diff --git a/src/directives/resource/powerMine.ts b/src/directives/resource/powerMine.ts new file mode 100644 index 000000000..c48cf3eb7 --- /dev/null +++ b/src/directives/resource/powerMine.ts @@ -0,0 +1,163 @@ +import {Directive} from '../Directive'; +import {profile} from '../../profiler/decorator'; +import {PowerDrillOverlord} from '../../overlords/powerMining/PowerDrill'; +import {calculateFormationStrength} from "../../utilities/creepUtils"; +import {PowerHaulingOverlord} from "../../overlords/powerMining/PowerHauler"; +import {log} from "../../console/log"; + + +interface DirectivePowerMineMemory extends FlagMemory { + totalResources?: number; + /* + 0: init + 1: mining started + 2: mining near done, hauling started + 3: mining done + 4: hauling picking is complete + */ + state: number; +} + + +/** + * PowerMining directive: kills power banks and collects the resources. + */ +@profile +export class DirectivePowerMine extends Directive { + + static directiveName = 'powerMine'; + static color = COLOR_YELLOW; + static secondaryColor = COLOR_RED; + static requiredRCL = 7; + + private _powerBank: StructurePowerBank | undefined; + private _drops: { [resourceType: string]: Resource[] }; + + memory: DirectivePowerMineMemory; + + constructor(flag: Flag) { + super(flag, colony => colony.level >= DirectivePowerMine.requiredRCL); + this._powerBank = this.powerBank; + this.memory.state = this.memory.state || 0; + } + + spawnMoarOverlords() { + if (this.memory.state < 3) { + this.overlords.powerMine = new PowerDrillOverlord(this); + } + if (this.memory.state > 1) { + this.overlords.powerHaul = new PowerHaulingOverlord(this); + } + } + + get drops(): { [resourceType: string]: Resource[] } { + if (!this.pos.isVisible) { + return {}; + } + if (!this._drops) { + let drops = (this.pos.lookFor(LOOK_RESOURCES) || []) as Resource[]; + this._drops = _.groupBy(drops, drop => drop.resourceType); + } + return this._drops; + } + + get hasDrops(): boolean { + return _.keys(this.drops).length > 0; + } + + get powerBank(): StructurePowerBank | undefined { + this._powerBank = this._powerBank || this.room ? this.flag.pos.lookForStructure(STRUCTURE_POWER_BANK) as StructurePowerBank : undefined; + return this._powerBank; + } + + /** + * Total amount of resources remaining to be transported; cached into memory in case room loses visibility + */ + get totalResources(): number { + if (this.memory.totalResources == undefined) { + return 5000; // pick some non-zero number so that powerMiners will spawn + } + if (this.pos.isVisible) { + this.memory.totalResources = this.powerBank ? this.powerBank.power : this.memory.totalResources; // update total amount remaining + } + return this.memory.totalResources; + } + + calculateRemainingLifespan() { + if (!this.room) { + return undefined; + } else if (this.powerBank == undefined) { + return 0; + } else { + let tally = calculateFormationStrength(this.powerBank.pos.findInRange(FIND_MY_CREEPS, 4)); + let healStrength: number = tally.heal * HEAL_POWER || 0; + let attackStrength: number = tally.attack * ATTACK_POWER || 0; + // PB have 50% hitback, avg damage is attack strength if its enough healing, otherwise healing + let avgDamagePerTick = Math.min(attackStrength, healStrength*2); + return this.powerBank.hits / avgDamagePerTick; + } + } + + + manageState() { + let currentState = this.memory.state; + log.debug(`Managing state ${currentState} of directive ${this.print} with PB ${this.powerBank}`); + if (currentState == 0 && this.powerBank && this.powerBank.hits < this.powerBank.hitsMax) { + if (this.powerBank.pos.findInRange(FIND_MY_CREEPS, 3).length == 0 && this.powerBank.pos.findInRange(FIND_HOSTILE_CREEPS, 3).length > 0) { + // Power bank is damage but we didn't mine it + Game.notify(`Power bank mining ${this.print} failed as someone else is mining this location.`); + log.alert(`Power bank mining ${this.print} failed as someone else is mining this location.`); + this.remove(); + } else { + // Set to mining started + this.memory.state = 1; + } + } else if ((currentState == 0 || currentState == 1) && this.room && (!this.powerBank || this.powerBank.hits < 500000)) { + Game.notify('Activating spawning haulers for power mining in room ' + this.pos.roomName); + log.info('Activating spawning haulers for power mining in room ' + this.pos.roomName); + this.memory.state = 2; + } else if ((currentState == 0 || currentState == 1 || currentState == 2) && this.room && this.pos.isVisible && !this.powerBank) { + if (!this.hasDrops) { + // TODO this had an error where it triggered incorrectly + Game.notify(`WE FAILED. SORRY CHIEF, COULDN'T FINISH POWER MINING IN ${this.print} DELETING Directive at time ${Game.time}`); + log.error(`WE FAILED. SORRY CHIEF, COULDN'T FINISH POWER MINING IN ${this.room} DELETING Directive at time: ${Game.time}`); + this.remove(); + } else { + // If somehow there is no bank but there is drops where bank was + this.memory.state = 3; + } + } else if (currentState == 2 && this.room && (!this.powerBank) && this.hasDrops) { + Game.notify(`Mining is complete for ${this.print} in ${this.room.print} at time ${Game.time}`); + log.alert(`Mining is complete for ${this.print} in ${this.room.print} at time ${Game.time}`); + this.memory.state = 3; + // TODO reassign them to guard the bank + delete this.overlords["powerMine"]; + this._powerBank = undefined; // This might be fluff + } else if (currentState == 3 && this.room && this.pos.isVisible && !this.hasDrops) { + Game.notify(`Hauler pickup is complete for ${this.print} in ${this.room.print} at time ${Game.time}`); + // Hauler pickup is now complete + log.alert(`Hauler pickup is complete for ${this.print} in ${this.room.print} at time ${Game.time}`); + this.memory.state = 4; + // TODO Stop spawning haulers + } else if (currentState == 4 && this.overlords.powerHaul && (this.overlords.powerHaul as PowerHaulingOverlord).checkIfStillCarryingPower() == undefined) { + // TODO Doesn't give enough time to pick up power + Game.notify(`Hauling complete for ${this.print} at time ${Game.time}. Final power collected was ${(this.overlords.powerHaul as PowerHaulingOverlord).totalCollected} out of ${this.memory.totalResources}`); + log.alert(`Hauling complete for ${this.print} at time ${Game.time}. Final power collected was ${(this.overlords.powerHaul as PowerHaulingOverlord).totalCollected} out of ${this.memory.totalResources}`); + this.remove(); + } else { + log.debug(`Power mining ${this.print} is in state ${currentState}`); + // Todo this isn't error but needs other stuff + } + } + + init(): void { + this.alert(`PowerMine directive active`); + } + + run(): void { + if (Game.time % 9 == 0) { + this.manageState(); + } + } +} + diff --git a/src/directives/situational/nukeResponse.ts b/src/directives/situational/nukeResponse.ts index ccdbe6494..bd592d849 100644 --- a/src/directives/situational/nukeResponse.ts +++ b/src/directives/situational/nukeResponse.ts @@ -41,16 +41,27 @@ export class DirectiveNukeResponse extends Directive { } } + /** + * Returns whether a position should be reinforced or not + * @param pos + * @param ignoreExtensions + */ + static shouldReinforceLocation(pos: RoomPosition, ignoreExtensions = true) { + let dontReinforce: StructureConstant[] = [STRUCTURE_ROAD, STRUCTURE_RAMPART, STRUCTURE_WALL]; + if (ignoreExtensions) { dontReinforce.push(STRUCTURE_EXTENSION);} + return _.filter(pos.lookFor(LOOK_STRUCTURES), + s => !_.contains(dontReinforce, s.structureType)).length > 0; + } + run(): void { // Build ramparts at all positions affected by nukes with structures on them if (Game.time % 50 == 0) { if (this.nuke) { const rampartPositions = _.filter(this.nuke.pos.getPositionsInRange(2), function(pos) { // Rampart should be built to protect all non-road, non-barrier structures in nuke range - return _.filter(pos.lookFor(LOOK_STRUCTURES), - s => s.structureType != STRUCTURE_ROAD && - s.structureType != STRUCTURE_RAMPART && - s.structureType != STRUCTURE_WALL).length > 0; + return DirectiveNukeResponse.shouldReinforceLocation(pos, + // only reinforce extensions if room has spare energy + pos.room && pos.room.storage && pos.room.storage.energy > 500000); }); for (const pos of rampartPositions) { // Build a rampart if there isn't one already diff --git a/src/hiveClusters/commandCenter.ts b/src/hiveClusters/commandCenter.ts index 51e0c07ef..423373640 100644 --- a/src/hiveClusters/commandCenter.ts +++ b/src/hiveClusters/commandCenter.ts @@ -135,12 +135,16 @@ export class CommandCenter extends HiveCluster { } } // Refill power spawn - if (this.powerSpawn && this.powerSpawn.energy < this.powerSpawn.energyCapacity) { - this.transportRequests.requestInput(this.powerSpawn, Priority.NormalLow); + if (this.powerSpawn) { + if (this.powerSpawn.energy < this.powerSpawn.energyCapacity) { + this.transportRequests.requestInput(this.powerSpawn, Priority.NormalLow); + } else if (this.powerSpawn.power < this.powerSpawn.powerCapacity) { + this.transportRequests.requestInput(this.powerSpawn, Priority.Low, {resourceType: RESOURCE_POWER}); + } } // Refill nuker with low priority if (this.nuker) { - if (this.nuker.energy < this.nuker.energyCapacity && this.storage.energy > 200000) { + if (this.nuker.energy < this.nuker.energyCapacity && this.storage.energy > 200000 && this.nuker.cooldown <= 1000) { this.transportRequests.requestInput(this.nuker, Priority.Low); } if (this.nuker.ghodium < this.nuker.ghodiumCapacity diff --git a/src/hiveClusters/upgradeSite.ts b/src/hiveClusters/upgradeSite.ts index b0509830d..9da1162cb 100644 --- a/src/hiveClusters/upgradeSite.ts +++ b/src/hiveClusters/upgradeSite.ts @@ -10,6 +10,7 @@ import {HiveCluster} from './_HiveCluster'; interface UpgradeSiteMemory { stats: { downtime: number }; + speedFactor : number; // Multiplier on upgrade parts for fast growth } @@ -101,6 +102,9 @@ export class UpgradeSite extends HiveCluster { } if (this.controller.level == 8) { upgradePower = Math.min(upgradePower, 15); // don't go above 15 work parts at RCL 8 + } else if (this.controller.level >= 6) { + // Can set a room to upgrade at an accelerated rate manually + upgradePower = this.memory.speedFactor != undefined ? upgradePower*this.memory.speedFactor : upgradePower; } return upgradePower; } else { diff --git a/src/intel/CombatIntel.ts b/src/intel/CombatIntel.ts index c10f02cca..a07f90c4e 100644 --- a/src/intel/CombatIntel.ts +++ b/src/intel/CombatIntel.ts @@ -11,6 +11,7 @@ import {boostResources} from '../resources/map_resources'; import {Cartographer} from '../utilities/Cartographer'; import {toCreep, Zerg} from '../zerg/Zerg'; import {RoomIntel} from './RoomIntel'; +import {Visualizer} from "../visuals/Visualizer"; interface CombatIntelMemory { cache: { @@ -96,6 +97,31 @@ export class CombatIntel { } + static computeCreepDamagePotentialMatrix(room: Room, creeps: Creep[], startingMatrix?: CostMatrix): CostMatrix | undefined { + if (room) { + const matrix = startingMatrix || new PathFinder.CostMatrix(); + + creeps.forEach(creep => { + const meleeAttack = CombatIntel.getAttackPotential(creep); + const rangedAttack = CombatIntel.getRangedAttackPotential(creep); + // const heal = CombatIntel.getHealPotential(creep); + if (meleeAttack > 0) { + creep.pos.neighbors.forEach(pos => + matrix.set(pos.x, pos.y, matrix.get(pos.x, pos.y) + meleeAttack * ATTACK_POWER)) + } + if (rangedAttack > 0) { + creep.pos.getPositionsInRange(3).forEach(pos => + matrix.set(pos.x, pos.y, matrix.get(pos.x, pos.y) + rangedAttack * RANGED_ATTACK_POWER)) + } + }); + + Visualizer.displayCostMatrix(matrix, room.name); + + return matrix; + } + } + + // Fallback and exit calculations ================================================================================== private findBestExit(matrix: CostMatrix, towers: StructureTower[], diff --git a/src/intel/RoomIntel.ts b/src/intel/RoomIntel.ts index c9046ed45..b55295c6b 100644 --- a/src/intel/RoomIntel.ts +++ b/src/intel/RoomIntel.ts @@ -7,6 +7,11 @@ import {ExpansionEvaluator} from '../strategy/ExpansionEvaluator'; import {getCacheExpiration, irregularExponentialMovingAverage} from '../utilities/utils'; import {Zerg} from '../zerg/Zerg'; import {MY_USERNAME} from '../~settings'; +import {DirectivePowerMine} from "../directives/resource/powerMine"; +import {Cartographer, ROOMTYPE_ALLEY} from "../utilities/Cartographer"; +import {getAllColonies} from "../Colony"; +import {Segmenter} from "../memory/Segmenter"; +import {log} from "../console/log"; const RECACHE_TIME = 2500; const OWNED_RECACHE_TIME = 1000; @@ -351,9 +356,56 @@ export class RoomIntel { return 0; } + /** + * Find PowerBanks within range of maxRange and power above minPower to mine + * Creates directive to mine it + * TODO refactor when factory resources come out to be more generic + */ + private static minePowerBanks(room: Room) { + let powerSetting = Memory.settings.powerCollection; + if (powerSetting.enabled && Game.time % 300 == 0 && Cartographer.roomType(room.name) == ROOMTYPE_ALLEY) { + let powerBank = _.first(room.find(FIND_STRUCTURES).filter(struct => struct.structureType == STRUCTURE_POWER_BANK)) as StructurePowerBank; + if (powerBank != undefined && powerBank.ticksToDecay > 4000 && powerBank.power >= powerSetting.minPower) { + Game.notify(`Looking for power banks in ${room} found ${powerBank} with power ${powerBank.power} and ${powerBank.ticksToDecay} TTL.`); + if (DirectivePowerMine.isPresent(powerBank.pos, 'pos')) { + Game.notify(`Already mining room ${powerBank.room}!`); + return; + } + + let colonies = getAllColonies().filter(colony => colony.level > 6); + + for (let colony of colonies) { + let route = Game.map.findRoute(colony.room, powerBank.room); + if (route != -2 && route.length <= powerSetting.maxRange) { + Game.notify(`FOUND POWER BANK IN RANGE ${route.length}, STARTING MINING ${powerBank.room}`); + DirectivePowerMine.create(powerBank.pos); + return; + } + } + + } + } + } + + static requestZoneData() { + const checkOnTick = 123; + if (Game.time % 1000 == checkOnTick - 2) { + Segmenter.requestForeignSegment('LeagueOfAutomatedNations', 96); + } else if (Game.time % 1000 == checkOnTick - 1 ) { + const loanData = Segmenter.getForeignSegment(); + if (loanData) { + Memory.zoneRooms = loanData; + } else { + log.error('Empty LOAN data'); + } + } + } + static run(): void { let alreadyComputedScore = false; + this.requestZoneData(); + for (const name in Game.rooms) { const room: Room = Game.rooms[name]; @@ -390,7 +442,7 @@ export class RoomIntel { if (room.controller && Game.time % 5 == 0) { this.recordControllerInfo(room.controller); } - + this.minePowerBanks(room); } } diff --git a/src/logistics/SpawnGroup.ts b/src/logistics/SpawnGroup.ts index 99bb11262..39766f6cd 100644 --- a/src/logistics/SpawnGroup.ts +++ b/src/logistics/SpawnGroup.ts @@ -153,7 +153,7 @@ export class SpawnGroup { bestHatchery.enqueue(request); } else { log.warning(`Could not enqueue creep ${request.setup.role} in ${this.roomName}, ` + - `no hatchery with ${maxCost} energy capacity`); + `for Overlord ${request.overlord.name}, no hatchery with ${maxCost} energy capacity.`); } } } diff --git a/src/memory/Memory.ts b/src/memory/Memory.ts index 35db27790..626d65263 100644 --- a/src/memory/Memory.ts +++ b/src/memory/Memory.ts @@ -191,6 +191,9 @@ export class Mem { if (!Memory.settings) { Memory.settings = {} as any; } + if (!Memory.playerCreepTracker) { + Memory.playerCreepTracker = {}; + } if (!USE_PROFILER) { delete Memory.profiler; } @@ -199,6 +202,12 @@ export class Mem { operationMode: DEFAULT_OPERATION_MODE, log : {}, enableVisuals: true, + resourceCollectionMode: 0, + powerCollection: { + enabled: false, + maxRange: 5, + minPower: 5000, + }, }); if (!Memory.stats) { Memory.stats = {}; diff --git a/src/overlords/Overlord.ts b/src/overlords/Overlord.ts index 3c0ec52a2..3e90ba467 100644 --- a/src/overlords/Overlord.ts +++ b/src/overlords/Overlord.ts @@ -472,6 +472,14 @@ export abstract class Overlord { } } + getZerg() { + return this._zerg; + } + + getCombatZerg() { + return this._combatZerg; + } + /** * Request any needed boosting resources from terminal network */ diff --git a/src/overlords/colonization/pioneer.ts b/src/overlords/colonization/pioneer.ts index 085b05243..8921e33c9 100644 --- a/src/overlords/colonization/pioneer.ts +++ b/src/overlords/colonization/pioneer.ts @@ -63,9 +63,9 @@ export class PioneerOverlord extends Overlord { // Build and recharge if (pioneer.carry.energy == 0) { pioneer.task = Tasks.recharge(); - } else if (this.room && this.room.controller && - (this.room.controller.ticksToDowngrade < 2500 || !this.spawnSite) && - !(this.room.controller.upgradeBlocked > 0)) { + } else if (this.room && this.room.controller && (this.room.controller.ticksToDowngrade + < (0.1 * CONTROLLER_DOWNGRADE[this.colony.controller.level]) || !this.spawnSite) + && !(this.room.controller.upgradeBlocked > 0)) { // Save controller if it's about to downgrade or if you have nothing else to do pioneer.task = Tasks.upgrade(this.room.controller); } else if (this.spawnSite) { diff --git a/src/overlords/core/worker.ts b/src/overlords/core/worker.ts index 95f85fff3..bf51b310a 100644 --- a/src/overlords/core/worker.ts +++ b/src/overlords/core/worker.ts @@ -12,6 +12,8 @@ import {Cartographer, ROOMTYPE_CONTROLLER} from '../../utilities/Cartographer'; import {minBy} from '../../utilities/utils'; import {Zerg} from '../../zerg/Zerg'; import {Overlord, ZergOptions} from '../Overlord'; +import {DirectiveNukeResponse} from "../../directives/situational/nukeResponse"; +import {Visualizer} from "../../visuals/Visualizer"; /** * Spawns general-purpose workers, which maintain a colony, performing actions such as building, repairing, fortifying, @@ -108,8 +110,10 @@ export class WorkerOverlord extends Overlord { if (this.room.find(FIND_NUKES).length > 0) { for (const rampart of this.colony.room.ramparts) { const neededHits = this.neededRampartHits(rampart); - if (rampart.hits < neededHits) { + if (rampart.hits < neededHits && rampart.pos.findInRange(FIND_NUKES, 3).length > 0 + && DirectiveNukeResponse.shouldReinforceLocation(rampart.pos)) { this.nukeDefenseRamparts.push(rampart); + Visualizer.marker(rampart.pos, {color: 'gold'}); this.nukeDefenseHitsRemaining[rampart.id] = neededHits - rampart.hits; } } @@ -132,7 +136,7 @@ export class WorkerOverlord extends Overlord { for (const nuke of rampart.pos.lookFor(LOOK_NUKES)) { neededHits += 10e6; } - for (const nuke of rampart.pos.findInRange(FIND_NUKES, 3)) { + for (const nuke of rampart.pos.findInRange(FIND_NUKES, 2)) { if (nuke.pos != rampart.pos) { neededHits += 5e6; } @@ -334,8 +338,15 @@ export class WorkerOverlord extends Overlord { private handleWorker(worker: Zerg) { if (worker.carry.energy > 0) { - // Upgrade controller if close to downgrade - if (this.colony.controller.ticksToDowngrade <= (this.colony.level >= 4 ? 10000 : 2000)) { + + // TODO Add high priority to block controller with ramparts/walls in case of downgrade attack + // FIXME workers get stalled at controller in case of downgrade attack + // Upgrade controller if close to downgrade or if getting controller attacked/was downgraded + const downgradeLevel = CONTROLLER_DOWNGRADE[this.colony.controller.level] * + (this.colony.controller.level < 4 ? .3 : .7); + if ((!this.colony.controller.upgradeBlocked || this.colony.controller.upgradeBlocked < 30) + && (this.colony.controller.ticksToDowngrade <= downgradeLevel + || this.colony.controller.progress > this.colony.controller.progressTotal)) { if (this.upgradeActions(worker)) return; } // Repair damaged non-road non-barrier structures diff --git a/src/overlords/defense/distraction.ts b/src/overlords/defense/distraction.ts new file mode 100644 index 000000000..f0b032213 --- /dev/null +++ b/src/overlords/defense/distraction.ts @@ -0,0 +1,61 @@ +import {CreepSetup} from '../../creepSetups/CreepSetup'; +import {CombatSetups, Roles} from '../../creepSetups/setups'; +import {DirectiveInvasionDefense} from '../../directives/defense/invasionDefense'; +import {CombatIntel} from '../../intel/CombatIntel'; +import {OverlordPriority} from '../../priorities/priorities_overlords'; +import {profile} from '../../profiler/decorator'; +import {boostResources} from '../../resources/map_resources'; +import {CombatZerg} from '../../zerg/CombatZerg'; +import {CombatOverlord} from '../CombatOverlord'; + +/** + * 5 Move 1 RA creep that avoids all enemies and distracts attackers. + * Just for fun + * TODO: Make them prefer swamps when at max hp + */ +@profile +export class DistractionOverlord extends CombatOverlord { + + distractions: CombatZerg[]; + room: Room; + + static settings = { + retreatHitsPercent : 0.85, + reengageHitsPercent: 0.95, + }; + + constructor(directive: DirectiveInvasionDefense, + boosted = false, + priority = OverlordPriority.defense.rangedDefense) { + super(directive, 'distraction', priority, 1); + this.distractions = this.combatZerg(Roles.ranged); + } + + private handleDistraction(distraction: CombatZerg): void { + if (this.room.hostiles.length > 0) { + distraction.autoCombat(this.room.name, false, 5, {preferRamparts: false}); + this.taunt(distraction, this.room.hostiles[0].owner.username); + const nearbyHostiles = this.room.hostiles.filter(hostile => hostile.pos.getRangeTo(distraction) <= 6); + if (nearbyHostiles.length > 0) { + distraction.kite(nearbyHostiles); + } + } + } + + static taunt(distraction: CombatZerg, name?: string) { + const taunts: string[] = ['Heylisten!', 'Pssssst', 'So close', '๐ŸŽฃ', 'Try harder', 'Get good;)', 'Base โฌ†๏ธ', '๐Ÿ”œ', + 'โš ๏ธSwampโš ๏ธ', 'Follow me!', 'Catch Me!', `Hi ${name || ''}`, '๐Ÿ‘๐Ÿ‘๐Ÿ‘', '๐ŸŽ๏ธ VROOM']; + distraction.sayRandom(taunts, true); + } + + init() { + this.reassignIdleCreeps(Roles.ranged); + const setup = CombatSetups.hydralisks.distraction; + this.wishlist(1, setup); + } + + run() { + console.log(`Distraction overlord running in ${this.room.print} with ${this.distractions}!`); + this.autoRun(this.distractions, distraction => this.handleDistraction(distraction)); + } +} diff --git a/src/overlords/defense/rangedDefense.ts b/src/overlords/defense/rangedDefense.ts index 1ce728d34..522232b9b 100644 --- a/src/overlords/defense/rangedDefense.ts +++ b/src/overlords/defense/rangedDefense.ts @@ -15,7 +15,9 @@ import {CombatOverlord} from '../CombatOverlord'; export class RangedDefenseOverlord extends CombatOverlord { hydralisks: CombatZerg[]; + room: Room; + directive: DirectiveInvasionDefense; static settings = { retreatHitsPercent : 0.85, @@ -47,7 +49,12 @@ export class RangedDefenseOverlord extends CombatOverlord { const towerDamage = this.room.hostiles[0] ? CombatIntel.towerDamageAtPos(this.room.hostiles[0].pos) || 0 : 0; const worstDamageMultiplier = _.min(_.map(this.room.hostiles, creep => CombatIntel.minimumDamageTakenMultiplier(creep))); - return Math.ceil(.5 + 1.5 * healAmount / (worstDamageMultiplier * (hydraliskDamage + towerDamage + 1))); + let finalValue = Math.ceil(.5 + 1.5 * healAmount / (worstDamageMultiplier * (hydraliskDamage + towerDamage + 1))); + // if ((Game.time - this.directive.memory.safeSince) > this.directive.safeSpawnHaltTime) { + // console.log(`Been safe for ${(Game.time - this.directive.memory.safeSince)}, setting hydralisk wishlist in ${this.room.print} to 0.`) + // return 0 + // } + return finalValue; } init() { diff --git a/src/overlords/mining/extractor.ts b/src/overlords/mining/extractor.ts index 835b11054..f22a8ac5b 100644 --- a/src/overlords/mining/extractor.ts +++ b/src/overlords/mining/extractor.ts @@ -37,6 +37,11 @@ export class ExtractorOverlord extends Overlord { this.populateStructures(); } + // If mineral is ready to be mined, make a container + private shouldHaveContainer() { + return this.mineral && (this.mineral.mineralAmount > 0 || this.mineral.ticksToRegeneration < 2000); + } + private populateStructures() { if (Game.rooms[this.pos.roomName]) { this.extractor = this.pos.lookForStructure(STRUCTURE_EXTRACTOR) as StructureExtractor | undefined; @@ -82,8 +87,8 @@ export class ExtractorOverlord extends Overlord { } private buildOutputIfNeeded(): void { - // Create container if there is not already one being built and no link - if (!this.container) { + // Create container if there is not already one being built + if (!this.container && this.shouldHaveContainer()) { const containerSite = _.first(_.filter(this.pos.findInRange(FIND_MY_CONSTRUCTION_SITES, 2), site => site.structureType == STRUCTURE_CONTAINER)); if (!containerSite) { @@ -110,7 +115,7 @@ export class ExtractorOverlord extends Overlord { if (_.sum(drone.carry) == 0) { drone.task = Tasks.harvest(this.mineral!); } - // Else see if there is an output to depsit to or to maintain + // Else see if there is an output to deposit to or to maintain else if (this.container) { drone.task = Tasks.transferAll(this.container); // Move onto the output container if you're the only drone diff --git a/src/overlords/mining/miner.ts b/src/overlords/mining/miner.ts index 866cb7552..2af7336a3 100644 --- a/src/overlords/mining/miner.ts +++ b/src/overlords/mining/miner.ts @@ -32,6 +32,8 @@ export class MiningOverlord extends Overlord { directive: DirectiveHarvest; room: Room | undefined; source: Source | undefined; + secondSource: Source | undefined; + isDisabled: boolean; container: StructureContainer | undefined; link: StructureLink | undefined; constructionSite: ConstructionSite | undefined; @@ -65,6 +67,9 @@ export class MiningOverlord extends Overlord { this.energyPerTick = SOURCE_ENERGY_NEUTRAL_CAPACITY / ENERGY_REGEN_TIME; } this.miningPowerNeeded = Math.ceil(this.energyPerTick / HARVEST_POWER) + 1; + + //this.checkForNearbyMines(); + // Decide operating mode if (Cartographer.roomType(this.pos.roomName) == ROOMTYPE_SOURCEKEEPER) { this.mode = 'SK'; @@ -72,6 +77,9 @@ export class MiningOverlord extends Overlord { } else if (this.colony.room.energyCapacityAvailable < StandardMinerSetupCost) { this.mode = 'early'; this.setup = Setups.drones.miners.default; + } else if (this.checkIfDoubleMine() && this.colony.room.energyCapacityAvailable > DoubleMinerSetupCost) { + this.mode = 'double'; + this.setup = Setups.drones.miners.double; } else if (this.link) { this.mode = 'link'; this.setup = Setups.drones.miners.default; @@ -81,8 +89,11 @@ export class MiningOverlord extends Overlord { // todo: double miner condition } const miningPowerEach = this.setup.getBodyPotential(WORK, this.colony); + // this.minersNeeded = this.isDisabled ? 0 : Math.min(Math.ceil(this.miningPowerNeeded / miningPowerEach), + // this.pos.availableNeighbors(true).length); this.minersNeeded = Math.min(Math.ceil(this.miningPowerNeeded / miningPowerEach), - this.pos.availableNeighbors(true).length); + this.pos.availableNeighbors(true).length); + this.minersNeeded = this.isDisabled ? 0 : this.minersNeeded; // Allow drop mining at low levels this.allowDropMining = this.colony.level < MiningOverlord.settings.dropMineUntilRCL; if (this.mode != 'early' && !this.allowDropMining) { @@ -97,6 +108,51 @@ export class MiningOverlord extends Overlord { } } + checkIfDoubleMine() { + if (Game.rooms[this.pos.roomName]) { + //console.log('Double mining check'); + this.source = _.first(this.pos.lookFor(LOOK_SOURCES)); + let nearby = this.source.pos.findInRange(FIND_SOURCES, 2).filter(source => this.source != source); + if (nearby.length > 0) { + this.secondSource = nearby[0]; + // If its over 1 spot away, is there spot in between to mine? + if (!this.source.pos.isNearTo(this.secondSource)) { + let miningPos = this.source.pos.getPositionAtDirection(this.source.pos.getDirectionTo(this.secondSource.pos)); + if (!miningPos.isWalkable()) { + //console.log(`Double mining found but there is no spot between ${this.secondSource} ${this.secondSource.pos.print} isWalkable ${miningPos}`); + return false; + } + } + console.log(`Double mining found ${this.secondSource} ${this.secondSource.pos.print}`); + if (this.source.id > this.secondSource.id) { + //console.log(`This is a disabled mining ${this.directive.name} via source id`); + this.isDisabled = true; + } + return true; + } + } + return false; + } + + checkForNearbyMines() { + if (Game.rooms[this.pos.roomName]) { + //console.log('Double mining check'); + this.source = _.first(this.pos.lookFor(LOOK_SOURCES)); + let nearbySources = this.source.pos.findInRange(FIND_SOURCES, 8) + .filter(source => { + //if (source != this.source) {console.log(`Source path length is ${source.pos.findPathTo(this.source!).length} in ${source.room.print}`)} + return this.source != source && source.pos.findPathTo(this.source!).length < 8; + }); + if (nearbySources.length > 0) { + //console.log(`Nearby sources are ${nearbySources} in ${this.room!.print}`); + } + } + } + + /** + * TODO Note to self - make directive to make a double miner + */ + get distance(): number { return this.directive.distance; } @@ -146,7 +202,7 @@ export class MiningOverlord extends Overlord { } /** - * Add or remove containers as needed to keep exactly one of contaner | link + * Add or remove containers as needed to keep exactly one of container | link */ private addRemoveContainer(): void { if (this.allowDropMining) { @@ -318,6 +374,66 @@ export class MiningOverlord extends Overlord { } } + // Drop mining + if (this.allowDropMining) { + miner.harvest(this.source!); + if (miner.carry.energy > 0.8 * miner.carryCapacity) { // move over the drop when you're close to full + let biggestDrop = maxBy(miner.pos.findInRange(miner.room.droppedEnergy, 1), drop => drop.amount); + if (biggestDrop) { + miner.goTo(biggestDrop); + } + } + if (miner.carry.energy == miner.carryCapacity) { // drop when you are full + miner.drop(RESOURCE_ENERGY); + } + return; + } + } + + /** + * Actions for handling double mining + */ + private doubleMiningActions(miner: Zerg) { + // Approach mining site + if (this.goToMiningSite(miner)) return; + //console.log(`Double mining with ${miner.print} here ${this.source!.pos.print}, ${this.source}, ${this.secondSource} is disabled ${this.isDisabled}`); + + // Link mining + if (this.link) { + if (this.source && this.source.energy > 0) { + miner.harvest(this.source!); + } else { + miner.harvest(this.secondSource!); + } + if (miner.carry.energy > 0.9 * miner.carryCapacity) { + miner.transfer(this.link, RESOURCE_ENERGY); + } + return; + } else { + log.warning(`Link miner ${miner.print} has no link!`); + } + + // Container mining + if (this.container) { + if (this.container.hits < this.container.hitsMax + && miner.carry.energy >= Math.min(miner.carryCapacity, REPAIR_POWER * miner.getActiveBodyparts(WORK))) { + return miner.repair(this.container); + } else if (this.source && this.source.energy > 0) { + return miner.harvest(this.source!); + } else { + return miner.harvest(this.secondSource!); + } + } + + // Build output site + if (this.constructionSite) { + if (miner.carry.energy >= Math.min(miner.carryCapacity, BUILD_POWER * miner.getActiveBodyparts(WORK))) { + return miner.build(this.constructionSite); + } else { + return miner.harvest(this.source!); + } + } + // Drop mining if (this.allowDropMining) { miner.harvest(this.source!); @@ -379,7 +495,7 @@ export class MiningOverlord extends Overlord { case 'SK': return this.standardMiningActions(miner); case 'double': - return this.standardMiningActions(miner); + return this.doubleMiningActions(miner); default: log.error(`UNHANDLED MINER STATE FOR ${miner.print} (MODE: ${this.mode})`); } diff --git a/src/overlords/offense/harass.ts b/src/overlords/offense/harass.ts new file mode 100644 index 000000000..8fb8fc87d --- /dev/null +++ b/src/overlords/offense/harass.ts @@ -0,0 +1,80 @@ +import {CreepSetup} from '../../creepSetups/CreepSetup'; +import {CombatSetups, Roles} from '../../creepSetups/setups'; +import {DirectiveInvasionDefense} from '../../directives/defense/invasionDefense'; +import {CombatIntel} from '../../intel/CombatIntel'; +import {OverlordPriority} from '../../priorities/priorities_overlords'; +import {profile} from '../../profiler/decorator'; +import {boostResources} from '../../resources/map_resources'; +import {CombatZerg} from '../../zerg/CombatZerg'; +import {CombatOverlord} from '../CombatOverlord'; +import {DirectiveHarass} from "../../directives/offense/harass"; +import {log} from "../../console/log"; + +/** + * Spawns ranged harassers to stop mining for an enemy room + */ +@profile +export class HarassOverlord extends CombatOverlord { + + hydralisks: CombatZerg[]; + room: Room; + targetRemoteToHarass: string; + directive: DirectiveHarass; + + + static settings = { + retreatHitsPercent : 0.85, + reengageHitsPercent: 0.95, + }; + + constructor(directive: DirectiveHarass, + boosted = false, + priority = OverlordPriority.offense.harass) { + super(directive, 'harass', priority, 1); + this.directive = directive; + this.hydralisks = this.combatZerg(Roles.ranged, { + boostWishlist: boosted ? [boostResources.ranged_attack[3], boostResources.heal[3], boostResources.move[3]] + : undefined + }); + } + + private handleHarass(hydralisk: CombatZerg): void { + hydralisk.autoCombat(this.targetRemoteToHarass || hydralisk.room.name); + + //this.chooseRemoteToHarass(hydralisk, hydralisk.room.name); + if (!this.targetRemoteToHarass) { + this.chooseRemoteToHarass(hydralisk, hydralisk.room.name); + } + if (this.targetRemoteToHarass && hydralisk.room.name != this.targetRemoteToHarass) { + hydralisk.goToRoom(this.targetRemoteToHarass); + } else if (hydralisk.room.dangerousPlayerHostiles.length > 2) { + // Time to move on + this.chooseRemoteToHarass(hydralisk, hydralisk.room.name); + } + // Clean up construction sites then move on to another room + } + + chooseRemoteToHarass(hydralisk: CombatZerg, currentRoom: string) { + if (!this.directive.memory.roomsToHarass || this.directive.memory.roomsToHarass.length == 0) { + this.directive.memory.roomsToHarass = this.directive.findNearbyReservedRoomsForHarassment(); + } + let nextRoom = this.directive.memory.roomsToHarass.shift(); + if (nextRoom) { + this.directive.memory.roomsToHarass.push(nextRoom); + this.targetRemoteToHarass = nextRoom; + log.debug(`Selecting new target of ${this.targetRemoteToHarass} for ${hydralisk.print} from ${this.directive.memory.roomsToHarass}`); + hydralisk.say(`Tgt ${this.targetRemoteToHarass}`); + hydralisk.goToRoom(this.targetRemoteToHarass); + } + } + + init() { + this.reassignIdleCreeps(Roles.ranged); + const setup = CombatSetups.hydralisks.default; + this.wishlist(1, setup); + } + + run() { + this.autoRun(this.hydralisks, hydralisk => this.handleHarass(hydralisk)); + } +} diff --git a/src/overlords/offense/roomPoisoner.ts b/src/overlords/offense/roomPoisoner.ts new file mode 100644 index 000000000..3803580ba --- /dev/null +++ b/src/overlords/offense/roomPoisoner.ts @@ -0,0 +1,96 @@ +import {log} from '../../console/log'; +import {Roles, Setups} from '../../creepSetups/setups'; +import {Pathing} from '../../movement/Pathing'; +import {DirectivePoisonRoom} from '../../directives/offense/poisonRoom'; +import {OverlordPriority} from '../../priorities/priorities_overlords'; +import {profile} from '../../profiler/decorator'; +import {Tasks} from '../../tasks/Tasks'; +import {Zerg} from '../../zerg/Zerg'; +import {Overlord} from '../Overlord'; + +/** + * Spawn roomPoisoner - upgrqde controller to lvl2, wall in controller then sources. + */ +@profile +export class RoomPoisonerOverlord extends Overlord { + + roomPoisoners: Zerg[]; + controllerWallSites: ConstructionSite[] | undefined; + sourcesWallSites: ConstructionSite[] | undefined; + + constructor(directive: DirectivePoisonRoom, priority = OverlordPriority.offense.roomPoisoner) { + super(directive, 'contaminate', priority); + this.roomPoisoners = this.zerg(Roles.roomPoisoner); + this.controllerWallSites = (this.room && this.room.controller) ? _.filter(this.room.constructionSites, + s => s.structureType == STRUCTURE_WALL && + s.pos.isNearTo(this.room!.controller!.pos)) : undefined; + this.sourcesWallSites = (this.room && this.room.controller) ? _.filter(this.room.constructionSites, + s => s.structureType == STRUCTURE_WALL && + !s.pos.isNearTo(this.room!.controller!.pos)) : undefined; + } + + refresh() { + super.refresh(); + this.controllerWallSites = (this.room && this.room.controller) ? _.filter(this.room.constructionSites, + s => s.structureType == STRUCTURE_WALL && + s.pos.isNearTo(this.room!.controller!.pos)) : undefined; + this.sourcesWallSites = (this.room && this.room.controller) ? _.filter(this.room.constructionSites, + s => s.structureType == STRUCTURE_WALL && + !s.pos.isNearTo(this.room!.controller!.pos)) : undefined; + } + + init() { + this.wishlist(1, Setups.roomPoisoner); + } + + private findStructureBlockingController(roomPoisoner: Zerg): Structure | undefined { + const blockingPos = Pathing.findBlockingPos(roomPoisoner.pos, roomPoisoner.room.controller!.pos, + _.filter(roomPoisoner.room.structures, s => !s.isWalkable)); + if (blockingPos) { + const structure = blockingPos.lookFor(LOOK_STRUCTURES)[0]; + if (structure) { + return structure; + } else { + log.error(`${this.print}: no structure at blocking pos ${blockingPos.print}! (Why?)`); + } + } + } + + private handleRoomPoisoner(roomPoisoner: Zerg): void { + // Ensure you are in the assigned room + if (roomPoisoner.room == this.room && !roomPoisoner.pos.isEdge) { + //corner case: unclaimed controller blocked, while sources not 100% bloked + if(!this.room.my && this.sourcesWallSites && this.controllerWallSites && + this.controllerWallSites.length ==0 && this.sourcesWallSites.length > 0){ + + const dismantleTarget = this.findStructureBlockingController(roomPoisoner); + if (dismantleTarget) { + roomPoisoner.task = Tasks.dismantle(dismantleTarget); + return; + } + } + + + // recharge + if (roomPoisoner.carry.energy == 0) { + roomPoisoner.task = Tasks.recharge(); + } else if (this.room && this.room.controller && + (this.room.controller.level < 2) && + !(this.room.controller.upgradeBlocked > 0)) { + // upgrade controller to level 2 to unlock walls + roomPoisoner.task = Tasks.upgrade(this.room.controller); + } else if (this.controllerWallSites && this.controllerWallSites.length) { + roomPoisoner.task = Tasks.build(this.controllerWallSites[0]); + } else if (this.sourcesWallSites && this.sourcesWallSites.length) { + roomPoisoner.task = Tasks.build(this.sourcesWallSites[0]); + } + } else { + roomPoisoner.goTo(this.pos, {ensurePath: true, avoidSK: true}); + } + } + + run() { + this.autoRun(this.roomPoisoners, roomPoisoner => this.handleRoomPoisoner(roomPoisoner)); + } +} + diff --git a/src/overlords/powerMining/PowerDrill.ts b/src/overlords/powerMining/PowerDrill.ts new file mode 100644 index 000000000..34fc41c79 --- /dev/null +++ b/src/overlords/powerMining/PowerDrill.ts @@ -0,0 +1,211 @@ +import {CombatZerg} from '../../zerg/CombatZerg'; +import {DirectiveSKOutpost} from '../../directives/colony/outpostSK'; +import {RoomIntel} from '../../intel/RoomIntel'; +import {minBy} from '../../utilities/utils'; +import {Mem} from '../../memory/Memory'; +import {debug, log} from '../../console/log'; +import {OverlordPriority} from '../../priorities/priorities_overlords'; +import {Visualizer} from '../../visuals/Visualizer'; +import {profile} from '../../profiler/decorator'; +import {CombatOverlord} from '../CombatOverlord'; +import {CombatSetups, Roles} from '../../creepSetups/setups'; +import {OverlordMemory} from '../Overlord'; +import {DirectivePowerMine} from "../../directives/resource/powerMine"; +import {DirectiveHaul} from "../../directives/resource/haul"; +import {calculateFormationStrength} from "../../utilities/creepUtils"; +import {Zerg} from "../../zerg/Zerg"; +import {MoveOptions} from "../../movement/Movement"; + +interface PowerDrillOverlordMemory extends OverlordMemory { + targetPBID?: string; +} + +/** + * PowerDrillOverlord -- spawns drills and coolant to mine power banks + */ +@profile +export class PowerDrillOverlord extends CombatOverlord { + + static requiredRCL = 7; + + directive: DirectivePowerMine; + memory: PowerDrillOverlordMemory; + partnerMap: Map; + + drills: CombatZerg[]; + coolant: CombatZerg[]; + + constructor(directive: DirectivePowerMine, priority = OverlordPriority.powerMine.drill) { + super(directive, 'powerDrill', priority, PowerDrillOverlord.requiredRCL); + this.directive = directive; + this.priority += this.outpostIndex * OverlordPriority.powerMine.roomIncrement; + this.drills = this.combatZerg(Roles.drill); + this.coolant = this.combatZerg(Roles.coolant); + this.memory = Mem.wrap(this.directive.memory, 'powerDrill'); + this.partnerMap = new Map(); + } + + refresh() { + super.refresh(); + this.memory = Mem.wrap(this.directive.memory, 'powerDrill'); + + } + + init() { + this.wishlist(1, CombatSetups.drill.default); + this.wishlist(2, CombatSetups.coolant.small); + } + + private getHostileDrill(powerBank: StructurePowerBank) { + return powerBank.hits < powerBank.hitsMax && powerBank.pos.findInRange(FIND_HOSTILE_CREEPS, 2)[0]; + } + + private handleHostileDrill(hostileDrill: Creep, powerBank: StructurePowerBank) { + Game.notify(`${hostileDrill.owner.username} power harvesting ${powerBank.room.name}, competing for same power bank.`); + // this.directive.remove(); + } + + private handleDrill(drill: CombatZerg) { + if (drill.spawning) { + return; + } + if (!this.directive.powerBank) { + if (!this.room) { + // We are not there yet + } else { + // If power bank is dead + if (this.directive.powerBank == undefined && this.directive.memory.state < 2) { + Game.notify(`Power bank in ${this.room.print} is dead.`); + drill.say('๐Ÿ’€ RIP ๐Ÿ’€'); + let result = drill.retire(); + if (result == ERR_BUSY) { + // drill spawning, find something else to do with them + } + log.notify("FINISHED POWER MINING IN " + this.room + " DELETING CREEP at time: " + Game.time.toString() + " result: " + result); + return; + } + } + } + + // Go to power room + if (!this.room || drill.room != this.room || drill.pos.isEdge || !this.directive.powerBank) { + // log.debugCreep(drill, `Going to room!`); + log.notify("Drill is moving to power site in " + this.pos.roomName + "."); + drill.goTo(this.pos); + return; + } + + // Handle killing bank + if (drill.pos.isNearTo(this.directive.powerBank)) { + if (!this.partnerMap.get(drill.name)) { + this.partnerMap.set(drill.name, []); + } + PowerDrillOverlord.periodicSay(drill,'Drillingโš’๏ธ'); + drill.attack(this.directive.powerBank); + } else { + PowerDrillOverlord.periodicSay(drill,'๐Ÿš—Traveling๐Ÿš—'); + drill.goTo(this.directive.powerBank); + } + } + + private handleCoolant(coolant: CombatZerg) { + if (coolant.spawning) { + return; + } + // Go to powerbank room + if (!this.room || coolant.room != this.room || coolant.pos.isEdge) { + // log.debugCreep(coolant, `Going to room!`); + coolant.healSelfIfPossible(); + coolant.goTo(this.pos); + return; + } else if (!this.directive.powerBank) { + // If power bank is dead + Game.notify("Power bank in " + this.room + " is dead."); + coolant.say('๐Ÿ’€ RIP ๐Ÿ’€'); + coolant.retire(); + return; + } + if (coolant.pos.getRangeTo(this.directive.powerBank) > 3) { + coolant.goTo(this.directive.powerBank); + } else if (coolant.pos.findInRange(FIND_MY_CREEPS, 1).filter(creep => _.contains(creep.name, "drill")).length == 0) { + let target = _.sample(_.filter(this.drills, drill => drill.hits < drill.hitsMax)); + if (target) { + coolant.goTo(target, {range: 1, noPush: true}); + } + } + // else if (coolant.pos.getRangeTo(this.directive.powerBank) == 1) { + // coolant.move(Math.round(Math.random()*7) as DirectionConstant); + // } + else { + let drill = _.sample(_.filter(this.drills, drill => drill.hits < drill.hitsMax)); + if (drill) { coolant.goTo(drill); } + } + + coolant.autoHeal(); + } + + // private findDrillToPartner(coolant: CombatZerg) { + // let needsHealing = _.min(Array.from(this.partnerMap.keys()), key => this.partnerMap.get(key)!.length); + // if (this.partnerMap.get(needsHealing)) { + // this.partnerMap.get(needsHealing)!.concat(coolant.name); + // coolant.say(needsHealing.toString()); + // coolant.memory.partner = needsHealing; + // } else { + // + // } + // //console.log(JSON.stringify(this.partnerMap)); + // // let newPartner = _.sample(_.filter(this.drills, drill => this.room == drill.room)); + // // coolant.memory.partner = newPartner != undefined ? newPartner.name : undefined; + // coolant.say('Partnering!'); + // } + // + // private runPartnerHealing(coolant: CombatZerg) { + // if (coolant.memory.partner) { + // let drill = Game.creeps[coolant.memory.partner]; + // if (!drill) { + // // Partner is dead + // coolant.memory.partner = undefined; + // this.findDrillToPartner(coolant) + // } else if (!coolant.pos.isNearTo(drill)) { + // PowerDrillOverlord.periodicSay(coolant,'๐Ÿš—Traveling๏ธ'); + // coolant.goTo(drill); + // } else { + // PowerDrillOverlord.periodicSay(coolant,'โ„๏ธCoolingโ„๏ธ'); + // coolant.heal(drill); + // } + // if (Game.time % 10 == PowerDrillOverlord.getCreepNameOffset(coolant)) { + // this.findDrillToPartner(coolant); + // } + // return; + // } else { + // this.findDrillToPartner(coolant); + // } + // } + + static periodicSay(zerg: Zerg, text: string) { + if (Game.time % 10 == PowerDrillOverlord.getCreepNameOffset(zerg)) { + zerg.say(text, true); + } + } + + static getCreepNameOffset(zerg: Zerg) { + return parseInt(zerg.name.charAt(zerg.name.length-1)) || 0; + } + + run() { + this.autoRun(this.drills, drill => this.handleDrill(drill)); + this.autoRun(this.coolant, coolant => this.handleCoolant(coolant)); + if (this.directive.memory.state >= 3) { + Game.notify("DELETING ALL POWER MINING CREEPS BECAUSE STATE IS >= 3 in " + this.directive.print); + this.drills.forEach(drill => drill.retire()); + this.coolant.forEach(coolant => coolant.retire()); + } + } + + visuals() { + if (this.room && this.directive.powerBank) { + Visualizer.marker(this.directive.powerBank.pos); + } + } + +} diff --git a/src/overlords/powerMining/PowerHauler.ts b/src/overlords/powerMining/PowerHauler.ts new file mode 100644 index 000000000..ddb27523e --- /dev/null +++ b/src/overlords/powerMining/PowerHauler.ts @@ -0,0 +1,141 @@ +import {Overlord} from '../Overlord'; +import {OverlordPriority} from '../../priorities/priorities_overlords'; +import {Zerg} from '../../zerg/Zerg'; +import {Tasks} from '../../tasks/Tasks'; +import {log} from '../../console/log'; +import {Energetics} from '../../logistics/Energetics'; +import {profile} from '../../profiler/decorator'; +import {Roles, Setups} from '../../creepSetups/setups'; +import {calculateFormationStrength} from "../../utilities/creepUtils"; +import {DirectivePowerMine} from "../../directives/resource/powerMine"; + +/** + * Spawns special-purpose haulers for transporting resources to/from a specified target + */ +@profile +export class PowerHaulingOverlord extends Overlord { + + haulers: Zerg[]; + directive: DirectivePowerMine; + tickToSpawnOn: number; + numHaulers: number; + totalCollected: number; + + // TODO bug where haulers can come from tiny rooms not ready yet + requiredRCL = 6; + // Allow time for body to spawn + prespawnAmount = 350; + + constructor(directive: DirectivePowerMine, priority = OverlordPriority.collectionUrgent.haul) { + super(directive, 'powerHaul', priority); + this.directive = directive; + this.haulers = this.zerg(Roles.transport); + this.totalCollected = this.totalCollected || 0; + // Spawn haulers to collect ALL the power at the same time. + let haulingPartsNeeded = this.directive.totalResources/CARRY_CAPACITY; + // Calculate amount of hauling each hauler provides in a lifetime + let haulerCarryParts = Setups.transporters.default.getBodyPotential(CARRY, this.colony); + // Calculate number of haulers + this.numHaulers = Math.ceil(haulingPartsNeeded/haulerCarryParts); + // setup time to request the haulers + this.tickToSpawnOn = Game.time + (this.directive.calculateRemainingLifespan() || 0) - this.prespawnAmount; + } + + init() { + if (!this.colony.storage || _.sum(this.colony.storage.store) > Energetics.settings.storage.total.cap) { + return; + } + } + + protected handleHauler(hauler: Zerg) { + if (_.sum(hauler.carry) == 0) { + if (this.directive.memory.state >= 4) { + // FIXME: Maybe ditch this and put it as a separate on-finishing method to reassign + hauler.say('๐Ÿ’€ RIP ๐Ÿ’€',true); + log.warning(`${hauler.name} is committing suicide as directive is done!`); + this.numHaulers = 0; + hauler.retire(); + } + // Travel to directive and collect resources + if (hauler.inSameRoomAs(this.directive)) { + // Pick up drops first + if (this.directive.hasDrops) { + let allDrops: Resource[] = _.flatten(_.values(this.directive.drops)); + let drop = allDrops[0]; + if (drop) { + hauler.task = Tasks.pickup(drop); + return; + } + } else if (this.directive.powerBank) { + if (hauler.pos.getRangeTo(this.directive.powerBank) > 4) { + hauler.goTo(this.directive.powerBank); + } else { + hauler.say('๐Ÿšฌ', true); + } + return; + } else if (this.room && this.room.drops) { + let allDrops: Resource[] = _.flatten(_.values(this.room.drops)); + let drop = allDrops[0]; + if (drop) { + hauler.task = Tasks.pickup(drop); + return; + } else { + hauler.say('๐Ÿ’€ RIP ๐Ÿ’€',true); + log.warning(`${hauler.name} is committing suicide!`); + hauler.retire(); + return; + } + } + // Shouldn't reach here + log.warning(`${hauler.name} in ${hauler.room.print}: nothing to collect!`); + } else { + hauler.goTo(this.directive); + } + } else { + // Travel to colony room and deposit resources + if (hauler.inSameRoomAs(this.colony)) { + for (let resourceType in hauler.carry) { + if (hauler.carry[resourceType] == 0) continue; + if (resourceType == RESOURCE_ENERGY) { // prefer to put energy in storage + if (this.colony.storage && _.sum(this.colony.storage.store) < STORAGE_CAPACITY) { + hauler.task = Tasks.transfer(this.colony.storage, resourceType); + return; + } else if (this.colony.terminal && _.sum(this.colony.terminal.store) < TERMINAL_CAPACITY) { + hauler.task = Tasks.transfer(this.colony.terminal, resourceType); + return; + } + } else { // prefer to put minerals in terminal + this.totalCollected += hauler.carry.power || 0; + if (this.colony.terminal && _.sum(this.colony.terminal.store) < TERMINAL_CAPACITY) { + hauler.task = Tasks.transfer(this.colony.terminal, resourceType); + return; + } else if (this.colony.storage && _.sum(this.colony.storage.store) < STORAGE_CAPACITY) { + hauler.task = Tasks.transfer(this.colony.storage, resourceType); + return; + } + } + } + // Shouldn't reach here + log.warning(`${hauler.name} in ${hauler.room.print}: nowhere to put resources!`); + } else { + hauler.task = Tasks.goToRoom(this.colony.room.name); + } + } + } + + checkIfStillCarryingPower() { + return _.find(this.haulers, hauler => hauler.carry.power != undefined && hauler.carry.power > 0); + } + + run() { + if (Game.time >= this.tickToSpawnOn && this.directive.memory.state < 4) { + this.wishlist(this.numHaulers, Setups.transporters.default); + } + for (let hauler of this.haulers) { + if (hauler.isIdle) { + this.handleHauler(hauler); + } + hauler.run(); + } + } +} \ No newline at end of file diff --git a/src/overlords/situational/hauler.ts b/src/overlords/situational/hauler.ts index 8164b270d..cb7758b1e 100644 --- a/src/overlords/situational/hauler.ts +++ b/src/overlords/situational/hauler.ts @@ -55,7 +55,7 @@ export class HaulingOverlord extends Overlord { // Pick up drops first if (this.directive.hasDrops) { const allDrops: Resource[] = _.flatten(_.values(this.directive.drops)); - const drop = allDrops[0]; + const drop = _.find(allDrops, drop => drop.resourceType != "energy") || allDrops[0]; if (drop) { hauler.task = Tasks.pickup(drop); return; diff --git a/src/priorities/priorities_overlords.ts b/src/priorities/priorities_overlords.ts index bef3ab436..44d260d92 100644 --- a/src/priorities/priorities_overlords.ts +++ b/src/priorities/priorities_overlords.ts @@ -22,7 +22,9 @@ export let OverlordPriority = { destroy : 300, healPoint : 301, siege : 302, - controllerAttack: 399 + controllerAttack: 399, + harass : 571, + roomPoisoner : 399, }, colonization: { // Colonizing new rooms @@ -72,6 +74,13 @@ export let OverlordPriority = { roomIncrement: 5, }, + powerMine: { + cool : 1050, + drill : 1051, + gather : 604, + roomIncrement: 5, + }, + collection: { // Non-urgent collection of resources, like from a deserted storage haul: 1100 }, diff --git a/src/prototypes/Room.ts b/src/prototypes/Room.ts index d55ac450f..285be8290 100644 --- a/src/prototypes/Room.ts +++ b/src/prototypes/Room.ts @@ -97,7 +97,7 @@ Object.defineProperty(Room.prototype, 'playerHostiles', { if (!this._playerHostiles) { this._playerHostiles = _.filter(this.hostiles, (creep: Creep) => creep.owner.username != 'Invader' - && creep.owner.username != 'Source Keeper'); + && creep.owner.username != 'Source Keeper' && creep.owner.username != 'zGeneral'); } return this._playerHostiles; }, diff --git a/src/roomPlanner/RoomPlanner.ts b/src/roomPlanner/RoomPlanner.ts index 5b14fb46d..b1208031d 100644 --- a/src/roomPlanner/RoomPlanner.ts +++ b/src/roomPlanner/RoomPlanner.ts @@ -8,7 +8,7 @@ import {Pathing} from '../movement/Pathing'; import {BuildPriorities, DemolishStructurePriorities} from '../priorities/priorities_structures'; import {profile} from '../profiler/decorator'; import {bullet} from '../utilities/stringConstants'; -import {derefCoords, maxBy, onPublicServer} from '../utilities/utils'; +import {derefCoords, hasMinerals, maxBy, onPublicServer} from '../utilities/utils'; import {Visualizer} from '../visuals/Visualizer'; import {MY_USERNAME} from '../~settings'; import {BarrierPlanner} from './BarrierPlanner'; @@ -16,6 +16,7 @@ import {bunkerLayout} from './layouts/bunker'; import {commandCenterLayout} from './layouts/commandCenter'; import {hatcheryLayout} from './layouts/hatchery'; import {RoadPlanner} from './RoadPlanner'; +import {DirectiveHaul} from "../directives/resource/haul"; export interface BuildingPlannerOutput { name: string; @@ -638,6 +639,11 @@ export class RoomPlanner { && (structureType == STRUCTURE_STORAGE || structureType == STRUCTURE_TERMINAL)) { break; // don't destroy terminal or storage when under RCL4 - can use energy inside } + if (this.colony.level < 6 + && structureType == STRUCTURE_TERMINAL && hasMinerals(( structure).store)) { + DirectiveHaul.create(structure.pos); + break; // don't destroy terminal when under RCL6 if there are resources available. + } if (structureType != STRUCTURE_WALL && structureType != STRUCTURE_RAMPART) { this.memory.relocating = true; } diff --git a/src/tasks/instances/harvest.ts b/src/tasks/instances/harvest.ts index ca94de5af..8efbea396 100644 --- a/src/tasks/instances/harvest.ts +++ b/src/tasks/instances/harvest.ts @@ -14,7 +14,7 @@ export class TaskHarvest extends Task { } isValidTask() { - return _.sum(this.creep.carry) < this.creep.carryCapacity; + return _.sum(this.creep.carry) < this.creep.carryCapacity * .95; } isValidTarget() { diff --git a/src/utilities/Cartographer.ts b/src/utilities/Cartographer.ts index a2708e4bb..fca73640f 100644 --- a/src/utilities/Cartographer.ts +++ b/src/utilities/Cartographer.ts @@ -1,4 +1,5 @@ import {profile} from '../profiler/decorator'; +import {log} from "../console/log"; export const ROOMTYPE_SOURCEKEEPER = 'SK'; export const ROOMTYPE_CORE = 'CORE'; @@ -214,4 +215,24 @@ export class Cartographer { }; } + static isNoviceRoom(roomName: string): boolean { + if (Memory.zoneRooms) { + const roomInfo = Memory.zoneRooms[roomName]; + return !!roomInfo && !!roomInfo['novice']; + } else { + log.alert(`Checking novice room before segment is set in ${roomName}!`); + return false; + } + } + + static isRespawnRoom(roomName: string): boolean { + if (Memory.zoneRooms) { + const roomInfo = Memory.zoneRooms[roomName]; + return !!roomInfo && !!roomInfo['respawnArea']; + } else { + log.alert(`Checking respawn room before segment is set in ${roomName}!`); + return false; + } + } + } diff --git a/src/utilities/creepUtils.ts b/src/utilities/creepUtils.ts new file mode 100644 index 000000000..7e5b1a6b7 --- /dev/null +++ b/src/utilities/creepUtils.ts @@ -0,0 +1,45 @@ +// Creep utilities that don't belong anywhere else + + +// Does not account for range, just total of body parts +export function calculateFormationStrength(creeps : Creep[]): Record { + let tally: Record = { + move : 0, + work : 0, + carry : 0, + attack : 0, + ranged_attack : 0, + tough : 0, + heal : 0, + claim : 0, + }; + + _.forEach(creeps, + function (unit) { + let individualTally = calculateBodyPotential(unit.body); + for (let bodyType in individualTally) { + let type = bodyType as BodyPartConstant; + tally[type] += individualTally[type]; + } + }); + return tally; +} + +export function calculateBodyPotential(body : BodyPartDefinition[]): Record { + let tally: Record = { + move : 0, + work : 0, + carry : 0, + attack : 0, + ranged_attack : 0, + tough : 0, + heal : 0, + claim : 0, + }; + _.forEach(body, function (bodyPart) { + // Needs boost logic + tally[bodyPart.type] += 1; + } + ); + return tally; +} diff --git a/src/utilities/utils.ts b/src/utilities/utils.ts index cca769e95..cd2cbf49c 100644 --- a/src/utilities/utils.ts +++ b/src/utilities/utils.ts @@ -34,6 +34,15 @@ export function hasMinerals(store: { [resourceType: string]: number }): boolean return false; } +export function hasContents(store: { [resourceType: string]: number }): boolean { + for (let resourceType in store) { + if ((store[resourceType] || 0) > 0) { + return true; + } + } + return false; +} + /** * Obtain the username of the player */ diff --git a/src/zerg/CombatZerg.ts b/src/zerg/CombatZerg.ts index 514ad707d..afd6c37eb 100644 --- a/src/zerg/CombatZerg.ts +++ b/src/zerg/CombatZerg.ts @@ -1,5 +1,5 @@ import {CombatIntel} from '../intel/CombatIntel'; -import {Movement, NO_ACTION} from '../movement/Movement'; +import {CombatMoveOptions, Movement, MoveOptions, NO_ACTION} from '../movement/Movement'; import {profile} from '../profiler/decorator'; import {insideBunkerBounds} from '../roomPlanner/layouts/bunker'; import {CombatTargeting} from '../targeting/CombatTargeting'; @@ -163,7 +163,7 @@ export class CombatZerg extends Zerg { */ autoRanged(possibleTargets = this.room.hostiles, allowMassAttack = true) { const target = CombatTargeting.findBestCreepTargetInRange(this, 3, possibleTargets) - || CombatTargeting.findBestStructureTargetInRange(this, 3); + || CombatTargeting.findBestStructureTargetInRange(this, 3,false); //disabled allowUnowned structure attack in order not to desrtory poison walls this.debug(`Ranged target: ${target}`); if (target) { if (allowMassAttack @@ -180,7 +180,7 @@ export class CombatZerg extends Zerg { */ autoHeal(allowRangedHeal = true, friendlies = this.room.creeps) { const target = CombatTargeting.findBestHealingTargetInRange(this, allowRangedHeal ? 3 : 1, friendlies); - this.debug(`Heal taget: ${target}`); + this.debug(`Heal target: ${target}`); if (target) { if (this.pos.getRangeTo(target) <= 1) { return this.heal(target); @@ -228,7 +228,7 @@ export class CombatZerg extends Zerg { /** * Navigate to a room, then engage hostile creeps there, perform medic actions, etc. */ - autoCombat(roomName: string, verbose = false) { + autoCombat(roomName: string, verbose = false, preferredRange?: number, options?: CombatMoveOptions) { // Do standard melee, ranged, and heal actions if (this.getActiveBodyparts(ATTACK) > 0) { @@ -256,7 +256,7 @@ export class CombatZerg extends Zerg { // Fight within the room const target = CombatTargeting.findTarget(this); const preferRanged = this.getActiveBodyparts(RANGED_ATTACK) > this.getActiveBodyparts(ATTACK); - const targetRange = preferRanged ? 3 : 1; + const targetRange = preferredRange || preferRanged ? 3 : 1; this.debug(`${target}, ${targetRange}`); if (target) { const avoid = []; @@ -264,10 +264,10 @@ export class CombatZerg extends Zerg { if (preferRanged) { const meleeHostiles = _.filter(this.room.hostiles, h => CombatIntel.getAttackDamage(h) > 0); for (const hostile of meleeHostiles) { - avoid.push({pos: hostile.pos, range: 2}); + avoid.push({pos: hostile.pos, range: targetRange - 1}); } } - return Movement.combatMove(this, [{pos: target.pos, range: targetRange}], []); + return Movement.combatMove(this, [{pos: target.pos, range: targetRange}], avoid, options); } } diff --git a/src/zerg/PowerZerg.ts b/src/zerg/PowerZerg.ts new file mode 100644 index 000000000..36e5bff54 --- /dev/null +++ b/src/zerg/PowerZerg.ts @@ -0,0 +1,51 @@ +import {CombatIntel} from '../intel/CombatIntel'; +import {Movement, NO_ACTION} from '../movement/Movement'; +import {profile} from '../profiler/decorator'; +import {CombatTargeting} from '../targeting/CombatTargeting'; +import {GoalFinder} from '../targeting/GoalFinder'; +import {randomHex} from '../utilities/utils'; +import {Zerg} from './Zerg'; + +interface CombatZergMemory extends CreepMemory { + recovering: boolean; + lastInDanger: number; + partner?: string; + swarm?: string; +} + +export const DEFAULT_PARTNER_TICK_DIFFERENCE = 650; +export const DEFAULT_SWARM_TICK_DIFFERENCE = 500; + +/** + * CombatZerg is an extension of the Zerg class which contains additional combat-related methods + */ +@profile +export class PowerZerg extends Zerg { + + memory: CombatZergMemory; + isPowerZerg: boolean; + + constructor(creep: Creep, notifyWhenAttacked = true) { + super(creep, notifyWhenAttacked); + this.isPowerZerg = true; + _.defaults(this.memory, { + recovering : false, + lastInDanger: 0, + targets : {} + }); + } + + static fatigue() { + return 0; + } + + static body() { + return [MOVE]; + } + + static attack(target: Creep | Structure): 0 | -1 | -4 | -7 | -9 | -12 | -11 { + return ERR_TIRED; + } + + +} diff --git a/src/zerg/Zerg.ts b/src/zerg/Zerg.ts index 38bbef367..cf7374b1f 100644 --- a/src/zerg/Zerg.ts +++ b/src/zerg/Zerg.ts @@ -511,6 +511,17 @@ export class Zerg { setOverlord(this, newOverlord); } + // TODO add retire/reassignment logic + // Eg. creep get repurposed, it gets recycled, etc + /** + * When a zerg has no more use for it's current overlord, it will be retired. + * For now, that means RIP + */ + retire() { + this.say('๐Ÿ’€ RIP ๐Ÿ’€', true); + return this.suicide(); + } + /* Reassigns the creep to work under a new overlord and as a new role. */ reassign(newOverlord: Overlord | null, newRole: string, invalidateTask = true) { this.overlord = newOverlord; diff --git a/src/zerg/ZergShell.ts b/src/zerg/ZergShell.ts new file mode 100644 index 000000000..44b11c033 --- /dev/null +++ b/src/zerg/ZergShell.ts @@ -0,0 +1,592 @@ +import {Colony} from '../Colony'; +import {log} from '../console/log'; +import {isCreep, isZerg} from '../declarations/typeGuards'; +import {CombatIntel} from '../intel/CombatIntel'; +import {Movement, MoveOptions} from '../movement/Movement'; +import {Overlord} from '../overlords/Overlord'; +import {profile} from '../profiler/decorator'; +import {initializeTask} from '../tasks/initializer'; +import {Task} from '../tasks/Task'; +import {NEW_OVERMIND_INTERVAL} from '../~settings'; +import {PowerZerg} from "./PowerZerg"; + +export function getOverlord(creep: Zerg | Creep): Overlord | null { + if (creep.memory[_MEM.OVERLORD]) { + return Overmind.overlords[creep.memory[_MEM.OVERLORD]!] || null; + } else { + return null; + } +} + +export function setOverlord(creep: Zerg | Creep, newOverlord: Overlord | null) { + // Remove cache references to old assignments + const roleName = creep.memory.role; + const ref = creep.memory[_MEM.OVERLORD]; + const oldOverlord: Overlord | null = ref ? Overmind.overlords[ref] : null; + if (ref && Overmind.cache.overlords[ref] && Overmind.cache.overlords[ref][roleName]) { + _.remove(Overmind.cache.overlords[ref][roleName], name => name == creep.name); + } + if (newOverlord) { + // Change to the new overlord's colony + creep.memory[_MEM.COLONY] = newOverlord.colony.name; + // Change assignments in memory + creep.memory[_MEM.OVERLORD] = newOverlord.ref; + // Update the cache references + if (!Overmind.cache.overlords[newOverlord.ref]) { + Overmind.cache.overlords[newOverlord.ref] = {}; + } + if (!Overmind.cache.overlords[newOverlord.ref][roleName]) { + Overmind.cache.overlords[newOverlord.ref][roleName] = []; + } + Overmind.cache.overlords[newOverlord.ref][roleName].push(creep.name); + } else { + creep.memory[_MEM.OVERLORD] = null; + } + if (oldOverlord) oldOverlord.recalculateCreeps(); + if (newOverlord) newOverlord.recalculateCreeps(); +} + +export function normalizeZerg(creep: Zerg | Creep): Zerg | Creep { + return Overmind.zerg[creep.name] || creep; +} + +export function toCreep(creep: Zerg | Creep): Creep { + return isZerg(creep) ? creep.creep : creep; +} + +// Last pipeline is more complex because it depends on the energy a creep has; sidelining this for now +const actionPipelines: string[][] = [ + ['harvest', 'attack', 'build', 'repair', 'dismantle', 'attackController', 'rangedHeal', 'heal'], + ['rangedAttack', 'rangedMassAttack', 'build', 'repair', 'rangedHeal'], + // ['upgradeController', 'build', 'repair', 'withdraw', 'transfer', 'drop'], +]; + +interface ParkingOptions { + range: number; + exactRange: boolean; + offroad: boolean; +} + +interface FleeOptions { + dropEnergy?: boolean; + invalidateTask?: boolean; +} + +const RANGES = { + BUILD : 3, + REPAIR : 3, + TRANSFER: 1, + WITHDRAW: 1, + HARVEST : 1, + DROP : 0, +}; + +/** + * The Zerg class is a wrapper for owned creeps and contains all wrapped creep methods and many additional methods for + * direct control of a creep. + */ +@profile +export class Zerg { + + creep: Creep | PowerZerg; // The creep that this wrapper class will control + body: BodyPartDefinition[]; // These properties are all wrapped from this.creep.* to this.* + carry: StoreDefinition; // | + carryCapacity: number; // | + fatigue: number; // | + hits: number; // | + hitsMax: number; // | + id: string; // | + memory: CreepMemory; // | See the ICreepMemory interface for structure + name: string; // | + pos: RoomPosition; // | + nextPos: RoomPosition; // | The next position the creep will be in after registering a move intent + ref: string; // | + roleName: string; // | + room: Room; // | + saying: string; // | + spawning: boolean; // | + ticksToLive: number | undefined; // | + lifetime: number; + actionLog: { [actionName: string]: boolean }; // Tracks the actions that a creep has completed this tick + blockMovement: boolean; // Whether the zerg is allowed to move or not + private _task: Task | null; // Cached Task object that is instantiated once per tick and on change + + constructor(creep: Creep, notifyWhenAttacked = true) { + // Copy over creep references + this.creep = creep; + this.body = creep.body; + this.carry = creep.carry; + this.carryCapacity = creep.carryCapacity; + this.fatigue = creep.fatigue; + this.hits = creep.hits; + this.hitsMax = creep.hitsMax; + this.id = creep.id; + this.memory = creep.memory; + this.name = creep.name; + this.pos = creep.pos; + this.nextPos = creep.pos; + this.ref = creep.ref; + this.roleName = creep.memory.role; + this.room = creep.room; + this.saying = creep.saying; + this.spawning = creep.spawning; + this.ticksToLive = creep.ticksToLive; + // Extra properties + this.lifetime = this.getBodyparts(CLAIM) > 0 ? CREEP_CLAIM_LIFE_TIME : CREEP_LIFE_TIME; + this.actionLog = {}; + this.blockMovement = false; + // Register global references + Overmind.zerg[this.name] = this; + global[this.name] = this; + // Handle attack notification when at lifetime - 1 + if (!notifyWhenAttacked && (this.ticksToLive || 0) >= this.lifetime - (NEW_OVERMIND_INTERVAL + 1)) { + // creep.notifyWhenAttacked only uses the 0.2CPU intent cost if it changes the intent value + this.notifyWhenAttacked(notifyWhenAttacked); + } + } + + /** + * Refresh all changeable properties of the creep or delete from Overmind and global when dead + */ + refresh(): void { + const creep = Game.creeps[this.name]; + if (creep) { + this.creep = creep; + this.pos = creep.pos; + this.nextPos = creep.pos; + this.body = creep.body; + this.carry = creep.carry; + this.carryCapacity = creep.carryCapacity; + this.fatigue = creep.fatigue; + this.hits = creep.hits; + this.memory = creep.memory; + this.roleName = creep.memory.role; + this.room = creep.room; + this.saying = creep.saying; + this.spawning = creep.spawning; + this.ticksToLive = creep.ticksToLive; + this.actionLog = {}; + this.blockMovement = false; + this._task = null; // todo + } else { + log.debug(`Deleting from global`); + delete Overmind.zerg[this.name]; + delete global[this.name]; + } + } + + debug(...args: any[]) { + if (this.memory.debug) { + log.debug(this.print, args); + } + } + + get ticksUntilSpawned(): number | undefined { + if (this.spawning) { + const spawner = this.pos.lookForStructure(STRUCTURE_SPAWN) as StructureSpawn; + if (spawner && spawner.spawning) { + return spawner.spawning.remainingTime; + } else { + // Shouldn't ever get here + console.log(`Error determining ticks to spawn for ${this.name} @ ${this.pos.print}!`); + } + } + } + + get print(): string { + return '[' + this.name + ']'; + } + + cancelOrder(methodName: string): OK | ERR_NOT_FOUND { + const result = this.creep.cancelOrder(methodName); + if (result == OK) this.actionLog[methodName] = false; + return result; + } + + drop(resourceType: ResourceConstant, amount?: number) { + const result = this.creep.drop(resourceType, amount); + if (!this.actionLog.drop) this.actionLog.drop = (result == OK); + return result; + } + + goDrop(pos: RoomPosition, resourceType: ResourceConstant, amount?: number) { + if (this.pos.inRangeToPos(pos, RANGES.DROP)) { + return this.drop(resourceType, amount); + } else { + return this.goTo(pos); + } + } + + generateSafeMode(target: StructureController) { + return this.creep.generateSafeMode(target); + } + + harvest(source: Source | Mineral) { + const result = this.creep.harvest(source); + if (!this.actionLog.harvest) this.actionLog.harvest = (result == OK); + return result; + } + + goHarvest(source: Source | Mineral) { + if (this.pos.inRangeToPos(source.pos, RANGES.HARVEST)) { + return this.harvest(source); + } else { + return this.goTo(source); + } + } + + move(direction: DirectionConstant, force = false) { + if (!this.blockMovement && !force) { + const result = this.creep.move(direction); + if (result == OK) { + if (!this.actionLog.move) this.actionLog.move = true; + this.nextPos = this.pos.getPositionAtDirection(direction); + } + return result; + } else { + return ERR_BUSY; + } + } + + notifyWhenAttacked(enabled: boolean) { + return this.creep.notifyWhenAttacked(enabled); + } + + pickup(resource: Resource) { + const result = this.creep.pickup(resource); + if (!this.actionLog.pickup) this.actionLog.pickup = (result == OK); + return result; + } + + /* Say a message; maximum message length is 10 characters */ + say(message: string, pub?: boolean) { + return this.creep.say(message, pub); + } + + signController(target: StructureController, text: string) { + const result = this.creep.signController(target, text); + if (!this.actionLog.signController) this.actionLog.signController = (result == OK); + return result; + } + + suicide() { + return this.creep.suicide(); + } + + transfer(target: Creep | Zerg | Structure, resourceType: ResourceConstant = RESOURCE_ENERGY, amount?: number) { + let result: ScreepsReturnCode; + if (target instanceof Zerg) { + result = this.creep.transfer(target.creep, resourceType, amount); + } else { + result = this.creep.transfer(target, resourceType, amount); + } + if (!this.actionLog.transfer) this.actionLog.transfer = (result == OK); + return result; + } + + goTransfer(target: Creep | Zerg | Structure, resourceType: ResourceConstant = RESOURCE_ENERGY, amount?: number) { + if (this.pos.inRangeToPos(target.pos, RANGES.TRANSFER)) { + return this.transfer(target, resourceType, amount); + } else { + return this.goTo(target); + } + } + + withdraw(target: Structure | Tombstone, resourceType: ResourceConstant = RESOURCE_ENERGY, amount?: number) { + const result = this.creep.withdraw(target, resourceType, amount); + if (!this.actionLog.withdraw) this.actionLog.withdraw = (result == OK); + return result; + } + + goWithdraw(target: Structure | Tombstone, resourceType: ResourceConstant = RESOURCE_ENERGY, amount?: number) { + if (this.pos.inRangeToPos(target.pos, RANGES.WITHDRAW)) { + return this.withdraw(target, resourceType, amount); + } else { + return this.goTo(target); + } + } + + // Simultaneous creep actions -------------------------------------------------------------------------------------- + + /** + * Determine whether the given action will conflict with an action the creep has already taken. + * See http://docs.screeps.com/simultaneous-actions.html for more details. + */ + canExecute(actionName: string): boolean { + // Only one action can be executed from within a single pipeline + let conflictingActions: string[] = [actionName]; + for (const pipeline of actionPipelines) { + if (pipeline.includes(actionName)) conflictingActions = conflictingActions.concat(pipeline); + } + for (const action of conflictingActions) { + if (this.actionLog[action]) { + return false; + } + } + return true; + } + + // Body configuration and related data ----------------------------------------------------------------------------- + + getActiveBodyparts(type: BodyPartConstant): number { + return this.creep.getActiveBodyparts(type); + } + + /* The same as creep.getActiveBodyparts, but just counts bodyparts regardless of condition. */ + getBodyparts(partType: BodyPartConstant): number { + return _.filter(this.body, (part: BodyPartDefinition) => part.type == partType).length; + } + + // Custom creep methods ============================================================================================ + + // Carry methods + + get hasMineralsInCarry(): boolean { + for (const resourceType in this.carry) { + if (resourceType != RESOURCE_ENERGY && (this.carry[resourceType] || 0) > 0) { + return true; + } + } + return false; + } + + // Boosting logic -------------------------------------------------------------------------------------------------- + + get boosts(): _ResourceConstantSansEnergy[] { + return this.creep.boosts; + } + + get boostCounts(): { [boostType: string]: number } { + return this.creep.boostCounts; + } + + get needsBoosts(): boolean { + if (this.overlord) { + return this.overlord.shouldBoost(this); + } + return false; + } + + // Overlord logic -------------------------------------------------------------------------------------------------- + + get overlord(): Overlord | null { + return getOverlord(this); + } + + set overlord(newOverlord: Overlord | null) { + setOverlord(this, newOverlord); + } + + /* Reassigns the creep to work under a new overlord and as a new role. */ + reassign(newOverlord: Overlord | null, newRole: string, invalidateTask = true) { + this.overlord = newOverlord; + this.roleName = newRole; + this.memory.role = newRole; + if (invalidateTask) { + this.task = null; + } + } + + // Task logic ------------------------------------------------------------------------------------------------------ + + /** + * Wrapper for _task + */ + get task(): Task | null { + if (!this._task) { + this._task = this.memory.task ? initializeTask(this.memory.task) : null; + } + return this._task; + } + + /** + * Assign the creep a task with the setter, replacing creep.assign(Task) + */ + set task(task: Task | null) { + // Unregister target from old task if applicable + const oldProtoTask = this.memory.task; + if (oldProtoTask) { + const oldRef = oldProtoTask._target.ref; + if (Overmind.cache.targets[oldRef]) { + _.remove(Overmind.cache.targets[oldRef], name => name == this.name); + } + } + // Set the new task + this.memory.task = task ? task.proto : null; + if (task) { + if (task.target) { + // Register task target in cache if it is actively targeting something (excludes goTo and similar) + if (!Overmind.cache.targets[task.target.ref]) { + Overmind.cache.targets[task.target.ref] = []; + } + Overmind.cache.targets[task.target.ref].push(this.name); + } + // Register references to creep + task.creep = this; + } + // Clear cache + this._task = null; + } + + /** + * Does the creep have a valid task at the moment? + */ + get hasValidTask(): boolean { + return !!this.task && this.task.isValid(); + } + + /** + * Creeps are idle if they don't have a task. + */ + get isIdle(): boolean { + return !this.task || !this.task.isValid(); + } + + /** + * Execute the task you currently have. + */ + run(): number | undefined { + if (this.task) { + return this.task.run(); + } + } + + // Colony association ---------------------------------------------------------------------------------------------- + + /** + * Colony that the creep belongs to. + */ + get colony(): Colony { + return Overmind.colonies[this.memory[_MEM.COLONY]]; + } + + set colony(newColony: Colony) { + this.memory[_MEM.COLONY] = newColony.name; + } + + /** + * If the creep is in a colony room or outpost + */ + get inColonyRoom(): boolean { + return Overmind.colonyMap[this.room.name] == this.memory[_MEM.COLONY]; + } + + // Movement and location ------------------------------------------------------------------------------------------- + + goTo(destination: RoomPosition | HasPos, options: MoveOptions = {}) { + return Movement.goTo(this, destination, options); + } + + goToRoom(roomName: string, options: MoveOptions = {}) { + return Movement.goToRoom(this, roomName, options); + } + + inSameRoomAs(target: HasPos): boolean { + return this.pos.roomName == target.pos.roomName; + } + + safelyInRoom(roomName: string): boolean { + return this.room.name == roomName && !this.pos.isEdge; + } + + get inRampart(): boolean { + return this.creep.inRampart; + } + + get isMoving(): boolean { + const moveData = this.memory._go as MoveData | undefined; + return !!moveData && !!moveData.path && moveData.path.length > 1; + } + + /** + * Kite around hostiles in the room + */ + kite(avoidGoals: (RoomPosition | HasPos)[] = this.room.hostiles, options: MoveOptions = {}): number | undefined { + _.defaults(options, { + fleeRange: 5 + }); + return Movement.kite(this, avoidGoals, options); + } + + private defaultFleeGoals() { + let fleeGoals: (RoomPosition | HasPos)[] = []; + fleeGoals = fleeGoals.concat(this.room.hostiles) + .concat(_.filter(this.room.keeperLairs, lair => (lair.ticksToSpawn || Infinity) < 10)); + return fleeGoals; + } + + /** + * Flee from hostiles in the room, while not repathing every tick + */ + flee(avoidGoals: (RoomPosition | HasPos)[] = this.room.fleeDefaults, + fleeOptions: FleeOptions = {}, + moveOptions: MoveOptions = {}): boolean { + if (avoidGoals.length == 0) { + return false; + } else if (this.room.controller && this.room.controller.my && this.room.controller.safeMode) { + return false; + } else { + const fleeing = Movement.flee(this, avoidGoals, fleeOptions.dropEnergy, moveOptions) != undefined; + if (fleeing) { + // Drop energy if needed + if (fleeOptions.dropEnergy && this.carry.energy > 0) { + const nearbyContainers = this.pos.findInRange(this.room.storageUnits, 1); + if (nearbyContainers.length > 0) { + this.transfer(_.first(nearbyContainers), RESOURCE_ENERGY); + } else { + this.drop(RESOURCE_ENERGY); + } + } + // Invalidate task + if (fleeOptions.invalidateTask) { + this.task = null; + } + } + return fleeing; + } + } + + /** + * Park the creep off-roads + */ + park(pos: RoomPosition = this.pos, maintainDistance = false): number { + return Movement.park(this, pos, maintainDistance); + } + + /** + * Moves a creep off of the current tile to the first available neighbor + */ + moveOffCurrentPos(): number | undefined { + return Movement.moveOffCurrentPos(this); + } + + /** + * Moves onto an exit tile + */ + moveOnExit(): ScreepsReturnCode | undefined { + return Movement.moveOnExit(this); + } + + /** + * Moves off of an exit tile + */ + moveOffExit(avoidSwamp = true): ScreepsReturnCode { + return Movement.moveOffExit(this, avoidSwamp); + } + + moveOffExitToward(pos: RoomPosition, detour = true): number | undefined { + return Movement.moveOffExitToward(this, pos, detour); + } + + // Miscellaneous fun stuff ----------------------------------------------------------------------------------------- + + sayLoop(messageList: string[], pub?: boolean) { + return this.say(messageList[Game.time % messageList.length], pub); + } + + sayRandom(phrases: string[], pub?: boolean) { + return this.say(phrases[Math.floor(Math.random() * phrases.length)], pub); + } + +} +