Index: ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 26519)
+++ ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 26520)
@@ -1,302 +1,303 @@
{
"Title": "Programming",
"Content": [
{
"Title": "Programming managers",
"List": [
{ "nick": "Acumen", "name": "Stuart Walpole" },
{ "nick": "Dak Lozar", "name": "Dave Loeser" },
{ "nick": "h20", "name": "Daniel Wilhelm" },
{ "nick": "Janwas", "name": "Jan Wassenberg" },
{ "nick": "Raj", "name": "Raj Sharma" }
]
},
{
"Subtitle": "Special thanks to",
"List": [
{ "nick": "leper", "name": "Georg Kilzer" },
{ "nick": "Ykkrosh", "name": "Philip Taylor" }
]
},
{
"List": [
{ "nick": "01d55" },
{ "nick": "aBothe", "name": "Alexander Bothe" },
{ "nick": "Acumen", "name": "Stuart Walpole" },
{ "nick": "adrian", "name": "Adrian Boguszewszki" },
{ "name": "Adrian Fatol" },
{ "nick": "AI-Amsterdam" },
{ "nick": "Alan", "name": "Alan Kemp" },
{ "nick": "Alex", "name": "Alexander Yakobovich" },
{ "nick": "alpha123", "name": "Peter P. Cannici" },
+ { "nick": "alre" },
{ "nick": "Ampaex", "name": "Antonio Vazquez" },
{ "name": "André Puel" },
{ "nick": "andy5995", "name": "Andy Alt" },
{ "nick": "Angen" },
{ "nick": "Arfrever", "name": "Arfrever Frehtes Taifersar Arahesis" },
{ "nick": "ArnH", "name": "Arno Hemelhof" },
{ "nick": "Aurium", "name": "Aurélio Heckert" },
{ "nick": "azayrahmad", "name": "Aziz Rahmad" },
{ "nick": "badmadblacksad", "name": "Martin F" },
{ "nick": "badosu", "name": "Amadeus Folego" },
{ "nick": "bb", "name": "Bouke Jansen" },
{ "nick": "Bellaz89", "name": "Andrea Bellandi" },
{ "nick": "Ben", "name": "Ben Vinegar" },
{ "nick": "Bird" },
{ "nick": "Blue", "name": "Richard Welsh" },
{ "nick": "bmwiedemann" },
{ "nick": "boeseRaupe", "name": "Michael Kluge" },
{ "nick": "bog_dan_ro", "name": "BogDan Vatra" },
{ "nick": "Bonk", "name": "Christopher Ebbert" },
{ "nick": "Boudica" },
{ "nick": "Caius", "name": "Lars Kemmann" },
{ "nick": "Calefaction", "name": "Matt Holmes" },
{ "nick": "Calvinh", "name": "Carl-Johan Höiby" },
{ "nick": "causative", "name": "Bart Parkis" },
{ "name": "Cédric Houbart" },
{ "nick": "Ceres" },
{ "nick": "Chakakhan", "name": "Kenny Long" },
{ "nick": "Clockwork-Muse", "name": "Stephen A. Imhoff" },
{ "nick": "cpc", "name": "Clément Pit-Claudel" },
{ "nick": "Cracker78", "name": "Chad Heim" },
{ "nick": "Crynux", "name": "Stephen J. Fewer" },
{ "nick": "cwprogger" },
{ "nick": "cygal", "name": "Quentin Pradet" },
{ "nick": "Dak Lozar", "name": "Dave Loeser" },
{ "nick": "dalerank", "name": "Sergey Kushnirenko" },
{ "nick": "dan", "name": "Dan Strandberg" },
{ "nick": "DanCar", "name": "Daniel Cardenas" },
{ "nick": "danger89", "name": "Melroy van den Berg" },
{ "name": "Daniel Trevitz" },
{ "nick": "Dariost", "name": "Dario Ostuni" },
{ "nick": "Dave", "name": "David Protasowski" },
{ "name": "David Marshall" },
{ "nick": "dax", "name": "Dacian Fiordean" },
{ "nick": "deebee", "name": "Deepak Anthony" },
{ "nick": "Deiz" },
{ "nick": "Dietger", "name": "Dietger van Antwerpen" },
{ "nick": "DigitalSeraphim", "name": "Nick Owens" },
{ "nick": "dp304" },
{ "nick": "dpiquet", "name": "Damien Piquet" },
{ "nick": "dumbo" },
{ "nick": "Dunedan", "name": "Daniel Roschka" },
{ "nick": "dvangennip", "name": "Doménique" },
{ "nick": "DynamoFox" },
{ "nick": "Echelon9", "name": "Rhys Kidd" },
{ "nick": "echotangoecho" },
{ "nick": "eihrul", "name": "Lee Salzman" },
{ "nick": "elexis", "name": "Alexander Heinsius" },
{ "nick": "EmjeR", "name": "Matthijs de Rijk" },
{ "nick": "EMontana" },
{ "nick": "ericb" },
{ "nick": "evanssthomas", "name": "Evans Thomas" },
{ "nick": "Evulant", "name": "Alexander S." },
{ "nick": "fabio", "name": "Fabio Pedretti" },
{ "nick": "falsevision", "name": "Mahdi Khodadadifard" },
{ "nick": "fatherbushido", "name": "Nicolas Tisserand" },
{ "nick": "fcxSanya", "name": "Alexander Olkhovskiy" },
{ "nick": "FeXoR", "name": "Florian Finke" },
{ "nick": "Fire Giant", "name": "Malte Schwarzkopf" },
{ "name": "Fork AD" },
{ "nick": "fpre", "name": "Frederick Stallmeyer" },
{ "nick": "Freagarach" },
{ "nick": "freenity", "name": "Anton Galitch" },
{ "nick": "Gallaecio", "name": "Adrián Chaves" },
{ "nick": "gbish (aka Iny)", "name": "Grant Bishop" },
{ "nick": "Gee", "name": "Gustav Larsson" },
{ "nick": "Gentz", "name": "Hal Gentz" },
{ "nick": "gerbilOFdoom" },
{ "nick": "godlikeldh" },
{ "nick": "greybeard", "name": "Joe Cocovich" },
{ "nick": "grillaz" },
{ "nick": "Grugnas", "name": "Giuseppe Tranchese" },
{ "nick": "gudo" },
{ "nick": "Guuts", "name": "Matthew Guttag" },
{ "nick": "h20", "name": "Daniel Wilhelm" },
{ "nick": "Hannibal_Barca", "name": "Clive Juhász S." },
{ "nick": "Haommin" },
{ "nick": "happyconcepts", "name": "Ben Bird" },
{ "nick": "historic_bruno", "name": "Ben Brian" },
{ "nick": "hyiltiz", "name": "Hormet Yiltiz" },
{ "nick": "idanwin" },
{ "nick": "Imarok", "name": "J. S." },
{ "nick": "Inari" },
{ "nick": "infyquest", "name": "Vijay Kiran Kamuju" },
{ "nick": "irishninja", "name": "Brian Broll" },
{ "nick": "IronNerd", "name": "Matthew McMullan" },
{ "nick": "Itms", "name": "Nicolas Auvray" },
{ "nick": "Jaison", "name": "Marco tom Suden" },
{ "nick": "jammus", "name": "James Scott" },
{ "nick": "Jammyjamjamman", "name": "James Sherratt" },
{ "nick": "Janwas", "name": "Jan Wassenberg" },
{ "nick": "javiergodas", "name": "Javier Godas Vieitez" },
{ "nick": "JCWasmx86" },
{ "nick": "Jgwman" },
{ "nick": "JonBaer", "name": "Jon Baer" },
{ "nick": "Josh", "name": "Joshua J. Bakita" },
{ "nick": "joskar", "name": "Johnny Oskarsson" },
{ "nick": "jP_wanN", "name": "Jonas Platte" },
{ "nick": "Jubalbarca", "name": "James Baillie" },
{ "nick": "JubJub", "name": "Sebastian Vetter" },
{ "nick": "jurgemaister" },
{ "nick": "kabzerek", "name": "Grzegorz Kabza" },
{ "nick": "Kai", "name": "Kai Chen" },
{ "name": "Kareem Ergawy" },
{ "nick": "kevmo", "name": "Kevin Caffrey" },
{ "nick": "kezz", "name": "Graeme Kerry" },
{ "nick": "kingadami", "name": "Adam Winsor" },
{ "nick": "kingbasil", "name": "Giannis Fafalios" },
{ "nick": "Krinkle", "name": "Timo Tijhof" },
{ "nick": "Kuba386", "name": "Jakub Kośmicki" },
{ "nick": "lafferjm", "name": "Justin Lafferty" },
{ "nick": "Langbart" },
{ "nick": "LeanderH", "name": "Leander Hemelhof" },
{ "nick": "leper", "name": "Georg Kilzer" },
{ "nick": "Link Mauve", "name": "Emmanuel Gil Peyrot" },
{ "nick": "LittleDev" },
{ "nick": "livingaftermidnight", "name": "Will Dull" },
{ "nick": "lonehawk", "name": "Vignesh Krishnan" },
{ "nick": "Louhike" },
{ "nick": "lsdh" },
{ "nick": "Ludovic", "name": "Ludovic Rousseau" },
{ "nick": "luiko", "name": "Luis Carlos Garcia Barajas" },
{ "nick": "m0l0t0ph", "name": "Christoph Gielisch" },
{ "nick": "madmax", "name": "Abhijit Nandy" },
{ "nick": "madpilot", "name": "Guido Falsi" },
{ "nick": "mammadori", "name": "Marco Amadori" },
{ "nick": "marder", "name": "Stefan R. F." },
{ "nick": "markcho" },
{ "nick": "MarkT", "name": "Mark Thompson" },
{ "nick": "Markus" },
{ "nick": "Mate-86", "name": "Mate Kovacs" },
{ "nick": "Matei", "name": "Matei Zaharia" },
{ "nick": "MatSharrow" },
{ "nick": "MattDoerksen", "name": "Matt Doerksen" },
{ "nick": "mattlott", "name": "Matt Lott" },
{ "nick": "maveric", "name": "Anton Protko" },
{ "nick": "Micnasty", "name": "Travis Gorkin" },
{ "name": "Mikołaj \"Bajter\" Korcz" },
{ "nick": "mimo" },
{ "nick": "mk12", "name": "Mitchell Kember" },
{ "nick": "mmayfield45", "name": "Michael Mayfield" },
{ "nick": "mmoanis", "name": "Mohamed Moanis" },
{ "nick": "Molotov", "name": "Dario Alvarez" },
{ "nick": "mpmoreti", "name": "Marcos Paulo Moreti" },
{ "nick": "mreiland", "name": "Michael Reiland" },
{ "nick": "myconid" },
{ "nick": "n1xc0d3r", "name": "Luis Guerrero" },
{ "nick": "nani", "name": "S. N." },
{ "nick": "nd3c3nt", "name": "Gavin Fowler" },
{ "nick": "nephele" },
{ "nick": "Nescio" },
{ "nick": "niektb", "name": "Niek ten Brinke" },
{ "nick": "nikagra", "name": "Mikita Hradovich" },
{ "nick": "njm" },
{ "nick": "NoMonkey", "name": "John Mena" },
{ "nick": "norsnor" },
{ "nick": "notpete", "name": "Rich Cross" },
{ "nick": "Nullus" },
{ "nick": "nwtour" },
{ "nick": "odoaker", "name": "Ágoston Sipos" },
{ "nick": "Offensive ePeen", "name": "Jared Ryan Bills" },
{ "nick": "Ols", "name": "Oliver Whiteman" },
{ "nick": "olsner", "name": "Simon Brenner" },
{ "nick": "OptimusShepard", "name": "Pirmin Stanglmeier" },
{ "nick": "otero" },
{ "nick": "Palaxin", "name": "David A. Freitag" },
{ "name": "Paul Withers" },
{ "nick": "paulobezerr", "name": "Paulo George Gomes Bezerra" },
{ "nick": "pcpa", "name": "Paulo Andrade" },
{ "nick": "Pendingchaos" },
{ "nick": "PeteVasi", "name": "Pete Vasiliauskas" },
{ "nick": "pilino1234" },
{ "nick": "PingvinBetyar", "name": "Schronk Tamás" },
{ "nick": "plugwash", "name": "Peter Michael Green" },
{ "nick": "Polakrity" },
{ "nick": "Poya", "name": "Poya Manouchehri" },
{ "nick": "prefect", "name": "Nicolai Hähnle" },
{ "nick": "Prodigal Son" },
{ "nick": "pstumpf", "name": "Pascal Stumpf" },
{ "nick": "pyrolink", "name": "Andrew Decker" },
{ "nick": "quantumstate", "name": "Jonathan Waller" },
{ "nick": "QuickShot", "name": "Walter Krawec" },
{ "nick": "quonter" },
{ "nick": "qwertz" },
{ "nick": "Radagast" },
{ "nick": "Raj", "name": "Raj Sharma" },
{ "nick": "ramtzok1", "name": "Ram" },
{ "nick": "rapidelectron", "name": "Christian Weihsbach" },
{ "nick": "r-a-sattarov", "name": "Ramil Sattarov" },
{ "nick": "RedFox", "name": "Jorma Rebane" },
{ "nick": "RefinedCode" },
{ "nick": "Riemer" },
{ "name": "Rolf Sievers" },
{ "nick": "s0600204", "name": "Matthew Norwood" },
{ "nick": "sacha_vrand", "name": "Sacha Vrand" },
{ "nick": "SafaAlfulaij" },
{ "name": "Samuel Guarnieri" },
{ "nick": "Samulis", "name": "Sam Gossner" },
{ "nick": "Sandarac" },
{ "nick": "sanderd17", "name": "Sander Deryckere" },
{ "nick": "sathyam", "name": "Sathyam Vellal" },
{ "nick": "sbirmi", "name": "Sharad Birmiwal" },
{ "nick": "sbte", "name": "Sven Baars" },
{ "nick": "scroogie", "name": "André Gemünd" },
{ "nick": "scythetwirler", "name": "Casey X." },
{ "nick": "sera", "name": "Ralph Sennhauser" },
{ "nick": "serveurix" },
{ "nick": "Shane", "name": "Shane Grant" },
{ "nick": "shh" },
{ "nick": "Silk", "name": "Josh Godsiff" },
{ "nick": "silure" },
{ "nick": "Simikolon", "name": "Yannick & Simon" },
{ "nick": "smiley", "name": "M. L." },
{ "nick": "Spahbod", "name": "Omid Davoodi" },
{ "nick": "Stan", "name": "Stanislas Dolcini" },
{ "nick": "Stefan" },
{ "nick": "StefanBruens", "name": "Stefan Brüns" },
{ "nick": "stilz", "name": "Sławomir Zborowski" },
{ "nick": "stwf", "name": "Steven Fuchs" },
{ "nick": "svott", "name": "Sven Ott" },
{ "nick": "t4nk004" },
{ "nick": "tau" },
{ "nick": "tbm", "name": "Martin Michlmayr" },
{ "nick": "Teiresias" },
{ "nick": "temple" },
{ "nick": "texane" },
{ "nick": "thamlett", "name": "Timothy Hamlett" },
{ "nick": "thedrunkyak", "name": "Dan Fuhr" },
{ "nick": "Tobbi" },
{ "nick": "Toonijn", "name": "Toon Baeyens" },
{ "nick": "TrinityDeath", "name": "Jethro Lu" },
{ "nick": "triumvir", "name": "Corin Schedler" },
{ "nick": "trompetin17", "name": "Juan Guillermo" },
{ "nick": "tpearson", "name": "Timothy Pearson" },
{ "nick": "user1", "name": "A. C." },
{ "nick": "usey11" },
{ "nick": "vincent_c", "name": "Vincent Cheng" },
{ "nick": "vinhig", "name": "Vincent Higginson" },
{ "nick": "vladislavbelov", "name": "Vladislav Belov" },
{ "nick": "voroskoi" },
{ "nick": "vts", "name": "Jeroen DR" },
{ "nick": "wacko", "name": "Andrew Spiering" },
{ "nick": "WhiteTreePaladin", "name": "Brian Ashley" },
{ "nick": "wowgetoffyourcellphone", "name": "Justus Avramenko" },
{ "nick": "wraitii", "name": "Lancelot de Ferrière le Vayer" },
{ "nick": "Xentelian", "name": "Mark Strawson" },
{ "nick": "Xienen", "name": "Dayle Flowers" },
{ "nick": "xone47", "name": "Brent Johnson" },
{ "nick": "xtizer", "name": "Matt Green" },
{ "nick": "yashi", "name": "Yasushi Shoji" },
{ "nick": "Ykkrosh", "name": "Philip Taylor" },
{ "nick": "Yves" },
{ "nick": "z0rg", "name": "Sébastien Maire" },
{ "nick": "Zeusthor", "name": "Jeffrey Tavares" },
{ "nick": "zoot" },
{ "nick": "zsol", "name": "Zsolt Dollenstein" },
{ "nick": "ztamas", "name": "Tamas Zolnai" },
{ "nick": "Zyi", "name": "Charles De Meulenaer" }
]
}
]
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/Formation.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 26519)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Formation.js (revision 26520)
@@ -1,1036 +1,1047 @@
function Formation() {}
Formation.prototype.Schema =
"" +
"" +
""+
"2"+
""+
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
+ "" +
+ "" +
+ "" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
// Distance at which we'll switch between column/box formations.
var g_ColumnDistanceThreshold = 128;
+// Distance under which the formation will not try to turn towards the target position.
+var g_RotateDistanceThreshold = 1;
+
Formation.prototype.variablesToSerialize = [
"lastOrderVariant",
"members",
"memberPositions",
"maxRowsUsed",
"maxColumnsUsed",
"finishedEntities",
"idleEntities",
"columnar",
"rearrange",
"formationMembersWithAura",
"width",
"depth",
"twinFormations",
"formationSeparation",
"offsets"
];
Formation.prototype.Init = function(deserialized = false)
{
+ this.maxTurningAngle = +this.template.MaxTurningAngle;
this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
this.shiftRows = this.template.ShiftRows == "true";
this.separationMultiplier = {
"width": +this.template.UnitSeparationWidthMultiplier,
"depth": +this.template.UnitSeparationDepthMultiplier
};
this.sloppiness = +this.template.Sloppiness;
this.widthDepthRatio = +this.template.WidthDepthRatio;
this.minColumns = +(this.template.MinColumns || 0);
this.maxColumns = +(this.template.MaxColumns || 0);
this.maxRows = +(this.template.MaxRows || 0);
this.centerGap = +(this.template.CenterGap || 0);
if (this.template.AnimationVariants)
{
this.animationvariants = [];
let differentAnimationVariants = this.template.AnimationVariants.split(/\s*;\s*/);
// Loop over the different rectangulars that will map to different animation variants.
for (let rectAnimationVariant of differentAnimationVariants)
{
let rect, replacementAnimationVariant;
[rect, replacementAnimationVariant] = rectAnimationVariant.split(/\s*:\s*/);
let rows, columns;
[rows, columns] = rect.split(/\s*,\s*/);
let minRow, maxRow, minColumn, maxColumn;
[minRow, maxRow] = rows.split(/\s*\.\.\s*/);
[minColumn, maxColumn] = columns.split(/\s*\.\.\s*/);
this.animationvariants.push({
"minRow": +minRow,
"maxRow": +maxRow,
"minColumn": +minColumn,
"maxColumn": +maxColumn,
"name": replacementAnimationVariant
});
}
}
this.lastOrderVariant = undefined;
// Entity IDs currently belonging to this formation.
this.members = [];
this.memberPositions = {};
this.maxRowsUsed = 0;
this.maxColumnsUsed = [];
// Entities that have finished the original task.
this.finishedEntities = new Set();
this.idleEntities = new Set();
// Whether we're travelling in column (vs box) formation.
this.columnar = false;
// Whether we should rearrange all formation members.
this.rearrange = true;
// Members with a formation aura.
this.formationMembersWithAura = [];
this.width = 0;
this.depth = 0;
this.twinFormations = [];
// Distance from which two twin formations will merge into one.
this.formationSeparation = 0;
if (deserialized)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
.SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
};
Formation.prototype.Serialize = function()
{
let result = {};
for (let key of this.variablesToSerialize)
result[key] = this[key];
return result;
};
Formation.prototype.Deserialize = function(data)
{
this.Init(true);
for (let key in data)
this[key] = data[key];
};
/**
* Set the value from which two twin formations will become one.
*/
Formation.prototype.SetFormationSeparation = function(value)
{
this.formationSeparation = value;
};
Formation.prototype.GetSize = function()
{
return { "width": this.width, "depth": this.depth };
};
Formation.prototype.GetSpeedMultiplier = function()
{
return +this.template.SpeedMultiplier;
};
Formation.prototype.GetMemberCount = function()
{
return this.members.length;
};
Formation.prototype.GetMembers = function()
{
return this.members;
};
Formation.prototype.GetClosestMember = function(ent, filter)
{
let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
return INVALID_ENTITY;
let entPosition = cmpEntPosition.GetPosition2D();
let closestMember = INVALID_ENTITY;
let closestDistance = Infinity;
for (let member of this.members)
{
if (filter && !filter(ent))
continue;
let cmpPosition = Engine.QueryInterface(member, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition2D();
let dist = entPosition.distanceToSquared(pos);
if (dist < closestDistance)
{
closestMember = member;
closestDistance = dist;
}
}
return closestMember;
};
/**
* Returns the 'primary' member of this formation (typically the most
* important unit type), for e.g. playing a representative sound.
* Returns undefined if no members.
* TODO: Actually implement something like that. Currently this just returns
* the arbitrary first one.
*/
Formation.prototype.GetPrimaryMember = function()
{
return this.members[0];
};
/**
* Get the formation animation variant for a certain member of this formation.
* @param entity The entity ID to get the animation for.
* @return The name of the animation variant as defined in the template,
* e.g. "testudo_front" or undefined if does not exist.
*/
Formation.prototype.GetFormationAnimationVariant = function(entity)
{
if (!this.animationvariants || !this.animationvariants.length || this.columnar || !this.memberPositions[entity])
return undefined;
let row = this.memberPositions[entity].row;
let column = this.memberPositions[entity].column;
for (let i = 0; i < this.animationvariants.length; ++i)
{
let minRow = this.animationvariants[i].minRow;
if (minRow < 0)
minRow += this.maxRowsUsed + 1;
if (row < minRow)
continue;
let maxRow = this.animationvariants[i].maxRow;
if (maxRow < 0)
maxRow += this.maxRowsUsed + 1;
if (row > maxRow)
continue;
let minColumn = this.animationvariants[i].minColumn;
if (minColumn < 0)
minColumn += this.maxColumnsUsed[row] + 1;
if (column < minColumn)
continue;
let maxColumn = this.animationvariants[i].maxColumn;
if (maxColumn < 0)
maxColumn += this.maxColumnsUsed[row] + 1;
if (column > maxColumn)
continue;
return this.animationvariants[i].name;
}
return undefined;
};
Formation.prototype.SetFinishedEntity = function(ent)
{
// Rotate the entity to the correct angle.
const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
const cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld())
cmpEntPosition.TurnTo(cmpPosition.GetRotation().y);
this.finishedEntities.add(ent);
};
Formation.prototype.UnsetFinishedEntity = function(ent)
{
this.finishedEntities.delete(ent);
};
Formation.prototype.ResetFinishedEntities = function()
{
this.finishedEntities.clear();
};
Formation.prototype.AreAllMembersFinished = function()
{
return this.finishedEntities.size === this.members.length;
};
Formation.prototype.SetIdleEntity = function(ent)
{
this.idleEntities.add(ent);
};
Formation.prototype.UnsetIdleEntity = function(ent)
{
this.idleEntities.delete(ent);
};
Formation.prototype.ResetIdleEntities = function()
{
this.idleEntities.clear();
};
Formation.prototype.AreAllMembersIdle = function()
{
return this.idleEntities.size === this.members.length;
};
/**
* Set whether we are allowed to rearrange formation members.
*/
Formation.prototype.SetRearrange = function(rearrange)
{
this.rearrange = rearrange;
};
/**
* Initialize the members of this formation.
* Must only be called once.
* All members must implement UnitAI.
*/
Formation.prototype.SetMembers = function(ents)
{
this.members = ents;
for (let ent of this.members)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(this.entity);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras && cmpAuras.HasFormationAura())
{
this.formationMembersWithAura.push(ent);
cmpAuras.ApplyFormationAura(ents);
}
}
this.offsets = undefined;
// Locate this formation controller in the middle of its members.
this.MoveToMembersCenter();
// Compute the speed etc. of the formation.
this.ComputeMotionParameters();
};
/**
* Remove the given list of entities.
* The entities must already be members of this formation.
* @param {boolean} rename - Whether the removal was part of an entity rename
(prevents disbanding of the formation when under the member limit).
*/
Formation.prototype.RemoveMembers = function(ents, renamed = false)
{
this.offsets = undefined;
this.members = this.members.filter(ent => ents.indexOf(ent) === -1);
for (let ent of ents)
{
this.finishedEntities.delete(ent);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.UpdateWorkOrders();
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.RemoveFormationAura(ents);
// The unit with the aura is also removed from the formation.
if (ents.indexOf(ent) !== -1)
cmpAuras.RemoveFormationAura(this.members);
}
this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; });
// If there's nobody left, destroy the formation
// unless this is a rename where we can have 0 members temporarily.
if (this.members.length < +this.template.RequiredMemberCount && !renamed)
{
this.Disband();
return;
}
this.ComputeMotionParameters();
if (!this.rearrange)
return;
// Rearrange the remaining members.
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
};
Formation.prototype.AddMembers = function(ents)
{
this.offsets = undefined;
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.ApplyFormationAura(ents);
}
this.members = this.members.concat(ents);
for (let ent of ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(this.entity);
if (!cmpUnitAI.GetOrders().length)
cmpUnitAI.SetNextState("FORMATIONMEMBER.IDLE");
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras && cmpAuras.HasFormationAura())
{
this.formationMembersWithAura.push(ent);
cmpAuras.ApplyFormationAura(this.members);
}
}
this.ComputeMotionParameters();
if (!this.rearrange)
return;
this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
};
/**
* Remove all members and destroy the formation.
*/
Formation.prototype.Disband = function()
{
for (let ent of this.members)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.SetFormationController(INVALID_ENTITY);
}
for (let ent of this.formationMembersWithAura)
{
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
cmpAuras.RemoveFormationAura(this.members);
}
this.members = [];
this.finishedEntities.clear();
this.formationMembersWithAura = [];
this.offsets = undefined;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
// Hack: switch to a clean state to stop timers.
cmpUnitAI.UnitFsm.SwitchToNextState(cmpUnitAI, "");
Engine.DestroyEntity(this.entity);
};
/**
* Set all members to form up into the formation shape.
* @param {boolean} moveCenter - The formation center will be reinitialized
* to the center of the units.
* @param {boolean} force - All individual orders of the formation units are replaced,
* otherwise the order to walk into formation is just pushed to the front.
* @param {string | undefined} variant - Variant to be passed as order parameter.
*/
Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant)
{
if (!this.members.length)
return;
let active = [];
let positions = [];
- let rotations = 0;
for (let ent of this.members)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
active.push(ent);
// Query the 2D position as the exact height calculation isn't needed,
// but bring the position to the correct coordinates.
positions.push(cmpPosition.GetPosition2D());
- rotations += cmpPosition.GetRotation().y;
}
- let avgpos = Vector2D.average(positions);
-
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Reposition the formation if we're told to or if we don't already have a position.
if (moveCenter || (cmpPosition && !cmpPosition.IsInWorld()))
- this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / active.length);
-
- this.lastOrderVariant = variant;
- // Switch between column and box if necessary.
- let cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
- let walkingDistance = cmpFormationUnitAI.ComputeWalkingDistance();
- let columnar = walkingDistance > g_ColumnDistanceThreshold;
- if (columnar != this.columnar)
{
- this.columnar = columnar;
- this.offsets = undefined;
+ const oldRotation = cmpPosition.GetRotation().y;
+ const avgpos = Vector2D.average(positions);
+
+ // Switch between column and box if necessary.
+ const cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
+ const columnar = cmpFormationUnitAI.ComputeWalkingDistance() > g_ColumnDistanceThreshold;
+ if (columnar != this.columnar)
+ {
+ this.columnar = columnar;
+ this.offsets = undefined;
+ }
+
+ let newRotation = oldRotation;
+ const targetPosition = cmpFormationUnitAI.GetTargetPositions()[0];
+ if (targetPosition !== undefined && avgpos.distanceToSquared(targetPosition) > g_RotateDistanceThreshold)
+ newRotation = avgpos.angleTo(targetPosition);
+
+ cmpPosition.TurnTo(newRotation);
+ if (!this.areAnglesSimilar(newRotation, oldRotation))
+ this.offsets = undefined;
}
+ this.lastOrderVariant = variant;
+
let offsetsChanged = false;
- let newOrientation = this.GetEstimatedOrientation(avgpos);
if (!this.offsets)
{
this.offsets = this.ComputeFormationOffsets(active, positions);
offsetsChanged = true;
}
let xMax = 0;
let yMax = 0;
let xMin = 0;
let yMin = 0;
if (force)
// Reset finishedEntities as FormationWalk is called.
this.ResetFinishedEntities();
for (let i = 0; i < this.offsets.length; ++i)
{
let offset = this.offsets[i];
let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
if (!cmpUnitAI)
{
warn("Entities without UnitAI in formation are not supported.");
continue;
}
let data =
{
"target": this.entity,
"x": offset.x,
"z": offset.y,
"offsetsChanged": offsetsChanged,
"variant": variant
};
cmpUnitAI.AddOrder("FormationWalk", data, !force);
xMax = Math.max(xMax, offset.x);
yMax = Math.max(yMax, offset.y);
xMin = Math.min(xMin, offset.x);
yMin = Math.min(yMin, offset.y);
}
this.width = xMax - xMin;
this.depth = yMax - yMin;
};
Formation.prototype.MoveToMembersCenter = function()
{
let positions = [];
let rotations = 0;
for (let ent of this.members)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
positions.push(cmpPosition.GetPosition2D());
rotations += cmpPosition.GetRotation().y;
}
let avgpos = Vector2D.average(positions);
this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / positions.length);
};
/**
* Set formation position.
* If formation is not in world at time this is called, set new rotation and flag for range manager.
*/
Formation.prototype.SetupPositionAndHandleRotation = function(x, y, rot)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return;
let wasInWorld = cmpPosition.IsInWorld();
cmpPosition.JumpTo(x, y);
if (wasInWorld)
return;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetEntityFlag(this.entity, "normal", false);
cmpPosition.TurnTo(rot);
};
Formation.prototype.GetAvgFootprint = function(active)
{
let footprints = [];
for (let ent of active)
{
let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
if (cmpFootprint)
footprints.push(cmpFootprint.GetShape());
}
if (!footprints.length)
return { "width": 1, "depth": 1 };
let r = { "width": 0, "depth": 0 };
for (let shape of footprints)
{
if (shape.type == "circle")
{
r.width += shape.radius * 2;
r.depth += shape.radius * 2;
}
else if (shape.type == "square")
{
r.width += shape.width;
r.depth += shape.depth;
}
}
r.width /= footprints.length;
r.depth /= footprints.length;
return r;
};
Formation.prototype.ComputeFormationOffsets = function(active, positions)
{
let separation = this.GetAvgFootprint(active);
separation.width *= this.separationMultiplier.width;
separation.depth *= this.separationMultiplier.depth;
let sortingClasses;
if (this.columnar)
sortingClasses = ["Cavalry", "Infantry"];
else
sortingClasses = this.sortingClasses.slice();
sortingClasses.push("Unknown");
// The entities will be assigned to positions in the formation in
// the same order as the types list is ordered.
let types = {};
for (let i = 0; i < sortingClasses.length; ++i)
types[sortingClasses[i]] = [];
for (let i in active)
{
let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
let classes = cmpIdentity.GetClassesList();
let done = false;
for (let c = 0; c < sortingClasses.length; ++c)
{
if (classes.indexOf(sortingClasses[c]) > -1)
{
types[sortingClasses[c]].push({ "ent": active[i], "pos": positions[i] });
done = true;
break;
}
}
if (!done)
types.Unknown.push({ "ent": active[i], "pos": positions[i] });
}
let count = active.length;
let shape = this.template.FormationShape;
let shiftRows = this.shiftRows;
let centerGap = this.centerGap;
let sortingOrder = this.template.SortingOrder;
let offsets = [];
// Choose a sensible size/shape for the various formations, depending on number of units.
let cols;
if (this.columnar)
{
shape = "square";
cols = Math.min(count, 3);
shiftRows = false;
centerGap = 0;
sortingOrder = null;
}
else
{
let depth = Math.sqrt(count / this.widthDepthRatio);
if (this.maxRows && depth > this.maxRows)
depth = this.maxRows;
cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0));
if (cols < this.minColumns)
cols = Math.min(count, this.minColumns);
if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth)
cols = this.maxColumns;
}
// Define special formations here.
if (this.template.FormationShape == "special" && Engine.QueryInterface(this.entity, IID_Identity).GetGenericName() == "Scatter")
{
let width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5;
for (let i = 0; i < count; ++i)
{
let obj = new Vector2D(randFloat(0, width), randFloat(0, width));
obj.row = 1;
obj.column = i + 1;
offsets.push(obj);
}
}
// For non-special formations, calculate the positions based on the number of entities.
this.maxColumnsUsed = [];
this.maxRowsUsed = 0;
if (shape != "special")
{
offsets = [];
let r = 0;
let left = count;
// While there are units left, start a new row in the formation.
while (left > 0)
{
// Save the position of the row.
let z = -r * separation.depth;
// Alternate between the left and right side of the center to have a symmetrical distribution.
let side = 1;
let n;
// Determine the number of entities in this row of the formation.
if (shape == "square")
{
n = cols;
if (shiftRows)
n -= r % 2;
}
else if (shape == "triangle")
{
if (shiftRows)
n = r + 1;
else
n = r * 2 + 1;
}
if (!shiftRows && n > left)
n = left;
for (let c = 0; c < n && left > 0; ++c)
{
// Switch sides for the next entity.
side *= -1;
let x;
if (n % 2 == 0)
x = side * (Math.floor(c / 2) + 0.5) * separation.width;
else
x = side * Math.ceil(c / 2) * separation.width;
if (centerGap)
{
// Don't use the center position with a center gap.
if (x == 0)
continue;
x += side * centerGap / 2;
}
let column = Math.ceil(n / 2) + Math.ceil(c / 2) * side;
let r1 = randFloat(-1, 1) * this.sloppiness;
let r2 = randFloat(-1, 1) * this.sloppiness;
offsets.push(new Vector2D(x + r1, z + r2));
offsets[offsets.length - 1].row = r + 1;
offsets[offsets.length - 1].column = column;
left--;
}
++r;
this.maxColumnsUsed[r] = n;
}
this.maxRowsUsed = r;
}
// Make sure the average offset is zero, as the formation is centered around that
// calculating offset distances without a zero average makes no sense, as the formation
// will jump to a different position any time.
let avgoffset = Vector2D.average(offsets);
offsets.forEach(function(o) {o.sub(avgoffset);});
// Sort the available places in certain ways.
// The places first in the list will contain the heaviest units as defined by the order
// of the types list.
if (sortingOrder == "fillFromTheSides")
offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);});
else if (sortingOrder == "fillToTheCenter")
offsets.sort(function(o1, o2) {
return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y));
});
// Query the 2D position of the formation.
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- let formationPos = cmpPosition.GetPosition2D();
+ const realPositions = this.GetRealOffsetPositions(offsets);
// Use realistic place assignment,
// every soldier searches the closest available place in the formation.
let newOffsets = [];
- let realPositions = this.GetRealOffsetPositions(offsets, formationPos);
- for (let i = sortingClasses.length; i; --i)
+ for (const i of sortingClasses.reverse())
{
- let t = types[sortingClasses[i - 1]];
+ const t = types[i];
if (!t.length)
continue;
let usedOffsets = offsets.splice(-t.length);
let usedRealPositions = realPositions.splice(-t.length);
for (let entPos of t)
{
let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets);
usedRealPositions.splice(closestOffsetId, 1);
newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]);
newOffsets[newOffsets.length - 1].ent = entPos.ent;
}
}
return newOffsets;
};
/**
* Search the closest position in the realPositions list to the given entity.
* @param entPos - Object with entity position and entity ID.
* @param realPositions - The world coordinates of the available offsets.
* @param offsets
* @return The index of the closest offset position.
*/
Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets)
{
let pos = entPos.pos;
let closestOffsetId = -1;
let offsetDistanceSq = Infinity;
for (let i = 0; i < realPositions.length; ++i)
{
let distSq = pos.distanceToSquared(realPositions[i]);
if (distSq < offsetDistanceSq)
{
offsetDistanceSq = distSq;
closestOffsetId = i;
}
}
this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column };
return closestOffsetId;
};
/**
* Get the world positions for a list of offsets in this formation.
*/
-Formation.prototype.GetRealOffsetPositions = function(offsets, pos)
+Formation.prototype.GetRealOffsetPositions = function(offsets)
{
+ const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ const pos = cmpPosition.GetPosition2D();
+ const rot = cmpPosition.GetRotation().y;
+ const sin = Math.sin(rot);
+ const cos = Math.cos(rot);
let offsetPositions = [];
- let { sin, cos } = this.GetEstimatedOrientation(pos);
// Calculate the world positions.
for (let o of offsets)
offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin));
return offsetPositions;
};
/**
- * Calculate the estimated rotation of the formation based on the current rotation.
- * Return the sine and cosine of the angle.
+ * Returns true if the two given angles (in radians)
+ * are smaller than the maximum turning angle of the formation and therfore allow
+ * the formation turn without reassigning positions.
*/
-Formation.prototype.GetEstimatedOrientation = function(pos)
+
+Formation.prototype.areAnglesSimilar = function(a1, a2)
{
- let r = {};
- let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
- if (!cmpPosition)
- return r;
- let rot = cmpPosition.GetRotation().y;
- r.sin = Math.sin(rot);
- r.cos = Math.cos(rot);
- return r;
+ const d = Math.abs(a1 - a2) % 2 * Math.PI;
+ return d < this.maxTurningAngle || d > 2 * Math.PI - this.maxTurningAngle;
};
/**
* Set formation controller's speed based on its current members.
*/
Formation.prototype.ComputeMotionParameters = function()
{
- let maxRadius = 0;
let minSpeed = Infinity;
let minAcceleration = Infinity;
for (let ent of this.members)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
{
minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
minAcceleration = Math.min(minAcceleration, cmpUnitMotion.GetAcceleration());
}
}
minSpeed *= this.GetSpeedMultiplier();
let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed());
cmpUnitMotion.SetAcceleration(minAcceleration);
};
Formation.prototype.ShapeUpdate = function()
{
if (!this.rearrange)
return;
// Check the distance to twin formations, and merge if
// the formations could collide.
for (let i = this.twinFormations.length - 1; i >= 0; --i)
{
// Only do the check on one side.
if (this.twinFormations[i] <= this.entity)
continue;
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position);
let cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation);
if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation ||
!cmpPosition.IsInWorld() || !cmpOtherPosition.IsInWorld())
continue;
let thisPosition = cmpPosition.GetPosition2D();
let otherPosition = cmpOtherPosition.GetPosition2D();
let dx = thisPosition.x - otherPosition.x;
let dy = thisPosition.y - otherPosition.y;
let dist = Math.sqrt(dx * dx + dy * dy);
let thisSize = this.GetSize();
let otherSize = cmpOtherFormation.GetSize();
let minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) +
Math.max(otherSize.width / 2, otherSize.depth / 2) +
this.formationSeparation;
if (minDist < dist)
continue;
// Merge the members from the twin formation into this one
// twin formations should always have exactly the same orders.
let otherMembers = cmpOtherFormation.members;
cmpOtherFormation.RemoveMembers(otherMembers);
this.AddMembers(otherMembers);
Engine.DestroyEntity(this.twinFormations[i]);
this.twinFormations.splice(i, 1);
}
// Switch between column and box if necessary.
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
let walkingDistance = cmpUnitAI.ComputeWalkingDistance();
let columnar = walkingDistance > g_ColumnDistanceThreshold;
if (columnar != this.columnar)
{
this.offsets = undefined;
this.columnar = columnar;
// Disable moveCenter so we can't get stuck in a loop of switching
// shape causing center to change causing shape to switch back.
this.MoveMembersIntoFormation(false, true, this.lastOrderVariant);
}
};
Formation.prototype.ResetOrderVariant = function()
{
this.lastOrderVariant = undefined;
};
Formation.prototype.OnGlobalOwnershipChanged = function(msg)
{
// When an entity is captured or destroyed, it should no longer be
// controlled by this formation.
if (this.members.indexOf(msg.entity) != -1)
this.RemoveMembers([msg.entity]);
if (msg.entity === this.entity && msg.to !== INVALID_PLAYER)
Engine.QueryInterface(this.entity, IID_Visual)?.SetVariant("animationVariant", QueryPlayerIDInterface(msg.to, IID_Identity).GetCiv());
};
Formation.prototype.OnGlobalEntityRenamed = function(msg)
{
if (this.members.indexOf(msg.entity) === -1)
return;
if (this.finishedEntities.delete(msg.entity))
this.finishedEntities.add(msg.newentity);
// Save rearranging to temporarily set it to false.
let temp = this.rearrange;
this.rearrange = false;
// First remove the old member to be able to reuse its position.
this.RemoveMembers([msg.entity], true);
this.AddMembers([msg.newentity]);
this.memberPositions[msg.newentity] = this.memberPositions[msg.entity];
this.rearrange = temp;
};
Formation.prototype.RegisterTwinFormation = function(entity)
{
let cmpFormation = Engine.QueryInterface(entity, IID_Formation);
if (!cmpFormation)
return;
this.twinFormations.push(entity);
cmpFormation.twinFormations.push(this.entity);
};
Formation.prototype.DeleteTwinFormations = function()
{
for (let ent of this.twinFormations)
{
let cmpFormation = Engine.QueryInterface(ent, IID_Formation);
if (cmpFormation)
cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1);
}
this.twinFormations = [];
};
Formation.prototype.LoadFormation = function(newTemplate)
{
const newFormation = ChangeEntityTemplate(this.entity, newTemplate);
return Engine.QueryInterface(newFormation, IID_UnitAI);
};
Formation.prototype.OnEntityRenamed = function(msg)
{
const members = clone(this.members);
this.Disband();
Engine.QueryInterface(msg.newentity, IID_Formation).SetMembers(members);
};
Engine.RegisterComponentType(IID_Formation, "Formation", Formation);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 26519)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js (revision 26520)
@@ -1,532 +1,534 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Position.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Turretable.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Formation.js");
Engine.LoadComponentScript("UnitAI.js");
/**
* Fairly straightforward test that entity renaming is handled
* by unitAI states. These ought to be augmented with integration tests, ideally.
*/
function TestTargetEntityRenaming(init_state, post_state, setup)
{
ResetState();
const player_ent = 5;
const target_ent = 6;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => {},
"SetTimeout": () => {}
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => false
});
let unitAI = ConstructComponent(player_ent, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive",
"FleeDistance": 10
});
unitAI.OnCreate();
setup(unitAI, player_ent, target_ent);
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state);
unitAI.OnGlobalEntityRenamed({
"entity": target_ent,
"newentity": target_ent + 1
});
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state);
}
TestTargetEntityRenaming(
"INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE",
(unitAI, player_ent, target_ent) => {
unitAI.CanGarrison = (target) => target == target_ent;
unitAI.MoveToTargetRange = (target) => target == target_ent;
unitAI.AbleToMove = () => true;
unitAI.Garrison(target_ent, false);
}
);
TestTargetEntityRenaming(
"INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.REPAIR.REPAIRING",
(unitAI, player_ent, target_ent) => {
AddMock(player_ent, IID_Builder, {
"StartRepairing": () => true,
"StopRepairing": () => {}
});
QueryBuilderListInterface = () => {};
unitAI.CheckTargetRange = () => true;
unitAI.CanRepair = (target) => target == target_ent;
unitAI.Repair(target_ent, false, false);
}
);
TestTargetEntityRenaming(
"INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING",
(unitAI, player_ent, target_ent) => {
PositionHelper.DistanceBetweenEntities = () => 10;
unitAI.CheckTargetRangeExplicit = () => false;
AddMock(player_ent, IID_UnitMotion, {
"MoveToTargetRange": () => true,
"GetRunMultiplier": () => 1,
"SetSpeedMultiplier": () => {},
"GetAcceleration": () => 1,
"StopMoving": () => {}
});
unitAI.Flee(target_ent, false);
}
);
/* Regression test.
* Tests the FSM behaviour of a unit when walking as part of a formation,
* then exiting the formation.
* mode == 0: There is no enemy unit nearby.
* mode == 1: There is a live enemy unit nearby.
* mode == 2: There is a dead enemy unit nearby.
*/
function TestFormationExiting(mode)
{
ResetState();
var playerEntity = 5;
var unit = 10;
var enemy = 20;
var controller = 30;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": function() { },
"SetTimeout": function() { },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) {
return 1;
},
"EnableActiveQuery": function(id) { },
"ResetActiveQuery": function(id) { if (mode == 0) return []; return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": function(id) { return playerEntity; },
"GetNumPlayers": function() { return 2; },
});
AddMock(playerEntity, IID_Player, {
"IsAlly": function() { return false; },
"IsEnemy": function() { return true; },
"GetEnemies": function() { return [2]; },
});
var unitAI = ConstructComponent(unit, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
AddMock(unit, IID_Identity, {
"GetClassesList": function() { return []; },
});
AddMock(unit, IID_Ownership, {
"GetOwner": function() { return 1; },
});
AddMock(unit, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(); },
"GetPosition2D": function() { return new Vector2D(); },
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
});
AddMock(unit, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"GetAcceleration": () => 1,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
"SetMemberOfFormation": () => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
AddMock(unit, IID_Vision, {
"GetRange": function() { return 10; },
});
AddMock(unit, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
"GetBestAttackAgainst": function(t) { return "melee"; },
"GetPreference": function(t) { return 0; },
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
"CanAttack": function(v) { return true; },
"CompareEntitiesByPreference": function(a, b) { return 0; },
"IsTargetInRange": () => true,
"StartAttacking": () => true
});
unitAI.OnCreate();
unitAI.SetupAttackRangeQuery(1);
if (mode == 1)
{
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 10; },
});
AddMock(enemy, IID_UnitAI, {
"IsAnimal": () => "false",
"IsDangerousAnimal": () => "false"
});
}
else if (mode == 2)
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 0; },
});
let controllerFormation = ConstructComponent(controller, "Formation", {
"FormationShape": "square",
"ShiftRows": "false",
"SortingClasses": "",
"WidthDepthRatio": 1,
"UnitSeparationWidthMultiplier": 1,
"UnitSeparationDepthMultiplier": 1,
"SpeedMultiplier": 1,
"Sloppiness": 0
});
let controllerAI = ConstructComponent(controller, "UnitAI", {
"FormationController": "true",
"DefaultStance": "aggressive"
});
AddMock(controller, IID_Position, {
"JumpTo": function(x, z) { this.x = x; this.z = z; },
+ "TurnTo": function() {},
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function() { return new Vector2D(this.x, this.z); },
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
"MoveOutOfWorld": () => {}
});
AddMock(controller, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"StopMoving": () => {},
"SetSpeedMultiplier": () => {},
"SetAcceleration": (accel) => {},
"MoveToPointRange": () => true,
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
controllerAI.OnCreate();
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.IDLE");
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
controllerFormation.SetMembers([unit]);
controllerAI.Walk(100, 100, false);
TS_ASSERT_EQUALS(controllerAI.fsmStateName, "FORMATIONCONTROLLER.WALKING");
TS_ASSERT_EQUALS(unitAI.fsmStateName, "FORMATIONMEMBER.WALKING");
controllerFormation.Disband();
unitAI.UnitFsm.ProcessMessage(unitAI, { "type": "Timer" });
if (mode == 0)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
else if (mode == 1)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
else if (mode == 2)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.IDLE");
else
TS_FAIL("invalid mode");
}
function TestMoveIntoFormationWhileAttacking()
{
ResetState();
var playerEntity = 5;
var controller = 10;
var enemy = 20;
var unit = 30;
var units = [];
var unitCount = 8;
var unitAIs = [];
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": function() { },
"SetTimeout": function() { },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"CreateActiveQuery": function(ent, minRange, maxRange, players, iid, flags, accountForSize) {
return 1;
},
"EnableActiveQuery": function(id) { },
"ResetActiveQuery": function(id) { return [enemy]; },
"DisableActiveQuery": function(id) { },
"GetEntityFlagMask": function(identifier) { },
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "special/formations/line_closed"; },
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": function(id) { return playerEntity; },
"GetNumPlayers": function() { return 2; },
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": (ent, target, min, max) => true
});
AddMock(playerEntity, IID_Player, {
"IsAlly": function() { return false; },
"IsEnemy": function() { return true; },
"GetEnemies": function() { return [2]; },
});
// create units
for (var i = 0; i < unitCount; i++)
{
units.push(unit + i);
var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
AddMock(unit + i, IID_Identity, {
"GetClassesList": function() { return []; },
});
AddMock(unit + i, IID_Ownership, {
"GetOwner": function() { return 1; },
});
AddMock(unit + i, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() { return new Vector3D(); },
"GetPosition2D": function() { return new Vector2D(); },
"GetRotation": function() { return { "y": 0 }; },
"IsInWorld": function() { return true; },
});
AddMock(unit + i, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"GetAcceleration": () => 1,
"MoveToFormationOffset": (target, x, z) => {},
"MoveToTargetRange": (target, min, max) => true,
"SetMemberOfFormation": () => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
AddMock(unit + i, IID_Vision, {
"GetRange": function() { return 10; },
});
AddMock(unit + i, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"GetFullAttackRange": function() { return { "max": 40, "min": 0 }; },
"GetBestAttackAgainst": function(t) { return "melee"; },
"GetTimers": function() { return { "prepare": 500, "repeat": 1000 }; },
"CanAttack": function(v) { return true; },
"CompareEntitiesByPreference": function(a, b) { return 0; },
"IsTargetInRange": () => true,
"StartAttacking": () => true,
"StopAttacking": () => {}
});
unitAI.OnCreate();
unitAI.SetupAttackRangeQuery(1);
unitAIs.push(unitAI);
}
// create enemy
AddMock(enemy, IID_Health, {
"GetHitpoints": function() { return 40; },
});
let controllerFormation = ConstructComponent(controller, "Formation", {
"FormationShape": "square",
"ShiftRows": "false",
"SortingClasses": "",
"WidthDepthRatio": 1,
"UnitSeparationWidthMultiplier": 1,
"UnitSeparationDepthMultiplier": 1,
"SpeedMultiplier": 1,
"Sloppiness": 0
});
let controllerAI = ConstructComponent(controller, "UnitAI", {
"FormationController": "true",
"DefaultStance": "aggressive"
});
AddMock(controller, IID_Position, {
"GetTurretParent": () => INVALID_ENTITY,
"JumpTo": function(x, z) { this.x = x; this.z = z; },
+ "TurnTo": function() {},
"GetPosition": function(){ return new Vector3D(this.x, 0, this.z); },
"GetPosition2D": function(){ return new Vector2D(this.x, this.z); },
"GetRotation": () => ({ "y": 0 }),
"IsInWorld": () => true,
"MoveOutOfWorld": () => {},
});
AddMock(controller, IID_UnitMotion, {
"GetWalkSpeed": () => 1,
"SetSpeedMultiplier": (speed) => {},
"SetAcceleration": (accel) => {},
"MoveToPointRange": (x, z, minRange, maxRange) => {},
"StopMoving": () => {},
"SetFacePointAfterMove": () => {},
"GetFacePointAfterMove": () => true,
"GetPassabilityClassName": () => "default"
});
AddMock(controller, IID_Attack, {
"GetRange": function() { return { "max": 10, "min": 0 }; },
"CanAttackAsFormation": function() { return false; },
});
controllerAI.OnCreate();
controllerFormation.SetMembers(units);
controllerAI.Attack(enemy, []);
for (let ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
controllerAI.MoveIntoFormation({ "name": "Circle" });
// let all units be in position
for (let ent of unitAIs)
controllerFormation.SetFinishedEntity(ent);
for (let ent of unitAIs)
TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
controllerFormation.Disband();
}
TestFormationExiting(0);
TestFormationExiting(1);
TestFormationExiting(2);
TestMoveIntoFormationWhileAttacking();
function TestWalkAndFightTargets()
{
const ent = 10;
let unitAI = ConstructComponent(ent, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive",
"FleeDistance": 10
});
unitAI.OnCreate();
unitAI.losAttackRangeQuery = true;
// The result is stored here
let result;
unitAI.PushOrderFront = function(type, order)
{
if (type === "Attack" && order?.target)
result = order.target;
};
// Create some targets.
AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false });
AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 });
AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 });
unitAI.CanAttack = function(target)
{
return target !== ent+2 && target !== ent+7;
};
AddMock(ent, IID_Attack, {
"GetPreference": (target) => ({
[ent+4]: 0,
[ent+5]: 1,
[ent+6]: 2,
[ent+7]: 0
}?.[target])
});
let runTest = function(ents, res)
{
result = undefined;
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ResetActiveQuery": () => ents
});
TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res);
TS_ASSERT_EQUALS(result, res);
};
// No entities.
runTest([]);
// Entities that cannot be attacked.
runTest([ent+1, ent+2, ent+7]);
// No preference, one attackable entity.
runTest([ent+1, ent+2, ent+3], ent+3);
// Check preferences.
runTest([ent+1, ent+2, ent+3, ent+4], ent+4);
runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4);
runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4);
runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4);
runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5);
runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6);
runTest([ent+1, ent+2, ent+7, ent+3], ent+3);
}
TestWalkAndFightTargets();
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_formation.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_formation.xml (revision 26519)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_formation.xml (revision 26520)
@@ -1,78 +1,79 @@
2
Requires at least 2 Soldiers or Siege Engines.
1
square
+ 0.785
Hero Champion Cavalry Melee Ranged
false
1
1
1
0
false
true
true
false
false
false
false
false
false
false
0
upright
false
0
10
0.75
aggressive
true
12.0
true
true
2
true
1
100
0.1
100
large
100
props/units/standards/formation.xml
true
false
false