diff --git a/.gitignore b/.gitignore index 393ad28..21fc365 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +test-bots/ + ### Node ### # Logs logs diff --git a/agent/create-agent.js b/agent/create-agent.js index eadc322..b61fff8 100644 --- a/agent/create-agent.js +++ b/agent/create-agent.js @@ -36,13 +36,16 @@ function createAgent(blueprint = {}) { race: Race.RANDOM, ...blueprint.settings, }, - interface: blueprint.interface || { raw: true }, + interface: blueprint.interface || { raw: true, rawCropToPlayableArea: true }, canAfford(unitTypeId, earmarkName) { const { data } = this._world; const { minerals, vespene } = this; const earmarks = data.getEarmarkTotals(earmarkName); const unitType = data.getUnitTypeData(unitTypeId); + + // console.log("current earmarks", earmarks); + // console.log("mineral cost:", unitType.mineralCost); const result = ( (minerals - earmarks.minerals >= unitType.mineralCost) && @@ -185,7 +188,7 @@ function createAgent(blueprint = {}) { * @TODO: the first time we see an enemy unit, we should set the value of this on the agent and then * memoize it */ - this.enemy = { + this.opponent = { race: enemyPlayer.raceRequested !== Race.RANDOM ? enemyPlayer.raceRequested : Race.NORACE, }; } diff --git a/constants/enums.js b/constants/enums.js index 586e69a..6f59f08 100644 --- a/constants/enums.js +++ b/constants/enums.js @@ -186,6 +186,17 @@ const AbilityDataTarget = { const { valuesById: AbilityDataTargetId } = api.lookupType('AbilityData').lookupEnum('Target'); +/** + * @enum {SC2APIProtocol.CloakState} + */ +const CloakState = { + CLOAKED: 1, + CLOAKEDDETECTED: 2, + NOTCLOAKED: 3, +}; + +const { valuesById: CloakStateId } = api.lookupEnum('CloakState'); + module.exports = { AbilityDataTarget, AbilityDataTargetId, @@ -198,6 +209,8 @@ module.exports = { BuildOrder, BuildOrderId, BuildResult, + CloakState, + CloakStateId, Difficulty, DifficultyId, DisplayType, diff --git a/engine/create-engine.js b/engine/create-engine.js index b3e1cd3..ce03536 100644 --- a/engine/create-engine.js +++ b/engine/create-engine.js @@ -7,6 +7,7 @@ const argv = require('yargs') .number('StartPort') .number('GamePort') .string('LadderServer') + .string('OpponentId') .argv; const pascalCase = require('pascal-case'); // const chalk = require('chalk'); @@ -63,6 +64,7 @@ function createEngine(options = {}) { /** @type {Engine} */ const engine = { + getWorld() { return world; }, _totalLoopDelay: 0, _gameLeft: false, launcher, @@ -163,7 +165,7 @@ function createEngine(options = {}) { }; if (isManaged) { - let sPort = /** @type {number} */ argv.StartPort + 1; + let sPort = argv.StartPort + 1; participant = { ...participant, @@ -209,6 +211,11 @@ function createEngine(options = {}) { async firstRun() { const { data, resources, agent } = world; + if (isManaged) { + agent.opponent = agent.opponent || {}; + agent.opponent.id = argv.OpponentId; + } + /** @type {SC2APIProtocol.ResponseData} */ const gameData = await _client.data({ abilityId: true, @@ -362,6 +369,9 @@ function createEngine(options = {}) { // debug system runs last because it updates the in-client debug display return debugSystem(world); }, + shutdown() { + world.resources.get().actions._client.close(); + }, _lastRequest: null, }; diff --git a/engine/create-unit.js b/engine/create-unit.js index 3e943c1..0af94c9 100644 --- a/engine/create-unit.js +++ b/engine/create-unit.js @@ -2,7 +2,8 @@ const UnitType = require("../constants/unit-type"); const Ability = require("../constants/ability"); -const { Alliance, WeaponTargetType, Attribute } = require("../constants/enums"); +const { TownhallRace } = require("../constants/race-map"); +const { Alliance, WeaponTargetType, Attribute, CloakState, Race } = require("../constants/enums"); const { techLabTypes, reactorTypes, @@ -26,15 +27,43 @@ function createUnit(unitData, { data, resources }) { const { alliance } = unitData; + /** @type {Unit} */ const blueprint = { tag: unitData.tag, lastSeen: frame.getGameLoop(), noQueue: unitData.orders.length === 0, labels: new Map(), _availableAbilities: [], - async burrow() { + async inject(t) { + if (this.canInject()) { + let target; + if (t) { + target = t; + } else { + const [closestIdleHatch] = units.getClosest( + this.pos, + units.getById(TownhallRace[Race.ZERG]), + 3 + ).filter(u => u.isIdle()); + + if (closestIdleHatch) { + target = closestIdleHatch; + } else { + return; + } + } + + return actions.do(Ability.EFFECT_INJECTLARVA, this.tag, { target }); + } + }, + async blink(target, opts = {}) { + if (this.canBlink()) { + return actions.do(Ability.EFFECT_BLINK, this.tag, { target, ...opts }); + } + }, + async burrow(opts = {}) { if (this.is(UnitType.WIDOWMINE)) { - return actions.do(Ability.BURROWDOWN, this.tag); + return actions.do(Ability.BURROWDOWN, this.tag, opts); } }, async toggle(options = {}) { @@ -68,6 +97,9 @@ function createUnit(unitData, { data, resources }) { getLabel(name) { return this.labels.get(name); }, + getLife() { + return this.health / this.healthMax * 100; + }, abilityAvailable(id) { return this._availableAbilities.includes(id); }, @@ -80,12 +112,18 @@ function createUnit(unitData, { data, resources }) { is(type) { return this.unitType === type; }, + isCloaked() { + return this.cloak !== CloakState.NOTCLOAKED; + }, isConstructing() { return this.orders.some(o => constructionAbilities.includes(o.abilityId)); }, isCombatUnit() { return combatTypes.includes(this.unitType); }, + isEnemy() { + return this.alliance === Alliance.ENEMY; + }, isFinished() { return this.buildProgress >= 1; }, @@ -130,6 +168,9 @@ function createUnit(unitData, { data, resources }) { // if this unit wasn't updated this frame, this will be false return this.lastSeen === frame.getGameLoop(); }, + isIdle() { + return this.noQueue; + }, isStructure() { return this.data().attributes.includes(Attribute.STRUCTURE); }, @@ -141,6 +182,12 @@ function createUnit(unitData, { data, resources }) { const addon = units.getByTag(this.addOnTag); return techLabTypes.includes(addon.unitType); }, + canInject() { + return this.abilityAvailable(Ability.EFFECT_INJECTLARVA); + }, + canBlink() { + return this.abilityAvailable(Ability.EFFECT_BLINK) || this.abilityAvailable(Ability.EFFECT_BLINK_STALKER); + }, canMove() { return this._availableAbilities.includes(Ability.MOVE); }, diff --git a/engine/create-world.js b/engine/create-world.js index 8c8b49e..687c5df 100644 --- a/engine/create-world.js +++ b/engine/create-world.js @@ -10,7 +10,7 @@ const MapManager = require('../resources/map'); const UnitManager = require('../resources/units'); /** @returns {World} */ -function createWorld() { +function createWorld(client) { const world = { agent: null, data: null, @@ -26,7 +26,7 @@ function createWorld() { debug: Debugger(world), units: UnitManager(world), events: EventChannel(world), - actions: ActionManager(world), + actions: ActionManager(world, client), }); return world; diff --git a/engine/data-storage.js b/engine/data-storage.js index da24230..d0ac714 100644 --- a/engine/data-storage.js +++ b/engine/data-storage.js @@ -1,6 +1,7 @@ "use strict"; const debugEarmark = require('debug')('sc2:debug:earmark'); +const { Race, Attribute } = require('../constants/enums'); const { AbilitiesByUnit } = require('../constants'); /** @@ -29,7 +30,20 @@ function createDataManager() { .map(unitAbility => parseInt(unitAbility[0], 10)); }, getUnitTypeData(unitTypeId) { - return this.get('units')[unitTypeId]; + /** @type {SC2APIProtocol.UnitTypeData} */ + const unitData = this.get('units')[unitTypeId]; + + /** + * Fixes unit cost for zerg structures (removes the 'drone cost' inflation) + */ + if (unitData.race === Race.ZERG && unitData.attributes.includes(Attribute.STRUCTURE)) { + return { + ...unitData, + mineralCost: unitData.mineralCost - 50, + }; + } else { + return unitData; + } }, getUpgradeData(upgradeId) { return this.get('upgrades')[upgradeId]; diff --git a/engine/launcher.js b/engine/launcher.js index e0e8c3a..09445c8 100644 --- a/engine/launcher.js +++ b/engine/launcher.js @@ -9,24 +9,13 @@ const path = require('path'); const findP = require('find-process'); let EXECUTE_INFO_PATH; -if (os.platform() === 'darwin') { - EXECUTE_INFO_PATH = path.join('Library', 'Application Support', 'Blizzard', 'StarCraft II', 'ExecuteInfo.txt'); -} else { - EXECUTE_INFO_PATH = path.join('Documents', 'StarCraft II', 'ExecuteInfo.txt'); -} - -const HOME_DIR = os.homedir(); - -const executeInfoText = fs.readFileSync(path.join(HOME_DIR, EXECUTE_INFO_PATH)).toString(); -const executablePath = executeInfoText.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/m)[2]; - -const parsedPath = executablePath.split(path.sep); -const execName = parsedPath[parsedPath.length - 1]; - -const basePath = parsedPath.slice(0, parsedPath.findIndex(s => s === 'StarCraft II') + 1).join(path.sep); +let executablePath; +let execName; +let basePath; /** @type {Launcher} */ async function launcher(options = {}) { + setupFilePaths(); const opts = { listen: '127.0.0.1', port: 5000, @@ -119,4 +108,22 @@ async function findMap(mapName) { throw new Error(`Map "${mapName}" not found`); } +function setupFilePaths() { + if (os.platform() === 'darwin') { + EXECUTE_INFO_PATH = path.join('Library', 'Application Support', 'Blizzard', 'StarCraft II', 'ExecuteInfo.txt'); + } else { + EXECUTE_INFO_PATH = path.join('Documents', 'StarCraft II', 'ExecuteInfo.txt'); + } + + const HOME_DIR = os.homedir(); + + const executeInfoText = fs.readFileSync(path.join(HOME_DIR, EXECUTE_INFO_PATH)).toString(); + executablePath = executeInfoText.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/m)[2]; + + const parsedPath = executablePath.split(path.sep); + execName = parsedPath[parsedPath.length - 1]; + + basePath = parsedPath.slice(0, parsedPath.findIndex(s => s === 'StarCraft II') + 1).join(path.sep); +} + module.exports = { launcher, findMap }; diff --git a/interfaces.d.ts b/interfaces.d.ts index 8ff66d4..8f98467 100644 --- a/interfaces.d.ts +++ b/interfaces.d.ts @@ -104,8 +104,9 @@ interface SystemWrapper { _system: System; } -type Enemy = { - race: SC2APIProtocol.Race; +type Opponent = { + id?: string; + race?: SC2APIProtocol.Race; } interface PlayerData extends SC2APIProtocol.PlayerCommon, SC2APIProtocol.PlayerRaw { } @@ -122,7 +123,7 @@ interface Agent extends PlayerData { canAffordUpgrade: (upgradeId: number) => boolean; hasTechFor: (unitTypeId: number) => boolean; race?: SC2APIProtocol.Race; - enemy?: Enemy; + opponent?: Opponent; settings: SC2APIProtocol.PlayerSetup; systems: SystemWrapper[]; use: (sys: (SystemWrapper | SystemWrapper[])) => void; @@ -140,25 +141,36 @@ interface Unit extends SC2APIProtocol.Unit { availableAbilities: () => Array; data: () => SC2APIProtocol.UnitTypeData; is: (unitType: UnitTypeId) => boolean; + isCloaked: () => boolean; + isConstructing: () => boolean; isCombatUnit: () => boolean; + isEnemy: () => boolean; isFinished: () => boolean; isWorker: () => boolean; isTownhall: () => boolean; isGasMine: () => boolean; isMineralField: () => boolean; isStructure: () => boolean; + isIdle: () => boolean; isCurrent: () => boolean; isHolding: () => boolean; isGathering: (type?: 'minerals' | 'vespene') => boolean; + isReturning: () => boolean; hasReactor: () => boolean; hasTechLab: () => boolean; hasNoLabels: () => boolean; + canInject: () => boolean; + canBlink: () => boolean; canMove: () => boolean; canShootUp: () => boolean; update: (unit: SC2APIProtocol.Unit) => void; + inject: (target?: Unit) => Promise; + blink: (target: Point2D, opts: AbilityOptions) => Promise; toggle: (options: AbilityOptions) => Promise; + burrow: (options: AbilityOptions) => Promise; addLabel: (name: string, value: any) => Map; hasLabel: (name: string) => boolean; + getLife: () => number; getLabel: (name: string) => any; removeLabel: (name: string) => boolean; } @@ -173,7 +185,7 @@ interface UnitResource { getRangedCombatUnits(): Unit[]; getAll: (filter?: (number | UnitFilter)) => Unit[]; getAlive: (filter?: (number | UnitFilter)) => Unit[]; - getById: (unitTypeId: number, filter?: UnitFilter) => Unit[]; + getById: (unitTypeId: (number | number[]), filter?: UnitFilter) => Unit[]; getByTag(unitTags: string): Unit; getByTag(unitTags: string[]): Unit[]; getClosest(pos: Point2D, units: Unit[], n?: number): Unit[]; @@ -584,5 +596,6 @@ interface Engine { dispatch: () => Promise; systems: SystemWrapper[]; firstRun: () => Promise + getWorld: () => World onGameEnd: (results: SC2APIProtocol.PlayerResult[]) => GameResult; } diff --git a/package.json b/package.json index ef12b14..dfc707a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-sc2/core", - "version": "0.8.2", + "version": "0.9.0-beta.1", "description": "A lightweight node.js framework to facilitate writing agents (or 'bots') for Starcraft II in JavaScript.", "main": "sc2.js", "typings": "sc2.d.ts", @@ -20,10 +20,11 @@ "eslint-config-problems": "^1.1.0" }, "dependencies": { - "@node-sc2/proto": "^0.5.7", + "@node-sc2/proto": "^0.6.0-alpha.0", + "array.prototype.flat": "^1.2.3", "bluebird": "^3.5.3", "bresenham": "0.0.4", - "chalk": "^2.4.1", + "chalk": "^3.0.0", "convert-hrtime": "^2.0.0", "create-error": "^0.3.1", "debug": "^4.1.0", @@ -38,6 +39,7 @@ "protobufjs": "^6.8.8", "range": "0.0.3", "shortid": "^2.2.14", + "uint1array": "^1.1.1", "yargs": "^12.0.5" } } diff --git a/resources/actions.js b/resources/actions.js index 1f13511..72535d6 100644 --- a/resources/actions.js +++ b/resources/actions.js @@ -27,13 +27,17 @@ function getRandomN(arr, n) { * @returns {ActionManager} * @param {World} world */ -function createActionManager(world) { - const protoClient = createTransport(); +function createActionManager(world, client) { + const protoClient = client || createTransport(); return { _client: protoClient, async do(abilityId, ts, opts = {}) { - const tags = Array.isArray(ts) ? ts : [ts]; + let tags = Array.isArray(ts) ? ts : [ts]; + + if (tags[0].tag) { + tags = tags.map(u => u.tag); + } /** @type {SC2APIProtocol.ActionRawUnitCommand} */ const doAction = { @@ -57,7 +61,9 @@ function createActionManager(world) { return res; }); }, - async smart(units, pos, queue = false) { + async smart(us, pos, queue = false) { + const units = Array.isArray(us) ? us : [us]; + const smartTo = { abilityId: Ability.SMART, unitTags: units.map(u => u.tag), diff --git a/resources/debug.js b/resources/debug.js index cb3b6ce..64c2da6 100644 --- a/resources/debug.js +++ b/resources/debug.js @@ -1,6 +1,7 @@ "use strict"; // eslint-disable-next-line +const debugWidget = require('debug')('sc2:DebugWidget'); const Color = require('../constants/color'); const getRandom = require('../utils/get-random'); const { cellsInFootprint } = require('../utils/geometry/plane'); @@ -22,8 +23,10 @@ function createDebugger(world) { updateScreen() { const { actions: { _client } } = world.resources.get(); - this._drawDebugWidget(widgetData); - + if (debugWidget.enabled) { + this._drawDebugWidget(widgetData); + } + const debugCommands = Object.values(commands).reduce((commands, command) => { return commands.concat(command); }, []); diff --git a/resources/map.js b/resources/map.js index 5d11fae..b1150bc 100644 --- a/resources/map.js +++ b/resources/map.js @@ -1,9 +1,10 @@ 'use strict'; const PF = require('pathfinding'); -const debugWeights = require('debug')('sc2:silly:DebugWeights'); +const debugWeights = require('debug')('sc2:DebugWeights'); +const debugPathable = require('debug')('sc2:DebugPathable'); const { enums: { Alliance }, Color } = require('../constants'); -const { add, distance, avgPoints, closestPoint } = require('../utils/geometry/point'); +const { add, distance, avgPoints, closestPoint, areEqual } = require('../utils/geometry/point'); const { gridsInCircle } = require('../utils/geometry/angle'); const { cellsInFootprint } = require('../utils/geometry/plane'); const { gasMineTypes } = require('../constants/groups'); @@ -76,6 +77,10 @@ function createMapManager(world) { const point = createPoint2D(p); return !!this._mapState.visibility[point.y][point.x]; }, + isRamp(p) { + const point = createPoint2D(p); + return !!this._ramps.find(c => areEqual(point, c)); + }, hasCreep(p) { const point = createPoint2D(p); return !!this._mapState.creep[point.y][point.x]; @@ -136,6 +141,11 @@ function createMapManager(world) { closestPathable(point, r = 3) { const allPathable = gridsInCircle(point, r, { normalize: true }) .filter(p => this.isPathable(p)); + + if (allPathable.length <= 0) { + throw new Error(`No pathable points within ${r} radius of point ${point}`); + } + return closestPoint(point, allPathable); }, /** @@ -153,7 +163,9 @@ function createMapManager(world) { distance: this.path(startPoint, add(expansion.townhallPosition, 3)).length, }; }) - .filter(exp => exp.distance > 0) + .filter((exp) => { + return exp.distance > 0; + }) .sort((a, b) => a.distance - b.distance)[0]; return expansionOrder[closestIndex]; @@ -185,6 +197,8 @@ function createMapManager(world) { return this._grids.height[point.y][point.x] / 10; }, getCombatRally() { + if (this.isCustom()) return this.getCenter(); + const numOfBases = this.getOccupiedExpansions().length; if (combatRally && combatRally.numOfBases === numOfBases ) return combatRally.pos; @@ -226,6 +240,22 @@ function createMapManager(world) { setGrids(newGrids) { // merging allows to update partial grids, or individual this._grids = { ...this._grids, ...newGrids }; + + if (debugPathable.enabled) { + world.resources.get().debug.setDrawCells('debugPathable', this._grids.pathing.reduce((cells, row, y) => { + row.forEach((node, x) => { + if (this.isPathable({x, y})) { + cells.push({ + pos: {x, y}, + text: `p-able`, + color: Color.LIME_GREEN, + }); + } + }); + + return cells; + }, [])); + } }, setSize(mapSize) { this._mapSize = mapSize; @@ -258,6 +288,9 @@ function createMapManager(world) { if (graph) { this._graph = graph; } else { + // @WIP: uncomment for maybe useful debugging? + // console.log(this._mapSize.x, this._mapSize.y, this._grids.pathing.length, this._grids.pathing[0].length) + // console.log(this._grids.pathing, this._grids.pathing[0]) const newGraph = new PF.Grid(this._mapSize.x, this._mapSize.y, this._grids.pathing); newGraph.nodes.forEach((row) => { row.forEach((node) => { diff --git a/resources/units.js b/resources/units.js index aa985ed..c5497a3 100644 --- a/resources/units.js +++ b/resources/units.js @@ -96,6 +96,8 @@ function createUnits(world) { } return filterArr(theUnits, filter); + } else if (typeof filter === 'function') { + return this.getAll().filter(filter); } else if (typeof filter === 'number') { return Array.from(this._units[filter].values()); } else { @@ -109,8 +111,10 @@ function createUnits(world) { getAlive(filter) { return this.getAll(filter).filter(u => u.isCurrent()); }, - getById(unitTypeId, filter = { alliance: Alliance.SELF }) { - return this.getAlive(filter).filter(u => u.unitType === unitTypeId); + getById(unitTypeIds, filter = { alliance: Alliance.SELF }) { + const typeIds = Array.isArray(unitTypeIds) ? unitTypeIds : [unitTypeIds]; + + return this.getAlive(filter).filter(u => typeIds.includes(u.unitType)); }, // @ts-ignore overloads are hard apparently getByTag(unitTags) { @@ -124,7 +128,7 @@ function createUnits(world) { }, getCombatUnits(filter = Alliance.SELF) { return this.getAlive(filter) - .filter(u => combatTypes.includes(u.unitType)); + .filter(u => u.isCombatUnit()); }, getRangedCombatUnits() { return this.getCombatUnits() @@ -142,6 +146,9 @@ function createUnits(world) { return workers.filter(u => !u.labels.has('command')); } }, + getIdle() { + return this.getAlive(Alliance.SELF).filter(u => u.noQueue); + }, getIdleWorkers() { return this.getWorkers().filter(w => w.noQueue); }, diff --git a/systems/builder-plugin.js b/systems/builder-plugin.js index f2ad560..0d66aa3 100644 --- a/systems/builder-plugin.js +++ b/systems/builder-plugin.js @@ -1,6 +1,7 @@ 'use strict'; const debugBuild = require('debug')('sc2:debug:build'); +const debugDraw = require('debug')('sc2:DrawDebug'); const debugBuildSilly = require('debug')('sc2:silly:build'); const { distance, distanceX, distanceY } = require('../utils/geometry/point'); const getRandom = require('../utils/get-random'); @@ -172,12 +173,15 @@ function builderPlugin(system) { } if (buildTask.touched === false) { - debugBuild(`starting new build task: %o`, buildTask); + const taskName = buildTask.type === 'ability' ? AbilityId[buildTask.id] + : buildTask.type === 'upgrade' ? UpgradeId[buildTask.id] + : UnitTypeId[buildTask.id]; + debugBuild(`starting new build task: ${buildTask.type} ${taskName}`); buildTask.started = gameLoop; buildTask.touched = true; } - if (debugBuild.enabled) { + if (debugDraw.enabled) { world.resources.get().debug.setDrawTextScreen('buildOrder', [{ pos: { x: 0.85, y: 0.1 }, text: `Build:\n\n${this.state[buildSym].map((buildTask, i) => { diff --git a/systems/frame.js b/systems/frame.js index d1933a2..70309dc 100644 --- a/systems/frame.js +++ b/systems/frame.js @@ -22,6 +22,7 @@ const frameSystem = { ]); frame._gameInfo = gameInfo; + frame._gameLoop = responseObservation.observation.gameLoop; frame._observation = responseObservation.observation; }, async onStep({ resources }) { diff --git a/systems/map.js b/systems/map.js index a57286c..354686f 100644 --- a/systems/map.js +++ b/systems/map.js @@ -5,6 +5,7 @@ const debugDrawMap = require('debug')('sc2:DrawDebugMap'); const debugDrawWalls = require('debug')('sc2:DrawDebugWalls'); const debugDebug = require('debug')('sc2:debug:MapSystem'); const silly = require('debug')('sc2:silly:MapSystem'); +const flat = require('array.prototype.flat'); const hullJs = require('hull.js'); const bresenham = require('bresenham'); const createSystem = require('./index'); @@ -13,7 +14,7 @@ const { createClusters: findClusters } = require('../utils/map/cluster'); const createExpansion = require('../engine/create-expansion'); const floodFill = require('../utils/map/flood'); const { distanceAAShapeAndPoint } = require('../utils/geometry/plane'); -const { distance, avgPoints, areEqual, closestPoint, createPoint2D} = require('../utils/geometry/point'); +const { distance, avgPoints, areEqual, closestPoint, createPoint2D, getNeighbors } = require('../utils/geometry/point'); const { frontOfGrid } = require('../utils/map/region'); const { gridsInCircle } = require('../utils/geometry/angle'); const { MineralField, VespeneGeyser, Townhall, getFootprint } = require('../utils/geometry/units'); @@ -21,7 +22,7 @@ const { cellsInFootprint } = require('../utils/geometry/plane'); const { Alliance } = require('../constants/enums'); const { MapDecompositionError } = require('../engine/errors'); const Color = require('../constants/color'); -const { UnitTypeId } = require('../constants/'); +const { UnitType, UnitTypeId } = require('../constants'); const { vespeneGeyserTypes, unbuildablePlateTypes, mineralFieldTypes } = require('../constants/groups'); /** @@ -134,109 +135,151 @@ function calculateRamps(minimap) { * @param {World} world */ function calculateWall(world, expansion) { - const { map, debug } = world.resources.get(); + const { map, units, debug } = world.resources.get(); + const { placement } = map.getGrids(); const hull = expansion.areas.hull; const foeHull = frontOfGrid(world, hull); // debug.setDrawCells('fonHull', foeHull.map(fh => ({ pos: fh })), { size: 0.50, color: Color.YELLOW, cube: true, persistText: true }); - const { pathing, miniMap } = map.getGrids(); - - const decomp = foeHull.reduce((decomp, { x, y }) => { - const neighbors = [ - { y: y - 1, x}, - { y, x: x - 1}, - { y, x: x + 1}, - { y: y + 1, x}, - ]; - - const diagNeighbors = [ - { y: y - 1, x: x - 1}, - { y: y - 1, x: x + 1}, - { y: y + 1, x: x - 1}, - { y: y + 1, x: x + 1}, - ]; - - const deadNeighbors = neighbors.filter(({ x, y }) => pathing[y][x] === 1); - const deadDiagNeighbors = diagNeighbors.filter(({ x, y }) => pathing[y][x] === 1); - - if ((deadNeighbors.length <= 0) && (deadDiagNeighbors.length <= 0)) { - if (neighbors.filter(({ x, y }) => miniMap[y][x] === 114).length <= 0) { - decomp.liveHull.push({ x, y }); - } else { - decomp.liveRamp.push({ x, y }); - } - } - - decomp.deadHull = decomp.deadHull.concat(deadNeighbors); - return decomp; - }, { deadHull: [], liveHull: [], liveRamp: [] }); - const live = decomp.liveHull.length > 0 ? decomp.liveHull : decomp.liveRamp; + /** + * @FIXME: this is duplicated logic and can prolly be consolidated + */ - // debug.setDrawCells(`liveHull-${Math.floor(expansion.townhallPosition.x)}`, live.map(fh => ({ pos: fh })), { size: 0.5, color: Color.LIME_GREEN, cube: true }); - // debug.setDrawCells(`deadHull-${Math.floor(expansion.townhallPosition.x)}`, decomp.deadHull.map(fh => ({ pos: fh })), { size: 0.5, color: Color.RED, cube: true }); - const deadHullClusters = decomp.deadHull.reduce((clusters, dh) => { - if (clusters.length <= 0) { - const newCluster = [dh]; - newCluster.centroid = dh; - clusters.push(newCluster); + const plates = units.getByType(unbuildablePlateTypes); + const cellsBlockedByUnbuildableUnit = flat( + plates.map(plate => { + const footprint = getFootprint(plate.unitType); + return cellsInFootprint(createPoint2D(plate.pos), footprint); + }) + ); + // debug.setDrawCells('blockedCells', cellsBlockedByUnbuildableUnit.map(fh => ({ pos: fh })), { size: 0.50, color: Color.YELLOW, cube: true, persistText: true }); + + /** + * + * @param {Boolean} [rampDesired] + * @returns {Array} + */ + function findAllPossibleWalls(rampDesired = true) { + const rampTest = point => rampDesired ? map.isRamp(point) : !map.isRamp(point); + + const { deadHull, liveHull } = foeHull.reduce((decomp, point) => { + const neighbors = getNeighbors(point, false); + const diagNeighbors = getNeighbors(point, true, true); + + const deadNeighbors = neighbors.filter(point => !map.isPathable(point)); + const deadDiagNeighbors = diagNeighbors.filter(point => !map.isPathable(point)); + + if ((deadNeighbors.length <= 0) && (deadDiagNeighbors.length <= 0)) { + if (neighbors.some(rampTest)) { + if ( + (!neighbors.some(point => cellsBlockedByUnbuildableUnit.some(cell => areEqual(cell,point)))) + && (!diagNeighbors.some(point => cellsBlockedByUnbuildableUnit.some(cell => areEqual(cell,point)))) + ) { + decomp.liveHull.push(point); + } else { + // the ether... + } + } else { + // the ether... + } + } + + decomp.deadHull = decomp.deadHull.concat( + deadNeighbors.filter((neighborCell) => { + const neighborsNeighbors = getNeighbors(neighborCell); + return neighborsNeighbors.some(rampTest); + }) + ); + + return decomp; + }, { deadHull: [], liveHull: [] }); + + // debug.setDrawCells(`liveHull-${Math.floor(expansion.townhallPosition.x)}`, live.map(fh => ({ pos: fh })), { size: 0.75, color: Color.LIME_GREEN, cube: true }); + // debug.setDrawCells(`deadHull-${Math.floor(expansion.townhallPosition.x)}`, decomp.deadHull.map(fh => ({ pos: fh })), { size: 0.75, color: Color.RED, cube: true }); + + const deadHullClusters = deadHull.reduce((clusters, deadHullCell) => { + if (clusters.length <= 0) { + const newCluster = [deadHullCell]; + newCluster.centroid = deadHullCell; + clusters.push(newCluster); + return clusters; + } + + const clusterIndex = clusters.findIndex(cluster => distance(cluster.centroid, deadHullCell) < (liveHull.length - 1)); + if (clusterIndex !== -1) { + clusters[clusterIndex].push(deadHullCell); + clusters[clusterIndex].centroid = avgPoints(clusters[clusterIndex]); + } else { + const newCluster = [deadHullCell]; + newCluster.centroid = deadHullCell; + clusters.push(newCluster); + } + return clusters; - } - - const clusterIndex = clusters.findIndex(cluster => distance(cluster.centroid, dh) < (live.length - 1)); - if (clusterIndex !== -1) { - clusters[clusterIndex].push(dh); - clusters[clusterIndex].centroid = avgPoints(clusters[clusterIndex]); - } else { - const newCluster = [dh]; - newCluster.centroid = dh; - clusters.push(newCluster); - } - - return clusters; - }, []); + }, []); + + // debug.setDrawTextWorld(`liveHullLength-${Math.floor(expansion.townhallPosition.x)}`, [{ pos: createPoint2D(avgPoints(live)), text: `${live.length}` }]); + + // deadHullClusters.forEach((cluster, i) => { + // debug.setDrawCells(`dhcluster-${Math.floor(expansion.townhallPosition.x)}-${i}`, cluster.map(fh => ({ pos: fh })), { size: 0.8, cube: true }); + // }); + + return deadHullClusters.reduce((walls, cluster, i) => { + const possibleWalls = flat( + cluster.map(cell => { + const notOwnClusters = deadHullClusters.filter((c, j) => j !== i); + return notOwnClusters.map(jcluster => { + const closestCell = closestPoint(cell, jcluster); + const line = []; + bresenham(cell.x, cell.y, closestCell.x, closestCell.y, (x, y) => line.push({x, y})); + return line; + }); + }) + ); + + return walls.concat(possibleWalls); + }, []) + .map(wall => { + const first = wall[0]; + const last = wall[wall.length -1]; + + const newGraph = map.newGraph(placement.map(row => row.map(cell => cell === 0 ? 1 : 0))); + + newGraph.setWalkableAt(first.x, first.y, true); + newGraph.setWalkableAt(last.x, last.y, true); + return map.path(wall[0], wall[wall.length -1], { graph: newGraph, diagonal: true }) + .map(([x, y]) => ({ x, y })); + }) + .map(wall => { + // debug.setDrawCells(`middleWallCalc-${Math.floor(expansion.townhallPosition.x)}`, wall.map(fh => ({ pos: fh })), { size: 1, color: Color.HOT_PINK, cube: false }); + return wall.filter(cell => map.isPlaceable(cell)); + }) + .sort((a, b) => a.length - b.length) + .filter(wall => wall.length >= liveHull.length) + .filter (wall => distance(avgPoints(wall), avgPoints(liveHull)) <= liveHull.length) + } - // debug.setDrawTextWorld(`liveHullLength-${Math.floor(expansion.townhallPosition.x)}`, [{ pos: createPoint2D(avgPoints(live)), text: `${live.length}` }]); + // try first assuming we have a nat ramp + let allPossibleWalls = findAllPossibleWalls(true); - // deadHullClusters.forEach((cluster, i) => { - // debug.setDrawCells(`dhcluster-${Math.floor(expansion.townhallPosition.x)}-${i}`, cluster.map(fh => ({ pos: fh })), { size: 0.8, cube: true }); - // }); + if (!allPossibleWalls[0]) { + // now try assuming there is no ramp + allPossibleWalls = findAllPossibleWalls(false); + } - const allPossibleWalls = deadHullClusters.reduce((walls, cluster, i) => { - const possibleWalls = cluster.map(cell => { - const notOwnClusters = deadHullClusters.filter((c, j) => j !== i); - return notOwnClusters.map(jcluster => { - const closestCell = closestPoint(cell, jcluster); - const line = []; - bresenham(cell.x, cell.y, closestCell.x, closestCell.y, (x, y) => line.push({x, y})); - return line; - }); - }).reduce((walls, wall) => walls.concat(wall), []); - - return walls.concat(possibleWalls); - }, []) - .map(wall => { - const first = wall[0]; - const last = wall[wall.length -1]; - - const newGraph = map.newGraph(map._grids.placement.map(row => row.map(cell => cell === 0 ? 1 : 0))); - - newGraph.setWalkableAt(first.x, first.y, true); - newGraph.setWalkableAt(last.x, last.y, true); - return map.path(wall[0], wall[wall.length -1], { graph: newGraph, diagonal: true }) - .map(([x, y]) => ({ x, y })); - }) - .map(wall => wall.filter(cell => map.isPlaceable(cell))) - .sort((a, b) => a.length - b.length) - .filter(wall => wall.length >= (live.length)) - .filter (wall => distance(avgPoints(wall), avgPoints(live)) <= live.length) - .filter((wall, i, arr) => wall.length === arr[0].length); - + /** + * @FIXME: we just sort of assume we always found a wall here... should be some contingency + */ const [shortestWall] = allPossibleWalls; - + if (debugDrawWalls.enabled) { - debug.setDrawCells(`dhwall`, shortestWall.map(fh => ({ pos: fh })), { size: 0.8, color: Color.YELLOW, cube: true, persistText: true, }); + debug.setDrawCells( + `dhwall`, + shortestWall.map(fh => ({ pos: fh })), + { size: 0.9, color: Color.FUCHSIA, cube: true, persistText: true } + ); } expansion.areas.wall = shortestWall; @@ -269,7 +312,7 @@ function calculateExpansions(world) { // @TODO: handle the case of more than 1 enemy location const pathingEL = createPoint2D(map.getLocations().enemy[0]); pathingEL.x = pathingEL.x + 3; - + let expansions; try { expansions = findClusters([...mineralFields, ...vespeneGeysers]) @@ -277,7 +320,6 @@ function calculateExpansions(world) { .map((expansion) => { const start = { ...expansion.townhallPosition }; start.x = start.x + 3; - const paths = { pathFromMain: map.path(start, pathingSL), pathFromEnemy: map.path(start, pathingEL), @@ -311,6 +353,9 @@ function calculateExpansions(world) { const mapSystem = { name: 'MapSystem', type: 'engine', + defaultOptions: { + stepIncrement: 1, + }, async onGameStart(world) { const { units, frame, map, debug } = world.resources.get(); const { startRaw } = frame.getGameInfo(); @@ -331,6 +376,19 @@ const mapSystem = { map.setRamps(calculateRamps(map._grids.miniMap).map((rPoint) => { return Object.assign(rPoint, { z: map.getHeight(rPoint) }); })); + + map.setRamps(map._ramps.filter((possibleRamp) => { + const { x, y } = possibleRamp; + // this is necessary because some maps have random tiles that aren't placeable but are pathable + const neighbors = [ + { y: y - 1, x}, + { y, x: x - 1}, + { y, x: x + 1}, + { y: y + 1, x}, + ]; + + return neighbors.some(point => map.isRamp(point)); + })); // mark cells under geysers, destructable plates, and mineral fields as initially unplaceable const geysers = units.getGasGeysers(); @@ -459,7 +517,7 @@ const mapSystem = { row.forEach((placeable, x) => { cells.push({ pos: { x, y }, - size: 0.2, + size: 0.5, color: placeable ? Color.LIME_GREEN : Color.RED, }); }); @@ -512,7 +570,11 @@ const mapSystem = { async onUnitDestroyed({ resources }, deadUnit) { const { map } = resources.get(); - if (deadUnit.isStructure()) { + if (deadUnit.isStructure() && !deadUnit.isGasMine()) { + if (deadUnit.is(UnitType.PYLON)) { + // @WIP: unsure what this was debugging for, but leaving commented for posterity until i figure it out + // console.log(resources.get().frame.getGameLoop(), `pylon destroyed (or canceled?) progress is/was: ${deadUnit.buildProgress}`); + } const { pos } = deadUnit; const footprint = getFootprint(deadUnit.unitType); diff --git a/systems/unit-plugin.js b/systems/unit-plugin.js index 5804783..e2e2477 100644 --- a/systems/unit-plugin.js +++ b/systems/unit-plugin.js @@ -29,9 +29,12 @@ function unitPlugin(system) { const systemUnits = unitsFilter.reduce((sysUnits, unitType) =>{ sysUnits[unitType] = units.getById(unitType) .filter(u => u.isFinished()) - .filter(systemUnit => { + .filter((systemUnit) => { const unitTag = systemUnit.tag; - return Object.values(labeledUnits).every(labelUnit => labelUnit.tag !== unitTag); + const labeledTags = Object.values(labeledUnits) + .reduce((pool, arr) => pool.concat(arr), []) + .map(lu => lu.tag); + return !labeledTags.includes(unitTag); }); return sysUnits; }, {}); @@ -55,25 +58,21 @@ function unitPlugin(system) { const onIdleFunctions = system.idleFunctions || {}; system.onUnitIdle = async function(world, unit) { - labelsFilter.forEach((label) => { + const foundLabelFn = labelsFilter.find((label) => { if (unit.hasLabel(label)) { - return onIdleFunctions[label] ? - reflect(onIdleFunctions[label], world, unit) : - onIdleFunctions.labeled ? - reflect(onIdleFunctions.labeled, world, unit, label) : - onIdleFunctions[unit.unitType] ? - reflect(onIdleFunctions[unit.unitType], world, unit) : - undefined; + return !!onIdleFunctions[label]; } }); - if (unitsFilter.includes(unit.unitType)) { - if (onIdleFunctions[unit.unitType]) { - return reflect(onIdleFunctions[unit.unitType], world, unit); - } + if (foundLabelFn) { + return reflect(onIdleFunctions[foundLabelFn], world, unit); + } else if (!unit.hasNoLabels() && onIdleFunctions.labeled) { + return reflect(onIdleFunctions.labeled, world, unit, [...unit.labels.keys()][0]); + } else if (onIdleFunctions[unit.unitType]) { + return reflect(onIdleFunctions[unit.unitType], world, unit); + } else { + return systemOnUnitIdle(world, unit); } - - return systemOnUnitIdle(world, unit); }; system.onUnitCreated = async function(world, unit) { diff --git a/systems/unit.js b/systems/unit.js index 3248dd4..2d579e4 100644 --- a/systems/unit.js +++ b/systems/unit.js @@ -62,6 +62,24 @@ const unitSystem = { ); }; + /** + * Test for a unit transforming type + * @param {SC2APIProtocol.Unit} incData + * @param {Unit} currentUnit + */ + const isTransforming = (incData, currentUnit) => { + return currentUnit.unitType !== incData.unitType; + }; + + /** + * Test for a unit finishing a burrow + * @param {SC2APIProtocol.Unit} incData + * @param {Unit} currentUnit + */ + const hasBurrowed = (incData, currentUnit) => { + return incData.isBurrowed && !currentUnit.isBurrowed; + }; + /** * Test for unit entering a skirmish * @param {SC2APIProtocol.Unit} incData @@ -108,6 +126,15 @@ const unitSystem = { data: currentUnit, type: 'all', }); + + // the unit is finished, but it's also idle + if (unitData.orders.length > 0) { + events.write({ + name: "unitIdle", + data: currentUnit, + type: 'all', + }); + } } if (isIdle(unitData, currentUnit)) { @@ -118,6 +145,22 @@ const unitSystem = { }); } + if (isTransforming(unitData, currentUnit)) { + events.write({ + name: "unitIsTransforming", + data: currentUnit, + type: 'all', + }); + } + + if (hasBurrowed(unitData, currentUnit)) { + events.write({ + name: "unitHasBurrowed", + data: currentUnit, + type: 'all', + }); + } + if (hasEngaged(unitData, currentUnit)) { events.write({ name: "unitHasEngaged", @@ -165,6 +208,15 @@ const unitSystem = { data: newUnit, type: 'all', }); + + // if it's unrallied, then it's also idle + if (unitData.orders.length > 0) { + events.write({ + name: "unitIdle", + data: newUnit, + type: 'all', + }); + } } else if (unitData.alliance === Alliance.ENEMY) { events.write({ name: "enemyFirstSeen", diff --git a/utils/geometry/point.js b/utils/geometry/point.js index ebf472e..399ae54 100644 --- a/utils/geometry/point.js +++ b/utils/geometry/point.js @@ -179,35 +179,49 @@ const createPoint2D = ({x, y}) => ({ x: Math.floor(x), y: Math.floor(y) }); */ const createPoint = ({x, y, z}) => ({ x: Math.floor(x), y: Math.floor(y), z: Math.floor(z) }); + /** * - * @param {Point2D} point - * @param {boolean} includeDiagonal + * @param {Point2D} param0 + * @returns {Point2D[]} */ -function getNeighbors(point, includeDiagonal = true) { - const normal = createPoint2D(point); +const getAdjacents = ({ x, y }) => [ + { y: y - 1, x}, + { y, x: x - 1}, + { y, x: x + 1}, + { y: y + 1, x}, +]; - const getAdjacents = ({ x, y }) => [ - { y: y - 1, x}, - { y, x: x - 1}, - { y, x: x + 1}, - { y: y + 1, x}, - ]; +/** + * + * @param {Point2D} param0 + * @returns {Point2D[]} + */ +const getDiagonals = ({ x, y }) => [ + { y: y - 1, x: x - 1}, + { y: y - 1, x: x + 1}, + { y: y + 1, x: x - 1}, + { y: y + 1, x: x + 1}, +]; - const getDiags = ({ x, y }) => [ - { y: y - 1, x: x - 1}, - { y: y - 1, x: x + 1}, - { y: y + 1, x: x - 1}, - { y: y + 1, x: x + 1}, - ]; +/** + * + * @param {Point2D} point + * @param {boolean} includeDiagonals + * @param {boolean} diagonalsOnly + */ +function getNeighbors(point, includeDiagonals = true, diagonalsOnly = false) { + const normal = createPoint2D(point); let neighbors = getAdjacents(normal); - if (includeDiagonal) { - neighbors = neighbors.concat(getDiags(normal)); + if (includeDiagonals && diagonalsOnly) { + return getDiagonals(normal); + } else if (includeDiagonals) { + return neighbors.concat(getDiagonals(normal)); + } else { + return neighbors; } - - return neighbors; } module.exports = { diff --git a/utils/map/flood.js b/utils/map/flood.js index d426be8..aba3839 100644 --- a/utils/map/flood.js +++ b/utils/map/flood.js @@ -14,8 +14,8 @@ const { distance } = require('../geometry/point'); */ function floodFill(mapData, x, y, oldVal, newVal, maxReach = false, filled = [], startPos) { - const mapWidth = mapData.length; - const mapHeight = mapData[0].length; + const mapHeight = mapData.length; + const mapWidth = mapData[0].length; if (startPos == null) { startPos = { x, y }; @@ -29,6 +29,7 @@ function floodFill(mapData, x, y, oldVal, newVal, maxReach = false, filled = [], return filled; } + // console.log(`in floodFill - x: ${x}, y: ${y}, maxReach: ${maxReach}`); if (mapData[y][x] !== oldVal || filled.some(pix => pix.x === x && pix.y === y)) { return filled; } diff --git a/utils/map/grid.js b/utils/map/grid.js index d788cc2..224a85a 100644 --- a/utils/map/grid.js +++ b/utils/map/grid.js @@ -1,23 +1,42 @@ 'use strict'; const chalk = require('chalk'); +// const Uint1Array = require('uint1array').default; - /** - * @param {Grid2D} grid - */ -function debugGrid(grid) { - const displayGrid = grid.slice(); - displayGrid.reverse().forEach((row) => { +/** + * @param {Grid2D} grid + */ +function debugGrid(grid, playable, height = false) { + const pointInRect = ({p0, p1}, x, y) => ( + (x > p0.x && x < p1.x) && (y > p0.y && y < p1.y) + ); + + const displayGrid = grid.slice().reverse(); + + displayGrid.forEach((row, x) => { console.log( - row.map((pixel) => { + [...row].map((pixel, y) => { + if (height === true) { + return chalk.rgb(0, pixel, 0)('X'); + } switch(pixel) { case 0: - return ' '; + if (!pointInRect(playable, x, y)) { + return chalk.bgRed` `; + } else { + return chalk.bgGreen` `; + } case 1: return '░'; - case 104: + case 66: + return chalk.bgBlue` `; + case 114: // @ts-ignore - return chalk.bgGreen`░`; + return chalk.bgCyanBright`░`; + case 104: + + // ??? + return; case 72: // @ts-ignore return chalk.bgRed`░`; @@ -29,32 +48,99 @@ function debugGrid(grid) { }); } +/** + * @param {Uint8Array} buffer + * @param {number} i + * @param {number} bit + */ +function readSingleBit(buffer, i, bit){ + return (buffer[i] >> bit) % 2; +} + +// /** +// * @param {SC2APIProtocol.ImageData} imageData +// * @param {number} width +// * @returns {Grid2D} +// */ +// function consumeImageData(imageData, width, height = Infinity) { +// if (!width) { +// throw new Error('Map width needed to digest raw grids!'); +// } + +// const BYTE_OFFSET = imageData.data.byteOffset; +// const BYTE_LENGTH = imageData.data.byteLength; +// const BITS_PER_PIXEL = imageData.bitsPerPixel; +// const WIDTH_IN_BYTES = BITS_PER_PIXEL === 1 +// ? width * 0.125 +// : width; + +// /* new fast code, 0.02ms per digestion */ +// const arrayBuffer = imageData.data.buffer; + +// const View = BITS_PER_PIXEL === 1 +// ? Uint1Array +// : Uint8Array; + +// const result = []; +// let i = 0; + +// while (i < BYTE_LENGTH) { +// if (result.length >= height) { +// break; +// } + +// if (arrayBuffer.byteLength < (BYTE_OFFSET + i )) { +// break; +// } + +// result.push(new View(arrayBuffer, i + BYTE_OFFSET, WIDTH_IN_BYTES)); +// i += WIDTH_IN_BYTES; +// } + +// return result.reverse(); +// } + /** * @param {SC2APIProtocol.ImageData} imageData * @param {number} width * @returns {Grid2D} */ -function consumeImageData(imageData, width) { - /* new fast code, 0.02ms per digestion */ - const arrayBuffer = imageData.data.buffer; +function consumeImageData(imageData, width, height = Infinity) { + if (!width) { + throw new Error('Map width needed to digest raw grids!'); + } - const result = []; - let i = 0; - - while (i < imageData.data.byteLength) { - result.push(new Uint8Array(arrayBuffer, i + imageData.data.byteOffset, width)); - i += width; + let data = imageData.data.slice(); + const BITS_PER_PIXEL = imageData.bitsPerPixel; + + if (BITS_PER_PIXEL === 1 ) { + data = data.reduce((pixels, byte) => { + pixels.push( + (byte >> 7) & 1, + (byte >> 6) & 1, + (byte >> 5) & 1, + (byte >> 4) & 1, + (byte >> 3) & 1, + (byte >> 2) & 1, + (byte >> 1) & 1, + (byte >> 0) & 1 + ); + + return pixels; + }, []); } - - return result.reverse(); - /* old slow code, ~2ms per digestion */ - // const gridarr = [...imageData.data]; + const result = []; - // const grid2d = []; - // while(gridarr.length) grid2d.push(gridarr.splice(0, width)); + while (data.length > 0) { + if (result.length > height) { + break; + } - // return grid2d; + result.push(data.splice(0, width)); + } + + return result; } /** @@ -64,54 +150,49 @@ function consumeImageData(imageData, width) { */ function consumeRawGrids(raw) { const { mapSize, placementGrid: plGrid, pathingGrid: paGrid, terrainHeight: teGrid } = raw; - const width = mapSize.x; + const { x, y } = mapSize; + + const placementGrid2D = consumeImageData(plGrid, x, y); + // debugGrid(placementGrid2D, raw.playableArea); - const placementGrid2D = consumeImageData(plGrid, width); - const pathingGrid2D = consumeImageData(paGrid, width); - const heightGrid2D = consumeImageData(teGrid, width); + const pathingGrid2D = consumeImageData(paGrid, x, y).map(row => row.map(cell => cell === 0 ? 1 : 0)); + // debugGrid(pathingGrid2D, raw.playableArea); + const heightGrid2D = consumeImageData(teGrid, x, y); + const height = heightGrid2D.map((row) => { return row.map(tile => { /** * functional approximation just isn't good enough here... * so unless we store a float, this is the other option - */ - const approx = Math.round((-100 + 200 * tile / 255)) * 10; - return Math.ceil(approx / 5) * 5; + const approx = Math.ceil( + Math.round(((tile - 127) / 8) * 10) / 5 + ) * 5; + if (approx < 0) return 0; + else return approx; }); }); - const placement = placementGrid2D.map((row) => { - return row.map(pixel => { - return pixel === 255 ? 1 : 0; - }); - }); - - //debugGrid(placement); - - const pathing = pathingGrid2D.map((row) => { - return row.map(pixel => { - return pixel === 255 ? 1 : 0; + const miniMap = placementGrid2D.map((row, y) => { + return row.map((pixel, x) => { + if (pixel === 1 && pathingGrid2D[y][x] === 0) { + return 66; + } else if (pixel === 0 && pathingGrid2D[y][x] === 0) { + return 114; + } else { + return pixel; + } }); }); - //debugGrid(pathing); + // debugGrid(miniMap, raw.playableArea); return { height, - placement, - pathing, - miniMap: placement.map((row, y) => { - return row.map((pixel, x) => { - if (pixel === 1 && pathing[y][x] === 1) { - return 66; - } else if (pixel === 0 && pathing[y][x] === 0) { - return 114; - } else { - return pixel; - } - }); - }), + miniMap, + placement: placementGrid2D, + pathing: pathingGrid2D, }; }