1
0
forked from mirrors/0ad

Petra: rewrite of the dock placement. It should be more robust and compatible with the new pathfinder now

This was SVN commit r16457.
This commit is contained in:
mimo
2015-03-22 22:00:59 +00:00
parent 64adec79da
commit e35970b700
8 changed files with 416 additions and 239 deletions
@@ -388,7 +388,7 @@ m.Map.prototype.add = function(map)
}
};
m.Map.prototype.findBestTile = function(radius, obstructionTiles)
m.Map.prototype.findBestTile = function(radius, obstruction)
{
// Find the best non-obstructed tile
let bestIdx = 0;
@@ -398,8 +398,8 @@ m.Map.prototype.findBestTile = function(radius, obstructionTiles)
let v = this.map[i];
if (v > bestVal)
{
var j = API3.getMaxMapIndex(i, this, obstructionTiles);
if (obstructionTiles.map[j] <= radius)
let j = this.getNonObstructedTile(i, radius, obstruction);
if (j < 0)
continue;
bestVal = v;
bestIdx = j;
@@ -409,6 +409,94 @@ m.Map.prototype.findBestTile = function(radius, obstructionTiles)
return [bestIdx, bestVal];
};
// return any non obstructed (small) tile inside the (big) tile i from obstruction map
m.Map.prototype.getNonObstructedTile = function(i, radius, obstruction)
{
let ratio = this.cellSize / obstruction.cellSize;
let ix = (i % this.width) * ratio;
let iy = Math.floor(i / this.width) * ratio;
let w = obstruction.width;
for (let kx = ix; kx < ix + ratio; ++kx)
{
if (kx < radius || kx >= w - radius)
continue;
for (let ky = iy; ky < iy + ratio; ++ky)
{
if (ky < radius || ky >= w - radius)
continue;
if (obstruction.isObstructedTile(kx, ky, radius))
continue;
return (kx + ky*w);
}
}
return -1;
};
// return true is the area centered on tile kx-ky and with radius is obstructed
m.Map.prototype.isObstructedTile = function(kx, ky, radius)
{
let w = this.width;
if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] == 0)
return true;
for (let dy = 0; dy <= radius; ++dy)
{
let dxmax = radius - dy;
let xp = kx + (ky + dy)*w;
let xm = kx + (ky - dy)*w;
for (let dx = -dxmax; dx <= dxmax; ++dx)
if (this.map[xp + dx] == 0 || this.map[xm + dx] == 0)
return true;
}
return false;
};
// returns the nearest obstructed point
// TODO check that the landpassmap index is the same
m.Map.prototype.findNearestObstructed = function(i, radius)
{
var w = this.width;
var ix = i % w;
var iy = Math.floor(i / w);
var n = (this.cellSize > 8) ? 1 : Math.floor(8 / this.cellSize);
for (let i = 1; i <= n; ++i)
{
let kx = ix - i;
let ky = iy + i;
for (let j = 1; j <= 8*i; ++j)
{
if (this.isObstructedTile(kx, ky, radius))
{
let akx = Math.abs(kx-ix);
let aky = Math.abs(ky-iy);
if (akx >= aky)
{
if (kx > ix)
--kx;
else
++kx;
}
if (aky >= akx)
{
if (ky > iy)
--ky;
else
++ky;
}
return (kx + w*ky);
}
if (j <= 2*i+1)
++kx;
else if (j <= 4*i+1)
--ky;
else if (j < 6*i+1)
--kx;
else
++ky;
}
}
return -1;
};
// returns the point with the lowest (but still > radius) point in the immediate vicinity
m.Map.prototype.findLowestNeighbor = function(x,y,radius)
{
@@ -31,9 +31,9 @@ m.SharedScript = function(settings)
this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal
this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement.
// Resource maps data.
// By how much to divide the resource amount for plotting (ie a tree having 200 wood is "4").
// By how much to divide the resource amount when filling the map (ie a tree having 200 wood is "4").
this.decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0};
}
};
//Return a simple object (using no classes etc) that will be serialized
//into saved games
@@ -294,7 +294,6 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
var obstructions = m.createObstructionMap(gameState, this.accessIndex, template);
obstructions.expandInfluences();
var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))).toEntityArray();
var ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
@@ -303,15 +302,15 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
var bestVal = undefined;
var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize);
var territoryMap = gameState.sharedScript.territoryMap;
var territoryMap = gameState.ai.HQ.territoryMap;
var width = territoryMap.width;
var cellSize = territoryMap.cellSize;
for (var p = 0; p < this.territoryIndices.length; ++p)
{
var j = this.territoryIndices[p];
var i = API3.getMaxMapIndex(j, territoryMap, obstructions);
if (obstructions.map[i] <= radius) // check room around
var i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0) // no room around
continue;
// we add 3 times the needed resource and once the other two (not food)
@@ -384,7 +383,7 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
if (bestVal !== undefined && total < bestVal)
continue;
bestVal = total;
bestIdx = j;
bestIdx = i;
}
if (this.Config.debug > 2)
@@ -392,9 +391,9 @@ m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
if (bestVal <= 0)
return {"quality": bestVal, "pos": [0, 0]};
var i = API3.getMaxMapIndex(bestIdx, territoryMap, obstructions);
var x = ((i % obstructions.width) + 0.5) * obstructions.cellSize;
var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return {"quality": bestVal, "pos": [x, z]};
};
@@ -552,7 +552,6 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource,
// obstruction map
var obstructions = m.createObstructionMap(gameState, 0, template);
obstructions.expandInfluences();
var ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
var dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClassesOr(["CivCentre", "Elephant"])));
@@ -588,16 +587,16 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource,
{
if (this.territoryMap.getOwnerIndex(j) != 0)
continue;
// We require that it is accessible
var index = gameState.ai.accessibility.landPassMap[j];
// with enough room around to build the cc
var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// we require that it is accessible
var index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
if (proxyAccess && nbShips === 0 && proxyAccess !== index)
continue;
// and with enough room around to build the cc
var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions);
if (obstructions.map[i] <= radius)
continue;
var norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps
// checking distance to other cc
@@ -680,7 +679,7 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource,
if (bestVal !== undefined && val < bestVal)
continue;
bestVal = val;
bestIdx = j;
bestIdx = i;
}
Engine.ProfileStop();
@@ -693,13 +692,12 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource,
// not good enough.
if (bestVal < cut)
return false;
var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions);
var x = (i % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
var index = gameState.ai.accessibility.landPassMap[i];
var index = gameState.ai.accessibility.landPassMap[bestIdx];
for (var base of this.baseManagers)
{
if (!base.anchor || base.accessIndex === index)
@@ -709,7 +707,7 @@ m.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource,
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x,z];
return [x, z];
};
// Returns the best position to build a new Civil Centre
@@ -738,7 +736,6 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template)
// obstruction map
var obstructions = m.createObstructionMap(gameState, 0, template);
obstructions.expandInfluences();
var bestIdx = undefined;
var bestVal = undefined;
@@ -753,13 +750,13 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template)
{
if (this.territoryMap.getOwnerIndex(j) != 0)
continue;
// We require that it is accessible
var index = gameState.ai.accessibility.landPassMap[j];
if (!this.landRegions[index])
// with enough room around to build the cc
var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
// and with enough room around to build the cc
var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions);
if (obstructions.map[i] <= radius)
// we require that it is accessible
var index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
continue;
// checking distances to other cc
@@ -818,7 +815,7 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template)
if (bestVal !== undefined && currentVal > bestVal)
continue;
bestVal = currentVal;
bestIdx = j;
bestIdx = i;
}
if (this.Config.debug > 1)
@@ -829,12 +826,11 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template)
if (bestVal === undefined)
return undefined;
var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions);
var x = (i % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Define a minimal number of wanted ships in the seas reaching this new base
var index = gameState.ai.accessibility.landPassMap[i];
var index = gameState.ai.accessibility.landPassMap[bestIdx];
for (var base of this.baseManagers)
{
if (!base.anchor || base.accessIndex === index)
@@ -844,7 +840,7 @@ m.HQ.prototype.findStrategicCCLocation = function(gameState, template)
this.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
return [x,z];
return [x, z];
};
// Returns the best position to build a new market: if the allies already have a market, build it as far as possible
@@ -862,7 +858,6 @@ m.HQ.prototype.findMarketLocation = function(gameState, template)
// obstruction map
var obstructions = m.createObstructionMap(gameState, 0, template);
obstructions.expandInfluences();
var bestIdx = undefined;
var bestVal = undefined;
@@ -880,8 +875,8 @@ m.HQ.prototype.findMarketLocation = function(gameState, template)
if (this.basesMap.map[j] == 0) // only in our territory
continue;
// with enough room around to build the cc
var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions);
if (obstructions.map[i] <= radius)
var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
var index = gameState.ai.accessibility.landPassMap[i];
if (!this.landRegions[index])
@@ -907,7 +902,7 @@ m.HQ.prototype.findMarketLocation = function(gameState, template)
if (bestVal !== undefined && maxDist < bestVal)
continue;
bestVal = maxDist;
bestIdx = j;
bestIdx = i;
}
if (this.Config.debug > 1)
@@ -924,9 +919,8 @@ m.HQ.prototype.findMarketLocation = function(gameState, template)
(expectedGain < 8 && (!template.hasClass("BarterMarket") || gameState.getOwnStructures().filter(API3.Filters.byClass("BarterMarket")).length > 0)))
return false;
var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions);
var x = (i % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestIdx], expectedGain];
};
@@ -953,9 +947,9 @@ m.HQ.prototype.findDefensiveLocation = function(gameState, template)
// obstruction map
var obstructions = m.createObstructionMap(gameState, 0, template);
obstructions.expandInfluences();
var bestIdx = undefined;
var bestJdx = undefined;
var bestVal = undefined;
var width = this.territoryMap.width;
var cellSize = this.territoryMap.cellSize;
@@ -979,9 +973,9 @@ m.HQ.prototype.findDefensiveLocation = function(gameState, template)
}
if (this.basesMap.map[j] == 0) // inaccessible cell
continue;
// and with enough room around to build the cc
var i = API3.getMaxMapIndex(j, this.territoryMap, obstructions);
if (obstructions.map[i] <= radius)
// with enough room around to build the cc
var i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
@@ -1037,16 +1031,16 @@ m.HQ.prototype.findDefensiveLocation = function(gameState, template)
if (bestVal !== undefined && minDist > bestVal)
continue;
bestVal = minDist;
bestIdx = j;
bestIdx = i;
bestJdx = j;
}
if (bestVal === undefined)
return undefined;
var i = API3.getMaxMapIndex(bestIdx, this.territoryMap, obstructions);
var x = (i % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestIdx]];
var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return [x, z, this.basesMap.map[bestJdx]];
};
m.HQ.prototype.buildTemple = function(gameState, queues)
@@ -8,6 +8,7 @@ m.createObstructionMap = function(gameState, accessIndex, template)
{
var passabilityMap = gameState.getMap();
var territoryMap = gameState.ai.territoryMap;
var ratio = territoryMap.cellSize / passabilityMap.cellSize;
// default values
var placementType = "land";
@@ -28,110 +29,43 @@ m.createObstructionMap = function(gameState, accessIndex, template)
if (placementType == "shore")
{
if (passabilityMap.cellSize == 4)
{
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction")
| gameState.getPassabilityClassMask("building-shore")
| gameState.getPassabilityClassMask("default");
var obstructionDefault = gameState.getPassabilityClassMask("default");
}
else // new pathFinder branch
{
var obstructionMask = gameState.getPassabilityClassMask("default-no-clearance")
| gameState.getPassabilityClassMask("building-shore")
| gameState.getPassabilityClassMask("default-terrain-only");
var obstructionDefault = gameState.getPassabilityClassMask("default-terrain-only");
}
var okay = false;
for (var x = 0; x < passabilityMap.width; ++x)
{
for (var y = 0; y < passabilityMap.height; ++y)
{
var i = x + y*passabilityMap.width;
var xter = Math.floor((x+0.5)*passabilityMap.cellSize / territoryMap.cellSize);
var yter = Math.floor((y+0.5)*passabilityMap.cellSize / territoryMap.cellSize);
var iter = xter + yter*territoryMap.width;
var tilePlayer = (territoryMap.data[iter] & m.TERRITORY_PLAYER_MASK);
if (gameState.isPlayerEnemy(tilePlayer) && tilePlayer !== 0)
{
obstructionTiles[i] = 0;
continue;
}
if ((passabilityMap.data[i] & (gameState.getPassabilityClassMask("building-shore") | obstructionDefault)))
{
obstructionTiles[i] = 0;
continue;
}
okay = false;
var positions = [[0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1]];
var available = 0;
for (var stuff of positions)
{
var index = x + stuff[0] + (y+stuff[1])*passabilityMap.width;
var index2 = x + stuff[0]*2 + (y+stuff[1]*2)*passabilityMap.width;
var index3 = x + stuff[0]*3 + (y+stuff[1]*3)*passabilityMap.width;
var index4 = x + stuff[0]*4 + (y+stuff[1]*4)*passabilityMap.width;
if ((passabilityMap.data[index] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index,true) > 500)
if ((passabilityMap.data[index2] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index2,true) > 500)
if ((passabilityMap.data[index3] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index3,true) > 500)
if ((passabilityMap.data[index4] & obstructionDefault) && gameState.ai.accessibility.getRegionSizei(index4,true) > 500)
{
if (available < 2)
available++;
else
okay = true;
}
}
// checking for accessibility: if a neighbor is inaccessible, this is too. If it's not on the same "accessible map" as us, we crash-i~u.
var radius = 3;
for (var xx = -radius;xx <= radius; xx++)
for (var yy = -radius;yy <= radius; yy++)
{
var id = x + xx + (y+yy)*passabilityMap.width;
if (id > 0 && id < passabilityMap.data.length)
if (gameState.ai.terrainAnalyzer.map[id] === 0 || gameState.ai.terrainAnalyzer.map[id] == 30 || gameState.ai.terrainAnalyzer.map[id] == 40)
okay = false;
}
obstructionTiles[i] = okay ? 255 : 0;
}
}
var passMap = gameState.ai.accessibility.navalPassMap;
var obstructionMask = gameState.getPassabilityClassMask("building-shore");
}
else
{
var playerID = PlayerID;
if (passabilityMap.cellSize == 4)
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction")
| gameState.getPassabilityClassMask("building-land");
else // new pathFinder branch
var obstructionMask = gameState.getPassabilityClassMask("default-no-clearance")
| gameState.getPassabilityClassMask("building-land");
for (var i = 0; i < passabilityMap.data.length; ++i)
var passMap = gameState.ai.accessibility.landPassMap;
var obstructionMask = gameState.getPassabilityClassMask("building-land");
}
if (passabilityMap.cellSize == 4)
var obstructionMask = obstructionMask | gameState.getPassabilityClassMask("foundationObstruction");
else // new pathFinder branch
var obstructionMask = obstructionMask | gameState.getPassabilityClassMask("default-no-clearance");
for (var k = 0; k < territoryMap.data.length; ++k)
{
let tilePlayer = (territoryMap.data[k] & m.TERRITORY_PLAYER_MASK);
if ((!buildNeutral && tilePlayer == 0) ||
(!buildOwn && tilePlayer == PlayerID) ||
(!buildAlly && tilePlayer != PlayerID && gameState.isPlayerAlly(tilePlayer)) ||
(!buildEnemy && tilePlayer != 0 && gameState.isPlayerEnemy(tilePlayer)))
continue;
let x = ratio * (k % territoryMap.width);
let y = ratio * (Math.floor(k / territoryMap.width));
for (let ix = 0; ix < ratio; ++ix)
{
var x = i % passabilityMap.width;
var y = Math.floor(i / passabilityMap.width);
var xter = Math.floor((x+0.5)*passabilityMap.cellSize / territoryMap.cellSize);
var yter = Math.floor((y+0.5)*passabilityMap.cellSize / territoryMap.cellSize);
var iter = xter + yter*territoryMap.width;
var tilePlayer = (territoryMap.data[iter] & m.TERRITORY_PLAYER_MASK);
var invalidTerritory = (
(!buildOwn && tilePlayer == playerID) ||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
(!buildNeutral && tilePlayer == 0) ||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
);
if (accessIndex)
var tileAccessible = (accessIndex === gameState.ai.accessibility.landPassMap[i]);
else
var tileAccessible = true;
obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 255;
for (let iy = 0; iy < ratio; ++iy)
{
let i = x + ix + (y + iy)*passabilityMap.width;
if (placementType != "shore" && accessIndex && accessIndex !== passMap[i])
continue;
if (!(passabilityMap.data[i] & obstructionMask))
obstructionTiles[i] = 255;
}
}
}
var map = new API3.Map(gameState.sharedScript, "passability", obstructionTiles);
map.setMaxVal(255);
@@ -145,9 +79,9 @@ m.createObstructionMap = function(gameState, accessIndex, template)
if (ent.buildCategory() === category && ent.position())
{
var pos = ent.position();
var x = Math.round(pos[0] / gameState.cellSize);
var z = Math.round(pos[1] / gameState.cellSize);
map.addInfluence(x, z, minDist/gameState.cellSize, -255, 'constant');
var x = Math.round(pos[0] / passabilityMap.cellSize);
var z = Math.round(pos[1] / passabilityMap.cellSize);
map.addInfluence(x, z, minDist/passability.cellSize, -255, 'constant');
}
});
}
@@ -407,12 +407,11 @@ m.NavalManager.prototype.splitTransport = function(gameState, plan)
* create a transport from a garrisoned ship to a land location
* needed at start game when starting with a garrisoned ship
*/
m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos)
m.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess)
{
let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos);
if (fromAccess !== 1)
return;
let toAccess = gameState.ai.accessibility.getAccessValue(toPos);
if (toAccess < 2)
return;
@@ -71,22 +71,23 @@ m.ConstructionPlan.prototype.start = function(gameState)
this.metadata.base = pos.base;
if (pos.access)
this.metadata.access = pos.access; // needed for Docks for the position is on water
this.metadata.access = pos.access; // needed for Docks whose position is on water
else
this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]);
if (this.template.buildCategory() === "Dock")
{
// try to place it a bit inside the land if possible
let cosang = Math.cos(pos.angle);
let sinang = Math.sin(pos.angle);
if (this.template.get("Obstruction") && this.template.get("Obstruction/Static"))
var radius = (+this.template.get("Obstruction/Static/@depth"))/2;
else
var radius = 0;
for (let step = 0; step < radius; step += 4)
builders[0].construct(this.type, pos.x+step*sinang, pos.z+step*cosang,
pos.angle, this.metadata);
// adjust a bit the position if needed
// TODO we would need groundLevel and waterLevel to do it properly
let cosa = Math.cos(pos.angle);
let sina = Math.sin(pos.angle);
let shiftMax = gameState.ai.HQ.territoryMap.cellSize;
for (let shift = 0; shift <= shiftMax; shift += 2)
{
builders[0].construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata);
if (shift > 0)
builders[0].construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata);
}
}
else if (pos.x == pos.xx && pos.z == pos.zz)
builders[0].construct(this.type, pos.x, pos.z, pos.angle, this.metadata);
@@ -99,9 +100,9 @@ m.ConstructionPlan.prototype.start = function(gameState)
this.onStart(gameState);
Engine.ProfileStop();
// TODO should have a ConstructionStarted event in case the construct order fails
// TODO should have a ConstructionStarted even in case the construct order fails
if (this.metadata && this.metadata.proximity)
gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z]);
gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access);
};
// TODO for dock, we should allow building them outside territory, and we should check that we are along the right sea
@@ -157,13 +158,6 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState)
}
}
// First, find all tiles that are far enough away from obstructions:
var obstructions = m.createObstructionMap(gameState, 0, template);
obstructions.expandInfluences();
//obstructions.dumpIm(template.buildCategory() + "_obstructions.png");
// Compute each tile's closeness to friendly structures:
var placement = new API3.Map(gameState.sharedScript, "territory");
@@ -291,12 +285,16 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState)
}
}
}
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this
// allows room for units to walk between buildings.
// Find the best non-obstructed:
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close,
// this allows room for units to walk between buildings.
// note: not for houses and dropsites who ought to be closer to either each other or a resource.
// also not for fields who can be stacked quite a bit
var obstructions = m.createObstructionMap(gameState, 0, template);
//obstructions.dumpIm(template.buildCategory() + "_obstructions.png");
var radius = 0;
if (template.hasClass("Fortress") || this.type === gameState.applyCiv("structures/{civ}_siege_workshop")
|| this.type === gameState.applyCiv("structures/{civ}_elephant_stables"))
@@ -306,7 +304,6 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState)
else
radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize);
// Find the best non-obstructed
if (template.hasClass("House") && !alreadyHasHouses)
{
// try to get some space to place several houses first
@@ -327,17 +324,35 @@ m.ConstructionPlan.prototype.findGoodPosition = function(gameState)
var x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
var xx = x;
var zz = z;
if (template.hasClass("House") || template.hasClass("Field") || template.resourceDropsiteTypes() !== undefined)
var secondBest = obstructions.findLowestNeighbor(x,z);
else
var secondBest = [x,z];
{
if (obstructions.cellSize != 4) // new pathFinder branch
{
let secondBest = obstructions.findNearestObstructed(bestIdx, radius);
if (secondBest >= 0)
{
x = ((secondBest % obstructions.width) + 0.5) * obstructions.cellSize;
z = (Math.floor(secondBest / obstructions.width) + 0.5) * obstructions.cellSize;
xx = x;
zz = z;
}
}
else
{
obstructions.expandInfluences();
let secondBest = obstructions.findLowestNeighbor(x,z);
xx = secondBest[0];
zz = secondBest[1];
}
}
var territorypos = placement.gamePosToMapPos([x,z]);
var territoryIndex = territorypos[0] + territorypos[1]*placement.width;
let territorypos = placement.gamePosToMapPos([x,z]);
let territoryIndex = territorypos[0] + territorypos[1]*placement.width;
// default angle = 3*Math.PI/4;
return { "x": x, "z": z, "angle": 3*Math.PI/4, "xx": secondBest[0], "zz": secondBest[1],
"base": gameState.ai.HQ.basesMap.map[territoryIndex] };
return { "x": x, "z": z, "angle": 3*Math.PI/4, "xx": xx, "zz": zz, "base": gameState.ai.HQ.basesMap.map[territoryIndex] };
};
/**
@@ -348,14 +363,16 @@ m.ConstructionPlan.prototype.findDockPosition = function(gameState)
{
var template = this.template;
var cellSize = gameState.cellSize; // size of each tile
var territoryMap = gameState.ai.HQ.territoryMap;
var obstructions = m.createObstructionMap(gameState, 0, template);
//obstructions.dumpIm(template.buildCategory() + "_obstructions.png");
var bestIdx = undefined;
var bestVal = 0;
var bestJdx = undefined;
var bestAngle = undefined;
var bestLand = undefined;
var bestVal = -1;
var landPassMap = gameState.ai.accessibility.landPassMap;
var navalPassMap = gameState.ai.accessibility.navalPassMap;
@@ -366,69 +383,98 @@ m.ConstructionPlan.prototype.findDockPosition = function(gameState)
if (this.metadata.proximity)
proxyAccess = gameState.ai.accessibility.getAccessValue(this.metadata.proximity);
var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize);
var halfSize = 0; // used for dock angle
var halfDepth = 0; // used by checkPlacement
var halfWidth = 0; // used by checkPlacement
if (template.get("Footprint/Square"))
{
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
halfDepth = +template.get("Footprint/Square/@depth") / 2;
halfWidth = +template.get("Footprint/Square/@width") / 2;
}
else if (template.get("Footprint/Circle"))
{
halfSize = +template.get("Footprint/Circle/@radius");
halfDepth = halfSize;
halfWidth = halfSize;
}
var maxres = 10;
for (let j = 0; j < territoryMap.length; ++j)
{
if (obstructions.map[j] <= 0)
var i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0)
continue;
var landAccess = this.getLandAccess(gameState, i, radius+1, obstructions.width);
if (landAccess.size == 0)
continue;
if (this.metadata)
{
if (this.metadata.land && this.metadata.land.indexOf(landPassMap[j]) === -1)
if (this.metadata.land && !landAccess.has(+this.metadata.land))
continue;
if (this.metadata.sea && navalPassMap[j] !== this.metadata.sea)
if (this.metadata.sea && navalPassMap[i] != +this.metadata.sea)
continue;
if (nbShips === 0 && proxyAccess && proxyAccess > 1 && landPassMap[j] !== proxyAccess)
if (nbShips === 0 && proxyAccess && proxyAccess > 1 && !landAccess.has(proxyAccess))
continue;
}
let tileOwner = territoryMap.getOwnerIndex(j);
if (tileOwner !== 0 && gameState.isPlayerEnemy(tileOwner))
continue;
var res = Math.min(maxres, this.getResourcesAround(gameState, j, 80));
var dist;
if (this.metadata.proximity)
{
// if proximity is given, we look for the nearest point
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
let dist = API3.SquareVectorDistance(this.metadata.proximity, pos);
if (bestIdx !== undefined && dist > bestVal)
continue;
bestVal = dist;
bestIdx = j;
var pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
dist = API3.SquareVectorDistance(this.metadata.proximity, pos);
dist = Math.sqrt(dist) + 15 * (maxres - res);
}
else
{
// if not in our (or allied) territory, we do not want it too far to be able to defend it
let nearby = m.getFrontierProximity(gameState, j);
if (nearby > 4)
dist = m.getFrontierProximity(gameState, j);
if (dist > 4)
continue;
bestVal = 1;
bestIdx = j;
dist = dist + 0.4 * (maxres - res)
}
if (bestIdx !== undefined && dist > bestVal)
continue;
var x = ((i % obstructions.width) + 0.5) * obstructions.cellSize;
var z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize;
var angle = this.getDockAngle(gameState, x, z, halfSize);
if (angle === false)
continue;
var land = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle);
if (land < 2 || !gameState.ai.HQ.landRegions[land])
continue;
if (this.metadata.proximity && gameState.ai.accessibility.regionSize[land] < 4000)
continue;
bestVal = dist;
bestIdx = i;
bestJdx = j;
bestAngle = angle;
bestLand = land;
}
if (bestVal <= 0)
if (bestVal < 0)
return false;
var x = ((bestIdx % territoryMap.width) + 0.5) * cellSize;
var z = (Math.floor(bestIdx / territoryMap.width) + 0.5) * cellSize;
// Needed for dock placement whose position will be changed
var access = gameState.ai.accessibility.getAccessValue([x, z]);
// for Dock placement, we need to improve the position of the building as the position given here
// is only the position on the shore, while the need the position of the center of the building
// We also need to find the angle of the building
var angle = this.getDockAngle(gameState, x, z);
if (angle === false)
return false;
var x = ((bestIdx % obstructions.width) + 0.5) * obstructions.cellSize;
var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
// Assign this dock to a base
var baseIndex = gameState.ai.HQ.basesMap.map[bestIdx];
var baseIndex = gameState.ai.HQ.basesMap.map[bestJdx];
if (!baseIndex)
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex !== access)
if (base.accessIndex !== bestLand)
continue;
baseIndex = base.ID;
break;
@@ -442,32 +488,28 @@ m.ConstructionPlan.prototype.findDockPosition = function(gameState)
}
}
return { "x": x, "z": z, "angle": angle, "xx": x, "zz": z, "base": baseIndex, "access": access };
return { "x": x, "z": z, "angle": bestAngle, "xx": x, "zz": z, "base": baseIndex, "access": bestLand };
};
// Algorithm taken from the function GetDockAngle in helpers/Commands.js
m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z)
// Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js
m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size)
{
var radius = this.template.obstructionRadius();
if (!radius)
return false;
var pos = gameState.ai.accessibility.gamePosToMapPos([x, z]);
var j = pos[0] + pos[1]*gameState.ai.accessibility.width;
var seaRef = gameState.ai.accessibility.navalPassMap[j];
if (seaRef < 2)
return false;
const numPoints = 16;
for (var dist = 0; dist < 2; ++dist)
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var pos = [ x - (2+dist)*radius*Math.sin(angle), z + (2+dist)*radius*Math.cos(angle)];
var pos = gameState.ai.accessibility.gamePosToMapPos(pos);
var j = pos[0] + pos[1]*gameState.ai.accessibility.width;
var seaAccess = gameState.ai.accessibility.navalPassMap[j];
var landAccess = gameState.ai.accessibility.landPassMap[j];
if (seaAccess == seaRef && landAccess < 2)
let angle = (i/numPoints)*2*Math.PI;
pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)];
pos = gameState.ai.accessibility.gamePosToMapPos(pos);
let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.navalPassMap[j] == seaRef)
waterPoints.push(i);
}
var length = waterPoints.length;
@@ -504,6 +546,126 @@ m.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z)
return false;
};
// Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js
// to determine the special dock requirements
m.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle)
{
let sz = halfDepth * Math.sin(angle);
let cz = halfDepth * Math.cos(angle);
// center back position
let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]);
let j = pos[0] + pos[1]*gameState.ai.accessibility.width;
let ret = gameState.ai.accessibility.landPassMap[j];
if (ret < 2)
return 0;
// center front position
pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2)
return 0;
// additional constraints compared to BuildRestriction.js to assure we have enough place to build
let sw = halfWidth * Math.cos(angle) * 3 / 4;
let cw = halfWidth * Math.sin(angle) * 3 / 4;
pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] != ret)
return 0;
pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]);
j = pos[0] + pos[1]*gameState.ai.accessibility.width;
if (gameState.ai.accessibility.landPassMap[j] != ret)
return 0;
return ret;
};
// get the list of all the land access from this position
m.ConstructionPlan.prototype.getLandAccess = function(gameState, i, radius, w)
{
var access = new Set();
var landPassMap = gameState.ai.accessibility.landPassMap;
var kx = i % w;
var ky = Math.floor(i / w);
var land;
for (let dy = 0; dy <= radius; ++dy)
{
let dxmax = radius - dy;
let xp = kx + (ky + dy)*w;
let xm = kx + (ky - dy)*w;
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
if (kx + dx < 0 || kx + dx >= w)
continue;
if (ky + dy >= 0 && ky + dy < w)
{
land = landPassMap[xp + dx];
if (land > 1 && !access.has(land))
access.add(land);
}
if (ky - dy >= 0 && ky - dy < w)
{
land = landPassMap[xm + dx];
if (land > 1 && !access.has(land))
access.add(land);
}
}
}
return access;
};
// get the sum of the resources (except food) around, inside a given radius
// resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood
m.ConstructionPlan.prototype.getResourcesAround = function(gameState, i, radius)
{
let resourceMaps = gameState.sharedScript.resourceMaps;
let w = resourceMaps["wood"].width;
let cellSize = resourceMaps["wood"].cellSize;
let size = Math.floor(radius / cellSize);
let ix = i % w;
let iy = Math.floor(i / w);
let total = 0;
let nbcell = 0;
for (let k in resourceMaps)
{
if (k === "food")
continue;
let weigh0 = (k === "wood") ? 2 : 1;
for (let dy = 0; dy <= size; ++dy)
{
let dxmax = size - dy;
let ky = iy + dy;
if (ky >= 0 && ky < w)
{
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
let kx = ix + dx;
if (kx < 0 || kx >= w)
continue;
let ddx = (dx > 0) ? dx : -dx;
let weight = weigh0 * (dxmax - ddx) / size;
total += weight * resourceMaps[k].map[kx + w * ky];
nbcell += weight;
}
}
if (dy == 0)
continue;
ky = iy - dy;
if (ky >= 0 && ky < w)
{
for (let dx = -dxmax; dx <= dxmax; ++dx)
{
let kx = ix + dx;
if (kx < 0 || kx >= w)
continue;
let ddx = (dx > 0) ? dx : -dx;
let weight = weigh0 * (dxmax - ddx) / size;
total += weight * resourceMaps[k].map[kx + w * ky];
nbcell += weight;
}
}
}
}
return (nbcell ? (total / nbcell) : 0);
};
m.ConstructionPlan.prototype.Serialize = function()
{
let prop = {
@@ -88,6 +88,7 @@ m.HQ.prototype.assignStartingEntities = function(gameState)
this.navalRegions[sea] = true;
// if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport
// when a construction will start (see createTransportIfNeeded)
if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship"))
for (let id of ent.garrisoned())
ent.unload(id);