forked from 0ad/0ad
681 lines
17 KiB
JavaScript
681 lines
17 KiB
JavaScript
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// ClumpPlacer
|
|
//
|
|
// Class for generating a roughly circular clump of points
|
|
//
|
|
// size: The average number of points in the clump
|
|
// coherence: How much the radius of the clump varies (1.0 = circle, 0.0 = very random)
|
|
// smoothness: How smooth the border of the clump is (1.0 = few "peaks", 0.0 = very jagged)
|
|
// failfraction: Percentage of place attempts allowed to fail (optional)
|
|
// x, z: Tile coordinates of placer center (optional)
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function ClumpPlacer(size, coherence, smoothness, failFraction, x, z)
|
|
{
|
|
this.size = size;
|
|
this.coherence = coherence;
|
|
this.smoothness = smoothness;
|
|
this.failFraction = (failFraction !== undefined ? failFraction : 0);
|
|
this.x = (x !== undefined ? x : -1);
|
|
this.z = (z !== undefined ? z : -1);
|
|
}
|
|
|
|
ClumpPlacer.prototype.place = function(constraint)
|
|
{
|
|
// Preliminary bounds check
|
|
if (!g_Map.inMapBounds(this.x, this.z) || !constraint.allows(this.x, this.z))
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
var retVec = [];
|
|
|
|
var size = getMapSize();
|
|
var gotRet = new Array(size);
|
|
for (var i = 0; i < size; ++i)
|
|
{
|
|
gotRet[i] = new Uint8Array(size); // bool / uint8
|
|
}
|
|
|
|
var radius = sqrt(this.size / PI);
|
|
var perim = 4 * radius * 2 * PI;
|
|
var intPerim = ceil(perim);
|
|
|
|
var ctrlPts = 1 + Math.floor(1.0/Math.max(this.smoothness,1.0/intPerim));
|
|
|
|
if (ctrlPts > radius * 2 * PI)
|
|
{
|
|
ctrlPts = Math.floor(radius * 2 * PI) + 1;
|
|
}
|
|
|
|
var noise = new Float32Array(intPerim); //float32
|
|
var ctrlCoords = new Float32Array(ctrlPts+1); //float32
|
|
var ctrlVals = new Float32Array(ctrlPts+1); //float32
|
|
|
|
// Generate some interpolated noise
|
|
for (var i=0; i < ctrlPts; i++)
|
|
{
|
|
ctrlCoords[i] = i * perim / ctrlPts;
|
|
ctrlVals[i] = 2.0*randFloat();
|
|
}
|
|
|
|
var c = 0;
|
|
var looped = 0;
|
|
for (var i=0; i < intPerim; i++)
|
|
{
|
|
if (ctrlCoords[(c+1) % ctrlPts] < i && !looped)
|
|
{
|
|
c = (c+1) % ctrlPts;
|
|
if (c == ctrlPts-1)
|
|
looped = 1;
|
|
}
|
|
|
|
// Cubic interpolation of ctrlVals
|
|
var t = (i - ctrlCoords[c]) / ((looped ? perim : ctrlCoords[(c+1)%ctrlPts]) - ctrlCoords[c]);
|
|
var v0 = ctrlVals[(c+ctrlPts-1)%ctrlPts];
|
|
var v1 = ctrlVals[c];
|
|
var v2 = ctrlVals[(c+1)%ctrlPts];
|
|
var v3 = ctrlVals[(c+2)%ctrlPts];
|
|
var P = (v3 - v2) - (v0 - v1);
|
|
var Q = (v0 - v1) - P;
|
|
var R = v2 - v0;
|
|
var S = v1;
|
|
|
|
noise[i] = P*t*t*t + Q*t*t + R*t + S;
|
|
}
|
|
|
|
var failed = 0;
|
|
for (var p=0; p < intPerim; p++)
|
|
{
|
|
var th = 2 * PI * p / perim;
|
|
var r = radius * (1 + (1-this.coherence)*noise[p]);
|
|
var s = sin(th);
|
|
var c = cos(th);
|
|
var xx=this.x;
|
|
var yy=this.z;
|
|
|
|
for (var k=0; k < ceil(r); k++)
|
|
{
|
|
var i = Math.floor(xx);
|
|
var j = Math.floor(yy);
|
|
if (g_Map.inMapBounds(i, j) && constraint.allows(i, j))
|
|
{
|
|
if (!gotRet[i][j])
|
|
{ // Only include each point once
|
|
gotRet[i][j] = 1;
|
|
retVec.push(new PointXZ(i, j));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
failed++;
|
|
}
|
|
xx += s;
|
|
yy += c;
|
|
}
|
|
}
|
|
|
|
return ((failed > this.size*this.failFraction) ? undefined : retVec);
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// Chain Placer
|
|
//
|
|
// Class for generating a more random clump of points it randomly creates circles around the edges of the current clump
|
|
//
|
|
// minRadius: minimum radius of the circles
|
|
// maxRadius: maximum radius of the circles
|
|
// numCircles: the number of the circles
|
|
// failfraction: Percentage of place attempts allowed to fail (optional)
|
|
// x, z: Tile coordinates of placer center (optional)
|
|
// fcc: Farthest circle center (optional)
|
|
// q: a list containing numbers. each time if the list still contains values, pops one from the end and uses it as the radius (optional)
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function ChainPlacer(minRadius, maxRadius, numCircles, failFraction, x, z, fcc, q)
|
|
{
|
|
this.minRadius = minRadius;
|
|
this.maxRadius = maxRadius;
|
|
this.numCircles = numCircles
|
|
this.failFraction = (failFraction !== undefined ? failFraction : 0);
|
|
this.x = (x !== undefined ? x : -1);
|
|
this.z = (z !== undefined ? z : -1);
|
|
this.fcc = (fcc !== undefined ? fcc : 0);
|
|
this.q = (q !== undefined ? q : []);
|
|
}
|
|
|
|
ChainPlacer.prototype.place = function(constraint)
|
|
{
|
|
// Preliminary bounds check
|
|
if (!g_Map.inMapBounds(this.x, this.z) || !constraint.allows(this.x, this.z))
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
var retVec = [];
|
|
var size = getMapSize();
|
|
var failed = 0, count = 0;
|
|
var queueEmpty = (this.q.length ? false : true);
|
|
|
|
var gotRet = new Array(size);
|
|
for (var i = 0; i < size; ++i)
|
|
{
|
|
gotRet[i] = new Array(size);
|
|
for (var j = 0; j < size; ++j)
|
|
{
|
|
gotRet[i][j] = -1;
|
|
}
|
|
}
|
|
|
|
--size;
|
|
|
|
if (this.minRadius < 1) this.minRadius = 1;
|
|
if (this.minRadius > this.maxRadius) this.minRadius = this.maxRadius;
|
|
|
|
var edges = [[this.x, this.z]]
|
|
|
|
for (var i = 0; i < this.numCircles; ++i)
|
|
{
|
|
var point = edges[randInt(edges.length)];
|
|
var cx = point[0], cz = point[1];
|
|
|
|
if (queueEmpty)
|
|
{
|
|
var radius = randInt(this.minRadius, this.maxRadius);
|
|
}
|
|
else
|
|
{
|
|
var radius = this.q.pop();
|
|
queueEmpty = (this.q.length ? false : true);
|
|
}
|
|
|
|
//log (edges);
|
|
|
|
var sx = cx - radius, lx = cx + radius;
|
|
var sz = cz - radius, lz = cz + radius;
|
|
|
|
sx = (sx < 0 ? 0 : sx);
|
|
sz = (sz < 0 ? 0 : sz);
|
|
lx = (lx > size ? size : lx);
|
|
lz = (lz > size ? size : lz);
|
|
|
|
var radius2 = radius * radius;
|
|
var dx, dz;
|
|
|
|
//log (uneval([sx, sz, lx, lz]));
|
|
|
|
for (var ix = sx; ix <= lx; ++ix)
|
|
{
|
|
for (var iz = sz; iz <= lz; ++ iz)
|
|
{
|
|
dx = ix - cx;
|
|
dz = iz - cz;
|
|
if (dx * dx + dz * dz <= radius2)
|
|
{
|
|
if (g_Map.inMapBounds(ix, iz) && constraint.allows(ix, iz))
|
|
{
|
|
var state = gotRet[ix][iz];
|
|
if (state == -1)
|
|
{
|
|
retVec.push(new PointXZ(ix, iz));
|
|
gotRet[ix][iz] = -2;
|
|
}
|
|
else if (state >= 0)
|
|
{
|
|
//log (uneval(edges));
|
|
//log (state)
|
|
var s = edges.splice(state, 1);
|
|
//log (uneval(s));
|
|
//log (uneval(edges));
|
|
gotRet[ix][iz] = -2;
|
|
|
|
var edgesLength = edges.length;
|
|
for (var k = state; k < edges.length; ++k)
|
|
{
|
|
--gotRet[edges[k][0]][edges[k][1]];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++failed;
|
|
}
|
|
++count;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var ix = sx; ix <= lx; ++ix)
|
|
{
|
|
for (var iz = sz; iz <= lz; ++ iz)
|
|
{
|
|
if (this.fcc)
|
|
if ((this.x - ix) > this.fcc || (ix - this.x) > this.fcc || (this.z - iz) > this.fcc || (iz - this.z) > this.fcc)
|
|
continue;
|
|
|
|
if (gotRet[ix][iz] == -2)
|
|
{
|
|
if (ix > 0)
|
|
{
|
|
if (gotRet[ix-1][iz] == -1)
|
|
{
|
|
edges.push([ix, iz]);
|
|
gotRet[ix][iz] = edges.length - 1;
|
|
continue;
|
|
}
|
|
}
|
|
if (iz > 0)
|
|
{
|
|
if (gotRet[ix][iz-1] == -1)
|
|
{
|
|
edges.push([ix, iz]);
|
|
gotRet[ix][iz] = edges.length - 1;
|
|
continue;
|
|
}
|
|
}
|
|
if (ix < size)
|
|
{
|
|
if (gotRet[ix+1][iz] == -1)
|
|
{
|
|
edges.push([ix, iz]);
|
|
gotRet[ix][iz] = edges.length - 1;
|
|
continue;
|
|
}
|
|
}
|
|
if (iz < size)
|
|
{
|
|
if (gotRet[ix][iz+1] == -1)
|
|
{
|
|
edges.push([ix, iz]);
|
|
gotRet[ix][iz] = edges.length - 1;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
return ((failed > count*this.failFraction) ? undefined : retVec);
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// RectPlacer
|
|
//
|
|
// Class for generating a rectangular block of points
|
|
//
|
|
// x1,z1: Top left corner of block
|
|
// x2,z2: Bottom right corner of block
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function RectPlacer(x1, z1, x2, z2)
|
|
{
|
|
this.x1 = x1;
|
|
this.z1 = z1;
|
|
this.x2 = x2;
|
|
this.z2 = z2;
|
|
|
|
if (x1 > x2 || z1 > z2)
|
|
{
|
|
throw("RectPlacer: incorrect bounds on rect");
|
|
}
|
|
}
|
|
|
|
RectPlacer.prototype.place = function(constraint)
|
|
{
|
|
// Preliminary bounds check
|
|
if (!g_Map.inMapBounds(this.x1, this.z1) || !constraint.allows(this.x1, this.z1) ||
|
|
!g_Map.inMapBounds(this.x2, this.z2) || !constraint.allows(this.x2, this.z2))
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
var ret = [];
|
|
|
|
var x2 = this.x2;
|
|
var z2 = this.z2;
|
|
|
|
for (var x=this.x1; x < x2; x++)
|
|
{
|
|
for (var z=this.z1; z < z2; z++)
|
|
{
|
|
if (g_Map.inMapBounds(x, z) && constraint.allows(x, z))
|
|
{
|
|
ret.push(new PointXZ(x, z));
|
|
}
|
|
else
|
|
{
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// ObjectGroupPlacer
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function ObjectGroupPlacer() {}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// SimpleObject
|
|
//
|
|
// Class specifying a type of entity that can be placed on the map
|
|
//
|
|
// type: The entity's template name
|
|
// minCount,maxCount: The number of objects to place
|
|
// minDistance,maxDistance: The distance between placed objects
|
|
// minAngle,maxAngle: The variation in angle of placed objects (optional)
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function SimpleObject(type, minCount, maxCount, minDistance, maxDistance, minAngle, maxAngle)
|
|
{
|
|
this.type = type;
|
|
this.minCount = minCount;
|
|
this.maxCount = maxCount;
|
|
this.minDistance = minDistance;
|
|
this.maxDistance = maxDistance;
|
|
this.minAngle = (minAngle !== undefined ? minAngle : 0);
|
|
this.maxAngle = (maxAngle !== undefined ? maxAngle : 2*PI);
|
|
|
|
if (minCount > maxCount)
|
|
warn("SimpleObject: minCount should be less than or equal to maxCount");
|
|
|
|
if (minDistance > maxDistance)
|
|
warn("SimpleObject: minDistance should be less than or equal to maxDistance");
|
|
|
|
if (minAngle > maxAngle)
|
|
warn("SimpleObject: minAngle should be less than or equal to maxAngle");
|
|
}
|
|
|
|
SimpleObject.prototype.place = function(cx, cz, player, avoidSelf, constraint)
|
|
{
|
|
var failCount = 0;
|
|
var count = randInt(this.minCount, this.maxCount);
|
|
var resultObjs = [];
|
|
|
|
for (var i=0; i < count; i++)
|
|
{
|
|
while(true)
|
|
{
|
|
var distance = randFloat(this.minDistance, this.maxDistance);
|
|
var direction = randFloat(0, 2*PI);
|
|
|
|
var x = cx + 0.5 + distance*cos(direction);
|
|
var z = cz + 0.5 + distance*sin(direction);
|
|
var fail = false; // reset place failure flag
|
|
|
|
if (!g_Map.validT(x, z))
|
|
{
|
|
fail = true;
|
|
}
|
|
else
|
|
{
|
|
if (avoidSelf)
|
|
{
|
|
var length = resultObjs.length;
|
|
for (var i = 0; (i < length) && !fail; i++)
|
|
{
|
|
var dx = x - resultObjs[i].position.x;
|
|
var dy = z - resultObjs[i].position.z;
|
|
|
|
if ((dx*dx + dy*dy) < 1)
|
|
{
|
|
fail = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fail)
|
|
{
|
|
if (!constraint.allows(Math.floor(x), Math.floor(z)))
|
|
{
|
|
fail = true;
|
|
}
|
|
else
|
|
{ // if we got here, we're good
|
|
var angle = randFloat(this.minAngle, this.maxAngle);
|
|
resultObjs.push(new Entity(this.type, player, x, z, angle));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fail)
|
|
{
|
|
failCount++;
|
|
if (failCount > 20) // TODO: Make this adjustable
|
|
{
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return resultObjs;
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// RandomObject
|
|
//
|
|
// Class specifying entities that can be placed on the map, selected randomly
|
|
//
|
|
// types: Array of entity template names
|
|
// minCount,maxCount: The number of objects to place
|
|
// minDistance,maxDistance: The distance between placed objects
|
|
// minAngle,maxAngle: The variation in angle of placed objects (optional)
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function RandomObject(types, minCount, maxCount, minDistance, maxDistance, minAngle, maxAngle)
|
|
{
|
|
this.types = types;
|
|
this.minCount = minCount;
|
|
this.maxCount = maxCount;
|
|
this.minDistance = minDistance;
|
|
this.maxDistance = maxDistance;
|
|
this.minAngle = (minAngle !== undefined ? minAngle : 0);
|
|
this.maxAngle = (maxAngle !== undefined ? maxAngle : 2*PI);
|
|
|
|
if (minCount > maxCount)
|
|
warn("RandomObject: minCount should be less than or equal to maxCount");
|
|
|
|
if (minDistance > maxDistance)
|
|
warn("RandomObject: minDistance should be less than or equal to maxDistance");
|
|
|
|
if (minAngle > maxAngle)
|
|
warn("RandomObject: minAngle should be less than or equal to maxAngle");
|
|
}
|
|
|
|
RandomObject.prototype.place = function(cx, cz, player, avoidSelf, constraint)
|
|
{
|
|
var failCount = 0;
|
|
var count = randInt(this.minCount, this.maxCount);
|
|
var resultObjs = [];
|
|
|
|
for (var i=0; i < count; i++)
|
|
{
|
|
while(true)
|
|
{
|
|
var distance = randFloat(this.minDistance, this.maxDistance);
|
|
var direction = randFloat(0, 2*PI);
|
|
|
|
var x = cx + 0.5 + distance*cos(direction);
|
|
var z = cz + 0.5 + distance*sin(direction);
|
|
var fail = false; // reset place failure flag
|
|
|
|
if (!g_Map.validT(x, z))
|
|
{
|
|
fail = true;
|
|
}
|
|
else
|
|
{
|
|
if (avoidSelf)
|
|
{
|
|
var length = resultObjs.length;
|
|
for (var i = 0; (i < length) && !fail; i++)
|
|
{
|
|
var dx = x - resultObjs[i].position.x;
|
|
var dy = z - resultObjs[i].position.z;
|
|
|
|
if ((dx*dx + dy*dy) < 1)
|
|
{
|
|
fail = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fail)
|
|
{
|
|
if (!constraint.allows(Math.floor(x), Math.floor(z)))
|
|
{
|
|
fail = true;
|
|
}
|
|
else
|
|
{ // if we got here, we're good
|
|
var angle = randFloat(this.minAngle, this.maxAngle);
|
|
|
|
//Randomly select entity
|
|
var type = this.types[randInt(this.types.length)];
|
|
resultObjs.push(new Entity(type, player, x, z, angle));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fail)
|
|
{
|
|
failCount++;
|
|
if (failCount > 20) // TODO: Make this adjustable
|
|
{
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return resultObjs;
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// SimpleGroup
|
|
//
|
|
// Class for placing groups of different objects
|
|
//
|
|
// elements: Array of SimpleObjects
|
|
// avoidSelf: Objects will not overlap
|
|
// tileClass: Optional tile class to add with these objects
|
|
// x,z: Tile coordinates of center of placer
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function SimpleGroup(elements, avoidSelf, tileClass, x, z)
|
|
{
|
|
this.elements = elements;
|
|
this.tileClass = (tileClass !== undefined ? getTileClass(tileClass) : undefined);
|
|
this.avoidSelf = (avoidSelf !== undefined ? avoidSelf : false);
|
|
this.x = (x !== undefined ? x : -1);
|
|
this.z = (z !== undefined ? z : -1);
|
|
}
|
|
|
|
SimpleGroup.prototype.place = function(player, constraint)
|
|
{
|
|
var resultObjs = [];
|
|
|
|
// Try placement of objects
|
|
var length = this.elements.length;
|
|
for (var i = 0; i < length; i++)
|
|
{
|
|
var objs = this.elements[i].place(this.x, this.z, player, this.avoidSelf, constraint);
|
|
if (objs === undefined)
|
|
{ // Failure
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
for (var j = 0; j < objs.length; ++j)
|
|
{
|
|
resultObjs.push(objs[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add placed objects to map
|
|
length = resultObjs.length;
|
|
for (var i=0; i < length; i++)
|
|
{
|
|
g_Map.addObject(resultObjs[i]);
|
|
|
|
if (this.tileClass !== undefined)
|
|
{ // Convert position to integer number of tiles
|
|
this.tileClass.add(Math.floor(resultObjs[i].position.x/CELL_SIZE), Math.floor(resultObjs[i].position.z/CELL_SIZE));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// RandomGroup
|
|
//
|
|
// Class for placing group of a random simple object
|
|
//
|
|
// elements: Array of SimpleObjects
|
|
// avoidSelf: Objects will not overlap
|
|
// tileClass: Optional tile class to add with these objects
|
|
// x,z: Tile coordinates of center of placer
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function RandomGroup(elements, avoidSelf, tileClass, x, z)
|
|
{
|
|
this.elements = elements;
|
|
this.tileClass = (tileClass !== undefined ? getTileClass(tileClass) : undefined);
|
|
this.avoidSelf = (avoidSelf !== undefined ? avoidSelf : false);
|
|
this.x = (x !== undefined ? x : -1);
|
|
this.z = (z !== undefined ? z : -1);
|
|
}
|
|
|
|
RandomGroup.prototype.place = function(player, constraint)
|
|
{
|
|
var resultObjs = [];
|
|
|
|
// Pick one of the object placers at random
|
|
var placer = this.elements[randInt(this.elements.length)];
|
|
|
|
var objs = placer.place(this.x, this.z, player, this.avoidSelf, constraint);
|
|
if (objs === undefined)
|
|
{ // Failure
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
for (var j = 0; j < objs.length; ++j)
|
|
{
|
|
resultObjs.push(objs[j]);
|
|
}
|
|
}
|
|
|
|
// Add placed objects to map
|
|
var length = resultObjs.length;
|
|
for (var i=0; i < length; i++)
|
|
{
|
|
g_Map.addObject(resultObjs[i]);
|
|
|
|
if (this.tileClass !== undefined)
|
|
{ // Convert position to integer number of tiles
|
|
this.tileClass.add(Math.floor(resultObjs[i].position.x/CELL_SIZE), Math.floor(resultObjs[i].position.z/CELL_SIZE));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|