diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 7db14e8..444e17b --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "a-star": "git+https://github.com/andrewrk/node-astar.git", "factorio-blueprint": "git+https://github.com/demipixel/factorio-blueprint.git", + "js-graph-algorithms": "^1.0.18", "meow": "^3.7.0", "yamljs": "^0.2.10" } diff --git a/tools/lib/addBeacons.js b/tools/lib/addBeacons.js index bc91b86..c8b3224 100644 --- a/tools/lib/addBeacons.js +++ b/tools/lib/addBeacons.js @@ -1,14 +1,7 @@ -const PUMPJACK_EXIT_DIRECTION = { - 0: { x: 2, y: -1 }, - 1: { x: 3, y: 0 }, - 2: { x: 0, y: 3 }, - 3: { x: -1, y: 2 } -}; - const ACTIVATIONS = { sigmoid: x => 1 / (1 + Math.pow(Math.E, Math.max(-60, Math.min(60, x * 5.0)))), relu: x => Math.max(x, 0.0) -} +}; const NODES = ` @@ -21,7 +14,7 @@ const NODES = key: parseInt(m[1]), bias: parseInt(m[2]), activation: m[3] - } + }; }); @@ -187,7 +180,7 @@ DefaultConnectionGene(key=(4063, 0), weight=2.28399291126, enabled=True) from: parseInt(m[1]), to: parseInt(m[2]), weight: parseInt(m[3]) - } : null + } : null; }).filter(connection => !!connection); function runNN(input) { @@ -222,12 +215,7 @@ function shouldPlaceBeacon(bp, posX, posY) { return reach && runNN(input) >= 0.5; } -function placeBeacons(bp, getPumpjackOutput) { - const tempPipes = []; - bp.entities.filter(ent => ent.name == 'pumpjack').forEach(pumpjack => { - tempPipes.push(bp.createEntity('pipe', getPumpjackOutput(pumpjack))); - }); - +function placeBeacons(bp) { const start = bp.topLeft().subtract({ x: 5, y: 5 }); const end = bp.bottomRight().add({ x: 5, y: 5 }); for (let x = start.x; x <= end.x; x++) { @@ -246,8 +234,6 @@ function placeBeacons(bp, getPumpjackOutput) { } } } - - tempPipes.forEach(pipe => bp.removeEntity(pipe)); } -module.exports = placeBeacons +module.exports = placeBeacons; diff --git a/tools/lib/addPipes.js b/tools/lib/addPipes.js new file mode 100644 index 0000000..8971a7c --- /dev/null +++ b/tools/lib/addPipes.js @@ -0,0 +1,308 @@ +const aStar = require('a-star'); +const jsgraphs = require('js-graph-algorithms'); +const Victor = require('victor'); + + +const PUMPJACK_EXIT_DIRECTION = { + 0: { x: 2, y: -1 }, + 1: { x: 3, y: 0 }, + 2: { x: 0, y: 3 }, + 3: { x: -1, y: 2 } +}; + +const MAX_UNDERGROUND_REACH = 11; // Includes underground pipes + +const SIDES = [ + { x: 0, y: -1 }, + { x: 1, y: 0 }, + { x: 0, y: 1 }, + { x: -1, y: 0 }, +]; + +const DIRECTION_FROM_OFFSET = { + '0,-1': 0, + '1,0': 2, + '0,1': 4, + '-1,0': 6 +}; + +let ROTATE_ALL = false; +let FLIP_ALL = false; + +function getPumpjackOutput(pumpjack, direction) { + const ROTATE_OFFSET = ((4 - ROTATE_ALL) % 4); + const ORDER = [0, -1, 2, 3]; + if (typeof direction == 'undefined') direction = pumpjack.direction/2; + if (direction % 2 == 0) ORDER.reverse(); + let offset = { + x: PUMPJACK_EXIT_DIRECTION[(direction + ROTATE_ALL) % 4].x, + y: PUMPJACK_EXIT_DIRECTION[(direction + ROTATE_ALL) % 4].y + }; + offset.x = ORDER[(ORDER.indexOf(offset.x) + ROTATE_OFFSET) % ORDER.length]; + offset.y = ORDER[(ORDER.indexOf(offset.y) + ROTATE_OFFSET) % ORDER.length]; + if (FLIP_ALL) offset.x = 2 - offset.x; + + return pumpjack.position.clone().add(offset); +} + +function getStepFromDirection(direction) { + let out = new Victor(SIDES[direction].x, SIDES[direction].y); + if (FLIP_ALL) out.x = -out.x; + return out; +} + +function connectPumpjack(bp, pumpjack, target, xLimit){ + const hasBeacons = bp.entities.some(ent => ent.name == 'beacon'); + + // Bound the search space for improved speed + const boundsX = [bp.bottomLeft().x - 1, xLimit]; + const boundsY = [bp.topRight().y - 1, bp.bottomLeft().y + 1]; + + const result = aStar({ + start: [null, 'start'], + + isEnd: ([node, meta]) => { + if (meta == 'start') return false; + if (node.x == target.x && node.y == target.y) return true; + let entity = bp.findEntity(node); + if (entity && entity.name == 'pipe') return true; + for (let i = 0; i < SIDES.length; i++) { + const entity = bp.findEntity(node.clone().add(SIDES[i])); + if (entity && entity.name == 'pipe') { + return true; + } + } + return false; + }, + + neighbor: ([node, meta]) => { + let isClear = (p) => ( + p.x >= boundsX[0] && p.x <= boundsX[1] && + p.y >= boundsY[0] && p.y <= boundsY[1] && + (!bp.findEntity(p) || (bp.findEntity(p).name == 'beacon' || + bp.findEntity(p).name == 'pipe')) + ); + let walkInDirection = (first, step) => { + let pos = first.clone(); + let steps = []; + for (let j = 0; j < MAX_UNDERGROUND_REACH; j++){ + if (!isClear(pos)) + break; + steps.push(pos); + pos = pos.clone().add(step); + } + return steps; + }; + + let neighbors = []; + if (meta == 'start') { + for (let dir of [0, 1, 2, 3]) { + let step = getStepFromDirection(dir); + let out = getPumpjackOutput(pumpjack, dir); + neighbors.push(...walkInDirection(out, step).map(p => [p, out])); + } + } else { + for (let step of SIDES) { + let pos = node.clone().add(step); + neighbors.push(...walkInDirection(pos, step).map(p => [p, null])); + } + } + return neighbors; + }, + + distance: ([nodeA, metaA], [nodeB, metaB]) => { + const beaconPenalty = 100; + let fromStart = false; + if (metaA == 'start'){ + fromStart = true; + nodeA = metaB; + if (nodeA.isEqualTo(nodeB)) { + const entity = bp.findEntity(nodeB); + return 1 + beaconPenalty * (entity && entity.name == 'beacon'); + } + } + let beaconCount = 0; + let dir = nodeB.clone().subtract(nodeA).normalize(); + if (hasBeacons) { + let step = nodeA; + let prevEntity = false; + do { + let entity = bp.findEntity(step); + if (entity && entity.name == 'beacon' && prevEntity != entity){ + beaconCount++; + prevEntity = entity; + } + step = step.clone().add(dir); + } while (!step.isEqualTo(nodeB)); + } + let dist = Math.abs(nodeA.x - nodeB.x) + Math.abs(nodeA.y - nodeB.y); + if (fromStart) + dist += 1; + if (dist > 3) + dist = 3 + (dist-3)/10; + return dist + beaconCount * beaconPenalty; + }, + + heuristic: ([node, meta]) => { + if (meta == 'start') return 0; + // Note: we could actually estimate by dividing by 4. + // We deliberately underestimate to encourage the alorgithm to probe + // around a bit more – it might discover an already existing pipe that + // it could connect to. + return (Math.abs(node.x - target.x) + Math.abs(node.y - target.y))/20; + }, + + hash: ([node, meta]) => { + if (meta == 'start') + return 'start'; + else + return node.x + ',' + node.y; + }, + + timeout: 5000 + }); + + if (result.status != 'success') { + if (result.status == 'noPath'){ + throw new Error('Could not create path for all pipes!'); + } + else throw new Error('Took too long to generate pipe paths!'); + } + + result.path.shift(); + let outputPos = result.path[0][1]; + for (let i = 0; i < 4; i++) { + if (getPumpjackOutput(pumpjack, i).isEqualTo(outputPos)){ + pumpjack.setDirection(i * 2); + } + } + + if (bp.findEntity(outputPos)) bp.findEntity(outputPos).remove(); + bp.createEntity('pipe', outputPos, 0, true); + + + let prev = outputPos; + result.path.forEach(([pos, meta]) => { + if (prev.distance(pos) < 2){ + if (bp.findEntity(pos)) bp.findEntity(pos).remove(); + bp.createEntity('pipe', pos, 0, true); + } else { + let dir = pos.clone().subtract(prev).normalize(); + let step = prev.clone().add(dir); + while (!step.isEqualTo(pos)){ + if (bp.findEntity(step)) bp.findEntity(step).remove(); + bp.createEntity('pipe', step, 0, true); + step = step.clone().add(dir); + } + if (bp.findEntity(pos)) bp.findEntity(pos).remove(); + bp.createEntity('pipe', pos, 0, true); + } + prev = pos; + }); +} + +function addPipes(bp, target){ + let pumpjacks = bp.entities.filter(ent => ent.name == 'pumpjack'); + + // Generate minimum spanning tree + let vertices = pumpjacks.map(pj => pj.position); + vertices.splice(0, 0, target); + let graph = new jsgraphs.WeightedGraph(vertices.length); + for (let i = 0; i < vertices.length; i++){ + for (let j = 0; j < vertices.length; j++){ + if (i >= j) continue; + const weight = (Math.abs(vertices[i].x - vertices[j].x) + + Math.abs(vertices[i].y - vertices[j].y)); + graph.addEdge(new jsgraphs.Edge(i, j, weight)); + } + } + let mst = new jsgraphs.Graph(vertices.length); + let mstEdges = new jsgraphs.EagerPrimMST(graph).mst; + mstEdges.forEach(e => mst.addEdge(e.from(), e.to())); + console.timeLog("outpost", "pipes mst done"); + + // Turn spanning tree into pipes, in BFS order starting at the target + let visited = [true]; + let queue = mst.adj(0).map(x => [target, x]); + while (queue.length) { + let [fromPos, toI] = queue.shift(); + visited[toI] = true; + let pumpjack = pumpjacks[toI - 1]; + + connectPumpjack(bp, pumpjack, fromPos, target.x); + mst.adj(toI).filter(x => !visited[x]) + .forEach(x => queue.push([getPumpjackOutput(pumpjack), x])); + } + console.timeLog("outpost", "pipes connection done"); +} + + +function simplifyPipes(bp, start, minLength = 3) { + const checked = {}; + const stack = [start]; + + const straights = []; // List of straight sets of pipe + + while (stack.length > 0) { + const pos = stack.pop(); + const entity = bp.findEntity(pos); + if (checked[pos.x + ',' + pos.y] || !entity || entity.name != 'pipe') continue; + checked[pos.x + ',' + pos.y] = true; + + let sidePipes = []; + SIDES.forEach(side => { + let sidePos = pos.clone().add(side); + let ent = bp.findEntity(sidePos); + if (!!ent && (ent.name == 'pipe' || + (ent.name == 'pumpjack' && (getPumpjackOutput(ent).x == pos.x && getPumpjackOutput(ent).y == pos.y)))){ + sidePipes.push(sidePos); + } + }); + + if (pos.from) { + let shouldUnderground = sidePipes.length == 2 && (sidePipes[0].x - pos.x == pos.x - sidePipes[1].x) && (sidePipes[0].y - pos.y == pos.y - + sidePipes[1].y); + + const offsetX = pos.from.x - pos.x; + const offsetY = pos.from.y - pos.y; + const direction = DIRECTION_FROM_OFFSET[offsetX + ',' + offsetY]; + + if (pos.from.underground && pos.from.underground.length != MAX_UNDERGROUND_REACH && shouldUnderground) { + pos.from.underground.push(pos); + pos.underground = pos.from.underground; + } else if (shouldUnderground && (!pos.from.underground || pos.from.underground.length == MAX_UNDERGROUND_REACH)) { + pos.underground = [pos]; + pos.underground.direction = direction; + straights.push(pos.underground); + } + } else if (sidePipes.length == 1 && sidePipes[0].x == pos.x - 1) { + pos.underground = [pos]; + pos.underground.direction = 2; + straights.push(pos.underground); + } + + SIDES.forEach(side => { + const newPos = pos.clone().add(side); + newPos.from = pos; + stack.push(newPos); + }); + } + + straights.filter(s => s.length >= minLength).forEach(straight => { + straight.forEach(pos => bp.findEntity(pos).remove()); + bp.createEntity('pipe_to_ground', straight[0], straight.direction); + bp.createEntity('pipe_to_ground', straight[straight.length - 1], (straight.direction + 4) % 8); + }); + console.timeLog("outpost", "pipes undegrounding done"); +} + +function generatePipes(bp, target, rotateAll, flipAll){ + ROTATE_ALL = rotateAll; + FLIP_ALL = flipAll; + console.log("R", rotateAll, "F", flipAll); + addPipes(bp, target); + simplifyPipes(bp, target); +} + + +module.exports = generatePipes; diff --git a/tools/lib/powerUtils.js b/tools/lib/powerUtils.js new file mode 100644 index 0000000..2fc4a40 --- /dev/null +++ b/tools/lib/powerUtils.js @@ -0,0 +1,154 @@ +const Victor = require('victor'); +const jsgraphs = require('js-graph-algorithms'); +const aStar = require('a-star'); + +const MAX_POLE_DISTANCE = 9; + +function connectConsumers(bp, allConsumers){ + // Picking the best poles is a weighted set cover problem. + // We approximate a good solution with a greedy algorithm + let hashPos = pos => pos.x + ',' + pos.y; + let posFromHash = function(hash){ + let [x, y] = hash.split(','); + return new Victor(parseInt(x), parseInt(y)); + }; + + let poleMap = new Map(); + let consumerMap = new Map(); + allConsumers.forEach(consumer => { + let poleCount = 0; + for (let x = -3; x < consumer.size.x + 3; x++) { + for (let y = -3; y < consumer.size.y + 3; y++) { + const pos = { x: x + consumer.position.x, y: y + consumer.position.y }; + const posHash = hashPos(pos); + if (bp.findEntity(pos)) + continue; + poleCount++; + if (!poleMap.has(posHash)){ + poleMap.set(posHash, [consumer]); + } else { + poleMap.get(posHash).push(consumer); + } + } + } + consumerMap.set(consumer, poleCount); + }); + + let minDistance = function(posHash, consumers) { + let poleCenter = posFromHash(posHash).add(new Victor(0.5, 0.5)); + let poleDistances = consumers.map(c => c.center().distance(poleCenter)); + return Math.min(...poleDistances) + ( + poleDistances.reduce((a, b) => a + b, 0) / poleDistances.length / 100); + }; + + let poleQueue = Array.from(poleMap.entries()).sort( + (a, b) => ((a[1].length*10000 - minDistance(a[0], a[1])) - + (b[1].length*10000 - minDistance(b[0], b[1])))); + for (let [pole, consumers] of poleQueue){ + let canRemove = consumers.every(c => consumerMap.get(c) > 1); + if (canRemove) { + poleMap.delete(pole); + consumers.forEach(c => consumerMap.set(c, consumerMap.get(c) - 1)); + } + } + for (let pole of poleMap.keys()){ + bp.createEntity('medium_electric_pole', posFromHash(pole)); + } + console.timeLog("outpost", "consumer poles done"); +} + + +function connectPoles(bp){ + // Make sure that the power poles form a connected graph + // First, we construct a MST over the power poles + const poles = bp.entities.filter(ent => ent.name == 'medium_electric_pole') + .map(ent => ent.position); + let poleGraph = new jsgraphs.WeightedGraph(poles.length); + for (let i = 0; i < poles.length; i++){ + for (let j = 0; j < i; j++){ + let distance = poles[i].distance(poles[j]); + poleGraph.addEdge(new jsgraphs.Edge(i, j, distance)); + } + } + let mstEdges = new jsgraphs.EagerPrimMST(poleGraph).mst; + + // Power poles that are at most MAX_POLE_DISTANCE away from each other have a + // direct power line connection. For the remaining edges of the MST, we have + // to add new power poles to create a connection. + let newEdges = mstEdges.filter(e => e.weight > MAX_POLE_DISTANCE); + for (let edge of newEdges){ + // We use A* to place new poles between the two existing ones + const result = aStar({ + start: poles[edge.from()], + isEnd: pos => pos.isEqualTo(poles[edge.to()]), + + neighbor: pos => { + let neighbors = []; + for (let i = -9; i <= 9; i++){ + for (let j = -9; j <= 9; j++){ + if (i == j) continue; + let offset = new Victor(i, j); + if (offset.length() <= 9){ + let nPos = pos.clone().add(offset); + let ent = bp.findEntity(nPos); + if (!ent || ent.name == 'medium_electric_pole'){ + neighbors.push(nPos); + } + } + } + } + return neighbors; + }, + + distance: (posA, posB) => { + // We mostly care for the amount of poles that need to be placed. + // Euclidian distance doesn't really matter. But we add a very slight + // penalty of the square distance to encourage the algorithm to generate + // evenly spaced power poles, which are more pleasing to the eye. + let polePrice = 1; + + let entB = bp.findEntity(posB); + if (entB && entB.name == 'medium_electric_pole'){ + let entA = bp.findEntity(posA); + if (entA && entA.name == 'medium_electric_pole') + return 0; + polePrice = 0; + } + + let distance = posA.distanceSq(posB); + return polePrice + distance / 1000; + }, + + heuristic: pos => { // eslint-disable-line no-loop-func + let distance = pos.distance(poles[edge.to()]); + if (distance == 0) return 0; + let poleCount = Math.ceil(distance / 9); + let avgDistance = distance / poleCount; + return poleCount * (1 + avgDistance * avgDistance / 1000) - 1; + }, + + hash: pos => pos.toString(), + + timeout: 1000, + }); + + + if (result.status != 'success') { + if (result.status == 'noPath'){ + throw new Error('Could not create path for electricity!'); + } + else throw new Error('Took too long to generate electricity paths!'); + } + + for (let pole of result.path){ + if (!bp.findEntity(pole)) + bp.createEntity('medium_electric_pole', {x: pole.x, y: pole.y}); + } + } + console.timeLog("outpost", "pole connectivity done"); +} + +module.exports = { + connectConsumers: connectConsumers, + connectPoles: connectPoles, +}; diff --git a/tools/oilOutpost.js b/tools/oilOutpost.js index 201cb81..8dbe542 100644 --- a/tools/oilOutpost.js +++ b/tools/oilOutpost.js @@ -1,39 +1,17 @@ const Blueprint = require('factorio-blueprint'); -const aStar = require('a-star'); const Victor = require('victor'); const generateDefenses = require('./lib/defenses'); const generateTrainStation = require('./lib/trainStation'); const fixRail = require('./lib/fixRail'); const addBeacons = require('./lib/addBeacons'); - -const PUMPJACK_EXIT_DIRECTION = { - 0: { x: 2, y: -1 }, - 2: { x: 3, y: 0 }, - 4: { x: 0, y: 3 }, - 6: { x: -1, y: 2 } -}; - -const SIDES = [ - { x: 1, y: 0 }, - { x: -1, y: 0 }, - { x: 0, y: 1 }, - { x: 0, y: -1 } -]; - -const DIRECTION_FROM_OFFSET = { - '0,-1': 0, - '1,0': 2, - '0,1': 4, - '-1,0': 6 -}; +const addPipes = require('./lib/addPipes'); +const powerUtils = require('./lib/powerUtils'); function useOrDefault(value, def) { return isNaN(parseInt(value)) || value == undefined ? def : parseInt(value); } -const MAX_UNDERGROUND_REACH = 11; // Includes underground pipes - module.exports = function(string, opt = {}) { if (!string) throw new Error('You must provide a blueprint string with pumpjacks!'); @@ -71,11 +49,6 @@ module.exports = function(string, opt = {}) { const SINGLE_HEADED_TRAIN = opt.exitRoute || false; const ADDITIONAL_SPACE = useOrDefault(opt.addtionalStationSpace, 0); - // Bot info - const BOT_BASED = opt.botBased || false; - const REQUEST_TYPE = opt.requestItem || 'iron_ore'; - const REQUEST_AMOUNT = useOrDefault(opt.requestAmount, 4800); - // Tiles const CONCRETE = opt.concrete || ''; const BORDER_CONCRETE = opt.borderConcrete || ''; @@ -83,29 +56,14 @@ module.exports = function(string, opt = {}) { const newEntityData = {}; - if (CONCRETE) newEntityData[CONCRETE] = { type: 'tile' } - if (BORDER_CONCRETE && !newEntityData[BORDER_CONCRETE]) newEntityData[BORDER_CONCRETE] = { type: 'tile' } - if (TRACK_CONCRETE && !newEntityData[TRACK_CONCRETE]) newEntityData[TRACK_CONCRETE] = { type: 'tile' } + if (CONCRETE) newEntityData[CONCRETE] = { type: 'tile' }; + if (BORDER_CONCRETE && !newEntityData[BORDER_CONCRETE]) newEntityData[BORDER_CONCRETE] = { type: 'tile' }; + if (TRACK_CONCRETE && !newEntityData[TRACK_CONCRETE]) newEntityData[TRACK_CONCRETE] = { type: 'tile' }; Blueprint.setEntityData(newEntityData); - function getPumpjackOutput(pumpjack) { - const ROTATE_OFFSET = ((4 - ROTATE_ALL) % 4); - const ORDER = [0, -1, 2, 3]; - if (pumpjack.direction % 4 == 0) ORDER.reverse(); - let offset = { - x: PUMPJACK_EXIT_DIRECTION[(pumpjack.direction + ROTATE_ALL * 2) % 8].x, - y: PUMPJACK_EXIT_DIRECTION[(pumpjack.direction + - ROTATE_ALL * 2) % 8].y - }; - offset.x = ORDER[(ORDER.indexOf(offset.x) + ROTATE_OFFSET) % ORDER.length]; - offset.y = ORDER[(ORDER.indexOf(offset.y) + ROTATE_OFFSET) % ORDER.length]; - if (FLIP_ALL) offset.x = 2 - offset.x; - - return pumpjack.position.clone().add(offset); - } - const templateBp = new Blueprint(string); let bp = new Blueprint(); + console.time("outpost"); bp.placeBlueprint(templateBp, { x: 0, y: 0 }, (4 - ROTATE_ALL) % 4); @@ -122,7 +80,6 @@ module.exports = function(string, opt = {}) { });*/ bp.fixCenter({ x: 1, y: 0 }); } - bp = new Blueprint(bp.toObject()); bp.fixCenter({ x: 0.5, y: 0.5 }); // Tracks in a blueprint always offsets everything by 0.5, so let's keep it clean @@ -147,174 +104,30 @@ module.exports = function(string, opt = {}) { bp.entities.forEach(ent => { if (ent.name != 'pumpjack') throw new Error('The blueprint must only contain pumpjacks and one track!'); - else if (bp.findEntity(getPumpjackOutput(ent))) { - throw new Error('A pumpjack is facing into another pumpjack!'); - } }); - if (USE_BEACONS) addBeacons(bp, getPumpjackOutput); + console.timeLog("outpost", "prepare"); - let target = new Victor(bp.topRight().x + 1, (bp.topRight().y + bp.bottomRight().y) / 2); + if (USE_BEACONS){ + addBeacons(bp); + console.timeLog("outpost", "beacons"); + } - target.x += -(target.x % 2) + alignment.x + let target = new Victor(bp.topRight().x + 1, (bp.topRight().y + bp.bottomRight().y) / 2); + target.x += -(target.x % 2) + alignment.x; target.y += -(target.y % 2) + alignment.y; - bp.entities.filter(ent => ent.name == 'pumpjack').forEach(pumpjack => { - let powered = false; - bp.entities.filter(ent => ent.name == 'medium_electric_pole').forEach(pole => { - if (powered) return; - if ((pole.position.x - pumpjack.position.x <= 5) && - (pole.position.y - pumpjack.position.y <= 5) && - (pumpjack.position.x - pole.position.x <= 3) && - (pumpjack.position.y - pole.position.y <= 3)) { - powered = true; - } - }); - if (powered) return; - - const output = getPumpjackOutput(pumpjack); - const electricPoleLocations = []; - for (let x = -1; x <= 3; x++) { - for (let y = -1; y <= 3; y++) { - if (x != -1 && x != 3 && y != -1 && y != 3) continue; // Only on edges - const pos = { x: x + pumpjack.position.x, y: y + pumpjack.position.y } - if (pos.x == output.x && pos.y == output.y) continue; - if (x == output.x || y == output.y) electricPoleLocations.push(pos); - else electricPoleLocations.unshift(pos); - } - } - for (let i = 0; i < electricPoleLocations.length; i++) { - const x = electricPoleLocations[i].x; - const y = electricPoleLocations[i].y; - if (bp.findEntity({ x, y })) continue; - let blocking = false; - SIDES.forEach(side => { - const ent = bp.findEntity({ x: x + side.x, y: y + side.y }); - if (!ent || ent.name != 'pumpjack') return; - const otherOutput = getPumpjackOutput(ent); - if (otherOutput.x == x && otherOutput.y == y) blocking = true; - }); - if (!blocking) { - bp.createEntity('medium_electric_pole', { x, y }); - break; - } - } - }); - - bp.entities.filter(ent => ent.name == 'pumpjack').forEach(pumpjack => { - if (bp.findEntity(getPumpjackOutput(pumpjack))) return; - // if (bp.findEntity(getPumpjackOutput(pumpjack))) bp.findEntity(getPumpjackOutput(pumpjack)).remove(); - const result = aStar({ - start: getPumpjackOutput(pumpjack), - isEnd: (node) => { - if (node.x == target.x && node.y == target.y) return true; - for (let i = 0; i < SIDES.length; i++) { - const entity = bp.findEntity(node.clone().add(SIDES[i])); - if (entity && entity.name == 'pipe') { - return true; - } - } - return false; - }, - neighbor: (node) => { - return SIDES.map(side => node.clone().add(side)) - .filter(pos => !bp.findEntity(pos) || bp.findEntity(pos).name == 'beacon') - .filter(pos => pos.x <= target.x); - }, - distance: (nodeA, nodeB) => { - let scale = 1.0; - if (USE_BEACONS) { - const entityA = bp.findEntity(nodeA); - const entityB = bp.findEntity(nodeB); - if ((entityA && entityA.name == 'beacon') || (entityB && entityB.name == 'beacon')) { - scale = 100; - } - } - return (Math.abs(nodeA.x - nodeB.x) * 0.98 + Math.abs(nodeA.y - nodeB.y)) * scale; - }, - heuristic: (node) => { - return 0; //Math.abs(node.x - target.x) + Math.abs(node.y - target.y); - }, - hash: (node) => { - return node.x + ',' + node.y - }, - timeout: 5000 - }); - if (result.status != 'success') { - if (result.status == 'noPath') throw new Error('Could not create path for all pipes!'); - else throw new Error('Took too long to generate pipe paths!'); - } - result.path.forEach(pos => { - if (bp.findEntity(pos)) bp.findEntity(pos).remove(); - bp.createEntity('pipe', pos, 0, true); - }); - }); - - function simplifyPipes(bp, start, minLength = 3) { - const checked = {}; - const stack = [start]; - - const straights = []; // List of straight sets of pipe - - while (stack.length > 0) { - const pos = stack.pop(); - const entity = bp.findEntity(pos); - if (checked[pos.x + ',' + pos.y] || !entity || entity.name != 'pipe') continue; - checked[pos.x + ',' + pos.y] = true; - - const sidePipes = SIDES.map(side => bp.findEntity(pos.clone().add(side))) - .filter(ent => !!ent) - .filter(ent => ent.name == 'pipe' || (ent.name == 'pumpjack' && (getPumpjackOutput(ent).x == pos.x && getPumpjackOutput(ent).y == pos.y))) - .map(ent => ent.position); - - if (pos.from) { - let shouldUnderground = sidePipes.length == 2 && (sidePipes[0].x - pos.x == pos.x - sidePipes[1].x) && (sidePipes[0].y - pos.y == pos.y - - sidePipes[1].y) - - const offsetX = pos.from.x - pos.x; - const offsetY = pos.from.y - pos.y; - const direction = DIRECTION_FROM_OFFSET[offsetX + ',' + offsetY]; - - if (pos.from.underground && pos.from.underground.length != MAX_UNDERGROUND_REACH && shouldUnderground) { - pos.from.underground.push(pos); - pos.underground = pos.from.underground; - } else if (shouldUnderground && (!pos.from.underground || pos.from.underground.length == MAX_UNDERGROUND_REACH)) { - pos.underground = [pos]; - pos.underground.direction = direction; - straights.push(pos.underground); - } - } else if (sidePipes.length == 1 && sidePipes[0].x == pos.x - 1) { - pos.underground = [pos]; - pos.underground.direction = 2; - straights.push(pos.underground); - } - - SIDES.forEach(side => { - const newPos = pos.clone().add(side); - newPos.from = pos; - stack.push(newPos); - }); - } - - straights.filter(s => s.length >= minLength).forEach(straight => { - straight.forEach(pos => bp.findEntity(pos).remove()); - bp.createEntity('pipe_to_ground', straight[0], straight.direction); - bp.createEntity('pipe_to_ground', straight[straight.length - 1], (straight.direction + 4) % 8); - }); - } - - simplifyPipes(bp, target); + addPipes(bp, target, ROTATE_ALL, FLIP_ALL); + console.timeLog("outpost", "pipes done"); const lowerX = bp.topLeft().x; let upperY = bp.bottomLeft().y; let lowerY = Math.min(bp.topLeft().y, INCLUDE_RADAR ? -3 : 0); - let trainStopLocation = null; - if (INCLUDE_TRAIN_STATION) { - trainStopLocation = generateTrainStation(bp, + generateTrainStation(bp, { x: target.x + 3 + 3 * TANKS, y: target.y - 2 }, - Math.max(bp.bottomRight().y, target.y + 2 + WAGONS * 7 + ((SINGLE_HEADED_TRAIN ? 0 : 1) * LOCOMOTIVES) * 7), + Math.max(bp.bottomRight().y, target.y + 2 + WAGONS * 7 + ((SINGLE_HEADED_TRAIN ? 0 : 1) * LOCOMOTIVES) * 7), { LOCOMOTIVES, TRACK_CONCRETE, @@ -353,8 +166,7 @@ module.exports = function(string, opt = {}) { bp.createEntity('pipe', { x: target.x + 3 * TANKS - j, y: target.y + CONNECT_OFFSET + 3 + (i - 1) * 7 + 3 }); } } - } else { - trainStopLocation = { x: target.x + 3 + 3 * TANKS, y: target.y - 2 }; + console.timeLog("outpost", "station done"); } const upperX = bp.topRight().x + ADDITIONAL_SPACE; @@ -369,6 +181,11 @@ module.exports = function(string, opt = {}) { BORDER_CONCRETE, INCLUDE_LIGHTS }); + console.timeLog("outpost", "defenses done"); + + powerUtils.connectConsumers(bp, bp.entities.filter( + ent => ent.name == 'pumpjack' || ent.name == 'beacon')); + powerUtils.connectPoles(bp); if (MODULE) { bp.entities.filter(ent => ent.name == 'pumpjack').forEach(ent => { @@ -404,5 +221,6 @@ module.exports = function(string, opt = {}) { finalBp.placeBlueprint(bp, { x: 0, y: 0 }, ROTATE_ALL); finalBp.name = NAME.replace('%pumpjacks%', finalBp.entities.filter(e => e.name == 'pumpjack').length); + console.timeLog("outpost", "wrapup"); return finalBp.encode(); -} +};