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(""); 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("
" + 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); }