Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitMotionFlying.js (revision 22419)
@@ -1,336 +1,336 @@
// (A serious implementation of this might want to use C++ instead of JS
// for performance; this is just for fun.)
const SHORT_FINAL = 2.5;
function UnitMotionFlying() {}
UnitMotionFlying.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
UnitMotionFlying.prototype.Init = function()
{
this.hasTarget = false;
this.reachedTarget = false;
this.targetX = 0;
this.targetZ = 0;
this.targetMinRange = 0;
this.targetMaxRange = 0;
this.speed = 0;
this.landing = false;
this.onGround = true;
this.pitch = 0;
this.roll = 0;
this.waterDeath = false;
this.passabilityClass = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).GetPassabilityClass(this.template.PassabilityClass);
};
UnitMotionFlying.prototype.OnUpdate = function(msg)
{
var turnLength = msg.turnLength;
if (!this.hasTarget)
return;
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
var pos = cmpPosition.GetPosition();
var angle = cmpPosition.GetRotation().y;
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
var ground = Math.max(cmpTerrain.GetGroundLevel(pos.x, pos.z), cmpWaterManager.GetWaterLevel(pos.x, pos.z));
var newangle = angle;
var canTurn = true;
if (this.landing)
{
if (this.speed > 0 && this.onGround)
{
if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater == "true")
this.waterDeath = true;
this.pitch = 0;
// Deaccelerate forwards...at a very reduced pace.
if (this.waterDeath)
this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate * 10);
else
this.speed = Math.max(0, this.speed - turnLength * this.template.BrakingRate);
canTurn = false;
// Clamp to ground if below it, or descend if above
if (pos.y < ground)
pos.y = ground;
else if (pos.y > ground)
pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
}
else if (this.speed == 0 && this.onGround)
{
if (this.waterDeath && cmpHealth)
cmpHealth.Kill();
else
{
this.pitch = 0;
// We've stopped.
if (cmpGarrisonHolder)
cmpGarrisonHolder.AllowGarrisoning(true,"UnitMotionFlying");
canTurn = false;
this.hasTarget = false;
this.landing = false;
// summon planes back from the edge of the map
var terrainSize = cmpTerrain.GetMapSize();
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager.GetLosCircular())
{
var mapRadius = terrainSize/2;
var x = pos.x - mapRadius;
var z = pos.z - mapRadius;
var div = (mapRadius - 12) / Math.sqrt(x*x + z*z);
if (div < 1)
{
pos.x = mapRadius + x*div;
pos.z = mapRadius + z*div;
newangle += Math.PI;
}
}
else
{
pos.x = Math.max(Math.min(pos.x, terrainSize - 12), 12);
pos.z = Math.max(Math.min(pos.z, terrainSize - 12), 12);
newangle += Math.PI;
}
}
}
else
{
// Final Approach
// We need to slow down to land!
this.speed = Math.max(this.template.LandingSpeed, this.speed - turnLength * this.template.SlowingRate);
canTurn = false;
var targetHeight = ground;
// Steep, then gradual descent.
if ((pos.y - targetHeight) / this.template.FlyingHeight > 1 / SHORT_FINAL)
this.pitch = - Math.PI / 18;
else
this.pitch = Math.PI / 18;
var descentRate = ((pos.y - targetHeight) / this.template.FlyingHeight * this.template.ClimbRate + SHORT_FINAL) * SHORT_FINAL;
if (pos.y < targetHeight)
pos.y = Math.max(targetHeight, pos.y + turnLength * descentRate);
else if (pos.y > targetHeight)
pos.y = Math.max(targetHeight, pos.y - turnLength * descentRate);
if (targetHeight == pos.y)
{
this.onGround = true;
if (targetHeight == cmpWaterManager.GetWaterLevel(pos.x, pos.z) && this.template.DiesInWater)
this.waterDeath = true;
}
}
}
else
{
// If we haven't reached max speed yet then we're still on the ground;
// otherwise we're taking off or flying
// this.onGround in case of a go-around after landing (but not fully stopped)
if (this.speed < this.template.TakeoffSpeed && this.onGround)
{
if (cmpGarrisonHolder)
cmpGarrisonHolder.AllowGarrisoning(false,"UnitMotionFlying");
this.pitch = 0;
// Accelerate forwards
this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
canTurn = false;
// Clamp to ground if below it, or descend if above
if (pos.y < ground)
pos.y = ground;
else if (pos.y > ground)
pos.y = Math.max(ground, pos.y - turnLength * this.template.ClimbRate);
}
else
{
this.onGround = false;
// Climb/sink to max height above ground
this.speed = Math.min(this.template.MaxSpeed, this.speed + turnLength * this.template.AccelRate);
var targetHeight = ground + (+this.template.FlyingHeight);
if (Math.abs(pos.y-targetHeight) > this.template.FlyingHeight/5)
{
this.pitch = Math.PI / 9;
canTurn = false;
}
else
this.pitch = 0;
if (pos.y < targetHeight)
pos.y = Math.min(targetHeight, pos.y + turnLength * this.template.ClimbRate);
else if (pos.y > targetHeight)
{
pos.y = Math.max(targetHeight, pos.y - turnLength * this.template.ClimbRate);
this.pitch = -1 * this.pitch;
}
}
}
// If we're in range of the target then tell people that we've reached it
// (TODO: quantisation breaks this)
var distFromTarget = Math.euclidDistance2D(pos.x, pos.z, this.targetX, this.targetZ);
if (!this.reachedTarget && this.targetMinRange <= distFromTarget && distFromTarget <= this.targetMaxRange)
{
this.reachedTarget = true;
Engine.PostMessage(this.entity, MT_MotionChanged, { "starting": false, "error": false });
}
// If we're facing away from the target, and are still fairly close to it,
// then carry on going straight so we overshoot in a straight line
var isBehindTarget = ((this.targetX - pos.x) * Math.sin(angle) + (this.targetZ - pos.z) * Math.cos(angle) < 0);
// Overshoot the target: carry on straight
if (isBehindTarget && distFromTarget < this.template.MaxSpeed * this.template.OvershootTime)
canTurn = false;
if (canTurn)
{
// Turn towards the target
var targetAngle = Math.atan2(this.targetX - pos.x, this.targetZ - pos.z);
var delta = targetAngle - angle;
// Wrap delta to -pi..pi
delta = (delta + Math.PI) % (2*Math.PI); // range -2pi..2pi
if (delta < 0) delta += 2*Math.PI; // range 0..2pi
delta -= Math.PI; // range -pi..pi
// Clamp to max rate
var deltaClamped = Math.min(Math.max(delta, -this.template.TurnRate * turnLength), this.template.TurnRate * turnLength);
// Calculate new orientation, in a peculiar way in order to make sure the
// result gets close to targetAngle (rather than being n*2*pi out)
newangle = targetAngle + deltaClamped - delta;
if (newangle - angle > Math.PI / 18)
this.roll = Math.PI / 9;
else if (newangle - angle < -Math.PI / 18)
this.roll = - Math.PI / 9;
else
this.roll = newangle - angle;
}
else
this.roll = 0;
pos.x += this.speed * turnLength * Math.sin(angle);
pos.z += this.speed * turnLength * Math.cos(angle);
cmpPosition.SetHeightFixed(pos.y);
cmpPosition.TurnTo(newangle);
cmpPosition.SetXZRotation(this.pitch, this.roll);
cmpPosition.MoveTo(pos.x, pos.z);
};
UnitMotionFlying.prototype.MoveToPointRange = function(x, z, minRange, maxRange)
{
this.hasTarget = true;
this.landing = false;
this.reachedTarget = false;
this.targetX = x;
this.targetZ = z;
this.targetMinRange = minRange;
this.targetMaxRange = maxRange;
return true;
};
UnitMotionFlying.prototype.MoveToTargetRange = function(target, minRange, maxRange)
{
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return false;
var targetPos = cmpTargetPosition.GetPosition2D();
this.hasTarget = true;
this.reachedTarget = false;
this.targetX = targetPos.x;
this.targetZ = targetPos.y;
this.targetMinRange = minRange;
this.targetMaxRange = maxRange;
return true;
};
UnitMotionFlying.prototype.GetWalkSpeed = function()
{
return +this.template.MaxSpeed;
};
UnitMotionFlying.prototype.SetSpeedMultiplier = function()
{
// ignore this, the speed is always the walk speed
};
UnitMotionFlying.prototype.GetRunMultiplier = function()
{
return 1;
};
UnitMotionFlying.prototype.GetCurrentSpeed = function()
{
return this.speed;
};
UnitMotionFlying.prototype.GetSpeedMultiplier = function()
{
return this.GetCurrentSpeed() / this.GetWalkSpeed();
-}
+};
UnitMotionFlying.prototype.GetPassabilityClassName = function()
{
return this.template.PassabilityClass;
};
UnitMotionFlying.prototype.GetPassabilityClass = function()
{
return this.passabilityClass;
};
UnitMotionFlying.prototype.FaceTowardsPoint = function(x, z)
{
// Ignore this - angle is controlled by the target-seeking code instead
};
UnitMotionFlying.prototype.SetFacePointAfterMove = function()
{
// Ignore this - angle is controlled by the target-seeking code instead
};
UnitMotionFlying.prototype.StopMoving = function()
{
//Invert
if (!this.waterDeath)
this.landing = !this.landing;
};
UnitMotionFlying.prototype.SetDebugOverlay = function(enabled)
{
};
Engine.RegisterComponentType(IID_UnitMotion, "UnitMotionFlying", UnitMotionFlying);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Math.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Math.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Math.js (revision 22419)
@@ -1,144 +1,144 @@
/**
* Tests for consistent and correct math results
*/
// +0 is different than -0, but standard equality won't test that
function isNegativeZero(z) { return z === 0 && 1/z === -Infinity; }
function isPositiveZero(z) { return z === 0 && 1/z === Infinity; }
// rounding
TS_ASSERT_EQUALS(0.1+0.2, 0.30000000000000004);
TS_ASSERT_EQUALS(0.1+0.7+0.3, 1.0999999999999999);
// cos
TS_ASSERT_EQUALS(Math.cos(Math.PI/2), 0);
TS_ASSERT_UNEVAL_EQUALS(Math.cos(NaN), NaN);
TS_ASSERT_EQUALS(Math.cos(0), 1);
TS_ASSERT_EQUALS(Math.cos(-0), 1);
TS_ASSERT_UNEVAL_EQUALS(Math.cos(Infinity), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.cos(-Infinity), NaN);
// sin
TS_ASSERT_EQUALS(Math.sin(Math.PI), 0);
TS_ASSERT_UNEVAL_EQUALS(Math.sin(NaN), NaN);
TS_ASSERT(isPositiveZero(Math.sin(0)));
// TS_ASSERT(isNegativeZero(Math.sin(-0))); TODO: doesn't match spec
TS_ASSERT_UNEVAL_EQUALS(Math.sin(Infinity), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.sin(-Infinity), NaN);
TS_ASSERT_EQUALS(Math.sin(1e-15), 7.771561172376096e-16);
// atan
TS_ASSERT_UNEVAL_EQUALS(Math.atan(NaN), NaN);
TS_ASSERT(isPositiveZero(Math.atan(0)));
TS_ASSERT(isNegativeZero(Math.atan(-0)));
TS_ASSERT_EQUALS(Math.atan(Infinity), Math.PI/2);
TS_ASSERT_EQUALS(Math.atan(-Infinity), -Math.PI/2);
TS_ASSERT_EQUALS(Math.atan(1e-310), 1.00000000003903e-310);
TS_ASSERT_EQUALS(Math.atan(100), 1.5607966601078411);
// atan2
TS_ASSERT_UNEVAL_EQUALS(Math.atan2(NaN, 1), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.atan2(1, NaN), NaN);
TS_ASSERT_EQUALS(Math.atan2(1, 0), Math.PI/2);
TS_ASSERT_EQUALS(Math.atan2(1, -0), Math.PI/2);
TS_ASSERT(isPositiveZero(Math.atan2(0, 1)));
TS_ASSERT(isPositiveZero(Math.atan2(0, 0)));
TS_ASSERT_EQUALS(Math.atan2(0, -0), Math.PI);
TS_ASSERT_EQUALS(Math.atan2(0, -1), Math.PI);
TS_ASSERT(isNegativeZero(Math.atan2(-0, 1)));
TS_ASSERT(isNegativeZero(Math.atan2(-0, 0)));
TS_ASSERT_EQUALS(Math.atan2(-0, -0), -Math.PI);
TS_ASSERT_EQUALS(Math.atan2(-0, -1), -Math.PI);
TS_ASSERT_EQUALS(Math.atan2(-1, 0), -Math.PI/2);
TS_ASSERT_EQUALS(Math.atan2(-1, -0), -Math.PI/2);
TS_ASSERT(isPositiveZero(Math.atan2(1.7e308, Infinity)));
TS_ASSERT_EQUALS(Math.atan2(1.7e308, -Infinity), Math.PI);
TS_ASSERT(isNegativeZero(Math.atan2(-1.7e308, Infinity)));
TS_ASSERT_EQUALS(Math.atan2(-1.7e308, -Infinity), -Math.PI);
TS_ASSERT_EQUALS(Math.atan2(Infinity, -1.7e308), Math.PI/2);
TS_ASSERT_EQUALS(Math.atan2(-Infinity, 1.7e308), -Math.PI/2);
TS_ASSERT_EQUALS(Math.atan2(Infinity, Infinity), Math.PI/4);
TS_ASSERT_EQUALS(Math.atan2(Infinity, -Infinity), 3*Math.PI/4);
TS_ASSERT_EQUALS(Math.atan2(-Infinity, Infinity), -Math.PI/4);
TS_ASSERT_EQUALS(Math.atan2(-Infinity, -Infinity), -3*Math.PI/4);
TS_ASSERT_EQUALS(Math.atan2(1e-310, 2), 5.0000000001954e-311);
// exp
TS_ASSERT_UNEVAL_EQUALS(Math.exp(NaN), NaN);
TS_ASSERT_EQUALS(Math.exp(0), 1);
TS_ASSERT_EQUALS(Math.exp(-0), 1);
TS_ASSERT_EQUALS(Math.exp(Infinity), Infinity);
TS_ASSERT(isPositiveZero(Math.exp(-Infinity)));
TS_ASSERT_EQUALS(Math.exp(10), 22026.465794806707);
// log
TS_ASSERT_UNEVAL_EQUALS(Math.log("NaN"), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.log(-1), NaN);
TS_ASSERT_EQUALS(Math.log(0), -Infinity);
TS_ASSERT_EQUALS(Math.log(-0), -Infinity);
TS_ASSERT(isPositiveZero(Math.log(1)));
TS_ASSERT_EQUALS(Math.log(Infinity), Infinity);
TS_ASSERT_EQUALS(Math.log(Math.E), 0.9999999999999991);
TS_ASSERT_EQUALS(Math.log(Math.E*Math.E*Math.E), 2.999999999999999);
// pow
TS_ASSERT_EQUALS(Math.pow(NaN, 0), 1);
TS_ASSERT_EQUALS(Math.pow(NaN, -0), 1);
TS_ASSERT_UNEVAL_EQUALS(Math.pow(NaN, 100), NaN);
TS_ASSERT_EQUALS(Math.pow(1.7e308, Infinity), Infinity);
TS_ASSERT_EQUALS(Math.pow(-1.7e308, Infinity), Infinity);
TS_ASSERT(isPositiveZero(Math.pow(1.7e308, -Infinity)));
TS_ASSERT(isPositiveZero(Math.pow(-1.7e308, -Infinity)));
TS_ASSERT_UNEVAL_EQUALS(Math.pow(1, Infinity), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.pow(-1, Infinity), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.pow(1, -Infinity), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.pow(-1, -Infinity), NaN);
TS_ASSERT(isPositiveZero(Math.pow(1e-310, Infinity)));
TS_ASSERT(isPositiveZero(Math.pow(-1e-310, Infinity)));
TS_ASSERT_EQUALS(Math.pow(1e-310, -Infinity), Infinity);
TS_ASSERT_EQUALS(Math.pow(-1e-310, -Infinity), Infinity);
TS_ASSERT_EQUALS(Math.pow(Infinity, 1e-310), Infinity);
TS_ASSERT(isPositiveZero(Math.pow(Infinity, -1e-310)));
TS_ASSERT_EQUALS(Math.pow(-Infinity, 101), -Infinity);
TS_ASSERT_EQUALS(Math.pow(-Infinity, 1.7e308), Infinity);
TS_ASSERT(isNegativeZero(Math.pow(-Infinity, -101)));
TS_ASSERT(isPositiveZero(Math.pow(-Infinity, -1.7e308)));
TS_ASSERT(isPositiveZero(Math.pow(0, 1e-310)));
TS_ASSERT_EQUALS(Math.pow(0, -1e-310), Infinity);
TS_ASSERT(isNegativeZero(Math.pow(-0, 101)));
TS_ASSERT(isPositiveZero(Math.pow(-0, 1e-310)));
TS_ASSERT_EQUALS(Math.pow(-0, -101), -Infinity);
TS_ASSERT_EQUALS(Math.pow(-0, -1e-310), Infinity);
TS_ASSERT_UNEVAL_EQUALS(Math.pow(-1.7e308, 1e-310), NaN);
TS_ASSERT_EQUALS(Math.pow(Math.PI, -100), 1.9275814160560185e-50);
// sqrt
TS_ASSERT_UNEVAL_EQUALS(Math.sqrt(NaN), NaN);
TS_ASSERT_UNEVAL_EQUALS(Math.sqrt(-1e-323), NaN);
TS_ASSERT(isPositiveZero(Math.sqrt(0)));
TS_ASSERT(isNegativeZero(Math.sqrt(-0)));
TS_ASSERT_EQUALS(Math.sqrt(Infinity), Infinity);
TS_ASSERT_EQUALS(Math.sqrt(1e-323), 3.1434555694052576e-162);
// square
TS_ASSERT_UNEVAL_EQUALS(Math.square(NaN), NaN);
TS_ASSERT(isPositiveZero(Math.square(0)));
TS_ASSERT(isPositiveZero(Math.square(-0)));
TS_ASSERT_EQUALS(Math.square(Infinity), Infinity);
TS_ASSERT_EQUALS(Math.square(1.772979291871526e-81),3.143455569405258e-162);
-TS_ASSERT_EQUALS(Math.square(1e+155), Infinity)
+TS_ASSERT_EQUALS(Math.square(1e+155), Infinity);
TS_ASSERT_UNEVAL_EQUALS(Math.square(1), 1);
TS_ASSERT_UNEVAL_EQUALS(Math.square(20), 400);
TS_ASSERT_UNEVAL_EQUALS(Math.square(300), 90000);
TS_ASSERT_UNEVAL_EQUALS(Math.square(4000), 16000000);
TS_ASSERT_UNEVAL_EQUALS(Math.square(50000), 2500000000);
TS_ASSERT_UNEVAL_EQUALS(Math.square(-3), 9);
TS_ASSERT_UNEVAL_EQUALS(Math.square(-8), 64);
TS_ASSERT_UNEVAL_EQUALS(Math.square(0.123), 0.015129);
// euclid distance
TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance2D(0, 0, 3, 4), 5);
TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance2D(-4, -4, -5, -4), 1);
TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance2D(1e+140, 1e+140, 0, 0), 1.414213562373095e+140);
TS_ASSERT_UNEVAL_EQUALS(Math.euclidDistance3D(0, 0, 0, 20, 48, 165), 173);
Index: ps/trunk/binaries/data/tests/test_setup.js
===================================================================
--- ps/trunk/binaries/data/tests/test_setup.js (revision 22418)
+++ ps/trunk/binaries/data/tests/test_setup.js (revision 22419)
@@ -1,62 +1,62 @@
// The engine provides:
// Engine.TS_FAIL = function(msg) { ... }
//
// This file define global helper functions for common test assertions.
// (Functions should be defined with local names too, so they show up properly
// in stack traces.)
function fail(msg)
{
// Get a list of callers
let trace = (new Error).stack.split("\n");
// Remove the Error ctor and this function from the stack
trace = trace.splice(2);
trace = "Stack trace:\n" + trace.join("\n");
Engine.TS_FAIL(trace + msg);
}
global.TS_FAIL = function TS_FAIL(msg)
{
fail(msg);
-}
+};
global.TS_ASSERT = function TS_ASSERT(e)
{
if (!e)
fail("Assert failed");
-}
+};
global.TS_ASSERT_EQUALS = function TS_ASSERT_EQUALS(x, y)
{
if (!(x === y))
fail("Expected equal, got "+uneval(x)+" !== "+uneval(y));
-}
+};
global.TS_ASSERT_EQUALS_APPROX = function TS_ASSERT_EQUALS_APPROX(x, y, maxDifference)
{
TS_ASSERT_NUMBER(maxDifference);
if (Math.abs(x - y) > maxDifference)
fail("Expected almost equal, got " + uneval(x) + " !== " + uneval(y));
-}
+};
global.TS_ASSERT_UNEVAL_EQUALS = function TS_ASSERT_UNEVAL_EQUALS(x, y)
{
if (!(uneval(x) === uneval(y)))
fail("Expected equal, got "+uneval(x)+" !== "+uneval(y));
-}
+};
global.TS_ASSERT_EXCEPTION = function(func)
{
try {
func();
Engine.TS_FAIL("Missed exception at:\n" + new Error().stack);
} catch (e) {
}
-}
+};
global.TS_ASSERT_NUMBER = function(value)
{
if (typeof value != "number" || !isFinite(value))
fail("The given value must be a real number!");
-}
+};
Index: ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js
===================================================================
--- ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js (revision 22418)
+++ ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js (revision 22419)
@@ -1,90 +1,90 @@
function TestScript1_values() {}
TestScript1_values.prototype.Init = function() {
this.x = +this.template.x;
this.str = "this is a string";
this.things = { a: 1, b: "2", c: [3, "4", [5, []]] };
};
TestScript1_values.prototype.GetX = function() {
// print(uneval(this));
return this.x;
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_values", TestScript1_values);
// -------- //
function TestScript1_entity() {}
TestScript1_entity.prototype.GetX = function() {
// Test that .entity is readonly
try {
delete this.entity;
Engine.TS_FAIL("Missed exception");
} catch (e) { }
try {
this.entity = -1;
Engine.TS_FAIL("Missed exception");
} catch (e) { }
// and return the value
return this.entity;
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_entity", TestScript1_entity);
// -------- //
function TestScript1_nontree() {}
TestScript1_nontree.prototype.Init = function() {
var n = [1];
this.x = [n, n, null, { y: n }];
this.x[2] = this.x;
};
TestScript1_nontree.prototype.GetX = function() {
// print(uneval(this)+"\n");
this.x[0][0] += 1;
return this.x[0][0] + this.x[1][0] + this.x[2][0][0] + this.x[3].y[0];
-}
+};
Engine.RegisterComponentType(IID_Test1, "TestScript1_nontree", TestScript1_nontree);
// -------- //
function TestScript1_custom() {}
TestScript1_custom.prototype.Init = function() {
this.y = 2;
};
TestScript1_custom.prototype.Serialize = function() {
return {c:1};
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_custom", TestScript1_custom);
// -------- //
function TestScript1_getter() {}
TestScript1_getter.prototype.Init = function() {
this.x = 100;
this.__defineGetter__('x', function () { print("FAIL\n"); die(); return 200; });
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_getter", TestScript1_getter);
// -------- //
function TestScript1_consts() {}
TestScript1_consts.prototype.Schema = "";
TestScript1_consts.prototype.GetX = function() {
return (+this.entity) + (+this.template.x);
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_consts", TestScript1_consts);
Index: ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js (revision 22418)
+++ ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js (revision 22419)
@@ -1,61 +1,61 @@
// We want to pass callback functions for the different buttons in a convenient way.
// Because passing functions accross compartment boundaries is a pain, we just store them here together with some optional arguments.
// The messageBox page will return the code of the pressed button and the according function will be called.
var g_MessageBoxBtnFunctions = [];
var g_MessageBoxCallbackArgs = [];
function messageBoxCallbackFunction(btnCode)
{
if (btnCode !== undefined && g_MessageBoxBtnFunctions[btnCode])
{
// Cache the variables to make it possible to call a messageBox from a callback function.
let callbackFunction = g_MessageBoxBtnFunctions[btnCode];
let callbackArgs = g_MessageBoxCallbackArgs[btnCode];
g_MessageBoxBtnFunctions = [];
g_MessageBoxCallbackArgs = [];
if (callbackArgs !== undefined)
callbackFunction(callbackArgs);
else
callbackFunction();
return;
}
g_MessageBoxBtnFunctions = [];
g_MessageBoxCallbackArgs = [];
-};
+}
function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs)
{
if (g_MessageBoxBtnFunctions && g_MessageBoxBtnFunctions.length)
{
warn("A messagebox was called when a previous callback function is still set, aborting!");
return;
}
g_MessageBoxBtnFunctions = mbBtnCode;
g_MessageBoxCallbackArgs = mbCallbackArgs || g_MessageBoxCallbackArgs;
Engine.PushGuiPage("page_msgbox.xml", {
"width": mbWidth,
"height": mbHeight,
"message": mbMessage,
"title": mbTitle,
"buttonCaptions": mbButtonCaptions,
"callback": mbBtnCode && "messageBoxCallbackFunction"
});
}
function openURL(url)
{
Engine.OpenURL(url);
messageBox(
600, 200,
sprintf(
translate("Opening %(url)s\n in default web browser. Please wait…"),
{ "url": url }
),
translate("Opening page"));
}
Index: ps/trunk/binaries/data/mods/mod/gui/termsdialog/termsdialog.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/termsdialog/termsdialog.js (revision 22418)
+++ ps/trunk/binaries/data/mods/mod/gui/termsdialog/termsdialog.js (revision 22419)
@@ -1,89 +1,89 @@
/**
* This implements a basic "Clickwrap agreement", which is an industry standard:
*
* The European Court of Justice decided in the case El Majdoub (case nr C-322/14) that click-wrap agreements are acceptable under certain circumstances
* as proof of the acceptance of terms and conditions (in the meaning of Regulation 44/2001, now replaced by Regulation 1215/2012).
* See https://eur-lex.europa.eu/legal-content/en/TXT/HTML/?uri=uriserv%3AOJ.C_.2015.236.01.0019.01.ENG
* The user should be able to save and print the text of the terms.
*/
var g_TermsPage;
var g_TermsFile;
var g_TermsSprintf;
function init(data)
{
g_TermsPage = data.page;
g_TermsFile = data.file;
g_TermsSprintf = data.sprintf;
Engine.GetGUIObjectByName("title").caption = data.title;
initURLButtons(data.termsURL, data.urlButtons);
initLanguageSelection();
}
function initURLButtons(termsURL, urlButtons)
{
if (termsURL)
urlButtons.unshift({
// Translation: Label of a button that when pressed opens the Terms and Conditions in the default webbrowser.
"caption": translate("View online"),
"url": termsURL
});
urlButtons.forEach((urlButton, i) => {
let button = Engine.GetGUIObjectByName("button[" + i + "]");
button.caption = urlButton.caption;
button.hidden = false;
button.tooltip = sprintf(translate("Open %(url)s in the browser."), {
"url": urlButton.url
});
button.onPress = () => {
openURL(urlButton.url);
};
});
}
function initLanguageSelection()
{
let languageLabel = Engine.GetGUIObjectByName("languageLabel");
- let languageLabelWidth = Engine.GetTextWidth(languageLabel.font, languageLabel.caption)
+ let languageLabelWidth = Engine.GetTextWidth(languageLabel.font, languageLabel.caption);
languageLabel.size = "0 0 " + languageLabelWidth + " 100%";
let languageDropdown = Engine.GetGUIObjectByName("languageDropdown");
languageDropdown.size = (languageLabelWidth + 10) + " 4 100% 100%";
languageDropdown.list = (() => {
let displayNames = Engine.GetSupportedLocaleDisplayNames();
let baseNames = Engine.GetSupportedLocaleBaseNames();
// en-US
let list = [displayNames[0]];
// current locale
let currentLocaleDict = Engine.GetFallbackToAvailableDictLocale(Engine.GetCurrentLocale());
if (currentLocaleDict != baseNames[0])
list.push(displayNames[baseNames.indexOf(currentLocaleDict)]);
return list;
})();
languageDropdown.onSelectionChange = () => {
Engine.GetGUIObjectByName("mainText").caption =
sprintf(
languageDropdown.selected == 1 ?
Engine.TranslateLines(Engine.ReadFile(g_TermsFile)) :
Engine.ReadFile(g_TermsFile),
g_TermsSprintf);
};
languageDropdown.selected = languageDropdown.list.length - 1;
}
function closeTerms(accepted)
{
Engine.PopGuiPageCB({
"page": g_TermsPage,
"accepted": accepted
});
}
Index: ps/trunk/binaries/data/mods/mod/hwdetect/test.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/hwdetect/test.js (revision 22418)
+++ ps/trunk/binaries/data/mods/mod/hwdetect/test.js (revision 22419)
@@ -1,43 +1,43 @@
// Run in a standalone JS shell like
// js -e 'var global={}' -f hwdetect.js -f test_data.js -f test.js > output.html
// where test_data.js is a giant file that's probably not publicly available yet
// (ask Philip if you want a copy), then look at output.html and make sure it's
// applying the hwdetected settings in the appropriate cases
print("");
print("");
print("");
print("
");
print("");
print("OS");
print(" | GL_RENDERER");
print(" | Output");
print(" | Warnings");
hwdetectTestData.sort(function(a, b) {
if (a.GL_RENDERER < b.GL_RENDERER)
return -1;
if (b.GL_RENDERER < a.GL_RENDERER)
return +1;
return 0;
});
for (var settings of hwdetectTestData)
{
var output = RunDetection(settings);
var os = (settings.os_linux ? "linux" : settings.os_macosx ? "macosx" : settings.os_win ? "win" : "???");
var disabled = [];
for (var d of ["disable_audio", "disable_s3tc", "disable_shadows", "disable_shadowpcf", "disable_allwater", "disable_fancywater", "override_renderpath"])
if (output[d] !== undefined)
- disabled.push(d+"="+output[d])
+ disabled.push(d+"="+output[d]);
print(" |
");
print("" + os);
print(" | " + settings.GL_RENDERER);
print(" | " + disabled.join(" "));
print(" | " + output.warnings.concat(output.dialog_warnings).join("\n"));
}
print(" |
");
Index: ps/trunk/binaries/data/mods/public/gui/session/menu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/gui/session/menu.js (revision 22419)
@@ -1,1255 +1,1255 @@
// Menu / panel border size
var MARGIN = 4;
// Includes the main menu button
const NUM_BUTTONS = 10;
// Regular menu buttons
var BUTTON_HEIGHT = 32;
// The position where the bottom of the menu will end up (currently 228)
const END_MENU_POSITION = (BUTTON_HEIGHT * NUM_BUTTONS) + MARGIN;
// Menu starting position: bottom
const MENU_BOTTOM = 0;
// Menu starting position: top
const MENU_TOP = MENU_BOTTOM - END_MENU_POSITION;
// Number of pixels per millisecond to move
var MENU_SPEED = 1.2;
// Trade menu: step for probability changes
var STEP = 5;
// Shown in the trade dialog.
var g_IdleTraderTextColor = "orange";
/**
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
"code": "",
"page": "page_structree.xml"
};
/**
* The barter constants should match with the simulation
* Quantity of goods to sell per click.
*/
const g_BarterResourceSellQuantity = 100;
/**
* Multiplier to be applied when holding the massbarter hotkey.
*/
const g_BarterMultiplier = 5;
/**
* Barter actions, as mapped to the names of GUI Buttons.
*/
const g_BarterActions = ["Buy", "Sell"];
/**
* Currently selected resource type to sell in the barter GUI.
*/
var g_BarterSell;
var g_IsMenuOpen = false;
var g_IsDiplomacyOpen = false;
var g_IsTradeOpen = false;
var g_IsObjectivesOpen = false;
/**
* Used to disable a specific bribe button for the time we are waiting for the result of the bribe after it was clicked.
* It contains an array per viewedPlayer. This array is a list of the players that were bribed.
*/
var g_BribeButtonsWaiting = {};
/**
* Remember last viewed summary panel and charts.
*/
var g_SummarySelectedData;
// Redefined every time someone makes a tribute (so we can save some data in a closure). Called in input.js handleInputBeforeGui.
var g_FlushTributing = function() {};
function initMenu()
{
Engine.GetGUIObjectByName("menu").size = "100%-164 " + MENU_TOP + " 100% " + MENU_BOTTOM;
// TODO: Atlas should pass g_GameAttributes.settings
for (let button of ["menuExitButton", "summaryButton", "objectivesButton", "diplomacyButton"])
Engine.GetGUIObjectByName(button).enabled = !Engine.IsAtlasRunning();
}
function updateMenuPosition(dt)
{
let menu = Engine.GetGUIObjectByName("menu");
let maxOffset = g_IsMenuOpen ?
END_MENU_POSITION - menu.size.bottom :
menu.size.top - MENU_TOP;
if (maxOffset <= 0)
return;
let offset = Math.min(MENU_SPEED * dt, maxOffset) * (g_IsMenuOpen ? +1 : -1);
let size = menu.size;
size.top += offset;
size.bottom += offset;
menu.size = size;
}
// Opens the menu by revealing the screen which contains the menu
function openMenu()
{
g_IsMenuOpen = true;
}
// Closes the menu and resets position
function closeMenu()
{
g_IsMenuOpen = false;
}
function toggleMenu()
{
g_IsMenuOpen = !g_IsMenuOpen;
}
function optionsMenuButton()
{
closeOpenDialogs();
openOptions();
}
function lobbyDialogButton()
{
if (!Engine.HasXmppClient())
return;
closeOpenDialogs();
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
}
function chatMenuButton()
{
closeOpenDialogs();
openChat();
}
function diplomacyMenuButton()
{
closeOpenDialogs();
openDiplomacy();
}
function pauseMenuButton()
{
togglePause();
}
function resignMenuButton()
{
closeOpenDialogs();
pauseGame();
messageBox(
400, 200,
translate("Are you sure you want to resign?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[resumeGame, resignGame]
);
}
function exitMenuButton()
{
closeOpenDialogs();
pauseGame();
let messageTypes = {
"host": {
"caption": translate("Are you sure you want to quit? Leaving will disconnect all other players."),
"buttons": [resumeGame, leaveGame]
},
"client": {
"caption": translate("Are you sure you want to quit?"),
"buttons": [resumeGame, resignQuestion]
},
"singleplayer": {
"caption": translate("Are you sure you want to quit?"),
"buttons": [resumeGame, leaveGame]
}
};
let messageType = g_IsNetworked && g_IsController ? "host" :
(g_IsNetworked && !g_IsObserver ? "client" : "singleplayer");
messageBox(
400, 200,
messageTypes[messageType].caption,
translate("Confirmation"),
[translate("No"), translate("Yes")],
messageTypes[messageType].buttons
);
}
function resignQuestion()
{
messageBox(
400, 200,
translate("Do you want to resign or will you return soon?"),
translate("Confirmation"),
[translate("I will return"), translate("I resign")],
[leaveGame, resignGame],
[true, false]
);
}
function openDeleteDialog(selection)
{
closeOpenDialogs();
let deleteSelectedEntities = function(selectionArg)
{
Engine.PostNetworkCommand({
"type": "delete-entities",
"entities": selectionArg
});
};
messageBox(
400, 200,
translate("Destroy everything currently selected?"),
translate("Delete"),
[translate("No"), translate("Yes")],
[resumeGame, deleteSelectedEntities],
[null, selection]
);
}
function openSave()
{
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_savegame.xml", {
"savedGameData": getSavedGameData(),
"callback": "resumeGame"
});
}
function openOptions()
{
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_options.xml", {
"callback": "optionsPageClosed"
});
}
function optionsPageClosed(data)
{
for (let callback of data)
if (global[callback])
global[callback]();
resumeGame();
}
function openChat(command = "")
{
if (g_Disconnected)
return;
closeOpenDialogs();
let chatAddressee = Engine.GetGUIObjectByName("chatAddressee");
chatAddressee.selected = chatAddressee.list_data.indexOf(command);
Engine.GetGUIObjectByName("chatInput").focus();
Engine.GetGUIObjectByName("chatDialogPanel").hidden = false;
updateChatHistory();
}
function closeChat()
{
Engine.GetGUIObjectByName("chatInput").caption = "";
Engine.GetGUIObjectByName("chatInput").blur(); // Remove focus
Engine.GetGUIObjectByName("chatDialogPanel").hidden = true;
}
function resizeDiplomacyDialog()
{
let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel");
let size = dialog.size;
let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size;
let widthOffset = g_ResourceData.GetCodes().length * (tribSize.right - tribSize.left) / 2;
size.left -= widthOffset;
size.right += widthOffset;
let firstRow = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
let heightOffset = (g_Players.length - 1) * (firstRow.bottom - firstRow.top) / 2;
size.top -= heightOffset;
size.bottom += heightOffset;
dialog.size = size;
}
function initChatWindow()
{
let filters = prepareForDropdown(g_ChatHistoryFilters);
let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
chatHistoryFilter.list = filters.text;
chatHistoryFilter.list_data = filters.key;
chatHistoryFilter.selected = 0;
Engine.GetGUIObjectByName("extendedChat").checked =
Engine.ConfigDB_GetValue("user", "chat.session.extended") == "true";
resizeChatWindow();
}
function resizeChatWindow()
{
// Hide/show the panel
let chatHistoryPage = Engine.GetGUIObjectByName("chatHistoryPage");
let extended = Engine.GetGUIObjectByName("extendedChat").checked;
chatHistoryPage.hidden = !extended;
// Resize the window
let chatDialogPanel = Engine.GetGUIObjectByName("chatDialogPanel");
if (extended)
{
chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelLarge").size;
// Adjust the width so that the chat history is in the golden ratio
let chatHistory = Engine.GetGUIObjectByName("chatHistory");
let height = chatHistory.getComputedSize().bottom - chatHistory.getComputedSize().top;
let width = (1 + Math.sqrt(5)) / 2 * height;
let size = chatDialogPanel.size;
size.left = -width / 2 - chatHistory.size.left;
size.right = width / 2 + chatHistory.size.left;
chatDialogPanel.size = size;
}
else
chatDialogPanel.size = Engine.GetGUIObjectByName("chatDialogPanelSmall").size;
}
function updateChatHistory()
{
if (Engine.GetGUIObjectByName("chatDialogPanel").hidden ||
!Engine.GetGUIObjectByName("extendedChat").checked)
return;
let chatHistoryFilter = Engine.GetGUIObjectByName("chatHistoryFilter");
let selected = chatHistoryFilter.list_data[chatHistoryFilter.selected];
Engine.GetGUIObjectByName("chatHistory").caption =
g_ChatHistory.filter(msg => msg.filter[selected]).map(msg =>
Engine.ConfigDB_GetValue("user", "chat.timestamp") == "true" ?
sprintf(translate("%(time)s %(message)s"), {
"time": msg.timePrefix,
"message": msg.txt
}) :
msg.txt
).join("\n");
}
function onToggleChatWindowExtended()
{
saveSettingAndWriteToUserConfig("chat.session.extended", String(Engine.GetGUIObjectByName("extendedChat").checked));
resizeChatWindow();
Engine.GetGUIObjectByName("chatInput").focus();
}
function openDiplomacy()
{
closeOpenDialogs();
if (g_ViewedPlayer < 1)
return;
g_IsDiplomacyOpen = true;
updateDiplomacy(true);
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = false;
}
function closeDiplomacy()
{
g_IsDiplomacyOpen = false;
Engine.GetGUIObjectByName("diplomacyDialogPanel").hidden = true;
}
function toggleDiplomacy()
{
let open = g_IsDiplomacyOpen;
closeOpenDialogs();
if (!open)
openDiplomacy();
}
function updateDiplomacy(opening = false)
{
if (g_ViewedPlayer < 1 || !g_IsDiplomacyOpen)
return;
let simState = GetSimState();
let isCeasefireActive = simState.ceasefireActive;
let hasSharedLos = GetSimState().players[g_ViewedPlayer].hasSharedLos;
// Get offset for one line
let onesize = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size;
let rowsize = onesize.bottom - onesize.top;
// We don't include gaia
for (let i = 1; i < g_Players.length; ++i)
{
let myself = i == g_ViewedPlayer;
let playerInactive = isPlayerObserver(g_ViewedPlayer) || isPlayerObserver(i);
let hasAllies = g_Players.filter(player => player.isMutualAlly[g_ViewedPlayer]).length > 1;
diplomacySetupTexts(i, rowsize);
diplomacyFormatStanceButtons(i, myself || playerInactive || isCeasefireActive || g_Players[g_ViewedPlayer].teamsLocked);
// Tribute buttons do not need to be updated onTick, and should not because of massTributing
if (opening)
diplomacyFormatTributeButtons(i, myself || playerInactive);
diplomacyFormatAttackRequestButton(i, myself || playerInactive || isCeasefireActive || !hasAllies || !g_Players[i].isEnemy[g_ViewedPlayer]);
diplomacyFormatSpyRequestButton(i, myself || playerInactive || g_Players[i].isMutualAlly[g_ViewedPlayer] && hasSharedLos);
}
let diplomacyCeasefireCounter = Engine.GetGUIObjectByName("diplomacyCeasefireCounter");
diplomacyCeasefireCounter.caption = sprintf(
translateWithContext("ceasefire", "Remaining ceasefire time: %(time)s."),
{ "time": timeToString(simState.ceasefireTimeRemaining) }
);
diplomacyCeasefireCounter.hidden = !isCeasefireActive;
}
function diplomacySetupTexts(i, rowsize)
{
// Apply offset
let row = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]");
let size = row.size;
size.top = rowsize * (i - 1);
size.bottom = rowsize * i;
row.size = size;
row.hidden = false;
row.sprite = "color:" + rgbToGuiColor(g_DisplayedPlayerColors[i], 32);
setOutcomeIcon(g_Players[i].state, "diplomacyPlayerOutcome[" + (i - 1) + "]");
let diplomacyPlayerName = Engine.GetGUIObjectByName("diplomacyPlayerName[" + (i - 1) + "]");
diplomacyPlayerName.caption = colorizePlayernameByID(i);
diplomacyPlayerName.tooltip = translateAISettings(g_GameAttributes.settings.PlayerData[i]);
Engine.GetGUIObjectByName("diplomacyPlayerCiv[" + (i - 1) + "]").caption = g_CivData[g_Players[i].civ].Name;
Engine.GetGUIObjectByName("diplomacyPlayerTeam[" + (i - 1) + "]").caption =
g_Players[i].team < 0 ? translateWithContext("team", "None") : g_Players[i].team + 1;
Engine.GetGUIObjectByName("diplomacyPlayerTheirs[" + (i - 1) + "]").caption =
i == g_ViewedPlayer ? "" :
g_Players[i].isAlly[g_ViewedPlayer] ?
translate("Ally") :
g_Players[i].isNeutral[g_ViewedPlayer] ? translate("Neutral") : translate("Enemy");
}
function diplomacyFormatStanceButtons(i, hidden)
{
for (let stance of ["Ally", "Neutral", "Enemy"])
{
let button = Engine.GetGUIObjectByName("diplomacyPlayer" + stance + "[" + (i - 1) + "]");
button.hidden = hidden;
if (hidden)
continue;
let isCurrentStance = g_Players[g_ViewedPlayer]["is" + stance][i];
button.caption = isCurrentStance ? translate("x") : "";
button.enabled = controlsPlayer(g_ViewedPlayer) && !isCurrentStance;
button.onPress = (function(player, stance) { return function() {
Engine.PostNetworkCommand({
"type": "diplomacy",
"player": i,
"to": stance.toLowerCase()
});
}; })(i, stance);
}
}
function diplomacyFormatTributeButtons(i, hidden)
{
let resCodes = g_ResourceData.GetCodes();
let r = 0;
for (let resCode of resCodes)
{
let button = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]");
if (!button)
{
warn("Current GUI limits prevent displaying more than " + r + " tribute buttons!");
break;
}
Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]_image").sprite = "stretched:session/icons/resources/" + resCode + ".png";
button.hidden = hidden;
setPanelObjectPosition(button, r, r + 1, 0);
++r;
if (hidden)
continue;
button.enabled = controlsPlayer(g_ViewedPlayer);
button.tooltip = formatTributeTooltip(i, resCode, 100);
button.onPress = (function(i, resCode, button) {
// Shift+click to send 500, shift+click+click to send 1000, etc.
// See INPUT_MASSTRIBUTING in input.js
let multiplier = 1;
return function() {
let isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute");
if (isBatchTrainPressed)
{
inputState = INPUT_MASSTRIBUTING;
multiplier += multiplier == 1 ? 4 : 5;
}
let amounts = {};
for (let res of resCodes)
amounts[res] = 0;
amounts[resCode] = 100 * multiplier;
button.tooltip = formatTributeTooltip(i, resCode, amounts[resCode]);
// This is in a closure so that we have access to `player`, `amounts`, and `multiplier` without some
// evil global variable hackery.
g_FlushTributing = function() {
Engine.PostNetworkCommand({ "type": "tribute", "player": i, "amounts": amounts });
multiplier = 1;
button.tooltip = formatTributeTooltip(i, resCode, 100);
};
if (!isBatchTrainPressed)
g_FlushTributing();
};
})(i, resCode, button);
}
}
function diplomacyFormatAttackRequestButton(i, hidden)
{
let button = Engine.GetGUIObjectByName("diplomacyAttackRequest[" + (i - 1) + "]");
button.hidden = hidden;
if (hidden)
return;
button.enabled = controlsPlayer(g_ViewedPlayer);
button.tooltip = translate("Request your allies to attack this enemy");
button.onPress = (function(i) { return function() {
Engine.PostNetworkCommand({ "type": "attack-request", "source": g_ViewedPlayer, "player": i });
}; })(i);
}
function diplomacyFormatSpyRequestButton(i, hidden)
{
let button = Engine.GetGUIObjectByName("diplomacySpyRequest[" + (i - 1) + "]");
let template = GetTemplateData("special/spy");
button.hidden = hidden || !template || GetSimState().players[g_ViewedPlayer].disabledTemplates["special/spy"];
if (button.hidden)
return;
button.enabled = controlsPlayer(g_ViewedPlayer) &&
!(g_BribeButtonsWaiting[g_ViewedPlayer] && g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) != -1);
let modifier = "";
let tooltips = [translate("Bribe a random unit from this player and share its vision during a limited period.")];
if (!button.enabled)
modifier = "color:0 0 0 127:grayscale:";
else
{
if (template.requiredTechnology)
{
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": g_ViewedPlayer
});
if (!technologyEnabled)
{
modifier = "color:0 0 0 127:grayscale:";
button.enabled = false;
tooltips.push(getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[g_ViewedPlayer].civ));
}
}
if (template.cost)
{
let modifiedTemplate = clone(template);
for (let res in template.cost)
modifiedTemplate.cost[res] = Math.floor(GetSimState().players[i].spyCostMultiplier * template.cost[res]);
tooltips.push(getEntityCostTooltip(modifiedTemplate));
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": modifiedTemplate.cost,
"player": g_ViewedPlayer
});
let costRatio = Engine.GetTemplate("special/spy").VisionSharing.FailureCostRatio;
if (costRatio > 0)
{
tooltips.push(translate("A failed bribe will cost you:"));
for (let res in modifiedTemplate.cost)
modifiedTemplate.cost[res] = Math.floor(costRatio * modifiedTemplate.cost[res]);
tooltips.push(getEntityCostTooltip(modifiedTemplate));
}
if (neededResources)
{
if (button.enabled)
modifier = resourcesToAlphaMask(neededResources) + ":";
button.enabled = false;
tooltips.push(getNeededResourcesTooltip(neededResources));
}
}
}
let icon = Engine.GetGUIObjectByName("diplomacySpyRequestImage[" + (i - 1) + "]");
icon.sprite = modifier + "stretched:session/icons/bribes.png";
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (function(i, button) { return function() {
Engine.PostNetworkCommand({ "type": "spy-request", "source": g_ViewedPlayer, "player": i });
if (!g_BribeButtonsWaiting[g_ViewedPlayer])
g_BribeButtonsWaiting[g_ViewedPlayer] = [];
// Don't push i twice
if (g_BribeButtonsWaiting[g_ViewedPlayer].indexOf(i) == -1)
g_BribeButtonsWaiting[g_ViewedPlayer].push(i);
diplomacyFormatSpyRequestButton(i, false);
}; })(i, button);
}
function resizeTradeDialog()
{
let dialog = Engine.GetGUIObjectByName("tradeDialogPanel");
let size = dialog.size;
let width = size.right - size.left;
let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size;
width += g_ResourceData.GetCodes().length * (tradeSize.right - tradeSize.left);
size.left = -width / 2;
size.right = width / 2;
dialog.size = size;
}
function openTrade()
{
closeOpenDialogs();
if (g_ViewedPlayer < 1)
return;
g_IsTradeOpen = true;
let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer);
let button = {};
let resCodes = g_ResourceData.GetCodes();
let currTradeSelection = resCodes[0];
let updateTradeButtons = function()
{
for (let res in button)
{
button[res].label.caption = proba[res] + "%";
button[res].sel.hidden = !controlsPlayer(g_ViewedPlayer) || res != currTradeSelection;
button[res].up.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 100 || proba[currTradeSelection] == 0;
button[res].dn.hidden = !controlsPlayer(g_ViewedPlayer) || res == currTradeSelection || proba[res] == 0 || proba[currTradeSelection] == 100;
}
};
hideRemaining("tradeResources", resCodes.length);
Engine.GetGUIObjectByName("tradeHelp").hidden = false;
for (let i = 0; i < resCodes.length; ++i)
{
let resCode = resCodes[i];
let barterResource = Engine.GetGUIObjectByName("barterResource[" + i + "]");
if (!barterResource)
{
warn("Current GUI limits prevent displaying more than " + i + " resources in the barter dialog!");
break;
}
// Barter:
barterOpenCommon(resCode, i, "barter");
setPanelObjectPosition(barterResource, i, i + 1);
// Trade:
let tradeResource = Engine.GetGUIObjectByName("tradeResource[" + i + "]");
if (!tradeResource)
{
warn("Current GUI limits prevent displaying more than " + i + " resources in the trading goods selection dialog!");
break;
}
setPanelObjectPosition(tradeResource, i, i + 1);
let icon = Engine.GetGUIObjectByName("tradeResourceIcon[" + i + "]");
icon.sprite = "stretched:session/icons/resources/" + resCode + ".png";
let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp[" + i + "]");
let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn[" + i + "]");
button[resCode] = {
"up": buttonUp,
"dn": buttonDn,
"label": Engine.GetGUIObjectByName("tradeResourceText[" + i + "]"),
"sel": Engine.GetGUIObjectByName("tradeResourceSelection[" + i + "]")
};
proba[resCode] = proba[resCode] || 0;
let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton[" + i + "]");
buttonResource.enabled = controlsPlayer(g_ViewedPlayer);
buttonResource.onPress = (resource => {
return () => {
if (Engine.HotkeyIsPressed("session.fulltradeswap"))
{
for (let res of resCodes)
proba[res] = 0;
proba[resource] = 100;
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
}
currTradeSelection = resource;
updateTradeButtons();
};
})(resCode);
buttonUp.enabled = controlsPlayer(g_ViewedPlayer);
buttonUp.onPress = (resource => {
return () => {
proba[resource] += Math.min(STEP, proba[currTradeSelection]);
proba[currTradeSelection] -= Math.min(STEP, proba[currTradeSelection]);
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
updateTradeButtons();
};
})(resCode);
buttonDn.enabled = controlsPlayer(g_ViewedPlayer);
buttonDn.onPress = (resource => {
return () => {
proba[currTradeSelection] += Math.min(STEP, proba[resource]);
proba[resource] -= Math.min(STEP, proba[resource]);
Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba });
updateTradeButtons();
};
})(resCode);
}
updateTradeButtons();
updateTraderTexts();
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = false;
}
function updateTraderTexts()
{
let traderNumber = Engine.GuiInterfaceCall("GetTraderNumber", g_ViewedPlayer);
Engine.GetGUIObjectByName("traderCountText").caption = getIdleLandTradersText(traderNumber) + "\n\n" + getIdleShipTradersText(traderNumber);
}
function initBarterButtons()
{
g_BarterSell = g_ResourceData.GetCodes()[0];
}
/**
* Code common to both the Barter Panel and the Trade/Barter Dialog, that
* only needs to be run when the panel or dialog is opened by the player.
*
* @param {string} resourceCode
* @param {number} idx - Element index within its set
* @param {string} prefix - Common prefix of the gui elements to be worked upon
*/
function barterOpenCommon(resourceCode, idx, prefix)
{
let barterButton = {};
for (let action of g_BarterActions)
barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]");
let resource = resourceNameWithinSentence(resourceCode);
barterButton.Buy.tooltip = sprintf(translate("Buy %(resource)s"), { "resource": resource });
barterButton.Sell.tooltip = sprintf(translate("Sell %(resource)s"), { "resource": resource });
barterButton.Sell.onPress = function() {
g_BarterSell = resourceCode;
updateSelectionDetails();
updateBarterButtons();
};
}
/**
* Code common to both the Barter Panel and the Trade/Barter Dialog, that
* needs to be run on simulation update and when relevant hotkeys
* (i.e. massbarter) are pressed.
*
* @param {string} resourceCode
* @param {number} idx - Element index within its set
* @param {string} prefix - Common prefix of the gui elements to be worked upon
* @param {number} player
*/
function barterUpdateCommon(resourceCode, idx, prefix, player)
{
let barterButton = {};
let barterIcon = {};
let barterAmount = {};
for (let action of g_BarterActions)
{
barterButton[action] = Engine.GetGUIObjectByName(prefix + action + "Button[" + idx + "]");
barterIcon[action] = Engine.GetGUIObjectByName(prefix + action + "Icon[" + idx + "]");
barterAmount[action] = Engine.GetGUIObjectByName(prefix + action + "Amount[" + idx + "]");
}
let selectionIcon = Engine.GetGUIObjectByName(prefix + "SellSelection[" + idx + "]");
let amountToSell = g_BarterResourceSellQuantity;
if (Engine.HotkeyIsPressed("session.massbarter"))
amountToSell *= g_BarterMultiplier;
let isSelected = resourceCode == g_BarterSell;
let grayscale = isSelected ? "color:0 0 0 100:grayscale:" : "";
// Select color of the sell button
let neededRes = {};
neededRes[resourceCode] = amountToSell;
let canSellCurrent = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": player
}) ? "color:255 0 0 80:" : "";
// Select color of the buy button
neededRes = {};
neededRes[g_BarterSell] = amountToSell;
let canBuyAny = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": neededRes,
"player": player
}) ? "color:255 0 0 80:" : "";
barterIcon.Sell.sprite = canSellCurrent + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png";
barterIcon.Buy.sprite = canBuyAny + "stretched:" + grayscale + "session/icons/resources/" + resourceCode + ".png";
barterAmount.Sell.caption = "-" + amountToSell;
let prices = GetSimState().players[player].barterPrices;
barterAmount.Buy.caption = "+" + Math.round(prices.sell[g_BarterSell] / prices.buy[resourceCode] * amountToSell);
barterButton.Buy.onPress = function() {
Engine.PostNetworkCommand({
"type": "barter",
"sell": g_BarterSell,
"buy": resourceCode,
"amount": amountToSell
});
};
barterButton.Buy.hidden = isSelected;
barterButton.Buy.enabled = controlsPlayer(player);
barterButton.Sell.hidden = false;
selectionIcon.hidden = !isSelected;
}
function updateBarterButtons()
{
let playerState = GetSimState().players[g_ViewedPlayer];
if (!playerState)
return;
let canBarter = playerState.canBarter;
Engine.GetGUIObjectByName("barterNoMarketsMessage").hidden = canBarter;
Engine.GetGUIObjectByName("barterResources").hidden = !canBarter;
Engine.GetGUIObjectByName("barterHelp").hidden = !canBarter;
if (canBarter)
- g_ResourceData.GetCodes().forEach((resCode, i) => { barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer) });
+ g_ResourceData.GetCodes().forEach((resCode, i) => { barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer); });
}
function getIdleLandTradersText(traderNumber)
{
let active = traderNumber.landTrader.trading;
let garrisoned = traderNumber.landTrader.garrisoned;
let inactive = traderNumber.landTrader.total - active - garrisoned;
let messageTypes = {
"active": {
"garrisoned": {
"no-inactive": translate("%(openingTradingString)s, and %(garrisonedString)s."),
"inactive": translate("%(openingTradingString)s, %(garrisonedString)s, and %(inactiveString)s.")
},
"no-garrisoned": {
"no-inactive": translate("%(openingTradingString)s."),
"inactive": translate("%(openingTradingString)s, and %(inactiveString)s.")
}
},
"no-active": {
"garrisoned": {
"no-inactive": translate("%(openingGarrisonedString)s."),
"inactive": translate("%(openingGarrisonedString)s, and %(inactiveString)s.")
},
"no-garrisoned": {
"inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive),
"no-inactive": translate("There are no land traders.")
}
}
};
let message = messageTypes[active ? "active" : "no-active"][garrisoned ? "garrisoned" : "no-garrisoned"][inactive ? "inactive" : "no-inactive"];
let activeString = sprintf(
translatePlural(
"There is %(numberTrading)s land trader trading",
"There are %(numberTrading)s land traders trading",
active
),
{ "numberTrading": active }
);
let inactiveString = sprintf(
active || garrisoned ?
translatePlural(
"%(numberOfLandTraders)s inactive",
"%(numberOfLandTraders)s inactive",
inactive
) :
translatePlural(
"%(numberOfLandTraders)s land trader inactive",
"%(numberOfLandTraders)s land traders inactive",
inactive
),
{ "numberOfLandTraders": inactive }
);
let garrisonedString = sprintf(
active || inactive ?
translatePlural(
"%(numberGarrisoned)s garrisoned on a trading merchant ship",
"%(numberGarrisoned)s garrisoned on a trading merchant ship",
garrisoned
) :
translatePlural(
"There is %(numberGarrisoned)s land trader garrisoned on a trading merchant ship",
"There are %(numberGarrisoned)s land traders garrisoned on a trading merchant ship",
garrisoned
),
{ "numberGarrisoned": garrisoned }
);
return sprintf(message, {
"openingTradingString": activeString,
"openingGarrisonedString": garrisonedString,
"garrisonedString": garrisonedString,
"inactiveString": coloredText(inactiveString, g_IdleTraderTextColor)
});
}
function getIdleShipTradersText(traderNumber)
{
let active = traderNumber.shipTrader.trading;
let inactive = traderNumber.shipTrader.total - active;
let messageTypes = {
"active": {
"inactive": translate("%(openingTradingString)s, and %(inactiveString)s."),
"no-inactive": translate("%(openingTradingString)s.")
},
"no-active": {
"inactive": translatePlural("There is %(inactiveString)s.", "There are %(inactiveString)s.", inactive),
"no-inactive": translate("There are no merchant ships.")
}
};
let message = messageTypes[active ? "active" : "no-active"][inactive ? "inactive" : "no-inactive"];
let activeString = sprintf(
translatePlural(
"There is %(numberTrading)s merchant ship trading",
"There are %(numberTrading)s merchant ships trading",
active
),
{ "numberTrading": active }
);
let inactiveString = sprintf(
active ?
translatePlural(
"%(numberOfShipTraders)s inactive",
"%(numberOfShipTraders)s inactive",
inactive
) :
translatePlural(
"%(numberOfShipTraders)s merchant ship inactive",
"%(numberOfShipTraders)s merchant ships inactive",
inactive
),
{ "numberOfShipTraders": inactive }
);
return sprintf(message, {
"openingTradingString": activeString,
"inactiveString": coloredText(inactiveString, g_IdleTraderTextColor)
});
}
function closeTrade()
{
g_IsTradeOpen = false;
Engine.GetGUIObjectByName("tradeDialogPanel").hidden = true;
}
function toggleTrade()
{
let open = g_IsTradeOpen;
closeOpenDialogs();
if (!open)
openTrade();
}
function toggleTutorial()
{
let tutorialPanel = Engine.GetGUIObjectByName("tutorialPanel");
tutorialPanel.hidden = !tutorialPanel.hidden ||
!Engine.GetGUIObjectByName("tutorialText").caption;
}
function updateGameSpeedControl()
{
Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
let player = g_Players[Engine.GetPlayerID()];
g_GameSpeeds = getGameSpeedChoices(!player || player.state != "active");
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.list = g_GameSpeeds.Title;
gameSpeed.list_data = g_GameSpeeds.Speed;
let simRate = Engine.GetSimRate();
let gameSpeedIdx = g_GameSpeeds.Speed.indexOf(+simRate.toFixed(2));
if (gameSpeedIdx == -1)
warn("Unknown gamespeed:" + simRate);
gameSpeed.selected = gameSpeedIdx != -1 ? gameSpeedIdx : g_GameSpeeds.Default;
gameSpeed.onSelectionChange = function() {
changeGameSpeed(+this.list_data[this.selected]);
};
}
function toggleGameSpeed()
{
let gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
gameSpeed.hidden = !gameSpeed.hidden;
}
function toggleObjectives()
{
let open = g_IsObjectivesOpen;
closeOpenDialogs();
if (!open)
openObjectives();
}
function openObjectives()
{
g_IsObjectivesOpen = true;
let player = g_Players[Engine.GetPlayerID()];
let playerState = player && player.state;
let isActive = !playerState || playerState == "active";
Engine.GetGUIObjectByName("gameDescriptionText").caption = getGameDescription();
let objectivesPlayerstate = Engine.GetGUIObjectByName("objectivesPlayerstate");
objectivesPlayerstate.hidden = isActive;
objectivesPlayerstate.caption = g_PlayerStateMessages[playerState] || "";
let gameDescription = Engine.GetGUIObjectByName("gameDescription");
let gameDescriptionSize = gameDescription.size;
gameDescriptionSize.top = Engine.GetGUIObjectByName(
isActive ? "objectivesTitle" : "objectivesPlayerstate").size.bottom;
gameDescription.size = gameDescriptionSize;
Engine.GetGUIObjectByName("objectivesPanel").hidden = false;
}
function closeObjectives()
{
g_IsObjectivesOpen = false;
Engine.GetGUIObjectByName("objectivesPanel").hidden = true;
}
/**
* Allows players to see their own summary.
* If they have shared ally vision researched, they are able to see the summary of there allies too.
*/
function openGameSummary()
{
closeOpenDialogs();
pauseGame();
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
Engine.PushGuiPage("page_summary.xml", {
"sim": {
"mapSettings": g_GameAttributes.settings,
"playerStates": extendedSimState.players.filter((state, player) =>
g_IsObserver || player == 0 || player == g_ViewedPlayer ||
extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
"timeElapsed": extendedSimState.timeElapsed
},
"gui": {
"dialog": true,
"isInGame": true
},
"selectedData": g_SummarySelectedData,
"callback": "resumeGameAndSaveSummarySelectedData"
});
}
function openStrucTree()
{
closeOpenDialogs();
pauseGame();
// TODO add info about researched techs and unlocked entities
Engine.PushGuiPage(g_CivInfo.page, {
"civ": g_CivInfo.code || g_Players[g_ViewedPlayer].civ,
"callback": "storeCivInfoPage"
});
}
function storeCivInfoPage(data)
{
g_CivInfo.code = data.civ;
g_CivInfo.page = data.page;
resumeGame();
}
/**
* Pause or resume the game.
*
* @param explicit - true if the player explicitly wants to pause or resume.
* If this argument isn't set, a multiplayer game won't be paused and the pause overlay
* won't be shown in single player.
*/
function pauseGame(pause = true, explicit = false)
{
// The NetServer only supports pausing after all clients finished loading the game.
if (g_IsNetworked && (!explicit || !g_IsNetworkedActive))
return;
if (explicit)
g_Paused = pause;
Engine.SetPaused(g_Paused || pause, !!explicit);
if (g_IsNetworked)
{
setClientPauseState(Engine.GetPlayerGUID(), g_Paused);
return;
}
updatePauseOverlay();
}
function resumeGame(explicit = false)
{
pauseGame(false, explicit);
}
function resumeGameAndSaveSummarySelectedData(data)
{
g_SummarySelectedData = data.summarySelectedData;
resumeGame(data.explicitResume);
}
/**
* Called when the current player toggles a pause button.
*/
function togglePause()
{
if (!Engine.GetGUIObjectByName("pauseButton").enabled)
return;
closeOpenDialogs();
pauseGame(!g_Paused, true);
}
/**
* Called when a client pauses or resumes in a multiplayer game.
*/
function setClientPauseState(guid, paused)
{
// Update the list of pausing clients.
let index = g_PausingClients.indexOf(guid);
if (paused && index == -1)
g_PausingClients.push(guid);
else if (!paused && index != -1)
g_PausingClients.splice(index, 1);
updatePauseOverlay();
Engine.SetPaused(!!g_PausingClients.length, false);
}
/**
* Update the pause overlay.
*/
function updatePauseOverlay()
{
Engine.GetGUIObjectByName("pauseButton").caption = g_Paused ? translate("Resume") : translate("Pause");
Engine.GetGUIObjectByName("resumeMessage").hidden = !g_Paused;
Engine.GetGUIObjectByName("pausedByText").hidden = !g_IsNetworked;
Engine.GetGUIObjectByName("pausedByText").caption = sprintf(translate("Paused by %(players)s"),
{ "players": g_PausingClients.map(guid => colorizePlayernameByGUID(guid)).join(translateWithContext("Separator for a list of players", ", ")) });
Engine.GetGUIObjectByName("pauseOverlay").hidden = !(g_Paused || g_PausingClients.length);
Engine.GetGUIObjectByName("pauseOverlay").onPress = g_Paused ? togglePause : function() {};
}
function openManual()
{
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_manual.xml", {
"page": "manual/intro",
"title": translate("Manual"),
"url": "https://trac.wildfiregames.com/wiki/0adManual",
"callback": "resumeGame"
});
}
function closeOpenDialogs()
{
closeMenu();
closeChat();
closeDiplomacy();
closeTrade();
closeObjectives();
}
function formatTributeTooltip(playerID, resourceCode, amount)
{
return sprintf(translate("Tribute %(resourceAmount)s %(resourceType)s to %(playerName)s. Shift-click to tribute %(greaterAmount)s."), {
"resourceAmount": amount,
"resourceType": resourceNameWithinSentence(resourceCode),
"playerName": colorizePlayernameByID(playerID),
"greaterAmount": amount < 500 ? 500 : amount + 500
});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/gui/session/selection_panels.js (revision 22419)
@@ -1,1180 +1,1180 @@
/**
* Contains the layout and button settings per selection panel
*
* getItems returns a list of basic items used to fill the panel.
* This method is obligated. If the items list is empty, the panel
* won't be rendered.
*
* Then there's a loop over all items provided. In the loop,
* the item and some other standard data is added to a data object.
*
* The standard data is
* {
* "i": index
* "item": item coming from the getItems function
* "playerState": playerState
* "unitEntStates": states of the selected entities
* "rowLength": rowLength
* "numberOfItems": number of items that will be processed
* "button": gui Button object
* "icon": gui Icon object
* "guiSelection": gui button Selection overlay
* "countDisplay": gui caption space
* }
*
* Then for every data object, the setupButton function is called which
* sets the view and handlers of the button.
*/
// Cache some formation info
// Available formations per player
let g_AvailableFormations = new Map();
let g_FormationsInfo = new Map();
let g_SelectionPanels = {};
g_SelectionPanels.Alert = {
"getMaxNumberOfItems": function()
{
return 2;
},
"getItems": function(unitEntStates)
{
return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : [];
},
"setupButton": function(data)
{
data.button.onPress = function() {
switch (data.item)
{
case "raise":
raiseAlert();
return;
case "end":
endOfAlert();
return;
}
};
switch (data.item)
{
case "raise":
data.icon.sprite = "stretched:session/icons/bell_level1.png";
data.button.tooltip = translate("Raise an alert!");
break;
case "end":
data.button.tooltip = translate("End of alert.");
data.icon.sprite = "stretched:session/icons/bell_level0.png";
break;
}
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Barter = {
"getMaxNumberOfItems": function()
{
return 4;
},
"rowLength": 4,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
// If more than `rowLength` resources, don't display icons.
if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetCodes().length > this.rowLength)
return [];
return g_ResourceData.GetCodes();
},
"setupButton": function(data)
{
barterOpenCommon(data.item, data.i, "unitBarter");
barterUpdateCommon(data.item, data.i, "unitBarter", data.player);
let button = {};
for (let action of g_BarterActions)
button[action] = Engine.GetGUIObjectByName("unitBarter" + action + "Button[" + data.i + "]");
setPanelObjectPosition(button.Sell, data.i, data.rowLength);
setPanelObjectPosition(button.Buy, data.i + data.rowLength, data.rowLength);
return true;
}
};
g_SelectionPanels.Command = {
"getMaxNumberOfItems": function()
{
return 6;
},
"getItems": function(unitEntStates)
{
let commands = [];
for (let command in g_EntityCommands)
{
let info = g_EntityCommands[command].getInfo(unitEntStates);
if (info)
{
info.name = command;
commands.push(info);
}
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performCommand(data.unitEntStates, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled =
g_IsObserver && data.item.name == "focus-rally" ||
controlsPlayer(data.player) && (data.item.name != "delete" ||
data.unitEntStates.some(state => !isUndeletable(state)));
data.icon.sprite = "stretched:session/icons/" + data.item.icon;
let size = data.button.size;
// relative to the center ( = 50%)
size.rleft = 50;
size.rright = 50;
// offset from the center calculation, count on square buttons, so size.bottom is the width too
size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1);
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.AllyCommand = {
"getMaxNumberOfItems": function()
{
return 2;
},
"conflictsWith": ["Command"],
"getItems": function(unitEntStates)
{
let commands = [];
for (let command in g_AllyEntityCommands)
for (let state of unitEntStates)
{
let info = g_AllyEntityCommands[command].getInfo(state);
if (info)
{
info.name = command;
commands.push(info);
break;
}
}
return commands;
},
"setupButton": function(data)
{
data.button.tooltip = data.item.tooltip;
data.button.onPress = function() {
if (data.item.callback)
data.item.callback(data.item);
else
performAllyCommand(data.unitEntStates[0].id, data.item.name);
};
data.countDisplay.caption = data.item.count || "";
data.button.enabled = !!data.item.count;
let grayscale = data.button.enabled ? "" : "grayscale:";
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + data.item.icon;
let size = data.button.size;
// relative to the center ( = 50%)
size.rleft = 50;
size.rright = 50;
// offset from the center calculation, count on square buttons, so size.bottom is the width too
size.left = (data.i - data.numberOfItems / 2) * (size.bottom + 1);
size.right = size.left + size.bottom;
data.button.size = size;
return true;
}
};
g_SelectionPanels.Construction = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function()
{
return getAllBuildableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.player
});
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, 1),
"player": data.player
});
data.button.onPress = function() { startBuildingPlacement(data.item, data.playerState); };
data.button.onPressRight = function() { showTemplateDetails(data.item); };
let tooltips = [
getEntityNamesFormatted,
getVisibleEntityClassesFormatted,
getAurasTooltip,
getEntityTooltip,
getEntityCostTooltip,
getGarrisonTooltip,
getPopulationBonusTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template));
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
data.button.enabled = controlsPlayer(data.player);
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Formation = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"conflictsWith": ["Garrison"],
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !hasClass(state, "Unit")))
return [];
if (!g_AvailableFormations.has(unitEntStates[0].player))
g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));
let availableFormations = g_AvailableFormations.get(unitEntStates[0].player);
// Hide the panel if all formations are disabled
if (availableFormations.some(formation => canMoveSelectionIntoFormation(formation)))
return availableFormations;
return [];
},
"setupButton": function(data)
{
if (!g_FormationsInfo.has(data.item))
g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));
let formationInfo = g_FormationsInfo.get(data.item);
let formationOk = canMoveSelectionIntoFormation(data.item);
let unitIds = data.unitEntStates.map(state => state.id);
let formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
"ents": unitIds,
"formationTemplate": data.item
});
data.button.onPress = function() {
performFormation(unitIds, data.item);
};
let tooltip = translate(formationInfo.name);
if (!formationOk && formationInfo.tooltip)
tooltip += "\n" + coloredText(translate(formationInfo.tooltip), "red");
data.button.tooltip = tooltip;
data.button.enabled = formationOk && controlsPlayer(data.player);
let grayscale = formationOk ? "" : "grayscale:";
data.guiSelection.hidden = !formationSelected;
data.icon.sprite = "stretched:" + grayscale + "session/icons/" + formationInfo.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Garrison = {
"getMaxNumberOfItems": function()
{
return 12;
},
"rowLength": 4,
"conflictsWith": ["Barter"],
"getItems": function(unitEntStates)
{
if (unitEntStates.every(state => !state.garrisonHolder))
return [];
let groups = new EntityGroups();
for (let state of unitEntStates)
if (state.garrisonHolder)
groups.add(state.garrisonHolder.entities);
return groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
data.button.onPress = function() {
unloadTemplate(template.selectionGroupName || entState.template, entState.player);
};
data.countDisplay.caption = data.item.ents.length || "";
let canUngarrison =
g_ViewedPlayer == data.player ||
g_ViewedPlayer == entState.player;
data.button.enabled = canUngarrison && controlsPlayer(g_ViewedPlayer);
data.button.tooltip = (canUngarrison || g_IsObserver ?
sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) }) + "\n" +
translate("Single-click to unload 1. Shift-click to unload all of this type.") :
getEntityNames(template)) + "\n" +
sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[entState.player].name
});
data.guiSelection.sprite = getPlayerHighlightColor(entState.player);
data.button.sprite_disabled = data.button.sprite;
// Selection panel buttons only appear disabled if they
// also appear disabled to the owner of the building.
data.icon.sprite =
(canUngarrison || g_IsObserver ? "" : "grayscale:") +
"stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Gate = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntStates)
{
let hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked);
let hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked);
if (hideLocked && hideUnlocked)
return [];
return [
{
"hidden": hideLocked,
"tooltip": translate("Lock Gate"),
"icon": "session/icons/lock_locked.png",
"locked": true
},
{
"hidden": hideUnlocked,
"tooltip": translate("Unlock Gate"),
"icon": "session/icons/lock_unlocked.png",
"locked": false
}
];
},
"setupButton": function(data)
{
data.button.onPress = function() { lockGate(data.item.locked); };
data.button.tooltip = data.item.tooltip;
data.button.enabled = controlsPlayer(data.player);
data.guiSelection.hidden = data.item.hidden;
data.icon.sprite = "stretched:" + data.item.icon;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Pack = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntStates)
{
let checks = {};
for (let state of unitEntStates)
{
if (!state.pack)
continue;
if (state.pack.progress == 0)
{
if (state.pack.packed)
checks.unpackButton = true;
else
checks.packButton = true;
}
else if (state.pack.packed)
checks.unpackCancelButton = true;
else
checks.packCancelButton = true;
}
let items = [];
if (checks.packButton)
items.push({
"packing": false,
"packed": false,
"tooltip": translate("Pack"),
"callback": function() { packUnit(true); }
});
if (checks.unpackButton)
items.push({
"packing": false,
"packed": true,
"tooltip": translate("Unpack"),
"callback": function() { packUnit(false); }
});
if (checks.packCancelButton)
items.push({
"packing": true,
"packed": false,
"tooltip": translate("Cancel Packing"),
"callback": function() { cancelPackUnit(true); }
});
if (checks.unpackCancelButton)
items.push({
"packing": true,
"packed": true,
"tooltip": translate("Cancel Unpacking"),
"callback": function() { cancelPackUnit(false); }
});
return items;
},
"setupButton": function(data)
{
data.button.onPress = function() {data.item.callback(data.item); };
data.button.tooltip = data.item.tooltip;
if (data.item.packing)
data.icon.sprite = "stretched:session/icons/cancel.png";
else if (data.item.packed)
data.icon.sprite = "stretched:session/icons/unpack.png";
else
data.icon.sprite = "stretched:session/icons/pack.png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
g_SelectionPanels.Queue = {
"getMaxNumberOfItems": function()
{
return 16;
},
/**
* Returns a list of all items in the productionqueue of the selection
* The first entry of every entity's production queue will come before
* the second entry of every entity's production queue
*/
"getItems": function(unitEntStates)
{
let queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (let state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
queue.push({
"producingEnt": state.id,
"queuedItem": state.production.queue[i]
});
foundNew = true;
}
}
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
{
let numRows = Math.ceil(numberOfItems / rowLength);
let panel = Engine.GetGUIObjectByName("unitQueuePanel");
let size = panel.size;
let buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
let margin = 4;
size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin;
panel.size = size;
},
"setupButton": function(data)
{
let queuedItem = data.item.queuedItem;
// Differentiate between units and techs
let template;
if (queuedItem.unitTemplate)
template = GetTemplateData(queuedItem.unitTemplate);
else if (queuedItem.technologyTemplate)
template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ);
else
{
warning("Unknown production queue template " + uneval(queuedItem));
return false;
}
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };
let tooltip = getEntityNames(template);
if (queuedItem.neededSlots)
{
tooltip += "\n" + coloredText(translate("Insufficient population capacity:"), "red");
tooltip += "\n" + sprintf(translate("%(population)s %(neededSlots)s"), {
"population": resourceIcon("population"),
"neededSlots": queuedItem.neededSlots
});
}
data.button.tooltip = tooltip;
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
let guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
let size = guiObject.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
guiObject.size = size;
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Research = {
"getMaxNumberOfItems": function()
{
return 8;
},
"getItems": function(unitEntStates)
{
let ret = [];
if (unitEntStates.length == 1)
return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret :
unitEntStates[0].production.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": unitEntStates[0].production.techCostMultiplier,
"researchFacilityId": unitEntStates[0].id
}));
for (let state of unitEntStates)
{
if (!state.production || !state.production.technologies)
continue;
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
let filteredTechs = state.production.technologies.filter(
tech => tech != null && !ret.some(
item =>
(item.tech == tech ||
item.tech.pair &&
tech.pair &&
item.tech.bottom == tech.bottom &&
item.tech.top == tech.top) &&
Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k])
));
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.production.techCostMultiplier,
"researchFacilityId": state.id
})));
}
return ret;
},
"hideItem": function(i, rowLength) // Called when no item is found
{
Engine.GetGUIObjectByName("unitResearchButton[" + i + "]").hidden = true;
// We also remove the paired tech and the pair symbol
Engine.GetGUIObjectByName("unitResearchButton[" + (i + rowLength) + "]").hidden = true;
Engine.GetGUIObjectByName("unitResearchPair[" + i + "]").hidden = true;
},
"setupButton": function(data)
{
if (!data.item.tech)
{
g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
return false;
}
// Start position (start at the bottom)
let position = data.i + data.rowLength;
// Only show the top button for pairs
if (!data.item.tech.pair)
Engine.GetGUIObjectByName("unitResearchButton[" + data.i + "]").hidden = true;
// Set up the tech connector
let pair = Engine.GetGUIObjectByName("unitResearchPair[" + data.i + "]");
pair.hidden = data.item.tech.pair == null;
setPanelObjectPosition(pair, data.i, data.rowLength);
// Handle one or two techs (tech pair)
let player = data.player;
let playerState = GetSimState().players[player];
for (let tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech])
{
// Don't change the object returned by GetTechnologyData
let template = clone(GetTechnologyData(tech, playerState.civ));
if (!template)
return false;
for (let res in template.cost)
template.cost[res] *= data.item.techCostMultiplier[res];
let neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": template.cost,
"player": player
});
let requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
"tech": tech,
"player": player
});
let button = Engine.GetGUIObjectByName("unitResearchButton[" + position + "]");
let icon = Engine.GetGUIObjectByName("unitResearchIcon[" + position + "]");
let tooltips = [
getEntityNamesFormatted,
getEntityTooltip,
getEntityCostTooltip,
showTemplateViewerOnRightClickTooltip
].map(func => func(template));
if (!requirementsPassed)
{
let tip = template.requirementsTooltip;
let reqs = template.reqs;
for (let req of reqs)
{
if (!req.entities)
continue;
let entityCounts = [];
for (let entity of req.entities)
{
let current = 0;
switch (entity.check)
{
case "count":
current = playerState.classCounts[entity.class] || 0;
break;
case "variants":
current = playerState.typeCountsByClass[entity.class] ?
Object.keys(playerState.typeCountsByClass[entity.class]).length : 0;
break;
}
let remaining = entity.number - current;
if (remaining < 1)
continue;
entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), {
"number": remaining,
"class": entity.class
}));
}
tip += " " + sprintf(translate("Remaining: %(entityCounts)s"), {
"entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", "))
});
}
tooltips.push(tip);
}
tooltips.push(getNeededResourcesTooltip(neededResources));
button.tooltip = tooltips.filter(tip => tip).join("\n");
button.onPress = (t => function() {
addResearchToQueue(data.item.researchFacilityId, t);
})(tech);
button.onPressRight = (t => function () {
showTemplateDetails(
t,
GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv);
})(tech);
if (data.item.tech.pair)
{
// On mouse enter, show a cross over the other icon
let unchosenIcon = Engine.GetGUIObjectByName("unitResearchUnchosenIcon[" + (position + data.rowLength) % (2 * data.rowLength) + "]");
button.onMouseEnter = function() {
unchosenIcon.hidden = false;
};
button.onMouseLeave = function() {
unchosenIcon.hidden = true;
};
}
button.hidden = false;
let modifier = "";
if (!requirementsPassed)
{
button.enabled = false;
modifier += "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
button.enabled = false;
modifier += resourcesToAlphaMask(neededResources) + ":";
}
else
button.enabled = controlsPlayer(data.player);
if (template.icon)
icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(button, position, data.rowLength);
// Prepare to handle the top button (if any)
position -= data.rowLength;
}
return true;
}
};
g_SelectionPanels.Selection = {
"getMaxNumberOfItems": function()
{
return 16;
},
"rowLength": 4,
"getItems": function(unitEntStates)
{
if (unitEntStates.length < 2)
return [];
return g_Selection.groups.getEntsGrouped();
},
"setupButton": function(data)
{
let entState = GetEntityState(data.item.ents[0]);
let template = GetTemplateData(entState.template);
if (!template)
return false;
for (let ent of data.item.ents)
{
let state = GetEntityState(ent);
if (state.resourceCarrying && state.resourceCarrying.length !== 0)
{
if (!data.carried)
data.carried = {};
let carrying = state.resourceCarrying[0];
if (data.carried[carrying.type])
data.carried[carrying.type] += carrying.amount;
else
data.carried[carrying.type] = carrying.amount;
}
if (state.trader && state.trader.goods && state.trader.goods.amount)
{
if (!data.carried)
data.carried = {};
let amount = state.trader.goods.amount;
let type = state.trader.goods.type;
let totalGain = amount.traderGain;
if (amount.market1Gain)
totalGain += amount.market1Gain;
if (amount.market2Gain)
totalGain += amount.market2Gain;
if (data.carried[type])
data.carried[type] += totalGain;
else
data.carried[type] = totalGain;
}
}
let unitOwner = GetEntityState(data.item.ents[0]).player;
let tooltip = getEntityNames(template);
if (data.carried)
tooltip += "\n" + Object.keys(data.carried).map(res =>
resourceIcon(res) + data.carried[res]
).join(" ");
if (g_IsObserver)
tooltip += "\n" + sprintf(translate("Player: %(playername)s"), {
"playername": g_Players[unitOwner].name
});
data.button.tooltip = tooltip;
data.guiSelection.sprite = getPlayerHighlightColor(unitOwner);
data.guiSelection.hidden = !g_IsObserver;
data.countDisplay.caption = data.item.ents.length || "";
data.button.onPress = function() { changePrimarySelectionGroup(data.item.key, false); };
data.button.onPressRight = function() { changePrimarySelectionGroup(data.item.key, true); };
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Stance = {
"getMaxNumberOfItems": function()
{
return 5;
},
"getItems": function(unitEntStates)
{
if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
return [];
return unitEntStates[0].unitAI.selectableStances;
},
"setupButton": function(data)
{
let unitIds = data.unitEntStates.map(state => state.id);
data.button.onPress = function() { performStance(unitIds, data.item); };
data.button.tooltip = getStanceDisplayName(data.item) + "\n" +
"[font=\"sans-13\"]" + getStanceTooltip(data.item) + "[/font]";
data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": unitIds,
"stance": data.item
});
data.icon.sprite = "stretched:session/icons/stances/" + data.item + ".png";
data.button.enabled = controlsPlayer(data.player);
setPanelObjectPosition(data.button, data.i, data.rowLength);
return true;
}
};
g_SelectionPanels.Training = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function()
{
return getAllTrainableEntitiesFromSelection();
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item);
if (!template)
return false;
let technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": template.requiredTechnology,
"player": data.player
});
- let unitIds = data.unitEntStates.map(status => status.id)
+ let unitIds = data.unitEntStates.map(status => status.id);
let [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
getTrainingStatus(unitIds, data.item, data.playerState);
let trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;
let neededResources;
if (template.cost)
neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(template, trainNum),
"player": data.player
});
data.button.onPress = function() {
if (!neededResources)
addTrainingToQueue(unitIds, data.item, data.playerState);
};
data.button.onPressRight = function() {
showTemplateDetails(data.item);
};
data.countDisplay.caption = trainNum > 1 ? trainNum : "";
let tooltips = [
"[font=\"sans-bold-16\"]" +
colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
"[/font]" + " " + getEntityNamesFormatted(template),
getVisibleEntityClassesFormatted(template),
getAurasTooltip(template),
getEntityTooltip(template),
getEntityCostTooltip(template, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
];
let limits = getEntityLimitAndCount(data.playerState, data.item);
tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers));
if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
tooltips = tooltips.concat([
getHealthTooltip,
getAttackTooltip,
getSplashDamageTooltip,
getHealerTooltip,
getArmorTooltip,
getGarrisonTooltip,
getProjectilesTooltip,
getSpeedTooltip
].map(func => func(template)));
tooltips.push(showTemplateViewerOnRightClickTooltip());
tooltips.push(
formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
getRequiredTechnologyTooltip(technologyEnabled, template.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources));
data.button.tooltip = tooltips.filter(tip => tip).join("\n");
let modifier = "";
if (!technologyEnabled || limits.canBeAddedCount == 0)
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else
{
data.button.enabled = controlsPlayer(data.player);
if (neededResources)
modifier = resourcesToAlphaMask(neededResources) + ":";
}
if (template.icon)
data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;
let index = data.i + getNumberOfRightPanelButtons();
setPanelObjectPosition(data.button, index, data.rowLength);
return true;
}
};
g_SelectionPanels.Upgrade = {
"getMaxNumberOfItems": function()
{
return 24 - getNumberOfRightPanelButtons();
},
"getItems": function(unitEntStates)
{
// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
if (unitEntStates.some(state => state.template != unitEntStates[0].template))
return false;
return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
},
"setupButton": function(data)
{
let template = GetTemplateData(data.item.entity);
if (!template)
return false;
let technologyEnabled = true;
if (data.item.requiredTechnology)
technologyEnabled = Engine.GuiInterfaceCall("IsTechnologyResearched", {
"tech": data.item.requiredTechnology,
"player": data.player
});
let neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", {
"cost": multiplyEntityCosts(data.item, data.unitEntStates.length),
"player": data.player
});
let limits = getEntityLimitAndCount(data.playerState, data.item.entity);
let progress = data.unitEntStates[0].upgrade.progress || 0;
let isUpgrading = data.unitEntStates[0].upgrade.template == data.item.entity;
let tooltip;
if (!progress)
{
let tooltips = [];
if (data.item.tooltip)
tooltips.push(sprintf(translate("Upgrade into a %(name)s. %(tooltip)s"), {
"name": template.name.generic,
"tooltip": translate(data.item.tooltip)
}));
else
tooltips.push(sprintf(translate("Upgrade into a %(name)s."), {
"name": template.name.generic
}));
tooltips.push(
getEntityCostComponentsTooltipString(data.item, undefined, data.unitEntStates.length),
formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
getRequiredTechnologyTooltip(technologyEnabled, data.item.requiredTechnology, GetSimState().players[data.player].civ),
getNeededResourcesTooltip(neededResources),
showTemplateViewerOnRightClickTooltip());
tooltip = tooltips.filter(tip => tip).join("\n");
data.button.onPress = function() { upgradeEntity(data.item.entity); };
}
else if (isUpgrading)
{
tooltip = translate("Cancel Upgrading");
data.button.onPress = function() { cancelUpgradeEntity(); };
}
else
{
tooltip = translate("Cannot upgrade when the entity is already upgrading.");
data.button.onPress = function() {};
}
data.button.enabled = controlsPlayer(data.player);
data.button.tooltip = tooltip;
data.button.onPressRight = function() {
showTemplateDetails(data.item.entity);
};
let modifier = "";
if (!isUpgrading)
if (progress || !technologyEnabled || limits.canBeAddedCount == 0 &&
!hasSameRestrictionCategory(data.item.entity, data.unitEntStates[0].template))
{
data.button.enabled = false;
modifier = "color:0 0 0 127:grayscale:";
}
else if (neededResources)
{
data.button.enabled = false;
modifier = resourcesToAlphaMask(neededResources) + ":";
}
data.icon.sprite = modifier + "stretched:session/" +
(data.item.icon || "portraits/" + template.icon);
data.countDisplay.caption = data.unitEntStates.length > 1 ? data.unitEntStates.length : "";
let progressOverlay = Engine.GetGUIObjectByName("unitUpgradeProgressSlider[" + data.i + "]");
if (isUpgrading)
{
let size = progressOverlay.size;
size.top = size.left + Math.round(progress * (size.right - size.left));
progressOverlay.size = size;
}
progressOverlay.hidden = !isUpgrading;
setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
return true;
}
};
/**
* Pauses game and opens the template details viewer for a selected entity or technology.
*
* Technologies don't have a set civ, so we pass along the native civ of
* the template of the entity that's researching it.
*
* @param {string} [civCode] - The template name of the entity that researches the selected technology.
*/
function showTemplateDetails(templateName, civCode)
{
pauseGame();
Engine.PushGuiPage("page_viewer.xml", {
"templateName": templateName,
"callback": "resumeGame",
"civ": civCode
});
}
/**
* If two panels need the same space, so they collide,
* the one appearing first in the order is rendered.
*
* Note that the panel needs to appear in the list to get rendered.
*/
let g_PanelsOrder = [
// LEFT PANE
"Barter", // Must always be visible on markets
"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
"Alert",
"Formation",
"Stance", // Normal together with formation
// RIGHT PANE
"Gate", // Must always be shown on gates
"Pack", // Must always be shown on packable entities
"Upgrade", // Must always be shown on upgradable entities
"Training",
"Construction",
"Research", // Normal together with training
// UNIQUE PANES (importance doesn't matter)
"Command",
"AllyCommand",
"Queue",
"Selection",
];
Index: ps/trunk/binaries/data/mods/public/maps/random/alpine_valley.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/alpine_valley.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/alpine_valley.js (revision 22419)
@@ -1,544 +1,544 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
TILE_CENTERED_HEIGHT_MAP = true;
/**
* This class creates random mountainranges without enclosing any area completely.
*
* To determine their location, a graph is created where each vertex is a possible starting or
* ending location of a mountainrange and each edge a possible mountainrange.
*
* That graph starts nearly complete (i.e almost every vertex is connected to most other vertices).
* After a random edge was chosen and placed as a mountainrange,
* all edges that intersect, that leave a too small gap to another mountainrange or that are connected to
* too many other mountainranges are removed from the graph.
* This is repeated until all edges were removed.
*/
function MountainRangeBuilder(args)
{
/**
* These parameters paint the mountainranges after their location was determined.
*/
this.pathplacer = args.pathplacer;
this.painters = args.painters;
this.constraint = args.constraint;
this.mountainWidth = args.mountainWidth;
/**
* Minimum geometric distance between two mountains that don't end in one place (disjoint edges).
*/
this.minDistance = args.mountainWidth + args.passageWidth;
/**
* Array of Vector2D locations where a mountainrange can start or end.
*/
this.vertices = args.points;
/**
* Number of mountainranges starting or ending at the given point.
*/
this.vertexDegree = this.vertices.map(p => 0);
/**
* Highest number of mountainranges that can meet in one point (maximum degree of each vertex).
*/
this.maxDegree = args.maxDegree;
/**
* Each possible edge is an array containing two vertex indices.
* The algorithm adds possible edges consecutively and removes subsequently invalid edges.
*/
this.possibleEdges = [];
this.InitPossibleEdges();
/**
* A two-dimensional array of booleans that are true if the two corresponding vertices may be connected by a new edge (mountainrange).
* It is initialized with some points that should never be connected and updated with every placed edge.
* The purpose is to rule out any cycles in the graph, i.e. prevent any territory enclosed by mountainranges.
*/
this.verticesConnectable = [];
this.InitConnectable();
/**
* Currently iterated item of possibleEdges that is either used as a mountainrange or removed from the possibleEdges.
*/
this.index = undefined;
/**
* These variables hold the indices of the two points of that edge and the location of them as a Vector2D.
*/
this.currentEdge = undefined;
this.currentEdgeStart = undefined;
this.currentEdgeEnd = undefined;
}
MountainRangeBuilder.prototype.InitPossibleEdges = function()
{
for (let i = 0; i < this.vertices.length; ++i)
for (let j = numPlayers; j < this.vertices.length; ++j)
if (j > i)
this.possibleEdges.push([i, j]);
};
MountainRangeBuilder.prototype.InitConnectable = function()
{
for (let i = 0; i < this.vertices.length; ++i)
{
this.verticesConnectable[i] = [];
for (let j = 0; j < this.vertices.length; ++j)
this.verticesConnectable[i][j] = i >= numPlayers || j >= numPlayers || i == j || i != j - 1 && i != j + 1;
}
};
MountainRangeBuilder.prototype.SetConnectable = function(isConnectable)
{
this.verticesConnectable[this.currentEdge[0]][this.currentEdge[1]] = isConnectable;
this.verticesConnectable[this.currentEdge[1]][this.currentEdge[0]] = isConnectable;
};
MountainRangeBuilder.prototype.UpdateCurrentEdge = function()
{
this.currentEdge = this.possibleEdges[this.index];
this.currentEdgeStart = this.vertices[this.currentEdge[0]];
this.currentEdgeEnd = this.vertices[this.currentEdge[1]];
};
/**
* Remove all edges that are too close to the current mountainrange or intersect.
*/
MountainRangeBuilder.prototype.RemoveInvalidEdges = function()
{
for (let i = 0; i < this.possibleEdges.length; ++i)
{
this.UpdateCurrentEdge();
let comparedEdge = this.possibleEdges[i];
let comparedEdgeStart = this.vertices[comparedEdge[0]];
let comparedEdgeEnd = this.vertices[comparedEdge[1]];
let edge0Equal = this.currentEdgeStart == comparedEdgeStart;
let edge1Equal = this.currentEdgeStart == comparedEdgeEnd;
let edge2Equal = this.currentEdgeEnd == comparedEdgeEnd;
let edge3Equal = this.currentEdgeEnd == comparedEdgeStart;
if (!edge0Equal && !edge2Equal && !edge1Equal && !edge3Equal && testLineIntersection(this.currentEdgeStart, this.currentEdgeEnd, comparedEdgeStart, comparedEdgeEnd, this.minDistance) ||
( edge0Equal && !edge2Equal || !edge1Equal && edge3Equal) && distanceOfPointFromLine(this.currentEdgeStart, this.currentEdgeEnd, comparedEdgeEnd) < this.minDistance ||
(!edge0Equal && edge2Equal || edge1Equal && !edge3Equal) && distanceOfPointFromLine(this.currentEdgeStart, this.currentEdgeEnd, comparedEdgeStart) < this.minDistance)
{
this.possibleEdges.splice(i, 1);
--i;
if (this.index > i)
--this.index;
}
}
};
/**
* Tests using depth-first-search if the graph according to pointsConnectable contains a cycle,
* i.e. if adding the currentEdge would result in an area enclosed by mountainranges.
*/
MountainRangeBuilder.prototype.HasCycles = function()
{
let tree = [];
let backtree = [];
let pointQueue = [this.currentEdge[0]];
while (pointQueue.length)
{
let selectedPoint = pointQueue.shift();
if (tree.indexOf(selectedPoint) == -1)
{
tree.push(selectedPoint);
backtree.push(-1);
}
for (let i = 0; i < this.vertices.length; ++i)
{
if (this.verticesConnectable[selectedPoint][i] || i == backtree[tree.lastIndexOf(selectedPoint)])
continue;
// If the current point was encountered already, then a cycle was identified.
if (tree.indexOf(i) != -1)
return true;
// Otherwise visit this point next
pointQueue.unshift(i);
tree.push(i);
backtree.push(selectedPoint);
}
}
return false;
};
MountainRangeBuilder.prototype.PaintCurrentEdge = function()
{
this.pathplacer.start = this.currentEdgeStart;
this.pathplacer.end = this.currentEdgeEnd;
this.pathplacer.width = this.mountainWidth;
// Creating mountainrange
if (!createArea(this.pathplacer, this.painters, this.constraint))
return false;
// Creating circular mountains at both ends of that mountainrange
for (let point of [this.currentEdgeStart, this.currentEdgeEnd])
createArea(
new ClumpPlacer(diskArea(this.mountainWidth / 2), 0.95, 0.6, Infinity, point),
this.painters,
this.constraint);
return true;
};
/**
* This is the only function meant to be publicly accessible.
*/
MountainRangeBuilder.prototype.CreateMountainRanges = function()
{
g_Map.log("Creating mountainrange with " + this.possibleEdges.length + " possible edges");
- let max = this.possibleEdges.length
+ let max = this.possibleEdges.length;
while (this.possibleEdges.length)
{
Engine.SetProgress(35 - 15 * this.possibleEdges.length / max);
this.index = randIntExclusive(0, this.possibleEdges.length);
this.UpdateCurrentEdge();
this.SetConnectable(false);
if (this.vertexDegree[this.currentEdge[0]] < this.maxDegree &&
this.vertexDegree[this.currentEdge[1]] < this.maxDegree &&
!this.HasCycles() &&
this.PaintCurrentEdge())
{
++this.vertexDegree[this.currentEdge[0]];
++this.vertexDegree[this.currentEdge[1]];
this.RemoveInvalidEdges();
}
else
this.SetConnectable(true);
this.possibleEdges.splice(this.index, 1);
}
};
if (randBool())
{
RandomMapLogger.prototype.printDirectly("Setting late spring biome.\n");
var tPrimary = ["alpine_dirt_grass_50"];
var tForestFloor = "alpine_forrestfloor";
var tCliff = ["alpine_cliff_a", "alpine_cliff_b", "alpine_cliff_c"];
var tSecondary = "alpine_grass_rocky";
var tHalfSnow = ["alpine_grass_snow_50", "alpine_dirt_snow"];
var tSnowLimited = ["alpine_snow_rocky"];
var tDirt = "alpine_dirt";
var tRoad = "new_alpine_citytile";
var tRoadWild = "new_alpine_citytile";
var oPine = "gaia/flora_tree_pine";
var oBerryBush = "gaia/flora_bush_berry";
var oDeer = "gaia/fauna_deer";
var oRabbit = "gaia/fauna_rabbit";
var oStoneLarge = "gaia/geology_stonemine_alpine_quarry";
var oStoneSmall = "gaia/geology_stone_alpine_a";
var oMetalLarge = "gaia/geology_metal_alpine_slabs";
var aGrass = "actor|props/flora/grass_soft_small_tall.xml";
var aGrassShort = "actor|props/flora/grass_soft_large.xml";
var aRockLarge = "actor|geology/stone_granite_med.xml";
var aRockMedium = "actor|geology/stone_granite_med.xml";
var aBushMedium = "actor|props/flora/bush_medit_me.xml";
var aBushSmall = "actor|props/flora/bush_medit_sm.xml";
}
else
{
RandomMapLogger.prototype.printDirectly("Setting winter biome.\n");
var tPrimary = ["alpine_snow_a", "alpine_snow_b"];
var tForestFloor = "alpine_forrestfloor_snow";
var tCliff = ["alpine_cliff_snow"];
var tSecondary = "alpine_grass_snow_50";
var tHalfSnow = ["alpine_grass_snow_50", "alpine_dirt_snow"];
var tSnowLimited = ["alpine_snow_a", "alpine_snow_b"];
var tDirt = "alpine_dirt";
var tRoad = "new_alpine_citytile";
var tRoadWild = "new_alpine_citytile";
var oPine = "gaia/flora_tree_pine_w";
var oBerryBush = "gaia/flora_bush_berry";
var oDeer = "gaia/fauna_deer";
var oRabbit = "gaia/fauna_rabbit";
var oStoneLarge = "gaia/geology_stonemine_alpine_quarry";
var oStoneSmall = "gaia/geology_stone_alpine_a";
var oMetalLarge = "gaia/geology_metal_alpine_slabs";
var aGrass = "actor|props/flora/grass_soft_dry_small_tall.xml";
var aGrassShort = "actor|props/flora/grass_soft_dry_large.xml";
var aRockLarge = "actor|geology/stone_granite_med.xml";
var aRockMedium = "actor|geology/stone_granite_med.xml";
var aBushMedium = "actor|props/flora/bush_medit_me_dry.xml";
var aBushSmall = "actor|props/flora/bush_medit_sm_dry.xml";
}
var heightLand = 3;
var heightOffsetBump = 2;
var snowlineHeight = 29;
var heightMountain = 30;
const pForest = [tForestFloor + TERRAIN_SEPARATOR + oPine, tForestFloor];
var g_Map = new RandomMap(heightLand, tPrimary);
const numPlayers = getNumPlayers();
const mapCenter = g_Map.getCenter();
var clPlayer = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var [playerIDs, playerPosition, playerAngle, startAngle] = playerPlacementCircle(fractionToTiles(0.35));
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"PlayerTileClass": clPlayer,
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad
},
"Chicken": {
},
"Berries": {
"template": oBerryBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
"Trees": {
"template": oPine
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(20);
new MountainRangeBuilder({
"pathplacer": new PathPlacer(undefined, undefined, undefined, 0.4, scaleByMapSize(3, 12), 0.1, 0.1, 0.1),
"painters":[
new LayeredPainter([tCliff, tPrimary], [3]),
new SmoothElevationPainter(ELEVATION_SET, heightMountain, 2),
new TileClassPainter(clHill)
],
"constraint": avoidClasses(clPlayer, 20),
"passageWidth": scaleByMapSize(10, 15),
"mountainWidth": scaleByMapSize(9, 15),
"maxDegree": 3,
"points": [
// Four points near each player
...distributePointsOnCircle(numPlayers, startAngle + Math.PI / numPlayers, fractionToTiles(0.49), mapCenter)[0],
...distributePointsOnCircle(numPlayers, startAngle + Math.PI / numPlayers * 1.4, fractionToTiles(0.34), mapCenter)[0],
...distributePointsOnCircle(numPlayers, startAngle + Math.PI / numPlayers * 0.6, fractionToTiles(0.34), mapCenter)[0],
...distributePointsOnCircle(numPlayers, startAngle + Math.PI / numPlayers, fractionToTiles(0.18), mapCenter)[0],
mapCenter
]
}).CreateMountainRanges();
Engine.SetProgress(35);
paintTerrainBasedOnHeight(heightLand + 0.1, snowlineHeight, 0, tCliff);
paintTerrainBasedOnHeight(snowlineHeight, heightMountain, 3, tSnowLimited);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 2),
avoidClasses(clPlayer, 10),
scaleByMapSize(100, 200));
Engine.SetProgress(40);
g_Map.log("Creating hills");
createAreas(
new ClumpPlacer(scaleByMapSize(40, 150), 0.2, 0.1, Infinity),
[
new LayeredPainter([tCliff, tSnowLimited], [2]),
new SmoothElevationPainter(ELEVATION_SET, heightMountain, 2),
new TileClassPainter(clHill)
],
avoidClasses(clPlayer, 20, clHill, 14),
scaleByMapSize(10, 80) * numPlayers
);
Engine.SetProgress(50);
g_Map.log("Creating forests");
var [forestTrees, stragglerTrees] = getTreeCounts(500, 3000, 0.7);
var types = [
[[tForestFloor, tPrimary, pForest], [tForestFloor, pForest]]
];
var size = forestTrees / (scaleByMapSize(2,8) * numPlayers);
var num = Math.floor(size / types.length);
for (let type of types)
createAreas(
new ClumpPlacer(forestTrees / num, 0.1, 0.1, Infinity),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
avoidClasses(clPlayer, 12, clForest, 10, clHill, 0),
num);
Engine.SetProgress(60);
g_Map.log("Creating dirt patches");
for (let size of [scaleByMapSize(3, 48), scaleByMapSize(5, 84), scaleByMapSize(8, 128)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
[
new LayeredPainter([[tDirt, tHalfSnow], [tHalfSnow, tSnowLimited]], [2]),
new TileClassPainter(clDirt)
],
avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12),
scaleByMapSize(15, 45));
g_Map.log("Creating grass patches");
for (let size of [scaleByMapSize(2, 32), scaleByMapSize(3, 48), scaleByMapSize(5, 80)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
new TerrainPainter(tSecondary),
avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12),
scaleByMapSize(15, 45));
Engine.SetProgress(65);
g_Map.log("Creating stone mines");
var group = new SimpleGroup([new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], true, clRock);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 1, clPlayer, 20, clRock, 10, clHill, 1),
scaleByMapSize(4,16), 100
);
g_Map.log("Creating small stone mines");
group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3)], true, clRock);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 1, clPlayer, 20, clRock, 10, clHill, 1),
scaleByMapSize(4,16), 100
);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 1, clPlayer, 20, clMetal, 10, clRock, 5, clHill, 1),
scaleByMapSize(4,16), 100
);
Engine.SetProgress(70);
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockMedium, 1,3, 0,1)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clForest, 0, clPlayer, 0, clHill, 0),
scaleByMapSize(16, 262), 50
);
g_Map.log("Creating large decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clForest, 0, clPlayer, 0, clHill, 0),
scaleByMapSize(8, 131), 50
);
Engine.SetProgress(75);
g_Map.log("Creating deer");
group = new SimpleGroup(
[new SimpleObject(oDeer, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 0, clPlayer, 10, clHill, 1, clFood, 20),
3 * numPlayers, 50
);
g_Map.log("Creating berry bush");
group = new SimpleGroup(
[new SimpleObject(oBerryBush, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 0, clPlayer, 20, clHill, 1, clFood, 10),
randIntInclusive(1, 4) * numPlayers + 2, 50
);
g_Map.log("Creating rabbit");
group = new SimpleGroup(
[new SimpleObject(oRabbit, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 0, clPlayer, 10, clHill, 1, clFood, 20),
3 * numPlayers, 50
);
Engine.SetProgress(85);
createStragglerTrees(
[oPine],
avoidClasses(clForest, 1, clHill, 1, clPlayer, 12, clMetal, 6, clRock, 6),
clForest,
stragglerTrees);
g_Map.log("Creating small grass tufts");
var planetm = 1;
group = new SimpleGroup(
[new SimpleObject(aGrassShort, 1,2, 0,1, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clHill, 2, clPlayer, 2, clDirt, 0),
planetm * scaleByMapSize(13, 200)
);
Engine.SetProgress(90);
g_Map.log("Creating large grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrass, 2,4, 0,1.8, -Math.PI / 8, Math.PI / 8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clHill, 2, clPlayer, 2, clDirt, 1, clForest, 0),
planetm * scaleByMapSize(13, 200)
);
Engine.SetProgress(95);
g_Map.log("Creating bushes");
group = new SimpleGroup(
[new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clHill, 1, clPlayer, 1, clDirt, 1),
planetm * scaleByMapSize(13, 200), 50
);
placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clMetal, 4, clRock, 4, clHill, 4, clFood, 2));
setSkySet(pickRandom(["cirrus", "cumulus", "sunny"]));
setSunRotation(randomAngle());
setSunElevation(Math.PI * randFloat(1/5, 1/3));
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js (revision 22419)
@@ -1,283 +1,283 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("heightmap");
const tPrimary = ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d",
"temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"];
const heightLand = 0;
var g_Map = new RandomMap(heightLand, tPrimary);
var numPlayers = getNumPlayers();
var mapSize = g_Map.getSize();
var mapCenter = g_Map.getCenter();
// Set target min and max height depending on map size to make average stepness the same on all map sizes
var heightRange = {"min": MIN_HEIGHT * mapSize / 8192, "max": MAX_HEIGHT * mapSize / 8192};
// Since erosion is not predictable, actual water coverage can differ much with the same value
var averageWaterCoverage = scaleByMapSize(1/5, 1/3);
var heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min);
var heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT;
setWaterHeight(heightSeaGround);
var textueByHeight = [];
// Deep water
textueByHeight.push({"upperHeightLimit": heightRange.min + 1/3 * (heightSeaGroundAdjusted - heightRange.min), "terrain": "temp_sea_rocks"});
// Medium deep water (with fish)
var terrains = ["temp_sea_weed"];
terrains = terrains.concat(terrains, terrains, terrains, terrains);
terrains = terrains.concat(terrains, terrains, terrains, terrains);
terrains.push("temp_sea_weed|gaia/fauna_fish");
textueByHeight.push({"upperHeightLimit": heightRange.min + 2/3 * (heightSeaGroundAdjusted - heightRange.min), "terrain": terrains});
// Flat Water
textueByHeight.push({"upperHeightLimit": heightRange.min + 3/3 * (heightSeaGroundAdjusted - heightRange.min), "terrain": "temp_mud_a"});
// Water surroundings/bog (with stone/metal some rabits and bushes)
var terrains = ["temp_plants_bog", "temp_plants_bog_aut", "temp_dirt_gravel_plants", "temp_grass_d"];
terrains = terrains.concat(terrains, terrains, terrains, terrains, terrains);
terrains = ["temp_plants_bog|gaia/flora_bush_temperate"].concat(terrains, terrains);
terrains = ["temp_dirt_gravel_plants|gaia/geology_metal_temperate", "temp_dirt_gravel_plants|gaia/geology_stone_temperate", "temp_plants_bog|gaia/fauna_rabbit"].concat(terrains, terrains);
terrains = ["temp_plants_bog_aut|gaia/flora_tree_dead"].concat(terrains, terrains);
textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 1/6 * (heightRange.max - heightSeaGroundAdjusted), "terrain": terrains});
// Juicy grass near bog
textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 2/6 * (heightRange.max - heightSeaGroundAdjusted),
"terrain": ["temp_grass", "temp_grass_d", "temp_grass_long_b", "temp_grass_plants"]});
// Medium level grass
// var testActor = "actor|geology/decal_stone_medit_a.xml";
textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 3/6 * (heightRange.max - heightSeaGroundAdjusted),
"terrain": ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_mossy"]});
// Long grass near forest border
textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 4/6 * (heightRange.max - heightSeaGroundAdjusted),
"terrain": ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d", "temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"]});
// Forest border (With wood/food plants/deer/rabits)
var terrains = ["temp_grass_plants|gaia/flora_tree_euro_beech", "temp_grass_mossy|gaia/flora_tree_poplar", "temp_grass_mossy|gaia/flora_tree_poplar_lombardy",
"temp_grass_long|gaia/flora_bush_temperate", "temp_mud_plants|gaia/flora_bush_temperate", "temp_mud_plants|gaia/flora_bush_badlands",
"temp_grass_long|gaia/flora_tree_apple", "temp_grass_clovers|gaia/flora_bush_berry", "temp_grass_clovers_2|gaia/flora_bush_grapes",
"temp_grass_plants|gaia/fauna_deer", "temp_grass_long_b|gaia/fauna_rabbit"];
var numTerrains = terrains.length;
for (var i = 0; i < numTerrains; i++)
terrains.push("temp_grass_plants");
textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 5/6 * (heightRange.max - heightSeaGroundAdjusted), "terrain": terrains});
// Unpassable woods
textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 6/6 * (heightRange.max - heightSeaGroundAdjusted),
"terrain": ["temp_grass_mossy|gaia/flora_tree_oak", "temp_forestfloor_pine|gaia/flora_tree_pine",
"temp_grass_mossy|gaia/flora_tree_oak", "temp_forestfloor_pine|gaia/flora_tree_pine",
"temp_mud_plants|gaia/flora_tree_dead", "temp_plants_bog|gaia/flora_tree_oak_large",
"temp_dirt_gravel_plants|gaia/flora_tree_aleppo_pine", "temp_forestfloor_autumn|gaia/flora_tree_carob"]});
Engine.SetProgress(5);
var lowerHeightLimit = textueByHeight[3].upperHeightLimit;
var upperHeightLimit = textueByHeight[6].upperHeightLimit;
var playerPosition;
var playerIDs;
while (true)
{
- g_Map.log("Randomizing heightmap")
+ g_Map.log("Randomizing heightmap");
createArea(
new MapBoundsPlacer(),
new RandomElevationPainter(heightRange.min, heightRange.max));
// More cycles yield bigger structures
g_Map.log("Smoothing map");
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(2, 1, 20));
g_Map.log("Rescaling map");
rescaleHeightmap(heightRange.min, heightRange.max, g_Map.height);
g_Map.log("Mark valid heightrange for player starting positions");
let tHeightRange = g_Map.createTileClass();
let area = createArea(
new DiskPlacer(fractionToTiles(0.5) - MAP_BORDER_WIDTH, mapCenter),
new TileClassPainter(tHeightRange),
new HeightConstraint(lowerHeightLimit, upperHeightLimit));
let players = area && playerPlacementRandom(sortAllPlayers(), stayClasses(tHeightRange, 15), true);
if (players)
{
[playerIDs, playerPosition] = players;
break;
}
g_Map.log("Too few starting locations");
}
Engine.SetProgress(60);
g_Map.log("Painting terrain by height and add props");
var propDensity = 1; // 1 means as determined in the loop, less for large maps as set below
if (mapSize > 500)
propDensity = 1/4;
else if (mapSize > 400)
propDensity = 3/4;
for (let x = 0; x < mapSize; ++x)
for (let y = 0; y < mapSize; ++y)
{
let position = new Vector2D(x, y);
if (!g_Map.validHeight(position))
continue;
var textureMinHeight = heightRange.min;
for (var i = 0; i < textueByHeight.length; i++)
{
if (g_Map.getHeight(position) >= textureMinHeight && g_Map.getHeight(position) <= textueByHeight[i].upperHeightLimit)
{
createTerrain(textueByHeight[i].terrain).place(position);
let template;
if (i == 0) // ...deep water
{
if (randBool(propDensity / 100))
template = "actor|props/flora/pond_lillies_large.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/water_lillies.xml";
}
if (i == 1) // ...medium water (with fish)
{
if (randBool(propDensity / 200))
template = "actor|props/flora/pond_lillies_large.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/water_lillies.xml";
}
if (i == 2) // ...low water/mud
{
if (randBool(propDensity / 200))
template = "actor|props/flora/water_log.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/water_lillies.xml";
else if (randBool(propDensity / 40))
template = "actor|geology/highland_c.xml";
else if (randBool(propDensity / 20))
template = "actor|props/flora/reeds_pond_lush_b.xml";
else if (randBool(propDensity / 10))
template = "actor|props/flora/reeds_pond_lush_a.xml";
}
if (i == 3) // ...water suroundings/bog
{
if (randBool(propDensity / 200))
template = "actor|props/flora/water_log.xml";
else if (randBool(propDensity / 100))
template = "actor|geology/highland_c.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/reeds_pond_lush_a.xml";
}
if (i == 4) // ...low height grass
{
if (randBool(propDensity / 800))
template = "actor|props/flora/grass_field_flowering_tall.xml";
else if (randBool(propDensity / 400))
template = "actor|geology/gray_rock1.xml";
else if (randBool(propDensity / 200))
template = "actor|props/flora/bush_tempe_sm_lush.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/bush_tempe_b.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/grass_soft_small_tall.xml";
}
if (i == 5) // ...medium height grass
{
if (randBool(propDensity / 800))
template = "actor|geology/decal_stone_medit_a.xml";
else if (randBool(propDensity / 400))
template = "actor|props/flora/decals_flowers_daisies.xml";
else if (randBool(propDensity / 200))
template = "actor|props/flora/bush_tempe_underbrush.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/grass_soft_small_tall.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/grass_temp_field.xml";
}
if (i == 6) // ...high height grass
{
if (randBool(propDensity / 400))
template = "actor|geology/stone_granite_boulder.xml";
else if (randBool(propDensity / 200))
template = "actor|props/flora/foliagebush.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/bush_tempe_underbrush.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/grass_soft_small_tall.xml";
else if (randBool(propDensity / 20))
template = "actor|props/flora/ferns.xml";
}
if (i == 7) // ...forest border (with wood/food plants/deer/rabits)
{
if (randBool(propDensity / 400))
template = "actor|geology/highland_c.xml";
else if (randBool(propDensity / 200))
template = "actor|props/flora/bush_tempe_a.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/ferns.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/grass_soft_tuft_a.xml";
}
if (i == 8) // ...woods
{
if (randBool(propDensity / 200))
template = "actor|geology/highland2_moss.xml";
else if (randBool(propDensity / 100))
template = "actor|props/flora/grass_soft_tuft_a.xml";
else if (randBool(propDensity / 40))
template = "actor|props/flora/ferns.xml";
}
if (template)
g_Map.placeEntityAnywhere(template, 0, position, randomAngle());
break;
}
else
textureMinHeight = textueByHeight[i].upperHeightLimit;
}
}
Engine.SetProgress(90);
if (isNomad())
placePlayersNomad(g_Map.createTileClass(), new HeightConstraint(lowerHeightLimit, upperHeightLimit));
else
{
g_Map.log("Placing players and starting resources");
let resourceDistance = 8;
let resourceSpacing = 1;
let resourceCount = 4;
for (let i = 0; i < numPlayers; ++i)
{
placeCivDefaultStartingEntities(playerPosition[i], playerIDs[i], false);
for (let j = 1; j <= 4; ++j)
{
let uAngle = BUILDING_ORIENTATION - Math.PI * (2-j) / 2;
for (let k = 0; k < resourceCount; ++k)
{
let pos = Vector2D.sum([
playerPosition[i],
new Vector2D(resourceDistance, 0).rotate(-uAngle),
new Vector2D(k * resourceSpacing, 0).rotate(-uAngle - Math.PI/2),
new Vector2D(-0.75 * resourceSpacing * Math.floor(resourceCount / 2), 0).rotate(-uAngle - Math.PI/2)
]);
g_Map.placeEntityPassable(j % 2 ? "gaia/flora_tree_cypress" : "gaia/flora_bush_berry", 0, pos, randomAngle());
}
}
}
}
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js (revision 22419)
@@ -1,201 +1,201 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
var templateStone = "gaia/geology_stone_temperate";
var templateStoneMine = "gaia/geology_stonemine_temperate_quarry";
var templateMetalMine = "gaia/geology_metal_temperate_slabs";
var templateTemple = "gaia/ruins/unfinished_greek_temple";
var terrainPrimary = ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d", "temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"];
var terrainWood = ['temp_grass_mossy|gaia/flora_tree_oak', 'temp_forestfloor_pine|gaia/flora_tree_pine', 'temp_mud_plants|gaia/flora_tree_dead',
'temp_plants_bog|gaia/flora_tree_oak_large', "temp_dirt_gravel_plants|gaia/flora_tree_aleppo_pine", 'temp_forestfloor_autumn|gaia/flora_tree_carob']; //'temp_forestfloor_autumn|gaia/flora_tree_fig'
var terrainWoodBorder = ['temp_grass_plants|gaia/flora_tree_euro_beech', 'temp_grass_mossy|gaia/flora_tree_poplar', 'temp_grass_mossy|gaia/flora_tree_poplar_lombardy',
'temp_grass_long|gaia/flora_bush_temperate', 'temp_mud_plants|gaia/flora_bush_temperate', 'temp_mud_plants|gaia/flora_bush_badlands',
'temp_grass_long|gaia/flora_tree_apple', 'temp_grass_clovers|gaia/flora_bush_berry', 'temp_grass_clovers_2|gaia/flora_bush_grapes',
'temp_grass_plants|gaia/fauna_deer', "temp_grass_long_b|gaia/fauna_rabbit", "temp_grass_plants"];
var terrainBase = ["temp_dirt_gravel", "temp_grass_b"];
var terrainBaseBorder = ["temp_grass_b", "temp_grass_b", "temp_grass", "temp_grass_c", "temp_grass_mossy"];
var terrainBaseCenter = ['temp_dirt_gravel', 'temp_dirt_gravel', 'temp_grass_b'];
var terrainPath = ['temp_road', "temp_road_overgrown", 'temp_grass_b'];
var terrainHill = ["temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_cliff_a"];
var terrainHillBorder = ["temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_dirt_gravel_plants",
"temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_dirt_gravel_plants",
"temp_highlands", "temp_highlands", "temp_highlands", "temp_cliff_b", "temp_dirt_gravel_plants",
"temp_highlands", "temp_highlands", "temp_highlands", "temp_cliff_b", "temp_dirt_gravel_plants",
"temp_highlands|gaia/fauna_goat"];
var heightPath = -2;
var heightLand = 0;
var heightOffsetRandomPath = 1;
var g_Map = new RandomMap(heightLand, terrainPrimary);
var mapSize = g_Map.getSize();
var mapRadius = mapSize/2;
var mapCenter = g_Map.getCenter();
var clPlayer = g_Map.createTileClass();
var clPath = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var numPlayers = getNumPlayers();
var baseRadius = 20;
var minPlayerRadius = Math.min(mapRadius - 1.5 * baseRadius, 5/8 * mapRadius);
var maxPlayerRadius = Math.min(mapRadius - baseRadius, 3/4 * mapRadius);
var playerPosition = [];
var playerAngle = [];
var playerAngleStart = randomAngle();
var playerAngleAddAvrg = 2 * Math.PI / numPlayers;
var playerAngleMaxOff = playerAngleAddAvrg/4;
var radiusEC = Math.max(mapRadius/8, baseRadius/2);
var resourceRadius = fractionToTiles(1/3);
var resourcePerPlayer = [templateStone, templateMetalMine];
// For large maps there are memory errors with too many trees. A density of 256*192/mapArea works with 0 players.
// Around each player there is an area without trees so with more players the max density can increase a bit.
var maxTreeDensity = Math.min(256 * (192 + 8 * numPlayers) / Math.square(mapSize), 1); // Has to be tweeked but works ok
var bushChance = 1/3; // 1 means 50% chance in deepest wood, 0.5 means 25% chance in deepest wood
var playerIDs = sortAllPlayers();
for (var i=0; i < numPlayers; i++)
{
playerAngle[i] = (playerAngleStart + i * playerAngleAddAvrg + randFloat(0, playerAngleMaxOff)) % (2 * Math.PI);
playerPosition[i] = Vector2D.add(mapCenter, new Vector2D(randFloat(minPlayerRadius, maxPlayerRadius), 0).rotate(-playerAngle[i]).round());
}
Engine.SetProgress(10);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"BaseResourceClass": clBaseResource,
// player class painted below
"CityPatch": {
"radius": 0.8 * baseRadius,
"smoothness": 1/8,
"painters": [
new LayeredPainter([terrainBaseBorder, terrainBase, terrainBaseCenter], [baseRadius/4, baseRadius/4]),
new TileClassPainter(clPlayer)
]
},
"Chicken": {
},
"Berries": {
"template": "gaia/flora_bush_grapes",
"minCount": 2,
"maxCount": 2,
"distance": 12,
"minDist": 5,
"maxDist": 8
},
"Mines": {
"types": [
{ "template": templateMetalMine },
{ "template": templateStoneMine }
],
"minAngle": Math.PI / 2,
"maxAngle": Math.PI
},
"Trees": {
"template": "gaia/flora_tree_oak_large",
"count": 2
}
});
Engine.SetProgress(30);
g_Map.log("Painting paths");
var pathBlending = numPlayers <= 4;
for (let i = 0; i < numPlayers + (pathBlending ? 1 : 0); ++i)
for (let j = pathBlending ? 0 : i + 1; j < numPlayers + 1; ++j)
{
let pathStart = i < numPlayers ? playerPosition[i] : mapCenter;
let pathEnd = j < numPlayers ? playerPosition[j] : mapCenter;
createArea(
new RandomPathPlacer(pathStart, pathEnd, 1.25, baseRadius / 2, pathBlending),
[
new TerrainPainter(terrainPath),
new SmoothElevationPainter(ELEVATION_SET, heightPath, 2, heightOffsetRandomPath),
new TileClassPainter(clPath)
],
avoidClasses(clBaseResource, 4));
}
Engine.SetProgress(50);
g_Map.log("Placing expansion resources");
for (let i = 0; i < numPlayers; ++i)
for (let rIndex = 0; rIndex < resourcePerPlayer.length; ++rIndex)
{
let angleDist = numPlayers > 1 ?
(playerAngle[(i + 1) % numPlayers] - playerAngle[i] + 2 * Math.PI) % (2 * Math.PI) :
2 * Math.PI;
// they are supposed to be in between players on the same radius
let angle = playerAngle[i] + angleDist * (rIndex + 1) / (resourcePerPlayer.length + 1);
let position = Vector2D.add(mapCenter, new Vector2D(resourceRadius, 0).rotate(-angle)).round();
g_Map.placeEntityPassable(resourcePerPlayer[rIndex], 0, position, randomAngle());
createArea(
new ClumpPlacer(40, 1/2, 1/8, Infinity, position),
[
new LayeredPainter([terrainHillBorder, terrainHill], [1]),
new ElevationPainter(randFloat(1, 2)),
new TileClassPainter(clHill)
]);
}
Engine.SetProgress(60);
g_Map.log("Placing temple");
g_Map.placeEntityPassable(templateTemple, 0, mapCenter, randomAngle());
clBaseResource.add(mapCenter);
g_Map.log("Creating central mountain");
createArea(
new ClumpPlacer(Math.square(radiusEC), 1/2, 1/8, Infinity, mapCenter),
[
new LayeredPainter([terrainHillBorder, terrainHill], [radiusEC/4]),
new ElevationPainter(randFloat(1, 2)),
new TileClassPainter(clHill)
]);
// Woods and general hight map
for (var x = 0; x < mapSize; x++)
- for (var z = 0;z < mapSize;z++)
+ for (var z = 0; z < mapSize; z++)
{
let position = new Vector2D(x, z);
// The 0.5 is a correction for the entities placed on the center of tiles
var radius = mapCenter.distanceTo(Vector2D.add(position, new Vector2D(0.5, 0.5)));
var minDistToSL = mapSize;
for (var i=0; i < numPlayers; i++)
minDistToSL = Math.min(minDistToSL, position.distanceTo(playerPosition[i]));
// Woods tile based
var tDensFactSL = Math.max(Math.min((minDistToSL - baseRadius) / baseRadius, 1), 0);
var tDensFactRad = Math.abs((resourceRadius - radius) / resourceRadius);
var tDensFactEC = Math.max(Math.min((radius - radiusEC) / radiusEC, 1), 0);
var tDensActual = maxTreeDensity * tDensFactSL * tDensFactRad * tDensFactEC;
if (randBool(tDensActual) && g_Map.validTile(position))
{
let border = tDensActual < randFloat(0, bushChance * maxTreeDensity);
if (avoidClasses(clPath, 1, clHill, border ? 0 : 1).allows(position))
{
createTerrain(border ? terrainWoodBorder : terrainWood).place(position);
g_Map.setHeight(position, randFloat(0, 1));
clForest.add(position);
}
}
// General height map
let hVarMiddleHill = fractionToTiles(1 / 64) * (1 + Math.cos(3/2 * Math.PI * radius / mapRadius));
var hVarHills = 5 * (1 + Math.sin(x / 10) * Math.sin(z / 10));
g_Map.setHeight(position, g_Map.getHeight(position) + hVarMiddleHill + hVarHills + 1);
}
Engine.SetProgress(95);
placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clBaseResource, 4, clHill, 4));
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/dodecanese.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/dodecanese.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/dodecanese.js (revision 22419)
@@ -1,430 +1,430 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
TILE_CENTERED_HEIGHT_MAP = true;
const tCity = "medit_city_pavement";
const tCityPlaza = "medit_city_pavement";
const tHill = ["medit_grass_shrubs", "medit_rocks_grass_shrubs", "medit_rocks_shrubs", "medit_rocks_grass", "medit_shrubs"];
const tMainDirt = "medit_dirt";
const tCliff = "medit_cliff_aegean";
const tForestFloor = "medit_grass_wild";
const tPrimary = ["medit_grass_shrubs", "medit_grass_wild", "medit_rocks_grass_shrubs", "medit_dirt_b", "medit_plants_dirt", "medit_grass_flowers"];
const tDirt = "medit_dirt_b";
const tDirt2 = "medit_rocks_grass";
const tDirt3 = "medit_rocks_shrubs";
const tDirtCracks = "medit_dirt_c";
const tShoreLower = "medit_sand_wet";
const tShoreUpper = "medit_sand";
const tCoralsLower = "medit_sea_coral_deep";
const tCoralsUpper = "medit_sea_coral_plants";
const tWater = "medit_sea_depths";
const tLavaOuter = "LavaTest06";
const tLavaInner = "LavaTest05";
const oBerryBush = "gaia/flora_bush_berry";
const oDeer = "gaia/fauna_deer";
const oFish = "gaia/fauna_fish";
const oSheep = "gaia/fauna_sheep";
const oGoat = "gaia/fauna_goat";
const oRabbit = "gaia/fauna_rabbit";
const oStoneLarge = "gaia/geology_stonemine_medit_quarry";
const oStoneSmall = "gaia/geology_stone_mediterranean";
const oMetalLarge = "gaia/geology_metal_mediterranean_slabs";
const oMetalSmall = "gaia/geology_metal_mediterranean";
const oDatePalm = "gaia/flora_tree_cretan_date_palm_short";
const oSDatePalm = "gaia/flora_tree_cretan_date_palm_tall";
const oCarob = "gaia/flora_tree_carob";
const oFanPalm = "gaia/flora_tree_medit_fan_palm";
const oPoplar = "gaia/flora_tree_poplar_lombardy";
const oCypress = "gaia/flora_tree_cypress";
const oBush = "gaia/flora_bush_temperate";
const aBush1 = actorTemplate("props/flora/bush_medit_sm");
const aBush2 = actorTemplate("props/flora/bush_medit_me");
const aBush3 = actorTemplate("props/flora/bush_medit_la");
const aBush4 = actorTemplate("props/flora/bush_medit_me");
const aDecorativeRock = actorTemplate("geology/stone_granite_med");
const aBridge = actorTemplate("props/special/eyecandy/bridge_edge_wooden");
const aSmokeBig = actorTemplate("particle/smoke_volcano");
const aSmokeSmall = actorTemplate("particle/smoke_curved");
const pForest1 = [
tForestFloor,
tForestFloor + TERRAIN_SEPARATOR + oCarob,
tForestFloor + TERRAIN_SEPARATOR + oDatePalm,
tForestFloor + TERRAIN_SEPARATOR + oSDatePalm,
tForestFloor];
const pForest2 = [
tForestFloor,
tForestFloor + TERRAIN_SEPARATOR + oFanPalm,
tForestFloor + TERRAIN_SEPARATOR + oPoplar,
tForestFloor + TERRAIN_SEPARATOR + oCypress];
const heightSeaGround = -8;
const heightCoralsLower = -6;
const heightCoralsUpper = -4;
const heightSeaBump = -2.5;
const heightShoreLower = -2;
const heightBridge = -0.5;
const heightShoreUpper = 1;
const heightLand = 3;
const heightOffsetBump = 2;
const heightHill = 8;
const heightVolano = 25;
var g_Map = new RandomMap(heightSeaGround, tWater);
var numPlayers = getNumPlayers();
var clIsland = g_Map.createTileClass();
var clWater = g_Map.createTileClass();
var clPlayer = g_Map.createTileClass();
var clPlayerIsland = g_Map.createTileClass();
var clShore = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clGrass = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clVolcano = g_Map.createTileClass();
var clBridge = g_Map.createTileClass();
const playerIslandRadius = scaleByMapSize(20, 29);
const bridgeLength = 16;
const maxBridges = scaleByMapSize(2, 12);
var [playerIDs, playerPosition] = playerPlacementRandom(sortAllPlayers());
g_Map.log("Creating player islands");
for (let position of playerPosition)
createArea(
new ChainPlacer(2, 6, scaleByMapSize(15, 50), Infinity, position, 0, [playerIslandRadius]),
[
new TerrainPainter(tPrimary),
new SmoothElevationPainter(ELEVATION_SET, heightLand, 4),
new TileClassPainter(clIsland)
].concat(isNomad() ? [] : [new TileClassPainter(clPlayerIsland)]));
Engine.SetProgress(10);
g_Map.log("Creating islands");
createAreas(
new ChainPlacer(6, Math.floor(scaleByMapSize(8, 10)), Math.floor(scaleByMapSize(10, 35)), 0.2),
[
new TerrainPainter(tPrimary),
new SmoothElevationPainter(ELEVATION_SET, heightLand, 4),
new TileClassPainter(clIsland)
],
avoidClasses(clIsland, 6),
scaleByMapSize(25, 80));
Engine.SetProgress(20);
// Notice that the Constraints become much shorter when avoiding water rather than staying on islands
g_Map.log("Marking water");
createArea(
new MapBoundsPlacer(),
new TileClassPainter(clWater),
new HeightConstraint(-Infinity, heightShoreLower));
Engine.SetProgress(30);
g_Map.log("Creating undersea bumps");
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(4, 6)), Math.floor(scaleByMapSize(16, 40)), 0.5),
new SmoothElevationPainter(ELEVATION_SET, heightSeaBump, 3),
avoidClasses(clIsland, 2),
scaleByMapSize(10, 50));
Engine.SetProgress(35);
g_Map.log("Creating volcano");
var areasVolcano = createAreas(
new ClumpPlacer(diskArea(scaleByMapSize(4, 8)), 0.5, 0.5, 0.1),
[
new LayeredPainter([tLavaOuter, tLavaInner], [4]),
new SmoothElevationPainter(ELEVATION_SET, heightVolano, 6),
new TileClassPainter(clVolcano)
],
[
new NearTileClassConstraint(clIsland, 8),
avoidClasses(clHill, 5, clPlayerIsland, 0),
],
1,
200);
createBumps(avoidClasses(clWater, 0, clPlayer, 10, clVolcano, 0));
Engine.SetProgress(40);
g_Map.log("Creating large bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, 1),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 3),
avoidClasses(clWater, 2, clVolcano, 0, clPlayer, 10),
scaleByMapSize(20, 200));
Engine.SetProgress(45);
g_Map.log("Creating hills");
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(4, 6)), Math.floor(scaleByMapSize(16, 40)), 0.5),
[
new LayeredPainter([tCliff, tHill], [2]),
new SmoothElevationPainter(ELEVATION_SET, heightHill, 2),
new TileClassPainter(clHill)
],
avoidClasses(clWater, 1, clPlayer, 12, clVolcano, 0, clHill, 15),
scaleByMapSize(4, 13));
Engine.SetProgress(50);
g_Map.log("Painting corals");
paintTerrainBasedOnHeight(-Infinity, heightCoralsLower, Elevation_IncludeMin_ExcludeMax, tWater);
paintTerrainBasedOnHeight(heightCoralsLower, heightCoralsUpper, Elevation_IncludeMin_ExcludeMax, tCoralsLower);
paintTerrainBasedOnHeight(heightCoralsUpper, heightShoreLower, Elevation_IncludeMin_ExcludeMax, tCoralsUpper);
g_Map.log("Painting shoreline");
var areaShoreline = createArea(
new HeightPlacer(Elevation_IncludeMin_ExcludeMax, heightShoreLower, heightShoreUpper),
[
new TerrainPainter(tShoreLower),
new TileClassPainter(clShore)
],
avoidClasses(clVolcano, 0));
createArea(
new HeightPlacer(Elevation_IncludeMin_ExcludeMax, heightShoreUpper, heightLand),
new TerrainPainter(tShoreUpper),
avoidClasses(clVolcano, 0));
Engine.SetProgress(60);
g_Map.log("Creating dirt patches");
createLayeredPatches(
[scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)],
[tDirt3, tDirt2, [tDirt, tMainDirt], [tDirtCracks, tMainDirt]],
[1, 1, 1],
avoidClasses(clWater, 4, clVolcano, 2, clForest, 1, clDirt, 2, clGrass, 2, clHill, 1),
scaleByMapSize(15, 45),
clDirt);
Engine.SetProgress(65);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"BaseResourceClass": clBaseResource,
"PlayerTileClass": clPlayer,
"Walls": "towers",
"CityPatch": {
"radius": playerIslandRadius / 4,
"outerTerrain": tCityPlaza,
"innerTerrain": tCity
},
"Chicken": {
},
"Berries": {
"template": oBerryBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
// sufficient trees around
"Decoratives": {
"template": aBush1
}
});
Engine.SetProgress(70);
g_Map.log("Creating stone mines");
createMines(
[
[new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)],
[new SimpleObject(oStoneSmall, 2, 5, 1, 3)]
],
avoidClasses(clWater, 4, clVolcano, 4, clPlayerIsland, 0, clBaseResource, 4, clForest, 3, clMetal, 4, clRock, 4),
clRock,
scaleByMapSize(4, 16));
Engine.SetProgress(75);
g_Map.log("Creating metal mines");
createMines(
[
[new SimpleObject(oMetalSmall, 0, 1, 0, 4), new SimpleObject(oMetalLarge, 1, 1, 0, 4)],
[new SimpleObject(oMetalSmall, 2, 5, 1, 3)]
],
avoidClasses(clWater, 4, clPlayerIsland, 0, clVolcano, 4, clBaseResource, 4, clForest, 3, clMetal, 4, clRock, 4),
clMetal,
scaleByMapSize(4, 16));
Engine.SetProgress(80);
placePlayersNomad(clPlayer, avoidClasses(clWater, 12, clVolcano, 4, clMetal, 4, clRock, 4, clHill, 4));
var [forestTrees, stragglerTrees] = getTreeCounts(800, 4000, 0.7);
createForests(
[tForestFloor, tForestFloor, tForestFloor, pForest1, pForest2],
avoidClasses(clWater, 2, clPlayer, 4, clVolcano, 2, clForest, 1, clBaseResource, 4, clMetal, 4, clRock, 4),
clForest,
forestTrees,
200);
Engine.SetProgress(85);
createFood(
[
[new SimpleObject(oSheep, 5, 7, 0, 4)],
[new SimpleObject(oGoat, 2, 4, 0, 3)],
[new SimpleObject(oDeer, 2, 4, 0, 2)],
[new SimpleObject(oRabbit, 3, 9, 0, 4)],
[new SimpleObject(oBerryBush, 3, 5, 0, 4)]
],
[
scaleByMapSize(5, 20),
scaleByMapSize(5, 20),
scaleByMapSize(5, 20),
scaleByMapSize(5, 20),
3 * numPlayers
],
avoidClasses(clWater, 1, clPlayer, 15, clVolcano, 4, clBaseResource, 4, clHill, 2, clMetal, 4, clRock, 4),
clFood);
Engine.SetProgress(87);
createFood(
[
[new SimpleObject(oFish, 2, 3, 0, 2)]
],
[
3 * numPlayers
],
avoidClasses(clIsland, 8, clFood, 10, clVolcano, 4),
clFood);
createStragglerTrees(
[oPoplar, oCypress, oFanPalm, oDatePalm, oSDatePalm],
avoidClasses(clWater, 1, clVolcano, 4, clPlayer, 12, clForest, 1, clMetal, 4, clRock, 4),
clForest,
stragglerTrees,
200);
g_Map.log("Creating bushes");
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(oBush, 3, 5, 0, 4)], true),
0,
[avoidClasses(clWater, 1, clVolcano, 4, clPlayer, 5, clForest, 1, clBaseResource, 4, clMetal, 4, clRock, 4)],
scaleByMapSize(20, 50));
createDecoration(
[
[
new SimpleObject(aDecorativeRock, 1, 3, 0, 1)
],
[
new SimpleObject(aBush2, 1, 2, 0, 1),
new SimpleObject(aBush1, 1, 3, 0, 2),
new SimpleObject(aBush4, 1, 2, 0, 1),
new SimpleObject(aBush3, 1, 3, 0, 2)
]
],
[
scaleByMapSize(16, 262),
scaleByMapSize(40, 360)
],
avoidClasses(clWater, 4, clPlayer, 5, clVolcano, 4, clForest, 1, clBaseResource, 4, clRock, 4, clMetal, 4, clHill, 1));
g_Map.log("Creating bridges");
var bridges = 0;
for (let bridgeStart of shuffleArray(areaShoreline.getPoints()))
{
if (new NearTileClassConstraint(clBridge, bridgeLength * 8).allows(bridgeStart))
continue;
for (let direction = 0; direction < 4; ++direction)
{
let bridgeAngle = direction * Math.PI / 2;
let bridgeDirection = new Vector2D(1, 0).rotate(bridgeAngle);
let areaOffset = new Vector2D(1, 1);
let bridgeOffset = new Vector2D(direction % 2 ? 2 : 0, direction % 2 ? 0 : 2);
let bridgeCenter1 = Vector2D.add(bridgeStart, Vector2D.mult(bridgeDirection, bridgeLength / 2));
let bridgeCenter2 = Vector2D.add(bridgeCenter1, bridgeOffset);
if (avoidClasses(clWater, 0).allows(bridgeCenter1) && avoidClasses(clWater, 0).allows(bridgeCenter2))
continue;
let bridgeEnd1 = Vector2D.add(bridgeStart, Vector2D.mult(bridgeDirection, bridgeLength));
let bridgeEnd2 = Vector2D.add(bridgeEnd1, bridgeOffset);
if (avoidClasses(clShore, 0).allows(bridgeEnd1) && avoidClasses(clShore, 0).allows(bridgeEnd2))
continue;
let bridgePerpendicular = bridgeDirection.perpendicular();
let bridgeP = Vector2D.mult(bridgePerpendicular, bridgeLength / 2).round();
if (avoidClasses(clWater, 0).allows(Vector2D.add(bridgeCenter1, bridgeP)) ||
avoidClasses(clWater, 0).allows(Vector2D.sub(bridgeCenter2, bridgeP)))
continue;
++bridges;
// This bridge model is not centered on the horizontal plane, so the angle is messy
// TILE_CENTERED_HEIGHT_MAP also influences the outcome of the placement.
let bridgeOrientation = direction % 2 ? 0 : Math.PI / 2;
bridgeCenter1[direction % 2 ? "y" : "x"] += 0.25;
- bridgeCenter2[direction % 2 ? "y" : "x"] -= 0.25
+ bridgeCenter2[direction % 2 ? "y" : "x"] -= 0.25;
g_Map.placeEntityAnywhere(aBridge, 0, bridgeCenter1, bridgeOrientation);
g_Map.placeEntityAnywhere(aBridge, 0, bridgeCenter2, bridgeOrientation + Math.PI);
createArea(
new RectPlacer(Vector2D.sub(bridgeStart, areaOffset), Vector2D.add(bridgeEnd1, areaOffset)),
[
new ElevationPainter(heightBridge),
new TileClassPainter(clBridge)
]);
for (let center of [bridgeStart, bridgeEnd2])
createArea(
new DiskPlacer(2, center),
new SmoothingPainter(1, 1, 1));
break;
}
if (bridges >= maxBridges)
- break
+ break;
}
g_Map.log("Creating smoke");
if (areasVolcano.length)
{
createObjectGroupsByAreas(
new SimpleGroup([new SimpleObject(aSmokeBig, 1, 1, 0, 4)], false),
0,
stayClasses(clVolcano, 6),
scaleByMapSize(4, 12),
20,
areasVolcano);
createObjectGroupsByAreas(
new SimpleGroup([new SimpleObject(aSmokeSmall, 2, 2, 0, 4)], false),
0,
stayClasses(clVolcano, 4),
scaleByMapSize(4, 12),
20,
areasVolcano);
}
Engine.SetProgress(90);
setSkySet("cumulus");
setSunColor(0.87, 0.78, 0.49);
setWaterColor(0, 0.501961, 1);
setWaterTint(0.5, 1, 1);
setWaterWaviness(4.0);
setWaterType("ocean");
setWaterMurkiness(0.49);
setFogFactor(0.3);
setFogThickness(0.25);
setPPEffect("hdr");
setPPContrast(0.62);
setPPSaturation(0.51);
setPPBloom(0.12);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js (revision 22419)
@@ -1,26 +1,26 @@
var elephantinePlayerID = 0;
Trigger.prototype.InitElephantine = function()
{
this.InitElephantine_DefenderStance();
this.InitElephantine_GarrisonBuildings();
};
Trigger.prototype.InitElephantine_DefenderStance = function()
{
for (let ent of TriggerHelper.GetPlayerEntitiesByClass(elephantinePlayerID, "Soldier"))
- TriggerHelper.SetUnitStance(ent, "defensive")
+ TriggerHelper.SetUnitStance(ent, "defensive");
};
Trigger.prototype.InitElephantine_GarrisonBuildings = function()
{
let kushInfantryUnits = TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Infantry", "kush", undefined, "Elite", true);
let kushSupportUnits = TriggerHelper.GetTemplateNamesByClasses("FemaleCitizen Healer", "kush", undefined, "Elite", true);
TriggerHelper.SpawnAndGarrisonAtClasses(elephantinePlayerID, "Tower", kushInfantryUnits, 1);
TriggerHelper.SpawnAndGarrisonAtClasses(elephantinePlayerID, "Wonder Temple Pyramid", kushInfantryUnits.concat(kushSupportUnits), 1);
};
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "InitElephantine", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/maps/random/fields_of_meroe.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/fields_of_meroe.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/fields_of_meroe.js (revision 22419)
@@ -1,451 +1,451 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("rmbiome");
if (g_MapSettings.Biome)
setSelectedBiome();
else
setBiome("fields_of_meroe/dry");
const tMainDirt = g_Terrains.mainDirt;
const tSecondaryDirt = g_Terrains.secondaryDirt;
const tDirt = g_Terrains.dirt;
const tLush = "desert_grass_a";
const tSLush = "desert_grass_a_sand";
const tFarmland = "desert_farmland";
const tRoad = "savanna_tile_a";
const tRoadWild = "desert_city_tile";
const tRiverBank = "savanna_riparian_wet";
const tForestFloor = "savanna_forestfloor_b";
const oBush = g_Gaia.berry;
const oBaobab = "gaia/flora_tree_baobab";
const oAcacia = "gaia/flora_tree_acacia";
const oDatePalm = "gaia/flora_tree_date_palm";
const oSDatePalm = "gaia/flora_tree_cretan_date_palm_short";
const oGazelle = "gaia/fauna_gazelle";
const oGiraffe = "gaia/fauna_giraffe";
const oLion = "gaia/fauna_lion";
const oFish = "gaia/fauna_fish";
const oHawk = "gaia/fauna_hawk";
const oStoneLarge = "gaia/geology_stonemine_savanna_quarry";
const oStoneSmall = "gaia/geology_stone_desert_small";
const oMetalLarge = "gaia/geology_metal_savanna_slabs";
const oMetalSmall = "gaia/geology_metal_desert_small";
const oHouse = "structures/kush_house";
const oFarmstead = "structures/kush_farmstead";
const oField = "structures/kush_field";
const oPyramid = "structures/kush_pyramid_small";
const oPyramidLarge = "structures/kush_pyramid_large";
const oKushUnits = isNomad() ?
"units/kush_support_female_citizen" :
"units/kush_infantry_javelinist_merc_e";
const aRain = g_Decoratives.rain;
const aBushA = g_Decoratives.bushA;
const aBushB = g_Decoratives.bushB;
const aBushes = [aBushA, aBushB];
const aReeds = "actor|props/flora/reeds_pond_lush_a.xml";
const aRockA = g_Decoratives.rock;
const aRockB = "actor|geology/shoreline_large.xml";
const aRockC = "actor|geology/shoreline_small.xml";
const pForestP = [tForestFloor + TERRAIN_SEPARATOR + oAcacia, tForestFloor];
const heightSeaGround = g_Heights.seaGround;
const heightReedsDepth = -2.5;
const heightCataract = -1;
const heightShore = 1;
const heightLand = 2;
const heightDunes = 11;
const heightOffsetBump = 1.4;
const heightOffsetBumpPassage = 4;
const g_Map = new RandomMap(heightLand, tMainDirt);
const numPlayers = getNumPlayers();
const mapCenter = g_Map.getCenter();
const mapBounds = g_Map.getBounds();
var clPlayer = g_Map.createTileClass();
var clKushiteVillages = g_Map.createTileClass();
var clRiver = g_Map.createTileClass();
var clShore = g_Map.createTileClass();
var clDunes = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clRain = g_Map.createTileClass();
var clCataract = g_Map.createTileClass();
var kushVillageBuildings = {
"houseA": { "template": oHouse, "offset": new Vector2D(5, 5) },
"houseB": { "template": oHouse, "offset": new Vector2D(5, 0) },
"houseC": { "template": oHouse, "offset": new Vector2D(5, -5) },
"farmstead": { "template": oFarmstead, "offset": new Vector2D(-5, 0) },
"fieldA": { "template": oField, "offset": new Vector2D(-5, 5) },
"fieldB": { "template": oField, "offset": new Vector2D(-5, -5) },
"pyramid": { "template": oPyramid, "offset": new Vector2D(0, -5) }
};
const riverTextures = [
{
"left": fractionToTiles(0),
"right": fractionToTiles(0.04),
"terrain": tLush,
"tileClass": clShore
},
{
"left": fractionToTiles(0.04),
"right": fractionToTiles(0.06),
"terrain": tSLush,
"tileClass": clShore
}
];
const riverAngle = Math.PI/5;
paintRiver({
"parallel": false,
"start": new Vector2D(fractionToTiles(0.25), mapBounds.top).rotateAround(riverAngle, mapCenter),
"end": new Vector2D(fractionToTiles(0.25), mapBounds.bottom).rotateAround(riverAngle, mapCenter),
"width": scaleByMapSize(12, 36),
"fadeDist": scaleByMapSize(3, 12),
"deviation": 1,
"heightRiverbed": heightSeaGround,
"heightLand": heightShore,
"meanderShort": 14,
"meanderLong": 18,
"waterFunc": (position, height, z) => {
clRiver.add(position);
createTerrain(tRiverBank).place(position);
},
"landFunc": (position, shoreDist1, shoreDist2) => {
for (let riv of riverTextures)
if (riv.left < +shoreDist1 && +shoreDist1 < riv.right || riv.left < -shoreDist2 && -shoreDist2 < riv.right)
{
riv.tileClass.add(position);
if (riv.terrain)
createTerrain(riv.terrain).place(position);
}
}
});
Engine.SetProgress(10);
g_Map.log("Creating cataracts");
for (let x of [fractionToTiles(randFloat(0.15, 0.25)), fractionToTiles(randFloat(0.75, 0.85))])
{
let anglePassage = riverAngle + Math.PI / 2 * randFloat(0.8, 1.2);
let areaPassage = createArea(
new PathPlacer(
new Vector2D(x, mapBounds.bottom).rotateAround(anglePassage, mapCenter),
new Vector2D(x, mapBounds.top).rotateAround(anglePassage, mapCenter),
scaleByMapSize(20, 30),
0,
1,
0,
0,
Infinity),
[
new SmoothElevationPainter(ELEVATION_SET, heightCataract, 2),
new TileClassPainter(clCataract)
],
new HeightConstraint(-Infinity, 0));
createAreasInAreas(
new ClumpPlacer(4, 0.4, 0.6, 0.5),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBumpPassage, 2),
undefined,
scaleByMapSize(15, 30),
20,
[areaPassage]);
createObjectGroupsByAreas(
new SimpleGroup([new SimpleObject(aReeds, 2, 4, 0, 1)], true),
0,
undefined,
scaleByMapSize(20, 50),
20,
- [areaPassage])
+ [areaPassage]);
}
var [playerIDs, playerPosition] = playerPlacementRandom(sortAllPlayers(), avoidClasses(clRiver, 15, clPlayer, 30));
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad,
"radius": 10,
"width": 3,
"painters": [new TileClassPainter(clPlayer)]
},
"Chicken": {
},
"Berries": {
"template": oBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{
"type": "stone_formation",
"template": oStoneSmall,
"terrain": tSecondaryDirt
}
],
"groupElements": [new RandomObject(aBushes, 2, 4, 2, 3)]
},
"Trees": {
"template": pickRandom([oBaobab, oAcacia]),
"count": 3
}
});
Engine.SetProgress(15);
g_Map.log("Getting random coordinates for Kushite settlements");
var kushiteTownPositions = [];
for (let retryCount = 0; retryCount < scaleByMapSize(3, 10); ++retryCount)
{
let coordinate = g_Map.randomCoordinate(true);
if (new AndConstraint(avoidClasses(clPlayer, 40, clForest, 5, clKushiteVillages, 50, clRiver, 15)).allows(coordinate))
{
kushiteTownPositions.push(coordinate);
createArea(
new ClumpPlacer(40, 0.6, 0.3, Infinity, coordinate),
[
new TerrainPainter(tRoad),
new TileClassPainter(clKushiteVillages)
]);
}
}
g_Map.log("Placing the Kushite buildings");
for (let coordinate of kushiteTownPositions)
{
for (let building in kushVillageBuildings)
g_Map.placeEntityPassable(kushVillageBuildings[building].template, 0, Vector2D.add(coordinate, kushVillageBuildings[building].offset), Math.PI);
createObjectGroup(new SimpleGroup([new SimpleObject(oKushUnits, 5, 7, 1, 2)], true, clKushiteVillages, coordinate), 0);
}
g_Map.log("Creating kushite pyramids");
createObjectGroups(
new SimpleGroup([new SimpleObject(oPyramidLarge, 1, 1, 0, 1)], true, clKushiteVillages),
0,
avoidClasses(clPlayer, 20, clForest, 5, clKushiteVillages, 30, clRiver, 10),
scaleByMapSize(1, 7),
200);
Engine.SetProgress(20);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, 1),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 2),
new StaticConstraint(avoidClasses(clPlayer, 5, clKushiteVillages, 10, clRiver, 20)),
scaleByMapSize(300, 800));
g_Map.log("Creating dunes");
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(4, 6)), Math.floor(scaleByMapSize(5, 15)), 0.5),
[
new SmoothElevationPainter(ELEVATION_SET, heightDunes, 2),
new TileClassPainter(clDunes)
],
avoidClasses(clPlayer, 3, clRiver, 20, clDunes, 10, clKushiteVillages, 10),
scaleByMapSize(1, 3) * numPlayers * 3);
Engine.SetProgress(25);
var [forestTrees, stragglerTrees] = getTreeCounts(400, 2000, 0.7);
createForests(
[tMainDirt[0], tForestFloor, tForestFloor, pForestP, pForestP],
avoidClasses(clPlayer, 20, clForest, 20, clDunes, 2, clRiver, 20, clKushiteVillages, 10),
clForest,
forestTrees);
Engine.SetProgress(40);
g_Map.log("Creating dirt patches");
for (let size of [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)])
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5),
new LayeredPainter([tSecondaryDirt, tDirt], [1]),
avoidClasses(clDunes, 0, clForest, 0, clPlayer, 5, clRiver, 10),
scaleByMapSize(50, 90));
g_Map.log("Creating patches of farmland");
for (let size of [scaleByMapSize(30, 40), scaleByMapSize(35, 50)])
createAreas(
new ClumpPlacer(size, 0.4, 0.6),
new TerrainPainter(tFarmland),
avoidClasses(clDunes, 3, clForest, 3, clPlayer, 5, clKushiteVillages, 5, clRiver, 10),
scaleByMapSize(1, 10));
Engine.SetProgress(60);
g_Map.log("Creating stone mines");
createObjectGroups(
new SimpleGroup(
[
new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1),
new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)
], true, clRock),
0,
avoidClasses(clRiver, 4, clCataract, 4, clPlayer, 20, clRock, 15, clKushiteVillages, 5, clDunes, 2, clForest, 4),
scaleByMapSize(2, 8),
50);
g_Map.log("Creating small stone quarries");
createObjectGroups(
new SimpleGroup([new SimpleObject(oStoneSmall, 2, 5, 1, 3, 0, 2 * Math.PI, 1)], true, clRock),
0,
avoidClasses(clRiver, 4, clCataract, 4, clPlayer, 20, clRock, 15, clKushiteVillages, 5, clDunes, 2, clForest, 4),
scaleByMapSize(2, 8),
50);
g_Map.log("Creating metal mines");
createObjectGroups(
new SimpleGroup(
[
new SimpleObject(oMetalSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1),
new SimpleObject(oMetalLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)
], true, clMetal),
0,
avoidClasses(clRiver, 4, clCataract, 4, clPlayer, 20, clRock, 10, clMetal, 15, clKushiteVillages, 5, clDunes, 2, clForest, 4),
scaleByMapSize(2, 8),
50);
g_Map.log("Creating small metal quarries");
createObjectGroups(
new SimpleGroup([new SimpleObject(oMetalSmall, 2, 5, 1, 3, 0, 2 * Math.PI, 1)], true, clMetal),
0,
avoidClasses(clRiver, 4, clCataract, 4, clPlayer, 20, clRock, 10, clMetal, 15, clKushiteVillages, 5, clDunes, 2, clForest, 4),
scaleByMapSize(2, 8),
50);
Engine.SetProgress(70);
g_Map.log("Creating gazelle");
createObjectGroups(
new SimpleGroup([new SimpleObject(oGazelle, 4, 6, 1, 4)], true, clFood),
0,
avoidClasses(clForest, 0, clKushiteVillages, 10, clPlayer, 5, clDunes, 1, clFood, 25, clRiver, 2, clMetal, 4, clRock, 4),
2 * numPlayers,
50);
g_Map.log("Creating giraffe");
createObjectGroups(
new SimpleGroup([new SimpleObject(oGiraffe, 4, 6, 1, 4)], true, clFood),
0,
avoidClasses(clForest, 0, clKushiteVillages, 10, clPlayer, 5, clDunes, 1, clFood, 25, clRiver, 2, clMetal, 4, clRock, 4),
2 * numPlayers,
50);
g_Map.log("Creating lions");
if (!isNomad())
createObjectGroups(
new SimpleGroup([new SimpleObject(oLion, 2, 3, 0, 2)], true, clFood),
0,
avoidClasses(clForest, 0, clKushiteVillages, 10, clPlayer, 5, clDunes, 1, clFood, 25, clRiver, 2, clMetal, 4, clRock, 4),
3 * numPlayers,
50);
g_Map.log("Creating hawk");
for (let i = 0; i < scaleByMapSize(1, 3); ++i)
g_Map.placeEntityAnywhere(oHawk, 0, mapCenter, randomAngle());
g_Map.log("Creating fish");
createObjectGroups(
new SimpleGroup([new SimpleObject(oFish, 1, 2, 0, 1)], true, clFood),
0,
[stayClasses(clRiver, 4), avoidClasses(clFood, 16, clCataract, 10)],
scaleByMapSize(15, 80),
50);
Engine.SetProgress(80);
createStragglerTrees(
[oBaobab, oAcacia],
avoidClasses(clForest, 3, clFood, 1, clDunes, 1, clPlayer, 1, clMetal, 6, clRock, 6, clRiver, 15, clKushiteVillages, 15),
clForest,
stragglerTrees);
createStragglerTrees(
[oBaobab, oAcacia],
avoidClasses(clForest, 1, clFood, 1, clDunes, 3, clPlayer, 1, clMetal, 6, clRock, 6, clRiver, 15, clKushiteVillages, 15),
clForest,
stragglerTrees * (isNomad() ? 3 : 1));
createStragglerTrees(
[oDatePalm, oSDatePalm],
[avoidClasses(clPlayer, 5, clFood, 1), stayClasses(clShore, 2)],
clForest,
stragglerTrees * 10);
Engine.SetProgress(90);
g_Map.log("Creating reeds on the shore");
createObjectGroups(
new SimpleGroup([new SimpleObject(aReeds, 3, 5, 0, 1)], true),
0,
[
new HeightConstraint(heightReedsDepth, heightShore),
avoidClasses(clCataract, 2)
],
scaleByMapSize(500, 1000),
50);
g_Map.log("Creating small decorative rocks");
createObjectGroups(
new SimpleGroup([new SimpleObject(aRockA, 2, 4, 0, 1)], true),
0,
avoidClasses(clForest, 0, clPlayer, 0, clDunes, 0, clRiver, 5, clCataract, 5, clMetal, 4, clRock, 4),
scaleByMapSize(16, 262),
50);
createObjectGroups(
new SimpleGroup([new SimpleObject(aRockB, 1, 2, 0, 1), new SimpleObject(aRockC, 1, 3, 0, 1)], true),
0,
[
new NearTileClassConstraint(clCataract, 5),
new HeightConstraint(-Infinity, heightShore)
],
scaleByMapSize(30, 50),
50);
g_Map.log("Creating bushes");
createObjectGroups(
new SimpleGroup([new SimpleObject(aBushB, 1, 2, 0, 1), new SimpleObject(aBushA, 1, 3, 0, 2)], true),
0,
avoidClasses(clForest, 0, clPlayer, 0, clDunes, 0, clRiver, 15, clMetal, 4, clRock, 4),
scaleByMapSize(50, 500),
50);
Engine.SetProgress(95);
g_Map.log("Creating rain drops");
if (aRain)
createObjectGroups(
new SimpleGroup([new SimpleObject(aRain, 1, 1, 1, 4)], true, clRain),
0,
avoidClasses(clRain, 5),
scaleByMapSize(60, 200));
Engine.SetProgress(98);
placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clKushiteVillages, 18, clMetal, 4, clRock, 4, clDunes, 4, clFood, 2, clRiver, 5));
setSunElevation(Math.PI / 8);
setSunRotation(randomAngle());
setSunColor(0.746, 0.718, 0.539);
setWaterColor(0.292, 0.347, 0.691);
setWaterTint(0.550, 0.543, 0.437);
setFogColor(0.8, 0.76, 0.61);
setFogThickness(0.2);
setFogFactor(0.2);
setPPEffect("hdr");
setPPContrast(0.65);
setPPSaturation(0.42);
setPPBloom(0.6);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/flood.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/flood.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/flood.js (revision 22419)
@@ -1,299 +1,299 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("rmbiome");
setSelectedBiome();
const tMainTerrain = g_Terrains.mainTerrain;
const tForestFloor1 = g_Terrains.forestFloor1;
const tForestFloor2 = g_Terrains.forestFloor2;
const tCliff = g_Terrains.cliff;
const tTier1Terrain = g_Terrains.tier1Terrain;
const tTier2Terrain = g_Terrains.tier2Terrain;
const tTier3Terrain = g_Terrains.tier3Terrain;
const tRoad = g_Terrains.road;
const tRoadWild = g_Terrains.roadWild;
const tTier4Terrain = g_Terrains.tier4Terrain;
const tShore = g_Terrains.shore;
const tWater = g_Terrains.water;
var tHill = g_Terrains.hill;
var tDirt = g_Terrains.dirt;
if (currentBiome() == "generic/temperate")
{
tDirt = ["medit_shrubs_a", "grass_field"];
tHill = ["grass_field", "peat_temp"];
}
const oTree1 = g_Gaia.tree1;
const oTree2 = g_Gaia.tree2;
const oTree3 = g_Gaia.tree3;
const oTree4 = g_Gaia.tree4;
const oTree5 = g_Gaia.tree5;
const oFruitBush = g_Gaia.fruitBush;
const oMainHuntableAnimal = g_Gaia.mainHuntableAnimal;
const oFish = g_Gaia.fish;
const oSecondaryHuntableAnimal = g_Gaia.secondaryHuntableAnimal;
const oStoneLarge = g_Gaia.stoneLarge;
const oMetalLarge = g_Gaia.metalLarge;
const aGrass = g_Decoratives.grass;
const aGrassShort = g_Decoratives.grassShort;
const aRockLarge = g_Decoratives.rockLarge;
const aRockMedium = g_Decoratives.rockMedium;
const aBushMedium = g_Decoratives.bushMedium;
const aBushSmall = g_Decoratives.bushSmall;
const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2];
const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1];
const heightSeaGround = -2;
const heightLand = 2;
const shoreRadius = 6;
var g_Map = new RandomMap(heightSeaGround, tWater);
const clPlayer = g_Map.createTileClass();
const clHill = g_Map.createTileClass();
const clMountain = g_Map.createTileClass();
const clForest = g_Map.createTileClass();
const clDirt = g_Map.createTileClass();
const clRock = g_Map.createTileClass();
const clMetal = g_Map.createTileClass();
const clFood = g_Map.createTileClass();
const clBaseResource = g_Map.createTileClass();
const numPlayers = getNumPlayers();
const mapSize = g_Map.getSize();
const mapCenter = g_Map.getCenter();
-g_Map.log("Creating player islands...")
+g_Map.log("Creating player islands...");
var [playerIDs, playerPosition] = playerPlacementCircle(fractionToTiles(0.38));
for (let i = 0; i < numPlayers; ++i)
createArea(
new ClumpPlacer(diskArea(1.4 * defaultPlayerBaseRadius()), 0.8, 0.1, Infinity, playerPosition[i]),
[
new LayeredPainter([tShore, tMainTerrain], [shoreRadius]),
new SmoothElevationPainter(ELEVATION_SET, heightLand, shoreRadius),
new TileClassPainter(clHill)
]);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"PlayerTileClass": clPlayer,
"BaseResourceClass": clBaseResource,
"Walls": false,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad
},
"Chicken": {
},
"Berries": {
"template": oFruitBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
"Trees": {
"template": oTree2,
"count": 50,
"maxDist": 16,
"maxDistGroup": 7
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(40);
g_Map.log("Creating central island");
createArea(
new ChainPlacer(
6,
Math.floor(scaleByMapSize(10, 15)),
Math.floor(scaleByMapSize(200, 300)),
Infinity,
mapCenter,
0,
[Math.floor(fractionToTiles(0.01))]),
[
new LayeredPainter([tShore, tMainTerrain], [shoreRadius, 100]),
new SmoothElevationPainter(ELEVATION_SET, heightLand, shoreRadius),
new TileClassPainter(clHill)
],
avoidClasses(clPlayer, 40));
for (let m = 0; m < randIntInclusive(20, 34); ++m)
{
let elevRand = randIntInclusive(6, 20);
createArea(
new ChainPlacer(
7,
15,
Math.floor(scaleByMapSize(15, 20)),
Infinity,
new Vector2D(fractionToTiles(randFloat(0, 1)), fractionToTiles(randFloat(0, 1))),
0,
[Math.floor(fractionToTiles(0.01))]),
[
new LayeredPainter([tDirt, tHill], [Math.floor(elevRand / 3), 40]),
new SmoothElevationPainter(ELEVATION_SET, elevRand, Math.floor(elevRand / 3)),
new TileClassPainter(clHill)
],
[avoidClasses(clBaseResource, 2, clPlayer, 40), stayClasses(clHill, 6)]);
}
for (let m = 0; m < randIntInclusive(8, 17); ++m)
{
let elevRand = randIntInclusive(15, 29);
createArea(
new ChainPlacer(
5,
8,
Math.floor(scaleByMapSize(15, 20)),
Infinity,
new Vector2D(randIntExclusive(0, mapSize), randIntExclusive(0, mapSize)),
0,
[Math.floor(fractionToTiles(0.01))]),
[
new LayeredPainter([tCliff, tForestFloor2], [Math.floor(elevRand / 3), 40]),
new SmoothElevationPainter(ELEVATION_MODIFY, elevRand, Math.floor(elevRand / 3)),
new TileClassPainter(clMountain)
],
[avoidClasses(clBaseResource, 2, clPlayer, 40), stayClasses(clHill, 6)]);
}
g_Map.log("Creating center bounty");
createObjectGroup(
new SimpleGroup(
[new SimpleObject(oMetalLarge, 3, 6, 25, Math.floor(fractionToTiles(0.25)))],
true,
clBaseResource,
mapCenter),
0,
[avoidClasses(clBaseResource, 20, clPlayer, 40, clMountain, 4), stayClasses(clHill, 10)]);
createObjectGroup(
new SimpleGroup(
[new SimpleObject(oStoneLarge, 3, 6, 25, Math.floor(fractionToTiles(0.25)))],
true,
clBaseResource,
mapCenter),
0,
[avoidClasses(clBaseResource, 20, clPlayer, 40, clMountain, 4), stayClasses(clHill, 10)]);
g_Map.log("Creating fish");
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(oFish, 2, 3, 0, 2)], true, clFood),
0,
avoidClasses(clHill, 10, clFood, 20),
10 * numPlayers,
60);
var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(0.7));
createForests(
[tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2],
[avoidClasses(clPlayer, 25, clForest, 10, clBaseResource, 3, clMetal, 6, clRock, 6, clMountain, 2), stayClasses(clHill, 6)],
clForest,
forestTrees);
let types = [oTree1, oTree2, oTree4, oTree3];
createStragglerTrees(
types,
[avoidClasses(clBaseResource, 2, clMetal, 6, clRock, 6, clMountain, 2, clPlayer, 25), stayClasses(clHill, 6)],
clForest,
stragglerTrees);
Engine.SetProgress(65);
g_Map.log("Creating dirt patches");
var numb = currentBiome() == "generic/savanna" ? 3 : 1;
for (let size of [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)])
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5),
[
new LayeredPainter([[tMainTerrain, tTier1Terrain], [tTier1Terrain, tTier2Terrain], [tTier2Terrain, tTier3Terrain]], [1, 1]),
new TileClassPainter(clDirt)
],
avoidClasses(clForest, 0, clMountain, 0, clDirt, 5, clPlayer, 10),
numb * scaleByMapSize(15, 45));
g_Map.log("Painting shorelines");
paintTerrainBasedOnHeight(1, heightLand, 0, tMainTerrain);
paintTerrainBasedOnHeight(heightSeaGround, 1, 3, tTier1Terrain);
g_Map.log("Creating grass patches");
for (let size of [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)])
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5),
new TerrainPainter(tTier4Terrain),
avoidClasses(clForest, 0, clMountain, 0, clDirt, 5, clPlayer, 10),
numb * scaleByMapSize(15, 45));
createFood(
[
[new SimpleObject(oMainHuntableAnimal, 5, 7, 0, 4)],
[new SimpleObject(oSecondaryHuntableAnimal, 2, 3, 0, 2)]
],
[3 * numPlayers, 3 * numPlayers],
[avoidClasses(clForest, 0, clPlayer, 20, clMountain, 1, clFood, 4, clRock, 6, clMetal, 6), stayClasses(clHill, 6)],
clFood);
Engine.SetProgress(75);
createFood(
[
[new SimpleObject(oFruitBush, 5, 7, 0, 4)]
],
[3 * numPlayers],
[avoidClasses(clForest, 0, clPlayer, 15, clMountain, 1, clFood, 4, clRock, 6, clMetal, 6), stayClasses(clHill, 6)],
clFood);
Engine.SetProgress(85);
var planetm = currentBiome() == "generic/tropic" ? 8 : 1;
createDecoration(
[
[new SimpleObject(aRockMedium, 1, 3, 0, 1)],
[new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)],
[new SimpleObject(aGrassShort, 2, 15, 0, 1)],
[new SimpleObject(aGrass, 2, 10, 0, 1.8), new SimpleObject(aGrassShort, 3, 10, 1.2, 2.5)],
[new SimpleObject(aBushMedium, 1, 5, 0, 2), new SimpleObject(aBushSmall, 2, 4, 0, 2)]
],
[
scaleByMapSize(16, 262),
scaleByMapSize(8, 131),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200)
],
avoidClasses(clForest, 2, clPlayer, 20, clMountain, 5, clFood, 1, clBaseResource, 2));
var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(0.1));
createForests(
[tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2],
avoidClasses(clPlayer, 30, clHill, 10, clFood, 5),
clForest,
forestTrees);
g_Map.log("Creating small grass tufts");
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(aGrassShort, 1, 2, 0, 1)]),
0,
[avoidClasses(clMountain, 2, clPlayer, 2, clDirt, 0), stayClasses(clHill, 8)],
planetm * scaleByMapSize(13, 200));
placePlayersNomad(
clPlayer,
new AndConstraint([
stayClasses(clHill, 2),
avoidClasses(clMountain, 2, clForest, 1, clMetal, 4, clRock, 4, clFood, 2)]));
setSkySet(pickRandom(["cloudless", "cumulus", "overcast"]));
setWaterMurkiness(0.4);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/hellas.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/hellas.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/hellas.js (revision 22419)
@@ -1,559 +1,559 @@
/**
* Heightmap image source:
* Imagery by Jesse Allen, NASA's Earth Observatory,
* using data from the General Bathymetric Chart of the Oceans (GEBCO)
* produced by the British Oceanographic Data Centre.
* https://visibleearth.nasa.gov/view.php?id=73934
*
* Licensing: Public Domain, https://visibleearth.nasa.gov/useterms.php
*
* The heightmap image is reproduced using:
* wget https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73934/gebco_08_rev_elev_C1_grey_geo.tif
* lat=37; lon=23; width=7; # including crete
* gdal_translate -projwin $((lon-width/2)) $((lat+width/2)) $((lon+width/2)) $((lat-width/2)) gebco_08_rev_elev_C1_grey_geo.tif hellas.tif
* convert hellas.tif -contrast-stretch 0 hellas.png
* No further changes should be applied to the image to keep it easily interchangeable.
*/
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
TILE_CENTERED_HEIGHT_MAP = true;
var mapStyles = [
// mainland
{
"minMapSize": 0,
"enabled": randBool(0.15),
"landRatio": [0.95, 1]
},
// lots of water
{
"minMapSize": 384,
"enabled": randBool(1/4),
"landRatio": [0.3, 0.5]
},
// few water
{
"minMapSize": 192,
"enabled": true,
"landRatio": [0.65, 0.9]
}
];
const heightmapHellas = convertHeightmap1Dto2D(Engine.LoadHeightmapImage("maps/random/hellas.png"));
const biomes = Engine.ReadJSONFile("maps/random/hellas_biomes.json");
const heightScale = num => num * g_MapSettings.Size / 320;
const heightSeaGround = heightScale(-6);
const heightReedsMin = heightScale(-2);
const heightReedsMax = heightScale(-0.5);
const heightShoreline = heightScale(1);
const heightLowlands = heightScale(30);
const heightHighlands = heightScale(60);
const heightmapMin = 0;
const heightmapMax = 100;
var g_Map = new RandomMap(0, biomes.lowlands.terrains.main);
var mapSize = g_Map.getSize();
var mapCenter = g_Map.getCenter();
var numPlayers = getNumPlayers();
var clWater;
var clCliffs;
var clPlayer = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clDock = g_Map.createTileClass();
var constraintLowlands = new HeightConstraint(heightShoreline, heightLowlands);
var constraintHighlands = new HeightConstraint(heightLowlands, heightHighlands);
var constraintMountains = new HeightConstraint(heightHighlands, Infinity);
var [minLandRatio, maxLandRatio] = mapStyles.filter(mapStyle => mapSize >= mapStyle.minMapSize).sort((a, b) => a.enabled - b.enabled).pop().landRatio;
var [minCliffRatio, maxCliffRatio] = [maxLandRatio < 0.75 ? 0 : 0.08, 0.18];
var playerIDs = sortAllPlayers();
var playerPosition;
// Pick a random subset of the heightmap that meets the mapStyle and has space for all players
var subAreaSize;
var subAreaTopLeft;
while (true)
{
subAreaSize = Math.floor(randFloat(0.01, 0.2) * heightmapHellas.length);
subAreaTopLeft = new Vector2D(randFloat(0, 1), randFloat(0, 1)).mult(heightmapHellas.length - subAreaSize).floor();
let heightmap = extractHeightmap(heightmapHellas, subAreaTopLeft, subAreaSize);
let heightmapPainter = new HeightmapPainter(heightmap, heightmapMin, heightmapMax);
// Quick area test
let points = new DiskPlacer(heightmap.length / 2 - MAP_BORDER_WIDTH, new Vector2D(1, 1).mult(heightmap.length / 2)).place(new NullConstraint());
let landArea = 0;
for (let point of points)
if (heightmapPainter.scaleHeight(heightmap[point.x][point.y]) > heightShoreline)
++landArea;
let landRatio = landArea / points.length;
g_Map.log("Chosen heightmap at " + uneval(subAreaTopLeft) + " of size " + subAreaSize + ", land-ratio: " + landRatio.toFixed(3));
if (landRatio < minLandRatio || landRatio > maxLandRatio)
continue;
g_Map.log("Copying heightmap");
createArea(
new MapBoundsPlacer(),
heightmapPainter);
g_Map.log("Measuring land area");
let passableLandArea = createArea(
new DiskPlacer(fractionToTiles(0.5), mapCenter),
undefined,
new HeightConstraint(heightShoreline, Infinity));
if (!passableLandArea)
continue;
landRatio = passableLandArea.getPoints().length / diskArea(fractionToTiles(0.5));
g_Map.log("Land ratio: " + landRatio.toFixed(3));
if (landRatio < minLandRatio || landRatio > maxLandRatio)
continue;
g_Map.log("Lowering sea ground");
clWater = g_Map.createTileClass();
createArea(
new MapBoundsPlacer(),
[
new SmoothElevationPainter(ELEVATION_SET, heightSeaGround, 5),
new TileClassPainter(clWater)
],
new HeightConstraint(-Infinity, heightShoreline));
let cliffsRatio;
while (true)
{
createArea(
new DiskPlacer(fractionToTiles(0.5) - MAP_BORDER_WIDTH, mapCenter),
new SmoothingPainter(1, 0.5, 1));
Engine.SetProgress(25);
clCliffs = g_Map.createTileClass();
// Marking cliffs
let cliffsArea = createArea(
new MapBoundsPlacer(),
new TileClassPainter(clCliffs),
[
avoidClasses(clWater, 2),
new SlopeConstraint(2, Infinity)
]);
cliffsRatio = cliffsArea.getPoints().length / Math.square(g_Map.getSize());
g_Map.log("Smoothing heightmap, cliff ratio: " + cliffsRatio.toFixed(3));
if (cliffsRatio < maxCliffRatio)
break;
}
if (cliffsRatio < minCliffRatio)
{
g_Map.log("Too few cliffs: " + cliffsRatio);
continue;
}
if (isNomad())
break;
g_Map.log("Finding player locations");
let players = playerPlacementRandom(
playerIDs,
avoidClasses(
clCliffs, scaleByMapSize(6, 15),
clWater, scaleByMapSize(10, 20)));
if (players)
{
[playerIDs, playerPosition] = players;
break;
}
g_Map.log("Too few player locations, starting over");
}
Engine.SetProgress(35);
if (!isNomad())
{
g_Map.log("Flattening initial CC area");
let playerRadius = defaultPlayerBaseRadius() * 0.8;
for (let position of playerPosition)
createArea(
new ClumpPlacer(diskArea(playerRadius), 0.95, 0.6, Infinity, position),
new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(position), playerRadius / 2));
Engine.SetProgress(38);
}
g_Map.log("Painting lowlands");
createArea(
new MapBoundsPlacer(),
new TerrainPainter(biomes.lowlands.terrains.main),
constraintLowlands);
Engine.SetProgress(40);
g_Map.log("Painting highlands");
createArea(
new MapBoundsPlacer(),
new TerrainPainter(biomes.highlands.terrains.main),
constraintHighlands);
Engine.SetProgress(45);
g_Map.log("Painting mountains");
createArea(
new MapBoundsPlacer(),
new TerrainPainter(biomes.common.terrains.cliffs),
[
avoidClasses(clWater, 2),
constraintMountains
]);
Engine.SetProgress(48);
g_Map.log("Painting water and shoreline");
createArea(
new MapBoundsPlacer(),
new TerrainPainter(biomes.water.terrains.main),
new HeightConstraint(-Infinity, heightShoreline));
Engine.SetProgress(50);
g_Map.log("Painting cliffs");
createArea(
new MapBoundsPlacer(),
new TerrainPainter(biomes.common.terrains.cliffs),
[
avoidClasses(clWater, 2),
new SlopeConstraint(2, Infinity)
]);
Engine.SetProgress(55);
for (let i = 0; i < numPlayers; ++i)
{
if (isNomad())
break;
let localBiome = constraintHighlands.allows(playerPosition[i]) ? biomes.highlands : biomes.lowlands;
placePlayerBase({
"playerID": playerIDs[i],
"playerPosition": playerPosition[i],
"PlayerTileClass": clPlayer,
"Walls": "towers",
"BaseResourceClass": clBaseResource,
"baseResourceConstraint": avoidClasses(clPlayer, 4, clWater, 1, clCliffs, 1),
"CityPatch": {
"outerTerrain": biomes.common.terrains.roadWild,
"innerTerrain": biomes.common.terrains.road
},
"Chicken": {
"template": localBiome.gaia.fauna.startingAnimal,
"groupCount": 1,
"minGroupCount": 4,
"maxGroupCount": 4
},
"Berries": {
"template": localBiome.gaia.flora.fruitBush,
"minCount": 3,
"maxCount": 3
},
"Mines": {
"types": [
{ "template": biomes.common.gaia.mines.metalLarge },
{ "template": biomes.common.gaia.mines.stoneLarge }
],
"minAngle": Math.PI / 2,
"maxAngle": Math.PI
},
"Trees": {
"template": pickRandom(localBiome.gaia.flora.trees),
"count": 15
}
// No decoratives
});
}
Engine.SetProgress(60);
g_Map.log("Placing docks");
placeDocks(
biomes.shoreline.gaia.dock,
0,
scaleByMapSize(1, 2) * 100,
clWater,
clDock,
heightReedsMax,
heightShoreline,
[avoidClasses(clDock, 50), new StaticConstraint(avoidClasses(clPlayer, 30, clCliffs, 8))],
0,
- 50)
+ 50);
Engine.SetProgress(65);
let [forestTrees, stragglerTrees] = getTreeCounts(600, 4000, 0.7);
let biomeTreeRatioHighlands = 0.4;
for (let biome of ["lowlands", "highlands"])
createForests(
[
biomes[biome].terrains.main,
biomes[biome].terrains.forestFloors[0],
biomes[biome].terrains.forestFloors[1],
biomes[biome].terrains.forests[0],
biomes[biome].terrains.forests[1]
],
[
biome == "highlands" ? constraintHighlands : constraintLowlands,
avoidClasses(clPlayer, 20, clForest, 18, clCliffs, 1, clWater, 2)
],
clForest,
forestTrees * (biome == "highlands" ? biomeTreeRatioHighlands : 1 - biomeTreeRatioHighlands));
Engine.SetProgress(70);
g_Map.log("Creating stone mines");
var minesStone = [
[new SimpleObject(biomes.common.gaia.mines.stoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)],
[new SimpleObject(biomes.common.gaia.mines.stoneSmall, 2, 3, 1, 3, 0, 2 * Math.PI, 1)]
];
for (let mine of minesStone)
createObjectGroups(
new SimpleGroup(mine, true, clRock),
0,
[avoidClasses(clForest, 1, clPlayer, 20, clRock, 18, clCliffs, 2, clWater, 2, clDock, 6)],
scaleByMapSize(2, 12),
50);
Engine.SetProgress(75);
g_Map.log("Creating metal mines");
var minesMetal = [
[new SimpleObject(biomes.common.gaia.mines.metalLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)],
[new SimpleObject(biomes.common.gaia.mines.metalSmall, 2, 3, 1, 3, 0, 2 * Math.PI, 1)]
];
for (let mine of minesMetal)
createObjectGroups(
new SimpleGroup(mine, true, clMetal),
0,
[avoidClasses(clForest, 1, clPlayer, 20, clRock, 8, clMetal, 18, clCliffs, 2, clWater, 2, clDock, 6)],
scaleByMapSize(2, 12),
50);
Engine.SetProgress(80);
for (let biome of ["lowlands", "highlands"])
createStragglerTrees(
biomes[biome].gaia.flora.trees,
[
biome == "highlands" ? constraintHighlands : constraintLowlands,
avoidClasses(clForest, 8, clCliffs, 1, clPlayer, 12, clMetal, 6, clRock, 6, clCliffs, 2, clWater, 2, clDock, 6)
],
clForest,
stragglerTrees * (biome == "highlands" ? biomeTreeRatioHighlands * 4 : 1 - biomeTreeRatioHighlands));
Engine.SetProgress(85);
createFood(
[
[new SimpleObject(biomes.highlands.gaia.fauna.horse, 3, 5, 0, 4)],
[new SimpleObject(biomes.highlands.gaia.fauna.pony, 2, 3, 0, 4)],
[new SimpleObject(biomes.highlands.gaia.flora.fruitBush, 5, 7, 0, 4)]
],
[
scaleByMapSize(2, 16),
scaleByMapSize(2, 12),
scaleByMapSize(2, 20)
],
[
avoidClasses(clForest, 0, clPlayer, 20, clFood, 16, clCliffs, 2, clWater, 2, clRock, 4, clMetal, 4, clDock, 6),
constraintHighlands
],
clFood);
Engine.SetProgress(90);
createFood(
[
[new SimpleObject(biomes.lowlands.gaia.fauna.sheep, 2, 3, 0, 2)],
[new SimpleObject(biomes.lowlands.gaia.fauna.rabbit, 2, 3, 0, 2)],
[new SimpleObject(biomes.lowlands.gaia.flora.fruitBush, 5, 7, 0, 4)]
],
[
scaleByMapSize(2, 16),
scaleByMapSize(2, 12),
scaleByMapSize(1, 20)
],
[
avoidClasses(clForest, 0, clPlayer, 20, clFood, 16, clCliffs, 2, clWater, 2, clRock, 4, clMetal, 4, clDock, 6),
constraintLowlands
],
clFood);
Engine.SetProgress(93);
createFood(
[
[new SimpleObject(biomes.highlands.gaia.fauna.goat, 3, 5, 0, 4)]
],
[
3 * numPlayers
],
[
avoidClasses(clForest, 1, clPlayer, 20, clFood, 20, clCliffs, 1, clRock, 4, clMetal, 4, clDock, 6),
constraintMountains
],
clFood);
g_Map.log("Creating hawk");
for (let i = 0; i < scaleByMapSize(0, 2); ++i)
g_Map.placeEntityAnywhere(biomes.highlands.gaia.fauna.hawk, 0, mapCenter, randomAngle());
g_Map.log("Creating fish");
createObjectGroups(
new SimpleGroup([new SimpleObject(biomes.water.gaia.fauna.fish, 1, 1, 0, 3)], true, clFood),
0,
[stayClasses(clWater, 8), avoidClasses(clFood, 8, clDock, 6)],
scaleByMapSize(15, 50),
100);
Engine.SetProgress(95);
g_Map.log("Creating grass patches");
for (let biome of ["lowlands", "highlands"])
for (let patch of biomes[biome].terrains.patches)
createPatches(
[scaleByMapSize(3, 7), scaleByMapSize(5, 15)],
patch,
[
biome == "highlands" ? constraintHighlands : constraintLowlands,
avoidClasses(clForest, 0, clDirt, 5, clPlayer, 12, clCliffs, 2, clWater, 2),
],
scaleByMapSize(15, 45) / biomes[biome].terrains.patches.length,
clDirt);
Engine.SetProgress(96);
for (let biome of ["lowlands", "highlands"])
{
createDecoration(
[
[new SimpleObject(actorTemplate(biomes[biome].actors.mushroom), 1, 4, 1, 2)],
[
new SimpleObject(actorTemplate(biomes.common.actors.grass), 2, 4, 0, 1.8),
new SimpleObject(actorTemplate(biomes.common.actors.grassShort), 3,6, 1.2, 2.5)
],
[
new SimpleObject(actorTemplate(biomes.common.actors.bushMedium), 1, 2, 0, 2),
new SimpleObject(actorTemplate(biomes.common.actors.bushSmall), 2, 4, 0, 2)
]
],
[
scaleByMapSize(20, 300),
scaleByMapSize(13, 200),
scaleByMapSize(13, 200),
scaleByMapSize(13, 200)
],
[
biome == "highlands" ? constraintHighlands : constraintLowlands,
avoidClasses(clCliffs, 1, clPlayer, 15, clForest, 1, clRock, 4, clMetal, 4),
]);
createDecoration(
[
biomes[biome].actors.stones.map(template => new SimpleObject(actorTemplate(template), 1, 3, 0, 1))
],
[
biomes[biome].actors.stones.map(template => scaleByMapSize(2, 40) * randIntInclusive(1, 3))
],
[
biome == "highlands" ? constraintHighlands : constraintLowlands,
avoidClasses(clWater, 4, clPlayer, 15, clForest, 1, clRock, 4, clMetal, 4),
]);
}
Engine.SetProgress(98);
g_Map.log("Creating temple");
createObjectGroups(
new SimpleGroup([new SimpleObject(biomes.highlands.gaia.athen.temple, 1, 1, 0, 0)], true),
0,
[
avoidClasses(clCliffs, 4, clWater, 4, clPlayer, 40, clForest, 4, clRock, 4, clMetal, 4),
constraintHighlands
],
1,
200);
g_Map.log("Creating statues");
createObjectGroups(
new SimpleGroup([new SimpleObject(actorTemplate(biomes.lowlands.actors.athen.statue), 1, 1, 0, 0)], true),
0,
[
avoidClasses(clCliffs, 2, clWater, 4, clPlayer, 30, clForest, 1, clRock, 8, clMetal, 8, clDock, 6),
constraintLowlands
],
scaleByMapSize(1, 2),
50);
g_Map.log("Creating campfire");
createObjectGroups(
new SimpleGroup([new SimpleObject(actorTemplate(biomes.common.actors.campfire), 1, 1, 0, 0)], true),
0,
[avoidClasses(clCliffs, 2, clWater, 4, clPlayer, 30, clForest, 1, clRock, 8, clMetal, 8, clDock, 6)],
scaleByMapSize(0, 2),
50);
g_Map.log("Creating oxybeles");
createObjectGroups(
new SimpleGroup([new SimpleObject(biomes.highlands.gaia.athen.oxybeles, 1, 1, 0, 0)], true),
0,
[
avoidClasses(clCliffs, 2, clPlayer, 30, clForest, 1, clRock, 4, clMetal, 4),
constraintHighlands
],
scaleByMapSize(0, 2),
100);
g_Map.log("Creating handcart");
createObjectGroups(
new SimpleGroup([new SimpleObject(actorTemplate(biomes.highlands.actors.handcart), 1, 1, 0, 0)], true),
0,
[
avoidClasses(clCliffs, 1, clPlayer, 15, clForest, 1, clRock, 4, clMetal, 4),
constraintHighlands
],
scaleByMapSize(1, 4),
50);
g_Map.log("Creating water log");
createObjectGroups(
new SimpleGroup([new SimpleObject(actorTemplate(biomes.water.actors.waterlog), 1, 1, 0, 0)], true),
0,
[stayClasses(clWater, 4)],
scaleByMapSize(1, 2),
10);
g_Map.log("Creating reeds");
createObjectGroups(
new SimpleGroup(
[
new SimpleObject(actorTemplate(biomes.shoreline.actors.reeds), 5, 12, 1, 4),
new SimpleObject(actorTemplate(biomes.shoreline.actors.lillies), 1, 2, 1, 5)
],
false,
clDirt),
0,
new HeightConstraint(heightReedsMin, heightReedsMax),
scaleByMapSize(10, 25),
20);
placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clMetal, 4, clRock, 4, clFood, 2, clCliffs, 2, clWater, 15));
Engine.SetProgress(99);
setWaterColor(0.024, 0.212, 0.024);
setWaterTint(0.133, 0.725, 0.855);
setWaterMurkiness(0.8);
setWaterWaviness(3);
setFogFactor(0);
setPPEffect("hdr");
setPPSaturation(0.51);
setPPContrast(0.62);
setPPBloom(0.12);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/hyrcanian_shores.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/hyrcanian_shores.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/hyrcanian_shores.js (revision 22419)
@@ -1,351 +1,351 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
const tPrimary = "temp_grass_long";
const tGrass = ["temp_grass_clovers"];
const tGrassPForest = "temp_plants_bog";
const tGrassDForest = "alpine_dirt_grass_50";
const tCliff = ["temp_cliff_a", "temp_cliff_b"];
const tGrassA = "temp_grass_d";
const tGrassB = "temp_grass_c";
const tGrassC = "temp_grass_clovers_2";
const tHill = ["temp_highlands", "temp_grass_long_b"];
const tRoad = "temp_road";
const tRoadWild = "temp_road_overgrown";
const tGrassPatch = "temp_grass_plants";
const tShore = "medit_sand_wet";
const tWater = "medit_sand_wet";
const oPoplar = "gaia/flora_tree_poplar";
const oPalm = "gaia/flora_tree_cretan_date_palm_short";
const oApple = "gaia/flora_tree_apple";
const oOak = "gaia/flora_tree_oak";
const oBerryBush = "gaia/flora_bush_berry";
const oDeer = "gaia/fauna_deer";
const oFish = "gaia/fauna_fish";
const oGoat = "gaia/fauna_goat";
const oBoar = "gaia/fauna_boar";
const oStoneLarge = "gaia/geology_stonemine_temperate_quarry";
const oStoneSmall = "gaia/geology_stone_temperate";
const oMetalLarge = "gaia/geology_metal_temperate_slabs";
const aGrass = "actor|props/flora/grass_soft_large_tall.xml";
const aGrassShort = "actor|props/flora/grass_soft_large.xml";
const aRockLarge = "actor|geology/stone_granite_large.xml";
const aRockMedium = "actor|geology/stone_granite_med.xml";
const aBushMedium = "actor|props/flora/bush_medit_me_lush.xml";
const aBushSmall = "actor|props/flora/bush_medit_sm_lush.xml";
const pForestD = [tGrassDForest + TERRAIN_SEPARATOR + oPoplar, tGrassDForest];
const pForestP = [tGrassPForest + TERRAIN_SEPARATOR + oOak, tGrassPForest];
const heightSeaGround1 = -3;
const heightShore1 = -1.5;
const heightShore2 = 0;
const heightLand = 1;
const heightOffsetBump = 4;
const heightHill = 15;
var g_Map = new RandomMap(heightLand, tPrimary);
const mapCenter = g_Map.getCenter();
const mapBounds = g_Map.getBounds();
const numPlayers = getNumPlayers();
var clPlayer = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clWater = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clHighlands = g_Map.createTileClass();
-var waterPosition = fractionToTiles(0.25)
+var waterPosition = fractionToTiles(0.25);
var highlandsPosition = fractionToTiles(0.75);
var startAngle = randomAngle();
placePlayerBases({
"PlayerPlacement": [sortAllPlayers(), playerPlacementLine(startAngle, mapCenter, fractionToTiles(0.2))],
"PlayerTileClass": clPlayer,
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad
},
"Chicken": {
},
"Berries": {
"template": oBerryBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
"Trees": {
"template": oOak,
"count": 2
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(10);
paintRiver({
"parallel": true,
"start": new Vector2D(mapBounds.left, mapBounds.top).rotateAround(startAngle, mapCenter),
"end": new Vector2D(mapBounds.right, mapBounds.top).rotateAround(startAngle, mapCenter),
"width": 2 * waterPosition,
"fadeDist": scaleByMapSize(6, 25),
"deviation": 0,
"heightRiverbed": heightSeaGround1,
"heightLand": heightLand,
"meanderShort": 20,
"meanderLong": 0,
"waterFunc": (position, height, riverFraction) => {
if (height < heightShore2)
clWater.add(position);
createTerrain(height < heightShore1 ? tWater : tShore).place(position);
}
});
Engine.SetProgress(20);
g_Map.log("Marking highlands area");
createArea(
new ConvexPolygonPlacer(
[
new Vector2D(mapBounds.left, mapBounds.top - highlandsPosition),
new Vector2D(mapBounds.right, mapBounds.top - highlandsPosition),
new Vector2D(mapBounds.left, mapBounds.bottom),
new Vector2D(mapBounds.right, mapBounds.bottom)
].map(pos => pos.rotateAround(startAngle, mapCenter)),
Infinity),
new TileClassPainter(clHighlands));
g_Map.log("Creating fish");
for (let i = 0; i < scaleByMapSize(10, 20); ++i)
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(oFish, 2, 3, 0, 2)], true, clFood),
0,
[stayClasses(clWater, 2), avoidClasses(clFood, 3)],
numPlayers,
50);
Engine.SetProgress(25);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(10, 60), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 3),
stayClasses(clHighlands, 1),
scaleByMapSize(300, 600));
Engine.SetProgress(30);
g_Map.log("Creating hills");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 150), 0.2, 0.1, Infinity),
[
new LayeredPainter([tCliff, tHill], [2]),
new SmoothElevationPainter(ELEVATION_SET, heightHill, 2),
new TileClassPainter(clHill)
],
avoidClasses(clPlayer, 20, clWater, 5, clHill, 15, clHighlands, 5),
scaleByMapSize(1, 4) * numPlayers);
Engine.SetProgress(35);
g_Map.log("Creating mainland forests");
var [forestTrees, stragglerTrees] = getTreeCounts(500, 2500, 0.7);
var types = [
[[tGrassDForest, tGrass, pForestD], [tGrassDForest, pForestD]]
];
var size = forestTrees * 1.3 / (scaleByMapSize(2,8) * numPlayers);
var num = Math.floor(0.7 * size / types.length);
for (let type of types)
createAreas(
new ClumpPlacer(forestTrees / num, 0.1, 0.1, Infinity),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
avoidClasses(clPlayer, 20, clWater, 3, clForest, 10, clHill, 0, clBaseResource, 3),
num);
Engine.SetProgress(45);
g_Map.log("Creating highland forests");
var types = [
[[tGrassDForest, tGrass, pForestP], [tGrassDForest, pForestP]]
];
var size = forestTrees / (scaleByMapSize(2,8) * numPlayers);
var num = Math.floor(size / types.length);
for (let type of types)
createAreas(
new ClumpPlacer(forestTrees / num, 0.1, 0.1, Infinity),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
avoidClasses(clPlayer, 20, clWater, 3, clForest, 2, clHill, 0),
num);
Engine.SetProgress(70);
g_Map.log("Creating dirt patches");
for (let size of [scaleByMapSize(3, 48), scaleByMapSize(5, 84), scaleByMapSize(8, 128)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
[
new LayeredPainter([[tGrass, tGrassA], [tGrassA, tGrassB], [tGrassB, tGrassC]], [1, 1]),
new TileClassPainter(clDirt)
],
avoidClasses(clWater, 1, clForest, 0, clHill, 0, clDirt, 5, clPlayer, 4),
scaleByMapSize(15, 45));
Engine.SetProgress(75);
g_Map.log("Creating grass patches");
for (let size of [scaleByMapSize(2, 32), scaleByMapSize(3, 48), scaleByMapSize(5, 80)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
new LayeredPainter([tGrassC, tGrassPatch], [2]),
avoidClasses(clWater, 1, clForest, 0, clHill, 0, clDirt, 5, clPlayer, 6, clBaseResource, 6),
scaleByMapSize(15, 45));
Engine.SetProgress(80);
g_Map.log("Creating stone mines");
var group = new SimpleGroup([new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clWater, 0, clForest, 1, clPlayer, 20, clRock, 10, clHill, 2)],
scaleByMapSize(4,16), 100
);
g_Map.log("Creating small stone quarries");
group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clWater, 0, clForest, 1, clPlayer, 20, clRock, 10, clHill, 2)],
scaleByMapSize(4,16), 100
);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clWater, 0, clForest, 1, clPlayer, 20, clMetal, 10, clRock, 5, clHill, 2)],
scaleByMapSize(4,16), 100
);
Engine.SetProgress(85);
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockMedium, 1,3, 0,1)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 0, clHill, 0),
scaleByMapSize(16, 262), 50
);
Engine.SetProgress(90);
g_Map.log("Creating large decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 0, clHill, 0),
scaleByMapSize(8, 131), 50
);
g_Map.log("Creating deer");
group = new SimpleGroup(
[new SimpleObject(oDeer, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clHill, 0, clFood, 5),
6 * numPlayers, 50
);
g_Map.log("Creating sheep");
group = new SimpleGroup(
[new SimpleObject(oGoat, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clHill, 0, clFood, 20),
3 * numPlayers, 50
);
g_Map.log("Creating berry bush");
group = new SimpleGroup(
[new SimpleObject(oBerryBush, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 6, clForest, 0, clPlayer, 20, clHill, 1, clFood, 10),
randIntInclusive(1, 4) * numPlayers + 2, 50
);
g_Map.log("Creating boar");
group = new SimpleGroup(
[new SimpleObject(oBoar, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clHill, 0, clFood, 20),
2 * numPlayers, 50
);
createStragglerTrees(
[oPoplar, oPalm, oApple],
avoidClasses(clWater, 1, clForest, 1, clHill, 1, clPlayer, 10, clMetal, 6, clRock, 6),
clForest,
stragglerTrees);
g_Map.log("Creating small grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrassShort, 1,2, 0,1, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 2, clHill, 2, clPlayer, 2, clDirt, 0),
scaleByMapSize(13, 200)
);
g_Map.log("Creating large grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrass, 2,4, 0,1.8, -Math.PI / 8, Math.PI / 8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 3, clHill, 2, clPlayer, 2, clDirt, 1, clForest, 0),
scaleByMapSize(13, 200)
);
Engine.SetProgress(95);
g_Map.log("Creating bushes");
group = new SimpleGroup(
[new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 1, clHill, 1, clPlayer, 1, clDirt, 1),
scaleByMapSize(13, 200), 50
);
placePlayersNomad(clPlayer, avoidClasses(clWater, 4, clForest, 1, clMetal, 4, clRock, 4, clHill, 4, clFood, 2));
setSkySet("cirrus");
setWaterColor(0.114, 0.192, 0.463);
setWaterTint(0.255, 0.361, 0.651);
setWaterWaviness(2.0);
setWaterType("ocean");
setWaterMurkiness(0.83);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/lake.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/lake.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/lake.js (revision 22419)
@@ -1,253 +1,253 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("rmbiome");
setSelectedBiome();
const tMainTerrain = g_Terrains.mainTerrain;
const tForestFloor1 = g_Terrains.forestFloor1;
const tForestFloor2 = g_Terrains.forestFloor2;
const tCliff = g_Terrains.cliff;
const tTier1Terrain = g_Terrains.tier1Terrain;
const tTier2Terrain = g_Terrains.tier2Terrain;
const tTier3Terrain = g_Terrains.tier3Terrain;
const tHill = g_Terrains.hill;
const tRoad = g_Terrains.road;
const tRoadWild = g_Terrains.roadWild;
const tTier4Terrain = g_Terrains.tier4Terrain;
const tShore = g_Terrains.shore;
const tWater = g_Terrains.water;
const oTree1 = g_Gaia.tree1;
const oTree2 = g_Gaia.tree2;
const oTree3 = g_Gaia.tree3;
const oTree4 = g_Gaia.tree4;
const oTree5 = g_Gaia.tree5;
const oFruitBush = g_Gaia.fruitBush;
const oMainHuntableAnimal = g_Gaia.mainHuntableAnimal;
const oFish = g_Gaia.fish;
const oSecondaryHuntableAnimal = g_Gaia.secondaryHuntableAnimal;
const oStoneLarge = g_Gaia.stoneLarge;
const oStoneSmall = g_Gaia.stoneSmall;
const oMetalLarge = g_Gaia.metalLarge;
const aGrass = g_Decoratives.grass;
const aGrassShort = g_Decoratives.grassShort;
const aRockLarge = g_Decoratives.rockLarge;
const aRockMedium = g_Decoratives.rockMedium;
const aBushMedium = g_Decoratives.bushMedium;
const aBushSmall = g_Decoratives.bushSmall;
const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2];
const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1];
const heightSeaGround = -3;
const heightLand = 3;
var g_Map = new RandomMap(heightLand, tMainTerrain);
const numPlayers = getNumPlayers();
const mapSize = g_Map.getSize();
const mapCenter = g_Map.getCenter();
var clPlayer = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clWater = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var [playerIDs, playerPosition] = playerPlacementCircle(fractionToTiles(0.35));
g_Map.log("Preventing water in player territory");
for (let i = 0; i < numPlayers; ++i)
addCivicCenterAreaToClass(playerPosition[i], clPlayer);
-g_Map.log("Creating the lake...")
+g_Map.log("Creating the lake...");
createArea(
new ChainPlacer(
2,
Math.floor(scaleByMapSize(5, 16)),
Math.floor(scaleByMapSize(35, 200)),
Infinity,
mapCenter,
0,
[Math.floor(fractionToTiles(0.2))]),
[
new SmoothElevationPainter(ELEVATION_SET, heightSeaGround, 4),
new TileClassPainter(clWater)
],
avoidClasses(clPlayer, 20));
g_Map.log("Creating more shore jaggedness");
createAreas(
new ChainPlacer(2, Math.floor(scaleByMapSize(4, 6)), 3, Infinity),
[
new LayeredPainter([tCliff, tHill], [2]),
new TileClassUnPainter(clWater)
],
borderClasses(clWater, 4, 7),
scaleByMapSize(12, 130) * 2,
150);
paintTerrainBasedOnHeight(2.4, 3.4, 3, tMainTerrain);
paintTerrainBasedOnHeight(1, 2.4, 0, tShore);
paintTerrainBasedOnHeight(-8, 1, 2, tWater);
paintTileClassBasedOnHeight(-6, 0, 1, clWater);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
// PlayerTileClass marked above
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad
},
"Chicken": {
},
"Berries": {
"template": oFruitBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
"Trees": {
"template": oTree1,
"count": 5
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(20);
createBumps(avoidClasses(clWater, 2, clPlayer, 20));
if (randBool())
createHills([tMainTerrain, tCliff, tHill], avoidClasses(clPlayer, 20, clHill, 15, clWater, 2), clHill, scaleByMapSize(1, 4) * numPlayers);
else
createMountains(tCliff, avoidClasses(clPlayer, 20, clHill, 15, clWater, 2), clHill, scaleByMapSize(1, 4) * numPlayers);
var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(1));
createForests(
[tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2],
avoidClasses(clPlayer, 20, clForest, 17, clHill, 0, clWater, 2),
clForest,
forestTrees);
Engine.SetProgress(50);
g_Map.log("Creating dirt patches");
createLayeredPatches(
[scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)],
[[tMainTerrain,tTier1Terrain],[tTier1Terrain,tTier2Terrain], [tTier2Terrain,tTier3Terrain]],
[1,1],
avoidClasses(clWater, 3, clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12),
scaleByMapSize(15, 45),
clDirt);
g_Map.log("Creating grass patches");
createPatches(
[scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)],
tTier4Terrain,
avoidClasses(clWater, 3, clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12),
scaleByMapSize(15, 45),
clDirt);
Engine.SetProgress(55);
g_Map.log("Creating stone mines");
createMines(
[
[new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)],
[new SimpleObject(oStoneSmall, 2,5, 1,3)]
],
avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clRock, 10, clHill, 1),
clRock);
g_Map.log("Creating metal mines");
createMines(
[
[new SimpleObject(oMetalLarge, 1,1, 0,4)]
],
avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clMetal, 10, clRock, 5, clHill, 1),
clMetal
);
Engine.SetProgress(65);
var planetm = 1;
if (currentBiome() == "generic/tropic")
planetm = 8;
createDecoration(
[
[new SimpleObject(aRockMedium, 1, 3, 0, 1)],
[new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)],
[new SimpleObject(aGrassShort, 1, 2, 0, 1)],
[new SimpleObject(aGrass, 2, 4, 0, 1.8), new SimpleObject(aGrassShort, 3, 6, 1.2, 2.5)],
[new SimpleObject(aBushMedium, 1, 2, 0, 2), new SimpleObject(aBushSmall, 2, 4, 0, 2)]
],
[
scaleByMapSize(16, 262),
scaleByMapSize(8, 131),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200),
planetm * scaleByMapSize(13, 200)
],
avoidClasses(clWater, 0, clForest, 0, clPlayer, 0, clHill, 0));
Engine.SetProgress(70);
createFood(
[
[new SimpleObject(oMainHuntableAnimal, 5, 7, 0, 4)],
[new SimpleObject(oSecondaryHuntableAnimal, 2, 3, 0, 2)]
],
[
3 * numPlayers,
3 * numPlayers
],
avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clHill, 1, clFood, 20),
clFood);
createFood(
[
[new SimpleObject(oFruitBush, 5, 7, 0, 4)]
],
[
3 * numPlayers
],
avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clHill, 1, clFood, 10),
clFood);
createFood(
[
[new SimpleObject(oFish, 2, 3, 0, 2)]
],
[
25 * numPlayers
],
[avoidClasses(clFood, 20), stayClasses(clWater, 6)],
clFood);
Engine.SetProgress(85);
createStragglerTrees(
[oTree1, oTree2, oTree4, oTree3],
avoidClasses(clWater, 5, clForest, 7, clHill, 1, clPlayer, 12, clMetal, 6, clRock, 6),
clForest,
stragglerTrees);
placePlayersNomad(clPlayer, avoidClasses(clWater, 4, clHill, 2, clForest, 1, clMetal, 4, clRock, 4, clFood, 2));
setWaterWaviness(4.0);
setWaterType("lake");
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/migration.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/migration.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/migration.js (revision 22419)
@@ -1,373 +1,373 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("rmbiome");
setSelectedBiome();
const tMainTerrain = g_Terrains.mainTerrain;
const tForestFloor1 = g_Terrains.forestFloor1;
const tForestFloor2 = g_Terrains.forestFloor2;
const tCliff = g_Terrains.cliff;
const tTier1Terrain = g_Terrains.tier1Terrain;
const tTier2Terrain = g_Terrains.tier2Terrain;
const tTier3Terrain = g_Terrains.tier3Terrain;
const tHill = g_Terrains.hill;
const tRoad = g_Terrains.road;
const tRoadWild = g_Terrains.roadWild;
const tTier4Terrain = g_Terrains.tier4Terrain;
const tShore = g_Terrains.shore;
const tWater = g_Terrains.water;
const oTree1 = g_Gaia.tree1;
const oTree2 = g_Gaia.tree2;
const oTree3 = g_Gaia.tree3;
const oTree4 = g_Gaia.tree4;
const oTree5 = g_Gaia.tree5;
const oFruitBush = g_Gaia.fruitBush;
const oMainHuntableAnimal = g_Gaia.mainHuntableAnimal;
const oFish = g_Gaia.fish;
const oSecondaryHuntableAnimal = g_Gaia.secondaryHuntableAnimal;
const oStoneLarge = g_Gaia.stoneLarge;
const oStoneSmall = g_Gaia.stoneSmall;
const oMetalLarge = g_Gaia.metalLarge;
const oWoodTreasure = "gaia/treasure/wood";
const oDock = "skirmish/structures/default_dock";
const aGrass = g_Decoratives.grass;
const aGrassShort = g_Decoratives.grassShort;
const aRockLarge = g_Decoratives.rockLarge;
const aRockMedium = g_Decoratives.rockMedium;
const aBushMedium = g_Decoratives.bushMedium;
const aBushSmall = g_Decoratives.bushSmall;
const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2];
const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1];
const heightSeaGround = -5;
const heightLand = 3;
const heightHill = 18;
const heightOffsetBump = 2;
var g_Map = new RandomMap(heightSeaGround, tWater);
const numPlayers = getNumPlayers();
const mapSize = g_Map.getSize();
const mapCenter = g_Map.getCenter();
var clPlayer = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clLand = g_Map.createTileClass();
var clIsland = g_Map.createTileClass();
var startAngle = randomAngle();
var playerIDs = sortAllPlayers();
var [playerPosition, playerAngle] = playerPlacementCustomAngle(
fractionToTiles(0.35),
mapCenter,
i => startAngle - Math.PI * (i + 1) / (numPlayers + 1));
g_Map.log("Creating player islands and docks");
for (let i = 0; i < numPlayers; ++i)
{
createArea(
new ClumpPlacer(diskArea(defaultPlayerBaseRadius()), 0.8, 0.1, Infinity, playerPosition[i]),
[
new LayeredPainter([tWater, tShore, tMainTerrain], [1, 4]),
new SmoothElevationPainter(ELEVATION_SET, heightLand, 4),
new TileClassPainter(clIsland),
new TileClassPainter(isNomad() ? clLand : clPlayer)
]);
if (isNomad())
continue;
let dockLocation = findLocationInDirectionBasedOnHeight(playerPosition[i], mapCenter, -3 , 2.6, 3);
g_Map.placeEntityPassable(oDock, playerIDs[i], dockLocation, playerAngle[i] + Math.PI);
}
Engine.SetProgress(10);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"PlayerTileClass": clPlayer,
"BaseResourceClass": clBaseResource,
"Walls": false,
// No city patch
"Chicken": {
},
"Berries": {
"template": oFruitBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
"Treasures": {
"types": [
{
"template": oWoodTreasure,
"count": 14
}
]
},
"Trees": {
"template": oTree1,
"count": scaleByMapSize(12, 30)
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(15);
g_Map.log("Create the continent body");
-var continentPosition = Vector2D.add(mapCenter, new Vector2D(0, fractionToTiles(0.38)).rotate(-startAngle)).round()
+var continentPosition = Vector2D.add(mapCenter, new Vector2D(0, fractionToTiles(0.38)).rotate(-startAngle)).round();
createArea(
new ClumpPlacer(diskArea(fractionToTiles(0.4)), 0.8, 0.08, Infinity, continentPosition),
[
new LayeredPainter([tWater, tShore, tMainTerrain], [4, 2]),
new SmoothElevationPainter(ELEVATION_SET, heightLand, 4),
new TileClassPainter(clLand)
],
avoidClasses(clIsland, 8));
Engine.SetProgress(20);
g_Map.log("Creating shore jaggedness");
createAreas(
new ClumpPlacer(scaleByMapSize(15, 80), 0.2, 0.1, Infinity),
[
new LayeredPainter([tMainTerrain, tMainTerrain], [2]),
new SmoothElevationPainter(ELEVATION_SET, heightLand, 4),
new TileClassPainter(clLand)
],
[
borderClasses(clLand, 6, 3),
avoidClasses(clIsland, 8)
],
scaleByMapSize(2, 15) * 20,
150);
paintTerrainBasedOnHeight(1, 3, 0, tShore);
paintTerrainBasedOnHeight(-8, 1, 2, tWater);
Engine.SetProgress(25);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 2),
[avoidClasses(clIsland, 10), stayClasses(clLand, 3)],
scaleByMapSize(100, 200)
);
Engine.SetProgress(30);
g_Map.log("Creating hills");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 150), 0.2, 0.1, Infinity),
[
new LayeredPainter([tCliff, tHill], [2]),
new SmoothElevationPainter(ELEVATION_SET, heightHill, 2),
new TileClassPainter(clHill)
],
[avoidClasses(clIsland, 10, clHill, 15), stayClasses(clLand, 7)],
scaleByMapSize(1, 4) * numPlayers
);
Engine.SetProgress(34);
g_Map.log("Creating forests");
var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(1));
var types = [
[[tForestFloor2, tMainTerrain, pForest1], [tForestFloor2, pForest1]],
[[tForestFloor1, tMainTerrain, pForest2], [tForestFloor1, pForest2]]
];
var size = forestTrees / (scaleByMapSize(2,8) * numPlayers) *
(currentBiome() == "generic/savanna" ? 2 : 1);
var num = Math.floor(size / types.length);
for (let type of types)
createAreas(
new ClumpPlacer(forestTrees / num, 0.1, 0.1, Infinity),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
[avoidClasses(clPlayer, 6, clForest, 10, clHill, 0), stayClasses(clLand, 7)],
num);
Engine.SetProgress(38);
g_Map.log("Creating dirt patches");
for (let size of [scaleByMapSize(3, 48), scaleByMapSize(5, 84), scaleByMapSize(8, 128)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
[
new LayeredPainter(
[[tMainTerrain, tTier1Terrain], [tTier1Terrain, tTier2Terrain], [tTier2Terrain, tTier3Terrain]],
[1, 1]),
new TileClassPainter(clDirt)
],
[
avoidClasses(
clForest, 0,
clHill, 0,
clDirt, 5,
clIsland, 0),
stayClasses(clLand, 7)
],
scaleByMapSize(15, 45));
Engine.SetProgress(42);
g_Map.log("Creating grass patches");
for (let size of [scaleByMapSize(2, 32), scaleByMapSize(3, 48), scaleByMapSize(5, 80)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
new TerrainPainter(tTier4Terrain),
[avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clIsland, 0), stayClasses(clLand, 7)],
scaleByMapSize(15, 45));
Engine.SetProgress(46);
g_Map.log("Creating stone mines");
var group = new SimpleGroup([new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clRock, 10, clHill, 1), stayClasses(clLand, 7)],
scaleByMapSize(4,16), 100
);
Engine.SetProgress(50);
g_Map.log("Creating small stone quarries");
group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clRock, 10, clHill, 1), stayClasses(clLand, 7)],
scaleByMapSize(4,16), 100
);
Engine.SetProgress(54);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clMetal, 10, clRock, 5, clHill, 1), stayClasses(clLand, 7)],
scaleByMapSize(4,16), 100
);
Engine.SetProgress(58);
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockMedium, 1,3, 0,1)],
true
);
createObjectGroupsDeprecated(
group, 0,
[avoidClasses(clForest, 0, clPlayer, 0, clHill, 0), stayClasses(clLand, 6)],
scaleByMapSize(16, 262), 50
);
Engine.SetProgress(62);
g_Map.log("Creating large decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)],
true
);
createObjectGroupsDeprecated(
group, 0,
[avoidClasses(clForest, 0, clPlayer, 0, clHill, 0), stayClasses(clLand, 6)],
scaleByMapSize(8, 131), 50
);
Engine.SetProgress(66);
g_Map.log("Creating deer");
group = new SimpleGroup(
[new SimpleObject(oMainHuntableAnimal, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 0, clPlayer, 10, clHill, 1, clFood, 20), stayClasses(clLand, 7)],
3 * numPlayers, 50
);
Engine.SetProgress(70);
g_Map.log("Creating sheep");
group = new SimpleGroup(
[new SimpleObject(oSecondaryHuntableAnimal, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 0, clPlayer, 10, clHill, 1, clFood, 20), stayClasses(clLand, 7)],
3 * numPlayers, 50
);
Engine.SetProgress(74);
g_Map.log("Creating fruit bush");
group = new SimpleGroup(
[new SimpleObject(oFruitBush, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 0, clPlayer, 8, clHill, 1, clFood, 20), stayClasses(clLand, 7)],
randIntInclusive(1, 4) * numPlayers + 2, 50
);
Engine.SetProgress(78);
g_Map.log("Creating fish");
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(oFish, 2,3, 0,2)], true, clFood),
0,
avoidClasses(clLand, 2, clPlayer, 2, clHill, 0, clFood, 20),
25 * numPlayers, 60
);
Engine.SetProgress(82);
createStragglerTrees(
[oTree1, oTree2, oTree4, oTree3],
[avoidClasses(clForest, 1, clHill, 1, clPlayer, 9, clMetal, 6, clRock, 6), stayClasses(clLand, 9)],
clForest,
stragglerTrees);
Engine.SetProgress(86);
var planetm = currentBiome() == "generic/tropic" ? 8 : 1;
g_Map.log("Creating small grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrassShort, 1,2, 0,1, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clHill, 2, clPlayer, 2, clDirt, 0), stayClasses(clLand, 6)],
planetm * scaleByMapSize(13, 200)
);
Engine.SetProgress(90);
g_Map.log("Creating large grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrass, 2,4, 0,1.8, -Math.PI / 8, Math.PI / 8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clHill, 2, clPlayer, 2, clDirt, 1, clForest, 0), stayClasses(clLand, 6)],
planetm * scaleByMapSize(13, 200)
);
Engine.SetProgress(94);
g_Map.log("Creating bushes");
group = new SimpleGroup(
[new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)]
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clHill, 1, clPlayer, 1, clDirt, 1), stayClasses(clLand, 6)],
planetm * scaleByMapSize(13, 200), 50
);
Engine.SetProgress(98);
setSkySet(pickRandom(["cirrus", "cumulus", "sunny"]));
setSunRotation(randomAngle());
setSunElevation(randFloat(1/5, 1/3) * Math.PI);
setWaterWaviness(2);
placePlayersNomad(clPlayer, [stayClasses(clIsland, 4), avoidClasses(clForest, 1, clMetal, 4, clRock, 4, clHill, 4, clFood, 2)]);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/oasis.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/oasis.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/oasis.js (revision 22419)
@@ -1,314 +1,314 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
const tSand = ["desert_sand_dunes_100", "desert_dirt_cracks","desert_sand_smooth", "desert_dirt_rough", "desert_dirt_rough_2", "desert_sand_smooth"];
const tDune = ["desert_sand_dunes_50"];
const tForestFloor = "desert_forestfloor_palms";
const tDirt = ["desert_dirt_rough","desert_dirt_rough","desert_dirt_rough", "desert_dirt_rough_2", "desert_dirt_rocks_2"];
-const tRoad = "desert_city_tile";;
-const tRoadWild = "desert_city_tile";;
+const tRoad = "desert_city_tile";
+const tRoadWild = "desert_city_tile";
const tShore = "dirta";
const tWater = "desert_sand_wet";
const ePalmShort = "gaia/flora_tree_cretan_date_palm_short";
const ePalmTall = "gaia/flora_tree_cretan_date_palm_tall";
const eBush = "gaia/flora_bush_grapes";
const eCamel = "gaia/fauna_camel";
const eGazelle = "gaia/fauna_gazelle";
const eLion = "gaia/fauna_lion";
const eLioness = "gaia/fauna_lioness";
const eStoneMine = "gaia/geology_stonemine_desert_quarry";
const eMetalMine = "gaia/geology_metal_desert_slabs";
const aFlower1 = "actor|props/flora/decals_flowers_daisies.xml";
const aWaterFlower = "actor|props/flora/water_lillies.xml";
const aReedsA = "actor|props/flora/reeds_pond_lush_a.xml";
const aReedsB = "actor|props/flora/reeds_pond_lush_b.xml";
const aRock = "actor|geology/stone_desert_med.xml";
const aBushA = "actor|props/flora/bush_desert_dry_a.xml";
const aBushB = "actor|props/flora/bush_desert_dry_a.xml";
const aSand = "actor|particle/blowing_sand.xml";
const pForestMain = [tForestFloor + TERRAIN_SEPARATOR + ePalmShort, tForestFloor + TERRAIN_SEPARATOR + ePalmTall, tForestFloor];
const pOasisForestLight = [tForestFloor + TERRAIN_SEPARATOR + ePalmShort, tForestFloor + TERRAIN_SEPARATOR + ePalmTall, tForestFloor,tForestFloor,tForestFloor
,tForestFloor,tForestFloor,tForestFloor,tForestFloor];
const heightSeaGround = -3;
-const heightFloraMin = -2.5
+const heightFloraMin = -2.5;
const heightFloraReedsMax = -1.9;
const heightFloraMax = -1;
const heightLand = 1;
const heightSand = 3.4;
const heightOasisPath = 4;
const heightOffsetBump = 4;
const heightOffsetDune = 18;
var g_Map = new RandomMap(heightLand, tSand);
const numPlayers = getNumPlayers();
const mapSize = g_Map.getSize();
const mapCenter = g_Map.getCenter();
var clPlayer = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clOasis = g_Map.createTileClass();
var clPassage = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
-var waterRadius = scaleByMapSize(7, 50)
+var waterRadius = scaleByMapSize(7, 50);
var shoreDistance = scaleByMapSize(4, 10);
var forestDistance = scaleByMapSize(6, 20);
var [playerIDs, playerPosition] = playerPlacementCircle(fractionToTiles(0.35));
-g_Map.log("Creating small oasis near the players...")
+g_Map.log("Creating small oasis near the players...");
var forestDist = 1.2 * defaultPlayerBaseRadius();
for (let i = 0; i < numPlayers; ++i)
{
let forestPosition;
let forestAngle;
do {
forestAngle = Math.PI / 3 * randFloat(1, 2);
forestPosition = Vector2D.add(playerPosition[i], new Vector2D(forestDist, 0).rotate(-forestAngle));
} while (
!createArea(
new ClumpPlacer(70, 1, 0.5, Infinity, forestPosition),
[
new LayeredPainter([tForestFloor, pForestMain], [0]),
new TileClassPainter(clBaseResource)
],
avoidClasses(clBaseResource, 0)));
let waterPosition;
let flowerPosition;
let reedsPosition;
do {
let waterAngle = forestAngle + randFloat(1, 5) / 3 * Math.PI;
waterPosition = Vector2D.add(forestPosition, new Vector2D(6, 0).rotate(-waterAngle)).round();
flowerPosition = Vector2D.add(forestPosition, new Vector2D(3, 0).rotate(-waterAngle)).round();
reedsPosition = Vector2D.add(forestPosition, new Vector2D(5, 0).rotate(-waterAngle)).round();
} while (
!createArea(
new ClumpPlacer(diskArea(4.5), 0.9, 0.4, Infinity, waterPosition),
[
new LayeredPainter([tShore, tWater], [1]),
new SmoothElevationPainter(ELEVATION_SET, heightSeaGround, 3)
],
avoidClasses(clBaseResource, 0)));
createObjectGroup(new SimpleGroup([new SimpleObject(aFlower1, 1, 5, 0, 3)], true, undefined, flowerPosition), 0);
createObjectGroup(new SimpleGroup([new SimpleObject(aReedsA, 1, 3, 0, 0)], true, undefined, reedsPosition), 0);
}
Engine.SetProgress(20);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
"PlayerTileClass": clPlayer,
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad,
"painters": [
new TileClassPainter(clPlayer)
]
},
"Chicken": {
},
"Berries": {
"template": eBush
},
"Mines": {
"types": [
{ "template": eMetalMine },
{ "template": eStoneMine },
],
"distance": defaultPlayerBaseRadius(),
"maxAngle": Math.PI / 2,
"groupElements": shuffleArray([aBushA, aBushB, ePalmShort, ePalmTall]).map(t => new SimpleObject(t, 1, 1, 3, 4))
}
// Starting trees were set above
// No decoratives
});
Engine.SetProgress(30);
g_Map.log("Creating central oasis");
createArea(
new ClumpPlacer(diskArea(forestDistance + shoreDistance + waterRadius), 0.8, 0.2, Infinity, mapCenter),
[
new LayeredPainter([pOasisForestLight, tWater], [forestDistance]),
new SmoothElevationPainter(ELEVATION_SET, heightSeaGround, forestDistance + shoreDistance),
new TileClassPainter(clOasis)
]);
Engine.SetProgress(40);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 3),
avoidClasses(clPlayer, 10, clBaseResource, 6, clOasis, 4),
scaleByMapSize(30, 70));
g_Map.log("Creating dirt patches");
createAreas(
new ClumpPlacer(80, 0.3, 0.06, Infinity),
new TerrainPainter(tDirt),
avoidClasses(clPlayer, 10, clBaseResource, 6, clOasis, 4, clForest, 4),
scaleByMapSize(15, 50));
g_Map.log("Creating dunes");
createAreas(
new ClumpPlacer(120, 0.3, 0.06, Infinity),
[
new TerrainPainter(tDune),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetDune, 30)
],
avoidClasses(clPlayer, 10, clBaseResource, 6, clOasis, 4, clForest, 4),
scaleByMapSize(15, 50));
Engine.SetProgress(50);
if (mapSize > 150 && randBool())
{
g_Map.log("Creating path though the oasis");
let pathWidth = scaleByMapSize(7, 18);
let points = distributePointsOnCircle(2, randomAngle(), waterRadius + shoreDistance + forestDistance + pathWidth, mapCenter)[0];
createArea(
new PathPlacer(points[0], points[1], pathWidth, 0.4, 1, 0.2, 0),
[
new TerrainPainter(tSand),
new SmoothElevationPainter(ELEVATION_SET, heightOasisPath, 5),
new TileClassPainter(clPassage)
]);
}
g_Map.log("Creating some straggler trees around the passage");
var group = new SimpleGroup([new SimpleObject(ePalmTall, 1,1, 0,0),new SimpleObject(ePalmShort, 1, 2, 1, 2), new SimpleObject(aBushA, 0,2, 1,3)], true, clForest);
createObjectGroupsDeprecated(group, 0, stayClasses(clPassage, 3), scaleByMapSize(60, 250), 100);
g_Map.log("Creating stone mines");
group = new SimpleGroup([new SimpleObject(eStoneMine, 1,1, 0,0),new SimpleObject(ePalmShort, 1,2, 3,3),new SimpleObject(ePalmTall, 0,1, 3,3)
,new SimpleObject(aBushB, 1,1, 2,2), new SimpleObject(aBushA, 0,2, 1,3)], true, clRock);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clOasis, 10, clForest, 1, clPlayer, 30, clRock, 10,clBaseResource, 2, clHill, 1),
scaleByMapSize(6,25), 100
);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(eMetalMine, 1,1, 0,0),new SimpleObject(ePalmShort, 1,2, 2,3),new SimpleObject(ePalmTall, 0,1, 2,2)
,new SimpleObject(aBushB, 1,1, 2,2), new SimpleObject(aBushA, 0,2, 1,3)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clOasis, 10, clForest, 1, clPlayer, 30, clMetal, 10,clBaseResource, 2, clRock, 10, clHill, 1),
scaleByMapSize(6,25), 100
);
Engine.SetProgress(65);
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup( [new SimpleObject(aRock, 2,4, 0,2)], true, undefined );
createObjectGroupsDeprecated(group, 0, avoidClasses(clOasis, 3, clForest, 0, clPlayer, 10, clHill, 1, clFood, 20), 30, scaleByMapSize(10, 50));
Engine.SetProgress(70);
g_Map.log("Creating camels");
group = new SimpleGroup(
[new SimpleObject(eCamel, 1,2, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clOasis, 3, clForest, 0, clPlayer, 10, clHill, 1, clFood, 20),
1 * numPlayers, 50
);
Engine.SetProgress(75);
g_Map.log("Creating gazelles");
group = new SimpleGroup(
[new SimpleObject(eGazelle, 2,4, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clOasis, 3, clForest, 0, clPlayer, 10, clHill, 1, clFood, 20),
1 * numPlayers, 50
);
Engine.SetProgress(85);
g_Map.log("Creating oasis animals");
for (let i = 0; i < scaleByMapSize(5, 30); ++i)
{
let animalPos = Vector2D.add(mapCenter, new Vector2D(forestDistance + shoreDistance + waterRadius, 0).rotate(randomAngle()));
createObjectGroup(
new RandomGroup(
[
new SimpleObject(eLion, 1, 2, 0, 4),
new SimpleObject(eLioness, 1, 2, 2, 4),
new SimpleObject(eGazelle, 4, 6, 1, 5),
new SimpleObject(eCamel, 1, 2, 1, 5)
],
true,
clFood,
animalPos),
0);
}
Engine.SetProgress(90);
g_Map.log("Creating bushes");
var group = new SimpleGroup(
[new SimpleObject(aBushB, 1,2, 0,2), new SimpleObject(aBushA, 2,4, 0,2)]
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clOasis, 2, clHill, 1, clPlayer, 1, clPassage, 1),
scaleByMapSize(10, 40), 20
);
var objectsWaterFlora = [
new SimpleObject(aReedsA, 5, 12, 0, 2),
new SimpleObject(aReedsB, 5, 12, 0, 2)
];
g_Map.log("Creating sand blows and beautifications");
for (var sandx = 0; sandx < mapSize; sandx += 4)
for (var sandz = 0; sandz < mapSize; sandz += 4)
{
let position = new Vector2D(sandx, sandz);
let height = g_Map.getHeight(position);
if (height > heightSand)
{
if (randBool((height - heightSand) / 1.4))
createObjectGroup(new SimpleGroup([new SimpleObject(aSand, 0, 1, 0, 2)], true, undefined, position), 0);
}
else if (height > heightFloraMin && height < heightFloraMax)
{
if (randBool(0.4))
createObjectGroup(new SimpleGroup([new SimpleObject(aWaterFlower, 1, 4, 1, 2)], true, undefined, position), 0);
else if (randBool(0.7) && height < heightFloraReedsMax)
createObjectGroup(new SimpleGroup(objectsWaterFlora, true, undefined, position), 0);
if (clPassage.countMembersInRadius(position, 2))
{
if (randBool(0.4))
createObjectGroup(new SimpleGroup([new SimpleObject(aWaterFlower, 1, 4, 1, 2)], true, undefined, position), 0);
else if (randBool(0.7) && height < heightFloraReedsMax)
createObjectGroup(new SimpleGroup(objectsWaterFlora, true, undefined, position), 0);
}
}
}
placePlayersNomad(clPlayer, avoidClasses(clOasis, 4, clForest, 1, clMetal, 4, clRock, 4, clHill, 4, clFood, 2));
setSkySet("sunny");
setSunColor(0.914,0.827,0.639);
setSunRotation(Math.PI/3);
setSunElevation(0.5);
setWaterColor(0, 0.227, 0.843);
setWaterTint(0, 0.545, 0.859);
setWaterWaviness(1.0);
setWaterType("clap");
setWaterMurkiness(0.5);
setTerrainAmbientColor(0.45, 0.5, 0.6);
setUnitsAmbientColor(0.501961, 0.501961, 0.501961);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/pompeii.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/pompeii.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/pompeii.js (revision 22419)
@@ -1,463 +1,463 @@
/**
* Heightmap image source:
* Imagery by Jesse Allen, NASA's Earth Observatory,
* using data from the General Bathymetric Chart of the Oceans (GEBCO)
* produced by the British Oceanographic Data Centre.
* https://visibleearth.nasa.gov/view.php?id=73934
*
* Licensing: Public Domain, https://visibleearth.nasa.gov/useteEngine.php
*
* The heightmap image is reproduced using:
* wget https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73934/gebco_08_rev_elev_C1_grey_geo.tif
* lat=41.1; lon=14.25; width=1.4;
* lat1=$(bc <<< ";scale=5;$lat-$width/2"); lon1=$(bc <<< ";scale=5;$lon+$width/2"); lat2=$(bc <<< ";scale=5;$lat+$width/2"); lon2=$(bc <<< ";scale=5;$lon-$width/2")
* gdal_translate -projwin $lon2 $lat2 $lon1 $lat1 gebco_08_rev_elev_C1_grey_geo.tif pompeii.tif
* convert pompeii.tif -resize 512 -contrast-stretch 0 pompeii.png
* No further changes should be applied to the image to keep it easily interchangeable.
*/
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("rmgen2");
Engine.LoadLibrary("rmbiome");
setBiome("generic/mediterranean");
g_Terrains.lavaOuter = "LavaTest06";
g_Terrains.lavaInner = "LavaTest05";
g_Terrains.lavaCenter = "LavaTest04";
g_Terrains.mainTerrain = "ocean_rock_a";
g_Terrains.forestFloor1 = "dirt_burned";
g_Terrains.forestFloor2 = "shoreline_stoney_a";
g_Terrains.tier1Terrain = "rock_metamorphic";
g_Terrains.tier2Terrain = "fissures";
g_Terrains.tier3Terrain = "LavaTest06";
g_Terrains.tier4Terrain = "ocean_rock_b";
g_Terrains.roadWild = "road1";
g_Terrains.road = "road1";
g_Terrains.water = "ocean_rock_a";
g_Terrains.cliff = "ocean_rock_b";
g_Gaia.mainHuntableAnimal = "gaia/fauna_goat";
g_Gaia.secondaryHuntableAnimal = "gaia/fauna_hawk";
g_Gaia.fruitBush = "gaia/fauna_chicken";
g_Gaia.fish = "gaia/fauna_fish";
g_Gaia.tree1 = "gaia/flora_tree_dead";
g_Gaia.tree2 = "gaia/flora_tree_oak_dead";
g_Gaia.tree3 = "gaia/flora_tree_dead";
g_Gaia.tree4 = "gaia/flora_tree_oak_dead";
g_Gaia.tree5 = "gaia/flora_tree_dead";
g_Gaia.stoneSmall = "gaia/geology_stone_alpine_a";
g_Gaia.columnsDoric = "gaia/ruins/column_doric";
g_Gaia.romanStatue = "gaia/ruins/stone_statues_roman";
g_Gaia.unfinishedTemple = "gaia/ruins/unfinished_greek_temple";
g_Gaia.dock = "structures/rome_dock";
g_Gaia.dockRubble = "rubble/rubble_rome_dock";
g_Decoratives.smoke1 = "actor|particle/smoke_volcano.xml";
g_Decoratives.smoke2 = "actor|particle/smoke_curved.xml";
g_Decoratives.grass = "actor|props/flora/grass_field_parched_short.xml";
g_Decoratives.grassShort = "actor|props/flora/grass_soft_dry_tuft_a.xml";
g_Decoratives.bushMedium = "actor|props/special/eyecandy/barrels_buried.xml";
g_Decoratives.bushSmall = "actor|props/special/eyecandy/handcart_1_broken.xml";
g_Decoratives.skeleton = "actor|props/special/eyecandy/skeleton.xml";
g_Decoratives.shipwrecks = [
"actor|props/special/eyecandy/shipwreck_hull.xml",
"actor|props/special/eyecandy/shipwreck_ram_side.xml",
"actor|props/special/eyecandy/shipwreck_sail_boat.xml",
"actor|props/special/eyecandy/shipwreck_sail_boat_cut.xml",
"actor|props/special/eyecandy/barrels_floating.xml"
];
g_Decoratives.statues = [
"actor|props/special/eyecandy/statue_aphrodite_huge.xml",
"actor|props/special/eyecandy/sele_colonnade.xml",
"actor|props/special/eyecandy/well_1_b.xml",
"actor|props/special/eyecandy/anvil.xml",
"actor|props/special/eyecandy/wheel_laying.xml",
"actor|props/special/eyecandy/vase_rome_a.xml"
];
const heightScale = num => num * g_MapSettings.Size / 320;
const heightSeaGround = heightScale(-30);
const heightDockMin = heightScale(-6);
const heightShorelineMin = heightScale(-1);
const heightShorelineMax = heightScale(0);
const heightWaterLevel = heightScale(0);
const heightDockMax = heightScale(1);
const heightLavaVesuv = heightScale(38);
const heightMountains = 140;
var g_Map = new RandomMap(0, g_Terrains.mainTerrain);
var mapCenter = g_Map.getCenter();
initTileClasses(["decorative", "lava", "dock"]);
g_Map.LoadHeightmapImage("pompeii.png", 0, heightMountains);
Engine.SetProgress(15);
g_Map.log("Lowering sea ground");
createArea(
new MapBoundsPlacer(),
new SmoothElevationPainter(ELEVATION_SET, heightSeaGround, 2),
new HeightConstraint(-Infinity, heightWaterLevel));
Engine.SetProgress(20);
g_Map.log("Smoothing heightmap");
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.8, 1));
Engine.SetProgress(25);
g_Map.log("Marking water");
createArea(
new MapBoundsPlacer(),
new TileClassPainter(g_TileClasses.water),
new HeightConstraint(-Infinity, heightWaterLevel));
Engine.SetProgress(30);
g_Map.log("Marking land");
createArea(
new MapBoundsPlacer(),
new TileClassPainter(g_TileClasses.land),
avoidClasses(g_TileClasses.water, 0));
Engine.SetProgress(35);
g_Map.log("Painting cliffs");
createArea(
new MapBoundsPlacer(),
[
new TerrainPainter(g_Terrains.cliff),
new TileClassPainter(g_TileClasses.mountain),
],
[
avoidClasses(g_TileClasses.water, 2),
new SlopeConstraint(2, Infinity)
]);
Engine.SetProgress(45);
g_Map.log("Painting lava");
var areaVesuv = createArea(
new RectPlacer(new Vector2D(mapCenter.x, fractionToTiles(0.3)), new Vector2D(fractionToTiles(0.7), fractionToTiles(0.15))),
[
new LayeredPainter([g_Terrains.lavaOuter,g_Terrains.lavaInner, g_Terrains.lavaCenter], [scaleByMapSize(1, 3), 2]),
new TileClassPainter(g_TileClasses.lava)
],
new HeightConstraint(heightLavaVesuv, Infinity));
Engine.SetProgress(46);
g_Map.log("Adding smoke");
createObjectGroupsByAreas(
new SimpleGroup(
[
new SimpleObject(g_Decoratives.smoke1, 1, 1, 0, 4),
new SimpleObject(g_Decoratives.smoke2, 2, 2, 0, 4)
],
false),
0,
stayClasses(g_TileClasses.lava, 0),
scaleByMapSize(4, 12),
20,
[areaVesuv]);
Engine.SetProgress(48);
if (!isNomad())
{
g_Map.log("Placing players");
let [playerIDs, playerPosition] = createBases(
...playerPlacementRandom(
sortAllPlayers(),
[
avoidClasses(g_TileClasses.mountain, 5),
stayClasses(g_TileClasses.land, scaleByMapSize(5, 15))
]),
false);
g_Map.log("Flatten the initial CC area");
for (let position of playerPosition)
createArea(
new ClumpPlacer(diskArea(defaultPlayerBaseRadius() * 0.8), 0.95, 0.6, Infinity, position),
new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(position), 6));
}
Engine.SetProgress(50);
g_Map.log("Placing docks");
var dockTypes = [
{ "template": g_Gaia.dock, "count": scaleByMapSize(1, 2) },
{ "template": g_Gaia.dockRubble, "count": scaleByMapSize(2, 3) }
];
for (let dockType of dockTypes)
placeDocks(
dockType.template,
0,
dockType.count,
g_TileClasses.water,
g_TileClasses.dock,
heightDockMin,
heightDockMax,
[
avoidClasses(g_TileClasses.dock, scaleByMapSize(10, 25)),
new StaticConstraint(avoidClasses(
g_TileClasses.mountain, scaleByMapSize(6, 8),
g_TileClasses.baseResource, 10))
],
0,
- 50)
+ 50);
Engine.SetProgress(55);
addElements([
{
"func": addLayeredPatches,
"avoid": [
g_TileClasses.dirt, 5,
g_TileClasses.forest, 2,
g_TileClasses.mountain, 2,
g_TileClasses.player, 12,
g_TileClasses.lava, 2,
g_TileClasses.water, 3
],
"sizes": ["normal"],
"mixes": ["normal"],
"amounts": ["normal"]
},
{
"func": addDecoration,
"avoid": [
g_TileClasses.forest, 2,
g_TileClasses.mountain, 2,
g_TileClasses.player, 12,
g_TileClasses.lava, 2,
g_TileClasses.water, 3
],
"sizes": ["normal"],
"mixes": ["normal"],
"amounts": ["normal"]
}
]);
Engine.SetProgress(60);
addElements(shuffleArray([
{
"func": addMetal,
"avoid": [
g_TileClasses.berries, 5,
g_TileClasses.forest, 3,
g_TileClasses.mountain, 2,
g_TileClasses.player, 30,
g_TileClasses.rock, 10,
g_TileClasses.metal, 20,
g_TileClasses.lava, 5,
g_TileClasses.water, 3
],
"sizes": ["normal"],
"mixes": ["same"],
"amounts": ["few"]
},
{
"func": addStone,
"avoid": [
g_TileClasses.berries, 5,
g_TileClasses.forest, 3,
g_TileClasses.mountain, 2,
g_TileClasses.player, 30,
g_TileClasses.rock, 20,
g_TileClasses.metal, 10,
g_TileClasses.lava, 5,
g_TileClasses.water, 5
],
"sizes": ["normal"],
"mixes": ["same"],
"amounts": ["few"]
},
{
"func": addForests,
"avoid": [
g_TileClasses.berries, 5,
g_TileClasses.forest, 18,
g_TileClasses.metal, 3,
g_TileClasses.mountain, 5,
g_TileClasses.player, 20,
g_TileClasses.rock, 3,
g_TileClasses.water, 2
],
"sizes": ["normal"],
"mixes": ["same"],
"amounts": ["many"]
}
]));
Engine.SetProgress(65);
addElements(shuffleArray([
{
"func": addAnimals,
"avoid": [
g_TileClasses.animals, 20,
g_TileClasses.forest, 2,
g_TileClasses.metal, 2,
g_TileClasses.mountain, 1,
g_TileClasses.player, 20,
g_TileClasses.rock, 2,
g_TileClasses.lava, 10,
g_TileClasses.water, 3
],
"sizes": ["normal"],
"mixes": ["same"],
"amounts": ["few"]
},
{
"func": addFish,
"avoid": [
g_TileClasses.fish, 12,
g_TileClasses.player, 8
],
"stay": [g_TileClasses.water, 4],
"sizes": ["normal"],
"mixes": ["same"],
"amounts": ["few"]
},
{
"func": addStragglerTrees,
"avoid": [
g_TileClasses.berries, 5,
g_TileClasses.forest, 7,
g_TileClasses.metal, 2,
g_TileClasses.mountain, 1,
g_TileClasses.player, 12,
g_TileClasses.rock, 2,
g_TileClasses.lava, 5,
g_TileClasses.water, 5
],
"sizes": ["normal"],
"mixes": ["same"],
"amounts": ["tons"]
}
]));
Engine.SetProgress(70);
g_Map.log("Adding gatherable stone statues");
createObjectGroups(
new SimpleGroup(
[new SimpleObject(g_Gaia.romanStatue, 1, 1, 1, 4)],
true,
g_TileClasses.metal
),
0,
avoidClasses(
g_TileClasses.water, 2,
g_TileClasses.player, 20,
g_TileClasses.mountain, 3,
g_TileClasses.forest, 2,
g_TileClasses.lava, 5,
g_TileClasses.metal, 20
),
5 * scaleByMapSize(1, 4),
50);
Engine.SetProgress(75);
g_Map.log("Adding stone ruins");
createObjectGroups(
new SimpleGroup(
[
new SimpleObject(g_Gaia.unfinishedTemple, 0, 1, 1, 4),
new SimpleObject(g_Gaia.columnsDoric, 1, 1, 1, 4)
],
true,
g_TileClasses.decorative
),
0,
avoidClasses(
g_TileClasses.water, 2,
g_TileClasses.player, 20,
g_TileClasses.mountain, 5,
g_TileClasses.forest, 2,
g_TileClasses.lava, 5,
g_TileClasses.decorative, 20
),
scaleByMapSize(1, 4),
20);
Engine.SetProgress(80);
g_Map.log("Adding shipwrecks");
createObjectGroups(
new SimpleGroup(g_Decoratives.shipwrecks.map(shipwreck => new SimpleObject(shipwreck, 0, 1, 1, 20)), true, g_TileClasses.decorative),
0,
[
avoidClasses(g_TileClasses.decorative, 20),
stayClasses(g_TileClasses.water, 0)
],
scaleByMapSize(1, 5),
20);
Engine.SetProgress(85);
g_Map.log("Adding more statues");
createObjectGroups(
new SimpleGroup(g_Decoratives.statues.map(ruin => new SimpleObject(ruin, 0, 1, 1, 20)), true, g_TileClasses.decorative),
0,
avoidClasses(
g_TileClasses.water, 2,
g_TileClasses.player, 20,
g_TileClasses.mountain, 2,
g_TileClasses.forest, 2,
g_TileClasses.lava, 5,
g_TileClasses.decorative, 20
),
scaleByMapSize(3, 15),
30);
Engine.SetProgress(90);
g_Map.log("Adding skeletons");
createObjectGroups(
new SimpleGroup(
[new SimpleObject(g_Decoratives.skeleton, 3, 10, 1, 7)],
true,
g_TileClasses.dirt
),
0,
avoidClasses(
g_TileClasses.water, 2,
g_TileClasses.player, 10,
g_TileClasses.mountain, 2,
g_TileClasses.forest, 2,
g_TileClasses.decorative, 2
),
scaleByMapSize(1, 5),
50);
Engine.SetProgress(95);
placePlayersNomad(
g_Map.createTileClass(),
[
stayClasses(g_TileClasses.land, 5),
avoidClasses(
g_TileClasses.forest, 1,
g_TileClasses.rock, 4,
g_TileClasses.metal, 4,
g_TileClasses.animals, 2,
g_TileClasses.mountain, 2)
]);
setWaterTint(0.5, 0.5, 0.5);
setWaterColor(0.3, 0.3, 0.3);
setWaterWaviness(8);
setWaterMurkiness(0.87);
setWaterType("lake");
setTerrainAmbientColor(0.3, 0.3, 0.3);
setUnitsAmbientColor(0.3, 0.3, 0.3);
setSunColor(0.8, 0.8, 0.8);
setSunRotation(Math.PI);
setSunElevation(1/2);
setFogFactor(0);
setFogThickness(0);
setFogColor(0.69, 0.616, 0.541);
setSkySet("stormy");
setPPEffect("hdr");
setPPContrast(0.67);
setPPSaturation(0.42);
setPPBloom(0.23);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/rhine_marshlands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rhine_marshlands.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rhine_marshlands.js (revision 22419)
@@ -1,309 +1,309 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
const tGrass = ["temp_grass", "temp_grass", "temp_grass_d"];
const tForestFloor = "temp_plants_bog";
const tGrassA = "temp_grass_plants";
const tGrassB = "temp_plants_bog";
const tMud = "temp_mud_a";
const tRoad = "temp_road";
const tRoadWild = "temp_road_overgrown";
const tShoreBlend = "temp_grass_plants";
const tShore = "temp_plants_bog";
const tWater = "temp_mud_a";
const oBeech = "gaia/flora_tree_euro_beech";
const oOak = "gaia/flora_tree_oak";
const oBerryBush = "gaia/flora_bush_berry";
const oDeer = "gaia/fauna_deer";
const oHorse = "gaia/fauna_horse";
const oWolf = "gaia/fauna_wolf";
const oRabbit = "gaia/fauna_rabbit";
const oStoneLarge = "gaia/geology_stonemine_temperate_quarry";
const oStoneSmall = "gaia/geology_stone_temperate";
const oMetalLarge = "gaia/geology_metal_temperate_slabs";
const aGrass = "actor|props/flora/grass_soft_small_tall.xml";
const aGrassShort = "actor|props/flora/grass_soft_large.xml";
const aRockLarge = "actor|geology/stone_granite_med.xml";
const aRockMedium = "actor|geology/stone_granite_med.xml";
const aReeds = "actor|props/flora/reeds_pond_lush_a.xml";
const aLillies = "actor|props/flora/water_lillies.xml";
const aBushMedium = "actor|props/flora/bush_medit_me.xml";
const aBushSmall = "actor|props/flora/bush_medit_sm.xml";
const pForestD = [tForestFloor + TERRAIN_SEPARATOR + oBeech, tForestFloor];
const pForestP = [tForestFloor + TERRAIN_SEPARATOR + oOak, tForestFloor];
-const heightMarsh = -2
+const heightMarsh = -2;
const heightLand = 1;
const heightOffsetBumpWater = 1;
const heightOffsetBumpLand = 2;
var g_Map = new RandomMap(heightLand, tGrass);
const numPlayers = getNumPlayers();
var clPlayer = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clWater = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
placePlayerBases({
"PlayerPlacement": playerPlacementCircle(fractionToTiles(0.35)),
"PlayerTileClass": clPlayer,
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad
},
"Chicken": {
},
"Berries": {
"template": oBerryBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
]
},
"Trees": {
"template": oBeech
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(15);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBumpLand, 2),
avoidClasses(clPlayer, 13),
scaleByMapSize(300, 800));
g_Map.log("Creating marshes");
for (let i = 0; i < 7; ++i)
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(6, 12)), Math.floor(scaleByMapSize(15, 60)), 0.8),
[
new LayeredPainter([tShoreBlend, tShore, tWater], [1, 1]),
new SmoothElevationPainter(ELEVATION_SET, heightMarsh, 3),
new TileClassPainter(clWater)
],
avoidClasses(clPlayer, 20, clWater, Math.round(scaleByMapSize(7,16)*randFloat(0.8,1.35))),
scaleByMapSize(4,20));
g_Map.log("Creating reeds");
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(aReeds, 5, 10, 0, 4), new SimpleObject(aLillies, 5, 10, 0, 4)], true),
0,
stayClasses(clWater, 1),
scaleByMapSize(400,2000), 100);
Engine.SetProgress(40);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBumpWater, 2),
stayClasses(clWater, 2),
scaleByMapSize(50, 100));
g_Map.log("Creating forests");
var [forestTrees, stragglerTrees] = getTreeCounts(500, 2500, 0.7);
var types = [
[[tForestFloor, tGrass, pForestD], [tForestFloor, pForestD]],
[[tForestFloor, tGrass, pForestP], [tForestFloor, pForestP]]
];
var size = forestTrees / (scaleByMapSize(3,6) * numPlayers);
var num = Math.floor(size / types.length);
for (let type of types)
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), forestTrees / (num * Math.floor(scaleByMapSize(2, 4))), Infinity),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
avoidClasses(clPlayer, 20, clWater, 0, clForest, 10),
num);
Engine.SetProgress(50);
g_Map.log("Creating mud patches");
for (let size of [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)])
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, Infinity),
[
new LayeredPainter([tGrassA, tGrassB, tMud], [1, 1]),
new TileClassPainter(clDirt)
],
avoidClasses(clWater, 1, clForest, 0, clDirt, 5, clPlayer, 8),
scaleByMapSize(15, 45));
g_Map.log("Creating stone mines");
var group = new SimpleGroup([new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clWater, 0, clForest, 1, clPlayer, 20, clRock, 10)],
scaleByMapSize(4,16), 100
);
g_Map.log("Creating small stone quarries");
group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clWater, 0, clForest, 1, clPlayer, 20, clRock, 10)],
scaleByMapSize(4,16), 100
);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clWater, 0, clForest, 1, clPlayer, 20, clMetal, 10, clRock, 5)],
scaleByMapSize(4,16), 100
);
Engine.SetProgress(60);
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockMedium, 1,3, 0,1)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 0),
scaleByMapSize(16, 262), 50
);
Engine.SetProgress(65);
g_Map.log("Creating large decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 0),
scaleByMapSize(8, 131), 50
);
Engine.SetProgress(70);
g_Map.log("Creating deer");
group = new SimpleGroup(
[new SimpleObject(oDeer, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clFood, 13),
6 * numPlayers, 50
);
g_Map.log("Creating horse");
group = new SimpleGroup(
[new SimpleObject(oHorse, 1,3, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clFood, 13),
3 * numPlayers, 50
);
Engine.SetProgress(75);
g_Map.log("Creating rabbit");
group = new SimpleGroup(
[new SimpleObject(oRabbit, 5,7, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clFood, 13),
6 * numPlayers, 50
);
g_Map.log("Creating wolf");
group = new SimpleGroup(
[new SimpleObject(oWolf, 1,3, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 0, clForest, 0, clPlayer, 20, clFood, 13),
3 * numPlayers, 50
);
g_Map.log("Creating berry bush");
group = new SimpleGroup(
[new SimpleObject(oBerryBush, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clFood, 10),
randIntInclusive(1, 4) * numPlayers + 2, 50
);
Engine.SetProgress(80);
createStragglerTrees(
[oOak, oBeech],
avoidClasses(clForest, 1, clPlayer, 13, clMetal, 6, clRock, 6, clWater, 0),
clForest,
stragglerTrees);
Engine.SetProgress(85);
g_Map.log("Creating small grass tufts");
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(aGrassShort, 1, 2, 0, 1)]),
0,
avoidClasses(clWater, 2, clPlayer, 13, clDirt, 0),
scaleByMapSize(13, 200));
Engine.SetProgress(90);
g_Map.log("Creating large grass tufts");
createObjectGroupsDeprecated(
new SimpleGroup(
[
new SimpleObject(aGrass, 2, 4, 0, 1.8),
new SimpleObject(aGrassShort, 3, 6, 1.2, 2.5)
]),
0,
avoidClasses(clWater, 3, clPlayer, 13, clDirt, 1, clForest, 0),
scaleByMapSize(13, 200));
Engine.SetProgress(95);
g_Map.log("Creating bushes");
createObjectGroupsDeprecated(
new SimpleGroup(
[
new SimpleObject(aBushMedium, 1, 2, 0, 2),
new SimpleObject(aBushSmall, 2, 4, 0, 2)
]),
0,
avoidClasses(clWater, 1, clPlayer, 13, clDirt, 1),
scaleByMapSize(13, 200),
50);
placePlayersNomad(clPlayer, avoidClasses(clWater, 4, clForest, 1, clMetal, 4, clRock, 4, clFood, 2));
setSkySet("cirrus");
setWaterColor(0.753,0.635,0.345); // muddy brown
setWaterTint(0.161,0.514,0.635); // clear blue for blueness
setWaterMurkiness(0.8);
setWaterWaviness(1.0);
setWaterType("clap");
setFogThickness(0.25);
setFogFactor(0.6);
setPPEffect("hdr");
setPPSaturation(0.44);
setPPBloom(0.3);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/rmbiome/randombiome.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmbiome/randombiome.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmbiome/randombiome.js (revision 22419)
@@ -1,89 +1,89 @@
var g_BiomeID;
var g_Terrains = {};
var g_Gaia = {};
var g_Decoratives = {};
var g_ResourceCounts = {};
var g_Heights = {};
function currentBiome()
{
return g_BiomeID;
}
function setSelectedBiome()
{
// TODO: Replace ugly default for atlas by a dropdown
setBiome(g_MapSettings.Biome || "generic/alpine");
}
function setBiome(biomeID)
{
RandomMapLogger.prototype.printDirectly("Setting biome " + biomeID + ".\n");
loadBiomeFile("defaultbiome");
setSkySet(pickRandom(["cirrus", "cumulus", "sunny"]));
setSunRotation(randomAngle());
setSunElevation(Math.PI * randFloat(1/6, 1/3));
g_BiomeID = biomeID;
loadBiomeFile(biomeID);
Engine.LoadLibrary("rmbiome/" + dirname(biomeID));
let setupBiomeFunc = global["setupBiome_" + basename(biomeID)];
if (setupBiomeFunc)
setupBiomeFunc();
}
/**
* Copies JSON contents to defined global variables.
*/
function loadBiomeFile(file)
{
let path = "maps/random/rmbiome/" + file + ".json";
if (!Engine.FileExists(path))
{
error("Could not load biome file '" + file + "'");
return;
}
- let biome = Engine.ReadJSONFile(path)
+ let biome = Engine.ReadJSONFile(path);
let copyProperties = (from, to) => {
for (let prop in from)
{
if (from[prop] !== null && typeof from[prop] == "object" && !Array.isArray(from[prop]))
{
if (!to[prop])
to[prop] = {};
copyProperties(from[prop], to[prop]);
}
else
to[prop] = from[prop];
}
};
for (let rmsGlobal in biome)
{
if (rmsGlobal == "Description")
continue;
if (!global["g_" + rmsGlobal])
throw new Error(rmsGlobal + " not defined!");
copyProperties(biome[rmsGlobal], global["g_" + rmsGlobal]);
}
}
function rBiomeTreeCount(multiplier = 1)
{
return [
g_ResourceCounts.trees.min * multiplier,
g_ResourceCounts.trees.max * multiplier,
g_ResourceCounts.trees.forestProbability
];
}
Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/Constraint.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmgen/Constraint.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/Constraint.js (revision 22419)
@@ -1,222 +1,222 @@
/**
* @file A Constraint decides if a tile satisfies a condition defined by the class.
*/
/**
* The NullConstraint is always satisfied.
*/
function NullConstraint() {}
NullConstraint.prototype.allows = function(position)
{
return true;
};
/**
* The AndConstraint is met if every given Constraint is satisfied by the tile.
*/
function AndConstraint(constraints)
{
if (constraints instanceof Array)
- this.constraints = constraints
+ this.constraints = constraints;
else if (!constraints)
this.constraints = [];
else
this.constraints = [constraints];
}
AndConstraint.prototype.allows = function(position)
{
return this.constraints.every(constraint => constraint.allows(position));
};
/**
* The StayAreasConstraint is met if some of the given Areas contains the point.
*/
function StayAreasConstraint(areas)
{
this.areas = areas;
}
StayAreasConstraint.prototype.allows = function(position)
{
return this.areas.some(area => area.contains(position));
};
/**
* The StayAreasConstraint is met if the point is adjacent to one of the given Areas and not contained by that Area.
*/
function AdjacentToAreaConstraint(areas)
{
this.areas = areas;
}
AdjacentToAreaConstraint.prototype.allows = function(position)
{
return this.areas.some(area =>
!area.contains(position) &&
g_Map.getAdjacentPoints(position).some(adjacentPosition => area.contains(adjacentPosition)));
};
/**
* The AvoidAreasConstraint is met if none of the given Areas contain the point.
*/
function AvoidAreasConstraint(areas)
{
this.areas = areas;
}
AvoidAreasConstraint.prototype.allows = function(position)
{
- return this.areas.every(area => !area.contains(position))
+ return this.areas.every(area => !area.contains(position));
};
/**
* The StayTextureConstraint is met if the tile has the given texture.
*/
function StayTextureConstraint(texture)
{
this.texture = texture;
}
StayTextureConstraint.prototype.allows = function(position)
{
return g_Map.getTexture(position) == this.texture;
};
/**
* The AvoidTextureConstraint is met if the terrain texture of the tile is different from the given texture.
*/
function AvoidTextureConstraint(texture)
{
this.texture = texture;
}
AvoidTextureConstraint.prototype.allows = function(position)
{
return g_Map.getTexture(position) != this.texture;
};
/**
* The AvoidTileClassConstraint is met if there are no tiles marked with the given TileClass within the given radius of the tile.
*/
function AvoidTileClassConstraint(tileClass, distance)
{
this.tileClass = tileClass;
this.distance = distance;
}
AvoidTileClassConstraint.prototype.allows = function(position)
{
return this.tileClass.countMembersInRadius(position, this.distance) == 0;
};
/**
* The StayInTileClassConstraint is met if every tile within the given radius of the tile is marked with the given TileClass.
*/
function StayInTileClassConstraint(tileClass, distance)
{
this.tileClass = tileClass;
this.distance = distance;
}
StayInTileClassConstraint.prototype.allows = function(position)
{
return this.tileClass.countNonMembersInRadius(position, this.distance) == 0;
};
/**
* The NearTileClassConstraint is met if at least one tile within the given radius of the tile is marked with the given TileClass.
*/
function NearTileClassConstraint(tileClass, distance)
{
this.tileClass = tileClass;
this.distance = distance;
}
NearTileClassConstraint.prototype.allows = function(position)
{
return this.tileClass.countMembersInRadius(position, this.distance) > 0;
};
/**
* The BorderTileClassConstraint is met if there are
* tiles not marked with the given TileClass within distanceInside of the tile and
* tiles marked with the given TileClass within distanceOutside of the tile.
*/
function BorderTileClassConstraint(tileClass, distanceInside, distanceOutside)
{
this.tileClass = tileClass;
this.distanceInside = distanceInside;
this.distanceOutside = distanceOutside;
}
BorderTileClassConstraint.prototype.allows = function(position)
{
return this.tileClass.countMembersInRadius(position, this.distanceOutside) > 0 &&
this.tileClass.countNonMembersInRadius(position, this.distanceInside) > 0;
};
/**
* The HeightConstraint is met if the elevation of the tile is within the given range.
* One can pass Infinity to only test for one side.
*/
function HeightConstraint(minHeight, maxHeight)
{
this.minHeight = minHeight;
this.maxHeight = maxHeight;
}
HeightConstraint.prototype.allows = function(position)
{
return this.minHeight <= g_Map.getHeight(position) && g_Map.getHeight(position) <= this.maxHeight;
};
/**
* The SlopeConstraint is met if the steepness of the terrain is within the given range.
*/
function SlopeConstraint(minSlope, maxSlope)
{
this.minSlope = minSlope;
this.maxSlope = maxSlope;
}
SlopeConstraint.prototype.allows = function(position)
{
return this.minSlope <= g_Map.getSlope(position) && g_Map.getSlope(position) <= this.maxSlope;
};
/**
* The StaticConstraint is used for performance improvements of existing Constraints.
* It is evaluated for the entire map when the Constraint is created.
* So when a createAreas or createObjectGroups call uses this, it can rely on the cache,
* rather than reevaluating it for every randomized coordinate.
* Account for the fact that the cache is never updated!
*/
function StaticConstraint(constraints)
{
let mapSize = g_Map.getSize();
this.constraint = new AndConstraint(constraints);
this.cache = new Array(mapSize).fill(0).map(() => new Uint8Array(mapSize));
}
StaticConstraint.prototype.allows = function(position)
{
if (!this.cache[position.x][position.y])
this.cache[position.x][position.y] = this.constraint.allows(position) ? 2 : 1;
return this.cache[position.x][position.y] == 2;
};
/**
* Constrains the area to any tile on the map that is passable.
*/
function PassableMapAreaConstraint()
{
}
PassableMapAreaConstraint.prototype.allows = function(position)
{
return g_Map.validTilePassable(position);
};
Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/TileClass.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmgen/TileClass.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/TileClass.js (revision 22419)
@@ -1,159 +1,159 @@
//////////////////////////////////////////////////////////////////////
// RangeOp
//
// Class for efficiently finding number of points within a range
//
//////////////////////////////////////////////////////////////////////
function RangeOp(size)
{
// Get smallest power of 2 which is greater than or equal to size
this.nn = 1;
while (this.nn < size) {
this.nn *= 2;
}
this.vals = new Int16Array(2*this.nn); // int16
}
RangeOp.prototype.set = function(pos, amt)
{
this.add(pos, amt - this.vals[this.nn + pos]);
};
RangeOp.prototype.add = function(pos, amt)
{
for(var s = this.nn; s >= 1; s /= 2)
{
this.vals[s + pos] += amt;
pos = Math.floor(pos/2);
}
};
RangeOp.prototype.get = function(start, end)
{
var ret = 0;
var i = 1;
var nn = this.nn;
// Count from start to end by powers of 2
for (; start+i <= end; i *= 2)
{
if (start & i)
{ // For each bit in start
ret += this.vals[nn/i + Math.floor(start/i)];
start += i;
}
}
//
while(i >= 1)
{
if(start+i <= end)
{
ret += this.vals[nn/i + Math.floor(start/i)];
start += i;
}
i /= 2;
}
return ret;
};
/**
* Class that can be tagged to any tile. Can be used to constrain placers and entity placement to given areas.
*/
function TileClass(size)
{
this.size = size;
this.inclusionCount = [];
this.rangeCount = [];
for (let i=0; i < size; ++i)
{
this.inclusionCount[i] = new Int16Array(size); //int16
this.rangeCount[i] = new RangeOp(size);
}
}
TileClass.prototype.has = function(position)
{
return !!this.inclusionCount[position.x] && !!this.inclusionCount[position.x][position.y];
-}
+};
TileClass.prototype.add = function(position)
{
if (!this.inclusionCount[position.x][position.y] && g_Map.validTile(position))
this.rangeCount[position.y].add(position.x, 1);
++this.inclusionCount[position.x][position.y];
};
TileClass.prototype.remove = function(position)
{
--this.inclusionCount[position.x][position.y];
if (!this.inclusionCount[position.x][position.y])
this.rangeCount[position.y].add(position.x, -1);
};
TileClass.prototype.countInRadius = function(position, radius, returnMembers)
{
let members = 0;
let nonMembers = 0;
let radius2 = Math.square(radius);
for (let y = position.y - radius; y <= position.y + radius; ++y)
{
let iy = Math.floor(y);
if (radius >= 27) // Switchover point before RangeOp actually performs better than a straight algorithm
{
if (iy >= 0 && iy < this.size)
{
let dx = Math.sqrt(Math.square(radius) - Math.square(y - position.y));
let minX = Math.max(Math.floor(position.x - dx), 0);
let maxX = Math.min(Math.floor(position.x + dx), this.size - 1) + 1;
let newMembers = this.rangeCount[iy].get(minX, maxX);
members += newMembers;
nonMembers += maxX - minX - newMembers;
}
}
else // Simply check the tiles one by one to find the number
{
let dy = iy - position.y;
let xMin = Math.max(Math.floor(position.x - radius), 0);
let xMax = Math.max(Math.ceil(position.x + radius), this.size - 1);
for (let ix = xMin; ix <= xMax; ++ix)
{
let dx = ix - position.x;
if (Math.square(dx) + Math.square(dy) <= radius2)
{
if (this.inclusionCount[ix] && this.inclusionCount[ix][iy] && this.inclusionCount[ix][iy] > 0)
++members;
else
++nonMembers;
}
}
}
}
if (returnMembers)
return members;
else
return nonMembers;
};
TileClass.prototype.countMembersInRadius = function(position, radius)
{
return this.countInRadius(position, radius, true);
};
TileClass.prototype.countNonMembersInRadius = function(position, radius)
{
return this.countInRadius(position, radius, false);
};
Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer/noncentered/ConvexPolygonPlacer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer/noncentered/ConvexPolygonPlacer.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer/noncentered/ConvexPolygonPlacer.js (revision 22419)
@@ -1,70 +1,70 @@
/**
* Returns all points on the tilegrid within the convex hull of the given positions.
*/
function ConvexPolygonPlacer(points, failFraction = 0)
{
this.polygonVertices = this.getConvexHull(points.map(point => point.clone().round()));
this.failFraction = failFraction;
-};
+}
ConvexPolygonPlacer.prototype.place = function(constraint)
{
let points = [];
let count = 0;
let failed = 0;
for (let point of getPointsInBoundingBox(getBoundingBox(this.polygonVertices)))
{
if (this.polygonVertices.some((vertex, i) =>
distanceOfPointFromLine(this.polygonVertices[i], this.polygonVertices[(i + 1) % this.polygonVertices.length], point) > 0))
continue;
++count;
if (g_Map.inMapBounds(point) && constraint.allows(point))
points.push(point);
else
++failed;
}
return failed <= this.failFraction * count ? points : undefined;
};
/**
* Applies the gift-wrapping algorithm.
* Returns a sorted subset of the given points that are the vertices of the convex polygon containing all given points.
*/
ConvexPolygonPlacer.prototype.getConvexHull = function(points)
{
let uniquePoints = [];
for (let point of points)
if (uniquePoints.every(p => p.x != point.x || p.y != point.y))
uniquePoints.push(point);
// Start with the leftmost point
let result = [uniquePoints.reduce((leftMost, point) => point.x < leftMost.x ? point : leftMost, uniquePoints[0])];
// Add the vector most left of the most recently added point until a cycle is reached
while (result.length < uniquePoints.length)
{
let nextLeftmostPoint;
// Of all points, find the one that is leftmost
for (let point of uniquePoints)
{
if (point == result[result.length - 1])
continue;
if (!nextLeftmostPoint || distanceOfPointFromLine(nextLeftmostPoint, result[result.length - 1], point) <= 0)
nextLeftmostPoint = point;
}
// If it was a known one, then the remaining points are inside this hull
if (result.indexOf(nextLeftmostPoint) != -1)
break;
result.push(nextLeftmostPoint);
}
return result;
};
Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer/noncentered/EntitiesObstructionPlacer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer/noncentered/EntitiesObstructionPlacer.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/placer/noncentered/EntitiesObstructionPlacer.js (revision 22419)
@@ -1,31 +1,31 @@
/**
* The EntityObstructionPlacer returns all points on the obstruction of the given template at the given position and angle that meet the constraint.
* It can be used for more concise collision avoidance.
*/
function EntitiesObstructionPlacer(entities, margin = 0, failFraction = Infinity)
{
this.entities = entities;
this.margin = margin;
this.failFraction = failFraction;
}
EntitiesObstructionPlacer.prototype.place = function(constraint)
{
let points = [];
for (let entity of this.entities)
{
let halfObstructionSize = getObstructionSize(entity.templateName, this.margin).div(2);
let obstructionCorners = [
new Vector2D(-halfObstructionSize.x, -halfObstructionSize.y),
new Vector2D(-halfObstructionSize.x, +halfObstructionSize.y),
new Vector2D(+halfObstructionSize.x, -halfObstructionSize.y),
new Vector2D(+halfObstructionSize.x, +halfObstructionSize.y)
].map(corner => Vector2D.add(entity.GetPosition2D(), corner.rotate(-entity.rotation.y)));
- points = points.concat(new ConvexPolygonPlacer(obstructionCorners, this.failFraction).place(constraint))
+ points = points.concat(new ConvexPolygonPlacer(obstructionCorners, this.failFraction).place(constraint));
}
return points;
};
Index: ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 22419)
@@ -1,322 +1,322 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("heightmap");
setSkySet("fog");
setFogFactor(0.35);
setFogThickness(0.19);
setWaterColor(0.501961, 0.501961, 0.501961);
setWaterTint(0.25098, 0.501961, 0.501961);
setWaterWaviness(0.5);
setWaterType("clap");
setWaterMurkiness(0.75);
setPPSaturation(0.37);
setPPContrast(0.4);
setPPBrightness(0.4);
setPPEffect("hdr");
setPPBloom(0.4);
var oStoneLarge = 'gaia/geology_stonemine_alpine_quarry';
var oMetalLarge = 'gaia/geology_metal_alpine_slabs';
var oFish = "gaia/fauna_fish";
var aGrass = 'actor|props/flora/grass_soft_small_tall.xml';
var aGrassShort = 'actor|props/flora/grass_soft_large.xml';
var aRockLarge = 'actor|geology/stone_granite_med.xml';
var aRockMedium = 'actor|geology/stone_granite_med.xml';
var aBushMedium = 'actor|props/flora/bush_medit_me.xml';
var aBushSmall = 'actor|props/flora/bush_medit_sm.xml';
var aReeds = 'actor|props/flora/reeds_pond_lush_b.xml';
var terrainPrimary = ["temp_grass_plants", "temp_plants_bog"];
var terrainWood = ['alpine_forrestfloor|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine'];
var terrainWoodBorder = ['new_alpine_grass_mossy|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine',
'temp_grass_long|gaia/flora_bush_temperate', 'temp_grass_clovers|gaia/flora_bush_berry', 'temp_grass_clovers_2|gaia/flora_bush_grapes',
'temp_grass_plants|gaia/fauna_deer', 'temp_grass_plants|gaia/fauna_rabbit', 'new_alpine_grass_dirt_a'];
var terrainBase = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_grass_plants|gaia/fauna_sheep'];
var terrainBaseBorder = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants',
'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants'];
var baseTex = ['temp_road', 'temp_road_overgrown'];
var terrainPath = ['temp_road', 'temp_road_overgrown'];
var tWater = ['dirt_brown_d'];
var tWaterBorder = ['dirt_brown_d'];
const heightLand = 1;
const heightOffsetPath = -0.1;
var g_Map = new RandomMap(heightLand, terrainPrimary);
var clPlayer = g_Map.createTileClass();
var clPath = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clWater = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clOpen = g_Map.createTileClass();
var mapSize = g_Map.getSize();
var mapCenter = g_Map.getCenter();
var mapRadius = mapSize/2;
var numPlayers = getNumPlayers();
var baseRadius = 15;
var minPlayerRadius = Math.min(mapRadius - 1.5 * baseRadius, 5/8 * mapRadius);
var maxPlayerRadius = Math.min(mapRadius - baseRadius, 3/4 * mapRadius);
var playerPosition = [];
var playerAngleStart = randomAngle();
var playerAngleAddAvrg = 2 * Math.PI / numPlayers;
var playerAngleMaxOff = playerAngleAddAvrg/4;
var resourceRadius = fractionToTiles(1/3);
// Setup woods
// For large maps there are memory errors with too many trees. A density of 256*192/mapArea works with 0 players.
// Around each player there is an area without trees so with more players the max density can increase a bit.
var maxTreeDensity = Math.min(256 * (192 + 8 * numPlayers) / Math.square(mapSize), 1); // Has to be tweeked but works ok
var bushChance = 1/3; // 1 means 50% chance in deepest wood, 0.5 means 25% chance in deepest wood
// Set height limits and water level by map size
// Set target min and max height depending on map size to make average steepness about the same on all map sizes
var heightRange = {'min': MIN_HEIGHT * (g_Map.size + 512) / 8192, 'max': MAX_HEIGHT * (g_Map.size + 512) / 8192, 'avg': (MIN_HEIGHT * (g_Map.size + 512) +MAX_HEIGHT * (g_Map.size + 512))/16384};
// Set average water coverage
var averageWaterCoverage = 1/5; // NOTE: Since erosion is not predictable actual water coverage might vary much with the same values
var heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min);
var heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT;
setWaterHeight(heightSeaGround);
// Setting a 3x3 Grid as initial heightmap
var initialReliefmap = [[heightRange.max, heightRange.max, heightRange.max], [heightRange.max, heightRange.min, heightRange.max], [heightRange.max, heightRange.max, heightRange.max]];
setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialReliefmap);
g_Map.log("Smoothing map");
createArea(
new MapBoundsPlacer(),
new SmoothingPainter(1, 0.8, 5));
rescaleHeightmap(heightRange.min, heightRange.max);
var heighLimits = [
heightRange.min + 1/3 * (heightSeaGroundAdjusted - heightRange.min), // 0 Deep water
heightRange.min + 2/3 * (heightSeaGroundAdjusted - heightRange.min), // 1 Medium Water
heightRange.min + (heightSeaGroundAdjusted - heightRange.min), // 2 Shallow water
heightSeaGroundAdjusted + 1/8 * (heightRange.max - heightSeaGroundAdjusted), // 3 Shore
heightSeaGroundAdjusted + 2/8 * (heightRange.max - heightSeaGroundAdjusted), // 4 Low ground
heightSeaGroundAdjusted + 3/8 * (heightRange.max - heightSeaGroundAdjusted), // 5 Player and path height
heightSeaGroundAdjusted + 4/8 * (heightRange.max - heightSeaGroundAdjusted), // 6 High ground
heightSeaGroundAdjusted + 5/8 * (heightRange.max - heightSeaGroundAdjusted), // 7 Lower forest border
heightSeaGroundAdjusted + 6/8 * (heightRange.max - heightSeaGroundAdjusted), // 8 Forest
heightSeaGroundAdjusted + 7/8 * (heightRange.max - heightSeaGroundAdjusted), // 9 Upper forest border
heightSeaGroundAdjusted + (heightRange.max - heightSeaGroundAdjusted)]; // 10 Hilltop
g_Map.log("Locating and smoothing playerbases");
for (let i = 0; i < numPlayers; ++i)
{
playerPosition[i] = Vector2D.add(
mapCenter,
new Vector2D(randFloat(minPlayerRadius, maxPlayerRadius), 0).rotate(
-((playerAngleStart + i * playerAngleAddAvrg + randFloat(0, playerAngleMaxOff)) % (2 * Math.PI)))).round();
createArea(
new ClumpPlacer(diskArea(20), 0.8, 0.8, Infinity, playerPosition[i]),
new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(playerPosition[i]), 20));
}
placePlayerBases({
"PlayerPlacement": [sortAllPlayers(), playerPosition],
"BaseResourceClass": clBaseResource,
"Walls": false,
// player class painted below
"CityPatch": {
"radius": 0.8 * baseRadius,
"smoothness": 1/8,
"painters": [
new TerrainPainter([baseTex], [baseRadius/4, baseRadius/4]),
new TileClassPainter(clPlayer)
]
},
// No chicken
"Berries": {
"template": "gaia/flora_bush_berry",
"minCount": 2,
"maxCount": 2
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
],
"distance": 15,
"minAngle": Math.PI / 2,
"maxAngle": Math.PI
},
"Trees": {
"template": "gaia/flora_tree_oak_large",
"count": 2
}
});
g_Map.log("Creating mines");
for (let [minHeight, maxHeight] of [[heighLimits[3], (heighLimits[4] + heighLimits[3]) / 2], [(heighLimits[5] + heighLimits[6]) / 2, heighLimits[7]]])
for (let [template, tileClass] of [[oStoneLarge, clRock], [oMetalLarge, clMetal]])
createObjectGroups(
new SimpleGroup([new SimpleObject(template, 1, 1, 0, 4)], true, tileClass),
0,
[
new HeightConstraint(minHeight, maxHeight),
avoidClasses(clForest, 4, clPlayer, 20, clMetal, 40, clRock, 40)
],
scaleByMapSize(2, 8),
100,
false);
Engine.SetProgress(50);
g_Map.log("Painting textures");
var betweenShallowAndShore = (heighLimits[3] + heighLimits[2]) / 2;
createArea(
new HeightPlacer(Elevation_IncludeMin_IncludeMax, heighLimits[2], betweenShallowAndShore),
new LayeredPainter([terrainBase, terrainBaseBorder], [5]));
paintTileClassBasedOnHeight(heighLimits[2], betweenShallowAndShore, 1, clOpen);
createArea(
new HeightPlacer(Elevation_IncludeMin_IncludeMax, heightRange.min, heighLimits[2]),
new LayeredPainter([tWaterBorder, tWater], [2]));
paintTileClassBasedOnHeight(heightRange.min, heighLimits[2], 1, clWater);
Engine.SetProgress(60);
g_Map.log("Painting paths");
var pathBlending = numPlayers <= 4;
for (let i = 0; i < numPlayers + (pathBlending ? 1 : 0); ++i)
for (let j = pathBlending ? 0 : i + 1; j < numPlayers + 1; ++j)
{
let pathStart = i < numPlayers ? playerPosition[i] : mapCenter;
let pathEnd = j < numPlayers ? playerPosition[j] : mapCenter;
createArea(
new RandomPathPlacer(pathStart, pathEnd, 1.75, baseRadius / 2, pathBlending),
[
new TerrainPainter(terrainPath),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetPath, 1),
new TileClassPainter(clPath)
],
avoidClasses(clPath, 0, clOpen, 0 ,clWater, 4, clBaseResource, 4));
}
Engine.SetProgress(75);
g_Map.log("Creating decoration");
createDecoration(
[
[new SimpleObject(aRockMedium, 1, 3, 0, 1)],
[new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)],
[new SimpleObject(aGrassShort, 1, 2, 0, 1)],
[new SimpleObject(aGrass, 2, 4, 0, 1.8), new SimpleObject(aGrassShort, 3, 6, 1.2, 2.5)],
[new SimpleObject(aBushMedium, 1, 2, 0, 2), new SimpleObject(aBushSmall, 2, 4, 0, 2)]
],
[
scaleByMapSize(16, 262),
scaleByMapSize(8, 131),
scaleByMapSize(13, 200),
scaleByMapSize(13, 200),
scaleByMapSize(13, 200)
],
avoidClasses(clForest, 1, clPlayer, 0, clPath, 3, clWater, 3));
Engine.SetProgress(80);
g_Map.log("Growing fish");
createFood(
[
[new SimpleObject(oFish, 2, 3, 0, 2)]
],
[
100 * numPlayers
],
[avoidClasses(clFood, 5), stayClasses(clWater, 4)],
clFood);
Engine.SetProgress(85);
g_Map.log("Planting reeds");
var types = [aReeds];
for (let type of types)
createObjectGroupsDeprecated(
new SimpleGroup([new SimpleObject(type, 1, 1, 0, 0)], true),
0,
borderClasses(clWater, 0, 6),
scaleByMapSize(1, 2) * 1000,
1000);
Engine.SetProgress(90);
g_Map.log("Planting trees");
for (var x = 0; x < mapSize; x++)
- for (var z = 0;z < mapSize;z++)
+ for (var z = 0; z < mapSize; z++)
{
let position = new Vector2D(x, z);
if (!g_Map.validTile(position))
continue;
// The 0.5 is a correction for the entities placed on the center of tiles
let radius = Vector2D.add(position, new Vector2D(0.5, 0.5)).distanceTo(mapCenter);
var minDistToSL = mapSize;
for (let i = 0; i < numPlayers; ++i)
minDistToSL = Math.min(minDistToSL, position.distanceTo(playerPosition[i]));
// Woods tile based
var tDensFactSL = Math.max(Math.min((minDistToSL - baseRadius) / baseRadius, 1), 0);
var tDensFactRad = Math.abs((resourceRadius - radius) / resourceRadius);
var tDensActual = (maxTreeDensity * tDensFactSL * tDensFactRad)*0.75;
if (!randBool(tDensActual))
continue;
let border = tDensActual < randFloat(0, bushChance * maxTreeDensity);
let constraint = border ?
avoidClasses(clPath, 1, clOpen, 2, clWater, 3, clMetal, 4, clRock, 4) :
avoidClasses(clPath, 2, clOpen, 3, clWater, 4, clMetal, 4, clRock, 4);
if (constraint.allows(position))
{
clForest.add(position);
createTerrain(border ? terrainWoodBorder : terrainWood).place(position);
}
}
placePlayersNomad(clPlayer, avoidClasses(clWater, 4, clForest, 1, clFood, 2, clMetal, 4, clRock, 4));
Engine.SetProgress(100);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/maps/random/syria.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/syria.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/syria.js (revision 22419)
@@ -1,285 +1,285 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
const tMainDirt = ["desert_dirt_rocks_1", "desert_dirt_cracks"];
const tForestFloor1 = "forestfloor_dirty";
const tForestFloor2 = "desert_forestfloor_palms";
const tGrassSands = "desert_grass_a_sand";
const tGrass = "desert_grass_a";
const tSecondaryDirt = "medit_dirt_dry";
const tCliff = ["desert_cliff_persia_1", "desert_cliff_persia_2"];
const tHill = ["desert_dirt_rocks_1", "desert_dirt_rocks_2", "desert_dirt_rocks_3"];
const tDirt = ["desert_dirt_rough", "desert_dirt_rough_2"];
-const tRoad = "desert_shore_stones";;
-const tRoadWild = "desert_grass_a_stones";;
+const tRoad = "desert_shore_stones";
+const tRoadWild = "desert_grass_a_stones";
const oTamarix = "gaia/flora_tree_tamarix";
const oPalm = "gaia/flora_tree_date_palm";
const oPine = "gaia/flora_tree_aleppo_pine";
const oBush = "gaia/flora_bush_grapes";
const oCamel = "gaia/fauna_camel";
const oGazelle = "gaia/fauna_gazelle";
const oLion = "gaia/fauna_lion";
const oStoneLarge = "gaia/geology_stonemine_desert_quarry";
const oStoneSmall = "gaia/geology_stone_desert_small";
const oMetalLarge = "gaia/geology_metal_desert_slabs";
const aRock = "actor|geology/stone_desert_med.xml";
const aBushA = "actor|props/flora/bush_desert_dry_a.xml";
const aBushB = "actor|props/flora/bush_desert_dry_a.xml";
const aBushes = [aBushA, aBushB];
const pForestP = [tForestFloor2 + TERRAIN_SEPARATOR + oPalm, tForestFloor2];
const pForestT = [tForestFloor1 + TERRAIN_SEPARATOR + oTamarix,tForestFloor2];
const heightLand = 1;
const heightHill = 22;
const heightOffsetBump = 2;
var g_Map = new RandomMap(heightLand, tMainDirt);
const mapCenter = g_Map.getCenter();
const numPlayers = getNumPlayers();
var clPlayer = g_Map.createTileClass();
var clHill = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clGrass = g_Map.createTileClass();
var [playerIDs, playerPosition] = playerPlacementCircle(fractionToTiles(0.35));
g_Map.log("Creating big grass patches around the playerbases");
for (let i = 0; i < numPlayers; ++i)
{
if (!isNomad())
createArea(
new ClumpPlacer(diskArea(defaultPlayerBaseRadius()), 0.9, 0.5, Infinity, playerPosition[i]),
new TileClassPainter(clPlayer));
createArea(
new ChainPlacer(
2,
Math.floor(scaleByMapSize(5, 12)),
Math.floor(scaleByMapSize(25, 60)) / (isNomad() ? 2 : 1),
Infinity,
playerPosition[i],
0,
[Math.floor(scaleByMapSize(16, 30))]),
[
new LayeredPainter([tGrassSands, tGrass], [3]),
new TileClassPainter(clGrass)
]);
}
Engine.SetProgress(10);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
// PlayerTileClass marked above
"BaseResourceClass": clBaseResource,
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad,
"radius": 10,
"width": 3
},
"Chicken": {
},
"Berries": {
"template": oBush
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
],
"groupElements": [new RandomObject(aBushes, 2, 4, 2, 3)]
},
"Trees": {
"template": pickRandom([oPalm, oTamarix]),
"count": 3
}
// No decoratives
});
Engine.SetProgress(20);
g_Map.log("Creating bumps");
createAreas(
new ClumpPlacer(scaleByMapSize(20, 50), 0.3, 0.06, Infinity),
new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetBump, 2),
avoidClasses(clPlayer, 13),
scaleByMapSize(300, 800));
g_Map.log("Creating hills");
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(4, 6)), Math.floor(scaleByMapSize(16, 40)), 0.5),
[
new LayeredPainter([tCliff, tHill], [2]),
new SmoothElevationPainter(ELEVATION_SET, heightHill, 2),
new TileClassPainter(clHill)
],
avoidClasses(clPlayer, 3, clGrass, 1, clHill, 10),
scaleByMapSize(1, 3) * numPlayers * 3);
Engine.SetProgress(25);
g_Map.log("Creating forests");
var [forestTrees, stragglerTrees] = getTreeCounts(400, 2000, 0.7);
var types = [
[[tMainDirt, tForestFloor2, pForestP], [tForestFloor2, pForestP]],
[[tMainDirt, tForestFloor1, pForestT], [tForestFloor1, pForestT]]
];
var size = forestTrees / (scaleByMapSize(3,6) * numPlayers);
var num = Math.floor(size / types.length);
for (let type of types)
createAreas(
new ChainPlacer(
1,
Math.floor(scaleByMapSize(3, 5)),
forestTrees / (num * Math.floor(scaleByMapSize(2, 4))),
0.5),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
avoidClasses(clPlayer, 1, clGrass, 1, clForest, 10, clHill, 1),
num);
Engine.SetProgress(40);
g_Map.log("Creating dirt patches");
for (let size of [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)])
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5),
new LayeredPainter([tSecondaryDirt, tDirt], [1]),
avoidClasses(clHill, 0, clForest, 0, clPlayer, 8, clGrass, 1),
scaleByMapSize(50, 90));
Engine.SetProgress(60);
g_Map.log("Creating big patches");
for (let size of [scaleByMapSize(6, 30), scaleByMapSize(10, 50), scaleByMapSize(16, 70)])
createAreas(
new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5),
new LayeredPainter([tSecondaryDirt, tDirt], [1]),
avoidClasses(clHill, 0, clForest, 0, clPlayer, 8, clGrass, 1),
scaleByMapSize(30, 90));
Engine.SetProgress(70);
g_Map.log("Creating stone mines");
createObjectGroupsDeprecated(
new SimpleGroup(
[
new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1),
new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4),
new RandomObject(aBushes, 2, 4, 0, 2)
],
true,
clRock),
0,
[avoidClasses(clForest, 1, clPlayer, 10, clRock, 10, clHill, 1, clGrass, 1)],
scaleByMapSize(2, 8),
100);
g_Map.log("Creating small stone quarries");
var group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3), new RandomObject(aBushes, 2,4, 0,2)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clRock, 10, clHill, 1, clGrass, 1)],
scaleByMapSize(2,8), 100
);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4), new RandomObject(aBushes, 2,4, 0,2)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clMetal, 10, clRock, 5, clHill, 1, clGrass, 1)],
scaleByMapSize(2,8), 100
);
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRock, 1,3, 0,1)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clForest, 0, clPlayer, 0, clHill, 0),
scaleByMapSize(16, 262), 50
);
g_Map.log("Creating bushes");
group = new SimpleGroup(
[new SimpleObject(aBushB, 1,2, 0,1), new SimpleObject(aBushA, 1,3, 0,2)],
true
);
createObjectGroupsDeprecated(
group, 0,
avoidClasses(clForest, 0, clPlayer, 0, clHill, 0),
scaleByMapSize(50, 500), 50
);
Engine.SetProgress(80);
g_Map.log("Creating gazelle");
group = new SimpleGroup(
[new SimpleObject(oGazelle, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 0, clPlayer, 1, clHill, 1, clFood, 20, clGrass, 2),
3 * numPlayers, 50
);
g_Map.log("Creating lions");
group = new SimpleGroup(
[new SimpleObject(oLion, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 0, clPlayer, 1, clHill, 1, clFood, 20, clGrass, 2),
3 * numPlayers, 50
);
g_Map.log("Creating camels");
group = new SimpleGroup(
[new SimpleObject(oCamel, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
avoidClasses(clForest, 0, clPlayer, 1, clHill, 1, clFood, 20, clGrass, 2),
3 * numPlayers, 50
);
Engine.SetProgress(85);
createStragglerTrees(
[oPalm, oTamarix, oPine],
avoidClasses(clForest, 1, clHill, 1, clPlayer, 1, clMetal, 6, clRock, 6),
clForest,
stragglerTrees);
createStragglerTrees(
[oPalm, oTamarix, oPine],
[avoidClasses(clForest, 1, clHill, 1, clPlayer, 1, clMetal, 6, clRock, 6), stayClasses(clGrass, 3)],
clForest,
stragglerTrees * (isNomad() ? 3 : 1));
placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clMetal, 4, clRock, 4, clHill, 4, clFood, 2));
setSkySet("sunny");
setSunElevation(Math.PI / 8);
setSunRotation(randomAngle());
setSunColor(0.746, 0.718, 0.539);
setWaterColor(0.292, 0.347, 0.691);
setWaterTint(0.550, 0.543, 0.437);
setWaterMurkiness(0.83);
setFogColor(0.8, 0.76, 0.61);
setFogThickness(0.2);
setFogFactor(0.4);
setPPEffect("hdr");
setPPContrast(0.65);
setPPSaturation(0.42);
setPPBloom(0.6);
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Mirage.js (revision 22419)
@@ -1,227 +1,227 @@
const VIS_HIDDEN = 0;
const VIS_FOGGED = 1;
const VIS_VISIBLE = 2;
function Mirage() {}
Mirage.prototype.Schema =
"Mirage entities replace real entities in the fog-of-war." +
"";
Mirage.prototype.Init = function()
{
this.player = null;
this.parent = INVALID_ENTITY;
this.miragedIids = new Set();
this.classesList = [];
this.numBuilders = 0;
this.buildTime = {};
this.maxHitpoints = null;
this.hitpoints = null;
this.repairable = null;
this.unhealable = null;
this.injured = null;
this.capturePoints = [];
this.maxCapturePoints = 0;
this.maxAmount = null;
this.amount = null;
this.type = null;
this.isInfinite = null;
this.killBeforeGather = null;
this.maxGatherers = null;
this.numGatherers = null;
this.traders = null;
this.marketType = null;
this.internationalBonus = null;
};
Mirage.prototype.SetParent = function(ent)
{
this.parent = ent;
};
Mirage.prototype.GetPlayer = function()
{
return this.player;
};
Mirage.prototype.SetPlayer = function(player)
{
this.player = player;
};
Mirage.prototype.Mirages = function(iid)
{
return this.miragedIids.has(iid);
};
// ============================
// Parent entity data
Mirage.prototype.CopyIdentity = function(cmpIdentity)
{
this.miragedIids.add(IID_Identity);
// In almost all cases we want to ignore mirage entities when querying Identity components of owned entities.
// To avoid adding a test everywhere, we don't transfer the classeslist in the template but here.
// We clone this since the classes list is not synchronized and since the mirage should be a snapshot of the entity at the given time.
this.classesList = clone(cmpIdentity.GetClassesList());
};
-Mirage.prototype.GetClassesList = function() { return this.classesList };
+Mirage.prototype.GetClassesList = function() { return this.classesList; };
// Foundation data
Mirage.prototype.CopyFoundation = function(cmpFoundation)
{
this.miragedIids.add(IID_Foundation);
this.numBuilders = cmpFoundation.GetNumBuilders();
this.buildTime = cmpFoundation.GetBuildTime();
};
Mirage.prototype.GetNumBuilders = function() { return this.numBuilders; };
Mirage.prototype.GetBuildTime = function() { return this.buildTime; };
// Repairable data (numBuilders and buildTime shared with foundation as entities can't have both)
Mirage.prototype.CopyRepairable = function(cmpRepairable)
{
this.miragedIids.add(IID_Repairable);
this.numBuilders = cmpRepairable.GetNumBuilders();
this.buildTime = cmpRepairable.GetBuildTime();
};
// Health data
Mirage.prototype.CopyHealth = function(cmpHealth)
{
this.miragedIids.add(IID_Health);
this.maxHitpoints = cmpHealth.GetMaxHitpoints();
this.hitpoints = cmpHealth.GetHitpoints();
this.repairable = cmpHealth.IsRepairable();
this.injured = cmpHealth.IsInjured();
this.unhealable = cmpHealth.IsUnhealable();
};
Mirage.prototype.GetMaxHitpoints = function() { return this.maxHitpoints; };
Mirage.prototype.GetHitpoints = function() { return this.hitpoints; };
Mirage.prototype.IsRepairable = function() { return this.repairable; };
Mirage.prototype.IsInjured = function() { return this.injured; };
Mirage.prototype.IsUnhealable = function() { return this.unhealable; };
// Capture data
Mirage.prototype.CopyCapturable = function(cmpCapturable)
{
this.miragedIids.add(IID_Capturable);
this.capturePoints = clone(cmpCapturable.GetCapturePoints());
this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
};
Mirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; };
Mirage.prototype.GetCapturePoints = function() { return this.capturePoints; };
Mirage.prototype.CanCapture = Capturable.prototype.CanCapture;
// ResourceSupply data
Mirage.prototype.CopyResourceSupply = function(cmpResourceSupply)
{
this.miragedIids.add(IID_ResourceSupply);
this.maxAmount = cmpResourceSupply.GetMaxAmount();
this.amount = cmpResourceSupply.GetCurrentAmount();
this.type = cmpResourceSupply.GetType();
this.isInfinite = cmpResourceSupply.IsInfinite();
this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather();
this.maxGatherers = cmpResourceSupply.GetMaxGatherers();
this.numGatherers = cmpResourceSupply.GetNumGatherers();
};
Mirage.prototype.GetMaxAmount = function() { return this.maxAmount; };
Mirage.prototype.GetCurrentAmount = function() { return this.amount; };
Mirage.prototype.GetType = function() { return this.type; };
Mirage.prototype.IsInfinite = function() { return this.isInfinite; };
Mirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; };
Mirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; };
Mirage.prototype.GetNumGatherers = function() { return this.numGatherers; };
// Market data
Mirage.prototype.CopyMarket = function(cmpMarket)
{
this.miragedIids.add(IID_Market);
this.traders = new Set();
for (let trader of cmpMarket.GetTraders())
{
let cmpTrader = Engine.QueryInterface(trader, IID_Trader);
let cmpOwnership = Engine.QueryInterface(trader, IID_Ownership);
if (!cmpTrader || !cmpOwnership)
{
cmpMarket.RemoveTrader(trader);
continue;
}
if (this.player != cmpOwnership.GetOwner())
continue;
cmpTrader.SwitchMarket(cmpMarket.entity, this.entity);
cmpMarket.RemoveTrader(trader);
this.AddTrader(trader);
}
this.marketType = cmpMarket.GetType();
this.internationalBonus = cmpMarket.GetInternationalBonus();
};
Mirage.prototype.HasType = function(type) { return this.marketType.has(type); };
Mirage.prototype.GetInternationalBonus = function() { return this.internationalBonus; };
Mirage.prototype.AddTrader = function(trader) { this.traders.add(trader); };
Mirage.prototype.RemoveTrader = function(trader) { this.traders.delete(trader); };
Mirage.prototype.UpdateTraders = function(msg)
{
let cmpMarket = Engine.QueryInterface(this.parent, IID_Market);
if (!cmpMarket) // The parent market does not exist anymore
{
for (let trader of this.traders)
{
let cmpTrader = Engine.QueryInterface(trader, IID_Trader);
if (cmpTrader)
cmpTrader.RemoveMarket(this.entity);
}
return;
}
// The market becomes visible, switch all traders from the mirage to the market
for (let trader of this.traders)
{
let cmpTrader = Engine.QueryInterface(trader, IID_Trader);
if (!cmpTrader)
continue;
cmpTrader.SwitchMarket(this.entity, cmpMarket.entity);
this.RemoveTrader(trader);
cmpMarket.AddTrader(trader);
}
};
// ============================
Mirage.prototype.OnVisibilityChanged = function(msg)
{
// Mirages get VIS_HIDDEN when the original entity becomes VIS_VISIBLE.
if (msg.player != this.player || msg.newVisibility != VIS_HIDDEN)
return;
if (this.miragedIids.has(IID_Market))
this.UpdateTraders(msg);
if (this.parent == INVALID_ENTITY)
Engine.DestroyEntity(this.entity);
else
Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": this.parent });
};
Engine.RegisterComponentType(IID_Mirage, "Mirage", Mirage);
Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 22419)
@@ -1,481 +1,481 @@
function TechnologyManager() {}
TechnologyManager.prototype.Schema =
"";
TechnologyManager.prototype.Serialize = function()
{
// The modifications cache will be affected by property reads from the GUI and other places so we shouldn't
// serialize it.
var ret = {};
for (var i in this)
{
if (this.hasOwnProperty(i))
ret[i] = this[i];
}
ret.modificationCache = {};
return ret;
};
TechnologyManager.prototype.Init = function()
{
// Holds names of technologies that have been researched.
this.researchedTechs = new Set();
// Maps from technolgy name to the entityID of the researcher.
this.researchQueued = new Map();
// Holds technologies which are being researched currently (non-queued).
this.researchStarted = new Set();
// This stores the modifications to unit stats from researched technologies
// Example data: {"ResourceGatherer/Rates/food.grain": [
// {"multiply": 1.15, "affects": ["FemaleCitizen", "Infantry Sword"]},
// {"add": 2}
// ]}
this.modifications = {};
this.modificationCache = {}; // Caches the values after technologies have been applied
// e.g. { "Attack/Melee/Damage/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...}
// where 5 and 7 are entity id's
this.classCounts = {}; // stores the number of entities of each Class
this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e.
// {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...}
// Some technologies are automatically researched when their conditions are met. They have no cost and are
// researched instantly. This allows civ bonuses and more complicated technologies.
this.unresearchedAutoResearchTechs = new Set();
let allTechs = TechnologyTemplates.GetAll();
for (let key in allTechs)
if (allTechs[key].autoResearch || allTechs[key].top)
this.unresearchedAutoResearchTechs.add(key);
};
TechnologyManager.prototype.OnUpdate = function()
{
this.UpdateAutoResearch();
};
// This function checks if the requirements of any autoresearch techs are met and if they are it researches them
TechnologyManager.prototype.UpdateAutoResearch = function()
{
for (let key of this.unresearchedAutoResearchTechs)
{
let tech = TechnologyTemplates.Get(key);
if ((tech.autoResearch && this.CanResearch(key))
|| (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom))))
{
this.unresearchedAutoResearchTechs.delete(key);
this.ResearchTechnology(key);
return; // We will have recursively handled any knock-on effects so can just return
}
}
};
// Checks an entity template to see if its technology requirements have been met
TechnologyManager.prototype.CanProduce = function (templateName)
{
var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempManager.GetTemplate(templateName);
if (template.Identity && template.Identity.RequiredTechnology)
return this.IsTechnologyResearched(template.Identity.RequiredTechnology);
// If there is no required technology then this entity can be produced
return true;
};
TechnologyManager.prototype.IsTechnologyQueued = function(tech)
{
return this.researchQueued.has(tech);
};
TechnologyManager.prototype.IsTechnologyResearched = function(tech)
{
return this.researchedTechs.has(tech);
};
TechnologyManager.prototype.IsTechnologyStarted = function(tech)
{
return this.researchStarted.has(tech);
};
// Checks the requirements for a technology to see if it can be researched at the current time
TechnologyManager.prototype.CanResearch = function(tech)
{
let template = TechnologyTemplates.Get(tech);
if (!template)
{
warn("Technology \"" + tech + "\" does not exist");
return false;
}
if (template.top && this.IsInProgress(template.top) ||
template.bottom && this.IsInProgress(template.bottom))
return false;
if (template.pair && !this.CanResearch(template.pair))
return false;
if (this.IsInProgress(tech))
return false;
if (this.IsTechnologyResearched(tech))
return false;
return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv()));
};
/**
* Private function for checking a set of requirements is met
* @param {object} reqs - Technology requirements as derived from the technology template by globalscripts
* @param {boolean} civonly - True if only the civ requirement is to be checked
*
* @return true if the requirements pass, false otherwise
*/
TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false)
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (!reqs)
return false;
if (civonly || !reqs.length)
return true;
return reqs.some(req => {
return Object.keys(req).every(type => {
switch (type)
{
case "techs":
return req[type].every(this.IsTechnologyResearched, this);
case "entities":
return req[type].every(this.DoesEntitySpecPass, this);
}
return false;
});
});
};
TechnologyManager.prototype.DoesEntitySpecPass = function(entity)
{
switch (entity.check)
{
case "count":
if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number)
return false;
break;
case "variants":
if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number)
return false;
break;
}
return true;
};
TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg)
{
// This automatically updates classCounts and typeCountsByClass
var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID();
if (msg.to == playerID)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (!cmpIdentity)
return;
var classes = cmpIdentity.GetClassesList();
// don't use foundations for the class counts but check if techs apply (e.g. health increase)
if (!Engine.QueryInterface(msg.entity, IID_Foundation))
{
for (let cls of classes)
{
this.classCounts[cls] = this.classCounts[cls] || 0;
this.classCounts[cls] += 1;
this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {};
this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0;
this.typeCountsByClass[cls][template] += 1;
}
}
// Newly created entity, check if any researched techs might apply
// (only do this for new entities because even if an entity is converted or captured,
// we want it to maintain whatever technologies previously applied)
if (msg.from == INVALID_PLAYER)
{
var modifiedComponents = {};
for (var name in this.modifications)
{
// We only need to find one one tech per component for a match
var modifications = this.modifications[name];
var component = name.split("/")[0];
for (let modif of modifications)
if (DoesModificationApply(modif, classes))
{
if (!modifiedComponents[component])
modifiedComponents[component] = [];
modifiedComponents[component].push(name);
}
}
// Send mesage(s) to the entity so it knows about researched techs
for (var component in modifiedComponents)
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] });
}
}
if (msg.from == playerID)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);
// don't use foundations for the class counts
if (!Engine.QueryInterface(msg.entity, IID_Foundation))
{
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (cmpIdentity)
{
var classes = cmpIdentity.GetClassesList();
for (let cls of classes)
{
this.classCounts[cls] -= 1;
if (this.classCounts[cls] <= 0)
delete this.classCounts[cls];
this.typeCountsByClass[cls][template] -= 1;
if (this.typeCountsByClass[cls][template] <= 0)
delete this.typeCountsByClass[cls][template];
}
}
}
this.clearModificationCache(msg.entity);
}
};
// Marks a technology as researched. Note that this does not verify that the requirements are met.
TechnologyManager.prototype.ResearchTechnology = function(tech)
{
this.StoppedResearch(tech, false);
var modifiedComponents = {};
this.researchedTechs.add(tech);
// store the modifications in an easy to access structure
let template = TechnologyTemplates.Get(tech);
if (template.modifications)
{
let derivedModifiers = DeriveModificationsFromTech(template);
for (let modifierPath in derivedModifiers)
{
if (!this.modifications[modifierPath])
this.modifications[modifierPath] = [];
this.modifications[modifierPath] = this.modifications[modifierPath].concat(derivedModifiers[modifierPath]);
let component = modifierPath.split("/")[0];
if (!modifiedComponents[component])
modifiedComponents[component] = [];
modifiedComponents[component].push(modifierPath);
this.modificationCache[modifierPath] = {};
}
}
if (template.replaces && template.replaces.length > 0)
{
for (var i of template.replaces)
{
if (!i || this.IsTechnologyResearched(i))
continue;
this.researchedTechs.add(i);
// Change the EntityLimit if any
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined)
{
let playerID = cmpPlayer.GetPlayerID();
let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.UpdateLimitsFromTech(i);
}
}
}
this.UpdateAutoResearch();
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined)
return;
var playerID = cmpPlayer.GetPlayerID();
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = cmpRangeManager.GetEntitiesByPlayer(playerID);
ents.push(this.entity);
// Change the EntityLimit if any
var cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.UpdateLimitsFromTech(tech);
// always send research finished message
Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech});
for (var component in modifiedComponents)
{
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]});
Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]});
}
if (tech.startsWith("phase") && !template.autoResearch)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "phase",
"players": [playerID],
"phaseName": tech,
"phaseState": "completed"
});
}
};
// Clears the cached data for an entity from the modifications cache
TechnologyManager.prototype.clearModificationCache = function(ent)
{
for (var valueName in this.modificationCache)
delete this.modificationCache[valueName][ent];
};
// Caching layer in front of ApplyModificationsWorker
// Note: be careful with the type of curValue, if it should be a numerical
// value and is derived from template data, you must convert the string
// from the template to a number using the + operator, before calling
// this function!
TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent)
{
if (!this.modificationCache[valueName])
this.modificationCache[valueName] = {};
if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
return curValue;
this.modificationCache[valueName][ent] = {
"origValue": curValue,
"newValue": GetTechModifiedProperty(this.modifications, cmpIdentity.GetClassesList(), valueName, curValue)
};
}
return this.modificationCache[valueName][ent].newValue;
};
// Alternative version of ApplyModifications, applies to templates instead of entities
TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template)
{
if (!template || !template.Identity)
return curValue;
return GetTechModifiedProperty(this.modifications, GetIdentityClasses(template.Identity), valueName, curValue);
};
/**
* Marks a technology as being queued for research at the given entityID.
*/
TechnologyManager.prototype.QueuedResearch = function(tech, researcher)
{
this.researchQueued.set(tech, researcher);
};
// Marks a technology as actively being researched
TechnologyManager.prototype.StartedResearch = function(tech, notification)
{
this.researchStarted.add(tech);
if (notification && tech.startsWith("phase"))
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "phase",
"players": [cmpPlayer.GetPlayerID()],
"phaseName": tech,
"phaseState": "started"
});
}
};
/**
* Marks a technology as not being currently researched and optionally sends a GUI notification.
*/
TechnologyManager.prototype.StoppedResearch = function(tech, notification)
{
if (notification && tech.startsWith("phase") && this.researchStarted.has(tech))
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"type": "phase",
"players": [cmpPlayer.GetPlayerID()],
"phaseName": tech,
"phaseState": "aborted"
});
}
this.researchQueued.delete(tech);
this.researchStarted.delete(tech);
};
/**
* Checks whether a technology is set to be researched.
*/
TechnologyManager.prototype.IsInProgress = function(tech)
{
return this.researchQueued.has(tech);
};
/**
* Returns the names of technologies that are currently being researched (non-queued).
*/
TechnologyManager.prototype.GetStartedTechs = function()
{
return this.researchStarted;
};
/**
* Gets the entity currently researching the technology.
*/
TechnologyManager.prototype.GetResearcher = function(tech)
{
- return this.researchQueued.get(tech)
+ return this.researchQueued.get(tech);
};
/**
* Called by GUIInterface for PlayerData. AI use.
*/
TechnologyManager.prototype.GetQueuedResearch = function()
{
return this.researchQueued;
};
/**
* Returns the names of technologies that have already been researched.
*/
TechnologyManager.prototype.GetResearchedTechs = function()
{
return this.researchedTechs;
};
TechnologyManager.prototype.GetClassCounts = function()
{
return this.classCounts;
};
TechnologyManager.prototype.GetTypeCountsByClass = function()
{
return this.typeCountsByClass;
};
Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js (revision 22419)
@@ -1,24 +1,24 @@
Engine.RegisterInterface("TestSetup");
-function TestSetup() {};
+function TestSetup() {}
TestSetup.prototype.Init = function() {};
Engine.RegisterSystemComponentType(IID_TestSetup, "TestSetup", TestSetup);
let cmpTestSetup = ConstructComponent(SYSTEM_ENTITY, "TestSetup", { "property": "value" });
TS_ASSERT_EXCEPTION(() => { cmpTestSetup.template = "replacement forbidden"; });
TS_ASSERT_EXCEPTION(() => { cmpTestSetup.template.property = "modification forbidden"; });
TS_ASSERT_EXCEPTION(() => { cmpTestSetup.template.other_property = "insertion forbidden"; });
TS_ASSERT_EXCEPTION(() => { delete cmpTestSetup.entity; });
TS_ASSERT_EXCEPTION(() => { delete cmpTestSetup.template; });
TS_ASSERT_EXCEPTION(() => { delete cmpTestSetup.template.property; });
TS_ASSERT_UNEVAL_EQUALS(cmpTestSetup.template, { "property": "value" });
TS_ASSERT_NUMBER(0);
TS_ASSERT_NUMBER(1);
TS_ASSERT_NUMBER(-1);
TS_ASSERT_NUMBER(0.5);
TS_ASSERT_NUMBER(1/3);
TS_ASSERT_NUMBER(Math.sqrt(2));
TS_ASSERT_NUMBER(Math.PI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_RallyPoint.js (revision 22419)
@@ -1,79 +1,79 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("RallyPoint.js");
function initialRallyPointTest(test_function)
{
ResetState();
let entityID = 123;
let cmpRallyPoint = ConstructComponent(entityID, "RallyPoint", {});
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), []);
cmpRallyPoint.AddPosition(3, 1415);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }]);
cmpRallyPoint.AddPosition(926, 535);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }, { "x": 926, "z": 535 }]);
let targetID = 456;
let myData = { "command": "write a unit test", "target": targetID };
cmpRallyPoint.AddData(myData);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }, { "x": 926, "z": 535 }]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), [myData]);
let targetID2 = 789;
let myData2 = { "command": "this time really", "target": targetID2 };
cmpRallyPoint.AddData(myData2);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), [myData, myData2]);
if (test_function(cmpRallyPoint))
{
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), []);
}
else
{
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), [myData, myData2]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 3, "z": 1415 }, { "x": 926, "z": 535 }]);
}
}
initialRallyPointTest((cmpRallyPoint) => {});
initialRallyPointTest((cmpRallyPoint) => {
- cmpRallyPoint.Unset()
+ cmpRallyPoint.Unset();
return true;
});
initialRallyPointTest((cmpRallyPoint) => {
- cmpRallyPoint.Reset()
+ cmpRallyPoint.Reset();
return true;
});
// Construction
initialRallyPointTest((cmpRallyPoint) => {
cmpRallyPoint.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": 1 });
return false;
});
// Capturing
initialRallyPointTest((cmpRallyPoint) => {
cmpRallyPoint.OnOwnershipChanged({ "from": 1, "to": 2 });
return true;
});
// Destruction
initialRallyPointTest((cmpRallyPoint) => {
cmpRallyPoint.OnOwnershipChanged({ "from": 2, "to": INVALID_PLAYER });
return false;
});
// Gaia
initialRallyPointTest((cmpRallyPoint) => {
cmpRallyPoint.OnOwnershipChanged({ "from": 2, "to": 0 });
return true;
});
Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 22419)
@@ -1,927 +1,927 @@
/**
* @file Contains functionality to place walls on random maps.
*/
/**
* Set some globals for this module.
*/
var g_WallStyles = loadWallsetsFromCivData();
var g_FortressTypes = createDefaultFortressTypes();
/**
* Fetches wallsets from {civ}.json files, and then uses them to load
* basic wall elements.
*/
function loadWallsetsFromCivData()
{
let wallsets = {};
for (let civ in g_CivData)
{
let civInfo = g_CivData[civ];
if (!civInfo.WallSets)
continue;
for (let path of civInfo.WallSets)
{
// File naming conventions:
// - other/wallset_{style}
// - structures/{civ}_wallset_{style}
let style = basename(path).split("_");
style = style[0] == "wallset" ? style[1] : style[0] + "_" + style[2];
if (!wallsets[style])
wallsets[style] = loadWallset(Engine.GetTemplate(path), civ);
}
}
return wallsets;
}
function loadWallset(wallsetPath, civ)
{
let newWallset = { "curves": [] };
let wallsetData = GetTemplateDataHelper(wallsetPath).wallSet;
for (let element in wallsetData.templates)
if (element == "curves")
for (let filename of wallsetData.templates.curves)
newWallset.curves.push(readyWallElement(filename, civ));
else
newWallset[element] = readyWallElement(wallsetData.templates[element], civ);
newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length;
return newWallset;
}
/**
* Fortress class definition
*
* We use "fortress" to describe a closed wall built of multiple wall
* elements attached together surrounding a central point. We store the
* abstract of the wall (gate, tower, wall, ...) and only apply the style
* when we get to build it.
*
* @param {string} type - Descriptive string, example: "tiny". Not really needed (WallTool.wallTypes["type string"] is used). Mainly for custom wall elements.
* @param {array} [wall] - Array of wall element strings. May be defined at a later point.
* Example: ["medium", "cornerIn", "gate", "cornerIn", "medium", "cornerIn", "gate", "cornerIn"]
* @param {object} [centerToFirstElement] - Vector from the visual center of the fortress to the first wall element.
* @param {number} [centerToFirstElement.x]
* @param {number} [centerToFirstElement.y]
*/
function Fortress(type, wall=[], centerToFirstElement=undefined)
{
this.type = type;
this.wall = wall;
this.centerToFirstElement = centerToFirstElement;
}
function createDefaultFortressTypes()
{
let defaultFortresses = {};
/**
* Define some basic default fortress types.
*/
let addFortress = (type, walls) => defaultFortresses[type] = { "wall": walls.concat(walls, walls, walls) };
addFortress("tiny", ["gate", "tower", "short", "cornerIn", "short", "tower"]);
addFortress("small", ["gate", "tower", "medium", "cornerIn", "medium", "tower"]);
addFortress("medium", ["gate", "tower", "long", "cornerIn", "long", "tower"]);
addFortress("normal", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "medium", "cornerIn", "medium", "tower"]);
addFortress("large", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]);
addFortress("veryLarge", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "long", "cornerIn", "long", "cornerOut", "medium", "cornerIn", "medium", "tower"]);
addFortress("giant", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]);
/**
* Define some fortresses based on those above, but designed for use
* with the "palisades" wallset.
*/
for (let fortressType in defaultFortresses)
{
const fillTowersBetween = ["short", "medium", "long", "start", "end", "cornerIn", "cornerOut"];
const newKey = fortressType + "Palisades";
const oldWall = defaultFortresses[fortressType].wall;
defaultFortresses[newKey] = { "wall": [] };
for (let j = 0; j < oldWall.length; ++j)
{
defaultFortresses[newKey].wall.push(oldWall[j]);
if (j + 1 < oldWall.length &&
fillTowersBetween.indexOf(oldWall[j]) != -1 &&
fillTowersBetween.indexOf(oldWall[j + 1]) != -1)
{
defaultFortresses[newKey].wall.push("tower");
}
}
}
return defaultFortresses;
}
/**
* Define some helper functions
*/
/**
* Get a wall element of a style.
*
* Valid elements:
* long, medium, short, start, end, cornerIn, cornerOut, tower, fort, gate, entry, entryTower, entryFort
*
* Dynamic elements:
* `gap_{x}` returns a non-blocking gap of length `x` meters.
* `turn_{x}` returns a zero-length turn of angle `x` radians.
*
* Any other arbitrary string passed will be attempted to be used as: `structures/{civ}_{arbitrary_string}`.
*
* @param {string} element - What sort of element to fetch.
* @param {string} [style] - The style from which this element should come from.
* @returns {object} The wall element requested. Or a tower element.
*/
function getWallElement(element, style)
{
style = validateStyle(style);
if (g_WallStyles[style][element])
return g_WallStyles[style][element];
// Attempt to derive any unknown elements.
// Defaults to a wall tower piece
const quarterBend = Math.PI / 2;
let wallset = g_WallStyles[style];
let civ = style.split("_")[0];
let ret = wallset.tower ? clone(wallset.tower) : { "angle": 0, "bend": 0, "length": 0, "indent": 0 };
switch (element)
{
case "cornerIn":
if (wallset.curves)
for (let curve of wallset.curves)
if (curve.bend == quarterBend)
ret = curve;
if (ret.bend != quarterBend)
{
ret.angle += Math.PI / 4;
ret.indent = ret.length / 4;
ret.length = 0;
ret.bend = Math.PI / 2;
}
break;
case "cornerOut":
if (wallset.curves)
for (let curve of wallset.curves)
if (curve.bend == quarterBend)
{
ret = clone(curve);
ret.angle += Math.PI / 2;
ret.indent -= ret.indent * 2;
}
if (ret.bend != quarterBend)
{
ret.angle -= Math.PI / 4;
ret.indent = -ret.length / 4;
ret.length = 0;
}
ret.bend = -Math.PI / 2;
break;
case "entry":
ret.templateName = undefined;
ret.length = wallset.gate.length;
break;
case "entryTower":
ret.templateName = g_CivData[civ] ? "structures/" + civ + "_defense_tower" : "other/palisades_rocks_watchtower";
ret.indent = ret.length * -3;
ret.length = wallset.gate.length;
break;
case "entryFort":
ret = clone(wallset.fort);
ret.angle -= Math.PI;
ret.length *= 1.5;
ret.indent = ret.length;
break;
case "start":
if (wallset.end)
{
ret = clone(wallset.end);
ret.angle += Math.PI;
}
break;
case "end":
if (wallset.end)
ret = wallset.end;
break;
default:
if (element.startsWith("gap_"))
{
ret.templateName = undefined;
ret.angle = 0;
ret.length = +element.slice("gap_".length);
}
else if (element.startsWith("turn_"))
{
ret.templateName = undefined;
ret.bend = +element.slice("turn_".length) * Math.PI;
ret.length = 0;
}
else
{
if (!g_CivData[civ])
civ = Object.keys(g_CivData)[0];
let templateName = "structures/" + civ + "_" + element;
if (Engine.TemplateExists(templateName))
{
ret.indent = ret.length * (element == "outpost" || element.endsWith("_tower") ? -3 : 3.5);
ret.templateName = templateName;
ret.length = 0;
}
else
warn("Unrecognised wall element: '" + element + "' (" + style + "). Defaulting to " + (wallset.tower ? "'tower'." : "a blank element."));
}
}
// Cache to save having to calculate this element again.
g_WallStyles[style][element] = deepfreeze(ret);
return ret;
}
/**
* Prepare a wall element for inclusion in a style.
*
* @param {string} path - The template path to read values from
*/
function readyWallElement(path, civCode)
{
path = path.replace(/\{civ\}/g, civCode);
let template = GetTemplateDataHelper(Engine.GetTemplate(path), null, null, {}, g_DamageTypes, {});
let length = template.wallPiece ? template.wallPiece.length : template.obstruction.shape.width;
return deepfreeze({
"templateName": path,
"angle": template.wallPiece ? template.wallPiece.angle : Math.PI,
"length": length / TERRAIN_TILE_SIZE,
"indent": template.wallPiece ? template.wallPiece.indent / TERRAIN_TILE_SIZE : 0,
"bend": template.wallPiece ? template.wallPiece.bend : 0
});
}
/**
* Returns a list of objects containing all information to place all the wall elements entities with placeObject (but the player ID)
* Placing the first wall element at startX/startY placed with an angle given by orientation
* An alignment can be used to get the "center" of a "wall" (more likely used for fortresses) with getCenterToFirstElement
*
* @param {Vector2D} position
* @param {array} [wall]
* @param {string} [style]
* @param {number} [orientation]
* @returns {array}
*/
function getWallAlignment(position, wall = [], style = "athen_stone", orientation = 0)
{
style = validateStyle(style);
let alignment = [];
let wallPosition = position.clone();
for (let i = 0; i < wall.length; ++i)
{
let element = getWallElement(wall[i], style);
if (!element && i == 0)
{
warn("Not a valid wall element: style = " + style + ", wall[" + i + "] = " + wall[i] + "; " + uneval(element));
continue;
}
// Add wall elements entity placement arguments to the alignment
alignment.push({
"position": Vector2D.sub(wallPosition, new Vector2D(element.indent, 0).rotate(-orientation)),
"templateName": element.templateName,
"angle": orientation + element.angle
});
// Preset vars for the next wall element
if (i + 1 < wall.length)
{
orientation += element.bend;
let nextElement = getWallElement(wall[i + 1], style);
if (!nextElement)
{
warn("Not a valid wall element: style = " + style + ", wall[" + (i + 1) + "] = " + wall[i + 1] + "; " + uneval(nextElement));
continue;
}
let distance = (element.length + nextElement.length) / 2 - g_WallStyles[style].overlap;
// Corrections for elements with indent AND bending
let indent = element.indent;
let bend = element.bend;
if (bend != 0 && indent != 0)
{
// Indent correction to adjust distance
distance += indent * Math.sin(bend);
// Indent correction to normalize indentation
wallPosition.add(new Vector2D(indent).rotate(-orientation));
}
// Set the next coordinates of the next element in the wall without indentation adjustment
wallPosition.add(new Vector2D(distance, 0).rotate(-orientation).perpendicular());
}
}
return alignment;
}
/**
* Center calculation works like getting the center of mass assuming all wall elements have the same "weight"
*
* Used to get centerToFirstElement of fortresses by default
*
* @param {number} alignment
* @returns {object} Vector from the center of the set of aligned wallpieces to the first wall element.
*/
function getCenterToFirstElement(alignment)
{
return alignment.reduce((result, align) => result.sub(Vector2D.div(align.position, alignment.length)), new Vector2D(0, 0));
}
/**
* Does not support bending wall elements like corners.
*
* @param {string} style
* @param {array} wall
* @returns {number} The sum length (in terrain cells, not meters) of the provided wall.
*/
function getWallLength(style, wall)
{
style = validateStyle(style);
let length = 0;
let overlap = g_WallStyles[style].overlap;
for (let element of wall)
length += getWallElement(element, style).length - overlap;
return length;
}
/**
* Makes sure the style exists and, if not, provides a fallback.
*
* @param {string} style
* @param {number} [playerId]
* @returns {string} Valid style.
*/
function validateStyle(style, playerId = 0)
{
if (!style || !g_WallStyles[style])
{
if (playerId == 0)
return Object.keys(g_WallStyles)[0];
style = getCivCode(playerId) + "_stone";
return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style;
}
return style;
}
/**
* Define the different wall placer functions
*/
/**
* Places an abitrary wall beginning at the location comprised of the array of elements provided.
*
* @param {Vector2D} position
* @param {array} [wall] - Array of wall element types. Example: ["start", "long", "tower", "long", "end"]
* @param {string} [style] - Wall style string.
* @param {number} [playerId] - Identifier of the player for whom the wall will be placed.
* @param {number} [orientation] - Angle at which the first wall element is placed.
* 0 means "outside" or "front" of the wall is right (positive X) like placeObject
* It will then be build towards top/positive Y (if no bending wall elements like corners are used)
* Raising orientation means the wall is rotated counter-clockwise like placeObject
*/
function placeWall(position, wall = [], style, playerId = 0, orientation = 0, constraints = undefined)
{
style = validateStyle(style, playerId);
let entities = [];
let constraint = new StaticConstraint(constraints);
for (let align of getWallAlignment(position, wall, style, orientation))
if (align.templateName && g_Map.inMapBounds(align.position) && constraint.allows(align.position.clone().floor()))
entities.push(g_Map.placeEntityPassable(align.templateName, playerId, align.position, align.angle));
return entities;
}
/**
* Places an abitrarily designed "fortress" (closed loop of wall elements)
* centered around a given point.
*
* The fortress wall should always start with the main entrance (like
* "entry" or "gate") to get the orientation correct.
*
* @param {Vector2D} centerPosition
* @param {object} [fortress] - If not provided, defaults to the predefined "medium" fortress type.
* @param {string} [style] - Wall style string.
* @param {number} [playerId] - Identifier of the player for whom the wall will be placed.
* @param {number} [orientation] - Angle the first wall element (should be a gate or entrance) is placed. Default is 0
*/
function placeCustomFortress(centerPosition, fortress, style, playerId = 0, orientation = 0, constraints = undefined)
{
fortress = fortress || g_FortressTypes.medium;
style = validateStyle(style, playerId);
// Calculate center if fortress.centerToFirstElement is undefined (default)
let centerToFirstElement = fortress.centerToFirstElement;
if (centerToFirstElement === undefined)
centerToFirstElement = getCenterToFirstElement(getWallAlignment(new Vector2D(0, 0), fortress.wall, style));
// Placing the fortress wall
let position = Vector2D.sum([
centerPosition,
new Vector2D(centerToFirstElement.x, 0).rotate(-orientation),
new Vector2D(centerToFirstElement.y, 0).perpendicular().rotate(-orientation)
]);
return placeWall(position, fortress.wall, style, playerId, orientation, constraints);
}
/**
* Places a predefined fortress centered around the provided point.
*
* @see Fortress
*
* @param {string} [type] - Predefined fortress type, as used as a key in g_FortressTypes.
*/
function placeFortress(centerPosition, type = "medium", style, playerId = 0, orientation = 0, constraints = undefined)
{
return placeCustomFortress(centerPosition, g_FortressTypes[type], style, playerId, orientation, constraints);
}
/**
* Places a straight wall from a given point to another, using the provided
* wall parts repeatedly.
*
* Note: Any "bending" wall pieces passed will be complained about.
*
* @param {Vector2D} startPosition - Approximate start point of the wall.
* @param {Vector2D} targetPosition - Approximate end point of the wall.
* @param {array} [wallPart=["tower", "long"]]
* @param {number} [playerId]
* @param {boolean} [endWithFirst] - If true, the first wall element will also be the last.
*/
function placeLinearWall(startPosition, targetPosition, wallPart = undefined, style, playerId = 0, endWithFirst = true, constraints = undefined)
{
wallPart = wallPart || ["tower", "long"];
style = validateStyle(style, playerId);
// Check arguments
for (let element of wallPart)
if (getWallElement(element, style).bend != 0)
warn("placeLinearWall : Bending is not supported by this function, but the following bending wall element was used: " + element);
// Setup number of wall parts
let totalLength = startPosition.distanceTo(targetPosition);
let wallPartLength = getWallLength(style, wallPart);
let numParts = Math.ceil(totalLength / wallPartLength);
if (endWithFirst)
numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength);
// Setup scale factor
let scaleFactor = totalLength / (numParts * wallPartLength);
if (endWithFirst)
scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length);
// Setup angle
let wallAngle = getAngle(startPosition.x, startPosition.y, targetPosition.x, targetPosition.y);
let placeAngle = wallAngle - Math.PI / 2;
// Place wall entities
let entities = [];
let position = startPosition.clone();
let overlap = g_WallStyles[style].overlap;
let constraint = new StaticConstraint(constraints);
for (let partIndex = 0; partIndex < numParts; ++partIndex)
for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex)
{
let wallEle = getWallElement(wallPart[elementIndex], style);
let wallLength = (wallEle.length - overlap) / 2;
let dist = new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle);
// Length correction
position.add(dist);
// Indent correction
let place = Vector2D.add(position, new Vector2D(0, wallEle.indent).rotate(-wallAngle));
if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor()))
entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle));
position.add(dist);
}
if (endWithFirst)
{
let wallEle = getWallElement(wallPart[0], style);
let wallLength = (wallEle.length - overlap) / 2;
position.add(new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle));
if (wallEle.templateName && g_Map.inMapBounds(position) && constraint.allows(position.clone().floor()))
entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, position, placeAngle + wallEle.angle));
}
return entities;
}
/**
* Places a (semi-)circular wall of repeated wall elements around a central
* point at a given radius.
*
* The wall does not have to be closed, and can be left open in the form
* of an arc if maxAngle < 2 * Pi. In this case, the orientation determines
* where this open part faces, with 0 meaning "right" like an unrotated
* building's drop-point.
*
* Note: Any "bending" wall pieces passed will be complained about.
*
* @param {Vector2D} center - Center of the circle or arc.
* @param (number} radius - Approximate radius of the circle. (Given the maxBendOff argument)
* @param {array} [wallPart]
* @param {string} [style]
* @param {number} [playerId]
* @param {number} [orientation] - Angle at which the first wall element is placed.
* @param {number} [maxAngle] - How far the wall should circumscribe the center. Default is Pi * 2 (for a full circle).
* @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. For full circles, the default is false. For arcs, true.
* @param {number} [maxBendOff] Optional. How irregular the circle should be. 0 means regular circle, PI/2 means very irregular. Default is 0 (regular circle)
*/
function placeCircularWall(center, radius, wallPart, style, playerId = 0, orientation = 0, maxAngle = 2 * Math.PI, endWithFirst, maxBendOff = 0, constraints = undefined)
{
wallPart = wallPart || ["tower", "long"];
style = validateStyle(style, playerId);
if (endWithFirst === undefined)
endWithFirst = maxAngle < Math.PI * 2 - 0.001; // Can this be done better?
// Check arguments
if (maxBendOff > Math.PI / 2 || maxBendOff < 0)
warn("placeCircularWall : maxBendOff should satisfy 0 < maxBendOff < PI/2 (~1.5rad) but it is: " + maxBendOff);
for (let element of wallPart)
if (getWallElement(element, style).bend != 0)
warn("placeCircularWall : Bending is not supported by this function, but the following bending wall element was used: " + element);
// Setup number of wall parts
let totalLength = maxAngle * radius;
let wallPartLength = getWallLength(style, wallPart);
let numParts = Math.ceil(totalLength / wallPartLength);
if (endWithFirst)
numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength);
// Setup scale factor
let scaleFactor = totalLength / (numParts * wallPartLength);
if (endWithFirst)
scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length);
// Place wall entities
let entities = [];
let constraint = new StaticConstraint(constraints);
let actualAngle = orientation;
let position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle));
let overlap = g_WallStyles[style].overlap;
for (let partIndex = 0; partIndex < numParts; ++partIndex)
for (let wallEle of wallPart)
{
wallEle = getWallElement(wallEle, style);
// Width correction
let addAngle = scaleFactor * (wallEle.length - overlap) / radius;
let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle));
let place = Vector2D.average([position, target]);
let placeAngle = actualAngle + addAngle / 2;
// Indent correction
place.sub(new Vector2D(wallEle.indent, 0).rotate(-placeAngle));
// Placement
if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor()))
entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle));
// Prepare for the next wall element
actualAngle += addAngle;
position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle));
}
if (endWithFirst)
{
let wallEle = getWallElement(wallPart[0], style);
let addAngle = scaleFactor * wallEle.length / radius;
- let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle))
+ let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle));
let place = Vector2D.average([position, target]);
let placeAngle = actualAngle + addAngle / 2;
if (g_Map.inMapBounds(place) && constraint.allows(place.clone().floor()))
entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle));
}
return entities;
}
/**
* Places a polygonal wall of repeated wall elements around a central
* point at a given radius.
*
* Note: Any "bending" wall pieces passed will be ignored.
*
* @param {Vector2D} centerPosition
* @param {number} radius
* @param {array} [wallPart]
* @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners.
* @param {string} [style]
* @param {number} [playerId]
* @param {number} [orientation] - Direction the first wall piece or opening in the wall faces.
* @param {number} [numCorners] - How many corners the polygon will have.
* @param {boolean} [skipFirstWall] - If the first linear wall part will be left opened as entrance.
*/
function placePolygonalWall(centerPosition, radius, wallPart, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners = 8, skipFirstWall = true, constraints = undefined)
{
wallPart = wallPart || ["long", "tower"];
style = validateStyle(style, playerId);
let entities = [];
let constraint = new StaticConstraint(constraints);
let angleAdd = Math.PI * 2 / numCorners;
let angleStart = orientation - angleAdd / 2;
let corners = new Array(numCorners).fill(0).map((zero, i) =>
Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleStart - i * angleAdd)));
for (let i = 0; i < numCorners; ++i)
{
let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y);
if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor()))
entities.push(
g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner));
if (!skipFirstWall || i != 0)
{
let cornerLength = getWallElement(cornerWallElement, style).length / 2;
let cornerAngle = angleToCorner + angleAdd / 2;
let targetCorner = (i + 1) % numCorners;
let cornerPosition = new Vector2D(cornerLength, 0).rotate(-cornerAngle).perpendicular();
entities = entities.concat(
placeLinearWall(
// Adjustment to the corner element width (approximately)
Vector2D.sub(corners[i], cornerPosition),
Vector2D.add(corners[targetCorner], cornerPosition),
wallPart,
style,
playerId,
undefined,
constraints));
}
}
return entities;
}
/**
* Places an irregular polygonal wall consisting of parts semi-randomly
* chosen from a provided assortment, built around a central point at a
* given radius.
*
* Note: Any "bending" wall pieces passed will be ... I'm not sure. TODO: test what happens!
*
* Note: The wallPartsAssortment is last because it's the hardest to set.
*
* @param {Vector2D} centerPosition
* @param {number} radius
* @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners.
* @param {string} [style]
* @param {number} [playerId]
* @param {number} [orientation] - Direction the first wallpiece or opening in the wall faces.
* @param {number} [numCorners] - How many corners the polygon will have.
* @param {number} [irregularity] - How irregular the polygon will be. 0 = regular, 1 = VERY irregular.
* @param {boolean} [skipFirstWall] - If true, the first linear wall part will be left open as an entrance.
* @param {array} [wallPartsAssortment] - An array of wall part arrays to choose from for each linear wall connecting the corners.
*/
function placeIrregularPolygonalWall(centerPosition, radius, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners, irregularity = 0.5, skipFirstWall = false, wallPartsAssortment = undefined, constraints = undefined)
{
style = validateStyle(style, playerId);
numCorners = numCorners || randIntInclusive(5, 7);
// Generating a generic wall part assortment with each wall part including 1 gate lengthened by walls and towers
// NOTE: It might be a good idea to write an own function for that...
let defaultWallPartsAssortment = [["short"], ["medium"], ["long"], ["gate", "tower", "short"]];
let centeredWallPart = ["gate"];
let extendingWallPartAssortment = [["tower", "long"], ["tower", "medium"]];
defaultWallPartsAssortment.push(centeredWallPart);
for (let assortment of extendingWallPartAssortment)
{
let wallPart = centeredWallPart;
for (let j = 0; j < radius; ++j)
{
if (j % 2 == 0)
wallPart = wallPart.concat(assortment);
else
{
assortment.reverse();
wallPart = assortment.concat(wallPart);
assortment.reverse();
}
defaultWallPartsAssortment.push(wallPart);
}
}
// Setup optional arguments to the default
wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment;
// Setup angles
let angleToCover = Math.PI * 2;
let angleAddList = [];
for (let i = 0; i < numCorners; ++i)
{
// Randomize covered angles. Variety scales down with raising angle though...
angleAddList.push(angleToCover / (numCorners - i) * (1 + randFloat(-irregularity, irregularity)));
angleToCover -= angleAddList[angleAddList.length - 1];
}
// Setup corners
let corners = [];
let angleActual = orientation - angleAddList[0] / 2;
for (let i = 0; i < numCorners; ++i)
{
corners.push(Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleActual)));
if (i < numCorners - 1)
angleActual += angleAddList[i + 1];
}
// Setup best wall parts for the different walls (a bit confusing naming...)
let wallPartLengths = [];
let maxWallPartLength = 0;
for (let wallPart of wallPartsAssortment)
{
let length = getWallLength(style, wallPart);
wallPartLengths.push(length);
if (length > maxWallPartLength)
maxWallPartLength = length;
}
let wallPartList = []; // This is the list of the wall parts to use for the walls between the corners, not to confuse with wallPartsAssortment!
for (let i = 0; i < numCorners; ++i)
{
let bestWallPart = []; // This is a simple wall part not a wallPartsAssortment!
let bestWallLength = Infinity;
let targetCorner = (i + 1) % numCorners;
// NOTE: This is not quite the length the wall will be in the end. Has to be tweaked...
let wallLength = corners[i].distanceTo(corners[targetCorner]);
let numWallParts = Math.ceil(wallLength / maxWallPartLength);
for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex)
{
let linearWallLength = numWallParts * wallPartLengths[partIndex];
if (linearWallLength < bestWallLength && linearWallLength > wallLength)
{
bestWallPart = wallPartsAssortment[partIndex];
bestWallLength = linearWallLength;
}
}
wallPartList.push(bestWallPart);
}
// Place Corners and walls
let entities = [];
let constraint = new StaticConstraint(constraints);
for (let i = 0; i < numCorners; ++i)
{
let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y);
if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor()))
entities.push(
g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner));
if (!skipFirstWall || i != 0)
{
let cornerLength = getWallElement(cornerWallElement, style).length / 2;
let targetCorner = (i + 1) % numCorners;
let startAngle = angleToCorner + angleAddList[i] / 2;
let targetAngle = angleToCorner + angleAddList[targetCorner] / 2;
entities = entities.concat(
placeLinearWall(
// Adjustment to the corner element width (approximately)
Vector2D.sub(corners[i], new Vector2D(cornerLength, 0).perpendicular().rotate(-startAngle)),
Vector2D.add(corners[targetCorner], new Vector2D(cornerLength, 0).rotate(-targetAngle - Math.PI / 2)),
wallPartList[i],
style,
playerId,
false,
constraints));
}
}
return entities;
}
/**
* Places a generic fortress with towers at the edges connected with long
* walls and gates, positioned around a central point at a given radius.
*
* The difference between this and the other two Fortress placement functions
* is that those place a predefined fortress, regardless of terrain type.
* This function attempts to intelligently place a wall circuit around
* the central point taking into account terrain and other obstacles.
*
* This is the default Iberian civ bonus starting wall.
*
* @param {Vector2D} center - The approximate center coordinates of the fortress
* @param {number} [radius] - The approximate radius of the wall to be placed.
* @param {number} [playerId]
* @param {string} [style]
* @param {number} [irregularity] - 0 = circle, 1 = very spiky
* @param {number} [gateOccurence] - Integer number, every n-th walls will be a gate instead.
* @param {number} [maxTries] - How often the function tries to find a better fitting shape.
*/
function placeGenericFortress(center, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTries = 100, constraints = undefined)
{
style = validateStyle(style, playerId);
// Setup some vars
let startAngle = randomAngle();
let actualOff = new Vector2D(radius, 0).rotate(-startAngle);
let actualAngle = startAngle;
let pointDistance = getWallLength(style, ["long", "tower"]);
// Searching for a well fitting point derivation
let tries = 0;
let bestPointDerivation;
let minOverlap = 1000;
let overlap;
while (tries < maxTries && minOverlap > g_WallStyles[style].overlap)
{
let pointDerivation = [];
let distanceToTarget = 1000;
while (true)
{
let indent = randFloat(-irregularity * pointDistance, irregularity * pointDistance);
let tmp = new Vector2D(radius + indent, 0).rotate(-actualAngle - pointDistance / radius);
let tmpAngle = getAngle(actualOff.x, actualOff.y, tmp.x, tmp.y);
actualOff.add(new Vector2D(pointDistance, 0).rotate(-tmpAngle));
actualAngle = getAngle(0, 0, actualOff.x, actualOff.y);
pointDerivation.push(actualOff.clone());
distanceToTarget = pointDerivation[0].distanceTo(actualOff);
let numPoints = pointDerivation.length;
if (numPoints > 3 && distanceToTarget < pointDistance) // Could be done better...
{
overlap = pointDistance - pointDerivation[numPoints - 1].distanceTo(pointDerivation[0]);
if (overlap < minOverlap)
{
minOverlap = overlap;
bestPointDerivation = pointDerivation;
}
break;
}
}
++tries;
}
log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries");
// Place wall
let entities = [];
let constraint = new StaticConstraint(constraints);
for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex)
{
let start = Vector2D.add(center, bestPointDerivation[pointIndex]);
let target = Vector2D.add(center, bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length]);
let angle = getAngle(start.x, start.y, target.x, target.y);
let element = (pointIndex + 1) % gateOccurence == 0 ? "gate" : "long";
element = getWallElement(element, style);
if (element.templateName)
{
let pos = Vector2D.add(start, new Vector2D(start.distanceTo(target) / 2, 0).rotate(-angle));
if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor()))
entities.push(g_Map.placeEntityPassable(element.templateName, playerId, pos, angle - Math.PI / 2 + element.angle));
}
// Place tower
start = Vector2D.add(center, bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length]);
angle = getAngle(start.x, start.y, target.x, target.y);
let tower = getWallElement("tower", style);
let pos = Vector2D.add(center, bestPointDerivation[pointIndex]);
if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor()))
entities.push(
g_Map.placeEntityPassable(tower.templateName, playerId, pos, angle - Math.PI / 2 + tower.angle));
}
return entities;
}
Index: ps/trunk/binaries/data/mods/public/maps/random/snowflake_searocks.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/snowflake_searocks.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/maps/random/snowflake_searocks.js (revision 22419)
@@ -1,446 +1,446 @@
Engine.LoadLibrary("rmgen");
Engine.LoadLibrary("rmgen-common");
Engine.LoadLibrary("rmbiome");
TILE_CENTERED_HEIGHT_MAP = true;
setSelectedBiome();
const tMainTerrain = g_Terrains.mainTerrain;
const tForestFloor1 = g_Terrains.forestFloor1;
const tForestFloor2 = g_Terrains.forestFloor2;
const tCliff = g_Terrains.cliff;
const tTier1Terrain = g_Terrains.tier1Terrain;
const tTier2Terrain = g_Terrains.tier2Terrain;
const tTier3Terrain = g_Terrains.tier3Terrain;
const tHill = g_Terrains.mainTerrain;
const tRoad = g_Terrains.road;
const tRoadWild = g_Terrains.roadWild;
const tTier4Terrain = g_Terrains.tier4Terrain;
const tWater = g_Terrains.water;
const oTree1 = g_Gaia.tree1;
const oTree2 = g_Gaia.tree2;
const oTree3 = g_Gaia.tree3;
const oTree4 = g_Gaia.tree4;
const oTree5 = g_Gaia.tree5;
const oFruitBush = g_Gaia.fruitBush;
const oMainHuntableAnimal = g_Gaia.mainHuntableAnimal;
const oSecondaryHuntableAnimal = g_Gaia.secondaryHuntableAnimal;
const oStoneLarge = g_Gaia.stoneLarge;
const oStoneSmall = g_Gaia.stoneSmall;
const oMetalLarge = g_Gaia.metalLarge;
const aGrass = g_Decoratives.grass;
const aGrassShort = g_Decoratives.grassShort;
const aRockLarge = g_Decoratives.rockLarge;
const aRockMedium = g_Decoratives.rockMedium;
const aBushMedium = g_Decoratives.bushMedium;
const aBushSmall = g_Decoratives.bushSmall;
const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2];
const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1];
const heightIsland = 20;
const heightSeaGround = -5;
var g_Map = new RandomMap(heightSeaGround, tWater);
const numPlayers = getNumPlayers();
const mapSize = g_Map.getSize();
const mapCenter = g_Map.getCenter();
var clPlayer = g_Map.createTileClass();
var clForest = g_Map.createTileClass();
var clDirt = g_Map.createTileClass();
var clRock = g_Map.createTileClass();
var clMetal = g_Map.createTileClass();
var clFood = g_Map.createTileClass();
var clBaseResource = g_Map.createTileClass();
var clLand = g_Map.createTileClass();
const playerIslandRadius = scaleByMapSize(15, 30);
const islandBetweenPlayerAndCenterDist = 0.16;
const islandBetweenPlayerAndCenterRadius = 0.81;
const centralIslandRadius = 0.36;
var [playerIDs, playerPosition, playerAngle, startAngle] = playerPlacementCircle(fractionToTiles(0.35));
var numIslands = 0;
var isConnected = [];
var islandPos = [];
function initIsConnected()
{
for (let m = 0; m < numIslands; ++m)
{
isConnected[m] = [];
for (let n = 0; n < numIslands; ++n)
isConnected[m][n] = 0;
}
}
function createIsland(islandID, size, tileClass)
{
createArea(
new ClumpPlacer(size * diskArea(playerIslandRadius), 0.95, 0.6, Infinity, islandPos[islandID]),
[
new TerrainPainter(tHill),
new SmoothElevationPainter(ELEVATION_SET, heightIsland, 2),
new TileClassPainter(tileClass)
]);
}
function createIslandAtRadialLocation(playerID, islandID, playerIDOffset, distFromCenter, islandRadius)
{
let angle = startAngle + (playerID * 2 + playerIDOffset) * Math.PI / numPlayers;
islandPos[islandID] = Vector2D.add(mapCenter, new Vector2D(fractionToTiles(distFromCenter), 0).rotate(-angle)).round();
createIsland(islandID, islandRadius, clLand);
}
function createSnowflakeSearockWithCenter(sizeID)
{
let [tertiaryIslandDist, tertiaryIslandRadius, islandBetweenPlayersDist, islandBetweenPlayersRadius] = islandSizes[sizeID];
let islandID_center = 4 * numPlayers;
numIslands = islandID_center + 1;
initIsConnected();
g_Map.log("Creating central island");
islandPos[islandID_center] = mapCenter;
createIsland(islandID_center, centralIslandRadius, clLand);
for (let playerID = 0; playerID < numPlayers; ++playerID)
{
let playerID_neighbor = playerID + 1 < numPlayers ? playerID + 1 : 0;
let islandID_player = playerID;
let islandID_playerNeighbor = playerID_neighbor;
let islandID_betweenPlayers = playerID + numPlayers;
let islandID_betweenPlayerAndCenter = playerID + 2 * numPlayers;
let islandID_betweenPlayerAndCenterNeighbor = playerID_neighbor + 2 * numPlayers;
let islandID_tertiary = playerID + 3 * numPlayers;
g_Map.log("Creating island between the player and their neighbor");
isConnected[islandID_betweenPlayers][islandID_player] = 1;
isConnected[islandID_betweenPlayers][islandID_playerNeighbor] = 1;
createIslandAtRadialLocation(playerID, islandID_betweenPlayers, 1, islandBetweenPlayersDist, islandBetweenPlayersRadius);
g_Map.log("Creating an island between the player and the center");
isConnected[islandID_betweenPlayerAndCenter][islandID_player] = 1;
isConnected[islandID_betweenPlayerAndCenter][islandID_center] = 1;
isConnected[islandID_betweenPlayerAndCenter][islandID_betweenPlayerAndCenterNeighbor] = 1;
createIslandAtRadialLocation(playerID, islandID_betweenPlayerAndCenter, 0, islandBetweenPlayerAndCenterDist, islandBetweenPlayerAndCenterRadius);
g_Map.log("Creating tertiary island, at the map border");
isConnected[islandID_tertiary][islandID_betweenPlayers] = 1;
createIslandAtRadialLocation(playerID, islandID_tertiary, 1, tertiaryIslandDist, tertiaryIslandRadius);
}
}
/**
* Creates one island in front of every player and connects it with the neighbors.
*/
function createSnowflakeSearockWithoutCenter()
{
numIslands = 2 * numPlayers;
initIsConnected();
for (let playerID = 0; playerID < numPlayers; ++playerID)
{
let playerID_neighbor = playerID + 1 < numPlayers ? playerID + 1 : 0;
let islandID_player = playerID;
let islandID_playerNeighbor = playerID_neighbor;
let islandID_inFrontOfPlayer = playerID + numPlayers;
let islandID_inFrontOfPlayerNeighbor = playerID_neighbor + numPlayers;
isConnected[islandID_player][islandID_playerNeighbor] = 1;
isConnected[islandID_player][islandID_inFrontOfPlayer] = 1;
isConnected[islandID_inFrontOfPlayer][islandID_inFrontOfPlayerNeighbor] = 1;
createIslandAtRadialLocation(playerID, islandID_inFrontOfPlayer, 0, islandBetweenPlayerAndCenterDist, islandBetweenPlayerAndCenterRadius);
}
}
function createSnowflakeSearockTiny()
{
numIslands = numPlayers + 1;
initIsConnected();
let islandID_center = numPlayers;
g_Map.log("Creating central island");
islandPos[islandID_center] = mapCenter;
createIsland(numPlayers, 1, clLand);
for (let playerID = 0; playerID < numPlayers; ++playerID)
{
let islandID_player = playerID;
isConnected[islandID_player][islandID_center] = 1;
}
}
const islandSizes = {
"medium": [0.41, 0.49, 0.26, 1],
"large1": [0.41, 0.49, 0.24, 1],
"large2": [0.41, 0.36, 0.28, 0.81]
};
if (mapSize <= 128)
{
createSnowflakeSearockTiny();
}
else if (mapSize <= 192)
{
createSnowflakeSearockWithoutCenter();
}
else if (mapSize <= 256)
{
if (numPlayers < 6)
createSnowflakeSearockWithCenter("medium");
else
createSnowflakeSearockWithoutCenter();
}
else if (mapSize <= 320)
{
if (numPlayers < 8)
createSnowflakeSearockWithCenter("medium");
else
createSnowflakeSearockWithoutCenter();
}
else
createSnowflakeSearockWithCenter(numPlayers < 6 ? "large1" : "large2");
g_Map.log("Creating player islands");
for (let i = 0; i < numPlayers; ++i)
{
islandPos[i] = playerPosition[i];
createIsland(i, 1, isNomad() ? clLand : clPlayer);
}
g_Map.log("Creating connectors");
for (let i = 0; i < numIslands; ++i)
for (let j = 0; j < numIslands; ++j)
if (isConnected[i][j])
createArea(
new PathPlacer(islandPos[i], islandPos[j], 11, 0, 1, 0, 0, Infinity),
[
new SmoothElevationPainter(ELEVATION_SET, heightIsland, 2),
new TerrainPainter(tHill),
new TileClassPainter(clLand)
]);
-g_Map.log("Painting cliffs")
+g_Map.log("Painting cliffs");
createArea(
new MapBoundsPlacer(),
new TerrainPainter(tCliff),
new SlopeConstraint(2, Infinity));
Engine.SetProgress(30);
placePlayerBases({
"PlayerPlacement": [playerIDs, playerPosition],
// PlayerTileClass already marked above
"BaseResourceClass": clBaseResource,
"baseResourceConstraint": stayClasses(clPlayer, 4),
"Walls": "towers",
"CityPatch": {
"outerTerrain": tRoadWild,
"innerTerrain": tRoad
},
"Chicken": {
},
"Berries": {
"template": oFruitBush,
"distance": playerIslandRadius - 4
},
"Mines": {
"types": [
{ "template": oMetalLarge },
{ "template": oStoneLarge }
],
"distance": playerIslandRadius - 4
},
"Trees": {
"template": oTree1,
"count": scaleByMapSize(10, 50),
"minDist": 11,
"maxDist": 11
},
"Decoratives": {
"template": aGrassShort
}
});
Engine.SetProgress(40);
g_Map.log("Creating forests");
var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(1));
var types = [
[[tForestFloor2, tMainTerrain, pForest1], [tForestFloor2, pForest1]],
[[tForestFloor1, tMainTerrain, pForest2], [tForestFloor1, pForest2]]
];
var size = forestTrees / (scaleByMapSize(2, 8) * numPlayers) * (currentBiome() == "generic/savanna" ? 2 : 1);
var num = Math.floor(size / types.length);
for (let type of types)
createAreas(
new ClumpPlacer(forestTrees / num, 0.1, 0.1, Infinity),
[
new LayeredPainter(type, [2]),
new TileClassPainter(clForest)
],
[avoidClasses(clPlayer, 6, clForest, 10), stayClasses(clLand, 4)],
num);
Engine.SetProgress(55);
g_Map.log("Creating stone mines");
var group = new SimpleGroup([new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clRock, 10), stayClasses(clLand, 5)],
5*scaleByMapSize(4,16), 100
);
g_Map.log("Creating small stone quarries");
group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3)], true, clRock);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clRock, 10), stayClasses(clLand, 5)],
5*scaleByMapSize(4,16), 100
);
g_Map.log("Creating metal mines");
group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 1, clPlayer, 10, clMetal, 10, clRock, 5), stayClasses(clLand, 5)],
5*scaleByMapSize(4,16), 100
);
Engine.SetProgress(65);
g_Map.log("Creating dirt patches");
for (let size of [scaleByMapSize(3, 48), scaleByMapSize(5, 84), scaleByMapSize(8, 128)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
[
new LayeredPainter([[tMainTerrain, tTier1Terrain],[tTier1Terrain, tTier2Terrain], [tTier2Terrain, tTier3Terrain]], [1, 1]),
new TileClassPainter(clDirt)
],
[avoidClasses(clForest, 0, clDirt, 5, clPlayer, 12), stayClasses(clLand, 5)],
scaleByMapSize(15, 45));
g_Map.log("Creating grass patches");
for (let size of [scaleByMapSize(2, 32), scaleByMapSize(3, 48), scaleByMapSize(5, 80)])
createAreas(
new ClumpPlacer(size, 0.3, 0.06, 0.5),
new TerrainPainter(tTier4Terrain),
[avoidClasses(clForest, 0, clDirt, 5, clPlayer, 12), stayClasses(clLand, 5)],
scaleByMapSize(15, 45));
g_Map.log("Creating small decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockMedium, 1,3, 0,1)],
true
);
createObjectGroupsDeprecated(
group, 0,
[avoidClasses(clForest, 0, clPlayer, 0), stayClasses(clLand, 4)],
scaleByMapSize(16, 262), 50
);
g_Map.log("Creating large decorative rocks");
group = new SimpleGroup(
[new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)],
true
);
createObjectGroupsDeprecated(
group, 0,
[avoidClasses(clForest, 0, clPlayer, 0), stayClasses(clLand, 4)],
scaleByMapSize(8, 131), 50
);
Engine.SetProgress(70);
g_Map.log("Creating deer");
group = new SimpleGroup(
[new SimpleObject(oMainHuntableAnimal, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 0, clPlayer, 10, clFood, 20), stayClasses(clLand, 4)],
3 * numPlayers, 50
);
Engine.SetProgress(75);
g_Map.log("Creating sheep");
group = new SimpleGroup(
[new SimpleObject(oSecondaryHuntableAnimal, 2,3, 0,2)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 0, clPlayer, 10, clFood, 20), stayClasses(clLand, 4)],
3 * numPlayers, 50
);
g_Map.log("Creating fruits");
group = new SimpleGroup(
[new SimpleObject(oFruitBush, 5,7, 0,4)],
true, clFood
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clForest, 0, clPlayer, 10, clFood, 20), stayClasses(clLand, 4)],
3 * numPlayers, 50
);
Engine.SetProgress(85);
createStragglerTrees(
[oTree1, oTree2, oTree4, oTree3],
[avoidClasses(clForest, 1, clPlayer, 9, clMetal, 6, clRock, 6), stayClasses(clLand, 4)],
clForest,
stragglerTrees);
var planetm = 1;
if (currentBiome() == "generic/tropic")
planetm = 8;
g_Map.log("Creating small grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrassShort, 1,2, 0,1, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clPlayer, 2, clDirt, 0), stayClasses(clLand, 4)],
planetm * scaleByMapSize(13, 200)
);
Engine.SetProgress(90);
g_Map.log("Creating large grass tufts");
group = new SimpleGroup(
[new SimpleObject(aGrass, 2,4, 0,1.8, -Math.PI / 8, Math.PI / 8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -Math.PI / 8, Math.PI / 8)]
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clPlayer, 2, clDirt, 1, clForest, 0), stayClasses(clLand, 4)],
planetm * scaleByMapSize(13, 200)
);
Engine.SetProgress(95);
g_Map.log("Creating bushes");
group = new SimpleGroup(
[new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)]
);
createObjectGroupsDeprecated(group, 0,
[avoidClasses(clPlayer, 1, clDirt, 1), stayClasses(clLand, 4)],
planetm * scaleByMapSize(13, 200), 50
);
placePlayersNomad(
clPlayer,
[
stayClasses(clLand, 8),
avoidClasses(clForest, 1, clMetal, 4, clRock, 4, clFood, 2)
]);
setSkySet(pickRandom(["cirrus", "cumulus", "sunny"]));
setSunRotation(randomAngle());
setSunElevation(Math.PI * randFloat(1/5, 1/3));
g_Map.ExportMap();
Index: ps/trunk/binaries/data/mods/public/simulation/components/Heal.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Heal.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Heal.js (revision 22419)
@@ -1,140 +1,140 @@
function Heal() {}
Heal.prototype.Schema =
"Controls the healing abilities of the unit." +
"" +
"20" +
"" +
"heal_overlay_range.png" +
"heal_overlay_range_mask.png" +
"0.35" +
"" +
"5" +
"2000" +
"Cavalry" +
"Support Infantry" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"";
Heal.prototype.Init = function()
{
};
Heal.prototype.Serialize = null; // we have no dynamic state to save
Heal.prototype.GetTimers = function()
{
return {
"prepare": 1000,
"repeat": this.GetRate()
};
};
Heal.prototype.GetHP = function()
{
return ApplyValueModificationsToEntity("Heal/HP", +this.template.HP, this.entity);
};
Heal.prototype.GetRate = function()
{
return ApplyValueModificationsToEntity("Heal/Rate", +this.template.Rate, this.entity);
};
Heal.prototype.GetRange = function()
{
return {
"min": 0,
"max": ApplyValueModificationsToEntity("Heal/Range", +this.template.Range, this.entity)
};
};
Heal.prototype.GetUnhealableClasses = function()
{
return this.template.UnhealableClasses._string || "";
};
Heal.prototype.GetHealableClasses = function()
{
return this.template.HealableClasses._string || "";
};
Heal.prototype.GetRangeOverlays = function()
{
if (!this.template.RangeOverlay)
return [];
return [{
"radius": this.GetRange().max,
"texture": this.template.RangeOverlay.LineTexture,
"textureMask": this.template.RangeOverlay.LineTextureMask,
"thickness": +this.template.RangeOverlay.LineThickness
- }]
+ }];
};
/**
* Heal the target entity. This should only be called after a successful range
* check, and should only be called after GetTimers().repeat msec has passed
* since the last call to PerformHeal.
*/
Heal.prototype.PerformHeal = function(target)
{
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth)
return;
let targetState = cmpHealth.Increase(this.GetHP());
// Add XP
let cmpLoot = Engine.QueryInterface(target, IID_Loot);
let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
if (targetState !== undefined && cmpLoot && cmpPromotion)
{
// HP healed * XP per HP
cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp());
}
//TODO we need a sound file
// PlaySound("heal_impact", this.entity);
};
Heal.prototype.OnValueModification = function(msg)
{
if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1)
return;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (!cmpUnitAI)
return;
cmpUnitAI.UpdateRangeQueries();
};
Engine.RegisterComponentType(IID_Heal, "Heal", Heal);
Index: ps/trunk/binaries/data/mods/public/simulation/components/SkirmishReplacer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/SkirmishReplacer.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/SkirmishReplacer.js (revision 22419)
@@ -1,96 +1,96 @@
function SkirmishReplacer() {}
SkirmishReplacer.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"";
SkirmishReplacer.prototype.Init = function()
{
};
SkirmishReplacer.prototype.Serialize = null; // We have no dynamic state to save
function getReplacementEntities(civ)
{
return Engine.ReadJSONFile("simulation/data/civs/" + civ + ".json").SkirmishReplacements;
}
SkirmishReplacer.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to == 0)
warn("Skirmish map elements can only be owned by regular players. Please delete entity "+this.entity+" or change the ownership to a non-gaia player.");
};
SkirmishReplacer.prototype.ReplaceEntities = function()
{
var cmpPlayer = QueryOwnerInterface(this.entity);
var civ = cmpPlayer.GetCiv();
var replacementEntities = getReplacementEntities(civ);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity);
- let specialFilters = templateName.substr(0, templateName.lastIndexOf("|") + 1)
+ let specialFilters = templateName.substr(0, templateName.lastIndexOf("|") + 1);
templateName = removeFiltersFromTemplateName(templateName);
if (templateName in replacementEntities)
templateName = replacementEntities[templateName];
else if (this.template && "general" in this.template)
templateName = this.template.general;
else
templateName = "";
if (!templateName || civ == "gaia")
{
Engine.DestroyEntity(this.entity);
return;
}
templateName = specialFilters + templateName.replace(/\{civ\}/g, civ);
var cmpCurPosition = Engine.QueryInterface(this.entity, IID_Position);
var replacement = Engine.AddEntity(templateName);
if (!replacement)
{
Engine.DestroyEntity(this.entity);
return;
}
var cmpReplacementPosition = Engine.QueryInterface(replacement, IID_Position);
var pos = cmpCurPosition.GetPosition2D();
cmpReplacementPosition.JumpTo(pos.x, pos.y);
var rot = cmpCurPosition.GetRotation();
cmpReplacementPosition.SetYRotation(rot.y);
var cmpCurOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpReplacementOwnership = Engine.QueryInterface(replacement, IID_Ownership);
cmpReplacementOwnership.SetOwner(cmpCurOwnership.GetOwner());
Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": replacement });
Engine.DestroyEntity(this.entity);
};
/**
* Replace this entity with a civ-specific entity
* Message is sent right before InitGame() is called, in InitGame.js
* Replacement needs to happen early on real games to not confuse the AI
*/
SkirmishReplacer.prototype.OnSkirmishReplace = function(msg)
{
this.ReplaceEntities();
};
/**
* Replace this entity with a civ-specific entity
* This is needed for Atlas, when the entity isn't replaced before the game starts,
* so it needs to be replaced on the first turn.
*/
SkirmishReplacer.prototype.OnUpdate = function(msg)
{
this.ReplaceEntities();
};
Engine.RegisterComponentType(IID_SkirmishReplacer, "SkirmishReplacer", SkirmishReplacer);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 22419)
@@ -1,5887 +1,5887 @@
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"standground" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"skittish" +
"domestic" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"";
// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackersAlways: anything that hurts us is a viable target,
// possibly overriding user orders!
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondChase: start chasing after the enemy
// respondChaseBeyondVision: start chasing, and don't stop even if it's out
// of this unit's vision range (though still visible to the player)
// respondStandGround: attack enemy but don't move at all
// respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
"violent": {
"targetVisibleEnemies": true,
"targetAttackersAlways": true,
"respondFlee": false,
"respondChase": true,
"respondChaseBeyondVision": true,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"aggressive": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": true,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"defensive": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": true,
"selectable": true
},
"passive": {
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": true,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": true
},
"standground": {
"targetVisibleEnemies": true,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": true,
"respondHoldGround": false,
"selectable": true
},
"none": {
// Only to be used by AI or trigger scripts
"targetVisibleEnemies": false,
"targetAttackersAlways": false,
"respondFlee": false,
"respondChase": false,
"respondChaseBeyondVision": false,
"respondStandGround": false,
"respondHoldGround": false,
"selectable": false
}
};
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
UnitAI.prototype.UnitFsmSpec = {
// Default event handlers:
"MovementUpdate": function(msg) {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"LosHealRangeUpdate": function(msg) {
// ignore newly-seen injured units by default
},
"Attacked": function(msg) {
// ignore attacker
},
"HealthChanged": function(msg) {
// ignore
},
"PackFinished": function(msg) {
// ignore
},
"PickupCanceled": function(msg) {
// ignore
},
"TradingCanceled": function(msg) {
// ignore
},
"GuardedAttacked": function(msg) {
// ignore
},
// Formation handlers:
"FormationLeave": function(msg) {
// ignore when we're not in FORMATIONMEMBER
},
// Called when being told to walk as part of a formation
"Order.FormationWalk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("FORMATIONMEMBER.WALKING");
},
// Special orders:
// (these will be overridden by various states)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building if necessary.
let range = 4;
if (this.CheckTargetRangeExplicit(msg.data.target, range, -1))
this.FinishOrder();
else
{
this.order.data.min = range;
this.SetNextState("INDIVIDUAL.WALKING");
}
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.Stop": function(msg) {
// We have no control over non-domestic animals.
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
// Stop moving immediately.
this.StopMoving();
this.FinishOrder();
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
"Order.Walk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.WalkAndFight": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING"); // WalkAndFight not applicable for animals
else
this.SetNextState("INDIVIDUAL.WALKINGANDFIGHTING");
},
"Order.WalkToTarget": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units:
// 1. If packed, we can move.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
if (this.CheckTargetRangeExplicit(this.order.data.target, 0, 0))
{
// We are already at the target, or can't move at all
this.FinishOrder();
return true;
}
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.PickupUnit": function(msg) {
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return;
}
if (this.CheckTargetRangeExplicit(this.order.data.target, 0, 0))
{
this.FinishOrder();
return;
}
// Check if we need to move TODO implement a better way to know if we are on the shoreline
var needToMove = true;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.lastShorelinePosition && cmpPosition && (this.lastShorelinePosition.x == cmpPosition.GetPosition().x)
&& (this.lastShorelinePosition.z == cmpPosition.GetPosition().z))
{
// we were already on the shoreline, and have not moved since
if (DistanceBetweenEntities(this.entity, this.order.data.target) < 50)
needToMove = false;
}
if (needToMove)
this.SetNextState("INDIVIDUAL.PICKUP.APPROACHING");
else
this.SetNextState("INDIVIDUAL.PICKUP.LOADING");
},
"Order.Guard": function(msg) {
if (!this.AddGuard(this.order.data.target))
{
this.FinishOrder();
return;
}
if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
else
this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
},
"Order.Flee": function(msg) {
if (this.IsAnimal())
this.SetNextState("ANIMAL.FLEEING");
else
this.SetNextState("INDIVIDUAL.FLEEING");
},
"Order.Attack": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Work out how to attack the given target
var type = this.GetBestAttackAgainst(this.order.data.target, this.order.data.allowCapture);
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.order.data.attackType = type;
// If we are already at the target, try attacking it from here
if (this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
// For packable units within attack range:
// 1. If unpacked, we can attack the target.
// 2. If packed, we first need to unpack, then follow case 1.
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return;
}
// If we can't reach the target, but are standing ground, then abandon this attack order.
// Unless we're hunting, that's a special case where we should continue attacking our target.
if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting || this.IsTurret())
{
this.FinishOrder();
return;
}
// For packable units out of attack range:
// 1. If packed, we need to move to attack range and then unpack.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
},
"Order.Patrol": function(msg) {
if (this.IsAnimal() || this.IsTurret())
{
this.FinishOrder();
return;
}
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("INDIVIDUAL.PATROL");
},
"Order.Heal": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
{
this.FinishOrder();
return;
}
// Check if the target is in range
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this heal order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
},
"Order.Gather": function(msg) {
// If the target is still alive, we need to kill it first
if (this.MustKillGatherTarget(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (!this.GetBestAttackAgainst(this.order.data.target, false))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
this.FinishOrder();
return;
}
// The target was visible when this order was issued,
// but could now be invisible again.
if (!this.CheckTargetVisible(this.order.data.target))
{
if (this.order.data.secondTry === undefined)
{
this.order.data.secondTry = true;
this.PushOrderFront("Walk", this.order.data.lastPos);
}
else
{
// We couldn't move there, or the target moved away
this.FinishOrder();
}
return;
}
this.PushOrderFront("Attack", { "target": this.order.data.target, "force": !!this.order.data.force, "hunting": true, "allowCapture": false });
return;
}
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer))
this.SetNextState("INDIVIDUAL.GATHER.GATHERING");
else
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
},
"Order.GatherNearPosition": function(msg) {
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
},
"Order.ReturnResource": function(msg) {
// Check if the dropsite is already in range
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
Engine.QueryInterface(this.entity, IID_ResourceGatherer).CommitResources(dropsiteTypes);
// Stop showing the carried resource animation.
this.SetDefaultAnimationVariant();
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
},
"Order.Trade": function(msg) {
// We must check if this trader has both markets in case it was a back-to-work order
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || !cmpTrader.HasBothMarkets())
{
this.FinishOrder();
return;
}
// TODO find the nearest way-point from our position, and start with it
this.waypoints = undefined;
this.SetNextState("TRADE.APPROACHINGMARKET");
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.CheckTargetRange(this.order.data.target, IID_Builder))
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
else
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
},
"Order.Garrison": function(msg) {
if (this.IsTurret())
{
this.SetNextState("IDLE");
return;
}
else if (this.IsGarrisoned())
{
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
return;
}
// For packable units:
// 1. If packed, we can move to the garrison target.
// 2. If unpacked, we first need to pack, then follow case 1.
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
},
"Order.Ungarrison": function() {
this.FinishOrder();
this.isGarrisoned = false;
},
"Order.Cheering": function(msg) {
this.SetNextState("INDIVIDUAL.CHEERING");
},
"Order.Pack": function(msg) {
if (this.CanPack())
this.SetNextState("INDIVIDUAL.PACKING");
},
"Order.Unpack": function(msg) {
if (this.CanUnpack())
this.SetNextState("INDIVIDUAL.UNPACKING");
},
"Order.CancelPack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
"Order.CancelUnpack": function(msg) {
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
cmpPack.CancelPack();
this.FinishOrder();
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKING");
},
"Order.WalkAndFight": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("WALKINGANDFIGHTING");
},
"Order.MoveIntoFormation": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("FORMING");
},
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
if (!this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToTarget": function(msg) {
if (!this.CheckTargetRangeExplicit(this.order.data.target, 0, 0))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.WalkToPointRange": function(msg) {
if (!this.CheckPointRangeExplicit(this.order.data.x, this.order.data.z, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.Patrol": function(msg) {
this.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.SetNextState("PATROL");
},
"Order.Guard": function(msg) {
this.CallMemberFunction("Guard", [msg.data.target, false]);
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
"Order.Stop": function(msg) {
if (!this.IsAttackingAsFormation())
this.CallMemberFunction("Stop", [false]);
this.StopMoving();
this.FinishOrder();
},
"Order.Attack": function(msg) {
var target = msg.data.target;
var allowCapture = msg.data.allowCapture;
var cmpTargetUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpTargetUnitAI && cmpTargetUnitAI.IsFormationMember())
target = cmpTargetUnitAI.GetFormationController();
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.SetNextState("COMBAT.APPROACHING");
return;
}
this.FinishOrder();
return;
}
this.CallMemberFunction("Attack", [target, allowCapture, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
"Order.Garrison": function(msg) {
if (!Engine.QueryInterface(msg.data.target, IID_GarrisonHolder))
{
this.FinishOrder();
return;
}
// Check if we are already in range, otherwise walk there
if (!this.CheckGarrisonRange(msg.data.target))
{
if (!this.CheckTargetVisible(msg.data.target))
{
this.FinishOrder();
return;
}
else
{
this.SetNextState("GARRISON.APPROACHING");
return;
}
}
this.SetNextState("GARRISON.GARRISONING");
},
"Order.Gather": function(msg) {
if (this.MustKillGatherTarget(msg.data.target))
{
// The target was visible when this order was given,
// but could now be invisible.
if (!this.CheckTargetVisible(msg.data.target))
{
if (msg.data.secondTry === undefined)
{
msg.data.secondTry = true;
this.PushOrderFront("Walk", msg.data.lastPos);
}
else
{
// We couldn't move there, or the target moved away
this.FinishOrder();
}
return;
}
this.PushOrderFront("Attack", { "target": msg.data.target, "force": !!msg.data.force, "hunting": true, "allowCapture": false });
return;
}
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.CanGather(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target isn't gatherable or not visible any more.
this.FinishOrder();
// TODO: Should we issue a gather-near-position order
// if the target isn't gatherable/doesn't exist anymore?
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Gather", [msg.data.target, false]);
this.SetNextState("MEMBER");
},
"Order.GatherNearPosition": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckPointRangeExplicit(msg.data.x, msg.data.z, 0, 20))
{
// Out of range; move there in formation
this.PushOrderFront("WalkToPointRange", { "x": msg.data.x, "z": msg.data.z, "min": 0, "max": 20 });
return;
}
this.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
this.SetNextState("MEMBER");
},
"Order.Heal": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Heal", [msg.data.target, false]);
this.SetNextState("MEMBER");
},
"Order.Repair": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The building was finished or destroyed
this.FinishOrder();
else
// Out of range move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextState("MEMBER");
},
"Order.ReturnResource": function(msg) {
// TODO: on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target) || !this.CheckTargetVisible(msg.data.target))
// The target was destroyed
this.FinishOrder();
else
// Out of range; move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
this.CallMemberFunction("ReturnResource", [msg.data.target, false]);
this.SetNextState("MEMBER");
},
"Order.Pack": function(msg) {
this.CallMemberFunction("Pack", [false]);
this.SetNextState("MEMBER");
},
"Order.Unpack": function(msg) {
this.CallMemberFunction("Unpack", [false]);
this.SetNextState("MEMBER");
},
"IDLE": {
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
},
},
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (this.CheckRange(this.order.data) && this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
},
"WALKINGANDFIGHTING": {
"enter": function(msg) {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
// check if there are no enemies to attack
this.FindWalkAndFightTargets();
},
"MovementUpdate": function(msg) {
if (this.CheckRange(this.order.data) && this.FinishOrder())
this.CallMemberFunction("ResetFinishOrder", []);
},
},
"PATROL": {
"enter": function(msg) {
// Memorize the origin position in case that we want to go back
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
{
this.FinishOrder();
return;
}
if (!this.patrolStartPosOrder)
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
}
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.StartTimer(0, 1000);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"Timer": function(msg) {
// Check if there are no enemies to attack
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopTimer();
this.StopMoving();
delete this.patrolStartPosOrder;
},
"MovementUpdate": function() {
if (!this.CheckRange(this.order.data))
return;
/**
* A-B-A-B-..:
* if the user only commands one patrol order, the patrol will be between
* the last position and the defined waypoint
* A-B-C-..-A-B-..:
* otherwise, the patrol is only between the given patrol commands and the
* last position is not included (last position = the position where the unit
* is located at the time of the first patrol order)
*/
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
this.PushOrder(this.order.type, this.order.data);
this.FinishOrder();
},
},
"GARRISON":{
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToGarrisonRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
this.SetNextState("GARRISONING");
},
},
"GARRISONING": {
"enter": function() {
// If a pickup has been requested, cancel it as it will be requested by members
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
this.CallMemberFunction("Garrison", [this.order.data.target, false]);
this.SetNextState("MEMBER");
},
},
},
"FORMING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
if (!this.CheckRange(this.order.data))
return;
if (this.FinishOrder())
{
this.CallMemberFunction("ResetFinishOrder", []);
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.FindInPosition();
}
},
"COMBAT": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true, true);
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.CallMemberFunction("Attack", [this.order.data.target, this.order.data.allowCapture, false]);
if (cmpAttack.CanAttackAsFormation())
this.SetNextState("COMBAT.ATTACKING");
else
this.SetNextState("MEMBER");
},
},
"ATTACKING": {
// Wait for individual members to finish
"enter": function(msg) {
var target = this.order.data.target;
var allowCapture = this.order.data.allowCapture;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
return true;
}
this.FinishOrder();
return true;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// TODO fix the rearranging while attacking as formation
cmpFormation.SetRearrange(!this.IsAttackingAsFormation());
cmpFormation.MoveMembersIntoFormation(false, false);
this.StartTimer(200, 200);
return false;
},
"Timer": function(msg) {
var target = this.order.data.target;
var allowCapture = this.order.data.allowCapture;
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetAttackRange(target, target))
{
if (this.TargetIsAlive(target) && this.CheckTargetVisible(target))
{
this.FinishOrder();
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": allowCapture });
return;
}
this.FinishOrder();
return;
}
},
"leave": function(msg) {
this.StopTimer();
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.SetRearrange(true);
},
},
},
"MEMBER": {
// Wait for individual members to finish
"enter": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(false);
this.StopMoving();
this.StartTimer(1000, 1000);
},
"Timer": function(msg) {
// Have all members finished the task?
if (!this.TestAllMemberFunction("HasFinishedOrder", []))
return;
this.CallMemberFunction("ResetFinishOrder", []);
// Execute the next order
if (this.FinishOrder())
{
// if WalkAndFight order, look for new target before moving again
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
},
"leave": function(msg) {
this.StopTimer();
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.MoveToMembersCenter();
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"FormationLeave": function(msg) {
// We're not in a formation anymore, so no need to track this.
this.finishedOrder = false;
// Stop moving as soon as the formation disbands
this.StopMoving();
// If the controller handled an order but some members rejected it,
// they will have no orders and be in the FORMATIONMEMBER.IDLE state.
if (this.orderQueue.length)
{
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
}
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
"Order.LeaveFoundation": function(msg) {
// If foundation is not ally of entity, or if entity is unpacked siege,
// ignore the order
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target) && !Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager).IsCeasefireActive() ||
this.IsPacking() || this.CanPack() || this.IsTurret())
{
this.FinishOrder();
return;
}
// Move a tile outside the building
let range = 4;
if (this.CheckTargetRangeExplicit(msg.data.target, range, -1))
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
else
{
this.order.data.min = range;
this.SetNextState("WALKINGTOPOINT");
}
},
"IDLE": {
"enter": function() {
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
return true;
},
},
"WALKING": {
"enter": function() {
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(this.order.data.target, this.order.data.x, this.order.data.z);
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpFormation && cmpVisual)
{
cmpVisual.ReplaceMoveAnimation("walk", cmpFormation.GetFormationAnimation(this.entity, "walk"));
cmpVisual.ReplaceMoveAnimation("run", cmpFormation.GetFormationAnimation(this.entity, "run"));
}
this.SelectAnimation("move");
},
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified.
"MovementUpdate": function(msg) {
// We can only finish this order if the move was really completed.
if (!this.CheckRange(this.order.data) || msg.error)
return;
if (this.FinishOrder())
return;
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
cmpVisual.ResetMoveAnimation("walk");
cmpVisual.ResetMoveAnimation("run");
}
let cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.SetInPosition(this.entity);
},
},
// Special case used by Order.LeaveFoundation
"WALKINGTOPOINT": {
"enter": function() {
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.UnsetInPosition(this.entity);
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"MovementUpdate": function() {
if (!this.CheckRange(this.order.data))
return;
this.StopMoving();
this.FinishOrder();
},
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"enter": function() {
// Sanity-checking
if (this.IsAnimal())
error("Animal got moved into INDIVIDUAL.* state");
},
"Attacked": function(msg) {
// Respond to attack if we always target attackers or during unforced orders
if (this.GetStance().targetAttackersAlways || !this.order || !this.order.data || !this.order.data.force)
this.RespondToTargetedEntities([msg.data.attacker]);
},
"GuardedAttacked": function(msg) {
// do nothing if we have a forced order in queue before the guard order
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type == "Guard")
break;
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
return;
}
// if we already are targeting another unit still alive, finish with it first
if (this.order && (this.order.type == "WalkAndFight" || this.order.type == "Attack"))
if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker))
return;
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpIdentity && cmpIdentity.HasClass("Support") &&
cmpHealth && cmpHealth.IsInjured())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf))
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// if the attacker is a building and we can repair the guarded, repair it rather than attacking
var cmpBuildingAI = Engine.QueryInterface(msg.data.attacker, IID_BuildingAI);
if (cmpBuildingAI && this.CanRepair(this.isGuardOf))
{
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
return;
}
// target the unit
if (this.CheckTargetVisible(msg.data.attacker))
this.PushOrderFront("Attack", { "target": msg.data.attacker, "force": false, "allowCapture": true });
else
{
var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
// if we already had a WalkAndFight, keep only the most recent one in case the target has moved
if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
{
this.orderQueue.splice(1, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
}
}
},
"IDLE": {
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
var animationName = "idle";
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
this.SelectAnimation(animationName);
// If we have some orders, it is because we are in an intermediary state
// from FinishOrder (SetNextState("IDLE") is only executed when we get
// a ProcessMessage), and thus we should not start another order which could
// put us in a weird state
if (this.orderQueue.length > 0 && !this.IsGarrisoned())
return false;
// If the unit is guarding/escorting, go back to its duty
if (this.isGuardOf)
{
this.Guard(this.isGuardOf, false);
return true;
}
// The GUI and AI want to know when a unit is idle, but we don't
// want to send frequent spurious messages if the unit's only
// idle for an instant and will quickly go off and do something else.
// So we'll set a timer here and only report the idle event if we
// remain idle
this.StartTimer(1000);
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
return true; // (abort the FSM transition since we may have already switched state)
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
if (this.FindNewTargets())
return true; // (abort the FSM transition since we may have already switched state)
// Nobody to attack - stay in idle
return false;
},
"leave": function() {
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
cmpRangeManager.DisableActiveQuery(this.losHealRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"LosRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackEntitiesByPreference(msg.data.added);
}
},
"LosHealRangeUpdate": function(msg) {
this.RespondToHealableEntities(msg.data.added);
},
"MoveCompleted": function() {
this.SelectAnimation("idle");
},
"Timer": function(msg) {
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
},
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function () {
this.SelectAnimation("idle");
this.StopMoving();
},
"MovementUpdate": function() {
if (this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"WALKINGANDFIGHTING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
this.SelectAnimation("move");
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"leave": function(msg) {
this.StopMoving();
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"MovementUpdate": function() {
if (this.CheckRange(this.order.data))
this.FinishOrder();
},
},
"PATROL": {
"enter": function() {
// Memorize the origin position in case that we want to go back
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld() ||
!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
if (!this.patrolStartPosOrder)
{
this.patrolStartPosOrder = cmpPosition.GetPosition();
this.patrolStartPosOrder.targetClasses = this.order.data.targetClasses;
this.patrolStartPosOrder.allowCapture = this.order.data.allowCapture;
}
this.StartTimer(0, 1000);
this.SetAnimationVariant("combat");
this.SelectAnimation("move");
},
"leave": function() {
this.StopMoving();
this.StopTimer();
delete this.patrolStartPosOrder;
this.SetDefaultAnimationVariant();
},
"Timer": function(msg) {
this.FindWalkAndFightTargets();
},
"MovementUpdate": function() {
if (!this.CheckRange(this.order.data))
return;
if (this.orderQueue.length == 1)
this.PushOrder("Patrol", this.patrolStartPosOrder);
this.PushOrder(this.order.type, this.order.data);
this.FinishOrder();
},
},
"GUARD": {
"RemoveGuard": function() {
this.StopMoving();
this.FinishOrder();
},
"ESCORTING": {
"enter": function() {
if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.StartTimer(0, 1000);
this.SelectAnimation("move");
this.SetHeldPositionOnEntity(this.isGuardOf);
return false;
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.FinishOrder();
return;
}
// Adapt the speed to the one of the target if needed
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
if (cmpObstructionManager.IsInTargetRange(this.entity, this.isGuardOf, 0, 3 * this.guardRange, false))
{
let cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
if (cmpUnitAI)
{
let speed = cmpUnitAI.GetWalkSpeed();
if (speed < this.GetWalkSpeed())
this.SetSpeedMultiplier(speed / this.GetWalkSpeed());
}
}
this.SetHeldPositionOnEntity(this.isGuardOf);
},
"leave": function(msg) {
this.StopMoving();
this.ResetSpeedMultiplier();
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"MovementUpdate": function() {
if (this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("GUARDING");
},
},
"GUARDING": {
"enter": function() {
this.StopMoving();
this.StartTimer(1000, 1000);
this.SetHeldPositionOnEntity(this.entity);
this.SetAnimationVariant("combat");
this.SelectAnimation("idle");
return false;
},
"LosRangeUpdate": function(msg) {
// Start attacking one of the newly-seen enemy (if any)
if (this.GetStance().targetVisibleEnemies)
this.AttackEntitiesByPreference(msg.data.added);
},
"Timer": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.isGuardOf))
{
this.FinishOrder();
return;
}
// Then check is the target has moved and try following it.
// TODO: find out what to do if we cannot move.
if (!this.CheckTargetRangeExplicit(this.isGuardOf, 0, this.guardRange) &&
this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
this.SetNextState("ESCORTING");
else
{
// if nothing better to do, check if the guarded needs to be healed or repaired
var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
if (cmpHealth && cmpHealth.IsInjured())
{
if (this.CanHeal(this.isGuardOf))
this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
else if (this.CanRepair(this.isGuardOf))
this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
}
}
},
"leave": function(msg) {
this.StopTimer();
this.SetDefaultAnimationVariant();
},
},
},
"FLEEING": {
"enter": function() {
// We use the distance between the entities to account for ranged attacks
this.order.data.distanceToFlee = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
// Use unit motion directly to ignore the visibility check. TODO: change this if we add LOS to fauna.
if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) ||
!cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1))
{
this.FinishOrder();
return true;
}
this.PlaySound("panic");
// Run quickly
this.SelectAnimation("move");
this.SetSpeedMultiplier(this.GetRunMultiplier());
},
"HealthChanged": function() {
this.SetSpeedMultiplier(this.GetRunMultiplier());
},
"leave": function() {
this.ResetSpeedMultiplier();
this.StopMoving();
},
"MovementUpdate": function() {
// When we've run far enough, stop fleeing
if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1))
this.FinishOrder();
},
// TODO: what if we run into more enemies while fleeing?
},
"COMBAT": {
"Order.LeaveFoundation": function(msg) {
// Ignore the order as we're busy.
return { "discardOrder": true };
},
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else who's attacking us
// unless it's a melee attack since they may be blocking our way to the target
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || !this.order.data.force))
this.RespondToTargetedEntities([msg.data.attacker]);
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
// Show carried resources when walking.
this.SetDefaultAnimationVariant();
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MovementUpdate": function() {
if (!this.CheckTargetAttackRange(this.order.data.target, this.order.data.attackType))
return;
// If the unit needs to unpack, do so
if (this.CanUnpack())
{
this.PushOrderFront("Unpack", { "force": true });
return;
}
this.SetNextState("ATTACKING");
},
},
"ATTACKING": {
"enter": function() {
var target = this.order.data.target;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity);
this.order.data.target = target;
}
// Check the target is still alive and attackable
if (this.CanAttack(target) && !this.CheckTargetAttackRange(target, this.order.data.attackType))
{
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("COMBAT.CHASING");
return true;
}
}
this.StopMoving();
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.attackTimers = cmpAttack.GetTimers(this.order.data.attackType);
// If the repeat time since the last attack hasn't elapsed,
// delay this attack to avoid attacking too fast.
var prepare = this.attackTimers.prepare;
if (this.lastAttacked)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastAttacked + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.oldAttackType = this.order.data.attackType;
// add prefix + no capital first letter for attackType
var animationName = "attack_" + this.order.data.attackType.toLowerCase();
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
animationName = cmpFormation.GetFormationAnimation(this.entity, animationName);
}
this.SetAnimationVariant("combat");
this.SelectAnimation(animationName);
this.SetAnimationSync(prepare, this.attackTimers.repeat);
this.StartTimer(prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(this.order.data.target);
},
"leave": function() {
var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (cmpBuildingAI)
cmpBuildingAI.SetUnitAITarget(0);
this.StopTimer();
this.SetDefaultAnimationVariant();
},
"Timer": function(msg) {
var target = this.order.data.target;
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
// if the target is a formation, save the attacking formation, and pick a member
if (cmpFormation)
{
var thisObject = this;
var filter = function(t) {
return thisObject.CanAttack(t);
};
this.order.data.formationTarget = target;
target = cmpFormation.GetClosestMember(this.entity, filter);
this.order.data.target = target;
}
// Check the target is still alive and attackable
if (this.CanAttack(target))
{
// If we are hunting, first update the target position of the gather order so we know where will be the killed animal
if (this.order.data.hunting && this.orderQueue[1] && this.orderQueue[1].data.lastPos)
{
var cmpPosition = Engine.QueryInterface(this.order.data.target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
// Store the initial position, so that we can find the rest of the herd later
if (!this.orderQueue[1].data.initPos)
this.orderQueue[1].data.initPos = this.orderQueue[1].data.lastPos;
this.orderQueue[1].data.lastPos = cmpPosition.GetPosition();
// We still know where the animal is, so we shouldn't give up before going there
this.orderQueue[1].data.secondTry = undefined;
}
}
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastAttacked = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
// BuildingAI has it's own attack-routine
var cmpBuildingAI = Engine.QueryInterface(this.entity, IID_BuildingAI);
if (!cmpBuildingAI)
{
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(this.order.data.attackType, target);
}
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, this.order.data.attackType))
{
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("COMBAT.CHASING");
return;
}
}
// if we're targetting a formation, find a new member of that formation
var cmpTargetFormation = Engine.QueryInterface(this.order.data.formationTarget || INVALID_ENTITY, IID_Formation);
// if there is no target, it means previously searching for the target inside the target formation failed, so don't repeat the search
if (target && cmpTargetFormation)
{
this.order.data.target = this.order.data.formationTarget;
this.TimerHandler(msg.data, msg.lateness);
return;
}
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
// Except if in WalkAndFight mode where we look for more ennemies around before moving again
if (this.FinishOrder())
{
if (this.IsWalkingAndFighting())
this.FindWalkAndFightTargets();
return;
}
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
{
// Attempt to immediately re-enter the timer function, to avoid wasting the attack.
// Packable units may have switched to PACKING state, thus canceling the timer and having order.data.attackType undefined.
if (this.orderQueue.length > 0 && this.orderQueue[0].data && this.orderQueue[0].data.attackType &&
this.orderQueue[0].data.attackType == this.oldAttackType)
this.TimerHandler(msg.data, msg.lateness);
return;
}
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
"Attacked": function(msg) {
// If we are capturing and are attacked by something that we would not capture, attack that entity instead
if (this.order.data.attackType == "Capture" && (this.GetStance().targetAttackersAlways || !this.order.data.force)
&& this.order.data.target != msg.data.attacker && this.GetBestAttackAgainst(msg.data.attacker, true) != "Capture")
this.RespondToTargetedEntities([msg.data.attacker]);
},
},
"CHASING": {
"enter": function() {
if (!this.MoveToTargetAttackRange(this.order.data.target, this.order.data.attackType))
{
this.FinishOrder();
return true;
}
// Show weapons rather than carried resources.
this.SetAnimationVariant("combat");
this.SelectAnimation("move");
var cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsFleeing())
{
// Run after a fleeing target
this.SetSpeedMultiplier(this.GetRunMultiplier());
}
this.StartTimer(1000, 1000);
},
"leave": function() {
this.ResetSpeedMultiplier();
// Show carried resources when walking.
this.SetDefaultAnimationVariant();
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack, this.order.data.attackType))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MovementUpdate": function() {
this.SetNextState("ATTACKING");
},
},
},
"GATHER": {
"APPROACHING": {
"enter": function() {
this.gatheringTarget = this.order.data.target; // temporary, deleted in "leave".
// check that we can gather from the resource we're supposed to gather from.
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
var cmpMirage = Engine.QueryInterface(this.gatheringTarget, IID_Mirage);
if ((!cmpMirage || !cmpMirage.Mirages(IID_ResourceSupply)) &&
(!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity)) ||
!this.MoveTo(this.order.data, IID_ResourceGatherer))
{
// The GATHERING timer will handle finding a valid resource.
this.SetNextState("GATHERING");
return true;
}
this.SelectAnimation("move");
return false;
},
"MovementUpdate": function(msg) {
// If we failed, the GATHERING timer will handle finding a valid resource.
this.SetNextState("GATHERING");
},
"leave": function() {
this.StopMoving();
this.SetDefaultAnimationVariant();
if (!this.gatheringTarget)
return;
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function(msg) {
// If we failed, the GATHERING timer will handle finding a valid resource.
this.SetNextState("GATHERING");
},
},
"GATHERING": {
"enter": function() {
this.gatheringTarget = this.order.data.target || INVALID_ENTITY; // deleted in "leave".
// Check if the resource is full.
// Will only be added if we're not already in.
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpSupply;
if (cmpOwnership)
cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (!cmpSupply || !cmpSupply.AddGatherer(cmpOwnership.GetOwner(), this.entity))
{
this.StartTimer(0);
return false;
}
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.order.data.autoharvest = true;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var rate = cmpResourceGatherer.GetTargetGatherRate(this.gatheringTarget);
if (!rate)
{
// Try to find another target if the current one stopped existing
if (!Engine.QueryInterface(this.gatheringTarget, IID_Identity))
{
// Let the Timer logic handle this
this.StartTimer(0);
return false;
}
// No rate, give up on gathering
this.FinishOrder();
return true;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
var offset = 1000/rate;
var repeat = offset;
this.StartTimer(offset, repeat);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.StopMoving();
this.SetDefaultAnimationVariant();
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename);
}
return false;
},
"leave": function() {
this.StopTimer();
// don't use ownership because this is called after a conversion/resignation
// and the ownership would be invalid then.
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply)
cmpSupply.RemoveGatherer(this.entity);
delete this.gatheringTarget;
// Show the carried resource, if we've gathered anything.
this.SetDefaultAnimationVariant();
},
"Timer": function(msg) {
var resourceTemplate = this.order.data.template;
var resourceType = this.order.data.type;
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
var cmpSupply = Engine.QueryInterface(this.gatheringTarget, IID_ResourceSupply);
if (cmpSupply && cmpSupply.IsAvailable(cmpOwnership.GetOwner(), this.entity))
{
// Check we can still reach and gather from the target
if (this.CheckTargetRange(this.gatheringTarget, IID_ResourceGatherer) && this.CanGather(this.gatheringTarget))
{
// Gather the resources:
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(this.gatheringTarget))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
cmpResourceGatherer.DropResources();
// Collect from the target
var status = cmpResourceGatherer.PerformGather(this.gatheringTarget);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
// We can gather more from this target, do so in the next timer
if (!status.exhausted)
return;
}
else
{
// Try to follow the target
if (this.MoveToTargetRange(this.gatheringTarget, IID_ResourceGatherer))
{
this.SetNextState("APPROACHING");
return;
}
// Our target is no longer visible - go to its last known position first
// and then hopefully it will become visible.
if (!this.CheckTargetVisible(target) && this.order.data.lastPos)
{
this.PushOrderFront("Walk", {
"x": this.order.data.lastPos.x,
"z": this.order.data.lastPos.z,
"force": this.order.data.force
});
return;
}
}
}
// We're already in range, can't get anywhere near it or the target is exhausted.
var herdPos = this.order.data.initPos;
// Give up on this order and try our next queued order
// but first check what is our next order and, if needed, insert a returnResource order
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer.IsCarrying(resourceType.generic) &&
this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" &&
(this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic))
{
let nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
this.orderQueue.splice(1, 0, { "type": "ReturnResource", "data": { "target": nearby, "force": false } });
}
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// Try to find a new resource of the same specific type near our current position:
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function(ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// If hunting, try to go to the initial herd position to see if we are more lucky
if (herdPos)
{
this.GatherNearPosition(herdPos.x, herdPos.z, resourceType, resourceTemplate);
return;
}
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites - just give up
},
},
},
"HEAL": {
"Attacked": function(msg) {
// If we stand ground we will rather die than flee
if (!this.GetStance().respondStandGround && !this.order.data.force)
this.Flee(msg.data.attacker, false);
},
"APPROACHING": {
"enter": function() {
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.SetNextState("HEALING");
return true;
}
if (!this.MoveTo(this.order.data, IID_Heal))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopMoving();
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal, null))
{
// Return to our original position unless we have a better order.
if (!this.FinishOrder() && this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MovementUpdate": function() {
this.SetNextState("HEALING");
},
},
"HEALING": {
"enter": function() {
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
this.healTimers = cmpHeal.GetTimers();
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
var prepare = this.healTimers.prepare;
if (this.lastHealed)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
this.StopMoving();
this.SelectAnimation("heal");
this.SetAnimationSync(prepare, this.healTimers.repeat);
this.StartTimer(prepare, this.healTimers.repeat);
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check the target is still alive and healable
if (this.TargetIsAlive(target) && this.CanHeal(target))
{
// Check if we can still reach the target
if (this.CheckTargetRange(target, IID_Heal))
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastHealed = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
cmpHeal.PerformHeal(target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.CanPack())
{
this.PushOrderFront("Pack", { "force": true });
return;
}
this.SetNextState("HEAL.APPROACHING");
}
}
// Can't reach it, healed to max hp or doesn't exist any more - give up
if (this.FinishOrder())
return;
// Heal another one
if (this.FindNewHealTargets())
return;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
},
},
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data, IID_ResourceGatherer))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with the carry animation after stopping moving
this.SelectAnimation("idle");
},
"MovementUpdate": function() {
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
cmpResourceGatherer.CommitResources(dropsiteTypes);
// Stop showing the carried resource animation.
this.SetDefaultAnimationVariant();
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// The dropsite was destroyed, or we couldn't reach it, or ownership changed
// Look for a new one.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var genericType = cmpResourceGatherer.GetMainCarryingType();
var nearby = this.FindNearestDropsite(genericType);
if (nearby)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"APPROACHINGMARKET": {
"enter": function() {
if (!this.MoveToMarket(this.order.data.target))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function() {
if (!this.CheckTargetRange(this.order.data.target, IID_Trader))
return;
if (this.waypoints && this.waypoints.length)
{
if (!this.MoveToMarket(this.order.data.target))
this.StopTrading();
}
else
this.PerformTradeAndMoveToNextMarket(this.order.data.target);
},
},
"TradingCanceled": function(msg) {
if (msg.market != this.order.data.target)
return;
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let otherMarket = cmpTrader && cmpTrader.GetFirstMarket();
this.StopTrading();
if (otherMarket)
this.WalkToTarget(otherMarket);
},
},
"REPAIR": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data, IID_Builder))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function() {
if (this.CheckRange(this.order.data, IID_Builder))
this.SetNextState("REPAIRING");
},
},
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if (this.order.data.force)
this.order.data.autoharvest = true;
this.order.data.force = false;
this.repairTarget = this.order.data.target; // temporary, deleted in "leave".
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return true;
}
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
{
this.SetNextState("APPROACHING");
return true;
}
// Check if the target is still repairable
var cmpHealth = Engine.QueryInterface(this.repairTarget, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() >= cmpHealth.GetMaxHitpoints())
{
// The building was already finished/fully repaired before we arrived;
// let the ConstructionFinished handler handle this.
this.OnGlobalConstructionFinished({"entity": this.repairTarget, "newentity": this.repairTarget});
return true;
}
this.StopMoving();
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.AddBuilder(this.entity);
this.SelectAnimation("build");
this.StartTimer(1000, 1000);
return false;
},
"leave": function() {
let cmpBuilderList = QueryBuilderListInterface(this.repairTarget);
if (cmpBuilderList)
cmpBuilderList.RemoveBuilder(this.entity);
delete this.repairTarget;
this.StopTimer();
},
"Timer": function(msg) {
// Check we can still reach and repair the target
if (!this.CanRepair(this.repairTarget))
{
// No longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return;
}
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(this.repairTarget);
// if the building is completed, the leave() function will be called
// by the ConstructionFinished message
// in that case, the repairTarget is deleted, and we can just return
if (!this.repairTarget)
return;
if (!this.CheckTargetRange(this.repairTarget, IID_Builder))
this.SetNextState("APPROACHING");
},
},
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
// Save the current order's data in case we need it later
var oldData = this.order.data;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
var oldState = this.GetCurrentState();
// Drop any resource we can if we are in range when the construction finishes
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
if (cmpResourceGatherer && cmpResourceDropsite && this.CheckTargetRange(msg.data.newentity, IID_Builder) &&
this.CanReturnResource(msg.data.newentity, true))
{
let dropsiteTypes = cmpResourceDropsite.GetTypes();
cmpResourceGatherer.CommitResources(dropsiteTypes);
this.SetDefaultAnimationVariant();
}
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
{
if (this.CanReturnResource(msg.data.newentity, true))
{
this.SetDefaultAnimationVariant();
this.PushOrderFront("ReturnResource", { "target": msg.data.newentity, "force": false });
}
return;
}
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldData.autocontinue)
return;
// If this building was e.g. a farm of ours, the entities that received
// the build command should start gathering from it
if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
{
if (this.CanReturnResource(msg.data.newentity, true))
{
this.SetDefaultAnimationVariant();
this.PushOrder("ReturnResource", { "target": msg.data.newentity, "force": false });
}
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead of ours, entities that received
// the build command should look for nearby resources to gather
if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false))
{
var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
var types = cmpResourceDropsite.GetTypes();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
var nearby = this.FindNearbyResource(function(ent, type, template) {
return (types.indexOf(type.generic) != -1);
}, msg.data.newentity);
if (nearby)
{
this.PerformGather(nearby, true, false);
return;
}
}
// Look for a nearby foundation to help with
var nearbyFoundation = this.FindNearbyFoundation();
if (nearbyFoundation)
{
this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldData.autocontinue, "force": false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState === "INDIVIDUAL.REPAIR.APPROACHING")
{
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
}
},
},
"GARRISON": {
"enter": function() {
// If the garrisonholder should pickup, warn it so it can take needed action
var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.CanPickup(this.entity))
{
this.pickup = this.order.data.target; // temporary, deleted in "leave"
Engine.PostMessage(this.pickup, MT_PickupRequested, { "entity": this.entity });
}
},
"leave": function() {
// If a pickup has been requested and not yet canceled, cancel it
if (this.pickup)
{
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
},
"APPROACHING": {
"enter": function() {
if (!this.MoveToGarrisonRange(this.order.data.target))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function() {
if (this.CheckGarrisonRange(this.order.data.target))
this.SetNextState("GARRISONED");
},
},
"GARRISONED": {
"enter": function() {
if (this.order.data.target)
var target = this.order.data.target;
else
{
this.FinishOrder();
return true;
}
if (this.IsGarrisoned())
return false;
// Check that we can garrison here
if (this.CanGarrison(target))
{
// Check that we're in range of the garrison target
if (this.CheckGarrisonRange(target))
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
// Check that garrisoning succeeds
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
if (this.formationController)
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
{
// disable rearrange for this removal,
// but enable it again for the next
// move command
var rearrange = cmpFormation.rearrange;
cmpFormation.SetRearrange(false);
cmpFormation.RemoveMembers([this.entity]);
cmpFormation.SetRearrange(rearrange);
}
}
// Check if we are garrisoned in a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (cmpResourceDropsite && this.CanReturnResource(target, true))
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
cmpResourceGatherer.CommitResources(dropsiteTypes);
this.SetDefaultAnimationVariant();
}
}
// If a pickup has been requested, remove it
if (this.pickup)
{
var cmpHolderPosition = Engine.QueryInterface(target, IID_Position);
var cmpHolderUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderPosition)
cmpHolderUnitAI.lastShorelinePosition = cmpHolderPosition.GetPosition();
Engine.PostMessage(this.pickup, MT_PickupCanceled, { "entity": this.entity });
delete this.pickup;
}
if (this.IsTurret())
this.SetNextState("IDLE");
return false;
}
}
else
{
// Unable to reach the target, try again (or follow if it is a moving target)
// except if the does not exits anymore or its orders have changed
if (this.pickup)
{
var cmpUnitAI = Engine.QueryInterface(this.pickup, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.HasPickupOrder(this.entity))
{
this.FinishOrder();
return true;
}
}
if (!this.CheckTargetRangeExplicit(target, 0, 0) && this.MoveToTarget(target))
{
this.SetNextState("APPROACHING");
return false;
}
}
}
// Garrisoning failed for some reason, so finish the order
this.FinishOrder();
return true;
},
"leave": function() {
}
},
},
"CHEERING": {
"enter": function() {
// Unit is invulnerable while cheering
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(2800, 2800);
return false;
},
"leave": function() {
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
},
"Timer": function(msg) {
this.FinishOrder();
},
},
"PACKING": {
"enter": function() {
this.StopMoving();
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Pack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while packing
},
},
"UNPACKING": {
"enter": function() {
this.StopMoving();
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
cmpPack.Unpack();
},
"PackFinished": function(msg) {
this.FinishOrder();
},
"leave": function() {
},
"Attacked": function(msg) {
// Ignore attacks while unpacking
},
},
"PICKUP": {
"APPROACHING": {
"enter": function() {
if (!this.MoveTo(this.order.data))
{
this.FinishOrder();
return true;
}
this.SelectAnimation("move");
},
"leave": function() {
this.StopMoving();
},
"MovementUpdate": function() {
if (this.CheckRange(this.order.data))
this.SetNextState("LOADING");
},
"PickupCanceled": function() {
this.StopMoving();
this.FinishOrder();
},
},
"LOADING": {
"enter": function() {
this.StopMoving();
this.SelectAnimation("idle");
var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull())
{
this.FinishOrder();
return true;
}
return false;
},
"PickupCanceled": function() {
this.FinishOrder();
},
},
},
},
"ANIMAL": {
"Attacked": function(msg) {
if (this.template.NaturalBehaviour == "skittish" ||
this.template.NaturalBehaviour == "passive")
{
this.Flee(msg.data.attacker, false);
}
else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive")
{
if (this.CanAttack(msg.data.attacker))
this.Attack(msg.data.attacker, false);
}
else if (this.template.NaturalBehaviour == "domestic")
{
// Never flee, stop what we were doing
this.SetNextState("IDLE");
}
},
"Order.LeaveFoundation": function(msg) {
// Move a tile outside the building
let range = 4;
if (this.CheckTargetRangeExplicit(msg.data.target, range, -1))
{
this.FinishOrder();
return;
}
this.order.data.min = range;
this.SetNextState("WALKING");
},
"IDLE": {
// (We need an IDLE state so that FinishOrder works)
"enter": function() {
// Start feeding immediately
this.SetNextState("FEEDING");
return true;
},
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
this.SelectAnimation("walk", false, 1);
this.SetFacePointAfterMove(false);
this.MoveRandomly(+this.template.RoamDistance);
// Set a random timer to switch to feeding state
this.StartTimer(randIntInclusive(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
},
"leave": function() {
this.StopMoving();
this.StopTimer();
this.SetFacePointAfterMove(true);
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.IsDangerousAnimal())
{
this.AttackVisibleEntity(msg.data.added);
}
// TODO: if two units enter our range together, we'll attack the
// first and then the second won't trigger another LosRangeUpdate
// so we won't notice it. Probably we should do something with
// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
// find any units that are already in range.
},
"Timer": function(msg) {
this.SetNextState("FEEDING");
},
"MovementUpdate": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
"FEEDING": {
"enter": function() {
// Stop and eat for a while
this.SelectAnimation("feeding");
this.StopMoving();
this.StartTimer(randIntInclusive(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
},
"leave": function() {
this.StopTimer();
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.template.NaturalBehaviour == "violent")
{
this.AttackVisibleEntity(msg.data.added);
}
},
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals
"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
"WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals
// only used for domestic animals
},
};
UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.finishedOrder = false; // used to find if all formation members finished the order
this.heldPosition = undefined;
// Queue of remembered works
this.workOrders = [];
this.isGuardOf = undefined;
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = undefined;
this.lastHealed = undefined;
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsTurret = function()
{
if (!this.IsGarrisoned())
return false;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY;
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsFormationMember = function()
{
return (this.formationController != INVALID_ENTITY);
};
UnitAI.prototype.HasFinishedOrder = function()
{
return this.finishedOrder;
};
UnitAI.prototype.ResetFinishOrder = function()
{
this.finishedOrder = false;
};
UnitAI.prototype.IsAnimal = function()
{
return (this.template.NaturalBehaviour ? true : false);
};
UnitAI.prototype.IsDangerousAnimal = function()
{
return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" ||
this.template.NaturalBehaviour == "aggressive"));
};
UnitAI.prototype.IsDomestic = function()
{
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
return cmpIdentity && cmpIdentity.HasClass("Domestic");
};
UnitAI.prototype.IsHealer = function()
{
return Engine.QueryInterface(this.entity, IID_Heal);
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
UnitAI.prototype.IsGarrisoned = function()
{
return this.isGarrisoned;
};
UnitAI.prototype.SetGarrisoned = function()
{
this.isGarrisoned = true;
};
UnitAI.prototype.GetGarrisonHolder = function()
{
if (this.IsGarrisoned())
{
for (let order of this.orderQueue)
if (order.type == "Garrison")
return order.data.target;
}
return INVALID_ENTITY;
};
UnitAI.prototype.ShouldRespondToEndOfAlert = function()
{
return !this.orderQueue.length || this.orderQueue[0].type == "Garrison";
};
UnitAI.prototype.IsFleeing = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "FLEEING");
};
UnitAI.prototype.IsWalking = function()
{
var state = this.GetCurrentState().split(".").pop();
return (state == "WALKING");
};
/**
* Return true if the current order is WalkAndFight or Patrol.
*/
UnitAI.prototype.IsWalkingAndFighting = function()
{
if (this.IsFormationMember())
return false;
return this.orderQueue.length > 0 && (this.orderQueue[0].type == "WalkAndFight" || this.orderQueue[0].type == "Patrol");
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsAnimal())
this.UnitFsm.Init(this, "ANIMAL.FEEDING");
else if (this.IsFormationController())
this.UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
this.UnitFsm.Init(this, "INDIVIDUAL.IDLE");
this.isIdle = true;
};
UnitAI.prototype.OnDiplomacyChanged = function(msg)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
this.SetupRangeQueries();
if (this.isGuardOf && !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf))
this.RemoveGuard();
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQueries();
if (this.isGuardOf && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, this.isGuardOf)))
this.RemoveGuard();
// If the unit isn't being created or dying, reset stance and clear orders
if (msg.to != INVALID_PLAYER && msg.from != INVALID_PLAYER)
{
// Switch to a virgin state to let states execute their leave handlers.
// except if garrisoned or cheering or (un)packing, in which case we only clear the order queue
if (this.isGarrisoned || this.IsPacking() || this.orderQueue[0] && this.orderQueue[0].type == "Cheering")
{
this.orderQueue.length = Math.min(this.orderQueue.length, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
}
else
{
let index = this.GetCurrentState().indexOf(".");
if (index != -1)
this.UnitFsm.SwitchToNextState(this, this.GetCurrentState().slice(0,index));
this.Stop(false);
}
this.workOrders = [];
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader)
cmpTrader.StopTrading();
this.SetStance(this.template.DefaultStance);
if (this.IsTurret())
this.SetTurretStance();
}
};
UnitAI.prototype.OnDestroy = function()
{
// Switch to an empty state to let states execute their leave handlers.
this.UnitFsm.SwitchToNextState(this, "");
// Clean up range queries
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
};
UnitAI.prototype.OnVisionRangeChanged = function(msg)
{
// Update range queries
if (this.entity == msg.entity)
this.SetupRangeQueries();
};
UnitAI.prototype.HasPickupOrder = function(entity)
{
return this.orderQueue.some(order => order.type == "PickupUnit" && order.data.target == entity);
};
UnitAI.prototype.OnPickupRequested = function(msg)
{
// First check if we already have such a request
if (this.HasPickupOrder(msg.entity))
return;
// Otherwise, insert the PickUp order after the last forced order
this.PushOrderAfterForced("PickupUnit", { "target": msg.entity });
};
UnitAI.prototype.OnPickupCanceled = function(msg)
{
for (let i = 0; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].type != "PickupUnit" || this.orderQueue[i].data.target != msg.entity)
continue;
if (i == 0)
this.UnitFsm.ProcessMessage(this, {"type": "PickupCanceled", "data": msg});
else
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
break;
}
};
// Wrapper function that sets up the normal and healer range queries.
UnitAI.prototype.SetupRangeQueries = function()
{
this.SetupRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
};
UnitAI.prototype.UpdateRangeQueries = function()
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
this.SetupRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losRangeQuery));
if (this.IsHealer() && this.losHealRangeQuery)
this.SetupHealRangeQuery(cmpRangeManager.IsActiveQueryEnabled(this.losHealRangeQuery));
};
// Set up a range query for all enemy and gaia units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupRangeQuery = function(enable = true)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner -1), creating a range query is pointless
if (!cmpPlayer)
return;
// Exclude allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
var players = cmpPlayer.GetEnemies();
var range = this.GetQueryRange(IID_Attack);
this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
if (enable)
cmpRangeManager.EnableActiveQuery(this.losRangeQuery);
};
// Set up a range query for all own or ally units within LOS range
// which can be healed.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupHealRangeQuery = function(enable = true)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losHealRangeQuery)
{
cmpRangeManager.DestroyActiveQuery(this.losHealRangeQuery);
this.losHealRangeQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
// If we are being destructed (owner -1), creating a range query is pointless
if (!cmpPlayer)
return;
var players = cmpPlayer.GetAllies();
var range = this.GetQueryRange(IID_Heal);
this.losHealRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, cmpRangeManager.GetEntityFlagMask("injured"));
if (enable)
cmpRangeManager.EnableActiveQuery(this.losHealRangeQuery);
};
//// FSM linkage functions ////
// Setting the next state to the current state will leave/re-enter the top-most substate.
UnitAI.prototype.SetNextState = function(state)
{
this.UnitFsm.SetNextState(this, state);
};
UnitAI.prototype.DeferMessage = function(msg)
{
this.UnitFsm.DeferMessage(this, msg);
};
UnitAI.prototype.GetCurrentState = function()
{
return this.UnitFsm.GetCurrentState(this);
};
UnitAI.prototype.FsmStateNameChanged = function(state)
{
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
};
/**
* Call when the current order has been completed (or failed).
* Removes the current order from the queue, and processes the
* next one (if any). Returns false and defaults to IDLE
* if there are no remaining orders or if the unit is not
* inWorld and not garrisoned (thus usually waiting to be destroyed).
*/
UnitAI.prototype.FinishOrder = function()
{
if (!this.orderQueue.length)
{
let stack = new Error().stack.trimRight().replace(/^/mg, ' '); // indent each line
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
}
this.orderQueue.shift();
this.order = this.orderQueue[0];
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (this.orderQueue.length && (this.IsGarrisoned() || cmpPosition && cmpPosition.IsInWorld()))
{
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
return this.FinishOrder();
// Otherwise we've successfully processed a new order
return true;
}
this.orderQueue = [];
this.order = undefined;
// Switch to IDLE as a default state, but only if our current state is not IDLE
// as this can trigger infinite loops by entering IDLE repeatedly.
if (!this.GetCurrentState().endsWith(".IDLE"))
this.SetNextState("IDLE");
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// Check if there are queued formation orders
if (this.IsFormationMember())
{
let cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
// Inform the formation controller that we finished this task
this.finishedOrder = true;
// We don't want to carry out the default order
// if there are still queued formation orders left
if (cmpUnitAI.GetOrders().length > 1)
return true;
}
}
return false;
};
/**
* Add an order onto the back of the queue,
* and execute it if we didn't already have an order.
*/
UnitAI.prototype.PushOrder = function(type, data)
{
var order = { "type": type, "data": data };
this.orderQueue.push(order);
// If we didn't already have an order, then process this new one
if (this.orderQueue.length == 1)
{
this.order = order;
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
this.FinishOrder();
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Add an order onto the front of the queue,
* and execute it immediately.
*/
UnitAI.prototype.PushOrderFront = function(type, data)
{
var order = { "type": type, "data": data };
// If current order is cheering then add new order after it
// same thing if current order if packing/unpacking
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue.unshift(cheeringOrder, order);
}
else if (this.order && this.IsPacking())
{
var packingOrder = this.orderQueue.shift();
this.orderQueue.unshift(packingOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
let ret = this.UnitFsm.ProcessMessage(this,
{ "type": "Order."+this.order.type, "data": this.order.data }
);
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret && ret.discardOrder)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
/**
* Insert an order after the last forced order onto the queue
* and after the other orders of the same type
*/
UnitAI.prototype.PushOrderAfterForced = function(type, data)
{
if (!this.order || ((!this.order.data || !this.order.data.force) && this.order.type != type))
this.PushOrderFront(type, data);
else
{
for (let i = 1; i < this.orderQueue.length; ++i)
{
if (this.orderQueue[i].data && this.orderQueue[i].data.force)
continue;
if (this.orderQueue[i].type == type)
continue;
this.orderQueue.splice(i, 0, {"type": type, "data": data});
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
return;
}
this.PushOrder(type, data);
}
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// Remember the previous work orders to be able to go back to them later if required
if (data && data.force)
{
if (this.IsFormationController())
this.CallMemberFunction("UpdateWorkOrders", [type]);
else
this.UpdateWorkOrders(type);
}
let garrisonHolder = this.IsGarrisoned() && type != "Ungarrison" ? this.GetGarrisonHolder() : null;
// Special cases of orders that shouldn't be replaced:
// 1. Cheering - we're invulnerable, add order after we finish
// 2. Packing/unpacking - we're immobile, add order after we finish (unless it's cancel)
// TODO: maybe a better way of doing this would be to use priority levels
if (this.order && this.order.type == "Cheering")
{
var order = { "type": type, "data": data };
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder, order];
}
else if (this.IsPacking() && type != "CancelPack" && type != "CancelUnpack")
{
var order = { "type": type, "data": data };
var packingOrder = this.orderQueue.shift();
this.orderQueue = [packingOrder, order];
}
else
{
this.orderQueue = [];
this.PushOrder(type, data);
}
if (garrisonHolder)
this.PushOrder("Garrison", { "target": garrisonHolder });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.GetOrders = function()
{
return this.orderQueue.slice();
};
UnitAI.prototype.AddOrders = function(orders)
{
orders.forEach(order => this.PushOrder(order.type, order.data));
};
UnitAI.prototype.GetOrderData = function()
{
var orders = [];
for (let order of this.orderQueue)
if (order.data)
orders.push(clone(order.data));
return orders;
};
UnitAI.prototype.UpdateWorkOrders = function(type)
{
var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource";
// If we are being re-affected to a work order, forget the previous ones
if (isWorkType(type))
{
this.workOrders = [];
return;
}
// Then if we already have work orders, keep them
if (this.workOrders.length)
return;
// First if the unit is in a formation, get its workOrders from it
if (this.IsFormationMember())
{
var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpUnitAI)
{
for (var i = 0; i < cmpUnitAI.orderQueue.length; ++i)
{
if (isWorkType(cmpUnitAI.orderQueue[i].type))
{
this.workOrders = cmpUnitAI.orderQueue.slice(i);
return;
}
}
}
}
// If nothing found, take the unit orders
for (var i = 0; i < this.orderQueue.length; ++i)
{
if (isWorkType(this.orderQueue[i].type))
{
this.workOrders = this.orderQueue.slice(i);
return;
}
}
};
UnitAI.prototype.BackToWork = function()
{
if (this.workOrders.length == 0)
return false;
if (this.IsGarrisoned())
{
let cmpGarrisonHolder = Engine.QueryInterface(this.GetGarrisonHolder(), IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.PerformEject([this.entity], false))
return false;
}
// Clear the order queue considering special orders not to avoid
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [cheeringOrder];
}
else
this.orderQueue = [];
this.AddOrders(this.workOrders);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// And if the unit is in a formation, remove it from the formation
if (this.IsFormationMember())
{
var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers([this.entity]);
}
this.workOrders = [];
return true;
};
UnitAI.prototype.HasWorkOrders = function()
{
return this.workOrders.length > 0;
};
UnitAI.prototype.GetWorkOrders = function()
{
return this.workOrders;
};
UnitAI.prototype.SetWorkOrders = function(orders)
{
this.workOrders = orders;
};
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
if (data.timerRepeat === undefined)
this.timer = undefined;
this.UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
//// Message handlers /////
UnitAI.prototype.OnMotionChanged = function(msg)
{
this.UnitFsm.ProcessMessage(this, { "type": "MovementUpdate", "error": msg.error });
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
{
// TODO: This is a bit inefficient since every unit listens to every
// construction message - ideally we could scope it to only the one we're building
this.UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
let changed = false;
for (let order of this.orderQueue)
{
if (order.data && order.data.target && order.data.target == msg.entity)
{
changed = true;
order.data.target = msg.newentity;
}
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
{
changed = true;
order.data.formationTarget = msg.newentity;
}
}
if (changed)
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.OnAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};
UnitAI.prototype.OnGuardedAttacked = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
};
UnitAI.prototype.OnHealthChanged = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
else if (msg.tag == this.losHealRangeQuery)
this.UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
};
UnitAI.prototype.OnPackFinished = function(msg)
{
this.UnitFsm.ProcessMessage(this, {"type": "PackFinished", "packed": msg.packed});
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpUnitMotion)
return 0;
return cmpUnitMotion.GetWalkSpeed();
};
UnitAI.prototype.GetRunMultiplier = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpUnitMotion)
return 0;
return cmpUnitMotion.GetRunMultiplier();
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
UnitAI.prototype.TargetIsAlive = function(ent)
{
var cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
return true;
var cmpHealth = QueryMiragedInterface(ent, IID_Health);
return cmpHealth && cmpHealth.GetHitpoints() != 0;
};
/**
* Returns true if the target exists and needs to be killed before
* beginning to gather resources from it.
*/
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
if (!cmpResourceSupply.GetKillBeforeGather())
return false;
return this.TargetIsAlive(ent);
};
/**
* Returns the entity ID of the nearest resource supply where the given
* filter returns true, or undefined if none can be found.
* if target if given, the nearest is computed versus this target position.
* TODO: extend this to exclude resources that already have lots of
* gatherers.
*/
UnitAI.prototype.FindNearbyResource = function(filter, target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
var owner = cmpOwnership.GetOwner();
// We accept resources owned by Gaia or any player
var players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
var range = 64; // TODO: what's a sensible number?
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let entity = this.entity;
if (target)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
entity = target;
}
var nearby = cmpRangeManager.ExecuteQuery(entity, 0, range, players, IID_ResourceSupply);
return nearby.find(ent => {
if (!this.CanGather(ent) || !this.CheckTargetVisible(ent))
return false;
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
var amount = cmpResourceSupply.GetCurrentAmount();
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
// Remove "resource|" prefix from template names, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
return amount > 0 && cmpResourceSupply.IsAvailable(owner, this.entity) && filter(ent, type, template);
});
};
/**
* Returns the entity ID of the nearest resource dropsite that accepts
* the given type, or undefined if none can be found.
*/
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position)
+ let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return undefined;
let pos = cmpPosition.GetPosition2D();
let bestDropsite;
let bestDist = Infinity;
// Maximum distance a point on an obstruction can be from the center of the obstruction.
let maxDifference = 40;
// Find dropsites owned by this unit's player or allied ones if allowed.
let owner = cmpOwnership.GetOwner();
let cmpPlayer = QueryOwnerInterface(this.entity);
let players = cmpPlayer && cmpPlayer.HasSharedDropsites() ? cmpPlayer.GetMutualAllies() : [owner];
let nearbyDropsites = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
let isShip = Engine.QueryInterface(this.entity, IID_Identity).HasClass("Ship");
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
for (let dropsite of nearbyDropsites)
{
// Ships are unable to reach land dropsites and shouldn't attempt to do so.
if (isShip && !Engine.QueryInterface(dropsite, IID_Identity).HasClass("Naval"))
continue;
let cmpResourceDropsite = Engine.QueryInterface(dropsite, IID_ResourceDropsite);
if (!cmpResourceDropsite.AcceptsType(genericType) || !this.CheckTargetVisible(dropsite))
continue;
if (Engine.QueryInterface(dropsite, IID_Ownership).GetOwner() != owner && !cmpResourceDropsite.IsShared())
continue;
// The range manager sorts entities by the distance to their center,
// but we want the distance to the point where resources will be dropped off.
let dist = cmpObstructionManager.DistanceToPoint(dropsite, pos.x, pos.y);
if (dist == -1)
continue;
if (dist < bestDist)
{
bestDropsite = dropsite;
bestDist = dist;
}
else if (dist > bestDist + maxDifference)
break;
}
return bestDropsite;
};
/**
* Returns the entity ID of the nearest building that needs to be constructed,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyFoundation = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return undefined;
// Find buildings owned by this unit's player
var players = [cmpOwnership.GetOwner()];
var range = 64; // TODO: what's a sensible number?
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_Foundation);
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
return nearby.find(ent => !Engine.QueryInterface(ent, IID_Foundation).IsFinished());
};
/**
* Play a sound appropriate to the current entity.
*/
UnitAI.prototype.PlaySound = function(name)
{
// If we're a formation controller, use the sounds from our first member
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
var member = cmpFormation.GetPrimaryMember();
if (member)
PlaySound(name, member);
}
else
{
// Otherwise use our own sounds
PlaySound(name, this.entity);
}
};
/*
* Set a visualActor animation variant.
* By changing the animation variant, you can change animations based on unitAI state.
* If there are no specific variants or the variant doesn't exist in the actor,
* the actor fallbacks to any existing animation.
* @param type if present, switch to a specific animation variant.
*/
UnitAI.prototype.SetAnimationVariant = function(type)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetVariant("animationVariant", type);
return;
};
/*
* Reset the animation variant to default behavior
* Default behavior is to pick a resource-carrying variant if resources are being carried.
* Otherwise pick nothing in particular.
*/
UnitAI.prototype.SetDefaultAnimationVariant = function()
{
let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
{
this.SetAnimationVariant("");
return;
}
let type = cmpResourceGatherer.GetLastCarriedType();
if (type)
{
let typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
typename = "carry_" + type.specific;
this.SetAnimationVariant(typename);
return;
}
this.SetAnimationVariant("");
};
UnitAI.prototype.SelectAnimation = function(name, once = false, speed = 1.0)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
// Special case: the "move" animation gets turned into a special
// movement mode that deals with speeds and walk/run automatically
if (name == "move")
{
// Speed to switch from walking to running animations
cmpVisual.SelectMovementAnimation(this.GetWalkSpeed());
return;
}
cmpVisual.SelectAnimation(name, once, speed);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
UnitAI.prototype.StopMoving = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.StopMoving();
};
/**
* Generic dispatcher for other MoveTo functions.
* @param iid - Interface ID (optional) implementing GetRange
* @param type - Range type for the interface call
* @returns whether the move succeeded or failed.
*/
UnitAI.prototype.MoveTo = function(data, iid, type)
{
if (data.target)
{
if (data.min || data.max)
return this.MoveToTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
else if (!iid)
return this.MoveToTarget(data.target);
return this.MoveToTargetRange(data.target, iid, type);
}
else if (data.min || data.max)
return this.MoveToPointRange(data.x, data.z, data.min || -1, data.max || -1);
return this.MoveToPoint(data.x, data.z);
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
};
UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};
UnitAI.prototype.MoveToTarget = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
if (!this.CheckTargetVisible(target) || this.IsTurret())
return false;
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Move unit so we hope the target is in the attack range
* for melee attacks, this goes straight to the default range checks
* for ranged attacks, the parabolic range is used
*/
UnitAI.prototype.MoveToTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation())
return false;
}
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.MoveToTargetRange(target, IID_Attack, type);
if (!this.CheckTargetVisible(target))
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(type);
var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
var s = thisCmpPosition.GetPosition();
var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition.IsInWorld())
return false;
var t = targetCmpPosition.GetPosition();
// h is positive when I'm higher than the target
var h = s.y-t.y+range.elevationBonus;
// No negative roots please
if (h>-range.max/2)
var parabolicMaxRange = Math.sqrt(Math.square(range.max) + 2 * range.max * h);
else
// return false? Or hope you come close enough?
var parabolicMaxRange = 0;
//return false;
// the parabole changes while walking, take something in the middle
var guessedMaxRange = (range.max + parabolicMaxRange)/2;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion.MoveToTargetRange(target, range.min, guessedMaxRange))
return true;
// if that failed, try closer
return cmpUnitMotion.MoveToTargetRange(target, range.min, Math.min(range.max, parabolicMaxRange));
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
UnitAI.prototype.MoveToGarrisonRange = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
/**
* Generic dispatcher for other Check...Range functions.
* @param iid - Interface ID (optional) implementing GetRange
* @param type - Range type for the interface call
*/
UnitAI.prototype.CheckRange = function(data, iid, type)
{
if (data.target)
{
if (data.min || data.max)
return this.CheckTargetRangeExplicit(data.target, data.min || -1, data.max || -1);
else if (!iid)
return this.CheckTargetRangeExplicit(data.target, 0, 0);
return this.CheckTargetRange(data.target, iid, type);
}
else if (data.min || data.max)
return this.CheckPointRangeExplicit(data.x, data.z, data.min || -1, data.max || -1);
return this.CheckPointRangeExplicit(data.x, data.z, 0, 0);
};
UnitAI.prototype.CheckPointRangeExplicit = function(x, z, min, max)
{
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInPointRange(this.entity, x, z, min, max, false);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return false;
var range = cmpRanged.GetRange(type);
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
/**
* Check if the target is inside the attack range
* For melee attacks, this goes straigt to the regular range calculation
* For ranged attacks, the parabolic formula is used to accout for bigger ranges
* when the target is lower, and smaller ranges when the target is higher
*/
UnitAI.prototype.CheckTargetAttackRange = function(target, type)
{
// for formation members, the formation will take care of the range check
if (this.IsFormationMember())
{
var cmpFormationUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
if (cmpFormationUnitAI && cmpFormationUnitAI.IsAttackingAsFormation()
&& cmpFormationUnitAI.order.data.target == target)
return true;
}
var cmpFormation = Engine.QueryInterface(target, IID_Formation);
if (cmpFormation)
target = cmpFormation.GetClosestMember(this.entity);
if (type != "Ranged")
return this.CheckTargetRange(target, IID_Attack, type);
var targetCmpPosition = Engine.QueryInterface(target, IID_Position);
if (!targetCmpPosition || !targetCmpPosition.IsInWorld())
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var range = cmpAttack.GetRange(type);
var thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!thisCmpPosition.IsInWorld())
return false;
var s = thisCmpPosition.GetPosition();
var t = targetCmpPosition.GetPosition();
var h = s.y-t.y+range.elevationBonus;
var maxRangeSq = 2*range.max*(h + range.max/2);
if (maxRangeSq < 0)
return false;
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, Math.sqrt(maxRangeSq), false);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, min, max, false);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
range.max += cmpObstruction.GetUnitRadius()*1.5; // multiply by something larger than sqrt(2)
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
// Entities that are hidden and miraged are considered visible
var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
UnitAI.prototype.FaceTowardsTarget = function(target)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
var targetpos = cmpTargetPosition.GetPosition2D();
var angle = cmpPosition.GetPosition2D().angleTo(targetpos);
var rot = cmpPosition.GetRotation();
var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
if (Math.abs(delta) > 0.2)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.y);
}
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var halfvision = cmpVision.GetRange() / 2;
var pos = cmpPosition.GetPosition();
var heldPosition = this.heldPosition;
if (heldPosition === undefined)
heldPosition = { "x": pos.x, "z": pos.z };
return Math.euclidDistance2D(pos.x, pos.z, heldPosition.x, heldPosition.z) < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var range = cmpVision.GetRange();
var distance = DistanceBetweenEntities(this.entity, target);
return distance < range;
};
UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target, allowCapture);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
var target = ents.find(target => this.CanAttack(target));
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
var target = ents.find(target =>
this.CanAttack(target)
&& this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, this.GetBestAttackAgainst(target, true))
&& (this.GetStance().respondChaseBeyondVision || this.CheckTargetIsInVisionRange(target))
);
if (!target)
return false;
this.PushOrderFront("Attack", { "target": target, "force": false, "allowCapture": true });
return true;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents);
if (this.GetStance().respondFlee)
{
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
var ent = ents.find(ent => this.CanHeal(ent));
if (!ent)
return false;
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid, type)
{
// Forced orders shouldn't be interrupted.
if (force)
return false;
// If we are guarding/escorting, don't abandon as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
var cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return false;
}
// Stop if we're in hold-ground mode and it's too far from the holding point
if (this.GetStance().respondHoldGround)
{
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, type))
return true;
}
// Stop if it's left our vision range, unless we're especially persistent
if (!this.GetStance().respondChaseBeyondVision)
{
if (!this.CheckTargetIsInVisionRange(target))
return true;
}
// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
// and will continue moving to its last seen position and then stop)
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (this.IsTurret())
return false;
if (this.GetStance().respondChase)
return true;
// If we are guarding/escorting, chase at least as long as the guarded unit is in target range of the attacker
if (this.isGuardOf)
{
let cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
let cmpAttack = Engine.QueryInterface(target, IID_Attack);
if (cmpUnitAI && cmpAttack &&
cmpAttack.GetAttackTypes().some(type => cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, type)))
return true;
}
if (force)
return true;
return false;
};
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
this.UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.GetFormationTemplate = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetCurrentTemplateName(this.formationController) || "special/formations/null";
};
UnitAI.prototype.MoveIntoFormation = function(cmd)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
// Add new order to move into formation at the current position
this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
};
UnitAI.prototype.GetTargetPositions = function()
{
var targetPositions = [];
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "WalkAndFight":
case "WalkToPointRange":
case "MoveIntoFormation":
case "GatherNearPosition":
case "Patrol":
targetPositions.push(new Vector2D(order.data.x, order.data.z));
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Guard":
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
// Find the target unit's position
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return targetPositions;
targetPositions.push(cmpTargetPosition.GetPosition2D());
return targetPositions;
case "Stop":
return [];
default:
error("GetTargetPositions: Unrecognised order type '"+order.type+"'");
return [];
}
}
return targetPositions;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition2D();
var targetPositions = this.GetTargetPositions();
for (var i = 0; i < targetPositions.length; ++i)
{
distance += pos.distanceTo(targetPositions[i]);
// Remember this as the start position for the next order
pos = targetPositions[i];
}
// Return the total distance to the end of the order queue
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (this.expectedRoute)
this.expectedRoute = undefined;
if (queued)
this.PushOrder(type, data);
else
{
// May happen if an order arrives on the same turn the unit is garrisoned
// in that case, just forget the order as this will lead to an infinite loop
if (this.IsGarrisoned() && !this.IsTurret() && type != "Ungarrison")
return;
this.ReplaceOrder(type, data);
}
};
/**
* Adds guard/escort order to the queue, forced by the player.
*/
UnitAI.prototype.Guard = function(target, queued)
{
if (!this.CanGuard())
{
this.WalkToTarget(target, queued);
return;
}
// if we already had an old guard order, do nothing if the target is the same
// and the order is running, otherwise remove the previous order
if (this.isGuardOf)
{
if (this.isGuardOf == target && this.order && this.order.type == "Guard")
return;
else
this.RemoveGuard();
}
this.AddOrder("Guard", { "target": target, "force": false }, queued);
};
UnitAI.prototype.AddGuard = function(target)
{
if (!this.CanGuard())
return false;
var cmpGuard = Engine.QueryInterface(target, IID_Guard);
if (!cmpGuard)
return false;
// Do not allow to guard a unit already guarding
var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsGuardOf())
return false;
this.isGuardOf = target;
this.guardRange = cmpGuard.GetRange(this.entity);
cmpGuard.AddGuard(this.entity);
return true;
};
UnitAI.prototype.RemoveGuard = function()
{
if (!this.isGuardOf)
return;
let cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
if (cmpGuard)
cmpGuard.RemoveGuard(this.entity);
this.guardRange = undefined;
this.isGuardOf = undefined;
if (!this.order)
return;
if (this.order.type == "Guard")
this.UnitFsm.ProcessMessage(this, { "type": "RemoveGuard" });
else
for (let i = 1; i < this.orderQueue.length; ++i)
if (this.orderQueue[i].type == "Guard")
this.orderQueue.splice(i, 1);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.IsGuardOf = function()
{
return this.isGuardOf;
};
UnitAI.prototype.SetGuardOf = function(entity)
{
// entity may be undefined
this.isGuardOf = entity;
};
UnitAI.prototype.CanGuard = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Do not let a unit already guarded to guard. This would work in principle,
// but would clutter the gui with too much buttons to take all cases into account
var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
if (cmpGuard && cmpGuard.GetEntities().length)
return false;
return this.template.CanGuard == "true";
};
UnitAI.prototype.CanPatrol = function()
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
return this.IsFormationController() || this.template.CanPatrol == "true";
};
/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
{
if (this.expectedRoute && queued)
this.expectedRoute.push({ "x": x, "z": z });
else
this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds walk to point range order to queue, forced by the player.
*/
UnitAI.prototype.WalkToPointRange = function(x, z, min, max, queued)
{
this.AddOrder("Walk", { "x": x, "z": z, "min": min, "max": max, "force": true }, queued);
};
/**
* Adds stop order to queue, forced by the player.
*/
UnitAI.prototype.Stop = function(queued)
{
this.AddOrder("Stop", { "force": true }, queued);
};
/**
* Adds walk-to-target order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued);
};
/**
* Adds walk-and-fight order to queue, this only occurs in response
* to a player order, and so is forced.
* If targetClasses is given, only entities matching the targetClasses can be attacked.
*/
UnitAI.prototype.WalkAndFight = function(x, z, targetClasses, allowCapture = true, queued = false)
{
this.AddOrder("WalkAndFight", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
};
UnitAI.prototype.Patrol = function(x, z, targetClasses, allowCapture = true, queued = false)
{
if (!this.CanPatrol())
{
this.Walk(x, z, queued);
return;
}
this.AddOrder("Patrol", { "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "force": true }, queued);
};
/**
* Adds leave foundation order to queue, treated as forced.
*/
UnitAI.prototype.LeaveFoundation = function(target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere
// Ignore also the request if we are packing
if (this.order && (this.order.type == "LeaveFoundation" || (this.order.type == "Flee" && this.order.data.target == target) || this.IsPacking()))
return;
this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
};
/**
* Adds attack order to the queue, forced by the player.
*/
UnitAI.prototype.Attack = function(target, allowCapture = true, queued = false)
{
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
this.MoveToTargetRange(target, IID_Heal);
else
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
{
if (target == this.entity)
return;
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (this.IsGarrisoned())
this.AddOrder("Ungarrison", null, false);
};
/**
* Adds a garrison order for units that are already garrisoned in the garrison holder.
*/
UnitAI.prototype.Autogarrison = function(target)
{
this.isGarrisoned = true;
this.PushOrderFront("Garrison", { "target": target });
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Gather = function(target, queued)
{
this.PerformGather(target, queued, true);
};
/**
* Internal function to abstract the force parameter.
*/
UnitAI.prototype.PerformGather = function(target, queued, force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
var type;
var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (cmpResourceSupply)
type = cmpResourceSupply.GetType();
else
error("CanGather allowed gathering from invalid entity");
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(target);
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
// Remember the position of our target, if any, in case it disappears
// later and we want to head to its last known position
var lastPos = undefined;
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
lastPos = cmpPosition.GetPosition();
this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued);
};
/**
* Adds gather-near-position order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
{
// Remove "resource|" prefix from template name, if present.
if (template.indexOf("resource|") != -1)
template = template.slice(9);
if (this.IsFormationController() || Engine.QueryInterface(this.entity, IID_ResourceGatherer))
this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
else
this.AddOrder("Walk", { "x": x, "z": z, "force": false }, queued);
};
/**
* Adds heal order to the queue, forced by the player.
*/
UnitAI.prototype.Heal = function(target, queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Heal", { "target": target, "force": true }, queued);
};
/**
* Adds return resource order to the queue, forced by the player.
*/
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
/**
* Adds trade order to the queue. Either walk to the first market, or
* start a new route. Not forced, so it can be interrupted by attacks.
* The possible route may be given directly as a SetupTradeRoute argument
* if coming from a RallyPoint, or through this.expectedRoute if a user command.
*/
UnitAI.prototype.SetupTradeRoute = function(target, source, route, queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
// AI has currently no access to BackToWork
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer && cmpPlayer.IsAI() && !this.IsFormationController() &&
this.workOrders.length && this.workOrders[0].type == "Trade")
{
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets() &&
(cmpTrader.GetFirstMarket() == target && cmpTrader.GetSecondMarket() == source ||
cmpTrader.GetFirstMarket() == source && cmpTrader.GetSecondMarket() == target))
{
this.BackToWork();
return;
}
}
var marketsChanged = this.SetTargetMarket(target, source);
if (!marketsChanged)
return;
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (cmpTrader.HasBothMarkets())
{
let data = {
"target": cmpTrader.GetFirstMarket(),
"route": route,
"force": false
};
if (this.expectedRoute)
{
if (!route && this.expectedRoute.length)
data.route = this.expectedRoute.slice();
this.expectedRoute = undefined;
}
if (this.IsFormationController())
{
this.CallMemberFunction("AddOrder", ["Trade", data, queued]);
let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (cmpFormation)
cmpFormation.Disband();
}
else
this.AddOrder("Trade", data, queued);
}
else
{
if (this.IsFormationController())
this.CallMemberFunction("WalkToTarget", [cmpTrader.GetFirstMarket(), queued]);
else
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
this.expectedRoute = [];
}
};
UnitAI.prototype.SetTargetMarket = function(target, source)
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader)
return false;
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (this.IsFormationController())
this.CallMemberFunction("SetTargetMarket", [target, source]);
return marketsChanged;
};
UnitAI.prototype.SwitchMarketOrder = function(oldMarket, newMarket)
{
if (this.order && this.order.data && this.order.data.target && this.order.data.target == oldMarket)
this.order.data.target = newMarket;
};
UnitAI.prototype.MoveToMarket = function(targetMarket)
{
if (this.waypoints && this.waypoints.length > 1)
{
let point = this.waypoints.pop();
return this.MoveToPoint(point.x, point.z) || this.MoveToMarket(targetMarket);
}
this.waypoints = undefined;
return this.MoveToTarget(targetMarket);
};
UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (!this.CheckTargetRange(currentMarket, IID_Trader))
{
if (!this.MoveToMarket(currentMarket)) // If the current market is not reached try again
this.StopTrading();
return;
}
let cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
let nextMarket = cmpTrader.PerformTrade(currentMarket);
let amount = cmpTrader.GetGoods().amount;
if (!nextMarket || !amount || !amount.traderGain)
{
this.StopTrading();
return;
}
this.order.data.target = nextMarket;
if (this.order.data.route && this.order.data.route.length)
{
this.waypoints = this.order.data.route.slice();
if (this.order.data.target == cmpTrader.GetSecondMarket())
this.waypoints.reverse();
this.waypoints.unshift(null); // additionnal dummy point for the market
}
if (this.MoveToMarket(nextMarket)) // We've started walking to the next market
this.SetNextState("APPROACHINGMARKET");
else
this.StopTrading();
};
UnitAI.prototype.MarketRemoved = function(market)
{
if (this.order && this.order.data && this.order.data.target && this.order.data.target == market)
this.UnitFsm.ProcessMessage(this, { "type": "TradingCanceled", "market": market });
};
UnitAI.prototype.StopTrading = function()
{
this.StopMoving();
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.StopTrading();
};
/**
* Adds repair/build order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued);
};
/**
* Adds flee order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.Flee = function(target, queued)
{
this.AddOrder("Flee", { "target": target, "force": false }, queued);
};
/**
* Adds cheer order to the queue. Forced so it won't be interrupted by attacks.
*/
UnitAI.prototype.Cheer = function()
{
this.AddOrder("Cheering", { "force": true }, false);
};
UnitAI.prototype.Pack = function(queued)
{
// Check that we can pack
if (this.CanPack())
this.AddOrder("Pack", { "force": true }, queued);
};
UnitAI.prototype.Unpack = function(queued)
{
// Check that we can unpack
if (this.CanUnpack())
this.AddOrder("Unpack", { "force": true }, queued);
};
UnitAI.prototype.CancelPack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && !cmpPack.IsPacked())
this.AddOrder("CancelPack", { "force": true }, queued);
};
UnitAI.prototype.CancelUnpack = function(queued)
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
if (cmpPack && cmpPack.IsPacking() && cmpPack.IsPacked())
this.AddOrder("CancelUnpack", { "force": true }, queued);
};
UnitAI.prototype.SetStance = function(stance)
{
if (g_Stances[stance])
{
this.stance = stance;
Engine.PostMessage(this.entity, MT_UnitStanceChanged, { "to": this.stance });
}
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
};
UnitAI.prototype.SwitchToStance = function(stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
this.SetStance(stance);
// Stop moving if switching to stand ground
// TODO: Also stop existing orders in a sensible way
if (stance == "standground")
this.StopMoving();
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
};
UnitAI.prototype.SetTurretStance = function()
{
this.previousStance = undefined;
if (this.GetStance().respondStandGround)
return;
for (let stance in g_Stances)
{
if (!g_Stances[stance].respondStandGround)
continue;
this.previousStance = this.GetStanceName();
this.SwitchToStance(stance);
return;
}
};
UnitAI.prototype.ResetTurretStance = function()
{
if (!this.previousStance)
return;
this.SwitchToStance(this.previousStance);
this.previousStance = undefined;
};
/**
* Resets losRangeQuery, and if there are some targets in range that we can
* attack then we start attacking and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewTargets = function()
{
if (!this.losRangeQuery)
return false;
if (!this.GetStance().targetVisibleEnemies)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.AttackEntitiesByPreference(cmpRangeManager.ResetActiveQuery(this.losRangeQuery));
};
UnitAI.prototype.FindWalkAndFightTargets = function()
{
if (this.IsFormationController())
{
var cmpUnitAI;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
for (var ent of cmpFormation.members)
{
if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI)))
continue;
var targets = cmpUnitAI.GetTargetsFromUnit();
for (var targ of targets)
{
if (!cmpUnitAI.CanAttack(targ))
continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
var targetClasses = this.order.data.targetClasses;
if (targetClasses.attack && cmpIdentity
&& !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
continue;
if (targetClasses.avoid && cmpIdentity
&& MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
continue;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
continue;
}
this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture });
return true;
}
}
return false;
}
var targets = this.GetTargetsFromUnit();
for (var targ of targets)
{
if (!this.CanAttack(targ))
continue;
if (this.order.data.targetClasses)
{
var cmpIdentity = Engine.QueryInterface(targ, IID_Identity);
var targetClasses = this.order.data.targetClasses;
if (cmpIdentity && targetClasses.attack
&& !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack))
continue;
if (cmpIdentity && targetClasses.avoid
&& MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid))
continue;
// Only used by the AIs to prevent some choices of targets
if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ])
continue;
}
this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture });
return true;
}
// healers on a walk-and-fight order should heal injured units
if (this.IsHealer())
return this.FindNewHealTargets();
return false;
};
UnitAI.prototype.GetTargetsFromUnit = function()
{
if (!this.losRangeQuery)
return [];
if (!this.GetStance().targetVisibleEnemies)
return [];
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return [];
var attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var entities = cmpRangeManager.ResetActiveQuery(this.losRangeQuery);
var targets = entities.filter(function(v) { return cmpAttack.CanAttack(v) && attackfilter(v); })
.sort(function(a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); });
return targets;
};
/**
* Resets losHealRangeQuery, and if there are some targets in range that we can heal
* then we start healing and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewHealTargets = function()
{
if (!this.losHealRangeQuery)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return this.RespondToHealableEntities(cmpRangeManager.ResetActiveQuery(this.losHealRangeQuery));
};
UnitAI.prototype.GetQueryRange = function(iid)
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
ret.min = range.min;
ret.max = Math.min(range.max, cmpVision.GetRange());
}
else if (this.GetStance().respondChase)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
else if (this.GetStance().respondHoldGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetFullAttackRange();
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var vision = cmpVision.GetRange();
ret.max = Math.min(range.max + vision / 2, vision);
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
UnitAI.prototype.GetSelectableStances = function()
{
if (this.IsTurret())
return [];
return Object.keys(g_Stances).filter(key => g_Stances[key].selectable);
};
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
/*
* Make the unit walk at its normal pace.
*/
UnitAI.prototype.ResetSpeedMultiplier = function()
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(1);
};
UnitAI.prototype.SetSpeedMultiplier = function(speed)
{
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetSpeedMultiplier(speed);
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = {"x": x, "z": z};
};
UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
};
UnitAI.prototype.GetHeldPosition = function()
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false);
return true;
}
return false;
};
//// Helper functions ////
UnitAI.prototype.CanAttack = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(target);
};
UnitAI.prototype.CanGarrison = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
// Verify that the target is owned by this entity's player or a mutual ally of this player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Don't let animals garrison for now
// (If we want to support that, we'll need to change Order.Garrison so it
// doesn't move the animal into an INVIDIDUAL.* state)
if (this.IsAnimal())
return false;
return true;
};
UnitAI.prototype.CanGather = function(target)
{
if (this.IsTurret())
return false;
// The target must be a valid resource supply, or the mirage of one.
var cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that we can gather from this target
if (!cmpResourceGatherer.GetTargetGatherRate(target))
return false;
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
// No need to call "cmpResourceSupply.IsAvailable()" either because that
// would cause units to walk to full entities instead of choosing another one
// nearby to gather from, which is undesirable.
return true;
};
UnitAI.prototype.CanHeal = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Heal commands
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
// Verify that the target is owned by the same player as the entity or of an ally
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Verify that the target is not unhealable (or at max health)
var cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.IsUnhealable())
return false;
// Verify that the target has no unhealable class
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetUnhealableClasses()))
return false;
// Verify that the target is a healable class
if (MatchesClassList(cmpIdentity.GetClassesList(), cmpHeal.GetHealableClasses()))
return true;
return false;
};
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to ReturnResource commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that the target is a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
// Verify that we are carrying some resources,
// and can return our current resource to this target
var type = cmpResourceGatherer.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
// Verify that the dropsite is owned by this entity's player (or a mutual ally's if allowed)
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return true;
var cmpPlayer = QueryOwnerInterface(this.entity);
return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() &&
cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanTrade = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Trade commands
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
return cmpTrader && cmpTrader.CanTrade(target);
};
UnitAI.prototype.CanRepair = function(target)
{
if (this.IsTurret())
return false;
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return false;
// Verify that the target can be either built or repaired
var cmpFoundation = QueryMiragedInterface(target, IID_Foundation);
var cmpRepairable = Engine.QueryInterface(target, IID_Repairable);
if (!cmpFoundation && !cmpRepairable)
return false;
// Verify that the target is owned by an ally of this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
return cmpOwnership && IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target);
};
UnitAI.prototype.CanPack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && !cmpPack.IsPacking() && !cmpPack.IsPacked();
};
UnitAI.prototype.CanUnpack = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && !cmpPack.IsPacking() && cmpPack.IsPacked();
};
UnitAI.prototype.IsPacking = function()
{
var cmpPack = Engine.QueryInterface(this.entity, IID_Pack);
return cmpPack && cmpPack.IsPacking();
};
//// Formation specific functions ////
UnitAI.prototype.IsAttackingAsFormation = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttackAsFormation()
&& this.GetCurrentState() == "FORMATIONCONTROLLER.COMBAT.ATTACKING";
};
//// Animal specific functions ////
UnitAI.prototype.MoveRandomly = function(distance)
{
// To minimize drift all across the map, animals describe circles
// approximated by polygons.
// And to avoid getting stuck in obstacles or narrow spaces, each side
// of the polygon is obtained by trying to go away from a point situated
// half a meter backwards of the current position, after rotation.
// We also add a fluctuation on the length of each side of the polygon (dist)
// which, in addition to making the move more random, helps escaping narrow spaces
// with bigger values of dist.
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpPosition || !cmpPosition.IsInWorld() || !cmpUnitMotion)
return;
let pos = cmpPosition.GetPosition();
let ang = cmpPosition.GetRotation().y;
if (!this.roamAngle)
{
this.roamAngle = (randBool() ? 1 : -1) * Math.PI / 6;
ang -= this.roamAngle / 2;
this.startAngle = ang;
}
else if (Math.abs((ang - this.startAngle + Math.PI) % (2 * Math.PI) - Math.PI) < Math.abs(this.roamAngle / 2))
this.roamAngle *= randBool() ? 1 : -1;
let halfDelta = randFloat(this.roamAngle / 4, this.roamAngle * 3 / 4);
// First half rotation to decrease the impression of immediate rotation
ang += halfDelta;
cmpUnitMotion.FaceTowardsPoint(pos.x + 0.5 * Math.sin(ang), pos.z + 0.5 * Math.cos(ang));
// Then second half of the rotation
ang += halfDelta;
let dist = randFloat(0.5, 1.5) * distance;
cmpUnitMotion.MoveToPointRange(pos.x - 0.5 * Math.sin(ang), pos.z - 0.5 * Math.cos(ang), dist, -1);
};
UnitAI.prototype.SetFacePointAfterMove = function(val)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion)
cmpMotion.SetFacePointAfterMove(val);
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
if (!ents.length)
return false;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
var attackfilter = function(e) {
var cmpOwnership = Engine.QueryInterface(e, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() > 0)
return true;
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
};
let entsByPreferences = {};
let preferences = [];
let entsWithoutPref = [];
for (let ent of ents)
{
if (!attackfilter(ent))
continue;
let pref = cmpAttack.GetPreference(ent);
if (pref === null || pref === undefined)
entsWithoutPref.push(ent);
else if (!entsByPreferences[pref])
{
preferences.push(pref);
entsByPreferences[pref] = [ent];
}
else
entsByPreferences[pref].push(ent);
}
if (preferences.length)
{
preferences.sort((a, b) => a - b);
for (let pref of preferences)
if (this.RespondToTargetedEntities(entsByPreferences[pref]))
return true;
}
return this.RespondToTargetedEntities(entsWithoutPref);
};
/**
* Call obj.funcname(args) on UnitAI components of all formation members.
*/
UnitAI.prototype.CallMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return;
cmpFormation.GetMembers().forEach(ent => {
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
/**
* Call obj.functname(args) on UnitAI components of all formation members,
* and return true if all calls return true.
*/
UnitAI.prototype.TestAllMemberFunction = function(funcname, args)
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
if (!cmpFormation)
return false;
return cmpFormation.GetMembers().every(ent => {
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI[funcname].apply(cmpUnitAI, args);
});
};
UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec);
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_EntityLimits.js (revision 22419)
@@ -1,203 +1,203 @@
Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
Engine.LoadComponentScript("interfaces/Player.js");
Engine.LoadComponentScript("EntityLimits.js");
let template ={
"Limits": {
"Tower": 5,
"Wonder": 1,
"Hero": 2,
"Champion": 1
},
"LimitChangers": {
"Tower": { "Monument": 1 }
},
"LimitRemovers": {
"Tower": { "RequiredTechs": { "_string": "TechA" } },
"Hero": { "RequiredClasses": { "_string": "Aegis" } }
}
};
AddMock(10, IID_Player, {
"GetPlayerID": id => 1
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"PushNotification": () => {}
});
let cmpEntityLimits = ConstructComponent(10, "EntityLimits", template);
// Test getters
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimitChangers(), { "Tower": { "Monument": 1 } });
// Test training restrictions
TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero"));
TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero", 1));
TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero", 2));
for (let ent = 60; ent < 63; ++ent)
{
AddMock(ent, IID_TrainingRestrictions, {
"GetCategory": () => "Hero"
});
}
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 60, "from": INVALID_PLAYER, "to": 1 });
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 61, "from": 2, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 2, "Champion": 0 });
TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero"));
TS_ASSERT(!cmpEntityLimits.AllowedToTrain("Hero", 1));
// Restrictions can be enforced
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 62, "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 3, "Champion": 0 });
for (let ent = 60; ent < 63; ++ent)
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": ent, "from": 1, "to": INVALID_PLAYER });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
// Test building restrictions
AddMock(70, IID_BuildRestrictions, {
"GetCategory": () => "Wonder"
});
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 70, "from": 3, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 1, "Hero": 0, "Champion": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
// AllowedToBuild is used after foundation placement, which are meant to be replaced
TS_ASSERT(cmpEntityLimits.AllowedToBuild("Wonder"));
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 70, "from": 1, "to": INVALID_PLAYER });
// Test limit changers
AddMock(80, IID_Identity, {
"GetClassesList": () => ["Monument"]
});
AddMock(81, IID_Identity, {
"GetClassesList": () => ["Monument"]
});
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 80, "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5 + 1, "Wonder": 1, "Hero": 2, "Champion": 1 });
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": 1, "to": INVALID_PLAYER });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
// Foundations don't change limits
AddMock(81, IID_Foundation, {});
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 81, "from": 1, "to": INVALID_PLAYER });
// Test limit removers by classes
AddMock(90, IID_Identity, {
"GetClassesList": () => ["Aegis"]
});
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 90, "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": undefined, "Champion": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
AddMock(91, IID_TrainingRestrictions, {
"GetCategory": () => "Hero"
});
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 91, "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": undefined, "Champion": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 1, "Champion": 0 });
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 90, "from": 1, "to": INVALID_PLAYER });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 1, "Champion": 0 });
// Edge case
AddMock(92, IID_TrainingRestrictions, {
"GetCategory": () => "Hero"
});
AddMock(92, IID_Identity, {
"GetClassesList": () => ["Aegis"]
});
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 92, "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": undefined, "Champion": 1 });
TS_ASSERT(cmpEntityLimits.AllowedToTrain("Hero", 157));
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 2, "Champion": 0 });
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 91, "from": 1, "to": INVALID_PLAYER });
cmpEntityLimits.OnGlobalOwnershipChanged({ "entity": 92, "from": 1, "to": INVALID_PLAYER });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
// Test AllowedToReplace
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetTemplate": name => {
switch (name)
{
case "templateA":
return { "TrainingRestrictions": { "Category": "Champion" } };
case "templateB":
return { "TrainingRestrictions": { "Category": "Hero" } };
case "templateC":
return { "BuildRestrictions": { "Category": "Wonder" } };
case "templateD":
return { "BuildRestrictions": { "Category": "Tower" } };
default:
return null;
}
},
"GetCurrentTemplateName": id => {
switch (id)
{
case 100:
return "templateA";
case 101:
return "templateB";
case 102:
return "templateC";
case 103:
return "templateD";
default:
return null;
}
}
});
-cmpEntityLimits.ChangeCount("Champion", 1)
-TS_ASSERT(cmpEntityLimits.AllowedToReplace(100, "templateA"))
-TS_ASSERT(!cmpEntityLimits.AllowedToReplace(101, "templateA"))
-cmpEntityLimits.ChangeCount("Champion", -1)
-
-cmpEntityLimits.ChangeCount("Tower", 5)
-TS_ASSERT(!cmpEntityLimits.AllowedToReplace(102, "templateD"))
-TS_ASSERT(cmpEntityLimits.AllowedToReplace(103, "templateD"))
-cmpEntityLimits.ChangeCount("Tower", -5)
+cmpEntityLimits.ChangeCount("Champion", 1);
+TS_ASSERT(cmpEntityLimits.AllowedToReplace(100, "templateA"));
+TS_ASSERT(!cmpEntityLimits.AllowedToReplace(101, "templateA"));
+cmpEntityLimits.ChangeCount("Champion", -1);
+
+cmpEntityLimits.ChangeCount("Tower", 5);
+TS_ASSERT(!cmpEntityLimits.AllowedToReplace(102, "templateD"));
+TS_ASSERT(cmpEntityLimits.AllowedToReplace(103, "templateD"));
+cmpEntityLimits.ChangeCount("Tower", -5);
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetCounts(), { "Tower": 0, "Wonder": 0, "Hero": 0, "Champion": 0 });
// Test limit removers by tech
cmpEntityLimits.UpdateLimitsFromTech("TechB");
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": 5, "Wonder": 1, "Hero": 2, "Champion": 1 });
cmpEntityLimits.UpdateLimitsFromTech("TechA");
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": undefined, "Wonder": 1, "Hero": 2, "Champion": 1 });
cmpEntityLimits.UpdateLimitsFromTech("TechA");
TS_ASSERT_UNEVAL_EQUALS(cmpEntityLimits.GetLimits(), { "Tower": undefined, "Wonder": 1, "Hero": 2, "Champion": 1 });
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Vector.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Vector.js (revision 22418)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Vector.js (revision 22419)
@@ -1,309 +1,309 @@
var brokenVector = {
"lengthSquared": () => "incompatible vector"
};
// Test Vector2D add, mult, distance
{
let v1 = new Vector2D();
TS_ASSERT_EQUALS(v1.x, 0);
TS_ASSERT_EQUALS(v1.y, 0);
let v2 = new Vector2D(3, 4);
TS_ASSERT_EQUALS(v1.distanceTo(v2), 5);
v2.mult(3);
TS_ASSERT_EQUALS(v2.x, 9);
TS_ASSERT_EQUALS(v2.y, 12);
v2.add(new Vector2D(1, 2));
TS_ASSERT_EQUALS(v2.x, 10);
TS_ASSERT_EQUALS(v2.y, 14);
}
// Test Vector2D isEqualTo
{
TS_ASSERT(Vector2D.isEqualTo(new Vector2D(4, 5), new Vector2D(2, 2.5).mult(2)));
}
// Test Vector2D normalization
{
let v3 = new Vector2D(0, 5).normalize();
TS_ASSERT_EQUALS(v3.x, 0);
TS_ASSERT_EQUALS(v3.y, 1);
v3.set(-8, 0).normalize();
TS_ASSERT_EQUALS(v3.x, -1);
TS_ASSERT_EQUALS(v3.y, 0);
}
// Test Vector2D rotation
{
let v4 = new Vector2D(2, -5).rotate(4 * Math.PI);
TS_ASSERT_EQUALS(v4.x, 2);
TS_ASSERT_EQUALS(v4.y, -5);
v4.rotate(Math.PI);
TS_ASSERT_EQUALS(v4.x, -2);
TS_ASSERT_EQUALS(v4.y, 5);
// Result of rotating (1, 0)
let unitCircle = [
{
"angle": Math.PI / 2,
"x": 0,
"y": 1
},
{
"angle": Math.PI / 3,
"x": 1/2,
"y": Math.sqrt(3) / 2
},
{
"angle": Math.PI / 4,
"x": Math.sqrt(2) / 2,
"y": Math.sqrt(2) / 2
},
{
"angle": Math.PI / 6,
"x": Math.sqrt(3) / 2,
"y": 1/2
}
];
let epsilon = 0.00000001;
for (let expectedVector of unitCircle)
{
let computedVector = new Vector2D(1, 0).rotate(-expectedVector.angle);
TS_ASSERT_EQUALS_APPROX(computedVector.x, expectedVector.x, epsilon);
TS_ASSERT_EQUALS_APPROX(computedVector.y, expectedVector.y, epsilon);
}
}
// Test Vector2D rotation further
{
let epsilon = 0.00000001;
for (let i = 0; i <= 128; ++i)
{
let angle = i / 128 * Math.PI;
let vec1 = new Vector2D(Math.cos(angle), Math.sin(angle));
let vec2 = new Vector2D(1, 0).rotate(-angle);
TS_ASSERT_EQUALS_APPROX(vec1.x, vec2.x, epsilon);
TS_ASSERT_EQUALS_APPROX(vec1.y, vec2.y, epsilon);
let vec3 = new Vector2D(Math.sin(angle), Math.cos(angle));
let vec4 = new Vector2D(0, 1).rotate(angle);
TS_ASSERT_EQUALS_APPROX(vec3.x, vec4.x, epsilon);
TS_ASSERT_EQUALS_APPROX(vec3.y, vec4.y, epsilon);
}
}
// Test Vector2D rotation around a center
{
let epsilon = 0.00000001;
let v1 = new Vector2D(-4, 8).rotateAround(Math.PI / 3, new Vector2D(-1, -3));
TS_ASSERT_EQUALS_APPROX(v1.x, 7.02627944, epsilon);
TS_ASSERT_EQUALS_APPROX(v1.y, 5.09807617, epsilon);
}
// Test Vector2D dot product
{
TS_ASSERT_EQUALS(new Vector2D(2, 3).dot(new Vector2D(4, 5)), 23);
}
// Test Vector2D cross product
{
TS_ASSERT_EQUALS(new Vector2D(3, 5).cross(new Vector2D(-4, -1/3)), 19);
}
// Test Vector2D length and compareLength
{
TS_ASSERT_EQUALS(new Vector2D(20, 21).length(), 29);
let v5 = new Vector2D(10, 20);
TS_ASSERT_EQUALS(v5.compareLength(new Vector2D(5, 8)), 1);
TS_ASSERT_EQUALS(v5.compareLength(new Vector2D(500, 800)), -1);
TS_ASSERT_EQUALS(v5.compareLength(new Vector2D(10, 20)), 0);
TS_ASSERT(isNaN(v5.compareLength(brokenVector)));
}
// Test Vector2D rotation furthermore
{
let epsilon = 0.00000001;
let v5 = new Vector2D(10, 20);
let v6 = v5.clone();
TS_ASSERT_EQUALS(v5.x, v6.x);
TS_ASSERT_EQUALS(v5.y, v6.y);
TS_ASSERT(Math.abs(v5.dot(v6.rotate(Math.PI / 2))) < epsilon);
}
// Test Vector2D perpendicular
{
let v7 = new Vector2D(4, 5).perpendicular();
TS_ASSERT_EQUALS(v7.x, -5);
TS_ASSERT_EQUALS(v7.y, 4);
let v8 = new Vector2D(0, 0).perpendicular();
TS_ASSERT_EQUALS(v8.x, 0);
TS_ASSERT_EQUALS(v8.y, 0);
}
// Test Vector2D angleTo
{
let v1 = new Vector2D(1, 1);
let v2 = new Vector2D(1, 3);
let v3 = new Vector2D(3, 1);
TS_ASSERT_EQUALS(v1.angleTo(v2), 0);
TS_ASSERT_EQUALS(v1.angleTo(v3), Math.PI / 2);
TS_ASSERT_EQUALS(v3.angleTo(v2), -Math.PI / 4);
}
// Test Vector2D min / max functions
{
let v1 = new Vector2D(-1, 8);
let v2 = new Vector2D(-2, -1);
- let min = Vector2D.min(v1, v2)
+ let min = Vector2D.min(v1, v2);
TS_ASSERT_EQUALS(min.x, -2);
TS_ASSERT_EQUALS(min.y, -1);
- let max = Vector2D.max(v1, v2)
+ let max = Vector2D.max(v1, v2);
TS_ASSERT_EQUALS(max.x, -1);
TS_ASSERT_EQUALS(max.y, 8);
}
// Test Vector2D list functions
{
let list = [
new Vector2D(),
new Vector2D(-1, 5),
new Vector2D(89, -123),
new Vector2D(55, 66),
];
let sum = Vector2D.sum(list);
TS_ASSERT_EQUALS(sum.x, 143);
TS_ASSERT_EQUALS(sum.y, -52);
let avg = Vector2D.average(list);
TS_ASSERT_EQUALS(avg.x, 35.75);
TS_ASSERT_EQUALS(avg.y, -13);
}
// Test Vector2D round
{
let v1 = new Vector2D(-4.5, 8.2).round();
TS_ASSERT_EQUALS(v1.x, -4);
TS_ASSERT_EQUALS(v1.y, 8);
let v2 = new Vector2D(NaN, NaN).round();
TS_ASSERT(isNaN(v2.x));
TS_ASSERT(isNaN(v2.y));
let v3 = new Vector2D().round();
TS_ASSERT_EQUALS(v3.x, 0);
TS_ASSERT_EQUALS(v3.y, 0);
}
// Test Vector2D floor
{
let v1 = new Vector2D(-4.5, 8.9).floor();
TS_ASSERT_EQUALS(v1.x, -5);
TS_ASSERT_EQUALS(v1.y, 8);
let v2 = new Vector2D().floor();
TS_ASSERT_EQUALS(v2.x, 0);
TS_ASSERT_EQUALS(v2.y, 0);
}
// Vector3D tests
// Test Vector3D isEqualTo
{
TS_ASSERT(Vector3D.isEqualTo(new Vector3D(2, 5, 14), new Vector3D(0, 5, 10).add(new Vector3D(2, 0, 4))));
}
// Test Vector3D distance and compareLength
{
let v1 = new Vector3D(2, 5, 14);
TS_ASSERT_EQUALS(v1.distanceTo(new Vector3D()), 15);
TS_ASSERT(isNaN(v1.compareLength(brokenVector)));
}
// Test Vector3D mult
{
let v2 = new Vector3D(2, 5, 14).mult(3);
TS_ASSERT_EQUALS(v2.x, 6);
TS_ASSERT_EQUALS(v2.y, 15);
TS_ASSERT_EQUALS(v2.z, 42);
}
// Test Vector3D dot product
{
TS_ASSERT_EQUALS(new Vector3D(1, 2, 3).dot(new Vector3D(4, 5, 6)), 32);
}
// Test Vector3D clone
{
let v3 = new Vector3D(9, 10, 11);
let v4 = v3.clone();
TS_ASSERT_EQUALS(v3.x, v4.x);
TS_ASSERT_EQUALS(v3.y, v4.y);
TS_ASSERT_EQUALS(v3.z, v4.z);
}
// Test Vector3D cross product
{
let v5 = new Vector3D(1, 2, 3).cross(new Vector3D(4, 5, 6));
TS_ASSERT_EQUALS(v5.x, -3);
TS_ASSERT_EQUALS(v5.y, 6);
TS_ASSERT_EQUALS(v5.z, -3);
}
// Test Vector3D horizAngleTo
{
let v1 = new Vector3D(1, 1, 1);
let v2 = new Vector3D(1, 2, 3);
let v3 = new Vector3D(3, 10, 1);
TS_ASSERT_EQUALS(v1.horizAngleTo(v2), 0);
TS_ASSERT_EQUALS(v1.horizAngleTo(v3), Math.PI / 2);
TS_ASSERT_EQUALS(v3.horizAngleTo(v2), -Math.PI / 4);
}
// Test Vector3D round
{
let v1 = new Vector3D(-1.1, 2.2, 3.3).round();
TS_ASSERT_EQUALS(v1.x, -1);
TS_ASSERT_EQUALS(v1.y, 2);
TS_ASSERT_EQUALS(v1.z, 3);
let v2 = new Vector3D(NaN, NaN, NaN).round();
TS_ASSERT(isNaN(v2.x));
TS_ASSERT(isNaN(v2.y));
TS_ASSERT(isNaN(v2.z));
let v3 = new Vector3D().round();
TS_ASSERT_EQUALS(v3.x, 0);
TS_ASSERT_EQUALS(v3.y, 0);
TS_ASSERT_EQUALS(v3.z, 0);
let v4 = new Vector3D(71.8, 73.4, 73.89).round();
TS_ASSERT_EQUALS(v4.x, 72);
TS_ASSERT_EQUALS(v4.y, 73);
TS_ASSERT_EQUALS(v4.z, 74);
}
// Test Vector3D floor
{
let v1 = new Vector3D(-1.1, 2.2, 3.9).floor();
TS_ASSERT_EQUALS(v1.x, -2);
TS_ASSERT_EQUALS(v1.y, 2);
TS_ASSERT_EQUALS(v1.z, 3);
let v3 = new Vector3D().floor();
TS_ASSERT_EQUALS(v3.x, 0);
TS_ASSERT_EQUALS(v3.y, 0);
TS_ASSERT_EQUALS(v3.z, 0);
}